@vamos-ui/react 0.3.0 → 0.4.1
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/README.md +4 -0
- package/dist/blocks/block-heading.js +1 -1
- package/dist/blocks/block-image.js +1 -1
- package/dist/blocks/block-link.js +2 -2
- package/dist/blocks/index.d.ts +5 -5
- package/dist/blocks/index.js +5 -5
- package/dist/blocks/labels.d.ts +8 -0
- package/dist/blocks/labels.js +60 -0
- package/dist/blocks/section-header.js +1 -1
- package/dist/blocks/section.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/feature-tabs.js +1 -1
- package/dist/blocks/site/footer.d.ts +11 -2
- package/dist/blocks/site/footer.js +36 -11
- 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 +5 -3
- 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 +17 -1
- package/dist/blocks/site/shared/labels-2.js +832 -6
- package/dist/blocks/site/shared/open-now.d.ts +1 -1
- package/dist/blocks/site/shared/open-now.js +1 -1
- package/dist/blocks/site/shared/opening-hours.d.ts +18 -2
- package/dist/blocks/site/shared/opening-hours.js +36 -7
- package/dist/blocks/site/shared/tap.d.ts +9 -0
- package/dist/blocks/site/shared/tap.js +16 -0
- package/dist/blocks/site/site-shell.js +3 -3
- 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/business/approval-flow.js +2 -2
- package/dist/business/import-wizard.js +1 -1
- package/dist/business/line-items.js +1 -1
- package/dist/business/multi-step-form.js +2 -2
- package/dist/business/permission-matrix.js +1 -1
- package/dist/business/setup-checklist.js +2 -2
- package/dist/business/signer-status.js +1 -1
- package/dist/charts/bar-chart.js +1 -1
- package/dist/charts/bar-list.js +1 -1
- package/dist/charts/chart-shell.d.ts +9 -2
- package/dist/charts/chart-shell.js +11 -5
- package/dist/charts/chart.js +1 -1
- package/dist/charts/combo-chart.js +5 -7
- package/dist/charts/donut-chart.js +1 -1
- package/dist/charts/funnel.js +1 -1
- package/dist/charts/gauge.js +2 -2
- package/dist/charts/utils.d.ts +10 -0
- package/dist/charts/utils.js +46 -0
- package/dist/components/accordion.d.ts +1 -1
- package/dist/components/accordion.js +8 -8
- package/dist/components/activity-timeline.js +11 -11
- package/dist/components/address-field.js +3 -3
- package/dist/components/alert-dialog.d.ts +1 -1
- package/dist/components/alert-dialog.js +8 -8
- package/dist/components/alert.js +2 -2
- package/dist/components/app-shell.js +3 -3
- package/dist/components/aspect-ratio.d.ts +26 -0
- package/dist/components/aspect-ratio.js +42 -0
- package/dist/components/attachment-list.js +2 -2
- package/dist/components/audit-log.js +1 -1
- package/dist/components/avatar.d.ts +1 -1
- package/dist/components/avatar.js +8 -8
- package/dist/components/backup-codes.js +1 -1
- package/dist/components/badge.js +3 -3
- package/dist/components/breadcrumb.js +10 -9
- package/dist/components/button-group.js +4 -4
- package/dist/components/button.js +4 -4
- package/dist/components/calendar.js +5 -5
- package/dist/components/card.js +8 -8
- package/dist/components/carousel.d.ts +12 -3
- package/dist/components/carousel.js +17 -14
- package/dist/components/checkbox.d.ts +1 -1
- package/dist/components/checkbox.js +6 -6
- package/dist/components/collapsible.d.ts +36 -0
- package/dist/components/collapsible.js +37 -0
- package/dist/components/color-picker.js +3 -3
- package/dist/components/combobox.js +1 -1
- package/dist/components/command.js +9 -9
- package/dist/components/comment-thread.js +3 -3
- package/dist/components/context-menu.d.ts +1 -1
- package/dist/components/context-menu.js +9 -9
- package/dist/components/currency-input.js +1 -1
- package/dist/components/date-picker.js +2 -2
- package/dist/components/date-range-picker.js +1 -1
- package/dist/components/description-list.js +2 -2
- package/dist/components/dialog.d.ts +1 -1
- package/dist/components/dialog.js +9 -9
- package/dist/components/dropdown-menu.d.ts +1 -1
- package/dist/components/dropdown-menu.js +9 -9
- package/dist/components/editor-layout.js +9 -9
- package/dist/components/empty-state.js +2 -2
- package/dist/components/entity-card.js +1 -1
- package/dist/components/file-upload.js +5 -5
- package/dist/components/formatted-input.js +3 -3
- package/dist/components/hierarchy-chart.js +1 -1
- package/dist/components/hover-card.d.ts +1 -1
- package/dist/components/hover-card.js +3 -3
- package/dist/components/iban-input.js +1 -1
- package/dist/components/id-input.js +3 -3
- package/dist/components/image.d.ts +18 -2
- package/dist/components/image.js +16 -9
- package/dist/components/input-otp.js +6 -6
- package/dist/components/input.d.ts +1 -1
- package/dist/components/input.js +6 -6
- package/dist/components/item.js +4 -4
- package/dist/components/kbd.js +3 -3
- package/dist/components/list-layout.js +9 -9
- package/dist/components/menubar.d.ts +96 -0
- package/dist/components/menubar.js +123 -0
- package/dist/components/native-select.js +1 -1
- package/dist/components/navigation-menu.d.ts +1 -1
- package/dist/components/navigation-menu.js +12 -11
- package/dist/components/notification-center.js +4 -4
- package/dist/components/page-header.js +3 -3
- package/dist/components/pagination.js +9 -9
- package/dist/components/phone-input.js +1 -1
- package/dist/components/pin-map.js +1 -1
- package/dist/components/placeholder-art.d.ts +51 -0
- package/dist/components/placeholder-art.js +82 -0
- package/dist/components/popover.d.ts +1 -1
- package/dist/components/popover.js +7 -7
- package/dist/components/progress.js +3 -3
- package/dist/components/qr-code.js +1 -1
- package/dist/components/radio-group.d.ts +1 -1
- package/dist/components/radio-group.js +4 -4
- package/dist/components/record-detail-layout.js +10 -10
- package/dist/components/record-picker.js +2 -2
- package/dist/components/resizable.js +2 -2
- package/dist/components/scroll-area.d.ts +1 -1
- package/dist/components/scroll-area.js +4 -4
- package/dist/components/select.d.ts +1 -1
- package/dist/components/select.js +10 -10
- package/dist/components/separator.d.ts +1 -1
- package/dist/components/separator.js +3 -3
- package/dist/components/settings-layout.js +6 -6
- package/dist/components/sheet.js +3 -3
- package/dist/components/sidebar.js +12 -11
- package/dist/components/signature-pad.js +1 -1
- package/dist/components/skeleton.js +5 -5
- package/dist/components/sla-timer.js +2 -2
- package/dist/components/slider.d.ts +1 -1
- package/dist/components/slider.js +2 -2
- package/dist/components/slug-input.js +1 -1
- package/dist/components/sparkline.js +1 -1
- package/dist/components/spinner.js +2 -2
- package/dist/components/stat.js +5 -5
- package/dist/components/status-picker.js +2 -2
- package/dist/components/stepper.js +1 -1
- package/dist/components/switch.d.ts +1 -1
- package/dist/components/switch.js +3 -3
- package/dist/components/table.js +10 -10
- package/dist/components/tabs.d.ts +1 -1
- package/dist/components/tabs.js +11 -11
- package/dist/components/tag-input.js +1 -1
- package/dist/components/task-list.js +1 -1
- package/dist/components/time-picker.js +3 -3
- package/dist/components/toggle.d.ts +2 -1
- package/dist/components/toggle.js +7 -6
- package/dist/components/tooltip.d.ts +1 -1
- package/dist/components/tooltip.js +5 -4
- package/dist/components/top-bar.js +4 -4
- package/dist/components/tour.js +1 -1
- package/dist/components/tree-select.js +2 -2
- package/dist/components/tree-view.js +3 -3
- package/dist/components/typography.js +11 -11
- package/dist/components/user-picker.js +2 -2
- package/dist/components/version-history.js +3 -3
- package/dist/data-table/data-table.js +1 -1
- package/dist/data-table/filter-bar.js +2 -2
- package/dist/data-table/index.d.ts +15 -8
- package/dist/data-table/index.js +9 -8
- package/dist/data-table/saved-views.js +1 -1
- package/dist/editor/block-editor.js +1 -1
- package/dist/editor/context.js +1 -1
- package/dist/editor/menus.js +1 -1
- package/dist/fonts/font-picker.js +3 -3
- package/dist/fonts/pairing-picker.js +1 -1
- package/dist/fonts/provider.js +1 -1
- package/dist/index.d.ts +188 -90
- package/dist/index.js +94 -90
- package/dist/lib/data-islands.js +1 -1
- package/dist/lib/field-context.js +1 -1
- package/dist/lib/floating-arrow.js +1 -1
- package/dist/lib/islands.js +1 -1
- package/dist/lib/layer.js +3 -3
- package/dist/lib/locale.js +2 -2
- package/dist/lib/menu.js +2 -2
- package/dist/lib/provider.js +4 -4
- package/dist/lib/theme-scope.js +4 -3
- package/dist/lib/value-format.js +1 -1
- package/dist/lib/variants.js +2 -2
- 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/focal-point.js +1 -1
- package/dist/media/gallery.js +1 -1
- package/dist/media/image-cropper.js +1 -1
- package/dist/media/image-field.js +2 -2
- package/dist/media/lightbox.js +4 -3
- package/dist/media/link-preview.js +3 -3
- package/dist/media/media-library.js +4 -4
- package/dist/media/media-picker.js +1 -1
- package/dist/media/media-provider.js +1 -1
- package/dist/media/video-player.js +4 -4
- package/dist/motion/count-up.js +1 -1
- package/dist/motion/motion-scope.js +2 -2
- package/dist/motion/page-transition.js +1 -1
- package/dist/motion/parallax.js +2 -2
- package/dist/motion/pointer.js +3 -3
- package/dist/motion/reveal.js +3 -3
- package/dist/motion/skeleton-swap.js +1 -1
- package/dist/motion/text-reveal.d.ts +3 -1
- package/dist/motion/text-reveal.js +6 -4
- package/dist/motion/ticker.js +1 -1
- package/dist/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/dist/render/block-renderer.js +1 -1
- package/dist/scheduler/gantt.js +2 -2
- package/dist/scheduler/resource-calendar.js +2 -2
- package/dist/scheduler/scheduler.js +3 -3
- package/dist/scheduler/time-tracker.js +4 -4
- package/dist/sortable/kanban.js +2 -2
- package/dist/sortable/sortable-list.js +2 -2
- package/dist/sortable/sortable.js +4 -4
- 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
|
@@ -4,12 +4,35 @@ import { cn2 as cn } from "./shared/kit-2.js";
|
|
|
4
4
|
import { Section, Container } from "../section.js";
|
|
5
5
|
import { BlockImage } from "../block-image.js";
|
|
6
6
|
import { BlockLink } from "../block-link.js";
|
|
7
|
+
import { JsonLd } from "../json-ld.js";
|
|
7
8
|
import { labelTemplates, site2Text } from "./shared/labels-2.js";
|
|
8
9
|
import { addressLines, fieldAttr, prettyPhone, telHref, useBlockIds2 } from "./shared/kit-2.js";
|
|
9
10
|
import { BrandIcon, BRAND_NAMES } from "./shared/brand-icons.js";
|
|
10
11
|
import { ConsentSettingsButton } from "./shared/consent-island.js";
|
|
11
12
|
import { ContactFormIsland } from "./shared/contact-form-island.js";
|
|
12
13
|
import { contactIslandLabelKeys } from "./shared/island-keys.js";
|
|
14
|
+
import { TAP } from "./shared/tap.js";
|
|
15
|
+
/** `Organization` JSON-LD from the footer's details (absolute brand href becomes `url`). */
|
|
16
|
+
export function footerJsonLd(props) {
|
|
17
|
+
const { brand, company, contact, social = [] } = props;
|
|
18
|
+
const address = contact?.address;
|
|
19
|
+
return {
|
|
20
|
+
"@type": "Organization",
|
|
21
|
+
name: brand.name,
|
|
22
|
+
...(company?.legalName && company.legalName !== brand.name ? { legalName: company.legalName } : null),
|
|
23
|
+
...(brand.href && /^https?:\/\//.test(brand.href) ? { url: brand.href } : null),
|
|
24
|
+
...(contact?.phone ? { telephone: contact.phone } : null),
|
|
25
|
+
...(contact?.email ? { email: contact.email } : null),
|
|
26
|
+
...(address
|
|
27
|
+
? {
|
|
28
|
+
address: typeof address === "string"
|
|
29
|
+
? address.replace(/\n/g, ", ")
|
|
30
|
+
: { "@type": "PostalAddress", streetAddress: address.line, addressLocality: address.district ? `${address.district}, ${address.city}` : address.city, ...(address.region ? { addressRegion: address.region } : null), ...(address.postalCode ? { postalCode: address.postalCode } : null), addressCountry: address.country },
|
|
31
|
+
}
|
|
32
|
+
: null),
|
|
33
|
+
...(social.length ? { sameAs: social.map((s) => s.href) } : null),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
13
36
|
const isImage = (v) => !!v && typeof v === "object" && "src" in v && !React.isValidElement(v);
|
|
14
37
|
/**
|
|
15
38
|
* Site footer: navigation, contact, socials, legal links and the company details e-commerce law
|
|
@@ -21,41 +44,43 @@ const isImage = (v) => !!v && typeof v === "object" && "src" in v && !React.isVa
|
|
|
21
44
|
* company={{ legalName: "Vamos Dijital Ajans Ltd. Şti.", registry: [{ label: "Tax no.", value: "923 145 6780" }] }} />
|
|
22
45
|
* ```
|
|
23
46
|
*/
|
|
24
|
-
export function Footer({ variant = "columns", brand, about, columns = [], contact, social = [], legalLinks = [], company, credit, cookieSettings = true, newsletter, year, id, anchor, theme, className, labels, locale = "en-US", components, editMode, }) {
|
|
47
|
+
export function Footer({ variant = "columns", brand, about, columns = [], contact, social = [], legalLinks = [], company, credit, cookieSettings = true, newsletter, year, jsonLd = true, id, anchor, theme, className, labels, locale = "en-US", components, editMode, }) {
|
|
25
48
|
const t = site2Text(labels, locale);
|
|
26
49
|
const { base } = useBlockIds2(id, "footer");
|
|
27
50
|
const y = String(year ?? new Date().getFullYear());
|
|
28
51
|
const ctx = { t, locale, components, labels, editMode };
|
|
29
52
|
const copyright = t("copyright", { year: y, name: company?.legalName ?? brand.name });
|
|
30
|
-
|
|
53
|
+
// The edit canvas skips it; the document renderer collects the definition's JSON-LD instead.
|
|
54
|
+
const structured = jsonLd && !editMode ? _jsx(JsonLd, { data: { ...footerJsonLd({ brand, company, contact, social }), ...(typeof jsonLd === "object" ? jsonLd : null) } }) : null;
|
|
55
|
+
const legalLine = (_jsxs("div", { className: "flex flex-col gap-3 text-small text-fg-muted @4xl:flex-row @4xl:items-center @4xl:justify-between @4xl:gap-8", children: [_jsx("p", { children: copyright }), _jsx("nav", { "aria-label": t("legal"), children: _jsxs("ul", { className: "flex flex-wrap items-center gap-x-5 gap-y-2 pointer-coarse:gap-y-0", children: [legalLinks.map((l, i) => (_jsx("li", { children: _jsx(BlockLink, { href: l.href, newTab: l.newTab, components: components, labels: labels, locale: locale, className: cn(TAP, "rounded-[2px] text-fg-muted underline-offset-[0.25em] transition-colors hover:text-fg hover:underline focus-ring"), children: l.label }) }, i))), cookieSettings ? (_jsx("li", { children: _jsx(ConsentSettingsButton, { className: cn(TAP, "text-fg-muted transition-colors hover:text-fg"), children: t("cookieSettings") }) })) : null, credit ? (_jsx("li", { children: _jsx(BlockLink, { href: credit.href, newTab: credit.newTab, components: components, labels: labels, locale: locale, className: cn(TAP, "rounded-[2px] text-fg-muted underline-offset-[0.25em] hover:text-fg hover:underline focus-ring"), children: t("madeBy", { name: typeof credit.label === "string" ? credit.label : "" }) }) })) : null] }) })] }));
|
|
31
56
|
// The legal name already sits in the © line; the company row only earns its place with registry data.
|
|
32
57
|
const companyLine = company?.registry?.length ? (_jsxs("div", { className: "flex flex-col gap-1.5 @4xl:flex-row @4xl:items-baseline @4xl:gap-6", ...fieldAttr(editMode, "company"), children: [_jsx("p", { className: "text-small text-fg-muted", children: company.legalName }), company.registry?.length ? (_jsx("dl", { className: "flex flex-wrap gap-x-6 gap-y-1 font-mono text-eyebrow uppercase tracking-[0.06em] text-fg-muted", children: company.registry.map((r, i) => (_jsxs("div", { className: "flex gap-2", children: [_jsx("dt", { children: r.label }), _jsx("dd", { className: "tabular text-fg-muted", children: r.value })] }, i))) })) : null] })) : null;
|
|
33
58
|
if (variant === "minimal") {
|
|
34
|
-
return (
|
|
59
|
+
return (_jsxs(Section, { as: "footer", theme: { rhythm: "tight", ...theme }, anchor: anchor, blockId: id, blockType: "footer", editMode: editMode, className: cn("vb-footer", className), "data-variant": "minimal", "aria-label": t("footer"), children: [structured, _jsxs(Container, { className: "flex flex-col gap-6 @4xl:flex-row @4xl:items-center @4xl:justify-between", children: [_jsx(BrandMark, { brand: brand, ctx: ctx }), columns[0]?.links.length ? (_jsx("nav", { "aria-label": t("footerNav"), children: _jsx("ul", { className: "flex flex-wrap gap-x-6 gap-y-2 text-small", children: columns.flatMap((c) => c.links).slice(0, 5).map((l, i) => (_jsx("li", { children: _jsx(FooterLink, { link: l, ctx: ctx }) }, i))) }) })) : null, _jsxs("div", { className: "flex flex-wrap items-center gap-x-6 gap-y-3", children: [_jsx(SocialList, { social: social, ctx: ctx }), _jsx("p", { className: "text-small text-fg-muted", children: copyright })] })] })] }));
|
|
35
60
|
}
|
|
36
|
-
const linkColumns = columns.slice(0, 4).map((c, i) => (_jsxs("nav", { "aria-labelledby": `${base}-col-${i}`, className: "flex flex-col gap-4", children: [_jsx("p", { id: `${base}-col-${i}`, className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: c.title }), _jsx("ul", { className: "flex flex-col gap-2.5", children: c.links.slice(0, 8).map((l, j) => (_jsx("li", { children: _jsx(FooterLink, { link: l, ctx: ctx }) }, j))) })] }, i)));
|
|
37
|
-
const contactBlock = contact ? (_jsxs("div", { className: "flex flex-col gap-2 text-small text-fg-muted", children: [contact.address ? (_jsx("address", { className: "not-italic", children: (typeof contact.address === "string" ? contact.address.split("\n") : addressLines(contact.address, { locale })).map((l, i) => (_jsx("span", { className: "block", children: l }, i))) })) : null, contact.phone ? (_jsx("a", { href: telHref(contact.phone), className: "self-start text-fg tabular underline decoration-line underline-offset-[0.25em] hover:decoration-current focus-ring rounded-[2px]", "data-track": "call", children: prettyPhone(contact.phone) })) : null, contact.email ? (_jsx("a", { href: `mailto:${contact.email}`, className: "self-start text-fg underline decoration-line underline-offset-[0.25em] hover:decoration-current focus-ring rounded-[2px]", "data-track": "email", children: contact.email })) : null] })) : null;
|
|
61
|
+
const linkColumns = columns.slice(0, 4).map((c, i) => (_jsxs("nav", { "aria-labelledby": `${base}-col-${i}`, className: "flex flex-col gap-4", children: [_jsx("p", { id: `${base}-col-${i}`, className: "font-mono text-eyebrow uppercase tracking-[0.08em] text-fg-muted", children: c.title }), _jsx("ul", { className: "flex flex-col gap-2.5 pointer-coarse:gap-0", children: c.links.slice(0, 8).map((l, j) => (_jsx("li", { children: _jsx(FooterLink, { link: l, ctx: ctx }) }, j))) })] }, i)));
|
|
62
|
+
const contactBlock = contact ? (_jsxs("div", { className: "flex flex-col gap-2 text-small text-fg-muted", children: [contact.address ? (_jsx("address", { className: "not-italic", children: (typeof contact.address === "string" ? contact.address.split("\n") : addressLines(contact.address, { locale })).map((l, i) => (_jsx("span", { className: "block", children: l }, i))) })) : null, contact.phone ? (_jsx("a", { href: telHref(contact.phone), className: cn(TAP, "self-start text-fg tabular underline decoration-line underline-offset-[0.25em] hover:decoration-current focus-ring rounded-[2px]"), "data-track": "call", children: prettyPhone(contact.phone) })) : null, contact.email ? (_jsx("a", { href: `mailto:${contact.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: contact.email })) : null] })) : null;
|
|
38
63
|
const newsletterBlock = newsletter ? (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("p", { className: "text-body font-medium text-fg", children: newsletter.title ?? t("newsletter") }), _jsx("p", { className: "text-small text-fg-muted", children: newsletter.hint ?? t("newsletterHint") }), _jsx(ContactFormIsland, { action: newsletter.action, fields: [{ name: "email", required: true, label: t("emailAddress") }], labels: { ...labelTemplates(labels, contactIslandLabelKeys), send: t("subscribe"), sent: t("subscribed") }, locale: locale, layout: "minimal", idBase: `${base}-nl`, className: "vb-footer-newsletter" })] })) : null;
|
|
39
64
|
if (variant === "wordmark") {
|
|
40
|
-
return (_jsxs(Section, { as: "footer", theme: theme, anchor: anchor, blockId: id, blockType: "footer", editMode: editMode, className: cn("vb-footer overflow-hidden pb-0!", className), "data-variant": "wordmark", "aria-label": t("footer"), children: [_jsxs(Container, { className: "flex flex-col gap-12", children: [_jsxs("div", { className: "grid gap-x-(--gutter) gap-y-10 @2xl:grid-cols-2 @4xl:grid-cols-12", children: [_jsxs("div", { className: "flex flex-col gap-5 @4xl:col-span-4", children: [about ? _jsx("p", { className: "max-w-[40ch] text-fg-muted text-pretty", children: about }) : null, contactBlock, _jsx(SocialList, { social: social, ctx: ctx })] }), _jsx("div", { className: "grid grid-cols-2 gap-x-(--gutter) gap-y-10 @2xl:col-span-1 @4xl:col-span-8 @4xl:grid-cols-4", children: linkColumns })] }), newsletterBlock ? _jsx("div", { className: "max-w-xl", children: newsletterBlock }) : null, _jsxs("div", { className: "flex flex-col gap-4 border-t border-line pt-6", children: [companyLine, legalLine] })] }), _jsx("p", { "aria-hidden": true, className: "vb-wordmark select-none whitespace-nowrap px-(--gutter) font-display text-display-2xl leading-[0.78] text-fg",
|
|
65
|
+
return (_jsxs(Section, { as: "footer", theme: theme, anchor: anchor, blockId: id, blockType: "footer", editMode: editMode, className: cn("vb-footer overflow-hidden pb-0!", className), "data-variant": "wordmark", "aria-label": t("footer"), children: [structured, _jsxs(Container, { className: "flex flex-col gap-12", children: [_jsxs("div", { className: "grid gap-x-(--gutter) gap-y-10 @2xl:grid-cols-2 @4xl:grid-cols-12", children: [_jsxs("div", { className: "flex flex-col gap-5 @4xl:col-span-4", children: [about ? _jsx("p", { className: "max-w-[40ch] text-fg-muted text-pretty", children: about }) : null, contactBlock, _jsx(SocialList, { social: social, ctx: ctx })] }), _jsx("div", { className: "grid grid-cols-2 gap-x-(--gutter) gap-y-10 @2xl:col-span-1 @4xl:col-span-8 @4xl:grid-cols-4", children: linkColumns })] }), newsletterBlock ? _jsx("div", { className: "max-w-xl", children: newsletterBlock }) : null, _jsxs("div", { className: "flex flex-col gap-4 border-t border-line pt-6", children: [companyLine, legalLine] })] }), _jsx("p", { "aria-hidden": true, className: "vb-wordmark select-none whitespace-nowrap px-(--gutter) font-display text-display-2xl leading-[0.78] text-fg",
|
|
41
66
|
// Fill the width: a short name at the fixed display size reads as an accidental crop. The
|
|
42
67
|
// size never goes above what fits, so a long name is not cut at the side; wide display
|
|
43
68
|
// faces (Syne 800 and the like) set `--vb-wordmark-em` (average glyph width, default 0.58em).
|
|
44
69
|
style: typeof brand.name === "string" ? { fontSize: `min(calc((100cqi - 2 * var(--gutter, 1.5rem)) / (${Math.max(4, [...brand.name].length)} * var(--vb-wordmark-em, 0.58))), 22rem)` } : undefined, children: brand.name })] }));
|
|
45
70
|
}
|
|
46
|
-
return (
|
|
71
|
+
return (_jsxs(Section, { as: "footer", theme: theme, anchor: anchor, blockId: id, blockType: "footer", editMode: editMode, className: cn("vb-footer", className), "data-variant": "columns", "aria-label": t("footer"), children: [structured, _jsxs(Container, { className: "flex flex-col gap-14", children: [_jsxs("div", { className: "grid gap-x-(--gutter) gap-y-12 @4xl:grid-cols-12", children: [_jsxs("div", { className: "flex flex-col gap-6 @4xl:col-span-4", children: [_jsx(BrandMark, { brand: brand, ctx: ctx }), about ? (_jsx("p", { className: "max-w-[40ch] text-small text-fg-muted text-pretty", ...fieldAttr(editMode, "about"), children: about })) : null, contactBlock, _jsx(SocialList, { social: social, ctx: ctx })] }), _jsx("div", { className: cn("grid grid-cols-2 gap-x-(--gutter) gap-y-10 @4xl:col-span-7 @4xl:col-start-6", columns.length >= 3 && "@2xl:grid-cols-3", columns.length >= 4 && "@6xl:grid-cols-4"), children: linkColumns })] }), newsletterBlock ? _jsx("div", { className: "max-w-xl border-t border-line pt-10", children: newsletterBlock }) : null, _jsxs("div", { className: "flex flex-col gap-4 border-t border-line pt-6", children: [companyLine, legalLine] })] })] }));
|
|
47
72
|
}
|
|
48
73
|
function BrandMark({ brand, ctx }) {
|
|
49
|
-
const mark = isImage(brand.logo) ? (_jsx(BlockImage, { ...brand.logo, alt: brand.logo.alt ?? brand.name, fit: "contain", className: "h-8 w-auto bg-transparent", components: ctx.components })) : ((brand.logo ?? _jsx("span", { className: "font-display text-display-m leading-none text-fg", children: brand.name })));
|
|
74
|
+
const mark = isImage(brand.logo) ? (_jsx(BlockImage, { ...brand.logo, alt: brand.logo.alt ?? brand.name, fit: "contain", sizes: "12rem", className: "h-8 w-auto bg-transparent", components: ctx.components })) : ((brand.logo ?? _jsx("span", { className: "font-display text-display-m leading-none text-fg", children: brand.name })));
|
|
50
75
|
if (!brand.href)
|
|
51
76
|
return _jsx("div", { className: "self-start", children: mark });
|
|
52
|
-
return (_jsx(BlockLink, { href: brand.href, components: ctx.components, labels: ctx.labels, locale: ctx.locale, "aria-label": brand.name, className: "self-start rounded-[2px] focus-ring", children: mark }));
|
|
77
|
+
return (_jsx(BlockLink, { href: brand.href, components: ctx.components, labels: ctx.labels, locale: ctx.locale, "aria-label": brand.name, className: "inline-flex min-h-6 items-center self-start rounded-[2px] focus-ring pointer-coarse:min-h-11", children: mark }));
|
|
53
78
|
}
|
|
54
79
|
function FooterLink({ link, ctx }) {
|
|
55
|
-
return (_jsx(BlockLink, { href: link.href, newTab: link.newTab, components: ctx.components, labels: ctx.labels, locale: ctx.locale, className: "rounded-[2px] text-small text-fg underline-offset-[0.25em] transition-colors hover:text-accent-ink hover:underline focus-ring", children: link.label }));
|
|
80
|
+
return (_jsx(BlockLink, { href: link.href, newTab: link.newTab, components: ctx.components, labels: ctx.labels, locale: ctx.locale, className: cn(TAP, "rounded-[2px] text-small text-fg underline-offset-[0.25em] transition-colors hover:text-accent-ink hover:underline focus-ring"), children: link.label }));
|
|
56
81
|
}
|
|
57
82
|
function SocialList({ social, ctx }) {
|
|
58
83
|
if (!social?.length)
|
|
59
84
|
return null;
|
|
60
|
-
return (_jsx("ul", { className: "flex flex-wrap items-center gap-1", "aria-label": ctx.t("social"), children: social.slice(0, 6).map((s, i) => (_jsx("li", { children: _jsx("a", { href: s.href, target: "_blank", rel: "noopener noreferrer", "aria-label": `${BRAND_NAMES[s.network]} ${ctx.t("opensInNewTab")}`, className: "inline-flex size-10 items-center justify-center rounded-ui-sm text-fg-muted transition-colors hover:bg-hover hover:text-fg focus-ring", "data-track": `social:${s.network}`, children: _jsx(BrandIcon, { name: s.network, className: "size-[18px]" }) }) }, i))) }));
|
|
85
|
+
return (_jsx("ul", { className: "flex flex-wrap items-center gap-1 pointer-coarse:-ms-1", "aria-label": ctx.t("social"), children: social.slice(0, 6).map((s, i) => (_jsx("li", { children: _jsx("a", { href: s.href, target: "_blank", rel: "noopener noreferrer", "aria-label": `${BRAND_NAMES[s.network]} ${ctx.t("opensInNewTab")}`, className: "inline-flex size-10 items-center justify-center rounded-ui-sm text-fg-muted transition-colors hover:bg-hover hover:text-fg focus-ring pointer-coarse:size-11", "data-track": `social:${s.network}`, children: _jsx(BrandIcon, { name: s.network, className: "size-[18px]" }) }) }, i))) }));
|
|
61
86
|
}
|
|
@@ -22,7 +22,12 @@ export interface GallerySectionProps extends Omit<BlockBaseProps, "variant" | "l
|
|
|
22
22
|
eyebrow?: React.ReactNode;
|
|
23
23
|
title?: React.ReactNode;
|
|
24
24
|
lead?: React.ReactNode;
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* 3–40 images, `alt` required. `sizes` per variant: mosaic `min(34vw, 26rem)` per 4-col cell
|
|
27
|
+
* (`min(67vw, 51rem)` for wide/big) above 42rem, 50vw/100vw below · grid `min(34vw, 26rem)` /
|
|
28
|
+
* 50vw · filmstrip from the cell height × ratio (≈ 12–24rem for 4:5) · split-caption
|
|
29
|
+
* `min(59vw, 45rem)` / 100vw.
|
|
30
|
+
*/
|
|
26
31
|
images: GalleryImage[];
|
|
27
32
|
/** Open images in a full-screen viewer. Default true. */
|
|
28
33
|
lightbox?: boolean;
|
|
@@ -8,6 +8,7 @@ import { site2Text } from "./shared/labels-2.js";
|
|
|
8
8
|
import { textOf, useBlockIds2 } from "./shared/kit-2.js";
|
|
9
9
|
import { GalleryLightbox } from "./shared/gallery-lightbox.js";
|
|
10
10
|
import { Stagger } from "../../motion/reveal.js";
|
|
11
|
+
import { columnSizes, filmstripSizes } from "./shared/image-sizes.js";
|
|
11
12
|
/**
|
|
12
13
|
* Venue, food and project photos — the section restaurants, spas and hotels live on. Server
|
|
13
14
|
* grid; the lightbox is the Media group's viewer (focus trap, Esc, arrows, "Image 3 of 12"),
|
|
@@ -25,19 +26,19 @@ export function GallerySection({ variant: variantProp = "mosaic", eyebrow, title
|
|
|
25
26
|
const header = title || eyebrow || lead ? _jsx(SectionHeader, { eyebrow: eyebrow, title: title, lead: lead, headingId: headingId, headingLevel: headingLevel, editMode: editMode, labels: labels, locale: locale }) : null;
|
|
26
27
|
let body;
|
|
27
28
|
if (variant === "filmstrip") {
|
|
28
|
-
body = (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx("ul", { className: "vb-filmstrip scrollbar-none flex snap-x snap-mandatory gap-2 overflow-x-auto pb-1", tabIndex: 0, "aria-label": textOf(title).replace(/\*/g, "") || t("galleryImages", { count: images.length }), children: images.map((g, i) => (_jsx("li", { className: "shrink-0 snap-start", children: _jsx(Cell, { g: g, i: i, ctx: ctx, className: "h-[clamp(15rem,38cqi,30rem)]", style: { aspectRatio: g.image.width && g.image.height ? `${g.image.width} / ${g.image.height}` : "4 / 5" }, sizes:
|
|
29
|
+
body = (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx("ul", { className: "vb-filmstrip scrollbar-none flex snap-x snap-mandatory gap-2 overflow-x-auto pb-1", tabIndex: 0, "aria-label": textOf(title).replace(/\*/g, "") || t("galleryImages", { count: images.length }), children: images.map((g, i) => (_jsx("li", { className: "shrink-0 snap-start", children: _jsx(Cell, { g: g, i: i, ctx: ctx, className: "h-[clamp(15rem,38cqi,30rem)]", style: { aspectRatio: g.image.width && g.image.height ? `${g.image.width} / ${g.image.height}` : "4 / 5" }, sizes: filmstripSizes(g.image.width && g.image.height ? g.image.width / g.image.height : 0.8), fill: true }) }, i))) }), _jsx("p", { className: "vb-container font-mono text-eyebrow tabular uppercase tracking-[0.08em] text-fg-muted", children: t("galleryImages", { count: images.length }) })] }));
|
|
29
30
|
}
|
|
30
31
|
else if (variant === "mosaic") {
|
|
31
|
-
body = (_jsx(Stagger, { asChild: true, effect: "clip", disabled: editMode, children: _jsx("ul", { className: "vb-mosaic grid grid-cols-2 gap-2 @2xl:grid-cols-12", children: images.map((g, i) => (_jsx("li", { "data-span": g.span ?? "1", className: "vb-mosaic-cell min-h-0", children: _jsx(Cell, { g: g, i: i, ctx: ctx, className: "size-full", sizes: "(
|
|
32
|
+
body = (_jsx(Stagger, { asChild: true, effect: "clip", disabled: editMode, children: _jsx("ul", { className: "vb-mosaic grid grid-cols-2 gap-2 @2xl:grid-cols-12", children: images.map((g, i) => (_jsx("li", { "data-span": g.span ?? "1", className: "vb-mosaic-cell min-h-0", children: _jsx(Cell, { g: g, i: i, ctx: ctx, className: "size-full", sizes: g.span === "wide" || g.span === "big" ? columnSizes(8 / 12, { from: "42rem" }) : columnSizes(4 / 12, { from: "42rem", narrow: "50vw" }), fill: true }) }, i))) }) }));
|
|
32
33
|
}
|
|
33
34
|
else if (variant === "split-caption") {
|
|
34
35
|
body = null;
|
|
35
36
|
}
|
|
36
37
|
else {
|
|
37
38
|
const a = aspect === "mixed" ? undefined : aspect;
|
|
38
|
-
body = (_jsx(Stagger, { asChild: true, disabled: editMode, children: _jsx("ul", { className: "grid grid-cols-2 gap-x-3 gap-y-6 @4xl:grid-cols-3 @4xl:gap-x-4", children: images.map((g, i) => (_jsx("li", { children: _jsxs("figure", { className: "flex flex-col gap-2.5", children: [_jsx(Cell, { g: g, i: i, ctx: ctx, aspect: a ?? "3:2", sizes:
|
|
39
|
+
body = (_jsx(Stagger, { asChild: true, disabled: editMode, children: _jsx("ul", { className: "grid grid-cols-2 gap-x-3 gap-y-6 @4xl:grid-cols-3 @4xl:gap-x-4", children: images.map((g, i) => (_jsx("li", { children: _jsxs("figure", { className: "flex flex-col gap-2.5", children: [_jsx(Cell, { g: g, i: i, ctx: ctx, aspect: a ?? "3:2", sizes: columnSizes(1 / 3, { narrow: "50vw" }) }), g.caption ? _jsx("figcaption", { className: "text-small text-fg-muted", children: g.caption }) : null] }) }, i))) }) }));
|
|
39
40
|
}
|
|
40
|
-
const content = variant === "split-caption" ? (_jsxs(Container, { className: "grid gap-x-(--gutter) gap-y-10 @4xl:grid-cols-12", children: [_jsx("div", { className: "@4xl:col-span-4", children: _jsx("div", { className: "flex flex-col gap-6 @4xl:sticky @4xl:top-[calc(var(--topbar-h,0px)+2rem)]", children: header }) }), _jsx("ol", { className: "flex flex-col gap-12 @4xl:col-span-7 @4xl:col-start-6", children: images.map((g, i) => (_jsx("li", { children: _jsxs("figure", { className: "flex flex-col gap-4", children: [_jsx(Cell, { g: g, i: i, ctx: ctx, sizes:
|
|
41
|
+
const content = variant === "split-caption" ? (_jsxs(Container, { className: "grid gap-x-(--gutter) gap-y-10 @4xl:grid-cols-12", children: [_jsx("div", { className: "@4xl:col-span-4", children: _jsx("div", { className: "flex flex-col gap-6 @4xl:sticky @4xl:top-[calc(var(--topbar-h,0px)+2rem)]", children: header }) }), _jsx("ol", { className: "flex flex-col gap-12 @4xl:col-span-7 @4xl:col-start-6", children: images.map((g, i) => (_jsx("li", { children: _jsxs("figure", { className: "flex flex-col gap-4", children: [_jsx(Cell, { g: g, i: i, ctx: ctx, sizes: columnSizes(7 / 12) }), g.caption ? (_jsxs("figcaption", { className: "flex gap-4 text-small text-fg-muted text-pretty", children: [_jsx("span", { "aria-hidden": true, className: "font-mono text-eyebrow tabular text-fg-subtle", children: t("imageCounter", { index: i + 1, total: images.length }) }), _jsx("span", { children: g.caption })] })) : null] }) }, i))) })] })) : (_jsxs(_Fragment, { children: [header ? _jsx(Container, { children: header }) : null, variant === "filmstrip" ? _jsx("div", { className: "vb-filmstrip-wrap", children: body }) : _jsx(Container, { children: body })] }));
|
|
41
42
|
return (_jsx(Section, { theme: theme, anchor: anchor, headingId: title ? headingId : undefined, blockId: id, blockType: "gallery", editMode: editMode, className: cn("vb-gallery", className), "data-variant": variant, children: _jsx("div", { className: "flex flex-col gap-10 @4xl:gap-14", children: lightbox ? (_jsx(GalleryLightbox, { items: images.map((g) => ({ src: g.image.src, alt: g.image.alt, width: g.image.width, height: g.image.height, caption: textOf(g.caption) || undefined, focalPoint: g.image.focal })), children: content })) : (content) }) }));
|
|
42
43
|
}
|
|
43
44
|
function Cell({ g, i, ctx, className, style, sizes, aspect, fill }) {
|
|
@@ -13,11 +13,12 @@ export interface HeroProps extends Omit<BlockBaseProps, "variant" | "labels"> {
|
|
|
13
13
|
variant?: HeroVariant;
|
|
14
14
|
/** Small mono line above the title ("Agency · Maltepe, Istanbul"). */
|
|
15
15
|
eyebrow?: React.ReactNode;
|
|
16
|
-
/** The page `h1`. A string supports one `*emphasis
|
|
16
|
+
/** The page `h1`. A string supports one `*emphasis*`; `\n` sets a line break (each line is a block, joined by a real space so search engines and screen readers read "közde, muhabbet"). ≤ 9 words reads best. */
|
|
17
17
|
title: React.ReactNode;
|
|
18
18
|
lead?: React.ReactNode;
|
|
19
19
|
/** Up to two actions; one primary. */
|
|
20
20
|
actions?: SiteCta[];
|
|
21
|
+
/** `sizes`: editorial strip and full-bleed `100vw`; split-media portrait `(min-width: 56rem) 42vw, 100vw` (it bleeds to the edge). Above the fold: always `priority`. */
|
|
21
22
|
media?: SiteImage;
|
|
22
23
|
/** full-bleed: muted looping MP4 (`media` is the required poster). */
|
|
23
24
|
video?: {
|
|
@@ -27,7 +28,7 @@ export interface HeroProps extends Omit<BlockBaseProps, "variant" | "labels"> {
|
|
|
27
28
|
caption?: React.ReactNode;
|
|
28
29
|
/** Small true facts ("Founded" · "2014"), up to 4. */
|
|
29
30
|
meta?: SiteMeta[];
|
|
30
|
-
/** product: the product screenshot (no device frame). */
|
|
31
|
+
/** product: the product screenshot (no device frame). `sizes`: `min(100vw, 76rem)`. */
|
|
31
32
|
screenshot?: SiteImage;
|
|
32
33
|
/** product: a live composition instead of a screenshot (plain JSX only). */
|
|
33
34
|
visual?: React.ReactNode;
|
package/dist/blocks/site/hero.js
CHANGED
|
@@ -6,6 +6,8 @@ import { BlockImage } from "../block-image.js";
|
|
|
6
6
|
import { BlockLink } from "../block-link.js";
|
|
7
7
|
import { HeroVideo } from "./hero-video.js";
|
|
8
8
|
import { ArrowEnd, CtaList, field, siteText, useBlockIds } from "./_kit-1.js";
|
|
9
|
+
import { headingLines } from "./shared/heading-lines.js";
|
|
10
|
+
import { columnSizes, SITE_SIZES } from "./shared/image-sizes.js";
|
|
9
11
|
/**
|
|
10
12
|
* The first screen and the page `h1`, in five characters per sector — never the generic
|
|
11
13
|
* "centred title + two buttons + abstract visual". Server component; the background video is a
|
|
@@ -43,28 +45,28 @@ function Meta({ items, className }) {
|
|
|
43
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))) }));
|
|
44
46
|
}
|
|
45
47
|
function Title({ p, size, className }) {
|
|
46
|
-
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", enter(p.editMode, 1, "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) }));
|
|
47
49
|
}
|
|
48
50
|
/* editorial ----------------------------------------------------------- */
|
|
49
51
|
function Editorial(p) {
|
|
50
52
|
const { eyebrow, lead, actions, meta, media, components, labels, locale, editMode } = p;
|
|
51
|
-
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:
|
|
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] }));
|
|
52
54
|
}
|
|
53
55
|
/* split-media ---------------------------------------------------------- */
|
|
54
56
|
function SplitMedia(p) {
|
|
55
57
|
const { eyebrow, lead, actions, meta, media, caption, components, labels, locale, editMode } = p;
|
|
56
|
-
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:
|
|
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] }));
|
|
57
59
|
}
|
|
58
60
|
/* full-bleed ----------------------------------------------------------- */
|
|
59
61
|
function FullBleed(p) {
|
|
60
62
|
const { eyebrow, lead, actions, media, video, aside, height = "auto", components, labels, locale, editMode } = p;
|
|
61
63
|
const t = siteText(labels, locale);
|
|
62
|
-
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:
|
|
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] })] }));
|
|
63
65
|
}
|
|
64
66
|
/* product -------------------------------------------------------------- */
|
|
65
67
|
function Product(p) {
|
|
66
68
|
const { eyebrow, lead, actions, release, screenshot, visual, meta, components, labels, locale, editMode } = p;
|
|
67
|
-
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:
|
|
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] }));
|
|
68
70
|
}
|
|
69
71
|
/* statement ------------------------------------------------------------ */
|
|
70
72
|
function Statement(p) {
|
|
@@ -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;
|