@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.
Files changed (166) hide show
  1. package/dist/blocks/block-image.js +1 -1
  2. package/dist/blocks/block-link.js +1 -1
  3. package/dist/blocks/index.d.ts +2 -2
  4. package/dist/blocks/index.js +2 -2
  5. package/dist/blocks/site/_kit-1.d.ts +1 -0
  6. package/dist/blocks/site/_kit-1.js +3 -1
  7. package/dist/blocks/site/announcement-bar.js +1 -1
  8. package/dist/blocks/site/article-layout.d.ts +1 -0
  9. package/dist/blocks/site/article-layout.js +2 -1
  10. package/dist/blocks/site/blog-list.d.ts +1 -0
  11. package/dist/blocks/site/blog-list.js +3 -2
  12. package/dist/blocks/site/contact-dock.js +3 -1
  13. package/dist/blocks/site/contact-form.js +1 -1
  14. package/dist/blocks/site/cta.d.ts +2 -2
  15. package/dist/blocks/site/cta.js +8 -5
  16. package/dist/blocks/site/faq.js +2 -1
  17. package/dist/blocks/site/feature-grid.d.ts +1 -0
  18. package/dist/blocks/site/feature-grid.js +7 -5
  19. package/dist/blocks/site/footer.d.ts +1 -1
  20. package/dist/blocks/site/footer.js +8 -7
  21. package/dist/blocks/site/gallery-section.d.ts +6 -1
  22. package/dist/blocks/site/gallery-section.js +6 -4
  23. package/dist/blocks/site/hero.d.ts +3 -2
  24. package/dist/blocks/site/hero.js +14 -6
  25. package/dist/blocks/site/logo-strip.d.ts +6 -3
  26. package/dist/blocks/site/logo-strip.js +8 -7
  27. package/dist/blocks/site/map-hours.d.ts +2 -2
  28. package/dist/blocks/site/map-hours.js +4 -2
  29. package/dist/blocks/site/media-text.d.ts +4 -1
  30. package/dist/blocks/site/media-text.js +6 -4
  31. package/dist/blocks/site/menu-list.d.ts +7 -3
  32. package/dist/blocks/site/menu-list.js +34 -7
  33. package/dist/blocks/site/navbar.d.ts +1 -0
  34. package/dist/blocks/site/navbar.js +6 -5
  35. package/dist/blocks/site/not-found.js +2 -1
  36. package/dist/blocks/site/page-intro.d.ts +1 -0
  37. package/dist/blocks/site/page-intro.js +2 -1
  38. package/dist/blocks/site/pricing-table.js +2 -1
  39. package/dist/blocks/site/process.d.ts +1 -0
  40. package/dist/blocks/site/process.js +4 -2
  41. package/dist/blocks/site/registry-1.d.ts +1 -1
  42. package/dist/blocks/site/registry-2.d.ts +5 -0
  43. package/dist/blocks/site/registry-2.js +2 -2
  44. package/dist/blocks/site/rich-text-section.js +2 -1
  45. package/dist/blocks/site/shared/heading-lines.d.ts +9 -0
  46. package/dist/blocks/site/shared/heading-lines.js +32 -0
  47. package/dist/blocks/site/shared/image-sizes.d.ts +25 -0
  48. package/dist/blocks/site/shared/image-sizes.js +37 -0
  49. package/dist/blocks/site/shared/island-keys.d.ts +1 -1
  50. package/dist/blocks/site/shared/island-keys.js +1 -1
  51. package/dist/blocks/site/shared/labels-2.d.ts +9 -1
  52. package/dist/blocks/site/shared/labels-2.js +24 -6
  53. package/dist/blocks/site/shared/open-now.d.ts +1 -1
  54. package/dist/blocks/site/shared/open-now.js +1 -1
  55. package/dist/blocks/site/shared/opening-hours.d.ts +18 -2
  56. package/dist/blocks/site/shared/opening-hours.js +36 -7
  57. package/dist/blocks/site/shared/tap.d.ts +9 -0
  58. package/dist/blocks/site/shared/tap.js +16 -0
  59. package/dist/blocks/site/site-shell.d.ts +11 -0
  60. package/dist/blocks/site/site-shell.js +43 -16
  61. package/dist/blocks/site/stats-band.d.ts +3 -1
  62. package/dist/blocks/site/stats-band.js +11 -7
  63. package/dist/blocks/site/team.d.ts +1 -1
  64. package/dist/blocks/site/team.js +4 -3
  65. package/dist/blocks/site/testimonials.d.ts +2 -2
  66. package/dist/blocks/site/testimonials.js +5 -4
  67. package/dist/business/index.d.ts +1 -0
  68. package/dist/business/index.js +2 -0
  69. package/dist/business/signer-status.d.ts +53 -0
  70. package/dist/business/signer-status.js +76 -0
  71. package/dist/charts/bar-chart.js +6 -2
  72. package/dist/charts/chart-shell.d.ts +9 -2
  73. package/dist/charts/chart-shell.js +10 -4
  74. package/dist/charts/chart.js +1 -1
  75. package/dist/charts/combo-chart.js +5 -7
  76. package/dist/charts/utils.d.ts +10 -0
  77. package/dist/charts/utils.js +46 -0
  78. package/dist/components/_qr-core.d.ts +26 -0
  79. package/dist/components/_qr-core.js +376 -0
  80. package/dist/components/activity-timeline.js +5 -5
  81. package/dist/components/aspect-ratio.d.ts +26 -0
  82. package/dist/components/aspect-ratio.js +42 -0
  83. package/dist/components/backup-codes.d.ts +42 -0
  84. package/dist/components/backup-codes.js +67 -0
  85. package/dist/components/breadcrumb.d.ts +2 -2
  86. package/dist/components/breadcrumb.js +1 -1
  87. package/dist/components/carousel.d.ts +12 -3
  88. package/dist/components/carousel.js +8 -5
  89. package/dist/components/collapsible.d.ts +36 -0
  90. package/dist/components/collapsible.js +37 -0
  91. package/dist/components/command.d.ts +8 -2
  92. package/dist/components/command.js +7 -2
  93. package/dist/components/image.d.ts +18 -2
  94. package/dist/components/image.js +12 -5
  95. package/dist/components/input.d.ts +1 -1
  96. package/dist/components/input.js +1 -1
  97. package/dist/components/menubar.d.ts +96 -0
  98. package/dist/components/menubar.js +123 -0
  99. package/dist/components/pin-map.d.ts +58 -0
  100. package/dist/components/pin-map.js +167 -0
  101. package/dist/components/placeholder-art.d.ts +51 -0
  102. package/dist/components/placeholder-art.js +82 -0
  103. package/dist/components/qr-code.d.ts +36 -0
  104. package/dist/components/qr-code.js +27 -0
  105. package/dist/components/record-detail-layout.d.ts +2 -2
  106. package/dist/components/record-detail-layout.js +1 -1
  107. package/dist/components/sidebar.js +8 -8
  108. package/dist/components/sla-timer.d.ts +75 -0
  109. package/dist/components/sla-timer.js +142 -0
  110. package/dist/components/slug-input.js +1 -1
  111. package/dist/components/tour.d.ts +60 -0
  112. package/dist/components/tour.js +139 -0
  113. package/dist/index.d.ts +9 -0
  114. package/dist/index.js +9 -0
  115. package/dist/lib/public.d.ts +1 -0
  116. package/dist/lib/public.js +1 -0
  117. package/dist/lib/value-format.js +1 -1
  118. package/dist/locale-data/core/holidays.d.ts +1 -1
  119. package/dist/locale-data/core/holidays.js +1 -1
  120. package/dist/locales/ar.d.ts +2087 -0
  121. package/dist/locales/ar.js +2088 -0
  122. package/dist/locales/de.d.ts +2087 -0
  123. package/dist/locales/de.js +2088 -0
  124. package/dist/locales/en.d.ts +1 -0
  125. package/dist/locales/en.js +1 -0
  126. package/dist/locales/es.d.ts +2087 -0
  127. package/dist/locales/es.js +2087 -0
  128. package/dist/locales/fr.d.ts +2087 -0
  129. package/dist/locales/fr.js +2088 -0
  130. package/dist/locales/tr.d.ts +1 -0
  131. package/dist/locales/tr.js +1 -0
  132. package/dist/media/gallery.js +3 -1
  133. package/dist/media/media-library.js +1 -1
  134. package/dist/motion/core.d.ts +32 -0
  135. package/dist/motion/core.js +77 -0
  136. package/dist/motion/count-up.d.ts +29 -0
  137. package/dist/motion/count-up.js +92 -0
  138. package/dist/motion/index.d.ts +11 -0
  139. package/dist/motion/index.js +13 -0
  140. package/dist/motion/motion-scope.d.ts +17 -0
  141. package/dist/motion/motion-scope.js +16 -0
  142. package/dist/motion/page-transition.d.ts +33 -0
  143. package/dist/motion/page-transition.js +61 -0
  144. package/dist/motion/parallax.d.ts +37 -0
  145. package/dist/motion/parallax.js +90 -0
  146. package/dist/motion/pointer.d.ts +38 -0
  147. package/dist/motion/pointer.js +117 -0
  148. package/dist/motion/reveal.d.ts +72 -0
  149. package/dist/motion/reveal.js +133 -0
  150. package/dist/motion/scroll-shrink.d.ts +18 -0
  151. package/dist/motion/scroll-shrink.js +47 -0
  152. package/dist/motion/skeleton-swap.d.ts +17 -0
  153. package/dist/motion/skeleton-swap.js +43 -0
  154. package/dist/motion/text-reveal.d.ts +32 -0
  155. package/dist/motion/text-reveal.js +31 -0
  156. package/dist/motion/ticker.d.ts +35 -0
  157. package/dist/motion/ticker.js +16 -0
  158. package/dist/render/block-renderer.js +1 -1
  159. package/dist/sortable/sortable-tree.d.ts +4 -0
  160. package/dist/sortable/sortable-tree.js +16 -4
  161. package/dist/styles/blocks.css +21 -12
  162. package/dist/styles/editors.css +2 -1
  163. package/dist/styles/media.css +31 -5
  164. package/dist/styles/motion.css +389 -0
  165. package/dist/styles.css +41 -10
  166. package/package.json +5 -1
