@robr0/design-system 0.1.0 → 0.2.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.
Files changed (39) hide show
  1. package/README.md +6 -4
  2. package/components/Breadcrumb/Breadcrumb.css +9 -1
  3. package/components/Button/Button.d.ts +32 -16
  4. package/components/Button/Button.js +66 -58
  5. package/components/Card/Card.d.ts +17 -5
  6. package/components/Card/Card.js +101 -80
  7. package/components/Checkbox/Checkbox.d.ts +24 -12
  8. package/components/Checkbox/Checkbox.js +99 -85
  9. package/components/Combobox/Combobox.d.ts +19 -7
  10. package/components/Combobox/Combobox.js +18 -9
  11. package/components/DateInput/DateInput.d.ts +18 -21
  12. package/components/DateInput/DateInput.js +71 -63
  13. package/components/DatePicker/DatePicker.d.ts +9 -2
  14. package/components/DatePicker/DatePicker.js +127 -129
  15. package/components/Dialog/Dialog.d.ts +15 -4
  16. package/components/Dialog/Dialog.js +133 -116
  17. package/components/Drawer/Drawer.d.ts +15 -4
  18. package/components/Drawer/Drawer.js +133 -119
  19. package/components/Dropdown/Dropdown.d.ts +24 -8
  20. package/components/Dropdown/Dropdown.js +226 -186
  21. package/components/FileInput/FileInput.d.ts +13 -17
  22. package/components/FileInput/FileInput.js +177 -158
  23. package/components/Input/Input.d.ts +23 -22
  24. package/components/Input/Input.js +87 -65
  25. package/components/RadioButton/RadioButton.d.ts +26 -13
  26. package/components/RadioButton/RadioButton.js +95 -70
  27. package/components/SelectionCard/SelectionCard.d.ts +10 -4
  28. package/components/SelectionCard/SelectionCard.js +72 -56
  29. package/components/Slider/Slider.d.ts +19 -10
  30. package/components/Slider/Slider.js +50 -40
  31. package/components/Table/Table.d.ts +10 -2
  32. package/components/Table/Table.js +58 -58
  33. package/components/Textarea/Textarea.d.ts +21 -22
  34. package/components/Textarea/Textarea.js +77 -68
  35. package/components/ToggleGroup/ToggleGroup.d.ts +18 -8
  36. package/components/ToggleGroup/ToggleGroup.js +61 -44
  37. package/components/ToggleSwitch/ToggleSwitch.d.ts +17 -9
  38. package/components/ToggleSwitch/ToggleSwitch.js +57 -44
  39. package/package.json +1 -1
@@ -1,196 +1,236 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
- import { useState, useRef, useCallback, useEffect } from "react";
2
+ import React, { useState, useRef, useCallback, useEffect } from "react";
3
3
  import "./Dropdown.css";
4
4
  import "../../fonts/material-symbols.css";
