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,1118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StreamWeaver
4
+ module Adapter
5
+ # Renderers for document components that carry no framework behavior.
6
+ #
7
+ # These emit plain markup and CSS -- no Alpine directives, no HTMX
8
+ # attributes, no server round-trips -- so they are identical whether the
9
+ # page is rendered by the server (AlpineJS adapter) or in the browser by
10
+ # Opal. Both adapters include this module rather than maintaining two
11
+ # copies.
12
+ #
13
+ # Including adapters must provide:
14
+ #
15
+ # #inject_component_css(view, key, css)
16
+ # Emit +css+ into the page once per key. The mechanism differs per
17
+ # adapter (a <style> tag server-side, a collected stylesheet in the
18
+ # browser), which is why it stays out of this module.
19
+ #
20
+ # #inject_sidebar_toc_assets(view)
21
+ # Emit the TOC stylesheet plus whatever powers scroll-spy.
22
+ #
23
+ # #inject_code_highlighting(view)
24
+ # Make a syntax highlighter available to the markup #render_code_block
25
+ # emits.
26
+ #
27
+ # #render_code_block_copy_button(view, component)
28
+ # Emit the clipboard affordance for a code block, if the adapter has
29
+ # one. That is behavior rather than document structure, so it is a hook
30
+ # instead of shared markup.
31
+ #
32
+ # #inject_mermaid_assets(view)
33
+ # Emit the mermaid stylesheet plus whatever powers rendering/zoom/pan.
34
+ #
35
+ # These are asset/behavior seams: the *markup* is identical across
36
+ # adapters but what backs it is not. Highlighting, scroll-spy, and copy
37
+ # decorate markup that is already complete without them, so an adapter
38
+ # can legitimately no-op those three -- the document still renders, it
39
+ # just won't highlight, scroll-spy, or copy. Mermaid is not like the
40
+ # other three: the JS *is* the renderer (the diagram source lives in a
41
+ # data attribute, not visible text), so a no-op #inject_mermaid_assets
42
+ # leaves an empty box, not a degraded-but-readable diagram. A host that
43
+ # renders mermaid at all must actually bundle the engine -- see
44
+ # OpalBuilder#copy_browser_assets, which does this unconditionally.
45
+ #
46
+ # The view object must respond to the tag methods used below plus #plain
47
+ # and #raw. Phlex satisfies this server-side; StreamWeaver::Opal::OpalRenderer
48
+ # implements the same surface in the browser.
49
+ module Static
50
+ DOC_HEADER_CSS = <<~CSS
51
+ /* ===========================================
52
+ DocHeader & DocSectionHeader (sw- prefix)
53
+ =========================================== */
54
+
55
+ /* Suppress the StreamWeaver app shell h1 — doc_header is the title */
56
+ body:has(.sw-doc-header) > h1 {
57
+ display: none;
58
+ }
59
+
60
+ /* Tighten body top padding when doc_header takes over the page header role */
61
+ body:has(.sw-doc-header) {
62
+ padding-top: 3rem;
63
+ }
64
+ .sw-doc-header {
65
+ border-bottom: 1px solid var(--sw-border, #e0e0e0);
66
+ padding: 2.5rem 0 2rem;
67
+ margin-bottom: 2.5rem;
68
+ }
69
+
70
+ .sw-doc-header__eyebrow {
71
+ font-family: var(--sw-font-mono, 'SFMono-Regular', 'Cascadia Code', monospace);
72
+ font-size: 0.69rem;
73
+ letter-spacing: 0.12em;
74
+ text-transform: uppercase;
75
+ color: var(--sw-text-dim, #6b6860);
76
+ margin-bottom: 0.75rem;
77
+ }
78
+
79
+ .sw-doc-header__title {
80
+ font-family: Charter, 'Bitstream Charter', 'Sitka Text', Cambria, Georgia, serif;
81
+ font-size: 2rem;
82
+ font-weight: 400;
83
+ line-height: 1.2;
84
+ color: var(--sw-text, #141413);
85
+ margin-bottom: 1.25rem;
86
+ }
87
+
88
+ .sw-doc-header__meta {
89
+ display: flex;
90
+ flex-wrap: wrap;
91
+ gap: 0.5rem 1.25rem;
92
+ font-size: 0.8125rem;
93
+ color: var(--sw-text-dim, #6b6860);
94
+ align-items: center;
95
+ }
96
+
97
+ .sw-doc-header__meta-item {
98
+ display: inline;
99
+ }
100
+
101
+ .sw-doc-header__pill {
102
+ display: inline-flex;
103
+ align-items: center;
104
+ font-size: 0.69rem;
105
+ font-weight: 500;
106
+ letter-spacing: 0.04em;
107
+ padding: 2px 8px;
108
+ border-radius: 3px;
109
+ }
110
+
111
+ .sw-doc-header__pill--default {
112
+ background: color-mix(in oklch, var(--sw-info, #2563eb) 12%, transparent);
113
+ color: var(--sw-info, #2563eb);
114
+ }
115
+
116
+ .sw-doc-header__pill--warn {
117
+ background: color-mix(in oklch, var(--sw-warning, #d97706) 12%, transparent);
118
+ color: color-mix(in oklch, var(--sw-warning, #d97706) 80%, #000);
119
+ }
120
+
121
+ .sw-doc-header__pill--good {
122
+ background: color-mix(in oklch, var(--sw-success, #16a34a) 12%, transparent);
123
+ color: color-mix(in oklch, var(--sw-success, #16a34a) 80%, #000);
124
+ }
125
+
126
+ /* Section eyebrow */
127
+ .sw-doc-section-header {
128
+ margin-bottom: 1rem;
129
+ }
130
+
131
+ .sw-doc-section-header__eyebrow {
132
+ font-family: var(--sw-font-mono, 'SFMono-Regular', 'Cascadia Code', monospace);
133
+ font-size: 0.66rem;
134
+ letter-spacing: 0.1em;
135
+ text-transform: uppercase;
136
+ color: var(--sw-text-dim, #a09d96);
137
+ margin-bottom: 0.625rem;
138
+ display: flex;
139
+ align-items: center;
140
+ gap: 0.625rem;
141
+ }
142
+
143
+ .sw-doc-section-header__eyebrow::after {
144
+ content: '';
145
+ flex: 1;
146
+ height: 1px;
147
+ background: var(--sw-border, #e0e0e0);
148
+ }
149
+
150
+ /* Use high specificity to beat body.sw-theme-document h2 rules */
151
+ .sw-doc-section-header > .sw-doc-section-header__title {
152
+ font-family: Charter, 'Bitstream Charter', 'Sitka Text', Cambria, Georgia, serif;
153
+ font-size: 1.4rem;
154
+ font-weight: 400;
155
+ line-height: 1.25;
156
+ color: var(--sw-text, #141413);
157
+ margin: 0;
158
+ padding-bottom: 0;
159
+ border-bottom: none;
160
+ }
161
+ CSS
162
+
163
+ # Moved here from adapter/alpinejs.rb verbatim (stream_weaver-mermaid-
164
+ # extension) -- this styling has no Alpine dependency and now backs
165
+ # #render_mermaid for both adapters.
166
+ def mermaid_css
167
+ <<~CSS
168
+ /* ===========================================
169
+ Mermaid Component Styles (sw- prefix)
170
+ =========================================== */
171
+ .sw-mermaid {
172
+ position: relative;
173
+ overflow: hidden;
174
+ border: 1px solid var(--sw-border, #e0e0e0);
175
+ border-radius: var(--sw-radius-md, 6px);
176
+ background: var(--sw-surface, #ffffff);
177
+ padding: 1rem;
178
+ margin: 0.5rem 0;
179
+ }
180
+
181
+ .sw-mermaid--compact {
182
+ padding: 0.25rem;
183
+ margin: 0;
184
+ border: none;
185
+ background: transparent;
186
+ }
187
+
188
+ .sw-mermaid--zoom {
189
+ cursor: grab;
190
+ min-height: 200px;
191
+ }
192
+
193
+ .sw-mermaid--zoom:active {
194
+ cursor: grabbing;
195
+ }
196
+
197
+ .sw-mermaid__diagram {
198
+ display: flex;
199
+ justify-content: center;
200
+ align-items: center;
201
+ min-height: 60px;
202
+ }
203
+
204
+ .sw-mermaid__diagram svg {
205
+ max-width: 100%;
206
+ height: auto;
207
+ }
208
+
209
+ /* views.rb's global `p { color: var(--sw-color-text-muted) }` typography rule
210
+ is unscoped and beats Mermaid's own inline node-label color (mermaid renders
211
+ HTML labels as <p> inside a foreignObject) -- inheritance loses to any rule
212
+ that targets the element directly, even one set via a colored `style X
213
+ color:#fff` directive on an ancestor. Net effect: every mermaid node label
214
+ renders the same muted gray regardless of the diagram author's color choice,
215
+ unreadable on a dark-filled node. Force these <p> tags back to inheriting
216
+ from Mermaid's own label wrapper. */
217
+ .sw-mermaid__diagram svg foreignObject p {
218
+ color: inherit !important;
219
+ }
220
+
221
+ .sw-mermaid--compact .sw-mermaid__diagram svg {
222
+ max-height: 150px;
223
+ }
224
+
225
+ .sw-mermaid__controls {
226
+ position: absolute;
227
+ top: 0.5rem;
228
+ right: 0.5rem;
229
+ display: flex;
230
+ gap: 0.25rem;
231
+ z-index: 10;
232
+ }
233
+
234
+ .sw-mermaid__btn {
235
+ width: 28px;
236
+ height: 28px;
237
+ display: flex;
238
+ align-items: center;
239
+ justify-content: center;
240
+ background: var(--sw-surface-elevated, #f3f3f3);
241
+ border: 1px solid var(--sw-border, #e0e0e0);
242
+ border-radius: var(--sw-radius-sm, 4px);
243
+ cursor: pointer;
244
+ font-size: 1rem;
245
+ line-height: 1;
246
+ color: var(--sw-text, #111);
247
+ transition: background 150ms ease-out;
248
+ }
249
+
250
+ .sw-mermaid__btn:hover {
251
+ background: var(--sw-accent, #0d9488);
252
+ color: #fff;
253
+ border-color: var(--sw-accent, #0d9488);
254
+ }
255
+
256
+ .sw-mermaid__error {
257
+ color: var(--sw-error, #dc2626);
258
+ font-family: var(--sw-font-mono, monospace);
259
+ font-size: 0.85rem;
260
+ padding: 1rem;
261
+ }
262
+
263
+ /* Fullscreen expand overlay (stream_weaver-yjv) -- the doc column's
264
+ max-width shrinks a wide diagram's fixed-px labels proportionally
265
+ no matter how the layout is tuned; the overlay removes that
266
+ constraint entirely instead of trying to out-negotiate it.
267
+ A <dialog> via showModal(), not a hand-rolled div: the browser's
268
+ top layer means no z-index arms race with this file's own
269
+ toasts/nav (see page_shell.rb, alpinejs.rb elsewhere), plus
270
+ Escape-to-close, focus management, and an inert background for
271
+ free -- overriding only the UA default box (border/padding/
272
+ background/position) that a plain <dialog> ships with. */
273
+ dialog.sw-mermaid-fullscreen-overlay {
274
+ max-width: none;
275
+ max-height: none;
276
+ width: 100%;
277
+ height: 100%;
278
+ margin: 0;
279
+ border: 0;
280
+ padding: 4rem 2rem 2rem;
281
+ background: transparent;
282
+ /* The dialog itself does NOT scroll -- .content does (below).
283
+ A first version had overflow: auto here, which made the
284
+ dialog the scroll container; close/hint are positioned
285
+ against the dialog, so they scrolled away with the diagram
286
+ instead of staying pinned to the viewport. Caught live: opened
287
+ a tall diagram and scrolled down past the hint text. */
288
+ overflow: hidden;
289
+ }
290
+
291
+ dialog.sw-mermaid-fullscreen-overlay[open] {
292
+ display: flex;
293
+ align-items: flex-start;
294
+ justify-content: center;
295
+ }
296
+
297
+ dialog.sw-mermaid-fullscreen-overlay::backdrop {
298
+ background: rgba(0, 0, 0, 0.75);
299
+ }
300
+
301
+ .sw-mermaid-fullscreen-overlay__content {
302
+ position: relative;
303
+ background: var(--sw-surface, #fff);
304
+ border-radius: var(--sw-radius-md, 6px);
305
+ padding: 1.5rem;
306
+ cursor: grab;
307
+ /* No max-width: the whole point is to render the diagram at
308
+ natural (or zoomed) size rather than shrink the SVG to fit.
309
+ max-height IS bounded, to the space the dialog's own padding
310
+ leaves -- that's what makes this the scroll container instead
311
+ of the dialog (see the dialog rule above). */
312
+ max-height: 100%;
313
+ overflow: auto;
314
+ overscroll-behavior: contain;
315
+ }
316
+
317
+ .sw-mermaid-fullscreen-overlay__svg-wrapper svg {
318
+ display: block;
319
+ height: auto; /* fallback only -- see cloneSvgWrapper for the real fix */
320
+ /* The real sizing (concrete px width/height from the SVG's own
321
+ viewBox) is set inline via JS (cloneSvgWrapper) -- mermaid's
322
+ width="100%" has nothing solid to resolve against inside
323
+ .content's flex layout otherwise, and no stylesheet rule can
324
+ outrank an inline style regardless. max-width: none here is
325
+ belt-and-suspenders for the same reason. */
326
+ max-width: none;
327
+ }
328
+
329
+ /* Positioned against the dialog itself (its nearest positioned
330
+ ancestor once it's the top-layer element), not the viewport --
331
+ no z-index needed inside a top-layer dialog. */
332
+ .sw-mermaid-fullscreen-overlay__close {
333
+ position: absolute;
334
+ top: 1rem;
335
+ right: 1.5rem;
336
+ width: 40px;
337
+ height: 40px;
338
+ display: flex;
339
+ align-items: center;
340
+ justify-content: center;
341
+ background: var(--sw-surface-elevated, #f3f3f3);
342
+ border: 1px solid var(--sw-border, #e0e0e0);
343
+ border-radius: var(--sw-radius-sm, 4px);
344
+ cursor: pointer;
345
+ font-size: 1.25rem;
346
+ line-height: 1;
347
+ color: var(--sw-text, #111);
348
+ }
349
+
350
+ .sw-mermaid-fullscreen-overlay__close:hover {
351
+ background: var(--sw-accent, #0d9488);
352
+ color: #fff;
353
+ border-color: var(--sw-accent, #0d9488);
354
+ }
355
+
356
+ .sw-mermaid-fullscreen-overlay__hint {
357
+ position: absolute;
358
+ bottom: 1rem;
359
+ left: 50%;
360
+ transform: translateX(-50%);
361
+ background: var(--sw-surface-elevated, #f3f3f3);
362
+ border: 1px solid var(--sw-border, #e0e0e0);
363
+ border-radius: var(--sw-radius-sm, 4px);
364
+ padding: 0.35rem 0.75rem;
365
+ font-size: 0.8rem;
366
+ color: var(--sw-text-dim, #6b6860);
367
+ white-space: nowrap;
368
+ }
369
+
370
+ /* views.rb sets `html { overflow-x: auto }` on every StreamWeaver
371
+ page, which makes <html> its own scroll container and stops
372
+ <body>'s overflow from propagating to the viewport -- so the
373
+ scroll lock has to sit on <html>, or it locks an element that
374
+ was never the one scrolling in the first place. */
375
+ html.sw-mermaid-fullscreen-open,
376
+ html.sw-mermaid-fullscreen-open body {
377
+ overflow: hidden;
378
+ }
379
+
380
+ @media print {
381
+ .sw-mermaid__controls { display: none; }
382
+ }
383
+ CSS
384
+ end
385
+
386
+ def render_text(view, content, tone = nil, options = {})
387
+ if tone
388
+ inject_text_tone_css(view)
389
+ css_classes = ["sw-text", "sw-text--#{tone}"]
390
+ css_classes << options[:class] if options[:class]
391
+ attrs = { class: css_classes.join(" ") }
392
+ attrs[:style] = options[:style] if options[:style]
393
+ view.p(**attrs) { content }
394
+ else
395
+ view.p { content }
396
+ end
397
+ end
398
+
399
+ def render_callout(view, component, state)
400
+ inject_callout_css(view)
401
+
402
+ view.div(class: "sw-callout #{component.variant_class}", role: "note") do
403
+ view.div(class: "sw-callout__icon", "aria-hidden" => "true") do
404
+ view.plain(component.icon)
405
+ end
406
+ view.div(class: "sw-callout__body") do
407
+ if component.title
408
+ view.div(class: "sw-callout__title") { component.title }
409
+ end
410
+ view.div(class: "sw-callout__content") do
411
+ component.children.each { |child| child.render(view, state) }
412
+ end
413
+ end
414
+ end
415
+ end
416
+
417
+ def render_comparison(view, component, state)
418
+ inject_comparison_css(view)
419
+
420
+ view.div(class: "sw-comparison") do
421
+ # Before panel
422
+ view.div(class: "sw-comparison__panel sw-comparison__panel--before") do
423
+ view.div(class: "sw-comparison__label") { component.before_label }
424
+ view.div(class: "sw-comparison__content") do
425
+ component.before_children.each { |child| child.render(view, state) }
426
+ end
427
+ end
428
+ # After panel
429
+ view.div(class: "sw-comparison__panel sw-comparison__panel--after") do
430
+ view.div(class: "sw-comparison__label") { component.after_label }
431
+ view.div(class: "sw-comparison__content") do
432
+ component.after_children.each { |child| child.render(view, state) }
433
+ end
434
+ end
435
+ end
436
+ end
437
+
438
+ def render_doc_header(view, component, state)
439
+ inject_doc_header_css(view)
440
+
441
+ view.header(class: "sw-doc-header") do
442
+ if component.eyebrow
443
+ view.div(class: "sw-doc-header__eyebrow") { view.plain(component.eyebrow) }
444
+ end
445
+ view.h1(class: "sw-doc-header__title") { view.plain(component.title) }
446
+ unless component.pills.empty?
447
+ view.div(class: "sw-doc-header__meta") do
448
+ component.pills.each do |pill|
449
+ if pill.is_a?(Hash)
450
+ variant = pill[:variant] || :default
451
+ view.span(class: "sw-doc-header__pill sw-doc-header__pill--#{variant}") do
452
+ view.plain(pill[:text].to_s)
453
+ end
454
+ else
455
+ view.span(class: "sw-doc-header__meta-item") { view.plain(pill.to_s) }
456
+ end
457
+ end
458
+ end
459
+ end
460
+ end
461
+ end
462
+
463
+ def render_doc_section_header(view, component, state)
464
+ inject_doc_header_css(view)
465
+
466
+ attrs = { class: "sw-doc-section-header" }
467
+ attrs[:id] = component.anchor_id if component.anchor_id
468
+ view.div(**attrs) do
469
+ view.div(class: "sw-doc-section-header__eyebrow") { view.plain(component.number) }
470
+ view.h2(class: "sw-doc-section-header__title") { view.plain(component.title) }
471
+ end
472
+ end
473
+
474
+ def inject_callout_css(view)
475
+ inject_component_css(view, :callout, callout_css)
476
+ end
477
+
478
+ def inject_comparison_css(view)
479
+ inject_component_css(view, :comparison, comparison_css)
480
+ end
481
+
482
+ def inject_text_tone_css(view)
483
+ inject_component_css(view, :text_tone, text_tone_css)
484
+ end
485
+
486
+ def inject_doc_header_css(view)
487
+ inject_component_css(view, :doc_header, doc_header_css)
488
+ end
489
+
490
+ def callout_css
491
+ <<~CSS
492
+ /* ===========================================
493
+ Callout Styles (sw- prefix, T11)
494
+ =========================================== */
495
+ .sw-callout {
496
+ display: flex;
497
+ gap: 0.75rem;
498
+ padding: var(--sw-callout-padding, 1rem 1.25rem);
499
+ border-radius: var(--sw-radius-md, 6px);
500
+ border-left: 4px solid var(--sw-info, #2563eb);
501
+ background: color-mix(in oklch, var(--sw-info) 6%, var(--sw-surface, #ffffff));
502
+ margin: 0.75rem 0;
503
+ }
504
+
505
+ .sw-callout__icon {
506
+ flex-shrink: 0;
507
+ font-size: 1.25rem;
508
+ line-height: 1.4;
509
+ }
510
+
511
+ .sw-callout__body {
512
+ flex: 1;
513
+ min-width: 0;
514
+ }
515
+
516
+ .sw-callout__title {
517
+ font-weight: 700;
518
+ font-size: 0.9375rem;
519
+ margin-bottom: 0.25rem;
520
+ color: var(--sw-text, #111111);
521
+ }
522
+
523
+ .sw-callout__content {
524
+ font-size: 0.875rem;
525
+ color: var(--sw-text, #111111);
526
+ line-height: 1.6;
527
+ }
528
+
529
+ /* Variant colors */
530
+ .sw-callout--info {
531
+ border-left-color: var(--sw-info, #2563eb);
532
+ background: color-mix(in oklch, var(--sw-info) 6%, var(--sw-surface, #ffffff));
533
+ }
534
+
535
+ .sw-callout--warning {
536
+ border-left-color: var(--sw-warning, #d97706);
537
+ background: color-mix(in oklch, var(--sw-warning) 6%, var(--sw-surface, #ffffff));
538
+ }
539
+
540
+ .sw-callout--success {
541
+ border-left-color: var(--sw-success, #16a34a);
542
+ background: color-mix(in oklch, var(--sw-success) 6%, var(--sw-surface, #ffffff));
543
+ }
544
+
545
+ .sw-callout--error {
546
+ border-left-color: var(--sw-error, #dc2626);
547
+ background: color-mix(in oklch, var(--sw-error) 6%, var(--sw-surface, #ffffff));
548
+ }
549
+
550
+ .sw-callout--tip {
551
+ border-left-color: #7c3aed;
552
+ background: color-mix(in oklch, #7c3aed 6%, var(--sw-surface, #ffffff));
553
+ }
554
+
555
+ .sw-callout--decision {
556
+ border-left-color: #b45309;
557
+ background: color-mix(in oklch, #b45309 6%, var(--sw-surface, #ffffff));
558
+ }
559
+
560
+ .sw-callout--risk {
561
+ border-left-color: #dc2626;
562
+ background: color-mix(in oklch, #dc2626 10%, var(--sw-surface, #ffffff));
563
+ }
564
+
565
+ /* Dark mode adjustments */
566
+ html.dark .sw-callout {
567
+ background: color-mix(in oklch, var(--sw-info) 8%, var(--sw-surface, oklch(0.205 0 0)));
568
+ }
569
+ html.dark .sw-callout--info {
570
+ background: color-mix(in oklch, var(--sw-info) 8%, var(--sw-surface, oklch(0.205 0 0)));
571
+ }
572
+ html.dark .sw-callout--warning {
573
+ background: color-mix(in oklch, var(--sw-warning) 8%, var(--sw-surface, oklch(0.205 0 0)));
574
+ }
575
+ html.dark .sw-callout--success {
576
+ background: color-mix(in oklch, var(--sw-success) 8%, var(--sw-surface, oklch(0.205 0 0)));
577
+ }
578
+ html.dark .sw-callout--error {
579
+ background: color-mix(in oklch, var(--sw-error) 8%, var(--sw-surface, oklch(0.205 0 0)));
580
+ }
581
+ html.dark .sw-callout--tip {
582
+ background: color-mix(in oklch, #a78bfa 8%, var(--sw-surface, oklch(0.205 0 0)));
583
+ }
584
+ html.dark .sw-callout--decision {
585
+ background: color-mix(in oklch, #fbbf24 8%, var(--sw-surface, oklch(0.205 0 0)));
586
+ }
587
+ html.dark .sw-callout--risk {
588
+ background: color-mix(in oklch, #f87171 10%, var(--sw-surface, oklch(0.205 0 0)));
589
+ }
590
+ CSS
591
+ end
592
+
593
+ def comparison_css
594
+ <<~CSS
595
+ /* ===========================================
596
+ Comparison Styles (sw- prefix, T11)
597
+ =========================================== */
598
+ .sw-comparison {
599
+ display: flex;
600
+ gap: 1rem;
601
+ margin: 0.75rem 0;
602
+ }
603
+
604
+ .sw-comparison__panel {
605
+ flex: 1;
606
+ min-width: 0;
607
+ border: 1px solid var(--sw-border, #e0e0e0);
608
+ border-radius: var(--sw-radius-md, 6px);
609
+ background: var(--sw-surface, #ffffff);
610
+ overflow: hidden;
611
+ }
612
+
613
+ .sw-comparison__label {
614
+ padding: 0.5rem 1rem;
615
+ font-size: 0.75rem;
616
+ font-weight: 700;
617
+ text-transform: uppercase;
618
+ letter-spacing: 0.05em;
619
+ color: var(--sw-text-dim, #444444);
620
+ background: var(--sw-surface-elevated, #f3f3f3);
621
+ border-bottom: 1px solid var(--sw-border, #e0e0e0);
622
+ }
623
+
624
+ .sw-comparison__content {
625
+ padding: 1rem;
626
+ }
627
+
628
+ /* Before panel: subtle red/warm tint */
629
+ .sw-comparison__panel--before {
630
+ border-top: 3px solid var(--sw-error, #dc2626);
631
+ }
632
+
633
+ /* After panel: subtle green/cool tint */
634
+ .sw-comparison__panel--after {
635
+ border-top: 3px solid var(--sw-success, #16a34a);
636
+ }
637
+
638
+ /* Responsive: stack vertically on narrow viewports */
639
+ @media (max-width: 767px) {
640
+ .sw-comparison {
641
+ flex-direction: column;
642
+ }
643
+ }
644
+ CSS
645
+ end
646
+
647
+ def text_tone_css
648
+ <<~CSS
649
+ /* ===========================================
650
+ Text Tone Variants (sw- prefix, FAC-P2.2)
651
+ =========================================== */
652
+ .sw-text--muted {
653
+ color: var(--sw-color-text-muted, #6b7280);
654
+ }
655
+
656
+ .sw-text--caption {
657
+ font-size: var(--sw-font-size-sm, 0.875rem);
658
+ color: var(--sw-color-text-muted, #6b7280);
659
+ }
660
+
661
+ .sw-text--error {
662
+ color: var(--sw-error, #dc2626);
663
+ }
664
+
665
+ .sw-text--success {
666
+ color: var(--sw-success, #16a34a);
667
+ }
668
+ CSS
669
+ end
670
+
671
+ def doc_header_css
672
+ DOC_HEADER_CSS
673
+ end
674
+
675
+ def render_sidebar_toc(view, component, state)
676
+ inject_sidebar_toc_assets(view)
677
+
678
+ view.nav(class: "sw-sidebar-toc", "aria-label" => "Table of contents") do
679
+ view.div(class: "sw-sidebar-toc__nav") do
680
+ component.sections.each do |section|
681
+ view.a(
682
+ class: "sw-sidebar-toc__link",
683
+ href: "##{section[:id]}",
684
+ "data-sw-toc-target" => section[:id]
685
+ ) { section[:label] }
686
+ end
687
+ end
688
+ end
689
+ end
690
+
691
+ # Markup shape sw-mermaid-zoom.js expects, shared by both adapters
692
+ # (stream_weaver-mermaid-extension): a `.sw-mermaid__controls` button
693
+ # bar (unicode glyphs, not inline SVG -- SharePoint's HTML-preview
694
+ # sanitizer was found to strip an <svg> button silently while three
695
+ # plain-text-node buttons next to it survived, see the expand button
696
+ # below), plus a `.sw-mermaid__diagram` div carrying the code via a
697
+ # data attribute rather than text content, so the JS can re-render
698
+ # into it without re-parsing displayed text.
699
+ #
700
+ # This used to be AlpineJS-only, on the theory that mermaid was
701
+ # "behavior" (Alpine's x-data/x-init) rather than structure. It never
702
+ # was -- sw-mermaid-zoom.js self-inits on DOMContentLoaded the same
703
+ # way sw-sidebar-toc.js does, with no Alpine dependency at all. The
704
+ # Opal adapter's separate, simpler version (bare code as text content,
705
+ # no controls) just never grew the zoom/expand markup AlpineJS did,
706
+ # which meant every Opal-rendered host -- opal-build's standalone
707
+ # HTML and the browser extension -- silently lost zoom/pan/expand.
708
+ def render_mermaid(view, component, state)
709
+ inject_mermaid_assets(view)
710
+
711
+ attrs = { id: component.diagram_id, class: component.css_classes }
712
+ attrs["data-sw-mermaid-elk"] = "true" if component.elk?
713
+ attrs["data-sw-mermaid-vars"] = component.theme_vars_json if component.theme_vars
714
+
715
+ view.div(**attrs) do
716
+ # Controls: expand is always available (stream_weaver-yjv) --
717
+ # the in-place zoom mechanism (zoom: true) helps but doesn't fix
718
+ # the actual problem, which is the container itself: a wide
719
+ # diagram's fixed-px labels shrink proportionally to fit an
720
+ # 800-1400px doc column regardless of pan/zoom. Expand opens the
721
+ # diagram in a full-viewport overlay instead, with no container
722
+ # width to shrink against. +/-/reset stay opt-in via zoom: true;
723
+ # expand needs no opt-in since it has no in-place layout cost.
724
+ view.div(class: "sw-mermaid__controls") do
725
+ if component.zoom
726
+ view.button(
727
+ type: "button",
728
+ class: "sw-mermaid__btn",
729
+ "data-sw-zoom" => "in",
730
+ "aria-label" => "Zoom in",
731
+ title: "Zoom in"
732
+ ) { "+" }
733
+ view.button(
734
+ type: "button",
735
+ class: "sw-mermaid__btn",
736
+ "data-sw-zoom" => "out",
737
+ "aria-label" => "Zoom out",
738
+ title: "Zoom out"
739
+ ) { "−" }
740
+ view.button(
741
+ type: "button",
742
+ class: "sw-mermaid__btn",
743
+ "data-sw-zoom" => "reset",
744
+ "aria-label" => "Reset zoom",
745
+ title: "Reset"
746
+ ) { "↺" }
747
+ end
748
+ view.button(
749
+ type: "button",
750
+ class: "sw-mermaid__btn",
751
+ "data-sw-zoom" => "expand",
752
+ "aria-label" => "Expand to full screen",
753
+ title: "Expand to full screen"
754
+ ) { "⛶" }
755
+ end
756
+
757
+ # The diagram rendering area.
758
+ # Mermaid code stored as data attribute; JS reads it to render.
759
+ view.div(
760
+ class: "sw-mermaid__diagram",
761
+ "data-sw-mermaid-code" => component.code
762
+ )
763
+ end
764
+ end
765
+
766
+ def render_code_block(view, component, state)
767
+ inject_code_highlighting(view)
768
+
769
+ view.div(class: "sw-code-block") do
770
+ # Header bar (optional) -- file path label and/or copy affordance
771
+ if component.file || component.copy
772
+ view.div(class: "sw-code-block__header") do
773
+ if component.file
774
+ view.span(class: "sw-code-block__file") { component.file }
775
+ end
776
+ render_code_block_copy_button(view, component) if component.copy
777
+ end
778
+ end
779
+
780
+ # Code container
781
+ scroll_style = component.scroll ? "overflow: auto; max-height: 500px;" : ""
782
+ view.div(class: "sw-code-block__body", style: scroll_style) do
783
+ view.pre(class: "sw-code-block__pre") do
784
+ view.code(class: component.language_class) do
785
+ view.plain(component.display_code)
786
+ end
787
+ end
788
+ end
789
+
790
+ # Truncation indicator
791
+ if component.truncated?
792
+ view.div(class: "sw-code-block__truncated") do
793
+ view.plain("... #{component.total_lines - component.truncate} more lines")
794
+ end
795
+ end
796
+ end
797
+ end
798
+
799
+ def sidebar_toc_css
800
+ <<~CSS
801
+ /* ===========================================
802
+ SidebarToc Styles (sw- prefix, T11)
803
+ =========================================== */
804
+ .sw-sidebar-toc {
805
+ position: sticky;
806
+ top: 1rem;
807
+ z-index: 10;
808
+ }
809
+
810
+ /* Undo the base overflow-x:auto on #app-container (views.rb's
811
+ `body[class*="sw-layout-"] > #app-container`) -- a non-visible
812
+ overflow-x computes overflow-y to auto, making the container a
813
+ scroll container and killing position:sticky for the TOC. Must
814
+ out-specify that selector (#app-container:has(...) alone is one
815
+ type selector short, see spec/components/sidebar_toc_spec.rb)
816
+ and must stay outside @media: the mobile top nav is sticky too.
817
+ Code/diff/board components scroll locally (e.g.
818
+ .sw-code-block__body), so this doesn't affect them; a wide
819
+ *table* with no scrollable:/sticky_header: option has no local
820
+ overflow wrapper and will now overflow to the page-level
821
+ `html { overflow-x: auto }` scroller instead -- accepted, since
822
+ a stuck TOC beats a nested table scrollbar. */
823
+ body[class*="sw-layout-"] > #app-container:has(> .sw-sidebar-toc) {
824
+ overflow: visible;
825
+ }
826
+
827
+ .sw-sidebar-toc__nav {
828
+ counter-reset: sw-toc-counter;
829
+ }
830
+
831
+ /* Desktop: vertical sidebar */
832
+ @media (min-width: 1000px) {
833
+ .sw-sidebar-toc {
834
+ grid-column: 1;
835
+ /* Span a large number of implicit rows so the sidebar's grid
836
+ area covers the full content height (unknown ahead of time,
837
+ since content renders as flat siblings with no wrapper) —
838
+ without this, position:sticky only holds within the
839
+ sidebar's own (short) auto row. */
840
+ grid-row: 1 / 9999;
841
+ align-self: start;
842
+ top: 2rem;
843
+ }
844
+
845
+ .sw-sidebar-toc__nav {
846
+ display: flex;
847
+ flex-direction: column;
848
+ gap: 0.25rem;
849
+ max-height: calc(100vh - 4rem);
850
+ overflow-y: auto;
851
+ }
852
+ }
853
+
854
+ /* Mobile: horizontal scrollable bar */
855
+ @media (max-width: 999px) {
856
+ .sw-sidebar-toc {
857
+ top: 0;
858
+ background: var(--sw-surface, #ffffff);
859
+ border-bottom: 1px solid var(--sw-border, #e0e0e0);
860
+ padding: 0.5rem 0;
861
+ margin: 0 -1rem 1rem -1rem;
862
+ width: calc(100% + 2rem);
863
+ }
864
+
865
+ .sw-sidebar-toc__nav {
866
+ display: flex;
867
+ flex-direction: row;
868
+ gap: 0.25rem;
869
+ overflow-x: auto;
870
+ -webkit-overflow-scrolling: touch;
871
+ scrollbar-width: none;
872
+ padding: 0 1rem;
873
+ }
874
+
875
+ .sw-sidebar-toc__nav::-webkit-scrollbar {
876
+ display: none;
877
+ }
878
+
879
+ .sw-sidebar-toc__link {
880
+ white-space: nowrap;
881
+ flex-shrink: 0;
882
+ }
883
+ }
884
+
885
+ .sw-sidebar-toc__link {
886
+ display: flex;
887
+ align-items: baseline;
888
+ gap: 0.5rem;
889
+ counter-increment: sw-toc-counter;
890
+ padding: 0.375rem 0.75rem;
891
+ font-size: 0.8125rem;
892
+ color: var(--sw-text-dim, #444444);
893
+ text-decoration: none;
894
+ border-radius: var(--sw-radius-sm, 4px);
895
+ border-left: 2px solid transparent;
896
+ transition: color 150ms ease-out, background 150ms ease-out, border-color 150ms ease-out;
897
+ }
898
+
899
+ .sw-sidebar-toc__link::before {
900
+ content: counter(sw-toc-counter, decimal-leading-zero);
901
+ flex-shrink: 0;
902
+ font-family: var(--sw-font-mono, 'SFMono-Regular', 'Cascadia Code', monospace);
903
+ font-size: 0.625rem;
904
+ color: var(--sw-text-dim, #6b7280);
905
+ }
906
+
907
+ .sw-sidebar-toc__link:hover {
908
+ color: var(--sw-text, #111111);
909
+ background: var(--sw-surface-elevated, #f3f3f3);
910
+ }
911
+
912
+ .sw-sidebar-toc__link.sw-is-active {
913
+ color: var(--sw-accent, #0d9488);
914
+ border-left-color: var(--sw-accent, #0d9488);
915
+ font-weight: 600;
916
+ background: color-mix(in oklch, var(--sw-accent) 6%, transparent);
917
+ }
918
+
919
+ @media (max-width: 999px) {
920
+ .sw-sidebar-toc__link {
921
+ border-left: none;
922
+ border-bottom: 2px solid transparent;
923
+ }
924
+
925
+ .sw-sidebar-toc__link.sw-is-active {
926
+ border-left-color: transparent;
927
+ border-bottom-color: var(--sw-accent, #0d9488);
928
+ }
929
+ }
930
+
931
+ /* ── Document layout fix ──
932
+ When sidebar_toc is present:
933
+ - Expand the body to give the sidebar room
934
+ - Remove the card chrome from #app-container
935
+ - Remove top padding (hidden h1 leaves a gap) */
936
+ @media (min-width: 1000px) {
937
+ /* All the :has() checks below are scoped to a DIRECT child
938
+ sidebar_toc/doc_header (the flat prd_dsl.rb-style page
939
+ pattern: sidebar_toc and doc content as siblings directly
940
+ inside #app-container). Without the ">" combinator, :has()
941
+ matches sidebar_toc/doc_header at ANY depth -- so a page
942
+ that nests sidebar_toc deep inside an app_shell (its own
943
+ main/columns/column layout) would still hijack #app-container
944
+ into this grid. Since app_shell is then #app-container's
945
+ ONLY child, CSS Grid auto-placement drops it into just the
946
+ first (toc-width) column and leaves the second column empty
947
+ -- collapsing the entire app_shell to ~220px wide. */
948
+ body:has(> #app-container > .sw-sidebar-toc) {
949
+ max-width: 1200px;
950
+ }
951
+
952
+ body:has(> #app-container > .sw-doc-header) > h1 + #app-container,
953
+ #app-container:has(> .sw-sidebar-toc) {
954
+ padding-top: 0;
955
+ box-shadow: none;
956
+ border: none;
957
+ background: transparent;
958
+ border-radius: 0;
959
+ }
960
+
961
+ /* Sidebar + content as a CSS grid (replaces the old
962
+ float + negative-margin hack, which let the sidebar scroll
963
+ away instead of staying sticky). The overflow:visible fix
964
+ that makes position:sticky work lives on the
965
+ `body[class*="sw-layout-"] > #app-container:has(> .sw-sidebar-toc)`
966
+ rule above, not here -- it has to apply below 1000px too,
967
+ for the mobile top nav. */
968
+ #app-container:has(> .sw-sidebar-toc) {
969
+ --sw-toc-width: 220px;
970
+ --sw-toc-gap: 2rem;
971
+ display: grid;
972
+ grid-template-columns: var(--sw-toc-width) minmax(0, 1fr);
973
+ column-gap: var(--sw-toc-gap);
974
+ }
975
+
976
+ /* doc_header stays a normal column-2 grid item (never claims
977
+ column 1) so it can't block the sidebar's row-span below —
978
+ it bleeds visually full-width via negative margin instead. */
979
+ #app-container:has(> .sw-sidebar-toc) > .sw-doc-header {
980
+ margin-left: calc(-1 * (var(--sw-toc-width) + var(--sw-toc-gap)));
981
+ padding-left: calc(var(--sw-toc-width) + var(--sw-toc-gap));
982
+ }
983
+ }
984
+ CSS
985
+ end
986
+
987
+ def render_diff_block(view, component, state)
988
+ inject_code_highlighting(view)
989
+ inject_diff_block_css(view)
990
+
991
+ parsed = component.parsed_lines
992
+ view.div(class: "sw-diff-block") do
993
+ view.pre(class: "sw-diff-block__pre") do
994
+ parsed.each do |dl|
995
+ if dl.type == :hunk_header
996
+ view.span(class: "sw-diff-block__hunk-header") { view.plain(dl.content) }
997
+ next
998
+ end
999
+
1000
+ line_class = case dl.type
1001
+ when :removed then "sw-diff-block__line sw-diff-block__line--removed"
1002
+ when :added then "sw-diff-block__line sw-diff-block__line--added"
1003
+ else "sw-diff-block__line sw-diff-block__line--context"
1004
+ end
1005
+
1006
+ view.span(class: line_class) do
1007
+ view.span(class: "sw-diff-block__gutter") do
1008
+ old_str = dl.old_num ? dl.old_num.to_s : ""
1009
+ new_str = dl.new_num ? dl.new_num.to_s : ""
1010
+ view.span(class: "sw-diff-block__gutter-old") { view.plain(old_str) }
1011
+ view.span(class: "sw-diff-block__gutter-new") { view.plain(new_str) }
1012
+ end
1013
+ view.span(class: "sw-diff-block__prefix") { view.plain(dl.prefix) }
1014
+ view.code(class: component.language_class) { view.plain(dl.content) }
1015
+ end
1016
+ end
1017
+ end
1018
+ end
1019
+ end
1020
+
1021
+ def inject_diff_block_css(view)
1022
+ inject_component_css(view, :diff_block, diff_block_css)
1023
+ end
1024
+
1025
+ def diff_block_css
1026
+ <<~CSS
1027
+ /* -- DiffBlock -- */
1028
+ .sw-diff-block {
1029
+ border: 1px solid var(--sw-border, #e0e0e0);
1030
+ border-radius: var(--sw-radius-md, 6px);
1031
+ overflow: hidden;
1032
+ margin: 0.75rem 0;
1033
+ background: var(--sw-surface, #1d1f21);
1034
+ color: #c5c8c6;
1035
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1036
+ font-size: 0.875rem;
1037
+ }
1038
+
1039
+ .sw-diff-block__pre {
1040
+ margin: 0;
1041
+ padding: 0.5rem 0;
1042
+ background: transparent;
1043
+ white-space: pre;
1044
+ overflow-x: auto;
1045
+ }
1046
+
1047
+ .sw-diff-block__hunk-header {
1048
+ display: block;
1049
+ padding: 0.15rem 0.75rem;
1050
+ color: var(--sw-text-dim, #6b7280);
1051
+ background: color-mix(in oklch, #4b5563 15%, transparent);
1052
+ font-style: italic;
1053
+ white-space: pre;
1054
+ }
1055
+
1056
+ .sw-diff-block__line {
1057
+ display: flex;
1058
+ align-items: baseline;
1059
+ line-height: 1.5em;
1060
+ min-height: 1.5em;
1061
+ padding-right: 0.75rem;
1062
+ }
1063
+
1064
+ .sw-diff-block__line--removed {
1065
+ background: color-mix(in oklch, #dc2626 14%, transparent);
1066
+ }
1067
+
1068
+ .sw-diff-block__line--added {
1069
+ background: color-mix(in oklch, #16a34a 14%, transparent);
1070
+ }
1071
+
1072
+ .sw-diff-block__gutter {
1073
+ display: inline-flex;
1074
+ gap: 0;
1075
+ user-select: none;
1076
+ color: var(--sw-text-dim, #6b7280);
1077
+ flex-shrink: 0;
1078
+ }
1079
+
1080
+ .sw-diff-block__gutter-old,
1081
+ .sw-diff-block__gutter-new {
1082
+ display: inline-block;
1083
+ width: 2.5rem;
1084
+ padding: 0 0.5rem;
1085
+ text-align: right;
1086
+ }
1087
+
1088
+ .sw-diff-block__prefix {
1089
+ display: inline-block;
1090
+ width: 1.2rem;
1091
+ text-align: center;
1092
+ flex-shrink: 0;
1093
+ user-select: none;
1094
+ color: var(--sw-text-dim, #6b7280);
1095
+ }
1096
+
1097
+ .sw-diff-block__line--removed .sw-diff-block__prefix {
1098
+ color: #f87171;
1099
+ }
1100
+
1101
+ .sw-diff-block__line--added .sw-diff-block__prefix {
1102
+ color: #4ade80;
1103
+ }
1104
+
1105
+ .sw-diff-block__line code {
1106
+ background: transparent;
1107
+ color: inherit;
1108
+ padding: 0;
1109
+ font-size: inherit;
1110
+ font-family: inherit;
1111
+ white-space: pre;
1112
+ flex: 1;
1113
+ }
1114
+ CSS
1115
+ end
1116
+ end
1117
+ end
1118
+ end