@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,145 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { DayRailChip, DayRailChipSkeleton } from "./day-rail-chip";
|
|
3
|
+
import type { DayRailChipLabels } from "./event-types";
|
|
4
|
+
|
|
5
|
+
const labels: DayRailChipLabels = { noEvents: "No events" };
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof DayRailChip> = {
|
|
8
|
+
title: "Events/DayRailChip",
|
|
9
|
+
component: DayRailChip,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
args: { labels },
|
|
12
|
+
parameters: {
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
"One day in the timeline's day rail. An empty day keeps its place in the rail with an em dash and no density dot — ten of fourteen days routinely read empty, and dropping them would make the rail lie about how far ahead it reaches.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof DayRailChip>;
|
|
24
|
+
|
|
25
|
+
export const Active: Story = {
|
|
26
|
+
args: { label: "Today", dayNumber: "3", count: 15, isActive: true },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const HasEvents: Story = {
|
|
30
|
+
args: { label: "Fri", dayNumber: "4", count: 17 },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Weekend days keep the bold weight while inactive, so they stay findable. */
|
|
34
|
+
export const Weekend: Story = {
|
|
35
|
+
args: { label: "Sat", dayNumber: "5", count: 23, isWeekend: true },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** No dot, an em dash rather than "0", muted label — but still in the rail. */
|
|
39
|
+
export const Empty: Story = {
|
|
40
|
+
args: { label: "Fri", dayNumber: "11", count: 0 },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** The full fortnight, as the rail actually renders it. */
|
|
44
|
+
export const TheRail: Story = {
|
|
45
|
+
render: () => {
|
|
46
|
+
const days: Array<[string, string, number, boolean]> = [
|
|
47
|
+
["Today", "3", 15, false],
|
|
48
|
+
["Fri", "4", 17, true],
|
|
49
|
+
["Sat", "5", 23, true],
|
|
50
|
+
["Sun", "6", 10, true],
|
|
51
|
+
["Mon", "7", 5, false],
|
|
52
|
+
["Tue", "8", 3, false],
|
|
53
|
+
["Wed", "9", 3, false],
|
|
54
|
+
["Thu", "10", 2, false],
|
|
55
|
+
["Fri", "11", 0, true],
|
|
56
|
+
["Sat", "12", 4, true],
|
|
57
|
+
["Sun", "13", 3, true],
|
|
58
|
+
["Mon", "14", 0, false],
|
|
59
|
+
];
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex max-w-[390px] gap-2 overflow-x-auto px-4 pb-1">
|
|
62
|
+
{days.map(([label, dayNumber, count, isWeekend], i) => (
|
|
63
|
+
<DayRailChip
|
|
64
|
+
key={`${label}-${dayNumber}`}
|
|
65
|
+
label={label}
|
|
66
|
+
dayNumber={dayNumber}
|
|
67
|
+
count={count}
|
|
68
|
+
isWeekend={isWeekend}
|
|
69
|
+
isActive={i === 0}
|
|
70
|
+
labels={labels}
|
|
71
|
+
onClick={() => {}}
|
|
72
|
+
/>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Loading state — one chip's footprint (62×50, 12px radius). */
|
|
80
|
+
export const Loading: Story = {
|
|
81
|
+
render: () => <DayRailChipSkeleton />,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** The full fortnight, loading — six chips at the rail's own `gap-2` (8px). */
|
|
85
|
+
export const LoadingRail: Story = {
|
|
86
|
+
render: () => (
|
|
87
|
+
<div className="flex max-w-[390px] gap-2 overflow-x-auto px-4 pb-1">
|
|
88
|
+
{Array.from({ length: 6 }).map((_, index) => (
|
|
89
|
+
<DayRailChipSkeleton key={index} />
|
|
90
|
+
))}
|
|
91
|
+
</div>
|
|
92
|
+
),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** A loading rail beside the loaded one, so any geometry drift is visible at a glance. */
|
|
96
|
+
export const LoadingVsLoaded: Story = {
|
|
97
|
+
render: () => (
|
|
98
|
+
<div className="space-y-2">
|
|
99
|
+
<div className="flex max-w-[390px] gap-2 overflow-x-auto px-4 pb-1">
|
|
100
|
+
{Array.from({ length: 6 }).map((_, index) => (
|
|
101
|
+
<DayRailChipSkeleton key={index} />
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
<div className="flex max-w-[390px] gap-2 overflow-x-auto px-4 pb-1">
|
|
105
|
+
<DayRailChip label="Today" dayNumber="3" count={15} isActive labels={labels} onClick={() => {}} />
|
|
106
|
+
<DayRailChip label="Fri" dayNumber="4" count={17} labels={labels} onClick={() => {}} />
|
|
107
|
+
<DayRailChip label="Sat" dayNumber="5" count={23} isWeekend labels={labels} onClick={() => {}} />
|
|
108
|
+
<DayRailChip label="Sun" dayNumber="6" count={10} isWeekend labels={labels} onClick={() => {}} />
|
|
109
|
+
<DayRailChip label="Mon" dayNumber="7" count={5} labels={labels} onClick={() => {}} />
|
|
110
|
+
<DayRailChip label="Tue" dayNumber="8" count={3} labels={labels} onClick={() => {}} />
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** With a filter applied, counts redraw and most days fall to zero. */
|
|
117
|
+
export const FilteredToFreeEntry: Story = {
|
|
118
|
+
render: () => {
|
|
119
|
+
const days: Array<[string, string, number]> = [
|
|
120
|
+
["Today", "3", 2],
|
|
121
|
+
["Fri", "4", 3],
|
|
122
|
+
["Sat", "5", 2],
|
|
123
|
+
["Sun", "6", 0],
|
|
124
|
+
["Mon", "7", 1],
|
|
125
|
+
["Tue", "8", 0],
|
|
126
|
+
["Wed", "9", 1],
|
|
127
|
+
["Thu", "10", 0],
|
|
128
|
+
];
|
|
129
|
+
return (
|
|
130
|
+
<div className="flex max-w-[390px] gap-2 overflow-x-auto px-4 pb-1">
|
|
131
|
+
{days.map(([label, dayNumber, count], i) => (
|
|
132
|
+
<DayRailChip
|
|
133
|
+
key={`${label}-${dayNumber}`}
|
|
134
|
+
label={label}
|
|
135
|
+
dayNumber={dayNumber}
|
|
136
|
+
count={count}
|
|
137
|
+
isActive={i === 2}
|
|
138
|
+
labels={labels}
|
|
139
|
+
onClick={() => {}}
|
|
140
|
+
/>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
import { Skeleton } from "./skeleton";
|
|
6
|
+
import type { DayRailChipLabels } from "./event-types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* DayRailChip — one day in the horizontally scrolling day rail.
|
|
10
|
+
*
|
|
11
|
+
* Three states the design distinguishes and that are easy to collapse by
|
|
12
|
+
* accident:
|
|
13
|
+
* - ACTIVE: FILLED — accent ground, accent-foreground label, count and dot.
|
|
14
|
+
* - HAS EVENTS: card ground, normal foreground, an accent density dot.
|
|
15
|
+
* - EMPTY: muted text, NO dot, an em dash instead of "0". A day with nothing
|
|
16
|
+
* on stays in the rail — ten of fourteen days routinely read empty, and
|
|
17
|
+
* hiding them makes the rail lie about how far it reaches — but the caller
|
|
18
|
+
* decides whether it is interactive, and a rail that NAVIGATES has nothing
|
|
19
|
+
* to send an empty day to.
|
|
20
|
+
*
|
|
21
|
+
* ── Why active is filled ──────────────────────────────────────────────────
|
|
22
|
+
* It was an accent border and accent text. Measured against the real tokens
|
|
23
|
+
* that is a 1px hairline plus a label at 1.16:1 against its inactive
|
|
24
|
+
* neighbours in light mode — and in dark, where `--primary` (0.66) is DIMMER
|
|
25
|
+
* than `--foreground` (0.96), the active chip read as the recessed one at
|
|
26
|
+
* 2.86:1. Survivable while a large pinned day header also announced the day;
|
|
27
|
+
* not survivable once the rail became the only indicator.
|
|
28
|
+
*
|
|
29
|
+
* An eye scanning fourteen near-identical 62px boxes horizontally, in
|
|
30
|
+
* peripheral vision, while the page moves, resolves AREA and LUMINANCE STEP
|
|
31
|
+
* long before it resolves the hue of a hairline or the weight of 11.5px type.
|
|
32
|
+
* The fill presents ~3,100px² at 5.97:1 (light) / 6.09:1 (dark). A tinted
|
|
33
|
+
* ground would not do it: `bg-accent/10` and `--accent-subtle` both measure
|
|
34
|
+
* ~1.15:1 against the chip's own card, which is no signal at all.
|
|
35
|
+
*
|
|
36
|
+
* `--accent`, deliberately, not `--gradient-primary`: white on that gradient's
|
|
37
|
+
* top stop is 4.45:1, under AA for text, and the gradient has no `.dark`
|
|
38
|
+
* override so the chip's ink direction would invert against the rest of the
|
|
39
|
+
* dark theme. `--accent` also already means "current" here — the bottom nav's
|
|
40
|
+
* active tab uses it.
|
|
41
|
+
*
|
|
42
|
+
* The border stays 1px in BOTH states. Thickening it on active would change
|
|
43
|
+
* the box model, and these chips sit in an equal-width grid that would reflow
|
|
44
|
+
* on every scroll-spy tick.
|
|
45
|
+
*
|
|
46
|
+
* Renders a link when `href` is given, a button when only `onClick` is, and a
|
|
47
|
+
* plain div otherwise. The button announces `aria-current`, not
|
|
48
|
+
* `aria-pressed`: this is the current item in a set of days, not a toggle.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
export type DayRailChipLinkProps = {
|
|
52
|
+
href: string;
|
|
53
|
+
className?: string;
|
|
54
|
+
children: React.ReactNode;
|
|
55
|
+
"aria-current"?: "page";
|
|
56
|
+
onClick?: () => void;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function DefaultLink({ href, ...props }: DayRailChipLinkProps) {
|
|
60
|
+
return <a href={href} {...props} />;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type DayRailChipProps = {
|
|
64
|
+
/** Short weekday label, already localised — "Today", "Fri", "Sâm". */
|
|
65
|
+
label: string;
|
|
66
|
+
/** Day of month, as displayed. */
|
|
67
|
+
dayNumber: string;
|
|
68
|
+
/** Events on this day, after any active filter. */
|
|
69
|
+
count: number;
|
|
70
|
+
isActive?: boolean;
|
|
71
|
+
/** Keeps the bold weight when inactive. */
|
|
72
|
+
isWeekend?: boolean;
|
|
73
|
+
labels: DayRailChipLabels;
|
|
74
|
+
href?: string;
|
|
75
|
+
linkComponent?: React.ComponentType<DayRailChipLinkProps>;
|
|
76
|
+
onClick?: () => void;
|
|
77
|
+
className?: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export function DayRailChip({
|
|
81
|
+
label,
|
|
82
|
+
dayNumber,
|
|
83
|
+
count,
|
|
84
|
+
isActive,
|
|
85
|
+
isWeekend,
|
|
86
|
+
labels,
|
|
87
|
+
href,
|
|
88
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
89
|
+
onClick,
|
|
90
|
+
className,
|
|
91
|
+
}: DayRailChipProps) {
|
|
92
|
+
const hasEvents = count > 0;
|
|
93
|
+
|
|
94
|
+
const rootClasses = cn(
|
|
95
|
+
"w-[62px] shrink-0 rounded-xl border px-1 py-2 text-center",
|
|
96
|
+
"transition-colors duration-150 motion-reduce:transition-none",
|
|
97
|
+
isActive ? "border-accent bg-accent" : "border-border bg-card",
|
|
98
|
+
(href || onClick) &&
|
|
99
|
+
// `ring-offset-background` is not decoration: Tailwind's default offset
|
|
100
|
+
// colour is white, which painted a white halo around a focused chip on
|
|
101
|
+
// the dark rail.
|
|
102
|
+
"cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
103
|
+
className
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const body = (
|
|
107
|
+
<>
|
|
108
|
+
<div
|
|
109
|
+
className={cn(
|
|
110
|
+
"text-[11.5px]",
|
|
111
|
+
isActive || isWeekend ? "font-bold" : "font-semibold",
|
|
112
|
+
isActive
|
|
113
|
+
? "text-accent-foreground"
|
|
114
|
+
: hasEvents
|
|
115
|
+
? "text-foreground"
|
|
116
|
+
// NOT `/70`, which measured 2.96:1 light and 4.17:1 dark — an AA
|
|
117
|
+
// failure in both. An empty day is inert content, not a disabled
|
|
118
|
+
// form control, so nothing exempts it. The em dash and the
|
|
119
|
+
// missing dot already say "empty"; `muted-foreground` against
|
|
120
|
+
// `foreground` is still an 18.8 → 5.5 step and plainly quieter.
|
|
121
|
+
: "text-muted-foreground"
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
{label} {dayNumber}
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div className="mt-[3px] flex items-center justify-center gap-1">
|
|
128
|
+
{/* Kept on the active chip, not dropped: losing it would shift the
|
|
129
|
+
count 5px sideways every time the day changes under the scroll spy.
|
|
130
|
+
Inactive is full `bg-accent`, not `bg-primary/40` — at 40% alpha it
|
|
131
|
+
measured 2.46:1 / 1.93:1 (invisible at 4px), and in light mode
|
|
132
|
+
`--primary` is near-black, so the "warm density dot" was grey. */}
|
|
133
|
+
{hasEvents && (
|
|
134
|
+
<span
|
|
135
|
+
className={cn(
|
|
136
|
+
"h-1 w-1 rounded-full",
|
|
137
|
+
isActive ? "bg-accent-foreground" : "bg-accent"
|
|
138
|
+
)}
|
|
139
|
+
aria-hidden
|
|
140
|
+
/>
|
|
141
|
+
)}
|
|
142
|
+
<span
|
|
143
|
+
className={cn(
|
|
144
|
+
// Full opacity on active, never dimmed: `/80` lands at 4.48:1, an
|
|
145
|
+
// AA fail by 0.02. Hierarchy inside a 50px box comes from the
|
|
146
|
+
// label's weight and the type-size step, which is enough.
|
|
147
|
+
"font-mono text-[10.5px] tabular-nums",
|
|
148
|
+
isActive ? "text-accent-foreground" : "text-muted-foreground"
|
|
149
|
+
)}
|
|
150
|
+
>
|
|
151
|
+
{hasEvents ? count : <span aria-label={labels.noEvents}>—</span>}
|
|
152
|
+
</span>
|
|
153
|
+
</div>
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (href) {
|
|
158
|
+
return (
|
|
159
|
+
<LinkComponent
|
|
160
|
+
href={href}
|
|
161
|
+
className={rootClasses}
|
|
162
|
+
aria-current={isActive ? "page" : undefined}
|
|
163
|
+
onClick={onClick}
|
|
164
|
+
>
|
|
165
|
+
{body}
|
|
166
|
+
</LinkComponent>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (onClick) {
|
|
171
|
+
return (
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
className={rootClasses}
|
|
175
|
+
// `aria-current`, not `aria-pressed`. The rail used to FILTER, where a
|
|
176
|
+
// toggle semantic was right; it now navigates, and this is the current
|
|
177
|
+
// day in a set of days.
|
|
178
|
+
aria-current={isActive ? "true" : undefined}
|
|
179
|
+
onClick={onClick}
|
|
180
|
+
>
|
|
181
|
+
{body}
|
|
182
|
+
</button>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return <div className={rootClasses}>{body}</div>;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export type DayRailChipSkeletonProps = {
|
|
190
|
+
className?: string;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Mirrors one `<DayRailChip>`'s footprint (62×50, 12px radius — the source
|
|
195
|
+
* design's own day-rail skeleton geometry, which also happens to match this
|
|
196
|
+
* chip's rendered size: label + count rows inside `py-2` padding). The caller
|
|
197
|
+
* repeats it with the rail's own `gap-2` to build the loading rail.
|
|
198
|
+
*/
|
|
199
|
+
export function DayRailChipSkeleton({ className }: DayRailChipSkeletonProps) {
|
|
200
|
+
return <Skeleton aria-hidden="true" className={cn("h-[50px] w-[62px] shrink-0", className)} />;
|
|
201
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { DriftRail } from "./drift-rail";
|
|
3
|
+
import { FeaturedRailItem } from "./featured-rail-item";
|
|
4
|
+
|
|
5
|
+
const CHIP_LABELS = [
|
|
6
|
+
"Live music",
|
|
7
|
+
"Rooftop",
|
|
8
|
+
"Free entry",
|
|
9
|
+
"Late night",
|
|
10
|
+
"DJ set",
|
|
11
|
+
"Comedy",
|
|
12
|
+
"Art walk",
|
|
13
|
+
"Brunch",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function Chip({ label }: { label: string }) {
|
|
17
|
+
return (
|
|
18
|
+
<span className="inline-flex h-9 shrink-0 items-center rounded-full border border-border bg-card px-4 text-sm font-medium">
|
|
19
|
+
{label}
|
|
20
|
+
</span>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Explicitly annotated rather than `satisfies`: the meta-level decorator makes
|
|
25
|
+
// the inferred type reference storybook's internal CSF module, which tsc cannot
|
|
26
|
+
// name portably (TS2742).
|
|
27
|
+
const meta: Meta<typeof DriftRail> = {
|
|
28
|
+
title: "Events/DriftRail",
|
|
29
|
+
component: DriftRail,
|
|
30
|
+
tags: ["autodocs"],
|
|
31
|
+
parameters: {
|
|
32
|
+
docs: {
|
|
33
|
+
description: {
|
|
34
|
+
component:
|
|
35
|
+
"An infinite horizontal marquee. Duplicates its children once so a `translateX(-50%)` loop is seamless — pass a fixed, ordered list. Pauses on hover, pauses when scrolled off-screen (`IntersectionObserver`), and stops outright under `prefers-reduced-motion`.",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default meta;
|
|
42
|
+
type Story = StoryObj<typeof DriftRail>;
|
|
43
|
+
|
|
44
|
+
export const Chips: Story = {
|
|
45
|
+
render: () => (
|
|
46
|
+
<div className="max-w-[420px]">
|
|
47
|
+
<DriftRail>
|
|
48
|
+
{CHIP_LABELS.map((label) => (
|
|
49
|
+
<Chip key={label} label={label} />
|
|
50
|
+
))}
|
|
51
|
+
</DriftRail>
|
|
52
|
+
</div>
|
|
53
|
+
),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Two rails at once — the design's opposing-direction pair, forward at 42s and reverse at 52s. */
|
|
57
|
+
export const OpposingPair: Story = {
|
|
58
|
+
render: () => (
|
|
59
|
+
<div className="max-w-[420px] space-y-4">
|
|
60
|
+
<DriftRail>
|
|
61
|
+
{CHIP_LABELS.slice(0, 5).map((label) => (
|
|
62
|
+
<Chip key={label} label={label} />
|
|
63
|
+
))}
|
|
64
|
+
</DriftRail>
|
|
65
|
+
<DriftRail reverse durationSeconds={52}>
|
|
66
|
+
{CHIP_LABELS.slice(3).map((label) => (
|
|
67
|
+
<Chip key={label} label={label} />
|
|
68
|
+
))}
|
|
69
|
+
</DriftRail>
|
|
70
|
+
</div>
|
|
71
|
+
),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Wrapping the featured rail's own card — the rail supplies the motion, the card stays static content. */
|
|
75
|
+
export const WithFeaturedItems: Story = {
|
|
76
|
+
render: () => (
|
|
77
|
+
<div className="max-w-[420px]">
|
|
78
|
+
<DriftRail durationSeconds={30}>
|
|
79
|
+
{["Warehouse Rave", "Jazz on the Rooftop", "Open Mic Night", "Street Food Market"].map(
|
|
80
|
+
(title) => (
|
|
81
|
+
<FeaturedRailItem
|
|
82
|
+
key={title}
|
|
83
|
+
title={title}
|
|
84
|
+
venue="Atelier Café"
|
|
85
|
+
note="Sat 5 · 20:00"
|
|
86
|
+
/>
|
|
87
|
+
)
|
|
88
|
+
)}
|
|
89
|
+
</DriftRail>
|
|
90
|
+
</div>
|
|
91
|
+
),
|
|
92
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useId, useRef } from "react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DriftRail — an infinite horizontal marquee for logos, tag chips or small
|
|
8
|
+
* poster tiles. The caller passes one set of children; the component renders
|
|
9
|
+
* a second, `aria-hidden` copy right after it so a `translateX(-50%)` loop is
|
|
10
|
+
* seamless. Because the loop depends on the duplicate being pixel-identical
|
|
11
|
+
* to the first pass, keep `children` a fixed, ordered list — no per-render
|
|
12
|
+
* randomisation.
|
|
13
|
+
*
|
|
14
|
+
* Motion is scoped to this instance via a generated id (no shared stylesheet,
|
|
15
|
+
* no class collisions between multiple rails on one page) and is paused two
|
|
16
|
+
* ways, per the source design: an `IntersectionObserver` stops the loop the
|
|
17
|
+
* moment the rail scrolls off-screen, and `prefers-reduced-motion: reduce`
|
|
18
|
+
* removes the animation outright rather than merely slowing it.
|
|
19
|
+
*/
|
|
20
|
+
export type DriftRailProps = {
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
/** Runs the loop right-to-left instead of left-to-right. */
|
|
23
|
+
reverse?: boolean;
|
|
24
|
+
durationSeconds?: number;
|
|
25
|
+
/** Pauses the loop while the pointer hovers the rail. */
|
|
26
|
+
pauseOnHover?: boolean;
|
|
27
|
+
className?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const DRIFT_RAIL_DEFAULT_DURATION_SECONDS = 42;
|
|
31
|
+
|
|
32
|
+
export function DriftRail({
|
|
33
|
+
children,
|
|
34
|
+
reverse = false,
|
|
35
|
+
durationSeconds = DRIFT_RAIL_DEFAULT_DURATION_SECONDS,
|
|
36
|
+
pauseOnHover = true,
|
|
37
|
+
className,
|
|
38
|
+
}: DriftRailProps) {
|
|
39
|
+
const scopeId = useId().replace(/[^a-zA-Z0-9]/g, "");
|
|
40
|
+
const scopeClass = `drift-rail-${scopeId}`;
|
|
41
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const node = containerRef.current;
|
|
45
|
+
if (!node || typeof IntersectionObserver === "undefined") return undefined;
|
|
46
|
+
|
|
47
|
+
const observer = new IntersectionObserver(
|
|
48
|
+
([entry]) => {
|
|
49
|
+
node.dataset.offscreen = entry.isIntersecting ? "false" : "true";
|
|
50
|
+
},
|
|
51
|
+
{ threshold: 0 }
|
|
52
|
+
);
|
|
53
|
+
observer.observe(node);
|
|
54
|
+
return () => observer.disconnect();
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div ref={containerRef} className={cn(scopeClass, "overflow-hidden py-1.5", className)}>
|
|
59
|
+
<style>{`
|
|
60
|
+
.${scopeClass} .drift-rail-run {
|
|
61
|
+
animation: ${scopeClass}-run ${durationSeconds}s linear infinite;
|
|
62
|
+
animation-direction: ${reverse ? "reverse" : "normal"};
|
|
63
|
+
}
|
|
64
|
+
.${scopeClass}[data-offscreen="true"] .drift-rail-run {
|
|
65
|
+
animation-play-state: paused;
|
|
66
|
+
}
|
|
67
|
+
@keyframes ${scopeClass}-run {
|
|
68
|
+
from { transform: translateX(0); }
|
|
69
|
+
to { transform: translateX(-50%); }
|
|
70
|
+
}
|
|
71
|
+
@media (prefers-reduced-motion: reduce) {
|
|
72
|
+
.${scopeClass} .drift-rail-run {
|
|
73
|
+
animation: none;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
${pauseOnHover ? `.${scopeClass}:hover .drift-rail-run { animation-play-state: paused; }` : ""}
|
|
77
|
+
`}</style>
|
|
78
|
+
<div className="drift-rail-run flex w-max items-stretch gap-3.5 motion-reduce:[animation:none]">
|
|
79
|
+
{children}
|
|
80
|
+
<div className="contents" aria-hidden="true">
|
|
81
|
+
{children}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { EmptyState, SearchEmptyState, ErrorEmptyState } from "./empty-state";
|
|
3
3
|
|
|
4
|
-
const meta = {
|
|
4
|
+
const meta: Meta<typeof EmptyState> = {
|
|
5
5
|
title: "Components/EmptyState",
|
|
6
6
|
component: EmptyState,
|
|
7
7
|
tags: ["autodocs"],
|
|
@@ -15,10 +15,10 @@ const meta = {
|
|
|
15
15
|
options: ["sm", "md", "lg"],
|
|
16
16
|
},
|
|
17
17
|
},
|
|
18
|
-
}
|
|
18
|
+
};
|
|
19
19
|
|
|
20
20
|
export default meta;
|
|
21
|
-
type Story = StoryObj<typeof
|
|
21
|
+
type Story = StoryObj<typeof EmptyState>;
|
|
22
22
|
|
|
23
23
|
export const Default: Story = {
|
|
24
24
|
args: {
|