@bearmenu/ui 0.8.56 → 0.8.58

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/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "@bearmenu/ui",
3
- "version": "0.8.56",
4
- "packageManager": "pnpm@10.28.1",
5
- "description": "BearMenu design system \u2014 shared UI tokens, primitives, and components",
3
+ "version": "0.8.58",
4
+ "description": "BearMenu design system — shared UI tokens, primitives, and components",
6
5
  "license": "UNLICENSED",
7
6
  "repository": {
8
7
  "type": "git",
@@ -17,226 +16,20 @@
17
16
  "src"
18
17
  ],
19
18
  "exports": {
20
- "./components/accordion": "./src/components/accordion.tsx",
21
- "./components/action-bar": "./src/components/action-bar.tsx",
22
- "./components/activity-row": "./src/components/activity-row.tsx",
23
- "./components/alert": "./src/components/alert.tsx",
24
- "./components/alert-dialog": "./src/components/alert-dialog.tsx",
25
- "./components/aspect-ratio": "./src/components/aspect-ratio.tsx",
26
- "./components/avatar": "./src/components/avatar.tsx",
27
- "./components/badge": "./src/components/badge.tsx",
28
- "./components/banner": "./src/components/banner.tsx",
29
- "./components/booking-row": "./src/components/booking-row.tsx",
30
- "./components/booking-summary-card": "./src/components/booking-summary-card.tsx",
31
- "./components/breadcrumb": "./src/components/breadcrumb.tsx",
32
- "./components/button": "./src/components/button.tsx",
33
- "./components/button-group": "./src/components/button-group.tsx",
34
- "./components/calendar": "./src/components/calendar.tsx",
35
- "./components/card": "./src/components/card.tsx",
36
- "./components/carousel": "./src/components/carousel.tsx",
37
- "./components/category-deck": "./src/components/category-deck.tsx",
38
- "./components/checkbox": "./src/components/checkbox.tsx",
39
- "./components/circular-progress": "./src/components/circular-progress.tsx",
40
- "./components/coach-marks": "./src/components/coach-marks.tsx",
41
- "./components/collapsible": "./src/components/collapsible.tsx",
42
- "./components/combobox": "./src/components/combobox.tsx",
43
- "./components/command": "./src/components/command.tsx",
44
- "./components/concept-card": "./src/components/concept-card.tsx",
45
- "./components/concept-compare": "./src/components/concept-compare.tsx",
46
- "./components/concept-compare-rail": "./src/components/concept-compare-rail.tsx",
47
- "./components/concept-convergence": "./src/components/concept-convergence.tsx",
48
- "./components/convergence": "./src/components/convergence.tsx",
49
- "./components/coverflow": "./src/components/coverflow.tsx",
50
- "./components/currency-input": "./src/components/currency-input.tsx",
51
- "./components/date-input": "./src/components/date-input.tsx",
52
- "./components/day-rail-chip": "./src/components/day-rail-chip.tsx",
53
- "./components/description-list": "./src/components/description-list.tsx",
54
- "./components/dialog": "./src/components/dialog.tsx",
55
- "./components/doorway-tiles": "./src/components/doorway-tiles.tsx",
56
- "./components/drawer": "./src/components/drawer.tsx",
57
- "./components/drift-rail": "./src/components/drift-rail.tsx",
58
- "./components/dropdown-menu": "./src/components/dropdown-menu.tsx",
59
- "./components/empty-state": "./src/components/empty-state.tsx",
60
- "./components/event-card": "./src/components/event-card.tsx",
61
- "./components/event-compact-row": "./src/components/event-compact-row.tsx",
62
- "./components/event-day-header": "./src/components/event-day-header.tsx",
63
- "./components/event-day-separator": "./src/components/event-day-separator.tsx",
64
- "./components/event-detail-panel": "./src/components/event-detail-panel.tsx",
65
- "./components/event-hub-hero": "./src/components/event-hub-hero.tsx",
66
- "./components/event-index-row": "./src/components/event-index-row.tsx",
67
- "./components/event-list-row": "./src/components/event-list-row.tsx",
68
- "./components/event-list-row-wide": "./src/components/event-list-row-wide.tsx",
69
- "./components/event-poster-fallback": "./src/components/event-poster-fallback.tsx",
70
- "./components/event-tonight-strip": "./src/components/event-tonight-strip.tsx",
71
- "./components/event-types": "./src/components/event-types.ts",
72
- "./components/events-empty-state": "./src/components/events-empty-state.tsx",
73
- "./components/explainer-steps": "./src/components/explainer-steps.tsx",
74
- "./components/facet-group": "./src/components/facet-group.tsx",
75
- "./components/featured-rail-item": "./src/components/featured-rail-item.tsx",
76
- "./components/field": "./src/components/field.tsx",
77
- "./components/filter-category-row": "./src/components/filter-category-row.tsx",
78
- "./components/form": "./src/components/form.tsx",
79
- "./components/glass-ground": "./src/components/glass-ground.tsx",
80
- "./components/grade-atom": "./src/components/grade-atom.tsx",
81
- "./components/haptic-switch": "./src/components/haptic-switch.tsx",
82
- "./components/hold-countdown": "./src/components/hold-countdown.tsx",
83
- "./components/hub-surface": "./src/components/hub-surface.tsx",
84
- "./components/hue-chip": "./src/components/hue-chip.tsx",
85
- "./components/icon": "./src/components/icon.tsx",
86
- "./components/input": "./src/components/input.tsx",
87
- "./components/input-group": "./src/components/input-group.tsx",
88
- "./components/input-otp": "./src/components/input-otp.tsx",
89
- "./components/item": "./src/components/item.tsx",
90
- "./components/kbd": "./src/components/kbd.tsx",
91
- "./components/label": "./src/components/label.tsx",
92
- "./components/kitchen-dossier": "./src/components/kitchen-dossier.tsx",
93
- "./components/lazy-markdown": "./src/components/lazy-markdown.tsx",
94
- "./components/link-button": "./src/components/link-button.tsx",
95
- "./components/markdown-renderer": "./src/components/markdown-renderer.tsx",
96
- "./components/menu-dead-end": "./src/components/menu-dead-end.tsx",
97
- "./components/menu-family-block": "./src/components/menu-family-block.tsx",
98
- "./components/menu-family-card": "./src/components/menu-family-card.tsx",
99
- "./components/menu-field": "./src/components/menu-field.tsx",
100
- "./components/menu-filter-chips": "./src/components/menu-filter-chips.tsx",
101
- "./components/menu-group-rows": "./src/components/menu-group-rows.tsx",
102
- "./components/menu-hub-hero": "./src/components/menu-hub-hero.tsx",
103
- "./components/menu-index-header": "./src/components/menu-index-header.tsx",
104
- "./components/menu-index-list": "./src/components/menu-index-list.tsx",
105
- "./components/menu-intent-hero": "./src/components/menu-intent-hero.tsx",
106
- "./components/menu-item": "./src/components/menu-item.tsx",
107
- "./components/menu-pick-row": "./src/components/menu-pick-row.tsx",
108
- "./components/menu-kitchen-list": "./src/components/menu-kitchen-list.tsx",
109
- "./components/menu-mocks": "./src/components/menu-mocks.ts",
110
- "./components/menu-types": "./src/components/menu-types.ts",
111
- "./components/motion-icon": "./src/components/motion-icon.tsx",
112
- "./components/motion-icon-actions": "./src/components/motion-icon-actions.tsx",
113
- "./components/motion-icon-controls": "./src/components/motion-icon-controls.tsx",
114
- "./components/motion-icon-set": "./src/components/motion-icon-set.tsx",
115
- "./components/multi-select-combobox": "./src/components/multi-select-combobox.tsx",
116
- "./components/multi-select-menu": "./src/components/multi-select-menu.tsx",
117
- "./components/native-select": "./src/components/native-select.tsx",
118
- "./components/open-now-list": "./src/components/open-now-list.tsx",
119
- "./components/pagination": "./src/components/pagination.tsx",
120
- "./components/pan-zoom-viewport": "./src/components/pan-zoom-viewport.tsx",
121
- "./components/party-counter": "./src/components/party-counter.tsx",
122
- "./components/phone-frame": "./src/components/phone-frame.tsx",
123
- "./components/photo-ground": "./src/components/photo-ground.tsx",
124
- "./components/place-about-tab": "./src/components/place-about-tab.tsx",
125
- "./components/place-card": "./src/components/place-card.tsx",
126
- "./components/place-details-mobile": "./src/components/place-details-mobile.tsx",
127
- "./components/place-schedule-week": "./src/components/place-schedule-week.tsx",
128
- "./components/place-types": "./src/components/place-types.ts",
129
- "./components/popover": "./src/components/popover.tsx",
130
- "./components/poster-stack": "./src/components/poster-stack.tsx",
131
- "./components/poster-tape": "./src/components/poster-tape.tsx",
132
- "./components/poster-wall": "./src/components/poster-wall.tsx",
133
- "./components/progress": "./src/components/progress.tsx",
134
- "./components/qa-block": "./src/components/qa-block.tsx",
135
- "./components/qr-panel": "./src/components/qr-panel.tsx",
136
- "./components/quiet-stretch": "./src/components/quiet-stretch.tsx",
137
- "./components/radio-group": "./src/components/radio-group.tsx",
138
- "./components/reconcile-bar": "./src/components/reconcile-bar.tsx",
139
- "./components/responsive-overlay": "./src/components/responsive-overlay.tsx",
140
- "./components/rating": "./src/components/rating.tsx",
141
- "./components/scroll-area": "./src/components/scroll-area.tsx",
142
- "./components/scroll-fade": "./src/components/scroll-fade.tsx",
143
- "./components/search-bar": "./src/components/search-bar.tsx",
144
- "./components/searchable-select": "./src/components/searchable-select.tsx",
145
- "./components/section-header": "./src/components/section-header.tsx",
146
- "./components/segmented-nav": "./src/components/segmented-nav.tsx",
147
- "./components/select": "./src/components/select.tsx",
148
- "./components/selection-bar": "./src/components/selection-bar.tsx",
149
- "./components/separator": "./src/components/separator.tsx",
150
- "./components/sheet": "./src/components/sheet.tsx",
151
- "./components/signature-chip": "./src/components/signature-chip.tsx",
152
- "./components/skeleton": "./src/components/skeleton.tsx",
153
- "./components/skeleton-card": "./src/components/skeleton-card.tsx",
154
- "./components/slider": "./src/components/slider.tsx",
155
- "./components/sliding-panels": "./src/components/sliding-panels.tsx",
156
- "./components/sonner": "./src/components/sonner.tsx",
157
- "./components/sort-option-list": "./src/components/sort-option-list.tsx",
158
- "./components/spinner": "./src/components/spinner.tsx",
159
- "./components/split-share-row": "./src/components/split-share-row.tsx",
160
- "./components/stat-banner": "./src/components/stat-banner.tsx",
161
- "./components/stepper": "./src/components/stepper.tsx",
162
- "./components/sticky-container": "./src/components/sticky-container.tsx",
163
- "./components/story": "./src/components/story.tsx",
164
- "./components/switch": "./src/components/switch.tsx",
165
- "./components/table": "./src/components/table.tsx",
166
- "./components/table-ledger-card": "./src/components/table-ledger-card.tsx",
167
- "./components/table-trigger-band": "./src/components/table-trigger-band.tsx",
168
- "./components/tabs": "./src/components/tabs.tsx",
169
- "./components/tag": "./src/components/tag.tsx",
170
- "./components/tag-group": "./src/components/tag-group.tsx",
171
- "./components/text": "./src/components/text.tsx",
172
- "./components/textarea": "./src/components/textarea.tsx",
173
- "./components/time-slot": "./src/components/time-slot.tsx",
174
- "./components/timeline-day": "./src/components/timeline-day.tsx",
175
- "./components/timeline-hour": "./src/components/timeline-hour.tsx",
176
- "./components/timeline-hour-puck": "./src/components/timeline-hour-puck.tsx",
177
- "./components/toggle": "./src/components/toggle.tsx",
178
- "./components/toggle-group": "./src/components/toggle-group.tsx",
179
- "./components/tooltip": "./src/components/tooltip.tsx",
180
- "./components/top-comparison": "./src/components/top-comparison.tsx",
181
- "./components/top-cross-link": "./src/components/top-cross-link.tsx",
182
- "./components/top-ranking-entry": "./src/components/top-ranking-entry.tsx",
183
- "./components/top-ranking-hero": "./src/components/top-ranking-hero.tsx",
184
- "./components/top-ranking-legend": "./src/components/top-ranking-legend.tsx",
185
- "./components/top-types": "./src/components/top-types.ts",
186
- "./components/use-mobile": "./src/components/use-mobile.tsx",
187
- "./components/venue-events-block": "./src/components/venue-events-block.tsx",
188
- "./components/venue-grade-dimensions": "./src/components/venue-grade-dimensions.tsx",
189
- "./components/venue-quotes": "./src/components/venue-quotes.tsx",
190
- "./components/venue-wall": "./src/components/venue-wall.tsx",
191
- "./components/weekday-chip": "./src/components/weekday-chip.tsx",
192
- "./components/weekday-theme-banner": "./src/components/weekday-theme-banner.tsx",
193
- "./components/weekend-panel": "./src/components/weekend-panel.tsx",
194
19
  "./globals.css": "./src/globals.css",
195
- "./hooks/use-drag-swipe": "./src/hooks/use-drag-swipe.ts",
196
- "./hooks/use-haptic": "./src/hooks/use-haptic.ts",
197
- "./hooks/use-media-query": "./src/hooks/use-media-query.ts",
198
- "./hooks/use-mobile": "./src/hooks/use-mobile.ts",
199
- "./hooks/use-mounted-theme": "./src/hooks/use-mounted-theme.ts",
200
- "./hooks/use-sticky": "./src/hooks/use-sticky.ts",
201
- "./lib/card-hover": "./src/lib/card-hover.ts",
202
- "./lib/grade": "./src/lib/grade.ts",
203
- "./lib/icon-depth": "./src/lib/icon-depth.ts",
204
- "./lib/icon-registry": "./src/lib/icon-registry.ts",
205
- "./lib/motion": "./src/lib/motion.ts",
206
- "./lib/overlay-pill": "./src/lib/overlay-pill.ts",
207
- "./lib/utils": "./src/lib/utils.ts",
208
- "./components/filter-option-chip": "./src/components/filter-option-chip.tsx",
209
- "./components/filter-option-group": "./src/components/filter-option-group.tsx",
210
- "./components/filter-panel-footer": "./src/components/filter-panel-footer.tsx"
211
- },
212
- "publishConfig": {
213
- "exports": {
214
- "./globals.css": "./src/globals.css",
215
- "./components/*": {
216
- "types": "./dist/components/*.d.ts",
217
- "import": "./dist/components/*.js"
218
- },
219
- "./hooks/*": {
220
- "types": "./dist/hooks/*.d.ts",
221
- "import": "./dist/hooks/*.js"
222
- },
223
- "./lib/*": {
224
- "types": "./dist/lib/*.d.ts",
225
- "import": "./dist/lib/*.js"
226
- }
20
+ "./components/*": {
21
+ "types": "./dist/components/*.d.ts",
22
+ "import": "./dist/components/*.js"
23
+ },
24
+ "./hooks/*": {
25
+ "types": "./dist/hooks/*.d.ts",
26
+ "import": "./dist/hooks/*.js"
27
+ },
28
+ "./lib/*": {
29
+ "types": "./dist/lib/*.d.ts",
30
+ "import": "./dist/lib/*.js"
227
31
  }
228
32
  },
229
- "scripts": {
230
- "storybook": "storybook dev -p 6006",
231
- "build-storybook": "storybook build",
232
- "build": "tsup",
233
- "dev": "tsup --watch",
234
- "typecheck": "tsc --noEmit",
235
- "test": "vitest run",
236
- "test:watch": "vitest",
237
- "clean": "rm -rf dist",
238
- "prepublishOnly": "pnpm run build"
239
- },
240
33
  "peerDependencies": {
241
34
  "@hookform/resolvers": "^3.0.0 || ^5.0.0",
242
35
  "@radix-ui/react-accordion": "*",
@@ -345,5 +138,15 @@
345
138
  "vaul": "^1.1.2",
346
139
  "vitest": "^4.1.5",
347
140
  "zod": "^3.25.67"
141
+ },
142
+ "scripts": {
143
+ "storybook": "storybook dev -p 6006",
144
+ "build-storybook": "storybook build",
145
+ "build": "tsup",
146
+ "dev": "tsup --watch",
147
+ "typecheck": "tsc --noEmit",
148
+ "test": "vitest run",
149
+ "test:watch": "vitest",
150
+ "clean": "rm -rf dist"
348
151
  }
349
- }
152
+ }
@@ -82,6 +82,13 @@ describe("WeekdayChip", () => {
82
82
  render(<WeekdayChip weekday="Tue" day="11" unavailable />);
83
83
  expect(screen.getByRole("button")).toBeDisabled();
84
84
  });
