@bearmenu/ui 0.8.4 → 0.8.6

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 (157) hide show
  1. package/dist/chunk-637L7XQ3.js +85 -0
  2. package/dist/chunk-6XMYEZVR.js +26 -0
  3. package/dist/chunk-DNSXMHYX.js +8 -0
  4. package/dist/chunk-FDITZ2VM.js +16 -0
  5. package/dist/chunk-GPH6L4DX.js +55 -0
  6. package/dist/{chunk-N2KNBPAQ.js → chunk-QXHMPJ35.js} +1 -0
  7. package/dist/chunk-TT3WFS6S.js +379 -0
  8. package/dist/chunk-UXWAOISG.js +170 -0
  9. package/dist/{chunk-UZ3PCIE5.js → chunk-XWMRABZW.js} +2 -2
  10. package/dist/{chunk-VBA45GLP.js → chunk-ZDAHKI3A.js} +12 -2
  11. package/dist/components/alert.d.ts +2 -2
  12. package/dist/components/badge.d.ts +1 -1
  13. package/dist/components/button.d.ts +1 -1
  14. package/dist/components/card.d.ts +2 -2
  15. package/dist/components/card.js +4 -127
  16. package/dist/components/category-deck.d.ts +50 -0
  17. package/dist/components/category-deck.js +128 -0
  18. package/dist/components/command.d.ts +15 -15
  19. package/dist/components/concept-card.d.ts +43 -0
  20. package/dist/components/concept-card.js +81 -0
  21. package/dist/components/coverflow.d.ts +123 -0
  22. package/dist/components/coverflow.js +300 -0
  23. package/dist/components/date-input.d.ts +1 -1
  24. package/dist/components/day-rail-chip.d.ts +82 -0
  25. package/dist/components/day-rail-chip.js +105 -0
  26. package/dist/components/drift-rail.d.ts +28 -0
  27. package/dist/components/drift-rail.js +3 -0
  28. package/dist/components/event-card.d.ts +97 -0
  29. package/dist/components/event-card.js +161 -0
  30. package/dist/components/event-compact-row.d.ts +72 -0
  31. package/dist/components/event-compact-row.js +133 -0
  32. package/dist/components/event-day-header.d.ts +65 -0
  33. package/dist/components/event-day-header.js +95 -0
  34. package/dist/components/event-day-separator.d.ts +46 -0
  35. package/dist/components/event-day-separator.js +50 -0
  36. package/dist/components/event-detail-panel.d.ts +80 -0
  37. package/dist/components/event-detail-panel.js +251 -0
  38. package/dist/components/event-hub-hero.d.ts +34 -0
  39. package/dist/components/event-hub-hero.js +57 -0
  40. package/dist/components/event-index-row.d.ts +68 -0
  41. package/dist/components/event-index-row.js +84 -0
  42. package/dist/components/event-list-row-wide.d.ts +101 -0
  43. package/dist/components/event-list-row-wide.js +218 -0
  44. package/dist/components/event-list-row.d.ts +82 -0
  45. package/dist/components/event-list-row.js +173 -0
  46. package/dist/components/event-poster-fallback.d.ts +20 -0
  47. package/dist/components/event-poster-fallback.js +3 -0
  48. package/dist/components/event-types.d.ts +118 -0
  49. package/dist/components/event-types.js +2 -0
  50. package/dist/components/events-empty-state.d.ts +44 -0
  51. package/dist/components/events-empty-state.js +73 -0
  52. package/dist/components/facet-group.d.ts +93 -0
  53. package/dist/components/facet-group.js +222 -0
  54. package/dist/components/featured-rail-item.d.ts +42 -0
  55. package/dist/components/featured-rail-item.js +88 -0
  56. package/dist/components/hour-band.d.ts +68 -0
  57. package/dist/components/hour-band.js +53 -0
  58. package/dist/components/hour-group.d.ts +67 -0
  59. package/dist/components/hour-group.js +59 -0
  60. package/dist/components/item.d.ts +2 -2
  61. package/dist/components/motion-icon-controls.js +3 -379
  62. package/dist/components/multi-select-combobox.js +1 -1
  63. package/dist/components/place-about-tab.js +3 -3
  64. package/dist/components/place-details-mobile.js +3 -3
  65. package/dist/components/poster-stack.d.ts +30 -0
  66. package/dist/components/poster-stack.js +104 -0
  67. package/dist/components/poster-tape.d.ts +36 -0
  68. package/dist/components/poster-tape.js +64 -0
  69. package/dist/components/poster-wall.d.ts +25 -0
  70. package/dist/components/poster-wall.js +183 -0
  71. package/dist/components/quiet-stretch.d.ts +43 -0
  72. package/dist/components/quiet-stretch.js +54 -0
  73. package/dist/components/section-header.d.ts +53 -0
  74. package/dist/components/section-header.js +63 -0
  75. package/dist/components/segmented-nav.d.ts +62 -0
  76. package/dist/components/segmented-nav.js +54 -0
  77. package/dist/components/skeleton-card.js +1 -1
  78. package/dist/components/skeleton.d.ts +17 -0
  79. package/dist/components/skeleton.js +1 -1
  80. package/dist/components/spinner.d.ts +2 -2
  81. package/dist/components/tag-group.js +3 -26
  82. package/dist/components/tag.d.ts +2 -2
  83. package/dist/components/tag.js +1 -1
  84. package/dist/components/text.d.ts +3 -3
  85. package/dist/components/timeline-band.d.ts +46 -0
  86. package/dist/components/timeline-band.js +5 -0
  87. package/dist/components/venue-events-block.d.ts +143 -0
  88. package/dist/components/venue-events-block.js +222 -0
  89. package/dist/components/weekend-panel.d.ts +44 -0
  90. package/dist/components/weekend-panel.js +231 -0
  91. package/package.json +1 -1
  92. package/src/components/card.tsx +44 -1
  93. package/src/components/category-deck.stories.tsx +96 -0
  94. package/src/components/category-deck.tsx +227 -0
  95. package/src/components/concept-card.stories.tsx +80 -0
  96. package/src/components/concept-card.tsx +143 -0
  97. package/src/components/coverflow.stories.tsx +240 -0
  98. package/src/components/coverflow.test.tsx +193 -0
  99. package/src/components/coverflow.tsx +550 -0
  100. package/src/components/day-rail-chip.stories.tsx +145 -0
  101. package/src/components/day-rail-chip.tsx +201 -0
  102. package/src/components/drift-rail.stories.tsx +92 -0
  103. package/src/components/drift-rail.tsx +86 -0
  104. package/src/components/event-card.stories.tsx +248 -0
  105. package/src/components/event-card.test.tsx +177 -0
  106. package/src/components/event-card.tsx +321 -0
  107. package/src/components/event-compact-row.stories.tsx +221 -0
  108. package/src/components/event-compact-row.tsx +268 -0
  109. package/src/components/event-day-header.stories.tsx +124 -0
  110. package/src/components/event-day-header.tsx +180 -0
  111. package/src/components/event-day-separator.stories.tsx +70 -0
  112. package/src/components/event-day-separator.tsx +95 -0
  113. package/src/components/event-detail-panel.stories.tsx +191 -0
  114. package/src/components/event-detail-panel.tsx +360 -0
  115. package/src/components/event-hub-hero.stories.tsx +66 -0
  116. package/src/components/event-hub-hero.tsx +110 -0
  117. package/src/components/event-index-row.stories.tsx +116 -0
  118. package/src/components/event-index-row.tsx +181 -0
  119. package/src/components/event-list-row-wide.stories.tsx +204 -0
  120. package/src/components/event-list-row-wide.tsx +386 -0
  121. package/src/components/event-list-row.stories.tsx +159 -0
  122. package/src/components/event-list-row.tsx +335 -0
  123. package/src/components/event-poster-fallback.stories.tsx +52 -0
  124. package/src/components/event-poster-fallback.tsx +31 -0
  125. package/src/components/event-types.ts +133 -0
  126. package/src/components/events-empty-state.stories.tsx +59 -0
  127. package/src/components/events-empty-state.tsx +131 -0
  128. package/src/components/facet-group.stories.tsx +247 -0
  129. package/src/components/facet-group.test.tsx +225 -0
  130. package/src/components/facet-group.tsx +352 -0
  131. package/src/components/featured-rail-item.stories.tsx +86 -0
  132. package/src/components/featured-rail-item.tsx +153 -0
  133. package/src/components/hour-band.stories.tsx +159 -0
  134. package/src/components/hour-band.tsx +126 -0
  135. package/src/components/hour-group.stories.tsx +100 -0
  136. package/src/components/hour-group.tsx +124 -0
  137. package/src/components/poster-stack.stories.tsx +68 -0
  138. package/src/components/poster-stack.tsx +155 -0
  139. package/src/components/poster-tape.stories.tsx +70 -0
  140. package/src/components/poster-tape.tsx +122 -0
  141. package/src/components/poster-wall.stories.tsx +95 -0
  142. package/src/components/poster-wall.tsx +220 -0
  143. package/src/components/quiet-stretch.stories.tsx +62 -0
  144. package/src/components/quiet-stretch.tsx +115 -0
  145. package/src/components/section-header.stories.tsx +70 -0
  146. package/src/components/section-header.tsx +134 -0
  147. package/src/components/segmented-nav.stories.tsx +118 -0
  148. package/src/components/segmented-nav.tsx +113 -0
  149. package/src/components/skeleton.tsx +18 -0
  150. package/src/components/tag.tsx +11 -1
  151. package/src/components/timeline-band.stories.tsx +206 -0
  152. package/src/components/timeline-band.tsx +207 -0
  153. package/src/components/venue-events-block.stories.tsx +216 -0
  154. package/src/components/venue-events-block.tsx +408 -0
  155. package/src/components/weekend-panel.stories.tsx +92 -0
  156. package/src/components/weekend-panel.tsx +322 -0
  157. package/src/globals.css +19 -0
