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,1528 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # StreamWeaver Interactive Tutorial
5
+ # A self-documenting app that teaches StreamWeaver using StreamWeaver itself.
6
+ #
7
+ # Run with: streamweaver tutorial
8
+ # (Or: ruby examples/advanced/tutorial.rb)
9
+
10
+ require_relative '../../lib/stream_weaver'
11
+
12
+ # Source identifier for tracking apps in service
13
+ SOURCE = "tutorial"
14
+
15
+ # Track loaded apps: section_id => { app_id:, aliased_url: }
16
+ LOADED_APPS = {}
17
+
18
+ # CodeMirror 5 CDN URLs
19
+ CODEMIRROR_CSS = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css"
20
+ CODEMIRROR_JS = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"
21
+ CODEMIRROR_RUBY = "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/ruby/ruby.min.js"
22
+
23
+ # Cleanup all loaded apps on exit
24
+ at_exit do
25
+ next if LOADED_APPS.empty?
26
+ puts "\nCleaning up #{LOADED_APPS.size} tutorial playground(s)..."
27
+ # Use a temporary includer to call ServiceClient methods
28
+ Object.new.extend(StreamWeaver::ServiceClient).clear_source_via_service(SOURCE)
29
+ end
30
+
31
+ # Section data object (DHH-style: prefer objects over hashes)
32
+ Section = Data.define(:id, :nav_title, :title, :content, :code) do
33
+ def state_key = :"#{id}_edited_code"
34
+ end
35
+
36
+ # =============================================================================
37
+ # SECTIONS - Each section is self-contained for easy editing
38
+ # =============================================================================
39
+
40
+ module Sections
41
+ PHILOSOPHY = Section.new(
42
+ id: :philosophy,
43
+ nav_title: "Philosophy",
44
+ title: "Why StreamWeaver?",
45
+ content: <<~MD,
46
+ ## Express Intent, Nothing Else
47
+
48
+ Think about what a simple UI actually requires: some text, a few inputs,
49
+ maybe a dropdown, a button. **That's it.** That's what you're trying to build.
50
+
51
+ But to get there you're dealing with HTML structure, CSS styling, JavaScript,
52
+ controllers, state management...
53
+
54
+ ### The Ruby Way
55
+
56
+ Ruby's beauty is that you can express your intent and nothing else.
57
+ StreamWeaver brings that philosophy to UI:
58
+
59
+ > "I want a title, an input, and a button that does something."
60
+
61
+ That's exactly what you write. No more, no less.
62
+
63
+ ### Token Efficiency for AI
64
+
65
+ When building with Claude Code or other AI assistants, this minimal approach
66
+ pays off even more:
67
+
68
+ - **Smaller generation = faster + cheaper** - Concise DSL instead of verbose HTML/React
69
+ - **More context** - Less code means more room for your actual problem
70
+ - **Data-only generation** - Pre-build the app, let AI just generate data
71
+ MD
72
+ # Standalone example with real interactivity
73
+ code: <<~RUBY
74
+ header1 "Welcome!"
75
+ text_field :name, placeholder: "Your name"
76
+
77
+ if state[:name].to_s.strip != ""
78
+ text "Hello, \#{state[:name]}!"
79
+ else
80
+ text "Type your name above."
81
+ end
82
+ RUBY
83
+ )
84
+
85
+ HELLO_WORLD = Section.new(
86
+ id: :hello_world,
87
+ nav_title: "Hello World",
88
+ title: "Your First App",
89
+ content: <<~MD,
90
+ ## The Simplest App
91
+
92
+ Every StreamWeaver app starts with `app` and a title.
93
+ Inside the block, you describe your UI.
94
+
95
+ ### Try It
96
+
97
+ Type your name in the input below. Notice how the greeting
98
+ appears automatically - no JavaScript, no event handlers,
99
+ just Ruby.
100
+ MD
101
+ code: <<~RUBY
102
+ header1 "Welcome!"
103
+ text_field :name, placeholder: "Your name"
104
+
105
+ if state[:name].to_s.strip != ""
106
+ text "Hello, \#{state[:name]}!"
107
+ end
108
+ RUBY
109
+ )
110
+
111
+ FOUR_MODES = Section.new(
112
+ id: :four_modes,
113
+ nav_title: "The Four Modes",
114
+ title: "Standalone, Agentic, Service, Canvas/Panel",
115
+ content: <<~MD,
116
+ ## Four Ways To Run a StreamWeaver App
117
+
118
+ Every mode uses the same DSL you just saw - what changes is **how it's
119
+ launched** and **who's driving**. Get oriented before going further:
120
+
121
+ | Mode | Launch | Who it's for |
122
+ |---|---|---|
123
+ | **Standalone** | `ruby app.rb` (calls `.run!`) | A person, in a browser, for the life of the process |
124
+ | **Agentic** | `StreamWeaver.run_once!` inside a script | A script needs one answer, then keeps going |
125
+ | **Service** | `streamweaver run app.rb` | Multiple apps on one server, human `/apps/:slug` URLs |
126
+ | **Canvas / Panel** | `streamweaver panel <session>` + `canvas-push` | An agent (Claude Code) drives live UI mid-conversation |
127
+
128
+ ### When To Use Which
129
+
130
+ - Building a tool a human opens in a tab and uses for a while? **Standalone.**
131
+ - A script needs a single decision from a human before continuing? **Agentic** (`run_once!`).
132
+ - Running several apps side by side with memorable URLs? **Service.**
133
+ - An agent needs to show live, updating UI while it works? **Canvas/Panel.**
134
+
135
+ This tutorial is itself a **standalone** app (`ruby examples/advanced/tutorial.rb`).
136
+ Its "Run" button launches each playground into the **service** - you'll see
137
+ that in action a few lessons from now, and each of the other three modes
138
+ gets its own lesson later on.
139
+ MD
140
+ code: <<~RUBY
141
+ select :demo_mode, [
142
+ "Standalone (ruby app.rb)",
143
+ "Agentic (run_once!)",
144
+ "Service (streamweaver run)",
145
+ "Canvas/Panel (streamweaver panel)"
146
+ ], default: "Standalone (ruby app.rb)"
147
+
148
+ case state[:demo_mode]
149
+ when "Standalone (ruby app.rb)"
150
+ alert(variant: :info) { text 'app("My App") { ... }.run!' }
151
+ when "Agentic (run_once!)"
152
+ alert(variant: :info) { text "StreamWeaver.run_once! { ... } # blocks, returns a Hash" }
153
+ when "Service (streamweaver run)"
154
+ alert(variant: :info) { text "streamweaver run app.rb -> http://localhost:4567/apps/your-app" }
155
+ when "Canvas/Panel (streamweaver panel)"
156
+ alert(variant: :info) { text "streamweaver panel my-session, then canvas-push my-session" }
157
+ end
158
+ RUBY
159
+ )
160
+
161
+ GETTING_INPUT = Section.new(
162
+ id: :getting_input,
163
+ nav_title: "Getting Input",
164
+ title: "Text Fields & State",
165
+ content: <<~MD,
166
+ ## How State Works
167
+
168
+ Every input binds to a **state key**. When you write:
169
+
170
+ ```ruby
171
+ text_field :name
172
+ ```
173
+
174
+ StreamWeaver automatically:
175
+ 1. Creates `state[:name]`
176
+ 2. Syncs the input value with state
177
+ 3. Re-renders when state changes
178
+
179
+ ### Conditional Display
180
+
181
+ Since the app block re-evaluates on state changes, use normal Ruby conditionals:
182
+ MD
183
+ code: <<~RUBY
184
+ text_field :email, placeholder: "Email"
185
+ text_area :message, placeholder: "Your message...", rows: 3
186
+
187
+ if state[:email].to_s.include?("@")
188
+ alert(variant: :success) { text "Valid email!" }
189
+ elsif state[:email].to_s.length > 0
190
+ alert(variant: :warning) { text "Need @ in email" }
191
+ end
192
+ RUBY
193
+ )
194
+
195
+ MAKING_CHOICES = Section.new(
196
+ id: :making_choices,
197
+ nav_title: "Making Choices",
198
+ title: "Selection Components",
199
+ content: <<~MD,
200
+ ## Dropdowns, Checkboxes, Radio Buttons
201
+
202
+ StreamWeaver provides several ways to capture user choices:
203
+
204
+ - **select** - Dropdown menu
205
+ - **checkbox** - Boolean toggle
206
+ - **radio_group** - Single choice from options
207
+ - **checkbox_group** - Multiple selections
208
+ MD
209
+ code: <<~RUBY
210
+ header3 "Preferences"
211
+ select :priority, ["Low", "Medium", "High"], default: "Medium"
212
+ checkbox :urgent, "Mark as urgent"
213
+
214
+ text "Priority: \#{state[:priority]}"
215
+ text "Urgent: \#{state[:urgent] ? 'YES!' : 'no'}"
216
+ RUBY
217
+ )
218
+
219
+ TAKING_ACTION = Section.new(
220
+ id: :taking_action,
221
+ nav_title: "Taking Action",
222
+ title: "Buttons & Callbacks",
223
+ content: <<~MD,
224
+ ## Making Things Happen
225
+
226
+ Buttons execute code when clicked. The callback receives the current state:
227
+
228
+ ```ruby
229
+ button "Click Me" do |state|
230
+ # Your code here
231
+ state[:count] += 1
232
+ end
233
+ ```
234
+
235
+ ### Button Styles
236
+
237
+ Use `style: :secondary` for less prominent actions.
238
+ MD
239
+ code: <<~RUBY
240
+ state[:count] ||= 0
241
+
242
+ header2 "Count: \#{state[:count]}"
243
+
244
+ hstack spacing: :sm do
245
+ button "+" do |s|
246
+ s[:count] += 1
247
+ end
248
+ button "-", style: :secondary do |s|
249
+ s[:count] -= 1
250
+ end
251
+ button "Reset", style: :secondary do |s|
252
+ s[:count] = 0
253
+ end
254
+ end
255
+ RUBY
256
+ )
257
+
258
+ LAYOUT = Section.new(
259
+ id: :layout,
260
+ nav_title: "Layout",
261
+ title: "Cards, Columns & Stacks",
262
+ content: <<~MD,
263
+ ## Organizing Your UI
264
+
265
+ StreamWeaver provides several layout components:
266
+
267
+ - **card** - Styled container with optional header/footer
268
+ - **columns** - Multi-column layouts
269
+ - **vstack/hstack** - Vertical/horizontal stacking
270
+ - **grid** - Responsive grid layouts
271
+
272
+ ### Labeled Card Headers
273
+
274
+ `card_header "Title", badge: "C1", meta: "..."` renders a small mono
275
+ badge before the title and right-aligned meta text after it - handy for
276
+ compact, labeled sections (a status panel, a numbered step, ...).
277
+ MD
278
+ code: <<~RUBY
279
+ columns widths: ['30%', '70%'] do
280
+ column do
281
+ card do
282
+ card_header "Sidebar"
283
+ card_body do
284
+ text "Navigation"
285
+ button "Option 1"
286
+ button "Option 2", style: :secondary
287
+ end
288
+ end
289
+ end
290
+ column do
291
+ card do
292
+ card_header "Main Content", badge: "C1", meta: "content pane"
293
+ card_body do
294
+ text_field :search, placeholder: "Search..."
295
+ text "Wide area for content"
296
+ end
297
+ end
298
+ end
299
+ end
300
+ RUBY
301
+ )
302
+
303
+ TABLES = Section.new(
304
+ id: :tables,
305
+ nav_title: "Tables",
306
+ title: "Displaying Data",
307
+ content: <<~MD,
308
+ ## Tables and Lists
309
+
310
+ Build data displays using divs with CSS grid or flexbox.
311
+
312
+ ### The Pattern
313
+
314
+ Use nested divs with grid styling:
315
+
316
+ ```ruby
317
+ div style: "display: grid; grid-template-columns: repeat(3, 1fr);" do
318
+ # Header row
319
+ div { text "Name" }
320
+ # Data rows...
321
+ end
322
+ ```
323
+
324
+ ### Iteration
325
+
326
+ Loop over data with `.each` to generate rows.
327
+ MD
328
+ code: <<~RUBY
329
+ data = [
330
+ { name: "Alice", role: "Admin", active: "Yes" },
331
+ { name: "Bob", role: "User", active: "Yes" },
332
+ { name: "Charlie", role: "Guest", active: "No" }
333
+ ]
334
+
335
+ # Grid-based table
336
+ div style: "display: grid; grid-template-columns: 1fr 1fr 80px; border: 1px solid #ddd; border-radius: 4px;" do
337
+ # Header
338
+ ["Name", "Role", "Active?"].each do |h|
339
+ div style: "padding: 10px; background: #f5f5f5; font-weight: 600; border-bottom: 2px solid #ddd;" do
340
+ text h
341
+ end
342
+ end
343
+
344
+ # Data rows
345
+ data.each do |row|
346
+ [:name, :role, :active].each do |col|
347
+ div style: "padding: 10px; border-bottom: 1px solid #eee;" do
348
+ text row[col]
349
+ end
350
+ end
351
+ end
352
+ end
353
+ RUBY
354
+ )
355
+
356
+ MODALS = Section.new(
357
+ id: :modals,
358
+ nav_title: "Modals",
359
+ title: "Dialogs & Confirmations",
360
+ content: <<~MD,
361
+ ## Modal Dialogs
362
+
363
+ Use modals for confirmations, forms, or any content that needs focus.
364
+
365
+ ### How Modals Work
366
+
367
+ 1. Define the modal with a unique key
368
+ 2. Set `state[:key_open] = true` to show it
369
+ 3. Use `modal_footer` for action buttons
370
+
371
+ ### Sizes
372
+
373
+ Available sizes: `:sm`, `:md`, `:lg`, `:xl`
374
+ MD
375
+ code: <<~RUBY
376
+ button "Delete Item" do |s|
377
+ s[:confirm_open] = true
378
+ end
379
+
380
+ if state[:confirmed]
381
+ alert(variant: :success) { text "Item deleted!" }
382
+ end
383
+
384
+ modal :confirm, title: "Confirm Delete", size: :sm do
385
+ text "Are you sure you want to delete this item?"
386
+ text "This action cannot be undone."
387
+
388
+ modal_footer do
389
+ button "Delete", style: :primary do |s|
390
+ s[:confirmed] = true
391
+ s[:confirm_open] = false
392
+ end
393
+ button "Cancel", style: :secondary do |s|
394
+ s[:confirm_open] = false
395
+ end
396
+ end
397
+ end
398
+ RUBY
399
+ )
400
+
401
+ THEMES = Section.new(
402
+ id: :themes,
403
+ nav_title: "Themes",
404
+ title: "Custom Styling",
405
+ content: <<~MD,
406
+ ## Themes
407
+
408
+ StreamWeaver theming has two independent layers, both switchable via CSS
409
+ variables and the `style` attribute.
410
+
411
+ ### Built-in Themes (`app theme: :name`)
412
+
413
+ - `:default` - Warm Industrial
414
+ - `:dashboard` - Data Dense
415
+ - `:document` - Reading Mode (serif, generous line-height)
416
+ - `:doc` - Compact Editorial (tight, magazine-like)
417
+
418
+ ### Presets (`theme_preset :name`)
419
+
420
+ A second layer of curated font + color palettes you can drop onto any
421
+ theme with one line: `:editorial`, `:technical`, `:warm`, `:minimal`,
422
+ `:terminal`, and `:sketch` (hand-drawn wireframe look - rough.js borders
423
+ and a Caveat handwriting font).
424
+
425
+ ```ruby
426
+ app "My App", theme: :doc do
427
+ theme_preset :sketch
428
+ # ...
429
+ end.run!
430
+ ```
431
+
432
+ ### Dark Mode, Automatically
433
+
434
+ `theme_toggle mode: :auto` adds a dark/light/auto button that **follows
435
+ the OS `prefers-color-scheme`** out of the box and remembers the user's
436
+ override in `localStorage`. `theme_switcher` renders a full picker over
437
+ every registered theme instead of a single toggle - try both below,
438
+ they actually change this page's theme.
439
+
440
+ ### CSS Variables & Inline Styles
441
+
442
+ Override colors per-app with `theme_overrides:`, or reach for `style:`
443
+ on any component for one-off styling:
444
+ MD
445
+ code: <<~RUBY
446
+ theme_switcher
447
+ theme_toggle mode: :auto
448
+
449
+ # Using CSS variables
450
+ div style: "background: var(--sw-color-primary); color: white; padding: 1rem; border-radius: 8px; margin-top: 0.75rem;" do
451
+ text "Primary colored box"
452
+ end
453
+
454
+ div style: "margin-top: 1rem;" do end
455
+
456
+ # Custom styling
457
+ div style: "background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 1.5rem; border-radius: 12px; color: white;" do
458
+ header3 "Gradient Header"
459
+ text "Custom styled content"
460
+ end
461
+ RUBY
462
+ )
463
+
464
+ NAVIGATION = Section.new(
465
+ id: :navigation,
466
+ nav_title: "Navigation",
467
+ title: "navbar, nav_item, link_to & State Routing",
468
+ content: <<~MD,
469
+ ## Cross-App Navigation
470
+
471
+ - `navbar` - a horizontal nav bar container
472
+ - `nav_item "Label", href: "/path", active: true` - bold and
473
+ non-clickable when active, a link otherwise
474
+ - `link_to "Label", href: "/path"` - a plain inline anchor
475
+
476
+ ```ruby
477
+ navbar do
478
+ nav_item "Dashboard", href: "/", active: true
479
+ nav_item "Settings", href: "/settings"
480
+ end
481
+ link_to "Docs", href: "https://example.com"
482
+ ```
483
+
484
+ ### `route_by` / `route_with` - State Routing, Not HTTP Routing
485
+
486
+ These map a URL to **state**, not to a server-side handler - the same
487
+ StreamWeaver view still renders, just seeded differently.
488
+ `route_by :page, home: "/", settings: "/settings"` gives one state key a
489
+ bidirectional URL mapping; `route_with(parser:, builder:)` handles
490
+ parameterized routes (`/post/:id`) with two lambdas. See
491
+ `docs/routing.md` for the full parser/builder contract - including a
492
+ Common Pitfalls section worth reading once your app grows past a
493
+ handful of routes.
494
+
495
+ If you need a genuine HTTP route that bypasses rendering entirely (a
496
+ webhook, a JSON API, a file download), that's `endpoint` - next lesson.
497
+
498
+ **Heads up:** Run below launches into the multi-app *service*, where
499
+ every app lives under `/apps/:slug`. State routing itself works the
500
+ same way there as standalone - visiting `/apps/:slug/settings`
501
+ correctly seeds `state[:page] = :settings`. What doesn't carry over is
502
+ this snippet's hardcoded `/settings` href, which points above the
503
+ service's `/apps/:slug` scope and will 404 if you click it there. Save
504
+ this code to a file and `ruby` it directly to see the hrefs resolve at
505
+ the real root.
506
+ MD
507
+ code: <<~RUBY
508
+ route_by :page, home: "/", settings: "/settings"
509
+ state[:page] ||= :home
510
+
511
+ navbar do
512
+ nav_item "Home", href: "/", active: state[:page] == :home
513
+ nav_item "Settings", href: "/settings", active: state[:page] == :settings
514
+ end
515
+
516
+ case state[:page]
517
+ when :home
518
+ header3 "Home"
519
+ text "Visiting / sets state[:page] = :home"
520
+ when :settings
521
+ header3 "Settings"
522
+ text "Visiting /settings sets state[:page] = :settings"
523
+ end
524
+ RUBY
525
+ )
526
+
527
+ RESOURCE_DSL = Section.new(
528
+ id: :resource_dsl,
529
+ nav_title: "Resource DSL",
530
+ title: "CRUD Scaffolding",
531
+ content: <<~MD,
532
+ ## One Block, Full CRUD
533
+
534
+ `resource :name, store: MyStore do field ... end` replaces 30-50 lines
535
+ of hand-written routes/state/forms with index, show, new, edit, and
536
+ delete - all deep-linkable.
537
+
538
+ | URL | Action |
539
+ |---|---|
540
+ | `GET /posts` | Index - table with View / Edit / Delete |
541
+ | `GET /posts/new` | New - form, Create button |
542
+ | `GET /post/:id` | Show - card with field values |
543
+ | `GET /post/:id/edit` | Edit - form seeded from the record |
544
+ | Delete button | Inline confirm, then destroys |
545
+
546
+ Stores are duck-typed - any object with `all`, `find`, `create`,
547
+ `update`, `destroy` works. See `docs/resource-dsl.md` for the full
548
+ field-type table, override blocks, and named-route helpers
549
+ (`posts_path`, `post_path(rec)`, ...).
550
+
551
+ Declare `page`/`route` calls **before** `resource` blocks - routing is
552
+ first-registered-wins, so the root path needs its own landing page.
553
+
554
+ **Heads up:** like the Navigation lesson, Run below launches into the
555
+ multi-app service, where every app lives under `/apps/:slug`. Deep
556
+ links like `/apps/:slug/posts` correctly seed the index action there -
557
+ what doesn't carry over is this snippet's hardcoded "View Posts" href,
558
+ which points above the service's `/apps/:slug` scope and will 404 if
559
+ you click it there. Save this code to a file and `ruby` it directly to
560
+ click through the real thing.
561
+ MD
562
+ code: <<~RUBY
563
+ module PostStore
564
+ @posts = [{ id: '1', title: 'Hello', body: 'First post', status: 'published' }]
565
+ def self.all; @posts; end
566
+ def self.find(id); @posts.find { |p| p[:id] == id }; end
567
+ def self.create(attrs)
568
+ id = ((@posts.map { |p| p[:id].to_i }.max || 0) + 1).to_s
569
+ @posts << { id: id, **attrs }; id
570
+ end
571
+ def self.update(id, attrs); post = find(id) or return false; post.merge!(attrs); true; end
572
+ def self.destroy(id); @posts.reject! { |p| p[:id] == id }; true; end
573
+ end
574
+
575
+ page :home, '/' do
576
+ header1 "Blog"
577
+ link_to "View Posts", href: "/posts" # posts_path isn't defined until `resource` below runs
578
+ end
579
+
580
+ resource :post, store: PostStore do
581
+ field :title, :string
582
+ field :body, :text
583
+ field :status, :enum, values: %w[draft published]
584
+ end
585
+ RUBY
586
+ )
587
+
588
+ ENDPOINTS = Section.new(
589
+ id: :endpoints,
590
+ nav_title: "Endpoints",
591
+ title: "The endpoint DSL - a Real HTTP Escape Hatch",
592
+ content: <<~MD,
593
+ ## When You Need Real HTTP
594
+
595
+ `route_by`/`route_with` map URLs to *state* - same page, different
596
+ seed. Sometimes you need a genuine HTTP route instead: a webhook
597
+ receiver, a JSON API, a file download. That's `endpoint`.
598
+
599
+ ```ruby
600
+ app "My App" do
601
+ endpoint :get, "/api/status" do |req|
602
+ { ok: true, uptime: 42 } # Hash -> 200 application/json
603
+ end
604
+ end.run!
605
+ ```
606
+
607
+ - Supported verbs: `:get`, `:post`, `:put`, `:patch`, `:delete`
608
+ - The block receives the raw `Rack::Request`; return a `Hash` (JSON), a
609
+ `String` (HTML), or `[status, headers, body]` for full control
610
+ - Endpoints bypass state/session/CSRF entirely - no StreamWeaver
611
+ machinery, just Rack
612
+ - **Reserved paths**: anything under `/update`, `/action/*`, `/submit`,
613
+ `/event/*`, `/form/*`, `/theme/*`, `/sw/*` always loses to
614
+ StreamWeaver's internal routes - `endpoint` warns at registration
615
+ time if you collide
616
+
617
+ ### This Tutorial Eats Its Own Dog Food
618
+
619
+ This tutorial app registers a real endpoint:
620
+ `endpoint :get, "/tutorial/api/hello"`. With the tutorial running, hit
621
+ it from another terminal (check the startup banner for the actual port
622
+ - StreamWeaver auto-picks one starting at 4567):
623
+
624
+ ```bash
625
+ curl http://127.0.0.1:<PORT>/tutorial/api/hello
626
+ ```
627
+ MD
628
+ code: <<~RUBY
629
+ endpoint :get, "/api/ping" do |req|
630
+ { ok: true, message: "pong", name: req.params["name"] || "world" }
631
+ end
632
+
633
+ header3 "Live Endpoint"
634
+ text "Registered: GET /api/ping"
635
+ text "Click Run, then curl the playground URL's /api/ping"
636
+ RUBY
637
+ )
638
+
639
+ SERVICE_MODE = Section.new(
640
+ id: :service_mode,
641
+ nav_title: "Service Mode",
642
+ title: "One Server, Many Apps, Human URLs",
643
+ content: <<~MD,
644
+ ## Multiple Apps, One Server
645
+
646
+ `streamweaver run app.rb` (or just `streamweaver app.rb`) starts (or
647
+ reuses) a single background service and serves your app at a
648
+ human-readable slug URL like `/apps/sales-dashboard`, derived from the
649
+ app's declared name (falling back to the filename). The old opaque hex
650
+ `/apps/:app_id` URL still resolves too, as a canonical fallback.
651
+
652
+ - Slugs that collide across different files get a numeric suffix
653
+ (`-2`, `-3`, ...)
654
+ - Re-loading the same file reuses its existing slug
655
+ - `streamweaver list` shows every loaded app and its URL;
656
+ `streamweaver remove <app_id>` / `streamweaver clear` tear them down
657
+
658
+ **This tutorial already uses the service** - every "Run" button on a
659
+ code panel loads that playground into the service and hands you back
660
+ its slug URL. That's `streamweaver`'s service doing exactly this job,
661
+ one playground at a time.
662
+ MD
663
+ code: <<~RUBY
664
+ text_field :demo_app_name, placeholder: "App name...", default: "Sales Dashboard"
665
+
666
+ name = state[:demo_app_name].to_s.strip
667
+ name = "Sales Dashboard" if name.empty?
668
+ slug = name.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\\A-+|-+\\z/, "")
669
+
670
+ header3 "Derived slug"
671
+ code_block "/apps/\#{slug}", lang: "text"
672
+ text "A second app with the same name gets /apps/\#{slug}-2, and so on."
673
+ RUBY
674
+ )
675
+
676
+ CANVAS_PANEL = Section.new(
677
+ id: :canvas_panel,
678
+ nav_title: "Canvas / Panel",
679
+ title: "Live UI For Coding Agents",
680
+ content: <<~MD,
681
+ ## A Canvas Claude Code Can Push To
682
+
683
+ Canvas/Panel mode gives an agent (Claude Code or similar) a
684
+ **persistent browser session** it can push UI into and read responses
685
+ from, mid-conversation - no polling, no screenshots.
686
+
687
+ This lesson is a CLI walkthrough, not an in-page demo - canvas needs
688
+ its own terminal and browser pane. Copy these into a real terminal:
689
+
690
+ ```bash
691
+ # 1. Open a panel - splits iTerm2, opens the canvas in the right pane
692
+ # (falls back to your system browser outside iTerm2 / without iterm2_ruby)
693
+ streamweaver panel demo-session
694
+
695
+ # 2. Push some UI to it (DSL via stdin)
696
+ streamweaver canvas-push demo-session <<'RUBY'
697
+ header1 "Working..."
698
+ spinner label: "Crunching numbers"
699
+ RUBY
700
+
701
+ # 3. Ask the user something and block for the answer
702
+ streamweaver canvas-wait demo-session
703
+ # => {"choice":"B"}
704
+
705
+ # High-level one-shot helpers - skip the create/push/wait dance
706
+ streamweaver pick "Pick a branch" "main" "feature/x" "feature/y"
707
+ streamweaver confirm "Deploy to production?"
708
+
709
+ # When you're done
710
+ streamweaver canvas-close demo-session
711
+ ```
712
+
713
+ See `docs/canvas-roadmap.md` and `docs/streamweaver-for-ai-agents.md`
714
+ for the full pattern catalog.
715
+ MD
716
+ code: <<~RUBY
717
+ # This is a shell walkthrough, not a StreamWeaver app - copy it into a
718
+ # terminal rather than clicking Run.
719
+
720
+ # streamweaver panel demo-session
721
+ # streamweaver canvas-push demo-session <<'RUBY'
722
+ # header1 "Working..."
723
+ # spinner label: "Crunching numbers"
724
+ # RUBY
725
+ # streamweaver canvas-wait demo-session
726
+ RUBY
727
+ )
728
+
729
+ PATTERNS = Section.new(
730
+ id: :patterns,
731
+ nav_title: "Patterns",
732
+ title: "Real-World Patterns",
733
+ content: <<~MD,
734
+ ## Multi-Step Forms
735
+
736
+ Combine state and conditionals to build wizards and multi-step flows.
737
+
738
+ ### The Pattern
739
+
740
+ 1. Track current step in state
741
+ 2. Use `case` or `if` to show the right content
742
+ 3. Buttons update the step
743
+
744
+ This same pattern works for:
745
+ - Onboarding flows
746
+ - Checkout processes
747
+ - Survey wizards
748
+ MD
749
+ code: <<~RUBY
750
+ state[:step] ||= 1
751
+
752
+ # Progress indicator
753
+ hstack spacing: :sm do
754
+ [1, 2, 3].each do |n|
755
+ style = state[:step] >= n ? "background: var(--sw-color-primary); color: white;" : "background: #eee; color: #666;"
756
+ div style: "\#{style} width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold;" do
757
+ text n.to_s
758
+ end
759
+ end
760
+ end
761
+
762
+ div style: "margin: 1rem 0;" do end
763
+
764
+ case state[:step]
765
+ when 1
766
+ header3 "Step 1: Your Info"
767
+ text_field :wizard_name, placeholder: "Your name"
768
+ button "Next" do |s|
769
+ s[:step] = 2 if s[:wizard_name].to_s.strip != ""
770
+ end
771
+ when 2
772
+ header3 "Step 2: Preferences"
773
+ select :wizard_plan, ["Free", "Pro", "Enterprise"]
774
+ hstack spacing: :sm do
775
+ button "Back", style: :secondary do |s| s[:step] = 1 end
776
+ button "Next" do |s| s[:step] = 3 end
777
+ end
778
+ when 3
779
+ header3 "Step 3: Confirm"
780
+ text "Name: \#{state[:wizard_name]}"
781
+ text "Plan: \#{state[:wizard_plan] || 'Free'}"
782
+ hstack spacing: :sm do
783
+ button "Back", style: :secondary do |s| s[:step] = 2 end
784
+ button "Submit" do |s| s[:done] = true end
785
+ end
786
+ end
787
+
788
+ if state[:done]
789
+ alert(variant: :success) { text "All done! Welcome, \#{state[:wizard_name]}!" }
790
+ end
791
+ RUBY
792
+ )
793
+
794
+ ALL_SECTIONS = [
795
+ PHILOSOPHY,
796
+ HELLO_WORLD,
797
+ FOUR_MODES,
798
+ GETTING_INPUT,
799
+ MAKING_CHOICES,
800
+ TAKING_ACTION,
801
+ LAYOUT,
802
+ TABLES,
803
+ MODALS,
804
+ THEMES,
805
+ NAVIGATION,
806
+ RESOURCE_DSL,
807
+ ENDPOINTS,
808
+ SERVICE_MODE,
809
+ CANVAS_PANEL,
810
+ PATTERNS
811
+ ].freeze
812
+ end
813
+
814
+ # =============================================================================
815
+ # SERVICE HELPERS - API calls to StreamWeaver service
816
+ # =============================================================================
817
+
818
+ module ServiceHelpers
819
+ include StreamWeaver::ServiceClient
820
+
821
+ def run_playground_via_service(section_id, code)
822
+ # Remove old version if same section was already loaded
823
+ if LOADED_APPS[section_id]
824
+ remove_app_via_service(LOADED_APPS[section_id][:app_id])
825
+ LOADED_APPS.delete(section_id)
826
+ end
827
+
828
+ # Write temp file with full app wrapper
829
+ temp_file = "/tmp/streamweaver_tutorial_#{section_id}.rb"
830
+ full_code = <<~RUBY
831
+ require 'stream_weaver'
832
+
833
+ app "Tutorial: #{section_id}" do
834
+ #{code}
835
+ end
836
+ RUBY
837
+ File.write(temp_file, full_code)
838
+
839
+ # Load via ServiceClient
840
+ result = load_app_via_service(temp_file, source: SOURCE, name: "tutorial/#{section_id}")
841
+
842
+ if result[:ok]
843
+ LOADED_APPS[section_id] = {
844
+ app_id: result[:app_id],
845
+ aliased_url: result[:aliased_url]
846
+ }
847
+ end
848
+
849
+ result
850
+ end
851
+
852
+ def remove_playground_via_service(app_id)
853
+ remove_app_via_service(app_id)
854
+ LOADED_APPS.delete_if { |_id, info| info[:app_id] == app_id }
855
+ end
856
+ end
857
+
858
+ # =============================================================================
859
+ # HELPER MODULE - Reusable rendering helpers
860
+ # =============================================================================
861
+
862
+ module TutorialHelpers
863
+ include ServiceHelpers
864
+
865
+ def code_panel(section_id, original_code)
866
+ # Store edited code in state (initialized in main app)
867
+ state_key = :"#{section_id}_edited_code"
868
+
869
+ # Track if modified
870
+ is_modified = state[state_key] != original_code
871
+
872
+ # Use CodeMirror editor with syntax highlighting
873
+ code_editor state_key, language: :ruby, readonly: false, height: "350px"
874
+
875
+ # Show modified indicator
876
+ if is_modified
877
+ div style: "margin-top: 4px; font-size: 12px; color: #666;" do
878
+ text "Modified"
879
+ end
880
+ end
881
+ end
882
+
883
+ def nav_link(section, current_id)
884
+ is_active = section.id == current_id
885
+
886
+ # Check if modified
887
+ is_modified = state[:"#{section.id}_edited_code"] != section.code
888
+
889
+ # Styled list item instead of button - looks like a ToC
890
+ nav_item_style = <<~CSS.gsub("\n", " ")
891
+ display: block;
892
+ padding: 8px 12px;
893
+ cursor: pointer;
894
+ border-radius: 4px;
895
+ border-left: 3px solid #{is_active ? 'var(--sw-color-primary)' : 'transparent'};
896
+ background: #{is_active ? '#f0f7ff' : 'transparent'};
897
+ color: #{is_active ? '#1a73e8' : '#444'};
898
+ font-weight: #{is_active ? '500' : 'normal'};
899
+ font-size: 14px;
900
+ text-decoration: none;
901
+ border-top: none;
902
+ border-right: none;
903
+ border-bottom: none;
904
+ text-align: left;
905
+ width: 100%;
906
+ CSS
907
+
908
+ # Use stable label for button ID (don't include indicator)
909
+ # Show modified indicator separately in the button content
910
+ div style: "display: flex; align-items: center;" do
911
+ if is_modified
912
+ div style: "width: 6px; height: 6px; background: var(--sw-color-primary); border-radius: 50%; margin-right: 6px;" do end
913
+ end
914
+ button section.nav_title, style: nav_item_style do |s|
915
+ s[:current_section] = section.id
916
+ end
917
+ end
918
+ end
919
+
920
+ def check_syntax(code)
921
+ require 'open3'
922
+ temp_file = "/tmp/sw_tutorial_syntax_#{Process.pid}.rb"
923
+ File.write(temp_file, code)
924
+ _stdout, stderr, status = Open3.capture3("ruby", "-c", temp_file)
925
+ File.delete(temp_file) rescue nil
926
+
927
+ if status.success?
928
+ { ok: true, message: "Syntax OK" }
929
+ else
930
+ error = stderr.gsub(temp_file, "code")
931
+ { ok: false, message: error.strip }
932
+ end
933
+ end
934
+
935
+ def code_action_buttons(section, current)
936
+ is_modified = state[:"#{section.id}_edited_code"] != section.code
937
+
938
+ hstack spacing: :xs do
939
+ # Check syntax
940
+ button "Check", style: :secondary do |s|
941
+ result = check_syntax(s[:"#{section.id}_edited_code"])
942
+ if result[:ok]
943
+ s[:syntax_result] = :ok
944
+ else
945
+ s[:syntax_result] = :error
946
+ s[:syntax_error] = result[:message]
947
+ end
948
+ end
949
+
950
+ # Reset this lesson only - ALWAYS render with block for stable ID
951
+ # The block checks is_modified at execution time, not render time
952
+ reset_style = is_modified ? :secondary : "padding: 8px 16px; background: #f5f5f5; color: #aaa; border: 1px solid #ddd; border-radius: 6px; cursor: default;"
953
+ button "Reset", style: reset_style do |s|
954
+ # Only reset if actually modified (check at execution time)
955
+ edited = s[:"#{section.id}_edited_code"]
956
+ if edited != section.code
957
+ s[:"#{section.id}_reset"] = true
958
+ s[:syntax_result] = nil
959
+ end
960
+ end
961
+
962
+ # Run as standalone app
963
+ button "Run" do |s|
964
+ edited_code = s[:"#{section.id}_edited_code"]
965
+ result = check_syntax(edited_code)
966
+ if result[:ok]
967
+ run_result = run_playground_via_service(section.id, edited_code)
968
+ if run_result[:ok]
969
+ open_in_browser(run_result[:url])
970
+ s[:standalone_launched] = section.id
971
+ s[:syntax_result] = nil
972
+ else
973
+ s[:syntax_result] = :error
974
+ s[:syntax_error] = run_result[:error]
975
+ end
976
+ else
977
+ s[:syntax_result] = :error
978
+ s[:syntax_error] = result[:message]
979
+ end
980
+ end
981
+ end
982
+
983
+ # Show running playground link
984
+ if LOADED_APPS[section.id]
985
+ app_info = LOADED_APPS[section.id]
986
+ url = app_info[:aliased_url] || "http://localhost:#{service_port}/apps/#{app_info[:app_id]}"
987
+ div style: "margin-top: 8px; padding: 6px 10px; background: #e8f5e9; border-radius: 4px; font-size: 13px; display: flex; justify-content: space-between; align-items: center;" do
988
+ div style: "color: #2e7d32;" do
989
+ text "Running"
990
+ if app_info[:aliased_url]
991
+ div style: "font-family: monospace; font-size: 11px; color: #666;" do
992
+ text app_info[:aliased_url].sub("http://localhost:#{service_port}", "")
993
+ end
994
+ end
995
+ end
996
+ hstack spacing: :xs do
997
+ button "Open", style: "padding: 2px 8px; font-size: 12px; background: #4caf50; color: white; border: none; border-radius: 3px; cursor: pointer;" do |_s|
998
+ open_in_browser(url)
999
+ end
1000
+ button "Stop", style: "padding: 2px 8px; font-size: 12px; background: transparent; color: #666; border: none; cursor: pointer;" do |s|
1001
+ remove_playground_via_service(app_info[:app_id])
1002
+ LOADED_APPS.delete(section.id)
1003
+ s[:standalone_launched] = nil
1004
+ end
1005
+ end
1006
+ end
1007
+ end
1008
+
1009
+ # Show syntax result
1010
+ if state[:syntax_result] == :ok
1011
+ div style: "margin-top: 8px; padding: 6px 10px; background: #d4edda; color: #155724; border-radius: 4px; font-size: 13px;" do
1012
+ text "Syntax OK"
1013
+ end
1014
+ elsif state[:syntax_result] == :error
1015
+ div style: "margin-top: 8px; padding: 6px 10px; background: #f8d7da; color: #721c24; border-radius: 4px; font-size: 13px; font-family: monospace; white-space: pre-wrap;" do
1016
+ text state[:syntax_error]
1017
+ end
1018
+ end
1019
+ end
1020
+
1021
+ def clear_playgrounds_button
1022
+ # Always render for stable button IDs
1023
+ if LOADED_APPS.any?
1024
+ button "Clear Playgrounds", style: :secondary do |s|
1025
+ LOADED_APPS.each { |_id, info| remove_playground_via_service(info[:app_id]) }
1026
+ LOADED_APPS.clear
1027
+ s[:standalone_launched] = nil
1028
+ end
1029
+ else
1030
+ # Disabled placeholder for stable ID
1031
+ button "Clear Playgrounds", style: "padding: 8px 16px; background: #f5f5f5; color: #aaa; border: 1px solid #ddd; border-radius: 6px; cursor: default;", submit: false
1032
+ end
1033
+ end
1034
+ end
1035
+
1036
+ # =============================================================================
1037
+ # DEMO RENDERERS - Live demos for each section
1038
+ # =============================================================================
1039
+
1040
+ module DemoRenderers
1041
+ # Dynamic dispatch instead of case statement (DRY)
1042
+ def render_demo(section_id, state)
1043
+ method_name = :"render_#{section_id}_demo"
1044
+ send(method_name, state) if respond_to?(method_name, true)
1045
+ end
1046
+
1047
+ def render_philosophy_demo(state)
1048
+ card do
1049
+ card_header "Try It"
1050
+ card_body do
1051
+ header1 "Welcome!"
1052
+ text_field :demo_philosophy_name, placeholder: "Your name"
1053
+ if state[:demo_philosophy_name].to_s.strip != ""
1054
+ text "Hello, #{state[:demo_philosophy_name]}! Welcome to StreamWeaver."
1055
+ else
1056
+ text "Type your name above to see reactive updates."
1057
+ end
1058
+ end
1059
+ end
1060
+ end
1061
+
1062
+ def render_hello_world_demo(state)
1063
+ card do
1064
+ card_header "Try It"
1065
+ card_body do
1066
+ header1 "Welcome!"
1067
+ text_field :demo_name, placeholder: "Your name"
1068
+ if state[:demo_name].to_s.strip != ""
1069
+ text "Hello, #{state[:demo_name]}!"
1070
+ end
1071
+ end
1072
+ end
1073
+ end
1074
+
1075
+ def render_four_modes_demo(state)
1076
+ card do
1077
+ card_header "Try It"
1078
+ card_body do
1079
+ select :demo_mode_pick, [
1080
+ "Standalone (ruby app.rb)",
1081
+ "Agentic (run_once!)",
1082
+ "Service (streamweaver run)",
1083
+ "Canvas/Panel (streamweaver panel)"
1084
+ ], default: "Standalone (ruby app.rb)"
1085
+
1086
+ case state[:demo_mode_pick]
1087
+ when "Standalone (ruby app.rb)"
1088
+ alert(variant: :info) { text 'app("My App") { ... }.run!' }
1089
+ when "Agentic (run_once!)"
1090
+ alert(variant: :info) { text "StreamWeaver.run_once! { ... } # blocks, returns a Hash" }
1091
+ when "Service (streamweaver run)"
1092
+ alert(variant: :info) { text "streamweaver run app.rb -> http://localhost:4567/apps/your-app" }
1093
+ when "Canvas/Panel (streamweaver panel)"
1094
+ alert(variant: :info) { text "streamweaver panel my-session, then canvas-push my-session" }
1095
+ end
1096
+ end
1097
+ end
1098
+ end
1099
+
1100
+ def render_getting_input_demo(state)
1101
+ card do
1102
+ card_header "Try It"
1103
+ card_body do
1104
+ text_field :demo_email, placeholder: "Email"
1105
+ text_area :demo_message, placeholder: "Your message...", rows: 3
1106
+
1107
+ if state[:demo_email].to_s.include?("@")
1108
+ alert(variant: :success) { text "Valid email format!" }
1109
+ elsif state[:demo_email].to_s.length > 0
1110
+ alert(variant: :warning) { text "Please include @ in email" }
1111
+ end
1112
+ end
1113
+ end
1114
+ end
1115
+
1116
+ def render_making_choices_demo(state)
1117
+ card do
1118
+ card_header "Try It"
1119
+ card_body do
1120
+ select :demo_priority, ["Low", "Medium", "High"], default: "Medium"
1121
+ checkbox :demo_urgent, "Mark as urgent"
1122
+ radio_group :demo_category, ["Bug", "Feature", "Question"]
1123
+
1124
+ if state[:demo_priority] || state[:demo_category]
1125
+ vstack spacing: :sm do
1126
+ text "Priority: #{state[:demo_priority] || 'Not set'}"
1127
+ text "Urgent: #{state[:demo_urgent] ? 'Yes' : 'No'}"
1128
+ text "Category: #{state[:demo_category] || 'Not set'}"
1129
+ end
1130
+ end
1131
+ end
1132
+ end
1133
+ end
1134
+
1135
+ def render_taking_action_demo(state)
1136
+ card do
1137
+ card_header "Try It"
1138
+ card_body do
1139
+ state[:demo_count] ||= 0
1140
+
1141
+ header2 "Count: #{state[:demo_count]}"
1142
+
1143
+ hstack spacing: :sm do
1144
+ button "Increment" do |s|
1145
+ s[:demo_count] += 1
1146
+ end
1147
+ button "Decrement", style: :secondary do |s|
1148
+ s[:demo_count] -= 1
1149
+ end
1150
+ button "Reset", style: :secondary do |s|
1151
+ s[:demo_count] = 0
1152
+ end
1153
+ end
1154
+ end
1155
+ end
1156
+ end
1157
+
1158
+ def render_layout_demo(_state)
1159
+ card do
1160
+ card_header "Try It"
1161
+ card_body do
1162
+ columns widths: ["40%", "60%"] do
1163
+ column do
1164
+ card do
1165
+ card_body do
1166
+ text "Sidebar (40%)"
1167
+ end
1168
+ end
1169
+ end
1170
+ column do
1171
+ card do
1172
+ card_header "Main Content", badge: "C1", meta: "content pane"
1173
+ card_body do
1174
+ vstack spacing: :sm do
1175
+ text "Main content (60%)"
1176
+ hstack spacing: :sm do
1177
+ alert(variant: :info) { text "Alert 1" }
1178
+ alert(variant: :success) { text "Alert 2" }
1179
+ end
1180
+ end
1181
+ end
1182
+ end
1183
+ end
1184
+ end
1185
+ end
1186
+ end
1187
+ end
1188
+
1189
+ def render_tables_demo(_state)
1190
+ card do
1191
+ card_header "Try It"
1192
+ card_body do
1193
+ data = [
1194
+ { name: "Alice", role: "Admin", active: "Yes" },
1195
+ { name: "Bob", role: "User", active: "Yes" },
1196
+ { name: "Charlie", role: "Guest", active: "No" }
1197
+ ]
1198
+
1199
+ div style: "display: grid; grid-template-columns: 1fr 1fr 80px; border: 1px solid #ddd; border-radius: 4px;" do
1200
+ # Header
1201
+ ["Name", "Role", "Active?"].each do |h|
1202
+ div style: "padding: 10px; background: #f5f5f5; font-weight: 600; border-bottom: 2px solid #ddd;" do
1203
+ text h
1204
+ end
1205
+ end
1206
+
1207
+ # Data rows
1208
+ data.each do |row|
1209
+ [:name, :role, :active].each do |col|
1210
+ div style: "padding: 10px; border-bottom: 1px solid #eee;" do
1211
+ text row[col]
1212
+ end
1213
+ end
1214
+ end
1215
+ end
1216
+ end
1217
+ end
1218
+ end
1219
+
1220
+ def render_modals_demo(state)
1221
+ card do
1222
+ card_header "Try It"
1223
+ card_body do
1224
+ button "Show Confirmation" do |s|
1225
+ s[:demo_modal_open] = true
1226
+ end
1227
+
1228
+ if state[:demo_modal_confirmed]
1229
+ alert(variant: :success) { text "Action confirmed!" }
1230
+ end
1231
+
1232
+ modal :demo_modal, title: "Confirm Action", size: :sm do
1233
+ text "Do you want to proceed with this action?"
1234
+
1235
+ modal_footer do
1236
+ button "Confirm" do |s|
1237
+ s[:demo_modal_confirmed] = true
1238
+ s[:demo_modal_open] = false
1239
+ end
1240
+ button "Cancel", style: :secondary do |s|
1241
+ s[:demo_modal_open] = false
1242
+ end
1243
+ end
1244
+ end
1245
+ end
1246
+ end
1247
+ end
1248
+
1249
+ def render_themes_demo(_state)
1250
+ card do
1251
+ card_header "Try It"
1252
+ card_body do
1253
+ hstack spacing: :sm, align: :center do
1254
+ theme_switcher
1255
+ theme_toggle mode: :auto
1256
+ end
1257
+ div style: "background: var(--sw-color-primary); color: white; padding: 1rem; border-radius: 8px; margin-top: 0.75rem; margin-bottom: 0.5rem;" do
1258
+ text "Primary color box"
1259
+ end
1260
+ div style: "background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 1rem; border-radius: 8px; color: white;" do
1261
+ text "Custom gradient"
1262
+ end
1263
+ end
1264
+ end
1265
+ end
1266
+
1267
+ def render_navigation_demo(_state)
1268
+ card do
1269
+ card_header "Try It"
1270
+ card_body do
1271
+ text "Preview of the navbar look (click Run above to try real state-routing URLs in an isolated playground):"
1272
+ navbar do
1273
+ nav_item "Home", active: true
1274
+ nav_item "Settings"
1275
+ nav_item "Profile"
1276
+ end
1277
+ div style: "margin-top: 0.75rem;" do end
1278
+ link_to "External docs example", href: "https://example.com"
1279
+ end
1280
+ end
1281
+ end
1282
+
1283
+ def render_resource_dsl_demo(_state)
1284
+ card do
1285
+ card_header "Try It"
1286
+ card_body do
1287
+ text "Preview of a generated index view (real resource blocks are deep-linkable; click Run for the live version):"
1288
+ div style: "display: grid; grid-template-columns: 1fr 1fr 160px; border: 1px solid #ddd; border-radius: 4px; margin-top: 0.5rem;" do
1289
+ ["Title", "Status", "Actions"].each do |h|
1290
+ div style: "padding: 8px 10px; background: #f5f5f5; font-weight: 600; border-bottom: 2px solid #ddd;" do
1291
+ text h
1292
+ end
1293
+ end
1294
+ [["Hello", "published"], ["Draft idea", "draft"]].each do |title, status|
1295
+ div(style: "padding: 8px 10px; border-bottom: 1px solid #eee;") { text title }
1296
+ div(style: "padding: 8px 10px; border-bottom: 1px solid #eee;") { text status }
1297
+ div(style: "padding: 8px 10px; border-bottom: 1px solid #eee;") { text "View · Edit · Delete" }
1298
+ end
1299
+ end
1300
+ end
1301
+ end
1302
+ end
1303
+
1304
+ def render_endpoints_demo(_state)
1305
+ card do
1306
+ card_header "Try It", badge: "Live", meta: "runs against this very page"
1307
+ card_body do
1308
+ text "This tutorial registered a real endpoint. From another terminal:"
1309
+ code_block "curl http://127.0.0.1:<PORT>/tutorial/api/hello", lang: "bash"
1310
+ text "(Swap <PORT> for whatever the startup banner printed - StreamWeaver auto-picks a free port starting at 4567.)"
1311
+ end
1312
+ end
1313
+ end
1314
+
1315
+ def render_service_mode_demo(state)
1316
+ card do
1317
+ card_header "Try It"
1318
+ card_body do
1319
+ text_field :demo_app_name, placeholder: "App name...", default: "Sales Dashboard"
1320
+
1321
+ name = state[:demo_app_name].to_s.strip
1322
+ name = "Sales Dashboard" if name.empty?
1323
+ slug = name.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-+|-+\z/, "")
1324
+
1325
+ header3 "Derived slug"
1326
+ code_block "/apps/#{slug}", lang: "text"
1327
+ text "A second app with the same name gets /apps/#{slug}-2, and so on."
1328
+ end
1329
+ end
1330
+ end
1331
+
1332
+ def render_canvas_panel_demo(_state)
1333
+ card do
1334
+ card_header "Try It", meta: "in a real terminal"
1335
+ card_body do
1336
+ alert(variant: :info) { text "Canvas/Panel needs its own terminal + browser pane - there's nothing to click here." }
1337
+ text "Copy the commands from the Code panel into a terminal to try it for real."
1338
+ end
1339
+ end
1340
+ end
1341
+
1342
+ def render_patterns_demo(state)
1343
+ card do
1344
+ card_header "Try It"
1345
+ card_body do
1346
+ state[:demo_step] ||= 1
1347
+
1348
+ # Progress dots
1349
+ hstack spacing: :xs do
1350
+ [1, 2, 3].each do |n|
1351
+ bg = state[:demo_step] >= n ? "var(--sw-color-primary)" : "#ddd"
1352
+ fg = state[:demo_step] >= n ? "white" : "#666"
1353
+ div style: "background: #{bg}; color: #{fg}; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: bold;" do
1354
+ text n.to_s
1355
+ end
1356
+ end
1357
+ end
1358
+
1359
+ div style: "margin: 0.75rem 0;" do end
1360
+
1361
+ case state[:demo_step]
1362
+ when 1
1363
+ text_field :demo_wizard_name, placeholder: "Your name"
1364
+ button "Next" do |s|
1365
+ s[:demo_step] = 2 if s[:demo_wizard_name].to_s.strip != ""
1366
+ end
1367
+ when 2
1368
+ select :demo_wizard_plan, ["Free", "Pro", "Enterprise"]
1369
+ hstack spacing: :sm do
1370
+ button "Back", style: :secondary do |s| s[:demo_step] = 1 end
1371
+ button "Next" do |s| s[:demo_step] = 3 end
1372
+ end
1373
+ when 3
1374
+ text "Name: #{state[:demo_wizard_name]}"
1375
+ text "Plan: #{state[:demo_wizard_plan] || 'Free'}"
1376
+ hstack spacing: :sm do
1377
+ button "Back", style: :secondary do |s| s[:demo_step] = 2 end
1378
+ button "Done" do |s| s[:demo_wizard_done] = true end
1379
+ end
1380
+ end
1381
+
1382
+ if state[:demo_wizard_done]
1383
+ alert(variant: :success) { text "Wizard complete!" }
1384
+ end
1385
+ end
1386
+ end
1387
+ end
1388
+ end
1389
+
1390
+ # =============================================================================
1391
+ # MAIN APP
1392
+ # =============================================================================
1393
+
1394
+ # Check for --reset flag
1395
+ RESET_MODE = ARGV.include?('--reset')
1396
+
1397
+ generated_app = app(
1398
+ "StreamWeaver Tutorial",
1399
+ layout: :fluid,
1400
+ theme: :default,
1401
+ stylesheets: [CODEMIRROR_CSS],
1402
+ scripts: [CODEMIRROR_JS, CODEMIRROR_RUBY],
1403
+ components: [TutorialHelpers, DemoRenderers]
1404
+ ) do
1405
+ # Self-demonstrating endpoint for the "Endpoints" lesson - a real HTTP route
1406
+ # you can curl while the tutorial is running: see Sections::ENDPOINTS.
1407
+ endpoint :get, "/tutorial/api/hello" do |req|
1408
+ { ok: true, message: "Hello from the StreamWeaver tutorial's own endpoint!", name: req.params["name"] || "world" }
1409
+ end
1410
+
1411
+ # Force clear state if --reset flag was passed
1412
+ if RESET_MODE && !state[:_reset_done]
1413
+ state.clear
1414
+ state[:_reset_done] = true
1415
+ state[:current_section] = :philosophy
1416
+ end
1417
+
1418
+ # Initialize state
1419
+ state[:current_section] ||= :philosophy
1420
+
1421
+ # Initialize edited code states for all sections
1422
+ # Check for reset flags (set by Reset button, survives session filtering)
1423
+ Sections::ALL_SECTIONS.each do |section|
1424
+ reset_flag = :"#{section.id}_reset"
1425
+ if state[reset_flag]
1426
+ # Reset was clicked - force original code and clear flag
1427
+ state[:"#{section.id}_edited_code"] = section.code
1428
+ state.delete(reset_flag)
1429
+ else
1430
+ state[:"#{section.id}_edited_code"] ||= section.code
1431
+ end
1432
+ end
1433
+
1434
+
1435
+ # Find current section
1436
+ current = Sections::ALL_SECTIONS.find { |s| s.id == state[:current_section] }
1437
+ current ||= Sections::PHILOSOPHY
1438
+
1439
+ # Header
1440
+ hstack justify: :between, align: :center do
1441
+ header1 "StreamWeaver Tutorial"
1442
+ hstack spacing: :sm, align: :center do
1443
+ text "Learn by doing"
1444
+ clear_playgrounds_button
1445
+ button "Reset All", style: :secondary do |s|
1446
+ # Clear all state including edited code, but stay on current section
1447
+ current_section = s[:current_section]
1448
+ s.clear
1449
+ s[:current_section] = current_section || :philosophy
1450
+ # Set reset flags for all sections (init code will restore originals)
1451
+ Sections::ALL_SECTIONS.each do |section|
1452
+ s[:"#{section.id}_reset"] = true
1453
+ end
1454
+ end
1455
+ end
1456
+ end
1457
+
1458
+ # 3-column layout with stable widths
1459
+ div style: "display: flex; gap: 1.5rem; align-items: flex-start;" do
1460
+ # LEFT: Navigation (fixed width)
1461
+ div style: "flex: 0 0 180px; min-width: 180px;" do
1462
+ div style: "font-weight: 600; font-size: 14px; color: #666; margin-bottom: 8px; padding: 0 12px;" do
1463
+ text "Contents"
1464
+ end
1465
+ div do
1466
+ Sections::ALL_SECTIONS.each do |section|
1467
+ nav_link(section, state[:current_section])
1468
+ end
1469
+ end
1470
+ end
1471
+
1472
+ # MIDDLE: Content (roughly 50/50 with code)
1473
+ div style: "flex: 1 1 auto; min-width: 400px;" do
1474
+ header2 current.title
1475
+ md current.content
1476
+
1477
+ # Live demo
1478
+ header3 "Interactive Demo"
1479
+ render_demo(current.id, state)
1480
+ end
1481
+
1482
+ # RIGHT: Code (flexible - takes remaining space)
1483
+ div style: "flex: 1 1 auto; min-width: 400px;" do
1484
+ # Header row with Code label and action buttons
1485
+ div style: "display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;" do
1486
+ div style: "font-weight: 600; font-size: 14px; color: #666;" do
1487
+ text "Code"
1488
+ end
1489
+ code_action_buttons(current, current)
1490
+ end
1491
+
1492
+ # Code editor in a card
1493
+ card do
1494
+ card_body do
1495
+ code_panel(current.id, current.code)
1496
+ end
1497
+ end
1498
+ end
1499
+ end
1500
+
1501
+ # Footer navigation (use stable labels for button IDs)
1502
+ hstack justify: :between do
1503
+ current_idx = Sections::ALL_SECTIONS.index(current)
1504
+
1505
+ # Previous button - always render with stable label
1506
+ if current_idx > 0
1507
+ prev_section = Sections::ALL_SECTIONS[current_idx - 1]
1508
+ button "Previous", style: :secondary do |s|
1509
+ s[:current_section] = prev_section.id
1510
+ end
1511
+ else
1512
+ button "Previous", style: "padding: 8px 16px; background: #f5f5f5; color: #aaa; border: 1px solid #ddd; border-radius: 6px; cursor: default;", submit: false
1513
+ end
1514
+
1515
+ # Next button - always render with stable label
1516
+ if current_idx < Sections::ALL_SECTIONS.length - 1
1517
+ next_section = Sections::ALL_SECTIONS[current_idx + 1]
1518
+ button "Next", style: :primary do |s|
1519
+ s[:current_section] = next_section.id
1520
+ end
1521
+ else
1522
+ button "Next", style: "padding: 8px 16px; background: #f5f5f5; color: #aaa; border: 1px solid #ddd; border-radius: 6px; cursor: default;", submit: false
1523
+ end
1524
+ end
1525
+ end
1526
+
1527
+ # Always run - tutorial command runs this standalone
1528
+ generated_app.run!