@hyphen/hyphen-components 7.8.0 → 7.9.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/index.d.ts CHANGED
@@ -427,7 +427,7 @@ interface BadgeProps extends BoxProps {
427
427
  }
428
428
  declare const Badge: React__default.ForwardRefExoticComponent<Omit<BadgeProps, "ref"> & React__default.RefAttributes<HTMLDivElement>>;
429
429
 
430
- type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'danger' | 'link';
430
+ type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'danger' | 'success' | 'link';
431
431
  type ButtonSize = 'sm' | 'md' | 'lg';
432
432
  interface BaseButtonProps {
433
433
  /**
@@ -2271,17 +2271,49 @@ declare function useSidebar(sideOverride?: SidebarSide): {
2271
2271
  toggleSidebar: () => void;
2272
2272
  };
2273
2273
  declare const SidebarProvider: React__default.ForwardRefExoticComponent<Omit<React__default.ClassAttributes<HTMLDivElement> & React__default.HTMLAttributes<HTMLDivElement> & {
2274
+ /**
2275
+ * Initial open state for uncontrolled usage. Pass a boolean for both
2276
+ * sides, or `{ left, right }` to set each side independently.
2277
+ */
2274
2278
  defaultOpen?: SidebarOpenValue;
2279
+ /**
2280
+ * Controlled open state. Pass a boolean for both sides, or
2281
+ * `{ left, right }` to control each side independently.
2282
+ */
2275
2283
  open?: SidebarOpenValue;
2284
+ /**
2285
+ * localStorage key(s) the open state is written to when a sidebar is
2286
+ * toggled. A string applies to the left sidebar, with `_right` appended
2287
+ * for the right sidebar; pass `{ left, right }` to set each explicitly.
2288
+ */
2276
2289
  storageKey?: SidebarStorageKey;
2290
+ /**
2291
+ * Called when a sidebar is toggled, with the new open state and which
2292
+ * side was toggled.
2293
+ */
2277
2294
  onOpenChange?: (open: boolean, side?: SidebarSide) => void;
2278
2295
  }, "ref"> & React__default.RefAttributes<HTMLDivElement>>;
2279
2296
  declare const Sidebar: React__default.ForwardRefExoticComponent<Omit<React__default.ClassAttributes<HTMLDivElement> & React__default.HTMLAttributes<HTMLDivElement> & {
2297
+ /**
2298
+ * Which edge of the viewport the sidebar is attached to.
2299
+ */
2280
2300
  side?: "left" | "right";
2301
+ /**
2302
+ * Collapse behavior when the sidebar is toggled closed: slide fully out
2303
+ * of view (`offcanvas`), collapse to a narrow icon strip (`icon`), or
2304
+ * stay at full width (`none`).
2305
+ */
2281
2306
  collapsible?: "offcanvas" | "icon" | "none";
2282
2307
  }, "ref"> & React__default.RefAttributes<HTMLDivElement>>;
2283
2308
  declare const SidebarTrigger: React__default.ForwardRefExoticComponent<Omit<BaseButtonProps & React__default.ButtonHTMLAttributes<HTMLButtonElement> & React__default.RefAttributes<HTMLButtonElement> & {
2309
+ /**
2310
+ * Which sidebar to toggle. Defaults to the side of the containing
2311
+ * Sidebar, or `left` outside of one.
2312
+ */
2284
2313
  side?: SidebarSide;
2314
+ /**
2315
+ * Icon shown on the trigger button.
2316
+ */
2285
2317
  iconName?: IconName;
2286
2318
  }, "ref"> & React__default.RefAttributes<HTMLButtonElement>>;
2287
2319
  declare const SidebarInset: React__default.ForwardRefExoticComponent<Omit<React__default.DetailedHTMLProps<React__default.HTMLAttributes<HTMLElement>, HTMLElement>, "ref"> & React__default.RefAttributes<HTMLDivElement>>;
@@ -2291,8 +2323,18 @@ declare const SidebarContent: React__default.ForwardRefExoticComponent<Omit<Reac
2291
2323
  declare const SidebarMenu: React__default.ForwardRefExoticComponent<Omit<React__default.DetailedHTMLProps<React__default.HTMLAttributes<HTMLUListElement>, HTMLUListElement>, "ref"> & React__default.RefAttributes<HTMLUListElement>>;
