@bearmenu/ui 0.8.13 → 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 (285) 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-3NMG3KNA.js → chunk-SXQZ7DLH.js} +3 -3
  31. package/dist/{chunk-UDTG3RSE.js → chunk-T6S2ZBV6.js} +2 -2
  32. package/dist/{chunk-FKNBZF5T.js → chunk-WYTJR2YU.js} +1 -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-P7XTW2CG.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.js +2 -2
  40. package/dist/components/aspect-ratio.js +2 -2
  41. package/dist/components/avatar.js +1 -1
  42. package/dist/components/badge.js +2 -2
  43. package/dist/components/breadcrumb.js +1 -1
  44. package/dist/components/button-group.js +1 -1
  45. package/dist/components/button.js +2 -2
  46. package/dist/components/calendar.js +2 -2
  47. package/dist/components/card.js +3 -3
  48. package/dist/components/carousel.js +2 -2
  49. package/dist/components/category-deck.js +2 -2
  50. package/dist/components/checkbox.js +2 -2
  51. package/dist/components/circular-progress.js +1 -1
  52. package/dist/components/collapsible.js +1 -1
  53. package/dist/components/combobox.js +5 -5
  54. package/dist/components/command.js +3 -3
  55. package/dist/components/concept-card.js +2 -2
  56. package/dist/components/concept-compare-rail.js +3 -3
  57. package/dist/components/concept-compare.d.ts +2 -1
  58. package/dist/components/concept-compare.js +5 -5
  59. package/dist/components/concept-convergence.js +4 -4
  60. package/dist/components/convergence.js +3 -3
  61. package/dist/components/coverflow.js +1 -1
  62. package/dist/components/currency-input.js +2 -2
  63. package/dist/components/date-input.js +2 -2
  64. package/dist/components/day-rail-chip.js +2 -2
  65. package/dist/components/description-list.js +1 -1
  66. package/dist/components/dialog.js +2 -2
  67. package/dist/components/doorway-tiles.d.ts +57 -0
  68. package/dist/components/doorway-tiles.js +86 -0
  69. package/dist/components/drawer.js +1 -1
  70. package/dist/components/drift-rail.js +1 -1
  71. package/dist/components/dropdown-menu.js +1 -1
  72. package/dist/components/empty-state.js +3 -3
  73. package/dist/components/event-card.js +4 -4
  74. package/dist/components/event-compact-row.js +4 -4
  75. package/dist/components/event-day-header.js +2 -2
  76. package/dist/components/event-day-separator.js +3 -3
  77. package/dist/components/event-detail-panel.js +4 -4
  78. package/dist/components/event-hub-hero.js +2 -2
  79. package/dist/components/event-index-row.js +4 -4
  80. package/dist/components/event-list-row-wide.js +5 -5
  81. package/dist/components/event-list-row.js +5 -5
  82. package/dist/components/event-poster-fallback.js +1 -1
  83. package/dist/components/event-types.js +1 -1
  84. package/dist/components/events-empty-state.js +1 -1
  85. package/dist/components/explainer-steps.js +1 -1
  86. package/dist/components/facet-group.js +2 -2
  87. package/dist/components/featured-rail-item.js +2 -2
  88. package/dist/components/field.js +2 -2
  89. package/dist/components/filter-category-row.js +3 -3
  90. package/dist/components/form.js +2 -2
  91. package/dist/components/glass-ground.js +2 -2
  92. package/dist/components/grade-atom.d.ts +3 -0
  93. package/dist/components/grade-atom.js +4 -0
  94. package/dist/components/haptic-button.js +2 -2
  95. package/dist/components/hour-band.js +4 -4
  96. package/dist/components/hour-group.js +4 -4
  97. package/dist/components/hub-surface.js +1 -1
  98. package/dist/components/hue-chip.js +1 -1
  99. package/dist/components/icon.js +1 -1
  100. package/dist/components/input-group.js +1 -1
  101. package/dist/components/input-otp.js +1 -1
  102. package/dist/components/input.js +2 -2
  103. package/dist/components/item.js +1 -1
  104. package/dist/components/kbd.js +1 -1
  105. package/dist/components/kitchen-dossier.d.ts +2 -1
  106. package/dist/components/kitchen-dossier.js +3 -3
  107. package/dist/components/label.js +2 -2
  108. package/dist/components/lazy-markdown.js +1 -1
  109. package/dist/components/link-button.js +2 -2
  110. package/dist/components/markdown-renderer.js +1 -1
  111. package/dist/components/menu-dead-end.d.ts +2 -1
  112. package/dist/components/menu-dead-end.js +1 -1
  113. package/dist/components/menu-family-block.d.ts +2 -1
  114. package/dist/components/menu-family-block.js +14 -8
  115. package/dist/components/menu-family-card.d.ts +2 -1
  116. package/dist/components/menu-family-card.js +1 -1
  117. package/dist/components/menu-field.js +1 -1
  118. package/dist/components/menu-filter-chips.d.ts +2 -1
  119. package/dist/components/menu-filter-chips.js +3 -3
  120. package/dist/components/menu-group-rows.d.ts +2 -1
  121. package/dist/components/menu-group-rows.js +2 -2
  122. package/dist/components/menu-hub-hero.js +1 -1
  123. package/dist/components/menu-index-header.d.ts +2 -1
  124. package/dist/components/menu-index-header.js +2 -2
  125. package/dist/components/menu-index-list.d.ts +2 -1
  126. package/dist/components/menu-index-list.js +4 -4
  127. package/dist/components/menu-intent-hero.d.ts +3 -2
  128. package/dist/components/menu-intent-hero.js +1 -1
  129. package/dist/components/menu-item.d.ts +2 -1
  130. package/dist/components/menu-item.js +4 -4
  131. package/dist/components/menu-kitchen-list.d.ts +2 -1
  132. package/dist/components/menu-kitchen-list.js +4 -4
  133. package/dist/components/menu-mocks.d.ts +16 -5
  134. package/dist/components/menu-mocks.js +23 -2
  135. package/dist/components/menu-types.d.ts +21 -34
  136. package/dist/components/motion-icon-actions.d.ts +27 -0
  137. package/dist/components/motion-icon-actions.js +336 -0
  138. package/dist/components/motion-icon-controls.js +3 -3
  139. package/dist/components/motion-icon-set.d.ts +13 -1
  140. package/dist/components/motion-icon-set.js +197 -6
  141. package/dist/components/motion-icon.d.ts +15 -1
  142. package/dist/components/motion-icon.js +2 -2
  143. package/dist/components/multi-select-combobox.js +7 -7
  144. package/dist/components/multi-select-menu.js +2 -2
  145. package/dist/components/native-select.js +1 -1
  146. package/dist/components/open-now-list.d.ts +2 -1
  147. package/dist/components/open-now-list.js +2 -2
  148. package/dist/components/pagination.js +2 -2
  149. package/dist/components/phone-frame.js +1 -1
  150. package/dist/components/photo-ground.js +1 -1
  151. package/dist/components/place-about-tab.d.ts +2 -1
  152. package/dist/components/place-about-tab.js +6 -6
  153. package/dist/components/place-card.d.ts +94 -12
  154. package/dist/components/place-card.js +203 -128
  155. package/dist/components/place-details-mobile.d.ts +2 -1
  156. package/dist/components/place-details-mobile.js +7 -7
  157. package/dist/components/place-schedule-week.d.ts +2 -1
  158. package/dist/components/place-schedule-week.js +3 -3
  159. package/dist/components/place-types.d.ts +3 -265
  160. package/dist/components/popover.js +2 -2
  161. package/dist/components/poster-stack.js +1 -1
  162. package/dist/components/poster-tape.js +1 -1
  163. package/dist/components/poster-wall.d.ts +22 -6
  164. package/dist/components/poster-wall.js +197 -12
  165. package/dist/components/progress.js +1 -1
  166. package/dist/components/qa-block.d.ts +2 -1
  167. package/dist/components/qa-block.js +1 -1
  168. package/dist/components/quiet-stretch.js +1 -1
  169. package/dist/components/radio-group.js +1 -1
  170. package/dist/components/rating.js +1 -1
  171. package/dist/components/scroll-area.js +1 -1
  172. package/dist/components/scroll-fade.js +2 -2
  173. package/dist/components/search-bar.js +1 -1
  174. package/dist/components/searchable-select.js +5 -5
  175. package/dist/components/section-header.js +2 -2
  176. package/dist/components/segmented-nav.d.ts +24 -4
  177. package/dist/components/segmented-nav.js +11 -2
  178. package/dist/components/select.js +1 -1
  179. package/dist/components/selection-bar.js +1 -1
  180. package/dist/components/separator.js +2 -2
  181. package/dist/components/sheet.js +1 -1
  182. package/dist/components/signature-chip.js +3 -3
  183. package/dist/components/skeleton-card.js +3 -3
  184. package/dist/components/skeleton.js +2 -2
  185. package/dist/components/slider.js +1 -1
  186. package/dist/components/sliding-panels.js +2 -2
  187. package/dist/components/snap-rail.js +2 -2
  188. package/dist/components/sonner.js +1 -1
  189. package/dist/components/sort-option-list.js +1 -1
  190. package/dist/components/spinner.js +1 -1
  191. package/dist/components/stat-banner.js +1 -1
  192. package/dist/components/stepper.js +1 -1
  193. package/dist/components/sticky-container.js +1 -1
  194. package/dist/components/story.js +1 -1
  195. package/dist/components/switch.js +1 -1
  196. package/dist/components/table.js +1 -1
  197. package/dist/components/tabs.js +2 -2
  198. package/dist/components/tag-group.js +2 -2
  199. package/dist/components/tag.js +2 -2
  200. package/dist/components/text.js +2 -2
  201. package/dist/components/textarea.js +1 -1
  202. package/dist/components/timeline-band.js +4 -4
  203. package/dist/components/timeline-playhead.js +4 -4
  204. package/dist/components/toggle-group.js +2 -2
  205. package/dist/components/toggle.js +2 -2
  206. package/dist/components/tooltip.js +1 -1
  207. package/dist/components/top-comparison.d.ts +2 -1
  208. package/dist/components/top-comparison.js +1 -1
  209. package/dist/components/top-cross-link.d.ts +2 -1
  210. package/dist/components/top-cross-link.js +1 -1
  211. package/dist/components/top-ranking-entry.d.ts +2 -1
  212. package/dist/components/top-ranking-entry.js +6 -6
  213. package/dist/components/top-ranking-hero.d.ts +2 -1
  214. package/dist/components/top-ranking-hero.js +1 -1
  215. package/dist/components/top-ranking-legend.js +1 -1
  216. package/dist/components/top-types.d.ts +3 -1
  217. package/dist/components/use-mobile.js +1 -1
  218. package/dist/components/venue-events-block.js +5 -5
  219. package/dist/components/venue-grade-dimensions.d.ts +2 -1
  220. package/dist/components/venue-grade-dimensions.js +1 -1
  221. package/dist/components/venue-quotes.d.ts +2 -1
  222. package/dist/components/venue-quotes.js +1 -1
  223. package/dist/components/venue-wall.d.ts +2 -1
  224. package/dist/components/venue-wall.js +1 -1
  225. package/dist/components/weekday-theme-banner.js +1 -1
  226. package/dist/components/weekend-panel.js +4 -4
  227. package/dist/hooks/use-drag-swipe.js +1 -1
  228. package/dist/hooks/use-haptic.js +1 -1
  229. package/dist/hooks/use-media-query.js +1 -1
  230. package/dist/hooks/use-mobile.js +1 -1
  231. package/dist/hooks/use-mounted-theme.js +1 -1
  232. package/dist/hooks/use-sticky.js +1 -1
  233. package/dist/lib/card-hover.js +2 -18
  234. package/dist/lib/grade.d.ts +54 -0
  235. package/dist/lib/grade.js +2 -0
  236. package/dist/lib/icon-depth.d.ts +35 -0
  237. package/dist/lib/icon-depth.js +2 -0
  238. package/dist/lib/icon-registry.js +1 -1
  239. package/dist/lib/motion.js +2 -2
  240. package/dist/lib/overlay-pill.d.ts +30 -0
  241. package/dist/lib/overlay-pill.js +2 -0
  242. package/dist/lib/utils.js +1 -1
  243. package/dist/place-types-BHdeMMiG.d.ts +317 -0
  244. package/package.json +1 -1
  245. package/src/components/colour-dictionary.stories.tsx +268 -82
  246. package/src/components/doorway-tiles.stories.tsx +83 -0
  247. package/src/components/doorway-tiles.test.tsx +51 -0
  248. package/src/components/doorway-tiles.tsx +225 -0
  249. package/src/components/grade-atom.stories.tsx +81 -0
  250. package/src/components/grade-atom.tsx +256 -0
  251. package/src/components/hue-chip.stories.tsx +2 -2
  252. package/src/components/menu-family-block.stories.tsx +29 -0
  253. package/src/components/menu-family-block.test.tsx +51 -0
  254. package/src/components/menu-family-block.tsx +20 -4
  255. package/src/components/menu-intent-hero.tsx +1 -1
  256. package/src/components/menu-mocks.ts +39 -3
  257. package/src/components/menu-types.ts +17 -33
  258. package/src/components/motion-icon-actions.test.tsx +115 -0
  259. package/src/components/motion-icon-actions.tsx +580 -0
  260. package/src/components/motion-icon-controls.test.tsx +1 -0
  261. package/src/components/motion-icon-controls.tsx +18 -13
  262. package/src/components/motion-icon-set.stories.tsx +62 -3
  263. package/src/components/motion-icon-set.tsx +349 -2
  264. package/src/components/motion-icon.test.tsx +180 -1
  265. package/src/components/motion-icon.tsx +97 -8
  266. package/src/components/place-card.stories.tsx +66 -76
  267. package/src/components/place-card.test.tsx +188 -100
  268. package/src/components/place-card.tsx +356 -148
  269. package/src/components/place-types.ts +17 -4
  270. package/src/components/poster-wall.stories.tsx +84 -0
  271. package/src/components/poster-wall.test.tsx +94 -3
  272. package/src/components/poster-wall.tsx +257 -9
  273. package/src/components/segmented-nav.stories.tsx +72 -1
  274. package/src/components/segmented-nav.test.tsx +96 -0
  275. package/src/components/segmented-nav.tsx +36 -4
  276. package/src/globals.css +26 -0
  277. package/src/lib/grade.test.ts +82 -0
  278. package/src/lib/grade.ts +76 -0
  279. package/src/lib/icon-depth.ts +35 -0
  280. package/src/lib/overlay-pill.ts +29 -0
  281. package/dist/components/menu-family-tiles.d.ts +0 -19
  282. package/dist/components/menu-family-tiles.js +0 -57
  283. package/src/components/menu-family-tiles.stories.tsx +0 -54
  284. package/src/components/menu-family-tiles.test.tsx +0 -30
  285. package/src/components/menu-family-tiles.tsx +0 -132