@@ -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: "100vw", components: components, labels: labels, editMode: editMode, field: "props.media" }) })) : null] }));
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: "(min-width: 56rem) 42vw, 100vw", 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] }));
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: "100vw", 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, children: eyebrow }), _jsx(Title, { p: p, size: "display-xl" }), _jsx(Lead, { editMode: editMode, className: "max-w-[52ch] text-fg", children: lead }), _jsx(CtaList, { actions: actions, components: components, labels: labels, locale: locale, editMode: editMode, className: "pt-2" })] }), aside ? (_jsxs("aside", { className: "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", ...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] })] }));
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: "min(100vw, 90rem)", components: components, labels: labels }) })) : null] }));
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; static under reduced motion.
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 CSS keyframes (paused on
36
- * hover, focus and reduced motion; the duplicate copy is `aria-hidden`).
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 CSS keyframes (paused on
9
- * hover, focus and reduced motion; the duplicate copy is `aria-hidden`).
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, hidden) => {
20
- const img = (_jsxs(_Fragment, { children: [_jsx(BlockImage, { ...l.image, alt: hidden ? undefined : l.name, decorative: hidden, fit: "contain", className: cn("vb-logo", l.imageDark && "vb-logo-light"), components: components }), l.imageDark ? _jsx(BlockImage, { ...l.imageDark, decorative: true, fit: "contain", className: "vb-logo vb-logo-dark", components: components }) : null] }));
21
- const inner = l.link && !hidden ? (_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);
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("div", { className: "vb-marquee", style: { "--vb-marquee-duration": `${speed}s` }, children: _jsxs("ul", { className: "vb-marquee-track", children: [logos.map((l, i) => logo(l, i)), logos.map((l, i) => logo(l, i, true))] }) })] })) : 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
+ 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 now · closes at 22:00" island. Default true. */
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: "(min-width: 56rem) 58vw, 100vw", 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;
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
- /** The image (also the video poster). */
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("(min-width: 56rem) 60vw, 100vw", "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 })] }));
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("(min-width: 56rem) 60vw, 100vw") }), _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 })] }));
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("(min-width: 56rem) 40vw, 100vw", "rounded-ui") }), _jsx("div", { className: cn("@4xl:col-span-6", end ? "@4xl:order-1 @4xl:col-start-1" : "@4xl:col-start-7"), children: text })] }));
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("div", { className: "vb-reveal", children: content }) }));
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
- export type MenuTag = "vegetarian" | "vegan" | "gluten-free" | "spicy" | "new" | "chef-pick";
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 QR-menu page. Category jump bar with plain
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 = { vegetarian: "tagVegetarian", vegan: "tagVegan", "gluten-free": "tagGlutenFree", spicy: "tagSpicy", new: "tagNew", "chef-pick": "tagChefPick" };
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 QR-menu page. Category jump bar with plain
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
- const ctx = { t, locale, components, labels, editMode, variant, bookHref, showAllergens };
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("h3", { 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));
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("h4", { 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 })] })] }));
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("h4", { 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: "text-small font-medium text-fg underline decoration-line underline-offset-[0.25em] hover:decoration-current focus-ring rounded-[2px]", "data-track": "book", children: ctx.t("book") })) : null })] }));
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 === "vegan" ? ["https://schema.org/VeganDiet"] : tag === "vegetarian" ? ["https://schema.org/VegetarianDiet"] : tag === "gluten-free" ? ["https://schema.org/GlutenFreeDiet"] : [])),
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"> {