@robr0/design-system 0.7.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 +4 -4
- package/components/Accordion/Accordion.js +1 -0
- package/components/AgentPlan/AgentPlan.css +231 -0
- package/components/AgentPlan/AgentPlan.d.ts +41 -0
- package/components/AgentPlan/AgentPlan.js +100 -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.css +105 -0
- package/components/DataTable/DataTable.d.ts +81 -0
- package/components/DataTable/DataTable.js +236 -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.css +328 -0
- package/components/EventCalendar/EventCalendar.d.ts +51 -0
- package/components/EventCalendar/EventCalendar.js +214 -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 +240 -0
- package/components/ModelPicker/ModelPicker.d.ts +60 -0
- package/components/ModelPicker/ModelPicker.js +218 -0
- package/components/NavList/NavList.js +1 -0
- package/components/NotificationCenter/NotificationCenter.css +262 -0
- package/components/NotificationCenter/NotificationCenter.d.ts +72 -0
- package/components/NotificationCenter/NotificationCenter.js +129 -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/field.glsl.d.ts +11 -5
- package/components/ShaderField/field.glsl.js +4 -3
- package/components/ShaderField/useShaderField.d.ts +19 -2
- package/components/ShaderField/useShaderField.js +18 -3
- 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/Timeline/Timeline.css +17 -0
- package/components/Timeline/Timeline.d.ts +2 -0
- package/components/Timeline/Timeline.js +1 -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 +246 -20
- package/components/registry.json.d.ts +246 -20
- package/components/registry.json.js +2 -2
- package/index.d.ts +16 -2
- package/index.js +32 -1
- package/package.json +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
NUMBER INPUT COMPONENT
|
|
3
|
+
Numeric field with stepper buttons and clamping
|
|
4
|
+
============================================ */
|
|
5
|
+
|
|
6
|
+
/* Base */
|
|
7
|
+
|
|
8
|
+
.ds-numberinput {
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
gap: var(--gap-sm); /* 8px */
|
|
12
|
+
width: 100%;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* ============================================
|
|
16
|
+
SHELL — border, fill and focus ring live here
|
|
17
|
+
============================================ */
|
|
18
|
+
|
|
19
|
+
.ds-numberinput__shell {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: stretch;
|
|
22
|
+
width: 100%;
|
|
23
|
+
border: var(--border-xs) solid var(--color-input-border-primary);
|
|
24
|
+
border-radius: var(--radius-md); /* 12px — all inputs */
|
|
25
|
+
background-color: var(--color-input-bg-primary);
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
transition: border-color var(--motion-duration-base) var(--motion-ease-standard), background-color var(--motion-duration-base) var(--motion-ease-standard);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.ds-numberinput__shell:hover:not(:focus-within) {
|
|
31
|
+
border-color: var(--color-input-border-hover);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.ds-numberinput__shell:focus-within {
|
|
35
|
+
border-color: var(--color-input-border-selected);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* ============================================
|
|
39
|
+
FIELD
|
|
40
|
+
============================================ */
|
|
41
|
+
|
|
42
|
+
.ds-numberinput__input {
|
|
43
|
+
flex: 1;
|
|
44
|
+
min-width: 0;
|
|
45
|
+
padding: var(--padding-sm) var(--padding-xs); /* 8px 6px */
|
|
46
|
+
border: none;
|
|
47
|
+
outline: none;
|
|
48
|
+
background-color: transparent;
|
|
49
|
+
text-align: center;
|
|
50
|
+
|
|
51
|
+
/* Typography */
|
|
52
|
+
font-family: var(--font-paragraph-family);
|
|
53
|
+
font-size: var(--font-paragraph-size);
|
|
54
|
+
font-weight: var(--font-paragraph-weight);
|
|
55
|
+
line-height: var(--font-paragraph-line-height);
|
|
56
|
+
letter-spacing: var(--font-paragraph-letter-spacing);
|
|
57
|
+
color: var(--color-input-text-primary);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.ds-numberinput__input::placeholder {
|
|
61
|
+
color: var(--color-input-text-placeholder);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Hide the native spinners — the steppers replace them */
|
|
65
|
+
.ds-numberinput__input {
|
|
66
|
+
appearance: textfield;
|
|
67
|
+
-moz-appearance: textfield;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.ds-numberinput__input::-webkit-outer-spin-button,
|
|
71
|
+
.ds-numberinput__input::-webkit-inner-spin-button {
|
|
72
|
+
-webkit-appearance: none;
|
|
73
|
+
margin: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ============================================
|
|
77
|
+
STEPPERS
|
|
78
|
+
============================================ */
|
|
79
|
+
|
|
80
|
+
.ds-numberinput__stepper {
|
|
81
|
+
display: flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
justify-content: center;
|
|
84
|
+
padding: 0 var(--padding-sm-md); /* 12px */
|
|
85
|
+
border: none;
|
|
86
|
+
background-color: transparent;
|
|
87
|
+
color: var(--color-icon-primary);
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
transition: background-color var(--motion-duration-fast) var(--motion-ease-standard), color var(--motion-duration-fast) var(--motion-ease-standard);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.ds-numberinput__stepper .material-symbols-rounded {
|
|
93
|
+
--icon-size: var(--icon-size-sm);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.ds-numberinput__stepper:hover:not(:disabled) {
|
|
97
|
+
background-color: var(--color-action-passive-bg-hover);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.ds-numberinput__stepper:active:not(:disabled) {
|
|
101
|
+
background-color: var(--color-action-passive-bg-active);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.ds-numberinput__stepper:focus-visible {
|
|
105
|
+
outline: var(--border-md) solid var(--color-input-border-selected);
|
|
106
|
+
outline-offset: calc(-2 * var(--border-md));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.ds-numberinput__stepper:disabled {
|
|
110
|
+
opacity: 0.4;
|
|
111
|
+
cursor: not-allowed;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* ============================================
|
|
115
|
+
ERROR STATE
|
|
116
|
+
============================================ */
|
|
117
|
+
|
|
118
|
+
.ds-numberinput--error .ds-numberinput__shell,
|
|
119
|
+
.ds-numberinput--error .ds-numberinput__shell:focus-within {
|
|
120
|
+
border-color: var(--color-status-error-border);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ============================================
|
|
124
|
+
DISABLED STATE
|
|
125
|
+
============================================ */
|
|
126
|
+
|
|
127
|
+
.ds-numberinput--disabled .ds-numberinput__shell {
|
|
128
|
+
background-color: var(--color-input-bg-disabled);
|
|
129
|
+
border-color: var(--color-input-border-disabled);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.ds-numberinput--disabled .ds-numberinput__input {
|
|
133
|
+
color: var(--color-input-text-disabled);
|
|
134
|
+
cursor: not-allowed;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.ds-numberinput--disabled .ds-numberinput__input::placeholder {
|
|
138
|
+
color: var(--color-input-text-disabled);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* ============================================
|
|
142
|
+
SIZE — COMPACT
|
|
143
|
+
============================================ */
|
|
144
|
+
|
|
145
|
+
.ds-numberinput--compact .ds-numberinput__input {
|
|
146
|
+
padding: var(--padding-xs) var(--padding-xs); /* 6px */
|
|
147
|
+
font-size: var(--font-paragraph-sm-size);
|
|
148
|
+
line-height: var(--font-paragraph-sm-line-height);
|
|
149
|
+
letter-spacing: var(--font-paragraph-sm-letter-spacing);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.ds-numberinput--compact .ds-numberinput__stepper {
|
|
153
|
+
padding: 0 var(--padding-sm); /* 8px */
|
|
154
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** Props owned by NumberInput itself — everything else falls through to the <input>. */
|
|
3
|
+
type NumberInputOwnProps = {
|
|
4
|
+
/** Input label text */
|
|
5
|
+
label?: string;
|
|
6
|
+
/** Current value (controlled). Pass an empty string for an empty field */
|
|
7
|
+
value?: number | '';
|
|
8
|
+
/** Initial value (uncontrolled) */
|
|
9
|
+
defaultValue?: number;
|
|
10
|
+
/** Minimum allowed value — steppers stop here, and typed values clamp to it on blur */
|
|
11
|
+
min?: number;
|
|
12
|
+
/** Maximum allowed value — steppers stop here, and typed values clamp to it on blur */
|
|
13
|
+
max?: number;
|
|
14
|
+
/** Amount each stepper click (or arrow key) changes the value by */
|
|
15
|
+
step?: number;
|
|
16
|
+
/** Component size (not the native character-width `size` attribute) */
|
|
17
|
+
size?: 'default' | 'compact';
|
|
18
|
+
/** Error state — shows error styling and message */
|
|
19
|
+
error?: boolean;
|
|
20
|
+
/** Helper or error message displayed below the input */
|
|
21
|
+
helperText?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Convenience callback receiving the numeric value directly — `null` when
|
|
24
|
+
* the field is empty or unparseable. Fires on typing alongside `onChange`
|
|
25
|
+
* (which keeps the standard React event signature so form libraries work
|
|
26
|
+
* unmodified) and also on stepper clicks, where no native change event exists.
|
|
27
|
+
*/
|
|
28
|
+
onValueChange?: (value: number | null) => void;
|
|
29
|
+
/** Additional CSS classes — applied to the wrapper, not the <input> */
|
|
30
|
+
className?: string;
|
|
31
|
+
};
|
|
32
|
+
export interface NumberInputProps extends NumberInputOwnProps, Omit<React.ComponentPropsWithoutRef<'input'>, keyof NumberInputOwnProps | 'type'> {
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Numeric field with increment and decrement steppers and min/max clamping.
|
|
36
|
+
*
|
|
37
|
+
* The native spinners are hidden and replaced by stepper buttons that clamp to
|
|
38
|
+
* `min`/`max` and disable at the bounds; a typed out-of-range value clamps into
|
|
39
|
+
* range on blur. Forwards a ref to the underlying `<input>` and spreads
|
|
40
|
+
* unrecognised props onto it, so `inputMode`, `data-*` and form-library
|
|
41
|
+
* registration all work.
|
|
42
|
+
*/
|
|
43
|
+
export declare const NumberInput: React.ForwardRefExoticComponent<NumberInputProps & React.RefAttributes<HTMLInputElement>>;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { Field } from "../Field/Field.js";
|
|
5
|
+
import "./NumberInput.css";
|
|
6
|
+
import "../../fonts/material-symbols.css";
|
|
7
|
+
const parseValue = (raw) => {
|
|
8
|
+
if (raw === "" || raw === void 0) return null;
|
|
9
|
+
const parsed = typeof raw === "number" ? raw : parseFloat(raw);
|
|
10
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
11
|
+
};
|
|
12
|
+
const decimalsOf = (value) => {
|
|
13
|
+
const text = String(value);
|
|
14
|
+
const dot = text.indexOf(".");
|
|
15
|
+
return dot === -1 ? 0 : text.length - dot - 1;
|
|
16
|
+
};
|
|
17
|
+
const NumberInput = React.forwardRef(
|
|
18
|
+
({
|
|
19
|
+
label,
|
|
20
|
+
value,
|
|
21
|
+
defaultValue,
|
|
22
|
+
min,
|
|
23
|
+
max,
|
|
24
|
+
step = 1,
|
|
25
|
+
size = "default",
|
|
26
|
+
disabled = false,
|
|
27
|
+
required = false,
|
|
28
|
+
error = false,
|
|
29
|
+
helperText,
|
|
30
|
+
onChange,
|
|
31
|
+
onValueChange,
|
|
32
|
+
onBlur,
|
|
33
|
+
className = "",
|
|
34
|
+
name,
|
|
35
|
+
id,
|
|
36
|
+
...rest
|
|
37
|
+
}, ref) => {
|
|
38
|
+
const baseClass = "ds-numberinput";
|
|
39
|
+
const isControlled = value !== void 0;
|
|
40
|
+
const [internalValue, setInternalValue] = useState(
|
|
41
|
+
defaultValue !== void 0 ? String(defaultValue) : ""
|
|
42
|
+
);
|
|
43
|
+
const currentValue = isControlled ? value : internalValue;
|
|
44
|
+
const currentNumber = parseValue(currentValue);
|
|
45
|
+
const classes = [
|
|
46
|
+
baseClass,
|
|
47
|
+
size === "compact" ? `${baseClass}--compact` : "",
|
|
48
|
+
error ? `${baseClass}--error` : "",
|
|
49
|
+
disabled ? `${baseClass}--disabled` : "",
|
|
50
|
+
className
|
|
51
|
+
].filter(Boolean).join(" ");
|
|
52
|
+
const clamp = (candidate) => {
|
|
53
|
+
if (min !== void 0 && candidate < min) return min;
|
|
54
|
+
if (max !== void 0 && candidate > max) return max;
|
|
55
|
+
return candidate;
|
|
56
|
+
};
|
|
57
|
+
const commit = (next) => {
|
|
58
|
+
if (!isControlled) setInternalValue(String(next));
|
|
59
|
+
onValueChange?.(next);
|
|
60
|
+
};
|
|
61
|
+
const stepBy = (direction) => {
|
|
62
|
+
const base = currentNumber ?? 0;
|
|
63
|
+
const precision = Math.max(decimalsOf(base), decimalsOf(step));
|
|
64
|
+
commit(clamp(Number((base + direction * step).toFixed(precision))));
|
|
65
|
+
};
|
|
66
|
+
const handleChange = (e) => {
|
|
67
|
+
if (!isControlled) setInternalValue(e.target.value);
|
|
68
|
+
onChange?.(e);
|
|
69
|
+
onValueChange?.(parseValue(e.target.value));
|
|
70
|
+
};
|
|
71
|
+
const handleBlur = (e) => {
|
|
72
|
+
const typed = parseValue(e.target.value);
|
|
73
|
+
if (typed !== null) {
|
|
74
|
+
const clamped = clamp(typed);
|
|
75
|
+
if (clamped !== typed) commit(clamped);
|
|
76
|
+
}
|
|
77
|
+
onBlur?.(e);
|
|
78
|
+
};
|
|
79
|
+
const atMin = min !== void 0 && currentNumber !== null && currentNumber <= min;
|
|
80
|
+
const atMax = max !== void 0 && currentNumber !== null && currentNumber >= max;
|
|
81
|
+
const inputId = id || name || label?.toLowerCase().replace(/\s+/g, "-");
|
|
82
|
+
const describedBy = helperText ? `${inputId}-helper` : rest["aria-describedby"];
|
|
83
|
+
return /* @__PURE__ */ jsx(
|
|
84
|
+
Field,
|
|
85
|
+
{
|
|
86
|
+
className: classes,
|
|
87
|
+
label,
|
|
88
|
+
helperText,
|
|
89
|
+
error,
|
|
90
|
+
required,
|
|
91
|
+
disabled,
|
|
92
|
+
size,
|
|
93
|
+
id: inputId,
|
|
94
|
+
children: /* @__PURE__ */ jsxs("div", { className: `${baseClass}__shell`, children: [
|
|
95
|
+
/* @__PURE__ */ jsx(
|
|
96
|
+
"button",
|
|
97
|
+
{
|
|
98
|
+
type: "button",
|
|
99
|
+
className: `${baseClass}__stepper`,
|
|
100
|
+
"aria-label": "Decrease value",
|
|
101
|
+
disabled: disabled || atMin,
|
|
102
|
+
onClick: () => stepBy(-1),
|
|
103
|
+
children: /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", "aria-hidden": "true", children: "remove" })
|
|
104
|
+
}
|
|
105
|
+
),
|
|
106
|
+
/* @__PURE__ */ jsx(
|
|
107
|
+
"input",
|
|
108
|
+
{
|
|
109
|
+
...rest,
|
|
110
|
+
ref,
|
|
111
|
+
className: `${baseClass}__input`,
|
|
112
|
+
type: "number",
|
|
113
|
+
id: inputId,
|
|
114
|
+
name,
|
|
115
|
+
value: currentValue,
|
|
116
|
+
min,
|
|
117
|
+
max,
|
|
118
|
+
step,
|
|
119
|
+
disabled,
|
|
120
|
+
required,
|
|
121
|
+
"aria-label": rest["aria-label"] || label,
|
|
122
|
+
"aria-invalid": error,
|
|
123
|
+
"aria-describedby": describedBy,
|
|
124
|
+
onChange: handleChange,
|
|
125
|
+
onBlur: handleBlur
|
|
126
|
+
}
|
|
127
|
+
),
|
|
128
|
+
/* @__PURE__ */ jsx(
|
|
129
|
+
"button",
|
|
130
|
+
{
|
|
131
|
+
type: "button",
|
|
132
|
+
className: `${baseClass}__stepper`,
|
|
133
|
+
"aria-label": "Increase value",
|
|
134
|
+
disabled: disabled || atMax,
|
|
135
|
+
onClick: () => stepBy(1),
|
|
136
|
+
children: /* @__PURE__ */ jsx("span", { className: "material-symbols-rounded", "aria-hidden": "true", children: "add" })
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
] })
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
NumberInput.displayName = "NumberInput";
|
|
145
|
+
export {
|
|
146
|
+
NumberInput
|
|
147
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
PIN INPUT COMPONENT
|
|
3
|
+
Segmented one-time-code input
|
|
4
|
+
============================================ */
|
|
5
|
+
|
|
6
|
+
/* Base — the Field wrapper carries the root class */
|
|
7
|
+
.ds-pininput {
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
gap: var(--gap-sm); /* 8px */
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* ============================================
|
|
14
|
+
CELLS ROW
|
|
15
|
+
============================================ */
|
|
16
|
+
|
|
17
|
+
.ds-pininput__cells {
|
|
18
|
+
display: flex;
|
|
19
|
+
gap: var(--gap-sm); /* 8px */
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* ============================================
|
|
23
|
+
CELL
|
|
24
|
+
============================================ */
|
|
25
|
+
|
|
26
|
+
.ds-pininput__cell {
|
|
27
|
+
/* Fixed control geometry, like the ToggleSwitch track */
|
|
28
|
+
width: 44px;
|
|
29
|
+
height: 52px;
|
|
30
|
+
padding: 0;
|
|
31
|
+
text-align: center;
|
|
32
|
+
|
|
33
|
+
border: var(--border-xs) solid var(--color-input-border-primary);
|
|
34
|
+
border-radius: var(--radius-md); /* 12px */
|
|
35
|
+
background-color: var(--color-input-bg-primary);
|
|
36
|
+
|
|
37
|
+
/* Typography */
|
|
38
|
+
font-family: var(--font-paragraph-em-family);
|
|
39
|
+
font-size: var(--font-paragraph-em-size);
|
|
40
|
+
font-weight: var(--font-paragraph-em-weight);
|
|
41
|
+
line-height: var(--font-paragraph-em-line-height);
|
|
42
|
+
letter-spacing: var(--font-paragraph-em-letter-spacing);
|
|
43
|
+
color: var(--color-input-text-primary);
|
|
44
|
+
|
|
45
|
+
/* Transitions */
|
|
46
|
+
transition: border-color var(--motion-duration-base) var(--motion-ease-standard), background-color var(--motion-duration-base) var(--motion-ease-standard);
|
|
47
|
+
outline: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ============================================
|
|
51
|
+
STATES
|
|
52
|
+
============================================ */
|
|
53
|
+
|
|
54
|
+
.ds-pininput__cell:hover:not(:focus):not(:disabled) {
|
|
55
|
+
border-color: var(--color-input-border-hover);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.ds-pininput__cell:focus {
|
|
59
|
+
border-color: var(--color-input-border-selected);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.ds-pininput__cell:focus-visible {
|
|
63
|
+
outline: 2px solid var(--color-action-primary-bg);
|
|
64
|
+
outline-offset: 2px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ============================================
|
|
68
|
+
ERROR STATE
|
|
69
|
+
============================================ */
|
|
70
|
+
|
|
71
|
+
.ds-pininput--error .ds-pininput__cell {
|
|
72
|
+
border-color: var(--color-status-error-border);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.ds-pininput--error .ds-pininput__cell:focus {
|
|
76
|
+
border-color: var(--color-status-error-border);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* ============================================
|
|
80
|
+
DISABLED STATE
|
|
81
|
+
============================================ */
|
|
82
|
+
|
|
83
|
+
.ds-pininput--disabled .ds-pininput__cells {
|
|
84
|
+
opacity: 0.4;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.ds-pininput--disabled .ds-pininput__cell {
|
|
88
|
+
background-color: var(--color-input-bg-disabled);
|
|
89
|
+
border-color: var(--color-input-border-disabled);
|
|
90
|
+
color: var(--color-input-text-disabled);
|
|
91
|
+
cursor: not-allowed;
|
|
92
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
/** Props owned by PinInput itself — everything else falls through to the group container. */
|
|
3
|
+
type PinInputOwnProps = {
|
|
4
|
+
/** Number of cells, one character each */
|
|
5
|
+
length?: number;
|
|
6
|
+
/** Current code (controlled) — character at index n renders in cell n */
|
|
7
|
+
value?: string;
|
|
8
|
+
/** Initial code (uncontrolled) */
|
|
9
|
+
defaultValue?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Convenience callback receiving the joined code whenever any cell changes.
|
|
12
|
+
* There is no single native `onChange` here: the control is a group of
|
|
13
|
+
* one-character inputs, so the value callback is the primary API.
|
|
14
|
+
*/
|
|
15
|
+
onValueChange?: (value: string) => void;
|
|
16
|
+
/** Fires once when every cell is filled, with the complete code */
|
|
17
|
+
onComplete?: (value: string) => void;
|
|
18
|
+
/** Render cells as password fields, hiding the entered characters */
|
|
19
|
+
mask?: boolean;
|
|
20
|
+
/** Accepted characters — `numeric` rejects non-digits and sets a numeric keyboard */
|
|
21
|
+
format?: 'numeric' | 'alphanumeric';
|
|
22
|
+
/** Label text rendered above the cells; also names the group for screen readers */
|
|
23
|
+
label?: string;
|
|
24
|
+
/** Helper or error message displayed below the cells */
|
|
25
|
+
helperText?: string;
|
|
26
|
+
/** Error state — recolours cell borders and the helper text */
|
|
27
|
+
error?: boolean;
|
|
28
|
+
/** Whether the whole control is disabled */
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
/** Additional CSS classes — applied to the wrapper, not the cells */
|
|
31
|
+
className?: string;
|
|
32
|
+
};
|
|
33
|
+
export interface PinInputProps extends PinInputOwnProps, Omit<React.ComponentPropsWithoutRef<'div'>, keyof PinInputOwnProps> {
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Segmented one-time-code input: one cell per character, with auto-advance,
|
|
37
|
+
* Backspace retreat, arrow-key movement and paste distribution.
|
|
38
|
+
*
|
|
39
|
+
* The ref forwards to the FIRST cell's `<input>`, so `ref.current.focus()`
|
|
40
|
+
* focuses the start of the code. Unrecognised props spread onto the
|
|
41
|
+
* `role="group"` container `<div>` that wraps the cells — there is no single
|
|
42
|
+
* underlying input for them to land on — so `data-*` and `aria-*` attributes
|
|
43
|
+
* describe the group.
|
|
44
|
+
*/
|
|
45
|
+
export declare const PinInput: React.ForwardRefExoticComponent<PinInputProps & React.RefAttributes<HTMLInputElement>>;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Field } from "../Field/Field.js";
|
|
5
|
+
import "./PinInput.css";
|
|
6
|
+
const toChars = (value, length) => Array.from({ length }, (_, i) => value[i] ?? "");
|
|
7
|
+
const PinInput = React.forwardRef(
|
|
8
|
+
({
|
|
9
|
+
length = 6,
|
|
10
|
+
value,
|
|
11
|
+
defaultValue,
|
|
12
|
+
onValueChange,
|
|
13
|
+
onComplete,
|
|
14
|
+
mask = false,
|
|
15
|
+
format = "numeric",
|
|
16
|
+
label,
|
|
17
|
+
helperText,
|
|
18
|
+
error = false,
|
|
19
|
+
disabled = false,
|
|
20
|
+
className = "",
|
|
21
|
+
id,
|
|
22
|
+
...rest
|
|
23
|
+
}, ref) => {
|
|
24
|
+
const baseClass = "ds-pininput";
|
|
25
|
+
const generatedId = React.useId();
|
|
26
|
+
const controlId = id || generatedId;
|
|
27
|
+
const isControlled = value !== void 0;
|
|
28
|
+
const [internalChars, setInternalChars] = React.useState(
|
|
29
|
+
() => toChars(defaultValue ?? "", length)
|
|
30
|
+
);
|
|
31
|
+
const chars = isControlled ? toChars(value, length) : internalChars;
|
|
32
|
+
const cellRefs = React.useRef([]);
|
|
33
|
+
const setCellRef = (index) => (node) => {
|
|
34
|
+
cellRefs.current[index] = node;
|
|
35
|
+
if (index === 0) {
|
|
36
|
+
if (typeof ref === "function") ref(node);
|
|
37
|
+
else if (ref) ref.current = node;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const classes = [
|
|
41
|
+
baseClass,
|
|
42
|
+
error ? `${baseClass}--error` : "",
|
|
43
|
+
disabled ? `${baseClass}--disabled` : "",
|
|
44
|
+
className
|
|
45
|
+
].filter(Boolean).join(" ");
|
|
46
|
+
const sanitize = (raw) => raw.replace(format === "numeric" ? /[^0-9]/g : /[^a-zA-Z0-9]/g, "");
|
|
47
|
+
const focusCell = (index) => {
|
|
48
|
+
const clamped = Math.max(0, Math.min(index, length - 1));
|
|
49
|
+
const cell = cellRefs.current[clamped];
|
|
50
|
+
cell?.focus();
|
|
51
|
+
cell?.select();
|
|
52
|
+
};
|
|
53
|
+
const commit = (next) => {
|
|
54
|
+
if (!isControlled) setInternalChars(next);
|
|
55
|
+
onValueChange?.(next.join(""));
|
|
56
|
+
};
|
|
57
|
+
const joined = chars.join("");
|
|
58
|
+
const complete = joined.length === length;
|
|
59
|
+
const wasComplete = React.useRef(complete);
|
|
60
|
+
React.useEffect(() => {
|
|
61
|
+
if (complete && !wasComplete.current) onComplete?.(joined);
|
|
62
|
+
wasComplete.current = complete;
|
|
63
|
+
}, [complete, joined, onComplete]);
|
|
64
|
+
const handleCellChange = (index) => (e) => {
|
|
65
|
+
const clean = sanitize(e.target.value);
|
|
66
|
+
if (e.target.value && !clean) return;
|
|
67
|
+
const next = [...chars];
|
|
68
|
+
if (clean.length <= 1) {
|
|
69
|
+
next[index] = clean;
|
|
70
|
+
commit(next);
|
|
71
|
+
if (clean) focusCell(index + 1);
|
|
72
|
+
} else {
|
|
73
|
+
let cursor = index;
|
|
74
|
+
for (const ch of clean) {
|
|
75
|
+
if (cursor >= length) break;
|
|
76
|
+
next[cursor] = ch;
|
|
77
|
+
cursor += 1;
|
|
78
|
+
}
|
|
79
|
+
commit(next);
|
|
80
|
+
focusCell(cursor);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const handleCellKeyDown = (index) => (e) => {
|
|
84
|
+
if (e.key === "Backspace") {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
const next = [...chars];
|
|
87
|
+
if (next[index]) {
|
|
88
|
+
next[index] = "";
|
|
89
|
+
commit(next);
|
|
90
|
+
focusCell(index - 1);
|
|
91
|
+
} else if (index > 0) {
|
|
92
|
+
next[index - 1] = "";
|
|
93
|
+
commit(next);
|
|
94
|
+
focusCell(index - 1);
|
|
95
|
+
}
|
|
96
|
+
} else if (e.key === "ArrowLeft") {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
focusCell(index - 1);
|
|
99
|
+
} else if (e.key === "ArrowRight") {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
focusCell(index + 1);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const handlePaste = (e) => {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
const clean = sanitize(e.clipboardData.getData("text")).slice(0, length);
|
|
107
|
+
if (!clean) return;
|
|
108
|
+
commit(toChars(clean, length));
|
|
109
|
+
focusCell(clean.length);
|
|
110
|
+
};
|
|
111
|
+
const describedBy = helperText ? `${controlId}-helper` : rest["aria-describedby"];
|
|
112
|
+
const characterNoun = format === "numeric" ? "Digit" : "Character";
|
|
113
|
+
return /* @__PURE__ */ jsx(
|
|
114
|
+
Field,
|
|
115
|
+
{
|
|
116
|
+
className: classes,
|
|
117
|
+
label,
|
|
118
|
+
helperText,
|
|
119
|
+
error,
|
|
120
|
+
disabled,
|
|
121
|
+
id: controlId,
|
|
122
|
+
children: /* @__PURE__ */ jsx(
|
|
123
|
+
"div",
|
|
124
|
+
{
|
|
125
|
+
...rest,
|
|
126
|
+
role: "group",
|
|
127
|
+
"aria-labelledby": label ? `${controlId}-label` : void 0,
|
|
128
|
+
"aria-label": !label ? rest["aria-label"] : void 0,
|
|
129
|
+
"aria-describedby": describedBy,
|
|
130
|
+
className: `${baseClass}__cells`,
|
|
131
|
+
children: chars.map((char, index) => /* @__PURE__ */ jsx(
|
|
132
|
+
"input",
|
|
133
|
+
{
|
|
134
|
+
ref: setCellRef(index),
|
|
135
|
+
className: `${baseClass}__cell`,
|
|
136
|
+
type: mask ? "password" : "text",
|
|
137
|
+
inputMode: format === "numeric" ? "numeric" : "text",
|
|
138
|
+
autoComplete: index === 0 ? "one-time-code" : "off",
|
|
139
|
+
maxLength: 1,
|
|
140
|
+
value: char,
|
|
141
|
+
disabled,
|
|
142
|
+
id: index === 0 ? controlId : void 0,
|
|
143
|
+
"aria-label": `${characterNoun} ${index + 1} of ${length}`,
|
|
144
|
+
"aria-invalid": error || void 0,
|
|
145
|
+
onChange: handleCellChange(index),
|
|
146
|
+
onKeyDown: handleCellKeyDown(index),
|
|
147
|
+
onPaste: handlePaste,
|
|
148
|
+
onFocus: (e) => e.target.select()
|
|
149
|
+
},
|
|
150
|
+
index
|
|
151
|
+
))
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
PinInput.displayName = "PinInput";
|
|
159
|
+
export {
|
|
160
|
+
PinInput
|
|
161
|
+
};
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
padding: var(--padding-md); /* 16px */
|
|
29
29
|
border: var(--border-xs) solid var(--color-bg-container-border);
|
|
30
30
|
border-radius: var(--radius-sm); /* 8px */
|
|
31
|
-
background
|
|
31
|
+
/* Page background, not container: the container fill is semi-transparent
|
|
32
|
+
in dark mode, and a floating panel must not show text through itself */
|
|
33
|
+
background-color: var(--color-bg-page-primary);
|
|
32
34
|
box-shadow: var(--shadow-floating);
|
|
33
35
|
|
|
34
36
|
/* Hidden by default */
|
|
@@ -88,12 +90,9 @@
|
|
|
88
90
|
SIZE — COMPACT
|
|
89
91
|
============================================ */
|
|
90
92
|
|
|
93
|
+
/* The size class rides on the panel element itself, so this is a plain
|
|
94
|
+
compound selector — a descendant form would never match */
|
|
91
95
|
.ds-popover--compact {
|
|
92
96
|
padding: var(--padding-sm); /* 8px */
|
|
93
97
|
min-width: 160px;
|
|
94
98
|
}
|
|
95
|
-
|
|
96
|
-
.ds-popover--compact .ds-popover__panel {
|
|
97
|
-
padding: var(--padding-sm);
|
|
98
|
-
min-width: 160px;
|
|
99
|
-
}
|