@bearmenu/ui 0.8.3 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2WDJR6K7.js +76 -0
- 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/alert.d.ts +2 -2
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- 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/command.d.ts +15 -15
- 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/date-input.d.ts +1 -1
- 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 +161 -0
- package/dist/components/event-compact-row.d.ts +72 -0
- package/dist/components/event-compact-row.js +133 -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 +118 -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/icon.d.ts +18 -0
- package/dist/components/icon.js +31 -0
- package/dist/components/item.d.ts +2 -2
- package/dist/components/motion-icon-controls.d.ts +26 -0
- package/dist/components/motion-icon-controls.js +4 -0
- package/dist/components/motion-icon-set.d.ts +27 -0
- package/dist/components/motion-icon-set.js +139 -0
- package/dist/components/motion-icon.d.ts +44 -0
- package/dist/components/motion-icon.js +3 -0
- 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/spinner.d.ts +2 -2
- 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/text.d.ts +3 -3
- 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/dist/lib/icon-registry.d.ts +58 -0
- package/dist/lib/icon-registry.js +39 -0
- package/package.json +2 -1
- package/src/components/accordion.stories.tsx +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/combobox.stories.tsx +1 -1
- 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/currency-input.stories.tsx +3 -3
- 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/empty-state.stories.tsx +3 -3
- package/src/components/event-card.stories.tsx +248 -0
- package/src/components/event-card.test.tsx +177 -0
- package/src/components/event-card.tsx +321 -0
- package/src/components/event-compact-row.stories.tsx +221 -0
- package/src/components/event-compact-row.tsx +268 -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 +133 -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/form.stories.tsx +1 -1
- 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/icon.tsx +57 -0
- package/src/components/input-otp.stories.tsx +1 -1
- package/src/components/markdown-renderer.stories.tsx +1 -1
- package/src/components/motion-icon-controls.test.tsx +78 -0
- package/src/components/motion-icon-controls.tsx +552 -0
- package/src/components/motion-icon-set.stories.tsx +185 -0
- package/src/components/motion-icon-set.tsx +219 -0
- package/src/components/motion-icon.test.tsx +159 -0
- package/src/components/motion-icon.tsx +163 -0
- package/src/components/multi-select-combobox.stories.tsx +1 -1
- package/src/components/phone-frame.stories.tsx +1 -1
- 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/rating.stories.tsx +3 -3
- package/src/components/searchable-select.stories.tsx +1 -1
- 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/selection-bar.stories.tsx +1 -1
- package/src/components/skeleton-card.stories.tsx +3 -3
- package/src/components/skeleton.tsx +18 -0
- package/src/components/sliding-panels.stories.tsx +1 -1
- package/src/components/stepper.stories.tsx +1 -1
- package/src/components/sticky-container.stories.tsx +3 -3
- 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/toggle-group.stories.tsx +1 -1
- package/src/components/tooltip.stories.tsx +2 -2
- 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
- package/src/lib/icon-registry.ts +139 -0
- package/src/test/setup.ts +5 -1
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { memo, useState } from "react";
|
|
4
|
+
import { Gift, MapPin, Radio } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
import { EventPosterFallback } from "./event-poster-fallback";
|
|
7
|
+
import { Skeleton } from "./skeleton";
|
|
8
|
+
import { Tag } from "./tag";
|
|
9
|
+
import { TagGroup } from "./tag-group";
|
|
10
|
+
import {
|
|
11
|
+
resolveEventEntryFee,
|
|
12
|
+
type EventCardLabels,
|
|
13
|
+
type EventFormatters,
|
|
14
|
+
type EventPreview,
|
|
15
|
+
} from "./event-types";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* EventCard — the poster card every events surface reuses.
|
|
19
|
+
*
|
|
20
|
+
* RENDERS AN ANCHOR WHENEVER `href` IS GIVEN, and that is not incidental. The
|
|
21
|
+
* consumer app's events hub was measured serving 2,751 words and 50 links in
|
|
22
|
+
* its raw HTML but only 12 links — none of them to an event — in the rendered
|
|
23
|
+
* DOM, because the hydrated cards were `<button onClick>` rather than links.
|
|
24
|
+
* Pass `href` AND `onClick`: the anchor is real for crawlers, middle-click and
|
|
25
|
+
* "open in new tab", while `onClick` opens the detail sheet (call
|
|
26
|
+
* `preventDefault()` there). Falls back to a `<button>` when only `onClick` is
|
|
27
|
+
* given, and a plain `<div>` when neither is.
|
|
28
|
+
*
|
|
29
|
+
* Like `place-card.tsx`, this takes a DS-owned view model, injected labels and
|
|
30
|
+
* injected formatters — no app schema, no app hooks, no i18n, and a plain
|
|
31
|
+
* `<img>` so it works outside Next.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export type EventCardLinkProps = {
|
|
35
|
+
href: string;
|
|
36
|
+
className?: string;
|
|
37
|
+
children: React.ReactNode;
|
|
38
|
+
onClick?: (mouseEvent: React.MouseEvent) => void;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function DefaultLink({ href, ...props }: EventCardLinkProps) {
|
|
42
|
+
return <a href={href} {...props} />;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type EventCardProps = {
|
|
46
|
+
event: EventPreview;
|
|
47
|
+
labels: EventCardLabels;
|
|
48
|
+
formatters: EventFormatters;
|
|
49
|
+
/**
|
|
50
|
+
* Poster ratio. `4/3` is the house shape shared with the dish/drink/place
|
|
51
|
+
* cards — the default, and what every card sitting in a column should use.
|
|
52
|
+
* `3/4` is the hub's original portrait deck/rail/tape ratio. `4/5` matches
|
|
53
|
+
* the Instagram source most real posters ship at (1080x1350) and is used by
|
|
54
|
+
* the hub's editorial poster surfaces (`CategoryDeck`, `PosterTape`,
|
|
55
|
+
* `Coverflow`) — it crops less than `3/4` and is not the same as it.
|
|
56
|
+
*/
|
|
57
|
+
posterAspect?: "4/3" | "3/4" | "4/5";
|
|
58
|
+
/** Larger title — the featured/highlight slot. Never a clamp: the title wraps in full either way. */
|
|
59
|
+
emphasis?: boolean;
|
|
60
|
+
/** Top-left overlay pill (the design's "Busy" hype marker). Include its icon. */
|
|
61
|
+
badge?: React.ReactNode;
|
|
62
|
+
/** Bottom-left overlay pill naming the category. Include its icon. */
|
|
63
|
+
tagLabel?: React.ReactNode;
|
|
64
|
+
/**
|
|
65
|
+
* Caller-supplied display labels, rendered as small chips in the text area
|
|
66
|
+
* beneath the venue/meta line. `EventPreview.tags` is taxonomy coordinates
|
|
67
|
+
* (`slug`/`axis_slug`), not display text, so the DS cannot render them
|
|
68
|
+
* itself. Distinct from `tagLabel`: that is a single overlay pill on the
|
|
69
|
+
* poster (the design's category affordance for posters); this is an
|
|
70
|
+
* in-body list for the rest of an event's tags. Cap the count at the call
|
|
71
|
+
* site — this card does not.
|
|
72
|
+
*/
|
|
73
|
+
tags?: string[];
|
|
74
|
+
/**
|
|
75
|
+
* The live line, fully composed by the caller (e.g. "Live now · ends around
|
|
76
|
+
* 23:00"). Composed outside the DS deliberately: the clause order differs by
|
|
77
|
+
* locale, and "is this live" depends on the app's clock, not the DS's.
|
|
78
|
+
*/
|
|
79
|
+
liveStatus?: string;
|
|
80
|
+
/** Hide the day/time line — the timeline supplies its own hour heading. */
|
|
81
|
+
hideWhen?: boolean;
|
|
82
|
+
/** Overrides the day label when the card appears outside its own day section. */
|
|
83
|
+
dayLabel?: string;
|
|
84
|
+
/** Append the entry fee to the meta line. Off by default, matching the design's base card. */
|
|
85
|
+
showPrice?: boolean;
|
|
86
|
+
href?: string;
|
|
87
|
+
/** Defaults to a plain `<a>`. Pass `next/link` in a Next app. */
|
|
88
|
+
linkComponent?: React.ComponentType<EventCardLinkProps>;
|
|
89
|
+
onClick?: (event: EventPreview, mouseEvent: React.MouseEvent) => void;
|
|
90
|
+
className?: string;
|
|
91
|
+
/** Forwarded to the poster's `loading` attribute (`"eager"` when true). */
|
|
92
|
+
priority?: boolean;
|
|
93
|
+
/** Rendered instead of the poster when it's absent or failed to load. Defaults to `<EventPosterFallback />`. */
|
|
94
|
+
fallback?: React.ReactNode;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const PILL_BASE =
|
|
98
|
+
"inline-flex items-center gap-1 h-6 max-w-[70%] px-2 rounded-full text-[11px] font-semibold";
|
|
99
|
+
|
|
100
|
+
export const EventCard = memo(function EventCard({
|
|
101
|
+
event,
|
|
102
|
+
labels,
|
|
103
|
+
formatters,
|
|
104
|
+
posterAspect = "4/3",
|
|
105
|
+
emphasis,
|
|
106
|
+
badge,
|
|
107
|
+
tagLabel,
|
|
108
|
+
tags,
|
|
109
|
+
liveStatus,
|
|
110
|
+
hideWhen,
|
|
111
|
+
dayLabel,
|
|
112
|
+
showPrice,
|
|
113
|
+
href,
|
|
114
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
115
|
+
onClick,
|
|
116
|
+
className,
|
|
117
|
+
priority,
|
|
118
|
+
fallback = <EventPosterFallback />,
|
|
119
|
+
}: EventCardProps) {
|
|
120
|
+
const [imgError, setImgError] = useState(false);
|
|
121
|
+
|
|
122
|
+
const fee = resolveEventEntryFee(event);
|
|
123
|
+
const showPoster = !!event.poster_url && !imgError;
|
|
124
|
+
const venue = event.place_name ?? event.location;
|
|
125
|
+
|
|
126
|
+
const body = (
|
|
127
|
+
<>
|
|
128
|
+
<div
|
|
129
|
+
className={cn(
|
|
130
|
+
"relative overflow-hidden rounded-xl bg-muted",
|
|
131
|
+
posterAspect === "3/4"
|
|
132
|
+
? "aspect-[3/4]"
|
|
133
|
+
: posterAspect === "4/5"
|
|
134
|
+
? "aspect-[4/5]"
|
|
135
|
+
: "aspect-[4/3]"
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
138
|
+
{showPoster ? (
|
|
139
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
140
|
+
<img
|
|
141
|
+
src={event.poster_url}
|
|
142
|
+
alt={event.title}
|
|
143
|
+
title={event.title}
|
|
144
|
+
loading={priority ? "eager" : "lazy"}
|
|
145
|
+
decoding="async"
|
|
146
|
+
className="h-full w-full object-cover"
|
|
147
|
+
onError={() => setImgError(true)}
|
|
148
|
+
/>
|
|
149
|
+
) : (
|
|
150
|
+
fallback
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
{/* Top overlay row — hype badge left, entry fee right. The empty span
|
|
154
|
+
keeps the fee pill right-aligned when there is no badge. */}
|
|
155
|
+
<div className="absolute inset-x-2 top-2 flex items-start justify-between gap-2">
|
|
156
|
+
{badge ? <span className={cn(PILL_BASE, "bg-black/55 text-white")}>{badge}</span> : <span />}
|
|
157
|
+
{fee.kind === "free" && (
|
|
158
|
+
<span className={cn(PILL_BASE, "bg-success font-bold text-success-foreground")}>
|
|
159
|
+
<Gift className="h-3 w-3" aria-hidden />
|
|
160
|
+
{labels.free}
|
|
161
|
+
</span>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{tagLabel && (
|
|
166
|
+
<div className="absolute inset-x-2 bottom-2 flex gap-1">
|
|
167
|
+
<span className={cn(PILL_BASE, "bg-black/55 text-white")}>{tagLabel}</span>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{/* NEVER CLAMPED, in any variant. The title is the one thing a card
|
|
173
|
+
exists to say, and "Movie Night: Futni m…" says none of it — the
|
|
174
|
+
truncated form was the reported defect. It wraps to as many lines as
|
|
175
|
+
the event needs; the card is `flex-col` and grows downward, so a
|
|
176
|
+
three-line title lengthens the card rather than pushing anything out
|
|
177
|
+
of it. In a grid, give the row `items-start` so the extra lines hang
|
|
178
|
+
below instead of stretching every sibling. */}
|
|
179
|
+
<h3
|
|
180
|
+
className={cn(
|
|
181
|
+
"mt-2.5 leading-[1.35]",
|
|
182
|
+
emphasis ? "text-lg font-bold tracking-[-0.02em]" : "text-sm font-semibold"
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
185
|
+
{event.title}
|
|
186
|
+
</h3>
|
|
187
|
+
|
|
188
|
+
<div className="mt-0.5 flex min-w-0 items-center gap-1 text-xs font-medium text-foreground/85">
|
|
189
|
+
<MapPin className="h-3 w-3 shrink-0" aria-hidden />
|
|
190
|
+
<span className="truncate">{venue}</span>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{!hideWhen && (
|
|
194
|
+
<div className="mt-[3px] flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
195
|
+
<span>{dayLabel ?? formatters.day(event.starts_at)}</span>
|
|
196
|
+
<span className="text-border" aria-hidden>
|
|
197
|
+
·
|
|
198
|
+
</span>
|
|
199
|
+
<span>{formatters.time(event.starts_at)}</span>
|
|
200
|
+
{showPrice && fee.kind === "paid" && (
|
|
201
|
+
<>
|
|
202
|
+
<span className="text-border" aria-hidden>
|
|
203
|
+
·
|
|
204
|
+
</span>
|
|
205
|
+
<span>{formatters.price(fee.amountRon)}</span>
|
|
206
|
+
</>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
|
|
211
|
+
{tags && tags.length > 0 && (
|
|
212
|
+
<TagGroup className="mt-1.5">
|
|
213
|
+
{tags.map((tag) => (
|
|
214
|
+
<Tag key={tag} size="sm">
|
|
215
|
+
{tag}
|
|
216
|
+
</Tag>
|
|
217
|
+
))}
|
|
218
|
+
</TagGroup>
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
{liveStatus && (
|
|
222
|
+
<div className="mt-1 flex items-center gap-1.5 text-xs font-bold text-success">
|
|
223
|
+
<Radio className="h-3 w-3 shrink-0" aria-hidden />
|
|
224
|
+
{liveStatus}
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
</>
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const rootClasses = cn(
|
|
231
|
+
"flex min-w-0 flex-col text-left",
|
|
232
|
+
(href || onClick) &&
|
|
233
|
+
"cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-xl",
|
|
234
|
+
className
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
if (href) {
|
|
238
|
+
return (
|
|
239
|
+
<LinkComponent
|
|
240
|
+
href={href}
|
|
241
|
+
className={rootClasses}
|
|
242
|
+
onClick={onClick ? (mouseEvent) => onClick(event, mouseEvent) : undefined}
|
|
243
|
+
>
|
|
244
|
+
{body}
|
|
245
|
+
</LinkComponent>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (onClick) {
|
|
250
|
+
return (
|
|
251
|
+
<button type="button" className={rootClasses} onClick={(e) => onClick(event, e)}>
|
|
252
|
+
{body}
|
|
253
|
+
</button>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return <div className={rootClasses}>{body}</div>;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
export type EventCardSkeletonProps = {
|
|
261
|
+
/** Matches `EventCardProps["posterAspect"]` — keep the poster box the same shape. */
|
|
262
|
+
posterAspect?: "4/3" | "3/4" | "4/5";
|
|
263
|
+
/** Matches `EventCardProps["emphasis"]` — the featured slot's taller title. */
|
|
264
|
+
emphasis?: boolean;
|
|
265
|
+
/** Matches `EventCardProps["hideWhen"]` — drops the day/time line placeholder too. */
|
|
266
|
+
hideWhen?: boolean;
|
|
267
|
+
className?: string;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Mirrors `<EventCard>`'s geometry so nothing shifts when data lands — with
|
|
272
|
+
* one honest caveat since the title stopped being clamped: the placeholder
|
|
273
|
+
* reserves two lines (three under `emphasis`), and a real title longer than
|
|
274
|
+
* that will grow the card past the skeleton it replaces. Reserving more would
|
|
275
|
+
* only move the shift to the far commoner short-title case.
|
|
276
|
+
*/
|
|
277
|
+
export function EventCardSkeleton({
|
|
278
|
+
posterAspect = "4/3",
|
|
279
|
+
emphasis,
|
|
280
|
+
hideWhen,
|
|
281
|
+
className,
|
|
282
|
+
}: EventCardSkeletonProps) {
|
|
283
|
+
return (
|
|
284
|
+
<div aria-hidden="true" className={cn("flex min-w-0 flex-col text-left", className)}>
|
|
285
|
+
<Skeleton
|
|
286
|
+
className={cn(
|
|
287
|
+
posterAspect === "3/4"
|
|
288
|
+
? "aspect-[3/4]"
|
|
289
|
+
: posterAspect === "4/5"
|
|
290
|
+
? "aspect-[4/5]"
|
|
291
|
+
: "aspect-[4/3]"
|
|
292
|
+
)}
|
|
293
|
+
/>
|
|
294
|
+
|
|
295
|
+
{emphasis ? (
|
|
296
|
+
<div className="mt-2.5 space-y-2">
|
|
297
|
+
<Skeleton className="h-5 w-full" />
|
|
298
|
+
<Skeleton className="h-5 w-full" />
|
|
299
|
+
<Skeleton className="h-5 w-1/2" />
|
|
300
|
+
</div>
|
|
301
|
+
) : (
|
|
302
|
+
<div className="mt-2.5 space-y-1.5">
|
|
303
|
+
<Skeleton className="h-3.5 w-full" />
|
|
304
|
+
<Skeleton className="h-3.5 w-2/3" />
|
|
305
|
+
</div>
|
|
306
|
+
)}
|
|
307
|
+
|
|
308
|
+
<div className="mt-0.5 flex items-center gap-1">
|
|
309
|
+
<Skeleton className="h-3 w-3 shrink-0 rounded-full" />
|
|
310
|
+
<Skeleton className="h-3 w-24" />
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
{!hideWhen && (
|
|
314
|
+
<div className="mt-[3px] flex items-center gap-1.5">
|
|
315
|
+
<Skeleton className="h-3 w-16" />
|
|
316
|
+
<Skeleton className="h-3 w-10" />
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { EventCompactRow, EventCompactRowSkeleton } from "./event-compact-row";
|
|
3
|
+
import type { EventCardLabels, EventFormatters, EventPreview } from "./event-types";
|
|
4
|
+
|
|
5
|
+
const labels: EventCardLabels = { free: "Free", liveNow: "Live now" };
|
|
6
|
+
|
|
7
|
+
const formatters: EventFormatters = {
|
|
8
|
+
time: (iso) => new Date(iso).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" }),
|
|
9
|
+
day: (iso) =>
|
|
10
|
+
new Date(iso).toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short" }),
|
|
11
|
+
price: (amount) => `${amount} RON`,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const event = (id: string, title: string, place: string, startsAt: string, fee?: number): EventPreview => ({
|
|
15
|
+
id,
|
|
16
|
+
title,
|
|
17
|
+
location: place,
|
|
18
|
+
place_name: place,
|
|
19
|
+
starts_at: startsAt,
|
|
20
|
+
entry_fee: fee,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const meta: Meta<typeof EventCompactRow> = {
|
|
24
|
+
title: "Events/EventCompactRow",
|
|
25
|
+
component: EventCompactRow,
|
|
26
|
+
tags: ["autodocs"],
|
|
27
|
+
args: { labels, formatters },
|
|
28
|
+
decorators: [
|
|
29
|
+
(Story) => (
|
|
30
|
+
<div className="w-[358px]">
|
|
31
|
+
<Story />
|
|
32
|
+
</div>
|
|
33
|
+
),
|
|
34
|
+
],
|
|
35
|
+
parameters: {
|
|
36
|
+
docs: {
|
|
37
|
+
description: {
|
|
38
|
+
component:
|
|
39
|
+
"The dense row used inside `<HourGroup>` once the night thins out. Unlike `<EventListRow>`, it carries neither a date badge nor a trailing CTA — the timeline's hour heading already names the hour, and every row here means 'open me'.",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export default meta;
|
|
46
|
+
type Story = StoryObj<typeof EventCompactRow>;
|
|
47
|
+
|
|
48
|
+
export const Default: Story = {
|
|
49
|
+
args: { event: event("1", "Psihotrop Live", "Atelier Café", "2026-09-05T20:00:00", 50) },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const FreeEntry: Story = {
|
|
53
|
+
args: { event: event("2", "Vinyl Happy Hour", "Zorki Photo Cafe", "2026-09-04T18:00:00", 0) },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* No poster — the gradient fallback, which this row did not have until the
|
|
58
|
+
* thumbnail grew. A blank `bg-muted` box passed for "no image" at 34px; at
|
|
59
|
+
* 64x80 beside real posters it reads as broken.
|
|
60
|
+
*/
|
|
61
|
+
export const NoPoster: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
event: {
|
|
64
|
+
...event("np", "Loops & Books at The Square One", "The Square One", "2026-09-04T18:30:00"),
|
|
65
|
+
poster_url: undefined,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The hover wash, which cannot be asserted in a test and has to be looked at.
|
|
72
|
+
* Point at a row: the whole row washes and the title takes `--primary`.
|
|
73
|
+
*
|
|
74
|
+
* Both surfaces are shown because the treatment had to work on each. The row
|
|
75
|
+
* renders straight on `--background` in the timeline and inside an elevated
|
|
76
|
+
* `Card` in the hub's venue block — a wash keyed to a background token is a
|
|
77
|
+
* no-op on one of the two, which is why this one is keyed to the text colour.
|
|
78
|
+
*/
|
|
79
|
+
export const HoverOnBothSurfaces: Story = {
|
|
80
|
+
render: (args) => (
|
|
81
|
+
<div className="space-y-4">
|
|
82
|
+
<div className="rounded-xl bg-background p-3">
|
|
83
|
+
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
|
|
84
|
+
on --background
|
|
85
|
+
</p>
|
|
86
|
+
<EventCompactRow {...args} href="#a" />
|
|
87
|
+
<EventCompactRow {...args} event={event("h2", "Frat House Party with DJ M4RA", "Flying Circus", "2026-09-04T23:00:00")} href="#b" />
|
|
88
|
+
</div>
|
|
89
|
+
<div className="rounded-xl border border-border bg-card p-3">
|
|
90
|
+
<p className="mb-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
|
|
91
|
+
on --card
|
|
92
|
+
</p>
|
|
93
|
+
<EventCompactRow {...args} href="#c" />
|
|
94
|
+
<EventCompactRow {...args} event={event("h4", "Horticulture Workshop at DOT", "DOT", "2026-09-04T19:00:00")} href="#d" />
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
),
|
|
98
|
+
args: { event: event("h1", "Last Open Air with Dominik Muélar", "Iconic Garden", "2026-09-04T18:00:00") },
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** `hideTime` drops the leading time — for when the enclosing `HourGroup` already names it. */
|
|
102
|
+
export const HiddenTime: Story = {
|
|
103
|
+
args: {
|
|
104
|
+
event: event("3", "Quiz Night", "Flying Circus", "2026-09-03T19:00:00"),
|
|
105
|
+
hideTime: true,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A long title WRAPS — the row grows taller rather than cutting the event's
|
|
111
|
+
* name off. This story used to demonstrate the opposite; the single truncated
|
|
112
|
+
* line was the reported defect, on this row and every sibling.
|
|
113
|
+
*/
|
|
114
|
+
export const LongTitle: Story = {
|
|
115
|
+
args: {
|
|
116
|
+
event: event(
|
|
117
|
+
"4",
|
|
118
|
+
"Stand-up Comedy with Dobrotă, Cîrje & Florin — The Late Show, second edition",
|
|
119
|
+
"Klausen Pubhouse",
|
|
120
|
+
"2026-09-04T19:30:00",
|
|
121
|
+
45
|
|
122
|
+
),
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/** The mobile variant — a slightly larger poster and chip. */
|
|
127
|
+
export const Small: Story = {
|
|
128
|
+
args: {
|
|
129
|
+
size: "sm",
|
|
130
|
+
event: event("5", "Beer Battle", "Irish Music Pub", "2026-09-03T20:00:00", 0),
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `subtitle` replaces the default "time · venue" sub-line entirely — the hub's
|
|
136
|
+
* venue block case, where the block's header already names the venue and the
|
|
137
|
+
* event's description is what actually distinguishes rows.
|
|
138
|
+
*/
|
|
139
|
+
export const WithSubtitle: Story = {
|
|
140
|
+
args: {
|
|
141
|
+
event: event("6", "Karaoke Thursdays", "Yolka", "2026-09-03T21:00:00"),
|
|
142
|
+
subtitle: "Sing your heart out with the house band backing every song.",
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/** Default sub-line beside the `subtitle` override, so the difference is visible. */
|
|
147
|
+
export const DefaultVsSubtitle: Story = {
|
|
148
|
+
render: () => (
|
|
149
|
+
<div>
|
|
150
|
+
<EventCompactRow
|
|
151
|
+
event={event("6a", "Karaoke Thursdays", "Yolka", "2026-09-03T21:00:00")}
|
|
152
|
+
labels={labels}
|
|
153
|
+
formatters={formatters}
|
|
154
|
+
/>
|
|
155
|
+
<EventCompactRow
|
|
156
|
+
event={event("6b", "Karaoke Thursdays", "Yolka", "2026-09-03T21:00:00")}
|
|
157
|
+
labels={labels}
|
|
158
|
+
formatters={formatters}
|
|
159
|
+
subtitle="Sing your heart out with the house band backing every song."
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** Tag chips below the sub-line — labels the caller derives from `event.tags`. */
|
|
166
|
+
export const WithTags: Story = {
|
|
167
|
+
args: {
|
|
168
|
+
event: event("7", "Psihotrop Live", "Atelier Café", "2026-09-05T20:00:00", 50),
|
|
169
|
+
tags: ["Live Music", "Rock"],
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/** Loading state — identical geometry to `Default`, so nothing shifts on data arrival. */
|
|
174
|
+
export const Loading: Story = {
|
|
175
|
+
render: () => <EventCompactRowSkeleton />,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/** The `size="sm"` loading geometry. */
|
|
179
|
+
export const LoadingSmall: Story = {
|
|
180
|
+
render: () => <EventCompactRowSkeleton size="sm" />,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** A loading row above a loaded one, so any geometry drift is visible at a glance. */
|
|
184
|
+
export const LoadingVsLoaded: Story = {
|
|
185
|
+
render: () => (
|
|
186
|
+
<div>
|
|
187
|
+
<EventCompactRowSkeleton />
|
|
188
|
+
<EventCompactRow
|
|
189
|
+
event={event("1", "Psihotrop Live", "Atelier Café", "2026-09-05T20:00:00", 50)}
|
|
190
|
+
labels={labels}
|
|
191
|
+
formatters={formatters}
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/** A run of rows as they stack inside `HourGroup`. */
|
|
198
|
+
export const AStretch: Story = {
|
|
199
|
+
render: () => (
|
|
200
|
+
<div>
|
|
201
|
+
<EventCompactRow
|
|
202
|
+
event={event("a", "Psihotrop Live", "Atelier Café", "2026-09-03T20:00:00", 50)}
|
|
203
|
+
labels={labels}
|
|
204
|
+
formatters={formatters}
|
|
205
|
+
hideTime
|
|
206
|
+
/>
|
|
207
|
+
<EventCompactRow
|
|
208
|
+
event={event("b", "Quiz Night", "Flying Circus", "2026-09-03T20:00:00")}
|
|
209
|
+
labels={labels}
|
|
210
|
+
formatters={formatters}
|
|
211
|
+
hideTime
|
|
212
|
+
/>
|
|
213
|
+
<EventCompactRow
|
|
214
|
+
event={event("c", "Beer Battle", "Irish Music Pub", "2026-09-03T20:00:00", 0)}
|
|
215
|
+
labels={labels}
|
|
216
|
+
formatters={formatters}
|
|
217
|
+
hideTime
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
),
|
|
221
|
+
};
|