@bearmenu/ui 0.8.8 → 0.8.10

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 (206) hide show
  1. package/dist/chunk-24RFI4RQ.js +167 -0
  2. package/dist/chunk-2XCZJWTV.js +121 -0
  3. package/dist/chunk-5RWTOT5Y.js +72 -0
  4. package/dist/chunk-62ES6N32.js +27 -0
  5. package/dist/chunk-AKQ57NXY.js +58 -0
  6. package/dist/chunk-CLLY6XB3.js +41 -0
  7. package/dist/chunk-DTXNDLSE.js +125 -0
  8. package/dist/chunk-F6FMZJDG.js +113 -0
  9. package/dist/{chunk-2WDJR6K7.js → chunk-G7IFL4Z4.js} +4 -1
  10. package/dist/chunk-MZL7CGCL.js +109 -0
  11. package/dist/chunk-Q6QH52LJ.js +38 -0
  12. package/dist/chunk-SY6RV476.js +48 -0
  13. package/dist/chunk-TH6W2PWG.js +213 -0
  14. package/dist/chunk-UDN7UHJW.js +60 -0
  15. package/dist/{chunk-TT3WFS6S.js → chunk-YOKHBAIH.js} +11 -11
  16. package/dist/components/alert.d.ts +2 -2
  17. package/dist/components/badge.d.ts +1 -1
  18. package/dist/components/concept-compare-rail.d.ts +50 -0
  19. package/dist/components/concept-compare-rail.js +5 -0
  20. package/dist/components/concept-compare.d.ts +110 -0
  21. package/dist/components/concept-compare.js +238 -0
  22. package/dist/components/concept-convergence.d.ts +41 -0
  23. package/dist/components/concept-convergence.js +53 -0
  24. package/dist/components/convergence.d.ts +25 -0
  25. package/dist/components/convergence.js +4 -0
  26. package/dist/components/date-input.d.ts +1 -1
  27. package/dist/components/event-card.js +1 -1
  28. package/dist/components/event-compact-row.d.ts +1 -1
  29. package/dist/components/event-compact-row.js +1 -1
  30. package/dist/components/event-detail-panel.d.ts +171 -25
  31. package/dist/components/event-detail-panel.js +472 -175
  32. package/dist/components/event-index-row.js +1 -1
  33. package/dist/components/event-list-row-wide.js +2 -2
  34. package/dist/components/event-list-row.js +2 -2
  35. package/dist/components/explainer-steps.d.ts +34 -0
  36. package/dist/components/explainer-steps.js +36 -0
  37. package/dist/components/glass-ground.d.ts +66 -0
  38. package/dist/components/glass-ground.js +3 -0
  39. package/dist/components/hub-surface.d.ts +27 -0
  40. package/dist/components/hub-surface.js +30 -0
  41. package/dist/components/hue-chip.d.ts +70 -0
  42. package/dist/components/hue-chip.js +3 -0
  43. package/dist/components/kitchen-dossier.d.ts +88 -0
  44. package/dist/components/kitchen-dossier.js +187 -0
  45. package/dist/components/menu-dead-end.d.ts +56 -0
  46. package/dist/components/menu-dead-end.js +49 -0
  47. package/dist/components/menu-family-block.d.ts +99 -0
  48. package/dist/components/menu-family-block.js +155 -0
  49. package/dist/components/menu-family-card.d.ts +65 -0
  50. package/dist/components/menu-family-card.js +79 -0
  51. package/dist/components/menu-field.d.ts +57 -0
  52. package/dist/components/menu-field.js +93 -0
  53. package/dist/components/menu-filter-chips.d.ts +86 -0
  54. package/dist/components/menu-filter-chips.js +122 -0
  55. package/dist/components/menu-group-rows.d.ts +53 -0
  56. package/dist/components/menu-group-rows.js +67 -0
  57. package/dist/components/menu-hub-hero.d.ts +65 -0
  58. package/dist/components/menu-hub-hero.js +103 -0
  59. package/dist/components/menu-index-header.d.ts +37 -0
  60. package/dist/components/menu-index-header.js +68 -0
  61. package/dist/components/menu-index-list.d.ts +48 -0
  62. package/dist/components/menu-index-list.js +96 -0
  63. package/dist/components/menu-intent-hero.d.ts +69 -0
  64. package/dist/components/menu-intent-hero.js +87 -0
  65. package/dist/components/menu-intent-tiles.d.ts +31 -0
  66. package/dist/components/menu-intent-tiles.js +50 -0
  67. package/dist/components/menu-item.d.ts +127 -0
  68. package/dist/components/menu-item.js +5 -0
  69. package/dist/components/menu-kitchen-list.d.ts +82 -0
  70. package/dist/components/menu-kitchen-list.js +5 -0
  71. package/dist/components/menu-mocks.d.ts +90 -0
  72. package/dist/components/menu-mocks.js +504 -0
  73. package/dist/components/menu-types.d.ts +421 -0
  74. package/dist/components/menu-types.js +1 -0
  75. package/dist/components/motion-icon-controls.js +2 -2
  76. package/dist/components/motion-icon-set.js +21 -9
  77. package/dist/components/motion-icon.d.ts +22 -1
  78. package/dist/components/motion-icon.js +1 -1
  79. package/dist/components/open-now-list.d.ts +40 -0
  80. package/dist/components/open-now-list.js +69 -0
  81. package/dist/components/photo-ground.d.ts +64 -0
  82. package/dist/components/photo-ground.js +3 -0
  83. package/dist/components/place-about-tab.js +2 -2
  84. package/dist/components/place-details-mobile.js +2 -2
  85. package/dist/components/place-types.d.ts +112 -1
  86. package/dist/components/qa-block.d.ts +36 -0
  87. package/dist/components/qa-block.js +24 -0
  88. package/dist/components/scroll-fade.js +3 -121
  89. package/dist/components/signature-chip.d.ts +37 -0
  90. package/dist/components/signature-chip.js +4 -0
  91. package/dist/components/snap-rail.d.ts +48 -5
  92. package/dist/components/snap-rail.js +3 -51
  93. package/dist/components/stat-banner.d.ts +47 -0
  94. package/dist/components/stat-banner.js +61 -0
  95. package/dist/components/story.d.ts +34 -0
  96. package/dist/components/story.js +3 -0
  97. package/dist/components/tag.d.ts +2 -2
  98. package/dist/components/text.d.ts +1 -1
  99. package/dist/components/top-comparison.d.ts +28 -0
  100. package/dist/components/top-comparison.js +76 -0
  101. package/dist/components/top-cross-link.d.ts +34 -0
  102. package/dist/components/top-cross-link.js +101 -0
  103. package/dist/components/top-ranking-entry.d.ts +53 -0
  104. package/dist/components/top-ranking-entry.js +217 -0
  105. package/dist/components/top-ranking-hero.d.ts +61 -0
  106. package/dist/components/top-ranking-hero.js +99 -0
  107. package/dist/components/top-ranking-legend.d.ts +69 -0
  108. package/dist/components/top-ranking-legend.js +124 -0
  109. package/dist/components/top-types.d.ts +109 -0
  110. package/dist/components/top-types.js +1 -0
  111. package/dist/components/venue-grade-dimensions.d.ts +38 -0
  112. package/dist/components/venue-grade-dimensions.js +3 -0
  113. package/dist/components/venue-quotes.d.ts +32 -0
  114. package/dist/components/venue-quotes.js +3 -0
  115. package/dist/components/venue-wall.d.ts +41 -0
  116. package/dist/components/venue-wall.js +118 -0
  117. package/dist/components/weekday-theme-banner.d.ts +70 -0
  118. package/dist/components/weekday-theme-banner.js +127 -0
  119. package/dist/components/weekend-panel.js +2 -2
  120. package/package.json +1 -1
  121. package/src/components/colour-dictionary.stories.tsx +227 -0
  122. package/src/components/concept-compare-rail.tsx +131 -0
  123. package/src/components/concept-compare.stories.tsx +172 -0
  124. package/src/components/concept-compare.tsx +443 -0
  125. package/src/components/concept-convergence.stories.tsx +53 -0
  126. package/src/components/concept-convergence.tsx +90 -0
  127. package/src/components/convergence.stories.tsx +54 -0
  128. package/src/components/convergence.tsx +177 -0
  129. package/src/components/event-compact-row.tsx +1 -1
  130. package/src/components/event-detail-panel.stories.tsx +205 -97
  131. package/src/components/event-detail-panel.test.tsx +181 -0
  132. package/src/components/event-detail-panel.tsx +703 -185
  133. package/src/components/explainer-steps.stories.tsx +46 -0
  134. package/src/components/explainer-steps.tsx +74 -0
  135. package/src/components/glass-ground.tsx +129 -0
  136. package/src/components/hub-surface.stories.tsx +68 -0
  137. package/src/components/hub-surface.tsx +50 -0
  138. package/src/components/hue-chip.stories.tsx +76 -0
  139. package/src/components/hue-chip.test.tsx +62 -0
  140. package/src/components/hue-chip.tsx +99 -0
  141. package/src/components/kitchen-dossier.stories.tsx +108 -0
  142. package/src/components/kitchen-dossier.tsx +322 -0
  143. package/src/components/menu-dead-end.stories.tsx +67 -0
  144. package/src/components/menu-dead-end.tsx +127 -0
  145. package/src/components/menu-family-block.stories.tsx +91 -0
  146. package/src/components/menu-family-block.tsx +314 -0
  147. package/src/components/menu-family-card.stories.tsx +120 -0
  148. package/src/components/menu-family-card.tsx +191 -0
  149. package/src/components/menu-field.stories.tsx +74 -0
  150. package/src/components/menu-field.tsx +199 -0
  151. package/src/components/menu-filter-chips.stories.tsx +107 -0
  152. package/src/components/menu-filter-chips.tsx +238 -0
  153. package/src/components/menu-group-rows.stories.tsx +54 -0
  154. package/src/components/menu-group-rows.tsx +133 -0
  155. package/src/components/menu-hub-hero.stories.tsx +92 -0
  156. package/src/components/menu-hub-hero.tsx +187 -0
  157. package/src/components/menu-index-header.stories.tsx +72 -0
  158. package/src/components/menu-index-header.tsx +125 -0
  159. package/src/components/menu-index-list.stories.tsx +50 -0
  160. package/src/components/menu-index-list.tsx +149 -0
  161. package/src/components/menu-intent-hero.stories.tsx +67 -0
  162. package/src/components/menu-intent-hero.tsx +189 -0
  163. package/src/components/menu-intent-tiles.stories.tsx +54 -0
  164. package/src/components/menu-intent-tiles.tsx +76 -0
  165. package/src/components/menu-item.tsx +416 -0
  166. package/src/components/menu-kitchen-list.stories.tsx +67 -0
  167. package/src/components/menu-kitchen-list.test.tsx +78 -0
  168. package/src/components/menu-kitchen-list.tsx +284 -0
  169. package/src/components/menu-mocks.ts +592 -0
  170. package/src/components/menu-types.ts +442 -0
  171. package/src/components/motion-icon-controls.tsx +11 -11
  172. package/src/components/motion-icon-set.tsx +36 -8
  173. package/src/components/motion-icon.test.tsx +23 -1
  174. package/src/components/motion-icon.tsx +20 -0
  175. package/src/components/open-now-list.stories.tsx +76 -0
  176. package/src/components/open-now-list.tsx +127 -0
  177. package/src/components/photo-ground.tsx +147 -0
  178. package/src/components/place-types.ts +112 -0
  179. package/src/components/qa-block.stories.tsx +43 -0
  180. package/src/components/qa-block.tsx +60 -0
  181. package/src/components/signature-chip.tsx +77 -0
  182. package/src/components/snap-rail.tsx +133 -9
  183. package/src/components/stat-banner.stories.tsx +100 -0
  184. package/src/components/stat-banner.tsx +117 -0
  185. package/src/components/story.tsx +225 -0
  186. package/src/components/top-comparison.stories.tsx +78 -0
  187. package/src/components/top-comparison.tsx +151 -0
  188. package/src/components/top-cross-link.stories.tsx +84 -0
  189. package/src/components/top-cross-link.tsx +171 -0
  190. package/src/components/top-ranking-entry.stories.tsx +171 -0
  191. package/src/components/top-ranking-entry.test.tsx +105 -0
  192. package/src/components/top-ranking-entry.tsx +425 -0
  193. package/src/components/top-ranking-hero.stories.tsx +76 -0
  194. package/src/components/top-ranking-hero.tsx +218 -0
  195. package/src/components/top-ranking-legend.stories.tsx +78 -0
  196. package/src/components/top-ranking-legend.tsx +239 -0
  197. package/src/components/top-types.ts +110 -0
  198. package/src/components/venue-grade-dimensions.stories.tsx +83 -0
  199. package/src/components/venue-grade-dimensions.tsx +91 -0
  200. package/src/components/venue-quotes.stories.tsx +59 -0
  201. package/src/components/venue-quotes.tsx +64 -0
  202. package/src/components/venue-wall.stories.tsx +78 -0
  203. package/src/components/venue-wall.tsx +185 -0
  204. package/src/components/weekday-theme-banner.stories.tsx +113 -0
  205. package/src/components/weekday-theme-banner.tsx +231 -0
  206. package/dist/{chunk-XWMRABZW.js → chunk-HSIYYA2E.js} +1 -1
@@ -0,0 +1,592 @@
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
+ MenuFamilyBlockPreview,
24
+ MenuFamilyPreview,
25
+ MenuFilterChip,
26
+ MenuIndexEntry,
27
+ MenuIntentFigure,
28
+ MenuIntentTile,
29
+ MenuItemPreview,
30
+ MenuKitchenPreview,
31
+ MenuLoosenOption,
32
+ MenuVenuePreview,
33
+ QaItem,
34
+ } from "./menu-types";
35
+ import type { PlacePreview } from "./place-types";
36
+
37
+ export const mockGradients = {
38
+ // The forest dictionary (docs/colour-dictionary.md, Foundations/Colour dictionary):
39
+ // wheat, ember, pine, maple, lake water. Post-generator films these, so they
40
+ // must be the same values the app paints.
41
+ pasta: "linear-gradient(150deg, oklch(0.555 0.105 80), oklch(0.455 0.10 70))",
42
+ meat: "linear-gradient(150deg, oklch(0.49 0.13 28), oklch(0.41 0.12 22))",
43
+ greens: "linear-gradient(150deg, oklch(0.505 0.115 148), oklch(0.415 0.11 142))",
44
+ sweet: "linear-gradient(150deg, oklch(0.505 0.14 50), oklch(0.405 0.13 42))",
45
+ cocktail: "linear-gradient(150deg, oklch(0.485 0.095 205), oklch(0.405 0.09 212))",
46
+ } as const;
47
+
48
+ export const mockPlaces: PlacePreview[] = [
49
+ {
50
+ id: "p1",
51
+ slug: "the-copper-yard",
52
+ name: "The Copper Yard",
53
+ open_now: true,
54
+ rating: { rating: 4.6, count: 318 },
55
+ hours: [{ opens_at: 12, closes_at: 23 }],
56
+ price_dishes: { price_range_low: 19, price_range_high: 62, median_price: 28 },
57
+ cuisines: ["italian", "pizza"],
58
+ type: "pub",
59
+ busyness: [
60
+ { hour: 12, score: 22 },
61
+ { hour: 13, score: 26 },
62
+ { hour: 14, score: 30 },
63
+ { hour: 15, score: 38 },
64
+ { hour: 16, score: 52 },
65
+ { hour: 17, score: 74 },
66
+ { hour: 18, score: 92 },
67
+ { hour: 19, score: 100 },
68
+ { hour: 20, score: 86 },
69
+ { hour: 21, score: 64 },
70
+ ],
71
+ reviews_digest: {
72
+ review_count: 318,
73
+ grade: {
74
+ overall: 8.4,
75
+ show_number: true,
76
+ descriptor: "Foarte bun",
77
+ confidence: "high",
78
+ dimensions: {
79
+ food: 89,
80
+ service: 82,
81
+ value: 86,
82
+ ambiance: 78,
83
+ portion: 91,
84
+ consistency: 84,
85
+ },
86
+ },
87
+ value: { value: "fair" },
88
+ tags: [{ value: "casual" }, { value: "cozy" }, { value: "family_friendly" }],
89
+ highlights: [
90
+ {
91
+ text: "The wings are the reason to sit at the bar — hot honey, and they keep going until the kitchen shuts.",
92
+ review_ids: ["r1", "r2", "r3"],
93
+ },
94
+ {
95
+ text: "Last kitchen open on this street. We came at half eleven and still ate properly.",
96
+ review_ids: ["r4", "r5"],
97
+ },
98
+ ],
99
+ must_try: [
100
+ {
101
+ name: "Chicken wings, 6 pc",
102
+ why: "Buttermilk brine and hot honey, fried to order until the kitchen shuts.",
103
+ strength: "signature",
104
+ slug: "chicken-wings",
105
+ mentions: 23,
106
+ sizes_and_prices: [{ size: 320, price: 32 }],
107
+ review_ids: ["r1", "r2", "r3"],
108
+ },
109
+ ],
110
+ },
111
+ },
112
+ {
113
+ id: "p2",
114
+ slug: "salt-and-stone",
115
+ name: "Salt & Stone",
116
+ open_now: true,
117
+ rating: { rating: 4.8, count: 204 },
118
+ hours: [{ opens_at: 11, closes_at: 22 }],
119
+ price_dishes: { price_range_low: 38, price_range_high: 84, median_price: 49 },
120
+ cuisines: ["italian"],
121
+ type: "restaurant",
122
+ reviews_digest: {
123
+ review_count: 204,
124
+ grade: {
125
+ overall: 8.9,
126
+ show_number: true,
127
+ dimensions: { food: 93, service: 88, value: 74, ambiance: 85 },
128
+ },
129
+ must_try: [
130
+ {
131
+ name: "Truffle Tagliatelle",
132
+ why: "Made in-house each morning with black truffle and parmesan.",
133
+ strength: "signature",
134
+ slug: "truffle-tagliatelle",
135
+ mentions: 17,
136
+ sizes_and_prices: [{ size: 320, price: 62 }],
137
+ review_ids: ["r6", "r7"],
138
+ },
139
+ ],
140
+ },
141
+ },
142
+ {
143
+ id: "p3",
144
+ slug: "the-quiet-hours",
145
+ name: "The Quiet Hours",
146
+ open_now: true,
147
+ rating: { rating: 4.5, count: 96 },
148
+ hours: [{ opens_at: 8, closes_at: 20 }],
149
+ price_dishes: { price_range_low: 8, price_range_high: 26, median_price: 14 },
150
+ type: "coffee_shop",
151
+ reviews_digest: {
152
+ review_count: 96,
153
+ // A coffee bar is graded on coffee_quality and never on food — the
154
+ // dimension row must survive the absence rather than draw a zero.
155
+ grade: {
156
+ overall: 8.6,
157
+ show_number: true,
158
+ dimensions: { coffee_quality: 91, service: 86, value: 80, ambiance: 88 },
159
+ },
160
+ must_try: [
161
+ {
162
+ name: "Flat white",
163
+ strength: "frequent",
164
+ slug: "flat-white",
165
+ mentions: 28,
166
+ sizes_and_prices: [{ size: 220, price: 14 }],
167
+ },
168
+ ],
169
+ },
170
+ },
171
+ {
172
+ id: "p4",
173
+ slug: "green-room-canteen",
174
+ name: "Green Room Canteen",
175
+ open_now: false,
176
+ rating: { rating: 4.7, count: 141 },
177
+ price_dishes: { price_range_low: 22, price_range_high: 51, median_price: 31 },
178
+ type: "restaurant",
179
+ // No digest at all: a place the census has not reached. Every venue surface
180
+ // has to render this without collapsing.
181
+ },
182
+ ];
183
+
184
+ const placeBySlug = (slug: string) => mockPlaces.find((place) => place.slug === slug)!;
185
+
186
+ export const mockVenues: MenuVenuePreview[] = [
187
+ {
188
+ place: placeBySlug("the-copper-yard"),
189
+ href: "/places/the-copper-yard",
190
+ closes_at_label: "23:00",
191
+ signature: {
192
+ item: {
193
+ id: "s1",
194
+ slug: "chicken-wings",
195
+ name: "Chicken wings, 6 pc",
196
+ kind: "dish",
197
+ mentions: 23,
198
+ sizes_and_prices: [{ size: 320, price: 32 }],
199
+ },
200
+ why: "Buttermilk brine and hot honey, fried to order until the kitchen shuts.",
201
+ isSignature: true,
202
+ href: "/menu/dishes/chicken-wings",
203
+ },
204
+ },
205
+ {
206
+ place: placeBySlug("salt-and-stone"),
207
+ href: "/places/salt-and-stone",
208
+ closes_at_label: "22:00",
209
+ signature: {
210
+ item: {
211
+ id: "s2",
212
+ slug: "truffle-tagliatelle",
213
+ name: "Truffle Tagliatelle",
214
+ kind: "dish",
215
+ mentions: 17,
216
+ sizes_and_prices: [{ size: 320, price: 62 }],
217
+ },
218
+ why: "Made in-house each morning with black truffle and parmesan.",
219
+ isSignature: true,
220
+ href: "/menu/dishes/truffle-tagliatelle",
221
+ },
222
+ },
223
+ {
224
+ place: placeBySlug("the-quiet-hours"),
225
+ href: "/places/the-quiet-hours",
226
+ closes_at_label: "20:00",
227
+ signature: {
228
+ item: {
229
+ id: "s3",
230
+ slug: "flat-white",
231
+ name: "Flat white",
232
+ kind: "drink",
233
+ mentions: 28,
234
+ sizes_and_prices: [{ size: 220, price: 14 }],
235
+ },
236
+ href: "/menu/drinks/flat-white",
237
+ },
238
+ },
239
+ {
240
+ place: placeBySlug("green-room-canteen"),
241
+ href: "/places/green-room-canteen",
242
+ // No signature: the row must still read as a venue.
243
+ },
244
+ ];
245
+
246
+ export const mockItems: MenuItemPreview[] = [
247
+ {
248
+ id: "i1",
249
+ slug: "truffle-tagliatelle",
250
+ name: "Truffle Tagliatelle",
251
+ kind: "dish",
252
+ category_slug: "pasta",
253
+ mentions: 17,
254
+ sizes_and_prices: [{ size: 320, price: 62 }],
255
+ place: { slug: "salt-and-stone", name: "Salt & Stone" },
256
+ },
257
+ {
258
+ id: "i2",
259
+ slug: "chicken-wings",
260
+ name: "Chicken wings, 6 pc",
261
+ kind: "dish",
262
+ category_slug: "chicken-dish",
263
+ mentions: 23,
264
+ sizes_and_prices: [{ size: 320, price: 32 }],
265
+ place: { slug: "the-copper-yard", name: "The Copper Yard" },
266
+ },
267
+ {
268
+ id: "i3",
269
+ slug: "burrata-heirloom-tomato",
270
+ name: "Burrata & heirloom tomato",
271
+ kind: "dish",
272
+ category_slug: "salad",
273
+ mentions: 14,
274
+ sizes_and_prices: [{ size: 300, price: 49 }],
275
+ place: { slug: "green-room-canteen", name: "Green Room Canteen" },
276
+ },
277
+ {
278
+ id: "i4",
279
+ slug: "flat-white",
280
+ name: "Flat white",
281
+ kind: "drink",
282
+ category_slug: "non-alcoholic",
283
+ mentions: 28,
284
+ sizes_and_prices: [{ size: 220, price: 14 }],
285
+ place: { slug: "the-quiet-hours", name: "The Quiet Hours" },
286
+ },
287
+ {
288
+ id: "i5",
289
+ slug: "house-negroni",
290
+ name: "House negroni",
291
+ kind: "drink",
292
+ category_slug: "cocktail",
293
+ // Deliberately unmentioned: the badge must not render "0".
294
+ mentions: 0,
295
+ sizes_and_prices: [{ size: 150, price: 32 }],
296
+ place: { slug: "the-copper-yard", name: "The Copper Yard" },
297
+ },
298
+ ];
299
+
300
+ export const mockConcept: MenuConceptPreview = {
301
+ slug: "margherita",
302
+ title: "Margherita",
303
+ kind: "dish",
304
+ venue_count: 87,
305
+ price_low: 10,
306
+ price_high: 120,
307
+ alias_count: 12,
308
+ mentions: 214,
309
+ rows: [
310
+ {
311
+ place_slug: "the-copper-yard",
312
+ place_name: "The Copper Yard",
313
+ alias: "Pizza Margherita",
314
+ price: 34,
315
+ mentions: 23,
316
+ rating: 4.6,
317
+ place_type: "pub",
318
+ featured: true,
319
+ },
320
+ {
321
+ place_slug: "salt-and-stone",
322
+ place_name: "Salt & Stone",
323
+ alias: "Margherita D.O.P.",
324
+ price: 46,
325
+ mentions: 17,
326
+ rating: 4.8,
327
+ place_type: "restaurant",
328
+ },
329
+ {
330
+ place_slug: "green-room-canteen",
331
+ place_name: "Green Room Canteen",
332
+ alias: "Marinara Bianca",
333
+ price: 52,
334
+ mentions: 9,
335
+ rating: 4.7,
336
+ place_type: "canteen",
337
+ },
338
+ ],
339
+ };
340
+
341
+ export const mockFamily: MenuFamilyPreview = {
342
+ slug: "italian",
343
+ name: "Italian",
344
+ venue_count: 247,
345
+ chips: [
346
+ { slug: "pizza", label: "Pizza", count: 120 },
347
+ { slug: "pasta", label: "Pasta", count: 96 },
348
+ { slug: "salad", label: "Salad", count: 88 },
349
+ { slug: "chicken-dish", label: "Chicken", count: 77 },
350
+ ],
351
+ price_low: 32,
352
+ price_high: 84,
353
+ price_median: 49,
354
+ venue_photo_urls: [],
355
+ };
356
+
357
+ /** The drinks counterpart, and the reason `chip.count` is optional. */
358
+ export const mockDrinkFamily: MenuFamilyPreview = {
359
+ slug: "cocktail",
360
+ name: "Cocktails",
361
+ venue_count: 21,
362
+ // No counts: there is no drink-group facet on places, so these ship as bare
363
+ // labels rather than with an invented number.
364
+ chips: [
365
+ { slug: "spritz", label: "Spritz" },
366
+ { slug: "negroni", label: "Negroni" },
367
+ { slug: "sour", label: "Sour" },
368
+ ],
369
+ price_low: 18,
370
+ price_high: 54,
371
+ price_median: 28,
372
+ venue_photo_urls: [],
373
+ };
374
+
375
+ const intentGradient = (hue: number) =>
376
+ `linear-gradient(150deg, oklch(0.52 0.14 ${hue}), oklch(0.43 0.13 ${hue - 4}))`;
377
+
378
+ export const mockIntentTiles: MenuIntentTile[] = [
379
+ { slug: "italian", label: "Italian tonight", caption: "247 kitchens", href: "/top/italian?open=now", gradient: intentGradient(40) },
380
+ { slug: "burgeri", label: "Burgers", caption: "229 kitchens", href: "/top/burgeri", gradient: intentGradient(25) },
381
+ { slug: "restaurante", label: "Open right now", caption: "by the hour", href: "/top/restaurante?open=now", gradient: intentGradient(60) },
382
+ { slug: "brunch", label: "Brunch", caption: "314 kitchens", href: "/top/brunch", gradient: intentGradient(75) },
383
+ { slug: "vegan", label: "Vegan", caption: "108 kitchens", href: "/top/vegan", gradient: intentGradient(148) },
384
+ { slug: "centru", label: "In Centru", caption: "by neighbourhood", href: "/top/centru", gradient: intentGradient(300) },
385
+ ];
386
+
387
+ /** Formatters the stories pass. Real callers inject locale-aware ones. */
388
+ export const mockFormatters = {
389
+ price: (value: number) => `${value} lei`,
390
+ number: (value: number) => value.toLocaleString("en-US"),
391
+ score: (value: number) => (value / 10).toFixed(1),
392
+ };
393
+
394
+ export const mockHubLabels = {
395
+ mentions: (count: number) => `${count} mentions`,
396
+ venues: (count: number) => `${count} kitchens`,
397
+ seeAll: "See all",
398
+ signature: "SIGNATURE",
399
+ openNow: "Open now",
400
+ until: "until",
401
+ };
402
+
403
+ /* ── intent page ────────────────────────────────────────────────────────── */
404
+
405
+ /**
406
+ * The intent page's kitchen rows.
407
+ *
408
+ * The gaps are the gaps the API actually has, and they are the reason the row
409
+ * degrades the way it does: `must_try` entries arrive from a LIST endpoint, so
410
+ * a third of them carry no `slug` (nothing to open — the dish line goes inert)
411
+ * and none of them carry `mentions` (only `review_ids.length`, which is absent
412
+ * for census items — so the count line disappears rather than reading zero).
413
+ * A layout that survives these survives production.
414
+ */
415
+ export const mockKitchens: MenuKitchenPreview[] = [
416
+ {
417
+ place: placeBySlug("salt-and-stone"),
418
+ href: "/places/salt-and-stone",
419
+ metaLabel: "courtyard · Centru",
420
+ hoursLabel: "kitchen until 23:00",
421
+ priceLabel: "62 lei",
422
+ dish: {
423
+ name: "Truffle Tagliatelle",
424
+ href: "/menu/dishes/truffle-tagliatelle",
425
+ kicker: "MOST NAMED HERE",
426
+ meta: "17 reviews name it",
427
+ },
428
+ },
429
+ {
430
+ place: placeBySlug("the-copper-yard"),
431
+ href: "/places/the-copper-yard",
432
+ metaLabel: "pizzeria · Centru",
433
+ hoursLabel: "kitchen until 23:30",
434
+ priceLabel: "54 lei",
435
+ dish: {
436
+ name: "Tagliatelle cu Hribi",
437
+ href: "/menu/dishes/tagliatelle-cu-hribi",
438
+ kicker: "MOST NAMED HERE",
439
+ meta: "14 reviews name it",
440
+ },
441
+ },
442
+ {
443
+ // Closed, and its must-try has no live menu row — the dish line is inert.
444
+ place: { ...placeBySlug("the-quiet-hours"), open_now: false },
445
+ href: "/places/the-quiet-hours",
446
+ metaLabel: "bistro · Gheorgheni",
447
+ hoursLabel: "closed · opens 12:00",
448
+ priceLabel: "58 lei",
449
+ dish: { name: "Risotto ai Funghi", kicker: "MOST NAMED HERE" },
450
+ },
451
+ {
452
+ // No dish at all: the row must still read as a venue.
453
+ place: placeBySlug("green-room-canteen"),
454
+ href: "/places/green-room-canteen",
455
+ metaLabel: "bistro · Centru",
456
+ priceLabel: "38 lei",
457
+ },
458
+ ];
459
+
460
+ /** The same rows in a filtered state — each answering the filter, not volunteering. */
461
+ export const mockMatchedKitchens: MenuKitchenPreview[] = mockKitchens.slice(0, 3).map((kitchen) => ({
462
+ ...kitchen,
463
+ dish: kitchen.dish && {
464
+ ...kitchen.dish,
465
+ kicker: "WHY IT MATCHES",
466
+ meta: "mid-range · 300g",
467
+ matched: true,
468
+ matchLabel: "vegetarian",
469
+ },
470
+ }));
471
+
472
+ export const mockIntentFigures: MenuIntentFigure[] = [
473
+ { key: "total", value: "247", label: "kitchens" },
474
+ { key: "pizza", value: "120", label: "make a pizza", onSelect: () => {} },
475
+ { key: "pasta", value: "96", label: "make pasta", onSelect: () => {} },
476
+ { key: "open", value: "58", label: "open now", onSelect: () => {} },
477
+ ];
478
+
479
+ /**
480
+ * Note there is no zero-count chip here, and that is not an oversight: the
481
+ * facets endpoint omits values that would match nothing, so a chip can only
482
+ * become `empty` client-side, after it was offered and the reader's next filter
483
+ * emptied it. The `Dead` story constructs that case deliberately.
484
+ */
485
+ export const mockFilterChips: MenuFilterChip[] = [
486
+ { key: "open", label: "Open now", count: 58, active: true },
487
+ { key: "price", label: "Mid-range", count: 96 },
488
+ { key: "terrace", label: "Terrace", count: 31 },
489
+ { key: "veg", label: "Has a vegetarian dish", count: 84 },
490
+ { key: "late", label: "Serving past midnight", count: 4 },
491
+ { key: "centru", label: "Centru", count: 90 },
492
+ ];
493
+
494
+ export const mockLoosenOptions: MenuLoosenOption[] = [
495
+ { key: "open", label: "Drop “Open now”", gainLabel: "+8 kitchens" },
496
+ { key: "price", label: "Drop “Mid-range”", gainLabel: "+7 kitchens" },
497
+ { key: "veg", label: "Drop “Vegetarian”", gainLabel: "+2 kitchens" },
498
+ ];
499
+
500
+ /* ── the browse index ───────────────────────────────────────────────────── */
501
+
502
+ /**
503
+ * The long tail is the point of these fixtures.
504
+ *
505
+ * Cluj's cuisine facet runs from 457 down to 1, and twenty-two of the
506
+ * thirty-eight have fewer than twenty places. A layout tested only on the head
507
+ * of that list looks fine and then ships a page whose last two columns are
508
+ * single digits.
509
+ */
510
+ export const mockIndexEntries: MenuIndexEntry[] = [
511
+ ["Breakfast & brunch", 314],
512
+ ["Romanian", 299],
513
+ ["Bakery", 250],
514
+ ["Italian", 247],
515
+ ["Pizza", 245],
516
+ ["Fast food", 229],
517
+ ["Vegetarian", 172],
518
+ ["American", 153],
519
+ ["Vegan", 108],
520
+ ["Mediterranean", 78],
521
+ ["Seafood", 77],
522
+ ["Eastern European", 67],
523
+ ["Fusion", 61],
524
+ ["Middle Eastern", 58],
525
+ ["BBQ", 57],
526
+ ["Deli", 51],
527
+ ["Steakhouse", 49],
528
+ ["Japanese", 34],
529
+ ["French", 26],
530
+ ["Sushi", 23],
531
+ ["Turkish", 22],
532
+ ["Chinese", 14],
533
+ ["Greek", 12],
534
+ ["Indian", 11],
535
+ ["Thai", 7],
536
+ ["German", 5],
537
+ ["Lebanese", 3],
538
+ ["Portuguese", 1],
539
+ ].map(([name, count], index) => ({
540
+ slug: String(name).toLowerCase().replace(/[^a-z]+/g, "-"),
541
+ name: String(name),
542
+ count: Number(count),
543
+ href: `/menu/cuisines/${String(name).toLowerCase().replace(/[^a-z]+/g, "-")}`,
544
+ gradient: Object.values(mockGradients)[index % 5],
545
+ }));
546
+
547
+ export const mockFamilyBlock: MenuFamilyBlockPreview = {
548
+ slug: "italian",
549
+ name: "Italian",
550
+ venueCount: 247,
551
+ chips: [
552
+ { slug: "pizza", label: "Pizza", count: 120 },
553
+ { slug: "pasta", label: "Pasta", count: 96 },
554
+ { slug: "salad", label: "Salad", count: 88 },
555
+ ],
556
+ priceBandLabel: "22–86 lei",
557
+ photoUrls: [],
558
+ // Three, as the cuisines index sends, and stripped the same way: no dish and
559
+ // no price. The dish column is ~110px inside this card, which truncates the
560
+ // name and wraps its kicker to three lines; the price is already the band
561
+ // above.
562
+ venues: mockKitchens
563
+ .slice(0, 3)
564
+ .map((kitchen) => ({ ...kitchen, dish: undefined, priceLabel: undefined })),
565
+ href: "/menu/cuisines/italian",
566
+ };
567
+
568
+ /** No photographs, no venue, no price — the degraded block still reads. */
569
+ export const mockBareFamilyBlock: MenuFamilyBlockPreview = {
570
+ slug: "portuguese",
571
+ name: "Portuguese",
572
+ venueCount: 1,
573
+ href: "/menu/cuisines/portuguese",
574
+ };
575
+
576
+ export const mockQa: QaItem[] = [
577
+ {
578
+ question: "Which Italian kitchen is best in Cluj?",
579
+ answer:
580
+ "Salt & Stone is named most often — seventeen reviews single out the truffle tagliatelle. The Copper Yard is second on volume and about eight lei cheaper across the menu.",
581
+ },
582
+ {
583
+ question: "What does an Italian main cost?",
584
+ answer:
585
+ "Pasta runs 38 to 84 lei and pizza 30 to 68 across these kitchens. Ninety-six of them sit in the mid-range price band.",
586
+ },
587
+ {
588
+ question: "Anywhere open late?",
589
+ answer:
590
+ "Four keep the kitchen going past midnight — The Copper Yard until 00:00 is the latest of the Italian rooms.",
591
+ },
592
+ ];