@streamoid/chat-components 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +40 -4
  2. package/dist/index.js +1065 -140
  3. package/package.json +14 -1
package/dist/index.d.ts CHANGED
@@ -1,5 +1,28 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
 
3
+ interface FieldOption {
4
+ id: string;
5
+ label: string;
6
+ description?: string;
7
+ }
8
+ interface TodoItem {
9
+ id: string;
10
+ content: string;
11
+ status: "pending" | "completed";
12
+ }
13
+ interface DynamicFormField {
14
+ id: string;
15
+ type: "text" | "textarea" | "number" | "select" | "multiselect" | "checkbox" | "radio" | "approval" | "todo_list" | "file";
16
+ label?: string;
17
+ description?: string;
18
+ placeholder?: string;
19
+ required?: boolean;
20
+ default_value?: unknown;
21
+ options?: FieldOption[];
22
+ items?: TodoItem[];
23
+ accept?: string;
24
+ multiple?: boolean;
25
+ }
3
26
  interface DynamicFormProps {
4
27
  workspaceId: string;
5
28
  userId: string;
@@ -8,10 +31,23 @@ interface DynamicFormProps {
8
31
  isActive: boolean;
9
32
  submitted?: boolean;
10
33
  submittedValues?: Record<string, unknown>;
11
- uiProps: Record<string, unknown>;
34
+ uiProps: {
35
+ title?: string;
36
+ description?: string;
37
+ fields?: DynamicFormField[];
38
+ submit_button_text?: string;
39
+ cancel_button_text?: string;
40
+ _componentName?: string;
41
+ [key: string]: unknown;
42
+ };
12
43
  meta?: Record<string, unknown>;
13
- onSubmit: (values: Record<string, unknown>, message?: string) => void;
14
- onCancel?: () => void;
44
+ onSubmit: (values: Record<string, unknown>, message?: string) => Promise<void> | void;
45
+ onFileUpload?: (formData: FormData) => Promise<{
46
+ status: string;
47
+ data?: {
48
+ download_url: string;
49
+ };
50
+ }>;
15
51
  }
16
52
  declare function DynamicForm(props: DynamicFormProps): react_jsx_runtime.JSX.Element;
17
53
 
@@ -30,4 +66,4 @@ declare const dynamicFormManifest: {
30
66
  };
31
67
  };
32
68
 
33
- export { DynamicForm, dynamicFormManifest };
69
+ export { DynamicForm, type DynamicFormField, type DynamicFormProps, dynamicFormManifest };
package/dist/index.js CHANGED
@@ -1,163 +1,1088 @@
1
1
  // src/DynamicForm.tsx
