@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
@@ -0,0 +1,225 @@
1
+ import { cn } from "../lib/utils";
2
+ import type { MenuLinkComponent, MenuLinkProps } from "./menu-types";
3
+ import { PhotoGround } from "./photo-ground";
4
+
5
+ /**
6
+ * DoorwayTiles — a grid of doors: every class the reader browses by, as
7
+ * coloured tiles two to a row (four from `lg`), closed by one tile that opens
8
+ * the whole index. The menu hub's cuisine and going-out families are one
9
+ * caller; the homepage's occasions are another.
10
+ *
11
+ * ## A tile, not a card
12
+ *
13
+ * The tile is the class's NAME, its venue count and one photograph on the
14
+ * class's hue, so seven doors and the index fit on one phone screen. The
15
+ * reader is choosing a door here, not learning about one; the summary lives
16
+ * on the page the tile opens.
17
+ *
18
+ * ## Two treatments, one rule: object → polaroid, atmosphere → ground
19
+ *
20
+ * `polaroid` — one square photograph, tilted, running off the tile's top and
21
+ * right edges, with the name and the count under its lower edge. For a tile
22
+ * about a THING: a plate, a glass, a loaf. The print reads as "this is the
23
+ * thing"; the hue is what tells seven tiles apart at a glance.
24
+ *
25
+ * `ground` — the photograph IS the tile, multiplied by the hue. For a tile
26
+ * about a MOOD: a room, a table, a night. There is no specimen to print, so
27
+ * the room becomes the ground. Under `mix-blend-multiply` the photograph can
28
+ * never be lighter than the hue — the brightest pixel is the bare hue and
29
+ * everything else a darker shade of it — so seven tinted rooms stay seven
30
+ * distinguishable hues, and the label never lands on a highlight. A second
31
+ * layer of the hue at 22% lies over the image so shadows stay the hue's own
32
+ * dark and never black: black under wine or plum reads as a broken image.
33
+ *
34
+ * The treatment is per grid, never per tile. Sibling tiles share one shape.
35
+ *
36
+ * The photograph is the CLASS's — a plate, a room, a glass — served from the
37
+ * consumer's own origin, never a venue's (a venue on a tile about every venue
38
+ * reads as an endorsement). A class with no photograph gets the same tilted
39
+ * square holding `PhotoGround`'s serif initial (polaroid) or the initial on
40
+ * the bare hue (ground), so the silhouette never changes.
41
+ *
42
+ * Every measure on the polaroid is a percentage of the tile, so the tile and
43
+ * the design stay in proportion from a 170px phone cell to a 260px desktop
44
+ * one: 82% of the height, 8% above the top, 4% past the right, 8° over.
45
+ *
46
+ * ## The scrim is shared, and it is measured
47
+ *
48
+ * The honey and terrace hues sit around `oklch(0.6…)`, where white at 14.5px
49
+ * is about 3.7:1 bare — under the 4.5:1 the label needs. The scrim runs
50
+ * three quarters of the tile, 0.64 at the foot and 0.46 at 40% of its own
51
+ * height, so white passes 4.5:1 on every hue in the dictionary up to a
52
+ * two-line label's FIRST line (honey, the brightest, is 4.69:1 there in the
53
+ * worst case of a pure-white photo highlight). An earlier three-fifths scrim
54
+ * ran out exactly where a wrapped "Breakfast & brunch" starts.
55
+ *
56
+ * Server-safe.
57
+ */
58
+
59
+ function DefaultLink({ href, ...props }: MenuLinkProps) {
60
+ return <a href={href} {...props} />;
61
+ }
62
+
63
+ /**
64
+ * A door — a cuisine, a going-out family, an occasion — as the doorway grid
65
+ * draws it: a hue, a name, a venue count and one photograph.
66
+ *
67
+ * `href` is required: a tile that cannot resolve to a real destination is not
68
+ * rendered rather than linking nowhere.
69
+ */
70
+ export type DoorwayTile = {
71
+ slug: string;
72
+ label: string;
73
+ /**
74
+ * The venue count, already formatted by the caller — "64", "1,204" — because
75
+ * thousands separators are locale-dependent and the DS owns no locale.
76
+ */
77
+ countLabel?: string;
78
+ href: string;
79
+ /**
80
+ * Finished CSS gradient for the tile's ground, e.g.
81
+ * `linear-gradient(150deg, …)`.
82
+ *
83
+ * A gradient rather than a token name, for the same reason `<MenuField>`
84
+ * takes one: the hue tables are app-owned, and a component referencing
85
+ * `var(--h-pizza)` renders an invisible tile in any consumer that does not
86
+ * happen to define that variable. A missing gradient falls back to `bg-muted`
87
+ * — visible and obviously unstyled, rather than white text on white.
88
+ */
89
+ gradient?: string;
90
+ /** Pre-resolved photograph of the CLASS, from the consumer's own origin. */
91
+ photoUrl?: string;
92
+ };
93
+
94
+ export type DoorwayTilesTreatment = "polaroid" | "ground";
95
+
96
+ export type DoorwayTilesProps = {
97
+ tiles: DoorwayTile[];
98
+ /**
99
+ * The closing tile: the index behind the doors, drawn exactly like them —
100
+ * its own hue and photograph, `countLabel` as the caption. Only its place in
101
+ * the grid says it is the index. Omit when there is none.
102
+ */
103
+ all?: DoorwayTile;
104
+ /**
105
+ * `polaroid`: hue ground, one tilted print — a tile about a thing.
106
+ * `ground`: the photograph is the ground, multiplied by the hue — a tile
107
+ * about a mood. Required, per grid: two callers, two words, no hidden default.
108
+ */
109
+ treatment: DoorwayTilesTreatment;
110
+ linkComponent?: MenuLinkComponent;
111
+ className?: string;
112
+ };
113
+
114
+ /**
115
+ * The tile: 16:10 on the phone (the design's 170×110 cell), 2:1 from `lg`,
116
+ * never shorter than two lines of name plus the count need.
117
+ */
118
+ const TILE =
119
+ "group relative isolate flex aspect-[16/10] min-h-[108px] flex-col justify-end overflow-hidden rounded-2xl bg-muted p-3.5 text-white lg:aspect-[2/1] lg:p-4 " +
120
+ "transition-[transform,box-shadow] duration-200 hover:-translate-y-0.5 hover:shadow-[var(--shadow-card-hover)] active:translate-y-0 active:scale-[0.99] motion-reduce:transition-none " +
121
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background";
122
+
123
+ /**
124
+ * The polaroid. Same literal family as `PHOTO_GROUND` for the shadow, never a
125
+ * black `rgba` — the shadow is the ground the card family already stands on.
126
+ */
127
+ const POLAROID =
128
+ "absolute -right-[4%] -top-[8%] z-0 block aspect-square h-[82%] overflow-hidden rounded-[10%] rotate-[8deg] " +
129
+ "shadow-[0_10px_22px_-6px_oklch(0.16_0.012_55_/_0.65)] transition-transform duration-300 group-hover:rotate-[5deg] motion-reduce:transition-none";
130
+
131
+ /** One scrim for both treatments — see the docblock for the measure. */
132
+ const SCRIM =
133
+ "after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:z-0 after:h-3/4 after:content-[''] " +
134
+ "after:bg-[linear-gradient(to_top,oklch(0.16_0.012_55_/_0.64)_0%,oklch(0.16_0.012_55_/_0.46)_40%,transparent_100%)]";
135
+
136
+ /** The ground photograph, multiplied by the hue the tile already paints. */
137
+ const GROUND_PHOTO =
138
+ "absolute inset-0 z-0 h-full w-full object-cover mix-blend-multiply " +
139
+ "transition-transform duration-500 ease-out group-hover:scale-[1.04] motion-reduce:transition-none motion-reduce:group-hover:scale-100";
140
+
141
+ /** 22% of the hue back over the photograph: shadows stay the hue's dark, never black. */
142
+ const GROUND_LIFT = "absolute inset-0 z-0 opacity-[0.22]";
143
+
144
+ const GROUND_GLYPH =
145
+ "absolute inset-0 z-0 flex items-center justify-center font-serif text-[64px] font-semibold leading-none text-white/15";
146
+
147
+ export function DoorwayTiles({
148
+ tiles,
149
+ all,
150
+ treatment,
151
+ linkComponent: LinkComponent = DefaultLink,
152
+ className,
153
+ }: DoorwayTilesProps) {
154
+ const cells = all ? [...tiles, all] : tiles;
155
+ const isPolaroid = treatment === "polaroid";
156
+
157
+ return (
158
+ <div className={cn("grid grid-cols-2 gap-3 lg:grid-cols-4 lg:gap-3.5", className)}>
159
+ {cells.map((tile) => {
160
+ const ground = tile.gradient ? { backgroundImage: tile.gradient } : undefined;
161
+ return (
162
+ <LinkComponent
163
+ key={tile.slug}
164
+ href={tile.href}
165
+ className={cn(TILE, SCRIM)}
166
+ style={ground}
167
+ >
168
+ {isPolaroid ? (
169
+ <span aria-hidden className={POLAROID}>
170
+ {tile.photoUrl ? (
171
+ /* Not lazy: the grid sits directly under the hero, and these
172
+ photographs are the first thing the reader looks at. */
173
+ // eslint-disable-next-line @next/next/no-img-element
174
+ <img
175
+ src={tile.photoUrl}
176
+ alt=""
177
+ decoding="async"
178
+ className="absolute inset-0 h-full w-full object-cover"
179
+ />
180
+ ) : (
181
+ <PhotoGround
182
+ photos={[undefined]}
183
+ initials={tile.label.charAt(0)}
184
+ mode="stack"
185
+ glyphClassName="text-[44px]"
186
+ />
187
+ )}
188
+ </span>
189
+ ) : tile.photoUrl ? (
190
+ <>
191
+ {/* eslint-disable-next-line @next/next/no-img-element */}
192
+ <img src={tile.photoUrl} alt="" decoding="async" className={GROUND_PHOTO} />
193
+ <span aria-hidden className={GROUND_LIFT} style={ground} />
194
+ </>
195
+ ) : (
196
+ <span aria-hidden className={GROUND_GLYPH}>
197
+ {tile.label.charAt(0)}
198
+ </span>
199
+ )}
200
+
201
+ {/* On the polaroid both lines keep to the left 55%: the print owns the right. */}
202
+ <span
203
+ className={cn(
204
+ "relative z-[1] line-clamp-2 text-[14.5px] font-extrabold leading-[1.2] tracking-[-0.015em] lg:text-[16px]",
205
+ isPolaroid && "max-w-[55%]"
206
+ )}
207
+ >
208
+ {tile.label}
209
+ </span>
210
+ {tile.countLabel && (
211
+ <span
212
+ className={cn(
213
+ "relative z-[1] mt-0.5 truncate text-[12px] font-medium leading-[1.35] tabular-nums text-white/80 lg:text-[13px]",
214
+ isPolaroid && "max-w-[55%]"
215
+ )}
216
+ >
217
+ {tile.countLabel}
218
+ </span>
219
+ )}
220
+ </LinkComponent>
221
+ );
222
+ })}
223
+ </div>
224
+ );
225
+ }
@@ -0,0 +1,81 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { GradeAtom, type GradeAtomLabels } from "./grade-atom";
3
+
4
+ const labels: GradeAtomLabels = {
5
+ soft: "Worth exploring",
6
+ bands: {
7
+ exceptional: "Exceptional",
8
+ great: "Great",
9
+ good: "Good",
10
+ decent: "Decent",
11
+ weak: "Weak",
12
+ },
13
+ aria: (value) => `Grade ${value} out of 10`,
14
+ capped: "Capped for low confidence",
15
+ };
16
+
17
+ const meta = {
18
+ title: "Components/GradeAtom",
19
+ component: GradeAtom,
20
+ tags: ["autodocs"],
21
+ args: { labels, grade: { overall: 8.4 }, size: "md" },
22
+ } satisfies Meta<typeof GradeAtom>;
23
+
24
+ export default meta;
25
+ type Story = StoryObj<typeof meta>;
26
+
27
+ export const Default: Story = {};
28
+
29
+ /** One hue family per band, none shared by neighbours. */
30
+ export const Bands: Story = {
31
+ render: (args) => (
32
+ <div className="flex flex-wrap items-center gap-3">
33
+ {[9.4, 8.1, 6.7, 5.3, 4.2].map((overall) => (
34
+ <GradeAtom key={overall} {...args} grade={{ overall }} />
35
+ ))}
36
+ </div>
37
+ ),
38
+ };
39
+
40
+ export const Sizes: Story = {
41
+ render: (args) => (
42
+ <div className="flex flex-wrap items-end gap-3">
43
+ <GradeAtom {...args} size="xs" />
44
+ <GradeAtom {...args} size="sm" />
45
+ <GradeAtom {...args} size="md" />
46
+ <GradeAtom {...args} size="lg" />
47
+ </div>
48
+ ),
49
+ };
50
+
51
+ /** `show_number=false` — the backend recommends against a number; nothing at `xs`. */
52
+ export const Soft: Story = {
53
+ args: { grade: { overall: 5.1, show_number: false } },
54
+ };
55
+
56
+ export const Capped: Story = {
57
+ args: { grade: { overall: 7.9, capped: true } },
58
+ };
59
+
60
+ export const OnPhoto: Story = {
61
+ args: { onPhoto: true, size: "lg" },
62
+ decorators: [
63
+ (Story) => (
64
+ <div
65
+ className="flex h-40 w-72 items-end rounded-xl p-4"
66
+ style={{
67
+ backgroundImage:
68
+ "url(https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=800&q=80)",
69
+ backgroundSize: "cover",
70
+ }}
71
+ >
72
+ <Story />
73
+ </div>
74
+ ),
75
+ ],
76
+ };
77
+
78
+ /** The disclosure-trigger form used by the place hero. */
79
+ export const WithInfo: Story = {
80
+ args: { showInfo: true, size: "lg" },
81
+ };
@@ -0,0 +1,256 @@
1
+ "use client";
2
+
3
+ /**
4
+ * GradeAtom — the single, scalable visual for the ReviewsDigest `grade`
5
+ * (1–10 Romanian school scale, food-anchored). It is the hero metric that
6
+ * replaces the Google star across place surfaces; the star is demoted into the
7
+ * meta line by each caller, not here.
8
+ *
9
+ * One shape, four sizes (xs card → lg page hero). The number is always the
10
+ * hero; the descriptor, capped shield and (future) confidence scale in at
11
+ * larger sizes. Colour band comes from `grade.overall`; the label text is
12
+ * always the caller's — the backend `descriptor` is null in practice and the
13
+ * apps own this vocabulary, so it arrives translated in `labels`.
14
+ *
15
+ * States:
16
+ * - normal → coloured pill: number (+ descriptor at sm+) (+ capped shield)
17
+ * - `show_number=false` → neutral pill with a positive, honest soft label
18
+ * ("Worth exploring" / "Merită explorat"), no number
19
+ * - no `overall` → renders nothing, so callers fall back to the Google star
20
+ */
21
+
22
+ import { Info, ShieldAlert } from "lucide-react";
23
+ import { cn } from "../lib/utils";
24
+ import {
25
+ formatGrade,
26
+ resolveGradeBand,
27
+ shouldShowGradeNumber,
28
+ type GradeBand,
29
+ } from "../lib/grade";
30
+ import type { PlaceGrade } from "./place-types";
31
+
32
+ export type GradeAtomSize = "xs" | "sm" | "md" | "lg";
33
+
34
+ /** Labels passed to `<GradeAtom>`. All translation happens in the caller. */
35
+ export type GradeAtomLabels = {
36
+ /** The `show_number=false` state, e.g. "Worth exploring". */
37
+ soft: string;
38
+ /** Band → descriptor shown beside the number from `sm` up, e.g. "Great". */
39
+ bands: Record<GradeBand, string>;
40
+ /** Accessible name for the pill, e.g. "Grade 8.2 out of 10". */
41
+ aria: (value: string) => string;
42
+ /** Title + aria suffix when `grade.capped`, e.g. "Capped for low confidence". */
43
+ capped: string;
44
+ };
45
+
46
+ export type GradeAtomProps = {
47
+ grade: PlaceGrade;
48
+ labels: GradeAtomLabels;
49
+ /** xs = card chip · sm = chat/compact · md = drawer · lg = page hero. */
50
+ size?: GradeAtomSize;
51
+ /** Force-hide the descriptor even at sizes that would show it (tight rows). */
52
+ hideDescriptor?: boolean;
53
+ /** Rendered over a photo (e.g. the page hero). Switches to a frosted-dark
54
+ * shell with the band hue on the number + ring, so it stays legible and
55
+ * band-recognizable on any image. */
56
+ onPhoto?: boolean;
57
+ /**
58
+ * Adds a trailing `Info` glyph, for the one caller where the chip IS the
59
+ * disclosure trigger (the place hero, wrapped in a grade explainer).
60
+ *
61
+ * Off by default and it must stay that way: on cards this atom is a readout,
62
+ * and an affordance marker on something that doesn't open would be a lie. The
63
+ * glyph goes INSIDE the pill rather than beside it — outside, it would be a
64
+ * bare icon on unpredictable photo luminance, and it would read as a second
65
+ * control next to the chip instead of part of it.
66
+ */
67
+ showInfo?: boolean;
68
+ className?: string;
69
+ };
70
+
71
+ // Background + text + inset ring per band, with dark-mode tints. Same shape
72
+ // everywhere — only the fill changes — so the bands read as one family.
73
+ // Fill-led bands: the colour lives in the FILL so each chip reads as a distinct
74
+ // hue at a glance — even at xs before the number is read. Five separate hue
75
+ // families (gold / green / blue / orange / rose); no two adjacent bands share
76
+ // one. `-800` text on `-100` fill clears WCAG AA.
77
+ const BAND_CLASSES: Record<GradeBand, string> = {
78
+ exceptional:
79
+ "bg-amber-100 text-amber-800 ring-amber-400 dark:bg-amber-500/20 dark:text-amber-300 dark:ring-amber-500/60",
80
+ great:
81
+ "bg-emerald-100 text-emerald-800 ring-emerald-400 dark:bg-emerald-500/20 dark:text-emerald-300 dark:ring-emerald-500/60",
82
+ good: "bg-sky-100 text-sky-800 ring-sky-400 dark:bg-sky-500/20 dark:text-sky-300 dark:ring-sky-500/60",
83
+ decent:
84
+ "bg-orange-100 text-orange-800 ring-orange-400 dark:bg-orange-500/20 dark:text-orange-300 dark:ring-orange-500/60",
85
+ weak: "bg-rose-100 text-rose-800 ring-rose-400 dark:bg-rose-500/20 dark:text-rose-200 dark:ring-rose-500/60",
86
+ };
87
+
88
+ const SOFT_CLASSES = "bg-muted text-muted-foreground ring-border";
89
+
90
+ // On-photo variant: a frosted-dark shell — established by the hero's other
91
+ // on-photo chips — with the band hue moved onto the NUMBER text + the ring
92
+ // (the two elements the eye hits first), descriptor in white. A drop-shadow
93
+ // lifts the pill off bright photo regions. Keeps a guaranteed contrast floor
94
+ // that a light fill can't promise on an unpredictable image.
95
+ const PHOTO_SHELL =
96
+ "bg-black/45 text-white backdrop-blur-sm ring-white/10 drop-shadow-[0_2px_6px_rgba(0,0,0,0.55)]";
97
+
98
+ const BAND_CLASSES_PHOTO: Record<GradeBand, { ring: string; num: string }> = {
99
+ exceptional: { ring: "ring-amber-400/70", num: "text-amber-300" },
100
+ great: { ring: "ring-emerald-400/70", num: "text-emerald-300" },
101
+ good: { ring: "ring-sky-400/70", num: "text-sky-300" },
102
+ // Brighter/more saturated than the warm-canvas orange so it doesn't bleed into
103
+ // warm (restaurant) photos or compress toward the sky band on the dark shell.
104
+ decent: { ring: "ring-orange-300/90", num: "text-orange-200" },
105
+ weak: { ring: "ring-rose-400/70", num: "text-rose-200" },
106
+ };
107
+
108
+ // `minW` is what keeps a rail of chips rectangular. `formatGrade` strips a
109
+ // trailing `.0`, so the value is 1–3 glyphs ("9", "10", "8.2") and a
110
+ // content-sized pill made every card's chip a different width — the number is
111
+ // the one element users compare ACROSS cards, and a ragged right edge reads as
112
+ // noise in the comparison. Fixed on the box rather than by padding the number
113
+ // to "9.0": that decimal is real precision the grade doesn't carry, and it
114
+ // would follow `formatGrade` onto the detail hero at `text-3xl`.
115
+ //
116
+ // Each value clears the widest number the pill can hold at that size (three
117
+ // glyphs of `tabular-nums` plus the horizontal padding) with a little room
118
+ // spare. It never binds when a descriptor or the capped shield is present —
119
+ // that content is already wider — so only the bare-number chips are affected.
120
+ const SIZE: Record<
121
+ GradeAtomSize,
122
+ {
123
+ box: string;
124
+ minW: string;
125
+ num: string;
126
+ desc: string;
127
+ gap: string;
128
+ shield: string;
129
+ showDesc: boolean;
130
+ }
131
+ > = {
132
+ xs: {
133
+ box: "h-6 px-1.5 rounded-md",
134
+ minW: "min-w-9",
135
+ num: "text-[13px]",
136
+ desc: "text-2xs",
137
+ gap: "gap-1",
138
+ shield: "h-3 w-3",
139
+ showDesc: false,
140
+ },
141
+ sm: {
142
+ box: "h-7 px-2 rounded-md",
143
+ minW: "min-w-11",
144
+ num: "text-sm",
145
+ desc: "text-2xs",
146
+ gap: "gap-1.5",
147
+ shield: "h-3 w-3",
148
+ showDesc: true,
149
+ },
150
+ md: {
151
+ box: "h-9 px-2.5 rounded-lg",
152
+ minW: "min-w-14",
153
+ num: "text-lg",
154
+ desc: "text-xs",
155
+ gap: "gap-1.5",
156
+ shield: "h-3.5 w-3.5",
157
+ showDesc: true,
158
+ },
159
+ lg: {
160
+ box: "h-12 px-3.5 rounded-xl",
161
+ minW: "min-w-20",
162
+ num: "text-3xl",
163
+ desc: "text-sm",
164
+ gap: "gap-2",
165
+ shield: "h-4 w-4",
166
+ showDesc: true,
167
+ },
168
+ };
169
+
170
+ export function GradeAtom({
171
+ grade,
172
+ labels,
173
+ size = "md",
174
+ hideDescriptor,
175
+ onPhoto,
176
+ showInfo,
177
+ className,
178
+ }: GradeAtomProps) {
179
+ const overall = grade.overall;
180
+
181
+ // No grade yet → render nothing; the surface keeps its Google star fallback.
182
+ if (overall == null) return null;
183
+
184
+ const sz = SIZE[size];
185
+
186
+ // Same footprint as the capped shield, so the two trailing glyphs line up when
187
+ // a capped grade shows both. `opacity-70` rather than a colour: it has to sit
188
+ // one step behind the descriptor on the frosted shell AND on the five light
189
+ // band fills, and inheriting each shell's own text colour is the only rule
190
+ // that holds in both.
191
+ const infoGlyph = showInfo ? (
192
+ <Info className={cn("shrink-0 opacity-70", sz.shield)} aria-hidden />
193
+ ) : null;
194
+
195
+ // Soft state: the backend recommends against showing a number (thin/divided
196
+ // signal). Positive, honest framing instead of a bare low score.
197
+ //
198
+ // Suppressed at `xs`: the card title-row slot is sized for a 2–3 glyph
199
+ // number, and this label is a sentence — it wrapped inside its own
200
+ // fixed-height pill and forced the place name into a hard truncation. The
201
+ // caller falls back to its Google star instead (gate on `showsGradeAtom`, and
202
+ // see its note for why callers can't test `overall != null` any more).
203
+ if (!shouldShowGradeNumber(grade)) {
204
+ if (size === "xs") return null;
205
+ return (
206
+ <span
207
+ className={cn(
208
+ "inline-flex items-center justify-center font-medium ring-1 ring-inset",
209
+ onPhoto ? PHOTO_SHELL : SOFT_CLASSES,
210
+ sz.box,
211
+ sz.minW,
212
+ sz.desc,
213
+ sz.gap,
214
+ className
215
+ )}
216
+ >
217
+ {labels.soft}
218
+ {infoGlyph}
219
+ </span>
220
+ );
221
+ }
222
+
223
+ const band = resolveGradeBand(overall);
224
+ const value = formatGrade(overall);
225
+ const descriptor = labels.bands[band];
226
+ const showDesc = sz.showDesc && !hideDescriptor;
227
+ const photo = onPhoto ? BAND_CLASSES_PHOTO[band] : null;
228
+
229
+ return (
230
+ <span
231
+ className={cn(
232
+ // `justify-center` so a 1-glyph "9" sits centred in the reserved width
233
+ // instead of hugging the left edge of a pill sized for "8.2".
234
+ "inline-flex items-center justify-center font-semibold ring-1 ring-inset",
235
+ photo ? cn(PHOTO_SHELL, photo.ring) : BAND_CLASSES[band],
236
+ sz.box,
237
+ sz.minW,
238
+ sz.gap,
239
+ className
240
+ )}
241
+ aria-label={grade.capped ? `${labels.aria(value)} — ${labels.capped}` : labels.aria(value)}
242
+ title={grade.capped ? labels.capped : undefined}
243
+ >
244
+ <span className={cn("font-extrabold tabular-nums leading-none", sz.num, photo?.num)}>
245
+ {value}
246
+ </span>
247
+ {showDesc && (
248
+ <span className={cn("font-medium leading-none", sz.desc, onPhoto && "text-white/80")}>
249
+ {descriptor}
250
+ </span>
251
+ )}
252
+ {grade.capped && <ShieldAlert className={cn("shrink-0", sz.shield)} aria-hidden />}
253
+ {infoGlyph}
254
+ </span>
255
+ );
256
+ }
@@ -34,9 +34,9 @@ export const EveryFamily: Story = {
34
34
  ["Breakfast & Brunch", "oklch(0.62 0.14 82)"],
35
35
  ["Romanian", "oklch(0.48 0.16 25)"],
36
36
  ["Italian", "oklch(0.56 0.15 38)"],
37
- ["Vegetarian", "oklch(0.52 0.14 148)"],
37
+ ["Vegetarian", "oklch(0.48 0.13 150)"],
38
38
  ["Bars", "oklch(0.47 0.10 205)"],
39
- ["Clubs", "oklch(0.42 0.11 300)"],
39
+ ["Clubs", "oklch(0.34 0.06 210)"],
40
40
  ].map(([label, hue]) => (
41
41
  <HueChip key={label} hue={hue}>
42
42
  {label}
@@ -78,6 +78,35 @@ export const Bare: Story = {
78
78
  },
79
79
  };
80
80
 
81
+ /**
82
+ * The dishes index's concept card — the same block with the unit swapped:
83
+ * a kicker over the name, the names count beside the kitchens, the mentions
84
+ * in the corner pill, name chips instead of dish-group chips, and no hue.
85
+ */
86
+ export const Concept: Story = {
87
+ args: {
88
+ family: {
89
+ ...mockFamilyBlock,
90
+ slug: "papanasi",
91
+ name: "Papanași",
92
+ kicker: "Dessert concept",
93
+ venueCount: 70,
94
+ facts: ["50 names"],
95
+ badge: "168 mentions",
96
+ priceBandLabel: "8–44 lei",
97
+ chips: [
98
+ { slug: "a", label: "Papanași cu smântână", count: 0 },
99
+ { slug: "b", label: "Papanași ropogoși", count: 0 },
100
+ { slug: "c", label: "+48", count: 0 },
101
+ ],
102
+ photoUrls: PHOTOS,
103
+ },
104
+ labels: { ...labels, browse: "See all 70 kitchens", browseShort: "See all 70" },
105
+ hue: undefined,
106
+ gradient: undefined,
107
+ },
108
+ };
109
+
81
110
  /** The ink runs top to bottom: clear photography under the name, the rows on the inked foot. */
82
111
  export const Mobile: Story = {
83
112
  args: { family: { ...mockFamilyBlock, photoUrls: PHOTOS } },
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { MenuFamilyBlock, type MenuFamilyBlockLabels } from "./menu-family-block";
4
+ import type { MenuFamilyBlockPreview } from "./menu-types";
5
+
6
+ const labels: MenuFamilyBlockLabels = {
7
+ venueUnit: (count) => `${count} kitchens`,
8
+ more: (count) => `+${count} more`,
9
+ priceBand: "kitchen price band",
10
+ browse: "Browse",
11
+ kitchens: {
12
+ openVenue: (name) => `Open ${name}`,
13
+ openDish: (name) => `See ${name}`,
14
+ openNow: "Open now",
15
+ },
16
+ };
17
+
18
+ const family: MenuFamilyBlockPreview = {
19
+ slug: "italian",
20
+ name: "Italian",
21
+ venueCount: 96,
22
+ href: "/menu/cuisines/italian",
23
+ };
24
+
25
+ describe("MenuFamilyBlock", () => {
26
+ it("draws the overflow pill from the venues it cannot show", () => {
27
+ render(<MenuFamilyBlock family={family} labels={labels} />);
28
+ expect(screen.getByText("+96 more")).toBeInTheDocument();
29
+ });
30
+
31
+ it("as a concept card: the badge replaces the overflow pill, the kicker and facts join the count line", () => {
32
+ render(
33
+ <MenuFamilyBlock
34
+ family={{
35
+ ...family,
36
+ kicker: "Dessert concept",
37
+ facts: ["50 names"],
38
+ badge: "168 mentions",
39
+ priceBandLabel: "8–44 lei",
40
+ }}
41
+ labels={labels}
42
+ />
43
+ );
44
+ expect(screen.getByText("168 mentions")).toBeInTheDocument();
45
+ expect(screen.queryByText("+96 more")).not.toBeInTheDocument();
46
+ expect(screen.getByText("Dessert concept")).toBeInTheDocument();
47
+ expect(screen.getByText("96 kitchens")).toBeInTheDocument();
48
+ expect(screen.getByText("50 names")).toBeInTheDocument();
49
+ expect(screen.getByText("8–44 lei")).toBeInTheDocument();
50
+ });
51
+ });