@robr0/design-system 0.1.0 → 0.3.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 (91) hide show
  1. package/README.md +13 -9
  2. package/components/Accordion/Accordion.js +0 -1
  3. package/components/Avatar/Avatar.js +1 -0
  4. package/components/Breadcrumb/Breadcrumb.css +9 -1
  5. package/components/Breadcrumb/Breadcrumb.d.ts +7 -1
  6. package/components/Breadcrumb/Breadcrumb.js +3 -2
  7. package/components/Button/Button.css +18 -0
  8. package/components/Button/Button.d.ts +38 -16
  9. package/components/Button/Button.js +76 -58
  10. package/components/Card/Card.css +6 -0
  11. package/components/Card/Card.d.ts +17 -5
  12. package/components/Card/Card.js +101 -80
  13. package/components/Checkbox/Checkbox.d.ts +24 -12
  14. package/components/Checkbox/Checkbox.js +99 -85
  15. package/components/CircularButton/CircularButton.css +11 -0
  16. package/components/CircularButton/CircularButton.d.ts +7 -1
  17. package/components/CircularButton/CircularButton.js +15 -4
  18. package/components/CodeBlock/CodeBlock.js +10 -1
  19. package/components/ColorPicker/ColorPicker.css +278 -0
  20. package/components/ColorPicker/ColorPicker.d.ts +50 -0
  21. package/components/ColorPicker/ColorPicker.js +338 -0
  22. package/components/Combobox/Combobox.css +0 -36
  23. package/components/Combobox/Combobox.d.ts +19 -7
  24. package/components/Combobox/Combobox.js +106 -89
  25. package/components/CommandPalette/CommandPalette.css +1 -15
  26. package/components/CommandPalette/CommandPalette.js +6 -5
  27. package/components/ContextMenu/ContextMenu.css +262 -0
  28. package/components/ContextMenu/ContextMenu.d.ts +26 -0
  29. package/components/ContextMenu/ContextMenu.js +350 -0
  30. package/components/DateInput/DateInput.css +0 -36
  31. package/components/DateInput/DateInput.d.ts +18 -21
  32. package/components/DateInput/DateInput.js +76 -64
  33. package/components/DatePicker/DatePicker.d.ts +9 -2
  34. package/components/DatePicker/DatePicker.js +137 -128
  35. package/components/Dialog/Dialog.d.ts +15 -4
  36. package/components/Dialog/Dialog.js +136 -118
  37. package/components/Drawer/Drawer.d.ts +15 -4
  38. package/components/Drawer/Drawer.js +138 -119
  39. package/components/Dropdown/Dropdown.css +0 -36
  40. package/components/Dropdown/Dropdown.d.ts +24 -8
  41. package/components/Dropdown/Dropdown.js +219 -169
  42. package/components/DropdownMenu/DropdownMenu.js +10 -7
  43. package/components/Field/Field.css +80 -0
  44. package/components/Field/Field.d.ts +50 -0
  45. package/components/Field/Field.js +58 -0
  46. package/components/Field/FieldContext.d.ts +42 -0
  47. package/components/Field/FieldContext.js +7 -0
  48. package/components/FileInput/FileInput.css +0 -36
  49. package/components/FileInput/FileInput.d.ts +13 -17
  50. package/components/FileInput/FileInput.js +168 -141
  51. package/components/Input/Input.css +0 -43
  52. package/components/Input/Input.d.ts +23 -22
  53. package/components/Input/Input.js +93 -66
  54. package/components/Kbd/Kbd.css +28 -0
  55. package/components/Kbd/Kbd.d.ts +19 -0
  56. package/components/Kbd/Kbd.js +14 -0
  57. package/components/Popover/Popover.d.ts +0 -6
  58. package/components/Popover/Popover.js +22 -16
  59. package/components/ProgressBar/ProgressBar.js +1 -1
  60. package/components/RadioButton/RadioButton.d.ts +26 -13
  61. package/components/RadioButton/RadioButton.js +95 -70
  62. package/components/SelectionCard/SelectionCard.css +6 -2
  63. package/components/SelectionCard/SelectionCard.d.ts +10 -4
  64. package/components/SelectionCard/SelectionCard.js +72 -56
  65. package/components/Skeleton/Skeleton.css +2 -0
  66. package/components/Slider/Slider.d.ts +19 -10
  67. package/components/Slider/Slider.js +50 -40
  68. package/components/Spinner/Spinner.css +11 -0
  69. package/components/Spinner/Spinner.d.ts +2 -2
  70. package/components/Swatch/Swatch.css +52 -0
  71. package/components/Swatch/Swatch.d.ts +34 -0
  72. package/components/Swatch/Swatch.js +44 -0
  73. package/components/Table/Table.css +19 -0
  74. package/components/Table/Table.d.ts +10 -2
  75. package/components/Table/Table.js +58 -58
  76. package/components/Textarea/Textarea.css +0 -43
  77. package/components/Textarea/Textarea.d.ts +21 -22
  78. package/components/Textarea/Textarea.js +78 -67
  79. package/components/ToggleGroup/ToggleGroup.d.ts +18 -8
  80. package/components/ToggleGroup/ToggleGroup.js +61 -44
  81. package/components/ToggleSwitch/ToggleSwitch.css +7 -2
  82. package/components/ToggleSwitch/ToggleSwitch.d.ts +17 -9
  83. package/components/ToggleSwitch/ToggleSwitch.js +57 -44
  84. package/components/registry.d.ts +47 -10
  85. package/components/registry.js +5 -1
  86. package/components/registry.json +498 -56
  87. package/components/registry.json.d.ts +498 -56
  88. package/components/registry.json.js +4 -1
  89. package/index.d.ts +8 -1
  90. package/index.js +16 -1
  91. package/package.json +1 -1
