@attrus-ds/core 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +80 -8
  2. package/dist/css/components/buttons.css +1 -1
  3. package/dist/css/components/combobox.css +17 -1
  4. package/dist/css/components/inputs.css +3 -55
  5. package/dist/css/components/overlays.css +28 -59
  6. package/dist/css/components/primitives.css +10 -1
  7. package/dist/css/components/sidebar.css +3 -3
  8. package/dist/css/components/smart-search.css +91 -0
  9. package/dist/css/components/table.css +10 -8
  10. package/dist/css/components/tabs.css +2 -2
  11. package/dist/css/components/toast.css +4 -0
  12. package/dist/css/components/tooltip.css +5 -0
  13. package/dist/css/index.css +1 -0
  14. package/dist/css/tokens.css +100 -96
  15. package/dist/react/AppSidebar/AppSidebar.d.ts +50 -0
  16. package/dist/react/AppSidebar/AppSidebar.tsx +197 -0
  17. package/dist/react/AppTopbar/AppTopbar.d.ts +58 -0
  18. package/dist/react/AppTopbar/AppTopbar.tsx +203 -0
  19. package/dist/react/Atoms/Atoms.d.ts +149 -0
  20. package/dist/react/Atoms/Atoms.tsx +407 -0
  21. package/dist/react/Button/Button.d.ts +25 -0
  22. package/dist/react/Button/Button.tsx +83 -0
  23. package/dist/react/Callout/Callout.d.ts +29 -0
  24. package/dist/react/Callout/Callout.tsx +98 -0
  25. package/dist/react/Card/Card.d.ts +38 -0
  26. package/dist/react/Card/Card.tsx +93 -0
  27. package/dist/react/Combobox/Combobox.d.ts +52 -0
  28. package/dist/react/Combobox/Combobox.tsx +254 -0
  29. package/dist/react/DataTable/DataTable.d.ts +81 -0
  30. package/dist/react/DataTable/DataTable.tsx +308 -0
  31. package/dist/react/Drawer/Drawer.d.ts +35 -0
  32. package/dist/react/Drawer/Drawer.tsx +226 -0
  33. package/dist/react/Input/Input.d.ts +155 -0
  34. package/dist/react/Input/Input.tsx +478 -0
  35. package/dist/react/Modal/Modal.d.ts +27 -0
  36. package/dist/react/Modal/Modal.tsx +173 -0
  37. package/dist/react/NumberInput/NumberInput.d.ts +25 -0
  38. package/dist/react/NumberInput/NumberInput.tsx +117 -0
  39. package/dist/react/Pill/Pill.d.ts +31 -0
  40. package/dist/react/Pill/Pill.tsx +104 -0
  41. package/dist/react/SearchPill/SearchPill.d.ts +14 -0
  42. package/dist/react/SearchPill/SearchPill.tsx +40 -0
  43. package/dist/react/SmartSearch/SmartSearch.d.ts +65 -0
  44. package/dist/react/SmartSearch/SmartSearch.tsx +271 -0
  45. package/dist/react/Stepper/Stepper.d.ts +25 -0
  46. package/dist/react/Stepper/Stepper.tsx +94 -0
  47. package/dist/react/Tabs/Tabs.d.ts +32 -0
  48. package/dist/react/Tabs/Tabs.tsx +147 -0
  49. package/dist/react/Toast/Toast.d.ts +30 -0
  50. package/dist/react/Toast/Toast.tsx +94 -0
  51. package/dist/react/Tooltip/Tooltip.d.ts +23 -0
  52. package/dist/react/Tooltip/Tooltip.tsx +80 -0
  53. package/dist/react/ZeroState/ZeroState.d.ts +17 -0
  54. package/dist/react/ZeroState/ZeroState.tsx +45 -0
  55. package/dist/react/index.ts +35 -0
  56. package/dist/tokens/attrus-tokens.flat.json +1 -1
  57. package/dist/tokens/attrus-tokens.json +1 -1
  58. package/package.json +20 -2
