@bearmenu/ui 0.8.8 → 0.8.10

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 (206) hide show
  1. package/dist/chunk-24RFI4RQ.js +167 -0
  2. package/dist/chunk-2XCZJWTV.js +121 -0
  3. package/dist/chunk-5RWTOT5Y.js +72 -0
  4. package/dist/chunk-62ES6N32.js +27 -0
  5. package/dist/chunk-AKQ57NXY.js +58 -0
  6. package/dist/chunk-CLLY6XB3.js +41 -0
  7. package/dist/chunk-DTXNDLSE.js +125 -0
  8. package/dist/chunk-F6FMZJDG.js +113 -0
  9. package/dist/{chunk-2WDJR6K7.js → chunk-G7IFL4Z4.js} +4 -1
  10. package/dist/chunk-MZL7CGCL.js +109 -0
  11. package/dist/chunk-Q6QH52LJ.js +38 -0
  12. package/dist/chunk-SY6RV476.js +48 -0
  13. package/dist/chunk-TH6W2PWG.js +213 -0
  14. package/dist/chunk-UDN7UHJW.js +60 -0
  15. package/dist/{chunk-TT3WFS6S.js → chunk-YOKHBAIH.js} +11 -11
  16. package/dist/components/alert.d.ts +2 -2
  17. package/dist/components/badge.d.ts +1 -1
  18. package/dist/components/concept-compare-rail.d.ts +50 -0
  19. package/dist/components/concept-compare-rail.js +5 -0
  20. package/dist/components/concept-compare.d.ts +110 -0
  21. package/dist/components/concept-compare.js +238 -0
  22. package/dist/components/concept-convergence.d.ts +41 -0
  23. package/dist/components/concept-convergence.js +53 -0
  24. package/dist/components/convergence.d.ts +25 -0
  25. package/dist/components/convergence.js +4 -0
  26. package/dist/components/date-input.d.ts +1 -1
  27. package/dist/components/event-card.js +1 -1
  28. package/dist/components/event-compact-row.d.ts +1 -1
  29. package/dist/components/event-compact-row.js +1 -1
  30. package/dist/components/event-detail-panel.d.ts +171 -25
  31. package/dist/components/event-detail-panel.js +472 -175
  32. package/dist/components/event-index-row.js +1 -1
  33. package/dist/components/event-list-row-wide.js +2 -2
  34. package/dist/components/event-list-row.js +2 -2
  35. package/dist/components/explainer-steps.d.ts +34 -0
  36. package/dist/components/explainer-steps.js +36 -0
  37. package/dist/components/glass-ground.d.ts +66 -0
  38. package/dist/components/glass-ground.js +3 -0
  39. package/dist/components/hub-surface.d.ts +27 -0
  40. package/dist/components/hub-surface.js +30 -0
  41. package/dist/components/hue-chip.d.ts +70 -0
  42. package/dist/components/hue-chip.js +3 -0
  43. package/dist/components/kitchen-dossier.d.ts +88 -0
  44. package/dist/components/kitchen-dossier.js +187 -0
  45. package/dist/components/menu-dead-end.d.ts +56 -0
  46. package/dist/components/menu-dead-end.js +49 -0
  47. package/dist/components/menu-family-block.d.ts +99 -0
  48. package/dist/components/menu-family-block.js +155 -0
  49. package/dist/components/menu-family-card.d.ts +65 -0
  50. package/dist/components/menu-family-card.js +79 -0
  51. package/dist/components/menu-field.d.ts +57 -0
  52. package/dist/components/menu-field.js +93 -0
  53. package/dist/components/menu-filter-chips.d.ts +86 -0
  54. package/dist/components/menu-filter-chips.js +122 -0
  55. package/dist/components/menu-group-rows.d.ts +53 -0
  56. package/dist/components/menu-group-rows.js +67 -0
  57. package/dist/components/menu-hub-hero.d.ts +65 -0
  58. package/dist/components/menu-hub-hero.js +103 -0
  59. package/dist/components/menu-index-header.d.ts +37 -0
  60. package/dist/components/menu-index-header.js +68 -0
  61. package/dist/components/menu-index-list.d.ts +48 -0
  62. package/dist/components/menu-index-list.js +96 -0
  63. package/dist/components/menu-intent-hero.d.ts +69 -0
  64. package/dist/components/menu-intent-hero.js +87 -0
  65. package/dist/components/menu-intent-tiles.d.ts +31 -0
  66. package/dist/components/menu-intent-tiles.js +50 -0
  67. package/dist/components/menu-item.d.ts +127 -0
  68. package/dist/components/menu-item.js +5 -0
  69. package/dist/components/menu-kitchen-list.d.ts +82 -0
  70. package/dist/components/menu-kitchen-list.js +5 -0
  71. package/dist/components/menu-mocks.d.ts +90 -0
  72. package/dist/components/menu-mocks.js +504 -0
  73. package/dist/components/menu-types.d.ts +421 -0
  74. package/dist/components/menu-types.js +1 -0
  75. package/dist/components/motion-icon-controls.js +2 -2
  76. package/dist/components/motion-icon-set.js +21 -9
  77. package/dist/components/motion-icon.d.ts +22 -1
  78. package/dist/components/motion-icon.js +1 -1
  79. package/dist/components/open-now-list.d.ts +40 -0
  80. package/dist/components/open-now-list.js +69 -0
  81. package/dist/components/photo-ground.d.ts +64 -0
  82. package/dist/components/photo-ground.js +3 -0
  83. package/dist/components/place-about-tab.js +2 -2
  84. package/dist/components/place-details-mobile.js +2 -2
  85. package/dist/components/place-types.d.ts +112 -1
  86. package/dist/components/qa-block.d.ts +36 -0
  87. package/dist/components/qa-block.js +24 -0
  88. package/dist/components/scroll-fade.js +3 -121
  89. package/dist/components/signature-chip.d.ts +37 -0
  90. package/dist/components/signature-chip.js +4 -0
  91. package/dist/components/snap-rail.d.ts +48 -5
  92. package/dist/components/snap-rail.js +3 -51
  93. package/dist/components/stat-banner.d.ts +47 -0
  94. package/dist/components/stat-banner.js +61 -0
  95. package/dist/components/story.d.ts +34 -0
  96. package/dist/components/story.js +3 -0
  97. package/dist/components/tag.d.ts +2 -2
  98. package/dist/components/text.d.ts +1 -1
  99. package/dist/components/top-comparison.d.ts +28 -0
  100. package/dist/components/top-comparison.js +76 -0
  101. package/dist/components/top-cross-link.d.ts +34 -0
  102. package/dist/components/top-cross-link.js +101 -0
  103. package/dist/components/top-ranking-entry.d.ts +53 -0
  104. package/dist/components/top-ranking-entry.js +217 -0
  105. package/dist/components/top-ranking-hero.d.ts +61 -0
  106. package/dist/components/top-ranking-hero.js +99 -0
  107. package/dist/components/top-ranking-legend.d.ts +69 -0
  108. package/dist/components/top-ranking-legend.js +124 -0
  109. package/dist/components/top-types.d.ts +109 -0
  110. package/dist/components/top-types.js +1 -0
  111. package/dist/components/venue-grade-dimensions.d.ts +38 -0
  112. package/dist/components/venue-grade-dimensions.js +3 -0
  113. package/dist/components/venue-quotes.d.ts +32 -0
  114. package/dist/components/venue-quotes.js +3 -0
  115. package/dist/components/venue-wall.d.ts +41 -0
  116. package/dist/components/venue-wall.js +118 -0
  117. package/dist/components/weekday-theme-banner.d.ts +70 -0
  118. package/dist/components/weekday-theme-banner.js +127 -0
  119. package/dist/components/weekend-panel.js +2 -2
  120. package/package.json +1 -1
  121. package/src/components/colour-dictionary.stories.tsx +227 -0
  122. package/src/components/concept-compare-rail.tsx +131 -0
  123. package/src/components/concept-compare.stories.tsx +172 -0
  124. package/src/components/concept-compare.tsx +443 -0
  125. package/src/components/concept-convergence.stories.tsx +53 -0
  126. package/src/components/concept-convergence.tsx +90 -0
  127. package/src/components/convergence.stories.tsx +54 -0
  128. package/src/components/convergence.tsx +177 -0
  129. package/src/components/event-compact-row.tsx +1 -1
  130. package/src/components/event-detail-panel.stories.tsx +205 -97
  131. package/src/components/event-detail-panel.test.tsx +181 -0
  132. package/src/components/event-detail-panel.tsx +703 -185
  133. package/src/components/explainer-steps.stories.tsx +46 -0
  134. package/src/components/explainer-steps.tsx +74 -0
  135. package/src/components/glass-ground.tsx +129 -0
  136. package/src/components/hub-surface.stories.tsx +68 -0
  137. package/src/components/hub-surface.tsx +50 -0
  138. package/src/components/hue-chip.stories.tsx +76 -0
  139. package/src/components/hue-chip.test.tsx +62 -0
  140. package/src/components/hue-chip.tsx +99 -0
  141. package/src/components/kitchen-dossier.stories.tsx +108 -0
  142. package/src/components/kitchen-dossier.tsx +322 -0
  143. package/src/components/menu-dead-end.stories.tsx +67 -0
  144. package/src/components/menu-dead-end.tsx +127 -0
  145. package/src/components/menu-family-block.stories.tsx +91 -0
  146. package/src/components/menu-family-block.tsx +314 -0
  147. package/src/components/menu-family-card.stories.tsx +120 -0
  148. package/src/components/menu-family-card.tsx +191 -0
  149. package/src/components/menu-field.stories.tsx +74 -0
  150. package/src/components/menu-field.tsx +199 -0
  151. package/src/components/menu-filter-chips.stories.tsx +107 -0
  152. package/src/components/menu-filter-chips.tsx +238 -0
  153. package/src/components/menu-group-rows.stories.tsx +54 -0
  154. package/src/components/menu-group-rows.tsx +133 -0
  155. package/src/components/menu-hub-hero.stories.tsx +92 -0
  156. package/src/components/menu-hub-hero.tsx +187 -0
  157. package/src/components/menu-index-header.stories.tsx +72 -0
  158. package/src/components/menu-index-header.tsx +125 -0
  159. package/src/components/menu-index-list.stories.tsx +50 -0
  160. package/src/components/menu-index-list.tsx +149 -0
  161. package/src/components/menu-intent-hero.stories.tsx +67 -0
  162. package/src/components/menu-intent-hero.tsx +189 -0
  163. package/src/components/menu-intent-tiles.stories.tsx +54 -0
  164. package/src/components/menu-intent-tiles.tsx +76 -0
  165. package/src/components/menu-item.tsx +416 -0
  166. package/src/components/menu-kitchen-list.stories.tsx +67 -0
  167. package/src/components/menu-kitchen-list.test.tsx +78 -0
  168. package/src/components/menu-kitchen-list.tsx +284 -0
  169. package/src/components/menu-mocks.ts +592 -0
  170. package/src/components/menu-types.ts +442 -0
  171. package/src/components/motion-icon-controls.tsx +11 -11
  172. package/src/components/motion-icon-set.tsx +36 -8
  173. package/src/components/motion-icon.test.tsx +23 -1
  174. package/src/components/motion-icon.tsx +20 -0
  175. package/src/components/open-now-list.stories.tsx +76 -0
  176. package/src/components/open-now-list.tsx +127 -0
  177. package/src/components/photo-ground.tsx +147 -0
  178. package/src/components/place-types.ts +112 -0
  179. package/src/components/qa-block.stories.tsx +43 -0
  180. package/src/components/qa-block.tsx +60 -0
  181. package/src/components/signature-chip.tsx +77 -0
  182. package/src/components/snap-rail.tsx +133 -9
  183. package/src/components/stat-banner.stories.tsx +100 -0
  184. package/src/components/stat-banner.tsx +117 -0
  185. package/src/components/story.tsx +225 -0
  186. package/src/components/top-comparison.stories.tsx +78 -0
  187. package/src/components/top-comparison.tsx +151 -0
  188. package/src/components/top-cross-link.stories.tsx +84 -0
  189. package/src/components/top-cross-link.tsx +171 -0
  190. package/src/components/top-ranking-entry.stories.tsx +171 -0
  191. package/src/components/top-ranking-entry.test.tsx +105 -0
  192. package/src/components/top-ranking-entry.tsx +425 -0
  193. package/src/components/top-ranking-hero.stories.tsx +76 -0
  194. package/src/components/top-ranking-hero.tsx +218 -0
  195. package/src/components/top-ranking-legend.stories.tsx +78 -0
  196. package/src/components/top-ranking-legend.tsx +239 -0
  197. package/src/components/top-types.ts +110 -0
  198. package/src/components/venue-grade-dimensions.stories.tsx +83 -0
  199. package/src/components/venue-grade-dimensions.tsx +91 -0
  200. package/src/components/venue-quotes.stories.tsx +59 -0
  201. package/src/components/venue-quotes.tsx +64 -0
  202. package/src/components/venue-wall.stories.tsx +78 -0
  203. package/src/components/venue-wall.tsx +185 -0
  204. package/src/components/weekday-theme-banner.stories.tsx +113 -0
  205. package/src/components/weekday-theme-banner.tsx +231 -0
  206. package/dist/{chunk-XWMRABZW.js → chunk-HSIYYA2E.js} +1 -1
