@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
|
@@ -0,0 +1,408 @@
|
|
|
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/search-bar/SearchBar.tsx
|
|
31
|
+
function cn(...inputs) {
|
|
32
|
+
return tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
33
|
+
}
|
|
34
|
+
function useDebouncedCallback(fn, delayMs) {
|
|
35
|
+
const fnRef = React__namespace.useRef(fn);
|
|
36
|
+
const delayRef = React__namespace.useRef(delayMs);
|
|
37
|
+
fnRef.current = fn;
|
|
38
|
+
delayRef.current = delayMs;
|
|
39
|
+
const timerRef = React__namespace.useRef(null);
|
|
40
|
+
const debounced = React__namespace.useMemo(() => {
|
|
41
|
+
const clear = () => {
|
|
42
|
+
if (timerRef.current !== null) {
|
|
43
|
+
clearTimeout(timerRef.current);
|
|
44
|
+
timerRef.current = null;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const call = (...args) => {
|
|
48
|
+
clear();
|
|
49
|
+
timerRef.current = setTimeout(() => {
|
|
50
|
+
timerRef.current = null;
|
|
51
|
+
fnRef.current(...args);
|
|
52
|
+
}, delayRef.current);
|
|
53
|
+
};
|
|
54
|
+
call.cancel = clear;
|
|
55
|
+
call.flush = (...args) => {
|
|
56
|
+
clear();
|
|
57
|
+
fnRef.current(...args);
|
|
58
|
+
};
|
|
59
|
+
return call;
|
|
60
|
+
}, []);
|
|
61
|
+
React__namespace.useEffect(() => {
|
|
62
|
+
return () => {
|
|
63
|
+
if (timerRef.current !== null) {
|
|
64
|
+
clearTimeout(timerRef.current);
|
|
65
|
+
timerRef.current = null;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}, []);
|
|
69
|
+
return debounced;
|
|
70
|
+
}
|
|
71
|
+
var inputVariants = classVarianceAuthority.cva(
|
|
72
|
+
[
|
|
73
|
+
// Field row layout — icons + input on one line, vertically centred.
|
|
74
|
+
"flex w-full items-center",
|
|
75
|
+
// Colour transitions for the focus border change.
|
|
76
|
+
"transition-colors",
|
|
77
|
+
// I-beam text cursor over the WHOLE field row (not just the inner input),
|
|
78
|
+
// so clicking anywhere in the padding/icon area reads as "type here". This
|
|
79
|
+
// is the only hover affordance — hover intentionally does NOT recolour the
|
|
80
|
+
// border (user-authorized deviation from the Figma). `cursor-text` sits in
|
|
81
|
+
// the base; `has-[:disabled]:cursor-not-allowed` below is declared AFTER it
|
|
82
|
+
// so it wins on a disabled field (same-specificity utilities, later class
|
|
83
|
+
// takes precedence — confirmed by tailwind-merge ordering).
|
|
84
|
+
"cursor-text",
|
|
85
|
+
// Disabled: not-allowed cursor + block pointer interaction on the whole
|
|
86
|
+
// field (the native `disabled` on the input already stops typing; icons are
|
|
87
|
+
// dimmed separately by the `disabled`-derived `iconStateClass` in <Input>).
|
|
88
|
+
// Declared after `cursor-text` so it overrides the I-beam when disabled.
|
|
89
|
+
"has-[:disabled]:cursor-not-allowed"
|
|
90
|
+
],
|
|
91
|
+
{
|
|
92
|
+
variants: {
|
|
93
|
+
variation: {
|
|
94
|
+
field: [
|
|
95
|
+
"bg-input-background border border-input-border rounded-sm",
|
|
96
|
+
// Focus (mouse or keyboard) strengthens the whole border to the
|
|
97
|
+
// accent. Hover does NOT change colour (cursor-only affordance).
|
|
98
|
+
"focus-within:border-input-border-accent"
|
|
99
|
+
],
|
|
100
|
+
line: [
|
|
101
|
+
"border-b border-input-border",
|
|
102
|
+
// Focus (mouse or keyboard) strengthens the bottom line to the
|
|
103
|
+
// accent. Hover does NOT change colour (cursor-only affordance).
|
|
104
|
+
"focus-within:border-input-border-accent"
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
size: {
|
|
108
|
+
// sm/lg heights reuse the Button scale; gap follows Figma per size.
|
|
109
|
+
sm: "h-8 gap-1",
|
|
110
|
+
lg: "h-10 gap-2"
|
|
111
|
+
},
|
|
112
|
+
error: {
|
|
113
|
+
// Error out-ranks focus: declare the destructive border for the resting
|
|
114
|
+
// state AND its `focus-within:` form so an errored field never recolours
|
|
115
|
+
// to the accent when focused (a bare `border-…` alone would lose to the
|
|
116
|
+
// variation's `focus-within:` accent utility above). No `hover:` form is
|
|
117
|
+
// needed — hover no longer changes colour on any variation. The `error`
|
|
118
|
+
// variant's classes are emitted after the variation's, so at equal
|
|
119
|
+
// specificity the destructive `focus-within:` wins by order.
|
|
120
|
+
true: "border-input-destructive-foreground-accent focus-within:border-input-destructive-foreground-accent",
|
|
121
|
+
false: ""
|
|
122
|
+
},
|
|
123
|
+
// Bottom-line visibility toggle. LINE ONLY — the hide effect is applied
|
|
124
|
+
// via a compound variant below (scoped to `variation:line`), so `field`
|
|
125
|
+
// is unaffected. Boolean → `true`/`false`; `true` (default) keeps today's
|
|
126
|
+
// behaviour. No classes here: `true` is the resting `border-input-border`
|
|
127
|
+
// from the variation, and `false`'s hide is the compound variant.
|
|
128
|
+
showBottomLine: {
|
|
129
|
+
true: "",
|
|
130
|
+
false: ""
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
compoundVariants: [
|
|
134
|
+
// field padding — Figma field-lg = px-3 (12px) / py-2 (8px).
|
|
135
|
+
{ variation: "field", size: "lg", className: "px-3 py-2" },
|
|
136
|
+
// field-sm: DERIVED. px-2 (8px = spacing/2) steps down from field-lg's
|
|
137
|
+
// px-3 (12px) and equals py-2 for a uniform spacing/2 inset all around.
|
|
138
|
+
{ variation: "field", size: "sm", className: "px-2 py-2" },
|
|
139
|
+
// line — no horizontal padding (Figma px-0); vertical padding per size.
|
|
140
|
+
{ variation: "line", size: "lg", className: "py-2" },
|
|
141
|
+
{ variation: "line", size: "sm", className: "py-2.5" },
|
|
142
|
+
// Hide the resting bottom line (LINE ONLY): recolour to transparent while
|
|
143
|
+
// keeping `border-b` for a stable 1px width (no layout shift). Gated on
|
|
144
|
+
// `error:false` so an errored field keeps its destructive rest border
|
|
145
|
+
// (transparent would otherwise last-win over it). Focus still wins:
|
|
146
|
+
// `focus-within:border-input-border-accent` out-ranks `border-transparent`
|
|
147
|
+
// (later in class order), so the line reappears on focus. `field` is
|
|
148
|
+
// untouched — this compound only matches `variation:line`.
|
|
149
|
+
{
|
|
150
|
+
variation: "line",
|
|
151
|
+
showBottomLine: false,
|
|
152
|
+
error: false,
|
|
153
|
+
className: "border-transparent"
|
|
154
|
+
}
|
|
155
|
+
],
|
|
156
|
+
defaultVariants: {
|
|
157
|
+
variation: "field",
|
|
158
|
+
size: "lg",
|
|
159
|
+
error: false,
|
|
160
|
+
showBottomLine: true
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
function Input({
|
|
165
|
+
className,
|
|
166
|
+
fieldClassName,
|
|
167
|
+
wrapperClassName,
|
|
168
|
+
variation = "field",
|
|
169
|
+
size = "lg",
|
|
170
|
+
error = false,
|
|
171
|
+
// LINE-only bottom-line toggle. Destructured OUT of `props` so it never
|
|
172
|
+
// spreads onto the native <input> (invalid DOM attribute → React warning).
|
|
173
|
+
showBottomLine = true,
|
|
174
|
+
label,
|
|
175
|
+
optional = false,
|
|
176
|
+
hint,
|
|
177
|
+
leftSlot,
|
|
178
|
+
rightSlot,
|
|
179
|
+
disabled,
|
|
180
|
+
id: idProp,
|
|
181
|
+
ref,
|
|
182
|
+
...props
|
|
183
|
+
}) {
|
|
184
|
+
const generatedId = React__namespace.useId();
|
|
185
|
+
const id = idProp ?? generatedId;
|
|
186
|
+
const hintId = hint ? `${id}-hint` : void 0;
|
|
187
|
+
const iconStateClass = cn(
|
|
188
|
+
error ? "text-input-destructive-foreground-accent" : "text-input-foreground",
|
|
189
|
+
disabled && "text-input-foreground-muted"
|
|
190
|
+
);
|
|
191
|
+
const HintIcon = error ? react.WarningIcon : react.InfoIcon;
|
|
192
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("flex flex-col gap-1.5", wrapperClassName), children: [
|
|
193
|
+
label != null && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
194
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
195
|
+
"label",
|
|
196
|
+
{
|
|
197
|
+
htmlFor: id,
|
|
198
|
+
"data-slot": "input-label",
|
|
199
|
+
className: cn(
|
|
200
|
+
"caption-03-medium uppercase",
|
|
201
|
+
error ? "text-input-destructive-foreground-accent" : "text-input-foreground",
|
|
202
|
+
disabled && "text-input-foreground-muted"
|
|
203
|
+
),
|
|
204
|
+
children: label
|
|
205
|
+
}
|
|
206
|
+
),
|
|
207
|
+
optional && /* @__PURE__ */ jsxRuntime.jsx(
|
|
208
|
+
"span",
|
|
209
|
+
{
|
|
210
|
+
"data-slot": "input-optional",
|
|
211
|
+
className: "body-04 text-input-foreground-muted",
|
|
212
|
+
children: "Optional"
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
] }),
|
|
216
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
217
|
+
"div",
|
|
218
|
+
{
|
|
219
|
+
"data-slot": "input-field",
|
|
220
|
+
"data-variation": variation,
|
|
221
|
+
"data-error": error || void 0,
|
|
222
|
+
className: cn(
|
|
223
|
+
inputVariants({ variation, size, error, showBottomLine }),
|
|
224
|
+
// Icon slots inherit the shared state colour; 16px default size.
|
|
225
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
226
|
+
iconStateClass,
|
|
227
|
+
fieldClassName
|
|
228
|
+
),
|
|
229
|
+
children: [
|
|
230
|
+
leftSlot != null && /* @__PURE__ */ jsxRuntime.jsx(
|
|
231
|
+
"span",
|
|
232
|
+
{
|
|
233
|
+
"data-slot": "input-leading",
|
|
234
|
+
"aria-hidden": "true",
|
|
235
|
+
className: "flex items-center",
|
|
236
|
+
children: leftSlot
|
|
237
|
+
}
|
|
238
|
+
),
|
|
239
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
240
|
+
"input",
|
|
241
|
+
{
|
|
242
|
+
id,
|
|
243
|
+
"data-slot": "input",
|
|
244
|
+
disabled,
|
|
245
|
+
"aria-invalid": error || void 0,
|
|
246
|
+
"aria-describedby": hintId,
|
|
247
|
+
className: cn(
|
|
248
|
+
// Typography (Figma `body-03`) + fill the row. `min-w-0` lets the
|
|
249
|
+
// input shrink inside the flex row instead of overflowing.
|
|
250
|
+
"body-03 w-full min-w-0 bg-transparent outline-none",
|
|
251
|
+
// Typed text + caret colour (Figma `input-foreground-accent`).
|
|
252
|
+
"text-input-foreground-accent caret-input-foreground-accent",
|
|
253
|
+
// Placeholder colour (Figma `input-foreground-muted`).
|
|
254
|
+
"placeholder:text-input-foreground-muted",
|
|
255
|
+
// Disabled text goes muted; keep the not-allowed cursor.
|
|
256
|
+
"disabled:cursor-not-allowed disabled:text-input-foreground-muted",
|
|
257
|
+
className
|
|
258
|
+
),
|
|
259
|
+
ref,
|
|
260
|
+
...props
|
|
261
|
+
}
|
|
262
|
+
),
|
|
263
|
+
rightSlot != null && // NOT aria-hidden: the trailing slot may host an interactive control
|
|
264
|
+
// (e.g. SearchBar's clear button) that must stay reachable by AT +
|
|
265
|
+
// keyboard, and a focusable element inside aria-hidden is an ARIA
|
|
266
|
+
// violation. Decorative bare-<svg> icons are silent to AT on their own
|
|
267
|
+
// (no role / no accessible name), so dropping aria-hidden here does not
|
|
268
|
+
// make them announced. The leading slot stays decorative (aria-hidden).
|
|
269
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { "data-slot": "input-trailing", className: "flex items-center", children: rightSlot })
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
),
|
|
273
|
+
hint != null && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
274
|
+
"p",
|
|
275
|
+
{
|
|
276
|
+
id: hintId,
|
|
277
|
+
"data-slot": "input-hint",
|
|
278
|
+
className: cn(
|
|
279
|
+
"body-04 flex items-center gap-1",
|
|
280
|
+
error ? "text-input-destructive-foreground-accent" : "text-input-foreground",
|
|
281
|
+
disabled && "text-input-foreground-muted"
|
|
282
|
+
),
|
|
283
|
+
children: [
|
|
284
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
285
|
+
HintIcon,
|
|
286
|
+
{
|
|
287
|
+
"aria-hidden": "true",
|
|
288
|
+
weight: "regular",
|
|
289
|
+
className: "size-4 shrink-0"
|
|
290
|
+
}
|
|
291
|
+
),
|
|
292
|
+
hint
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
] });
|
|
297
|
+
}
|
|
298
|
+
var DEFAULT_DEBOUNCE_MS = 300;
|
|
299
|
+
function SearchBar({
|
|
300
|
+
onSearch,
|
|
301
|
+
debounceMs = DEFAULT_DEBOUNCE_MS,
|
|
302
|
+
hint,
|
|
303
|
+
wrapperClassName,
|
|
304
|
+
fieldClassName,
|
|
305
|
+
className,
|
|
306
|
+
disabled,
|
|
307
|
+
value,
|
|
308
|
+
defaultValue,
|
|
309
|
+
onChange,
|
|
310
|
+
onKeyDown,
|
|
311
|
+
ref,
|
|
312
|
+
...props
|
|
313
|
+
}) {
|
|
314
|
+
const isControlled = value !== void 0;
|
|
315
|
+
const innerRef = React__namespace.useRef(null);
|
|
316
|
+
const setInputRef = React__namespace.useCallback(
|
|
317
|
+
(node) => {
|
|
318
|
+
innerRef.current = node;
|
|
319
|
+
if (typeof ref === "function") ref(node);
|
|
320
|
+
else if (ref)
|
|
321
|
+
ref.current = node;
|
|
322
|
+
},
|
|
323
|
+
[ref]
|
|
324
|
+
);
|
|
325
|
+
const [hasValueState, setHasValueState] = React__namespace.useState(
|
|
326
|
+
() => String(value ?? defaultValue ?? "").length > 0
|
|
327
|
+
);
|
|
328
|
+
const hasValue = isControlled ? String(value ?? "").length > 0 : hasValueState;
|
|
329
|
+
const debouncedSearch = useDebouncedCallback((next) => {
|
|
330
|
+
onSearch?.(next);
|
|
331
|
+
}, debounceMs);
|
|
332
|
+
const handleChange = (event) => {
|
|
333
|
+
onChange?.(event);
|
|
334
|
+
if (!isControlled) setHasValueState(event.currentTarget.value.length > 0);
|
|
335
|
+
debouncedSearch(event.currentTarget.value);
|
|
336
|
+
};
|
|
337
|
+
const handleKeyDown = (event) => {
|
|
338
|
+
onKeyDown?.(event);
|
|
339
|
+
if (event.key === "Enter") {
|
|
340
|
+
debouncedSearch.flush(event.currentTarget.value);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
const handleClear = () => {
|
|
344
|
+
const input = innerRef.current;
|
|
345
|
+
if (!input) return;
|
|
346
|
+
const setter = Object.getOwnPropertyDescriptor(
|
|
347
|
+
window.HTMLInputElement.prototype,
|
|
348
|
+
"value"
|
|
349
|
+
)?.set;
|
|
350
|
+
setter?.call(input, "");
|
|
351
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
352
|
+
if (!isControlled) setHasValueState(false);
|
|
353
|
+
debouncedSearch.cancel();
|
|
354
|
+
onSearch?.("");
|
|
355
|
+
input.focus();
|
|
356
|
+
};
|
|
357
|
+
const showClear = hasValue && !disabled;
|
|
358
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
359
|
+
Input,
|
|
360
|
+
{
|
|
361
|
+
type: "search",
|
|
362
|
+
variation: "field",
|
|
363
|
+
size: "lg",
|
|
364
|
+
ref: setInputRef,
|
|
365
|
+
disabled,
|
|
366
|
+
hint,
|
|
367
|
+
value,
|
|
368
|
+
defaultValue,
|
|
369
|
+
onChange: handleChange,
|
|
370
|
+
onKeyDown: handleKeyDown,
|
|
371
|
+
className: cn(
|
|
372
|
+
// Suppress the browser's native `type=search` clear affordance
|
|
373
|
+
// (WebKit/Chrome render `::-webkit-search-cancel-button`) — SearchBar
|
|
374
|
+
// provides its own clear button, so the native one would show a second
|
|
375
|
+
// "X". `hidden` (display:none) is used rather than `appearance-none`:
|
|
376
|
+
// that pseudo-element is governed by `-webkit-appearance`, which the
|
|
377
|
+
// unprefixed `appearance:none` Tailwind emits does not affect.
|
|
378
|
+
// Caller `className` is merged after and can still override.
|
|
379
|
+
"[&::-webkit-search-cancel-button]:hidden",
|
|
380
|
+
className
|
|
381
|
+
),
|
|
382
|
+
wrapperClassName,
|
|
383
|
+
fieldClassName,
|
|
384
|
+
leftSlot: /* @__PURE__ */ jsxRuntime.jsx(react.MagnifyingGlassIcon, { weight: "regular" }),
|
|
385
|
+
rightSlot: showClear ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
386
|
+
"button",
|
|
387
|
+
{
|
|
388
|
+
type: "button",
|
|
389
|
+
"aria-label": "Clear search",
|
|
390
|
+
onClick: handleClear,
|
|
391
|
+
"data-slot": "search-clear",
|
|
392
|
+
className: cn(
|
|
393
|
+
// Reset native button chrome; inherit the field's icon colour.
|
|
394
|
+
"inline-flex cursor-pointer items-center justify-center rounded-xs bg-transparent p-0 text-current",
|
|
395
|
+
// Standard ADR-0010 system focus ring (this button is a momentary
|
|
396
|
+
// control — the ADR-0011 no-ring exception applies only to the
|
|
397
|
+
// text input itself, NOT to this button).
|
|
398
|
+
"outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-focus"
|
|
399
|
+
),
|
|
400
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(react.XIcon, { weight: "regular", "aria-hidden": "true" })
|
|
401
|
+
}
|
|
402
|
+
) : void 0,
|
|
403
|
+
...props
|
|
404
|
+
}
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
exports.SearchBar = SearchBar;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
interface SearchBarProps extends Omit<React.ComponentProps<"input">, "size"> {
|
|
4
|
+
/**
|
|
5
|
+
* Fired with the current value, **debounced** by `debounceMs` (default 300).
|
|
6
|
+
* Runs ALONGSIDE the native `onChange` (which still fires immediately per
|
|
7
|
+
* keystroke — ADR-0010: the native input is the source of truth). Also fired
|
|
8
|
+
* immediately (debounce cancelled) on clear (`""`) and on Enter.
|
|
9
|
+
*/
|
|
10
|
+
onSearch?: (value: string) => void;
|
|
11
|
+
/** Debounce delay for `onSearch`, in milliseconds. Default `300`. */
|
|
12
|
+
debounceMs?: number;
|
|
13
|
+
/** Helper text below the field (forwarded to Input's `hint`). */
|
|
14
|
+
hint?: React.ReactNode;
|
|
15
|
+
/** Class names for Input's outer wrapper (the vertical field/hint stack). */
|
|
16
|
+
wrapperClassName?: string;
|
|
17
|
+
/** Class names for Input's field row (border/background box), not the `<input>`. */
|
|
18
|
+
fieldClassName?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* SearchBar — a search field composed from `<Input>` (it renders Input; it does
|
|
22
|
+
* NOT fork Input's markup). Fixed to Input's `field` variation at `lg` size (no
|
|
23
|
+
* `label`), with a leading MagnifyingGlass icon and a trailing clear (X) button
|
|
24
|
+
* that appears only when there's a value.
|
|
25
|
+
*
|
|
26
|
+
* Value model (ADR-0010): the native `<input>` is the single source of truth.
|
|
27
|
+
* Use it uncontrolled (`defaultValue`) or controlled (`value` + `onChange`);
|
|
28
|
+
* `onChange` fires immediately on every keystroke and is never replaced.
|
|
29
|
+
* `onSearch` is a debounced convenience on top, fired ALONGSIDE `onChange`.
|
|
30
|
+
*
|
|
31
|
+
* Clear-button visibility is keyed off whether the field currently has a value.
|
|
32
|
+
* We track that as a minimal boolean UI flag (`hasValue`) — NOT a mirror of the
|
|
33
|
+
* value string: the actual text is always read from the input via `ref` (for
|
|
34
|
+
* `onSearch`/flush), so the native input stays the source of truth. For
|
|
35
|
+
* controlled use the flag is derived from the `value` prop each render; for
|
|
36
|
+
* uncontrolled use it seeds from `defaultValue` and updates on each input event.
|
|
37
|
+
*
|
|
38
|
+
* `ref` (a normal React 19 prop) forwards to the underlying `<input>`.
|
|
39
|
+
*/
|
|
40
|
+
declare function SearchBar({ onSearch, debounceMs, hint, wrapperClassName, fieldClassName, className, disabled, value, defaultValue, onChange, onKeyDown, ref, ...props }: SearchBarProps): React.JSX.Element;
|
|
41
|
+
|
|
42
|
+
export { SearchBar, type SearchBarProps };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
interface SearchBarProps extends Omit<React.ComponentProps<"input">, "size"> {
|
|
4
|
+
/**
|
|
5
|
+
* Fired with the current value, **debounced** by `debounceMs` (default 300).
|
|
6
|
+
* Runs ALONGSIDE the native `onChange` (which still fires immediately per
|
|
7
|
+
* keystroke — ADR-0010: the native input is the source of truth). Also fired
|
|
8
|
+
* immediately (debounce cancelled) on clear (`""`) and on Enter.
|
|
9
|
+
*/
|
|
10
|
+
onSearch?: (value: string) => void;
|
|
11
|
+
/** Debounce delay for `onSearch`, in milliseconds. Default `300`. */
|
|
12
|
+
debounceMs?: number;
|
|
13
|
+
/** Helper text below the field (forwarded to Input's `hint`). */
|
|
14
|
+
hint?: React.ReactNode;
|
|
15
|
+
/** Class names for Input's outer wrapper (the vertical field/hint stack). */
|
|
16
|
+
wrapperClassName?: string;
|
|
17
|
+
/** Class names for Input's field row (border/background box), not the `<input>`. */
|
|
18
|
+
fieldClassName?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* SearchBar — a search field composed from `<Input>` (it renders Input; it does
|
|
22
|
+
* NOT fork Input's markup). Fixed to Input's `field` variation at `lg` size (no
|
|
23
|
+
* `label`), with a leading MagnifyingGlass icon and a trailing clear (X) button
|
|
24
|
+
* that appears only when there's a value.
|
|
25
|
+
*
|
|
26
|
+
* Value model (ADR-0010): the native `<input>` is the single source of truth.
|
|
27
|
+
* Use it uncontrolled (`defaultValue`) or controlled (`value` + `onChange`);
|
|
28
|
+
* `onChange` fires immediately on every keystroke and is never replaced.
|
|
29
|
+
* `onSearch` is a debounced convenience on top, fired ALONGSIDE `onChange`.
|
|
30
|
+
*
|
|
31
|
+
* Clear-button visibility is keyed off whether the field currently has a value.
|
|
32
|
+
* We track that as a minimal boolean UI flag (`hasValue`) — NOT a mirror of the
|
|
33
|
+
* value string: the actual text is always read from the input via `ref` (for
|
|
34
|
+
* `onSearch`/flush), so the native input stays the source of truth. For
|
|
35
|
+
* controlled use the flag is derived from the `value` prop each render; for
|
|
36
|
+
* uncontrolled use it seeds from `defaultValue` and updates on each input event.
|
|
37
|
+
*
|
|
38
|
+
* `ref` (a normal React 19 prop) forwards to the underlying `<input>`.
|
|
39
|
+
*/
|
|
40
|
+
declare function SearchBar({ onSearch, debounceMs, hint, wrapperClassName, fieldClassName, className, disabled, value, defaultValue, onChange, onKeyDown, ref, ...props }: SearchBarProps): React.JSX.Element;
|
|
41
|
+
|
|
42
|
+
export { SearchBar, type SearchBarProps };
|