2292
2324
  declare const SidebarMenuItem: React__default.ForwardRefExoticComponent<Omit<React__default.DetailedHTMLProps<React__default.LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>, "ref"> & React__default.RefAttributes<HTMLLIElement>>;
2293
2325
  declare const SidebarMenuButton: React__default.ForwardRefExoticComponent<Omit<React__default.ClassAttributes<HTMLButtonElement> & React__default.ButtonHTMLAttributes<HTMLButtonElement> & {
2326
+ /**
2327
+ * Render the child element (e.g. a router link) instead of a `button`.
2328
+ */
2294
2329
  asChild?: boolean;
2330
+ /**
2331
+ * Marks the item as the current page.
2332
+ */
2295
2333
  isActive?: boolean;
2334
+ /**
2335
+ * Tooltip shown when the sidebar is collapsed to icons (desktop only).
2336
+ * Pass a string, or TooltipContent props for full control.
2337
+ */
2296
2338
  tooltip?: string | React__default.ComponentProps<typeof TooltipContent>;
2297
2339
  }, "ref"> & React__default.RefAttributes<HTMLButtonElement>>;
2298
2340
  declare const SidebarGroup: React__default.ForwardRefExoticComponent<Omit<React__default.DetailedHTMLProps<React__default.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & React__default.RefAttributes<HTMLDivElement>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyphen/hyphen-components",
3
- "version": "7.8.0",
3
+ "version": "7.9.0",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "@hyphen"
@@ -56,7 +56,7 @@
56
56
  "react-select": "^5.10.0",
57
57
  "react-select-event": "^5.5.1",
58
58
  "uuid": "^11.1.1",
59
- "@hyphen/hyphen-design-tokens": "^7.8.0"
59
+ "@hyphen/hyphen-design-tokens": "^7.9.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@babel/core": "^7.27.4",
@@ -108,6 +108,7 @@
108
108
  "react": "^18.3.1",
109
109
  "react-dom": "^18.3.1",
110
110
  "react-is": "^18.3.1",
111
+ "remark-gfm": "^4.0.1",
111
112
  "sass": "^1.77.8",
112
113
  "sass-loader": "^13.3.3",
113
114
  "size-limit": "^11.2.0",
@@ -5,6 +5,7 @@ export const BUTTON_VARIANTS: ButtonVariant[] = [
5
5
  'secondary',
6
6
  'tertiary',
7
7
  'danger',
8
+ 'success',
8
9
  ];
9
10
 
10
11
  export const BUTTON_SIZES: ButtonSize[] = ['sm', 'md', 'lg'];
@@ -3,7 +3,8 @@
3
3
  :root {
4
4
  --button-box-shadow-focus: 0 0 0 4px var(--color-base-grey-300);
5
5
  --button-neutral-box-shadow-focus: 0 0 0 4px var(--color-base-grey-200);
6
- --button-danger-box-shadow-focus: 0 0 0 4px var(--color-base-danger-200);
6
+ --button-danger-box-shadow-focus: 0 0 0 4px var(--color-base-red-200);
7
+ --button-success-box-shadow-focus: 0 0 0 4px var(--color-base-green-200);
7
8
  }
8
9
 
9
10
  @mixin size-sm {
@@ -245,6 +246,36 @@
245
246
  }
246
247
  }
247
248
 
