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,549 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # stream_weaver-8mj pixel-parity rebuild: Forrest's bar is IDENTICAL --
5
+ # "if we cannot make it look identical, then we have not succeeded." The
6
+ # prior slice (gsd/analysis/08-design-parity-fights.md, superseded by this
7
+ # rewrite) invented its own class names (tyrion-card, tyrion-board, ...)
8
+ # and fought StreamWeaver's own component CSS to skin them. This version
9
+ # instead emits tyrion's OWN class names (wr-board, wr-card, topbar,
10
+ # sidebar, story-row, as-parchment, ...) using only primitive
11
+ # div/phrase/header/clickable calls -- no Board/Lane/BoardCard/Sidebar/
12
+ # Navbar -- so the REAL public/shared.css (consumed at runtime, never
13
+ # copied into this repo) governs every element directly, with nothing of
14
+ # StreamWeaver's own competing for the same classes.
15
+ #
16
+ # Reference read (READ-ONLY, never copied): ~/work/tyrion web/app.rb,
17
+ # web/views/layout.rb, web/views/war_room.rb, web/views/active_story.rb,
18
+ # web/public/shared.css. Every class name/structure below was re-derived
19
+ # by reading those files, not copy-pasted.
20
+ #
21
+ # Fixture data only -- invented story titles, no real Tyrion project data.
22
+ #
23
+ # Env vars (both optional; the slice degrades gracefully without either):
24
+ # SW_PARITY_CSS=/path/to/tyrion/web/public/shared.css
25
+ # Serves the REAL CSS at runtime via `stylesheets:` local-path
26
+ # auto-detection. Unset: only this slice's own minimal seam CSS loads,
27
+ # and every wr-*/topbar/sidebar/as-* class renders unstyled (bare
28
+ # browser defaults) -- a stark but honest way to see exactly how much
29
+ # of the look comes from the real stylesheet.
30
+ # SW_PARITY_ASSETS=/path/to/tyrion/web/public/assets
31
+ # Real crest/dragon/lantern/map art via a local-file endpoint. Unset:
32
+ # unicode glyph fallbacks (🦁/🐉) and the plain CSS gradient tyrion's
33
+ # own shared.css already falls back to when its background-image url
34
+ # 404s (see tyrion_slice.css).
35
+ #
36
+ # Run: SW_PARITY_CSS=... SW_PARITY_ASSETS=... ruby examples/parity/tyrion_warroom_slice.rb
37
+
38
+ require_relative "../../lib/stream_weaver"
39
+ require "base64"
40
+ require "time"
41
+
42
+ PARITY_ASSETS_DIR = ENV["SW_PARITY_ASSETS"]
43
+ PARITY_CSS_PATH = ENV["SW_PARITY_CSS"]&.then { |p| File.expand_path(p) }
44
+ SLICE_CSS_PATH = File.expand_path("assets/tyrion_slice.css", __dir__)
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Real art, resolved to /sw-asset/ URLs *before* App.new -- ComponentAssets
48
+ # is a module-level registry (no App instance required), which sidesteps a
49
+ # real ordering gap: `stylesheets:` is resolved eagerly when App.new runs,
50
+ # but App#local_asset needs `self` to be the App instance, which only
51
+ # exists *inside* the DSL block that App.new's `stylesheets:` keyword
52
+ # argument is evaluated before. There is no way to build a `local_asset`
53
+ # URL early enough to hand to `stylesheets:` through the public API alone
54
+ # -- calling the registry directly is the only route. See catalog finding
55
+ # below for the fuller writeup (this is a second surfacing of the same
56
+ # gap, not a new one).
57
+ # ---------------------------------------------------------------------------
58
+ def parity_art_url(basename)
59
+ return nil unless PARITY_ASSETS_DIR
60
+ path = File.join(PARITY_ASSETS_DIR, basename)
61
+ return nil unless File.exist?(path)
62
+
63
+ "/sw-asset/#{StreamWeaver::ComponentAssets.register_file(File.expand_path(path))}/#{basename}"
64
+ end
65
+
66
+ CREST_URL = parity_art_url("LionCrest.png")
67
+ DRAGON_URL = parity_art_url("dragon.png")
68
+ LANTERN_URL = parity_art_url("lantern.png")
69
+ MAP_URL = parity_art_url("strategy_map.png")
70
+
71
+ stylesheet_list = [PARITY_CSS_PATH, SLICE_CSS_PATH].compact
72
+ extra_asset_dirs = [PARITY_ASSETS_DIR].compact
73
+ extra_asset_dirs << File.dirname(PARITY_CSS_PATH) if PARITY_CSS_PATH
74
+
75
+ # Real shared.css hardcodes `.topbar`/`.wr-board` background-image as a
76
+ # *relative* url("assets/strategy_map.png") -- there's no way to point
77
+ # that at our resolved MAP_URL without either rewriting the real CSS
78
+ # (never copied/edited, per the ticket's own constraint) or shipping an
79
+ # override rule of our own. `stylesheets:` entries that aren't a real
80
+ # local file pass through unchanged (see App#resolve_stylesheet_href) --
81
+ # including a `data:` URI, which isn't caught by the URL-scheme guard
82
+ # there (no `//` after the colon) -- so a small inline override, appended
83
+ # last, gets the real photo in without a temp file or an extra
84
+ # assets_dirs: entry.
85
+ if MAP_URL
86
+ bg_css = <<~CSS
87
+ body .topbar { background-image: linear-gradient(to bottom, rgba(0,0,0,.48), rgba(13,10,7,.88)), url('#{MAP_URL}'); }
88
+ body .wr-board { background: linear-gradient(180deg, rgba(8,10,11,.18), rgba(8,10,11,.28)), url('#{MAP_URL}') center/cover no-repeat; }
89
+ CSS
90
+ stylesheet_list << "data:text/css;base64,#{Base64.strict_encode64(bg_css)}"
91
+ end
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Store: in-memory, DB-shaped -- records live here, never in the state hash.
95
+ # ---------------------------------------------------------------------------
96
+ module StoryStore
97
+ # Mirrors the real tyrion server's *current* lane shape (stream_weaver-8mj
98
+ # A/B fidelity pass): Queue 6 / Active 0 / Blocked 0 / Done 8, curled from
99
+ # http://127.0.0.1:4579/warroom -- fixture slugs below are invented
100
+ # (slug-style, matching this file's existing medieval theme), never the
101
+ # real project's story names. With Active and Blocked both empty, the
102
+ # "Here Be Dragons" watermark (unconditionally rendered in both this
103
+ # slice and the real war_room.rb -- see the render_wr_col call below) has
104
+ # nothing to be covered by in those two lanes and reads exactly as it
105
+ # does live: centered, undimmed, not fighting card backgrounds for the
106
+ # gaps it shows through.
107
+ DEFS = [
108
+ { slug: "forge-the-beacon-relay", epic_slug: "field-ops", status: :done,
109
+ context: "Beacon-relay handshake wired between both outposts.",
110
+ intent: "Two outposts need a shared signal before the muster begins.",
111
+ next_action: nil,
112
+ criteria: [["Relay reaches both towers", :met], ["Signal survives storm interference", :met], ["Watch rotation briefed", :met]],
113
+ notes: [[:progress, "First handshake test succeeded at half range."], [:progress, "Full 4-league range held through the night watch."]],
114
+ stale: false },
115
+ { slug: "mend-the-gate-hinge", epic_slug: "field-ops", status: :done,
116
+ context: "Gate hinge replaced and load-tested.",
117
+ intent: "Keep the east gate from seizing before winter.",
118
+ next_action: nil,
119
+ criteria: [["Hinge replaced", :met], ["Load test passed", :met]],
120
+ notes: [[:progress, "New hinge forged and fitted."], [:progress, "Load-tested to double the rated weight."]],
121
+ stale: false },
122
+ { slug: "seal-the-old-cistern", epic_slug: "river-trade", status: :done,
123
+ context: "Cistern sealed; pressure test passed.",
124
+ intent: "Stop the slow leak before the dry season.",
125
+ next_action: nil,
126
+ criteria: [["Cistern sealed", :met], ["Pressure test passed", :met]],
127
+ notes: [[:progress, "Seal held at full pressure for a full day."]], stale: false },
128
+ { slug: "clear-the-ashwood-pass", epic_slug: "field-ops", status: :done,
129
+ context: "Riders confirmed the pass safe for the muster.",
130
+ intent: "Confirm the ashwood pass is clear before the muster marches through.",
131
+ next_action: nil,
132
+ criteria: [["Riders dispatched", :met], ["Pass condition confirmed", :met]],
133
+ notes: [[:progress, "Riders returned at dusk -- pass reported clear."]], stale: false },
134
+ { slug: "repair-the-north-bridge", epic_slug: "field-ops", status: :done,
135
+ context: "Crossbeams placed; load-bearing test passed.",
136
+ intent: "The north bridge must hold before the autumn floods.",
137
+ next_action: nil,
138
+ criteria: [["Timber delivered", :met], ["Crossbeams placed", :met], ["Load-bearing test", :met]],
139
+ notes: [[:progress, "Timber arrived a day ahead of schedule."], [:progress, "Load test held at double the rated weight."]],
140
+ stale: false },
141
+ { slug: "translate-the-sunken-tablet", epic_slug: "river-trade", status: :done,
142
+ context: "Scriptorium translator resolved the old script.",
143
+ intent: "The tablet may name the vault's true owner.",
144
+ next_action: nil,
145
+ criteria: [["Tablet recovered", :met], ["Translator found", :met]],
146
+ notes: [[:progress, "Scriptorium translator confirmed the vault's owner."]], stale: false },
147
+ { slug: "negotiate-toll-rights", epic_slug: "river-trade", status: :done,
148
+ context: "Guild council approved the toll rights.",
149
+ intent: "Toll rights unlock the southern supply route.",
150
+ next_action: nil,
151
+ criteria: [["Guild proposal submitted", :met], ["Council vote scheduled", :met]],
152
+ notes: [[:progress, "Council vote passed on the second reading."]], stale: false },
153
+ { slug: "draft-the-harvest-ledger", epic_slug: "field-ops", status: :done,
154
+ context: "Ledger reconciled against the quartermaster's count.",
155
+ intent: "The harvest ledger must balance before winter stores are sealed.",
156
+ next_action: nil,
157
+ criteria: [["Ledger drafted", :met], ["Counts reconciled", :met]],
158
+ notes: [[:progress, "Ledger balanced on the first reconciliation pass."]], stale: false },
159
+ { slug: "chart-the-southern-vault", epic_slug: "field-ops", status: :pending,
160
+ context: "Survey pass before the vault dig begins.",
161
+ intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
162
+ { slug: "restock-the-armory", epic_slug: "field-ops", status: :pending,
163
+ context: "Quartermaster count due before the next campaign.",
164
+ intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
165
+ { slug: "draft-the-spring-muster", epic_slug: "field-ops", status: :pending,
166
+ context: "First pass on troop assignments.",
167
+ intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
168
+ { slug: "ready-the-siege-engines", epic_slug: "field-ops", status: :pending,
169
+ context: "Inventory pass before the engines are wheeled out.",
170
+ intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
171
+ { slug: "survey-the-flooded-cellar", epic_slug: "river-trade", status: :pending,
172
+ context: "Waiting on a dry spell before the survey crew goes down.",
173
+ intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
174
+ { slug: "catalog-the-relic-hoard", epic_slug: "river-trade", status: :pending,
175
+ context: "First pass on the recovered hoard, pending an appraiser.",
176
+ intent: nil, next_action: nil, criteria: [], notes: [], stale: false }
177
+ ].freeze
178
+
179
+ # Demo-only lane emptying (stream_weaver-8mj empty-state proof): set
180
+ # SW_PARITY_EMPTY_LANE=pending (or done/active/blocked) to force that
181
+ # lane empty regardless of the fixture above. Unset by default -- the
182
+ # mirrored shape above (Active/Blocked already empty) is the normal demo.
183
+ EMPTY_LANE = ENV["SW_PARITY_EMPTY_LANE"]&.to_sym
184
+
185
+ @stories = DEFS.each_with_index.map do |d, i|
186
+ d.merge(
187
+ id: i + 1,
188
+ last_note_at: d[:status] == :in_progress ? (d[:stale] ? Time.now - (9 * 3600) : Time.now - (i + 1) * 240) : nil,
189
+ completed_at: d[:status] == :done ? Time.now - ((i + 1) * 3600 * 9) : nil
190
+ )
191
+ end
192
+
193
+ class << self
194
+ def all = @stories
195
+ def find(id) = @stories.find { |s| s[:id] == id.to_i }
196
+ def by_status(status) = status == EMPTY_LANE ? [] : @stories.select { |s| s[:status] == status }
197
+ end
198
+ end
199
+
200
+ def relative_time(t)
201
+ return "" unless t
202
+ mins = ((Time.now - t) / 60).round
203
+ return "#{mins}m ago" if mins < 60
204
+ hours = mins / 60
205
+ return "#{hours}h ago" if hours < 24
206
+ "#{hours / 24}d ago"
207
+ end
208
+
209
+ STATUS_LABEL = { pending: "pending", in_progress: "in progress", done: "done", blocked: "blocked" }.freeze
210
+ STATUS_CSS = { pending: "pending", in_progress: "in-progress", done: "done", blocked: "blocked" }.freeze
211
+
212
+ App = StreamWeaver::App.new(
213
+ "Tyrion War Room (parity slice)",
214
+ chrome: false,
215
+ theme: :dashboard,
216
+ theme_overrides: { color_bg: "#0D0A07", color_text: "#F5E6C8" },
217
+ fonts: ["Cinzel:wght@400;600;700", "IBM+Plex+Mono:wght@300;400", "Lora:ital,wght@0,400;1,400;1,600"],
218
+ stylesheets: stylesheet_list,
219
+ assets_dirs: extra_asset_dirs
220
+ ) do
221
+ # =========================================================================
222
+ # Shared chrome: topbar + sidebar, rendered on every page. Real DOM:
223
+ # web/views/layout.rb -- .topbar > .topbar-main + .topbar-nav, .sidebar.
224
+ # =========================================================================
225
+
226
+ def art_glyph(url, glyph, style:)
227
+ if url
228
+ div(style: "#{style}background-image:url('#{url}');background-size:contain;background-repeat:no-repeat;background-position:center;display:inline-block;")
229
+ else
230
+ phrase(glyph, style: "#{style}display:inline-block;text-align:center;")
231
+ end
232
+ end
233
+
234
+ current_action = state[StreamWeaver::Resource::StateKeys::ACTION]
235
+ current_story = current_action.is_a?(Symbol) && current_action.to_s.start_with?("story_") ? StoryStore.find(current_action.to_s.delete_prefix("story_")) : nil
236
+ active_tab = if current_story
237
+ current_story[:status] == :in_progress ? :active : :warroom
238
+ else
239
+ :warroom
240
+ end
241
+
242
+ div(class: "shell") do
243
+ div(class: "topbar") do
244
+ div(class: "topbar-main") do
245
+ div(class: "topbar-brand") do
246
+ art_glyph(CREST_URL, "🦁", style: "height:44px;width:44px;margin-right:10px;vertical-align:middle;")
247
+ phrase("TYRION", style: "font-family:'Cinzel',serif;font-size:24px;font-weight:700;color:var(--gold-bright);letter-spacing:0.12em;")
248
+ end
249
+ phrase("·", class: "topbar-sep")
250
+ phrase("field-ops", class: "topbar-crumb")
251
+ phrase("·", class: "topbar-sep")
252
+ phrase(current_story ? current_story[:slug] : "warroom-parity", class: "topbar-crumb active")
253
+ div(class: "topbar-git") do
254
+ phrase("⎇ parity/warroom-slice", class: "pill pill-neutral")
255
+ phrase("✗ 2", class: "pill pill-amber")
256
+ end
257
+ end
258
+ div(class: "topbar-nav") do
259
+ [
260
+ [:warroom, "⚔ War Room", "/"],
261
+ [:roadmap, "🗺 Roadmap", "#"],
262
+ [:active, "📖 Active Story", "#"],
263
+ [:global, "🌍 Global View", "#"],
264
+ [:discoveries, "💡 Discoveries", "#"],
265
+ [:about, "❓ About Tyrion", "#"]
266
+ ].each do |id, label, href|
267
+ clickable(href: href, class: active_tab == id ? "demo-tab active" : "demo-tab") { phrase(label) }
268
+ end
269
+ end
270
+ end
271
+
272
+ div(class: "sidebar") do
273
+ div(style: "padding:10px 14px 0;font-size:12px;color:var(--text-dim);font-family:var(--font-mono);") { phrase("field-ops › warroom-parity") }
274
+ div(class: "sidebar-section") { phrase("Stories · warroom-parity") }
275
+
276
+ StoryStore.all.each do |story|
277
+ row_class = case story[:status]
278
+ when :done then "story-row done"
279
+ when :in_progress then "story-row active"
280
+ else "story-row"
281
+ end
282
+ icon_glyph = story[:status] == :pending ? "○" : "●"
283
+ icon_color = case story[:status]
284
+ when :done then "var(--emerald)"
285
+ when :in_progress then "var(--amber)"
286
+ else "var(--ink-faint)"
287
+ end
288
+ clickable(href: "/stories/#{story[:id]}", class: row_class) do
289
+ phrase(icon_glyph, class: story[:status] == :in_progress ? "s-icon s-icon-pulse" : "s-icon", style: "color:#{icon_color}")
290
+ phrase(story[:slug], class: "s-name")
291
+ end
292
+ end
293
+
294
+ # Mirrors the reference's current discoveries shape: an active spike,
295
+ # zero promotable ("ready") discoveries -- so no ready row, matching
296
+ # Layout's `if disc[:ready_count] > 0` gate in the real app -- and 5
297
+ # unformalized marks.
298
+ div(class: "disc-strip") do
299
+ div(class: "sidebar-section") { phrase("Discoveries") }
300
+ clickable(href: "#", class: "disc-row") do
301
+ phrase("spike", class: "d-pill spike")
302
+ phrase("the flooded cellar timing", class: "d-label")
303
+ end
304
+ clickable(href: "#", class: "disc-row") do
305
+ phrase("5 marks", class: "d-pill mark")
306
+ phrase("unformalized", class: "d-label")
307
+ end
308
+ end
309
+ end
310
+
311
+ # =======================================================================
312
+ # War Room board -- real DOM: web/views/war_room.rb.
313
+ # =======================================================================
314
+ page(:warroom, "/") do
315
+ div(class: "main-content visible") do
316
+ div(class: "wr-board") do
317
+ # Always rendered, centered, behind the columns (z-index:1 vs
318
+ # .wr-columns' z-index:2) -- not an empty-lane conditional. The
319
+ # original slice's report mischaracterized this as an empty-state
320
+ # overlay; the real war_room.rb renders it unconditionally as a
321
+ # background watermark, only visible through the gaps between
322
+ # cards.
323
+ div(class: "wr-dragon-zone") do
324
+ div do
325
+ art_glyph(DRAGON_URL, "🐉", style: "width:70px;height:70px;opacity:.82;filter:drop-shadow(0 6px 12px rgba(0,0,0,.5));margin-bottom:10px;")
326
+ div(class: "wr-dragon-zone-copy") do
327
+ header3("Here Be Dragons")
328
+ # `.wr-dragon-zone-copy p` (real CSS) targets a bare <p> tag,
329
+ # not a class -- text() with no tone: renders exactly that
330
+ # (Components::Text's untoned branch emits `view.p { content
331
+ # }` with zero attrs), so three stacked <p>s (margin:0 in the
332
+ # real CSS) read the same as the original's one <p> + <br>s.
333
+ text("Uncertain requirements")
334
+ text("External dependency")
335
+ text("Needs human decision")
336
+ end
337
+ end
338
+ end
339
+
340
+ div(class: "wr-columns") do
341
+ render_wr_col("Queue", "Pending", StoryStore.by_status(:pending), "wr-col")
342
+ render_wr_col("Active Campaign", "In Progress", StoryStore.by_status(:in_progress), "wr-col", header_class: "active-hdr")
343
+ render_wr_col("Blocked Frontier", "Blocked", StoryStore.by_status(:blocked), "wr-col", header_class: "dragons-hdr", show_dragon: true, title_prefix: "🐉 ")
344
+ render_wr_col("Shipped Keep", "Done", StoryStore.by_status(:done), "wr-col", header_class: "done-hdr")
345
+ end
346
+
347
+ active_stories = StoryStore.by_status(:in_progress)
348
+ if active_stories.size == 1
349
+ render_single_resume_strip(active_stories.first)
350
+ elsif active_stories.size > 1
351
+ render_multi_lane_strip(active_stories)
352
+ end
353
+ end
354
+ end
355
+ end
356
+
357
+ # =======================================================================
358
+ # Story detail pages -- one real deep-linkable URL per fixture story
359
+ # (page() matches on a literal path, not a param placeholder -- with
360
+ # only 10 fixture stories, registering one page per id is the correct
361
+ # DSL-native way to get real /stories/:id URLs; `resource()` exists for
362
+ # this but pulls in its own DefaultViews scaffolding (edit/new/destroy
363
+ # markup this slice doesn't want fighting the real as-* CSS for
364
+ # control it doesn't need). Real DOM: web/views/active_story.rb.
365
+ # =======================================================================
366
+ StoryStore.all.each do |story|
367
+ page(:"story_#{story[:id]}", "/stories/#{story[:id]}") do
368
+ div(class: "main-content visible") do
369
+ div(class: "as-outer") do
370
+ div(class: "as-scroll") do
371
+ div(class: "as-parchment") do
372
+ clickable(href: "/", style: "display:inline-block;font-size:12px;font-family:'IBM Plex Mono',monospace;color:var(--ink-faint);text-decoration:none;margin-bottom:12px;opacity:.7;") { phrase("← War Room") }
373
+
374
+ div(class: "as-hero-status") do
375
+ div(class: "status-badge #{STATUS_CSS.fetch(story[:status])}") do
376
+ div(class: "dot")
377
+ phrase(STATUS_LABEL.fetch(story[:status]))
378
+ end
379
+ if story[:last_note_at]
380
+ phrase("last note #{relative_time(story[:last_note_at])}", style: "font-size:13px;color:var(--ink-muted)")
381
+ end
382
+ end
383
+ div(class: "as-hero-title") { phrase(story[:slug]) }
384
+
385
+ if story[:intent]
386
+ div(style: "background:rgba(180,140,80,.08);border-left:3px solid rgba(180,140,80,.35);border-radius:0 6px 6px 0;padding:10px 14px;margin-bottom:14px;") do
387
+ div(style: "font-size:10px;font-family:'IBM Plex Mono',monospace;color:var(--ink-faint);letter-spacing:.1em;margin-bottom:6px;") { phrase("MISSION BRIEF") }
388
+ div(style: "font-family:'Lora',serif;font-size:14px;color:var(--ink-dim);font-style:italic;line-height:1.6;") { phrase(story[:intent]) }
389
+ end
390
+ end
391
+
392
+ div(class: "as-block") do
393
+ div(class: "as-block-label") { phrase("Current Context") }
394
+ div(style: "font-size:11px;font-family:'IBM Plex Mono',monospace;color:var(--ink-faint);margin-top:-3px;margin-bottom:5px;") { phrase("set by implementing agent") }
395
+ div(class: "as-block-text") { phrase(story[:context]) }
396
+ end
397
+
398
+ div(class: "as-block") do
399
+ div(class: "as-block-label") { phrase("Next Action") }
400
+ div(style: "font-size:11px;font-family:'IBM Plex Mono',monospace;color:var(--ink-faint);margin-top:-3px;margin-bottom:5px;") { phrase("agent sets this before handing off") }
401
+ div(class: "as-block-text") { phrase(story[:next_action] || "(not set)") }
402
+ end
403
+
404
+ div(class: "as-split") do
405
+ div do
406
+ div(class: "as-block-label", style: "margin-bottom:8px") { phrase("Criteria") }
407
+ if story[:criteria].any?
408
+ story[:criteria].each do |text, status|
409
+ div(class: status == :met ? "as-crit-row done" : "as-crit-row") do
410
+ div(class: status == :met ? "crit-check checked" : "crit-check")
411
+ div(class: "crit-text") { phrase(text) }
412
+ end
413
+ end
414
+ else
415
+ div(style: "font-size:13px;color:var(--ink-faint);font-style:italic;") { phrase("No criteria yet") }
416
+ end
417
+ end
418
+
419
+ div(style: "display:flex;flex-direction:column;") do
420
+ div(class: "as-block-label", style: "margin-bottom:8px") { phrase("Notes") }
421
+ note_anchor = story[:last_note_at] || story[:completed_at] || Time.now
422
+ story[:notes].each_with_index do |(kind, body), idx|
423
+ div(class: "as-note-entry #{kind}") do
424
+ div(class: "note-meta") { phrase("#{kind} · #{relative_time(note_anchor - (idx * 3600))}") }
425
+ div(class: "note-body") { phrase(body) }
426
+ end
427
+ end
428
+ # Static/decorative -- functional note-adding is out of
429
+ # scope for this pixel-parity pass (see catalog "not
430
+ # attempted").
431
+ div(class: "as-quick-note", style: "margin-top:auto;") do
432
+ div(style: "flex:1;font-family:var(--font-mono);font-size:14px;color:var(--ink-faint);font-style:italic;") { phrase("progress note + Enter…") }
433
+ end
434
+ end
435
+ end
436
+ end
437
+
438
+ if story[:status] == :in_progress || story[:next_action] || story[:last_note_at]
439
+ div(class: "as-resume-strip") do
440
+ div(class: "as-rs-panel") do
441
+ div(class: "as-rs-label") { phrase("LAST NOTE") }
442
+ div(class: "as-rs-text") { phrase(story[:notes].first ? "#{relative_time(story[:last_note_at])} — \"#{story[:notes].first[1].to_s[0, 60]}\"" : "no notes yet") }
443
+ end
444
+ div(class: "as-rs-panel as-rs-beacon") do
445
+ art_glyph(LANTERN_URL, "🏮", style: "height:44px;width:44px;filter:drop-shadow(0 0 10px rgba(245,158,11,0.75));")
446
+ div do
447
+ div(class: "as-rs-label") { phrase("RESUME POINT") }
448
+ div(class: "as-rs-text") do
449
+ met = story[:criteria].count { |_, s| s == :met }
450
+ phrase(story[:criteria].any? ? "#{story[:slug]} · #{met}/#{story[:criteria].size} criteria met" : story[:slug])
451
+ end
452
+ end
453
+ end
454
+ div(class: "as-rs-panel") do
455
+ div(class: "as-rs-label") { phrase("NEXT ACTION") }
456
+ div(class: "as-rs-text") { phrase(story[:next_action] || "(not set)") }
457
+ end
458
+ end
459
+ end
460
+ end
461
+ end
462
+ end
463
+ end
464
+ end
465
+ end
466
+ end
467
+
468
+ def render_wr_col(title, sub, stories, col_class, header_class: nil, show_dragon: false, title_prefix: "")
469
+ div(class: col_class) do
470
+ div(class: ["wr-col-header", header_class].compact.join(" ")) do
471
+ art_glyph(DRAGON_URL, "🐉", style: "height:22px;width:22px;opacity:.85;filter:drop-shadow(0 2px 4px rgba(0,0,0,.4));") if show_dragon
472
+ div do
473
+ div(class: "wr-col-title") { phrase("#{title_prefix}#{title}") }
474
+ div(class: "wr-col-sub") { phrase(sub) }
475
+ end
476
+ phrase(stories.size.to_s, class: stories.size.positive? && show_dragon ? "wr-col-count danger" : "wr-col-count")
477
+ end
478
+
479
+ stories.each do |story|
480
+ if story[:status] == :done
481
+ render_wr_done_card(story)
482
+ else
483
+ render_wr_card(story)
484
+ end
485
+ end
486
+ end
487
+ end
488
+
489
+ def render_wr_card(story)
490
+ card_style = story[:status] == :blocked ? "border-left:3px solid #b91c1c;background:linear-gradient(180deg,rgba(220,180,160,.97),rgba(190,150,120,.94));" : ""
491
+ clickable(href: "/stories/#{story[:id]}") do
492
+ div(class: story[:stale] ? "wr-card stale" : "wr-card", style: card_style) do
493
+ div(class: "wr-stale-badge") { phrase("⚠ STALE #{relative_time(story[:last_note_at])}") } if story[:stale]
494
+ phrase(story[:epic_slug], class: "wr-card-id")
495
+ header4(story[:slug], class: "wr-card-title")
496
+ div(class: "wr-card-ctx") { phrase(story[:context].to_s[0, 60]) } if story[:context]
497
+ div(class: "wr-card-tags") { phrase(STATUS_LABEL.fetch(story[:status]), class: "wr-tag") }
498
+ end
499
+ end
500
+ end
501
+
502
+ def render_wr_done_card(story)
503
+ clickable(href: "/stories/#{story[:id]}") do
504
+ div(class: "wr-card done-card") do
505
+ phrase(story[:epic_slug], class: "wr-card-id")
506
+ header4(story[:slug], class: "wr-card-title")
507
+ div(class: "wr-card-tags") { phrase("✓ done · #{relative_time(story[:completed_at])}", style: "font-size:11px;color:#166534;") }
508
+ end
509
+ end
510
+ end
511
+
512
+ def render_single_resume_strip(story)
513
+ div(class: "wr-resume-strip") do
514
+ div(class: "wr-thread-card") do
515
+ div(class: "wr-tc-label") { phrase("⚡ Active") }
516
+ div(class: "wr-tc-text") { phrase(story[:slug]) }
517
+ end
518
+ div(class: "wr-thread-card beacon-card") do
519
+ div(class: "wr-tc-label") do
520
+ art_glyph(LANTERN_URL, "🏮", style: "height:20px;width:20px;filter:drop-shadow(0 0 8px rgba(245,158,11,.8));")
521
+ phrase(" Resume Point")
522
+ end
523
+ div(class: "wr-tc-title") { phrase(story[:slug]) }
524
+ div(class: "wr-tc-meta") { phrase("🕐 #{relative_time(story[:last_note_at])}") }
525
+ end
526
+ div(class: "wr-thread-card") do
527
+ div(class: "wr-tc-label") { phrase("→ Next Action") }
528
+ div(class: "wr-tc-text") { phrase(story[:next_action] || "(not set)") }
529
+ end
530
+ end
531
+ end
532
+
533
+ def render_multi_lane_strip(active_stories)
534
+ div(class: "wr-resume-strip wr-resume-strip--multi") do
535
+ div(class: "wr-thread-card") do
536
+ div(class: "wr-tc-label") { phrase("⚡ #{active_stories.size} active lanes") }
537
+ div(class: "wr-tc-text") { phrase("No single resume point — each lane owns its own story.") }
538
+ end
539
+ active_stories.each do |s|
540
+ div(class: "wr-thread-card") do
541
+ div(class: "wr-tc-label") { phrase("🛤 field-ops") }
542
+ div(class: "wr-tc-title") { phrase(s[:slug]) }
543
+ div(class: "wr-tc-meta") { phrase("🕐 #{relative_time(s[:last_note_at])}") }
544
+ end
545
+ end
546
+ end
547
+ end
548
+
549
+ App.generate.run! if __FILE__ == $0
@@ -0,0 +1,124 @@
1
+ # Running StreamWeaver with Puma-dev
2
+
3
+ This example demonstrates how to run StreamWeaver apps with [Puma-dev](https://github.com/puma/puma-dev), which provides memorable URLs like `http://myapp.test` for your local development.
4
+
5
+ ## Quick Comparison
6
+
7
+ | Feature | Standalone (`ruby app.rb`) | Puma-dev (`config.ru`) |
8
+ |---------|----------------------------|------------------------|
9
+ | **Start command** | `ruby app.rb` | `puma-dev link` (once) |
10
+ | **URL** | `http://localhost:4567` (auto-port) | `http://myapp.test` |
11
+ | **Browser** | Opens automatically | Manual (on-demand) |
12
+ | **Multiple apps** | Need separate terminals | All available via `.test` URLs |
13
+ | **Best for** | Quick scripts, one-offs | Development, multiple apps |
14
+
15
+ ## Why Use Puma-dev?
16
+
17
+ - **Memorable URLs**: Access your app at `http://[directory-name].test` instead of remembering port numbers
18
+ - **Always Available**: No need to start the app manually - Puma-dev starts it on first request
19
+ - **Multiple Apps**: Run multiple StreamWeaver apps side-by-side with their own URLs
20
+ - **No Browser Auto-open**: Apps run on-demand without opening the browser automatically
21
+
22
+ ## Setup
23
+
24
+ ### 1. Install Puma-dev
25
+
26
+ ```bash
27
+ # macOS
28
+ brew install puma/puma/puma-dev
29
+ sudo puma-dev -setup
30
+ puma-dev -install
31
+
32
+ # Linux
33
+ gem install puma-dev
34
+ puma-dev -setup
35
+ puma-dev -install
36
+ ```
37
+
38
+ ### 2. Create Your App
39
+
40
+ Create a directory for your app with a `config.ru` file:
41
+
42
+ ```ruby
43
+ # config.ru
44
+ require 'bundler/setup'
45
+ require 'stream_weaver'
46
+
47
+ App = app "My StreamWeaver App" do
48
+ header1 "Hello from Puma-dev!"
49
+ text_field :message, placeholder: "Enter a message"
50
+
51
+ if state[:message] && !state[:message].empty?
52
+ text "You said: #{state[:message]}"
53
+ end
54
+ end
55
+
56
+ run App
57
+ ```
58
+
59
+ ### 3. Link to Puma-dev
60
+
61
+ ```bash
62
+ cd /path/to/your/app
63
+ puma-dev link
64
+ ```
65
+
66
+ This creates a symlink in `~/.puma-dev/` pointing to your app directory.
67
+
68
+ ### 4. Access Your App
69
+
70
+ Open your browser and navigate to:
71
+ ```
72
+ http://[directory-name].test
73
+ ```
74
+
75
+ For example, if your directory is named `myapp`, visit `http://myapp.test`.
76
+
77
+ ## How It Works
78
+
79
+ When running under Puma-dev:
80
+
81
+ 1. **Port Detection**: StreamWeaver detects the `PORT` environment variable set by Puma-dev
82
+ 2. **No Auto-browser**: The browser won't auto-open (since `PORT` is set)
83
+ 3. **Same Behavior**: All other StreamWeaver features work exactly the same
84
+
85
+ When running normally (`ruby app.rb`):
86
+
87
+ 1. **Auto Port**: StreamWeaver finds an available port starting from 4567
88
+ 2. **Auto Browser**: Opens your browser automatically
89
+ 3. **Self-contained**: No external dependencies
90
+
91
+ ## Environment Variable Priority
92
+
93
+ StreamWeaver checks for port in this order:
94
+ 1. `options[:port]` - Explicitly passed to `run!`
95
+ 2. `STREAMWEAVER_PORT` - Custom StreamWeaver port
96
+ 3. `PORT` - Standard port (used by Puma-dev, Heroku, etc.)
97
+ 4. Auto-detect - Find available port starting from 4567
98
+
99
+ ## Converting Existing Apps
100
+
101
+ To run an existing StreamWeaver app with Puma-dev:
102
+
103
+ 1. Create a `config.ru` in your app's directory
104
+ 2. Replace `App.run!` with `run App` in `config.ru`
105
+ 3. Link to Puma-dev: `puma-dev link`
106
+ 4. Access at `http://[directory-name].test`
107
+
108
+ ## Troubleshooting
109
+
110
+ **App not accessible?**
111
+ - Check Puma-dev is running: `puma-dev -status`
112
+ - Verify symlink exists: `ls -la ~/.puma-dev/`
113
+ - Check logs: `tail -f ~/Library/Logs/puma-dev.log` (macOS)
114
+
115
+ **Want both modes?**
116
+ Keep your original `app.rb` with `App.run!` for standalone mode, and create a separate `config.ru` with `run App` for Puma-dev mode.
117
+
118
+ ## Files in This Example
119
+
120
+ - **`config.ru`** - Puma-dev/Rack configuration (use with `puma-dev link`)
121
+ - **`standalone_app.rb`** - Traditional standalone mode (use with `ruby standalone_app.rb`)
122
+ - **`README.md`** - This file
123
+
124
+ Try both to see the difference!