@@ -1,156 +1,219 @@
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
+ import { Field } from "../Field/Field.js";
3
4
  import "./Dropdown.css";
4
5
  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)) {
6
+ const Dropdown = React.forwardRef(
7
+ ({
8
+ label,
9
+ placeholder = "Select an option",
10
+ value,
11
+ options,
12
+ groups,
13
+ size = "default",
14
+ disabled = false,
15
+ required = false,
16
+ error = false,
17
+ helperText,
18
+ onValueChange,
19
+ onChange,
20
+ className = "",
21
+ ariaLabel,
22
+ name,
23
+ id,
24
+ ...rest
25
+ }, ref) => {
26
+ const [isOpen, setIsOpen] = useState(false);
27
+ const [focusedIndex, setFocusedIndex] = useState(-1);
28
+ const dropdownRef = useRef(null);
29
+ const listRef = useRef(null);
30
+ const setRootRef = (node) => {
31
+ dropdownRef.current = node;
32
+ if (typeof ref === "function") ref(node);
33
+ else if (ref) ref.current = node;
34
+ };
35
+ const baseClass = "ds-dropdown";
36
+ const isGrouped = groups && groups.length > 0;
37
+ const classes = [
38
+ baseClass,
39
+ size === "compact" ? `${baseClass}--compact` : "",
40
+ isOpen ? `${baseClass}--open` : "",
41
+ error ? `${baseClass}--error` : "",
42
+ disabled ? `${baseClass}--disabled` : "",
43
+ className
44
+ ].filter(Boolean).join(" ");
45
+ const flatOptions = isGrouped ? groups.flatMap((g) => g.options) : options;
46
+ const selectedOption = flatOptions.find((opt) => opt.value === value);
47
+ const inputId = id || name || label?.toLowerCase().replace(/\s+/g, "-");
48
+ const handleToggle = () => {
49
+ if (!disabled) {
50
+ setIsOpen((prev) => !prev);
51
+ setFocusedIndex(-1);
52
+ }
53
+ };
54
+ const handleSelect = (optionValue) => {
55
+ onValueChange?.(optionValue);
56
+ onChange?.(optionValue);
50
57
  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":
58
+ };
59
+ const handleClickOutside = useCallback((e) => {
60
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
95
61
  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
62
  }
128
- ),
129
- isOpen && /* @__PURE__ */ jsx(
130
- "ul",
63
+ }, []);
64
+ useEffect(() => {
65
+ document.addEventListener("mousedown", handleClickOutside);
66
+ return () => document.removeEventListener("mousedown", handleClickOutside);
67
+ }, [handleClickOutside]);
68
+ const handleKeyDown = (e) => {
69
+ if (disabled) return;
70
+ switch (e.key) {
71
+ case "Enter":
72
+ case " ":
73
+ e.preventDefault();
74
+ if (isOpen && focusedIndex >= 0 && !flatOptions[focusedIndex]?.disabled) {
75
+ handleSelect(flatOptions[focusedIndex].value);
76
+ } else {
77
+ setIsOpen((prev) => !prev);
78
+ }
79
+ break;
80
+ case "ArrowDown":
81
+ e.preventDefault();
82
+ if (!isOpen) {
83
+ setIsOpen(true);
84
+ } else {
85
+ setFocusedIndex((prev) => {
86
+ let next = prev + 1;
87
+ while (next < flatOptions.length && flatOptions[next].disabled) next++;
88
+ return next < flatOptions.length ? next : prev;
89
+ });
90
+ }
91
+ break;
92
+ case "ArrowUp":
93
+ e.preventDefault();
94
+ if (isOpen) {
95
+ setFocusedIndex((prev) => {
96
+ let next = prev - 1;
97
+ while (next >= 0 && flatOptions[next].disabled) next--;
98
+ return next >= 0 ? next : prev;
99
+ });
100
+ }
101
+ break;
102
+ case "Escape":
103
+ setIsOpen(false);
104
+ break;
105
+ case "Tab":
106
+ setIsOpen(false);
107
+ break;
108
+ }
109
+ };
110
+ useEffect(() => {
111
+ if (isOpen && focusedIndex >= 0 && listRef.current) {
112
+ const focusedEl = listRef.current.children[focusedIndex];
113
+ focusedEl?.scrollIntoView({ block: "nearest" });
114
+ }
115
+ }, [focusedIndex, isOpen]);
116
+ return /* @__PURE__ */ jsxs(
117
+ Field,
131
118
  {
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(
119
+ ...rest,
120
+ className: classes,
121
+ ref: setRootRef,
122
+ label,
123
+ helperText,
124
+ error,
125
+ required,
126
+ disabled,
127
+ size,
128
+ id: inputId,
129
+ children: [
130
+ /* @__PURE__ */ jsxs(
131
+ "div",
132
+ {
133
+ className: `${baseClass}__trigger`,
134
+ role: "combobox",
135
+ "aria-expanded": isOpen,
136
+ "aria-haspopup": "listbox",
137
+ "aria-labelledby": label ? `${inputId}-label` : void 0,
138
+ "aria-label": ariaLabel || rest["aria-label"] || (!label ? placeholder : void 0),
139
+ "aria-controls": `${inputId}-listbox`,
140
+ "aria-describedby": helperText ? `${inputId}-helper` : rest["aria-describedby"],
141
+ "aria-invalid": error || void 0,
142
+ tabIndex: disabled ? -1 : 0,
143
+ onClick: handleToggle,
144
+ onKeyDown: handleKeyDown,
145
+ children: [
146
+ /* @__PURE__ */ jsx(
147
+ "span",
148
+ {
149
+ className: `${baseClass}__value ${!selectedOption ? `${baseClass}__value--placeholder` : ""}`,
150
+ children: selectedOption ? selectedOption.label : placeholder
151
+ }
152
+ ),
153
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__chevron material-symbols-rounded`, "aria-hidden": "true", children: "expand_more" })
154
+ ]
155
+ }
156
+ ),
157
+ isOpen && /* @__PURE__ */ jsx(
158
+ "ul",
159
+ {
160
+ className: [`${baseClass}__menu`, isGrouped ? `${baseClass}__menu--grouped` : ""].filter(Boolean).join(" "),
161
+ role: "listbox",
162
+ id: `${inputId}-listbox`,
163
+ ref: listRef,
164
+ "aria-labelledby": label ? `${inputId}-label` : void 0,
165
+ children: isGrouped ? (() => {
166
+ let flatIndex = 0;
167
+ return groups.map((group, groupIdx) => /* @__PURE__ */ jsxs("li", { className: `${baseClass}__group`, role: "none", children: [
168
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__group-label`, role: "presentation", children: group.label }),
169
+ /* @__PURE__ */ jsx(
170
+ "ul",
171
+ {
172
+ className: `${baseClass}__group-list`,
173
+ role: "group",
174
+ "aria-label": group.label,
175
+ children: group.options.map((option) => {
176
+ const myIndex = flatIndex++;
177
+ return /* @__PURE__ */ jsxs(
178
+ "li",
179
+ {
180
+ className: [
181
+ `${baseClass}__option`,
182
+ option.value === value ? `${baseClass}__option--selected` : "",
183
+ option.disabled ? `${baseClass}__option--disabled` : "",
184
+ myIndex === focusedIndex ? `${baseClass}__option--focused` : ""
185
+ ].filter(Boolean).join(" "),
186
+ role: "option",
187
+ "aria-selected": option.value === value,
188
+ "aria-disabled": option.disabled,
189
+ onClick: () => !option.disabled && handleSelect(option.value),
190
+ children: [
191
+ option.label,
192
+ option.value === value && /* @__PURE__ */ jsx(
193
+ "span",
194
+ {
195
+ className: `${baseClass}__check material-symbols-rounded`,
196
+ "aria-hidden": "true",
197
+ children: "check"
198
+ }
199
+ )
200
+ ]
201
+ },
202
+ option.value
203
+ );
204
+ })
205
+ }
206
+ ),
207
+ groupIdx < groups.length - 1 && /* @__PURE__ */ jsx("li", { className: `${baseClass}__separator`, role: "separator" })
208
+ ] }, `group-${groupIdx}`));
209
+ })() : options.map((option, index) => /* @__PURE__ */ jsxs(
147
210
  "li",
148
211
  {
149
212
  className: [
150
213
  `${baseClass}__option`,
151
214
  option.value === value ? `${baseClass}__option--selected` : "",
152
215
  option.disabled ? `${baseClass}__option--disabled` : "",
153
- myIndex === focusedIndex ? `${baseClass}__option--focused` : ""
216
+ index === focusedIndex ? `${baseClass}__option--focused` : ""
154
217
  ].filter(Boolean).join(" "),
155
218
  role: "option",
156
219
  "aria-selected": option.value === value,
@@ -158,39 +221,26 @@ const Dropdown = ({
158
221
  onClick: () => !option.disabled && handleSelect(option.value),
159
222
  children: [
160
223
  option.label,
161
- option.value === value && /* @__PURE__ */ jsx("span", { className: `${baseClass}__check material-symbols-rounded`, "aria-hidden": "true", children: "check" })
224
+ option.value === value && /* @__PURE__ */ jsx(
225
+ "span",
226
+ {
227
+ className: `${baseClass}__check material-symbols-rounded`,
228
+ "aria-hidden": "true",
229
+ children: "check"
230
+ }
231
+ )
162
232
  ]
163
233
  },
164
234
  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
- ))
235
+ ))
236
+ }
237
+ )
238
+ ]
189
239
  }