@@ -0,0 +1,177 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+ import { motion, useReducedMotion, type Variants } from "framer-motion";
5
+ import { cn } from "../lib/utils";
6
+ import { duration, ease, springSnappy, stagger } from "../lib/motion";
7
+
8
+ /**
9
+ * Convergence — many things on the left becoming one thing on the right.
10
+ *
11
+ * The diagram the explainers are built on: several spellings of a dish
12
+ * converging on one concept; a column of reviews converging on one mention
13
+ * count. The DS draws the SHAPE — a stack of sources, hairlines curving into
14
+ * a single point, the target — and the caller draws the nodes, because what a
15
+ * source is (a chip, a redacted line, a thumbnail) is the caller's data.
16
+ *
17
+ * ## The motion tells the story
18
+ *
19
+ * Three beats on mount, overlapping, under 700ms: the many arrive (each
20
+ * source slides in from the left, staggered), the lines draw toward the one
21
+ * (each curve starts once its source is about half visible, so cause reads
22
+ * before effect), and the one lands (the target scales in on a snappy spring
23
+ * while the last lines are still in flight — it catches them rather than
24
+ * waiting for a clean hand-off). Self-contained: the sheet this sits in
25
+ * animates by CSS `data-state`, which nothing here could await, and the
26
+ * `delayChildren` already puts the content a beat behind the surface.
27
+ *
28
+ * `useReducedMotion` owns the fallback — the stylesheet's global rule only
29
+ * shortens CSS transitions and does not reach Framer's own scheduler. Under
30
+ * it the diagram renders settled and fades in once. `animate={false}` is a
31
+ * separate switch for static reuse (a docs frame), independent of the
32
+ * reader's setting.
33
+ *
34
+ * ## Why the lines are an SVG and not borders
35
+ *
36
+ * A curve from each source to one point cannot be a border on anything. The
37
+ * SVG stretches to the sources column's height (`preserveAspectRatio: none`)
38
+ * and keeps its hairlines at one pixel through `vector-effect`, so the curves
39
+ * follow however tall the stack is without the stroke thickening with it.
40
+ * `pathLength` is normalised by Framer to the path's own space, so the draw
41
+ * is unaffected by the stretch.
42
+ */
43
+
44
+ /** The first line starts once the first source is about half visible. */
45
+ const LINE_LEAD = 0.15;
46
+ /** The target lands before the last line finishes — it catches them. */
47
+ const TARGET_DELAY = 0.38;
48
+
49
+ const sourcesContainer: Variants = {
50
+ hidden: {},
51
+ show: { transition: { delayChildren: 0.06, staggerChildren: stagger.tight } },
52
+ };
53
+
54
+ const sourceItem: Variants = {
55
+ hidden: { opacity: 0, x: -8 },
56
+ show: { opacity: 1, x: 0, transition: { duration: duration.quick, ease: ease.enter } },
57
+ };
58
+
59
+ const targetItem: Variants = {
60
+ hidden: { opacity: 0, scale: 0.92 },
61
+ show: { opacity: 1, scale: 1, transition: { delay: TARGET_DELAY, ...springSnappy } },
62
+ };
63
+
64
+ export type ConvergeLinesProps = {
65
+ /** How many sources the lines leave from. */
66
+ count: number;
67
+ /** Draw the curves in, source by source. Off: drawn already. */
68
+ animate?: boolean;
69
+ className?: string;
70
+ };
71
+
72
+ const LINES_WIDTH = 40;
73
+ const LINES_HEIGHT = 100;
74
+
75
+ export function ConvergeLines({ count, animate = true, className }: ConvergeLinesProps) {
76
+ const mid = LINES_HEIGHT / 2;
77
+ return (
78
+ <svg
79
+ aria-hidden
80
+ viewBox={`0 0 ${LINES_WIDTH} ${LINES_HEIGHT}`}
81
+ preserveAspectRatio="none"
82
+ className={cn("h-full w-10 text-border", className)}
83
+ >
84
+ {Array.from({ length: count }, (_, index) => {
85
+ const y = ((index + 0.5) / count) * LINES_HEIGHT;
86
+ const d = `M0 ${y} C ${LINES_WIDTH / 2} ${y}, ${LINES_WIDTH / 2} ${mid}, ${LINES_WIDTH} ${mid}`;
87
+ const stroke = {
88
+ d,
89
+ fill: "none",
90
+ stroke: "currentColor",
91
+ strokeWidth: 1.5,
92
+ strokeLinecap: "round" as const,
93
+ vectorEffect: "non-scaling-stroke" as const,
94
+ };
95
+ return animate ? (
96
+ <motion.path
97
+ key={index}
98
+ {...stroke}
99
+ initial={{ pathLength: 0, opacity: 0 }}
100
+ animate={{ pathLength: 1, opacity: 1 }}
101
+ transition={{
102
+ delay: LINE_LEAD + index * stagger.tight,
103
+ duration: duration.standard,
104
+ ease: ease.standard,
105
+ }}
106
+ />
107
+ ) : (
108
+ <path key={index} {...stroke} />
109
+ );
110
+ })}
111
+ </svg>
112
+ );
113
+ }
114
+
115
+ export type ConvergenceProps = {
116
+ /** The many. One node each; the stack fans slightly so it reads as a spread, not a list. */
117
+ sources: ReactNode[];
118
+ /** The one. */
119
+ target: ReactNode;
120
+ /** The entrance choreography. Off for static reuse; the reader's reduced-motion setting is honoured either way. */
121
+ animate?: boolean;
122
+ /** The curves' colour, e.g. `"text-white/30"` on a dark stage. Defaults to the border. */
123
+ linesClassName?: string;
124
+ className?: string;
125
+ };
126
+
127
+ export function Convergence({
128
+ sources,
129
+ target,
130
+ animate = true,
131
+ linesClassName,
132
+ className,
133
+ }: ConvergenceProps) {
134
+ const reduce = useReducedMotion();
135
+ const run = animate && !reduce;
136
+ const fadeOnly = animate && reduce;
137
+
138
+ return (
139
+ <motion.div
140
+ className={cn("grid grid-cols-[minmax(0,1fr)_auto_auto] items-center", className)}
141
+ initial={fadeOnly ? { opacity: 0 } : false}
142
+ animate={fadeOnly ? { opacity: 1 } : undefined}
143
+ transition={{ duration: duration.quick, ease: ease.standard }}
144
+ >
145
+ <motion.div
146
+ className={cn(
147
+ "flex min-w-0 flex-col items-start justify-center gap-2 py-1",
148
+ /* The fan: the middle sources sit a little further right than the
149
+ ends, so five chips read as one hand of cards, not as a menu. */
150
+ "[&>*:nth-child(2)]:translate-x-1.5 [&>*:nth-child(3)]:translate-x-3 [&>*:nth-child(4)]:translate-x-1.5"
151
+ )}
152
+ variants={run ? sourcesContainer : undefined}
153
+ initial={run ? "hidden" : false}
154
+ animate={run ? "show" : undefined}
155
+ >
156
+ {sources.map((node, index) => (
157
+ <motion.div key={index} className="flex max-w-full" variants={run ? sourceItem : undefined}>
158
+ {node}
159
+ </motion.div>
160
+ ))}
161
+ </motion.div>
162
+
163
+ <div className="self-stretch">
164
+ <ConvergeLines count={sources.length} animate={run} className={linesClassName} />
165
+ </div>
166
+
167
+ <motion.div
168
+ className="flex items-center"
169
+ variants={run ? targetItem : undefined}
170
+ initial={run ? "hidden" : false}
171
+ animate={run ? "show" : undefined}
172
+ >
173
+ {target}
174
+ </motion.div>
175
+ </motion.div>
176
+ );
177
+ }
@@ -74,7 +74,7 @@ export type EventCompactRowProps = {
74
74
  * repeating it on each one is noise — the caller has something more
75
75
  * differentiating to say instead (the event's own description). The
76
76
  * timeline keeps the default: there the venue IS what tells rows apart
77
- * across the night. Mirrors `menu-item-row.tsx`'s prop of the same name and
77
+ * across the night. Mirrors `menu-item.tsx`'s prop of the same name and
78
78
  * intent.
79
79
  */
80
80
  subtitle?: string;
@@ -1,81 +1,127 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
- import { ExternalLink, Navigation, Phone, Share2, X } from "lucide-react";
3
2
  import {
3
+ ExternalLink,
4
+ Navigation,
5
+ Phone,
6
+ Share2,
7
+ X,
8
+ CalendarClock,
9
+ } from "lucide-react";
10
+ import { Button } from "./button";
11
+ import {
12
+ EventDetailFooter,
13
+ EventDetailGhostAction,
4
14
  EventDetailPanel,
15
+ EventDetailPanelError,
5
16
  EventDetailPanelSkeleton,
6
17
  type EventDetailPanelLabels,
7
18
  } from "./event-detail-panel";
8
19
  import type { EventFormatters, EventPreview } from "./event-types";
9
20
 
10
21
  const labels: EventDetailPanelLabels = {
11
- free: "Free",
12
- liveNow: "Live now",
22
+ free: "Free entry",
23
+ liveNow: "Now",
24
+ ended: "Ended",
13
25
  venueHeading: "Event venue",
26
+ venueNoPage: "name only — no venue page yet",
27
+ lineupHeading: "Line-up",
14
28
  aboutHeading: "About this event",
29
+ noDescription: "Event details coming soon.",
30
+ readMore: "Read more",
31
+ readLess: "Read less",
15
32
  };
16
33
 
17
34
  const formatters: EventFormatters = {
18
- time: () => "20:00",
19
- day: () => "Sat, Sep 5",
35
+ time: () => "17:30",
36
+ day: () => "Tue, Sep 8",
20
37
  price: (amount) => `${amount} RON`,
21
38
  };
22
39
 
40
+ const description =
41
+ "An event for future brides featuring the new Rosa Clara 2027 collection and wedding planning advice from industry specialists. Doors at 17:00. The first look runs about an hour, then the atelier stays open for fittings until half seven. Bubbles throughout, and the kitchen puts out canapés at six.";
42
+
23
43
  const event: EventPreview = {
24
44
  id: "e1",
25
- title: "Psihotrop Live",
26
- description:
27
- "A full live set spanning the band's whole catalogue, with support from Mara Ionescu. Doors at 19:30.",
28
- location: "Atelier Café",
29
- place_name: "Atelier Café",
30
- starts_at: "2026-09-05T20:00:00",
45
+ title: "Brides & Bubbles at Sophie Wedding House",
46
+ description,
47
+ location: "Sophie Wedding House",
48
+ place_name: "Sophie Wedding House",
49
+ place_slug: "sophie-wedding-house",
50
+ starts_at: "2026-09-08T17:30:00",
31
51
  entry_fee: 50,
52
+ hype_score: 72,
32
53
  };
33
54
 
55
+ const performers = [
56
+ "Rosa Clara",
57
+ "Sophie Atelier",
58
+ "Mara Ionescu",
59
+ "DJ Vlad",
60
+ "Trio Boema",
61
+ "Ana Pop",
62
+ "Mihai Ionescu",
63
+ "Quartetto",
64
+ ];
65
+
34
66
  const venue = {
35
- name: "Atelier Café",
36
- meta: "Str. Iuliu Maniu 5 · open until 01:00",
67
+ name: "Sophie Wedding House",
68
+ address: "Str. Iuliu Maniu 5",
37
69
  ratingLabel: "4.7",
38
- href: "/places/atelier-cafe",
70
+ isOpenNow: true,
71
+ href: "/places/sophie-wedding-house",
39
72
  };
40
73
 
41
- const roundBtn =
42
- "flex h-9 w-9 items-center justify-center rounded-full bg-white/90 text-foreground";
74
+ const overlayButton =
75
+ "flex h-9 w-9 items-center justify-center rounded-full bg-black/40 text-white backdrop-blur-sm";
43
76
 
44
- const headerActions = (
77
+ const sheetActions = (
45
78
  <>
46
- <button type="button" aria-label="Share" className={`absolute left-3.5 top-3.5 ${roundBtn}`}>
79
+ <button type="button" aria-label="Share" className={overlayButton}>
47
80
  <Share2 className="h-4 w-4" aria-hidden />
48
81
  </button>
49
- <button type="button" aria-label="Close" className={`absolute right-3.5 top-3.5 ${roundBtn}`}>
50
- <X className="h-[17px] w-[17px]" aria-hidden />
51
- </button>
52
- </>
53
- );
54
-
55
- const footer = (
56
- <>
57
- <button
58
- type="button"
59
- className="flex h-[46px] flex-1 items-center justify-center gap-2 rounded-xl bg-primary text-sm font-bold text-primary-foreground"
60
- >
61
- <Phone className="h-4 w-4" aria-hidden />
62
- Reserve a spot
63
- </button>
64
82
  <button
65
83
  type="button"
66
- className="flex h-[46px] items-center gap-2 rounded-xl border border-border px-4 text-sm font-bold"
84
+ aria-label="Close"
85
+ className={`ml-auto ${overlayButton}`}
67
86
  >
68
- <Navigation className="h-4 w-4" aria-hidden />
69
- Directions
87
+ <X className="h-[17px] w-[17px]" aria-hidden />
70
88
  </button>
71
89
  </>
72
90
  );
73
91
 
74
- const externalLink = (
75
- <a href="#" className="flex items-center gap-1.5 text-[13.5px] font-bold text-primary">
76
- <ExternalLink className="h-3.5 w-3.5" aria-hidden />
77
- View on Instagram
78
- </a>
92
+ const hypePill = (
93
+ <span className="inline-flex h-6 items-center gap-1 rounded-full bg-[image:var(--gradient-primary)] px-2.5 text-[11.5px] font-bold text-primary-foreground">
94
+ Popular
95
+ </span>
96
+ );
97
+
98
+ const footer = (
99
+ <EventDetailFooter
100
+ ghosts={[
101
+ <EventDetailGhostAction
102
+ key="dir"
103
+ icon={<Navigation className="h-3.5 w-3.5" aria-hidden />}
104
+ href="https://maps.google.com"
105
+ target="_blank"
106
+ rel="noopener noreferrer"
107
+ >
108
+ Directions
109
+ </EventDetailGhostAction>,
110
+ <EventDetailGhostAction
111
+ key="page"
112
+ icon={<ExternalLink className="h-3.5 w-3.5" aria-hidden />}
113
+ href="/events/e/brides-and-bubbles"
114
+ >
115
+ Event page
116
+ </EventDetailGhostAction>,
117
+ ]}
118
+ primary={
119
+ <Button fullWidth>
120
+ <Phone aria-hidden />
121
+ Get tickets · 50 RON
122
+ </Button>
123
+ }
124
+ />
79
125
  );
80
126
 
81
127
  const meta: Meta<typeof EventDetailPanel> = {
@@ -86,18 +132,20 @@ const meta: Meta<typeof EventDetailPanel> = {
86
132
  event,
87
133
  labels,
88
134
  formatters,
89
- dateLine: "Saturday, September 5 · Starts at 20:00",
135
+ dateLine: "Tuesday, September 8 · Starts at 17:30",
90
136
  venue,
91
- tags: ["Live music"],
92
- headerActions,
137
+ tags: ["Workshop", "Tasting"],
138
+ performers,
139
+ showAllPerformersLabel: "Show all 8 performers",
140
+ hype: hypePill,
141
+ headerActions: sheetActions,
93
142
  footer,
94
- externalLink,
95
143
  },
96
144
  parameters: {
97
145
  docs: {
98
146
  description: {
99
147
  component:
100
- "The BODY of the detail sheet, not a sheet itself the DS already ships `Sheet` and `Drawer`, and the app mounts this inside the side sheet on desktop and the bottom sheet on mobile. This is the terminal surface for acting on an event, which is why the cards that open it carry no actions of their own.",
148
+ "The body of the event detail sheet and the base the event page extends. Not a sheet: mount it in `Sheet` (desktop) or `Drawer` (mobile). The hero is capped per host so the venue card clears the fold; status, fee, hype and tags share one chip row; the footer is one primary over a ghost row.",
101
149
  },
102
150
  },
103
151
  },
@@ -106,86 +154,146 @@ const meta: Meta<typeof EventDetailPanel> = {
106
154
  export default meta;
107
155
  type Story = StoryObj<typeof EventDetailPanel>;
108
156
 
109
- /** Desktop side sheet 460px wide, 4:5 poster. */
157
+ /** The desktop side sheet: 480px wide, hero capped at 300px. */
110
158
  export const SideSheet: Story = {
111
159
  decorators: [
112
160
  (Story) => (
113
- <div className="h-[820px] w-[460px] overflow-hidden rounded-[18px] shadow-2xl">
161
+ <div className="h-[820px] w-[480px] overflow-hidden rounded-2xl shadow-[var(--shadow-overlay)]">
114
162
  <Story />
115
163
  </div>
116
164
  ),
117
165
  ],
118
166
  };
119
167
 
120
- /** Mobile bottom sheet compact size, same 4:5 poster, tighter type. The hero scrolls away with the body; only the footer stays put. */
121
- export const BottomSheet: Story = {
122
- args: { size: "compact" },
168
+ /** The phone drawer: `compact` caps the hero at 318px. The host draws the handle and a Back pill. */
169
+ export const PhoneDrawer: Story = {
170
+ args: {
171
+ size: "compact",
172
+ headerActions: (
173
+ <>
174
+ <span className="inline-flex h-9 items-center gap-1.5 rounded-full bg-black/40 pl-2.5 pr-3 text-[13px] font-semibold text-white backdrop-blur-sm">
175
+ Back
176
+ </span>
177
+ <button
178
+ type="button"
179
+ aria-label="Share"
180
+ className={`ml-auto ${overlayButton}`}
181
+ >
182
+ <Share2 className="h-4 w-4" aria-hidden />
183
+ </button>
184
+ </>
185
+ ),
186
+ },
123
187
  decorators: [
124
188
  (Story) => (
125
- <div className="h-[720px] w-full max-w-[390px] overflow-hidden rounded-t-[20px] shadow-2xl">
126
- <div className="mx-auto mt-2.5 mb-1 h-1 w-[38px] rounded-full bg-border" />
189
+ <div className="h-[720px] w-[390px] overflow-hidden rounded-t-[20px] shadow-[var(--shadow-overlay)]">
127
190
  <Story />
128
191
  </div>
129
192
  ),
130
193
  ],
131
194
  };
132
195
 
133
- /** Live events get the on-air chip beside the category and price. */
134
- export const LiveNow: Story = {
135
- args: { isLive: true },
136
- decorators: [
137
- (Story) => (
138
- <div className="h-[820px] w-[460px] overflow-hidden rounded-[18px] shadow-2xl">
139
- <Story />
140
- </div>
141
- ),
142
- ],
196
+ /** Live, free, no poster: the gradient fallback, the Now chip and the free chip. */
197
+ export const LiveFreeNoPoster: Story = {
198
+ args: {
199
+ event: { ...event, poster_url: undefined, entry_fee: 0 },
200
+ status: "live",
201
+ },
202
+ decorators: SideSheet.decorators,
143
203
  };
144
204
 
145
- /** A free event: `entry_fee: 0` renders the free chip instead of a price. */
146
- export const FreeEntry: Story = {
147
- args: { event: { ...event, entry_fee: 0 } },
148
- decorators: [
149
- (Story) => (
150
- <div className="h-[820px] w-[460px] overflow-hidden rounded-[18px] shadow-2xl">
151
- <Story />
152
- </div>
153
- ),
154
- ],
205
+ /** Starting soon: the accent countdown chip. */
206
+ export const Soon: Story = {
207
+ args: { status: "soon", soonLabel: "Starts in 2h 10m" },
208
+ decorators: SideSheet.decorators,
155
209
  };
156
210
 
157
- /** No description and no venue — the sections drop out rather than render empty headings. */
211
+ /**
212
+ * Unknown hour, no booking, a venue with no page, no line-up, no description.
213
+ * Every optional region drops out; the venue card says why it is not a link.
214
+ */
158
215
  export const Sparse: Story = {
159
216
  args: {
160
- event: { ...event, description: undefined, entry_fee: undefined },
161
- venue: undefined,
162
- tags: undefined,
163
- externalLink: undefined,
217
+ event: { ...event, description: "", hype_score: 0 },
218
+ dateLine: "Tuesday, September 8 · time to be announced",
219
+ venue: { name: "Sophie Wedding House" },
220
+ performers: undefined,
221
+ hype: undefined,
222
+ footer: (
223
+ <EventDetailFooter
224
+ ghosts={[
225
+ <EventDetailGhostAction
226
+ key="page"
227
+ icon={<ExternalLink className="h-3.5 w-3.5" aria-hidden />}
228
+ href="/events/e/x"
229
+ >
230
+ See the event page
231
+ </EventDetailGhostAction>,
232
+ ]}
233
+ primary={
234
+ <Button fullWidth>
235
+ <Navigation aria-hidden />
236
+ Directions
237
+ </Button>
238
+ }
239
+ />
240
+ ),
164
241
  },
165
- decorators: [
166
- (Story) => (
167
- <div className="h-[560px] w-[460px] overflow-hidden rounded-[18px] shadow-2xl">
168
- <Story />
169
- </div>
242
+ decorators: SideSheet.decorators,
243
+ };
244
+
245
+ /** Ended: poster desaturated, title muted, explicit chip, and the primary replaced rather than disabled. */
246
+ export const Ended: Story = {
247
+ args: {
248
+ status: "ended",
249
+ footer: (
250
+ <EventDetailFooter
251
+ ghosts={[
252
+ <EventDetailGhostAction
253
+ key="dir"
254
+ icon={<Navigation className="h-3.5 w-3.5" aria-hidden />}
255
+ href="https://maps.google.com"
256
+ target="_blank"
257
+ rel="noopener noreferrer"
258
+ >
259
+ Directions
260
+ </EventDetailGhostAction>,
261
+ <EventDetailGhostAction
262
+ key="page"
263
+ icon={<ExternalLink className="h-3.5 w-3.5" aria-hidden />}
264
+ href="/events/e/x"
265
+ >
266
+ Event page
267
+ </EventDetailGhostAction>,
268
+ ]}
269
+ primary={
270
+ <Button fullWidth>
271
+ <CalendarClock aria-hidden />
272
+ See what's on tonight
273
+ </Button>
274
+ }
275
+ />
170
276
  ),
171
- ],
277
+ },
278
+ decorators: SideSheet.decorators,
172
279
  };
173
280
 
174
- /** Desktop side sheet loading state. */
175
- export const Skeleton: Story = {
176
- render: () => (
177
- <div className="h-[820px] w-[460px] overflow-hidden rounded-[18px] shadow-2xl">
178
- <EventDetailPanelSkeleton />
179
- </div>
180
- ),
281
+ export const Loading: Story = {
282
+ render: () => <EventDetailPanelSkeleton headerActions={sheetActions} />,
283
+ decorators: SideSheet.decorators,
181
284
  };
182
285
 
183
- /** Mobile bottom sheet loading state — compact size, same 4:5 poster. */
184
- export const SkeletonCompact: Story = {
286
+ export const Error: Story = {
185
287
  render: () => (
186
- <div className="h-[720px] w-full max-w-[390px] overflow-hidden rounded-t-[20px] shadow-2xl">
187
- <div className="mx-auto mt-2.5 mb-1 h-1 w-[38px] rounded-full bg-border" />
188
- <EventDetailPanelSkeleton size="compact" />
189
- </div>
288
+ <EventDetailPanelError
289
+ title="We could not load this event"
290
+ description="It may have been taken down, or the connection dropped on the way. The rest of tonight is still there."
291
+ retryLabel="Try again"
292
+ onRetry={() => {}}
293
+ secondaryLabel="See what's on tonight"
294
+ secondaryHref="/events/schedule"
295
+ headerActions={sheetActions}
296
+ />
190
297
  ),
298
+ decorators: SideSheet.decorators,
191
299
  };