diff --git a/modules/blox/blox/hero/component.jsx b/modules/blox/blox/hero/component.jsx index b8d0bc4a..bf8eecad 100644 --- a/modules/blox/blox/hero/component.jsx +++ b/modules/blox/blox/hero/component.jsx @@ -8,72 +8,158 @@ import {Icon} from "../../shared/components/Icon.jsx"; // Simple markdown renderer export function renderText(text) { if (!text) return ""; - return text + return String(text) .replace(/\*\*(.*?)\*\*/g, "$1") .replace(/\*(.*?)\*/g, "$1") .replace(/`(.*?)`/g, "$1"); } +// Title renderer: supports [highlight] syntax for gradient-text spans (in addition to markdown). +// `[word]` becomes a primary→secondary gradient span. Negative lookahead `(?!\()` avoids +// clobbering markdown link syntax `[text](url)`. +const HIGHLIGHT_CLS = + "bg-gradient-to-r from-primary-500 via-secondary-500 to-primary-500 dark:from-primary-400 dark:via-secondary-400 dark:to-primary-400 bg-clip-text text-transparent"; + +export function renderTitle(text) { + if (!text) return ""; + return String(text) + .replace(/\[([^\]]+)\](?!\()/g, `$1`) + .replace(/\*\*(.*?)\*\*/g, "$1") + .replace(/\*(.*?)\*/g, "$1"); +} + // Process URLs export function processUrl(url) { if (!url) return {href: "#"}; - if (url.startsWith("http://") || url.startsWith("https://")) { - return { - href: url, - target: "_blank", - rel: "noopener", - }; + return {href: url, target: "_blank", rel: "noopener"}; } - - if (url.startsWith("#")) { - return {href: url}; - } - return {href: url}; } +// Full literal class strings (Tailwind scanner safety) +const ACTION_STYLES = { + gradient: + "rounded-full bg-gradient-to-r from-primary-600 to-secondary-600 px-6 py-3 text-sm font-semibold text-white shadow-lg shadow-primary-500/25 hover:shadow-xl hover:shadow-primary-500/40 hover:from-primary-500 hover:to-secondary-500 transition-all duration-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500", + solid: + "rounded-full bg-primary-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-primary-500 transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600", + outline: + "rounded-full px-6 py-3 text-sm font-semibold ring-1 ring-inset ring-gray-300 dark:ring-gray-600 text-gray-900 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-800/60 transition-colors", + ghost: + "rounded-full px-6 py-3 text-sm font-semibold text-gray-900 dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800/60 transition-colors", + text: "text-sm font-semibold leading-6 text-gray-900 dark:text-gray-100 hover:text-gray-700 dark:hover:text-gray-300 transition-colors", +}; + +function ActionButton({action, defaultStyle, iconSvg}) { + if (!action?.url || !action?.text) return null; + const styleKey = action.style && ACTION_STYLES[action.style] ? action.style : defaultStyle; + const cls = ACTION_STYLES[styleKey]; + const url = processUrl(action.url); + // text-style without an explicit icon shows the classic trailing arrow + const showTextArrow = styleKey === "text" && !iconSvg; + + return ( + + + {iconSvg && ( + + + + )} + {showTextArrow && } + + ); +} + +// Star strip for the trust block. Supports fractional values (e.g. 4.5 → 4 full + half). +const STAR_FULL = ``; +const STAR_EMPTY = ``; + +function StarStrip({stars}) { + const n = Math.max(0, Math.min(5, Number(stars) || 0)); + const full = Math.floor(n); + return ( +
+ {Array.from({length: 5}).map((_, i) => ( + + ))} +
+ ); +} + +function TrustStrip({trust}) { + if (!trust) return null; + const {stars, text} = trust; + if (!stars && !text) return null; + return ( +
+ {stars != null && } + {text && } +
+ ); +} + +// Announcement: pill above the title with optional leading badge. +// announcement.badge: {text, color?} — color is one of: primary | green | amber | rose +const BADGE_COLORS = { + primary: "bg-primary-100 text-primary-700 dark:bg-primary-900/40 dark:text-primary-300", + green: "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300", + amber: "bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300", + rose: "bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300", +}; + +function AnnouncementPill({announcement}) { + if (!announcement?.text) return null; + const badge = announcement.badge; + const badgeCls = badge && BADGE_COLORS[badge.color] ? BADGE_COLORS[badge.color] : BADGE_COLORS.primary; + + return ( + + ); +} + // Hero Block Component - Single implementation -export const HeroBlock = ({content, design, _id, icon_svg}) => { +export const HeroBlock = ({content, design, _id, icon_svg, secondary_icon_svg}) => { const paddingClasses = design?.no_padding ? "" : "py-32 sm:py-48 lg:py-56"; return (
- {/* Announcement Banner */} - {content.announcement?.text && ( - - )} + - {/* Main Content */}
+ {/* Eyebrow */} + {content.eyebrow && ( +

+ )} + {/* Title */} {content.title && (

)} @@ -87,50 +173,13 @@ export const HeroBlock = ({content, design, _id, icon_svg}) => { {/* Action Buttons */} {(content.primary_action?.url || content.secondary_action?.url) && ( -
- {/* Primary CTA */} - {content.primary_action?.url && ( - - - {content.primary_action.icon && ( - - - - )} - - )} - - {/* Secondary CTA */} - {content.secondary_action?.url && ( - - - - - )} +
+ +
)} + +

diff --git a/modules/blox/layouts/_partials/blox/preact-wrapper.html b/modules/blox/layouts/_partials/blox/preact-wrapper.html index b3bfac54..41ce3d6e 100644 --- a/modules/blox/layouts/_partials/blox/preact-wrapper.html +++ b/modules/blox/layouts/_partials/blox/preact-wrapper.html @@ -25,6 +25,14 @@ {{ end }} {{ end }} +{{/* Resolve icon for secondary_action (hero) */}} +{{ with $block.content.secondary_action.icon }} + {{ $icon_data := partial "functions/get_icon_data" (dict "name" .) }} + {{ if $icon_data }} + {{ $props = merge $props (dict "secondary_icon_svg" $icon_data) }} + {{ end }} +{{ end }} + {{/* Resolve icon for single button (cta-card) */}} {{ with $block.content.button.icon }} {{ $icon_data := partial "functions/get_icon_data" (dict "name" .) }} diff --git a/modules/blox/schemas/blocks.json b/modules/blox/schemas/blocks.json index e090bba3..8caa0bd2 100644 --- a/modules/blox/schemas/blocks.json +++ b/modules/blox/schemas/blocks.json @@ -78,7 +78,17 @@ "categories": ["marketing", "landing"], "meta": { "hugoblox": { - "capabilities": ["headline", "subtitle", "dual-cta", "announcement-banner"], + "capabilities": [ + "headline", + "title-highlight", + "eyebrow", + "subtitle", + "dual-cta", + "cta-styles", + "announcement-banner", + "announcement-badge", + "trust-strip" + ], "complexity": "simple", "preact": true, "schemaUrl": "https://api.hugoblox.com/v1/blocks/hero/schema" diff --git a/templates/startup-landing-page/content/_index.md b/templates/startup-landing-page/content/_index.md index bde96c85..35558fb4 100644 --- a/templates/startup-landing-page/content/_index.md +++ b/templates/startup-landing-page/content/_index.md @@ -6,20 +6,30 @@ type: landing sections: - block: hero content: - title: Build Your Landing Pages with Hugo Blox - text: 🧱 EASY. FREE (OPEN SOURCE). NO-CODE 🧱 + eyebrow: The open-source page builder + title: Build your landing page with [Hugo Blox] + text: Production-grade blocks. Edit in Markdown and YAML. Deploy anywhere — for free. primary_action: text: Get Started url: https://hugoblox.com/templates/ icon: rocket-launch + style: gradient secondary_action: text: Read the docs url: https://docs.hugoblox.com + icon: book-open + style: ghost announcement: - text: "Announcing the release of version 1." + badge: + text: NEW + color: primary + text: Hugo Blox v1 is here. link: - text: "Read more" + text: Read more url: "/blog/" + trust: + stars: 5 + text: Loved by **10,000+** developers · Trusted by teams at Google, OpenAI, NVIDIA design: spacing: padding: [0, 0, 0, 0]