@bearmenu/ui 0.8.11 → 0.8.13
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 → chunk-3NMG3KNA.js} +1 -1
- package/dist/{chunk-5RWTOT5Y.js → chunk-CNJQUHCU.js} +11 -1
- package/dist/{chunk-DTXNDLSE.js → chunk-FKNBZF5T.js} +15 -0
- package/dist/{chunk-H3ZZDX5L.js → chunk-P7XTW2CG.js} +2 -2
- package/dist/{chunk-JS3EINZD.js → chunk-VAXOQS7W.js} +12 -0
- package/dist/{chunk-AKQ57NXY.js → chunk-ZQ53AGML.js} +2 -2
- package/dist/components/alert-dialog.js +1 -1
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/avatar.d.ts +3 -3
- package/dist/components/badge.d.ts +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/card.d.ts +2 -2
- package/dist/components/carousel.js +1 -1
- 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.d.ts +1 -1
- package/dist/components/concept-compare.js +29 -8
- package/dist/components/concept-convergence.js +1 -1
- package/dist/components/dialog.d.ts +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/haptic-button.js +1 -1
- package/dist/components/input-group.d.ts +4 -4
- package/dist/components/input.d.ts +2 -2
- package/dist/components/item.d.ts +17 -7
- package/dist/components/item.js +15 -10
- package/dist/components/kbd.d.ts +2 -2
- package/dist/components/kitchen-dossier.d.ts +2 -0
- package/dist/components/kitchen-dossier.js +25 -18
- package/dist/components/link-button.js +1 -1
- package/dist/components/menu-family-block.d.ts +15 -9
- package/dist/components/menu-family-block.js +63 -60
- package/dist/components/menu-family-card.js +1 -1
- 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 +8 -6
- 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-kitchen-list.js +1 -1
- 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 +1 -1
- package/dist/components/native-select.d.ts +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/searchable-select.js +1 -1
- package/dist/components/select.d.ts +1 -1
- package/dist/components/sliding-panels.js +1 -1
- package/dist/components/snap-rail.d.ts +2 -7
- package/dist/components/snap-rail.js +1 -1
- package/dist/components/spinner.d.ts +4 -4
- package/dist/components/tag.d.ts +4 -4
- package/dist/components/text.d.ts +5 -5
- package/dist/components/toggle.d.ts +1 -1
- package/dist/components/venue-grade-dimensions.js +1 -1
- package/dist/components/weekday-theme-banner.d.ts +24 -3
- package/dist/components/weekday-theme-banner.js +62 -14
- package/package.json +1 -1
- package/src/components/button.stories.tsx +25 -2
- package/src/components/button.tsx +12 -0
- package/src/components/colour-dictionary.stories.tsx +1 -1
- package/src/components/concept-compare.tsx +40 -9
- package/src/components/item.tsx +69 -45
- package/src/components/kitchen-dossier.tsx +45 -24
- package/src/components/menu-family-block.tsx +86 -79
- 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.tsx +13 -7
- 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-kitchen-list.tsx +4 -1
- package/src/components/menu-mocks.ts +32 -9
- package/src/components/menu-types.ts +32 -9
- package/src/components/photo-ground.tsx +13 -1
- package/src/components/snap-rail.tsx +19 -0
- package/src/components/venue-grade-dimensions.tsx +9 -2
- package/src/components/weekday-theme-banner.stories.tsx +43 -4
- package/src/components/weekday-theme-banner.tsx +94 -17
- 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,
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
+
* thirty-eight cuisines, in the markup at every width. That makes it the
|
|
18
|
+
* page's entire link-equity job, which in turn means the rich cards above
|
|
19
|
+
* are free to be a swipe rail on a phone: nothing a crawler needs waits on a
|
|
20
|
+
* swipe, 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 —
|
|
19
|
-
*
|
|
20
|
-
*
|
|
27
|
+
* Get this backwards — mount the rows behind the expander on tap — and the
|
|
28
|
+
* page silently stops linking to twenty-six pages it is supposed to be the
|
|
29
|
+
* 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.
|
|
@@ -150,7 +150,10 @@ function KitchenRow({
|
|
|
150
150
|
</span>
|
|
151
151
|
|
|
152
152
|
<div className="flex min-w-0 flex-1 flex-col gap-0.5 lg:w-[226px] lg:flex-none">
|
|
153
|
-
|
|
153
|
+
{/* One line with an ellipsis on a phone, where the row is the whole
|
|
154
|
+
width; two lines from `lg`, where the column is 226px and
|
|
155
|
+
"ADRIATICA GELATERIA ITALIANA" would otherwise lose its half. */}
|
|
156
|
+
<span className="truncate text-[14.5px] font-bold tracking-[-0.01em] lg:line-clamp-2 lg:whitespace-normal lg:text-[15.5px] lg:leading-tight">
|
|
154
157
|
{place.name}
|
|
155
158
|
</span>
|
|
156
159
|
<span className="flex items-center gap-1 truncate text-xs text-muted-foreground">
|
|
@@ -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
|
|
@@ -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>
|
|
@@ -166,6 +166,14 @@ SnapRail.displayName = "SnapRail";
|
|
|
166
166
|
* `onActiveChange` and wants the dots somewhere other than directly under it —
|
|
167
167
|
* inside a foot band, say. Decorative, as on the rail: the rail is the control.
|
|
168
168
|
*/
|
|
169
|
+
/**
|
|
170
|
+
* Past this many pages the dots stop being a picture of the rail — twelve
|
|
171
|
+
* 6px dots with their gaps already run 138px, and a cuisines index has
|
|
172
|
+
* thirty-six — so the row becomes a counter, "7 / 36", which says the same
|
|
173
|
+
* two things (where, and how far) at any length.
|
|
174
|
+
*/
|
|
175
|
+
const MAX_DOTS = 12;
|
|
176
|
+
|
|
169
177
|
export function SnapRailDots({
|
|
170
178
|
count,
|
|
171
179
|
active,
|
|
@@ -175,6 +183,17 @@ export function SnapRailDots({
|
|
|
175
183
|
active: number;
|
|
176
184
|
className?: string;
|
|
177
185
|
}) {
|
|
186
|
+
if (count > MAX_DOTS) {
|
|
187
|
+
return (
|
|
188
|
+
<div
|
|
189
|
+
aria-hidden
|
|
190
|
+
className={cn("flex justify-center text-[11px] tabular-nums text-muted-foreground", className)}
|
|
191
|
+
>
|
|
192
|
+
{active + 1} / {count}
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
178
197
|
return (
|
|
179
198
|
<div aria-hidden className={cn("flex justify-center gap-1.5", className)}>
|
|
180
199
|
{Array.from({ length: count }, (_, index) => (
|
|
@@ -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>
|
|
@@ -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) => (
|