249
+ &.success {
250
+ background-color: var(--color-background-button-success);
251
+ color: var(--color-font-button-success);
252
+
253
+ &:not(:disabled):hover {
254
+ background-color: var(--color-background-button-success-hover);
255
+ color: var(--color-font-button-success-hover);
256
+ }
257
+
258
+ &:not(:disabled):active {
259
+ background-color: var(--color-background-button-success-active);
260
+ color: var(--color-font-button-success-active);
261
+ }
262
+
263
+ &:focus {
264
+ outline: 0;
265
+ box-shadow: var(--button-success-box-shadow-focus);
266
+ }
267
+
268
+ &:focus-visible {
269
+ outline: 0;
270
+ box-shadow: var(--button-success-box-shadow-focus);
271
+ }
272
+
273
+ &:focus:not(:focus-visible) {
274
+ outline: 0;
275
+ box-shadow: none;
276
+ }
277
+ }
278
+
248
279
  &.link {
249
280
  display: inline-flex;
250
281
  background-color: transparent;
@@ -11,6 +11,7 @@ const BUTTON_VARIANTS: ButtonVariant[] = [
11
11
  'secondary',
12
12
  'tertiary',
13
13
  'danger',
14
+ 'success',
14
15
  'link',
15
16
  ];
16
17
  const BUTTON_SIZES: ButtonSize[] = ['sm', 'md', 'lg'];
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { BUTTON_SIZES, BUTTON_VARIANTS } from './Button.constants';
3
- import { Button, ButtonVariant } from './Button';
3
+ import { Button } from './Button';
4
4
  import { fireEvent, render, screen } from '@testing-library/react';
5
5
 
6
6
  const renderButton = (props = {}) => render(<Button {...props} />);
@@ -306,6 +306,16 @@ describe('Button', () => {
306
306
  const spinnerElement = document.getElementsByClassName('spinner')[0];
307
307
  expect(spinnerElement).toBeInTheDocument();
308
308
  });
309
+
310
+ test('renders the spinning indicator when button is success', () => {
311
+ renderButton({
312
+ isLoading: true,
313
+ variant: 'success',
314
+ children: 'Button is loading',
315
+ });
316
+ const spinnerElement = document.getElementsByClassName('spinner')[0];
317
+ expect(spinnerElement).toBeInTheDocument();
318
+ });
309
319
  });
310
320
 
