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,3940 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'phlex'
4
+ require 'json'
5
+
6
+ module StreamWeaver
7
+ module Views
8
+ # Full page view for initial load (includes <html>, <head>, <body>)
9
+ class AppView < Phlex::HTML
10
+ attr_reader :adapter, :app
11
+ attr_reader :current_fragment_id
12
+
13
+ # @param app [StreamWeaver::App] The app instance
14
+ # @param state [Hash] The current state
15
+ # @param adapter [StreamWeaver::Adapter::Base] The adapter for rendering
16
+ # @param is_agentic [Boolean] Whether running in agentic mode
17
+ # @param session_theme [Symbol, nil] Theme override from session
18
+ def initialize(app, state, adapter, is_agentic = false, session_theme: nil)
19
+ @app = app
20
+ @state = state
21
+ @adapter = adapter
22
+ @is_agentic = is_agentic
23
+ @session_theme = session_theme
24
+ @scripts = app.scripts
25
+ @stylesheets = app.stylesheets
26
+ @inline_stylesheets = app.inline_stylesheets
27
+ end
28
+
29
+ def view_template
30
+ # Render the body content first so that any component CSS injected
31
+ # along the way (adapter#inject_component_css) lands in
32
+ # @component_css_registry before <head> is built below -- <head>
33
+ # must come first in document order, but which components (and thus
34
+ # which CSS) will render is only known by actually rendering them
35
+ # (stream_weaver-1lo). Captured rather than appended directly so it
36
+ # can be replayed into <body> after <head> is emitted.
37
+ body_html = capture { render_page_body }
38
+
39
+ doctype
40
+ html do
41
+ head do
42
+ # Pins cascade layer order before any framework CSS is emitted
43
+ # (stream_weaver-oeo). All framework CSS lives in this one named
44
+ # layer; any unlayered user stylesheet always wins the cascade
45
+ # regardless of specificity or document order, so the
46
+ # stream_weaver-1lo hoist-to-<head> trick below is now belt-and-
47
+ # suspenders rather than the only thing making user CSS win.
48
+ style { raw(safe("@layer #{StreamWeaver::CSS::LAYER_NAME};")) }
49
+
50
+ title { @app.title }
51
+ meta(charset: "utf-8")
52
+ meta(name: "viewport", content: "width=device-width, initial-scale=1")
53
+ # Favicon (emoji or URL)
54
+ if (fav = @app.favicon_href)
55
+ link(rel: "icon", href: fav)
56
+ end
57
+ # Inject adapter-specific CDN scripts using Phlex methods
58
+ @adapter.render_cdn_scripts(self)
59
+
60
+ # Framework-injected component CSS (collected during the body
61
+ # render above), before user stylesheets: below -- so that at
62
+ # equal specificity, user stylesheets win on document order
63
+ # instead of always losing to the framework's own rule
64
+ # (stream_weaver-1lo). Also wrapped in the shared layer
65
+ # (stream_weaver-oeo) so this ordering is now a nicety, not a
66
+ # requirement -- an unlayered user rule wins even if it lands
67
+ # earlier in the document.
68
+ unless component_css_registry.empty?
69
+ style { raw(safe(StreamWeaver::CSS.layer_wrap(component_css_registry.values.join("\n")))) }
70
+ end
71
+
72
+ # Add custom stylesheets
73
+ @stylesheets.each do |href|
74
+ link(rel: "stylesheet", href: href)
75
+ end
76
+
77
+ # Inline stylesheets declared via `use_stylesheet` in the DSL
78
+ # body (stream_weaver-9uk) -- same document-order/cascade
79
+ # position as the linked stylesheets above.
80
+ @inline_stylesheets.each do |css|
81
+ style { raw(safe(css)) }
82
+ end
83
+
84
+ # Add custom scripts
85
+ @scripts.each do |src|
86
+ script(src: src)
87
+ end
88
+
89
+ # Component-scoped CSS/JS (declared via css/css_path/js_path class macros)
90
+ render_component_assets
91
+
92
+ # Chart.js CDN - only load when charts are present
93
+ if @app.has_charts?
94
+ script(src: "https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js")
95
+ end
96
+
97
+ # SSE client for streaming push updates (when app has stream block or timers)
98
+ if (@app.stream_block || @app.has_timers?) && @adapter.respond_to?(:render_sse_client)
99
+ @adapter.render_sse_client(self)
100
+ end
101
+
102
+ # URL routing: popstate handler for browser back/forward
103
+ if @app.routes && @adapter.respond_to?(:render_routing_scripts)
104
+ @adapter.render_routing_scripts(self)
105
+ end
106
+
107
+ # Google Fonts: Source Sans 3 + Crimson Pro (for document theme) + any app-declared fonts
108
+ link(rel: "preconnect", href: "https://fonts.googleapis.com")
109
+ link(rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous")
110
+ link(rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;500;600&family=Source+Sans+3:wght@400;500;600;700&display=swap")
111
+ if @app.fonts.any?
112
+ custom_href = Fonts.google_fonts_href(@app.fonts)
113
+ link(rel: "stylesheet", href: custom_href) if custom_href
114
+ end
115
+ style do
116
+ raw(safe(StreamWeaver::CSS.layer_wrap(self.class.master_theme_css)))
117
+ end
118
+
119
+ # Visual Skills CSS foundation (--sw-* semantic tokens)
120
+ style do
121
+ raw(safe(StreamWeaver::CSS.layer_wrap(StreamWeaver::Theme.visual_skills_css)))
122
+ end
123
+
124
+ # Dev-loud-failure overlay styling (dev-loud-failure-overlay).
125
+ # Always emitted -- inert unless RACK_ENV=development actually
126
+ # renders `.sw-dev-fallback` markup into a fallback response.
127
+ style do
128
+ raw(safe(StreamWeaver::CSS.layer_wrap(StreamWeaver::CSS.dev_fallback_css)))
129
+ end
130
+
131
+ # Dark mode: check localStorage / system preference, apply .dark on <html>
132
+ # Also provides swToggleTheme() / swGetTheme() for auto-mode support
133
+ script do
134
+ theme_toggle = @app.components.find { |c| c.is_a?(Components::ThemeToggle) }
135
+ raw(safe(StreamWeaver::Theme::AutoMode.inline_script(default_mode: theme_toggle&.mode || :auto)))
136
+ end
137
+ end
138
+ body(class: body_classes) do
139
+ raw(safe(body_html))
140
+ end
141
+ end
142
+ end
143
+
144
+ # Everything that lives inside <body> -- extracted so view_template
145
+ # can render it once via `capture` (see the comment there) and replay
146
+ # the resulting string after <head> is built.
147
+ def render_page_body
148
+ # Custom theme CSS (for registered themes)
149
+ render_custom_theme_css
150
+
151
+ # Theme overrides as inline CSS
152
+ render_theme_overrides if @app.theme_overrides.any?
153
+
154
+ layout_entry = @app.layout_entry
155
+ if layout_entry&.dig(:exclusive) && layout_entry[:render_block]
156
+ instance_exec(&layout_entry[:render_block])
157
+ else
158
+ # Skip the chrome's own h1 when the app already renders its
159
+ # leading content as a header1 -- otherwise the page doubles up
160
+ # (FAC-9u2).
161
+ first_content = @app.components.first
162
+ own_leading_h1 = first_content.is_a?(Components::Header) && first_content.level == 1
163
+ h1 { @app.title } unless own_leading_h1
164
+ # Merge adapter-specific container attributes with container id
165
+ div(id: "app-container", "data-sw-state-version" => @app.render_state.state_version,
166
+ **@adapter.container_attributes(@state)) do
167
+ render_components
168
+ end
169
+ end
170
+ end
171
+
172
+ # Collects component CSS registered via adapter#inject_component_css
173
+ # during the body render, keyed so re-registering the same key is a
174
+ # no-op (cross-instance dedup within one page render).
175
+ def component_css_registry
176
+ @component_css_registry ||= {}
177
+ end
178
+
179
+ def register_component_css(key, css)
180
+ component_css_registry[key] = css
181
+ end
182
+
183
+ def with_fragment(id)
184
+ previous = @current_fragment_id
185
+ @current_fragment_id = id
186
+ yield
187
+ ensure
188
+ @current_fragment_id = previous
189
+ end
190
+
191
+ def self.master_theme_css
192
+ <<~CSS
193
+ /* ===========================================
194
+ shadcn Token Layer
195
+ Bridge: tokens fall back to --sw-* vars,
196
+ so register_theme still works.
197
+ =========================================== */
198
+ :root {
199
+ --background: var(--sw-color-bg, oklch(1 0 0));
200
+ --foreground: var(--sw-color-text, oklch(0.145 0 0));
201
+ --card: var(--sw-color-bg-card, oklch(1 0 0));
202
+ --card-foreground: var(--sw-color-text, oklch(0.145 0 0));
203
+ --popover: var(--sw-color-bg-card, oklch(1 0 0));
204
+ --popover-foreground: var(--sw-color-text, oklch(0.145 0 0));
205
+ --primary: var(--sw-color-primary, oklch(0.205 0 0));
206
+ --primary-foreground: oklch(0.985 0 0);
207
+ --secondary: var(--sw-color-secondary, oklch(0.97 0 0));
208
+ --secondary-foreground: var(--sw-color-secondary-foreground, oklch(0.205 0 0));
209
+ --muted: var(--sw-color-bg-elevated, oklch(0.97 0 0));
210
+ --muted-foreground: var(--sw-color-text-muted, oklch(0.556 0 0));
211
+ --accent: var(--sw-color-accent, oklch(0.97 0 0));
212
+ --accent-foreground: oklch(0.205 0 0);
213
+ --destructive: oklch(0.577 0.245 27.325);
214
+ --destructive-foreground: #fff;
215
+ --border: var(--sw-color-border, oklch(0.922 0 0));
216
+ --input: var(--sw-color-border, oklch(0.922 0 0));
217
+ --ring: oklch(0.708 0 0);
218
+ --radius: var(--sw-radius-md, 0.5rem);
219
+ --warning: hsl(38 92% 50%);
220
+ --warning-foreground: #fff;
221
+ --success: hsl(142 71% 45%);
222
+ --success-foreground: #fff;
223
+ --info: hsl(217 91% 60%);
224
+ --info-foreground: #fff;
225
+ }
226
+
227
+ /* Re-bridge on body: the --sw-color-* tokens these vars fall back to
228
+ are themselves declared on body (body.sw-theme-*, theme_overrides:,
229
+ html.dark body -- see below), which is a descendant of :root/<html>.
230
+ A custom property's var() fallback resolves against what's in scope
231
+ AT THE ELEMENT declaring it, so the :root block above can never see
232
+ a per-theme override made on body -- it's always stuck on its own
233
+ fallback. Redeclaring here, on body itself, lets these tokens see
234
+ whatever --sw-color-* body ends up with (default theme, a custom
235
+ registered theme, or an app's theme_overrides:) instead of only ever
236
+ reflecting the light default (stream_weaver-9u2). */
237
+ body {
238
+ --background: var(--sw-color-bg, oklch(1 0 0));
239
+ --foreground: var(--sw-color-text, oklch(0.145 0 0));
240
+ --card: var(--sw-color-bg-card, oklch(1 0 0));
241
+ --card-foreground: var(--sw-color-text, oklch(0.145 0 0));
242
+ --popover: var(--sw-color-bg-card, oklch(1 0 0));
243
+ --popover-foreground: var(--sw-color-text, oklch(0.145 0 0));
244
+ --primary: var(--sw-color-primary, oklch(0.205 0 0));
245
+ --secondary: var(--sw-color-secondary, oklch(0.97 0 0));
246
+ --secondary-foreground: var(--sw-color-secondary-foreground, oklch(0.205 0 0));
247
+ --muted: var(--sw-color-bg-elevated, oklch(0.97 0 0));
248
+ --muted-foreground: var(--sw-color-text-muted, oklch(0.556 0 0));
249
+ --accent: var(--sw-color-accent, oklch(0.97 0 0));
250
+ --border: var(--sw-color-border, oklch(0.922 0 0));
251
+ --input: var(--sw-color-border, oklch(0.922 0 0));
252
+ --radius: var(--sw-radius-md, 0.5rem);
253
+ }
254
+
255
+ .dark {
256
+ --background: oklch(0.145 0 0);
257
+ --foreground: oklch(0.985 0 0);
258
+ --card: oklch(0.205 0 0);
259
+ --card-foreground: oklch(0.985 0 0);
260
+ --popover: oklch(0.205 0 0);
261
+ --popover-foreground: oklch(0.985 0 0);
262
+ --primary: oklch(0.922 0 0);
263
+ --primary-foreground: oklch(0.205 0 0);
264
+ --secondary: oklch(0.269 0 0);
265
+ --secondary-foreground: oklch(0.985 0 0);
266
+ --muted: oklch(0.269 0 0);
267
+ --muted-foreground: oklch(0.708 0 0);
268
+ --accent: oklch(0.269 0 0);
269
+ --accent-foreground: oklch(0.985 0 0);
270
+ --destructive: oklch(0.704 0.191 22.216);
271
+ --destructive-foreground: oklch(0.985 0 0);
272
+ --border: oklch(1 0 0 / 10%);
273
+ --input: oklch(1 0 0 / 15%);
274
+ --ring: oklch(0.556 0 0);
275
+ --warning: hsl(38 92% 60%);
276
+ --warning-foreground: hsl(38 92% 10%);
277
+ --success: hsl(142 71% 55%);
278
+ --success-foreground: hsl(142 71% 10%);
279
+ --info: hsl(217 91% 70%);
280
+ --info-foreground: hsl(217 91% 10%);
281
+ }
282
+
283
+ /* Dark mode: override sw-color-* tokens to match shadcn dark values.
284
+ html.dark body beats body.sw-theme-* specificity (0-1-2 vs 0-1-1). */
285
+ html.dark body {
286
+ --sw-color-bg: oklch(0.145 0 0);
287
+ --sw-color-bg-card: oklch(0.205 0 0);
288
+ --sw-color-bg-elevated: oklch(0.269 0 0);
289
+ --sw-color-text: oklch(0.985 0 0);
290
+ --sw-color-text-muted: oklch(0.708 0 0);
291
+ --sw-color-text-light: oklch(0.556 0 0);
292
+ --sw-color-border: oklch(1 0 0 / 10%);
293
+ --sw-color-border-strong: oklch(1 0 0 / 15%);
294
+ --sw-color-border-focus: var(--sw-color-primary);
295
+ --sw-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
296
+ --sw-shadow-md: 0 4px 8px rgba(0, 0, 0, 0.4);
297
+ --sw-shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.5);
298
+ --sw-shadow-xl: 0 12px 24px rgba(0, 0, 0, 0.6);
299
+ --sw-shadow-inner: inset 0 1px 2px rgba(0, 0, 0, 0.2);
300
+ }
301
+
302
+ /* shadcn base resets */
303
+ body { background-color: var(--background); color: var(--foreground); }
304
+
305
+ .sw-focus-ring:focus-visible {
306
+ outline: none;
307
+ box-shadow: 0 0 0 2px var(--background), 0 0 0 4px var(--ring);
308
+ }
309
+
310
+ /* ===========================================
311
+ StreamWeaver CSS - Multi-Theme System
312
+ =========================================== */
313
+
314
+ /* Base variables (shared across all themes) */
315
+ :root {
316
+ /* Transitions - Smooth, deliberate */
317
+ --sw-transition-fast: 120ms ease-out;
318
+ --sw-transition: 200ms ease-out;
319
+ --sw-transition-slow: 350ms ease-out;
320
+
321
+ /* Tooltip */
322
+ --sw-tooltip-bg: #292524;
323
+ --sw-tooltip-text: #fafaf9;
324
+ }
325
+
326
+ /* ===========================================
327
+ Theme: Default (Warm Industrial)
328
+ Distinctive, craft-inspired aesthetic
329
+ =========================================== */
330
+ body.sw-theme-default {
331
+ /* Typography - Source Sans 3: humanist, readable, distinctive */
332
+ --sw-font-display: 'Source Sans 3', system-ui, sans-serif;
333
+ --sw-font-body: 'Source Sans 3', system-ui, sans-serif;
334
+ --sw-font-family: var(--sw-font-body);
335
+ --sw-font-size-base: 17px;
336
+ --sw-font-size-sm: 15px;
337
+ --sw-font-size-lg: 19px;
338
+ --sw-font-size-xl: 24px;
339
+ --sw-line-height: 1.6;
340
+
341
+ /* Colors - High Contrast with Warm Accent
342
+ Amber/brass, not terracotta/orange -- the previous burnt-orange
343
+ (#c2410c) sat close enough to browser validation-error red that
344
+ it read as a warning state on cards rather than a brand accent. */
345
+ --sw-color-primary: #a16207; /* Brass/ochre */
346
+ --sw-color-primary-hover: #854d0e; /* Deeper brass */
347
+ --sw-color-primary-light: #fef3c7; /* Warm pale tint */
348
+ --sw-color-primary-glow: rgba(161, 98, 7, 0.12);
349
+
350
+ /* Colors - High Contrast Neutrals */
351
+ --sw-color-text: #111111; /* Near black - high contrast */
352
+ --sw-color-text-muted: #444444; /* Dark gray - still readable */
353
+ --sw-color-text-light: #888888; /* Placeholder gray */
354
+ --sw-color-bg: #f2ede4; /* Warm light tan -- was #f8f8f8, nearly
355
+ indistinguishable from the white card
356
+ background below; this gives visible
357
+ page/card separation */
358
+ --sw-color-bg-card: #ffffff;
359
+ --sw-color-bg-elevated: #f3f3f3; /* Subtle elevation */
360
+ --sw-color-border: #e0e0e0; /* Clean border */
361
+ --sw-color-border-strong: #cccccc;
362
+ --sw-color-border-focus: var(--sw-color-primary);
363
+
364
+ /* Colors - Secondary */
365
+ --sw-color-secondary: #333333;
366
+ --sw-color-secondary-hover: #1a1a1a;
367
+ --sw-color-secondary-foreground: #ffffff;
368
+
369
+ /* Colors - Accent (teal for links/highlights) */
370
+ --sw-color-accent: #0d9488;
371
+ --sw-color-accent-light: #e6fffa;
372
+
373
+ /* Spacing -- was "Generous" (1.25/2/3/4rem), measured ~21% taller
374
+ than :doc for identical Dashboard content at the same viewport
375
+ width with no proportional benefit -- tightened to be close to
376
+ :doc's own scale while keeping a touch more room than dashboard
377
+ (which is deliberately the densest theme). */
378
+ --sw-spacing-xs: 0.375rem;
379
+ --sw-spacing-sm: 0.5rem;
380
+ --sw-spacing-md: 1rem;
381
+ --sw-spacing-lg: 1.5rem;
382
+ --sw-spacing-xl: 2rem;
383
+ --sw-spacing-2xl: 3rem;
384
+
385
+ /* Border Radius */
386
+ --sw-radius-sm: 3px;
387
+ --sw-radius-md: 6px;
388
+ --sw-radius-lg: 10px;
389
+ --sw-radius-xl: 16px;
390
+
391
+ /* Shadows - Deep, warm */
392
+ --sw-shadow-sm: 0 1px 2px rgba(28, 25, 23, 0.04), 0 1px 3px rgba(28, 25, 23, 0.06);
393
+ --sw-shadow-md: 0 4px 8px -2px rgba(28, 25, 23, 0.08), 0 2px 4px -1px rgba(28, 25, 23, 0.04);
394
+ --sw-shadow-lg: 0 12px 24px -4px rgba(28, 25, 23, 0.12), 0 4px 8px -2px rgba(28, 25, 23, 0.06);
395
+ --sw-shadow-xl: 0 20px 40px -8px rgba(28, 25, 23, 0.16), 0 8px 16px -4px rgba(28, 25, 23, 0.08);
396
+ --sw-shadow-inner: inset 0 1px 2px rgba(28, 25, 23, 0.06);
397
+
398
+ /* Card styling */
399
+ --sw-card-border-left: 3px solid var(--sw-color-primary);
400
+
401
+ /* Term highlighting */
402
+ --sw-term-color: var(--sw-color-primary);
403
+ --sw-term-bg-hover: var(--sw-color-primary-light);
404
+ }
405
+
406
+ /* Default-specific adjustments.
407
+ h1/h3 are already identical to :doc's (both fall through to the
408
+ shared, theme-unaware base rules -- rem against a fixed 16px
409
+ root, not var(--sw-font-size-base)). h2 was the one real gap:
410
+ :doc already has its own tuned override (1.45rem = 23.2px) but
411
+ default never got one, so it fell through to the generic 1.625rem
412
+ (26px) instead -- match :doc's value so both themes share one
413
+ coherent heading scale instead of only mostly agreeing. */
414
+ body.sw-theme-default h2 {
415
+ font-size: 1.45rem;
416
+ }
417
+
418
+ /* ===========================================
419
+ Theme: Dashboard (Data Dense)
420
+ Optimized for data tables, metrics, scanning
421
+ =========================================== */
422
+ body.sw-theme-dashboard {
423
+ /* Typography - Tighter, more compact */
424
+ --sw-font-display: 'Source Sans 3', system-ui, sans-serif;
425
+ --sw-font-body: 'Source Sans 3', system-ui, sans-serif;
426
+ --sw-font-family: var(--sw-font-body);
427
+ --sw-font-size-base: 15px;
428
+ --sw-font-size-sm: 13px;
429
+ --sw-font-size-lg: 17px;
430
+ --sw-font-size-xl: 21px;
431
+ --sw-line-height: 1.5;
432
+
433
+ /* Colors - Same palette, cleaner */
434
+ --sw-color-primary: #a16207;
435
+ --sw-color-primary-hover: #854d0e;
436
+ --sw-color-primary-light: #fef3c7;
437
+ --sw-color-primary-glow: rgba(161, 98, 7, 0.08);
438
+
439
+ --sw-color-text: #111111;
440
+ --sw-color-text-muted: #555555;
441
+ --sw-color-text-light: #888888;
442
+ --sw-color-bg: #fafafa;
443
+ --sw-color-bg-card: #ffffff;
444
+ --sw-color-bg-elevated: #f5f5f5;
445
+ --sw-color-border: #e5e5e5;
446
+ --sw-color-border-strong: #d0d0d0;
447
+ --sw-color-border-focus: var(--sw-color-primary);
448
+
449
+ --sw-color-secondary: #333333;
450
+ --sw-color-secondary-hover: #1a1a1a;
451
+ --sw-color-secondary-foreground: #ffffff;
452
+
453
+ --sw-color-accent: #0d9488;
454
+ --sw-color-accent-light: #e6fffa;
455
+
456
+ /* Spacing - Reduced for density */
457
+ --sw-spacing-xs: 0.3125rem;
458
+ --sw-spacing-sm: 0.4375rem;
459
+ --sw-spacing-md: 0.75rem;
460
+ --sw-spacing-lg: 1.125rem;
461
+ --sw-spacing-xl: 1.5rem;
462
+ --sw-spacing-2xl: 2.25rem;
463
+
464
+ /* Table/callout density -- tighter than the 0.75rem/1rem shared default,
465
+ since "Data Dense" is this theme's whole reason to exist */
466
+ --sw-table-header-padding: 6px 10px;
467
+ --sw-table-cell-padding: 7px 10px;
468
+ --sw-callout-padding: 10px 14px;
469
+
470
+ /* Border Radius - Slightly smaller */
471
+ --sw-radius-sm: 2px;
472
+ --sw-radius-md: 4px;
473
+ --sw-radius-lg: 6px;
474
+ --sw-radius-xl: 10px;
475
+
476
+ /* Shadows - Minimal */
477
+ --sw-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
478
+ --sw-shadow-md: 0 2px 4px rgba(0, 0, 0, 0.06);
479
+ --sw-shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.08);
480
+ --sw-shadow-xl: 0 8px 16px rgba(0, 0, 0, 0.1);
481
+ --sw-shadow-inner: inset 0 1px 2px rgba(0, 0, 0, 0.04);
482
+
483
+ /* Card styling - No accent bar */
484
+ --sw-card-border-left: 1px solid var(--sw-color-border);
485
+
486
+ --sw-term-color: var(--sw-color-primary);
487
+ --sw-term-bg-hover: var(--sw-color-primary-light);
488
+ }
489
+
490
+ /* Dashboard-specific adjustments */
491
+ body.sw-theme-dashboard h2 {
492
+ font-size: 1.25rem;
493
+ margin-top: var(--sw-spacing-lg);
494
+ margin-bottom: var(--sw-spacing-sm);
495
+ }
496
+
497
+ body.sw-theme-dashboard .score-table th,
498
+ body.sw-theme-dashboard .score-table td {
499
+ padding: 6px 10px;
500
+ }
501
+
502
+ /* ===========================================
503
+ Theme: Document (Reading Optimized)
504
+ Long-form reading, focus mode, elegant
505
+ =========================================== */
506
+ body.sw-theme-document {
507
+ /* Typography - Serif, larger, generous line height */
508
+ --sw-font-display: 'Source Sans 3', system-ui, sans-serif;
509
+ --sw-font-body: 'Crimson Pro', Georgia, 'Times New Roman', serif;
510
+ --sw-font-family: var(--sw-font-body);
511
+ --sw-font-size-base: 19px;
512
+ --sw-font-size-sm: 16px;
513
+ --sw-font-size-lg: 22px;
514
+ --sw-font-size-xl: 28px;
515
+ --sw-line-height: 1.85;
516
+
517
+ /* Colors - Warm paper tones */
518
+ --sw-color-primary: #6b7280; /* Muted gray-blue */
519
+ --sw-color-primary-hover: #4b5563;
520
+ --sw-color-primary-light: #f3f4f6;
521
+ --sw-color-primary-glow: rgba(107, 114, 128, 0.1);
522
+
523
+ --sw-color-text: #1a1a1a;
524
+ --sw-color-text-muted: #4a4a4a;
525
+ --sw-color-text-light: #7a7a7a;
526
+ --sw-color-bg: #faf8f5; /* Warm paper */
527
+ --sw-color-bg-card: #ffffff;
528
+ --sw-color-bg-elevated: #f5f3f0;
529
+ --sw-color-border: #e8e4df;
530
+ --sw-color-border-strong: #d8d4cf;
531
+ --sw-color-border-focus: var(--sw-color-primary);
532
+
533
+ --sw-color-secondary: #4a4a4a;
534
+ --sw-color-secondary-hover: #2a2a2a;
535
+ --sw-color-secondary-foreground: #ffffff;
536
+
537
+ --sw-color-accent: #2563eb;
538
+ --sw-color-accent-light: #eff6ff;
539
+
540
+ /* Spacing - More generous for reading */
541
+ --sw-spacing-xs: 0.5rem;
542
+ --sw-spacing-sm: 0.875rem;
543
+ --sw-spacing-md: 1.5rem;
544
+ --sw-spacing-lg: 2.5rem;
545
+ --sw-spacing-xl: 4rem;
546
+ --sw-spacing-2xl: 5rem;
547
+
548
+ /* Border Radius - Softer */
549
+ --sw-radius-sm: 4px;
550
+ --sw-radius-md: 8px;
551
+ --sw-radius-lg: 12px;
552
+ --sw-radius-xl: 20px;
553
+
554
+ /* Shadows - Very subtle */
555
+ --sw-shadow-sm: none;
556
+ --sw-shadow-md: 0 1px 3px rgba(0, 0, 0, 0.04);
557
+ --sw-shadow-lg: 0 2px 6px rgba(0, 0, 0, 0.06);
558
+ --sw-shadow-xl: 0 4px 12px rgba(0, 0, 0, 0.08);
559
+ --sw-shadow-inner: none;
560
+
561
+ /* Card styling - No accent, subtle */
562
+ --sw-card-border-left: none;
563
+
564
+ --sw-term-color: var(--sw-color-accent);
565
+ --sw-term-bg-hover: var(--sw-color-accent-light);
566
+ }
567
+
568
+ /* Document-specific adjustments
569
+ The 720px cap is a prose reading-width, so it's scoped off of
570
+ body:not(:has(.sw-app-shell)) -- an app_shell page manages its
571
+ own width via its sidebar+main grid, and capping the whole body
572
+ at 720px squeezes that grid down to a sliver (e.g. two 1fr chart
573
+ columns collapsing to ~180px each instead of filling the content
574
+ pane), well below what a plain document's max-width shouldn't
575
+ ever apply to. */
576
+ body.sw-theme-document:not(:has(.sw-app-shell)) {
577
+ max-width: 720px;
578
+ }
579
+
580
+ /* #app-container re-applies --sw-spacing-xl as its own padding on top
581
+ of body's --sw-spacing-xl padding (see the shared `body` and
582
+ `#app-container` rules above). Document theme's --sw-spacing-xl
583
+ (4rem) is the largest of any theme, so that doubling costs 128px
584
+ of width off the top -- fine for prose, but enough to squeeze an
585
+ app_shell's sidebar+main grid down until 2-column chart layouts
586
+ lose bars/points to an undersized canvas. Halve the inner layer
587
+ only in the app_shell case; the outer body padding alone still
588
+ gives comfortable page margins. */
589
+ body.sw-theme-document:has(.sw-app-shell) #app-container {
590
+ padding: calc(var(--sw-spacing-xl) / 2);
591
+ }
592
+
593
+ body.sw-theme-document p {
594
+ margin-bottom: 1.5em;
595
+ }
596
+
597
+ body.sw-theme-document h1, body.sw-theme-document h2,
598
+ body.sw-theme-document h3, body.sw-theme-document h4 {
599
+ font-family: var(--sw-font-display);
600
+ }
601
+
602
+ body.sw-theme-document h2 {
603
+ margin-top: 3rem;
604
+ }
605
+
606
+ body.sw-theme-document .card {
607
+ border-left: none;
608
+ border: 1px solid var(--sw-color-border);
609
+ }
610
+
611
+ /* ===========================================
612
+ Theme: Doc (Anthropic-artifact editorial)
613
+ Compact editorial — Charter serif headers, system sans body
614
+ =========================================== */
615
+ body.sw-theme-doc {
616
+ --sw-font-display: Charter, 'Bitstream Charter', 'Sitka Text', Cambria, Georgia, serif;
617
+ --sw-font-body: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif;
618
+ --sw-font-family: var(--sw-font-body);
619
+ --sw-font-mono: 'SFMono-Regular', 'SF Mono', 'Cascadia Code', Menlo, 'Courier New', monospace;
620
+ --sw-font-size-base: 15px;
621
+ --sw-font-size-sm: 13px;
622
+ --sw-font-size-lg: 17px;
623
+ --sw-font-size-xl: 22px;
624
+ --sw-line-height: 1.65;
625
+
626
+ /* Colors — artifact-exact palette */
627
+ --sw-color-primary: #1E4ED8;
628
+ --sw-color-primary-hover: #1a3fb0;
629
+ --sw-color-primary-light: #EEF2FF;
630
+ --sw-color-primary-glow: rgba(30, 78, 216, 0.1);
631
+
632
+ --sw-color-text: #181714;
633
+ --sw-color-text-muted: #6B6860;
634
+ --sw-color-text-light: #A09D96;
635
+ --sw-color-bg: #F5F4EF;
636
+ --sw-color-bg-card: #EDECE6;
637
+ --sw-color-bg-elevated: #ECEAE3;
638
+ --sw-color-border: #E0DED6;
639
+ --sw-color-border-strong: #C8C6BE;
640
+ --sw-color-border-focus: var(--sw-color-primary);
641
+
642
+ --sw-color-secondary: #6B6860;
643
+ --sw-color-secondary-hover: #4a4845;
644
+ --sw-color-secondary-foreground: #ffffff;
645
+
646
+ --sw-color-accent: #1E4ED8;
647
+ --sw-color-accent-light: #EEF2FF;
648
+
649
+ /* Spacing — compact editorial */
650
+ --sw-spacing-xs: 0.25rem;
651
+ --sw-spacing-sm: 0.5rem;
652
+ --sw-spacing-md: 1rem;
653
+ --sw-spacing-lg: 1.5rem;
654
+ --sw-spacing-xl: 2rem;
655
+ --sw-spacing-2xl: 3.25rem;
656
+
657
+ /* Border Radius — minimal */
658
+ --sw-radius-sm: 2px;
659
+ --sw-radius-md: 4px;
660
+ --sw-radius-lg: 6px;
661
+ --sw-radius-xl: 8px;
662
+
663
+ /* Shadows — near-flat */
664
+ --sw-shadow-sm: none;
665
+ --sw-shadow-md: 0 1px 2px rgba(0, 0, 0, 0.04);
666
+ --sw-shadow-lg: 0 2px 4px rgba(0, 0, 0, 0.06);
667
+ --sw-shadow-xl: 0 4px 8px rgba(0, 0, 0, 0.08);
668
+ --sw-shadow-inner: none;
669
+
670
+ /* Card — no accent left border */
671
+ --sw-card-border-left: none;
672
+
673
+ /* Shared-component density tokens — let render_table/callout_css read
674
+ theme-owned values via normal CSS cascade instead of !important */
675
+ --sw-table-header-padding: 8px 12px;
676
+ --sw-table-cell-padding: 9px 12px;
677
+ --sw-callout-padding: 14px 16px;
678
+
679
+ --sw-term-color: var(--sw-color-accent);
680
+ --sw-term-bg-hover: var(--sw-color-accent-light);
681
+ }
682
+
683
+ /* Doc theme — dark mode overrides (data-sw-theme="dark" lives on <html>).
684
+ Colors only — type/spacing/radius/shadow cascade from the light block above. */
685
+ html[data-sw-theme="dark"] body.sw-theme-doc {
686
+ --sw-color-primary: #6699FF;
687
+ --sw-color-primary-hover: #8AB0FF;
688
+ --sw-color-primary-light: #1B2740;
689
+ --sw-color-primary-glow: rgba(102, 153, 255, 0.15);
690
+
691
+ --sw-color-text: #ECEAE3;
692
+ --sw-color-text-muted: #A8A399;
693
+ --sw-color-text-light: #716C61;
694
+ --sw-color-bg: #1A1714;
695
+ --sw-color-bg-card: #232019;
696
+ --sw-color-bg-elevated: #2A251F;
697
+ --sw-color-border: #3A352D;
698
+ --sw-color-border-strong: #4A453B;
699
+ --sw-color-border-focus: var(--sw-color-primary);
700
+
701
+ --sw-color-secondary: #A8A399;
702
+ --sw-color-secondary-hover: #C5C0B5;
703
+ --sw-color-secondary-foreground: #1A1714;
704
+
705
+ --sw-color-accent: #6699FF;
706
+ --sw-color-accent-light: #1B2740;
707
+ }
708
+
709
+ /* Doc theme — serif section headers at artifact-exact size */
710
+ body.sw-theme-doc h2 {
711
+ font-family: var(--sw-font-display);
712
+ font-size: 1.45rem;
713
+ }
714
+
715
+ /* Doc apps render flat component lists (no <section> wrappers) —
716
+ the section boundary marker is .sw-doc-section-header, so the
717
+ artifact's section { margin-bottom: 52px } becomes margin-top here.
718
+ Adjacent-sibling margin collapsing yields max(prev, 52px) = 52px,
719
+ same effective gap as the artifact. */
720
+ body.sw-theme-doc .sw-doc-section-header {
721
+ margin-top: 52px;
722
+ }
723
+
724
+ /* render_table/callout_css now read --sw-table-header-padding /
725
+ --sw-table-cell-padding / --sw-callout-padding (set in the doc theme
726
+ variable block above) via normal CSS custom-property cascade — no
727
+ !important needed since it's no longer a specificity fight, just a
728
+ token lookup. Remaining rules here are cosmetic details the token
729
+ system doesn't cover yet. */
730
+ body.sw-theme-doc .sw-table {
731
+ font-size: .875rem;
732
+ }
733
+ body.sw-theme-doc .sw-table th {
734
+ font-size: 11px;
735
+ }
736
+ body.sw-theme-doc .sw-table td:first-child {
737
+ white-space: nowrap;
738
+ }
739
+
740
+ /* Doc theme — code blocks always render in the dark
741
+ reverse-video scheme (Forrest, live UAT screenshot,
742
+ 2026-09-03), regardless of the page's own light/dark
743
+ toggle: unlike the rest of the doc theme (which tracks
744
+ light/dark via --sw-color-bg-card etc.), a code block's
745
+ own background is pinned to these tokens in BOTH modes.
746
+ `.sw-code-block` normally reads --sw-surface, which IS
747
+ bridged to --sw-color-bg-card on body (theme.rb) -- so
748
+ the prior symptom wasn't a raw #ffffff fallback, it was
749
+ the doc theme's own warm near-white card colour showing
750
+ through a wrapper Prism's tomorrow-theme token colours
751
+ (loaded for every code_block, adapter/alpinejs.rb
752
+ inject_prism_cdn) assume is dark. Scoped to
753
+ .sw-theme-doc only -- every other theme's code blocks
754
+ are untouched. */
755
+ body.sw-theme-doc {
756
+ --sw-doc-code-bg: #1e1e1e;
757
+ --sw-doc-code-bg-elevated: #252526;
758
+ --sw-doc-code-border: #333333;
759
+ --sw-doc-code-text: #d4d4d4;
760
+ --sw-doc-code-text-dim: #9a9a9a;
761
+ }
762
+ body.sw-theme-doc .sw-code-block {
763
+ background: var(--sw-doc-code-bg);
764
+ border-color: var(--sw-doc-code-border);
765
+ }
766
+ body.sw-theme-doc .sw-code-block__header {
767
+ background: var(--sw-doc-code-bg-elevated);
768
+ border-color: var(--sw-doc-code-border);
769
+ color: var(--sw-doc-code-text-dim);
770
+ }
771
+ body.sw-theme-doc .sw-code-block__copy {
772
+ background: #2d2d2d;
773
+ border-color: #444444;
774
+ color: var(--sw-doc-code-text);
775
+ }
776
+ body.sw-theme-doc .sw-code-block__copy:hover {
777
+ background: #3a3a3a;
778
+ }
779
+ body.sw-theme-doc .sw-code-block__pre,
780
+ body.sw-theme-doc .sw-code-block__pre code {
781
+ color: var(--sw-doc-code-text);
782
+ }
783
+ body.sw-theme-doc .sw-code-block__truncated {
784
+ background: var(--sw-doc-code-bg-elevated);
785
+ border-color: var(--sw-doc-code-border);
786
+ color: var(--sw-doc-code-text-dim);
787
+ }
788
+
789
+ /* ===========================================
790
+ Base Styles
791
+ =========================================== */
792
+ *, *::before, *::after {
793
+ box-sizing: border-box;
794
+ }
795
+
796
+ /* Allow overflow with scrollbar when needed */
797
+ html {
798
+ overflow-x: auto;
799
+ }
800
+
801
+ body {
802
+ font-family: var(--sw-font-body);
803
+ font-size: var(--sw-font-size-base);
804
+ line-height: var(--sw-line-height);
805
+ color: var(--sw-color-text);
806
+ margin: 0;
807
+ padding: 0;
808
+ background: var(--sw-color-bg);
809
+ -webkit-font-smoothing: antialiased;
810
+ -moz-osx-font-smoothing: grayscale;
811
+ min-height: 100vh;
812
+ }
813
+
814
+ /* Layout modes */
815
+ body[class*="sw-layout-"] { margin: 0 auto; padding: var(--sw-spacing-xl); }
816
+ body.sw-layout-default { max-width: 900px; }
817
+ body.sw-layout-wide { max-width: 1100px; }
818
+ body.sw-layout-full { max-width: 1400px; }
819
+ body.sw-layout-fluid { max-width: 100%; padding-left: var(--sw-spacing-xl); padding-right: var(--sw-spacing-xl); }
820
+
821
+ /* chrome: false gets a minimal padding baseline so content isn't
822
+ glued to the viewport edge -- still just one low-specificity
823
+ class rule, easily overridden to zero by an app's own CSS
824
+ (FAC-9u2). */
825
+ body.sw-chromeless { padding: var(--sw-spacing-lg); }
826
+
827
+ /* Page title outside container */
828
+ body > h1 {
829
+ font-size: 2.5rem;
830
+ font-weight: 700;
831
+ color: var(--sw-color-text);
832
+ letter-spacing: -0.02em;
833
+ margin: 0 0 var(--sw-spacing-lg) 0;
834
+ }
835
+
836
+ body[class*="sw-layout-"] > #app-container {
837
+ background: var(--sw-color-bg-card);
838
+ padding: var(--sw-spacing-xl);
839
+ border-radius: var(--sw-radius-lg);
840
+ box-shadow: var(--sw-shadow-md);
841
+ border: 1px solid var(--sw-color-border);
842
+ overflow-x: auto;
843
+ word-wrap: break-word;
844
+ overflow-wrap: break-word;
845
+ }
846
+
847
+ /* ===========================================
848
+ Typography - Clean, Scannable Hierarchy
849
+ =========================================== */
850
+ h1, h2, h3, h4, h5, h6 {
851
+ font-family: var(--sw-font-body);
852
+ font-weight: 600;
853
+ letter-spacing: -0.01em;
854
+ color: var(--sw-color-text);
855
+ }
856
+
857
+ h1 {
858
+ font-size: 2.25rem;
859
+ font-weight: 700;
860
+ margin-top: 0;
861
+ margin-bottom: var(--sw-spacing-lg);
862
+ }
863
+
864
+ h2 {
865
+ font-size: 1.625rem;
866
+ font-weight: 600;
867
+ margin-top: var(--sw-spacing-xl);
868
+ margin-bottom: var(--sw-spacing-md);
869
+ padding-bottom: var(--sw-spacing-sm);
870
+ border-bottom: 2px solid var(--sw-color-border);
871
+ }
872
+
873
+ h3 {
874
+ font-size: 1.25rem;
875
+ font-weight: 600;
876
+ margin-top: var(--sw-spacing-lg);
877
+ margin-bottom: var(--sw-spacing-sm);
878
+ }
879
+
880
+ h4 {
881
+ font-size: 1.2rem;
882
+ font-weight: 700;
883
+ margin-top: var(--sw-spacing-md);
884
+ margin-bottom: var(--sw-spacing-xs);
885
+ }
886
+
887
+ h5 {
888
+ font-size: 1.1rem;
889
+ font-weight: 700;
890
+ margin-top: var(--sw-spacing-md);
891
+ margin-bottom: var(--sw-spacing-xs);
892
+ }
893
+
894
+ h6 {
895
+ font-size: 1rem;
896
+ font-weight: 700;
897
+ text-transform: uppercase;
898
+ letter-spacing: 0.05em;
899
+ color: var(--sw-color-text-muted);
900
+ margin-top: var(--sw-spacing-md);
901
+ margin-bottom: var(--sw-spacing-xs);
902
+ }
903
+
904
+ p {
905
+ color: var(--sw-color-text-muted);
906
+ line-height: var(--sw-line-height);
907
+ margin: 0 0 var(--sw-spacing-md) 0;
908
+ word-wrap: break-word;
909
+ overflow-wrap: break-word;
910
+ }
911
+
912
+ ul, ol {
913
+ margin: 0 0 var(--sw-spacing-md) 1.25rem;
914
+ color: var(--sw-color-text-muted);
915
+ line-height: var(--sw-line-height);
916
+ }
917
+
918
+ li {
919
+ margin-bottom: 0.3125rem;
920
+ }
921
+
922
+ li:last-child {
923
+ margin-bottom: 0;
924
+ }
925
+
926
+ /* Handle long URLs and strings */
927
+ a, code, pre {
928
+ word-wrap: break-word;
929
+ overflow-wrap: break-word;
930
+ word-break: break-word;
931
+ }
932
+
933
+ /* Inline code -- monospace fonts render optically larger than body
934
+ text at the same declared size, so scale down to match */
935
+ code {
936
+ font-size: .825em;
937
+ background: var(--sw-surface-elevated, #ECEAE3);
938
+ padding: 1px 5px;
939
+ border-radius: 3px;
940
+ }
941
+
942
+ pre code, .sw-code-block__pre code {
943
+ font-size: inherit;
944
+ background: none;
945
+ padding: 0;
946
+ }
947
+
948
+ /* Strong text */
949
+ strong, b {
950
+ font-weight: 600;
951
+ color: var(--sw-color-text);
952
+ }
953
+
954
+ p + input, p + select, p + textarea {
955
+ margin-top: var(--sw-spacing-xs);
956
+ }
957
+
958
+ p:has(+ input), p:has(+ select), p:has(+ textarea) {
959
+ margin-bottom: var(--sw-spacing-xs);
960
+ font-weight: 500;
961
+ color: var(--sw-color-text);
962
+ }
963
+
964
+ /* ===========================================
965
+ Form Controls - Refined Inputs
966
+ =========================================== */
967
+ input[type="text"], input[type="email"], select, textarea {
968
+ padding: var(--sw-spacing-md);
969
+ margin: var(--sw-spacing-sm) 0 var(--sw-spacing-md) 0;
970
+ border: 1px solid var(--input);
971
+ border-radius: var(--radius);
972
+ font-size: var(--sw-font-size-base);
973
+ font-family: var(--sw-font-body);
974
+ width: 100%;
975
+ background: var(--background);
976
+ color: var(--foreground);
977
+ box-shadow: var(--sw-shadow-inner);
978
+ transition:
979
+ border-color var(--sw-transition),
980
+ box-shadow var(--sw-transition),
981
+ background-color var(--sw-transition);
982
+ }
983
+
984
+ input[type="text"]:hover, input[type="email"]:hover, select:hover, textarea:hover {
985
+ border-color: var(--sw-color-border-strong);
986
+ }
987
+
988
+ input[type="text"]:focus, input[type="email"]:focus, select:focus, textarea:focus {
989
+ outline: none;
990
+ border-color: var(--ring);
991
+ box-shadow:
992
+ 0 0 0 2px var(--background),
993
+ 0 0 0 4px var(--ring);
994
+ }
995
+
996
+ input[type="text"]::placeholder, input[type="email"]::placeholder, textarea::placeholder {
997
+ color: var(--muted-foreground);
998
+ font-style: italic;
999
+ }
1000
+
1001
+ textarea {
1002
+ resize: vertical;
1003
+ min-height: 100px;
1004
+ line-height: 1.5;
1005
+ }
1006
+
1007
+ /* Visible label wrapper for text_field/text_area/select/chip_group
1008
+ (FAC-9u2 -- these silently dropped label: before). */
1009
+ .sw-field {
1010
+ display: flex;
1011
+ flex-direction: column;
1012
+ gap: var(--sw-spacing-xs);
1013
+ }
1014
+
1015
+ .sw-field__label {
1016
+ font-size: var(--sw-font-size-sm);
1017
+ font-weight: 600;
1018
+ color: var(--sw-color-text);
1019
+ }
1020
+
1021
+ .sw-field input, .sw-field select, .sw-field textarea, .sw-field .sw-chip-group {
1022
+ margin-top: 0;
1023
+ }
1024
+
1025
+ /* ===========================================
1026
+ App Header - Full-width header bar
1027
+ =========================================== */
1028
+ .sw-app-header {
1029
+ display: flex;
1030
+ justify-content: space-between;
1031
+ align-items: center;
1032
+ padding: var(--sw-spacing-md) var(--sw-spacing-lg);
1033
+ margin: calc(-1 * var(--sw-spacing-xl)) calc(-1 * var(--sw-spacing-xl)) var(--sw-spacing-lg) calc(-1 * var(--sw-spacing-xl));
1034
+ font-family: var(--sw-font-body);
1035
+ }
1036
+
1037
+ .sw-app-header-brand {
1038
+ display: flex;
1039
+ align-items: baseline;
1040
+ gap: var(--sw-spacing-sm);
1041
+ }
1042
+
1043
+ .sw-app-header-title {
1044
+ font-size: var(--sw-font-size-lg);
1045
+ font-weight: 600;
1046
+ }
1047
+
1048
+ .sw-app-header-subtitle {
1049
+ font-size: var(--sw-font-size-sm);
1050
+ opacity: 0.7;
1051
+ }
1052
+
1053
+ .sw-app-header-actions {
1054
+ display: flex;
1055
+ align-items: center;
1056
+ gap: var(--sw-spacing-sm);
1057
+ }
1058
+
1059
+ /* Header variants */
1060
+ .sw-app-header-dark {
1061
+ background: var(--card);
1062
+ color: var(--card-foreground);
1063
+ }
1064
+
1065
+ .sw-app-header-dark .sw-app-header-subtitle {
1066
+ color: var(--muted-foreground);
1067
+ }
1068
+
1069
+ .sw-app-header-dark button,
1070
+ .sw-app-header-dark .btn {
1071
+ background: transparent;
1072
+ border: 1px solid rgba(255, 255, 255, 0.3);
1073
+ color: #ffffff;
1074
+ margin: 0;
1075
+ }
1076
+
1077
+ .sw-app-header-dark button:hover,
1078
+ .sw-app-header-dark .btn:hover {
1079
+ background: rgba(255, 255, 255, 0.1);
1080
+ border-color: rgba(255, 255, 255, 0.5);
1081
+ }
1082
+
1083
+ .sw-app-header-light {
1084
+ background: var(--sw-color-bg-elevated);
1085
+ color: var(--sw-color-text);
1086
+ border-bottom: 1px solid var(--sw-color-border);
1087
+ }
1088
+
1089
+ .sw-app-header-primary {
1090
+ background: var(--sw-color-primary);
1091
+ color: #ffffff;
1092
+ }
1093
+
1094
+ .sw-app-header-primary button,
1095
+ .sw-app-header-primary .btn {
1096
+ background: rgba(255, 255, 255, 0.2);
1097
+ border: none;
1098
+ color: #ffffff;
1099
+ margin: 0;
1100
+ }
1101
+
1102
+ .sw-app-header-primary button:hover,
1103
+ .sw-app-header-primary .btn:hover {
1104
+ background: rgba(255, 255, 255, 0.3);
1105
+ }
1106
+
1107
+ /* ===========================================
1108
+ Buttons - Bold, Confident
1109
+ =========================================== */
1110
+ button, .btn {
1111
+ display: inline-flex;
1112
+ align-items: center;
1113
+ justify-content: center;
1114
+ gap: var(--sw-spacing-xs);
1115
+ padding: var(--sw-spacing-sm) var(--sw-spacing-md);
1116
+ margin: var(--sw-spacing-sm) var(--sw-spacing-sm) var(--sw-spacing-sm) 0;
1117
+ border: none;
1118
+ border-radius: var(--radius);
1119
+ font-family: var(--sw-font-body);
1120
+ font-size: var(--sw-font-size-sm);
1121
+ font-weight: 600;
1122
+ letter-spacing: 0.01em;
1123
+ cursor: pointer;
1124
+ white-space: nowrap;
1125
+ transition:
1126
+ background var(--sw-transition),
1127
+ color var(--sw-transition),
1128
+ transform var(--sw-transition-fast),
1129
+ box-shadow var(--sw-transition);
1130
+ position: relative;
1131
+ overflow: hidden;
1132
+ }
1133
+
1134
+ button:hover {
1135
+ transform: translateY(-1px);
1136
+ }
1137
+
1138
+ button:active {
1139
+ transform: translateY(0) scale(0.98);
1140
+ }
1141
+
1142
+ /* Primary */
1143
+ .btn-primary {
1144
+ background: var(--primary);
1145
+ color: var(--primary-foreground);
1146
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
1147
+ }
1148
+
1149
+ .btn-primary:hover {
1150
+ opacity: 0.9;
1151
+ }
1152
+
1153
+ /* Secondary */
1154
+ .btn-secondary {
1155
+ background: var(--secondary);
1156
+ color: var(--secondary-foreground);
1157
+ border: 1px solid var(--border);
1158
+ }
1159
+
1160
+ .btn-secondary:hover {
1161
+ opacity: 0.85;
1162
+ }
1163
+
1164
+ /* Destructive */
1165
+ .btn-destructive {
1166
+ background: var(--destructive);
1167
+ color: var(--destructive-foreground);
1168
+ }
1169
+
1170
+ .btn-destructive:hover {
1171
+ opacity: 0.9;
1172
+ }
1173
+
1174
+ /* Outline */
1175
+ .btn-outline {
1176
+ background: transparent;
1177
+ color: var(--foreground);
1178
+ border: 1px solid var(--border);
1179
+ }
1180
+
1181
+ .btn-outline:hover {
1182
+ background: var(--accent);
1183
+ color: var(--accent-foreground);
1184
+ }
1185
+
1186
+ /* Ghost */
1187
+ .btn-ghost {
1188
+ background: transparent;
1189
+ color: var(--foreground);
1190
+ }
1191
+
1192
+ .btn-ghost:hover {
1193
+ background: var(--accent);
1194
+ color: var(--accent-foreground);
1195
+ }
1196
+
1197
+ /* Quiet -- minimal chrome, for inline row/table actions (rivet grammar) */
1198
+ .btn-quiet {
1199
+ background: transparent;
1200
+ box-shadow: none;
1201
+ color: var(--foreground);
1202
+ font-weight: 500;
1203
+ }
1204
+
1205
+ .btn-quiet:hover {
1206
+ background: var(--accent);
1207
+ color: var(--accent-foreground);
1208
+ }
1209
+
1210
+ /* Size: sm -- compact, for inline table actions */
1211
+ .btn-sm {
1212
+ padding: var(--sw-spacing-xs) var(--sw-spacing-sm);
1213
+ font-size: var(--sw-font-size-sm);
1214
+ font-weight: 500;
1215
+ }
1216
+
1217
+ /* Table action cells: inline row of buttons instead of stacked (FAC-9u2) */
1218
+ .sw-table__actions {
1219
+ display: flex;
1220
+ align-items: center;
1221
+ gap: var(--sw-spacing-xs);
1222
+ flex-wrap: wrap;
1223
+ }
1224
+
1225
+ /* Button focus states */
1226
+ button:focus-visible {
1227
+ outline: none;
1228
+ box-shadow:
1229
+ 0 0 0 2px var(--background),
1230
+ 0 0 0 4px var(--ring);
1231
+ }
1232
+
1233
+ /* Button loading state (HTMX hx-disabled-elt or manual disable) */
1234
+ button:disabled, button.htmx-request {
1235
+ opacity: 0.65;
1236
+ cursor: not-allowed;
1237
+ transform: none;
1238
+ pointer-events: none;
1239
+ }
1240
+
1241
+ button:disabled::after, button.htmx-request:not(.sw-no-loading-indicator)::after {
1242
+ content: "";
1243
+ position: absolute;
1244
+ top: 50%;
1245
+ left: 50%;
1246
+ width: 0.85em;
1247
+ height: 0.85em;
1248
+ margin-top: -0.425em;
1249
+ margin-left: -0.425em;
1250
+ border: 2px solid currentColor;
1251
+ border-top-color: transparent;
1252
+ border-radius: 50%;
1253
+ opacity: 0;
1254
+ animation:
1255
+ sw-btn-spinner-appear 0ms 300ms forwards,
1256
+ sw-spin 0.7s 300ms linear infinite;
1257
+ }
1258
+
1259
+ @keyframes sw-btn-spinner-appear {
1260
+ to { opacity: 1; }
1261
+ }
1262
+
1263
+ /* Swap-target busy treatment during in-flight requests (FAC-P1.5).
1264
+ Delay-in via transition-delay so fast responses never flicker;
1265
+ undimming (class removed) is immediate. */
1266
+ #app-container {
1267
+ transition: opacity var(--sw-transition-fast, 120ms) ease-out;
1268
+ }
1269
+
1270
+ #app-container.htmx-request {
1271
+ opacity: 0.85;
1272
+ transition-delay: 150ms;
1273
+ }
1274
+
1275
+ /* ===========================================
1276
+ Checkbox & Labels
1277
+ =========================================== */
1278
+ label {
1279
+ display: flex;
1280
+ align-items: center;
1281
+ margin: var(--sw-spacing-md) 0;
1282
+ cursor: pointer;
1283
+ user-select: none;
1284
+ color: var(--sw-color-text);
1285
+ }
1286
+
1287
+ input[type="checkbox"] {
1288
+ margin-right: var(--sw-spacing-sm);
1289
+ width: 18px;
1290
+ height: 18px;
1291
+ cursor: pointer;
1292
+ accent-color: var(--sw-color-primary);
1293
+ }
1294
+
1295
+ select {
1296
+ cursor: pointer;
1297
+ }
1298
+
1299
+ /* ===========================================
1300
+ Radio Group
1301
+ =========================================== */
1302
+ .radio-group {
1303
+ display: flex;
1304
+ flex-direction: column;
1305
+ gap: var(--sw-spacing-sm);
1306
+ margin: var(--sw-spacing-sm) 0 var(--sw-spacing-md) 0;
1307
+ }
1308
+
1309
+ .radio-option {
1310
+ display: flex;
1311
+ align-items: center;
1312
+ gap: var(--sw-spacing-sm);
1313
+ padding: var(--sw-spacing-sm) var(--sw-spacing-md);
1314
+ border: 1px solid var(--sw-color-border);
1315
+ border-radius: var(--sw-radius-md);
1316
+ cursor: pointer;
1317
+ transition: background-color var(--sw-transition), border-color var(--sw-transition);
1318
+ margin: 0;
1319
+ }
1320
+
1321
+ .radio-option:hover {
1322
+ background-color: var(--sw-color-bg);
1323
+ border-color: var(--sw-color-text-light);
1324
+ }
1325
+
1326
+ .radio-option input[type="radio"] {
1327
+ margin: 0;
1328
+ cursor: pointer;
1329
+ accent-color: var(--sw-color-primary);
1330
+ }
1331
+
1332
+ .radio-option input[type="radio"]:checked + span {
1333
+ font-weight: 500;
1334
+ color: var(--sw-color-primary);
1335
+ }
1336
+
1337
+ /* ===========================================
1338
+ Checkbox Group
1339
+ =========================================== */
1340
+ .checkbox-group {
1341
+ display: flex;
1342
+ flex-direction: column;
1343
+ gap: var(--sw-spacing-sm);
1344
+ margin: var(--sw-spacing-sm) 0 var(--sw-spacing-md) 0;
1345
+ }
1346
+
1347
+ .checkbox-group-actions {
1348
+ display: flex;
1349
+ gap: var(--sw-spacing-sm);
1350
+ margin-bottom: var(--sw-spacing-sm);
1351
+ }
1352
+
1353
+ .checkbox-group-actions .btn-sm {
1354
+ padding: var(--sw-spacing-xs) var(--sw-spacing-sm);
1355
+ font-size: var(--sw-font-size-sm);
1356
+ }
1357
+
1358
+ .checkbox-item {
1359
+ display: flex;
1360
+ align-items: flex-start;
1361
+ gap: var(--sw-spacing-sm);
1362
+ padding: var(--sw-spacing-sm) var(--sw-spacing-md);
1363
+ border: 1px solid var(--sw-color-border);
1364
+ border-radius: var(--sw-radius-md);
1365
+ cursor: pointer;
1366
+ transition: background-color var(--sw-transition), border-color var(--sw-transition);
1367
+ margin: 0;
1368
+ }
1369
+
1370
+ .checkbox-item:hover {
1371
+ background-color: var(--sw-color-bg);
1372
+ border-color: var(--sw-color-text-light);
1373
+ }
1374
+
1375
+ .checkbox-item input[type="checkbox"] {
1376
+ margin: 0;
1377
+ margin-top: 2px;
1378
+ cursor: pointer;
1379
+ accent-color: var(--sw-color-primary);
1380
+ flex-shrink: 0;
1381
+ }
1382
+
1383
+ .checkbox-item input[type="checkbox"]:checked ~ p {
1384
+ color: var(--sw-color-primary);
1385
+ }
1386
+
1387
+ /* ===========================================
1388
+ Card - Clean, Elevated
1389
+ =========================================== */
1390
+ .card {
1391
+ background: var(--card);
1392
+ color: var(--card-foreground);
1393
+ border: 1px solid var(--border);
1394
+ border-left: var(--sw-card-border-left);
1395
+ border-radius: calc(var(--radius) + 4px);
1396
+ padding: var(--sw-spacing-lg);
1397
+ margin-bottom: var(--sw-spacing-md);
1398
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
1399
+ overflow: hidden;
1400
+ word-wrap: break-word;
1401
+ overflow-wrap: break-word;
1402
+ max-width: 100%;
1403
+ }
1404
+
1405
+ /* Cards using the header/body sub-component pattern manage their
1406
+ own padding so the header's shaded band can reach the card's
1407
+ edges flush against its rounded corners. */
1408
+ .card:has(> .card-header) {
1409
+ padding: 0;
1410
+ }
1411
+
1412
+ .card h3 {
1413
+ margin-top: 0;
1414
+ margin-bottom: var(--sw-spacing-sm);
1415
+ color: var(--card-foreground);
1416
+ }
1417
+
1418
+ /* Card sub-components */
1419
+ .card-header {
1420
+ padding: 1.5rem 1.5rem 0;
1421
+ margin-bottom: 0;
1422
+ border-bottom: none;
1423
+ }
1424
+
1425
+ .card-header h1, .card-header h2, .card-header h3,
1426
+ .card-header h4, .card-header h5, .card-header h6 {
1427
+ margin: 0;
1428
+ }
1429
+
1430
+ .card-header--badged {
1431
+ display: flex;
1432
+ align-items: center;
1433
+ gap: 12px;
1434
+ padding: 14px 18px;
1435
+ background: var(--sw-surface-elevated, #EDECE6);
1436
+ border-bottom: 1px solid var(--sw-border, #E0DED6);
1437
+ }
1438
+
1439
+ .card-header__badge {
1440
+ font-family: var(--sw-font-mono, 'SFMono-Regular', 'Cascadia Code', monospace);
1441
+ font-size: 11px;
1442
+ font-weight: 600;
1443
+ color: var(--sw-accent);
1444
+ background: color-mix(in oklch, var(--sw-accent) 12%, transparent);
1445
+ padding: 2px 7px;
1446
+ border-radius: 3px;
1447
+ flex-shrink: 0;
1448
+ }
1449
+
1450
+ .card-header__title {
1451
+ font-weight: 600;
1452
+ font-size: .95rem;
1453
+ }
1454
+
1455
+ .card-header__meta {
1456
+ font-size: .8rem;
1457
+ color: var(--sw-text-dim);
1458
+ margin-left: auto;
1459
+ white-space: nowrap;
1460
+ }
1461
+
1462
+ .card-body {
1463
+ padding: 1.5rem;
1464
+ padding-top: 0;
1465
+ overflow-x: auto;
1466
+ }
1467
+
1468
+ .card-body > *:first-child {
1469
+ margin-top: 0;
1470
+ }
1471
+
1472
+ .card-body > *:last-child {
1473
+ margin-bottom: 0;
1474
+ }
1475
+
1476
+ .card-footer {
1477
+ padding: 0 1.5rem 1.5rem;
1478
+ margin-top: 0;
1479
+ border-top: 1px solid var(--border);
1480
+ display: flex;
1481
+ justify-content: flex-end;
1482
+ gap: var(--sw-spacing-sm);
1483
+ }
1484
+
1485
+ .card-footer button {
1486
+ margin: 0;
1487
+ }
1488
+
1489
+ /* ===========================================
1490
+ Stack Components (VStack / HStack)
1491
+ =========================================== */
1492
+ .sw-vstack {
1493
+ display: flex;
1494
+ flex-direction: column;
1495
+ }
1496
+
1497
+ .sw-hstack {
1498
+ display: flex;
1499
+ flex-direction: row;
1500
+ flex-wrap: wrap;
1501
+ }
1502
+
1503
+ /* Alignment classes */
1504
+ .sw-align-start { align-items: flex-start; }
1505
+ .sw-align-center { align-items: center; }
1506
+ .sw-align-end { align-items: flex-end; }
1507
+ .sw-align-stretch { align-items: stretch; }
1508
+
1509
+ /* Justify classes (for hstack mainly) */
1510
+ .sw-justify-start { justify-content: flex-start; }
1511
+ .sw-justify-center { justify-content: center; }
1512
+ .sw-justify-end { justify-content: flex-end; }
1513
+ .sw-justify-between { justify-content: space-between; }
1514
+
1515
+ /* Divider support for stacks */
1516
+ .sw-vstack.sw-divider > *:not(:last-child) {
1517
+ border-bottom: 1px solid var(--sw-color-border);
1518
+ padding-bottom: inherit;
1519
+ }
1520
+
1521
+ .sw-hstack.sw-divider > *:not(:last-child) {
1522
+ border-right: 1px solid var(--sw-color-border);
1523
+ padding-right: inherit;
1524
+ }
1525
+
1526
+ /* ===========================================
1527
+ Grid Component
1528
+ =========================================== */
1529
+ .sw-grid {
1530
+ display: grid;
1531
+ }
1532
+
1533
+ /* Responsive grid - mobile first */
1534
+ @media (max-width: 639px) {
1535
+ .sw-grid[data-cols-sm] { grid-template-columns: repeat(var(--sw-grid-cols-sm, 1), 1fr); }
1536
+ }
1537
+
1538
+ @media (min-width: 640px) and (max-width: 1023px) {
1539
+ .sw-grid[data-cols-md] { grid-template-columns: repeat(var(--sw-grid-cols-md, 2), 1fr); }
1540
+ }
1541
+
1542
+ @media (min-width: 1024px) {
1543
+ .sw-grid[data-cols-lg] { grid-template-columns: repeat(var(--sw-grid-cols-lg, 3), 1fr); }
1544
+ }
1545
+
1546
+ /* ===========================================
1547
+ Tabs Component
1548
+ =========================================== */
1549
+ .sw-tabs {
1550
+ margin: var(--sw-spacing-md) 0;
1551
+ }
1552
+
1553
+ .sw-tabs-list {
1554
+ display: flex;
1555
+ gap: var(--sw-spacing-xs);
1556
+ border-bottom: 2px solid var(--border);
1557
+ margin-bottom: var(--sw-spacing-md);
1558
+ }
1559
+
1560
+ .sw-tab-trigger {
1561
+ padding: var(--sw-spacing-sm) var(--sw-spacing-md);
1562
+ border: none;
1563
+ background: transparent;
1564
+ font-family: var(--sw-font-body);
1565
+ font-size: var(--sw-font-size-base);
1566
+ font-weight: 500;
1567
+ color: var(--muted-foreground);
1568
+ cursor: pointer;
1569
+ /* Only transition color on hover, not border/active state - prevents flash during HTMX swaps */
1570
+ transition: color var(--sw-transition);
1571
+ position: relative;
1572
+ margin: 0;
1573
+ margin-bottom: -2px;
1574
+ }
1575
+
1576
+ .sw-tab-trigger:hover {
1577
+ color: var(--foreground);
1578
+ transform: none;
1579
+ }
1580
+
1581
+ .sw-tab-trigger.sw-tab-active {
1582
+ color: var(--foreground);
1583
+ background: var(--background);
1584
+ border-bottom: 2px solid var(--foreground);
1585
+ }
1586
+
1587
+ .sw-tab-panel {
1588
+ padding: var(--sw-spacing-sm) 0;
1589
+ }
1590
+
1591
+ /* Tabs Variants */
1592
+ .sw-tabs-enclosed .sw-tabs-list {
1593
+ border-bottom: none;
1594
+ gap: 0;
1595
+ }
1596
+
1597
+ .sw-tabs-enclosed .sw-tab-trigger {
1598
+ border: 1px solid transparent;
1599
+ border-bottom: none;
1600
+ border-radius: var(--sw-radius-md) var(--sw-radius-md) 0 0;
1601
+ background: var(--sw-color-bg-elevated);
1602
+ margin-bottom: -1px;
1603
+ }
1604
+
1605
+ .sw-tabs-enclosed .sw-tab-trigger.sw-tab-active {
1606
+ background: var(--sw-color-bg-card);
1607
+ border-color: var(--sw-color-border);
1608
+ border-bottom-color: var(--sw-color-bg-card);
1609
+ }
1610
+
1611
+ .sw-tabs-enclosed .sw-tab-panel {
1612
+ border: 1px solid var(--sw-color-border);
1613
+ border-radius: 0 var(--sw-radius-md) var(--sw-radius-md) var(--sw-radius-md);
1614
+ padding: var(--sw-spacing-md);
1615
+ background: var(--sw-color-bg-card);
1616
+ }
1617
+
1618
+ .sw-tabs-soft-rounded .sw-tabs-list {
1619
+ border-bottom: none;
1620
+ background: var(--muted);
1621
+ padding: var(--sw-spacing-xs);
1622
+ border-radius: var(--sw-radius-lg);
1623
+ }
1624
+
1625
+ .sw-tabs-soft-rounded .sw-tab-trigger {
1626
+ border-radius: var(--radius);
1627
+ margin-bottom: 0;
1628
+ }
1629
+
1630
+ .sw-tabs-soft-rounded .sw-tab-trigger.sw-tab-active {
1631
+ background: var(--background);
1632
+ box-shadow: var(--sw-shadow-sm);
1633
+ border-bottom: none;
1634
+ }
1635
+
1636
+ /* ===========================================
1637
+ Breadcrumbs Component
1638
+ =========================================== */
1639
+ .sw-breadcrumbs {
1640
+ margin: var(--sw-spacing-sm) 0;
1641
+ }
1642
+
1643
+ .sw-breadcrumbs-list {
1644
+ display: flex;
1645
+ align-items: center;
1646
+ flex-wrap: wrap;
1647
+ list-style: none;
1648
+ padding: 0;
1649
+ margin: 0;
1650
+ gap: var(--sw-spacing-xs);
1651
+ }
1652
+
1653
+ .sw-breadcrumb-item {
1654
+ display: flex;
1655
+ align-items: center;
1656
+ gap: var(--sw-spacing-xs);
1657
+ }
1658
+
1659
+ .sw-breadcrumb-separator {
1660
+ color: var(--sw-color-text-light);
1661
+ font-size: var(--sw-font-size-sm);
1662
+ }
1663
+
1664
+ .sw-breadcrumb-link {
1665
+ color: var(--sw-color-primary);
1666
+ text-decoration: none;
1667
+ font-size: var(--sw-font-size-sm);
1668
+ transition: color var(--sw-transition);
1669
+ }
1670
+
1671
+ .sw-breadcrumb-link:hover {
1672
+ color: var(--sw-color-primary-hover);
1673
+ text-decoration: underline;
1674
+ }
1675
+
1676
+ .sw-breadcrumb-current {
1677
+ color: var(--sw-color-text-muted);
1678
+ font-size: var(--sw-font-size-sm);
1679
+ }
1680
+
1681
+ /* ===========================================
1682
+ Navbar Component
1683
+ =========================================== */
1684
+ .sw-navbar {
1685
+ display: flex;
1686
+ align-items: center;
1687
+ gap: var(--sw-spacing-xs);
1688
+ padding: var(--sw-spacing-xs) var(--sw-spacing-md);
1689
+ background: var(--sw-color-bg-elevated);
1690
+ border-bottom: 1px solid var(--border);
1691
+ margin-bottom: var(--sw-spacing-md);
1692
+ }
1693
+
1694
+ .sw-navbar-item {
1695
+ padding: var(--sw-spacing-xs) var(--sw-spacing-sm);
1696
+ color: var(--sw-color-text-muted);
1697
+ text-decoration: none;
1698
+ border-radius: var(--sw-radius-sm);
1699
+ font-size: var(--sw-font-size-sm);
1700
+ transition: color var(--sw-transition), background var(--sw-transition);
1701
+ }
1702
+
1703
+ .sw-navbar-item:hover {
1704
+ color: var(--sw-color-text);
1705
+ background: var(--muted);
1706
+ }
1707
+
1708
+ .sw-navbar-item-active {
1709
+ font-weight: 600;
1710
+ color: var(--sw-color-text);
1711
+ cursor: default;
1712
+ }
1713
+
1714
+ .sw-navbar-item__close {
1715
+ margin-left: 0.35em;
1716
+ opacity: 0.65;
1717
+ font-weight: 400;
1718
+ }
1719
+
1720
+ /* ===========================================
1721
+ Dropdown/Menu Component
1722
+ =========================================== */
1723
+ .sw-dropdown {
1724
+ position: relative;
1725
+ display: inline-block;
1726
+ }
1727
+
1728
+ .sw-dropdown-trigger {
1729
+ cursor: pointer;
1730
+ }
1731
+
1732
+ .sw-dropdown-menu {
1733
+ position: absolute;
1734
+ top: 100%;
1735
+ left: 0;
1736
+ z-index: 100;
1737
+ min-width: 180px;
1738
+ margin-top: var(--sw-spacing-xs);
1739
+ padding: var(--sw-spacing-xs) 0;
1740
+ background: var(--popover);
1741
+ color: var(--popover-foreground);
1742
+ border: 1px solid var(--border);
1743
+ border-radius: var(--radius);
1744
+ box-shadow: var(--sw-shadow-lg);
1745
+ }
1746
+
1747
+ .sw-menu-item {
1748
+ display: block;
1749
+ width: 100%;
1750
+ padding: var(--sw-spacing-sm) var(--sw-spacing-md);
1751
+ border: none;
1752
+ background: transparent;
1753
+ font-family: var(--sw-font-body);
1754
+ font-size: var(--sw-font-size-sm);
1755
+ font-weight: 400;
1756
+ color: var(--sw-color-text);
1757
+ text-align: left;
1758
+ cursor: pointer;
1759
+ transition: background var(--sw-transition);
1760
+ margin: 0;
1761
+ }
1762
+
1763
+ .sw-menu-item:hover {
1764
+ background: var(--accent);
1765
+ color: var(--accent-foreground);
1766
+ transform: none;
1767
+ }
1768
+
1769
+ .sw-menu-item-destructive {
1770
+ color: var(--destructive);
1771
+ }
1772
+
1773
+ .sw-menu-item-destructive:hover {
1774
+ background: color-mix(in oklch, var(--destructive) 10%, transparent);
1775
+ }
1776
+
1777
+ .sw-menu-divider {
1778
+ margin: var(--sw-spacing-xs) 0;
1779
+ border: none;
1780
+ border-top: 1px solid var(--sw-color-border);
1781
+ }
1782
+
1783
+ /* Dropdown transitions */
1784
+ .sw-transition-enter { transition: all var(--sw-transition); }
1785
+ .sw-transition-enter-start { opacity: 0; transform: translateY(-4px); }
1786
+ .sw-transition-enter-end { opacity: 1; transform: translateY(0); }
1787
+ .sw-transition-leave { transition: all var(--sw-transition-fast); }
1788
+ .sw-transition-leave-start { opacity: 1; transform: translateY(0); }
1789
+ .sw-transition-leave-end { opacity: 0; transform: translateY(-4px); }
1790
+
1791
+ /* ===========================================
1792
+ Modal Component
1793
+ =========================================== */
1794
+ .sw-modal-wrapper {
1795
+ position: relative;
1796
+ }
1797
+
1798
+ .sw-modal-backdrop {
1799
+ position: fixed;
1800
+ inset: 0;
1801
+ background: rgba(0, 0, 0, 0.5);
1802
+ z-index: 999;
1803
+ }
1804
+
1805
+ .sw-modal {
1806
+ position: fixed;
1807
+ top: 50%;
1808
+ left: 50%;
1809
+ transform: translate(-50%, -50%);
1810
+ z-index: 1000;
1811
+ background: var(--popover);
1812
+ color: var(--popover-foreground);
1813
+ border: 1px solid var(--border);
1814
+ border-radius: var(--sw-radius-lg);
1815
+ box-shadow: var(--sw-shadow-xl);
1816
+ max-height: 90vh;
1817
+ overflow: hidden;
1818
+ display: flex;
1819
+ flex-direction: column;
1820
+ }
1821
+
1822
+ /* Modal sizes */
1823
+ .sw-modal-sm { width: min(400px, 90vw); }
1824
+ .sw-modal-md { width: min(560px, 90vw); }
1825
+ .sw-modal-lg { width: min(800px, 90vw); }
1826
+ .sw-modal-xl { width: min(1140px, 95vw); }
1827
+
1828
+ .sw-modal-header {
1829
+ display: flex;
1830
+ align-items: center;
1831
+ justify-content: space-between;
1832
+ padding: var(--sw-spacing-md) var(--sw-spacing-lg);
1833
+ border-bottom: 1px solid var(--border);
1834
+ flex-shrink: 0;
1835
+ }
1836
+
1837
+ .sw-modal-title {
1838
+ margin: 0;
1839
+ font-size: 1.25rem;
1840
+ font-weight: 600;
1841
+ color: var(--sw-color-text);
1842
+ }
1843
+
1844
+ .sw-modal-close {
1845
+ background: transparent;
1846
+ border: none;
1847
+ font-size: 1.5rem;
1848
+ line-height: 1;
1849
+ color: var(--sw-color-text-muted);
1850
+ cursor: pointer;
1851
+ padding: 0;
1852
+ margin: 0;
1853
+ width: 32px;
1854
+ height: 32px;
1855
+ display: flex;
1856
+ align-items: center;
1857
+ justify-content: center;
1858
+ border-radius: var(--sw-radius-sm);
1859
+ transition: background var(--sw-transition), color var(--sw-transition);
1860
+ }
1861
+
1862
+ .sw-modal-close:hover {
1863
+ background: var(--sw-color-bg-elevated);
1864
+ color: var(--sw-color-text);
1865
+ transform: none;
1866
+ }
1867
+
1868
+ .sw-modal-close-only {
1869
+ position: absolute;
1870
+ top: var(--sw-spacing-sm);
1871
+ right: var(--sw-spacing-sm);
1872
+ }
1873
+
1874
+ .sw-modal-body {
1875
+ padding: var(--sw-spacing-lg);
1876
+ overflow-y: auto;
1877
+ flex: 1;
1878
+ }
1879
+
1880
+ .sw-modal-body > *:first-child {
1881
+ margin-top: 0;
1882
+ }
1883
+
1884
+ .sw-modal-body > *:last-child {
1885
+ margin-bottom: 0;
1886
+ }
1887
+
1888
+ .sw-modal-footer {
1889
+ display: flex;
1890
+ justify-content: flex-end;
1891
+ gap: var(--sw-spacing-sm);
1892
+ padding: var(--sw-spacing-md) var(--sw-spacing-lg);
1893
+ border-top: 1px solid var(--border);
1894
+ background: var(--muted);
1895
+ flex-shrink: 0;
1896
+ }
1897
+
1898
+ .sw-modal-footer button {
1899
+ margin: 0;
1900
+ }
1901
+
1902
+ /* Modal transitions */
1903
+ .sw-transition-fade-enter { transition: opacity var(--sw-transition); }
1904
+ .sw-transition-fade-enter-start { opacity: 0; }
1905
+ .sw-transition-fade-enter-end { opacity: 1; }
1906
+ .sw-transition-fade-leave { transition: opacity var(--sw-transition-fast); }
1907
+ .sw-transition-fade-leave-start { opacity: 1; }
1908
+ .sw-transition-fade-leave-end { opacity: 0; }
1909
+
1910
+ .sw-transition-modal-enter { transition: all var(--sw-transition); }
1911
+ .sw-transition-modal-enter-start { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }
1912
+ .sw-transition-modal-enter-end { opacity: 1; transform: translate(-50%, -50%) scale(1); }
1913
+ .sw-transition-modal-leave { transition: all var(--sw-transition-fast); }
1914
+ .sw-transition-modal-leave-start { opacity: 1; transform: translate(-50%, -50%) scale(1); }
1915
+ .sw-transition-modal-leave-end { opacity: 0; transform: translate(-50%, -50%) scale(0.95); }
1916
+
1917
+ /* ===========================================
1918
+ Alert Component
1919
+ =========================================== */
1920
+ .sw-alert {
1921
+ display: flex;
1922
+ align-items: flex-start;
1923
+ gap: var(--sw-spacing-sm);
1924
+ padding: var(--sw-spacing-md);
1925
+ margin: var(--sw-spacing-md) 0;
1926
+ border-radius: var(--sw-radius-md);
1927
+ border: 1px solid;
1928
+ }
1929
+
1930
+ .sw-alert-icon {
1931
+ flex-shrink: 0;
1932
+ font-size: 1.25rem;
1933
+ line-height: 1;
1934
+ }
1935
+
1936
+ .sw-alert-content {
1937
+ flex: 1;
1938
+ min-width: 0;
1939
+ }
1940
+
1941
+ .sw-alert-title {
1942
+ display: block;
1943
+ margin-bottom: var(--sw-spacing-xs);
1944
+ font-weight: 600;
1945
+ }
1946
+
1947
+ .sw-alert-dismiss {
1948
+ flex-shrink: 0;
1949
+ background: transparent;
1950
+ border: none;
1951
+ font-size: 1.25rem;
1952
+ line-height: 1;
1953
+ cursor: pointer;
1954
+ opacity: 0.6;
1955
+ padding: 0;
1956
+ margin: 0;
1957
+ width: 24px;
1958
+ height: 24px;
1959
+ display: flex;
1960
+ align-items: center;
1961
+ justify-content: center;
1962
+ border-radius: var(--sw-radius-sm);
1963
+ transition: opacity var(--sw-transition), background var(--sw-transition);
1964
+ }
1965
+
1966
+ .sw-alert-dismiss:hover {
1967
+ opacity: 1;
1968
+ background: rgba(0, 0, 0, 0.1);
1969
+ transform: none;
1970
+ }
1971
+
1972
+ /* Alert variants */
1973
+ .sw-alert-info {
1974
+ background: color-mix(in oklch, var(--info) 10%, transparent);
1975
+ border-color: color-mix(in oklch, var(--info) 30%, transparent);
1976
+ color: var(--info);
1977
+ }
1978
+ .sw-alert-info .sw-alert-icon { color: var(--info); }
1979
+
1980
+ .sw-alert-success {
1981
+ background: color-mix(in oklch, var(--success) 10%, transparent);
1982
+ border-color: color-mix(in oklch, var(--success) 30%, transparent);
1983
+ color: var(--success);
1984
+ }
1985
+ .sw-alert-success .sw-alert-icon { color: var(--success); }
1986
+
1987
+ .sw-alert-warning {
1988
+ background: color-mix(in oklch, var(--warning) 10%, transparent);
1989
+ border-color: color-mix(in oklch, var(--warning) 30%, transparent);
1990
+ color: var(--warning);
1991
+ }
1992
+ .sw-alert-warning .sw-alert-icon { color: var(--warning); }
1993
+
1994
+ .sw-alert-error {
1995
+ background: color-mix(in oklch, var(--destructive) 10%, transparent);
1996
+ border-color: color-mix(in oklch, var(--destructive) 30%, transparent);
1997
+ color: var(--destructive);
1998
+ }
1999
+ .sw-alert-error .sw-alert-icon { color: var(--destructive); }
2000
+
2001
+ /* ===========================================
2002
+ Toast Component (Multi-toast Stack)
2003
+ =========================================== */
2004
+ .sw-toast-container {
2005
+ position: fixed;
2006
+ z-index: 1100;
2007
+ pointer-events: none;
2008
+ display: flex;
2009
+ flex-direction: column;
2010
+ gap: var(--sw-spacing-sm);
2011
+ max-height: 100vh;
2012
+ overflow: hidden;
2013
+ }
2014
+
2015
+ .sw-toast-container > * {
2016
+ pointer-events: auto;
2017
+ }
2018
+
2019
+ .sw-toast-top-right { top: var(--sw-spacing-lg); right: var(--sw-spacing-lg); }
2020
+ .sw-toast-top-left { top: var(--sw-spacing-lg); left: var(--sw-spacing-lg); }
2021
+ .sw-toast-bottom-right { bottom: var(--sw-spacing-lg); right: var(--sw-spacing-lg); flex-direction: column-reverse; }
2022
+ .sw-toast-bottom-left { bottom: var(--sw-spacing-lg); left: var(--sw-spacing-lg); flex-direction: column-reverse; }
2023
+
2024
+ .sw-toast {
2025
+ display: flex;
2026
+ align-items: center;
2027
+ gap: var(--sw-spacing-sm);
2028
+ min-width: 280px;
2029
+ max-width: 420px;
2030
+ padding: var(--sw-spacing-md);
2031
+ background: var(--card);
2032
+ color: var(--card-foreground);
2033
+ border-radius: var(--radius);
2034
+ box-shadow: var(--sw-shadow-xl);
2035
+ border: 1px solid var(--border);
2036
+ }
2037
+
2038
+ .sw-toast-icon {
2039
+ flex-shrink: 0;
2040
+ font-size: 1.25rem;
2041
+ line-height: 1;
2042
+ }
2043
+
2044
+ .sw-toast-message {
2045
+ flex: 1;
2046
+ min-width: 0;
2047
+ font-size: var(--sw-font-size-sm);
2048
+ }
2049
+
2050
+ .sw-toast-dismiss {
2051
+ flex-shrink: 0;
2052
+ background: transparent;
2053
+ border: none;
2054
+ font-size: 1rem;
2055
+ line-height: 1;
2056
+ cursor: pointer;
2057
+ opacity: 0.5;
2058
+ padding: 0;
2059
+ margin: 0;
2060
+ width: 20px;
2061
+ height: 20px;
2062
+ display: flex;
2063
+ align-items: center;
2064
+ justify-content: center;
2065
+ border-radius: var(--sw-radius-sm);
2066
+ transition: opacity var(--sw-transition);
2067
+ }
2068
+
2069
+ .sw-toast-dismiss:hover {
2070
+ opacity: 1;
2071
+ transform: none;
2072
+ }
2073
+
2074
+ /* Toast variants */
2075
+ .sw-toast-info .sw-toast-icon { color: var(--info); }
2076
+ .sw-toast-success .sw-toast-icon { color: var(--success); }
2077
+ .sw-toast-warning .sw-toast-icon { color: var(--warning); }
2078
+ .sw-toast-error .sw-toast-icon { color: var(--destructive); }
2079
+
2080
+ /* Toast transitions */
2081
+ .sw-transition-toast-enter { transition: all var(--sw-transition); }
2082
+ .sw-transition-toast-enter-start { opacity: 0; transform: translateX(100%); }
2083
+ .sw-transition-toast-enter-end { opacity: 1; transform: translateX(0); }
2084
+ .sw-transition-toast-leave { transition: all var(--sw-transition); }
2085
+ .sw-transition-toast-leave-start { opacity: 1; transform: translateX(0); }
2086
+ .sw-transition-toast-leave-end { opacity: 0; transform: translateX(100%); }
2087
+
2088
+ /* ===========================================
2089
+ Progress Bar Component
2090
+ =========================================== */
2091
+ .sw-progress {
2092
+ position: relative;
2093
+ height: 8px;
2094
+ background: var(--muted);
2095
+ border-radius: var(--sw-radius-sm);
2096
+ overflow: hidden;
2097
+ margin: var(--sw-spacing-sm) 0;
2098
+ }
2099
+
2100
+ .sw-progress-bar {
2101
+ height: 100%;
2102
+ background: var(--primary);
2103
+ border-radius: var(--sw-radius-sm);
2104
+ transition: width 0.3s ease;
2105
+ }
2106
+
2107
+ .sw-progress-label {
2108
+ position: absolute;
2109
+ right: var(--sw-spacing-xs);
2110
+ top: 50%;
2111
+ transform: translateY(-50%);
2112
+ font-size: 10px;
2113
+ font-weight: 600;
2114
+ color: var(--sw-color-text);
2115
+ text-shadow: 0 0 2px var(--sw-color-bg-card);
2116
+ }
2117
+
2118
+ /* Progress variants */
2119
+ .sw-progress-success .sw-progress-bar { background: var(--success); }
2120
+ .sw-progress-warning .sw-progress-bar { background: var(--warning); }
2121
+ .sw-progress-error .sw-progress-bar { background: var(--destructive); }
2122
+
2123
+ /* Animated progress */
2124
+ .sw-progress-animated .sw-progress-bar {
2125
+ background-image: linear-gradient(
2126
+ 45deg,
2127
+ rgba(255, 255, 255, 0.15) 25%,
2128
+ transparent 25%,
2129
+ transparent 50%,
2130
+ rgba(255, 255, 255, 0.15) 50%,
2131
+ rgba(255, 255, 255, 0.15) 75%,
2132
+ transparent 75%,
2133
+ transparent
2134
+ );
2135
+ background-size: 1rem 1rem;
2136
+ animation: sw-progress-stripes 1s linear infinite;
2137
+ }
2138
+
2139
+ @keyframes sw-progress-stripes {
2140
+ 0% { background-position: 1rem 0; }
2141
+ 100% { background-position: 0 0; }
2142
+ }
2143
+
2144
+ /* ===========================================
2145
+ Spinner Component
2146
+ =========================================== */
2147
+ .sw-spinner-container {
2148
+ display: inline-flex;
2149
+ align-items: center;
2150
+ gap: var(--sw-spacing-sm);
2151
+ }
2152
+
2153
+ .sw-spinner {
2154
+ border: 2px solid var(--sw-color-border);
2155
+ border-top-color: var(--sw-color-primary);
2156
+ border-radius: 50%;
2157
+ animation: sw-spin 0.8s linear infinite;
2158
+ }
2159
+
2160
+ .sw-spinner-sm { width: 16px; height: 16px; }
2161
+ .sw-spinner-md { width: 24px; height: 24px; }
2162
+ .sw-spinner-lg { width: 40px; height: 40px; border-width: 3px; }
2163
+
2164
+ .sw-spinner-label {
2165
+ font-size: var(--sw-font-size-sm);
2166
+ color: var(--sw-color-text-muted);
2167
+ }
2168
+
2169
+ @keyframes sw-spin {
2170
+ to { transform: rotate(360deg); }
2171
+ }
2172
+
2173
+ /* ===========================================
2174
+ Theme Switcher Component
2175
+ =========================================== */
2176
+ .sw-theme-switcher {
2177
+ display: inline-flex;
2178
+ align-items: center;
2179
+ gap: var(--sw-spacing-sm);
2180
+ }
2181
+
2182
+ .sw-theme-switcher-fixed {
2183
+ position: fixed;
2184
+ top: var(--sw-spacing-md);
2185
+ right: var(--sw-spacing-md);
2186
+ z-index: 1000;
2187
+ }
2188
+
2189
+ .sw-theme-switcher-label {
2190
+ font-size: var(--sw-font-size-sm);
2191
+ color: var(--sw-color-text-muted);
2192
+ font-weight: 500;
2193
+ }
2194
+
2195
+ .sw-theme-switcher-dropdown {
2196
+ position: relative;
2197
+ }
2198
+
2199
+ .sw-theme-switcher-trigger {
2200
+ display: flex;
2201
+ align-items: center;
2202
+ gap: var(--sw-spacing-xs);
2203
+ padding: var(--sw-spacing-xs) var(--sw-spacing-sm);
2204
+ background: var(--sw-color-bg-card);
2205
+ border: 1px solid var(--sw-color-border);
2206
+ border-radius: var(--sw-radius-md);
2207
+ font-size: var(--sw-font-size-sm);
2208
+ color: var(--sw-color-text);
2209
+ cursor: pointer;
2210
+ transition: border-color var(--sw-transition), box-shadow var(--sw-transition);
2211
+ }
2212
+
2213
+ .sw-theme-switcher-trigger:hover {
2214
+ border-color: var(--sw-color-border-strong);
2215
+ transform: none;
2216
+ }
2217
+
2218
+ .sw-theme-switcher-trigger:focus {
2219
+ outline: none;
2220
+ border-color: var(--sw-color-primary);
2221
+ box-shadow: 0 0 0 2px var(--sw-color-primary-glow);
2222
+ }
2223
+
2224
+ .sw-theme-switcher-arrow {
2225
+ font-size: 10px;
2226
+ color: var(--sw-color-text-muted);
2227
+ }
2228
+
2229
+ .sw-theme-switcher-menu {
2230
+ position: absolute;
2231
+ top: 100%;
2232
+ right: 0;
2233
+ margin-top: var(--sw-spacing-xs);
2234
+ min-width: 180px;
2235
+ background: var(--sw-color-bg-card);
2236
+ border: 1px solid var(--sw-color-border);
2237
+ border-radius: var(--sw-radius-md);
2238
+ box-shadow: var(--sw-shadow-lg);
2239
+ overflow: hidden;
2240
+ z-index: 1001;
2241
+ }
2242
+
2243
+ .sw-theme-switcher-option {
2244
+ display: flex;
2245
+ flex-direction: column;
2246
+ align-items: flex-start;
2247
+ width: 100%;
2248
+ padding: var(--sw-spacing-sm) var(--sw-spacing-md);
2249
+ background: transparent;
2250
+ border: none;
2251
+ text-align: left;
2252
+ cursor: pointer;
2253
+ transition: background var(--sw-transition-fast);
2254
+ }
2255
+
2256
+ .sw-theme-switcher-option:hover {
2257
+ background: var(--sw-color-bg-elevated);
2258
+ transform: none;
2259
+ }
2260
+
2261
+ .sw-theme-switcher-option-label {
2262
+ font-size: var(--sw-font-size-sm);
2263
+ font-weight: 500;
2264
+ color: var(--sw-color-text);
2265
+ }
2266
+
2267
+ .sw-theme-switcher-option-desc {
2268
+ font-size: 12px;
2269
+ color: var(--sw-color-text-muted);
2270
+ }
2271
+
2272
+ /* ===========================================
2273
+ Lesson Text & Terms (Educational Content)
2274
+ =========================================== */
2275
+ .lesson-text {
2276
+ position: relative;
2277
+ line-height: 1.8;
2278
+ font-size: var(--sw-font-size-lg);
2279
+ color: var(--sw-color-text);
2280
+ }
2281
+
2282
+ .sw-term, .term {
2283
+ text-decoration: underline;
2284
+ text-decoration-style: dotted;
2285
+ text-decoration-color: var(--sw-term-color);
2286
+ text-underline-offset: 3px;
2287
+ cursor: help;
2288
+ color: var(--sw-term-color);
2289
+ font-weight: 500;
2290
+ padding: 0 2px;
2291
+ border-radius: var(--sw-radius-sm);
2292
+ transition: background-color var(--sw-transition), color var(--sw-transition);
2293
+ }
2294
+
2295
+ .sw-term:hover, .sw-term:focus,
2296
+ .term:hover, .term:focus {
2297
+ background-color: var(--sw-term-bg-hover);
2298
+ outline: none;
2299
+ }
2300
+
2301
+ /* ===========================================
2302
+ Tooltip
2303
+ =========================================== */
2304
+ .sw-tooltip, .tooltip {
2305
+ position: fixed;
2306
+ transform: translateX(-50%) translateY(-100%);
2307
+ background: var(--sw-tooltip-bg);
2308
+ color: var(--sw-tooltip-text);
2309
+ padding: var(--sw-spacing-md);
2310
+ border-radius: var(--sw-radius-md);
2311
+ font-size: var(--sw-font-size-sm);
2312
+ max-width: 350px;
2313
+ box-shadow: var(--sw-shadow-lg);
2314
+ z-index: 1000;
2315
+ cursor: pointer;
2316
+ white-space: normal;
2317
+ word-wrap: break-word;
2318
+ }
2319
+
2320
+ .sw-tooltip::after, .tooltip::after {
2321
+ content: '';
2322
+ position: absolute;
2323
+ top: 100%;
2324
+ left: 50%;
2325
+ transform: translateX(-50%);
2326
+ border: 8px solid transparent;
2327
+ border-top-color: var(--sw-tooltip-bg);
2328
+ }
2329
+
2330
+ .tooltip-content {
2331
+ line-height: 1.5;
2332
+ }
2333
+
2334
+ .tooltip-hint {
2335
+ font-size: 12px;
2336
+ color: #94a3b8;
2337
+ margin-top: var(--sw-spacing-sm);
2338
+ font-style: italic;
2339
+ }
2340
+
2341
+ /* ===========================================
2342
+ Collapsible - Refined Accordion
2343
+ =========================================== */
2344
+ .collapsible {
2345
+ margin: var(--sw-spacing-md) 0;
2346
+ border: 1px solid var(--sw-color-border);
2347
+ border-radius: var(--sw-radius-lg);
2348
+ overflow: hidden;
2349
+ transition: box-shadow var(--sw-transition);
2350
+ }
2351
+
2352
+ .collapsible:hover {
2353
+ box-shadow: var(--sw-shadow-sm);
2354
+ }
2355
+
2356
+ .collapsible-header {
2357
+ display: flex;
2358
+ align-items: center;
2359
+ gap: var(--sw-spacing-sm);
2360
+ padding: var(--sw-spacing-md) var(--sw-spacing-lg);
2361
+ background: var(--sw-color-bg-elevated);
2362
+ cursor: pointer;
2363
+ transition: background var(--sw-transition);
2364
+ user-select: none;
2365
+ }
2366
+
2367
+ .collapsible-header:hover {
2368
+ background: var(--sw-color-border);
2369
+ }
2370
+
2371
+ .collapsible-icon {
2372
+ font-size: 10px;
2373
+ color: var(--sw-color-primary);
2374
+ width: 16px;
2375
+ height: 16px;
2376
+ display: flex;
2377
+ align-items: center;
2378
+ justify-content: center;
2379
+ background: var(--sw-color-primary-light);
2380
+ border-radius: var(--sw-radius-sm);
2381
+ transition: transform var(--sw-transition);
2382
+ }
2383
+
2384
+ .collapsible-label {
2385
+ font-family: var(--sw-font-body);
2386
+ font-weight: 600;
2387
+ font-size: var(--sw-font-size-sm);
2388
+ color: var(--sw-color-text);
2389
+ letter-spacing: 0.01em;
2390
+ }
2391
+
2392
+ .collapsible-content {
2393
+ padding: var(--sw-spacing-lg);
2394
+ border-top: 1px solid var(--sw-color-border);
2395
+ line-height: var(--sw-line-height);
2396
+ background: var(--sw-color-bg-card);
2397
+ }
2398
+
2399
+ .sw-collapsible-subtitle {
2400
+ font-size: 0.8rem;
2401
+ color: var(--sw-color-text-muted, #666);
2402
+ }
2403
+
2404
+ .sw-collapsible-badge {
2405
+ margin-left: auto;
2406
+ }
2407
+
2408
+ /* ===========================================
2409
+ Scroll Box
2410
+ =========================================== */
2411
+ .sw-scroll-box {
2412
+ position: relative;
2413
+ border: 1px solid var(--sw-color-border);
2414
+ border-radius: var(--sw-radius-md);
2415
+ padding: var(--sw-spacing-md);
2416
+ background: var(--sw-color-bg-card);
2417
+ transition: box-shadow var(--sw-transition);
2418
+ scrollbar-width: thin;
2419
+ scrollbar-color: var(--sw-color-border-strong) transparent;
2420
+ -webkit-mask-image: linear-gradient(to bottom, black calc(100% - 28px), transparent 100%);
2421
+ mask-image: linear-gradient(to bottom, black calc(100% - 28px), transparent 100%);
2422
+ }
2423
+
2424
+ .sw-scroll-box::-webkit-scrollbar { width: 6px; }
2425
+ .sw-scroll-box::-webkit-scrollbar-track { background: transparent; }
2426
+ .sw-scroll-box::-webkit-scrollbar-thumb {
2427
+ background: var(--sw-color-border-strong);
2428
+ border-radius: var(--sw-radius-sm);
2429
+ }
2430
+ .sw-scroll-box::-webkit-scrollbar-thumb:hover {
2431
+ background: var(--sw-color-text-muted);
2432
+ }
2433
+
2434
+ /* ===========================================
2435
+ Score Table
2436
+ =========================================== */
2437
+ .score-table {
2438
+ width: 100%;
2439
+ border-collapse: collapse;
2440
+ margin: 12px 0;
2441
+ font-size: var(--sw-font-size-sm);
2442
+ }
2443
+
2444
+ .score-table th,
2445
+ .score-table td {
2446
+ padding: 8px 12px;
2447
+ text-align: left;
2448
+ border-bottom: 1px solid var(--sw-color-border);
2449
+ }
2450
+
2451
+ .score-table th {
2452
+ background: var(--sw-color-bg);
2453
+ font-weight: 600;
2454
+ color: var(--sw-color-text);
2455
+ }
2456
+
2457
+ .score-cell {
2458
+ font-weight: bold;
2459
+ text-align: center !important;
2460
+ border-radius: var(--sw-radius-sm);
2461
+ width: 60px;
2462
+ }
2463
+
2464
+ .score-high {
2465
+ background: #d4edda;
2466
+ color: #155724;
2467
+ }
2468
+
2469
+ .score-medium {
2470
+ background: #fff3cd;
2471
+ color: #856404;
2472
+ }
2473
+
2474
+ .score-low {
2475
+ background: #f8d7da;
2476
+ color: #721c24;
2477
+ }
2478
+
2479
+ .score-meaning {
2480
+ color: var(--sw-color-text-muted);
2481
+ font-style: italic;
2482
+ }
2483
+
2484
+ /* ===========================================
2485
+ Status Badge
2486
+
2487
+ "status-badge"/"status-badge-*" are the legacy,
2488
+ unprefixed hooks (still styled for back-compat --
2489
+ deprecated, removed at 1.0; this generic name is the
2490
+ exact collision stream_weaver-lyb was filed for).
2491
+ "sw-status-badge"/"sw-status-badge--*"/"sw-status-badge__*"
2492
+ are the documented stable hooks (stream_weaver-oeo).
2493
+ =========================================== */
2494
+ .status-badge, .sw-status-badge {
2495
+ display: inline-flex;
2496
+ align-items: center;
2497
+ gap: var(--sw-spacing-xs);
2498
+ padding: var(--sw-spacing-xs) var(--sw-spacing-sm);
2499
+ border-radius: var(--sw-radius-sm);
2500
+ font-size: var(--sw-font-size-sm);
2501
+ margin: var(--sw-spacing-xs) var(--sw-spacing-sm) var(--sw-spacing-xs) 0;
2502
+ }
2503
+
2504
+ .status-badge-strong, .sw-status-badge--strong {
2505
+ background-color: rgba(16, 185, 129, 0.1);
2506
+ color: #059669;
2507
+ }
2508
+
2509
+ .status-badge-maybe, .sw-status-badge--maybe {
2510
+ background-color: rgba(245, 158, 11, 0.1);
2511
+ color: #d97706;
2512
+ }
2513
+
2514
+ .status-badge-skip, .sw-status-badge--skip {
2515
+ background-color: rgba(239, 68, 68, 0.1);
2516
+ color: #dc2626;
2517
+ }
2518
+
2519
+ .status-badge-icon, .sw-status-badge__icon {
2520
+ font-size: 1em;
2521
+ }
2522
+
2523
+ .status-badge-label, .sw-status-badge__label {
2524
+ font-weight: 600;
2525
+ }
2526
+
2527
+ .status-badge-reasoning, .sw-status-badge__reasoning {
2528
+ color: var(--sw-color-text-muted);
2529
+ }
2530
+
2531
+ /* ===========================================
2532
+ Tag Buttons
2533
+ =========================================== */
2534
+ .tag-buttons {
2535
+ display: flex;
2536
+ flex-wrap: wrap;
2537
+ gap: var(--sw-spacing-xs);
2538
+ margin: var(--sw-spacing-sm) 0;
2539
+ }
2540
+
2541
+ .tag-btn {
2542
+ padding: var(--sw-spacing-xs) var(--sw-spacing-sm);
2543
+ border: 1px solid var(--sw-color-border);
2544
+ border-radius: var(--sw-radius-sm);
2545
+ background: var(--sw-color-bg-card);
2546
+ color: var(--sw-color-text);
2547
+ font-size: var(--sw-font-size-sm);
2548
+ cursor: pointer;
2549
+ transition: all var(--sw-transition);
2550
+ margin: 0;
2551
+ }
2552
+
2553
+ .tag-btn:hover {
2554
+ border-color: var(--sw-color-primary);
2555
+ background: rgba(59, 130, 246, 0.05);
2556
+ }
2557
+
2558
+ .tag-btn-selected {
2559
+ border-color: var(--sw-color-primary);
2560
+ background: var(--sw-color-primary);
2561
+ color: white;
2562
+ }
2563
+
2564
+ .tag-buttons-destructive .tag-btn:hover {
2565
+ border-color: #dc2626;
2566
+ background: rgba(220, 38, 38, 0.05);
2567
+ }
2568
+
2569
+ .tag-buttons-destructive .tag-btn-selected {
2570
+ border-color: #dc2626;
2571
+ background: #dc2626;
2572
+ color: white;
2573
+ }
2574
+
2575
+ /* ===========================================
2576
+ External Link Button
2577
+ =========================================== */
2578
+ .external-link-btn {
2579
+ display: inline-flex;
2580
+ align-items: center;
2581
+ gap: var(--sw-spacing-xs);
2582
+ text-decoration: none;
2583
+ }
2584
+
2585
+ .external-link-btn::after {
2586
+ content: "↗";
2587
+ font-size: 0.8em;
2588
+ }
2589
+
2590
+ /* ===========================================
2591
+ Columns Layout
2592
+ =========================================== */
2593
+ .sw-columns {
2594
+ display: flex;
2595
+ gap: var(--sw-spacing-md);
2596
+ align-items: flex-start;
2597
+ }
2598
+
2599
+ .sw-column {
2600
+ min-width: 0; /* Prevent flex items from overflowing */
2601
+ }
2602
+
2603
+ .sw-column p {
2604
+ margin: var(--sw-spacing-xs) 0;
2605
+ color: var(--sw-color-text);
2606
+ }
2607
+
2608
+ /* Monica-style sidebar sections */
2609
+ .sw-column.sidebar-facts {
2610
+ display: flex;
2611
+ flex-direction: column;
2612
+ gap: var(--sw-spacing-sm);
2613
+ }
2614
+
2615
+ /* "sidebar-section" is the legacy, unprefixed name (still
2616
+ styled for back-compat -- deprecated, removed at 1.0;
2617
+ this generic name is the exact collision stream_weaver-lyb
2618
+ was filed for, e.g. a real app's own ".sidebar-section").
2619
+ "sw-sidebar-section" is the documented stable hook
2620
+ (stream_weaver-oeo) -- apply it via `class: "sw-sidebar-section"`
2621
+ on any div/text call; there is no dedicated component for
2622
+ this utility class. */
2623
+ .sidebar-section, .sw-sidebar-section {
2624
+ background: var(--sw-color-bg-card);
2625
+ border: 1px solid var(--sw-color-border);
2626
+ border-radius: var(--sw-radius-md);
2627
+ padding: var(--sw-spacing-md);
2628
+ }
2629
+
2630
+ .sidebar-section h4, .sw-sidebar-section h4 {
2631
+ margin: 0 0 var(--sw-spacing-xs) 0;
2632
+ color: var(--sw-color-text-muted);
2633
+ font-size: 11px;
2634
+ font-weight: 600;
2635
+ text-transform: uppercase;
2636
+ letter-spacing: 0.05em;
2637
+ }
2638
+
2639
+ .sidebar-section p, .sw-sidebar-section p {
2640
+ margin: 0;
2641
+ color: var(--sw-color-text);
2642
+ font-size: var(--sw-font-size-sm);
2643
+ }
2644
+
2645
+ .sidebar-section a, .sw-sidebar-section a {
2646
+ color: var(--sw-color-primary);
2647
+ text-decoration: none;
2648
+ }
2649
+
2650
+ .sidebar-section a:hover, .sw-sidebar-section a:hover {
2651
+ text-decoration: underline;
2652
+ }
2653
+
2654
+ /* Responsive: Stack columns on mobile */
2655
+ @media (max-width: 640px) {
2656
+ .sw-columns {
2657
+ flex-direction: column;
2658
+ }
2659
+
2660
+ .sw-column {
2661
+ flex: none !important;
2662
+ width: 100% !important;
2663
+ }
2664
+ }
2665
+
2666
+ /* ===========================================
2667
+ Utilities
2668
+ =========================================== */
2669
+ .todo-item {
2670
+ padding: var(--sw-spacing-sm);
2671
+ margin: var(--sw-spacing-xs) 0;
2672
+ background: var(--sw-color-bg);
2673
+ border-radius: var(--sw-radius-sm);
2674
+ display: flex;
2675
+ justify-content: space-between;
2676
+ align-items: center;
2677
+ }
2678
+
2679
+ /* Alpine.js cloak */
2680
+ [x-cloak] {
2681
+ display: none !important;
2682
+ }
2683
+
2684
+ /* ===========================================
2685
+ Hover Effects (for div hover_class)
2686
+ =========================================== */
2687
+ .sw-hover-highlight {
2688
+ background: var(--sw-color-primary) !important;
2689
+ color: white !important;
2690
+ border-color: var(--sw-color-primary) !important;
2691
+ }
2692
+
2693
+ .sw-hover-lift {
2694
+ transform: translateY(-4px);
2695
+ box-shadow: var(--sw-shadow-lg);
2696
+ }
2697
+
2698
+ .sw-hover-glow {
2699
+ box-shadow: 0 0 20px var(--sw-color-primary);
2700
+ border-color: var(--sw-color-primary);
2701
+ }
2702
+
2703
+ /* Demo/example box for hover demonstrations */
2704
+ .hover-demo-box {
2705
+ padding: 2rem;
2706
+ border: 2px solid var(--sw-color-border);
2707
+ border-radius: var(--sw-radius-md);
2708
+ text-align: center;
2709
+ transition: all 0.2s ease;
2710
+ background: var(--sw-color-bg-card);
2711
+ }
2712
+
2713
+ /* Mobile touch support */
2714
+ @media (hover: none) {
2715
+ .sw-term, .term {
2716
+ cursor: pointer;
2717
+ }
2718
+ }
2719
+
2720
+ /* ===========================================
2721
+ Animations - Subtle, Non-distracting
2722
+ =========================================== */
2723
+ @keyframes fadeIn {
2724
+ from { opacity: 0; }
2725
+ to { opacity: 1; }
2726
+ }
2727
+
2728
+ /* Gentle page load */
2729
+ #app-container {
2730
+ animation: fadeIn 0.3s ease-out;
2731
+ }
2732
+
2733
+ /* Reduce motion for accessibility */
2734
+ @media (prefers-reduced-motion: reduce) {
2735
+ *, *::before, *::after {
2736
+ animation-duration: 0.01ms !important;
2737
+ animation-iteration-count: 1 !important;
2738
+ transition-duration: 0.01ms !important;
2739
+ }
2740
+ }
2741
+
2742
+ /* ===========================================
2743
+ Code Editor (CodeMirror 5)
2744
+ =========================================== */
2745
+ .sw-code-editor-wrapper {
2746
+ border: 1px solid var(--sw-color-border);
2747
+ border-radius: var(--sw-radius-md);
2748
+ overflow: hidden;
2749
+ }
2750
+
2751
+ .sw-code-editor-wrapper .CodeMirror {
2752
+ height: 100%;
2753
+ font-family: 'Monaco', 'Menlo', 'Consolas', 'Liberation Mono', monospace;
2754
+ font-size: 14px;
2755
+ line-height: 1.5;
2756
+ }
2757
+
2758
+ .sw-code-editor-wrapper .CodeMirror-gutters {
2759
+ background: var(--sw-color-bg);
2760
+ border-right: 1px solid var(--sw-color-border);
2761
+ }
2762
+
2763
+ .sw-code-editor-wrapper .CodeMirror-linenumber {
2764
+ color: var(--sw-color-text-muted);
2765
+ }
2766
+
2767
+ /* ===========================================
2768
+ Embedded Mode - Disable standalone styles
2769
+ Add class="sw-embedded" to body to use minimal styles
2770
+ =========================================== */
2771
+ body.sw-embedded {
2772
+ max-width: none;
2773
+ margin: 0;
2774
+ padding: 0;
2775
+ background: transparent;
2776
+ }
2777
+
2778
+ body.sw-embedded #app-container {
2779
+ background: transparent;
2780
+ padding: 0;
2781
+ border-radius: 0;
2782
+ box-shadow: none;
2783
+ }
2784
+
2785
+ /* ===========================================
2786
+ Dashboard Components (Cabinet Control style)
2787
+ =========================================== */
2788
+
2789
+ /* Status Dot - Colored indicator with optional glow */
2790
+ .sw-status-dot {
2791
+ display: inline-block;
2792
+ border-radius: 50%;
2793
+ flex-shrink: 0;
2794
+ }
2795
+
2796
+ .sw-status-dot-sm { width: 6px; height: 6px; }
2797
+ .sw-status-dot-md { width: 10px; height: 10px; }
2798
+ .sw-status-dot-lg { width: 14px; height: 14px; }
2799
+
2800
+ .sw-status-dot-red {
2801
+ background: var(--destructive);
2802
+ box-shadow: 0 0 8px color-mix(in oklch, var(--destructive) 40%, transparent);
2803
+ }
2804
+ .sw-status-dot-yellow {
2805
+ background: var(--warning);
2806
+ box-shadow: 0 0 8px color-mix(in oklch, var(--warning) 40%, transparent);
2807
+ }
2808
+ .sw-status-dot-green {
2809
+ background: var(--success);
2810
+ box-shadow: 0 0 8px color-mix(in oklch, var(--success) 40%, transparent);
2811
+ }
2812
+ .sw-status-dot-gray {
2813
+ background: var(--muted-foreground);
2814
+ opacity: 0.6;
2815
+ }
2816
+
2817
+ .sw-status-dot-pulse {
2818
+ animation: sw-pulse 2s ease-in-out infinite;
2819
+ }
2820
+
2821
+ @keyframes sw-pulse {
2822
+ 0%, 100% { opacity: 1; transform: scale(1); }
2823
+ 50% { opacity: 0.7; transform: scale(1.1); }
2824
+ }
2825
+
2826
+ /* Badge - Count/label pills */
2827
+ .sw-badge {
2828
+ display: inline-flex;
2829
+ align-items: center;
2830
+ justify-content: center;
2831
+ font-family: 'JetBrains Mono', 'Monaco', 'Consolas', monospace;
2832
+ font-weight: 600;
2833
+ border-radius: 10px;
2834
+ white-space: nowrap;
2835
+ }
2836
+
2837
+ .sw-badge-sm { font-size: 0.65rem; padding: 0.15rem 0.4rem; }
2838
+ .sw-badge-md { font-size: 0.75rem; padding: 0.2rem 0.5rem; }
2839
+
2840
+ .sw-badge-default {
2841
+ background: var(--muted);
2842
+ color: var(--muted-foreground);
2843
+ }
2844
+ .sw-badge-danger {
2845
+ background: var(--destructive);
2846
+ color: var(--destructive-foreground);
2847
+ }
2848
+ .sw-badge-warning {
2849
+ background: var(--warning);
2850
+ color: var(--warning-foreground);
2851
+ }
2852
+ .sw-badge-success {
2853
+ background: var(--success);
2854
+ color: var(--success-foreground);
2855
+ }
2856
+ .sw-badge-info {
2857
+ background: var(--info);
2858
+ color: var(--info-foreground);
2859
+ }
2860
+
2861
+ /* Stat Display - Large number with label */
2862
+ .sw-stat {
2863
+ text-align: center;
2864
+ }
2865
+
2866
+ .sw-stat-sm .sw-stat-value { font-size: 1.25rem; }
2867
+ .sw-stat-md .sw-stat-value { font-size: 1.5rem; }
2868
+ .sw-stat-lg .sw-stat-value { font-size: 2rem; }
2869
+
2870
+ .sw-stat-value {
2871
+ font-family: 'JetBrains Mono', 'Monaco', 'Consolas', monospace;
2872
+ font-weight: 600;
2873
+ line-height: 1.2;
2874
+ }
2875
+
2876
+ .sw-stat-value-default { color: var(--foreground); }
2877
+ .sw-stat-value-blue { color: #58a6ff; }
2878
+ .sw-stat-value-purple { color: #a371f7; }
2879
+ .sw-stat-value-green { color: #3fb950; }
2880
+ .sw-stat-value-red { color: #f85149; }
2881
+ .sw-stat-value-yellow { color: #d29922; }
2882
+
2883
+ .sw-stat-label {
2884
+ font-size: 0.65rem;
2885
+ text-transform: uppercase;
2886
+ letter-spacing: 0.05em;
2887
+ color: var(--muted-foreground);
2888
+ margin-top: 0.25rem;
2889
+ }
2890
+
2891
+ /* Type Tag - Activity type badges */
2892
+ .sw-type-tag {
2893
+ display: inline-block;
2894
+ font-size: 0.65rem;
2895
+ text-transform: uppercase;
2896
+ letter-spacing: 0.05em;
2897
+ padding: 0.2rem 0.5rem;
2898
+ border-radius: 4px;
2899
+ font-weight: 500;
2900
+ white-space: nowrap;
2901
+ }
2902
+
2903
+ .sw-type-tag-blue {
2904
+ background: rgba(88, 166, 255, 0.15);
2905
+ color: #58a6ff;
2906
+ }
2907
+ .sw-type-tag-green {
2908
+ background: rgba(63, 185, 80, 0.15);
2909
+ color: #3fb950;
2910
+ }
2911
+ .sw-type-tag-red {
2912
+ background: rgba(248, 81, 73, 0.15);
2913
+ color: #f85149;
2914
+ }
2915
+ .sw-type-tag-purple {
2916
+ background: rgba(163, 113, 247, 0.15);
2917
+ color: #a371f7;
2918
+ }
2919
+ .sw-type-tag-yellow {
2920
+ background: rgba(210, 153, 34, 0.15);
2921
+ color: #d29922;
2922
+ }
2923
+ .sw-type-tag-gray {
2924
+ background: rgba(139, 148, 158, 0.15);
2925
+ color: #8b949e;
2926
+ }
2927
+
2928
+ /* Pulse Indicator - Animated status with label */
2929
+ .sw-pulse-indicator {
2930
+ display: inline-flex;
2931
+ align-items: center;
2932
+ gap: 0.5rem;
2933
+ font-family: 'JetBrains Mono', 'Monaco', 'Consolas', monospace;
2934
+ font-size: 0.8rem;
2935
+ color: var(--muted-foreground);
2936
+ }
2937
+
2938
+ .sw-pulse-dot {
2939
+ width: 8px;
2940
+ height: 8px;
2941
+ border-radius: 50%;
2942
+ animation: sw-pulse 2s ease-in-out infinite;
2943
+ }
2944
+
2945
+ .sw-pulse-dot-green { background: #3fb950; }
2946
+ .sw-pulse-dot-red { background: #f85149; }
2947
+ .sw-pulse-dot-yellow { background: #d29922; }
2948
+ .sw-pulse-dot-blue { background: #58a6ff; }
2949
+
2950
+ .sw-pulse-label {
2951
+ color: var(--muted-foreground);
2952
+ }
2953
+
2954
+ /* Priority Item - Escalation-style with colored border */
2955
+ .sw-priority-item {
2956
+ background: var(--sw-color-bg-elevated, #f5f5f5);
2957
+ border-radius: 8px;
2958
+ padding: 1rem;
2959
+ border-left: 3px solid;
2960
+ cursor: pointer;
2961
+ transition: all 0.2s ease;
2962
+ }
2963
+
2964
+ .sw-priority-item:hover {
2965
+ transform: translateX(4px);
2966
+ }
2967
+
2968
+ .sw-priority-critical { border-left-color: #f85149; }
2969
+ .sw-priority-urgent { border-left-color: #d29922; }
2970
+ .sw-priority-high { border-left-color: #58a6ff; }
2971
+ .sw-priority-normal { border-left-color: #3fb950; }
2972
+ .sw-priority-low { border-left-color: #8b949e; }
2973
+
2974
+ .sw-priority-label {
2975
+ font-size: 0.6rem;
2976
+ text-transform: uppercase;
2977
+ letter-spacing: 0.08em;
2978
+ font-weight: 600;
2979
+ margin-bottom: 0.5rem;
2980
+ }
2981
+
2982
+ .sw-priority-critical .sw-priority-label { color: #f85149; }
2983
+ .sw-priority-urgent .sw-priority-label { color: #d29922; }
2984
+ .sw-priority-high .sw-priority-label { color: #58a6ff; }
2985
+ .sw-priority-normal .sw-priority-label { color: #3fb950; }
2986
+ .sw-priority-low .sw-priority-label { color: #8b949e; }
2987
+
2988
+ .sw-priority-title {
2989
+ font-size: 0.85rem;
2990
+ font-weight: 500;
2991
+ margin: 0 0 0.35rem 0;
2992
+ color: var(--sw-color-text, #111);
2993
+ }
2994
+
2995
+ .sw-priority-description {
2996
+ font-size: 0.75rem;
2997
+ color: var(--sw-color-text-muted, #666);
2998
+ line-height: 1.4;
2999
+ margin: 0 0 0.5rem 0;
3000
+ }
3001
+
3002
+ .sw-priority-meta {
3003
+ display: flex;
3004
+ align-items: center;
3005
+ justify-content: space-between;
3006
+ font-size: 0.7rem;
3007
+ color: var(--sw-color-text-muted, #888);
3008
+ }
3009
+
3010
+ .sw-priority-meta-right {
3011
+ color: #58a6ff;
3012
+ font-weight: 500;
3013
+ }
3014
+
3015
+ /* Activity Item - Time + title + summary + type */
3016
+ .sw-activity-item {
3017
+ padding: 1rem;
3018
+ display: grid;
3019
+ grid-template-columns: auto 1fr auto;
3020
+ gap: 1rem;
3021
+ align-items: start;
3022
+ border-bottom: 1px solid var(--sw-color-border, #e0e0e0);
3023
+ transition: background 0.15s ease;
3024
+ }
3025
+
3026
+ .sw-activity-item:hover {
3027
+ background: var(--sw-color-bg-elevated, #f5f5f5);
3028
+ }
3029
+
3030
+ .sw-activity-item:last-child {
3031
+ border-bottom: none;
3032
+ }
3033
+
3034
+ .sw-activity-time {
3035
+ font-family: 'JetBrains Mono', 'Monaco', 'Consolas', monospace;
3036
+ font-size: 0.7rem;
3037
+ color: var(--sw-color-text-muted, #888);
3038
+ white-space: nowrap;
3039
+ padding-top: 0.15rem;
3040
+ }
3041
+
3042
+ .sw-activity-content {
3043
+ min-width: 0;
3044
+ }
3045
+
3046
+ .sw-activity-title {
3047
+ font-size: 0.9rem;
3048
+ font-weight: 500;
3049
+ margin: 0 0 0.25rem 0;
3050
+ color: var(--sw-color-text, #111);
3051
+ }
3052
+
3053
+ .sw-activity-summary {
3054
+ font-size: 0.8rem;
3055
+ color: var(--sw-color-text-muted, #666);
3056
+ line-height: 1.4;
3057
+ margin: 0;
3058
+ }
3059
+
3060
+ /* ===========================================
3061
+ Timeline Event (Run Viewer style)
3062
+ =========================================== */
3063
+
3064
+ .sw-timeline-event {
3065
+ padding: 6px 10px;
3066
+ border-radius: 4px;
3067
+ cursor: pointer;
3068
+ display: flex;
3069
+ flex-wrap: wrap;
3070
+ align-items: center;
3071
+ gap: 8px;
3072
+ border-left: 3px solid transparent;
3073
+ font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
3074
+ font-size: 13px;
3075
+ }
3076
+
3077
+ .sw-timeline-event:hover {
3078
+ background: var(--sw-color-bg-hover, #2a2a4a);
3079
+ }
3080
+
3081
+ .sw-timeline-event__idx {
3082
+ color: #555;
3083
+ width: 24px;
3084
+ text-align: right;
3085
+ flex-shrink: 0;
3086
+ }
3087
+
3088
+ .sw-timeline-event__badge {
3089
+ display: inline-block;
3090
+ width: 100px;
3091
+ padding: 1px 6px;
3092
+ border-radius: 3px;
3093
+ text-align: center;
3094
+ font-size: 11px;
3095
+ flex-shrink: 0;
3096
+ }
3097
+
3098
+ .sw-timeline-event__ts {
3099
+ color: #666;
3100
+ flex-shrink: 0;
3101
+ }
3102
+
3103
+ .sw-timeline-event__label {
3104
+ color: #ccc;
3105
+ }
3106
+
3107
+ .sw-timeline-event__detail {
3108
+ width: 100%;
3109
+ padding: 10px 0 6px 36px;
3110
+ }
3111
+
3112
+ .sw-timeline-event__field {
3113
+ margin-bottom: 4px;
3114
+ }
3115
+
3116
+ .sw-timeline-event__field-key {
3117
+ color: #aaa;
3118
+ }
3119
+
3120
+ .sw-timeline-event__field-value pre {
3121
+ margin: 4px 0 4px 12px;
3122
+ padding: 8px;
3123
+ background: #0d0d1a;
3124
+ border-radius: 4px;
3125
+ overflow-x: auto;
3126
+ white-space: pre-wrap;
3127
+ }
3128
+
3129
+ /* Type: phase — cyan */
3130
+ .sw-timeline-event--phase {
3131
+ border-left-color: #00bcd4;
3132
+ }
3133
+ .sw-timeline-event--phase .sw-timeline-event__badge {
3134
+ background: #0e3a3f;
3135
+ color: #00bcd4;
3136
+ }
3137
+
3138
+ /* Type: snapshot — yellow */
3139
+ .sw-timeline-event--snapshot {
3140
+ border-left-color: #ffc107;
3141
+ }
3142
+ .sw-timeline-event--snapshot .sw-timeline-event__badge {
3143
+ background: #3f3500;
3144
+ color: #ffc107;
3145
+ }
3146
+
3147
+ /* Type: intervention — purple */
3148
+ .sw-timeline-event--intervention {
3149
+ border-left-color: #ce93d8;
3150
+ }
3151
+ .sw-timeline-event--intervention .sw-timeline-event__badge {
3152
+ background: #3a1a44;
3153
+ color: #ce93d8;
3154
+ }
3155
+
3156
+ /* Type: timeout — red */
3157
+ .sw-timeline-event--timeout {
3158
+ border-left-color: #ef5350;
3159
+ }
3160
+ .sw-timeline-event--timeout .sw-timeline-event__badge {
3161
+ background: #3f0e0e;
3162
+ color: #ef5350;
3163
+ }
3164
+
3165
+ /* Type: guard — red */
3166
+ .sw-timeline-event--guard {
3167
+ border-left-color: #ef5350;
3168
+ }
3169
+ .sw-timeline-event--guard .sw-timeline-event__badge {
3170
+ background: #3f0e0e;
3171
+ color: #ef5350;
3172
+ }
3173
+
3174
+ /* Type: final — green */
3175
+ .sw-timeline-event--final {
3176
+ border-left-color: #66bb6a;
3177
+ }
3178
+ .sw-timeline-event--final .sw-timeline-event__badge {
3179
+ background: #0e3f12;
3180
+ color: #66bb6a;
3181
+ }
3182
+
3183
+ /* ===========================================
3184
+ Layout Components (Cabinet Control style)
3185
+ =========================================== */
3186
+
3187
+ /* App Shell - Two-column layout */
3188
+ .sw-app-shell {
3189
+ display: grid;
3190
+ gap: var(--sw-shell-gap, 1.5rem);
3191
+ min-height: 100vh;
3192
+ }
3193
+
3194
+ .sw-app-shell-sidebar-right {
3195
+ grid-template-columns: 1fr var(--sw-shell-sidebar-width, 320px);
3196
+ }
3197
+
3198
+ .sw-app-shell-sidebar-left {
3199
+ grid-template-columns: var(--sw-shell-sidebar-width, 320px) 1fr;
3200
+ }
3201
+
3202
+ .sw-app-shell-sidebar-left .sw-app-shell-sidebar {
3203
+ order: -1;
3204
+ }
3205
+
3206
+ .sw-app-shell-main {
3207
+ min-width: 0;
3208
+ }
3209
+
3210
+ .sw-app-shell-sidebar {
3211
+ min-width: 0;
3212
+ }
3213
+
3214
+ /* Sidebar */
3215
+ .sw-sidebar {
3216
+ display: flex;
3217
+ flex-direction: column;
3218
+ gap: var(--sw-spacing-md, 1rem);
3219
+ }
3220
+
3221
+ .sw-sidebar-sticky {
3222
+ position: sticky;
3223
+ top: var(--sw-spacing-lg, 1.5rem);
3224
+ max-height: calc(100vh - var(--sw-spacing-xl, 3rem));
3225
+ overflow-y: auto;
3226
+ }
3227
+
3228
+ .sw-sidebar-header {
3229
+ display: flex;
3230
+ align-items: center;
3231
+ justify-content: space-between;
3232
+ margin-bottom: var(--sw-spacing-sm, 0.5rem);
3233
+ }
3234
+
3235
+ .sw-sidebar-title {
3236
+ font-size: 0.85rem;
3237
+ font-weight: 600;
3238
+ text-transform: uppercase;
3239
+ letter-spacing: 0.05em;
3240
+ color: var(--muted-foreground);
3241
+ margin: 0;
3242
+ }
3243
+
3244
+ .sw-sidebar-content {
3245
+ display: flex;
3246
+ flex-direction: column;
3247
+ gap: var(--sw-spacing-sm, 0.5rem);
3248
+ }
3249
+
3250
+ /* Main Content */
3251
+ .sw-main-content {
3252
+ display: flex;
3253
+ flex-direction: column;
3254
+ gap: var(--sw-spacing-md, 1rem);
3255
+ }
3256
+
3257
+ /* Expandable Card */
3258
+ .sw-expandable-card {
3259
+ background: var(--sw-color-bg-elevated, #f5f5f5);
3260
+ border: 1px solid var(--sw-color-border, #e0e0e0);
3261
+ border-radius: var(--sw-radius-lg, 10px);
3262
+ overflow: hidden;
3263
+ transition: box-shadow 0.2s ease, transform 0.2s ease;
3264
+ }
3265
+
3266
+ .sw-expandable-card:hover {
3267
+ box-shadow: var(--sw-shadow-md, 0 4px 8px rgba(0,0,0,0.1));
3268
+ transform: translateY(-2px);
3269
+ }
3270
+
3271
+ .sw-expandable-card-header {
3272
+ display: flex;
3273
+ align-items: center;
3274
+ gap: 0.75rem;
3275
+ padding: 1rem 1.25rem;
3276
+ cursor: pointer;
3277
+ user-select: none;
3278
+ }
3279
+
3280
+ .sw-expandable-card-titles {
3281
+ flex: 1;
3282
+ min-width: 0;
3283
+ }
3284
+
3285
+ .sw-expandable-card-title {
3286
+ font-size: 1rem;
3287
+ font-weight: 600;
3288
+ margin: 0;
3289
+ color: var(--sw-color-text, #111);
3290
+ }
3291
+
3292
+ .sw-expandable-card-subtitle {
3293
+ font-size: 0.8rem;
3294
+ color: var(--sw-color-text-muted, #666);
3295
+ margin: 0.25rem 0 0 0;
3296
+ }
3297
+
3298
+ .sw-expandable-card-chevron {
3299
+ font-size: 0.7rem;
3300
+ color: var(--sw-color-text-muted, #888);
3301
+ transition: transform 0.2s ease;
3302
+ }
3303
+
3304
+ .sw-expandable-card-body {
3305
+ padding: 0 1.25rem 1.25rem 1.25rem;
3306
+ border-top: 1px solid var(--sw-color-border, #e0e0e0);
3307
+ }
3308
+
3309
+ /* Transition classes for Alpine.js */
3310
+ .sw-transition-enter {
3311
+ transition: all 0.2s ease-out;
3312
+ }
3313
+ .sw-transition-enter-start {
3314
+ opacity: 0;
3315
+ transform: translateY(-0.5rem);
3316
+ }
3317
+ .sw-transition-enter-end {
3318
+ opacity: 1;
3319
+ transform: translateY(0);
3320
+ }
3321
+ .sw-transition-leave {
3322
+ transition: all 0.15s ease-in;
3323
+ }
3324
+ .sw-transition-leave-start {
3325
+ opacity: 1;
3326
+ }
3327
+ .sw-transition-leave-end {
3328
+ opacity: 0;
3329
+ }
3330
+
3331
+ /* Responsive: Stack on smaller screens */
3332
+ @media (max-width: 900px) {
3333
+ .sw-app-shell {
3334
+ grid-template-columns: 1fr !important;
3335
+ }
3336
+
3337
+ .sw-app-shell-sidebar {
3338
+ order: 0 !important;
3339
+ }
3340
+
3341
+ .sw-sidebar-sticky {
3342
+ position: static;
3343
+ max-height: none;
3344
+ }
3345
+ }
3346
+
3347
+ /* ===========================================
3348
+ Mobile Responsive
3349
+ =========================================== */
3350
+
3351
+ /* Tablet */
3352
+ @media (max-width: 900px) {
3353
+ body {
3354
+ padding: var(--sw-spacing-sm);
3355
+ }
3356
+
3357
+ h1 { font-size: 1.75rem; }
3358
+ h2 { font-size: 1.35rem; }
3359
+ h3 { font-size: 1.15rem; }
3360
+ }
3361
+
3362
+ /* Phone */
3363
+ @media (max-width: 640px) {
3364
+ body {
3365
+ padding: var(--sw-spacing-xs);
3366
+ }
3367
+
3368
+ .card {
3369
+ border-radius: 0;
3370
+ padding: var(--sw-spacing-md);
3371
+ }
3372
+
3373
+ .sw-scroll-box {
3374
+ padding: var(--sw-spacing-sm);
3375
+ }
3376
+
3377
+ h1 { font-size: 1.5rem; }
3378
+ h2 { font-size: 1.25rem; }
3379
+ h3 { font-size: 1.1rem; }
3380
+
3381
+ /* Tabs: horizontal scroll */
3382
+ .sw-tabs-list {
3383
+ overflow-x: auto;
3384
+ -webkit-overflow-scrolling: touch;
3385
+ scrollbar-width: none;
3386
+ flex-wrap: nowrap;
3387
+ }
3388
+ .sw-tabs-list::-webkit-scrollbar {
3389
+ display: none;
3390
+ }
3391
+ .sw-tab-trigger {
3392
+ white-space: nowrap;
3393
+ flex-shrink: 0;
3394
+ padding: var(--sw-spacing-xs) var(--sw-spacing-sm);
3395
+ font-size: var(--sw-font-size-sm);
3396
+ min-height: 44px;
3397
+ }
3398
+
3399
+ /* HStacks: tighter gap */
3400
+ .sw-hstack {
3401
+ gap: var(--sw-spacing-xs);
3402
+ }
3403
+
3404
+ /* Grid: single column override */
3405
+ .sw-grid {
3406
+ grid-template-columns: 1fr !important;
3407
+ }
3408
+
3409
+ /* Forms: prevent iOS auto-zoom, 44px touch targets */
3410
+ input[type="text"],
3411
+ input[type="email"],
3412
+ select,
3413
+ textarea {
3414
+ font-size: 16px;
3415
+ min-height: 44px;
3416
+ }
3417
+ button, .btn {
3418
+ min-height: 44px;
3419
+ }
3420
+
3421
+ /* Modals: full-screen on phones */
3422
+ .sw-modal {
3423
+ width: 100vw !important;
3424
+ max-width: 100vw !important;
3425
+ height: 100vh;
3426
+ max-height: 100vh;
3427
+ border-radius: 0;
3428
+ margin: 0;
3429
+ }
3430
+
3431
+ /* Toasts: full-width at top */
3432
+ .sw-toast-container {
3433
+ left: 0 !important;
3434
+ right: 0 !important;
3435
+ top: 0 !important;
3436
+ bottom: auto !important;
3437
+ width: 100%;
3438
+ padding: var(--sw-spacing-xs);
3439
+ }
3440
+ .sw-toast {
3441
+ border-radius: 0;
3442
+ }
3443
+
3444
+ /* Tables: horizontal scroll */
3445
+ .score-table {
3446
+ display: block;
3447
+ overflow-x: auto;
3448
+ -webkit-overflow-scrolling: touch;
3449
+ }
3450
+ }
3451
+
3452
+ /* ===========================================
3453
+ Dark Theme (Cabinet Control style)
3454
+ =========================================== */
3455
+ body.sw-theme-dark {
3456
+ --sw-font-display: 'Outfit', 'Source Sans 3', system-ui, sans-serif;
3457
+ --sw-font-body: 'Outfit', 'Source Sans 3', system-ui, sans-serif;
3458
+ --sw-font-family: var(--sw-font-body);
3459
+ --sw-font-mono: 'JetBrains Mono', 'Monaco', 'Consolas', monospace;
3460
+ --sw-font-size-base: 15px;
3461
+ --sw-font-size-sm: 13px;
3462
+ --sw-font-size-lg: 17px;
3463
+ --sw-font-size-xl: 21px;
3464
+ --sw-line-height: 1.5;
3465
+
3466
+ /* Dark color palette */
3467
+ --sw-color-bg-deep: #0a0e14;
3468
+ --sw-color-bg: #0a0e14;
3469
+ --sw-color-bg-card: #131820;
3470
+ --sw-color-bg-elevated: #1a2029;
3471
+ --sw-color-bg-hover: #242d3a;
3472
+ --sw-color-border: #2a3544;
3473
+ --sw-color-text: #e6edf3;
3474
+ --sw-color-text-muted: #8b949e;
3475
+ --sw-color-text-light: #565d66;
3476
+
3477
+ /* Accent colors */
3478
+ --sw-color-primary: #58a6ff;
3479
+ --sw-color-primary-hover: #79b8ff;
3480
+ --sw-color-primary-light: rgba(88, 166, 255, 0.1);
3481
+ --sw-color-primary-glow: rgba(88, 166, 255, 0.2);
3482
+ --sw-color-accent: #a371f7;
3483
+ --sw-color-accent-light: rgba(163, 113, 247, 0.1);
3484
+
3485
+ /* Status colors */
3486
+ --sw-color-status-red: #f85149;
3487
+ --sw-color-status-yellow: #d29922;
3488
+ --sw-color-status-green: #3fb950;
3489
+
3490
+ /* Secondary */
3491
+ --sw-color-secondary: #8b949e;
3492
+ --sw-color-secondary-hover: #adbac7;
3493
+ --sw-color-secondary-foreground: #0a0e14;
3494
+
3495
+ /* Spacing */
3496
+ --sw-spacing-xs: 0.375rem;
3497
+ --sw-spacing-sm: 0.5rem;
3498
+ --sw-spacing-md: 0.875rem;
3499
+ --sw-spacing-lg: 1.25rem;
3500
+ --sw-spacing-xl: 1.75rem;
3501
+ --sw-spacing-2xl: 2.5rem;
3502
+
3503
+ /* Border radius */
3504
+ --sw-radius-sm: 4px;
3505
+ --sw-radius-md: 6px;
3506
+ --sw-radius-lg: 10px;
3507
+ --sw-radius-xl: 12px;
3508
+
3509
+ /* Shadows */
3510
+ --sw-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
3511
+ --sw-shadow-md: 0 4px 8px rgba(0, 0, 0, 0.4);
3512
+ --sw-shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
3513
+ --sw-shadow-xl: 0 12px 32px rgba(0, 0, 0, 0.6);
3514
+ --sw-shadow-inner: inset 0 1px 2px rgba(0, 0, 0, 0.2);
3515
+
3516
+ /* Card styling */
3517
+ --sw-card-border-left: 3px solid var(--sw-color-accent);
3518
+
3519
+ /* Term highlighting */
3520
+ --sw-term-color: var(--sw-color-primary);
3521
+ --sw-term-bg-hover: var(--sw-color-primary-light);
3522
+ }
3523
+
3524
+ /* Dark theme base styles */
3525
+ body.sw-theme-dark {
3526
+ background: var(--sw-color-bg-deep);
3527
+ color: var(--sw-color-text);
3528
+ }
3529
+
3530
+ body.sw-theme-dark[class*="sw-layout-"] > #app-container {
3531
+ background: var(--sw-color-bg-card);
3532
+ border-color: var(--sw-color-border);
3533
+ }
3534
+
3535
+ body.sw-theme-dark h1,
3536
+ body.sw-theme-dark h2,
3537
+ body.sw-theme-dark h3,
3538
+ body.sw-theme-dark h4 {
3539
+ color: var(--sw-color-text);
3540
+ }
3541
+
3542
+ body.sw-theme-dark h2 {
3543
+ border-bottom-color: var(--sw-color-border);
3544
+ }
3545
+
3546
+ body.sw-theme-dark input[type="text"],
3547
+ body.sw-theme-dark input[type="email"],
3548
+ body.sw-theme-dark select,
3549
+ body.sw-theme-dark textarea {
3550
+ background: var(--sw-color-bg-elevated);
3551
+ border-color: var(--sw-color-border);
3552
+ color: var(--sw-color-text);
3553
+ }
3554
+
3555
+ body.sw-theme-dark input[type="text"]:focus,
3556
+ body.sw-theme-dark input[type="email"]:focus,
3557
+ body.sw-theme-dark select:focus,
3558
+ body.sw-theme-dark textarea:focus {
3559
+ background: var(--sw-color-bg-hover);
3560
+ border-color: var(--sw-color-primary);
3561
+ }
3562
+
3563
+ body.sw-theme-dark .btn-primary {
3564
+ background: var(--sw-color-primary);
3565
+ color: white;
3566
+ }
3567
+
3568
+ body.sw-theme-dark .btn-secondary {
3569
+ background: var(--sw-color-bg-elevated);
3570
+ color: var(--sw-color-text);
3571
+ border-color: var(--sw-color-border);
3572
+ }
3573
+
3574
+ /* Dark theme dashboard components */
3575
+ body.sw-theme-dark .sw-badge-default {
3576
+ background: var(--sw-color-bg-hover);
3577
+ color: var(--sw-color-text-muted);
3578
+ }
3579
+
3580
+ body.sw-theme-dark .sw-priority-item {
3581
+ background: var(--sw-color-bg-elevated);
3582
+ }
3583
+
3584
+ body.sw-theme-dark .sw-priority-item:hover {
3585
+ background: var(--sw-color-bg-hover);
3586
+ }
3587
+
3588
+ body.sw-theme-dark .sw-activity-item:hover {
3589
+ background: var(--sw-color-bg-hover);
3590
+ }
3591
+
3592
+ body.sw-theme-dark .sw-activity-item {
3593
+ border-bottom-color: var(--sw-color-border);
3594
+ }
3595
+
3596
+ /* Dark theme layout components */
3597
+ body.sw-theme-dark .sw-expandable-card {
3598
+ background: var(--sw-color-bg-elevated);
3599
+ border-color: var(--sw-color-border);
3600
+ }
3601
+
3602
+ body.sw-theme-dark .sw-expandable-card:hover {
3603
+ background: var(--sw-color-bg-hover);
3604
+ }
3605
+
3606
+ body.sw-theme-dark .sw-expandable-card-body {
3607
+ border-top-color: var(--sw-color-border);
3608
+ }
3609
+
3610
+ /* Expandable card highlight (e.g. current timeline slot) */
3611
+ .sw-expandable-card--highlight {
3612
+ border-left: 4px solid var(--sw-color-primary, #3b82f6);
3613
+ background: color-mix(in srgb, var(--sw-color-primary, #3b82f6) 5%, var(--sw-color-bg-elevated, #f5f5f5));
3614
+ }
3615
+
3616
+ body.sw-theme-dark .sw-expandable-card--highlight {
3617
+ background: color-mix(in srgb, var(--sw-color-primary, #3b82f6) 8%, var(--sw-color-bg-elevated));
3618
+ }
3619
+ CSS
3620
+ end
3621
+
3622
+ private
3623
+
3624
+ # Render all components
3625
+ def render_components
3626
+ @app.components.each do |component|
3627
+ component.render(self, @state)
3628
+ end
3629
+
3630
+ # Add submit button for agentic mode
3631
+ render_agentic_submit_button if @is_agentic
3632
+ end
3633
+
3634
+ # Render the submit button for agentic mode
3635
+ def render_agentic_submit_button
3636
+ div(style: "margin-top: 30px; padding-top: 20px; border-top: 2px solid #e0e0e0;") do
3637
+ p(style: "color: #666; font-size: 14px;") { "Submit this form to return data to the calling agent:" }
3638
+ button(
3639
+ type: "button",
3640
+ class: "btn btn-primary",
3641
+ style: "background: #28a745; font-weight: bold;",
3642
+ "hx-post" => "/submit",
3643
+ "hx-include" => @adapter.input_selector
3644
+ ) { "🤖 Submit to Agent" }
3645
+ end
3646
+ end
3647
+
3648
+ # Generate body classes for layout and theme.
3649
+ # Exclusive layouts supply their own body_classes; the sw-layout-* class is omitted.
3650
+ def body_classes
3651
+ effective_theme = @session_theme || @app.theme
3652
+ theme_class = "sw-theme-#{effective_theme}"
3653
+
3654
+ layout_entry = @app.layout_entry
3655
+ if layout_entry&.dig(:exclusive)
3656
+ extra = layout_entry[:body_classes].join(" ")
3657
+ [extra, theme_class].reject(&:empty?).join(" ")
3658
+ else
3659
+ "sw-layout-#{@app.layout} #{theme_class}"
3660
+ end
3661
+ end
3662
+
3663
+ # Get the effective theme (session override or app default)
3664
+ def effective_theme
3665
+ @session_theme || @app.theme
3666
+ end
3667
+
3668
+ # Render inline CSS for theme overrides.
3669
+ #
3670
+ # Scoped to body.sw-theme-<active theme> rather than a bare `body`
3671
+ # selector: every built-in theme's own CSS uses that same
3672
+ # body.sw-theme-<name> selector (specificity 0-1-1), which always
3673
+ # beats a plain `body {}` (0-0-1) regardless of source order. Matching
3674
+ # the selector's specificity means the tie is broken by cascade order
3675
+ # instead -- this block is emitted in <body>, after the built-in
3676
+ # theme's own block in <head>, so it wins (stream_weaver-ckz; same
3677
+ # technique as the `html.dark body` dark-mode override above).
3678
+ def render_theme_overrides
3679
+ warn_unknown_theme_override_tokens
3680
+ css_vars = @app.theme_overrides.map do |key, value|
3681
+ css_var = key.to_s.tr('_', '-')
3682
+ # Add sw- prefix if not present
3683
+ css_var = "sw-#{css_var}" unless css_var.start_with?('sw-')
3684
+ "--#{css_var}: #{value};"
3685
+ end.join("\n ")
3686
+
3687
+ style do
3688
+ raw(safe(StreamWeaver::CSS.layer_wrap("body.sw-theme-#{effective_theme} { #{css_vars} }")))
3689
+ end
3690
+ end
3691
+
3692
+ def warn_unknown_theme_override_tokens
3693
+ unknown = @app.theme_overrides.keys.reject { |key| Theme::VARIABLE_SCHEMA.key?(key.to_sym) }
3694
+ return if unknown.empty?
3695
+
3696
+ warn "StreamWeaver: Unknown theme override token(s) #{unknown.map(&:inspect).join(', ')} -- " \
3697
+ "no matching entry in Theme::VARIABLE_SCHEMA, so this override is emitted as a raw --sw- custom property " \
3698
+ "that no built-in styling reads."
3699
+ end
3700
+
3701
+ # Render CSS for custom registered themes
3702
+ def render_custom_theme_css
3703
+ theme_name = effective_theme
3704
+ # Only render if it's a custom theme (not built-in)
3705
+ return if StreamWeaver::App::BUILT_IN_THEMES.include?(theme_name)
3706
+
3707
+ custom_theme = StreamWeaver.get_theme(theme_name)
3708
+ return unless custom_theme
3709
+
3710
+ style do
3711
+ raw(safe(StreamWeaver::CSS.layer_wrap(custom_theme.to_css)))
3712
+ end
3713
+ end
3714
+
3715
+ # Emit CSS/JS declared via the css/css_path/js_path class macros on component classes.
3716
+ # Deduped by component class so multiple instances only emit their assets once.
3717
+ # Also collects from layout slots and emits the layout's own css_path if registered.
3718
+ def render_component_assets
3719
+ all_components = @app.components + (@app.layout_slots || {}).values.flatten
3720
+
3721
+ # Layout-level CSS file
3722
+ layout_entry = @app.layout_entry
3723
+ if layout_entry&.dig(:css_path)
3724
+ path = layout_entry[:css_path]
3725
+ key = ComponentAssets.file_key(path)
3726
+ link(rel: "stylesheet", href: "/sw-asset/#{key}/#{File.basename(path)}")
3727
+ end
3728
+
3729
+ css_strings, css_paths, js_paths = ComponentAssets.collect(all_components)
3730
+
3731
+ css_strings.each { |css| style { raw(safe(css)) } }
3732
+ css_paths.each do |path|
3733
+ key = ComponentAssets.file_key(path)
3734
+ link(rel: "stylesheet", href: "/sw-asset/#{key}/#{File.basename(path)}")
3735
+ end
3736
+ js_paths.each do |path|
3737
+ key = ComponentAssets.file_key(path)
3738
+ script(src: "/sw-asset/#{key}/#{File.basename(path)}")
3739
+ end
3740
+ end
3741
+
3742
+ # Render all components in a named layout slot.
3743
+ # Used inside register_layout render blocks.
3744
+ def render_slot(name)
3745
+ ((@app.layout_slots || {})[name] || []).each { |c| c.render(self, @state) }
3746
+ end
3747
+
3748
+ # Emit the main reactive content region (#app-container).
3749
+ # Use this inside a register_layout render block to place the reactive app content.
3750
+ def main_content_region
3751
+ div(id: "app-container", "data-sw-state-version" => @app.render_state.state_version,
3752
+ **@adapter.container_attributes(@state)) do
3753
+ render_components
3754
+ end
3755
+ end
3756
+ end
3757
+
3758
+ # Partial view for HTMX updates (just the app-container content)
3759
+ # Includes state data for Alpine.js reinitialization after HTMX swap
3760
+ class AppContentView < Phlex::HTML
3761
+ attr_reader :adapter, :app
3762
+ attr_reader :current_fragment_id
3763
+
3764
+ # @param app [StreamWeaver::App] The app instance
3765
+ # @param state [Hash] The current state
3766
+ # @param adapter [StreamWeaver::Adapter::Base] The adapter for rendering
3767
+ # @param is_agentic [Boolean] Whether running in agentic mode
3768
+ def initialize(app, state, adapter, is_agentic = false)
3769
+ @app = app
3770
+ @state = state
3771
+ @adapter = adapter
3772
+ @is_agentic = is_agentic
3773
+ end
3774
+
3775
+ def view_template
3776
+ # Include fresh state data for Alpine.js reinitialization
3777
+ # This allows JavaScript to update the outer container's x-data after HTMX swap
3778
+ # See: Alpine.js Defer Mutations Pattern in adapter/alpinejs.rb
3779
+ state_json_data = @state.transform_keys(&:to_s)
3780
+ state_json_data["_sw_version"] = @app.render_state.state_version
3781
+ if @app.respond_to?(:transient_keys) && @app.transient_keys.any?
3782
+ state_json_data["_transient"] = @app.transient_keys.map(&:to_s)
3783
+ end
3784
+ state_json = JSON.generate(state_json_data)
3785
+ script(type: "application/json", id: "sw-state-data") { raw safe(state_json) }
3786
+
3787
+ @app.components.each do |component|
3788
+ component.render(self, @state)
3789
+ end
3790
+
3791
+ # Add submit button for agentic mode
3792
+ render_agentic_submit_button if @is_agentic
3793
+ end
3794
+
3795
+ def with_fragment(id)
3796
+ previous = @current_fragment_id
3797
+ @current_fragment_id = id
3798
+ yield
3799
+ ensure
3800
+ @current_fragment_id = previous
3801
+ end
3802
+
3803
+ private
3804
+
3805
+ # Render the submit button for agentic mode
3806
+ def render_agentic_submit_button
3807
+ div(style: "margin-top: 30px; padding-top: 20px; border-top: 2px solid #e0e0e0;") do
3808
+ p(style: "color: #666; font-size: 14px;") { "Submit this form to return data to the calling agent:" }
3809
+ button(
3810
+ type: "button",
3811
+ class: "btn btn-primary",
3812
+ style: "background: #28a745; font-weight: bold;",
3813
+ "hx-post" => "/submit",
3814
+ "hx-include" => @adapter.input_selector
3815
+ ) { "🤖 Submit to Agent" }
3816
+ end
3817
+ end
3818
+ end
3819
+
3820
+ # Shared by FragmentContentView and RowSwapView: renders one declared
3821
+ # `updates:` fragment as an OOB element. Row-granular narrowing
3822
+ # (stream_weaver-95k) applies independently per extra -- a toolbar button
3823
+ # whose own fragment has nothing to narrow can still narrow an extra
3824
+ # fragment's table (the common "Add" button outside the table, `updates:`
3825
+ # pointing at the table's fragment, is exactly this shape). Falls back to
3826
+ # the full-fragment OOB swap (pre-existing FAC-P1 behavior) whenever
3827
+ # `swap` is nil -- i.e. that extra's mutation wasn't provably row-local.
3828
+ # OOB elements always use htmx's native swap styles (plain innerHTML/
3829
+ # outerHTML), never an extension-only style like "morph:innerHTML" --
3830
+ # htmx core's own hx-swap-oob scanning doesn't dispatch through swap-style
3831
+ # extensions, so those are silently skipped in a real browser
3832
+ # (stream_weaver-e4p). Morph stays available for the PRIMARY target only.
3833
+ module ExtraFragmentRendering
3834
+ def render_extra(fragment, swap)
3835
+ return render_row_oob(swap) if swap
3836
+
3837
+ div(id: fragment.id, "hx-swap-oob" => "innerHTML") do
3838
+ with_fragment(fragment.id) { fragment.children.each { |child| child.render(self, @state) } }
3839
+ end
3840
+ end
3841
+
3842
+ def render_row_oob(swap)
3843
+ table = swap[:table]
3844
+ case swap[:kind]
3845
+ when :edit
3846
+ adapter.render_table_row(self, table.resolved_rows[swap[:idx]], swap[:idx], table.table_options, @state, table.dom_id,
3847
+ extra_attrs: { "hx-swap-oob" => "outerHTML" })
3848
+ when :delete
3849
+ tr(id: swap[:row_dom_id], "hx-swap-oob" => "delete")
3850
+ when :create
3851
+ adapter.render_table_row(self, table.resolved_rows[swap[:idx]], swap[:idx], table.table_options, @state, table.dom_id,
3852
+ extra_attrs: { "hx-swap-oob" => "beforeend:##{table.dom_id} tbody" })
3853
+ end
3854
+ end
3855
+ end
3856
+
3857
+ class FragmentContentView < AppContentView
3858
+ include ExtraFragmentRendering
3859
+
3860
+ # @param extra_swaps [Array<Hash, nil>] One row-swap analysis (or nil) per
3861
+ # entry in `updates:`, positionally aligned (stream_weaver-95k)
3862
+ def initialize(app, state, adapter, fragment, updates: [], extra_swaps: [], state_patch:)
3863
+ super(app, state, adapter, false)
3864
+ @fragment = fragment
3865
+ @updates = updates
3866
+ @extra_swaps = extra_swaps
3867
+ @state_patch = state_patch
3868
+ end
3869
+
3870
+ def view_template
3871
+ with_fragment(@fragment.id) { @fragment.children.each { |child| child.render(self, @state) } }
3872
+ @updates.each_with_index { |fragment, i| render_extra(fragment, @extra_swaps[i]) }
3873
+ # nil for a deferred fragment's auto-fetch, which claims no state
3874
+ # version (see InteractionRunner#deferred_fetch?).
3875
+ raw safe(StatePatchView.new(@state_patch).call) if @state_patch
3876
+ end
3877
+ end
3878
+
3879
+ # Row-granular counterpart to FragmentContentView (stream_weaver-95k): renders
3880
+ # just the mutated row's content (or nothing, for a delete) instead of the
3881
+ # whole fragment. The caller retargets/reswaps the *primary* content via
3882
+ # HX-Retarget/HX-Reswap response headers (InteractionRunner picks the row's
3883
+ # own id, or the table's tbody, as the target) rather than hx-swap-oob on the
3884
+ # primary content itself -- that reuses the exact header mechanism the runner
3885
+ # already has for full-view fallback, and keeps the primary swap composable
3886
+ # with htmx's normal target/swap-style handling (morph, delete, beforeend)
3887
+ # instead of hand-rolling those semantics via OOB attributes.
3888
+ # Declared `updates:` extras and the state patch still need hx-swap-oob,
3889
+ # since they are never part of the (retargeted) primary swap zone.
3890
+ class RowSwapView < AppContentView
3891
+ include ExtraFragmentRendering
3892
+
3893
+ # @param app [StreamWeaver::App] The app instance
3894
+ # @param state [Hash] The current state
3895
+ # @param adapter [StreamWeaver::Adapter::Base] The adapter for rendering
3896
+ # @param updates [Array<Components::Fragment>] Declared `updates:` fragments, rendered OOB
3897
+ # @param extra_swaps [Array<Hash, nil>] One row-swap analysis (or nil) per entry in `updates:`
3898
+ # @param state_patch [Hash] The state patch (see InteractionRunner#state_patch)
3899
+ # @param primary [Proc, nil] Renders the primary (retargeted) content into the view; nil for a delete (empty body)
3900
+ def initialize(app, state, adapter, updates:, state_patch:, extra_swaps: [], primary: nil)
3901
+ super(app, state, adapter, false)
3902
+ @updates = updates
3903
+ @extra_swaps = extra_swaps
3904
+ @state_patch = state_patch
3905
+ @primary = primary
3906
+ end
3907
+
3908
+ def view_template
3909
+ @primary&.call(self)
3910
+ @updates.each_with_index { |fragment, i| render_extra(fragment, @extra_swaps[i]) }
3911
+ raw safe(StatePatchView.new(@state_patch, true).call)
3912
+ end
3913
+ end
3914
+
3915
+ class StatePatchView < Phlex::HTML
3916
+ # `oob` is positional, not a keyword arg: existing call sites pass the
3917
+ # patch hash bare (`StatePatchView.new(set: ..., delete: ..., version: ...)`),
3918
+ # relying on Ruby folding an un-braced trailing hash into the sole
3919
+ # positional `patch` param. Declaring `oob:` as a keyword instead would
3920
+ # make Ruby try to parse that call's hash as keyword arguments (it isn't).
3921
+ def initialize(patch, oob = false)
3922
+ @patch = patch
3923
+ @oob = oob
3924
+ end
3925
+
3926
+ def view_template
3927
+ json = JSON.generate(@patch).gsub("<", "\\u003c")
3928
+ attrs = { type: "application/json", id: "sw-state-patch" }
3929
+ # RowSwapView's primary swap zone is a row/tbody, not the fragment, so
3930
+ # the patch script can't ride along inside the primary content like it
3931
+ # does for FragmentContentView -- it needs its own OOB delivery.
3932
+ # beforeend into #app-container (not an id-match OOB swap) so this
3933
+ # doesn't depend on a `#sw-state-patch` element already existing in
3934
+ # the DOM (stream_weaver-95k).
3935
+ attrs["hx-swap-oob"] = "beforeend:#app-container" if @oob
3936
+ script(**attrs) { raw safe(json) }
3937
+ end
3938
+ end
3939
+ end
3940
+ end