@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.
Files changed (194) hide show
  1. package/dist/chunk-2WDJR6K7.js +76 -0
  2. package/dist/chunk-637L7XQ3.js +85 -0
  3. package/dist/chunk-6XMYEZVR.js +26 -0
  4. package/dist/chunk-DNSXMHYX.js +8 -0
  5. package/dist/chunk-FDITZ2VM.js +16 -0
  6. package/dist/chunk-GPH6L4DX.js +55 -0
  7. package/dist/{chunk-N2KNBPAQ.js → chunk-QXHMPJ35.js} +1 -0
  8. package/dist/chunk-TT3WFS6S.js +379 -0
  9. package/dist/chunk-UXWAOISG.js +170 -0
  10. package/dist/{chunk-UZ3PCIE5.js → chunk-XWMRABZW.js} +2 -2
  11. package/dist/{chunk-VBA45GLP.js → chunk-ZDAHKI3A.js} +12 -2
  12. package/dist/components/alert.d.ts +2 -2
  13. package/dist/components/badge.d.ts +1 -1
  14. package/dist/components/button.d.ts +1 -1
  15. package/dist/components/card.d.ts +2 -2
  16. package/dist/components/card.js +4 -127
  17. package/dist/components/category-deck.d.ts +50 -0
  18. package/dist/components/category-deck.js +128 -0
  19. package/dist/components/command.d.ts +15 -15
  20. package/dist/components/concept-card.d.ts +43 -0
  21. package/dist/components/concept-card.js +81 -0
  22. package/dist/components/coverflow.d.ts +123 -0
  23. package/dist/components/coverflow.js +300 -0
  24. package/dist/components/date-input.d.ts +1 -1
  25. package/dist/components/day-rail-chip.d.ts +82 -0
  26. package/dist/components/day-rail-chip.js +105 -0
  27. package/dist/components/drift-rail.d.ts +28 -0
  28. package/dist/components/drift-rail.js +3 -0
  29. package/dist/components/event-card.d.ts +97 -0
  30. package/dist/components/event-card.js +161 -0
  31. package/dist/components/event-compact-row.d.ts +72 -0
  32. package/dist/components/event-compact-row.js +133 -0
  33. package/dist/components/event-day-header.d.ts +65 -0
  34. package/dist/components/event-day-header.js +95 -0
  35. package/dist/components/event-day-separator.d.ts +46 -0
  36. package/dist/components/event-day-separator.js +50 -0
  37. package/dist/components/event-detail-panel.d.ts +80 -0
  38. package/dist/components/event-detail-panel.js +251 -0
  39. package/dist/components/event-hub-hero.d.ts +34 -0
  40. package/dist/components/event-hub-hero.js +57 -0
  41. package/dist/components/event-index-row.d.ts +68 -0
  42. package/dist/components/event-index-row.js +84 -0
  43. package/dist/components/event-list-row-wide.d.ts +101 -0
  44. package/dist/components/event-list-row-wide.js +218 -0
  45. package/dist/components/event-list-row.d.ts +82 -0
  46. package/dist/components/event-list-row.js +173 -0
  47. package/dist/components/event-poster-fallback.d.ts +20 -0
  48. package/dist/components/event-poster-fallback.js +3 -0
  49. package/dist/components/event-types.d.ts +118 -0
  50. package/dist/components/event-types.js +2 -0
  51. package/dist/components/events-empty-state.d.ts +44 -0
  52. package/dist/components/events-empty-state.js +73 -0
  53. package/dist/components/facet-group.d.ts +93 -0
  54. package/dist/components/facet-group.js +222 -0
  55. package/dist/components/featured-rail-item.d.ts +42 -0
  56. package/dist/components/featured-rail-item.js +88 -0
  57. package/dist/components/hour-band.d.ts +68 -0
  58. package/dist/components/hour-band.js +53 -0
  59. package/dist/components/hour-group.d.ts +67 -0
  60. package/dist/components/hour-group.js +59 -0
  61. package/dist/components/icon.d.ts +18 -0
  62. package/dist/components/icon.js +31 -0
  63. package/dist/components/item.d.ts +2 -2
  64. package/dist/components/motion-icon-controls.d.ts +26 -0
  65. package/dist/components/motion-icon-controls.js +4 -0
  66. package/dist/components/motion-icon-set.d.ts +27 -0
  67. package/dist/components/motion-icon-set.js +139 -0
  68. package/dist/components/motion-icon.d.ts +44 -0
  69. package/dist/components/motion-icon.js +3 -0
  70. package/dist/components/multi-select-combobox.js +1 -1
  71. package/dist/components/place-about-tab.js +3 -3
  72. package/dist/components/place-details-mobile.js +3 -3
  73. package/dist/components/poster-stack.d.ts +30 -0
  74. package/dist/components/poster-stack.js +104 -0
  75. package/dist/components/poster-tape.d.ts +36 -0
  76. package/dist/components/poster-tape.js +64 -0
  77. package/dist/components/poster-wall.d.ts +25 -0
  78. package/dist/components/poster-wall.js +183 -0
  79. package/dist/components/quiet-stretch.d.ts +43 -0
  80. package/dist/components/quiet-stretch.js +54 -0
  81. package/dist/components/section-header.d.ts +53 -0
  82. package/dist/components/section-header.js +63 -0
  83. package/dist/components/segmented-nav.d.ts +62 -0
  84. package/dist/components/segmented-nav.js +54 -0
  85. package/dist/components/skeleton-card.js +1 -1
  86. package/dist/components/skeleton.d.ts +17 -0
  87. package/dist/components/skeleton.js +1 -1
  88. package/dist/components/spinner.d.ts +2 -2
  89. package/dist/components/tag-group.js +3 -26
  90. package/dist/components/tag.d.ts +2 -2
  91. package/dist/components/tag.js +1 -1
  92. package/dist/components/text.d.ts +3 -3
  93. package/dist/components/timeline-band.d.ts +46 -0
  94. package/dist/components/timeline-band.js +5 -0
  95. package/dist/components/venue-events-block.d.ts +143 -0
  96. package/dist/components/venue-events-block.js +222 -0
  97. package/dist/components/weekend-panel.d.ts +44 -0
  98. package/dist/components/weekend-panel.js +231 -0
  99. package/dist/lib/icon-registry.d.ts +58 -0
  100. package/dist/lib/icon-registry.js +39 -0
  101. package/package.json +2 -1
  102. package/src/components/accordion.stories.tsx +1 -1
  103. package/src/components/card.tsx +44 -1
  104. package/src/components/category-deck.stories.tsx +96 -0
  105. package/src/components/category-deck.tsx +227 -0
  106. package/src/components/combobox.stories.tsx +1 -1
  107. package/src/components/concept-card.stories.tsx +80 -0
  108. package/src/components/concept-card.tsx +143 -0
  109. package/src/components/coverflow.stories.tsx +240 -0
  110. package/src/components/coverflow.test.tsx +193 -0
  111. package/src/components/coverflow.tsx +550 -0
  112. package/src/components/currency-input.stories.tsx +3 -3
  113. package/src/components/day-rail-chip.stories.tsx +145 -0
  114. package/src/components/day-rail-chip.tsx +201 -0
  115. package/src/components/drift-rail.stories.tsx +92 -0
  116. package/src/components/drift-rail.tsx +86 -0
  117. package/src/components/empty-state.stories.tsx +3 -3
  118. package/src/components/event-card.stories.tsx +248 -0
  119. package/src/components/event-card.test.tsx +177 -0
  120. package/src/components/event-card.tsx +321 -0
  121. package/src/components/event-compact-row.stories.tsx +221 -0
  122. package/src/components/event-compact-row.tsx +268 -0
  123. package/src/components/event-day-header.stories.tsx +124 -0
  124. package/src/components/event-day-header.tsx +180 -0
  125. package/src/components/event-day-separator.stories.tsx +70 -0
  126. package/src/components/event-day-separator.tsx +95 -0
  127. package/src/components/event-detail-panel.stories.tsx +191 -0
  128. package/src/components/event-detail-panel.tsx +360 -0
  129. package/src/components/event-hub-hero.stories.tsx +66 -0
  130. package/src/components/event-hub-hero.tsx +110 -0
  131. package/src/components/event-index-row.stories.tsx +116 -0
  132. package/src/components/event-index-row.tsx +181 -0
  133. package/src/components/event-list-row-wide.stories.tsx +204 -0
  134. package/src/components/event-list-row-wide.tsx +386 -0
  135. package/src/components/event-list-row.stories.tsx +159 -0
  136. package/src/components/event-list-row.tsx +335 -0
  137. package/src/components/event-poster-fallback.stories.tsx +52 -0
  138. package/src/components/event-poster-fallback.tsx +31 -0
  139. package/src/components/event-types.ts +133 -0
  140. package/src/components/events-empty-state.stories.tsx +59 -0
  141. package/src/components/events-empty-state.tsx +131 -0
  142. package/src/components/facet-group.stories.tsx +247 -0
  143. package/src/components/facet-group.test.tsx +225 -0
  144. package/src/components/facet-group.tsx +352 -0
  145. package/src/components/featured-rail-item.stories.tsx +86 -0
  146. package/src/components/featured-rail-item.tsx +153 -0
  147. package/src/components/form.stories.tsx +1 -1
  148. package/src/components/hour-band.stories.tsx +159 -0
  149. package/src/components/hour-band.tsx +126 -0
  150. package/src/components/hour-group.stories.tsx +100 -0
  151. package/src/components/hour-group.tsx +124 -0
  152. package/src/components/icon.tsx +57 -0
  153. package/src/components/input-otp.stories.tsx +1 -1
  154. package/src/components/markdown-renderer.stories.tsx +1 -1
  155. package/src/components/motion-icon-controls.test.tsx +78 -0
  156. package/src/components/motion-icon-controls.tsx +552 -0
  157. package/src/components/motion-icon-set.stories.tsx +185 -0
  158. package/src/components/motion-icon-set.tsx +219 -0
  159. package/src/components/motion-icon.test.tsx +159 -0
  160. package/src/components/motion-icon.tsx +163 -0
  161. package/src/components/multi-select-combobox.stories.tsx +1 -1
  162. package/src/components/phone-frame.stories.tsx +1 -1
  163. package/src/components/poster-stack.stories.tsx +68 -0
  164. package/src/components/poster-stack.tsx +155 -0
  165. package/src/components/poster-tape.stories.tsx +70 -0
  166. package/src/components/poster-tape.tsx +122 -0
  167. package/src/components/poster-wall.stories.tsx +95 -0
  168. package/src/components/poster-wall.tsx +220 -0
  169. package/src/components/quiet-stretch.stories.tsx +62 -0
  170. package/src/components/quiet-stretch.tsx +115 -0
  171. package/src/components/rating.stories.tsx +3 -3
  172. package/src/components/searchable-select.stories.tsx +1 -1
  173. package/src/components/section-header.stories.tsx +70 -0
  174. package/src/components/section-header.tsx +134 -0
  175. package/src/components/segmented-nav.stories.tsx +118 -0
  176. package/src/components/segmented-nav.tsx +113 -0
  177. package/src/components/selection-bar.stories.tsx +1 -1
  178. package/src/components/skeleton-card.stories.tsx +3 -3
  179. package/src/components/skeleton.tsx +18 -0
  180. package/src/components/sliding-panels.stories.tsx +1 -1
  181. package/src/components/stepper.stories.tsx +1 -1
  182. package/src/components/sticky-container.stories.tsx +3 -3
  183. package/src/components/tag.tsx +11 -1
  184. package/src/components/timeline-band.stories.tsx +206 -0
  185. package/src/components/timeline-band.tsx +207 -0
  186. package/src/components/toggle-group.stories.tsx +1 -1
  187. package/src/components/tooltip.stories.tsx +2 -2
  188. package/src/components/venue-events-block.stories.tsx +216 -0
  189. package/src/components/venue-events-block.tsx +408 -0
  190. package/src/components/weekend-panel.stories.tsx +92 -0
  191. package/src/components/weekend-panel.tsx +322 -0
  192. package/src/globals.css +19 -0
  193. package/src/lib/icon-registry.ts +139 -0
  194. 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
