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