@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.
Files changed (113) hide show
  1. package/dist/chunk-62ES6N32.js +27 -0
  2. package/dist/chunk-AXV5O2IZ.js +58 -0
  3. package/dist/chunk-Q6QH52LJ.js +38 -0
  4. package/dist/chunk-TH6W2PWG.js +213 -0
  5. package/dist/chunk-THOVPZNN.js +37 -0
  6. package/dist/chunk-YS6BHKZO.js +92 -0
  7. package/dist/components/alert.d.ts +2 -2
  8. package/dist/components/badge.d.ts +1 -1
  9. package/dist/components/button.d.ts +1 -1
  10. package/dist/components/command.d.ts +7 -7
  11. package/dist/components/concept-compare.d.ts +73 -0
  12. package/dist/components/concept-compare.js +162 -0
  13. package/dist/components/event-card.js +1 -1
  14. package/dist/components/event-compact-row.d.ts +1 -1
  15. package/dist/components/event-compact-row.js +1 -1
  16. package/dist/components/event-detail-panel.d.ts +171 -25
  17. package/dist/components/event-detail-panel.js +471 -174
  18. package/dist/components/event-list-row-wide.js +1 -1
  19. package/dist/components/event-list-row.js +1 -1
  20. package/dist/components/hub-surface.d.ts +27 -0
  21. package/dist/components/hub-surface.js +20 -0
  22. package/dist/components/menu-family-card.d.ts +59 -0
  23. package/dist/components/menu-family-card.js +121 -0
  24. package/dist/components/menu-field.d.ts +57 -0
  25. package/dist/components/menu-field.js +3 -0
  26. package/dist/components/menu-hub-hero.d.ts +65 -0
  27. package/dist/components/menu-hub-hero.js +103 -0
  28. package/dist/components/menu-intent-tiles.d.ts +31 -0
  29. package/dist/components/menu-intent-tiles.js +50 -0
  30. package/dist/components/menu-item.d.ts +127 -0
  31. package/dist/components/menu-item.js +5 -0
  32. package/dist/components/menu-mocks.d.ts +55 -0
  33. package/dist/components/menu-mocks.js +379 -0
  34. package/dist/components/menu-types.d.ts +245 -0
  35. package/dist/components/menu-types.js +1 -0
  36. package/dist/components/motion-icon-set.js +15 -3
  37. package/dist/components/open-now-list.d.ts +40 -0
  38. package/dist/components/open-now-list.js +69 -0
  39. package/dist/components/place-types.d.ts +112 -1
  40. package/dist/components/ranked-venue-list.d.ts +49 -0
  41. package/dist/components/ranked-venue-list.js +87 -0
  42. package/dist/components/signature-chip.d.ts +37 -0
  43. package/dist/components/signature-chip.js +4 -0
  44. package/dist/components/signature-line-list.d.ts +45 -0
  45. package/dist/components/signature-line-list.js +99 -0
  46. package/dist/components/snap-rail.d.ts +24 -4
  47. package/dist/components/snap-rail.js +66 -8
  48. package/dist/components/spinner.d.ts +2 -2
  49. package/dist/components/stat-banner.d.ts +47 -0
  50. package/dist/components/stat-banner.js +61 -0
  51. package/dist/components/tag.d.ts +2 -2
  52. package/dist/components/text.d.ts +3 -3
  53. package/dist/components/venue-busyness-strip.d.ts +35 -0
  54. package/dist/components/venue-busyness-strip.js +37 -0
  55. package/dist/components/venue-grade-dimensions.d.ts +36 -0
  56. package/dist/components/venue-grade-dimensions.js +3 -0
  57. package/dist/components/venue-quotes.d.ts +32 -0
  58. package/dist/components/venue-quotes.js +3 -0
  59. package/dist/components/venue-signature-block.d.ts +39 -0
  60. package/dist/components/venue-signature-block.js +6 -0
  61. package/dist/components/venue-spotlight.d.ts +74 -0
  62. package/dist/components/venue-spotlight.js +138 -0
  63. package/dist/components/venue-wall.d.ts +41 -0
  64. package/dist/components/venue-wall.js +118 -0
  65. package/dist/components/weekday-theme-banner.d.ts +70 -0
  66. package/dist/components/weekday-theme-banner.js +127 -0
  67. package/package.json +1 -1
  68. package/src/components/colour-dictionary.stories.tsx +227 -0
  69. package/src/components/concept-compare.stories.tsx +104 -0
  70. package/src/components/concept-compare.tsx +276 -0
  71. package/src/components/event-compact-row.tsx +1 -1
  72. package/src/components/event-detail-panel.stories.tsx +205 -97
  73. package/src/components/event-detail-panel.test.tsx +181 -0
  74. package/src/components/event-detail-panel.tsx +703 -185
  75. package/src/components/hub-surface.stories.tsx +68 -0
  76. package/src/components/hub-surface.tsx +40 -0
  77. package/src/components/menu-family-card.stories.tsx +112 -0
  78. package/src/components/menu-family-card.tsx +211 -0
  79. package/src/components/menu-field.stories.tsx +74 -0
  80. package/src/components/menu-field.tsx +199 -0
  81. package/src/components/menu-hub-hero.stories.tsx +92 -0
  82. package/src/components/menu-hub-hero.tsx +187 -0
  83. package/src/components/menu-intent-tiles.stories.tsx +54 -0
  84. package/src/components/menu-intent-tiles.tsx +76 -0
  85. package/src/components/menu-item.tsx +416 -0
  86. package/src/components/menu-mocks.ts +417 -0
  87. package/src/components/menu-types.ts +259 -0
  88. package/src/components/motion-icon-set.tsx +30 -3
  89. package/src/components/open-now-list.stories.tsx +76 -0
  90. package/src/components/open-now-list.tsx +127 -0
  91. package/src/components/place-types.ts +112 -0
  92. package/src/components/ranked-venue-list.stories.tsx +80 -0
  93. package/src/components/ranked-venue-list.tsx +177 -0
  94. package/src/components/signature-chip.tsx +77 -0
  95. package/src/components/signature-line-list.stories.tsx +85 -0
  96. package/src/components/signature-line-list.tsx +136 -0
  97. package/src/components/snap-rail.tsx +90 -9
  98. package/src/components/stat-banner.stories.tsx +100 -0
  99. package/src/components/stat-banner.tsx +117 -0
  100. package/src/components/venue-busyness-strip.stories.tsx +66 -0
  101. package/src/components/venue-busyness-strip.tsx +64 -0
  102. package/src/components/venue-grade-dimensions.stories.tsx +83 -0
  103. package/src/components/venue-grade-dimensions.tsx +91 -0
  104. package/src/components/venue-quotes.stories.tsx +59 -0
  105. package/src/components/venue-quotes.tsx +64 -0
  106. package/src/components/venue-signature-block.stories.tsx +68 -0
  107. package/src/components/venue-signature-block.tsx +71 -0
  108. package/src/components/venue-spotlight.stories.tsx +141 -0
  109. package/src/components/venue-spotlight.tsx +236 -0
  110. package/src/components/venue-wall.stories.tsx +78 -0
  111. package/src/components/venue-wall.tsx +185 -0
  112. package/src/components/weekday-theme-banner.stories.tsx +113 -0
  113. 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
+ }