@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,118 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { CalendarClock, LayoutGrid, List, SlidersHorizontal } from "lucide-react";
|
|
3
|
+
import { SegmentedNav } from "./segmented-nav";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Components/SegmentedNav",
|
|
7
|
+
component: SegmentedNav,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
"A segmented control whose segments are links to separate routes. Use `<Tabs>` for in-page panels and `<ToggleGroup>` for button semantics — this one renders a nav landmark and marks the active segment with `aria-current=\"page\"`.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
} satisfies Meta<typeof SegmentedNav>;
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
type Story = StoryObj<typeof meta>;
|
|
21
|
+
|
|
22
|
+
const ICON = "h-4 w-4";
|
|
23
|
+
|
|
24
|
+
const eventsItems = [
|
|
25
|
+
{ id: "hub", href: "/events", label: "Hub", icon: <LayoutGrid className={ICON} aria-hidden /> },
|
|
26
|
+
{
|
|
27
|
+
id: "schedule",
|
|
28
|
+
href: "/events/schedule",
|
|
29
|
+
label: "Timeline",
|
|
30
|
+
icon: <CalendarClock className={ICON} aria-hidden />,
|
|
31
|
+
},
|
|
32
|
+
{ id: "list", href: "/events/list", label: "List", icon: <List className={ICON} aria-hidden /> },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** The events surface's three routes — the control this was built for. */
|
|
36
|
+
export const Events: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
items: eventsItems,
|
|
39
|
+
activeId: "schedule",
|
|
40
|
+
label: "Events views",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Segments stretch to fill the row below `lg`, matching the source design's mobile toggle. */
|
|
45
|
+
export const Mobile: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
items: eventsItems,
|
|
48
|
+
activeId: "hub",
|
|
49
|
+
label: "Events views",
|
|
50
|
+
},
|
|
51
|
+
parameters: { viewport: { defaultViewport: "mobile1" } },
|
|
52
|
+
render: (args) => (
|
|
53
|
+
<div className="w-[358px]">
|
|
54
|
+
<SegmentedNav {...args} />
|
|
55
|
+
</div>
|
|
56
|
+
),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* BESIDE OTHER CONTROLS — the case that used to fail silently.
|
|
61
|
+
*
|
|
62
|
+
* As a flex child the nav sized to its content, so the segments' `flex-1`
|
|
63
|
+
* distributed only the width of three labels and the rest of the row went to
|
|
64
|
+
* whatever sat next to it. The `w-full` below `lg` is what makes the segments
|
|
65
|
+
* actually own the row here.
|
|
66
|
+
*/
|
|
67
|
+
export const InAControlRow: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
items: eventsItems,
|
|
70
|
+
activeId: "schedule",
|
|
71
|
+
label: "Events views",
|
|
72
|
+
},
|
|
73
|
+
render: (args) => (
|
|
74
|
+
<div className="w-[390px] rounded-xl border border-border p-2">
|
|
75
|
+
<div className="flex items-center gap-2">
|
|
76
|
+
<SegmentedNav {...args} />
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
aria-label="Filters"
|
|
80
|
+
className="ml-auto flex h-9 w-9 shrink-0 items-center justify-center rounded-full border border-border"
|
|
81
|
+
>
|
|
82
|
+
<SlidersHorizontal className="h-4 w-4" aria-hidden />
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Labels alone, no icons. */
|
|
90
|
+
export const WithoutIcons: Story = {
|
|
91
|
+
args: {
|
|
92
|
+
items: [
|
|
93
|
+
{ id: "all", href: "/all", label: "All" },
|
|
94
|
+
{ id: "free", href: "/free", label: "Free" },
|
|
95
|
+
{ id: "paid", href: "/paid", label: "Paid" },
|
|
96
|
+
],
|
|
97
|
+
activeId: "free",
|
|
98
|
+
label: "Entry filter",
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* In a Next app, pass `next/link` as `linkComponent` for client-side navigation
|
|
104
|
+
* and prefetch. The default is a plain `<a>` so the component stays usable in
|
|
105
|
+
* a non-Next app.
|
|
106
|
+
*/
|
|
107
|
+
export const WithCustomLinkComponent: Story = {
|
|
108
|
+
args: {
|
|
109
|
+
items: eventsItems,
|
|
110
|
+
activeId: "list",
|
|
111
|
+
label: "Events views",
|
|
112
|
+
linkComponent: ({ href, children, ...props }) => (
|
|
113
|
+
<a href={href} data-router="custom" {...props}>
|
|
114
|
+
{children}
|
|
115
|
+
</a>
|
|
116
|
+
),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SegmentedNav — a segmented control whose segments are LINKS, not buttons.
|
|
8
|
+
*
|
|
9
|
+
* Use this when the segments are separate routes. For segments that swap
|
|
10
|
+
* content inside one page, use `<Tabs>` (Radix tab-panel semantics) or
|
|
11
|
+
* `<ToggleGroup>` (button semantics) instead — a nav landmark with
|
|
12
|
+
* `aria-current="page"` is only correct when each segment is a destination.
|
|
13
|
+
*
|
|
14
|
+
* FRAMEWORK-AGNOSTIC BY DESIGN. This renders a plain `<a>` unless the caller
|
|
15
|
+
* passes `linkComponent`. It deliberately does NOT import `next/link` the way
|
|
16
|
+
* `link-button.tsx` does — that import is what quietly makes a component
|
|
17
|
+
* Next-only, and this one has to work in admin (Vite) too.
|
|
18
|
+
*
|
|
19
|
+
* Segments stretch to fill the row on mobile and shrink to their content from
|
|
20
|
+
* `lg` up, matching the `.mtog` / `.tog` split in the source design.
|
|
21
|
+
*
|
|
22
|
+
* The `w-full` below `lg` is what makes that sentence TRUE, and it is not
|
|
23
|
+
* redundant with the segments' own `flex-1`. `flex-1` only distributes the
|
|
24
|
+
* width the nav already has; the nav itself sizes to content whenever its
|
|
25
|
+
* parent is a flex row, so a caller placing this beside other controls got
|
|
26
|
+
* three segments hugging their labels with dead space alongside — the
|
|
27
|
+
* component quietly not doing the thing its own docblock claimed. Callers that
|
|
28
|
+
* genuinely want it to hug on mobile can pass `w-auto` through `className`,
|
|
29
|
+
* which wins via `cn`'s merge.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export type SegmentedNavItem = {
|
|
33
|
+
/** Stable identity, compared against `activeId`. */
|
|
34
|
+
id: string;
|
|
35
|
+
href: string;
|
|
36
|
+
label: string;
|
|
37
|
+
/** Rendered before the label. Size it at the call site (the design uses 15–16px). */
|
|
38
|
+
icon?: React.ReactNode;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The shape `linkComponent` must accept. `next/link` satisfies it structurally,
|
|
43
|
+
* so callers can pass it directly.
|
|
44
|
+
*/
|
|
45
|
+
export type SegmentedNavLinkProps = {
|
|
46
|
+
href: string;
|
|
47
|
+
className?: string;
|
|
48
|
+
children: React.ReactNode;
|
|
49
|
+
"aria-current"?: "page";
|
|
50
|
+
onClick?: () => void;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function DefaultLink({ href, ...props }: SegmentedNavLinkProps) {
|
|
54
|
+
return <a href={href} {...props} />;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type SegmentedNavProps = {
|
|
58
|
+
items: SegmentedNavItem[];
|
|
59
|
+
/** `id` of the segment for the current route. */
|
|
60
|
+
activeId: string;
|
|
61
|
+
/** Accessible name for the nav landmark. Translated by the caller. */
|
|
62
|
+
label: string;
|
|
63
|
+
/** Defaults to a plain `<a>`. Pass `next/link` in a Next app. */
|
|
64
|
+
linkComponent?: React.ComponentType<SegmentedNavLinkProps>;
|
|
65
|
+
/** Fired with the segment id on click — analytics, haptics. Never navigation. */
|
|
66
|
+
onSelect?: (id: string) => void;
|
|
67
|
+
className?: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export function SegmentedNav({
|
|
71
|
+
items,
|
|
72
|
+
activeId,
|
|
73
|
+
label,
|
|
74
|
+
linkComponent: LinkComponent = DefaultLink,
|
|
75
|
+
onSelect,
|
|
76
|
+
className,
|
|
77
|
+
}: SegmentedNavProps) {
|
|
78
|
+
return (
|
|
79
|
+
<nav
|
|
80
|
+
aria-label={label}
|
|
81
|
+
className={cn(
|
|
82
|
+
"flex w-full items-center gap-0.5 rounded-lg border border-border bg-card p-0.5",
|
|
83
|
+
"lg:inline-flex lg:w-fit",
|
|
84
|
+
className
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
{items.map((item) => {
|
|
88
|
+
const isActive = item.id === activeId;
|
|
89
|
+
return (
|
|
90
|
+
<LinkComponent
|
|
91
|
+
key={item.id}
|
|
92
|
+
href={item.href}
|
|
93
|
+
aria-current={isActive ? "page" : undefined}
|
|
94
|
+
onClick={onSelect ? () => onSelect(item.id) : undefined}
|
|
95
|
+
className={cn(
|
|
96
|
+
"inline-flex h-9 flex-1 items-center justify-center gap-1.5 rounded-md px-3",
|
|
97
|
+
"text-[13px] font-semibold lg:flex-none lg:text-sm",
|
|
98
|
+
"transition-colors duration-150 motion-reduce:transition-none",
|
|
99
|
+
"cursor-pointer touch-manipulation select-none",
|
|
100
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
101
|
+
isActive
|
|
102
|
+
? "bg-primary/10 text-primary"
|
|
103
|
+
: "text-muted-foreground hover:text-foreground"
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
{item.icon}
|
|
107
|
+
{item.label}
|
|
108
|
+
</LinkComponent>
|
|
109
|
+
);
|
|
110
|
+
})}
|
|
111
|
+
</nav>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { SkeletonCard } from "./skeleton-card";
|
|
3
3
|
|
|
4
|
-
const meta = {
|
|
4
|
+
const meta: Meta<typeof SkeletonCard> = {
|
|
5
5
|
title: "Components/SkeletonCard",
|
|
6
6
|
component: SkeletonCard,
|
|
7
7
|
tags: ["autodocs"],
|
|
8
|
-
}
|
|
8
|
+
};
|
|
9
9
|
|
|
10
10
|
export default meta;
|
|
11
|
-
type Story = StoryObj<typeof
|
|
11
|
+
type Story = StoryObj<typeof SkeletonCard>;
|
|
12
12
|
|
|
13
13
|
export const Experience: Story = {
|
|
14
14
|
render: () => <SkeletonCard variant="experience" className="w-[350px]" />,
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
2
|
import { cn } from "../lib/utils";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The shimmer stops under `prefers-reduced-motion: reduce`.
|
|
6
|
+
*
|
|
7
|
+
* It did not, and this is the one animation in the package that is guaranteed
|
|
8
|
+
* to be on screen during every slow load, on every surface, in multiples — a
|
|
9
|
+
* list of twelve skeleton rows is twelve sweeping gradients. A reader who has
|
|
10
|
+
* asked the OS for less motion is exactly the reader least served by that.
|
|
11
|
+
*
|
|
12
|
+
* `motion-reduce:` on the `before:` pseudo-element rather than a media query in
|
|
13
|
+
* a style block, because the animation is declared as a utility class here and
|
|
14
|
+
* a class beats a class predictably; the components that animate via an inline
|
|
15
|
+
* `animation-play-state` need the media-query form instead, and say so.
|
|
16
|
+
*
|
|
17
|
+
* The skeleton still reads as a skeleton without the sweep — it keeps its muted
|
|
18
|
+
* fill and its geometry, which is what actually communicates "content is coming
|
|
19
|
+
* and it will be this shape".
|
|
20
|
+
*/
|
|
4
21
|
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
5
22
|
return (
|
|
6
23
|
<div
|
|
@@ -8,6 +25,7 @@ function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>)
|
|
|
8
25
|
className={cn(
|
|
9
26
|
"relative overflow-hidden rounded-xl bg-muted",
|
|
10
27
|
"before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-foreground/[0.04] before:to-transparent",
|
|
28
|
+
"motion-reduce:before:animate-none motion-reduce:before:hidden",
|
|
11
29
|
className
|
|
12
30
|
)}
|
|
13
31
|
{...props}
|
|
@@ -10,7 +10,7 @@ const meta = {
|
|
|
10
10
|
} satisfies Meta<typeof SlidingPanels>;
|
|
11
11
|
|
|
12
12
|
export default meta;
|
|
13
|
-
type Story = StoryObj<typeof
|
|
13
|
+
type Story = StoryObj<typeof SlidingPanels>;
|
|
14
14
|
|
|
15
15
|
function SlidingPanelsDemo() {
|
|
16
16
|
const [currentPanel, setCurrentPanel] = useState("panel-1");
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { StickyContainer } from "./sticky-container";
|
|
3
3
|
|
|
4
|
-
const meta = {
|
|
4
|
+
const meta: Meta<typeof StickyContainer> = {
|
|
5
5
|
title: "Components/StickyContainer",
|
|
6
6
|
component: StickyContainer,
|
|
7
7
|
tags: ["autodocs"],
|
|
8
|
-
}
|
|
8
|
+
};
|
|
9
9
|
|
|
10
10
|
export default meta;
|
|
11
|
-
type Story = StoryObj<typeof
|
|
11
|
+
type Story = StoryObj<typeof StickyContainer>;
|
|
12
12
|
|
|
13
13
|
export const Default: Story = {
|
|
14
14
|
render: () => (
|
package/src/components/tag.tsx
CHANGED
|
@@ -16,9 +16,19 @@ const tagVariants = cva(
|
|
|
16
16
|
warning: "border-warning/30 bg-warning/10 text-warning",
|
|
17
17
|
destructive: "border-destructive/30 bg-destructive/10 text-destructive",
|
|
18
18
|
outline: "border-border bg-transparent text-foreground",
|
|
19
|
+
/**
|
|
20
|
+
* Decorative metadata — a tag the reader scans past, not one that
|
|
21
|
+
* labels a state. `outline` at full `text-foreground` is too loud
|
|
22
|
+
* repeated a dozen times down a list, and `default`'s `bg-muted` fill
|
|
23
|
+
* is reserved: in the event row family a FILLED chip means live.
|
|
24
|
+
*/
|
|
25
|
+
quiet: "border-border bg-transparent text-muted-foreground",
|
|
19
26
|
},
|
|
20
27
|
size: {
|
|
21
|
-
|
|
28
|
+
// 22px/11px, matching the event row family's chip. 10px was the
|
|
29
|
+
// smallest type in the package and below the floor used everywhere
|
|
30
|
+
// else; there is no surface that wants it.
|
|
31
|
+
sm: "h-[22px] px-2 text-[11px]",
|
|
22
32
|
default: "h-6 px-2.5",
|
|
23
33
|
lg: "h-7 px-3 text-sm",
|
|
24
34
|
},
|
|
@@ -0,0 +1,206 @@
|
|
|
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
|
+
};
|