stream_weaver 0.3.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.
Files changed (381) hide show
  1. checksums.yaml +7 -0
  2. data/.beads/.gitignore +77 -0
  3. data/.beads/README.md +81 -0
  4. data/.beads/config.yaml +54 -0
  5. data/.beads/metadata.json +7 -0
  6. data/.rspec +3 -0
  7. data/AGENTS.md +91 -0
  8. data/CHANGELOG.md +294 -0
  9. data/CLAUDE.md +67 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +565 -0
  12. data/Rakefile +13 -0
  13. data/assets/streamweaver-hero.jpg +0 -0
  14. data/bench/baselines/ledger.rb +58 -0
  15. data/bench/baselines/warroom.rb +51 -0
  16. data/bench/fixtures/ledger.rb +76 -0
  17. data/bench/fixtures/warroom.rb +60 -0
  18. data/bench/results/6daf8b8.md +102 -0
  19. data/bench/results/c8d749b.md +102 -0
  20. data/bench/results/eb06ab9.md +102 -0
  21. data/bench/run.rb +151 -0
  22. data/bench/support.rb +84 -0
  23. data/copy_this/dto_run_sample_run_7631.html +220 -0
  24. data/docs/SERVICE_MODE.md +302 -0
  25. data/docs/architecture/how_streamweaver_works.md +772 -0
  26. data/docs/blog/2026-01-16-streamweaver-introduction.md +515 -0
  27. data/docs/bug-2026-08-31-canvas-bridge-socket-collision.md +83 -0
  28. data/docs/canvas-ipc-session-summary.md +127 -0
  29. data/docs/canvas-panel-workflow.md +172 -0
  30. data/docs/canvas-read.md +147 -0
  31. data/docs/canvas-roadmap.md +275 -0
  32. data/docs/case-studies/2026-09-03-didx-canvas-read-shelf.md +147 -0
  33. data/docs/claude-code-companion-skill-spec.md +567 -0
  34. data/docs/components_reference.md +1223 -0
  35. data/docs/crud-patterns.md +265 -0
  36. data/docs/endpoints.md +122 -0
  37. data/docs/for_llms.md +1 -0
  38. data/docs/form-patterns.md +165 -0
  39. data/docs/frontend-only.md +169 -0
  40. data/docs/html-artifact-audit.md +238 -0
  41. data/docs/ideas/2025-01-01-as-a-service.md +203 -0
  42. data/docs/ideas/2026-01-16-charm-tui-exploration.md +542 -0
  43. data/docs/ideas/2026-08-27-static-doc-shelf-export.md +153 -0
  44. data/docs/ideas/dhh-review-future-refactors.md +142 -0
  45. data/docs/opal-jamstack.md +47 -0
  46. data/docs/opal-npm-direction.md +184 -0
  47. data/docs/opal-spike-findings.md +194 -0
  48. data/docs/plans/2025-12-16-form-blocks-design.md +124 -0
  49. data/docs/plans/2025-12-30-examples-browser-design.md +152 -0
  50. data/docs/plans/2026-01-02-charts-design.md +288 -0
  51. data/docs/plans/2026-01-10-canvas-ipc-design.md +247 -0
  52. data/docs/plans/2026-01-19-cabinet-control-components.md +176 -0
  53. data/docs/plans/2026-01-26-gem-release-and-panel.md +140 -0
  54. data/docs/plans/canvas-claude-project-design.md +238 -0
  55. data/docs/plans/canvas-doc-location-and-discovery.md +32 -0
  56. data/docs/plans/org-doc-preview-surfaces.md +293 -0
  57. data/docs/plans/shadcn-polish-plan.md +417 -0
  58. data/docs/porting-artifacts.md +252 -0
  59. data/docs/reference/agent-skills-comparison.md +369 -0
  60. data/docs/reference/travel-state-prd.artifact.html +780 -0
  61. data/docs/research/2026-08-17-hotwire-alike-landscape.md +268 -0
  62. data/docs/research/2026-08-17-hotwire-concept-map.md +404 -0
  63. data/docs/research/2026-08-22-lazy-fragments-trigger-decision.md +246 -0
  64. data/docs/research/2026-08-22-learnhotwire-syllabus-coverage.md +239 -0
  65. data/docs/research/frontend-only-matrix.md +278 -0
  66. data/docs/research/streamweaver-way-spike-findings.md +529 -0
  67. data/docs/resource-dsl.md +342 -0
  68. data/docs/routing.md +302 -0
  69. data/docs/ruby-ui-comparison.md +319 -0
  70. data/docs/shared-dsl-fragments.md +356 -0
  71. data/docs/streamweaver-for-ai-agents.md +401 -0
  72. data/docs/streamweaver-frontend-vision.md +123 -0
  73. data/docs/streamweaver_canvas/glimmer_initial_final_layer.rb +77 -0
  74. data/docs/streamweaver_canvas/save_example.rb +9 -0
  75. data/docs/templates.md +213 -0
  76. data/docs/testing.md +155 -0
  77. data/docs/theming-hooks.md +281 -0
  78. data/docs/tutorials/the-streamweaver-way.md +882 -0
  79. data/docs/university/capability-inventory.md +137 -0
  80. data/docs/university/dependency-survey.md +114 -0
  81. data/docs/university/design-spec.md +217 -0
  82. data/docs/university/mockups/code-block-doc-theme-dark.png +0 -0
  83. data/docs/university/mockups/code-block-doc-theme-light.png +0 -0
  84. data/docs/university/mockups/course-list-dark.png +0 -0
  85. data/docs/university/mockups/course-list-light.png +0 -0
  86. data/docs/university/mockups/course_canvas_mockup.rb +741 -0
  87. data/docs/university/mockups/step-screen-dark.png +0 -0
  88. data/docs/university/mockups/step-screen-light.png +0 -0
  89. data/docs/university/roadmap.md +72 -0
  90. data/docs/university/send-to-coworker.md +77 -0
  91. data/docs/university/worker-session-mining.md +109 -0
  92. data/docs/visual/browser-rendering-spike.md +173 -0
  93. data/docs/visual/builder-visual-plan-analysis.md +378 -0
  94. data/docs/visual/sw-plan-format-exploration.md +511 -0
  95. data/docs/visual/sw-plan-rendering-deep-dive.md +453 -0
  96. data/docs/visual-skills/PROGRESS.md +60 -0
  97. data/docs/visual-skills/SESSION-CONTEXT.md +207 -0
  98. data/docs/visual-skills/analysis/components.md +317 -0
  99. data/docs/visual-skills/analysis/overlap.md +360 -0
  100. data/docs/visual-skills/analysis/pi-design-deck.md +1102 -0
  101. data/docs/visual-skills/analysis/streamweaver-inventory.md +93 -0
  102. data/docs/visual-skills/analysis/unified-specs.feature +1003 -0
  103. data/docs/visual-skills/analysis/visual-explainer.md +964 -0
  104. data/docs/visual-skills/blog/blog-series-outline.md +39 -0
  105. data/docs/visual-skills/blog/token-efficiency.md +73 -0
  106. data/docs/visual-skills/design/architecture.md +1725 -0
  107. data/docs/visual-skills/design/codex-review.md +340 -0
  108. data/docs/visual-skills/design/dhh-review.md +209 -0
  109. data/docs/visual-skills/design/evolution.md +100 -0
  110. data/docs/visual-skills/design/gemini-review.md +55 -0
  111. data/docs/visual-skills/design/review-synthesis.md +149 -0
  112. data/docs/visual-skills/implementation/STATE.md +22 -0
  113. data/docs/visual-skills/implementation/plan.md +347 -0
  114. data/docs/visual-skills/implementation/spike-findings.md +237 -0
  115. data/docs/visual-skills/implementation/tasks.md +702 -0
  116. data/docs/visual-skills/lessons-learned/process.md +145 -0
  117. data/examples/README.md +96 -0
  118. data/examples/advanced/all_components.rb +177 -0
  119. data/examples/advanced/examples_browser.rb +394 -0
  120. data/examples/advanced/teachables_browser.rb +261 -0
  121. data/examples/advanced/theme_tweaker.rb +366 -0
  122. data/examples/advanced/tutorial.rb +1528 -0
  123. data/examples/agentic/agentic_form.rb +30 -0
  124. data/examples/agentic/agentic_form_autoclose.rb +30 -0
  125. data/examples/agentic/cultivation_tracker.rb +69 -0
  126. data/examples/basic/hello_world.rb +22 -0
  127. data/examples/basic/opal_tabs_table.rb +44 -0
  128. data/examples/basic/todo_list.rb +39 -0
  129. data/examples/button_loading_test.rb +36 -0
  130. data/examples/canvas/mermaid_canvas_demo.sh +151 -0
  131. data/examples/charts/bar_chart_demo.rb +57 -0
  132. data/examples/charts/line_chart_demo.rb +73 -0
  133. data/examples/charts/pie_area_demo.rb +87 -0
  134. data/examples/charts/stacked_bar_chart_demo.rb +69 -0
  135. data/examples/claude_code/README.md +85 -0
  136. data/examples/claude_code/codebreaker/.claude/commands/infiltrate.md +632 -0
  137. data/examples/claude_code/codebreaker/.claude/settings.json +11 -0
  138. data/examples/claude_code/codebreaker/.claude/settings.local.json +17 -0
  139. data/examples/claude_code/codebreaker/.claude/skills/infiltrate.md +0 -0
  140. data/examples/claude_code/codebreaker/README.md +213 -0
  141. data/examples/claude_code/tutorial/.claude/commands/learn.md +453 -0
  142. data/examples/claude_code/tutorial/README.md +102 -0
  143. data/examples/claude_code/verification_flow/.claude/commands/verify.md +171 -0
  144. data/examples/claude_code/verification_flow/README.md +126 -0
  145. data/examples/components/annotated_code_demo.rb +54 -0
  146. data/examples/components/callout_demo.rb +33 -0
  147. data/examples/components/checkbox_group_demo.rb +46 -0
  148. data/examples/components/design_review.css +468 -0
  149. data/examples/components/design_review_demo.rb +44 -0
  150. data/examples/components/design_review_dsl.rb +300 -0
  151. data/examples/components/diff_block_demo.rb +112 -0
  152. data/examples/components/events_demo.rb +184 -0
  153. data/examples/components/form_demo.rb +62 -0
  154. data/examples/components/lesson_demo.rb +53 -0
  155. data/examples/components/markdown_demo.rb +110 -0
  156. data/examples/components/mermaid_demo.rb +166 -0
  157. data/examples/components/pareto_set.rb +64 -0
  158. data/examples/components/prd_demo.rb +21 -0
  159. data/examples/components/prd_dsl.rb +385 -0
  160. data/examples/components/quiz_demo.rb +47 -0
  161. data/examples/components/run_viewer_demo.rb +100 -0
  162. data/examples/components/score_and_collapsible_demo.rb +82 -0
  163. data/examples/components/select_stale_value_smoke_test.rb +39 -0
  164. data/examples/components/table_demo.rb +247 -0
  165. data/examples/components/timer_with_state_demo.rb +58 -0
  166. data/examples/components/todo_due_dates.rb +63 -0
  167. data/examples/components/uat_gaps_demo.rb +112 -0
  168. data/examples/dashboard/feed_simulator.rb +60 -0
  169. data/examples/dashboard/live_dashboard.rb +80 -0
  170. data/examples/dashboard_components.rb +108 -0
  171. data/examples/deferred_fragments_demo.rb +62 -0
  172. data/examples/generate_more_spike/README.md +63 -0
  173. data/examples/generate_more_spike/app.rb +441 -0
  174. data/examples/git_health.sh +401 -0
  175. data/examples/layout/layout_components_demo.rb +174 -0
  176. data/examples/layout/modal_demo.rb +215 -0
  177. data/examples/layout/navigation_demo.rb +227 -0
  178. data/examples/layout/route_tabs_demo.rb +52 -0
  179. data/examples/layout/routing_demo.rb +52 -0
  180. data/examples/layout/scroll_box_demo.rb +154 -0
  181. data/examples/lazy_fragments_demo.rb +151 -0
  182. data/examples/my_todos/README.md +29 -0
  183. data/examples/my_todos/my_todos.rb +312 -0
  184. data/examples/my_todos/store.rb +86 -0
  185. data/examples/opal/reactive_demo.rb +62 -0
  186. data/examples/opal/scenarios/s1_counter.rb +15 -0
  187. data/examples/opal/scenarios/s2_search_filter.rb +17 -0
  188. data/examples/opal/scenarios/s3_sibling_tabs.rb +25 -0
  189. data/examples/opal/scenarios/s4_shopping_cart.rb +34 -0
  190. data/examples/opal/scenarios/s5_watch.rb +39 -0
  191. data/examples/opal/scenarios/s6_on_start.rb +20 -0
  192. data/examples/opal/scenarios/s7_wizard.rb +33 -0
  193. data/examples/opal/scenarios/s8_loan_calculator.rb +21 -0
  194. data/examples/opal/scenarios/s9_dashboard.rb +27 -0
  195. data/examples/operations_dashboard_demo.rb +167 -0
  196. data/examples/panel_demo.sh +447 -0
  197. data/examples/parity/assets/tyrion_slice.css +86 -0
  198. data/examples/parity/rivet_people_slice.rb +182 -0
  199. data/examples/parity/tyrion_components.css +777 -0
  200. data/examples/parity/tyrion_warroom_components.rb +414 -0
  201. data/examples/parity/tyrion_warroom_slice.rb +549 -0
  202. data/examples/puma_dev/README.md +124 -0
  203. data/examples/puma_dev/config.ru +34 -0
  204. data/examples/puma_dev/standalone_app.rb +34 -0
  205. data/examples/scaffolding/blog.rb +49 -0
  206. data/examples/scaffolding/utf_lite.rb +105 -0
  207. data/examples/styling/feedback_demo.rb +229 -0
  208. data/examples/styling/style_showcase.rb +172 -0
  209. data/examples/styling/theme_demo.rb +508 -0
  210. data/examples/timer_health_checker.rb +111 -0
  211. data/examples/timer_showcase.rb +131 -0
  212. data/examples/tutorials/render_markdown.rb +112 -0
  213. data/examples/tutorials/streamweaver_way_tutorial.rb +44 -0
  214. data/examples/tutorials/tutorial_content.rb +1082 -0
  215. data/examples/visual_skills/design_deck_demo.rb +314 -0
  216. data/examples/visual_skills/explainer_demo.rb +499 -0
  217. data/exe/streamweaver +9 -0
  218. data/gsd/ROADMAP-1.0.md +167 -0
  219. data/gsd/STATE.md +13 -0
  220. data/gsd/research/market-positioning-research.md +139 -0
  221. data/gsd/research/production-patterns-research.md +270 -0
  222. data/gsd/research/repo-audit-1.0.md +274 -0
  223. data/lib/stream_weaver/action_token.rb +58 -0
  224. data/lib/stream_weaver/adapter/alpinejs.rb +8249 -0
  225. data/lib/stream_weaver/adapter/base.rb +591 -0
  226. data/lib/stream_weaver/adapter/opal.rb +334 -0
  227. data/lib/stream_weaver/adapter/static.rb +1118 -0
  228. data/lib/stream_weaver/admin.rb +176 -0
  229. data/lib/stream_weaver/app.rb +1821 -0
  230. data/lib/stream_weaver/assets/js/sw-copy.js +50 -0
  231. data/lib/stream_weaver/assets/js/sw-heredoc-rewrite.js +68 -0
  232. data/lib/stream_weaver/assets/js/sw-keyboard.js +165 -0
  233. data/lib/stream_weaver/assets/js/sw-mermaid-zoom.js +621 -0
  234. data/lib/stream_weaver/assets/js/sw-route-tabs.js +53 -0
  235. data/lib/stream_weaver/assets/js/sw-sidebar-toc.js +116 -0
  236. data/lib/stream_weaver/assets/js/sw-slide-nav.js +103 -0
  237. data/lib/stream_weaver/canvas/bridge.rb +239 -0
  238. data/lib/stream_weaver/canvas/bridge_server.rb +646 -0
  239. data/lib/stream_weaver/canvas/client.rb +298 -0
  240. data/lib/stream_weaver/canvas/doc_roots.rb +233 -0
  241. data/lib/stream_weaver/canvas/doc_store.rb +252 -0
  242. data/lib/stream_weaver/canvas/gist_publisher.rb +264 -0
  243. data/lib/stream_weaver/canvas/gist_save_handler.rb +89 -0
  244. data/lib/stream_weaver/canvas/gist_store.rb +135 -0
  245. data/lib/stream_weaver/canvas/helpers.rb +109 -0
  246. data/lib/stream_weaver/canvas/history.rb +90 -0
  247. data/lib/stream_weaver/canvas/protocol.rb +86 -0
  248. data/lib/stream_weaver/canvas/reader.rb +853 -0
  249. data/lib/stream_weaver/canvas/save_doc_widget.rb +457 -0
  250. data/lib/stream_weaver/canvas/scroll_top_hint.rb +21 -0
  251. data/lib/stream_weaver/canvas/session.rb +132 -0
  252. data/lib/stream_weaver/cli.rb +3235 -0
  253. data/lib/stream_weaver/component_assets.rb +70 -0
  254. data/lib/stream_weaver/component_registry.rb +67 -0
  255. data/lib/stream_weaver/component_renderer.rb +48 -0
  256. data/lib/stream_weaver/components/annotated_code.rb +53 -0
  257. data/lib/stream_weaver/components/api_endpoint.rb +42 -0
  258. data/lib/stream_weaver/components/callout.rb +59 -0
  259. data/lib/stream_weaver/components/chart.rb +84 -0
  260. data/lib/stream_weaver/components/code_block.rb +77 -0
  261. data/lib/stream_weaver/components/comparison.rb +39 -0
  262. data/lib/stream_weaver/components/decision.rb +38 -0
  263. data/lib/stream_weaver/components/deck/close_overlay.rb +84 -0
  264. data/lib/stream_weaver/components/deck/confirmation_bar.rb +60 -0
  265. data/lib/stream_weaver/components/deck/deck_option.rb +61 -0
  266. data/lib/stream_weaver/components/deck/deck_slide.rb +77 -0
  267. data/lib/stream_weaver/components/deck/deck_state.rb +469 -0
  268. data/lib/stream_weaver/components/deck/deck_summary.rb +73 -0
  269. data/lib/stream_weaver/components/deck/design_deck.rb +60 -0
  270. data/lib/stream_weaver/components/deck/generate_more_controls.rb +84 -0
  271. data/lib/stream_weaver/components/deck/model_selector.rb +84 -0
  272. data/lib/stream_weaver/components/deck/skeleton_placeholder.rb +36 -0
  273. data/lib/stream_weaver/components/diff_block.rb +125 -0
  274. data/lib/stream_weaver/components/doc_header.rb +57 -0
  275. data/lib/stream_weaver/components/image_block.rb +70 -0
  276. data/lib/stream_weaver/components/implementation_map.rb +33 -0
  277. data/lib/stream_weaver/components/keyboard_shortcuts.rb +94 -0
  278. data/lib/stream_weaver/components/kpi_dashboard.rb +78 -0
  279. data/lib/stream_weaver/components/mermaid.rb +79 -0
  280. data/lib/stream_weaver/components/pipeline.rb +63 -0
  281. data/lib/stream_weaver/components/sidebar_toc.rb +46 -0
  282. data/lib/stream_weaver/components/slide_container.rb +141 -0
  283. data/lib/stream_weaver/components/timeline_event.rb +58 -0
  284. data/lib/stream_weaver/components/wireframe.rb +29 -0
  285. data/lib/stream_weaver/components/wireframe_block.rb +34 -0
  286. data/lib/stream_weaver/components.rb +2711 -0
  287. data/lib/stream_weaver/css.rb +230 -0
  288. data/lib/stream_weaver/dev_fallback_overlay.rb +61 -0
  289. data/lib/stream_weaver/display_dsl.rb +1004 -0
  290. data/lib/stream_weaver/export/html_exporter.rb +478 -0
  291. data/lib/stream_weaver/feed.rb +34 -0
  292. data/lib/stream_weaver/feed_builder.rb +31 -0
  293. data/lib/stream_weaver/fonts.rb +33 -0
  294. data/lib/stream_weaver/interaction_runner.rb +487 -0
  295. data/lib/stream_weaver/iterm.rb +460 -0
  296. data/lib/stream_weaver/layout_registry.rb +92 -0
  297. data/lib/stream_weaver/opal/bridge.rb +52 -0
  298. data/lib/stream_weaver/opal/builder.rb +193 -0
  299. data/lib/stream_weaver/opal/env.rb +29 -0
  300. data/lib/stream_weaver/opal/reactive_state.rb +75 -0
  301. data/lib/stream_weaver/opal/regexp_anchor_patch.rb +62 -0
  302. data/lib/stream_weaver/opal/renderer.rb +77 -0
  303. data/lib/stream_weaver/opal/runtime.rb +250 -0
  304. data/lib/stream_weaver/opal/shell.rb +145 -0
  305. data/lib/stream_weaver/opal/string_bridge.rb +50 -0
  306. data/lib/stream_weaver/opal/stubs/diff.min.js +1 -0
  307. data/lib/stream_weaver/opal/stubs/digest.rb +15 -0
  308. data/lib/stream_weaver/opal/stubs/marked.umd.js +79 -0
  309. data/lib/stream_weaver/opal/stubs/md5.rb +3 -0
  310. data/lib/stream_weaver/opal/stubs/morphdom.min.js +775 -0
  311. data/lib/stream_weaver/opal/stubs/prism-tomorrow.min.css +1 -0
  312. data/lib/stream_weaver/opal/stubs/prism.min.js +1967 -0
  313. data/lib/stream_weaver/opal_entry.rb +136 -0
  314. data/lib/stream_weaver/org/inline.rb +95 -0
  315. data/lib/stream_weaver/org/reader.rb +561 -0
  316. data/lib/stream_weaver/org/recording_context.rb +86 -0
  317. data/lib/stream_weaver/org/source_splitter.rb +63 -0
  318. data/lib/stream_weaver/org/writer.rb +300 -0
  319. data/lib/stream_weaver/page_shell.rb +530 -0
  320. data/lib/stream_weaver/portfile.rb +79 -0
  321. data/lib/stream_weaver/pushable.rb +42 -0
  322. data/lib/stream_weaver/resource/default_views.rb +111 -0
  323. data/lib/stream_weaver/resource/field_input.rb +25 -0
  324. data/lib/stream_weaver/resource/state_keys.rb +16 -0
  325. data/lib/stream_weaver/resource/store.rb +18 -0
  326. data/lib/stream_weaver/resource.rb +104 -0
  327. data/lib/stream_weaver/server.rb +1471 -0
  328. data/lib/stream_weaver/service.rb +1240 -0
  329. data/lib/stream_weaver/service_client.rb +104 -0
  330. data/lib/stream_weaver/session_store.rb +186 -0
  331. data/lib/stream_weaver/skills/streamweaver-canvas-safe/SKILL.md +66 -0
  332. data/lib/stream_weaver/skills/streamweaver-canvas-safe/examples/canvas-safe-showcase.rb +98 -0
  333. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/actions-and-buttons.md +47 -0
  334. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/charts-and-diagrams.md +38 -0
  335. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/deck.md +32 -0
  336. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/inputs-and-forms.md +65 -0
  337. data/lib/stream_weaver/skills/streamweaver-canvas-safe/references/tabs-and-navigation.md +53 -0
  338. data/lib/stream_weaver/skills/streamweaver-doc-builder/SKILL.md +298 -0
  339. data/lib/stream_weaver/skills/streamweaver-visual-companion/SKILL.md +130 -0
  340. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.css +468 -0
  341. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example.rb +39 -0
  342. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/design-review-example_dsl.rb +300 -0
  343. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example.rb +24 -0
  344. data/lib/stream_weaver/skills/streamweaver-visual-companion/examples/doc-parity-example_dsl.rb +385 -0
  345. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/checkpoints-and-forms.md +28 -0
  346. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/cleanup-and-panel.md +44 -0
  347. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/example-gallery.md +8 -0
  348. data/lib/stream_weaver/skills/streamweaver-visual-companion/references/persistence.md +58 -0
  349. data/lib/stream_weaver/skills/streamweaver-way/SKILL.md +396 -0
  350. data/lib/stream_weaver/skills/visual-plan/SKILL.md +201 -0
  351. data/lib/stream_weaver/skills/visual-recap/SKILL.md +244 -0
  352. data/lib/stream_weaver/streamer.rb +63 -0
  353. data/lib/stream_weaver/templates/choices.rb +149 -0
  354. data/lib/stream_weaver/templates/code.rb +157 -0
  355. data/lib/stream_weaver/templates/confirm.rb +110 -0
  356. data/lib/stream_weaver/templates/diff.rb +195 -0
  357. data/lib/stream_weaver/templates/info.rb +142 -0
  358. data/lib/stream_weaver/templates/table.rb +169 -0
  359. data/lib/stream_weaver/templates/wizard.rb +271 -0
  360. data/lib/stream_weaver/theme/auto_mode.rb +116 -0
  361. data/lib/stream_weaver/theme/presets.rb +555 -0
  362. data/lib/stream_weaver/theme.rb +639 -0
  363. data/lib/stream_weaver/university/canvas.rb +924 -0
  364. data/lib/stream_weaver/university/course.rb +699 -0
  365. data/lib/stream_weaver/university/demos/counter.rb +33 -0
  366. data/lib/stream_weaver/university/demos/dashboard.rb +147 -0
  367. data/lib/stream_weaver/university/demos/decision_form.rb +143 -0
  368. data/lib/stream_weaver/university/demos.rb +50 -0
  369. data/lib/stream_weaver/university/listener.rb +467 -0
  370. data/lib/stream_weaver/university/progress.rb +201 -0
  371. data/lib/stream_weaver/university/runner.rb +138 -0
  372. data/lib/stream_weaver/university/scripts/growing_doc.rb +609 -0
  373. data/lib/stream_weaver/university/scripts/growing_doc_state.rb +91 -0
  374. data/lib/stream_weaver/utils.rb +31 -0
  375. data/lib/stream_weaver/version.rb +5 -0
  376. data/lib/stream_weaver/views/canvas/reader_layout.erb +800 -0
  377. data/lib/stream_weaver/views.rb +3940 -0
  378. data/lib/stream_weaver.rb +122 -0
  379. data/llms.txt +1269 -0
  380. data/sig/stream_weaver.rbs +4 -0
  381. metadata +598 -0
