@uploadista/react 0.0.20-beta.2 → 0.0.20-beta.3

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 (36) hide show
  1. package/dist/components/index.d.mts +3 -3
  2. package/dist/components/index.mjs +1 -1
  3. package/dist/flow-upload-list-SbpCaxRq.mjs +2 -0
  4. package/dist/flow-upload-list-SbpCaxRq.mjs.map +1 -0
  5. package/dist/hooks/index.d.mts +3 -3
  6. package/dist/hooks/index.mjs +1 -1
  7. package/dist/index.d.mts +6 -6
  8. package/dist/index.mjs +1 -1
  9. package/dist/{uploadista-provider-D-N-eL2l.d.mts → uploadista-provider-C1l0iBc9.d.mts} +512 -318
  10. package/dist/uploadista-provider-C1l0iBc9.d.mts.map +1 -0
  11. package/dist/use-upload-BvvGROMR.mjs +2 -0
  12. package/dist/use-upload-BvvGROMR.mjs.map +1 -0
  13. package/dist/{use-uploadista-client-m9nF-irM.d.mts → use-uploadista-client-DHbLSpIb.d.mts} +120 -286
  14. package/dist/use-uploadista-client-DHbLSpIb.d.mts.map +1 -0
  15. package/dist/use-uploadista-events-BwUD-2Ck.mjs +2 -0
  16. package/dist/use-uploadista-events-BwUD-2Ck.mjs.map +1 -0
  17. package/dist/{use-upload-metrics-DhzS4lhG.d.mts → use-uploadista-events-CtDXJYrR.d.mts} +169 -371
  18. package/dist/use-uploadista-events-CtDXJYrR.d.mts.map +1 -0
  19. package/package.json +6 -6
  20. package/src/components/flow-primitives.tsx +839 -0
  21. package/src/components/index.tsx +31 -13
  22. package/src/hooks/index.ts +25 -37
  23. package/src/index.ts +90 -81
  24. package/dist/upload-zone-BjWHuP7p.mjs +0 -6
  25. package/dist/upload-zone-BjWHuP7p.mjs.map +0 -1
  26. package/dist/uploadista-provider-D-N-eL2l.d.mts.map +0 -1
  27. package/dist/use-upload-BDHVhQsI.mjs +0 -2
  28. package/dist/use-upload-BDHVhQsI.mjs.map +0 -1
  29. package/dist/use-upload-metrics-Df90wIos.mjs +0 -2
  30. package/dist/use-upload-metrics-Df90wIos.mjs.map +0 -1
  31. package/dist/use-upload-metrics-DhzS4lhG.d.mts.map +0 -1
  32. package/dist/use-uploadista-client-m9nF-irM.d.mts.map +0 -1
  33. package/src/components/flow-input.tsx +0 -299
  34. package/src/components/flow-upload-zone.tsx +0 -441
  35. package/src/hooks/use-flow-execution.ts +0 -502
  36. package/src/hooks/use-flow-upload.ts +0 -299
