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,396 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: streamweaver-way
|
|
3
|
+
description: Use when building or changing an interactive StreamWeaver app or feature — inline editing, live search/filtering, per-row buttons, hover cards, infinite scroll, tabs, or any region slow enough to hold up the page. Encodes "The StreamWeaver Way": the conventions (fragments, defer:/lazy:, form_for, interactive-id keying, dev-loud/prod-self-heal) that make app code zero-JavaScript by default. For one-off mockups, diagrams, or dashboards to look at, use streamweaver-visual-companion instead; for long-form editorial documents, use streamweaver-doc-builder.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# The StreamWeaver Way
|
|
7
|
+
|
|
8
|
+
StreamWeaver is an *omakase* layer, not a widget kit: the backend and the frontend were
|
|
9
|
+
designed as one system, so the conventions below are the framework, not style advice.
|
|
10
|
+
Follow them and you write no JavaScript. Fight them and you write a lot.
|
|
11
|
+
|
|
12
|
+
**Read the laws. Drill into a recipe only when you're building that thing.**
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## The Laws
|
|
17
|
+
|
|
18
|
+
**1. Key by what is unique per position on the page, not by what the content is about.**
|
|
19
|
+
Every interactive element needs a server-dispatchable id, and StreamWeaver derives one
|
|
20
|
+
for you (label + block source location, with `-dup-N` auto-disambiguation on repeats), so
|
|
21
|
+
you rarely pass anything. Auto-derived ids are *position*-stable, not *content*-stable:
|
|
22
|
+
insert or delete an earlier row and every later suffix shifts. Reach for `key:` only when
|
|
23
|
+
identity must outlive reordering, filtering, or deletion — per-row Edit/Delete buttons,
|
|
24
|
+
per-row fragments. Then key it by the thing that is unique *per row on this page* (the
|
|
25
|
+
todo), never by what the row happens to display (its assignee) — two rows sharing an
|
|
26
|
+
assignee is the classic collision, and it fails silently: hovering the second card shows
|
|
27
|
+
the first one's data. Precedence is `id:` > `key:` > auto. `key:` takes stable scalars
|
|
28
|
+
only (String/Symbol/Integer; anything else raises). Never key by positional index.
|
|
29
|
+
|
|
30
|
+
**2. Deferred over hand-rolled timers.** A slow region gets `fragment(..., defer: true)`
|
|
31
|
+
so the shell ships now and the region lands later. Never an `every(...)` poll, never a
|
|
32
|
+
"loading" flag toggled by a second request you wrote yourself. Measured on one 1.5s
|
|
33
|
+
region: initial GET 0.018s instead of 1.5s.
|
|
34
|
+
|
|
35
|
+
**3. Lazy means visible.** `lazy: true` holds the fetch until the fragment is actually
|
|
36
|
+
*visible* — scrolled into view, or CSS flipping an ancestor out of `display: none`.
|
|
37
|
+
Hidden content costs exactly nothing, and it fetches exactly once. That interlock is what
|
|
38
|
+
buys you hover cards and infinite scroll with no scroll handler and no state counter.
|
|
39
|
+
|
|
40
|
+
**4. Dev loud, prod self-heals.** A stale action token gets a 409 + full-container
|
|
41
|
+
re-render that is byte-for-byte identical in production — the user never sees an error.
|
|
42
|
+
In development only, the same response prepends a dismissible `.sw-dev-fallback` overlay
|
|
43
|
+
naming the stale target and the likely cause. This is a deliberate inversion of Hotwire's
|
|
44
|
+
"Content missing": keep the debugging signal, don't charge the user for it. If you see
|
|
45
|
+
that overlay, your wiring is wrong — don't style it away.
|
|
46
|
+
|
|
47
|
+
**5. State your intent in DSL verbs. Zero custom JavaScript is the default, not an
|
|
48
|
+
aspiration.** All four benchmark features below run with no script tag, no inline JS, no
|
|
49
|
+
hand-written Alpine — only DSL verbs and CSS. If you're reaching for JavaScript, you have
|
|
50
|
+
almost certainly missed a verb. Do not touch adapter internals (`lib/stream_weaver/
|
|
51
|
+
adapter/*`) or emit raw `hx-*` attributes from app code; the verbs own those.
|
|
52
|
+
|
|
53
|
+
**6. Trilaws as design filters.** Run every feature through all three:
|
|
54
|
+
*Matt* — is it findable and digestible (progressive disclosure for the long tail)?
|
|
55
|
+
*Forrest* — zero friction plus a real perk (if it can be automatic, it must be)?
|
|
56
|
+
*Gloria* — is the default path the correct path (design for the brain you have, not the
|
|
57
|
+
one that remembers to pass the right option)? Law 1's auto-keying is Gloria's Law in
|
|
58
|
+
code: the default already disambiguates, so the silent-wrong-callback bug can't happen.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Core mechanics (everything below builds on these)
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
app 'My App' do
|
|
66
|
+
state[:count] ||= 0 # the whole block RE-RUNS on every interaction
|
|
67
|
+
text "Count: #{state[:count]}"
|
|
68
|
+
button('+') { |s| s[:count] += 1 }
|
|
69
|
+
end.run!
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**`fragment(name) { ... }`** marks a region that interactions *inside it* swap on their
|
|
73
|
+
own, instead of swapping the whole app container. Everything interactive rendered inside
|
|
74
|
+
a fragment is auto-scoped to it — that scoping is the whole mechanism the recipes use.
|
|
75
|
+
|
|
76
|
+
| Form | Fetch timing | Use for |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `fragment(:n) { }` | inline, with the page | scoping a swap to one region |
|
|
79
|
+
| `fragment(:n, defer: true) { }` | right after page load | slow regions off the critical path |
|
|
80
|
+
| `fragment(:n, lazy: true) { }` | first time it is visible | hover cards, infinite scroll, tab panels |
|
|
81
|
+
|
|
82
|
+
`lazy: true` implies `defer: true`. `placeholder:` accepts nothing (small spinner), a
|
|
83
|
+
String (rendered as text), or a Proc (run as DSL). **Give the placeholder height** — the
|
|
84
|
+
observer watches the wrapper, and a zero-area target never intersects.
|
|
85
|
+
|
|
86
|
+
**Three rules that apply to every deferred/lazy fragment:**
|
|
87
|
+
|
|
88
|
+
- The block is skipped on the shell render, so anything it *registers* (actions, timers)
|
|
89
|
+
isn't registered until the fetch. Keep registration outside deferred blocks.
|
|
90
|
+
- A **named**-action button (`button 'X', action: :foo`) inside one does not fire — its
|
|
91
|
+
token is minted outside the session's action manifest. Use a block button
|
|
92
|
+
(`button('X') { |s| ... }`), which dispatches by id and is unaffected.
|
|
93
|
+
- Each fetch re-runs its **ancestors'** blocks. Nest for staging, not to split one
|
|
94
|
+
expensive block into cheaper pieces.
|
|
95
|
+
|
|
96
|
+
**`form_for`** is the record-bound form primitive: seeds fields from a record, infers
|
|
97
|
+
create vs. update from record identity, coerces + validates on submit, calls
|
|
98
|
+
`store.create`/`store.update`, flashes, and PRGs. **Only the fields you declare are read
|
|
99
|
+
from the submitted state** — that allowlist is edit-safety by construction, not by
|
|
100
|
+
remembering to pass the right URL.
|
|
101
|
+
|
|
102
|
+
A **store** is any object answering `all`, `find(id)`, `create(attrs)`, `update(id, attrs)`,
|
|
103
|
+
`destroy(id)`. A plain module over an Array is enough to build all four recipes.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## The four benchmark recipes
|
|
108
|
+
|
|
109
|
+
These are the four Turbo Frames features from the learnhotwire.com course, in
|
|
110
|
+
StreamWeaver, with zero custom JavaScript. Recipe 1 defines the store; recipes 2-4 add
|
|
111
|
+
one method each to it, shown inline. Each is complete and runnable.
|
|
112
|
+
|
|
113
|
+
### 1. Inline editing (a row becomes a form in place)
|
|
114
|
+
|
|
115
|
+
One `fragment` per row, keyed by record id, branching on an `editing_id` in state. The
|
|
116
|
+
Edit button and the form submit both swap **just this row**, because both are rendered
|
|
117
|
+
inside that row's fragment.
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
require 'stream_weaver'
|
|
121
|
+
|
|
122
|
+
module TodoStore
|
|
123
|
+
@todos = [{ id: '1', title: 'Buy milk', completed: false },
|
|
124
|
+
{ id: '2', title: 'Ship it', completed: true }]
|
|
125
|
+
class << self
|
|
126
|
+
def all = @todos
|
|
127
|
+
def find(id) = @todos.find { |t| t[:id] == id.to_s }
|
|
128
|
+
def create(attrs) = (id = (@todos.size + 1).to_s; @todos << { id: id, **attrs }; id)
|
|
129
|
+
def update(id, attrs) = (t = find(id)) ? (t.merge!(attrs); true) : false
|
|
130
|
+
def destroy(id) = (@todos.reject! { |t| t[:id] == id.to_s }; true)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Declare ONLY the editable field. A submit through this form cannot reach
|
|
135
|
+
# :completed -- the allowlist is enforced by construction.
|
|
136
|
+
TITLE_ONLY = [StreamWeaver::Field.new(:title, :string, {})].freeze
|
|
137
|
+
|
|
138
|
+
app 'My Todos' do
|
|
139
|
+
TodoStore.all.each do |todo|
|
|
140
|
+
fragment("todo-#{todo[:id]}") do
|
|
141
|
+
if state[:editing_id].to_s == todo[:id]
|
|
142
|
+
form_for(
|
|
143
|
+
store: TodoStore,
|
|
144
|
+
fields: TITLE_ONLY,
|
|
145
|
+
name: :"todo_#{todo[:id]}_form",
|
|
146
|
+
record: todo,
|
|
147
|
+
on_success: ->(_id) { state[:editing_id] = nil }
|
|
148
|
+
) { submit_label 'Save' }
|
|
149
|
+
button('Cancel', style: :secondary, key: "cancel-#{todo[:id]}") { |s| s[:editing_id] = nil }
|
|
150
|
+
else
|
|
151
|
+
hstack spacing: :sm do
|
|
152
|
+
text "#{todo[:completed] ? '☑' : '☐'} #{todo[:title]}"
|
|
153
|
+
button('Edit', style: :secondary, key: "edit-#{todo[:id]}") { |s| s[:editing_id] = todo[:id] }
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end.run!
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Gotchas**
|
|
162
|
+
|
|
163
|
+
- Per-row buttons **must** carry `key:` (Law 1). Without it the derived ids are
|
|
164
|
+
position-stable only, and a delete or a filter shifts them under you.
|
|
165
|
+
- `on_success:` is **required** for standalone `form_for` (no `resource` block to fall
|
|
166
|
+
back to for the post-submit transition).
|
|
167
|
+
- Standalone `form_for` derives its flash noun by stripping `_form` from `name:`, so
|
|
168
|
+
`:todo_1_form` flashes `"Todo_1 updated."` — cosmetic, known, unfixed.
|
|
169
|
+
- Inside a `resource` block the shorter form works and the transition is free:
|
|
170
|
+
`form_for :todo, record: todo do submit_label 'Save' end`.
|
|
171
|
+
|
|
172
|
+
### 2. Scoped live search (submit-as-you-type)
|
|
173
|
+
|
|
174
|
+
`text_field` auto-submits on input — no Stimulus controller, no debounce code. The only
|
|
175
|
+
decision is **where the field lives**, and it is a real trade-off:
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
# Add to TodoStore. The blank guard is load-bearing -- see gotchas.
|
|
179
|
+
def TodoStore.search(query)
|
|
180
|
+
return all if query.to_s.strip.empty?
|
|
181
|
+
needle = query.to_s.downcase
|
|
182
|
+
all.select { |t| t[:title].downcase.include?(needle) }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
app 'Search' do
|
|
186
|
+
# B -- field INSIDE the fragment: scoped morph, fewer bytes, focus and caret
|
|
187
|
+
# survive. Cost: the field re-renders itself on every keystroke.
|
|
188
|
+
fragment(:results) do
|
|
189
|
+
text_field :query, placeholder: 'Filter todos…'
|
|
190
|
+
results = TodoStore.search(state[:query])
|
|
191
|
+
text "#{results.length} of #{TodoStore.all.length} todos"
|
|
192
|
+
results.first(8).each { |t| text "• #{t[:title]}" }
|
|
193
|
+
end
|
|
194
|
+
end.run!
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Gotchas**
|
|
198
|
+
|
|
199
|
+
- **`text_field :query, updates: :results` is silently accepted and silently ignored.**
|
|
200
|
+
This is the natural transliteration of Rails' `data: {turbo_frame: :todos}` and it looks
|
|
201
|
+
right; `updates:` is read only by `button` and `clickable`. There is no
|
|
202
|
+
`data-turbo-frame` equivalent for inputs yet.
|
|
203
|
+
- Consequently an input can target only its own enclosing fragment or `#app-container`.
|
|
204
|
+
**A field placed outside the results fragment swaps the whole container** — it works and
|
|
205
|
+
filters correctly, it just costs the whole body every keystroke. Prefer arrangement B
|
|
206
|
+
above unless you specifically need the field to never re-render itself.
|
|
207
|
+
- Guard the blank query in your store (`return all if query.to_s.strip.empty?`) or
|
|
208
|
+
clearing the box makes the whole list vanish.
|
|
209
|
+
- Search is a GET filter, so put the query in the URL via `route_with` if you want it
|
|
210
|
+
shareable.
|
|
211
|
+
|
|
212
|
+
### 3. Lazy hover cards (CSS reveals it; revealing it is what fetches it)
|
|
213
|
+
|
|
214
|
+
Nothing on the page fetches anything until the pointer arrives. Three lines of CSS plus
|
|
215
|
+
`lazy: true` — the two halves interlock.
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
# Each todo carries a :user_id; UserStore is any lookup, deliberately slow here
|
|
219
|
+
# so "eager or lazy?" is observable.
|
|
220
|
+
module UserStore
|
|
221
|
+
USERS = { 'u1' => { id: 'u1', name: 'Ada Lovelace', role: 'Engineering' },
|
|
222
|
+
'u2' => { id: 'u2', name: 'Alan Turing', role: 'Research' } }.freeze
|
|
223
|
+
def self.find(id) = USERS[id]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
app 'Directory' do
|
|
227
|
+
use_stylesheet <<~CSS
|
|
228
|
+
.hovercard { position: relative; display: inline-block; cursor: help; }
|
|
229
|
+
.hovercard .hovercard-panel { display: none; position: absolute; top: 1.6em; left: 0; z-index: 50; min-width: 15rem; }
|
|
230
|
+
.hovercard:hover .hovercard-panel { display: block; }
|
|
231
|
+
CSS
|
|
232
|
+
|
|
233
|
+
TodoStore.all.each do |todo|
|
|
234
|
+
user = UserStore.find(todo[:user_id])
|
|
235
|
+
div class: 'hovercard' do
|
|
236
|
+
text user[:name]
|
|
237
|
+
div class: 'hovercard-panel' do
|
|
238
|
+
# Keyed by the TODO, not the user -- see the gotcha below.
|
|
239
|
+
fragment(:"hovercard_#{todo[:id]}", lazy: true, placeholder: 'Loading…') do
|
|
240
|
+
card do
|
|
241
|
+
header3 user[:name]
|
|
242
|
+
text user[:role]
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end.run!
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**Gotchas**
|
|
252
|
+
|
|
253
|
+
- **Key by the todo, not the user.** Two todos sharing an assignee would emit the same
|
|
254
|
+
fragment name twice; hovering the second shows the first one's card, silently. This is
|
|
255
|
+
Law 1 in its purest form.
|
|
256
|
+
- `display: none` means no fetch, for as long as it lasts — that is the guarantee that
|
|
257
|
+
makes six cards cost zero on page load. Don't hide the panel with `visibility: hidden`
|
|
258
|
+
or `opacity: 0`; those stay visible to the observer and every card fetches eagerly.
|
|
259
|
+
- It fetches **once**. Hover away and back and the landed content stays put.
|
|
260
|
+
- To verify laziness: `document.querySelectorAll('.sw-fragment-lazy').length` counts
|
|
261
|
+
fragments that have *not* fetched yet.
|
|
262
|
+
|
|
263
|
+
### 4. Russian-doll infinite scroll (nested lazy fragments)
|
|
264
|
+
|
|
265
|
+
**The page the reader is on renders in the shell; only the NEXT page is lazy.** Page N's
|
|
266
|
+
rows go out inline, and its block ends by declaring page N+1 as a lazy fragment whose own
|
|
267
|
+
block recurses. Page N+1 does not exist in the DOM — and never fetches — until page N has
|
|
268
|
+
been scrolled past. That recursion is the entire pagination logic: no scroll handler, no
|
|
269
|
+
page counter in state, no "Load more" button.
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
# One page's slice -- never everything-so-far. Empty return terminates the chain.
|
|
273
|
+
TodoStore::PER_PAGE = 10
|
|
274
|
+
def TodoStore.page(number) = all.drop(([number.to_i, 1].max - 1) * PER_PAGE).first(PER_PAGE)
|
|
275
|
+
|
|
276
|
+
def scroll_page(number)
|
|
277
|
+
# THIS page renders now, in whatever response we're already writing.
|
|
278
|
+
TodoStore.page(number).each { |t| card { text t[:title] } }
|
|
279
|
+
|
|
280
|
+
# Only the NEXT page is deferred. The guard is load-bearing: without it the
|
|
281
|
+
# chain dead-ends in an empty fragment that fetches nothing.
|
|
282
|
+
nxt = number + 1
|
|
283
|
+
unless TodoStore.page(nxt).empty?
|
|
284
|
+
fragment(:"page_#{nxt}", lazy: true, placeholder: 'Loading…') do
|
|
285
|
+
scroll_page(nxt) # recursion lives INSIDE the lazy fragment
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
app('Feed') { scroll_page(1) }.run!
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**Gotchas**
|
|
294
|
+
|
|
295
|
+
- **Do not wrap page 1 in the lazy fragment.** Writing
|
|
296
|
+
`fragment(:"page_#{number}", lazy: true) { rows; recurse }` and calling it with `1` is
|
|
297
|
+
the tempting shape and it is wrong: the block is skipped on the shell render (see the
|
|
298
|
+
three rules above), so the served HTML contains no rows at all — just a placeholder. It
|
|
299
|
+
*looks* like it works only because page 1's placeholder sits in the initial viewport, so
|
|
300
|
+
the observer fires immediately. A lazy fragment already in the viewport fetches at once,
|
|
301
|
+
which is exactly what hides this mistake. Render the current page, lazy the next one —
|
|
302
|
+
that is also what Turbo does, and it is what keeps the shell honest for crawlers, for a
|
|
303
|
+
reader with no JavaScript, and for anyone reading the served HTML.
|
|
304
|
+
- Have the store return **one page's slice** (`all.drop((n - 1) * PER_PAGE).first(PER_PAGE)`),
|
|
305
|
+
never everything-so-far. Each response should be a constant size; if payloads grow
|
|
306
|
+
per page, you're re-sending rows.
|
|
307
|
+
- Each fetch re-runs its ancestors' blocks, so keep per-page work cheap. Nested ids are
|
|
308
|
+
`parent--child`: page 2's fragment is declared beside page 1's rows, so it is
|
|
309
|
+
`sw-frag-page-2`; page 3's is declared inside page 2's block, giving
|
|
310
|
+
`sw-frag-page-2--page-3`.
|
|
311
|
+
- **Inside a tab, the lazy fragment goes INSIDE a `tab` block, never beside one** at the
|
|
312
|
+
top level of a `tabs` block — a non-tab child there shifts every panel index. Route tabs
|
|
313
|
+
(`tabs :view, url: true`) render inactive panels as `display: none`, so a lazy fragment
|
|
314
|
+
in one waits until that tab is opened:
|
|
315
|
+
|
|
316
|
+
```ruby
|
|
317
|
+
tabs :view, url: true do
|
|
318
|
+
tab('Summary') { text summary_line }
|
|
319
|
+
tab('Revenue') { fragment(:revenue, lazy: true) { revenue_table } }
|
|
320
|
+
end
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Anti-patterns (each one has bitten someone)
|
|
326
|
+
|
|
327
|
+
- **`defer { ... }`** — the bare verb is not the deferred primitive. It now raises and
|
|
328
|
+
names the real one: `fragment(:name, defer: true)`.
|
|
329
|
+
- **`every(...)` to refresh a slow region** — that's Law 2. Use `defer:`.
|
|
330
|
+
- **Named-action buttons inside a deferred/lazy fragment** — they don't fire. Block
|
|
331
|
+
buttons.
|
|
332
|
+
- **Keying by positional index** — `key: i` is exactly the identity that reordering
|
|
333
|
+
destroys. Key by record id.
|
|
334
|
+
- **`text "**bold**"`** — `text` does not render markdown. Use `md`.
|
|
335
|
+
- **`spacer` / `divider`** — don't exist. `div(style: 'height:24px')`.
|
|
336
|
+
- **`<button onclick="location.href=...">`** — bypasses htmx, full reload, loses state.
|
|
337
|
+
Use `link_to` / `nav_item` with a real `href`.
|
|
338
|
+
- **Assuming port 4567** — StreamWeaver finds a free port and prints the real URL. Read
|
|
339
|
+
stdout.
|
|
340
|
+
|
|
341
|
+
**Catch keying bugs before they ship:** `strict_ids` turns a derived-id collision from a
|
|
342
|
+
warning into a hard failure. It raises in development and test, and degrades to a warning
|
|
343
|
+
in production (a live page shouldn't 500 over an id the framework already disambiguated).
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
StreamWeaver::App.new('My App', strict_ids: true) { ... } # per app
|
|
347
|
+
StreamWeaver.strict_ids = true # global
|
|
348
|
+
SW_STRICT_IDS=1 ruby my_app.rb # env, e.g. in CI
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Going deeper (pointers only — these are not summarized here)
|
|
354
|
+
|
|
355
|
+
**The narrated twin.** `docs/tutorials/the-streamweaver-way.md` — "Build My Todos:
|
|
356
|
+
the StreamWeaver Way" builds these same four features step by step, why-first, each
|
|
357
|
+
step naming the law it obeys, the gotcha it avoids, and the measured number. This
|
|
358
|
+
skill prescribes; the tutorial explains. Point a human at the tutorial. Also runs as
|
|
359
|
+
a `:doc`-theme app: `SW_NO_OPEN=1 ruby examples/tutorials/streamweaver_way_tutorial.rb`.
|
|
360
|
+
|
|
361
|
+
**The worked example set.** `examples/my_todos/` is all four recipes above in one running
|
|
362
|
+
app, annotated with the Rails mechanism each one mirrors. Boot it with
|
|
363
|
+
`SW_NO_OPEN=1 ruby examples/my_todos/my_todos.rb`, and `SW_HOVERCARD_DELAY=1.5` to feel
|
|
364
|
+
the difference lazy makes.
|
|
365
|
+
|
|
366
|
+
**`llms.txt` (aliased as `docs/for_llms.md`)** — the full DSL reference. Sections that
|
|
367
|
+
extend this skill, by name:
|
|
368
|
+
|
|
369
|
+
| Section | What it adds |
|
|
370
|
+
|---|---|
|
|
371
|
+
| Interactive IDs and keying | Full precedence rules, `strict_ids`, `clickable` |
|
|
372
|
+
| Record-Bound Forms (`form_for`) | Every option, in-block DSL, validation semantics |
|
|
373
|
+
| Turbo Frame-Style Inline Editing with `form_for` | The `resource`-based variant of recipe 1 |
|
|
374
|
+
| Deferred Fragments (`fragment ..., defer: true`) | Placeholders, nesting, export behavior |
|
|
375
|
+
| Lazy Fragments (`fragment ..., lazy: true`) | Both guarantees, and all three recipes |
|
|
376
|
+
| Dev Loud, Prod Self-Heal | What the overlay names, and what it can't detect |
|
|
377
|
+
| Resource Scaffolding | `resource` CRUD, named-route helpers |
|
|
378
|
+
| Route Tabs (`tabs url: true`) | Deep-linkable tabs, URL authority ordering |
|
|
379
|
+
| Repo Conventions | The full anti-pattern list |
|
|
380
|
+
|
|
381
|
+
**Reference docs.** `docs/resource-dsl.md` (`resource` + `form_for` full reference),
|
|
382
|
+
`docs/routing.md` (**read Common Pitfalls before any app with more than a couple of
|
|
383
|
+
routes**), `docs/crud-patterns.md`, `docs/components_reference.md`.
|
|
384
|
+
|
|
385
|
+
**Decision docs — read when you want the *why*, or are extending the primitive.**
|
|
386
|
+
`docs/research/streamweaver-way-spike-findings.md` (what broke and how each break was
|
|
387
|
+
closed — the source of every gotcha above),
|
|
388
|
+
`docs/research/2026-08-22-lazy-fragments-trigger-decision.md` (why `intersect once` and
|
|
389
|
+
not `revealed`),
|
|
390
|
+
`docs/research/2026-08-22-learnhotwire-syllabus-coverage.md` (Hotwire feature-by-feature
|
|
391
|
+
coverage: what exists, what's next),
|
|
392
|
+
`docs/streamweaver-frontend-vision.md` (the strategic thesis).
|
|
393
|
+
|
|
394
|
+
**Sibling skills.** `streamweaver-visual-companion` for mockups, diagrams, and dashboards
|
|
395
|
+
you just want to look at; `streamweaver-doc-builder` for long-form editorial documents.
|
|
396
|
+
This skill is for apps people interact with.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: visual-plan
|
|
3
|
+
description: Use before starting any non-trivial implementation task — renders a live canvas showing the implementation map, open decisions, and wireframes so the user can sign off before code is written
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Visual Plan
|
|
7
|
+
|
|
8
|
+
Pre-flight planning via StreamWeaver canvas. Push a live plan showing what will change and why, flag open decisions, and block for sign-off on anything that needs user input — all before touching any code.
|
|
9
|
+
|
|
10
|
+
## When to Use
|
|
11
|
+
|
|
12
|
+
Before any task where:
|
|
13
|
+
- Multiple files will change
|
|
14
|
+
- An architecture decision is open (database choice, API shape, component structure)
|
|
15
|
+
- A UI surface is being added or significantly reworked
|
|
16
|
+
- The user said "show me the plan first"
|
|
17
|
+
|
|
18
|
+
Skip for trivial edits (one-line fix, rename, config change) — overhead exceeds value.
|
|
19
|
+
|
|
20
|
+
## !! DO NOT LAUNCH STANDALONE SERVERS PER QUESTION !!
|
|
21
|
+
|
|
22
|
+
Same rule as the visual companion: **never** run `ruby app.rb` or `streamweaver <file.rb>` for each planning update. Use `canvas-push` to update a single persistent window throughout the session.
|
|
23
|
+
|
|
24
|
+
## Starting a Plan Session
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Open a named canvas panel — opens in iTerm2 split pane, or browser tab elsewhere
|
|
28
|
+
streamweaver panel plan
|
|
29
|
+
|
|
30
|
+
# Push the initial plan content
|
|
31
|
+
streamweaver canvas-push plan <<'RUBY'
|
|
32
|
+
header1 "Implementation Plan"
|
|
33
|
+
|
|
34
|
+
implementation_map(files: [
|
|
35
|
+
{ path: "lib/foo/bar.rb", note: "Add #process method for new pipeline step" },
|
|
36
|
+
{ path: "lib/foo/pipeline.rb", note: "Wire bar into pipeline chain" },
|
|
37
|
+
{ path: "spec/foo/bar_spec.rb", note: "Cover happy path and nil input edge case" }
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
decision(question: "Should bar be stateless or hold config?") do
|
|
41
|
+
option(id: :stateless, label: "Stateless",
|
|
42
|
+
detail: "Simple — take config as args each call. Easy to test.",
|
|
43
|
+
recommended: true)
|
|
44
|
+
option(id: :stateful, label: "Stateful",
|
|
45
|
+
detail: "Holds config in instance. Needed if config is expensive to build.")
|
|
46
|
+
end
|
|
47
|
+
RUBY
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Tell the user: "Check the plan canvas at [url]. Let me know in the terminal if you want to adjust anything."
|
|
51
|
+
|
|
52
|
+
## The Planning Loop
|
|
53
|
+
|
|
54
|
+
1. Push the full plan canvas (files + decisions + wireframes)
|
|
55
|
+
2. Tell the user the canvas URL, end your turn
|
|
56
|
+
3. User reviews and responds in the terminal
|
|
57
|
+
4. If a decision needs sign-off: use `canvas-wait` (see below) to block until they click
|
|
58
|
+
5. Once approved: start implementation; push progress updates to the same canvas
|
|
59
|
+
|
|
60
|
+
**The canvas stays open while you implement.** Push updates as files change.
|
|
61
|
+
|
|
62
|
+
## DSL Reference
|
|
63
|
+
|
|
64
|
+
### `implementation_map` — what files change and why
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
implementation_map(files: [
|
|
68
|
+
{ path: "lib/payments/processor.rb", note: "Add retry logic for network errors" },
|
|
69
|
+
{ path: "lib/payments/errors.rb", note: "New RetryableError class" },
|
|
70
|
+
{ path: "spec/payments/processor_spec.rb", note: "Cover retry behavior" }
|
|
71
|
+
])
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`note:` should explain the why, not the what ("Add retry logic" not "modify method").
|
|
75
|
+
|
|
76
|
+
### `decision` — open architecture choices
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
decision(question: "Which caching layer?") do
|
|
80
|
+
option(id: :redis, label: "Redis", detail: "Shared, fast, extra infra dep", recommended: true)
|
|
81
|
+
option(id: :memory, label: "In-memory", detail: "Zero dep, lost on restart")
|
|
82
|
+
option(id: :none, label: "No cache", detail: "Simplest — acceptable if <50ms")
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Mark the option you'd choose `recommended: true`.
|
|
87
|
+
|
|
88
|
+
### `callout(:decision)` — tradeoff flags inline
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
callout(variant: :decision, title: "Auth approach affects test setup") do
|
|
92
|
+
text "If we use JWT here, the test helpers need to generate tokens — adds ~30 lines to spec/support."
|
|
93
|
+
text "If we use session cookies, existing test helpers work as-is."
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Use for tradeoffs that don't need a full option matrix — a constraint, an implicit choice, a flag for the user.
|
|
98
|
+
|
|
99
|
+
### `callout(:risk)` — things that could go wrong
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
callout(variant: :risk, title: "Migration is irreversible") do
|
|
103
|
+
text "DROP COLUMN is non-reversible in production without a new migration."
|
|
104
|
+
text "Confirm data is not needed before proceeding."
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Use for anything that could bite silently: data loss, breaking changes, non-idempotent operations, external API calls with side effects.
|
|
109
|
+
|
|
110
|
+
### `wireframe` — UI mockups for new surfaces
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
wireframe(surface: :browser) do
|
|
114
|
+
<<~HTML
|
|
115
|
+
<div class="wf-card">
|
|
116
|
+
<h2>Invoice #1042</h2>
|
|
117
|
+
<p class="wf-muted">Due 2026-07-01</p>
|
|
118
|
+
<button class="primary">Pay now</button>
|
|
119
|
+
</div>
|
|
120
|
+
HTML
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Surfaces: `:browser`, `:desktop`, `:mobile`, `:phone`, `:tablet`, `:popover`, `:card`, `:widget`, `:panel`.
|
|
125
|
+
|
|
126
|
+
Use for UI stories where the layout matters. Skip if the change is purely logic or data.
|
|
127
|
+
|
|
128
|
+
### Combining components
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
streamweaver canvas-push plan <<'RUBY'
|
|
132
|
+
header1 "Plan: Add Invoice Detail Page"
|
|
133
|
+
|
|
134
|
+
implementation_map(files: [
|
|
135
|
+
{ path: "app/controllers/invoices_controller.rb", note: "Add #show action" },
|
|
136
|
+
{ path: "app/views/invoices/show.html.erb", note: "New view" },
|
|
137
|
+
{ path: "spec/controllers/invoices_controller_spec.rb", note: "Cover show + 404" }
|
|
138
|
+
])
|
|
139
|
+
|
|
140
|
+
decision(question: "Render total in controller or view?") do
|
|
141
|
+
option(id: :controller, label: "Controller", detail: "Testable, consistent", recommended: true)
|
|
142
|
+
option(id: :view, label: "View helper", detail: "Less boilerplate if reused")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
callout(variant: :risk, title: "Invoice data includes PII") do
|
|
146
|
+
text "Confirm auth check on #show — anonymous access must 403, not 404."
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
wireframe(surface: :browser) do
|
|
150
|
+
'<h1>Invoice #1042</h1><p class="wf-muted">$240.00 due</p><button class="primary">Pay</button>'
|
|
151
|
+
end
|
|
152
|
+
RUBY
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Blocking for Sign-Off with `canvas-wait`
|
|
156
|
+
|
|
157
|
+
When a decision needs explicit user approval before you proceed, add interactive controls and wait:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
streamweaver canvas-push plan <<'RUBY'
|
|
161
|
+
header2 "Approve to continue"
|
|
162
|
+
radio_group :db_choice, ["PostgreSQL (recommended)", "SQLite", "Needs more discussion"]
|
|
163
|
+
button "Approve and proceed"
|
|
164
|
+
RUBY
|
|
165
|
+
|
|
166
|
+
# Blocks until user clicks the button — prints JSON with their selection
|
|
167
|
+
result=$(streamweaver canvas-wait plan)
|
|
168
|
+
echo "User chose: $result"
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`canvas-wait` returns JSON: `{"type":"action","element":"button","value":"Approve and proceed","state":{"db_choice":"PostgreSQL (recommended)"}}`.
|
|
172
|
+
|
|
173
|
+
Use when the decision changes the implementation path, is hard to reverse, or the user should weigh in before code is written. Skip for low-stakes choices — don't block when the user can redirect in the next turn.
|
|
174
|
+
|
|
175
|
+
## Updating the Canvas During Implementation
|
|
176
|
+
|
|
177
|
+
Once approved, keep the canvas live as work progresses:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# After each file is written
|
|
181
|
+
streamweaver canvas-push plan <<'RUBY'
|
|
182
|
+
header1 "Implementation Plan — In Progress"
|
|
183
|
+
|
|
184
|
+
implementation_map(files: [
|
|
185
|
+
{ path: "lib/foo/bar.rb", note: "[done]" },
|
|
186
|
+
{ path: "lib/foo/pipeline.rb", note: "[wip]" },
|
|
187
|
+
{ path: "spec/foo/bar_spec.rb", note: "[pending]" }
|
|
188
|
+
])
|
|
189
|
+
RUBY
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The user sees live status without having to ask. The plan evolves from "what will change" to "what has changed."
|
|
193
|
+
|
|
194
|
+
## Known Gotchas
|
|
195
|
+
|
|
196
|
+
- **`undefined method` errors on new components** — the canvas bridge may be an orphaned process from a previous session running older code. Fix: `streamweaver canvas-stop && streamweaver panel plan`. The fresh bridge picks up the latest gem code.
|
|
197
|
+
- `spacer` and `divider` don't exist — use `div(style: "height:Npx")`
|
|
198
|
+
- `text` does not render markdown — use `md` for bold/italic/links
|
|
199
|
+
- `decision` block scope: only `option(...)` calls are valid inside it — other DSL methods throw
|
|
200
|
+
- StreamWeaver auto-selects an available port — capture the URL from stdout, don't hardcode 4567
|
|
201
|
+
- Canvas sessions default to `:fluid` (full-width) — good for wide implementation maps; use `--layout=default` for a narrower card
|