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,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'set'
5
+
6
+ module StreamWeaver
7
+ # Registry for files served via the /sw-asset/ route.
8
+ # Maps a short SHA key → absolute path so only pre-registered files are served.
9
+ module ComponentAssets
10
+ @file_registry = {}
11
+
12
+ class << self
13
+ # Register a file and return its URL key.
14
+ def register_file(abs_path)
15
+ key = Digest::SHA256.hexdigest(abs_path)[0, 16]
16
+ @file_registry[key] = abs_path
17
+ key
18
+ end
19
+
20
+ # Resolve a key back to an absolute path, or nil if unknown.
21
+ def resolve_file(key)
22
+ @file_registry[key]
23
+ end
24
+
25
+ # Return the key for an already-registered path (registers if new).
26
+ def file_key(abs_path)
27
+ key = Digest::SHA256.hexdigest(abs_path)[0, 16]
28
+ @file_registry[key] ||= abs_path
29
+ key
30
+ end
31
+
32
+ # Walk a component tree and collect unique CSS/JS declarations.
33
+ # Returns [css_strings, css_paths, js_paths] — all deduped by component class.
34
+ def collect(components, seen_classes = Set.new)
35
+ css_strings = []
36
+ css_paths = []
37
+ js_paths = []
38
+
39
+ components.each do |c|
40
+ klass = c.class
41
+ unless seen_classes.include?(klass)
42
+ seen_classes << klass
43
+ if klass.respond_to?(:component_css_strings)
44
+ css_strings.concat(klass.component_css_strings)
45
+ end
46
+ if klass.respond_to?(:component_css_path) && klass.component_css_path
47
+ css_paths << klass.component_css_path
48
+ end
49
+ if klass.respond_to?(:component_js_path) && klass.component_js_path
50
+ js_paths << klass.component_js_path
51
+ end
52
+ end
53
+
54
+ if c.respond_to?(:children) && !c.children.empty?
55
+ child_css_s, child_css_p, child_js_p = collect(c.children, seen_classes)
56
+ css_strings.concat(child_css_s)
57
+ css_paths.concat(child_css_p)
58
+ js_paths.concat(child_js_p)
59
+ end
60
+ end
61
+
62
+ [css_strings, css_paths, js_paths]
63
+ end
64
+
65
+ def reset!
66
+ @file_registry.clear
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StreamWeaver
4
+ # Registry for third-party component classes.
5
+ #
6
+ # Allows downstream gems to contribute DSL methods without monkey-patching App or DisplayDSL.
7
+ # Registered components are available globally on every app instance via DisplayDSL.
8
+ #
9
+ # @example
10
+ # StreamWeaver.register_component(MyGem::Banner, dsl: :banner)
11
+ #
12
+ # Then in any app block:
13
+ # banner(text: "Hello") { ... }
14
+ module ComponentRegistry
15
+ @registry = {}
16
+
17
+ class << self
18
+ def register(klass, dsl:, adapter: :alpinejs)
19
+ dsl_name = dsl.to_sym
20
+ @registry[dsl_name] = { class: klass, adapter: adapter }
21
+
22
+ # Install the DSL method on DisplayDSL so every App picks it up immediately.
23
+ # with_container handles both leaf (no block) and container (block) usage.
24
+ DisplayDSL.module_eval do
25
+ define_method(dsl_name) do |*args, **kwargs, &block|
26
+ with_container(klass.new(*args, **kwargs), &block)
27
+ end
28
+ end
29
+ end
30
+
31
+ def [](dsl_name)
32
+ @registry[dsl_name.to_sym]
33
+ end
34
+
35
+ def registered?(dsl_name)
36
+ @registry.key?(dsl_name.to_sym)
37
+ end
38
+
39
+ def each(&block)
40
+ @registry.each(&block)
41
+ end
42
+
43
+ def all
44
+ @registry.dup
45
+ end
46
+
47
+ # Remove all registrations (intended for test isolation only).
48
+ def reset!
49
+ @registry.each_key do |dsl_name|
50
+ DisplayDSL.undef_method(dsl_name) if DisplayDSL.method_defined?(dsl_name)
51
+ end
52
+ @registry.clear
53
+ end
54
+ end
55
+ end
56
+
57
+ class << self
58
+ # Register a component class with a DSL method name.
59
+ #
60
+ # @param klass [Class] A class that inherits from Components::Base
61
+ # @param dsl [Symbol, String] The DSL method name to define on App/DisplayDSL
62
+ # @param adapter [Symbol] Adapter hint (:alpinejs, :opal) — informational for now
63
+ def register_component(klass, dsl:, adapter: :alpinejs)
64
+ ComponentRegistry.register(klass, dsl: dsl, adapter: adapter)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StreamWeaver
4
+ # Renders StreamWeaver components to HTML strings via Phlex.
5
+ # Used by Feed and Streamer to turn component DSL blocks into HTML
6
+ # that can be pushed via SSE or POST /stream/push.
7
+ #
8
+ # @example
9
+ # components = [Components::StatDisplay.new(value: 42, label: "RPS", color: :blue)]
10
+ # html = ComponentRenderer.render_html(adapter, components)
11
+ class ComponentRenderer < Phlex::HTML
12
+ attr_reader :adapter
13
+
14
+ def initialize(adapter, components, state = {})
15
+ @adapter = adapter
16
+ @components = Array(components)
17
+ @state = state
18
+ end
19
+
20
+ def view_template
21
+ @components.each { |c| c.render(self, @state) }
22
+ end
23
+
24
+ # Fragment scope tracking, matching Views::AppContentView. Components::Fragment
25
+ # asks every view it renders into for this, so a renderer without it raises
26
+ # NoMethodError on any app that uses `fragment`.
27
+ attr_reader :current_fragment_id
28
+
29
+ def with_fragment(id)
30
+ previous = @current_fragment_id
31
+ @current_fragment_id = id
32
+ yield
33
+ ensure
34
+ @current_fragment_id = previous
35
+ end
36
+
37
+ # Render components to an HTML string
38
+ #
39
+ # @param adapter [StreamWeaver::Adapter::Base] The adapter (default: AlpineJS)
40
+ # @param components [Array<Components::Base>] Components to render
41
+ # @param state [Hash] State hash (default: empty)
42
+ # @return [String] Rendered HTML
43
+ def self.render_html(adapter = nil, components = [], state = {})
44
+ adapter ||= Adapter::AlpineJS.new
45
+ new(adapter, components, state).call
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StreamWeaver
4
+ module Components
5
+ # Code block with line-number-pinned annotation bubbles in a side panel.
6
+ # Uses Prism.js for syntax highlighting. Note: highlighting is applied per line,
7
+ # so multi-line constructs (heredocs, block comments) may highlight each line
8
+ # independently — this component optimises for annotation alignment, not perfect highlighting.
9
+ # Renders a side-by-side layout: code pane left, annotation panel right.
10
+ # Each annotation is aligned to its target line; annotated lines get a subtle highlight.
11
+ # Annotations are positioned by line number (1-based, in-range). Out-of-range lines
12
+ # produce dangling bubbles with no highlighted counterpart — that is visible authoring feedback.
13
+ class AnnotatedCode < Base
14
+ Annotation = Struct.new(:line, :note, keyword_init: true)
15
+
16
+ attr_reader :language, :annotations
17
+
18
+ def initialize(language: nil, annotations: [], **options)
19
+ @language = language
20
+ @annotations = annotations.map { |a| Annotation.new(line: a[:line], note: a[:note]) }
21
+ @options = options
22
+ end
23
+
24
+ def code=(value)
25
+ @code = value.to_s
26
+ end
27
+
28
+ def code
29
+ @code || ""
30
+ end
31
+
32
+ def lines
33
+ code.lines
34
+ end
35
+
36
+ def line_count
37
+ lines.length
38
+ end
39
+
40
+ def language_class
41
+ language ? "language-#{language}" : "language-none"
42
+ end
43
+
44
+ def annotated_lines
45
+ @annotated_lines ||= Set.new(annotations.map(&:line))
46
+ end
47
+
48
+ def render(view, state)
49
+ view.adapter.render_annotated_code(view, self, state)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StreamWeaver
4
+ module Components
5
+ # Single API endpoint spec card: method badge, path, description, params table, response schema.
6
+ # DSL: api_endpoint(method: "POST", path: "/users", description: "Create user",
7
+ # params: [{name: "email", type: "string", required: true}],
8
+ # response: {id: "integer", email: "string"})
9
+ class ApiEndpoint < Base
10
+ DEFAULT_BADGE_COLOR = "#6b7280"
11
+ BADGE_COLORS = {
12
+ "GET" => "#2563eb",
13
+ "POST" => "#16a34a",
14
+ "PUT" => "#d97706",
15
+ "PATCH" => "#d97706",
16
+ "DELETE" => "#dc2626",
17
+ "HEAD" => DEFAULT_BADGE_COLOR,
18
+ "OPTIONS" => DEFAULT_BADGE_COLOR
19
+ }.freeze
20
+
21
+ attr_reader :http_method, :path, :description, :params, :response
22
+
23
+ def initialize(method:, path:, description: nil, params: [], response: {}, **options)
24
+ @http_method = method.to_s.upcase
25
+ @path = path.to_s
26
+ @description = description
27
+ @params = Array(params)
28
+ @response = response.is_a?(Hash) ? response : {}
29
+ @options = options
30
+ end
31
+
32
+ def badge_color = BADGE_COLORS.fetch(@http_method, DEFAULT_BADGE_COLOR)
33
+ def description? = @description && !@description.empty?
34
+ def params? = @params.any?
35
+ def response? = !@response.empty?
36
+
37
+ def render(view, state)
38
+ view.adapter.render_api_endpoint(view, self, state)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StreamWeaver
4
+ module Components
5
+ # Non-dismissible callout box with colored left border and icon area.
6
+ #
7
+ # Unlike Alert (which is dismissible), Callout is a static content box
8
+ # used for tips, warnings, notes, etc. in explainer documents.
9
+ #
10
+ # Variants: :info (blue), :warning (amber), :success (green),
11
+ # :error (red), :tip (purple)
12
+ #
13
+ # @example
14
+ # callout(variant: :warning, title: "Caution") do
15
+ # text "Be careful with this API."
16
+ # end
17
+ class Callout < Base
18
+ VARIANTS = %i[info warning success error tip decision risk].freeze
19
+
20
+ attr_reader :variant, :title
21
+ attr_accessor :children
22
+
23
+ # @param variant [Symbol] Callout type (:info, :warning, :success, :error, :tip)
24
+ # @param title [String, nil] Optional callout title
25
+ # @param options [Hash] Additional options
26
+ def initialize(variant: :info, title: nil, **options)
27
+ @variant = VARIANTS.include?(variant) ? variant : :info
28
+ @title = title
29
+ @options = options
30
+ @children = []
31
+ end
32
+
33
+ # Icon character for each variant
34
+ # @return [String] Unicode icon
35
+ def icon
36
+ case @variant
37
+ when :info then "\u2139\uFE0F" # info
38
+ when :warning then "\u26A0\uFE0F" # warning
39
+ when :success then "\u2705" # check
40
+ when :error then "\u274C" # cross
41
+ when :tip then "\u{1F4A1}" # lightbulb
42
+ when :decision then "\u2696\uFE0F" # scales (decision)
43
+ when :risk then "\u{1F53A}" # red triangle up (risk)
44
+ else "\u2139\uFE0F"
45
+ end
46
+ end
47
+
48
+ # CSS modifier class for the variant
49
+ # @return [String]
50
+ def variant_class
51
+ "sw-callout--#{@variant}"
52
+ end
53
+
54
+ def render(view, state)
55
+ view.adapter.render_callout(view, self, state)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module StreamWeaver
6
+ module Components
7
+ # Chart.js wrapper component.
8
+ # Renders a Chart.js chart (bar, line, pie, doughnut, radar).
9
+ # CDN loads lazily -- Chart.js 4 is only loaded when this component is used.
10
+ # Dark mode aware: reads --sw-text and --sw-border for grid/text colors.
11
+ #
12
+ # sw- CSS classes:
13
+ # sw-chart - outer container
14
+ # sw-chart__canvas - the canvas element
15
+ #
16
+ # @example Bar chart
17
+ # chart type: :bar, data: {
18
+ # labels: ["A", "B", "C"],
19
+ # datasets: [{ label: "Values", data: [1, 2, 3] }]
20
+ # }
21
+ #
22
+ # @example Line chart with custom height
23
+ # chart type: :line, data: { labels: [...], datasets: [...] }, height: 400
24
+ #
25
+ # @example Pie chart
26
+ # chart type: :pie, data: { labels: ["X", "Y"], datasets: [{ data: [60, 40] }] }
27
+ class Chart < Base
28
+ VALID_TYPES = %i[bar line pie doughnut radar].freeze
29
+ CHART_JS_CDN = "https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"
30
+
31
+ attr_reader :chart_type, :data, :chart_options, :height
32
+
33
+ # @param type [Symbol] Chart type (:bar, :line, :pie, :doughnut, :radar)
34
+ # @param data [Hash] Chart.js data config (labels, datasets)
35
+ # @param options [Hash] Chart.js options config
36
+ # @param height [Integer] Canvas height in pixels (default: 300)
37
+ def initialize(type:, data:, options: {}, height: 300, **extra)
38
+ @chart_type = type.to_sym
39
+ unless VALID_TYPES.include?(@chart_type)
40
+ raise ArgumentError, "Invalid chart type: #{@chart_type}. Must be one of: #{VALID_TYPES.join(', ')}"
41
+ end
42
+ @data = data
43
+ @chart_options = options
44
+ @height = height
45
+ super(**extra)
46
+ end
47
+
48
+ def render(view, state)
49
+ view.adapter.render_chartjs(view, self, state)
50
+ end
51
+
52
+ # Generate a unique canvas ID for this chart instance
53
+ def canvas_id
54
+ @canvas_id ||= "sw-chart-#{object_id}"
55
+ end
56
+
57
+ # Serialize data to JSON for the JS initializer
58
+ def data_json
59
+ JSON.generate(deep_stringify(@data))
60
+ end
61
+
62
+ # Serialize options to JSON for the JS initializer
63
+ def options_json
64
+ JSON.generate(deep_stringify(@chart_options))
65
+ end
66
+
67
+ private
68
+
69
+ # Deep-convert symbol keys to strings for JSON compatibility
70
+ def deep_stringify(obj)
71
+ case obj
72
+ when Hash
73
+ obj.each_with_object({}) do |(k, v), h|
74
+ h[k.to_s] = deep_stringify(v)
75
+ end
76
+ when Array
77
+ obj.map { |v| deep_stringify(v) }
78
+ else
79
+ obj
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StreamWeaver
4
+ module Components
5
+ # Syntax-highlighted code block component using Prism.js CDN.
6
+ #
7
+ # Renders code with optional language-based syntax highlighting,
8
+ # file path header, line truncation for thumbnails, and scrollable container.
9
+ #
10
+ # Uses Prism.js with autoloader plugin for lazy per-language loading.
11
+ # CDN scripts are only included when this component is used.
12
+ #
13
+ # @example Basic usage
14
+ # code_block("puts 'hello'", lang: "ruby")
15
+ #
16
+ # @example With file header
17
+ # code_block(code, file: "src/app.rb", lang: "ruby")
18
+ #
19
+ # @example Truncated for thumbnail
20
+ # code_block(code, truncate: 10, lang: "ruby")
21
+ class CodeBlock < Base
22
+ attr_reader :code, :lang, :file, :truncate, :scroll, :copy
23
+
24
+ # @param code [String] The source code to display
25
+ # @param lang [String, nil] Language for syntax highlighting (e.g. "ruby", "javascript")
26
+ # @param file [String, nil] File path to show in header bar
27
+ # @param truncate [Integer, nil] Maximum number of lines to show (nil = show all)
28
+ # @param scroll [Boolean] Whether to enable scrolling for long code (default: true)
29
+ # @param copy [Boolean] Show a copy-to-clipboard affordance in the header (default: false)
30
+ # @param options [Hash] Additional options
31
+ def initialize(code, lang: nil, file: nil, truncate: nil, scroll: true, copy: false, **options)
32
+ @code = code.to_s
33
+ @lang = lang
34
+ @file = file
35
+ @truncate = truncate
36
+ @scroll = scroll
37
+ @copy = copy
38
+ @options = options
39
+ end
40
+
41
+ # Return the display code, truncated if needed
42
+ # @return [String] The code to render
43
+ def display_code
44
+ return @code unless @truncate
45
+
46
+ lines = @code.lines
47
+ return @code if lines.length <= @truncate
48
+
49
+ lines.first(@truncate).join
50
+ end
51
+
52
+ # Whether the code was truncated
53
+ # @return [Boolean]
54
+ def truncated?
55
+ return false unless @truncate
56
+
57
+ @code.lines.length > @truncate
58
+ end
59
+
60
+ # Total line count of original code
61
+ # @return [Integer]
62
+ def total_lines
63
+ @code.lines.length
64
+ end
65
+
66
+ # Prism.js language class (e.g. "language-ruby")
67
+ # @return [String]
68
+ def language_class
69
+ @lang ? "language-#{@lang}" : "language-none"
70
+ end
71
+
72
+ def render(view, state)
73
+ view.adapter.render_code_block(view, self, state)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StreamWeaver
4
+ module Components
5
+ # Side-by-side comparison panels with "before" and "after" named regions.
6
+ #
7
+ # Each panel renders its children independently. On narrow viewports
8
+ # (<768px), panels stack vertically.
9
+ #
10
+ # Uses named block DSL: `before { ... }` and `after { ... }` inside
11
+ # the comparison block populate the two panels.
12
+ #
13
+ # @example
14
+ # comparison(before_label: "Old", after_label: "New") do
15
+ # before { text "Version 1" }
16
+ # after { text "Version 2" }
17
+ # end
18
+ class Comparison < Base
19
+ attr_reader :before_label, :after_label
20
+ attr_accessor :children, :before_children, :after_children
21
+
22
+ # @param before_label [String] Label for the "before" panel
23
+ # @param after_label [String] Label for the "after" panel
24
+ # @param options [Hash] Additional options
25
+ def initialize(before_label: "Before", after_label: "After", **options)
26
+ @before_label = before_label
27
+ @after_label = after_label
28
+ @options = options
29
+ @children = []
30
+ @before_children = []
31
+ @after_children = []
32
+ end
33
+
34
+ def render(view, state)
35
+ view.adapter.render_comparison(view, self, state)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StreamWeaver
4
+ module Components
5
+ # Architecture decision block with labeled options and a recommended flag.
6
+ # Renders a question heading with option cards; recommended option gets a badge,
7
+ # non-recommended options are visually de-emphasized.
8
+ #
9
+ # @example
10
+ # decision(question: "Which database should we use?") do
11
+ # option(id: :pg, label: "PostgreSQL", detail: "Full ACID, rich extensions", recommended: true)
12
+ # option(id: :sqlite, label: "SQLite", detail: "Zero-dependency, great for dev")
13
+ # end
14
+ class Decision < Base
15
+ Option = Struct.new(:id, :label, :detail, :recommended, keyword_init: true)
16
+
17
+ attr_reader :question
18
+
19
+ def initialize(question:, **options)
20
+ @question = question
21
+ @options = options
22
+ @decision_options = []
23
+ end
24
+
25
+ def options
26
+ @decision_options
27
+ end
28
+
29
+ def add_option(id:, label:, detail:, recommended: false)
30
+ @decision_options << Option.new(id: id, label: label, detail: detail, recommended: recommended)
31
+ end
32
+
33
+ def render(view, state)
34
+ view.adapter.render_decision(view, self, state)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StreamWeaver
4
+ module Components
5
+ module Deck
6
+ # Full-screen overlay shown after deck submit or cancel.
7
+ # Displays a status message with blur backdrop and optional auto-close tab.
8
+ #
9
+ # sw- CSS classes:
10
+ # sw-close-overlay - full-screen overlay wrapper
11
+ # sw-close-overlay--submitted - green status (submitted)
12
+ # sw-close-overlay--cancelled - amber status (cancelled)
13
+ # sw-close-overlay__backdrop - blur backdrop layer
14
+ # sw-close-overlay__content - centered content box
15
+ # sw-close-overlay__icon - status icon (checkmark or X)
16
+ # sw-close-overlay__message - status message text
17
+ # sw-close-overlay__countdown - auto-close countdown display
18
+ # sw-close-overlay__close-btn - manual close button
19
+ #
20
+ # @example
21
+ # close_overlay(status: :submitted, message: "Deck submitted successfully!")
22
+ # close_overlay(status: :cancelled, message: "Deck cancelled.")
23
+ class CloseOverlay < Base
24
+ attr_reader :status, :message, :auto_close_delay
25
+
26
+ VALID_STATUSES = [:submitted, :cancelled].freeze
27
+
28
+ # @param status [Symbol] Status type (:submitted or :cancelled)
29
+ # @param message [String] Status message to display
30
+ # @param auto_close_delay [Integer] Delay before auto-closing tab in ms (default: 800)
31
+ # @param options [Hash] Additional options
32
+ def initialize(status:, message:, auto_close_delay: 800, **options)
33
+ @status = validate_status(status)
34
+ @message = message
35
+ @auto_close_delay = auto_close_delay
36
+ super(**options)
37
+ end
38
+
39
+ def render(view, state)
40
+ view.adapter.render_close_overlay(view, self, state)
41
+ end
42
+
43
+ # Whether this is a submitted status.
44
+ # @return [Boolean]
45
+ def submitted?
46
+ @status == :submitted
47
+ end
48
+
49
+ # Whether this is a cancelled status.
50
+ # @return [Boolean]
51
+ def cancelled?
52
+ @status == :cancelled
53
+ end
54
+
55
+ # Status-specific CSS modifier class.
56
+ # @return [String]
57
+ def status_modifier
58
+ "sw-close-overlay--#{@status}"
59
+ end
60
+
61
+ # CSS class list
62
+ def css_classes
63
+ "sw-close-overlay #{status_modifier}"
64
+ end
65
+
66
+ # Status icon character.
67
+ # @return [String]
68
+ def icon
69
+ submitted? ? "✓" : "✕"
70
+ end
71
+
72
+ private
73
+
74
+ def validate_status(status)
75
+ status = status.to_sym
76
+ unless VALID_STATUSES.include?(status)
77
+ raise ArgumentError, "Invalid status '#{status}'. Must be one of: #{VALID_STATUSES.join(', ')}"
78
+ end
79
+ status
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end