@caddis/cli 0.4.45 → 0.4.47

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 (193) hide show
  1. package/bundles/antigravity-plugin/plugin.json +1 -1
  2. package/bundles/antigravity-plugin/skills/flow-studio/README.md +46 -7
  3. package/bundles/antigravity-plugin/skills/flow-studio/SKILL.md +81 -81
  4. package/bundles/antigravity-plugin/skills/flow-studio/examples/README.md +8 -0
  5. package/bundles/antigravity-plugin/skills/flow-studio/examples/agent-journey.json +103 -0
  6. package/bundles/antigravity-plugin/skills/flow-studio/examples/approval-flow.json +134 -0
  7. package/bundles/antigravity-plugin/skills/flow-studio/examples/ci-cd-pipeline.json +132 -0
  8. package/bundles/antigravity-plugin/skills/flow-studio/examples/incident-timeline.json +109 -0
  9. package/bundles/antigravity-plugin/skills/flow-studio/examples/order-desk.json +99 -0
  10. package/bundles/antigravity-plugin/skills/flow-studio/examples/service-map.json +133 -0
  11. package/bundles/antigravity-plugin/skills/flow-studio/examples/state-machine.json +106 -0
  12. package/bundles/antigravity-plugin/skills/flow-studio/examples/ticket-lifecycle.json +107 -0
  13. package/bundles/antigravity-plugin/skills/flow-studio/model.schema.json +27 -8
  14. package/bundles/antigravity-plugin/skills/flow-studio/react/FlowStudioView.test.tsx +82 -0
  15. package/bundles/antigravity-plugin/skills/flow-studio/react/FlowStudioView.tsx +67 -0
  16. package/bundles/antigravity-plugin/skills/flow-studio/react/README.md +37 -0
  17. package/bundles/antigravity-plugin/skills/flow-studio/react/index.ts +3 -0
  18. package/bundles/antigravity-plugin/skills/flow-studio/react/native/FlowCanvas.tsx +58 -0
  19. package/bundles/antigravity-plugin/skills/flow-studio/react/native/Inspector.tsx +27 -0
  20. package/bundles/antigravity-plugin/skills/flow-studio/react/native/index.ts +6 -0
  21. package/bundles/antigravity-plugin/skills/flow-studio/react/native/native.css +18 -0
  22. package/bundles/antigravity-plugin/skills/flow-studio/react/native/native.test.tsx +161 -0
  23. package/bundles/antigravity-plugin/skills/flow-studio/react/native/useFlowGeometry.ts +22 -0
  24. package/bundles/antigravity-plugin/skills/flow-studio/react/native/useTrace.ts +29 -0
  25. package/bundles/antigravity-plugin/skills/flow-studio/react/native/useViewport.ts +75 -0
  26. package/bundles/antigravity-plugin/skills/flow-studio/react/states.tsx +13 -0
  27. package/bundles/antigravity-plugin/skills/flow-studio/references/design-language.md +177 -0
  28. package/bundles/antigravity-plugin/skills/flow-studio/references/implement-in-app.md +213 -0
  29. package/bundles/antigravity-plugin/skills/flow-studio/references/model-schema.md +254 -0
  30. package/bundles/antigravity-plugin/skills/flow-studio/references/notes-and-spec.md +74 -0
  31. package/bundles/antigravity-plugin/skills/flow-studio/references/patterns.md +385 -49
  32. package/bundles/antigravity-plugin/skills/flow-studio/references/ux-rules.md +91 -0
  33. package/bundles/antigravity-plugin/skills/flow-studio/references/verify.md +124 -0
  34. package/bundles/antigravity-plugin/skills/flow-studio/scripts/build-engine.mjs +54 -14
  35. package/bundles/antigravity-plugin/skills/flow-studio/scripts/build.mjs +21 -4
  36. package/bundles/antigravity-plugin/skills/flow-studio/scripts/check.mjs +320 -28
  37. package/bundles/antigravity-plugin/skills/flow-studio/scripts/default-notes.json +67 -0
  38. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/auto-entry-guard.mjs +5 -0
  39. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/auto-entry-guard.test.mjs +14 -0
  40. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/auto-entry.test.mjs +31 -0
  41. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/build-engine.test.mjs +30 -2
  42. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/check-args.mjs +18 -0
  43. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/check-args.test.mjs +15 -0
  44. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/check-selftest.test.mjs +64 -4
  45. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/cli.test.mjs +115 -0
  46. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/demo-pack.mjs +23 -0
  47. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/demo-pack.test.mjs +34 -0
  48. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/docs.test.mjs +128 -0
  49. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/library.test.mjs +26 -0
  50. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-cleanup.mjs +12 -0
  51. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-cleanup.test.mjs +45 -0
  52. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-page.mjs +3 -0
  53. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-page.test.mjs +9 -0
  54. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/styles.test.mjs +37 -0
  55. package/bundles/antigravity-plugin/skills/flow-studio/scripts/react-proof.mjs +92 -0
  56. package/bundles/antigravity-plugin/skills/flow-studio/scripts/validate.mjs +5 -1
  57. package/bundles/antigravity-plugin/skills/flow-studio/src/autolayout.test.ts +78 -0
  58. package/bundles/antigravity-plugin/skills/flow-studio/src/autolayout.ts +132 -0
  59. package/bundles/antigravity-plugin/skills/flow-studio/src/chrome.ts +11 -7
  60. package/bundles/antigravity-plugin/skills/flow-studio/src/controls.ts +8 -0
  61. package/bundles/antigravity-plugin/skills/flow-studio/src/ctx.ts +16 -0
  62. package/bundles/antigravity-plugin/skills/flow-studio/src/diff.test.ts +31 -0
  63. package/bundles/antigravity-plugin/skills/flow-studio/src/diff.ts +34 -0
  64. package/bundles/antigravity-plugin/skills/flow-studio/src/engine.ts +83 -32
  65. package/bundles/antigravity-plugin/skills/flow-studio/src/inspector.ts +23 -5
  66. package/bundles/antigravity-plugin/skills/flow-studio/src/interact.ts +21 -8
  67. package/bundles/antigravity-plugin/skills/flow-studio/src/layout.test.ts +75 -0
  68. package/bundles/antigravity-plugin/skills/flow-studio/src/layout.ts +49 -11
  69. package/bundles/antigravity-plugin/skills/flow-studio/src/lib.ts +20 -0
  70. package/bundles/antigravity-plugin/skills/flow-studio/src/metrics.test.ts +57 -0
  71. package/bundles/antigravity-plugin/skills/flow-studio/src/metrics.ts +29 -0
  72. package/bundles/antigravity-plugin/skills/flow-studio/src/model.test.ts +62 -2
  73. package/bundles/antigravity-plugin/skills/flow-studio/src/model.ts +101 -16
  74. package/bundles/antigravity-plugin/skills/flow-studio/src/notes.ts +8 -0
  75. package/bundles/antigravity-plugin/skills/flow-studio/src/page.ts +21 -0
  76. package/bundles/antigravity-plugin/skills/flow-studio/src/playback.test.ts +104 -0
  77. package/bundles/antigravity-plugin/skills/flow-studio/src/playback.ts +106 -0
  78. package/bundles/antigravity-plugin/skills/flow-studio/src/playbackui.ts +193 -0
  79. package/bundles/antigravity-plugin/skills/flow-studio/src/render.ts +66 -18
  80. package/bundles/antigravity-plugin/skills/flow-studio/src/routing.test.ts +21 -1
  81. package/bundles/antigravity-plugin/skills/flow-studio/src/routing.ts +32 -0
  82. package/bundles/antigravity-plugin/skills/flow-studio/src/rules.test.ts +48 -1
  83. package/bundles/antigravity-plugin/skills/flow-studio/src/rules.ts +52 -1
  84. package/bundles/antigravity-plugin/skills/flow-studio/src/selection.test.ts +12 -0
  85. package/bundles/antigravity-plugin/skills/flow-studio/src/selection.ts +4 -0
  86. package/bundles/antigravity-plugin/skills/flow-studio/src/stateCss.test.ts +7 -0
  87. package/bundles/antigravity-plugin/skills/flow-studio/src/stateCss.ts +1 -0
  88. package/bundles/antigravity-plugin/skills/flow-studio/src/styles.css +44 -10
  89. package/bundles/antigravity-plugin/skills/flow-studio/src/trace.test.ts +21 -1
  90. package/bundles/antigravity-plugin/skills/flow-studio/src/trace.ts +18 -0
  91. package/bundles/antigravity-plugin/skills/flow-studio/templates/engine.html +995 -82
  92. package/bundles/antigravity-plugin/skills/flow-studio/templates/engine.shell.html +1 -39
  93. package/bundles/antigravity-plugin/skills/flow-studio/templates/flow-studio.css +191 -0
  94. package/bundles/antigravity-plugin/skills/flow-studio/templates/flow-studio.lib.d.mts +1 -0
  95. package/bundles/antigravity-plugin/skills/flow-studio/templates/flow-studio.lib.mjs +2492 -0
  96. package/bundles/antigravity-plugin/skills/flow-studio/templates/shell-body.html +50 -0
  97. package/bundles/codex/.codex/skills/frontend/flow-studio/README.md +46 -7
  98. package/bundles/codex/.codex/skills/frontend/flow-studio/SKILL.md +81 -81
  99. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/README.md +8 -0
  100. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/agent-journey.json +103 -0
  101. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/approval-flow.json +134 -0
  102. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/ci-cd-pipeline.json +132 -0
  103. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/incident-timeline.json +109 -0
  104. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/order-desk.json +99 -0
  105. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/service-map.json +133 -0
  106. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/state-machine.json +106 -0
  107. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/ticket-lifecycle.json +107 -0
  108. package/bundles/codex/.codex/skills/frontend/flow-studio/model.schema.json +27 -8
  109. package/bundles/codex/.codex/skills/frontend/flow-studio/react/FlowStudioView.test.tsx +82 -0
  110. package/bundles/codex/.codex/skills/frontend/flow-studio/react/FlowStudioView.tsx +67 -0
  111. package/bundles/codex/.codex/skills/frontend/flow-studio/react/README.md +37 -0
  112. package/bundles/codex/.codex/skills/frontend/flow-studio/react/index.ts +3 -0
  113. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/FlowCanvas.tsx +58 -0
  114. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/Inspector.tsx +27 -0
  115. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/index.ts +6 -0
  116. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/native.css +18 -0
  117. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/native.test.tsx +161 -0
  118. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/useFlowGeometry.ts +22 -0
  119. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/useTrace.ts +29 -0
  120. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/useViewport.ts +75 -0
  121. package/bundles/codex/.codex/skills/frontend/flow-studio/react/states.tsx +13 -0
  122. package/bundles/codex/.codex/skills/frontend/flow-studio/references/design-language.md +177 -0
  123. package/bundles/codex/.codex/skills/frontend/flow-studio/references/implement-in-app.md +213 -0
  124. package/bundles/codex/.codex/skills/frontend/flow-studio/references/model-schema.md +254 -0
  125. package/bundles/codex/.codex/skills/frontend/flow-studio/references/notes-and-spec.md +74 -0
  126. package/bundles/codex/.codex/skills/frontend/flow-studio/references/patterns.md +385 -49
  127. package/bundles/codex/.codex/skills/frontend/flow-studio/references/ux-rules.md +91 -0
  128. package/bundles/codex/.codex/skills/frontend/flow-studio/references/verify.md +124 -0
  129. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/build-engine.mjs +54 -14
  130. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/build.mjs +21 -4
  131. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/check.mjs +320 -28
  132. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/default-notes.json +67 -0
  133. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/auto-entry-guard.mjs +5 -0
  134. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/auto-entry-guard.test.mjs +14 -0
  135. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/auto-entry.test.mjs +31 -0
  136. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/build-engine.test.mjs +30 -2
  137. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/check-args.mjs +18 -0
  138. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/check-args.test.mjs +15 -0
  139. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/check-selftest.test.mjs +64 -4
  140. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/cli.test.mjs +115 -0
  141. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/demo-pack.mjs +23 -0
  142. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/demo-pack.test.mjs +34 -0
  143. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/docs.test.mjs +128 -0
  144. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/library.test.mjs +26 -0
  145. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-cleanup.mjs +12 -0
  146. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-cleanup.test.mjs +45 -0
  147. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-page.mjs +3 -0
  148. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-page.test.mjs +9 -0
  149. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/styles.test.mjs +37 -0
  150. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/react-proof.mjs +92 -0
  151. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/validate.mjs +5 -1
  152. package/bundles/codex/.codex/skills/frontend/flow-studio/src/autolayout.test.ts +78 -0
  153. package/bundles/codex/.codex/skills/frontend/flow-studio/src/autolayout.ts +132 -0
  154. package/bundles/codex/.codex/skills/frontend/flow-studio/src/chrome.ts +11 -7
  155. package/bundles/codex/.codex/skills/frontend/flow-studio/src/controls.ts +8 -0
  156. package/bundles/codex/.codex/skills/frontend/flow-studio/src/ctx.ts +16 -0
  157. package/bundles/codex/.codex/skills/frontend/flow-studio/src/diff.test.ts +31 -0
  158. package/bundles/codex/.codex/skills/frontend/flow-studio/src/diff.ts +34 -0
  159. package/bundles/codex/.codex/skills/frontend/flow-studio/src/engine.ts +83 -32
  160. package/bundles/codex/.codex/skills/frontend/flow-studio/src/inspector.ts +23 -5
  161. package/bundles/codex/.codex/skills/frontend/flow-studio/src/interact.ts +21 -8
  162. package/bundles/codex/.codex/skills/frontend/flow-studio/src/layout.test.ts +75 -0
  163. package/bundles/codex/.codex/skills/frontend/flow-studio/src/layout.ts +49 -11
  164. package/bundles/codex/.codex/skills/frontend/flow-studio/src/lib.ts +20 -0
  165. package/bundles/codex/.codex/skills/frontend/flow-studio/src/metrics.test.ts +57 -0
  166. package/bundles/codex/.codex/skills/frontend/flow-studio/src/metrics.ts +29 -0
  167. package/bundles/codex/.codex/skills/frontend/flow-studio/src/model.test.ts +62 -2
  168. package/bundles/codex/.codex/skills/frontend/flow-studio/src/model.ts +101 -16
  169. package/bundles/codex/.codex/skills/frontend/flow-studio/src/notes.ts +8 -0
  170. package/bundles/codex/.codex/skills/frontend/flow-studio/src/page.ts +21 -0
  171. package/bundles/codex/.codex/skills/frontend/flow-studio/src/playback.test.ts +104 -0
  172. package/bundles/codex/.codex/skills/frontend/flow-studio/src/playback.ts +106 -0
  173. package/bundles/codex/.codex/skills/frontend/flow-studio/src/playbackui.ts +193 -0
  174. package/bundles/codex/.codex/skills/frontend/flow-studio/src/render.ts +66 -18
  175. package/bundles/codex/.codex/skills/frontend/flow-studio/src/routing.test.ts +21 -1
  176. package/bundles/codex/.codex/skills/frontend/flow-studio/src/routing.ts +32 -0
  177. package/bundles/codex/.codex/skills/frontend/flow-studio/src/rules.test.ts +48 -1
  178. package/bundles/codex/.codex/skills/frontend/flow-studio/src/rules.ts +52 -1
  179. package/bundles/codex/.codex/skills/frontend/flow-studio/src/selection.test.ts +12 -0
  180. package/bundles/codex/.codex/skills/frontend/flow-studio/src/selection.ts +4 -0
  181. package/bundles/codex/.codex/skills/frontend/flow-studio/src/stateCss.test.ts +7 -0
  182. package/bundles/codex/.codex/skills/frontend/flow-studio/src/stateCss.ts +1 -0
  183. package/bundles/codex/.codex/skills/frontend/flow-studio/src/styles.css +44 -10
  184. package/bundles/codex/.codex/skills/frontend/flow-studio/src/trace.test.ts +21 -1
  185. package/bundles/codex/.codex/skills/frontend/flow-studio/src/trace.ts +18 -0
  186. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/engine.html +995 -82
  187. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/engine.shell.html +1 -39
  188. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/flow-studio.css +191 -0
  189. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/flow-studio.lib.d.mts +1 -0
  190. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/flow-studio.lib.mjs +2492 -0
  191. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/shell-body.html +50 -0
  192. package/bundles/manifest.json +2 -2
  193. package/package.json +1 -1
