@myriadcodelabs/uiflow 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +190 -0
- package/dist/flow.d.ts +89 -0
- package/dist/flow.d.ts.map +1 -0
- package/dist/flow.js +149 -0
- package/dist/flow.js.map +1 -0
- package/dist/uiflow.d.ts +2 -0
- package/dist/uiflow.d.ts.map +1 -0
- package/dist/uiflow.js +2 -0
- package/dist/uiflow.js.map +1 -0
- package/package.json +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MyriadCode
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# UIFlow
|
|
2
|
+
|
|
3
|
+
Explicit, code-first UI flow orchestration for React. UIFlow lets you define flows as plain objects with named steps, mix UI and logic steps, and move between them by returning the next step name. It’s useful when you want predictable, testable multi‑step experiences without wiring up routers, wizards, or state machines by hand.
|
|
4
|
+
|
|
5
|
+
## Why it matters
|
|
6
|
+
|
|
7
|
+
- **Clarity:** Flows are defined in one place with explicit step names and transitions.
|
|
8
|
+
- **Flexibility:** Combine UI steps and async logic steps in the same flow.
|
|
9
|
+
- **Reusability:** Share cross‑flow state through event channels.
|
|
10
|
+
|
|
11
|
+
## Quick example
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import React from "react";
|
|
15
|
+
import { FlowRunner, defineFlow, createFlowChannel } from "@myriadcodelabs/uiflow";
|
|
16
|
+
|
|
17
|
+
type StudyData = {
|
|
18
|
+
deckId: string;
|
|
19
|
+
cards: CardWithState[];
|
|
20
|
+
activeCardId: string | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type CardWithState = {
|
|
24
|
+
id: string;
|
|
25
|
+
question: string;
|
|
26
|
+
answer: string;
|
|
27
|
+
flipped: boolean;
|
|
28
|
+
rating: "easy" | "medium" | "hard" | null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ShowCardOutput =
|
|
32
|
+
| { action: "flip"; cardId: string }
|
|
33
|
+
| { action: "rate"; rating: Rating; cardId: string }
|
|
34
|
+
| { action: "next"; cardId: string };
|
|
35
|
+
|
|
36
|
+
const studiedCounter = createFlowChannel<number>(0);
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
const studyFlow = defineFlow<StudyData>(
|
|
40
|
+
{
|
|
41
|
+
// step 1
|
|
42
|
+
fetchCards: {
|
|
43
|
+
input: (data) => ({ deckId: data.deckId }),
|
|
44
|
+
action: async ({ deckId }, data) => {
|
|
45
|
+
const cards = await fakeFetchCards(deckId);
|
|
46
|
+
data.cards = cards.map((card) => ({ ...card, flipped: false, rating: null }));
|
|
47
|
+
data.activeCardId = null;
|
|
48
|
+
return { ok: true };
|
|
49
|
+
},
|
|
50
|
+
onOutput: () => "decide",
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// step 2
|
|
54
|
+
decide: {
|
|
55
|
+
input: (data) => ({ hasCards: data.cards.length > 0 }),
|
|
56
|
+
action: ({ hasCards }) => hasCards,
|
|
57
|
+
onOutput: (_, exists) => (exists ? "study" : "noCard"),
|
|
58
|
+
},
|
|
59
|
+
// if no card present
|
|
60
|
+
noCard: {
|
|
61
|
+
input: () => ({}),
|
|
62
|
+
view: NoCardView,
|
|
63
|
+
onOutput: () => {},
|
|
64
|
+
},
|
|
65
|
+
// step 3
|
|
66
|
+
study: {
|
|
67
|
+
input: (data) => ({ cards: data.cards }),
|
|
68
|
+
view: CardView,
|
|
69
|
+
onOutput: (data, output, events) => {
|
|
70
|
+
const card = data.cards.find((c) => c.id === output.cardId);
|
|
71
|
+
if (!card) return "study";
|
|
72
|
+
|
|
73
|
+
if (output.action === "flip") {
|
|
74
|
+
data.activeCardId = card.id;
|
|
75
|
+
card.flipped = true;
|
|
76
|
+
return "study";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (output.action === "rate") {
|
|
80
|
+
data.activeCardId = card.id;
|
|
81
|
+
card.rating = output.rating ?? null;
|
|
82
|
+
return "review";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (output.action === "next") {
|
|
86
|
+
events?.studiedCounter.emit((c) => c + 1);
|
|
87
|
+
data.activeCardId = null;
|
|
88
|
+
return "fetchCards";
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// step 4: if user does review
|
|
94
|
+
review: {
|
|
95
|
+
input: (data) => ({
|
|
96
|
+
deckId: data.deckId,
|
|
97
|
+
cardId: data.activeCardId!,
|
|
98
|
+
rating: data.cards.find((c) => c.id === data.activeCardId)?.rating!,
|
|
99
|
+
}),
|
|
100
|
+
action: async ({ deckId, cardId, rating }) => {
|
|
101
|
+
await fakeReviewCard(deckId, cardId, rating);
|
|
102
|
+
return { ok: true };
|
|
103
|
+
},
|
|
104
|
+
onOutput: (data, _, events) => {
|
|
105
|
+
events?.studiedCounter.emit((c) => c + 1);
|
|
106
|
+
data.activeCardId = null;
|
|
107
|
+
return "fetchCards";
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{ start: "fetchCards" }
|
|
112
|
+
);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The example components are defined here.
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
const CardView: React.FC<{
|
|
119
|
+
input: { cards: CardWithState[] };
|
|
120
|
+
output: OutputHandle<ShowCardOutput>;
|
|
121
|
+
}> = ({ input, output }) => (
|
|
122
|
+
<div>
|
|
123
|
+
{input.cards.map((card) => (
|
|
124
|
+
<div key={card.id}>
|
|
125
|
+
<div>{card.question}</div>
|
|
126
|
+
{card.flipped ? <div>{card.answer}</div> : null}
|
|
127
|
+
<button onClick={() => output.emit({ cardId: card.id, action: "flip" })}>Show Answer</button>
|
|
128
|
+
<button onClick={() => output.emit({ cardId: card.id, action: "rate", rating: "easy" })}>Easy</button>
|
|
129
|
+
<button onClick={() => output.emit({ cardId: card.id, action: "rate", rating: "medium" })}>Medium</button>
|
|
130
|
+
<button onClick={() => output.emit({ cardId: card.id, action: "rate", rating: "hard" })}>Hard</button>
|
|
131
|
+
<button onClick={() => output.emit({ cardId: card.id, action: "next" })}>Next</button>
|
|
132
|
+
</div>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const NoCardView: React.FC<{ input: {}; output: { emit: () => void } }> = () => (
|
|
138
|
+
<div>No cards available.</div>
|
|
139
|
+
);
|
|
140
|
+
```
|
|
141
|
+
The FlowRunner is used to call the flow and set initial data and channels.
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
export function App() {
|
|
145
|
+
return (
|
|
146
|
+
<FlowRunner
|
|
147
|
+
flow={studyFlow}
|
|
148
|
+
initialData={{ deckId: "deck-1", cards: [], activeCardId: null }}
|
|
149
|
+
eventChannels={{ studiedCounter }}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## API Reference (exported only)
|
|
156
|
+
|
|
157
|
+
### 1) Where the flow is called
|
|
158
|
+
|
|
159
|
+
#### `FlowRunner` (React component)
|
|
160
|
+
Runs a flow and renders UI steps.
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
<FlowRunner flow={flow} initialData={initialData} eventChannels={channels} />
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- `flow: FlowDefinition<D>` — created by `defineFlow`.
|
|
167
|
+
- `initialData: D` — shared mutable data for this flow instance.
|
|
168
|
+
- `eventChannels?: EventChannels` — optional shared channels; emitting causes re-render.
|
|
169
|
+
|
|
170
|
+
### 2) Where the flow is defined
|
|
171
|
+
|
|
172
|
+
#### `defineFlow<D>(steps: FlowSteps<D>, options: DefineFlowOptions): FlowDefinition<D>`
|
|
173
|
+
Creates a flow definition from a steps map and a required `start` step name.
|
|
174
|
+
|
|
175
|
+
- `DefineFlowOptions.start: string` — name of the first step.
|
|
176
|
+
|
|
177
|
+
#### `createFlowChannel<T>(initial: T): FlowChannel<T>`
|
|
178
|
+
Creates a shared channel for cross‑flow communication.
|
|
179
|
+
|
|
180
|
+
- `FlowChannel.get(): T` — read the current value.
|
|
181
|
+
- `FlowChannel.emit(update: T | (prev: T) => T): void` — update value and notify subscribers.
|
|
182
|
+
- `FlowChannel.subscribe(listener: () => void): () => void` — listen for changes.
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
### 3) A UI component
|
|
186
|
+
|
|
187
|
+
#### `OutputHandle<O>`
|
|
188
|
+
Used by UI steps to emit output back into the flow.
|
|
189
|
+
|
|
190
|
+
- `OutputHandle.emit(output: O): void`
|
package/dist/flow.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export type Updater<T> = T | ((prev: T) => T);
|
|
3
|
+
export interface FlowChannel<T> {
|
|
4
|
+
get: () => T;
|
|
5
|
+
emit: (update: Updater<T>) => void;
|
|
6
|
+
subscribe: (listener: () => void) => () => void;
|
|
7
|
+
}
|
|
8
|
+
export type EventChannels = Record<string, FlowChannel<any>>;
|
|
9
|
+
export declare function createFlowChannel<T>(initial: T): FlowChannel<T>;
|
|
10
|
+
/**
|
|
11
|
+
* Shared mutable data for a flow instance.
|
|
12
|
+
* You can refine this to a generic later, e.g. <D>.
|
|
13
|
+
*/
|
|
14
|
+
export type FlowData = Record<string, any>;
|
|
15
|
+
/**
|
|
16
|
+
* Output handle given to UI components.
|
|
17
|
+
* They call output.done(...) when they're finished.
|
|
18
|
+
*/
|
|
19
|
+
export interface OutputHandle<O = any> {
|
|
20
|
+
emit: (output: O) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* UI step:
|
|
24
|
+
* - Prepares `input` from `data`
|
|
25
|
+
* - Renders `view`
|
|
26
|
+
* - Receives `output` from the component via output.done()
|
|
27
|
+
* - `onOutput` decides next step and can mutate data
|
|
28
|
+
*/
|
|
29
|
+
export interface UiStep<D extends FlowData = FlowData, I = any, O = any> {
|
|
30
|
+
input: (data: D, events?: EventChannels) => I;
|
|
31
|
+
view: React.ComponentType<{
|
|
32
|
+
input: I;
|
|
33
|
+
output: OutputHandle<O>;
|
|
34
|
+
}>;
|
|
35
|
+
onOutput: (data: D, output: O, events?: EventChannels) => string | void | Promise<string | void>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Action (logic) step:
|
|
39
|
+
* - Prepares `args` from `data`
|
|
40
|
+
* - Executes `action` (sync/async)
|
|
41
|
+
* - `onOutput` decides next step and can mutate data
|
|
42
|
+
* - No UI
|
|
43
|
+
*/
|
|
44
|
+
export interface ActionStep<D extends FlowData = FlowData, I = any, O = any> {
|
|
45
|
+
input: (data: D, events?: EventChannels) => I;
|
|
46
|
+
action: (input: I, data: D, events?: EventChannels) => O | Promise<O>;
|
|
47
|
+
onOutput: (data: D, output: O, events?: EventChannels) => string | void | Promise<string | void>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A flow step is either:
|
|
51
|
+
* - a UI step (has `view`)
|
|
52
|
+
* - an action step (has `action`)
|
|
53
|
+
* but never both at the same time by convention.
|
|
54
|
+
*/
|
|
55
|
+
export type FlowStep<D extends FlowData = FlowData> = UiStep<D, any, any> | ActionStep<D, any, any>;
|
|
56
|
+
/**
|
|
57
|
+
* Map of step names -> step definitions.
|
|
58
|
+
*/
|
|
59
|
+
export type FlowSteps<D extends FlowData = FlowData> = Record<string, FlowStep<D>>;
|
|
60
|
+
/**
|
|
61
|
+
* Flow definition object returned by defineFlow.
|
|
62
|
+
*/
|
|
63
|
+
export interface FlowDefinition<D extends FlowData = FlowData> {
|
|
64
|
+
steps: FlowSteps<D>;
|
|
65
|
+
start: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Options when defining a flow.
|
|
69
|
+
*/
|
|
70
|
+
export interface DefineFlowOptions {
|
|
71
|
+
start: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Main entry point to define a flow.
|
|
75
|
+
*/
|
|
76
|
+
export declare function defineFlow<D extends FlowData = FlowData>(steps: FlowSteps<D>, options: DefineFlowOptions): FlowDefinition<D>;
|
|
77
|
+
export interface FlowRunnerProps<D extends FlowData = FlowData> {
|
|
78
|
+
flow: FlowDefinition<D>;
|
|
79
|
+
initialData: D;
|
|
80
|
+
eventChannels?: EventChannels;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* FlowRunner:
|
|
84
|
+
* - drives the current step (UI or action)
|
|
85
|
+
* - manages transitions
|
|
86
|
+
* - renders UI steps
|
|
87
|
+
*/
|
|
88
|
+
export declare function FlowRunner<D extends FlowData = FlowData>(props: Readonly<FlowRunnerProps<D>>): import("react/jsx-runtime").JSX.Element;
|
|
89
|
+
//# sourceMappingURL=flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow.d.ts","sourceRoot":"","sources":["../src/flow.tsx"],"names":[],"mappings":"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"}
|
package/dist/flow.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
/* eslint-disable react/no-unescaped-entities */
|
|
4
|
+
// src/flow.tsx
|
|
5
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
6
|
+
// Factory function to create a channel.
|
|
7
|
+
// IMPORTANT: This lives OUTSIDE React, so multiple FlowRunners can share it.
|
|
8
|
+
export function createFlowChannel(initial) {
|
|
9
|
+
let value = initial;
|
|
10
|
+
const listeners = new Set();
|
|
11
|
+
return {
|
|
12
|
+
get: () => value,
|
|
13
|
+
emit: (update) => {
|
|
14
|
+
value = typeof update === "function" ? update(value) : update;
|
|
15
|
+
listeners.forEach((l) => l()); // notify all subscribers
|
|
16
|
+
},
|
|
17
|
+
subscribe: (listener) => {
|
|
18
|
+
listeners.add(listener);
|
|
19
|
+
return () => listeners.delete(listener);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Main entry point to define a flow.
|
|
25
|
+
*/
|
|
26
|
+
export function defineFlow(steps, options) {
|
|
27
|
+
if (!options.start || !steps[options.start]) {
|
|
28
|
+
throw new Error(`defineFlow: 'start' must be provided and exist in steps. Got '${options.start}'.`);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
steps,
|
|
32
|
+
start: options.start,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* FlowRunner:
|
|
37
|
+
* - drives the current step (UI or action)
|
|
38
|
+
* - manages transitions
|
|
39
|
+
* - renders UI steps
|
|
40
|
+
*/
|
|
41
|
+
export function FlowRunner(props) {
|
|
42
|
+
const eventChannelsRef = useRef(undefined);
|
|
43
|
+
eventChannelsRef.current ?? (eventChannelsRef.current = props.eventChannels);
|
|
44
|
+
const eventChannels = eventChannelsRef.current;
|
|
45
|
+
const { flow, initialData } = props;
|
|
46
|
+
// We keep data and currentStep in state so React re-renders on change.
|
|
47
|
+
const [state, setState] = useState({
|
|
48
|
+
currentStep: flow.start,
|
|
49
|
+
data: { ...initialData },
|
|
50
|
+
});
|
|
51
|
+
const [busy, setBusy] = useState(false); // for action steps
|
|
52
|
+
const isMountedRef = useRef(true);
|
|
53
|
+
// NEW:
|
|
54
|
+
// This state is never used directly.
|
|
55
|
+
// It only exists to force a re-render when event channels change.
|
|
56
|
+
const [_tick, setTick] = useState(0);
|
|
57
|
+
// NEW:
|
|
58
|
+
// Subscribe to every provided channel.
|
|
59
|
+
// When any channel emits, we trigger a re-render of this FlowRunner.
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!eventChannels)
|
|
62
|
+
return;
|
|
63
|
+
const unsubs = Object.values(eventChannels).map((ch) => ch.subscribe(() => setTick((x) => x + 1)));
|
|
64
|
+
return () => unsubs.forEach((u) => u());
|
|
65
|
+
}, []);
|
|
66
|
+
const { currentStep, data } = state;
|
|
67
|
+
const applyTransition = (nextStepName) => {
|
|
68
|
+
if (!isMountedRef.current)
|
|
69
|
+
return;
|
|
70
|
+
if (nextStepName && flow.steps[nextStepName]) {
|
|
71
|
+
setState((prev) => ({
|
|
72
|
+
...prev,
|
|
73
|
+
currentStep: nextStepName,
|
|
74
|
+
data: { ...prev.data }, // new reference to trigger React updates (memo-safe)
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// no next step: just re-render with updated data if any
|
|
79
|
+
setState((prev) => ({
|
|
80
|
+
...prev,
|
|
81
|
+
data: { ...prev.data },
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
return () => {
|
|
87
|
+
isMountedRef.current = false;
|
|
88
|
+
};
|
|
89
|
+
}, []);
|
|
90
|
+
const step = flow.steps[currentStep];
|
|
91
|
+
// If the step is an action (no view), run it in an effect.
|
|
92
|
+
const isActionStep = step.action && !step.view;
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!isActionStep)
|
|
95
|
+
return;
|
|
96
|
+
const actionStep = step;
|
|
97
|
+
(async () => {
|
|
98
|
+
try {
|
|
99
|
+
setBusy(true);
|
|
100
|
+
const input = actionStep.input(state.data, eventChannels);
|
|
101
|
+
const output = await actionStep.action(input, state.data, eventChannels);
|
|
102
|
+
const next = await actionStep.onOutput(state.data, output, eventChannels);
|
|
103
|
+
applyTransition(next);
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
console.error("FlowRunner action step error:", e);
|
|
107
|
+
// In a real lib, route to a dedicated error step or surface error up
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
if (isMountedRef.current)
|
|
111
|
+
setBusy(false);
|
|
112
|
+
}
|
|
113
|
+
})();
|
|
114
|
+
// We only want to run this when step changes, not on arbitrary data changes.
|
|
115
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
116
|
+
}, [currentStep]);
|
|
117
|
+
if (!step) {
|
|
118
|
+
// Fails fast if flow is misconfigured.
|
|
119
|
+
return (_jsxs("div", { children: [_jsx("strong", { children: "FlowRunner error:" }), " Unknown step \"", currentStep, "\"."] }));
|
|
120
|
+
}
|
|
121
|
+
// Helper to apply a next step (if returned) and ensure React re-renders
|
|
122
|
+
// -----------------------
|
|
123
|
+
// ACTION STEP HANDLING
|
|
124
|
+
// -----------------------
|
|
125
|
+
// If it's an action step, show a simple placeholder or nothing.
|
|
126
|
+
if (isActionStep) {
|
|
127
|
+
// You can customize this: spinner, skeleton, etc.
|
|
128
|
+
return _jsx("div", { children: busy ? "Processing..." : null });
|
|
129
|
+
}
|
|
130
|
+
// -----------------------
|
|
131
|
+
// UI STEP HANDLING
|
|
132
|
+
// -----------------------
|
|
133
|
+
const uiStep = step;
|
|
134
|
+
const ViewComponent = uiStep.view;
|
|
135
|
+
const input = uiStep.input(data, eventChannels);
|
|
136
|
+
const outputHandle = {
|
|
137
|
+
emit: async (output) => {
|
|
138
|
+
try {
|
|
139
|
+
const next = await uiStep.onOutput(data, output, eventChannels);
|
|
140
|
+
applyTransition(next);
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
console.error("FlowRunner UI step onOutput error:", e);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
return _jsx(ViewComponent, { input: input, output: outputHandle });
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=flow.js.map
|
package/dist/flow.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow.js","sourceRoot":"","sources":["../src/flow.tsx"],"names":[],"mappings":"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"}
|
package/dist/uiflow.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uiflow.d.ts","sourceRoot":"","sources":["../src/uiflow.tsx"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC"}
|
package/dist/uiflow.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uiflow.js","sourceRoot":"","sources":["../src/uiflow.tsx"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@myriadcodelabs/uiflow",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Explicit, code-first UI flow orchestration for React.",
|
|
5
|
+
"keywords": [],
|
|
6
|
+
"author": "Muhammad Ismail Khan",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"peerDependencies": {
|
|
9
|
+
"react": ">=18"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/react": "^19.2.10",
|
|
13
|
+
"typescript": "^5.9.3"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"main": "./dist/uiflow.js",
|
|
20
|
+
"types": "./dist/uiflow.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/uiflow.d.ts",
|
|
24
|
+
"default": "./dist/uiflow.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
29
|
+
"clean": "rimraf dist",
|
|
30
|
+
"build": "pnpm run clean && tsc"
|
|
31
|
+
}
|
|
32
|
+
}
|