@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.
Files changed (59) hide show
  1. package/dist/client.cjs +188 -107
  2. package/dist/client.css +463 -441
  3. package/dist/client.d.cts +15 -13
  4. package/dist/client.d.ts +15 -13
  5. package/dist/client.js +192 -110
  6. package/dist/index.cjs +188 -107
  7. package/dist/index.css +463 -441
  8. package/dist/index.d.cts +15 -13
  9. package/dist/index.d.ts +15 -13
  10. package/dist/index.js +192 -110
  11. package/package.json +2 -1
  12. package/src/components/interaction/Button/Button.tsx +35 -8
  13. package/src/components/interaction/Command/Command.module.css +6 -5
  14. package/src/components/interaction/Dialog/Dialog.module.css +8 -5
  15. package/src/components/interaction/Dialog/Dialog.spec.tsx +10 -10
  16. package/src/components/interaction/Dialog/Dialog.stories.tsx +2 -2
  17. package/src/components/interaction/Dialog/Dialog.tsx +24 -8
  18. package/src/components/interaction/Drawer/Drawer.module.css +3 -4
  19. package/src/components/interaction/DropdownMenu/DropdownMenu.module.css +6 -4
  20. package/src/components/interaction/DropdownMenu/DropdownMenu.tsx +1 -1
  21. package/src/components/interaction/DropdownMenu/index.ts +1 -0
  22. package/src/components/interaction/Popover/Popover.module.css +3 -3
  23. package/src/components/interaction/Toast/Toast.module.css +25 -8
  24. package/src/components/interaction/Toast/Toast.tsx +10 -1
  25. package/src/components/interaction/form/Checkbox/Checkbox.module.css +1 -1
  26. package/src/components/interaction/form/Checkbox/Checkbox.tsx +3 -2
  27. package/src/components/interaction/form/Combobox/Combobox.module.css +3 -2
  28. package/src/components/interaction/form/Combobox/Combobox.tsx +11 -3
  29. package/src/components/interaction/form/Combobox/index.ts +1 -0
  30. package/src/components/interaction/form/FileInput/FileInput.tsx +15 -2
  31. package/src/components/interaction/form/FormInput/FormInput.tsx +5 -4
  32. package/src/components/interaction/form/NumberInput/NumberInput.tsx +21 -17
  33. package/src/components/interaction/form/Radio/Radio.module.css +1 -1
  34. package/src/components/interaction/form/Radio/Radio.tsx +3 -2
  35. package/src/components/interaction/form/Select/Select.spec.tsx +2 -1
  36. package/src/components/interaction/form/Select/Select.tsx +3 -3
  37. package/src/components/interaction/form/Select/index.ts +1 -0
  38. package/src/components/interaction/form/Slider/Slider.tsx +5 -4
  39. package/src/components/interaction/form/Switch/Switch.tsx +1 -1
  40. package/src/components/interaction/form/Textarea/Textarea.tsx +1 -1
  41. package/src/components/interaction/form/atoms/Message.tsx +2 -2
  42. package/src/components/layout/ButtonGroup/ButtonGroup.tsx +1 -1
  43. package/src/components/layout/Card/Card.module.css +3 -2
  44. package/src/components/layout/Card/Card.tsx +3 -0
  45. package/src/components/layout/SectionHeader/SectionHeader.tsx +2 -3
  46. package/src/components/ui/Accordion/Accordion.tsx +1 -1
  47. package/src/components/ui/Accordion/index.ts +1 -0
  48. package/src/components/ui/Breadcrumb/Breadcrumb.tsx +2 -2
  49. package/src/components/ui/Breadcrumb/index.ts +1 -0
  50. package/src/components/ui/Calendar/Calendar.tsx +8 -3
  51. package/src/components/ui/DescriptionList/DescriptionList.tsx +1 -1
  52. package/src/components/ui/DescriptionList/index.ts +1 -0
  53. package/src/components/ui/Link/Link.tsx +3 -1
  54. package/src/components/ui/Separator/Separator.module.css +1 -1
  55. package/src/components/ui/Table/Table.tsx +1 -1
  56. package/src/components/ui/Table/index.ts +1 -0
  57. package/src/components/ui/Tabs/Tabs.tsx +1 -1
  58. package/src/components/ui/Tabs/index.ts +1 -0
  59. 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.4",
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 allChildren = (
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
- {!!iconStart && <span className={css.prefix}>{iconStart}</span>}
61
+ {Boolean(iconStart) && <span className={css.prefix}>{iconStart}</span>}
50
62
  {children}
51
- {!!iconEnd && <span className={css.suffix}>{iconEnd}</span>}
63
+ {Boolean(iconEnd) && <span className={css.suffix}>{iconEnd}</span>}
52
64
  </>
53
65
  );