5
- const Dropdown = ({
6
- label,
7
- placeholder = "Select an option",
8
- value,
9
- options,
10
- groups,
11
- size = "default",
12
- disabled = false,
13
- required = false,
14
- error = false,
15
- helperText,
16
- onChange,
17
- className = "",
18
- ariaLabel,
19
- name,
20
- id
21
- }) => {
22
- const [isOpen, setIsOpen] = useState(false);
23
- const [focusedIndex, setFocusedIndex] = useState(-1);
24
- const dropdownRef = useRef(null);
25
- const listRef = useRef(null);
26
- const baseClass = "ds-dropdown";
27
- const isGrouped = groups && groups.length > 0;
28
- const sizeClass = size === "compact" ? `${baseClass}--compact` : "";
29
- const openClass = isOpen ? `${baseClass}--open` : "";
30
- const errorClass = error ? `${baseClass}--error` : "";
31
- const disabledClass = disabled ? `${baseClass}--disabled` : "";
32
- const classes = [baseClass, sizeClass, openClass, errorClass, disabledClass, className].filter(Boolean).join(" ");
33
- const flatOptions = isGrouped ? groups.flatMap((g) => g.options) : options;
34
- const selectedOption = flatOptions.find((opt) => opt.value === value);
35
- const inputId = id || name || label?.toLowerCase().replace(/\s+/g, "-");
36
- const handleToggle = () => {
37
- if (!disabled) {
38
- setIsOpen((prev) => !prev);
39
- setFocusedIndex(-1);
40
- }
41
- };
42
- const handleSelect = (optionValue) => {
43
- if (onChange) {
44
- onChange(optionValue);
45
- }
46
- setIsOpen(false);
47
- };
48
- const handleClickOutside = useCallback((e) => {
49
- if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
5
+ const Dropdown = React.forwardRef(
6
+ ({
7
+ label,
8
+ placeholder = "Select an option",
9
+ value,
10
+ options,
11
+ groups,
12
+ size = "default",
13
+ disabled = false,
14
+ required = false,
15
+ error = false,
16
+ helperText,
17
+ onValueChange,
18
+ onChange,
19
+ className = "",
20
+ ariaLabel,
21
+ name,
22
+ id,
23
+ ...rest
24
+ }, ref) => {
25
+ const [isOpen, setIsOpen] = useState(false);
26
+ const [focusedIndex, setFocusedIndex] = useState(-1);
27
+ const dropdownRef = useRef(null);
28
+ const listRef = useRef(null);
29
+ const setRootRef = (node) => {
30
+ dropdownRef.current = node;
31
+ if (typeof ref === "function") ref(node);
32
+ else if (ref) ref.current = node;
33
+ };
34
+ const baseClass = "ds-dropdown";
35
+ const isGrouped = groups && groups.length > 0;
36
+ const classes = [
37
+ baseClass,
38
+ size === "compact" ? `${baseClass}--compact` : "",
39
+ isOpen ? `${baseClass}--open` : "",
40
+ error ? `${baseClass}--error` : "",
41
+ disabled ? `${baseClass}--disabled` : "",
42
+ className
43
+ ].filter(Boolean).join(" ");
44
+ const flatOptions = isGrouped ? groups.flatMap((g) => g.options) : options;
45
+ const selectedOption = flatOptions.find((opt) => opt.value === value);
46
+ const inputId = id || name || label?.toLowerCase().replace(/\s+/g, "-");
47
+ const handleToggle = () => {
48
+ if (!disabled) {
49
+ setIsOpen((prev) => !prev);
50
+ setFocusedIndex(-1);
51
+ }
52
+ };
53
+ const handleSelect = (optionValue) => {
54
+ onValueChange?.(optionValue);
55
+ onChange?.(optionValue);
50
56
  setIsOpen(false);
51
- }
52
- }, []);
53
- useEffect(() => {
54
- document.addEventListener("mousedown", handleClickOutside);
55
- return () => document.removeEventListener("mousedown", handleClickOutside);
56
- }, [handleClickOutside]);
57
- const handleKeyDown = (e) => {
58
- if (disabled) return;
59
- switch (e.key) {
60
- case "Enter":
61
- case " ":
62
- e.preventDefault();
63
- if (isOpen && focusedIndex >= 0 && !flatOptions[focusedIndex]?.disabled) {
64
- handleSelect(flatOptions[focusedIndex].value);
65
- } else {
66
- setIsOpen((prev) => !prev);
67
- }
68
- break;
69
- case "ArrowDown":
70
- e.preventDefault();
71
- if (!isOpen) {
72
- setIsOpen(true);
73
- } else {
74
- setFocusedIndex((prev) => {
75
- let next = prev + 1;
76
- while (next < flatOptions.length && flatOptions[next].disabled) next++;
77
- return next < flatOptions.length ? next : prev;
78
- });
79
- }
80
- break;
81
- case "ArrowUp":
82
- e.preventDefault();
83
- if (isOpen) {
84
- setFocusedIndex((prev) => {
85
- let next = prev - 1;
86
- while (next >= 0 && flatOptions[next].disabled) next--;
87
- return next >= 0 ? next : prev;
88
- });
89
- }
90
- break;
91
- case "Escape":
92
- setIsOpen(false);
93
- break;
94
- case "Tab":
57
+ };
58
+ const handleClickOutside = useCallback((e) => {
59
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
95
60
  setIsOpen(false);
96
- break;
97
- }
98
- };
99
- useEffect(() => {
100
- if (isOpen && focusedIndex >= 0 && listRef.current) {
101
- const focusedEl = listRef.current.children[focusedIndex];
102
- focusedEl?.scrollIntoView({ block: "nearest" });
103
- }
104
- }, [focusedIndex, isOpen]);
105
- return /* @__PURE__ */ jsxs("div", { className: classes, ref: dropdownRef, children: [
106
- label && /* @__PURE__ */ jsxs("label", { className: `${baseClass}__label`, id: `${inputId}-label`, children: [
107
- label,
108
- required && /* @__PURE__ */ jsx("span", { className: `${baseClass}__required`, "aria-hidden": "true", children: " *" })
109
- ] }),
110
- /* @__PURE__ */ jsxs(
111
- "div",
112
- {
113
- className: `${baseClass}__trigger`,
114
- role: "combobox",
115
- "aria-expanded": isOpen,
116
- "aria-haspopup": "listbox",
117
- "aria-labelledby": label ? `${inputId}-label` : void 0,
118
- "aria-label": ariaLabel || (!label ? placeholder : void 0),
119
- "aria-controls": `${inputId}-listbox`,
120
- tabIndex: disabled ? -1 : 0,
121
- onClick: handleToggle,
122
- onKeyDown: handleKeyDown,
123
- children: [
124
- /* @__PURE__ */ jsx("span", { className: `${baseClass}__value ${!selectedOption ? `${baseClass}__value--placeholder` : ""}`, children: selectedOption ? selectedOption.label : placeholder }),
125
- /* @__PURE__ */ jsx("span", { className: `${baseClass}__chevron material-symbols-rounded`, "aria-hidden": "true", children: "expand_more" })
126
- ]
127
61
  }
128
- ),
129
- isOpen && /* @__PURE__ */ jsx(
130
- "ul",
131
- {
132
- className: [
133
- `${baseClass}__menu`,
134
- isGrouped ? `${baseClass}__menu--grouped` : ""
135
- ].filter(Boolean).join(" "),
136
- role: "listbox",
137
- id: `${inputId}-listbox`,
138
- ref: listRef,
139
- "aria-labelledby": label ? `${inputId}-label` : void 0,
140
- children: isGrouped ? (() => {
141
- let flatIndex = 0;
142
- return groups.map((group, groupIdx) => /* @__PURE__ */ jsxs("li", { className: `${baseClass}__group`, role: "none", children: [
143
- /* @__PURE__ */ jsx("span", { className: `${baseClass}__group-label`, role: "presentation", children: group.label }),
144
- /* @__PURE__ */ jsx("ul", { className: `${baseClass}__group-list`, role: "group", "aria-label": group.label, children: group.options.map((option) => {
145
- const myIndex = flatIndex++;
146
- return /* @__PURE__ */ jsxs(
147
- "li",
148
- {
149
- className: [
150
- `${baseClass}__option`,
151
- option.value === value ? `${baseClass}__option--selected` : "",
152
- option.disabled ? `${baseClass}__option--disabled` : "",
153
- myIndex === focusedIndex ? `${baseClass}__option--focused` : ""
154
- ].filter(Boolean).join(" "),
155
- role: "option",
156
- "aria-selected": option.value === value,
157
- "aria-disabled": option.disabled,
158
- onClick: () => !option.disabled && handleSelect(option.value),
159
- children: [
160
- option.label,
161
- option.value === value && /* @__PURE__ */ jsx("span", { className: `${baseClass}__check material-symbols-rounded`, "aria-hidden": "true", children: "check" })
162
- ]
163
- },
164
- option.value
165
- );
166
- }) }),
167
- groupIdx < groups.length - 1 && /* @__PURE__ */ jsx("li", { className: `${baseClass}__separator`, role: "separator" })
168
- ] }, `group-${groupIdx}`));
169
- })() : options.map((option, index) => /* @__PURE__ */ jsxs(
170
- "li",
171
- {
172
- className: [
173
- `${baseClass}__option`,
174
- option.value === value ? `${baseClass}__option--selected` : "",
175
- option.disabled ? `${baseClass}__option--disabled` : "",
176
- index === focusedIndex ? `${baseClass}__option--focused` : ""
177
- ].filter(Boolean).join(" "),
178
- role: "option",
179
- "aria-selected": option.value === value,
180
- "aria-disabled": option.disabled,
181
- onClick: () => !option.disabled && handleSelect(option.value),
182
- children: [
183
- option.label,
184
- option.value === value && /* @__PURE__ */ jsx("span", { className: `${baseClass}__check material-symbols-rounded`, "aria-hidden": "true", children: "check" })
185
- ]
186
- },
187
- option.value
188
- ))
62
+ }, []);
63
+ useEffect(() => {
64
+ document.addEventListener("mousedown", handleClickOutside);
65
+ return () => document.removeEventListener("mousedown", handleClickOutside);
66
+ }, [handleClickOutside]);
67
+ const handleKeyDown = (e) => {
68
+ if (disabled) return;
69
+ switch (e.key) {
70
+ case "Enter":
71
+ case " ":
72
+ e.preventDefault();
73
+ if (isOpen && focusedIndex >= 0 && !flatOptions[focusedIndex]?.disabled) {
74
+ handleSelect(flatOptions[focusedIndex].value);
75
+ } else {
76
+ setIsOpen((prev) => !prev);
77
+ }
78
+ break;
79
+ case "ArrowDown":
80
+ e.preventDefault();
81
+ if (!isOpen) {
82
+ setIsOpen(true);
83
+ } else {
84
+ setFocusedIndex((prev) => {
85
+ let next = prev + 1;
86
+ while (next < flatOptions.length && flatOptions[next].disabled) next++;
87
+ return next < flatOptions.length ? next : prev;
88
+ });
89
+ }
90
+ break;
91
+ case "ArrowUp":
92
+ e.preventDefault();
93
+ if (isOpen) {
94
+ setFocusedIndex((prev) => {
95
+ let next = prev - 1;
96
+ while (next >= 0 && flatOptions[next].disabled) next--;
97
+ return next >= 0 ? next : prev;
98
+ });
99
+ }
100
+ break;
101
+ case "Escape":
102
+ setIsOpen(false);
103
+ break;
104
+ case "Tab":
105
+ setIsOpen(false);
106
+ break;
189
107
  }
190
- ),
191
- helperText && /* @__PURE__ */ jsx("p", { className: `${baseClass}__helper`, id: `${inputId}-helper`, children: helperText })
192
- ] });
193
- };
108
+ };
109
+ useEffect(() => {
110
+ if (isOpen && focusedIndex >= 0 && listRef.current) {
111
+ const focusedEl = listRef.current.children[focusedIndex];
112
+ focusedEl?.scrollIntoView({ block: "nearest" });
113
+ }
114
+ }, [focusedIndex, isOpen]);
115
+ return /* @__PURE__ */ jsxs("div", { ...rest, className: classes, ref: setRootRef, children: [
116
+ label && /* @__PURE__ */ jsxs("label", { className: `${baseClass}__label`, id: `${inputId}-label`, children: [
117
+ label,
118
+ required && /* @__PURE__ */ jsxs("span", { className: `${baseClass}__required`, "aria-hidden": "true", children: [
119
+ " ",
120
+ "*"
121
+ ] })
122
+ ] }),
123
+ /* @__PURE__ */ jsxs(
124
+ "div",
125
+ {
126
+ className: `${baseClass}__trigger`,
127
+ role: "combobox",
128
+ "aria-expanded": isOpen,
129
+ "aria-haspopup": "listbox",
130
+ "aria-labelledby": label ? `${inputId}-label` : void 0,
131
+ "aria-label": ariaLabel || rest["aria-label"] || (!label ? placeholder : void 0),
132
+ "aria-controls": `${inputId}-listbox`,
133
+ tabIndex: disabled ? -1 : 0,
134
+ onClick: handleToggle,
135
+ onKeyDown: handleKeyDown,
136
+ children: [
137
+ /* @__PURE__ */ jsx(
138
+ "span",
139
+ {
140
+ className: `${baseClass}__value ${!selectedOption ? `${baseClass}__value--placeholder` : ""}`,
141
+ children: selectedOption ? selectedOption.label : placeholder
142
+ }
143
+ ),
144
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__chevron material-symbols-rounded`, "aria-hidden": "true", children: "expand_more" })
145
+ ]
146
+ }
147
+ ),
148
+ isOpen && /* @__PURE__ */ jsx(
149
+ "ul",
150
+ {
151
+ className: [`${baseClass}__menu`, isGrouped ? `${baseClass}__menu--grouped` : ""].filter(Boolean).join(" "),
152
+ role: "listbox",
153
+ id: `${inputId}-listbox`,
154
+ ref: listRef,
155
+ "aria-labelledby": label ? `${inputId}-label` : void 0,
156
+ children: isGrouped ? (() => {
157
+ let flatIndex = 0;
158
+ return groups.map((group, groupIdx) => /* @__PURE__ */ jsxs("li", { className: `${baseClass}__group`, role: "none", children: [
159
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__group-label`, role: "presentation", children: group.label }),
160
+ /* @__PURE__ */ jsx(
161
+ "ul",
162
+ {
163
+ className: `${baseClass}__group-list`,
164
+ role: "group",
165
+ "aria-label": group.label,
166
+ children: group.options.map((option) => {
167
+ const myIndex = flatIndex++;
168
+ return /* @__PURE__ */ jsxs(
169
+ "li",
170
+ {
171
+ className: [
172
+ `${baseClass}__option`,
173
+ option.value === value ? `${baseClass}__option--selected` : "",
174
+ option.disabled ? `${baseClass}__option--disabled` : "",
175
+ myIndex === focusedIndex ? `${baseClass}__option--focused` : ""
176
+ ].filter(Boolean).join(" "),
177
+ role: "option",
178
+ "aria-selected": option.value === value,
179
+ "aria-disabled": option.disabled,
180
+ onClick: () => !option.disabled && handleSelect(option.value),
181
+ children: [
182
+ option.label,
183
+ option.value === value && /* @__PURE__ */ jsx(
184
+ "span",
185
+ {
186
+ className: `${baseClass}__check material-symbols-rounded`,
187
+ "aria-hidden": "true",
188
+ children: "check"
189
+ }
190
+ )
191
+ ]
192
+ },
193
+ option.value
194
+ );
195
+ })
196
+ }
197
+ ),
198
+ groupIdx < groups.length - 1 && /* @__PURE__ */ jsx("li", { className: `${baseClass}__separator`, role: "separator" })
199
+ ] }, `group-${groupIdx}`));
200
+ })() : options.map((option, index) => /* @__PURE__ */ jsxs(
201
+ "li",
202
+ {
203
+ className: [
204
+ `${baseClass}__option`,
205
+ option.value === value ? `${baseClass}__option--selected` : "",
206
+ option.disabled ? `${baseClass}__option--disabled` : "",
207
+ index === focusedIndex ? `${baseClass}__option--focused` : ""
208
+ ].filter(Boolean).join(" "),
209
+ role: "option",
210
+ "aria-selected": option.value === value,
211
+ "aria-disabled": option.disabled,
212
+ onClick: () => !option.disabled && handleSelect(option.value),
213
+ children: [
214
+ option.label,
215
+ option.value === value && /* @__PURE__ */ jsx(
216
+ "span",
217
+ {
218
+ className: `${baseClass}__check material-symbols-rounded`,
219
+ "aria-hidden": "true",
220
+ children: "check"
221
+ }
222
+ )
223
+ ]
224
+ },
225
+ option.value
226
+ ))
227
+ }
228
+ ),
229
+ helperText && /* @__PURE__ */ jsx("p", { className: `${baseClass}__helper`, id: `${inputId}-helper`, children: helperText })
230
+ ] });
231
+ }
232
+ );
233
+ Dropdown.displayName = "Dropdown";
194
234
  export {
195
235
  Dropdown
196
236
  };
