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,414 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # stream_weaver-oeo proof: Forrest's direction was "duplicate the sinatra
5
+ # apps' theming niceness (semantic classes + ONE user stylesheet owning
6
+ # the look, zero framework fights) instead of the intrusive escape-hatch
7
+ # path the pixel slice used." examples/parity/tyrion_warroom_slice.rb
8
+ # proved pixel-identical parity is achievable, but only by inventing
9
+ # tyrion's own class names from scratch with primitive div/phrase/header
10
+ # calls and never touching a real StreamWeaver component (Board, Lane,
11
+ # Sidebar, Navbar, ... were all deliberately unused there, see that
12
+ # file's own header comment).
13
+ #
14
+ # This slice is the other half of the bet: same fixture data, same two
15
+ # routes (/ and /stories/:id), but built with the PRETTY DSL --
16
+ # app_shell/sidebar/navbar/nav_item/board/lane/board_card/clickable/card
17
+ # -- styled by exactly ONE stylesheet (tyrion_components.css) that
18
+ # targets ONLY the sw- hook classes documented in docs/theming-hooks.md
19
+ # (plus its own tc- prefixed classes for the handful of elements with no
20
+ # dedicated component -- topbar branding, war-table hero background,
21
+ # story-detail page chrome; see design-parity-fights.md findings #2 and
22
+ # #6, both still-open gaps this slice routes around the same way the
23
+ # pixel slice did).
24
+ #
25
+ # Because every framework style now lives in @layer stream-weaver
26
+ # (stream_weaver-oeo commit 1), tyrion_components.css needs zero
27
+ # specificity workarounds anywhere -- no `body` prefix, no !important,
28
+ # nothing the pixel slice's CSS had to resort to.
29
+ #
30
+ # Palette/type values below were re-derived by reading (READ-ONLY, never
31
+ # copied) ~/work/tyrion/web/public/shared.css's :root custom properties --
32
+ # every rule in tyrion_components.css is hand-written against those
33
+ # values, not copy-pasted, and never references SW_PARITY_CSS (this slice
34
+ # doesn't load the real stylesheet at all -- that's the whole point).
35
+ #
36
+ # Fixture data only -- invented story titles, no real Tyrion project data.
37
+ #
38
+ # Env vars (optional; the slice degrades gracefully without it):
39
+ # SW_PARITY_ASSETS=/path/to/tyrion/web/public/assets
40
+ # Real crest/dragon/lantern/map art via a local-file endpoint. Unset:
41
+ # unicode glyph fallbacks (๐Ÿฆ/๐Ÿ‰/๐Ÿฎ) and no war-table photo (the
42
+ # gradient-only fallback already baked into tyrion_components.css).
43
+ #
44
+ # Run: SW_PARITY_ASSETS=... ruby examples/parity/tyrion_warroom_components.rb
45
+
46
+ require_relative "../../lib/stream_weaver"
47
+ require "base64"
48
+ require "time"
49
+
50
+ PARITY_ASSETS_DIR = ENV["SW_PARITY_ASSETS"]
51
+ COMPONENTS_CSS_PATH = File.expand_path("tyrion_components.css", __dir__)
52
+
53
+ def parity_art_url(basename)
54
+ return nil unless PARITY_ASSETS_DIR
55
+
56
+ path = File.join(PARITY_ASSETS_DIR, basename)
57
+ return nil unless File.exist?(path)
58
+
59
+ "/sw-asset/#{StreamWeaver::ComponentAssets.register_file(File.expand_path(path))}/#{basename}"
60
+ end
61
+
62
+ CREST_URL = parity_art_url("LionCrest.png")
63
+ DRAGON_URL = parity_art_url("dragon.png")
64
+ LANTERN_URL = parity_art_url("lantern.png")
65
+ MAP_URL = parity_art_url("strategy_map.png")
66
+
67
+ # Same seam tyrion_warroom_slice.rb's own header comment documents: the
68
+ # real .topbar/.wr-board background-image is a *relative*
69
+ # url("assets/strategy_map.png") in the real CSS, so there's no `class:`
70
+ # hook that can carry a resolved /sw-asset/ URL -- it has to be a CSS
71
+ # background-image rule. This slice never loads the real CSS at all
72
+ # (that's the whole point), so it's a small inline override targeting
73
+ # this slice's OWN hook classes (.sw-topbar, .tc-board-hero), appended
74
+ # after tyrion_components.css so it wins ties. Unset MAP_URL: no override,
75
+ # tyrion_components.css's own gradient-only fallback stands as-is.
76
+ stylesheet_list = [COMPONENTS_CSS_PATH]
77
+ if MAP_URL
78
+ bg_css = <<~CSS
79
+ .sw-topbar { background-image: linear-gradient(to bottom, rgba(0,0,0,.48), rgba(13,10,7,.88)), url('#{MAP_URL}'); background-size: cover; background-position: center 35%; }
80
+ .tc-board-hero { background-image: linear-gradient(180deg, rgba(8,10,11,.18), rgba(8,10,11,.28)), url('#{MAP_URL}'); background-size: cover; background-position: center; }
81
+ CSS
82
+ stylesheet_list << "data:text/css;base64,#{Base64.strict_encode64(bg_css)}"
83
+ end
84
+
85
+ # ---------------------------------------------------------------------------
86
+ # Store: identical fixture data to tyrion_warroom_slice.rb (same shape,
87
+ # same mirrored lane counts curled from the real war room -- see that
88
+ # file's StoryStore comment for the full provenance note).
89
+ # ---------------------------------------------------------------------------
90
+ module StoryStore
91
+ DEFS = [
92
+ { slug: "forge-the-beacon-relay", epic_slug: "field-ops", status: :done,
93
+ context: "Beacon-relay handshake wired between both outposts.",
94
+ intent: "Two outposts need a shared signal before the muster begins.",
95
+ next_action: nil,
96
+ criteria: [["Relay reaches both towers", :met], ["Signal survives storm interference", :met], ["Watch rotation briefed", :met]],
97
+ notes: [[:progress, "First handshake test succeeded at half range."], [:progress, "Full 4-league range held through the night watch."]],
98
+ stale: false },
99
+ { slug: "mend-the-gate-hinge", epic_slug: "field-ops", status: :done,
100
+ context: "Gate hinge replaced and load-tested.",
101
+ intent: "Keep the east gate from seizing before winter.",
102
+ next_action: nil,
103
+ criteria: [["Hinge replaced", :met], ["Load test passed", :met]],
104
+ notes: [[:progress, "New hinge forged and fitted."], [:progress, "Load-tested to double the rated weight."]],
105
+ stale: false },
106
+ { slug: "seal-the-old-cistern", epic_slug: "river-trade", status: :done,
107
+ context: "Cistern sealed; pressure test passed.",
108
+ intent: "Stop the slow leak before the dry season.",
109
+ next_action: nil,
110
+ criteria: [["Cistern sealed", :met], ["Pressure test passed", :met]],
111
+ notes: [[:progress, "Seal held at full pressure for a full day."]], stale: false },
112
+ { slug: "clear-the-ashwood-pass", epic_slug: "field-ops", status: :done,
113
+ context: "Riders confirmed the pass safe for the muster.",
114
+ intent: "Confirm the ashwood pass is clear before the muster marches through.",
115
+ next_action: nil,
116
+ criteria: [["Riders dispatched", :met], ["Pass condition confirmed", :met]],
117
+ notes: [[:progress, "Riders returned at dusk -- pass reported clear."]], stale: false },
118
+ { slug: "repair-the-north-bridge", epic_slug: "field-ops", status: :done,
119
+ context: "Crossbeams placed; load-bearing test passed.",
120
+ intent: "The north bridge must hold before the autumn floods.",
121
+ next_action: nil,
122
+ criteria: [["Timber delivered", :met], ["Crossbeams placed", :met], ["Load-bearing test", :met]],
123
+ notes: [[:progress, "Timber arrived a day ahead of schedule."], [:progress, "Load test held at double the rated weight."]],
124
+ stale: false },
125
+ { slug: "translate-the-sunken-tablet", epic_slug: "river-trade", status: :done,
126
+ context: "Scriptorium translator resolved the old script.",
127
+ intent: "The tablet may name the vault's true owner.",
128
+ next_action: nil,
129
+ criteria: [["Tablet recovered", :met], ["Translator found", :met]],
130
+ notes: [[:progress, "Scriptorium translator confirmed the vault's owner."]], stale: false },
131
+ { slug: "negotiate-toll-rights", epic_slug: "river-trade", status: :done,
132
+ context: "Guild council approved the toll rights.",
133
+ intent: "Toll rights unlock the southern supply route.",
134
+ next_action: nil,
135
+ criteria: [["Guild proposal submitted", :met], ["Council vote scheduled", :met]],
136
+ notes: [[:progress, "Council vote passed on the second reading."]], stale: false },
137
+ { slug: "draft-the-harvest-ledger", epic_slug: "field-ops", status: :done,
138
+ context: "Ledger reconciled against the quartermaster's count.",
139
+ intent: "The harvest ledger must balance before winter stores are sealed.",
140
+ next_action: nil,
141
+ criteria: [["Ledger drafted", :met], ["Counts reconciled", :met]],
142
+ notes: [[:progress, "Ledger balanced on the first reconciliation pass."]], stale: false },
143
+ { slug: "chart-the-southern-vault", epic_slug: "field-ops", status: :pending,
144
+ context: "Survey pass before the vault dig begins.",
145
+ intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
146
+ { slug: "restock-the-armory", epic_slug: "field-ops", status: :pending,
147
+ context: "Quartermaster count due before the next campaign.",
148
+ intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
149
+ { slug: "draft-the-spring-muster", epic_slug: "field-ops", status: :pending,
150
+ context: "First pass on troop assignments.",
151
+ intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
152
+ { slug: "ready-the-siege-engines", epic_slug: "field-ops", status: :pending,
153
+ context: "Inventory pass before the engines are wheeled out.",
154
+ intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
155
+ { slug: "survey-the-flooded-cellar", epic_slug: "river-trade", status: :pending,
156
+ context: "Waiting on a dry spell before the survey crew goes down.",
157
+ intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
158
+ { slug: "catalog-the-relic-hoard", epic_slug: "river-trade", status: :pending,
159
+ context: "First pass on the recovered hoard, pending an appraiser.",
160
+ intent: nil, next_action: nil, criteria: [], notes: [], stale: false }
161
+ ].freeze
162
+
163
+ EMPTY_LANE = ENV["SW_PARITY_EMPTY_LANE"]&.to_sym
164
+
165
+ @stories = DEFS.each_with_index.map do |d, i|
166
+ d.merge(
167
+ id: i + 1,
168
+ last_note_at: d[:status] == :in_progress ? (d[:stale] ? Time.now - (9 * 3600) : Time.now - (i + 1) * 240) : nil,
169
+ completed_at: d[:status] == :done ? Time.now - ((i + 1) * 3600 * 9) : nil
170
+ )
171
+ end
172
+
173
+ class << self
174
+ def all = @stories
175
+ def find(id) = @stories.find { |s| s[:id] == id.to_i }
176
+ def by_status(status) = status == EMPTY_LANE ? [] : @stories.select { |s| s[:status] == status }
177
+ end
178
+ end
179
+
180
+ def relative_time(t)
181
+ return "" unless t
182
+ mins = ((Time.now - t) / 60).round
183
+ return "#{mins}m ago" if mins < 60
184
+ hours = mins / 60
185
+ return "#{hours}h ago" if hours < 24
186
+ "#{hours / 24}d ago"
187
+ end
188
+
189
+ STATUS_LABEL = { pending: "pending", in_progress: "in progress", done: "done", blocked: "blocked" }.freeze
190
+ # Lane tone: -> board's own semantic tokens (--sw-success/warning/error/info),
191
+ # not tyrion's literal hex values -- the right abstraction level for a
192
+ # reusable option (design-parity-fights.md's own "not attempted" note #2).
193
+ LANE_TONE = { pending: :neutral, in_progress: :warning, blocked: :error, done: :success }.freeze
194
+
195
+ App = StreamWeaver::App.new(
196
+ "Tyrion War Room (components slice)",
197
+ chrome: false,
198
+ theme: :dashboard,
199
+ theme_overrides: { color_bg: "#0D0A07", color_text: "#F5E6C8" },
200
+ fonts: ["Cinzel:wght@400;600;700", "IBM+Plex+Mono:wght@300;400", "Lora:ital,wght@0,400;1,400;1,600"],
201
+ # Local-path auto-detection (App#resolve_stylesheet_href) serves this
202
+ # file via /sw-asset/... -- no hand-rolled endpoint needed, unlike the
203
+ # pixel slice's SW_PARITY_CSS proxy for the (much larger, real) shared.css.
204
+ stylesheets: stylesheet_list,
205
+ assets_dirs: [PARITY_ASSETS_DIR].compact
206
+ ) do
207
+ def art_glyph(url, glyph, css_class:)
208
+ if url
209
+ div(class: "#{css_class} tc-art-img", style: "background-image:url('#{url}');")
210
+ else
211
+ phrase(glyph, class: "#{css_class} tc-art-glyph")
212
+ end
213
+ end
214
+
215
+ current_action = state[StreamWeaver::Resource::StateKeys::ACTION]
216
+ current_story = current_action.is_a?(Symbol) && current_action.to_s.start_with?("story_") ? StoryStore.find(current_action.to_s.delete_prefix("story_")) : nil
217
+
218
+ # =========================================================================
219
+ # Topbar chrome: the real `topbar` component (stream_weaver-oeo) --
220
+ # design-parity-fights.md finding #6 closed, no more hand-assembled
221
+ # div/phrase soup here.
222
+ # =========================================================================
223
+ topbar(icon: CREST_URL || "๐Ÿฆ", wordmark: "TYRION",
224
+ breadcrumbs: ["field-ops", current_story ? current_story[:slug] : "warroom-components"]) do
225
+ phrase("โއ parity/warroom-components", class: "tc-pill")
226
+ phrase("โœ— 2", class: "tc-pill tc-pill--amber")
227
+ end
228
+ navbar(class: "tc-navbar") do
229
+ nav_item("โš” War Room", href: "/", active: current_story.nil?, close: current_story.nil?)
230
+ nav_item("๐Ÿ—บ Roadmap", href: "#")
231
+ nav_item("๐Ÿ“– Active Story", href: "#")
232
+ nav_item("๐ŸŒ Global View", href: "#")
233
+ nav_item("๐Ÿ’ก Discoveries", href: "#")
234
+ nav_item("โ“ About Tyrion", href: "#")
235
+ end
236
+
237
+ # =========================================================================
238
+ # app_shell owns the sidebar + main-content split -- the one real
239
+ # StreamWeaver layout primitive that maps directly onto tyrion's chrome.
240
+ # =========================================================================
241
+ app_shell(sidebar_position: :left, sidebar_width: "240px", gap: "0", class: "tc-shell") do
242
+ sidebar(sticky: true, class: "tc-sidebar") do
243
+ div(class: "tc-sidebar-path") { phrase("field-ops โ€บ warroom-components") }
244
+ div(class: "sw-sidebar-section") { phrase("Stories ยท warroom-components") }
245
+
246
+ StoryStore.all.each do |story|
247
+ icon_glyph = story[:status] == :pending ? "โ—‹" : "โ—"
248
+ clickable(href: "/stories/#{story[:id]}", class: "tc-story-row tc-story-row--#{story[:status]}") do
249
+ phrase(icon_glyph, class: "tc-story-icon")
250
+ phrase(story[:slug], class: "tc-story-name")
251
+ end
252
+ end
253
+
254
+ div(class: "tc-discoveries") do
255
+ div(class: "sw-sidebar-section") { phrase("Discoveries") }
256
+ clickable(href: "#", class: "tc-disc-row") do
257
+ phrase("spike", class: "tc-disc-pill tc-disc-pill--spike")
258
+ phrase("the flooded cellar timing", class: "tc-disc-label")
259
+ end
260
+ clickable(href: "#", class: "tc-disc-row") do
261
+ phrase("5 marks", class: "tc-disc-pill tc-disc-pill--mark")
262
+ phrase("unformalized", class: "tc-disc-label")
263
+ end
264
+ end
265
+ end
266
+
267
+ main do
268
+ # ======================================================================
269
+ # War Room board -- the actual Board/Lane/BoardCard proof.
270
+ # ======================================================================
271
+ page(:warroom, "/") do
272
+ div(class: "tc-board-hero") do
273
+ div(class: "tc-dragon-zone") do
274
+ art_glyph(DRAGON_URL, "๐Ÿ‰", css_class: "tc-dragon-glyph")
275
+ header3("Here Be Dragons")
276
+ text("Uncertain requirements")
277
+ text("External dependency")
278
+ text("Needs human decision")
279
+ end
280
+
281
+ board(pinned_headers: true) do
282
+ [[:pending, "Queue", "Pending"], [:in_progress, "Active Campaign", "In Progress"],
283
+ [:blocked, "Blocked Frontier", "Blocked"], [:done, "Shipped Keep", "Done"]].each do |status, title, sub|
284
+ stories = StoryStore.by_status(status)
285
+ # lane's icon: slot (stream_weaver-oeo) replaces the
286
+ # title-prefix workaround design-parity-fights.md catalogued.
287
+ lane(title, tone: LANE_TONE.fetch(status), subtitle: sub, icon: status == :blocked ? (DRAGON_URL || "๐Ÿ‰") : nil) do
288
+ stories.each { |story| render_wr_card(story) }
289
+ end
290
+ end
291
+ end
292
+ end
293
+ end
294
+
295
+ # ======================================================================
296
+ # Story detail -- no dedicated "story detail" component exists
297
+ # (same gap as the pixel slice); built from card/card_header/card_body,
298
+ # which do have sw- hooks, plus tc- classes for the parts that don't
299
+ # map onto any existing primitive (hero status line, resume strip).
300
+ # ======================================================================
301
+ StoryStore.all.each do |story|
302
+ page(:"story_#{story[:id]}", "/stories/#{story[:id]}") do
303
+ div(class: "tc-story-page") do
304
+ clickable(href: "/", class: "tc-back-link") { phrase("โ† War Room") }
305
+
306
+ card(class: "tc-story-hero") do
307
+ card_body do
308
+ div(class: "tc-hero-status") do
309
+ phrase(STATUS_LABEL.fetch(story[:status]), class: "sw-status-badge sw-status-badge--#{story[:status] == :in_progress ? 'maybe' : (story[:status] == :done ? 'strong' : 'skip')}")
310
+ if story[:last_note_at]
311
+ phrase("last note #{relative_time(story[:last_note_at])}", class: "tc-hero-meta")
312
+ end
313
+ end
314
+ header1(story[:slug], class: "tc-hero-title")
315
+
316
+ if story[:intent]
317
+ div(class: "tc-brief") do
318
+ phrase("MISSION BRIEF", class: "tc-brief-label")
319
+ text(story[:intent], class: "tc-brief-text")
320
+ end
321
+ end
322
+ end
323
+ end
324
+
325
+ card do
326
+ card_header("Current Context")
327
+ card_body { text(story[:context]) }
328
+ end
329
+
330
+ card do
331
+ card_header("Next Action")
332
+ card_body { text(story[:next_action] || "(not set)") }
333
+ end
334
+
335
+ div(class: "tc-story-split") do
336
+ card do
337
+ card_header("Criteria")
338
+ card_body do
339
+ if story[:criteria].any?
340
+ story[:criteria].each do |crit_text, status|
341
+ div(class: "tc-crit-row tc-crit-row--#{status}") do
342
+ phrase(status == :met ? "โœ“" : "โ—‹", class: "tc-crit-check")
343
+ phrase(crit_text, class: "tc-crit-text")
344
+ end
345
+ end
346
+ else
347
+ text("No criteria yet")
348
+ end
349
+ end
350
+ end
351
+
352
+ card do
353
+ card_header("Notes")
354
+ card_body do
355
+ note_anchor = story[:last_note_at] || story[:completed_at] || Time.now
356
+ story[:notes].each_with_index do |(kind, body), idx|
357
+ div(class: "tc-note-entry") do
358
+ phrase("#{kind} ยท #{relative_time(note_anchor - (idx * 3600))}", class: "tc-note-meta")
359
+ text(body, class: "tc-note-body")
360
+ end
361
+ end
362
+ end
363
+ end
364
+ end
365
+
366
+ if story[:status] == :in_progress || story[:next_action] || story[:last_note_at]
367
+ div(class: "tc-resume-strip") do
368
+ div(class: "tc-rs-panel") do
369
+ phrase("LAST NOTE", class: "tc-rs-label")
370
+ text(story[:notes].first ? "#{relative_time(story[:last_note_at])} โ€” \"#{story[:notes].first[1].to_s[0, 60]}\"" : "no notes yet", class: "tc-rs-text")
371
+ end
372
+ div(class: "tc-rs-panel tc-rs-panel--beacon") do
373
+ art_glyph(LANTERN_URL, "๐Ÿฎ", css_class: "tc-lantern-glyph")
374
+ div do
375
+ phrase("RESUME POINT", class: "tc-rs-label")
376
+ met = story[:criteria].count { |_, s| s == :met }
377
+ text(story[:criteria].any? ? "#{story[:slug]} ยท #{met}/#{story[:criteria].size} criteria met" : story[:slug], class: "tc-rs-text")
378
+ end
379
+ end
380
+ div(class: "tc-rs-panel") do
381
+ phrase("NEXT ACTION", class: "tc-rs-label")
382
+ text(story[:next_action] || "(not set)", class: "tc-rs-text")
383
+ end
384
+ end
385
+ end
386
+ end
387
+ end
388
+ end
389
+ end
390
+ end
391
+ end
392
+
393
+ def render_wr_card(story)
394
+ card_class = if story[:stale]
395
+ "tc-card--stale"
396
+ elsif story[:status] == :done
397
+ "tc-card--done"
398
+ end
399
+ clickable(href: "/stories/#{story[:id]}") do
400
+ board_card(tone: story[:status] == :blocked ? :error : nil, class: card_class) do
401
+ phrase("โš  STALE #{relative_time(story[:last_note_at])}", class: "tc-stale-badge") if story[:stale]
402
+ phrase(story[:epic_slug], class: "tc-card-epic")
403
+ header4(story[:slug])
404
+ text(story[:context].to_s[0, 60], class: "tc-card-ctx") if story[:context] && story[:status] != :done
405
+ if story[:status] == :done
406
+ phrase("โœ“ done ยท #{relative_time(story[:completed_at])}", class: "tc-card-done-meta")
407
+ else
408
+ badge(STATUS_LABEL.fetch(story[:status]), variant: :default, size: :sm)
409
+ end
410
+ end
411
+ end
412
+ end
413
+
414
+ App.generate.run! if __FILE__ == $0