@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.
- package/dist/chunk-24RFI4RQ.js +167 -0
- package/dist/chunk-2XCZJWTV.js +121 -0
- package/dist/chunk-5RWTOT5Y.js +72 -0
- package/dist/chunk-62ES6N32.js +27 -0
- package/dist/chunk-AKQ57NXY.js +58 -0
- package/dist/chunk-CLLY6XB3.js +41 -0
- package/dist/chunk-DTXNDLSE.js +125 -0
- package/dist/chunk-F6FMZJDG.js +113 -0
- package/dist/{chunk-2WDJR6K7.js → chunk-G7IFL4Z4.js} +4 -1
- package/dist/chunk-MZL7CGCL.js +109 -0
- package/dist/chunk-Q6QH52LJ.js +38 -0
- package/dist/chunk-SY6RV476.js +48 -0
- package/dist/chunk-TH6W2PWG.js +213 -0
- package/dist/chunk-UDN7UHJW.js +60 -0
- package/dist/{chunk-TT3WFS6S.js → chunk-YOKHBAIH.js} +11 -11
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/concept-compare-rail.d.ts +50 -0
- package/dist/components/concept-compare-rail.js +5 -0
- package/dist/components/concept-compare.d.ts +110 -0
- package/dist/components/concept-compare.js +238 -0
- package/dist/components/concept-convergence.d.ts +41 -0
- package/dist/components/concept-convergence.js +53 -0
- package/dist/components/convergence.d.ts +25 -0
- package/dist/components/convergence.js +4 -0
- package/dist/components/date-input.d.ts +1 -1
- package/dist/components/event-card.js +1 -1
- package/dist/components/event-compact-row.d.ts +1 -1
- package/dist/components/event-compact-row.js +1 -1
- package/dist/components/event-detail-panel.d.ts +171 -25
- package/dist/components/event-detail-panel.js +472 -175
- package/dist/components/event-index-row.js +1 -1
- package/dist/components/event-list-row-wide.js +2 -2
- package/dist/components/event-list-row.js +2 -2
- package/dist/components/explainer-steps.d.ts +34 -0
- package/dist/components/explainer-steps.js +36 -0
- package/dist/components/glass-ground.d.ts +66 -0
- package/dist/components/glass-ground.js +3 -0
- package/dist/components/hub-surface.d.ts +27 -0
- package/dist/components/hub-surface.js +30 -0
- package/dist/components/hue-chip.d.ts +70 -0
- package/dist/components/hue-chip.js +3 -0
- package/dist/components/kitchen-dossier.d.ts +88 -0
- package/dist/components/kitchen-dossier.js +187 -0
- package/dist/components/menu-dead-end.d.ts +56 -0
- package/dist/components/menu-dead-end.js +49 -0
- package/dist/components/menu-family-block.d.ts +99 -0
- package/dist/components/menu-family-block.js +155 -0
- package/dist/components/menu-family-card.d.ts +65 -0
- package/dist/components/menu-family-card.js +79 -0
- package/dist/components/menu-field.d.ts +57 -0
- package/dist/components/menu-field.js +93 -0
- package/dist/components/menu-filter-chips.d.ts +86 -0
- package/dist/components/menu-filter-chips.js +122 -0
- package/dist/components/menu-group-rows.d.ts +53 -0
- package/dist/components/menu-group-rows.js +67 -0
- package/dist/components/menu-hub-hero.d.ts +65 -0
- package/dist/components/menu-hub-hero.js +103 -0
- package/dist/components/menu-index-header.d.ts +37 -0
- package/dist/components/menu-index-header.js +68 -0
- package/dist/components/menu-index-list.d.ts +48 -0
- package/dist/components/menu-index-list.js +96 -0
- package/dist/components/menu-intent-hero.d.ts +69 -0
- package/dist/components/menu-intent-hero.js +87 -0
- package/dist/components/menu-intent-tiles.d.ts +31 -0
- package/dist/components/menu-intent-tiles.js +50 -0
- package/dist/components/menu-item.d.ts +127 -0
- package/dist/components/menu-item.js +5 -0
- package/dist/components/menu-kitchen-list.d.ts +82 -0
- package/dist/components/menu-kitchen-list.js +5 -0
- package/dist/components/menu-mocks.d.ts +90 -0
- package/dist/components/menu-mocks.js +504 -0
- package/dist/components/menu-types.d.ts +421 -0
- package/dist/components/menu-types.js +1 -0
- package/dist/components/motion-icon-controls.js +2 -2
- package/dist/components/motion-icon-set.js +21 -9
- package/dist/components/motion-icon.d.ts +22 -1
- package/dist/components/motion-icon.js +1 -1
- package/dist/components/open-now-list.d.ts +40 -0
- package/dist/components/open-now-list.js +69 -0
- package/dist/components/photo-ground.d.ts +64 -0
- package/dist/components/photo-ground.js +3 -0
- package/dist/components/place-about-tab.js +2 -2
- package/dist/components/place-details-mobile.js +2 -2
- package/dist/components/place-types.d.ts +112 -1
- package/dist/components/qa-block.d.ts +36 -0
- package/dist/components/qa-block.js +24 -0
- package/dist/components/scroll-fade.js +3 -121
- package/dist/components/signature-chip.d.ts +37 -0
- package/dist/components/signature-chip.js +4 -0
- package/dist/components/snap-rail.d.ts +48 -5
- package/dist/components/snap-rail.js +3 -51
- package/dist/components/stat-banner.d.ts +47 -0
- package/dist/components/stat-banner.js +61 -0
- package/dist/components/story.d.ts +34 -0
- package/dist/components/story.js +3 -0
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/text.d.ts +1 -1
- package/dist/components/top-comparison.d.ts +28 -0
- package/dist/components/top-comparison.js +76 -0
- package/dist/components/top-cross-link.d.ts +34 -0
- package/dist/components/top-cross-link.js +101 -0
- package/dist/components/top-ranking-entry.d.ts +53 -0
- package/dist/components/top-ranking-entry.js +217 -0
- package/dist/components/top-ranking-hero.d.ts +61 -0
- package/dist/components/top-ranking-hero.js +99 -0
- package/dist/components/top-ranking-legend.d.ts +69 -0
- package/dist/components/top-ranking-legend.js +124 -0
- package/dist/components/top-types.d.ts +109 -0
- package/dist/components/top-types.js +1 -0
- package/dist/components/venue-grade-dimensions.d.ts +38 -0
- package/dist/components/venue-grade-dimensions.js +3 -0
- package/dist/components/venue-quotes.d.ts +32 -0
- package/dist/components/venue-quotes.js +3 -0
- package/dist/components/venue-wall.d.ts +41 -0
- package/dist/components/venue-wall.js +118 -0
- package/dist/components/weekday-theme-banner.d.ts +70 -0
- package/dist/components/weekday-theme-banner.js +127 -0
- package/dist/components/weekend-panel.js +2 -2
- package/package.json +1 -1
- package/src/components/colour-dictionary.stories.tsx +227 -0
- package/src/components/concept-compare-rail.tsx +131 -0
- package/src/components/concept-compare.stories.tsx +172 -0
- package/src/components/concept-compare.tsx +443 -0
- package/src/components/concept-convergence.stories.tsx +53 -0
- package/src/components/concept-convergence.tsx +90 -0
- package/src/components/convergence.stories.tsx +54 -0
- package/src/components/convergence.tsx +177 -0
- package/src/components/event-compact-row.tsx +1 -1
- package/src/components/event-detail-panel.stories.tsx +205 -97
- package/src/components/event-detail-panel.test.tsx +181 -0
- package/src/components/event-detail-panel.tsx +703 -185
- package/src/components/explainer-steps.stories.tsx +46 -0
- package/src/components/explainer-steps.tsx +74 -0
- package/src/components/glass-ground.tsx +129 -0
- package/src/components/hub-surface.stories.tsx +68 -0
- package/src/components/hub-surface.tsx +50 -0
- package/src/components/hue-chip.stories.tsx +76 -0
- package/src/components/hue-chip.test.tsx +62 -0
- package/src/components/hue-chip.tsx +99 -0
- package/src/components/kitchen-dossier.stories.tsx +108 -0
- package/src/components/kitchen-dossier.tsx +322 -0
- package/src/components/menu-dead-end.stories.tsx +67 -0
- package/src/components/menu-dead-end.tsx +127 -0
- package/src/components/menu-family-block.stories.tsx +91 -0
- package/src/components/menu-family-block.tsx +314 -0
- package/src/components/menu-family-card.stories.tsx +120 -0
- package/src/components/menu-family-card.tsx +191 -0
- package/src/components/menu-field.stories.tsx +74 -0
- package/src/components/menu-field.tsx +199 -0
- package/src/components/menu-filter-chips.stories.tsx +107 -0
- package/src/components/menu-filter-chips.tsx +238 -0
- package/src/components/menu-group-rows.stories.tsx +54 -0
- package/src/components/menu-group-rows.tsx +133 -0
- package/src/components/menu-hub-hero.stories.tsx +92 -0
- package/src/components/menu-hub-hero.tsx +187 -0
- package/src/components/menu-index-header.stories.tsx +72 -0
- package/src/components/menu-index-header.tsx +125 -0
- package/src/components/menu-index-list.stories.tsx +50 -0
- package/src/components/menu-index-list.tsx +149 -0
- package/src/components/menu-intent-hero.stories.tsx +67 -0
- package/src/components/menu-intent-hero.tsx +189 -0
- package/src/components/menu-intent-tiles.stories.tsx +54 -0
- package/src/components/menu-intent-tiles.tsx +76 -0
- package/src/components/menu-item.tsx +416 -0
- package/src/components/menu-kitchen-list.stories.tsx +67 -0
- package/src/components/menu-kitchen-list.test.tsx +78 -0
- package/src/components/menu-kitchen-list.tsx +284 -0
- package/src/components/menu-mocks.ts +592 -0
- package/src/components/menu-types.ts +442 -0
- package/src/components/motion-icon-controls.tsx +11 -11
- package/src/components/motion-icon-set.tsx +36 -8
- package/src/components/motion-icon.test.tsx +23 -1
- package/src/components/motion-icon.tsx +20 -0
- package/src/components/open-now-list.stories.tsx +76 -0
- package/src/components/open-now-list.tsx +127 -0
- package/src/components/photo-ground.tsx +147 -0
- package/src/components/place-types.ts +112 -0
- package/src/components/qa-block.stories.tsx +43 -0
- package/src/components/qa-block.tsx +60 -0
- package/src/components/signature-chip.tsx +77 -0
- package/src/components/snap-rail.tsx +133 -9
- package/src/components/stat-banner.stories.tsx +100 -0
- package/src/components/stat-banner.tsx +117 -0
- package/src/components/story.tsx +225 -0
- package/src/components/top-comparison.stories.tsx +78 -0
- package/src/components/top-comparison.tsx +151 -0
- package/src/components/top-cross-link.stories.tsx +84 -0
- package/src/components/top-cross-link.tsx +171 -0
- package/src/components/top-ranking-entry.stories.tsx +171 -0
- package/src/components/top-ranking-entry.test.tsx +105 -0
- package/src/components/top-ranking-entry.tsx +425 -0
- package/src/components/top-ranking-hero.stories.tsx +76 -0
- package/src/components/top-ranking-hero.tsx +218 -0
- package/src/components/top-ranking-legend.stories.tsx +78 -0
- package/src/components/top-ranking-legend.tsx +239 -0
- package/src/components/top-types.ts +110 -0
- package/src/components/venue-grade-dimensions.stories.tsx +83 -0
- package/src/components/venue-grade-dimensions.tsx +91 -0
- package/src/components/venue-quotes.stories.tsx +59 -0
- package/src/components/venue-quotes.tsx +64 -0
- package/src/components/venue-wall.stories.tsx +78 -0
- package/src/components/venue-wall.tsx +185 -0
- package/src/components/weekday-theme-banner.stories.tsx +113 -0
- package/src/components/weekday-theme-banner.tsx +231 -0
- 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
|
|
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: "
|
|
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: () => "
|
|
19
|
-
day: () => "
|
|
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: "
|
|
26
|
-
description
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
starts_at: "2026-09-
|
|
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: "
|
|
36
|
-
|
|
67
|
+
name: "Sophie Wedding House",
|
|
68
|
+
address: "Str. Iuliu Maniu 5",
|
|
37
69
|
ratingLabel: "4.7",
|
|
38
|
-
|
|
70
|
+
isOpenNow: true,
|
|
71
|
+
href: "/places/sophie-wedding-house",
|
|
39
72
|
};
|
|
40
73
|
|
|
41
|
-
const
|
|
42
|
-
"flex h-9 w-9 items-center justify-center rounded-full bg-
|
|
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
|
|
77
|
+
const sheetActions = (
|
|
45
78
|
<>
|
|
46
|
-
<button type="button" aria-label="Share" className={
|
|
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
|
-
|
|
84
|
+
aria-label="Close"
|
|
85
|
+
className={`ml-auto ${overlayButton}`}
|
|
67
86
|
>
|
|
68
|
-
<
|
|
69
|
-
Directions
|
|
87
|
+
<X className="h-[17px] w-[17px]" aria-hidden />
|
|
70
88
|
</button>
|
|
71
89
|
</>
|
|
72
90
|
);
|
|
73
91
|
|
|
74
|
-
const
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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: "
|
|
135
|
+
dateLine: "Tuesday, September 8 · Starts at 17:30",
|
|
90
136
|
venue,
|
|
91
|
-
tags: ["
|
|
92
|
-
|
|
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
|
|
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
|
-
/**
|
|
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-[
|
|
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
|
-
/**
|
|
121
|
-
export const
|
|
122
|
-
args: {
|
|
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-
|
|
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
|
|
134
|
-
export const
|
|
135
|
-
args: {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
/**
|
|
146
|
-
export const
|
|
147
|
-
args: {
|
|
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
|
-
/**
|
|
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:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
184
|
-
export const SkeletonCompact: Story = {
|
|
286
|
+
export const Error: Story = {
|
|
185
287
|
render: () => (
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
};
|