@robr0/design-system 0.8.0 → 0.9.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 (95) hide show
  1. package/README.md +3 -3
  2. package/components/Accordion/Accordion.js +1 -0
  3. package/components/AgentPlan/AgentPlan.css +2 -2
  4. package/components/AgentPlan/AgentPlan.js +1 -0
  5. package/components/AgentStatus/AgentStatus.js +1 -0
  6. package/components/AiButton/AiButton.js +1 -0
  7. package/components/AlertDialog/AlertDialog.js +1 -0
  8. package/components/AnchorNav/AnchorNav.css +63 -0
  9. package/components/AnchorNav/AnchorNav.d.ts +33 -0
  10. package/components/AnchorNav/AnchorNav.js +82 -0
  11. package/components/AppLayout/AppLayout.js +1 -0
  12. package/components/AppSidebar/AppSidebar.js +1 -0
  13. package/components/Avatar/Avatar.js +1 -0
  14. package/components/Button/Button.js +1 -0
  15. package/components/Card/Card.js +1 -0
  16. package/components/Carousel/Carousel.js +1 -0
  17. package/components/ChatThread/ChatThread.js +1 -0
  18. package/components/Checkbox/Checkbox.js +1 -0
  19. package/components/CodeBlock/CodeBlock.js +1 -0
  20. package/components/CodeDiff/CodeDiff.css +175 -0
  21. package/components/CodeDiff/CodeDiff.d.ts +58 -0
  22. package/components/CodeDiff/CodeDiff.js +114 -0
  23. package/components/ColorPicker/ColorPicker.js +1 -0
  24. package/components/Combobox/Combobox.js +1 -0
  25. package/components/CommandPalette/CommandPalette.js +1 -0
  26. package/components/Composer/Composer.js +1 -0
  27. package/components/ContactCard/ContactCard.js +1 -0
  28. package/components/ContextMenu/ContextMenu.js +1 -0
  29. package/components/DataTable/DataTable.js +1 -0
  30. package/components/DateInput/DateInput.js +1 -0
  31. package/components/DatePicker/DatePicker.js +1 -0
  32. package/components/Dialog/Dialog.js +1 -0
  33. package/components/Drawer/Drawer.js +1 -0
  34. package/components/Dropdown/Dropdown.js +1 -0
  35. package/components/DropdownMenu/DropdownMenu.css +1 -1
  36. package/components/DropdownMenu/DropdownMenu.js +1 -0
  37. package/components/EventCalendar/EventCalendar.js +1 -0
  38. package/components/Field/Field.js +1 -0
  39. package/components/Field/FieldContext.js +1 -0
  40. package/components/FileInput/FileInput.js +1 -0
  41. package/components/Input/Input.js +1 -0
  42. package/components/InterruptCard/InterruptCard.css +11 -21
  43. package/components/InterruptCard/InterruptCard.d.ts +5 -6
  44. package/components/MessageCard/MessageCard.css +1 -2
  45. package/components/ModelPicker/ModelPicker.css +3 -1
  46. package/components/ModelPicker/ModelPicker.js +1 -0
  47. package/components/NavList/NavList.js +1 -0
  48. package/components/NotificationCenter/NotificationCenter.js +1 -0
  49. package/components/NumberInput/NumberInput.css +154 -0
  50. package/components/NumberInput/NumberInput.d.ts +44 -0
  51. package/components/NumberInput/NumberInput.js +147 -0
  52. package/components/Pagination/Pagination.js +1 -0
  53. package/components/PinInput/PinInput.css +92 -0
  54. package/components/PinInput/PinInput.d.ts +46 -0
  55. package/components/PinInput/PinInput.js +161 -0
  56. package/components/Popover/Popover.css +5 -6
  57. package/components/Popover/Popover.js +1 -0
  58. package/components/RadioButton/RadioButton.js +1 -0
  59. package/components/Reasoning/Reasoning.js +1 -0
  60. package/components/SegmentedControl/SegmentedControl.js +1 -0
  61. package/components/SelectionCard/SelectionCard.js +1 -0
  62. package/components/ShaderField/ShaderField.js +1 -0
  63. package/components/ShaderField/useShaderField.js +1 -0
  64. package/components/Slider/Slider.js +1 -0
  65. package/components/Sparkline/Sparkline.css +52 -0
  66. package/components/Sparkline/Sparkline.d.ts +49 -0
  67. package/components/Sparkline/Sparkline.js +86 -0
  68. package/components/Stepper/Stepper.css +207 -0
  69. package/components/Stepper/Stepper.d.ts +41 -0
  70. package/components/Stepper/Stepper.js +61 -0
  71. package/components/Tabs/Tabs.js +1 -0
  72. package/components/TagInput/TagInput.css +160 -0
  73. package/components/TagInput/TagInput.d.ts +43 -0
  74. package/components/TagInput/TagInput.js +137 -0
  75. package/components/Textarea/Textarea.js +1 -0
  76. package/components/TimePicker/TimePicker.css +230 -0
  77. package/components/TimePicker/TimePicker.d.ts +46 -0
  78. package/components/TimePicker/TimePicker.js +246 -0
  79. package/components/Toast/Toast.js +1 -0
  80. package/components/ToggleGroup/ToggleGroup.js +1 -0
  81. package/components/ToggleSwitch/ToggleSwitch.js +1 -0
  82. package/components/ToolCall/ToolCall.css +7 -11
  83. package/components/ToolCall/ToolCall.js +1 -0
  84. package/components/Tooltip/Tooltip.js +1 -0
  85. package/components/TreeView/TreeView.css +119 -0
  86. package/components/TreeView/TreeView.d.ts +45 -0
  87. package/components/TreeView/TreeView.js +232 -0
  88. package/components/registry.d.ts +22 -6
  89. package/components/registry.js +3 -1
  90. package/components/registry.json +206 -20
  91. package/components/registry.json.d.ts +206 -20
  92. package/components/registry.json.js +2 -2
  93. package/index.d.ts +11 -2
  94. package/index.js +21 -1
  95. package/package.json +1 -1
