@boostdev/design-system-components 0.1.4 → 0.1.6
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/client.cjs +188 -107
- package/dist/client.css +463 -441
- package/dist/client.d.cts +15 -13
- package/dist/client.d.ts +15 -13
- package/dist/client.js +192 -110
- package/dist/index.cjs +188 -107
- package/dist/index.css +463 -441
- package/dist/index.d.cts +15 -13
- package/dist/index.d.ts +15 -13
- package/dist/index.js +192 -110
- package/package.json +2 -1
- package/src/components/interaction/Button/Button.tsx +35 -8
- package/src/components/interaction/Command/Command.module.css +6 -5
- package/src/components/interaction/Dialog/Dialog.module.css +8 -5
- package/src/components/interaction/Dialog/Dialog.spec.tsx +10 -10
- package/src/components/interaction/Dialog/Dialog.stories.tsx +2 -2
- package/src/components/interaction/Dialog/Dialog.tsx +24 -8
- package/src/components/interaction/Drawer/Drawer.module.css +3 -4
- package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +6 -4
- package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +1 -1
- package/src/components/interaction/DropdownMenu/index.ts +1 -0
- package/src/components/interaction/Popover/Popover.module.css +3 -3
- package/src/components/interaction/Toast/Toast.module.css +25 -8
- package/src/components/interaction/Toast/Toast.tsx +10 -1
- package/src/components/interaction/form/Checkbox/Checkbox.module.css +1 -1
- package/src/components/interaction/form/Checkbox/Checkbox.tsx +3 -2
- package/src/components/interaction/form/Combobox/Combobox.module.css +3 -2
- package/src/components/interaction/form/Combobox/Combobox.tsx +11 -3
- package/src/components/interaction/form/Combobox/index.ts +1 -0
- package/src/components/interaction/form/FileInput/FileInput.tsx +15 -2
- package/src/components/interaction/form/FormInput/FormInput.tsx +5 -4
- package/src/components/interaction/form/NumberInput/NumberInput.tsx +21 -17
- package/src/components/interaction/form/Radio/Radio.module.css +1 -1
- package/src/components/interaction/form/Radio/Radio.tsx +3 -2
- package/src/components/interaction/form/Select/Select.spec.tsx +2 -1
- package/src/components/interaction/form/Select/Select.tsx +3 -3
- package/src/components/interaction/form/Select/index.ts +1 -0
- package/src/components/interaction/form/Slider/Slider.tsx +5 -4
- package/src/components/interaction/form/Switch/Switch.tsx +1 -1
- package/src/components/interaction/form/Textarea/Textarea.tsx +1 -1
- package/src/components/interaction/form/atoms/Message.tsx +2 -2
- package/src/components/layout/ButtonGroup/ButtonGroup.tsx +1 -1
- package/src/components/layout/Card/Card.module.css +3 -2
- package/src/components/layout/Card/Card.tsx +3 -0
- package/src/components/layout/SectionHeader/SectionHeader.tsx +2 -3
- package/src/components/ui/Accordion/Accordion.tsx +1 -1
- package/src/components/ui/Accordion/index.ts +1 -0
- package/src/components/ui/Breadcrumb/Breadcrumb.tsx +2 -2
- package/src/components/ui/Breadcrumb/index.ts +1 -0
- package/src/components/ui/Calendar/Calendar.tsx +8 -3
- package/src/components/ui/DescriptionList/DescriptionList.tsx +1 -1
- package/src/components/ui/DescriptionList/index.ts +1 -0
- package/src/components/ui/Link/Link.tsx +3 -1
- package/src/components/ui/Separator/Separator.module.css +1 -1
- package/src/components/ui/Table/Table.tsx +1 -1
- package/src/components/ui/Table/index.ts +1 -0
- package/src/components/ui/Tabs/Tabs.tsx +1 -1
- package/src/components/ui/Tabs/index.ts +1 -0
- package/src/index.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boostdev/design-system-components",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "BoostDev React component library: accessible, token-driven components built on @boostdev/design-system-foundation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"React",
|
|
@@ -115,6 +115,7 @@
|
|
|
115
115
|
"@types/react-dom": "^18",
|
|
116
116
|
"@vitejs/plugin-react": "^5.1.4",
|
|
117
117
|
"eslint": "^9",
|
|
118
|
+
"git-cliff": "^2.12.0",
|
|
118
119
|
"identity-obj-proxy": "^3.0.0",
|
|
119
120
|
"jsdom": "^28.1.0",
|
|
120
121
|
"storybook": "^8.6.18",
|
|
@@ -22,7 +22,7 @@ interface ButtonProps {
|
|
|
22
22
|
|
|
23
23
|
export function Button({
|
|
24
24
|
children,
|
|
25
|
-
className
|
|
25
|
+
className,
|
|
26
26
|
variant = 'primary',
|
|
27
27
|
type = 'button',
|
|
28
28
|
iconStart,
|
|
@@ -44,25 +44,52 @@ export function Button({
|
|
|
44
44
|
className,
|
|
45
45
|
);
|
|
46
46
|
|
|
47
|
-
const
|
|
47
|
+
const handleAnchorClick: MouseEventHandler<HTMLAnchorElement> = (e) => {
|
|
48
|
+
if (disabled) {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
onClick?.(e);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleButtonClick: MouseEventHandler<HTMLButtonElement> = (e) => {
|
|
56
|
+
onClick?.(e);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const content = (
|
|
48
60
|
<>
|
|
49
|
-
{
|
|
61
|
+
{Boolean(iconStart) && <span className={css.prefix}>{iconStart}</span>}
|
|
50
62
|
{children}
|
|
51
|
-
{
|
|
63
|
+
{Boolean(iconEnd) && <span className={css.suffix}>{iconEnd}</span>}
|
|
52
64
|
</>
|
|
53
65
|
);
|
|
54
66
|
|
|
55
67
|
if (href) {
|
|
56
68
|
return (
|
|
57
|
-
<a
|
|
58
|
-
{
|
|
69
|
+
<a
|
|
70
|
+
className={classNames}
|
|
71
|
+
href={disabled ? undefined : href}
|
|
72
|
+
target={target}
|
|
73
|
+
rel={rel}
|
|
74
|
+
aria-disabled={disabled}
|
|
75
|
+
tabIndex={disabled ? -1 : undefined}
|
|
76
|
+
onClick={handleAnchorClick}
|
|
77
|
+
{...rest}
|
|
78
|
+
>
|
|
79
|
+
{content}
|
|
59
80
|
</a>
|
|
60
81
|
);
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
return (
|
|
64
|
-
<button
|
|
65
|
-
{
|
|
85
|
+
<button
|
|
86
|
+
type={type}
|
|
87
|
+
className={classNames}
|
|
88
|
+
disabled={disabled}
|
|
89
|
+
onClick={handleButtonClick}
|
|
90
|
+
{...rest}
|
|
91
|
+
>
|
|
92
|
+
{content}
|
|
66
93
|
</button>
|
|
67
94
|
);
|
|
68
95
|
}
|
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
.palette {
|
|
18
18
|
display: flex;
|
|
19
19
|
flex-direction: column;
|
|
20
|
-
background-color: var(--color_bg);
|
|
20
|
+
background-color: var(--command_color, var(--color_bg));
|
|
21
|
+
color: var(--command_on-color, var(--color_on-bg));
|
|
21
22
|
border-radius: var(--border_radius--m);
|
|
22
23
|
box-shadow: var(--shadow_xl);
|
|
23
24
|
overflow: hidden;
|
|
24
25
|
max-height: 70vh;
|
|
25
|
-
border: 1px solid
|
|
26
|
+
border: 1px solid currentcolor;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
.searchRow {
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
align-items: center;
|
|
31
32
|
gap: var(--space_xs);
|
|
32
33
|
padding: var(--space_s) var(--space_m);
|
|
33
|
-
border-block-end: 1px solid
|
|
34
|
+
border-block-end: 1px solid currentcolor;
|
|
34
35
|
flex-shrink: 0;
|
|
35
36
|
}
|
|
36
37
|
|
|
@@ -59,7 +60,7 @@
|
|
|
59
60
|
.escHint {
|
|
60
61
|
font-size: var(--font_size--body--s);
|
|
61
62
|
color: var(--color_on-bg--muted);
|
|
62
|
-
border: 1px solid
|
|
63
|
+
border: 1px solid currentcolor;
|
|
63
64
|
border-radius: var(--border_radius--xs);
|
|
64
65
|
padding: 0.1em 0.4em;
|
|
65
66
|
flex-shrink: 0;
|
|
@@ -114,7 +115,7 @@
|
|
|
114
115
|
.shortcut {
|
|
115
116
|
font-size: var(--font_size--body--s);
|
|
116
117
|
color: var(--color_on-bg--muted);
|
|
117
|
-
border: 1px solid
|
|
118
|
+
border: 1px solid currentcolor;
|
|
118
119
|
border-radius: var(--border_radius--xs);
|
|
119
120
|
padding: 0.1em 0.4em;
|
|
120
121
|
}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
@layer component {
|
|
2
2
|
.dialog {
|
|
3
|
-
display: grid;
|
|
4
|
-
gap: var(--grid_gap);
|
|
5
3
|
font-size: var(--font_size--body--s);
|
|
6
4
|
border: 2px solid currentcolor;
|
|
7
|
-
background-color: var(--color_grey--subtle);
|
|
8
|
-
color: var(--color_on-grey--subtle);
|
|
5
|
+
background-color: var(--dialog_color ,var(--color_grey--subtle));
|
|
6
|
+
color: var(--dialog_on-color, var(--color_on-grey--subtle));
|
|
9
7
|
border-radius: var(--border_radius--m);
|
|
10
8
|
padding: var(--space_l);
|
|
11
9
|
overflow: visible;
|
|
12
10
|
max-width: 90svw;
|
|
13
11
|
}
|
|
14
12
|
|
|
13
|
+
.dialogContent {
|
|
14
|
+
display: grid;
|
|
15
|
+
gap: var(--grid_gap);
|
|
16
|
+
}
|
|
17
|
+
|
|
15
18
|
.closeForm {
|
|
16
19
|
position: absolute;
|
|
17
20
|
inset-block-start: -1em;
|
|
@@ -27,7 +30,7 @@
|
|
|
27
30
|
justify-content: center;
|
|
28
31
|
border-radius: 50%;
|
|
29
32
|
background-color: var(--color_bg);
|
|
30
|
-
color:
|
|
33
|
+
color: currentcolor;
|
|
31
34
|
border: 2px solid currentcolor;
|
|
32
35
|
cursor: pointer;
|
|
33
36
|
}
|
|
@@ -14,30 +14,30 @@ beforeEach(() => {
|
|
|
14
14
|
|
|
15
15
|
describe('Dialog', () => {
|
|
16
16
|
it('renders children', () => {
|
|
17
|
-
render(<Dialog
|
|
17
|
+
render(<Dialog isOpen><p>Dialog content</p></Dialog>);
|
|
18
18
|
expect(screen.getByText('Dialog content')).toBeInTheDocument();
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
-
it('calls showModal when
|
|
22
|
-
render(<Dialog
|
|
21
|
+
it('calls showModal when isOpen becomes true', () => {
|
|
22
|
+
render(<Dialog isOpen><span>Content</span></Dialog>);
|
|
23
23
|
expect(HTMLDialogElement.prototype.showModal).toHaveBeenCalled();
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
it('renders a close button with aria-label', () => {
|
|
27
|
-
render(<Dialog
|
|
27
|
+
render(<Dialog isOpen><span>Content</span></Dialog>);
|
|
28
28
|
expect(screen.getByRole('button', { name: /close dialog/i })).toBeInTheDocument();
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
it('calls
|
|
31
|
+
it('calls onClose when close button is clicked', async () => {
|
|
32
32
|
const user = userEvent.setup();
|
|
33
|
-
const
|
|
34
|
-
render(<Dialog
|
|
33
|
+
const onClose = vi.fn();
|
|
34
|
+
render(<Dialog isOpen onClose={onClose}><span>Content</span></Dialog>);
|
|
35
35
|
await user.click(screen.getByRole('button', { name: /close dialog/i }));
|
|
36
|
-
expect(
|
|
36
|
+
expect(onClose).toHaveBeenCalled();
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
it('does not call showModal when
|
|
40
|
-
render(<Dialog
|
|
39
|
+
it('does not call showModal when isOpen is false', () => {
|
|
40
|
+
render(<Dialog isOpen={false}><span>Content</span></Dialog>);
|
|
41
41
|
expect(HTMLDialogElement.prototype.showModal).not.toHaveBeenCalled();
|
|
42
42
|
});
|
|
43
43
|
});
|
|
@@ -14,7 +14,7 @@ type Story = StoryObj<typeof meta>;
|
|
|
14
14
|
|
|
15
15
|
export const Default: Story = {
|
|
16
16
|
args: {
|
|
17
|
-
|
|
17
|
+
isOpen: true,
|
|
18
18
|
children: <p>This is dialog content.</p>,
|
|
19
19
|
},
|
|
20
20
|
};
|
|
@@ -25,7 +25,7 @@ export const Interactive: Story = {
|
|
|
25
25
|
return (
|
|
26
26
|
<>
|
|
27
27
|
<Button onClick={() => setOpen(true)}>Open Dialog</Button>
|
|
28
|
-
<Dialog
|
|
28
|
+
<Dialog isOpen={open} onClose={() => setOpen(false)}>
|
|
29
29
|
<h2>Dialog Title</h2>
|
|
30
30
|
<p>This is the dialog body content.</p>
|
|
31
31
|
<Button onClick={() => setOpen(false)}>Close</Button>
|
|
@@ -5,30 +5,44 @@ import { cn } from '@boostdev/design-system-foundation';
|
|
|
5
5
|
interface DialogProps {
|
|
6
6
|
children: ReactNode;
|
|
7
7
|
className?: string;
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
isOpen?: boolean;
|
|
9
|
+
onClose?: () => void;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function Dialog({ children,
|
|
12
|
+
export function Dialog({ children, isOpen = false, className, onClose }: DialogProps) {
|
|
13
13
|
const dialogRef = useRef<HTMLDialogElement | null>(null);
|
|
14
14
|
|
|
15
15
|
useEffect(() => {
|
|
16
16
|
const dialog = dialogRef.current;
|
|
17
17
|
if (!dialog) return;
|
|
18
|
-
if (
|
|
18
|
+
if (isOpen) {
|
|
19
19
|
dialog.showModal();
|
|
20
20
|
} else if (dialog.open) {
|
|
21
21
|
dialog.close();
|
|
22
22
|
}
|
|
23
|
-
}, [
|
|
23
|
+
}, [isOpen]);
|
|
24
|
+
|
|
25
|
+
const handleBackdropClick = (e: React.MouseEvent<HTMLDialogElement>) => {
|
|
26
|
+
if (e.target === dialogRef.current) onClose?.();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const handleCancel = (e: React.SyntheticEvent) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
onClose?.();
|
|
32
|
+
};
|
|
24
33
|
|
|
25
34
|
return (
|
|
26
|
-
<dialog
|
|
35
|
+
<dialog
|
|
36
|
+
ref={dialogRef}
|
|
37
|
+
className={cn(className, css.dialog)}
|
|
38
|
+
onClick={handleBackdropClick}
|
|
39
|
+
onCancel={handleCancel}
|
|
40
|
+
>
|
|
27
41
|
<form method="dialog" className={css.closeForm}>
|
|
28
42
|
<button
|
|
29
43
|
type="submit"
|
|
30
44
|
className={css.closeButton}
|
|
31
|
-
onClick={
|
|
45
|
+
onClick={onClose}
|
|
32
46
|
aria-label="Close dialog"
|
|
33
47
|
>
|
|
34
48
|
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
@@ -36,7 +50,9 @@ export function Dialog({ children, isVisible = false, className, handleClose }:
|
|
|
36
50
|
</svg>
|
|
37
51
|
</button>
|
|
38
52
|
</form>
|
|
39
|
-
{
|
|
53
|
+
<div className={css.dialogContent}>
|
|
54
|
+
{children}
|
|
55
|
+
</div>
|
|
40
56
|
</dialog>
|
|
41
57
|
);
|
|
42
58
|
}
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
flex-direction: column;
|
|
25
25
|
width: min(28rem, 90vw);
|
|
26
26
|
height: 100%;
|
|
27
|
-
background-color: var(--color_bg);
|
|
28
|
-
color: var(--color_on-bg);
|
|
27
|
+
background-color: var(--drawer_color, var(--color_bg));
|
|
28
|
+
color: var(--drawer_on-color, var(--color_on-bg));
|
|
29
29
|
box-shadow: var(--shadow_xl);
|
|
30
30
|
overflow: hidden;
|
|
31
31
|
transition: transform var(--animation_transition-duration) var(--animation_easing);
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
align-items: center;
|
|
45
45
|
justify-content: space-between;
|
|
46
46
|
padding: var(--space_m) var(--space_l);
|
|
47
|
-
border-block-end: 1px solid
|
|
47
|
+
border-block-end: 1px solid currentcolor;
|
|
48
48
|
flex-shrink: 0;
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -64,7 +64,6 @@
|
|
|
64
64
|
height: 2rem;
|
|
65
65
|
border-radius: 50%;
|
|
66
66
|
cursor: pointer;
|
|
67
|
-
color: var(--color_on-bg);
|
|
68
67
|
transition: var(--animation_transition);
|
|
69
68
|
}
|
|
70
69
|
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
min-width: 10rem;
|
|
12
12
|
padding: var(--space_xxs) 0;
|
|
13
13
|
border-radius: var(--border_radius--s);
|
|
14
|
-
border: 1px solid
|
|
15
|
-
background-color: var(--color_bg);
|
|
14
|
+
border: 1px solid currentcolor;
|
|
15
|
+
background-color: var(--dropdown_color, var(--color_bg));
|
|
16
|
+
color: var(--dropdown_on-color, var(--color_on-bg));
|
|
16
17
|
box-shadow: var(--shadow_m);
|
|
17
18
|
list-style: none;
|
|
18
19
|
margin: 0;
|
|
@@ -23,8 +24,9 @@
|
|
|
23
24
|
|
|
24
25
|
.separator {
|
|
25
26
|
border: none;
|
|
26
|
-
border-top: 1px solid
|
|
27
|
+
border-top: 1px solid currentcolor;
|
|
27
28
|
margin: var(--space_xxs) 0;
|
|
29
|
+
opacity: 0.15;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
.item {
|
|
@@ -36,7 +38,7 @@
|
|
|
36
38
|
padding: var(--space_xs) var(--space_m);
|
|
37
39
|
font-family: var(--font_family--body);
|
|
38
40
|
font-size: var(--font_size--body);
|
|
39
|
-
color:
|
|
41
|
+
color: currentcolor;
|
|
40
42
|
cursor: pointer;
|
|
41
43
|
box-sizing: border-box;
|
|
42
44
|
transition: var(--animation_transition);
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
min-width: 12rem;
|
|
11
11
|
padding: var(--space_m);
|
|
12
12
|
border-radius: var(--border_radius--s);
|
|
13
|
-
border: 1px solid
|
|
14
|
-
background-color: var(--color_bg);
|
|
13
|
+
border: 1px solid currentcolor;
|
|
14
|
+
background-color: var(--popover_color, var(--color_bg));
|
|
15
15
|
box-shadow: var(--shadow_m);
|
|
16
|
-
color: var(--color_on-bg);
|
|
16
|
+
color: var(--popover_on-color, var(--color_on-bg));
|
|
17
17
|
font-size: var(--font_size--body);
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
.toast {
|
|
13
13
|
padding: var(--space_m) var(--space_l);
|
|
14
|
-
background-color: var(--color_bg);
|
|
15
|
-
color: var(--color_on-bg);
|
|
14
|
+
background-color: var(--toast_color, var(--color_bg));
|
|
15
|
+
color: var(--toast_on-color, var(--color_on-bg));
|
|
16
|
+
border: 1px solid currentcolor;
|
|
16
17
|
border-radius: var(--border_radius--s);
|
|
17
18
|
box-shadow: var(--shadow_s);
|
|
18
19
|
display: flex;
|
|
@@ -22,16 +23,32 @@
|
|
|
22
23
|
animation: slideIn var(--animation_duration--fast) var(--animation_easing);
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
.message {
|
|
27
|
+
flex: 1;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
.closeButton {
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
flex-shrink: 0;
|
|
35
|
+
width: 1.25rem;
|
|
36
|
+
height: 1.25rem;
|
|
37
|
+
padding: 0;
|
|
38
|
+
background: none;
|
|
39
|
+
border: none;
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
color: inherit;
|
|
42
|
+
opacity: 0.6;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.closeButton:hover {
|
|
46
|
+
opacity: 1;
|
|
31
47
|
}
|
|
32
48
|
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
.closeButton svg {
|
|
50
|
+
width: 1rem;
|
|
51
|
+
height: 1rem;
|
|
35
52
|
}
|
|
36
53
|
}
|
|
37
54
|
|
|
@@ -52,7 +52,16 @@ function ToastItem({ toast, onRemove }: { toast: Toast; onRemove: () => void })
|
|
|
52
52
|
return () => clearTimeout(timer);
|
|
53
53
|
}, [onRemove]);
|
|
54
54
|
|
|
55
|
-
return
|
|
55
|
+
return (
|
|
56
|
+
<div className={cn(css.toast, css[`--variant_${toast.variant}`])} role="status">
|
|
57
|
+
<span className={css.message}>{toast.message}</span>
|
|
58
|
+
<button type="button" className={css.closeButton} onClick={onRemove} aria-label="Dismiss">
|
|
59
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
60
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M18 6L6 18M6 6l12 12" />
|
|
61
|
+
</svg>
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
export function useToast() {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
margin-block-start: 0.25em;
|
|
18
18
|
width: var(--inputSize);
|
|
19
19
|
height: var(--inputSize);
|
|
20
|
-
border: 1px solid var(--
|
|
20
|
+
border: 1px solid var(--color_on-bg);
|
|
21
21
|
border-radius: var(--border_radius--xs);
|
|
22
22
|
appearance: none;
|
|
23
23
|
background-color: var(--color_bg);
|
|
@@ -17,17 +17,18 @@ export function Checkbox({ label, name, error, hint, className, ...props }: Chec
|
|
|
17
17
|
const id = name + useId();
|
|
18
18
|
const hintId = id + 'hint';
|
|
19
19
|
const errorId = id + 'error';
|
|
20
|
-
const describedBy =
|
|
20
|
+
const describedBy = [error && errorId, hint && hintId].filter(Boolean).join(' ') || undefined;
|
|
21
21
|
|
|
22
22
|
return (
|
|
23
23
|
<InputContainer className={cn(css.checkboxGroup, className)}>
|
|
24
24
|
<div className={css.inputWrapper}>
|
|
25
25
|
<input
|
|
26
26
|
aria-describedby={describedBy}
|
|
27
|
+
aria-invalid={!!error}
|
|
27
28
|
type="checkbox"
|
|
28
29
|
id={id}
|
|
29
30
|
name={name}
|
|
30
|
-
className={
|
|
31
|
+
className={cn(css.checkbox, error && css.checkboxError)}
|
|
31
32
|
{...props}
|
|
32
33
|
/>
|
|
33
34
|
<Label id={id} label={label} />
|
|
@@ -73,8 +73,9 @@
|
|
|
73
73
|
margin: 0;
|
|
74
74
|
padding: var(--space_xxs) 0;
|
|
75
75
|
border-radius: var(--border_radius--xs);
|
|
76
|
-
border: 1px solid
|
|
77
|
-
background-color: var(--color_bg);
|
|
76
|
+
border: 1px solid currentcolor;
|
|
77
|
+
background-color: var(--combobox_color, var(--color_bg));
|
|
78
|
+
color: var(--combobox_on-color, var(--color_on-bg));
|
|
78
79
|
box-shadow: var(--shadow_m);
|
|
79
80
|
}
|
|
80
81
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
useId,
|
|
7
7
|
useRef,
|
|
8
8
|
useState,
|
|
9
|
+
useMemo,
|
|
9
10
|
} from 'react';
|
|
10
11
|
import css from './Combobox.module.css';
|
|
11
12
|
import { cn } from '@boostdev/design-system-foundation';
|
|
@@ -13,7 +14,7 @@ import { InputContainer } from '../atoms/InputContainer';
|
|
|
13
14
|
import { Label } from '../atoms/Label';
|
|
14
15
|
import { Message } from '../atoms/Message';
|
|
15
16
|
|
|
16
|
-
interface ComboboxOption {
|
|
17
|
+
export interface ComboboxOption {
|
|
17
18
|
value: string;
|
|
18
19
|
label: string;
|
|
19
20
|
disabled?: boolean;
|
|
@@ -26,6 +27,7 @@ interface ComboboxProps {
|
|
|
26
27
|
placeholder?: string;
|
|
27
28
|
value?: string;
|
|
28
29
|
onChange?: (value: string) => void;
|
|
30
|
+
disabled?: boolean;
|
|
29
31
|
error?: string;
|
|
30
32
|
hint?: string;
|
|
31
33
|
className?: string;
|
|
@@ -38,6 +40,7 @@ export function Combobox({
|
|
|
38
40
|
placeholder,
|
|
39
41
|
value,
|
|
40
42
|
onChange,
|
|
43
|
+
disabled = false,
|
|
41
44
|
error,
|
|
42
45
|
hint,
|
|
43
46
|
className,
|
|
@@ -46,10 +49,14 @@ export function Combobox({
|
|
|
46
49
|
const listboxId = id + 'listbox';
|
|
47
50
|
const hintId = id + 'hint';
|
|
48
51
|
const errorId = id + 'error';
|
|
49
|
-
const describedBy = error
|
|
52
|
+
const describedBy = [error && errorId, hint && hintId].filter(Boolean).join(' ') || undefined;
|
|
50
53
|
|
|
51
|
-
const selectedOption = options.find(o => o.value === value);
|
|
54
|
+
const selectedOption = useMemo(() => options.find(o => o.value === value), [options, value]);
|
|
52
55
|
const [inputValue, setInputValue] = useState(selectedOption?.label ?? '');
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
setInputValue(selectedOption?.label ?? '');
|
|
59
|
+
}, [selectedOption]);
|
|
53
60
|
const [isOpen, setIsOpen] = useState(false);
|
|
54
61
|
const [highlightedIndex, setHighlightedIndex] = useState(-1);
|
|
55
62
|
|
|
@@ -134,6 +141,7 @@ export function Combobox({
|
|
|
134
141
|
autoComplete="off"
|
|
135
142
|
placeholder={placeholder}
|
|
136
143
|
value={inputValue}
|
|
144
|
+
disabled={disabled}
|
|
137
145
|
className={cn(css.input, error ? css.inputError : undefined)}
|
|
138
146
|
onChange={handleInputChange}
|
|
139
147
|
onKeyDown={handleKeyDown}
|
|
@@ -31,11 +31,21 @@ export function FileInput({
|
|
|
31
31
|
const uid = name + useId();
|
|
32
32
|
const hintId = uid + 'hint';
|
|
33
33
|
const errorId = uid + 'error';
|
|
34
|
-
const describedBy = error
|
|
34
|
+
const describedBy = [error && errorId, hint && hintId].filter(Boolean).join(' ') || undefined;
|
|
35
35
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
36
36
|
const [isDragging, setIsDragging] = useState(false);
|
|
37
37
|
const [fileNames, setFileNames] = useState<string[]>([]);
|
|
38
38
|
|
|
39
|
+
const isFileAccepted = (file: File): boolean => {
|
|
40
|
+
if (!accept) return true;
|
|
41
|
+
return accept.split(',').some(token => {
|
|
42
|
+
const t = token.trim();
|
|
43
|
+
if (t.startsWith('.')) return file.name.toLowerCase().endsWith(t.toLowerCase());
|
|
44
|
+
if (t.endsWith('/*')) return file.type.startsWith(t.slice(0, -1));
|
|
45
|
+
return file.type === t;
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
39
49
|
const handleFiles = (files: FileList | null) => {
|
|
40
50
|
if (!files) return;
|
|
41
51
|
setFileNames(Array.from(files).map(f => f.name));
|
|
@@ -48,7 +58,10 @@ export function FileInput({
|
|
|
48
58
|
e.preventDefault();
|
|
49
59
|
setIsDragging(false);
|
|
50
60
|
if (disabled) return;
|
|
51
|
-
|
|
61
|
+
const dt = new DataTransfer();
|
|
62
|
+
Array.from(e.dataTransfer.files).filter(isFileAccepted).forEach(f => dt.items.add(f));
|
|
63
|
+
if (inputRef.current) inputRef.current.files = dt.files;
|
|
64
|
+
handleFiles(dt.files.length > 0 ? dt.files : null);
|
|
52
65
|
};
|
|
53
66
|
|
|
54
67
|
const handleDragOver = (e: DragEvent<HTMLLabelElement>) => {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { InputHTMLAttributes, useId, ReactNode } from 'react';
|
|
1
|
+
import { InputHTMLAttributes, useId, ReactNode, HTMLInputTypeAttribute } from 'react';
|
|
2
2
|
import css from './FormInput.module.css';
|
|
3
3
|
import { Label } from '../atoms/Label';
|
|
4
4
|
import { Message } from '../atoms/Message';
|
|
5
5
|
import { cn } from '@boostdev/design-system-foundation';
|
|
6
6
|
import { InputContainer } from '../atoms/InputContainer';
|
|
7
7
|
|
|
8
|
-
interface FormInputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
8
|
+
interface FormInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
|
|
9
9
|
label: ReactNode;
|
|
10
10
|
name: string;
|
|
11
|
+
type?: Exclude<HTMLInputTypeAttribute, 'checkbox' | 'radio' | 'file'>;
|
|
11
12
|
ariaLabel?: string;
|
|
12
13
|
error?: string;
|
|
13
14
|
hint?: string;
|
|
@@ -26,7 +27,7 @@ export function FormInput({
|
|
|
26
27
|
const id = name + useId();
|
|
27
28
|
const hintId = id + 'hint';
|
|
28
29
|
const errorId = id + 'error';
|
|
29
|
-
const describedBy =
|
|
30
|
+
const describedBy = [error && errorId, hint && hintId].filter(Boolean).join(' ') || undefined;
|
|
30
31
|
|
|
31
32
|
return (
|
|
32
33
|
<InputContainer className={cn(css.formGroup, className)}>
|
|
@@ -37,7 +38,7 @@ export function FormInput({
|
|
|
37
38
|
aria-label={ariaLabel}
|
|
38
39
|
id={id}
|
|
39
40
|
name={name}
|
|
40
|
-
className={
|
|
41
|
+
className={cn(css.input, error && css.inputError)}
|
|
41
42
|
{...props}
|
|
42
43
|
/>
|
|
43
44
|
<Message inputId={id} type="error" message={error} />
|