@@ -11,23 +11,16 @@ export interface FileInputFile {
11
11
  /** Per-file error message — replaces the size line */
12
12
  error?: string;
13
13
  }
14
- export interface FileInputProps {
14
+ /** Props owned by FileInput itself — everything else falls through to the native <input type="file">. */
15
+ type FileInputOwnProps = {
15
16
  /** Field label above the dropzone */
16
17
  label?: string;
17
18
  /** Instructional copy inside the dropzone */
18
19
  placeholder?: string;
19
- /** Comma-separated accept list, e.g. 'image/*,.pdf' */
20
- accept?: string;
21
- /** Allow selecting more than one file */
22
- multiple?: boolean;
23
20
  /** Files to list under the dropzone */
24
21
  files?: FileInputFile[];
25
- /** Component size */
22
+ /** Component size (not the native character-width `size` attribute) */
26
23
  size?: 'default' | 'compact';
27
- /** Whether the input is disabled */
28
- disabled?: boolean;
29
- /** Whether the input is required */
30
- required?: boolean;
31
24
  /** Error state — shows error styling and message */
32
25
  error?: boolean;
33
26
  /** Helper or error message displayed below the field */
@@ -36,19 +29,22 @@ export interface FileInputProps {
36
29
  onFilesSelected?: (files: File[]) => void;
37
30
  /** Called with the id of a file whose remove button was pressed */
38
31
  onFileRemove?: (id: string) => void;
39
- /** Additional CSS classes */
32
+ /** Additional CSS classes — applied to the wrapper, not the <input> */
40
33
  className?: string;
41
- /** Accessible name override */
34
+ /** @deprecated Pass the native `aria-label` attribute instead. */
42
35
  ariaLabel?: string;
43
- /** HTML name attribute */
44
- name?: string;
45
- /** HTML id attribute */
46
- id?: string;
36
+ };
37
+ export interface FileInputProps extends FileInputOwnProps, Omit<React.ComponentPropsWithoutRef<'input'>, keyof FileInputOwnProps | 'type'> {
47
38
  }
48
39
  /**
49
40
  * FileInput component — a click-or-drop zone paired with a list of the
50
41
  * chosen files. The list is fully controlled: pass `files` and handle
51
42
  * `onFilesSelected` / `onFileRemove` so upload progress and per-file
52
43
  * errors stay owned by the host app.
44
+ *
45
+ * Forwards a ref to the underlying `<input type="file">` — useful for
46
+ * programmatically opening the picker or clearing the selection — and spreads
47
+ * unrecognised props onto it.
53
48
  */
54
- export declare const FileInput: ({ label, placeholder, accept, multiple, files, size, disabled, required, error, helperText, onFilesSelected, onFileRemove, className, ariaLabel, name, id, }: FileInputProps) => React.JSX.Element;
49
+ export declare const FileInput: React.ForwardRefExoticComponent<FileInputProps & React.RefAttributes<HTMLInputElement>>;
50
+ export {};