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,924 @@
1
+ # frozen_string_literal: true
2
+
3
+ # StreamWeaver University -- the course-list canvas `streamweaver get-started`
4
+ # pushes (stories: course-list-canvas, progress-ledger).
5
+ #
6
+ # Dual-mode single file (shared-DSL pattern, same as
7
+ # docs/university/mockups/course_canvas_mockup.rb):
8
+ # canvas: streamweaver canvas-push university < lib/stream_weaver/university/canvas.rb
9
+ # standalone: ruby lib/stream_weaver/university/canvas.rb
10
+ #
11
+ # `CLI.push_get_started_placeholder_canvas` pushes this file's own text
12
+ # (`File.read`) verbatim, the same "one string of Ruby, eval'd fresh on
13
+ # every push" contract canvas-push uses for a file piped over stdin. Every
14
+ # reference here uses `require` (not `require_relative`), because the bridge
15
+ # process evals the pushed text with no filename
16
+ # (lib/stream_weaver/canvas/bridge.rb `handle_push`), so relative resolution
17
+ # has nothing to resolve against -- only the `$LOAD_PATH`-relative absolute
18
+ # form works there.
19
+ #
20
+ # CSS and the body are local variables (`_css`, `_body`), not constants --
21
+ # matching the mockup's own convention. The whole file gets instance_eval'd
22
+ # fresh on every single push to the SAME long-running bridge process; a
23
+ # `CONST = ...` in that position would warn "already initialized constant"
24
+ # on the second push onward (harmless, but noisy in the bridge log). Local
25
+ # vars reassign silently; so does `def self.foo` (Ruby never warns on method
26
+ # redefinition), which is why `step_states` below is a method, not baked
27
+ # into the body.
28
+ #
29
+ # Visual design is docs/university/design-spec.md v2; CSS below is lifted
30
+ # verbatim from docs/university/mockups/course_canvas_mockup.rb (approved
31
+ # as-is) with the review-only `tabs` rules trimmed. The mockup's separate
32
+ # step screen was built as-designed (step-1-canvas-push) and then folded
33
+ # back into an inline Details expansion on this same course list
34
+ # (single-mode, 2026-09-03, design-spec.md's "Revision note") -- most of
35
+ # that CSS lives on under `.uni-step__expansion`, below.
36
+ #
37
+ # Buttons dispatch for real (default `submit: true`, `id:` not `key:`):
38
+ # App#button only derives a key-based id when a block or `action:` is
39
+ # present (lib/stream_weaver/app.rb `button`) -- a bare `key:` with neither
40
+ # falls back to a per-render counter, which isn't a stable target for a
41
+ # listener to parse a step number back out of. `id:` is always used
42
+ # verbatim (sanitized), so `id: "mark-done-3"` becomes a predictable
43
+ # `btn_mark_done_mark-done-3`. The mockup itself uses `submit: false` (fully
44
+ # decorative) because it is a review artifact, not the built app.
45
+
46
+ require 'stream_weaver/university/course'
47
+ require 'stream_weaver/university/progress'
48
+ require 'stream_weaver/university/runner'
49
+
50
+ # Compact `A::B::C` form, not nested `module A; module B; module C`
51
+ # blocks -- deliberately. When this whole file's text is instance_eval'd as
52
+ # a STRING (canvas push / CLI.render_dsl_to_html; see the file header), a
53
+ # nested `module StreamWeaver; ...; end` reopening does NOT reopen the real
54
+ # top-level `StreamWeaver` -- instance_eval(String) makes the definee the
55
+ # caller's singleton class, so `module Foo` there *creates a new shadow
56
+ # constant on that singleton class* rather than finding the existing global
57
+ # one, and every later same-eval reference to `StreamWeaver::*` then
58
+ # resolves against that empty shadow instead. The compact form looks up
59
+ # `StreamWeaver::University` by ordinary constant resolution *before*
60
+ # opening `Canvas`, which finds the real module (already loaded by the
61
+ # `require`s above) and is safe in both this file's require'd-as-a-library
62
+ # use (specs) and its instance_eval'd-as-a-string use (the live push).
63
+ module StreamWeaver::University::Canvas
64
+ # step[:number] => :done / :current / :todo, computed fresh from the
65
+ # ledger every render -- design-spec section 6, items 4 & 8 (resume
66
+ # band + rail + row marks all key off exactly these three values).
67
+ def self.step_states(progress, steps: StreamWeaver::University::Course::GETTING_STARTED_STEPS)
68
+ next_number = steps.find { |s| !progress.done?(s[:number]) }&.dig(:number)
69
+ steps.each_with_object({}) do |step, memo|
70
+ memo[step[:number]] = if progress.done?(step[:number])
71
+ :done
72
+ elsif step[:number] == next_number
73
+ :current
74
+ else
75
+ :todo
76
+ end
77
+ end
78
+ end
79
+
80
+ # "Step N done -- pick up at step N+1." (or, on the last step, the
81
+ # course-complete variant), rendered from `progress.last_done` on the
82
+ # SAME re-push that wrote it -- not a bridge toast. A toast queued
83
+ # alongside this push's HTML both arrive in one ~500ms poll response,
84
+ # and the client unconditionally clears any toast the instant new HTML
85
+ # lands (bridge_server.rb's poll()), so it never has a chance to paint.
86
+ # Putting the message in the HTML itself has no such race.
87
+ def self.mark_done_message(step_number, steps: StreamWeaver::University::Course::GETTING_STARTED_STEPS)
88
+ total = steps.size
89
+ if step_number.to_i >= total
90
+ "Step #{step_number} done -- that's the whole course!"
91
+ else
92
+ "Step #{step_number} done -- pick up at step #{step_number.to_i + 1}."
93
+ end
94
+ end
95
+
96
+ # A plain markdown bullet list from an array of lines -- what
97
+ # "What you should see" renders as, both on the step screen and inline
98
+ # on a course-list row (`md` turns this into the checked `.uni-payoff`
99
+ # list via its own `- ` list syntax).
100
+ def self.bullets(lines)
101
+ lines.map { |line| "- #{line}" }.join("\n")
102
+ end
103
+
104
+ # "Re-run" once step `number`'s prompt has actually gone out before --
105
+ # either it ever landed (requested_at, permanent, stamped only on a
106
+ # :sent status) or the last click on this exact step was a failed/
107
+ # degraded send (last_run, which requested_at never records). Shared by
108
+ # the hero button and each step row's own primary button so the two can
109
+ # never say different things about the same step -- round-6 UAT shipped
110
+ # them as two separate inline copies, and the hero one was missed
111
+ # entirely (Forrest, follow-up). Repeat (done steps) never calls this.
112
+ def self.run_label(progress, last_run, number)
113
+ ever_sent = progress.requested_at(number) || (last_run && last_run['step'].to_i == number)
114
+ ever_sent ? "Re-run" : "Run"
115
+ end
116
+ end
117
+
118
+ _css = <<~CSS
119
+ body {
120
+ /* ---- Grounds: the page is the room, panels are the instruments ------- */
121
+ --uni-page: #E7E4D9;
122
+ --uni-panel: #FDFCF9;
123
+ --uni-bar: #F3F1E8;
124
+ --uni-sunk: #F1EFE6;
125
+ --uni-line: #D6D2C4;
126
+ --uni-line-soft: #E6E3D7;
127
+
128
+ /* ---- State language: one triad, used identically everywhere ---------- */
129
+ --uni-done: #17754A;
130
+ --uni-done-bg: #E2F0E8;
131
+ --uni-now: var(--sw-color-accent);
132
+ --uni-now-bg: #E5ECFC;
133
+ --uni-next: #6E6959;
134
+ --uni-next-bg: #EFECE1;
135
+ --uni-rail-track: #D6D1BF;
136
+
137
+ --uni-ink: var(--sw-color-text);
138
+ --uni-muted: #5F5C54;
139
+ --uni-on-solid: #ffffff;
140
+ --uni-step: 130ms cubic-bezier(0.16, 1, 0.3, 1);
141
+ }
142
+
143
+ html[data-sw-theme="dark"] body {
144
+ --uni-page: #131110;
145
+ --uni-panel: #221F1A;
146
+ --uni-bar: #2A2620;
147
+ --uni-sunk: #1B1815;
148
+ --uni-line: #383327;
149
+ --uni-line-soft: #2C2821;
150
+
151
+ --uni-done: #57C98A;
152
+ --uni-done-bg: #17301F;
153
+ --uni-now-bg: #1C2946;
154
+ --uni-next: #9A9285;
155
+ --uni-next-bg: #2C2821;
156
+ --uni-rail-track: #322D25;
157
+ --uni-muted: var(--sw-color-text-muted);
158
+ --uni-on-solid: #131110;
159
+ }
160
+
161
+ body[class*="sw-layout-"] {
162
+ max-width: 860px; margin: 0 auto;
163
+ padding: 26px 24px 120px;
164
+ background: var(--uni-page);
165
+ font-size: 16px;
166
+ }
167
+
168
+ .uni-sr {
169
+ position: absolute; width: 1px; height: 1px;
170
+ margin: -1px; padding: 0; overflow: hidden;
171
+ clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
172
+ }
173
+ #app-container {
174
+ background: transparent; border: none; box-shadow: none;
175
+ padding: 0; margin: 0;
176
+ }
177
+
178
+ body ::selection {
179
+ background: color-mix(in srgb, var(--uni-now) 24%, transparent);
180
+ color: var(--uni-ink);
181
+ }
182
+ body *:focus-visible {
183
+ outline: 2px solid var(--uni-now);
184
+ outline-offset: 2px;
185
+ border-radius: 4px;
186
+ }
187
+ body { scrollbar-width: thin; scrollbar-color: var(--uni-line) transparent; }
188
+ body ::-webkit-scrollbar { width: 11px; height: 11px; }
189
+ body ::-webkit-scrollbar-thumb {
190
+ background: var(--uni-line); border-radius: 6px;
191
+ border: 3px solid var(--uni-page);
192
+ }
193
+ body ::-webkit-scrollbar-track { background: transparent; }
194
+
195
+ .uni-i {
196
+ display: inline-block; width: 1em; height: 1em;
197
+ background-color: currentColor; flex: none;
198
+ -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
199
+ -webkit-mask-position: center; mask-position: center;
200
+ -webkit-mask-size: contain; mask-size: contain;
201
+ }
202
+ .uni-i--check {
203
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.2 8.4l3.1 3.1 6.5-7'/%3E%3C/svg%3E");
204
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.2 8.4l3.1 3.1 6.5-7'/%3E%3C/svg%3E");
205
+ }
206
+ .uni-i--lock {
207
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3.3' y='6.9' width='9.4' height='6.6' rx='1.4'/%3E%3Cpath d='M5.6 6.9V5.1a2.4 2.4 0 0 1 4.8 0v1.8'/%3E%3C/svg%3E");
208
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3.3' y='6.9' width='9.4' height='6.6' rx='1.4'/%3E%3Cpath d='M5.6 6.9V5.1a2.4 2.4 0 0 1 4.8 0v1.8'/%3E%3C/svg%3E");
209
+ }
210
+
211
+ body .sw-topbar {
212
+ border-bottom: none;
213
+ padding: 0; margin: 0 0 8px;
214
+ background: transparent;
215
+ }
216
+ body .sw-topbar-wordmark {
217
+ font-family: var(--sw-font-display);
218
+ font-size: 27px; font-weight: 600;
219
+ letter-spacing: -0.015em; text-transform: none;
220
+ color: var(--uni-ink);
221
+ }
222
+ body .sw-topbar-crumb,
223
+ body .sw-topbar-separator { font-size: 14px; color: var(--uni-muted); }
224
+ body .sw-topbar-crumb--active { color: var(--uni-ink); font-weight: 650; }
225
+ .uni-tagline {
226
+ display: block; max-width: 62ch;
227
+ font-size: 15px; line-height: 1.55; color: var(--uni-muted);
228
+ margin: 0 0 30px;
229
+ }
230
+
231
+ body .sw-theme-toggle__label { display: none; }
232
+ body .sw-theme-toggle__btn {
233
+ background: var(--uni-panel); border: 1px solid var(--uni-line);
234
+ border-radius: 7px; padding: 7px; box-shadow: none;
235
+ color: var(--uni-muted); line-height: 0;
236
+ transition: border-color var(--uni-step), color var(--uni-step);
237
+ }
238
+ body .sw-theme-toggle__btn:hover { border-color: var(--uni-now); color: var(--uni-now); }
239
+ body .sw-theme-toggle__icon {
240
+ display: inline-block; width: 16px; height: 16px; font-size: 0;
241
+ background-color: currentColor;
242
+ -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
243
+ -webkit-mask-position: center; mask-position: center;
244
+ -webkit-mask-size: contain; mask-size: contain;
245
+ }
246
+ body .sw-theme-toggle__icon:nth-of-type(1) {
247
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M13.4 9.6A5.8 5.8 0 0 1 6.4 2.6a5.8 5.8 0 1 0 7 7z'/%3E%3C/svg%3E");
248
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M13.4 9.6A5.8 5.8 0 0 1 6.4 2.6a5.8 5.8 0 1 0 7 7z'/%3E%3C/svg%3E");
249
+ }
250
+ body .sw-theme-toggle__icon:nth-of-type(2) {
251
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='8' cy='8' r='3.1'/%3E%3Cpath d='M8 1.2v1.6M8 13.2v1.6M1.2 8h1.6M13.2 8h1.6M3.2 3.2l1.1 1.1M11.7 11.7l1.1 1.1M12.8 3.2l-1.1 1.1M4.3 11.7l-1.1 1.1'/%3E%3C/svg%3E");
252
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='8' cy='8' r='3.1'/%3E%3Cpath d='M8 1.2v1.6M8 13.2v1.6M1.2 8h1.6M13.2 8h1.6M3.2 3.2l1.1 1.1M11.7 11.7l1.1 1.1M12.8 3.2l-1.1 1.1M4.3 11.7l-1.1 1.1'/%3E%3C/svg%3E");
253
+ }
254
+
255
+ body .sw-card.uni-course {
256
+ background: var(--uni-panel);
257
+ border: 1px solid var(--uni-line);
258
+ border-left: 1px solid var(--uni-line);
259
+ border-radius: 12px;
260
+ padding: 0; margin: 0 0 14px;
261
+ box-shadow: 0 1px 2px rgba(24, 23, 20, 0.05), 0 6px 18px -10px rgba(24, 23, 20, 0.16);
262
+ overflow: hidden;
263
+ }
264
+ html[data-sw-theme="dark"] body .sw-card.uni-course {
265
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 8px 22px -12px rgba(0, 0, 0, 0.6);
266
+ }
267
+ body .sw-card.uni-course--dormant {
268
+ background: transparent;
269
+ border-color: var(--uni-line);
270
+ box-shadow: none;
271
+ }
272
+
273
+ body .sw-card-header.uni-course__bar {
274
+ display: flex; align-items: center; gap: 12px;
275
+ background: var(--uni-bar);
276
+ border-bottom: 1px solid var(--uni-line);
277
+ padding: 16px 22px; margin: 0;
278
+ }
279
+ body .uni-course--dormant .sw-card-header.uni-course__bar {
280
+ background: transparent; border-bottom: none;
281
+ padding: 13px 20px 0;
282
+ }
283
+ body .sw-card.uni-course--dormant { margin-bottom: 10px; }
284
+ .uni-course__dot {
285
+ width: 9px; height: 9px; border-radius: 50%; flex: none;
286
+ background: var(--uni-now);
287
+ box-shadow: 0 0 0 3.5px color-mix(in srgb, var(--uni-now) 20%, transparent);
288
+ }
289
+ .uni-course--dormant .uni-course__dot {
290
+ background: transparent; border: 1.5px solid var(--uni-next);
291
+ box-shadow: none;
292
+ }
293
+ body.sw-theme-doc h2.uni-course__name {
294
+ font-family: var(--sw-font-body);
295
+ font-size: 20px; font-weight: 700; line-height: 1.2;
296
+ letter-spacing: -0.012em; color: var(--uni-ink);
297
+ margin: 0; padding: 0; border-bottom: none; flex: 1 1 auto;
298
+ }
299
+ body.sw-theme-doc .uni-course--dormant h2.uni-course__name {
300
+ font-size: 18px; font-weight: 650; color: var(--uni-muted);
301
+ }
302
+ .uni-chip {
303
+ font-size: 11.5px; font-weight: 700; letter-spacing: 0.07em;
304
+ text-transform: uppercase; padding: 5px 9px; border-radius: 5px;
305
+ flex: none; white-space: nowrap;
306
+ background: var(--uni-now-bg); color: var(--uni-now);
307
+ }
308
+ .uni-chip--done { background: var(--uni-done-bg); color: var(--uni-done); }
309
+ .uni-chip--soon {
310
+ display: inline-flex; align-items: center; gap: 5px;
311
+ background: var(--uni-next-bg); color: var(--uni-next);
312
+ }
313
+ .uni-chip--soon .uni-i { font-size: 12px; }
314
+ .uni-course__blurb {
315
+ display: block; font-size: 14.5px; line-height: 1.5;
316
+ color: var(--uni-muted); padding: 3px 20px 14px 41px;
317
+ max-width: 70ch;
318
+ }
319
+
320
+ body .sw-card-body.uni-course__body { padding: 0; }
321
+
322
+ .uni-resume {
323
+ background: var(--uni-sunk);
324
+ border-bottom: 1px solid var(--uni-line);
325
+ padding: 22px 22px 20px;
326
+ }
327
+ .uni-resume__lead {
328
+ display: block;
329
+ font-family: var(--sw-font-display);
330
+ font-size: clamp(1.35rem, 3.4vw, 1.55rem);
331
+ font-weight: 600; line-height: 1.16; letter-spacing: -0.016em;
332
+ text-wrap: balance; color: var(--uni-ink); margin: 0 0 5px;
333
+ }
334
+ .uni-resume__sub {
335
+ display: block; max-width: 58ch;
336
+ font-size: 15px; line-height: 1.5; color: var(--uni-muted);
337
+ margin-bottom: 18px;
338
+ }
339
+ .uni-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
340
+ .uni-resume .uni-actions { margin-bottom: 20px; }
341
+
342
+ body .sw-button.uni-btn {
343
+ display: inline-flex; align-items: center; gap: 7px;
344
+ font-family: var(--sw-font-body);
345
+ font-size: 14px; font-weight: 650; line-height: 1;
346
+ padding: 10px 15px; border-radius: 7px;
347
+ border: 1px solid transparent; background: transparent;
348
+ box-shadow: none; text-transform: none;
349
+ color: var(--uni-ink); cursor: pointer;
350
+ transition: background var(--uni-step), color var(--uni-step),
351
+ border-color var(--uni-step), transform var(--uni-step);
352
+ }
353
+ body .sw-button.uni-btn .uni-i { font-size: 14px; }
354
+ body .sw-button.uni-btn--run {
355
+ background: var(--uni-now); color: var(--uni-on-solid);
356
+ padding: 12px 20px; font-size: 15px; font-weight: 650;
357
+ box-shadow: 0 1px 2px color-mix(in srgb, var(--uni-now) 30%, transparent);
358
+ }
359
+ body .sw-button.uni-btn--run:hover {
360
+ background: var(--sw-color-primary-hover); transform: translateY(-1px);
361
+ }
362
+ body .sw-button.uni-btn--outline {
363
+ border-color: var(--uni-line); background: var(--uni-panel);
364
+ color: var(--uni-ink);
365
+ }
366
+ body .sw-button.uni-btn--outline:hover { border-color: var(--uni-now); color: var(--uni-now); }
367
+ body .sw-button.uni-btn--quiet {
368
+ color: var(--uni-muted); font-weight: 600;
369
+ padding: 9px 12px; font-size: 13.5px;
370
+ }
371
+ body .sw-button.uni-btn--quiet:hover { color: var(--uni-now); background: var(--uni-now-bg); }
372
+
373
+ .uni-rail { display: flex; align-items: center; gap: 13px; }
374
+ .uni-rail__track { display: flex; align-items: center; gap: 5px; }
375
+ .uni-rail__seg { width: 30px; height: 6px; border-radius: 3px; background: var(--uni-rail-track); }
376
+ .uni-rail__seg--done { background: var(--uni-done); }
377
+ .uni-rail__seg--current { background: var(--uni-now); width: 40px; }
378
+ .uni-rail__label {
379
+ font-size: 13px; font-weight: 600; color: var(--uni-muted);
380
+ font-variant-numeric: tabular-nums;
381
+ }
382
+
383
+ .uni-step {
384
+ display: grid;
385
+ grid-template-columns: 28px 1fr auto;
386
+ align-items: center;
387
+ gap: 0 15px;
388
+ padding: 15px 22px;
389
+ border-bottom: 1px solid var(--uni-line-soft);
390
+ transition: background var(--uni-step);
391
+ }
392
+ .uni-step:last-child { border-bottom: none; }
393
+ .uni-step__mark {
394
+ width: 28px; height: 28px; border-radius: 50%;
395
+ display: flex; align-items: center; justify-content: center;
396
+ font-size: 13.5px; font-weight: 700;
397
+ font-variant-numeric: tabular-nums;
398
+ }
399
+ .uni-step--done .uni-step__mark {
400
+ background: var(--uni-done); color: var(--uni-on-solid);
401
+ }
402
+ .uni-step--done .uni-step__mark .uni-i { font-size: 16px; }
403
+ .uni-step--current .uni-step__mark {
404
+ background: var(--uni-now); color: var(--uni-on-solid);
405
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--uni-now) 18%, transparent);
406
+ }
407
+ .uni-step--todo .uni-step__mark {
408
+ background: transparent; color: var(--uni-next);
409
+ border: 1.5px solid var(--uni-next);
410
+ }
411
+ body.sw-theme-doc h3.uni-step__title {
412
+ font-family: var(--sw-font-body);
413
+ font-size: 17px; font-weight: 650; line-height: 1.28;
414
+ letter-spacing: -0.008em; color: var(--uni-ink);
415
+ margin: 0 0 3px; padding: 0; border-bottom: none;
416
+ }
417
+ .uni-step__payoff {
418
+ display: block; font-size: 14.5px; line-height: 1.45;
419
+ color: var(--uni-muted); max-width: 54ch;
420
+ }
421
+ .uni-step--current { background: var(--uni-now-bg); }
422
+ body.sw-theme-doc .uni-step--done h3.uni-step__title {
423
+ color: var(--uni-muted); font-weight: 600;
424
+ }
425
+ .uni-step__actions { display: flex; align-items: center; gap: 4px; }
426
+
427
+ /* The row a Run/Repeat click just targeted, expanded in place --
428
+ grid-column spans every track (the row's own 3-column grid otherwise
429
+ auto-places a 4th child back into column 1, the 28px mark column).
430
+ Indented to 43px (28px mark + 15px gap) so it lines up under the
431
+ title/payoff text, not the mark. */
432
+ .uni-step__expect {
433
+ grid-column: 1 / -1;
434
+ margin: 2px 0 4px 43px;
435
+ padding-top: 10px;
436
+ border-top: 1px dashed var(--uni-line-soft);
437
+ }
438
+ .uni-step__expect-label {
439
+ display: block;
440
+ font-size: 12px; font-weight: 700; letter-spacing: 0.06em;
441
+ text-transform: uppercase; color: var(--uni-muted);
442
+ margin-bottom: 6px;
443
+ }
444
+ @media (max-width: 620px) {
445
+ .uni-step__expect { grid-column: 1 / -1; margin-left: 0; }
446
+ }
447
+
448
+ /* A row's full Details expansion -- why it matters, the prompt, the
449
+ payoff checklist, Mark done + a next hint. Single-mode: this replaces
450
+ the old separate step screen outright, so it reuses that screen's own
451
+ content classes (.uni-label, .uni-prose, .uni-promptbox, .uni-payoff)
452
+ rather than a second copy of them. Same grid-column / indent trick as
453
+ .uni-step__expect, above, since it lives in the same 3-column row. */
454
+ .uni-step__expansion {
455
+ grid-column: 1 / -1;
456
+ margin: 4px 0 4px 43px;
457
+ padding-top: 14px;
458
+ border-top: 1px dashed var(--uni-line-soft);
459
+ }
460
+ .uni-step__expansion > .uni-label:first-child { margin-top: 0; }
461
+ .uni-step__expansion-foot {
462
+ display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
463
+ margin-top: 22px;
464
+ }
465
+ @media (max-width: 620px) {
466
+ .uni-step__expansion { grid-column: 1 / -1; margin-left: 0; }
467
+ }
468
+
469
+ /* Report band for the last Run/Repeat click -- what the driver did, or
470
+ why it refused to send. Sits between the resume band and the step
471
+ rows so it's directly under the button that produced it. */
472
+ .uni-run-notice {
473
+ background: var(--uni-now-bg);
474
+ border-bottom: 1px solid var(--uni-line);
475
+ border-left: 3px solid var(--uni-now);
476
+ padding: 15px 22px 16px;
477
+ }
478
+ /* Only one distinction matters visually -- it went out, or it didn't --
479
+ so a status added later styles itself correctly by construction. */
480
+ .uni-run-notice--degraded {
481
+ background: var(--uni-next-bg);
482
+ border-left-color: var(--uni-next);
483
+ }
484
+ .uni-run-notice--sent { border-left-color: var(--uni-done); background: var(--uni-done-bg); }
485
+ .uni-run-notice__msg {
486
+ display: block; max-width: 66ch;
487
+ font-size: 14.5px; line-height: 1.5; font-weight: 600;
488
+ color: var(--uni-ink); margin: 0;
489
+ }
490
+ .uni-run-notice__hint {
491
+ display: block; max-width: 66ch;
492
+ font-size: 13.5px; line-height: 1.5; color: var(--uni-muted);
493
+ margin-top: 10px;
494
+ }
495
+ .uni-run-notice .sw-code-block { margin: 12px 0 0; }
496
+
497
+ .uni-divider {
498
+ display: flex; align-items: center; gap: 14px;
499
+ margin: 38px 0 16px;
500
+ }
501
+ .uni-divider__label {
502
+ font-size: 15px; font-weight: 650; color: var(--uni-ink);
503
+ letter-spacing: -0.005em; flex: none;
504
+ }
505
+ .uni-divider__rule { height: 1px; background: var(--uni-line); flex: 1 1 auto; }
506
+
507
+ .uni-note {
508
+ margin-top: 30px; padding: 16px 20px;
509
+ background: var(--uni-sunk);
510
+ border: 1px solid var(--uni-line-soft);
511
+ border-radius: 10px;
512
+ font-size: 14.5px; line-height: 1.6; color: var(--uni-muted);
513
+ max-width: 74ch;
514
+ }
515
+ .uni-note code {
516
+ font-family: var(--sw-font-mono); font-size: 13.5px;
517
+ background: var(--uni-panel); border: 1px solid var(--uni-line);
518
+ border-radius: 4px; padding: 2px 6px; color: var(--uni-ink);
519
+ }
520
+
521
+ /* The all-done recap -- what the resume band's congratulation opens
522
+ onto. Same card, same body; the step rows below are untouched. */
523
+ .uni-recap {
524
+ padding: 4px 22px 22px;
525
+ border-bottom: 1px solid var(--uni-line);
526
+ }
527
+ .uni-recap > .uni-label:first-child { margin-top: 0; }
528
+ .uni-recap__foot {
529
+ display: flex; align-items: center; gap: 14px;
530
+ margin-top: 22px; flex-wrap: wrap;
531
+ }
532
+
533
+ /* The quiet "start over" escape hatch on the course list itself, for
534
+ anyone resetting before finishing. Hidden once all_done, where the
535
+ recap above offers the same action at its natural place instead. */
536
+ .uni-reset { margin-top: 12px; text-align: right; }
537
+
538
+ /* ---- Step Details expansion (single-mode: inline on a course-list row,
539
+ not a separate screen -- these classes are what the old step screen's
540
+ content used, kept as-is and reused by the expansion body below). --- */
541
+ .uni-label {
542
+ display: block;
543
+ font-size: 15px; font-weight: 650; color: var(--uni-ink);
544
+ letter-spacing: -0.005em; margin: 32px 0 11px;
545
+ }
546
+ .uni-prose { max-width: 66ch; font-size: 16px; line-height: 1.6; color: var(--uni-ink); }
547
+ .uni-prose p { margin: 0 0 0.8em; }
548
+ .uni-prose p:last-child { margin-bottom: 0; }
549
+
550
+ .uni-promptbox {
551
+ border: 1px solid var(--uni-line); border-radius: 9px;
552
+ background: var(--uni-sunk); overflow: hidden; margin: 30px 0 18px;
553
+ }
554
+ .uni-promptbox__bar {
555
+ display: flex; align-items: center; justify-content: space-between; gap: 12px;
556
+ background: var(--uni-bar); border-bottom: 1px solid var(--uni-line);
557
+ padding: 9px 15px;
558
+ font-size: 12px; font-weight: 700; letter-spacing: 0.06em;
559
+ text-transform: uppercase; color: var(--uni-muted);
560
+ }
561
+ .uni-prompt {
562
+ display: block;
563
+ font-family: var(--sw-font-mono);
564
+ font-size: 13px; line-height: 1.65;
565
+ white-space: pre-wrap; color: var(--uni-ink);
566
+ padding: 16px 17px; overflow-x: auto;
567
+ }
568
+
569
+ .uni-payoff ul { list-style: none; padding: 0; margin: 0; max-width: 64ch; }
570
+ /* A hanging indent (padding-left + absolutely positioned check), not a
571
+ grid row -- a grid item split every inline child of the <li> onto its
572
+ own auto-placed row (`.uni-payoff` items come from `md`, so a step
573
+ whose text has any inline markdown -- a `code` span, say -- produces
574
+ more than one child), and the auto-placed rows landed in the 20px
575
+ icon column, wrapping that text one character per line. Position
576
+ keeps the check glyph out of the flow entirely, so the <li> is back to
577
+ one ordinary block wrapping however many inline children it has. */
578
+ .uni-payoff li {
579
+ position: relative;
580
+ font-size: 15px; line-height: 1.5; color: var(--uni-ink);
581
+ padding: 7px 0 7px 31px;
582
+ }
583
+ .uni-payoff li::before {
584
+ content: ""; position: absolute; left: 0; top: 11px;
585
+ width: 15px; height: 15px;
586
+ background-color: var(--uni-done);
587
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.2 8.4l3.1 3.1 6.5-7'/%3E%3C/svg%3E");
588
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.2 8.4l3.1 3.1 6.5-7'/%3E%3C/svg%3E");
589
+ -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
590
+ -webkit-mask-size: contain; mask-size: contain;
591
+ }
592
+
593
+ /* The hint text next to Mark done, inside a row's expansion footer
594
+ (.uni-step__expansion-foot, above) -- "Unlocks step N+1." or "That is
595
+ the whole course." Also used, with .uni-foot__spacer, by the
596
+ completion recap's own footer (.uni-recap__foot) -- kept here since
597
+ the old step-screen-only .uni-foot container is gone, but these two
598
+ were never step-screen-exclusive. */
599
+ .uni-foot__hint { font-size: 14px; color: var(--uni-muted); }
600
+ .uni-foot__spacer { flex: 1 1 auto; }
601
+
602
+ @media (max-width: 700px) {
603
+ body[class*="sw-layout-"] { padding-bottom: 150px; }
604
+ }
605
+ @media (max-width: 620px) {
606
+ .uni-step { grid-template-columns: 28px 1fr; row-gap: 10px; }
607
+ .uni-step > .uni-step__actions { grid-column: 2; justify-self: start; }
608
+ .uni-resume__lead { font-size: 1.3rem; }
609
+ .uni-recap__foot .uni-foot__spacer { display: none; }
610
+ }
611
+
612
+ @media (prefers-reduced-motion: reduce) {
613
+ body * { transition-duration: 0.01ms !important; }
614
+ }
615
+ CSS
616
+
617
+ _body = proc do
618
+ use_theme :doc
619
+ use_layout :default
620
+ use_stylesheet _css
621
+
622
+ # Without this marker, the adapter's showFeedback() replaces the whole
623
+ # page with "✓ Submitted -- You can close this window" on the first click
624
+ # of ANY button (adapter/alpinejs.rb, the else branch of the
625
+ # #sw-canvas-continue lookup). That terminal screen is right for a
626
+ # one-shot form and catastrophic for a control panel meant to be clicked
627
+ # all session: it took out the entire course list on the first Run.
628
+ # With the marker, a click shows this brief message instead, and the
629
+ # listener's re-push swaps the real page back in.
630
+ canvas_continue message: "Working..."
631
+
632
+ steps = StreamWeaver::University::Course::GETTING_STARTED_STEPS
633
+ total = steps.size
634
+ progress = StreamWeaver::University::Progress.load
635
+ states = StreamWeaver::University::Canvas.step_states(progress, steps: steps)
636
+ done_count = progress.done_steps.count { |n| n <= total }
637
+ current_number = steps.find { |s| states[s[:number]] == :current }&.dig(:number)
638
+ all_done = done_count == total
639
+ # Single-mode: there is exactly one screen. Which row (if any) renders
640
+ # its full Details expansion inline comes straight off the ledger, same
641
+ # as done/last_run -- which is also what makes a deep link work, since
642
+ # whoever set this before the canvas was last (re)pushed gets that row
643
+ # auto-expanded on load with no navigation step of its own.
644
+ expanded_number = progress.expanded_step
645
+ # Hoisted above the resume band (not just the run-notice band that reads
646
+ # it below) because the course-list row loop also reads it -- a Run or
647
+ # Repeat click expands that step's "What you should see" inline on its
648
+ # row, right there next to the button the user just pressed.
649
+ last_run = progress.last_run
650
+ # Mutually exclusive with last_run -- mark_done!/record_run! each clear
651
+ # the other, so at most one of the two notice bands below ever renders.
652
+ last_done = progress.last_done
653
+
654
+ topbar(wordmark: "StreamWeaver University") do
655
+ theme_toggle mode: :auto
656
+ end
657
+ phrase "Learn StreamWeaver by running it. Every step is a real app that appears " \
658
+ "in this pane while you stay in your terminal. Your progress is saved as you go, " \
659
+ "so you can close this and pick up right where you left off, even after a reboot -- " \
660
+ "reset any time from the link at the bottom of the list.",
661
+ class: "uni-tagline"
662
+
663
+ # ---- Getting Started: open, and holding its own state --------------------
664
+ card(depth: :elevated, class: "uni-course") do
665
+ card_header(class: "uni-course__bar") do
666
+ div(class: "uni-course__dot") {}
667
+ header2 "Getting Started", class: "uni-course__name"
668
+ chip_label = if all_done
669
+ "Complete"
670
+ elsif done_count.zero?
671
+ "Not started"
672
+ else
673
+ "In progress"
674
+ end
675
+ phrase chip_label, class: "uni-chip#{' uni-chip--done' if all_done}"
676
+ end
677
+
678
+ card_body(class: "uni-course__body") do
679
+ div(class: "uni-resume") do
680
+ if all_done
681
+ phrase "All five steps done.", class: "uni-resume__lead"
682
+ phrase "Nice work -- repeat any step below, or look at what's next on the shelf.",
683
+ class: "uni-resume__sub"
684
+ elsif done_count.zero?
685
+ phrase "Start with step 1.", class: "uni-resume__lead"
686
+ phrase steps.first[:payoff], class: "uni-resume__sub"
687
+ else
688
+ current_step = steps.find { |s| s[:number] == current_number }
689
+ phrase "Pick up at step #{current_number}.", class: "uni-resume__lead"
690
+ phrase current_step[:payoff], class: "uni-resume__sub"
691
+ end
692
+
693
+ div(class: "uni-actions") do
694
+ run_number = current_number || 1
695
+ unless all_done
696
+ hero_label = StreamWeaver::University::Canvas.run_label(progress, last_run, run_number)
697
+ button "#{hero_label} step #{run_number}", id: "hero-run-#{run_number}",
698
+ class: "uni-btn uni-btn--run"
699
+ end
700
+ if done_count.positive? && !all_done
701
+ repeat_number = [run_number - 1, 1].max
702
+ button "Repeat step #{repeat_number}", id: "hero-repeat-#{repeat_number}",
703
+ class: "uni-btn uni-btn--quiet"
704
+ end
705
+ end
706
+
707
+ div(class: "uni-rail") do
708
+ div(class: "uni-rail__track") do
709
+ steps.each do |step|
710
+ seg_class = case states[step[:number]]
711
+ when :done then "uni-rail__seg uni-rail__seg--done"
712
+ when :current then "uni-rail__seg uni-rail__seg--current"
713
+ else "uni-rail__seg"
714
+ end
715
+ div(class: seg_class) {}
716
+ end
717
+ end
718
+ phrase "#{done_count} of #{total} done", class: "uni-rail__label"
719
+ end
720
+ end
721
+
722
+ # Mark-done's confirmation -- "Step N done -- pick up at step N+1."
723
+ # -- reusing the same green/--sent band a successful Run gets.
724
+ # Checked first: mark_done! clears last_run, so the two never both
725
+ # have something to say, but last_done is the more specific read
726
+ # when it's present. Rendered BEFORE the completion recap below on
727
+ # purpose: this is the report of the click that just happened
728
+ # (design-spec's "report band ... sits directly under the button
729
+ # that produced it"), and on the step that finishes the course,
730
+ # that click's own "that's the whole course!" line should read as
731
+ # the lead-in to the recap, not a footnote under it.
732
+ if last_done
733
+ div(class: "uni-run-notice uni-run-notice--sent") do
734
+ phrase StreamWeaver::University::Canvas.mark_done_message(last_done['step']),
735
+ class: "uni-run-notice__msg"
736
+ end
737
+ # What the last Run/Repeat click actually did. On anything but a
738
+ # clean send the prompt itself is offered here with a copy button --
739
+ # the degraded path's whole experience, and the fallback whenever the
740
+ # recorded worker session has gone away (driver-worker-runner
741
+ # criteria 4 and 5). The canvas never talks to iTerm; it reports what
742
+ # the driver wrote to the ledger.
743
+ elsif last_run
744
+ status = last_run['status'].to_s
745
+ run_step = last_run['step'].to_i
746
+ run_sent = StreamWeaver::University::Runner.sent?(status)
747
+ run_prompt = StreamWeaver::University::Course.prompt_for(run_step)
748
+
749
+ div(class: "uni-run-notice uni-run-notice--#{run_sent ? 'sent' : 'degraded'}") do
750
+ phrase StreamWeaver::University::Runner.message_for(status, run_step),
751
+ class: "uni-run-notice__msg"
752
+ if !run_sent && run_prompt
753
+ # The human gets the prompt as written -- line breaks and all.
754
+ # Only the wire gets Runner.one_line's collapsed form, because
755
+ # a clipboard paste has no keystroke-per-newline problem.
756
+ code_block run_prompt, lang: "text", copy: true
757
+ phrase "Copy it, then paste it into the terminal where your agent is " \
758
+ "running and press Enter.",
759
+ class: "uni-run-notice__hint"
760
+ end
761
+ end
762
+ end
763
+
764
+ # The completion recap -- what the resume band's "look at what's
765
+ # next on the shelf" line points at once every step is done. Sits
766
+ # inside this same course-list render (not a separate screen) so
767
+ # "Run/Repeat stays available from the list" is true by
768
+ # construction: the step rows below are untouched.
769
+ if all_done
770
+ div(class: "uni-recap") do
771
+ phrase "What you learned", class: "uni-label"
772
+ md steps.map { |s| "- **Step #{s[:number]}:** #{s[:payoff]}" }.join("\n"),
773
+ class: "uni-payoff"
774
+
775
+ phrase "Where to go next", class: "uni-label"
776
+ md <<~MD, class: "uni-prose"
777
+ - Browse the reference docs in `docs/`.
778
+ - Run `streamweaver showcase` for a tour of every component.
779
+ - Read worked examples in `examples/`.
780
+ - Run `streamweaver tutorial` for the classic component-by-component walkthrough.
781
+ MD
782
+
783
+ div(class: "uni-recap__foot") do
784
+ phrase "Run or Repeat any step below to go through it again.",
785
+ class: "uni-foot__hint"
786
+ div(class: "uni-foot__spacer") {}
787
+ button "Reset course", id: "reset-course", class: "uni-btn uni-btn--quiet"
788
+ end
789
+ end
790
+ end
791
+
792
+ steps.each do |step|
793
+ number = step[:number]
794
+ state = states[number]
795
+ expanded = expanded_number == number
796
+ run_label = StreamWeaver::University::Canvas.run_label(progress, last_run, number)
797
+ div(class: "uni-step uni-step--#{state}") do
798
+ div(class: "uni-step__mark") do
799
+ if state == :done
800
+ phrase "", class: "uni-i uni-i--check"
801
+ phrase "Step #{number}, done", class: "uni-sr"
802
+ else
803
+ phrase number.to_s
804
+ phrase(state == :current ? "Step #{number}, current step" : "Step #{number}, not started",
805
+ class: "uni-sr")
806
+ end
807
+ end
808
+ div do
809
+ header3 step[:title], class: "uni-step__title"
810
+ phrase step[:payoff], class: "uni-step__payoff"
811
+ end
812
+ div(class: "uni-step__actions") do
813
+ case state
814
+ when :done
815
+ button "Repeat", id: "repeat-#{number}", class: "uni-btn uni-btn--quiet"
816
+ when :current
817
+ button run_label, id: "run-#{number}", class: "uni-btn uni-btn--outline"
818
+ button "Mark done", id: "mark-done-#{number}", class: "uni-btn uni-btn--quiet"
819
+ else
820
+ button run_label, id: "run-#{number}", class: "uni-btn uni-btn--quiet"
821
+ end
822
+ button expanded ? "Hide" : "Details", id: "view-#{number}", class: "uni-btn uni-btn--quiet"
823
+ end
824
+ # Expands exactly the row a Run/Repeat click just targeted, so
825
+ # "what should this do" is answered right there without opening
826
+ # Details -- last_run is what a click just wrote, and mark_done!
827
+ # clears it, so this can never point at a step that was actually
828
+ # finished instead of run. Skipped when the row's own full
829
+ # Details expansion (below) is already open: that one repeats
830
+ # "What you should see" itself, and showing both would say it
831
+ # twice.
832
+ if last_run && last_run['step'].to_i == number && !expanded
833
+ div(class: "uni-step__expect") do
834
+ phrase "What you should see", class: "uni-step__expect-label"
835
+ md StreamWeaver::University::Canvas.bullets(step[:what_you_should_see]),
836
+ class: "uni-payoff"
837
+ end
838
+ end
839
+
840
+ # Single-mode: "Details" expands THIS row in place -- why it
841
+ # matters, the prompt with Run/Copy, the payoff checklist, and a
842
+ # Mark done + next-step hint -- rather than navigating to a
843
+ # separate step screen. Expanding one row collapses any other
844
+ # (expanded_number holds at most one step), and there is nothing
845
+ # to get "back" from: Hide (the same button, relabeled) closes
846
+ # it in place.
847
+ if expanded
848
+ div(class: "uni-step__expansion") do
849
+ phrase "Why this matters", class: "uni-label"
850
+ md step[:why_it_matters], class: "uni-prose"
851
+
852
+ div(class: "uni-promptbox") do
853
+ div(class: "uni-promptbox__bar") do
854
+ phrase "The prompt your worker session receives"
855
+ end
856
+ phrase step[:prompt], class: "uni-prompt"
857
+ end
858
+ div(class: "uni-actions") do
859
+ button "Run in worker session", id: "run-#{number}", class: "uni-btn uni-btn--run"
860
+ copy_button "Copy prompt", text: step[:prompt],
861
+ copied_label: "Copied", class: "uni-btn uni-btn--outline"
862
+ end
863
+
864
+ phrase "What you should see", class: "uni-label"
865
+ md StreamWeaver::University::Canvas.bullets(step[:what_you_should_see]),
866
+ class: "uni-payoff"
867
+
868
+ div(class: "uni-step__expansion-foot") do
869
+ button "Mark step #{number} done", id: "mark-done-#{number}", class: "uni-btn uni-btn--outline"
870
+ if number < total
871
+ phrase "Unlocks step #{number + 1}.", class: "uni-foot__hint"
872
+ else
873
+ phrase "That is the whole course.", class: "uni-foot__hint"
874
+ end
875
+ end
876
+ end
877
+ end
878
+ end
879
+ end
880
+ end
881
+ end
882
+
883
+ # ---- The shelf: closed, dormant, no controls ------------------------------
884
+ div(class: "uni-divider") do
885
+ phrase "In the works", class: "uni-divider__label"
886
+ div(class: "uni-divider__rule") {}
887
+ end
888
+
889
+ StreamWeaver::University::Course::FUTURE_COURSES.each do |course|
890
+ card(depth: :recessed, class: "uni-course uni-course--dormant") do
891
+ card_header(class: "uni-course__bar") do
892
+ div(class: "uni-course__dot") {}
893
+ header2 course[:name], class: "uni-course__name"
894
+ div(class: "uni-chip uni-chip--soon") do
895
+ phrase "", class: "uni-i uni-i--lock"
896
+ phrase "Soon"
897
+ end
898
+ end
899
+ phrase course[:blurb], class: "uni-course__blurb"
900
+ end
901
+ end
902
+
903
+ md "Looking for the old component tour? Run `streamweaver tutorial` -- the classic " \
904
+ "walkthrough of every component. Older than these courses, and being refreshed.",
905
+ class: "uni-note"
906
+
907
+ # A second "Reset course" entry point, for anyone who wants to start
908
+ # over before finishing -- the recap screen (above, `all_done`) offers
909
+ # its own, so this one steps aside there rather than rendering the same
910
+ # id: "reset-course" button twice on one page.
911
+ unless all_done
912
+ div(class: "uni-reset") do
913
+ button "Reset course", id: "reset-course", class: "uni-btn uni-btn--quiet"
914
+ end
915
+ end
916
+ end
917
+
918
+ if respond_to?(:use_theme)
919
+ # canvas-push / canvas-read: self is already a StreamWeaver::App
920
+ instance_eval(&_body)
921
+ elsif __FILE__ == $PROGRAM_NAME
922
+ require_relative "../../stream_weaver"
923
+ app("StreamWeaver University") { instance_eval(&_body) }.run!
924
+ end