@bearmenu/ui 0.8.17 → 0.8.19

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.
Files changed (59) hide show
  1. package/dist/{chunk-WYTJR2YU.js → chunk-5CPBSNWQ.js} +6 -1
  2. package/dist/chunk-BRXI6EDO.js +84 -0
  3. package/dist/{chunk-54FQQZZJ.js → chunk-EQYB4Q77.js} +1 -1
  4. package/dist/chunk-QRSIJC4X.js +156 -0
  5. package/dist/components/concept-compare-rail.js +2 -2
  6. package/dist/components/concept-compare.js +2 -2
  7. package/dist/components/event-card.d.ts +22 -4
  8. package/dist/components/event-card.js +95 -50
  9. package/dist/components/event-day-header.d.ts +5 -3
  10. package/dist/components/event-day-header.js +12 -4
  11. package/dist/components/event-hub-hero.d.ts +12 -2
  12. package/dist/components/event-hub-hero.js +11 -5
  13. package/dist/components/event-tonight-strip.d.ts +50 -0
  14. package/dist/components/event-tonight-strip.js +67 -0
  15. package/dist/components/menu-family-block.js +4 -3
  16. package/dist/components/menu-hub-hero.d.ts +7 -1
  17. package/dist/components/menu-hub-hero.js +2 -1
  18. package/dist/components/poster-stack.d.ts +36 -3
  19. package/dist/components/poster-stack.js +87 -48
  20. package/dist/components/snap-rail.d.ts +2 -2
  21. package/dist/components/snap-rail.js +1 -1
  22. package/dist/components/timeline-day.d.ts +54 -0
  23. package/dist/components/timeline-day.js +65 -0
  24. package/dist/components/timeline-hour-puck.d.ts +47 -0
  25. package/dist/components/timeline-hour-puck.js +3 -0
  26. package/dist/components/timeline-hour.d.ts +96 -0
  27. package/dist/components/{timeline-band.js → timeline-hour.js} +2 -1
  28. package/dist/components/timeline-playhead.js +1 -3
  29. package/package.json +1 -1
  30. package/src/components/event-card.tsx +70 -13
  31. package/src/components/event-day-header.tsx +19 -7
  32. package/src/components/event-hub-hero.tsx +25 -3
  33. package/src/components/event-tonight-strip.stories.tsx +78 -0
  34. package/src/components/event-tonight-strip.test.tsx +49 -0
  35. package/src/components/event-tonight-strip.tsx +134 -0
  36. package/src/components/menu-family-block.tsx +13 -6
  37. package/src/components/menu-hub-hero.test.tsx +13 -0
  38. package/src/components/menu-hub-hero.tsx +9 -2
  39. package/src/components/poster-stack.stories.tsx +43 -1
  40. package/src/components/poster-stack.tsx +104 -19
  41. package/src/components/snap-rail.tsx +6 -1
  42. package/src/components/timeline-day.stories.tsx +201 -0
  43. package/src/components/timeline-day.test.tsx +78 -0
  44. package/src/components/timeline-day.tsx +114 -0
  45. package/src/components/timeline-hour-puck.tsx +142 -0
  46. package/src/components/timeline-hour.tsx +266 -0
  47. package/src/components/timeline-playhead.tsx +1 -1
  48. package/dist/chunk-NSNEANOQ.js +0 -104
  49. package/dist/components/hour-band.d.ts +0 -66
  50. package/dist/components/hour-band.js +0 -81
  51. package/dist/components/hour-group.d.ts +0 -67
  52. package/dist/components/hour-group.js +0 -59
  53. package/dist/components/timeline-band.d.ts +0 -64
  54. package/src/components/hour-band.stories.tsx +0 -182
  55. package/src/components/hour-band.tsx +0 -146
  56. package/src/components/hour-group.stories.tsx +0 -100
  57. package/src/components/hour-group.tsx +0 -124
  58. package/src/components/timeline-band.stories.tsx +0 -206
  59. package/src/components/timeline-band.tsx +0 -247