54
66
 
55
67
  if (href) {
56
68
  return (
57
- <a className={classNames} href={href} target={target} rel={rel} onClick={onClick as MouseEventHandler<HTMLAnchorElement>} {...rest}>
58
- {allChildren}
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 type={type} className={classNames} disabled={disabled} onClick={onClick as MouseEventHandler<HTMLButtonElement>} {...rest}>
65
- {allChildren}
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 var(--color_bg--subtle);
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 var(--color_bg--subtle);
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 var(--color_on-bg--muted);
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 var(--color_on-bg--muted);
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: var(--color_on-bg);
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 isVisible><p>Dialog content</p></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 isVisible becomes true', () => {
22
- render(<Dialog isVisible><span>Content</span></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 isVisible><span>Content</span></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 handleClose when close button is clicked', async () => {
31
+ it('calls onClose when close button is clicked', async () => {
32
32
  const user = userEvent.setup();
33
- const handleClose = vi.fn();
34
- render(<Dialog isVisible handleClose={handleClose}><span>Content</span></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(handleClose).toHaveBeenCalled();
36
+ expect(onClose).toHaveBeenCalled();
37
37
  });
38
38
 
39
- it('does not call showModal when isVisible is false', () => {
40
- render(<Dialog isVisible={false}><span>Content</span></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
- isVisible: true,
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 isVisible={open} handleClose={() => setOpen(false)}>
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
- isVisible?: boolean;
9
- handleClose?: () => void;
8
+ isOpen?: boolean;
9
+ onClose?: () => void;
10
10
  }
11
11
 
12
- export function Dialog({ children, isVisible = false, className, handleClose }: DialogProps) {
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 (isVisible) {
18
+ if (isOpen) {
19
19
  dialog.showModal();
20
20
  } else if (dialog.open) {
21
21
  dialog.close();
22
22
  }
23
- }, [isVisible]);
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 ref={dialogRef} className={cn(className, css.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={handleClose}
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
- {children}
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 var(--color_bg--subtle);
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 var(--color_bg--subtle);
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 var(--color_bg--subtle);
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: var(--color_on-bg);
41
+ color: currentcolor;
40
42
  cursor: pointer;
41
43
  box-sizing: border-box;
42
44
  transition: var(--animation_transition);
@@ -11,7 +11,7 @@ import {
11
11
  import css from './DropdownMenu.module.css';
12
12
  import { cn } from '@boostdev/design-system-foundation';
13
13
 
14
- interface DropdownMenuItem {
14
+ export interface DropdownMenuItem {
15
15
  id: string;
16
16
  label: string;
17
17
  onClick?: () => void;
@@ -1 +1,2 @@
1
1
  export { DropdownMenu } from './DropdownMenu';
2
+ export type { DropdownMenuItem } from './DropdownMenu';
@@ -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 var(--color_bg--subtle);
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
- .--variant_success {
26
- border-left: 4px solid var(--color_success);
26
+ .message {
27
+ flex: 1;
27
28
  }
28
29
 
29
- .--variant_error {
30
- border-left: 4px solid var(--color_error);
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
- .--variant_info {
34
- border-left: 4px solid var(--color_interactive);
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 <div className={cn(css.toast, css[`--variant_${toast.variant}`])}>{toast.message}</div>;
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(--color_border);
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 = !!error ? errorId : hintId;
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={`${css.checkbox} ${error ? css.checkboxError : ''}`}
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 var(--color_bg--subtle);
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 ? errorId : hintId;
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}
@@ -1 +1,2 @@
1
1
  export { Combobox } from './Combobox';
2
+ export type { ComboboxOption } from './Combobox';
@@ -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 ? errorId : hintId;
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
- handleFiles(e.dataTransfer.files);
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 = !!error ? errorId : hintId;
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={`${css.input} ${error ? css.inputError : ''}`}
41
+ className={cn(css.input, error && css.inputError)}
41
42
  {...props}
42
43
  />
43
44
  <Message inputId={id} type="error" message={error} />