@bearmenu/ui 0.8.13 → 0.8.15

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 (294) hide show
  1. package/README.md +0 -1
  2. package/dist/{chunk-66MAPLXS.js → chunk-2ADCZXZJ.js} +1 -1
  3. package/dist/{chunk-2DEIXTBN.js → chunk-2ZXIXQIU.js} +1 -1
  4. package/dist/{chunk-YOKHBAIH.js → chunk-4OF4T4AI.js} +12 -11
  5. package/dist/{chunk-MZL7CGCL.js → chunk-4XDJXMLF.js} +2 -2
  6. package/dist/{chunk-VAXOQS7W.js → chunk-5FCC5L5F.js} +3 -3
  7. package/dist/{chunk-YYGB4G4Q.js → chunk-CZK2MRS2.js} +1 -1
  8. package/dist/chunk-DJD3EMPK.js +126 -0
  9. package/dist/chunk-EZG2NELX.js +5 -0
  10. package/dist/{chunk-VG3PND3D.js → chunk-FMTJGO76.js} +1 -1
  11. package/dist/{chunk-UXWAOISG.js → chunk-FVSKGCK3.js} +2 -2
  12. package/dist/{chunk-2XCZJWTV.js → chunk-GNCWJWFE.js} +1 -1
  13. package/dist/{chunk-X3WMRKOW.js → chunk-HDR7V6IZ.js} +1 -1
  14. package/dist/{chunk-HSIYYA2E.js → chunk-HQU6QVAN.js} +5 -5
  15. package/dist/chunk-I5V446BX.js +27 -0
  16. package/dist/{chunk-WCTHZKU7.js → chunk-ICOU5QHT.js} +1 -1
  17. package/dist/{chunk-G7IFL4Z4.js → chunk-L3VEMP5A.js} +57 -9
  18. package/dist/chunk-LQWGZIYA.js +5 -0
  19. package/dist/{chunk-XNA2INVQ.js → chunk-MCOHLWRS.js} +2 -2
  20. package/dist/{chunk-3GWWZKX7.js → chunk-MMUBH76A.js} +21 -1
  21. package/dist/{chunk-ZDAHKI3A.js → chunk-MWB7S54O.js} +1 -1
  22. package/dist/{chunk-D5QHEHXJ.js → chunk-NMEVJT5Q.js} +1 -1
  23. package/dist/{chunk-3TNPSN4D.js → chunk-NSNEANOQ.js} +2 -2
  24. package/dist/{chunk-WSRWFA6K.js → chunk-NWZGSLPU.js} +1 -1
  25. package/dist/{chunk-LEAYBICI.js → chunk-OIV7B44M.js} +1 -1
  26. package/dist/{chunk-2CXU5WZO.js → chunk-P43B3ONY.js} +1 -1
  27. package/dist/{chunk-FRPPIXMY.js → chunk-PDTPO6NG.js} +1 -1
  28. package/dist/{chunk-6XMYEZVR.js → chunk-PX6RICS5.js} +1 -1
  29. package/dist/{chunk-6TEEY5YA.js → chunk-S33K4RC5.js} +1 -1
  30. package/dist/{chunk-3VMDCCCX.js → chunk-SC2VUTUI.js} +1 -1
  31. package/dist/{chunk-3NMG3KNA.js → chunk-SXQZ7DLH.js} +3 -3
  32. package/dist/{chunk-UDTG3RSE.js → chunk-T6S2ZBV6.js} +2 -2
  33. package/dist/{chunk-R47NEOSI.js → chunk-V4E333QS.js} +29 -4
  34. package/dist/{chunk-FKNBZF5T.js → chunk-WYTJR2YU.js} +1 -1
  35. package/dist/{chunk-QXHMPJ35.js → chunk-XBBEX4SF.js} +1 -1
  36. package/dist/chunk-XCB3F6SF.js +16 -0
  37. package/dist/{chunk-7BWTYWM3.js → chunk-ZLMRVBD7.js} +1 -1
  38. package/dist/{chunk-P7XTW2CG.js → chunk-ZLPWEHZO.js} +1 -1
  39. package/dist/components/accordion.js +1 -1
  40. package/dist/components/alert-dialog.js +3 -3
  41. package/dist/components/alert.js +2 -2
  42. package/dist/components/aspect-ratio.js +2 -2
  43. package/dist/components/avatar.js +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 +4 -0
  48. package/dist/components/button.js +3 -3
  49. package/dist/components/calendar.js +3 -3
  50. package/dist/components/card.js +3 -3
  51. package/dist/components/carousel.js +3 -3
  52. package/dist/components/category-deck.js +2 -2
  53. package/dist/components/checkbox.js +2 -2
  54. package/dist/components/circular-progress.js +1 -1
  55. package/dist/components/collapsible.js +1 -1
  56. package/dist/components/combobox.js +6 -6
  57. package/dist/components/command.js +3 -3
  58. package/dist/components/concept-card.js +2 -2
  59. package/dist/components/concept-compare-rail.js +3 -3
  60. package/dist/components/concept-compare.d.ts +2 -1
  61. package/dist/components/concept-compare.js +5 -5
  62. package/dist/components/concept-convergence.js +4 -4
  63. package/dist/components/convergence.js +3 -3
  64. package/dist/components/coverflow.js +1 -1
  65. package/dist/components/currency-input.js +2 -2
  66. package/dist/components/date-input.js +2 -2
  67. package/dist/components/day-rail-chip.js +2 -2
  68. package/dist/components/description-list.js +1 -1
  69. package/dist/components/dialog.js +2 -2
  70. package/dist/components/doorway-tiles.d.ts +57 -0
  71. package/dist/components/doorway-tiles.js +86 -0
  72. package/dist/components/drawer.js +1 -1
  73. package/dist/components/drift-rail.js +1 -1
  74. package/dist/components/dropdown-menu.js +1 -1
  75. package/dist/components/empty-state.js +4 -4
  76. package/dist/components/event-card.js +4 -4
  77. package/dist/components/event-compact-row.js +4 -4
  78. package/dist/components/event-day-header.js +2 -2
  79. package/dist/components/event-day-separator.js +3 -3
  80. package/dist/components/event-detail-panel.js +5 -5
  81. package/dist/components/event-hub-hero.js +2 -2
  82. package/dist/components/event-index-row.js +4 -4
  83. package/dist/components/event-list-row-wide.js +6 -6
  84. package/dist/components/event-list-row.js +5 -5
  85. package/dist/components/event-poster-fallback.js +1 -1
  86. package/dist/components/event-types.js +1 -1
  87. package/dist/components/events-empty-state.js +1 -1
  88. package/dist/components/explainer-steps.js +1 -1
  89. package/dist/components/facet-group.js +2 -2
  90. package/dist/components/featured-rail-item.js +2 -2
  91. package/dist/components/field.js +2 -2
  92. package/dist/components/filter-category-row.js +3 -3
  93. package/dist/components/form.js +2 -2
  94. package/dist/components/glass-ground.js +2 -2
  95. package/dist/components/grade-atom.d.ts +3 -0
  96. package/dist/components/grade-atom.js +4 -0
  97. package/dist/components/hour-band.js +4 -4
  98. package/dist/components/hour-group.js +4 -4
  99. package/dist/components/hub-surface.js +1 -1
  100. package/dist/components/hue-chip.js +1 -1
  101. package/dist/components/icon.js +1 -1
  102. package/dist/components/input-group.js +1 -1
  103. package/dist/components/input-otp.js +1 -1
  104. package/dist/components/input.js +2 -2
  105. package/dist/components/item.js +1 -1
  106. package/dist/components/kbd.js +1 -1
  107. package/dist/components/kitchen-dossier.d.ts +2 -1
  108. package/dist/components/kitchen-dossier.js +4 -4
  109. package/dist/components/label.js +2 -2
  110. package/dist/components/lazy-markdown.js +1 -1
  111. package/dist/components/link-button.js +3 -3
  112. package/dist/components/markdown-renderer.js +1 -1
  113. package/dist/components/menu-dead-end.d.ts +2 -1
  114. package/dist/components/menu-dead-end.js +1 -1
  115. package/dist/components/menu-family-block.d.ts +2 -1
  116. package/dist/components/menu-family-block.js +15 -9
  117. package/dist/components/menu-family-card.d.ts +2 -1
  118. package/dist/components/menu-family-card.js +1 -1
  119. package/dist/components/menu-field.js +1 -1
  120. package/dist/components/menu-filter-chips.d.ts +2 -1
  121. package/dist/components/menu-filter-chips.js +3 -3
  122. package/dist/components/menu-group-rows.d.ts +2 -1
  123. package/dist/components/menu-group-rows.js +2 -2
  124. package/dist/components/menu-hub-hero.js +1 -1
  125. package/dist/components/menu-index-header.d.ts +2 -1
  126. package/dist/components/menu-index-header.js +2 -2
  127. package/dist/components/menu-index-list.d.ts +2 -1
  128. package/dist/components/menu-index-list.js +5 -5
  129. package/dist/components/menu-intent-hero.d.ts +3 -2
  130. package/dist/components/menu-intent-hero.js +1 -1
  131. package/dist/components/menu-item.d.ts +2 -1
  132. package/dist/components/menu-item.js +4 -4
  133. package/dist/components/menu-kitchen-list.d.ts +2 -1
  134. package/dist/components/menu-kitchen-list.js +4 -4
  135. package/dist/components/menu-mocks.d.ts +16 -5
  136. package/dist/components/menu-mocks.js +23 -2
  137. package/dist/components/menu-types.d.ts +21 -34
  138. package/dist/components/motion-icon-actions.d.ts +27 -0
  139. package/dist/components/motion-icon-actions.js +336 -0
  140. package/dist/components/motion-icon-controls.js +3 -3
  141. package/dist/components/motion-icon-set.d.ts +13 -1
  142. package/dist/components/motion-icon-set.js +197 -6
  143. package/dist/components/motion-icon.d.ts +15 -1
  144. package/dist/components/motion-icon.js +2 -2
  145. package/dist/components/multi-select-combobox.js +8 -8
  146. package/dist/components/multi-select-menu.js +2 -2
  147. package/dist/components/native-select.js +1 -1
  148. package/dist/components/open-now-list.d.ts +2 -1
  149. package/dist/components/open-now-list.js +2 -2
  150. package/dist/components/pagination.js +3 -3
  151. package/dist/components/phone-frame.js +1 -1
  152. package/dist/components/photo-ground.js +1 -1
  153. package/dist/components/place-about-tab.d.ts +2 -1
  154. package/dist/components/place-about-tab.js +6 -6
  155. package/dist/components/place-card.d.ts +94 -12
  156. package/dist/components/place-card.js +203 -128
  157. package/dist/components/place-details-mobile.d.ts +2 -1
  158. package/dist/components/place-details-mobile.js +7 -7
  159. package/dist/components/place-schedule-week.d.ts +2 -1
  160. package/dist/components/place-schedule-week.js +3 -3
  161. package/dist/components/place-types.d.ts +3 -265
  162. package/dist/components/popover.js +2 -2
  163. package/dist/components/poster-stack.js +1 -1
  164. package/dist/components/poster-tape.js +1 -1
  165. package/dist/components/poster-wall.d.ts +22 -6
  166. package/dist/components/poster-wall.js +197 -12
  167. package/dist/components/progress.js +1 -1
  168. package/dist/components/qa-block.d.ts +2 -1
  169. package/dist/components/qa-block.js +1 -1
  170. package/dist/components/quiet-stretch.js +1 -1
  171. package/dist/components/radio-group.js +1 -1
  172. package/dist/components/rating.js +1 -1
  173. package/dist/components/scroll-area.js +1 -1
  174. package/dist/components/scroll-fade.js +2 -2
  175. package/dist/components/search-bar.js +1 -1
  176. package/dist/components/searchable-select.js +6 -6
  177. package/dist/components/section-header.js +2 -2
  178. package/dist/components/segmented-nav.d.ts +24 -4
  179. package/dist/components/segmented-nav.js +11 -2
  180. package/dist/components/select.js +1 -1
  181. package/dist/components/selection-bar.js +1 -1
  182. package/dist/components/separator.js +2 -2
  183. package/dist/components/sheet.js +1 -1
  184. package/dist/components/signature-chip.js +3 -3
  185. package/dist/components/skeleton-card.js +3 -3
  186. package/dist/components/skeleton.js +2 -2
  187. package/dist/components/slider.js +1 -1
  188. package/dist/components/sliding-panels.js +3 -3
  189. package/dist/components/snap-rail.js +2 -2
  190. package/dist/components/sonner.js +1 -1
  191. package/dist/components/sort-option-list.js +1 -1
  192. package/dist/components/spinner.js +1 -1
  193. package/dist/components/stat-banner.js +1 -1
  194. package/dist/components/stepper.js +1 -1
  195. package/dist/components/sticky-container.js +1 -1
  196. package/dist/components/story.js +1 -1
  197. package/dist/components/switch.js +1 -1
  198. package/dist/components/table.js +1 -1
  199. package/dist/components/tabs.js +2 -2
  200. package/dist/components/tag-group.js +2 -2
  201. package/dist/components/tag.js +2 -2
  202. package/dist/components/text.js +2 -2
  203. package/dist/components/textarea.js +1 -1
  204. package/dist/components/timeline-band.js +4 -4
  205. package/dist/components/timeline-playhead.js +4 -4
  206. package/dist/components/toggle-group.js +2 -2
  207. package/dist/components/toggle.js +2 -2
  208. package/dist/components/tooltip.js +1 -1
  209. package/dist/components/top-comparison.d.ts +2 -1
  210. package/dist/components/top-comparison.js +1 -1
  211. package/dist/components/top-cross-link.d.ts +2 -1
  212. package/dist/components/top-cross-link.js +1 -1
  213. package/dist/components/top-ranking-entry.d.ts +2 -1
  214. package/dist/components/top-ranking-entry.js +6 -6
  215. package/dist/components/top-ranking-hero.d.ts +2 -1
  216. package/dist/components/top-ranking-hero.js +1 -1
  217. package/dist/components/top-ranking-legend.js +1 -1
  218. package/dist/components/top-types.d.ts +3 -1
  219. package/dist/components/use-mobile.js +1 -1
  220. package/dist/components/venue-events-block.js +5 -5
  221. package/dist/components/venue-grade-dimensions.d.ts +2 -1
  222. package/dist/components/venue-grade-dimensions.js +1 -1
  223. package/dist/components/venue-quotes.d.ts +2 -1
  224. package/dist/components/venue-quotes.js +1 -1
  225. package/dist/components/venue-wall.d.ts +2 -1
  226. package/dist/components/venue-wall.js +1 -1
  227. package/dist/components/weekday-theme-banner.js +1 -1
  228. package/dist/components/weekend-panel.js +4 -4
  229. package/dist/hooks/use-drag-swipe.js +1 -1
  230. package/dist/hooks/use-haptic.js +2 -2
  231. package/dist/hooks/use-media-query.js +1 -1
  232. package/dist/hooks/use-mobile.js +1 -1
  233. package/dist/hooks/use-mounted-theme.js +1 -1
  234. package/dist/hooks/use-sticky.js +1 -1
  235. package/dist/lib/card-hover.js +2 -18
  236. package/dist/lib/grade.d.ts +54 -0
  237. package/dist/lib/grade.js +2 -0
  238. package/dist/lib/icon-depth.d.ts +35 -0
  239. package/dist/lib/icon-depth.js +2 -0
  240. package/dist/lib/icon-registry.js +1 -1
  241. package/dist/lib/motion.js +2 -2
  242. package/dist/lib/overlay-pill.d.ts +30 -0
  243. package/dist/lib/overlay-pill.js +2 -0
  244. package/dist/lib/utils.js +1 -1
  245. package/dist/place-types-BHdeMMiG.d.ts +317 -0
  246. package/package.json +1 -1
  247. package/src/components/button.tsx +5 -1
  248. package/src/components/colour-dictionary.stories.tsx +268 -82
  249. package/src/components/doorway-tiles.stories.tsx +83 -0
  250. package/src/components/doorway-tiles.test.tsx +51 -0
  251. package/src/components/doorway-tiles.tsx +225 -0
  252. package/src/components/grade-atom.stories.tsx +81 -0
  253. package/src/components/grade-atom.tsx +256 -0
  254. package/src/components/hue-chip.stories.tsx +2 -2
  255. package/src/components/menu-family-block.stories.tsx +29 -0
  256. package/src/components/menu-family-block.test.tsx +51 -0
  257. package/src/components/menu-family-block.tsx +20 -4
  258. package/src/components/menu-intent-hero.tsx +1 -1
  259. package/src/components/menu-mocks.ts +39 -3
  260. package/src/components/menu-types.ts +17 -33
  261. package/src/components/motion-icon-actions.test.tsx +115 -0
  262. package/src/components/motion-icon-actions.tsx +580 -0
  263. package/src/components/motion-icon-controls.test.tsx +1 -0
  264. package/src/components/motion-icon-controls.tsx +18 -13
  265. package/src/components/motion-icon-set.stories.tsx +62 -3
  266. package/src/components/motion-icon-set.tsx +349 -2
  267. package/src/components/motion-icon.test.tsx +180 -1
  268. package/src/components/motion-icon.tsx +97 -8
  269. package/src/components/place-card.stories.tsx +66 -76
  270. package/src/components/place-card.test.tsx +188 -100
  271. package/src/components/place-card.tsx +356 -148
  272. package/src/components/place-types.ts +17 -4
  273. package/src/components/poster-wall.stories.tsx +84 -0
  274. package/src/components/poster-wall.test.tsx +94 -3
  275. package/src/components/poster-wall.tsx +257 -9
  276. package/src/components/segmented-nav.stories.tsx +72 -1
  277. package/src/components/segmented-nav.test.tsx +96 -0
  278. package/src/components/segmented-nav.tsx +36 -4
  279. package/src/globals.css +26 -0
  280. package/src/hooks/use-haptic.test.tsx +80 -0
  281. package/src/hooks/use-haptic.ts +45 -4
  282. package/src/lib/grade.test.ts +82 -0
  283. package/src/lib/grade.ts +76 -0
  284. package/src/lib/icon-depth.ts +35 -0
  285. package/src/lib/overlay-pill.ts +29 -0
  286. package/dist/components/haptic-button.d.ts +0 -12
  287. package/dist/components/haptic-button.js +0 -17
  288. package/dist/components/menu-family-tiles.d.ts +0 -19
  289. package/dist/components/menu-family-tiles.js +0 -57
  290. package/src/components/haptic-button.stories.tsx +0 -41
  291. package/src/components/haptic-button.tsx +0 -19
  292. package/src/components/menu-family-tiles.stories.tsx +0 -54
  293. package/src/components/menu-family-tiles.test.tsx +0 -30
  294. package/src/components/menu-family-tiles.tsx +0 -132