@@ -0,0 +1,106 @@
1
+ {
2
+ "version": 1,
3
+ "meta": {
4
+ "title": "ORDER STATE MACHINE",
5
+ "help": "Track lifecycle transitions of an order across active processing, cancellation, fulfillment, and returns.",
6
+ "asOf": "2026-10-06",
7
+ "source": "Fictional Contoso Retail state machine. No real customer or order records are used."
8
+ },
9
+ "layout": "auto",
10
+ "columns": [
11
+ { "id": "creation", "title": "Creation", "width": 140 },
12
+ { "id": "payment", "title": "Payment", "width": 150 },
13
+ { "id": "fulfillment", "title": "Fulfillment", "width": 160 },
14
+ { "id": "delivery", "title": "Delivery", "width": 150 },
15
+ { "id": "post_sale", "title": "Post-Sale", "width": 150 }
16
+ ],
17
+ "lanes": [
18
+ { "id": "order", "title": "Order Lifecycle" },
19
+ { "id": "logistics", "title": "Fulfillment & Logistics" },
20
+ { "id": "service", "title": "Returns & Service" }
21
+ ],
22
+ "states": {
23
+ "active": { "word": "ACTIVE", "tone": "ok", "border": "solid", "edge": "flow", "motion": "flow", "bucket": "good" },
24
+ "terminal": { "word": "TERMINAL", "tone": "muted", "border": "solid", "edge": "dash-short", "motion": "none", "bucket": "good" },
25
+ "error": { "word": "ERROR", "tone": "crit", "border": "dotted", "edge": "dot", "motion": "breathe", "bucket": "gap" }
26
+ },
27
+ "nodes": [
28
+ { "id": "st_draft", "kind": "start", "lane": "order", "state": "active", "title": "Draft", "subtitle": "Cart checkout" },
29
+ { "id": "st_submitted", "kind": "card", "lane": "order", "state": "active", "title": "Submitted", "subtitle": "Awaiting payment" },
30
+ { "id": "gate_paid", "kind": "gateway", "lane": "order", "state": "active", "title": "Paid?", "subtitle": "Payment verification" },
31
+ { "id": "st_paid", "kind": "card", "lane": "order", "state": "active", "title": "Paid", "subtitle": "Payment settled" },
32
+ { "id": "st_packed", "kind": "card", "lane": "logistics", "state": "active", "title": "Packed", "subtitle": "Warehouse parcel" },
33
+ { "id": "st_shipped", "kind": "card", "lane": "logistics", "state": "active", "title": "Shipped", "subtitle": "In transit" },
34
+ { "id": "st_delivered", "kind": "card", "lane": "logistics", "state": "terminal", "title": "Delivered", "subtitle": "Handed to customer" },
35
+ { "id": "st_cancelled", "kind": "card", "lane": "order", "state": "error", "title": "Cancelled", "subtitle": "Order voided" },
36
+ { "id": "st_ret_req", "kind": "card", "lane": "service", "state": "active", "title": "Return requested", "subtitle": "RMA initiated" },
37
+ { "id": "st_returned", "kind": "card", "lane": "service", "state": "active", "title": "Returned", "subtitle": "Parcel received" },
38
+ { "id": "st_refunded", "kind": "card", "lane": "service", "state": "terminal", "title": "Refunded", "subtitle": "Funds credited", "sla": 30 },
39
+ { "id": "st_pay_fail", "kind": "end", "lane": "order", "state": "error", "title": "Payment failed", "subtitle": "Authorization error" },
40
+ { "id": "st_archived", "kind": "end", "lane": "logistics", "state": "terminal", "title": "Archived", "subtitle": "Order complete" }
41
+ ],
42
+ "links": [
43
+ { "id": "l_draft_sub", "from": "st_draft", "to": "st_submitted", "state": "active" },
44
+ { "id": "l_sub_gate", "from": "st_submitted", "to": "gate_paid", "state": "active" },
45
+ { "id": "l_gate_paid", "from": "gate_paid", "to": "st_paid", "state": "active", "label": "Authorized" },
46
+ { "id": "l_gate_fail", "from": "gate_paid", "to": "st_pay_fail", "state": "error", "label": "Declined" },
47
+ { "id": "l_paid_pack", "from": "st_paid", "to": "st_packed", "state": "active" },
48
+ { "id": "l_paid_canc", "from": "st_paid", "to": "st_cancelled", "state": "error", "label": "Customer cancel" },
49
+ { "id": "l_pack_ship", "from": "st_packed", "to": "st_shipped", "state": "active" },
50
+ { "id": "l_ship_deliv", "from": "st_shipped", "to": "st_delivered", "state": "active" },
51
+ { "id": "l_deliv_arch", "from": "st_delivered", "to": "st_archived", "state": "terminal" },
52
+ { "id": "l_deliv_ret", "from": "st_delivered", "to": "st_ret_req", "state": "active", "label": "Request return" },
53
+ { "id": "l_ret_deliv", "from": "st_ret_req", "to": "st_delivered", "state": "active", "kind": "loop-back", "label": "Return rejected" },
54
+ { "id": "l_ret_done", "from": "st_ret_req", "to": "st_returned", "state": "active", "label": "Return accepted" },
55
+ { "id": "l_ret_refund", "from": "st_returned", "to": "st_refunded", "state": "terminal" },
56
+ { "id": "l_canc_draft", "from": "st_cancelled", "to": "st_draft", "state": "active", "kind": "loop-back", "label": "Draft after cancel" },
57
+ { "id": "l_canc_ref", "from": "st_cancelled", "to": "st_refunded", "state": "terminal", "label": "Refund balance" }
58
+ ],
59
+ "inspector": {
60
+ "relations": [
61
+ { "id": "prior", "label": "Previous state", "dir": "up" },
62
+ { "id": "down", "label": "Next transition", "dir": "down" },
63
+ { "id": "terminal", "label": "Settled state", "dir": "down", "filter": { "kind": "end" } }
64
+ ],
65
+ "actions": [
66
+ {
67
+ "id": "sim_cancel",
68
+ "label": "Simulate: cancel order",
69
+ "dir": "down",
70
+ "staleText": "Order cancelled by customer. Refund process initiated."
71
+ }
72
+ ]
73
+ },
74
+ "notes": [
75
+ { "anchor": "#viewport", "title": "State transition model", "text": "This diagram defines legal state transitions from draft creation through post-sale settlement." },
76
+ { "anchor": "#n_gate_paid", "title": "Payment gateway", "text": "Orders branch to fulfillment on successful payment or terminate if payment authorization fails." },
77
+ { "anchor": "#n_st_ret_req", "title": "Return request evaluation", "text": "Return requests branch either to return acceptance and refund or route back to delivered status if rejected." },
78
+ { "anchor": "#playbar", "title": "Playback controls", "text": "Step through normal fulfillment or cancellation workflows to inspect transitions and SLA timing." }
79
+ ],
80
+ "scenarios": [
81
+ {
82
+ "id": "normal_order",
83
+ "label": "Normal order",
84
+ "steps": [
85
+ { "link": "l_draft_sub", "at": 0, "caption": "Customer submits order draft" },
86
+ { "link": "l_sub_gate", "at": 10, "caption": "Payment authorization requested" },
87
+ { "link": "l_gate_paid", "at": 20, "caption": "Payment confirmed successfully" },
88
+ { "link": "l_paid_pack", "at": 35, "caption": "Warehouse packs parcel" },
89
+ { "link": "l_pack_ship", "at": 50, "caption": "Courier picks up shipment" },
90
+ { "link": "l_ship_deliv", "at": 70, "caption": "Package delivered to customer" },
91
+ { "link": "l_deliv_arch", "at": 85, "caption": "Order archived after return period expires" }
92
+ ]
93
+ },
94
+ {
95
+ "id": "cancel_paid",
96
+ "label": "Cancel after payment",
97
+ "steps": [
98
+ { "link": "l_draft_sub", "at": 0, "caption": "Customer drafts order" },
99
+ { "link": "l_sub_gate", "at": 10, "caption": "Payment gateway contacted" },
100
+ { "link": "l_gate_paid", "at": 20, "caption": "Order paid" },
101
+ { "link": "l_paid_canc", "at": 30, "caption": "Customer cancels order before shipping" },
102
+ { "link": "l_canc_ref", "at": 45, "caption": "Refund issued after SLA target threshold" }
103
+ ]
104
+ }
105
+ ]
106
+ }
@@ -0,0 +1,107 @@
1
+ {
2
+ "version": 1,
3
+ "meta": {
4
+ "title": "SUPPORT TICKET LIFECYCLE",
5
+ "help": "Track support tickets from submission to resolution across four operational tiers. Follow escalation paths and identify SLA breach risks.",
6
+ "asOf": "2026-10-06",
7
+ "source": "Fictional Contoso Retail support desk data. No real customer or ticket records are used."
8
+ },
9
+ "layout": "columns-lanes",
10
+ "columns": [
11
+ { "id": "new", "title": "New", "width": 150 },
12
+ { "id": "triage", "title": "Triage", "width": 150 },
13
+ { "id": "progress", "title": "In progress", "width": 160 },
14
+ { "id": "waiting", "title": "Waiting", "width": 150 },
15
+ { "id": "resolved", "title": "Resolved", "width": 160 },
16
+ { "id": "closed", "title": "Closed", "width": 150 }
17
+ ],
18
+ "lanes": [
19
+ { "id": "customer", "title": "Customer" },
20
+ { "id": "l1", "title": "Support L1" },
21
+ { "id": "l2", "title": "Support L2" },
22
+ { "id": "billing", "title": "Billing team" }
23
+ ],
24
+ "states": {
25
+ "on_time": { "word": "ON TIME", "tone": "ok", "border": "solid", "edge": "flow", "motion": "flow", "bucket": "good" },
26
+ "at_risk": { "word": "AT RISK", "tone": "warn", "border": "dashed", "edge": "dash", "motion": "flow-slow", "bucket": "neutral" },
27
+ "breached": { "word": "BREACHED", "tone": "crit", "border": "dotted", "edge": "dot", "motion": "breathe", "bucket": "gap" },
28
+ "closed": { "word": "CLOSED", "tone": "muted", "border": "solid", "edge": "dash-short", "motion": "none", "bucket": "good" }
29
+ },
30
+ "nodes": [
31
+ { "id": "cust-submit", "kind": "start", "lane": "customer", "col": 0, "row": 0, "state": "on_time", "title": "Submit ticket" },
32
+ { "id": "cust-notify", "kind": "event", "lane": "customer", "col": 1, "row": 0, "state": "on_time", "title": "Receipt notice" },
33
+ { "id": "cust-confirm", "kind": "end", "lane": "customer", "col": 5, "row": 0, "state": "closed", "title": "Confirm resolution" },
34
+ { "id": "l1-triage", "kind": "gateway", "lane": "l1", "col": 1, "row": 2, "state": "on_time", "title": "Triage issue" },
35
+ { "id": "l1-work", "kind": "card", "lane": "l1", "col": 2, "row": 2, "state": "on_time", "title": "Apply standard fix", "metrics": { "label": "Active tickets", "value": 9, "max": 15, "unit": "tickets", "series": [4, 6, 7, 9, 8, 9] } },
36
+ { "id": "l1-wait", "kind": "card", "lane": "l1", "col": 3, "row": 2, "state": "at_risk", "title": "Await customer", "sla": 120, "metrics": { "label": "Hold queue", "value": 5, "max": 10, "unit": "tickets" } },
37
+ { "id": "l1-resolve", "kind": "card", "lane": "l1", "col": 4, "row": 2, "state": "on_time", "title": "Resolve ticket", "sla": 120, "metrics": { "label": "Resolved today", "value": 24, "unit": "tickets" } },
38
+ { "id": "l2-investigate", "kind": "card", "lane": "l2", "col": 2, "row": 5, "state": "on_time", "title": "Diagnose fault", "metrics": { "label": "Deep analysis", "value": 4, "max": 8, "unit": "tickets" } },
39
+ { "id": "l2-qa", "kind": "card", "lane": "l2", "col": 3, "row": 5, "state": "on_time", "title": "Verify fix" },
40
+ { "id": "l2-resolve", "kind": "card", "lane": "l2", "col": 4, "row": 5, "state": "on_time", "title": "Deploy code fix" },
41
+ { "id": "bill-review", "kind": "card", "lane": "billing", "col": 2, "row": 8, "state": "on_time", "title": "Review charges" },
42
+ { "id": "bill-audit", "kind": "card", "lane": "billing", "col": 3, "row": 8, "state": "on_time", "title": "Audit credit" },
43
+ { "id": "bill-adjust", "kind": "card", "lane": "billing", "col": 4, "row": 8, "state": "on_time", "title": "Issue refund" },
44
+ { "id": "ticket-close", "kind": "end", "lane": "billing", "col": 5, "row": 8, "state": "closed", "title": "Archive file" }
45
+ ],
46
+ "links": [
47
+ { "id": "l-submit-triage", "from": "cust-submit", "to": "l1-triage", "state": "on_time" },
48
+ { "id": "l-submit-notify", "from": "cust-submit", "to": "cust-notify", "state": "on_time" },
49
+ { "id": "l-triage-work", "from": "l1-triage", "to": "l1-work", "state": "on_time", "label": "Tier 1 issue" },
50
+ { "id": "l-triage-l2", "from": "l1-triage", "to": "l2-investigate", "state": "on_time", "label": "Escalate L2" },
51
+ { "id": "l-triage-bill", "from": "l1-triage", "to": "bill-review", "state": "on_time", "label": "Billing issue" },
52
+ { "id": "l-work-wait", "from": "l1-work", "to": "l1-wait", "state": "at_risk", "label": "Request details" },
53
+ { "id": "l-work-resolve", "from": "l1-work", "to": "l1-resolve", "state": "on_time", "label": "Fix ready" },
54
+ { "id": "l-wait-resolve", "from": "l1-wait", "to": "l1-resolve", "state": "on_time", "label": "Details given" },
55
+ { "id": "l-l2-qa", "from": "l2-investigate", "to": "l2-qa", "state": "on_time" },
56
+ { "id": "l-l2-back", "from": "l2-investigate", "to": "l1-triage", "state": "at_risk", "kind": "loop-back", "label": "Back to triage" },
57
+ { "id": "l-qa-resolve", "from": "l2-qa", "to": "l2-resolve", "state": "on_time" },
58
+ { "id": "l-bill-audit", "from": "bill-review", "to": "bill-audit", "state": "on_time" },
59
+ { "id": "l-audit-adjust", "from": "bill-audit", "to": "bill-adjust", "state": "on_time" },
60
+ { "id": "l-resolve-confirm", "from": "l1-resolve", "to": "cust-confirm", "state": "on_time" },
61
+ { "id": "l-l2resolve-confirm", "from": "l2-resolve", "to": "cust-confirm", "state": "on_time" },
62
+ { "id": "l-adjust-close", "from": "bill-adjust", "to": "ticket-close", "state": "on_time" },
63
+ { "id": "l-reopen", "from": "cust-confirm", "to": "l1-work", "state": "at_risk", "kind": "loop-back", "label": "Reopened" }
64
+ ],
65
+ "scenarios": [
66
+ {
67
+ "id": "happy",
68
+ "label": "Happy path",
69
+ "steps": [
70
+ { "link": "l-submit-triage", "at": 0, "caption": "Ticket submitted by customer" },
71
+ { "link": "l-triage-work", "at": 15, "caption": "Triaged as standard Tier 1 fix" },
72
+ { "link": "l-work-resolve", "at": 45, "caption": "Standard solution applied" },
73
+ { "link": "l-resolve-confirm", "at": 65, "caption": "Solution submitted for customer review" }
74
+ ]
75
+ },
76
+ {
77
+ "id": "escalated",
78
+ "label": "Escalated",
79
+ "steps": [
80
+ { "link": "l-submit-triage", "at": 0, "caption": "Customer submits urgent ticket" },
81
+ { "link": "l-triage-work", "at": 15, "caption": "Initial triage begins in Tier 1" },
82
+ { "link": "l-work-wait", "at": 40, "caption": "Additional system logs requested" },
83
+ { "link": "l-wait-resolve", "at": 150, "caption": "Customer responds after SLA threshold" },
84
+ { "link": "l-resolve-confirm", "at": 170, "caption": "Resolution sent to customer" },
85
+ { "link": "l-reopen", "at": 190, "caption": "Customer reports recurrence" },
86
+ { "link": "l-work-resolve", "at": 215, "caption": "Secondary remediation verified" },
87
+ { "link": "l-resolve-confirm", "at": 235, "caption": "Updated fix accepted" }
88
+ ]
89
+ }
90
+ ],
91
+ "inspector": {
92
+ "relations": [
93
+ { "id": "preceding", "label": "Came from", "dir": "up" },
94
+ { "id": "down", "label": "Hands off to", "dir": "down" },
95
+ { "id": "closing", "label": "Resolution path", "dir": "down", "filter": { "kind": "end" } }
96
+ ],
97
+ "actions": [
98
+ { "id": "sim-breach", "label": "Simulate: SLA breach", "dir": "down", "staleText": "Simulating SLA breach ripple across support queues" }
99
+ ]
100
+ },
101
+ "notes": [
102
+ { "anchor": "#n_l1-triage", "title": "Initial triage gateway", "text": "Tier 1 triage evaluates request urgency and routes tickets to appropriate support queues." },
103
+ { "anchor": "#n_l1-wait", "title": "Customer response SLA", "text": "Tickets placed on hold await customer details and carry a strict two-hour target window." },
104
+ { "anchor": "#n_cust-confirm", "title": "Resolution verification", "text": "The ticket is reopened if customer validation fails during final review." },
105
+ { "anchor": "#playbar", "title": "Scenario playback", "text": "Switch between happy path and escalated breach workflows to observe token movement." }
106
+ ]
107
+ }
@@ -19,9 +19,9 @@
19
19
  "labels": { "type": "object", "additionalProperties": { "type": "string" } }
