@bearmenu/ui 0.8.4 → 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-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/item.d.ts +2 -2
- 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/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/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 +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/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,180 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ChevronRight } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
import { Skeleton } from "./skeleton";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* EventDayHeader — the day divider in the timeline: an oversized day number,
|
|
10
|
+
* the weekday, and a meta line.
|
|
11
|
+
*
|
|
12
|
+
* The meta line is ONE caller-composed string. The design's variants
|
|
13
|
+
* ("September · 15 events · 3 live", "September · nothing listed yet",
|
|
14
|
+
* "September · 2 of 23 are free") do not decompose into counts, and building
|
|
15
|
+
* them from parts inside the DS would bake in English clause order.
|
|
16
|
+
*
|
|
17
|
+
* A day with nothing on still gets a header — the empty state belongs under it,
|
|
18
|
+
* not instead of it, so the reader can see the date they are looking at.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export type EventDayHeaderLinkProps = {
|
|
22
|
+
href: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function DefaultLink({ href, ...props }: EventDayHeaderLinkProps) {
|
|
28
|
+
return <a href={href} {...props} />;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type EventDayHeaderProps = {
|
|
32
|
+
/** Day of month as displayed, e.g. "03". */
|
|
33
|
+
dayNumber: string;
|
|
34
|
+
/** Weekday name, already localised. */
|
|
35
|
+
weekday: string;
|
|
36
|
+
/** Month plus counts, composed by the caller. */
|
|
37
|
+
meta: string;
|
|
38
|
+
/**
|
|
39
|
+
* Trailing slot, right-aligned. The desktop design puts a "See the whole day →"
|
|
40
|
+
* chip here; mobile has no slot and falls back to the chevron below. Passing an
|
|
41
|
+
* action REPLACES the chevron rather than sitting beside it — two affordances
|
|
42
|
+
* for one destination is the duplication this design set out to remove.
|
|
43
|
+
*
|
|
44
|
+
* The action is rendered outside the header's own link/button when the header
|
|
45
|
+
* is itself interactive, so an interactive chip is never nested inside an
|
|
46
|
+
* anchor (invalid HTML, and it steals the row's click target).
|
|
47
|
+
*/
|
|
48
|
+
action?: React.ReactNode;
|
|
49
|
+
/**
|
|
50
|
+
* Larger desktop scale: 58px day number, 18px weekday, 12.5px meta,
|
|
51
|
+
* against the mobile 40/15/11.5.
|
|
52
|
+
*/
|
|
53
|
+
size?: "sm" | "default";
|
|
54
|
+
href?: string;
|
|
55
|
+
linkComponent?: React.ComponentType<EventDayHeaderLinkProps>;
|
|
56
|
+
onClick?: () => void;
|
|
57
|
+
className?: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function EventDayHeader({
|
|
61
|
+
dayNumber,
|
|
62
|
+
weekday,
|
|
63
|
+
meta,
|
|
64
|
+
action,
|
|
65
|
+
size = "sm",
|
|
66
|
+
href,
|
|
67
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
68
|
+
onClick,
|
|
69
|
+
className,
|
|
70
|
+
}: EventDayHeaderProps) {
|
|
71
|
+
const isInteractive = !!href || !!onClick;
|
|
72
|
+
const large = size === "default";
|
|
73
|
+
|
|
74
|
+
const body = (
|
|
75
|
+
<>
|
|
76
|
+
<div
|
|
77
|
+
className={cn(
|
|
78
|
+
"font-black leading-[0.82] tracking-[-0.05em] tabular-nums",
|
|
79
|
+
large ? "text-[58px]" : "text-[40px]"
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{dayNumber}
|
|
83
|
+
</div>
|
|
84
|
+
<div className={large ? "pb-1" : "pb-0.5"}>
|
|
85
|
+
<div
|
|
86
|
+
className={cn("font-extrabold tracking-[-0.015em]", large ? "text-lg" : "text-[15px]")}
|
|
87
|
+
>
|
|
88
|
+
{weekday}
|
|
89
|
+
</div>
|
|
90
|
+
<div
|
|
91
|
+
className={cn(
|
|
92
|
+
"mt-0.5 text-muted-foreground",
|
|
93
|
+
large ? "whitespace-nowrap text-[12.5px]" : "text-[11.5px]"
|
|
94
|
+
)}
|
|
95
|
+
>
|
|
96
|
+
{meta}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
{/* The chevron only stands in when there is no action slot — see `action`. */}
|
|
100
|
+
{!action && isInteractive && (
|
|
101
|
+
<ChevronRight className="ml-auto mb-1 h-[18px] w-[18px] text-muted-foreground" aria-hidden />
|
|
102
|
+
)}
|
|
103
|
+
</>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// The interactive region covers the date only, never the action slot. Nesting
|
|
107
|
+
// the design's "See the whole day" chip inside the header's own anchor would
|
|
108
|
+
// be invalid HTML and would swallow the chip's click.
|
|
109
|
+
const dateClasses = cn(
|
|
110
|
+
"flex min-w-0 items-end gap-3 text-left",
|
|
111
|
+
!action && "flex-1",
|
|
112
|
+
isInteractive &&
|
|
113
|
+
"cursor-pointer rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const date = href ? (
|
|
117
|
+
<LinkComponent href={href} className={dateClasses}>
|
|
118
|
+
{body}
|
|
119
|
+
</LinkComponent>
|
|
120
|
+
) : onClick ? (
|
|
121
|
+
<button type="button" className={dateClasses} onClick={onClick}>
|
|
122
|
+
{body}
|
|
123
|
+
</button>
|
|
124
|
+
) : (
|
|
125
|
+
<div className={dateClasses}>{body}</div>
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
className={cn(
|
|
131
|
+
"relative flex w-full items-end gap-3 pb-3 pt-[18px]",
|
|
132
|
+
large && "gap-[18px] pb-4",
|
|
133
|
+
className
|
|
134
|
+
)}
|
|
135
|
+
>
|
|
136
|
+
{/* NO BOTTOM RULE. A 50px day number above its own month/count line is
|
|
137
|
+
already the loudest thing on the page, and the timeline puts an hour
|
|
138
|
+
puck carrying its own hairline immediately beneath — so this drew a
|
|
139
|
+
second horizontal line a few pixels from a first, dividing a heading
|
|
140
|
+
from the content it introduces rather than separating two things a
|
|
141
|
+
reader could run together. Rules stay where they divide peers: rows
|
|
142
|
+
in a list, an hour heading from its count. */}
|
|
143
|
+
{date}
|
|
144
|
+
{action && <div className="ml-auto mb-1.5 shrink-0">{action}</div>}
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export type EventDayHeaderSkeletonProps = {
|
|
150
|
+
/** Reserves space for the chevron — only render this once you know the header will be tappable. */
|
|
151
|
+
interactive?: boolean;
|
|
152
|
+
className?: string;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Mirrors `<EventDayHeader>`'s geometry. The big day-number block (86×50,
|
|
157
|
+
* 10px radius) and the weekday/meta column (120×16, 200×12) come from the
|
|
158
|
+
* source design's own desktop day-header skeleton — the chevron does not: the
|
|
159
|
+
* design's spec for it (a 170×34 pill pushed right) matches nothing in this
|
|
160
|
+
* component, whose only trailing affordance is an 18×18 chevron icon, so that
|
|
161
|
+
* placeholder is sized from the real component instead.
|
|
162
|
+
*/
|
|
163
|
+
export function EventDayHeaderSkeleton({ interactive, className }: EventDayHeaderSkeletonProps) {
|
|
164
|
+
return (
|
|
165
|
+
<div
|
|
166
|
+
aria-hidden="true"
|
|
167
|
+
className={cn("relative flex w-full items-end gap-3 pb-3 pt-[18px] text-left", className)}
|
|
168
|
+
>
|
|
169
|
+
{/* Mirrors the real header, which no longer draws a bottom rule. */}
|
|
170
|
+
<Skeleton className="h-[50px] w-[86px] rounded-[10px]" />
|
|
171
|
+
|
|
172
|
+
<div className="space-y-0.5 pb-0.5">
|
|
173
|
+
<Skeleton className="h-4 w-[120px]" />
|
|
174
|
+
<Skeleton className="h-3 w-[200px]" />
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
{interactive && <Skeleton className="mb-1 ml-auto h-[18px] w-[18px] shrink-0 rounded-full" />}
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { EventDaySeparator, EventDaySeparatorSkeleton } from "./event-day-separator";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof EventDaySeparator> = {
|
|
5
|
+
title: "Events/EventDaySeparator",
|
|
6
|
+
component: EventDaySeparator,
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
decorators: [
|
|
9
|
+
(Story) => (
|
|
10
|
+
<div className="w-[600px]">
|
|
11
|
+
<Story />
|
|
12
|
+
</div>
|
|
13
|
+
),
|
|
14
|
+
],
|
|
15
|
+
parameters: {
|
|
16
|
+
docs: {
|
|
17
|
+
description: {
|
|
18
|
+
component:
|
|
19
|
+
"The day divider in the list view. Distinct from `<EventDayHeader>` (the timeline's oversized day-number header) — different surface, different density. Both `label` and `count` are caller-composed strings.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
type Story = StoryObj<typeof EventDaySeparator>;
|
|
27
|
+
|
|
28
|
+
export const Default: Story = {
|
|
29
|
+
args: { label: "Today · Thursday 3 September", count: "15 events" },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const NoCount: Story = {
|
|
33
|
+
args: { label: "Friday 4 September" },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const Small: Story = {
|
|
37
|
+
args: { label: "Today · Thursday 3 September", count: "15 events", size: "sm" },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Loading state — identical geometry to `Default`, so nothing shifts on data arrival. */
|
|
41
|
+
export const Loading: Story = {
|
|
42
|
+
render: () => <EventDaySeparatorSkeleton />,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** The `size="sm"` loading geometry. */
|
|
46
|
+
export const LoadingSmall: Story = {
|
|
47
|
+
render: () => <EventDaySeparatorSkeleton size="sm" />,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Loading above loaded, so any geometry drift is visible at a glance. */
|
|
51
|
+
export const LoadingVsLoaded: Story = {
|
|
52
|
+
render: () => (
|
|
53
|
+
<div>
|
|
54
|
+
<EventDaySeparatorSkeleton />
|
|
55
|
+
<EventDaySeparator label="Today · Thursday 3 September" count="15 events" />
|
|
56
|
+
</div>
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** A stack, as the list view actually renders consecutive days. */
|
|
61
|
+
export const Stack: Story = {
|
|
62
|
+
render: () => (
|
|
63
|
+
<div>
|
|
64
|
+
<EventDaySeparator label="Today · Thursday 3 September" count="15 events" />
|
|
65
|
+
<div className="py-2 text-sm text-muted-foreground">…events for today…</div>
|
|
66
|
+
<EventDaySeparator label="Friday 4 September" count="17 events" />
|
|
67
|
+
<div className="py-2 text-sm text-muted-foreground">…events for tomorrow…</div>
|
|
68
|
+
</div>
|
|
69
|
+
),
|
|
70
|
+
};
|
|
@@ -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
|
+
};
|