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,193 @@
1
+ # frozen_string_literal: true
2
+ require "fileutils"
3
+ require_relative "shell"
4
+ require_relative "../css"
5
+ require_relative "../theme"
6
+
7
+ module StreamWeaver
8
+ module Opal
9
+ class OpalBuilder
10
+ # opal is the heavyweight compiler behind `streamweaver opal-build` --
11
+ # not a runtime dependency of the gem (see stream_weaver.gemspec), so a
12
+ # doc-shelf-only install must never be forced to have it. Load it here,
13
+ # on first actual use, instead of at file-load time.
14
+ def self.require_opal!
15
+ require "opal"
16
+ rescue LoadError
17
+ raise LoadError, "Opal features need the opal gem: gem install opal"
18
+ end
19
+
20
+ # Convenience entry point
21
+ def self.build(app_file, output_dir: "dist", title: nil, theme: nil)
22
+ require_opal!
23
+ new(app_file, output_dir: output_dir, title: title, theme: theme).call
24
+ end
25
+
26
+ def initialize(app_file, output_dir: "dist", title: nil, theme: nil)
27
+ @app_file = app_file
28
+ @output_dir = output_dir
29
+ @title = title || derive_title
30
+ @theme = theme
31
+ @lib_root = File.expand_path(File.join(__dir__, "../.."))
32
+ @stubs_root = File.join(__dir__, "stubs")
33
+ end
34
+
35
+ def call
36
+ FileUtils.mkdir_p(@output_dir)
37
+ write_app_js
38
+ copy_morphdom
39
+ copy_marked
40
+ copy_browser_assets
41
+ write_theme_css
42
+ write_index_html
43
+ end
44
+
45
+ private
46
+
47
+ # Assets committed under opal/stubs, plus mermaid from the gitignored
48
+ # vendor dir (see bin/vendor_browser_assets -- it is too large to commit).
49
+ # Anything missing falls back to its CDN in OpalShell, which keeps builds
50
+ # working for web deployment; only offline and browser-extension hosts
51
+ # actually require the local copies.
52
+ #
53
+ # sw-mermaid-zoom.js is different from the rest: it is StreamWeaver's own
54
+ # code, not a third-party library, so it has no CDN to fall back to and
55
+ # is always committed (unlike mermaid.min.js itself). It is copied
56
+ # unconditionally -- since Adapter::Static#render_mermaid (shared by both
57
+ # adapters) always writes the mermaid source into a data attribute rather
58
+ # than element text, this engine is the only thing that can read it. A
59
+ # build missing it would render an empty box, not a diagram
60
+ # (stream_weaver-mermaid-extension).
61
+ def copy_browser_assets
62
+ {
63
+ "prism.min.js" => File.join(@stubs_root, "prism.min.js"),
64
+ "prism-tomorrow.min.css" => File.join(@stubs_root, "prism-tomorrow.min.css"),
65
+ "diff.min.js" => File.join(@stubs_root, "diff.min.js"),
66
+ "mermaid.min.js" => mermaid_source_path,
67
+ "sw-mermaid-zoom.js" => File.join(@lib_root, "stream_weaver", "assets", "js", "sw-mermaid-zoom.js")
68
+ }.each do |name, src|
69
+ next unless src && File.exist?(src)
70
+
71
+ FileUtils.cp(src, output_path(name))
72
+ end
73
+
74
+ return if mermaid_source_path && File.exist?(mermaid_source_path)
75
+
76
+ warn "[OpalBuilder] mermaid.min.js not vendored -- falling back to CDN. " \
77
+ "Run bin/vendor_browser_assets for an offline build."
78
+ end
79
+
80
+ def mermaid_source_path
81
+ @mermaid_source_path ||= File.join(@lib_root, "..", "vendor", "js", "mermaid.min.js")
82
+ end
83
+
84
+ def local_asset(name)
85
+ File.exist?(output_path(name)) ? name : nil
86
+ end
87
+
88
+ def write_app_js
89
+ File.write(output_path("app.js"), compile.to_s)
90
+ end
91
+
92
+ def copy_morphdom
93
+ src = File.join(@stubs_root, "morphdom.min.js")
94
+ FileUtils.cp(src, output_path("morphdom.min.js")) if File.exist?(src)
95
+ end
96
+
97
+ def copy_marked
98
+ src = File.join(@stubs_root, "marked.umd.js")
99
+ FileUtils.cp(src, output_path("marked.umd.js")) if File.exist?(src)
100
+ end
101
+
102
+ def write_theme_css
103
+ css = StreamWeaver::CSS.full_stylesheet
104
+ css += "\n" + StreamWeaver::Theme.visual_skills_css
105
+ css += "\n" + StreamWeaver::CSS.animation_css
106
+ if @theme
107
+ unless StreamWeaver::Theme::Presets.get(@theme.to_sym)
108
+ warn "[OpalBuilder] Unknown theme preset: #{@theme}"
109
+ end
110
+ css += "\n" + StreamWeaver::Theme::Presets.generate_preset_css(@theme.to_sym)
111
+ end
112
+ File.write(output_path("sw-theme.css"), css)
113
+ end
114
+
115
+ def write_index_html
116
+ morphdom_js = File.exist?(output_path("morphdom.min.js")) ? "morphdom.min.js" : nil
117
+ marked_js = File.exist?(output_path("marked.umd.js")) ? "marked.umd.js" : nil
118
+ File.write(output_path("index.html"),
119
+ OpalShell.render(
120
+ title: @title,
121
+ app_js: "app.js",
122
+ morphdom_js: morphdom_js,
123
+ marked_js: marked_js,
124
+ prism_js: local_asset("prism.min.js"),
125
+ prism_css: local_asset("prism-tomorrow.min.css"),
126
+ diff_js: local_asset("diff.min.js"),
127
+ mermaid_js: local_asset("mermaid.min.js"),
128
+ mermaid_zoom_js: local_asset("sw-mermaid-zoom.js"),
129
+ theme_css: File.exist?(output_path("sw-theme.css")) ? "sw-theme.css" : nil,
130
+ google_fonts_url: google_fonts_url_for_build,
131
+ body_theme: @theme ? "sw-theme-default sw-theme-#{@theme}" : "sw-theme-default",
132
+ dark_mode_script: StreamWeaver::Theme::AutoMode.inline_script
133
+ ))
134
+ end
135
+
136
+ def google_fonts_url_for_build
137
+ if @theme && (preset = StreamWeaver::Theme::Presets.get(@theme.to_sym))
138
+ StreamWeaver::Theme::Presets.google_fonts_url(preset)
139
+ else
140
+ "https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;500;600&family=Source+Sans+3:wght@400;500;600;700&display=swap"
141
+ end
142
+ end
143
+
144
+ def compile
145
+ builder = build_opal_bundle
146
+ builder.build_str(stripped_source, File.basename(@app_file))
147
+ end
148
+
149
+ def build_opal_bundle
150
+ ::Opal::Builder.new(missing_require_severity: :ignore).tap do |b|
151
+ b.append_paths(@stubs_root)
152
+ b.append_paths(@lib_root)
153
+ b.build("opal")
154
+ # Must come immediately after "opal" (which defines Opal.regexp) --
155
+ # see regexp_anchor_patch.rb's own header comment for what bug
156
+ # this fixes and why the position matters (every compiled file
157
+ # captures its own local reference to the function it patches, at
158
+ # load time). This build (`streamweaver opal-build`, the AOT
159
+ # per-file compile path) doesn't currently hit the bug this
160
+ # patches -- nothing in its output builds a runtime-interpolated
161
+ # \A/\z-anchored regex today -- but bin/build_extension's bundle
162
+ # does, and keeping both Opal builders in sync means the fix
163
+ # doesn't silently stop applying the moment that changes.
164
+ b.build("stream_weaver/opal/regexp_anchor_patch")
165
+ build_stdlib(b)
166
+ b.build("stream_weaver/opal_entry")
167
+ end
168
+ end
169
+
170
+ def build_stdlib(builder)
171
+ %w[set cgi json digest thread].each do |lib|
172
+ builder.build(lib)
173
+ rescue => e
174
+ warn "[OpalBuilder] Could not build stdlib '#{lib}': #{e.message}"
175
+ end
176
+ end
177
+
178
+ def stripped_source
179
+ File.read(@app_file)
180
+ .gsub(/^\s*require_relative\s+['"][^'"]+['"]\s*$/, "")
181
+ .gsub(/^\s*require\s+['"]stream_weaver['"]\s*$/, "")
182
+ end
183
+
184
+ def output_path(filename)
185
+ File.join(@output_dir, filename)
186
+ end
187
+
188
+ def derive_title
189
+ File.basename(@app_file, ".rb").tr("_-", " ").split.map(&:capitalize).join(" ")
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ # backtick_javascript: true
3
+
4
+ module StreamWeaver
5
+ module Opal
6
+ # Answers "is there a DOM here?" for code that is compiled once and then
7
+ # runs in two very different hosts.
8
+ #
9
+ # The same bundle powers a browser tab (where `document` and `window` exist
10
+ # and the runtime patches live nodes) and a bare Node process (where neither
11
+ # exists and the runtime can only build strings). Rather than making every
12
+ # caller take a "browser or not" argument -- which would change the shape of
13
+ # `app()` and break existing builds -- the browser-only paths ask here and
14
+ # no-op when the answer is no.
15
+ #
16
+ # Under MRI this is always false: MRI is neither host, and short-circuiting
17
+ # before the backtick keeps the JS probe from being evaluated as a shell
18
+ # command.
19
+ module Env
20
+ def self.dom?
21
+ return false unless RUBY_ENGINE == "opal"
22
+
23
+ # :nocov:
24
+ `typeof document !== 'undefined' && document !== null && typeof document.createElement === 'function'`
25
+ # :nocov:
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StreamWeaver
4
+ module Opal
5
+ class ReactiveState
6
+ def initialize(hash = {})
7
+ @data = hash.transform_keys(&:to_sym)
8
+ @watchers = Hash.new { |h, k| h[k] = [] }
9
+ @tracking = nil
10
+ @track_map = Hash.new { |h, k| h[k] = [] }
11
+ @change_hooks = []
12
+ end
13
+
14
+ def [](key)
15
+ key = key.to_sym
16
+ @track_map[key] << @tracking if @tracking && !@track_map[key].include?(@tracking)
17
+ @data[key]
18
+ end
19
+
20
+ def []=(key, value)
21
+ key = key.to_sym
22
+ old = @data[key]
23
+ @data[key] = value
24
+ return if old == value
25
+ notify_watchers(key)
26
+ @change_hooks.each { |h| h.call(key) }
27
+ end
28
+
29
+ def watch(key, &block)
30
+ @watchers[key.to_sym] << block
31
+ end
32
+
33
+ def on_any_change(&block)
34
+ @change_hooks << block
35
+ end
36
+
37
+ def track(region_id)
38
+ prev, @tracking = @tracking, region_id
39
+ yield
40
+ ensure
41
+ @tracking = prev
42
+ end
43
+
44
+ def reset_tracking
45
+ @track_map.clear
46
+ end
47
+
48
+ def dependencies_for(region_id)
49
+ @track_map.each_with_object([]) { |(key, ids), arr| arr << key if ids.include?(region_id) }
50
+ end
51
+
52
+ def dependencies_for_key(key)
53
+ @track_map[key.to_sym].dup
54
+ end
55
+
56
+ def ==(other)
57
+ other.is_a?(ReactiveState) ? @data == other.to_h : @data == other
58
+ end
59
+
60
+ def to_h
61
+ @data.dup
62
+ end
63
+
64
+ def key?(key)
65
+ @data.key?(key.to_sym)
66
+ end
67
+
68
+ private
69
+
70
+ def notify_watchers(key)
71
+ @watchers[key].each { |w| w.call(@data[key]) }
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+ # backtick_javascript: true
3
+
4
+ # Fixes a real Opal bug: \A and \z anchors are only translated to JS's ^/$
5
+ # for a regex LITERAL known at compile time. An INTERPOLATED regex literal
6
+ # (`/foo#{bar}baz/`) goes through Opal.regexp, which joins the source parts
7
+ # and passes them straight to `new RegExp(...)` with zero anchor
8
+ # translation. \A/\z aren't valid JS regex escapes, so they silently match
9
+ # nothing instead of anchoring -- no error, just a pattern that quietly
10
+ # never matches.
11
+ #
12
+ # Regexp.new(string) is NOT the same bug and does not need this patch --
13
+ # it has its own, separate corelib-level translation
14
+ # (`regexp.replace('\A', '^').replace('\z', '$')`, no interpolation
15
+ # involved at all). That handling has its own narrower limitations (no /g,
16
+ # so only the first \A and first \z in a pattern get translated; \Z isn't
17
+ # handled) but is unaffected by, and doesn't fix, the Opal.regexp bug this
18
+ # file patches.
19
+ #
20
+ # Found via StreamWeaver::Org::Reader#callout_marker_match, which builds
21
+ # /\A\*(#{Regexp.union(VARIANT_EMOJI.keys)})...\*\z/ at runtime -- every
22
+ # emoji-marked callout silently misclassified as a card. Not specific to
23
+ # Regexp.union, emoji, or that one call site: reproduced with a plain
24
+ # string interpolated into an anchored pattern, no union/escape involved --
25
+ # it's specifically about interpolation, not about Regexp.new.
26
+ #
27
+ # Must load AFTER `opal` (which defines Opal.regexp) and BEFORE anything
28
+ # else in the build sequence -- every compiled file captures its OWN
29
+ # `$regexp = Opal.regexp` local alias at module-load time (a per-file
30
+ # Opal codegen optimization), so patching Opal.regexp from a script loaded
31
+ # after the whole bundle has already executed is too late: those aliases
32
+ # already point at the original, unpatched function. Compiling this as
33
+ # part of the same Opal::Builder sequence (see bin/build_extension),
34
+ # positioned right after `opal` and before any other `builder.build` call,
35
+ # is what makes every later file's alias capture pick up the fix instead.
36
+ #
37
+ # \A -> ^ is an exact translation as long as the pattern never reaches JS's
38
+ # `m` (multiline) flag -- confirmed by reading Opal.regexp's own source: it
39
+ # only ever sets JS's `s` (dotall) for Ruby's /m modifier, never JS's `m`,
40
+ # so ^ stays equivalent to \A for every pattern this function is ever
41
+ # called with. \z -> $ is the same story for \z specifically; \Z is treated
42
+ # the same way here for simplicity, though it isn't exact (\Z also matches
43
+ # just before a trailing newline, $ without /m doesn't) -- fine for this
44
+ # codebase's actual patterns, worth knowing if a future one relies on that
45
+ # distinction.
46
+ #
47
+ # Not yet filed upstream -- tracked as stream_weaver-mg6b.
48
+ if RUBY_ENGINE == "opal"
49
+ %x{
50
+ (function () {
51
+ var original = Opal.regexp;
52
+ Opal.regexp = function (parts, flags) {
53
+ for (var i = 0; i < parts.length; i++) {
54
+ if (typeof parts[i] === "string") {
55
+ parts[i] = parts[i].replace(/\\A/g, "^").replace(/\\[zZ]/g, "$");
56
+ }
57
+ }
58
+ return original(parts, flags);
59
+ };
60
+ })();
61
+ }
62
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module StreamWeaver
6
+ module Opal
7
+ class OpalRenderer
8
+ attr_reader :adapter
9
+
10
+ def initialize(adapter, state)
11
+ @adapter = adapter
12
+ @state = state
13
+ @output = []
14
+ end
15
+
16
+ # Block tags — open tag, yield, close tag.
17
+ # If the block returns a String and nothing was added to @output inside the block,
18
+ # treat the return value as plain text (mirrors Phlex behavior for simple text blocks).
19
+ %w[div span p ul ol li h1 h2 h3 h4 h5 h6 form label select textarea
20
+ nav header footer main section article aside table thead tbody tr th td
21
+ button fieldset legend details summary strong em a
22
+ pre code blockquote figure figcaption caption style script small
23
+ time mark sub sup abbr dl dt dd].each do |tag|
24
+ define_method(tag) do |**attrs, &block|
25
+ @output << "<#{tag}#{attrs_to_html(attrs)}>"
26
+ if block
27
+ before_len = @output.length
28
+ result = block.call
29
+ # If block didn't add anything (returned a string), emit it as plain text
30
+ if @output.length == before_len && result.is_a?(String)
31
+ @output << html_escape(result)
32
+ end
33
+ end
34
+ @output << "</#{tag}>"
35
+ end
36
+ end
37
+
38
+ # Void (self-closing) tags
39
+ %w[input hr br img link meta].each do |tag|
40
+ define_method(tag) do |**attrs|
41
+ @output << "<#{tag}#{attrs_to_html(attrs)}>"
42
+ end
43
+ end
44
+
45
+ def plain(text)
46
+ @output << html_escape(text.to_s)
47
+ end
48
+
49
+ def raw(html)
50
+ @output << html.to_s
51
+ end
52
+
53
+ # Phlex marks trusted strings with #safe before #raw will emit them.
54
+ # Here #raw already emits verbatim, so this is just the identity function
55
+ # -- it exists so shared renderers can call view.raw(view.safe(css))
56
+ # unchanged in both adapters.
57
+ def safe(html) = html.to_s
58
+
59
+ def to_html
60
+ @output.join
61
+ end
62
+
63
+ private
64
+
65
+ def attrs_to_html(attrs)
66
+ return "" if attrs.empty?
67
+ " " + attrs.filter_map do |k, v|
68
+ next if v == false
69
+ key = k.to_s.tr("_", "-")
70
+ v == true ? key : "#{key}=\"#{html_escape(v.to_s)}\""
71
+ end.join(" ")
72
+ end
73
+
74
+ def html_escape(str) = CGI.escapeHTML(str.to_s)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "reactive_state"
4
+ require_relative "shell"
5
+
6
+ module StreamWeaver
7
+ module Opal
8
+ class OpalRuntime
9
+ class << self
10
+ attr_accessor :current
11
+ end
12
+
13
+ attr_reader :state
14
+
15
+ def initialize(adapter:)
16
+ @adapter = adapter
17
+ @state = ReactiveState.new
18
+ @callbacks = {}
19
+ @block = nil
20
+ @start_hooks = []
21
+ @start_hooks_fired = false
22
+ @watchers_initialized = false
23
+ @sync_rendering = false
24
+ @rerender_pending = false
25
+ @state.on_any_change { schedule_rerender }
26
+ end
27
+
28
+ def watchers_initialized? = @watchers_initialized
29
+
30
+ def set_block(&block)
31
+ @block = block
32
+ end
33
+
34
+ def update_state(key, value)
35
+ @state[key] = value
36
+ end
37
+
38
+ def register_callback(dom_id, &proc)
39
+ @callbacks[dom_id] = proc
40
+ end
41
+
42
+ def invoke_callback(dom_id)
43
+ @callbacks[dom_id]&.call(@state)
44
+ end
45
+
46
+ def register_start_hook(block)
47
+ @start_hooks << block unless @start_hooks_fired
48
+ end
49
+
50
+ def render_html
51
+ @callbacks.clear
52
+ @state.reset_tracking
53
+ OpalRuntime.current = self
54
+
55
+ # First pass: build components and determine count (no tracking yet)
56
+ app = StreamWeaver::App.new("__opal__", &@block)
57
+ app.rebuild_with_state(@state)
58
+ @watchers_initialized = true
59
+ n = app.components.length
60
+
61
+ register_component_callbacks(app.components)
62
+
63
+ # Second pass: render each component inside its own track region.
64
+ # rebuild_with_state is called inside each track block so that state
65
+ # reads in the DSL block (e.g. `text state[:name].to_s`) are recorded
66
+ # against the correct region_id.
67
+ # DSL-time reads (state[:key] inside the app block) happen during rebuild_with_state,
68
+ # so we re-build once per region inside track() to attribute reads to the correct region.
69
+ parts = (0...n).map do |i|
70
+ region_html = @state.track("sw-region-#{i}") do
71
+ scoped_app = StreamWeaver::App.new("__opal__", &@block)
72
+ scoped_app.rebuild_with_state(@state)
73
+ component = scoped_app.components[i]
74
+ sub = OpalRenderer.new(@adapter, @state)
75
+ component.render(sub, @state) if component
76
+ sub.to_html
77
+ end
78
+ "<div id=\"sw-region-#{i}\">#{region_html}</div>"
79
+ end
80
+ parts.join
81
+ ensure
82
+ OpalRuntime.current = nil
83
+ end
84
+
85
+ # --- DOM-free rendering -------------------------------------------------
86
+ #
87
+ # render_html already builds the whole document without touching the DOM;
88
+ # everything below is what a non-browser host (Node, a CLI) needs around
89
+ # it. Nothing here references `window` or `document`, directly or through
90
+ # a callee, so the same compiled bundle renders in a bare Node process.
91
+
92
+ # The document body, as a string. Same markup the browser paints.
93
+ def render_body_html
94
+ render_html
95
+ end
96
+
97
+ # Per-component CSS gathered during the last render.
98
+ #
99
+ # In a browser the adapter appends this to <head> and the caller never
100
+ # needs it. With no <head> to append to, the caller has to place it, so
101
+ # the collected text is handed back rather than dropped -- a document
102
+ # rendered to a file is styled because of this.
103
+ def collected_css
104
+ return "" unless @adapter.respond_to?(:collected_css_text)
105
+
106
+ @adapter.collected_css_text
107
+ end
108
+
109
+ # A complete, standalone HTML document: body markup baked in, component
110
+ # CSS inlined, no app.js and no runtime boot. This is the artifact a
111
+ # `streamweaver-render doc.rb > doc.html` CLI writes out.
112
+ #
113
+ # stylesheet: framework CSS to inline ahead of the component CSS. Defaults
114
+ # to whatever is reachable from the current host (see CSS.base_stylesheet)
115
+ # -- a host that ships the full theme file can pass its contents instead.
116
+ # Remaining options are forwarded to OpalShell.render.
117
+ def render_document(title: "StreamWeaver Document", stylesheet: nil, **shell_options)
118
+ body = render_html
119
+ css = [stylesheet || StreamWeaver::CSS.base_stylesheet, collected_css]
120
+ .reject { |c| c.nil? || c.to_s.strip.empty? }.join("\n")
121
+
122
+ OpalShell.render(
123
+ title: title,
124
+ app_js: nil,
125
+ body_html: body,
126
+ inline_css: css,
127
+ **shell_options
128
+ )
129
+ end
130
+
131
+ def register_component_callbacks(components)
132
+ Array(components).each do |c|
133
+ c.register_callbacks(@callbacks)
134
+ register_component_callbacks(c.children)
135
+ end
136
+ end
137
+
138
+ def render_and_patch
139
+ @sync_rendering = true
140
+ html = render_html
141
+ patch_dom(html)
142
+ ensure
143
+ @sync_rendering = false
144
+ fire_start_hooks_once
145
+ end
146
+
147
+ def invoke_and_patch(dom_id)
148
+ @sync_rendering = true
149
+ invoke_callback(dom_id)
150
+ patch_dom(render_html)
151
+ ensure
152
+ @sync_rendering = false
153
+ end
154
+
155
+ def update_and_patch(key, value)
156
+ @sync_rendering = true
157
+ update_state(key, value)
158
+ affected_regions = @state.dependencies_for_key(key.to_sym)
159
+ if affected_regions.empty?
160
+ patch_dom(render_html)
161
+ else
162
+ html = render_html
163
+ patch_regions(affected_regions, html)
164
+ end
165
+ ensure
166
+ @sync_rendering = false
167
+ end
168
+
169
+ def patch_regions(region_ids, full_html)
170
+ # :nocov:
171
+ %x{
172
+ var parser = new DOMParser();
173
+ var doc = parser.parseFromString('<div id="sw-app">' + #{full_html} + '</div>', 'text/html');
174
+ var ids = #{region_ids};
175
+ for (var i = 0; i < ids.length; i++) {
176
+ var id = ids[i];
177
+ var newRegion = doc.getElementById(id);
178
+ var oldRegion = document.getElementById(id);
179
+ if (newRegion && oldRegion) { morphdom(oldRegion, newRegion); }
180
+ }
181
+ }
182
+ # :nocov:
183
+ announce_render
184
+ end
185
+
186
+ def fire_start_hooks_once
187
+ return if @start_hooks_fired
188
+ @start_hooks_fired = true
189
+ hooks = @start_hooks.dup
190
+ schedule_start_hooks(hooks)
191
+ end
192
+
193
+ def schedule_start_hooks(hooks)
194
+ # :nocov:
195
+ runtime = self
196
+ %x{
197
+ setTimeout(function() {
198
+ #{hooks.each(&:call)};
199
+ #{runtime.schedule_rerender};
200
+ }, 0);
201
+ }
202
+ # :nocov:
203
+ end
204
+
205
+ def schedule_rerender
206
+ # State mutations during a sync render are dropped — app blocks should not
207
+ # write state as a side effect of building the view.
208
+ return if @sync_rendering || @rerender_pending
209
+ @rerender_pending = true
210
+ return unless RUBY_ENGINE == "opal"
211
+ runtime = self
212
+ # :nocov:
213
+ %x{
214
+ setTimeout(function() {
215
+ #{runtime.perform_async_render};
216
+ }, 0);
217
+ }
218
+ # :nocov:
219
+ end
220
+
221
+ def perform_async_render
222
+ @rerender_pending = false
223
+ render_and_patch
224
+ end
225
+
226
+ def patch_dom(html)
227
+ # :nocov:
228
+ %x{ morphdom(document.getElementById('sw-app'), '<div id="sw-app">' + #{html} + '</div>') }
229
+ # :nocov:
230
+ announce_render
231
+ end
232
+
233
+ # Signals that freshly patched markup is in the DOM.
234
+ #
235
+ # Libraries that decorate rendered output rather than produce it --
236
+ # Prism, Mermaid -- have to run after a patch, and again after every
237
+ # subsequent one, because morphdom replaces nodes and takes their
238
+ # decoration with it. Start hooks and re-renders are scheduled through
239
+ # setTimeout, so there is no moment after start() when a caller can
240
+ # simply assume the DOM is settled; this event is that signal.
241
+ def announce_render
242
+ return unless RUBY_ENGINE == "opal"
243
+
244
+ # :nocov:
245
+ %x{ document.dispatchEvent(new CustomEvent("sw:render")) }
246
+ # :nocov:
247
+ end
248
+ end
249
+ end
250
+ end