20
20
  }
21
21
  },
22
- "layout": { "const": "columns-lanes" },
22
+ "layout": { "enum": ["columns-lanes", "free", "auto"] },
23
23
  "columns": {
24
- "type": "array", "minItems": 1,
24
+ "type": "array",
25
25
  "items": {
26
26
  "type": "object", "required": ["id", "title", "width"], "additionalProperties": false,
27
27
  "properties": {
@@ -31,7 +31,7 @@
31
31
  }
32
32
  },
33
33
  "lanes": {
34
- "type": "array", "minItems": 1,
34
+ "type": "array",
35
35
  "items": {
36
36
  "type": "object", "required": ["id", "title"], "additionalProperties": false,
37
37
  "properties": {
@@ -57,18 +57,25 @@
57
57
  "nodes": {
58
58
  "type": "array",
59
59
  "items": {
60
- "type": "object", "required": ["id", "kind", "lane", "col", "row", "state", "title"], "additionalProperties": false,
60
+ "type": "object", "required": ["id", "kind", "state", "title"], "additionalProperties": false,
61
61
  "properties": {
62
62
  "id": { "type": "string" },
63
63
  "kind": { "enum": ["card", "group", "chip", "gateway", "start", "end", "event", "placeholder", "note"] },
64
64
  "lane": { "type": "string" }, "col": { "type": "integer", "minimum": 0 }, "colSpan": { "type": "integer", "minimum": 1 },
65
- "row": { "type": "number" }, "state": { "type": "string" },
65
+ "row": { "type": "number" }, "x": { "type": "number" }, "y": { "type": "number" },
66
+ "w": { "type": "number", "exclusiveMinimum": 0 }, "h": { "type": "number", "exclusiveMinimum": 0 },
67
+ "state": { "type": "string" },
66
68
  "stateIn": { "type": "object", "additionalProperties": { "type": "string" } },
67
69
  "visibleIn": { "type": "array", "items": { "type": "string" } },
68
70
  "title": { "type": "string" }, "subtitle": { "type": "string" },
69
71
  "members": { "type": "array", "items": { "type": "string" } },
70
72
  "weight": { "type": "number", "minimum": 0 }, "ref": { "type": "string" },
71
- "upgrade": { "type": "string" }, "allowIsolated": { "type": "boolean" }
73
+ "upgrade": { "type": "string" }, "allowIsolated": { "type": "boolean" },
74
+ "contains": { "type": "array", "items": { "type": "string" } }, "collapsed": { "type": "boolean", "default": true, "description": "A group with contains starts collapsed unless collapsed is false." },
75
+ "sla": { "type": "number", "exclusiveMinimum": 0, "description": "Target service level agreement in seconds" },
76
+ "metrics": { "type": "object", "required": ["value"], "additionalProperties": false,
77
+ "properties": { "value": { "type": "number" }, "max": { "type": ["number", "null"], "description": "Upper bound for the badge fill bar; null means there is no maximum" }, "unit": { "type": "string" }, "label": { "type": "string" },
78
+ "series": { "type": "array", "maxItems": 60, "items": { "type": "number" } } } }
72
79
  }
73
80
  }
74
81
  },
@@ -79,7 +86,8 @@
79
86
  "properties": {
80
87
  "id": { "type": "string" }, "from": { "type": "string" }, "to": { "type": "string" }, "state": { "type": "string" },
81
88
  "stateIn": { "type": "object", "additionalProperties": { "type": "string" } },
82
- "visibleIn": { "type": "array", "items": { "type": "string" } }, "label": { "type": "string" }
89
+ "visibleIn": { "type": "array", "items": { "type": "string" } }, "label": { "type": "string" },
90
+ "kind": { "enum": ["forward", "loop-back"] }
83
91
  }
84
92
  }
85
93
  },
@@ -112,6 +120,10 @@
112
120
  }
113
121
  }
