@vamos-ui/react 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blocks/block-image.js +1 -1
- package/dist/blocks/block-link.js +1 -1
- package/dist/blocks/index.d.ts +2 -2
- package/dist/blocks/index.js +2 -2
- package/dist/blocks/site/_kit-1.js +1 -1
- package/dist/blocks/site/announcement-bar.js +1 -1
- package/dist/blocks/site/article-layout.d.ts +1 -0
- package/dist/blocks/site/article-layout.js +2 -1
- package/dist/blocks/site/blog-list.d.ts +1 -0
- package/dist/blocks/site/blog-list.js +3 -2
- package/dist/blocks/site/contact-dock.js +3 -1
- package/dist/blocks/site/contact-form.js +1 -1
- package/dist/blocks/site/cta.d.ts +2 -2
- package/dist/blocks/site/cta.js +6 -4
- package/dist/blocks/site/feature-grid.d.ts +1 -0
- package/dist/blocks/site/feature-grid.js +5 -4
- package/dist/blocks/site/footer.d.ts +1 -1
- package/dist/blocks/site/footer.js +8 -7
- package/dist/blocks/site/gallery-section.d.ts +6 -1
- package/dist/blocks/site/gallery-section.js +5 -4
- package/dist/blocks/site/hero.d.ts +3 -2
- package/dist/blocks/site/hero.js +7 -5
- package/dist/blocks/site/logo-strip.d.ts +1 -0
- package/dist/blocks/site/logo-strip.js +1 -1
- package/dist/blocks/site/map-hours.d.ts +2 -2
- package/dist/blocks/site/map-hours.js +4 -2
- package/dist/blocks/site/media-text.d.ts +4 -1
- package/dist/blocks/site/media-text.js +4 -3
- package/dist/blocks/site/menu-list.d.ts +7 -3
- package/dist/blocks/site/menu-list.js +34 -7
- package/dist/blocks/site/navbar.d.ts +1 -0
- package/dist/blocks/site/navbar.js +6 -5
- package/dist/blocks/site/not-found.js +2 -1
- package/dist/blocks/site/page-intro.d.ts +1 -0
- package/dist/blocks/site/page-intro.js +2 -1
- package/dist/blocks/site/process.d.ts +1 -0
- package/dist/blocks/site/process.js +2 -1
- package/dist/blocks/site/registry-1.d.ts +1 -1
- package/dist/blocks/site/registry-2.d.ts +5 -0
- package/dist/blocks/site/registry-2.js +2 -2
- package/dist/blocks/site/rich-text-section.js +2 -1
- package/dist/blocks/site/shared/heading-lines.d.ts +9 -0
- package/dist/blocks/site/shared/heading-lines.js +32 -0
- package/dist/blocks/site/shared/image-sizes.d.ts +25 -0
- package/dist/blocks/site/shared/image-sizes.js +37 -0
- package/dist/blocks/site/shared/island-keys.d.ts +1 -1
- package/dist/blocks/site/shared/island-keys.js +1 -1
- package/dist/blocks/site/shared/labels-2.d.ts +9 -1
- package/dist/blocks/site/shared/labels-2.js +24 -6
- package/dist/blocks/site/shared/open-now.d.ts +1 -1
- package/dist/blocks/site/shared/open-now.js +1 -1
- package/dist/blocks/site/shared/opening-hours.d.ts +18 -2
- package/dist/blocks/site/shared/opening-hours.js +36 -7
- package/dist/blocks/site/shared/tap.d.ts +9 -0
- package/dist/blocks/site/shared/tap.js +16 -0
- package/dist/blocks/site/team.d.ts +1 -1
- package/dist/blocks/site/team.js +4 -3
- package/dist/blocks/site/testimonials.d.ts +2 -2
- package/dist/blocks/site/testimonials.js +3 -3
- package/dist/charts/bar-chart.js +1 -1
- package/dist/charts/chart-shell.d.ts +9 -2
- package/dist/charts/chart-shell.js +10 -4
- package/dist/charts/chart.js +1 -1
- package/dist/charts/combo-chart.js +5 -7
- package/dist/charts/utils.d.ts +10 -0
- package/dist/charts/utils.js +46 -0
- package/dist/components/activity-timeline.js +5 -5
- package/dist/components/aspect-ratio.d.ts +26 -0
- package/dist/components/aspect-ratio.js +42 -0
- package/dist/components/carousel.d.ts +12 -3
- package/dist/components/carousel.js +8 -5
- package/dist/components/collapsible.d.ts +36 -0
- package/dist/components/collapsible.js +37 -0
- package/dist/components/image.d.ts +18 -2
- package/dist/components/image.js +12 -5
- package/dist/components/menubar.d.ts +96 -0
- package/dist/components/menubar.js +123 -0
- package/dist/components/placeholder-art.d.ts +51 -0
- package/dist/components/placeholder-art.js +82 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/lib/value-format.js +1 -1
- package/dist/locales/ar.d.ts +2087 -0
- package/dist/locales/ar.js +2088 -0
- package/dist/locales/de.d.ts +2087 -0
- package/dist/locales/de.js +2088 -0
- package/dist/locales/en.d.ts +1 -0
- package/dist/locales/en.js +1 -0
- package/dist/locales/es.d.ts +2087 -0
- package/dist/locales/es.js +2087 -0
- package/dist/locales/fr.d.ts +2087 -0
- package/dist/locales/fr.js +2088 -0
- package/dist/locales/tr.d.ts +1 -0
- package/dist/locales/tr.js +1 -0
- package/dist/media/media-library.js +1 -1
- package/dist/motion/text-reveal.d.ts +3 -1
- package/dist/motion/text-reveal.js +5 -3
- package/dist/render/block-renderer.js +1 -1
- package/dist/styles/blocks.css +18 -9
- package/dist/styles/editors.css +2 -1
- package/dist/styles/media.css +31 -5
- package/dist/styles.css +39 -9
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -19,7 +19,7 @@ export function LogoStrip({ variant = "row", caption, logos, monochrome = true,
|
|
|
19
19
|
const t = siteText(labels, locale);
|
|
20
20
|
const marquee = variant === "marquee" && logos.length >= 4;
|
|
21
21
|
const logo = (l, i) => {
|
|
22
|
-
const img = (_jsxs(_Fragment, { children: [_jsx(BlockImage, { ...l.image, alt: l.name, 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] }));
|
|
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
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));
|
|
24
24
|
};
|
|
25
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;
|
|
@@ -15,7 +15,7 @@ export interface Branch {
|
|
|
15
15
|
phone?: string;
|
|
16
16
|
email?: string;
|
|
17
17
|
hours: OpeningHours;
|
|
18
|
-
/** Static map picture for the facade (greyscale, an accent pin on top). */
|
|
18
|
+
/** Static map picture for the facade (greyscale, an accent pin on top). `sizes`: `(min-width: 56rem) min(59vw, 45rem), 100vw`. */
|
|
19
19
|
mapImage?: SiteImage2;
|
|
20
20
|
/** Google Business Profile / place link. */
|
|
21
21
|
placeUrl?: string;
|
|
@@ -40,7 +40,7 @@ export interface MapHoursProps extends Omit<BlockBaseProps, "variant" | "labels"
|
|
|
40
40
|
mapProvider?: "google" | "osm" | "none";
|
|
41
41
|
/** Direction links. Default google · apple (Yandex is common in Turkey, Waze elsewhere). */
|
|
42
42
|
directions?: DirectionsProvider[];
|
|
43
|
-
/** "Open
|
|
43
|
+
/** "Open · until 10 PM" island ("Closing soon · open until 10 PM", "Closed · opens tomorrow at 12 PM"). Default true. */
|
|
44
44
|
showOpenNow?: boolean;
|
|
45
45
|
/**
|
|
46
46
|
* Weekly hours: `week` (default) lists all seven days, `open-days` hides closed days (a venue
|
|
@@ -10,6 +10,8 @@ import { OpenNow } from "./shared/open-now.js";
|
|
|
10
10
|
import { ConsentGate } from "./shared/consent-gate.js";
|
|
11
11
|
import { openNowLabelKeys } from "./shared/island-keys.js";
|
|
12
12
|
import { BrandIcon, BRAND_NAMES } from "./shared/brand-icons.js";
|
|
13
|
+
import { columnSizes } from "./shared/image-sizes.js";
|
|
14
|
+
import { TAP } from "./shared/tap.js";
|
|
13
15
|
const DIRECTION_BRAND = { google: "google-maps", apple: "apple-maps", yandex: "yandex-maps", waze: "waze" };
|
|
14
16
|
const query = (b) => (b.address.geo ? `${b.address.geo.lat},${b.address.geo.lng}` : [b.name, ...addressLines(b.address, { withCountry: true })].join(", "));
|
|
15
17
|
/** Direction URL for a provider (no API keys). */
|
|
@@ -87,7 +89,7 @@ export function MapHours({ variant: variantProp = "map-split", eyebrow, title, l
|
|
|
87
89
|
}
|
|
88
90
|
function MapFacade({ branch, ctx }) {
|
|
89
91
|
const src = ctx.mapProvider !== "none" ? mapEmbedUrl(branch, ctx.mapProvider) : undefined;
|
|
90
|
-
const preview = branch.mapImage?.src ? (_jsxs(_Fragment, { children: [_jsx(BlockImage, { ...branch.mapImage, decorative: true, aspect: "4:3", sizes:
|
|
92
|
+
const preview = branch.mapImage?.src ? (_jsxs(_Fragment, { children: [_jsx(BlockImage, { ...branch.mapImage, decorative: true, aspect: "4:3", sizes: columnSizes(7 / 12), className: "size-full", imgClassName: "vb-map-img", components: ctx.components }), _jsx("span", { "aria-hidden": true, className: "vb-map-pin absolute start-1/2 top-1/2 -translate-x-1/2 -translate-y-full text-accent rtl:translate-x-1/2", children: _jsxs("svg", { viewBox: "0 0 32 40", className: "h-10 w-8 drop-shadow-sm", children: [_jsx("path", { d: "M16 0C7.16 0 0 7.16 0 16c0 11.2 14.4 23.2 15.04 23.72a1.5 1.5 0 0 0 1.92 0C17.6 39.2 32 27.2 32 16 32 7.16 24.84 0 16 0Z", fill: "currentColor" }), _jsx("circle", { cx: "16", cy: "16", r: "6", fill: "var(--accent-fg)" })] }) })] })) : null;
|
|
91
93
|
if (!src)
|
|
92
94
|
return preview ? _jsx("div", { className: "vb-gate relative aspect-[4/3] overflow-hidden rounded-ui bg-surface-2", children: preview }) : null;
|
|
93
95
|
return (_jsx(ConsentGate, { src: src, title: ctx.t("mapOf", { name: branch.name }), loadLabel: ctx.t("loadMap"), notice: ctx.t("mapNotice", { provider: ctx.mapProvider === "osm" ? "OpenStreetMap" : "Google Maps" }), rememberLabel: ctx.t("rememberChoice"), category: "functional", children: preview }));
|
|
@@ -117,7 +119,7 @@ function BranchDetails({ branch, ctx, index }) {
|
|
|
117
119
|
function InfoColumns({ branch, ctx }) {
|
|
118
120
|
const summary = summarizeHours(branch.hours, ctx.locale).filter((s) => ctx.hoursDisplay !== "open-days" || !s.closed);
|
|
119
121
|
const showHours = ctx.hoursDisplay !== "none" || ctx.showOpenNow;
|
|
120
|
-
return (_jsxs("div", { className: cn("grid gap-x-(--gutter) gap-y-10 border-t border-line pt-8", showHours ? "@2xl:grid-cols-3" : "@2xl:grid-cols-2"), children: [_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: ctx.t("address") }), _jsx(AddressBlock, { branch: branch, ctx: ctx, large: true }), branch.notes ? _jsx("p", { className: "text-small text-fg-muted", children: branch.notes }) : null, ctx.directions[0] ? (_jsxs("a", { href: directionsUrl(ctx.directions[0], branch), target: "_blank", rel: "noopener noreferrer", className: "mt-1 self-start text-small font-medium text-fg underline decoration-line underline-offset-[0.25em] hover:decoration-current focus-ring rounded-[2px]", "data-track": `directions:${ctx.directions[0]}`, children: [ctx.t("directionsVia", { provider: BRAND_NAMES[DIRECTION_BRAND[ctx.directions[0]]] }), _jsxs("span", { className: "sr-only", children: [" ", ctx.t("opensInNewTab")] })] })) : null] }), showHours ? (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("div", { className: "flex flex-wrap items-baseline justify-between gap-3", children: _jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: ctx.t("hours") }) }), ctx.hoursDisplay !== "none" ? (_jsx("ul", { className: "flex flex-col gap-1.5 text-lead", children: summary.map((s, i) => (_jsxs("li", { className: "flex justify-between gap-6 tabular", children: [_jsx("span", { className: "text-fg-muted", children: s.days }), _jsx("span", { className: s.closed ? "text-fg-subtle" : "text-fg", children: s.closed ? ctx.t("closedDay") : s.text })] }, i))) })) : null, ctx.showOpenNow ? _jsx(OpenNow, { hours: branch.hours, labels: ctx.openNow, locale: ctx.locale, now: ctx.now, className: "mt-2" }) : null] })) : null, _jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: ctx.t("contactDetails") }), branch.phone ? (_jsx("a", { href: telHref(branch.phone), className: "self-start text-lead font-medium tabular text-fg hover:text-accent-ink focus-ring rounded-[2px]", "data-track": "call", children: prettyPhone(branch.phone) })) : null, branch.email ? (_jsx("a", { href: `mailto:${branch.email}`, className: "self-start text-fg underline decoration-line underline-offset-[0.25em] hover:decoration-current focus-ring rounded-[2px]", "data-track": "email", children: branch.email })) : null] })] }));
|
|
122
|
+
return (_jsxs("div", { className: cn("grid gap-x-(--gutter) gap-y-10 border-t border-line pt-8", showHours ? "@2xl:grid-cols-3" : "@2xl:grid-cols-2"), children: [_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: ctx.t("address") }), _jsx(AddressBlock, { branch: branch, ctx: ctx, large: true }), branch.notes ? _jsx("p", { className: "text-small text-fg-muted", children: branch.notes }) : null, ctx.directions[0] ? (_jsxs("a", { href: directionsUrl(ctx.directions[0], branch), target: "_blank", rel: "noopener noreferrer", className: "mt-1 inline-flex min-h-6 items-center self-start pointer-coarse:min-h-11 text-small font-medium text-fg underline decoration-line underline-offset-[0.25em] hover:decoration-current focus-ring rounded-[2px]", "data-track": `directions:${ctx.directions[0]}`, children: [ctx.t("directionsVia", { provider: BRAND_NAMES[DIRECTION_BRAND[ctx.directions[0]]] }), _jsxs("span", { className: "sr-only", children: [" ", ctx.t("opensInNewTab")] })] })) : null] }), showHours ? (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("div", { className: "flex flex-wrap items-baseline justify-between gap-3", children: _jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: ctx.t("hours") }) }), ctx.hoursDisplay !== "none" ? (_jsx("ul", { className: "flex flex-col gap-1.5 text-lead", children: summary.map((s, i) => (_jsxs("li", { className: "flex justify-between gap-6 tabular", children: [_jsx("span", { className: "text-fg-muted", children: s.days }), _jsx("span", { className: s.closed ? "text-fg-subtle" : "text-fg", children: s.closed ? ctx.t("closedDay") : s.text })] }, i))) })) : null, ctx.showOpenNow ? _jsx(OpenNow, { hours: branch.hours, labels: ctx.openNow, locale: ctx.locale, now: ctx.now, className: "mt-2" }) : null] })) : null, _jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: ctx.t("contactDetails") }), branch.phone ? (_jsx("a", { href: telHref(branch.phone), className: cn(TAP, "self-start text-lead font-medium tabular text-fg hover:text-accent-ink focus-ring rounded-[2px]"), "data-track": "call", children: prettyPhone(branch.phone) })) : null, branch.email ? (_jsx("a", { href: `mailto:${branch.email}`, className: cn(TAP, "self-start text-fg underline decoration-line underline-offset-[0.25em] hover:decoration-current focus-ring rounded-[2px]"), "data-track": "email", children: branch.email })) : null] })] }));
|
|
121
123
|
}
|
|
122
124
|
function BranchPicker({ branches, ctx, name }) {
|
|
123
125
|
const list = branches.slice(0, 12);
|
|
@@ -22,7 +22,10 @@ export interface MediaTextProps extends Omit<BlockBaseProps, "variant" | "labels
|
|
|
22
22
|
/** Short points with accent dashes (≤ 6). */
|
|
23
23
|
bullets?: React.ReactNode[];
|
|
24
24
|
cta?: SiteCta;
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* The image (also the video poster). `sizes`: side `(min-width: 56rem) min(42vw, 32rem), 100vw` ·
|
|
27
|
+
* overlap `min(67vw, 51rem)` · bleed `59vw` (runs to the section edge); 100vw on narrow screens.
|
|
28
|
+
*/
|
|
26
29
|
media: SiteImage;
|
|
27
30
|
/** Static preview → plays only after a click (YouTube via youtube-nocookie). */
|
|
28
31
|
video?: MediaTextVideo;
|
|
@@ -8,6 +8,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
10
|
import { Reveal } from "../../motion/reveal.js";
|
|
11
|
+
import { columnSizes } from "./shared/image-sizes.js";
|
|
11
12
|
/** Embed URL for a video facade (YouTube → youtube-nocookie, Vimeo → player). */
|
|
12
13
|
export function videoEmbedUrl(v) {
|
|
13
14
|
if (v.provider === "youtube") {
|
|
@@ -44,13 +45,13 @@ export function MediaText({ variant = "side", eyebrow, title, body, bullets, cta
|
|
|
44
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] }));
|
|
45
46
|
let content;
|
|
46
47
|
if (variant === "overlap") {
|
|
47
|
-
content = (_jsxs(Container, { className: "grid gap-y-0 @4xl:grid-cols-12 @4xl:pb-[calc(var(--section-y)*0.4)]", children: [_jsx("div", { className: cn("@4xl:col-span-8 @4xl:row-start-1", end && "@4xl:col-start-5"), children: image(
|
|
48
|
+
content = (_jsxs(Container, { className: "grid gap-y-0 @4xl:grid-cols-12 @4xl:pb-[calc(var(--section-y)*0.4)]", children: [_jsx("div", { className: cn("@4xl:col-span-8 @4xl:row-start-1", end && "@4xl:col-start-5"), children: image(columnSizes(8 / 12), "rounded-ui") }), _jsx("div", { className: cn("vb-overlap-panel surface relative -mt-12 mx-[clamp(0.75rem,3cqi,1.5rem)] rounded-ui p-[clamp(1.5rem,4cqi,3rem)] @4xl:row-start-1 @4xl:mx-0 @4xl:mt-0 @4xl:self-end @4xl:translate-y-[calc(var(--section-y)*0.35)]", end ? "@4xl:col-span-6 @4xl:col-start-1" : "@4xl:col-span-6 @4xl:col-start-7"), children: text })] }));
|
|
48
49
|
}
|
|
49
50
|
else if (variant === "bleed") {
|
|
50
|
-
content = (_jsxs(Container, { className: "grid gap-x-(--gutter) gap-y-10 @4xl:grid-cols-12 @4xl:items-start", children: [_jsx("div", { className: cn("@4xl:col-span-7", end ? "vb-bleed-end @4xl:order-2 @4xl:col-start-6" : "vb-bleed-start"), children: image(
|
|
51
|
+
content = (_jsxs(Container, { className: "grid gap-x-(--gutter) gap-y-10 @4xl:grid-cols-12 @4xl:items-start", children: [_jsx("div", { className: cn("@4xl:col-span-7", end ? "vb-bleed-end @4xl:order-2 @4xl:col-start-6" : "vb-bleed-start"), children: image(columnSizes(7 / 12, { bleed: true })) }), _jsx("div", { className: cn("@4xl:col-span-4 @4xl:pt-4", end ? "@4xl:order-1 @4xl:col-start-1" : "@4xl:col-start-9"), children: text })] }));
|
|
51
52
|
}
|
|
52
53
|
else {
|
|
53
|
-
content = (_jsxs(Container, { className: "grid gap-x-(--gutter) gap-y-10 @4xl:grid-cols-12 @4xl:items-start", children: [_jsx("div", { className: cn("@4xl:col-span-5", end && "@4xl:order-2 @4xl:col-start-8"), children: image(
|
|
54
|
+
content = (_jsxs(Container, { className: "grid gap-x-(--gutter) gap-y-10 @4xl:grid-cols-12 @4xl:items-start", children: [_jsx("div", { className: cn("@4xl:col-span-5", end && "@4xl:order-2 @4xl:col-start-8"), children: image(columnSizes(5 / 12), "rounded-ui") }), _jsx("div", { className: cn("@4xl:col-span-6", end ? "@4xl:order-1 @4xl:col-start-1" : "@4xl:col-start-7"), children: text })] }));
|
|
54
55
|
}
|
|
55
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 }) }));
|
|
56
57
|
}
|
|
@@ -4,7 +4,9 @@ import type { MoneyValue } from "../schema/types.js";
|
|
|
4
4
|
import { type Site2BlockLabels } from "./shared/labels-2.js";
|
|
5
5
|
import { type SiteImage2 } from "./shared/kit-2.js";
|
|
6
6
|
export type MenuVariant = "classic" | "photo" | "service-list";
|
|
7
|
-
|
|
7
|
+
/** Dietary and house tags, rendered as words (never emoji). */
|
|
8
|
+
export declare const MENU_TAGS: readonly ["vegetarian", "vegan", "gluten-free", "dairy-free", "nut-free", "halal", "spicy", "new", "chef-pick", "to-share", "seasonal"];
|
|
9
|
+
export type MenuTag = (typeof MENU_TAGS)[number];
|
|
8
10
|
/** The 14 EU / Codex allergens. */
|
|
9
11
|
export declare const ALLERGENS: readonly ["gluten", "crustaceans", "eggs", "fish", "peanuts", "soybeans", "milk", "nuts", "celery", "mustard", "sesame", "sulphites", "lupin", "molluscs"];
|
|
10
12
|
export type Allergen = (typeof ALLERGENS)[number];
|
|
@@ -23,7 +25,7 @@ export interface MenuItem {
|
|
|
23
25
|
duration?: React.ReactNode;
|
|
24
26
|
tags?: MenuTag[];
|
|
25
27
|
allergens?: Allergen[];
|
|
26
|
-
/** Square photo (`photo` variant). */
|
|
28
|
+
/** Square photo (`photo` variant), 72px (`sizes="72px"`). */
|
|
27
29
|
image?: SiteImage2;
|
|
28
30
|
/** "Not today" — shown as a label, never a strikethrough. */
|
|
29
31
|
unavailable?: boolean;
|
|
@@ -59,7 +61,9 @@ export interface MenuListProps extends Omit<BlockBaseProps, "variant" | "labels"
|
|
|
59
61
|
labels?: Site2BlockLabels;
|
|
60
62
|
}
|
|
61
63
|
/**
|
|
62
|
-
* Restaurant menus and spa/salon price lists — also a
|
|
64
|
+
* Restaurant menus and spa/salon price lists — also a whole menu page, so a site never has to
|
|
65
|
+
* link out to a third-party QR menu: pass `headingLevel={1}` and the title becomes the page `h1`,
|
|
66
|
+
* categories `h2`, dishes `h3`. Category jump bar with plain
|
|
63
67
|
* anchors, money through `Intl` (full currency name for screen readers), tags as words (never
|
|
64
68
|
* emoji), the 14 allergens localized. Server component, 0 JS.
|
|
65
69
|
*
|
|
@@ -7,11 +7,35 @@ import { BlockImage } from "../block-image.js";
|
|
|
7
7
|
import { BlockLink } from "../block-link.js";
|
|
8
8
|
import { site2Text } from "./shared/labels-2.js";
|
|
9
9
|
import { fieldAttr, formatMoney, formatMoneyLong, textOf, useBlockIds2 } from "./shared/kit-2.js";
|
|
10
|
+
/** Dietary and house tags, rendered as words (never emoji). */
|
|
11
|
+
export const MENU_TAGS = ["vegetarian", "vegan", "gluten-free", "dairy-free", "nut-free", "halal", "spicy", "new", "chef-pick", "to-share", "seasonal"];
|
|
10
12
|
/** The 14 EU / Codex allergens. */
|
|
11
13
|
export const ALLERGENS = ["gluten", "crustaceans", "eggs", "fish", "peanuts", "soybeans", "milk", "nuts", "celery", "mustard", "sesame", "sulphites", "lupin", "molluscs"];
|
|
12
|
-
const TAG_KEY = {
|
|
14
|
+
const TAG_KEY = {
|
|
15
|
+
vegetarian: "tagVegetarian",
|
|
16
|
+
vegan: "tagVegan",
|
|
17
|
+
"gluten-free": "tagGlutenFree",
|
|
18
|
+
"dairy-free": "tagDairyFree",
|
|
19
|
+
"nut-free": "tagNutFree",
|
|
20
|
+
halal: "tagHalal",
|
|
21
|
+
spicy: "tagSpicy",
|
|
22
|
+
new: "tagNew",
|
|
23
|
+
"chef-pick": "tagChefPick",
|
|
24
|
+
"to-share": "tagToShare",
|
|
25
|
+
seasonal: "tagSeasonal",
|
|
26
|
+
};
|
|
27
|
+
const DIET = {
|
|
28
|
+
vegan: "https://schema.org/VeganDiet",
|
|
29
|
+
vegetarian: "https://schema.org/VegetarianDiet",
|
|
30
|
+
"gluten-free": "https://schema.org/GlutenFreeDiet",
|
|
31
|
+
"dairy-free": "https://schema.org/LowLactoseDiet",
|
|
32
|
+
halal: "https://schema.org/HalalDiet",
|
|
33
|
+
};
|
|
34
|
+
const hTag = (level) => `h${Math.min(6, Math.max(2, level))}`;
|
|
13
35
|
/**
|
|
14
|
-
* Restaurant menus and spa/salon price lists — also a
|
|
36
|
+
* Restaurant menus and spa/salon price lists — also a whole menu page, so a site never has to
|
|
37
|
+
* link out to a third-party QR menu: pass `headingLevel={1}` and the title becomes the page `h1`,
|
|
38
|
+
* categories `h2`, dishes `h3`. Category jump bar with plain
|
|
15
39
|
* anchors, money through `Intl` (full currency name for screen readers), tags as words (never
|
|
16
40
|
* emoji), the 14 allergens localized. Server component, 0 JS.
|
|
17
41
|
*
|
|
@@ -25,10 +49,13 @@ export function MenuList({ variant = "classic", eyebrow, title, lead, categories
|
|
|
25
49
|
const categories = all.filter((c) => c.items.length > 0 || editMode);
|
|
26
50
|
const nav = showNav ?? categories.length > 2;
|
|
27
51
|
const catId = (c, i) => c.anchor ?? `${slugify(textOf(c.title), { locale }) || "section"}-${i}`;
|
|
28
|
-
|
|
52
|
+
// Outline follows the block title: h2 title → h3 categories → h4 dishes; h1 page title → h2 → h3.
|
|
53
|
+
const catLevel = title ? headingLevel + 1 : headingLevel;
|
|
54
|
+
const CatH = hTag(catLevel);
|
|
55
|
+
const ctx = { t, locale, components, labels, editMode, variant, bookHref, showAllergens, ItemH: hTag(catLevel + 1) };
|
|
29
56
|
return (_jsx(Section, { theme: theme, anchor: anchor, headingId: title ? headingId : undefined, blockId: id, blockType: "menu", editMode: editMode, className: cn("vb-menu", className), "data-variant": variant, children: _jsxs(Container, { className: "flex flex-col gap-10 @4xl:gap-14", children: [title || eyebrow || lead ? _jsx(SectionHeader, { eyebrow: eyebrow, title: title, lead: lead, headingId: headingId, headingLevel: headingLevel, align: "split", editMode: editMode, labels: labels, locale: locale }) : null, nav ? (_jsx("nav", { "aria-label": t("menuCategories"), className: "vb-menu-nav sticky top-[var(--topbar-h,0px)] z-10 -mx-(--gutter) border-b border-line bg-scene px-(--gutter)", children: _jsx("ul", { className: "scrollbar-none flex gap-1 overflow-x-auto py-2", children: categories.map((c, i) => (_jsx("li", { className: "shrink-0", children: _jsx("a", { href: `#${base}-${catId(c, i)}`, className: "inline-flex h-10 items-center whitespace-nowrap rounded-full px-3.5 text-sm text-fg-muted transition-colors hover:bg-hover hover:text-fg focus-ring", children: c.title }) }, i))) }) })) : null, _jsx("div", { className: "flex flex-col gap-14 @4xl:gap-20", children: categories.map((c, ci) => {
|
|
30
57
|
const hid = `${base}-${catId(c, ci)}`;
|
|
31
|
-
return (_jsxs("section", { id: hid, "aria-labelledby": `${hid}-h`, className: "vb-menu-cat flex flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2 border-t-[3px] border-double border-line pt-5 @2xl:flex-row @2xl:items-baseline @2xl:justify-between @2xl:gap-8", children: [_jsx(
|
|
58
|
+
return (_jsxs("section", { id: hid, "aria-labelledby": `${hid}-h`, className: "vb-menu-cat flex flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2 border-t-[3px] border-double border-line pt-5 @2xl:flex-row @2xl:items-baseline @2xl:justify-between @2xl:gap-8", children: [_jsx(CatH, { id: `${hid}-h`, className: "font-display text-display-m text-fg", ...fieldAttr(editMode, `categories.${ci}.title`), children: c.title }), c.note ? _jsx("p", { className: "max-w-[44ch] text-small text-fg-muted @2xl:text-end", children: c.note }) : null] }), variant === "service-list" ? (_jsx("ul", { className: "flex flex-col", children: c.items.map((item, ii) => (_jsx(ServiceRow, { item: item, ctx: ctx }, ii))) })) : (_jsx("ul", { className: cn("grid gap-x-(--gutter) gap-y-7", variant === "classic" && "@4xl:grid-cols-2 @4xl:gap-x-16"), children: c.items.map((item, ii) => (_jsx(DishItem, { item: item, ctx: ctx }, ii))) }))] }, ci));
|
|
32
59
|
}) }), priceNote ? (_jsx("p", { className: "border-t border-line pt-5 text-small text-fg-muted", ...fieldAttr(editMode, "priceNote"), children: priceNote })) : null] }) }));
|
|
33
60
|
}
|
|
34
61
|
function Price({ price, ctx, className }) {
|
|
@@ -57,12 +84,12 @@ function Allergens({ item, ctx }) {
|
|
|
57
84
|
}
|
|
58
85
|
function DishItem({ item, ctx }) {
|
|
59
86
|
const photo = ctx.variant === "photo";
|
|
60
|
-
return (_jsxs("li", { "data-unavailable": item.unavailable ? "" : undefined, className: cn("vb-menu-item flex gap-4", item.unavailable && "opacity-70"), children: [photo ? (item.image?.src ? (_jsx(BlockImage, { ...item.image, alt: item.image.alt ?? "", decorative: !item.image.alt, aspect: "1:1", sizes: "72px", className: "size-[72px] shrink-0 rounded-ui-sm", components: ctx.components })) : (_jsx("span", { "aria-hidden": true, className: "size-[72px] shrink-0" }))) : null, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [_jsxs("div", { className: "flex items-baseline gap-3", children: [_jsx(
|
|
87
|
+
return (_jsxs("li", { "data-unavailable": item.unavailable ? "" : undefined, className: cn("vb-menu-item flex gap-4", item.unavailable && "opacity-70"), children: [photo ? (item.image?.src ? (_jsx(BlockImage, { ...item.image, alt: item.image.alt ?? "", decorative: !item.image.alt, aspect: "1:1", sizes: "72px", className: "size-[72px] shrink-0 rounded-ui-sm", components: ctx.components })) : (_jsx("span", { "aria-hidden": true, className: "size-[72px] shrink-0" }))) : null, _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [_jsxs("div", { className: "flex items-baseline gap-3", children: [_jsx(ctx.ItemH, { className: "text-body font-medium text-fg", children: item.name }), !photo ? _jsx("span", { "aria-hidden": true, className: "vb-leader min-w-6 flex-1 translate-y-[-0.3em] border-b border-dotted border-fg-subtle" }) : _jsx("span", { className: "flex-1" }), _jsx(Prices, { item: item, ctx: ctx })] }), item.description ? _jsx("p", { className: "max-w-[56ch] text-small text-fg-muted text-pretty", children: item.description }) : null, _jsx(Tags, { item: item, ctx: ctx }), _jsx(Allergens, { item: item, ctx: ctx })] })] }));
|
|
61
88
|
}
|
|
62
89
|
function ServiceRow({ item, ctx }) {
|
|
63
90
|
const href = item.bookHref ?? ctx.bookHref;
|
|
64
91
|
const name = textOf(item.name);
|
|
65
|
-
return (_jsxs("li", { "data-unavailable": item.unavailable ? "" : undefined, className: "grid gap-x-(--gutter) gap-y-2 border-b border-line py-5 first:border-t @2xl:grid-cols-12 @2xl:items-baseline", children: [_jsxs("div", { className: "flex flex-col gap-1.5 @2xl:col-span-6", children: [_jsx(
|
|
92
|
+
return (_jsxs("li", { "data-unavailable": item.unavailable ? "" : undefined, className: "grid gap-x-(--gutter) gap-y-2 border-b border-line py-5 first:border-t @2xl:grid-cols-12 @2xl:items-baseline", children: [_jsxs("div", { className: "flex flex-col gap-1.5 @2xl:col-span-6", children: [_jsx(ctx.ItemH, { className: "text-body font-medium text-fg", children: item.name }), item.description ? _jsx("p", { className: "text-small text-fg-muted text-pretty", children: item.description }) : null, _jsx(Tags, { item: item, ctx: ctx })] }), _jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.06em] text-fg-muted @2xl:col-span-2", children: item.prices.length === 1 ? item.duration : null }), _jsx("div", { className: "@2xl:col-span-3 @2xl:text-end", children: _jsx(Prices, { item: item, ctx: ctx }) }), _jsx("div", { className: "@2xl:col-span-1 @2xl:text-end", children: href && !item.unavailable ? (_jsx(BlockLink, { href: href, components: ctx.components, labels: ctx.labels, locale: ctx.locale, "aria-label": name ? ctx.t("bookService", { service: name }) : undefined, className: "inline-flex min-h-6 items-center text-small font-medium text-fg underline decoration-line underline-offset-[0.25em] hover:decoration-current focus-ring rounded-[2px] pointer-coarse:min-h-11", "data-track": "book", children: ctx.t("book") })) : null })] }));
|
|
66
93
|
}
|
|
67
94
|
/** schema.org `Menu` → `MenuSection` → `MenuItem` + `Offer`. */
|
|
68
95
|
export function menuJsonLd(props) {
|
|
@@ -77,7 +104,7 @@ export function menuJsonLd(props) {
|
|
|
77
104
|
name: textOf(i.name),
|
|
78
105
|
description: textOf(i.description) || undefined,
|
|
79
106
|
offers: i.prices.filter((p) => p.amount).map((p) => ({ "@type": "Offer", price: p.amount.amount, priceCurrency: p.amount.currency, name: textOf(p.label) || undefined })),
|
|
80
|
-
suitableForDiet: (i.tags ?? []).flatMap((tag) => (tag
|
|
107
|
+
suitableForDiet: (i.tags ?? []).flatMap((tag) => (DIET[tag] ? [DIET[tag]] : [])),
|
|
81
108
|
})),
|
|
82
109
|
})),
|
|
83
110
|
};
|
|
@@ -28,6 +28,7 @@ export interface NavbarProps extends Omit<BlockBaseProps, "variant" | "labels">
|
|
|
28
28
|
* rail: links inside one framed pill in the middle, "Sign in" + "Try free" at the end.
|
|
29
29
|
*/
|
|
30
30
|
variant?: NavbarVariant;
|
|
31
|
+
/** Logo image (`sizes="12rem"`; the mark is 1.75rem tall). */
|
|
31
32
|
logo?: SiteImage;
|
|
32
33
|
/** Shown instead of `logo` inside dark sections / dark mode. */
|
|
33
34
|
logoDark?: SiteImage;
|
|
@@ -8,6 +8,7 @@ import { BlockLink } from "../block-link.js";
|
|
|
8
8
|
import { SkipLink } from "../section.js";
|
|
9
9
|
import { NavbarSubmenu } from "./navbar-submenu.js";
|
|
10
10
|
import { CtaList, useBlockIds, siteText } from "./_kit-1.js";
|
|
11
|
+
import { TAP } from "./shared/tap.js";
|
|
11
12
|
/** National format in the number's own locale, international elsewhere ("0216 000 00 00" for tr). */
|
|
12
13
|
export function formatSitePhone(e164, locale = "en-US") {
|
|
13
14
|
const p = parsePhoneNumberFromString(e164);
|
|
@@ -34,16 +35,16 @@ export function Navbar({ variant = "bar", logo, logoDark, wordmark, homeHref = "
|
|
|
34
35
|
const lang = baseLanguage(locale);
|
|
35
36
|
const phoneText = phone ? formatSitePhone(phone, locale) : undefined;
|
|
36
37
|
const linkProps = { components, labels, locale };
|
|
37
|
-
const brand = (_jsxs(BlockLink, { href: homeHref, ...linkProps, className: "vb-nav-brand flex shrink-0 items-center gap-3 rounded-[2px] focus-ring", children: [logo ? (_jsxs(_Fragment, { children: [_jsx(BlockImage, { ...logo, decorative: !!wordmark || logo.decorative, alt: wordmark ? undefined : logo.alt, priority: true, className: cn("vb-nav-logo", logoDark && "vb-logo-light"), components: components }), logoDark ? _jsx(BlockImage, { ...logoDark, decorative: true, priority: true, className: "vb-nav-logo vb-logo-dark", components: components }) : null] })) : null, wordmark || !logo ? (_jsx("span", { className: cn("vb-nav-wordmark font-display text-fg", variant === "split" ? "text-[1.625rem]" : "text-[1.375rem]"), "data-field": editMode ? "props.wordmark" : undefined, children: wordmark })) : null] }));
|
|
38
|
+
const brand = (_jsxs(BlockLink, { href: homeHref, ...linkProps, className: "vb-nav-brand flex min-h-6 shrink-0 items-center gap-3 pointer-coarse:min-h-11 rounded-[2px] focus-ring", children: [logo ? (_jsxs(_Fragment, { children: [_jsx(BlockImage, { ...logo, decorative: !!wordmark || logo.decorative, alt: wordmark ? undefined : logo.alt, priority: true, sizes: "12rem", className: cn("vb-nav-logo", logoDark && "vb-logo-light"), components: components }), logoDark ? _jsx(BlockImage, { ...logoDark, decorative: true, priority: true, sizes: "12rem", className: "vb-nav-logo vb-logo-dark", components: components }) : null] })) : null, wordmark || !logo ? (_jsx("span", { className: cn("vb-nav-wordmark font-display text-fg", variant === "split" ? "text-[1.625rem]" : "text-[1.375rem]"), "data-field": editMode ? "props.wordmark" : undefined, children: wordmark })) : null] }));
|
|
38
39
|
const renderLink = (item, key, className) => (_jsx(BlockLink, { href: item.href, newTab: item.newTab, ...linkProps, "aria-current": isCurrent(item.href, currentPath) ? "page" : undefined, className: cn("vb-nav-link", className), children: item.label }, key));
|
|
39
40
|
const desktopItem = (item, i) => item.children?.length ? (_jsx("li", { children: _jsx(NavbarSubmenu, { label: item.label, active: inGroup(item, currentPath), wide: item.children.some((c) => c.description), children: _jsx("ul", { className: cn("grid gap-0.5", item.children.some((c) => c.description) && "grid-cols-2"), children: item.children.map((c, j) => (_jsx("li", { children: _jsxs(BlockLink, { href: c.href, newTab: c.newTab, ...linkProps, "aria-current": isCurrent(c.href, currentPath) ? "page" : undefined, className: "vb-nav-sublink flex flex-col gap-0.5 rounded-ui-sm px-3 py-2.5 text-sm text-fg transition-colors hover:bg-hover focus-ring", children: [_jsx("span", { className: "font-medium", children: c.label }), c.description ? _jsx("span", { className: "text-small leading-snug text-fg-muted", children: c.description }) : null] }) }, j))) }) }) }, i)) : (_jsx("li", { children: renderLink(item, i) }, i));
|
|
40
41
|
const linkList = (list, offset = 0, className) => (_jsx("ul", { className: cn("vb-nav-links hidden items-center @4xl:flex", variant === "rail" ? "gap-1" : "gap-7", className), children: list.map((it, i) => desktopItem(it, i + offset)) }));
|
|
41
|
-
const phoneLink = phone ? (_jsxs("a", { href: `tel:${phone}`, "aria-label": t("callUs", { phone: phoneText }), "data-track": "call", className: "vb-nav-phone hidden items-center gap-2 rounded-[2px] font-mono text-small tabular text-fg-muted transition-colors hover:text-fg focus-ring @6xl:inline-flex", children: [_jsx(Phone, { "aria-hidden": true, strokeWidth: 1.5, className: "size-3.5" }), phoneText] })) : null;
|
|
42
|
+
const phoneLink = phone ? (_jsxs("a", { href: `tel:${phone}`, "aria-label": t("callUs", { phone: phoneText }), "data-track": "call", className: "vb-nav-phone hidden min-h-6 items-center gap-2 rounded-[2px] font-mono text-small tabular text-fg-muted transition-colors hover:text-fg focus-ring @6xl:inline-flex", children: [_jsx(Phone, { "aria-hidden": true, strokeWidth: 1.5, className: "size-3.5" }), phoneText] })) : null;
|
|
42
43
|
const localeSwitch = locales?.length ? (_jsx("ul", { "aria-label": t("languages"), className: "hidden items-center gap-1 font-mono text-eyebrow uppercase tracking-[0.08em] @4xl:flex", children: locales.map((l, i) => {
|
|
43
44
|
const current = l.current ?? baseLanguage(l.code) === lang;
|
|
44
|
-
return (_jsxs("li", { className: "flex items-center gap-1", children: [i > 0 ? _jsx("span", { "aria-hidden": true, className: "text-fg-subtle", children: "/" }) : null, _jsx("a", { href: l.href, hrefLang: l.code, lang: l.code, "aria-current": current ? "true" : undefined, className: cn("rounded-[2px] px-0.5 transition-colors focus-ring", current ? "text-fg" : "text-fg-muted hover:text-fg"), children: l.code })] }, l.code));
|
|
45
|
+
return (_jsxs("li", { className: "flex items-center gap-1", children: [i > 0 ? _jsx("span", { "aria-hidden": true, className: "text-fg-subtle", children: "/" }) : null, _jsx("a", { href: l.href, hrefLang: l.code, lang: l.code, "aria-current": current ? "true" : undefined, className: cn("inline-flex min-h-6 min-w-6 items-center justify-center rounded-[2px] px-0.5 transition-colors focus-ring", current ? "text-fg" : "text-fg-muted hover:text-fg"), children: l.code })] }, l.code));
|
|
45
46
|
}) })) : null;
|
|
46
|
-
const trigger = (always) => (_jsx("button", { type: "button", popoverTarget: panelId, "aria-haspopup": "dialog", "aria-controls": panelId, className: cn("vb-nav-trigger inline-flex h-10 items-center gap-2 rounded-ui px-2 text-sm font-medium text-fg transition-colors hover:bg-hover focus-ring", !always && "@4xl:hidden"), children: always ? (_jsx("span", { className: "font-mono text-eyebrow uppercase tracking-[0.08em]", children: t("openMenu") })) : (_jsxs(_Fragment, { children: [_jsx(Menu, { "aria-hidden": true, strokeWidth: 1.5, className: "size-5" }), _jsx("span", { className: "sr-only", children: t("openMenu") })] })) }));
|
|
47
|
+
const trigger = (always) => (_jsx("button", { type: "button", popoverTarget: panelId, "aria-haspopup": "dialog", "aria-controls": panelId, className: cn("vb-nav-trigger inline-flex h-10 min-w-10 items-center justify-center gap-2 rounded-ui px-2 text-sm font-medium text-fg transition-colors hover:bg-hover focus-ring pointer-coarse:h-11 pointer-coarse:min-w-11", !always && "@4xl:hidden"), children: always ? (_jsx("span", { className: "font-mono text-eyebrow uppercase tracking-[0.08em]", children: t("openMenu") })) : (_jsxs(_Fragment, { children: [_jsx(Menu, { "aria-hidden": true, strokeWidth: 1.5, className: "size-5" }), _jsx("span", { className: "sr-only", children: t("openMenu") })] })) }));
|
|
47
48
|
const primaryAction = actions?.length ? _jsx(CtaList, { actions: actions, ...linkProps, size: "md", max: variant === "bar" || variant === "minimal" ? 1 : 2, className: "hidden flex-nowrap gap-x-2 @2xl:flex" }) : null;
|
|
48
49
|
let row;
|
|
49
50
|
if (variant === "split") {
|
|
@@ -60,5 +61,5 @@ export function Navbar({ variant = "bar", logo, logoDark, wordmark, homeHref = "
|
|
|
60
61
|
row = (_jsxs(_Fragment, { children: [brand, linkList(items, 0, "ms-4"), _jsxs("div", { className: "ms-auto flex items-center gap-5", children: [phoneLink, localeSwitch, primaryAction, trigger(false)] })] }));
|
|
61
62
|
}
|
|
62
63
|
const panelLinkSize = variant === "minimal" ? "font-display text-display-l" : "font-display text-display-m";
|
|
63
|
-
return (_jsxs("header", { id: anchor, "data-variant": variant, "data-sticky": sticky ? "" : undefined, "data-overlay": overlay ? "" : undefined, "data-block-id": editMode ? id : undefined, className: cn("vb-navbar", overlay && "dark", className), children: [skipLink ? _jsx(SkipLink, { labels: labels, locale: locale }) : null, _jsx("nav", { "aria-label": t("mainNavigation"), className: cn("vb-container vb-navbar-row items-center", variant === "split" ? "grid grid-cols-[1fr_auto_1fr] gap-6" : "flex gap-6"), children: row }), _jsx("div", { id: panelId, popover: "auto", role: "dialog", "aria-label": t("menuDialog"), className: "vb-nav-panel", children: _jsxs("div", { className: "vb-container flex min-h-full flex-col", children: [_jsxs("div", { className: "vb-navbar-row flex items-center justify-between gap-6", children: [brand, _jsxs("button", { type: "button", popoverTarget: panelId, popoverTargetAction: "hide", className: "inline-flex h-10 items-center gap-2 rounded-ui px-2 text-fg transition-colors hover:bg-hover focus-ring", children: [_jsx("span", { className: variant === "minimal" ? "font-mono text-eyebrow uppercase tracking-[0.08em]" : "sr-only", children: t("closeMenu") }), _jsx(X, { "aria-hidden": true, strokeWidth: 1.5, className: "size-5" })] })] }), _jsx("ul", { className: "flex flex-col py-[clamp(1.5rem,6vh,4rem)]", children: items.map((it, i) => (_jsxs("li", { className: "border-b border-line", children: [renderLink(it, i, cn("vb-nav-panel-link block py-3 text-fg", panelLinkSize)), it.children?.length ? (_jsx("ul", { className: "flex flex-wrap gap-x-6 gap-y-2 pb-4", children: it.children.map((c, j) => (_jsx("li", { children: renderLink(c, j, "text-small text-fg-muted hover:text-fg") }, j))) })) : null] }, i))) }), _jsxs("div", { className: "mt-auto flex flex-col gap-8 pb-[max(2rem,env(safe-area-inset-bottom))]", children: [actions?.length ? _jsx(CtaList, { actions: actions, ...linkProps, size: "lg" }) : null, phone || panelMeta?.length || locales?.length ? (_jsxs("dl", { className: "grid gap-x-8 gap-y-4 border-t border-line pt-6 text-small sm:grid-cols-3", children: [phone ? (_jsxs("div", { children: [_jsx("dt", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: t("phoneLabel") }), _jsx("dd", { className: "mt-1", children: _jsx("a", { href: `tel:${phone}`, "data-track": "call", className: "tabular text-fg underline decoration-line underline-offset-4 focus-ring", children: phoneText }) })] })) : null, panelMeta?.map((m, i) => (_jsxs("div", { children: [_jsx("dt", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: m.label }), _jsx("dd", { className: "mt-1 text-fg", children: m.value })] }, i))), locales?.length ? (_jsxs("div", { children: [_jsx("dt", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: t("languages") }), _jsx("dd", { className: "mt-1 flex gap-3", children: locales.map((l) => (_jsx("a", { href: l.href, hrefLang: l.code, lang: l.code, "aria-current": (l.current ?? baseLanguage(l.code) === lang) ? "true" : undefined, className: "font-mono uppercase text-fg-muted aria-[current]:text-fg focus-ring", children: l.code }, l.code))) })] })) : null] })) : null] })] }) })] }));
|
|
64
|
+
return (_jsxs("header", { id: anchor, "data-variant": variant, "data-sticky": sticky ? "" : undefined, "data-overlay": overlay ? "" : undefined, "data-block-id": editMode ? id : undefined, className: cn("vb-navbar", overlay && "dark", className), children: [skipLink ? _jsx(SkipLink, { labels: labels, locale: locale }) : null, _jsx("nav", { "aria-label": t("mainNavigation"), className: cn("vb-container vb-navbar-row items-center", variant === "split" ? "grid grid-cols-[1fr_auto_1fr] gap-6" : "flex gap-6"), children: row }), _jsx("div", { id: panelId, popover: "auto", role: "dialog", "aria-label": t("menuDialog"), className: "vb-nav-panel", children: _jsxs("div", { className: "vb-container flex min-h-full flex-col", children: [_jsxs("div", { className: "vb-navbar-row flex items-center justify-between gap-6", children: [brand, _jsxs("button", { type: "button", popoverTarget: panelId, popoverTargetAction: "hide", className: "inline-flex h-10 min-w-10 items-center justify-center gap-2 rounded-ui px-2 text-fg transition-colors hover:bg-hover focus-ring pointer-coarse:h-11 pointer-coarse:min-w-11", children: [_jsx("span", { className: variant === "minimal" ? "font-mono text-eyebrow uppercase tracking-[0.08em]" : "sr-only", children: t("closeMenu") }), _jsx(X, { "aria-hidden": true, strokeWidth: 1.5, className: "size-5" })] })] }), _jsx("ul", { className: "flex flex-col py-[clamp(1.5rem,6vh,4rem)]", children: items.map((it, i) => (_jsxs("li", { className: "border-b border-line", children: [renderLink(it, i, cn("vb-nav-panel-link block py-3 text-fg", panelLinkSize)), it.children?.length ? (_jsx("ul", { className: "flex flex-wrap gap-x-6 gap-y-2 pb-4 pointer-coarse:gap-y-0", children: it.children.map((c, j) => (_jsx("li", { children: renderLink(c, j, cn(TAP, "text-small text-fg-muted hover:text-fg")) }, j))) })) : null] }, i))) }), _jsxs("div", { className: "mt-auto flex flex-col gap-8 pb-[max(2rem,env(safe-area-inset-bottom))]", children: [actions?.length ? _jsx(CtaList, { actions: actions, ...linkProps, size: "lg" }) : null, phone || panelMeta?.length || locales?.length ? (_jsxs("dl", { className: "grid gap-x-8 gap-y-4 border-t border-line pt-6 text-small sm:grid-cols-3", children: [phone ? (_jsxs("div", { children: [_jsx("dt", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: t("phoneLabel") }), _jsx("dd", { className: "mt-1", children: _jsx("a", { href: `tel:${phone}`, "data-track": "call", className: cn(TAP, "tabular text-fg underline decoration-line underline-offset-4 focus-ring"), children: phoneText }) })] })) : null, panelMeta?.map((m, i) => (_jsxs("div", { children: [_jsx("dt", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: m.label }), _jsx("dd", { className: "mt-1 text-fg", children: m.value })] }, i))), locales?.length ? (_jsxs("div", { children: [_jsx("dt", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: t("languages") }), _jsx("dd", { className: "mt-1 flex gap-3 pointer-coarse:gap-1", children: locales.map((l) => (_jsx("a", { href: l.href, hrefLang: l.code, lang: l.code, "aria-current": (l.current ?? baseLanguage(l.code) === lang) ? "true" : undefined, className: cn(TAP, "min-w-6 justify-center font-mono uppercase text-fg-muted aria-[current]:text-fg focus-ring pointer-coarse:min-w-11"), children: l.code }, l.code))) })] })) : null] })) : null] })] }) })] }));
|
|
64
65
|
}
|
|
@@ -7,6 +7,7 @@ import { BlockHeading } from "../block-heading.js";
|
|
|
7
7
|
import { BlockLink } from "../block-link.js";
|
|
8
8
|
import { site2Text } from "./shared/labels-2.js";
|
|
9
9
|
import { fieldAttr, prettyPhone, telHref, useBlockIds2 } from "./shared/kit-2.js";
|
|
10
|
+
import { headingLines } from "./shared/heading-lines.js";
|
|
10
11
|
const KIND = {
|
|
11
12
|
"404": { code: "notFoundCode", title: "notFoundTitle", text: "notFoundText" },
|
|
12
13
|
"500": { code: "serverErrorCode", title: "serverErrorTitle", text: "serverErrorText" },
|
|
@@ -27,7 +28,7 @@ export function NotFound({ variant = "plain", kind = "404", title, text, links =
|
|
|
27
28
|
const { base, headingId } = useBlockIds2(id, "not-found");
|
|
28
29
|
const k = KIND[kind];
|
|
29
30
|
const showLinks = variant === "links";
|
|
30
|
-
return (_jsx(Section, { theme: theme, anchor: anchor, headingId: headingId, blockId: id, blockType: "not-found", editMode: editMode, className: cn("vb-not-found", className), "data-kind": kind, "data-variant": variant, children: _jsxs(Container, { className: "grid gap-x-(--gutter) gap-y-14 @4xl:grid-cols-12", children: [_jsxs("div", { className: "flex flex-col gap-6 @4xl:col-span-8", children: [_jsxs("p", { className: "flex items-center gap-3 font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: [_jsx("span", { className: "tabular text-fg", children: t(k.code) }), _jsx("span", { "aria-hidden": true, className: "h-px w-8 bg-current opacity-40" })] }), _jsx(BlockHeading, { id: headingId, level: headingLevel, size: "display-l", text: title ?? t(k.title), editMode: editMode, field: "props.title", labels: labels, locale: locale, className: "max-w-[20ch]" }), _jsx("p", { className: "max-w-[52ch] text-lead text-fg-muted text-pretty", ...fieldAttr(editMode, "text"), children: text ?? t(k.text) }), _jsxs("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-3 pt-2", children: [reset ? (_jsxs("button", { type: "button", onClick: reset, className: cn(buttonVariants({ variant: "primary", size: "lg" }), "vb-action-primary"), children: [_jsx(RotateCcw, { "aria-hidden": true, strokeWidth: 1.75 }), t("tryAgain")] })) : null, _jsxs(BlockLink, { href: homeHref, appearance: reset ? "link" : kind === "maintenance" && phone ? "secondary" : "primary", components: components, labels: labels, locale: locale, children: [t("backHome"), reset ? _jsx(ArrowRight, { "aria-hidden": true, strokeWidth: 1.5, className: "size-[1em] rtl:-scale-x-100" }) : null] }), kind === "maintenance" && phone ? (_jsx("a", { href: telHref(phone), className: cn(buttonVariants({ variant: "primary", size: "lg" }), "vb-action-primary tabular"), "data-track": "call", children: prettyPhone(phone) })) : null] })] }), showLinks ? (_jsxs("div", { className: "flex flex-col gap-10 border-t border-line pt-8 @4xl:col-span-12", children: [search ? (_jsxs("form", { role: "search", action: search.action, method: "get", className: "flex max-w-xl flex-col gap-2", children: [_jsx("label", { htmlFor: `${base}-q`, className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: t("searchSite") }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("span", { className: "relative flex-1", children: [_jsx(Search, { "aria-hidden": true, strokeWidth: 1.5, className: "pointer-events-none absolute start-3 top-1/2 size-4 -translate-y-1/2 text-fg-muted" }), _jsx("input", { id: `${base}-q`, type: "search", name: search.param ?? "q", placeholder: t("searchPlaceholder"), className: "h-12 w-full rounded-ui border border-field-border bg-field ps-9 pe-3 text-body text-fg placeholder:text-fg-subtle transition-colors hover:border-field-border-hover focus-ring" })] }), _jsx("button", { type: "submit", className: buttonVariants({ variant: "secondary", size: "lg" }), children: t("search") })] })] })) : null, links.length ? (_jsxs("nav", { "aria-labelledby": `${base}-pop`, className: "flex flex-col gap-4", children: [_jsx("p", { id: `${base}-pop`, className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: t("popularPages") }), _jsx("ul", { className: "grid gap-x-(--gutter) @2xl:grid-cols-2", children: links.slice(0, 6).map((l, i) => (_jsx("li", { className: "border-t border-line", children: _jsxs(BlockLink, { href: l.href, newTab: l.newTab, components: components, labels: labels, locale: locale, className: "group/nf flex items-center justify-between gap-4 py-4 text-body text-fg transition-colors hover:text-accent-ink focus-ring rounded-[2px]", children: [_jsx("span", { children: l.label }), _jsx(ArrowRight, { "aria-hidden": true, strokeWidth: 1.5, className: "size-4 text-fg-subtle transition-colors group-hover/nf:text-current rtl:-scale-x-100" })] }) }, i))) })] })) : null, phone && kind !== "maintenance" ? (_jsx("p", { className: "text-small text-fg-muted", children: t("stillStuck", { phone: "\u0000" })
|
|
31
|
+
return (_jsx(Section, { theme: theme, anchor: anchor, headingId: headingId, blockId: id, blockType: "not-found", editMode: editMode, className: cn("vb-not-found", className), "data-kind": kind, "data-variant": variant, children: _jsxs(Container, { className: "grid gap-x-(--gutter) gap-y-14 @4xl:grid-cols-12", children: [_jsxs("div", { className: "flex flex-col gap-6 @4xl:col-span-8", children: [_jsxs("p", { className: "flex items-center gap-3 font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: [_jsx("span", { className: "tabular text-fg", children: t(k.code) }), _jsx("span", { "aria-hidden": true, className: "h-px w-8 bg-current opacity-40" })] }), _jsx(BlockHeading, { id: headingId, level: headingLevel, size: "display-l", text: headingLines(title ?? t(k.title)), editMode: editMode, field: "props.title", labels: labels, locale: locale, className: "max-w-[20ch]" }), _jsx("p", { className: "max-w-[52ch] text-lead text-fg-muted text-pretty", ...fieldAttr(editMode, "text"), children: text ?? t(k.text) }), _jsxs("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-3 pt-2", children: [reset ? (_jsxs("button", { type: "button", onClick: reset, className: cn(buttonVariants({ variant: "primary", size: "lg" }), "vb-action-primary"), children: [_jsx(RotateCcw, { "aria-hidden": true, strokeWidth: 1.75 }), t("tryAgain")] })) : null, _jsxs(BlockLink, { href: homeHref, appearance: reset ? "link" : kind === "maintenance" && phone ? "secondary" : "primary", components: components, labels: labels, locale: locale, children: [t("backHome"), reset ? _jsx(ArrowRight, { "aria-hidden": true, strokeWidth: 1.5, className: "size-[1em] rtl:-scale-x-100" }) : null] }), kind === "maintenance" && phone ? (_jsx("a", { href: telHref(phone), className: cn(buttonVariants({ variant: "primary", size: "lg" }), "vb-action-primary tabular"), "data-track": "call", children: prettyPhone(phone) })) : null] })] }), showLinks ? (_jsxs("div", { className: "flex flex-col gap-10 border-t border-line pt-8 @4xl:col-span-12", children: [search ? (_jsxs("form", { role: "search", action: search.action, method: "get", className: "flex max-w-xl flex-col gap-2", children: [_jsx("label", { htmlFor: `${base}-q`, className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: t("searchSite") }), _jsxs("div", { className: "flex gap-2", children: [_jsxs("span", { className: "relative flex-1", children: [_jsx(Search, { "aria-hidden": true, strokeWidth: 1.5, className: "pointer-events-none absolute start-3 top-1/2 size-4 -translate-y-1/2 text-fg-muted" }), _jsx("input", { id: `${base}-q`, type: "search", name: search.param ?? "q", placeholder: t("searchPlaceholder"), className: "h-12 w-full rounded-ui border border-field-border bg-field ps-9 pe-3 text-body text-fg placeholder:text-fg-subtle transition-colors hover:border-field-border-hover focus-ring" })] }), _jsx("button", { type: "submit", className: buttonVariants({ variant: "secondary", size: "lg" }), children: t("search") })] })] })) : null, links.length ? (_jsxs("nav", { "aria-labelledby": `${base}-pop`, className: "flex flex-col gap-4", children: [_jsx("p", { id: `${base}-pop`, className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: t("popularPages") }), _jsx("ul", { className: "grid gap-x-(--gutter) @2xl:grid-cols-2", children: links.slice(0, 6).map((l, i) => (_jsx("li", { className: "border-t border-line", children: _jsxs(BlockLink, { href: l.href, newTab: l.newTab, components: components, labels: labels, locale: locale, className: "group/nf flex items-center justify-between gap-4 py-4 text-body text-fg transition-colors hover:text-accent-ink focus-ring rounded-[2px]", children: [_jsx("span", { children: l.label }), _jsx(ArrowRight, { "aria-hidden": true, strokeWidth: 1.5, className: "size-4 text-fg-subtle transition-colors group-hover/nf:text-current rtl:-scale-x-100" })] }) }, i))) })] })) : null, phone && kind !== "maintenance" ? (_jsx("p", { className: "text-small text-fg-muted", children: t("stillStuck", { phone: "\u0000" })
|
|
31
32
|
.split("\u0000")
|
|
32
33
|
.flatMap((part, i) => i === 0
|
|
33
34
|
? [part]
|
|
@@ -20,6 +20,7 @@ export interface PageIntroProps extends Omit<BlockBaseProps, "variant" | "labels
|
|
|
20
20
|
/** The page `h1` (inner pages). One `*emphasis*`. */
|
|
21
21
|
title: React.ReactNode;
|
|
22
22
|
lead?: React.ReactNode;
|
|
23
|
+
/** 21:9 band under the intro. `sizes`: `min(100vw, 90rem)`. */
|
|
23
24
|
media?: SiteImage;
|
|
24
25
|
/** ruled: facts on the end side ("Service area" · "Asian side"), up to 3. */
|
|
25
26
|
meta?: SiteMeta[];
|
|
@@ -5,6 +5,7 @@ import { BlockHeading } from "../block-heading.js";
|
|
|
5
5
|
import { BlockImage } from "../block-image.js";
|
|
6
6
|
import { BlockLink } from "../block-link.js";
|
|
7
7
|
import { field, siteText, useBlockIds } from "./_kit-1.js";
|
|
8
|
+
import { headingLines } from "./shared/heading-lines.js";
|
|
8
9
|
/** `BreadcrumbList` JSON-LD for the crumbs (absolute URLs need `origin`). */
|
|
9
10
|
export function breadcrumbJsonLd(crumbs, origin = "") {
|
|
10
11
|
if (!crumbs.length)
|
|
@@ -31,7 +32,7 @@ export function PageIntro({ variant = "simple", breadcrumb, eyebrow, title, lead
|
|
|
31
32
|
const last = i === breadcrumb.length - 1;
|
|
32
33
|
return (_jsxs("li", { className: "flex items-center gap-2", children: [i > 0 ? (_jsx("span", { "aria-hidden": true, className: "text-fg-subtle/70 rtl:-scale-x-100", children: "/" })) : null, c.href && !last ? (_jsx(BlockLink, { href: c.href, ...linkProps, className: "rounded-[2px] transition-colors hover:text-fg focus-ring", children: c.label })) : (_jsx("span", { "aria-current": last ? "page" : undefined, className: last ? "text-fg-muted" : undefined, children: c.label }))] }, i));
|
|
33
34
|
}) }) })) : eyebrow ? (_jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", ...field(editMode, "eyebrow"), children: eyebrow })) : null;
|
|
34
|
-
const heading = _jsx(BlockHeading, { id: headingId, level: headingLevel, size: "display-l", text: title, editMode: editMode, field: "props.title", labels: labels, locale: locale, className: "max-w-[20ch]" });
|
|
35
|
+
const heading = _jsx(BlockHeading, { id: headingId, level: headingLevel, size: "display-l", text: headingLines(title), editMode: editMode, field: "props.title", labels: labels, locale: locale, className: "max-w-[20ch]" });
|
|
35
36
|
const leadEl = lead ? (_jsx("p", { className: "vb-lead max-w-[52ch] text-lead text-fg-muted", ...field(editMode, "lead"), children: lead })) : null;
|
|
36
37
|
let content;
|
|
37
38
|
if (variant === "ruled") {
|
|
@@ -8,6 +8,7 @@ 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"> {
|
|
@@ -5,6 +5,7 @@ import { SectionHeader } from "../section-header.js";
|
|
|
5
5
|
import { BlockImage } from "../block-image.js";
|
|
6
6
|
import { CtaList, indexLabel, useBlockIds } from "./_kit-1.js";
|
|
7
7
|
import { Reveal } from "../../motion/reveal.js";
|
|
8
|
+
import { columnSizes } from "./shared/image-sizes.js";
|
|
8
9
|
/**
|
|
9
10
|
* "How we work" — discovery → design → launch; a first spa visit; SaaS setup. An ordered list
|
|
10
11
|
* (the order is announced; the numbers are visual). Server component.
|
|
@@ -25,7 +26,7 @@ export function Steps({ variant = "vertical", eyebrow, title, lead, steps, actio
|
|
|
25
26
|
return (_jsx(Tag, { className: cn("text-body text-fg-muted text-pretty", Tag === "p" ? "max-w-[52ch]" : "flex min-w-0 flex-col gap-3"), ...f(i, "text"), children: s.text }));
|
|
26
27
|
};
|
|
27
28
|
const duration = (s, i) => s.duration ? (_jsx("span", { className: "inline-flex w-fit items-center rounded-full border border-line px-2.5 py-0.5 font-mono text-[0.6875rem] tabular tracking-[0.04em] text-fg-muted", ...f(i, "duration"), children: s.duration })) : null;
|
|
28
|
-
const media = (s) => (s.media ? _jsx(BlockImage, { ...s.media, aspect: "3:2", sizes:
|
|
29
|
+
const media = (s) => (s.media ? _jsx(BlockImage, { ...s.media, aspect: "3:2", sizes: columnSizes(4 / 12, { narrow: "min(100vw, 28rem)" }), className: "mt-2 max-w-md rounded-ui", components: components, labels: labels }) : null);
|
|
29
30
|
const header = title || eyebrow || lead ? _jsx(SectionHeader, { eyebrow: eyebrow, title: title, lead: lead, align: variant === "horizontal" && lead ? "split" : "start", headingLevel: headingLevel, headingId: headingId, editMode: editMode, labels: labels, locale: locale }) : null;
|
|
30
31
|
const actionsEl = actions?.length ? _jsx(CtaList, { actions: actions, components: components, labels: labels, locale: locale, editMode: editMode }) : null;
|
|
31
32
|
let content;
|
|
@@ -535,7 +535,7 @@ export declare const mediaTextBlock: import("../schema/index.js").BlockDefinitio
|
|
|
535
535
|
"4:5": "4:5";
|
|
536
536
|
"3:2": "3:2";
|
|
537
537
|
}>>;
|
|
538
|
-
}, z.core.$strip>, "side" | "
|
|
538
|
+
}, z.core.$strip>, "side" | "bleed" | "overlap">;
|
|
539
539
|
export declare const ctaBlock: import("../schema/index.js").BlockDefinition<z.ZodObject<{
|
|
540
540
|
eyebrow: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
541
541
|
title: z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>;
|
|
@@ -589,9 +589,14 @@ export declare const menuBlock: import("../schema/index.js").BlockDefinition<z.Z
|
|
|
589
589
|
vegetarian: "vegetarian";
|
|
590
590
|
vegan: "vegan";
|
|
591
591
|
"gluten-free": "gluten-free";
|
|
592
|
+
"dairy-free": "dairy-free";
|
|
593
|
+
"nut-free": "nut-free";
|
|
594
|
+
halal: "halal";
|
|
592
595
|
spicy: "spicy";
|
|
593
596
|
new: "new";
|
|
594
597
|
"chef-pick": "chef-pick";
|
|
598
|
+
"to-share": "to-share";
|
|
599
|
+
seasonal: "seasonal";
|
|
595
600
|
}>>>;
|
|
596
601
|
allergens: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
597
602
|
sesame: "sesame";
|
|
@@ -10,7 +10,7 @@ import { BlogList } from "./blog-list.js";
|
|
|
10
10
|
import { ArticleLayout } from "./article-layout.js";
|
|
11
11
|
import { ContactForm } from "./contact-form.js";
|
|
12
12
|
import { MapHours, localBusinessJsonLd } from "./map-hours.js";
|
|
13
|
-
import { MenuList, menuJsonLd, ALLERGENS } from "./menu-list.js";
|
|
13
|
+
import { MenuList, menuJsonLd, ALLERGENS, MENU_TAGS } from "./menu-list.js";
|
|
14
14
|
import { GallerySection } from "./gallery-section.js";
|
|
15
15
|
import { ConsentBanner } from "./consent-banner.js";
|
|
16
16
|
import { ContactDock } from "./contact-dock.js";
|
|
@@ -367,7 +367,7 @@ const MenuItemSchema = f.group({
|
|
|
367
367
|
description: f.text({ max: 160 }).optional(),
|
|
368
368
|
prices: f.list(f.group({ label: f.text({ max: 20 }).optional(), amount: f.money().optional() }), { min: 1, max: 4 }),
|
|
369
369
|
duration: f.text({ max: 12 }).optional(),
|
|
370
|
-
tags: z.array(z.enum(
|
|
370
|
+
tags: z.array(z.enum(MENU_TAGS)).optional(),
|
|
371
371
|
allergens: z.array(z.enum(ALLERGENS)).optional(),
|
|
372
372
|
image: f.image({ aspect: "1:1" }).optional(),
|
|
373
373
|
unavailable: f.toggle().optional(),
|
|
@@ -4,6 +4,7 @@ import { Container, Section } from "../section.js";
|
|
|
4
4
|
import { BlockHeading } from "../block-heading.js";
|
|
5
5
|
import { RichText } from "../rich-text.js";
|
|
6
6
|
import { field, useBlockIds } from "./_kit-1.js";
|
|
7
|
+
import { headingLines } from "./shared/heading-lines.js";
|
|
7
8
|
/**
|
|
8
9
|
* Long text — an about page, a privacy notice, a service description. Pure server rendering of
|
|
9
10
|
* the RichDoc format; no editor code reaches the site.
|
|
@@ -15,7 +16,7 @@ import { field, useBlockIds } from "./_kit-1.js";
|
|
|
15
16
|
export function RichTextSection({ variant = "column", eyebrow, title, body, dropCap, note, size = "md", id, theme, anchor, className, headingLevel = 2, labels, locale, components, editMode }) {
|
|
16
17
|
const { base, headingId } = useBlockIds(id, "text");
|
|
17
18
|
const offset = (title ? 1 : 0);
|
|
18
|
-
const heading = title ? _jsx(BlockHeading, { id: headingId, level: headingLevel, size: "display-m", text: title, editMode: editMode, field: "props.title", labels: labels, locale: locale }) : null;
|
|
19
|
+
const heading = title ? _jsx(BlockHeading, { id: headingId, level: headingLevel, size: "display-m", text: headingLines(title), editMode: editMode, field: "props.title", labels: labels, locale: locale }) : null;
|
|
19
20
|
const eyebrowEl = eyebrow ? (_jsx("p", { className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", ...field(editMode, "eyebrow"), children: eyebrow })) : null;
|
|
20
21
|
const text = (cls) => (_jsx(RichText, { doc: body, size: size, headingOffset: offset, components: components, labels: labels, locale: locale, editMode: editMode, field: "props.body", className: cls }));
|
|
21
22
|
let content;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A heading string with hand-set line breaks: `"Ateşte közde,\nmuhabbet sofrada."`. Each line is
|
|
4
|
+
* a block `<span>` and the lines are joined by a real space, so `textContent` (search engines,
|
|
5
|
+
* screen readers, reader mode, copy-paste) reads "közde, muhabbet", never "közde,muhabbet".
|
|
6
|
+
* `*emphasis*` works across and within lines. Strings without `\n` and ReactNodes pass through
|
|
7
|
+
* unchanged (BlockHeading renders their emphasis). Server-safe.
|
|
8
|
+
*/
|
|
9
|
+
export declare function headingLines(text: React.ReactNode): React.ReactNode;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { parseEmphasis } from "../../schema/emphasis.js";
|
|
4
|
+
/**
|
|
5
|
+
* A heading string with hand-set line breaks: `"Ateşte közde,\nmuhabbet sofrada."`. Each line is
|
|
6
|
+
* a block `<span>` and the lines are joined by a real space, so `textContent` (search engines,
|
|
7
|
+
* screen readers, reader mode, copy-paste) reads "közde, muhabbet", never "közde,muhabbet".
|
|
8
|
+
* `*emphasis*` works across and within lines. Strings without `\n` and ReactNodes pass through
|
|
9
|
+
* unchanged (BlockHeading renders their emphasis). Server-safe.
|
|
10
|
+
*/
|
|
11
|
+
export function headingLines(text) {
|
|
12
|
+
if (typeof text !== "string" || !text.includes("\n"))
|
|
13
|
+
return text;
|
|
14
|
+
const lines = [[]];
|
|
15
|
+
for (const part of parseEmphasis(text)) {
|
|
16
|
+
part.text.split("\n").forEach((chunk, i) => {
|
|
17
|
+
if (i > 0)
|
|
18
|
+
lines.push([]);
|
|
19
|
+
if (chunk)
|
|
20
|
+
lines[lines.length - 1].push({ text: chunk, em: part.em });
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const filled = lines
|
|
24
|
+
.map((parts) => parts.map((p, i, all) => {
|
|
25
|
+
let t = i === 0 ? p.text.trimStart() : p.text;
|
|
26
|
+
if (i === all.length - 1)
|
|
27
|
+
t = t.trimEnd();
|
|
28
|
+
return { ...p, text: t };
|
|
29
|
+
}).filter((p) => p.text))
|
|
30
|
+
.filter((parts) => parts.length);
|
|
31
|
+
return filled.map((parts, i) => (_jsxs(React.Fragment, { children: [i > 0 ? " " : null, _jsx("span", { className: "vb-heading-line block", children: parts.map((p, j) => (p.em ? _jsx("em", { children: p.text }, j) : _jsx(React.Fragment, { children: p.text }, j))) })] }, i)));
|
|
32
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** `--container` (76rem) — the content column every Container caps at. */
|
|
2
|
+
export declare const CONTAINER_REM = 76;
|
|
3
|
+
/**
|
|
4
|
+
* A column that spans `fraction` of the container from `from` up, and `narrow` below.
|
|
5
|
+
* `columnSizes(5 / 12)` → `"(min-width: 56rem) min(42vw, 32rem), 100vw"`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function columnSizes(fraction: number, opts?: {
|
|
8
|
+
from?: "42rem" | "56rem";
|
|
9
|
+
narrow?: string;
|
|
10
|
+
bleed?: boolean;
|
|
11
|
+
}): string;
|
|
12
|
+
/** Common presets. */
|
|
13
|
+
export declare const SITE_SIZES: {
|
|
14
|
+
/** Edge-to-edge photo (full-bleed hero, editorial strip). */
|
|
15
|
+
readonly bleed: "100vw";
|
|
16
|
+
/** A photo as wide as the content container. */
|
|
17
|
+
readonly container: "min(100vw, 76rem)";
|
|
18
|
+
/** A photo only shown on narrow containers (< 56rem), full width there. */
|
|
19
|
+
readonly narrowOnly: "min(100vw, 56rem)";
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Filmstrip cell: fixed height `clamp(15rem, 38cqi, 30rem)`, width = height × ratio.
|
|
23
|
+
* `filmstripSizes(4 / 5)` → `"max(12rem, min(31vw, 24rem))"`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function filmstripSizes(ratio: number): string;
|