@@ -32,7 +32,13 @@
32
32
  */
33
33
 
34
34
  import * as React from "react";
35
- import { motion, useAnimationControls, type Transition, type Variants } from "framer-motion";
35
+ import {
36
+ motion,
37
+ useAnimationControls,
38
+ useReducedMotion,
39
+ type Transition,
40
+ type Variants,
41
+ } from "framer-motion";
36
42
 
37
43
  import { cn } from "../lib/utils";
38
44
 
@@ -107,50 +113,133 @@ const rootVariants: Variants = {
107
113
  play: { strokeWidth: 2.25, transition: { duration: 0.12 } },
108
114
  };
109
115
 
116
+ /** The longest gesture in the set, with a little room: a play started this recently is still on screen. */
117
+ const GESTURE_MS = 700;
118
+ /**
119
+ * How long a performed-but-not-yet-active glyph keeps its end pose before
120
+ * snapping back to rest — the route the tap triggered usually confirms within
121
+ * this, and a pour that cuts to empty at 620ms only to refill at 900ms is
122
+ * worse than one that waits.
123
+ */
124
+ const HOLD_GRACE_MS = 900;
125
+
110
126
  /**
111
127
  * Mount poses without playing; pointer down plays; arriving plays unless the
112
128
  * pointer already started it; leaving returns.
113
129
  *
114
130
  * Both effects compare against a ref rather than trusting the dependency array,
115
131
  * so they fire once per actual change and not once per render.
132
+ *
133
+ * A gesture's last keyframe draws the same PICTURE as the held pose, but not
134
+ * always with the same VALUES: the grid's tiles end one slot around the
135
+ * board, the calendar's reel ends four rows up. Left there, the next `rest`
136
+ * tween would perform a second, backwards gesture on leave. So once `play`
137
+ * resolves the values snap — invisibly — to the pose they depict: `active`
138
+ * straight away if the state has arrived, else `rest` after a grace period
139
+ * that a late-arriving `active` cancels. Leaving while a play is in flight
140
+ * defers to that same snap rather than springing the parts back mid-gesture.
116
141
  */
