@financedistrict/apps-ui 0.1.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/LICENSE +19 -0
- package/README.md +119 -0
- package/dist/Radio-BYWz1n7p.d.cts +32 -0
- package/dist/Radio-BYWz1n7p.d.ts +32 -0
- package/dist/SelectTrigger-By0iPBI3.d.cts +78 -0
- package/dist/SelectTrigger-By0iPBI3.d.ts +78 -0
- package/dist/accordion.cjs +322 -0
- package/dist/accordion.d.cts +156 -0
- package/dist/accordion.d.ts +156 -0
- package/dist/accordion.js +2 -0
- package/dist/apps-ui.umd.js +1 -0
- package/dist/button.cjs +366 -0
- package/dist/button.d.cts +52 -0
- package/dist/button.d.ts +52 -0
- package/dist/button.js +3 -0
- package/dist/checkbox.cjs +74 -0
- package/dist/checkbox.d.cts +63 -0
- package/dist/checkbox.d.ts +63 -0
- package/dist/checkbox.js +2 -0
- package/dist/chunk-3RU7JMFE.js +27 -0
- package/dist/chunk-5TWRK4NW.js +783 -0
- package/dist/chunk-7EOSDFS3.js +9 -0
- package/dist/chunk-AIQ4UOGK.js +142 -0
- package/dist/chunk-B3MMGTPV.js +200 -0
- package/dist/chunk-DAHQYLUI.js +146 -0
- package/dist/chunk-DGBXADSS.js +1 -0
- package/dist/chunk-E6KPNTWW.js +295 -0
- package/dist/chunk-EOAB6G2D.js +149 -0
- package/dist/chunk-FAOVKDVM.js +32 -0
- package/dist/chunk-FV76Z4Z2.js +78 -0
- package/dist/chunk-GLTLBUZ7.js +285 -0
- package/dist/chunk-GTZFV43T.js +154 -0
- package/dist/chunk-HRGZNL4A.js +88 -0
- package/dist/chunk-IEVN3J2F.js +98 -0
- package/dist/chunk-IRS3T6LF.js +352 -0
- package/dist/chunk-IYYHCQXG.js +146 -0
- package/dist/chunk-JIXW3455.js +89 -0
- package/dist/chunk-N4VAP2GB.js +77 -0
- package/dist/chunk-SJ2ZRYVV.js +167 -0
- package/dist/chunk-SNU4YXOR.js +66 -0
- package/dist/chunk-TT6JQATV.js +99 -0
- package/dist/chunk-UPQ3ZPCO.js +66 -0
- package/dist/chunk-UVUSVFOH.js +264 -0
- package/dist/chunk-WJFPGGJB.js +255 -0
- package/dist/chunk-WXEBQA7A.js +371 -0
- package/dist/chunk-Y2O4YYZQ.js +184 -0
- package/dist/cut-corner.cjs +111 -0
- package/dist/cut-corner.d.cts +49 -0
- package/dist/cut-corner.d.ts +49 -0
- package/dist/cut-corner.js +1 -0
- package/dist/dialog.cjs +441 -0
- package/dist/dialog.d.cts +240 -0
- package/dist/dialog.d.ts +240 -0
- package/dist/dialog.js +3 -0
- package/dist/feature-card.cjs +182 -0
- package/dist/feature-card.d.cts +54 -0
- package/dist/feature-card.d.ts +54 -0
- package/dist/feature-card.js +2 -0
- package/dist/filter.cjs +873 -0
- package/dist/filter.d.cts +129 -0
- package/dist/filter.d.ts +129 -0
- package/dist/filter.js +6 -0
- package/dist/index.cjs +4583 -0
- package/dist/index.d.cts +29 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +27 -0
- package/dist/inline-select.cjs +856 -0
- package/dist/inline-select.d.cts +92 -0
- package/dist/inline-select.d.ts +92 -0
- package/dist/inline-select.js +6 -0
- package/dist/input.cjs +263 -0
- package/dist/input.d.cts +157 -0
- package/dist/input.d.ts +157 -0
- package/dist/input.js +4 -0
- package/dist/panel.cjs +185 -0
- package/dist/panel.d.cts +103 -0
- package/dist/panel.d.ts +103 -0
- package/dist/panel.js +2 -0
- package/dist/property-grid.cjs +110 -0
- package/dist/property-grid.d.cts +56 -0
- package/dist/property-grid.d.ts +56 -0
- package/dist/property-grid.js +2 -0
- package/dist/radio.cjs +74 -0
- package/dist/radio.d.cts +39 -0
- package/dist/radio.d.ts +39 -0
- package/dist/radio.js +2 -0
- package/dist/scrollbar.cjs +34 -0
- package/dist/scrollbar.d.cts +35 -0
- package/dist/scrollbar.d.ts +35 -0
- package/dist/scrollbar.js +1 -0
- package/dist/search-bar.cjs +408 -0
- package/dist/search-bar.d.cts +42 -0
- package/dist/search-bar.d.ts +42 -0
- package/dist/search-bar.js +4 -0
- package/dist/select.cjs +998 -0
- package/dist/select.d.cts +357 -0
- package/dist/select.d.ts +357 -0
- package/dist/select.js +5 -0
- package/dist/selectable-feature-card.cjs +159 -0
- package/dist/selectable-feature-card.d.cts +74 -0
- package/dist/selectable-feature-card.d.ts +74 -0
- package/dist/selectable-feature-card.js +3 -0
- package/dist/sidebar-item.cjs +175 -0
- package/dist/sidebar-item.d.cts +75 -0
- package/dist/sidebar-item.d.ts +75 -0
- package/dist/sidebar-item.js +2 -0
- package/dist/switch.cjs +86 -0
- package/dist/switch.d.cts +72 -0
- package/dist/switch.d.ts +72 -0
- package/dist/switch.js +2 -0
- package/dist/table-cell.cjs +196 -0
- package/dist/table-cell.d.cts +76 -0
- package/dist/table-cell.d.ts +76 -0
- package/dist/table-cell.js +2 -0
- package/dist/table-title.cjs +34 -0
- package/dist/table-title.d.cts +16 -0
- package/dist/table-title.d.ts +16 -0
- package/dist/table-title.js +2 -0
- package/dist/tabs.cjs +299 -0
- package/dist/tabs.d.cts +116 -0
- package/dist/tabs.d.ts +116 -0
- package/dist/tabs.js +2 -0
- package/dist/toast.cjs +87 -0
- package/dist/toast.d.cts +48 -0
- package/dist/toast.d.ts +48 -0
- package/dist/toast.js +2 -0
- package/dist/token-input.cjs +415 -0
- package/dist/token-input.d.cts +90 -0
- package/dist/token-input.d.ts +90 -0
- package/dist/token-input.js +3 -0
- package/dist/tooltip.cjs +384 -0
- package/dist/tooltip.d.cts +142 -0
- package/dist/tooltip.d.ts +142 -0
- package/dist/tooltip.js +2 -0
- package/package.json +200 -0
- package/src/fonts.css +33 -0
package/dist/toast.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as class_variance_authority_types from 'class-variance-authority/types';
|
|
3
|
+
|
|
4
|
+
interface ToastProps extends Omit<React.ComponentProps<"div">, "children"> {
|
|
5
|
+
/**
|
|
6
|
+
* Status. `success` → a polite `role="status"` region; `error` → an
|
|
7
|
+
* assertive `role="alert"` region. Also selects the leading icon + its color.
|
|
8
|
+
*/
|
|
9
|
+
variation?: "success" | "error";
|
|
10
|
+
/** The message body (`body-03`). */
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Dismiss handler. When provided, the trailing close button is rendered and
|
|
14
|
+
* wired to it; omit it for a toast with no close affordance.
|
|
15
|
+
*/
|
|
16
|
+
onClose?: () => void;
|
|
17
|
+
/** Ref forwarded to the root element. */
|
|
18
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Toast — the Figma `Toast` (node `5020:2203`). A presentational dark
|
|
22
|
+
* notification chip: a leading status icon, a message, and an optional close
|
|
23
|
+
* button. It is a live region (`role="status"` for success, `role="alert"` for
|
|
24
|
+
* error) so it announces when mounted; positioning, stacking, and auto-dismiss
|
|
25
|
+
* are the consumer's concern (there is no toast manager here).
|
|
26
|
+
*/
|
|
27
|
+
declare function Toast({ variation, children, onClose, className, ref, ...props }: ToastProps): React.JSX.Element;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Toast styling — the Figma `Toast` (node `5020:2203`). Binds ONLY to the
|
|
31
|
+
* semantic token layer (never primitives); theme switching is automatic via
|
|
32
|
+
* `.dark`, so no `dark:` variants. The surface is the fixed dark "reversed"
|
|
33
|
+
* card, so the root is not variation-dependent — only the leading status
|
|
34
|
+
* icon's color changes with `variation`.
|
|
35
|
+
*/
|
|
36
|
+
/** Root row — dark reversed surface; identical for both variations. */
|
|
37
|
+
declare const toastVariants: (props?: class_variance_authority_types.ClassProp | undefined) => string;
|
|
38
|
+
/** Leading 20px status icon slot; color per variation. */
|
|
39
|
+
declare const toastIconVariants: (props?: ({
|
|
40
|
+
variation?: "error" | "success" | null | undefined;
|
|
41
|
+
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
42
|
+
/**
|
|
43
|
+
* Trailing close button — muted, brightens on hover; shared ADR-0010 focus
|
|
44
|
+
* ring (`rounded-xs` so the ring follows the small hit area).
|
|
45
|
+
*/
|
|
46
|
+
declare const toastCloseVariants: (props?: class_variance_authority_types.ClassProp | undefined) => string;
|
|
47
|
+
|
|
48
|
+
export { Toast, type ToastProps, toastCloseVariants, toastIconVariants, toastVariants };
|
package/dist/toast.js
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var React = require('react');
|
|
4
|
+
var react = require('@phosphor-icons/react');
|
|
5
|
+
var clsx = require('clsx');
|
|
6
|
+
var tailwindMerge = require('tailwind-merge');
|
|
7
|
+
var classVarianceAuthority = require('class-variance-authority');
|
|
8
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
9
|
+
|
|
10
|
+
function _interopNamespace(e) {
|
|
11
|
+
if (e && e.__esModule) return e;
|
|
12
|
+
var n = Object.create(null);
|
|
13
|
+
if (e) {
|
|
14
|
+
Object.keys(e).forEach(function (k) {
|
|
15
|
+
if (k !== 'default') {
|
|
16
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
17
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
get: function () { return e[k]; }
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
n.default = e;
|
|
25
|
+
return Object.freeze(n);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
29
|
+
|
|
30
|
+
// src/token-input/TokenInput.tsx
|
|
31
|
+
function cn(...inputs) {
|
|
32
|
+
return tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
33
|
+
}
|
|
34
|
+
var inputVariants = classVarianceAuthority.cva(
|
|
35
|
+
[
|
|
36
|
+
// Field row layout — icons + input on one line, vertically centred.
|
|
37
|
+
"flex w-full items-center",
|
|
38
|
+
// Colour transitions for the focus border change.
|
|
39
|
+
"transition-colors",
|
|
40
|
+
// I-beam text cursor over the WHOLE field row (not just the inner input),
|
|
41
|
+
// so clicking anywhere in the padding/icon area reads as "type here". This
|
|
42
|
+
// is the only hover affordance — hover intentionally does NOT recolour the
|
|
43
|
+
// border (user-authorized deviation from the Figma). `cursor-text` sits in
|
|
44
|
+
// the base; `has-[:disabled]:cursor-not-allowed` below is declared AFTER it
|
|
45
|
+
// so it wins on a disabled field (same-specificity utilities, later class
|
|
46
|
+
// takes precedence — confirmed by tailwind-merge ordering).
|
|
47
|
+
"cursor-text",
|
|
48
|
+
// Disabled: not-allowed cursor + block pointer interaction on the whole
|
|
49
|
+
// field (the native `disabled` on the input already stops typing; icons are
|
|
50
|
+
// dimmed separately by the `disabled`-derived `iconStateClass` in <Input>).
|
|
51
|
+
// Declared after `cursor-text` so it overrides the I-beam when disabled.
|
|
52
|
+
"has-[:disabled]:cursor-not-allowed"
|
|
53
|
+
],
|
|
54
|
+
{
|
|
55
|
+
variants: {
|
|
56
|
+
variation: {
|
|
57
|
+
field: [
|
|
58
|
+
"bg-input-background border border-input-border rounded-sm",
|
|
59
|
+
// Focus (mouse or keyboard) strengthens the whole border to the
|
|
60
|
+
// accent. Hover does NOT change colour (cursor-only affordance).
|
|
61
|
+
"focus-within:border-input-border-accent"
|
|
62
|
+
],
|
|
63
|
+
line: [
|
|
64
|
+
"border-b border-input-border",
|
|
65
|
+
// Focus (mouse or keyboard) strengthens the bottom line to the
|
|
66
|
+
// accent. Hover does NOT change colour (cursor-only affordance).
|
|
67
|
+
"focus-within:border-input-border-accent"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
size: {
|
|
71
|
+
// sm/lg heights reuse the Button scale; gap follows Figma per size.
|
|
72
|
+
sm: "h-8 gap-1",
|
|
73
|
+
lg: "h-10 gap-2"
|
|
74
|
+
},
|
|
75
|
+
error: {
|
|
76
|
+
// Error out-ranks focus: declare the destructive border for the resting
|
|
77
|
+
// state AND its `focus-within:` form so an errored field never recolours
|
|
78
|
+
// to the accent when focused (a bare `border-…` alone would lose to the
|
|
79
|
+
// variation's `focus-within:` accent utility above). No `hover:` form is
|
|
80
|
+
// needed — hover no longer changes colour on any variation. The `error`
|
|
81
|
+
// variant's classes are emitted after the variation's, so at equal
|
|
82
|
+
// specificity the destructive `focus-within:` wins by order.
|
|
83
|
+
true: "border-input-destructive-foreground-accent focus-within:border-input-destructive-foreground-accent",
|
|
84
|
+
false: ""
|
|
85
|
+
},
|
|
86
|
+
// Bottom-line visibility toggle. LINE ONLY — the hide effect is applied
|
|
87
|
+
// via a compound variant below (scoped to `variation:line`), so `field`
|
|
88
|
+
// is unaffected. Boolean → `true`/`false`; `true` (default) keeps today's
|
|
89
|
+
// behaviour. No classes here: `true` is the resting `border-input-border`
|
|
90
|
+
// from the variation, and `false`'s hide is the compound variant.
|
|
91
|
+
showBottomLine: {
|
|
92
|
+
true: "",
|
|
93
|
+
false: ""
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
compoundVariants: [
|
|
97
|
+
// field padding — Figma field-lg = px-3 (12px) / py-2 (8px).
|
|
98
|
+
{ variation: "field", size: "lg", className: "px-3 py-2" },
|
|
99
|
+
// field-sm: DERIVED. px-2 (8px = spacing/2) steps down from field-lg's
|
|
100
|
+
// px-3 (12px) and equals py-2 for a uniform spacing/2 inset all around.
|
|
101
|
+
{ variation: "field", size: "sm", className: "px-2 py-2" },
|
|
102
|
+
// line — no horizontal padding (Figma px-0); vertical padding per size.
|
|
103
|
+
{ variation: "line", size: "lg", className: "py-2" },
|
|
104
|
+
{ variation: "line", size: "sm", className: "py-2.5" },
|
|
105
|
+
// Hide the resting bottom line (LINE ONLY): recolour to transparent while
|
|
106
|
+
// keeping `border-b` for a stable 1px width (no layout shift). Gated on
|
|
107
|
+
// `error:false` so an errored field keeps its destructive rest border
|
|
108
|
+
// (transparent would otherwise last-win over it). Focus still wins:
|
|
109
|
+
// `focus-within:border-input-border-accent` out-ranks `border-transparent`
|
|
110
|
+
// (later in class order), so the line reappears on focus. `field` is
|
|
111
|
+
// untouched — this compound only matches `variation:line`.
|
|
112
|
+
{
|
|
113
|
+
variation: "line",
|
|
114
|
+
showBottomLine: false,
|
|
115
|
+
error: false,
|
|
116
|
+
className: "border-transparent"
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
defaultVariants: {
|
|
120
|
+
variation: "field",
|
|
121
|
+
size: "lg",
|
|
122
|
+
error: false,
|
|
123
|
+
showBottomLine: true
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
function Token({
|
|
128
|
+
value,
|
|
129
|
+
invalid = false,
|
|
130
|
+
disabled = false,
|
|
131
|
+
onRemove
|
|
132
|
+
}) {
|
|
133
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
134
|
+
"span",
|
|
135
|
+
{
|
|
136
|
+
"data-slot": "token",
|
|
137
|
+
"data-invalid": invalid || void 0,
|
|
138
|
+
tabIndex: disabled ? void 0 : 0,
|
|
139
|
+
onKeyDown: disabled ? void 0 : (event) => {
|
|
140
|
+
if (event.key === "Delete" || event.key === "Backspace") {
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
onRemove("keyboard");
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
className: cn(
|
|
146
|
+
// `group` so the ✕ can react to the badge's hover/focus (see below).
|
|
147
|
+
// Height is exactly 22px = 20px (`body-03` line) + 2px border, with NO
|
|
148
|
+
// vertical padding: this keeps the field a constant 40px tall
|
|
149
|
+
// (badge 22 + 8+8 field padding + 1+1 field border = 40, empty or filled).
|
|
150
|
+
// Adding `py-*` here would push the field past 40px once tokens appear.
|
|
151
|
+
"group inline-flex items-center gap-1 min-w-5 rounded-sm border px-1",
|
|
152
|
+
// Default: neutral card border + background. Invalid: destructive accent
|
|
153
|
+
// recolour (there is no `card-destructive` token — reuse `input-destructive-*`).
|
|
154
|
+
invalid ? "border-input-destructive-border bg-card-background" : "border-card-border bg-card-background",
|
|
155
|
+
// Interactive chrome (only when not disabled): hover/focus fill to
|
|
156
|
+
// `card-accent` (the DS's "stronger/hover" step up from `card-background`)
|
|
157
|
+
// and the STANDARD ADR-0010/0012 focus ring on the whole badge — the ✕
|
|
158
|
+
// itself carries no ring. `cursor-default` overrides the field's I-beam
|
|
159
|
+
// so a badge doesn't read as "type here".
|
|
160
|
+
!disabled && "cursor-default hover:bg-card-accent focus-visible:bg-card-accent outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus"
|
|
161
|
+
),
|
|
162
|
+
children: [
|
|
163
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
164
|
+
"span",
|
|
165
|
+
{
|
|
166
|
+
"data-slot": "token-label",
|
|
167
|
+
className: cn(
|
|
168
|
+
"body-03",
|
|
169
|
+
// Disabled wins over invalid: the Figma disabled badge (561:12782) is
|
|
170
|
+
// fully muted — a disabled field is inert, so no destructive red.
|
|
171
|
+
disabled ? "text-card-foreground-muted" : invalid ? "text-input-destructive-foreground-accent" : "text-card-foreground"
|
|
172
|
+
),
|
|
173
|
+
children: value
|
|
174
|
+
}
|
|
175
|
+
),
|
|
176
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
177
|
+
"button",
|
|
178
|
+
{
|
|
179
|
+
type: "button",
|
|
180
|
+
"aria-label": `Remove ${value}`,
|
|
181
|
+
onClick: () => onRemove("pointer"),
|
|
182
|
+
disabled,
|
|
183
|
+
tabIndex: -1,
|
|
184
|
+
"data-slot": "token-remove",
|
|
185
|
+
className: cn(
|
|
186
|
+
// Reset native button chrome.
|
|
187
|
+
"inline-flex cursor-pointer items-center justify-center rounded-xs bg-transparent p-0",
|
|
188
|
+
// Icon colour follows the badge state. Disabled wins (fully muted, no
|
|
189
|
+
// hover strengthen — the badge is inert). Valid: muted at rest,
|
|
190
|
+
// strengthens to `card-foreground` on badge hover/focus (via `group-*`).
|
|
191
|
+
// Invalid: stays destructive (a strengthen would drop the flag).
|
|
192
|
+
disabled ? "text-card-foreground-muted" : invalid ? "text-input-destructive-foreground-accent" : "text-card-foreground-muted group-hover:text-card-foreground group-focus-visible:text-card-foreground",
|
|
193
|
+
"disabled:cursor-not-allowed"
|
|
194
|
+
),
|
|
195
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(react.XIcon, { weight: "regular", "aria-hidden": "true", className: "size-4" })
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
]
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
function splitOnSeparators(raw, separators) {
|
|
203
|
+
if (separators.length === 0) return [raw];
|
|
204
|
+
const escaped = separators.map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("");
|
|
205
|
+
return raw.split(new RegExp(`[${escaped}]`));
|
|
206
|
+
}
|
|
207
|
+
function TokenInput({
|
|
208
|
+
className,
|
|
209
|
+
fieldClassName,
|
|
210
|
+
wrapperClassName,
|
|
211
|
+
value,
|
|
212
|
+
defaultValue,
|
|
213
|
+
onChange,
|
|
214
|
+
separators = [","],
|
|
215
|
+
commitOnBlur = false,
|
|
216
|
+
validate,
|
|
217
|
+
label,
|
|
218
|
+
optional = false,
|
|
219
|
+
hint,
|
|
220
|
+
error = false,
|
|
221
|
+
disabled,
|
|
222
|
+
placeholder,
|
|
223
|
+
id: idProp,
|
|
224
|
+
onKeyDown,
|
|
225
|
+
onBlur,
|
|
226
|
+
onInput,
|
|
227
|
+
ref,
|
|
228
|
+
...props
|
|
229
|
+
}) {
|
|
230
|
+
const isControlled = value !== void 0;
|
|
231
|
+
const [tokensState, setTokensState] = React__namespace.useState(
|
|
232
|
+
() => defaultValue ?? []
|
|
233
|
+
);
|
|
234
|
+
const tokens = isControlled ? value : tokensState;
|
|
235
|
+
const innerRef = React__namespace.useRef(null);
|
|
236
|
+
const setInputRef = React__namespace.useCallback(
|
|
237
|
+
(node) => {
|
|
238
|
+
innerRef.current = node;
|
|
239
|
+
if (typeof ref === "function") ref(node);
|
|
240
|
+
else if (ref)
|
|
241
|
+
ref.current = node;
|
|
242
|
+
},
|
|
243
|
+
[ref]
|
|
244
|
+
);
|
|
245
|
+
const generatedId = React__namespace.useId();
|
|
246
|
+
const id = idProp ?? generatedId;
|
|
247
|
+
const hintId = hint ? `${id}-hint` : void 0;
|
|
248
|
+
const commitTokens = (next) => {
|
|
249
|
+
if (!isControlled) setTokensState(next);
|
|
250
|
+
onChange?.(next);
|
|
251
|
+
};
|
|
252
|
+
const addTokens = (pieces) => {
|
|
253
|
+
const next = [...tokens];
|
|
254
|
+
const seen = new Set(next.map((t) => t.toLowerCase()));
|
|
255
|
+
let changed = false;
|
|
256
|
+
for (const piece of pieces) {
|
|
257
|
+
const trimmed = piece.trim();
|
|
258
|
+
if (trimmed === "") continue;
|
|
259
|
+
const key = trimmed.toLowerCase();
|
|
260
|
+
if (seen.has(key)) continue;
|
|
261
|
+
seen.add(key);
|
|
262
|
+
next.push(trimmed);
|
|
263
|
+
changed = true;
|
|
264
|
+
}
|
|
265
|
+
if (changed) commitTokens(next);
|
|
266
|
+
};
|
|
267
|
+
const removeTokenAt = (index, origin) => {
|
|
268
|
+
commitTokens(tokens.filter((_, i) => i !== index));
|
|
269
|
+
if (origin === "keyboard") innerRef.current?.focus();
|
|
270
|
+
};
|
|
271
|
+
const handleInput = (event) => {
|
|
272
|
+
onInput?.(event);
|
|
273
|
+
const input = event.currentTarget;
|
|
274
|
+
const raw = input.value;
|
|
275
|
+
if (separators.some((sep) => raw.includes(sep))) {
|
|
276
|
+
addTokens(splitOnSeparators(raw, separators));
|
|
277
|
+
input.value = "";
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const handleKeyDown = (event) => {
|
|
281
|
+
onKeyDown?.(event);
|
|
282
|
+
if (event.key === "Backspace" && event.currentTarget.value === "" && tokens.length > 0) {
|
|
283
|
+
event.preventDefault();
|
|
284
|
+
removeTokenAt(tokens.length - 1);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
const handleBlur = (event) => {
|
|
288
|
+
onBlur?.(event);
|
|
289
|
+
if (commitOnBlur) {
|
|
290
|
+
const input = event.currentTarget;
|
|
291
|
+
if (input.value.trim() !== "") {
|
|
292
|
+
addTokens([input.value]);
|
|
293
|
+
input.value = "";
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
const handleFieldMouseDown = (event) => {
|
|
298
|
+
const target = event.target;
|
|
299
|
+
if (target.closest("[data-slot=token]")) return;
|
|
300
|
+
if (target === innerRef.current) return;
|
|
301
|
+
event.preventDefault();
|
|
302
|
+
innerRef.current?.focus();
|
|
303
|
+
};
|
|
304
|
+
const iconStateClass = cn(
|
|
305
|
+
error ? "text-input-destructive-foreground-accent" : "text-input-foreground",
|
|
306
|
+
disabled && "text-input-foreground-muted"
|
|
307
|
+
);
|
|
308
|
+
const HintIcon = error ? react.WarningIcon : react.InfoIcon;
|
|
309
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col gap-1.5", wrapperClassName), children: [
|
|
310
|
+
label != null && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
311
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
312
|
+
"label",
|
|
313
|
+
{
|
|
314
|
+
htmlFor: id,
|
|
315
|
+
"data-slot": "input-label",
|
|
316
|
+
className: cn(
|
|
317
|
+
"caption-03-medium uppercase",
|
|
318
|
+
error ? "text-input-destructive-foreground-accent" : "text-input-foreground",
|
|
319
|
+
disabled && "text-input-foreground-muted"
|
|
320
|
+
),
|
|
321
|
+
children: label
|
|
322
|
+
}
|
|
323
|
+
),
|
|
324
|
+
optional && /* @__PURE__ */ jsxRuntime.jsx(
|
|
325
|
+
"span",
|
|
326
|
+
{
|
|
327
|
+
"data-slot": "input-optional",
|
|
328
|
+
className: "body-04 text-input-foreground-muted",
|
|
329
|
+
children: "Optional"
|
|
330
|
+
}
|
|
331
|
+
)
|
|
332
|
+
] }),
|
|
333
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
334
|
+
"div",
|
|
335
|
+
{
|
|
336
|
+
"data-slot": "token-input-field",
|
|
337
|
+
"data-error": error || void 0,
|
|
338
|
+
onMouseDown: handleFieldMouseDown,
|
|
339
|
+
className: cn(
|
|
340
|
+
// Reuse Input's field styling (bg/border/rounded/focus-within/error).
|
|
341
|
+
inputVariants({ variation: "field", size: "lg", error }),
|
|
342
|
+
// Layout overrides (tailwind-merge last-wins): grow with wrapped rows,
|
|
343
|
+
// keep single-row min height, allow badges + input to wrap, uniform
|
|
344
|
+
// spacing/2 inset (overrides field-lg's px-3 py-2).
|
|
345
|
+
"h-auto min-h-10 flex-wrap gap-1 p-2",
|
|
346
|
+
// Icon slots inherit the shared state colour.
|
|
347
|
+
iconStateClass,
|
|
348
|
+
fieldClassName
|
|
349
|
+
),
|
|
350
|
+
children: [
|
|
351
|
+
tokens.map((token, index) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
352
|
+
Token,
|
|
353
|
+
{
|
|
354
|
+
value: token,
|
|
355
|
+
invalid: validate ? !validate(token) : false,
|
|
356
|
+
disabled,
|
|
357
|
+
onRemove: (origin) => removeTokenAt(index, origin)
|
|
358
|
+
},
|
|
359
|
+
`${token}-${index}`
|
|
360
|
+
)),
|
|
361
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
362
|
+
"input",
|
|
363
|
+
{
|
|
364
|
+
id,
|
|
365
|
+
"data-slot": "token-input",
|
|
366
|
+
disabled,
|
|
367
|
+
placeholder: tokens.length === 0 ? placeholder : void 0,
|
|
368
|
+
"aria-invalid": error || void 0,
|
|
369
|
+
"aria-describedby": hintId,
|
|
370
|
+
className: cn(
|
|
371
|
+
// Typography + grow to fill remaining row width. `min-w-0` lets the
|
|
372
|
+
// input shrink; a small basis keeps it usable when tokens wrap.
|
|
373
|
+
"body-03 w-0 min-w-16 flex-1 bg-transparent outline-none",
|
|
374
|
+
"text-input-foreground-accent caret-input-foreground-accent",
|
|
375
|
+
"placeholder:text-input-foreground-muted",
|
|
376
|
+
"disabled:cursor-not-allowed disabled:text-input-foreground-muted",
|
|
377
|
+
className
|
|
378
|
+
),
|
|
379
|
+
ref: setInputRef,
|
|
380
|
+
onInput: handleInput,
|
|
381
|
+
onKeyDown: handleKeyDown,
|
|
382
|
+
onBlur: handleBlur,
|
|
383
|
+
...props
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
]
|
|
387
|
+
}
|
|
388
|
+
),
|
|
389
|
+
hint != null && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
390
|
+
"p",
|
|
391
|
+
{
|
|
392
|
+
id: hintId,
|
|
393
|
+
"data-slot": "input-hint",
|
|
394
|
+
className: cn(
|
|
395
|
+
"body-04 flex items-center gap-1",
|
|
396
|
+
error ? "text-input-destructive-foreground-accent" : "text-input-foreground",
|
|
397
|
+
disabled && "text-input-foreground-muted"
|
|
398
|
+
),
|
|
399
|
+
children: [
|
|
400
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
401
|
+
HintIcon,
|
|
402
|
+
{
|
|
403
|
+
"aria-hidden": "true",
|
|
404
|
+
weight: "regular",
|
|
405
|
+
className: "size-4 shrink-0"
|
|
406
|
+
}
|
|
407
|
+
),
|
|
408
|
+
hint
|
|
409
|
+
]
|
|
410
|
+
}
|
|
411
|
+
)
|
|
412
|
+
] });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
exports.TokenInput = TokenInput;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
interface TokenInputProps extends Omit<React.ComponentProps<"input">, "size" | "value" | "defaultValue" | "onChange"> {
|
|
4
|
+
/**
|
|
5
|
+
* Committed tokens (controlled). Pair with `onChange`. When provided, the
|
|
6
|
+
* component does not hold its own token state — the parent owns the array.
|
|
7
|
+
*/
|
|
8
|
+
value?: string[];
|
|
9
|
+
/**
|
|
10
|
+
* Initial committed tokens (uncontrolled). Seeds the internal token state;
|
|
11
|
+
* ignored once `value` is provided (controlled mode).
|
|
12
|
+
*/
|
|
13
|
+
defaultValue?: string[];
|
|
14
|
+
/**
|
|
15
|
+
* Fires whenever a token is added or removed, with the full next tokens array.
|
|
16
|
+
* The in-progress draft text lives in the native `<input>` and is NOT reported
|
|
17
|
+
* here — the component's value is the committed tokens array.
|
|
18
|
+
*/
|
|
19
|
+
onChange?: (tokens: string[]) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Characters that commit the current draft into token(s). Default `[","]`.
|
|
22
|
+
* When the draft contains any of these chars it is split on all of them,
|
|
23
|
+
* trimmed, empties dropped, case-insensitive duplicates dropped, then pushed.
|
|
24
|
+
* No key other than a separator commits (Enter never commits).
|
|
25
|
+
*/
|
|
26
|
+
separators?: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Opt-in: commit a non-empty trailing draft as one token on blur. Default
|
|
29
|
+
* `false` (strict separator-only commit). Useful so a form never loses a
|
|
30
|
+
* half-typed trailing entry.
|
|
31
|
+
*/
|
|
32
|
+
commitOnBlur?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Optional per-token predicate. Default `undefined` = no validation, nothing
|
|
35
|
+
* is ever flagged (the generic component accepts anything). When provided,
|
|
36
|
+
* tokens failing it are KEPT but flagged destructive ("flag but allow") and
|
|
37
|
+
* expose `data-invalid`. The DS ships no domain-specific validator.
|
|
38
|
+
*/
|
|
39
|
+
validate?: (token: string) => boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Uppercase label shown above the field. When omitted the label row is not
|
|
42
|
+
* rendered. Paired with the `<input>` via generated `id`/`htmlFor` (or your
|
|
43
|
+
* own `id`) so the control is properly named for assistive tech.
|
|
44
|
+
*/
|
|
45
|
+
label?: React.ReactNode;
|
|
46
|
+
/**
|
|
47
|
+
* Marks the field as not required — renders an "Optional" tag on the right of
|
|
48
|
+
* the label row (Figma). Has no effect without a `label`.
|
|
49
|
+
*/
|
|
50
|
+
optional?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Helper text shown below the field, with a leading 16px icon (info by
|
|
53
|
+
* default, warning when `error`). Linked to the input via `aria-describedby`.
|
|
54
|
+
*/
|
|
55
|
+
hint?: React.ReactNode;
|
|
56
|
+
/**
|
|
57
|
+
* Field-level error state (distinct from per-token invalid flagging): sets
|
|
58
|
+
* `aria-invalid`, turns the label, border, icon and hint to the destructive
|
|
59
|
+
* accent, and swaps the hint icon to a warning — exactly as Input's `error`.
|
|
60
|
+
*/
|
|
61
|
+
error?: boolean;
|
|
62
|
+
/** Class names for the outer wrapper (the vertical label/field/hint stack). */
|
|
63
|
+
wrapperClassName?: string;
|
|
64
|
+
/** Class names for the field row (border/background box), not the `<input>`. */
|
|
65
|
+
fieldClassName?: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* TokenInput — a domain-agnostic token / tag input. As the user types, text
|
|
69
|
+
* commits into removable badges (tokens) on a configurable separator (default
|
|
70
|
+
* comma). The DS ships it knowing NOTHING about emails or any other domain:
|
|
71
|
+
* validation, copy and separators are all supplied at the call site (mirrors
|
|
72
|
+
* SearchBar-over-Input layering — ship the generic primitive, let the app
|
|
73
|
+
* specialize it).
|
|
74
|
+
*
|
|
75
|
+
* Composition — "reuse variants only": TokenInput does NOT render <Input> (its
|
|
76
|
+
* badges wrap inside the field and its leading slot is decorative-by-contract).
|
|
77
|
+
* It reuses `inputVariants({ variation: "field", size: "lg", error })` — the
|
|
78
|
+
* hard-to-duplicate token/focus/error handling — and overrides layout only via
|
|
79
|
+
* `cn` (tailwind-merge last-wins): `h-auto min-h-10 flex-wrap p-2`. The thin
|
|
80
|
+
* label + hint rows are replicated locally, matching Input's exact classes. The
|
|
81
|
+
* `field`/`lg` axes are fixed and NOT exposed as props (per the Figma scope).
|
|
82
|
+
*
|
|
83
|
+
* Value model: committed tokens are a `string[]` (controlled `value`+`onChange`
|
|
84
|
+
* or uncontrolled `defaultValue`); the draft lives in the native `<input>` (the
|
|
85
|
+
* source of truth for the in-progress text), read via a merged `ref`. `ref`
|
|
86
|
+
* forwards to the `<input>`.
|
|
87
|
+
*/
|
|
88
|
+
declare function TokenInput({ className, fieldClassName, wrapperClassName, value, defaultValue, onChange, separators, commitOnBlur, validate, label, optional, hint, error, disabled, placeholder, id: idProp, onKeyDown, onBlur, onInput, ref, ...props }: TokenInputProps): React.JSX.Element;
|
|
89
|
+
|
|
90
|
+
export { TokenInput, type TokenInputProps };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
interface TokenInputProps extends Omit<React.ComponentProps<"input">, "size" | "value" | "defaultValue" | "onChange"> {
|
|
4
|
+
/**
|
|
5
|
+
* Committed tokens (controlled). Pair with `onChange`. When provided, the
|
|
6
|
+
* component does not hold its own token state — the parent owns the array.
|
|
7
|
+
*/
|
|
8
|
+
value?: string[];
|
|
9
|
+
/**
|
|
10
|
+
* Initial committed tokens (uncontrolled). Seeds the internal token state;
|
|
11
|
+
* ignored once `value` is provided (controlled mode).
|
|
12
|
+
*/
|
|
13
|
+
defaultValue?: string[];
|
|
14
|
+
/**
|
|
15
|
+
* Fires whenever a token is added or removed, with the full next tokens array.
|
|
16
|
+
* The in-progress draft text lives in the native `<input>` and is NOT reported
|
|
17
|
+
* here — the component's value is the committed tokens array.
|
|
18
|
+
*/
|
|
19
|
+
onChange?: (tokens: string[]) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Characters that commit the current draft into token(s). Default `[","]`.
|
|
22
|
+
* When the draft contains any of these chars it is split on all of them,
|
|
23
|
+
* trimmed, empties dropped, case-insensitive duplicates dropped, then pushed.
|
|
24
|
+
* No key other than a separator commits (Enter never commits).
|
|
25
|
+
*/
|
|
26
|
+
separators?: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Opt-in: commit a non-empty trailing draft as one token on blur. Default
|
|
29
|
+
* `false` (strict separator-only commit). Useful so a form never loses a
|
|
30
|
+
* half-typed trailing entry.
|
|
31
|
+
*/
|
|
32
|
+
commitOnBlur?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Optional per-token predicate. Default `undefined` = no validation, nothing
|
|
35
|
+
* is ever flagged (the generic component accepts anything). When provided,
|
|
36
|
+
* tokens failing it are KEPT but flagged destructive ("flag but allow") and
|
|
37
|
+
* expose `data-invalid`. The DS ships no domain-specific validator.
|
|
38
|
+
*/
|
|
39
|
+
validate?: (token: string) => boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Uppercase label shown above the field. When omitted the label row is not
|
|
42
|
+
* rendered. Paired with the `<input>` via generated `id`/`htmlFor` (or your
|
|
43
|
+
* own `id`) so the control is properly named for assistive tech.
|
|
44
|
+
*/
|
|
45
|
+
label?: React.ReactNode;
|
|
46
|
+
/**
|
|
47
|
+
* Marks the field as not required — renders an "Optional" tag on the right of
|
|
48
|
+
* the label row (Figma). Has no effect without a `label`.
|
|
49
|
+
*/
|
|
50
|
+
optional?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Helper text shown below the field, with a leading 16px icon (info by
|
|
53
|
+
* default, warning when `error`). Linked to the input via `aria-describedby`.
|
|
54
|
+
*/
|
|
55
|
+
hint?: React.ReactNode;
|
|
56
|
+
/**
|
|
57
|
+
* Field-level error state (distinct from per-token invalid flagging): sets
|
|
58
|
+
* `aria-invalid`, turns the label, border, icon and hint to the destructive
|
|
59
|
+
* accent, and swaps the hint icon to a warning — exactly as Input's `error`.
|
|
60
|
+
*/
|
|
61
|
+
error?: boolean;
|
|
62
|
+
/** Class names for the outer wrapper (the vertical label/field/hint stack). */
|
|
63
|
+
wrapperClassName?: string;
|
|
64
|
+
/** Class names for the field row (border/background box), not the `<input>`. */
|
|
65
|
+
fieldClassName?: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* TokenInput — a domain-agnostic token / tag input. As the user types, text
|
|
69
|
+
* commits into removable badges (tokens) on a configurable separator (default
|
|
70
|
+
* comma). The DS ships it knowing NOTHING about emails or any other domain:
|
|
71
|
+
* validation, copy and separators are all supplied at the call site (mirrors
|
|
72
|
+
* SearchBar-over-Input layering — ship the generic primitive, let the app
|
|
73
|
+
* specialize it).
|
|
74
|
+
*
|
|
75
|
+
* Composition — "reuse variants only": TokenInput does NOT render <Input> (its
|
|
76
|
+
* badges wrap inside the field and its leading slot is decorative-by-contract).
|
|
77
|
+
* It reuses `inputVariants({ variation: "field", size: "lg", error })` — the
|
|
78
|
+
* hard-to-duplicate token/focus/error handling — and overrides layout only via
|
|
79
|
+
* `cn` (tailwind-merge last-wins): `h-auto min-h-10 flex-wrap p-2`. The thin
|
|
80
|
+
* label + hint rows are replicated locally, matching Input's exact classes. The
|
|
81
|
+
* `field`/`lg` axes are fixed and NOT exposed as props (per the Figma scope).
|
|
82
|
+
*
|
|
83
|
+
* Value model: committed tokens are a `string[]` (controlled `value`+`onChange`
|
|
84
|
+
* or uncontrolled `defaultValue`); the draft lives in the native `<input>` (the
|
|
85
|
+
* source of truth for the in-progress text), read via a merged `ref`. `ref`
|
|
86
|
+
* forwards to the `<input>`.
|
|
87
|
+
*/
|
|
88
|
+
declare function TokenInput({ className, fieldClassName, wrapperClassName, value, defaultValue, onChange, separators, commitOnBlur, validate, label, optional, hint, error, disabled, placeholder, id: idProp, onKeyDown, onBlur, onInput, ref, ...props }: TokenInputProps): React.JSX.Element;
|
|
89
|
+
|
|
90
|
+
export { TokenInput, type TokenInputProps };
|