85
+
86
+ it("prints a detail line under the date only when given one", () => {
87
+ const { rerender, container } = render(<WeekdayChip weekday="Fri" day="13" detail="12" />);
88
+ expect(container.querySelector("[data-slot='weekday-chip-detail']")).toHaveTextContent("12");
89
+ rerender(<WeekdayChip weekday="Fri" day="13" />);
90
+ expect(container.querySelector("[data-slot='weekday-chip-detail']")).toBeNull();
91
+ });
85
92
  });
86
93
 
87
94
  describe("HoldCountdown", () => {
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { DayRailChip } from "./day-rail-chip";
4
+
5
+ const labels = { noEvents: "no events" };
6
+
7
+ describe("DayRailChip", () => {
8
+ // Without a label a screen reader read "Vin 1312": the date and the count
9
+ // as one number (bearmenu-fe HIG review, 2026-11-10).
10
+ it("speaks its aria-label instead of its text run together", () => {
11
+ render(
12
+ <DayRailChip
13
+ label="Vin"
14
+ dayNumber="13"
15
+ count={12}
16
+ labels={labels}
17
+ onClick={() => {}}
18
+ aria-label="Vineri 13 noiembrie, 12 evenimente"
19
+ />
20
+ );
21
+ expect(
22
+ screen.getByRole("button", { name: "Vineri 13 noiembrie, 12 evenimente" })
23
+ ).toBeInTheDocument();
24
+ });
25
+
26
+ it("names a link chip and an inert one the same way", () => {
27
+ const { rerender } = render(
28
+ <DayRailChip
29
+ label="Sâm"
30
+ dayNumber="14"
31
+ count={3}
32
+ labels={labels}
33
+ href="/ro/cluj-napoca/events?day=2026-11-14"
34
+ aria-label="Sâmbătă 14 noiembrie, 3 evenimente"
35
+ />
36
+ );
37
+ expect(
38
+ screen.getByRole("link", { name: "Sâmbătă 14 noiembrie, 3 evenimente" })
39
+ ).toBeInTheDocument();
40
+ rerender(
41
+ <DayRailChip
42
+ label="Lun"
43
+ dayNumber="16"
44
+ count={0}
45
+ labels={labels}
46
+ aria-label="Luni 16 noiembrie, niciun eveniment"
47
+ />
48
+ );
49
+ expect(
50
+ screen.getByRole("group", { name: "Luni 16 noiembrie, niciun eveniment" })
51
+ ).toBeInTheDocument();
52
+ });
53
+
54
+ it("keeps its text as the name when no label is given", () => {
55
+ render(<DayRailChip label="Azi" dayNumber="10" count={7} labels={labels} onClick={() => {}} />);
56
+ expect(screen.getByRole("button")).toHaveTextContent("Azi 107");
57
+ });
58
+ });
@@ -53,6 +53,7 @@ export type DayRailChipLinkProps = {
53
53
  className?: string;
54
54
  children: React.ReactNode;
55
55
  "aria-current"?: "page";
56
+ "aria-label"?: string;
56
57
  onClick?: () => void;
57
58
  };
58
59
 
@@ -75,6 +76,12 @@ export type DayRailChipProps = {
75
76
  linkComponent?: React.ComponentType<DayRailChipLinkProps>;
76
77
  onClick?: () => void;
77
78
  className?: string;
79
+ /**
80
+ * The day for assistive tech — "Vineri 13 noiembrie, 12 evenimente". Without
81
+ * it a screen reader reads the chip's text run together: "Vin 1312", the
82
+ * date and the count as one number (bearmenu-fe HIG review, 2026-11-10).
83
+ */
84
+ "aria-label"?: string;
78
85
  };
79
86
 
80
87
  export function DayRailChip({
@@ -88,6 +95,7 @@ export function DayRailChip({
88
95
  linkComponent: LinkComponent = DefaultLink,
89
96
  onClick,
90
97
  className,
98
+ "aria-label": ariaLabel,
91
99
  }: DayRailChipProps) {
92
100
  const hasEvents = count > 0;
93
101
 
@@ -160,6 +168,7 @@ export function DayRailChip({
160
168
  href={href}
161
169
  className={rootClasses}
162
170
  aria-current={isActive ? "page" : undefined}
171
+ aria-label={ariaLabel}
163
172
  onClick={onClick}
164
173
  >
165
174
  {body}
@@ -176,6 +185,7 @@ export function DayRailChip({
176
185
  // toggle semantic was right; it now navigates, and this is the current
177
186
  // day in a set of days.
178
187
  aria-current={isActive ? "true" : undefined}
188
+ aria-label={ariaLabel}
179
189
  onClick={onClick}
180
190
  >
181
191
  {body}
@@ -183,7 +193,13 @@ export function DayRailChip({
183
193
  );
184
194
  }
185
195
 
186
- return <div className={rootClasses}>{body}</div>;
196
+ // An inert day has no role to carry a label; `role="group"` gives the name
197
+ // somewhere to live without inventing an interaction.
198
+ return (
199
+ <div className={rootClasses} role={ariaLabel ? "group" : undefined} aria-label={ariaLabel}>
200
+ {body}
201
+ </div>
202
+ );
187
203
  }
188
204
 
189
205
  export type DayRailChipSkeletonProps = {
@@ -0,0 +1,166 @@
1
+ import * as React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { Separator } from "./separator";
4
+ import { Skeleton } from "./skeleton";
5
+ import { TimeSlot, type TimeSlotFit } from "./time-slot";
6
+ import { TimeSlotRail } from "./time-slot-rail";
7
+
8
+ const meta: Meta<typeof TimeSlotRail> = {
9
+ title: "Booking/TimeSlotRail",
10
+ component: TimeSlotRail,
11
+ tags: ["autodocs"],
12
+ parameters: {
13
+ docs: {
14
+ description: {
15
+ component:
16
+ "A single row of touch-size TimeSlots that scrolls sideways. Arrow keys move focus, Enter or Space picks, the selected chip is centred on mount. Every string is a prop.",
17
+ },
18
+ },
19
+ },
20
+ };
21
+
22
+ export default meta;
23
+ type Story = StoryObj<typeof TimeSlotRail>;
24
+
25
+ type Slot = { time: string; fit: TimeSlotFit; meta: string };
26
+
27
+ const SLOTS: Slot[] = [
28
+ { time: "17:00", fit: "fits", meta: "R1" },
29
+ { time: "17:30", fit: "fits", meta: "R1" },
30
+ { time: "18:00", fit: "fits", meta: "R1+R2" },
31
+ { time: "18:30", fit: "tight", meta: "Join R3+R4" },
32
+ { time: "19:00", fit: "fits", meta: "R1+R2" },
33
+ { time: "19:30", fit: "fits", meta: "R1+R2" },
34
+ { time: "20:00", fit: "tight", meta: "Join R3+R4" },
35
+ { time: "20:30", fit: "none", meta: "Full" },
36
+ { time: "21:00", fit: "none", meta: "Full" },
37
+ { time: "21:30", fit: "tight", meta: "Join R5+R6" },
38
+ { time: "22:00", fit: "fits", meta: "R2" },
39
+ { time: "22:30", fit: "fits", meta: "R2" },
40
+ ];
41
+
42
+ const SCROLL_LABELS = { previous: "Earlier times", next: "Later times" };
43
+
44
+ function Chips({
45
+ selected,
46
+ onSelect,
47
+ disabled,
48
+ fit,
49
+ }: {
50
+ selected: string;
51
+ onSelect: (time: string) => void;
52
+ disabled?: boolean;
53
+ fit?: TimeSlotFit;
54
+ }) {
55
+ return (
56
+ <>
57
+ {SLOTS.map((slot) => (
58
+ <TimeSlot
59
+ key={slot.time}
60
+ size="touch"
61
+ time={slot.time}
62
+ fit={fit ?? slot.fit}
63
+ meta={fit === "none" ? "Full" : slot.meta}
64
+ selected={selected === slot.time}
65
+ disabled={disabled}
66
+ onSelect={() => onSelect(slot.time)}
67
+ aria-label={`${slot.time}, ${slot.meta}`}
68
+ className="w-[84px] shrink-0 snap-center"
69
+ />
70
+ ))}
71
+ </>
72
+ );
73
+ }
74
+
75
+ function Inset({ children, dark }: { children: React.ReactNode; dark?: boolean }) {
76
+ return (
77
+ <div
78
+ className={`${dark ? "dark " : ""}w-[343px] rounded-xl bg-background p-1.5 text-foreground`}
79
+ >
80
+ {children}
81
+ </div>
82
+ );
83
+ }
84
+
85
+ function Interactive({ dark, disabled }: { dark?: boolean; disabled?: boolean }) {
86
+ const [selected, setSelected] = React.useState("19:30");
87
+ return (
88
+ <Inset dark={dark}>
89
+ <TimeSlotRail aria-label="Start time on Wed 18 Nov" scrollLabels={SCROLL_LABELS}>
90
+ <Chips selected={selected} onSelect={setSelected} disabled={disabled} />
91
+ </TimeSlotRail>
92
+ </Inset>
93
+ );
94
+ }
95
+
96
+ /** The pick bar's inset at 375px: 3.7 chips show, the partial one says "scroll". */
97
+ export const Default: Story = { render: () => <Interactive /> };
98
+
99
+ export const Dark: Story = { render: () => <Interactive dark /> };
100
+
101
+ /** The whole rail locks while the write is out. */
102
+ export const Locked: Story = { render: () => <Interactive disabled /> };
103
+
104
+ /** Every chip in one fit state, to compare the three side by side. */
105
+ export const FitStates: Story = {
106
+ render: () => (
107
+ <div className="flex flex-col gap-3">
108
+ {(["fits", "tight", "none"] as const).map((fit) => (
109
+ <Inset key={fit}>
110
+ <TimeSlotRail aria-label={`All ${fit}`}>
111
+ <Chips selected="19:30" onSelect={() => {}} fit={fit} />
112
+ </TimeSlotRail>
113
+ </Inset>
114
+ ))}
115
+ </div>
116
+ ),
117
+ };
118
+
119
+ /** Two service periods split by a separator, each its own labelled group. */
120
+ export const Periods: Story = {
121
+ render: () => {
122
+ const [selected, setSelected] = React.useState("20:00");
123
+ const lunch = SLOTS.slice(0, 4);
124
+ const dinner = SLOTS.slice(4);
125
+ const chips = (slots: Slot[]) =>
126
+ slots.map((slot) => (
127
+ <TimeSlot
128
+ key={slot.time}
129
+ size="touch"
130
+ time={slot.time}
131
+ fit={slot.fit}
132
+ meta={slot.meta}
133
+ selected={selected === slot.time}
134
+ onSelect={() => setSelected(slot.time)}
135
+ className="w-[84px] shrink-0 snap-center"
136
+ />
137
+ ));
138
+ return (
139
+ <Inset>
140
+ <TimeSlotRail aria-label="Start time on Wed 18 Nov" scrollLabels={SCROLL_LABELS}>
141
+ <div role="group" aria-label="Lunch" className="flex gap-1.5">
142
+ {chips(lunch)}
143
+ </div>
144
+ <Separator orientation="vertical" className="mx-1 h-8 self-center" />
145
+ <div role="group" aria-label="Dinner" className="flex gap-1.5">
146
+ {chips(dinner)}
147
+ </div>
148
+ </TimeSlotRail>
149
+ </Inset>
150
+ );
151
+ },
152
+ };
153
+
154
+ /** Loading is the caller's: skeletons of the same footprint, so the inset does not jump. */
155
+ export const Loading: Story = {
156
+ render: () => (
157
+ <Inset>
158
+ <div role="status" className="flex gap-1.5 overflow-hidden">
159
+ <span className="sr-only">Checking tables for 12…</span>
160
+ {Array.from({ length: 5 }, (_, i) => (
161
+ <Skeleton key={i} aria-hidden className="h-[52px] w-[84px] shrink-0 rounded-xl" />
162
+ ))}
163
+ </div>
164
+ </Inset>
165
+ ),
166
+ };