diff --git a/modules/blox/blox/logos/client.jsx b/modules/blox/blox/logos/client.jsx new file mode 100644 index 00000000..80fe3b13 --- /dev/null +++ b/modules/blox/blox/logos/client.jsx @@ -0,0 +1,15 @@ +import {render} from "preact"; +import {LogosBlock} from "./component.jsx"; + +function renderLogosBlocks() { + const blocks = document.querySelectorAll('[data-block-type="logos"]'); + blocks.forEach((block) => { + const propsData = block.dataset.props; + if (propsData) { + const props = JSON.parse(propsData); + render(, block); + } + }); +} + +renderLogosBlocks(); diff --git a/modules/blox/blox/logos/component.jsx b/modules/blox/blox/logos/component.jsx new file mode 100644 index 00000000..f04da436 --- /dev/null +++ b/modules/blox/blox/logos/component.jsx @@ -0,0 +1,207 @@ +import {Icon} from "../../shared/components/Icon.jsx"; + +const MARQUEE_CSS = ` +@keyframes hbx-logos-marquee { + from { transform: translateX(0); } + to { transform: translateX(-50%); } +} +.hbx-logos-track { + animation: hbx-logos-marquee var(--hbx-marquee-dur, 30s) linear infinite; + will-change: transform; +} +.hbx-logos-track:hover { animation-play-state: paused; } +`; + +// Slot widths keep every logo at a consistent visual footprint regardless of SVG aspect ratio +const SLOT_W = {sm: "w-24", md: "w-32", lg: "w-40"}; +// Max-width caps very wide SVGs inside their slot (height × ~2.5) +const MAX_W = {sm: "3.5rem", md: "5rem", lg: "7rem"}; +// Heights passed as inline style so Icon.jsx's auto-sizing fires correctly +const HEIGHT = {sm: "1.75rem", md: "2.5rem", lg: "3.5rem"}; + +// Full-string Tailwind filter classes — no dynamic concatenation +const FILTER = { + grayscale: "grayscale opacity-60 hover:grayscale-0 hover:opacity-100 transition-all duration-300", + white: "brightness-0 invert transition-all duration-300", + color: "transition-all duration-300", +}; + +function renderText(text) { + if (!text) return ""; + return String(text) + .replace(/\*\*(.*?)\*\*/g, "$1") + .replace(/\*(.*?)\*/g, "$1"); +} + +function LogoItem({item, idx, logoStyle, logoSize, icon_svgs, item_images, padY = "py-3"}) { + const iconSvg = item.icon ? (icon_svgs?.[item.icon] ?? null) : null; + const imgData = item_images?.[String(idx)] ?? null; + const height = HEIGHT[logoSize] ?? HEIGHT.md; + const maxW = MAX_W[logoSize] ?? MAX_W.md; + const slotW = SLOT_W[logoSize] ?? SLOT_W.md; + + // Per-item overrides: style and scale + const itemStyle = item.style ?? logoStyle; + const filter = FILTER[itemStyle] ?? FILTER.grayscale; + // scale: compensates for SVG intrinsic whitespace or atypical artwork areas. + // e.g. scale: 1.3 enlarges a logo whose SVG has excessive padding baked into its viewBox. + const scale = typeof item.scale === "number" ? item.scale : 1; + const scaleStyle = scale !== 1 ? `;transform:scale(${scale})` : ""; + + let visual; + if (iconSvg) { + visual = ( + + ); + } else if (imgData) { + visual = ( + {item.name + ); + } else if (item.name) { + visual = {item.name}; + } else { + return null; + } + + const isExt = item.url && (item.url.startsWith("http://") || item.url.startsWith("https://")); + // Fixed-width slot: every logo occupies the same footprint → consistent visual weight + const cls = `flex items-center justify-center flex-shrink-0 ${slotW} ${padY}`; + + return item.url ? ( + + {visual} + + ) : ( +
{visual}
+ ); +} + +function LogoRow({items, logoStyle, logoSize, icon_svgs, item_images}) { + return ( +
+ {items.map((item, i) => ( + + ))} +
+ ); +} + +function LogoGrid({items, logoStyle, logoSize, icon_svgs, item_images}) { + const cols = + items.length <= 3 ? "grid-cols-3" : + items.length <= 4 ? "grid-cols-2 sm:grid-cols-4" : + items.length <= 6 ? "grid-cols-3 sm:grid-cols-6" : + "grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6"; + return ( +
+ {items.map((item, i) => ( + + ))} +
+ ); +} + +function LogoMarquee({items, logoStyle, logoSize, icon_svgs, item_images, speed}) { + const doubled = [...items, ...items]; + return ( + <> + + {/* + mask-image fades edges without depending on the section background colour — + works on any bg (white, gray-50, dark, gradient, etc.) + */} +
+
+ {doubled.map((item, i) => ( + + ))} +
+
+ + ); +} + +export const LogosBlock = ({content = {}, design = {}, icon_svgs = {}, item_images = {}}) => { + const rawItems = Array.isArray(content.items) ? content.items + : Array.isArray(content.logos) ? content.logos + : []; + + const {title, subtitle, cta} = content; + const layout = design.layout || design.display_mode || "row"; + const logoStyle = design.logo_style || "grayscale"; + const logoSize = design.logo_size || "md"; + const speed = design.marquee_speed ?? 30; + + const isExtCta = cta?.url && (cta.url.startsWith("http://") || cta.url.startsWith("https://")); + + return ( +
+
+ {(title || subtitle) && ( +
+ {title && ( +

+ )} + {subtitle && ( +

+ )} +

+ )} + + {layout === "marquee" && ( + + )} + {layout === "grid" && ( + + )} + {layout !== "marquee" && layout !== "grid" && ( + + )} + + {cta?.text && cta?.url && ( + + )} +
+
+ ); +}; diff --git a/modules/blox/blox/logos/manifest.json b/modules/blox/blox/logos/manifest.json index 87213855..8b530d75 100644 --- a/modules/blox/blox/logos/manifest.json +++ b/modules/blox/blox/logos/manifest.json @@ -1,13 +1,15 @@ { "id": "logos", "name": "Logos", - "version": "1.0.0", + "version": "2.0.0", "license": "MIT", - "category": "content", - "tags": ["logos", "partners", "sponsors", "collaborators", "brands", "clients", "affiliations"], - "description": "Display partner, sponsor, or collaborator logos with interactive effects and multiple display modes", + "category": "marketing", + "intent": ["inform"], + "verticals": ["startup", "saas", "agency", "academic-cv", "lab", "dev-portfolio"], + "tags": ["logos", "social-proof", "partners", "sponsors", "clients", "brands", "affiliations", "marquee"], + "description": "Social-proof logo row: display partner, client, or sponsor logos via the brands icon pack (or image files). Supports row, grid, and infinite-marquee layouts with grayscale/color/white filter styles.", "author": "Hugo Blox", "homepage": "https://hugoblox.com/blocks/", "repository": "https://github.com/HugoBlox/kit", - "keywords": ["hugo", "static-site", "logos", "partners", "sponsors", "carousel", "marquee"] + "keywords": ["hugo", "static-site", "logos", "social-proof", "brands", "marquee"] } diff --git a/templates/startup-landing-page/content/_index.md b/templates/startup-landing-page/content/_index.md index 0de88726..4beb5c9a 100644 --- a/templates/startup-landing-page/content/_index.md +++ b/templates/startup-landing-page/content/_index.md @@ -36,6 +36,35 @@ sections: size: cover position: center parallax: false + - block: logos + content: + title: Trusted by teams at + items: + - icon: brands/github + name: GitHub + - icon: brands/google + name: Google + - icon: brands/microsoft + name: Microsoft + - icon: brands/nvidia + name: NVIDIA + - icon: brands/openai + name: OpenAI + - icon: brands/anthropic + name: Anthropic + - icon: brands/stripe + name: Stripe + - icon: brands/vercel + name: Vercel + design: + layout: marquee + logo_style: grayscale + logo_size: md + marquee_speed: 35 + css_class: "bg-gray-50 dark:bg-gray-900" + spacing: + padding: ["2rem", 0, "2rem", 0] + - block: stats content: items: