@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,421 @@
1
+ import { ComponentType, CSSProperties, ReactNode } from 'react';
2
+ import { MenuSizeAndPrice, PlacePreview } from './place-types.js';
3
+
4
+ /**
5
+ * Structural mirror of the openapi `Dish` / `Drink` schemas, decoupled from any
6
+ * one app's generated types. FE, admin and post-generator map their domain
7
+ * shapes into these before passing data to the menu-hub surfaces.
8
+ *
9
+ * Same contract as `place-types.ts` and `event-types.ts`: no app schemas, no
10
+ * app hooks, no i18n. Every user-facing string arrives through a `*Labels`
11
+ * object, every destination through an `href`, and every locale-sensitive
12
+ * format through an injected formatter — the DS never owns a translation key
13
+ * or an `Intl` locale.
14
+ *
15
+ * ## One set of components, two tabs
16
+ *
17
+ * Dishes and drinks are the same shape. `MenuItemPreview.kind` is the only
18
+ * discriminator, and nothing in this file branches on it — a drinks hub is a
19
+ * different view model and a different `*Labels` object, never a second set of
20
+ * components. Anything that would need a `if (kind === 'drink')` inside a
21
+ * component belongs in the caller's mapper instead.
22
+ */
23
+
24
+ /**
25
+ * The shape every menu surface's `linkComponent` must accept.
26
+ *
27
+ * One type rather than a clone per component: these surfaces nest each other —
28
+ * a family card renders a `<MenuItem>`, a spotlight renders a signature block —
29
+ * and a caller passes ONE `next/link` down through all of them. Subtly
30
+ * different clones made those hand-offs fail to typecheck for no design reason.
31
+ */
32
+ type MenuLinkProps = {
33
+ href: string;
34
+ className?: string;
35
+ 'aria-label'?: string;
36
+ 'aria-current'?: 'page';
37
+ /** Intent tiles paint their hue on the link element itself. */
38
+ style?: CSSProperties;
39
+ children?: ReactNode;
40
+ };
41
+ type MenuLinkComponent = ComponentType<MenuLinkProps>;
42
+ /**
43
+ * A dish or a drink as every hub surface consumes it.
44
+ *
45
+ * `mentions` is the census count of review mentions and is populated on every
46
+ * item in the catalogue — it is the ranking signal the hub is built on. Do not
47
+ * confuse it with `review_count`, which counts full review objects and is zero
48
+ * for most items; a surface that ranks on `review_count` renders an empty page.
49
+ */
50
+ type MenuItemPreview = {
51
+ id: string;
52
+ slug: string;
53
+ name: string;
54
+ kind: 'dish' | 'drink';
55
+ description?: string;
56
+ /** API category code (pizza, pasta, cocktail, non-alcoholic, …). Localised by the caller. */
57
+ category_slug?: string;
58
+ sizes_and_prices?: MenuSizeAndPrice[];
59
+ /** Censused review mentions. The hub's ranking signal. */
60
+ mentions?: number;
61
+ /** Full review objects citing this item. Sparse — not a ranking signal. */
62
+ review_count?: number;
63
+ /** 0–100 recommendation score. */
64
+ score?: number;
65
+ /** The venue serving it. Menu items are always venue-scoped. */
66
+ place?: MenuItemPlaceRef;
67
+ /**
68
+ * Pre-resolved photo URL. Menu-item photography is AI-generated and hidden in
69
+ * production, so hub surfaces draw a typographic `<MenuFieldTile>` instead
70
+ * and leave this unset. It exists for the surfaces that legitimately show one
71
+ * (concept detail) and for consumers outside the consumer app.
72
+ */
73
+ photo_url?: string;
74
+ };
75
+ /** The venue attribution carried on a menu item. */
76
+ type MenuItemPlaceRef = {
77
+ slug: string;
78
+ name: string;
79
+ /** Pre-resolved venue thumbnail. Venue photography is real; item photography is not. */
80
+ thumbnail_url?: string;
81
+ };
82
+ /**
83
+ * A dish or drink concept — one item across every kitchen that makes it, under
84
+ * whatever name each of them uses. The page's keystone, and the one thing the
85
+ * catalogue can say that a single menu cannot.
86
+ */
87
+ type MenuConceptPreview = {
88
+ slug: string;
89
+ /** Canonical concept name, e.g. "Margherita". */
90
+ title: string;
91
+ kind: 'dish' | 'drink';
92
+ /** Venues serving it. */
93
+ venue_count?: number;
94
+ price_low?: number;
95
+ price_high?: number;
96
+ /** Distinct names it appears under across those menus. */
97
+ alias_count?: number;
98
+ /** Summed censused mentions across every venue's version. */
99
+ mentions?: number;
100
+ rows?: MenuConceptRow[];
101
+ };
102
+ /** One venue's version of a concept, as drawn by `<ConceptCompare>`. */
103
+ type MenuConceptRow = {
104
+ place_slug: string;
105
+ place_name: string;
106
+ /** What this kitchen calls it on its own menu. */
107
+ alias: string;
108
+ price: number;
109
+ mentions?: number;
110
+ rating?: number;
111
+ /** Venue type line, e.g. "pub". Localised by the caller. */
112
+ place_type?: string;
113
+ /** Neighbourhood, e.g. "Centru". */
114
+ place_area?: string;
115
+ /**
116
+ * Pre-resolved VENUE photograph.
117
+ *
118
+ * The row is place-led: five restaurants that disagree about a name, not five
119
+ * bars of similar length. A price bar was drawn here first and it measured
120
+ * the one thing already written beside it in words, while the venue — the
121
+ * thing you are actually choosing between — had no image at all.
122
+ */
123
+ place_thumbnail_url?: string;
124
+ open_now?: boolean;
125
+ /** Marks the row the surface should lead with (cheapest, or most named). */
126
+ featured?: boolean;
127
+ /** Portion, already formatted with its unit, e.g. "260g". */
128
+ portion_label?: string;
129
+ };
130
+ /**
131
+ * A browsable grouping of venues — a cuisine on the dishes tab, a drink family
132
+ * on the drinks tab. Both are sets of VENUES, never sets of items: cuisine
133
+ * exists only on places, so a card counting dishes cannot be built.
134
+ */
135
+ type MenuFamilyPreview = {
136
+ slug: string;
137
+ /** Display name, already localised by the caller. */
138
+ name: string;
139
+ /** Venues in this family. */
140
+ venue_count: number;
141
+ /**
142
+ * Facet chips. Counts are VENUES, not items — they come from the places
143
+ * facet narrowed by this family. The drinks tab has no equivalent facet, so
144
+ * its chips ship without counts; `count` is optional for that reason.
145
+ */
146
+ chips?: MenuFamilyChip[];
147
+ /** Price band across the family's venues. */
148
+ price_low?: number;
149
+ price_high?: number;
150
+ price_median?: number;
151
+ /** Pre-resolved venue photos for the mosaic. Venue photography only. */
152
+ venue_photo_urls?: string[];
153
+ };
154
+ type MenuFamilyChip = {
155
+ slug: string;
156
+ label: string;
157
+ /** Venue count. Absent where no facet backs the chip. */
158
+ count?: number;
159
+ };
160
+ /**
161
+ * A venue as the hub's venue-scoped surfaces consume it: the place itself plus
162
+ * the pre-resolved bits a hub row needs and `PlacePreview` does not carry —
163
+ * which signature to lead with, and where the row points.
164
+ */
165
+ type MenuVenuePreview = {
166
+ place: PlacePreview;
167
+ href: string;
168
+ /**
169
+ * The signature to draw. Callers pick it from `reviews_digest.must_try`
170
+ * rather than the surface re-deriving the rule on every row.
171
+ */
172
+ signature?: MenuVenueSignature;
173
+ /** Closing time as an already-formatted wall clock, e.g. "23:00". */
174
+ closes_at_label?: string;
175
+ };
176
+ /**
177
+ * The resolved signature on a venue row.
178
+ *
179
+ * It carries a whole `MenuItemPreview` rather than a flattened name and price,
180
+ * because every surface that draws one renders `<MenuItem>` — the same row a
181
+ * venue's own menu uses — and that component takes an item. Flattening it here
182
+ * was what let the hub's version drift into a different-looking row.
183
+ */
184
+ type MenuVenueSignature = {
185
+ item: MenuItemPreview;
186
+ /** The reviewers' reason, from the digest. Rendered in place of the description. */
187
+ why?: string;
188
+ href?: string;
189
+ /** The digest called this the venue's signature rather than merely frequent. */
190
+ isSignature?: boolean;
191
+ };
192
+ /**
193
+ * An intent tile — a family plus a constraint, resolving to a category page.
194
+ *
195
+ * Only about thirty-two of the thirty-eight cuisines have a page, so `href` is
196
+ * required: a tile that cannot resolve to a real destination is not rendered
197
+ * rather than linking nowhere.
198
+ */
199
+ type MenuIntentTile = {
200
+ slug: string;
201
+ label: string;
202
+ /** Supporting line, e.g. "247 kitchens". Already formatted by the caller. */
203
+ caption?: string;
204
+ href: string;
205
+ /**
206
+ * Finished CSS gradient for the tile's ground, e.g.
207
+ * `linear-gradient(150deg, …)`.
208
+ *
209
+ * A gradient rather than a token name, for the same reason `<MenuField>`
210
+ * takes one: the hue tables are app-owned, and a component referencing
211
+ * `var(--h-pizza)` renders an invisible tile in any consumer that does not
212
+ * happen to define that variable. A missing gradient falls back to `bg-muted`
213
+ * — visible and obviously unstyled, rather than white text on white.
214
+ */
215
+ gradient?: string;
216
+ /** Icon node, resolved by the caller — the DS does not pick icons for it. */
217
+ icon?: ReactNode;
218
+ };
219
+ /**
220
+ * One figure in the intent hero — a number the reader can tap to filter by.
221
+ *
222
+ * The value arrives pre-formatted because the figures are not all the same
223
+ * quantity: "247" is a count and "58" is a count of a subset, but a caller may
224
+ * also want "4 past midnight", and a component that formatted numbers itself
225
+ * would need the locale it is forbidden to own.
226
+ */
227
+ type MenuIntentFigure = {
228
+ key: string;
229
+ /** Pre-formatted, e.g. "247". */
230
+ value: string;
231
+ /** The unit, e.g. "kitchens" or "make a pizza". */
232
+ label: string;
233
+ /**
234
+ * The state this figure applies, as a URL.
235
+ *
236
+ * An `href` rather than only a callback because the filter state lives in the
237
+ * query string: a link is server-rendered, crawlable, middle-clickable and
238
+ * survives with no JavaScript, and `?open=now` is an indexable state we
239
+ * actively want followed. `onSelect` remains for consumers holding the state
240
+ * in memory; `href` wins when both are set.
241
+ */
242
+ href?: string;
243
+ /**
244
+ * Applies this figure as a filter without navigating. A figure with neither
245
+ * `href` nor `onSelect` renders as a static fact rather than as a control
246
+ * that does nothing — the total is usually one, since filtering to "all of
247
+ * them" is not a state.
248
+ */
249
+ onSelect?: () => void;
250
+ /** The filter this figure applies is already on. */
251
+ active?: boolean;
252
+ };
253
+ /**
254
+ * A filter chip on the intent page.
255
+ *
256
+ * Every chip counts KITCHENS, whatever facet backs it — a dietary chip reads
257
+ * "has a vegetarian dish" and counts venues, not dishes. Mixing venue counts
258
+ * and item counts in one row is what made the earlier version unreadable, so
259
+ * the type carries no unit: there is only ever one.
260
+ */
261
+ type MenuFilterChip = {
262
+ key: string;
263
+ label: string;
264
+ /** Kitchens this chip would leave. Absent where no facet backs it. */
265
+ count?: number;
266
+ active?: boolean;
267
+ /**
268
+ * Zero kitchens under the current filters — dimmed, but STILL TAPPABLE.
269
+ *
270
+ * Tapping swaps it in and drops whichever active filter conflicts, with a
271
+ * line saying which. A disabled chip would leave the reader guessing why the
272
+ * thing they want is unavailable; this one answers it by being pressed.
273
+ */
274
+ empty?: boolean;
275
+ /** Icon node, resolved by the caller. */
276
+ icon?: ReactNode;
277
+ };
278
+ /**
279
+ * A venue row on the intent page — the page's spine.
280
+ *
281
+ * Distinct from `MenuVenuePreview` (the hub's ranked row) in one structural
282
+ * way that is the whole point of the surface: the dish carries its own `href`,
283
+ * so the row has TWO targets. The row goes to the venue; the dish name opens
284
+ * the item drawer. `RankedVenueList` wraps its entire row in one link and
285
+ * therefore cannot express this, which is why this is a separate shape rather
286
+ * than a flag on that one.
287
+ */
288
+ type MenuKitchenPreview = {
289
+ place: PlacePreview;
290
+ /** The venue page. The row's primary target. */
291
+ href: string;
292
+ /** Venue type and district, composed and localised by the caller, e.g. "pizzeria · Centru". */
293
+ metaLabel?: string;
294
+ /**
295
+ * "kitchen until 23:00", or "closed · opens 12:00". Composed by the caller
296
+ * because the wording differs by scope — a bar is not a kitchen.
297
+ */
298
+ hoursLabel?: string;
299
+ dish?: MenuKitchenDish;
300
+ /** Pre-formatted, e.g. "62 lei". */
301
+ priceLabel?: string;
302
+ };
303
+ /** The dish a kitchen row leads with. The row's second target. */
304
+ type MenuKitchenDish = {
305
+ name: string;
306
+ /**
307
+ * Opens the item drawer. Omit for an inert dish line — a must-try the
308
+ * reviews named that has no live menu row behind it.
309
+ */
310
+ href?: string;
311
+ /** Kicker over the name: "MOST NAMED HERE", or "WHY IT MATCHES" when filtered. */
312
+ kicker: string;
313
+ /** Under the name: "17 reviews name it", or "mid-range · 300g" when matched. */
314
+ meta?: string;
315
+ /**
316
+ * This row is answering a filter rather than volunteering its most-named
317
+ * dish. Draws the match mark and tints the block, so a reader scanning a
318
+ * filtered list can see that every row changed its reason for being there.
319
+ */
320
+ matched?: boolean;
321
+ /** The filter it matched, e.g. "vegetarian". */
322
+ matchLabel?: string;
323
+ };
324
+ /**
325
+ * One row in a browse index — the complete set, at a glance.
326
+ *
327
+ * The index is the page's whole SEO job: it carries a crawlable link to every
328
+ * destination the city has, which is what frees the richer blocks below it to
329
+ * be progressive. So it holds nothing but what a reader needs to choose — a
330
+ * name, a count and a colour.
331
+ */
332
+ type MenuIndexEntry = {
333
+ slug: string;
334
+ name: string;
335
+ count: number;
336
+ href: string;
337
+ /** Finished CSS gradient for the mark. Falls back to the border colour. */
338
+ gradient?: string;
339
+ };
340
+ /**
341
+ * A family on the cuisines index — the hub's card, opened out sideways.
342
+ *
343
+ * Deliberately thin. The reader here is choosing a cuisine, not a venue, and
344
+ * every field the block does NOT carry is a decision the next page owns.
345
+ */
346
+ type MenuFamilyBlockPreview = {
347
+ slug: string;
348
+ name: string;
349
+ venueCount: number;
350
+ /** Facet chips. Labels only — the block never prints their counts. */
351
+ chips?: MenuFamilyChip[];
352
+ /** Composed price band, e.g. "22–86 lei". */
353
+ priceBandLabel?: string;
354
+ /** Venue photography only — item photography is AI-generated and never shown. */
355
+ photoUrls?: string[];
356
+ /**
357
+ * Venues, as kitchen rows. The block draws every one it is given — the caller
358
+ * decides how many, because how much of a claim the block makes is the page's
359
+ * decision, not the component's.
360
+ *
361
+ * The same `MenuKitchenPreview` the intent page's list takes, rendered by the
362
+ * same component — a family block that grew its own venue row would be the
363
+ * fourth near-identical row in the package, and they drift the moment one of
364
+ * them gets a fix.
365
+ */
366
+ venues?: MenuKitchenPreview[];
367
+ /** The family's own page. */
368
+ href: string;
369
+ };
370
+ /**
371
+ * One "loosen this" row on a dead end — a filter, and what dropping it returns.
372
+ *
373
+ * The count is the point: "drop Open now" is a guess, "+8 kitchens" is an
374
+ * answer, and a dead end that cannot say which filter costs the most is just
375
+ * an empty state with extra steps.
376
+ */
377
+ type MenuLoosenOption = {
378
+ key: string;
379
+ /** e.g. 'Drop "Open now"'. */
380
+ label: string;
381
+ /** e.g. "+8 kitchens". Pre-formatted. */
382
+ gainLabel: string;
383
+ /**
384
+ * The state without this filter, as a URL.
385
+ *
386
+ * An `href` because dropping a filter IS a navigation wherever the filter set
387
+ * lives in the query string — which makes it server-renderable, shareable and
388
+ * undoable with the back button. `onSelect` remains for consumers holding the
389
+ * state in memory; `href` wins when both are set.
390
+ */
391
+ href?: string;
392
+ onSelect?: () => void;
393
+ };
394
+ /** One question and its answer. Static prose — never an accordion; see `<QaBlock>`. */
395
+ type QaItem = {
396
+ question: string;
397
+ answer: string;
398
+ };
399
+ /** Formatters injected into every menu surface. The DS owns no `Intl` locale. */
400
+ type MenuFormatters = {
401
+ /** 24 → "24 lei". */
402
+ price: (value: number) => string;
403
+ /** 1717 → "1,717". */
404
+ number: (value: number) => string;
405
+ };
406
+ /** Strings shared by every menu-hub surface. */
407
+ type MenuHubLabels = {
408
+ /** Mention-count badge. Pass a translator that handles ICU pluralization. */
409
+ mentions: (count: number) => string;
410
+ /** Venue-count line. Pass a translator that handles ICU pluralization. */
411
+ venues: (count: number) => string;
412
+ /** "See all" affordance on a section header. */
413
+ seeAll: string;
414
+ /** Badge on a must-try row the digest called `signature`. */
415
+ signature: string;
416
+ openNow: string;
417
+ /** Prefixes a closing time, e.g. "until". */
418
+ until: string;
419
+ };
420
+
421
+ export { type MenuConceptPreview, type MenuConceptRow, type MenuFamilyBlockPreview, type MenuFamilyChip, type MenuFamilyPreview, type MenuFilterChip, type MenuFormatters, type MenuHubLabels, type MenuIndexEntry, type MenuIntentFigure, type MenuIntentTile, type MenuItemPlaceRef, type MenuItemPreview, type MenuKitchenDish, type MenuKitchenPreview, type MenuLinkComponent, type MenuLinkProps, type MenuLoosenOption, MenuSizeAndPrice, type MenuVenuePreview, type MenuVenueSignature, type QaItem };
@@ -0,0 +1 @@
1
+
@@ -1,4 +1,4 @@
1
- export { MotionBookmark, MotionCheck, MotionChevron, MotionCopy, MotionEye, MotionHeart, MotionPlayPause, MotionRefresh, MotionSearch, MotionShare, MotionSliders, MotionSort, MotionStar, MotionTheme } from '../chunk-TT3WFS6S.js';
2
- import '../chunk-2WDJR6K7.js';
1
+ export { MotionBookmark, MotionCheck, MotionChevron, MotionCopy, MotionEye, MotionHeart, MotionPlayPause, MotionRefresh, MotionSearch, MotionShare, MotionSliders, MotionSort, MotionStar, MotionTheme } from '../chunk-YOKHBAIH.js';
2
+ import '../chunk-G7IFL4Z4.js';
3
3
  import '../chunk-FEK5XGZW.js';
