@nucleux/structured-input 0.5.0 → 0.7.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/dist/index.d.cts CHANGED
@@ -34,9 +34,126 @@ interface StructuredInputProps extends Omit<React.HTMLAttributes<HTMLDivElement>
34
34
  }
35
35
  /**
36
36
  * An agent-generated clarifying question rendered as a structured choice —
37
- * single or multi-select options, a free-text escape, and a skip.
37
+ * single or multi-select options, a free-text escape, and a skip. The other
38
+ * structured-input variants are composable siblings: {@link StructuredInputSlots}
39
+ * (named slots), {@link StructuredInputTags} (tagged input) and
40
+ * {@link StructuredInputWizard} (wizard step).
38
41
  * Source: Figma "Expressive Input / Structured Input".
39
42
  */
40
43
  declare const StructuredInput: React.ForwardRefExoticComponent<StructuredInputProps & React.RefAttributes<HTMLDivElement>>;
44
+ interface StructuredInputSlot {
45
+ /** Key in the values object. */
46
+ name: string;
47
+ /** Plain-language label, e.g. `"Audience"`. */
48
+ label: string;
49
+ /** Placeholder / example value. */
50
+ placeholder?: string;
51
+ /** Choices — renders a select instead of a text field. */
52
+ options?: StructuredOption[];
53
+ /** Helper text under the field. */
54
+ helper?: string;
55
+ /**
56
+ * Marks the initial value as the agent's suggestion, with the reason, e.g.
57
+ * `"from the meeting invite"`. Once the person changes it, it reads “Edited by you”.
58
+ */
59
+ suggested?: string;
60
+ /** Span the full row. */
61
+ wide?: boolean;
62
+ }
63
+ type StructuredInputValues = Record<string, string>;
64
+ interface StructuredInputSlotsProps extends Omit<React.HTMLAttributes<HTMLFormElement>, "onSubmit" | "defaultValue"> {
65
+ /** The 2–4 slots the prompt template needs. */
66
+ slots: StructuredInputSlot[];
67
+ /** Values (controlled). */
68
+ values?: StructuredInputValues;
69
+ /** Initial values (uncontrolled). */
70
+ defaultValues?: StructuredInputValues;
71
+ /** Called on every change. */
72
+ onValuesChange?: (values: StructuredInputValues) => void;
73
+ /** Show a freeform override field. `true` uses the label `"Anything else?"`; a string sets the label. */
74
+ freeform?: boolean | string;
75
+ /** Called on submit. Never requires every slot — any value (or the freeform text) is enough. */
76
+ onSubmit?: (values: StructuredInputValues, freeform: string) => void;
77
+ /** Label of the submit button. Defaults to `"Generate"`. */
78
+ submitLabel?: React.ReactNode;
79
+ /** Accessible name of the form. Defaults to `"Structured prompt builder"`. */
80
+ label?: string;
81
+ }
82
+ /**
83
+ * Named slots: a short set of plain-language fields (2–4) that fill a prompt
84
+ * template — text fields or selects, an optional freeform override, and a
85
+ * submit that never requires every field. Agent-suggested values are marked
86
+ * “Suggested” with their reason and become “Edited by you” once changed; the
87
+ * component never changes a value the person set.
88
+ */
89
+ declare const StructuredInputSlots: React.ForwardRefExoticComponent<StructuredInputSlotsProps & React.RefAttributes<HTMLFormElement>>;
90
+ interface StructuredInputTagTrigger {
91
+ /** The prefix that starts a tag, e.g. `"@"`, `"p:"`, `"/"`. */
92
+ trigger: string;
93
+ /** What the tag refers to, e.g. `"Person"`. */
94
+ label: string;
95
+ /** Values offered while typing after the trigger. */
96
+ suggestions?: string[];
97
+ }
98
+ type StructuredInputPart = {
99
+ type: "text";
100
+ text: string;
101
+ } | {
102
+ type: "tag";
103
+ trigger: string;
104
+ value: string;
105
+ };
106
+ interface StructuredInputTagsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSubmit" | "defaultValue" | "onChange"> {
107
+ /** Recognised tag prefixes. Defaults to `@` person, `p:` project, `/` file. */
108
+ triggers?: StructuredInputTagTrigger[];
109
+ /** Committed parts (controlled). */
110
+ value?: StructuredInputPart[];
111
+ /** Initial parts (uncontrolled). */
112
+ defaultValue?: StructuredInputPart[];
113
+ /** Called when parts are added or removed. */
114
+ onValueChange?: (parts: StructuredInputPart[]) => void;
115
+ /** Called on Enter or the send button with the parts and their plain-text form. */
116
+ onSubmit?: (parts: StructuredInputPart[], text: string) => void;
117
+ /** Accessible name and placeholder of the text field. */
118
+ placeholder?: string;
119
+ /** Label of the send button. Defaults to `"Send"`. */
120
+ submitLabel?: React.ReactNode;
121
+ }
122
+ /** Serialise parts into the plain prompt text, e.g. `"Update @sam on p:Apollo"`. */
123
+ declare function structuredPartsToText(parts: StructuredInputPart[]): string;
124
+ /**
125
+ * Tagged input: a prompt field where typed prefixes (`@` person, `p:` project,
126
+ * `/` file — configurable) become removable tag chips, with optional
127
+ * type-ahead suggestions shown as buttons. Backspace on an empty field removes
128
+ * the last tag; Enter sends. Tags are optional — plain text always works.
129
+ */
130
+ declare const StructuredInputTags: React.ForwardRefExoticComponent<StructuredInputTagsProps & React.RefAttributes<HTMLDivElement>>;
131
+ interface StructuredInputWizardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSubmit" | "title"> {
132
+ /** Step titles, e.g. `["Intent", "Audience", "Tone"]`. */
133
+ steps: string[];
134
+ /** Current step, 0-based (controlled). */
135
+ step?: number;
136
+ /** Initial step (uncontrolled). */
137
+ defaultStep?: number;
138
+ /** Called when Back / Next move. */
139
+ onStepChange?: (step: number) => void;
140
+ /** The current step's question, e.g. `"Who is the brief for?"`. */
141
+ description?: React.ReactNode;
142
+ /** Called from the last step (or from “Generate now” when `allowEarlySubmit`). */
143
+ onSubmit?: () => void;
144
+ /** Label of the final button. Defaults to `"Generate"`. */
145
+ submitLabel?: React.ReactNode;
146
+ /** Offer “Generate now” before the last step — fields are never all required. Defaults to `true`. */
147
+ allowEarlySubmit?: boolean;
148
+ /** The current step's fields. */
149
+ children?: React.ReactNode;
150
+ }
151
+ /**
152
+ * Wizard step: a multi-step frame for high-stakes prompts (legal, HR, medical) —
153
+ * a progress bar, “Step 2 of 3 · Audience”, the step's question and fields
154
+ * (children), and Back / Next / Generate. Focus moves to the step heading when
155
+ * the step changes; nothing forces every step to be filled before generating.
156
+ */
157
+ declare const StructuredInputWizard: React.ForwardRefExoticComponent<StructuredInputWizardProps & React.RefAttributes<HTMLDivElement>>;
41
158
 