@@ -0,0 +1,155 @@
1
+ import * as React from 'react';
2
+
3
+ export type InputSize = 'sm' | 'md' | 'lg';
4
+ export type InputStatus = 'default' | 'error' | 'success' | 'warning';
5
+
6
+ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix'> {
7
+ /** Field label (renders the .field wrapper). */
8
+ label?: React.ReactNode;
9
+ /** Helper line under the control; takes the status tone. */
10
+ help?: React.ReactNode;
11
+ /** Validation state: error/success border + focus ring + help tone. */
12
+ status?: InputStatus;
13
+ /** sm=32px, md=40px (default), lg=48px. */
14
+ size?: InputSize;
15
+ /** Leading icon (decorative) — .affix.lead-pad. */
16
+ leadIcon?: React.ReactNode;
17
+ /** Trailing icon (decorative) — .affix.trail-pad. */
18
+ trailIcon?: React.ReactNode;
19
+ /** Makes the trailing slot a clickable .trail-btn (clear, reveal…). */
20
+ onTrailClick?: React.MouseEventHandler<HTMLButtonElement>;
21
+ /** Trailing button accessible label (required with onTrailClick). */
22
+ trailLabel?: string;
23
+ /** Attached prefix cell — .group .fix.l (e.g. "USD"). */
24
+ prefix?: React.ReactNode;
25
+ /** Attached suffix cell — .group .fix.r (e.g. ".00"). */
26
+ suffix?: React.ReactNode;
27
+ /** Dark-surface treatment (.on-inverse). */
28
+ onInverse?: boolean;
29
+ /** Quiet read-only: value only, no border (.is-readonly). */
30
+ readOnlyQuiet?: boolean;
31
+ /** Quiet / ghost input — bottom hairline only (`.input.underline`). */
32
+ underline?: boolean;
33
+ }
34
+
35
+ export declare const Input: React.FC<InputProps>;
36
+
37
+ export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
38
+ label?: React.ReactNode;
39
+ help?: React.ReactNode;
40
+ status?: InputStatus;
41
+ /** sm / md (default) / lg — maps to textarea.input.input-{size}. */
42
+ inputSize?: InputSize;
43
+ }
44
+ export declare const Textarea: React.FC<TextareaProps>;
45
+
46
+ export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
47
+ label?: React.ReactNode;
48
+ help?: React.ReactNode;
49
+ status?: InputStatus;
50
+ inputSize?: InputSize;
51
+ }
52
+ export declare const Select: React.FC<SelectProps>;
53
+
54
+ export interface SwitchProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
55
+ children?: React.ReactNode;
56
+ }
57
+ export declare const Switch: React.FC<SwitchProps>;
58
+
59
+ export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
60
+ children?: React.ReactNode;
61
+ /** Mixed state — sets input.indeterminate. */
62
+ indeterminate?: boolean;
63
+ /** Error ring on the box (`.check.is-error`). */
64
+ error?: boolean;
65
+ }
66
+ export declare const Checkbox: React.FC<CheckboxProps>;
67
+
68
+ export interface RadioProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
69
+ children?: React.ReactNode;
70
+ }
71
+ export declare const Radio: React.FC<RadioProps>;
72
+
73
+ /* ---------- Specialised inputs (inputs.html §8–13) ---------- */
74
+
75
+ export interface DateTriggerProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'value'> {
76
+ /** Filled label (e.g. "April 17, 2026" or a range). Empty → placeholder. */
77
+ value?: React.ReactNode;
78
+ placeholder?: React.ReactNode;
79
+ /** Leading icon (calendar / calendar-range / clock). */
80
+ icon?: React.ReactNode;
81
+ /** Error ring (`.is-error`). */
82
+ error?: boolean;
83
+ }
84
+ export declare const DateTrigger: React.FC<DateTriggerProps>;
85
+
86
+ export interface ChipsInputProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
87
+ /** Current chips (controlled). */
88
+ values: string[];
89
+ /** Enter/comma in the embedded input adds a chip. */
90
+ onAdd?: (value: string) => void;
91
+ /** × on a chip (or Backspace on empty input) removes. */
92
+ onRemove?: (value: string) => void;
93
+ placeholder?: string;
94
+ disabled?: boolean;
95
+ }
96
+ export declare const ChipsInput: React.FC<ChipsInputProps>;
97
+
98
+ export interface OtpProps {
99
+ /** Number of digit cells. Default 6. */
100
+ length?: number;
101
+ /** Current code (controlled). */
102
+ value: string;
103
+ onChange?: (code: string) => void;
104
+ /** Error ring on every cell. */
105
+ error?: boolean;
106
+ disabled?: boolean;
107
+ 'aria-label'?: string;
108
+ className?: string;
109
+ style?: React.CSSProperties;
110
+ }
111
+ export declare const Otp: React.FC<OtpProps>;
112
+
113
+ export interface FileDropProps extends React.HTMLAttributes<HTMLDivElement> {
114
+ onFiles?: (files: File[]) => void;
115
+ multiple?: boolean;
116
+ accept?: string;
117
+ title?: React.ReactNode;
118
+ /** Sub line — `.anchor` span = click-to-browse affordance. */
119
+ sub?: React.ReactNode;
120
+ icon?: React.ReactNode;
121
+ disabled?: boolean;
122
+ }
123
+ export declare const FileDrop: React.FC<FileDropProps>;
124
+
125
+ export type FileRowStatus = 'ok' | 'pending' | 'err';
126
+ export interface FileRowProps extends React.HTMLAttributes<HTMLLIElement> {
127
+ /** Icon tile tint via `data-kind` (img / sheet / doc / pdf…). */
128
+ kind?: string;
129
+ icon?: React.ReactNode;
130
+ /** Corner tag text on the tile (e.g. "PDF"). */
131
+ tag?: React.ReactNode;
132
+ name: React.ReactNode;
133
+ /** Mono sub line (size · date); err status tints it. */
134
+ sub?: React.ReactNode;
135
+ /** ok · pending · err. */
136
+ status?: FileRowStatus;
137
+ statusLabel?: React.ReactNode;
138
+ /** 0–100 — uploading hairline. */
139
+ progress?: number;
140
+ onRemove?: () => void;
141
+ }
142
+ export declare const FileRow: React.FC<FileRowProps>;
143
+
144
+ export interface FileListBoxProps extends React.HTMLAttributes<HTMLDivElement> {
145
+ head?: React.ReactNode;
146
+ /** Head-right action (e.g. "Clear all"). */
147
+ action?: React.ReactNode;
148
+ onActionClick?: () => void;
149
+ }
150
+ export declare const FileListBox: React.FC<FileListBoxProps>;
151
+
152
+ export interface SliderProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'size'> {}
153
+ export declare const Slider: React.FC<SliderProps>;
154
+
155
+ export default Input;
@@ -0,0 +1,478 @@
1
+ import * as React from 'react';
2
+
3
+ /**
4
+ * ATTRUS Input — typed wrapper over the canonical input system
5
+ * (preview/components/inputs.css). CSS stays the visual source of truth.
6
+ *
7
+ * Composes the canonical anatomy automatically:
8
+ * .field > label + control + .help (when label/help given)
9
+ * .affix.lead-pad/.trail-pad (when leadIcon/trailIcon given)
10
+ * .group > .fix.l + input + .fix.r (when prefix/suffix given)
11
+ *
12
+ * Note: prefix/suffix (.group) and leadIcon/trailIcon (.affix) are
13
+ * mutually exclusive — group wins if both are passed. Sizes apply to
14
+ * the plain/affix input; the group row is md-only by design.
15
+ */
16
+
17
+ export type InputSize = 'sm' | 'md' | 'lg';
18
+ export type InputStatus = 'default' | 'error' | 'success' | 'warning';
19
+
20
+ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'prefix'> {
21
+ /** Field label (renders the .field wrapper). */
22
+ label?: React.ReactNode;
23
+ /** Helper line under the control; takes the status tone. */
24
+ help?: React.ReactNode;
25
+ /** Validation state: error/success border + focus ring + help tone. */
26
+ status?: InputStatus;
27
+ /** sm=32px, md=40px (default), lg=48px. */
28
+ size?: InputSize;
29
+ /** Leading icon (decorative) — .affix.lead-pad. */
30
+ leadIcon?: React.ReactNode;
31
+ /** Trailing icon (decorative) — .affix.trail-pad. */
32
+ trailIcon?: React.ReactNode;
33
+ /** Makes the trailing slot a clickable .trail-btn (clear, reveal…). */
34
+ onTrailClick?: React.MouseEventHandler<HTMLButtonElement>;
35
+ /** Trailing button accessible label (required with onTrailClick). */
36
+ trailLabel?: string;
37
+ /** Attached prefix cell — .group .fix.l (e.g. "USD"). */
38
+ prefix?: React.ReactNode;
39
+ /** Attached suffix cell — .group .fix.r (e.g. ".00"). */
40
+ suffix?: React.ReactNode;
41
+ /** Dark-surface treatment (.on-inverse). */
42
+ onInverse?: boolean;
43
+ /** Quiet read-only: value only, no border (.is-readonly). */
44
+ readOnlyQuiet?: boolean;
45
+ /** Quiet / ghost input — bottom hairline only (`.input.underline`). */
46
+ underline?: boolean;
47
+ }
48
+
49
+ export const Input: React.FC<InputProps> = ({
50
+ label,
51
+ help,
52
+ status = 'default',
53
+ size = 'md',
54
+ leadIcon,
55
+ trailIcon,
56
+ onTrailClick,
57
+ trailLabel,
58
+ prefix,
59
+ suffix,
60
+ onInverse = false,
61
+ readOnlyQuiet = false,
62
+ underline = false,
63
+ className,
64
+ id,
65
+ ...rest
66
+ }) => {
67
+ const autoId = React.useId();
68
+ const inputId = id || (label ? autoId : undefined);
69
+
70
+ const inputCls = [
71
+ 'input',
72
+ size !== 'md' ? `input-${size}` : '',
73
+ status === 'error' ? 'is-error' : '',
74
+ status === 'success' ? 'is-success' : '',
75
+ onInverse ? 'on-inverse' : '',
76
+ readOnlyQuiet ? 'is-readonly' : '',
77
+ underline ? 'underline' : '',
78
+ className || '',
79
+ ]
80
+ .filter(Boolean)
81
+ .join(' ');
82
+
83
+ let control: React.ReactNode;
84
+
85
+ if (prefix != null || suffix != null) {
86
+ // Attached group — the inner input is styled by `.group > input`.
87
+ control = (
88
+ <div className={['group', status === 'error' ? 'is-error' : ''].filter(Boolean).join(' ')}>
89
+ {prefix != null ? <span className="fix l">{prefix}</span> : null}
90
+ <input id={inputId} readOnly={readOnlyQuiet || rest.readOnly} {...rest} />
91
+ {suffix != null ? <span className="fix r">{suffix}</span> : null}
92
+ </div>
93
+ );
94
+ } else if (leadIcon != null || trailIcon != null || onTrailClick) {
95
+ const affixCls = [
96
+ 'affix',
97
+ leadIcon != null ? 'lead-pad' : '',
98
+ trailIcon != null || onTrailClick ? 'trail-pad' : '',
99
+ ]
100
+ .filter(Boolean)
101
+ .join(' ');
102
+ control = (
103
+ <div className={affixCls}>
104
+ {leadIcon != null ? <span className="lead">{leadIcon}</span> : null}
105
+ <input id={inputId} className={inputCls} readOnly={readOnlyQuiet || rest.readOnly} {...rest} />
106
+ {onTrailClick ? (
107
+ <button type="button" className="trail-btn" aria-label={trailLabel || 'Action'} onClick={onTrailClick}>
108
+ {trailIcon}
109
+ </button>
110
+ ) : trailIcon != null ? (
111
+ <span className="trail">{trailIcon}</span>
112
+ ) : null}
113
+ </div>
114
+ );
115
+ } else {
116
+ control = <input id={inputId} className={inputCls} readOnly={readOnlyQuiet || rest.readOnly} {...rest} />;
117
+ }
118
+
119
+ if (label == null && help == null) return <React.Fragment>{control}</React.Fragment>;
120
+
121
+ const helpCls = [
122
+ 'help',
123
+ status === 'error' ? 'is-error' : '',
124
+ status === 'success' ? 'is-success' : '',
125
+ status === 'warning' ? 'is-warning' : '',
126
+ ]
127
+ .filter(Boolean)
128
+ .join(' ');
129
+
130
+ return (
131
+ <div className="field">
132
+ {label != null ? <label htmlFor={inputId}>{label}</label> : null}
133
+ {control}
134
+ {help != null ? <div className={helpCls}>{help}</div> : null}
135
+ </div>
136
+ );
137
+ };
138
+
139
+ export default Input;
140
+
141
+ /* ----------------------------------------------------------------
142
+ Companion form controls — same inputs.css, same .field anatomy.
143
+ ---------------------------------------------------------------- */
144
+
145
+ const fieldWrap = (
146
+ label: React.ReactNode,
147
+ help: React.ReactNode,
148
+ status: InputStatus,
149
+ id: string | undefined,
150
+ control: React.ReactNode,
151
+ ) => {
152
+ if (label == null && help == null) return <React.Fragment>{control}</React.Fragment>;
153
+ const helpCls = ['help', status === 'error' ? 'is-error' : '', status === 'success' ? 'is-success' : '', status === 'warning' ? 'is-warning' : ''].filter(Boolean).join(' ');
154
+ return (
155
+ <div className="field">
156
+ {label != null ? <label htmlFor={id}>{label}</label> : null}
157
+ {control}
158
+ {help != null ? <div className={helpCls}>{help}</div> : null}
159
+ </div>
160
+ );
161
+ };
162
+
163
+ export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
164
+ label?: React.ReactNode;
165
+ help?: React.ReactNode;
166
+ status?: InputStatus;
167
+ /** sm / md (default) / lg — maps to textarea.input.input-{size}. */
168
+ inputSize?: InputSize;
169
+ }
170
+ /** Multi-line text — `textarea.input` (auto height, min-height per size). */
171
+ export const Textarea: React.FC<TextareaProps> = ({ label, help, status = 'default', inputSize = 'md', className, id, ...rest }) => {
172
+ const autoId = React.useId();
173
+ const tid = id || (label ? autoId : undefined);
174
+ const cls = ['input', inputSize !== 'md' ? `input-${inputSize}` : '', status === 'error' ? 'is-error' : '', status === 'success' ? 'is-success' : '', className || ''].filter(Boolean).join(' ');
175
+ return fieldWrap(label, help, status, tid, <textarea id={tid} className={cls} {...rest} />);
176
+ };
177
+
178
+ export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
179
+ label?: React.ReactNode;
180
+ help?: React.ReactNode;
181
+ status?: InputStatus;
182
+ inputSize?: InputSize;
183
+ }
184
+ /** Native select — `.select` wrapper + chevron glyph. Children = <option>s. */
185
+ export const Select: React.FC<SelectProps> = ({ label, help, status = 'default', inputSize = 'md', className, id, children, ...rest }) => {
186
+ const autoId = React.useId();
187
+ const sid = id || (label ? autoId : undefined);
188
+ const cls = ['input', inputSize !== 'md' ? `input-${inputSize}` : '', status === 'error' ? 'is-error' : '', className || ''].filter(Boolean).join(' ');
189
+ const control = (
190
+ <div className="select">
191
+ <select id={sid} className={cls} {...rest}>{children}</select>
192
+ <span className="chev" aria-hidden="true">
193
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
194
+ </span>
195
+ </div>
196
+ );
197
+ return fieldWrap(label, help, status, sid, control);
198
+ };
199
+
200
+ export interface SwitchProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
201
+ /** Visible label to the right of the track. */
202
+ children?: React.ReactNode;
203
+ }
204
+ /** Toggle — `.switch` (hidden input + track + thumb; royal when checked). */
205
+ export const Switch: React.FC<SwitchProps> = ({ children, className, ...rest }) => (
206
+ <label className={['switch', className || ''].filter(Boolean).join(' ')}>
207
+ <input type="checkbox" {...rest} />
208
+ <span className="switch-track"><span className="switch-thumb" /></span>
209
+ {children != null ? <span className="switch-label">{children}</span> : null}
210
+ </label>
211
+ );
212
+
213
+ export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
214
+ children?: React.ReactNode;
215
+ /** Mixed state — sets input.indeterminate (bar instead of check). */
216
+ indeterminate?: boolean;
217
+ /** Error ring on the box (`.check.is-error`). */
218
+ error?: boolean;
219
+ }
220
+ /** Custom checkbox — `.check` (hidden input + box + check glyph). */
221
+ export const Checkbox: React.FC<CheckboxProps> = ({ children, indeterminate = false, error = false, className, ...rest }) => {
222
+ const ref = React.useRef<HTMLInputElement>(null);
223
+ React.useEffect(() => { if (ref.current) ref.current.indeterminate = indeterminate; }, [indeterminate]);
224
+ return (
225
+ <label className={['check', error ? 'is-error' : '', className || ''].filter(Boolean).join(' ')}>
226
+ <input ref={ref} type="checkbox" {...rest} />
227
+ <span className="check-box">
228
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
229
+ </span>
230
+ {children != null ? <span>{children}</span> : null}
231
+ </label>
232
+ );
233
+ };
234
+
235
+ export interface RadioProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
236
+ children?: React.ReactNode;
237
+ }
238
+ /** Custom radio — `.radio` (hidden input + dot). Group via shared `name`. */
239
+ export const Radio: React.FC<RadioProps> = ({ children, className, ...rest }) => (
240
+ <label className={['radio', className || ''].filter(Boolean).join(' ')}>
241
+ <input type="radio" {...rest} />
242
+ <span className="radio-dot" />
243
+ {children != null ? <span>{children}</span> : null}
244
+ </label>
245
+ );
246
+
247
+ /* ---------- Specialised inputs (inputs.html §8–13) ---------- */
248
+
249
+ const ChevronGlyph: React.FC = () => (
250
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
251
+ <polyline points="6 9 12 15 18 9" />
252
+ </svg>
253
+ );
254
+
255
+ export interface DateTriggerProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'value'> {
256
+ /** Filled label (e.g. "April 17, 2026" or "Apr 1 – Apr 30"). Empty → placeholder. */
257
+ value?: React.ReactNode;
258
+ placeholder?: React.ReactNode;
259
+ /** Leading icon (calendar / calendar-range / clock) — caller's <Icon/>. */
260
+ icon?: React.ReactNode;
261
+ /** Error ring (`.is-error`). */
262
+ error?: boolean;
263
+ }
264
+ /** Trigger button for calendar/time pickers — `.date-trigger`. The picker itself is not part of the input system. */
265
+ export const DateTrigger: React.FC<DateTriggerProps> = ({ value, placeholder = 'Select a date…', icon, error = false, className, ...rest }) => (
266
+ <button type="button" className={['date-trigger', error ? 'is-error' : '', className || ''].filter(Boolean).join(' ')} {...rest}>
267
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 'var(--space-2)' }}>
268
+ {icon}
269
+ {value != null ? value : <span className="placeholder">{placeholder}</span>}
270
+ </span>
271
+ <ChevronGlyph />
272
+ </button>
273
+ );
274
+
275
+ export interface ChipsInputProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
276
+ /** Current chips (controlled). */
277
+ values: string[];
278
+ /** Enter/comma in the embedded input adds a chip. */
279
+ onAdd?: (value: string) => void;
280
+ /** × on a chip removes it (also Backspace on empty input removes the last). */
281
+ onRemove?: (value: string) => void;
282
+ placeholder?: string;
283
+ disabled?: boolean;
284
+ }
285
+ /** Token input — `.chips` shell with removable `.chip`s + embedded text input. */
286
+ export const ChipsInput: React.FC<ChipsInputProps> = ({ values, onAdd, onRemove, placeholder = 'Add…', disabled = false, className, ...rest }) => {
287
+ const [draft, setDraft] = React.useState('');
288
+ const commit = () => {
289
+ const v = draft.trim();
290
+ if (v && onAdd) onAdd(v);
291
+ setDraft('');
292
+ };
293
+ return (
294
+ <div className={['chips', className || ''].filter(Boolean).join(' ')} {...rest}>
295
+ {values.map((v) => (
296
+ <span key={v} className="chip">
297
+ {v}
298
+ {onRemove ? (
299
+ <button type="button" className="chip-x" aria-label={`Remove ${v}`} disabled={disabled} onClick={() => onRemove(v)}>
300
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.25} strokeLinecap="round" aria-hidden="true">
301
+ <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
302
+ </svg>
303
+ </button>
304
+ ) : null}
305
+ </span>
306
+ ))}
307
+ <input
308
+ value={draft}
309
+ placeholder={placeholder}
310
+ disabled={disabled}
311
+ onChange={(e) => setDraft(e.target.value)}
312
+ onKeyDown={(e) => {
313
+ if (e.key === 'Enter' || e.key === ',') { e.preventDefault(); commit(); }
314
+ if (e.key === 'Backspace' && !draft && values.length && onRemove) onRemove(values[values.length - 1]);
315
+ }}
316
+ onBlur={commit}
317
+ />
318
+ </div>
319
+ );
320
+ };
321
+
322
+ export interface OtpProps {
323
+ /** Number of digit cells. Default 6. */
324
+ length?: number;
325
+ /** Current code (controlled) — up to `length` chars. */
326
+ value: string;
327
+ onChange?: (code: string) => void;
328
+ /** Error ring on every cell (`.is-error`). */
329
+ error?: boolean;
330
+ disabled?: boolean;
331
+ 'aria-label'?: string;
332
+ className?: string;
333
+ style?: React.CSSProperties;
334
+ }
335
+ /** One-time-password cells — `.otp`. Auto-advances focus; paste fills all cells. */
336
+ export const Otp: React.FC<OtpProps> = ({ length = 6, value, onChange, error = false, disabled = false, className, style, ...rest }) => {
337
+ const refs = React.useRef<Array<HTMLInputElement | null>>([]);
338
+ const set = (code: string) => { if (onChange) onChange(code.slice(0, length)); };
339
+ return (
340
+ <div className={['otp', className || ''].filter(Boolean).join(' ')} style={style} role="group" aria-label={rest['aria-label'] || 'Verification code'}>
341
+ {Array.from({ length }, (_, i) => (
342
+ <input
343
+ key={i}
344
+ ref={(el) => { refs.current[i] = el; }}
345
+ maxLength={1}
346
+ inputMode="numeric"
347
+ autoComplete={i === 0 ? 'one-time-code' : 'off'}
348
+ className={error ? 'is-error' : undefined}
349
+ value={value[i] || ''}
350
+ disabled={disabled}
351
+ onChange={(e) => {
352
+ const ch = e.target.value.replace(/\D/g, '').slice(-1);
353
+ const next = value.slice(0, i) + (ch || '') + value.slice(i + 1);
354
+ set(next);
355
+ if (ch && i < length - 1) refs.current[i + 1]?.focus();
356
+ }}
357
+ onKeyDown={(e) => {
358
+ if (e.key === 'Backspace' && !value[i] && i > 0) refs.current[i - 1]?.focus();
359
+ }}
360
+ onPaste={(e) => {
361
+ const digits = e.clipboardData.getData('text').replace(/\D/g, '');
362
+ if (digits) { e.preventDefault(); set(digits); refs.current[Math.min(digits.length, length) - 1]?.focus(); }
363
+ }}
364
+ />
365
+ ))}
366
+ </div>
367
+ );
368
+ };
369
+
370
+ export interface FileDropProps extends React.HTMLAttributes<HTMLDivElement> {
371
+ /** Called with the dropped/browsed files. */
372
+ onFiles?: (files: File[]) => void;
373
+ multiple?: boolean;
374
+ /** input[type=file] accept. */
375
+ accept?: string;
376
+ title?: React.ReactNode;
377
+ /** Sub line — the `.anchor` span is the click-to-browse affordance. */
378
+ sub?: React.ReactNode;
379
+ /** Leading glyph (e.g. upload-cloud icon). */
380
+ icon?: React.ReactNode;
381
+ disabled?: boolean;
382
+ }
383
+ /** Dashed dropzone — `.file-drop` with drag (`.is-dragging`) + click-to-browse. */
384
+ export const FileDrop: React.FC<FileDropProps> = ({ onFiles, multiple = true, accept, title = 'Drop files here', sub, icon, disabled = false, className, ...rest }) => {
385
+ const [dragging, setDragging] = React.useState(false);
386
+ const inputRef = React.useRef<HTMLInputElement>(null);
387
+ const emit = (list: FileList | null) => { if (list && list.length && onFiles) onFiles(Array.from(list)); };
388
+ return (
389
+ <div
390
+ className={['file-drop', dragging ? 'is-dragging' : '', className || ''].filter(Boolean).join(' ')}
391
+ role="button"
392
+ tabIndex={disabled ? undefined : 0}
393
+ aria-disabled={disabled || undefined}
394
+ onClick={() => { if (!disabled) inputRef.current?.click(); }}
395
+ onKeyDown={(e) => { if (!disabled && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); inputRef.current?.click(); } }}
396
+ onDragOver={(e) => { e.preventDefault(); if (!disabled) setDragging(true); }}
397
+ onDragLeave={() => setDragging(false)}
398
+ onDrop={(e) => { e.preventDefault(); setDragging(false); if (!disabled) emit(e.dataTransfer.files); }}
399
+ {...rest}
400
+ >
401
+ {icon}
402
+ <span className="ttl">{dragging ? 'Release to upload' : title}</span>
403
+ <span className="sub">{sub != null ? sub : <React.Fragment>or <span className="anchor">browse files</span></React.Fragment>}</span>
404
+ <input ref={inputRef} type="file" hidden multiple={multiple} accept={accept} disabled={disabled} onChange={(e) => { emit(e.target.files); e.target.value = ''; }} />
405
+ </div>
406
+ );
407
+ };
408
+
409
+ export type FileRowStatus = 'ok' | 'pending' | 'err';
410
+
411
+ export interface FileRowProps extends React.HTMLAttributes<HTMLLIElement> {
412
+ /** Icon tile kind — tints via `data-kind` (img / sheet / doc / pdf…). */
413
+ kind?: string;
414
+ /** Tile glyph (caller's <Icon/>) + optional corner tag text (e.g. "PDF"). */
415
+ icon?: React.ReactNode;
416
+ tag?: React.ReactNode;
417
+ name: React.ReactNode;
418
+ /** Mono sub line (size · date). Status err tints it. */
419
+ sub?: React.ReactNode;
420
+ /** ok (done) · pending (uploading) · err (failed — clickable to retry). */
421
+ status?: FileRowStatus;
422
+ statusLabel?: React.ReactNode;
423
+ /** 0–100 — renders the `.file-progress` hairline while uploading. */
424
+ progress?: number;
425
+ onRemove?: () => void;
426
+ }
427
+ /** Uploaded-file row — `.file-row` (icon tile · meta · status · remove). Wrap rows in <FileListBox>. */
428
+ export const FileRow: React.FC<FileRowProps> = ({ kind, icon, tag, name, sub, status, statusLabel, progress, onRemove, className, ...rest }) => (
429
+ <li className={['file-row', className || ''].filter(Boolean).join(' ')} {...rest}>
430
+ <span className="file-icon" data-kind={kind}>
431
+ {icon}
432
+ {tag != null ? <span className="file-icon-tag">{tag}</span> : null}
433
+ </span>
434
+ <span className="file-meta">
435
+ <span className="file-name">{name}</span>
436
+ {sub != null ? <span className={['file-sub', status === 'err' ? 'err' : ''].filter(Boolean).join(' ')}>{sub}</span> : null}
437
+ {progress != null ? (
438
+ <span className="file-progress"><span style={{ width: `${Math.max(0, Math.min(100, progress))}%` }} /></span>
439
+ ) : null}
440
+ </span>
441
+ {status != null ? <span className={`file-status ${status}`}>{statusLabel != null ? statusLabel : status === 'ok' ? 'Uploaded' : status === 'pending' ? 'Uploading…' : 'Retry'}</span> : null}
442
+ {onRemove ? (
443
+ <button type="button" className="file-remove" aria-label="Remove file" onClick={onRemove}>
444
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" aria-hidden="true">
445
+ <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
446
+ </svg>
447
+ </button>
448
+ ) : null}
449
+ </li>
450
+ );
451
+
452
+ export interface FileListBoxProps extends React.HTMLAttributes<HTMLDivElement> {
453
+ /** Uppercase head label (e.g. "3 files · 4.2 MB"). */
454
+ head?: React.ReactNode;
455
+ /** Head-right action (e.g. "Clear all") — `.file-list-action`. */
456
+ action?: React.ReactNode;
457
+ onActionClick?: () => void;
458
+ }
459
+ /** Post-upload list shell — `.file-list` with head + `.file-rows`. Children: <FileRow/>s. */
460
+ export const FileListBox: React.FC<FileListBoxProps> = ({ head, action, onActionClick, className, children, ...rest }) => (
461
+ <div className={['file-list', className || ''].filter(Boolean).join(' ')} {...rest}>
462
+ {head != null || action != null ? (
463
+ <div className="file-list-head">
464
+ <span>{head}</span>
465
+ {action != null ? (
466
+ <button type="button" className="file-list-action" onClick={onActionClick}>{action}</button>
467
+ ) : null}
468
+ </div>
469
+ ) : null}
470
+ <ul className="file-rows">{children}</ul>
471
+ </div>
472
+ );
473
+
474
+ export interface SliderProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'size'> {}
475
+ /** Range input — `.slider` (canonical track + thumb). Pair with a label that reflects the value. */
476
+ export const Slider: React.FC<SliderProps> = ({ className, ...rest }) => (
477
+ <input type="range" className={['slider', className || ''].filter(Boolean).join(' ')} {...rest} />
478
+ );
@@ -0,0 +1,27 @@
1
+ import * as React from 'react';
2
+
3
+ export type ModalSize = 'sm' | 'md' | 'lg' | 'xl';
4
+
5
+ export interface ModalProps {
6
+ open: boolean;
7
+ onClose?: () => void;
8
+ size?: ModalSize;
9
+ title?: React.ReactNode;
10
+ subtitle?: React.ReactNode;
11
+ /** Icon tile above the title (destructive confirmations). */
12
+ icon?: React.ReactNode;
13
+ /** Danger accent strip + icon tint (.modal-destructive). */
14
+ destructive?: boolean;
15
+ /** Footer row (.modal-footer) — canonical Buttons, right-aligned. */
16
+ footer?: React.ReactNode;
17
+ /** Left-side footer aux content (enables space-between). */
18
+ footerAux?: React.ReactNode;
19
+ /** Gallery/doc rendering: panel only (no scrim), overlay behaviours off
20
+ (focus trap, scroll-lock, Esc). Position it inside a relative frame. */
21
+ static?: boolean;
22
+ children?: React.ReactNode;
23
+ className?: string;
24
+ }
25
+
26
+ export declare const Modal: React.FC<ModalProps>;
27
+ export default Modal;