@bearmenu/ui 0.8.4 → 0.8.7
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-637L7XQ3.js +85 -0
- package/dist/chunk-6XMYEZVR.js +26 -0
- package/dist/chunk-DNSXMHYX.js +8 -0
- package/dist/chunk-FDITZ2VM.js +16 -0
- package/dist/chunk-GPH6L4DX.js +55 -0
- package/dist/{chunk-N2KNBPAQ.js → chunk-QXHMPJ35.js} +1 -0
- package/dist/chunk-TT3WFS6S.js +379 -0
- package/dist/chunk-UXWAOISG.js +170 -0
- package/dist/{chunk-UZ3PCIE5.js → chunk-XWMRABZW.js} +2 -2
- package/dist/{chunk-VBA45GLP.js → chunk-ZDAHKI3A.js} +12 -2
- package/dist/components/card.d.ts +2 -2
- package/dist/components/card.js +4 -127
- package/dist/components/category-deck.d.ts +50 -0
- package/dist/components/category-deck.js +128 -0
- package/dist/components/concept-card.d.ts +43 -0
- package/dist/components/concept-card.js +81 -0
- package/dist/components/coverflow.d.ts +123 -0
- package/dist/components/coverflow.js +300 -0
- package/dist/components/day-rail-chip.d.ts +82 -0
- package/dist/components/day-rail-chip.js +105 -0
- package/dist/components/drift-rail.d.ts +28 -0
- package/dist/components/drift-rail.js +3 -0
- package/dist/components/event-card.d.ts +97 -0
- package/dist/components/event-card.js +164 -0
- package/dist/components/event-compact-row.d.ts +73 -0
- package/dist/components/event-compact-row.js +134 -0
- package/dist/components/event-day-header.d.ts +65 -0
- package/dist/components/event-day-header.js +95 -0
- package/dist/components/event-day-separator.d.ts +46 -0
- package/dist/components/event-day-separator.js +50 -0
- package/dist/components/event-detail-panel.d.ts +80 -0
- package/dist/components/event-detail-panel.js +251 -0
- package/dist/components/event-hub-hero.d.ts +34 -0
- package/dist/components/event-hub-hero.js +57 -0
- package/dist/components/event-index-row.d.ts +68 -0
- package/dist/components/event-index-row.js +84 -0
- package/dist/components/event-list-row-wide.d.ts +101 -0
- package/dist/components/event-list-row-wide.js +218 -0
- package/dist/components/event-list-row.d.ts +82 -0
- package/dist/components/event-list-row.js +173 -0
- package/dist/components/event-poster-fallback.d.ts +20 -0
- package/dist/components/event-poster-fallback.js +3 -0
- package/dist/components/event-types.d.ts +122 -0
- package/dist/components/event-types.js +2 -0
- package/dist/components/events-empty-state.d.ts +44 -0
- package/dist/components/events-empty-state.js +73 -0
- package/dist/components/facet-group.d.ts +93 -0
- package/dist/components/facet-group.js +222 -0
- package/dist/components/featured-rail-item.d.ts +42 -0
- package/dist/components/featured-rail-item.js +88 -0
- package/dist/components/hour-band.d.ts +68 -0
- package/dist/components/hour-band.js +53 -0
- package/dist/components/hour-group.d.ts +67 -0
- package/dist/components/hour-group.js +59 -0
- package/dist/components/motion-icon-controls.js +3 -379
- package/dist/components/multi-select-combobox.js +1 -1
- package/dist/components/place-about-tab.js +3 -3
- package/dist/components/place-details-mobile.js +3 -3
- package/dist/components/poster-stack.d.ts +30 -0
- package/dist/components/poster-stack.js +104 -0
- package/dist/components/poster-tape.d.ts +36 -0
- package/dist/components/poster-tape.js +64 -0
- package/dist/components/poster-wall.d.ts +25 -0
- package/dist/components/poster-wall.js +183 -0
- package/dist/components/quiet-stretch.d.ts +43 -0
- package/dist/components/quiet-stretch.js +54 -0
- package/dist/components/section-header.d.ts +53 -0
- package/dist/components/section-header.js +63 -0
- package/dist/components/segmented-nav.d.ts +62 -0
- package/dist/components/segmented-nav.js +54 -0
- package/dist/components/skeleton-card.js +1 -1
- package/dist/components/skeleton.d.ts +17 -0
- package/dist/components/skeleton.js +1 -1
- package/dist/components/tag-group.js +3 -26
- package/dist/components/tag.d.ts +2 -2
- package/dist/components/tag.js +1 -1
- package/dist/components/timeline-band.d.ts +46 -0
- package/dist/components/timeline-band.js +5 -0
- package/dist/components/venue-events-block.d.ts +143 -0
- package/dist/components/venue-events-block.js +222 -0
- package/dist/components/weekend-panel.d.ts +44 -0
- package/dist/components/weekend-panel.js +231 -0
- package/package.json +1 -1
- package/src/components/card.tsx +44 -1
- package/src/components/category-deck.stories.tsx +96 -0
- package/src/components/category-deck.tsx +227 -0
- package/src/components/concept-card.stories.tsx +80 -0
- package/src/components/concept-card.tsx +143 -0
- package/src/components/coverflow.stories.tsx +240 -0
- package/src/components/coverflow.test.tsx +193 -0
- package/src/components/coverflow.tsx +550 -0
- package/src/components/day-rail-chip.stories.tsx +145 -0
- package/src/components/day-rail-chip.tsx +201 -0
- package/src/components/drift-rail.stories.tsx +92 -0
- package/src/components/drift-rail.tsx +86 -0
- package/src/components/event-card.stories.tsx +248 -0
- package/src/components/event-card.test.tsx +188 -0
- package/src/components/event-card.tsx +329 -0
- package/src/components/event-compact-row.stories.tsx +221 -0
- package/src/components/event-compact-row.test.tsx +51 -0
- package/src/components/event-compact-row.tsx +270 -0
- package/src/components/event-day-header.stories.tsx +124 -0
- package/src/components/event-day-header.tsx +180 -0
- package/src/components/event-day-separator.stories.tsx +70 -0
- package/src/components/event-day-separator.tsx +95 -0
- package/src/components/event-detail-panel.stories.tsx +191 -0
- package/src/components/event-detail-panel.tsx +360 -0
- package/src/components/event-hub-hero.stories.tsx +66 -0
- package/src/components/event-hub-hero.tsx +110 -0
- package/src/components/event-index-row.stories.tsx +116 -0
- package/src/components/event-index-row.tsx +181 -0
- package/src/components/event-list-row-wide.stories.tsx +204 -0
- package/src/components/event-list-row-wide.tsx +386 -0
- package/src/components/event-list-row.stories.tsx +159 -0
- package/src/components/event-list-row.tsx +335 -0
- package/src/components/event-poster-fallback.stories.tsx +52 -0
- package/src/components/event-poster-fallback.tsx +31 -0
- package/src/components/event-types.ts +137 -0
- package/src/components/events-empty-state.stories.tsx +59 -0
- package/src/components/events-empty-state.tsx +131 -0
- package/src/components/facet-group.stories.tsx +247 -0
- package/src/components/facet-group.test.tsx +225 -0
- package/src/components/facet-group.tsx +352 -0
- package/src/components/featured-rail-item.stories.tsx +86 -0
- package/src/components/featured-rail-item.tsx +153 -0
- package/src/components/hour-band.stories.tsx +159 -0
- package/src/components/hour-band.tsx +126 -0
- package/src/components/hour-group.stories.tsx +100 -0
- package/src/components/hour-group.tsx +124 -0
- package/src/components/poster-stack.stories.tsx +68 -0
- package/src/components/poster-stack.tsx +155 -0
- package/src/components/poster-tape.stories.tsx +70 -0
- package/src/components/poster-tape.tsx +122 -0
- package/src/components/poster-wall.stories.tsx +95 -0
- package/src/components/poster-wall.tsx +220 -0
- package/src/components/quiet-stretch.stories.tsx +62 -0
- package/src/components/quiet-stretch.tsx +115 -0
- package/src/components/section-header.stories.tsx +70 -0
- package/src/components/section-header.tsx +134 -0
- package/src/components/segmented-nav.stories.tsx +118 -0
- package/src/components/segmented-nav.tsx +113 -0
- package/src/components/skeleton.tsx +18 -0
- package/src/components/tag.tsx +11 -1
- package/src/components/timeline-band.stories.tsx +206 -0
- package/src/components/timeline-band.tsx +207 -0
- package/src/components/venue-events-block.stories.tsx +216 -0
- package/src/components/venue-events-block.tsx +408 -0
- package/src/components/weekend-panel.stories.tsx +92 -0
- package/src/components/weekend-panel.tsx +322 -0
- package/src/globals.css +19 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Calendar, ChevronRight, Radio, Star } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
import { EventPosterFallback } from "./event-poster-fallback";
|
|
7
|
+
import { Separator } from "./separator";
|
|
8
|
+
import { Skeleton } from "./skeleton";
|
|
9
|
+
import {
|
|
10
|
+
resolveEventEntryFee,
|
|
11
|
+
type EventFormatters,
|
|
12
|
+
type EventPreview,
|
|
13
|
+
} from "./event-types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* EventDetailPanel — the BODY of the event detail sheet.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately not a sheet. The design opens this as a side sheet over a dimmed
|
|
19
|
+
* page on desktop and a bottom sheet on mobile, and the DS already ships `Sheet`
|
|
20
|
+
* and `Drawer`; owning sheet mechanics a third time here would fork them. The
|
|
21
|
+
* consuming app mounts this inside whichever container the breakpoint calls for.
|
|
22
|
+
*
|
|
23
|
+
* This is the terminal surface for acting on an event, which is why the cards
|
|
24
|
+
* that open it carry no actions of their own — the footer is where booking and
|
|
25
|
+
* directions live.
|
|
26
|
+
*
|
|
27
|
+
* Everything is injected: strings via `labels`, times via `formatters`, and the
|
|
28
|
+
* close/share affordances as `headerActions` so the sheet container owns
|
|
29
|
+
* dismissal (bearmenu sheets close by drag handle, not by back navigation).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export type EventDetailPanelLinkProps = {
|
|
33
|
+
href: string;
|
|
34
|
+
className?: string;
|
|
35
|
+
children: React.ReactNode;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function DefaultLink({ href, ...props }: EventDetailPanelLinkProps) {
|
|
39
|
+
return <a href={href} {...props} />;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type EventDetailVenue = {
|
|
43
|
+
name: string;
|
|
44
|
+
/** Address plus opening line, composed by the caller. */
|
|
45
|
+
meta?: string;
|
|
46
|
+
ratingLabel?: string;
|
|
47
|
+
thumbnailUrl?: string;
|
|
48
|
+
href?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type EventDetailPanelLabels = {
|
|
52
|
+
free: string;
|
|
53
|
+
liveNow: string;
|
|
54
|
+
/** Section heading above the venue card. */
|
|
55
|
+
venueHeading: string;
|
|
56
|
+
/** Section heading above the description. */
|
|
57
|
+
aboutHeading: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type EventDetailPanelProps = {
|
|
61
|
+
event: EventPreview;
|
|
62
|
+
labels: EventDetailPanelLabels;
|
|
63
|
+
formatters: EventFormatters;
|
|
64
|
+
/**
|
|
65
|
+
* The date line, composed by the caller — the design reads
|
|
66
|
+
* "Saturday, September 5 · Starts at 20:00", whose clause order and
|
|
67
|
+
* preposition are locale-dependent.
|
|
68
|
+
*/
|
|
69
|
+
dateLine: string;
|
|
70
|
+
venue?: EventDetailVenue;
|
|
71
|
+
/** Category chips beside the live/price chips. */
|
|
72
|
+
tags?: string[];
|
|
73
|
+
isLive?: boolean;
|
|
74
|
+
/** Close and share controls, positioned over the poster by this component. */
|
|
75
|
+
headerActions?: React.ReactNode;
|
|
76
|
+
/** Footer actions — the booking CTA and directions. Rendered in a sticky footer. */
|
|
77
|
+
footer?: React.ReactNode;
|
|
78
|
+
/** An external link row, e.g. "View on Instagram". */
|
|
79
|
+
externalLink?: React.ReactNode;
|
|
80
|
+
/** `compact` is the mobile bottom sheet: tighter type and padding, same 4:5 poster. */
|
|
81
|
+
size?: "compact" | "default";
|
|
82
|
+
linkComponent?: React.ComponentType<EventDetailPanelLinkProps>;
|
|
83
|
+
className?: string;
|
|
84
|
+
/** Rendered instead of the poster when it's absent. Defaults to `<EventPosterFallback />`. */
|
|
85
|
+
fallback?: React.ReactNode;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const CHIP =
|
|
89
|
+
"inline-flex items-center gap-1 rounded-full border border-border px-2.5 text-xs";
|
|
90
|
+
|
|
91
|
+
export function EventDetailPanel({
|
|
92
|
+
event,
|
|
93
|
+
labels,
|
|
94
|
+
formatters,
|
|
95
|
+
dateLine,
|
|
96
|
+
venue,
|
|
97
|
+
tags,
|
|
98
|
+
isLive,
|
|
99
|
+
headerActions,
|
|
100
|
+
footer,
|
|
101
|
+
externalLink,
|
|
102
|
+
size = "default",
|
|
103
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
104
|
+
className,
|
|
105
|
+
fallback = <EventPosterFallback />,
|
|
106
|
+
}: EventDetailPanelProps) {
|
|
107
|
+
const compact = size === "compact";
|
|
108
|
+
const fee = resolveEventEntryFee(event);
|
|
109
|
+
|
|
110
|
+
const venueCard = venue && (
|
|
111
|
+
<div className="flex items-center gap-3 rounded-[13px] border border-border p-2.5">
|
|
112
|
+
<div
|
|
113
|
+
className={cn(
|
|
114
|
+
"shrink-0 overflow-hidden rounded-[10px] bg-muted",
|
|
115
|
+
compact ? "h-12 w-12" : "h-[52px] w-[52px]"
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
{venue.thumbnailUrl && (
|
|
119
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
120
|
+
<img
|
|
121
|
+
src={venue.thumbnailUrl}
|
|
122
|
+
alt=""
|
|
123
|
+
className="h-full w-full object-cover"
|
|
124
|
+
loading="lazy"
|
|
125
|
+
decoding="async"
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
<div className="min-w-0 flex-1">
|
|
130
|
+
<div className={cn("font-bold", compact ? "text-[13.5px]" : "text-sm")}>{venue.name}</div>
|
|
131
|
+
{venue.meta && (
|
|
132
|
+
<div className={cn("text-muted-foreground", compact ? "text-[11.5px]" : "text-xs")}>
|
|
133
|
+
{venue.meta}
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
{venue.ratingLabel && (
|
|
137
|
+
<div className="mt-0.5 flex items-center gap-1 text-xs font-bold">
|
|
138
|
+
<Star className="h-3 w-3" aria-hidden />
|
|
139
|
+
{venue.ratingLabel}
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className={cn("flex h-full flex-col overflow-hidden bg-card", className)}>
|
|
149
|
+
<div className="flex-1 overflow-y-auto">
|
|
150
|
+
<div className="relative aspect-[4/5] shrink-0 overflow-hidden bg-muted">
|
|
151
|
+
{event.poster_url ? (
|
|
152
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
153
|
+
<img
|
|
154
|
+
src={event.poster_url}
|
|
155
|
+
alt={event.title}
|
|
156
|
+
className="h-full w-full object-cover"
|
|
157
|
+
loading="eager"
|
|
158
|
+
decoding="async"
|
|
159
|
+
/>
|
|
160
|
+
) : (
|
|
161
|
+
fallback
|
|
162
|
+
)}
|
|
163
|
+
{headerActions}
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div
|
|
167
|
+
className={cn(
|
|
168
|
+
"flex flex-col gap-3.5",
|
|
169
|
+
compact ? "px-4 pt-4" : "px-[22px] pt-5"
|
|
170
|
+
)}
|
|
171
|
+
>
|
|
172
|
+
<h2
|
|
173
|
+
className={cn(
|
|
174
|
+
"font-extrabold leading-[1.25] tracking-[-0.02em]",
|
|
175
|
+
compact ? "text-xl" : "text-[21px]"
|
|
176
|
+
)}
|
|
177
|
+
>
|
|
178
|
+
{event.title}
|
|
179
|
+
</h2>
|
|
180
|
+
|
|
181
|
+
<div
|
|
182
|
+
className={cn(
|
|
183
|
+
"flex items-center gap-1.5 text-muted-foreground",
|
|
184
|
+
compact ? "text-[12.5px]" : "text-[13px]"
|
|
185
|
+
)}
|
|
186
|
+
>
|
|
187
|
+
<Calendar className={compact ? "h-3.5 w-3.5" : "h-4 w-4"} aria-hidden />
|
|
188
|
+
{dateLine}
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{/* The live chip is green, not the brand accent: "on now" reads as one
|
|
192
|
+
colour across every events surface, and it is the same green the app
|
|
193
|
+
already uses for "open now". */}
|
|
194
|
+
<div className="flex flex-wrap gap-1.5">
|
|
195
|
+
{isLive && (
|
|
196
|
+
<span
|
|
197
|
+
className={cn(
|
|
198
|
+
CHIP,
|
|
199
|
+
"h-7 border-success/30 bg-success/10 font-semibold text-success"
|
|
200
|
+
)}
|
|
201
|
+
>
|
|
202
|
+
<Radio className="h-3 w-3" aria-hidden />
|
|
203
|
+
{labels.liveNow}
|
|
204
|
+
</span>
|
|
205
|
+
)}
|
|
206
|
+
{tags?.map((tag) => (
|
|
207
|
+
<span key={tag} className={cn(CHIP, "h-7")}>
|
|
208
|
+
{tag}
|
|
209
|
+
</span>
|
|
210
|
+
))}
|
|
211
|
+
{fee.kind === "free" && (
|
|
212
|
+
<span className={cn(CHIP, "h-7 border-success/40 font-semibold text-success")}>
|
|
213
|
+
{labels.free}
|
|
214
|
+
</span>
|
|
215
|
+
)}
|
|
216
|
+
{fee.kind === "paid" && (
|
|
217
|
+
<span className={cn(CHIP, "h-7")}>{formatters.price(fee.amountRon)}</span>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
{venue && (
|
|
222
|
+
<div>
|
|
223
|
+
<div className="pb-2 text-[13px] font-extrabold">{labels.venueHeading}</div>
|
|
224
|
+
{venue.href ? (
|
|
225
|
+
<LinkComponent href={venue.href} className="block">
|
|
226
|
+
{venueCard}
|
|
227
|
+
</LinkComponent>
|
|
228
|
+
) : (
|
|
229
|
+
venueCard
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
|
|
234
|
+
{event.description && (
|
|
235
|
+
<div>
|
|
236
|
+
<div className="pb-1.5 text-[13px] font-extrabold">{labels.aboutHeading}</div>
|
|
237
|
+
<p
|
|
238
|
+
className={cn(
|
|
239
|
+
"leading-[1.6] text-muted-foreground",
|
|
240
|
+
compact ? "text-[13px]" : "text-[13.5px]"
|
|
241
|
+
)}
|
|
242
|
+
>
|
|
243
|
+
{event.description}
|
|
244
|
+
</p>
|
|
245
|
+
</div>
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
{externalLink}
|
|
249
|
+
<div className="h-2" aria-hidden />
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{footer && (
|
|
254
|
+
<div
|
|
255
|
+
className={cn(
|
|
256
|
+
"sticky bottom-0 flex shrink-0 gap-2.5 bg-card",
|
|
257
|
+
compact
|
|
258
|
+
? "px-4 pt-3 pb-[max(1.375rem,env(safe-area-inset-bottom))]"
|
|
259
|
+
: "px-[22px] pt-4 pb-[max(1.25rem,env(safe-area-inset-bottom))]"
|
|
260
|
+
)}
|
|
261
|
+
>
|
|
262
|
+
<Separator className="absolute inset-x-0 top-0" />
|
|
263
|
+
|
|
264
|
+
{footer}
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export type EventDetailPanelSkeletonProps = {
|
|
272
|
+
/** `compact` is the mobile bottom sheet: tighter type and padding, same 4:5 poster. */
|
|
273
|
+
size?: "compact" | "default";
|
|
274
|
+
/** Close and share controls, positioned over the poster — same slot as the real component. */
|
|
275
|
+
headerActions?: React.ReactNode;
|
|
276
|
+
className?: string;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/** Mirrors `<EventDetailPanel>`'s geometry exactly: the hero scrolls away with the body, the footer is sticky. */
|
|
280
|
+
export function EventDetailPanelSkeleton({
|
|
281
|
+
size = "default",
|
|
282
|
+
headerActions,
|
|
283
|
+
className,
|
|
284
|
+
}: EventDetailPanelSkeletonProps) {
|
|
285
|
+
const compact = size === "compact";
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<div
|
|
289
|
+
aria-hidden="true"
|
|
290
|
+
className={cn("flex h-full flex-col overflow-hidden bg-card", className)}
|
|
291
|
+
>
|
|
292
|
+
<div className="flex-1 overflow-y-auto">
|
|
293
|
+
<div className="relative aspect-[4/5] shrink-0 overflow-hidden bg-muted">
|
|
294
|
+
{headerActions}
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<div
|
|
298
|
+
className={cn(
|
|
299
|
+
"flex flex-col gap-3.5",
|
|
300
|
+
compact ? "px-4 pt-4" : "px-[22px] pt-5"
|
|
301
|
+
)}
|
|
302
|
+
>
|
|
303
|
+
<Skeleton className={cn("w-3/4", compact ? "h-5" : "h-[21px]")} />
|
|
304
|
+
|
|
305
|
+
<div className="flex items-center gap-1.5">
|
|
306
|
+
<Skeleton className={cn("shrink-0 rounded-full", compact ? "h-3.5 w-3.5" : "h-4 w-4")} />
|
|
307
|
+
<Skeleton className={cn("w-48", compact ? "h-[12.5px]" : "h-[13px]")} />
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<div className="flex flex-wrap gap-1.5">
|
|
311
|
+
<Skeleton className="h-7 w-20 rounded-full" />
|
|
312
|
+
<Skeleton className="h-7 w-16 rounded-full" />
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<div>
|
|
316
|
+
<Skeleton className="mb-2 h-[13px] w-28" />
|
|
317
|
+
<div className="flex items-center gap-3 rounded-[13px] border border-border p-2.5">
|
|
318
|
+
<Skeleton
|
|
319
|
+
className={cn(
|
|
320
|
+
"shrink-0 rounded-[10px]",
|
|
321
|
+
compact ? "h-12 w-12" : "h-[52px] w-[52px]"
|
|
322
|
+
)}
|
|
323
|
+
/>
|
|
324
|
+
<div className="min-w-0 flex-1">
|
|
325
|
+
<Skeleton className={cn("w-1/2", compact ? "h-[13.5px]" : "h-3.5")} />
|
|
326
|
+
<Skeleton className={cn("mt-1 w-3/4", compact ? "h-[11.5px]" : "h-3")} />
|
|
327
|
+
</div>
|
|
328
|
+
<Skeleton className="h-4 w-4 shrink-0 rounded-full" />
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<div>
|
|
333
|
+
<Skeleton className="mb-1.5 h-[13px] w-32" />
|
|
334
|
+
<div className="flex flex-col gap-1.5">
|
|
335
|
+
<Skeleton className={cn("w-full", compact ? "h-[13px]" : "h-[13.5px]")} />
|
|
336
|
+
<Skeleton className={cn("w-full", compact ? "h-[13px]" : "h-[13.5px]")} />
|
|
337
|
+
<Skeleton className={cn("w-2/3", compact ? "h-[13px]" : "h-[13.5px]")} />
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<div className="h-2" aria-hidden />
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<div
|
|
346
|
+
className={cn(
|
|
347
|
+
"sticky bottom-0 flex shrink-0 gap-2.5 bg-card",
|
|
348
|
+
compact
|
|
349
|
+
? "px-4 pt-3 pb-[max(1.375rem,env(safe-area-inset-bottom))]"
|
|
350
|
+
: "px-[22px] pt-4 pb-[max(1.25rem,env(safe-area-inset-bottom))]"
|
|
351
|
+
)}
|
|
352
|
+
>
|
|
353
|
+
<Separator className="absolute inset-x-0 top-0" />
|
|
354
|
+
|
|
355
|
+
<Skeleton className="h-[46px] flex-1 rounded-xl" />
|
|
356
|
+
<Skeleton className="h-[46px] w-24 rounded-xl" />
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { EventHubHero, EventHubHeroSkeleton } from "./event-hub-hero";
|
|
3
|
+
import { PosterStack } from "./poster-stack";
|
|
4
|
+
|
|
5
|
+
function posterDataUri(label: string, fill: string) {
|
|
6
|
+
return (
|
|
7
|
+
"data:image/svg+xml;utf8," +
|
|
8
|
+
encodeURIComponent(
|
|
9
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="300" height="400"><rect width="300" height="400" fill="${fill}"/><text x="50%" y="50%" font-family="monospace" font-size="20" fill="#fff" text-anchor="middle">${label}</text></svg>`
|
|
10
|
+
)
|
|
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 EventHubHero> = {
|
|
18
|
+
title: "Events/EventHubHero",
|
|
19
|
+
component: EventHubHero,
|
|
20
|
+
tags: ["autodocs"],
|
|
21
|
+
args: {
|
|
22
|
+
eyebrow: "Updated today",
|
|
23
|
+
title: "Everything happening in Bucharest",
|
|
24
|
+
intro:
|
|
25
|
+
"Concerts, parties, exhibitions and everything in between — hand-curated and updated every morning.",
|
|
26
|
+
stats: [
|
|
27
|
+
{ value: "312", label: "events this month" },
|
|
28
|
+
{ value: "48", label: "venues" },
|
|
29
|
+
{ value: "19", label: "free tonight" },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
decorators: [
|
|
33
|
+
(Story) => (
|
|
34
|
+
<div className="p-6">
|
|
35
|
+
<Story />
|
|
36
|
+
</div>
|
|
37
|
+
),
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default meta;
|
|
42
|
+
type Story = StoryObj<typeof EventHubHero>;
|
|
43
|
+
|
|
44
|
+
export const Default: Story = {};
|
|
45
|
+
|
|
46
|
+
export const WithPosterStackAside: Story = {
|
|
47
|
+
args: {
|
|
48
|
+
aside: (
|
|
49
|
+
<PosterStack
|
|
50
|
+
posters={[
|
|
51
|
+
{ src: posterDataUri("1", "#7a5c46"), alt: "Psihotrop poster" },
|
|
52
|
+
{ src: posterDataUri("2", "#4a6a5c"), alt: "Warehouse rave poster" },
|
|
53
|
+
{ src: posterDataUri("3", "#6a5c8a"), alt: "Jazz night poster" },
|
|
54
|
+
]}
|
|
55
|
+
/>
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const NoAside: Story = {
|
|
61
|
+
args: { aside: undefined },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const Skeleton: Story = {
|
|
65
|
+
render: () => <EventHubHeroSkeleton aside={<div className="aspect-[3/2] rounded-xl bg-muted" />} />,
|
|
66
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
import { Skeleton } from "./skeleton";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* EventHubHero — the hub's opening block: an eyebrow, a big headline, an
|
|
9
|
+
* intro line and a stats row on the left; an arbitrary slot (the poster
|
|
10
|
+
* stack, in the source design) on the right. Stacks on mobile.
|
|
11
|
+
*
|
|
12
|
+
* All copy and stat values arrive pre-formatted — the DS holds no `Intl`
|
|
13
|
+
* locale and does not format numbers.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type EventHubHeroStat = {
|
|
17
|
+
value: string;
|
|
18
|
+
label: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type EventHubHeroProps = {
|
|
22
|
+
/** Small caps-style label above the title, e.g. "Updated today". */
|
|
23
|
+
eyebrow: string;
|
|
24
|
+
title: string;
|
|
25
|
+
intro: string;
|
|
26
|
+
stats: EventHubHeroStat[];
|
|
27
|
+
/** The right-column slot — the poster stack in the source design. */
|
|
28
|
+
aside?: React.ReactNode;
|
|
29
|
+
className?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function EventHubHero({ eyebrow, title, intro, stats, aside, className }: EventHubHeroProps) {
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
className={cn(
|
|
36
|
+
"grid grid-cols-1 gap-8 lg:grid-cols-[1fr_620px] lg:items-center lg:gap-10",
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
<div className="flex flex-col gap-4">
|
|
41
|
+
<p className="text-[11.5px] font-extrabold tracking-[0.12em] text-muted-foreground">
|
|
42
|
+
{eyebrow}
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<h1 className="text-[31px] font-black leading-[1.05] tracking-[-0.035em] text-foreground md:text-[42px] md:tracking-[-0.04em]">
|
|
46
|
+
{title}
|
|
47
|
+
</h1>
|
|
48
|
+
|
|
49
|
+
<p className="max-w-[520px] text-[14px] leading-[1.6] text-muted-foreground md:text-[15px] md:leading-[1.65]">
|
|
50
|
+
{intro}
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
<div className="flex gap-[22px] pt-1.5 md:gap-[30px]">
|
|
54
|
+
{stats.map((stat) => (
|
|
55
|
+
<div key={stat.label} className="flex flex-col">
|
|
56
|
+
<span className="text-[24px] font-black leading-none tracking-[-0.03em] tabular-nums text-foreground md:text-[30px]">
|
|
57
|
+
{stat.value}
|
|
58
|
+
</span>
|
|
59
|
+
<span className="mt-1 text-[11px] text-muted-foreground md:mt-[5px] md:text-[12.5px]">
|
|
60
|
+
{stat.label}
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
{aside && <div className="min-w-0">{aside}</div>}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type EventHubHeroSkeletonProps = {
|
|
73
|
+
/** The right-column slot, e.g. a `PosterStack`-shaped placeholder. Mirrors `aside` on the real component. */
|
|
74
|
+
aside?: React.ReactNode;
|
|
75
|
+
className?: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export function EventHubHeroSkeleton({ aside, className }: EventHubHeroSkeletonProps) {
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
aria-hidden="true"
|
|
82
|
+
className={cn(
|
|
83
|
+
"grid grid-cols-1 gap-8 lg:grid-cols-[1fr_620px] lg:items-center lg:gap-10",
|
|
84
|
+
className
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
<div className="flex flex-col gap-4">
|
|
88
|
+
<Skeleton className="h-[11.5px] w-28" />
|
|
89
|
+
|
|
90
|
+
<Skeleton className="h-[31px] w-full max-w-[420px] md:h-[42px]" />
|
|
91
|
+
|
|
92
|
+
<div className="flex max-w-[520px] flex-col gap-1.5">
|
|
93
|
+
<Skeleton className="h-[14px] w-full md:h-[15px]" />
|
|
94
|
+
<Skeleton className="h-[14px] w-4/5 md:h-[15px]" />
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div className="flex gap-[22px] pt-1.5 md:gap-[30px]">
|
|
98
|
+
{Array.from({ length: 3 }, (_, index) => (
|
|
99
|
+
<div key={index} className="flex flex-col">
|
|
100
|
+
<Skeleton className="h-[24px] w-10 md:h-[30px]" />
|
|
101
|
+
<Skeleton className="mt-1 h-[11px] w-16 md:mt-[5px] md:h-[12.5px]" />
|
|
102
|
+
</div>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{aside && <div className="min-w-0">{aside}</div>}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Music, Wine } from "lucide-react";
|
|
3
|
+
import { EventIndexRow } from "./event-index-row";
|
|
4
|
+
import { EventDaySeparator } from "./event-day-separator";
|
|
5
|
+
import { EventPosterFallback } from "./event-poster-fallback";
|
|
6
|
+
|
|
7
|
+
const POSTER = "https://images.unsplash.com/photo-1514525253161-7a46d19cd819?w=256&h=256&fit=crop";
|
|
8
|
+
|
|
9
|
+
const DESCRIPTION =
|
|
10
|
+
"Trupa cântă un set de două ore din repertoriul propriu plus reinterpretări de standarde, cu pauză de treizeci de minute la mijloc. Barul rămâne deschis până la ora două, iar bucătăria ia ultima comandă la unsprezece.";
|
|
11
|
+
|
|
12
|
+
const meta: Meta<typeof EventIndexRow> = {
|
|
13
|
+
title: "Events/EventIndexRow",
|
|
14
|
+
component: EventIndexRow,
|
|
15
|
+
tags: ["autodocs"],
|
|
16
|
+
args: {
|
|
17
|
+
title: "Afterwork Karaoke",
|
|
18
|
+
href: "#event",
|
|
19
|
+
when: "26 iulie, 20:00–23:00",
|
|
20
|
+
venue: "Cafe Solas",
|
|
21
|
+
venueHref: "#place",
|
|
22
|
+
fee: "intrare liberă",
|
|
23
|
+
tags: ["Karaoke", "Live music"],
|
|
24
|
+
description: DESCRIPTION,
|
|
25
|
+
posterUrl: POSTER,
|
|
26
|
+
},
|
|
27
|
+
// The row sits on a tinted band on its real surface; the decorator mirrors
|
|
28
|
+
// that so the `outline` tags and the `bg-card/60` hover are judged against
|
|
29
|
+
// the ground they actually land on, not against plain `--background`.
|
|
30
|
+
decorators: [
|
|
31
|
+
(Story) => (
|
|
32
|
+
<div className="w-full max-w-[900px] bg-muted/50 p-6">
|
|
33
|
+
<Story />
|
|
34
|
+
</div>
|
|
35
|
+
),
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default meta;
|
|
40
|
+
type Story = StoryObj<typeof EventIndexRow>;
|
|
41
|
+
|
|
42
|
+
export const Default: Story = {};
|
|
43
|
+
|
|
44
|
+
/** The shape the section actually renders: a heading that ends in its own rule,
|
|
45
|
+
* then rows, the first of which suppresses its rule to avoid doubling. */
|
|
46
|
+
export const InAGroup: Story = {
|
|
47
|
+
render: (args) => (
|
|
48
|
+
<div>
|
|
49
|
+
<EventDaySeparator as="h2" label="Evenimente azi în Cluj-Napoca" count="3 evenimente" />
|
|
50
|
+
<EventIndexRow {...args} hideRule />
|
|
51
|
+
<EventIndexRow
|
|
52
|
+
{...args}
|
|
53
|
+
title="Degustare de vinuri naturale"
|
|
54
|
+
when="26 iulie, 19:00"
|
|
55
|
+
venue="Enoteca Veche"
|
|
56
|
+
fee="bilet 80 lei"
|
|
57
|
+
tags={["Wine tasting"]}
|
|
58
|
+
posterUrl={undefined}
|
|
59
|
+
fallback={<EventPosterFallback icon={<Wine className="h-5 w-5 md:h-7 md:w-7" aria-hidden />} />}
|
|
60
|
+
/>
|
|
61
|
+
<EventIndexRow
|
|
62
|
+
{...args}
|
|
63
|
+
title="Peggy Gou b2b special guest"
|
|
64
|
+
when="26 iulie, 23:00–05:00"
|
|
65
|
+
venue="Form Space"
|
|
66
|
+
fee="bilet 140 lei"
|
|
67
|
+
tags={["Techno", "Party"]}
|
|
68
|
+
fallback={<EventPosterFallback icon={<Music className="h-5 w-5 md:h-7 md:w-7" aria-hidden />} />}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** No poster: the gradient fallback is already layered behind the image slot,
|
|
75
|
+
* so this is what a missing OR a failed image renders — with no JS. */
|
|
76
|
+
export const NoPoster: Story = {
|
|
77
|
+
args: { posterUrl: undefined },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const TagDerivedFallback: Story = {
|
|
81
|
+
args: {
|
|
82
|
+
posterUrl: undefined,
|
|
83
|
+
fallback: <EventPosterFallback icon={<Wine className="h-5 w-5 md:h-7 md:w-7" aria-hidden />} />,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** An event nobody wrote a description for. The row closes up rather than
|
|
88
|
+
* reserving empty space, so a group of mixed rows still scans. */
|
|
89
|
+
export const NoDescription: Story = {
|
|
90
|
+
args: { description: undefined },
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** A venue with no place page — the name stays plain text rather than becoming
|
|
94
|
+
* a link to nowhere. */
|
|
95
|
+
export const NoVenueLink: Story = {
|
|
96
|
+
args: { venueHref: undefined },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const NoTags: Story = {
|
|
100
|
+
args: { tags: undefined },
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** Long titles WRAP. They are document-outline headings, so truncating one
|
|
104
|
+
* would hide text from the outline the section exists to build. */
|
|
105
|
+
export const LongTitle: Story = {
|
|
106
|
+
args: {
|
|
107
|
+
title:
|
|
108
|
+
"Concert aniversar: douăzeci de ani de la primul album, cu invitați speciali și orchestră de cameră",
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** `hideRule` on its own, for the first row directly under any heading that
|
|
113
|
+
* already draws a rule. */
|
|
114
|
+
export const FirstInGroup: Story = {
|
|
115
|
+
args: { hideRule: true },
|
|
116
|
+
};
|