311
321
  describe('Disabled and Loading', () => {
@@ -325,13 +335,7 @@ describe('Button', () => {
325
335
  expect(getButton('primary')).toHaveClass('primary');
326
336
  });
327
337
 
328
- const variants: ButtonVariant[] = [
329
- 'primary',
330
- 'secondary',
331
- 'tertiary',
332
- 'danger',
333
- ];
334
- variants.forEach((variant) => {
338
+ BUTTON_VARIANTS.forEach((variant) => {
335
339
  test(`renders component with variant: ${variant} when passed`, () => {
336
340
  renderButton({ variant, children: variant });
337
341
  expect(getButton(variant)).toHaveClass(variant);
@@ -13,6 +13,7 @@ export type ButtonVariant =
13
13
  | 'secondary'
14
14
  | 'tertiary'
15
15
  | 'danger'
16
+ | 'success'
16
17
  | 'link';
17
18
 
18
19
  export type ButtonSize = 'sm' | 'md' | 'lg';
@@ -0,0 +1,92 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { expect } from 'storybook/test';
4
+ import { FormControl } from './FormControl';
5
+
6
+ const meta = {
7
+ component: FormControl,
8
+ tags: ['ai-generated'],
9
+ } satisfies Meta<typeof FormControl>;
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof meta>;
13
+
14
+ export const Default: Story = {
15
+ args: {
16
+ id: 'first-name',
17
+ label: 'First name',
18
+ },
19
+ render: (args) => (
20
+ <FormControl {...args}>
21
+ <input id={args.id} type="text" />
22
+ </FormControl>
23
+ ),
24
+ play: async ({ canvas }) => {
25
+ // Proves the FormLabel htmlFor/input id wiring, not just that it mounted.
26
+ await expect(canvas.getByLabelText('First name')).toBeVisible();
27
+ },
28
+ };
29
+
30
+ export const WithHelpText: Story = {
31
+ args: {
32
+ id: 'email',
33
+ label: 'Email address',
34
+ helpText: 'We will never share your email.',
35
+ },
36
+ render: (args) => (
37
+ <FormControl {...args}>
38
+ <input id={args.id} type="email" />
39
+ </FormControl>
40
+ ),
41
+ };
42
+
43
+ export const WithError: Story = {
44
+ args: {
45
+ id: 'password',
46
+ label: 'Password',
47
+ error: 'Password must be at least 8 characters.',
48
+ },
49
+ render: (args) => (
50
+ <FormControl {...args}>
51
+ <input id={args.id} type="password" />
52
+ </FormControl>
53
+ ),
54
+ play: async ({ canvas }) => {
55
+ // The error prop renders as an InputValidationMessage below the input.
56
+ await expect(
57
+ canvas.getByText('Password must be at least 8 characters.')
58
+ ).toBeVisible();
59
+ },
60
+ };
61
+
62
+ export const Required: Story = {
63
+ args: {
64
+ id: 'username',
65
+ label: 'Username',
66
+ isRequired: true,
67
+ },
68
+ render: (args) => (
69
+ <FormControl {...args}>
70
+ <input id={args.id} type="text" required />
71
+ </FormControl>
72
+ ),
73
+ };
74
+
75
+ export const HiddenLabel: Story = {
76
+ args: {
77
+ id: 'search',
78
+ label: 'Search',
79
+ hideLabel: true,
80
+ },
81
+ render: (args) => (
82
+ <FormControl {...args}>
83
+ <input id={args.id} type="search" aria-label={args.label} />
84
+ </FormControl>
85
+ ),
86
+ play: async ({ canvas }) => {
87
+ // hideLabel suppresses the visible FormLabel entirely.
88
+ await expect(canvas.queryByText('Search')).not.toBeInTheDocument();
89
+ // The input must still be accessible via its aria-label.
90
+ await expect(canvas.getByLabelText('Search')).toBeVisible();
91
+ },
92
+ };
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { HelpText } from './HelpText';
4
+ import { Box } from '../Box/Box';
5
+
6
+ const meta = {
7
+ component: HelpText,
8
+ tags: ['ai-generated'],
9
+ } satisfies Meta<typeof HelpText>;
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof meta>;
13
+
14
+ export const Default: Story = {
15
+ args: {
16
+ children: 'Additional clarifying text to help describe the input.',
17
+ },
18
+ };
19
+
20
+ export const BelowAnInput: Story = {
21
+ render: () => (
22
+ <Box gap="2xs">
23
+ <label htmlFor="help-text-example">Email address</label>
24
+ <input
25
+ id="help-text-example"
26
+ type="email"
27
+ aria-describedby="help-text-example-hint"
28
+ />
29
+ {/* HelpText doesn't forward an id, so associate via a wrapper. */}
30
+ <div id="help-text-example-hint">
31
+ <HelpText>We will only use this to send your receipt.</HelpText>
32
+ </div>
33
+ </Box>
34
+ ),
35
+ };
36
+
37
+ export const WithRichContent: Story = {
38
+ args: {
39
+ children: (
40
+ <>
41
+ Must contain at least <strong>8 characters</strong>.
42
+ </>
43
+ ),
44
+ },
45
+ };
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react-vite';
3
+ import { expect } from 'storybook/test';
4
+ import { InputValidationMessage } from './InputValidationMessage';
5
+ import { Box } from '../Box/Box';
6
+
7
+ const meta = {
8
+ component: InputValidationMessage,
9
+ tags: ['ai-generated'],
10
+ } satisfies Meta<typeof InputValidationMessage>;
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof meta>;
14
+
15
+ export const Default: Story = {
16
+ args: {
17
+ children: 'This field is required.',
18
+ },
19
+ };
20
+
21
+ export const Sizes: Story = {
22
+ args: {
23
+ children: 'This field is required.',
24
+ },
25
+ render: () => (
26
+ <Box gap="sm">
27
+ <InputValidationMessage size="xs">
28
+ Extra small validation message
29
+ </InputValidationMessage>
30
+ <InputValidationMessage size="sm">
31
+ Small validation message
32
+ </InputValidationMessage>
33
+ <InputValidationMessage size="md">
34
+ Medium validation message
35
+ </InputValidationMessage>
36
+ </Box>
37
+ ),
38
+ };
39
+
40
+ export const CssCheck: Story = {
41
+ args: {
42
+ children: 'This field is required.',
43
+ },
44
+ play: async ({ canvas }) => {
45
+ const message = canvas.getByText('This field is required.');
46
+ // font-color-danger resolves to --color-font-danger (#dc2626 in the light
47
+ // theme) — fails if the design-token CSS did not load in the preview.
48
+ await expect(getComputedStyle(message).color).toBe('rgb(220, 38, 38)');
49
+ },
50
+ };
@@ -0,0 +1,208 @@
1
+ import { Canvas, Meta, ArgTypes } from '@storybook/addon-docs/blocks';
2
+ import {
3
+ SidebarProvider,
4
+ Sidebar,
5
+ SidebarTrigger,
6
+ SidebarMenuButton,
7
+ } from './Sidebar';
8
+ import * as Stories from './Sidebar.stories';
9
+
10
+ <Meta of={Stories} />
11
+
12
+ {/*
13
+ The embedded stories render in iframes, and the iframe is the story's
14
+ viewport. Keep it at least as wide as the desktop breakpoint (992px) so
15
+ the examples show the desktop sidebar instead of the mobile drawer,
16
+ scrolling horizontally when the docs column is narrower. Scoped to this
17
+ page's Canvas blocks via the sidebar-docs-canvas class.
18
+ */}
19
+
20
+ <style>
21
+ {`
22
+ .sidebar-docs-canvas .docs-story { overflow-x: auto; }
23
+ .sidebar-docs-canvas .docs-story iframe { min-width: 1100px; }
24
+ `}
25
+ </style>
26
+
27
+ # Sidebar
28
+
29
+ A composable, collapsible app sidebar for primary navigation. Use it to build an application sidebar with a header, scrollable content, grouped menus, and a footer, all wired to expand/collapse state that persists across visits and adapts to mobile automatically.
30
+
31
+ <Canvas of={Stories.SidebarExample} />
32
+
33
+ ## Anatomy
34
+
35
+ The Sidebar is composed from small building blocks. Compose only the pieces you need:
36
+
37
+ | Component | Purpose |
38
+ | ---------------------- | -------------------------------------------------------------------------------------------------- |
39
+ | `SidebarProvider` | Provides sidebar state (left and right) and keyboard shortcuts. Wrap your whole layout in it. |
40
+ | `Sidebar` | The sidebar container itself. Configure `side` and `collapsible` behavior. |
41
+ | `SidebarHeader` | Sticky top region, typically an org/team switcher. |
42
+ | `SidebarContent` | The scrollable middle region of the sidebar. |
43
+ | `SidebarFooter` | Sticky bottom region, typically the user menu. |
44
+ | `SidebarGroup` | A section within `SidebarContent`. |
45
+ | `SidebarGroupLabel` | Label for a group. Hidden automatically when collapsed to icons. |
46
+ | `SidebarMenu` | A menu (`ul`) of navigation items. |
47
+ | `SidebarMenuItem` | A single item (`li`) within a `SidebarMenu`. |
48
+ | `SidebarMenuButton` | The interactive element of an item. Supports `asChild`, `isActive`, and a collapsed-state `tooltip`.|
49
+ | `SidebarMenuAction` | A secondary action button rendered at the edge of a menu item (e.g. a "more" menu). |
50
+ | `SidebarMenuBadge` | A badge (e.g. a count) rendered at the edge of a menu item. |
51
+ | `SidebarMenuSub` | A nested sub-menu, typically inside a `Collapsible`. |
52
+ | `SidebarMenuSubItem` | A single item within a `SidebarMenuSub`. |
53
+ | `SidebarMenuSubButton` | The interactive element of a sub-menu item. Supports `asChild` and `isActive`. |
54
+ | `SidebarTrigger` | A button that toggles the sidebar. Place it in your page content or mobile header. |
55
+ | `SidebarRail` | A slim hover target on the sidebar edge for toggling with the mouse. |
56
+ | `SidebarInset` | The `main` content area that sits alongside the sidebar. |
57
+ | `useSidebar` | Hook for reading and controlling sidebar state from your own components. |
58
+
59
+ A typical app sidebar composition looks like this:
60
+
61
+ ```tsx
62
+ <SidebarProvider>
63
+ <Sidebar side="left" collapsible="icon">
64
+ <SidebarHeader>{/* org switcher */}</SidebarHeader>
65
+ <SidebarContent>
66
+ <SidebarGroup>
67
+ <SidebarGroupLabel>Platform</SidebarGroupLabel>
68
+ <SidebarMenu>
69
+ <SidebarMenuItem>
70
+ <SidebarMenuButton asChild isActive tooltip="Dashboard">
71
+ <a href="/">
72
+ <Icon name="dashboard" />
73
+ <span>Dashboard</span>
74
+ </a>
75
+ </SidebarMenuButton>
76
+ </SidebarMenuItem>
77
+ </SidebarMenu>
78
+ </SidebarGroup>
79
+ </SidebarContent>
80
+ <SidebarFooter>{/* user menu */}</SidebarFooter>
81
+ <SidebarRail />
82
+ </Sidebar>
83
+ <SidebarInset>
84
+ <SidebarTrigger />
85
+ {/* page content */}
86
+ </SidebarInset>
87
+ </SidebarProvider>
88
+ ```
89
+
90
+ ## Usage Guidelines
91
+
92
+ - Wrap the entire layout — sidebar and page content — in a single `SidebarProvider`. One provider manages both a left and a right sidebar.
93
+ - Wrap your app (or the layout) in `ResponsiveProvider` so `useIsMobile` can detect the viewport; below the `desktop` breakpoint the sidebar renders inside a [Drawer](/docs/components-drawer--docs) and is toggled with `SidebarTrigger`.
94
+ - Use `SidebarMenuButton asChild` to render your router's link component (or an `a` tag) so navigation items are real links.
95
+ - Give menu buttons a `tooltip` so labels remain discoverable when the sidebar is collapsed to icons.
96
+ - Set `isActive` on the menu button for the current route.
97
+ - Include a `SidebarRail` so users can toggle the sidebar by clicking its edge.
98
+
99
+ ## Collapsible Behavior
100
+
101
+ The `collapsible` prop on `Sidebar` controls what happens when the sidebar is toggled closed:
102
+
103
+ - `offcanvas` (default) — the sidebar slides fully out of view.
104
+ - `icon` — the sidebar collapses to a narrow icon strip; menu button tooltips appear on hover.
105
+ - `none` — the sidebar cannot collapse and stays at full width.
106
+
107
+ ## Right Sidebar
108
+
109
+ Set `side="right"` to render a sidebar on the opposite edge, useful for contextual panels. A right sidebar is wider by default (`24rem` vs `16rem`). Use `SidebarTrigger side="right"` to toggle it from within your content.
110
+
111
+ <Canvas of={Stories.SidebarRightExample} />
112
+
113
+ ## Left and Right Together
114
+
115
+ One `SidebarProvider` manages both sides independently. Pass per-side values to `defaultOpen` and `storageKey` as objects with `left` and `right` keys.
116
+
117
+ <Canvas of={Stories.SidebarBothSides} />
118
+
119
+ ## Collapsed by Default
120
+
121
+ Pass `defaultOpen={false}` to the provider to start collapsed.
122
+
123
+ <Canvas of={Stories.SidebarCollapsed} />
124
+
125
+ ## State Persistence
126
+
127
+ Whenever the user toggles a sidebar, the provider writes the new state to `localStorage` under the resolved `storageKey` (a string key applies to the left sidebar, with `_right` appended for the right sidebar; pass an object to set each side explicitly). Reading the stored value back is left to your app — pass it to `defaultOpen` on mount:
128
+
129
+ ```tsx
130
+ // Guard the read so it is safe in SSR environments (e.g. Next.js),
131
+ // where localStorage does not exist on the server.
132
+ const startExpanded =
133
+ typeof window !== 'undefined'
134
+ ? localStorage.getItem('sidebar_expanded') !== 'false'
135
+ : true;
136
+
137
+ <SidebarProvider storageKey="sidebar_expanded" defaultOpen={startExpanded}>
138
+ ...
139
+ </SidebarProvider>;
140
+ ```
141
+
142
+ ## Controlled Sidebar
143
+
144
+ Pass `open` (boolean, or `{ left, right }`) to control the sidebar yourself, and use `onOpenChange(open, side)` to respond to toggle requests. Omit `open` for uncontrolled behavior with `defaultOpen`.
145
+
146
+ ## Keyboard Shortcuts
147
+
148
+ - <kbd>[</kbd> toggles the left sidebar.
149
+ - <kbd>]</kbd> toggles the right sidebar.
150
+
151
+ Shortcuts are ignored while focus is in an input, textarea, select, or content-editable element.
152
+
153
+ ## useSidebar
154
+
155
+ Call `useSidebar()` inside a `SidebarProvider` to read or control sidebar state from your own components. Inside a `Sidebar` it resolves to that sidebar's side; elsewhere pass the side explicitly: `useSidebar('right')`.
156
+
157
+ It returns:
158
+
159
+ | Property | Type | Description |
160
+ | --------------- | ----------------------------- | -------------------------------------------------- |
161
+ | `state` | `'expanded' \| 'collapsed'` | Current visual state. |
162
+ | `open` | `boolean` | Whether the (desktop) sidebar is open. |
163
+ | `setOpen` | `(open: boolean) => void` | Set the desktop open state. |
164
+ | `openMobile` | `boolean` | Whether the mobile drawer is open. |
165
+ | `setOpenMobile` | `(open: boolean) => void` | Set the mobile drawer state. |
166
+ | `toggleSidebar` | `() => void` | Toggle the sidebar (desktop or mobile as needed). |
167
+ | `isMobile` | `boolean` | Whether the viewport is below the desktop breakpoint. |
168
+ | `side` | `'left' \| 'right'` | Which side this state refers to. |
169
+
170
+ ## Props
171
+
172
+ All components forward refs and accept the standard props of the elements they render (`className`, `style`, event handlers, etc.).
173
+
174
+ ### SidebarProvider
175
+
176
+ <ArgTypes of={SidebarProvider} />
177
+
178
+ ### Sidebar
179
+
180
+ <ArgTypes of={Sidebar} />
181
+
182
+ ### SidebarTrigger
183
+
184
+ Extends [Button](/docs/components-button--docs).
185
+
186
+ <ArgTypes of={SidebarTrigger} />
187
+
188
+ ### SidebarMenuButton
189
+
190
+ <ArgTypes of={SidebarMenuButton} />
191
+
192
+ ## Theming
193
+
194
+ The sidebar exposes CSS custom properties you can override on `SidebarProvider` (via `style`) or a parent element:
195
+
196
+ | Custom Property | Default | Description |
197
+ | ------------------------------ | ---------- | ----------------------------------------------- |
198
+ | `--sidebar-width` | `16rem` | Expanded width (`24rem` for a right sidebar). |
199
+ | `--sidebar-width-icon` | `44px` | Width when collapsed to icons. |
200
+ | `--sidebar-transition-duration`| `200ms` | Expand/collapse animation duration. |
201
+ | `--sidebar-transition-timing` | `linear` | Expand/collapse animation timing function. |
202
+
203
+ ## Accessibility
204
+
205
+ - `SidebarTrigger` renders a real button with an `aria-label` of "Toggle left sidebar" / "Toggle right sidebar".
206
+ - `SidebarRail` is removed from the tab order (`tabIndex={-1}`) since `SidebarTrigger` and the keyboard shortcuts cover keyboard users; it exposes an `aria-label` and a `title` with the shortcut.
207
+ - `SidebarMenu` and `SidebarMenuSub` render semantic lists, and `asChild` lets menu items render real links for correct link semantics.
208
+ - On mobile the sidebar renders inside a [Drawer](/docs/components-drawer--docs), which traps focus and closes with <kbd>Esc</kbd>.
@@ -50,6 +50,19 @@ const meta: Meta<typeof Sidebar> = {
50
50
  parameters: {
51
51
  layout: 'fullscreen',
52
52
  overrideDecorator: true,
53
+ // The sidebar examples fill 100svh, so render them in a fixed-height
54
+ // iframe on the docs page instead of inline at full height.
55
+ docs: {
56
+ story: {
57
+ inline: false,
58
+ height: '640px',
59
+ },
60
+ // Scopes the docs-page CSS in Sidebar.mdx (which forces a desktop-width
61
+ // viewport on the story iframes) to this page's Canvas blocks only.
62
+ canvas: {
63
+ className: 'sidebar-docs-canvas',
64
+ },
65
+ },
53
66
  chromatic: {
54
67
  modes: {
55
68
  light: allModes['light'],