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,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'rbconfig'
6
+
7
+ module StreamWeaver
8
+ # Client module for interacting with the StreamWeaver service.
9
+ # Include this in tools that load apps via the service API.
10
+ #
11
+ # @example
12
+ # class MyTool
13
+ # include StreamWeaver::ServiceClient
14
+ #
15
+ # def run_example(file_path)
16
+ # result = load_app_via_service(file_path, source: "my_tool")
17
+ # open_in_browser(result[:url]) if result[:ok]
18
+ # end
19
+ # end
20
+ module ServiceClient
21
+ # Get the service port from PID file or default
22
+ #
23
+ # @return [Integer] The port number
24
+ def service_port
25
+ info = Service.read_pid_file
26
+ info ? info[:port] : Service::DEFAULT_PORT
27
+ end
28
+
29
+ # Open a URL in the default browser (OS-aware)
30
+ #
31
+ # @param url [String] The URL to open
32
+ # Guarded here, at the root -- see StreamWeaver::CLI.open_browser and
33
+ # SinatraApp.open_browser for the same guard on the other two
34
+ # independent browser-open implementations in this codebase.
35
+ def open_in_browser(url)
36
+ return if ENV['SW_NO_OPEN']
37
+
38
+ case RbConfig::CONFIG['host_os']
39
+ when /darwin|mac os/
40
+ system('open', url)
41
+ when /linux|bsd/
42
+ system('xdg-open', url)
43
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
44
+ system('start', url)
45
+ end
46
+ end
47
+
48
+ # Load an app via the service API
49
+ #
50
+ # @param file_path [String] Path to the Ruby file defining the app
51
+ # @param source [String] Source identifier for tracking (e.g., "tutorial", "examples_browser")
52
+ # @param name [String, nil] Optional display name
53
+ # @return [Hash] Result with :ok, :app_id, :url, :aliased_url, or :error
54
+ def load_app_via_service(file_path, source:, name: nil)
55
+ expanded_path = File.expand_path(file_path)
56
+
57
+ uri = URI("http://localhost:#{service_port}/load-app")
58
+ params = { file_path: expanded_path, source: source }
59
+ params[:name] = name if name
60
+
61
+ response = Net::HTTP.post_form(uri, params)
62
+ result = JSON.parse(response.body)
63
+
64
+ if result['success']
65
+ {
66
+ ok: true,
67
+ app_id: result['app_id'],
68
+ name: result['name'],
69
+ url: "http://localhost:#{service_port}#{result['url']}",
70
+ aliased_url: result['aliased_url'] ? "http://localhost:#{service_port}#{result['aliased_url']}" : nil
71
+ }
72
+ else
73
+ { ok: false, error: result['error'] }
74
+ end
75
+ rescue Errno::ECONNREFUSED
76
+ { ok: false, error: "Service not running" }
77
+ rescue => e
78
+ { ok: false, error: e.message }
79
+ end
80
+
81
+ # Remove an app from the service
82
+ #
83
+ # @param app_id [String] The app ID to remove
84
+ # @return [Hash] Result with :ok key for consistency
85
+ def remove_app_via_service(app_id)
86
+ uri = URI("http://localhost:#{service_port}/remove-app")
87
+ response = Net::HTTP.post_form(uri, { app_id: app_id })
88
+ result = JSON.parse(response.body)
89
+ { ok: result['success'] }
90
+ rescue Errno::ECONNREFUSED, SocketError, Net::OpenTimeout
91
+ { ok: false }
92
+ end
93
+
94
+ # Clear all apps from a specific source
95
+ #
96
+ # @param source [String] The source identifier to clear
97
+ def clear_source_via_service(source)
98
+ uri = URI("http://localhost:#{service_port}/clear-source")
99
+ Net::HTTP.post_form(uri, { source: source })
100
+ rescue Errno::ECONNREFUSED, SocketError, Net::OpenTimeout
101
+ # Service might be down, that's ok during cleanup
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack/session/abstract/id'
4
+ require 'fileutils'
5
+
6
+ module StreamWeaver
7
+ # ── Session state filters ─────────────────────────────────────────────────
8
+ # Each store type owns its filter logic. Cookie store guards the 4KB limit;
9
+ # file store passes state through unchanged.
10
+
11
+ module SessionStore
12
+ class Base
13
+ # Flash is one-shot (flash-prg.md §1): visible in the response that set
14
+ # it, never persisted for the next request. Excluding it here -- at the
15
+ # single point every filter subclass already funnels through -- is what
16
+ # makes that true; the caller's `state` object itself is untouched, so
17
+ # the render that already read it for *this* response is unaffected.
18
+ FLASH_KEY = :_flash
19
+
20
+ def filter(state, app_transient: [], scope_names: [])
21
+ raise NotImplementedError, "#{self.class}#filter not implemented"
22
+ end
23
+
24
+ private
25
+
26
+ # Treat nil and empty string as blank — both are equivalent to "unset" for form fields.
27
+ def blank?(v)
28
+ v.nil? || v == ""
29
+ end
30
+
31
+ # Recurse exactly one level into hash values that are registered scopes
32
+ # (App#scope_names) so a blank field buried inside e.g. state[:person_form]
33
+ # gets stripped too, same as a blank top-level key — FAC-P3.1 §7. Arbitrary
34
+ # user-managed hash values that were never declared via scope/form/resource
35
+ # are left opaque, exactly as before.
36
+ def strip_scoped_blanks(state, scope_names)
37
+ return state if scope_names.empty?
38
+ state.each_with_object({}) do |(k, v), result|
39
+ result[k] = (scope_names.include?(k) && v.is_a?(Hash)) ? v.reject { |_, sv| blank?(sv) } : v
40
+ end
41
+ end
42
+ end
43
+
44
+ class FileStore < Base
45
+ # No size limit, but strip blank values — they'll be re-initialized by ||= on next load.
46
+ def filter(state, app_transient: [], scope_names: [])
47
+ strip_scoped_blanks(state, scope_names).reject { |k, v| blank?(v) || k == FLASH_KEY }
48
+ end
49
+ end
50
+
51
+ class CookieStore < Base
52
+ HARD_TRANSIENT = %i[code_content current_file_path examples _deck_state].freeze
53
+ LIMIT_BYTES = 4096
54
+ WARN_THRESHOLD = 3072 # warn at 75% capacity
55
+
56
+ def filter(state, app_transient: [], scope_names: [])
57
+ filtered = strip_scoped_blanks(state, scope_names).reject do |k, v|
58
+ blank?(v) ||
59
+ k == FLASH_KEY ||
60
+ HARD_TRANSIENT.include?(k) ||
61
+ app_transient.include?(k) ||
62
+ k.to_s.end_with?('_edited_code')
63
+ end
64
+ size = JSON.dump(filtered).bytesize
65
+ if size > WARN_THRESHOLD
66
+ $stderr.puts "[SW] WARNING: cookie session #{size}B / #{LIMIT_BYTES}B limit — " \
67
+ "set SW_SESSION_STORE=file to avoid silent data loss"
68
+ end
69
+ filtered
70
+ end
71
+ end
72
+
73
+ def self.build(store_name)
74
+ case store_name.to_s
75
+ when 'file' then FileStore.new
76
+ when 'cookie' then CookieStore.new
77
+ else raise ArgumentError, "Unknown SW_SESSION_STORE '#{store_name}'. Use 'file' or 'cookie'."
78
+ end
79
+ end
80
+ end
81
+
82
+ # ── File-backed Rack session middleware ────────────────────────────────────
83
+ # Stores one Marshal file per session under SW_SESSION_DIR.
84
+ # Compatible with Rack::Session::Abstract::PersistedSecure (same interface as Pool).
85
+
86
+ class FileSession < Rack::Session::Abstract::PersistedSecure
87
+ DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS.merge(
88
+ drop: false,
89
+ allow_fallback: true,
90
+ expire_after: 86_400
91
+ )
92
+
93
+ def initialize(app, options = {})
94
+ @session_dir = options.delete(:path) { ::File.join(Dir.home, '.config', 'stream_weaver', 'sessions') }
95
+ @expire_after = options.fetch(:expire_after, DEFAULT_OPTIONS[:expire_after])
96
+ @allow_fallback = options.fetch(:allow_fallback, DEFAULT_OPTIONS[:allow_fallback])
97
+ @mutex = Mutex.new
98
+ FileUtils.mkdir_p(@session_dir)
99
+ super
100
+ end
101
+
102
+ def generate_sid(*args, use_mutex: true)
103
+ loop do
104
+ sid = super(*args)
105
+ exists = use_mutex ? @mutex.synchronize { file_exists_for?(sid) } : file_exists_for?(sid)
106
+ break sid unless exists
107
+ end
108
+ end
109
+
110
+ def find_session(req, sid)
111
+ @mutex.synchronize do
112
+ session = sid && fetch_session_data(sid)
113
+ unless session
114
+ sid = generate_sid(use_mutex: false)
115
+ session = {}
116
+ # Don't write here — let write_session handle it so we don't create orphan files
117
+ end
118
+ [sid, session]
119
+ end
120
+ end
121
+
122
+ def write_session(req, session_id, new_session, options)
123
+ @mutex.synchronize do
124
+ write_file(session_id, new_session)
125
+ session_id
126
+ end
127
+ rescue => e
128
+ $stderr.puts "[SW] FileSession write error: #{e}"
129
+ false
130
+ end
131
+
132
+ def delete_session(req, session_id, options)
133
+ @mutex.synchronize do
134
+ remove_file(session_id)
135
+ unless options[:drop]
136
+ sid = generate_sid(use_mutex: false)
137
+ write_file(sid, {})
138
+ sid
139
+ end
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def session_path(sid)
146
+ id = sid.respond_to?(:private_id) ? sid.private_id : sid.to_s
147
+ ::File.join(@session_dir, "session_#{id.gsub(/[^a-zA-Z0-9\-]/, '_')}")
148
+ end
149
+
150
+ def file_exists_for?(sid)
151
+ ::File.exist?(session_path(sid))
152
+ end
153
+
154
+ # Renamed from load_session to avoid overriding Persisted#load_session(req)
155
+ def fetch_session_data(sid)
156
+ session = read_session_file(sid.private_id)
157
+ session ||= read_session_file(sid.public_id) if @allow_fallback && sid.respond_to?(:public_id)
158
+ session
159
+ rescue => e
160
+ $stderr.puts "[SW] FileSession load error: #{e}"
161
+ nil
162
+ end
163
+
164
+ def read_session_file(raw_id)
165
+ path = ::File.join(@session_dir, "session_#{raw_id.to_s.gsub(/[^a-zA-Z0-9\-]/, '_')}")
166
+ return nil unless ::File.exist?(path)
167
+ if ::File.mtime(path) < Time.now - @expire_after
168
+ ::File.delete(path)
169
+ return nil
170
+ end
171
+ Marshal.load(::File.binread(path))
172
+ rescue => e
173
+ $stderr.puts "[SW] FileSession read error for #{raw_id}: #{e}"
174
+ nil
175
+ end
176
+
177
+ def write_file(sid, session)
178
+ ::File.binwrite(session_path(sid), Marshal.dump(session))
179
+ end
180
+
181
+ def remove_file(sid)
182
+ path = session_path(sid)
183
+ ::File.delete(path) if ::File.exist?(path)
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: streamweaver-canvas-safe
3
+ description: Use before building a canvas doc, pushing a Save-as-doc that will be reopened later, or running `streamweaver export` — tells you which components stay honest with no backend behind them (canvas-read, an exported file) versus which ones need the live bridge or a real server, so the doc you hand someone doesn't quietly stop working the moment it leaves the live canvas
4
+ ---
5
+
6
+ # StreamWeaver Canvas-Safe
7
+
8
+ A canvas doc gets rendered in more places than the live canvas it was written for: `canvas-read` opens it with no app session, `streamweaver export` writes it out as a static file with no server at all. Same `.rb`, three runtimes. This skill is the compact version of that story — read it before you reach for a component you haven't checked, and drop into a reference file only when you're actually about to use one of the components it covers.
9
+
10
+ **Full source of truth:** `docs/frontend-only.md` (compatibility table + mechanisms) and `docs/research/frontend-only-matrix.md` (the ~95-row long tail with source lines and repro commands). This skill is a shorter, action-oriented cut of both — when in doubt, they win.
11
+
12
+ ## The three contexts
13
+
14
+ | Ctx | Name | Behind it | htmx? | Alpine? |
15
+ |---|---|---|---|---|
16
+ | A | Live canvas | `Canvas::BridgeServer` + WebSocket, an agent may be listening on `canvas-wait` | yes | yes |
17
+ | B | canvas-read | `Canvas::Reader`, render-only, no app session | yes | yes |
18
+ | C | Exported HTML | nothing — a static file | no | only if `x-data` is present |
19
+
20
+ ## Verdict vocabulary
21
+
22
+ - **WORKS** — full intended function.
23
+ - **DEGRADES** — reduced but honest (disabled control, visible title, read-only state) — the user can see less is on offer.
24
+ - **SILENTLY-DEAD** — renders, looks interactive, does nothing. No error, no visual sign. The one to design out of a doc before it ships.
25
+ - **BREAKS** — errors, blanks, or visibly wrong output.
26
+
27
+ ## Plays well everywhere (WORKS in A, B, and C)
28
+
29
+ Anything that owns its own state — Alpine `x-data` it never asks an enclosing scope for — or is flat markup with no server dependency. This is the safe backbone for any doc that needs to survive `canvas-read` or an export: `text`, `md`, `header1`–`header6`, `card`/`card_header`/`card_body`, `callout`, `table` (incl. `sortable: true`), `collapsible`/`expandable_card`/`dropdown`, `tabs` (eager, default — **not** `url: true`, see `references/tabs-and-navigation.md`), `sidebar_toc`, `mermaid`, the whole chart family (`chart`, `bar_chart`, `line_chart`, `pie_chart`, `sparkline`, `stacked_bar_chart`, `area_chart`, `hbar_chart`, `doughnut_chart` — fixed 2026-08-23, see `references/charts-and-diagrams.md`), `theme_toggle`/`theme_switcher`/`theme_preset`, `copy_button`, `code_block`, `keyboard_shortcuts` (as a legend), `use_stylesheet`, `doc_header`/`doc_section_header`, `badge`/`status_dot`, and every layout primitive (`columns`, `div`, `section`, `grid`, `vstack`/`hstack`, ...).
30
+
31
+ **If a doc is built only from this list, it renders identically in a live canvas, `canvas-read`, and an export.** `examples/canvas-safe-showcase.rb` proves it — see below.
32
+
33
+ ## sendEvent-only-on-live-canvas (WORKS in A only)
34
+
35
+ These dispatch through `window.sendEvent`, which only `Canvas::BridgeServer`'s `cdn_scripts` defines: `button`, `radio_group`, `clickable(action:)`, `menu_item` (action block), `form` submit, `tag_buttons`, `chip_group`. In B (canvas-read) they render honestly `disabled`/`aria-disabled` with an explanatory title — no `ReferenceError`, no self-mutating click. In C (export) htmx never loads, so their `hx-post` fallback never fires either — SILENTLY-DEAD, quiet and invisible. **Use these to build the live-canvas interaction; don't expect them to do anything once the doc is reopened as a saved doc or exported.** Detail + minimal examples: `references/actions-and-buttons.md` and `references/inputs-and-forms.md`. Saving the session **as `.org`** doesn't render them dead — it leaves them out entirely (one `#+STREAMWEAVER_OMITTED: <call>` keyword line each, counted as `omitted` in the save dialog's coverage notice), because a static org document cannot hold a live control. Save as `.rb` when the controls have to come back.
36
+
37
+ ## Needs a real server (`streamweaver run`/`serve` with the routes mounted — not canvas, not canvas-read, not export)
38
+
39
+ - Per-keystroke sync on `text_field`/`text_area`/`date_field`/`checkbox`/`select`/`checkbox_group` (auto-submit `hx-post /update` — 404s everywhere backend-less; the *value itself* still reaches an agent because `button`'s `getFormState()` harvests the DOM on click, but the field's own auto-submit never fires cleanly). See `references/inputs-and-forms.md`.
40
+ - `design_deck` option selection, notes, generate-more — DEGRADES to honestly read-only wherever `deck_server:` isn't set (canvas, canvas-read, export, and `streamweaver serve` alike). See `references/deck.md`.
41
+ - `modal` opening (server-state driven — can close client-side, can't open without a live push), `route_by`/`route`/`page`, `every`/`stream` (SSE — `AppView`-only, no client at all backend-less), `resource`/`form_for` (routed CRUD), `endpoint(...)`, `tabs lazy: true` (deprecated — blank panels, not this skill's concern to fix).
42
+ - `local_asset` / relative `image_block` src / any `/sw-asset/...` reference — BREAKS everywhere except `--inline-images` on export or a colocated file.
43
+
44
+ ## When to load a reference file
45
+
46
+ | Building with... | Read |
47
+ |---|---|
48
+ | `text_field`, `checkbox_group`, `chip_group`, `tag_buttons`, `form` blocks | `references/inputs-and-forms.md` |
49
+ | `button`, `clickable`, `menu_item`, submit buttons | `references/actions-and-buttons.md` |
50
+ | `mermaid`, `chart`/`bar_chart`/etc. | `references/charts-and-diagrams.md` |
51
+ | `tabs`, `collapsible`, `dropdown`, `modal`, `route_by` | `references/tabs-and-navigation.md` |
52
+ | `design_deck` | `references/deck.md` |
53
+
54
+ Each reference file is advice plus a minimal working DSL snippet plus the one gotcha that actually bites — load it only when you're about to use that component, not up front.
55
+
56
+ ## The comprehensive example
57
+
58
+ `examples/canvas-safe-showcase.rb` is a bare DSL body (`streamweaver-doc: v1`, no `app` wrapper — see `streamweaver-doc-builder` for the shared-body pattern) built entirely from the plays-well-everywhere list above. It was verified to render correctly — no silently-dead component, no missing script pairing — in all three contexts: pushed through the real bridge (websocket adapter, context A), served by `Canvas::Reader` (context B, `SW_NO_OPEN=1`, ephemeral port, killed and `lsof`-verified after), and run through `streamweaver export` (context C). Use it as the starting skeleton for a doc that must survive being saved and reopened later, or exported and handed to someone with no server at all.
59
+
60
+ It deliberately does **not** include any sendEvent-only component (button, radio_group, ...) — adding one is a live-canvas-only enhancement to layer on top, not part of the guaranteed-everywhere core. See `references/actions-and-buttons.md` for how to add interactivity honestly.
61
+
62
+ ## Known gotchas (cross-cutting, see `docs/frontend-only.md` for the full list)
63
+
64
+ - **Bare `x-model` with no owning `x-data` is silently-dead everywhere backend-less** — console-clean, no error. Every component above either owns its own `x-data` or uses server-round-trip inputs (which are covered under "needs a real server").
65
+ - **`canvas-wait`'s default only catches `action` events** (`button`, `clickable`, `menu_item`, `form` submit). `tag_buttons` and `chip_group` dispatch `change` — pass `--event change` or `--any`.
66
+ - **A CSP-locked viewer breaks every Alpine-dependent component in an export**, even ones marked WORKS above (tabs, collapsible, dropdown, theme_toggle) — `x-cloak` content never un-hides. `--offline` on export only inlines mermaid, not Alpine.
@@ -0,0 +1,98 @@
1
+ # streamweaver-doc: v1
2
+ # Canvas-safe showcase — every component below is drawn from the
3
+ # plays-well-everywhere list in SKILL.md. Verified to render correctly with
4
+ # no silently-dead component and no missing script pairing in all three
5
+ # backend-less contexts: the live bridge (websocket adapter), Canvas::Reader
6
+ # (canvas-read), and `streamweaver export`. Deliberately carries no
7
+ # sendEvent-only component (button, radio_group, ...) — see
8
+ # references/actions-and-buttons.md for how to layer live-canvas
9
+ # interactivity on top of a doc built from this skeleton.
10
+
11
+ sidebar_toc sections: [
12
+ { id: "overview", label: "Overview" },
13
+ { id: "data", label: "Data" },
14
+ { id: "diagram", label: "Diagram" }
15
+ ]
16
+
17
+ doc_header(
18
+ eyebrow: "streamweaver-canvas-safe · example",
19
+ title: "Canvas-Safe Component Showcase",
20
+ pills: [{ text: "Reference" }, "Renders identically in canvas, canvas-read, and export"]
21
+ )
22
+
23
+ doc_section_header "01", "Overview", id: "overview"
24
+
25
+ md <<~MD
26
+ Every component on this page owns its own state — either it's flat markup with
27
+ no server dependency, or it's Alpine `x-data` that never asks an enclosing
28
+ scope for anything. That's the whole trick: nothing here round-trips, so
29
+ nothing here has anything to lose when the doc is reopened with no backend
30
+ behind it.
31
+ MD
32
+
33
+ callout(variant: :info, title: "Why this matters") do
34
+ text "A doc built only from this component set survives being saved and reopened " \
35
+ "later, or exported and handed to someone with no StreamWeaver server running " \
36
+ "at all — byte-for-byte the same interactivity in every context."
37
+ end
38
+
39
+ card do
40
+ card_header "Status", badge: "Live", meta: "plays-well-everywhere"
41
+ card_body do
42
+ status_dot(status: :green)
43
+ text "All components below: WORKS in canvas, canvas-read, and export"
44
+ md "Collapse the section below to see client-side Alpine state that never touches a server."
45
+ end
46
+ end
47
+
48
+ collapsible "Show implementation notes" do
49
+ md "This panel opens and closes via Alpine `x-show` — no request, no round-trip, " \
50
+ "identical behavior whether a bridge is listening or not."
51
+ end
52
+
53
+ tabs :showcase_tabs do
54
+ tab "Plain tabs" do
55
+ text "Eager, client-side tabs (no url: true) — pure @click state, works everywhere."
56
+ end
57
+ tab "Why not url: true?" do
58
+ text "tabs url: true degrades on canvas (plain client tabs + an agent-only stderr " \
59
+ "warning) but becomes MORE capable in an export (real pushState routing). " \
60
+ "See references/tabs-and-navigation.md."
61
+ end
62
+ end
63
+
64
+ doc_section_header "02", "Data", id: "data"
65
+
66
+ table(
67
+ headers: ["Component", "A live canvas", "B canvas-read", "C export"],
68
+ rows: [
69
+ ["Everything on this page", "WORKS", "WORKS", "WORKS"],
70
+ ["button / clickable / form submit", "WORKS", "DEGRADES", "SILENTLY-DEAD"],
71
+ ["text_field auto-submit", "DEGRADES", "SILENTLY-DEAD", "SILENTLY-DEAD"]
72
+ ],
73
+ sortable: true
74
+ )
75
+
76
+ bar_chart data: { canvas: 3, "canvas-read": 3, export: 3 }
77
+
78
+ doc_section_header "03", "Diagram", id: "diagram"
79
+
80
+ mermaid <<~MERMAID
81
+ graph LR
82
+ A["Same .rb file"] --> B["Live canvas (A)"]
83
+ A --> C["canvas-read (B)"]
84
+ A --> D["streamweaver export (C)"]
85
+ B --> E["Identical render"]
86
+ C --> E
87
+ D --> E
88
+ MERMAID
89
+
90
+ code_block(<<~RUBY, lang: "ruby", copy: true)
91
+ # Pushed to a live canvas...
92
+ streamweaver canvas-push showcase < canvas-safe-showcase.rb
93
+ # ...or exported as a static file...
94
+ streamweaver export canvas-safe-showcase.rb -o showcase.html
95
+ # ...same DSL, same rendered result either way.
96
+ RUBY
97
+
98
+ theme_toggle
@@ -0,0 +1,47 @@
1
+ # Actions and Buttons
2
+
3
+ Seven components dispatch through `window.sendEvent`, which only the live bridge's `cdn_scripts` defines (context A). All seven share the same three-context shape: **WORKS on the live canvas, DEGRADES-honest on canvas-read, SILENTLY-DEAD in an export.** There is no way to make click-driven interactivity survive an export — a static file has nothing to dispatch to. Design around that instead: use these for the live-canvas conversation loop, and lean on the plays-well-everywhere list (SKILL.md) for anything that has to survive being saved and reopened.
4
+
5
+ ## button
6
+
7
+ ```ruby
8
+ button "Submit" do |state|
9
+ # runs when clicked, on the live canvas, with an agent listening on canvas-wait
10
+ end
11
+
12
+ button "Preview only", submit: false # decorative — WORKS in all three contexts, never dispatches
13
+ ```
14
+
15
+ Default `button` dispatches `sendEvent('action', {button: <token>, state: getFormState()})`. With no `canvas-wait` holder listening, the click still fires `showFeedback()` and replaces the container with "✓ Submitted" — the page is gone even though nothing consumed the event, so always have something waiting before you tell a user to click.
16
+
17
+ ## clickable(action:)
18
+
19
+ ```ruby
20
+ clickable(action: :open_row, key: "r1") { text "Row" }
21
+ ```
22
+
23
+ Ported to `sendEvent('action')` (disc-097) — same payload shape as `button`. Can't be natively `disabled` (it's a `<div>`), so on canvas-read it drops `aria-disabled` + no `tabindex` instead of `disabled` — a focusable element that does nothing would be its own lie.
24
+
25
+ ## menu_item (action block)
26
+
27
+ ```ruby
28
+ dropdown do
29
+ menu do
30
+ menu_item("Archive") { |state| state[:archived] = true }
31
+ end
32
+ end
33
+ ```
34
+
35
+ Dispatches `sendEvent('action', {button: 'menu_item_N', ...})`; the menu still closes on dispatch, live canvas only.
36
+
37
+ ## Waiting on the right event
38
+
39
+ `canvas-wait`'s default only catches `action` events (`button`, `clickable`, `menu_item`, `form` submit). `tag_buttons` and `chip_group` dispatch `change` instead — mirroring `radio_group` since they're a state change, not a submission. Pass `--event change` or `--any` if your doc's only dispatching control is one of those two, or the wait will hang forever on an event that's never coming.
40
+
41
+ ## Quoting
42
+
43
+ Author-supplied strings reaching a JS handler (tag labels, menu item text) route through JSON quoting — an apostrophe in a label won't produce a JS syntax error. The one exception is `external_link_button`'s `window.open(url)` call, which is emitted identically in HTTP and canvas mode by design (quoting it would change HTTP-mode output); an unquoted URL there is a known, tracked edge case (disc-107), not something this skill's examples exercise.
44
+
45
+ ## The gotcha
46
+
47
+ None of these components render any different markup between "will dispatch to an agent" and "will replace the page with a submitted-state placeholder because nobody's listening." Before telling a user to click a button, make sure a `canvas-wait` (with the right `--event`) is actually running — otherwise the click looks like it worked and just wasn't heard.
@@ -0,0 +1,38 @@
1
+ # Charts and Diagrams
2
+
3
+ Both families WORK in all three contexts. This is the one area where the fix (chart-export-allowlist, 2026-08-23) is recent enough to be worth restating rather than assuming.
4
+
5
+ ## Charts
6
+
7
+ ```ruby
8
+ bar_chart data: { calendar: 45, news: 120, tasks: 30 }
9
+ hbar_chart data: { "Phase A" => 25, "Phase B" => 45 }
10
+ line_chart data: { jan: 10, feb: 25, mar: 18 }
11
+ pie_chart data: { chrome: 60, safari: 25, firefox: 15 }
12
+ sparkline data: [3, 7, 4, 9, 2, 8]
13
+ stacked_bar_chart data: [{ label: "Q1", series: { a: 10, b: 20 } }]
14
+ area_chart data: { jan: 10, feb: 25, mar: 18 }
15
+ doughnut_chart data: { chrome: 60, safari: 25, firefox: 15 }
16
+ chart type: :bar, data: { a: 1, b: 2 } # the base component directly
17
+ ```
18
+
19
+ Every one of the shorthand methods above builds a `ChartBase` subclass under the hood, not `Components::Chart` itself. Before 2026-08-23, `streamweaver export`'s CDN-inclusion check tested `is_a?(Components::Chart)` — so every shorthand method failed the check while `chart type: ...` passed it. The exported markup still carried the guarded `x-init="if (typeof Chart !== 'undefined') { new Chart(...) }"`, so nothing threw; you got an empty bordered box, console-silent, that looked like a rendering glitch rather than a missing library. **Fixed:** the gate now checks `components_include?(Components::Chart) || components_include?(Components::ChartBase)` — the whole family, keyed on the base class so a future chart subclass can't silently join the dead list the way the shorthand methods did.
20
+
21
+ ## Mermaid
22
+
23
+ ```ruby
24
+ mermaid <<~MERMAID
25
+ graph LR
26
+ A["Calendar"] --> B["Sync script"] --> C["state.yaml"]
27
+ MERMAID
28
+
29
+ mermaid diagram_code, zoom: true # pan/zoom controls
30
+ mermaid diagram_code, compact: true # reduced padding for card embedding
31
+ mermaid diagram_code, layout: :elk # note: --offline export has no global ELK build
32
+ ```
33
+
34
+ `sw-mermaid-zoom.js` (the interaction engine) is inlined and travels into every export; the mermaid *library itself* is CDN-referenced by default. `streamweaver export --offline` inlines the mermaid library so diagrams survive a CSP-locked viewer — but that flag covers mermaid only, not Alpine or Prism, and it doesn't help `layout: :elk` (no global ELK build to inline).
35
+
36
+ ## The gotcha
37
+
38
+ Don't assume "WORKS in all three contexts" from this file also means "survives a CSP-locked viewer with no flags." A default (non-`--offline`) export still loads the chart/mermaid CDN scripts over the network — a CSP that blocks external hosts breaks them exactly like it breaks Alpine. `--offline` closes the mermaid gap; nothing closes the Chart.js one yet if you need a fully offline, CSP-safe chart export.
@@ -0,0 +1,32 @@
1
+ # Design Deck
2
+
3
+ `design_deck` navigation (moving between slides, `slide_container`/`slide`) is plain inlined JS (`sw-slide-nav.js`) — WORKS in every context, no server needed. Everything below is about the interactive part: option selection, notes, generate-more, model selection.
4
+
5
+ ```ruby
6
+ design_deck "Design Options" do
7
+ slide "arch", "Architecture" do
8
+ option("Monolith") { text "Simple, one deploy unit" }
9
+ option("Microservices") { text "Scales independently, more ops" }
10
+ end
11
+ end
12
+ ```
13
+
14
+ ## What changed 2026-08-23 (disc-096)
15
+
16
+ **Before:** `swDeckSelect` applied the selected CSS class and `aria-checked` **before** calling `fetch('/deck/select')`, with no `.catch`. Every backend-less context — canvas, canvas-read, export — has no `/deck/*` route, so the fetch 404s (or fails outright) and the failure was swallowed. The user saw a confirmed selection that was never recorded and never reached anything. Not just dead — it actively lied about recorded state, `aria-checked` included.
17
+
18
+ **After:** confirmation is success-gated. Visual/aria state only changes after the POST resolves; a failed request surfaces a console error naming the route and reason, and applies no visual change. An `aria-busy` state covers the in-flight moment. Separately, the whole interactive surface (option cards, notes textarea, submit, generate-more, model selector) renders **read-only** wherever `/deck/*` genuinely doesn't exist — gated by a `deck_server:` construction flag the adapter carries, set correctly at every render site including canvas, canvas-read, export, and `streamweaver serve` (service mode never mounted `/deck/*` either — a second, previously-undocumented instance of the same bug, now covered too).
19
+
20
+ ## Where it WORKS vs. where it's read-only
21
+
22
+ | Context | Deck interactivity |
23
+ |---|---|
24
+ | `streamweaver run` (real standalone server, `/deck/*` mounted) | WORKS — confirmation lands slightly after the click now (the fix), not before |
25
+ | Live canvas (A) | DEGRADES — honestly read-only, `/deck/*` doesn't exist here |
26
+ | canvas-read (B) | DEGRADES — same |
27
+ | Export (C) | DEGRADES — same |
28
+ | `streamweaver serve` (service mode) | DEGRADES — read-only, same previously-undocumented bug, now covered |
29
+
30
+ ## The gotcha
31
+
32
+ If you're building a doc meant to demo option selection interactively, `design_deck` will render but every click will be inert (honestly, not silently) unless it's opened under a real `streamweaver run` server that mounts `/deck/*`. Don't put deck-selection interactivity in a doc you intend to Save-as-doc and reopen later, or export — it'll render fine and simply not accept input in either context. There's also a separate, still-open gap: the deck JS posts to `/deck/*` by absolute path, ignoring the adapter's `url_prefix` — so even a service-mode app that mounted deck routes under a prefix would post to the wrong place. That's why today's fix makes it read-only rather than pointing it at the prefixed route; url-prefixing the deck JS is a prerequisite for ever flipping `deck_server: true` under `streamweaver serve`.
@@ -0,0 +1,65 @@
1
+ # Inputs and Forms
2
+
3
+ Every plain input (`text_field`, `text_area`, `date_field`, `checkbox`, `select`) auto-submits via a debounced `hx-post /update` on every keystroke or change. That request 404s on every backend-less context (A, B, C) — it needs `streamweaver run`/`serve` with the update route mounted. This is disc-106, tracked and deliberately out of scope; know it going in rather than chase a phantom 404 in the console.
4
+
5
+ The good news: on the live canvas (A), the value still reaches an agent — just not through the field's own auto-submit. `getFormState()` walks the DOM for every `[x-model]` element at the moment some *other* `sendEvent`-carrying control fires (a `button`, `clickable`, a form submit). So a `text_field` sitting next to a `button` works as a value carrier even though its own round-trip never completes.
6
+
7
+ ```ruby
8
+ text_field :city, placeholder: "City"
9
+ button "Submit" do |state|
10
+ # state[:city] is populated from getFormState() on the live canvas
11
+ end
12
+ ```
13
+
14
+ If you don't want the debounced 404 noise on canvas, pass `submit: false` — the field becomes an inert value carrier with no auto-submit at all (DEGRADES honestly instead of failing silently):
15
+
16
+ ```ruby
17
+ text_field :city, submit: false
18
+ ```
19
+
20
+ ## checkbox_group — array harvest, not auto-submit
21
+
22
+ ```ruby
23
+ checkbox_group :selected_items, select_all: "Select All", select_none: "Clear" do
24
+ items.each { |item| item(item.id) { text item.name } }
25
+ end
26
+ # button click elsewhere harvests state[:selected_items] = ["id1", "id3"]
27
+ ```
28
+
29
+ **Fixed 2026-08-23 (disc-098):** the harvest used to collapse every group to the *last* checked item's boolean — confidently wrong data, worse than dead. It's fixed now: items inside a `.checkbox-group` accumulate into an array; a lone `checkbox` outside a group still returns a boolean. `checkbox_group` itself was never ported to `sendEvent` — its auto-submit still 404s everywhere backend-less, same as the plain inputs above. The array only leaves the page when another control's click harvests it.
30
+
31
+ ## chip_group — multi-select, sendEvent-ported
32
+
33
+ ```ruby
34
+ chip_group :langs, %w[ruby js python] # multi: true by default
35
+ chip_group :lang, %w[ruby js python], multi: false # single-select
36
+ ```
37
+
38
+ Ported to `sendEvent('change')` on the live canvas (disc-097) — dispatches `{field: :langs, value: [...], state: getFormState()}` on every click, array-harvest-correct. See `references/actions-and-buttons.md` for the sendEvent-only-on-live-canvas rundown; canvas-read renders it honestly `aria-disabled`, export falls back to a dead `hx-post`.
39
+
40
+ ## tag_buttons — sendEvent-ported
41
+
42
+ ```ruby
43
+ tag_buttons :category, ["Fiction", "Non-fiction", "Mystery"]
44
+ ```
45
+
46
+ Same disposition as `chip_group`: `sendEvent('change')` on the live canvas, honest `aria-disabled` on canvas-read, dead `hx-post` in export.
47
+
48
+ ## form blocks
49
+
50
+ ```ruby
51
+ form :edit_person do
52
+ text_field :name, placeholder: "Name"
53
+ select :status, %w[active paused archived]
54
+ submit "Save" do |form_values|
55
+ # form_values = { name: "...", status: "..." }
56
+ end
57
+ cancel "Cancel"
58
+ end
59
+ ```
60
+
61
+ `submit` dispatches through `sendEvent('action')` on the live canvas (disc-097 — `form` was the one auto-submit-based component that got fully ported, not just harvest-fixed). `cancel` is Alpine-only client-side reset, so it works in all three contexts.
62
+
63
+ ## The gotcha
64
+
65
+ Checking a box or typing into a field, by itself, sends **nothing** anywhere. Only a `sendEvent`-carrying control's click harvests the current DOM state. If your doc's only interactive elements are plain inputs with no `button`/`clickable`/`form submit` alongside them, nothing an agent waits on will ever fire — the fields will look interactive (you can type, you can check boxes) and simply never report anywhere. Always pair inputs with at least one dispatching control.