42
- export { StructuredInput, type StructuredInputProps, type StructuredOption };
159
+ export { StructuredInput, type StructuredInputPart, type StructuredInputProps, type StructuredInputSlot, StructuredInputSlots, type StructuredInputSlotsProps, type StructuredInputTagTrigger, StructuredInputTags, type StructuredInputTagsProps, type StructuredInputValues, StructuredInputWizard, type StructuredInputWizardProps, type StructuredOption, structuredPartsToText };
package/dist/index.d.ts CHANGED
@@ -34,9 +34,126 @@ interface StructuredInputProps extends Omit<React.HTMLAttributes<HTMLDivElement>
34
34
  }
35
35
  /**
36
36
  * An agent-generated clarifying question rendered as a structured choice —
37
- * single or multi-select options, a free-text escape, and a skip.
37
+ * single or multi-select options, a free-text escape, and a skip. The other
38
+ * structured-input variants are composable siblings: {@link StructuredInputSlots}
39
+ * (named slots), {@link StructuredInputTags} (tagged input) and
40
+ * {@link StructuredInputWizard} (wizard step).
38
41
  * Source: Figma "Expressive Input / Structured Input".
39
42
  */
40
43
  declare const StructuredInput: React.ForwardRefExoticComponent<StructuredInputProps & React.RefAttributes<HTMLDivElement>>;
