@bearmenu/ui 0.8.10 → 0.8.12
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-UKBNQO6Q.js → chunk-2CXU5WZO.js} +1 -1
- package/dist/{chunk-62ES6N32.js → chunk-43RC7MHY.js} +1 -1
- package/dist/{chunk-Q6QH52LJ.js → chunk-6TEEY5YA.js} +2 -2
- package/dist/{chunk-UDN7UHJW.js → chunk-7A42J73Q.js} +3 -2
- package/dist/{chunk-5RWTOT5Y.js → chunk-CNJQUHCU.js} +11 -1
- package/dist/{chunk-TH6W2PWG.js → chunk-UDTG3RSE.js} +1 -1
- package/dist/{chunk-H3BV4VAG.js → chunk-VAXOQS7W.js} +14 -2
- package/dist/{chunk-SY6RV476.js → chunk-WCTHZKU7.js} +10 -21
- package/dist/{chunk-AKQ57NXY.js → chunk-ZQ53AGML.js} +2 -2
- package/dist/components/alert-dialog.js +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/button.js +1 -1
- package/dist/components/calendar.js +1 -1
- package/dist/components/carousel.js +1 -1
- package/dist/components/combobox.js +1 -1
- package/dist/components/concept-compare-rail.js +2 -2
- package/dist/components/concept-compare.d.ts +1 -1
- package/dist/components/concept-compare.js +31 -10
- package/dist/components/concept-convergence.js +2 -2
- package/dist/components/empty-state.js +1 -1
- package/dist/components/event-detail-panel.js +1 -1
- package/dist/components/event-list-row-wide.js +1 -1
- package/dist/components/filter-category-row.js +1 -1
- package/dist/components/glass-ground.d.ts +26 -31
- package/dist/components/glass-ground.js +1 -1
- package/dist/components/haptic-button.js +1 -1
- package/dist/components/input-group.d.ts +2 -2
- package/dist/components/input.d.ts +1 -1
- package/dist/components/item.d.ts +15 -5
- package/dist/components/item.js +15 -10
- package/dist/components/kitchen-dossier.d.ts +9 -7
- package/dist/components/kitchen-dossier.js +31 -25
- package/dist/components/link-button.js +1 -1
- package/dist/components/menu-family-block.d.ts +22 -17
- package/dist/components/menu-family-block.js +62 -61
- package/dist/components/menu-family-card.js +2 -4
- package/dist/components/menu-family-tiles.d.ts +19 -0
- package/dist/components/menu-family-tiles.js +57 -0
- package/dist/components/menu-hub-hero.js +18 -11
- package/dist/components/menu-index-list.d.ts +31 -12
- package/dist/components/menu-index-list.js +68 -31
- package/dist/components/menu-intent-hero.d.ts +1 -1
- package/dist/components/menu-item.js +3 -3
- package/dist/components/menu-mocks.d.ts +10 -3
- package/dist/components/menu-mocks.js +21 -9
- package/dist/components/menu-types.d.ts +33 -10
- package/dist/components/multi-select-combobox.js +2 -2
- package/dist/components/pagination.js +1 -1
- package/dist/components/photo-ground.d.ts +8 -2
- package/dist/components/photo-ground.js +1 -1
- package/dist/components/place-details-mobile.js +1 -1
- package/dist/components/searchable-select.js +1 -1
- package/dist/components/signature-chip.d.ts +2 -2
- package/dist/components/signature-chip.js +2 -2
- package/dist/components/sliding-panels.js +1 -1
- package/dist/components/top-ranking-entry.js +3 -3
- package/dist/components/venue-grade-dimensions.js +1 -1
- package/dist/components/venue-quotes.d.ts +1 -1
- package/dist/components/venue-quotes.js +1 -1
- package/dist/components/venue-wall.js +5 -3
- package/dist/components/weekday-theme-banner.d.ts +24 -3
- package/dist/components/weekday-theme-banner.js +63 -15
- package/package.json +1 -1
- package/src/components/badge.tsx +1 -1
- package/src/components/button.stories.tsx +25 -2
- package/src/components/button.tsx +14 -2
- package/src/components/colour-dictionary.stories.tsx +1 -1
- package/src/components/concept-compare-rail.tsx +2 -1
- package/src/components/concept-compare.stories.tsx +3 -3
- package/src/components/concept-compare.tsx +50 -14
- package/src/components/glass-ground.tsx +31 -56
- package/src/components/item.tsx +69 -45
- package/src/components/kitchen-dossier.stories.tsx +1 -1
- package/src/components/kitchen-dossier.tsx +60 -40
- package/src/components/menu-family-block.stories.tsx +3 -3
- package/src/components/menu-family-block.tsx +94 -98
- package/src/components/menu-family-card.stories.tsx +2 -2
- package/src/components/menu-family-card.tsx +6 -13
- package/src/components/menu-family-tiles.stories.tsx +54 -0
- package/src/components/menu-family-tiles.test.tsx +30 -0
- package/src/components/menu-family-tiles.tsx +132 -0
- package/src/components/menu-hub-hero.test.tsx +39 -0
- package/src/components/menu-hub-hero.tsx +30 -12
- package/src/components/menu-index-list.stories.tsx +6 -1
- package/src/components/menu-index-list.tsx +101 -54
- package/src/components/menu-intent-hero.tsx +1 -1
- package/src/components/menu-mocks.ts +32 -9
- package/src/components/menu-types.ts +32 -9
- package/src/components/photo-ground.tsx +14 -2
- package/src/components/signature-chip.tsx +3 -3
- package/src/components/venue-grade-dimensions.tsx +9 -2
- package/src/components/venue-quotes.stories.tsx +1 -1
- package/src/components/venue-quotes.tsx +4 -2
- package/src/components/venue-wall.tsx +9 -4
- package/src/components/weekday-theme-banner.stories.tsx +43 -4
- package/src/components/weekday-theme-banner.tsx +95 -18
- package/dist/components/menu-intent-tiles.d.ts +0 -31
- package/dist/components/menu-intent-tiles.js +0 -50
- package/src/components/menu-intent-tiles.stories.tsx +0 -54
- package/src/components/menu-intent-tiles.tsx +0 -76
|
@@ -1,23 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
2
5
|
import { cn } from "../lib/utils";
|
|
6
|
+
import { Button } from "./button";
|
|
3
7
|
import { Separator } from "./separator";
|
|
4
8
|
import { Skeleton } from "./skeleton";
|
|
5
9
|
import type { MenuIndexEntry, MenuLinkComponent, MenuLinkProps } from "./menu-types";
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
|
-
* MenuIndexList — the complete set,
|
|
12
|
+
* MenuIndexList — the complete set: one column on a phone, three from `lg`.
|
|
9
13
|
*
|
|
10
14
|
* ## This block is why the page can afford to be progressive
|
|
11
15
|
*
|
|
12
16
|
* It carries a crawlable link to EVERY destination the city has — all
|
|
13
|
-
* thirty-eight cuisines, not the
|
|
14
|
-
* it the page's entire link-equity job, which in turn means the rich
|
|
15
|
-
*
|
|
16
|
-
* crawler needs is hidden behind that control, because it is all
|
|
17
|
+
* thirty-eight cuisines, not the eight the page draws in full above. That
|
|
18
|
+
* makes it the page's entire link-equity job, which in turn means the rich
|
|
19
|
+
* blocks are free to load eight at a time behind a "show more": nothing a
|
|
20
|
+
* crawler needs is hidden behind that control, because it is all here.
|
|
21
|
+
*
|
|
22
|
+
* The phone's own "show all" is CSS, not markup: every row is in the DOM and
|
|
23
|
+
* the ones past `collapsedCount` are hidden below `lg` until the button is
|
|
24
|
+
* pressed. A list that mounted its tail on tap would have taken twenty-six
|
|
25
|
+
* links out of the page for the crawler that never taps.
|
|
17
26
|
*
|
|
18
|
-
* Get this backwards — put the whole set behind the disclosure and only
|
|
19
|
-
* links in the markup — and the page silently stops linking to
|
|
20
|
-
*
|
|
27
|
+
* Get this backwards — put the whole set behind the disclosure and only eight
|
|
28
|
+
* links in the markup — and the page silently stops linking to thirty pages
|
|
29
|
+
* it is supposed to be the index for.
|
|
21
30
|
*
|
|
22
31
|
* ## Ordered by count, and the counts overlap
|
|
23
32
|
*
|
|
@@ -25,6 +34,10 @@ import type { MenuIndexEntry, MenuLinkComponent, MenuLinkProps } from "./menu-ty
|
|
|
25
34
|
* places, because a trattoria is Italian AND pizza AND international. So this
|
|
26
35
|
* is a ranked list of ways in, never a partition, and no copy near it may total
|
|
27
36
|
* the numbers or call them a breakdown.
|
|
37
|
+
*
|
|
38
|
+
* A client component for the one piece of state the expander needs; the
|
|
39
|
+
* `linkComponent` a server caller hands in must therefore be a client
|
|
40
|
+
* reference (`ClientLink`), not `next/link` itself.
|
|
28
41
|
*/
|
|
29
42
|
|
|
30
43
|
/** @deprecated Alias of `MenuLinkProps`; kept for symmetry with the other menu surfaces. */
|
|
@@ -40,8 +53,15 @@ export type MenuIndexListProps = {
|
|
|
40
53
|
title: string;
|
|
41
54
|
/** One line under it, e.g. "Ordered by kitchens — every one has its own page". */
|
|
42
55
|
subtitle?: string;
|
|
43
|
-
/**
|
|
44
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Rows shown on a phone before the expander; the rest are hidden below `lg`
|
|
58
|
+
* until it is pressed. Omit to show every row at every width.
|
|
59
|
+
*/
|
|
60
|
+
collapsedCount?: number;
|
|
61
|
+
/** The expander's labels, e.g. "Show all 38 cuisines" / "Show fewer". */
|
|
62
|
+
expandLabels?: { showAll: string; showFewer: string };
|
|
63
|
+
/** The colour mark beside each name, keyed by slug. Falls back to the border colour. */
|
|
64
|
+
hues?: Record<string, string>;
|
|
45
65
|
linkComponent?: MenuLinkComponent;
|
|
46
66
|
className?: string;
|
|
47
67
|
};
|
|
@@ -50,10 +70,16 @@ export function MenuIndexList({
|
|
|
50
70
|
entries,
|
|
51
71
|
title,
|
|
52
72
|
subtitle,
|
|
53
|
-
|
|
73
|
+
collapsedCount,
|
|
74
|
+
expandLabels,
|
|
75
|
+
hues,
|
|
54
76
|
linkComponent: LinkComponent = DefaultLink,
|
|
55
77
|
className,
|
|
56
78
|
}: MenuIndexListProps) {
|
|
79
|
+
const [expanded, setExpanded] = useState(false);
|
|
80
|
+
const collapsible =
|
|
81
|
+
collapsedCount !== undefined && expandLabels !== undefined && entries.length > collapsedCount;
|
|
82
|
+
|
|
57
83
|
return (
|
|
58
84
|
<section
|
|
59
85
|
className={cn(
|
|
@@ -68,55 +94,76 @@ export function MenuIndexList({
|
|
|
68
94
|
)}
|
|
69
95
|
</div>
|
|
70
96
|
|
|
71
|
-
{/*
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
layout the top-left row had no rule and the top-right one, its
|
|
84
|
-
own neighbour, did. Ruling every row is both uniform and simpler
|
|
85
|
-
than restoring the exception per breakpoint. */}
|
|
86
|
-
<Separator />
|
|
87
|
-
<LinkComponent
|
|
88
|
-
href={entry.href}
|
|
89
|
-
className="group flex items-center gap-2.5 py-2.5 transition-colors hover:text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring motion-reduce:transition-none"
|
|
97
|
+
{/* One column on a phone — a row holds a name of realistic length, its
|
|
98
|
+
count and the chevron with room to spare, and the expander keeps the
|
|
99
|
+
list to twelve rows until asked. Three from `lg`. */}
|
|
100
|
+
<ul className="grid grid-cols-1 lg:grid-cols-3 lg:gap-x-8">
|
|
101
|
+
{entries.map((entry, index) => {
|
|
102
|
+
const hue = hues?.[entry.slug];
|
|
103
|
+
return (
|
|
104
|
+
<li
|
|
105
|
+
key={entry.slug}
|
|
106
|
+
className={cn(
|
|
107
|
+
collapsible && !expanded && index >= collapsedCount && "max-lg:hidden"
|
|
108
|
+
)}
|
|
90
109
|
>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
{/* The DS `Separator`, above every row including the first, so
|
|
111
|
+
the rule is uniform across columns. */}
|
|
112
|
+
<Separator />
|
|
113
|
+
<LinkComponent
|
|
114
|
+
href={entry.href}
|
|
115
|
+
className="group flex items-center gap-2.5 py-2.5 transition-colors hover:text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring motion-reduce:transition-none"
|
|
116
|
+
>
|
|
117
|
+
{/* The family's mark: a dot in its hue. */}
|
|
118
|
+
<span
|
|
119
|
+
aria-hidden
|
|
120
|
+
className={cn("h-2 w-2 shrink-0 rounded-full", !hue && !entry.gradient && "bg-border")}
|
|
121
|
+
style={
|
|
122
|
+
hue
|
|
123
|
+
? { backgroundColor: hue }
|
|
124
|
+
: entry.gradient
|
|
125
|
+
? { backgroundImage: entry.gradient }
|
|
126
|
+
: undefined
|
|
127
|
+
}
|
|
128
|
+
/>
|
|
129
|
+
<span className="min-w-0 flex-1 truncate text-sm font-medium">{entry.name}</span>
|
|
130
|
+
<span className="shrink-0 text-[13px] tabular-nums text-muted-foreground">
|
|
131
|
+
{entry.countLabel ?? String(entry.count)}
|
|
132
|
+
</span>
|
|
133
|
+
<ChevronRight
|
|
134
|
+
className="h-3.5 w-3.5 shrink-0 text-muted-foreground transition-colors group-hover:text-accent motion-reduce:transition-none"
|
|
135
|
+
aria-hidden
|
|
136
|
+
/>
|
|
137
|
+
</LinkComponent>
|
|
138
|
+
</li>
|
|
139
|
+
);
|
|
140
|
+
})}
|
|
110
141
|
</ul>
|
|
142
|
+
|
|
143
|
+
{collapsible && (
|
|
144
|
+
<Button
|
|
145
|
+
variant="outline"
|
|
146
|
+
fullWidth
|
|
147
|
+
className="mt-2 lg:hidden"
|
|
148
|
+
aria-expanded={expanded}
|
|
149
|
+
onClick={() => setExpanded((open) => !open)}
|
|
150
|
+
>
|
|
151
|
+
{expanded ? expandLabels.showFewer : expandLabels.showAll}
|
|
152
|
+
<ChevronDown
|
|
153
|
+
className={cn("transition-transform motion-reduce:transition-none", expanded && "rotate-180")}
|
|
154
|
+
aria-hidden
|
|
155
|
+
/>
|
|
156
|
+
</Button>
|
|
157
|
+
)}
|
|
111
158
|
</section>
|
|
112
159
|
);
|
|
113
160
|
}
|
|
114
161
|
|
|
115
162
|
// ─── Skeleton ───────────────────────────────────────────────────────────────
|
|
116
163
|
|
|
117
|
-
/**
|
|
164
|
+
/** One column on a phone, three from `lg` — the grid the list itself renders. */
|
|
118
165
|
export function MenuIndexListSkeleton({
|
|
119
|
-
rows =
|
|
166
|
+
rows = 12,
|
|
120
167
|
className,
|
|
121
168
|
}: {
|
|
122
169
|
rows?: number;
|
|
@@ -132,12 +179,12 @@ export function MenuIndexListSkeleton({
|
|
|
132
179
|
>
|
|
133
180
|
<Skeleton className="h-[15px] w-[140px]" />
|
|
134
181
|
<Skeleton className="mt-1.5 h-3 w-[240px]" />
|
|
135
|
-
<ul className="mt-2 grid grid-cols-
|
|
182
|
+
<ul className="mt-2 grid grid-cols-1 lg:grid-cols-3 lg:gap-x-8">
|
|
136
183
|
{Array.from({ length: rows }, (_, index) => (
|
|
137
184
|
<li key={index}>
|
|
138
185
|
<Separator />
|
|
139
186
|
<div className="flex items-center gap-2.5 py-2.5">
|
|
140
|
-
<Skeleton className="h-2
|
|
187
|
+
<Skeleton className="h-2 w-2 rounded-full" />
|
|
141
188
|
<Skeleton className="h-3.5 flex-1" />
|
|
142
189
|
<Skeleton className="h-3 w-6" />
|
|
143
190
|
</div>
|
|
@@ -55,7 +55,7 @@ export type MenuIntentHeroProps = {
|
|
|
55
55
|
* Finished CSS gradient for the ground, e.g. `linear-gradient(140deg, …)`.
|
|
56
56
|
*
|
|
57
57
|
* A gradient rather than a token name, for the same reason `<MenuField>` and
|
|
58
|
-
* `<
|
|
58
|
+
* `<MenuFamilyTiles>` take one: the hue tables are app-owned, and a component
|
|
59
59
|
* referencing `var(--h-pizza)` renders an unstyled panel in any consumer that
|
|
60
60
|
* does not define it. Missing, the panel falls back to the foreground colour
|
|
61
61
|
* — visibly unstyled rather than white text on white.
|
|
@@ -25,7 +25,7 @@ import type {
|
|
|
25
25
|
MenuFilterChip,
|
|
26
26
|
MenuIndexEntry,
|
|
27
27
|
MenuIntentFigure,
|
|
28
|
-
|
|
28
|
+
MenuFamilyTile,
|
|
29
29
|
MenuItemPreview,
|
|
30
30
|
MenuKitchenPreview,
|
|
31
31
|
MenuLoosenOption,
|
|
@@ -372,18 +372,41 @@ export const mockDrinkFamily: MenuFamilyPreview = {
|
|
|
372
372
|
venue_photo_urls: [],
|
|
373
373
|
};
|
|
374
374
|
|
|
375
|
-
const
|
|
375
|
+
const familyGradient = (hue: number) =>
|
|
376
376
|
`linear-gradient(150deg, oklch(0.52 0.14 ${hue}), oklch(0.43 0.13 ${hue - 4}))`;
|
|
377
377
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
378
|
+
/** A venue photograph stand-in: a tinted room, not a plate. */
|
|
379
|
+
const familyPhoto = (hue: number) =>
|
|
380
|
+
"data:image/svg+xml;utf8," +
|
|
381
|
+
encodeURIComponent(
|
|
382
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="320" height="320"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${hue} 28% 60%)"/><stop offset="1" stop-color="hsl(${hue} 22% 26%)"/></linearGradient></defs><rect width="320" height="320" fill="url(#g)"/><circle cx="100" cy="110" r="56" fill="hsl(${hue} 40% 82%)" opacity=".55"/><rect x="170" y="190" width="120" height="90" fill="hsl(${hue} 15% 18%)" opacity=".7"/></svg>`
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* The hub's seven cuisines, in the registry's order. The photograph is a
|
|
387
|
+
* venue's, never a plate's; the last one ships without, so the story shows
|
|
388
|
+
* the initial fallback beside six real tiles.
|
|
389
|
+
*/
|
|
390
|
+
export const mockFamilyTiles: MenuFamilyTile[] = [
|
|
391
|
+
{ slug: "italian", label: "Italian", countLabel: "66", href: "/menu/cuisines/italian", gradient: familyGradient(38), photoUrl: familyPhoto(30) },
|
|
392
|
+
{ slug: "romanian", label: "Romanian", countLabel: "64", href: "/menu/cuisines/romanian", gradient: familyGradient(25), photoUrl: familyPhoto(10) },
|
|
393
|
+
{ slug: "breakfast-brunch", label: "Breakfast & brunch", countLabel: "64", href: "/menu/cuisines/breakfast-brunch", gradient: familyGradient(82), photoUrl: familyPhoto(45) },
|
|
394
|
+
{ slug: "vegetarian", label: "Vegetarian", countLabel: "64", href: "/menu/cuisines/vegetarian", gradient: familyGradient(148), photoUrl: familyPhoto(120) },
|
|
395
|
+
{ slug: "pizza", label: "Pizza", countLabel: "63", href: "/menu/cuisines/pizza", gradient: familyGradient(58), photoUrl: familyPhoto(20) },
|
|
396
|
+
{ slug: "vegan", label: "Vegan", countLabel: "38", href: "/menu/cuisines/vegan", gradient: familyGradient(135), photoUrl: familyPhoto(100) },
|
|
397
|
+
{ slug: "bakery", label: "Bakeries & patisseries", countLabel: "65", href: "/menu/cuisines/bakery", gradient: familyGradient(72) },
|
|
385
398
|
];
|
|
386
399
|
|
|
400
|
+
/** The closing tile: stone, so it reads as the index and not as an eighth cuisine. */
|
|
401
|
+
export const mockFamilyTilesAll: MenuFamilyTile = {
|
|
402
|
+
slug: "all",
|
|
403
|
+
label: "All 38 cuisines",
|
|
404
|
+
countLabel: "The full index",
|
|
405
|
+
href: "/menu/cuisines",
|
|
406
|
+
gradient: "linear-gradient(150deg, oklch(0.50 0.03 70), oklch(0.40 0.028 64))",
|
|
407
|
+
photoUrl: familyPhoto(60),
|
|
408
|
+
};
|
|
409
|
+
|
|
387
410
|
/** Formatters the stories pass. Real callers inject locale-aware ones. */
|
|
388
411
|
export const mockFormatters = {
|
|
389
412
|
price: (value: number) => `${value} lei`,
|
|
@@ -134,6 +134,20 @@ export type MenuConceptRow = {
|
|
|
134
134
|
featured?: boolean
|
|
135
135
|
/** Portion, already formatted with its unit, e.g. "260g". */
|
|
136
136
|
portion_label?: string
|
|
137
|
+
/**
|
|
138
|
+
* What this room WON — "CHEAPEST", "MOST NAMED", "HIGHEST GRADE",
|
|
139
|
+
* "DEAREST" — already localised. A card whose rooms are four winners rather
|
|
140
|
+
* than four ranks sets one per room; without it the card falls back to
|
|
141
|
+
* "cheapest, then #2, #3, #4".
|
|
142
|
+
*/
|
|
143
|
+
badge?: string
|
|
144
|
+
/** Which figure the badge is about; that figure is emphasised. */
|
|
145
|
+
lead?: 'price' | 'mentions' | 'grade'
|
|
146
|
+
/**
|
|
147
|
+
* The venue's BearMenu grade, pre-formatted ("9.2"). Takes the star's
|
|
148
|
+
* place beside the kitchen's name; `rating` (Google's) shows only without it.
|
|
149
|
+
*/
|
|
150
|
+
grade?: string
|
|
137
151
|
}
|
|
138
152
|
|
|
139
153
|
/**
|
|
@@ -203,17 +217,21 @@ export type MenuVenueSignature = {
|
|
|
203
217
|
}
|
|
204
218
|
|
|
205
219
|
/**
|
|
206
|
-
*
|
|
220
|
+
* A family tile — a cuisine on the dishes tab, a going-out family on the
|
|
221
|
+
* drinks tab — as the hub's doorway grid draws it: a hue, a name, a venue
|
|
222
|
+
* count and one venue's photograph.
|
|
207
223
|
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* rather than linking nowhere.
|
|
224
|
+
* `href` is required: a tile that cannot resolve to a real destination is not
|
|
225
|
+
* rendered rather than linking nowhere.
|
|
211
226
|
*/
|
|
212
|
-
export type
|
|
227
|
+
export type MenuFamilyTile = {
|
|
213
228
|
slug: string
|
|
214
229
|
label: string
|
|
215
|
-
/**
|
|
216
|
-
|
|
230
|
+
/**
|
|
231
|
+
* The venue count, already formatted by the caller — "64", "1,204" — because
|
|
232
|
+
* thousands separators are locale-dependent and the DS owns no locale.
|
|
233
|
+
*/
|
|
234
|
+
countLabel?: string
|
|
217
235
|
href: string
|
|
218
236
|
/**
|
|
219
237
|
* Finished CSS gradient for the tile's ground, e.g.
|
|
@@ -226,8 +244,8 @@ export type MenuIntentTile = {
|
|
|
226
244
|
* — visible and obviously unstyled, rather than white text on white.
|
|
227
245
|
*/
|
|
228
246
|
gradient?: string
|
|
229
|
-
/**
|
|
230
|
-
|
|
247
|
+
/** Pre-resolved VENUE photograph. Venue photography is real; item photography is not. */
|
|
248
|
+
photoUrl?: string
|
|
231
249
|
}
|
|
232
250
|
|
|
233
251
|
/**
|
|
@@ -351,6 +369,11 @@ export type MenuIndexEntry = {
|
|
|
351
369
|
slug: string
|
|
352
370
|
name: string
|
|
353
371
|
count: number
|
|
372
|
+
/**
|
|
373
|
+
* `count`, already formatted ("1,204"). The list is a client component and a
|
|
374
|
+
* formatter cannot cross into it from a server caller; a string can.
|
|
375
|
+
*/
|
|
376
|
+
countLabel?: string
|
|
354
377
|
href: string
|
|
355
378
|
/** Finished CSS gradient for the mark. Falls back to the border colour. */
|
|
356
379
|
gradient?: string
|
|
@@ -14,7 +14,7 @@ import { cn } from "../lib/utils";
|
|
|
14
14
|
*
|
|
15
15
|
* It owns the photographs and their colour: the ground, the tiles, the fallback
|
|
16
16
|
* initial, the family's hue and an optional uniform dark overlay. It does NOT
|
|
17
|
-
* own the
|
|
17
|
+
* own the ink — the scrims and bands that carry a card's type run in
|
|
18
18
|
* a different direction on every card (down the family block's phone, across
|
|
19
19
|
* its desktop, in from both ends of the concept card) and are the card's own.
|
|
20
20
|
*
|
|
@@ -62,6 +62,12 @@ export type PhotoGroundProps = {
|
|
|
62
62
|
overlayClassName?: string;
|
|
63
63
|
/** How eagerly the first photograph loads. `stack` grounds are above the fold. */
|
|
64
64
|
loading?: "lazy" | "eager";
|
|
65
|
+
/**
|
|
66
|
+
* Classes on the fallback initial — its size, when the ground is smaller
|
|
67
|
+
* than a card. The 64px default fills a card cell; a tile's 88px polaroid
|
|
68
|
+
* passes `text-[44px]`.
|
|
69
|
+
*/
|
|
70
|
+
glyphClassName?: string;
|
|
65
71
|
className?: string;
|
|
66
72
|
};
|
|
67
73
|
|
|
@@ -76,6 +82,7 @@ export function PhotoGround({
|
|
|
76
82
|
overlay,
|
|
77
83
|
overlayClassName,
|
|
78
84
|
loading = "lazy",
|
|
85
|
+
glyphClassName,
|
|
79
86
|
className,
|
|
80
87
|
}: PhotoGroundProps) {
|
|
81
88
|
const initialOf = (index: number) =>
|
|
@@ -100,7 +107,12 @@ export function PhotoGround({
|
|
|
100
107
|
photograph is not a broken image, and drawing one as a placeholder
|
|
101
108
|
makes a real city look unfinished. */
|
|
102
109
|
<span className="absolute inset-0 flex items-center justify-center">
|
|
103
|
-
<span
|
|
110
|
+
<span
|
|
111
|
+
className={cn(
|
|
112
|
+
"font-serif text-[64px] font-semibold leading-none text-white/15",
|
|
113
|
+
glyphClassName
|
|
114
|
+
)}
|
|
115
|
+
>
|
|
104
116
|
{initialOf(index)}
|
|
105
117
|
</span>
|
|
106
118
|
</span>
|
|
@@ -19,8 +19,8 @@ import { cn } from "../lib/utils";
|
|
|
19
19
|
*
|
|
20
20
|
* `surface="image"` is for card covers, where the chip sits over the photo
|
|
21
21
|
* rather than in the caption block. The accent-on-canvas palette has no
|
|
22
|
-
* contrast guarantee over an arbitrary photo, so it swaps to the same
|
|
23
|
-
* fill the place badge uses there and keeps the dashed edge — which is what
|
|
22
|
+
* contrast guarantee over an arbitrary photo, so it swaps to the same solid
|
|
23
|
+
* ink fill the place badge uses there and keeps the dashed edge — which is what
|
|
24
24
|
* says "there is evidence behind this" — in the inverted colour.
|
|
25
25
|
*
|
|
26
26
|
* The label is passed in: the DS owns no translation keys.
|
|
@@ -64,7 +64,7 @@ export function SignatureChip({
|
|
|
64
64
|
shared,
|
|
65
65
|
"inline-flex items-center rounded-full border border-dashed",
|
|
66
66
|
onImage
|
|
67
|
-
? "border-background/70 bg-foreground/
|
|
67
|
+
? "border-background/70 bg-foreground/80 text-background hover:bg-foreground/90"
|
|
68
68
|
: "border-accent/40 text-accent hover:bg-accent/10",
|
|
69
69
|
"focus-visible:outline-none focus-visible:ring-2",
|
|
70
70
|
onImage ? "focus-visible:ring-background/70" : "focus-visible:ring-accent/40",
|
|
@@ -74,12 +74,19 @@ export function VenueGradeDimensions({
|
|
|
74
74
|
<dd className="text-[15px] font-extrabold tracking-[-0.02em] tabular-nums">
|
|
75
75
|
{formatScore(value)}
|
|
76
76
|
</dd>
|
|
77
|
+
{/* The track is `currentColor` at low alpha rather than `bg-muted`:
|
|
78
|
+
on the dossier's ink `--muted` is within a hair of the ground
|
|
79
|
+
and the empty bar vanished. */}
|
|
77
80
|
<div
|
|
78
81
|
aria-hidden
|
|
79
|
-
className="relative block h-[3px] overflow-hidden rounded-full bg-
|
|
82
|
+
className="relative block h-[3px] overflow-hidden rounded-full bg-current/15"
|
|
80
83
|
>
|
|
84
|
+
{/* `bg-[image:…]`, not `bg-[…]`: `--gradient-primary` is a
|
|
85
|
+
linear-gradient, and as a background-COLOR it is an invalid
|
|
86
|
+
declaration the browser drops — the fill rendered at the right
|
|
87
|
+
width with no paint at all. */}
|
|
81
88
|
<span
|
|
82
|
-
className="absolute inset-y-0 left-0 rounded-full bg-[var(--gradient-primary
|
|
89
|
+
className="absolute inset-y-0 left-0 rounded-full bg-current bg-[image:var(--gradient-primary)]"
|
|
83
90
|
style={{ width: `${Math.min(100, Math.max(0, value))}%` }}
|
|
84
91
|
/>
|
|
85
92
|
</div>
|
|
@@ -19,7 +19,7 @@ const meta: Meta<typeof VenueQuotes> = {
|
|
|
19
19
|
docs: {
|
|
20
20
|
description: {
|
|
21
21
|
component:
|
|
22
|
-
"The digest's standout lines as pull-quotes — one to three short positives, already resolved into the request locale. They are the only place on a hub where a reviewer speaks in their own register, so they are set in serif
|
|
22
|
+
"The digest's standout lines as pull-quotes — one to three short positives, already resolved into the request locale. They are the only place on a hub where a reviewer speaks in their own register, so they are set in quoted serif against the sans everything else uses. `onCite` turns a quote into a button opening the reviews behind it: `review_ids` is what makes the claim checkable, and a quote nobody can trace is just copy.",
|
|
23
23
|
},
|
|
24
24
|
},
|
|
25
25
|
},
|
|
@@ -6,7 +6,7 @@ import type { CitedText } from "./place-types";
|
|
|
6
6
|
*
|
|
7
7
|
* These are `reviews_digest.highlights`: one to three short positives already
|
|
8
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
|
|
9
|
+
* reviewer speaks in their own register, so they are set in quoted serif
|
|
10
10
|
* against the sans everything else uses.
|
|
11
11
|
*
|
|
12
12
|
* `onCite` turns a quote into a button that opens the reviews behind it —
|
|
@@ -39,8 +39,10 @@ export function VenueQuotes({ quotes, onCite, citeLabel, limit, className }: Ven
|
|
|
39
39
|
const citations = quote.review_ids?.length ?? 0;
|
|
40
40
|
const canCite = !!onCite && citations > 0;
|
|
41
41
|
|
|
42
|
+
// Upright, quoted: the italic face was the only reason the consumer's
|
|
43
|
+
// menu page loaded 281 KB of Fraunces italic at navigation time.
|
|
42
44
|
const text = (
|
|
43
|
-
<blockquote className="border-l-2 border-border pl-3 font-serif text-[13px]
|
|
45
|
+
<blockquote className="border-l-2 border-border pl-3 font-serif text-[13px] leading-[1.45] text-muted-foreground">
|
|
44
46
|
{`“${quote.text}”`}
|
|
45
47
|
</blockquote>
|
|
46
48
|
);
|
|
@@ -42,6 +42,9 @@ export type VenueWallProps = {
|
|
|
42
42
|
linkComponent?: MenuLinkComponent;
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
const WALL_CELLS = 18;
|
|
46
|
+
const PHONE_CELLS = 6;
|
|
47
|
+
|
|
45
48
|
export function VenueWall({
|
|
46
49
|
places,
|
|
47
50
|
hrefs,
|
|
@@ -64,8 +67,10 @@ export function VenueWall({
|
|
|
64
67
|
>
|
|
65
68
|
{tileUrls?.length ? (
|
|
66
69
|
<>
|
|
67
|
-
<div aria-hidden className="absolute inset-0 grid auto-rows-fr grid-cols-
|
|
68
|
-
{
|
|
70
|
+
<div aria-hidden className="absolute inset-0 grid auto-rows-fr grid-cols-3 gap-0.5 lg:grid-cols-6">
|
|
71
|
+
{/* Six cells on the phone, eighteen from `lg` — the hero's rule:
|
|
72
|
+
`hidden` keeps the lazy stills past the sixth from loading. */}
|
|
73
|
+
{Array.from({ length: WALL_CELLS }, (_, cell) => (
|
|
69
74
|
// eslint-disable-next-line @next/next/no-img-element
|
|
70
75
|
<img
|
|
71
76
|
key={cell}
|
|
@@ -73,7 +78,7 @@ export function VenueWall({
|
|
|
73
78
|
alt=""
|
|
74
79
|
loading="lazy"
|
|
75
80
|
decoding="async"
|
|
76
|
-
className="h-full w-full object-cover"
|
|
81
|
+
className={cn("h-full w-full object-cover", cell >= PHONE_CELLS && "hidden lg:block")}
|
|
77
82
|
/>
|
|
78
83
|
))}
|
|
79
84
|
</div>
|
|
@@ -151,7 +156,7 @@ export function VenueWall({
|
|
|
151
156
|
);
|
|
152
157
|
|
|
153
158
|
const chipClasses =
|
|
154
|
-
"inline-flex max-w-[220px] items-center gap-2 rounded-full bg-white/
|
|
159
|
+
"inline-flex max-w-[220px] items-center gap-2 rounded-full bg-white/[0.14] py-1 pl-1 pr-3.5";
|
|
155
160
|
|
|
156
161
|
return (
|
|
157
162
|
<li key={place.slug}>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import { Calendar,
|
|
2
|
+
import { Calendar, ChevronRight } from "lucide-react";
|
|
3
3
|
import { Button } from "./button";
|
|
4
4
|
import { WeekdayThemeBanner, type WeekdayThemeDay } from "./weekday-theme-banner";
|
|
5
5
|
|
|
@@ -56,9 +56,9 @@ export const Default: Story = {
|
|
|
56
56
|
body: "186 of them across 11 kitchens, from 32 lei. Two do a vegetarian version and four keep the grill on past midnight.",
|
|
57
57
|
weekLabel: "THE WEEK",
|
|
58
58
|
action: (
|
|
59
|
-
<Button size="
|
|
60
|
-
<PanelRightOpen className="h-3.5 w-3.5" />
|
|
59
|
+
<Button size="pill" variant="ink">
|
|
61
60
|
See today's picks
|
|
61
|
+
<ChevronRight aria-hidden />
|
|
62
62
|
</Button>
|
|
63
63
|
),
|
|
64
64
|
actionNote: "Bruno picks nine, ranked on review mentions",
|
|
@@ -66,6 +66,40 @@ export const Default: Story = {
|
|
|
66
66
|
},
|
|
67
67
|
};
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The phone: no labelled button — the banner is the target and the chevron
|
|
71
|
+
* on the right is the signal, so the banner stays as tall as its title.
|
|
72
|
+
*/
|
|
73
|
+
export const CompactAction: Story = {
|
|
74
|
+
args: {
|
|
75
|
+
days: days.map((day) => ({ ...day, name: `${day.name} in Cluj-Napoca`, shortHeadline: day.name })),
|
|
76
|
+
activeDay: 3,
|
|
77
|
+
imageUrl:
|
|
78
|
+
"data:image/svg+xml;utf8," +
|
|
79
|
+
encodeURIComponent(
|
|
80
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="630"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(30 30% 40%)"/><stop offset="1" stop-color="hsl(20 40% 20%)"/></linearGradient></defs><rect width="1440" height="630" fill="url(#g)"/><circle cx="1000" cy="330" r="220" fill="hsl(40 60% 70%)"/><circle cx="1000" cy="330" r="150" fill="hsl(25 70% 45%)"/></svg>'
|
|
81
|
+
),
|
|
82
|
+
kicker: "TODAY'S THEME",
|
|
83
|
+
kickerIcon: <Calendar className="h-3 w-3" />,
|
|
84
|
+
weekLabel: "THE WEEK",
|
|
85
|
+
action: (
|
|
86
|
+
<Button size="pill" variant="ink">
|
|
87
|
+
See today's picks
|
|
88
|
+
<ChevronRight aria-hidden />
|
|
89
|
+
</Button>
|
|
90
|
+
),
|
|
91
|
+
compactAction: { label: "See today's picks", onClick: () => {} },
|
|
92
|
+
},
|
|
93
|
+
parameters: { viewport: { defaultViewport: "mobile1" } },
|
|
94
|
+
decorators: [
|
|
95
|
+
(Story) => (
|
|
96
|
+
<div className="w-[390px] bg-background p-3">
|
|
97
|
+
<Story />
|
|
98
|
+
</div>
|
|
99
|
+
),
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
|
|
69
103
|
/** The drinks tab, same component. */
|
|
70
104
|
export const DrinksTab: Story = {
|
|
71
105
|
args: {
|
|
@@ -101,7 +135,12 @@ export const Mobile: Story = {
|
|
|
101
135
|
kickerIcon: <Calendar className="h-3 w-3" />,
|
|
102
136
|
body: "186 of them across 11 kitchens, from 32 lei.",
|
|
103
137
|
weekLabel: "THE WEEK",
|
|
104
|
-
action:
|
|
138
|
+
action: (
|
|
139
|
+
<Button size="pill" variant="ink">
|
|
140
|
+
See today's picks
|
|
141
|
+
<ChevronRight aria-hidden />
|
|
142
|
+
</Button>
|
|
143
|
+
),
|
|
105
144
|
},
|
|
106
145
|
decorators: [
|
|
107
146
|
(Story) => (
|