@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,181 @@
1
+ import type { ComponentType, ReactNode } from "react";
2
+ import { cn } from "../lib/utils";
3
+ import { EventPosterFallback } from "./event-poster-fallback";
4
+ import { Separator } from "./separator";
5
+ import { Tag } from "./tag";
6
+ import { TagGroup } from "./tag-group";
7
+
8
+ /**
9
+ * EventIndexRow — the row of an exhaustive, crawlable event index.
10
+ *
11
+ * NOT `<EventListRowWide>`: that row belongs to the list VIEW, and it carries
12
+ * a price column and a Reserve chip because the list view is where you act.
13
+ * This row makes no claim to be operable — it is a catalogue entry. Its only
14
+ * affordances are two real links: the title (stretched over the whole row via
15
+ * `after:absolute after:inset-0`) and the venue (escaping that overlay with
16
+ * `relative z-10`, the same nested-link pattern `<EventListRowWide>` uses for
17
+ * `menuHref`). That venue link is why this could not be a variant of
18
+ * `<EventCompactRow>` or `<EventListRow>`: both wrap their entire body in one
19
+ * anchor, so a second link inside would be a nested `<a>` — invalid HTML that
20
+ * browsers unnest, breaking both links.
21
+ *
22
+ * NO `"use client"`, AND THAT IS LOAD-BEARING. This row's surface is a
23
+ * server-only prerendered page carrying ~50 of them, and every sibling row in
24
+ * this family is a client component for one reason: `useState` to catch
25
+ * `<img onError>`. Here the fallback is layered BEHIND the poster instead, so
26
+ * a failed image reveals the designed gradient with no JS at all. The
27
+ * consequence to accept: an image that returns 200 but decodes to garbage
28
+ * shows an empty box rather than the fallback.
29
+ *
30
+ * THE DESCRIPTION IS NEVER TRUNCATED IN THE MARKUP — it is clamped with CSS
31
+ * (`line-clamp`), so the full text stays in the DOM. Callers must pass the
32
+ * whole string. On this surface that is the entire point: the text is the
33
+ * page's body copy, and a row that cut it at N characters would be deleting
34
+ * the thing the section exists to carry.
35
+ *
36
+ * ROW RULES: this row draws its own top rule, same as `<EventListRow>` and
37
+ * `<EventListRowWide>`. Pass `hideRule` on the first row under a heading that
38
+ * already ends in one (`<EventDaySeparator>` does), or you get two rules a few
39
+ * pixels apart.
40
+ */
41
+
42
+ export type EventIndexRowLinkProps = {
43
+ href: string;
44
+ className?: string;
45
+ children: ReactNode;
46
+ };
47
+
48
+ function DefaultLink({ href, ...props }: EventIndexRowLinkProps) {
49
+ return <a href={href} {...props} />;
50
+ }
51
+
52
+ export type EventIndexRowProps = {
53
+ /** Rendered inside `as` and allowed to WRAP — a document-outline heading is never truncated. */
54
+ title: string;
55
+ /** Heading level for `title`. Defaults to `h3`, one below the group headings. */
56
+ as?: "h2" | "h3" | "h4";
57
+ href: string;
58
+ /** Leading emphasised fact on the meta line — caller-composed date + time, e.g. "26 iulie, 20:00–23:00". */
59
+ when: string;
60
+ venue?: string;
61
+ /** Makes the venue a real second link. Omit for a venue with no place page. */
62
+ venueHref?: string;
63
+ /** Plain text, never a chip: "intrare liberă" / "bilet 40 lei". */
64
+ fee?: string;
65
+ /** Display labels. `EventPreview.tags` is taxonomy coordinates the DS cannot translate, so the caller resolves and caps them. */
66
+ tags?: string[];
67
+ /** FULL text. Clamped to 3 lines (mobile) / 2 (md+) in CSS only. */
68
+ description?: string;
69
+ posterUrl?: string;
70
+ fallback?: ReactNode;
71
+ /** Suppresses the top rule. Use on the first row under a heading that draws its own. */
72
+ hideRule?: boolean;
73
+ linkComponent?: ComponentType<EventIndexRowLinkProps>;
74
+ className?: string;
75
+ };
76
+
77
+ export function EventIndexRow({
78
+ title,
79
+ as: Heading = "h3",
80
+ href,
81
+ when,
82
+ venue,
83
+ venueHref,
84
+ fee,
85
+ tags,
86
+ description,
87
+ posterUrl,
88
+ fallback = <EventPosterFallback />,
89
+ hideRule,
90
+ linkComponent: LinkComponent = DefaultLink,
91
+ className,
92
+ }: EventIndexRowProps) {
93
+ const dot = (
94
+ <span className="px-1 text-border" aria-hidden>
95
+ ·
96
+ </span>
97
+ );
98
+
99
+ return (
100
+ <div className={cn("relative", className)}>
101
+ {!hideRule && <Separator className="absolute inset-x-0 top-0" />}
102
+
103
+ <div className="relative -mx-3 flex gap-3 rounded-lg px-3 py-4 transition-colors duration-150 hover:bg-card/60 motion-reduce:transition-none md:gap-4 md:py-5">
104
+ {/* Square poster at the same 72/104 the rest of the row family uses —
105
+ the editorial surfaces above this one are 4:3, but a third ratio
106
+ inside the ROW family would break the sibling-shape rule for one
107
+ section's sake. The fallback sits behind the image rather than
108
+ beside it in a ternary, which is what lets this whole row stay a
109
+ server component. */}
110
+ <div className="relative aspect-square w-[72px] shrink-0 overflow-hidden rounded-[9px] bg-muted md:w-[104px]">
111
+ <div className="absolute inset-0">{fallback}</div>
112
+ {posterUrl && (
113
+ // eslint-disable-next-line @next/next/no-img-element
114
+ <img
115
+ src={posterUrl}
116
+ alt=""
117
+ loading="lazy"
118
+ decoding="async"
119
+ className="absolute inset-0 h-full w-full object-cover"
120
+ />
121
+ )}
122
+ </div>
123
+
124
+ <div className="min-w-0 flex-1">
125
+ <Heading className="text-[15px] font-bold leading-snug tracking-[-0.01em] text-foreground md:text-[16.5px]">
126
+ <LinkComponent
127
+ href={href}
128
+ className="rounded-sm transition-colors duration-150 after:absolute after:inset-0 hover:text-primary motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
129
+ >
130
+ {title}
131
+ </LinkComponent>
132
+ </Heading>
133
+
134
+ <p className="mt-1 text-[12px] leading-5 text-muted-foreground md:text-[12.5px]">
135
+ <span className="font-semibold tabular-nums text-foreground">{when}</span>
136
+ {venue && (
137
+ <>
138
+ {dot}
139
+ {venueHref ? (
140
+ <LinkComponent
141
+ href={venueHref}
142
+ className="relative z-10 rounded-sm font-semibold text-foreground/85 underline decoration-border underline-offset-2 transition-colors duration-150 hover:text-primary hover:decoration-primary motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
143
+ >
144
+ {venue}
145
+ </LinkComponent>
146
+ ) : (
147
+ <span className="font-semibold text-foreground/85">{venue}</span>
148
+ )}
149
+ </>
150
+ )}
151
+ {fee && (
152
+ <>
153
+ {dot}
154
+ {fee}
155
+ </>
156
+ )}
157
+ </p>
158
+
159
+ {tags && tags.length > 0 && (
160
+ // `outline`, not the default: default `Tag` is `bg-muted`, and this
161
+ // row's surface is a `bg-muted/50` band — a 0.019-L delta in light
162
+ // mode, which is invisible. `outline` reads on both.
163
+ <TagGroup className="mt-2">
164
+ {tags.map((tag) => (
165
+ <Tag key={tag} size="sm" variant="outline">
166
+ {tag}
167
+ </Tag>
168
+ ))}
169
+ </TagGroup>
170
+ )}
171
+
172
+ {description && (
173
+ <p className="mt-2 line-clamp-3 max-w-[620px] text-[13px] leading-[1.6] text-muted-foreground md:line-clamp-2 md:text-[13.5px] md:leading-[1.55]">
174
+ {description}
175
+ </p>
176
+ )}
177
+ </div>
178
+ </div>
179
+ </div>
180
+ );
181
+ }
@@ -0,0 +1,204 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { EventListRowWide, EventListRowWideSkeleton } from "./event-list-row-wide";
3
+ import type { EventListRowWideLabels } from "./event-list-row-wide";
4
+ import type { EventDateBadgeFormatters, EventPreview } from "./event-types";
5
+
6
+ const labels: EventListRowWideLabels = {
7
+ free: "Free",
8
+ liveNow: "On now",
9
+ reserve: "Reserve",
10
+ notifyMe: "Notify me",
11
+ soldOut: "Sold out",
12
+ seeMenu: "See the menu",
13
+ };
14
+
15
+ const formatters: EventDateBadgeFormatters = {
16
+ time: (iso) => new Date(iso).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" }),
17
+ day: (iso) =>
18
+ new Date(iso).toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short" }),
19
+ price: (amount) => `${amount} RON`,
20
+ dayNumber: (iso) => String(new Date(iso).getDate()).padStart(2, "0"),
21
+ monthShort: (iso) =>
22
+ new Date(iso).toLocaleDateString("en-GB", { month: "short" }).toUpperCase(),
23
+ };
24
+
25
+ const event = (
26
+ id: string,
27
+ title: string,
28
+ place: string,
29
+ startsAt: string,
30
+ fee?: number,
31
+ description?: string
32
+ ): EventPreview => ({
33
+ id,
34
+ title,
35
+ location: place,
36
+ place_name: place,
37
+ starts_at: startsAt,
38
+ entry_fee: fee,
39
+ description,
40
+ });
41
+
42
+ const description =
43
+ "Rooftop set from sunset to close, resident DJs on the decks all night, kitchen open until midnight with the full grill menu.";
44
+
45
+ const meta: Meta<typeof EventListRowWide> = {
46
+ title: "Events/EventListRowWide",
47
+ component: EventListRowWide,
48
+ tags: ["autodocs"],
49
+ args: { labels, formatters },
50
+ decorators: [
51
+ (Story) => (
52
+ <div className="w-[860px]">
53
+ <Story />
54
+ </div>
55
+ ),
56
+ ],
57
+ parameters: {
58
+ docs: {
59
+ description: {
60
+ component:
61
+ "The desktop list row — denser than the poster card, richer than the mobile list row, with a description and a dedicated action column. The row root is a plain container: the title is the primary link with a stretched overlay (so the whole row stays one click target), and 'See the menu' — rendered only when `menuHref` is set — is a second, real link layered above the overlay so it wins clicks in its own area.",
62
+ },
63
+ },
64
+ },
65
+ };
66
+
67
+ export default meta;
68
+ type Story = StoryObj<typeof EventListRowWide>;
69
+
70
+ export const Default: Story = {
71
+ args: {
72
+ event: event("1", "Psihotrop Live", "Atelier Café", "2026-09-05T20:00:00", 50, description),
73
+ tags: ["Live music", "Rooftop"],
74
+ },
75
+ };
76
+
77
+ /**
78
+ * The nested-link pattern: the title is the primary link (stretched over the
79
+ * whole row via `after:absolute after:inset-0`), and "See the menu" is a real,
80
+ * independently targeted link that wins clicks in its own area.
81
+ */
82
+ export const WithMenuLink: Story = {
83
+ args: {
84
+ event: event("1b", "Psihotrop Live", "Atelier Café", "2026-09-05T20:00:00", 50, description),
85
+ tags: ["Live music", "Rooftop"],
86
+ href: "/events/psihotrop-live",
87
+ menuHref: "/places/atelier-cafe/menu",
88
+ onClick: (_event, mouseEvent) => mouseEvent.preventDefault(),
89
+ },
90
+ parameters: {
91
+ docs: {
92
+ description: {
93
+ story:
94
+ "Click the title anywhere in the row to open the event; click 'See the menu' to go to the venue's menu instead — both are real links, not one anchor with dead text nested inside it.",
95
+ },
96
+ },
97
+ },
98
+ };
99
+
100
+ /** No `menuHref` — the affordance is omitted entirely rather than rendered as dead text. */
101
+ export const WithoutMenuLink: Story = {
102
+ args: {
103
+ event: event("1c", "Psihotrop Live", "Atelier Café", "2026-09-05T20:00:00", 50, description),
104
+ tags: ["Live music", "Rooftop"],
105
+ href: "/events/psihotrop-live",
106
+ onClick: (_event, mouseEvent) => mouseEvent.preventDefault(),
107
+ },
108
+ };
109
+
110
+ export const FreeEntry: Story = {
111
+ args: {
112
+ event: event("2", "Vinyl Happy Hour", "Zorki Photo Cafe", "2026-09-04T18:00:00", 0, description),
113
+ tags: ["Vinyl", "Happy hour"],
114
+ },
115
+ };
116
+
117
+ /** No published fee is "TBC", never "Free". */
118
+ export const UnknownPrice: Story = {
119
+ args: { event: event("3", "Quiz Night", "Flying Circus", "2026-09-03T19:00:00") },
120
+ };
121
+
122
+ /** Live tints the date badge and adds the on-air chip. */
123
+ export const LiveNow: Story = {
124
+ args: {
125
+ event: event("4", "Charity Concert & Afterparty", "Flying Circus", "2026-09-03T21:00:00", 30, description),
126
+ isLive: true,
127
+ tags: ["Charity", "Concert"],
128
+ },
129
+ };
130
+
131
+ /** Sold out dims the row and swaps the CTA to "Notify me". */
132
+ export const SoldOut: Story = {
133
+ args: {
134
+ event: event("5", "Friday Night Techno", "Form Space", "2026-09-04T22:00:00", 50, description),
135
+ isSoldOut: true,
136
+ tags: ["Techno", "Club night"],
137
+ },
138
+ };
139
+
140
+ /** No `description` on the event — the block collapses instead of leaving a gap. */
141
+ export const NoDescription: Story = {
142
+ args: {
143
+ event: event("6", "Stand-up Comedy Night", "Klausen Pubhouse", "2026-09-04T19:30:00", 45),
144
+ tags: ["Comedy"],
145
+ },
146
+ };
147
+
148
+ /** Loading state — identical geometry to `Default`, so nothing shifts on data arrival. */
149
+ export const Loading: Story = {
150
+ render: () => <EventListRowWideSkeleton />,
151
+ };
152
+
153
+ /** `NoDescription`'s narrower geometry, loading. */
154
+ export const LoadingNoDescription: Story = {
155
+ render: () => <EventListRowWideSkeleton showDescription={false} />,
156
+ };
157
+
158
+ /** A loading row above a loaded one, so any geometry drift is visible at a glance. */
159
+ export const LoadingVsLoaded: Story = {
160
+ render: () => (
161
+ <div>
162
+ <EventListRowWideSkeleton />
163
+ <EventListRowWide
164
+ event={event("1", "Psihotrop Live", "Atelier Café", "2026-09-05T20:00:00", 50, description)}
165
+ labels={labels}
166
+ formatters={formatters}
167
+ tags={["Live music", "Rooftop"]}
168
+ />
169
+ </div>
170
+ ),
171
+ };
172
+
173
+ /** A day's worth of rows, as the desktop list view stacks them. */
174
+ export const ADay: Story = {
175
+ render: () => (
176
+ <div>
177
+ <EventListRowWide
178
+ event={event("a", "Psihotrop Live", "Atelier Café", "2026-09-03T20:00:00", 50, description)}
179
+ labels={labels}
180
+ formatters={formatters}
181
+ tags={["Live music", "Rooftop"]}
182
+ isLive
183
+ />
184
+ <EventListRowWide
185
+ event={event("b", "Quiz Night", "Flying Circus", "2026-09-03T20:00:00")}
186
+ labels={labels}
187
+ formatters={formatters}
188
+ />
189
+ <EventListRowWide
190
+ event={event("c", "Beer Battle", "Irish Music Pub", "2026-09-03T20:00:00", 0, description)}
191
+ labels={labels}
192
+ formatters={formatters}
193
+ tags={["Nightlife"]}
194
+ />
195
+ <EventListRowWide
196
+ event={event("e", "Schoolotheque Afterparty", "Flying Circus", "2026-09-03T22:00:00", 25, description)}
197
+ labels={labels}
198
+ formatters={formatters}
199
+ tags={["Afterparty"]}
200
+ isSoldOut
201
+ />
202
+ </div>
203
+ ),
204
+ };