44
+ interface StructuredInputSlot {
45
+ /** Key in the values object. */
46
+ name: string;
47
+ /** Plain-language label, e.g. `"Audience"`. */
48
+ label: string;
49
+ /** Placeholder / example value. */
50
+ placeholder?: string;
51
+ /** Choices — renders a select instead of a text field. */
52
+ options?: StructuredOption[];
53
+ /** Helper text under the field. */
54
+ helper?: string;
55
+ /**
56
+ * Marks the initial value as the agent's suggestion, with the reason, e.g.
57
+ * `"from the meeting invite"`. Once the person changes it, it reads “Edited by you”.
58
+ */
59
+ suggested?: string;
60
+ /** Span the full row. */
61
+ wide?: boolean;
62
+ }
63
+ type StructuredInputValues = Record<string, string>;
64
+ interface StructuredInputSlotsProps extends Omit<React.HTMLAttributes<HTMLFormElement>, "onSubmit" | "defaultValue"> {
65
+ /** The 2–4 slots the prompt template needs. */
66
+ slots: StructuredInputSlot[];
67
+ /** Values (controlled). */
68
+ values?: StructuredInputValues;
69
+ /** Initial values (uncontrolled). */
70
+ defaultValues?: StructuredInputValues;
71
+ /** Called on every change. */
72
+ onValuesChange?: (values: StructuredInputValues) => void;
73
+ /** Show a freeform override field. `true` uses the label `"Anything else?"`; a string sets the label. */
74
+ freeform?: boolean | string;
75
+ /** Called on submit. Never requires every slot — any value (or the freeform text) is enough. */
76
+ onSubmit?: (values: StructuredInputValues, freeform: string) => void;
77
+ /** Label of the submit button. Defaults to `"Generate"`. */
78
+ submitLabel?: React.ReactNode;
79
+ /** Accessible name of the form. Defaults to `"Structured prompt builder"`. */
80
+ label?: string;
81
+ }
82
+ /**
83
+ * Named slots: a short set of plain-language fields (2–4) that fill a prompt
84
+ * template — text fields or selects, an optional freeform override, and a
85
+ * submit that never requires every field. Agent-suggested values are marked
86
+ * “Suggested” with their reason and become “Edited by you” once changed; the
87
+ * component never changes a value the person set.
88
+ */
89
+ declare const StructuredInputSlots: React.ForwardRefExoticComponent<StructuredInputSlotsProps & React.RefAttributes<HTMLFormElement>>;
90
+ interface StructuredInputTagTrigger {
91
+ /** The prefix that starts a tag, e.g. `"@"`, `"p:"`, `"/"`. */
92
+ trigger: string;
93
+ /** What the tag refers to, e.g. `"Person"`. */
94
+ label: string;
95
+ /** Values offered while typing after the trigger. */
96
+ suggestions?: string[];
97
+ }
98
+ type StructuredInputPart = {
99
+ type: "text";
100
+ text: string;
101
+ } | {
102
+ type: "tag";
103
+ trigger: string;
104
+ value: string;
105
+ };
106
+ interface StructuredInputTagsProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSubmit" | "defaultValue" | "onChange"> {
107
+ /** Recognised tag prefixes. Defaults to `@` person, `p:` project, `/` file. */
108
+ triggers?: StructuredInputTagTrigger[];
109
+ /** Committed parts (controlled). */
110
+ value?: StructuredInputPart[];
111
+ /** Initial parts (uncontrolled). */
112
+ defaultValue?: StructuredInputPart[];
113
+ /** Called when parts are added or removed. */
114
+ onValueChange?: (parts: StructuredInputPart[]) => void;
115
+ /** Called on Enter or the send button with the parts and their plain-text form. */
116
+ onSubmit?: (parts: StructuredInputPart[], text: string) => void;
117
+ /** Accessible name and placeholder of the text field. */
118
+ placeholder?: string;
119
+ /** Label of the send button. Defaults to `"Send"`. */
120
+ submitLabel?: React.ReactNode;
121
+ }
122
+ /** Serialise parts into the plain prompt text, e.g. `"Update @sam on p:Apollo"`. */
123
+ declare function structuredPartsToText(parts: StructuredInputPart[]): string;
124
+ /**
125
+ * Tagged input: a prompt field where typed prefixes (`@` person, `p:` project,
126
+ * `/` file — configurable) become removable tag chips, with optional
127
+ * type-ahead suggestions shown as buttons. Backspace on an empty field removes
128
+ * the last tag; Enter sends. Tags are optional — plain text always works.
129
+ */
130
+ declare const StructuredInputTags: React.ForwardRefExoticComponent<StructuredInputTagsProps & React.RefAttributes<HTMLDivElement>>;
131
+ interface StructuredInputWizardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSubmit" | "title"> {
132
+ /** Step titles, e.g. `["Intent", "Audience", "Tone"]`. */
133
+ steps: string[];
134
+ /** Current step, 0-based (controlled). */
135
+ step?: number;
136
+ /** Initial step (uncontrolled). */
137
+ defaultStep?: number;
138
+ /** Called when Back / Next move. */
139
+ onStepChange?: (step: number) => void;
140
+ /** The current step's question, e.g. `"Who is the brief for?"`. */
141
+ description?: React.ReactNode;
142
+ /** Called from the last step (or from “Generate now” when `allowEarlySubmit`). */
143
+ onSubmit?: () => void;
144
+ /** Label of the final button. Defaults to `"Generate"`. */
145
+ submitLabel?: React.ReactNode;
146
+ /** Offer “Generate now” before the last step — fields are never all required. Defaults to `true`. */
147
+ allowEarlySubmit?: boolean;
148
+ /** The current step's fields. */
149
+ children?: React.ReactNode;
150
+ }
151
+ /**
152
+ * Wizard step: a multi-step frame for high-stakes prompts (legal, HR, medical) —
153
+ * a progress bar, “Step 2 of 3 · Audience”, the step's question and fields
154
+ * (children), and Back / Next / Generate. Focus moves to the step heading when
155
+ * the step changes; nothing forces every step to be filled before generating.
156
+ */
157
+ declare const StructuredInputWizard: React.ForwardRefExoticComponent<StructuredInputWizardProps & React.RefAttributes<HTMLDivElement>>;
41
158
 
