@bearmenu/ui 0.8.8 → 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-62ES6N32.js +27 -0
- package/dist/chunk-AXV5O2IZ.js +58 -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/alert.d.ts +2 -2
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -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/event-card.js +1 -1
- package/dist/components/event-compact-row.d.ts +1 -1
- package/dist/components/event-compact-row.js +1 -1
- package/dist/components/event-detail-panel.d.ts +171 -25
- package/dist/components/event-detail-panel.js +471 -174
- package/dist/components/event-list-row-wide.js +1 -1
- package/dist/components/event-list-row.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/ranked-venue-list.d.ts +49 -0
- package/dist/components/ranked-venue-list.js +87 -0
- 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 +24 -4
- package/dist/components/snap-rail.js +66 -8
- 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/tag.d.ts +2 -2
- package/dist/components/text.d.ts +3 -3
- 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/package.json +1 -1
- 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/event-compact-row.tsx +1 -1
- package/src/components/event-detail-panel.stories.tsx +205 -97
- package/src/components/event-detail-panel.test.tsx +181 -0
- package/src/components/event-detail-panel.tsx +703 -185
- package/src/components/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/ranked-venue-list.stories.tsx +80 -0
- package/src/components/ranked-venue-list.tsx +177 -0
- 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.tsx +90 -9
- package/src/components/stat-banner.stories.tsx +100 -0
- package/src/components/stat-banner.tsx +117 -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
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixtures for the menu-hub surfaces — used by their stories, and shipped so
|
|
3
|
+
* that consumers rendering these components outside an app (post-generator's
|
|
4
|
+
* Remotion frames, admin previews) do not each rebuild a plausible view model.
|
|
5
|
+
*
|
|
6
|
+
* ## Every venue here is invented
|
|
7
|
+
*
|
|
8
|
+
* The names, ratings and quotes are fabricated. Real venues must never appear
|
|
9
|
+
* in marketing or promotional renders — a real kitchen's rating in a promo
|
|
10
|
+
* frame is a claim about that business, made without its involvement, and it
|
|
11
|
+
* goes stale the moment the data moves. Anything filmed from these fixtures is
|
|
12
|
+
* safe to publish; anything filmed from live data is not.
|
|
13
|
+
*
|
|
14
|
+
* The SHAPES are real, though: the gaps are the gaps the API actually has, so
|
|
15
|
+
* a layout that survives these fixtures survives production. `mentions` is
|
|
16
|
+
* populated everywhere because it is populated everywhere upstream; one venue
|
|
17
|
+
* has no `must_try`, one has no photograph, and one grade is missing the
|
|
18
|
+
* dimensions a coffee bar never gets.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
MenuConceptPreview,
|
|
23
|
+
MenuFamilyPreview,
|
|
24
|
+
MenuIntentTile,
|
|
25
|
+
MenuItemPreview,
|
|
26
|
+
MenuVenuePreview,
|
|
27
|
+
} from "./menu-types";
|
|
28
|
+
import type { PlacePreview } from "./place-types";
|
|
29
|
+
|
|
30
|
+
export const mockGradients = {
|
|
31
|
+
// The forest dictionary (docs/colour-dictionary.md, Foundations/Colour dictionary):
|
|
32
|
+
// wheat, ember, pine, maple, lake water. Post-generator films these, so they
|
|
33
|
+
// must be the same values the app paints.
|
|
34
|
+
pasta: "linear-gradient(150deg, oklch(0.555 0.105 80), oklch(0.455 0.10 70))",
|
|
35
|
+
meat: "linear-gradient(150deg, oklch(0.49 0.13 28), oklch(0.41 0.12 22))",
|
|
36
|
+
greens: "linear-gradient(150deg, oklch(0.505 0.115 148), oklch(0.415 0.11 142))",
|
|
37
|
+
sweet: "linear-gradient(150deg, oklch(0.505 0.14 50), oklch(0.405 0.13 42))",
|
|
38
|
+
cocktail: "linear-gradient(150deg, oklch(0.485 0.095 205), oklch(0.405 0.09 212))",
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
export const mockPlaces: PlacePreview[] = [
|
|
42
|
+
{
|
|
43
|
+
id: "p1",
|
|
44
|
+
slug: "the-copper-yard",
|
|
45
|
+
name: "The Copper Yard",
|
|
46
|
+
open_now: true,
|
|
47
|
+
rating: { rating: 4.6, count: 318 },
|
|
48
|
+
hours: [{ opens_at: 12, closes_at: 23 }],
|
|
49
|
+
price_dishes: { price_range_low: 19, price_range_high: 62, median_price: 28 },
|
|
50
|
+
cuisines: ["italian", "pizza"],
|
|
51
|
+
type: "pub",
|
|
52
|
+
busyness: [
|
|
53
|
+
{ hour: 12, score: 22 },
|
|
54
|
+
{ hour: 13, score: 26 },
|
|
55
|
+
{ hour: 14, score: 30 },
|
|
56
|
+
{ hour: 15, score: 38 },
|
|
57
|
+
{ hour: 16, score: 52 },
|
|
58
|
+
{ hour: 17, score: 74 },
|
|
59
|
+
{ hour: 18, score: 92 },
|
|
60
|
+
{ hour: 19, score: 100 },
|
|
61
|
+
{ hour: 20, score: 86 },
|
|
62
|
+
{ hour: 21, score: 64 },
|
|
63
|
+
],
|
|
64
|
+
reviews_digest: {
|
|
65
|
+
review_count: 318,
|
|
66
|
+
grade: {
|
|
67
|
+
overall: 8.4,
|
|
68
|
+
show_number: true,
|
|
69
|
+
descriptor: "Foarte bun",
|
|
70
|
+
confidence: "high",
|
|
71
|
+
dimensions: {
|
|
72
|
+
food: 89,
|
|
73
|
+
service: 82,
|
|
74
|
+
value: 86,
|
|
75
|
+
ambiance: 78,
|
|
76
|
+
portion: 91,
|
|
77
|
+
consistency: 84,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
value: { value: "fair" },
|
|
81
|
+
tags: [{ value: "casual" }, { value: "cozy" }, { value: "family_friendly" }],
|
|
82
|
+
highlights: [
|
|
83
|
+
{
|
|
84
|
+
text: "The wings are the reason to sit at the bar — hot honey, and they keep going until the kitchen shuts.",
|
|
85
|
+
review_ids: ["r1", "r2", "r3"],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
text: "Last kitchen open on this street. We came at half eleven and still ate properly.",
|
|
89
|
+
review_ids: ["r4", "r5"],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
must_try: [
|
|
93
|
+
{
|
|
94
|
+
name: "Chicken wings, 6 pc",
|
|
95
|
+
why: "Buttermilk brine and hot honey, fried to order until the kitchen shuts.",
|
|
96
|
+
strength: "signature",
|
|
97
|
+
slug: "chicken-wings",
|
|
98
|
+
mentions: 23,
|
|
99
|
+
sizes_and_prices: [{ size: 320, price: 32 }],
|
|
100
|
+
review_ids: ["r1", "r2", "r3"],
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "p2",
|
|
107
|
+
slug: "salt-and-stone",
|
|
108
|
+
name: "Salt & Stone",
|
|
109
|
+
open_now: true,
|
|
110
|
+
rating: { rating: 4.8, count: 204 },
|
|
111
|
+
hours: [{ opens_at: 11, closes_at: 22 }],
|
|
112
|
+
price_dishes: { price_range_low: 38, price_range_high: 84, median_price: 49 },
|
|
113
|
+
cuisines: ["italian"],
|
|
114
|
+
type: "restaurant",
|
|
115
|
+
reviews_digest: {
|
|
116
|
+
review_count: 204,
|
|
117
|
+
grade: {
|
|
118
|
+
overall: 8.9,
|
|
119
|
+
show_number: true,
|
|
120
|
+
dimensions: { food: 93, service: 88, value: 74, ambiance: 85 },
|
|
121
|
+
},
|
|
122
|
+
must_try: [
|
|
123
|
+
{
|
|
124
|
+
name: "Truffle Tagliatelle",
|
|
125
|
+
why: "Made in-house each morning with black truffle and parmesan.",
|
|
126
|
+
strength: "signature",
|
|
127
|
+
slug: "truffle-tagliatelle",
|
|
128
|
+
mentions: 17,
|
|
129
|
+
sizes_and_prices: [{ size: 320, price: 62 }],
|
|
130
|
+
review_ids: ["r6", "r7"],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "p3",
|
|
137
|
+
slug: "the-quiet-hours",
|
|
138
|
+
name: "The Quiet Hours",
|
|
139
|
+
open_now: true,
|
|
140
|
+
rating: { rating: 4.5, count: 96 },
|
|
141
|
+
hours: [{ opens_at: 8, closes_at: 20 }],
|
|
142
|
+
price_dishes: { price_range_low: 8, price_range_high: 26, median_price: 14 },
|
|
143
|
+
type: "coffee_shop",
|
|
144
|
+
reviews_digest: {
|
|
145
|
+
review_count: 96,
|
|
146
|
+
// A coffee bar is graded on coffee_quality and never on food — the
|
|
147
|
+
// dimension row must survive the absence rather than draw a zero.
|
|
148
|
+
grade: {
|
|
149
|
+
overall: 8.6,
|
|
150
|
+
show_number: true,
|
|
151
|
+
dimensions: { coffee_quality: 91, service: 86, value: 80, ambiance: 88 },
|
|
152
|
+
},
|
|
153
|
+
must_try: [
|
|
154
|
+
{
|
|
155
|
+
name: "Flat white",
|
|
156
|
+
strength: "frequent",
|
|
157
|
+
slug: "flat-white",
|
|
158
|
+
mentions: 28,
|
|
159
|
+
sizes_and_prices: [{ size: 220, price: 14 }],
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "p4",
|
|
166
|
+
slug: "green-room-canteen",
|
|
167
|
+
name: "Green Room Canteen",
|
|
168
|
+
open_now: false,
|
|
169
|
+
rating: { rating: 4.7, count: 141 },
|
|
170
|
+
price_dishes: { price_range_low: 22, price_range_high: 51, median_price: 31 },
|
|
171
|
+
type: "restaurant",
|
|
172
|
+
// No digest at all: a place the census has not reached. Every venue surface
|
|
173
|
+
// has to render this without collapsing.
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const placeBySlug = (slug: string) => mockPlaces.find((place) => place.slug === slug)!;
|
|
178
|
+
|
|
179
|
+
export const mockVenues: MenuVenuePreview[] = [
|
|
180
|
+
{
|
|
181
|
+
place: placeBySlug("the-copper-yard"),
|
|
182
|
+
href: "/places/the-copper-yard",
|
|
183
|
+
closes_at_label: "23:00",
|
|
184
|
+
signature: {
|
|
185
|
+
item: {
|
|
186
|
+
id: "s1",
|
|
187
|
+
slug: "chicken-wings",
|
|
188
|
+
name: "Chicken wings, 6 pc",
|
|
189
|
+
kind: "dish",
|
|
190
|
+
mentions: 23,
|
|
191
|
+
sizes_and_prices: [{ size: 320, price: 32 }],
|
|
192
|
+
},
|
|
193
|
+
why: "Buttermilk brine and hot honey, fried to order until the kitchen shuts.",
|
|
194
|
+
isSignature: true,
|
|
195
|
+
href: "/menu/dishes/chicken-wings",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
place: placeBySlug("salt-and-stone"),
|
|
200
|
+
href: "/places/salt-and-stone",
|
|
201
|
+
closes_at_label: "22:00",
|
|
202
|
+
signature: {
|
|
203
|
+
item: {
|
|
204
|
+
id: "s2",
|
|
205
|
+
slug: "truffle-tagliatelle",
|
|
206
|
+
name: "Truffle Tagliatelle",
|
|
207
|
+
kind: "dish",
|
|
208
|
+
mentions: 17,
|
|
209
|
+
sizes_and_prices: [{ size: 320, price: 62 }],
|
|
210
|
+
},
|
|
211
|
+
why: "Made in-house each morning with black truffle and parmesan.",
|
|
212
|
+
isSignature: true,
|
|
213
|
+
href: "/menu/dishes/truffle-tagliatelle",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
place: placeBySlug("the-quiet-hours"),
|
|
218
|
+
href: "/places/the-quiet-hours",
|
|
219
|
+
closes_at_label: "20:00",
|
|
220
|
+
signature: {
|
|
221
|
+
item: {
|
|
222
|
+
id: "s3",
|
|
223
|
+
slug: "flat-white",
|
|
224
|
+
name: "Flat white",
|
|
225
|
+
kind: "drink",
|
|
226
|
+
mentions: 28,
|
|
227
|
+
sizes_and_prices: [{ size: 220, price: 14 }],
|
|
228
|
+
},
|
|
229
|
+
href: "/menu/drinks/flat-white",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
place: placeBySlug("green-room-canteen"),
|
|
234
|
+
href: "/places/green-room-canteen",
|
|
235
|
+
// No signature: the row must still read as a venue.
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
export const mockItems: MenuItemPreview[] = [
|
|
240
|
+
{
|
|
241
|
+
id: "i1",
|
|
242
|
+
slug: "truffle-tagliatelle",
|
|
243
|
+
name: "Truffle Tagliatelle",
|
|
244
|
+
kind: "dish",
|
|
245
|
+
category_slug: "pasta",
|
|
246
|
+
mentions: 17,
|
|
247
|
+
sizes_and_prices: [{ size: 320, price: 62 }],
|
|
248
|
+
place: { slug: "salt-and-stone", name: "Salt & Stone" },
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: "i2",
|
|
252
|
+
slug: "chicken-wings",
|
|
253
|
+
name: "Chicken wings, 6 pc",
|
|
254
|
+
kind: "dish",
|
|
255
|
+
category_slug: "chicken-dish",
|
|
256
|
+
mentions: 23,
|
|
257
|
+
sizes_and_prices: [{ size: 320, price: 32 }],
|
|
258
|
+
place: { slug: "the-copper-yard", name: "The Copper Yard" },
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: "i3",
|
|
262
|
+
slug: "burrata-heirloom-tomato",
|
|
263
|
+
name: "Burrata & heirloom tomato",
|
|
264
|
+
kind: "dish",
|
|
265
|
+
category_slug: "salad",
|
|
266
|
+
mentions: 14,
|
|
267
|
+
sizes_and_prices: [{ size: 300, price: 49 }],
|
|
268
|
+
place: { slug: "green-room-canteen", name: "Green Room Canteen" },
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: "i4",
|
|
272
|
+
slug: "flat-white",
|
|
273
|
+
name: "Flat white",
|
|
274
|
+
kind: "drink",
|
|
275
|
+
category_slug: "non-alcoholic",
|
|
276
|
+
mentions: 28,
|
|
277
|
+
sizes_and_prices: [{ size: 220, price: 14 }],
|
|
278
|
+
place: { slug: "the-quiet-hours", name: "The Quiet Hours" },
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: "i5",
|
|
282
|
+
slug: "house-negroni",
|
|
283
|
+
name: "House negroni",
|
|
284
|
+
kind: "drink",
|
|
285
|
+
category_slug: "cocktail",
|
|
286
|
+
// Deliberately unmentioned: the badge must not render "0".
|
|
287
|
+
mentions: 0,
|
|
288
|
+
sizes_and_prices: [{ size: 150, price: 32 }],
|
|
289
|
+
place: { slug: "the-copper-yard", name: "The Copper Yard" },
|
|
290
|
+
},
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
export const mockConcept: MenuConceptPreview = {
|
|
294
|
+
slug: "margherita",
|
|
295
|
+
title: "Margherita",
|
|
296
|
+
kind: "dish",
|
|
297
|
+
venue_count: 87,
|
|
298
|
+
price_low: 10,
|
|
299
|
+
price_high: 120,
|
|
300
|
+
alias_count: 12,
|
|
301
|
+
mentions: 214,
|
|
302
|
+
rows: [
|
|
303
|
+
{
|
|
304
|
+
place_slug: "the-copper-yard",
|
|
305
|
+
place_name: "The Copper Yard",
|
|
306
|
+
alias: "Pizza Margherita",
|
|
307
|
+
price: 34,
|
|
308
|
+
mentions: 23,
|
|
309
|
+
rating: 4.6,
|
|
310
|
+
place_type: "pub",
|
|
311
|
+
featured: true,
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
place_slug: "salt-and-stone",
|
|
315
|
+
place_name: "Salt & Stone",
|
|
316
|
+
alias: "Margherita D.O.P.",
|
|
317
|
+
price: 46,
|
|
318
|
+
mentions: 17,
|
|
319
|
+
rating: 4.8,
|
|
320
|
+
place_type: "restaurant",
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
place_slug: "green-room-canteen",
|
|
324
|
+
place_name: "Green Room Canteen",
|
|
325
|
+
alias: "Marinara Bianca",
|
|
326
|
+
price: 52,
|
|
327
|
+
mentions: 9,
|
|
328
|
+
rating: 4.7,
|
|
329
|
+
place_type: "canteen",
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
export const mockFamily: MenuFamilyPreview = {
|
|
335
|
+
slug: "italian",
|
|
336
|
+
name: "Italian",
|
|
337
|
+
venue_count: 247,
|
|
338
|
+
chips: [
|
|
339
|
+
{ slug: "pizza", label: "Pizza", count: 120 },
|
|
340
|
+
{ slug: "pasta", label: "Pasta", count: 96 },
|
|
341
|
+
{ slug: "salad", label: "Salad", count: 88 },
|
|
342
|
+
{ slug: "chicken-dish", label: "Chicken", count: 77 },
|
|
343
|
+
],
|
|
344
|
+
price_low: 32,
|
|
345
|
+
price_high: 84,
|
|
346
|
+
price_median: 49,
|
|
347
|
+
venue_photo_urls: [],
|
|
348
|
+
signature: {
|
|
349
|
+
item: {
|
|
350
|
+
id: "f1",
|
|
351
|
+
slug: "truffle-tagliatelle",
|
|
352
|
+
name: "Truffle Tagliatelle",
|
|
353
|
+
kind: "dish",
|
|
354
|
+
mentions: 17,
|
|
355
|
+
sizes_and_prices: [{ size: 320, price: 62 }],
|
|
356
|
+
},
|
|
357
|
+
why: "Salt & Stone",
|
|
358
|
+
href: "/menu/dishes/truffle-tagliatelle",
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
/** The drinks counterpart, and the reason `chip.count` is optional. */
|
|
363
|
+
export const mockDrinkFamily: MenuFamilyPreview = {
|
|
364
|
+
slug: "cocktail",
|
|
365
|
+
name: "Cocktails",
|
|
366
|
+
venue_count: 21,
|
|
367
|
+
// No counts: there is no drink-group facet on places, so these ship as bare
|
|
368
|
+
// labels rather than with an invented number.
|
|
369
|
+
chips: [
|
|
370
|
+
{ slug: "spritz", label: "Spritz" },
|
|
371
|
+
{ slug: "negroni", label: "Negroni" },
|
|
372
|
+
{ slug: "sour", label: "Sour" },
|
|
373
|
+
],
|
|
374
|
+
price_low: 18,
|
|
375
|
+
price_high: 54,
|
|
376
|
+
price_median: 28,
|
|
377
|
+
venue_photo_urls: [],
|
|
378
|
+
signature: {
|
|
379
|
+
item: {
|
|
380
|
+
id: "f2",
|
|
381
|
+
slug: "house-negroni",
|
|
382
|
+
name: "House negroni",
|
|
383
|
+
kind: "drink",
|
|
384
|
+
mentions: 16,
|
|
385
|
+
sizes_and_prices: [{ size: 150, price: 32 }],
|
|
386
|
+
},
|
|
387
|
+
why: "The Copper Yard",
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const intentGradient = (hue: number) =>
|
|
392
|
+
`linear-gradient(150deg, oklch(0.52 0.14 ${hue}), oklch(0.43 0.13 ${hue - 4}))`;
|
|
393
|
+
|
|
394
|
+
export const mockIntentTiles: MenuIntentTile[] = [
|
|
395
|
+
{ slug: "italian", label: "Italian tonight", caption: "247 kitchens", href: "/top/italian?open=now", gradient: intentGradient(40) },
|
|
396
|
+
{ slug: "burgeri", label: "Burgers", caption: "229 kitchens", href: "/top/burgeri", gradient: intentGradient(25) },
|
|
397
|
+
{ slug: "restaurante", label: "Open right now", caption: "by the hour", href: "/top/restaurante?open=now", gradient: intentGradient(60) },
|
|
398
|
+
{ slug: "brunch", label: "Brunch", caption: "314 kitchens", href: "/top/brunch", gradient: intentGradient(75) },
|
|
399
|
+
{ slug: "vegan", label: "Vegan", caption: "108 kitchens", href: "/top/vegan", gradient: intentGradient(148) },
|
|
400
|
+
{ slug: "centru", label: "In Centru", caption: "by neighbourhood", href: "/top/centru", gradient: intentGradient(300) },
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
/** Formatters the stories pass. Real callers inject locale-aware ones. */
|
|
404
|
+
export const mockFormatters = {
|
|
405
|
+
price: (value: number) => `${value} lei`,
|
|
406
|
+
number: (value: number) => value.toLocaleString("en-US"),
|
|
407
|
+
score: (value: number) => (value / 10).toFixed(1),
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
export const mockHubLabels = {
|
|
411
|
+
mentions: (count: number) => `${count} mentions`,
|
|
412
|
+
venues: (count: number) => `${count} kitchens`,
|
|
413
|
+
seeAll: "See all",
|
|
414
|
+
signature: "SIGNATURE",
|
|
415
|
+
openNow: "Open now",
|
|
416
|
+
until: "until",
|
|
417
|
+
};
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural mirror of the openapi `Dish` / `Drink` schemas, decoupled from any
|
|
3
|
+
* one app's generated types. FE, admin and post-generator map their domain
|
|
4
|
+
* shapes into these before passing data to the menu-hub surfaces.
|
|
5
|
+
*
|
|
6
|
+
* Same contract as `place-types.ts` and `event-types.ts`: no app schemas, no
|
|
7
|
+
* app hooks, no i18n. Every user-facing string arrives through a `*Labels`
|
|
8
|
+
* object, every destination through an `href`, and every locale-sensitive
|
|
9
|
+
* format through an injected formatter — the DS never owns a translation key
|
|
10
|
+
* or an `Intl` locale.
|
|
11
|
+
*
|
|
12
|
+
* ## One set of components, two tabs
|
|
13
|
+
*
|
|
14
|
+
* Dishes and drinks are the same shape. `MenuItemPreview.kind` is the only
|
|
15
|
+
* discriminator, and nothing in this file branches on it — a drinks hub is a
|
|
16
|
+
* different view model and a different `*Labels` object, never a second set of
|
|
17
|
+
* components. Anything that would need a `if (kind === 'drink')` inside a
|
|
18
|
+
* component belongs in the caller's mapper instead.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ComponentType, CSSProperties, ReactNode } from 'react'
|
|
22
|
+
|
|
23
|
+
import type { MenuSizeAndPrice, PlacePreview } from './place-types'
|
|
24
|
+
|
|
25
|
+
export type { MenuSizeAndPrice } from './place-types'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The shape every menu surface's `linkComponent` must accept.
|
|
29
|
+
*
|
|
30
|
+
* One type rather than a clone per component: these surfaces nest each other —
|
|
31
|
+
* a family card renders a `<MenuItem>`, a spotlight renders a signature block —
|
|
32
|
+
* and a caller passes ONE `next/link` down through all of them. Subtly
|
|
33
|
+
* different clones made those hand-offs fail to typecheck for no design reason.
|
|
34
|
+
*/
|
|
35
|
+
export type MenuLinkProps = {
|
|
36
|
+
href: string
|
|
37
|
+
className?: string
|
|
38
|
+
'aria-label'?: string
|
|
39
|
+
'aria-current'?: 'page'
|
|
40
|
+
/** Intent tiles paint their hue on the link element itself. */
|
|
41
|
+
style?: CSSProperties
|
|
42
|
+
children?: ReactNode
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type MenuLinkComponent = ComponentType<MenuLinkProps>
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A dish or a drink as every hub surface consumes it.
|
|
49
|
+
*
|
|
50
|
+
* `mentions` is the census count of review mentions and is populated on every
|
|
51
|
+
* item in the catalogue — it is the ranking signal the hub is built on. Do not
|
|
52
|
+
* confuse it with `review_count`, which counts full review objects and is zero
|
|
53
|
+
* for most items; a surface that ranks on `review_count` renders an empty page.
|
|
54
|
+
*/
|
|
55
|
+
export type MenuItemPreview = {
|
|
56
|
+
id: string
|
|
57
|
+
slug: string
|
|
58
|
+
name: string
|
|
59
|
+
kind: 'dish' | 'drink'
|
|
60
|
+
description?: string
|
|
61
|
+
/** API category code (pizza, pasta, cocktail, non-alcoholic, …). Localised by the caller. */
|
|
62
|
+
category_slug?: string
|
|
63
|
+
sizes_and_prices?: MenuSizeAndPrice[]
|
|
64
|
+
/** Censused review mentions. The hub's ranking signal. */
|
|
65
|
+
mentions?: number
|
|
66
|
+
/** Full review objects citing this item. Sparse — not a ranking signal. */
|
|
67
|
+
review_count?: number
|
|
68
|
+
/** 0–100 recommendation score. */
|
|
69
|
+
score?: number
|
|
70
|
+
/** The venue serving it. Menu items are always venue-scoped. */
|
|
71
|
+
place?: MenuItemPlaceRef
|
|
72
|
+
/**
|
|
73
|
+
* Pre-resolved photo URL. Menu-item photography is AI-generated and hidden in
|
|
74
|
+
* production, so hub surfaces draw a typographic `<MenuFieldTile>` instead
|
|
75
|
+
* and leave this unset. It exists for the surfaces that legitimately show one
|
|
76
|
+
* (concept detail) and for consumers outside the consumer app.
|
|
77
|
+
*/
|
|
78
|
+
photo_url?: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The venue attribution carried on a menu item. */
|
|
82
|
+
export type MenuItemPlaceRef = {
|
|
83
|
+
slug: string
|
|
84
|
+
name: string
|
|
85
|
+
/** Pre-resolved venue thumbnail. Venue photography is real; item photography is not. */
|
|
86
|
+
thumbnail_url?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A dish or drink concept — one item across every kitchen that makes it, under
|
|
91
|
+
* whatever name each of them uses. The page's keystone, and the one thing the
|
|
92
|
+
* catalogue can say that a single menu cannot.
|
|
93
|
+
*/
|
|
94
|
+
export type MenuConceptPreview = {
|
|
95
|
+
slug: string
|
|
96
|
+
/** Canonical concept name, e.g. "Margherita". */
|
|
97
|
+
title: string
|
|
98
|
+
kind: 'dish' | 'drink'
|
|
99
|
+
/** Venues serving it. */
|
|
100
|
+
venue_count?: number
|
|
101
|
+
price_low?: number
|
|
102
|
+
price_high?: number
|
|
103
|
+
/** Distinct names it appears under across those menus. */
|
|
104
|
+
alias_count?: number
|
|
105
|
+
/** Summed censused mentions across every venue's version. */
|
|
106
|
+
mentions?: number
|
|
107
|
+
rows?: MenuConceptRow[]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** One venue's version of a concept, as drawn by `<ConceptCompare>`. */
|
|
111
|
+
export type MenuConceptRow = {
|
|
112
|
+
place_slug: string
|
|
113
|
+
place_name: string
|
|
114
|
+
/** What this kitchen calls it on its own menu. */
|
|
115
|
+
alias: string
|
|
116
|
+
price: number
|
|
117
|
+
mentions?: number
|
|
118
|
+
rating?: number
|
|
119
|
+
/** Venue type line, e.g. "pub". Localised by the caller. */
|
|
120
|
+
place_type?: string
|
|
121
|
+
/** Neighbourhood, e.g. "Centru". */
|
|
122
|
+
place_area?: string
|
|
123
|
+
/**
|
|
124
|
+
* Pre-resolved VENUE photograph.
|
|
125
|
+
*
|
|
126
|
+
* The row is place-led: five restaurants that disagree about a name, not five
|
|
127
|
+
* bars of similar length. A price bar was drawn here first and it measured
|
|
128
|
+
* the one thing already written beside it in words, while the venue — the
|
|
129
|
+
* thing you are actually choosing between — had no image at all.
|
|
130
|
+
*/
|
|
131
|
+
place_thumbnail_url?: string
|
|
132
|
+
open_now?: boolean
|
|
133
|
+
/** Marks the row the surface should lead with (cheapest, or most named). */
|
|
134
|
+
featured?: boolean
|
|
135
|
+
/** Portion, already formatted with its unit, e.g. "260g". */
|
|
136
|
+
portion_label?: string
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A browsable grouping of venues — a cuisine on the dishes tab, a drink family
|
|
141
|
+
* on the drinks tab. Both are sets of VENUES, never sets of items: cuisine
|
|
142
|
+
* exists only on places, so a card counting dishes cannot be built.
|
|
143
|
+
*/
|
|
144
|
+
export type MenuFamilyPreview = {
|
|
145
|
+
slug: string
|
|
146
|
+
/** Display name, already localised by the caller. */
|
|
147
|
+
name: string
|
|
148
|
+
/** Venues in this family. */
|
|
149
|
+
venue_count: number
|
|
150
|
+
/**
|
|
151
|
+
* Facet chips. Counts are VENUES, not items — they come from the places
|
|
152
|
+
* facet narrowed by this family. The drinks tab has no equivalent facet, so
|
|
153
|
+
* its chips ship without counts; `count` is optional for that reason.
|
|
154
|
+
*/
|
|
155
|
+
chips?: MenuFamilyChip[]
|
|
156
|
+
/** Price band across the family's venues. */
|
|
157
|
+
price_low?: number
|
|
158
|
+
price_high?: number
|
|
159
|
+
price_median?: number
|
|
160
|
+
/** Pre-resolved venue photos for the fanned deck. Venue photography only. */
|
|
161
|
+
venue_photo_urls?: string[]
|
|
162
|
+
/** The item this family is most named for. */
|
|
163
|
+
signature?: MenuFamilySignature
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export type MenuFamilyChip = {
|
|
167
|
+
slug: string
|
|
168
|
+
label: string
|
|
169
|
+
/** Venue count. Absent where no facet backs the chip. */
|
|
170
|
+
count?: number
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** A family card's featured item. Same shape as a venue's, for the same reason. */
|
|
174
|
+
export type MenuFamilySignature = MenuVenueSignature
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* A venue as the hub's venue-scoped surfaces consume it: the place itself plus
|
|
178
|
+
* the pre-resolved bits a hub row needs and `PlacePreview` does not carry —
|
|
179
|
+
* which signature to lead with, and where the row points.
|
|
180
|
+
*/
|
|
181
|
+
export type MenuVenuePreview = {
|
|
182
|
+
place: PlacePreview
|
|
183
|
+
href: string
|
|
184
|
+
/**
|
|
185
|
+
* The signature to draw. Callers pick it from `reviews_digest.must_try`
|
|
186
|
+
* rather than the surface re-deriving the rule on every row.
|
|
187
|
+
*/
|
|
188
|
+
signature?: MenuVenueSignature
|
|
189
|
+
/** Closing time as an already-formatted wall clock, e.g. "23:00". */
|
|
190
|
+
closes_at_label?: string
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The resolved signature on a venue row.
|
|
195
|
+
*
|
|
196
|
+
* It carries a whole `MenuItemPreview` rather than a flattened name and price,
|
|
197
|
+
* because every surface that draws one renders `<MenuItem>` — the same row a
|
|
198
|
+
* venue's own menu uses — and that component takes an item. Flattening it here
|
|
199
|
+
* was what let the hub's version drift into a different-looking row.
|
|
200
|
+
*/
|
|
201
|
+
export type MenuVenueSignature = {
|
|
202
|
+
item: MenuItemPreview
|
|
203
|
+
/** The reviewers' reason, from the digest. Rendered in place of the description. */
|
|
204
|
+
why?: string
|
|
205
|
+
href?: string
|
|
206
|
+
/** The digest called this the venue's signature rather than merely frequent. */
|
|
207
|
+
isSignature?: boolean
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* An intent tile — a family plus a constraint, resolving to a category page.
|
|
212
|
+
*
|
|
213
|
+
* Only about thirty-two of the thirty-eight cuisines have a page, so `href` is
|
|
214
|
+
* required: a tile that cannot resolve to a real destination is not rendered
|
|
215
|
+
* rather than linking nowhere.
|
|
216
|
+
*/
|
|
217
|
+
export type MenuIntentTile = {
|
|
218
|
+
slug: string
|
|
219
|
+
label: string
|
|
220
|
+
/** Supporting line, e.g. "247 kitchens". Already formatted by the caller. */
|
|
221
|
+
caption?: string
|
|
222
|
+
href: string
|
|
223
|
+
/**
|
|
224
|
+
* Finished CSS gradient for the tile's ground, e.g.
|
|
225
|
+
* `linear-gradient(150deg, …)`.
|
|
226
|
+
*
|
|
227
|
+
* A gradient rather than a token name, for the same reason `<MenuField>`
|
|
228
|
+
* takes one: the hue tables are app-owned, and a component referencing
|
|
229
|
+
* `var(--h-pizza)` renders an invisible tile in any consumer that does not
|
|
230
|
+
* happen to define that variable. A missing gradient falls back to `bg-muted`
|
|
231
|
+
* — visible and obviously unstyled, rather than white text on white.
|
|
232
|
+
*/
|
|
233
|
+
gradient?: string
|
|
234
|
+
/** Icon node, resolved by the caller — the DS does not pick icons for it. */
|
|
235
|
+
icon?: ReactNode
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Formatters injected into every menu surface. The DS owns no `Intl` locale. */
|
|
239
|
+
export type MenuFormatters = {
|
|
240
|
+
/** 24 → "24 lei". */
|
|
241
|
+
price: (value: number) => string
|
|
242
|
+
/** 1717 → "1,717". */
|
|
243
|
+
number: (value: number) => string
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Strings shared by every menu-hub surface. */
|
|
247
|
+
export type MenuHubLabels = {
|
|
248
|
+
/** Mention-count badge. Pass a translator that handles ICU pluralization. */
|
|
249
|
+
mentions: (count: number) => string
|
|
250
|
+
/** Venue-count line. Pass a translator that handles ICU pluralization. */
|
|
251
|
+
venues: (count: number) => string
|
|
252
|
+
/** "See all" affordance on a section header. */
|
|
253
|
+
seeAll: string
|
|
254
|
+
/** Badge on a must-try row the digest called `signature`. */
|
|
255
|
+
signature: string
|
|
256
|
+
openNow: string
|
|
257
|
+
/** Prefixes a closing time, e.g. "until". */
|
|
258
|
+
until: string
|
|
259
|
+
}
|