@hyphen/hyphen-components 7.7.0 → 7.8.1
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/css/fonts.css +3 -3
- package/dist/css/index.css +108 -108
- package/dist/css/reset.css +3 -3
- package/dist/css/utilities.css +7 -7
- package/dist/css/variables.css +60 -62
- package/dist/hyphen-components.cjs.development.js.map +1 -1
- package/dist/hyphen-components.cjs.production.min.js.map +1 -1
- package/dist/hyphen-components.esm.js.map +1 -1
- package/dist/index.d.ts +44 -2
- package/package.json +21 -36
- package/src/components/FormControl/FormControl.stories.tsx +92 -0
- package/src/components/Heading/Heading.tsx +2 -2
- package/src/components/HelpText/HelpText.stories.tsx +45 -0
- package/src/components/InputValidationMessage/InputValidationMessage.stories.tsx +50 -0
- package/src/components/Sidebar/Sidebar.mdx +208 -0
- package/src/components/Sidebar/Sidebar.stories.tsx +13 -0
- package/src/components/Sidebar/Sidebar.tsx +42 -0
- package/src/docs/Brands.mdx +14 -12
- package/src/docs/DesignTokens.mdx +1 -1
- package/src/docs/intro.mdx +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -2004,14 +2004,14 @@ interface HeadingProps {
|
|
|
2004
2004
|
className?: string;
|
|
2005
2005
|
/**
|
|
2006
2006
|
* A variant token identifier to use for the text variant. Available variants found:
|
|
2007
|
-
* [here](https://github.com/
|
|
2007
|
+
* [here](https://github.com/Hyphen/hyphen-components/blob/main/packages/design-tokens/tokens/color/font.json).
|
|
2008
2008
|
*/
|
|
2009
2009
|
color?: FontColor;
|
|
2010
2010
|
/**
|
|
2011
2011
|
* By default, size is determined by the chosen tag (e.g. h1 is bigger than h2).
|
|
2012
2012
|
* However, size can be set independently so that its size is appropriate for the surrounding content.
|
|
2013
2013
|
* Available sizes found:
|
|
2014
|
-
* [here](https://github.com/
|
|
2014
|
+
* [here](https://github.com/Hyphen/hyphen-components/blob/main/packages/design-tokens/tokens/size/font.json).
|
|
2015
2015
|
*/
|
|
2016
2016
|
size?: HeadingSize | ResponsiveProp<HeadingSize>;
|
|
2017
2017
|
/**
|
|
@@ -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,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyphen/hyphen-components",
|
|
3
|
-
"version": "7.
|
|
4
|
-
"packageManager": "pnpm@10.20.0",
|
|
3
|
+
"version": "7.8.1",
|
|
5
4
|
"license": "MIT",
|
|
6
5
|
"author": {
|
|
7
6
|
"name": "@hyphen"
|
|
@@ -16,34 +15,8 @@
|
|
|
16
15
|
],
|
|
17
16
|
"repository": {
|
|
18
17
|
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/Hyphen/hyphen-components.git"
|
|
20
|
-
|
|
21
|
-
"scripts": {
|
|
22
|
-
"build": "cross-env IS_PUBLISHING=true tsup && cross-env IS_PUBLISHING=true webpack --config webpack.config.js && node scripts/create-cjs-shim.js",
|
|
23
|
-
"build-storybook": "storybook build",
|
|
24
|
-
"build-storybook-docs": "storybook build --docs",
|
|
25
|
-
"storybook-docs": "storybook dev --docs",
|
|
26
|
-
"lint": "eslint src --ext .ts,.tsx",
|
|
27
|
-
"lint-fix": "eslint src --ext .ts,.tsx --fix",
|
|
28
|
-
"size": "size-limit",
|
|
29
|
-
"start": "tsup --watch",
|
|
30
|
-
"storybook": "storybook dev -p 6006",
|
|
31
|
-
"test": "jest --passWithNoTests",
|
|
32
|
-
"test-watch": "jest --coverage --watch",
|
|
33
|
-
"semantic-release": "semantic-release",
|
|
34
|
-
"prepare": "pnpm run build",
|
|
35
|
-
"chromatic": "chromatic --project-token=chpt_8fddffb5556f0b7 --only-changed"
|
|
36
|
-
},
|
|
37
|
-
"husky": {
|
|
38
|
-
"hooks": {
|
|
39
|
-
"pre-commit": "eslint src --ext .ts,.tsx"
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
"prettier": {
|
|
43
|
-
"printWidth": 80,
|
|
44
|
-
"semi": true,
|
|
45
|
-
"singleQuote": true,
|
|
46
|
-
"trailingComma": "es5"
|
|
18
|
+
"url": "git+https://github.com/Hyphen/hyphen-components.git",
|
|
19
|
+
"directory": "packages/components"
|
|
47
20
|
},
|
|
48
21
|
"peerDependencies": {
|
|
49
22
|
"react": ">=16"
|
|
@@ -64,7 +37,6 @@
|
|
|
64
37
|
"dependencies": {
|
|
65
38
|
"@daypicker/react": "^10.0.1",
|
|
66
39
|
"@emotion/react": "^11.14.0",
|
|
67
|
-
"@hyphen/hyphen-design-tokens": "^7.7.0",
|
|
68
40
|
"@radix-ui/react-aspect-ratio": "^1.1.9",
|
|
69
41
|
"@radix-ui/react-collapsible": "^1.1.13",
|
|
70
42
|
"@radix-ui/react-dropdown-menu": "^2.1.17",
|
|
@@ -83,12 +55,12 @@
|
|
|
83
55
|
"react-router-dom": "^6.30.4",
|
|
84
56
|
"react-select": "^5.10.0",
|
|
85
57
|
"react-select-event": "^5.5.1",
|
|
86
|
-
"uuid": "^11.1.1"
|
|
58
|
+
"uuid": "^11.1.1",
|
|
59
|
+
"@hyphen/hyphen-design-tokens": "^7.8.1"
|
|
87
60
|
},
|
|
88
61
|
"devDependencies": {
|
|
89
62
|
"@babel/core": "^7.27.4",
|
|
90
63
|
"@chromatic-com/storybook": "^5.2.1",
|
|
91
|
-
"@semantic-release/commit-analyzer": "^11.1.0",
|
|
92
64
|
"@size-limit/preset-small-lib": "^11.2.0",
|
|
93
65
|
"@storybook/addon-a11y": "10.4.2",
|
|
94
66
|
"@storybook/addon-docs": "10.4.2",
|
|
@@ -124,7 +96,6 @@
|
|
|
124
96
|
"eslint-plugin-react": "^7.34.0",
|
|
125
97
|
"eslint-plugin-react-hooks": "^4.6.0",
|
|
126
98
|
"eslint-plugin-storybook": "10.4.2",
|
|
127
|
-
"husky": "^8.0.3",
|
|
128
99
|
"identity-obj-proxy": "^3.0.0",
|
|
129
100
|
"jest": "^29.7.0",
|
|
130
101
|
"jest-environment-jsdom": "^30.2.0",
|
|
@@ -137,9 +108,9 @@
|
|
|
137
108
|
"react": "^18.3.1",
|
|
138
109
|
"react-dom": "^18.3.1",
|
|
139
110
|
"react-is": "^18.3.1",
|
|
111
|
+
"remark-gfm": "^4.0.1",
|
|
140
112
|
"sass": "^1.77.8",
|
|
141
113
|
"sass-loader": "^13.3.3",
|
|
142
|
-
"semantic-release": "^25.0.2",
|
|
143
114
|
"size-limit": "^11.2.0",
|
|
144
115
|
"storybook": "10.4.2",
|
|
145
116
|
"ts-jest": "^29.4.6",
|
|
@@ -152,5 +123,19 @@
|
|
|
152
123
|
"webpack": "^5.94.0",
|
|
153
124
|
"webpack-cli": "^5.1.4",
|
|
154
125
|
"webpack-fix-style-only-entries": "^0.6.1"
|
|
126
|
+
},
|
|
127
|
+
"scripts": {
|
|
128
|
+
"build": "cross-env IS_PUBLISHING=true tsup && cross-env IS_PUBLISHING=true webpack --config webpack.config.js && node scripts/create-cjs-shim.js",
|
|
129
|
+
"build-storybook": "storybook build",
|
|
130
|
+
"build-storybook-docs": "storybook build --docs",
|
|
131
|
+
"storybook-docs": "storybook dev --docs",
|
|
132
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
133
|
+
"lint-fix": "eslint src --ext .ts,.tsx --fix",
|
|
134
|
+
"size": "size-limit",
|
|
135
|
+
"start": "tsup --watch",
|
|
136
|
+
"storybook": "storybook dev -p 6006",
|
|
137
|
+
"test": "jest --passWithNoTests",
|
|
138
|
+
"test-watch": "jest --coverage --watch",
|
|
139
|
+
"chromatic": "chromatic --only-changed"
|
|
155
140
|
}
|
|
156
|
-
}
|
|
141
|
+
}
|
|
@@ -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
|
+
};
|
|
@@ -26,14 +26,14 @@ export interface HeadingProps {
|
|
|
26
26
|
className?: string;
|
|
27
27
|
/**
|
|
28
28
|
* A variant token identifier to use for the text variant. Available variants found:
|
|
29
|
-
* [here](https://github.com/
|
|
29
|
+
* [here](https://github.com/Hyphen/hyphen-components/blob/main/packages/design-tokens/tokens/color/font.json).
|
|
30
30
|
*/
|
|
31
31
|
color?: FontColor;
|
|
32
32
|
/**
|
|
33
33
|
* By default, size is determined by the chosen tag (e.g. h1 is bigger than h2).
|
|
34
34
|
* However, size can be set independently so that its size is appropriate for the surrounding content.
|
|
35
35
|
* Available sizes found:
|
|
36
|
-
* [here](https://github.com/
|
|
36
|
+
* [here](https://github.com/Hyphen/hyphen-components/blob/main/packages/design-tokens/tokens/size/font.json).
|
|
37
37
|
*/
|
|
38
38
|
size?: HeadingSize | ResponsiveProp<HeadingSize>;
|
|
39
39
|
/**
|
|
@@ -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'],
|