@jarenjs/studio 0.83.2 → 0.84.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 (90) hide show
  1. package/README.md +21 -6
  2. package/contracts/data.contract.json +243 -0
  3. package/dist/types/component/data/actions.d.ts +234 -0
  4. package/dist/types/component/data/index.d.ts +9 -0
  5. package/dist/types/component/data/mount.d.ts +87 -0
  6. package/dist/types/component/data/project-widget.d.ts +27 -0
  7. package/dist/types/component/data/viewmodel.d.ts +85 -0
  8. package/dist/types/component/data/views.d.ts +148 -0
  9. package/dist/types/component/document.d.ts +82 -0
  10. package/dist/types/component/flow/actions.d.ts +708 -0
  11. package/dist/types/component/flow/index.d.ts +7 -0
  12. package/dist/types/component/flow/mount.d.ts +66 -0
  13. package/dist/types/component/flow/project-widget.d.ts +8 -0
  14. package/dist/types/component/flow/runtime.d.ts +57 -0
  15. package/dist/types/component/flow/views.d.ts +242 -0
  16. package/dist/types/component/host.d.ts +2 -2
  17. package/dist/types/component/index.d.ts +21 -15
  18. package/dist/types/component/mount.d.ts +51 -0
  19. package/dist/types/component/project-actions.d.ts +302 -0
  20. package/dist/types/component/project-controller.d.ts +81 -0
  21. package/dist/types/component/project-state.d.ts +1 -0
  22. package/dist/types/component/project.d.ts +327 -0
  23. package/dist/types/component/shared/host-widget.d.ts +24 -0
  24. package/dist/types/component/shared/memo.d.ts +12 -0
  25. package/dist/types/component/shared/nodes.d.ts +92 -0
  26. package/dist/types/component/shared/schema-options.d.ts +28 -0
  27. package/dist/types/component/shared/studio-kit.d.ts +78 -0
  28. package/dist/types/component/shared/ui.d.ts +136 -0
  29. package/dist/types/component/view.d.ts +6 -6
  30. package/dist/types/data/boot-stages.d.ts +97 -0
  31. package/dist/types/data/browser-worker.d.ts +22 -0
  32. package/dist/types/data/contract.d.ts +9 -0
  33. package/dist/types/data/editor.d.ts +81 -0
  34. package/dist/types/data/handlers.d.ts +130 -0
  35. package/dist/types/data/host.d.ts +8 -0
  36. package/dist/types/data/project-worker.d.ts +8 -0
  37. package/dist/types/data/runtime.d.ts +46 -0
  38. package/dist/types/data/state.d.ts +33 -0
  39. package/dist/types/data/storage.d.ts +26 -0
  40. package/dist/types/data/transport.d.ts +69 -0
  41. package/dist/types/flow-document.d.ts +19 -0
  42. package/dist/types/flow-editor.d.ts +88 -0
  43. package/docs/EDITORS.md +158 -0
  44. package/docs/PROJECT-FORMAT.md +1 -11
  45. package/package.json +32 -15
  46. package/src/component/data/actions.js +138 -0
  47. package/src/component/data/index.js +10 -0
  48. package/src/component/data/mount.js +63 -0
  49. package/src/component/data/project-widget.js +164 -0
  50. package/src/component/data/viewmodel.js +185 -0
  51. package/src/component/data/views.js +241 -0
  52. package/src/component/document.js +344 -0
  53. package/src/component/flow/actions.js +331 -0
  54. package/src/component/flow/index.js +8 -0
  55. package/src/component/flow/mount.js +53 -0
  56. package/src/component/flow/project-widget.js +44 -0
  57. package/src/component/flow/runtime.js +481 -0
  58. package/src/component/flow/views.js +196 -0
  59. package/src/component/host.js +2 -2
  60. package/src/component/index.js +19 -9
  61. package/src/component/mount.js +43 -0
  62. package/src/component/project-actions.js +189 -0
  63. package/src/component/project-controller.js +248 -0
  64. package/src/component/project-state.js +30 -0
  65. package/src/component/project.js +308 -0
  66. package/src/component/shared/host-widget.js +35 -0
  67. package/src/component/shared/memo.js +28 -0
  68. package/src/component/shared/nodes.js +94 -0
  69. package/src/component/shared/schema-options.js +30 -0
  70. package/src/component/shared/studio-kit.js +59 -0
  71. package/src/component/shared/ui.js +134 -0
  72. package/src/data/boot-stages.js +202 -0
  73. package/src/data/browser-worker.js +247 -0
  74. package/src/data/contract.js +7 -0
  75. package/src/data/editor.js +95 -0
  76. package/src/data/handlers.js +349 -0
  77. package/src/data/host.js +8 -0
  78. package/src/data/project-worker.js +26 -0
  79. package/src/data/runtime.js +462 -0
  80. package/src/data/state.js +46 -0
  81. package/src/data/storage.js +61 -0
  82. package/src/data/transport.js +215 -0
  83. package/src/flow-document.js +24 -0
  84. package/src/flow-editor.js +98 -0
  85. package/styles/data.css +53 -0
  86. package/styles/editor.css +150 -0
  87. package/styles/flow.css +99 -0
  88. package/styles/studio.css +1 -0
  89. package/dist/types/author.d.ts +0 -27
  90. package/src/author.js +0 -55
