@vamos-ui/react 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blocks/block-image.js +1 -1
- package/dist/blocks/block-link.js +1 -1
- package/dist/blocks/index.d.ts +2 -2
- package/dist/blocks/index.js +2 -2
- package/dist/blocks/site/_kit-1.d.ts +1 -0
- package/dist/blocks/site/_kit-1.js +3 -1
- package/dist/blocks/site/announcement-bar.js +1 -1
- package/dist/blocks/site/article-layout.d.ts +1 -0
- package/dist/blocks/site/article-layout.js +2 -1
- package/dist/blocks/site/blog-list.d.ts +1 -0
- package/dist/blocks/site/blog-list.js +3 -2
- package/dist/blocks/site/contact-dock.js +3 -1
- package/dist/blocks/site/contact-form.js +1 -1
- package/dist/blocks/site/cta.d.ts +2 -2
- package/dist/blocks/site/cta.js +8 -5
- package/dist/blocks/site/faq.js +2 -1
- package/dist/blocks/site/feature-grid.d.ts +1 -0
- package/dist/blocks/site/feature-grid.js +7 -5
- package/dist/blocks/site/footer.d.ts +1 -1
- package/dist/blocks/site/footer.js +8 -7
- package/dist/blocks/site/gallery-section.d.ts +6 -1
- package/dist/blocks/site/gallery-section.js +6 -4
- package/dist/blocks/site/hero.d.ts +3 -2
- package/dist/blocks/site/hero.js +14 -6
- package/dist/blocks/site/logo-strip.d.ts +6 -3
- package/dist/blocks/site/logo-strip.js +8 -7
- package/dist/blocks/site/map-hours.d.ts +2 -2
- package/dist/blocks/site/map-hours.js +4 -2
- package/dist/blocks/site/media-text.d.ts +4 -1
- package/dist/blocks/site/media-text.js +6 -4
- package/dist/blocks/site/menu-list.d.ts +7 -3
- package/dist/blocks/site/menu-list.js +34 -7
- package/dist/blocks/site/navbar.d.ts +1 -0
- package/dist/blocks/site/navbar.js +6 -5
- package/dist/blocks/site/not-found.js +2 -1
- package/dist/blocks/site/page-intro.d.ts +1 -0
- package/dist/blocks/site/page-intro.js +2 -1
- package/dist/blocks/site/pricing-table.js +2 -1
- package/dist/blocks/site/process.d.ts +1 -0
- package/dist/blocks/site/process.js +4 -2
- package/dist/blocks/site/registry-1.d.ts +1 -1
- package/dist/blocks/site/registry-2.d.ts +5 -0
- package/dist/blocks/site/registry-2.js +2 -2
- package/dist/blocks/site/rich-text-section.js +2 -1
- package/dist/blocks/site/shared/heading-lines.d.ts +9 -0
- package/dist/blocks/site/shared/heading-lines.js +32 -0
- package/dist/blocks/site/shared/image-sizes.d.ts +25 -0
- package/dist/blocks/site/shared/image-sizes.js +37 -0
- package/dist/blocks/site/shared/island-keys.d.ts +1 -1
- package/dist/blocks/site/shared/island-keys.js +1 -1
- package/dist/blocks/site/shared/labels-2.d.ts +9 -1
- package/dist/blocks/site/shared/labels-2.js +24 -6
- package/dist/blocks/site/shared/open-now.d.ts +1 -1
- package/dist/blocks/site/shared/open-now.js +1 -1
- package/dist/blocks/site/shared/opening-hours.d.ts +18 -2
- package/dist/blocks/site/shared/opening-hours.js +36 -7
- package/dist/blocks/site/shared/tap.d.ts +9 -0
- package/dist/blocks/site/shared/tap.js +16 -0
- package/dist/blocks/site/site-shell.d.ts +11 -0
- package/dist/blocks/site/site-shell.js +43 -16
- package/dist/blocks/site/stats-band.d.ts +3 -1
- package/dist/blocks/site/stats-band.js +11 -7
- package/dist/blocks/site/team.d.ts +1 -1
- package/dist/blocks/site/team.js +4 -3
- package/dist/blocks/site/testimonials.d.ts +2 -2
- package/dist/blocks/site/testimonials.js +5 -4
- package/dist/business/index.d.ts +1 -0
- package/dist/business/index.js +2 -0
- package/dist/business/signer-status.d.ts +53 -0
- package/dist/business/signer-status.js +76 -0
- package/dist/charts/bar-chart.js +6 -2
- package/dist/charts/chart-shell.d.ts +9 -2
- package/dist/charts/chart-shell.js +10 -4
- package/dist/charts/chart.js +1 -1
- package/dist/charts/combo-chart.js +5 -7
- package/dist/charts/utils.d.ts +10 -0
- package/dist/charts/utils.js +46 -0
- package/dist/components/_qr-core.d.ts +26 -0
- package/dist/components/_qr-core.js +376 -0
- package/dist/components/activity-timeline.js +5 -5
- package/dist/components/aspect-ratio.d.ts +26 -0
- package/dist/components/aspect-ratio.js +42 -0
- package/dist/components/backup-codes.d.ts +42 -0
- package/dist/components/backup-codes.js +67 -0
- package/dist/components/breadcrumb.d.ts +2 -2
- package/dist/components/breadcrumb.js +1 -1
- package/dist/components/carousel.d.ts +12 -3
- package/dist/components/carousel.js +8 -5
- package/dist/components/collapsible.d.ts +36 -0
- package/dist/components/collapsible.js +37 -0
- package/dist/components/command.d.ts +8 -2
- package/dist/components/command.js +7 -2
- package/dist/components/image.d.ts +18 -2
- package/dist/components/image.js +12 -5
- package/dist/components/input.d.ts +1 -1
- package/dist/components/input.js +1 -1
- package/dist/components/menubar.d.ts +96 -0
- package/dist/components/menubar.js +123 -0
- package/dist/components/pin-map.d.ts +58 -0
- package/dist/components/pin-map.js +167 -0
- package/dist/components/placeholder-art.d.ts +51 -0
- package/dist/components/placeholder-art.js +82 -0
- package/dist/components/qr-code.d.ts +36 -0
- package/dist/components/qr-code.js +27 -0
- package/dist/components/record-detail-layout.d.ts +2 -2
- package/dist/components/record-detail-layout.js +1 -1
- package/dist/components/sidebar.js +8 -8
- package/dist/components/sla-timer.d.ts +75 -0
- package/dist/components/sla-timer.js +142 -0
- package/dist/components/slug-input.js +1 -1
- package/dist/components/tour.d.ts +60 -0
- package/dist/components/tour.js +139 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/lib/public.d.ts +1 -0
- package/dist/lib/public.js +1 -0
- package/dist/lib/value-format.js +1 -1
- package/dist/locale-data/core/holidays.d.ts +1 -1
- package/dist/locale-data/core/holidays.js +1 -1
- package/dist/locales/ar.d.ts +2087 -0
- package/dist/locales/ar.js +2088 -0
- package/dist/locales/de.d.ts +2087 -0
- package/dist/locales/de.js +2088 -0
- package/dist/locales/en.d.ts +1 -0
- package/dist/locales/en.js +1 -0
- package/dist/locales/es.d.ts +2087 -0
- package/dist/locales/es.js +2087 -0
- package/dist/locales/fr.d.ts +2087 -0
- package/dist/locales/fr.js +2088 -0
- package/dist/locales/tr.d.ts +1 -0
- package/dist/locales/tr.js +1 -0
- package/dist/media/gallery.js +3 -1
- package/dist/media/media-library.js +1 -1
- package/dist/motion/core.d.ts +32 -0
- package/dist/motion/core.js +77 -0
- package/dist/motion/count-up.d.ts +29 -0
- package/dist/motion/count-up.js +92 -0
- package/dist/motion/index.d.ts +11 -0
- package/dist/motion/index.js +13 -0
- package/dist/motion/motion-scope.d.ts +17 -0
- package/dist/motion/motion-scope.js +16 -0
- package/dist/motion/page-transition.d.ts +33 -0
- package/dist/motion/page-transition.js +61 -0
- package/dist/motion/parallax.d.ts +37 -0
- package/dist/motion/parallax.js +90 -0
- package/dist/motion/pointer.d.ts +38 -0
- package/dist/motion/pointer.js +117 -0
- package/dist/motion/reveal.d.ts +72 -0
- package/dist/motion/reveal.js +133 -0
- package/dist/motion/scroll-shrink.d.ts +18 -0
- package/dist/motion/scroll-shrink.js +47 -0
- package/dist/motion/skeleton-swap.d.ts +17 -0
- package/dist/motion/skeleton-swap.js +43 -0
- package/dist/motion/text-reveal.d.ts +32 -0
- package/dist/motion/text-reveal.js +31 -0
- package/dist/motion/ticker.d.ts +35 -0
- package/dist/motion/ticker.js +16 -0
- package/dist/render/block-renderer.js +1 -1
- package/dist/sortable/sortable-tree.d.ts +4 -0
- package/dist/sortable/sortable-tree.js +16 -4
- package/dist/styles/blocks.css +21 -12
- package/dist/styles/editors.css +2 -1
- package/dist/styles/media.css +31 -5
- package/dist/styles/motion.css +389 -0
- package/dist/styles.css +41 -10
- package/package.json +5 -1
package/dist/blocks/site/hero.js
CHANGED
|
@@ -6,6 +6,8 @@ import { BlockImage } from "../block-image.js";
|
|
|
6
6
|
import { BlockLink } from "../block-link.js";
|
|
7
7
|
import { HeroVideo } from "./hero-video.js";
|
|
8
8
|
import { ArrowEnd, CtaList, field, siteText, useBlockIds } from "./_kit-1.js";
|
|
9
|
+
import { headingLines } from "./shared/heading-lines.js";
|
|
10
|
+
import { columnSizes, SITE_SIZES } from "./shared/image-sizes.js";
|
|
9
11
|
/**
|
|
10
12
|
* The first screen and the page `h1`, in five characters per sector — never the generic
|
|
11
13
|
* "centred title + two buttons + abstract visual". Server component; the background video is a
|
|
@@ -21,6 +23,12 @@ export function Hero(props) {
|
|
|
21
23
|
const sectionTheme = variant === "full-bleed" ? { ...theme, mode: "dark" } : theme;
|
|
22
24
|
return (_jsx(Section, { theme: sectionTheme, anchor: anchor, headingId: headingId, blockId: base, blockType: "hero", editMode: editMode, components: components, labels: labels, priority: true, "data-variant": variant, className: cn("vb-hero", className), children: variant === "split-media" ? (_jsx(SplitMedia, { ...props, headingId: headingId, headingLevel: headingLevel })) : variant === "full-bleed" ? (_jsx(FullBleed, { ...props, headingId: headingId, headingLevel: headingLevel })) : variant === "product" ? (_jsx(Product, { ...props, headingId: headingId, headingLevel: headingLevel })) : variant === "statement" ? (_jsx(Statement, { ...props, headingId: headingId, headingLevel: headingLevel })) : (_jsx(Editorial, { ...props, headingId: headingId, headingLevel: headingLevel })) }));
|
|
23
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Entrance cascade (motion.css `.vm-enter`): CSS keyframes from the first paint, no JS, so the
|
|
28
|
+
* hero never waits for hydration. Follows the SiteShell motion level; off in edit mode.
|
|
29
|
+
*/
|
|
30
|
+
const ENTER = ["[--vm-i:0]", "[--vm-i:1]", "[--vm-i:2]", "[--vm-i:3]", "[--vm-i:4]", "[--vm-i:5]"];
|
|
31
|
+
const enter = (editMode, i, kind) => editMode ? undefined : cn("vm-enter", ENTER[i], kind === "title" && "vm-enter-title", kind === "media" && "vm-enter-media");
|
|
24
32
|
function Eyebrow({ children, editMode, className }) {
|
|
25
33
|
if (!children)
|
|
26
34
|
return null;
|
|
@@ -37,31 +45,31 @@ function Meta({ items, className }) {
|
|
|
37
45
|
return (_jsx("dl", { className: cn("flex flex-col gap-1.5 font-mono text-small text-fg-muted", className), children: items.slice(0, 4).map((m, i) => (_jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { className: "text-fg-subtle after:content-['\u00B7'] after:ps-2", children: m.label }), _jsx("dd", { className: "text-fg", children: m.value })] }, i))) }));
|
|
38
46
|
}
|
|
39
47
|
function Title({ p, size, className }) {
|
|
40
|
-
return (_jsx(BlockHeading, { id: p.headingId, level: p.headingLevel, size: size, text: p.title, editMode: p.editMode, field: "props.title", labels: p.labels, locale: p.locale, className: cn("vb-hero-title", className) }));
|
|
48
|
+
return (_jsx(BlockHeading, { id: p.headingId, level: p.headingLevel, size: size, text: headingLines(p.title), editMode: p.editMode, field: "props.title", labels: p.labels, locale: p.locale, className: cn("vb-hero-title", enter(p.editMode, 1, "title"), className) }));
|
|
41
49
|
}
|
|
42
50
|
/* editorial ----------------------------------------------------------- */
|
|
43
51
|
function Editorial(p) {
|
|
44
52
|
const { eyebrow, lead, actions, meta, media, components, labels, locale, editMode } = p;
|
|
45
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Container, { className: "flex flex-col gap-8", children: [_jsx(Eyebrow, { editMode: editMode, children: eyebrow }), _jsx(Title, { p: p, size: "display-xl", className: "max-w-[18ch]" }), _jsxs("div", { className: "grid gap-x-(--gutter) gap-y-8 pt-2 @4xl:grid-cols-12 @4xl:items-end", children: [_jsxs("div", { className: "flex flex-col gap-6 @4xl:col-span-6 @6xl:col-span-5", children: [_jsx(Lead, { editMode: editMode, children: lead }), _jsx(CtaList, { actions: actions, components: components, labels: labels, locale: locale, max: 1, editMode: editMode })] }), _jsx(Meta, { items: meta, className: "@4xl:col-span-4 @4xl:col-start-9 @4xl:items-end @4xl:text-end" })] })] }), media ? (_jsx("div", { className: "vb-hero-strip mt-[calc(var(--section-y)*0.6)]", children: _jsx(BlockImage, { ...media, aspect: "21:9", priority: true, sizes:
|
|
53
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Container, { className: "flex flex-col gap-8", children: [_jsx(Eyebrow, { editMode: editMode, className: enter(editMode, 0), children: eyebrow }), _jsx(Title, { p: p, size: "display-xl", className: "max-w-[18ch]" }), _jsxs("div", { className: "grid gap-x-(--gutter) gap-y-8 pt-2 @4xl:grid-cols-12 @4xl:items-end", children: [_jsxs("div", { className: "flex flex-col gap-6 @4xl:col-span-6 @6xl:col-span-5", children: [_jsx(Lead, { editMode: editMode, className: enter(editMode, 2), children: lead }), _jsx(CtaList, { actions: actions, components: components, labels: labels, locale: locale, max: 1, editMode: editMode, className: enter(editMode, 3) })] }), _jsx(Meta, { items: meta, className: cn("@4xl:col-span-4 @4xl:col-start-9 @4xl:items-end @4xl:text-end", enter(editMode, 4)) })] })] }), media ? (_jsx("div", { className: cn("vb-hero-strip mt-[calc(var(--section-y)*0.6)]", enter(editMode, 4, "media")), children: _jsx(BlockImage, { ...media, aspect: "21:9", priority: true, sizes: SITE_SIZES.bleed, components: components, labels: labels, editMode: editMode, field: "props.media" }) })) : null] }));
|
|
46
54
|
}
|
|
47
55
|
/* split-media ---------------------------------------------------------- */
|
|
48
56
|
function SplitMedia(p) {
|
|
49
57
|
const { eyebrow, lead, actions, meta, media, caption, components, labels, locale, editMode } = p;
|
|
50
|
-
return (_jsxs(Container, { className: "grid gap-x-(--gutter) gap-y-10 @4xl:grid-cols-12 @4xl:items-start", children: [_jsxs("div", { className: "order-2 flex flex-col gap-7 @4xl:order-1 @4xl:col-span-7 @4xl:pt-[calc(var(--section-y)*0.35)] @6xl:pe-[calc(var(--gutter)*2)]", children: [_jsx(Eyebrow, { editMode: editMode, children: eyebrow }), _jsx(Title, { p: p, size: "display-xl" }), _jsx(Lead, { editMode: editMode, className: "max-w-[46ch]", children: lead }), _jsx(CtaList, { actions: actions, components: components, labels: labels, locale: locale, editMode: editMode, className: "pt-2" }), _jsx(Meta, { items: meta, className: "mt-auto border-t border-line pt-5 @4xl:mt-10" })] }), media ? (_jsxs("figure", { className: "vb-hero-portrait vb-bleed-end relative order-1 my-0 @4xl:order-2 @4xl:col-span-5", ...field(editMode, "media"), children: [_jsx(BlockImage, { ...media, aspect: "4:5", priority: true, sizes:
|
|
58
|
+
return (_jsxs(Container, { className: "grid gap-x-(--gutter) gap-y-10 @4xl:grid-cols-12 @4xl:items-start", children: [_jsxs("div", { className: "order-2 flex flex-col gap-7 @4xl:order-1 @4xl:col-span-7 @4xl:pt-[calc(var(--section-y)*0.35)] @6xl:pe-[calc(var(--gutter)*2)]", children: [_jsx(Eyebrow, { editMode: editMode, className: enter(editMode, 0), children: eyebrow }), _jsx(Title, { p: p, size: "display-xl" }), _jsx(Lead, { editMode: editMode, className: cn("max-w-[46ch]", enter(editMode, 2)), children: lead }), _jsx(CtaList, { actions: actions, components: components, labels: labels, locale: locale, editMode: editMode, className: cn("pt-2", enter(editMode, 3)) }), _jsx(Meta, { items: meta, className: cn("mt-auto border-t border-line pt-5 @4xl:mt-10", enter(editMode, 4)) })] }), media ? (_jsxs("figure", { className: cn("vb-hero-portrait vb-bleed-end relative order-1 my-0 @4xl:order-2 @4xl:col-span-5", enter(editMode, 1, "media")), ...field(editMode, "media"), children: [_jsx(BlockImage, { ...media, aspect: "4:5", priority: true, sizes: columnSizes(5 / 12, { bleed: true }), className: "aspect-square! @4xl:aspect-[4/5]!", components: components, labels: labels }), caption ? (_jsx("figcaption", { className: "absolute bottom-0 start-0 bg-scene px-3 py-2 font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", ...field(editMode, "caption"), children: caption })) : null] })) : null] }));
|
|
51
59
|
}
|
|
52
60
|
/* full-bleed ----------------------------------------------------------- */
|
|
53
61
|
function FullBleed(p) {
|
|
54
62
|
const { eyebrow, lead, actions, media, video, aside, height = "auto", components, labels, locale, editMode } = p;
|
|
55
63
|
const t = siteText(labels, locale);
|
|
56
|
-
return (_jsxs("div", { className: cn("vb-hero-bleed", height === "screen" && "vb-hero-screen"), children: [_jsxs("div", { className: "vb-hero-media", "aria-hidden": video ? undefined : "true", children: [media ? (_jsx(BlockImage, { ...media, decorative: true, priority: true, sizes:
|
|
64
|
+
return (_jsxs("div", { className: cn("vb-hero-bleed", height === "screen" && "vb-hero-screen"), children: [_jsxs("div", { className: cn("vb-hero-media", enter(editMode, 0, "media")), "aria-hidden": video ? undefined : "true", children: [media ? (_jsx(BlockImage, { ...media, decorative: true, priority: true, sizes: SITE_SIZES.bleed, className: "absolute inset-0 h-full", components: components, labels: labels, editMode: editMode, field: "props.media" })) : null, video && media ? _jsx(HeroVideo, { src: video.src, poster: media.src, pauseLabel: t("pauseVideo"), playLabel: t("resumeVideo") }) : null] }), _jsxs(Container, { className: "relative z-[1] grid w-full content-end gap-x-(--gutter) gap-y-10 @4xl:grid-cols-12 @4xl:items-end", children: [_jsxs("div", { className: "flex flex-col gap-6 @4xl:col-span-8 @6xl:col-span-7", children: [_jsx(Eyebrow, { editMode: editMode, className: enter(editMode, 0), children: eyebrow }), _jsx(Title, { p: p, size: "display-xl" }), _jsx(Lead, { editMode: editMode, className: cn("max-w-[52ch] text-fg", enter(editMode, 2)), children: lead }), _jsx(CtaList, { actions: actions, components: components, labels: labels, locale: locale, editMode: editMode, className: cn("pt-2", enter(editMode, 3)) })] }), aside ? (_jsxs("aside", { className: cn("vb-hero-aside surface flex flex-col gap-3 rounded-ui p-5 @4xl:col-span-4 @4xl:col-start-9 @6xl:col-span-3 @6xl:col-start-10", enter(editMode, 4)), ...field(editMode, "aside"), children: [_jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: aside.title }), aside.body ? _jsx("p", { className: "text-body tabular text-fg", children: aside.body }) : null, aside.cta ? _jsx(CtaList, { actions: [aside.cta], components: components, labels: labels, locale: locale, size: "md", max: 1, className: "pt-1 [&>a]:w-full" }) : null] })) : null] })] }));
|
|
57
65
|
}
|
|
58
66
|
/* product -------------------------------------------------------------- */
|
|
59
67
|
function Product(p) {
|
|
60
68
|
const { eyebrow, lead, actions, release, screenshot, visual, meta, components, labels, locale, editMode } = p;
|
|
61
|
-
return (_jsxs(Container, { className: "flex flex-col", children: [_jsxs("div", { className: "flex flex-col gap-7", children: [release ? (_jsxs(BlockLink, { href: release.href, components: components, labels: labels, locale: locale, editMode: editMode, field: "props.release", className: "group/rel inline-flex w-fit items-center gap-3 rounded-full border border-line py-1 ps-1 pe-3.5 text-small text-fg-muted transition-colors hover:border-field-border-hover hover:text-fg focus-ring", children: [release.tag ? _jsx("span", { className: "rounded-full bg-accent-soft px-2.5 py-0.5 font-mono text-eyebrow tracking-[0.04em] text-accent-ink", children: release.tag }) : null, _jsx("span", { children: release.label }), _jsx(ArrowEnd, { className: "group-hover/rel:translate-x-0.5 rtl:group-hover/rel:-translate-x-0.5" })] })) : (_jsx(Eyebrow, { editMode: editMode, children: eyebrow })), _jsxs("div", { className: "grid gap-x-(--gutter) gap-y-7 @4xl:grid-cols-12 @4xl:items-end", children: [_jsx(Title, { p: p, size: "display-xl", className: "@4xl:col-span-8" }), _jsxs("div", { className: "flex flex-col gap-6 @4xl:col-span-4", children: [_jsx(Lead, { editMode: editMode, children: lead }), _jsx(CtaList, { actions: actions, components: components, labels: labels, locale: locale, size: "md", editMode: editMode })] })] }), _jsx(Meta, { items: meta, className: "flex-row flex-wrap gap-x-6" })] }), screenshot || visual ? (_jsx("div", { className: "vb-hero-shot mt-[calc(var(--section-y)*0.55)]", ...field(editMode, "screenshot"), children: visual ?? _jsx(BlockImage, { ...screenshot, priority: true, sizes:
|
|
69
|
+
return (_jsxs(Container, { className: "flex flex-col", children: [_jsxs("div", { className: "flex flex-col gap-7", children: [release ? (_jsxs(BlockLink, { href: release.href, components: components, labels: labels, locale: locale, editMode: editMode, field: "props.release", className: cn("group/rel inline-flex w-fit items-center gap-3 rounded-full border border-line py-1 ps-1 pe-3.5 text-small text-fg-muted transition-colors hover:border-field-border-hover hover:text-fg focus-ring", enter(editMode, 0)), children: [release.tag ? _jsx("span", { className: "rounded-full bg-accent-soft px-2.5 py-0.5 font-mono text-eyebrow tracking-[0.04em] text-accent-ink", children: release.tag }) : null, _jsx("span", { children: release.label }), _jsx(ArrowEnd, { className: "group-hover/rel:translate-x-0.5 rtl:group-hover/rel:-translate-x-0.5" })] })) : (_jsx(Eyebrow, { editMode: editMode, className: enter(editMode, 0), children: eyebrow })), _jsxs("div", { className: "grid gap-x-(--gutter) gap-y-7 @4xl:grid-cols-12 @4xl:items-end", children: [_jsx(Title, { p: p, size: "display-xl", className: "@4xl:col-span-8" }), _jsxs("div", { className: "flex flex-col gap-6 @4xl:col-span-4", children: [_jsx(Lead, { editMode: editMode, className: enter(editMode, 2), children: lead }), _jsx(CtaList, { actions: actions, components: components, labels: labels, locale: locale, size: "md", editMode: editMode, className: enter(editMode, 3) })] })] }), _jsx(Meta, { items: meta, className: cn("flex-row flex-wrap gap-x-6", enter(editMode, 4)) })] }), screenshot || visual ? (_jsx("div", { className: cn("vb-hero-shot mt-[calc(var(--section-y)*0.55)]", enter(editMode, 5, "media")), ...field(editMode, "screenshot"), children: visual ?? _jsx(BlockImage, { ...screenshot, priority: true, sizes: SITE_SIZES.container, components: components, labels: labels }) })) : null] }));
|
|
62
70
|
}
|
|
63
71
|
/* statement ------------------------------------------------------------ */
|
|
64
72
|
function Statement(p) {
|
|
65
73
|
const { eyebrow, services, actions, components, labels, locale, editMode } = p;
|
|
66
|
-
return (_jsxs(Container, { className: "flex flex-col items-center gap-10 text-center", children: [_jsx(Eyebrow, { editMode: editMode, children: eyebrow }), _jsx(Title, { p: p, size: "display-2xl", className: "max-w-[16ch]" }), services?.length ? (_jsx("ul", { className: "flex max-w-(--prose) flex-wrap justify-center gap-x-3 gap-y-2 text-lead text-fg-muted", ...field(editMode, "services"), children: services.map((s, i) => (_jsxs("li", { className: "flex items-baseline gap-3", children: [i > 0 ? (_jsx("span", { "aria-hidden": true, className: "text-fg-subtle", children: "/" })) : null, _jsx(BlockLink, { href: s.href, components: components, labels: labels, locale: locale, className: "rounded-[2px] text-fg underline decoration-line underline-offset-[0.25em] transition-[text-decoration-color] hover:decoration-current focus-ring", children: s.label })] }, i))) })) : null, _jsx(CtaList, { actions: actions, components: components, labels: labels, locale: locale, editMode: editMode, className: "justify-center" })] }));
|
|
74
|
+
return (_jsxs(Container, { className: "flex flex-col items-center gap-10 text-center", children: [_jsx(Eyebrow, { editMode: editMode, className: enter(editMode, 0), children: eyebrow }), _jsx(Title, { p: p, size: "display-2xl", className: "max-w-[16ch]" }), services?.length ? (_jsx("ul", { className: cn("flex max-w-(--prose) flex-wrap justify-center gap-x-3 gap-y-2 text-lead text-fg-muted", enter(editMode, 2)), ...field(editMode, "services"), children: services.map((s, i) => (_jsxs("li", { className: "flex items-baseline gap-3", children: [i > 0 ? (_jsx("span", { "aria-hidden": true, className: "text-fg-subtle", children: "/" })) : null, _jsx(BlockLink, { href: s.href, components: components, labels: labels, locale: locale, className: "rounded-[2px] text-fg underline decoration-line underline-offset-[0.25em] transition-[text-decoration-color] hover:decoration-current focus-ring", children: s.label })] }, i))) })) : null, _jsx(CtaList, { actions: actions, components: components, labels: labels, locale: locale, editMode: editMode, className: cn("justify-center", enter(editMode, 3)) })] }));
|
|
67
75
|
}
|
|
@@ -5,6 +5,7 @@ export type LogoStripVariant = "row" | "marquee" | "grid";
|
|
|
5
5
|
export interface LogoItem {
|
|
6
6
|
/** Brand name — the logo's alt text. */
|
|
7
7
|
name: string;
|
|
8
|
+
/** Logo, 1.75rem tall, at most 9rem wide (`sizes="9rem"`). */
|
|
8
9
|
image: SiteImage;
|
|
9
10
|
/** Used inside dark sections / dark mode instead of `invert()`. */
|
|
10
11
|
imageDark?: SiteImage;
|
|
@@ -16,7 +17,8 @@ export interface LogoItem {
|
|
|
16
17
|
export interface LogoStripProps extends Omit<BlockBaseProps, "variant" | "labels"> {
|
|
17
18
|
/**
|
|
18
19
|
* row: caption at the start, logos on one line at an equal optical height (28px).
|
|
19
|
-
* marquee: an endless band with faded edges; pauses on hover/focus
|
|
20
|
+
* marquee: an endless band with faded edges (Ticker); pauses on hover/focus and with its pause
|
|
21
|
+
* control; static under reduced motion and `motion="off"`.
|
|
20
22
|
* grid: 4–6 columns separated by 1px gridlines, logos centred in cells.
|
|
21
23
|
*/
|
|
22
24
|
variant?: LogoStripVariant;
|
|
@@ -32,8 +34,9 @@ export interface LogoStripProps extends Omit<BlockBaseProps, "variant" | "labels
|
|
|
32
34
|
labels?: SiteBlockLabels;
|
|
33
35
|
}
|
|
34
36
|
/**
|
|
35
|
-
* "Brands we work with" proof. Server component; the marquee is
|
|
36
|
-
* hover, focus and
|
|
37
|
+
* "Brands we work with" proof. Server component; the marquee is the motion kit's Ticker: CSS
|
|
38
|
+
* keyframes, paused on hover, focus and with its pause control; static under reduced motion and
|
|
39
|
+
* SiteShell `motion="off"`; the duplicate copy is `aria-hidden` and `inert`).
|
|
37
40
|
*
|
|
38
41
|
* ```tsx
|
|
39
42
|
* <LogoStrip caption="Trusted by 120+ businesses since 2014" logos={[{ name: "Kıyı", image: { src, width, height } }]} />
|
|
@@ -3,10 +3,12 @@ import { cn } from "../../lib/cn.js";
|
|
|
3
3
|
import { Container, Section } from "../section.js";
|
|
4
4
|
import { BlockImage } from "../block-image.js";
|
|
5
5
|
import { BlockLink } from "../block-link.js";
|
|
6
|
+
import { Ticker } from "../../motion/ticker.js";
|
|
6
7
|
import { siteText, useBlockIds } from "./_kit-1.js";
|
|
7
8
|
/**
|
|
8
|
-
* "Brands we work with" proof. Server component; the marquee is
|
|
9
|
-
* hover, focus and
|
|
9
|
+
* "Brands we work with" proof. Server component; the marquee is the motion kit's Ticker: CSS
|
|
10
|
+
* keyframes, paused on hover, focus and with its pause control; static under reduced motion and
|
|
11
|
+
* SiteShell `motion="off"`; the duplicate copy is `aria-hidden` and `inert`).
|
|
10
12
|
*
|
|
11
13
|
* ```tsx
|
|
12
14
|
* <LogoStrip caption="Trusted by 120+ businesses since 2014" logos={[{ name: "Kıyı", image: { src, width, height } }]} />
|
|
@@ -16,11 +18,10 @@ export function LogoStrip({ variant = "row", caption, logos, monochrome = true,
|
|
|
16
18
|
const { base, headingId } = useBlockIds(id, "logos");
|
|
17
19
|
const t = siteText(labels, locale);
|
|
18
20
|
const marquee = variant === "marquee" && logos.length >= 4;
|
|
19
|
-
const logo = (l, i
|
|
20
|
-
const img = (_jsxs(_Fragment, { children: [_jsx(BlockImage, { ...l.image, alt:
|
|
21
|
-
|
|
22
|
-
return (_jsx("li", { "aria-hidden": hidden || undefined, "data-dup": hidden ? "" : undefined, className: "vb-logo-item flex shrink-0 items-center justify-center", "data-field": editMode && !hidden ? `props.logos.${i}` : undefined, children: inner }, `${hidden ? "d" : "l"}${i}`));
|
|
21
|
+
const logo = (l, i) => {
|
|
22
|
+
const img = (_jsxs(_Fragment, { children: [_jsx(BlockImage, { ...l.image, alt: l.name, fit: "contain", sizes: "9rem", className: cn("vb-logo", l.imageDark && "vb-logo-light"), components: components }), l.imageDark ? _jsx(BlockImage, { ...l.imageDark, decorative: true, fit: "contain", sizes: "9rem", className: "vb-logo vb-logo-dark", components: components }) : null] }));
|
|
23
|
+
return (_jsx("li", { className: "vb-logo-item flex shrink-0 items-center justify-center", "data-field": editMode ? `props.logos.${i}` : undefined, children: l.link ? (_jsx(BlockLink, { href: l.link.href, newTab: l.link.newTab, components: components, labels: labels, locale: locale, className: "flex items-center rounded-[2px] focus-ring", children: img })) : (img) }, i));
|
|
23
24
|
};
|
|
24
25
|
const captionEl = caption ? (_jsx("p", { id: headingId, className: cn("text-small text-fg-muted text-pretty", variant === "row" && "max-w-[26ch] shrink-0"), "data-field": editMode ? "props.caption" : undefined, children: caption })) : null;
|
|
25
|
-
return (_jsx(Section, { theme: theme, anchor: anchor, headingId: caption ? headingId : undefined, "aria-label": caption ? undefined : t("logos"), blockId: base, blockType: "logo-strip", editMode: editMode, className: cn("vb-logos", className), "data-variant": marquee ? "marquee" : variant === "marquee" ? "row" : variant, "data-mono": monochrome ? "" : undefined, children: marquee ? (_jsxs("div", { className: "flex flex-col gap-8", children: [captionEl ? _jsx(Container, { children: captionEl }) : null, _jsx(
|
|
26
|
+
return (_jsx(Section, { theme: theme, anchor: anchor, headingId: caption ? headingId : undefined, "aria-label": caption ? undefined : t("logos"), blockId: base, blockType: "logo-strip", editMode: editMode, className: cn("vb-logos", className), "data-variant": marquee ? "marquee" : variant === "marquee" ? "row" : variant, "data-mono": monochrome ? "" : undefined, children: marquee ? (_jsxs("div", { className: "flex flex-col gap-8", children: [captionEl ? _jsx(Container, { children: captionEl }) : null, _jsx(Ticker, { as: "ul", duration: speed, pauseLabel: t("pauseMotion"), className: "vb-marquee-band", "data-motion": editMode ? "off" : undefined, children: logos.map((l, i) => logo(l, i)) })] })) : variant === "grid" ? (_jsxs(Container, { className: "flex flex-col gap-8", children: [captionEl, _jsx("ul", { className: cn("vb-logo-grid grid grid-cols-2 @2xl:grid-cols-3", columns === 4 && "@4xl:grid-cols-4", columns === 5 && "@4xl:grid-cols-5", columns === 6 && "@4xl:grid-cols-4 @6xl:grid-cols-6"), children: logos.map((l, i) => logo(l, i)) })] })) : (_jsxs(Container, { className: "flex flex-col gap-8 @4xl:flex-row @4xl:items-center @4xl:gap-(--gutter)", children: [captionEl, _jsx("ul", { className: "flex flex-1 flex-wrap items-center gap-x-[clamp(2rem,5cqi,4rem)] gap-y-6", children: logos.map((l, i) => logo(l, i)) })] })) }));
|
|
26
27
|
}
|
|
@@ -15,7 +15,7 @@ export interface Branch {
|
|
|
15
15
|
phone?: string;
|
|
16
16
|
email?: string;
|
|
17
17
|
hours: OpeningHours;
|
|
18
|
-
/** Static map picture for the facade (greyscale, an accent pin on top). */
|
|
18
|
+
/** Static map picture for the facade (greyscale, an accent pin on top). `sizes`: `(min-width: 56rem) min(59vw, 45rem), 100vw`. */
|
|
19
19
|
mapImage?: SiteImage2;
|
|
20
20
|
/** Google Business Profile / place link. */
|
|
21
21
|
placeUrl?: string;
|
|
@@ -40,7 +40,7 @@ export interface MapHoursProps extends Omit<BlockBaseProps, "variant" | "labels"
|
|
|
40
40
|
mapProvider?: "google" | "osm" | "none";
|
|
41
41
|
/** Direction links. Default google · apple (Yandex is common in Turkey, Waze elsewhere). */
|
|
42
42
|
directions?: DirectionsProvider[];
|
|
43
|
-
/** "Open
|
|
43
|
+
/** "Open · until 10 PM" island ("Closing soon · open until 10 PM", "Closed · opens tomorrow at 12 PM"). Default true. */
|
|
44
44
|
showOpenNow?: boolean;
|
|
45
45
|
/**
|
|
46
46
|
* Weekly hours: `week` (default) lists all seven days, `open-days` hides closed days (a venue
|
|
@@ -10,6 +10,8 @@ import { OpenNow } from "./shared/open-now.js";
|
|
|
10
10
|
import { ConsentGate } from "./shared/consent-gate.js";
|
|
11
11
|
import { openNowLabelKeys } from "./shared/island-keys.js";
|
|
12
12
|
import { BrandIcon, BRAND_NAMES } from "./shared/brand-icons.js";
|
|
13
|
+
import { columnSizes } from "./shared/image-sizes.js";
|
|
14
|
+
import { TAP } from "./shared/tap.js";
|
|
13
15
|
const DIRECTION_BRAND = { google: "google-maps", apple: "apple-maps", yandex: "yandex-maps", waze: "waze" };
|
|
14
16
|
const query = (b) => (b.address.geo ? `${b.address.geo.lat},${b.address.geo.lng}` : [b.name, ...addressLines(b.address, { withCountry: true })].join(", "));
|
|
15
17
|
/** Direction URL for a provider (no API keys). */
|
|
@@ -87,7 +89,7 @@ export function MapHours({ variant: variantProp = "map-split", eyebrow, title, l
|
|
|
87
89
|
}
|
|
88
90
|
function MapFacade({ branch, ctx }) {
|
|
89
91
|
const src = ctx.mapProvider !== "none" ? mapEmbedUrl(branch, ctx.mapProvider) : undefined;
|
|
90
|
-
const preview = branch.mapImage?.src ? (_jsxs(_Fragment, { children: [_jsx(BlockImage, { ...branch.mapImage, decorative: true, aspect: "4:3", sizes:
|
|
92
|
+
const preview = branch.mapImage?.src ? (_jsxs(_Fragment, { children: [_jsx(BlockImage, { ...branch.mapImage, decorative: true, aspect: "4:3", sizes: columnSizes(7 / 12), className: "size-full", imgClassName: "vb-map-img", components: ctx.components }), _jsx("span", { "aria-hidden": true, className: "vb-map-pin absolute start-1/2 top-1/2 -translate-x-1/2 -translate-y-full text-accent rtl:translate-x-1/2", children: _jsxs("svg", { viewBox: "0 0 32 40", className: "h-10 w-8 drop-shadow-sm", children: [_jsx("path", { d: "M16 0C7.16 0 0 7.16 0 16c0 11.2 14.4 23.2 15.04 23.72a1.5 1.5 0 0 0 1.92 0C17.6 39.2 32 27.2 32 16 32 7.16 24.84 0 16 0Z", fill: "currentColor" }), _jsx("circle", { cx: "16", cy: "16", r: "6", fill: "var(--accent-fg)" })] }) })] })) : null;
|
|
91
93
|
if (!src)
|
|
92
94
|
return preview ? _jsx("div", { className: "vb-gate relative aspect-[4/3] overflow-hidden rounded-ui bg-surface-2", children: preview }) : null;
|
|
93
95
|
return (_jsx(ConsentGate, { src: src, title: ctx.t("mapOf", { name: branch.name }), loadLabel: ctx.t("loadMap"), notice: ctx.t("mapNotice", { provider: ctx.mapProvider === "osm" ? "OpenStreetMap" : "Google Maps" }), rememberLabel: ctx.t("rememberChoice"), category: "functional", children: preview }));
|
|
@@ -117,7 +119,7 @@ function BranchDetails({ branch, ctx, index }) {
|
|
|
117
119
|
function InfoColumns({ branch, ctx }) {
|
|
118
120
|
const summary = summarizeHours(branch.hours, ctx.locale).filter((s) => ctx.hoursDisplay !== "open-days" || !s.closed);
|
|
119
121
|
const showHours = ctx.hoursDisplay !== "none" || ctx.showOpenNow;
|
|
120
|
-
return (_jsxs("div", { className: cn("grid gap-x-(--gutter) gap-y-10 border-t border-line pt-8", showHours ? "@2xl:grid-cols-3" : "@2xl:grid-cols-2"), children: [_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: ctx.t("address") }), _jsx(AddressBlock, { branch: branch, ctx: ctx, large: true }), branch.notes ? _jsx("p", { className: "text-small text-fg-muted", children: branch.notes }) : null, ctx.directions[0] ? (_jsxs("a", { href: directionsUrl(ctx.directions[0], branch), target: "_blank", rel: "noopener noreferrer", className: "mt-1 self-start text-small font-medium text-fg underline decoration-line underline-offset-[0.25em] hover:decoration-current focus-ring rounded-[2px]", "data-track": `directions:${ctx.directions[0]}`, children: [ctx.t("directionsVia", { provider: BRAND_NAMES[DIRECTION_BRAND[ctx.directions[0]]] }), _jsxs("span", { className: "sr-only", children: [" ", ctx.t("opensInNewTab")] })] })) : null] }), showHours ? (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("div", { className: "flex flex-wrap items-baseline justify-between gap-3", children: _jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: ctx.t("hours") }) }), ctx.hoursDisplay !== "none" ? (_jsx("ul", { className: "flex flex-col gap-1.5 text-lead", children: summary.map((s, i) => (_jsxs("li", { className: "flex justify-between gap-6 tabular", children: [_jsx("span", { className: "text-fg-muted", children: s.days }), _jsx("span", { className: s.closed ? "text-fg-subtle" : "text-fg", children: s.closed ? ctx.t("closedDay") : s.text })] }, i))) })) : null, ctx.showOpenNow ? _jsx(OpenNow, { hours: branch.hours, labels: ctx.openNow, locale: ctx.locale, now: ctx.now, className: "mt-2" }) : null] })) : null, _jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: ctx.t("contactDetails") }), branch.phone ? (_jsx("a", { href: telHref(branch.phone), className: "self-start text-lead font-medium tabular text-fg hover:text-accent-ink focus-ring rounded-[2px]", "data-track": "call", children: prettyPhone(branch.phone) })) : null, branch.email ? (_jsx("a", { href: `mailto:${branch.email}`, className: "self-start text-fg underline decoration-line underline-offset-[0.25em] hover:decoration-current focus-ring rounded-[2px]", "data-track": "email", children: branch.email })) : null] })] }));
|
|
122
|
+
return (_jsxs("div", { className: cn("grid gap-x-(--gutter) gap-y-10 border-t border-line pt-8", showHours ? "@2xl:grid-cols-3" : "@2xl:grid-cols-2"), children: [_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: ctx.t("address") }), _jsx(AddressBlock, { branch: branch, ctx: ctx, large: true }), branch.notes ? _jsx("p", { className: "text-small text-fg-muted", children: branch.notes }) : null, ctx.directions[0] ? (_jsxs("a", { href: directionsUrl(ctx.directions[0], branch), target: "_blank", rel: "noopener noreferrer", className: "mt-1 inline-flex min-h-6 items-center self-start pointer-coarse:min-h-11 text-small font-medium text-fg underline decoration-line underline-offset-[0.25em] hover:decoration-current focus-ring rounded-[2px]", "data-track": `directions:${ctx.directions[0]}`, children: [ctx.t("directionsVia", { provider: BRAND_NAMES[DIRECTION_BRAND[ctx.directions[0]]] }), _jsxs("span", { className: "sr-only", children: [" ", ctx.t("opensInNewTab")] })] })) : null] }), showHours ? (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("div", { className: "flex flex-wrap items-baseline justify-between gap-3", children: _jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: ctx.t("hours") }) }), ctx.hoursDisplay !== "none" ? (_jsx("ul", { className: "flex flex-col gap-1.5 text-lead", children: summary.map((s, i) => (_jsxs("li", { className: "flex justify-between gap-6 tabular", children: [_jsx("span", { className: "text-fg-muted", children: s.days }), _jsx("span", { className: s.closed ? "text-fg-subtle" : "text-fg", children: s.closed ? ctx.t("closedDay") : s.text })] }, i))) })) : null, ctx.showOpenNow ? _jsx(OpenNow, { hours: branch.hours, labels: ctx.openNow, locale: ctx.locale, now: ctx.now, className: "mt-2" }) : null] })) : null, _jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: ctx.t("contactDetails") }), branch.phone ? (_jsx("a", { href: telHref(branch.phone), className: cn(TAP, "self-start text-lead font-medium tabular text-fg hover:text-accent-ink focus-ring rounded-[2px]"), "data-track": "call", children: prettyPhone(branch.phone) })) : null, branch.email ? (_jsx("a", { href: `mailto:${branch.email}`, className: cn(TAP, "self-start text-fg underline decoration-line underline-offset-[0.25em] hover:decoration-current focus-ring rounded-[2px]"), "data-track": "email", children: branch.email })) : null] })] }));
|
|
121
123
|
}
|
|
122
124
|
function BranchPicker({ branches, ctx, name }) {
|
|
123
125
|
const list = branches.slice(0, 12);
|
|
@@ -22,7 +22,10 @@ export interface MediaTextProps extends Omit<BlockBaseProps, "variant" | "labels
|
|
|
22
22
|
/** Short points with accent dashes (≤ 6). */
|
|
23
23
|
bullets?: React.ReactNode[];
|
|
24
24
|
cta?: SiteCta;
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* The image (also the video poster). `sizes`: side `(min-width: 56rem) min(42vw, 32rem), 100vw` ·
|
|
27
|
+
* overlap `min(67vw, 51rem)` · bleed `59vw` (runs to the section edge); 100vw on narrow screens.
|
|
28
|
+
*/
|
|
26
29
|
media: SiteImage;
|
|
27
30
|
/** Static preview → plays only after a click (YouTube via youtube-nocookie). */
|
|
28
31
|
video?: MediaTextVideo;
|
|
@@ -7,6 +7,8 @@ import { BlockImage } from "../block-image.js";
|
|
|
7
7
|
import { RichText } from "../rich-text.js";
|
|
8
8
|
import { EmbedFacade } from "./embed-facade.js";
|
|
9
9
|
import { CtaList, siteText, useBlockIds } from "./_kit-1.js";
|
|
10
|
+
import { Reveal } from "../../motion/reveal.js";
|
|
11
|
+
import { columnSizes } from "./shared/image-sizes.js";
|
|
10
12
|
/** Embed URL for a video facade (YouTube → youtube-nocookie, Vimeo → player). */
|
|
11
13
|
export function videoEmbedUrl(v) {
|
|
12
14
|
if (v.provider === "youtube") {
|
|
@@ -43,13 +45,13 @@ export function MediaText({ variant = "side", eyebrow, title, body, bullets, cta
|
|
|
43
45
|
const text = (_jsxs("div", { className: "flex flex-col gap-7", children: [_jsx(SectionHeader, { eyebrow: eyebrow, title: title, headingLevel: headingLevel, headingId: headingId, editMode: editMode, labels: labels, locale: locale }), body ? _jsx(RichText, { doc: body, components: components, labels: labels, locale: locale, headingOffset: 1, editMode: editMode, field: "props.body", className: "text-fg-muted" }) : null, bullets?.length ? (_jsx("ul", { className: "flex flex-col border-t border-line", "data-field": editMode ? "props.bullets" : undefined, children: bullets.slice(0, 6).map((b, i) => (_jsxs("li", { className: "flex gap-4 border-b border-line py-3 text-body text-fg", children: [_jsx("span", { "aria-hidden": true, className: "mt-[0.8em] h-px w-4 shrink-0 bg-accent" }), _jsx("span", { children: b })] }, i))) })) : null, cta ? _jsx(CtaList, { actions: [cta], components: components, labels: labels, locale: locale, max: 1, editMode: editMode }) : null] }));
|
|
44
46
|
let content;
|
|
45
47
|
if (variant === "overlap") {
|
|
46
|
-
content = (_jsxs(Container, { className: "grid gap-y-0 @4xl:grid-cols-12 @4xl:pb-[calc(var(--section-y)*0.4)]", children: [_jsx("div", { className: cn("@4xl:col-span-8 @4xl:row-start-1", end && "@4xl:col-start-5"), children: image(
|
|
48
|
+
content = (_jsxs(Container, { className: "grid gap-y-0 @4xl:grid-cols-12 @4xl:pb-[calc(var(--section-y)*0.4)]", children: [_jsx("div", { className: cn("@4xl:col-span-8 @4xl:row-start-1", end && "@4xl:col-start-5"), children: image(columnSizes(8 / 12), "rounded-ui") }), _jsx("div", { className: cn("vb-overlap-panel surface relative -mt-12 mx-[clamp(0.75rem,3cqi,1.5rem)] rounded-ui p-[clamp(1.5rem,4cqi,3rem)] @4xl:row-start-1 @4xl:mx-0 @4xl:mt-0 @4xl:self-end @4xl:translate-y-[calc(var(--section-y)*0.35)]", end ? "@4xl:col-span-6 @4xl:col-start-1" : "@4xl:col-span-6 @4xl:col-start-7"), children: text })] }));
|
|
47
49
|
}
|
|
48
50
|
else if (variant === "bleed") {
|
|
49
|
-
content = (_jsxs(Container, { className: "grid gap-x-(--gutter) gap-y-10 @4xl:grid-cols-12 @4xl:items-start", children: [_jsx("div", { className: cn("@4xl:col-span-7", end ? "vb-bleed-end @4xl:order-2 @4xl:col-start-6" : "vb-bleed-start"), children: image(
|
|
51
|
+
content = (_jsxs(Container, { className: "grid gap-x-(--gutter) gap-y-10 @4xl:grid-cols-12 @4xl:items-start", children: [_jsx("div", { className: cn("@4xl:col-span-7", end ? "vb-bleed-end @4xl:order-2 @4xl:col-start-6" : "vb-bleed-start"), children: image(columnSizes(7 / 12, { bleed: true })) }), _jsx("div", { className: cn("@4xl:col-span-4 @4xl:pt-4", end ? "@4xl:order-1 @4xl:col-start-1" : "@4xl:col-start-9"), children: text })] }));
|
|
50
52
|
}
|
|
51
53
|
else {
|
|
52
|
-
content = (_jsxs(Container, { className: "grid gap-x-(--gutter) gap-y-10 @4xl:grid-cols-12 @4xl:items-start", children: [_jsx("div", { className: cn("@4xl:col-span-5", end && "@4xl:order-2 @4xl:col-start-8"), children: image(
|
|
54
|
+
content = (_jsxs(Container, { className: "grid gap-x-(--gutter) gap-y-10 @4xl:grid-cols-12 @4xl:items-start", children: [_jsx("div", { className: cn("@4xl:col-span-5", end && "@4xl:order-2 @4xl:col-start-8"), children: image(columnSizes(5 / 12), "rounded-ui") }), _jsx("div", { className: cn("@4xl:col-span-6", end ? "@4xl:order-1 @4xl:col-start-1" : "@4xl:col-start-7"), children: text })] }));
|
|
53
55
|
}
|
|
54
|
-
return (_jsx(Section, { theme: theme, anchor: anchor, headingId: headingId, blockId: base, blockType: "media-text", editMode: editMode, className: cn("vb-media-text", variant === "overlap" && "vb-media-overlap", className), "data-variant": variant, "data-side": mediaSide, children: _jsx(
|
|
56
|
+
return (_jsx(Section, { theme: theme, anchor: anchor, headingId: headingId, blockId: base, blockType: "media-text", editMode: editMode, className: cn("vb-media-text", variant === "overlap" && "vb-media-overlap", className), "data-variant": variant, "data-side": mediaSide, children: _jsx(Reveal, { disabled: editMode, children: content }) }));
|
|
55
57
|
}
|
|
@@ -4,7 +4,9 @@ import type { MoneyValue } from "../schema/types.js";
|
|
|
4
4
|
import { type Site2BlockLabels } from "./shared/labels-2.js";
|
|
5
5
|
import { type SiteImage2 } from "./shared/kit-2.js";
|
|
6
6
|
export type MenuVariant = "classic" | "photo" | "service-list";
|
|
7
|
-
|
|
7
|
+
/** Dietary and house tags, rendered as words (never emoji). */
|
|
8
|
+
export declare const MENU_TAGS: readonly ["vegetarian", "vegan", "gluten-free", "dairy-free", "nut-free", "halal", "spicy", "new", "chef-pick", "to-share", "seasonal"];
|
|
9
|
+
export type MenuTag = (typeof MENU_TAGS)[number];
|
|
8
10
|
/** The 14 EU / Codex allergens. */
|
|
9
11
|
export declare const ALLERGENS: readonly ["gluten", "crustaceans", "eggs", "fish", "peanuts", "soybeans", "milk", "nuts", "celery", "mustard", "sesame", "sulphites", "lupin", "molluscs"];
|
|
10
12
|
export type Allergen = (typeof ALLERGENS)[number];
|
|
@@ -23,7 +25,7 @@ export interface MenuItem {
|
|
|
23
25
|
duration?: React.ReactNode;
|
|
24
26
|
tags?: MenuTag[];
|
|
25
27
|
allergens?: Allergen[];
|
|
26
|
-
/** Square photo (`photo` variant). */
|
|
28
|
+
/** Square photo (`photo` variant), 72px (`sizes="72px"`). */
|
|
27
29
|
image?: SiteImage2;
|
|
28
30
|
/** "Not today" — shown as a label, never a strikethrough. */
|
|
29
31
|
unavailable?: boolean;
|
|
@@ -59,7 +61,9 @@ export interface MenuListProps extends Omit<BlockBaseProps, "variant" | "labels"
|
|
|
59
61
|
labels?: Site2BlockLabels;
|
|
60
62
|
}
|
|
61
63
|
/**
|
|
62
|
-
* Restaurant menus and spa/salon price lists — also a
|
|
64
|
+
* Restaurant menus and spa/salon price lists — also a whole menu page, so a site never has to
|
|
65
|
+
* link out to a third-party QR menu: pass `headingLevel={1}` and the title becomes the page `h1`,
|
|
66
|
+
* categories `h2`, dishes `h3`. Category jump bar with plain
|
|
63
67
|
* anchors, money through `Intl` (full currency name for screen readers), tags as words (never
|
|
64
68
|
* emoji), the 14 allergens localized. Server component, 0 JS.
|
|
65
69
|
*
|
|
@@ -7,11 +7,35 @@ import { BlockImage } from "../block-image.js";
|
|
|
7
7
|
import { BlockLink } from "../block-link.js";
|
|
8
8
|
import { site2Text } from "./shared/labels-2.js";
|
|
9
9
|
import { fieldAttr, formatMoney, formatMoneyLong, textOf, useBlockIds2 } from "./shared/kit-2.js";
|
|
10
|
+
/** Dietary and house tags, rendered as words (never emoji). */
|
|
11
|
+
export const MENU_TAGS = ["vegetarian", "vegan", "gluten-free", "dairy-free", "nut-free", "halal", "spicy", "new", "chef-pick", "to-share", "seasonal"];
|
|
10
12
|
/** The 14 EU / Codex allergens. */
|
|
11
13
|
export const ALLERGENS = ["gluten", "crustaceans", "eggs", "fish", "peanuts", "soybeans", "milk", "nuts", "celery", "mustard", "sesame", "sulphites", "lupin", "molluscs"];
|
|
12
|
-
const TAG_KEY = {
|
|
14
|
+
const TAG_KEY = {
|
|
15
|
+
vegetarian: "tagVegetarian",
|
|
16
|
+
vegan: "tagVegan",
|
|
17
|
+
"gluten-free": "tagGlutenFree",
|
|
18
|
+
"dairy-free": "tagDairyFree",
|
|
19
|
+
"nut-free": "tagNutFree",
|
|
20
|
+
halal: "tagHalal",
|
|
21
|
+
spicy: "tagSpicy",
|
|
22
|
+
new: "tagNew",
|
|
23
|
+
"chef-pick": "tagChefPick",
|
|
24
|
+
"to-share": "tagToShare",
|
|
25
|
+
seasonal: "tagSeasonal",
|
|
26
|
+
};
|
|
27
|
+
const DIET = {
|
|
28
|
+
vegan: "https://schema.org/VeganDiet",
|
|
29
|
+
vegetarian: "https://schema.org/VegetarianDiet",
|
|
30
|
+
"gluten-free": "https://schema.org/GlutenFreeDiet",
|
|
31
|
+
"dairy-free": "https://schema.org/LowLactoseDiet",
|
|
32
|
+
halal: "https://schema.org/HalalDiet",
|
|
33
|
+
};
|
|
34
|
+
const hTag = (level) => `h${Math.min(6, Math.max(2, level))}`;
|
|
13
35
|
/**
|
|
14
|
-
* Restaurant menus and spa/salon price lists — also a
|
|
36
|
+
* Restaurant menus and spa/salon price lists — also a whole menu page, so a site never has to
|
|
37
|
+
* link out to a third-party QR menu: pass `headingLevel={1}` and the title becomes the page `h1`,
|
|
38
|
+
* categories `h2`, dishes `h3`. Category jump bar with plain
|
|
15
39
|
* anchors, money through `Intl` (full currency name for screen readers), tags as words (never
|
|
16
40
|
* emoji), the 14 allergens localized. Server component, 0 JS.
|
|
17
41
|
*
|
|
@@ -25,10 +49,13 @@ export function MenuList({ variant = "classic", eyebrow, title, lead, categories
|
|
|
25
49
|
const categories = all.filter((c) => c.items.length > 0 || editMode);
|
|
26
50
|
const nav = showNav ?? categories.length > 2;
|
|
27
51
|
const catId = (c, i) => c.anchor ?? `${slugify(textOf(c.title), { locale }) || "section"}-${i}`;
|
|
28
|
-
|
|
52
|
+
// Outline follows the block title: h2 title → h3 categories → h4 dishes; h1 page title → h2 → h3.
|
|
53
|
+
const catLevel = title ? headingLevel + 1 : headingLevel;
|
|
54
|
+
const CatH = hTag(catLevel);
|
|
55
|
+
const ctx = { t, locale, components, labels, editMode, variant, bookHref, showAllergens, ItemH: hTag(catLevel + 1) };
|
|
29
56
|
return (_jsx(Section, { theme: theme, anchor: anchor, headingId: title ? headingId : undefined, blockId: id, blockType: "menu", editMode: editMode, className: cn("vb-menu", className), "data-variant": variant, children: _jsxs(Container, { className: "flex flex-col gap-10 @4xl:gap-14", children: [title || eyebrow || lead ? _jsx(SectionHeader, { eyebrow: eyebrow, title: title, lead: lead, headingId: headingId, headingLevel: headingLevel, align: "split", editMode: editMode, labels: labels, locale: locale }) : null, nav ? (_jsx("nav", { "aria-label": t("menuCategories"), className: "vb-menu-nav sticky top-[var(--topbar-h,0px)] z-10 -mx-(--gutter) border-b border-line bg-scene px-(--gutter)", children: _jsx("ul", { className: "scrollbar-none flex gap-1 overflow-x-auto py-2", children: categories.map((c, i) => (_jsx("li", { className: "shrink-0", children: _jsx("a", { href: `#${base}-${catId(c, i)}`, className: "inline-flex h-10 items-center whitespace-nowrap rounded-full px-3.5 text-sm text-fg-muted transition-colors hover:bg-hover hover:text-fg focus-ring", children: c.title }) }, i))) }) })) : null, _jsx("div", { className: "flex flex-col gap-14 @4xl:gap-20", children: categories.map((c, ci) => {
|
|
30
57
|
const hid = `${base}-${catId(c, ci)}`;
|
|
31
|
-
return (_jsxs("section", { id: hid, "aria-labelledby": `${hid}-h`, className: "vb-menu-cat flex flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2 border-t-[3px] border-double border-line pt-5 @2xl:flex-row @2xl:items-baseline @2xl:justify-between @2xl:gap-8", children: [_jsx(
|
|
58
|
+
return (_jsxs("section", { id: hid, "aria-labelledby": `${hid}-h`, className: "vb-menu-cat flex flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2 border-t-[3px] border-double border-line pt-5 @2xl:flex-row @2xl:items-baseline @2xl:justify-between @2xl:gap-8", children: [_jsx(CatH, { id: `${hid}-h`, className: "font-display text-display-m text-fg", ...fieldAttr(editMode, `categories.${ci}.title`), children: c.title }), c.note ? _jsx("p", { className: "max-w-[44ch] text-small text-fg-muted @2xl:text-end", children: c.note }) : null] }), variant === "service-list" ? (_jsx("ul", { className: "flex flex-col", children: c.items.map((item, ii) => (_jsx(ServiceRow, { item: item, ctx: ctx }, ii))) })) : (_jsx("ul", { className: cn("grid gap-x-(--gutter) gap-y-7", variant === "classic" && "@4xl:grid-cols-2 @4xl:gap-x-16"), children: c.items.map((item, ii) => (_jsx(DishItem, { item: item, ctx: ctx }, ii))) }))] }, ci));
|
|
32
59
|
}) }), priceNote ? (_jsx("p", { className: "border-t border-line pt-5 text-small text-fg-muted", ...fieldAttr(editMode, "priceNote"), children: priceNote })) : null] }) }));
|
|
33
60
|
}
|
|
34
61
|
function Price({ price, ctx, className }) {
|
|
@@ -57,12 +84,12 @@ function Allergens({ item, ctx }) {
|
|
|
57
84
|
}
|
|
58
85
|
function DishItem({ item, ctx }) {
|
|
59
86
|
const photo = ctx.variant === "photo";
|
|
60
|
-
return (_jsxs("li", { "data-unavailable": item.unavailable ? "" : undefined, className: cn("vb-menu-item flex gap-4", item.unavailable && "opacity-70"), children: [photo ? (item.image?.src ? (_jsx(BlockImage, { ...item.image, alt: item.image.alt ?? "", decorative: !item.image.alt, aspect: "1:1", sizes: "72px", className: "size-[72px] shrink-0 rounded-ui-sm", components: ctx.components })) : (_jsx("span", { "aria-hidden": true, className: "size-[72px] shrink-0" }))) : null, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [_jsxs("div", { className: "flex items-baseline gap-3", children: [_jsx(
|
|
87
|
+
return (_jsxs("li", { "data-unavailable": item.unavailable ? "" : undefined, className: cn("vb-menu-item flex gap-4", item.unavailable && "opacity-70"), children: [photo ? (item.image?.src ? (_jsx(BlockImage, { ...item.image, alt: item.image.alt ?? "", decorative: !item.image.alt, aspect: "1:1", sizes: "72px", className: "size-[72px] shrink-0 rounded-ui-sm", components: ctx.components })) : (_jsx("span", { "aria-hidden": true, className: "size-[72px] shrink-0" }))) : null, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [_jsxs("div", { className: "flex items-baseline gap-3", children: [_jsx(ctx.ItemH, { className: "text-body font-medium text-fg", children: item.name }), !photo ? _jsx("span", { "aria-hidden": true, className: "vb-leader min-w-6 flex-1 translate-y-[-0.3em] border-b border-dotted border-fg-subtle" }) : _jsx("span", { className: "flex-1" }), _jsx(Prices, { item: item, ctx: ctx })] }), item.description ? _jsx("p", { className: "max-w-[56ch] text-small text-fg-muted text-pretty", children: item.description }) : null, _jsx(Tags, { item: item, ctx: ctx }), _jsx(Allergens, { item: item, ctx: ctx })] })] }));
|
|
61
88
|
}
|
|
62
89
|
function ServiceRow({ item, ctx }) {
|
|
63
90
|
const href = item.bookHref ?? ctx.bookHref;
|
|
64
91
|
const name = textOf(item.name);
|
|
65
|
-
return (_jsxs("li", { "data-unavailable": item.unavailable ? "" : undefined, className: "grid gap-x-(--gutter) gap-y-2 border-b border-line py-5 first:border-t @2xl:grid-cols-12 @2xl:items-baseline", children: [_jsxs("div", { className: "flex flex-col gap-1.5 @2xl:col-span-6", children: [_jsx(
|
|
92
|
+
return (_jsxs("li", { "data-unavailable": item.unavailable ? "" : undefined, className: "grid gap-x-(--gutter) gap-y-2 border-b border-line py-5 first:border-t @2xl:grid-cols-12 @2xl:items-baseline", children: [_jsxs("div", { className: "flex flex-col gap-1.5 @2xl:col-span-6", children: [_jsx(ctx.ItemH, { className: "text-body font-medium text-fg", children: item.name }), item.description ? _jsx("p", { className: "text-small text-fg-muted text-pretty", children: item.description }) : null, _jsx(Tags, { item: item, ctx: ctx })] }), _jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.06em] text-fg-muted @2xl:col-span-2", children: item.prices.length === 1 ? item.duration : null }), _jsx("div", { className: "@2xl:col-span-3 @2xl:text-end", children: _jsx(Prices, { item: item, ctx: ctx }) }), _jsx("div", { className: "@2xl:col-span-1 @2xl:text-end", children: href && !item.unavailable ? (_jsx(BlockLink, { href: href, components: ctx.components, labels: ctx.labels, locale: ctx.locale, "aria-label": name ? ctx.t("bookService", { service: name }) : undefined, className: "inline-flex min-h-6 items-center text-small font-medium text-fg underline decoration-line underline-offset-[0.25em] hover:decoration-current focus-ring rounded-[2px] pointer-coarse:min-h-11", "data-track": "book", children: ctx.t("book") })) : null })] }));
|
|
66
93
|
}
|
|
67
94
|
/** schema.org `Menu` → `MenuSection` → `MenuItem` + `Offer`. */
|
|
68
95
|
export function menuJsonLd(props) {
|
|
@@ -77,7 +104,7 @@ export function menuJsonLd(props) {
|
|
|
77
104
|
name: textOf(i.name),
|
|
78
105
|
description: textOf(i.description) || undefined,
|
|
79
106
|
offers: i.prices.filter((p) => p.amount).map((p) => ({ "@type": "Offer", price: p.amount.amount, priceCurrency: p.amount.currency, name: textOf(p.label) || undefined })),
|
|
80
|
-
suitableForDiet: (i.tags ?? []).flatMap((tag) => (tag
|
|
107
|
+
suitableForDiet: (i.tags ?? []).flatMap((tag) => (DIET[tag] ? [DIET[tag]] : [])),
|
|
81
108
|
})),
|
|
82
109
|
})),
|
|
83
110
|
};
|
|
@@ -28,6 +28,7 @@ export interface NavbarProps extends Omit<BlockBaseProps, "variant" | "labels">
|
|
|
28
28
|
* rail: links inside one framed pill in the middle, "Sign in" + "Try free" at the end.
|
|
29
29
|
*/
|
|
30
30
|
variant?: NavbarVariant;
|
|
31
|
+
/** Logo image (`sizes="12rem"`; the mark is 1.75rem tall). */
|
|
31
32
|
logo?: SiteImage;
|
|
32
33
|
/** Shown instead of `logo` inside dark sections / dark mode. */
|
|
33
34
|
logoDark?: SiteImage;
|
|
@@ -8,6 +8,7 @@ import { BlockLink } from "../block-link.js";
|
|
|
8
8
|
import { SkipLink } from "../section.js";
|
|
9
9
|
import { NavbarSubmenu } from "./navbar-submenu.js";
|
|
10
10
|
import { CtaList, useBlockIds, siteText } from "./_kit-1.js";
|
|
11
|
+
import { TAP } from "./shared/tap.js";
|
|
11
12
|
/** National format in the number's own locale, international elsewhere ("0216 000 00 00" for tr). */
|
|
12
13
|
export function formatSitePhone(e164, locale = "en-US") {
|
|
13
14
|
const p = parsePhoneNumberFromString(e164);
|
|
@@ -34,16 +35,16 @@ export function Navbar({ variant = "bar", logo, logoDark, wordmark, homeHref = "
|
|
|
34
35
|
const lang = baseLanguage(locale);
|
|
35
36
|
const phoneText = phone ? formatSitePhone(phone, locale) : undefined;
|
|
36
37
|
const linkProps = { components, labels, locale };
|
|
37
|
-
const brand = (_jsxs(BlockLink, { href: homeHref, ...linkProps, className: "vb-nav-brand flex shrink-0 items-center gap-3 rounded-[2px] focus-ring", children: [logo ? (_jsxs(_Fragment, { children: [_jsx(BlockImage, { ...logo, decorative: !!wordmark || logo.decorative, alt: wordmark ? undefined : logo.alt, priority: true, className: cn("vb-nav-logo", logoDark && "vb-logo-light"), components: components }), logoDark ? _jsx(BlockImage, { ...logoDark, decorative: true, priority: true, className: "vb-nav-logo vb-logo-dark", components: components }) : null] })) : null, wordmark || !logo ? (_jsx("span", { className: cn("vb-nav-wordmark font-display text-fg", variant === "split" ? "text-[1.625rem]" : "text-[1.375rem]"), "data-field": editMode ? "props.wordmark" : undefined, children: wordmark })) : null] }));
|
|
38
|
+
const brand = (_jsxs(BlockLink, { href: homeHref, ...linkProps, className: "vb-nav-brand flex min-h-6 shrink-0 items-center gap-3 pointer-coarse:min-h-11 rounded-[2px] focus-ring", children: [logo ? (_jsxs(_Fragment, { children: [_jsx(BlockImage, { ...logo, decorative: !!wordmark || logo.decorative, alt: wordmark ? undefined : logo.alt, priority: true, sizes: "12rem", className: cn("vb-nav-logo", logoDark && "vb-logo-light"), components: components }), logoDark ? _jsx(BlockImage, { ...logoDark, decorative: true, priority: true, sizes: "12rem", className: "vb-nav-logo vb-logo-dark", components: components }) : null] })) : null, wordmark || !logo ? (_jsx("span", { className: cn("vb-nav-wordmark font-display text-fg", variant === "split" ? "text-[1.625rem]" : "text-[1.375rem]"), "data-field": editMode ? "props.wordmark" : undefined, children: wordmark })) : null] }));
|
|
38
39
|
const renderLink = (item, key, className) => (_jsx(BlockLink, { href: item.href, newTab: item.newTab, ...linkProps, "aria-current": isCurrent(item.href, currentPath) ? "page" : undefined, className: cn("vb-nav-link", className), children: item.label }, key));
|
|
39
40
|
const desktopItem = (item, i) => item.children?.length ? (_jsx("li", { children: _jsx(NavbarSubmenu, { label: item.label, active: inGroup(item, currentPath), wide: item.children.some((c) => c.description), children: _jsx("ul", { className: cn("grid gap-0.5", item.children.some((c) => c.description) && "grid-cols-2"), children: item.children.map((c, j) => (_jsx("li", { children: _jsxs(BlockLink, { href: c.href, newTab: c.newTab, ...linkProps, "aria-current": isCurrent(c.href, currentPath) ? "page" : undefined, className: "vb-nav-sublink flex flex-col gap-0.5 rounded-ui-sm px-3 py-2.5 text-sm text-fg transition-colors hover:bg-hover focus-ring", children: [_jsx("span", { className: "font-medium", children: c.label }), c.description ? _jsx("span", { className: "text-small leading-snug text-fg-muted", children: c.description }) : null] }) }, j))) }) }) }, i)) : (_jsx("li", { children: renderLink(item, i) }, i));
|
|
40
41
|
const linkList = (list, offset = 0, className) => (_jsx("ul", { className: cn("vb-nav-links hidden items-center @4xl:flex", variant === "rail" ? "gap-1" : "gap-7", className), children: list.map((it, i) => desktopItem(it, i + offset)) }));
|
|
41
|
-
const phoneLink = phone ? (_jsxs("a", { href: `tel:${phone}`, "aria-label": t("callUs", { phone: phoneText }), "data-track": "call", className: "vb-nav-phone hidden items-center gap-2 rounded-[2px] font-mono text-small tabular text-fg-muted transition-colors hover:text-fg focus-ring @6xl:inline-flex", children: [_jsx(Phone, { "aria-hidden": true, strokeWidth: 1.5, className: "size-3.5" }), phoneText] })) : null;
|
|
42
|
+
const phoneLink = phone ? (_jsxs("a", { href: `tel:${phone}`, "aria-label": t("callUs", { phone: phoneText }), "data-track": "call", className: "vb-nav-phone hidden min-h-6 items-center gap-2 rounded-[2px] font-mono text-small tabular text-fg-muted transition-colors hover:text-fg focus-ring @6xl:inline-flex", children: [_jsx(Phone, { "aria-hidden": true, strokeWidth: 1.5, className: "size-3.5" }), phoneText] })) : null;
|
|
42
43
|
const localeSwitch = locales?.length ? (_jsx("ul", { "aria-label": t("languages"), className: "hidden items-center gap-1 font-mono text-eyebrow uppercase tracking-[0.08em] @4xl:flex", children: locales.map((l, i) => {
|
|
43
44
|
const current = l.current ?? baseLanguage(l.code) === lang;
|
|
44
|
-
return (_jsxs("li", { className: "flex items-center gap-1", children: [i > 0 ? _jsx("span", { "aria-hidden": true, className: "text-fg-subtle", children: "/" }) : null, _jsx("a", { href: l.href, hrefLang: l.code, lang: l.code, "aria-current": current ? "true" : undefined, className: cn("rounded-[2px] px-0.5 transition-colors focus-ring", current ? "text-fg" : "text-fg-muted hover:text-fg"), children: l.code })] }, l.code));
|
|
45
|
+
return (_jsxs("li", { className: "flex items-center gap-1", children: [i > 0 ? _jsx("span", { "aria-hidden": true, className: "text-fg-subtle", children: "/" }) : null, _jsx("a", { href: l.href, hrefLang: l.code, lang: l.code, "aria-current": current ? "true" : undefined, className: cn("inline-flex min-h-6 min-w-6 items-center justify-center rounded-[2px] px-0.5 transition-colors focus-ring", current ? "text-fg" : "text-fg-muted hover:text-fg"), children: l.code })] }, l.code));
|
|
45
46
|
}) })) : null;
|
|
46
|
-
const trigger = (always) => (_jsx("button", { type: "button", popoverTarget: panelId, "aria-haspopup": "dialog", "aria-controls": panelId, className: cn("vb-nav-trigger inline-flex h-10 items-center gap-2 rounded-ui px-2 text-sm font-medium text-fg transition-colors hover:bg-hover focus-ring", !always && "@4xl:hidden"), children: always ? (_jsx("span", { className: "font-mono text-eyebrow uppercase tracking-[0.08em]", children: t("openMenu") })) : (_jsxs(_Fragment, { children: [_jsx(Menu, { "aria-hidden": true, strokeWidth: 1.5, className: "size-5" }), _jsx("span", { className: "sr-only", children: t("openMenu") })] })) }));
|
|
47
|
+
const trigger = (always) => (_jsx("button", { type: "button", popoverTarget: panelId, "aria-haspopup": "dialog", "aria-controls": panelId, className: cn("vb-nav-trigger inline-flex h-10 min-w-10 items-center justify-center gap-2 rounded-ui px-2 text-sm font-medium text-fg transition-colors hover:bg-hover focus-ring pointer-coarse:h-11 pointer-coarse:min-w-11", !always && "@4xl:hidden"), children: always ? (_jsx("span", { className: "font-mono text-eyebrow uppercase tracking-[0.08em]", children: t("openMenu") })) : (_jsxs(_Fragment, { children: [_jsx(Menu, { "aria-hidden": true, strokeWidth: 1.5, className: "size-5" }), _jsx("span", { className: "sr-only", children: t("openMenu") })] })) }));
|
|
47
48
|
const primaryAction = actions?.length ? _jsx(CtaList, { actions: actions, ...linkProps, size: "md", max: variant === "bar" || variant === "minimal" ? 1 : 2, className: "hidden flex-nowrap gap-x-2 @2xl:flex" }) : null;
|
|
48
49
|
let row;
|
|
49
50
|
if (variant === "split") {
|
|
@@ -60,5 +61,5 @@ export function Navbar({ variant = "bar", logo, logoDark, wordmark, homeHref = "
|
|
|
60
61
|
row = (_jsxs(_Fragment, { children: [brand, linkList(items, 0, "ms-4"), _jsxs("div", { className: "ms-auto flex items-center gap-5", children: [phoneLink, localeSwitch, primaryAction, trigger(false)] })] }));
|
|
61
62
|
}
|
|
62
63
|
const panelLinkSize = variant === "minimal" ? "font-display text-display-l" : "font-display text-display-m";
|
|
63
|
-
return (_jsxs("header", { id: anchor, "data-variant": variant, "data-sticky": sticky ? "" : undefined, "data-overlay": overlay ? "" : undefined, "data-block-id": editMode ? id : undefined, className: cn("vb-navbar", overlay && "dark", className), children: [skipLink ? _jsx(SkipLink, { labels: labels, locale: locale }) : null, _jsx("nav", { "aria-label": t("mainNavigation"), className: cn("vb-container vb-navbar-row items-center", variant === "split" ? "grid grid-cols-[1fr_auto_1fr] gap-6" : "flex gap-6"), children: row }), _jsx("div", { id: panelId, popover: "auto", role: "dialog", "aria-label": t("menuDialog"), className: "vb-nav-panel", children: _jsxs("div", { className: "vb-container flex min-h-full flex-col", children: [_jsxs("div", { className: "vb-navbar-row flex items-center justify-between gap-6", children: [brand, _jsxs("button", { type: "button", popoverTarget: panelId, popoverTargetAction: "hide", className: "inline-flex h-10 items-center gap-2 rounded-ui px-2 text-fg transition-colors hover:bg-hover focus-ring", children: [_jsx("span", { className: variant === "minimal" ? "font-mono text-eyebrow uppercase tracking-[0.08em]" : "sr-only", children: t("closeMenu") }), _jsx(X, { "aria-hidden": true, strokeWidth: 1.5, className: "size-5" })] })] }), _jsx("ul", { className: "flex flex-col py-[clamp(1.5rem,6vh,4rem)]", children: items.map((it, i) => (_jsxs("li", { className: "border-b border-line", children: [renderLink(it, i, cn("vb-nav-panel-link block py-3 text-fg", panelLinkSize)), it.children?.length ? (_jsx("ul", { className: "flex flex-wrap gap-x-6 gap-y-2 pb-4", children: it.children.map((c, j) => (_jsx("li", { children: renderLink(c, j, "text-small text-fg-muted hover:text-fg") }, j))) })) : null] }, i))) }), _jsxs("div", { className: "mt-auto flex flex-col gap-8 pb-[max(2rem,env(safe-area-inset-bottom))]", children: [actions?.length ? _jsx(CtaList, { actions: actions, ...linkProps, size: "lg" }) : null, phone || panelMeta?.length || locales?.length ? (_jsxs("dl", { className: "grid gap-x-8 gap-y-4 border-t border-line pt-6 text-small sm:grid-cols-3", children: [phone ? (_jsxs("div", { children: [_jsx("dt", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: t("phoneLabel") }), _jsx("dd", { className: "mt-1", children: _jsx("a", { href: `tel:${phone}`, "data-track": "call", className: "tabular text-fg underline decoration-line underline-offset-4 focus-ring", children: phoneText }) })] })) : null, panelMeta?.map((m, i) => (_jsxs("div", { children: [_jsx("dt", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: m.label }), _jsx("dd", { className: "mt-1 text-fg", children: m.value })] }, i))), locales?.length ? (_jsxs("div", { children: [_jsx("dt", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: t("languages") }), _jsx("dd", { className: "mt-1 flex gap-3", children: locales.map((l) => (_jsx("a", { href: l.href, hrefLang: l.code, lang: l.code, "aria-current": (l.current ?? baseLanguage(l.code) === lang) ? "true" : undefined, className: "font-mono uppercase text-fg-muted aria-[current]:text-fg focus-ring", children: l.code }, l.code))) })] })) : null] })) : null] })] }) })] }));
|
|
64
|
+
return (_jsxs("header", { id: anchor, "data-variant": variant, "data-sticky": sticky ? "" : undefined, "data-overlay": overlay ? "" : undefined, "data-block-id": editMode ? id : undefined, className: cn("vb-navbar", overlay && "dark", className), children: [skipLink ? _jsx(SkipLink, { labels: labels, locale: locale }) : null, _jsx("nav", { "aria-label": t("mainNavigation"), className: cn("vb-container vb-navbar-row items-center", variant === "split" ? "grid grid-cols-[1fr_auto_1fr] gap-6" : "flex gap-6"), children: row }), _jsx("div", { id: panelId, popover: "auto", role: "dialog", "aria-label": t("menuDialog"), className: "vb-nav-panel", children: _jsxs("div", { className: "vb-container flex min-h-full flex-col", children: [_jsxs("div", { className: "vb-navbar-row flex items-center justify-between gap-6", children: [brand, _jsxs("button", { type: "button", popoverTarget: panelId, popoverTargetAction: "hide", className: "inline-flex h-10 min-w-10 items-center justify-center gap-2 rounded-ui px-2 text-fg transition-colors hover:bg-hover focus-ring pointer-coarse:h-11 pointer-coarse:min-w-11", children: [_jsx("span", { className: variant === "minimal" ? "font-mono text-eyebrow uppercase tracking-[0.08em]" : "sr-only", children: t("closeMenu") }), _jsx(X, { "aria-hidden": true, strokeWidth: 1.5, className: "size-5" })] })] }), _jsx("ul", { className: "flex flex-col py-[clamp(1.5rem,6vh,4rem)]", children: items.map((it, i) => (_jsxs("li", { className: "border-b border-line", children: [renderLink(it, i, cn("vb-nav-panel-link block py-3 text-fg", panelLinkSize)), it.children?.length ? (_jsx("ul", { className: "flex flex-wrap gap-x-6 gap-y-2 pb-4 pointer-coarse:gap-y-0", children: it.children.map((c, j) => (_jsx("li", { children: renderLink(c, j, cn(TAP, "text-small text-fg-muted hover:text-fg")) }, j))) })) : null] }, i))) }), _jsxs("div", { className: "mt-auto flex flex-col gap-8 pb-[max(2rem,env(safe-area-inset-bottom))]", children: [actions?.length ? _jsx(CtaList, { actions: actions, ...linkProps, size: "lg" }) : null, phone || panelMeta?.length || locales?.length ? (_jsxs("dl", { className: "grid gap-x-8 gap-y-4 border-t border-line pt-6 text-small sm:grid-cols-3", children: [phone ? (_jsxs("div", { children: [_jsx("dt", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: t("phoneLabel") }), _jsx("dd", { className: "mt-1", children: _jsx("a", { href: `tel:${phone}`, "data-track": "call", className: cn(TAP, "tabular text-fg underline decoration-line underline-offset-4 focus-ring"), children: phoneText }) })] })) : null, panelMeta?.map((m, i) => (_jsxs("div", { children: [_jsx("dt", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: m.label }), _jsx("dd", { className: "mt-1 text-fg", children: m.value })] }, i))), locales?.length ? (_jsxs("div", { children: [_jsx("dt", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: t("languages") }), _jsx("dd", { className: "mt-1 flex gap-3 pointer-coarse:gap-1", children: locales.map((l) => (_jsx("a", { href: l.href, hrefLang: l.code, lang: l.code, "aria-current": (l.current ?? baseLanguage(l.code) === lang) ? "true" : undefined, className: cn(TAP, "min-w-6 justify-center font-mono uppercase text-fg-muted aria-[current]:text-fg focus-ring pointer-coarse:min-w-11"), children: l.code }, l.code))) })] })) : null] })) : null] })] }) })] }));
|
|
64
65
|
}
|
|
@@ -7,6 +7,7 @@ import { BlockHeading } from "../block-heading.js";
|
|
|
7
7
|
import { BlockLink } from "../block-link.js";
|
|
8
8
|
import { site2Text } from "./shared/labels-2.js";
|
|
9
9
|
import { fieldAttr, prettyPhone, telHref, useBlockIds2 } from "./shared/kit-2.js";
|
|
10
|
+
import { headingLines } from "./shared/heading-lines.js";
|
|
10
11
|
const KIND = {
|
|
11
12
|
"404": { code: "notFoundCode", title: "notFoundTitle", text: "notFoundText" },
|
|
12
13
|
"500": { code: "serverErrorCode", title: "serverErrorTitle", text: "serverErrorText" },
|
|
@@ -27,7 +28,7 @@ export function NotFound({ variant = "plain", kind = "404", title, text, links =
|
|
|
27
28
|
const { base, headingId } = useBlockIds2(id, "not-found");
|
|
28
29
|
const k = KIND[kind];
|
|
29
30
|
const showLinks = variant === "links";
|
|
30
|
-
return (_jsx(Section, { theme: theme, anchor: anchor, headingId: headingId, blockId: id, blockType: "not-found", editMode: editMode, className: cn("vb-not-found", className), "data-kind": kind, "data-variant": variant, children: _jsxs(Container, { className: "grid gap-x-(--gutter) gap-y-14 @4xl:grid-cols-12", children: [_jsxs("div", { className: "flex flex-col gap-6 @4xl:col-span-8", children: [_jsxs("p", { className: "flex items-center gap-3 font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: [_jsx("span", { className: "tabular text-fg", children: t(k.code) }), _jsx("span", { "aria-hidden": true, className: "h-px w-8 bg-current opacity-40" })] }), _jsx(BlockHeading, { id: headingId, level: headingLevel, size: "display-l", text: title ?? t(k.title), editMode: editMode, field: "props.title", labels: labels, locale: locale, className: "max-w-[20ch]" }), _jsx("p", { className: "max-w-[52ch] text-lead text-fg-muted text-pretty", ...fieldAttr(editMode, "text"), children: text ?? t(k.text) }), _jsxs("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-3 pt-2", children: [reset ? (_jsxs("button", { type: "button", onClick: reset, className: cn(buttonVariants({ variant: "primary", size: "lg" }), "vb-action-primary"), children: [_jsx(RotateCcw, { "aria-hidden": true, strokeWidth: 1.75 }), t("tryAgain")] })) : null, _jsxs(BlockLink, { href: homeHref, appearance: reset ? "link" : kind === "maintenance" && phone ? "secondary" : "primary", components: components, labels: labels, locale: locale, children: [t("backHome"), reset ? _jsx(ArrowRight, { "aria-hidden": true, strokeWidth: 1.5, className: "size-[1em] rtl:-scale-x-100" }) : null] }), kind === "maintenance" && phone ? (_jsx("a", { href: telHref(phone), className: cn(buttonVariants({ variant: "primary", size: "lg" }), "vb-action-primary tabular"), "data-track": "call", children: prettyPhone(phone) })) : null] })] }), showLinks ? (_jsxs("div", { className: "flex flex-col gap-10 border-t border-line pt-8 @4xl:col-span-12", children: [search ? (_jsxs("form", { role: "search", action: search.action, method: "get", className: "flex max-w-xl flex-col gap-2", children: [_jsx("label", { htmlFor: `${base}-q`, className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: t("searchSite") }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("span", { className: "relative flex-1", children: [_jsx(Search, { "aria-hidden": true, strokeWidth: 1.5, className: "pointer-events-none absolute start-3 top-1/2 size-4 -translate-y-1/2 text-fg-muted" }), _jsx("input", { id: `${base}-q`, type: "search", name: search.param ?? "q", placeholder: t("searchPlaceholder"), className: "h-12 w-full rounded-ui border border-field-border bg-field ps-9 pe-3 text-body text-fg placeholder:text-fg-subtle transition-colors hover:border-field-border-hover focus-ring" })] }), _jsx("button", { type: "submit", className: buttonVariants({ variant: "secondary", size: "lg" }), children: t("search") })] })] })) : null, links.length ? (_jsxs("nav", { "aria-labelledby": `${base}-pop`, className: "flex flex-col gap-4", children: [_jsx("p", { id: `${base}-pop`, className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: t("popularPages") }), _jsx("ul", { className: "grid gap-x-(--gutter) @2xl:grid-cols-2", children: links.slice(0, 6).map((l, i) => (_jsx("li", { className: "border-t border-line", children: _jsxs(BlockLink, { href: l.href, newTab: l.newTab, components: components, labels: labels, locale: locale, className: "group/nf flex items-center justify-between gap-4 py-4 text-body text-fg transition-colors hover:text-accent-ink focus-ring rounded-[2px]", children: [_jsx("span", { children: l.label }), _jsx(ArrowRight, { "aria-hidden": true, strokeWidth: 1.5, className: "size-4 text-fg-subtle transition-colors group-hover/nf:text-current rtl:-scale-x-100" })] }) }, i))) })] })) : null, phone && kind !== "maintenance" ? (_jsx("p", { className: "text-small text-fg-muted", children: t("stillStuck", { phone: "\u0000" })
|
|
31
|
+
return (_jsx(Section, { theme: theme, anchor: anchor, headingId: headingId, blockId: id, blockType: "not-found", editMode: editMode, className: cn("vb-not-found", className), "data-kind": kind, "data-variant": variant, children: _jsxs(Container, { className: "grid gap-x-(--gutter) gap-y-14 @4xl:grid-cols-12", children: [_jsxs("div", { className: "flex flex-col gap-6 @4xl:col-span-8", children: [_jsxs("p", { className: "flex items-center gap-3 font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: [_jsx("span", { className: "tabular text-fg", children: t(k.code) }), _jsx("span", { "aria-hidden": true, className: "h-px w-8 bg-current opacity-40" })] }), _jsx(BlockHeading, { id: headingId, level: headingLevel, size: "display-l", text: headingLines(title ?? t(k.title)), editMode: editMode, field: "props.title", labels: labels, locale: locale, className: "max-w-[20ch]" }), _jsx("p", { className: "max-w-[52ch] text-lead text-fg-muted text-pretty", ...fieldAttr(editMode, "text"), children: text ?? t(k.text) }), _jsxs("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-3 pt-2", children: [reset ? (_jsxs("button", { type: "button", onClick: reset, className: cn(buttonVariants({ variant: "primary", size: "lg" }), "vb-action-primary"), children: [_jsx(RotateCcw, { "aria-hidden": true, strokeWidth: 1.75 }), t("tryAgain")] })) : null, _jsxs(BlockLink, { href: homeHref, appearance: reset ? "link" : kind === "maintenance" && phone ? "secondary" : "primary", components: components, labels: labels, locale: locale, children: [t("backHome"), reset ? _jsx(ArrowRight, { "aria-hidden": true, strokeWidth: 1.5, className: "size-[1em] rtl:-scale-x-100" }) : null] }), kind === "maintenance" && phone ? (_jsx("a", { href: telHref(phone), className: cn(buttonVariants({ variant: "primary", size: "lg" }), "vb-action-primary tabular"), "data-track": "call", children: prettyPhone(phone) })) : null] })] }), showLinks ? (_jsxs("div", { className: "flex flex-col gap-10 border-t border-line pt-8 @4xl:col-span-12", children: [search ? (_jsxs("form", { role: "search", action: search.action, method: "get", className: "flex max-w-xl flex-col gap-2", children: [_jsx("label", { htmlFor: `${base}-q`, className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: t("searchSite") }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("span", { className: "relative flex-1", children: [_jsx(Search, { "aria-hidden": true, strokeWidth: 1.5, className: "pointer-events-none absolute start-3 top-1/2 size-4 -translate-y-1/2 text-fg-muted" }), _jsx("input", { id: `${base}-q`, type: "search", name: search.param ?? "q", placeholder: t("searchPlaceholder"), className: "h-12 w-full rounded-ui border border-field-border bg-field ps-9 pe-3 text-body text-fg placeholder:text-fg-subtle transition-colors hover:border-field-border-hover focus-ring" })] }), _jsx("button", { type: "submit", className: buttonVariants({ variant: "secondary", size: "lg" }), children: t("search") })] })] })) : null, links.length ? (_jsxs("nav", { "aria-labelledby": `${base}-pop`, className: "flex flex-col gap-4", children: [_jsx("p", { id: `${base}-pop`, className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: t("popularPages") }), _jsx("ul", { className: "grid gap-x-(--gutter) @2xl:grid-cols-2", children: links.slice(0, 6).map((l, i) => (_jsx("li", { className: "border-t border-line", children: _jsxs(BlockLink, { href: l.href, newTab: l.newTab, components: components, labels: labels, locale: locale, className: "group/nf flex items-center justify-between gap-4 py-4 text-body text-fg transition-colors hover:text-accent-ink focus-ring rounded-[2px]", children: [_jsx("span", { children: l.label }), _jsx(ArrowRight, { "aria-hidden": true, strokeWidth: 1.5, className: "size-4 text-fg-subtle transition-colors group-hover/nf:text-current rtl:-scale-x-100" })] }) }, i))) })] })) : null, phone && kind !== "maintenance" ? (_jsx("p", { className: "text-small text-fg-muted", children: t("stillStuck", { phone: "\u0000" })
|
|
31
32
|
.split("\u0000")
|
|
32
33
|
.flatMap((part, i) => i === 0
|
|
33
34
|
? [part]
|
|
@@ -20,6 +20,7 @@ export interface PageIntroProps extends Omit<BlockBaseProps, "variant" | "labels
|
|
|
20
20
|
/** The page `h1` (inner pages). One `*emphasis*`. */
|
|
21
21
|
title: React.ReactNode;
|
|
22
22
|
lead?: React.ReactNode;
|
|
23
|
+
/** 21:9 band under the intro. `sizes`: `min(100vw, 90rem)`. */
|
|
23
24
|
media?: SiteImage;
|
|
24
25
|
/** ruled: facts on the end side ("Service area" · "Asian side"), up to 3. */
|
|
25
26
|
meta?: SiteMeta[];
|
|
@@ -5,6 +5,7 @@ import { BlockHeading } from "../block-heading.js";
|
|
|
5
5
|
import { BlockImage } from "../block-image.js";
|
|
6
6
|
import { BlockLink } from "../block-link.js";
|
|
7
7
|
import { field, siteText, useBlockIds } from "./_kit-1.js";
|
|
8
|
+
import { headingLines } from "./shared/heading-lines.js";
|
|
8
9
|
/** `BreadcrumbList` JSON-LD for the crumbs (absolute URLs need `origin`). */
|
|
9
10
|
export function breadcrumbJsonLd(crumbs, origin = "") {
|
|
10
11
|
if (!crumbs.length)
|
|
@@ -31,7 +32,7 @@ export function PageIntro({ variant = "simple", breadcrumb, eyebrow, title, lead
|
|
|
31
32
|
const last = i === breadcrumb.length - 1;
|
|
32
33
|
return (_jsxs("li", { className: "flex items-center gap-2", children: [i > 0 ? (_jsx("span", { "aria-hidden": true, className: "text-fg-subtle/70 rtl:-scale-x-100", children: "/" })) : null, c.href && !last ? (_jsx(BlockLink, { href: c.href, ...linkProps, className: "rounded-[2px] transition-colors hover:text-fg focus-ring", children: c.label })) : (_jsx("span", { "aria-current": last ? "page" : undefined, className: last ? "text-fg-muted" : undefined, children: c.label }))] }, i));
|
|
33
34
|
}) }) })) : eyebrow ? (_jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", ...field(editMode, "eyebrow"), children: eyebrow })) : null;
|
|
34
|
-
const heading = _jsx(BlockHeading, { id: headingId, level: headingLevel, size: "display-l", text: title, editMode: editMode, field: "props.title", labels: labels, locale: locale, className: "max-w-[20ch]" });
|
|
35
|
+
const heading = _jsx(BlockHeading, { id: headingId, level: headingLevel, size: "display-l", text: headingLines(title), editMode: editMode, field: "props.title", labels: labels, locale: locale, className: "max-w-[20ch]" });
|
|
35
36
|
const leadEl = lead ? (_jsx("p", { className: "vb-lead max-w-[52ch] text-lead text-fg-muted", ...field(editMode, "lead"), children: lead })) : null;
|
|
36
37
|
let content;
|
|
37
38
|
if (variant === "ruled") {
|
|
@@ -8,6 +8,7 @@ import { BlockLink } from "../block-link.js";
|
|
|
8
8
|
import { RichText } from "../rich-text.js";
|
|
9
9
|
import { site2Text } from "./shared/labels-2.js";
|
|
10
10
|
import { fieldAttr, formatMoney, formatMoneyLong, useBlockIds2 } from "./shared/kit-2.js";
|
|
11
|
+
import { Reveal, Stagger } from "../../motion/reveal.js";
|
|
11
12
|
/**
|
|
12
13
|
* Plan pricing for SaaS subscriptions, agency packages and retainers. Money through `Intl` per
|
|
13
14
|
* locale and currency, a period suffix, a mandatory tax note. Server component; the
|
|
@@ -25,7 +26,7 @@ export function PricingTable({ variant = "columns", eyebrow, title, lead, billin
|
|
|
25
26
|
const ctx = { t, locale, billing, components, labels, editMode };
|
|
26
27
|
// One primary per block: the highlighted plan, else the first plan asking for it.
|
|
27
28
|
const primaryIndex = Math.max(0, plans.findIndex((p) => p.highlight) >= 0 ? plans.findIndex((p) => p.highlight) : plans.findIndex((p) => p.cta?.style === "primary"));
|
|
28
|
-
return (_jsx(Section, { theme: theme, anchor: anchor, headingId: title ? headingId : undefined, blockId: id, blockType: "pricing", editMode: editMode, className: cn("vb-pricing", className), "data-billing": billing, "data-variant": single ? "single" : variant, children: _jsxs(Container, { className: "flex flex-col gap-12 @4xl:gap-16", children: [title || eyebrow || lead || billing === "both" ? (_jsxs("div", { className: "flex flex-col gap-8 @4xl:flex-row @4xl:items-end @4xl:justify-between", children: [_jsx(SectionHeader, { eyebrow: eyebrow, title: title, lead: lead, headingId: headingId, headingLevel: headingLevel, editMode: editMode, labels: labels, locale: locale }), billing === "both" ? _jsx(BillingToggle, { name: `${base}-period`, defaultPeriod: defaultPeriod, saving: yearlySaving, t: t }) : null] })) : null, single ? (_jsx(SinglePlan, { plan: plans[0], ctx: ctx })) : variant === "rows" ? (_jsx("ol", { className: "flex flex-col border-b border-line", children: plans.map((plan, i) => (_jsx(PlanRow, { plan: plan, index: i, ctx: ctx, primary: i === primaryIndex }, i))) })) : (_jsx("ul", { className: "vb-plan-grid grid", "data-cols": Math.min(plans.length, 4), children: plans.map((plan, i) => (_jsx(PlanColumn, { plan: plan, index: i, ctx: ctx, primary: i === primaryIndex }, i))) })), taxNote || footnote ? (_jsxs("div", { className: "flex flex-col gap-2 border-t border-line pt-5 text-small text-fg-muted @2xl:flex-row @2xl:items-start @2xl:justify-between @2xl:gap-10", children: [taxNote ? (_jsx("p", { className: "shrink-0 font-mono text-eyebrow uppercase tracking-[0.08em]", ...fieldAttr(editMode, "taxNote"), children: taxNote })) : null, footnote ? (_jsx("div", { className: "max-w-(--measure) @2xl:text-end", ...fieldAttr(editMode, "footnote"), children: isRichDoc(footnote) ? _jsx(RichText, { doc: footnote, size: "sm", components: components, labels: labels, locale: locale }) : footnote })) : null] })) : null] }) }));
|
|
29
|
+
return (_jsx(Section, { theme: theme, anchor: anchor, headingId: title ? headingId : undefined, blockId: id, blockType: "pricing", editMode: editMode, className: cn("vb-pricing", className), "data-billing": billing, "data-variant": single ? "single" : variant, children: _jsxs(Container, { className: "flex flex-col gap-12 @4xl:gap-16", children: [title || eyebrow || lead || billing === "both" ? (_jsxs("div", { className: "flex flex-col gap-8 @4xl:flex-row @4xl:items-end @4xl:justify-between", children: [_jsx(SectionHeader, { eyebrow: eyebrow, title: title, lead: lead, headingId: headingId, headingLevel: headingLevel, editMode: editMode, labels: labels, locale: locale }), billing === "both" ? _jsx(BillingToggle, { name: `${base}-period`, defaultPeriod: defaultPeriod, saving: yearlySaving, t: t }) : null] })) : null, single ? (_jsx(Reveal, { disabled: editMode, children: _jsx(SinglePlan, { plan: plans[0], ctx: ctx }) })) : variant === "rows" ? (_jsx(Stagger, { asChild: true, effect: "fade", disabled: editMode, children: _jsx("ol", { className: "flex flex-col border-b border-line", children: plans.map((plan, i) => (_jsx(PlanRow, { plan: plan, index: i, ctx: ctx, primary: i === primaryIndex }, i))) }) })) : (_jsx(Stagger, { asChild: true, disabled: editMode, children: _jsx("ul", { className: "vb-plan-grid grid", "data-cols": Math.min(plans.length, 4), children: plans.map((plan, i) => (_jsx(PlanColumn, { plan: plan, index: i, ctx: ctx, primary: i === primaryIndex }, i))) }) })), taxNote || footnote ? (_jsxs("div", { className: "flex flex-col gap-2 border-t border-line pt-5 text-small text-fg-muted @2xl:flex-row @2xl:items-start @2xl:justify-between @2xl:gap-10", children: [taxNote ? (_jsx("p", { className: "shrink-0 font-mono text-eyebrow uppercase tracking-[0.08em]", ...fieldAttr(editMode, "taxNote"), children: taxNote })) : null, footnote ? (_jsx("div", { className: "max-w-(--measure) @2xl:text-end", ...fieldAttr(editMode, "footnote"), children: isRichDoc(footnote) ? _jsx(RichText, { doc: footnote, size: "sm", components: components, labels: labels, locale: locale }) : footnote })) : null] })) : null] }) }));
|
|
29
30
|
}
|
|
30
31
|
const isRichDoc = (v) => !!v && typeof v === "object" && v.type === "doc" && Array.isArray(v.content);
|
|
31
32
|
function BillingToggle({ name, defaultPeriod, saving, t }) {
|
|
@@ -8,6 +8,7 @@ export interface StepsItem {
|
|
|
8
8
|
text?: React.ReactNode;
|
|
9
9
|
/** "1–2 weeks", "45 min". */
|
|
10
10
|
duration?: React.ReactNode;
|
|
11
|
+
/** 3:2 step photo, max 28rem wide. `sizes`: `(min-width: 56rem) min(34vw, 26rem), min(100vw, 28rem)`. */
|
|
11
12
|
media?: SiteImage;
|
|
12
13
|
}
|
|
13
14
|
export interface StepsProps extends Omit<BlockBaseProps, "variant" | "labels"> {
|