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,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'rbconfig'
|
|
6
|
+
|
|
7
|
+
module StreamWeaver
|
|
8
|
+
# Client module for interacting with the StreamWeaver service.
|
|
9
|
+
# Include this in tools that load apps via the service API.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# class MyTool
|
|
13
|
+
# include StreamWeaver::ServiceClient
|
|
14
|
+
#
|
|
15
|
+
# def run_example(file_path)
|
|
16
|
+
# result = load_app_via_service(file_path, source: "my_tool")
|
|
17
|
+
# open_in_browser(result[:url]) if result[:ok]
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
module ServiceClient
|
|
21
|
+
# Get the service port from PID file or default
|
|
22
|
+
#
|
|
23
|
+
# @return [Integer] The port number
|
|
24
|
+
def service_port
|
|
25
|
+
info = Service.read_pid_file
|
|
26
|
+
info ? info[:port] : Service::DEFAULT_PORT
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Open a URL in the default browser (OS-aware)
|
|
30
|
+
#
|
|
31
|
+
# @param url [String] The URL to open
|
|
32
|
+
# Guarded here, at the root -- see StreamWeaver::CLI.open_browser and
|
|
33
|
+
# SinatraApp.open_browser for the same guard on the other two
|
|
34
|
+
# independent browser-open implementations in this codebase.
|
|
35
|
+
def open_in_browser(url)
|
|
36
|
+
return if ENV['SW_NO_OPEN']
|
|
37
|
+
|
|
38
|
+
case RbConfig::CONFIG['host_os']
|
|
39
|
+
when /darwin|mac os/
|
|
40
|
+
system('open', url)
|
|
41
|
+
when /linux|bsd/
|
|
42
|
+
system('xdg-open', url)
|
|
43
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
|
44
|
+
system('start', url)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Load an app via the service API
|
|
49
|
+
#
|
|
50
|
+
# @param file_path [String] Path to the Ruby file defining the app
|
|
51
|
+
# @param source [String] Source identifier for tracking (e.g., "tutorial", "examples_browser")
|
|
52
|
+
# @param name [String, nil] Optional display name
|
|
53
|
+
# @return [Hash] Result with :ok, :app_id, :url, :aliased_url, or :error
|
|
54
|
+
def load_app_via_service(file_path, source:, name: nil)
|
|
55
|
+
expanded_path = File.expand_path(file_path)
|
|
56
|
+
|
|
57
|
+
uri = URI("http://localhost:#{service_port}/load-app")
|
|
58
|
+
params = { file_path: expanded_path, source: source }
|
|
59
|
+
params[:name] = name if name
|
|
60
|
+
|
|
61
|
+
response = Net::HTTP.post_form(uri, params)
|
|
62
|
+
result = JSON.parse(response.body)
|
|
63
|
+
|
|
64
|
+
if result['success']
|
|
65
|
+
{
|
|
66
|
+
ok: true,
|
|
67
|
+
app_id: result['app_id'],
|
|
68
|
+
name: result['name'],
|
|
69
|
+
url: "http://localhost:#{service_port}#{result['url']}",
|
|
70
|
+
aliased_url: result['aliased_url'] ? "http://localhost:#{service_port}#{result['aliased_url']}" : nil
|
|
71
|
+
}
|
|
72
|
+
else
|
|
73
|
+
{ ok: false, error: result['error'] }
|
|
74
|
+
end
|
|
75
|
+
rescue Errno::ECONNREFUSED
|
|
76
|
+
{ ok: false, error: "Service not running" }
|
|
77
|
+
rescue => e
|
|
78
|
+
{ ok: false, error: e.message }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Remove an app from the service
|
|
82
|
+
#
|
|
83
|
+
# @param app_id [String] The app ID to remove
|
|
84
|
+
# @return [Hash] Result with :ok key for consistency
|
|
85
|
+
def remove_app_via_service(app_id)
|
|
86
|
+
uri = URI("http://localhost:#{service_port}/remove-app")
|
|
87
|
+
response = Net::HTTP.post_form(uri, { app_id: app_id })
|
|
88
|
+
result = JSON.parse(response.body)
|
|
89
|
+
{ ok: result['success'] }
|
|
90
|
+
rescue Errno::ECONNREFUSED, SocketError, Net::OpenTimeout
|
|
91
|
+
{ ok: false }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Clear all apps from a specific source
|
|
95
|
+
#
|
|
96
|
+
# @param source [String] The source identifier to clear
|
|
97
|
+
def clear_source_via_service(source)
|
|
98
|
+
uri = URI("http://localhost:#{service_port}/clear-source")
|
|
99
|
+
Net::HTTP.post_form(uri, { source: source })
|
|
100
|
+
rescue Errno::ECONNREFUSED, SocketError, Net::OpenTimeout
|
|
101
|
+
# Service might be down, that's ok during cleanup
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rack/session/abstract/id'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module StreamWeaver
|
|
7
|
+
# ── Session state filters ─────────────────────────────────────────────────
|
|
8
|
+
# Each store type owns its filter logic. Cookie store guards the 4KB limit;
|
|
9
|
+
# file store passes state through unchanged.
|
|
10
|
+
|
|
11
|
+
module SessionStore
|
|
12
|
+
class Base
|
|
13
|
+
# Flash is one-shot (flash-prg.md §1): visible in the response that set
|
|
14
|
+
# it, never persisted for the next request. Excluding it here -- at the
|
|
15
|
+
# single point every filter subclass already funnels through -- is what
|
|
16
|
+
# makes that true; the caller's `state` object itself is untouched, so
|
|
17
|
+
# the render that already read it for *this* response is unaffected.
|
|
18
|
+
FLASH_KEY = :_flash
|
|
19
|
+
|
|
20
|
+
def filter(state, app_transient: [], scope_names: [])
|
|
21
|
+
raise NotImplementedError, "#{self.class}#filter not implemented"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# Treat nil and empty string as blank — both are equivalent to "unset" for form fields.
|
|
27
|
+
def blank?(v)
|
|
28
|
+
v.nil? || v == ""
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Recurse exactly one level into hash values that are registered scopes
|
|
32
|
+
# (App#scope_names) so a blank field buried inside e.g. state[:person_form]
|
|
33
|
+
# gets stripped too, same as a blank top-level key — FAC-P3.1 §7. Arbitrary
|
|
34
|
+
# user-managed hash values that were never declared via scope/form/resource
|
|
35
|
+
# are left opaque, exactly as before.
|
|
36
|
+
def strip_scoped_blanks(state, scope_names)
|
|
37
|
+
return state if scope_names.empty?
|
|
38
|
+
state.each_with_object({}) do |(k, v), result|
|
|
39
|
+
result[k] = (scope_names.include?(k) && v.is_a?(Hash)) ? v.reject { |_, sv| blank?(sv) } : v
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class FileStore < Base
|
|
45
|
+
# No size limit, but strip blank values — they'll be re-initialized by ||= on next load.
|
|
46
|
+
def filter(state, app_transient: [], scope_names: [])
|
|
47
|
+
strip_scoped_blanks(state, scope_names).reject { |k, v| blank?(v) || k == FLASH_KEY }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class CookieStore < Base
|
|
52
|
+
HARD_TRANSIENT = %i[code_content current_file_path examples _deck_state].freeze
|
|
53
|
+
LIMIT_BYTES = 4096
|
|
54
|
+
WARN_THRESHOLD = 3072 # warn at 75% capacity
|
|
55
|
+
|
|
56
|
+
def filter(state, app_transient: [], scope_names: [])
|
|
57
|
+
filtered = strip_scoped_blanks(state, scope_names).reject do |k, v|
|
|
58
|
+
blank?(v) ||
|
|
59
|
+
k == FLASH_KEY ||
|
|
60
|
+
HARD_TRANSIENT.include?(k) ||
|
|
61
|
+
app_transient.include?(k) ||
|
|
62
|
+
k.to_s.end_with?('_edited_code')
|
|
63
|
+
end
|
|
64
|
+
size = JSON.dump(filtered).bytesize
|
|
65
|
+
if size > WARN_THRESHOLD
|
|
66
|
+
$stderr.puts "[SW] WARNING: cookie session #{size}B / #{LIMIT_BYTES}B limit — " \
|
|
67
|
+
"set SW_SESSION_STORE=file to avoid silent data loss"
|
|
68
|
+
end
|
|
69
|
+
filtered
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.build(store_name)
|
|
74
|
+
case store_name.to_s
|
|
75
|
+
when 'file' then FileStore.new
|
|
76
|
+
when 'cookie' then CookieStore.new
|
|
77
|
+
else raise ArgumentError, "Unknown SW_SESSION_STORE '#{store_name}'. Use 'file' or 'cookie'."
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# ── File-backed Rack session middleware ────────────────────────────────────
|
|
83
|
+
# Stores one Marshal file per session under SW_SESSION_DIR.
|
|
84
|
+
# Compatible with Rack::Session::Abstract::PersistedSecure (same interface as Pool).
|
|
85
|
+
|
|
86
|
+
class FileSession < Rack::Session::Abstract::PersistedSecure
|
|
87
|
+
DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS.merge(
|
|
88
|
+
drop: false,
|
|
89
|
+
allow_fallback: true,
|
|
90
|
+
expire_after: 86_400
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def initialize(app, options = {})
|
|
94
|
+
@session_dir = options.delete(:path) { ::File.join(Dir.home, '.config', 'stream_weaver', 'sessions') }
|
|
95
|
+
@expire_after = options.fetch(:expire_after, DEFAULT_OPTIONS[:expire_after])
|
|
96
|
+
@allow_fallback = options.fetch(:allow_fallback, DEFAULT_OPTIONS[:allow_fallback])
|
|
97
|
+
@mutex = Mutex.new
|
|
98
|
+
FileUtils.mkdir_p(@session_dir)
|
|
99
|
+
super
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def generate_sid(*args, use_mutex: true)
|
|
103
|
+
loop do
|
|
104
|
+
sid = super(*args)
|
|
105
|
+
exists = use_mutex ? @mutex.synchronize { file_exists_for?(sid) } : file_exists_for?(sid)
|
|
106
|
+
break sid unless exists
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def find_session(req, sid)
|
|
111
|
+
@mutex.synchronize do
|
|
112
|
+
session = sid && fetch_session_data(sid)
|
|
113
|
+
unless session
|
|
114
|
+
sid = generate_sid(use_mutex: false)
|
|
115
|
+
session = {}
|
|
116
|
+
# Don't write here — let write_session handle it so we don't create orphan files
|
|
117
|
+
end
|
|
118
|
+
[sid, session]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def write_session(req, session_id, new_session, options)
|
|
123
|
+
@mutex.synchronize do
|
|
124
|
+
write_file(session_id, new_session)
|
|
125
|
+
session_id
|
|
126
|
+
end
|
|
127
|
+
rescue => e
|
|
128
|
+
$stderr.puts "[SW] FileSession write error: #{e}"
|
|
129
|
+
false
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def delete_session(req, session_id, options)
|
|
133
|
+
@mutex.synchronize do
|
|
134
|
+
remove_file(session_id)
|
|
135
|
+
unless options[:drop]
|
|
136
|
+
sid = generate_sid(use_mutex: false)
|
|
137
|
+
write_file(sid, {})
|
|
138
|
+
sid
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def session_path(sid)
|
|
146
|
+
id = sid.respond_to?(:private_id) ? sid.private_id : sid.to_s
|
|
147
|
+
::File.join(@session_dir, "session_#{id.gsub(/[^a-zA-Z0-9\-]/, '_')}")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def file_exists_for?(sid)
|
|
151
|
+
::File.exist?(session_path(sid))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Renamed from load_session to avoid overriding Persisted#load_session(req)
|
|
155
|
+
def fetch_session_data(sid)
|
|
156
|
+
session = read_session_file(sid.private_id)
|
|
157
|
+
session ||= read_session_file(sid.public_id) if @allow_fallback && sid.respond_to?(:public_id)
|
|
158
|
+
session
|
|
159
|
+
rescue => e
|
|
160
|
+
$stderr.puts "[SW] FileSession load error: #{e}"
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def read_session_file(raw_id)
|
|
165
|
+
path = ::File.join(@session_dir, "session_#{raw_id.to_s.gsub(/[^a-zA-Z0-9\-]/, '_')}")
|
|
166
|
+
return nil unless ::File.exist?(path)
|
|
167
|
+
if ::File.mtime(path) < Time.now - @expire_after
|
|
168
|
+
::File.delete(path)
|
|
169
|
+
return nil
|
|
170
|
+
end
|
|
171
|
+
Marshal.load(::File.binread(path))
|
|
172
|
+
rescue => e
|
|
173
|
+
$stderr.puts "[SW] FileSession read error for #{raw_id}: #{e}"
|
|
174
|
+
nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def write_file(sid, session)
|
|
178
|
+
::File.binwrite(session_path(sid), Marshal.dump(session))
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def remove_file(sid)
|
|
182
|
+
path = session_path(sid)
|
|
183
|
+
::File.delete(path) if ::File.exist?(path)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: streamweaver-canvas-safe
|
|
3
|
+
description: Use before building a canvas doc, pushing a Save-as-doc that will be reopened later, or running `streamweaver export` — tells you which components stay honest with no backend behind them (canvas-read, an exported file) versus which ones need the live bridge or a real server, so the doc you hand someone doesn't quietly stop working the moment it leaves the live canvas
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# StreamWeaver Canvas-Safe
|
|
7
|
+
|
|
8
|
+
A canvas doc gets rendered in more places than the live canvas it was written for: `canvas-read` opens it with no app session, `streamweaver export` writes it out as a static file with no server at all. Same `.rb`, three runtimes. This skill is the compact version of that story — read it before you reach for a component you haven't checked, and drop into a reference file only when you're actually about to use one of the components it covers.
|
|
9
|
+
|
|
10
|
+
**Full source of truth:** `docs/frontend-only.md` (compatibility table + mechanisms) and `docs/research/frontend-only-matrix.md` (the ~95-row long tail with source lines and repro commands). This skill is a shorter, action-oriented cut of both — when in doubt, they win.
|
|
11
|
+
|
|
12
|
+
## The three contexts
|
|
13
|
+
|
|
14
|
+
| Ctx | Name | Behind it | htmx? | Alpine? |
|
|
15
|
+
|---|---|---|---|---|
|
|
16
|
+
| A | Live canvas | `Canvas::BridgeServer` + WebSocket, an agent may be listening on `canvas-wait` | yes | yes |
|
|
17
|
+
| B | canvas-read | `Canvas::Reader`, render-only, no app session | yes | yes |
|
|
18
|
+
| C | Exported HTML | nothing — a static file | no | only if `x-data` is present |
|
|
19
|
+
|
|
20
|
+
## Verdict vocabulary
|
|
21
|
+
|
|
22
|
+
- **WORKS** — full intended function.
|
|
23
|
+
- **DEGRADES** — reduced but honest (disabled control, visible title, read-only state) — the user can see less is on offer.
|
|
24
|
+
- **SILENTLY-DEAD** — renders, looks interactive, does nothing. No error, no visual sign. The one to design out of a doc before it ships.
|
|
25
|
+
- **BREAKS** — errors, blanks, or visibly wrong output.
|
|
26
|
+
|
|
27
|
+
## Plays well everywhere (WORKS in A, B, and C)
|
|
28
|
+
|
|
29
|
+
Anything that owns its own state — Alpine `x-data` it never asks an enclosing scope for — or is flat markup with no server dependency. This is the safe backbone for any doc that needs to survive `canvas-read` or an export: `text`, `md`, `header1`–`header6`, `card`/`card_header`/`card_body`, `callout`, `table` (incl. `sortable: true`), `collapsible`/`expandable_card`/`dropdown`, `tabs` (eager, default — **not** `url: true`, see `references/tabs-and-navigation.md`), `sidebar_toc`, `mermaid`, the whole chart family (`chart`, `bar_chart`, `line_chart`, `pie_chart`, `sparkline`, `stacked_bar_chart`, `area_chart`, `hbar_chart`, `doughnut_chart` — fixed 2026-08-23, see `references/charts-and-diagrams.md`), `theme_toggle`/`theme_switcher`/`theme_preset`, `copy_button`, `code_block`, `keyboard_shortcuts` (as a legend), `use_stylesheet`, `doc_header`/`doc_section_header`, `badge`/`status_dot`, and every layout primitive (`columns`, `div`, `section`, `grid`, `vstack`/`hstack`, ...).
|
|
30
|
+
|
|
31
|
+
**If a doc is built only from this list, it renders identically in a live canvas, `canvas-read`, and an export.** `examples/canvas-safe-showcase.rb` proves it — see below.
|
|
32
|
+
|
|
33
|
+
## sendEvent-only-on-live-canvas (WORKS in A only)
|
|
34
|
+
|
|
35
|
+
These dispatch through `window.sendEvent`, which only `Canvas::BridgeServer`'s `cdn_scripts` defines: `button`, `radio_group`, `clickable(action:)`, `menu_item` (action block), `form` submit, `tag_buttons`, `chip_group`. In B (canvas-read) they render honestly `disabled`/`aria-disabled` with an explanatory title — no `ReferenceError`, no self-mutating click. In C (export) htmx never loads, so their `hx-post` fallback never fires either — SILENTLY-DEAD, quiet and invisible. **Use these to build the live-canvas interaction; don't expect them to do anything once the doc is reopened as a saved doc or exported.** Detail + minimal examples: `references/actions-and-buttons.md` and `references/inputs-and-forms.md`. Saving the session **as `.org`** doesn't render them dead — it leaves them out entirely (one `#+STREAMWEAVER_OMITTED: <call>` keyword line each, counted as `omitted` in the save dialog's coverage notice), because a static org document cannot hold a live control. Save as `.rb` when the controls have to come back.
|
|
36
|
+
|
|
37
|
+
## Needs a real server (`streamweaver run`/`serve` with the routes mounted — not canvas, not canvas-read, not export)
|
|
38
|
+
|
|
39
|
+
- Per-keystroke sync on `text_field`/`text_area`/`date_field`/`checkbox`/`select`/`checkbox_group` (auto-submit `hx-post /update` — 404s everywhere backend-less; the *value itself* still reaches an agent because `button`'s `getFormState()` harvests the DOM on click, but the field's own auto-submit never fires cleanly). See `references/inputs-and-forms.md`.
|
|
40
|
+
- `design_deck` option selection, notes, generate-more — DEGRADES to honestly read-only wherever `deck_server:` isn't set (canvas, canvas-read, export, and `streamweaver serve` alike). See `references/deck.md`.
|
|
41
|
+
- `modal` opening (server-state driven — can close client-side, can't open without a live push), `route_by`/`route`/`page`, `every`/`stream` (SSE — `AppView`-only, no client at all backend-less), `resource`/`form_for` (routed CRUD), `endpoint(...)`, `tabs lazy: true` (deprecated — blank panels, not this skill's concern to fix).
|
|
42
|
+
- `local_asset` / relative `image_block` src / any `/sw-asset/...` reference — BREAKS everywhere except `--inline-images` on export or a colocated file.
|
|
43
|
+
|
|
44
|
+
## When to load a reference file
|
|
45
|
+
|
|
46
|
+
| Building with... | Read |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `text_field`, `checkbox_group`, `chip_group`, `tag_buttons`, `form` blocks | `references/inputs-and-forms.md` |
|
|
49
|
+
| `button`, `clickable`, `menu_item`, submit buttons | `references/actions-and-buttons.md` |
|
|
50
|
+
| `mermaid`, `chart`/`bar_chart`/etc. | `references/charts-and-diagrams.md` |
|
|
51
|
+
| `tabs`, `collapsible`, `dropdown`, `modal`, `route_by` | `references/tabs-and-navigation.md` |
|
|
52
|
+
| `design_deck` | `references/deck.md` |
|
|
53
|
+
|
|
54
|
+
Each reference file is advice plus a minimal working DSL snippet plus the one gotcha that actually bites — load it only when you're about to use that component, not up front.
|
|
55
|
+
|
|
56
|
+
## The comprehensive example
|
|
57
|
+
|
|
58
|
+
`examples/canvas-safe-showcase.rb` is a bare DSL body (`streamweaver-doc: v1`, no `app` wrapper — see `streamweaver-doc-builder` for the shared-body pattern) built entirely from the plays-well-everywhere list above. It was verified to render correctly — no silently-dead component, no missing script pairing — in all three contexts: pushed through the real bridge (websocket adapter, context A), served by `Canvas::Reader` (context B, `SW_NO_OPEN=1`, ephemeral port, killed and `lsof`-verified after), and run through `streamweaver export` (context C). Use it as the starting skeleton for a doc that must survive being saved and reopened later, or exported and handed to someone with no server at all.
|
|
59
|
+
|
|
60
|
+
It deliberately does **not** include any sendEvent-only component (button, radio_group, ...) — adding one is a live-canvas-only enhancement to layer on top, not part of the guaranteed-everywhere core. See `references/actions-and-buttons.md` for how to add interactivity honestly.
|
|
61
|
+
|
|
62
|
+
## Known gotchas (cross-cutting, see `docs/frontend-only.md` for the full list)
|
|
63
|
+
|
|
64
|
+
- **Bare `x-model` with no owning `x-data` is silently-dead everywhere backend-less** — console-clean, no error. Every component above either owns its own `x-data` or uses server-round-trip inputs (which are covered under "needs a real server").
|
|
65
|
+
- **`canvas-wait`'s default only catches `action` events** (`button`, `clickable`, `menu_item`, `form` submit). `tag_buttons` and `chip_group` dispatch `change` — pass `--event change` or `--any`.
|
|
66
|
+
- **A CSP-locked viewer breaks every Alpine-dependent component in an export**, even ones marked WORKS above (tabs, collapsible, dropdown, theme_toggle) — `x-cloak` content never un-hides. `--offline` on export only inlines mermaid, not Alpine.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# streamweaver-doc: v1
|
|
2
|
+
# Canvas-safe showcase — every component below is drawn from the
|
|
3
|
+
# plays-well-everywhere list in SKILL.md. Verified to render correctly with
|
|
4
|
+
# no silently-dead component and no missing script pairing in all three
|
|
5
|
+
# backend-less contexts: the live bridge (websocket adapter), Canvas::Reader
|
|
6
|
+
# (canvas-read), and `streamweaver export`. Deliberately carries no
|
|
7
|
+
# sendEvent-only component (button, radio_group, ...) — see
|
|
8
|
+
# references/actions-and-buttons.md for how to layer live-canvas
|
|
9
|
+
# interactivity on top of a doc built from this skeleton.
|
|
10
|
+
|
|
11
|
+
sidebar_toc sections: [
|
|
12
|
+
{ id: "overview", label: "Overview" },
|
|
13
|
+
{ id: "data", label: "Data" },
|
|
14
|
+
{ id: "diagram", label: "Diagram" }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
doc_header(
|
|
18
|
+
eyebrow: "streamweaver-canvas-safe · example",
|
|
19
|
+
title: "Canvas-Safe Component Showcase",
|
|
20
|
+
pills: [{ text: "Reference" }, "Renders identically in canvas, canvas-read, and export"]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
doc_section_header "01", "Overview", id: "overview"
|
|
24
|
+
|
|
25
|
+
md <<~MD
|
|
26
|
+
Every component on this page owns its own state — either it's flat markup with
|
|
27
|
+
no server dependency, or it's Alpine `x-data` that never asks an enclosing
|
|
28
|
+
scope for anything. That's the whole trick: nothing here round-trips, so
|
|
29
|
+
nothing here has anything to lose when the doc is reopened with no backend
|
|
30
|
+
behind it.
|
|
31
|
+
MD
|
|
32
|
+
|
|
33
|
+
callout(variant: :info, title: "Why this matters") do
|
|
34
|
+
text "A doc built only from this component set survives being saved and reopened " \
|
|
35
|
+
"later, or exported and handed to someone with no StreamWeaver server running " \
|
|
36
|
+
"at all — byte-for-byte the same interactivity in every context."
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
card do
|
|
40
|
+
card_header "Status", badge: "Live", meta: "plays-well-everywhere"
|
|
41
|
+
card_body do
|
|
42
|
+
status_dot(status: :green)
|
|
43
|
+
text "All components below: WORKS in canvas, canvas-read, and export"
|
|
44
|
+
md "Collapse the section below to see client-side Alpine state that never touches a server."
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
collapsible "Show implementation notes" do
|
|
49
|
+
md "This panel opens and closes via Alpine `x-show` — no request, no round-trip, " \
|
|
50
|
+
"identical behavior whether a bridge is listening or not."
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
tabs :showcase_tabs do
|
|
54
|
+
tab "Plain tabs" do
|
|
55
|
+
text "Eager, client-side tabs (no url: true) — pure @click state, works everywhere."
|
|
56
|
+
end
|
|
57
|
+
tab "Why not url: true?" do
|
|
58
|
+
text "tabs url: true degrades on canvas (plain client tabs + an agent-only stderr " \
|
|
59
|
+
"warning) but becomes MORE capable in an export (real pushState routing). " \
|
|
60
|
+
"See references/tabs-and-navigation.md."
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
doc_section_header "02", "Data", id: "data"
|
|
65
|
+
|
|
66
|
+
table(
|
|
67
|
+
headers: ["Component", "A live canvas", "B canvas-read", "C export"],
|
|
68
|
+
rows: [
|
|
69
|
+
["Everything on this page", "WORKS", "WORKS", "WORKS"],
|
|
70
|
+
["button / clickable / form submit", "WORKS", "DEGRADES", "SILENTLY-DEAD"],
|
|
71
|
+
["text_field auto-submit", "DEGRADES", "SILENTLY-DEAD", "SILENTLY-DEAD"]
|
|
72
|
+
],
|
|
73
|
+
sortable: true
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
bar_chart data: { canvas: 3, "canvas-read": 3, export: 3 }
|
|
77
|
+
|
|
78
|
+
doc_section_header "03", "Diagram", id: "diagram"
|
|
79
|
+
|
|
80
|
+
mermaid <<~MERMAID
|
|
81
|
+
graph LR
|
|
82
|
+
A["Same .rb file"] --> B["Live canvas (A)"]
|
|
83
|
+
A --> C["canvas-read (B)"]
|
|
84
|
+
A --> D["streamweaver export (C)"]
|
|
85
|
+
B --> E["Identical render"]
|
|
86
|
+
C --> E
|
|
87
|
+
D --> E
|
|
88
|
+
MERMAID
|
|
89
|
+
|
|
90
|
+
code_block(<<~RUBY, lang: "ruby", copy: true)
|
|
91
|
+
# Pushed to a live canvas...
|
|
92
|
+
streamweaver canvas-push showcase < canvas-safe-showcase.rb
|
|
93
|
+
# ...or exported as a static file...
|
|
94
|
+
streamweaver export canvas-safe-showcase.rb -o showcase.html
|
|
95
|
+
# ...same DSL, same rendered result either way.
|
|
96
|
+
RUBY
|
|
97
|
+
|
|
98
|
+
theme_toggle
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Actions and Buttons
|
|
2
|
+
|
|
3
|
+
Seven components dispatch through `window.sendEvent`, which only the live bridge's `cdn_scripts` defines (context A). All seven share the same three-context shape: **WORKS on the live canvas, DEGRADES-honest on canvas-read, SILENTLY-DEAD in an export.** There is no way to make click-driven interactivity survive an export — a static file has nothing to dispatch to. Design around that instead: use these for the live-canvas conversation loop, and lean on the plays-well-everywhere list (SKILL.md) for anything that has to survive being saved and reopened.
|
|
4
|
+
|
|
5
|
+
## button
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
button "Submit" do |state|
|
|
9
|
+
# runs when clicked, on the live canvas, with an agent listening on canvas-wait
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
button "Preview only", submit: false # decorative — WORKS in all three contexts, never dispatches
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Default `button` dispatches `sendEvent('action', {button: <token>, state: getFormState()})`. With no `canvas-wait` holder listening, the click still fires `showFeedback()` and replaces the container with "✓ Submitted" — the page is gone even though nothing consumed the event, so always have something waiting before you tell a user to click.
|
|
16
|
+
|
|
17
|
+
## clickable(action:)
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
clickable(action: :open_row, key: "r1") { text "Row" }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Ported to `sendEvent('action')` (disc-097) — same payload shape as `button`. Can't be natively `disabled` (it's a `<div>`), so on canvas-read it drops `aria-disabled` + no `tabindex` instead of `disabled` — a focusable element that does nothing would be its own lie.
|
|
24
|
+
|
|
25
|
+
## menu_item (action block)
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
dropdown do
|
|
29
|
+
menu do
|
|
30
|
+
menu_item("Archive") { |state| state[:archived] = true }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Dispatches `sendEvent('action', {button: 'menu_item_N', ...})`; the menu still closes on dispatch, live canvas only.
|
|
36
|
+
|
|
37
|
+
## Waiting on the right event
|
|
38
|
+
|
|
39
|
+
`canvas-wait`'s default only catches `action` events (`button`, `clickable`, `menu_item`, `form` submit). `tag_buttons` and `chip_group` dispatch `change` instead — mirroring `radio_group` since they're a state change, not a submission. Pass `--event change` or `--any` if your doc's only dispatching control is one of those two, or the wait will hang forever on an event that's never coming.
|
|
40
|
+
|
|
41
|
+
## Quoting
|
|
42
|
+
|
|
43
|
+
Author-supplied strings reaching a JS handler (tag labels, menu item text) route through JSON quoting — an apostrophe in a label won't produce a JS syntax error. The one exception is `external_link_button`'s `window.open(url)` call, which is emitted identically in HTTP and canvas mode by design (quoting it would change HTTP-mode output); an unquoted URL there is a known, tracked edge case (disc-107), not something this skill's examples exercise.
|
|
44
|
+
|
|
45
|
+
## The gotcha
|
|
46
|
+
|
|
47
|
+
None of these components render any different markup between "will dispatch to an agent" and "will replace the page with a submitted-state placeholder because nobody's listening." Before telling a user to click a button, make sure a `canvas-wait` (with the right `--event`) is actually running — otherwise the click looks like it worked and just wasn't heard.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Charts and Diagrams
|
|
2
|
+
|
|
3
|
+
Both families WORK in all three contexts. This is the one area where the fix (chart-export-allowlist, 2026-08-23) is recent enough to be worth restating rather than assuming.
|
|
4
|
+
|
|
5
|
+
## Charts
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
bar_chart data: { calendar: 45, news: 120, tasks: 30 }
|
|
9
|
+
hbar_chart data: { "Phase A" => 25, "Phase B" => 45 }
|
|
10
|
+
line_chart data: { jan: 10, feb: 25, mar: 18 }
|
|
11
|
+
pie_chart data: { chrome: 60, safari: 25, firefox: 15 }
|
|
12
|
+
sparkline data: [3, 7, 4, 9, 2, 8]
|
|
13
|
+
stacked_bar_chart data: [{ label: "Q1", series: { a: 10, b: 20 } }]
|
|
14
|
+
area_chart data: { jan: 10, feb: 25, mar: 18 }
|
|
15
|
+
doughnut_chart data: { chrome: 60, safari: 25, firefox: 15 }
|
|
16
|
+
chart type: :bar, data: { a: 1, b: 2 } # the base component directly
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Every one of the shorthand methods above builds a `ChartBase` subclass under the hood, not `Components::Chart` itself. Before 2026-08-23, `streamweaver export`'s CDN-inclusion check tested `is_a?(Components::Chart)` — so every shorthand method failed the check while `chart type: ...` passed it. The exported markup still carried the guarded `x-init="if (typeof Chart !== 'undefined') { new Chart(...) }"`, so nothing threw; you got an empty bordered box, console-silent, that looked like a rendering glitch rather than a missing library. **Fixed:** the gate now checks `components_include?(Components::Chart) || components_include?(Components::ChartBase)` — the whole family, keyed on the base class so a future chart subclass can't silently join the dead list the way the shorthand methods did.
|
|
20
|
+
|
|
21
|
+
## Mermaid
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
mermaid <<~MERMAID
|
|
25
|
+
graph LR
|
|
26
|
+
A["Calendar"] --> B["Sync script"] --> C["state.yaml"]
|
|
27
|
+
MERMAID
|
|
28
|
+
|
|
29
|
+
mermaid diagram_code, zoom: true # pan/zoom controls
|
|
30
|
+
mermaid diagram_code, compact: true # reduced padding for card embedding
|
|
31
|
+
mermaid diagram_code, layout: :elk # note: --offline export has no global ELK build
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`sw-mermaid-zoom.js` (the interaction engine) is inlined and travels into every export; the mermaid *library itself* is CDN-referenced by default. `streamweaver export --offline` inlines the mermaid library so diagrams survive a CSP-locked viewer — but that flag covers mermaid only, not Alpine or Prism, and it doesn't help `layout: :elk` (no global ELK build to inline).
|
|
35
|
+
|
|
36
|
+
## The gotcha
|
|
37
|
+
|
|
38
|
+
Don't assume "WORKS in all three contexts" from this file also means "survives a CSP-locked viewer with no flags." A default (non-`--offline`) export still loads the chart/mermaid CDN scripts over the network — a CSP that blocks external hosts breaks them exactly like it breaks Alpine. `--offline` closes the mermaid gap; nothing closes the Chart.js one yet if you need a fully offline, CSP-safe chart export.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Design Deck
|
|
2
|
+
|
|
3
|
+
`design_deck` navigation (moving between slides, `slide_container`/`slide`) is plain inlined JS (`sw-slide-nav.js`) — WORKS in every context, no server needed. Everything below is about the interactive part: option selection, notes, generate-more, model selection.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
design_deck "Design Options" do
|
|
7
|
+
slide "arch", "Architecture" do
|
|
8
|
+
option("Monolith") { text "Simple, one deploy unit" }
|
|
9
|
+
option("Microservices") { text "Scales independently, more ops" }
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## What changed 2026-08-23 (disc-096)
|
|
15
|
+
|
|
16
|
+
**Before:** `swDeckSelect` applied the selected CSS class and `aria-checked` **before** calling `fetch('/deck/select')`, with no `.catch`. Every backend-less context — canvas, canvas-read, export — has no `/deck/*` route, so the fetch 404s (or fails outright) and the failure was swallowed. The user saw a confirmed selection that was never recorded and never reached anything. Not just dead — it actively lied about recorded state, `aria-checked` included.
|
|
17
|
+
|
|
18
|
+
**After:** confirmation is success-gated. Visual/aria state only changes after the POST resolves; a failed request surfaces a console error naming the route and reason, and applies no visual change. An `aria-busy` state covers the in-flight moment. Separately, the whole interactive surface (option cards, notes textarea, submit, generate-more, model selector) renders **read-only** wherever `/deck/*` genuinely doesn't exist — gated by a `deck_server:` construction flag the adapter carries, set correctly at every render site including canvas, canvas-read, export, and `streamweaver serve` (service mode never mounted `/deck/*` either — a second, previously-undocumented instance of the same bug, now covered too).
|
|
19
|
+
|
|
20
|
+
## Where it WORKS vs. where it's read-only
|
|
21
|
+
|
|
22
|
+
| Context | Deck interactivity |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `streamweaver run` (real standalone server, `/deck/*` mounted) | WORKS — confirmation lands slightly after the click now (the fix), not before |
|
|
25
|
+
| Live canvas (A) | DEGRADES — honestly read-only, `/deck/*` doesn't exist here |
|
|
26
|
+
| canvas-read (B) | DEGRADES — same |
|
|
27
|
+
| Export (C) | DEGRADES — same |
|
|
28
|
+
| `streamweaver serve` (service mode) | DEGRADES — read-only, same previously-undocumented bug, now covered |
|
|
29
|
+
|
|
30
|
+
## The gotcha
|
|
31
|
+
|
|
32
|
+
If you're building a doc meant to demo option selection interactively, `design_deck` will render but every click will be inert (honestly, not silently) unless it's opened under a real `streamweaver run` server that mounts `/deck/*`. Don't put deck-selection interactivity in a doc you intend to Save-as-doc and reopen later, or export — it'll render fine and simply not accept input in either context. There's also a separate, still-open gap: the deck JS posts to `/deck/*` by absolute path, ignoring the adapter's `url_prefix` — so even a service-mode app that mounted deck routes under a prefix would post to the wrong place. That's why today's fix makes it read-only rather than pointing it at the prefixed route; url-prefixing the deck JS is a prerequisite for ever flipping `deck_server: true` under `streamweaver serve`.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Inputs and Forms
|
|
2
|
+
|
|
3
|
+
Every plain input (`text_field`, `text_area`, `date_field`, `checkbox`, `select`) auto-submits via a debounced `hx-post /update` on every keystroke or change. That request 404s on every backend-less context (A, B, C) — it needs `streamweaver run`/`serve` with the update route mounted. This is disc-106, tracked and deliberately out of scope; know it going in rather than chase a phantom 404 in the console.
|
|
4
|
+
|
|
5
|
+
The good news: on the live canvas (A), the value still reaches an agent — just not through the field's own auto-submit. `getFormState()` walks the DOM for every `[x-model]` element at the moment some *other* `sendEvent`-carrying control fires (a `button`, `clickable`, a form submit). So a `text_field` sitting next to a `button` works as a value carrier even though its own round-trip never completes.
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
text_field :city, placeholder: "City"
|
|
9
|
+
button "Submit" do |state|
|
|
10
|
+
# state[:city] is populated from getFormState() on the live canvas
|
|
11
|
+
end
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
If you don't want the debounced 404 noise on canvas, pass `submit: false` — the field becomes an inert value carrier with no auto-submit at all (DEGRADES honestly instead of failing silently):
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
text_field :city, submit: false
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## checkbox_group — array harvest, not auto-submit
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
checkbox_group :selected_items, select_all: "Select All", select_none: "Clear" do
|
|
24
|
+
items.each { |item| item(item.id) { text item.name } }
|
|
25
|
+
end
|
|
26
|
+
# button click elsewhere harvests state[:selected_items] = ["id1", "id3"]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Fixed 2026-08-23 (disc-098):** the harvest used to collapse every group to the *last* checked item's boolean — confidently wrong data, worse than dead. It's fixed now: items inside a `.checkbox-group` accumulate into an array; a lone `checkbox` outside a group still returns a boolean. `checkbox_group` itself was never ported to `sendEvent` — its auto-submit still 404s everywhere backend-less, same as the plain inputs above. The array only leaves the page when another control's click harvests it.
|
|
30
|
+
|
|
31
|
+
## chip_group — multi-select, sendEvent-ported
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
chip_group :langs, %w[ruby js python] # multi: true by default
|
|
35
|
+
chip_group :lang, %w[ruby js python], multi: false # single-select
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Ported to `sendEvent('change')` on the live canvas (disc-097) — dispatches `{field: :langs, value: [...], state: getFormState()}` on every click, array-harvest-correct. See `references/actions-and-buttons.md` for the sendEvent-only-on-live-canvas rundown; canvas-read renders it honestly `aria-disabled`, export falls back to a dead `hx-post`.
|
|
39
|
+
|
|
40
|
+
## tag_buttons — sendEvent-ported
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
tag_buttons :category, ["Fiction", "Non-fiction", "Mystery"]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Same disposition as `chip_group`: `sendEvent('change')` on the live canvas, honest `aria-disabled` on canvas-read, dead `hx-post` in export.
|
|
47
|
+
|
|
48
|
+
## form blocks
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
form :edit_person do
|
|
52
|
+
text_field :name, placeholder: "Name"
|
|
53
|
+
select :status, %w[active paused archived]
|
|
54
|
+
submit "Save" do |form_values|
|
|
55
|
+
# form_values = { name: "...", status: "..." }
|
|
56
|
+
end
|
|
57
|
+
cancel "Cancel"
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`submit` dispatches through `sendEvent('action')` on the live canvas (disc-097 — `form` was the one auto-submit-based component that got fully ported, not just harvest-fixed). `cancel` is Alpine-only client-side reset, so it works in all three contexts.
|
|
62
|
+
|
|
63
|
+
## The gotcha
|
|
64
|
+
|
|
65
|
+
Checking a box or typing into a field, by itself, sends **nothing** anywhere. Only a `sendEvent`-carrying control's click harvests the current DOM state. If your doc's only interactive elements are plain inputs with no `button`/`clickable`/`form submit` alongside them, nothing an agent waits on will ever fire — the fields will look interactive (you can type, you can check boxes) and simply never report anywhere. Always pair inputs with at least one dispatching control.
|