@bearmenu/ui 0.8.16 → 0.8.18
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-WYTJR2YU.js → chunk-5CPBSNWQ.js} +6 -1
- package/dist/{chunk-36KEHCET.js → chunk-5LTILG3L.js} +15 -4
- package/dist/{chunk-P43B3ONY.js → chunk-5XREWOBB.js} +3 -1
- package/dist/{chunk-CLLY6XB3.js → chunk-6VOWT4YR.js} +6 -3
- package/dist/{chunk-S33K4RC5.js → chunk-BKKQ7GCZ.js} +1 -1
- package/dist/{chunk-ZLPWEHZO.js → chunk-EQYB4Q77.js} +2 -2
- package/dist/chunk-LYREUZH6.js +156 -0
- package/dist/chunk-PW247TG2.js +81 -0
- package/dist/chunk-QY66ME5B.js +207 -0
- package/dist/{chunk-T6S2ZBV6.js → chunk-SDWN6L4O.js} +1 -1
- package/dist/{chunk-CNJQUHCU.js → chunk-SWD6CJ35.js} +2 -1
- package/dist/{chunk-HDR7V6IZ.js → chunk-TIQNM4XD.js} +8 -1
- package/dist/components/alert-dialog.js +1 -1
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/button.js +1 -1
- package/dist/components/calendar.js +1 -1
- package/dist/components/card.d.ts +2 -2
- package/dist/components/carousel.js +1 -1
- package/dist/components/coach-marks.d.ts +63 -0
- package/dist/components/coach-marks.js +162 -0
- package/dist/components/combobox.js +1 -1
- package/dist/components/command.d.ts +15 -15
- package/dist/components/concept-compare-rail.js +3 -3
- package/dist/components/concept-compare.js +3 -3
- package/dist/components/concept-convergence.js +2 -2
- package/dist/components/doorway-tiles.d.ts +11 -0
- package/dist/components/doorway-tiles.js +5 -3
- package/dist/components/empty-state.js +1 -1
- package/dist/components/event-card.d.ts +22 -4
- package/dist/components/event-card.js +95 -50
- package/dist/components/event-day-header.d.ts +5 -3
- package/dist/components/event-day-header.js +12 -4
- package/dist/components/event-detail-panel.js +2 -2
- package/dist/components/event-door.d.ts +41 -0
- package/dist/components/event-door.js +65 -0
- package/dist/components/event-hub-hero.d.ts +10 -2
- package/dist/components/event-hub-hero.js +11 -5
- 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 +1 -1
- package/dist/components/event-tonight-strip.d.ts +50 -0
- package/dist/components/event-tonight-strip.js +67 -0
- package/dist/components/facet-group.d.ts +8 -1
- package/dist/components/facet-group.js +3 -0
- package/dist/components/filter-category-row.js +1 -1
- package/dist/components/hue-chip.js +1 -1
- package/dist/components/item.d.ts +2 -2
- package/dist/components/item.js +4 -1
- package/dist/components/kitchen-dossier.js +1 -1
- package/dist/components/link-button.js +1 -1
- package/dist/components/menu-family-block.d.ts +8 -1
- package/dist/components/menu-family-block.js +101 -68
- package/dist/components/menu-family-card.js +2 -2
- package/dist/components/menu-filter-chips.d.ts +12 -3
- package/dist/components/menu-filter-chips.js +22 -4
- package/dist/components/menu-hub-hero.d.ts +7 -1
- package/dist/components/menu-hub-hero.js +2 -1
- package/dist/components/menu-index-list.js +1 -1
- package/dist/components/menu-item.js +3 -3
- package/dist/components/menu-kitchen-list.d.ts +14 -2
- package/dist/components/menu-kitchen-list.js +1 -1
- package/dist/components/multi-select-combobox.js +2 -2
- package/dist/components/pagination.js +1 -1
- package/dist/components/photo-ground.d.ts +7 -1
- package/dist/components/photo-ground.js +1 -1
- package/dist/components/place-card.js +2 -2
- package/dist/components/place-details-mobile.js +1 -1
- package/dist/components/poster-stack.d.ts +36 -3
- package/dist/components/poster-stack.js +49 -18
- package/dist/components/searchable-select.js +1 -1
- package/dist/components/section-header.d.ts +7 -1
- package/dist/components/section-header.js +11 -7
- package/dist/components/signature-chip.js +2 -2
- package/dist/components/sliding-panels.js +1 -1
- package/dist/components/snap-rail.d.ts +2 -2
- package/dist/components/snap-rail.js +1 -1
- package/dist/components/spinner.d.ts +2 -2
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/text.d.ts +3 -3
- package/dist/components/timeline-day.d.ts +54 -0
- package/dist/components/timeline-day.js +65 -0
- package/dist/components/timeline-hour-puck.d.ts +47 -0
- package/dist/components/timeline-hour-puck.js +3 -0
- package/dist/components/timeline-hour.d.ts +96 -0
- package/dist/components/{timeline-band.js → timeline-hour.js} +2 -1
- package/dist/components/timeline-playhead.js +1 -3
- package/dist/components/toggle-group.js +9 -4
- package/dist/components/toggle.d.ts +1 -1
- package/dist/components/toggle.js +1 -1
- package/dist/components/top-ranking-entry.js +3 -3
- package/package.json +1 -1
- package/src/components/badge.stories.tsx +31 -1
- package/src/components/badge.tsx +2 -0
- package/src/components/button.stories.tsx +5 -4
- package/src/components/button.test.tsx +11 -1
- package/src/components/button.tsx +16 -5
- package/src/components/coach-marks.stories.tsx +79 -0
- package/src/components/coach-marks.test.tsx +140 -0
- package/src/components/coach-marks.tsx +252 -0
- package/src/components/doorway-tiles.stories.tsx +34 -0
- package/src/components/doorway-tiles.test.tsx +27 -0
- package/src/components/doorway-tiles.tsx +30 -2
- package/src/components/event-card.tsx +70 -13
- package/src/components/event-day-header.tsx +19 -7
- package/src/components/event-door.stories.tsx +88 -0
- package/src/components/event-door.tsx +97 -0
- package/src/components/event-hub-hero.tsx +21 -3
- package/src/components/event-tonight-strip.stories.tsx +78 -0
- package/src/components/event-tonight-strip.test.tsx +59 -0
- package/src/components/event-tonight-strip.tsx +134 -0
- package/src/components/facet-group.tsx +9 -0
- package/src/components/hue-chip.test.tsx +6 -3
- package/src/components/hue-chip.tsx +5 -2
- package/src/components/item.tsx +4 -1
- package/src/components/menu-family-block.stories.tsx +12 -0
- package/src/components/menu-family-block.tsx +53 -20
- package/src/components/menu-filter-chips.tsx +32 -4
- package/src/components/menu-hub-hero.test.tsx +13 -0
- package/src/components/menu-hub-hero.tsx +9 -2
- package/src/components/menu-kitchen-list.tsx +45 -6
- package/src/components/photo-ground.tsx +7 -0
- package/src/components/poster-stack.stories.tsx +43 -1
- package/src/components/poster-stack.tsx +98 -19
- package/src/components/section-header.test.tsx +15 -0
- package/src/components/section-header.tsx +26 -12
- package/src/components/snap-rail.tsx +6 -1
- package/src/components/timeline-day.stories.tsx +201 -0
- package/src/components/timeline-day.test.tsx +78 -0
- package/src/components/timeline-day.tsx +114 -0
- package/src/components/timeline-hour-puck.tsx +139 -0
- package/src/components/timeline-hour.tsx +266 -0
- package/src/components/timeline-playhead.tsx +1 -1
- package/src/components/toggle-group.stories.tsx +37 -1
- package/src/components/toggle-group.tsx +11 -6
- package/src/components/toggle.tsx +8 -0
- package/dist/chunk-NSNEANOQ.js +0 -104
- package/dist/chunk-SXQZ7DLH.js +0 -167
- package/dist/components/hour-band.d.ts +0 -66
- package/dist/components/hour-band.js +0 -81
- package/dist/components/hour-group.d.ts +0 -67
- package/dist/components/hour-group.js +0 -59
- package/dist/components/timeline-band.d.ts +0 -64
- package/src/components/hour-band.stories.tsx +0 -182
- package/src/components/hour-band.tsx +0 -146
- package/src/components/hour-group.stories.tsx +0 -100
- package/src/components/hour-group.tsx +0 -124
- package/src/components/timeline-band.stories.tsx +0 -206
- package/src/components/timeline-band.tsx +0 -247
|
@@ -99,6 +99,13 @@ export type MenuFamilyBlockProps = {
|
|
|
99
99
|
hue?: string;
|
|
100
100
|
/** Finished CSS gradient — the family's colour over the mosaic. */
|
|
101
101
|
gradient?: string;
|
|
102
|
+
/**
|
|
103
|
+
* `wide` (default): the phone card below `lg`, the two-column card — photo
|
|
104
|
+
* third, rows beside it — from `lg`. `compact`: the phone card at every
|
|
105
|
+
* width, for a rail on a desktop page where the two-column card is too much
|
|
106
|
+
* to scan across (the browse page's concept rail).
|
|
107
|
+
*/
|
|
108
|
+
variant?: "wide" | "compact";
|
|
102
109
|
linkComponent?: MenuLinkComponent;
|
|
103
110
|
className?: string;
|
|
104
111
|
};
|
|
@@ -108,17 +115,20 @@ const GROUND = PHOTO_GROUND;
|
|
|
108
115
|
|
|
109
116
|
/**
|
|
110
117
|
* The ink. Nearly absent at the top, heavier at the foot on the phone — the
|
|
111
|
-
* top 40% is the photograph, the rows sit on 0.92
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* it renders nothing in Storybook and the other three consumers.
|
|
118
|
+
* top 40% is the photograph, the rows sit on 0.92+. It is the layer the
|
|
119
|
+
* contrast rests on. Literal rather than `var(--scrim)`: that token lives in
|
|
120
|
+
* the app's stylesheet, not this package's, so a DS component reading it
|
|
121
|
+
* renders nothing in Storybook and the other three consumers.
|
|
116
122
|
*/
|
|
117
|
-
const
|
|
118
|
-
"bg-[linear-gradient(to_bottom,oklch(0.16_0.012_55_/_0.12)_0%,oklch(0.16_0.012_55_/_0.55)_38%,oklch(0.16_0.012_55_/_0.92)_62%,oklch(0.16_0.012_55_/_0.96)_100%)]
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
const INK_PHONE =
|
|
124
|
+
"bg-[linear-gradient(to_bottom,oklch(0.16_0.012_55_/_0.12)_0%,oklch(0.16_0.012_55_/_0.55)_38%,oklch(0.16_0.012_55_/_0.92)_62%,oklch(0.16_0.012_55_/_0.96)_100%)]";
|
|
125
|
+
/**
|
|
126
|
+
* From `lg` the ramp runs to the right: clear over the photo third, dark
|
|
127
|
+
* under the rows — the rows sit on the ink itself, no opaque paper, so the
|
|
128
|
+
* photograph still reads through the ramp's shoulder.
|
|
129
|
+
*/
|
|
130
|
+
const INK_WIDE =
|
|
131
|
+
"lg:bg-[linear-gradient(to_right,oklch(0.16_0.012_55_/_0.32)_0%,oklch(0.16_0.012_55_/_0.62)_34%,oklch(0.16_0.012_55_/_0.9)_52%,oklch(0.16_0.012_55_/_0.96)_100%)]";
|
|
122
132
|
|
|
123
133
|
/**
|
|
124
134
|
* A second, LOCAL scrim under the name, count and action — the photo column's
|
|
@@ -144,9 +154,11 @@ export function MenuFamilyBlock({
|
|
|
144
154
|
formatCount = String,
|
|
145
155
|
hue,
|
|
146
156
|
gradient,
|
|
157
|
+
variant = "wide",
|
|
147
158
|
linkComponent: LinkComponent = DefaultLink,
|
|
148
159
|
className,
|
|
149
160
|
}: MenuFamilyBlockProps) {
|
|
161
|
+
const wide = variant === "wide";
|
|
150
162
|
const [photo] = family.photoUrls ?? [];
|
|
151
163
|
// "+N more" counts past the rows shown, which are the venues the reader can
|
|
152
164
|
// see — the photograph is one of them, not a venue on its own.
|
|
@@ -161,7 +173,9 @@ export function MenuFamilyBlock({
|
|
|
161
173
|
className={cn(
|
|
162
174
|
/* `isolate` confines the blend and the backdrop-filter to this card.
|
|
163
175
|
`dark` scopes every token inside to its dark-theme value. */
|
|
164
|
-
|
|
176
|
+
/* A flex column, so a `h-full` card in a rail fills its row: siblings
|
|
177
|
+
with two rows and with three end on the same baseline. */
|
|
178
|
+
"dark relative isolate flex flex-col overflow-hidden rounded-[14px] text-foreground",
|
|
165
179
|
GROUND,
|
|
166
180
|
className
|
|
167
181
|
)}
|
|
@@ -171,21 +185,32 @@ export function MenuFamilyBlock({
|
|
|
171
185
|
<PhotoGround photos={[photo]} initials={initial} mode="stack" gradient={gradient} />
|
|
172
186
|
|
|
173
187
|
{/* ── Layer 2: the ink ────────────────────────────────────────────── */}
|
|
174
|
-
<span aria-hidden className={cn("absolute inset-0",
|
|
188
|
+
<span aria-hidden className={cn("absolute inset-0", INK_PHONE, wide && INK_WIDE)} />
|
|
175
189
|
|
|
176
190
|
{/* ── The content, floating ───────────────────────────────────────── */}
|
|
177
|
-
<div
|
|
191
|
+
<div
|
|
192
|
+
className={cn(
|
|
193
|
+
"relative grid flex-1 grid-cols-1 grid-rows-[auto_1fr]",
|
|
194
|
+
wide && "lg:grid-cols-[34%_1fr] lg:grid-rows-1"
|
|
195
|
+
)}
|
|
196
|
+
>
|
|
178
197
|
{/* The photo column. On the phone it is the card's head, with the
|
|
179
198
|
overflow pill pinned to the corner and the photograph clear above
|
|
180
199
|
the name; on desktop the left third, its stack anchored to the foot. */}
|
|
181
200
|
<div
|
|
182
201
|
className={cn(
|
|
183
|
-
"relative flex min-h-[220px] flex-col justify-end p-3.5
|
|
202
|
+
"relative flex min-h-[220px] flex-col justify-end p-3.5",
|
|
203
|
+
wide && "lg:min-h-[320px] lg:p-5",
|
|
184
204
|
TEXT_SCRIM
|
|
185
205
|
)}
|
|
186
206
|
>
|
|
187
207
|
{pill && (
|
|
188
|
-
<span
|
|
208
|
+
<span
|
|
209
|
+
className={cn(
|
|
210
|
+
"absolute right-3.5 top-3.5 rounded-full bg-[oklch(0.16_0.012_55_/_0.78)] px-2.5 py-1 text-[11px] font-semibold text-white",
|
|
211
|
+
wide && "lg:static lg:mb-3 lg:self-start"
|
|
212
|
+
)}
|
|
213
|
+
>
|
|
189
214
|
{pill}
|
|
190
215
|
</span>
|
|
191
216
|
)}
|
|
@@ -195,7 +220,12 @@ export function MenuFamilyBlock({
|
|
|
195
220
|
{family.kicker}
|
|
196
221
|
</span>
|
|
197
222
|
)}
|
|
198
|
-
<h3
|
|
223
|
+
<h3
|
|
224
|
+
className={cn(
|
|
225
|
+
"text-balance font-serif text-[26px] font-semibold leading-tight tracking-[-0.02em] text-white",
|
|
226
|
+
wide && "lg:text-[30px]"
|
|
227
|
+
)}
|
|
228
|
+
>
|
|
199
229
|
{family.name}
|
|
200
230
|
</h3>
|
|
201
231
|
{/* The count, the facts and the band on one line: a few facts about
|
|
@@ -230,7 +260,7 @@ export function MenuFamilyBlock({
|
|
|
230
260
|
aria-label={labels.browse}
|
|
231
261
|
className={cn(buttonVariants({ variant: "ink", size: "pill" }), "mt-3.5 w-max max-w-full")}
|
|
232
262
|
>
|
|
233
|
-
{labels.browseShort ? (
|
|
263
|
+
{labels.browseShort && wide ? (
|
|
234
264
|
<>
|
|
235
265
|
<span className="lg:hidden">{labels.browse}</span>
|
|
236
266
|
<span className="hidden lg:inline">{labels.browseShort}</span>
|
|
@@ -241,10 +271,10 @@ export function MenuFamilyBlock({
|
|
|
241
271
|
</LinkComponent>
|
|
242
272
|
</div>
|
|
243
273
|
|
|
244
|
-
{/* The row column: chips, then the rows, on
|
|
245
|
-
<div className={cn("flex min-w-0 flex-col px-3.5 pb-3.5 lg:p-5"
|
|
274
|
+
{/* The row column: chips, then the rows, on the ink's dark shoulder. */}
|
|
275
|
+
<div className={cn("flex min-w-0 flex-col px-3.5 pb-3.5", wide && "lg:p-5")}>
|
|
246
276
|
{family.chips && family.chips.length > 0 && (
|
|
247
|
-
<ul className="flex flex-wrap items-center gap-1.5 lg:justify-end">
|
|
277
|
+
<ul className={cn("flex flex-wrap items-center gap-1.5", wide && "lg:justify-end")}>
|
|
248
278
|
{family.chips.map((chip) => (
|
|
249
279
|
/* Label only. The counts are VENUES beside a dish group's name,
|
|
250
280
|
which every reader takes for dishes. */
|
|
@@ -261,6 +291,9 @@ export function MenuFamilyBlock({
|
|
|
261
291
|
<MenuKitchenList
|
|
262
292
|
kitchens={venues}
|
|
263
293
|
labels={labels.kitchens}
|
|
294
|
+
/* A half-width column: the dish goes under the venue, not
|
|
295
|
+
beside it. The compact card is the phone row throughout. */
|
|
296
|
+
layout={wide ? "stacked" : "phone"}
|
|
264
297
|
linkComponent={LinkComponent}
|
|
265
298
|
/>
|
|
266
299
|
</>
|
|
@@ -59,11 +59,19 @@ export type MenuFilterChipsProps = {
|
|
|
59
59
|
onMorePointerDown?: () => void;
|
|
60
60
|
/** Locale-aware integer formatter for the counts. Defaults to `String`. */
|
|
61
61
|
formatCount?: (value: number) => string;
|
|
62
|
+
/**
|
|
63
|
+
* `sm` (36px) is the row under a page header. `lg` (44px) is for a row
|
|
64
|
+
* inside a sticky bar, where the chips are the only thing the thumb can
|
|
65
|
+
* reach and 36px sits under the target floor.
|
|
66
|
+
*/
|
|
67
|
+
chipSize?: MenuFilterChipSize;
|
|
62
68
|
/** Accessible name for the row. */
|
|
63
69
|
ariaLabel?: string;
|
|
64
70
|
className?: string;
|
|
65
71
|
};
|
|
66
72
|
|
|
73
|
+
export type MenuFilterChipSize = "sm" | "lg";
|
|
74
|
+
|
|
67
75
|
export function MenuFilterChips({
|
|
68
76
|
chips,
|
|
69
77
|
onToggle,
|
|
@@ -73,6 +81,7 @@ export function MenuFilterChips({
|
|
|
73
81
|
moreIcon,
|
|
74
82
|
onMorePointerDown,
|
|
75
83
|
formatCount = String,
|
|
84
|
+
chipSize = "sm",
|
|
76
85
|
ariaLabel,
|
|
77
86
|
className,
|
|
78
87
|
}: MenuFilterChipsProps) {
|
|
@@ -99,6 +108,7 @@ export function MenuFilterChips({
|
|
|
99
108
|
<Chip
|
|
100
109
|
key={chip.key}
|
|
101
110
|
chip={chip}
|
|
111
|
+
size={chipSize}
|
|
102
112
|
onToggle={() => onToggle(chip.key)}
|
|
103
113
|
formatCount={formatCount}
|
|
104
114
|
/>
|
|
@@ -120,7 +130,11 @@ export function MenuFilterChips({
|
|
|
120
130
|
onPointerDown={onMorePointerDown}
|
|
121
131
|
aria-label={moreLabel}
|
|
122
132
|
aria-haspopup="dialog"
|
|
123
|
-
className=
|
|
133
|
+
className={cn(
|
|
134
|
+
"inline-flex shrink-0 items-center justify-center rounded-full border border-border text-accent transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none lg:hidden",
|
|
135
|
+
// The one control that never scrolls keeps the chips' height.
|
|
136
|
+
chipSize === "lg" ? "size-11" : "size-10"
|
|
137
|
+
)}
|
|
124
138
|
>
|
|
125
139
|
{moreIcon}
|
|
126
140
|
</button>
|
|
@@ -143,10 +157,12 @@ export function MenuFilterChips({
|
|
|
143
157
|
|
|
144
158
|
function Chip({
|
|
145
159
|
chip,
|
|
160
|
+
size,
|
|
146
161
|
onToggle,
|
|
147
162
|
formatCount,
|
|
148
163
|
}: {
|
|
149
164
|
chip: MenuFilterChip;
|
|
165
|
+
size: MenuFilterChipSize;
|
|
150
166
|
onToggle: () => void;
|
|
151
167
|
formatCount: (value: number) => string;
|
|
152
168
|
}) {
|
|
@@ -156,7 +172,8 @@ function Chip({
|
|
|
156
172
|
onClick={onToggle}
|
|
157
173
|
aria-pressed={chip.active}
|
|
158
174
|
className={cn(
|
|
159
|
-
"inline-flex
|
|
175
|
+
"inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-full border px-4 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none",
|
|
176
|
+
size === "lg" ? "h-11" : "h-9",
|
|
160
177
|
chip.active
|
|
161
178
|
? "border-accent font-semibold text-accent shadow-[0_1px_2px_oklch(0.51_0.095_45_/_0.08)]"
|
|
162
179
|
: "border-border bg-card text-foreground hover:border-accent/40",
|
|
@@ -226,12 +243,23 @@ export function MenuFilterSummary({
|
|
|
226
243
|
*/
|
|
227
244
|
const CHIP_WIDTHS = [128, 104, 96, 148, 112, 88];
|
|
228
245
|
|
|
229
|
-
export function MenuFilterChipsSkeleton({
|
|
246
|
+
export function MenuFilterChipsSkeleton({
|
|
247
|
+
className,
|
|
248
|
+
chipSize = "sm",
|
|
249
|
+
}: {
|
|
250
|
+
className?: string;
|
|
251
|
+
/** Match the row it stands in for, so the chips do not jump 8px on load. */
|
|
252
|
+
chipSize?: MenuFilterChipSize;
|
|
253
|
+
}) {
|
|
230
254
|
return (
|
|
231
255
|
<div className={cn("flex items-center gap-2 overflow-hidden", className)} aria-hidden>
|
|
232
256
|
<Skeleton className="h-3 w-[92px] shrink-0" />
|
|
233
257
|
{CHIP_WIDTHS.map((width, index) => (
|
|
234
|
-
<Skeleton
|
|
258
|
+
<Skeleton
|
|
259
|
+
key={index}
|
|
260
|
+
className={cn("shrink-0 rounded-full", chipSize === "lg" ? "h-11" : "h-9")}
|
|
261
|
+
style={{ width }}
|
|
262
|
+
/>
|
|
235
263
|
))}
|
|
236
264
|
</div>
|
|
237
265
|
);
|
|
@@ -28,6 +28,19 @@ describe("MenuHubHero mosaic", () => {
|
|
|
28
28
|
});
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
describe("MenuHubHero heading level", () => {
|
|
32
|
+
it("renders the headline as h1 by default", () => {
|
|
33
|
+
render(<MenuHubHero headline="Every kitchen" />);
|
|
34
|
+
expect(document.querySelector("h1")?.textContent).toBe("Every kitchen");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("renders as h2 for a page that already owns its h1", () => {
|
|
38
|
+
render(<MenuHubHero headline="Every kitchen" headingLevel="h2" />);
|
|
39
|
+
expect(document.querySelector("h1")).toBeNull();
|
|
40
|
+
expect(document.querySelector("h2")?.textContent).toBe("Every kitchen");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
31
44
|
describe("VenueWall mosaic", () => {
|
|
32
45
|
it("follows the hero's rule: six on the phone, eighteen from lg", () => {
|
|
33
46
|
render(<VenueWall places={[]} headline="Every menu" tileUrls={tiles} />);
|
|
@@ -60,6 +60,12 @@ export type MenuHubHeroProps = {
|
|
|
60
60
|
* a 1440px hero.
|
|
61
61
|
*/
|
|
62
62
|
tileUrls?: string[];
|
|
63
|
+
/**
|
|
64
|
+
* The element the headline renders as. `h1` by default; a page that
|
|
65
|
+
* already owns its `<h1>` (an sr-only SEO heading, as on `/menu`) passes
|
|
66
|
+
* `h2` so the document has one h1, not two.
|
|
67
|
+
*/
|
|
68
|
+
headingLevel?: "h1" | "h2";
|
|
63
69
|
className?: string;
|
|
64
70
|
};
|
|
65
71
|
|
|
@@ -84,6 +90,7 @@ export function MenuHubHero({
|
|
|
84
90
|
action,
|
|
85
91
|
backgroundUrl,
|
|
86
92
|
tileUrls,
|
|
93
|
+
headingLevel: Heading = "h1",
|
|
87
94
|
className,
|
|
88
95
|
}: MenuHubHeroProps) {
|
|
89
96
|
return (
|
|
@@ -160,9 +167,9 @@ export function MenuHubHero({
|
|
|
160
167
|
|
|
161
168
|
{/* 28px on a phone: 32px on a 358px column is five or six words a
|
|
162
169
|
line, which left "walk in blind." wrapping alone. */}
|
|
163
|
-
<
|
|
170
|
+
<Heading className="max-w-[640px] font-serif text-[28px] font-semibold leading-[1.06] tracking-[-0.025em] lg:text-[50px] lg:leading-[1.04]">
|
|
164
171
|
{headline}
|
|
165
|
-
</
|
|
172
|
+
</Heading>
|
|
166
173
|
|
|
167
174
|
{body && (
|
|
168
175
|
<p className="hidden max-w-[540px] text-sm leading-[1.6] opacity-[0.84] lg:block lg:text-[15.5px]">
|
|
@@ -61,6 +61,18 @@ export type MenuKitchenListLabels = {
|
|
|
61
61
|
showMore?: string;
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* `wide` (default): the intent page's full-width list — from `lg` the venue
|
|
66
|
+
* takes a fixed 226px column and the dish stands beside it in a column of its
|
|
67
|
+
* own. `stacked`: the list sits in a narrow column (a concept card's row
|
|
68
|
+
* half), so the venue keeps the row and the dish takes a SECOND LINE under it
|
|
69
|
+
* from `lg`; in a half-width card the dish column had ~100px and its kicker,
|
|
70
|
+
* name and meta wrapped into a stack of single words. `phone`: the phone row
|
|
71
|
+
* at every width — no dish line, the venue and its price only — for a card
|
|
72
|
+
* that keeps its phone form on a desktop rail.
|
|
73
|
+
*/
|
|
74
|
+
export type MenuKitchenListLayout = "wide" | "stacked" | "phone";
|
|
75
|
+
|
|
64
76
|
export type MenuKitchenListProps = {
|
|
65
77
|
kitchens: MenuKitchenPreview[];
|
|
66
78
|
labels: MenuKitchenListLabels;
|
|
@@ -69,6 +81,7 @@ export type MenuKitchenListProps = {
|
|
|
69
81
|
* navigates for the eleventh — the list is the page.
|
|
70
82
|
*/
|
|
71
83
|
onShowMore?: () => void;
|
|
84
|
+
layout?: MenuKitchenListLayout;
|
|
72
85
|
linkComponent?: MenuLinkComponent;
|
|
73
86
|
className?: string;
|
|
74
87
|
};
|
|
@@ -77,6 +90,7 @@ export function MenuKitchenList({
|
|
|
77
90
|
kitchens,
|
|
78
91
|
labels,
|
|
79
92
|
onShowMore,
|
|
93
|
+
layout = "wide",
|
|
80
94
|
linkComponent = DefaultLink,
|
|
81
95
|
className,
|
|
82
96
|
}: MenuKitchenListProps) {
|
|
@@ -90,7 +104,12 @@ export function MenuKitchenList({
|
|
|
90
104
|
because `<ol>` admits only list items, so a sibling element
|
|
91
105
|
between them would be invalid markup. */}
|
|
92
106
|
{index > 0 && <Separator />}
|
|
93
|
-
<KitchenRow
|
|
107
|
+
<KitchenRow
|
|
108
|
+
kitchen={kitchen}
|
|
109
|
+
labels={labels}
|
|
110
|
+
layout={layout}
|
|
111
|
+
linkComponent={linkComponent}
|
|
112
|
+
/>
|
|
94
113
|
</li>
|
|
95
114
|
))}
|
|
96
115
|
</ol>
|
|
@@ -111,17 +130,25 @@ export function MenuKitchenList({
|
|
|
111
130
|
function KitchenRow({
|
|
112
131
|
kitchen,
|
|
113
132
|
labels,
|
|
133
|
+
layout,
|
|
114
134
|
linkComponent: LinkComponent,
|
|
115
135
|
}: {
|
|
116
136
|
kitchen: MenuKitchenPreview;
|
|
117
137
|
labels: MenuKitchenListLabels;
|
|
138
|
+
layout: MenuKitchenListLayout;
|
|
118
139
|
linkComponent: MenuLinkComponent;
|
|
119
140
|
}) {
|
|
120
141
|
const { place, dish } = kitchen;
|
|
121
142
|
const openNow = place.open_now === true;
|
|
143
|
+
const wide = layout === "wide";
|
|
122
144
|
|
|
123
145
|
return (
|
|
124
|
-
<div
|
|
146
|
+
<div
|
|
147
|
+
className={cn(
|
|
148
|
+
"relative flex flex-wrap items-center gap-[11px] py-3 transition-colors hover:bg-muted/30 motion-reduce:transition-none lg:gap-4 lg:py-3.5",
|
|
149
|
+
wide && "lg:flex-nowrap"
|
|
150
|
+
)}
|
|
151
|
+
>
|
|
125
152
|
{/* The venue link, stretched. `z-[1]` — the dish link sits above it. */}
|
|
126
153
|
<LinkComponent
|
|
127
154
|
href={kitchen.href}
|
|
@@ -149,7 +176,12 @@ function KitchenRow({
|
|
|
149
176
|
/>
|
|
150
177
|
</span>
|
|
151
178
|
|
|
152
|
-
<div
|
|
179
|
+
<div
|
|
180
|
+
className={cn(
|
|
181
|
+
"flex min-w-0 flex-1 flex-col gap-0.5",
|
|
182
|
+
wide && "lg:w-[226px] lg:flex-none"
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
153
185
|
{/* One line with an ellipsis on a phone, where the row is the whole
|
|
154
186
|
width; two lines from `lg`, where the column is 226px and
|
|
155
187
|
"ADRIATICA GELATERIA ITALIANA" would otherwise lose its half. */}
|
|
@@ -190,8 +222,15 @@ function KitchenRow({
|
|
|
190
222
|
one tap away on the venue's own page, with room to state it.
|
|
191
223
|
|
|
192
224
|
`order-5` puts it last in the visual order without moving it in the
|
|
193
|
-
DOM, where it belongs to the venue.
|
|
194
|
-
|
|
225
|
+
DOM, where it belongs to the venue. In the `stacked` layout it is a
|
|
226
|
+
second LINE under the venue, indented past the thumbnail. */
|
|
227
|
+
<div
|
|
228
|
+
className={cn(
|
|
229
|
+
"hidden min-w-0 basis-full pl-[57px]",
|
|
230
|
+
layout !== "phone" && "lg:block",
|
|
231
|
+
wide ? "lg:order-none lg:flex-1 lg:basis-auto lg:pl-0" : "lg:order-5 lg:pl-[74px]"
|
|
232
|
+
)}
|
|
233
|
+
>
|
|
195
234
|
<span
|
|
196
235
|
className={cn(
|
|
197
236
|
"flex items-center gap-1.5 text-[9px] font-extrabold uppercase tracking-[0.1em]",
|
|
@@ -230,7 +269,7 @@ function KitchenRow({
|
|
|
230
269
|
</div>
|
|
231
270
|
)}
|
|
232
271
|
|
|
233
|
-
<div className="ml-auto flex shrink-0 items-center gap-[11px] lg:ml-0">
|
|
272
|
+
<div className={cn("ml-auto flex shrink-0 items-center gap-[11px]", wide && "lg:ml-0")}>
|
|
234
273
|
{kitchen.priceLabel && (
|
|
235
274
|
<span className="text-base font-extrabold tabular-nums text-accent">
|
|
236
275
|
{kitchen.priceLabel}
|
|
@@ -38,6 +38,13 @@ import { cn } from "../lib/utils";
|
|
|
38
38
|
/** The ground under the photography. Fixed, and deliberately not a token. */
|
|
39
39
|
export const PHOTO_GROUND = "bg-[oklch(0.21_0.012_55)]";
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The shadow a glyph wears when it stands on `PHOTO_GROUND` in a photograph's
|
|
43
|
+
* place — the card family's fallback treatment: a ground and a lifted glyph,
|
|
44
|
+
* never a bare icon on a flat fill. Same literal family as the ground.
|
|
45
|
+
*/
|
|
46
|
+
export const PHOTO_GLYPH_SHADOW = "drop-shadow-[0_2px_6px_oklch(0.16_0.012_55_/_0.6)]";
|
|
47
|
+
|
|
41
48
|
/** The uniform dark overlay a card may put over every photograph. */
|
|
42
49
|
const OVERLAY = "absolute inset-0 bg-[oklch(0.16_0.012_55_/_0.5)]";
|
|
43
50
|
|
|
@@ -56,7 +56,7 @@ export const TwoPosters: Story = {
|
|
|
56
56
|
},
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
/** No `src` renders the
|
|
59
|
+
/** No `src` renders the poster fallback, same as `EventCard`. */
|
|
60
60
|
export const MissingSources: Story = {
|
|
61
61
|
args: {
|
|
62
62
|
posters: [
|
|
@@ -66,3 +66,45 @@ export const MissingSources: Story = {
|
|
|
66
66
|
],
|
|
67
67
|
},
|
|
68
68
|
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Tappable: every poster an anchor to its event, the title captioned at its
|
|
72
|
+
* foot. Hover or focus a poster and it comes to the front; the press scale
|
|
73
|
+
* is on the anchor, the float on the wrapper, so the two never fight.
|
|
74
|
+
*/
|
|
75
|
+
export const Tappable: Story = {
|
|
76
|
+
args: {
|
|
77
|
+
posters: [
|
|
78
|
+
{
|
|
79
|
+
src: posterDataUri("1", "#7a5c46"),
|
|
80
|
+
alt: "Psihotrop poster",
|
|
81
|
+
caption: "Psihotrop — live at Form Space",
|
|
82
|
+
href: "#psihotrop",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
src: posterDataUri("2", "#4a6a5c"),
|
|
86
|
+
alt: "Warehouse rave poster",
|
|
87
|
+
caption: "Warehouse rave: all night long",
|
|
88
|
+
href: "#warehouse",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
src: posterDataUri("3", "#6a5c8a"),
|
|
92
|
+
alt: "Jazz night poster",
|
|
93
|
+
caption: "Jazz night at the Jazz Club",
|
|
94
|
+
href: "#jazz",
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** The phone geometry: three posters inside a 343px column, none past the gutter. */
|
|
101
|
+
export const Phone: Story = {
|
|
102
|
+
...Tappable,
|
|
103
|
+
decorators: [
|
|
104
|
+
(Story) => (
|
|
105
|
+
<div className="w-[343px] border-x border-dashed border-border">
|
|
106
|
+
<Story />
|
|
107
|
+
</div>
|
|
108
|
+
),
|
|
109
|
+
],
|
|
110
|
+
};
|