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,265 @@
|
|
|
1
|
+
# StreamWeaver CRUD Patterns
|
|
2
|
+
|
|
3
|
+
StreamWeaver handles CRUD differently from Rails. Because button callbacks are plain Ruby, data mutation happens directly — no controller layer, no POST-redirect-GET cycle. The URL routing layer handles deep-linking to edit views.
|
|
4
|
+
|
|
5
|
+
## The Core Insight
|
|
6
|
+
|
|
7
|
+
A StreamWeaver button callback IS a POST handler:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
button "Save" do |s|
|
|
11
|
+
initiative = load_initiative(s[:editing_id])
|
|
12
|
+
initiative[:title] = s[:edit_title]
|
|
13
|
+
initiative[:status] = s[:edit_status]
|
|
14
|
+
save_initiative(initiative)
|
|
15
|
+
s[:editing_initiative] = false # exit edit mode
|
|
16
|
+
s[:flash] = "Saved!"
|
|
17
|
+
end
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
No controller. No form action. No redirect. Just Ruby.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Read (Index + Show)
|
|
25
|
+
|
|
26
|
+
Standard StreamWeaver patterns — load from YAML/store, render in app block:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# Index: list all initiatives
|
|
30
|
+
initiatives = load_initiatives
|
|
31
|
+
table initiatives do
|
|
32
|
+
column :title
|
|
33
|
+
column :status
|
|
34
|
+
column(:id) { |i| button("View") { |s| s[:initiative_id] = i[:id] } }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Show: render detail when initiative_id is set
|
|
38
|
+
if state[:initiative_id]
|
|
39
|
+
init = load_initiative(state[:initiative_id])
|
|
40
|
+
card do
|
|
41
|
+
header3 init[:title]
|
|
42
|
+
text "Status: #{init[:status]}"
|
|
43
|
+
button "Edit" do |s|
|
|
44
|
+
s[:editing_initiative] = true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Create
|
|
53
|
+
|
|
54
|
+
Use a modal or inline form. State holds the draft fields; the save button commits to the store.
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
button "New Initiative", style: :primary do |s|
|
|
58
|
+
s[:creating_initiative] = true
|
|
59
|
+
s[:new_init_title] = ""
|
|
60
|
+
s[:new_init_status] = "active"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
modal :new_initiative, title: "New Initiative" do
|
|
64
|
+
text_field :new_init_title, placeholder: "Title", submit: false
|
|
65
|
+
select :new_init_status, %w[active paused completed], submit: false
|
|
66
|
+
|
|
67
|
+
modal_footer do
|
|
68
|
+
button "Create", style: :primary do |s|
|
|
69
|
+
id = create_initiative(title: s[:new_init_title], status: s[:new_init_status])
|
|
70
|
+
s[:creating_initiative] = false
|
|
71
|
+
s[:initiative_id] = id # navigate to new record
|
|
72
|
+
s[:flash] = "Initiative created"
|
|
73
|
+
end
|
|
74
|
+
button "Cancel" do |s|
|
|
75
|
+
s[:creating_initiative] = false
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The `modal :new_initiative` opens when `state[:new_initiative_open]` is true (StreamWeaver convention: `modal :key` → controlled by `state[:key_open]`).
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Update (Edit Form)
|
|
86
|
+
|
|
87
|
+
### Option A: Modal Edit (no URL change)
|
|
88
|
+
|
|
89
|
+
Good for quick edits on a list view. State holds the record being edited.
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
initiatives.each do |init|
|
|
93
|
+
hstack do
|
|
94
|
+
text init[:title]
|
|
95
|
+
button "Edit" do |s|
|
|
96
|
+
s[:editing_id] = init[:id]
|
|
97
|
+
# load current values into edit fields
|
|
98
|
+
s[:edit_title] = init[:title]
|
|
99
|
+
s[:edit_status] = init[:status]
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
if state[:editing_id]
|
|
105
|
+
modal :edit_initiative, title: "Edit Initiative" do
|
|
106
|
+
text_field :edit_title, placeholder: "Title", submit: false
|
|
107
|
+
select :edit_status, %w[active paused completed], submit: false
|
|
108
|
+
|
|
109
|
+
modal_footer do
|
|
110
|
+
button "Save", style: :primary do |s|
|
|
111
|
+
update_initiative(s[:editing_id], title: s[:edit_title], status: s[:edit_status])
|
|
112
|
+
s[:editing_id] = nil
|
|
113
|
+
s[:flash] = "Saved"
|
|
114
|
+
end
|
|
115
|
+
button "Cancel" do |s|
|
|
116
|
+
s[:editing_id] = nil
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Option B: URL-Addressable Edit Page
|
|
124
|
+
|
|
125
|
+
Pairs with `route_with` routing (see `routing.md`). Visiting `/initiative/init-001/edit` seeds `state[:editing_initiative] = true`.
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
# route_with parser handles: /initiative/:id/edit → { initiative_id: id, editing_initiative: true }
|
|
129
|
+
# route_with builder handles: editing_initiative? → /initiative/:id/edit
|
|
130
|
+
|
|
131
|
+
if state[:editing_initiative] && state[:initiative_id]
|
|
132
|
+
init = load_initiative(state[:initiative_id])
|
|
133
|
+
|
|
134
|
+
# Seed edit fields on first load
|
|
135
|
+
if state[:edit_seeded_for] != state[:initiative_id]
|
|
136
|
+
state[:edit_title] = init[:title]
|
|
137
|
+
state[:edit_status] = init[:status]
|
|
138
|
+
state[:edit_seeded_for] = state[:initiative_id]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
card do
|
|
142
|
+
header3 "Edit: #{init[:title]}"
|
|
143
|
+
|
|
144
|
+
text_field :edit_title, label: "Title", submit: false
|
|
145
|
+
select :edit_status, %w[active paused completed], submit: false
|
|
146
|
+
|
|
147
|
+
hstack do
|
|
148
|
+
button "Save", style: :primary do |s|
|
|
149
|
+
update_initiative(s[:initiative_id], title: s[:edit_title], status: s[:edit_status])
|
|
150
|
+
s[:editing_initiative] = false # URL drops back to /initiative/:id
|
|
151
|
+
s[:flash] = "Saved"
|
|
152
|
+
end
|
|
153
|
+
button "Cancel", style: :secondary do |s|
|
|
154
|
+
s[:editing_initiative] = false
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Field seeding pattern**: The `edit_seeded_for` guard ensures edit fields are seeded from the record once per initiative, not overwritten on every re-render. Without it, every button click would reset the form to the stored value.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Delete
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
button "Delete", style: :danger do |s|
|
|
169
|
+
s[:confirm_delete_id] = init[:id]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if state[:confirm_delete_id]
|
|
173
|
+
alert(variant: :warning) do
|
|
174
|
+
text "Delete this initiative? This cannot be undone."
|
|
175
|
+
hstack do
|
|
176
|
+
button "Confirm Delete", style: :danger do |s|
|
|
177
|
+
delete_initiative(s[:confirm_delete_id])
|
|
178
|
+
s[:confirm_delete_id] = nil
|
|
179
|
+
s[:initiative_id] = nil # navigate away
|
|
180
|
+
s[:flash] = "Deleted"
|
|
181
|
+
end
|
|
182
|
+
button "Cancel" do |s|
|
|
183
|
+
s[:confirm_delete_id] = nil
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Flash Messages
|
|
193
|
+
|
|
194
|
+
StreamWeaver has no built-in flash. Use `state[:flash]` with a conditional render:
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
if state[:flash]
|
|
198
|
+
alert(variant: :success) { text state[:flash] }
|
|
199
|
+
# Auto-clear: set flash to nil after it's rendered once
|
|
200
|
+
# Use a counter trick or a dedicated clear button
|
|
201
|
+
end
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
For auto-dismiss, use a toast:
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
# In button callback:
|
|
208
|
+
s[:toasts] ||= []
|
|
209
|
+
s[:toasts] << { id: SecureRandom.hex(4), message: "Saved!", type: :success, duration: 3000 }
|
|
210
|
+
|
|
211
|
+
# In render:
|
|
212
|
+
toast_container # renders toasts from state[:toasts]
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Store Adapter Pattern
|
|
218
|
+
|
|
219
|
+
Keep data access decoupled from the UI. Centralizing it makes the CRUD callbacks readable:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
module InitiativeStore
|
|
223
|
+
YAML_PATH = File.expand_path("~/cultiv-os/cabinet/initiatives/master-initiatives.yaml")
|
|
224
|
+
|
|
225
|
+
def self.all
|
|
226
|
+
YAML.safe_load_file(YAML_PATH, symbolize_names: true)[:initiatives] || []
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def self.find(id)
|
|
230
|
+
all.find { |i| i[:id] == id }
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def self.update(id, attrs)
|
|
234
|
+
data = YAML.safe_load_file(YAML_PATH)
|
|
235
|
+
idx = data["initiatives"].index { |i| i["id"] == id }
|
|
236
|
+
return false unless idx
|
|
237
|
+
data["initiatives"][idx].merge!(attrs.transform_keys(&:to_s))
|
|
238
|
+
File.write(YAML_PATH, data.to_yaml)
|
|
239
|
+
true
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# In app:
|
|
244
|
+
button "Save" do |s|
|
|
245
|
+
InitiativeStore.update(s[:initiative_id], title: s[:edit_title], status: s[:edit_status])
|
|
246
|
+
s[:editing_initiative] = false
|
|
247
|
+
end
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
See [docs/resource-dsl.md](resource-dsl.md) for the implemented `resource` DSL.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Decision Guide
|
|
257
|
+
|
|
258
|
+
| Interaction | Pattern |
|
|
259
|
+
|---|---|
|
|
260
|
+
| Read-only list | `table` + state filter |
|
|
261
|
+
| Quick edit (no URL change) | Modal edit + `state[:editing_id]` |
|
|
262
|
+
| Deep-linkable edit form | `route_with` + URL-addressable edit page |
|
|
263
|
+
| Inline edit (click cell) | Not yet supported — use modal |
|
|
264
|
+
| Bulk update | `checkbox_group` + bulk action button |
|
|
265
|
+
| Delete with confirmation | Confirm state flag + conditional alert |
|
data/docs/endpoints.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Custom HTTP Endpoints — `endpoint`
|
|
2
|
+
|
|
3
|
+
The "never rewrite in Sinatra" escape hatch: register a real HTTP route (webhook
|
|
4
|
+
receiver, JSON API, file download, health check) directly from the app DSL,
|
|
5
|
+
without leaving StreamWeaver or hand-rolling a separate Sinatra/Rails service.
|
|
6
|
+
|
|
7
|
+
**This is not URL routing.** [`docs/routing.md`](routing.md) (`route_by` /
|
|
8
|
+
`route_with` / `page`) maps URL paths to *state* so the same StreamWeaver view
|
|
9
|
+
renders differently per path — it's still an HTML page render. `endpoint` is
|
|
10
|
+
the opposite: a genuine Rack route that bypasses StreamWeaver's state
|
|
11
|
+
machinery, component tree, and view rendering entirely. Use `route_by`/`page`
|
|
12
|
+
for "different tab/view depending on the URL." Use `endpoint` for "something
|
|
13
|
+
that isn't a StreamWeaver page at all" — a webhook, a JSON API, a file
|
|
14
|
+
download.
|
|
15
|
+
|
|
16
|
+
## Basic usage
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
app "My App" do
|
|
20
|
+
endpoint :get, "/api/status" do |req|
|
|
21
|
+
{ ok: true, uptime: 42 } # Hash -> 200 application/json
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
endpoint :post, "/webhook/github" do |req|
|
|
25
|
+
payload = req.body.read # req is a Rack::Request
|
|
26
|
+
handle_github_event(payload)
|
|
27
|
+
[202, {}, "queued"] # Rack triplet passes through verbatim
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# ... normal StreamWeaver UI DSL below, in the same app
|
|
31
|
+
header1 "My App"
|
|
32
|
+
end.run!
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Supported verbs: `:get`, `:post`, `:put`, `:patch`, `:delete`.
|
|
36
|
+
|
|
37
|
+
## Return value conventions
|
|
38
|
+
|
|
39
|
+
The block's return value is converted into the HTTP response:
|
|
40
|
+
|
|
41
|
+
| Return type | Response |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `Hash` | `200`, `Content-Type: application/json`, body is `JSON.generate`d |
|
|
44
|
+
| `String` | `200`, `Content-Type: text/html`, body is the string as-is |
|
|
45
|
+
| `[status, headers, body]` (Array) | Passed through to Rack verbatim — full control (custom status, headers, streaming, file downloads, etc.) |
|
|
46
|
+
| anything else | `200`, `Content-Type: text/plain`, body is `#to_s` |
|
|
47
|
+
|
|
48
|
+
Use the Rack-triplet form for anything that doesn't fit the Hash/String
|
|
49
|
+
shortcuts — custom status codes, redirects, non-JSON/HTML content types, or
|
|
50
|
+
file downloads:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
endpoint :get, "/export/report.csv" do |req|
|
|
54
|
+
csv = generate_report_csv
|
|
55
|
+
[200, { 'Content-Type' => 'text/csv', 'Content-Disposition' => 'attachment; filename="report.csv"' }, csv]
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## The block argument
|
|
60
|
+
|
|
61
|
+
The block receives the raw [`Rack::Request`](https://www.rubydoc.info/gems/rack/Rack/Request)
|
|
62
|
+
for the current request — not StreamWeaver state, not a component. Use it like
|
|
63
|
+
you would in any Rack app:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
endpoint :get, "/api/echo" do |req|
|
|
67
|
+
{ query: req.params } # query string / route params
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
endpoint :post, "/webhook/github" do |req|
|
|
71
|
+
req.body.read # raw request body
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Gotcha:** if the incoming request's `Content-Type` is
|
|
76
|
+
`application/x-www-form-urlencoded` or `multipart/form-data`, Sinatra will
|
|
77
|
+
already have consumed the body to build `params` *before* your block runs —
|
|
78
|
+
`req.body.read` comes back empty in that case. Use `req.params` for form
|
|
79
|
+
submissions instead, or make sure the sender uses a different `Content-Type`
|
|
80
|
+
(e.g. `application/json`, `text/plain`) for endpoints that need the raw body.
|
|
81
|
+
|
|
82
|
+
## No CSRF, no session, no state
|
|
83
|
+
|
|
84
|
+
`endpoint` is a raw Rack route. It does **not** go through StreamWeaver's
|
|
85
|
+
session/state machinery, CSRF protection, or component rendering — the block
|
|
86
|
+
you write is the entire request handler. If you need auth, verify a webhook
|
|
87
|
+
signature, parse JSON, etc., do it yourself inside the block just as you would
|
|
88
|
+
in a plain Sinatra route.
|
|
89
|
+
|
|
90
|
+
## Precedence: internal routes always win
|
|
91
|
+
|
|
92
|
+
StreamWeaver's own framework routes (`/update`, `/action/*`, `/submit`,
|
|
93
|
+
`/event/*`, `/form/*`, `/theme/*`, `/sw/*`, plus a handful of others like `/`,
|
|
94
|
+
`/stream`, `/deck/*`) are defined before any `endpoint` you register. Sinatra
|
|
95
|
+
dispatches to the *first* route that matches a request, so if an `endpoint`
|
|
96
|
+
path collides with one of these, the internal route always wins — your block
|
|
97
|
+
is simply never reached.
|
|
98
|
+
|
|
99
|
+
`endpoint` warns at registration time when it detects a collision with a
|
|
100
|
+
known-reserved path/prefix, so the mistake is loud instead of silently eaten:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
StreamWeaver: endpoint POST /update collides with a StreamWeaver-internal route
|
|
104
|
+
and will never be reached — the internal route always wins.
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Pick paths outside StreamWeaver's reserved prefixes (`/action/`, `/event/`,
|
|
108
|
+
`/form/`, `/theme/`, `/sw/`) and outside `/update` and `/submit` to avoid this
|
|
109
|
+
entirely.
|
|
110
|
+
|
|
111
|
+
## Service mode (`streamweaver run`)
|
|
112
|
+
|
|
113
|
+
In standalone mode (`App#run!`), endpoints are served at the path exactly as
|
|
114
|
+
registered (e.g. `GET /api/status`).
|
|
115
|
+
|
|
116
|
+
In multi-app service mode, each app is mounted under `/apps/:app_id`, so its
|
|
117
|
+
endpoints are scoped under that prefix too: an app registered as `svc123`
|
|
118
|
+
serves `endpoint :get, "/api/status"` at `GET /apps/svc123/api/status` — and
|
|
119
|
+
equally via the app's human-readable slug, e.g. `GET /apps/my-dashboard/api/status`. The
|
|
120
|
+
same precedence rule applies — StreamWeaver's fixed `/apps/:app_id/...` routes
|
|
121
|
+
(update, action, event, form, theme) are defined first and always win on
|
|
122
|
+
collision.
|
data/docs/for_llms.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
../llms.txt
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# StreamWeaver Form Patterns
|
|
2
|
+
|
|
3
|
+
Canonical patterns for form/input/selection UIs. Use this to pick the right component for your use case.
|
|
4
|
+
|
|
5
|
+
## Pattern Reference
|
|
6
|
+
|
|
7
|
+
| Pattern | Use Case | Component | Example |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| Instant feedback | Filters, toggles | Default inputs (auto-submit) | `select :filter, options` |
|
|
10
|
+
| Multi-select then act | Batch operations | `checkbox_group` or `submit: false` + button | Check items then "Delete Selected" |
|
|
11
|
+
| Traditional form | Edit/create with Save/Cancel | `form` block | `form :edit_person do...end` |
|
|
12
|
+
| Single selection for later action | Pick category then add item | `select` with `submit: false` | `select :cat, names, submit: false` |
|
|
13
|
+
| Scrollable list | Long item lists | `scroll_box` | `scroll_box(max_height: "400px") do...end` |
|
|
14
|
+
|
|
15
|
+
## Examples
|
|
16
|
+
|
|
17
|
+
### Instant Feedback (default)
|
|
18
|
+
```ruby
|
|
19
|
+
# Every change triggers a server round-trip and re-render
|
|
20
|
+
select :filter, ["All", "Active", "Done"]
|
|
21
|
+
text_field :search, placeholder: "Search..."
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Single Selection for Later Action
|
|
25
|
+
```ruby
|
|
26
|
+
# User picks a category, then clicks Add — no round-trip on select change
|
|
27
|
+
cat_names = categories.map { |c| c["name"] }
|
|
28
|
+
select :category, cat_names, default: cat_names.first, submit: false
|
|
29
|
+
|
|
30
|
+
button "Add Item", style: :primary do |s|
|
|
31
|
+
chosen = s[:category] # string name, not an index
|
|
32
|
+
# ... use chosen
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Multi-Select Then Act
|
|
37
|
+
```ruby
|
|
38
|
+
checkbox_group :selected_items do
|
|
39
|
+
items.each { |item| item item.id do text item.name end }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
button "Delete Selected" do |s|
|
|
43
|
+
ids = s[:selected_items] # array of checked values
|
|
44
|
+
# ... delete them
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Traditional Form
|
|
49
|
+
```ruby
|
|
50
|
+
form :edit_person do
|
|
51
|
+
text_field :name, placeholder: "Name"
|
|
52
|
+
text_field :email, placeholder: "Email"
|
|
53
|
+
submit "Save" do |s|
|
|
54
|
+
data = s[:edit_person] # { name: "...", email: "..." }
|
|
55
|
+
# ... save
|
|
56
|
+
end
|
|
57
|
+
cancel "Cancel"
|
|
58
|
+
end
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Scrollable List
|
|
62
|
+
```ruby
|
|
63
|
+
scroll_box(max_height: "400px") do
|
|
64
|
+
items.each do |item|
|
|
65
|
+
hstack spacing: :xs, align: :center do
|
|
66
|
+
text item.name
|
|
67
|
+
button "Edit", id: "edit_#{item.id}" do |s|
|
|
68
|
+
# ...
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Anti-Patterns
|
|
76
|
+
|
|
77
|
+
### 1. Button-as-Radio
|
|
78
|
+
**Wrong** — Using buttons + state to simulate selection:
|
|
79
|
+
```ruby
|
|
80
|
+
# DON'T DO THIS
|
|
81
|
+
hstack do
|
|
82
|
+
categories.each_with_index do |cat, i|
|
|
83
|
+
button cat, style: state[:cat] == i ? :primary : :secondary do |s|
|
|
84
|
+
s[:cat] = i
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Right** — Use `select` or `radio_group`:
|
|
91
|
+
```ruby
|
|
92
|
+
select :cat, category_names, submit: false
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 2. Integer State Keys
|
|
96
|
+
**Wrong** — Storing array indices in session state:
|
|
97
|
+
```ruby
|
|
98
|
+
s[:cat] = i # breaks if list order changes
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Right** — Store the string name, resolve to index at action time:
|
|
102
|
+
```ruby
|
|
103
|
+
s[:cat_name] = cat["name"]
|
|
104
|
+
# later:
|
|
105
|
+
idx = categories.index { |c| c["name"] == s[:cat_name] } || 0
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 3. Forgetting `submit: false`
|
|
109
|
+
**Wrong** — Inputs inside button-action workflows auto-submit by default:
|
|
110
|
+
```ruby
|
|
111
|
+
text_field :label # triggers round-trip on every keystroke!
|
|
112
|
+
button "Add" do |s| ... end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Right**:
|
|
116
|
+
```ruby
|
|
117
|
+
text_field :label, submit: false
|
|
118
|
+
button "Add" do |s| ... end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 4. Session Cookie Overflow
|
|
122
|
+
Large text values in state overflow the 4KB cookie limit.
|
|
123
|
+
**Fix**: Use `transient: true` to exclude from the session cookie:
|
|
124
|
+
```ruby
|
|
125
|
+
text_area :paste_area, transient: true
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 5. Stale Session After Restart
|
|
129
|
+
Static dev secret preserves cookies across restarts. If your state schema changed, old cookies cause errors.
|
|
130
|
+
**Fix**: Store state as recoverable strings (not fragile indices). Use `--reset` flag or clear cookies after schema changes.
|
|
131
|
+
|
|
132
|
+
### 6. Transient Fields Never Resync From the Server
|
|
133
|
+
`transient: true` is documented above (#4) as "exclude from the session cookie" — but the
|
|
134
|
+
Alpine adapter's morph-merge goes further than that: it also skips writing the server's value
|
|
135
|
+
back into the browser's reactive store on *every* subsequent update, not just full-cookie
|
|
136
|
+
snapshots (`adapter/alpinejs.rb`, the `htmx:beforeSwap` handler — `if (transientKeys.has(k))
|
|
137
|
+
return;`). In practice this means once a transient field's DOM node exists, only the user's own
|
|
138
|
+
typing ever changes what it shows — a server-side `s[:x] = ""` after a button handler, or a
|
|
139
|
+
fresh prefill for a *different* record reusing the same state key, is silently dropped.
|
|
140
|
+
|
|
141
|
+
**Wrong** — clearing a transient field after its own submit handler:
|
|
142
|
+
```ruby
|
|
143
|
+
text_field :custom_item, submit: false, transient: true
|
|
144
|
+
button "Add" do |s|
|
|
145
|
+
add_item(s[:custom_item])
|
|
146
|
+
s[:custom_item] = "" # never reaches the browser — field still shows the old value
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Wrong** — reusing one transient key across multiple records' edit forms (e.g. a schema-driven
|
|
151
|
+
edit UI where every row's Time/Date field binds to the same `:edit_time`/`:edit_date` key
|
|
152
|
+
instead of a per-row key): opening row B's edit form after row A's still shows row A's value,
|
|
153
|
+
because the fresh prefill for row B never overwrites what's already sitting in the browser.
|
|
154
|
+
|
|
155
|
+
**Fix**: if a field's value must ever be authoritatively reset by the server (a post-submit
|
|
156
|
+
clear, a fresh prefill on reopen), don't mark it `transient: true` unless it's genuinely a
|
|
157
|
+
one-shot, never-cleared, never-reused input (the large-paste-area case #4 was written for).
|
|
158
|
+
Short single-value fields rarely have a real cookie-size reason to be transient — dropping the
|
|
159
|
+
flag is usually simpler than working around the resync gap. If cookie size is the actual
|
|
160
|
+
concern *and* the field needs server-driven resets, the two needs are currently in tension —
|
|
161
|
+
there's no flag that gives "excluded from cookie" without also losing resync.
|
|
162
|
+
|
|
163
|
+
Found via `~/work/cultiv-ai/apps/health_dashboard/app.rb`, 2026-08-20 — a custom-item field
|
|
164
|
+
that never visually cleared after Add, and an entry-edit form's Time/Date fields that leaked
|
|
165
|
+
one edit's value into the next.
|