@nucleux/md3-structured-input 0.0.0-stage → 0.6.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nucleux
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,43 @@
1
- # Temporary Holding Version
1
+ # @nucleux/md3-structured-input
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Material 3 Structured Input — a compact form for exact values, suggestions marked and every value yours — Nucleux agentic UI for React.
4
+
5
+ Part of **[Nucleux](https://github.com/intelligaia-open-labs/nucleux)** — agentic UI components for React. Install a single component, or the full kit via [`@nucleux/react`](https://www.npmjs.com/package/@nucleux/react).
6
+
7
+ This is the Material 3 twin of [`@nucleux/structured-input`](https://www.npmjs.com/package/@nucleux/structured-input) (`StructuredInput`). It shares the twin's API exactly — the props types are the twin's own — so you can switch families by changing only the import. Only the look differs: MD3 colour roles, shape, elevation, state layers and motion (stilled under reduced motion).
8
+
9
+ ## Exports
10
+
11
+ - **`Md3StructuredInput`** — a clarifying question as MD3 radio / checkbox rows, free-text escape and skip
12
+ - **`Md3StructuredInputSlots`** — 2–4 labelled MD3 outlined fields; “Suggested” marks become “Edited by you”
13
+ - **`Md3StructuredInputTags`** — `@` / `p:` / `/` tags become tonal input chips, suggestions as MD3 suggestion chips
14
+ - **`Md3StructuredInputWizard`** — multi-step MD3 card with a segmented progress track
15
+ - **`structuredPartsToText`** — re-exported from `@nucleux/structured-input`
16
+ - Types: `Md3StructuredInputProps`, `Md3StructuredInputSlotsProps`, `Md3StructuredInputTagsProps`, `Md3StructuredInputWizardProps`, `StructuredOption`, `StructuredInputSlot`, `StructuredInputValues`, `StructuredInputTagTrigger`, `StructuredInputPart`
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pnpm add @nucleux/md3-structured-input @nucleux/tokens
22
+ ```
23
+
24
+ `react` / `react-dom` are peer dependencies.
25
+
26
+ ## Usage
27
+
28
+ ```tsx
29
+ import { Md3StructuredInput } from "@nucleux/md3-structured-input";
30
+
31
+ <Md3StructuredInput
32
+ question="Which audience is this for?"
33
+ options={[{ value: "execs", label: "Executives" }, { value: "eng", label: "Engineers" }]}
34
+ onSubmit={(values) => answer(values)}
35
+ />
36
+ ```
37
+
38
+ Requires the theme once in your app: `import "@nucleux/tokens/styles.css";`
39
+ and the Tailwind preset from `@nucleux/tokens/preset`.
40
+
41
+ ## License
42
+
43
+ MIT © Nucleux
package/dist/index.cjs ADDED
@@ -0,0 +1,562 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var React = require('react');
5
+ var utils = require('@nucleux/utils');
6
+ var md3Button = require('@nucleux/md3-button');
7
+ var md3Card = require('@nucleux/md3-card');
8
+ var md3Chip = require('@nucleux/md3-chip');
9
+ var structuredInput = require('@nucleux/structured-input');
10
+ var lucideReact = require('lucide-react');
11
+ var jsxRuntime = require('react/jsx-runtime');
12
+
13
+ function _interopNamespace(e) {
14
+ if (e && e.__esModule) return e;
15
+ var n = Object.create(null);
16
+ if (e) {
17
+ Object.keys(e).forEach(function (k) {
18
+ if (k !== 'default') {
19
+ var d = Object.getOwnPropertyDescriptor(e, k);
20
+ Object.defineProperty(n, k, d.get ? d : {
21
+ enumerable: true,
22
+ get: function () { return e[k]; }
23
+ });
24
+ }
25
+ });
26
+ }
27
+ n.default = e;
28
+ return Object.freeze(n);
29
+ }
30
+
31
+ var React__namespace = /*#__PURE__*/_interopNamespace(React);
32
+
33
+ // src/index.tsx
34
+ var stateLayer = "before:absolute before:inset-0 before:bg-current before:opacity-0 before:transition-opacity before:content-[''] hover:before:opacity-[0.08] focus-visible:before:opacity-[0.12] active:before:opacity-[0.12]";
35
+ var fieldClass = utils.cn(
36
+ "w-full rounded-md-xs border border-md-outline bg-transparent px-4 text-base text-md-on-surface outline-none transition-colors",
37
+ "placeholder:text-md-on-surface-variant hover:border-md-on-surface",
38
+ "focus:border-md-primary focus:ring-1 focus:ring-inset focus:ring-md-primary",
39
+ "disabled:cursor-not-allowed disabled:opacity-[0.38]"
40
+ );
41
+ var Md3StructuredInput = React__namespace.forwardRef(
42
+ ({
43
+ question,
44
+ options,
45
+ multiple = false,
46
+ value,
47
+ defaultValue = [],
48
+ onValueChange,
49
+ allowOther = false,
50
+ otherPlaceholder = "Something else\u2026",
51
+ onSubmit,
52
+ onSkip,
53
+ submitLabel = "Continue",
54
+ skipLabel = "Skip",
55
+ className,
56
+ ...props
57
+ }, ref) => {
58
+ const [internal, setInternal] = React__namespace.useState(defaultValue);
59
+ const [other, setOther] = React__namespace.useState("");
60
+ const questionId = React__namespace.useId();
61
+ const groupRef = React__namespace.useRef(null);
62
+ const selected = value !== void 0 ? value : internal;
63
+ const setSelected = (next) => {
64
+ if (value === void 0) setInternal(next);
65
+ onValueChange?.(next);
66
+ };
67
+ const toggle = (val) => {
68
+ if (multiple) {
69
+ setSelected(selected.includes(val) ? selected.filter((v) => v !== val) : [...selected, val]);
70
+ } else {
71
+ setSelected([val]);
72
+ }
73
+ };
74
+ return /* @__PURE__ */ jsxRuntime.jsxs(
75
+ md3Card.Md3Card,
76
+ {
77
+ ref,
78
+ variant: "outlined",
79
+ className: utils.cn("flex flex-col gap-4 rounded-md-lg p-5 animate-nx-fade-in motion-reduce:animate-none", className),
80
+ ...props,
81
+ children: [
82
+ /* @__PURE__ */ jsxRuntime.jsx("p", { id: questionId, className: "text-base font-medium tracking-[0.15px] text-md-on-surface", children: question }),
83
+ /* @__PURE__ */ jsxRuntime.jsx(
84
+ "div",
85
+ {
86
+ ref: groupRef,
87
+ role: multiple ? "group" : "radiogroup",
88
+ "aria-labelledby": questionId,
89
+ className: "flex flex-col gap-1",
90
+ onKeyDown: (e) => {
91
+ if (multiple || !["ArrowDown", "ArrowUp", "ArrowRight", "ArrowLeft"].includes(e.key)) return;
92
+ const radios = Array.from(groupRef.current?.querySelectorAll('[role="radio"]') ?? []);
93
+ const i = radios.indexOf(document.activeElement);
94
+ if (i < 0) return;
95
+ e.preventDefault();
96
+ const step = e.key === "ArrowDown" || e.key === "ArrowRight" ? 1 : -1;
97
+ const next = (i + step + radios.length) % radios.length;
98
+ radios[next].focus();
99
+ setSelected([options[next].value]);
100
+ },
101
+ children: options.map((option, index) => {
102
+ const active = selected.includes(option.value);
103
+ const tabbable = multiple || index === Math.max(0, options.findIndex((o) => selected.includes(o.value)));
104
+ return /* @__PURE__ */ jsxRuntime.jsxs(
105
+ "button",
106
+ {
107
+ type: "button",
108
+ role: multiple ? "checkbox" : "radio",
109
+ "aria-checked": active,
110
+ tabIndex: tabbable ? 0 : -1,
111
+ onClick: () => toggle(option.value),
112
+ className: utils.cn(
113
+ "relative flex min-h-12 items-center gap-4 overflow-hidden rounded-md-md px-3 py-2 text-left text-sm transition-colors duration-200 motion-reduce:transition-none",
114
+ stateLayer,
115
+ "outline-none focus-visible:ring-2 focus-visible:ring-md-primary",
116
+ active ? "bg-md-secondary-container text-md-on-secondary-container" : "text-md-on-surface"
117
+ ),
118
+ children: [
119
+ multiple ? /* @__PURE__ */ jsxRuntime.jsx(
120
+ "span",
121
+ {
122
+ className: utils.cn(
123
+ "relative inline-flex size-[18px] shrink-0 items-center justify-center rounded-[2px] border-2 transition-colors [&_svg]:size-3.5",
124
+ active ? "border-md-primary bg-md-primary text-md-on-primary" : "border-md-on-surface-variant"
125
+ ),
126
+ children: active && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { strokeWidth: 3, "aria-hidden": "true" })
127
+ }
128
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
129
+ "span",
130
+ {
131
+ className: utils.cn(
132
+ "relative inline-flex size-5 shrink-0 items-center justify-center rounded-full border-2 transition-colors",
133
+ active ? "border-md-primary" : "border-md-on-surface-variant"
134
+ ),
135
+ children: /* @__PURE__ */ jsxRuntime.jsx(
136
+ "span",
137
+ {
138
+ className: utils.cn(
139
+ "size-2.5 rounded-full bg-md-primary transition-transform duration-200 motion-reduce:transition-none",
140
+ active ? "scale-100" : "scale-0"
141
+ )
142
+ }
143
+ )
144
+ }
145
+ ),
146
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative flex-1", children: option.label })
147
+ ]
148
+ },
149
+ option.value
150
+ );
151
+ })
152
+ }
153
+ ),
154
+ allowOther && /* @__PURE__ */ jsxRuntime.jsx(
155
+ "input",
156
+ {
157
+ type: "text",
158
+ value: other,
159
+ onChange: (e) => setOther(e.target.value),
160
+ placeholder: otherPlaceholder,
161
+ "aria-label": otherPlaceholder,
162
+ className: utils.cn(fieldClass, "h-12")
163
+ }
164
+ ),
165
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-2", children: [
166
+ onSkip && /* @__PURE__ */ jsxRuntime.jsx(md3Button.Md3Button, { variant: "text", onClick: onSkip, children: skipLabel }),
167
+ onSubmit && /* @__PURE__ */ jsxRuntime.jsx(
168
+ md3Button.Md3Button,
169
+ {
170
+ variant: "filled",
171
+ onClick: () => onSubmit(selected, allowOther ? other : void 0),
172
+ disabled: selected.length === 0 && !(allowOther && other.trim()),
173
+ children: submitLabel
174
+ }
175
+ )
176
+ ] })
177
+ ]
178
+ }
179
+ );
180
+ }
181
+ );
182
+ Md3StructuredInput.displayName = "Md3StructuredInput";
183
+ var Md3StructuredInputSlots = React__namespace.forwardRef(
184
+ ({
185
+ slots,
186
+ values: valuesProp,
187
+ defaultValues,
188
+ onValuesChange,
189
+ freeform = false,
190
+ onSubmit,
191
+ submitLabel = "Generate",
192
+ label = "Structured prompt builder",
193
+ className,
194
+ ...props
195
+ }, ref) => {
196
+ const [internal, setInternal] = React__namespace.useState(defaultValues ?? {});
197
+ const values = valuesProp ?? internal;
198
+ const [edited, setEdited] = React__namespace.useState({});
199
+ const [extra, setExtra] = React__namespace.useState("");
200
+ const baseId = React__namespace.useId();
201
+ const setValue = (name, value) => {
202
+ const next = { ...values, [name]: value };
203
+ if (valuesProp === void 0) setInternal(next);
204
+ setEdited((e) => ({ ...e, [name]: true }));
205
+ onValuesChange?.(next);
206
+ };
207
+ const hasAny = Object.values(values).some((v) => v && v.trim()) || extra.trim().length > 0;
208
+ const freeformLabel = typeof freeform === "string" ? freeform : "Anything else?";
209
+ return /* @__PURE__ */ jsxRuntime.jsxs(
210
+ "form",
211
+ {
212
+ ref,
213
+ "aria-label": label,
214
+ className: utils.cn("grid grid-cols-1 gap-4 text-md-on-surface sm:grid-cols-2", className),
215
+ onSubmit: (e) => {
216
+ e.preventDefault();
217
+ if (hasAny) onSubmit?.(values, extra);
218
+ },
219
+ ...props,
220
+ children: [
221
+ slots.map((slot) => {
222
+ const id = `${baseId}-${slot.name}`;
223
+ const helperId = `${id}-helper`;
224
+ const isEdited = !!edited[slot.name];
225
+ const mark = slot.suggested ? isEdited ? "edited" : "suggested" : null;
226
+ const describedBy = slot.helper || mark === "suggested" ? helperId : void 0;
227
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: utils.cn("flex flex-col gap-1.5", slot.wide && "sm:col-span-2"), children: [
228
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-6 items-center gap-2", children: [
229
+ /* @__PURE__ */ jsxRuntime.jsx("label", { htmlFor: id, className: "text-xs font-medium tracking-[0.5px] text-md-on-surface-variant", children: slot.label }),
230
+ mark && /* @__PURE__ */ jsxRuntime.jsxs(
231
+ "span",
232
+ {
233
+ className: utils.cn(
234
+ "inline-flex h-6 items-center gap-1 rounded-md-sm px-2 text-[11px] font-medium tracking-[0.5px] [&_svg]:size-3.5",
235
+ mark === "suggested" ? "bg-md-primary-container text-md-on-primary-container" : "bg-md-surface-container-high text-md-on-surface-variant"
236
+ ),
237
+ children: [
238
+ mark === "suggested" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.UserPen, { "aria-hidden": "true" }),
239
+ mark === "suggested" ? "Suggested" : "Edited by you"
240
+ ]
241
+ }
242
+ )
243
+ ] }),
244
+ slot.options ? /* @__PURE__ */ jsxRuntime.jsxs(
245
+ "select",
246
+ {
247
+ id,
248
+ value: values[slot.name] ?? "",
249
+ "aria-describedby": describedBy,
250
+ onChange: (e) => setValue(slot.name, e.target.value),
251
+ className: utils.cn(fieldClass, "h-14 cursor-pointer", !values[slot.name] && "text-md-on-surface-variant"),
252
+ children: [
253
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "", disabled: true, children: slot.placeholder ?? "Choose\u2026" }),
254
+ slot.options.map((o) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: o.value, children: typeof o.label === "string" || typeof o.label === "number" ? o.label : o.value }, o.value))
255
+ ]
256
+ }
257
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
258
+ "input",
259
+ {
260
+ id,
261
+ type: "text",
262
+ value: values[slot.name] ?? "",
263
+ placeholder: slot.placeholder,
264
+ "aria-describedby": describedBy,
265
+ onChange: (e) => setValue(slot.name, e.target.value),
266
+ className: utils.cn(fieldClass, "h-14")
267
+ }
268
+ ),
269
+ describedBy && /* @__PURE__ */ jsxRuntime.jsxs("p", { id: helperId, className: "px-4 text-xs text-md-on-surface-variant", children: [
270
+ mark === "suggested" ? `Suggested ${slot.suggested}. ` : "",
271
+ slot.helper
272
+ ] })
273
+ ] }, slot.name);
274
+ }),
275
+ freeform && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1.5 sm:col-span-2", children: [
276
+ /* @__PURE__ */ jsxRuntime.jsx(
277
+ "label",
278
+ {
279
+ htmlFor: `${baseId}-freeform`,
280
+ className: "text-xs font-medium tracking-[0.5px] text-md-on-surface-variant",
281
+ children: freeformLabel
282
+ }
283
+ ),
284
+ /* @__PURE__ */ jsxRuntime.jsx(
285
+ "textarea",
286
+ {
287
+ id: `${baseId}-freeform`,
288
+ rows: 2,
289
+ value: extra,
290
+ placeholder: "Say it in your own words \u2014 this overrides the fields above",
291
+ onChange: (e) => setExtra(e.target.value),
292
+ className: utils.cn(fieldClass, "resize-none py-3")
293
+ }
294
+ )
295
+ ] }),
296
+ onSubmit && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end sm:col-span-2", children: /* @__PURE__ */ jsxRuntime.jsx(md3Button.Md3Button, { type: "submit", variant: "filled", disabled: !hasAny, children: submitLabel }) })
297
+ ]
298
+ }
299
+ );
300
+ }
301
+ );
302
+ Md3StructuredInputSlots.displayName = "Md3StructuredInputSlots";
303
+ var DEFAULT_TRIGGERS = [
304
+ { trigger: "@", label: "Person" },
305
+ { trigger: "p:", label: "Project" },
306
+ { trigger: "/", label: "File" }
307
+ ];
308
+ var TAG_TONES = [
309
+ "bg-md-primary-container text-md-on-primary-container",
310
+ "bg-md-secondary-container text-md-on-secondary-container",
311
+ "bg-md-tertiary-container text-md-on-tertiary-container",
312
+ "bg-md-surface-container-high text-md-on-surface"
313
+ ];
314
+ var Md3StructuredInputTags = React__namespace.forwardRef(
315
+ ({
316
+ triggers = DEFAULT_TRIGGERS,
317
+ value: valueProp,
318
+ defaultValue,
319
+ onValueChange,
320
+ onSubmit,
321
+ placeholder = "Type @ for people, p: for projects, / for files",
322
+ submitLabel = "Send",
323
+ className,
324
+ ...props
325
+ }, ref) => {
326
+ const [internal, setInternal] = React__namespace.useState(defaultValue ?? []);
327
+ const parts = valueProp ?? internal;
328
+ const [draft, setDraft] = React__namespace.useState("");
329
+ const inputRef = React__namespace.useRef(null);
330
+ const hintId = React__namespace.useId();
331
+ const commit = (next) => {
332
+ if (valueProp === void 0) setInternal(next);
333
+ onValueChange?.(next);
334
+ };
335
+ const ordered = [...triggers].sort((a, b) => b.trigger.length - a.trigger.length);
336
+ const matchTag = (word) => {
337
+ const t = ordered.find((tr) => word.startsWith(tr.trigger) && word.length > tr.trigger.length);
338
+ return t ? { trigger: t.trigger, value: word.slice(t.trigger.length) } : null;
339
+ };
340
+ const lastWord = draft.split(" ").pop() ?? "";
341
+ const activeTrigger = ordered.find((tr) => lastWord.startsWith(tr.trigger));
342
+ const query = activeTrigger ? lastWord.slice(activeTrigger.trigger.length).toLowerCase() : "";
343
+ const suggestions = activeTrigger?.suggestions?.filter((s) => s.toLowerCase().startsWith(query)).slice(0, 5) ?? [];
344
+ const flush = (source = draft, forceTag) => {
345
+ const words = source.split(" ");
346
+ const last = words.pop() ?? "";
347
+ const tag = forceTag ?? matchTag(last);
348
+ const textBefore = (tag ? words.join(" ") : source).trim();
349
+ const next = [...parts];
350
+ if (textBefore) next.push({ type: "text", text: textBefore });
351
+ if (tag) next.push({ type: "tag", ...tag });
352
+ if (next.length !== parts.length) commit(next);
353
+ setDraft("");
354
+ return next;
355
+ };
356
+ const toneFor = (trigger) => {
357
+ const i = triggers.findIndex((t) => t.trigger === trigger);
358
+ return TAG_TONES[(i < 0 ? 0 : i) % TAG_TONES.length];
359
+ };
360
+ const labelFor = (trigger) => triggers.find((t) => t.trigger === trigger)?.label ?? "Tag";
361
+ const submit = () => {
362
+ const next = flush();
363
+ const text = structuredInput.structuredPartsToText(next);
364
+ if (text) onSubmit?.(next, text);
365
+ };
366
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, className: utils.cn("flex flex-col gap-2 text-md-on-surface", className), ...props, children: [
367
+ /* @__PURE__ */ jsxRuntime.jsxs(
368
+ "div",
369
+ {
370
+ className: utils.cn(
371
+ "flex min-h-14 flex-wrap items-center gap-1.5 rounded-md-xs border border-md-outline bg-transparent px-3 py-2 text-base text-md-on-surface transition-colors",
372
+ "hover:border-md-on-surface focus-within:border-md-primary focus-within:ring-1 focus-within:ring-inset focus-within:ring-md-primary"
373
+ ),
374
+ onClick: () => inputRef.current?.focus(),
375
+ children: [
376
+ parts.map(
377
+ (part, i) => part.type === "text" ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: part.text }, i) : /* @__PURE__ */ jsxRuntime.jsxs(
378
+ "span",
379
+ {
380
+ className: utils.cn(
381
+ "inline-flex h-7 items-center gap-0.5 rounded-md-sm pl-2 pr-0.5 text-sm font-medium animate-nx-fade-in motion-reduce:animate-none",
382
+ toneFor(part.trigger)
383
+ ),
384
+ children: [
385
+ part.trigger,
386
+ part.value,
387
+ /* @__PURE__ */ jsxRuntime.jsx(
388
+ "button",
389
+ {
390
+ type: "button",
391
+ "aria-label": `Remove ${labelFor(part.trigger).toLowerCase()} ${part.trigger}${part.value}`,
392
+ onClick: (e) => {
393
+ e.stopPropagation();
394
+ commit(parts.filter((_, j) => j !== i));
395
+ inputRef.current?.focus();
396
+ },
397
+ className: utils.cn(
398
+ "relative inline-flex size-6 items-center justify-center overflow-hidden rounded-full [&_svg]:size-3.5",
399
+ stateLayer,
400
+ "outline-none focus-visible:ring-2 focus-visible:ring-md-primary"
401
+ ),
402
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "relative", "aria-hidden": "true" })
403
+ }
404
+ )
405
+ ]
406
+ },
407
+ i
408
+ )
409
+ ),
410
+ /* @__PURE__ */ jsxRuntime.jsx(
411
+ "input",
412
+ {
413
+ ref: inputRef,
414
+ type: "text",
415
+ value: draft,
416
+ "aria-label": placeholder,
417
+ "aria-describedby": hintId,
418
+ placeholder: parts.length ? void 0 : placeholder,
419
+ onChange: (e) => {
420
+ const v = e.target.value;
421
+ if (v.endsWith(" ") && matchTag(v.trimEnd().split(" ").pop() ?? "")) flush(v.trimEnd());
422
+ else setDraft(v);
423
+ },
424
+ onKeyDown: (e) => {
425
+ if (e.key === " " && matchTag(lastWord)) {
426
+ e.preventDefault();
427
+ flush();
428
+ } else if (e.key === "Enter") {
429
+ e.preventDefault();
430
+ submit();
431
+ } else if (e.key === "Backspace" && draft === "" && parts.length) {
432
+ e.preventDefault();
433
+ const last = parts[parts.length - 1];
434
+ commit(parts.slice(0, -1));
435
+ if (last.type === "text") setDraft(last.text);
436
+ }
437
+ },
438
+ className: "h-8 min-w-[8rem] flex-1 bg-transparent text-base text-md-on-surface outline-none placeholder:text-md-on-surface-variant"
439
+ }
440
+ )
441
+ ]
442
+ }
443
+ ),
444
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { id: hintId, className: "sr-only", children: [
445
+ triggers.map((t) => `${t.trigger} for ${t.label.toLowerCase()}`).join(", "),
446
+ ". Space turns a tag into a chip; Backspace removes the last one; Enter sends."
447
+ ] }),
448
+ activeTrigger && suggestions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { role: "group", "aria-label": `${activeTrigger.label} suggestions`, className: "flex flex-wrap gap-2", children: suggestions.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(
449
+ md3Chip.Md3Chip,
450
+ {
451
+ variant: "suggestion",
452
+ onClick: () => {
453
+ flush(draft, { trigger: activeTrigger.trigger, value: s });
454
+ inputRef.current?.focus();
455
+ },
456
+ children: [
457
+ activeTrigger.trigger,
458
+ s
459
+ ]
460
+ },
461
+ s
462
+ )) }),
463
+ onSubmit && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsxRuntime.jsx(md3Button.Md3Button, { variant: "filled", onClick: submit, disabled: parts.length === 0 && draft.trim() === "", children: submitLabel }) })
464
+ ] });
465
+ }
466
+ );
467
+ Md3StructuredInputTags.displayName = "Md3StructuredInputTags";
468
+ var Md3StructuredInputWizard = React__namespace.forwardRef(
469
+ ({
470
+ steps,
471
+ step: stepProp,
472
+ defaultStep = 0,
473
+ onStepChange,
474
+ description,
475
+ onSubmit,
476
+ submitLabel = "Generate",
477
+ allowEarlySubmit = true,
478
+ className,
479
+ children,
480
+ ...props
481
+ }, ref) => {
482
+ const [internal, setInternal] = React__namespace.useState(defaultStep);
483
+ const step = Math.min(Math.max(stepProp ?? internal, 0), Math.max(steps.length - 1, 0));
484
+ const headingRef = React__namespace.useRef(null);
485
+ const headingId = React__namespace.useId();
486
+ const moved = React__namespace.useRef(false);
487
+ React__namespace.useEffect(() => {
488
+ if (moved.current) headingRef.current?.focus();
489
+ }, [step]);
490
+ const go = (next) => {
491
+ moved.current = true;
492
+ if (stepProp === void 0) setInternal(next);
493
+ onStepChange?.(next);
494
+ };
495
+ const last = step === steps.length - 1;
496
+ return /* @__PURE__ */ jsxRuntime.jsxs(
497
+ md3Card.Md3Card,
498
+ {
499
+ ref,
500
+ variant: "outlined",
501
+ role: "group",
502
+ "aria-labelledby": headingId,
503
+ className: utils.cn("flex flex-col gap-3 rounded-md-lg p-4", className),
504
+ ...props,
505
+ children: [
506
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-1", "aria-hidden": "true", children: steps.map((_, i) => /* @__PURE__ */ jsxRuntime.jsx(
507
+ "span",
508
+ {
509
+ className: utils.cn(
510
+ "h-1 flex-1 rounded-full transition-colors duration-300 motion-reduce:transition-none",
511
+ i <= step ? "bg-md-primary" : "bg-md-secondary-container"
512
+ )
513
+ },
514
+ i
515
+ )) }),
516
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
517
+ /* @__PURE__ */ jsxRuntime.jsxs(
518
+ "p",
519
+ {
520
+ ref: headingRef,
521
+ id: headingId,
522
+ tabIndex: -1,
523
+ className: "rounded-md-xs text-sm font-medium tracking-[0.1px] text-md-on-surface outline-none focus-visible:ring-2 focus-visible:ring-md-primary",
524
+ children: [
525
+ "Step ",
526
+ step + 1,
527
+ " of ",
528
+ steps.length,
529
+ " \xB7 ",
530
+ steps[step]
531
+ ]
532
+ }
533
+ ),
534
+ description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-md-on-surface-variant", children: description })
535
+ ] }),
536
+ children,
537
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
538
+ step > 0 && /* @__PURE__ */ jsxRuntime.jsx(md3Button.Md3Button, { variant: "text", onClick: () => go(step - 1), children: "Back" }),
539
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1" }),
540
+ !last && allowEarlySubmit && onSubmit && /* @__PURE__ */ jsxRuntime.jsxs(md3Button.Md3Button, { variant: "text", onClick: onSubmit, children: [
541
+ submitLabel,
542
+ " now"
543
+ ] }),
544
+ last ? onSubmit && /* @__PURE__ */ jsxRuntime.jsx(md3Button.Md3Button, { variant: "filled", onClick: onSubmit, children: submitLabel }) : /* @__PURE__ */ jsxRuntime.jsx(md3Button.Md3Button, { variant: "filled", onClick: () => go(step + 1), children: "Next" })
545
+ ] })
546
+ ]
547
+ }
548
+ );
549
+ }
550
+ );
551
+ Md3StructuredInputWizard.displayName = "Md3StructuredInputWizard";
552
+
553
+ Object.defineProperty(exports, "structuredPartsToText", {
554
+ enumerable: true,
555
+ get: function () { return structuredInput.structuredPartsToText; }
556
+ });
557
+ exports.Md3StructuredInput = Md3StructuredInput;
558
+ exports.Md3StructuredInputSlots = Md3StructuredInputSlots;
559
+ exports.Md3StructuredInputTags = Md3StructuredInputTags;
560
+ exports.Md3StructuredInputWizard = Md3StructuredInputWizard;
561
+ //# sourceMappingURL=index.cjs.map
562
+ //# sourceMappingURL=index.cjs.map