117
142
  export function useIconGesture(active: boolean, replayToken: number) {
118
143
  const controls = useAnimationControls();
144
+ const reducedMotion = useReducedMotion() === true;
119
145
  const lastPlayAt = React.useRef(0);
120
146
  const seenToken = React.useRef(0);
121
147
  // `null` until mounted, so the pose effect can tell a real change from the
122
148
  // first run and never performs the gesture on page load.
123
149
  const seenActive = React.useRef<boolean | null>(null);
150
+ const playCount = React.useRef(0);
151
+ const mounted = React.useRef(false);
152
+ const restTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
153
+
154
+ const clearRestTimer = React.useCallback(() => {
155
+ if (restTimer.current !== null) clearTimeout(restTimer.current);
156
+ restTimer.current = null;
157
+ }, []);
124
158
 
125
159
  React.useEffect(() => {
160
+ mounted.current = true;
126
161
  controls.set(active ? "active" : "rest");
127
162
  seenActive.current = active;
163
+ return () => {
164
+ mounted.current = false;
165
+ clearRestTimer();
166
+ };
128
167
  // Mount only — the two effects below own everything after that.
129
168
  // eslint-disable-next-line react-hooks/exhaustive-deps
130
169
  }, []);
131
170
 
171
+ // Guarded on a counter, so a replay that started mid-gesture is not cut
172
+ // short by the first one finishing, and on `mounted`, so a sheet that closes
173
+ // under its own close button never reaches `controls.set` after unmount.
174
+ //
175
+ // Reduced motion means less movement, not less feedback: the gesture is
176
+ // skipped and the icon goes straight to the pose it would have landed on,
177
+ // so a hold glyph still changes state and a return glyph stays put.
178
+ const perform = React.useCallback(async () => {
179
+ const id = ++playCount.current;
180
+ lastPlayAt.current = Date.now();
181
+ clearRestTimer();
182
+ if (reducedMotion) {
183
+ if (seenActive.current) controls.set("active");
184
+ return;
185
+ }
186
+ await controls.start("play");
187
+ if (!mounted.current || playCount.current !== id) return;
188
+ if (seenActive.current) {
189
+ controls.set("active");
190
+ return;
191
+ }
192
+ restTimer.current = setTimeout(() => {
193
+ restTimer.current = null;
194
+ if (mounted.current && !seenActive.current) controls.set("rest");
195
+ }, HOLD_GRACE_MS);
196
+ }, [controls, clearRestTimer, reducedMotion]);
197
+
132
198
  React.useEffect(() => {
133
199
  if (replayToken === seenToken.current) return;
134
200
  seenToken.current = replayToken;
135
- lastPlayAt.current = Date.now();
136
- void controls.start("play");
137
- }, [replayToken, controls]);
201
+ void perform();
202
+ }, [replayToken, perform]);
138
203
 
