@bearmenu/ui 0.8.7 → 0.8.9
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-637L7XQ3.js → chunk-3TNPSN4D.js} +28 -9
- package/dist/chunk-62ES6N32.js +27 -0
- package/dist/chunk-AHL45DD2.js +145 -0
- package/dist/chunk-AXV5O2IZ.js +58 -0
- package/dist/chunk-IYDH5IU6.js +66 -0
- package/dist/chunk-OPVKL4VS.js +18 -0
- package/dist/chunk-Q6QH52LJ.js +38 -0
- package/dist/chunk-TH6W2PWG.js +213 -0
- package/dist/chunk-THOVPZNN.js +37 -0
- package/dist/chunk-YS6BHKZO.js +92 -0
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/category-deck.js +10 -1
- package/dist/components/command.d.ts +7 -7
- package/dist/components/concept-compare.d.ts +73 -0
- package/dist/components/concept-compare.js +162 -0
- package/dist/components/coverflow.d.ts +11 -3
- package/dist/components/coverflow.js +34 -51
- package/dist/components/date-input.d.ts +1 -1
- package/dist/components/drift-rail.d.ts +24 -10
- package/dist/components/drift-rail.js +1 -1
- package/dist/components/event-card.d.ts +8 -0
- package/dist/components/event-card.js +64 -34
- package/dist/components/event-compact-row.d.ts +33 -6
- package/dist/components/event-compact-row.js +120 -39
- package/dist/components/event-day-header.js +3 -1
- package/dist/components/event-day-separator.d.ts +7 -1
- package/dist/components/event-day-separator.js +3 -2
- package/dist/components/event-detail-panel.d.ts +171 -25
- package/dist/components/event-detail-panel.js +471 -174
- package/dist/components/event-hub-hero.d.ts +25 -4
- package/dist/components/event-hub-hero.js +89 -19
- package/dist/components/event-list-row-wide.js +1 -1
- package/dist/components/event-list-row.d.ts +19 -6
- package/dist/components/event-list-row.js +79 -35
- package/dist/components/hour-band.d.ts +38 -40
- package/dist/components/hour-band.js +55 -27
- package/dist/components/hour-group.js +1 -1
- package/dist/components/hub-surface.d.ts +27 -0
- package/dist/components/hub-surface.js +20 -0
- package/dist/components/menu-family-card.d.ts +59 -0
- package/dist/components/menu-family-card.js +121 -0
- package/dist/components/menu-field.d.ts +57 -0
- package/dist/components/menu-field.js +3 -0
- package/dist/components/menu-hub-hero.d.ts +65 -0
- package/dist/components/menu-hub-hero.js +103 -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-mocks.d.ts +55 -0
- package/dist/components/menu-mocks.js +379 -0
- package/dist/components/menu-types.d.ts +245 -0
- package/dist/components/menu-types.js +1 -0
- package/dist/components/motion-icon-set.js +15 -3
- package/dist/components/open-now-list.d.ts +40 -0
- package/dist/components/open-now-list.js +69 -0
- package/dist/components/place-types.d.ts +112 -1
- package/dist/components/poster-tape.js +1 -1
- package/dist/components/poster-wall.d.ts +3 -3
- package/dist/components/poster-wall.js +73 -17
- package/dist/components/ranked-venue-list.d.ts +49 -0
- package/dist/components/ranked-venue-list.js +87 -0
- package/dist/components/section-header.d.ts +16 -1
- package/dist/components/section-header.js +26 -7
- package/dist/components/signature-chip.d.ts +37 -0
- package/dist/components/signature-chip.js +4 -0
- package/dist/components/signature-line-list.d.ts +45 -0
- package/dist/components/signature-line-list.js +99 -0
- package/dist/components/snap-rail.d.ts +54 -0
- package/dist/components/snap-rail.js +109 -0
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/stat-banner.d.ts +47 -0
- package/dist/components/stat-banner.js +61 -0
- package/dist/components/text.d.ts +3 -3
- package/dist/components/timeline-band.d.ts +20 -2
- package/dist/components/timeline-band.js +1 -1
- package/dist/components/timeline-playhead.d.ts +26 -0
- package/dist/components/timeline-playhead.js +25 -0
- package/dist/components/venue-busyness-strip.d.ts +35 -0
- package/dist/components/venue-busyness-strip.js +37 -0
- package/dist/components/venue-grade-dimensions.d.ts +36 -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-signature-block.d.ts +39 -0
- package/dist/components/venue-signature-block.js +6 -0
- package/dist/components/venue-spotlight.d.ts +74 -0
- package/dist/components/venue-spotlight.js +138 -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.d.ts +5 -4
- package/dist/components/weekend-panel.js +85 -71
- package/dist/hooks/use-drag-swipe.d.ts +44 -0
- package/dist/hooks/use-drag-swipe.js +2 -0
- package/dist/hooks/use-media-query.d.ts +17 -0
- package/dist/hooks/use-media-query.js +2 -0
- package/package.json +1 -1
- package/src/components/category-deck.tsx +19 -8
- package/src/components/colour-dictionary.stories.tsx +227 -0
- package/src/components/concept-compare.stories.tsx +104 -0
- package/src/components/concept-compare.tsx +276 -0
- package/src/components/coverflow.test.tsx +28 -1
- package/src/components/coverflow.tsx +37 -43
- package/src/components/drift-rail.tsx +167 -42
- package/src/components/event-card.tsx +55 -10
- package/src/components/event-compact-row.stories.tsx +30 -0
- package/src/components/event-compact-row.test.tsx +49 -0
- package/src/components/event-compact-row.tsx +162 -38
- package/src/components/event-day-header.tsx +3 -1
- package/src/components/event-day-separator.tsx +8 -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/event-hub-hero.stories.tsx +30 -0
- package/src/components/event-hub-hero.tsx +102 -30
- package/src/components/event-list-row.stories.tsx +34 -0
- package/src/components/event-list-row.test.tsx +53 -0
- package/src/components/event-list-row.tsx +135 -87
- package/src/components/hour-band.stories.tsx +28 -5
- package/src/components/hour-band.tsx +90 -70
- package/src/components/hub-surface.stories.tsx +68 -0
- package/src/components/hub-surface.tsx +40 -0
- package/src/components/menu-family-card.stories.tsx +112 -0
- package/src/components/menu-family-card.tsx +211 -0
- package/src/components/menu-field.stories.tsx +74 -0
- package/src/components/menu-field.tsx +199 -0
- package/src/components/menu-hub-hero.stories.tsx +92 -0
- package/src/components/menu-hub-hero.tsx +187 -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-mocks.ts +417 -0
- package/src/components/menu-types.ts +259 -0
- package/src/components/motion-icon-set.tsx +30 -3
- package/src/components/open-now-list.stories.tsx +76 -0
- package/src/components/open-now-list.tsx +127 -0
- package/src/components/place-types.ts +112 -0
- package/src/components/poster-wall.test.tsx +28 -0
- package/src/components/poster-wall.tsx +96 -23
- package/src/components/ranked-venue-list.stories.tsx +80 -0
- package/src/components/ranked-venue-list.tsx +177 -0
- package/src/components/section-header.stories.tsx +24 -0
- package/src/components/section-header.test.tsx +28 -0
- package/src/components/section-header.tsx +45 -7
- package/src/components/signature-chip.tsx +77 -0
- package/src/components/signature-line-list.stories.tsx +85 -0
- package/src/components/signature-line-list.tsx +136 -0
- package/src/components/snap-rail.stories.tsx +79 -0
- package/src/components/snap-rail.test.tsx +18 -0
- package/src/components/snap-rail.tsx +148 -0
- package/src/components/stat-banner.stories.tsx +100 -0
- package/src/components/stat-banner.tsx +117 -0
- package/src/components/timeline-band.tsx +52 -12
- package/src/components/timeline-playhead.stories.tsx +30 -0
- package/src/components/timeline-playhead.test.tsx +11 -0
- package/src/components/timeline-playhead.tsx +46 -0
- package/src/components/venue-busyness-strip.stories.tsx +66 -0
- package/src/components/venue-busyness-strip.tsx +64 -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-signature-block.stories.tsx +68 -0
- package/src/components/venue-signature-block.tsx +71 -0
- package/src/components/venue-spotlight.stories.tsx +141 -0
- package/src/components/venue-spotlight.tsx +236 -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/src/components/weekend-panel.test.tsx +43 -0
- package/src/components/weekend-panel.tsx +142 -108
- package/src/hooks/use-drag-swipe.test.tsx +66 -0
- package/src/hooks/use-drag-swipe.ts +105 -0
- package/src/hooks/use-media-query.ts +31 -0
- package/dist/chunk-GPH6L4DX.js +0 -55
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { cn } from "../lib/utils";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MenuHubHero — the claim that opens a menu hub, proven by live figures.
|
|
5
|
+
*
|
|
6
|
+
* A statement rather than a count ("Nobody should have to walk in blind"),
|
|
7
|
+
* over a dark ground, with the catalogue's real numbers under it and one proof
|
|
8
|
+
* line that demonstrates what the catalogue can do that a single menu cannot.
|
|
9
|
+
*
|
|
10
|
+
* ## Why the ground is a texture and not a photograph
|
|
11
|
+
*
|
|
12
|
+
* Menu-item photography is AI-generated and never shown, and one venue
|
|
13
|
+
* photograph behind a claim about every venue in the city argues the wrong
|
|
14
|
+
* thing. The tile grid reads as "many menus" without being any of them.
|
|
15
|
+
* Callers that DO have a real, licensed image for the slot can pass
|
|
16
|
+
* `backgroundUrl` — venue photography, never a plate.
|
|
17
|
+
*
|
|
18
|
+
* ## Figures
|
|
19
|
+
*
|
|
20
|
+
* `figures` is a list, not four fixed slots: the drinks tab has different ones
|
|
21
|
+
* and the count changes when a number turns out to have nothing behind it.
|
|
22
|
+
* Every value is a pre-formatted string because thousands separators are
|
|
23
|
+
* locale-dependent and the DS owns no locale.
|
|
24
|
+
*
|
|
25
|
+
* Server-safe.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export type MenuHubHeroFigure = {
|
|
29
|
+
/** Pre-formatted, e.g. "19,062". */
|
|
30
|
+
value: string;
|
|
31
|
+
label: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type MenuHubHeroProps = {
|
|
35
|
+
/** Small caps line above the headline, e.g. "397 MENUS IN CLUJ-NAPOCA, READ IN FULL". */
|
|
36
|
+
kicker?: string;
|
|
37
|
+
/** Icon for the kicker, resolved by the caller. */
|
|
38
|
+
kickerIcon?: React.ReactNode;
|
|
39
|
+
headline: string;
|
|
40
|
+
body?: string;
|
|
41
|
+
figures?: MenuHubHeroFigure[];
|
|
42
|
+
/**
|
|
43
|
+
* The proof line — one demonstrated fact, e.g. "A margherita: 87 kitchens
|
|
44
|
+
* make one, 10 to 120 lei". Composed by the caller; clause order is
|
|
45
|
+
* locale-dependent.
|
|
46
|
+
*/
|
|
47
|
+
proof?: React.ReactNode;
|
|
48
|
+
proofIcon?: React.ReactNode;
|
|
49
|
+
/** The action beside the proof line. Pass a rendered link or button. */
|
|
50
|
+
action?: React.ReactNode;
|
|
51
|
+
/** Real, licensed photograph for the ground. Venue imagery only — see above. */
|
|
52
|
+
backgroundUrl?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Tiles for the mosaic ground — generic, cinematic, licensed stills, served
|
|
55
|
+
* from the consumer's own origin.
|
|
56
|
+
*
|
|
57
|
+
* A mosaic rather than one photograph because the hero's claim is about EVERY
|
|
58
|
+
* kitchen in the city: a single image makes it about one of them. Given fewer
|
|
59
|
+
* tiles than cells the list repeats, so a caller can ship six and still fill
|
|
60
|
+
* a 1440px hero.
|
|
61
|
+
*/
|
|
62
|
+
tileUrls?: string[];
|
|
63
|
+
className?: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Enough cells to fill the widest hero without the grid reading as a pattern. */
|
|
67
|
+
const TEXTURE_CELLS = 28;
|
|
68
|
+
|
|
69
|
+
export function MenuHubHero({
|
|
70
|
+
kicker,
|
|
71
|
+
kickerIcon,
|
|
72
|
+
headline,
|
|
73
|
+
body,
|
|
74
|
+
figures,
|
|
75
|
+
proof,
|
|
76
|
+
proofIcon,
|
|
77
|
+
action,
|
|
78
|
+
backgroundUrl,
|
|
79
|
+
tileUrls,
|
|
80
|
+
className,
|
|
81
|
+
}: MenuHubHeroProps) {
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
className={cn(
|
|
85
|
+
"relative flex min-h-[260px] overflow-hidden rounded-[20px] bg-[oklch(0.21_0.012_55)] lg:min-h-[440px]",
|
|
86
|
+
className
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
{backgroundUrl ? (
|
|
90
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
91
|
+
<img
|
|
92
|
+
src={backgroundUrl}
|
|
93
|
+
alt=""
|
|
94
|
+
aria-hidden
|
|
95
|
+
className="absolute inset-0 h-full w-full object-cover opacity-[0.42]"
|
|
96
|
+
/>
|
|
97
|
+
) : (
|
|
98
|
+
<div
|
|
99
|
+
aria-hidden
|
|
100
|
+
className="absolute inset-0 grid auto-rows-fr grid-cols-4 gap-0.5 lg:grid-cols-7"
|
|
101
|
+
>
|
|
102
|
+
{Array.from({ length: TEXTURE_CELLS }, (_, cell) => {
|
|
103
|
+
const url = tileUrls?.length ? tileUrls[cell % tileUrls.length] : undefined;
|
|
104
|
+
|
|
105
|
+
return url ? (
|
|
106
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
107
|
+
<img
|
|
108
|
+
key={cell}
|
|
109
|
+
src={url}
|
|
110
|
+
alt=""
|
|
111
|
+
loading="lazy"
|
|
112
|
+
decoding="async"
|
|
113
|
+
className="h-full w-full object-cover"
|
|
114
|
+
/>
|
|
115
|
+
) : (
|
|
116
|
+
<div
|
|
117
|
+
key={cell}
|
|
118
|
+
className="bg-[repeating-linear-gradient(135deg,oklch(0.6_0.02_55)_0_9px,oklch(0.68_0.016_58)_9px_18px)] opacity-[0.42]"
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
})}
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{/*
|
|
126
|
+
Heavier over a photo mosaic than it was over flat texture: twelve
|
|
127
|
+
cinematic stills at full strength turn a claim into a collage and the
|
|
128
|
+
headline stops being the loudest thing on the banner. This keeps the
|
|
129
|
+
depth and gives the type its contrast back.
|
|
130
|
+
*/}
|
|
131
|
+
<div
|
|
132
|
+
aria-hidden
|
|
133
|
+
className={cn(
|
|
134
|
+
"absolute inset-0",
|
|
135
|
+
tileUrls?.length || backgroundUrl
|
|
136
|
+
? "bg-[linear-gradient(100deg,oklch(0.16_0.012_55_/_0.96)_0%,oklch(0.16_0.012_55_/_0.88)_52%,oklch(0.16_0.012_55_/_0.72)_100%)]"
|
|
137
|
+
: "bg-[linear-gradient(100deg,oklch(0.18_0.012_55_/_0.95)_0%,oklch(0.18_0.012_55_/_0.72)_52%,oklch(0.18_0.012_55_/_0.3)_100%)]"
|
|
138
|
+
)}
|
|
139
|
+
/>
|
|
140
|
+
|
|
141
|
+
<div className="relative z-[1] flex w-full flex-col justify-center gap-3 p-5 text-white lg:gap-4 lg:px-12 lg:py-11">
|
|
142
|
+
{kicker && (
|
|
143
|
+
<span className="inline-flex w-max items-center gap-[7px] rounded-full bg-white/15 px-3 py-1.5 text-[11px] font-extrabold tracking-[0.11em]">
|
|
144
|
+
{kickerIcon}
|
|
145
|
+
{kicker}
|
|
146
|
+
</span>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
<h1 className="max-w-[640px] font-serif text-[32px] font-semibold leading-[1.04] tracking-[-0.025em] lg:text-[50px]">
|
|
150
|
+
{headline}
|
|
151
|
+
</h1>
|
|
152
|
+
|
|
153
|
+
{body && (
|
|
154
|
+
<p className="hidden max-w-[540px] text-sm leading-[1.6] opacity-[0.84] lg:block lg:text-[15.5px]">
|
|
155
|
+
{body}
|
|
156
|
+
</p>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
{figures && figures.length > 0 && (
|
|
160
|
+
// Two by two on the phone: four figures in one row wrapped to three and an orphan.
|
|
161
|
+
<div className="grid grid-cols-2 gap-x-6 gap-y-3 pt-3 lg:flex lg:flex-wrap lg:gap-x-8 lg:gap-y-4 lg:pt-1.5">
|
|
162
|
+
{figures.map((figure) => (
|
|
163
|
+
<div key={figure.label}>
|
|
164
|
+
<div className="text-2xl font-black leading-none tracking-[-0.03em] tabular-nums lg:text-[32px]">
|
|
165
|
+
{figure.value}
|
|
166
|
+
</div>
|
|
167
|
+
<div className="mt-[5px] text-xs opacity-[0.72]">{figure.label}</div>
|
|
168
|
+
</div>
|
|
169
|
+
))}
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
{(proof || action) && (
|
|
174
|
+
<div className="mt-auto flex flex-wrap items-center gap-3 pt-5">
|
|
175
|
+
{proof && (
|
|
176
|
+
<span className="inline-flex min-h-11 items-center gap-2.5 rounded-full bg-white/10 px-4 py-2 text-[13px] backdrop-blur-[6px]">
|
|
177
|
+
{proofIcon}
|
|
178
|
+
{proof}
|
|
179
|
+
</span>
|
|
180
|
+
)}
|
|
181
|
+
{action}
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Beef, Coffee, Leaf, MapPin, Pizza, UtensilsCrossed } from "lucide-react";
|
|
3
|
+
import { MenuIntentTiles } from "./menu-intent-tiles";
|
|
4
|
+
import { mockIntentTiles } from "./menu-mocks";
|
|
5
|
+
|
|
6
|
+
const icons = [Pizza, Beef, Coffee, UtensilsCrossed, Leaf, MapPin];
|
|
7
|
+
const tiles = mockIntentTiles.map((tile, index) => {
|
|
8
|
+
const Icon = icons[index];
|
|
9
|
+
return { ...tile, icon: <Icon /> };
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const meta: Meta<typeof MenuIntentTiles> = {
|
|
13
|
+
title: "Menu/MenuIntentTiles",
|
|
14
|
+
component: MenuIntentTiles,
|
|
15
|
+
tags: ["autodocs"],
|
|
16
|
+
decorators: [
|
|
17
|
+
(Story) => (
|
|
18
|
+
<div className="w-[1100px] bg-background p-4">
|
|
19
|
+
<Story />
|
|
20
|
+
</div>
|
|
21
|
+
),
|
|
22
|
+
],
|
|
23
|
+
parameters: {
|
|
24
|
+
docs: {
|
|
25
|
+
description: {
|
|
26
|
+
component:
|
|
27
|
+
"The sentence people arrive with, as six coloured tiles. An intent is a family plus a constraint — Italian *tonight*, burgers, coffee — and each tile is a destination, so `href` is required on the view model: a category page that does not exist is filtered out by the caller rather than rendered as a tile linking nowhere. Roughly thirty-two of thirty-eight cuisines have a page, so that is the normal case, not the edge one.",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
type Story = StoryObj<typeof MenuIntentTiles>;
|
|
35
|
+
|
|
36
|
+
export const Default: Story = { args: { tiles } };
|
|
37
|
+
|
|
38
|
+
/** Two-up on a phone, six-up from `lg`. */
|
|
39
|
+
export const Mobile: Story = {
|
|
40
|
+
args: { tiles },
|
|
41
|
+
decorators: [
|
|
42
|
+
(Story) => (
|
|
43
|
+
<div className="w-[390px] bg-background p-3">
|
|
44
|
+
<Story />
|
|
45
|
+
</div>
|
|
46
|
+
),
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Without icons the tiles still read — the watermark is recognition, not information. */
|
|
51
|
+
export const WithoutIcons: Story = { args: { tiles: mockIntentTiles } };
|
|
52
|
+
|
|
53
|
+
/** Fewer tiles than a full row: the grid does not stretch them. */
|
|
54
|
+
export const Partial: Story = { args: { tiles: tiles.slice(0, 3) } };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ChevronRight } from "lucide-react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import type { MenuIntentTile, MenuLinkProps, MenuLinkComponent } from "./menu-types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* MenuIntentTiles — the sentence people arrive with, as six coloured tiles.
|
|
7
|
+
*
|
|
8
|
+
* An intent is a family plus a constraint: Italian *tonight*, burgers, coffee.
|
|
9
|
+
* Each tile is a destination, so `href` is required on the view model — a
|
|
10
|
+
* category page that does not exist is filtered out by the caller rather than
|
|
11
|
+
* rendered as a tile that links nowhere. Roughly thirty-two of thirty-eight
|
|
12
|
+
* cuisines have a page, so that case is normal, not exceptional.
|
|
13
|
+
*
|
|
14
|
+
* The icon is a watermark bleeding off the bottom-right corner at low opacity,
|
|
15
|
+
* not a badge: it gives the tile a silhouette to be recognised by in a rail
|
|
16
|
+
* without competing with the label. It is `aria-hidden` — the label already
|
|
17
|
+
* says what the tile is.
|
|
18
|
+
*
|
|
19
|
+
* Server-safe.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** @deprecated Alias of `MenuLinkProps`; kept so existing imports keep working. */
|
|
23
|
+
export type MenuIntentTilesLinkProps = MenuLinkProps;
|
|
24
|
+
|
|
25
|
+
function DefaultLink({ href, ...props }: MenuIntentTilesLinkProps) {
|
|
26
|
+
return <a href={href} {...props} />;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type MenuIntentTilesProps = {
|
|
30
|
+
tiles: MenuIntentTile[];
|
|
31
|
+
linkComponent?: MenuLinkComponent;
|
|
32
|
+
className?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function MenuIntentTiles({
|
|
36
|
+
tiles,
|
|
37
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
38
|
+
className,
|
|
39
|
+
}: MenuIntentTilesProps) {
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
className={cn(
|
|
43
|
+
"grid grid-cols-2 gap-3 lg:grid-cols-6 lg:gap-3.5",
|
|
44
|
+
className
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
{tiles.map((tile) => (
|
|
48
|
+
<LinkComponent
|
|
49
|
+
key={tile.slug}
|
|
50
|
+
href={tile.href}
|
|
51
|
+
className="relative flex min-h-[110px] flex-col overflow-hidden rounded-2xl bg-muted p-3.5 text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 lg:min-h-[124px] lg:px-3.5 lg:pb-3.5 lg:pt-[15px]"
|
|
52
|
+
style={tile.gradient ? { backgroundImage: tile.gradient } : undefined}
|
|
53
|
+
>
|
|
54
|
+
{tile.icon && (
|
|
55
|
+
<span
|
|
56
|
+
aria-hidden
|
|
57
|
+
className="pointer-events-none absolute -bottom-2.5 -right-1.5 z-0 opacity-[0.22] [&>svg]:h-[62px] [&>svg]:w-[62px]"
|
|
58
|
+
>
|
|
59
|
+
{tile.icon}
|
|
60
|
+
</span>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
<span className="relative z-[1] flex h-full flex-col">
|
|
64
|
+
<span className="text-[14.5px] font-extrabold leading-[1.2] tracking-[-0.015em]">
|
|
65
|
+
{tile.label}
|
|
66
|
+
</span>
|
|
67
|
+
{tile.caption && (
|
|
68
|
+
<span className="mt-[3px] text-[11.5px] leading-[1.35]">{tile.caption}</span>
|
|
69
|
+
)}
|
|
70
|
+
<ChevronRight className="mt-auto h-4 w-4 opacity-85" aria-hidden />
|
|
71
|
+
</span>
|
|
72
|
+
</LinkComponent>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|