@bearmenu/ui 0.8.12 → 0.8.14

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 (299) hide show
  1. package/dist/{chunk-66MAPLXS.js → chunk-2ADCZXZJ.js} +1 -1
  2. package/dist/{chunk-2DEIXTBN.js → chunk-2ZXIXQIU.js} +1 -1
  3. package/dist/{chunk-YOKHBAIH.js → chunk-4OF4T4AI.js} +12 -11
  4. package/dist/{chunk-MZL7CGCL.js → chunk-4XDJXMLF.js} +2 -2
  5. package/dist/{chunk-YYGB4G4Q.js → chunk-CZK2MRS2.js} +1 -1
  6. package/dist/chunk-DJD3EMPK.js +126 -0
  7. package/dist/chunk-EZG2NELX.js +5 -0
  8. package/dist/{chunk-VAXOQS7W.js → chunk-F76NCVVR.js} +1 -1
  9. package/dist/{chunk-VG3PND3D.js → chunk-FMTJGO76.js} +1 -1
  10. package/dist/{chunk-UXWAOISG.js → chunk-FVSKGCK3.js} +2 -2
  11. package/dist/{chunk-2XCZJWTV.js → chunk-GNCWJWFE.js} +1 -1
  12. package/dist/{chunk-X3WMRKOW.js → chunk-HDR7V6IZ.js} +1 -1
  13. package/dist/{chunk-HSIYYA2E.js → chunk-HQU6QVAN.js} +5 -5
  14. package/dist/chunk-I5V446BX.js +27 -0
  15. package/dist/{chunk-WCTHZKU7.js → chunk-ICOU5QHT.js} +1 -1
  16. package/dist/{chunk-G7IFL4Z4.js → chunk-L3VEMP5A.js} +57 -9
  17. package/dist/chunk-LQWGZIYA.js +5 -0
  18. package/dist/{chunk-XNA2INVQ.js → chunk-MCOHLWRS.js} +2 -2
  19. package/dist/{chunk-3GWWZKX7.js → chunk-MMUBH76A.js} +21 -1
  20. package/dist/{chunk-ZDAHKI3A.js → chunk-MWB7S54O.js} +1 -1
  21. package/dist/{chunk-D5QHEHXJ.js → chunk-NMEVJT5Q.js} +1 -1
  22. package/dist/{chunk-3TNPSN4D.js → chunk-NSNEANOQ.js} +2 -2
  23. package/dist/{chunk-WSRWFA6K.js → chunk-NWZGSLPU.js} +1 -1
  24. package/dist/{chunk-LEAYBICI.js → chunk-OIV7B44M.js} +1 -1
  25. package/dist/{chunk-2CXU5WZO.js → chunk-P43B3ONY.js} +1 -1
  26. package/dist/{chunk-FRPPIXMY.js → chunk-PDTPO6NG.js} +1 -1
  27. package/dist/{chunk-6XMYEZVR.js → chunk-PX6RICS5.js} +1 -1
  28. package/dist/{chunk-6TEEY5YA.js → chunk-S33K4RC5.js} +1 -1
  29. package/dist/{chunk-3VMDCCCX.js → chunk-SC2VUTUI.js} +1 -1
  30. package/dist/{chunk-24RFI4RQ.js → chunk-SXQZ7DLH.js} +4 -4
  31. package/dist/{chunk-UDTG3RSE.js → chunk-T6S2ZBV6.js} +2 -2
  32. package/dist/{chunk-DTXNDLSE.js → chunk-WYTJR2YU.js} +16 -1
  33. package/dist/{chunk-QXHMPJ35.js → chunk-XBBEX4SF.js} +1 -1
  34. package/dist/chunk-XCB3F6SF.js +16 -0
  35. package/dist/{chunk-7BWTYWM3.js → chunk-ZLMRVBD7.js} +1 -1
  36. package/dist/{chunk-7A42J73Q.js → chunk-ZLPWEHZO.js} +1 -1
  37. package/dist/components/accordion.js +1 -1
  38. package/dist/components/alert-dialog.js +2 -2
  39. package/dist/components/alert.d.ts +2 -2
  40. package/dist/components/alert.js +2 -2
  41. package/dist/components/aspect-ratio.js +2 -2
  42. package/dist/components/avatar.js +1 -1
  43. package/dist/components/badge.d.ts +1 -1
  44. package/dist/components/badge.js +2 -2
  45. package/dist/components/breadcrumb.js +1 -1
  46. package/dist/components/button-group.js +1 -1
  47. package/dist/components/button.d.ts +1 -1
  48. package/dist/components/button.js +2 -2
  49. package/dist/components/calendar.js +2 -2
  50. package/dist/components/card.d.ts +2 -2
  51. package/dist/components/card.js +3 -3
  52. package/dist/components/carousel.js +2 -2
  53. package/dist/components/category-deck.js +2 -2
  54. package/dist/components/checkbox.js +2 -2
  55. package/dist/components/circular-progress.js +1 -1
  56. package/dist/components/collapsible.js +1 -1
  57. package/dist/components/combobox.js +5 -5
  58. package/dist/components/command.d.ts +15 -15
  59. package/dist/components/command.js +3 -3
  60. package/dist/components/concept-card.js +2 -2
  61. package/dist/components/concept-compare-rail.js +3 -3
  62. package/dist/components/concept-compare.d.ts +2 -1
  63. package/dist/components/concept-compare.js +5 -5
  64. package/dist/components/concept-convergence.js +4 -4
  65. package/dist/components/convergence.js +3 -3
  66. package/dist/components/coverflow.js +1 -1
  67. package/dist/components/currency-input.js +2 -2
  68. package/dist/components/date-input.d.ts +1 -1
  69. package/dist/components/date-input.js +2 -2
  70. package/dist/components/day-rail-chip.js +2 -2
  71. package/dist/components/description-list.js +1 -1
  72. package/dist/components/dialog.js +2 -2
  73. package/dist/components/doorway-tiles.d.ts +57 -0
  74. package/dist/components/doorway-tiles.js +86 -0
  75. package/dist/components/drawer.js +1 -1
  76. package/dist/components/drift-rail.js +1 -1
  77. package/dist/components/dropdown-menu.js +1 -1
  78. package/dist/components/empty-state.js +3 -3
  79. package/dist/components/event-card.js +4 -4
  80. package/dist/components/event-compact-row.js +4 -4
  81. package/dist/components/event-day-header.js +2 -2
  82. package/dist/components/event-day-separator.js +3 -3
  83. package/dist/components/event-detail-panel.js +4 -4
  84. package/dist/components/event-hub-hero.js +2 -2
  85. package/dist/components/event-index-row.js +4 -4
  86. package/dist/components/event-list-row-wide.js +5 -5
  87. package/dist/components/event-list-row.js +5 -5
  88. package/dist/components/event-poster-fallback.js +1 -1
  89. package/dist/components/event-types.js +1 -1
  90. package/dist/components/events-empty-state.js +1 -1
  91. package/dist/components/explainer-steps.js +1 -1
  92. package/dist/components/facet-group.js +2 -2
  93. package/dist/components/featured-rail-item.js +2 -2
  94. package/dist/components/field.js +2 -2
  95. package/dist/components/filter-category-row.js +3 -3
  96. package/dist/components/form.js +2 -2
  97. package/dist/components/glass-ground.js +2 -2
  98. package/dist/components/grade-atom.d.ts +3 -0
  99. package/dist/components/grade-atom.js +4 -0
  100. package/dist/components/haptic-button.js +2 -2
  101. package/dist/components/hour-band.js +4 -4
  102. package/dist/components/hour-group.js +4 -4
  103. package/dist/components/hub-surface.js +1 -1
  104. package/dist/components/hue-chip.js +1 -1
  105. package/dist/components/icon.js +1 -1
  106. package/dist/components/input-group.js +1 -1
  107. package/dist/components/input-otp.js +1 -1
  108. package/dist/components/input.js +2 -2
  109. package/dist/components/item.d.ts +2 -2
  110. package/dist/components/item.js +1 -1
  111. package/dist/components/kbd.js +1 -1
  112. package/dist/components/kitchen-dossier.d.ts +2 -1
  113. package/dist/components/kitchen-dossier.js +3 -3
  114. package/dist/components/label.js +2 -2
  115. package/dist/components/lazy-markdown.js +1 -1
  116. package/dist/components/link-button.js +2 -2
  117. package/dist/components/markdown-renderer.js +1 -1
  118. package/dist/components/menu-dead-end.d.ts +2 -1
  119. package/dist/components/menu-dead-end.js +1 -1
  120. package/dist/components/menu-family-block.d.ts +2 -1
  121. package/dist/components/menu-family-block.js +14 -8
  122. package/dist/components/menu-family-card.d.ts +2 -1
  123. package/dist/components/menu-family-card.js +1 -1
  124. package/dist/components/menu-field.js +1 -1
  125. package/dist/components/menu-filter-chips.d.ts +2 -1
  126. package/dist/components/menu-filter-chips.js +3 -3
  127. package/dist/components/menu-group-rows.d.ts +2 -1
  128. package/dist/components/menu-group-rows.js +2 -2
  129. package/dist/components/menu-hub-hero.js +1 -1
  130. package/dist/components/menu-index-header.d.ts +2 -1
  131. package/dist/components/menu-index-header.js +2 -2
  132. package/dist/components/menu-index-list.d.ts +9 -8
  133. package/dist/components/menu-index-list.js +4 -4
  134. package/dist/components/menu-intent-hero.d.ts +3 -2
  135. package/dist/components/menu-intent-hero.js +1 -1
  136. package/dist/components/menu-item.d.ts +2 -1
  137. package/dist/components/menu-item.js +4 -4
  138. package/dist/components/menu-kitchen-list.d.ts +2 -1
  139. package/dist/components/menu-kitchen-list.js +4 -4
  140. package/dist/components/menu-mocks.d.ts +16 -5
  141. package/dist/components/menu-mocks.js +23 -2
  142. package/dist/components/menu-types.d.ts +21 -34
  143. package/dist/components/motion-icon-actions.d.ts +27 -0
  144. package/dist/components/motion-icon-actions.js +336 -0
  145. package/dist/components/motion-icon-controls.js +3 -3
  146. package/dist/components/motion-icon-set.d.ts +13 -1
  147. package/dist/components/motion-icon-set.js +197 -6
  148. package/dist/components/motion-icon.d.ts +15 -1
  149. package/dist/components/motion-icon.js +2 -2
  150. package/dist/components/multi-select-combobox.js +7 -7
  151. package/dist/components/multi-select-menu.js +2 -2
  152. package/dist/components/native-select.js +1 -1
  153. package/dist/components/open-now-list.d.ts +2 -1
  154. package/dist/components/open-now-list.js +2 -2
  155. package/dist/components/pagination.js +2 -2
  156. package/dist/components/phone-frame.js +1 -1
  157. package/dist/components/photo-ground.js +1 -1
  158. package/dist/components/place-about-tab.d.ts +2 -1
  159. package/dist/components/place-about-tab.js +6 -6
  160. package/dist/components/place-card.d.ts +94 -12
  161. package/dist/components/place-card.js +203 -128
  162. package/dist/components/place-details-mobile.d.ts +2 -1
  163. package/dist/components/place-details-mobile.js +7 -7
  164. package/dist/components/place-schedule-week.d.ts +2 -1
  165. package/dist/components/place-schedule-week.js +3 -3
  166. package/dist/components/place-types.d.ts +3 -265
  167. package/dist/components/popover.js +2 -2
  168. package/dist/components/poster-stack.js +1 -1
  169. package/dist/components/poster-tape.js +1 -1
  170. package/dist/components/poster-wall.d.ts +22 -6
  171. package/dist/components/poster-wall.js +197 -12
  172. package/dist/components/progress.js +1 -1
  173. package/dist/components/qa-block.d.ts +2 -1
  174. package/dist/components/qa-block.js +1 -1
  175. package/dist/components/quiet-stretch.js +1 -1
  176. package/dist/components/radio-group.js +1 -1
  177. package/dist/components/rating.js +1 -1
  178. package/dist/components/scroll-area.js +1 -1
  179. package/dist/components/scroll-fade.js +2 -2
  180. package/dist/components/search-bar.js +1 -1
  181. package/dist/components/searchable-select.js +5 -5
  182. package/dist/components/section-header.js +2 -2
  183. package/dist/components/segmented-nav.d.ts +24 -4
  184. package/dist/components/segmented-nav.js +11 -2
  185. package/dist/components/select.js +1 -1
  186. package/dist/components/selection-bar.js +1 -1
  187. package/dist/components/separator.js +2 -2
  188. package/dist/components/sheet.js +1 -1
  189. package/dist/components/signature-chip.js +3 -3
  190. package/dist/components/skeleton-card.js +3 -3
  191. package/dist/components/skeleton.js +2 -2
  192. package/dist/components/slider.js +1 -1
  193. package/dist/components/sliding-panels.js +2 -2
  194. package/dist/components/snap-rail.d.ts +0 -5
  195. package/dist/components/snap-rail.js +2 -2
  196. package/dist/components/sonner.js +1 -1
  197. package/dist/components/sort-option-list.js +1 -1
  198. package/dist/components/spinner.d.ts +2 -2
  199. package/dist/components/spinner.js +1 -1
  200. package/dist/components/stat-banner.js +1 -1
  201. package/dist/components/stepper.js +1 -1
  202. package/dist/components/sticky-container.js +1 -1
  203. package/dist/components/story.js +1 -1
  204. package/dist/components/switch.js +1 -1
  205. package/dist/components/table.js +1 -1
  206. package/dist/components/tabs.js +2 -2
  207. package/dist/components/tag-group.js +2 -2
  208. package/dist/components/tag.d.ts +2 -2
  209. package/dist/components/tag.js +2 -2
  210. package/dist/components/text.d.ts +3 -3
  211. package/dist/components/text.js +2 -2
  212. package/dist/components/textarea.js +1 -1
  213. package/dist/components/timeline-band.js +4 -4
  214. package/dist/components/timeline-playhead.js +4 -4
  215. package/dist/components/toggle-group.js +2 -2
  216. package/dist/components/toggle.js +2 -2
  217. package/dist/components/tooltip.js +1 -1
  218. package/dist/components/top-comparison.d.ts +2 -1
  219. package/dist/components/top-comparison.js +1 -1
  220. package/dist/components/top-cross-link.d.ts +2 -1
  221. package/dist/components/top-cross-link.js +1 -1
  222. package/dist/components/top-ranking-entry.d.ts +2 -1
  223. package/dist/components/top-ranking-entry.js +6 -6
  224. package/dist/components/top-ranking-hero.d.ts +2 -1
  225. package/dist/components/top-ranking-hero.js +1 -1
  226. package/dist/components/top-ranking-legend.js +1 -1
  227. package/dist/components/top-types.d.ts +3 -1
  228. package/dist/components/use-mobile.js +1 -1
  229. package/dist/components/venue-events-block.js +5 -5
  230. package/dist/components/venue-grade-dimensions.d.ts +2 -1
  231. package/dist/components/venue-grade-dimensions.js +1 -1
  232. package/dist/components/venue-quotes.d.ts +2 -1
  233. package/dist/components/venue-quotes.js +1 -1
  234. package/dist/components/venue-wall.d.ts +2 -1
  235. package/dist/components/venue-wall.js +1 -1
  236. package/dist/components/weekday-theme-banner.js +1 -1
  237. package/dist/components/weekend-panel.js +4 -4
  238. package/dist/hooks/use-drag-swipe.js +1 -1
  239. package/dist/hooks/use-haptic.js +1 -1
  240. package/dist/hooks/use-media-query.js +1 -1
  241. package/dist/hooks/use-mobile.js +1 -1
  242. package/dist/hooks/use-mounted-theme.js +1 -1
  243. package/dist/hooks/use-sticky.js +1 -1
  244. package/dist/lib/card-hover.js +2 -18
  245. package/dist/lib/grade.d.ts +54 -0
  246. package/dist/lib/grade.js +2 -0
  247. package/dist/lib/icon-depth.d.ts +35 -0
  248. package/dist/lib/icon-depth.js +2 -0
  249. package/dist/lib/icon-registry.js +1 -1
  250. package/dist/lib/motion.js +2 -2
  251. package/dist/lib/overlay-pill.d.ts +30 -0
  252. package/dist/lib/overlay-pill.js +2 -0
  253. package/dist/lib/utils.js +1 -1
  254. package/dist/place-types-BHdeMMiG.d.ts +317 -0
  255. package/package.json +1 -1
  256. package/src/components/colour-dictionary.stories.tsx +268 -82
  257. package/src/components/doorway-tiles.stories.tsx +83 -0
  258. package/src/components/doorway-tiles.test.tsx +51 -0
  259. package/src/components/doorway-tiles.tsx +225 -0
  260. package/src/components/grade-atom.stories.tsx +81 -0
  261. package/src/components/grade-atom.tsx +256 -0
  262. package/src/components/hue-chip.stories.tsx +2 -2
  263. package/src/components/menu-family-block.stories.tsx +29 -0
  264. package/src/components/menu-family-block.test.tsx +51 -0
  265. package/src/components/menu-family-block.tsx +20 -4
  266. package/src/components/menu-index-list.tsx +7 -7
  267. package/src/components/menu-intent-hero.tsx +1 -1
  268. package/src/components/menu-kitchen-list.tsx +4 -1
  269. package/src/components/menu-mocks.ts +39 -3
  270. package/src/components/menu-types.ts +17 -33
  271. package/src/components/motion-icon-actions.test.tsx +115 -0
  272. package/src/components/motion-icon-actions.tsx +580 -0
  273. package/src/components/motion-icon-controls.test.tsx +1 -0
  274. package/src/components/motion-icon-controls.tsx +18 -13
  275. package/src/components/motion-icon-set.stories.tsx +62 -3
  276. package/src/components/motion-icon-set.tsx +349 -2
  277. package/src/components/motion-icon.test.tsx +180 -1
  278. package/src/components/motion-icon.tsx +97 -8
  279. package/src/components/place-card.stories.tsx +66 -76
  280. package/src/components/place-card.test.tsx +188 -100
  281. package/src/components/place-card.tsx +356 -148
  282. package/src/components/place-types.ts +17 -4
  283. package/src/components/poster-wall.stories.tsx +84 -0
  284. package/src/components/poster-wall.test.tsx +94 -3
  285. package/src/components/poster-wall.tsx +257 -9
  286. package/src/components/segmented-nav.stories.tsx +72 -1
  287. package/src/components/segmented-nav.test.tsx +96 -0
  288. package/src/components/segmented-nav.tsx +36 -4
  289. package/src/components/snap-rail.tsx +19 -0
  290. package/src/globals.css +26 -0
  291. package/src/lib/grade.test.ts +82 -0
  292. package/src/lib/grade.ts +76 -0
  293. package/src/lib/icon-depth.ts +35 -0
  294. package/src/lib/overlay-pill.ts +29 -0
  295. package/dist/components/menu-family-tiles.d.ts +0 -19
  296. package/dist/components/menu-family-tiles.js +0 -57
  297. package/src/components/menu-family-tiles.stories.tsx +0 -54
  298. package/src/components/menu-family-tiles.test.tsx +0 -30
  299. package/src/components/menu-family-tiles.tsx +0 -132