@@ -0,0 +1,839 @@
1
+ "use client";
2
+
3
+ import type { FlowUploadOptions } from "@uploadista/client-browser";
4
+ import type {
5
+ FlowUploadState,
6
+ FlowUploadStatus,
7
+ InputExecutionState,
8
+ } from "@uploadista/client-core";
9
+ import type { TypedOutput } from "@uploadista/core/flow";
10
+ import {
11
+ type ReactNode,
12
+ createContext,
13
+ useCallback,
14
+ useContext,
15
+ } from "react";
16
+ import {
17
+ type DragDropState,
18
+ type UseDragDropReturn,
19
+ useDragDrop,
20
+ } from "../hooks/use-drag-drop";
21
+ import { type FlowInputMetadata, useFlow } from "../hooks/use-flow";
22
+
23
+ // Re-export types for convenience
24
+ export type {
25
+ FlowUploadState,
26
+ FlowUploadStatus,
27
+ InputExecutionState,
28
+ FlowInputMetadata,
29
+ };
30
+
31
+ // ============ FLOW CONTEXT ============
32
+
33
+ /**
34
+ * Context value provided by the Flow component root.
35
+ * Contains all flow state and actions.
36
+ */
37
+ export interface FlowContextValue {
38
+ /** Current upload state */
39
+ state: FlowUploadState;
40
+ /** Discovered input nodes metadata (null until discovery completes) */
41
+ inputMetadata: FlowInputMetadata[] | null;
42
+ /** Current input values set via setInput() */
43
+ inputs: Record<string, unknown>;
44
+ /** Per-input execution state for multi-input flows */
45
+ inputStates: ReadonlyMap<string, InputExecutionState>;
46
+
47
+ /** Set an input value for a specific node */
48
+ setInput: (nodeId: string, value: unknown) => void;
49
+ /** Execute the flow with current inputs */
50
+ execute: () => Promise<void>;
51
+ /** Upload a single file through the flow */
52
+ upload: (file: File | Blob) => Promise<void>;
53
+ /** Abort the current upload */
54
+ abort: () => void;
55
+ /** Pause the current upload */
56
+ pause: () => void;
57
+ /** Reset the upload state and clear all inputs */
58
+ reset: () => void;
59
+
60
+ /** Whether an upload or flow execution is in progress */
61
+ isUploading: boolean;
62
+ /** Whether the file is currently being uploaded */
63
+ isUploadingFile: boolean;
64
+ /** Whether the flow is currently processing */
65
+ isProcessing: boolean;
66
+ /** Whether the hook is discovering flow inputs */
67
+ isDiscoveringInputs: boolean;
68
+ }
69
+
70
+ const FlowContext = createContext<FlowContextValue | null>(null);
71
+
72
+ /**
73
+ * Hook to access flow context from within a Flow component.
74
+ * @throws Error if used outside of a Flow component
75
+ */
76
+ export function useFlowContext(): FlowContextValue {
77
+ const context = useContext(FlowContext);
78
+ if (!context) {
79
+ throw new Error(
80
+ "useFlowContext must be used within a <Flow> component. " +
81
+ "Wrap your component tree with <Flow flowId=\"...\" storageId=\"...\">",
82
+ );
83
+ }
84
+ return context;
85
+ }
86
+
87
+ // ============ FLOW INPUT CONTEXT ============
88
+
89
+ /**
90
+ * Context value for a specific input node within a Flow.
91
+ */
92
+ export interface FlowInputContextValue {
93
+ /** Input node ID */
94
+ nodeId: string;
95
+ /** Input metadata from flow discovery */
96
+ metadata: FlowInputMetadata;
97
+ /** Current value for this input */
98
+ value: unknown;
99
+ /** Set the value for this input */
100
+ setValue: (value: unknown) => void;
101
+ /** Per-input execution state (if available) */
102
+ state: InputExecutionState | undefined;
103
+ }
104
+
105
+ const FlowInputContext = createContext<FlowInputContextValue | null>(null);
106
+
107
+ /**
108
+ * Hook to access flow input context from within a Flow.Input component.
109
+ * @throws Error if used outside of a Flow.Input component
110
+ */
111
+ export function useFlowInputContext(): FlowInputContextValue {
112
+ const context = useContext(FlowInputContext);
113
+ if (!context) {
114
+ throw new Error(
115
+ "useFlowInputContext must be used within a <Flow.Input> component. " +
116
+ "Wrap your component with <Flow.Input nodeId=\"...\">",
117
+ );
118
+ }
119
+ return context;
120
+ }
121
+
122
+ // ============ FLOW ROOT COMPONENT ============
123
+
124
+ /**
125
+ * Props for the Flow root component.
126
+ */
127
+ export interface FlowProps {
128
+ /** Flow ID to execute */
129
+ flowId: string;
130
+ /** Storage ID for file uploads */
131
+ storageId: string;
132
+ /** Optional output node ID to wait for */
133
+ outputNodeId?: string;
134
+ /** Called when flow completes successfully */
135
+ onSuccess?: (outputs: TypedOutput[]) => void;
136
+ /** Called when flow fails */
137
+ onError?: (error: Error) => void;
138
+ /** Called on upload progress */
139
+ onProgress?: (
140
+ uploadId: string,
141
+ bytesUploaded: number,
142
+ totalBytes: number | null,
143
+ ) => void;
144
+ /** Called when flow completes with all outputs */
145
+ onFlowComplete?: (outputs: TypedOutput[]) => void;
146
+ /** Called when upload is aborted */
147
+ onAbort?: () => void;
148
+ /** Children to render */
149
+ children: ReactNode;
150
+ }
151
+
152
+ /**
153
+ * Root component for flow-based uploads.
154
+ * Provides context for all Flow sub-components.
155
+ *
156
+ * @example
157
+ * ```tsx
158
+ * <Flow flowId="image-optimizer" storageId="s3" onSuccess={handleSuccess}>
159
+ * <Flow.DropZone accept="image/*">
160
+ * {({ isDragging, getRootProps, getInputProps }) => (
161
+ * <div {...getRootProps()}>
162
+ * <input {...getInputProps()} />
163
+ * {isDragging ? "Drop here" : "Drag or click"}
164
+ * </div>
165
+ * )}
166
+ * </Flow.DropZone>
167
+ * </Flow>
168
+ * ```
169
+ */
170
+ function FlowRoot({
171
+ flowId,
172
+ storageId,
173
+ outputNodeId,
174
+ onSuccess,
175
+ onError,
176
+ onProgress,
177
+ onFlowComplete,
178
+ onAbort,
179
+ children,
180
+ }: FlowProps) {
181
+ const options: FlowUploadOptions = {
182
+ flowConfig: {
183
+ flowId,
184
+ storageId,
185
+ outputNodeId,
186
+ },
187
+ onSuccess,
188
+ onError,
189
+ onProgress,
190
+ onFlowComplete,
191
+ onAbort,
192
+ };
193
+
194
+ const flow = useFlow(options);
195
+
196
+ const contextValue: FlowContextValue = {
197
+ state: flow.state,
198
+ inputMetadata: flow.inputMetadata,
199
+ inputs: flow.inputs,
200
+ inputStates: flow.inputStates,
201
+ setInput: flow.setInput,
202
+ execute: flow.execute,
203
+ upload: flow.upload,
204
+ abort: flow.abort,
205
+ pause: flow.pause,
206
+ reset: flow.reset,
207
+ isUploading: flow.isUploading,
208
+ isUploadingFile: flow.isUploadingFile,
209
+ isProcessing: flow.isProcessing,
210
+ isDiscoveringInputs: flow.isDiscoveringInputs,
211
+ };
212
+
213
+ return (
214
+ <FlowContext.Provider value={contextValue}>{children}</FlowContext.Provider>
215
+ );
216
+ }
217
+
218
+ // ============ DROP ZONE PRIMITIVE ============
219
+
220
+ /**
221
+ * Render props for Flow.DropZone component.
222
+ */
223
+ export interface FlowDropZoneRenderProps {
224
+ /** Whether files are being dragged over */
225
+ isDragging: boolean;
226
+ /** Whether drag is over the zone */
227
+ isOver: boolean;
228
+ /** Upload progress (0-100) */
229
+ progress: number;
230
+ /** Current flow status */
231
+ status: FlowUploadStatus;
232
+ /** Props to spread on the drop zone container */
233
+ getRootProps: () => UseDragDropReturn["dragHandlers"];
234
+ /** Props to spread on the hidden file input */
235
+ getInputProps: () => UseDragDropReturn["inputProps"];
236
+ /** Open file picker programmatically */
237
+ openFilePicker: () => void;
238
+ /** Current drag-drop state */
239
+ dragDropState: DragDropState;
240
+ }
241
+
242
+ /**
243
+ * Props for Flow.DropZone component.
244
+ */
245
+ export interface FlowDropZoneProps {
246
+ /** Accepted file types (e.g., "image/*", ".pdf") */
247
+ accept?: string;
248
+ /** Maximum file size in bytes */
249
+ maxFileSize?: number;
250
+ /** Render function receiving drop zone state */
251
+ children: (props: FlowDropZoneRenderProps) => ReactNode;
252
+ }
253
+
254
+ /**
255
+ * Drop zone for single-file uploads within a Flow.
256
+ * Automatically calls flow.upload() when a file is dropped.
257
+ *
258
+ * @example
259
+ * ```tsx
260
+ * <Flow.DropZone accept="image/*">
261
+ * {({ isDragging, progress, getRootProps, getInputProps }) => (
262
+ * <div {...getRootProps()}>
263
+ * <input {...getInputProps()} />
264
+ * {isDragging ? "Drop here" : `Progress: ${progress}%`}
265
+ * </div>
266
+ * )}
267
+ * </Flow.DropZone>
268
+ * ```
269
+ */
270
+ function FlowDropZone({ accept, maxFileSize, children }: FlowDropZoneProps) {
271
+ const flow = useFlowContext();
272
+
273
+ const dragDrop = useDragDrop({
274
+ onFilesReceived: (files) => {
275
+ const file = files[0];
276
+ if (file) {
277
+ flow.upload(file);
278
+ }
279
+ },
280
+ accept: accept ? accept.split(",").map((t) => t.trim()) : undefined,
281
+ maxFileSize,
282
+ multiple: false,
283
+ });
284
+
285
+ const renderProps: FlowDropZoneRenderProps = {
286
+ isDragging: dragDrop.state.isDragging,
287
+ isOver: dragDrop.state.isOver,
288
+ progress: flow.state.progress,
289
+ status: flow.state.status,
290
+ getRootProps: () => dragDrop.dragHandlers,
291
+ getInputProps: () => dragDrop.inputProps,
292
+ openFilePicker: dragDrop.openFilePicker,
293
+ dragDropState: dragDrop.state,
294
+ };
295
+
296
+ return <>{children(renderProps)}</>;
297
+ }
298
+
299
+ // ============ INPUTS DISCOVERY PRIMITIVE ============
300
+
301
+ /**
302
+ * Render props for Flow.Inputs component.
303
+ */
304
+ export interface FlowInputsRenderProps {
305
+ /** Discovered input metadata */
306
+ inputs: FlowInputMetadata[];
307
+ /** Whether inputs are still being discovered */
308
+ isLoading: boolean;
309
+ }
310
+
311
+ /**
312
+ * Props for Flow.Inputs component.
313
+ */
314
+ export interface FlowInputsProps {
315
+ /** Render function receiving discovered inputs */
316
+ children: (props: FlowInputsRenderProps) => ReactNode;
317
+ }
318
+
319
+ /**
320
+ * Auto-discovers flow input nodes and provides them via render props.
321
+ *
322
+ * @example
323
+ * ```tsx
324
+ * <Flow.Inputs>
325
+ * {({ inputs, isLoading }) => (
326
+ * isLoading ? <Spinner /> : (
327
+ * inputs.map(input => (
328
+ * <Flow.Input key={input.nodeId} nodeId={input.nodeId}>
329
+ * ...
330
+ * </Flow.Input>
331
+ * ))
332
+ * )
333
+ * )}
334
+ * </Flow.Inputs>
335
+ * ```
336
+ */
337
+ function FlowInputs({ children }: FlowInputsProps) {
338
+ const flow = useFlowContext();
339
+
340
+ const renderProps: FlowInputsRenderProps = {
341
+ inputs: flow.inputMetadata ?? [],
342
+ isLoading: flow.isDiscoveringInputs,
343
+ };
344
+
345
+ return <>{children(renderProps)}</>;
346
+ }
347
+
348
+ // ============ INPUT PRIMITIVE ============
349
+
350
+ /**
351
+ * Props for Flow.Input component.
352
+ */
353
+ export interface FlowInputProps {
354
+ /** Input node ID */
355
+ nodeId: string;
356
+ /** Children (can be render function or regular children) */
357
+ children: ReactNode | ((props: FlowInputContextValue) => ReactNode);
358
+ }
359
+
360
+ /**
361
+ * Scoped input context provider for a specific input node.
362
+ * Children can access input-specific state via useFlowInputContext().
363
+ *
364
+ * @example
365
+ * ```tsx
366
+ * <Flow.Input nodeId="video-input">
367
+ * {({ metadata, value, setValue }) => (
368
+ * <div>
369
+ * <label>{metadata.nodeName}</label>
370
+ * <Flow.Input.DropZone>...</Flow.Input.DropZone>
371
+ * </div>
372
+ * )}
373
+ * </Flow.Input>
374
+ * ```
375
+ */
376
+ function FlowInput({ nodeId, children }: FlowInputProps) {
377
+ const flow = useFlowContext();
378
+
379
+ const metadata = flow.inputMetadata?.find((m) => m.nodeId === nodeId);
380
+
381
+ if (!metadata) {
382
+ // Input not yet discovered or doesn't exist
383
+ return null;
384
+ }
385
+
386
+ const contextValue: FlowInputContextValue = {
387
+ nodeId,
388
+ metadata,
389
+ value: flow.inputs[nodeId],
390
+ setValue: (value) => flow.setInput(nodeId, value),
391
+ state: flow.inputStates.get(nodeId),
392
+ };
393
+
394
+ return (
395
+ <FlowInputContext.Provider value={contextValue}>
396
+ {typeof children === "function" ? children(contextValue) : children}
397
+ </FlowInputContext.Provider>
398
+ );
399
+ }
400
+
401
+ // ============ INPUT DROP ZONE PRIMITIVE ============
402
+
403
+ /**
404
+ * Render props for Flow.Input.DropZone component.
405
+ */
406
+ export interface FlowInputDropZoneRenderProps {
407
+ /** Whether files are being dragged over */
408
+ isDragging: boolean;
409
+ /** Whether drag is over the zone */
410
+ isOver: boolean;
411
+ /** Current value for this input */
412
+ value: unknown;
413
+ /** Per-input progress (if available) */
414
+ progress: number;
415
+ /** Per-input status (if available) */
416
+ status: string;
417
+ /** Props to spread on the drop zone container */
418
+ getRootProps: () => UseDragDropReturn["dragHandlers"];
419
+ /** Props to spread on the hidden file input */
420
+ getInputProps: () => UseDragDropReturn["inputProps"];
421
+ /** Open file picker programmatically */
422
+ openFilePicker: () => void;
423
+ /** Current drag-drop state */
424
+ dragDropState: DragDropState;
425
+ }
426
+
427
+ /**
428
+ * Props for Flow.Input.DropZone component.
429
+ */
430
+ export interface FlowInputDropZoneProps {
431
+ /** Accepted file types (e.g., "image/*", ".pdf") */
432
+ accept?: string;
433
+ /** Maximum file size in bytes */
434
+ maxFileSize?: number;
435
+ /** Render function receiving drop zone state */
436
+ children: (props: FlowInputDropZoneRenderProps) => ReactNode;
437
+ }
438
+
439
+ /**
440
+ * Drop zone for a specific input within a Flow.Input.
441
+ * Sets the input value but does NOT trigger upload until Flow.Submit is clicked.
442
+ */
443
+ function FlowInputDropZone({
444
+ accept,
445
+ maxFileSize,
446
+ children,
447
+ }: FlowInputDropZoneProps) {
448
+ const input = useFlowInputContext();
449
+
450
+ const dragDrop = useDragDrop({
451
+ onFilesReceived: (files) => {
452
+ const file = files[0];
453
+ if (file) {
454
+ input.setValue(file);
455
+ }
456
+ },
457
+ accept: accept ? accept.split(",").map((t) => t.trim()) : undefined,
458
+ maxFileSize,
459
+ multiple: false,
460
+ });
461
+
462
+ const renderProps: FlowInputDropZoneRenderProps = {
463
+ isDragging: dragDrop.state.isDragging,
464
+ isOver: dragDrop.state.isOver,
465
+ value: input.value,
466
+ progress: input.state?.progress ?? 0,
467
+ status: input.state?.status ?? "idle",
468
+ getRootProps: () => dragDrop.dragHandlers,
469
+ getInputProps: () => dragDrop.inputProps,
470
+ openFilePicker: dragDrop.openFilePicker,
471
+ dragDropState: dragDrop.state,
472
+ };
473
+
474
+ return <>{children(renderProps)}</>;
475
+ }
476
+
477
+ // ============ INPUT URL FIELD PRIMITIVE ============
478
+
479
+ /**
480
+ * Props for Flow.Input.UrlField component.
481
+ */
482
+ export interface FlowInputUrlFieldProps
483
+ extends Omit<
484
+ React.InputHTMLAttributes<HTMLInputElement>,
485
+ "value" | "onChange" | "type"
486
+ > {
487
+ /** Placeholder text */
488
+ placeholder?: string;
489
+ }
490
+
491
+ /**
492
+ * URL input field for a specific input within a Flow.Input.
493
+ * Automatically binds to the input context value.
494
+ */
495
+ function FlowInputUrlField({
496
+ placeholder = "https://example.com/file",
497
+ ...props
498
+ }: FlowInputUrlFieldProps) {
499
+ const input = useFlowInputContext();
500
+ const isUrl = typeof input.value === "string";
501
+
502
+ return (
503
+ <input
504
+ type="url"
505
+ value={isUrl ? (input.value as string) : ""}
506
+ onChange={(e) => input.setValue(e.target.value)}
507
+ placeholder={placeholder}
508
+ {...props}
509
+ />
510
+ );
511
+ }
512
+
513
+ // ============ INPUT PREVIEW PRIMITIVE ============
514
+
515
+ /**
516
+ * Render props for Flow.Input.Preview component.
517
+ */
518
+ export interface FlowInputPreviewRenderProps {
519
+ /** Current value */
520
+ value: unknown;
521
+ /** Whether value is a File */
522
+ isFile: boolean;
523
+ /** Whether value is a URL string */
524
+ isUrl: boolean;
525
+ /** File name (if value is File) */
526
+ fileName: string | null;
527
+ /** File size in bytes (if value is File) */
528
+ fileSize: number | null;
529
+ /** Clear the input value */
530
+ clear: () => void;
531
+ }
532
+
533
+ /**
534
+ * Props for Flow.Input.Preview component.
535
+ */
536
+ export interface FlowInputPreviewProps {
537
+ /** Render function receiving preview state */
538
+ children: (props: FlowInputPreviewRenderProps) => ReactNode;
539
+ }
540
+
541
+ /**
542
+ * Preview component for showing the selected value within a Flow.Input.
543
+ */
544
+ function FlowInputPreview({ children }: FlowInputPreviewProps) {
545
+ const input = useFlowInputContext();
546
+
547
+ const isFile = input.value instanceof File;
548
+ const isUrl = typeof input.value === "string" && input.value.length > 0;
549
+
550
+ const renderProps: FlowInputPreviewRenderProps = {
551
+ value: input.value,
552
+ isFile,
553
+ isUrl,
554
+ fileName: isFile ? (input.value as File).name : null,
555
+ fileSize: isFile ? (input.value as File).size : null,
556
+ clear: () => input.setValue(undefined),
557
+ };
558
+
559
+ return <>{children(renderProps)}</>;
560
+ }
561
+
562
+ // ============ PROGRESS PRIMITIVE ============
563
+
564
+ /**
565
+ * Render props for Flow.Progress component.
566
+ */
567
+ export interface FlowProgressRenderProps {
568
+ /** Progress percentage (0-100) */
569
+ progress: number;
570
+ /** Bytes uploaded so far */
571
+ bytesUploaded: number;
572
+ /** Total bytes to upload (null if unknown) */
573
+ totalBytes: number | null;
574
+ /** Current status */
575
+ status: FlowUploadStatus;
576
+ }
577
+
578
+ /**
579
+ * Props for Flow.Progress component.
580
+ */
581
+ export interface FlowProgressProps {
582
+ /** Render function receiving progress state */
583
+ children: (props: FlowProgressRenderProps) => ReactNode;
584
+ }
585
+
586
+ /**
587
+ * Progress display component within a Flow.
588
+ */
589
+ function FlowProgress({ children }: FlowProgressProps) {
590
+ const flow = useFlowContext();
591
+
592
+ const renderProps: FlowProgressRenderProps = {
593
+ progress: flow.state.progress,
594
+ bytesUploaded: flow.state.bytesUploaded,
595
+ totalBytes: flow.state.totalBytes,
596
+ status: flow.state.status,
597
+ };
598
+
599
+ return <>{children(renderProps)}</>;
600
+ }
601
+
602
+ // ============ STATUS PRIMITIVE ============
603
+
604
+ /**
605
+ * Render props for Flow.Status component.
606
+ */
607
+ export interface FlowStatusRenderProps {
608
+ /** Current status */
609
+ status: FlowUploadStatus;
610
+ /** Current node being processed (if any) */
611
+ currentNodeName: string | null;
612
+ /** Current node type (if any) */
613
+ currentNodeType: string | null;
614
+ /** Error (if status is error) */
615
+ error: Error | null;
616
+ /** Job ID (if started) */
617
+ jobId: string | null;
618
+ /** Whether flow has started */
619
+ flowStarted: boolean;
620
+ /** Flow outputs (if completed) */
621
+ flowOutputs: TypedOutput[] | null;
622
+ }
623
+
624
+ /**
625
+ * Props for Flow.Status component.
626
+ */
627
+ export interface FlowStatusProps {
628
+ /** Render function receiving status state */
629
+ children: (props: FlowStatusRenderProps) => ReactNode;
630
+ }
631
+
632
+ /**
633
+ * Status display component within a Flow.
634
+ */
635
+ function FlowStatus({ children }: FlowStatusProps) {
636
+ const flow = useFlowContext();
637
+
638
+ const renderProps: FlowStatusRenderProps = {
639
+ status: flow.state.status,
640
+ currentNodeName: flow.state.currentNodeName,
641
+ currentNodeType: flow.state.currentNodeType,
642
+ error: flow.state.error,
643
+ jobId: flow.state.jobId,
644
+ flowStarted: flow.state.flowStarted,
645
+ flowOutputs: flow.state.flowOutputs,
646
+ };
647
+
648
+ return <>{children(renderProps)}</>;
649
+ }
650
+
651
+ // ============ ERROR PRIMITIVE ============
652
+
653
+ /**
654
+ * Render props for Flow.Error component.
655
+ */
656
+ export interface FlowErrorRenderProps {
657
+ /** Error object (null if no error) */
658
+ error: Error | null;
659
+ /** Whether there is an error */
660
+ hasError: boolean;
661
+ /** Error message */
662
+ message: string | null;
663
+ /** Reset the flow */
664
+ reset: () => void;
665
+ }
666
+
667
+ /**
668
+ * Props for Flow.Error component.
669
+ */
670
+ export interface FlowErrorProps {
671
+ /** Render function receiving error state */
672
+ children: (props: FlowErrorRenderProps) => ReactNode;
673
+ }
674
+
675
+ /**
676
+ * Error display component within a Flow.
677
+ */
678
+ function FlowError({ children }: FlowErrorProps) {
679
+ const flow = useFlowContext();
680
+
681
+ const renderProps: FlowErrorRenderProps = {
682
+ error: flow.state.error,
683
+ hasError: flow.state.status === "error",
684
+ message: flow.state.error?.message ?? null,
685
+ reset: flow.reset,
686
+ };
687
+
688
+ return <>{children(renderProps)}</>;
689
+ }
690
+
691
+ // ============ ACTION PRIMITIVES ============
692
+
693
+ /**
694
+ * Props for Flow.Submit component.
695
+ */
696
+ export interface FlowSubmitProps
697
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
698
+ /** Button content */
699
+ children: ReactNode;
700
+ }
701
+
702
+ /**
703
+ * Submit button that executes the flow with current inputs.
704
+ * Automatically disabled when uploading.
705
+ */
706
+ function FlowSubmit({ children, disabled, ...props }: FlowSubmitProps) {
707
+ const flow = useFlowContext();
708
+
709
+ const handleClick = useCallback(() => {
710
+ flow.execute();
711
+ }, [flow]);
712
+
713
+ return (
714
+ <button
715
+ type="button"
716
+ onClick={handleClick}
717
+ disabled={disabled || flow.isUploading}
718
+ {...props}
719
+ >
720
+ {children}
721
+ </button>
722
+ );
723
+ }
724
+
725
+ /**
726
+ * Props for Flow.Cancel component.
727
+ */
728
+ export interface FlowCancelProps
729
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
730
+ /** Button content */
731
+ children: ReactNode;
732
+ }
733
+
734
+ /**
735
+ * Cancel button that aborts the current upload.
736
+ */
737
+ function FlowCancel({ children, ...props }: FlowCancelProps) {
738
+ const flow = useFlowContext();
739
+
740
+ const handleClick = useCallback(() => {
741
+ flow.abort();
742
+ }, [flow]);
743
+
744
+ return (
745
+ <button type="button" onClick={handleClick} {...props}>
746
+ {children}
747
+ </button>
748
+ );
749
+ }
750
+
751
+ /**
752
+ * Props for Flow.Reset component.
753
+ */
754
+ export interface FlowResetProps
755
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onClick"> {
756
+ /** Button content */
757
+ children: ReactNode;
758
+ }
759
+
760
+ /**
761
+ * Reset button that clears all inputs and resets to idle state.
762
+ */
763
+ function FlowReset({ children, ...props }: FlowResetProps) {
764
+ const flow = useFlowContext();
765
+
766
+ const handleClick = useCallback(() => {
767
+ flow.reset();
768
+ }, [flow]);
769
+
770
+ return (
771
+ <button type="button" onClick={handleClick} {...props}>
772
+ {children}
773
+ </button>
774
+ );
775
+ }
776
+
777
+ // ============ COMPOUND COMPONENT EXPORT ============
778
+
779
+ /**
780
+ * Flow compound component for flow-based file uploads.
781
+ *
782
+ * Provides a composable, headless API for building flow upload interfaces.
783
+ * All sub-components use render props for complete UI control.
784
+ *
785
+ * @example Simple Drop Zone
786
+ * ```tsx
787
+ * <Flow flowId="image-optimizer" storageId="s3" onSuccess={handleSuccess}>
788
+ * <Flow.DropZone accept="image/*">
789
+ * {({ isDragging, progress, getRootProps, getInputProps }) => (
790
+ * <div {...getRootProps()}>
791
+ * <input {...getInputProps()} />
792
+ * {isDragging ? "Drop here" : "Drag or click"}
793
+ * {progress > 0 && <progress value={progress} max={100} />}
794
+ * </div>
795
+ * )}
796
+ * </Flow.DropZone>
797
+ * </Flow>
798
+ * ```
799
+ *
800
+ * @example Multi-Input Flow
801
+ * ```tsx
802
+ * <Flow flowId="video-processor" storageId="s3">
803
+ * <Flow.Inputs>
804
+ * {({ inputs }) => inputs.map(input => (
805
+ * <Flow.Input key={input.nodeId} nodeId={input.nodeId}>
806
+ * {({ metadata }) => (
807
+ * <div>
808
+ * <label>{metadata.nodeName}</label>
809
+ * <Flow.Input.DropZone accept="video/*">
810
+ * {({ getRootProps, getInputProps }) => (
811
+ * <div {...getRootProps()}>
812
+ * <input {...getInputProps()} />
813
+ * </div>
814
+ * )}
815
+ * </Flow.Input.DropZone>
816
+ * </div>
817
+ * )}
818
+ * </Flow.Input>
819
+ * ))}
820
+ * </Flow.Inputs>
821
+ * <Flow.Submit>Process</Flow.Submit>
822
+ * </Flow>
823
+ * ```
824
+ */
825
+ export const Flow = Object.assign(FlowRoot, {
826
+ DropZone: FlowDropZone,
827
+ Inputs: FlowInputs,
828
+ Input: Object.assign(FlowInput, {
829
+ DropZone: FlowInputDropZone,
830
+ UrlField: FlowInputUrlField,
831
+ Preview: FlowInputPreview,
832
+ }),
833
+ Progress: FlowProgress,
834
+ Status: FlowStatus,
835
+ Error: FlowError,
836
+ Submit: FlowSubmit,
837
+ Cancel: FlowCancel,
838
+ Reset: FlowReset,
839
+ });