114
122
  },
123
+ "scenarios": { "type": "array", "items": { "type": "object", "required": ["id", "label", "steps"], "additionalProperties": false,
124
+ "properties": { "id": { "type": "string" }, "label": { "type": "string" },
125
+ "steps": { "type": "array", "minItems": 1, "items": { "type": "object", "required": ["link"], "additionalProperties": false,
126
+ "properties": { "link": { "type": "string" }, "at": { "type": "number", "minimum": 0, "description": "Relative elapsed offset from scenario start in seconds" }, "dur": { "type": "number", "exclusiveMinimum": 0, "description": "Token travel animation duration in milliseconds (defaults to 900)" }, "caption": { "type": "string" } } } } } } },
115
127
  "notes": {
116
128
  "type": "array",
117
129
  "items": {
@@ -119,5 +131,12 @@
119
131
  "properties": { "anchor": { "type": "string" }, "title": { "type": "string" }, "text": { "type": "string" } }
120
132
  }
121
133
  }
122
- }
134
+ },
135
+ "allOf": [
136
+ { "if": { "properties": { "layout": { "const": "columns-lanes" } } },
137
+ "then": { "properties": { "columns": { "minItems": 1 }, "lanes": { "minItems": 1 },
138
+ "nodes": { "items": { "required": ["lane", "col", "row"] } } } } },
139
+ { "if": { "properties": { "layout": { "const": "free" } } },
140
+ "then": { "properties": { "lanes": { "minItems": 1 }, "nodes": { "items": { "required": ["lane", "x", "y"] } } } } }
141
+ ]
123
142
  }