@@ -152,6 +152,8 @@ export function MenuFamilyBlock({
152
152
  // see — the photograph is one of them, not a venue on its own.
153
153
  const venues = family.venues ?? [];
154
154
  const overflow = Math.max(family.venueCount - venues.length, 0);
155
+ // The corner pill: the caller's badge when it has one, else the overflow.
156
+ const pill = family.badge ?? (overflow > 0 ? labels.more(overflow) : undefined);
155
157
  const initial = (family.name.match(/\p{L}/u)?.[0] ?? "").toLocaleUpperCase();
156
158
 
157
159
  return (
@@ -182,19 +184,33 @@ export function MenuFamilyBlock({
182
184
  TEXT_SCRIM
183
185
  )}
184
186
  >
185
- {overflow > 0 && (
187
+ {pill && (
186
188
  <span className="absolute right-3.5 top-3.5 rounded-full bg-[oklch(0.16_0.012_55_/_0.78)] px-2.5 py-1 text-[11px] font-semibold text-white lg:static lg:mb-3 lg:self-start">
187
- {labels.more(overflow)}
189
+ {pill}
188
190
  </span>
189
191
  )}
190
192
 
193
+ {family.kicker && (
194
+ <span className="mb-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-white/70">
195
+ {family.kicker}
196
+ </span>
197
+ )}
191
198
  <h3 className="text-balance font-serif text-[26px] font-semibold leading-tight tracking-[-0.02em] text-white lg:text-[30px]">
192
199
  {family.name}
193
200
  </h3>
194
- {/* The count and the band on one line: two facts about the family,
195
- read together — "96 kitchens, 14–46 lei". */}
201
+ {/* The count, the facts and the band on one line: a few facts about
202
+ the family, read together — "96 kitchens, 14–46 lei" or
203
+ "70 kitchens · 50 names · 8–44 lei". */}
196
204
  <span className="mt-1 flex flex-wrap items-center gap-x-2.5 gap-y-1 text-[13px] font-medium tabular-nums text-white/70">
197
205
  {labels.venueUnit(family.venueCount)}
206
+ {family.facts?.map((fact) => (
207
+ <span key={fact} className="inline-flex items-center gap-2.5">
208
+ <span aria-hidden className="text-white/40">
209
+ ·
210
+ </span>
211
+ {fact}
212
+ </span>
213
+ ))}
198
214
  {family.priceBandLabel && (
199
215
  <span
200
216
  className="inline-flex items-center gap-1"
@@ -14,19 +14,19 @@ import type { MenuIndexEntry, MenuLinkComponent, MenuLinkProps } from "./menu-ty
14
14
  * ## This block is why the page can afford to be progressive
15
15
  *
16
16
  * It carries a crawlable link to EVERY destination the city has — all
17
- * thirty-eight cuisines, not the eight the page draws in full above. That
18
- * makes it the page's entire link-equity job, which in turn means the rich
19
- * blocks are free to load eight at a time behind a "show more": nothing a
20
- * crawler needs is hidden behind that control, because it is all here.
17
+ * thirty-eight cuisines, in the markup at every width. That makes it the
18
+ * page's entire link-equity job, which in turn means the rich cards above
19
+ * are free to be a swipe rail on a phone: nothing a crawler needs waits on a
20
+ * swipe, because it is all here.
21
21
  *
22
22
  * The phone's own "show all" is CSS, not markup: every row is in the DOM and
23
23
  * the ones past `collapsedCount` are hidden below `lg` until the button is
24
24
  * pressed. A list that mounted its tail on tap would have taken twenty-six
25
25
  * links out of the page for the crawler that never taps.
26
26
  *
27
- * Get this backwards — put the whole set behind the disclosure and only eight
28
- * links in the markup and the page silently stops linking to thirty pages
29
- * it is supposed to be the index for.
27
+ * Get this backwards — mount the rows behind the expander on tap — and the
28
+ * page silently stops linking to twenty-six pages it is supposed to be the
29
+ * index for.
30
30
  *
31
31
  * ## Ordered by count, and the counts overlap
32
32
  *
@@ -55,7 +55,7 @@ export type MenuIntentHeroProps = {
55
55
  * Finished CSS gradient for the ground, e.g. `linear-gradient(140deg, …)`.
56
56
  *
57
57
  * A gradient rather than a token name, for the same reason `<MenuField>` and
58
- * `<MenuFamilyTiles>` take one: the hue tables are app-owned, and a component
58
+ * `<DoorwayTiles>` take one: the hue tables are app-owned, and a component
59
59
  * referencing `var(--h-pizza)` renders an unstyled panel in any consumer that
60
60
  * does not define it. Missing, the panel falls back to the foreground colour
61
61
  * — visibly unstyled rather than white text on white.
@@ -150,7 +150,10 @@ function KitchenRow({
150
150
  </span>
151
151
 
152
152
  <div className="flex min-w-0 flex-1 flex-col gap-0.5 lg:w-[226px] lg:flex-none">
153
- <span className="truncate text-[14.5px] font-bold tracking-[-0.01em] lg:text-[15.5px]">
153
+ {/* One line with an ellipsis on a phone, where the row is the whole
154
+ width; two lines from `lg`, where the column is 226px and
155
+ "ADRIATICA GELATERIA ITALIANA" would otherwise lose its half. */}
156
+ <span className="truncate text-[14.5px] font-bold tracking-[-0.01em] lg:line-clamp-2 lg:whitespace-normal lg:text-[15.5px] lg:leading-tight">
154
157
  {place.name}
155
158
  </span>
156
159
  <span className="flex items-center gap-1 truncate text-xs text-muted-foreground">
@@ -25,13 +25,13 @@ import type {
25
25
  MenuFilterChip,
26
26
  MenuIndexEntry,
27
27
  MenuIntentFigure,
28
- MenuFamilyTile,
29
28
  MenuItemPreview,
30
29
  MenuKitchenPreview,
31
30
  MenuLoosenOption,
32
31
  MenuVenuePreview,
33
32
  QaItem,
34
33
  } from "./menu-types";
34
+ import type { DoorwayTile } from "./doorway-tiles";
35
35
  import type { PlacePreview } from "./place-types";
36
36
 
37
37
  export const mockGradients = {
@@ -387,7 +387,7 @@ const familyPhoto = (hue: number) =>
387
387
  * venue's, never a plate's; the last one ships without, so the story shows
388
388
  * the initial fallback beside six real tiles.
389
389
  */
390
- export const mockFamilyTiles: MenuFamilyTile[] = [
390
+ export const mockFamilyTiles: DoorwayTile[] = [
391
391
  { slug: "italian", label: "Italian", countLabel: "66", href: "/menu/cuisines/italian", gradient: familyGradient(38), photoUrl: familyPhoto(30) },
392
392
  { slug: "romanian", label: "Romanian", countLabel: "64", href: "/menu/cuisines/romanian", gradient: familyGradient(25), photoUrl: familyPhoto(10) },
393
393
  { slug: "breakfast-brunch", label: "Breakfast & brunch", countLabel: "64", href: "/menu/cuisines/breakfast-brunch", gradient: familyGradient(82), photoUrl: familyPhoto(45) },
@@ -398,7 +398,7 @@ export const mockFamilyTiles: MenuFamilyTile[] = [
398
398
  ];
399
399
 
400
400
  /** The closing tile: stone, so it reads as the index and not as an eighth cuisine. */
401
- export const mockFamilyTilesAll: MenuFamilyTile = {
401
+ export const mockFamilyTilesAll: DoorwayTile = {
402
402
  slug: "all",
403
403
  label: "All 38 cuisines",
404
404
  countLabel: "The full index",
@@ -407,6 +407,42 @@ export const mockFamilyTilesAll: MenuFamilyTile = {
407
407
  photoUrl: familyPhoto(60),
408
408
  };
409
409
 
410
+ /** An occasion's ground: a mood hue over a room photograph. */
411
+ const occasionGradient = (l: number, c: number, h: number) =>
412
+ `linear-gradient(150deg, oklch(${l} ${c} ${h}), color-mix(in oklab, oklch(${l} ${c} ${h}) 82%, black))`;
413
+
414
+ /** A room photograph stand-in with the mid-tone structure multiply needs. */
415
+ const occasionPhoto = (hue: number) =>
416
+ "data:image/svg+xml;utf8," +
417
+ encodeURIComponent(
418
+ `<svg xmlns="http://www.w3.org/2000/svg" width="640" height="320"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${hue} 20% 88%)"/><stop offset="1" stop-color="hsl(${hue} 18% 52%)"/></linearGradient></defs><rect width="640" height="320" fill="url(#g)"/><circle cx="420" cy="120" r="70" fill="white" opacity=".6"/><rect x="60" y="200" width="220" height="80" fill="hsl(${hue} 15% 40%)" opacity=".5"/></svg>`
419
+ );
420
+
421
+ /**
422
+ * The homepage's seven occasions in the day's order, ground treatment: the
423
+ * mood's hue multiplied over a room. The last one ships without a photograph
424
+ * so the story shows the initial on the bare hue.
425
+ */
426
+ export const mockOccasionTiles: DoorwayTile[] = [
427
+ { slug: "brunch", label: "Brunch", countLabel: "314 places", href: "/menu/cuisines/breakfast-brunch", gradient: occasionGradient(0.62, 0.14, 82), photoUrl: occasionPhoto(40) },
428
+ { slug: "laptop-and-coffee", label: "Laptop and coffee", countLabel: "52 places", href: "/occasions/laptop-and-coffee", gradient: occasionGradient(0.44, 0.1, 52), photoUrl: occasionPhoto(30) },
429
+ { slug: "quiet", label: "Somewhere quiet", countLabel: "108 places", href: "/occasions/quiet", gradient: occasionGradient(0.38, 0.09, 255), photoUrl: occasionPhoto(210) },
430
+ { slug: "something-sweet", label: "Something sweet", countLabel: "120 places", href: "/occasions/something-sweet", gradient: occasionGradient(0.5, 0.08, 335), photoUrl: occasionPhoto(330) },
431
+ { slug: "with-friends", label: "With friends", countLabel: "158 places", href: "/occasions/with-friends", gradient: occasionGradient(0.6, 0.14, 358), photoUrl: occasionPhoto(10) },
432
+ { slug: "date-night", label: "Date night", countLabel: "53 places", href: "/occasions/date-night", gradient: occasionGradient(0.44, 0.15, 18), photoUrl: occasionPhoto(0) },
433
+ { slug: "late-night", label: "Late night", countLabel: "59 places", href: "/occasions/late-night", gradient: occasionGradient(0.34, 0.1, 300) },
434
+ ];
435
+
436
+ /** The closing tile: stone, so it reads as the index and not as an eighth mood. */
437
+ export const mockOccasionTilesAll: DoorwayTile = {
438
+ slug: "all",
439
+ label: "All occasions",
440
+ countLabel: "14 sets",
441
+ href: "/occasions",
442
+ gradient: "linear-gradient(150deg, oklch(0.50 0.03 70), oklch(0.40 0.028 64))",
443
+ photoUrl: occasionPhoto(60),
444
+ };
445
+
410
446
  /** Formatters the stories pass. Real callers inject locale-aware ones. */
411
447
  export const mockFormatters = {
412
448
  price: (value: number) => `${value} lei`,
@@ -216,38 +216,6 @@ export type MenuVenueSignature = {
216
216
  isSignature?: boolean
217
217
  }
218
218
 
219
- /**
220
- * A family tile — a cuisine on the dishes tab, a going-out family on the
221
- * drinks tab — as the hub's doorway grid draws it: a hue, a name, a venue
222
- * count and one venue's photograph.
223
- *
224
- * `href` is required: a tile that cannot resolve to a real destination is not
225
- * rendered rather than linking nowhere.
226
- */
227
- export type MenuFamilyTile = {
228
- slug: string
229
- label: string
230
- /**
231
- * The venue count, already formatted by the caller — "64", "1,204" — because
232
- * thousands separators are locale-dependent and the DS owns no locale.
233
- */
234
- countLabel?: string
235
- href: string
236
- /**
237
- * Finished CSS gradient for the tile's ground, e.g.
238
- * `linear-gradient(150deg, …)`.
239
- *
240
- * A gradient rather than a token name, for the same reason `<MenuField>`
241
- * takes one: the hue tables are app-owned, and a component referencing
242
- * `var(--h-pizza)` renders an invisible tile in any consumer that does not
243
- * happen to define that variable. A missing gradient falls back to `bg-muted`
244
- * — visible and obviously unstyled, rather than white text on white.
245
- */
246
- gradient?: string
247
- /** Pre-resolved VENUE photograph. Venue photography is real; item photography is not. */
248
- photoUrl?: string
249
- }
250
-
251
219
  /**
252
220
  * One figure in the intent hero — a number the reader can tap to filter by.
253
221
  *
@@ -380,7 +348,9 @@ export type MenuIndexEntry = {
380
348
  }
381
349
 
382
350
  /**
383
- * A family on the cuisines index — the hub's card, opened out sideways.
351
+ * A family on the cuisines index — the hub's card, opened out sideways — and,
352
+ * with `kicker` / `facts` / `badge`, a concept on the dishes index: the same
353
+ * card with the unit swapped, as the design draws it.
384
354
  *
385
355
  * Deliberately thin. The reader here is choosing a cuisine, not a venue, and
386
356
  * every field the block does NOT carry is a decision the next page owns.
@@ -388,7 +358,21 @@ export type MenuIndexEntry = {
388
358
  export type MenuFamilyBlockPreview = {
389
359
  slug: string
390
360
  name: string
361
+ /** Small caps over the name, e.g. "DESSERT CONCEPT". Composed by the caller. */
362
+ kicker?: string
391
363
  venueCount: number
364
+ /**
365
+ * Further pre-formatted facts on the count line, after the unit and before
366
+ * the price band, e.g. `["50 names"]`. Each is one fact; the block draws the
367
+ * separators.
368
+ */
369
+ facts?: string[]
370
+ /**
371
+ * The pill in the photo's corner, e.g. "168 mentions". When given it replaces
372
+ * the "+N more" overflow pill, which the concept card has no room for beside
373
+ * a name-chip overflow that already says it.
374
+ */
375
+ badge?: string
392
376
  /** Facet chips. Labels only — the block never prints their counts. */
393
377
  chips?: MenuFamilyChip[]
394
378
  /** Composed price band, e.g. "22–86 lei". */
@@ -0,0 +1,115 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import * as React from "react";
4
+
5
+ import {
6
+ MotionArrow,
7
+ MotionCaret,
8
+ MotionX,
9
+ MotionCircleX,
10
+ MotionExternalLink,
11
+ MotionSend,
12
+ MotionNavigation,
13
+ MotionLocate,
14
+ MotionMaximize,
15
+ MotionPhone,
16
+ MotionMail,
17
+ MotionFlag,
18
+ MotionLogIn,
19
+ MotionLogOut,
20
+ MotionSettings,
21
+ } from "./motion-icon-actions";
22
+ import {
23
+ MotionLayoutGrid,
24
+ MotionWine,
25
+ MotionMap,
26
+ MotionFlame,
27
+ MotionCalendarClock,
28
+ MotionList,
29
+ } from "./motion-icon-set";
30
+ import type { MotionIconProps } from "./motion-icon";
31
+
32
+ /**
33
+ * Real framer-motion, no mock: every glyph has to mount, pose and replay
34
+ * without throwing, in both directions where it has one. The gestures
35
+ * themselves are judged in the Storybook gallery at 16px and 22px — nothing
36
+ * here can see whether a fold reads.
37
+ */
38
+ const GLYPHS: Array<[string, React.ComponentType<MotionIconProps>]> = [
39
+ ["Arrow →", (p) => <MotionArrow {...p} />],
40
+ ["Arrow ←", (p) => <MotionArrow direction="left" {...p} />],
41
+ ["Caret →", (p) => <MotionCaret {...p} />],
42
+ ["Caret ←", (p) => <MotionCaret direction="left" {...p} />],
43
+ ["X", MotionX],
44
+ ["CircleX", MotionCircleX],
45
+ ["ExternalLink", MotionExternalLink],
46
+ ["Send", MotionSend],
47
+ ["Navigation", MotionNavigation],
48
+ ["Locate", MotionLocate],
49
+ ["Maximize", MotionMaximize],
50
+ ["Phone", MotionPhone],
51
+ ["Mail", MotionMail],
52
+ ["Flag", MotionFlag],
53
+ ["LogIn", MotionLogIn],
54
+ ["LogOut", MotionLogOut],
55
+ ["Settings", MotionSettings],
56
+ ["LayoutGrid", MotionLayoutGrid],
57
+ ["Wine", MotionWine],
58
+ ["Map", MotionMap],
59
+ ["Flame", MotionFlame],
60
+ ["CalendarClock", MotionCalendarClock],
61
+ ["List", MotionList],
62
+ ];
63
+
64
+ describe.each(GLYPHS)("%s", (_name, Icon) => {
65
+ it("mounts at rest, holds, and replays without throwing", () => {
66
+ const { container, rerender } = render(<Icon active={false} replayToken={0} />);
67
+ const svg = container.querySelector("svg");
68
+ expect(svg).not.toBeNull();
69
+ expect(svg).toHaveAttribute("aria-hidden", "true");
70
+ expect(svg).toHaveAttribute("viewBox", "0 0 24 24");
71
+
72
+ rerender(<Icon active replayToken={1} />);
73
+ rerender(<Icon active={false} replayToken={2} />);
74
+ expect(container.querySelector("svg")).not.toBeNull();
75
+ });
76
+ });
77
+
78
+ describe("MotionArrow / MotionCaret", () => {
79
+ it("vendor the mirrored lucide paths rather than flipping the right-hand glyph", () => {
80
+ const right = render(<MotionArrow />).container.innerHTML;
81
+ const left = render(<MotionArrow direction="left" />).container.innerHTML;
82
+ expect(right).toContain('d="M5 12h14"');
83
+ expect(left).toContain('d="M19 12H5"');
84
+ });
85
+ });
86
+
87
+ describe("MotionCalendarClock", () => {
88
+ it("draws the elapsed wedge with butt caps so it stays a pie, not a blob", () => {
89
+ const { container } = render(<MotionCalendarClock />);
90
+ const wedge = container.querySelector('circle[r="2"]');
91
+ expect(wedge).toHaveAttribute("stroke-linecap", "butt");
92
+ expect(wedge).toHaveAttribute("stroke-width", "4");
93
+ });
94
+
95
+ it("is empty on first paint — the frame before the pose effect runs", () => {
96
+ // Without these two static attributes an un-dashed r=2 / width-4 circle
97
+ // is a solid disc on the face for one frame on every mount.
98
+ const { container } = render(<MotionCalendarClock />);
99
+ const wedge = container.querySelector('circle[r="2"]');
100
+ expect(wedge).toHaveAttribute("pathLength", "1");
101
+ expect(wedge).toHaveAttribute("stroke-dasharray", "0 1");
102
+ });
103
+ });
104
+
105
+ describe("MotionLogIn / MotionLogOut", () => {
106
+ it("clip the reel so the arriving arrow never draws over the label beside the button", () => {
107
+ const { container: inn } = render(<MotionLogIn />);
108
+ const { container: out } = render(<MotionLogOut />);
109
+ expect(inn.querySelector("clipPath rect")).toHaveAttribute("width", "17");
110
+ expect(out.querySelector("clipPath rect")).toHaveAttribute("x", "8");
111
+ // Two arrows each: the departing one and the one that takes its place.
112
+ expect(inn.querySelectorAll('path[d="m10 17 5-5-5-5"]')).toHaveLength(2);
113
+ expect(out.querySelectorAll('path[d="m16 17 5-5-5-5"]')).toHaveLength(2);
114
+ });
115
+ });