@@ -1,206 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react";
2
- import { ChevronDown, TimerOff } from "lucide-react";
3
- import { TimelineBand, TimelineBandSkeleton } from "./timeline-band";
4
- import { HourGroup } from "./hour-group";
5
- import { EventCompactRow } from "./event-compact-row";
6
- import { EventCard } from "./event-card";
7
- import type { EventCardLabels, EventFormatters, EventPreview } from "./event-types";
8
-
9
- const labels: EventCardLabels = { free: "Free", liveNow: "Live now" };
10
-
11
- const formatters: EventFormatters = {
12
- time: () => "20:00",
13
- day: () => "Thu, Sep 3",
14
- price: (amount) => `${amount} RON`,
15
- };
16
-
17
- const event = (id: string, title: string, place: string, fee?: number): EventPreview => ({
18
- id,
19
- title,
20
- location: place,
21
- place_name: place,
22
- starts_at: "2026-09-03T20:00:00",
23
- entry_fee: fee,
24
- });
25
-
26
- const meta: Meta<typeof TimelineBand> = {
27
- title: "Events/TimelineBand",
28
- component: TimelineBand,
29
- tags: ["autodocs"],
30
- decorators: [
31
- (Story) => (
32
- <div className="w-[1180px] bg-background">
33
- <Story />
34
- </div>
35
- ),
36
- ],
37
- parameters: {
38
- docs: {
39
- description: {
40
- component:
41
- "The desktop timeline's three-column row — lane, spine, content — and the desktop counterpart of `<HourBand>`. The band draws the geometry and the hairline only: every hour on the axis is a puck emitted by its own `<HourGroup spine>`, so an hour appears exactly once and always on the spine. Pass an em dash as a group's `time` for a quiet day, so it still reads as a point on the axis rather than a gap in it.",
42
- },
43
- },
44
- },
45
- };
46
-
47
- export default meta;
48
- type Story = StoryObj<typeof TimelineBand>;
49
-
50
- /**
51
- * THE FULL COMPOSITION, as the source design builds it. Density falls off left
52
- * to right and top to bottom: one feature poster in the lane, then a grid of
53
- * cards for the band's own hour, then compact rows for each later hour.
54
- *
55
- * EVERY hour is a station: each `HourGroup` carries `spine`, so 20:00, 21:00
56
- * and 22:00 all sit on the rail in the same grammar, each centred on its own
57
- * heading rule. The opening group passes `hideTime` implicitly via `spine` and
58
- * holds cards rather than rows — that is what the density ladder is, not a
59
- * difference in how the hour is drawn.
60
- */
61
- export const FullBand: Story = {
62
- args: {
63
- lane: (
64
- <div className="flex flex-col gap-5 pt-3.5">
65
- <EventCard
66
- event={event("f", "Psihotrop — From the Beginning to Infinity", "Atelier Café", 50)}
67
- labels={labels}
68
- formatters={formatters}
69
- emphasis
70
- hideWhen
71
- tagLabel="Live music"
72
- />
73
- <button
74
- type="button"
75
- className="inline-flex h-9 w-max items-center gap-1.5 rounded-full border border-border bg-card px-4 text-[13px] font-semibold"
76
- >
77
- <TimerOff className="h-3.5 w-3.5" aria-hidden />7 earlier events, 14:00 — 19:00
78
- <ChevronDown className="h-3.5 w-3.5" aria-hidden />
79
- </button>
80
- </div>
81
- ),
82
- children: (
83
- <>
84
- {/* The opening hour: its own station, holding cards rather than rows. */}
85
- <HourGroup spine time="20:00 — 20:30" countLabel="4 events">
86
- <div className="grid grid-cols-3 gap-5 pt-3.5">
87
- <EventCard
88
- event={event("c1", "Quiz Night", "Flying Circus")}
89
- labels={labels}
90
- formatters={formatters}
91
- hideWhen
92
- />
93
- <EventCard
94
- event={event("c2", "Beer Battle", "Irish Music Pub", 0)}
95
- labels={labels}
96
- formatters={formatters}
97
- hideWhen
98
- />
99
- <EventCard
100
- event={event("c3", "COJO", "MATTER", 40)}
101
- labels={labels}
102
- formatters={formatters}
103
- hideWhen
104
- />
105
- </div>
106
- </HourGroup>
107
-
108
- {/* Later hours thin out into rows — same station, denser content. */}
109
- <HourGroup spine time="21:00" countLabel="2 events">
110
- <EventCompactRow
111
- event={event("r1", "Karaoke Night", "VIBE OffSeason")}
112
- labels={labels}
113
- formatters={formatters}
114
- />
115
- <EventCompactRow
116
- event={event("r2", "Urban Sounds at Caro", "Caro", 0)}
117
- labels={labels}
118
- formatters={formatters}
119
- />
120
- </HourGroup>
121
-
122
- <HourGroup spine time="22:00 — 23:00" countLabel="2 events">
123
- <EventCompactRow
124
- event={event("r3", "Schoolotheque Afterparty", "Flying Circus", 25)}
125
- labels={labels}
126
- formatters={formatters}
127
- />
128
- <EventCompactRow
129
- event={event("r4", "Összerázó Party Vol. I", "After Eight", 30)}
130
- labels={labels}
131
- formatters={formatters}
132
- />
133
- </HourGroup>
134
- </>
135
- ),
136
- },
137
- };
138
-
139
- /** A minimal band — one poster, one group of rows. */
140
- export const Default: Story = {
141
- args: {
142
- lane: (
143
- <EventCard
144
- event={event("1", "Karaoke Night", "VIBE OffSeason")}
145
- labels={labels}
146
- formatters={formatters}
147
- posterAspect="4/3"
148
- hideWhen
149
- />
150
- ),
151
- children: (
152
- <HourGroup spine time="21:00" countLabel="3 events">
153
- <EventCompactRow
154
- event={event("2", "Urban Sounds at Caro", "Caro", 0)}
155
- labels={labels}
156
- formatters={formatters}
157
- />
158
- <EventCompactRow
159
- event={event("3", "Quiz Night", "Flying Circus")}
160
- labels={labels}
161
- formatters={formatters}
162
- />
163
- </HourGroup>
164
- ),
165
- },
166
- };
167
-
168
- /** The live hour: green-bordered puck, a slow ring pulse, and a NOW ON label
169
- * beneath it. This is the ONLY live marker on the surface — there is no
170
- * separate playhead printing a second, different time beside it. */
171
- export const Live: Story = {
172
- args: {
173
- lane: (
174
- <EventCard
175
- event={event("4", "Psihotrop — From the Beginning to Infinity", "Atelier Café", 50)}
176
- labels={labels}
177
- formatters={formatters}
178
- posterAspect="4/3"
179
- hideWhen
180
- liveStatus="Live now · ends around 23:00"
181
- />
182
- ),
183
- children: (
184
- <HourGroup spine isLive liveLabel="NOW ON" time="20:00" countLabel="1 event">
185
- <EventCompactRow
186
- event={event("5", "Charity Concert & Afterparty", "Flying Circus", 30)}
187
- labels={labels}
188
- formatters={formatters}
189
- />
190
- </HourGroup>
191
- ),
192
- },
193
- };
194
-
195
- /** A quiet hour: an em dash on the axis, no lane content. */
196
- export const EmptyHour: Story = {
197
- args: {
198
- lane: null,
199
- children: <HourGroup spine time="—" countLabel="0 events" note="Nothing listed yet." />,
200
- },
201
- };
202
-
203
- /** Holds the lane/spine/content geometry so nothing jumps when data lands. */
204
- export const Skeleton: Story = {
205
- render: () => <TimelineBandSkeleton />,
206
- };
@@ -1,247 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { Radio } from "lucide-react";
5
- import { cn } from "../lib/utils";
6
- import { Separator } from "./separator";
7
- import { Skeleton } from "./skeleton";
8
-
9
- /**
10
- * TimelineBand — the desktop timeline's three-column row: lane │ spine │ content.
11
- *
12
- * Desktop counterpart of `<HourBand>`. Where the mobile timeline collapses the
13
- * axis into a 52px gutter beside full-width cards, the desktop layout gives the
14
- * poster its own lane (`--timeline-lane`), a dedicated spine
15
- * (`--timeline-spine`) carrying the hairline, and a separate content column for
16
- * whatever follows.
17
- *
18
- * ── The band does not draw hours ──────────────────────────────────────────
19
- * IT USED TO, and only for the hour it opened on. Every LATER hour was an
20
- * inline `HourGroup` heading in the content column instead, so one concept was
21
- * drawn in two grammars 48px apart — a bordered puck ON the axis at the top of
22
- * the day, bare muted text OFF it for the rest of it. An axis whose ticks are
23
- * labelled once is not an axis, and that is exactly how it read: the later
24
- * hours were harder to follow than the first.
25
- *
26
- * Now every hour is a station and `HourGroup` emits its own, via
27
- * `TimelineHourPuck` below. The band keeps the geometry and the hairline and
28
- * nothing else — which is also why `puckTop` is gone: each puck centres itself
29
- * on its own heading rule, so there is no magic offset left for a caller to
30
- * tune against a lane height.
31
- *
32
- * The invariant the old docblock was reaching for still holds, and states more
33
- * simply now: AN HOUR APPEARS EXACTLY ONCE, AND ALWAYS ON THE SPINE.
34
- *
35
- * HOW THE CONTENT COLUMN IS MEANT TO BE FILLED — the source design composes it
36
- * in tiers, and getting this wrong is what makes the band look flat:
37
- *
38
- * lane one feature card (an `EventCard` with `emphasis`), plus any
39
- * "N earlier events" collapse control beneath it.
40
- * content an `<HourGroup spine>` per hour — the opening one holding a grid
41
- * of `EventCard`s, later ones holding the denser
42
- * `<EventCompactRow>` as the night thins out.
43
- *
44
- * So the density falls off across the row: one poster, then a few cards, then
45
- * rows.
46
- */
47
-
48
- /**
49
- * Ring pulse on the live puck — inherited from an earlier `TimelinePlayhead`,
50
- * which carried this surface's only motion. Kept at its 2.4s `ease-in-out` and
51
- * its reduced-motion kill switch, but BOX-SHADOW ONLY: a `transform: scale`
52
- * would make the hairline visibly breathe here, because this puck sits ON the
53
- * rail. (Today's `timeline-playhead.tsx` is static and sits between bands.)
54
- */
55
- const LIVE_PUCK_PULSE_CLASS = "bm-timeline-live-puck-pulse";
56
-
57
- /**
58
- * The mobile axis gutter, shared by `HourBand` and `TimelinePlayhead` so the
59
- * hour chips and the current-time label sit in one column. 58px is the width
60
- * of a five-glyph 24-hour label ("20:00") in the chip's mono type plus its
61
- * padding; a 12-hour locale's "09:00 AM" overflows it, which is one of the
62
- * reasons the events surfaces format 24-hour everywhere.
63
- */
64
- export const TIMELINE_MOBILE_GUTTER_CLASS = "w-[58px]";
65
-
66
- export type TimelineHourPuckProps = {
67
- /** The hour, already formatted and localised — or an em dash for a quiet day. */
68
- time: string;
69
- /** Marks this as the hour currently in progress. */
70
- isLive?: boolean;
71
- /** Shown under the puck when live, e.g. "NOW ON". Required to render it. */
72
- liveLabel?: string;
73
- /**
74
- * `station` (default): the bordered puck that sits ON the hairline, for the
75
- * desktop spine. `chip`: a filled, fixed-width lozenge with the hairline
76
- * starting BELOW it, for the mobile gutter — an app's time label, not a
77
- * station on a rail. In `chip` the live label is visually hidden: the tinted
78
- * chip plus a small on-air glyph are the visible signal (the glyph so the
79
- * state never rests on colour alone once the pulse is reduced away) and the
80
- * label stays for screen readers only.
81
- */
82
- variant?: "station" | "chip";
83
- className?: string;
84
- };
85
-
86
- /**
87
- * The station on the timeline's axis — ONE object, positioned by its host:
88
- * `HourGroup` centres it on its own heading rule out in the desktop spine,
89
- * `HourBand` pins it to the top of the mobile gutter.
90
- *
91
- * Extracted because it was hand-rolled twice and the copies had already
92
- * drifted — this one bordered and `rounded-[7px] px-[7px]`, `hour-band.tsx`'s
93
- * borderless and `rounded-md px-1.5`. The `bg-background` fill punches through
94
- * the hairline so the rail reads as continuous behind the label rather than
95
- * butting into it.
96
- *
97
- * The live label is absolutely positioned rather than a flex sibling: hosts
98
- * centre this puck with `-translate-y-1/2`, and a two-item column would centre
99
- * the STACK, lifting the puck off the line it exists to sit on.
100
- */
101
- export function TimelineHourPuck({
102
- time,
103
- isLive,
104
- liveLabel,
105
- variant = "station",
106
- className,
107
- }: TimelineHourPuckProps) {
108
- const isChip = variant === "chip";
109
-
110
- return (
111
- <span className={cn("relative flex justify-center", className)}>
112
- {isLive && (
113
- <style>{`
114
- @keyframes ${LIVE_PUCK_PULSE_CLASS} {
115
- 0%, 100% { box-shadow: 0 0 0 0 color-mix(in oklch, var(--success) 40%, transparent); }
116
- 50% { box-shadow: 0 0 0 4px color-mix(in oklch, var(--success) 0%, transparent); }
117
- }
118
- .${LIVE_PUCK_PULSE_CLASS} {
119
- animation: ${LIVE_PUCK_PULSE_CLASS} 2.4s ease-in-out infinite;
120
- }
121
- @media (prefers-reduced-motion: reduce) {
122
- .${LIVE_PUCK_PULSE_CLASS} { animation: none; }
123
- }
124
- `}</style>
125
- )}
126
- <span
127
- className={cn(
128
- "whitespace-nowrap font-mono tabular-nums",
129
- isChip
130
- ? cn(
131
- "inline-flex h-6 min-w-[58px] items-center justify-center gap-1 rounded-[7px] px-1 text-[12px] font-medium",
132
- isLive ? cn("bg-success/10 text-success", LIVE_PUCK_PULSE_CLASS) : "bg-muted text-foreground"
133
- )
134
- : cn(
135
- "rounded-[7px] border bg-background px-[7px] py-[3px] text-[12.5px] font-medium",
136
- isLive
137
- ? cn("border-success/40 text-success", LIVE_PUCK_PULSE_CLASS)
138
- : "border-border text-foreground"
139
- )
140
- )}
141
- >
142
- {isChip && isLive && <Radio className="h-2.5 w-2.5 shrink-0" aria-hidden />}
143
- {time}
144
- </span>
145
- {isLive && !!liveLabel && (
146
- <span
147
- className={cn(
148
- isChip
149
- ? "sr-only"
150
- : cn(
151
- "absolute top-[calc(100%+6px)] whitespace-nowrap bg-background px-1",
152
- "text-[9.5px] font-extrabold tracking-[0.08em] text-success"
153
- )
154
- )}
155
- >
156
- {liveLabel}
157
- </span>
158
- )}
159
- </span>
160
- );
161
- }
162
-
163
- export type TimelineBandProps = {
164
- /** The lane column — usually a poster card, or empty for a quiet hour. */
165
- lane: React.ReactNode;
166
- /** The content column, normally one `<HourGroup spine>` per hour. */
167
- children: React.ReactNode;
168
- className?: string;
169
- };
170
-
171
- export function TimelineBand({ lane, children, className }: TimelineBandProps) {
172
- return (
173
- <div className={cn("flex items-stretch", className)}>
174
- <div className="w-[var(--timeline-lane)] shrink-0">{lane}</div>
175
-
176
- <div className="relative flex w-[var(--timeline-spine)] shrink-0 justify-center self-stretch">
177
- <Separator orientation="vertical" className="h-full" />
178
- </div>
179
-
180
- <div className="flex min-w-0 flex-1 flex-col">{children}</div>
181
- </div>
182
- );
183
- }
184
-
185
- /**
186
- * A fixed-size shimmer block. `radiusPx` only needs to be passed for the
187
- * poster-sized blocks (12, matching `rounded-xl`) — every thinner text-line
188
- * height here (13–18px) is less than twice that radius, so the browser clamps
189
- * it to a pill automatically even left at the `Skeleton` default.
190
- */
191
- function sk(width: string, height: number, radiusPx = 12) {
192
- return (
193
- <Skeleton
194
- style={{ width, height: `${height}px`, borderRadius: `${radiusPx}px` }}
195
- />
196
- );
197
- }
198
-
199
- export type TimelineBandSkeletonProps = {
200
- className?: string;
201
- };
202
-
203
- /**
204
- * Holds the lane/spine/content geometry so nothing jumps when data lands.
205
- * The spine renders its hairline with no puck — there's no hour to show yet.
206
- */
207
- export function TimelineBandSkeleton({ className }: TimelineBandSkeletonProps) {
208
- return (
209
- <div className={cn("flex items-stretch", className)} aria-hidden="true">
210
- <div className="flex w-[var(--timeline-lane)] shrink-0 flex-col gap-3">
211
- {sk("100%", 297, 12)}
212
- {sk("70%", 18)}
213
- {sk("45%", 13)}
214
- </div>
215
-
216
- <div className="relative flex w-[var(--timeline-spine)] shrink-0 justify-center self-stretch">
217
- <Separator orientation="vertical" className="h-full" />
218
- </div>
219
-
220
- <div className="flex min-w-0 flex-1 flex-col gap-4">
221
- <div className="grid grid-cols-3 gap-[10px]">
222
- {Array.from({ length: 3 }, (_, index) => (
223
- <div key={index} className="flex flex-col gap-2">
224
- {sk("100%", 190, 12)}
225
- {sk("85%", 15)}
226
- {sk("55%", 12)}
227
- </div>
228
- ))}
229
- </div>
230
-
231
- <div className="flex flex-col">
232
- {Array.from({ length: 3 }, (_, index) => (
233
- <div key={index} className="relative flex items-center gap-[11px] py-2">
234
- <Separator className="absolute inset-x-0 top-0" />
235
-
236
- <Skeleton className="h-[34px] w-[34px] shrink-0 rounded-md" />
237
- <div className="flex min-w-0 flex-1 flex-col gap-1.5">
238
- {sk("60%", 13)}
239
- {sk("40%", 11.5)}
240
- </div>
241
- </div>
242
- ))}
243
- </div>
244
- </div>
245
- </div>
246
- );
247
- }