@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,95 @@
|
|
|
1
|
+
import type { ElementType } from "react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { Separator } from "./separator";
|
|
4
|
+
import { Skeleton } from "./skeleton";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* EventDaySeparator — the day divider in the list view (as opposed to
|
|
8
|
+
* `<EventDayHeader>`, the timeline's oversized day-number header — the two
|
|
9
|
+
* live on different surfaces with different densities, not a size variant of
|
|
10
|
+
* one component).
|
|
11
|
+
*
|
|
12
|
+
* Both `label` and `count` are caller-composed strings, same reasoning as
|
|
13
|
+
* `<EventDayHeader>`'s `meta`: "Today · Thursday 3 September" / "15 events"
|
|
14
|
+
* don't decompose into parts without baking in English clause order.
|
|
15
|
+
*
|
|
16
|
+
* Renders the label as a real heading (`as`, default `h2`) so a list of these
|
|
17
|
+
* gives the page a document outline instead of being purely decorative rules.
|
|
18
|
+
*
|
|
19
|
+
* NOTE ON WEIGHT: this heading is deliberately SMALLER and lighter than the
|
|
20
|
+
* row titles beneath it. The boundary is already the strongest structural
|
|
21
|
+
* event on the page — a rule to the right edge, 24px of air above it, a muted
|
|
22
|
+
* count beside it — so it needs no ink on top of that. And the day is chrome
|
|
23
|
+
* on a scan surface where the events are the content: when the chrome is the
|
|
24
|
+
* heaviest thing on screen, the eye lands on the same repeated word every
|
|
25
|
+
* eight rows and nothing in the actual inventory ever wins. If the boundary
|
|
26
|
+
* ever needs to read louder, the levers are the rule and the air (move the
|
|
27
|
+
* `<Separator>` above the label), not the weight.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export type EventDaySeparatorProps = {
|
|
31
|
+
label: string;
|
|
32
|
+
count?: string;
|
|
33
|
+
size?: "sm" | "default";
|
|
34
|
+
/** Heading level for `label`. Defaults to `h2`. */
|
|
35
|
+
as?: ElementType;
|
|
36
|
+
className?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function EventDaySeparator({
|
|
40
|
+
label,
|
|
41
|
+
count,
|
|
42
|
+
size = "default",
|
|
43
|
+
as: Tag = "h2",
|
|
44
|
+
className,
|
|
45
|
+
}: EventDaySeparatorProps) {
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
className={cn(
|
|
49
|
+
"flex items-center gap-[11px]",
|
|
50
|
+
size === "sm" ? "pt-[18px] pb-1" : "pt-6 pb-2",
|
|
51
|
+
className
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
<Tag className={cn("font-semibold", size === "sm" ? "text-[13px]" : "text-sm")}>
|
|
55
|
+
{label}
|
|
56
|
+
</Tag>
|
|
57
|
+
|
|
58
|
+
{count && (
|
|
59
|
+
<span className={cn("text-muted-foreground", size === "sm" ? "text-[11.5px]" : "text-xs")}>
|
|
60
|
+
{count}
|
|
61
|
+
</span>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
<Separator className="flex-1" />
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type EventDaySeparatorSkeletonProps = {
|
|
70
|
+
/** Matches `EventDaySeparatorProps["size"]`. */
|
|
71
|
+
size?: "sm" | "default";
|
|
72
|
+
/** Shows the count placeholder — omit for label-only separators. */
|
|
73
|
+
showCount?: boolean;
|
|
74
|
+
className?: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Mirrors `<EventDaySeparator>`'s geometry exactly so nothing shifts when data lands. */
|
|
78
|
+
export function EventDaySeparatorSkeleton({
|
|
79
|
+
size = "default",
|
|
80
|
+
showCount = true,
|
|
81
|
+
className,
|
|
82
|
+
}: EventDaySeparatorSkeletonProps) {
|
|
83
|
+
const isSm = size === "sm";
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
aria-hidden="true"
|
|
88
|
+
className={cn("flex items-center gap-[11px]", isSm ? "pt-[18px] pb-1" : "pt-6 pb-2", className)}
|
|
89
|
+
>
|
|
90
|
+
<Skeleton className={cn(isSm ? "h-3" : "h-3.5", "w-40")} />
|
|
91
|
+
{showCount && <Skeleton className="h-3 w-16" />}
|
|
92
|
+
<Separator className="flex-1" />
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { ExternalLink, Navigation, Phone, Share2, X } from "lucide-react";
|
|
3
|
+
import {
|
|
4
|
+
EventDetailPanel,
|
|
5
|
+
EventDetailPanelSkeleton,
|
|
6
|
+
type EventDetailPanelLabels,
|
|
7
|
+
} from "./event-detail-panel";
|
|
8
|
+
import type { EventFormatters, EventPreview } from "./event-types";
|
|
9
|
+
|
|
10
|
+
const labels: EventDetailPanelLabels = {
|
|
11
|
+
free: "Free",
|
|
12
|
+
liveNow: "Live now",
|
|
13
|
+
venueHeading: "Event venue",
|
|
14
|
+
aboutHeading: "About this event",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const formatters: EventFormatters = {
|
|
18
|
+
time: () => "20:00",
|
|
19
|
+
day: () => "Sat, Sep 5",
|
|
20
|
+
price: (amount) => `${amount} RON`,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const event: EventPreview = {
|
|
24
|
+
id: "e1",
|
|
25
|
+
title: "Psihotrop Live",
|
|
26
|
+
description:
|
|
27
|
+
"A full live set spanning the band's whole catalogue, with support from Mara Ionescu. Doors at 19:30.",
|
|
28
|
+
location: "Atelier Café",
|
|
29
|
+
place_name: "Atelier Café",
|
|
30
|
+
starts_at: "2026-09-05T20:00:00",
|
|
31
|
+
entry_fee: 50,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const venue = {
|
|
35
|
+
name: "Atelier Café",
|
|
36
|
+
meta: "Str. Iuliu Maniu 5 · open until 01:00",
|
|
37
|
+
ratingLabel: "4.7",
|
|
38
|
+
href: "/places/atelier-cafe",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const roundBtn =
|
|
42
|
+
"flex h-9 w-9 items-center justify-center rounded-full bg-white/90 text-foreground";
|
|
43
|
+
|
|
44
|
+
const headerActions = (
|
|
45
|
+
<>
|
|
46
|
+
<button type="button" aria-label="Share" className={`absolute left-3.5 top-3.5 ${roundBtn}`}>
|
|
47
|
+
<Share2 className="h-4 w-4" aria-hidden />
|
|
48
|
+
</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
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
className="flex h-[46px] items-center gap-2 rounded-xl border border-border px-4 text-sm font-bold"
|
|
67
|
+
>
|
|
68
|
+
<Navigation className="h-4 w-4" aria-hidden />
|
|
69
|
+
Directions
|
|
70
|
+
</button>
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const externalLink = (
|
|
75
|
+
<a href="#" className="flex items-center gap-1.5 text-[13.5px] font-bold text-primary">
|
|
76
|
+
<ExternalLink className="h-3.5 w-3.5" aria-hidden />
|
|
77
|
+
View on Instagram
|
|
78
|
+
</a>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const meta: Meta<typeof EventDetailPanel> = {
|
|
82
|
+
title: "Events/EventDetailPanel",
|
|
83
|
+
component: EventDetailPanel,
|
|
84
|
+
tags: ["autodocs"],
|
|
85
|
+
args: {
|
|
86
|
+
event,
|
|
87
|
+
labels,
|
|
88
|
+
formatters,
|
|
89
|
+
dateLine: "Saturday, September 5 · Starts at 20:00",
|
|
90
|
+
venue,
|
|
91
|
+
tags: ["Live music"],
|
|
92
|
+
headerActions,
|
|
93
|
+
footer,
|
|
94
|
+
externalLink,
|
|
95
|
+
},
|
|
96
|
+
parameters: {
|
|
97
|
+
docs: {
|
|
98
|
+
description: {
|
|
99
|
+
component:
|
|
100
|
+
"The BODY of the detail sheet, not a sheet itself — the DS already ships `Sheet` and `Drawer`, and the app mounts this inside the side sheet on desktop and the bottom sheet on mobile. This is the terminal surface for acting on an event, which is why the cards that open it carry no actions of their own.",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export default meta;
|
|
107
|
+
type Story = StoryObj<typeof EventDetailPanel>;
|
|
108
|
+
|
|
109
|
+
/** Desktop side sheet — 460px wide, 4:5 poster. */
|
|
110
|
+
export const SideSheet: Story = {
|
|
111
|
+
decorators: [
|
|
112
|
+
(Story) => (
|
|
113
|
+
<div className="h-[820px] w-[460px] overflow-hidden rounded-[18px] shadow-2xl">
|
|
114
|
+
<Story />
|
|
115
|
+
</div>
|
|
116
|
+
),
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** Mobile bottom sheet — compact size, same 4:5 poster, tighter type. The hero scrolls away with the body; only the footer stays put. */
|
|
121
|
+
export const BottomSheet: Story = {
|
|
122
|
+
args: { size: "compact" },
|
|
123
|
+
decorators: [
|
|
124
|
+
(Story) => (
|
|
125
|
+
<div className="h-[720px] w-full max-w-[390px] overflow-hidden rounded-t-[20px] shadow-2xl">
|
|
126
|
+
<div className="mx-auto mt-2.5 mb-1 h-1 w-[38px] rounded-full bg-border" />
|
|
127
|
+
<Story />
|
|
128
|
+
</div>
|
|
129
|
+
),
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** Live events get the on-air chip beside the category and price. */
|
|
134
|
+
export const LiveNow: Story = {
|
|
135
|
+
args: { isLive: true },
|
|
136
|
+
decorators: [
|
|
137
|
+
(Story) => (
|
|
138
|
+
<div className="h-[820px] w-[460px] overflow-hidden rounded-[18px] shadow-2xl">
|
|
139
|
+
<Story />
|
|
140
|
+
</div>
|
|
141
|
+
),
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** A free event: `entry_fee: 0` renders the free chip instead of a price. */
|
|
146
|
+
export const FreeEntry: Story = {
|
|
147
|
+
args: { event: { ...event, entry_fee: 0 } },
|
|
148
|
+
decorators: [
|
|
149
|
+
(Story) => (
|
|
150
|
+
<div className="h-[820px] w-[460px] overflow-hidden rounded-[18px] shadow-2xl">
|
|
151
|
+
<Story />
|
|
152
|
+
</div>
|
|
153
|
+
),
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** No description and no venue — the sections drop out rather than render empty headings. */
|
|
158
|
+
export const Sparse: Story = {
|
|
159
|
+
args: {
|
|
160
|
+
event: { ...event, description: undefined, entry_fee: undefined },
|
|
161
|
+
venue: undefined,
|
|
162
|
+
tags: undefined,
|
|
163
|
+
externalLink: undefined,
|
|
164
|
+
},
|
|
165
|
+
decorators: [
|
|
166
|
+
(Story) => (
|
|
167
|
+
<div className="h-[560px] w-[460px] overflow-hidden rounded-[18px] shadow-2xl">
|
|
168
|
+
<Story />
|
|
169
|
+
</div>
|
|
170
|
+
),
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/** Desktop side sheet loading state. */
|
|
175
|
+
export const Skeleton: Story = {
|
|
176
|
+
render: () => (
|
|
177
|
+
<div className="h-[820px] w-[460px] overflow-hidden rounded-[18px] shadow-2xl">
|
|
178
|
+
<EventDetailPanelSkeleton />
|
|
179
|
+
</div>
|
|
180
|
+
),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** Mobile bottom sheet loading state — compact size, same 4:5 poster. */
|
|
184
|
+
export const SkeletonCompact: Story = {
|
|
185
|
+
render: () => (
|
|
186
|
+
<div className="h-[720px] w-full max-w-[390px] overflow-hidden rounded-t-[20px] shadow-2xl">
|
|
187
|
+
<div className="mx-auto mt-2.5 mb-1 h-1 w-[38px] rounded-full bg-border" />
|
|
188
|
+
<EventDetailPanelSkeleton size="compact" />
|
|
189
|
+
</div>
|
|
190
|
+
),
|
|
191
|
+
};
|
|
@@ -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
|
+
}
|