@@ -0,0 +1,82 @@
1
+ import React, { StrictMode } from 'react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { render } from '@testing-library/react';
4
+ import type { Model, NoteDef } from '../src/model.ts';
5
+
6
+ const lifecycle = vi.hoisted(() => ({
7
+ alive: 0, updates: [] as Model[], mounts: 0, listenerCount: 0,
8
+ notes: [] as (NoteDef[] | undefined)[],
9
+ onSelect: undefined as ((id: string | null) => void) | undefined,
10
+ }));
11
+ vi.mock('../templates/flow-studio.lib.mjs', () => ({
12
+ SHELL_BODY_HTML: '<div id="viewport"></div>',
13
+ mount: (_root: ParentNode, _model: Model, options: { notes?: NoteDef[]; onSelect?: (id: string | null) => void }) => {
14
+ lifecycle.mounts++;
15
+ lifecycle.alive++;
16
+ lifecycle.listenerCount++;
17
+ lifecycle.notes.push(options.notes);
18
+ lifecycle.onSelect = options.onSelect;
19
+ return {
20
+ update: (model: Model) => { lifecycle.updates.push(model); return { ok: true, issues: [] }; },
21
+ destroy: () => { lifecycle.alive--; lifecycle.listenerCount--; },
22
+ };
23
+ },
24
+ }));
25
+
26
+ import { FlowStudioView } from './FlowStudioView.tsx';
27
+
28
+ const model = { version: 1, columns: [], lanes: [], nodes: [], links: [], states: {} } as unknown as Model;
29
+
30
+ describe('FlowStudioView', () => {
31
+ it('keeps one instance and listener set through StrictMode, updates once, and cleans up', () => {
32
+ const globals = window as Window & { flowStudioMount?: unknown };
33
+ const previousMount = globals.flowStudioMount;
34
+ lifecycle.alive = 0;
35
+ lifecycle.mounts = 0;
36
+ lifecycle.listenerCount = 0;
37
+ lifecycle.updates = [];
38
+ lifecycle.notes = [];
39
+ const select = vi.fn();
40
+ const rendered = render(<StrictMode><FlowStudioView model={model} onSelect={select} /></StrictMode>);
41
+ expect(lifecycle.mounts).toBe(2);
42
+ expect(lifecycle.alive).toBe(1);
43
+ expect(lifecycle.listenerCount).toBe(1);
44
+ expect(globals.flowStudioMount).toBeTypeOf('function');
45
+ lifecycle.onSelect?.('sample');
46
+ expect(select).toHaveBeenCalledWith('sample');
47
+ rendered.rerender(<StrictMode><FlowStudioView model={structuredClone(model)} onSelect={select} /></StrictMode>);
48
+ expect(lifecycle.updates).toHaveLength(0);
49
+ const changed = { ...model, meta: { title: 'Updated', help: '', asOf: '', source: '' } } as Model;
50
+ rendered.rerender(<StrictMode><FlowStudioView model={changed} onSelect={select} /></StrictMode>);
51
+ expect(lifecycle.updates).toEqual([changed]);
52
+ expect(lifecycle.alive).toBe(1);
53
+ rendered.unmount();
54
+ expect(lifecycle.alive).toBe(0);
55
+ expect(lifecycle.listenerCount).toBe(0);
56
+ expect(globals.flowStudioMount).toBe(previousMount);
57
+ });
58
+
59
+ it('passes notes at mount and ignores later notes-only changes', () => {
60
+ lifecycle.updates = [];
61
+ lifecycle.notes = [];
62
+ const first = [{ anchor: 'a', title: 'First', text: 'Mount-time note' }] satisfies NoteDef[];
63
+ const second = [{ anchor: 'b', title: 'Second', text: 'Changed note' }] satisfies NoteDef[];
64
+ const rendered = render(<FlowStudioView model={model} notes={first} />);
65
+ expect(lifecycle.notes).toEqual([first]);
66
+ rendered.rerender(<FlowStudioView model={model} notes={second} />);
67
+ expect(lifecycle.updates).toHaveLength(0);
68
+ expect(lifecycle.notes).toEqual([first]);
69
+ });
70
+
71
+ it('keeps window.flowStudioMount while any instance is mounted', () => {
72
+ const globals = window as Window & { flowStudioMount?: unknown };
73
+ const previous = globals.flowStudioMount;
74
+ const first = render(<FlowStudioView model={model} />);
75
+ const second = render(<FlowStudioView model={model} />);
76
+ expect(globals.flowStudioMount).toBeTypeOf('function');
77
+ first.unmount();
78
+ expect(globals.flowStudioMount).toBeTypeOf('function');
79
+ second.unmount();
80
+ expect(globals.flowStudioMount).toBe(previous);
81
+ });
82
+ });
@@ -0,0 +1,67 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { mount, SHELL_BODY_HTML, type FlowStudio } from "../templates/flow-studio.lib.mjs";
3
+ import type { Model, NoteDef } from "../src/model.ts";
4
+
5
+ export interface FlowStudioViewProps {
6
+ model: Model;
7
+ /** Read once at mount; later changes to this prop are ignored. For notes that change with the data, put them on `model.notes` — `update` forwards them. */
8
+ notes?: NoteDef[];
9
+ onSelect?: (id: string | null) => void;
10
+ className?: string;
11
+ }
12
+
13
+ // The engine publishes its mount helper on a shared global. Count mounted instances so the first
14
+ // mount claims the global and the last unmount releases it; unmounting one of several views must
15
+ // not clear the global while the others still run.
16
+ let mountedViews = 0;
17
+ let previousGlobalMount: typeof mount | undefined;
18
+
19
+ /** Mounts the finished engine in React. The shell stays outside React's child tree. */
20
+ export function FlowStudioView({ model, notes, onSelect, className }: FlowStudioViewProps) {
21
+ const root = useRef<HTMLDivElement>(null);
22
+ const api = useRef<FlowStudio | null>(null);
23
+ const currentModel = useRef(model);
24
+ const currentNotes = useRef(notes);
25
+ const currentSelect = useRef(onSelect);
26
+ const lastModel = useRef<string | null>(null);
27
+ currentModel.current = model;
28
+ currentNotes.current = notes;
29
+ currentSelect.current = onSelect;
30
+ const modelJson = JSON.stringify(model);
31
+
32
+ useEffect(() => {
33
+ const container = root.current;
34
+ if (!container) return;
35
+ container.innerHTML = SHELL_BODY_HTML;
36
+ const instance = mount(container, currentModel.current, {
37
+ notes: currentNotes.current,
38
+ onSelect: id => currentSelect.current?.(id),
39
+ });
40
+ if (mountedViews === 0) {
41
+ previousGlobalMount = window.flowStudioMount;
42
+ window.flowStudioMount = mount;
43
+ }
44
+ mountedViews++;
45
+ api.current = instance;
46
+ lastModel.current = JSON.stringify(currentModel.current);
47
+ return () => {
48
+ instance.destroy();
49
+ if (api.current === instance) api.current = null;
50
+ lastModel.current = null;
51
+ mountedViews--;
52
+ if (mountedViews === 0) {
53
+ if (previousGlobalMount) window.flowStudioMount = previousGlobalMount;
54
+ else delete window.flowStudioMount;
55
+ previousGlobalMount = undefined;
56
+ }
57
+ };
58
+ }, []);
59
+
60
+ useEffect(() => {
61
+ if (!api.current || lastModel.current === modelJson) return;
62
+ api.current.update(model);
63
+ lastModel.current = modelJson;
64
+ }, [modelJson, model]);
65
+
66
+ return <div ref={root} className={className} />;
67
+ }
@@ -0,0 +1,37 @@
1
+ # Flow studio in a React app
2
+
3
+ ## Choose a route
4
+
5
+ Use `FlowStudioView` when the page may use the engine's own look. It includes the complete canvas, inspector, notes, playback, search, and live update behavior. Use `native/` when the page must use the app's own components and design system. The native files are small examples built from the same pure source modules.
6
+
7
+ ## Install
8
+
9
+ Copy `react/`, `src/`, `templates/flow-studio.lib.mjs`, `templates/flow-studio.lib.d.mts`, and `templates/flow-studio.css` into the same relative layout in your app. Install React 18 or later. The native route needs only the pure modules under `src/`; the fast route uses the bundled library. Import `flow-studio.css` once for the fast route. Import `react/native/native.css` for the native example only.
10
+
11
+ ```tsx
12
+ import { FlowStudioView, FlowLoading, FlowEmpty, FlowError, type Model } from './react/index.ts';
13
+ import './templates/flow-studio.css';
14
+
15
+ type State = { kind: 'loading' } | { kind: 'ready'; model: Model; refreshError?: string } | { kind: 'error'; message: string };
16
+ function FlowPage({ state, onRetry, onSelect }: { state: State; onRetry: () => void; onSelect: (id: string | null) => void }) {
17
+ if (state.kind === 'loading') return <FlowLoading />;
18
+ if (state.kind === 'error') return <FlowError message={state.message} onRetry={onRetry} />;
19
+ if (state.model.nodes.length === 0) return <FlowEmpty model={state.model} />;
20
+ return <>{state.refreshError && <FlowError message={state.refreshError} onRetry={onRetry} />}
21
+ <FlowStudioView model={state.model} onSelect={onSelect} /></>;
22
+ }
23
+ ```
24
+
25
+ `FlowStudioView` takes a required `model` and optional `notes`, `onSelect`, and `className`. `notes` is read once at mount; later changes to the prop are ignored. For notes that change with the data, put them on the model (`model.notes`) — `update` forwards them. `onSelect` receives a stable node id or `null` when selection clears. The component mounts the engine once, sends changed models to `update`, and destroys its listeners on unmount. React StrictMode's development mount cycle is supported. Several views may be mounted at once; the engine's global mount helper stays available until the last one unmounts.
26
+
27
+ ## Live data contract
28
+
29
+ Map source rows to `Model` with stable node and link ids. Derive each id from a durable business key. Never derive ids from array positions or current sort order. Keep missing metrics typed as missing. Do not turn them into zero. Show `FlowLoading` before the first model, `FlowEmpty` when the model has no nodes, and `FlowError` for a request failure.
30
+
31
+ An update that fails model validation keeps the last good graph and shows a message in the engine's `#banner`. Keep the last good model in app state when a data request fails. The engine preserves the view and surviving selection on a valid model update.
32
+
33
+ ## Verify
34
+
35
+ From this skill folder, run `node scripts/react-proof.mjs`. It creates a disposable Vite app in the system temp folder, installs its test tools, runs Vitest and TypeScript, builds the app, starts a preview server, and runs the browser checks. It removes the app afterward. On Windows it uses the installed Edge browser; elsewhere it lets npm download Playwright's own browser. Your app can run the same browser checks with `node scripts/check.mjs http://localhost:5173/ --skip hostile`. Omit `--skip hostile` when you also provide a hostile test page.
36
+
37
+ For step by step integration and failure handling, see [`implement-in-app.md`](../references/implement-in-app.md).
@@ -0,0 +1,3 @@
1
+ export { FlowStudioView, type FlowStudioViewProps } from "./FlowStudioView.tsx";
2
+ export { FlowLoading, FlowEmpty, FlowError } from "./states.tsx";
3
+ export type { Model, NoteDef } from "../src/model.ts";
@@ -0,0 +1,58 @@
1
+ import type { ReactNode } from "react";
2
+ import type { Model, NodeDef } from "../../src/model.ts";
3
+ import { defaultView } from "../../src/rules.ts";
4
+ import { elbow, loopBack } from "../../src/routing.ts";
5
+ import { useFlowGeometry } from "./useFlowGeometry.ts";
6
+ import { useTrace } from "./useTrace.ts";
7
+ import { useViewport } from "./useViewport.ts";
8
+ import { Inspector } from "./Inspector.tsx";
9
+ import "./native.css";
10
+
11
+ export interface FlowCanvasProps {
12
+ model: Model;
13
+ renderNode: (node: NodeDef, context: { selected: boolean; connected: boolean }) => ReactNode;
14
+ mode?: string;
15
+ className?: string;
16
+ }
17
+
18
+ /** Small native example. Apps can replace every element while retaining the hooks. */
19
+ export function FlowCanvas({ model: input, renderNode, mode, className }: FlowCanvasProps) {
20
+ const activeMode = mode ?? defaultView(input);
21
+ const trace = useTrace(input, activeMode);
22
+ const { model, geometry } = useFlowGeometry(input, { x: 0, y: 0, k: 1 }, trace.collapsed, activeMode);
23
+ const viewport = useViewport(geometry.bounds);
24
+ const selected = model.nodes.find(node => node.id === trace.selected) ?? null;
25
+ const path = (link: typeof model.links[number]) => {
26
+ const from = geometry.nodes[link.from], to = geometry.nodes[link.to];
27
+ if (!from || !to) return "";
28
+ if (link.kind === "loop-back") return loopBack(from.x + from.w / 2, from.y + from.h, to.x + to.w / 2, to.y + to.h, geometry.loops[link.id]?.channelY ?? Math.max(from.y + from.h, to.y + to.h) + 20);
29
+ return elbow(from.x + from.w, from.y + from.h / 2, to.x, to.y + to.h / 2, (from.x + from.w + to.x) / 2);
30
+ };
31
+ return <section id="workspace" className={`flow-native ${className ?? ""}`}>
32
+ <div id="tools" className="flow-native-tools">
33
+ <button type="button" onClick={() => viewport.fit()} aria-label="Fit graph">Fit</button>
34
+ <button type="button" onClick={() => viewport.zoomAboutCenter(1.2)} aria-label="Zoom in">+</button>
35
+ <button type="button" onClick={() => viewport.zoomAboutCenter(1 / 1.2)} aria-label="Zoom out">−</button>
36
+ <span id="zoomLabel">{Math.round(viewport.view.k * 100)}%</span>
37
+ </div>
38
+ <div id="cols" className="flow-native-cols" aria-label="Columns">{model.columns.map((column, i) => <span key={column.id} style={{ left: (geometry.xs[i] ?? 0) * viewport.view.k + viewport.view.x, width: column.width * viewport.view.k }}>{column.title}</span>)}</div>
39
+ <div id="viewport" className="flow-native-viewport" {...viewport.bind}>
40
+ <div id="world" className="flow-native-world" style={{ width: geometry.world.w, height: geometry.world.h, transform: `translate(${viewport.view.x}px, ${viewport.view.y}px) scale(${viewport.view.k})` }}>
41
+ {model.lanes.map(lane => { const rect = geometry.lanes[lane.id]; return rect && <div key={lane.id} className="flow-native-lane" style={{ left: rect.x, top: rect.y, width: rect.w, height: rect.h }}><strong>{lane.title}</strong></div>; })}
42
+ <svg id="edges" className="flow-native-edges" width={geometry.world.w} height={geometry.world.h} aria-label="Links"><g id="edgeLayer">{trace.links.map(link => <path key={link.id} data-link-id={link.id} d={path(link)} fill="none" stroke="currentColor" strokeWidth="2" />)}</g></svg>
43
+ {model.nodes.filter(node => trace.visible.has(node.id)).map(node => {
44
+ const rect = geometry.nodes[node.id]; if (!rect) return null;
45
+ return <div key={node.id} id={`n_${node.id}`} className="flow-native-node" style={{ left: rect.x, top: rect.y, width: rect.w, height: rect.h }}>
46
+ <button type="button" className="flow-native-node-main" aria-label={node.title} aria-pressed={trace.selected === node.id} data-connected={trace.connected.has(node.id)}
47
+ onMouseEnter={() => trace.setHover(node.id)} onMouseLeave={() => trace.setHover(null)} onFocus={() => trace.setHover(node.id)} onBlur={() => trace.setHover(null)}
48
+ onClick={() => trace.setSelected(node.id)}>{renderNode(node, { selected: trace.selected === node.id, connected: trace.connected.has(node.id) })}</button>
49
+ {node.kind === "group" && node.contains?.length ? <button type="button" className="flow-native-toggle" aria-label={`${trace.collapsed.has(node.id) ? "Expand" : "Collapse"} ${node.title}`}
50
+ onClick={() => trace.toggleGroup(node.id)}>{trace.collapsed.has(node.id) ? "+" : "−"}</button> : null}
51
+ </div>;
52
+ })}
53
+ </div>
54
+ </div>
55
+ <div id="live" className="flow-native-live" aria-live="polite">{selected ? `${selected.title} selected` : ""}</div>
56
+ <Inspector model={model} node={selected} mode={activeMode} upstream={trace.upstream} downstream={trace.downstream} onClose={() => trace.setSelected(null)} onSelect={trace.setSelected} />
57
+ </section>;
58
+ }
@@ -0,0 +1,27 @@
1
+ import type { Model, NodeDef } from "../../src/model.ts";
2
+ import { bucketOf, nodeStateId } from "../../src/rules.ts";
3
+
4
+ export function Inspector({ model, node, mode, upstream, downstream, onClose, onSelect }: {
5
+ model: Model; node: NodeDef | null; mode: string; upstream: Set<string>; downstream: Set<string>;
6
+ onClose: () => void; onSelect?: (id: string) => void;
7
+ }) {
8
+ if (!node) return null;
9
+ const state = model.states[nodeStateId(model, mode, node)];
10
+ return <aside id="inspector" className="insp open" role="dialog" aria-label={`${node.title} details`} aria-modal="false">
11
+ <div className="insp-h"><div><h2>{node.title}</h2><small>{state?.word ?? "Unknown state"}</small></div><button type="button" onClick={onClose} aria-label="Close inspector">×</button></div>
12
+ <div className="insp-b">
13
+ {node.subtitle && <p>{node.subtitle}</p>}
14
+ <p>State: {bucketOf(model, nodeStateId(model, mode, node))}</p>
15
+ <div className="counts">
16
+ <button type="button" className="cbtn" aria-label={`${Math.max(0, upstream.size - 1)} upstream nodes`}>Upstream {Math.max(0, upstream.size - 1)}</button>
17
+ <button type="button" className="cbtn" aria-label={`${Math.max(0, downstream.size - 1)} downstream nodes`}>Downstream {Math.max(0, downstream.size - 1)}</button>
18
+ </div>
19
+ <div className="drill" aria-label="Connected nodes">
20
+ {[...new Set([...upstream, ...downstream])].filter(id => id !== node.id).map(id => {
21
+ const related = model.nodes.find(item => item.id === id);
22
+ return related && <button type="button" key={id} onClick={() => onSelect?.(id)}>{related.title}</button>;
23
+ })}
24
+ </div>
25
+ </div>
26
+ </aside>;
27
+ }
@@ -0,0 +1,6 @@
1
+ export { useViewport } from "./useViewport.ts";
2
+ export { useFlowGeometry } from "./useFlowGeometry.ts";
3
+ export { useTrace } from "./useTrace.ts";
4
+ export { FlowCanvas } from "./FlowCanvas.tsx";
5
+ export { Inspector } from "./Inspector.tsx";
6
+ export type { FlowCanvasProps } from "./FlowCanvas.tsx";