@@ -23,66 +23,189 @@ type Row = {
23
23
  const ROWS: Row[] = [
24
24
  {
25
25
  element: "Wheat and honey",
26
- why: "Grain and what goes on it — crust, pasta, pizza base, pastry, yolk — is this gold before it is cooked. Honey in the comb is the same colour, and so is morning light on toast, which is why brunch and beer (roasted malt) sit here too.",
26
+ why: "Grain and what goes on it — crust, pasta, pizza base, pastry, yolk — is this gold before it is cooked. Honey in the comb is the same colour, and so is morning light on toast, which is why brunch and beer (roasted malt) sit here too. Old gold, honey at the bottom of the jar, is the celebration. Turmeric and toasted sesame are honey's dark end: Indian and Middle Eastern sit under the bakeries because the gold is the same gold, taken out of the sun.",
27
27
  field: { family: "larder", tints: ["oklch(0.505 0.11 74)", "oklch(0.555 0.105 80)", "oklch(0.465 0.115 68)"], draws: "bread, pasta, pizza, eggs, sides, sauces, spirits" },
28
28
  familyCards: [{ label: "Breakfast & brunch", value: "oklch(0.62 0.14 82)" }, { label: "Bakeries", value: "oklch(0.58 0.11 72)" }, { label: "Beer", value: "oklch(0.62 0.13 86)" }],
29
- intents: [{ label: "Brunch", value: "oklch(0.62 0.14 82)" }],
29
+ intents: [{ label: "Brunch", value: "oklch(0.62 0.14 82)" }, { label: "Special occasion", value: "oklch(0.52 0.11 78)" }],
30
30
  },
31
31
  {
32
32
  element: "Lake water",
33
- why: "Fish, shellfish and soup come out of water, and a cocktail is a glass of it. A forest has a lake, not a sea: dark, blue-green, in shade — so the hue stays on the green side (198–220) and never reads as sky or as a pool.",
33
+ why: "Fish, shellfish and soup come out of water, and a cocktail is a glass of it. A forest has a lake, not a sea: dark, blue-green, in shade — so the hue stays on the green side (198–220) and never reads as sky or as a pool. After dark it is the one thing in the forest that is still a colour, so night is the lake with the light gone — not a sky, not a plum. Japanese is lake water because a Japanese kitchen starts with dashi, a clear cold stock, and ends with the fish; sushi is the fish near the surface, the light end; bacalhau is the cold deep.",
34
34
  field: { family: "sea", tints: ["oklch(0.485 0.095 205)", "oklch(0.535 0.09 198)", "oklch(0.445 0.095 212)"], draws: "fish, shellfish, soups, cocktails" },
35
- familyCards: [{ label: "Cocktails", value: "oklch(0.47 0.10 205)" }],
36
- intents: [{ label: "Cocktail bars", value: "oklch(0.47 0.10 205)" }],
35
+ familyCards: [{ label: "Cocktails", value: "oklch(0.47 0.10 205)" }, { label: "Clubs", value: "oklch(0.34 0.06 210)" }],
36
+ intents: [{ label: "Cocktail bars", value: "oklch(0.47 0.10 205)" }, { label: "With a view", value: "oklch(0.53 0.09 198)" }, { label: "Late night", value: "oklch(0.37 0.065 218)" }],
37
37
  },
38
38
  {
39
39
  element: "Ember and terracotta",
40
- why: "Meat is the family cooked over fire: the red of the coals, the crust on beef, cured and smoked meat. Red wine shares the glass and the colour. Terracotta is the same fire one step warmer — the oven's brick and the tomato — which is why Italian sits beside Romanian here.",
40
+ why: "Meat is the family cooked over fire: the red of the coals, the crust on beef, cured and smoked meat. Red wine shares the glass and the colour. Terracotta is the same fire one step warmer — the oven's brick and the tomato — which is why Italian sits beside Romanian here. The fire is also what people sit around. Everything cooked on coals sits here (the mangal, the parrilla, the tabletop grill) and everything red from the tomato: shakshuka, chili, pimentón. Lacquer red is the wok's fire. Chroma down is the ember banked, not the colour faded.",
41
41
  field: { family: "meat", tints: ["oklch(0.49 0.13 28)", "oklch(0.54 0.13 34)", "oklch(0.45 0.13 22)"], draws: "beef, game, cured meat, burgers, red wine" },
42
42
  familyCards: [{ label: "Romanian", value: "oklch(0.48 0.16 25)" }, { label: "Italian", value: "oklch(0.56 0.15 38)" }],
43
- intents: [{ label: "Burgers", value: "oklch(0.48 0.16 25)" }, { label: "Italian tonight", value: "oklch(0.56 0.15 38)" }],
43
+ intents: [{ label: "Burgers", value: "oklch(0.48 0.16 25)" }, { label: "Italian tonight", value: "oklch(0.56 0.15 38)" }, { label: "With friends", value: "oklch(0.53 0.14 34)" }, { label: "A lively night", value: "oklch(0.58 0.15 30)" }],
44
44
  },
45
45
  {
46
46
  element: "Maple and amber",
47
- why: "Sugar turns this colour when it meets heat: caramel, syrup, the top of a crème brûlée, the maple leaf in autumn — and a pizza crust out of the oven, which is why the Pizza card wears it. Beer's amber is the same chemistry, malt roasted, so beer items draw this field even though the beer card wears honey.",
47
+ why: "Sugar turns this colour when it meets heat: caramel, syrup, the top of a crème brûlée, the maple leaf in autumn — and a pizza crust out of the oven, which is why the Pizza card wears it. Beer's amber is the same chemistry, malt roasted, so beer items draw this field even though the beer card wears honey. Maple is American before it is anything else; syrup in autumn sun is the element's bright end and the one cuisine that wears it.",
48
48
  field: { family: "sweet", tints: ["oklch(0.505 0.14 50)", "oklch(0.555 0.135 58)", "oklch(0.465 0.14 44)"], draws: "desserts, pastry, beer" },
49
49
  familyCards: [{ label: "Pizza", value: "oklch(0.52 0.14 58)" }],
50
- intents: [],
50
+ intents: [{ label: "Something sweet", value: "oklch(0.58 0.13 60)" }, { label: "With kids", value: "oklch(0.51 0.14 48)" }],
51
51
  },
52
52
  {
53
53
  element: "Fern and pine",
54
- why: "The leaf itself. Salads, vegetables and the non-alcoholic glass — juice, lemonade, the herb in it — are the green things on the menu and take the forest's own green: pine-deep rather than lime, so it stays a forest and not a lawn.",
54
+ why: "The leaf itself. Salads, vegetables and the non-alcoholic glass — juice, lemonade, the herb in it — are the green things on the menu and take the forest's own green: pine-deep rather than lime, so it stays a forest and not a lawn. In shade, with the chroma down, it is the quietest place in the forest. A cuisine is the leaf when the leaf is the dish: tabbouleh, the herb plate beside the pho, the green curry, the tea leaf itself. In shade it is cabbage and dill; at its darkest, the mint in the glass and the spruce.",
55
55
  field: { family: "verdant", tints: ["oklch(0.505 0.115 148)", "oklch(0.555 0.11 138)", "oklch(0.465 0.115 156)"], draws: "salads, vegetables, non-alcoholic" },
56
- familyCards: [{ label: "Vegetarian", value: "oklch(0.52 0.14 148)" }, { label: "Vegan", value: "oklch(0.55 0.12 135)" }],
57
- intents: [{ label: "Vegan", value: "oklch(0.52 0.14 148)" }],
56
+ familyCards: [{ label: "Vegetarian", value: "oklch(0.48 0.13 150)" }, { label: "Vegan", value: "oklch(0.55 0.12 135)" }],
57
+ intents: [{ label: "Vegan", value: "oklch(0.55 0.12 135)" }, { label: "Somewhere quiet", value: "oklch(0.43 0.09 156)" }],
58
58
  },
59
59
  {
60
60
  element: "Bark and walnut",
61
- why: "The roasted bean is the colour of tree bark, and a coffee house is made of both — wood and roast. Walnut is the same brown darker: a lit doorway at night, which is what \"open right now\" looks like from the street. Coffee items themselves draw the non-alcoholic field (pine), not this.",
61
+ why: "The roasted bean is the colour of tree bark, and a coffee house is made of both — wood and roast. Walnut is the same brown darker: a lit doorway at night, which is what \"open right now\" looks like from the street. Bark in sun, one step lighter, is the trunk on the walk. Browned butter, adobo cooked down, the pub's oak and the beer hall's: bark is the kitchen that is wood and roast, and clay dried in the sun is the same colour, which is where the güveç sits. Coffee items themselves draw the non-alcoholic field (pine), not this.",
62
62
  familyCards: [{ label: "Coffee", value: "oklch(0.44 0.10 52)" }],
63
- intents: [{ label: "Coffee", value: "oklch(0.44 0.10 52)" }, { label: "Open right now", value: "oklch(0.44 0.08 62)" }],
63
+ intents: [{ label: "Coffee", value: "oklch(0.44 0.10 52)" }, { label: "Open right now", value: "oklch(0.44 0.08 62)" }, { label: "Laptop and coffee", value: "oklch(0.41 0.10 50)" }, { label: "With your dog", value: "oklch(0.54 0.08 66)" }],
64
64
  },
65
65
  {
66
66
  element: "Rosehip",
67
- why: "The forest's berry: the jam in the tart, the fruit on the cake, the glass of red. A red with no purple in it — the magenta and violet it replaced belong to no place a bear has been.",
67
+ why: "The forest's berry: the jam in the tart, the fruit on the cake, the glass of red. A red with no purple in it — the magenta and violet it replaced belong to no place a bear has been. The berry is also the beetroot, the pomegranate and the açaí — held under L 0.50 so it stays wine and never rose.",
68
68
  familyCards: [{ label: "Wine by the glass", value: "oklch(0.48 0.14 12)" }],
69
- intents: [{ label: "Patisseries", value: "oklch(0.52 0.15 12)" }],
69
+ intents: [{ label: "Patisseries", value: "oklch(0.52 0.15 12)" }, { label: "Date night", value: "oklch(0.41 0.15 10)" }],
70
70
  },
71
71
  {
72
72
  element: "Moss in sun",
73
- why: "The sunlit forest floor. Yellow-green because a terrace is the one intent about being outdoors in daylight; deeper than lime so it is ground, not a highlighter.",
73
+ why: "The sunlit forest floor. Yellow-green because a terrace is the one intent about being outdoors in daylight; deeper than lime so it is ground, not a highlighter. Trodden, it is the pitch. The sunlit hillside is also the olive grove, the rice terrace, and the forest's edge where meadow meets trees — which is fusion, because that edge is the one hue that is both gold and green.",
74
74
  familyCards: [],
75
- intents: [{ label: "On a terrace", value: "oklch(0.60 0.13 122)" }],
75
+ intents: [{ label: "On a terrace (ranking)", value: "oklch(0.60 0.13 122)" }, { label: "On a terrace (occasion)", value: "oklch(0.57 0.125 126)" }, { label: "Match day", value: "oklch(0.47 0.12 130)" }],
76
76
  },
77
77
  {
78
78
  element: "Stone",
79
- why: "No colour of its own. An untagged dish gets it because there is no ingredient to be coloured by; Centru gets it because the town is the one intent that is a place and not a taste. Never a section mark — as a mark, grey reads as absent.",
79
+ why: "No colour of its own. An untagged dish gets it because there is no ingredient to be coloured by; Centru gets it because the town is the one intent that is a place and not a taste. Never a section mark — as a mark, grey reads as absent. International is stone for the reason an untagged dish is: nothing to be coloured by, a step below the town's so the two greys are not one.",
80
80
  field: { family: "neutral", tints: ["oklch(0.505 0.02 60)", "oklch(0.555 0.022 66)", "oklch(0.465 0.018 54)"], draws: "an untagged dish only" },
81
81
  familyCards: [],
82
82
  intents: [{ label: "In Centru", value: "oklch(0.50 0.03 70)" }],
83
83
  },
84
84
  ];
85
85
 
86
+ /**
87
+ * Every tile destination in the consumer app, in the order each grid draws
88
+ * them, with the shade it owns. Mirrors the fe's `hub-gradients.ts` tables
89
+ * (`FAMILY_HUES`, `VENUE_TYPE_HUES`, `OCCASION_HUES`), whose test pins the
90
+ * rule: no two destinations share a value. Brunch is the one alias — its
91
+ * occasion tile opens the breakfast-brunch cuisine page, so it is that colour.
92
+ */
93
+ type TileGrid = { title: string; where: string; tiles: (Chip & { element: string })[] };
94
+
95
+ const TILE_GRIDS: TileGrid[] = [
96
+ {
97
+ title: "Menu hub · cuisines",
98
+ where: "/menu, seven tiles and the index",
99
+ tiles: [
100
+ { label: "Italian", value: "oklch(0.56 0.15 38)", element: "Ember and terracotta" },
101
+ { label: "Romanian", value: "oklch(0.48 0.16 25)", element: "Ember" },
102
+ { label: "Breakfast & brunch", value: "oklch(0.62 0.14 82)", element: "Wheat and honey" },
103
+ { label: "Vegetarian", value: "oklch(0.48 0.13 150)", element: "Fern and pine" },
104
+ { label: "Pizza", value: "oklch(0.52 0.14 58)", element: "Maple and amber" },
105
+ { label: "Vegan", value: "oklch(0.55 0.12 135)", element: "Fern" },
106
+ { label: "Bakeries", value: "oklch(0.58 0.11 72)", element: "Wheat and honey" },
107
+ { label: "All cuisines", value: "oklch(0.50 0.03 70)", element: "Stone" },
108
+ ],
109
+ },
110
+ {
111
+ title: "Drinks hub · going-out families",
112
+ where: "/menu/drinks, five tiles and the index",
113
+ tiles: [
114
+ { label: "Bars", value: "oklch(0.47 0.10 205)", element: "Lake water" },
115
+ { label: "Pubs", value: "oklch(0.55 0.13 72)", element: "Wheat and honey" },
116
+ { label: "Wine bars", value: "oklch(0.44 0.15 18)", element: "Rosehip" },
117
+ { label: "Cafés", value: "oklch(0.44 0.10 52)", element: "Bark and walnut" },
118
+ { label: "Clubs", value: "oklch(0.34 0.06 210)", element: "Lake water, after dark" },
119
+ { label: "All families", value: "oklch(0.50 0.03 70)", element: "Stone" },
120
+ ],
121
+ },
122
+ {
123
+ title: "Home · occasions",
124
+ where: "the seven on the home grid first, in the day's order, then the index",
125
+ tiles: [
126
+ { label: "Brunch", value: "oklch(0.62 0.14 82)", element: "Wheat and honey — alias of Breakfast & brunch" },
127
+ { label: "Laptop and coffee", value: "oklch(0.41 0.10 50)", element: "Walnut, the roast end" },
128
+ { label: "Somewhere quiet", value: "oklch(0.43 0.09 156)", element: "Pine, in shade" },
129
+ { label: "Something sweet", value: "oklch(0.58 0.13 60)", element: "Maple and amber" },
130
+ { label: "With friends", value: "oklch(0.53 0.14 34)", element: "Ember" },
131
+ { label: "Date night", value: "oklch(0.41 0.15 10)", element: "Rosehip, dark" },
132
+ { label: "Late night", value: "oklch(0.37 0.065 218)", element: "Lake water, after dark" },
133
+ { label: "All occasions", value: "oklch(0.50 0.03 70)", element: "Stone" },
134
+ { label: "A special occasion", value: "oklch(0.52 0.11 78)", element: "Old gold" },
135
+ { label: "A lively night", value: "oklch(0.58 0.15 30)", element: "Ember, flaring" },
136
+ { label: "With the kids", value: "oklch(0.51 0.14 48)", element: "Amber, syrup" },
137
+ { label: "With your dog", value: "oklch(0.54 0.08 66)", element: "Bark in sun" },
138
+ { label: "Match day", value: "oklch(0.47 0.12 130)", element: "Moss, trodden" },
139
+ { label: "On a terrace", value: "oklch(0.57 0.125 126)", element: "Moss in sun" },
140
+ { label: "With a view", value: "oklch(0.53 0.09 198)", element: "Lake water, light end" },
141
+ ],
142
+ },
143
+ ];
144
+
145
+ /**
146
+ * Every cuisine the API knows, in the consumer app's `CUISINE_HUES` (typed
147
+ * against the enum there, so a new cuisine cannot fall to stone). Cluj's 38
148
+ * first, in the order the cuisines index lists them; the rest after. The
149
+ * warm arc (44–86°) is nearly full — below L 0.48 the sRGB gamut caps chroma
150
+ * at bark's, so "dark amber" IS the cafés — which is why american, spanish,
151
+ * lebanese and colombian sit at the bright end, the only free cells left.
152
+ */
153
+ const CUISINE_TILES: (Chip & { element: string })[] = [
154
+ { label: "International", value: "oklch(0.44 0.03 62)", element: "Stone, in shade — no ingredient to be coloured by" },
155
+ { label: "Breakfast & brunch", value: "oklch(0.62 0.14 82)", element: "Wheat and honey" },
156
+ { label: "Romanian", value: "oklch(0.48 0.16 25)", element: "Ember" },
157
+ { label: "Bakery", value: "oklch(0.58 0.11 72)", element: "Wheat and honey" },
158
+ { label: "Italian", value: "oklch(0.56 0.15 38)", element: "Ember, terracotta" },
159
+ { label: "Pizza", value: "oklch(0.52 0.14 58)", element: "Maple and amber" },
160
+ { label: "Fast food", value: "oklch(0.38 0.10 20)", element: "Ember, banked" },
161
+ { label: "Vegetarian", value: "oklch(0.48 0.13 150)", element: "Fern and pine" },
162
+ { label: "American", value: "oklch(0.62 0.125 62)", element: "Maple syrup in autumn sun" },
163
+ { label: "Vegan", value: "oklch(0.55 0.12 135)", element: "Fern" },
164
+ { label: "Mediterranean", value: "oklch(0.50 0.08 126)", element: "Moss — the olive grove" },
165
+ { label: "Seafood", value: "oklch(0.44 0.075 220)", element: "Lake water, the deep" },
166
+ { label: "Eastern European", value: "oklch(0.38 0.085 155)", element: "Fern in shade — cabbage, dill" },
167
+ { label: "Fusion", value: "oklch(0.52 0.12 122)", element: "Moss — the forest's edge" },
168
+ { label: "Middle Eastern", value: "oklch(0.42 0.08 84)", element: "Wheat, the dark end — sesame" },
169
+ { label: "BBQ", value: "oklch(0.44 0.14 38)", element: "Ember — the coals" },
170
+ { label: "Deli", value: "oklch(0.48 0.10 66)", element: "Bark — rye crust" },
171
+ { label: "Steakhouse", value: "oklch(0.36 0.13 32)", element: "Ember, dark — the sear" },
172
+ { label: "Japanese", value: "oklch(0.50 0.085 220)", element: "Lake water — dashi" },
173
+ { label: "French", value: "oklch(0.48 0.07 46)", element: "Bark — beurre noisette" },
174
+ { label: "Sushi", value: "oklch(0.56 0.09 212)", element: "Lake water, light end" },
175
+ { label: "Turkish", value: "oklch(0.54 0.10 46)", element: "Bark — clay" },
176
+ { label: "Chinese", value: "oklch(0.40 0.15 22)", element: "Ember — lacquer red" },
177
+ { label: "Mexican", value: "oklch(0.54 0.16 24)", element: "Ember — chili and tomato" },
178
+ { label: "Korean", value: "oklch(0.44 0.11 24)", element: "Ember, muted — gochujang" },
179
+ { label: "Greek", value: "oklch(0.42 0.095 122)", element: "Moss in shade — the olive" },
180
+ { label: "Indian", value: "oklch(0.46 0.09 82)", element: "Wheat, deep — turmeric" },
181
+ { label: "British", value: "oklch(0.58 0.09 56)", element: "Bark in sun — the pub's oak" },
182
+ { label: "Thai", value: "oklch(0.44 0.13 139)", element: "Fern — green curry" },
183
+ { label: "Spanish", value: "oklch(0.60 0.13 44)", element: "Ember, bright — pimentón" },
184
+ { label: "German", value: "oklch(0.40 0.07 62)", element: "Bark, dark — the beer hall" },
185
+ { label: "Vietnamese", value: "oklch(0.46 0.095 143)", element: "Fern in shade — the herb plate" },
186
+ { label: "Lebanese", value: "oklch(0.60 0.13 150)", element: "Fern in sun — parsley" },
187
+ { label: "Cuban", value: "oklch(0.36 0.10 135)", element: "Fern, darkest — mint, tobacco leaf" },
188
+ { label: "Filipino", value: "oklch(0.36 0.07 60)", element: "Walnut — adobo" },
189
+ { label: "Brazilian", value: "oklch(0.42 0.11 8)", element: "Rosehip — açaí" },
190
+ { label: "Israeli", value: "oklch(0.50 0.11 30)", element: "Ember, brick — shakshuka" },
191
+ { label: "Portuguese", value: "oklch(0.40 0.06 198)", element: "Lake water, dark — bacalhau" },
192
+ { label: "Argentinian", value: "oklch(0.38 0.11 44)", element: "Ember, rust — the parrilla" },
193
+ { label: "Burmese", value: "oklch(0.52 0.125 153)", element: "Fern — the tea leaf" },
194
+ { label: "Colombian", value: "oklch(0.62 0.11 140)", element: "Fern in sun — the coffee hills" },
195
+ { label: "Indonesian", value: "oklch(0.54 0.085 122)", element: "Moss — the rice terrace" },
196
+ { label: "Malaysian", value: "oklch(0.56 0.135 151)", element: "Fern — pandan" },
197
+ { label: "Moroccan", value: "oklch(0.58 0.10 140)", element: "Fern in sun — mint tea" },
198
+ { label: "Nepalese", value: "oklch(0.38 0.085 122)", element: "Moss in shade — the terraces" },
199
+ { label: "Pakistani", value: "oklch(0.58 0.115 36)", element: "Ember, terracotta — the karahi" },
200
+ { label: "Persian", value: "oklch(0.36 0.14 16)", element: "Rosehip, darkest — pomegranate" },
201
+ { label: "Peruvian", value: "oklch(0.52 0.07 56)", element: "Bark, pale — the potato" },
202
+ { label: "Puerto Rican", value: "oklch(0.50 0.135 137)", element: "Fern — recaíto" },
203
+ { label: "Russian", value: "oklch(0.46 0.12 8)", element: "Rosehip — beetroot" },
204
+ { label: "Scandinavian", value: "oklch(0.40 0.11 150)", element: "Fern — the spruce" },
205
+ { label: "Singaporean", value: "oklch(0.40 0.105 134)", element: "Moss in shade — pandan, kaya" },
206
+ { label: "Taiwanese", value: "oklch(0.54 0.09 153)", element: "Fern, grey-green — oolong" },
207
+ ];
208
+
86
209
  const NOT_THESE: Chip[] = [
87
210
  { label: "sky blue", value: "oklch(0.5 0.13 240)" },
88
211
  { label: "teal", value: "oklch(0.55 0.12 190)" },
@@ -108,8 +231,7 @@ function Swatch({ label, value, angle = 135, tall }: Chip & { angle?: number; ta
108
231
  );
109
232
  }
110
233
 
111
- const TH = "px-3 py-2 text-left text-[11px] font-bold uppercase tracking-[0.08em] text-muted-foreground";
112
- const TD = "px-3 py-3 align-top";
234
+ const TH = "text-left text-[11px] font-bold uppercase tracking-[0.08em] text-muted-foreground";
113
235
 
114
236
  function ColourDictionaryPage() {
115
237
  return (
@@ -119,6 +241,7 @@ function ColourDictionaryPage() {
119
241
  <p className="mt-1 max-w-[90ch] text-[14px] leading-[1.55] text-foreground/85">
120
242
  Every hue is a forest object, at full strength — the floor and canopy of a forest and what a bear eats off it.
121
243
  Every colour a menu surface paints is in this table, grouped by the element it is, with why it belongs.
244
+ One destination, one shade: every page a tile opens has its own value, two tiles share one only when they open the same page, and two tiles that can sit in the same grid differ by element or by a lightness step a phone can see in daylight.
122
245
  <span className="ml-2 inline-flex items-center gap-2 align-middle text-[12px] text-muted-foreground">
123
246
  Not in the forest, so not in the palette:
124
247
  {NOT_THESE.map((c) => (
@@ -130,74 +253,137 @@ function ColourDictionaryPage() {
130
253
  </span>
131
254
  </p>
132
255
 
133
- <div className="mt-5 overflow-x-auto rounded-xl border border-border">
134
- <table className="w-full min-w-[1180px] border-collapse text-[13px]">
135
- <thead className="bg-muted/60">
136
- <tr>
137
- <th className={`${TH} w-[290px]`}>Element · why it belongs</th>
138
- <th className={TH}>Card fields · the three tints</th>
139
- <th className={TH}>Family cards · chips, price band, photo tint</th>
140
- <th className={TH}>Intent tiles</th>
141
- </tr>
142
- </thead>
143
- <tbody>
144
- {ROWS.map((row) => (
145
- <tr key={row.element} className="border-t border-border">
146
- <td className={TD}>
147
- <div className="text-[15px] font-extrabold tracking-[-0.01em]">{row.element}</div>
148
- <p className="mt-1 text-[12.5px] leading-[1.5] text-foreground/85">{row.why}</p>
149
- </td>
150
- <td className={TD}>
151
- {row.field ? (
152
- <div className="flex flex-col gap-2">
153
- <div className="flex gap-2">
154
- {row.field.tints.map((tint, index) => (
155
- <Swatch key={tint} label={`${row.field!.family}-${index}`} value={tint} tall />
156
- ))}
157
- </div>
158
- <div className="text-[11.5px] text-muted-foreground">
159
- <span className="font-semibold text-foreground/80">draws</span> {row.field.draws}
160
- </div>
161
- </div>
162
- ) : (
163
- <span className="text-[12px] text-muted-foreground">—</span>
164
- )}
165
- </td>
166
- <td className={TD}>
167
- {row.familyCards.length ? (
168
- <div className="flex gap-2">
169
- {row.familyCards.map((c) => (
170
- <Swatch key={c.label} {...c} angle={150} />
171
- ))}
172
- </div>
173
- ) : (
174
- <span className="text-[12px] text-muted-foreground">—</span>
175
- )}
176
- </td>
177
- <td className={TD}>
178
- {row.intents.length ? (
179
- <div className="flex gap-2">
180
- {row.intents.map((c) => (
181
- <Swatch key={c.label} {...c} angle={150} />
182
- ))}
183
- </div>
184
- ) : (
185
- <span className="text-[12px] text-muted-foreground">—</span>
186
- )}
187
- </td>
188
- </tr>
189
- ))}
190
- </tbody>
191
- </table>
256
+ {/* One block per element, the prose across the top and the three swatch
257
+ groups in one row under it. The old four-column table put the "why"
258
+ in a 290px column, so every row was as tall as its wrapped paragraph
259
+ and the three swatch columns beside it were mostly air. The list is
260
+ capped and scrolls on its own so the tile grids and the method stay
261
+ within reach. */}
262
+ <div className="mt-5 max-h-[70vh] overflow-y-auto rounded-xl border border-border">
263
+ <div className="sticky top-0 z-10 flex gap-6 border-b border-border bg-muted/60 px-4 py-2 backdrop-blur">
264
+ <span className={TH}>Element · why it belongs</span>
265
+ <span className={TH}>Card fields · the three tints</span>
266
+ <span className={TH}>Family cards · chips, price band, photo tint</span>
267
+ <span className={TH}>Intent tiles</span>
268
+ </div>
269
+ {ROWS.map((row, index) => (
270
+ <section
271
+ key={row.element}
272
+ className={index > 0 ? "border-t border-border px-4 py-4" : "px-4 py-4"}
273
+ >
274
+ <div className="flex flex-wrap items-baseline gap-x-4 gap-y-1">
275
+ <h3 className="text-[15px] font-extrabold tracking-[-0.01em]">{row.element}</h3>
276
+ <p className="max-w-[110ch] text-[12.5px] leading-[1.5] text-foreground/85">{row.why}</p>
277
+ </div>
278
+ <div className="mt-3 flex flex-wrap gap-x-10 gap-y-3">
279
+ {row.field && (
280
+ <div className="flex flex-col gap-1.5">
281
+ <span className="text-[10.5px] font-bold uppercase tracking-[0.08em] text-muted-foreground">
282
+ Card fields
283
+ </span>
284
+ <div className="flex gap-2">
285
+ {row.field.tints.map((tint, tintIndex) => (
286
+ <Swatch key={tint} label={`${row.field!.family}-${tintIndex}`} value={tint} tall />
287
+ ))}
288
+ </div>
289
+ <div className="text-[11.5px] text-muted-foreground">
290
+ <span className="font-semibold text-foreground/80">draws</span> {row.field.draws}
291
+ </div>
292
+ </div>
293
+ )}
294
+ {row.familyCards.length > 0 && (
295
+ <div className="flex flex-col gap-1.5">
296
+ <span className="text-[10.5px] font-bold uppercase tracking-[0.08em] text-muted-foreground">
297
+ Family cards
298
+ </span>
299
+ <div className="flex flex-wrap gap-2">
300
+ {row.familyCards.map((c) => (
301
+ <Swatch key={c.label} {...c} angle={150} />
302
+ ))}
303
+ </div>
304
+ </div>
305
+ )}
306
+ {row.intents.length > 0 && (
307
+ <div className="flex flex-col gap-1.5">
308
+ <span className="text-[10.5px] font-bold uppercase tracking-[0.08em] text-muted-foreground">
309
+ Intent tiles
310
+ </span>
311
+ <div className="flex flex-wrap gap-2">
312
+ {row.intents.map((c) => (
313
+ <Swatch key={c.label} {...c} angle={150} />
314
+ ))}
315
+ </div>
316
+ </div>
317
+ )}
318
+ </div>
319
+ </section>
320
+ ))}
192
321
  </div>
193
322
 
323
+ <section className="mt-10">
324
+ <h2 className="text-[15px] font-extrabold">The tiles — one destination, one shade</h2>
325
+ <p className="mt-1 max-w-[90ch] text-[13px] leading-[1.55] text-foreground/85">
326
+ Every tile in the consumer app, each grid in the order it draws them. No value appears twice across the three
327
+ grids except stone, which closes each one, and brunch, which opens the same page from two grids.
328
+ </p>
329
+ <div className="mt-4 flex flex-col gap-6">
330
+ {TILE_GRIDS.map((grid) => (
331
+ <div key={grid.title}>
332
+ <div className="flex items-baseline gap-2">
333
+ <h3 className="text-[13px] font-bold">{grid.title}</h3>
334
+ <span className="text-[11.5px] text-muted-foreground">{grid.where}</span>
335
+ </div>
336
+ <div className="mt-2 grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-8">
337
+ {grid.tiles.map((tile) => (
338
+ <div key={tile.label} className="flex flex-col gap-1">
339
+ <span
340
+ aria-hidden
341
+ className="flex aspect-[16/10] items-end rounded-xl p-2.5 text-[12px] font-extrabold leading-tight text-white lg:aspect-[2/1]"
342
+ style={{ backgroundImage: `linear-gradient(150deg, ${tile.value}, ${darker(tile.value)})` }}
343
+ >
344
+ {tile.label}
345
+ </span>
346
+ <span className="text-[11px] leading-tight text-muted-foreground">{tile.element}</span>
347
+ <code className="text-[9.5px] leading-tight text-muted-foreground">{tile.value.replace("oklch(", "").replace(")", "")}</code>
348
+ </div>
349
+ ))}
350
+ </div>
351
+ </div>
352
+ ))}
353
+ </div>
354
+
355
+ <div className="mt-8">
356
+ <div className="flex items-baseline gap-2">
357
+ <h3 className="text-[13px] font-bold">Cuisines index · every cuisine the API knows</h3>
358
+ <span className="text-[11.5px] text-muted-foreground">
359
+ /menu/cuisines — Cluj's 38 first in the order the index lists them, then the other 15; 53 values, 53 shades
360
+ </span>
361
+ </div>
362
+ <div className="mt-2 grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-8">
363
+ {CUISINE_TILES.map((tile) => (
364
+ <div key={tile.label} className="flex flex-col gap-1">
365
+ <span
366
+ aria-hidden
367
+ className="flex aspect-[16/10] items-end rounded-xl p-2.5 text-[12px] font-extrabold leading-tight text-white lg:aspect-[2/1]"
368
+ style={{ backgroundImage: `linear-gradient(150deg, ${tile.value}, ${darker(tile.value)})` }}
369
+ >
370
+ {tile.label}
371
+ </span>
372
+ <span className="text-[11px] leading-tight text-muted-foreground">{tile.element}</span>
373
+ <code className="text-[9.5px] leading-tight text-muted-foreground">{tile.value.replace("oklch(", "").replace(")", "")}</code>
374
+ </div>
375
+ ))}
376
+ </div>
377
+ </div>
378
+ </section>
379
+
194
380
  <div className="mt-8 grid gap-8 md:grid-cols-3">
195
381
  <section>
196
382
  <h2 className="text-[15px] font-extrabold">How a colour is chosen</h2>
197
383
  <ol className="mt-2 list-decimal space-y-1.5 pl-4 text-[13px] leading-[1.55] text-foreground/85">
198
384
  <li><b>What is the family made of, or where is it consumed?</b> Grain, fire, water, sugar, leaf; a night out, a morning, the town.</li>
199
385
  <li><b>Which forest element already has that colour — literally?</b> Wheat is gold, embers are red, lake water is blue-green. If nothing in a forest has it, it does not go in; that is how violet and magenta left.</li>
200
- <li><b>Take it at full strength.</b> An early pass pulled every chroma down to feel earthy and read as faded. Keep lightness 0.44–0.62 so white type at 0.26–0.30 alpha stays readable.</li>
386
+ <li><b>Take it at full strength.</b> An early pass pulled every chroma down to feel earthy and read as faded. Keep lightness 0.44–0.62 where translucent white type (0.26–0.30 alpha) sits on it; a tile that prints solid white may go down to 0.34 — the lake after dark, the roast end of walnut.</li>
201
387
  </ol>
202
388
  </section>
203
389
  <section>
@@ -209,7 +395,7 @@ function ColourDictionaryPage() {
209
395
  <section>
210
396
  <h2 className="text-[15px] font-extrabold">Where the values live</h2>
211
397
  <p className="mt-2 text-[13px] leading-[1.55] text-foreground/85">
212
- The design system owns the typography, the falloff and the layering (<code>MenuField</code>, <code>MenuFamilyCard</code>, <code>MenuFamilyTiles</code>) and takes a finished colour as a prop. The palette lives in the fe app: the eighteen <code>--menu-field-*</code> tokens in <code>app/globals.css</code>, <code>FAMILY_HUES</code> and <code>INTENT_GRADIENTS</code> in <code>hub-gradients.ts</code>, category → family in <code>category-colors.ts</code>. The Storybook mocks carry the same values so post-generator films what the app paints.
398
+ The design system owns the typography, the falloff and the layering (<code>MenuField</code>, <code>MenuFamilyCard</code>, <code>DoorwayTiles</code>) and takes a finished colour as a prop. The palette lives in the fe app: the eighteen <code>--menu-field-*</code> tokens in <code>app/globals.css</code>, <code>FAMILY_HUES</code> and <code>INTENT_GRADIENTS</code> in <code>hub-gradients.ts</code>, category → family in <code>category-colors.ts</code>. The Storybook mocks carry the same values so post-generator films what the app paints.
213
399
  </p>
214
400
  </section>
215
401
  </div>
@@ -0,0 +1,83 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { DoorwayTiles } from "./doorway-tiles";
3
+ import {
4
+ mockFamilyTiles,
5
+ mockFamilyTilesAll,
6
+ mockOccasionTiles,
7
+ mockOccasionTilesAll,
8
+ } from "./menu-mocks";
9
+
10
+ const meta: Meta<typeof DoorwayTiles> = {
11
+ title: "Menu/DoorwayTiles",
12
+ component: DoorwayTiles,
13
+ tags: ["autodocs"],
14
+ decorators: [
15
+ (Story) => (
16
+ <div className="w-[1100px] bg-background p-4">
17
+ <Story />
18
+ </div>
19
+ ),
20
+ ],
21
+ parameters: {
22
+ docs: {
23
+ description: {
24
+ component:
25
+ "A grid of doors — every class the reader browses by, as coloured tiles closed by one tile that opens the whole index. Two treatments, one rule: a tile about a thing (a cuisine, a family) prints one photograph as a tilted polaroid on the hue; a tile about a mood (an occasion) uses the photograph as the ground, multiplied by the hue. Two to a row on a phone, four from `lg`.",
26
+ },
27
+ },
28
+ },
29
+ };
30
+
31
+ export default meta;
32
+ type Story = StoryObj<typeof DoorwayTiles>;
33
+
34
+ export const Polaroid: Story = {
35
+ args: { treatment: "polaroid", tiles: mockFamilyTiles, all: mockFamilyTilesAll },
36
+ };
37
+
38
+ export const PolaroidMobile: Story = {
39
+ args: { treatment: "polaroid", tiles: mockFamilyTiles, all: mockFamilyTilesAll },
40
+ decorators: [
41
+ (Story) => (
42
+ <div className="w-[375px] bg-background p-4">
43
+ <Story />
44
+ </div>
45
+ ),
46
+ ],
47
+ };
48
+
49
+ /** Every tile without a photograph: the initial fills the polaroid. */
50
+ export const PolaroidWithoutPhotos: Story = {
51
+ args: {
52
+ treatment: "polaroid",
53
+ tiles: mockFamilyTiles.map(({ photoUrl: _photoUrl, ...tile }) => tile),
54
+ },
55
+ };
56
+
57
+ export const PolaroidPartial: Story = {
58
+ args: { treatment: "polaroid", tiles: mockFamilyTiles.slice(0, 5) },
59
+ };
60
+
61
+ /** The occasions: the mood's hue multiplied over a room. */
62
+ export const Ground: Story = {
63
+ args: { treatment: "ground", tiles: mockOccasionTiles, all: mockOccasionTilesAll },
64
+ };
65
+
66
+ export const GroundMobile: Story = {
67
+ args: { treatment: "ground", tiles: mockOccasionTiles, all: mockOccasionTilesAll },
68
+ decorators: [
69
+ (Story) => (
70
+ <div className="w-[375px] bg-background p-4">
71
+ <Story />
72
+ </div>
73
+ ),
74
+ ],
75
+ };
76
+
77
+ /** No photographs at all: the initial on the bare hue. */
78
+ export const GroundWithoutPhotos: Story = {
79
+ args: {
80
+ treatment: "ground",
81
+ tiles: mockOccasionTiles.map(({ photoUrl: _photoUrl, ...tile }) => tile),
82
+ },
83
+ };
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { DoorwayTiles } from "./doorway-tiles";
4
+ import {
5
+ mockFamilyTiles,
6
+ mockFamilyTilesAll,
7
+ mockOccasionTiles,
8
+ mockOccasionTilesAll,
9
+ } from "./menu-mocks";
10
+
11
+ describe("DoorwayTiles", () => {
12
+ it("renders one link per door plus the index tile, in order", () => {
13
+ render(
14
+ <DoorwayTiles treatment="polaroid" tiles={mockFamilyTiles} all={mockFamilyTilesAll} />,
15
+ );
16
+ const links = screen.getAllByRole("link");
17
+ expect(links).toHaveLength(mockFamilyTiles.length + 1);
18
+ expect(links[0]).toHaveAttribute("href", mockFamilyTiles[0].href);
19
+ expect(links.at(-1)).toHaveAttribute("href", mockFamilyTilesAll.href);
20
+ expect(links.at(-1)).toHaveTextContent(mockFamilyTilesAll.label);
21
+ });
22
+
23
+ // A door without a photograph keeps the polaroid — the initial fills it —
24
+ // so a row of seven never has one tile with a different silhouette.
25
+ it("draws the initial where a polaroid door has no photograph", () => {
26
+ const [withPhoto, withoutPhoto] = [mockFamilyTiles[0], mockFamilyTiles.at(-1)!];
27
+ expect(withoutPhoto.photoUrl).toBeUndefined();
28
+ render(<DoorwayTiles treatment="polaroid" tiles={[withPhoto, withoutPhoto]} />);
29
+ expect(document.querySelectorAll("img")).toHaveLength(1);
30
+ expect(screen.getByText(withoutPhoto.label.charAt(0))).toBeInTheDocument();
31
+ });
32
+
33
+ it("omits the index tile when no index is given", () => {
34
+ render(<DoorwayTiles treatment="polaroid" tiles={mockFamilyTiles.slice(0, 3)} />);
35
+ expect(screen.getAllByRole("link")).toHaveLength(3);
36
+ });
37
+
38
+ it("ground: the photograph is the tile, multiplied, and no polaroid is printed", () => {
39
+ render(
40
+ <DoorwayTiles treatment="ground" tiles={mockOccasionTiles} all={mockOccasionTilesAll} />,
41
+ );
42
+ const withPhoto = [...mockOccasionTiles, mockOccasionTilesAll].filter((t) => t.photoUrl);
43
+ const images = document.querySelectorAll("img");
44
+ expect(images).toHaveLength(withPhoto.length);
45
+ for (const img of images) expect(img.className).toContain("mix-blend-multiply");
46
+ expect(document.querySelector("[class*='rotate-']")).toBeNull();
47
+ // The one without a photograph shows its initial on the bare hue.
48
+ const bare = mockOccasionTiles.find((t) => !t.photoUrl)!;
49
+ expect(screen.getByText(bare.label.charAt(0))).toBeInTheDocument();
50
+ });
51
+ });