@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.
- package/README.md +3 -3
- package/components/Accordion/Accordion.js +1 -0
- package/components/AgentPlan/AgentPlan.css +2 -2
- package/components/AgentPlan/AgentPlan.js +1 -0
- package/components/AgentStatus/AgentStatus.js +1 -0
- package/components/AiButton/AiButton.js +1 -0
- package/components/AlertDialog/AlertDialog.js +1 -0
- package/components/AnchorNav/AnchorNav.css +63 -0
- package/components/AnchorNav/AnchorNav.d.ts +33 -0
- package/components/AnchorNav/AnchorNav.js +82 -0
- package/components/AppLayout/AppLayout.js +1 -0
- package/components/AppSidebar/AppSidebar.js +1 -0
- package/components/Avatar/Avatar.js +1 -0
- package/components/Button/Button.js +1 -0
- package/components/Card/Card.js +1 -0
- package/components/Carousel/Carousel.js +1 -0
- package/components/ChatThread/ChatThread.js +1 -0
- package/components/Checkbox/Checkbox.js +1 -0
- package/components/CodeBlock/CodeBlock.js +1 -0
- package/components/CodeDiff/CodeDiff.css +175 -0
- package/components/CodeDiff/CodeDiff.d.ts +58 -0
- package/components/CodeDiff/CodeDiff.js +114 -0
- package/components/ColorPicker/ColorPicker.js +1 -0
- package/components/Combobox/Combobox.js +1 -0
- package/components/CommandPalette/CommandPalette.js +1 -0
- package/components/Composer/Composer.js +1 -0
- package/components/ContactCard/ContactCard.js +1 -0
- package/components/ContextMenu/ContextMenu.js +1 -0
- package/components/DataTable/DataTable.js +1 -0
- package/components/DateInput/DateInput.js +1 -0
- package/components/DatePicker/DatePicker.js +1 -0
- package/components/Dialog/Dialog.js +1 -0
- package/components/Drawer/Drawer.js +1 -0
- package/components/Dropdown/Dropdown.js +1 -0
- package/components/DropdownMenu/DropdownMenu.css +1 -1
- package/components/DropdownMenu/DropdownMenu.js +1 -0
- package/components/EventCalendar/EventCalendar.js +1 -0
- package/components/Field/Field.js +1 -0
- package/components/Field/FieldContext.js +1 -0
- package/components/FileInput/FileInput.js +1 -0
- package/components/Input/Input.js +1 -0
- package/components/InterruptCard/InterruptCard.css +11 -21
- package/components/InterruptCard/InterruptCard.d.ts +5 -6
- package/components/MessageCard/MessageCard.css +1 -2
- package/components/ModelPicker/ModelPicker.css +3 -1
- package/components/ModelPicker/ModelPicker.js +1 -0
- package/components/NavList/NavList.js +1 -0
- package/components/NotificationCenter/NotificationCenter.js +1 -0
- package/components/NumberInput/NumberInput.css +154 -0
- package/components/NumberInput/NumberInput.d.ts +44 -0
- package/components/NumberInput/NumberInput.js +147 -0
- package/components/Pagination/Pagination.js +1 -0
- package/components/PinInput/PinInput.css +92 -0
- package/components/PinInput/PinInput.d.ts +46 -0
- package/components/PinInput/PinInput.js +161 -0
- package/components/Popover/Popover.css +5 -6
- package/components/Popover/Popover.js +1 -0
- package/components/RadioButton/RadioButton.js +1 -0
- package/components/Reasoning/Reasoning.js +1 -0
- package/components/SegmentedControl/SegmentedControl.js +1 -0
- package/components/SelectionCard/SelectionCard.js +1 -0
- package/components/ShaderField/ShaderField.js +1 -0
- package/components/ShaderField/useShaderField.js +1 -0
- package/components/Slider/Slider.js +1 -0
- package/components/Sparkline/Sparkline.css +52 -0
- package/components/Sparkline/Sparkline.d.ts +49 -0
- package/components/Sparkline/Sparkline.js +86 -0
- package/components/Stepper/Stepper.css +207 -0
- package/components/Stepper/Stepper.d.ts +41 -0
- package/components/Stepper/Stepper.js +61 -0
- package/components/Tabs/Tabs.js +1 -0
- package/components/TagInput/TagInput.css +160 -0
- package/components/TagInput/TagInput.d.ts +43 -0
- package/components/TagInput/TagInput.js +137 -0
- package/components/Textarea/Textarea.js +1 -0
- package/components/TimePicker/TimePicker.css +230 -0
- package/components/TimePicker/TimePicker.d.ts +46 -0
- package/components/TimePicker/TimePicker.js +246 -0
- package/components/Toast/Toast.js +1 -0
- package/components/ToggleGroup/ToggleGroup.js +1 -0
- package/components/ToggleSwitch/ToggleSwitch.js +1 -0
- package/components/ToolCall/ToolCall.css +7 -11
- package/components/ToolCall/ToolCall.js +1 -0
- package/components/Tooltip/Tooltip.js +1 -0
- package/components/TreeView/TreeView.css +119 -0
- package/components/TreeView/TreeView.d.ts +45 -0
- package/components/TreeView/TreeView.js +232 -0
- package/components/registry.d.ts +22 -6
- package/components/registry.js +3 -1
- package/components/registry.json +206 -20
- package/components/registry.json.d.ts +206 -20
- package/components/registry.json.js +2 -2
- package/index.d.ts +11 -2
- package/index.js +21 -1
- 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
|
+
};
|
package/components/Tabs/Tabs.js
CHANGED
|
@@ -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
|
+
};
|