@ds-mo/ui 0.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/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +969 -0
- package/dist/index.js +2623 -0
- package/dist/index.js.map +1 -0
- package/dist/r/accordion.json +79 -0
- package/dist/r/badge.json +69 -0
- package/dist/r/banner.json +90 -0
- package/dist/r/breadcrumb.json +70 -0
- package/dist/r/button-group.json +65 -0
- package/dist/r/button.json +137 -0
- package/dist/r/card.json +82 -0
- package/dist/r/checkbox.json +91 -0
- package/dist/r/divider.json +57 -0
- package/dist/r/empty-state.json +69 -0
- package/dist/r/error-boundary.json +66 -0
- package/dist/r/fade.json +65 -0
- package/dist/r/field.json +71 -0
- package/dist/r/header.json +67 -0
- package/dist/r/input.json +77 -0
- package/dist/r/loader.json +54 -0
- package/dist/r/menu.json +130 -0
- package/dist/r/modal.json +93 -0
- package/dist/r/pagination.json +80 -0
- package/dist/r/radio.json +85 -0
- package/dist/r/registry.json +1600 -0
- package/dist/r/scrollbar.json +68 -0
- package/dist/r/select.json +93 -0
- package/dist/r/sidebar.json +105 -0
- package/dist/r/skeleton.json +72 -0
- package/dist/r/slider.json +87 -0
- package/dist/r/surface.json +97 -0
- package/dist/r/tab-group.json +72 -0
- package/dist/r/tab.json +71 -0
- package/dist/r/table.json +110 -0
- package/dist/r/tag.json +110 -0
- package/dist/r/text.json +94 -0
- package/dist/r/toast.json +77 -0
- package/dist/r/toggle-button-group.json +65 -0
- package/dist/r/toggle-button.json +94 -0
- package/dist/r/toggle.json +65 -0
- package/dist/r/tooltip.json +86 -0
- package/package.json +80 -0
- package/src/components/Accordion/Accordion.module.css +66 -0
- package/src/components/Accordion/Accordion.stories.tsx +95 -0
- package/src/components/Accordion/Accordion.tsx +122 -0
- package/src/components/Accordion/index.ts +2 -0
- package/src/components/Badge/Badge.module.css +6 -0
- package/src/components/Badge/Badge.stories.tsx +99 -0
- package/src/components/Badge/Badge.tsx +23 -0
- package/src/components/Badge/index.ts +2 -0
- package/src/components/Banner/Banner.module.css +67 -0
- package/src/components/Banner/Banner.stories.tsx +81 -0
- package/src/components/Banner/Banner.tsx +109 -0
- package/src/components/Banner/index.ts +2 -0
- package/src/components/Breadcrumb/Breadcrumb.module.css +46 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +72 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +68 -0
- package/src/components/Breadcrumb/index.ts +2 -0
- package/src/components/Button/Button.module.css +343 -0
- package/src/components/Button/Button.stories.tsx +575 -0
- package/src/components/Button/Button.tsx +201 -0
- package/src/components/Button/index.ts +10 -0
- package/src/components/ButtonGroup/ButtonGroup.module.css +157 -0
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +384 -0
- package/src/components/ButtonGroup/ButtonGroup.tsx +122 -0
- package/src/components/ButtonGroup/index.ts +2 -0
- package/src/components/Card/Card.module.css +36 -0
- package/src/components/Card/Card.stories.tsx +78 -0
- package/src/components/Card/Card.tsx +53 -0
- package/src/components/Card/index.ts +2 -0
- package/src/components/Checkbox/Checkbox.module.css +47 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +129 -0
- package/src/components/Checkbox/Checkbox.tsx +68 -0
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/Divider/Divider.module.css +19 -0
- package/src/components/Divider/Divider.stories.tsx +55 -0
- package/src/components/Divider/Divider.tsx +28 -0
- package/src/components/Divider/index.ts +2 -0
- package/src/components/EmptyState/EmptyState.module.css +8 -0
- package/src/components/EmptyState/EmptyState.stories.tsx +53 -0
- package/src/components/EmptyState/EmptyState.tsx +31 -0
- package/src/components/EmptyState/index.ts +2 -0
- package/src/components/ErrorBoundary/ErrorBoundary.module.css +18 -0
- package/src/components/ErrorBoundary/ErrorBoundary.stories.tsx +38 -0
- package/src/components/ErrorBoundary/ErrorBoundary.tsx +50 -0
- package/src/components/ErrorBoundary/index.ts +2 -0
- package/src/components/Fade/Fade.module.css +31 -0
- package/src/components/Fade/Fade.stories.tsx +35 -0
- package/src/components/Fade/Fade.tsx +33 -0
- package/src/components/Fade/index.ts +2 -0
- package/src/components/Field/Field.module.css +5 -0
- package/src/components/Field/Field.stories.tsx +111 -0
- package/src/components/Field/Field.tsx +22 -0
- package/src/components/Field/index.ts +2 -0
- package/src/components/Header/Header.module.css +33 -0
- package/src/components/Header/Header.stories.tsx +52 -0
- package/src/components/Header/Header.tsx +42 -0
- package/src/components/Header/index.ts +2 -0
- package/src/components/Input/Input.module.css +71 -0
- package/src/components/Input/Input.stories.tsx +135 -0
- package/src/components/Input/Input.tsx +76 -0
- package/src/components/Input/index.ts +2 -0
- package/src/components/LabelWrap/LabelWrap.module.css +34 -0
- package/src/components/LabelWrap/LabelWrap.tsx +32 -0
- package/src/components/LabelWrap/index.ts +1 -0
- package/src/components/Loader/Loader.module.css +9 -0
- package/src/components/Loader/Loader.stories.tsx +44 -0
- package/src/components/Loader/Loader.tsx +43 -0
- package/src/components/Loader/index.ts +2 -0
- package/src/components/Menu/DestructiveMenuItem.module.css +57 -0
- package/src/components/Menu/DestructiveMenuItem.tsx +64 -0
- package/src/components/Menu/Menu.module.css +49 -0
- package/src/components/Menu/Menu.stories.tsx +129 -0
- package/src/components/Menu/Menu.tsx +269 -0
- package/src/components/Menu/MenuItem.module.css +84 -0
- package/src/components/Menu/MenuItem.tsx +101 -0
- package/src/components/Menu/index.ts +6 -0
- package/src/components/Modal/Modal.module.css +67 -0
- package/src/components/Modal/Modal.stories.tsx +71 -0
- package/src/components/Modal/Modal.tsx +122 -0
- package/src/components/Modal/index.ts +2 -0
- package/src/components/Pagination/Pagination.module.css +64 -0
- package/src/components/Pagination/Pagination.stories.tsx +51 -0
- package/src/components/Pagination/Pagination.tsx +103 -0
- package/src/components/Pagination/index.ts +2 -0
- package/src/components/Radio/Radio.module.css +61 -0
- package/src/components/Radio/Radio.stories.tsx +101 -0
- package/src/components/Radio/Radio.tsx +108 -0
- package/src/components/Radio/index.ts +2 -0
- package/src/components/Scrollbar/Scrollbar.module.css +81 -0
- package/src/components/Scrollbar/Scrollbar.stories.tsx +46 -0
- package/src/components/Scrollbar/Scrollbar.tsx +187 -0
- package/src/components/Scrollbar/index.ts +2 -0
- package/src/components/Select/Select.module.css +31 -0
- package/src/components/Select/Select.stories.tsx +59 -0
- package/src/components/Select/Select.tsx +92 -0
- package/src/components/Select/index.ts +2 -0
- package/src/components/Sidebar/Sidebar.module.css +161 -0
- package/src/components/Sidebar/Sidebar.stories.tsx +125 -0
- package/src/components/Sidebar/Sidebar.tsx +129 -0
- package/src/components/Sidebar/SidebarItem.tsx +57 -0
- package/src/components/Sidebar/SidebarSection.tsx +42 -0
- package/src/components/Sidebar/index.ts +6 -0
- package/src/components/Skeleton/Skeleton.module.css +53 -0
- package/src/components/Skeleton/Skeleton.stories.tsx +89 -0
- package/src/components/Skeleton/Skeleton.tsx +66 -0
- package/src/components/Skeleton/index.ts +2 -0
- package/src/components/Slider/Slider.module.css +62 -0
- package/src/components/Slider/Slider.stories.tsx +120 -0
- package/src/components/Slider/Slider.tsx +74 -0
- package/src/components/Slider/index.ts +2 -0
- package/src/components/Surface/Surface.module.css +222 -0
- package/src/components/Surface/Surface.stories.tsx +213 -0
- package/src/components/Surface/Surface.tsx +219 -0
- package/src/components/Surface/index.ts +12 -0
- package/src/components/Tab/Tab.module.css +66 -0
- package/src/components/Tab/Tab.stories.tsx +103 -0
- package/src/components/Tab/Tab.tsx +41 -0
- package/src/components/Tab/index.ts +2 -0
- package/src/components/TabGroup/TabGroup.module.css +26 -0
- package/src/components/TabGroup/TabGroup.stories.tsx +60 -0
- package/src/components/TabGroup/TabGroup.tsx +83 -0
- package/src/components/TabGroup/index.ts +2 -0
- package/src/components/Table/Table.module.css +130 -0
- package/src/components/Table/Table.stories.tsx +111 -0
- package/src/components/Table/Table.tsx +226 -0
- package/src/components/Table/index.ts +11 -0
- package/src/components/Tag/Tag.module.css +270 -0
- package/src/components/Tag/Tag.stories.tsx +399 -0
- package/src/components/Tag/Tag.tsx +162 -0
- package/src/components/Tag/index.ts +2 -0
- package/src/components/Text/Text.module.css +166 -0
- package/src/components/Text/Text.stories.tsx +212 -0
- package/src/components/Text/Text.tsx +181 -0
- package/src/components/Text/index.ts +13 -0
- package/src/components/Toast/Toast.module.css +91 -0
- package/src/components/Toast/Toast.stories.tsx +66 -0
- package/src/components/Toast/Toast.tsx +182 -0
- package/src/components/Toast/index.ts +8 -0
- package/src/components/Toggle/Toggle.module.css +87 -0
- package/src/components/Toggle/Toggle.stories.tsx +85 -0
- package/src/components/Toggle/Toggle.tsx +52 -0
- package/src/components/Toggle/index.ts +2 -0
- package/src/components/ToggleButton/ToggleButton.module.css +172 -0
- package/src/components/ToggleButton/ToggleButton.stories.tsx +299 -0
- package/src/components/ToggleButton/ToggleButton.tsx +118 -0
- package/src/components/ToggleButton/index.ts +2 -0
- package/src/components/ToggleButtonGroup/ToggleButtonGroup.module.css +163 -0
- package/src/components/ToggleButtonGroup/ToggleButtonGroup.stories.tsx +341 -0
- package/src/components/ToggleButtonGroup/ToggleButtonGroup.tsx +111 -0
- package/src/components/ToggleButtonGroup/index.ts +2 -0
- package/src/components/Tooltip/Tooltip.module.css +45 -0
- package/src/components/Tooltip/Tooltip.stories.tsx +46 -0
- package/src/components/Tooltip/Tooltip.tsx +224 -0
- package/src/components/Tooltip/index.ts +2 -0
- package/src/css-modules.d.ts +4 -0
- package/src/docs/ColorUsage.mdx +195 -0
- package/src/docs/ElevationUsage.mdx +95 -0
- package/src/docs/Introduction.mdx +97 -0
- package/src/docs/OpticalSizing.mdx +178 -0
- package/src/docs/TypographyUsage.mdx +121 -0
- package/src/index.ts +154 -0
- package/src/stories/Colors.stories.tsx +528 -0
- package/src/stories/Dimensions.stories.tsx +200 -0
- package/src/stories/Effects.stories.tsx +231 -0
- package/src/stories/Icons.stories.tsx +159 -0
- package/src/stories/Typography.stories.tsx +157 -0
- package/src/types/icons.ts +15 -0
- package/src/types/index.ts +1 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/css-tokens.ts +22 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
.checkbox {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
gap: var(--dimension-space-050);
|
|
5
|
+
padding: var(--dimension-space-075);
|
|
6
|
+
height: var(--dimension-size-400);
|
|
7
|
+
box-sizing: border-box;
|
|
8
|
+
cursor: pointer;
|
|
9
|
+
-webkit-user-select: none;
|
|
10
|
+
user-select: none;
|
|
11
|
+
border-radius: var(--dimension-radius-075);
|
|
12
|
+
transition: background-color var(--effect-motion-short-2);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.checkbox:hover { background-color: var(--color-interaction-hover); }
|
|
16
|
+
.checkbox:active { background-color: var(--color-interaction-pressed); }
|
|
17
|
+
|
|
18
|
+
.inactive {
|
|
19
|
+
cursor: not-allowed;
|
|
20
|
+
opacity: 0.5;
|
|
21
|
+
pointer-events: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Fallback checkbox box (when no icon components provided) */
|
|
25
|
+
.box {
|
|
26
|
+
width: 20px;
|
|
27
|
+
height: 20px;
|
|
28
|
+
border: 1.5px solid var(--color-foreground-secondary);
|
|
29
|
+
border-radius: var(--dimension-radius-050);
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
flex-shrink: 0;
|
|
34
|
+
transition: all var(--effect-motion-short-2);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.boxChecked {
|
|
38
|
+
background-color: var(--color-foreground-primary);
|
|
39
|
+
border-color: var(--color-foreground-primary);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.checkmark {
|
|
43
|
+
color: var(--color-foreground-on-bold-background-primary);
|
|
44
|
+
font-size: 14px;
|
|
45
|
+
line-height: 1;
|
|
46
|
+
font-weight: 700;
|
|
47
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { Checkbox } from './Checkbox';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Checkbox> = {
|
|
6
|
+
title: 'Primitives/Checkbox',
|
|
7
|
+
component: Checkbox,
|
|
8
|
+
args: {
|
|
9
|
+
label: 'Accept terms',
|
|
10
|
+
checked: false,
|
|
11
|
+
indeterminate: false,
|
|
12
|
+
inactive: false,
|
|
13
|
+
},
|
|
14
|
+
argTypes: {
|
|
15
|
+
label: { control: 'text' },
|
|
16
|
+
checked: { control: 'boolean' },
|
|
17
|
+
indeterminate: { control: 'boolean' },
|
|
18
|
+
inactive: { control: 'boolean' },
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof Checkbox>;
|
|
24
|
+
|
|
25
|
+
const Controlled = (props: React.ComponentProps<typeof Checkbox>) => {
|
|
26
|
+
const [checked, setChecked] = useState(props.checked ?? false);
|
|
27
|
+
return <Checkbox {...props} checked={checked} onChange={setChecked} />;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const Playground: Story = {
|
|
31
|
+
render: (args) => <Controlled {...args} />,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// ─── Matrix ───────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const col: React.CSSProperties = { display: 'flex', flexDirection: 'column', gap: 10 };
|
|
37
|
+
const row: React.CSSProperties = { display: 'flex', gap: 32, alignItems: 'center' };
|
|
38
|
+
const labelStyle: React.CSSProperties = { fontSize: 10, fontFamily: 'monospace', color: '#888', minWidth: 80, flexShrink: 0 };
|
|
39
|
+
const section = (text: string) => (
|
|
40
|
+
<div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '0.05em', textTransform: 'uppercase', color: '#555', marginTop: 8 }}>
|
|
41
|
+
{text}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
export const Matrix: Story = {
|
|
46
|
+
parameters: { layout: 'padded' },
|
|
47
|
+
render: () => (
|
|
48
|
+
<div style={{ ...col, fontFamily: 'sans-serif', gap: 0 }}>
|
|
49
|
+
{section('States × Enabled / Disabled')}
|
|
50
|
+
<div style={{ ...col, marginTop: 12 }}>
|
|
51
|
+
<div style={row}>
|
|
52
|
+
<span style={labelStyle}></span>
|
|
53
|
+
<span style={{ ...labelStyle, textAlign: 'center' }}>enabled</span>
|
|
54
|
+
<span style={{ ...labelStyle, textAlign: 'center' }}>disabled</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div style={row}>
|
|
57
|
+
<span style={labelStyle}>unchecked</span>
|
|
58
|
+
<Controlled label="Option" checked={false} />
|
|
59
|
+
<Checkbox label="Option" checked={false} inactive onChange={() => {}} />
|
|
60
|
+
</div>
|
|
61
|
+
<div style={row}>
|
|
62
|
+
<span style={labelStyle}>checked</span>
|
|
63
|
+
<Controlled label="Option" checked={true} />
|
|
64
|
+
<Checkbox label="Option" checked={true} inactive onChange={() => {}} />
|
|
65
|
+
</div>
|
|
66
|
+
<div style={row}>
|
|
67
|
+
<span style={labelStyle}>indeterminate</span>
|
|
68
|
+
<Checkbox label="Select all" checked={false} indeterminate onChange={() => {}} />
|
|
69
|
+
<Checkbox label="Select all" checked={false} indeterminate inactive onChange={() => {}} />
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// ─── Individual stories ───────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export const Default: Story = {
|
|
79
|
+
render: () => <Controlled label="Accept terms and conditions" />,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const Checked: Story = {
|
|
83
|
+
render: () => <Controlled label="Checked option" checked />,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const Indeterminate: Story = {
|
|
87
|
+
render: () => (
|
|
88
|
+
<Checkbox
|
|
89
|
+
label="Select all (3 of 5 selected)"
|
|
90
|
+
checked={false}
|
|
91
|
+
indeterminate
|
|
92
|
+
onChange={() => {}}
|
|
93
|
+
/>
|
|
94
|
+
),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const Inactive: Story = {
|
|
98
|
+
render: () => (
|
|
99
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
|
100
|
+
<Checkbox label="Disabled unchecked" checked={false} inactive onChange={() => {}} />
|
|
101
|
+
<Checkbox label="Disabled checked" checked={true} inactive onChange={() => {}} />
|
|
102
|
+
<Checkbox label="Disabled indeterminate" checked={false} indeterminate inactive onChange={() => {}} />
|
|
103
|
+
</div>
|
|
104
|
+
),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const Group: Story = {
|
|
108
|
+
render: () => {
|
|
109
|
+
const [selected, setSelected] = useState<Set<string>>(new Set(['option-b']));
|
|
110
|
+
const toggle = (key: string) => setSelected(prev => {
|
|
111
|
+
const next = new Set(prev);
|
|
112
|
+
next.has(key) ? next.delete(key) : next.add(key);
|
|
113
|
+
return next;
|
|
114
|
+
});
|
|
115
|
+
const options = ['Option A', 'Option B', 'Option C', 'Option D'];
|
|
116
|
+
return (
|
|
117
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
118
|
+
{options.map(opt => (
|
|
119
|
+
<Checkbox
|
|
120
|
+
key={opt}
|
|
121
|
+
label={opt}
|
|
122
|
+
checked={selected.has(opt.toLowerCase().replace(' ', '-'))}
|
|
123
|
+
onChange={() => toggle(opt.toLowerCase().replace(' ', '-'))}
|
|
124
|
+
/>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { cn } from '@/utils/cn';
|
|
3
|
+
import { Text } from '@/components/Text';
|
|
4
|
+
import type { IconComponent } from '@/types/icons';
|
|
5
|
+
import styles from './Checkbox.module.css';
|
|
6
|
+
|
|
7
|
+
export interface CheckboxProps {
|
|
8
|
+
label: string;
|
|
9
|
+
checked?: boolean;
|
|
10
|
+
indeterminate?: boolean;
|
|
11
|
+
onChange?: (checked: boolean) => void;
|
|
12
|
+
inactive?: boolean;
|
|
13
|
+
className?: string;
|
|
14
|
+
/** Icon for checked state. */
|
|
15
|
+
checkedIcon?: IconComponent;
|
|
16
|
+
/** Icon for unchecked state. */
|
|
17
|
+
uncheckedIcon?: IconComponent;
|
|
18
|
+
/** Icon for indeterminate state. */
|
|
19
|
+
indeterminateIcon?: IconComponent;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const Checkbox = forwardRef<HTMLDivElement, CheckboxProps>(
|
|
23
|
+
(
|
|
24
|
+
{
|
|
25
|
+
label,
|
|
26
|
+
checked = false,
|
|
27
|
+
indeterminate = false,
|
|
28
|
+
onChange,
|
|
29
|
+
inactive = false,
|
|
30
|
+
className,
|
|
31
|
+
checkedIcon: CheckedIcon,
|
|
32
|
+
uncheckedIcon: UncheckedIcon,
|
|
33
|
+
indeterminateIcon: IndeterminateIcon,
|
|
34
|
+
},
|
|
35
|
+
ref
|
|
36
|
+
) => {
|
|
37
|
+
const handleClick = () => {
|
|
38
|
+
if (!inactive) onChange?.(!checked);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const iconColor = indeterminate || checked ? 'var(--color-foreground-primary)' : 'var(--color-foreground-secondary)';
|
|
42
|
+
const Icon = indeterminate ? IndeterminateIcon : checked ? CheckedIcon : UncheckedIcon;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
ref={ref}
|
|
47
|
+
role="checkbox"
|
|
48
|
+
aria-checked={indeterminate ? 'mixed' : checked}
|
|
49
|
+
aria-inactive={inactive}
|
|
50
|
+
tabIndex={inactive ? -1 : 0}
|
|
51
|
+
className={cn(styles.checkbox, inactive && styles.inactive, className)}
|
|
52
|
+
onClick={handleClick}
|
|
53
|
+
onKeyDown={e => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); handleClick(); } }}
|
|
54
|
+
>
|
|
55
|
+
{Icon ? <Icon size={20} color={iconColor} /> : (
|
|
56
|
+
<span className={cn(styles.box, (checked || indeterminate) && styles.boxChecked)}>
|
|
57
|
+
{(checked || indeterminate) && <span className={styles.checkmark}>{indeterminate ? '−' : '✓'}</span>}
|
|
58
|
+
</span>
|
|
59
|
+
)}
|
|
60
|
+
<Text variant="text-body-medium" as="span" color="inherit">
|
|
61
|
+
{label}
|
|
62
|
+
</Text>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
Checkbox.displayName = 'Checkbox';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.divider {
|
|
2
|
+
border: none;
|
|
3
|
+
margin: 0;
|
|
4
|
+
flex-shrink: 0;
|
|
5
|
+
background-color: var(--color-border-secondary);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* Horizontal (default) */
|
|
9
|
+
.divider:not(.vertical) {
|
|
10
|
+
width: 100%;
|
|
11
|
+
height: 1px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Vertical */
|
|
15
|
+
.vertical {
|
|
16
|
+
width: 1px;
|
|
17
|
+
height: 100%;
|
|
18
|
+
align-self: stretch;
|
|
19
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { Divider } from './Divider';
|
|
4
|
+
import { Text } from '@/components/Text';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Divider> = {
|
|
7
|
+
title: 'Classic/Divider',
|
|
8
|
+
component: Divider,
|
|
9
|
+
args: {
|
|
10
|
+
orientation: 'horizontal',
|
|
11
|
+
},
|
|
12
|
+
argTypes: {
|
|
13
|
+
orientation: { control: 'select', options: ['horizontal', 'vertical'] },
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof Divider>;
|
|
19
|
+
|
|
20
|
+
export const Playground: Story = {};
|
|
21
|
+
|
|
22
|
+
export const Horizontal: Story = {
|
|
23
|
+
render: () => (
|
|
24
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, width: 300 }}>
|
|
25
|
+
<Text variant="text-body-medium" as="p">Section one content above the divider.</Text>
|
|
26
|
+
<Divider />
|
|
27
|
+
<Text variant="text-body-medium" as="p">Section two content below the divider.</Text>
|
|
28
|
+
</div>
|
|
29
|
+
),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const Vertical: Story = {
|
|
33
|
+
render: () => (
|
|
34
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 16, height: 40 }}>
|
|
35
|
+
<Text variant="text-body-medium" as="span">Left</Text>
|
|
36
|
+
<Divider orientation="vertical" />
|
|
37
|
+
<Text variant="text-body-medium" as="span">Right</Text>
|
|
38
|
+
</div>
|
|
39
|
+
),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const InList: Story = {
|
|
43
|
+
render: () => (
|
|
44
|
+
<div style={{ display: 'flex', flexDirection: 'column', width: 280 }}>
|
|
45
|
+
{['Item 1', 'Item 2', 'Item 3', 'Item 4'].map((item, i) => (
|
|
46
|
+
<React.Fragment key={item}>
|
|
47
|
+
{i > 0 && <Divider />}
|
|
48
|
+
<div style={{ padding: '12px 0' }}>
|
|
49
|
+
<Text variant="text-body-medium" as="span">{item}</Text>
|
|
50
|
+
</div>
|
|
51
|
+
</React.Fragment>
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
),
|
|
55
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { cn } from '@/utils/cn';
|
|
3
|
+
import styles from './Divider.module.css';
|
|
4
|
+
|
|
5
|
+
export type DividerOrientation = 'horizontal' | 'vertical';
|
|
6
|
+
|
|
7
|
+
export interface DividerProps {
|
|
8
|
+
/** Direction of the divider line. */
|
|
9
|
+
orientation?: DividerOrientation;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const Divider = forwardRef<HTMLHRElement, DividerProps>(
|
|
14
|
+
({ orientation = 'horizontal', className }, ref) => (
|
|
15
|
+
<hr
|
|
16
|
+
ref={ref}
|
|
17
|
+
role="separator"
|
|
18
|
+
aria-orientation={orientation}
|
|
19
|
+
className={cn(
|
|
20
|
+
styles.divider,
|
|
21
|
+
orientation === 'vertical' && styles.vertical,
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
Divider.displayName = 'Divider';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { EmptyState } from './EmptyState';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof EmptyState> = {
|
|
5
|
+
title: 'Components/EmptyState',
|
|
6
|
+
component: EmptyState,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default meta;
|
|
10
|
+
type Story = StoryObj<typeof EmptyState>;
|
|
11
|
+
|
|
12
|
+
export const NoContent: Story = {
|
|
13
|
+
args: {
|
|
14
|
+
type: 'no-content',
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const NoResults: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
type: 'no-results',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const NoResultsFilter: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
type: 'no-results-filter',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const NoAccess: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
type: 'no-access',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const CustomMessage: Story = {
|
|
37
|
+
args: {
|
|
38
|
+
type: 'no-content',
|
|
39
|
+
message: 'Create your first project to get started.',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const AllTypes: Story = {
|
|
44
|
+
render: () => (
|
|
45
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24, padding: 24 }}>
|
|
46
|
+
{(['no-content', 'no-results', 'no-results-filter', 'no-access'] as const).map(type => (
|
|
47
|
+
<div key={type} style={{ border: '1px solid var(--color-border-tertiary)', borderRadius: 8, padding: 24 }}>
|
|
48
|
+
<EmptyState type={type} />
|
|
49
|
+
</div>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
),
|
|
53
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { cn } from '@/utils/cn';
|
|
3
|
+
import { Text } from '@/components/Text';
|
|
4
|
+
import styles from './EmptyState.module.css';
|
|
5
|
+
|
|
6
|
+
export type EmptyStateType = 'no-content' | 'no-results' | 'no-results-filter' | 'no-access';
|
|
7
|
+
|
|
8
|
+
const defaultMessages: Record<EmptyStateType, string> = {
|
|
9
|
+
'no-content': 'No page contents to display.',
|
|
10
|
+
'no-results': 'No results found.',
|
|
11
|
+
'no-results-filter': 'No results found. Please update search criteria.',
|
|
12
|
+
'no-access': "You don't have access to this page.",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export interface EmptyStateProps {
|
|
16
|
+
type?: EmptyStateType;
|
|
17
|
+
message?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(
|
|
22
|
+
({ type = 'no-content', message, className }, ref) => (
|
|
23
|
+
<div ref={ref} className={cn(styles.emptyState, className)}>
|
|
24
|
+
<Text variant="text-body-medium" as="p" color="secondary">
|
|
25
|
+
{message || defaultMessages[type]}
|
|
26
|
+
</Text>
|
|
27
|
+
</div>
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
EmptyState.displayName = 'EmptyState';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.errorBoundary {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: center;
|
|
6
|
+
min-height: 400px;
|
|
7
|
+
padding: var(--dimension-space-400);
|
|
8
|
+
gap: var(--dimension-space-200);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.errorDetails {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
align-items: center;
|
|
15
|
+
gap: var(--dimension-space-200);
|
|
16
|
+
max-width: 600px;
|
|
17
|
+
text-align: center;
|
|
18
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { ErrorBoundary } from './ErrorBoundary';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ErrorBoundary> = {
|
|
5
|
+
title: 'Components/ErrorBoundary',
|
|
6
|
+
component: ErrorBoundary,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default meta;
|
|
10
|
+
type Story = StoryObj<typeof ErrorBoundary>;
|
|
11
|
+
|
|
12
|
+
const ThrowingComponent = () => {
|
|
13
|
+
throw new Error('Something went wrong!');
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const WithError: Story = {
|
|
17
|
+
render: () => (
|
|
18
|
+
<ErrorBoundary>
|
|
19
|
+
<ThrowingComponent />
|
|
20
|
+
</ErrorBoundary>
|
|
21
|
+
),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const WithCustomFallback: Story = {
|
|
25
|
+
render: () => (
|
|
26
|
+
<ErrorBoundary fallback={<div style={{ padding: 24, color: 'var(--color-foreground-bold-negative)' }}>Custom error UI</div>}>
|
|
27
|
+
<ThrowingComponent />
|
|
28
|
+
</ErrorBoundary>
|
|
29
|
+
),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const NoError: Story = {
|
|
33
|
+
render: () => (
|
|
34
|
+
<ErrorBoundary>
|
|
35
|
+
<div style={{ padding: 24 }}>This content renders normally.</div>
|
|
36
|
+
</ErrorBoundary>
|
|
37
|
+
),
|
|
38
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React, { Component, ReactNode } from 'react';
|
|
2
|
+
import { EmptyState } from '@/components/EmptyState';
|
|
3
|
+
import { Text } from '@/components/Text';
|
|
4
|
+
import styles from './ErrorBoundary.module.css';
|
|
5
|
+
|
|
6
|
+
export interface ErrorBoundaryProps {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
fallback?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ErrorBoundaryState {
|
|
12
|
+
hasError: boolean;
|
|
13
|
+
error: Error | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
17
|
+
constructor(props: ErrorBoundaryProps) {
|
|
18
|
+
super(props);
|
|
19
|
+
this.state = { hasError: false, error: null };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
|
|
23
|
+
return { hasError: true, error };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
override componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
27
|
+
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
handleReset = () => {
|
|
31
|
+
this.setState({ hasError: false, error: null });
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
override render() {
|
|
35
|
+
if (this.state.hasError) {
|
|
36
|
+
if (this.props.fallback) return this.props.fallback;
|
|
37
|
+
return (
|
|
38
|
+
<div className={styles.errorBoundary}>
|
|
39
|
+
<EmptyState type="no-content" />
|
|
40
|
+
<div className={styles.errorDetails}>
|
|
41
|
+
<Text variant="text-body-small" as="p" color="secondary">
|
|
42
|
+
Something went wrong. Please refresh the page.
|
|
43
|
+
</Text>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return this.props.children;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
.fade {
|
|
2
|
+
position: absolute;
|
|
3
|
+
pointer-events: none;
|
|
4
|
+
z-index: 10;
|
|
5
|
+
transform: translateZ(0);
|
|
6
|
+
backface-visibility: hidden;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@media (prefers-reduced-motion: reduce) {
|
|
10
|
+
.fade { transition: none; }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.fadeTop {
|
|
14
|
+
top: 0; left: 0; right: 0;
|
|
15
|
+
background: linear-gradient(to bottom, var(--background) 0%, var(--background) 50%, transparent 100%);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.fadeBottom {
|
|
19
|
+
bottom: 0; left: 0; right: 0;
|
|
20
|
+
background: linear-gradient(to top, var(--background) 0%, transparent 100%);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.fadeLeft {
|
|
24
|
+
top: 0; bottom: 0; left: 0;
|
|
25
|
+
background: linear-gradient(to right, var(--background) 0%, transparent 100%);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.fadeRight {
|
|
29
|
+
top: 0; bottom: 0; right: 0;
|
|
30
|
+
background: linear-gradient(to left, var(--background) 0%, transparent 100%);
|
|
31
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { Fade } from './Fade';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Fade> = {
|
|
5
|
+
title: 'Layout/Fade',
|
|
6
|
+
component: Fade,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default meta;
|
|
10
|
+
type Story = StoryObj<typeof Fade>;
|
|
11
|
+
|
|
12
|
+
export const Sides: Story = {
|
|
13
|
+
render: () => (
|
|
14
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24, padding: 24 }}>
|
|
15
|
+
{(['top', 'bottom', 'left', 'right'] as const).map(side => (
|
|
16
|
+
<div
|
|
17
|
+
key={side}
|
|
18
|
+
style={{
|
|
19
|
+
position: 'relative',
|
|
20
|
+
height: 120,
|
|
21
|
+
background: 'var(--color-background-secondary)',
|
|
22
|
+
borderRadius: 8,
|
|
23
|
+
overflow: 'hidden',
|
|
24
|
+
display: 'flex',
|
|
25
|
+
alignItems: 'center',
|
|
26
|
+
justifyContent: 'center',
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
<span>{side}</span>
|
|
30
|
+
<Fade side={side} height="40px" />
|
|
31
|
+
</div>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
),
|
|
35
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { cn } from '@/utils/cn';
|
|
3
|
+
import styles from './Fade.module.css';
|
|
4
|
+
|
|
5
|
+
export type FadeSide = 'top' | 'bottom' | 'left' | 'right';
|
|
6
|
+
|
|
7
|
+
export interface FadeProps {
|
|
8
|
+
side: FadeSide;
|
|
9
|
+
height: string;
|
|
10
|
+
background?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
style?: React.CSSProperties;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const sideClassMap: Record<FadeSide, string> = {
|
|
16
|
+
top: 'fadeTop',
|
|
17
|
+
bottom: 'fadeBottom',
|
|
18
|
+
left: 'fadeLeft',
|
|
19
|
+
right: 'fadeRight',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Fade = forwardRef<HTMLDivElement, FadeProps>(
|
|
23
|
+
({ side, height, background = 'var(--color-background-secondary)', className, style = {} }, ref) => (
|
|
24
|
+
<div
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn(styles.fade, styles[sideClassMap[side]], className)}
|
|
27
|
+
style={{ ...style, height, '--background': background } as React.CSSProperties}
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
Fade.displayName = 'Fade';
|