@tribepad/themis 1.0.19 → 1.1.0
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/elements/Sidebar/Sidebar.styles.d.ts +41 -0
- package/dist/elements/Sidebar/Sidebar.styles.d.ts.map +1 -0
- package/dist/elements/Sidebar/Sidebar.types.d.ts +224 -0
- package/dist/elements/Sidebar/Sidebar.types.d.ts.map +1 -0
- package/dist/elements/Sidebar/SidebarCompound.d.ts +39 -0
- package/dist/elements/Sidebar/SidebarCompound.d.ts.map +1 -0
- package/dist/elements/Sidebar/SidebarMobileDrawer.d.ts +7 -0
- package/dist/elements/Sidebar/SidebarMobileDrawer.d.ts.map +1 -0
- package/dist/elements/Sidebar/SidebarPanel.d.ts +21 -0
- package/dist/elements/Sidebar/SidebarPanel.d.ts.map +1 -0
- package/dist/elements/Sidebar/SidebarPopover.d.ts +13 -0
- package/dist/elements/Sidebar/SidebarPopover.d.ts.map +1 -0
- package/dist/elements/Sidebar/SidebarRail.d.ts +10 -0
- package/dist/elements/Sidebar/SidebarRail.d.ts.map +1 -0
- package/dist/elements/Sidebar/SidebarRoot.d.ts +24 -0
- package/dist/elements/Sidebar/SidebarRoot.d.ts.map +1 -0
- package/dist/elements/Sidebar/index.d.ts +5 -0
- package/dist/elements/Sidebar/index.d.ts.map +1 -0
- package/dist/elements/Sidebar/index.js +3 -0
- package/dist/elements/Sidebar/index.js.map +1 -0
- package/dist/elements/Sidebar/index.mjs +3 -0
- package/dist/elements/Sidebar/index.mjs.map +1 -0
- package/dist/elements/index.d.ts +4 -0
- package/dist/elements/index.d.ts.map +1 -1
- package/dist/elements/index.js +1 -1
- package/dist/elements/index.js.map +1 -1
- package/dist/elements/index.mjs +1 -1
- package/dist/elements/index.mjs.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/hooks/index.mjs.map +1 -1
- package/dist/hooks/useMediaQuery.d.ts +12 -0
- package/dist/hooks/useMediaQuery.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -1
- package/src/elements/Sidebar/Sidebar.stories.tsx +514 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tribepad/themis",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Accessible React component library built on React Aria primitives",
|
|
5
5
|
"author": "Tribepad <mbasford@tribepad.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -245,6 +245,11 @@
|
|
|
245
245
|
"name": "TextField only",
|
|
246
246
|
"path": "dist/elements/TextField/index.mjs",
|
|
247
247
|
"limit": "30 KB"
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"name": "Sidebar only",
|
|
251
|
+
"path": "dist/elements/Sidebar/index.mjs",
|
|
252
|
+
"limit": "35 KB"
|
|
248
253
|
}
|
|
249
254
|
],
|
|
250
255
|
"keywords": [
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
import { useState, type ReactElement } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import {
|
|
4
|
+
Home,
|
|
5
|
+
Inbox,
|
|
6
|
+
Calendar,
|
|
7
|
+
Users,
|
|
8
|
+
FileText,
|
|
9
|
+
Settings,
|
|
10
|
+
BarChart3,
|
|
11
|
+
MessageSquare,
|
|
12
|
+
Star,
|
|
13
|
+
HelpCircle,
|
|
14
|
+
Menu,
|
|
15
|
+
} from 'lucide-react';
|
|
16
|
+
import { Sidebar } from './SidebarCompound';
|
|
17
|
+
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '../Accordion';
|
|
18
|
+
import type { SidebarRailVariant } from './Sidebar.types';
|
|
19
|
+
|
|
20
|
+
const meta = {
|
|
21
|
+
title: 'Elements/Sidebar',
|
|
22
|
+
component: Sidebar,
|
|
23
|
+
tags: ['autodocs'],
|
|
24
|
+
parameters: {
|
|
25
|
+
layout: 'fullscreen',
|
|
26
|
+
docs: {
|
|
27
|
+
description: {
|
|
28
|
+
component:
|
|
29
|
+
'WCAG 2.2 AAA compliant sidebar navigation with slim icon rail, optional collapsible context panel, and hover/focus popovers. Built on React Aria Toolbar for keyboard navigation.',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
} satisfies Meta<typeof Sidebar>;
|
|
34
|
+
|
|
35
|
+
export default meta;
|
|
36
|
+
type Story = StoryObj<typeof meta>;
|
|
37
|
+
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// Helpers
|
|
40
|
+
// =============================================================================
|
|
41
|
+
|
|
42
|
+
function PanelContentForItem({ id }: { id: string }): ReactElement {
|
|
43
|
+
const title = id.charAt(0).toUpperCase() + id.slice(1);
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
<Sidebar.PanelHeader title={title} />
|
|
47
|
+
<div className="p-3 text-sm text-[var(--content-foreground)]">
|
|
48
|
+
<p>Content for {id}.</p>
|
|
49
|
+
<p className="mt-2 text-[var(--muted-foreground)]">
|
|
50
|
+
This panel shows contextual information for the selected navigation item.
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
</>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const NAV_ITEMS = [
|
|
58
|
+
{ id: 'home', icon: <Home />, label: 'Home' },
|
|
59
|
+
{ id: 'inbox', icon: <Inbox />, label: 'Inbox' },
|
|
60
|
+
{ id: 'calendar', icon: <Calendar />, label: 'Planner' },
|
|
61
|
+
{ id: 'teams', icon: <Users />, label: 'Teams' },
|
|
62
|
+
{ id: 'docs', icon: <FileText />, label: 'Docs' },
|
|
63
|
+
{ id: 'analytics', icon: <BarChart3 />, label: 'Analytics' },
|
|
64
|
+
] as const;
|
|
65
|
+
|
|
66
|
+
const BADGE_COUNTS: Record<string, number> = {
|
|
67
|
+
home: 3,
|
|
68
|
+
inbox: 14,
|
|
69
|
+
analytics: 150,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function SidebarDemo({
|
|
73
|
+
defaultCollapsed = false,
|
|
74
|
+
showPanel = true,
|
|
75
|
+
showPopovers = false,
|
|
76
|
+
showBadges = false,
|
|
77
|
+
railVariant = 'default' as SidebarRailVariant,
|
|
78
|
+
}: {
|
|
79
|
+
defaultCollapsed?: boolean;
|
|
80
|
+
showPanel?: boolean;
|
|
81
|
+
showPopovers?: boolean;
|
|
82
|
+
showBadges?: boolean;
|
|
83
|
+
railVariant?: SidebarRailVariant;
|
|
84
|
+
}): ReactElement {
|
|
85
|
+
const [activeId, setActiveId] = useState('home');
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className="flex h-screen bg-[var(--page-background)]">
|
|
89
|
+
<Sidebar defaultCollapsed={defaultCollapsed}>
|
|
90
|
+
<Sidebar.Rail aria-label="Main navigation" variant={railVariant}>
|
|
91
|
+
{showPanel && <Sidebar.CollapseButton />}
|
|
92
|
+
{NAV_ITEMS.map((item) => (
|
|
93
|
+
<Sidebar.RailItem
|
|
94
|
+
key={item.id}
|
|
95
|
+
id={item.id}
|
|
96
|
+
icon={item.icon}
|
|
97
|
+
label={item.label}
|
|
98
|
+
isActive={activeId === item.id}
|
|
99
|
+
onPress={() => setActiveId(item.id)}
|
|
100
|
+
badgeCount={showBadges ? BADGE_COUNTS[item.id] : undefined}
|
|
101
|
+
>
|
|
102
|
+
{showPopovers && (
|
|
103
|
+
<Sidebar.Popover>
|
|
104
|
+
<PanelContentForItem id={item.id} />
|
|
105
|
+
</Sidebar.Popover>
|
|
106
|
+
)}
|
|
107
|
+
</Sidebar.RailItem>
|
|
108
|
+
))}
|
|
109
|
+
</Sidebar.Rail>
|
|
110
|
+
{showPanel && (
|
|
111
|
+
<Sidebar.Panel width="md" aria-label={`${activeId} details`}>
|
|
112
|
+
<PanelContentForItem id={activeId} />
|
|
113
|
+
</Sidebar.Panel>
|
|
114
|
+
)}
|
|
115
|
+
</Sidebar>
|
|
116
|
+
<main className="flex-1 p-6">
|
|
117
|
+
<h1 className="text-2xl font-bold text-[var(--page-foreground)]">
|
|
118
|
+
{activeId.charAt(0).toUpperCase() + activeId.slice(1)}
|
|
119
|
+
</h1>
|
|
120
|
+
<p className="mt-2 text-[var(--muted-foreground)]">Main content area</p>
|
|
121
|
+
</main>
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// =============================================================================
|
|
127
|
+
// Stories
|
|
128
|
+
// =============================================================================
|
|
129
|
+
|
|
130
|
+
/** Default sidebar with rail and context panel */
|
|
131
|
+
export const Default: Story = {
|
|
132
|
+
render: () => <SidebarDemo />,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** Rail without a context panel */
|
|
136
|
+
export const RailOnly: Story = {
|
|
137
|
+
render: () => <SidebarDemo showPanel={false} />,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/** Panel starts in collapsed state */
|
|
141
|
+
export const CollapsedPanel: Story = {
|
|
142
|
+
render: () => <SidebarDemo defaultCollapsed />,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** Rail items with hover popover content — hover a non-active item to preview its panel content */
|
|
146
|
+
export const WithPopovers: Story = {
|
|
147
|
+
render: () => <SidebarDemo showPopovers />,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** Badge counts on rail items (including 99+ cap) */
|
|
151
|
+
export const WithBadges: Story = {
|
|
152
|
+
render: () => <SidebarDemo showBadges />,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/** Rail items that navigate via href */
|
|
156
|
+
export const LinkItems: Story = {
|
|
157
|
+
render: () => (
|
|
158
|
+
<div className="flex h-screen bg-[var(--page-background)]">
|
|
159
|
+
<Sidebar>
|
|
160
|
+
<Sidebar.Rail aria-label="Main navigation">
|
|
161
|
+
<Sidebar.RailItem id="home" icon={<Home />} label="Home" href="/" isActive />
|
|
162
|
+
<Sidebar.RailItem id="docs" icon={<FileText />} label="Docs" href="/docs" />
|
|
163
|
+
<Sidebar.RailItem id="help" icon={<HelpCircle />} label="Help" href="/help" />
|
|
164
|
+
</Sidebar.Rail>
|
|
165
|
+
</Sidebar>
|
|
166
|
+
<main className="flex-1 p-6">
|
|
167
|
+
<p className="text-[var(--page-foreground)]">These rail items render as links with href attributes.</p>
|
|
168
|
+
</main>
|
|
169
|
+
</div>
|
|
170
|
+
),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/** Default rail variant */
|
|
174
|
+
export const DefaultRail: Story = {
|
|
175
|
+
render: () => <SidebarDemo showPopovers railVariant="default" />,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/** Primary rail variant — uses primary action brand colour as background */
|
|
179
|
+
export const PrimaryRail: Story = {
|
|
180
|
+
render: () => <SidebarDemo showPopovers railVariant="primary" />,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** Both variants side by side for comparison */
|
|
184
|
+
export const RailVariantComparison: Story = {
|
|
185
|
+
render: () => {
|
|
186
|
+
const [variant, setVariant] = useState<SidebarRailVariant>('default');
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div className="flex flex-col h-screen bg-[var(--page-background)]">
|
|
190
|
+
<div className="p-2 border-b border-[var(--border)] flex gap-2">
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
onClick={() => setVariant('default')}
|
|
194
|
+
className={`px-3 py-1 text-sm rounded ${
|
|
195
|
+
variant === 'default'
|
|
196
|
+
? 'bg-[var(--primary-action)] text-[var(--primary-action-foreground)]'
|
|
197
|
+
: 'bg-[var(--secondary)] text-[var(--secondary-foreground)]'
|
|
198
|
+
}`}
|
|
199
|
+
>
|
|
200
|
+
Default
|
|
201
|
+
</button>
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={() => setVariant('primary')}
|
|
205
|
+
className={`px-3 py-1 text-sm rounded ${
|
|
206
|
+
variant === 'primary'
|
|
207
|
+
? 'bg-[var(--primary-action)] text-[var(--primary-action-foreground)]'
|
|
208
|
+
: 'bg-[var(--secondary)] text-[var(--secondary-foreground)]'
|
|
209
|
+
}`}
|
|
210
|
+
>
|
|
211
|
+
Primary
|
|
212
|
+
</button>
|
|
213
|
+
</div>
|
|
214
|
+
<div className="flex-1 overflow-hidden">
|
|
215
|
+
<SidebarDemo showPopovers railVariant={variant} />
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Mobile drawer — uses a high mobileBreakpoint to force mobile mode.
|
|
224
|
+
* Click the hamburger button to open the drawer.
|
|
225
|
+
*/
|
|
226
|
+
export const MobileDrawer: Story = {
|
|
227
|
+
render: () => {
|
|
228
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
229
|
+
const [activeId, setActiveId] = useState('home');
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<div className="flex flex-col h-screen bg-[var(--page-background)]">
|
|
233
|
+
{/* Top bar with hamburger */}
|
|
234
|
+
<header className="flex items-center gap-3 px-4 py-2 border-b border-[var(--border)] bg-[var(--content-background)]">
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
onClick={() => setMobileOpen(true)}
|
|
238
|
+
className="inline-flex items-center justify-center min-h-[44px] min-w-[44px] rounded-[var(--radius-md)] hover:bg-[var(--accent)] text-[var(--content-foreground)]"
|
|
239
|
+
aria-label="Open navigation"
|
|
240
|
+
>
|
|
241
|
+
<Menu className="size-5" />
|
|
242
|
+
</button>
|
|
243
|
+
<h1 className="text-lg font-semibold text-[var(--content-foreground)]">My App</h1>
|
|
244
|
+
</header>
|
|
245
|
+
|
|
246
|
+
{/* Sidebar with forced mobile mode (breakpoint set very high) */}
|
|
247
|
+
<Sidebar
|
|
248
|
+
mobileBreakpoint={99999}
|
|
249
|
+
isMobileOpen={mobileOpen}
|
|
250
|
+
onMobileOpenChange={setMobileOpen}
|
|
251
|
+
>
|
|
252
|
+
<Sidebar.Rail aria-label="Main navigation">
|
|
253
|
+
{NAV_ITEMS.map((item) => (
|
|
254
|
+
<Sidebar.RailItem
|
|
255
|
+
key={item.id}
|
|
256
|
+
id={item.id}
|
|
257
|
+
icon={item.icon}
|
|
258
|
+
label={item.label}
|
|
259
|
+
isActive={activeId === item.id}
|
|
260
|
+
badgeCount={BADGE_COUNTS[item.id]}
|
|
261
|
+
onPress={() => {
|
|
262
|
+
setActiveId(item.id);
|
|
263
|
+
}}
|
|
264
|
+
/>
|
|
265
|
+
))}
|
|
266
|
+
</Sidebar.Rail>
|
|
267
|
+
<Sidebar.Panel width="md" aria-label={`${activeId} details`}>
|
|
268
|
+
<PanelContentForItem id={activeId} />
|
|
269
|
+
</Sidebar.Panel>
|
|
270
|
+
</Sidebar>
|
|
271
|
+
|
|
272
|
+
{/* Main content */}
|
|
273
|
+
<main className="flex-1 p-6">
|
|
274
|
+
<h2 className="text-2xl font-bold text-[var(--page-foreground)]">
|
|
275
|
+
{activeId.charAt(0).toUpperCase() + activeId.slice(1)}
|
|
276
|
+
</h2>
|
|
277
|
+
<p className="mt-2 text-[var(--muted-foreground)]">
|
|
278
|
+
This story forces mobile mode by setting mobileBreakpoint to 99999px.
|
|
279
|
+
Click the hamburger menu to open the navigation drawer.
|
|
280
|
+
</p>
|
|
281
|
+
</main>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/** Mobile drawer with primary rail variant */
|
|
288
|
+
export const MobileDrawerPrimary: Story = {
|
|
289
|
+
render: () => {
|
|
290
|
+
const [mobileOpen, setMobileOpen] = useState(false);
|
|
291
|
+
const [activeId, setActiveId] = useState('home');
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<div className="flex flex-col h-screen bg-[var(--page-background)]">
|
|
295
|
+
<header className="flex items-center gap-3 px-4 py-2 border-b border-[var(--border)] bg-[var(--content-background)]">
|
|
296
|
+
<button
|
|
297
|
+
type="button"
|
|
298
|
+
onClick={() => setMobileOpen(true)}
|
|
299
|
+
className="inline-flex items-center justify-center min-h-[44px] min-w-[44px] rounded-[var(--radius-md)] hover:bg-[var(--accent)] text-[var(--content-foreground)]"
|
|
300
|
+
aria-label="Open navigation"
|
|
301
|
+
>
|
|
302
|
+
<Menu className="size-5" />
|
|
303
|
+
</button>
|
|
304
|
+
<h1 className="text-lg font-semibold text-[var(--content-foreground)]">My App (Primary)</h1>
|
|
305
|
+
</header>
|
|
306
|
+
|
|
307
|
+
<Sidebar
|
|
308
|
+
mobileBreakpoint={99999}
|
|
309
|
+
isMobileOpen={mobileOpen}
|
|
310
|
+
onMobileOpenChange={setMobileOpen}
|
|
311
|
+
>
|
|
312
|
+
<Sidebar.Rail aria-label="Main navigation" variant="primary">
|
|
313
|
+
{NAV_ITEMS.map((item) => (
|
|
314
|
+
<Sidebar.RailItem
|
|
315
|
+
key={item.id}
|
|
316
|
+
id={item.id}
|
|
317
|
+
icon={item.icon}
|
|
318
|
+
label={item.label}
|
|
319
|
+
isActive={activeId === item.id}
|
|
320
|
+
badgeCount={BADGE_COUNTS[item.id]}
|
|
321
|
+
onPress={() => {
|
|
322
|
+
setActiveId(item.id);
|
|
323
|
+
}}
|
|
324
|
+
/>
|
|
325
|
+
))}
|
|
326
|
+
</Sidebar.Rail>
|
|
327
|
+
<Sidebar.Panel width="md" aria-label={`${activeId} details`}>
|
|
328
|
+
<PanelContentForItem id={activeId} />
|
|
329
|
+
</Sidebar.Panel>
|
|
330
|
+
</Sidebar>
|
|
331
|
+
|
|
332
|
+
<main className="flex-1 p-6">
|
|
333
|
+
<h2 className="text-2xl font-bold text-[var(--page-foreground)]">
|
|
334
|
+
{activeId.charAt(0).toUpperCase() + activeId.slice(1)}
|
|
335
|
+
</h2>
|
|
336
|
+
<p className="mt-2 text-[var(--muted-foreground)]">
|
|
337
|
+
Mobile drawer with primary rail variant.
|
|
338
|
+
</p>
|
|
339
|
+
</main>
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/** Panel using Accordion for collapsible sections */
|
|
346
|
+
export const WithAccordionPanel: Story = {
|
|
347
|
+
render: () => {
|
|
348
|
+
const [activeId, setActiveId] = useState('inbox');
|
|
349
|
+
|
|
350
|
+
return (
|
|
351
|
+
<div className="flex h-screen bg-[var(--page-background)]">
|
|
352
|
+
<Sidebar>
|
|
353
|
+
<Sidebar.Rail aria-label="Main navigation">
|
|
354
|
+
<Sidebar.CollapseButton />
|
|
355
|
+
<Sidebar.RailItem
|
|
356
|
+
id="inbox"
|
|
357
|
+
icon={<Inbox />}
|
|
358
|
+
label="Inbox"
|
|
359
|
+
isActive={activeId === 'inbox'}
|
|
360
|
+
onPress={() => setActiveId('inbox')}
|
|
361
|
+
/>
|
|
362
|
+
<Sidebar.RailItem
|
|
363
|
+
id="teams"
|
|
364
|
+
icon={<Users />}
|
|
365
|
+
label="Teams"
|
|
366
|
+
isActive={activeId === 'teams'}
|
|
367
|
+
onPress={() => setActiveId('teams')}
|
|
368
|
+
/>
|
|
369
|
+
</Sidebar.Rail>
|
|
370
|
+
<Sidebar.Panel width="lg" aria-label="Inbox sections">
|
|
371
|
+
<Sidebar.PanelHeader title="Inbox" />
|
|
372
|
+
<div className="overflow-y-auto flex-1 p-3">
|
|
373
|
+
<Accordion type="multiple" defaultExpandedKeys={['favorites']}>
|
|
374
|
+
<AccordionItem id="favorites">
|
|
375
|
+
<AccordionTrigger>Favorites</AccordionTrigger>
|
|
376
|
+
<AccordionContent>
|
|
377
|
+
<ul className="space-y-1 px-3 py-2 text-sm">
|
|
378
|
+
<li className="flex items-center gap-2"><Star className="size-4" /> Project Alpha</li>
|
|
379
|
+
<li className="flex items-center gap-2"><Star className="size-4" /> Project Beta</li>
|
|
380
|
+
</ul>
|
|
381
|
+
</AccordionContent>
|
|
382
|
+
</AccordionItem>
|
|
383
|
+
<AccordionItem id="channels">
|
|
384
|
+
<AccordionTrigger>Channels</AccordionTrigger>
|
|
385
|
+
<AccordionContent>
|
|
386
|
+
<ul className="space-y-1 px-3 py-2 text-sm">
|
|
387
|
+
<li className="flex items-center gap-2"><MessageSquare className="size-4" /> General</li>
|
|
388
|
+
<li className="flex items-center gap-2"><MessageSquare className="size-4" /> Engineering</li>
|
|
389
|
+
</ul>
|
|
390
|
+
</AccordionContent>
|
|
391
|
+
</AccordionItem>
|
|
392
|
+
<AccordionItem id="dms">
|
|
393
|
+
<AccordionTrigger>Direct Messages</AccordionTrigger>
|
|
394
|
+
<AccordionContent>
|
|
395
|
+
<ul className="space-y-1 px-3 py-2 text-sm">
|
|
396
|
+
<li className="flex items-center gap-2"><Users className="size-4" /> Alice</li>
|
|
397
|
+
<li className="flex items-center gap-2"><Users className="size-4" /> Bob</li>
|
|
398
|
+
</ul>
|
|
399
|
+
</AccordionContent>
|
|
400
|
+
</AccordionItem>
|
|
401
|
+
</Accordion>
|
|
402
|
+
</div>
|
|
403
|
+
</Sidebar.Panel>
|
|
404
|
+
</Sidebar>
|
|
405
|
+
<main className="flex-1 p-6">
|
|
406
|
+
<h1 className="text-2xl font-bold text-[var(--page-foreground)]">Inbox</h1>
|
|
407
|
+
</main>
|
|
408
|
+
</div>
|
|
409
|
+
);
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
/** Controlled collapse and mobile state with external buttons */
|
|
414
|
+
export const Controlled: Story = {
|
|
415
|
+
render: () => {
|
|
416
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
417
|
+
|
|
418
|
+
return (
|
|
419
|
+
<div className="flex flex-col h-screen bg-[var(--page-background)]">
|
|
420
|
+
<div className="p-2 border-b border-[var(--border)] flex gap-2">
|
|
421
|
+
<button
|
|
422
|
+
type="button"
|
|
423
|
+
onClick={() => setCollapsed(!collapsed)}
|
|
424
|
+
className="px-3 py-1 text-sm rounded bg-[var(--primary-action)] text-[var(--primary-action-foreground)]"
|
|
425
|
+
>
|
|
426
|
+
{collapsed ? 'Expand Panel' : 'Collapse Panel'}
|
|
427
|
+
</button>
|
|
428
|
+
</div>
|
|
429
|
+
<div className="flex flex-1 overflow-hidden">
|
|
430
|
+
<Sidebar isCollapsed={collapsed} onCollapsedChange={setCollapsed}>
|
|
431
|
+
<Sidebar.Rail aria-label="Main navigation">
|
|
432
|
+
<Sidebar.CollapseButton />
|
|
433
|
+
<Sidebar.RailItem id="home" icon={<Home />} label="Home" isActive onPress={() => {}} />
|
|
434
|
+
<Sidebar.RailItem id="settings" icon={<Settings />} label="Settings" onPress={() => {}} />
|
|
435
|
+
</Sidebar.Rail>
|
|
436
|
+
<Sidebar.Panel width="md" aria-label="Details">
|
|
437
|
+
<Sidebar.PanelHeader title="Home" />
|
|
438
|
+
<div className="p-3 text-sm">Panel content here</div>
|
|
439
|
+
</Sidebar.Panel>
|
|
440
|
+
</Sidebar>
|
|
441
|
+
<main className="flex-1 p-6">
|
|
442
|
+
<p className="text-[var(--page-foreground)]">
|
|
443
|
+
Panel is {collapsed ? 'collapsed' : 'expanded'}
|
|
444
|
+
</p>
|
|
445
|
+
</main>
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
);
|
|
449
|
+
},
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
/** All panel width variants */
|
|
453
|
+
export const AllWidths: Story = {
|
|
454
|
+
render: () => {
|
|
455
|
+
const [width, setWidth] = useState<'sm' | 'md' | 'lg' | 'xl'>('md');
|
|
456
|
+
|
|
457
|
+
return (
|
|
458
|
+
<div className="flex flex-col h-screen bg-[var(--page-background)]">
|
|
459
|
+
<div className="p-2 border-b border-[var(--border)] flex gap-2">
|
|
460
|
+
{(['sm', 'md', 'lg', 'xl'] as const).map((w) => (
|
|
461
|
+
<button
|
|
462
|
+
key={w}
|
|
463
|
+
type="button"
|
|
464
|
+
onClick={() => setWidth(w)}
|
|
465
|
+
className={`px-3 py-1 text-sm rounded ${
|
|
466
|
+
width === w
|
|
467
|
+
? 'bg-[var(--primary-action)] text-[var(--primary-action-foreground)]'
|
|
468
|
+
: 'bg-[var(--secondary)] text-[var(--secondary-foreground)]'
|
|
469
|
+
}`}
|
|
470
|
+
>
|
|
471
|
+
{w}
|
|
472
|
+
</button>
|
|
473
|
+
))}
|
|
474
|
+
</div>
|
|
475
|
+
<div className="flex flex-1 overflow-hidden">
|
|
476
|
+
<Sidebar>
|
|
477
|
+
<Sidebar.Rail aria-label="Main navigation">
|
|
478
|
+
<Sidebar.CollapseButton />
|
|
479
|
+
<Sidebar.RailItem id="home" icon={<Home />} label="Home" isActive onPress={() => {}} />
|
|
480
|
+
</Sidebar.Rail>
|
|
481
|
+
<Sidebar.Panel width={width} aria-label="Details">
|
|
482
|
+
<Sidebar.PanelHeader title={`Width: ${width}`} />
|
|
483
|
+
<div className="p-3 text-sm">
|
|
484
|
+
Panel width: {width === 'sm' ? '220px' : width === 'md' ? '280px' : width === 'lg' ? '340px' : '420px'}
|
|
485
|
+
</div>
|
|
486
|
+
</Sidebar.Panel>
|
|
487
|
+
</Sidebar>
|
|
488
|
+
<main className="flex-1 p-6">
|
|
489
|
+
<p className="text-[var(--page-foreground)]">Main content</p>
|
|
490
|
+
</main>
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
);
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
/** Keyboard navigation demo */
|
|
498
|
+
export const KeyboardNavigation: Story = {
|
|
499
|
+
render: () => (
|
|
500
|
+
<div className="flex h-screen bg-[var(--page-background)]">
|
|
501
|
+
<SidebarDemo showPopovers />
|
|
502
|
+
<div className="fixed bottom-4 right-4 bg-[var(--content-background)] border border-[var(--border)] rounded-lg p-4 shadow-lg text-sm max-w-xs">
|
|
503
|
+
<h3 className="font-semibold mb-2 text-[var(--content-foreground)]">Keyboard Shortcuts</h3>
|
|
504
|
+
<ul className="space-y-1 text-[var(--muted-foreground)]">
|
|
505
|
+
<li><kbd className="font-mono bg-[var(--muted)] px-1 rounded">Tab</kbd> Enter/exit rail</li>
|
|
506
|
+
<li><kbd className="font-mono bg-[var(--muted)] px-1 rounded">↑↓</kbd> Navigate items</li>
|
|
507
|
+
<li><kbd className="font-mono bg-[var(--muted)] px-1 rounded">Home/End</kbd> First/last</li>
|
|
508
|
+
<li><kbd className="font-mono bg-[var(--muted)] px-1 rounded">Enter</kbd> Activate item</li>
|
|
509
|
+
<li><kbd className="font-mono bg-[var(--muted)] px-1 rounded">Esc</kbd> Close popover</li>
|
|
510
|
+
</ul>
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
),
|
|
514
|
+
};
|