@@ -0,0 +1,207 @@
1
+ /* ============================================
2
+ STEPPER COMPONENT
3
+ Step-by-step progress indicator for wizards
4
+ and multi-stage flows
5
+ ============================================ */
6
+
7
+ /* Base */
8
+
9
+ .ds-stepper {
10
+ display: flex;
11
+ align-items: flex-start;
12
+ list-style: none;
13
+ margin: 0;
14
+ padding: 0;
15
+ width: 100%;
16
+ }
17
+
18
+ .ds-stepper__step {
19
+ position: relative;
20
+ flex: 1 1 0;
21
+ min-width: 0;
22
+ display: flex;
23
+ justify-content: center;
24
+ }
25
+
26
+ /* Shared inner layout — a <button> when the step is clickable,
27
+ a <span> otherwise */
28
+ .ds-stepper__content {
29
+ display: flex;
30
+ flex-direction: column;
31
+ align-items: center;
32
+ gap: var(--gap-sm); /* 8px */
33
+ padding: 0;
34
+ border: none;
35
+ background-color: transparent;
36
+ font-family: inherit;
37
+ text-align: center;
38
+ color: inherit;
39
+ min-width: 0;
40
+ }
41
+
42
+ button.ds-stepper__content {
43
+ cursor: pointer;
44
+ }
45
+
46
+ button.ds-stepper__content:focus-visible {
47
+ outline: 2px solid var(--color-action-primary-bg);
48
+ outline-offset: 2px;
49
+ border-radius: var(--radius-sm);
50
+ }
51
+
52
+ /* ============================================
53
+ INDICATOR
54
+ 32px circle — check for complete, number
55
+ for active and upcoming
56
+ ============================================ */
57
+
58
+ .ds-stepper__indicator {
59
+ --icon-size: var(--icon-size-sm);
60
+ display: inline-flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ width: 32px;
64
+ height: 32px;
65
+ flex-shrink: 0;
66
+ border: var(--border-xs) solid transparent;
67
+ border-radius: var(--radius-full);
68
+ font-family: var(--font-paragraph-sm-em-family);
69
+ font-size: var(--font-paragraph-sm-em-size);
70
+ font-weight: var(--font-paragraph-sm-em-weight);
71
+ line-height: var(--font-paragraph-sm-em-line-height);
72
+ letter-spacing: var(--font-paragraph-sm-em-letter-spacing);
73
+ transition:
74
+ background-color var(--motion-duration-base) var(--motion-ease-standard),
75
+ border-color var(--motion-duration-base) var(--motion-ease-standard),
76
+ color var(--motion-duration-base) var(--motion-ease-standard);
77
+ }
78
+
79
+ /* States */
80
+
81
+ .ds-stepper__step--complete .ds-stepper__indicator {
82
+ border-color: var(--color-action-primary-border-secondary);
83
+ color: var(--color-action-primary-text-tertiary);
84
+ }
85
+
86
+ .ds-stepper__step--active .ds-stepper__indicator {
87
+ background-color: var(--color-action-primary-bg);
88
+ border-color: var(--color-action-primary-bg);
89
+ color: var(--color-action-primary-text);
90
+ }
91
+
92
+ .ds-stepper__step--upcoming .ds-stepper__indicator {
93
+ border-color: var(--color-bg-container-border);
94
+ color: var(--color-text-tertiary);
95
+ }
96
+
97
+ button.ds-stepper__content:hover .ds-stepper__indicator {
98
+ background-color: var(--color-action-passive-bg-hover);
99
+ }
100
+
101
+ .ds-stepper__step--active button.ds-stepper__content:hover .ds-stepper__indicator {
102
+ background-color: var(--color-action-primary-bg-hover);
103
+ }
104
+
105
+ /* ============================================
106
+ LABEL + DESCRIPTION
107
+ ============================================ */
108
+
109
+ .ds-stepper__text {
110
+ display: flex;
111
+ flex-direction: column;
112
+ gap: var(--gap-xxs); /* 2px */
113
+ min-width: 0;
114
+ }
115
+
116
+ .ds-stepper__label {
117
+ font-family: var(--font-paragraph-sm-em-family);
118
+ font-size: var(--font-paragraph-sm-em-size);
119
+ font-weight: var(--font-paragraph-sm-em-weight);
120
+ line-height: var(--font-paragraph-sm-em-line-height);
121
+ letter-spacing: var(--font-paragraph-sm-em-letter-spacing);
122
+ color: var(--color-text-secondary);
123
+ transition: color var(--motion-duration-base) var(--motion-ease-standard);
124
+ }
125
+
126
+ .ds-stepper__step--active .ds-stepper__label {
127
+ color: var(--color-text-primary);
128
+ }
129
+
130
+ .ds-stepper__step--upcoming .ds-stepper__label {
131
+ color: var(--color-text-tertiary);
132
+ }
133
+
134
+ button.ds-stepper__content:hover .ds-stepper__label {
135
+ color: var(--color-text-primary);
136
+ }
137
+
138
+ .ds-stepper__description {
139
+ font-family: var(--font-paragraph-sm-family);
140
+ font-size: var(--font-paragraph-sm-size);
141
+ font-weight: var(--font-paragraph-sm-weight);
142
+ line-height: var(--font-paragraph-sm-line-height);
143
+ letter-spacing: var(--font-paragraph-sm-letter-spacing);
144
+ color: var(--color-text-tertiary);
145
+ }
146
+
147
+ /* ============================================
148
+ CONNECTOR
149
+ Drawn between an indicator and the next;
150
+ turns action-coloured behind completed steps
151
+ ============================================ */
152
+
153
+ .ds-stepper__connector {
154
+ position: absolute;
155
+ top: 16px; /* vertical centre of the 32px indicator */
156
+ left: calc(50% + 24px);
157
+ right: calc(-50% + 24px);
158
+ height: var(--border-md);
159
+ transform: translateY(-50%);
160
+ background-color: var(--color-divider);
161
+ transition: background-color var(--motion-duration-base) var(--motion-ease-standard);
162
+ }
163
+
164
+ .ds-stepper__step--complete .ds-stepper__connector {
165
+ background-color: var(--color-action-primary-border-secondary);
166
+ }
167
+
168
+ /* ============================================
169
+ VERTICAL
170
+ ============================================ */
171
+
172
+ .ds-stepper--vertical {
173
+ flex-direction: column;
174
+ align-items: stretch;
175
+ }
176
+
177
+ .ds-stepper--vertical .ds-stepper__step {
178
+ flex: none;
179
+ justify-content: flex-start;
180
+ padding-bottom: var(--gap-lg); /* 20px */
181
+ }
182
+
183
+ .ds-stepper--vertical .ds-stepper__step:last-child {
184
+ padding-bottom: 0;
185
+ }
186
+
187
+ .ds-stepper--vertical .ds-stepper__content {
188
+ flex-direction: row;
189
+ align-items: flex-start;
190
+ gap: var(--gap-sm-md); /* 12px */
191
+ text-align: left;
192
+ }
193
+
194
+ /* Centre the first label line on the 32px indicator */
195
+ .ds-stepper--vertical .ds-stepper__text {
196
+ padding-top: calc((32px - var(--font-paragraph-sm-em-line-height)) / 2);
197
+ }
198
+
199
+ .ds-stepper--vertical .ds-stepper__connector {
200
+ top: calc(32px + var(--gap-xs));
201
+ bottom: var(--gap-xs);
202
+ left: 16px; /* horizontal centre of the 32px indicator */
203
+ right: auto;
204
+ width: var(--border-md);
205
+ height: auto;
206
+ transform: translateX(-50%);
207
+ }
@@ -0,0 +1,41 @@
1
+ import { default as React } from 'react';
2
+ /** A single step in a Stepper flow. */
3
+ export type StepperStep = {
4
+ /** Step label shown beside or below the indicator */
5
+ label: string;
6
+ /** Optional supporting line rendered under the label */
7
+ description?: string;
8
+ };
9
+ /** Props owned by Stepper itself — everything else falls through to the `<ol>`. */
10
+ type StepperOwnProps = {
11
+ /** Ordered list of steps in the flow */
12
+ steps: StepperStep[];
13
+ /**
14
+ * Index of the active step. Steps before it render as complete (check
15
+ * icon), steps after it as upcoming (step number). Pass `steps.length`
16
+ * to mark the whole flow complete.
17
+ */
18
+ activeStep: number;
19
+ /**
20
+ * Callback with the clicked step's index. When provided, completed steps
21
+ * and the active step become buttons so the reader can revisit an earlier
22
+ * stage; upcoming steps stay non-interactive either way.
23
+ */
24
+ onStepClick?: (index: number) => void;
25
+ /** Layout direction */
26
+ orientation?: 'horizontal' | 'vertical';
27
+ /** Additional CSS classes */
28
+ className?: string;
29
+ };
30
+ export interface StepperProps extends StepperOwnProps, Omit<React.ComponentPropsWithoutRef<'ol'>, keyof StepperOwnProps> {
31
+ }
32
+ /**
33
+ * Stepper component — step-by-step progress indicator for wizards and
34
+ * multi-stage flows. Renders an ordered list of steps with CSS-drawn
35
+ * connectors: completed steps show a check, the active step carries
36
+ * `aria-current="step"`, and upcoming steps show their number.
37
+ *
38
+ * Forwards a ref to the `<ol>` and spreads unrecognised props onto it.
39
+ */
40
+ export declare const Stepper: React.ForwardRefExoticComponent<StepperProps & React.RefAttributes<HTMLOListElement>>;
41
+ export {};
@@ -0,0 +1,61 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import "./Stepper.css";
5
+ import "../../fonts/material-symbols.css";
6
+ const Stepper = React.forwardRef(
7
+ ({
8
+ steps,
9
+ activeStep,
10
+ onStepClick,
11
+ orientation = "horizontal",
12
+ className = "",
13
+ ...rest
14
+ }, ref) => {
15
+ const baseClass = "ds-stepper";
16
+ const classes = [
17
+ baseClass,
18
+ orientation === "vertical" && `${baseClass}--vertical`,
19
+ className
20
+ ].filter(Boolean).join(" ");
21
+ return /* @__PURE__ */ jsx("ol", { ...rest, ref, className: classes, children: steps.map((step, index) => {
22
+ const status = index < activeStep ? "complete" : index === activeStep ? "active" : "upcoming";
23
+ const interactive = Boolean(onStepClick) && status !== "upcoming";
24
+ const indicator = /* @__PURE__ */ jsx("span", { className: `${baseClass}__indicator`, "aria-hidden": "true", children: status === "complete" ? /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", children: "check" }) : index + 1 });
25
+ const text = /* @__PURE__ */ jsxs("span", { className: `${baseClass}__text`, children: [
26
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__label`, children: step.label }),
27
+ step.description && /* @__PURE__ */ jsx("span", { className: `${baseClass}__description`, children: step.description })
28
+ ] });
29
+ return /* @__PURE__ */ jsxs(
30
+ "li",
31
+ {
32
+ className: `${baseClass}__step ${baseClass}__step--${status}`,
33
+ "aria-current": status === "active" ? "step" : void 0,
34
+ children: [
35
+ interactive ? /* @__PURE__ */ jsxs(
36
+ "button",
37
+ {
38
+ type: "button",
39
+ className: `${baseClass}__content`,
40
+ onClick: () => onStepClick?.(index),
41
+ children: [
42
+ indicator,
43
+ text
44
+ ]
45
+ }
46
+ ) : /* @__PURE__ */ jsxs("span", { className: `${baseClass}__content`, children: [
47
+ indicator,
48
+ text
49
+ ] }),
50
+ index < steps.length - 1 && /* @__PURE__ */ jsx("span", { className: `${baseClass}__connector`, "aria-hidden": "true" })
51
+ ]
52
+ },
53
+ index
54
+ );
55
+ }) });
56
+ }
57
+ );
58
+ Stepper.displayName = "Stepper";
59
+ export {
60
+ Stepper
61
+ };
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx, jsxs } from "react/jsx-runtime";
2
3
  import "react";
3
4
  import "./Tabs.css";
@@ -0,0 +1,160 @@
1
+ /* ============================================
2
+ TAG INPUT COMPONENT
3
+ Multi-value text input; committed entries
4
+ held as removable tags inside the field
5
+ ============================================ */
6
+
7
+ .ds-taginput {
8
+ display: flex;
9
+ flex-direction: column;
10
+ gap: var(--gap-sm); /* 8px */
11
+ width: 100%;
12
+ }
13
+
14
+ /* ============================================
15
+ SHELL — the focus-within box matching
16
+ Input's field border and radius
17
+ ============================================ */
18
+
19
+ .ds-taginput__shell {
20
+ display: flex;
21
+ flex-wrap: wrap;
22
+ align-items: center;
23
+ gap: var(--gap-xs); /* 4px */
24
+ padding: var(--padding-xs) var(--padding-md); /* 6px 16px */
25
+ border: var(--border-xs) solid var(--color-input-border-primary);
26
+ border-radius: var(--radius-md); /* 12px */
27
+ background-color: var(--color-input-bg-primary);
28
+ cursor: text;
29
+ transition: border-color var(--motion-duration-base) var(--motion-ease-standard);
30
+ }
31
+
32
+ .ds-taginput__shell:hover:not(:focus-within) {
33
+ border-color: var(--color-input-border-hover);
34
+ }
35
+
36
+ .ds-taginput__shell:focus-within {
37
+ border-color: var(--color-input-border-selected);
38
+ }
39
+
40
+ /* ============================================
41
+ TAG — pill holding one committed entry
42
+ ============================================ */
43
+
44
+ .ds-taginput__tag {
45
+ display: inline-flex;
46
+ align-items: center;
47
+ gap: var(--gap-xxs); /* 2px */
48
+ padding: var(--padding-xxxs) var(--padding-sm); /* 2px 8px */
49
+ border-radius: var(--radius-full);
50
+ background-color: var(--color-bg-container-secondary);
51
+ font-family: var(--font-paragraph-sm-em-family);
52
+ font-size: var(--font-paragraph-sm-em-size);
53
+ font-weight: var(--font-paragraph-sm-em-weight);
54
+ line-height: var(--font-paragraph-sm-em-line-height);
55
+ letter-spacing: var(--font-paragraph-sm-em-letter-spacing);
56
+ color: var(--color-text-primary);
57
+ max-width: 100%;
58
+ }
59
+
60
+ .ds-taginput__tag-label {
61
+ overflow: hidden;
62
+ text-overflow: ellipsis;
63
+ white-space: nowrap;
64
+ }
65
+
66
+ /* ============================================
67
+ REMOVE — trailing close button on each tag
68
+ ============================================ */
69
+
70
+ .ds-taginput__remove {
71
+ display: inline-flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ flex-shrink: 0;
75
+ margin: 0;
76
+ padding: 0;
77
+ border: none;
78
+ border-radius: var(--radius-full);
79
+ background-color: transparent;
80
+ color: var(--color-icon-primary);
81
+ cursor: pointer;
82
+ transition: color var(--motion-duration-base) var(--motion-ease-standard);
83
+ }
84
+
85
+ .ds-taginput__remove:not(:disabled):hover {
86
+ color: var(--color-text-primary);
87
+ }
88
+
89
+ .ds-taginput__remove:focus-visible {
90
+ outline: 2px solid var(--color-action-primary-bg);
91
+ outline-offset: 1px;
92
+ }
93
+
94
+ .ds-taginput__remove .material-symbols-rounded {
95
+ --icon-size: var(--icon-size-sm);
96
+ line-height: 1;
97
+ }
98
+
99
+ /* ============================================
100
+ INPUT — borderless draft field sharing the
101
+ shell's line with the tags
102
+ ============================================ */
103
+
104
+ .ds-taginput__input {
105
+ flex: 1;
106
+ min-width: 0;
107
+ border: none;
108
+ outline: none;
109
+ background-color: transparent;
110
+ padding: 0;
111
+ font-family: var(--font-paragraph-family);
112
+ font-size: var(--font-paragraph-size);
113
+ font-weight: var(--font-paragraph-weight);
114
+ line-height: var(--font-paragraph-line-height);
115
+ letter-spacing: var(--font-paragraph-letter-spacing);
116
+ color: var(--color-input-text-primary);
117
+ }
118
+
119
+ .ds-taginput__input::placeholder {
120
+ color: var(--color-input-text-placeholder);
121
+ }
122
+
123
+ /* ============================================
124
+ ERROR STATE
125
+ ============================================ */
126
+
127
+ .ds-taginput--error .ds-taginput__shell,
128
+ .ds-taginput--error .ds-taginput__shell:hover:not(:focus-within),
129
+ .ds-taginput--error .ds-taginput__shell:focus-within {
130
+ border-color: var(--color-status-error-border);
131
+ }
132
+
133
+ /* ============================================
134
+ DISABLED STATE
135
+ ============================================ */
136
+
137
+ .ds-taginput--disabled .ds-taginput__shell {
138
+ opacity: 0.4;
139
+ cursor: not-allowed;
140
+ }
141
+
142
+ .ds-taginput--disabled .ds-taginput__input,
143
+ .ds-taginput--disabled .ds-taginput__remove {
144
+ cursor: not-allowed;
145
+ }
146
+
147
+ /* ============================================
148
+ SIZE — COMPACT
149
+ ============================================ */
150
+
151
+ .ds-taginput--compact .ds-taginput__shell {
152
+ gap: var(--gap-xxs); /* 2px */
153
+ padding: var(--padding-xxs) var(--padding-sm-md); /* 4px 12px */
154
+ }
155
+
156
+ .ds-taginput--compact .ds-taginput__input {
157
+ font-size: var(--font-paragraph-sm-size);
158
+ line-height: var(--font-paragraph-sm-line-height);
159
+ letter-spacing: var(--font-paragraph-sm-letter-spacing);
160
+ }
@@ -0,0 +1,43 @@
1
+ import { default as React } from 'react';
2
+ /**
3
+ * Props owned by TagInput itself — everything else falls through to the inner
4
+ * <input>. The native `value`/`defaultValue` are excluded from the interface:
5
+ * the draft text is component-managed, and the committed entries live in
6
+ * `values`/`defaultValues` instead.
7
+ */
8
+ type TagInputOwnProps = {
9
+ /** Field label text */
10
+ label?: string;
11
+ /** Committed tags (controlled) — pair with `onValuesChange` */
12
+ values?: string[];
13
+ /** Initial tags when uncontrolled */
14
+ defaultValues?: string[];
15
+ /**
16
+ * Convenience callback receiving the full tag array whenever a tag is added
17
+ * or removed. The native `onChange` still fires for draft-text edits, which
18
+ * keeps the standard React event signature for form libraries.
19
+ */
20
+ onValuesChange?: (values: string[]) => void;
21
+ /** Maximum number of tags — at the limit the input stops accepting new ones */
22
+ maxTags?: number;
23
+ /** Component size (not the native character-width `size` attribute) */
24
+ size?: 'default' | 'compact';
25
+ /** Error state — shows error styling and message */
26
+ error?: boolean;
27
+ /** Helper or error message displayed below the control */
28
+ helperText?: string;
29
+ /** Additional CSS classes — applied to the wrapper, not the <input> */
30
+ className?: string;
31
+ };
32
+ export interface TagInputProps extends TagInputOwnProps, Omit<React.ComponentPropsWithoutRef<'input'>, keyof TagInputOwnProps | 'type' | 'value' | 'defaultValue'> {
33
+ }
34
+ /**
35
+ * Multi-value text input holding committed entries as removable tags.
36
+ *
37
+ * Enter or comma commits the trimmed draft as a tag (duplicates and empty
38
+ * drafts are ignored), Backspace on an empty draft removes the last tag, and
39
+ * each tag carries its own labelled remove button. Forwards a ref to the inner
40
+ * `<input>` and spreads unrecognised props onto it.
41
+ */
42
+ export declare const TagInput: React.ForwardRefExoticComponent<TagInputProps & React.RefAttributes<HTMLInputElement>>;
43
+ export {};
@@ -0,0 +1,137 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import React, { useState, useRef } from "react";
4
+ import { Field } from "../Field/Field.js";
5
+ import "./TagInput.css";
6
+ import "../../fonts/material-symbols.css";
7
+ const TagInput = React.forwardRef(
8
+ ({
9
+ label,
10
+ values,
11
+ defaultValues,
12
+ onValuesChange,
13
+ maxTags,
14
+ size = "default",
15
+ error = false,
16
+ helperText,
17
+ className = "",
18
+ placeholder = "",
19
+ disabled = false,
20
+ required = false,
21
+ onChange,
22
+ onKeyDown,
23
+ name,
24
+ id,
25
+ ...rest
26
+ }, ref) => {
27
+ const baseClass = "ds-taginput";
28
+ const isControlled = values !== void 0;
29
+ const [internalValues, setInternalValues] = useState(defaultValues ?? []);
30
+ const [draft, setDraft] = useState("");
31
+ const inputRef = useRef(null);
32
+ const tags = isControlled ? values : internalValues;
33
+ const atLimit = maxTags !== void 0 && tags.length >= maxTags;
34
+ const classes = [
35
+ baseClass,
36
+ size === "compact" ? `${baseClass}--compact` : "",
37
+ error ? `${baseClass}--error` : "",
38
+ disabled ? `${baseClass}--disabled` : "",
39
+ className
40
+ ].filter(Boolean).join(" ");
41
+ const setRef = (node) => {
42
+ inputRef.current = node;
43
+ if (typeof ref === "function") ref(node);
44
+ else if (ref) ref.current = node;
45
+ };
46
+ const updateValues = (next) => {
47
+ if (!isControlled) setInternalValues(next);
48
+ onValuesChange?.(next);
49
+ };
50
+ const commitDraft = () => {
51
+ const tag = draft.trim();
52
+ if (!tag || atLimit) return;
53
+ if (!tags.includes(tag)) updateValues([...tags, tag]);
54
+ setDraft("");
55
+ };
56
+ const removeTag = (index) => {
57
+ if (disabled) return;
58
+ updateValues(tags.filter((_, i) => i !== index));
59
+ };
60
+ const handleChange = (e) => {
61
+ onChange?.(e);
62
+ setDraft(e.target.value);
63
+ };
64
+ const handleKeyDown = (e) => {
65
+ onKeyDown?.(e);
66
+ if (e.defaultPrevented) return;
67
+ if (e.key === "Enter" && draft.trim() !== "") {
68
+ e.preventDefault();
69
+ commitDraft();
70
+ } else if (e.key === ",") {
71
+ e.preventDefault();
72
+ commitDraft();
73
+ } else if (e.key === "Backspace" && draft === "" && tags.length > 0) {
74
+ removeTag(tags.length - 1);
75
+ }
76
+ };
77
+ const handleShellClick = (e) => {
78
+ if (disabled) return;
79
+ if (e.target.closest(`.${baseClass}__remove`)) return;
80
+ inputRef.current?.focus();
81
+ };
82
+ const inputId = id || name || label?.toLowerCase().replace(/\s+/g, "-");
83
+ const describedBy = helperText ? `${inputId}-helper` : rest["aria-describedby"];
84
+ return /* @__PURE__ */ jsx(
85
+ Field,
86
+ {
87
+ className: classes,
88
+ label,
89
+ helperText,
90
+ error,
91
+ required,
92
+ disabled,
93
+ size,
94
+ id: inputId,
95
+ children: /* @__PURE__ */ jsxs("div", { className: `${baseClass}__shell`, onClick: handleShellClick, children: [
96
+ tags.map((tag, index) => /* @__PURE__ */ jsxs("span", { className: `${baseClass}__tag`, children: [
97
+ /* @__PURE__ */ jsx("span", { className: `${baseClass}__tag-label`, children: tag }),
98
+ /* @__PURE__ */ jsx(
99
+ "button",
100
+ {
101
+ type: "button",
102
+ className: `${baseClass}__remove`,
103
+ onClick: () => removeTag(index),
104
+ disabled,
105
+ "aria-label": `Remove ${tag}`,
106
+ children: /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", "aria-hidden": "true", children: "close" })
107
+ }
108
+ )
109
+ ] }, tag)),
110
+ /* @__PURE__ */ jsx(
111
+ "input",
112
+ {
113
+ ...rest,
114
+ ref: setRef,
115
+ className: `${baseClass}__input`,
116
+ type: "text",
117
+ id: inputId,
118
+ name,
119
+ value: draft,
120
+ placeholder: atLimit ? "" : placeholder,
121
+ disabled,
122
+ required,
123
+ "aria-invalid": error,
124
+ "aria-describedby": describedBy,
125
+ onChange: handleChange,
126
+ onKeyDown: handleKeyDown
127
+ }
128
+ )
129
+ ] })
130
+ }
131
+ );
132
+ }
133
+ );
134
+ TagInput.displayName = "TagInput";
135
+ export {
136
+ TagInput
137
+ };
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx, jsxs } from "react/jsx-runtime";
2
3
  import React from "react";
3
4
  import { Field } from "../Field/Field.js";