139
204
  React.useEffect(() => {
140
205
  if (seenActive.current === null || seenActive.current === active) return;
141
206
  seenActive.current = active;
207
+ if (reducedMotion) {
208
+ // No performance to wait for or to double: the pose is the feedback.
209
+ clearRestTimer();
210
+ controls.set(active ? "active" : "rest");
211
+ return;
212
+ }
213
+ const sincePlay = Date.now() - lastPlayAt.current;
142
214
  if (!active) {
215
+ // Mid-gesture: let it land; the settle above snaps to `rest` for us.
216
+ if (sincePlay < GESTURE_MS) return;
217
+ clearRestTimer();
143
218
  void controls.start("rest");
144
219
  return;
145
220
  }
146
- // The pointer that caused this state change already started the gesture.
147
- if (Date.now() - lastPlayAt.current < 600) return;
148
- void controls.start("play");
149
- }, [active, controls]);
221
+ // The pointer that caused this state change already performed the
222
+ // gesture in flight, or resolved and waiting in its end pose. Take the
223
+ // held pose rather than performing it twice.
224
+ if (sincePlay < GESTURE_MS + HOLD_GRACE_MS) {
225
+ clearRestTimer();
226
+ if (sincePlay >= GESTURE_MS) controls.set("active");
227
+ return;
228
+ }
229
+ void perform();
230
+ }, [active, controls, perform, clearRestTimer, reducedMotion]);
150
231
 