@@ -0,0 +1,621 @@
1
+ /**
2
+ * StreamWeaver Mermaid Zoom/Pan Engine
3
+ *
4
+ * Provides zoom (Ctrl+scroll, button controls) and pan (click-drag)
5
+ * for Mermaid diagram containers marked with sw-mermaid--zoom.
6
+ *
7
+ * Theme-aware: reads data-sw-theme from <html> to set Mermaid
8
+ * themeVariables dynamically. Listens for theme changes via
9
+ * MutationObserver on data-sw-theme.
10
+ *
11
+ * Usage: loaded lazily by the AlpineJS adapter only when a mermaid
12
+ * component is rendered. Initializes via swMermaidInit().
13
+ */
14
+ (function () {
15
+ 'use strict';
16
+
17
+ // =========================================================
18
+ // Constants
19
+ // =========================================================
20
+ var ZOOM_MIN = 0.25;
21
+ var ZOOM_MAX = 4.0;
22
+ var ZOOM_STEP = 0.15;
23
+ var CDN_BASE = 'https://cdn.jsdelivr.net/npm';
24
+ var MERMAID_VERSION = '11';
25
+ var ELK_VERSION = '0.1';
26
+
27
+ // =========================================================
28
+ // Theme mapping: reads data-sw-theme to pick Mermaid theme
29
+ // =========================================================
30
+ function getMermaidTheme() {
31
+ var attr = document.documentElement.getAttribute('data-sw-theme');
32
+ return attr === 'dark' ? 'dark' : 'default';
33
+ }
34
+
35
+ // Mermaid's color parser only understands hex/rgb/hsl/named colors, but StreamWeaver's
36
+ // theme tokens use modern CSS color functions (oklch(), color-mix()) that it chokes on
37
+ // (throws synchronously in mermaid.initialize(), aborting the render). Resolve any CSS
38
+ // color value through the browser's own computed style so Mermaid always receives rgb().
39
+ // getComputedStyle no longer downgrades modern color syntax to rgb() in current
40
+ // browsers (it echoes oklch()/lab() back as-is), so resolving through a probe element's
41
+ // computed style doesn't help. Rasterizing to a 1x1 canvas and reading the pixel back
42
+ // always yields true 8-bit sRGB values regardless of the input color space.
43
+ function toRgbColor(value) {
44
+ if (!value) return null;
45
+ if (!toRgbColor._ctx) {
46
+ var canvas = document.createElement('canvas');
47
+ canvas.width = 1;
48
+ canvas.height = 1;
49
+ toRgbColor._ctx = canvas.getContext('2d', { willReadFrequently: true });
50
+ }
51
+ var ctx = toRgbColor._ctx;
52
+ // Canvas2D silently ignores an unparseable fillStyle assignment rather than throwing
53
+ // (per spec) -- detect rejection by checking whether the sentinel value survived.
54
+ var sentinel = '#010203';
55
+ ctx.fillStyle = sentinel;
56
+ ctx.fillStyle = value;
57
+ if (ctx.fillStyle === sentinel) return null;
58
+ ctx.clearRect(0, 0, 1, 1);
59
+ ctx.fillRect(0, 0, 1, 1);
60
+ var data = ctx.getImageData(0, 0, 1, 1).data;
61
+ var alpha = data[3] / 255;
62
+ return alpha < 1
63
+ ? 'rgba(' + data[0] + ',' + data[1] + ',' + data[2] + ',' + alpha.toFixed(3) + ')'
64
+ : 'rgb(' + data[0] + ',' + data[1] + ',' + data[2] + ')';
65
+ }
66
+
67
+ function getThemeVariables() {
68
+ var style = getComputedStyle(document.documentElement);
69
+ var isDark = document.documentElement.getAttribute('data-sw-theme') === 'dark';
70
+
71
+ return {
72
+ primaryColor: toRgbColor(style.getPropertyValue('--sw-node-a').trim()) || (isDark ? '#f97316' : '#c2410c'),
73
+ secondaryColor: toRgbColor(style.getPropertyValue('--sw-node-b').trim()) || (isDark ? '#2dd4bf' : '#0d9488'),
74
+ tertiaryColor: toRgbColor(style.getPropertyValue('--sw-node-c').trim()) || (isDark ? '#a78bfa' : '#7c3aed'),
75
+ primaryTextColor: toRgbColor(style.getPropertyValue('--sw-text').trim()) || (isDark ? '#f5f5f5' : '#111111'),
76
+ // --sw-border is tuned for subtle card hairlines, not diagram connectors -- at
77
+ // that contrast, arrows/edges are nearly invisible against --sw-surface. Use the
78
+ // muted-text token instead: dark enough to read as a line, still lighter than
79
+ // primary text so it doesn't compete with node labels.
80
+ lineColor: toRgbColor(style.getPropertyValue('--sw-text-dim').trim()) || (isDark ? 'rgba(255,255,255,0.4)' : '#6b6860'),
81
+ background: toRgbColor(style.getPropertyValue('--sw-surface').trim()) || (isDark ? '#333' : '#ffffff'),
82
+ mainBkg: toRgbColor(style.getPropertyValue('--sw-surface').trim()) || (isDark ? '#333' : '#ffffff'),
83
+ fontFamily: style.getPropertyValue('--sw-font-body').trim() || 'system-ui, sans-serif'
84
+ };
85
+ }
86
+
87
+ // =========================================================
88
+ // Mermaid CDN Loader (lazy, ESM)
89
+ // =========================================================
90
+ var mermaidLoaded = false;
91
+ var mermaidLoadPromise = null;
92
+
93
+ function loadMermaid(needsElk) {
94
+ if (mermaidLoadPromise) return mermaidLoadPromise;
95
+
96
+ // An offline export (stream_weaver-dnq) inlines mermaid's classic
97
+ // build as a plain <script>, which sets this global directly -- no
98
+ // CDN request needed (or, on the CSP that flag exists for, possible
99
+ // at all). ELK still isn't covered by that inline (see the exporter's
100
+ // comment on mermaid_offline_script_tag), so an ELK diagram still
101
+ // falls through to the CDN import below either way.
102
+ if (globalThis.mermaid && !needsElk) {
103
+ // Assign synchronously, not inside a .then(): the value is already
104
+ // available, and deferring mermaidLoaded=true to a microtask leaves
105
+ // a window where reRenderAll() (an early theme-change mutation, say)
106
+ // would see mermaidLoaded still false and silently no-op.
107
+ window.__swMermaid = globalThis.mermaid;
108
+ mermaidLoaded = true;
109
+ mermaidLoadPromise = Promise.resolve(globalThis.mermaid);
110
+ return mermaidLoadPromise;
111
+ }
112
+
113
+ mermaidLoadPromise = new Promise(function (resolve, reject) {
114
+ // Dynamic ESM import for Mermaid 11
115
+ var mermaidUrl = CDN_BASE + '/mermaid@' + MERMAID_VERSION + '/dist/mermaid.esm.min.mjs';
116
+ var elkUrl = needsElk
117
+ ? CDN_BASE + '/@mermaid-js/layout-elk@' + ELK_VERSION + '/dist/mermaid-layout-elk.esm.min.mjs'
118
+ : null;
119
+
120
+ var imports = [import(mermaidUrl)];
121
+ if (elkUrl) imports.push(import(elkUrl));
122
+
123
+ Promise.all(imports)
124
+ .then(function (modules) {
125
+ var mermaid = modules[0].default || modules[0];
126
+ if (modules[1]) {
127
+ // Register ELK layout
128
+ var elk = modules[1].default || modules[1];
129
+ if (mermaid.registerLayoutLoaders) {
130
+ mermaid.registerLayoutLoaders(elk);
131
+ }
132
+ }
133
+ window.__swMermaid = mermaid;
134
+ mermaidLoaded = true;
135
+ resolve(mermaid);
136
+ })
137
+ .catch(function (err) {
138
+ // Don't cache a rejected promise forever -- a transient CDN
139
+ // failure (a flaky connection, not the CSP case this file's
140
+ // offline path exists for) would otherwise be unrecoverable for
141
+ // the rest of the page's life, since every future call just
142
+ // returns this same settled promise back at line 94.
143
+ mermaidLoadPromise = null;
144
+ reject(err);
145
+ });
146
+ });
147
+
148
+ return mermaidLoadPromise;
149
+ }
150
+
151
+ // =========================================================
152
+ // Render a single diagram
153
+ // =========================================================
154
+ function renderDiagram(container, mermaid, customVars) {
155
+ // Guard: skip if already rendered or a render is in progress.
156
+ // swMermaidInit() can be called concurrently (self-init on
157
+ // DOMContentLoaded, htmx:afterSwap, the canvas's live-update poll
158
+ // handler), and mermaid.render() is async — without this guard, two
159
+ // renders race on the same container ID, which corrupts mermaid's
160
+ // internal state.
161
+ if (container.hasAttribute('data-sw-mermaid-done')) return;
162
+ if (container.hasAttribute('data-sw-mermaid-rendering')) return;
163
+ container.setAttribute('data-sw-mermaid-rendering', 'true');
164
+
165
+ var diagramEl = container.querySelector('.sw-mermaid__diagram');
166
+ if (!diagramEl) { container.removeAttribute('data-sw-mermaid-rendering'); return; }
167
+
168
+ var code = diagramEl.getAttribute('data-sw-mermaid-code');
169
+ if (!code) { container.removeAttribute('data-sw-mermaid-rendering'); return; }
170
+
171
+ var id = container.id || ('sw-mermaid-' + Math.random().toString(36).slice(2, 9));
172
+
173
+ var themeVars = getThemeVariables();
174
+
175
+ // When custom per-block vars are provided, use Mermaid's %%{init}%% directive
176
+ // so each diagram gets its own theme independently of the global initialize() config.
177
+ // theme: 'base' lets themeVariables fully control node colors.
178
+ // Auto-derive mainBkg from primaryColor: in Mermaid's base theme, flowchart node fills
179
+ // use mainBkg, not primaryColor. Users shouldn't need to know this distinction.
180
+ if (customVars) {
181
+ var vars = Object.assign({}, customVars);
182
+ if (vars.primaryColor && !vars.mainBkg) {
183
+ vars.mainBkg = vars.primaryColor;
184
+ }
185
+ var initDirective = '%%{init: ' + JSON.stringify({ theme: 'base', themeVariables: vars }) + '}%%\n';
186
+ code = initDirective + code;
187
+ }
188
+
189
+ mermaid.initialize({
190
+ startOnLoad: false,
191
+ theme: getMermaidTheme(),
192
+ themeVariables: themeVars,
193
+ securityLevel: 'loose',
194
+ fontFamily: themeVars.fontFamily
195
+ });
196
+
197
+ mermaid.render(id + '-svg', code).then(function (result) {
198
+ diagramEl.innerHTML = result.svg;
199
+ container.removeAttribute('data-sw-mermaid-rendering');
200
+ container.setAttribute('data-sw-mermaid-done', 'true');
201
+ // Apply zoom if enabled
202
+ if (container.classList.contains('sw-mermaid--zoom')) {
203
+ initZoomPan(container, diagramEl);
204
+ }
205
+ // Expand is always wired, regardless of zoom: true (stream_weaver-yjv).
206
+ initExpand(container, diagramEl);
207
+ }).catch(function (err) {
208
+ container.removeAttribute('data-sw-mermaid-rendering');
209
+ diagramEl.innerHTML = '<pre class="sw-mermaid__error" style="color:var(--sw-error,#dc2626);padding:1rem;">'
210
+ + escapeHtml(err.message || String(err)) + '</pre>';
211
+ });
212
+ }
213
+
214
+ function escapeHtml(str) {
215
+ var div = document.createElement('div');
216
+ div.appendChild(document.createTextNode(str));
217
+ return div.innerHTML;
218
+ }
219
+
220
+ // =========================================================
221
+ // Zoom/Pan Engine
222
+ //
223
+ // Shared by the in-place zoom: true container and the fullscreen
224
+ // overlay below -- createZoomPan(hitArea, target) does the actual work;
225
+ // initZoomPan wraps it with the +/-/reset button wiring the in-place
226
+ // container has and the overlay doesn't. `signal`, when passed, ties
227
+ // every listener registered here to one AbortController so a single
228
+ // `.abort()` tears all of them down -- needed by the overlay, which is
229
+ // created and destroyed repeatedly over a page's lifetime; not needed
230
+ // by the in-place container, which lives as long as the diagram does.
231
+ // =========================================================
232
+ function createZoomPan(hitArea, target, signal) {
233
+ var state = { scale: 1, panX: 0, panY: 0, dragging: false, startX: 0, startY: 0 };
234
+ var opts = signal ? { signal: signal } : undefined;
235
+
236
+ function applyTransform() {
237
+ target.style.transformOrigin = '0 0';
238
+ target.style.transform =
239
+ 'translate(' + state.panX + 'px, ' + state.panY + 'px) scale(' + state.scale + ')';
240
+ }
241
+
242
+ // Ctrl+Scroll zoom, toward the cursor
243
+ hitArea.addEventListener('wheel', function (e) {
244
+ if (!e.ctrlKey && !e.metaKey) return; // Plain scroll pans natively otherwise.
245
+ e.preventDefault();
246
+
247
+ var delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
248
+ var newScale = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, state.scale + delta));
249
+
250
+ var rect = hitArea.getBoundingClientRect();
251
+ var cursorX = e.clientX - rect.left;
252
+ var cursorY = e.clientY - rect.top;
253
+
254
+ var scaleRatio = newScale / state.scale;
255
+ state.panX = cursorX - (cursorX - state.panX) * scaleRatio;
256
+ state.panY = cursorY - (cursorY - state.panY) * scaleRatio;
257
+ state.scale = newScale;
258
+ applyTransform();
259
+ }, Object.assign({ passive: false }, opts));
260
+
261
+ // Click-drag pan
262
+ hitArea.addEventListener('mousedown', function (e) {
263
+ if (e.button !== 0) return;
264
+ state.dragging = true;
265
+ state.startX = e.clientX - state.panX;
266
+ state.startY = e.clientY - state.panY;
267
+ hitArea.style.cursor = 'grabbing';
268
+ e.preventDefault();
269
+ }, opts);
270
+
271
+ document.addEventListener('mousemove', function (e) {
272
+ if (!state.dragging) return;
273
+ state.panX = e.clientX - state.startX;
274
+ state.panY = e.clientY - state.startY;
275
+ applyTransform();
276
+ }, opts);
277
+
278
+ document.addEventListener('mouseup', function () {
279
+ if (state.dragging) {
280
+ state.dragging = false;
281
+ hitArea.style.cursor = 'grab';
282
+ }
283
+ }, opts);
284
+
285
+ hitArea.style.cursor = 'grab';
286
+ return { state: state, applyTransform: applyTransform };
287
+ }
288
+
289
+ function initZoomPan(container, diagramEl) {
290
+ // reRenderAll() (a theme toggle) clears data-sw-mermaid-done and
291
+ // re-runs renderDiagram against this SAME container without ever
292
+ // recreating it -- without this guard, every toggle stacks another
293
+ // full set of wheel/mousedown/document-mousemove/document-mouseup
294
+ // listeners (the last two unremovable, with no AbortController) plus
295
+ // another click handler per button, each closing over its own
296
+ // independent zoom state. Same pattern as initExpand's
297
+ // data-sw-expand-wired guard just below.
298
+ if (container.hasAttribute('data-sw-zoom-wired')) return;
299
+ container.setAttribute('data-sw-zoom-wired', 'true');
300
+
301
+ var engine = createZoomPan(container, diagramEl);
302
+ var state = engine.state;
303
+ var applyTransform = engine.applyTransform;
304
+
305
+ // Wire up zoom control buttons
306
+ var controls = container.querySelector('.sw-mermaid__controls');
307
+ if (controls) {
308
+ var zoomInBtn = controls.querySelector('[data-sw-zoom="in"]');
309
+ var zoomOutBtn = controls.querySelector('[data-sw-zoom="out"]');
310
+ var resetBtn = controls.querySelector('[data-sw-zoom="reset"]');
311
+
312
+ if (zoomInBtn) {
313
+ zoomInBtn.addEventListener('click', function () {
314
+ state.scale = Math.min(ZOOM_MAX, state.scale + ZOOM_STEP);
315
+ applyTransform();
316
+ });
317
+ }
318
+ if (zoomOutBtn) {
319
+ zoomOutBtn.addEventListener('click', function () {
320
+ state.scale = Math.max(ZOOM_MIN, state.scale - ZOOM_STEP);
321
+ applyTransform();
322
+ });
323
+ }
324
+ if (resetBtn) {
325
+ resetBtn.addEventListener('click', function () {
326
+ state.scale = 1;
327
+ state.panX = 0;
328
+ state.panY = 0;
329
+ applyTransform();
330
+ });
331
+ }
332
+ // The buttons sit inside `container`, which also has its own
333
+ // mousedown-to-pan listener -- without this, clicking a button
334
+ // also starts a drag of the diagram underneath it.
335
+ controls.addEventListener('mousedown', function (e) { e.stopPropagation(); });
336
+ }
337
+ }
338
+
339
+ // =========================================================
340
+ // Fullscreen expand (stream_weaver-yjv)
341
+ //
342
+ // A wide diagram's fixed-px labels shrink proportionally to fit
343
+ // whatever doc column width it renders in -- the constraint is the
344
+ // container, not the SVG, so no amount of in-place pan/zoom fixes it.
345
+ // Expand re-hosts the already-rendered SVG (never moved, always cloned,
346
+ // so the in-place diagram and its own zoom/pan state if any are
347
+ // untouched) in a modal <dialog>, which has no width to shrink against.
348
+ //
349
+ // <dialog>.showModal() rather than a hand-rolled overlay div: it gives
350
+ // us the top layer (no z-index arms race with toasts/nav elsewhere in
351
+ // this codebase), Escape-to-close, focus moved in and restored to the
352
+ // expand button on close, and an inert background -- all of which a
353
+ // hand-rolled div would otherwise have to reimplement, badly.
354
+ //
355
+ // Pure client-side, no server route: works identically in the live
356
+ // canvas, the reader, and a static export (including an --offline one,
357
+ // since this file is what's already inlined there -- no new
358
+ // dependency).
359
+ // =========================================================
360
+ var fullscreen = null; // { dialog, abort } -- one at a time.
361
+ var fullscreenSeq = 0;
362
+
363
+ function initExpand(container, diagramEl) {
364
+ var btn = container.querySelector('.sw-mermaid__controls [data-sw-zoom="expand"]');
365
+ // reRenderAll() (theme switch) clears data-sw-mermaid-done and re-runs
366
+ // renderDiagram against this SAME button -- it replaces diagramEl's
367
+ // innerHTML but never recreates .sw-mermaid__controls. Without this
368
+ // guard, every theme toggle stacks another click handler on it, and
369
+ // each stacked handler opens (and immediately re-closes) its own
370
+ // overlay + AbortController on a single click.
371
+ if (!btn || btn.hasAttribute('data-sw-expand-wired')) return;
372
+ btn.setAttribute('data-sw-expand-wired', 'true');
373
+
374
+ btn.addEventListener('click', function () {
375
+ openFullscreen(diagramEl);
376
+ });
377
+ }
378
+
379
+ // Mermaid namespaces every internal id off the root <svg>'s own id --
380
+ // arrowhead markers, gradients, and the selectors inside the SVG's own
381
+ // inlined <style> all reference it via url(#...) or a bare selector. A
382
+ // plain cloneNode(true) reproduces that whole id set while the original
383
+ // is still in the document, and url(#...) resolves to the FIRST element
384
+ // in document order with a given id -- the original's, not the clone's.
385
+ // It renders correctly today, which is the trap: it breaks the moment
386
+ // the original is re-rendered or swapped out from under an open
387
+ // overlay (a theme toggle, a canvas push), silently dropping every
388
+ // arrowhead in the exact "complex diagram" view this feature exists
389
+ // for. Re-namespacing with one string swap (everything is prefixed off
390
+ // the same root id) fixes ids, references, and inline style selectors
391
+ // together in a single pass.
392
+ function cloneSvgWrapper(svg) {
393
+ var markup = svg.outerHTML;
394
+ if (svg.id) markup = markup.split(svg.id).join(svg.id + '-fs' + (++fullscreenSeq));
395
+
396
+ var wrapper = document.createElement('div');
397
+ wrapper.className = 'sw-mermaid-fullscreen-overlay__svg-wrapper';
398
+ wrapper.innerHTML = markup;
399
+
400
+ var svgEl = wrapper.firstElementChild;
401
+ // Mermaid's root <svg> carries width="100%" (an HTML attribute) plus
402
+ // an inline style="max-width: Npx" -- fine in the original in-place
403
+ // container, a normal block-level div with a concrete width to
404
+ // resolve 100% against. Here it isn't: `content` (this wrapper's
405
+ // parent) is a flex item with no explicit width, so "100%" has
406
+ // nothing solid to resolve against and collapses toward zero --
407
+ // verified live: the dialog rendered the diagram at a few px wide
408
+ // instead of natural size, the opposite of this feature's entire
409
+ // purpose. Fixed by setting concrete pixel dimensions read straight
410
+ // from the SVG's own viewBox -- which IS "natural size" -- so nothing
411
+ // here depends on any ancestor's computed width at all.
412
+ //
413
+ // Set via setAttribute, not svgEl.style.*: an inline-style CSP
414
+ // restriction (style-src-attr) was the first suspected cause of this
415
+ // still going blank on SharePoint after the width="100%" fix below,
416
+ // but that theory didn't hold up -- see the expand button's own icon
417
+ // in adapter/alpinejs.rb for what actually turned out to be wrong
418
+ // (markup sanitization, not CSS at all). That doesn't make this wrong
419
+ // to keep, though: this SVG is inserted by JS at runtime, not present
420
+ // in the page's original markup, so it was never at risk from a
421
+ // sanitizer that only processes uploaded HTML -- but setAttribute is
422
+ // no more complex than .style and has strictly fewer ways to be
423
+ // blocked by *some* CSP variant, so there's no reason to revert it.
424
+ // width=/height= are plain SVG geometry attributes regardless.
425
+ // removeAttribute clears mermaid's own inline max-width, the only
426
+ // thing that attribute contains.
427
+ var viewBox = svgEl.getAttribute('viewBox');
428
+ var dims = viewBox && viewBox.trim().split(/\s+/).map(Number);
429
+ if (dims && dims.length === 4 && dims[2] > 0 && dims[3] > 0) {
430
+ svgEl.setAttribute('width', dims[2]);
431
+ svgEl.setAttribute('height', dims[3]);
432
+ }
433
+ svgEl.removeAttribute('style');
434
+ return wrapper;
435
+ }
436
+
437
+ function openFullscreen(diagramEl) {
438
+ var svg = diagramEl.querySelector('svg');
439
+ if (!svg) return; // Not rendered yet, or the render errored -- nothing to show.
440
+ closeFullscreen(); // Only one overlay at a time.
441
+
442
+ // One AbortController per open: every listener below is registered
443
+ // against its signal, so closing is a single .abort() call and
444
+ // nothing can be left orphaned on `document` across repeated
445
+ // open/close cycles.
446
+ var abort = new AbortController();
447
+ var signal = abort.signal;
448
+
449
+ var dialog = document.createElement('dialog');
450
+ dialog.className = 'sw-mermaid-fullscreen-overlay';
451
+ dialog.setAttribute('aria-label', 'Diagram, expanded');
452
+
453
+ var content = document.createElement('div');
454
+ content.className = 'sw-mermaid-fullscreen-overlay__content';
455
+ var wrapper = cloneSvgWrapper(svg);
456
+ content.appendChild(wrapper);
457
+
458
+ var closeBtn = document.createElement('button');
459
+ closeBtn.type = 'button';
460
+ closeBtn.className = 'sw-mermaid-fullscreen-overlay__close';
461
+ closeBtn.setAttribute('aria-label', 'Close');
462
+ closeBtn.textContent = '✕';
463
+
464
+ var hint = document.createElement('div');
465
+ hint.className = 'sw-mermaid-fullscreen-overlay__hint';
466
+ hint.textContent = 'Scroll or drag to pan · Ctrl+scroll to zoom · Esc to close';
467
+
468
+ dialog.appendChild(content);
469
+ dialog.appendChild(closeBtn);
470
+ dialog.appendChild(hint);
471
+ document.body.appendChild(dialog);
472
+
473
+ // Same engine as zoom: true -- including its cursor-anchored zoom.
474
+ createZoomPan(content, wrapper, signal);
475
+
476
+ closeBtn.addEventListener('click', closeFullscreen, { signal: signal });
477
+ // mousedown, not click: a click's target is the nearest common
478
+ // ancestor of its mousedown and mouseup targets, so releasing a pan
479
+ // drag over the backdrop (the common case for a diagram wider than
480
+ // the viewport -- the exact case this feature is for) would report
481
+ // the dialog as the target and close the overlay mid-drag. mousedown
482
+ // fires on the actual element under the cursor, so a drag that
483
+ // begins on `content` can never trigger this.
484
+ dialog.addEventListener('mousedown', function (e) {
485
+ if (e.target === dialog) closeFullscreen();
486
+ }, { signal: signal });
487
+ // showModal()'s native Escape handling calls dialog.close(), which
488
+ // fires 'close' -- route that through the same cleanup so Escape and
489
+ // the close button and the backdrop click all converge on one path.
490
+ dialog.addEventListener('close', closeFullscreen, { signal: signal });
491
+
492
+ fullscreen = { dialog: dialog, abort: abort };
493
+ document.documentElement.classList.add('sw-mermaid-fullscreen-open');
494
+ dialog.showModal();
495
+ }
496
+
497
+ // Unconditional class removal: if the dialog ever leaves by a route
498
+ // other than this function (a canvas push replacing body content, an
499
+ // htmx swap), the scroll lock must not survive it -- an unscrollable
500
+ // page with no visible overlay to explain it is unrecoverable short of
501
+ // a reload.
502
+ function closeFullscreen() {
503
+ document.documentElement.classList.remove('sw-mermaid-fullscreen-open');
504
+ if (!fullscreen) return;
505
+
506
+ var closing = fullscreen;
507
+ fullscreen = null; // Before .close(), so the 'close' listener's re-entry no-ops.
508
+ closing.abort.abort(); // Every listener from this open, gone in one call.
509
+ if (closing.dialog.open) closing.dialog.close();
510
+ closing.dialog.remove();
511
+ }
512
+
513
+ // =========================================================
514
+ // Theme change observer: re-render on theme switch
515
+ // =========================================================
516
+ var themeObserverAttached = false;
517
+
518
+ function observeThemeChanges() {
519
+ // Called from swMermaidInit()'s loadMermaid().then() -- reached again
520
+ // on DOMContentLoaded, every htmx:afterSwap, and the canvas's
521
+ // live-update poll handler, so without this guard every one of those
522
+ // adds another MutationObserver on <html>, each firing reRenderAll()
523
+ // independently on the next theme toggle.
524
+ if (themeObserverAttached) return;
525
+ themeObserverAttached = true;
526
+
527
+ var observer = new MutationObserver(function (mutations) {
528
+ mutations.forEach(function (m) {
529
+ if (m.attributeName === 'data-sw-theme') {
530
+ reRenderAll();
531
+ }
532
+ });
533
+ });
534
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-sw-theme'] });
535
+ }
536
+
537
+ function getCustomVars(container) {
538
+ var raw = container.getAttribute('data-sw-mermaid-vars');
539
+ if (!raw) return null;
540
+ try { return JSON.parse(raw); } catch (e) { return null; }
541
+ }
542
+
543
+ function reRenderAll() {
544
+ if (!mermaidLoaded || !window.__swMermaid) return;
545
+ var containers = document.querySelectorAll('.sw-mermaid');
546
+ containers.forEach(function (container) {
547
+ // renderDiagram() no-ops when data-sw-mermaid-done is set (guards against
548
+ // concurrent initial-load races) — clear it so a theme change can force a fresh render.
549
+ container.removeAttribute('data-sw-mermaid-done');
550
+ renderDiagram(container, window.__swMermaid, getCustomVars(container));
551
+ });
552
+ }
553
+
554
+ // =========================================================
555
+ // Public API: called by the adapter's inline init script
556
+ // =========================================================
557
+ window.swMermaidInit = function () {
558
+ // Snapshot un-rendered containers now to check needsElk and decide whether
559
+ // to bother loading the CDN at all. The actual render queries fresh at CDN
560
+ // resolve time so it operates on whatever is currently in the DOM — not stale
561
+ // references to nodes that may have been replaced by a subsequent canvas-push
562
+ // before the async CDN load completed.
563
+ var snapshot = document.querySelectorAll('.sw-mermaid:not([data-sw-mermaid-done])');
564
+ if (snapshot.length === 0) return;
565
+
566
+ var needsElk = false;
567
+ snapshot.forEach(function (c) {
568
+ if (c.getAttribute('data-sw-mermaid-elk') === 'true') needsElk = true;
569
+ });
570
+
571
+ loadMermaid(needsElk).then(function (mermaid) {
572
+ // Re-query at render time: earlier snapshot nodes may be detached if the
573
+ // canvas was updated while the CDN was loading.
574
+ var containers = document.querySelectorAll('.sw-mermaid:not([data-sw-mermaid-done])');
575
+ containers.forEach(function (container) {
576
+ renderDiagram(container, mermaid, getCustomVars(container));
577
+ });
578
+ observeThemeChanges();
579
+ }).catch(function (err) {
580
+ // needsElk is page-global -- one elk: true diagram forces every
581
+ // diagram on the page down this CDN-import path, including in an
582
+ // --offline export, where the CDN is exactly what's blocked (ELK
583
+ // has no equivalent inlinable build; see the exporter's comment on
584
+ // mermaid_offline_script_tag). Without this, that failure was
585
+ // invisible: renderDiagram()'s own .catch only covers render
586
+ // errors, never a load failure, so every diagram on the page just
587
+ // stayed blank with nothing in the console but an unhandled
588
+ // rejection. Surface it the same way a render error already does.
589
+ document.querySelectorAll('.sw-mermaid__diagram:empty').forEach(function (diagramEl) {
590
+ diagramEl.innerHTML = '<pre class="sw-mermaid__error">'
591
+ + escapeHtml(err.message || String(err)) + '</pre>';
592
+ });
593
+ });
594
+ };
595
+
596
+ // Preload the mermaid CDN without rendering anything.
597
+ // Call this on page load so the CDN fetch completes before the first mermaid push arrives.
598
+ window.swMermaidPreload = function () { loadMermaid(false); };
599
+
600
+ // Re-trigger init after every HTMX swap so mermaid works in canvas-push sequences
601
+ // where the first push had no mermaid and a later push introduces it for the first time.
602
+ // swMermaidInit is idempotent: data-sw-mermaid-done guards against re-rendering.
603
+ document.addEventListener('htmx:afterSwap', function () {
604
+ if (document.querySelector('.sw-mermaid:not([data-sw-mermaid-done])')) {
605
+ window.swMermaidInit();
606
+ }
607
+ });
608
+
609
+ // Self-init once the document is fully parsed, so a page doesn't need
610
+ // Alpine (or any other trigger) to make its diagrams appear -- this file
611
+ // is the single thing a mermaid diagram actually depends on. Deferred to
612
+ // DOMContentLoaded rather than called here directly: this script is
613
+ // inlined at the position of the FIRST mermaid component in the page, so
614
+ // calling swMermaidInit() synchronously at parse time would miss any
615
+ // mermaid containers that appear later in the same document.
616
+ if (document.readyState === 'loading') {
617
+ document.addEventListener('DOMContentLoaded', window.swMermaidInit);
618
+ } else {
619
+ window.swMermaidInit();
620
+ }
621
+ })();
@@ -0,0 +1,53 @@
1
+ /**
2
+ * StreamWeaver Route Tabs (sw-route-tabs.js)
3
+ *
4
+ * Keeps a `tabs(..., url: true)` group's active index in the query string, so
5
+ * the tab a reader is looking at can be bookmarked, shared, and reached with
6
+ * the back button. Switching stays entirely client-side -- no request is made.
7
+ */
8
+ (function() {
9
+ 'use strict';
10
+
11
+ window.swRouteTabs = {
12
+ /**
13
+ * The active index this group's URL parameter asks for.
14
+ *
15
+ * The URL is the only thing read here. Falling back to the index the
16
+ * server rendered would resurrect a stale tab whenever an unrelated POST
17
+ * morphs the container from older session state.
18
+ *
19
+ * @param {string} key - The tabs group's state key
20
+ * @param {number} count - How many tabs the group has
21
+ * @returns {number} A valid index, or 0 when the parameter is absent,
22
+ * malformed, or past the last tab
23
+ */
24
+ read: function(key, count) {
25
+ var raw = new URLSearchParams(window.location.search).get(key);
26
+ if (!/^\d+$/.test(raw)) return 0;
27
+
28
+ var index = parseInt(raw, 10);
29
+ return index < count ? index : 0;
30
+ },
31
+
32
+ /**
33
+ * Record a tab switch in the URL.
34
+ *
35
+ * Merges into the query string as it stands at click time, so parameters
36
+ * this group knows nothing about -- another route tabs group's key, a
37
+ * filter some link put there -- survive the switch.
38
+ *
39
+ * @param {string} key - The tabs group's state key
40
+ * @param {number} index - The newly active index
41
+ */
42
+ push: function(key, index) {
43
+ var params = new URLSearchParams(window.location.search);
44
+ // Re-clicking the tab already showing would otherwise stack a duplicate
45
+ // entry, and the next Back press would look broken -- popstate would
46
+ // re-derive the same index and nothing on the page would move.
47
+ if (params.get(key) === String(index)) return;
48
+
49
+ params.set(key, index);
50
+ window.history.pushState({}, '', window.location.pathname + '?' + params.toString() + window.location.hash);
51
+ }
52
+ };
53
+ })();