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,924 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# StreamWeaver University -- the course-list canvas `streamweaver get-started`
|
|
4
|
+
# pushes (stories: course-list-canvas, progress-ledger).
|
|
5
|
+
#
|
|
6
|
+
# Dual-mode single file (shared-DSL pattern, same as
|
|
7
|
+
# docs/university/mockups/course_canvas_mockup.rb):
|
|
8
|
+
# canvas: streamweaver canvas-push university < lib/stream_weaver/university/canvas.rb
|
|
9
|
+
# standalone: ruby lib/stream_weaver/university/canvas.rb
|
|
10
|
+
#
|
|
11
|
+
# `CLI.push_get_started_placeholder_canvas` pushes this file's own text
|
|
12
|
+
# (`File.read`) verbatim, the same "one string of Ruby, eval'd fresh on
|
|
13
|
+
# every push" contract canvas-push uses for a file piped over stdin. Every
|
|
14
|
+
# reference here uses `require` (not `require_relative`), because the bridge
|
|
15
|
+
# process evals the pushed text with no filename
|
|
16
|
+
# (lib/stream_weaver/canvas/bridge.rb `handle_push`), so relative resolution
|
|
17
|
+
# has nothing to resolve against -- only the `$LOAD_PATH`-relative absolute
|
|
18
|
+
# form works there.
|
|
19
|
+
#
|
|
20
|
+
# CSS and the body are local variables (`_css`, `_body`), not constants --
|
|
21
|
+
# matching the mockup's own convention. The whole file gets instance_eval'd
|
|
22
|
+
# fresh on every single push to the SAME long-running bridge process; a
|
|
23
|
+
# `CONST = ...` in that position would warn "already initialized constant"
|
|
24
|
+
# on the second push onward (harmless, but noisy in the bridge log). Local
|
|
25
|
+
# vars reassign silently; so does `def self.foo` (Ruby never warns on method
|
|
26
|
+
# redefinition), which is why `step_states` below is a method, not baked
|
|
27
|
+
# into the body.
|
|
28
|
+
#
|
|
29
|
+
# Visual design is docs/university/design-spec.md v2; CSS below is lifted
|
|
30
|
+
# verbatim from docs/university/mockups/course_canvas_mockup.rb (approved
|
|
31
|
+
# as-is) with the review-only `tabs` rules trimmed. The mockup's separate
|
|
32
|
+
# step screen was built as-designed (step-1-canvas-push) and then folded
|
|
33
|
+
# back into an inline Details expansion on this same course list
|
|
34
|
+
# (single-mode, 2026-09-03, design-spec.md's "Revision note") -- most of
|
|
35
|
+
# that CSS lives on under `.uni-step__expansion`, below.
|
|
36
|
+
#
|
|
37
|
+
# Buttons dispatch for real (default `submit: true`, `id:` not `key:`):
|
|
38
|
+
# App#button only derives a key-based id when a block or `action:` is
|
|
39
|
+
# present (lib/stream_weaver/app.rb `button`) -- a bare `key:` with neither
|
|
40
|
+
# falls back to a per-render counter, which isn't a stable target for a
|
|
41
|
+
# listener to parse a step number back out of. `id:` is always used
|
|
42
|
+
# verbatim (sanitized), so `id: "mark-done-3"` becomes a predictable
|
|
43
|
+
# `btn_mark_done_mark-done-3`. The mockup itself uses `submit: false` (fully
|
|
44
|
+
# decorative) because it is a review artifact, not the built app.
|
|
45
|
+
|
|
46
|
+
require 'stream_weaver/university/course'
|
|
47
|
+
require 'stream_weaver/university/progress'
|
|
48
|
+
require 'stream_weaver/university/runner'
|
|
49
|
+
|
|
50
|
+
# Compact `A::B::C` form, not nested `module A; module B; module C`
|
|
51
|
+
# blocks -- deliberately. When this whole file's text is instance_eval'd as
|
|
52
|
+
# a STRING (canvas push / CLI.render_dsl_to_html; see the file header), a
|
|
53
|
+
# nested `module StreamWeaver; ...; end` reopening does NOT reopen the real
|
|
54
|
+
# top-level `StreamWeaver` -- instance_eval(String) makes the definee the
|
|
55
|
+
# caller's singleton class, so `module Foo` there *creates a new shadow
|
|
56
|
+
# constant on that singleton class* rather than finding the existing global
|
|
57
|
+
# one, and every later same-eval reference to `StreamWeaver::*` then
|
|
58
|
+
# resolves against that empty shadow instead. The compact form looks up
|
|
59
|
+
# `StreamWeaver::University` by ordinary constant resolution *before*
|
|
60
|
+
# opening `Canvas`, which finds the real module (already loaded by the
|
|
61
|
+
# `require`s above) and is safe in both this file's require'd-as-a-library
|
|
62
|
+
# use (specs) and its instance_eval'd-as-a-string use (the live push).
|
|
63
|
+
module StreamWeaver::University::Canvas
|
|
64
|
+
# step[:number] => :done / :current / :todo, computed fresh from the
|
|
65
|
+
# ledger every render -- design-spec section 6, items 4 & 8 (resume
|
|
66
|
+
# band + rail + row marks all key off exactly these three values).
|
|
67
|
+
def self.step_states(progress, steps: StreamWeaver::University::Course::GETTING_STARTED_STEPS)
|
|
68
|
+
next_number = steps.find { |s| !progress.done?(s[:number]) }&.dig(:number)
|
|
69
|
+
steps.each_with_object({}) do |step, memo|
|
|
70
|
+
memo[step[:number]] = if progress.done?(step[:number])
|
|
71
|
+
:done
|
|
72
|
+
elsif step[:number] == next_number
|
|
73
|
+
:current
|
|
74
|
+
else
|
|
75
|
+
:todo
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# "Step N done -- pick up at step N+1." (or, on the last step, the
|
|
81
|
+
# course-complete variant), rendered from `progress.last_done` on the
|
|
82
|
+
# SAME re-push that wrote it -- not a bridge toast. A toast queued
|
|
83
|
+
# alongside this push's HTML both arrive in one ~500ms poll response,
|
|
84
|
+
# and the client unconditionally clears any toast the instant new HTML
|
|
85
|
+
# lands (bridge_server.rb's poll()), so it never has a chance to paint.
|
|
86
|
+
# Putting the message in the HTML itself has no such race.
|
|
87
|
+
def self.mark_done_message(step_number, steps: StreamWeaver::University::Course::GETTING_STARTED_STEPS)
|
|
88
|
+
total = steps.size
|
|
89
|
+
if step_number.to_i >= total
|
|
90
|
+
"Step #{step_number} done -- that's the whole course!"
|
|
91
|
+
else
|
|
92
|
+
"Step #{step_number} done -- pick up at step #{step_number.to_i + 1}."
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A plain markdown bullet list from an array of lines -- what
|
|
97
|
+
# "What you should see" renders as, both on the step screen and inline
|
|
98
|
+
# on a course-list row (`md` turns this into the checked `.uni-payoff`
|
|
99
|
+
# list via its own `- ` list syntax).
|
|
100
|
+
def self.bullets(lines)
|
|
101
|
+
lines.map { |line| "- #{line}" }.join("\n")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# "Re-run" once step `number`'s prompt has actually gone out before --
|
|
105
|
+
# either it ever landed (requested_at, permanent, stamped only on a
|
|
106
|
+
# :sent status) or the last click on this exact step was a failed/
|
|
107
|
+
# degraded send (last_run, which requested_at never records). Shared by
|
|
108
|
+
# the hero button and each step row's own primary button so the two can
|
|
109
|
+
# never say different things about the same step -- round-6 UAT shipped
|
|
110
|
+
# them as two separate inline copies, and the hero one was missed
|
|
111
|
+
# entirely (Forrest, follow-up). Repeat (done steps) never calls this.
|
|
112
|
+
def self.run_label(progress, last_run, number)
|
|
113
|
+
ever_sent = progress.requested_at(number) || (last_run && last_run['step'].to_i == number)
|
|
114
|
+
ever_sent ? "Re-run" : "Run"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
_css = <<~CSS
|
|
119
|
+
body {
|
|
120
|
+
/* ---- Grounds: the page is the room, panels are the instruments ------- */
|
|
121
|
+
--uni-page: #E7E4D9;
|
|
122
|
+
--uni-panel: #FDFCF9;
|
|
123
|
+
--uni-bar: #F3F1E8;
|
|
124
|
+
--uni-sunk: #F1EFE6;
|
|
125
|
+
--uni-line: #D6D2C4;
|
|
126
|
+
--uni-line-soft: #E6E3D7;
|
|
127
|
+
|
|
128
|
+
/* ---- State language: one triad, used identically everywhere ---------- */
|
|
129
|
+
--uni-done: #17754A;
|
|
130
|
+
--uni-done-bg: #E2F0E8;
|
|
131
|
+
--uni-now: var(--sw-color-accent);
|
|
132
|
+
--uni-now-bg: #E5ECFC;
|
|
133
|
+
--uni-next: #6E6959;
|
|
134
|
+
--uni-next-bg: #EFECE1;
|
|
135
|
+
--uni-rail-track: #D6D1BF;
|
|
136
|
+
|
|
137
|
+
--uni-ink: var(--sw-color-text);
|
|
138
|
+
--uni-muted: #5F5C54;
|
|
139
|
+
--uni-on-solid: #ffffff;
|
|
140
|
+
--uni-step: 130ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
html[data-sw-theme="dark"] body {
|
|
144
|
+
--uni-page: #131110;
|
|
145
|
+
--uni-panel: #221F1A;
|
|
146
|
+
--uni-bar: #2A2620;
|
|
147
|
+
--uni-sunk: #1B1815;
|
|
148
|
+
--uni-line: #383327;
|
|
149
|
+
--uni-line-soft: #2C2821;
|
|
150
|
+
|
|
151
|
+
--uni-done: #57C98A;
|
|
152
|
+
--uni-done-bg: #17301F;
|
|
153
|
+
--uni-now-bg: #1C2946;
|
|
154
|
+
--uni-next: #9A9285;
|
|
155
|
+
--uni-next-bg: #2C2821;
|
|
156
|
+
--uni-rail-track: #322D25;
|
|
157
|
+
--uni-muted: var(--sw-color-text-muted);
|
|
158
|
+
--uni-on-solid: #131110;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
body[class*="sw-layout-"] {
|
|
162
|
+
max-width: 860px; margin: 0 auto;
|
|
163
|
+
padding: 26px 24px 120px;
|
|
164
|
+
background: var(--uni-page);
|
|
165
|
+
font-size: 16px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.uni-sr {
|
|
169
|
+
position: absolute; width: 1px; height: 1px;
|
|
170
|
+
margin: -1px; padding: 0; overflow: hidden;
|
|
171
|
+
clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
|
|
172
|
+
}
|
|
173
|
+
#app-container {
|
|
174
|
+
background: transparent; border: none; box-shadow: none;
|
|
175
|
+
padding: 0; margin: 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
body ::selection {
|
|
179
|
+
background: color-mix(in srgb, var(--uni-now) 24%, transparent);
|
|
180
|
+
color: var(--uni-ink);
|
|
181
|
+
}
|
|
182
|
+
body *:focus-visible {
|
|
183
|
+
outline: 2px solid var(--uni-now);
|
|
184
|
+
outline-offset: 2px;
|
|
185
|
+
border-radius: 4px;
|
|
186
|
+
}
|
|
187
|
+
body { scrollbar-width: thin; scrollbar-color: var(--uni-line) transparent; }
|
|
188
|
+
body ::-webkit-scrollbar { width: 11px; height: 11px; }
|
|
189
|
+
body ::-webkit-scrollbar-thumb {
|
|
190
|
+
background: var(--uni-line); border-radius: 6px;
|
|
191
|
+
border: 3px solid var(--uni-page);
|
|
192
|
+
}
|
|
193
|
+
body ::-webkit-scrollbar-track { background: transparent; }
|
|
194
|
+
|
|
195
|
+
.uni-i {
|
|
196
|
+
display: inline-block; width: 1em; height: 1em;
|
|
197
|
+
background-color: currentColor; flex: none;
|
|
198
|
+
-webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
|
|
199
|
+
-webkit-mask-position: center; mask-position: center;
|
|
200
|
+
-webkit-mask-size: contain; mask-size: contain;
|
|
201
|
+
}
|
|
202
|
+
.uni-i--check {
|
|
203
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.2 8.4l3.1 3.1 6.5-7'/%3E%3C/svg%3E");
|
|
204
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.2 8.4l3.1 3.1 6.5-7'/%3E%3C/svg%3E");
|
|
205
|
+
}
|
|
206
|
+
.uni-i--lock {
|
|
207
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3.3' y='6.9' width='9.4' height='6.6' rx='1.4'/%3E%3Cpath d='M5.6 6.9V5.1a2.4 2.4 0 0 1 4.8 0v1.8'/%3E%3C/svg%3E");
|
|
208
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3.3' y='6.9' width='9.4' height='6.6' rx='1.4'/%3E%3Cpath d='M5.6 6.9V5.1a2.4 2.4 0 0 1 4.8 0v1.8'/%3E%3C/svg%3E");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
body .sw-topbar {
|
|
212
|
+
border-bottom: none;
|
|
213
|
+
padding: 0; margin: 0 0 8px;
|
|
214
|
+
background: transparent;
|
|
215
|
+
}
|
|
216
|
+
body .sw-topbar-wordmark {
|
|
217
|
+
font-family: var(--sw-font-display);
|
|
218
|
+
font-size: 27px; font-weight: 600;
|
|
219
|
+
letter-spacing: -0.015em; text-transform: none;
|
|
220
|
+
color: var(--uni-ink);
|
|
221
|
+
}
|
|
222
|
+
body .sw-topbar-crumb,
|
|
223
|
+
body .sw-topbar-separator { font-size: 14px; color: var(--uni-muted); }
|
|
224
|
+
body .sw-topbar-crumb--active { color: var(--uni-ink); font-weight: 650; }
|
|
225
|
+
.uni-tagline {
|
|
226
|
+
display: block; max-width: 62ch;
|
|
227
|
+
font-size: 15px; line-height: 1.55; color: var(--uni-muted);
|
|
228
|
+
margin: 0 0 30px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
body .sw-theme-toggle__label { display: none; }
|
|
232
|
+
body .sw-theme-toggle__btn {
|
|
233
|
+
background: var(--uni-panel); border: 1px solid var(--uni-line);
|
|
234
|
+
border-radius: 7px; padding: 7px; box-shadow: none;
|
|
235
|
+
color: var(--uni-muted); line-height: 0;
|
|
236
|
+
transition: border-color var(--uni-step), color var(--uni-step);
|
|
237
|
+
}
|
|
238
|
+
body .sw-theme-toggle__btn:hover { border-color: var(--uni-now); color: var(--uni-now); }
|
|
239
|
+
body .sw-theme-toggle__icon {
|
|
240
|
+
display: inline-block; width: 16px; height: 16px; font-size: 0;
|
|
241
|
+
background-color: currentColor;
|
|
242
|
+
-webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
|
|
243
|
+
-webkit-mask-position: center; mask-position: center;
|
|
244
|
+
-webkit-mask-size: contain; mask-size: contain;
|
|
245
|
+
}
|
|
246
|
+
body .sw-theme-toggle__icon:nth-of-type(1) {
|
|
247
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M13.4 9.6A5.8 5.8 0 0 1 6.4 2.6a5.8 5.8 0 1 0 7 7z'/%3E%3C/svg%3E");
|
|
248
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M13.4 9.6A5.8 5.8 0 0 1 6.4 2.6a5.8 5.8 0 1 0 7 7z'/%3E%3C/svg%3E");
|
|
249
|
+
}
|
|
250
|
+
body .sw-theme-toggle__icon:nth-of-type(2) {
|
|
251
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='8' cy='8' r='3.1'/%3E%3Cpath d='M8 1.2v1.6M8 13.2v1.6M1.2 8h1.6M13.2 8h1.6M3.2 3.2l1.1 1.1M11.7 11.7l1.1 1.1M12.8 3.2l-1.1 1.1M4.3 11.7l-1.1 1.1'/%3E%3C/svg%3E");
|
|
252
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='8' cy='8' r='3.1'/%3E%3Cpath d='M8 1.2v1.6M8 13.2v1.6M1.2 8h1.6M13.2 8h1.6M3.2 3.2l1.1 1.1M11.7 11.7l1.1 1.1M12.8 3.2l-1.1 1.1M4.3 11.7l-1.1 1.1'/%3E%3C/svg%3E");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
body .sw-card.uni-course {
|
|
256
|
+
background: var(--uni-panel);
|
|
257
|
+
border: 1px solid var(--uni-line);
|
|
258
|
+
border-left: 1px solid var(--uni-line);
|
|
259
|
+
border-radius: 12px;
|
|
260
|
+
padding: 0; margin: 0 0 14px;
|
|
261
|
+
box-shadow: 0 1px 2px rgba(24, 23, 20, 0.05), 0 6px 18px -10px rgba(24, 23, 20, 0.16);
|
|
262
|
+
overflow: hidden;
|
|
263
|
+
}
|
|
264
|
+
html[data-sw-theme="dark"] body .sw-card.uni-course {
|
|
265
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 8px 22px -12px rgba(0, 0, 0, 0.6);
|
|
266
|
+
}
|
|
267
|
+
body .sw-card.uni-course--dormant {
|
|
268
|
+
background: transparent;
|
|
269
|
+
border-color: var(--uni-line);
|
|
270
|
+
box-shadow: none;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
body .sw-card-header.uni-course__bar {
|
|
274
|
+
display: flex; align-items: center; gap: 12px;
|
|
275
|
+
background: var(--uni-bar);
|
|
276
|
+
border-bottom: 1px solid var(--uni-line);
|
|
277
|
+
padding: 16px 22px; margin: 0;
|
|
278
|
+
}
|
|
279
|
+
body .uni-course--dormant .sw-card-header.uni-course__bar {
|
|
280
|
+
background: transparent; border-bottom: none;
|
|
281
|
+
padding: 13px 20px 0;
|
|
282
|
+
}
|
|
283
|
+
body .sw-card.uni-course--dormant { margin-bottom: 10px; }
|
|
284
|
+
.uni-course__dot {
|
|
285
|
+
width: 9px; height: 9px; border-radius: 50%; flex: none;
|
|
286
|
+
background: var(--uni-now);
|
|
287
|
+
box-shadow: 0 0 0 3.5px color-mix(in srgb, var(--uni-now) 20%, transparent);
|
|
288
|
+
}
|
|
289
|
+
.uni-course--dormant .uni-course__dot {
|
|
290
|
+
background: transparent; border: 1.5px solid var(--uni-next);
|
|
291
|
+
box-shadow: none;
|
|
292
|
+
}
|
|
293
|
+
body.sw-theme-doc h2.uni-course__name {
|
|
294
|
+
font-family: var(--sw-font-body);
|
|
295
|
+
font-size: 20px; font-weight: 700; line-height: 1.2;
|
|
296
|
+
letter-spacing: -0.012em; color: var(--uni-ink);
|
|
297
|
+
margin: 0; padding: 0; border-bottom: none; flex: 1 1 auto;
|
|
298
|
+
}
|
|
299
|
+
body.sw-theme-doc .uni-course--dormant h2.uni-course__name {
|
|
300
|
+
font-size: 18px; font-weight: 650; color: var(--uni-muted);
|
|
301
|
+
}
|
|
302
|
+
.uni-chip {
|
|
303
|
+
font-size: 11.5px; font-weight: 700; letter-spacing: 0.07em;
|
|
304
|
+
text-transform: uppercase; padding: 5px 9px; border-radius: 5px;
|
|
305
|
+
flex: none; white-space: nowrap;
|
|
306
|
+
background: var(--uni-now-bg); color: var(--uni-now);
|
|
307
|
+
}
|
|
308
|
+
.uni-chip--done { background: var(--uni-done-bg); color: var(--uni-done); }
|
|
309
|
+
.uni-chip--soon {
|
|
310
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
311
|
+
background: var(--uni-next-bg); color: var(--uni-next);
|
|
312
|
+
}
|
|
313
|
+
.uni-chip--soon .uni-i { font-size: 12px; }
|
|
314
|
+
.uni-course__blurb {
|
|
315
|
+
display: block; font-size: 14.5px; line-height: 1.5;
|
|
316
|
+
color: var(--uni-muted); padding: 3px 20px 14px 41px;
|
|
317
|
+
max-width: 70ch;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
body .sw-card-body.uni-course__body { padding: 0; }
|
|
321
|
+
|
|
322
|
+
.uni-resume {
|
|
323
|
+
background: var(--uni-sunk);
|
|
324
|
+
border-bottom: 1px solid var(--uni-line);
|
|
325
|
+
padding: 22px 22px 20px;
|
|
326
|
+
}
|
|
327
|
+
.uni-resume__lead {
|
|
328
|
+
display: block;
|
|
329
|
+
font-family: var(--sw-font-display);
|
|
330
|
+
font-size: clamp(1.35rem, 3.4vw, 1.55rem);
|
|
331
|
+
font-weight: 600; line-height: 1.16; letter-spacing: -0.016em;
|
|
332
|
+
text-wrap: balance; color: var(--uni-ink); margin: 0 0 5px;
|
|
333
|
+
}
|
|
334
|
+
.uni-resume__sub {
|
|
335
|
+
display: block; max-width: 58ch;
|
|
336
|
+
font-size: 15px; line-height: 1.5; color: var(--uni-muted);
|
|
337
|
+
margin-bottom: 18px;
|
|
338
|
+
}
|
|
339
|
+
.uni-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
340
|
+
.uni-resume .uni-actions { margin-bottom: 20px; }
|
|
341
|
+
|
|
342
|
+
body .sw-button.uni-btn {
|
|
343
|
+
display: inline-flex; align-items: center; gap: 7px;
|
|
344
|
+
font-family: var(--sw-font-body);
|
|
345
|
+
font-size: 14px; font-weight: 650; line-height: 1;
|
|
346
|
+
padding: 10px 15px; border-radius: 7px;
|
|
347
|
+
border: 1px solid transparent; background: transparent;
|
|
348
|
+
box-shadow: none; text-transform: none;
|
|
349
|
+
color: var(--uni-ink); cursor: pointer;
|
|
350
|
+
transition: background var(--uni-step), color var(--uni-step),
|
|
351
|
+
border-color var(--uni-step), transform var(--uni-step);
|
|
352
|
+
}
|
|
353
|
+
body .sw-button.uni-btn .uni-i { font-size: 14px; }
|
|
354
|
+
body .sw-button.uni-btn--run {
|
|
355
|
+
background: var(--uni-now); color: var(--uni-on-solid);
|
|
356
|
+
padding: 12px 20px; font-size: 15px; font-weight: 650;
|
|
357
|
+
box-shadow: 0 1px 2px color-mix(in srgb, var(--uni-now) 30%, transparent);
|
|
358
|
+
}
|
|
359
|
+
body .sw-button.uni-btn--run:hover {
|
|
360
|
+
background: var(--sw-color-primary-hover); transform: translateY(-1px);
|
|
361
|
+
}
|
|
362
|
+
body .sw-button.uni-btn--outline {
|
|
363
|
+
border-color: var(--uni-line); background: var(--uni-panel);
|
|
364
|
+
color: var(--uni-ink);
|
|
365
|
+
}
|
|
366
|
+
body .sw-button.uni-btn--outline:hover { border-color: var(--uni-now); color: var(--uni-now); }
|
|
367
|
+
body .sw-button.uni-btn--quiet {
|
|
368
|
+
color: var(--uni-muted); font-weight: 600;
|
|
369
|
+
padding: 9px 12px; font-size: 13.5px;
|
|
370
|
+
}
|
|
371
|
+
body .sw-button.uni-btn--quiet:hover { color: var(--uni-now); background: var(--uni-now-bg); }
|
|
372
|
+
|
|
373
|
+
.uni-rail { display: flex; align-items: center; gap: 13px; }
|
|
374
|
+
.uni-rail__track { display: flex; align-items: center; gap: 5px; }
|
|
375
|
+
.uni-rail__seg { width: 30px; height: 6px; border-radius: 3px; background: var(--uni-rail-track); }
|
|
376
|
+
.uni-rail__seg--done { background: var(--uni-done); }
|
|
377
|
+
.uni-rail__seg--current { background: var(--uni-now); width: 40px; }
|
|
378
|
+
.uni-rail__label {
|
|
379
|
+
font-size: 13px; font-weight: 600; color: var(--uni-muted);
|
|
380
|
+
font-variant-numeric: tabular-nums;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.uni-step {
|
|
384
|
+
display: grid;
|
|
385
|
+
grid-template-columns: 28px 1fr auto;
|
|
386
|
+
align-items: center;
|
|
387
|
+
gap: 0 15px;
|
|
388
|
+
padding: 15px 22px;
|
|
389
|
+
border-bottom: 1px solid var(--uni-line-soft);
|
|
390
|
+
transition: background var(--uni-step);
|
|
391
|
+
}
|
|
392
|
+
.uni-step:last-child { border-bottom: none; }
|
|
393
|
+
.uni-step__mark {
|
|
394
|
+
width: 28px; height: 28px; border-radius: 50%;
|
|
395
|
+
display: flex; align-items: center; justify-content: center;
|
|
396
|
+
font-size: 13.5px; font-weight: 700;
|
|
397
|
+
font-variant-numeric: tabular-nums;
|
|
398
|
+
}
|
|
399
|
+
.uni-step--done .uni-step__mark {
|
|
400
|
+
background: var(--uni-done); color: var(--uni-on-solid);
|
|
401
|
+
}
|
|
402
|
+
.uni-step--done .uni-step__mark .uni-i { font-size: 16px; }
|
|
403
|
+
.uni-step--current .uni-step__mark {
|
|
404
|
+
background: var(--uni-now); color: var(--uni-on-solid);
|
|
405
|
+
box-shadow: 0 0 0 4px color-mix(in srgb, var(--uni-now) 18%, transparent);
|
|
406
|
+
}
|
|
407
|
+
.uni-step--todo .uni-step__mark {
|
|
408
|
+
background: transparent; color: var(--uni-next);
|
|
409
|
+
border: 1.5px solid var(--uni-next);
|
|
410
|
+
}
|
|
411
|
+
body.sw-theme-doc h3.uni-step__title {
|
|
412
|
+
font-family: var(--sw-font-body);
|
|
413
|
+
font-size: 17px; font-weight: 650; line-height: 1.28;
|
|
414
|
+
letter-spacing: -0.008em; color: var(--uni-ink);
|
|
415
|
+
margin: 0 0 3px; padding: 0; border-bottom: none;
|
|
416
|
+
}
|
|
417
|
+
.uni-step__payoff {
|
|
418
|
+
display: block; font-size: 14.5px; line-height: 1.45;
|
|
419
|
+
color: var(--uni-muted); max-width: 54ch;
|
|
420
|
+
}
|
|
421
|
+
.uni-step--current { background: var(--uni-now-bg); }
|
|
422
|
+
body.sw-theme-doc .uni-step--done h3.uni-step__title {
|
|
423
|
+
color: var(--uni-muted); font-weight: 600;
|
|
424
|
+
}
|
|
425
|
+
.uni-step__actions { display: flex; align-items: center; gap: 4px; }
|
|
426
|
+
|
|
427
|
+
/* The row a Run/Repeat click just targeted, expanded in place --
|
|
428
|
+
grid-column spans every track (the row's own 3-column grid otherwise
|
|
429
|
+
auto-places a 4th child back into column 1, the 28px mark column).
|
|
430
|
+
Indented to 43px (28px mark + 15px gap) so it lines up under the
|
|
431
|
+
title/payoff text, not the mark. */
|
|
432
|
+
.uni-step__expect {
|
|
433
|
+
grid-column: 1 / -1;
|
|
434
|
+
margin: 2px 0 4px 43px;
|
|
435
|
+
padding-top: 10px;
|
|
436
|
+
border-top: 1px dashed var(--uni-line-soft);
|
|
437
|
+
}
|
|
438
|
+
.uni-step__expect-label {
|
|
439
|
+
display: block;
|
|
440
|
+
font-size: 12px; font-weight: 700; letter-spacing: 0.06em;
|
|
441
|
+
text-transform: uppercase; color: var(--uni-muted);
|
|
442
|
+
margin-bottom: 6px;
|
|
443
|
+
}
|
|
444
|
+
@media (max-width: 620px) {
|
|
445
|
+
.uni-step__expect { grid-column: 1 / -1; margin-left: 0; }
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* A row's full Details expansion -- why it matters, the prompt, the
|
|
449
|
+
payoff checklist, Mark done + a next hint. Single-mode: this replaces
|
|
450
|
+
the old separate step screen outright, so it reuses that screen's own
|
|
451
|
+
content classes (.uni-label, .uni-prose, .uni-promptbox, .uni-payoff)
|
|
452
|
+
rather than a second copy of them. Same grid-column / indent trick as
|
|
453
|
+
.uni-step__expect, above, since it lives in the same 3-column row. */
|
|
454
|
+
.uni-step__expansion {
|
|
455
|
+
grid-column: 1 / -1;
|
|
456
|
+
margin: 4px 0 4px 43px;
|
|
457
|
+
padding-top: 14px;
|
|
458
|
+
border-top: 1px dashed var(--uni-line-soft);
|
|
459
|
+
}
|
|
460
|
+
.uni-step__expansion > .uni-label:first-child { margin-top: 0; }
|
|
461
|
+
.uni-step__expansion-foot {
|
|
462
|
+
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
|
|
463
|
+
margin-top: 22px;
|
|
464
|
+
}
|
|
465
|
+
@media (max-width: 620px) {
|
|
466
|
+
.uni-step__expansion { grid-column: 1 / -1; margin-left: 0; }
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/* Report band for the last Run/Repeat click -- what the driver did, or
|
|
470
|
+
why it refused to send. Sits between the resume band and the step
|
|
471
|
+
rows so it's directly under the button that produced it. */
|
|
472
|
+
.uni-run-notice {
|
|
473
|
+
background: var(--uni-now-bg);
|
|
474
|
+
border-bottom: 1px solid var(--uni-line);
|
|
475
|
+
border-left: 3px solid var(--uni-now);
|
|
476
|
+
padding: 15px 22px 16px;
|
|
477
|
+
}
|
|
478
|
+
/* Only one distinction matters visually -- it went out, or it didn't --
|
|
479
|
+
so a status added later styles itself correctly by construction. */
|
|
480
|
+
.uni-run-notice--degraded {
|
|
481
|
+
background: var(--uni-next-bg);
|
|
482
|
+
border-left-color: var(--uni-next);
|
|
483
|
+
}
|
|
484
|
+
.uni-run-notice--sent { border-left-color: var(--uni-done); background: var(--uni-done-bg); }
|
|
485
|
+
.uni-run-notice__msg {
|
|
486
|
+
display: block; max-width: 66ch;
|
|
487
|
+
font-size: 14.5px; line-height: 1.5; font-weight: 600;
|
|
488
|
+
color: var(--uni-ink); margin: 0;
|
|
489
|
+
}
|
|
490
|
+
.uni-run-notice__hint {
|
|
491
|
+
display: block; max-width: 66ch;
|
|
492
|
+
font-size: 13.5px; line-height: 1.5; color: var(--uni-muted);
|
|
493
|
+
margin-top: 10px;
|
|
494
|
+
}
|
|
495
|
+
.uni-run-notice .sw-code-block { margin: 12px 0 0; }
|
|
496
|
+
|
|
497
|
+
.uni-divider {
|
|
498
|
+
display: flex; align-items: center; gap: 14px;
|
|
499
|
+
margin: 38px 0 16px;
|
|
500
|
+
}
|
|
501
|
+
.uni-divider__label {
|
|
502
|
+
font-size: 15px; font-weight: 650; color: var(--uni-ink);
|
|
503
|
+
letter-spacing: -0.005em; flex: none;
|
|
504
|
+
}
|
|
505
|
+
.uni-divider__rule { height: 1px; background: var(--uni-line); flex: 1 1 auto; }
|
|
506
|
+
|
|
507
|
+
.uni-note {
|
|
508
|
+
margin-top: 30px; padding: 16px 20px;
|
|
509
|
+
background: var(--uni-sunk);
|
|
510
|
+
border: 1px solid var(--uni-line-soft);
|
|
511
|
+
border-radius: 10px;
|
|
512
|
+
font-size: 14.5px; line-height: 1.6; color: var(--uni-muted);
|
|
513
|
+
max-width: 74ch;
|
|
514
|
+
}
|
|
515
|
+
.uni-note code {
|
|
516
|
+
font-family: var(--sw-font-mono); font-size: 13.5px;
|
|
517
|
+
background: var(--uni-panel); border: 1px solid var(--uni-line);
|
|
518
|
+
border-radius: 4px; padding: 2px 6px; color: var(--uni-ink);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/* The all-done recap -- what the resume band's congratulation opens
|
|
522
|
+
onto. Same card, same body; the step rows below are untouched. */
|
|
523
|
+
.uni-recap {
|
|
524
|
+
padding: 4px 22px 22px;
|
|
525
|
+
border-bottom: 1px solid var(--uni-line);
|
|
526
|
+
}
|
|
527
|
+
.uni-recap > .uni-label:first-child { margin-top: 0; }
|
|
528
|
+
.uni-recap__foot {
|
|
529
|
+
display: flex; align-items: center; gap: 14px;
|
|
530
|
+
margin-top: 22px; flex-wrap: wrap;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/* The quiet "start over" escape hatch on the course list itself, for
|
|
534
|
+
anyone resetting before finishing. Hidden once all_done, where the
|
|
535
|
+
recap above offers the same action at its natural place instead. */
|
|
536
|
+
.uni-reset { margin-top: 12px; text-align: right; }
|
|
537
|
+
|
|
538
|
+
/* ---- Step Details expansion (single-mode: inline on a course-list row,
|
|
539
|
+
not a separate screen -- these classes are what the old step screen's
|
|
540
|
+
content used, kept as-is and reused by the expansion body below). --- */
|
|
541
|
+
.uni-label {
|
|
542
|
+
display: block;
|
|
543
|
+
font-size: 15px; font-weight: 650; color: var(--uni-ink);
|
|
544
|
+
letter-spacing: -0.005em; margin: 32px 0 11px;
|
|
545
|
+
}
|
|
546
|
+
.uni-prose { max-width: 66ch; font-size: 16px; line-height: 1.6; color: var(--uni-ink); }
|
|
547
|
+
.uni-prose p { margin: 0 0 0.8em; }
|
|
548
|
+
.uni-prose p:last-child { margin-bottom: 0; }
|
|
549
|
+
|
|
550
|
+
.uni-promptbox {
|
|
551
|
+
border: 1px solid var(--uni-line); border-radius: 9px;
|
|
552
|
+
background: var(--uni-sunk); overflow: hidden; margin: 30px 0 18px;
|
|
553
|
+
}
|
|
554
|
+
.uni-promptbox__bar {
|
|
555
|
+
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
556
|
+
background: var(--uni-bar); border-bottom: 1px solid var(--uni-line);
|
|
557
|
+
padding: 9px 15px;
|
|
558
|
+
font-size: 12px; font-weight: 700; letter-spacing: 0.06em;
|
|
559
|
+
text-transform: uppercase; color: var(--uni-muted);
|
|
560
|
+
}
|
|
561
|
+
.uni-prompt {
|
|
562
|
+
display: block;
|
|
563
|
+
font-family: var(--sw-font-mono);
|
|
564
|
+
font-size: 13px; line-height: 1.65;
|
|
565
|
+
white-space: pre-wrap; color: var(--uni-ink);
|
|
566
|
+
padding: 16px 17px; overflow-x: auto;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.uni-payoff ul { list-style: none; padding: 0; margin: 0; max-width: 64ch; }
|
|
570
|
+
/* A hanging indent (padding-left + absolutely positioned check), not a
|
|
571
|
+
grid row -- a grid item split every inline child of the <li> onto its
|
|
572
|
+
own auto-placed row (`.uni-payoff` items come from `md`, so a step
|
|
573
|
+
whose text has any inline markdown -- a `code` span, say -- produces
|
|
574
|
+
more than one child), and the auto-placed rows landed in the 20px
|
|
575
|
+
icon column, wrapping that text one character per line. Position
|
|
576
|
+
keeps the check glyph out of the flow entirely, so the <li> is back to
|
|
577
|
+
one ordinary block wrapping however many inline children it has. */
|
|
578
|
+
.uni-payoff li {
|
|
579
|
+
position: relative;
|
|
580
|
+
font-size: 15px; line-height: 1.5; color: var(--uni-ink);
|
|
581
|
+
padding: 7px 0 7px 31px;
|
|
582
|
+
}
|
|
583
|
+
.uni-payoff li::before {
|
|
584
|
+
content: ""; position: absolute; left: 0; top: 11px;
|
|
585
|
+
width: 15px; height: 15px;
|
|
586
|
+
background-color: var(--uni-done);
|
|
587
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.2 8.4l3.1 3.1 6.5-7'/%3E%3C/svg%3E");
|
|
588
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.2 8.4l3.1 3.1 6.5-7'/%3E%3C/svg%3E");
|
|
589
|
+
-webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
|
|
590
|
+
-webkit-mask-size: contain; mask-size: contain;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/* The hint text next to Mark done, inside a row's expansion footer
|
|
594
|
+
(.uni-step__expansion-foot, above) -- "Unlocks step N+1." or "That is
|
|
595
|
+
the whole course." Also used, with .uni-foot__spacer, by the
|
|
596
|
+
completion recap's own footer (.uni-recap__foot) -- kept here since
|
|
597
|
+
the old step-screen-only .uni-foot container is gone, but these two
|
|
598
|
+
were never step-screen-exclusive. */
|
|
599
|
+
.uni-foot__hint { font-size: 14px; color: var(--uni-muted); }
|
|
600
|
+
.uni-foot__spacer { flex: 1 1 auto; }
|
|
601
|
+
|
|
602
|
+
@media (max-width: 700px) {
|
|
603
|
+
body[class*="sw-layout-"] { padding-bottom: 150px; }
|
|
604
|
+
}
|
|
605
|
+
@media (max-width: 620px) {
|
|
606
|
+
.uni-step { grid-template-columns: 28px 1fr; row-gap: 10px; }
|
|
607
|
+
.uni-step > .uni-step__actions { grid-column: 2; justify-self: start; }
|
|
608
|
+
.uni-resume__lead { font-size: 1.3rem; }
|
|
609
|
+
.uni-recap__foot .uni-foot__spacer { display: none; }
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
@media (prefers-reduced-motion: reduce) {
|
|
613
|
+
body * { transition-duration: 0.01ms !important; }
|
|
614
|
+
}
|
|
615
|
+
CSS
|
|
616
|
+
|
|
617
|
+
_body = proc do
|
|
618
|
+
use_theme :doc
|
|
619
|
+
use_layout :default
|
|
620
|
+
use_stylesheet _css
|
|
621
|
+
|
|
622
|
+
# Without this marker, the adapter's showFeedback() replaces the whole
|
|
623
|
+
# page with "✓ Submitted -- You can close this window" on the first click
|
|
624
|
+
# of ANY button (adapter/alpinejs.rb, the else branch of the
|
|
625
|
+
# #sw-canvas-continue lookup). That terminal screen is right for a
|
|
626
|
+
# one-shot form and catastrophic for a control panel meant to be clicked
|
|
627
|
+
# all session: it took out the entire course list on the first Run.
|
|
628
|
+
# With the marker, a click shows this brief message instead, and the
|
|
629
|
+
# listener's re-push swaps the real page back in.
|
|
630
|
+
canvas_continue message: "Working..."
|
|
631
|
+
|
|
632
|
+
steps = StreamWeaver::University::Course::GETTING_STARTED_STEPS
|
|
633
|
+
total = steps.size
|
|
634
|
+
progress = StreamWeaver::University::Progress.load
|
|
635
|
+
states = StreamWeaver::University::Canvas.step_states(progress, steps: steps)
|
|
636
|
+
done_count = progress.done_steps.count { |n| n <= total }
|
|
637
|
+
current_number = steps.find { |s| states[s[:number]] == :current }&.dig(:number)
|
|
638
|
+
all_done = done_count == total
|
|
639
|
+
# Single-mode: there is exactly one screen. Which row (if any) renders
|
|
640
|
+
# its full Details expansion inline comes straight off the ledger, same
|
|
641
|
+
# as done/last_run -- which is also what makes a deep link work, since
|
|
642
|
+
# whoever set this before the canvas was last (re)pushed gets that row
|
|
643
|
+
# auto-expanded on load with no navigation step of its own.
|
|
644
|
+
expanded_number = progress.expanded_step
|
|
645
|
+
# Hoisted above the resume band (not just the run-notice band that reads
|
|
646
|
+
# it below) because the course-list row loop also reads it -- a Run or
|
|
647
|
+
# Repeat click expands that step's "What you should see" inline on its
|
|
648
|
+
# row, right there next to the button the user just pressed.
|
|
649
|
+
last_run = progress.last_run
|
|
650
|
+
# Mutually exclusive with last_run -- mark_done!/record_run! each clear
|
|
651
|
+
# the other, so at most one of the two notice bands below ever renders.
|
|
652
|
+
last_done = progress.last_done
|
|
653
|
+
|
|
654
|
+
topbar(wordmark: "StreamWeaver University") do
|
|
655
|
+
theme_toggle mode: :auto
|
|
656
|
+
end
|
|
657
|
+
phrase "Learn StreamWeaver by running it. Every step is a real app that appears " \
|
|
658
|
+
"in this pane while you stay in your terminal. Your progress is saved as you go, " \
|
|
659
|
+
"so you can close this and pick up right where you left off, even after a reboot -- " \
|
|
660
|
+
"reset any time from the link at the bottom of the list.",
|
|
661
|
+
class: "uni-tagline"
|
|
662
|
+
|
|
663
|
+
# ---- Getting Started: open, and holding its own state --------------------
|
|
664
|
+
card(depth: :elevated, class: "uni-course") do
|
|
665
|
+
card_header(class: "uni-course__bar") do
|
|
666
|
+
div(class: "uni-course__dot") {}
|
|
667
|
+
header2 "Getting Started", class: "uni-course__name"
|
|
668
|
+
chip_label = if all_done
|
|
669
|
+
"Complete"
|
|
670
|
+
elsif done_count.zero?
|
|
671
|
+
"Not started"
|
|
672
|
+
else
|
|
673
|
+
"In progress"
|
|
674
|
+
end
|
|
675
|
+
phrase chip_label, class: "uni-chip#{' uni-chip--done' if all_done}"
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
card_body(class: "uni-course__body") do
|
|
679
|
+
div(class: "uni-resume") do
|
|
680
|
+
if all_done
|
|
681
|
+
phrase "All five steps done.", class: "uni-resume__lead"
|
|
682
|
+
phrase "Nice work -- repeat any step below, or look at what's next on the shelf.",
|
|
683
|
+
class: "uni-resume__sub"
|
|
684
|
+
elsif done_count.zero?
|
|
685
|
+
phrase "Start with step 1.", class: "uni-resume__lead"
|
|
686
|
+
phrase steps.first[:payoff], class: "uni-resume__sub"
|
|
687
|
+
else
|
|
688
|
+
current_step = steps.find { |s| s[:number] == current_number }
|
|
689
|
+
phrase "Pick up at step #{current_number}.", class: "uni-resume__lead"
|
|
690
|
+
phrase current_step[:payoff], class: "uni-resume__sub"
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
div(class: "uni-actions") do
|
|
694
|
+
run_number = current_number || 1
|
|
695
|
+
unless all_done
|
|
696
|
+
hero_label = StreamWeaver::University::Canvas.run_label(progress, last_run, run_number)
|
|
697
|
+
button "#{hero_label} step #{run_number}", id: "hero-run-#{run_number}",
|
|
698
|
+
class: "uni-btn uni-btn--run"
|
|
699
|
+
end
|
|
700
|
+
if done_count.positive? && !all_done
|
|
701
|
+
repeat_number = [run_number - 1, 1].max
|
|
702
|
+
button "Repeat step #{repeat_number}", id: "hero-repeat-#{repeat_number}",
|
|
703
|
+
class: "uni-btn uni-btn--quiet"
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
div(class: "uni-rail") do
|
|
708
|
+
div(class: "uni-rail__track") do
|
|
709
|
+
steps.each do |step|
|
|
710
|
+
seg_class = case states[step[:number]]
|
|
711
|
+
when :done then "uni-rail__seg uni-rail__seg--done"
|
|
712
|
+
when :current then "uni-rail__seg uni-rail__seg--current"
|
|
713
|
+
else "uni-rail__seg"
|
|
714
|
+
end
|
|
715
|
+
div(class: seg_class) {}
|
|
716
|
+
end
|
|
717
|
+
end
|
|
718
|
+
phrase "#{done_count} of #{total} done", class: "uni-rail__label"
|
|
719
|
+
end
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
# Mark-done's confirmation -- "Step N done -- pick up at step N+1."
|
|
723
|
+
# -- reusing the same green/--sent band a successful Run gets.
|
|
724
|
+
# Checked first: mark_done! clears last_run, so the two never both
|
|
725
|
+
# have something to say, but last_done is the more specific read
|
|
726
|
+
# when it's present. Rendered BEFORE the completion recap below on
|
|
727
|
+
# purpose: this is the report of the click that just happened
|
|
728
|
+
# (design-spec's "report band ... sits directly under the button
|
|
729
|
+
# that produced it"), and on the step that finishes the course,
|
|
730
|
+
# that click's own "that's the whole course!" line should read as
|
|
731
|
+
# the lead-in to the recap, not a footnote under it.
|
|
732
|
+
if last_done
|
|
733
|
+
div(class: "uni-run-notice uni-run-notice--sent") do
|
|
734
|
+
phrase StreamWeaver::University::Canvas.mark_done_message(last_done['step']),
|
|
735
|
+
class: "uni-run-notice__msg"
|
|
736
|
+
end
|
|
737
|
+
# What the last Run/Repeat click actually did. On anything but a
|
|
738
|
+
# clean send the prompt itself is offered here with a copy button --
|
|
739
|
+
# the degraded path's whole experience, and the fallback whenever the
|
|
740
|
+
# recorded worker session has gone away (driver-worker-runner
|
|
741
|
+
# criteria 4 and 5). The canvas never talks to iTerm; it reports what
|
|
742
|
+
# the driver wrote to the ledger.
|
|
743
|
+
elsif last_run
|
|
744
|
+
status = last_run['status'].to_s
|
|
745
|
+
run_step = last_run['step'].to_i
|
|
746
|
+
run_sent = StreamWeaver::University::Runner.sent?(status)
|
|
747
|
+
run_prompt = StreamWeaver::University::Course.prompt_for(run_step)
|
|
748
|
+
|
|
749
|
+
div(class: "uni-run-notice uni-run-notice--#{run_sent ? 'sent' : 'degraded'}") do
|
|
750
|
+
phrase StreamWeaver::University::Runner.message_for(status, run_step),
|
|
751
|
+
class: "uni-run-notice__msg"
|
|
752
|
+
if !run_sent && run_prompt
|
|
753
|
+
# The human gets the prompt as written -- line breaks and all.
|
|
754
|
+
# Only the wire gets Runner.one_line's collapsed form, because
|
|
755
|
+
# a clipboard paste has no keystroke-per-newline problem.
|
|
756
|
+
code_block run_prompt, lang: "text", copy: true
|
|
757
|
+
phrase "Copy it, then paste it into the terminal where your agent is " \
|
|
758
|
+
"running and press Enter.",
|
|
759
|
+
class: "uni-run-notice__hint"
|
|
760
|
+
end
|
|
761
|
+
end
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
# The completion recap -- what the resume band's "look at what's
|
|
765
|
+
# next on the shelf" line points at once every step is done. Sits
|
|
766
|
+
# inside this same course-list render (not a separate screen) so
|
|
767
|
+
# "Run/Repeat stays available from the list" is true by
|
|
768
|
+
# construction: the step rows below are untouched.
|
|
769
|
+
if all_done
|
|
770
|
+
div(class: "uni-recap") do
|
|
771
|
+
phrase "What you learned", class: "uni-label"
|
|
772
|
+
md steps.map { |s| "- **Step #{s[:number]}:** #{s[:payoff]}" }.join("\n"),
|
|
773
|
+
class: "uni-payoff"
|
|
774
|
+
|
|
775
|
+
phrase "Where to go next", class: "uni-label"
|
|
776
|
+
md <<~MD, class: "uni-prose"
|
|
777
|
+
- Browse the reference docs in `docs/`.
|
|
778
|
+
- Run `streamweaver showcase` for a tour of every component.
|
|
779
|
+
- Read worked examples in `examples/`.
|
|
780
|
+
- Run `streamweaver tutorial` for the classic component-by-component walkthrough.
|
|
781
|
+
MD
|
|
782
|
+
|
|
783
|
+
div(class: "uni-recap__foot") do
|
|
784
|
+
phrase "Run or Repeat any step below to go through it again.",
|
|
785
|
+
class: "uni-foot__hint"
|
|
786
|
+
div(class: "uni-foot__spacer") {}
|
|
787
|
+
button "Reset course", id: "reset-course", class: "uni-btn uni-btn--quiet"
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
steps.each do |step|
|
|
793
|
+
number = step[:number]
|
|
794
|
+
state = states[number]
|
|
795
|
+
expanded = expanded_number == number
|
|
796
|
+
run_label = StreamWeaver::University::Canvas.run_label(progress, last_run, number)
|
|
797
|
+
div(class: "uni-step uni-step--#{state}") do
|
|
798
|
+
div(class: "uni-step__mark") do
|
|
799
|
+
if state == :done
|
|
800
|
+
phrase "", class: "uni-i uni-i--check"
|
|
801
|
+
phrase "Step #{number}, done", class: "uni-sr"
|
|
802
|
+
else
|
|
803
|
+
phrase number.to_s
|
|
804
|
+
phrase(state == :current ? "Step #{number}, current step" : "Step #{number}, not started",
|
|
805
|
+
class: "uni-sr")
|
|
806
|
+
end
|
|
807
|
+
end
|
|
808
|
+
div do
|
|
809
|
+
header3 step[:title], class: "uni-step__title"
|
|
810
|
+
phrase step[:payoff], class: "uni-step__payoff"
|
|
811
|
+
end
|
|
812
|
+
div(class: "uni-step__actions") do
|
|
813
|
+
case state
|
|
814
|
+
when :done
|
|
815
|
+
button "Repeat", id: "repeat-#{number}", class: "uni-btn uni-btn--quiet"
|
|
816
|
+
when :current
|
|
817
|
+
button run_label, id: "run-#{number}", class: "uni-btn uni-btn--outline"
|
|
818
|
+
button "Mark done", id: "mark-done-#{number}", class: "uni-btn uni-btn--quiet"
|
|
819
|
+
else
|
|
820
|
+
button run_label, id: "run-#{number}", class: "uni-btn uni-btn--quiet"
|
|
821
|
+
end
|
|
822
|
+
button expanded ? "Hide" : "Details", id: "view-#{number}", class: "uni-btn uni-btn--quiet"
|
|
823
|
+
end
|
|
824
|
+
# Expands exactly the row a Run/Repeat click just targeted, so
|
|
825
|
+
# "what should this do" is answered right there without opening
|
|
826
|
+
# Details -- last_run is what a click just wrote, and mark_done!
|
|
827
|
+
# clears it, so this can never point at a step that was actually
|
|
828
|
+
# finished instead of run. Skipped when the row's own full
|
|
829
|
+
# Details expansion (below) is already open: that one repeats
|
|
830
|
+
# "What you should see" itself, and showing both would say it
|
|
831
|
+
# twice.
|
|
832
|
+
if last_run && last_run['step'].to_i == number && !expanded
|
|
833
|
+
div(class: "uni-step__expect") do
|
|
834
|
+
phrase "What you should see", class: "uni-step__expect-label"
|
|
835
|
+
md StreamWeaver::University::Canvas.bullets(step[:what_you_should_see]),
|
|
836
|
+
class: "uni-payoff"
|
|
837
|
+
end
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
# Single-mode: "Details" expands THIS row in place -- why it
|
|
841
|
+
# matters, the prompt with Run/Copy, the payoff checklist, and a
|
|
842
|
+
# Mark done + next-step hint -- rather than navigating to a
|
|
843
|
+
# separate step screen. Expanding one row collapses any other
|
|
844
|
+
# (expanded_number holds at most one step), and there is nothing
|
|
845
|
+
# to get "back" from: Hide (the same button, relabeled) closes
|
|
846
|
+
# it in place.
|
|
847
|
+
if expanded
|
|
848
|
+
div(class: "uni-step__expansion") do
|
|
849
|
+
phrase "Why this matters", class: "uni-label"
|
|
850
|
+
md step[:why_it_matters], class: "uni-prose"
|
|
851
|
+
|
|
852
|
+
div(class: "uni-promptbox") do
|
|
853
|
+
div(class: "uni-promptbox__bar") do
|
|
854
|
+
phrase "The prompt your worker session receives"
|
|
855
|
+
end
|
|
856
|
+
phrase step[:prompt], class: "uni-prompt"
|
|
857
|
+
end
|
|
858
|
+
div(class: "uni-actions") do
|
|
859
|
+
button "Run in worker session", id: "run-#{number}", class: "uni-btn uni-btn--run"
|
|
860
|
+
copy_button "Copy prompt", text: step[:prompt],
|
|
861
|
+
copied_label: "Copied", class: "uni-btn uni-btn--outline"
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
phrase "What you should see", class: "uni-label"
|
|
865
|
+
md StreamWeaver::University::Canvas.bullets(step[:what_you_should_see]),
|
|
866
|
+
class: "uni-payoff"
|
|
867
|
+
|
|
868
|
+
div(class: "uni-step__expansion-foot") do
|
|
869
|
+
button "Mark step #{number} done", id: "mark-done-#{number}", class: "uni-btn uni-btn--outline"
|
|
870
|
+
if number < total
|
|
871
|
+
phrase "Unlocks step #{number + 1}.", class: "uni-foot__hint"
|
|
872
|
+
else
|
|
873
|
+
phrase "That is the whole course.", class: "uni-foot__hint"
|
|
874
|
+
end
|
|
875
|
+
end
|
|
876
|
+
end
|
|
877
|
+
end
|
|
878
|
+
end
|
|
879
|
+
end
|
|
880
|
+
end
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
# ---- The shelf: closed, dormant, no controls ------------------------------
|
|
884
|
+
div(class: "uni-divider") do
|
|
885
|
+
phrase "In the works", class: "uni-divider__label"
|
|
886
|
+
div(class: "uni-divider__rule") {}
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
StreamWeaver::University::Course::FUTURE_COURSES.each do |course|
|
|
890
|
+
card(depth: :recessed, class: "uni-course uni-course--dormant") do
|
|
891
|
+
card_header(class: "uni-course__bar") do
|
|
892
|
+
div(class: "uni-course__dot") {}
|
|
893
|
+
header2 course[:name], class: "uni-course__name"
|
|
894
|
+
div(class: "uni-chip uni-chip--soon") do
|
|
895
|
+
phrase "", class: "uni-i uni-i--lock"
|
|
896
|
+
phrase "Soon"
|
|
897
|
+
end
|
|
898
|
+
end
|
|
899
|
+
phrase course[:blurb], class: "uni-course__blurb"
|
|
900
|
+
end
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
md "Looking for the old component tour? Run `streamweaver tutorial` -- the classic " \
|
|
904
|
+
"walkthrough of every component. Older than these courses, and being refreshed.",
|
|
905
|
+
class: "uni-note"
|
|
906
|
+
|
|
907
|
+
# A second "Reset course" entry point, for anyone who wants to start
|
|
908
|
+
# over before finishing -- the recap screen (above, `all_done`) offers
|
|
909
|
+
# its own, so this one steps aside there rather than rendering the same
|
|
910
|
+
# id: "reset-course" button twice on one page.
|
|
911
|
+
unless all_done
|
|
912
|
+
div(class: "uni-reset") do
|
|
913
|
+
button "Reset course", id: "reset-course", class: "uni-btn uni-btn--quiet"
|
|
914
|
+
end
|
|
915
|
+
end
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
if respond_to?(:use_theme)
|
|
919
|
+
# canvas-push / canvas-read: self is already a StreamWeaver::App
|
|
920
|
+
instance_eval(&_body)
|
|
921
|
+
elsif __FILE__ == $PROGRAM_NAME
|
|
922
|
+
require_relative "../../stream_weaver"
|
|
923
|
+
app("StreamWeaver University") { instance_eval(&_body) }.run!
|
|
924
|
+
end
|