2
- import { useState, useCallback } from "react";
3
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useState, useRef, useEffect, useCallback } from "react";
3
+
4
+ // src/ui/utils.ts
5
+ import { clsx } from "clsx";
6
+ import { twMerge } from "tailwind-merge";
7
+ function cn(...inputs) {
8
+ return twMerge(clsx(inputs));
9
+ }
10
+
11
+ // src/ui/card.tsx
12
+ import { jsx } from "react/jsx-runtime";
13
+ function Card({ className, ...props }) {
14
+ return /* @__PURE__ */ jsx(
15
+ "div",
16
+ {
17
+ "data-slot": "card",
18
+ className: cn(
19
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border",
20
+ className
21
+ ),
22
+ ...props
23
+ }
24
+ );
25
+ }
26
+ function CardHeader({ className, ...props }) {
27
+ return /* @__PURE__ */ jsx(
28
+ "div",
29
+ {
30
+ "data-slot": "card-header",
31
+ className: cn(
32
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 pt-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
33
+ className
34
+ ),
35
+ ...props
36
+ }
37
+ );
38
+ }
39
+ function CardTitle({ className, ...props }) {
40
+ return /* @__PURE__ */ jsx(
41
+ "h4",
42
+ {
43
+ "data-slot": "card-title",
44
+ className: cn("leading-none", className),
45
+ ...props
46
+ }
47
+ );
48
+ }
49
+ function CardDescription({ className, ...props }) {
50
+ return /* @__PURE__ */ jsx(
51
+ "p",
52
+ {
53
+ "data-slot": "card-description",
54
+ className: cn("text-muted-foreground", className),
55
+ ...props
56
+ }
57
+ );
58
+ }
59
+
60
+ // src/ui/button.tsx
61
+ import { Slot } from "@radix-ui/react-slot";
62
+ import { cva } from "class-variance-authority";
63
+ import { jsx as jsx2 } from "react/jsx-runtime";
64
+ var buttonVariants = cva(
65
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
66
+ {
67
+ variants: {
68
+ variant: {
69
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
70
+ destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
71
+ outline: "border bg-background text-foreground hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
72
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
73
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
74
+ link: "text-primary underline-offset-4 hover:underline"
75
+ },
76
+ size: {
77
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
78
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
79
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
80
+ icon: "size-9 rounded-md"
81
+ }
82
+ },
83
+ defaultVariants: {
84
+ variant: "default",
85
+ size: "default"
86
+ }
87
+ }
88
+ );
89
+ function Button({
90
+ className,
91
+ variant,
92
+ size,
93
+ asChild = false,
94
+ ...props
95
+ }) {
96
+ const Comp = asChild ? Slot : "button";
97
+ return /* @__PURE__ */ jsx2(
98
+ Comp,
99
+ {
100
+ "data-slot": "button",
101
+ className: cn(buttonVariants({ variant, size, className })),
102
+ ...props
103
+ }
104
+ );
105
+ }
106
+
107
+ // src/ui/input.tsx
108
+ import { jsx as jsx3 } from "react/jsx-runtime";
109
+ function Input({ className, type, ...props }) {
110
+ return /* @__PURE__ */ jsx3(
111
+ "input",
112
+ {
113
+ type,
114
+ "data-slot": "input",
115
+ className: cn(
116
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base bg-input-background transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
117
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
118
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
119
+ className
120
+ ),
121
+ ...props
122
+ }
123
+ );
124
+ }
125
+
126
+ // src/ui/textarea.tsx
127
+ import { jsx as jsx4 } from "react/jsx-runtime";
128
+ function Textarea({ className, ...props }) {
129
+ return /* @__PURE__ */ jsx4(
130
+ "textarea",
131
+ {
132
+ "data-slot": "textarea",
133
+ className: cn(
134
+ "resize-none border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-input-background px-3 py-2 text-base transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
135
+ className
136
+ ),
137
+ ...props
138
+ }
139
+ );
140
+ }
141
+
142
+ // src/ui/select.tsx
143
+ import * as SelectPrimitive from "@radix-ui/react-select";
144
+ import {
145
+ CheckIcon,
146
+ ChevronDownIcon,
147
+ ChevronUpIcon
148
+ } from "lucide-react";
149
+ import { jsx as jsx5, jsxs } from "react/jsx-runtime";
150
+ function Select({
151
+ ...props
152
+ }) {
153
+ return /* @__PURE__ */ jsx5(SelectPrimitive.Root, { "data-slot": "select", ...props });
154
+ }
155
+ function SelectValue({
156
+ ...props
157
+ }) {
158
+ return /* @__PURE__ */ jsx5(SelectPrimitive.Value, { "data-slot": "select-value", ...props });
159
+ }
160
+ function SelectTrigger({
161
+ className,
162
+ size = "default",
163
+ children,
164
+ ...props
165
+ }) {
166
+ return /* @__PURE__ */ jsxs(
167
+ SelectPrimitive.Trigger,
168
+ {
169
+ "data-slot": "select-trigger",
170
+ "data-size": size,
171
+ className: cn(
172
+ "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-full items-center justify-between gap-2 rounded-md border bg-input-background px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
173
+ className
174
+ ),
175
+ ...props,
176
+ children: [
177
+ children,
178
+ /* @__PURE__ */ jsx5(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx5(ChevronDownIcon, { className: "size-4 opacity-50" }) })
179
+ ]
180
+ }
181
+ );
182
+ }
183
+ function SelectContent({
184
+ className,
185
+ children,
186
+ position = "popper",
187
+ ...props
188
+ }) {
189
+ return /* @__PURE__ */ jsx5(SelectPrimitive.Portal, { children: /* @__PURE__ */ jsxs(
190
+ SelectPrimitive.Content,
191
+ {
192
+ "data-slot": "select-content",
193
+ className: cn(
194
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
195
+ position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
196
+ className
197
+ ),
198
+ position,
199
+ ...props,
200
+ children: [
201
+ /* @__PURE__ */ jsx5(SelectScrollUpButton, {}),
202
+ /* @__PURE__ */ jsx5(
203
+ SelectPrimitive.Viewport,
204
+ {
205
+ className: cn(
206
+ "p-1",
207
+ position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
208
+ ),
209
+ children
210
+ }
211
+ ),
212
+ /* @__PURE__ */ jsx5(SelectScrollDownButton, {})
213
+ ]
214
+ }
215
+ ) });
216
+ }
217
+ function SelectItem({
218
+ className,
219
+ children,
220
+ ...props
221
+ }) {
222
+ return /* @__PURE__ */ jsxs(
223
+ SelectPrimitive.Item,
224
+ {
225
+ "data-slot": "select-item",
226
+ className: cn(
227
+ "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
228
+ className
229
+ ),
230
+ ...props,
231
+ children: [
232
+ /* @__PURE__ */ jsx5("span", { className: "absolute right-2 flex size-3.5 items-center justify-center", children: /* @__PURE__ */ jsx5(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx5(CheckIcon, { className: "size-4" }) }) }),
233
+ /* @__PURE__ */ jsx5(SelectPrimitive.ItemText, { children })
234
+ ]
235
+ }
236
+ );
237
+ }
238
+ function SelectScrollUpButton({
239
+ className,
240
+ ...props
241
+ }) {
242
+ return /* @__PURE__ */ jsx5(
243
+ SelectPrimitive.ScrollUpButton,
244
+ {
245
+ "data-slot": "select-scroll-up-button",
246
+ className: cn(
247
+ "flex cursor-default items-center justify-center py-1",
248
+ className
249
+ ),
250
+ ...props,
251
+ children: /* @__PURE__ */ jsx5(ChevronUpIcon, { className: "size-4" })
252
+ }
253
+ );
254
+ }
255
+ function SelectScrollDownButton({
256
+ className,
257
+ ...props
258
+ }) {
259
+ return /* @__PURE__ */ jsx5(
260
+ SelectPrimitive.ScrollDownButton,
261
+ {
262
+ "data-slot": "select-scroll-down-button",
263
+ className: cn(
264
+ "flex cursor-default items-center justify-center py-1",
265
+ className
266
+ ),
267
+ ...props,
268
+ children: /* @__PURE__ */ jsx5(ChevronDownIcon, { className: "size-4" })
269
+ }
270
+ );
271
+ }
272
+
273
+ // src/ui/checkbox.tsx
274
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
275
+ import { CheckIcon as CheckIcon2 } from "lucide-react";
276
+ import { jsx as jsx6 } from "react/jsx-runtime";
277
+ function Checkbox({
278
+ className,
279
+ ...props
280
+ }) {
281
+ return /* @__PURE__ */ jsx6(
282
+ CheckboxPrimitive.Root,
283
+ {
284
+ "data-slot": "checkbox",
285
+ className: cn(
286
+ "peer border border-muted-foreground/40 bg-input-background dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
287
+ className
288
+ ),
289
+ ...props,
290
+ children: /* @__PURE__ */ jsx6(
291
+ CheckboxPrimitive.Indicator,
292
+ {
293
+ "data-slot": "checkbox-indicator",
294
+ className: "flex items-center justify-center text-current transition-none",
295
+ children: /* @__PURE__ */ jsx6(CheckIcon2, { className: "size-3.5" })
296
+ }
297
+ )
298
+ }
299
+ );
300
+ }
301
+
302
+ // src/ui/radio-group.tsx
303
+ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
304
+ import { CircleIcon } from "lucide-react";
305
+ import { jsx as jsx7 } from "react/jsx-runtime";
306
+ function RadioGroup({
307
+ className,
308
+ ...props
309
+ }) {
310
+ return /* @__PURE__ */ jsx7(
311
+ RadioGroupPrimitive.Root,
312
+ {
313
+ "data-slot": "radio-group",
314
+ className: cn("grid gap-3", className),
315
+ ...props
316
+ }
317
+ );
318
+ }
319
+ function RadioGroupItem({
320
+ className,
321
+ ...props
322
+ }) {
323
+ return /* @__PURE__ */ jsx7(
324
+ RadioGroupPrimitive.Item,
325
+ {
326
+ "data-slot": "radio-group-item",
327
+ className: cn(
328
+ "border-[1.5px] border-muted-foreground/50 text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
329
+ className
330
+ ),
331
+ ...props,
332
+ children: /* @__PURE__ */ jsx7(
333
+ RadioGroupPrimitive.Indicator,
334
+ {
335
+ "data-slot": "radio-group-indicator",
336
+ className: "relative flex items-center justify-center",
337
+ children: /* @__PURE__ */ jsx7(CircleIcon, { className: "fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" })
338
+ }
339
+ )
340
+ }
341
+ );
342
+ }
343
+
344
+ // src/ui/label.tsx
345
+ import * as LabelPrimitive from "@radix-ui/react-label";
346
+ import { jsx as jsx8 } from "react/jsx-runtime";
347
+ function Label2({
348
+ className,
349
+ ...props
350
+ }) {
351
+ return /* @__PURE__ */ jsx8(
352
+ LabelPrimitive.Root,
353
+ {
354
+ "data-slot": "label",
355
+ className: cn(
356
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
357
+ className
358
+ ),
359
+ ...props
360
+ }
361
+ );
362
+ }
363
+
364
+ // src/ui/progress.tsx
365
+ import * as ProgressPrimitive from "@radix-ui/react-progress";
366
+ import { jsx as jsx9 } from "react/jsx-runtime";
367
+ function Progress({
368
+ className,
369
+ value,
370
+ ...props
371
+ }) {
372
+ return /* @__PURE__ */ jsx9(
373
+ ProgressPrimitive.Root,
374
+ {
375
+ "data-slot": "progress",
376
+ className: cn(
377
+ "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
378
+ className
379
+ ),
380
+ ...props,
381
+ children: /* @__PURE__ */ jsx9(
382
+ ProgressPrimitive.Indicator,
383
+ {
384
+ "data-slot": "progress-indicator",
385
+ className: "bg-primary h-full w-full flex-1 transition-all",
386
+ style: { transform: `translateX(-${100 - (value || 0)}%)` }
387
+ }
388
+ )
389
+ }
390
+ );
391
+ }
392
+
393
+ // src/DynamicForm.tsx
394
+ import {
395
+ CheckCircle,
396
+ Loader2,
397
+ Send,
398
+ AlertCircle,
399
+ Upload,
400
+ File,
401
+ X,
402
+ Plus,
403
+ Pencil,
404
+ Trash2
405
+ } from "lucide-react";
406
+ import { Fragment, jsx as jsx10, jsxs as jsxs2 } from "react/jsx-runtime";
4
407
  function DynamicForm(props) {
5
- const { submitted, submittedValues, uiProps, onSubmit, onCancel } = props;
408
+ const {
409
+ submitted,
410
+ submittedValues,
411
+ uiProps,
412
+ onSubmit,
413
+ onFileUpload
414
+ } = props;
6
415
  const title = uiProps.title;
7
416
  const description = uiProps.description;
8
- const fields = uiProps.fields || [];
9
- const submitText = uiProps.submit_button_text || "Submit";
417
+ const fields = uiProps.fields ?? [];
418
+ const submitText = uiProps.submit_button_text ?? "Submit";
10
419
  const cancelText = uiProps.cancel_button_text;
11
- const [formValues, setFormValues] = useState(() => {
12
- if (submittedValues) return submittedValues;
13
- const initial = {};
14
- fields.forEach((f) => {
15
- initial[f.id] = f.default_value ?? "";
420
+ const getInitialValues = () => {
421
+ if (submittedValues) return { ...submittedValues };
422
+ const values = {};
423
+ fields.forEach((field) => {
424
+ if (field.default_value !== void 0) {
425
+ values[field.id] = field.default_value;
426
+ } else if (field.type === "checkbox" || field.type === "multiselect") {
427
+ values[field.id] = [];
428
+ } else if (field.type === "todo_list" && field.items) {
429
+ values[field.id] = field.items.map((item) => ({ ...item }));
430
+ } else if (field.type === "approval") {
431
+ values[field.id] = null;
432
+ } else {
433
+ values[field.id] = "";
434
+ }
16
435
  });
17
- return initial;
18
- });
19
- const [isSubmitted, setIsSubmitted] = useState(submitted === true);
20
- const handleChange = useCallback((fieldId, value) => {
21
- setFormValues((prev) => ({ ...prev, [fieldId]: value }));
436
+ return values;
437
+ };
438
+ const [formValues, setFormValues] = useState(getInitialValues);
439
+ const [isLoading, setIsLoading] = useState(false);
440
+ const [error, setError] = useState(null);
441
+ const [isSubmitted, setIsSubmitted] = useState(submitted ?? false);
442
+ const [submittedData, setSubmittedData] = useState(
443
+ submittedValues ?? null
444
+ );
445
+ const [editingTodoId, setEditingTodoId] = useState(null);
446
+ const [editingTodoContent, setEditingTodoContent] = useState("");
447
+ const [newTodoInputs, setNewTodoInputs] = useState({});
448
+ const [fileUploadState, setFileUploadState] = useState({});
449
+ const scrollRef = useRef(null);
450
+ const [canScrollUp, setCanScrollUp] = useState(false);
451
+ const [canScrollDown, setCanScrollDown] = useState(false);
452
+ const checkScroll = useCallback(() => {
453
+ const el = scrollRef.current;
454
+ if (!el) return;
455
+ const threshold = 4;
456
+ setCanScrollUp(el.scrollTop > threshold);
457
+ setCanScrollDown(el.scrollTop + el.clientHeight < el.scrollHeight - threshold);
22
458
  }, []);
23
- const handleSubmit = useCallback(() => {
24
- setIsSubmitted(true);
25
- onSubmit(formValues, "Form submitted.");
26
- }, [formValues, onSubmit]);
27
- if (isSubmitted) {
28
- return /* @__PURE__ */ jsx(
29
- "div",
30
- {
31
- style: {
32
- padding: "16px",
33
- border: "1px solid #e5e7eb",
34
- borderRadius: "8px",
35
- fontFamily: "system-ui, sans-serif"
36
- },
37
- children: /* @__PURE__ */ jsxs("p", { style: { fontWeight: 600, color: "#16a34a" }, children: [
38
- "\u2713 ",
39
- title || "Form",
40
- " submitted"
41
- ] })
459
+ useEffect(() => {
460
+ const el = scrollRef.current;
461
+ if (!el) return;
462
+ const raf = requestAnimationFrame(checkScroll);
463
+ el.addEventListener("scroll", checkScroll, { passive: true });
464
+ const ro = new ResizeObserver(checkScroll);
465
+ ro.observe(el);
466
+ const mo = new MutationObserver(checkScroll);
467
+ mo.observe(el, { childList: true, subtree: true });
468
+ return () => {
469
+ cancelAnimationFrame(raf);
470
+ el.removeEventListener("scroll", checkScroll);
471
+ ro.disconnect();
472
+ mo.disconnect();
473
+ };
474
+ }, [checkScroll, isSubmitted, isLoading]);
475
+ const handleFieldChange = (fieldId, value) => {
476
+ setFormValues((prev) => ({ ...prev, [fieldId]: value }));
477
+ setError(null);
478
+ };
479
+ const handleCheckboxChange = (fieldId, optionId, checked) => {
480
+ setFormValues((prev) => {
481
+ const cur = prev[fieldId] || [];
482
+ return {
483
+ ...prev,
484
+ [fieldId]: checked ? [...cur, optionId] : cur.filter((id) => id !== optionId)
485
+ };
486
+ });
487
+ };
488
+ const handleTodoToggle = (fieldId, itemId) => {
489
+ setFormValues((prev) => {
490
+ const items = prev[fieldId] || [];
491
+ return {
492
+ ...prev,
493
+ [fieldId]: items.map(
494
+ (item) => item.id === itemId ? { ...item, status: item.status === "completed" ? "pending" : "completed" } : item
495
+ )
496
+ };
497
+ });
498
+ };
499
+ const handleTodoEdit = (fieldId, itemId, newContent) => {
500
+ setFormValues((prev) => {
501
+ const items = prev[fieldId] || [];
502
+ return {
503
+ ...prev,
504
+ [fieldId]: items.map(
505
+ (item) => item.id === itemId ? { ...item, content: newContent } : item
506
+ )
507
+ };
508
+ });
509
+ };
510
+ const handleTodoAdd = (fieldId, content) => {
511
+ if (!content.trim()) return;
512
+ const newItem = {
513
+ id: `user_step_${Date.now()}`,
514
+ content: content.trim(),
515
+ status: "pending"
516
+ };
517
+ setFormValues((prev) => {
518
+ const items = prev[fieldId] || [];
519
+ return { ...prev, [fieldId]: [...items, newItem] };
520
+ });
521
+ };
522
+ const handleTodoDelete = (fieldId, itemId) => {
523
+ setFormValues((prev) => {
524
+ const items = prev[fieldId] || [];
525
+ return { ...prev, [fieldId]: items.filter((item) => item.id !== itemId) };
526
+ });
527
+ };
528
+ const handleFileUpload = async (fieldId, files, multiple = false) => {
529
+ if (!files || files.length === 0 || !onFileUpload) return;
530
+ setFileUploadState((prev) => ({
531
+ ...prev,
532
+ [fieldId]: { isUploading: true, progress: 0, fileName: null, error: null }
533
+ }));
534
+ try {
535
+ const uploadedUrls = [];
536
+ const fileNames = [];
537
+ for (let i = 0; i < files.length; i++) {
538
+ const file = files[i];
539
+ const fd = new FormData();
540
+ fd.append("file", file);
541
+ setFileUploadState((prev) => ({
542
+ ...prev,
543
+ [fieldId]: {
544
+ ...prev[fieldId],
545
+ progress: Math.round(i / files.length * 50),
546
+ fileName: file.name
547
+ }
548
+ }));
549
+ const result = await onFileUpload(fd);
550
+ if (result.status === "SUCCESS" && result.data?.download_url) {
551
+ uploadedUrls.push(result.data.download_url);
552
+ fileNames.push(file.name);
553
+ } else {
554
+ throw new Error("Upload failed");
555
+ }
556
+ setFileUploadState((prev) => ({
557
+ ...prev,
558
+ [fieldId]: { ...prev[fieldId], progress: Math.round((i + 1) / files.length * 100) }
559
+ }));
42
560
  }
43
- );
44
- }
45
- return /* @__PURE__ */ jsxs(
46
- "div",
47
- {
48
- style: {
49
- padding: "16px",
50
- border: "1px solid #e5e7eb",
51
- borderRadius: "8px",
52
- fontFamily: "system-ui, sans-serif",
53
- width: "100%"
54
- },
55
- children: [
56
- title && /* @__PURE__ */ jsx("h3", { style: { margin: "0 0 4px", fontSize: "16px" }, children: title }),
57
- description && /* @__PURE__ */ jsx("p", { style: { margin: "0 0 12px", fontSize: "13px", color: "#666" }, children: description }),
58
- fields.map((field) => /* @__PURE__ */ jsxs("div", { style: { marginBottom: "12px" }, children: [
59
- field.label && /* @__PURE__ */ jsxs(
60
- "label",
61
- {
62
- style: {
63
- display: "block",
64
- fontSize: "13px",
65
- fontWeight: 500,
66
- marginBottom: "4px"
561
+ const value = multiple ? uploadedUrls : uploadedUrls[0];
562
+ handleFieldChange(fieldId, value);
563
+ setFileUploadState((prev) => ({
564
+ ...prev,
565
+ [fieldId]: {
566
+ isUploading: false,
567
+ progress: 100,
568
+ fileName: multiple ? `${fileNames.length} file(s)` : fileNames[0],
569
+ error: null
570
+ }
571
+ }));
572
+ } catch (err) {
573
+ const msg = err instanceof Error ? err.message : "Upload failed";
574
+ console.error("File upload error:", err);
575
+ setFileUploadState((prev) => ({
576
+ ...prev,
577
+ [fieldId]: { isUploading: false, progress: 0, fileName: null, error: msg }
578
+ }));
579
+ }
580
+ };
581
+ const handleFileClear = (fieldId) => {
582
+ handleFieldChange(fieldId, "");
583
+ setFileUploadState((prev) => ({
584
+ ...prev,
585
+ [fieldId]: { isUploading: false, progress: 0, fileName: null, error: null }
586
+ }));
587
+ };
588
+ const getFieldStatusText = (field) => {
589
+ const value = formValues[field.id];
590
+ switch (field.type) {
591
+ case "text":
592
+ case "textarea": {
593
+ const s = value;
594
+ return s && s.trim().length > 0 ? `Text entered (${s.length} characters)` : null;
595
+ }
596
+ case "number":
597
+ return value !== "" && value !== null && value !== void 0 ? `Value set: ${value}` : null;
598
+ case "select": {
599
+ if (!value) return null;
600
+ const opt = field.options?.find((o) => o.id === value);
601
+ return `Selected: ${opt?.label ?? value}`;
602
+ }
603
+ case "multiselect":
604
+ case "checkbox": {
605
+ const arr = value;
606
+ if (!Array.isArray(arr) || arr.length === 0) return null;
607
+ const labels = arr.map((v) => {
608
+ const o = field.options?.find((opt) => opt.id === v);
609
+ return o?.label ?? v;
610
+ });
611
+ return `${arr.length} option(s) selected: ${labels.join(", ")}`;
612
+ }
613
+ case "radio": {
614
+ if (!value) return null;
615
+ const opt = field.options?.find((o) => o.id === value);
616
+ return `Selected: ${opt?.label ?? value}`;
617
+ }
618
+ default:
619
+ return null;
620
+ }
621
+ };
622
+ const handleApproval = async (fieldId, approved) => {
623
+ const action = approved ? "approve" : "reject";
624
+ handleFieldChange(fieldId, action);
625
+ await submitForm(approved ? "Approve" : "Reject", { ...formValues, [fieldId]: action });
626
+ };
627
+ const validateForm = () => {
628
+ for (const field of fields) {
629
+ if (field.required) {
630
+ const value = formValues[field.id];
631
+ if (value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0) {
632
+ setError(`${field.label} is required`);
633
+ return false;
634
+ }
635
+ }
636
+ }
637
+ return true;
638
+ };
639
+ const submitForm = async (userAction, values) => {
640
+ setIsLoading(true);
641
+ const resumePayload = {
642
+ user_action: userAction,
643
+ form_title: title,
644
+ values
645
+ };
646
+ const messageText = `User clicked "${userAction}" on form "${title}"`;
647
+ try {
648
+ await onSubmit(resumePayload, messageText);
649
+ setSubmittedData({ user_action: userAction, values });
650
+ setIsSubmitted(true);
651
+ } catch (err) {
652
+ const msg = err instanceof Error ? err.message : "Failed to submit form";
653
+ console.error("DynamicForm submit error:", err);
654
+ setError(msg);
655
+ } finally {
656
+ setIsLoading(false);
657
+ }
658
+ };
659
+ const handleSubmit = async (e) => {
660
+ e.preventDefault();
661
+ if (!validateForm()) return;
662
+ await submitForm(submitText, formValues);
663
+ };
664
+ const handleCancel = async () => {
665
+ await submitForm(cancelText ?? "Cancel", formValues);
666
+ };
667
+ const renderField = (field) => {
668
+ switch (field.type) {
669
+ case "text":
670
+ return /* @__PURE__ */ jsx10(
671
+ Input,
672
+ {
673
+ type: "text",
674
+ value: formValues[field.id] || "",
675
+ onChange: (e) => handleFieldChange(field.id, e.target.value),
676
+ placeholder: field.placeholder,
677
+ className: "bg-background w-full h-8 text-xs"
678
+ }
679
+ );
680
+ case "textarea":
681
+ return /* @__PURE__ */ jsx10(
682
+ Textarea,
683
+ {
684
+ value: formValues[field.id] || "",
685
+ onChange: (e) => handleFieldChange(field.id, e.target.value),
686
+ placeholder: field.placeholder,
687
+ className: "bg-background min-h-[72px] w-full text-xs"
688
+ }
689
+ );
690
+ case "number":
691
+ return /* @__PURE__ */ jsx10(
692
+ Input,
693
+ {
694
+ type: "number",
695
+ value: formValues[field.id] || "",
696
+ onChange: (e) => handleFieldChange(field.id, e.target.value ? Number(e.target.value) : ""),
697
+ placeholder: field.placeholder,
698
+ className: "bg-background w-full h-8 text-xs"
699
+ }
700
+ );
701
+ case "select":
702
+ return /* @__PURE__ */ jsxs2(
703
+ Select,
704
+ {
705
+ value: formValues[field.id] || "",
706
+ onValueChange: (value) => handleFieldChange(field.id, value),
707
+ children: [
708
+ /* @__PURE__ */ jsx10(SelectTrigger, { className: "bg-background w-full h-8 text-xs", children: /* @__PURE__ */ jsx10(SelectValue, { placeholder: field.placeholder || "Select an option..." }) }),
709
+ /* @__PURE__ */ jsx10(SelectContent, { children: field.options?.map((option) => /* @__PURE__ */ jsx10(SelectItem, { value: option.id, className: "text-xs", children: option.label }, option.id)) })
710
+ ]
711
+ }
712
+ );
713
+ case "multiselect":
714
+ case "checkbox":
715
+ return /* @__PURE__ */ jsx10("div", { className: "space-y-0.5", children: field.options?.map((option) => /* @__PURE__ */ jsxs2(
716
+ "label",
717
+ {
718
+ className: "flex items-start gap-2 cursor-pointer py-1 px-1.5 rounded hover:bg-muted/50 transition-colors",
719
+ children: [
720
+ /* @__PURE__ */ jsx10(
721
+ Checkbox,
722
+ {
723
+ checked: (formValues[field.id] || []).includes(option.id),
724
+ onCheckedChange: (checked) => handleCheckboxChange(field.id, option.id, !!checked),
725
+ className: "mt-0.5 h-3.5 w-3.5 border-[1.5px] border-muted-foreground/50"
726
+ }
727
+ ),
728
+ /* @__PURE__ */ jsxs2("div", { className: "flex-1", children: [
729
+ /* @__PURE__ */ jsx10("span", { className: "text-xs font-medium", children: option.label }),
730
+ option.description && /* @__PURE__ */ jsx10("p", { className: "text-[11px] text-muted-foreground", children: option.description })
731
+ ] })
732
+ ]
733
+ },
734
+ option.id
735
+ )) });
736
+ case "radio":
737
+ return /* @__PURE__ */ jsx10(
738
+ RadioGroup,
739
+ {
740
+ value: formValues[field.id] || "",
741
+ onValueChange: (value) => handleFieldChange(field.id, value),
742
+ className: "space-y-0.5",
743
+ children: field.options?.map((option) => /* @__PURE__ */ jsxs2(
744
+ "label",
745
+ {
746
+ className: "flex items-start gap-2 cursor-pointer py-1 px-1.5 rounded hover:bg-muted/50 transition-colors",
747
+ children: [
748
+ /* @__PURE__ */ jsx10(RadioGroupItem, { value: option.id, className: "mt-0.5" }),
749
+ /* @__PURE__ */ jsxs2("div", { className: "flex-1", children: [
750
+ /* @__PURE__ */ jsx10("span", { className: "text-xs font-medium", children: option.label }),
751
+ option.description && /* @__PURE__ */ jsx10("p", { className: "text-[11px] text-muted-foreground", children: option.description })
752
+ ] })
753
+ ]
67
754
  },
755
+ option.id
756
+ ))
757
+ }
758
+ );
759
+ case "approval":
760
+ return /* @__PURE__ */ jsxs2("div", { className: "flex gap-2 justify-end", children: [
761
+ /* @__PURE__ */ jsxs2(
762
+ Button,
763
+ {
764
+ type: "button",
765
+ variant: "destructive",
766
+ size: "sm",
767
+ onClick: () => handleApproval(field.id, false),
768
+ className: "bg-destructive/10 text-destructive hover:bg-destructive/20 border-transparent shadow-none h-8 text-xs",
769
+ disabled: isLoading,
68
770
  children: [
69
- field.label,
70
- field.required && /* @__PURE__ */ jsx("span", { style: { color: "#ef4444" }, children: " *" })
771
+ /* @__PURE__ */ jsx10(AlertCircle, { className: "w-3 h-3 mr-1.5" }),
772
+ "Reject"
71
773
  ]
72
774
  }
73
775
  ),
74
- field.type === "textarea" ? /* @__PURE__ */ jsx(
75
- "textarea",
776
+ /* @__PURE__ */ jsxs2(
777
+ Button,
76
778
  {
77
- style: {
78
- width: "100%",
79
- padding: "8px",
80
- border: "1px solid #d1d5db",
81
- borderRadius: "6px",
82
- fontSize: "14px",
83
- resize: "vertical"
84
- },
85
- placeholder: field.placeholder,
86
- value: formValues[field.id] || "",
87
- onChange: (e) => handleChange(field.id, e.target.value)
88
- }
89
- ) : field.type === "select" ? /* @__PURE__ */ jsxs(
90
- "select",
91
- {
92
- style: {
93
- width: "100%",
94
- padding: "8px",
95
- border: "1px solid #d1d5db",
96
- borderRadius: "6px",
97
- fontSize: "14px"
98
- },
99
- value: formValues[field.id] || "",
100
- onChange: (e) => handleChange(field.id, e.target.value),
779
+ type: "button",
780
+ size: "sm",
781
+ onClick: () => handleApproval(field.id, true),
782
+ className: "bg-emerald-600 hover:bg-emerald-700 text-white shadow-none border-transparent h-8 text-xs",
783
+ disabled: isLoading,
101
784
  children: [
102
- /* @__PURE__ */ jsx("option", { value: "", children: "Select..." }),
103
- field.options?.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
785
+ /* @__PURE__ */ jsx10(CheckCircle, { className: "w-3 h-3 mr-1.5" }),
786
+ "Approve"
104
787
  ]
105
788
  }
106
- ) : /* @__PURE__ */ jsx(
107
- "input",
108
- {
109
- style: {
110
- width: "100%",
111
- padding: "8px",
112
- border: "1px solid #d1d5db",
113
- borderRadius: "6px",
114
- fontSize: "14px"
115
- },
116
- type: field.type === "number" ? "number" : "text",
117
- placeholder: field.placeholder,
118
- value: formValues[field.id] || "",
119
- onChange: (e) => handleChange(field.id, e.target.value)
120
- }
121
789
  )
122
- ] }, field.id)),
123
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "8px", marginTop: "16px" }, children: [
124
- /* @__PURE__ */ jsx(
125
- "button",
790
+ ] });
791
+ case "todo_list": {
792
+ const todoItems = formValues[field.id] || [];
793
+ const completedCount = todoItems.filter((i) => i.status === "completed").length;
794
+ const totalCount = todoItems.length;
795
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-1.5", children: [
796
+ totalCount > 0 && /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5 text-[11px] text-muted-foreground", children: [
797
+ /* @__PURE__ */ jsx10(CheckCircle, { className: "w-3 h-3" }),
798
+ /* @__PURE__ */ jsxs2("span", { children: [
799
+ completedCount,
800
+ " of ",
801
+ totalCount,
802
+ " completed"
803
+ ] })
804
+ ] }),
805
+ /* @__PURE__ */ jsx10("div", { className: "space-y-0", children: todoItems.map((item) => /* @__PURE__ */ jsxs2(
806
+ "div",
126
807
  {
127
- onClick: handleSubmit,
128
- style: {
129
- padding: "8px 16px",
130
- background: "#111",
131
- color: "#fff",
132
- border: "none",
133
- borderRadius: "6px",
134
- cursor: "pointer",
135
- fontSize: "14px",
136
- fontWeight: 500
137
- },
138
- children: submitText
139
- }
140
- ),
141
- cancelText && /* @__PURE__ */ jsx(
142
- "button",
143
- {
144
- onClick: onCancel,
145
- style: {
146
- padding: "8px 16px",
147
- background: "transparent",
148
- color: "#666",
149
- border: "1px solid #d1d5db",
150
- borderRadius: "6px",
151
- cursor: "pointer",
152
- fontSize: "14px"
153
- },
154
- children: cancelText
155
- }
156
- )
157
- ] })
158
- ]
808
+ className: "flex items-start gap-2 py-1 px-1.5 rounded hover:bg-muted/30 transition-all group",
809
+ children: [
810
+ /* @__PURE__ */ jsx10(
811
+ Checkbox,
812
+ {
813
+ checked: item.status === "completed",
814
+ onCheckedChange: () => handleTodoToggle(field.id, item.id),
815
+ className: "mt-0.5 h-3.5 w-3.5 rounded-full border-[1.5px] border-muted-foreground/50"
816
+ }
817
+ ),
818
+ editingTodoId === item.id ? /* @__PURE__ */ jsx10(
819
+ Input,
820
+ {
821
+ type: "text",
822
+ value: editingTodoContent,
823
+ onChange: (e) => setEditingTodoContent(e.target.value),
824
+ onKeyDown: (e) => {
825
+ if (e.key === "Enter") {
826
+ handleTodoEdit(field.id, item.id, editingTodoContent);
827
+ setEditingTodoId(null);
828
+ setEditingTodoContent("");
829
+ } else if (e.key === "Escape") {
830
+ setEditingTodoId(null);
831
+ setEditingTodoContent("");
832
+ }
833
+ },
834
+ onBlur: () => {
835
+ handleTodoEdit(field.id, item.id, editingTodoContent);
836
+ setEditingTodoId(null);
837
+ setEditingTodoContent("");
838
+ },
839
+ autoFocus: true,
840
+ className: "flex-1 h-6 text-xs bg-background"
841
+ }
842
+ ) : /* @__PURE__ */ jsx10(
843
+ "span",
844
+ {
845
+ className: `text-xs flex-1 leading-snug ${item.status === "completed" ? "line-through text-muted-foreground" : ""}`,
846
+ children: item.content
847
+ }
848
+ ),
849
+ /* @__PURE__ */ jsx10("div", { className: "flex items-center gap-0.5 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity", children: editingTodoId !== item.id && /* @__PURE__ */ jsxs2(Fragment, { children: [
850
+ /* @__PURE__ */ jsx10(
851
+ "button",
852
+ {
853
+ type: "button",
854
+ onClick: () => {
855
+ setEditingTodoId(item.id);
856
+ setEditingTodoContent(item.content);
857
+ },
858
+ className: "p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground",
859
+ title: "Edit step",
860
+ children: /* @__PURE__ */ jsx10(Pencil, { className: "w-3 h-3" })
861
+ }
862
+ ),
863
+ /* @__PURE__ */ jsx10(
864
+ "button",
865
+ {
866
+ type: "button",
867
+ onClick: () => handleTodoDelete(field.id, item.id),
868
+ className: "p-1 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive",
869
+ title: "Remove step",
870
+ children: /* @__PURE__ */ jsx10(Trash2, { className: "w-3 h-3" })
871
+ }
872
+ )
873
+ ] }) })
874
+ ]
875
+ },
876
+ item.id
877
+ )) }),
878
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5 pt-1", children: [
879
+ /* @__PURE__ */ jsx10(
880
+ Input,
881
+ {
882
+ type: "text",
883
+ value: newTodoInputs[field.id] || "",
884
+ onChange: (e) => setNewTodoInputs((prev) => ({ ...prev, [field.id]: e.target.value })),
885
+ onKeyDown: (e) => {
886
+ if (e.key === "Enter") {
887
+ e.preventDefault();
888
+ handleTodoAdd(field.id, newTodoInputs[field.id] || "");
889
+ setNewTodoInputs((prev) => ({ ...prev, [field.id]: "" }));
890
+ }
891
+ },
892
+ placeholder: "Ask for follow-up changes",
893
+ className: "flex-1 h-7 text-xs bg-muted/30 border-transparent hover:bg-muted/50 focus:bg-background transition-colors"
894
+ }
895
+ ),
896
+ /* @__PURE__ */ jsx10(
897
+ Button,
898
+ {
899
+ type: "button",
900
+ variant: "ghost",
901
+ size: "sm",
902
+ disabled: !(newTodoInputs[field.id] || "").trim(),
903
+ onClick: () => {
904
+ handleTodoAdd(field.id, newTodoInputs[field.id] || "");
905
+ setNewTodoInputs((prev) => ({ ...prev, [field.id]: "" }));
906
+ },
907
+ className: "h-7 px-2",
908
+ children: /* @__PURE__ */ jsx10(Plus, { className: "w-3.5 h-3.5" })
909
+ }
910
+ )
911
+ ] })
912
+ ] });
913
+ }
914
+ case "file": {
915
+ const uploadState = fileUploadState[field.id] || {
916
+ isUploading: false,
917
+ progress: 0,
918
+ fileName: null,
919
+ error: null
920
+ };
921
+ const val = formValues[field.id];
922
+ const hasFile = !!val && (typeof val === "string" ? val.length > 0 : Array.isArray(val) && val.length > 0);
923
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
924
+ !hasFile && !uploadState.isUploading && /* @__PURE__ */ jsxs2("label", { className: "flex flex-col items-center justify-center w-full h-32 border border-dashed rounded-lg cursor-pointer bg-muted/20 hover:bg-muted/40 transition-all duration-200 border-border group", children: [
925
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center pt-5 pb-6", children: [
926
+ /* @__PURE__ */ jsx10(Upload, { className: "w-8 h-8 mb-2 text-muted-foreground group-hover:text-primary transition-colors" }),
927
+ /* @__PURE__ */ jsxs2("p", { className: "mb-1 text-sm text-muted-foreground", children: [
928
+ /* @__PURE__ */ jsx10("span", { className: "font-semibold group-hover:text-foreground transition-colors", children: "Click to upload" }),
929
+ " ",
930
+ "or drag and drop"
931
+ ] }),
932
+ field.accept && /* @__PURE__ */ jsxs2("p", { className: "text-xs text-muted-foreground/70", children: [
933
+ "Accepted: ",
934
+ field.accept
935
+ ] })
936
+ ] }),
937
+ /* @__PURE__ */ jsx10(
938
+ "input",
939
+ {
940
+ type: "file",
941
+ className: "hidden",
942
+ accept: field.accept,
943
+ multiple: field.multiple,
944
+ onChange: (e) => handleFileUpload(field.id, e.target.files, field.multiple)
945
+ }
946
+ )
947
+ ] }),
948
+ uploadState.isUploading && /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center w-full h-32 border rounded-lg bg-muted/20 border-border", children: [
949
+ /* @__PURE__ */ jsx10(Loader2, { className: "w-7 h-7 mb-2 text-primary animate-spin" }),
950
+ /* @__PURE__ */ jsxs2("p", { className: "text-sm text-muted-foreground", children: [
951
+ "Uploading ",
952
+ uploadState.fileName,
953
+ "..."
954
+ ] }),
955
+ /* @__PURE__ */ jsx10(Progress, { value: uploadState.progress, className: "w-48 mt-2.5 h-1.5" }),
956
+ /* @__PURE__ */ jsxs2("p", { className: "text-xs text-muted-foreground/70 mt-1.5", children: [
957
+ uploadState.progress,
958
+ "%"
959
+ ] })
960
+ ] }),
961
+ hasFile && !uploadState.isUploading && /* @__PURE__ */ jsxs2("div", { className: "space-y-2", children: [
962
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center space-x-2 text-emerald-600", children: [
963
+ /* @__PURE__ */ jsx10(CheckCircle, { className: "w-4 h-4" }),
964
+ /* @__PURE__ */ jsx10("span", { className: "text-sm font-medium", children: Array.isArray(val) ? `${val.length} file(s) uploaded successfully` : "File uploaded successfully" })
965
+ ] }),
966
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between gap-2 p-3 border rounded-lg bg-muted/30 border-border overflow-hidden", children: [
967
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center space-x-3 min-w-0 flex-1", children: [
968
+ /* @__PURE__ */ jsx10("div", { className: "w-10 h-10 bg-muted rounded-md flex items-center justify-center flex-shrink-0", children: /* @__PURE__ */ jsx10(File, { className: "w-5 h-5 text-muted-foreground" }) }),
969
+ /* @__PURE__ */ jsxs2("div", { className: "min-w-0 flex-1", children: [
970
+ /* @__PURE__ */ jsx10("p", { className: "text-sm font-medium truncate", children: uploadState.fileName || "File uploaded" }),
971
+ /* @__PURE__ */ jsx10("p", { className: "text-xs text-muted-foreground truncate", children: typeof val === "string" ? val : `${val.length} file(s)` })
972
+ ] })
973
+ ] }),
974
+ /* @__PURE__ */ jsx10(
975
+ Button,
976
+ {
977
+ type: "button",
978
+ variant: "ghost",
979
+ size: "sm",
980
+ onClick: () => handleFileClear(field.id),
981
+ className: "text-muted-foreground hover:text-destructive transition-colors",
982
+ children: /* @__PURE__ */ jsx10(X, { className: "w-4 h-4" })
983
+ }
984
+ )
985
+ ] })
986
+ ] }),
987
+ uploadState.error && /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 text-destructive text-sm bg-destructive/5 border border-destructive/20 rounded-lg p-2.5", children: [
988
+ /* @__PURE__ */ jsx10(AlertCircle, { className: "w-4 h-4 flex-shrink-0" }),
989
+ /* @__PURE__ */ jsx10("span", { children: uploadState.error })
990
+ ] })
991
+ ] });
992
+ }
993
+ default:
994
+ return /* @__PURE__ */ jsxs2("div", { className: "text-sm text-muted-foreground", children: [
995
+ "Unsupported field type: ",
996
+ field.type
997
+ ] });
159
998
  }