@@ -0,0 +1,331 @@
1
+ // @ts-check
2
+ /** Shared by the standalone flow page and the isolated project editor. */
3
+ export const FLOW_ACTIONS = {
4
+ 'flow/template': { effects: [{ run: 'flow-template', with: { name: '$payload' } }] },
5
+
6
+ 'flow/load': {
7
+ patch: [
8
+ { op: 'replace', path: '/flow/kind', value: '$payload.kind' },
9
+ { op: 'replace', path: '/flow/doc', value: '$payload.doc' },
10
+ { op: 'replace', path: '/flow/selection', value: null },
11
+ { op: 'replace', path: '/flow/connect', value: null },
12
+ { op: 'replace', path: '/flow/tab', value: 'diagram' },
13
+ { op: 'replace', path: '/flow/parseError', value: null },
14
+ { op: 'replace', path: '/flow/history', value: { past: [], future: [] } },
15
+ { op: 'replace', path: '/flow/run', value: null },
16
+ { op: 'replace', path: '/flow/runContext', value: '$payload.runContext' },
17
+ { op: 'replace', path: '/flow/dagInput', value: '$payload.dagInput' },
18
+ ],
19
+ },
20
+
21
+ 'flow/clear': {
22
+ patch: [
23
+ { op: 'replace', path: '/flow/kind', value: null },
24
+ { op: 'replace', path: '/flow/doc', value: null },
25
+ { op: 'replace', path: '/flow/selection', value: null },
26
+ { op: 'replace', path: '/flow/connect', value: null },
27
+ { op: 'replace', path: '/flow/parseError', value: null },
28
+ { op: 'replace', path: '/flow/history', value: { past: [], future: [] } },
29
+ { op: 'replace', path: '/flow/run', value: null },
30
+ ],
31
+ },
32
+
33
+ 'flow/tab': { patch: [{ op: 'replace', path: '/flow/tab', value: '$payload' }] },
34
+ // the phone pane switcher (Diagram · Inspector · Run) — pure chrome;
35
+ // `tab` (Diagram ↔ Text) is a different axis and stays independent
36
+ 'flow/pane': { patch: [{ op: 'replace', path: '/flow/mobilePane', value: '$payload' }] },
37
+
38
+ // A diagram click: plain pick — unless a connect source is armed and
39
+ // a node was clicked, in which case this IS the connect commit.
40
+ 'flow/pick': {
41
+ $if: [
42
+ { $and: [
43
+ '$.flow.connect',
44
+ { $or: [{ $eq: ['$payload.type', 'state'] }, { $eq: ['$payload.type', 'node'] }] },
45
+ ] },
46
+ { $if: [
47
+ { $eq: ['$.flow.kind', 'fsm'] },
48
+ { patch: [
49
+ { op: 'add', path: '/flow/history/past/-', value: '$.flow.doc' },
50
+ { op: 'replace', path: '/flow/history/future', value: [] },
51
+ { op: 'add', path: '/flow/doc/transitions/-',
52
+ value: { from: '$.flow.connect', event: null, to: '$payload.id' } },
53
+ { op: 'replace', path: '/flow/connect', value: null },
54
+ { op: 'replace', path: '/flow/selection', value: {
55
+ type: 'transition',
56
+ index: { $count: '$.flow.doc.transitions[*]' },
57
+ path: { $concat: ['/transitions/', { $string: { $count: '$.flow.doc.transitions[*]' } }] },
58
+ } },
59
+ ] },
60
+ { patch: [
61
+ { op: 'add', path: '/flow/history/past/-', value: '$.flow.doc' },
62
+ { op: 'replace', path: '/flow/history/future', value: [] },
63
+ { op: 'add', path: '/flow/doc/edges/-',
64
+ value: { from: '$.flow.connect', to: '$payload.id' } },
65
+ { op: 'replace', path: '/flow/connect', value: null },
66
+ { op: 'replace', path: '/flow/selection', value: {
67
+ type: 'edge',
68
+ index: { $count: '$.flow.doc.edges[*]' },
69
+ path: { $concat: ['/edges/', { $string: { $count: '$.flow.doc.edges[*]' } }] },
70
+ } },
71
+ ] },
72
+ ] },
73
+ { patch: [
74
+ { op: 'replace', path: '/flow/connect', value: null },
75
+ { op: 'replace', path: '/flow/selection', value: '$payload' },
76
+ // a plain pick IS "show me this one" — on a phone the inspector
77
+ // is a different pane, so the gesture carries the user to it.
78
+ // The connect-commit branches above deliberately do not: mid-
79
+ // connect the diagram is where the next click has to land.
80
+ { op: 'replace', path: '/flow/mobilePane', value: 'inspector' },
81
+ ] },
82
+ ],
83
+ },
84
+
85
+ 'flow/connect-arm': {
86
+ $if: [
87
+ { $or: [{ $eq: ['$.flow.selection.type', 'state'] }, { $eq: ['$.flow.selection.type', 'node'] }] },
88
+ { patch: [{ op: 'replace', path: '/flow/connect', value: '$.flow.selection.id' }] },
89
+ ],
90
+ },
91
+ 'flow/connect-cancel': { patch: [{ op: 'replace', path: '/flow/connect', value: null }] },
92
+
93
+ // Adding mints the new id in a JS effect (`flow-mint`): a count-based
94
+ // `'s' + (count + 1)` id may already exist after a delete — on the nodes
95
+ // object an add would then REPLACE the user's node, and on the states
96
+ // array it would append a duplicate id. The *-minted actions receive
97
+ // the uniqueness-scanned id via payload and stay plain patches.
98
+ 'flow/add-state': { effects: [{ run: 'flow-mint', with: { kind: 'state' } }] },
99
+ 'flow/add-node': { effects: [{ run: 'flow-mint', with: { kind: 'node' } }] },
100
+
101
+ 'flow/state-minted': {
102
+ patch: [
103
+ { op: 'add', path: '/flow/history/past/-', value: '$.flow.doc' },
104
+ { op: 'replace', path: '/flow/history/future', value: [] },
105
+ { op: 'add', path: '/flow/doc/states/-', value: '$payload' },
106
+ { op: 'replace', path: '/flow/selection', value: {
107
+ type: 'state',
108
+ id: '$payload',
109
+ path: { $concat: ['/states/', { $string: { $count: '$.flow.doc.states[*]' } }] },
110
+ } },
111
+ ],
112
+ },
113
+
114
+ 'flow/node-minted': {
115
+ patch: [
116
+ { op: 'add', path: '/flow/history/past/-', value: '$.flow.doc' },
117
+ { op: 'replace', path: '/flow/history/future', value: [] },
118
+ { op: 'add',
119
+ path: { $concat: ['/flow/doc/nodes/', '$payload'] },
120
+ value: { kind: 'task', run: '$payload' } },
121
+ { op: 'replace', path: '/flow/selection', value: {
122
+ type: 'node',
123
+ id: '$payload',
124
+ path: { $concat: ['/nodes/', '$payload'] },
125
+ } },
126
+ ],
127
+ },
128
+
129
+ // Deleting a state or node cascades to every transition/edge that
130
+ // references it — the cascade is computed BY THE QUERY (a filtered
131
+ // rebuild of the document); simple members are a computed remove.
132
+ 'flow/delete': {
133
+ $if: [
134
+ { $eq: ['$.flow.selection.type', 'state'] },
135
+ { patch: [
136
+ { op: 'add', path: '/flow/history/past/-', value: '$.flow.doc' },
137
+ { op: 'replace', path: '/flow/history/future', value: [] },
138
+ { op: 'replace', path: '/flow/doc', value: { $map: [
139
+ ['$$fsm', '0.1'],
140
+ ['initial', { $if: [
141
+ { $eq: ['$.flow.doc.initial', '$.flow.selection.id'] }, null, '$.flow.doc.initial'] }],
142
+ ['states', [{ $for: { s: '$.flow.doc.states[*]' },
143
+ $where: { $ne: [{ $if: [{ '$is-string': '$s' }, '$s', '$s.id'] }, '$.flow.selection.id'] },
144
+ $return: '$s' }]],
145
+ ['transitions', [{ $for: { t: '$.flow.doc.transitions[*]' },
146
+ $where: { $and: [
147
+ { $ne: ['$t.from', '$.flow.selection.id'] },
148
+ { $ne: ['$t.to', '$.flow.selection.id'] } ] },
149
+ $return: '$t' }]],
150
+ ] } },
151
+ { op: 'replace', path: '/flow/selection', value: null },
152
+ { op: 'replace', path: '/flow/connect', value: null },
153
+ ] },
154
+ { $if: [
155
+ { $eq: ['$.flow.selection.type', 'node'] },
156
+ { patch: [
157
+ { op: 'add', path: '/flow/history/past/-', value: '$.flow.doc' },
158
+ { op: 'replace', path: '/flow/history/future', value: [] },
159
+ { op: 'replace', path: '/flow/doc', value: { $map: [
160
+ ['$$dag', '0.1'],
161
+ ['nodes', { '$from-entries': { $for: { e: { $entries: '$.flow.doc.nodes' } },
162
+ $where: { $ne: ['$e.key', '$.flow.selection.id'] },
163
+ $return: '$e' } }],
164
+ ['edges', [{ $for: { g: '$.flow.doc.edges[*]' },
165
+ $where: { $and: [
166
+ { $ne: ['$g.from', '$.flow.selection.id'] },
167
+ { $ne: ['$g.to', '$.flow.selection.id'] } ] },
168
+ $return: '$g' }]],
169
+ ] } },
170
+ { op: 'replace', path: '/flow/selection', value: null },
171
+ { op: 'replace', path: '/flow/connect', value: null },
172
+ ] },
173
+ { patch: [
174
+ { op: 'add', path: '/flow/history/past/-', value: '$.flow.doc' },
175
+ { op: 'replace', path: '/flow/history/future', value: [] },
176
+ { op: 'remove', path: { $concat: ['/flow/doc', '$.flow.selection.path'] } },
177
+ { op: 'replace', path: '/flow/selection', value: null },
178
+ ] },
179
+ ] },
180
+ ],
181
+ },
182
+
183
+ 'flow/undo': {
184
+ $if: [{ $exists: '$.flow.history.past[*]' }, { patch: [
185
+ { op: 'add', path: '/flow/history/future/-', value: '$.flow.doc' },
186
+ { op: 'replace', path: '/flow/doc', value: '$.flow.history.past[-1]' },
187
+ { op: 'remove', path: { $concat: ['/flow/history/past/',
188
+ { $string: { $sub: [{ $count: '$.flow.history.past[*]' }, 1] } }] } },
189
+ { op: 'replace', path: '/flow/selection', value: null },
190
+ ] }],
191
+ },
192
+ 'flow/redo': {
193
+ $if: [{ $exists: '$.flow.history.future[*]' }, { patch: [
194
+ { op: 'add', path: '/flow/history/past/-', value: '$.flow.doc' },
195
+ { op: 'replace', path: '/flow/doc', value: '$.flow.history.future[-1]' },
196
+ { op: 'remove', path: { $concat: ['/flow/history/future/',
197
+ { $string: { $sub: [{ $count: '$.flow.history.future[*]' }, 1] } }] } },
198
+ { op: 'replace', path: '/flow/selection', value: null },
199
+ ] }],
200
+ },
201
+
202
+ // The text pane commits through the fail-closed parse effect: a
203
+ // broken edit reports here and never touches the document.
204
+ 'flow/text': { effects: [{ run: 'flow-parse', with: { text: '$event.value', kind: '$.flow.kind' } }] },
205
+ 'flow/parsed': {
206
+ patch: [
207
+ { op: 'add', path: '/flow/history/past/-', value: '$.flow.doc' },
208
+ { op: 'replace', path: '/flow/history/future', value: [] },
209
+ { op: 'replace', path: '/flow/doc', value: '$payload.doc' },
210
+ { op: 'replace', path: '/flow/parseError', value: null },
211
+ { op: 'replace', path: '/flow/selection', value: null },
212
+ ],
213
+ },
214
+ 'flow/parse-error': { patch: [{ op: 'replace', path: '/flow/parseError', value: '$payload.message' }] },
215
+
216
+ // Machine runs: the nested sandbox app boots from the generated
217
+ // actions (the host widget owns its lifecycle; `revision` reboots).
218
+ 'flow/run': {
219
+ patch: [
220
+ { op: 'replace', path: '/flow/run', value: { current: '$.flow.doc.initial', prev: null, log: [] } },
221
+ { op: 'replace', path: '/flow/revision', value: { $add: ['$.flow.revision', 1] } },
222
+ // booting is a request to WATCH it run (phone only; desktop shows
223
+ // every pane, so this patch is invisible there)
224
+ { op: 'replace', path: '/flow/mobilePane', value: 'run' },
225
+ ],
226
+ },
227
+ 'flow/stop': { patch: [{ op: 'replace', path: '/flow/run', value: null }] },
228
+ 'flow/send': { effects: [{ run: 'flow-run-send', with: { event: '$payload' } }] },
229
+ 'flow/run-tx': {
230
+ $if: ['$.flow.run', { patch: [
231
+ // capture the state we're leaving BEFORE overwriting current, so
232
+ // the diagram can glow the transition just taken
233
+ { op: 'replace', path: '/flow/run/prev', value: '$.flow.run.current' },
234
+ { op: 'replace', path: '/flow/run/current', value: '$payload.current' },
235
+ { op: 'add', path: '/flow/run/log/-', value: '$payload' },
236
+ ] }],
237
+ },
238
+ 'flow/run-log': {
239
+ $if: ['$.flow.run', { patch: [{ op: 'add', path: '/flow/run/log/-', value: '$payload' }] }],
240
+ },
241
+
242
+ // Dag runs: compile + execute through the boundary effect; per-node
243
+ // settlement records tint the diagram live.
244
+ 'flow/dag-input': { patch: [{ op: 'replace', path: '/flow/dagInput', value: '$event.value' }] },
245
+ 'flow/dag-run': {
246
+ patch: [
247
+ { op: 'replace', path: '/flow/run',
248
+ value: { running: true, nodes: {}, output: null, error: null, log: [] } },
249
+ { op: 'replace', path: '/flow/mobilePane', value: 'run' },
250
+ ],
251
+ effects: [{ run: 'flow-dag-run', with: { doc: '$.flow.doc', inputText: '$.flow.dagInput' } }],
252
+ },
253
+ 'flow/dag-node': {
254
+ $if: ['$.flow.run', { patch: [
255
+ { op: 'add', path: { $concat: ['/flow/run/nodes/', '$payload.id'] }, value: '$payload.status' },
256
+ { op: 'add', path: '/flow/run/log/-', value: '$payload' },
257
+ ] }],
258
+ },
259
+ 'flow/dag-done': {
260
+ $if: ['$.flow.run', { patch: [
261
+ { op: 'replace', path: '/flow/run/running', value: false },
262
+ { op: 'replace', path: '/flow/run/output', value: '$payload.output' },
263
+ ] }],
264
+ },
265
+ 'flow/dag-fail': {
266
+ $if: ['$.flow.run', { patch: [
267
+ { op: 'replace', path: '/flow/run/running', value: false },
268
+ { op: 'replace', path: '/flow/run/error', value: '$payload.message' },
269
+ ] }],
270
+ },
271
+ 'flow/dag-abort': { effects: [{ run: 'flow-dag-abort' }] },
272
+
273
+ // The inspector writes through the six standard form actions,
274
+ // mirrored from @jarenjs/app's createFormActions with one change:
275
+ // the target prepends the SELECTION's pointer, so one static action
276
+ // set serves whichever member is selected.
277
+ 'flow/f-input': { patch: [{
278
+ op: { $if: [{ $or: ['$payload.element', { $eq: ['$payload.pointer', ''] }] }, 'replace', 'add'] },
279
+ path: { $concat: ['/flow/doc', '$.flow.selection.path', '$payload.pointer'] },
280
+ value: '$event.value' }] },
281
+ 'flow/f-check': { patch: [{
282
+ op: { $if: [{ $or: ['$payload.element', { $eq: ['$payload.pointer', ''] }] }, 'replace', 'add'] },
283
+ path: { $concat: ['/flow/doc', '$.flow.selection.path', '$payload.pointer'] },
284
+ value: '$event.checked' }] },
285
+ 'flow/f-number': { patch: [{
286
+ op: { $if: [{ $or: ['$payload.element', { $eq: ['$payload.pointer', ''] }] }, 'replace', 'add'] },
287
+ path: { $concat: ['/flow/doc', '$.flow.selection.path', '$payload.pointer'] },
288
+ value: { $if: [{ $ne: ['$event.value', ''] }, { $number: '$event.value' }, null] } }] },
289
+ 'flow/f-json': { patch: [{
290
+ op: { $if: [{ $or: ['$payload.element', { $eq: ['$payload.pointer', ''] }] }, 'replace', 'add'] },
291
+ path: { $concat: ['/flow/doc', '$.flow.selection.path', '$payload.pointer'] },
292
+ value: '$event.formJsonValue' }] },
293
+ 'flow/f-add': { patch: [{
294
+ op: 'add',
295
+ path: { $concat: ['/flow/doc', '$.flow.selection.path', '$payload.pointer', '/-'] },
296
+ value: '$payload.value' }] },
297
+ 'flow/f-remove': { patch: [{
298
+ op: 'remove',
299
+ path: { $concat: ['/flow/doc', '$.flow.selection.path', '$payload.pointer'] } }] },
300
+ };
301
+
302
+ // SVG groups need the keyboard activation native buttons provide.
303
+ FLOW_ACTIONS['flow/key-pick'] = { $if: [{ $or: [
304
+ { $eq: ['$event.key', 'Enter'] }, { $eq: ['$event.key', ' '] },
305
+ ] }, FLOW_ACTIONS['flow/pick']] };
306
+
307
+ // Publication uses the same serialized transition queue as every manual gesture.
308
+ FLOW_ACTIONS['flow/replace'] = { $if: [
309
+ { $eq: [{ kind: '$.flow.kind', doc: '$.flow.doc' }, '$payload.expected'] },
310
+ { patch: [
311
+ { op: 'add', path: '/flow/history/past/-', value: '$.flow.doc' },
312
+ { op: 'replace', path: '/flow/history/future', value: [] },
313
+ { op: 'replace', path: '/flow/kind', value: '$payload.kind' },
314
+ { op: 'replace', path: '/flow/doc', value: '$payload.doc' },
315
+ { op: 'replace', path: '/flow/selection', value: null },
316
+ { op: 'replace', path: '/flow/connect', value: null },
317
+ { op: 'replace', path: '/flow/parseError', value: null },
318
+ { op: 'replace', path: '/flow/run', value: null },
319
+ ], effects: [{ run: 'flow-accepted', with: '$payload.requestId' }] },
320
+ { effects: [{ run: 'flow-refused', with: '$payload.requestId' }] },
321
+ ] };
322
+ FLOW_ACTIONS['flow/execute'] = {
323
+ patch: [
324
+ { op: 'replace', path: '/flow/runContext', value: '$payload.input' },
325
+ { op: 'replace', path: '/flow/dagInput', value: '$payload.inputText' },
326
+ { op: 'replace', path: '/flow/run', value: { $if: [{ $eq: ['$.flow.kind', 'dag'] },
327
+ { running: true, nodes: {}, output: null, error: null, log: [] }, '$.flow.run'] } },
328
+ { op: 'replace', path: '/flow/mobilePane', value: 'run' },
329
+ ],
330
+ effects: [{ run: 'flow-editor-run', with: { requestId: '$payload.requestId', kind: '$.flow.kind', doc: '$.flow.doc', inputText: '$payload.inputText', event: '$payload.event' } }],
331
+ };
@@ -0,0 +1,8 @@
1
+ //@ts-check
2
+ export { FLOW_ACTIONS } from './actions.js';
3
+ export { FLOW_RULES, FLOW_FORM_RULES } from './views.js';
4
+ export { createFlowRuntime, flowPageViewModel, flowText, textLossReason, memberAt } from './runtime.js';
5
+ export { createProjectFlowWidget } from './project-widget.js';
6
+ export { createFlowState, createFlowController } from '../../flow-editor.js';
7
+ export { validateFlowDocument } from '../../flow-document.js';
8
+ export { mountFlowEditor } from './mount.js';
@@ -0,0 +1,53 @@
1
+ //@ts-check
2
+ /** Assemble the shared Flow component. The public mount exposes only document operations. */
3
+ import { createApp, formEventFields } from '@jarenjs/app';
4
+ import { createTypeTestCompiler } from '@jarenjs/validate/query';
5
+ import { createFlowState, createFlowController } from '../../flow-editor.js';
6
+ import { FLOW_ACTIONS } from './actions.js';
7
+ import { FLOW_RULES, FLOW_FORM_RULES } from './views.js';
8
+ import { createFlowRuntime, flowPageViewModel } from './runtime.js';
9
+
10
+ /** @param {HTMLElement|null} node @param {any} options
11
+ * @returns {{ app: ReturnType<typeof createApp>, editor: Pick<ReturnType<typeof createFlowController>, 'read'|'validate'|'replace'|'apply'|'run'|'subscribe'|'dispose'> }} */
12
+ export function createFlowEditor(node, options) {
13
+ let app;
14
+ const runtime = createFlowRuntime({ ...options, getFlow: () => app.getState().flow });
15
+ const controller = createFlowController({ getApp: () => app, runtime });
16
+ app = createApp({
17
+ state: { flow: createFlowState(options) },
18
+ actions: { ...FLOW_ACTIONS, ...(options.resetDocument ? {
19
+ 'flow/clear': { effects: [{ run: 'flow-reset-document' }] },
20
+ } : {}) },
21
+ view: { $jslt: '0.1', modes: { flow: { unmatched: 'error' } }, rules: [
22
+ { match: '$', body: options.resetDocument
23
+ ? ['div', { class: 'project-flow' }, { $apply: ['$.ui.flow', 'flow'] }]
24
+ : { $apply: ['$.ui.flow', 'flow'] } }, ...FLOW_RULES, ...FLOW_FORM_RULES,
25
+ ] },
26
+ }, {
27
+ node, document: node?.ownerDocument, schedule: options.schedule,
28
+ compileTypeTest: createTypeTestCompiler(), eventFields: formEventFields,
29
+ viewModel: state => ({ ui: { flow: flowPageViewModel(state.flow, options.templates) } }),
30
+ widgets: { 'flow-doc': runtime.widget },
31
+ effects: { ...runtime.effects, ...controller.effects,
32
+ 'flow-reset-document': () => app.dispatch('flow/load', { kind: options.kind,
33
+ doc: structuredClone(options.document), runContext: structuredClone(options.input ?? null),
34
+ dagInput: JSON.stringify(options.input ?? null) }),
35
+ },
36
+ onError: options.onError,
37
+ });
38
+ controller.attach();
39
+ const editor = {
40
+ read: controller.read, validate: controller.validate, replace: controller.replace,
41
+ apply: controller.apply, run: controller.run, subscribe: controller.subscribe,
42
+ dispose() { controller.dispose(); app.destroy(); },
43
+ };
44
+ return { app, editor };
45
+ }
46
+
47
+ /**
48
+ * @param {HTMLElement|null} node
49
+ * @param {{ kind?: 'fsm'|'dag'|null, document?: any, input?: any, templates?: any[],
50
+ * template?: (name: string) => any, tasks?: Record<string, Function>,
51
+ * schedule?: (flush: () => void) => void, onError?: (error: Error) => void }} [options]
52
+ */
53
+ export function mountFlowEditor(node, options = {}) { return createFlowEditor(node, options).editor; }
@@ -0,0 +1,44 @@
1
+ //@ts-check
2
+ /** The existing Flow editor as a project-file widget. Its private app only
3
+ * receives the editor's local effects; document effects are recorded, never
4
+ * delegated to the website. Destroy owns the nested app and pending DAG. */
5
+ import { semanticKey } from '@jarenjs/core/object';
6
+ import { createFlowEditor } from './mount.js';
7
+
8
+ export function createProjectFlowWidget(env = {}) {
9
+ const boot = (handle, props) => {
10
+ handle.name = props.name;
11
+ handle.revision = props.revision;
12
+ handle.key = semanticKey(props.doc);
13
+ handle.inputKey = semanticKey(props.input ?? null);
14
+ handle.pendingEcho = false;
15
+ const { app, editor } = createFlowEditor(handle.host, { ...env,
16
+ kind: props.kind, document: props.doc, input: props.input, resetDocument: true, mintFromCount: false,
17
+ onError: err => handle.emit({ action: 'project/stage-error', with: err.message }),
18
+ });
19
+ handle.app = app; handle.editor = editor;
20
+ app.subscribe((state) => {
21
+ const key = semanticKey(state.flow.doc);
22
+ if (key === handle.key) return;
23
+ handle.key = key;
24
+ handle.pendingEcho = true;
25
+ handle.emit({ action: 'project/artifact-edit', with: { name: handle.name, doc: state.flow.doc } });
26
+ });
27
+ };
28
+ const destroy = (h) => { h.editor?.dispose(); h.app = null; };
29
+ return {
30
+ mount(host, props, emit) { const h = { host, emit }; boot(h, props); return h; },
31
+ update(h, props) {
32
+ // An echo of our own edit preserves selection, undo and the run pane.
33
+ if (h.name === props.name && h.key === semanticKey(props.doc)
34
+ && h.inputKey === semanticKey(props.input ?? null)
35
+ && (h.pendingEcho || h.revision === props.revision)) {
36
+ h.pendingEcho = false;
37
+ h.revision = props.revision;
38
+ return;
39
+ }
40
+ destroy(h); boot(h, props);
41
+ },
42
+ unmount: destroy,
43
+ };
44
+ }