@@ -0,0 +1,408 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Quote, Star } from "lucide-react";
5
+ import { cn } from "../lib/utils";
6
+ import { Card } from "./card";
7
+ import { Separator } from "./separator";
8
+ import { Skeleton } from "./skeleton";
9
+
10
+ /**
11
+ * VenueEventsBlock — "one venue, a whole fortnight": the venue and its
12
+ * upcoming nights on the left, its menu on the right. Every string arrives
13
+ * pre-formatted from the caller (ratings, prices, counts) — this component
14
+ * does no number or currency formatting, same contract as `EventCard`.
15
+ *
16
+ * Three independent link surfaces share one `linkComponent`: the venue
17
+ * header, each night row, and the "all events" chip. Each renders a link only
18
+ * when its own `href` is given, a plain element otherwise — none of them
19
+ * assume the others are interactive.
20
+ *
21
+ * `venue`/`nights`/`dishes` default to an empty venue / `[]` so a caller using
22
+ * only `venueContent`/`nightsContent`/`menuContent` never has to fabricate
23
+ * placeholder data just to satisfy these props.
24
+ */
25
+
26
+ export type VenueEventsBlockLinkProps = {
27
+ href: string;
28
+ className?: string;
29
+ children: React.ReactNode;
30
+ };
31
+
32
+ function DefaultLink({ href, ...props }: VenueEventsBlockLinkProps) {
33
+ return <a href={href} {...props} />;
34
+ }
35
+
36
+ export type VenueEventsBlockVenue = {
37
+ name: string;
38
+ ratingLabel?: string;
39
+ metaLabel?: string;
40
+ thumbnailUrl?: string;
41
+ isOpenNow?: boolean;
42
+ href?: string;
43
+ };
44
+
45
+ export type VenueEventsBlockNight = {
46
+ id: string;
47
+ dayLabel: string;
48
+ title: string;
49
+ timeLabel: string;
50
+ href?: string;
51
+ };
52
+
53
+ export type VenueEventsBlockDish = {
54
+ id: string;
55
+ name: string;
56
+ description?: string;
57
+ priceLabel: string;
58
+ quantityLabel?: string;
59
+ mentionsLabel?: string;
60
+ };
61
+
62
+ export type VenueEventsBlockProps = {
63
+ /**
64
+ * Plain venue data this block formats itself into the header card. Fine
65
+ * for a caller with nothing richer than name/rating/meta — and kept for
66
+ * the existing stories — but a consuming app that already owns a place
67
+ * card (real photo, description, cuisine/price meta) should reach for
68
+ * `venueContent` instead: rebuilding that card here would fork it, and the
69
+ * fork would drift the moment either copy changes.
70
+ */
71
+ venue?: VenueEventsBlockVenue;
72
+ /**
73
+ * Plain night data this block formats itself into a row. Fine for a
74
+ * caller with nothing richer than day/title/time — and kept for the
75
+ * existing stories — but a consuming app that already owns an event row
76
+ * component (poster, description, tags) should reach for `nightsContent`
77
+ * instead, for the same reason.
78
+ */
79
+ nights?: VenueEventsBlockNight[];
80
+ allEventsLabel: string;
81
+ allEventsHref?: string;
82
+ menuHeader?: React.ReactNode;
83
+ /**
84
+ * Plain dish data this block formats itself into a row. Fine for a caller
85
+ * with nothing richer than name/price/description — and kept for the
86
+ * existing stories — but a consuming app that already owns a menu-row
87
+ * component (signature badges, an evidence/mentions control, category
88
+ * marks, an ingredient-hue fallback) should reach for `menuContent`
89
+ * instead: rebuilding that row's markup here would fork it, and the fork
90
+ * would drift the moment either copy changes.
91
+ */
92
+ dishes?: VenueEventsBlockDish[];
93
+ /**
94
+ * Replaces the internally-rendered venue header entirely when given —
95
+ * `venue.name`/`ratingLabel`/`metaLabel`/`thumbnailUrl`/`isOpenNow` are
96
+ * then ignored (`venue.href` still wraps this slot in a link when given —
97
+ * a caller whose card is already a link should omit `venue.href` and let
98
+ * its own card own the anchor). Exists because this block should own the
99
+ * LAYOUT and nothing about the shape of a venue card: the consuming app
100
+ * has its own canonical place card (used on its place list, its search
101
+ * results, its map sheet), and the DS must not maintain a second
102
+ * implementation of it.
103
+ */
104
+ venueContent?: React.ReactNode;
105
+ /**
106
+ * Replaces the internally-rendered night rows entirely when given —
107
+ * `nights` is then ignored. Exists because this block should own the
108
+ * LAYOUT and nothing about the shape of a night row: the consuming app
109
+ * has its own canonical event row (used on its timeline, its event
110
+ * detail's "more from this venue"), and the DS must not maintain a
111
+ * second implementation of it. Render that row component here, one per
112
+ * item, with its own key/link/interaction — this slot only provides the
113
+ * column it sits in.
114
+ */
115
+ nightsContent?: React.ReactNode;
116
+ /**
117
+ * Replaces the internally-rendered dish rows entirely when given —
118
+ * `dishes` is then ignored. Exists because this block should own the
119
+ * LAYOUT (venue, nights, menu column) and nothing about the shape of a
120
+ * menu row: the consuming app has its own canonical row (used on its menu
121
+ * page, its place-details sheet, its must-try and pairings lists), and the
122
+ * DS must not maintain a second implementation of it. Render that row
123
+ * component here, one per item, with its own key/link/interaction — this
124
+ * slot only provides the column it sits in.
125
+ */
126
+ menuContent?: React.ReactNode;
127
+ footnote?: string;
128
+ linkComponent?: React.ComponentType<VenueEventsBlockLinkProps>;
129
+ className?: string;
130
+ };
131
+
132
+ export function VenueEventsBlock({
133
+ venue = { name: "" },
134
+ nights = [],
135
+ allEventsLabel,
136
+ allEventsHref,
137
+ menuHeader,
138
+ dishes = [],
139
+ venueContent,
140
+ nightsContent,
141
+ menuContent,
142
+ footnote,
143
+ linkComponent: LinkComponent = DefaultLink,
144
+ className,
145
+ }: VenueEventsBlockProps) {
146
+ const [thumbError, setThumbError] = React.useState(false);
147
+ const showThumb = !!venue.thumbnailUrl && !thumbError;
148
+
149
+ const venueHeader = (
150
+ <div className="flex items-center gap-3 rounded-xl border border-border bg-card p-3">
151
+ <div className="relative h-16 w-16 shrink-0 overflow-hidden rounded-lg bg-muted">
152
+ {showThumb && (
153
+ // eslint-disable-next-line @next/next/no-img-element
154
+ <img
155
+ src={venue.thumbnailUrl}
156
+ alt=""
157
+ loading="lazy"
158
+ decoding="async"
159
+ className="h-full w-full object-cover"
160
+ onError={() => setThumbError(true)}
161
+ />
162
+ )}
163
+ {venue.isOpenNow && (
164
+ <span
165
+ className="absolute right-1 top-1 h-1.5 w-1.5 rounded-full bg-success ring-2 ring-background"
166
+ aria-hidden
167
+ />
168
+ )}
169
+ </div>
170
+
171
+ <div className="min-w-0 flex-1">
172
+ <div className="flex items-center gap-2">
173
+ <span className="truncate text-sm font-semibold">{venue.name}</span>
174
+ {venue.ratingLabel && (
175
+ <span className="ml-auto flex shrink-0 items-center gap-1 text-xs font-medium text-foreground">
176
+ <Star className="h-3 w-3 fill-current text-primary" aria-hidden />
177
+ {venue.ratingLabel}
178
+ </span>
179
+ )}
180
+ </div>
181
+ {venue.metaLabel && (
182
+ <div className="mt-0.5 truncate text-xs text-muted-foreground">{venue.metaLabel}</div>
183
+ )}
184
+ </div>
185
+ </div>
186
+ );
187
+
188
+ return (
189
+ // `Card variant="elevatedPanel"` rather than a hand-rolled surface. This
190
+ // root WAS `border border-border bg-card` — a `surface` card reimplemented
191
+ // inside the DS, which is how it and the skeleton below drifted apart from
192
+ // the primitive and from each other.
193
+ //
194
+ // The elevation is the point: in light mode `--card` on `--background` is
195
+ // 1.04:1, so a panel this size was defined by a 1.32:1 hairline alone and
196
+ // read as a drawn rectangle rather than a raised thing.
197
+ //
198
+ // `rounded-2xl` is exactly the 18px this used to hardcode
199
+ // (`--radius` 0.625rem + 8px) and now re-scales with the token like every
200
+ // other surface. `overflow-hidden` clips descendants, never the element's
201
+ // own box-shadow, so the right column keeps its radius and the panel keeps
202
+ // its lift.
203
+ <Card
204
+ variant="elevatedPanel"
205
+ className={cn("grid grid-cols-1 overflow-hidden rounded-2xl md:grid-cols-2", className)}
206
+ >
207
+ <div className="p-5">
208
+ {venue.href ? (
209
+ <LinkComponent
210
+ href={venue.href}
211
+ className="block rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
212
+ >
213
+ {venueContent ?? venueHeader}
214
+ </LinkComponent>
215
+ ) : (
216
+ venueContent ?? venueHeader
217
+ )}
218
+
219
+ <div>
220
+ {nightsContent ??
221
+ nights.map((night) => {
222
+ const row = (
223
+ <>
224
+ <Separator className="absolute inset-x-0 top-0" />
225
+
226
+ <span className="w-11 shrink-0 font-mono text-xs text-muted-foreground">
227
+ {night.dayLabel}
228
+ </span>
229
+ {/* Event title, wrapped in full — same rule as every other
230
+ event surface. The day and time columns keep their fixed
231
+ widths, so a two-line title only makes the row taller. */}
232
+ <span className="min-w-0 flex-1 text-[13.5px] font-semibold leading-[1.35]">
233
+ {night.title}
234
+ </span>
235
+ <span className="shrink-0 font-mono text-xs text-muted-foreground">
236
+ {night.timeLabel}
237
+ </span>
238
+ </>
239
+ );
240
+
241
+ const rowClasses = "relative flex items-center gap-[11px] py-[9px]";
242
+
243
+ return night.href ? (
244
+ <LinkComponent
245
+ key={night.id}
246
+ href={night.href}
247
+ className={cn(
248
+ rowClasses,
249
+ "cursor-pointer transition-colors duration-150 hover:text-primary motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-md"
250
+ )}
251
+ >
252
+ {row}
253
+ </LinkComponent>
254
+ ) : (
255
+ <div key={night.id} className={rowClasses}>
256
+ {row}
257
+ </div>
258
+ );
259
+ })}
260
+ </div>
261
+
262
+ {allEventsHref ? (
263
+ <LinkComponent
264
+ href={allEventsHref}
265
+ className="mt-3 inline-flex h-9 items-center rounded-full border border-border px-3 text-xs font-semibold text-foreground transition-colors duration-150 hover:bg-muted motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
266
+ >
267
+ {allEventsLabel} ›
268
+ </LinkComponent>
269
+ ) : (
270
+ <div className="mt-3 inline-flex h-9 items-center rounded-full border border-border px-3 text-xs font-semibold text-foreground">
271
+ {allEventsLabel} ›
272
+ </div>
273
+ )}
274
+ </div>
275
+
276
+ <div className="relative bg-muted/50 px-[18px] pb-5 pt-4">
277
+ <Separator className="absolute inset-x-0 top-0 md:hidden" />
278
+ <Separator orientation="vertical" className="absolute inset-y-0 left-0 hidden md:block" />
279
+
280
+ {menuHeader}
281
+
282
+ {menuContent ??
283
+ dishes.map((dish) => (
284
+ <div
285
+ key={dish.id}
286
+ className="relative flex items-start gap-3 py-3 first:[&>[data-slot=separator]]:hidden"
287
+ >
288
+ <Separator className="absolute inset-x-0 top-0" />
289
+
290
+ <div className="min-w-0 flex-1">
291
+ <div className="text-[15px] font-medium leading-[1.3]">{dish.name}</div>
292
+ {dish.description && (
293
+ <div className="mt-[3px] line-clamp-1 text-xs text-muted-foreground">
294
+ {dish.description}
295
+ </div>
296
+ )}
297
+ <div className="mt-1.5 flex items-center gap-2 text-[11px] tabular-nums text-muted-foreground">
298
+ {dish.quantityLabel && <span>{dish.quantityLabel}</span>}
299
+ {dish.mentionsLabel && (
300
+ <span className="ml-auto inline-flex items-center gap-1 text-[11.5px] text-muted-foreground">
301
+ <Quote className="h-3 w-3" aria-hidden />
302
+ {dish.mentionsLabel}
303
+ </span>
304
+ )}
305
+ </div>
306
+ </div>
307
+ <span className="shrink-0 text-sm font-bold text-primary">{dish.priceLabel}</span>
308
+ </div>
309
+ ))}
310
+
311
+ {footnote && <div className="mt-3 text-xs text-muted-foreground">{footnote}</div>}
312
+ </div>
313
+ </Card>
314
+ );
315
+ }
316
+
317
+ export type VenueEventsBlockSkeletonProps = {
318
+ /**
319
+ * Mirrors `VenueEventsBlockProps.venueContent`: a caller rendering its own
320
+ * place card through that slot should pass its own card-shaped skeleton
321
+ * here too, so the loading state matches what actually mounts instead of
322
+ * the block's built-in thumbnail-and-two-lines placeholder.
323
+ */
324
+ venueContent?: React.ReactNode;
325
+ /**
326
+ * Mirrors `VenueEventsBlockProps.nightsContent`: a caller rendering its own
327
+ * row component through that slot should pass its own row-shaped
328
+ * skeletons here too, so the loading state matches what actually mounts
329
+ * instead of three generic placeholder rows.
330
+ */
331
+ nightsContent?: React.ReactNode;
332
+ /**
333
+ * Mirrors `VenueEventsBlockProps.menuContent`: a caller rendering its own
334
+ * row component through that slot should pass its own row-shaped skeleton
335
+ * here too, so the loading state matches what actually mounts instead of
336
+ * three generic placeholder rows.
337
+ */
338
+ menuContent?: React.ReactNode;
339
+ className?: string;
340
+ };
341
+
342
+ export function VenueEventsBlockSkeleton({
343
+ venueContent,
344
+ nightsContent,
345
+ menuContent,
346
+ className,
347
+ }: VenueEventsBlockSkeletonProps) {
348
+ return (
349
+ // Identical root to the component above, deliberately — the two drifted
350
+ // once already by both hand-rolling it, so they now share the variant.
351
+ <Card
352
+ aria-hidden="true"
353
+ variant="elevatedPanel"
354
+ className={cn("grid grid-cols-1 overflow-hidden rounded-2xl md:grid-cols-2", className)}
355
+ >
356
+ <div className="p-5">
357
+ {venueContent ?? (
358
+ <div className="flex items-center gap-3 rounded-xl border border-border bg-card p-3">
359
+ <Skeleton className="h-16 w-16 shrink-0 rounded-lg" />
360
+ <div className="min-w-0 flex-1">
361
+ <Skeleton className="h-4 w-2/5" />
362
+ <Skeleton className="mt-1.5 h-3 w-1/3" />
363
+ </div>
364
+ </div>
365
+ )}
366
+
367
+ <div>
368
+ {nightsContent ??
369
+ Array.from({ length: 3 }, (_, index) => (
370
+ <div key={index} className="relative flex items-center gap-[11px] py-[9px]">
371
+ <Separator className="absolute inset-x-0 top-0" />
372
+
373
+ <Skeleton className="h-3 w-11 shrink-0" />
374
+ <Skeleton className="h-[13.5px] flex-1" />
375
+ <Skeleton className="h-3 w-10 shrink-0" />
376
+ </div>
377
+ ))}
378
+ </div>
379
+
380
+ <Skeleton className="mt-3 h-9 w-40 rounded-full" />
381
+ </div>
382
+
383
+ <div className="relative bg-muted/50 px-[18px] pb-5 pt-4">
384
+ <Separator className="absolute inset-x-0 top-0 md:hidden" />
385
+ <Separator orientation="vertical" className="absolute inset-y-0 left-0 hidden md:block" />
386
+
387
+ <Skeleton className="mb-2 h-4 w-32" />
388
+
389
+ {menuContent ??
390
+ Array.from({ length: 3 }, (_, index) => (
391
+ <div
392
+ key={index}
393
+ className="relative flex items-start gap-3 py-3 first:[&>[data-slot=separator]]:hidden"
394
+ >
395
+ <Separator className="absolute inset-x-0 top-0" />
396
+
397
+ <div className="min-w-0 flex-1">
398
+ <Skeleton className="h-[15px] w-3/5" />
399
+ <Skeleton className="mt-[3px] h-3 w-4/5" />
400
+ <Skeleton className="mt-1.5 h-3 w-1/4" />
401
+ </div>
402
+ <Skeleton className="h-4 w-12 shrink-0" />
403
+ </div>
404
+ ))}
405
+ </div>
406
+ </Card>
407
+ );
408
+ }
@@ -0,0 +1,92 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { WeekendPanel, WeekendPanelSkeleton } from "./weekend-panel";
3
+
4
+ function fakeCard(label: string) {
5
+ return (
6
+ <div className="flex flex-col gap-1.5">
7
+ <div className="aspect-[4/3] w-full rounded-xl bg-muted" />
8
+ <div className="text-sm font-semibold">{label}</div>
9
+ <div className="text-xs text-muted-foreground">Atelier Café · 20:00</div>
10
+ </div>
11
+ );
12
+ }
13
+
14
+ // Explicitly annotated rather than `satisfies`: the meta-level decorator makes
15
+ // the inferred type reference storybook's internal CSF module, which tsc cannot
16
+ // name portably (TS2742).
17
+ const meta: Meta<typeof WeekendPanel> = {
18
+ title: "Events/WeekendPanel",
19
+ component: WeekendPanel,
20
+ tags: ["autodocs"],
21
+ args: {
22
+ dayLabel: "Friday",
23
+ countLabel: "23 events",
24
+ actionLabel: "See all",
25
+ actionHref: "/events?day=friday",
26
+ labels: { pause: "Pause carousel", play: "Play carousel" },
27
+ children: (
28
+ <>
29
+ {fakeCard("Psihotrop — Infinity")}
30
+ {fakeCard("Warehouse Sessions")}
31
+ {fakeCard("Jazz at the Attic")}
32
+ {fakeCard("Open Mic Night")}
33
+ </>
34
+ ),
35
+ },
36
+ decorators: [
37
+ (Story) => (
38
+ <div className="w-[380px]">
39
+ <Story />
40
+ </div>
41
+ ),
42
+ ],
43
+ parameters: {
44
+ docs: {
45
+ description: {
46
+ component:
47
+ "One day panel from the weekend section. Built for exactly four children (two pages of two) — the slide keyframes measure `translateX(-100% - gap)`, i.e. exactly one page. Auto-advances on a loop paused by hover, focus, and off-screen, and by a visible pause/play toggle next to the progress dots — the actual WCAG 2.2.2 mechanism, since hover/focus aren't reachable by every user. Falls back to a static two-column grid (control omitted) under `prefers-reduced-motion`.",
48
+ },
49
+ },
50
+ },
51
+ };
52
+
53
+ export default meta;
54
+ type Story = StoryObj<typeof WeekendPanel>;
55
+
56
+ export const Default: Story = {};
57
+
58
+ /** The bordered, primary-tinted panel — the design's "today" or "this weekend" highlight. */
59
+ export const Highlighted: Story = {
60
+ args: { isHighlighted: true },
61
+ };
62
+
63
+ /** Sibling panels stagger their loop so three side-by-side panels don't slide in lockstep. */
64
+ export const SlowerLoop: Story = {
65
+ args: { durationSeconds: 10 },
66
+ };
67
+
68
+ /**
69
+ * The WCAG 2.2.2 mechanism: click the pause/play toggle next to the dots.
70
+ * Manual pause is a latch — it stays paused until pressed again, independent
71
+ * of hover/focus, and stops the track and both dot animations together.
72
+ */
73
+ export const PauseControl: Story = {
74
+ args: { durationSeconds: 3 },
75
+ parameters: {
76
+ docs: {
77
+ description: {
78
+ story:
79
+ "Toggle the button next to the progress dots to pause/resume the carousel. Unlike hover-pause, this state persists while reading — the required manual mechanism for WCAG 2.2.2.",
80
+ },
81
+ },
82
+ },
83
+ };
84
+
85
+ /** The panel at rest — the two visible cards of the two-up carousel, no slide. */
86
+ export const Skeleton: Story = {
87
+ render: () => (
88
+ <div className="w-[380px]">
89
+ <WeekendPanelSkeleton />
90
+ </div>
91
+ ),
92
+ };