+ }
@@ -9,7 +9,7 @@ const meta = {
9
9
  } satisfies Meta<typeof SelectionBar>;
10
10
 
11
11
  export default meta;
12
- type Story = StoryObj<typeof meta>;
12
+ type Story = StoryObj<typeof SelectionBar>;
13
13
 
14
14
  export const Default: Story = {
15
15
  render: () => (
@@ -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
- } satisfies Meta<typeof SkeletonCard>;
8
+ };
9
9
 
10
10
  export default meta;
11
- type Story = StoryObj<typeof meta>;
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 meta>;
13
+ type Story = StoryObj<typeof SlidingPanels>;
14
14
 
15
15
  function SlidingPanelsDemo() {
16
16
  const [currentPanel, setCurrentPanel] = useState("panel-1");
@@ -10,7 +10,7 @@ const meta = {
10
10
  } satisfies Meta<typeof Stepper>;
11
11
 
12
12
  export default meta;
13
- type Story = StoryObj<typeof meta>;
13
+ type Story = StoryObj<typeof Stepper>;
14
14
 
15
15
  const steps = [
16
16
  { id: "account", title: "Account" },
@@ -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
- } satisfies Meta<typeof StickyContainer>;
8
+ };
9
9
 
10
10
  export default meta;
11
- type Story = StoryObj<typeof meta>;
11
+ type Story = StoryObj<typeof StickyContainer>;
12
12
 
13
13
  export const Default: Story = {
14
14
  render: () => (
@@ -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
- sm: "h-5 px-2 text-[10px]",
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
+ };