@bearmenu/ui 0.8.8 → 0.8.10
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/chunk-24RFI4RQ.js +167 -0
- package/dist/chunk-2XCZJWTV.js +121 -0
- package/dist/chunk-5RWTOT5Y.js +72 -0
- package/dist/chunk-62ES6N32.js +27 -0
- package/dist/chunk-AKQ57NXY.js +58 -0
- package/dist/chunk-CLLY6XB3.js +41 -0
- package/dist/chunk-DTXNDLSE.js +125 -0
- package/dist/chunk-F6FMZJDG.js +113 -0
- package/dist/{chunk-2WDJR6K7.js → chunk-G7IFL4Z4.js} +4 -1
- package/dist/chunk-MZL7CGCL.js +109 -0
- package/dist/chunk-Q6QH52LJ.js +38 -0
- package/dist/chunk-SY6RV476.js +48 -0
- package/dist/chunk-TH6W2PWG.js +213 -0
- package/dist/chunk-UDN7UHJW.js +60 -0
- package/dist/{chunk-TT3WFS6S.js → chunk-YOKHBAIH.js} +11 -11
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/concept-compare-rail.d.ts +50 -0
- package/dist/components/concept-compare-rail.js +5 -0
- package/dist/components/concept-compare.d.ts +110 -0
- package/dist/components/concept-compare.js +238 -0
- package/dist/components/concept-convergence.d.ts +41 -0
- package/dist/components/concept-convergence.js +53 -0
- package/dist/components/convergence.d.ts +25 -0
- package/dist/components/convergence.js +4 -0
- package/dist/components/date-input.d.ts +1 -1
- package/dist/components/event-card.js +1 -1
- package/dist/components/event-compact-row.d.ts +1 -1
- package/dist/components/event-compact-row.js +1 -1
- package/dist/components/event-detail-panel.d.ts +171 -25
- package/dist/components/event-detail-panel.js +472 -175
- package/dist/components/event-index-row.js +1 -1
- package/dist/components/event-list-row-wide.js +2 -2
- package/dist/components/event-list-row.js +2 -2
- package/dist/components/explainer-steps.d.ts +34 -0
- package/dist/components/explainer-steps.js +36 -0
- package/dist/components/glass-ground.d.ts +66 -0
- package/dist/components/glass-ground.js +3 -0
- package/dist/components/hub-surface.d.ts +27 -0
- package/dist/components/hub-surface.js +30 -0
- package/dist/components/hue-chip.d.ts +70 -0
- package/dist/components/hue-chip.js +3 -0
- package/dist/components/kitchen-dossier.d.ts +88 -0
- package/dist/components/kitchen-dossier.js +187 -0
- package/dist/components/menu-dead-end.d.ts +56 -0
- package/dist/components/menu-dead-end.js +49 -0
- package/dist/components/menu-family-block.d.ts +99 -0
- package/dist/components/menu-family-block.js +155 -0
- package/dist/components/menu-family-card.d.ts +65 -0
- package/dist/components/menu-family-card.js +79 -0
- package/dist/components/menu-field.d.ts +57 -0
- package/dist/components/menu-field.js +93 -0
- package/dist/components/menu-filter-chips.d.ts +86 -0
- package/dist/components/menu-filter-chips.js +122 -0
- package/dist/components/menu-group-rows.d.ts +53 -0
- package/dist/components/menu-group-rows.js +67 -0
- package/dist/components/menu-hub-hero.d.ts +65 -0
- package/dist/components/menu-hub-hero.js +103 -0
- package/dist/components/menu-index-header.d.ts +37 -0
- package/dist/components/menu-index-header.js +68 -0
- package/dist/components/menu-index-list.d.ts +48 -0
- package/dist/components/menu-index-list.js +96 -0
- package/dist/components/menu-intent-hero.d.ts +69 -0
- package/dist/components/menu-intent-hero.js +87 -0
- package/dist/components/menu-intent-tiles.d.ts +31 -0
- package/dist/components/menu-intent-tiles.js +50 -0
- package/dist/components/menu-item.d.ts +127 -0
- package/dist/components/menu-item.js +5 -0
- package/dist/components/menu-kitchen-list.d.ts +82 -0
- package/dist/components/menu-kitchen-list.js +5 -0
- package/dist/components/menu-mocks.d.ts +90 -0
- package/dist/components/menu-mocks.js +504 -0
- package/dist/components/menu-types.d.ts +421 -0
- package/dist/components/menu-types.js +1 -0
- package/dist/components/motion-icon-controls.js +2 -2
- package/dist/components/motion-icon-set.js +21 -9
- package/dist/components/motion-icon.d.ts +22 -1
- package/dist/components/motion-icon.js +1 -1
- package/dist/components/open-now-list.d.ts +40 -0
- package/dist/components/open-now-list.js +69 -0
- package/dist/components/photo-ground.d.ts +64 -0
- package/dist/components/photo-ground.js +3 -0
- package/dist/components/place-about-tab.js +2 -2
- package/dist/components/place-details-mobile.js +2 -2
- package/dist/components/place-types.d.ts +112 -1
- package/dist/components/qa-block.d.ts +36 -0
- package/dist/components/qa-block.js +24 -0
- package/dist/components/scroll-fade.js +3 -121
- package/dist/components/signature-chip.d.ts +37 -0
- package/dist/components/signature-chip.js +4 -0
- package/dist/components/snap-rail.d.ts +48 -5
- package/dist/components/snap-rail.js +3 -51
- package/dist/components/stat-banner.d.ts +47 -0
- package/dist/components/stat-banner.js +61 -0
- package/dist/components/story.d.ts +34 -0
- package/dist/components/story.js +3 -0
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/text.d.ts +1 -1
- package/dist/components/top-comparison.d.ts +28 -0
- package/dist/components/top-comparison.js +76 -0
- package/dist/components/top-cross-link.d.ts +34 -0
- package/dist/components/top-cross-link.js +101 -0
- package/dist/components/top-ranking-entry.d.ts +53 -0
- package/dist/components/top-ranking-entry.js +217 -0
- package/dist/components/top-ranking-hero.d.ts +61 -0
- package/dist/components/top-ranking-hero.js +99 -0
- package/dist/components/top-ranking-legend.d.ts +69 -0
- package/dist/components/top-ranking-legend.js +124 -0
- package/dist/components/top-types.d.ts +109 -0
- package/dist/components/top-types.js +1 -0
- package/dist/components/venue-grade-dimensions.d.ts +38 -0
- package/dist/components/venue-grade-dimensions.js +3 -0
- package/dist/components/venue-quotes.d.ts +32 -0
- package/dist/components/venue-quotes.js +3 -0
- package/dist/components/venue-wall.d.ts +41 -0
- package/dist/components/venue-wall.js +118 -0
- package/dist/components/weekday-theme-banner.d.ts +70 -0
- package/dist/components/weekday-theme-banner.js +127 -0
- package/dist/components/weekend-panel.js +2 -2
- package/package.json +1 -1
- package/src/components/colour-dictionary.stories.tsx +227 -0
- package/src/components/concept-compare-rail.tsx +131 -0
- package/src/components/concept-compare.stories.tsx +172 -0
- package/src/components/concept-compare.tsx +443 -0
- package/src/components/concept-convergence.stories.tsx +53 -0
- package/src/components/concept-convergence.tsx +90 -0
- package/src/components/convergence.stories.tsx +54 -0
- package/src/components/convergence.tsx +177 -0
- package/src/components/event-compact-row.tsx +1 -1
- package/src/components/event-detail-panel.stories.tsx +205 -97
- package/src/components/event-detail-panel.test.tsx +181 -0
- package/src/components/event-detail-panel.tsx +703 -185
- package/src/components/explainer-steps.stories.tsx +46 -0
- package/src/components/explainer-steps.tsx +74 -0
- package/src/components/glass-ground.tsx +129 -0
- package/src/components/hub-surface.stories.tsx +68 -0
- package/src/components/hub-surface.tsx +50 -0
- package/src/components/hue-chip.stories.tsx +76 -0
- package/src/components/hue-chip.test.tsx +62 -0
- package/src/components/hue-chip.tsx +99 -0
- package/src/components/kitchen-dossier.stories.tsx +108 -0
- package/src/components/kitchen-dossier.tsx +322 -0
- package/src/components/menu-dead-end.stories.tsx +67 -0
- package/src/components/menu-dead-end.tsx +127 -0
- package/src/components/menu-family-block.stories.tsx +91 -0
- package/src/components/menu-family-block.tsx +314 -0
- package/src/components/menu-family-card.stories.tsx +120 -0
- package/src/components/menu-family-card.tsx +191 -0
- package/src/components/menu-field.stories.tsx +74 -0
- package/src/components/menu-field.tsx +199 -0
- package/src/components/menu-filter-chips.stories.tsx +107 -0
- package/src/components/menu-filter-chips.tsx +238 -0
- package/src/components/menu-group-rows.stories.tsx +54 -0
- package/src/components/menu-group-rows.tsx +133 -0
- package/src/components/menu-hub-hero.stories.tsx +92 -0
- package/src/components/menu-hub-hero.tsx +187 -0
- package/src/components/menu-index-header.stories.tsx +72 -0
- package/src/components/menu-index-header.tsx +125 -0
- package/src/components/menu-index-list.stories.tsx +50 -0
- package/src/components/menu-index-list.tsx +149 -0
- package/src/components/menu-intent-hero.stories.tsx +67 -0
- package/src/components/menu-intent-hero.tsx +189 -0
- package/src/components/menu-intent-tiles.stories.tsx +54 -0
- package/src/components/menu-intent-tiles.tsx +76 -0
- package/src/components/menu-item.tsx +416 -0
- package/src/components/menu-kitchen-list.stories.tsx +67 -0
- package/src/components/menu-kitchen-list.test.tsx +78 -0
- package/src/components/menu-kitchen-list.tsx +284 -0
- package/src/components/menu-mocks.ts +592 -0
- package/src/components/menu-types.ts +442 -0
- package/src/components/motion-icon-controls.tsx +11 -11
- package/src/components/motion-icon-set.tsx +36 -8
- package/src/components/motion-icon.test.tsx +23 -1
- package/src/components/motion-icon.tsx +20 -0
- package/src/components/open-now-list.stories.tsx +76 -0
- package/src/components/open-now-list.tsx +127 -0
- package/src/components/photo-ground.tsx +147 -0
- package/src/components/place-types.ts +112 -0
- package/src/components/qa-block.stories.tsx +43 -0
- package/src/components/qa-block.tsx +60 -0
- package/src/components/signature-chip.tsx +77 -0
- package/src/components/snap-rail.tsx +133 -9
- package/src/components/stat-banner.stories.tsx +100 -0
- package/src/components/stat-banner.tsx +117 -0
- package/src/components/story.tsx +225 -0
- package/src/components/top-comparison.stories.tsx +78 -0
- package/src/components/top-comparison.tsx +151 -0
- package/src/components/top-cross-link.stories.tsx +84 -0
- package/src/components/top-cross-link.tsx +171 -0
- package/src/components/top-ranking-entry.stories.tsx +171 -0
- package/src/components/top-ranking-entry.test.tsx +105 -0
- package/src/components/top-ranking-entry.tsx +425 -0
- package/src/components/top-ranking-hero.stories.tsx +76 -0
- package/src/components/top-ranking-hero.tsx +218 -0
- package/src/components/top-ranking-legend.stories.tsx +78 -0
- package/src/components/top-ranking-legend.tsx +239 -0
- package/src/components/top-types.ts +110 -0
- package/src/components/venue-grade-dimensions.stories.tsx +83 -0
- package/src/components/venue-grade-dimensions.tsx +91 -0
- package/src/components/venue-quotes.stories.tsx +59 -0
- package/src/components/venue-quotes.tsx +64 -0
- package/src/components/venue-wall.stories.tsx +78 -0
- package/src/components/venue-wall.tsx +185 -0
- package/src/components/weekday-theme-banner.stories.tsx +113 -0
- package/src/components/weekday-theme-banner.tsx +231 -0
- package/dist/{chunk-XWMRABZW.js → chunk-HSIYYA2E.js} +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { MenuLinkComponent } from './menu-types.js';
|
|
3
|
+
import { TopExit } from './top-types.js';
|
|
4
|
+
import 'react';
|
|
5
|
+
import './place-types.js';
|
|
6
|
+
|
|
7
|
+
type TopRankingHeroProps = {
|
|
8
|
+
/** e.g. "RANKED · CUISINE". */
|
|
9
|
+
kicker?: string;
|
|
10
|
+
title: string;
|
|
11
|
+
/** One paragraph: the size of the pool, and what the order means. */
|
|
12
|
+
lede?: string;
|
|
13
|
+
/** e.g. "Updated 6 September 2026". */
|
|
14
|
+
updatedLabel?: string;
|
|
15
|
+
/** e.g. "247 kitchens considered · 4,318 reviews read". */
|
|
16
|
+
metaLabel?: string;
|
|
17
|
+
exits?: TopExit[];
|
|
18
|
+
/** Finished CSS gradient for the ground. */
|
|
19
|
+
gradient?: string;
|
|
20
|
+
/** Watermark behind the text — the category's icon, resolved by the caller. */
|
|
21
|
+
icon?: React.ReactNode;
|
|
22
|
+
linkComponent?: MenuLinkComponent;
|
|
23
|
+
className?: string;
|
|
24
|
+
};
|
|
25
|
+
declare function TopRankingHero({ kicker, title, lede, updatedLabel, metaLabel, exits, gradient, icon, linkComponent: LinkComponent, className, }: TopRankingHeroProps): react_jsx_runtime.JSX.Element;
|
|
26
|
+
/**
|
|
27
|
+
* TopMethodNote — how the ranking was computed, in one block.
|
|
28
|
+
*
|
|
29
|
+
* Under the hero rather than at the foot: a reader deciding whether to trust a
|
|
30
|
+
* list decides it before reading the list, not after.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* The note's surface, exported so a caller can put the same look on a TRIGGER.
|
|
34
|
+
*
|
|
35
|
+
* On `/top` the banner opens the grade explainer, and that explainer owns its
|
|
36
|
+
* own `<button>` (Radix clones it, so it cannot be handed a ready-made
|
|
37
|
+
* element). The caller therefore needs the surface without the element — hence
|
|
38
|
+
* a class string rather than a second component. `cn` at the call site lets the
|
|
39
|
+
* button's own `inline-flex` lose to the `flex` here.
|
|
40
|
+
*/
|
|
41
|
+
declare const topMethodNoteSurface = "flex w-full items-start gap-3 rounded-[14px] bg-muted p-4 text-left";
|
|
42
|
+
type TopMethodNoteProps = {
|
|
43
|
+
/** The heading fragment, e.g. "How this is ranked." Rendered bold, inline. */
|
|
44
|
+
heading: string;
|
|
45
|
+
children: React.ReactNode;
|
|
46
|
+
/** Rendered after the prose — a chevron where the note is a disclosure trigger. */
|
|
47
|
+
affordance?: React.ReactNode;
|
|
48
|
+
className?: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The note's CONTENT, with no element of its own.
|
|
52
|
+
*
|
|
53
|
+
* Every tag here is phrasing content — spans, not paragraphs — because this
|
|
54
|
+
* renders inside a `<button>` when the note is a trigger, and a `<p>` inside a
|
|
55
|
+
* button is invalid. Keeping one definition of the content means the static
|
|
56
|
+
* note and the interactive one cannot drift apart.
|
|
57
|
+
*/
|
|
58
|
+
declare function TopMethodNoteBody({ heading, children, affordance, }: Omit<TopMethodNoteProps, "className">): react_jsx_runtime.JSX.Element;
|
|
59
|
+
declare function TopMethodNote({ heading, children, affordance, className }: TopMethodNoteProps): react_jsx_runtime.JSX.Element;
|
|
60
|
+
|
|
61
|
+
export { TopMethodNote, TopMethodNoteBody, type TopMethodNoteProps, TopRankingHero, type TopRankingHeroProps, topMethodNoteSurface };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
2
|
+
import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
|
|
3
|
+
import { Trophy, CalendarCheck, LayoutList, ArrowRight, Info } from 'lucide-react';
|
|
4
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
function DefaultLink(_a) {
|
|
7
|
+
var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
|
|
8
|
+
return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
|
|
9
|
+
}
|
|
10
|
+
function TopRankingHero({
|
|
11
|
+
kicker,
|
|
12
|
+
title,
|
|
13
|
+
lede,
|
|
14
|
+
updatedLabel,
|
|
15
|
+
metaLabel,
|
|
16
|
+
exits,
|
|
17
|
+
gradient,
|
|
18
|
+
icon,
|
|
19
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
20
|
+
className
|
|
21
|
+
}) {
|
|
22
|
+
return /* @__PURE__ */ jsxs(
|
|
23
|
+
"header",
|
|
24
|
+
{
|
|
25
|
+
className: cn(
|
|
26
|
+
"relative overflow-hidden rounded-[18px] px-5 py-6 lg:px-8 lg:py-8",
|
|
27
|
+
!gradient && "bg-muted",
|
|
28
|
+
className
|
|
29
|
+
),
|
|
30
|
+
children: [
|
|
31
|
+
gradient && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "absolute inset-0", style: { backgroundImage: gradient } }),
|
|
32
|
+
icon && /* @__PURE__ */ jsx(
|
|
33
|
+
"span",
|
|
34
|
+
{
|
|
35
|
+
"aria-hidden": true,
|
|
36
|
+
className: "pointer-events-none absolute -right-6 -top-6 opacity-[0.14] [&>svg]:h-[150px] [&>svg]:w-[150px] lg:[&>svg]:h-[190px] lg:[&>svg]:w-[190px]",
|
|
37
|
+
children: icon
|
|
38
|
+
}
|
|
39
|
+
),
|
|
40
|
+
/* @__PURE__ */ jsxs("div", { className: "relative z-[1]", children: [
|
|
41
|
+
kicker && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5 rounded-full bg-background/70 px-3 py-1 text-[11px] font-extrabold uppercase tracking-[0.09em] backdrop-blur-[2px]", children: [
|
|
42
|
+
/* @__PURE__ */ jsx(Trophy, { className: "h-3 w-3 shrink-0", "aria-hidden": true }),
|
|
43
|
+
kicker
|
|
44
|
+
] }),
|
|
45
|
+
/* @__PURE__ */ jsx("h1", { className: "mt-3 max-w-[22ch] font-serif text-[30px] font-bold leading-[1.08] tracking-[-0.025em] lg:text-[44px]", children: title }),
|
|
46
|
+
lede && /* @__PURE__ */ jsx("p", { className: "mt-3 max-w-[62ch] text-[14.5px] leading-relaxed opacity-85", children: lede }),
|
|
47
|
+
(updatedLabel || metaLabel) && /* @__PURE__ */ jsxs("div", { className: "mt-4 flex flex-wrap items-center gap-x-3 gap-y-1.5 text-[12.5px]", children: [
|
|
48
|
+
updatedLabel && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1.5 font-semibold", children: [
|
|
49
|
+
/* @__PURE__ */ jsx(CalendarCheck, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": true }),
|
|
50
|
+
updatedLabel
|
|
51
|
+
] }),
|
|
52
|
+
updatedLabel && metaLabel && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "h-1 w-1 rounded-full bg-current opacity-40" }),
|
|
53
|
+
metaLabel && /* @__PURE__ */ jsx("span", { className: "opacity-75", children: metaLabel })
|
|
54
|
+
] }),
|
|
55
|
+
exits && exits.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-5 hidden flex-col gap-2 sm:flex-row lg:flex", children: exits.map((exit, index) => {
|
|
56
|
+
var _a;
|
|
57
|
+
return /* @__PURE__ */ jsxs(
|
|
58
|
+
LinkComponent,
|
|
59
|
+
{
|
|
60
|
+
href: exit.href,
|
|
61
|
+
className: cn(
|
|
62
|
+
"inline-flex h-11 items-center justify-center gap-2 rounded-xl px-4 text-sm font-bold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 motion-reduce:transition-none",
|
|
63
|
+
index === 0 ? "bg-background text-foreground hover:bg-background/90" : "border border-current/25 hover:bg-background/25"
|
|
64
|
+
),
|
|
65
|
+
children: [
|
|
66
|
+
(_a = exit.icon) != null ? _a : /* @__PURE__ */ jsx(LayoutList, { className: "h-4 w-4 shrink-0", "aria-hidden": true }),
|
|
67
|
+
exit.label,
|
|
68
|
+
/* @__PURE__ */ jsx(ArrowRight, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": true })
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
exit.href
|
|
72
|
+
);
|
|
73
|
+
}) })
|
|
74
|
+
] })
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
var topMethodNoteSurface = "flex w-full items-start gap-3 rounded-[14px] bg-muted p-4 text-left";
|
|
80
|
+
function TopMethodNoteBody({
|
|
81
|
+
heading,
|
|
82
|
+
children,
|
|
83
|
+
affordance
|
|
84
|
+
}) {
|
|
85
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
86
|
+
/* @__PURE__ */ jsx(Info, { className: "mt-0.5 h-4 w-4 shrink-0 text-muted-foreground", "aria-hidden": true }),
|
|
87
|
+
/* @__PURE__ */ jsxs("span", { className: "block text-[13px] leading-relaxed text-muted-foreground", children: [
|
|
88
|
+
/* @__PURE__ */ jsx("b", { className: "font-bold text-foreground", children: heading }),
|
|
89
|
+
" ",
|
|
90
|
+
children
|
|
91
|
+
] }),
|
|
92
|
+
affordance
|
|
93
|
+
] });
|
|
94
|
+
}
|
|
95
|
+
function TopMethodNote({ heading, children, affordance, className }) {
|
|
96
|
+
return /* @__PURE__ */ jsx("aside", { className: cn(topMethodNoteSurface, className), children: /* @__PURE__ */ jsx(TopMethodNoteBody, { heading, affordance, children }) });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { TopMethodNote, TopMethodNoteBody, TopRankingHero, topMethodNoteSurface };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TopRankingLegend — the ranking as a place you are inside of, not a table.
|
|
5
|
+
*
|
|
6
|
+
* ## What it stopped being
|
|
7
|
+
*
|
|
8
|
+
* It was a two-column table: rank, name, grade, with a rule under every row.
|
|
9
|
+
* Three things were wrong with that, and they compounded.
|
|
10
|
+
*
|
|
11
|
+
* The **grade column** was the loudest thing in it and the least useful thing
|
|
12
|
+
* for navigating — twenty-five near-identical numbers in accent colour,
|
|
13
|
+
* duplicating a grade shown at full size two inches to the right, and eating
|
|
14
|
+
* the width that was truncating the names. The one field you navigate by was
|
|
15
|
+
* the one being cut. It is gone.
|
|
16
|
+
*
|
|
17
|
+
* The **dividers** ran full-bleed to the column's right edge, so every rule was
|
|
18
|
+
* a small arrow pointing at the article and nothing ever visually ended the
|
|
19
|
+
* aside. Space separates now; contrast is spent on state instead.
|
|
20
|
+
*
|
|
21
|
+
* And there was **no active row at all** — twenty-five identical entries in a
|
|
22
|
+
* component whose entire job is orientation. That is what the observer below
|
|
23
|
+
* is for.
|
|
24
|
+
*
|
|
25
|
+
* ## The rank numeral is the rail
|
|
26
|
+
*
|
|
27
|
+
* The reference this is drawn from uses a status ring down the left edge. A
|
|
28
|
+
* ring is wrong here: it promises completion state we do not track and would
|
|
29
|
+
* never fill. The rank numeral is already present, already tabular, and is
|
|
30
|
+
* itself the thing being ranked — a better rail, for free.
|
|
31
|
+
*
|
|
32
|
+
* ## Grouped, because twenty-five is not a list you can hold
|
|
33
|
+
*
|
|
34
|
+
* By grade band, which is the one honest grouping available: the tiers are
|
|
35
|
+
* already printed on the entries, so the aside is not inventing a taxonomy. It
|
|
36
|
+
* also says something true — that the top few are a different class.
|
|
37
|
+
*
|
|
38
|
+
* The column will still exceed the viewport at twenty-five, and that is
|
|
39
|
+
* accepted rather than solved: an internally-scrolling sidebar is a second page,
|
|
40
|
+
* and grouping plus a live active row makes the overflow navigable.
|
|
41
|
+
*
|
|
42
|
+
* ## The phone keeps its chips
|
|
43
|
+
*
|
|
44
|
+
* Below `lg` this is a horizontal scroller of the flat list, unchanged. None of
|
|
45
|
+
* the above applies to a chip row — there is nothing to group, no column to
|
|
46
|
+
* inset, and the active state is a press away rather than a scroll away.
|
|
47
|
+
*/
|
|
48
|
+
type TopRankingLegendEntry = {
|
|
49
|
+
rank: number;
|
|
50
|
+
slug: string;
|
|
51
|
+
name: string;
|
|
52
|
+
};
|
|
53
|
+
type TopRankingLegendGroup = {
|
|
54
|
+
key: string;
|
|
55
|
+
/** Grade band, e.g. "Exceptional". Letterspaced caps — structure, not content. */
|
|
56
|
+
label: string;
|
|
57
|
+
entries: TopRankingLegendEntry[];
|
|
58
|
+
};
|
|
59
|
+
type TopRankingLegendProps = {
|
|
60
|
+
groups: TopRankingLegendGroup[];
|
|
61
|
+
/** e.g. "The ranking". */
|
|
62
|
+
heading: string;
|
|
63
|
+
/** The size and freshness of the thing, e.g. "25 places · updated September 2026". */
|
|
64
|
+
meta?: string;
|
|
65
|
+
className?: string;
|
|
66
|
+
};
|
|
67
|
+
declare function TopRankingLegend({ groups, heading, meta, className }: TopRankingLegendProps): react_jsx_runtime.JSX.Element;
|
|
68
|
+
|
|
69
|
+
export { TopRankingLegend, type TopRankingLegendEntry, type TopRankingLegendGroup, type TopRankingLegendProps };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { cn } from '../chunk-FEK5XGZW.js';
|
|
2
|
+
import '../chunk-3GWWZKX7.js';
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
var ACTIVE_BAND = "-20% 0px -75% 0px";
|
|
7
|
+
function TopRankingLegend({ groups, heading, meta, className }) {
|
|
8
|
+
const entries = groups.flatMap((group) => group.entries);
|
|
9
|
+
const [activeSlug, setActiveSlug] = useState(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (typeof IntersectionObserver === "undefined") return void 0;
|
|
12
|
+
const nodes = entries.map((entry) => document.getElementById(entry.slug)).filter((node) => Boolean(node));
|
|
13
|
+
if (nodes.length === 0) return void 0;
|
|
14
|
+
const observer = new IntersectionObserver(
|
|
15
|
+
(observed) => {
|
|
16
|
+
const inBand = observed.filter((record) => record.isIntersecting);
|
|
17
|
+
if (inBand.length === 0) return;
|
|
18
|
+
const topmost = inBand.reduce(
|
|
19
|
+
(best, record) => record.boundingClientRect.top < best.boundingClientRect.top ? record : best
|
|
20
|
+
);
|
|
21
|
+
setActiveSlug(topmost.target.id);
|
|
22
|
+
},
|
|
23
|
+
{ rootMargin: ACTIVE_BAND }
|
|
24
|
+
);
|
|
25
|
+
nodes.forEach((node) => observer.observe(node));
|
|
26
|
+
return () => observer.disconnect();
|
|
27
|
+
}, [entries.map((entry) => entry.slug).join("|")]);
|
|
28
|
+
return (
|
|
29
|
+
/*
|
|
30
|
+
* The height cap is a safety net, not the navigation model.
|
|
31
|
+
*
|
|
32
|
+
* `position: sticky` on an element TALLER than the viewport leaves its
|
|
33
|
+
* bottom permanently unreachable — at twenty-five rows on a 900px laptop
|
|
34
|
+
* you could never see ranks 20–25, however you scrolled. That is broken
|
|
35
|
+
* rather than merely tight, so the column scrolls internally once it
|
|
36
|
+
* exceeds the space available.
|
|
37
|
+
*
|
|
38
|
+
* It only ever engages on a viewport too short to fit the list: `max-height`
|
|
39
|
+
* with `overflow-y-auto` is inert when the content already fits, so nothing
|
|
40
|
+
* changes on a tall screen. `overscroll-contain` stops a flick inside the
|
|
41
|
+
* column from carrying on into the article behind it.
|
|
42
|
+
*/
|
|
43
|
+
/* @__PURE__ */ jsxs(
|
|
44
|
+
"nav",
|
|
45
|
+
{
|
|
46
|
+
"aria-label": heading,
|
|
47
|
+
className: cn(
|
|
48
|
+
/*
|
|
49
|
+
* Sticky at BOTH widths now, with a different offset and a different
|
|
50
|
+
* shape at each.
|
|
51
|
+
*
|
|
52
|
+
* On desktop it is the grouped column, pinned beside the article. On a
|
|
53
|
+
* phone it is the flat chip rail, pinned for the same reason the column
|
|
54
|
+
* is: this page is twenty-five long entries, so a reader at rank 14 is
|
|
55
|
+
* several screens from any means of jumping, and the rail scrolled away
|
|
56
|
+
* inside the first entry.
|
|
57
|
+
*
|
|
58
|
+
* The phone offset comes from `--legend-sticky-top`, which the CALLER
|
|
59
|
+
* sets, because it is that app's chrome — the consumer app pins under a
|
|
60
|
+
* fixed header at `calc(var(--header-offset) + env(safe-area-inset-top))`,
|
|
61
|
+
* exactly as its events day rail does. Reading the app's own
|
|
62
|
+
* `--header-offset` here would resolve to nothing in Storybook and in
|
|
63
|
+
* the other three consumers, and an invalid `calc` silently kills
|
|
64
|
+
* `top`, which turns the rail from mispositioned into not-sticky.
|
|
65
|
+
*/
|
|
66
|
+
"sticky top-[var(--legend-sticky-top,0px)] z-20 lg:top-4 lg:z-auto",
|
|
67
|
+
"lg:max-h-[calc(100vh-2rem)] lg:self-start lg:overflow-y-auto lg:overscroll-contain",
|
|
68
|
+
"lg:[scrollbar-width:thin]",
|
|
69
|
+
className
|
|
70
|
+
),
|
|
71
|
+
children: [
|
|
72
|
+
/* @__PURE__ */ jsxs("div", { className: "hidden lg:block", children: [
|
|
73
|
+
/* @__PURE__ */ jsx("h2", { className: "text-[15px] font-bold tracking-[-0.01em]", children: heading }),
|
|
74
|
+
meta && /* @__PURE__ */ jsx("p", { className: "mt-1 text-[11.5px] text-muted-foreground", children: meta })
|
|
75
|
+
] }),
|
|
76
|
+
/* @__PURE__ */ jsx("ul", { className: "scrollbar-hide -mx-[var(--page-gutter)] flex gap-2 overflow-x-auto bg-background px-[var(--page-gutter)] pb-3 pt-1 lg:hidden", children: entries.map((entry) => /* @__PURE__ */ jsx("li", { className: "shrink-0", children: /* @__PURE__ */ jsxs(
|
|
77
|
+
"a",
|
|
78
|
+
{
|
|
79
|
+
href: `#${entry.slug}`,
|
|
80
|
+
className: "inline-flex h-9 items-center gap-2 whitespace-nowrap rounded-full border border-border bg-card px-3.5 text-[13px] transition-colors hover:border-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none",
|
|
81
|
+
children: [
|
|
82
|
+
/* @__PURE__ */ jsx("span", { className: "font-bold tabular-nums text-muted-foreground", children: entry.rank }),
|
|
83
|
+
entry.name
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
) }, entry.slug)) }),
|
|
87
|
+
/* @__PURE__ */ jsx("div", { className: "hidden lg:mt-5 lg:-mx-3 lg:block", children: groups.map((group) => /* @__PURE__ */ jsxs("div", { className: "mb-4 last:mb-0", children: [
|
|
88
|
+
/* @__PURE__ */ jsx("p", { className: "px-3 pb-1.5 text-[10.5px] font-bold uppercase tracking-[0.11em] text-muted-foreground", children: group.label }),
|
|
89
|
+
/* @__PURE__ */ jsx("ul", { children: group.entries.map((entry) => {
|
|
90
|
+
const isActive = entry.slug === activeSlug;
|
|
91
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
92
|
+
"a",
|
|
93
|
+
{
|
|
94
|
+
href: `#${entry.slug}`,
|
|
95
|
+
"aria-current": isActive ? "location" : void 0,
|
|
96
|
+
className: cn(
|
|
97
|
+
"flex gap-2.5 rounded-lg px-3 py-2 text-[13px] leading-snug transition-colors motion-reduce:transition-none",
|
|
98
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
99
|
+
isActive ? "bg-accent-subtle font-semibold text-foreground" : "text-muted-foreground hover:bg-muted/50 hover:text-foreground"
|
|
100
|
+
),
|
|
101
|
+
children: [
|
|
102
|
+
/* @__PURE__ */ jsx(
|
|
103
|
+
"span",
|
|
104
|
+
{
|
|
105
|
+
className: cn(
|
|
106
|
+
"w-5 shrink-0 text-right font-bold tabular-nums",
|
|
107
|
+
isActive ? "text-accent" : "text-muted-foreground/70"
|
|
108
|
+
),
|
|
109
|
+
children: entry.rank
|
|
110
|
+
}
|
|
111
|
+
),
|
|
112
|
+
/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1", children: entry.name })
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
) }, entry.slug);
|
|
116
|
+
}) })
|
|
117
|
+
] }, group.key)) })
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { TopRankingLegend };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { MenuItemPreview } from './menu-types.js';
|
|
3
|
+
import './place-types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The editorial ranking's view model — `/top/[category]`.
|
|
7
|
+
*
|
|
8
|
+
* Separate from `menu-types.ts` on purpose. The menu surfaces describe what a
|
|
9
|
+
* city HAS; these describe an argument the product is making about it, and the
|
|
10
|
+
* shapes reflect that: an entry carries a cited quote and a dated price because
|
|
11
|
+
* a rank is a claim and the page has to show why.
|
|
12
|
+
*
|
|
13
|
+
* Same contract as everywhere else: no app schemas, no hooks, no i18n. Strings
|
|
14
|
+
* arrive through labels, destinations through `href`, numbers through injected
|
|
15
|
+
* formatters.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** One entry in the ranking. */
|
|
19
|
+
type TopRankEntry = {
|
|
20
|
+
rank: number;
|
|
21
|
+
slug: string;
|
|
22
|
+
name: string;
|
|
23
|
+
/** The venue page. */
|
|
24
|
+
href: string;
|
|
25
|
+
/** Neighbourhood, e.g. "Centru". */
|
|
26
|
+
district?: string;
|
|
27
|
+
/** Pre-resolved venue photograph. */
|
|
28
|
+
coverUrl?: string;
|
|
29
|
+
/** The app's grade, already formatted — "9.4". Absent where none exists. */
|
|
30
|
+
gradeLabel?: string;
|
|
31
|
+
/** The band that grade falls in, e.g. "Exceptional". Composed by the caller. */
|
|
32
|
+
tierLabel?: string;
|
|
33
|
+
/** Third-party star rating, shown only where there is no grade. */
|
|
34
|
+
rating?: number;
|
|
35
|
+
/**
|
|
36
|
+
* The editorial line — why this one is here, in the product's own voice.
|
|
37
|
+
*
|
|
38
|
+
* Distinct from `evidence`, which is a reviewer's words. Conflating them was
|
|
39
|
+
* the old page's problem: a quote presented as editorial reads as invented,
|
|
40
|
+
* and editorial presented as a quote reads as dishonest.
|
|
41
|
+
*/
|
|
42
|
+
editorial?: string;
|
|
43
|
+
/**
|
|
44
|
+
* The dishes this venue is named for, as PRODUCTION MENU ROWS.
|
|
45
|
+
*
|
|
46
|
+
* `MenuItemPreview`, so the entry renders the same `<MenuItem>` a venue's own
|
|
47
|
+
* menu and every hub surface renders. The first pass here hand-rolled a row —
|
|
48
|
+
* a bold name, a muted description, a price — which drifted from the real one
|
|
49
|
+
* immediately and meant a dish looked like a different object on this page
|
|
50
|
+
* than on every other.
|
|
51
|
+
*
|
|
52
|
+
* More than one, because a rank is a claim about a kitchen and one dish
|
|
53
|
+
* rarely carries it: the live page shows the group and two or three of its
|
|
54
|
+
* most-named plates.
|
|
55
|
+
*/
|
|
56
|
+
menuItems?: MenuItemPreview[];
|
|
57
|
+
/** The group those items sit in, e.g. "Pizza". From the venue's own menu. */
|
|
58
|
+
menuGroupLabel?: string;
|
|
59
|
+
/** A reviewer's own words, and how many said it. */
|
|
60
|
+
evidence?: TopEvidence;
|
|
61
|
+
};
|
|
62
|
+
type TopEvidence = {
|
|
63
|
+
/**
|
|
64
|
+
* A reviewer's own words. OPTIONAL: a venue can carry a mention count with no
|
|
65
|
+
* quotable snippet behind it, and the count is evidence in its own right —
|
|
66
|
+
* dropping the whole block for want of a quote loses the number that
|
|
67
|
+
* justifies the rank.
|
|
68
|
+
*/
|
|
69
|
+
quote?: string;
|
|
70
|
+
/**
|
|
71
|
+
* How many reviews back it — the figure alone, e.g. "37". Rendered as its own
|
|
72
|
+
* element beside `sourceLabel` so the number reads as a figure rather than as
|
|
73
|
+
* the first word of a sentence.
|
|
74
|
+
*/
|
|
75
|
+
count?: string;
|
|
76
|
+
/** What the count is evidence OF, e.g. "reviews praise the shawarma here". */
|
|
77
|
+
sourceLabel: string;
|
|
78
|
+
/**
|
|
79
|
+
* The rest of the cited reviews, behind a disclosure.
|
|
80
|
+
*
|
|
81
|
+
* A `<details>`, not a drawer, and that is the whole point: its contents stay
|
|
82
|
+
* in the HTML, so the evidence is crawlable and readable with no JavaScript.
|
|
83
|
+
* A page whose entire argument is "here is why" cannot ship most of the why
|
|
84
|
+
* in a modal.
|
|
85
|
+
*/
|
|
86
|
+
extraQuotes?: string[];
|
|
87
|
+
/** Summary text for the disclosure, e.g. "2 more reviews". Pluralised by the caller. */
|
|
88
|
+
extraLabel?: string;
|
|
89
|
+
};
|
|
90
|
+
/** One row of the side-by-side comparison. */
|
|
91
|
+
type TopComparisonRow = {
|
|
92
|
+
rank: number;
|
|
93
|
+
name: string;
|
|
94
|
+
href: string;
|
|
95
|
+
gradeLabel?: string;
|
|
96
|
+
signature?: string;
|
|
97
|
+
priceLabel?: string;
|
|
98
|
+
/** Kitchen serving past midnight. */
|
|
99
|
+
late?: boolean;
|
|
100
|
+
terrace?: boolean;
|
|
101
|
+
};
|
|
102
|
+
/** A way out of the article and into the browsing tool. */
|
|
103
|
+
type TopExit = {
|
|
104
|
+
label: string;
|
|
105
|
+
href: string;
|
|
106
|
+
icon?: ReactNode;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export type { TopComparisonRow, TopEvidence, TopExit, TopRankEntry };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { PlaceGradeDimensions } from './place-types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* VenueGradeDimensions — the per-dimension review scores as a row of bars.
|
|
6
|
+
*
|
|
7
|
+
* ## Why this renders what it is given rather than a fixed row
|
|
8
|
+
*
|
|
9
|
+
* A dimension the reviews never mention is ABSENT from the payload, not zero,
|
|
10
|
+
* and exactly one of `food` / `coffee_quality` / `drinks_quality` is scored per
|
|
11
|
+
* place — a coffee bar has no food score and a restaurant has no coffee one.
|
|
12
|
+
* So a component drawing seven fixed columns would invent readings for a third
|
|
13
|
+
* of the catalogue. It iterates `order`, skips what is missing, and lays out
|
|
14
|
+
* however many survive.
|
|
15
|
+
*
|
|
16
|
+
* Scores arrive 0–100 and display on the 0–10 scale the rest of the product
|
|
17
|
+
* grades on, so the bar width is the raw value and the numeral is that value
|
|
18
|
+
* over ten. `formatScore` is injected because a decimal separator is
|
|
19
|
+
* locale-dependent.
|
|
20
|
+
*
|
|
21
|
+
* Server-safe.
|
|
22
|
+
*/
|
|
23
|
+
type VenueGradeDimensionKey = keyof PlaceGradeDimensions;
|
|
24
|
+
/** Canonical order. Quality leads, and the three quality keys are mutually exclusive. */
|
|
25
|
+
declare const DIMENSION_ORDER: VenueGradeDimensionKey[];
|
|
26
|
+
type VenueGradeDimensionsProps = {
|
|
27
|
+
dimensions: PlaceGradeDimensions;
|
|
28
|
+
/** Dimension key → translated label. Keys with no label fall back to the key. */
|
|
29
|
+
labels: Partial<Record<VenueGradeDimensionKey, string>>;
|
|
30
|
+
/** Override the canonical order, or narrow which dimensions show at all. */
|
|
31
|
+
order?: VenueGradeDimensionKey[];
|
|
32
|
+
/** 0–100 → display numeral, e.g. `(v) => (v / 10).toFixed(1)`. */
|
|
33
|
+
formatScore: (value: number) => string;
|
|
34
|
+
className?: string;
|
|
35
|
+
};
|
|
36
|
+
declare function VenueGradeDimensions({ dimensions, labels, order, formatScore, className, }: VenueGradeDimensionsProps): react_jsx_runtime.JSX.Element | null;
|
|
37
|
+
|
|
38
|
+
export { DIMENSION_ORDER, type VenueGradeDimensionKey, VenueGradeDimensions, type VenueGradeDimensionsProps };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { CitedText } from './place-types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* VenueQuotes — the digest's standout lines, as pull-quotes.
|
|
6
|
+
*
|
|
7
|
+
* These are `reviews_digest.highlights`: one to three short positives already
|
|
8
|
+
* resolved into the request locale. They are the only place on a hub where a
|
|
9
|
+
* reviewer speaks in their own register, so they are set in serif italic
|
|
10
|
+
* against the sans everything else uses.
|
|
11
|
+
*
|
|
12
|
+
* `onCite` turns a quote into a button that opens the reviews behind it —
|
|
13
|
+
* `review_ids` is what makes the claim checkable, and a quote nobody can trace
|
|
14
|
+
* is just copy. Without the handler the quote renders as inert prose, which is
|
|
15
|
+
* correct on surfaces that have nowhere to open.
|
|
16
|
+
*
|
|
17
|
+
* No directive: inert quotes render on the server; passing `onCite` needs a
|
|
18
|
+
* client caller.
|
|
19
|
+
*/
|
|
20
|
+
type VenueQuotesProps = {
|
|
21
|
+
quotes: CitedText[];
|
|
22
|
+
/** Opens the reviews behind one quote. Omit to render inert prose. */
|
|
23
|
+
onCite?: (quote: CitedText) => void;
|
|
24
|
+
/** Accessible name for the cite action, e.g. `(n) => "Read 5 reviews"`. */
|
|
25
|
+
citeLabel?: (reviewCount: number) => string;
|
|
26
|
+
/** Cap on how many render. The digest ships at most three. */
|
|
27
|
+
limit?: number;
|
|
28
|
+
className?: string;
|
|
29
|
+
};
|
|
30
|
+
declare function VenueQuotes({ quotes, onCite, citeLabel, limit, className }: VenueQuotesProps): react_jsx_runtime.JSX.Element | null;
|
|
31
|
+
|
|
32
|
+
export { VenueQuotes, type VenueQuotesProps };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { MenuLinkComponent, MenuLinkProps } from './menu-types.js';
|
|
3
|
+
import { PlacePreview } from './place-types.js';
|
|
4
|
+
import 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* VenueWall — the catalogue's scale, stated as a row of the venues in it.
|
|
8
|
+
*
|
|
9
|
+
* A closing beat rather than a browse surface: the avatars are named and
|
|
10
|
+
* linked, but the module's job is to make "we read every menu in the city"
|
|
11
|
+
* land as something specific. It shows a handful and counts the rest, because
|
|
12
|
+
* a wall long enough to be exhaustive stops reading as places.
|
|
13
|
+
*
|
|
14
|
+
* Server-safe.
|
|
15
|
+
*/
|
|
16
|
+
/** @deprecated Alias of `MenuLinkProps`; kept so existing imports keep working. */
|
|
17
|
+
type VenueWallLinkProps = MenuLinkProps;
|
|
18
|
+
type VenueWallProps = {
|
|
19
|
+
places: PlacePreview[];
|
|
20
|
+
/** Destination per place, keyed by slug. Without one an avatar is inert. */
|
|
21
|
+
hrefs?: Record<string, string>;
|
|
22
|
+
kicker?: string;
|
|
23
|
+
kickerIcon?: React.ReactNode;
|
|
24
|
+
headline?: string;
|
|
25
|
+
body?: string;
|
|
26
|
+
/** Composed overflow chip, e.g. "+393 more". */
|
|
27
|
+
overflowLabel?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Tiles for the mosaic ground — the same licensed stills the hero uses.
|
|
30
|
+
* Without them the wall falls back to the flat diagonal texture, which is
|
|
31
|
+
* the degraded state rather than the design.
|
|
32
|
+
*/
|
|
33
|
+
tileUrls?: string[];
|
|
34
|
+
/** The action — pass a rendered link or button. */
|
|
35
|
+
action?: React.ReactNode;
|
|
36
|
+
className?: string;
|
|
37
|
+
linkComponent?: MenuLinkComponent;
|
|
38
|
+
};
|
|
39
|
+
declare function VenueWall({ places, hrefs, kicker, kickerIcon, headline, body, overflowLabel, tileUrls, action, linkComponent: LinkComponent, className, }: VenueWallProps): react_jsx_runtime.JSX.Element;
|
|
40
|
+
|
|
41
|
+
export { VenueWall, type VenueWallLinkProps, type VenueWallProps };
|