190
- ),
191
- helperText && /* @__PURE__ */ jsx("p", { className: `${baseClass}__helper`, id: `${inputId}-helper`, children: helperText })
192
- ] });
193
- };
240
+ );
241
+ }
242
+ );
243
+ Dropdown.displayName = "Dropdown";
194
244
  export {
195
245
  Dropdown
196
246
  };
@@ -1,5 +1,5 @@
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 "./DropdownMenu.css";
4
4
  import "../../fonts/material-symbols.css";
5
5
  function collectItems(entries) {
@@ -291,19 +291,22 @@ const DropdownMenu = ({
291
291
  ].filter(Boolean).join(" ");
292
292
  const itemCounter = { count: 0 };
293
293
  return /* @__PURE__ */ jsxs("div", { className: classes, ref: rootRef, children: [
294
- /* @__PURE__ */ jsx(
295
- "div",
294
+ /* @__PURE__ */ jsx("div", { className: `${baseClass}__trigger`, children: React.isValidElement(trigger) ? React.cloneElement(trigger, {
295
+ "aria-haspopup": "menu",
296
+ "aria-expanded": isOpen,
297
+ onClick: handleToggle,
298
+ onKeyDown: handleKeyDown
299
+ }) : /* @__PURE__ */ jsx(
300
+ "button",
296
301
  {
297
- className: `${baseClass}__trigger`,
298
- role: "button",
299
- tabIndex: 0,
302
+ type: "button",
300
303
  "aria-haspopup": "menu",
301
304
  "aria-expanded": isOpen,
302
305
  onClick: handleToggle,
303
306
  onKeyDown: handleKeyDown,
304
307
  children: trigger
305
308
  }
306
- ),
309
+ ) }),
307
310
  isOpen && /* @__PURE__ */ jsx(
308
311
  "ul",
309
312
  {
@@ -0,0 +1,80 @@
1
+ /* ============================================
2
+ FIELD COMPONENT
3
+ Label, required marker, and helper/error text
4
+ shared by every labelled form control.
5
+
6
+ Layout (flex column, gap) deliberately stays on the
7
+ consuming component's own root class — Field only
8
+ owns the label and helper scaffolding, so adopting it
9
+ changes no spacing.
10
+ ============================================ */
11
+
12
+ /* ============================================
13
+ LABEL
14
+ ============================================ */
15
+
16
+ .ds-field__label {
17
+ font-family: var(--font-paragraph-em-family);
18
+ font-size: var(--font-paragraph-em-size);
19
+ font-weight: var(--font-paragraph-em-weight);
20
+ line-height: var(--font-paragraph-em-line-height);
21
+ letter-spacing: var(--font-paragraph-em-letter-spacing);
22
+ color: var(--color-text-primary);
23
+ }
24
+
25
+ .ds-field__required {
26
+ color: var(--color-core-accent-coral);
27
+ }
28
+
29
+ /* ============================================
30
+ FOOTER — only rendered when an `aside` is supplied
31
+ (a character counter, a unit). Without one the helper
32
+ sits directly in the field column, so adopting Field
33
+ adds no wrapper to existing markup.
34
+ ============================================ */
35
+
36
+ .ds-field__footer {
37
+ display: flex;
38
+ align-items: flex-start;
39
+ justify-content: space-between;
40
+ gap: var(--gap-sm);
41
+ }
42
+
43
+ /* ============================================
44
+ HELPER TEXT
45
+ ============================================ */
46
+
47
+ .ds-field__helper {
48
+ font-family: var(--font-paragraph-sm-family);
49
+ font-size: var(--font-paragraph-sm-size);
50
+ font-weight: var(--font-paragraph-sm-weight);
51
+ line-height: var(--font-paragraph-sm-line-height);
52
+ letter-spacing: var(--font-paragraph-sm-letter-spacing);
53
+ color: var(--color-text-tertiary);
54
+ margin: 0;
55
+ }
56
+
57
+ /* ============================================
58
+ ERROR STATE
59
+ ============================================ */
60
+
61
+ .ds-field--error .ds-field__helper {
62
+ color: var(--color-status-error-border);
63
+ }
64
+
65
+ /* ============================================
66
+ DISABLED STATE
67
+ ============================================ */
68
+
69
+ .ds-field--disabled .ds-field__label {
70
+ color: var(--color-input-text-disabled);
71
+ }
72
+
73
+ /* ============================================
74
+ COMPACT SIZE
75
+ ============================================ */
76
+
77
+ .ds-field--compact .ds-field__label {
78
+ font-size: var(--font-paragraph-sm-size);
79
+ line-height: var(--font-paragraph-sm-line-height);
80
+ }
@@ -0,0 +1,50 @@
1
+ import { default as React } from 'react';
2
+ /** Props owned by Field itself — everything else falls through to the wrapper. */
3
+ type FieldOwnProps = {
4
+ /** Label text rendered above the control */
5
+ label?: string;
6
+ /** The form control this field labels */
7
+ children?: React.ReactNode;
8
+ /** Helper or error message rendered below the control */
9
+ helperText?: string;
10
+ /**
11
+ * Optional content rendered opposite the helper text — a character counter,
12
+ * a unit, a "0/280". Present only when supplied; without it the helper sits
13
+ * directly in the field's column and no extra wrapper is introduced.
14
+ */
15
+ aside?: React.ReactNode;
16
+ /** Error state — recolours the helper text and marks the control invalid */
17
+ error?: boolean;
18
+ /** Marks the field required and renders the required marker */
19
+ required?: boolean;
20
+ /** Whether the control is disabled — dims the label */
21
+ disabled?: boolean;
22
+ /** Component size */
23
+ size?: 'default' | 'compact';
24
+ /**
25
+ * id for the control. Generated when omitted, so the label/control/helper
26
+ * association works with no configuration.
27
+ */
28
+ id?: string;
29
+ /** Additional CSS classes */
30
+ className?: string;
31
+ };
32
+ export interface FieldProps extends FieldOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof FieldOwnProps> {
33
+ }
34
+ /**
35
+ * Field owns the scaffolding shared by every labelled form control: the label
36
+ * and its `htmlFor`, the required marker, the helper/error message, the ids
37
+ * that tie them together, and the `aria-describedby` / `aria-invalid` wiring.
38
+ *
39
+ * Controls compose inside it and read the wiring through `useField()` rather
40
+ * than deriving ids themselves — which is what keeps accessibility correct
41
+ * systemically instead of one component at a time.
42
+ *
43
+ * ```tsx
44
+ * <Field label="Email" helperText="We never share it." required>
45
+ * <MyControl />
46
+ * </Field>
47
+ * ```
48
+ */
49
+ export declare const Field: React.ForwardRefExoticComponent<FieldProps & React.RefAttributes<HTMLDivElement>>;
50
+ export {};
@@ -0,0 +1,58 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import React, { useId } from "react";
3
+ import { FieldContext } from "./FieldContext.js";
4
+ import "./Field.css";
5
+ const Field = React.forwardRef(
6
+ ({
7
+ label,
8
+ children,
9
+ helperText,
10
+ aside,
11
+ error = false,
12
+ required = false,
13
+ disabled = false,
14
+ size = "default",
15
+ id,
16
+ className = "",
17
+ ...rest
18
+ }, ref) => {
19
+ const baseClass = "ds-field";
20
+ const generatedId = useId();
21
+ const controlId = id || generatedId;
22
+ const describedBy = helperText ? `${controlId}-helper` : void 0;
23
+ const labelId = label ? `${controlId}-label` : void 0;
24
+ const classes = [
25
+ baseClass,
26
+ size === "compact" ? `${baseClass}--compact` : "",
27
+ error ? `${baseClass}--error` : "",
28
+ disabled ? `${baseClass}--disabled` : "",
29
+ className
30
+ ].filter(Boolean).join(" ");
31
+ const context = {
32
+ controlId,
33
+ labelId,
34
+ describedBy,
35
+ invalid: error,
36
+ required,
37
+ disabled
38
+ };
39
+ return /* @__PURE__ */ jsx(FieldContext.Provider, { value: context, children: /* @__PURE__ */ jsxs("div", { ...rest, ref, className: classes, children: [
40
+ label && /* @__PURE__ */ jsxs("label", { className: `${baseClass}__label`, id: labelId, htmlFor: controlId, children: [
41
+ label,
42
+ required && /* @__PURE__ */ jsxs("span", { className: `${baseClass}__required`, "aria-hidden": "true", children: [
43
+ " ",
44
+ "*"
45
+ ] })
46
+ ] }),
47
+ children,
48
+ aside ? /* @__PURE__ */ jsxs("div", { className: `${baseClass}__footer`, children: [
49
+ helperText ? /* @__PURE__ */ jsx("p", { className: `${baseClass}__helper`, id: describedBy, children: helperText }) : /* @__PURE__ */ jsx("span", {}),
50
+ aside
51
+ ] }) : helperText && /* @__PURE__ */ jsx("p", { className: `${baseClass}__helper`, id: describedBy, children: helperText })
52
+ ] }) });
53
+ }
54
+ );
55
+ Field.displayName = "Field";
56
+ export {
57
+ Field
58
+ };