42
- export { StructuredInput, type StructuredInputProps, type StructuredOption };
159
+ export { StructuredInput, type StructuredInputPart, type StructuredInputProps, type StructuredInputSlot, StructuredInputSlots, type StructuredInputSlotsProps, type StructuredInputTagTrigger, StructuredInputTags, type StructuredInputTagsProps, type StructuredInputValues, StructuredInputWizard, type StructuredInputWizardProps, type StructuredOption, structuredPartsToText };
package/dist/index.js CHANGED
@@ -1,7 +1,13 @@
1
1
  "use client";
2
2
  import * as React from 'react';
3
3
  import { cn } from '@nucleux/utils';
4
- import { Check } from 'lucide-react';
4
+ import { Check, X } from 'lucide-react';
5
+ import { Badge } from '@nucleux/badge';
6
+ import { Button } from '@nucleux/button';
7
+ import { Field, FieldLabel, FieldDescription } from '@nucleux/field';
8
+ import { Input } from '@nucleux/input';
9
+ import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@nucleux/select';
10
+ import { Textarea } from '@nucleux/textarea';
5
11
  import { jsxs, jsx } from 'react/jsx-runtime';
6
12
 
7
13
  // src/index.tsx
@@ -24,6 +30,8 @@ var StructuredInput = React.forwardRef(
24
30
  }, ref) => {
25
31
  const [internal, setInternal] = React.useState(defaultValue);
26
32
  const [other, setOther] = React.useState("");
33
+ const questionId = React.useId();
34
+ const groupRef = React.useRef(null);
27
35
  const selected = value !== void 0 ? value : internal;
28
36
  const setSelected = (next) => {
29
37
  if (value === void 0) setInternal(next);
@@ -43,21 +51,35 @@ var StructuredInput = React.forwardRef(
43
51
  className: cn("flex flex-col gap-4 rounded-xl border border-border bg-background p-5", className),
44
52
  ...props,
45
53
  children: [
46
- /* @__PURE__ */ jsx("p", { className: "text-base font-medium text-foreground", children: question }),
54
+ /* @__PURE__ */ jsx("p", { id: questionId, className: "text-base font-medium text-foreground", children: question }),
47
55
  /* @__PURE__ */ jsx(
48
56
  "div",
49
57
  {
58
+ ref: groupRef,
50
59
  role: multiple ? "group" : "radiogroup",
51
- "aria-label": typeof question === "string" ? question : "Options",
60
+ "aria-labelledby": questionId,
52
61
  className: "flex flex-col gap-2",
53
- children: options.map((option) => {
62
+ onKeyDown: (e) => {
63
+ if (multiple || !["ArrowDown", "ArrowUp", "ArrowRight", "ArrowLeft"].includes(e.key)) return;
64
+ const radios = Array.from(groupRef.current?.querySelectorAll('[role="radio"]') ?? []);
65
+ const i = radios.indexOf(document.activeElement);
66
+ if (i < 0) return;
67
+ e.preventDefault();
68
+ const step = e.key === "ArrowDown" || e.key === "ArrowRight" ? 1 : -1;
69
+ const next = (i + step + radios.length) % radios.length;
70
+ radios[next].focus();
71
+ setSelected([options[next].value]);
72
+ },
73
+ children: options.map((option, index) => {
54
74
  const active = selected.includes(option.value);
75
+ const tabbable = multiple || index === Math.max(0, options.findIndex((o) => selected.includes(o.value)));
55
76
  return /* @__PURE__ */ jsxs(
56
77
  "button",
57
78
  {
58
79
  type: "button",
59
80
  role: multiple ? "checkbox" : "radio",
60
81
  "aria-checked": active,
82
+ tabIndex: tabbable ? 0 : -1,
61
83
  onClick: () => toggle(option.value),
62
84
  className: cn(
63
85
  "flex items-center gap-3 rounded-lg border px-3 py-2.5 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
@@ -121,7 +143,328 @@ var StructuredInput = React.forwardRef(
121
143
  }
122
144
  );
123
145
  StructuredInput.displayName = "StructuredInput";
146
+ var StructuredInputSlots = React.forwardRef(
147
+ ({
148
+ slots,
149
+ values: valuesProp,
150
+ defaultValues,
151
+ onValuesChange,
152
+ freeform = false,
153
+ onSubmit,
154
+ submitLabel = "Generate",
155
+ label = "Structured prompt builder",
156
+ className,
157
+ ...props
158
+ }, ref) => {
159
+ const [internal, setInternal] = React.useState(defaultValues ?? {});
160
+ const values = valuesProp ?? internal;
161
+ const [edited, setEdited] = React.useState({});
162
+ const [extra, setExtra] = React.useState("");
163
+ const baseId = React.useId();
164
+ const setValue = (name, value) => {
165
+ const next = { ...values, [name]: value };
166
+ if (valuesProp === void 0) setInternal(next);
167
+ setEdited((e) => ({ ...e, [name]: true }));
168
+ onValuesChange?.(next);
169
+ };
170
+ const hasAny = Object.values(values).some((v) => v && v.trim()) || extra.trim().length > 0;
171
+ const freeformLabel = typeof freeform === "string" ? freeform : "Anything else?";
172
+ return /* @__PURE__ */ jsxs(
173
+ "form",
174
+ {
175
+ ref,
176
+ "aria-label": label,
177
+ className: cn("grid grid-cols-1 gap-3 sm:grid-cols-2", className),
178
+ onSubmit: (e) => {
179
+ e.preventDefault();
180
+ if (hasAny) onSubmit?.(values, extra);
181
+ },
182
+ ...props,
183
+ children: [
184
+ slots.map((slot) => {
185
+ const id = `${baseId}-${slot.name}`;
186
+ const helperId = `${id}-helper`;
187
+ const mark = slot.suggested ? edited[slot.name] ? { text: "Edited by you", variant: "secondary" } : { text: "Suggested", variant: "info" } : null;
188
+ const describedBy = slot.helper || mark && !edited[slot.name] ? helperId : void 0;
189
+ return /* @__PURE__ */ jsxs(Field, { className: cn("gap-1.5", slot.wide && "sm:col-span-2"), children: [
190
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
191
+ /* @__PURE__ */ jsx(FieldLabel, { id: `${id}-label`, htmlFor: id, className: "text-xs font-semibold text-muted-foreground", children: slot.label }),
192
+ mark && /* @__PURE__ */ jsx(Badge, { variant: mark.variant, children: mark.text })
193
+ ] }),
194
+ slot.options ? /* @__PURE__ */ jsxs(Select, { value: values[slot.name] || void 0, onValueChange: (v) => setValue(slot.name, v), children: [
195
+ /* @__PURE__ */ jsx(SelectTrigger, { id, "aria-labelledby": `${id}-label ${id}`, "aria-describedby": describedBy, children: /* @__PURE__ */ jsx(SelectValue, { placeholder: slot.placeholder ?? "Choose\u2026" }) }),
196
+ /* @__PURE__ */ jsx(SelectContent, { children: slot.options.map((o) => /* @__PURE__ */ jsx(SelectItem, { value: o.value, children: o.label }, o.value)) })
197
+ ] }) : /* @__PURE__ */ jsx(
198
+ Input,
199
+ {
200
+ id,
201
+ value: values[slot.name] ?? "",
202
+ placeholder: slot.placeholder,
203
+ "aria-describedby": describedBy,
204
+ onChange: (e) => setValue(slot.name, e.target.value)
205
+ }
206
+ ),
207
+ describedBy && /* @__PURE__ */ jsxs(FieldDescription, { id: helperId, className: "text-xs", children: [
208
+ mark && !edited[slot.name] ? `Suggested ${slot.suggested}. ` : "",
209
+ slot.helper
210
+ ] })
211
+ ] }, slot.name);
212
+ }),
213
+ freeform && /* @__PURE__ */ jsxs(Field, { className: "gap-1.5 sm:col-span-2", children: [
214
+ /* @__PURE__ */ jsx(FieldLabel, { htmlFor: `${baseId}-freeform`, className: "text-xs font-semibold text-muted-foreground", children: freeformLabel }),
215
+ /* @__PURE__ */ jsx(
216
+ Textarea,
217
+ {
218
+ id: `${baseId}-freeform`,
219
+ rows: 2,
220
+ value: extra,
221
+ placeholder: "Say it in your own words \u2014 this overrides the fields above",
222
+ onChange: (e) => setExtra(e.target.value)
223
+ }
224
+ )
225
+ ] }),
226
+ onSubmit && /* @__PURE__ */ jsx("div", { className: "flex justify-end sm:col-span-2", children: /* @__PURE__ */ jsx(Button, { type: "submit", variant: "cta", size: "sm", disabled: !hasAny, children: submitLabel }) })
227
+ ]
228
+ }
229
+ );
230
+ }
231
+ );
232
+ StructuredInputSlots.displayName = "StructuredInputSlots";
233
+ var DEFAULT_TRIGGERS = [
234
+ { trigger: "@", label: "Person" },
235
+ { trigger: "p:", label: "Project" },
236
+ { trigger: "/", label: "File" }
237
+ ];
238
+ var TAG_TONES = [
239
+ { variant: "secondary", className: "bg-brand-muted text-brand" },
240
+ { variant: "info" },
241
+ { variant: "success" },
242
+ { variant: "outline" }
243
+ ];
244
+ function structuredPartsToText(parts) {
245
+ return parts.map((p) => p.type === "text" ? p.text : `${p.trigger}${p.value}`).join(" ").replace(/\s+/g, " ").trim();
246
+ }
247
+ var StructuredInputTags = React.forwardRef(
248
+ ({
249
+ triggers = DEFAULT_TRIGGERS,
250
+ value: valueProp,
251
+ defaultValue,
252
+ onValueChange,
253
+ onSubmit,
254
+ placeholder = "Type @ for people, p: for projects, / for files",
255
+ submitLabel = "Send",
256
+ className,
257
+ ...props
258
+ }, ref) => {
259
+ const [internal, setInternal] = React.useState(defaultValue ?? []);
260
+ const parts = valueProp ?? internal;
261
+ const [draft, setDraft] = React.useState("");
262
+ const inputRef = React.useRef(null);
263
+ const hintId = React.useId();
264
+ const commit = (next) => {
265
+ if (valueProp === void 0) setInternal(next);
266
+ onValueChange?.(next);
267
+ };
268
+ const ordered = [...triggers].sort((a, b) => b.trigger.length - a.trigger.length);
269
+ const matchTag = (word) => {
270
+ const t = ordered.find((tr) => word.startsWith(tr.trigger) && word.length > tr.trigger.length);
271
+ return t ? { trigger: t.trigger, value: word.slice(t.trigger.length) } : null;
272
+ };
273
+ const lastWord = draft.split(" ").pop() ?? "";
274
+ const activeTrigger = ordered.find((tr) => lastWord.startsWith(tr.trigger));
275
+ const query = activeTrigger ? lastWord.slice(activeTrigger.trigger.length).toLowerCase() : "";
276
+ const suggestions = activeTrigger?.suggestions?.filter((s) => s.toLowerCase().startsWith(query)).slice(0, 5) ?? [];
277
+ const flush = (source = draft, forceTag) => {
278
+ const words = source.split(" ");
279
+ const last = words.pop() ?? "";
280
+ const tag = forceTag ?? matchTag(last);
281
+ const textBefore = (tag ? words.join(" ") : source).trim();
282
+ const next = [...parts];
283
+ if (textBefore) next.push({ type: "text", text: textBefore });
284
+ if (tag) next.push({ type: "tag", ...tag });
285
+ if (next.length !== parts.length) commit(next);
286
+ setDraft("");
287
+ return next;
288
+ };
289
+ const toneFor = (trigger) => {
290
+ const i = triggers.findIndex((t) => t.trigger === trigger);
291
+ return TAG_TONES[(i < 0 ? 0 : i) % TAG_TONES.length];
292
+ };
293
+ const labelFor = (trigger) => triggers.find((t) => t.trigger === trigger)?.label ?? "Tag";
294
+ const submit = () => {
295
+ const next = flush();
296
+ const text = structuredPartsToText(next);
297
+ if (text) onSubmit?.(next, text);
298
+ };
299
+ return /* @__PURE__ */ jsxs("div", { ref, className: cn("flex flex-col gap-2", className), ...props, children: [
300
+ /* @__PURE__ */ jsxs(
301
+ "div",
302
+ {
303
+ className: "flex flex-wrap items-center gap-1.5 rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground focus-within:ring-2 focus-within:ring-ring",
304
+ onClick: () => inputRef.current?.focus(),
305
+ children: [
306
+ parts.map(
307
+ (part, i) => part.type === "text" ? /* @__PURE__ */ jsx("span", { children: part.text }, i) : /* @__PURE__ */ jsxs(
308
+ Badge,
309
+ {
310
+ variant: toneFor(part.trigger).variant,
311
+ className: cn("gap-0.5 rounded-md pr-0.5 font-semibold", toneFor(part.trigger).className),
312
+ children: [
313
+ part.trigger,
314
+ part.value,
315
+ /* @__PURE__ */ jsx(
316
+ "button",
317
+ {
318
+ type: "button",
319
+ "aria-label": `Remove ${labelFor(part.trigger).toLowerCase()} ${part.trigger}${part.value}`,
320
+ onClick: (e) => {
321
+ e.stopPropagation();
322
+ commit(parts.filter((_, j) => j !== i));
323
+ inputRef.current?.focus();
324
+ },
325
+ className: "inline-flex size-4 items-center justify-center rounded-sm opacity-70 hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-3",
326
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
327
+ }
328
+ )
329
+ ]
330
+ },
331
+ i
332
+ )
333
+ ),
334
+ /* @__PURE__ */ jsx(
335
+ Input,
336
+ {
337
+ ref: inputRef,
338
+ value: draft,
339
+ "aria-label": placeholder,
340
+ "aria-describedby": hintId,
341
+ placeholder: parts.length ? void 0 : placeholder,
342
+ onChange: (e) => {
343
+ const v = e.target.value;
344
+ if (v.endsWith(" ") && matchTag(v.trimEnd().split(" ").pop() ?? "")) flush(v.trimEnd());
345
+ else setDraft(v);
346
+ },
347
+ onKeyDown: (e) => {
348
+ if (e.key === " " && matchTag(lastWord)) {
349
+ e.preventDefault();
350
+ flush();
351
+ } else if (e.key === "Enter") {
352
+ e.preventDefault();
353
+ submit();
354
+ } else if (e.key === "Backspace" && draft === "" && parts.length) {
355
+ e.preventDefault();
356
+ const last = parts[parts.length - 1];
357
+ commit(parts.slice(0, -1));
358
+ if (last.type === "text") setDraft(last.text);
359
+ }
360
+ },
361
+ className: "h-7 min-w-[8rem] flex-1 border-0 px-0 shadow-none focus-visible:ring-0 focus-visible:ring-offset-0"
362
+ }
363
+ )
364
+ ]
365
+ }
366
+ ),
367
+ /* @__PURE__ */ jsxs("p", { id: hintId, className: "sr-only", children: [
368
+ triggers.map((t) => `${t.trigger} for ${t.label.toLowerCase()}`).join(", "),
369
+ ". Space turns a tag into a chip; Backspace removes the last one; Enter sends."
370
+ ] }),
371
+ activeTrigger && suggestions.length > 0 && /* @__PURE__ */ jsx("div", { role: "group", "aria-label": `${activeTrigger.label} suggestions`, className: "flex flex-wrap gap-1", children: suggestions.map((s) => /* @__PURE__ */ jsxs(
372
+ Button,
373
+ {
374
+ variant: "secondary",
375
+ size: "sm",
376
+ className: "h-7 px-2",
377
+ onClick: () => {
378
+ flush(draft, { trigger: activeTrigger.trigger, value: s });
379
+ inputRef.current?.focus();
380
+ },
381
+ children: [
382
+ activeTrigger.trigger,
383
+ s
384
+ ]
385
+ },
386
+ s
387
+ )) }),
388
+ onSubmit && /* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(
389
+ Button,
390
+ {
391
+ variant: "cta",
392
+ size: "sm",
393
+ onClick: submit,
394
+ disabled: parts.length === 0 && draft.trim() === "",
395
+ children: submitLabel
396
+ }
397
+ ) })
398
+ ] });
399
+ }
400
+ );
401
+ StructuredInputTags.displayName = "StructuredInputTags";
402
+ var StructuredInputWizard = React.forwardRef(
403
+ ({
404
+ steps,
405
+ step: stepProp,
406
+ defaultStep = 0,
407
+ onStepChange,
408
+ description,
409
+ onSubmit,
410
+ submitLabel = "Generate",
411
+ allowEarlySubmit = true,
412
+ className,
413
+ children,
414
+ ...props
415
+ }, ref) => {
416
+ const [internal, setInternal] = React.useState(defaultStep);
417
+ const step = Math.min(Math.max(stepProp ?? internal, 0), Math.max(steps.length - 1, 0));
418
+ const headingRef = React.useRef(null);
419
+ const headingId = React.useId();
420
+ const moved = React.useRef(false);
421
+ React.useEffect(() => {
422
+ if (moved.current) headingRef.current?.focus();
423
+ }, [step]);
424
+ const go = (next) => {
425
+ moved.current = true;
426
+ if (stepProp === void 0) setInternal(next);
427
+ onStepChange?.(next);
428
+ };
429
+ const last = step === steps.length - 1;
430
+ return /* @__PURE__ */ jsxs(
431
+ "div",
432
+ {
433
+ ref,
434
+ role: "group",
435
+ "aria-labelledby": headingId,
436
+ className: cn("flex flex-col gap-3 rounded-xl border border-border bg-background p-4", className),
437
+ ...props,
438
+ children: [
439
+ /* @__PURE__ */ jsx("div", { className: "flex gap-1", "aria-hidden": "true", children: steps.map((_, i) => /* @__PURE__ */ jsx("span", { className: cn("h-1 flex-1 rounded-full", i <= step ? "bg-brand" : "bg-muted") }, i)) }),
440
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
441
+ /* @__PURE__ */ jsxs("p", { ref: headingRef, id: headingId, tabIndex: -1, className: "text-sm font-semibold text-foreground outline-none", children: [
442
+ "Step ",
443
+ step + 1,
444
+ " of ",
445
+ steps.length,
446
+ " \xB7 ",
447
+ steps[step]
448
+ ] }),
449
+ description && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: description })
450
+ ] }),
451
+ children,
452
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
453
+ step > 0 && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: () => go(step - 1), children: "Back" }),
454
+ /* @__PURE__ */ jsx("span", { className: "flex-1" }),
455
+ !last && allowEarlySubmit && onSubmit && /* @__PURE__ */ jsxs(Button, { variant: "link", size: "sm", onClick: onSubmit, children: [
456
+ submitLabel,
457
+ " now"
458
+ ] }),
459
+ last ? onSubmit && /* @__PURE__ */ jsx(Button, { variant: "cta", size: "sm", onClick: onSubmit, children: submitLabel }) : /* @__PURE__ */ jsx(Button, { variant: "cta", size: "sm", onClick: () => go(step + 1), children: "Next" })
460
+ ] })
461
+ ]
462
+ }
463
+ );
464
+ }
465
+ );
466
+ StructuredInputWizard.displayName = "StructuredInputWizard";
124
467
 
125
- export { StructuredInput };
468
+ export { StructuredInput, StructuredInputSlots, StructuredInputTags, StructuredInputWizard, structuredPartsToText };
126
469
  //# sourceMappingURL=index.js.map
127
470
  //# sourceMappingURL=index.js.map