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,460 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'timeout'
4
+ require 'shellwords'
5
+
6
+ module StreamWeaver
7
+ # iTerm2 integration for panel workflow via the optional iterm2_ruby gem
8
+ # (`gem install iterm2_ruby` — https://rubygems.org/gems/iterm2_ruby).
9
+ # Falls back to system browser when iTerm2 API is unavailable.
10
+ class ITerm
11
+ # iTerm2's built-in profile that renders a web view instead of a shell.
12
+ BROWSER_PROFILE = "Web Browser"
13
+
14
+ # Default window frames (points) for the two windows this class ever
15
+ # creates from scratch -- the University controller (narrow, tall) and a
16
+ # worker tab that couldn't inherit the caller's window (wide, short).
17
+ # Each is overridable via env as "x,y,width,height".
18
+ CONTROLLER_FRAME_DEFAULTS = { x: 40, y: 40, width: 760, height: 1200 }.freeze
19
+ WORKER_FRAME_DEFAULTS = { x: 80, y: 80, width: 1600, height: 1000 }.freeze
20
+
21
+ # Bracketed paste (DEC 2004). A TUI that has enabled the mode reads
22
+ # everything between these two sequences as one pasted block and takes
23
+ # the block whole, rather than interpreting each byte as it arrives --
24
+ # which is what lets the Return that follows read as a keypress.
25
+ PASTE_START = "\e[200~"
26
+ PASTE_END = "\e[201~"
27
+
28
+ class << self
29
+ def available?
30
+ return @available if defined?(@available)
31
+ @available = check_availability
32
+ end
33
+
34
+ # iTerm2's real value always has the shape "w0t0p0:<UUID>" -- the part
35
+ # after the colon is the session id every RPC in this file actually
36
+ # wants. A value with no colon at all (missing entirely, or garbled by
37
+ # something outside iTerm2) is never that shape, so it returns nil
38
+ # rather than passing the raw, meaningless string through as if it
39
+ # were a real session id (code review, round-9 UAT: `focus-me` calls
40
+ # this blind on every course prompt and must never mistake garbage
41
+ # for a target).
42
+ def current_session_guid
43
+ raw = ENV["ITERM_SESSION_ID"]
44
+ return nil unless raw&.include?(":")
45
+
46
+ guid = raw.split(":", 2).last
47
+ guid.empty? ? nil : guid
48
+ end
49
+
50
+ # True only when the user is inside iTerm2 on macOS but the optional gem
51
+ # isn't installed — the one case where a "gem install iterm2_ruby" hint
52
+ # is actionable rather than noise.
53
+ def gem_missing?
54
+ !available? &&
55
+ RbConfig::CONFIG["host_os"].match?(/darwin/) &&
56
+ !ENV["ITERM_SESSION_ID"].to_s.empty?
57
+ end
58
+
59
+ # Split a browser pane with the URL into `target_session` (any session
60
+ # id, e.g. a just-created worker tab's session -- not necessarily the
61
+ # calling pane). Defaults to the calling session (current_session_guid)
62
+ # when target_session is nil, matching the original single-pane
63
+ # `panel` behavior. Returns Hash with :type (:browser, :external, or
64
+ # nil) and :pane_id.
65
+ def split_vertical_with_url(url, open_browser: true, horizontal: false, target_session: nil)
66
+ return { type: nil, pane_id: nil } unless available?
67
+
68
+ pane_id = split_browser_pane(url, horizontal: horizontal, target_session: target_session)
69
+
70
+ if pane_id
71
+ { type: :browser, pane_id: pane_id }
72
+ elsif open_browser && !ENV['SW_NO_OPEN']
73
+ system("open", url)
74
+ { type: :external, pane_id: nil }
75
+ else
76
+ { type: nil, pane_id: nil }
77
+ end
78
+ end
79
+
80
+ # Opens `url` in an iTerm2 browser pane that owns a WHOLE WINDOW, and
81
+ # returns that browser session's id (nil if it could not be opened) --
82
+ # same shape as open_worker_tab.
83
+ #
84
+ # StreamWeaver University's canvas is a controller, not a sidecar: it
85
+ # gets its own window so the worker tab stays free for the agent and
86
+ # the demo canvas panes the course steps have it create. iTerm2's API
87
+ # has no "new browser window" call, so this composes three primitives
88
+ # that already work elsewhere in this file -- create_tab with no
89
+ # window_id makes a new window, split_pane with the Web Browser
90
+ # profile makes the browser, and the shell that came with the window
91
+ # is then closed so only the canvas is left.
92
+ #
93
+ # A brand new iTerm2 window opens at the profile's default size --
94
+ # unlike open_worker_tab below, this one has no caller's window to
95
+ # reuse (the controller is its own window by design), so it sizes
96
+ # itself explicitly via set_window_frame instead: a narrow controller
97
+ # shape (CONTROLLER_FRAME_DEFAULTS, overridable via SW_CONTROLLER_FRAME).
98
+ # Degrades silently on an older iterm2_ruby that lacks the method.
99
+ def open_browser_window(url)
100
+ return nil unless available?
101
+
102
+ with_timeout(10, default: nil) do
103
+ connect do |c|
104
+ created = c.create_tab
105
+ shell = created && created[:session_id]
106
+ next nil unless shell
107
+
108
+ apply_window_frame(c, created[:window_id], "SW_CONTROLLER_FRAME", CONTROLLER_FRAME_DEFAULTS)
109
+
110
+ pane = browser_pane_in(c, shell, url)
111
+
112
+ # Either way the shell goes: with a browser it would be clutter
113
+ # beside the canvas, and without one it is a window the user
114
+ # never asked for and cannot account for. Closing the only
115
+ # session takes the window with it. Scoped so a close that
116
+ # fails can never cost us a working pane's id -- losing that
117
+ # would leave a canvas window nothing can close later.
118
+ close_quietly(c, shell)
119
+ pane
120
+ end
121
+ end
122
+ rescue StandardError
123
+ nil
124
+ end
125
+
126
+ def close_pane(pane_id)
127
+ return false unless available? && pane_id
128
+ connect { |c| c.close_session(pane_id, force: true) }
129
+ rescue ITerm2::Error
130
+ false
131
+ end
132
+
133
+ # Brings an already-open pane to the front. Public wrapper around the
134
+ # private, connection-scoped activate_session_quietly used by the
135
+ # raise-on-run path (send_to_session, above) -- this is the same
136
+ # primitive for a caller that isn't already inside a `connect` block
137
+ # (`streamweaver canvas-raise`, round-7 UAT: a worker-initiated canvas
138
+ # push, unlike a Run submit, has nothing that raises it, and `panel`
139
+ # can't be called twice without splitting a second pane). Fire-and-
140
+ # forget, same as the private version: false only means "could not
141
+ # even try", not "did not land".
142
+ def activate_session(session_id)
143
+ return false unless available? && session_id
144
+
145
+ connect { |c| activate_session_quietly(c, session_id) }
146
+ true
147
+ rescue StandardError
148
+ false
149
+ end
150
+
151
+ def split_vertical_with_command(command)
152
+ return false unless available?
153
+
154
+ guid = current_session_guid or return false
155
+
156
+ connect do |c|
157
+ new_id = c.split_pane(guid, vertical: true)
158
+ c.send_text(new_id, "#{command}\n")
159
+ end
160
+ true
161
+ rescue ITerm2::Error
162
+ false
163
+ end
164
+
165
+ def navigate_browser(session_id, url)
166
+ return false unless available? && session_id
167
+ connect { |c| c.set_profile_property(session_id, "Initial URL", url) }
168
+ rescue ITerm2::Error
169
+ false
170
+ end
171
+
172
+ # Attempts an actual connection to iTerm2's Python API server (osascript
173
+ # auth handshake + websocket upgrade) -- distinct from `available?`,
174
+ # which only confirms the gem itself is installed/loadable. The one
175
+ # case this tells apart: gem installed, but iTerm2's own
176
+ # Preferences > General > Magic > Enable Python API toggle is off.
177
+ def python_api_reachable?
178
+ return false unless available?
179
+ with_timeout(5, default: false) { connect { true } }
180
+ rescue StandardError
181
+ false
182
+ end
183
+
184
+ # Create a new iTerm2 tab, cd into `dir` (defaults to the caller's cwd),
185
+ # then run `command` in it (e.g. the `claude` or `codex` CLI). A new
186
+ # iTerm2 tab starts in the user's home directory, not the caller's --
187
+ # without the cd, the agent launches at ~ instead of the project it was
188
+ # invoked from. Returns the new tab's primary session_id, or nil if
189
+ # unavailable or the RPC failed.
190
+ def open_worker_tab(command, dir: Dir.pwd)
191
+ return nil unless available?
192
+ with_timeout(8, default: nil) do
193
+ connect do |c|
194
+ # In the caller's own window when we can find it -- the window
195
+ # the user is already sitting in is by definition the size they
196
+ # chose, so that path leaves the frame untouched apart from
197
+ # growing it up to the worker minimum (and the calling PANE is
198
+ # untouched either way, which is the actual product promise).
199
+ # Only the fallback path -- a genuinely new window -- gets an
200
+ # explicit wide frame via set_window_frame, since a brand new
201
+ # iTerm2 window otherwise opens at the profile's default size
202
+ # (UAT found that unusably small twice over).
203
+ window_id = calling_window_id(c)
204
+ result = window_id ? c.create_tab(window_id: window_id) : c.create_tab
205
+ session_id = result && result[:session_id]
206
+ if session_id
207
+ window_id ? ensure_min_worker_frame(c, window_id) : apply_window_frame(c, result[:window_id], "SW_WORKER_FRAME", WORKER_FRAME_DEFAULTS)
208
+ c.send_text(session_id, "cd #{Shellwords.escape(dir)} && #{command}\n")
209
+ end
210
+ session_id
211
+ end
212
+ end
213
+ rescue StandardError
214
+ nil
215
+ end
216
+
217
+ # --- Driver adapter -------------------------------------------------
218
+ # The surface adapter above puts a canvas beside a terminal; these two
219
+ # put text *into* one specific terminal. StreamWeaver University's
220
+ # runner uses them to send a step's prompt to the worker session
221
+ # `get-started` recorded, and only that one -- see
222
+ # lib/stream_weaver/university/runner.rb.
223
+
224
+ # Types `text` into exactly `session_id`, then presses Return unless
225
+ # `submit: false`. Never falls back to the calling session: a
226
+ # mistargeted send drops a prompt into whatever pane the user happens
227
+ # to be looking at, which is the failure this whole path exists to
228
+ # prevent.
229
+ #
230
+ # Return is a carriage return, not a line feed -- that is what a
231
+ # terminal actually sends when a human presses the key, and a raw-mode
232
+ # TUI (the `claude` / `codex` CLIs) does not read an LF as submit.
233
+ #
234
+ # It also goes in a write of its OWN. UAT 2026-08-31: with the CR
235
+ # appended to the same write, the prompt appeared in the claude pane
236
+ # and just sat there -- the TUI read the whole write as pasted text,
237
+ # so the CR became a character in the composer rather than a keypress.
238
+ # A real Return is a separate event, so we send a separate one.
239
+ #
240
+ # UAT 2026-09-03 (round 3): a separate CR write still did not submit.
241
+ # The text write is therefore wrapped in bracketed paste markers, so
242
+ # the TUI knows exactly where the pasted block ends -- without them it
243
+ # keeps treating the following bytes as more of the same paste, and
244
+ # the CR is swallowed as paste content rather than read as Return.
245
+ # Only the text is bracketed; the CR must arrive OUTSIDE the block or
246
+ # the whole exercise is moot.
247
+ #
248
+ # Owning the keystroke here is what lets callers hand over prompt text
249
+ # and nothing terminal-shaped; a later herdr/cmux driver makes the
250
+ # same promise its own way.
251
+ def send_to_session(session_id, text, submit: true)
252
+ return false unless available? && session_id
253
+
254
+ with_timeout(8, default: false) do
255
+ connect do |c|
256
+ next false unless c.send_text(session_id, bracketed_paste(text))
257
+ next true unless submit
258
+ next false unless c.send_text(session_id, "\r")
259
+
260
+ # A successful submit is exactly the moment the worker's tab and
261
+ # window should come to front -- Forrest's UAT wants Run to feel
262
+ # like handing control to the agent, not typing into a pane he
263
+ # has to go find. Only a full send (text landed AND Return
264
+ # landed) gets here; a refused, degraded, or half-sent attempt
265
+ # returns false above and never raises anything. Fire-and-forget:
266
+ # no poll to confirm it landed (see activate_session_quietly).
267
+ activate_session_quietly(c, session_id)
268
+ true
269
+ end
270
+ end
271
+ rescue StandardError
272
+ false
273
+ end
274
+
275
+ # True only when `session_id` is still in iTerm2's live topology --
276
+ # the check that distinguishes "the worker tab is there" from "the
277
+ # user closed it an hour ago". False on any doubt (gem missing, API
278
+ # unreachable, RPC error), so callers degrade rather than guess.
279
+ def session_alive?(session_id)
280
+ return false unless available? && session_id
281
+
282
+ with_timeout(5, default: false) do
283
+ connect { |c| c.topology.any? { |s| s[:session_id] == session_id } }
284
+ end
285
+ rescue StandardError
286
+ false
287
+ end
288
+
289
+ private
290
+
291
+ APP_NAME = "StreamWeaver"
292
+
293
+ # `text` as one bracketed-paste block. A terminal that has not enabled
294
+ # the mode drops the markers on the floor, so this is safe to apply
295
+ # unconditionally rather than sniffing what is running in the pane.
296
+ def bracketed_paste(text) = "#{PASTE_START}#{text}#{PASTE_END}"
297
+
298
+ # The browser pane for `url`, or nil. The real client raises on a
299
+ # failed split (and returns nil only for an OK response with no
300
+ # session), so both spellings of failure are caught here rather than
301
+ # unwinding past the shell cleanup above.
302
+ def browser_pane_in(client, shell, url)
303
+ client.split_pane(
304
+ shell,
305
+ vertical: true,
306
+ profile_name: BROWSER_PROFILE,
307
+ profile_customizations: { "Initial URL" => url }
308
+ )
309
+ rescue StandardError
310
+ nil
311
+ end
312
+
313
+ def close_quietly(client, session_id)
314
+ client.close_session(session_id, force: true)
315
+ rescue StandardError
316
+ nil
317
+ end
318
+
319
+ # Sets window_id's frame to defaults, overridden by an "x,y,width,height"
320
+ # value in ENV[env_var] when present and parseable. Silently a no-op on
321
+ # an iterm2_ruby too old to have set_window_frame (one stderr hint,
322
+ # printed once) -- callers don't need to know which gem version is
323
+ # installed.
324
+ def apply_window_frame(client, window_id, env_var, defaults)
325
+ return unless window_id
326
+
327
+ unless client.respond_to?(:set_window_frame)
328
+ warn_frame_unsupported_once
329
+ return
330
+ end
331
+
332
+ frame = parse_frame_env(ENV[env_var], defaults)
333
+ client.set_window_frame(window_id, **frame)
334
+ rescue StandardError
335
+ nil
336
+ end
337
+
338
+ def parse_frame_env(raw, defaults)
339
+ return defaults if raw.to_s.empty?
340
+
341
+ parts = raw.split(",").map(&:strip)
342
+ return defaults unless parts.size == 4
343
+
344
+ x, y, width, height = parts.map { |p| Integer(p, exception: false) }
345
+ return defaults if [x, y, width, height].any?(&:nil?)
346
+
347
+ { x: x, y: y, width: width, height: height }
348
+ end
349
+
350
+ def warn_frame_unsupported_once
351
+ return if @frame_hint_shown
352
+ @frame_hint_shown = true
353
+ warn "StreamWeaver: window sizing needs iterm2_ruby >= 0.3.0 (gem install iterm2_ruby)"
354
+ end
355
+
356
+ # Grows window_id up to the worker minimum (WORKER_FRAME_DEFAULTS,
357
+ # overridable via SW_WORKER_FRAME) when the caller's own window --
358
+ # reused rather than created by open_worker_tab -- is smaller than
359
+ # that on either axis. Keeps the window's current x,y: this is a
360
+ # floor, not a reposition. Never shrinks a window already at or above
361
+ # the minimum, and never touches set_window_frame at all in that
362
+ # case. Degrades silently on an older iterm2_ruby that lacks
363
+ # get_window_frame.
364
+ def ensure_min_worker_frame(client, window_id)
365
+ return unless client.respond_to?(:get_window_frame)
366
+
367
+ current = client.get_window_frame(window_id)
368
+ return unless current && current[:width] && current[:height]
369
+
370
+ minimum = parse_frame_env(ENV["SW_WORKER_FRAME"], WORKER_FRAME_DEFAULTS)
371
+ return if current[:width] >= minimum[:width] && current[:height] >= minimum[:height]
372
+
373
+ client.set_window_frame(
374
+ window_id,
375
+ x: current[:x],
376
+ y: current[:y],
377
+ width: [current[:width], minimum[:width]].max,
378
+ height: [current[:height], minimum[:height]].max
379
+ )
380
+ rescue StandardError
381
+ nil
382
+ end
383
+
384
+ # Best-effort, fire-and-forget: brings the worker's tab and window to
385
+ # the front after a step prompt actually goes out, so Run feels like
386
+ # handing control to the agent rather than something the user has to
387
+ # go find. Never polls to confirm the raise actually landed (the gem
388
+ # author flagged iterm2_ruby's own client.focus as unreliable with
389
+ # multiple windows open) -- fire it and move on. The respond_to?
390
+ # check is cheap insurance, not a version gate: activate_session has
391
+ # shipped since iterm2_ruby 0.1.0, so this only degrades on a client
392
+ # missing it altogether. One memoized stderr hint either way; never
393
+ # raises past this, since a raise failing is not a reason to report
394
+ # the send itself as failed.
395
+ def activate_session_quietly(client, session_id)
396
+ unless client.respond_to?(:activate_session)
397
+ warn_activate_unsupported_once
398
+ return
399
+ end
400
+
401
+ client.activate_session(session_id)
402
+ rescue StandardError
403
+ nil
404
+ end
405
+
406
+ def warn_activate_unsupported_once
407
+ return if @activate_hint_shown
408
+ @activate_hint_shown = true
409
+ warn "StreamWeaver: raise-on-run needs an iterm2_ruby client with activate_session (gem install iterm2_ruby)"
410
+ end
411
+
412
+ # The window holding the session this process is running in, or nil if
413
+ # it can't be determined (not in iTerm2, or the lookup failed). Callers
414
+ # treat nil as "open a new window".
415
+ def calling_window_id(client)
416
+ guid = current_session_guid or return nil
417
+ client.topology.find { |s| s[:session_id] == guid }&.dig(:window_id)
418
+ rescue StandardError
419
+ nil
420
+ end
421
+
422
+ def check_availability
423
+ return false unless RbConfig::CONFIG["host_os"].match?(/darwin/)
424
+ # Fast check: ITERM_SESSION_ID is set when running inside iTerm2.
425
+ # Avoids the slow ITerm2.connect call (which spawns a Python subprocess
426
+ # and can hang for seconds when the Python API is slow to initialize).
427
+ return false unless ENV['ITERM_SESSION_ID']
428
+
429
+ require "iterm2"
430
+ true
431
+ rescue LoadError
432
+ false
433
+ end
434
+
435
+ def connect(&) = ITerm2.connect(app_name: APP_NAME, &)
436
+
437
+ def split_browser_pane(url, horizontal: false, target_session: nil)
438
+ guid = target_session || current_session_guid or return nil
439
+ with_timeout(8, default: nil) do
440
+ connect do |c|
441
+ c.split_pane(
442
+ guid,
443
+ vertical: !horizontal,
444
+ profile_name: BROWSER_PROFILE,
445
+ profile_customizations: { "Initial URL" => url }
446
+ )
447
+ end
448
+ end
449
+ rescue ITerm2::Error
450
+ nil
451
+ end
452
+
453
+ def with_timeout(seconds, default: nil)
454
+ Timeout.timeout(seconds) { yield }
455
+ rescue Timeout::Error
456
+ default
457
+ end
458
+ end
459
+ end
460
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StreamWeaver
4
+ # Registry for named page layouts.
5
+ #
6
+ # A layout controls the outer chrome (topbar, sidebar, main area) of a StreamWeaver app.
7
+ # The built-in `:default` layout is the standard single-column centered body.
8
+ # Custom layouts can opt out of the default chrome with `exclusive: true`.
9
+ #
10
+ # @example Registering a custom layout
11
+ # StreamWeaver.register_layout(:two_rail, exclusive: true, body_classes: %w[my-shell]) do
12
+ # div(class: "my-header") { render_slot(:header) }
13
+ # div(class: "my-content") do
14
+ # div(class: "my-sidebar") { render_slot(:sidebar_left) }
15
+ # main_content_region
16
+ # end
17
+ # end
18
+ #
19
+ # @example Using it in an app
20
+ # app "X", layout: :two_rail do
21
+ # layout_slot(:header) { text "Header content" }
22
+ # layout_slot(:sidebar_left) { text "Sidebar content" }
23
+ # text "Main content"
24
+ # end
25
+ module LayoutRegistry
26
+ CHROMELESS_ENTRY = {
27
+ exclusive: true,
28
+ # sw-chromeless carries a minimal, token-driven padding baseline (see
29
+ # views.rb) so bare content isn't glued to the viewport edge -- still
30
+ # overridable to zero by an app's own CSS (FAC-9u2).
31
+ body_classes: ["sw-chromeless"].freeze,
32
+ css_path: nil,
33
+ render_block: proc { main_content_region }
34
+ }.freeze
35
+
36
+ @registry = {}
37
+
38
+ class << self
39
+ # Register a named layout.
40
+ #
41
+ # @param name [Symbol, String] Layout identifier used as `layout:` in app(...)
42
+ # @param exclusive [Boolean] When true, suppresses default h1/body-padding/#app-container chrome
43
+ # @param body_classes [Array<String>] Extra classes added to <body> (in addition to sw-theme-*)
44
+ # @param css_path [String, nil] Absolute path to a CSS file for this layout
45
+ # @yield Block that is instance_exec'd on AppView — use Phlex DSL + render_slot/main_content_region
46
+ def register(name, exclusive: false, body_classes: [], css_path: nil, &render_block)
47
+ entry = {
48
+ exclusive: exclusive,
49
+ body_classes: Array(body_classes),
50
+ css_path: css_path,
51
+ render_block: render_block
52
+ }
53
+ ComponentAssets.register_file(css_path) if css_path
54
+ @registry[name.to_sym] = entry
55
+ end
56
+
57
+ def [](name)
58
+ @registry[name.to_sym]
59
+ end
60
+
61
+ def registered?(name)
62
+ @registry.key?(name.to_sym)
63
+ end
64
+
65
+ def all
66
+ @registry.dup
67
+ end
68
+
69
+ def chromeless
70
+ CHROMELESS_ENTRY
71
+ end
72
+
73
+ def reset!
74
+ @registry.clear
75
+ end
76
+ end
77
+ end
78
+
79
+ class << self
80
+ # Register a named page layout.
81
+ #
82
+ # @param name [Symbol, String] Layout identifier
83
+ # @param exclusive [Boolean] Opt out of default header/padding/#app-container chrome
84
+ # @param body_classes [Array<String>] Extra <body> classes
85
+ # @param css_path [String, nil] Layout-specific CSS file path
86
+ # @yield Phlex DSL block (instance_exec'd on AppView); call render_slot/main_content_region
87
+ def register_layout(name, exclusive: false, body_classes: [], css_path: nil, &block)
88
+ LayoutRegistry.register(name, exclusive: exclusive, body_classes: body_classes,
89
+ css_path: css_path, &block)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "env"
4
+
5
+ module StreamWeaver
6
+ module Opal
7
+ # Wires an OpalRuntime to window.SWRuntime in the browser.
8
+ # Registers delegated event listeners so no inline JS is needed in HTML.
9
+ #
10
+ # Every line below needs a DOM -- `window`, `document`, and event
11
+ # delegation -- so installing is a no-op where there is none. That is what
12
+ # lets `app()` stay a single unconditional call site while the same bundle
13
+ # also loads in Node, where the string path (StringBridge) takes over.
14
+ class OpalBridge
15
+ def initialize(runtime)
16
+ @runtime = runtime
17
+ end
18
+
19
+ def install
20
+ # :nocov:
21
+ return unless Env.dom?
22
+ runtime = @runtime
23
+ %x{
24
+ window.SWRuntime = {
25
+ start: function() {
26
+ #{runtime.render_and_patch}
27
+ document.addEventListener('click', function(e) {
28
+ var el = e.target.closest('[data-sw-invoke]');
29
+ if (el) #{runtime.invoke_and_patch(`el.dataset.swInvoke`)};
30
+ });
31
+ document.addEventListener('click', function(e) {
32
+ var el = e.target.closest('[data-sw-action]');
33
+ if (el && el.dataset.swAction === 'toggle-theme') {
34
+ if (typeof swToggleTheme === 'function') swToggleTheme();
35
+ }
36
+ });
37
+ document.addEventListener('input', function(e) {
38
+ var key = e.target.dataset && e.target.dataset.swUpdate;
39
+ if (key) #{runtime.update_and_patch(`key`, `e.target.value`)};
40
+ });
41
+ document.addEventListener('change', function(e) {
42
+ var key = e.target.dataset && e.target.dataset.swToggle;
43
+ if (key) #{runtime.update_and_patch(`key`, `e.target.checked`)};
44
+ });
45
+ }
46
+ };
47
+ }
48
+ # :nocov:
49
+ end
50
+ end
51
+ end
52
+ end