160
- );
999
+ };
1000
+ if (isLoading) {
1001
+ return /* @__PURE__ */ jsx10("div", { className: "w-full py-8 text-center animate-in fade-in duration-300", children: /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center gap-3", children: [
1002
+ /* @__PURE__ */ jsx10(Loader2, { className: "w-6 h-6 text-primary animate-spin" }),
1003
+ /* @__PURE__ */ jsx10("p", { className: "text-sm text-muted-foreground", children: "Submitting response..." })
1004
+ ] }) });
1005
+ }
1006
+ if (isSubmitted) {
1007
+ const wasCancelled = submittedData?.cancelled;
1008
+ return /* @__PURE__ */ jsx10("div", { className: "w-full py-6 animate-in fade-in duration-300", children: /* @__PURE__ */ jsxs2(
1009
+ "div",
1010
+ {
1011
+ className: `rounded-lg border p-4 flex items-start gap-4 ${wasCancelled ? "bg-amber-50/50 border-amber-200" : "bg-emerald-50/50 border-emerald-200"} dark:bg-muted/20 dark:border-border`,
1012
+ children: [
1013
+ /* @__PURE__ */ jsx10("div", { className: `mt-0.5 ${wasCancelled ? "text-amber-500" : "text-emerald-500"}`, children: /* @__PURE__ */ jsx10(CheckCircle, { className: "w-5 h-5" }) }),
1014
+ /* @__PURE__ */ jsxs2("div", { className: "flex-1", children: [
1015
+ /* @__PURE__ */ jsx10("h3", { className: "text-sm font-medium mb-1", children: wasCancelled ? "Cancelled" : "Response Submitted" }),
1016
+ /* @__PURE__ */ jsx10("p", { className: "text-xs text-muted-foreground mb-3", children: title }),
1017
+ submittedData && !wasCancelled && /* @__PURE__ */ jsx10("div", { className: "bg-background/50 rounded p-2 text-xs text-muted-foreground font-mono overflow-hidden", children: JSON.stringify(submittedData, null, 2) })
1018
+ ] })
1019
+ ]
1020
+ }
1021
+ ) });
1022
+ }
1023
+ return /* @__PURE__ */ jsxs2(Card, { className: "w-[60%] max-w-2xl max-h-[65vh] flex flex-col border border-border bg-card shadow-sm animate-in fade-in duration-300", children: [
1024
+ /* @__PURE__ */ jsxs2(CardHeader, { className: "pb-2 pt-4 px-5 flex-shrink-0", children: [
1025
+ /* @__PURE__ */ jsx10(CardTitle, { className: "text-sm font-semibold tracking-tight", children: title }),
1026
+ description && /* @__PURE__ */ jsx10(CardDescription, { className: "text-xs text-muted-foreground leading-snug", children: description })
1027
+ ] }),
1028
+ /* @__PURE__ */ jsxs2("form", { onSubmit: handleSubmit, className: "relative flex-1 min-h-0 flex flex-col", children: [
1029
+ /* @__PURE__ */ jsx10("div", { className: `scroll-fade-top ${!canScrollUp ? "fade-hidden" : ""}` }),
1030
+ /* @__PURE__ */ jsx10(
1031
+ "div",
1032
+ {
1033
+ ref: scrollRef,
1034
+ className: "px-5 overflow-y-auto flex-1 min-h-0 dynamic-form-scroll",
1035
+ children: /* @__PURE__ */ jsxs2("div", { className: "space-y-3 pb-2", children: [
1036
+ fields.map((field) => {
1037
+ const statusText = getFieldStatusText(field);
1038
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-1", children: [
1039
+ /* @__PURE__ */ jsxs2(Label2, { className: "text-xs font-medium flex items-center gap-1.5", children: [
1040
+ /* @__PURE__ */ jsx10("span", { children: field.label }),
1041
+ field.required && /* @__PURE__ */ jsx10("span", { className: "text-destructive text-[10px]", children: "*" })
1042
+ ] }),
1043
+ field.description && /* @__PURE__ */ jsx10("p", { className: "text-[11px] text-muted-foreground leading-snug", children: field.description }),
1044
+ renderField(field),
1045
+ statusText && field.type !== "file" && field.type !== "todo_list" && /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1 text-emerald-600 mt-0.5", children: [
1046
+ /* @__PURE__ */ jsx10(CheckCircle, { className: "w-2.5 h-2.5" }),
1047
+ /* @__PURE__ */ jsx10("span", { className: "text-[10px]", children: statusText })
1048
+ ] })
1049
+ ] }, field.id);
1050
+ }),
1051
+ error && /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 text-destructive text-xs bg-destructive/5 border border-destructive/20 rounded-md p-2", children: [
1052
+ /* @__PURE__ */ jsx10(AlertCircle, { className: "w-3.5 h-3.5 flex-shrink-0" }),
1053
+ /* @__PURE__ */ jsx10("span", { children: error })
1054
+ ] })
1055
+ ] })
1056
+ }
1057
+ ),
1058
+ /* @__PURE__ */ jsx10("div", { className: `scroll-fade-bottom ${!canScrollDown ? "fade-hidden" : ""}` }),
1059
+ /* @__PURE__ */ jsxs2("div", { className: "flex justify-end gap-2 px-5 py-2 flex-shrink-0", children: [
1060
+ cancelText && /* @__PURE__ */ jsx10(
1061
+ Button,
1062
+ {
1063
+ type: "button",
1064
+ variant: "ghost",
1065
+ size: "sm",
1066
+ onClick: handleCancel,
1067
+ className: "text-muted-foreground hover:text-foreground h-8 text-xs",
1068
+ children: cancelText
1069
+ }
1070
+ ),
1071
+ /* @__PURE__ */ jsxs2(
1072
+ Button,
1073
+ {
1074
+ type: "submit",
1075
+ size: "sm",
1076
+ className: "bg-primary text-primary-foreground shadow-none hover:bg-primary/90 h-8 text-xs",
1077
+ children: [
1078
+ /* @__PURE__ */ jsx10(Send, { className: "w-3 h-3 mr-1.5" }),
1079
+ submitText
1080
+ ]
1081
+ }
1082
+ )
1083
+ ] })
1084
+ ] })
1085
+ ] });
161
1086
  }
162
1087
 
163
1088
  // src/manifest.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamoid/chat-components",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Shared chat UI components for the Streamoid chat host — DynamicForm and other cross-service components",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -19,7 +19,20 @@
19
19
  "build": "tsup",
20
20
  "prepublishOnly": "npm run build"
21
21
  },
22
+ "dependencies": {
23
+ "@radix-ui/react-checkbox": "^1.1.0",
24
+ "@radix-ui/react-label": "^2.1.0",
25
+ "@radix-ui/react-progress": "^1.1.0",
26
+ "@radix-ui/react-radio-group": "^1.2.0",
27
+ "@radix-ui/react-select": "^2.1.0",
28
+ "@radix-ui/react-separator": "^1.1.0",
29
+ "@radix-ui/react-slot": "^1.1.0",
30
+ "class-variance-authority": "^0.7.0",
31
+ "clsx": "^2.1.0",
32
+ "tailwind-merge": "^2.3.0"
33
+ },
22
34
  "peerDependencies": {
35
+ "lucide-react": ">=0.200.0",
23
36
  "react": "^18.0.0",
24
37
  "react-dom": "^18.0.0"
25
38
  },