4
4
  import '../chunk-3GWWZKX7.js';
@@ -1,4 +1,4 @@
1
- import { beats, MotionIcon, returnTransition } from '../chunk-2WDJR6K7.js';
1
+ import { beats, pivot, MotionIcon, returnTransition } from '../chunk-G7IFL4Z4.js';
2
2
  import '../chunk-FEK5XGZW.js';
3
3
  import { __spreadProps, __spreadValues } from '../chunk-3GWWZKX7.js';
4
4
  import * as React from 'react';
@@ -28,11 +28,11 @@ function MotionHouse(props) {
28
28
  motion.path,
29
29
  {
30
30
  variants: houseRoof,
31
- style: { originX: "12px", originY: "10px" },
31
+ style: pivot(12, 10),
32
32
  d: "M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10"
33
33
  }
34
34
  ),
35
- /* @__PURE__ */ jsxs(motion.g, { variants: houseWalls, style: { originX: "12px", originY: "21px" }, children: [
35
+ /* @__PURE__ */ jsxs(motion.g, { variants: houseWalls, style: pivot(12, 21), children: [
36
36
  /* @__PURE__ */ jsx("path", { d: "M21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V10" }),
37
37
  /* @__PURE__ */ jsx("path", { d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" })
38
38
  ] })
@@ -40,12 +40,14 @@ function MotionHouse(props) {
40
40
  }
41
41
  var UTENSILS_TIMES = [0, 0.18, 0.72, 1];
42
42
  var SEPARATION = 5;
43
+ var FORK_LIFT = -1.5;
43
44
  var fork = {
44
- rest: { rotate: 0, x: 0, transition: returnTransition },
45
- active: { rotate: -44, x: -SEPARATION, transition: returnTransition },
45
+ rest: { rotate: 0, x: 0, y: 0, transition: returnTransition },
46
+ active: { rotate: -44, x: -SEPARATION, y: FORK_LIFT, transition: returnTransition },
46
47
  play: {
47
48
  rotate: [0, 6, -50, -44],
48
49
  x: [0, 0.8, -6, -SEPARATION],
50
+ y: [0, 0, FORK_LIFT - 0.3, FORK_LIFT],
49
51
  transition: beats(UTENSILS_TIMES, 0.6)
50
52
  }
51
53
  };
@@ -58,7 +60,17 @@ var knife = {
58
60
  transition: beats(UTENSILS_TIMES, 0.6)
59
61
  }
60
62
  };
61
- var CROSSING = { originX: "12px", originY: "12px" };
63
+ var CROSSING = pivot(12, 12);
64
+ var HANDLE_CROSSED = "M2.1 21.8 8.5 15.4";
65
+ var HANDLE_CLEAR = "M2.1 21.8 12 12";
66
+ var forkHandle = {
67
+ rest: { d: HANDLE_CROSSED, transition: returnTransition },
68
+ active: { d: HANDLE_CLEAR, transition: returnTransition },
69
+ play: {
70
+ d: [HANDLE_CROSSED, HANDLE_CROSSED, HANDLE_CLEAR, HANDLE_CLEAR],
71
+ transition: beats(UTENSILS_TIMES, 0.6)
72
+ }
73
+ };
62
74
  function MotionUtensils(props) {
63
75
  return /* @__PURE__ */ jsxs(MotionIcon, __spreadProps(__spreadValues({}, props), { children: [
64
76
  /* @__PURE__ */ jsx(
@@ -71,7 +83,7 @@ function MotionUtensils(props) {
71
83
  ),
72
84
  /* @__PURE__ */ jsxs(motion.g, { variants: fork, style: CROSSING, children: [
73
85
  /* @__PURE__ */ jsx("path", { d: "m16 2-2.3 2.3a3 3 0 0 0 0 4.2l1.8 1.8a3 3 0 0 0 4.2 0L22 8" }),
74
- /* @__PURE__ */ jsx("path", { d: "m2.1 21.8 6.4-6.3" }),
86
+ /* @__PURE__ */ jsx(motion.path, { variants: forkHandle, d: HANDLE_CROSSED }),
75
87
  /* @__PURE__ */ jsx("path", { d: "m19 5-7 7" })
76
88
  ] })
77
89
  ] }));
@@ -96,13 +108,13 @@ var pinDot = {
96
108
  }
97
109
  };
98
110
  function MotionMapPin(props) {
99
- return /* @__PURE__ */ jsx(MotionIcon, __spreadProps(__spreadValues({}, props), { children: /* @__PURE__ */ jsxs(motion.g, { variants: pinBody, style: { originX: "12px", originY: "22px" }, children: [
111
+ return /* @__PURE__ */ jsx(MotionIcon, __spreadProps(__spreadValues({}, props), { children: /* @__PURE__ */ jsxs(motion.g, { variants: pinBody, style: pivot(12, 22), children: [
100
112
  /* @__PURE__ */ jsx("path", { d: "M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0" }),
101
113
  /* @__PURE__ */ jsx(
102
114
  motion.circle,
103
115
  {
104
116
  variants: pinDot,
105
- style: { originX: "12px", originY: "10px" },
117
+ style: pivot(12, 10),
106
118
  fill: "currentColor",
107
119
  fillOpacity: 0,
108
120
  cx: "12",
@@ -23,6 +23,27 @@ type MotionIconVariant = "rest" | "active" | "play";
23
23
  declare function beats(times: number[], duration?: number): Transition;
24
24
  /** Leaving is not a performance — it returns the glyph, firmly and fast. */
25
25
  declare const returnTransition: Transition;
26
+ /**
27
+ * A moving part's pivot, in the icon's 24-unit VIEWBOX coordinates.
28
+ *
29
+ * Every part in this family wrote `style={{ originX: "12px", originY: "12px" }}`
30
+ * meaning "the centre of the icon" — and every one of them was pivoting
31
+ * somewhere else. Framer Motion sets `transform-box: fill-box` on any SVG
32
+ * element it transforms, which makes `transform-origin` relative to that
33
+ * element's OWN bounding box, not the viewBox. The chevron `m6 9 6 6 6-6` has
34
+ * its box at (6, 9), so "12px 12px" resolved to user-space (18, 21): a 180°
35
+ * flip around a point outside the arrow, which is why it visibly slid as it
36
+ * turned. A house's walls pivoting at its floor, a pin at its tip, a lens at
37
+ * its centre — all of them missed by the same offset for the same reason.
38
+ *
39
+ * `transform-box: view-box` is the single switch that makes px origins mean
40
+ * what they say. Declared once here so the next icon cannot get it wrong.
41
+ */
42
+ declare function pivot(x: number, y: number): {
43
+ readonly originX: `${number}px`;
44
+ readonly originY: `${number}px`;
45
+ readonly transformBox: "view-box";
46
+ };
26
47
  type PoseValues = Record<string, number>;
27
48
  /**
28
49
  * Builds the three variants for a moving part.
@@ -41,4 +62,4 @@ declare function MotionIcon({ active, replayToken, className, children, }: Motio
41
62
  children: React.ReactNode;
42
63
  }): react_jsx_runtime.JSX.Element;
43
64
 
44
- export { MotionIcon, type MotionIconProps, type MotionIconVariant, beats, poses, returnTransition, useIconGesture };
65
+ export { MotionIcon, type MotionIconProps, type MotionIconVariant, beats, pivot, poses, returnTransition, useIconGesture };
@@ -1,3 +1,3 @@
1
- export { MotionIcon, beats, poses, returnTransition, useIconGesture } from '../chunk-2WDJR6K7.js';
1
+ export { MotionIcon, beats, pivot, poses, returnTransition, useIconGesture } from '../chunk-G7IFL4Z4.js';
2
2
  import '../chunk-FEK5XGZW.js';
3
3
  import '../chunk-3GWWZKX7.js';
@@ -0,0 +1,40 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { MenuVenuePreview, MenuLinkComponent, MenuLinkProps } from './menu-types.js';
3
+ import 'react';
4
+ import './place-types.js';
5
+
6
+ /**
7
+ * OpenNowList — who is still serving, and until when.
8
+ *
9
+ * At half past ten this beats every other sort, which is why it is its own
10
+ * surface rather than a filter chip: the question is not "what is good" but
11
+ * "what is possible", and every other module on a hub answers the first one.
12
+ *
13
+ * The closing time arrives pre-formatted on the view model (`closes_at_label`)
14
+ * because the venue's own trading day can cross midnight — a bar closing at
15
+ * 04:00 closes on the following date — and resolving that needs the venue's
16
+ * timezone and the reader's clock, neither of which the DS has.
17
+ *
18
+ * The live dot is never the only signal that a venue is open: the closing pill
19
+ * carries the same fact in words, so the row reads without colour.
20
+ *
21
+ * Server-safe.
22
+ */
23
+ /** @deprecated Alias of `MenuLinkProps`; kept so existing imports keep working. */
24
+ type OpenNowListLinkProps = MenuLinkProps;
25
+ type OpenNowListProps = {
26
+ venues: MenuVenuePreview[];
27
+ /** Prefix on the closing pill, e.g. "until". */
28
+ untilLabel: string;
29
+ /** Composed meta line per venue, keyed by place slug, e.g. "pub · Centru". */
30
+ metaLabels?: Record<string, string>;
31
+ /** What the kitchen is still sending out, keyed by place slug. */
32
+ summaryLabels?: Record<string, string>;
33
+ /** Composed price band per venue, keyed by place slug, e.g. "19—62 lei". */
34
+ priceBandLabels?: Record<string, string>;
35
+ linkComponent?: MenuLinkComponent;
36
+ className?: string;
37
+ };
38
+ declare function OpenNowList({ venues, untilLabel, metaLabels, summaryLabels, priceBandLabels, linkComponent: LinkComponent, className, }: OpenNowListProps): react_jsx_runtime.JSX.Element;
39
+
40
+ export { OpenNowList, type OpenNowListLinkProps, type OpenNowListProps };
@@ -0,0 +1,69 @@
1
+ import { Separator } from '../chunk-WSRWFA6K.js';
2
+ import { cn } from '../chunk-FEK5XGZW.js';
3
+ import { __objRest, __spreadValues } from '../chunk-3GWWZKX7.js';
4
+ import { Clock, ChevronRight } from 'lucide-react';
5
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
+
7
+ function DefaultLink(_a) {
8
+ var _b = _a, { href } = _b, props = __objRest(_b, ["href"]);
9
+ return /* @__PURE__ */ jsx("a", __spreadValues({ href }, props));
10
+ }
11
+ function OpenNowList({
12
+ venues,
13
+ untilLabel,
14
+ metaLabels,
15
+ summaryLabels,
16
+ priceBandLabels,
17
+ linkComponent: LinkComponent = DefaultLink,
18
+ className
19
+ }) {
20
+ return /* @__PURE__ */ jsx("ul", { className: cn("flex flex-col", className), children: venues.map((venue, index) => {
21
+ const { place } = venue;
22
+ return /* @__PURE__ */ jsxs("li", { children: [
23
+ index > 0 && /* @__PURE__ */ jsx(Separator, {}),
24
+ /* @__PURE__ */ jsxs(
25
+ LinkComponent,
26
+ {
27
+ href: venue.href,
28
+ className: "flex flex-wrap items-center gap-x-3 gap-y-1.5 py-3 transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
29
+ children: [
30
+ /* @__PURE__ */ jsxs("span", { className: "relative h-14 w-14 shrink-0 overflow-hidden rounded-xl bg-[repeating-linear-gradient(135deg,oklch(0.78_0.018_55)_0_8px,oklch(0.84_0.012_58)_8px_16px)]", children: [
31
+ place.thumbnail_url && // eslint-disable-next-line @next/next/no-img-element
32
+ /* @__PURE__ */ jsx(
33
+ "img",
34
+ {
35
+ src: place.thumbnail_url,
36
+ alt: "",
37
+ className: "h-full w-full object-cover",
38
+ loading: "lazy"
39
+ }
40
+ ),
41
+ /* @__PURE__ */ jsx(
42
+ "i",
43
+ {
44
+ "aria-hidden": true,
45
+ className: "absolute right-1 top-1 h-[7px] w-[7px] rounded-full bg-[var(--free,currentColor)] ring-2 ring-white/80"
46
+ }
47
+ )
48
+ ] }),
49
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
50
+ /* @__PURE__ */ jsx("span", { className: "block text-sm font-bold leading-snug tracking-[-0.01em]", children: place.name }),
51
+ (metaLabels == null ? void 0 : metaLabels[place.slug]) && /* @__PURE__ */ jsx("span", { className: "mt-px block truncate text-[11.5px] text-muted-foreground", children: metaLabels[place.slug] })
52
+ ] }),
53
+ (summaryLabels == null ? void 0 : summaryLabels[place.slug]) && /* @__PURE__ */ jsx("span", { className: "order-last w-full truncate text-[12.5px] text-muted-foreground lg:order-none lg:w-auto lg:flex-1", children: summaryLabels[place.slug] }),
54
+ (venue.closes_at_label || (priceBandLabels == null ? void 0 : priceBandLabels[place.slug])) && /* @__PURE__ */ jsxs("span", { className: "flex shrink-0 flex-col items-end gap-1 lg:flex-row lg:items-center lg:gap-3", children: [
55
+ (priceBandLabels == null ? void 0 : priceBandLabels[place.slug]) && /* @__PURE__ */ jsx("span", { className: "text-[12.5px] tabular-nums text-muted-foreground", children: priceBandLabels[place.slug] }),
56
+ venue.closes_at_label && /* @__PURE__ */ jsxs("span", { className: "inline-flex h-6 items-center gap-1 rounded-full bg-[var(--free-subtle,var(--accent-subtle))] px-2.5 text-[11.5px] font-semibold tabular-nums", children: [
57
+ /* @__PURE__ */ jsx(Clock, { className: "h-[11px] w-[11px]", "aria-hidden": true }),
58
+ `${untilLabel} ${venue.closes_at_label}`
59
+ ] })
60
+ ] }),
61
+ /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4 shrink-0 text-muted-foreground", "aria-hidden": true })
62
+ ]
63
+ }
64
+ )
65
+ ] }, place.slug);
66
+ }) });
67
+ }
68
+
69
+ export { OpenNowList };