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,800 @@
|
|
|
1
|
+
<%
|
|
2
|
+
doc = @doc
|
|
3
|
+
# Browse (stream_weaver-rdh) can land here with no file open yet
|
|
4
|
+
# (GET /browse before any file is clicked) -- doc.theme/layout have
|
|
5
|
+
# nothing to fall back to in that case, so the body class falls back to
|
|
6
|
+
# the reader's own defaults instead of reading through a nil doc.
|
|
7
|
+
body_class = if doc
|
|
8
|
+
"sw-theme-#{doc.theme} sw-layout-#{doc.layout} sw-reader"
|
|
9
|
+
else
|
|
10
|
+
"sw-theme-#{StreamWeaver::Canvas::Reader.fallback_theme} " \
|
|
11
|
+
"sw-layout-#{StreamWeaver::Canvas::Reader.fallback_layout} sw-reader"
|
|
12
|
+
end
|
|
13
|
+
browse_mode = !!@browse_mode
|
|
14
|
+
# docs_groups/history_groups are meaningless in Browse mode (its sidebar
|
|
15
|
+
# replaces them entirely) -- skipping them isn't just wasted work, it
|
|
16
|
+
# avoids a NoMethodError the day canvas-read ships a "no configured
|
|
17
|
+
# files, Browse only" boot mode, where @file_list would be nil.
|
|
18
|
+
docs_groups = browse_mode ? {} : @file_list.docs_groups(repo: @repo_filter)
|
|
19
|
+
history_groups = browse_mode ? {} : @file_list.history_groups
|
|
20
|
+
# Repo filter (stream_weaver-iugu). repo_labels lists every discovered docs
|
|
21
|
+
# root; repo_qs carries the ACTIVE filter through every /?file=N link so it
|
|
22
|
+
# survives navigation -- without it, one Prev/Next click silently snaps the
|
|
23
|
+
# rail back to the host-repo default.
|
|
24
|
+
repo_labels = browse_mode ? [] : @file_list.repo_labels
|
|
25
|
+
repo_qs = @repo_param ? "&repo=#{ERB::Util.url_encode(@repo_param)}" : ''
|
|
26
|
+
is_history = @current_file && !browse_mode && @file_list.history_dir?(File.dirname(@current_file))
|
|
27
|
+
default_name = if is_history
|
|
28
|
+
session = File.basename(File.dirname(@current_file))
|
|
29
|
+
timestamp = File.basename(@current_file, '.rb')
|
|
30
|
+
"#{session}-#{timestamp}"
|
|
31
|
+
end
|
|
32
|
+
# @breadcrumbs/@browse_parent/@repo_docs_root come from the route (Reader
|
|
33
|
+
# #set_browse_ivars) -- the breadcrumb walk and repo_docs_root's
|
|
34
|
+
# filesystem/.git lookup are controller work, not view formatting.
|
|
35
|
+
browse_oob = StreamWeaver::Canvas::Reader::BROWSE_OOB
|
|
36
|
+
# Which groups get a delete control (stream_weaver-uvaj): only the two
|
|
37
|
+
# roots THIS process owns -- the repo it was launched in and the global
|
|
38
|
+
# store. A peer repo surfaced by discovery is readable here and never
|
|
39
|
+
# deletable, so its rows get no trash icon; that's the same boundary
|
|
40
|
+
# POST /delete-doc enforces server-side, mirrored into the UI so you're
|
|
41
|
+
# never offered a button that can only answer 403. Resolved once per
|
|
42
|
+
# render rather than per row -- each answer costs a realpath plus the
|
|
43
|
+
# .git walk behind repo_docs_root.
|
|
44
|
+
deletable_dirs = browse_mode ? [] : docs_groups.keys.select { |d| StreamWeaver::Canvas::Reader.deletable_dir?(d) }
|
|
45
|
+
%>
|
|
46
|
+
<!DOCTYPE html>
|
|
47
|
+
<html lang="en">
|
|
48
|
+
<head>
|
|
49
|
+
<meta charset="UTF-8">
|
|
50
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
51
|
+
<title>canvas-read</title>
|
|
52
|
+
|
|
53
|
+
<%# Framework CSS, identical ordering to the canvas (PageShell is the single
|
|
54
|
+
source of truth): layer pin, then CANVAS_CSS / master_theme_css /
|
|
55
|
+
visual_skills_css all inside @layer stream-weaver. The reader's own
|
|
56
|
+
chrome gets a later layer so it can't be outranked by, nor bleed into,
|
|
57
|
+
framework rules. %>
|
|
58
|
+
<%= StreamWeaver::PageShell.framework_css_html(extra_layers: %w[sw-reader-chrome]) %>
|
|
59
|
+
|
|
60
|
+
<%# The doc's own use_stylesheet CSS -- unlayered, so it wins everything.
|
|
61
|
+
Nothing to inline with no doc open yet (a bare Browse listing). %>
|
|
62
|
+
<%= StreamWeaver::PageShell.user_css_html(inline_stylesheets: doc ? doc.inline_stylesheets : []) %>
|
|
63
|
+
|
|
64
|
+
<%# Reader chrome. BOTH layered and #sw-reader-*-scoped, on purpose:
|
|
65
|
+
scoping alone would let CANVAS_CSS/master_theme_css's bare h1..h6/p/a
|
|
66
|
+
rules style the rail; layering alone would let an unscoped rule here
|
|
67
|
+
(e.g. `body { font-family }`) beat theme rules for the whole page.
|
|
68
|
+
The `html` padding rule below is the one deliberate exception -- it has
|
|
69
|
+
no framework equivalent to lose to. spec/canvas/reader_chrome_css_spec.rb
|
|
70
|
+
enforces this scoping; keep every new selector rooted at html or
|
|
71
|
+
#sw-reader-. %>
|
|
72
|
+
<style id="sw-reader-chrome-css">@layer sw-reader-chrome {
|
|
73
|
+
/* Offset for the fixed rail lives on `html`, NOT body: this shrinks html's
|
|
74
|
+
content box so body's own `margin: 0 auto` centering and the max-width
|
|
75
|
+
the sidebar_toc CSS sets on body both keep working untouched, with zero
|
|
76
|
+
cascade contact with views.rb's
|
|
77
|
+
`body[class*="sw-layout-"] { margin: 0 auto; padding: ... }` shorthand. */
|
|
78
|
+
html { padding-left: 220px; }
|
|
79
|
+
html.sw-rail-collapsed { padding-left: 0; }
|
|
80
|
+
|
|
81
|
+
/* Collapse is driven by `left`, never `transform`: a transformed ancestor
|
|
82
|
+
becomes the containing block for its position:fixed descendants, which
|
|
83
|
+
would trap the Save-as-doc modal (docked in #sw-reader-nav) inside the
|
|
84
|
+
220px rail instead of covering the viewport. */
|
|
85
|
+
#sw-reader-chrome {
|
|
86
|
+
position: fixed; top: 0; bottom: 0; left: 0; width: 220px; z-index: 40;
|
|
87
|
+
display: flex; flex-direction: column;
|
|
88
|
+
background: #f9fafb; border-right: 1px solid #d1d5db;
|
|
89
|
+
font-family: system-ui, sans-serif; font-size: 16px; line-height: 1.5;
|
|
90
|
+
color: #4b5563;
|
|
91
|
+
transition: left 160ms ease;
|
|
92
|
+
}
|
|
93
|
+
html.sw-rail-collapsed #sw-reader-chrome { left: -220px; }
|
|
94
|
+
|
|
95
|
+
#sw-reader-toggle {
|
|
96
|
+
position: absolute; top: 0.5rem; right: -2.25rem;
|
|
97
|
+
width: 2rem; height: 2rem; padding: 0; line-height: 1;
|
|
98
|
+
border: 1px solid #d1d5db; border-radius: 4px;
|
|
99
|
+
background: #f9fafb; color: #4b5563;
|
|
100
|
+
font-size: 0.95rem; cursor: pointer;
|
|
101
|
+
}
|
|
102
|
+
#sw-reader-toggle:hover { background: #e5e7eb; color: #111827; }
|
|
103
|
+
|
|
104
|
+
/* Only the file list scrolls; #sw-reader-nav stays docked at the bottom. */
|
|
105
|
+
#sw-reader-files { flex: 1; overflow-y: auto; min-height: 0; }
|
|
106
|
+
#sw-reader-files h2 {
|
|
107
|
+
margin: 0; padding: 0.75rem 1rem; font-size: 0.7rem; font-weight: 600;
|
|
108
|
+
text-transform: uppercase; letter-spacing: .08em; color: #6b7280;
|
|
109
|
+
border-bottom: 1px solid #e5e7eb;
|
|
110
|
+
}
|
|
111
|
+
#sw-reader-files .dir-header {
|
|
112
|
+
padding: 0.4rem 1rem; font-size: 0.75rem; color: #4b5563; cursor: pointer;
|
|
113
|
+
user-select: none; display: flex; align-items: center; gap: 0.4rem;
|
|
114
|
+
}
|
|
115
|
+
#sw-reader-files .dir-header:hover { background: #e5e7eb; }
|
|
116
|
+
/* Base glyph (▶) is the collapsed state; .expanded rotates it to point
|
|
117
|
+
down. Same convention as .history-section-header.expanded below, so
|
|
118
|
+
toggleSection() can drive both with one function. */
|
|
119
|
+
#sw-reader-files .dir-arrow { font-size: 0.6rem; transition: transform 0.15s; display: inline-block; }
|
|
120
|
+
#sw-reader-files .dir-header.expanded .dir-arrow { transform: rotate(90deg); }
|
|
121
|
+
#sw-reader-files .dir-files { list-style: none; margin: 0; padding: 0; }
|
|
122
|
+
#sw-reader-files .dir-files.collapsed { display: none; }
|
|
123
|
+
#sw-reader-files .dir-files li a {
|
|
124
|
+
display: block; padding: 0.35rem 1rem 0.35rem 1.75rem; font-size: 0.8rem;
|
|
125
|
+
color: #4b5563; text-decoration: none; white-space: nowrap;
|
|
126
|
+
overflow: hidden; text-overflow: ellipsis;
|
|
127
|
+
}
|
|
128
|
+
#sw-reader-files .dir-files li a:hover { background: #e5e7eb; color: #111827; }
|
|
129
|
+
#sw-reader-files .dir-files li a.active { background: #dbeafe; color: #1d4ed8; font-weight: 600; }
|
|
130
|
+
/* Delete control (stream_weaver-uvaj). The row becomes a flex line so
|
|
131
|
+
the trash sits at its right edge; the link keeps flex:1 + min-width:0
|
|
132
|
+
so a long name still ellipsizes instead of shoving the button out of
|
|
133
|
+
the 220px rail. Visible at rest, not hover-only: a control you have to
|
|
134
|
+
discover by hovering is friction, and it's the confirm dialog -- not
|
|
135
|
+
concealment -- that protects against a mis-click. */
|
|
136
|
+
#sw-reader-files .dir-files li { display: flex; align-items: center; }
|
|
137
|
+
#sw-reader-files .dir-files li a { flex: 1 1 auto; min-width: 0; }
|
|
138
|
+
#sw-reader-files .sw-doc-delete {
|
|
139
|
+
flex: 0 0 auto; border: none; background: none; cursor: pointer;
|
|
140
|
+
padding: 0.35rem 0.65rem 0.35rem 0.15rem; font-size: 0.72rem;
|
|
141
|
+
line-height: 1; opacity: 0.4;
|
|
142
|
+
}
|
|
143
|
+
#sw-reader-files .sw-doc-delete:hover { opacity: 1; }
|
|
144
|
+
|
|
145
|
+
/* Confirm dialog. Same modal treatment as the Save-as-doc widget's
|
|
146
|
+
(.sw-save-doc-*, further down), but rooted at #sw-reader-chrome so it
|
|
147
|
+
stays inside this block's scoping guard (reader_chrome_css_spec)
|
|
148
|
+
instead of needing a second unscoped <style> the way that widget did. */
|
|
149
|
+
#sw-reader-chrome [x-cloak] { display: none !important; }
|
|
150
|
+
#sw-reader-chrome .sw-doc-delete-modal {
|
|
151
|
+
position: fixed; inset: 0; z-index: 60;
|
|
152
|
+
background: rgba(15, 17, 23, 0.45);
|
|
153
|
+
display: flex; align-items: center; justify-content: center;
|
|
154
|
+
}
|
|
155
|
+
#sw-reader-chrome .sw-doc-delete-dialog {
|
|
156
|
+
background: #fff; border-radius: 8px; padding: 1.5rem;
|
|
157
|
+
width: min(420px, 90vw); box-shadow: 0 20px 50px rgba(0,0,0,0.25);
|
|
158
|
+
font-family: system-ui, sans-serif; color: #111827; text-align: left;
|
|
159
|
+
}
|
|
160
|
+
#sw-reader-chrome .sw-doc-delete-dialog h3 { margin: 0 0 0.5rem 0; font-size: 1.1rem; }
|
|
161
|
+
#sw-reader-chrome .sw-doc-delete-dialog p.hint {
|
|
162
|
+
margin: 0; color: #6b7280; font-size: 0.85rem; word-break: break-word;
|
|
163
|
+
}
|
|
164
|
+
#sw-reader-chrome .sw-doc-delete-error {
|
|
165
|
+
margin-top: 0.75rem; padding: 0.5rem 0.75rem;
|
|
166
|
+
background: #fee2e2; color: #991b1b; border-radius: 4px; font-size: 0.85rem;
|
|
167
|
+
}
|
|
168
|
+
#sw-reader-chrome .sw-doc-delete-actions {
|
|
169
|
+
display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 1rem;
|
|
170
|
+
}
|
|
171
|
+
#sw-reader-chrome .sw-doc-delete-actions button {
|
|
172
|
+
padding: 0.45rem 1rem; border-radius: 5px; font-size: 0.9rem;
|
|
173
|
+
cursor: pointer; border: 1px solid transparent;
|
|
174
|
+
}
|
|
175
|
+
#sw-reader-chrome .sw-doc-delete-actions button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
176
|
+
#sw-reader-chrome .sw-doc-delete-cancel { background: #f3f4f6; color: #374151; border-color: #d1d5db; }
|
|
177
|
+
#sw-reader-chrome .sw-doc-delete-cancel:hover:not(:disabled) { background: #e5e7eb; }
|
|
178
|
+
#sw-reader-chrome .sw-doc-delete-confirm { background: #b91c1c; color: #fff; }
|
|
179
|
+
#sw-reader-chrome .sw-doc-delete-confirm:hover:not(:disabled) { filter: brightness(1.08); }
|
|
180
|
+
|
|
181
|
+
/* .org format badge (stream_weaver-gnj8) -- unconditional on every org
|
|
182
|
+
entry, not just on a name collision with its .rb sibling; see the
|
|
183
|
+
ERB comment above this class's only use site for why. */
|
|
184
|
+
#sw-reader-files .sw-reader-file-badge {
|
|
185
|
+
display: inline-block; margin-left: 0.4rem; padding: 0.05rem 0.35rem;
|
|
186
|
+
font-size: 0.65rem; font-weight: 600; letter-spacing: 0.02em;
|
|
187
|
+
color: #6b7280; background: #e5e7eb; border-radius: 3px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Repo filter (stream_weaver-iugu) -- same chip treatment as
|
|
191
|
+
.browse-shortcuts below, since it is the same kind of control (a row
|
|
192
|
+
of small jump targets above the listing) and two different-looking
|
|
193
|
+
chip rows in one 220px rail would read as two unrelated mechanisms.
|
|
194
|
+
Only the .active state is new: unlike Browse's shortcuts, exactly one
|
|
195
|
+
of these is always the current view. */
|
|
196
|
+
#sw-reader-files .repo-filter {
|
|
197
|
+
padding: 0.5rem 1rem; display: flex; gap: 0.4rem; flex-wrap: wrap;
|
|
198
|
+
border-bottom: 1px solid #e5e7eb;
|
|
199
|
+
}
|
|
200
|
+
#sw-reader-files .repo-filter a {
|
|
201
|
+
font-size: 0.7rem; padding: 0.15rem 0.5rem; border-radius: 3px;
|
|
202
|
+
background: #e5e7eb; color: #374151; text-decoration: none;
|
|
203
|
+
max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
204
|
+
}
|
|
205
|
+
#sw-reader-files .repo-filter a:hover { background: #d1d5db; }
|
|
206
|
+
#sw-reader-files .repo-filter a.active {
|
|
207
|
+
background: #dbeafe; color: #1d4ed8; font-weight: 600;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Browse (stream_weaver-rdh). .browse-listing reuses .dir-files li a's
|
|
211
|
+
styling above (same selector chain) for consistent link appearance;
|
|
212
|
+
only the browse-specific chrome (shortcuts, breadcrumbs, up/back
|
|
213
|
+
links) needs its own rules. */
|
|
214
|
+
#sw-reader-files .browse-shortcuts {
|
|
215
|
+
padding: 0.5rem 1rem; display: flex; gap: 0.5rem; flex-wrap: wrap;
|
|
216
|
+
border-bottom: 1px solid #e5e7eb;
|
|
217
|
+
}
|
|
218
|
+
#sw-reader-files .browse-shortcuts a {
|
|
219
|
+
font-size: 0.7rem; padding: 0.15rem 0.5rem; border-radius: 3px;
|
|
220
|
+
background: #e5e7eb; color: #374151; text-decoration: none;
|
|
221
|
+
}
|
|
222
|
+
#sw-reader-files .browse-shortcuts a:hover { background: #d1d5db; }
|
|
223
|
+
#sw-reader-files .browse-crumbs {
|
|
224
|
+
padding: 0.4rem 1rem; font-size: 0.72rem; color: #6b7280;
|
|
225
|
+
word-break: break-all; border-bottom: 1px solid #e5e7eb;
|
|
226
|
+
}
|
|
227
|
+
#sw-reader-files .browse-crumbs a { color: #4b5563; text-decoration: none; }
|
|
228
|
+
#sw-reader-files .browse-crumbs a:hover { text-decoration: underline; }
|
|
229
|
+
#sw-reader-files .browse-crumbs .crumb-sep { color: #9ca3af; }
|
|
230
|
+
#sw-reader-files .browse-listing .browse-up,
|
|
231
|
+
#sw-reader-files .browse-listing .browse-dir { font-weight: 500; }
|
|
232
|
+
#sw-reader-files .browse-empty {
|
|
233
|
+
padding: 0.5rem 1rem 0.5rem 1.75rem; font-size: 0.75rem; color: #9ca3af;
|
|
234
|
+
font-style: italic;
|
|
235
|
+
}
|
|
236
|
+
#sw-reader-files .browse-back, #sw-reader-files .browse-link {
|
|
237
|
+
display: block; padding: 0.5rem 1rem; font-size: 0.75rem;
|
|
238
|
+
color: #4b5563; text-decoration: none; border-top: 1px solid #e5e7eb;
|
|
239
|
+
}
|
|
240
|
+
#sw-reader-files .browse-back:hover, #sw-reader-files .browse-link:hover {
|
|
241
|
+
background: #e5e7eb; color: #111827;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* History section (collapsed by default) */
|
|
245
|
+
#sw-reader-files .history-section-header {
|
|
246
|
+
border-top: 1px solid #e5e7eb; border-bottom: none;
|
|
247
|
+
cursor: pointer; user-select: none; display: flex; align-items: center; gap: 0.4rem;
|
|
248
|
+
}
|
|
249
|
+
#sw-reader-files .history-section-header:hover { background: #e5e7eb; color: #374151; }
|
|
250
|
+
#sw-reader-files .history-arrow { font-size: 0.6rem; transition: transform 0.15s; display: inline-block; }
|
|
251
|
+
#sw-reader-files .history-section-header.expanded .history-arrow { transform: rotate(90deg); }
|
|
252
|
+
#sw-reader-files .history-content.collapsed { display: none; }
|
|
253
|
+
#sw-reader-files .history-session .dir-header { font-size: 0.7rem; color: #6b7280; padding-left: 1.25rem; }
|
|
254
|
+
#sw-reader-files .history-session .dir-files li a {
|
|
255
|
+
padding-left: 2.25rem; font-family: ui-monospace, monospace; font-size: 0.72rem;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* Prev/Next docked in the rail -- not a fixed bottom bar, which would
|
|
259
|
+
collide with the Save-as-doc button at bottom right. flex-wrap
|
|
260
|
+
because Export (and, on a doc with mermaid, a second Export
|
|
261
|
+
(offline) row -- stream_weaver-dnq) sits below Prev/counter/Next,
|
|
262
|
+
which already fill the 220px width -- a fourth same-row item would
|
|
263
|
+
squeeze the counter. */
|
|
264
|
+
#sw-reader-nav {
|
|
265
|
+
flex-shrink: 0; border-top: 1px solid #e5e7eb; background: #f9fafb;
|
|
266
|
+
padding: 0.5rem 0.75rem; display: flex; flex-wrap: wrap; align-items: center;
|
|
267
|
+
justify-content: space-between; gap: 0.4rem;
|
|
268
|
+
}
|
|
269
|
+
#sw-reader-nav .nav-link {
|
|
270
|
+
background: #e5e7eb; color: #374151; border: none; padding: 0.3rem 0.6rem;
|
|
271
|
+
border-radius: 4px; cursor: pointer; font-size: 0.75rem;
|
|
272
|
+
text-decoration: none; display: inline-block;
|
|
273
|
+
}
|
|
274
|
+
#sw-reader-nav .nav-link:hover { background: #d1d5db; color: #111827; }
|
|
275
|
+
#sw-reader-nav .nav-link--disabled { opacity: 0.35; cursor: default; pointer-events: none; }
|
|
276
|
+
#sw-reader-nav .counter { font-size: 0.72rem; color: #6b7280; white-space: nowrap; }
|
|
277
|
+
#sw-reader-nav .nav-export { flex-basis: 100%; text-align: center; }
|
|
278
|
+
|
|
279
|
+
/* Mandatory below ~1100px: the sidebar_toc's own desktop breakpoint
|
|
280
|
+
(min-width: 1000px) measures the VIEWPORT, not the doc's content
|
|
281
|
+
region, so a permanently-present 220px rail leaves the doc cramped at
|
|
282
|
+
common laptop widths. Default to collapsed and let the toggle slide it
|
|
283
|
+
back in over the content. */
|
|
284
|
+
@media (max-width: 1100px) {
|
|
285
|
+
html { padding-left: 0; }
|
|
286
|
+
#sw-reader-chrome { left: -220px; }
|
|
287
|
+
html.sw-rail-open #sw-reader-chrome { left: 0; box-shadow: 0 0 30px rgba(0,0,0,0.18); }
|
|
288
|
+
}
|
|
289
|
+
}</style>
|
|
290
|
+
|
|
291
|
+
<%# CDN dependencies. Written out here rather than via adapter.cdn_scripts:
|
|
292
|
+
the adapter is in :websocket mode (for component-markup parity with the
|
|
293
|
+
canvas) and would emit a connect script for /canvas/reader/ws, which the
|
|
294
|
+
reader doesn't serve -- 5 failed reconnects in the console. %>
|
|
295
|
+
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
|
296
|
+
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
|
297
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
|
298
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css">
|
|
299
|
+
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
|
300
|
+
|
|
301
|
+
<%# Mermaid zoom/pan engine — inlined here so it self-inits on DOMContentLoaded %>
|
|
302
|
+
<script><%= @mermaid_zoom_js %></script>
|
|
303
|
+
<%# Kick off mermaid CDN preload immediately (same pattern as bridge server) %>
|
|
304
|
+
<script>if (window.swMermaidPreload) window.swMermaidPreload();</script>
|
|
305
|
+
</head>
|
|
306
|
+
<body class="<%= body_class %>">
|
|
307
|
+
|
|
308
|
+
<%# #app-container is a DIRECT body child, exactly as the canvas renders it.
|
|
309
|
+
Every sidebar_toc/theme selector that matters uses the ">" combinator
|
|
310
|
+
against `body[class*="sw-layout-"] > #app-container` -- nesting it inside
|
|
311
|
+
a wrapper leaves all of them dead. data-sw-body-class is read back by the
|
|
312
|
+
htmx:afterSwap handler below, since <body>'s own class attribute is never
|
|
313
|
+
part of the swapped region. %>
|
|
314
|
+
<div id="app-container" data-sw-body-class="<%= body_class %>">
|
|
315
|
+
<% if doc %>
|
|
316
|
+
<%= doc.html %>
|
|
317
|
+
<% else %>
|
|
318
|
+
<div style="padding: 3rem; text-align: center; color: #6b7280; font-family: system-ui, sans-serif;">
|
|
319
|
+
Pick a <code>.rb</code> or <code>.org</code> file from the browser on the left.
|
|
320
|
+
</div>
|
|
321
|
+
<% end %>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
<%# hx-boost lives on the chrome, not <body>: that keeps in-document
|
|
325
|
+
.sw-sidebar-toc__link anchors (#section-id jumps) structurally outside the
|
|
326
|
+
boosted subtree. hx-swap="outerHTML" is required -- hx-select keeps the
|
|
327
|
+
matched element itself, so innerHTML would nest a new #app-container
|
|
328
|
+
inside the old one on every click, killing every ">" selector and
|
|
329
|
+
getElementById('app-container') lookup after the first navigation.
|
|
330
|
+
#sw-reader-files is deliberately NOT in this default hx-select-oob --
|
|
331
|
+
stream_weaver-8v1 keeps accordion expand/collapse state alive across
|
|
332
|
+
Prev/Next and sidebar clicks precisely BY never swapping it. Browse
|
|
333
|
+
(stream_weaver-rdh) needs the opposite (the sidebar's own content has to
|
|
334
|
+
change between directories), so its links carry their own, wider
|
|
335
|
+
hx-select-oob (BROWSE_OOB below) that overrides this default -- boosted
|
|
336
|
+
per-element attributes win over the ancestor's, so normal docs/history
|
|
337
|
+
navigation is untouched. %>
|
|
338
|
+
<%# x-data for the delete confirm (stream_weaver-uvaj) sits HERE, on the one
|
|
339
|
+
piece of chrome that is never itself swapped -- the delete buttons live
|
|
340
|
+
in #sw-reader-files and the request they fire replaces that whole region,
|
|
341
|
+
so component state kept inside it would be torn out from under its own
|
|
342
|
+
in-flight callback. Descendants inherit the scope, which is all the
|
|
343
|
+
buttons need. %>
|
|
344
|
+
<nav id="sw-reader-chrome"
|
|
345
|
+
x-data="swDeleteDoc()" @keydown.escape.window="delOpen = false"
|
|
346
|
+
hx-boost="true" hx-target="#app-container" hx-select="#app-container"
|
|
347
|
+
hx-swap="outerHTML" hx-select-oob="#sw-reader-nav">
|
|
348
|
+
|
|
349
|
+
<button id="sw-reader-toggle" type="button" onclick="swToggleRail()"
|
|
350
|
+
title="Toggle file rail" aria-label="Toggle file rail">☰</button>
|
|
351
|
+
|
|
352
|
+
<div id="sw-reader-files">
|
|
353
|
+
<% if browse_mode %>
|
|
354
|
+
<%# Every link in this block carries its own hx-select-oob, wider than
|
|
355
|
+
the chrome's default (#sw-reader-nav alone, see the <nav> comment
|
|
356
|
+
above) -- Browse needs the sidebar's own CONTENT to change between
|
|
357
|
+
directories, unlike normal docs/history navigation, which needs it
|
|
358
|
+
to stay untouched so accordion state survives (stream_weaver-8v1).
|
|
359
|
+
A boosted element's own hx-* attributes override its ancestor's. %>
|
|
360
|
+
<h2>Browse</h2>
|
|
361
|
+
<%# First thing in the listing, not last -- previously sat after the
|
|
362
|
+
whole file/dir listing (browse-back below), which meant scrolling
|
|
363
|
+
past a potentially long directory to find the only way out of
|
|
364
|
+
Browse mode. stream_weaver-gnj8. %>
|
|
365
|
+
<a href="/?file=0" class="browse-back" <%= browse_oob %>>← Back to docs</a>
|
|
366
|
+
<div class="browse-shortcuts">
|
|
367
|
+
<% if @repo_docs_root %>
|
|
368
|
+
<a href="/browse?dir=<%= ERB::Util.url_encode(@repo_docs_root) %>" <%= browse_oob %>>This repo</a>
|
|
369
|
+
<% end %>
|
|
370
|
+
<a href="/browse?dir=<%= ERB::Util.url_encode(StreamWeaver::Canvas::DocStore::DEFAULT_ROOT) %>" <%= browse_oob %>>StreamWeaver (global)</a>
|
|
371
|
+
</div>
|
|
372
|
+
<div class="browse-crumbs">
|
|
373
|
+
<a href="/browse?dir=%2F" <%= browse_oob %>>/</a>
|
|
374
|
+
<% @breadcrumbs.each do |label, crumb_path| %>
|
|
375
|
+
<span class="crumb-sep">/</span><a href="/browse?dir=<%= ERB::Util.url_encode(crumb_path) %>" <%= browse_oob %>><%= ERB::Util.h(label) %></a>
|
|
376
|
+
<% end %>
|
|
377
|
+
</div>
|
|
378
|
+
<%# Filenames/dirnames come straight off the filesystem -- '"', '<', '&'
|
|
379
|
+
are all legal there, and unlike the fixed docs sections above,
|
|
380
|
+
Browse can be pointed at literally any directory (~/Downloads
|
|
381
|
+
included, where an attacker controls a dropped file's name). Every
|
|
382
|
+
filesystem-derived string in this block is ERB::Util.h'd; the
|
|
383
|
+
hrefs are already safe via url_encode. %>
|
|
384
|
+
<ul class="dir-files browse-listing">
|
|
385
|
+
<% if @browse_parent %>
|
|
386
|
+
<li><a href="/browse?dir=<%= ERB::Util.url_encode(@browse_parent) %>" class="browse-up" <%= browse_oob %>>.. (up)</a></li>
|
|
387
|
+
<% end %>
|
|
388
|
+
<% @browse_entries[:dirs].each do |name| %>
|
|
389
|
+
<li><a href="/browse?dir=<%= ERB::Util.url_encode(File.join(@browse_dir, name)) %>" class="browse-dir" <%= browse_oob %>>📁 <%= ERB::Util.h(name) %></a></li>
|
|
390
|
+
<% end %>
|
|
391
|
+
<% @browse_entries[:files].each do |name| %>
|
|
392
|
+
<% full = File.join(@browse_dir, name) %>
|
|
393
|
+
<li><a href="/open?path=<%= ERB::Util.url_encode(full) %>" class="<%= full == @current_file ? 'active' : '' %>" title="<%= ERB::Util.h(full) %>" <%= browse_oob %>><%= ERB::Util.h(name) %></a></li>
|
|
394
|
+
<% end %>
|
|
395
|
+
<% if @browse_entries[:dirs].empty? && @browse_entries[:files].empty? %>
|
|
396
|
+
<li class="browse-empty">(empty)</li>
|
|
397
|
+
<% end %>
|
|
398
|
+
</ul>
|
|
399
|
+
<% else %>
|
|
400
|
+
<h2><%= history_groups.any? ? 'Docs' : 'Files' %></h2>
|
|
401
|
+
<%# Repo filter (stream_weaver-iugu). Only rendered once there is more
|
|
402
|
+
than one repo to choose between -- a single-repo rail has nothing to
|
|
403
|
+
filter and the row would be pure noise. Each link carries browse_oob
|
|
404
|
+
for the same reason Browse's links do: the sidebar's own CONTENT has
|
|
405
|
+
to change, and the chrome's default OOB (#sw-reader-nav alone,
|
|
406
|
+
stream_weaver-8v1) deliberately leaves #sw-reader-files untouched. %>
|
|
407
|
+
<% if repo_labels.size > 1 %>
|
|
408
|
+
<div class="repo-filter">
|
|
409
|
+
<% repo_labels.each do |label| %>
|
|
410
|
+
<a href="/?file=<%= @current_index || 0 %>&repo=<%= ERB::Util.url_encode(label) %>"
|
|
411
|
+
class="<%= label == @repo_filter ? 'active' : '' %>" <%= browse_oob %>><%= ERB::Util.h(label) %></a>
|
|
412
|
+
<% end %>
|
|
413
|
+
<a href="/?file=<%= @current_index || 0 %>&repo=all"
|
|
414
|
+
class="<%= @repo_filter.nil? ? 'active' : '' %>" <%= browse_oob %>>All</a>
|
|
415
|
+
</div>
|
|
416
|
+
<% end %>
|
|
417
|
+
<% docs_groups.each do |dir, entries| %>
|
|
418
|
+
<% dir_label = @file_list.label_for(dir) %>
|
|
419
|
+
<% deletable = deletable_dirs.include?(dir) %>
|
|
420
|
+
<div class="dir-section">
|
|
421
|
+
<div class="dir-header expanded" onclick="toggleSection(this)">
|
|
422
|
+
<span class="dir-arrow">▶</span>
|
|
423
|
+
<span title="<%= dir %>"><%= dir_label %></span>
|
|
424
|
+
</div>
|
|
425
|
+
<ul class="dir-files">
|
|
426
|
+
<% entries.each do |filepath, idx| %>
|
|
427
|
+
<li>
|
|
428
|
+
<a href="/?file=<%= idx %><%= repo_qs %>"
|
|
429
|
+
class="<%= idx == @current_index ? 'active' : '' %>"
|
|
430
|
+
title="<%= filepath %>">
|
|
431
|
+
<%# History entries are always .rb (written by canvas-push,
|
|
432
|
+
see doc_store.rb) so plain File.basename(path, '.rb')
|
|
433
|
+
is still correct below in the history block -- this
|
|
434
|
+
docs_groups block is the one FileList.build now also
|
|
435
|
+
populates with .org, so it needs both extensions
|
|
436
|
+
stripped, not just one. %>
|
|
437
|
+
<%= File.basename(filepath).sub(/\.(rb|org)\z/, '') %>
|
|
438
|
+
<%# Shown unconditionally on every .org entry, not just on
|
|
439
|
+
a name collision (stream_weaver-gnj8) -- a same-stem
|
|
440
|
+
.rb/.org pair (demo.rb + demo.org) is otherwise
|
|
441
|
+
indistinguishable once the extension is stripped
|
|
442
|
+
above, and a badge that only sometimes appears would
|
|
443
|
+
be worse than one that's always there: you couldn't
|
|
444
|
+
trust its absence to mean "no other version exists."
|
|
445
|
+
.rb gets no badge -- it's the implied default. %>
|
|
446
|
+
<% if filepath.end_with?('.org') %><span class="sw-reader-file-badge">org</span><% end %>
|
|
447
|
+
</a>
|
|
448
|
+
<%# One row = one file: a .rb/.org pair are two entries with
|
|
449
|
+
two independent delete buttons, and deleting one never
|
|
450
|
+
cascades to its sibling. This button opens the confirm
|
|
451
|
+
dialog and nothing else -- the actual POST only ever fires
|
|
452
|
+
from the dialog's own Delete button (no JS confirm dialog
|
|
453
|
+
anywhere on this page; the design doc rules it out). Same
|
|
454
|
+
to_json-then-h quoting the Save-as-doc widget documents
|
|
455
|
+
below: a filesystem name can legally contain a quote or an
|
|
456
|
+
apostrophe, and either escape alone breaks out of the
|
|
457
|
+
attribute. %>
|
|
458
|
+
<% if deletable %>
|
|
459
|
+
<button type="button" class="sw-doc-delete"
|
|
460
|
+
title="Delete <%= ERB::Util.h(File.basename(filepath)) %>"
|
|
461
|
+
aria-label="Delete <%= ERB::Util.h(File.basename(filepath)) %>"
|
|
462
|
+
@click.prevent.stop="askDelete(<%= ERB::Util.h(filepath.to_json) %>, <%= ERB::Util.h(File.basename(filepath).to_json) %>)">🗑</button>
|
|
463
|
+
<% end %>
|
|
464
|
+
</li>
|
|
465
|
+
<% end %>
|
|
466
|
+
</ul>
|
|
467
|
+
</div>
|
|
468
|
+
<% end %>
|
|
469
|
+
|
|
470
|
+
<% if history_groups.any? %>
|
|
471
|
+
<h2 class="history-section-header" onclick="toggleSection(this)">
|
|
472
|
+
<span class="history-arrow">▶</span>
|
|
473
|
+
History (<%= history_groups.size %>)
|
|
474
|
+
</h2>
|
|
475
|
+
<div class="history-content collapsed">
|
|
476
|
+
<% history_groups.each do |dir, entries| %>
|
|
477
|
+
<% session_label = File.basename(dir) %>
|
|
478
|
+
<div class="dir-section history-session">
|
|
479
|
+
<div class="dir-header" onclick="toggleSection(this)">
|
|
480
|
+
<span class="dir-arrow">▶</span>
|
|
481
|
+
<span title="<%= dir %>"><%= session_label %></span>
|
|
482
|
+
</div>
|
|
483
|
+
<ul class="dir-files collapsed">
|
|
484
|
+
<% entries.each do |filepath, idx| %>
|
|
485
|
+
<li>
|
|
486
|
+
<a href="/?file=<%= idx %><%= repo_qs %>"
|
|
487
|
+
class="<%= idx == @current_index ? 'active' : '' %>"
|
|
488
|
+
title="<%= filepath %>">
|
|
489
|
+
<%= File.basename(filepath, '.rb') %>
|
|
490
|
+
</a>
|
|
491
|
+
</li>
|
|
492
|
+
<% end %>
|
|
493
|
+
</ul>
|
|
494
|
+
</div>
|
|
495
|
+
<% end %>
|
|
496
|
+
</div>
|
|
497
|
+
<% end %>
|
|
498
|
+
|
|
499
|
+
<a href="/browse" class="browse-link" <%= browse_oob %>>📁 Browse files…</a>
|
|
500
|
+
<% end %>
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
<div id="sw-reader-nav">
|
|
504
|
+
<%# @current_index is unset for a file opened via Browse (stream_weaver-rdh)
|
|
505
|
+
-- it isn't part of any FileList, so there's no index to page
|
|
506
|
+
Prev/Next through and no ?file=N to hand /export. Browsed files skip
|
|
507
|
+
this whole block rather than guessing at a meaningless index. %>
|
|
508
|
+
<% if @current_index %>
|
|
509
|
+
<% prev_idx = @current_index - 1 %>
|
|
510
|
+
<% next_idx = @current_index + 1 %>
|
|
511
|
+
<% if prev_idx >= 0 %>
|
|
512
|
+
<a href="/?file=<%= prev_idx %><%= repo_qs %>" class="nav-link" data-nav="prev">◀ Prev</a>
|
|
513
|
+
<% else %>
|
|
514
|
+
<span class="nav-link nav-link--disabled">◀ Prev</span>
|
|
515
|
+
<% end %>
|
|
516
|
+
<span class="counter"><%= @current_index + 1 %> of <%= @file_list.size %></span>
|
|
517
|
+
<% if next_idx < @file_list.size %>
|
|
518
|
+
<a href="/?file=<%= next_idx %><%= repo_qs %>" class="nav-link" data-nav="next">Next ▶</a>
|
|
519
|
+
<% else %>
|
|
520
|
+
<span class="nav-link nav-link--disabled">Next ▶</span>
|
|
521
|
+
<% end %>
|
|
522
|
+
|
|
523
|
+
<%# Available for every file, not just history snapshots. hx-boost="false"
|
|
524
|
+
is mandatory: this link is inside the boosted <nav>, and a boosted
|
|
525
|
+
click would swap the downloaded document into #app-container instead
|
|
526
|
+
of handing it to the browser as a file.
|
|
527
|
+
No `download` attribute: Content-Disposition: attachment already
|
|
528
|
+
triggers the save-as on success and leaves this page in place --
|
|
529
|
+
`download` would additionally force a 422/500 error body into
|
|
530
|
+
~/Downloads as an extensionless file instead of showing the error.
|
|
531
|
+
target="_blank" keeps a failed export's error text out of the way
|
|
532
|
+
of the page you were reading. %>
|
|
533
|
+
<a href="/export?file=<%= @current_index %>" class="nav-link nav-export"
|
|
534
|
+
hx-boost="false" target="_blank"
|
|
535
|
+
title="Download this doc as a standalone HTML file">⬇ Export HTML</a>
|
|
536
|
+
|
|
537
|
+
<%# Fetches and inlines mermaid's own library instead of referencing its CDN
|
|
538
|
+
(stream_weaver-dnq), so mermaid diagrams render in a viewer whose CSP
|
|
539
|
+
blocks external scripts entirely -- SharePoint's HTML preview is the
|
|
540
|
+
motivating case. Only shown on a doc that actually has a mermaid
|
|
541
|
+
diagram (@doc.html always contains "sw-mermaid" for one, in either
|
|
542
|
+
the diagram's own CSS class or its lazily-injected CSS block) --
|
|
543
|
+
the flag does nothing on a doc without one, so a second identical
|
|
544
|
+
button there would just be a confusing extra click with no effect. %>
|
|
545
|
+
<% if @doc.html.include?("sw-mermaid") %>
|
|
546
|
+
<a href="/export?file=<%= @current_index %>&offline=1" class="nav-link nav-export"
|
|
547
|
+
hx-boost="false" target="_blank"
|
|
548
|
+
title="Download as standalone HTML with mermaid's library inlined, for viewers that block external scripts (e.g. SharePoint)">⬇ Export HTML (offline)</a>
|
|
549
|
+
<% end %>
|
|
550
|
+
<% elsif @current_file %>
|
|
551
|
+
<span class="counter" title="<%= ERB::Util.h(@current_file) %>">Browsed: <%= ERB::Util.h(File.basename(@current_file)) %></span>
|
|
552
|
+
<% end %>
|
|
553
|
+
|
|
554
|
+
<% if is_history %>
|
|
555
|
+
<%# Save-as-doc widget — promotes this history snapshot to docs/streamweaver_canvas/.
|
|
556
|
+
Chrome (CSS + Alpine skeleton) shared with the canvas page's own widget via
|
|
557
|
+
SaveDocWidget (stream_weaver-e13); only the endpoint, default-name source,
|
|
558
|
+
the file: index, and copy below are specific to the reader. Rendered inside
|
|
559
|
+
this same is_history conditional (not hoisted up with #sw-reader-chrome-css)
|
|
560
|
+
because spec/canvas/reader_promote_history_spec.rb asserts the sw-save-doc-*
|
|
561
|
+
strings are absent entirely for docs files. %>
|
|
562
|
+
<%
|
|
563
|
+
# to_json gives valid JS-string quoting (handles embedded quotes/apostrophes
|
|
564
|
+
# from a filesystem-derived name correctly); ERB::Util.h then HTML-escapes
|
|
565
|
+
# the result for the surrounding "..." attribute. Either alone is wrong: bare
|
|
566
|
+
# to_json leaks a literal " that closes the attribute early (the exact q4y
|
|
567
|
+
# regression the spec below guards against); the old ERB::Util.h(name) with
|
|
568
|
+
# hand-written '...' quotes HTML-escapes ' to ', which the HTML parser
|
|
569
|
+
# decodes back to a literal ' before Alpine ever sees it, so an apostrophe in
|
|
570
|
+
# the name still breaks out.
|
|
571
|
+
%>
|
|
572
|
+
<%= StreamWeaver::Canvas::SaveDocWidget.render(
|
|
573
|
+
endpoint: '/save-doc',
|
|
574
|
+
button_title: 'Save this history snapshot as a persistent doc',
|
|
575
|
+
dialog_title: 'Save snapshot as doc',
|
|
576
|
+
hint_html: <<~HINT.strip,
|
|
577
|
+
<span x-show="format !== 'org'">Promotes this history entry to <code>docs/streamweaver_canvas/<name>.rb</code> (or <code>~/.streamweaver/canvas/</code> outside a git repo).</span>
|
|
578
|
+
<span x-show="format === 'org'">Promotes this history entry to a human-readable <code><name>.org</code> sibling file alongside the canonical <code>.rb</code>.</span>
|
|
579
|
+
HINT
|
|
580
|
+
# The reader's default name is derived from the archived snapshot's own
|
|
581
|
+
# filename, not wall-clock "now", so it's computed once server-side and
|
|
582
|
+
# needs no client-side recompute on openDialog() (reset_name_js: '').
|
|
583
|
+
name_init: ERB::Util.h(default_name.to_json),
|
|
584
|
+
# stream_weaver-j3b3: the reader has no live session/source_dir to carry
|
|
585
|
+
# (see the save-doc route's own comment) -- this just computes the same
|
|
586
|
+
# git-root auto-detection DocStore.save already does internally, so the
|
|
587
|
+
# toggle's label/hide-when-nil matches what "This repo" actually resolves
|
|
588
|
+
# to today.
|
|
589
|
+
source_dir: StreamWeaver::Canvas::DocStore.git_root(Dir.pwd),
|
|
590
|
+
extra_alpine_data: "file: #{@current_index},\n ",
|
|
591
|
+
extra_body_fields: 'file: this.file, ',
|
|
592
|
+
# display:contents keeps the widget (inside #sw-reader-nav, a flex row so
|
|
593
|
+
# the OOB swap refreshes its file: index on every boosted navigation) from
|
|
594
|
+
# becoming a fourth flex item and pushing Next off the right edge -- both
|
|
595
|
+
# its real children are position:fixed, so nothing lands in the flow.
|
|
596
|
+
host_class: 'sw-save-doc-host',
|
|
597
|
+
css_layer: 'sw-reader-chrome',
|
|
598
|
+
dialog_css_extra: "\n color: #111827; text-align: left;",
|
|
599
|
+
gist: gist_widget_data(default_name)
|
|
600
|
+
) %>
|
|
601
|
+
<% end %>
|
|
602
|
+
</div>
|
|
603
|
+
|
|
604
|
+
<%# Delete confirm (stream_weaver-uvaj). Outside #sw-reader-files and
|
|
605
|
+
#sw-reader-nav on purpose: both are swapped by the very request this
|
|
606
|
+
dialog triggers. Rendered only when something on screen is actually
|
|
607
|
+
deletable. position:fixed, so its placement in the rail's flex column
|
|
608
|
+
doesn't affect where it appears -- and the rail collapses with `left`
|
|
609
|
+
rather than `transform` precisely so fixed descendants stay
|
|
610
|
+
viewport-fixed (see #sw-reader-chrome's CSS comment). %>
|
|
611
|
+
<% if deletable_dirs.any? %>
|
|
612
|
+
<div x-show="delOpen" x-cloak class="sw-doc-delete-modal" @click.self="delOpen = false">
|
|
613
|
+
<div class="sw-doc-delete-dialog" @click.stop>
|
|
614
|
+
<h3>Delete this doc?</h3>
|
|
615
|
+
<p class="hint">
|
|
616
|
+
<%# Deliberately conditional about git: only ONE of the two deletable
|
|
617
|
+
roots is a repo. The global store (~/.streamweaver/canvas) has no
|
|
618
|
+
git safety net at all, and promising one there would be a lie at
|
|
619
|
+
exactly the moment it matters most. %>
|
|
620
|
+
<code x-text="delName"></code> is removed from disk. Nothing is staged or
|
|
621
|
+
committed — inside a repo the deletion simply shows up in <code>git status</code>,
|
|
622
|
+
the same as deleting the file in Finder.
|
|
623
|
+
</p>
|
|
624
|
+
<div x-show="delError" x-text="delError" class="sw-doc-delete-error"></div>
|
|
625
|
+
<div class="sw-doc-delete-actions">
|
|
626
|
+
<button type="button" class="sw-doc-delete-cancel" @click="delOpen = false" :disabled="delBusy">Cancel</button>
|
|
627
|
+
<%# hx-select-oob is here, not on #sw-reader-files: htmx resolves it
|
|
628
|
+
from the element passed as the request's `source`, and this button
|
|
629
|
+
is the one element that can carry the wider Browse-style OOB
|
|
630
|
+
without ALSO becoming an ancestor of the ordinary file links --
|
|
631
|
+
which would make every sidebar click re-render the rail and blow
|
|
632
|
+
away accordion state (stream_weaver-8v1). %>
|
|
633
|
+
<button type="button" class="sw-doc-delete-confirm"
|
|
634
|
+
<%= browse_oob %>
|
|
635
|
+
@click="doDelete()" :disabled="delBusy">
|
|
636
|
+
<span x-show="!delBusy">Delete</span>
|
|
637
|
+
<span x-show="delBusy">Deleting…</span>
|
|
638
|
+
</button>
|
|
639
|
+
</div>
|
|
640
|
+
</div>
|
|
641
|
+
</div>
|
|
642
|
+
<% end %>
|
|
643
|
+
</nav>
|
|
644
|
+
|
|
645
|
+
<script>
|
|
646
|
+
// Shared by docs/history-session dir-headers and the outer History (N)
|
|
647
|
+
// header -- both are a header immediately followed by the region it
|
|
648
|
+
// discloses, so one function drives all three. Arrow rotation is pure CSS
|
|
649
|
+
// (.expanded .dir-arrow / .history-arrow), not inline style, so it can't
|
|
650
|
+
// drift out of sync with the collapsed state the way per-glyph inline
|
|
651
|
+
// transforms did.
|
|
652
|
+
function toggleSection(header) {
|
|
653
|
+
header.classList.toggle('expanded');
|
|
654
|
+
header.nextElementSibling.classList.toggle('collapsed');
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Delete a saved doc (stream_weaver-uvaj). Defined as a global rather than
|
|
658
|
+
// inlined into x-data so the same component can be re-created verbatim
|
|
659
|
+
// every time htmx swaps the rail. This inline script runs during parsing,
|
|
660
|
+
// Alpine's CDN tag is `defer`, so the function always exists by the time
|
|
661
|
+
// Alpine evaluates the attribute.
|
|
662
|
+
function swDeleteDoc() {
|
|
663
|
+
return {
|
|
664
|
+
delOpen: false, delPath: null, delName: null, delBusy: false, delError: null,
|
|
665
|
+
|
|
666
|
+
// The row button's only job. Nothing is deleted until the dialog's own
|
|
667
|
+
// Delete button calls doDelete() -- there is deliberately no JS
|
|
668
|
+
// confirm dialog anywhere on this page.
|
|
669
|
+
askDelete(path, name) {
|
|
670
|
+
this.delPath = path;
|
|
671
|
+
this.delName = name;
|
|
672
|
+
this.delError = null;
|
|
673
|
+
this.delOpen = true;
|
|
674
|
+
},
|
|
675
|
+
|
|
676
|
+
async doDelete() {
|
|
677
|
+
if (this.delBusy) return;
|
|
678
|
+
this.delBusy = true;
|
|
679
|
+
this.delError = null;
|
|
680
|
+
let landing;
|
|
681
|
+
try {
|
|
682
|
+
const res = await fetch('/delete-doc', {
|
|
683
|
+
method: 'POST',
|
|
684
|
+
headers: {'Content-Type': 'application/json'},
|
|
685
|
+
body: JSON.stringify({path: this.delPath, file: swCurrentFileIndex()})
|
|
686
|
+
});
|
|
687
|
+
const data = await res.json();
|
|
688
|
+
if (res.ok && data.ok) {
|
|
689
|
+
landing = data.file;
|
|
690
|
+
this.delOpen = false;
|
|
691
|
+
} else {
|
|
692
|
+
this.delError = data.error || ('HTTP ' + res.status);
|
|
693
|
+
}
|
|
694
|
+
} catch (e) {
|
|
695
|
+
this.delError = e.message;
|
|
696
|
+
}
|
|
697
|
+
this.delBusy = false;
|
|
698
|
+
// Strictly after this component's own state is settled: the refresh
|
|
699
|
+
// swaps #sw-reader-files, and Alpine tears down every component
|
|
700
|
+
// inside it as it goes.
|
|
701
|
+
if (landing !== undefined) swRefreshAfterDelete(landing);
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function swCurrentFileIndex() {
|
|
707
|
+
const n = parseInt(new URLSearchParams(window.location.search).get('file'), 10);
|
|
708
|
+
return Number.isInteger(n) ? n : null;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Re-fetches the doc AND both sidebar regions through htmx rather than
|
|
712
|
+
// reloading the page. `source` is the element htmx resolves hx-select-oob
|
|
713
|
+
// from, and the dialog's Delete button carries the same wider OOB the
|
|
714
|
+
// Browse links use -- the chrome's default (#sw-reader-nav alone)
|
|
715
|
+
// deliberately leaves the file rail untouched, which is exactly what has to
|
|
716
|
+
// change here. fileIndex is null when nothing survived: ?file=0 against an
|
|
717
|
+
// empty list renders the no-doc-open placeholder rather than a 404.
|
|
718
|
+
function swRefreshAfterDelete(fileIndex) {
|
|
719
|
+
const repo = new URLSearchParams(window.location.search).get('repo');
|
|
720
|
+
let url = '/?file=' + (fileIndex === null ? 0 : fileIndex);
|
|
721
|
+
if (repo) url += '&repo=' + encodeURIComponent(repo);
|
|
722
|
+
window.history.replaceState(null, '', url);
|
|
723
|
+
htmx.ajax('GET', url, {
|
|
724
|
+
source: document.querySelector('.sw-doc-delete-confirm'),
|
|
725
|
+
target: '#app-container',
|
|
726
|
+
select: '#app-container',
|
|
727
|
+
swap: 'outerHTML'
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Rail collapse. Below 1100px the rail is collapsed by default and the
|
|
732
|
+
// toggle slides it in (.sw-rail-open); above, it's open by default and the
|
|
733
|
+
// toggle slides it out (.sw-rail-collapsed). Classes go on <html> because
|
|
734
|
+
// the offset rule they drive is `html { padding-left }`.
|
|
735
|
+
function swToggleRail() {
|
|
736
|
+
const root = document.documentElement;
|
|
737
|
+
if (window.matchMedia('(max-width: 1100px)').matches) {
|
|
738
|
+
root.classList.remove('sw-rail-collapsed');
|
|
739
|
+
root.classList.toggle('sw-rail-open');
|
|
740
|
+
} else {
|
|
741
|
+
root.classList.remove('sw-rail-open');
|
|
742
|
+
root.classList.toggle('sw-rail-collapsed');
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Keyboard arrows trigger Prev/Next anchors so they go through htmx
|
|
747
|
+
// (preserving sidebar state). The anchors handle their own enabled/disabled
|
|
748
|
+
// state via the [data-nav] selector — disabled state is rendered as <span>
|
|
749
|
+
// (no [data-nav]) so the click finds nothing.
|
|
750
|
+
//
|
|
751
|
+
// Suppressed while focus is in a text input/textarea/contenteditable (the
|
|
752
|
+
// Save-as-doc name field lives on this same page) -- otherwise correcting
|
|
753
|
+
// a typo with Left/Right silently navigates the reader out from under the
|
|
754
|
+
// open dialog. Also suppressed with any modifier held, so it doesn't
|
|
755
|
+
// shadow browser/OS shortcuts that happen to use the arrow keys.
|
|
756
|
+
document.addEventListener('keydown', (e) => {
|
|
757
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
758
|
+
if (e.target.closest('input, textarea, [contenteditable="true"]')) return;
|
|
759
|
+
// A mermaid fullscreen expand (stream_weaver-yjv) is a <dialog> opened
|
|
760
|
+
// via showModal(), which makes the rest of the page inert to *user*
|
|
761
|
+
// interaction -- but inert doesn't stop a programmatic .click() like
|
|
762
|
+
// this handler's, so without this guard, arrow-key nav still silently
|
|
763
|
+
// navigates the reader out from under an open diagram.
|
|
764
|
+
if (document.querySelector('dialog[open]')) return;
|
|
765
|
+
if (e.key === 'ArrowLeft') document.querySelector('[data-nav="prev"]')?.click();
|
|
766
|
+
if (e.key === 'ArrowRight') document.querySelector('[data-nav="next"]')?.click();
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// Only #app-container and #sw-reader-nav are swapped by default (Browse
|
|
770
|
+
// links, stream_weaver-rdh, additionally swap #sw-reader-files via their
|
|
771
|
+
// own wider BROWSE_OOB), so two things the rest of the page depends on
|
|
772
|
+
// have to be re-derived by hand after each swap:
|
|
773
|
+
// 1. <body>'s theme/layout classes — file N may be :doc themed and N+1 not.
|
|
774
|
+
// 2. the active link in #sw-reader-files, when it wasn't itself swapped
|
|
775
|
+
// this time (stream_weaver-8v1 -- the whole reason it usually isn't:
|
|
776
|
+
// an actual re-render would reset any accordion section a user had
|
|
777
|
+
// opened).
|
|
778
|
+
document.addEventListener('htmx:afterSwap', () => {
|
|
779
|
+
const container = document.getElementById('app-container');
|
|
780
|
+
if (container && container.dataset.swBodyClass) {
|
|
781
|
+
document.body.className = container.dataset.swBodyClass;
|
|
782
|
+
}
|
|
783
|
+
const current = new URLSearchParams(window.location.search).get('file');
|
|
784
|
+
// Browse/Open URLs (and "/", mid-redirect to "/?file=0") carry no
|
|
785
|
+
// ?file= at all -- current is null there. Without this guard,
|
|
786
|
+
// `href.searchParams.get('file') === current` reads as `null === null`
|
|
787
|
+
// for every single Browse link (none of them have a file= param
|
|
788
|
+
// either), so the whole listing lit up .active, and the one
|
|
789
|
+
// non-Browse link on that response ("Back to docs", also file-less)
|
|
790
|
+
// stripped the .active class the OOB swap had just correctly set. Only
|
|
791
|
+
// do this comparison when there's an actual file index to compare.
|
|
792
|
+
if (current === null) return;
|
|
793
|
+
document.querySelectorAll('#sw-reader-files .dir-files a').forEach((a) => {
|
|
794
|
+
const href = new URL(a.getAttribute('href'), window.location.origin);
|
|
795
|
+
a.classList.toggle('active', href.searchParams.get('file') === current);
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
</script>
|
|
799
|
+
</body>
|
|
800
|
+
</html>
|