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,772 @@
|
|
|
1
|
+
# How StreamWeaver Works: A Deep Dive
|
|
2
|
+
|
|
3
|
+
This document explains the internal architecture of StreamWeaver - how a Ruby DSL on the server handles frontend events and runs backend code.
|
|
4
|
+
|
|
5
|
+
## The Core Innovation
|
|
6
|
+
|
|
7
|
+
StreamWeaver's key insight: **Your Ruby code re-executes on every user interaction**. When a user types in a text field or clicks a button, the entire DSL block is re-evaluated with the new state. This creates a reactive programming model where your code runs on the server but responds to frontend events.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
app "Counter" do
|
|
11
|
+
# This block runs EVERY TIME any input changes or button is clicked
|
|
12
|
+
text "Count: #{state[:count] || 0}"
|
|
13
|
+
|
|
14
|
+
button "Increment" do |s|
|
|
15
|
+
s[:count] = (s[:count] || 0) + 1
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Architecture Overview
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
24
|
+
│ BROWSER │
|
|
25
|
+
│ ┌──────────────────────────────────────────────────────────────┐ │
|
|
26
|
+
│ │ Alpine.js (x-model) HTMX (hx-post) │ │
|
|
27
|
+
│ │ - Captures input values - Sends POST requests │ │
|
|
28
|
+
│ │ - Client-side state - Swaps HTML responses │ │
|
|
29
|
+
│ └──────────────────────────────────────────────────────────────┘ │
|
|
30
|
+
│ │ │
|
|
31
|
+
│ ▼ HTTP POST │
|
|
32
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
33
|
+
│
|
|
34
|
+
▼
|
|
35
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
36
|
+
│ SERVER │
|
|
37
|
+
│ ┌─────────────┐ ┌────────────────┐ ┌──────────────────────┐ │
|
|
38
|
+
│ │ SinatraApp │ │ App │ │ Adapter │ │
|
|
39
|
+
│ │ (Routes) │──▶│ (DSL + State) │──▶│ (HTML Generation) │ │
|
|
40
|
+
│ │ │ │ │ │ │ │
|
|
41
|
+
│ │ /update │ │ rebuild_with_ │ │ render_text_field() │ │
|
|
42
|
+
│ │ /action/:id │ │ state() │ │ render_button() │ │
|
|
43
|
+
│ │ /submit │ │ │ │ render_*() │ │
|
|
44
|
+
│ └─────────────┘ └────────────────┘ └──────────────────────┘ │
|
|
45
|
+
│ │ │
|
|
46
|
+
│ ▼ │
|
|
47
|
+
│ ┌────────────────┐ │
|
|
48
|
+
│ │ Views │ │
|
|
49
|
+
│ │ (Phlex HTML) │ │
|
|
50
|
+
│ └────────────────┘ │
|
|
51
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## The Request-Response Cycle
|
|
55
|
+
|
|
56
|
+
### 1. Initial Page Load (GET /)
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
Browser ─────GET /─────▶ SinatraApp
|
|
60
|
+
│
|
|
61
|
+
▼
|
|
62
|
+
App.rebuild_with_state({})
|
|
63
|
+
│
|
|
64
|
+
▼
|
|
65
|
+
Execute DSL block (instance_eval)
|
|
66
|
+
│
|
|
67
|
+
▼
|
|
68
|
+
Build component tree
|
|
69
|
+
│
|
|
70
|
+
▼
|
|
71
|
+
Adapter renders HTML with HTMX/Alpine attributes
|
|
72
|
+
│
|
|
73
|
+
▼
|
|
74
|
+
AppView generates full page
|
|
75
|
+
│
|
|
76
|
+
◀────────HTML────────────────┘
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 2. Input Change (typing in text field)
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
User types "hello"
|
|
83
|
+
│
|
|
84
|
+
▼
|
|
85
|
+
Alpine.js x-model captures value
|
|
86
|
+
│
|
|
87
|
+
▼
|
|
88
|
+
HTMX triggers POST /update (after debounce)
|
|
89
|
+
│
|
|
90
|
+
▼
|
|
91
|
+
SinatraApp#post '/update'
|
|
92
|
+
│
|
|
93
|
+
├─▶ state = session[:streamlit_state]
|
|
94
|
+
├─▶ sync_params_to_state(state) # state[:name] = "hello"
|
|
95
|
+
├─▶ app.rebuild_with_state(state) # Re-run DSL with new state
|
|
96
|
+
└─▶ AppContentView renders HTML
|
|
97
|
+
│
|
|
98
|
+
◀────────HTML partial─────────────────────┘
|
|
99
|
+
│
|
|
100
|
+
▼
|
|
101
|
+
HTMX swaps #app-container innerHTML
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 3. Button Click (executing a callback)
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
User clicks "Submit"
|
|
108
|
+
│
|
|
109
|
+
▼
|
|
110
|
+
HTMX POST /action/btn_submit_abc123
|
|
111
|
+
│
|
|
112
|
+
▼
|
|
113
|
+
SinatraApp#post '/action/:button_id'
|
|
114
|
+
│
|
|
115
|
+
├─▶ Find button by ID in component tree
|
|
116
|
+
├─▶ button.execute(state) # Runs the Ruby block!
|
|
117
|
+
│ │
|
|
118
|
+
│ └─▶ Your code: puts state[:name]
|
|
119
|
+
│
|
|
120
|
+
├─▶ app.rebuild_with_state(state)
|
|
121
|
+
└─▶ AppContentView renders HTML
|
|
122
|
+
│
|
|
123
|
+
◀────────HTML partial─────────────────────┘
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Core Classes Explained
|
|
127
|
+
|
|
128
|
+
### App (lib/stream_weaver/app.rb)
|
|
129
|
+
|
|
130
|
+
The `App` class is the heart of the DSL. It:
|
|
131
|
+
|
|
132
|
+
1. **Stores the DSL block** - The block you pass to `app` is saved and re-executed
|
|
133
|
+
2. **Manages state** - Tracks all form values in `@_state`
|
|
134
|
+
3. **Builds component tree** - DSL methods like `text_field`, `button` create Component instances
|
|
135
|
+
4. **Provides DSL methods** - All the UI building methods are defined here
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
class App
|
|
139
|
+
def initialize(title, &block)
|
|
140
|
+
@block = block # Your DSL code
|
|
141
|
+
@_state = {} # Current state
|
|
142
|
+
@components = [] # Component tree
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# This is the magic - re-runs your code with fresh state
|
|
146
|
+
def rebuild_with_state(current_state)
|
|
147
|
+
@_state = current_state
|
|
148
|
+
@components = [] # Clear old components
|
|
149
|
+
instance_eval(&@block) # Execute YOUR code
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# DSL methods create components
|
|
153
|
+
def text_field(key, **options)
|
|
154
|
+
@_state[key] ||= "" # Initialize state
|
|
155
|
+
@components << Components::TextField.new(key, **options)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def button(label, **options, &block)
|
|
159
|
+
@components << Components::Button.new(label, &block)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### SinatraApp (lib/stream_weaver/server.rb)
|
|
165
|
+
|
|
166
|
+
The generated Sinatra application that handles HTTP requests:
|
|
167
|
+
|
|
168
|
+
| Route | Purpose |
|
|
169
|
+
|-------|---------|
|
|
170
|
+
| `GET /` | Initial page load |
|
|
171
|
+
| `POST /update` | State sync from input changes |
|
|
172
|
+
| `POST /action/:button_id` | Button click handlers |
|
|
173
|
+
| `POST /submit` | Agentic mode final submission |
|
|
174
|
+
| `POST /event/:key` | on_change/on_blur callbacks |
|
|
175
|
+
| `POST /form/:form_name` | Deferred form submission |
|
|
176
|
+
| `POST /theme/:theme_name` | Runtime theme switching |
|
|
177
|
+
|
|
178
|
+
Key methods:
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
# Sync form params to state hash
|
|
182
|
+
def sync_params_to_state(state, excluded_keys: [])
|
|
183
|
+
params.each do |key, value|
|
|
184
|
+
state[key.to_sym] = coerce_param_value(value, state[key.to_sym])
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Find button in nested component tree
|
|
189
|
+
def self.find_button_recursive(components, button_id)
|
|
190
|
+
components.each do |component|
|
|
191
|
+
return component if component.is_a?(Components::Button) && component.id == button_id
|
|
192
|
+
if component.respond_to?(:children) && component.children
|
|
193
|
+
found = find_button_recursive(component.children, button_id)
|
|
194
|
+
return found if found
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
nil
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Adapter::AlpineJS (lib/stream_weaver/adapter/alpinejs.rb)
|
|
202
|
+
|
|
203
|
+
The adapter is responsible for rendering components with the right HTML attributes for the frontend framework (Alpine.js + HTMX).
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
def render_text_field(view, key, options, state)
|
|
207
|
+
view.input(
|
|
208
|
+
type: "text",
|
|
209
|
+
name: key.to_s,
|
|
210
|
+
value: state[key] || "",
|
|
211
|
+
"x-model" => key.to_s, # Alpine.js 2-way binding
|
|
212
|
+
"hx-post" => url("/update"), # HTMX POST on change
|
|
213
|
+
"hx-include" => "[x-model]", # Include all bound inputs
|
|
214
|
+
"hx-target" => "#app-container", # Replace this element
|
|
215
|
+
"hx-swap" => "innerHTML scroll:false", # Swap method
|
|
216
|
+
"hx-trigger" => "keyup changed delay:500ms" # Debounced trigger
|
|
217
|
+
)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def render_button(view, button_id, label, options, modal_context)
|
|
221
|
+
view.button(
|
|
222
|
+
"hx-post" => url("/action/#{button_id}"),
|
|
223
|
+
"hx-include" => "[x-model]",
|
|
224
|
+
"hx-target" => "#app-container",
|
|
225
|
+
"hx-swap" => "innerHTML scroll:false"
|
|
226
|
+
) { label }
|
|
227
|
+
end
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Components (lib/stream_weaver/components.rb)
|
|
231
|
+
|
|
232
|
+
Each UI element is a component class. Components:
|
|
233
|
+
- Store their configuration
|
|
234
|
+
- Delegate rendering to the adapter
|
|
235
|
+
- Can have callbacks (on_change, on_blur)
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
class TextField < Base
|
|
239
|
+
include Callbacks
|
|
240
|
+
attr_reader :key
|
|
241
|
+
|
|
242
|
+
def initialize(key, on_change: nil, **options)
|
|
243
|
+
@key = key
|
|
244
|
+
@options = options
|
|
245
|
+
init_callbacks(on_change: on_change)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def render(view, state)
|
|
249
|
+
view.adapter.render_text_field(view, @key, @options, state)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
class Button < Base
|
|
254
|
+
def initialize(label, stable_id, **options, &block)
|
|
255
|
+
@label = label
|
|
256
|
+
@action = block # Your Ruby callback
|
|
257
|
+
@button_id = "btn_#{label}_#{stable_id}"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def execute(state)
|
|
261
|
+
@action.call(state) if @action # Runs YOUR code!
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Views (lib/stream_weaver/views.rb)
|
|
267
|
+
|
|
268
|
+
Phlex-based views render the final HTML:
|
|
269
|
+
|
|
270
|
+
- **AppView** - Full page with `<html>`, `<head>`, `<body>`, CSS, JS
|
|
271
|
+
- **AppContentView** - Partial for HTMX swaps (just the component HTML)
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
class AppView < Phlex::HTML
|
|
275
|
+
def view_template
|
|
276
|
+
doctype
|
|
277
|
+
html do
|
|
278
|
+
head do
|
|
279
|
+
@adapter.render_cdn_scripts(self) # HTMX, Alpine.js
|
|
280
|
+
style { raw(safe(CSS_CONTENT)) } # Built-in CSS
|
|
281
|
+
end
|
|
282
|
+
body(class: body_classes) do
|
|
283
|
+
h1 { @app.title }
|
|
284
|
+
div(id: "app-container", **@adapter.container_attributes(@state)) do
|
|
285
|
+
@app.components.each { |c| c.render(self, @state) }
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Service Mode (lib/stream_weaver/service.rb)
|
|
294
|
+
|
|
295
|
+
Service mode runs a persistent server that can host multiple apps:
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
299
|
+
│ StreamWeaver Service │
|
|
300
|
+
│ │
|
|
301
|
+
│ ┌───────────────────┐ ┌───────────────────┐ │
|
|
302
|
+
│ │ App: abc123 │ │ App: def456 │ │
|
|
303
|
+
│ │ hello_world.rb │ │ todo_list.rb │ │
|
|
304
|
+
│ │ /apps/abc123 │ │ /apps/def456 │ │
|
|
305
|
+
│ └───────────────────┘ └───────────────────┘ │
|
|
306
|
+
│ │
|
|
307
|
+
│ ┌───────────────────────────────────────────────────────────────┐ │
|
|
308
|
+
│ │ Live Sessions │ │
|
|
309
|
+
│ │ │ │
|
|
310
|
+
│ │ claude_chat_1: │ │
|
|
311
|
+
│ │ - Browser polls /live/claude_chat_1/poll │ │
|
|
312
|
+
│ │ - Claude pushes via /live/claude_chat_1/push │ │
|
|
313
|
+
│ │ - User actions stored for Claude to retrieve │ │
|
|
314
|
+
│ └───────────────────────────────────────────────────────────────┘ │
|
|
315
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Service Features:
|
|
319
|
+
|
|
320
|
+
1. **Multi-app hosting** - Load apps dynamically via `/load-app`
|
|
321
|
+
2. **Live Sessions** - Polling-based update-in-place for agentic workflows
|
|
322
|
+
3. **Source-based routing** - Aliased URLs like `/examples/hello_world`
|
|
323
|
+
4. **Background launch** - `Service.launch_background` spawns a detached process
|
|
324
|
+
|
|
325
|
+
### Live Sessions Architecture
|
|
326
|
+
|
|
327
|
+
Live sessions enable Claude Code or other agents to push UI updates:
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
# Claude pushes content
|
|
331
|
+
POST /live/my_session/push
|
|
332
|
+
target: "#main"
|
|
333
|
+
content: "<h1>Hello!</h1>"
|
|
334
|
+
action: "replace"
|
|
335
|
+
|
|
336
|
+
# Browser polls for updates
|
|
337
|
+
GET /live/my_session/poll?since=1234567890
|
|
338
|
+
# Returns any updates since timestamp
|
|
339
|
+
|
|
340
|
+
# User interaction creates submission
|
|
341
|
+
POST /live/my_session/submit
|
|
342
|
+
# Form data stored for Claude to retrieve
|
|
343
|
+
|
|
344
|
+
# Claude retrieves user input
|
|
345
|
+
GET /live/my_session/submissions
|
|
346
|
+
# Returns pending submissions
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## State Management
|
|
350
|
+
|
|
351
|
+
State flows through the system:
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
┌──────────────┐
|
|
355
|
+
│ Session │
|
|
356
|
+
│ Cookie Store │
|
|
357
|
+
└──────────────┘
|
|
358
|
+
│
|
|
359
|
+
▼
|
|
360
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
361
|
+
│ State Hash │
|
|
362
|
+
│ │
|
|
363
|
+
│ { │
|
|
364
|
+
│ name: "hello", # From text_field :name │
|
|
365
|
+
│ active: true, # From checkbox :active │
|
|
366
|
+
│ settings: { # From form :settings │
|
|
367
|
+
│ email: "x@y.com", │
|
|
368
|
+
│ notify: true │
|
|
369
|
+
│ }, │
|
|
370
|
+
│ my_tabs: 0, # From tabs :my_tabs (active index) │
|
|
371
|
+
│ confirm_open: false, # From modal :confirm (open state) │
|
|
372
|
+
│ _toasts: [...] # Internal toast queue │
|
|
373
|
+
│ } │
|
|
374
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
375
|
+
│
|
|
376
|
+
▼
|
|
377
|
+
App.rebuild_with_state(state)
|
|
378
|
+
│
|
|
379
|
+
▼
|
|
380
|
+
DSL accesses via state[:key]
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### State Initialization
|
|
384
|
+
|
|
385
|
+
Components initialize their state keys when the DSL executes:
|
|
386
|
+
|
|
387
|
+
```ruby
|
|
388
|
+
def text_field(key, **options)
|
|
389
|
+
# Initialize if not present
|
|
390
|
+
@_state[key] ||= ""
|
|
391
|
+
@components << Components::TextField.new(key, **options)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def checkbox(key, label, **options)
|
|
395
|
+
@_state[key] ||= false
|
|
396
|
+
@components << Components::Checkbox.new(key, label, **options)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def tabs(key, **options, &block)
|
|
400
|
+
@_state[key] ||= 0 # Default to first tab
|
|
401
|
+
# ...
|
|
402
|
+
end
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Frontend Integration
|
|
406
|
+
|
|
407
|
+
### Alpine.js Role
|
|
408
|
+
|
|
409
|
+
Alpine.js provides:
|
|
410
|
+
- **`x-data`** - Initializes reactive state on #app-container
|
|
411
|
+
- **`x-model`** - Two-way binding between inputs and state
|
|
412
|
+
- **`x-show`** - Conditional display (tabs, modals, tooltips)
|
|
413
|
+
- **`@click`** - Event handlers for client-only interactions
|
|
414
|
+
|
|
415
|
+
```html
|
|
416
|
+
<div id="app-container" x-data="{name: '', active: false, my_tabs: 0}">
|
|
417
|
+
<input type="text" x-model="name" hx-post="/update" ...>
|
|
418
|
+
<div x-show="my_tabs === 0">Tab 1 content</div>
|
|
419
|
+
</div>
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### HTMX Role
|
|
423
|
+
|
|
424
|
+
HTMX handles server communication:
|
|
425
|
+
- **`hx-post`** - Where to send the request
|
|
426
|
+
- **`hx-include`** - What data to include (`[x-model]` = all bound inputs)
|
|
427
|
+
- **`hx-target`** - What to replace (`#app-container`)
|
|
428
|
+
- **`hx-swap`** - How to replace (`innerHTML scroll:false`)
|
|
429
|
+
- **`hx-trigger`** - When to fire (`keyup changed delay:500ms`)
|
|
430
|
+
|
|
431
|
+
### The Defer Mutations Pattern
|
|
432
|
+
|
|
433
|
+
A critical integration pattern prevents Alpine.js from overwriting HTMX updates:
|
|
434
|
+
|
|
435
|
+
```javascript
|
|
436
|
+
// Before swap: pause Alpine's mutation observer
|
|
437
|
+
document.addEventListener('htmx:beforeSwap', function(e) {
|
|
438
|
+
Alpine.deferMutations();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// After settle: reinitialize Alpine with fresh state from server
|
|
442
|
+
document.addEventListener('htmx:afterSettle', function(e) {
|
|
443
|
+
const stateEl = document.getElementById('sw-state-data');
|
|
444
|
+
const container = document.getElementById('app-container');
|
|
445
|
+
const newState = JSON.parse(stateEl.textContent);
|
|
446
|
+
const alpineData = Alpine.$data(container);
|
|
447
|
+
Object.keys(newState).forEach(key => {
|
|
448
|
+
alpineData[key] = newState[key];
|
|
449
|
+
});
|
|
450
|
+
Alpine.flushAndStopDeferringMutations();
|
|
451
|
+
});
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## Execution Modes
|
|
455
|
+
|
|
456
|
+
### 1. Standalone Mode (`App.run!`)
|
|
457
|
+
|
|
458
|
+
Traditional persistent server:
|
|
459
|
+
|
|
460
|
+
```ruby
|
|
461
|
+
app "My App" do
|
|
462
|
+
# ...
|
|
463
|
+
end.run!
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
- Opens browser automatically
|
|
467
|
+
- Runs until Ctrl+C
|
|
468
|
+
- State persists in session
|
|
469
|
+
|
|
470
|
+
### 2. Agentic Mode (`App.run_once!`)
|
|
471
|
+
|
|
472
|
+
One-shot form collection for AI agents:
|
|
473
|
+
|
|
474
|
+
```ruby
|
|
475
|
+
result = app "Get User Info" do
|
|
476
|
+
text_field :name
|
|
477
|
+
text_field :email
|
|
478
|
+
end.run_once!(auto_close_window: true)
|
|
479
|
+
|
|
480
|
+
puts result # => {name: "John", email: "john@example.com"}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
- Shows a "Submit to Agent" button
|
|
484
|
+
- Returns data when submitted
|
|
485
|
+
- Auto-closes browser (optional)
|
|
486
|
+
- Timeout after 300s (configurable)
|
|
487
|
+
|
|
488
|
+
### 3. Service Mode (multi-app)
|
|
489
|
+
|
|
490
|
+
Run multiple apps on one server:
|
|
491
|
+
|
|
492
|
+
```bash
|
|
493
|
+
# Start service
|
|
494
|
+
streamweaver service start
|
|
495
|
+
|
|
496
|
+
# Load apps
|
|
497
|
+
streamweaver run hello.rb
|
|
498
|
+
streamweaver run todo.rb
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Each app gets its own route (`/apps/{id}`) and isolated state.
|
|
502
|
+
|
|
503
|
+
## Form System
|
|
504
|
+
|
|
505
|
+
### Immediate Mode (default)
|
|
506
|
+
|
|
507
|
+
Every input change triggers a server round-trip:
|
|
508
|
+
|
|
509
|
+
```ruby
|
|
510
|
+
text_field :name # POST /update on every keystroke (debounced)
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Deferred Mode (form blocks)
|
|
514
|
+
|
|
515
|
+
Group inputs and submit together:
|
|
516
|
+
|
|
517
|
+
```ruby
|
|
518
|
+
form :settings do
|
|
519
|
+
text_field :email
|
|
520
|
+
checkbox :notify, "Send notifications"
|
|
521
|
+
|
|
522
|
+
submit "Save" do |values|
|
|
523
|
+
# values = {email: "...", notify: true}
|
|
524
|
+
save_settings(values)
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Form inputs use Alpine-only binding until submit, then POST to `/form/:name`.
|
|
530
|
+
|
|
531
|
+
## Callbacks and Events
|
|
532
|
+
|
|
533
|
+
### Button Callbacks
|
|
534
|
+
|
|
535
|
+
```ruby
|
|
536
|
+
button "Submit" do |state|
|
|
537
|
+
# This Ruby code runs on the server when clicked
|
|
538
|
+
puts state[:name]
|
|
539
|
+
state[:submitted] = true
|
|
540
|
+
end
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Input Callbacks
|
|
544
|
+
|
|
545
|
+
```ruby
|
|
546
|
+
text_field :email, on_change: ->(state, value) {
|
|
547
|
+
state[:valid] = value.include?("@")
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
text_field :search, on_blur: ->(state, value) {
|
|
551
|
+
state[:results] = search_api(value)
|
|
552
|
+
}
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
Routes to `/event/:key` instead of `/update` when callbacks are present.
|
|
556
|
+
|
|
557
|
+
## Additional DSL Features
|
|
558
|
+
|
|
559
|
+
### App Configuration Options
|
|
560
|
+
|
|
561
|
+
```ruby
|
|
562
|
+
app "My App",
|
|
563
|
+
layout: :wide, # :default, :wide, :full, :fluid
|
|
564
|
+
theme: :dashboard, # :default, :dashboard, :document
|
|
565
|
+
theme_overrides: { color_primary: "#0066cc" },
|
|
566
|
+
scripts: ["custom.js"],
|
|
567
|
+
stylesheets: ["custom.css"],
|
|
568
|
+
components: [MyHelpers] # Mixin modules
|
|
569
|
+
do
|
|
570
|
+
# ...
|
|
571
|
+
end
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### Dynamic Content with Procs
|
|
575
|
+
|
|
576
|
+
Text and markdown can accept procs for dynamic content:
|
|
577
|
+
|
|
578
|
+
```ruby
|
|
579
|
+
# Static
|
|
580
|
+
text "Hello"
|
|
581
|
+
|
|
582
|
+
# Dynamic - re-evaluated each render
|
|
583
|
+
text -> (s) { "Count: #{s[:count]}" }
|
|
584
|
+
|
|
585
|
+
# Also works with markdown
|
|
586
|
+
md -> (s) { "**Status:** #{s[:status]}" }
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### Header Aliases
|
|
590
|
+
|
|
591
|
+
```ruby
|
|
592
|
+
header1 "Main Title" # <h1>
|
|
593
|
+
header2 "Section" # <h2>
|
|
594
|
+
header3 "Subsection" # <h3>
|
|
595
|
+
header "Also h2" # alias for header2
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### Markdown Shorthand
|
|
599
|
+
|
|
600
|
+
```ruby
|
|
601
|
+
md "**Bold** and *italic*"
|
|
602
|
+
markdown "Same as md" # alias
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### Layout Components
|
|
606
|
+
|
|
607
|
+
```ruby
|
|
608
|
+
# Vertical/horizontal stacking
|
|
609
|
+
vstack spacing: :md do
|
|
610
|
+
text "Item 1"
|
|
611
|
+
text "Item 2"
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
hstack spacing: :sm, justify: :between do
|
|
615
|
+
button "Left"
|
|
616
|
+
button "Right"
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# Multi-column layouts
|
|
620
|
+
columns widths: ['30%', '70%'] do
|
|
621
|
+
column { text "Sidebar" }
|
|
622
|
+
column { text "Main content" }
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
# Grid layouts
|
|
626
|
+
grid columns: 3, gap: :md do
|
|
627
|
+
card { text "Card 1" }
|
|
628
|
+
card { text "Card 2" }
|
|
629
|
+
card { text "Card 3" }
|
|
630
|
+
end
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Container Components
|
|
634
|
+
|
|
635
|
+
```ruby
|
|
636
|
+
# Cards with sections
|
|
637
|
+
card do
|
|
638
|
+
card_header "Title"
|
|
639
|
+
card_body do
|
|
640
|
+
text "Content"
|
|
641
|
+
end
|
|
642
|
+
card_footer do
|
|
643
|
+
button "Action"
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# Collapsible sections
|
|
648
|
+
collapsible "More Details", expanded: false do
|
|
649
|
+
text "Hidden content"
|
|
650
|
+
end
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### Navigation Components
|
|
654
|
+
|
|
655
|
+
```ruby
|
|
656
|
+
# Tabs
|
|
657
|
+
tabs :my_tabs, variant: :line do # :line, :enclosed, :soft-rounded
|
|
658
|
+
tab "First" do
|
|
659
|
+
text "Tab 1 content"
|
|
660
|
+
end
|
|
661
|
+
tab "Second" do
|
|
662
|
+
text "Tab 2 content"
|
|
663
|
+
end
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
# Breadcrumbs
|
|
667
|
+
breadcrumbs separator: ">" do
|
|
668
|
+
crumb "Home", href: "/"
|
|
669
|
+
crumb "Products", href: "/products"
|
|
670
|
+
crumb "Current" # No href = current page
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
# Dropdowns
|
|
674
|
+
dropdown do
|
|
675
|
+
trigger { button "Menu" }
|
|
676
|
+
menu do
|
|
677
|
+
menu_item "Edit" do |s|
|
|
678
|
+
s[:action] = "edit"
|
|
679
|
+
end
|
|
680
|
+
menu_divider
|
|
681
|
+
menu_item "Delete", style: :destructive do |s|
|
|
682
|
+
s[:action] = "delete"
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
### Modal Dialogs
|
|
689
|
+
|
|
690
|
+
```ruby
|
|
691
|
+
# Open modal via button
|
|
692
|
+
button "Show Dialog" do |s|
|
|
693
|
+
s[:confirm_open] = true
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# Modal definition
|
|
697
|
+
modal :confirm, title: "Confirm", size: :sm do
|
|
698
|
+
text "Are you sure?"
|
|
699
|
+
|
|
700
|
+
modal_footer do
|
|
701
|
+
button "Yes" do |s|
|
|
702
|
+
s[:confirmed] = true
|
|
703
|
+
s[:confirm_open] = false
|
|
704
|
+
end
|
|
705
|
+
button "Cancel", style: :secondary do |s|
|
|
706
|
+
s[:confirm_open] = false
|
|
707
|
+
end
|
|
708
|
+
end
|
|
709
|
+
end
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
### Feedback Components
|
|
713
|
+
|
|
714
|
+
```ruby
|
|
715
|
+
# Alerts
|
|
716
|
+
alert(variant: :success) { text "Saved!" }
|
|
717
|
+
alert(variant: :error, title: "Error") { text "Failed to save" }
|
|
718
|
+
|
|
719
|
+
# Progress
|
|
720
|
+
progress_bar value: 75, show_label: true
|
|
721
|
+
|
|
722
|
+
# Spinner
|
|
723
|
+
spinner size: :md, label: "Loading..."
|
|
724
|
+
|
|
725
|
+
# Toasts (notifications)
|
|
726
|
+
toast_container position: :top_right
|
|
727
|
+
|
|
728
|
+
button "Save" do |s|
|
|
729
|
+
show_toast("Saved successfully!", variant: :success)
|
|
730
|
+
end
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### Charts
|
|
734
|
+
|
|
735
|
+
```ruby
|
|
736
|
+
bar_chart data: { "A" => 10, "B" => 20 }, title: "Sales"
|
|
737
|
+
line_chart labels: ["Jan", "Feb"], values: [10, 20]
|
|
738
|
+
pie_chart data: :chart_data # From state
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
### Helper Module Pattern
|
|
742
|
+
|
|
743
|
+
Extend the DSL with custom methods:
|
|
744
|
+
|
|
745
|
+
```ruby
|
|
746
|
+
module MyHelpers
|
|
747
|
+
def user_card(user)
|
|
748
|
+
card do
|
|
749
|
+
header3 user[:name]
|
|
750
|
+
text user[:email]
|
|
751
|
+
end
|
|
752
|
+
end
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
app "Users", components: [MyHelpers] do
|
|
756
|
+
state[:users].each do |user|
|
|
757
|
+
user_card(user)
|
|
758
|
+
end
|
|
759
|
+
end
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
## Summary: The Magic in 7 Steps
|
|
763
|
+
|
|
764
|
+
1. **You write a DSL block** - Ruby code that builds UI
|
|
765
|
+
2. **Block is stored in App** - Not executed immediately
|
|
766
|
+
3. **User interacts in browser** - Alpine.js captures, HTMX sends
|
|
767
|
+
4. **Server receives request** - Sinatra route handles it
|
|
768
|
+
5. **State is updated** - From form params or button callback
|
|
769
|
+
6. **DSL block re-executes** - `instance_eval(&@block)` with new state
|
|
770
|
+
7. **Fresh HTML returned** - HTMX swaps it in, loop continues
|
|
771
|
+
|
|
772
|
+
Your Ruby code IS the UI definition AND the event handlers. The framework handles the plumbing to make this seamless.
|