151
232
  return controls;
152
233
  }
153
234
 
235
+ /**
236
+ * An id safe inside `url(#…)`: React 18's `useId` contains colons, React 19's
237
+ * may contain guillemets, and neither survives a fragment reference.
238
+ */
239
+ export function useSvgId(prefix: string): string {
240
+ return `${prefix}-${React.useId().replace(/[^\w-]/g, "")}`;
241
+ }
242
+
154
243
  export function MotionIcon({
155
244
  active = false,
156
245
  replayToken = 0,
@@ -1,28 +1,40 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
- import { PlaceCard } from "./place-card";
2
+ import { Flame } from "lucide-react";
3
+ import { PlaceCard, PlaceCardSkeleton } from "./place-card";
4
+ import { Badge } from "./badge";
5
+ import { Skeleton as SkeletonBlock } from "./skeleton";
6
+ import { OVERLAY_ICON, OVERLAY_PILL } from "../lib/overlay-pill";
3
7
  import type { PlaceCardLabels, PlacePreview } from "./place-types";
4
8
 
5
9
  const labels: PlaceCardLabels = {
6
- open: "Open",
7
10
  closed: "Closed",
8
- until: "until",
9
- opensAt: "opens at",
10
- currency: "RON",
11
+ viewDetails: (name) => `View details for ${name}`,
12
+ priceLevel: (level) => `Price level ${level} of 4`,
13
+ grade: {
14
+ soft: "Worth exploring",
15
+ bands: {
16
+ exceptional: "Exceptional",
17
+ great: "Great",
18
+ good: "Good",
19
+ decent: "Decent",
20
+ weak: "Weak",
21
+ },
22
+ aria: (value) => `Grade ${value} out of 10`,
23
+ capped: "Capped for low confidence",
24
+ },
11
25
  };
12
26
 
13
27
  const basePlace: PlacePreview = {
14
28
  id: "1",
15
29
  slug: "casa-gri",
16
30
  name: "Casa Gri",
17
- description:
18
- "A modern bistro serving thoughtful European plates with a Mediterranean accent. Open daily until late.",
19
31
  open_now: true,
20
32
  rating: { rating: 4.6, count: 342 },
21
- hours: [{ opens_at: 11, closes_at: 23 }],
22
- today_schedule: [{ opens_at: 11, closes_at: 23 }],
23
- location: { distance: { meters: 1240 } },
24
- price_dishes: { price_range_low: 35, price_range_high: 90 },
25
- price_drinks: { price_range_low: 15, price_range_high: 45 },
33
+ cuisines: ["european", "mediterranean"],
34
+ areas: [{ name: "Centru" }],
35
+ price_dishes: { price_range_low: 35, price_range_high: 90, price_score: 2 },
36
+ reviews_digest: { grade: { overall: 8.4 } },
37
+ cover_url: "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=800&q=80",
26
38
  };
27
39
 
28
40
  const meta = {
@@ -30,91 +42,69 @@ const meta = {
30
42
  component: PlaceCard,
31
43
  tags: ["autodocs"],
32
44
  parameters: { layout: "centered" },
33
- args: {
34
- labels,
35
- place: basePlace,
36
- onClick: undefined,
37
- },
38
- } satisfies Meta<typeof PlaceCard>;
39
-
40
- export default meta;
41
- type Story = StoryObj<typeof meta>;
42
-
43
- export const Default: Story = {
45
+ args: { labels, place: basePlace },
44
46
  decorators: [
45
47
  (Story) => (
46
- <div className="w-[360px]">
48
+ <div className="w-[280px]">
47
49
  <Story />
48
50
  </div>
49
51
  ),
50
52
  ],
51
- };
53
+ } satisfies Meta<typeof PlaceCard>;
54
+
55
+ export default meta;
56
+ type Story = StoryObj<typeof meta>;
57
+
58
+ export const Default: Story = {};
52
59
 
53
60
  export const Closed: Story = {
54
- args: {
55
- place: { ...basePlace, open_now: false },
56
- },
57
- decorators: [
58
- (Story) => (
59
- <div className="w-[360px]">
60
- <Story />
61
- </div>
62
- ),
63
- ],
61
+ args: { place: { ...basePlace, open_now: false } },
64
62
  };
65
63
 
66
- export const NoRating: Story = {
64
+ /** Ungraded places fall back to the Google star in the metric slot. */
65
+ export const StarFallback: Story = {
66
+ args: { place: { ...basePlace, reviews_digest: undefined } },
67
+ };
68
+
69
+ /** Once geolocation is granted, distance replaces the neighbourhood. */
70
+ export const WithDistance: Story = {
71
+ args: { place: { ...basePlace, location: { distance: { meters: 1240 } } } },
72
+ };
73
+
74
+ export const NoCover: Story = {
75
+ args: { place: { ...basePlace, cover_url: undefined } },
76
+ };
77
+
78
+ /** A crawlable anchor; a plain click calls `onOpen`, a modified click navigates. */
79
+ export const Linked: Story = {
67
80
  args: {
68
- place: { ...basePlace, rating: undefined, review_count: undefined },
81
+ href: "/ro/cluj-napoca/places/casa-gri",
82
+ onOpen: (place) => alert(`Open drawer: ${place.name}`),
69
83
  },
70
- decorators: [
71
- (Story) => (
72
- <div className="w-[360px]">
73
- <Story />
74
- </div>
75
- ),
76
- ],
77
84
  };
78
85
 
79
- export const NoThumbnail: Story = {
86
+ /** The caller's marker in the top-left corner — a sibling of the card's root, so it may be interactive. */
87
+ export const WithPhotoOverlay: Story = {
80
88
  args: {
81
- place: { ...basePlace, thumbnail_url: undefined },
82
- },
83
- decorators: [
84
- (Story) => (
85
- <div className="w-[360px]">
86
- <Story />
87
- </div>
89
+ photoOverlay: (
90
+ <Badge variant="overlay" className={OVERLAY_PILL}>
91
+ <Flame className={OVERLAY_ICON} aria-hidden />
92
+ Trending
93
+ </Badge>
88
94
  ),
89
- ],
95
+ },
90
96
  };
91
97
 
92
- export const Interactive: Story = {
98
+ export const WithFooter: Story = {
93
99
  args: {
94
- onClick: (place) => alert(`Clicked: ${place.name}`),
100
+ footer: <span className="text-xs text-muted-foreground">Served as Margherita · 45 RON · 350g</span>,
95
101
  },
96
- decorators: [
97
- (Story) => (
98
- <div className="w-[360px]">
99
- <Story />
100
- </div>
101
- ),
102
- ],
103
102
  };
104
103
 
105
104
  export const Minimal: Story = {
106
- args: {
107
- place: {
108
- id: "2",
109
- slug: "tiny",
110
- name: "The Tiny Spot",
111
- },
112
- },
113
- decorators: [
114
- (Story) => (
115
- <div className="w-[360px]">
116
- <Story />
117
- </div>
118
- ),
119
- ],
105
+ args: { place: { id: "2", slug: "tiny", name: "The Tiny Spot" } },
106
+ };
107
+
108
+ export const Skeleton: Story = {
109
+ render: () => <PlaceCardSkeleton footer={<SkeletonBlock className="h-3 w-1/2" />} />,
120
110
  };