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,1223 @@
|
|
|
1
|
+
# StreamWeaver Components Reference
|
|
2
|
+
|
|
3
|
+
Complete reference for all StreamWeaver components. For quick usage, see the main `llms.txt`.
|
|
4
|
+
|
|
5
|
+
> Building a doc that also needs to render in `canvas-read` or a `streamweaver export`? Not every component below is fully interactive outside the live canvas — see [`docs/frontend-only.md`](frontend-only.md) for what survives each context and why.
|
|
6
|
+
|
|
7
|
+
## Text Display
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
text "Plain paragraph" # Literal text - what you type is what you get
|
|
11
|
+
text "Value: #{state[:value]}" # String interpolation works
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
> **WARNING for LLMs:** `text` renders its argument as a literal string. Markdown syntax like `**bold**` or `## heading` will appear as raw characters on screen — not as formatted HTML. Use `md` for markdown, or `header*` for section labels.
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
# WRONG — asterisks show up literally on screen
|
|
18
|
+
text "**Select the article PDF:**"
|
|
19
|
+
|
|
20
|
+
# CORRECT — use md for inline markdown formatting
|
|
21
|
+
md "**Select the article PDF:**"
|
|
22
|
+
|
|
23
|
+
# BETTER — use semantic headers for labels and section titles
|
|
24
|
+
header3 "Select the article PDF:"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Headers
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
header "Section Title" # <h2> - default header level
|
|
31
|
+
header1 "Page Title" # <h1>
|
|
32
|
+
header2 "Section" # <h2>
|
|
33
|
+
header3 "Subsection" # <h3>
|
|
34
|
+
header4 "Minor Section" # <h4>
|
|
35
|
+
header5 "Sub-subsection" # <h5>
|
|
36
|
+
header6 "Smallest" # <h6>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Markdown Content
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
md "**Bold**, *italic*, and `code`" # Full markdown parsing
|
|
43
|
+
md "## Headers work too" # Headers within markdown blocks
|
|
44
|
+
markdown "Same as md" # Alias for md
|
|
45
|
+
|
|
46
|
+
# Supported markdown:
|
|
47
|
+
# - **bold** → <strong>
|
|
48
|
+
# - *italic* → <em>
|
|
49
|
+
# - `code` → <code>
|
|
50
|
+
# - [link](url) → <a href="url">link</a>
|
|
51
|
+
# - ## headers → <h2>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Text Input
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
text_field :key, placeholder: "Hint text"
|
|
58
|
+
text_area :key, placeholder: "Multi-line", rows: 5
|
|
59
|
+
|
|
60
|
+
# Disable auto-submit (for display-only or manual submission)
|
|
61
|
+
text_field :key, placeholder: "Edit me", submit: false
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Dates
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
date_field :due_on, label: "Due date"
|
|
68
|
+
|
|
69
|
+
# Constrain the selectable range (both are ISO 8601 strings)
|
|
70
|
+
date_field :due_on, min: "2026-01-01", max: "2027-12-31"
|
|
71
|
+
|
|
72
|
+
# Disable auto-submit
|
|
73
|
+
date_field :due_on, submit: false
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Renders a native `<input type="date">` — no custom JS, no bundled calendar
|
|
77
|
+
widget. The browser supplies the calendar popup and, on mobile, its own
|
|
78
|
+
native date picker. Works the same as `text_field` inside `form`/`scope`
|
|
79
|
+
blocks.
|
|
80
|
+
|
|
81
|
+
State always holds an **ISO 8601 string** (`"YYYY-MM-DD"`), never a `Date`
|
|
82
|
+
object — coerce it yourself when you need one:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
if (parsed = StreamWeaver::Components::DateField.to_date(state[:due_on]))
|
|
86
|
+
text "Parsed as a Date: #{parsed.iso8601}"
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`DateField.to_date` returns `nil` for blank or unparsable input instead of
|
|
91
|
+
raising.
|
|
92
|
+
|
|
93
|
+
## Selection
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
# Dropdown
|
|
97
|
+
select :color, ["Red", "Green", "Blue"]
|
|
98
|
+
|
|
99
|
+
# Dropdown with default value
|
|
100
|
+
select :priority, ["Low", "Medium", "High"], default: "Medium"
|
|
101
|
+
|
|
102
|
+
# Radio buttons (all options visible)
|
|
103
|
+
radio_group :answer, ["Option A", "Option B", "Option C"]
|
|
104
|
+
|
|
105
|
+
# Disable auto-submit
|
|
106
|
+
select :role, ["Admin", "User"], submit: false
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Boolean
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
checkbox :agree, "I accept the terms"
|
|
113
|
+
# state[:agree] will be true/false
|
|
114
|
+
|
|
115
|
+
# Disable auto-submit
|
|
116
|
+
checkbox :preview_flag, "Show preview", submit: false
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Checkbox Group (Multi-Select with Select All/None)
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
# For batch selection (e.g., emails, files, items)
|
|
123
|
+
checkbox_group :selected_items, select_all: "Select All", select_none: "Clear" do
|
|
124
|
+
items.each do |item|
|
|
125
|
+
item item.id do
|
|
126
|
+
text item.name
|
|
127
|
+
# Any components can be nested here
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
# state[:selected_items] = ["id1", "id3", ...] (array of selected values)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Buttons
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
button "Primary" do |state|
|
|
138
|
+
# Action when clicked
|
|
139
|
+
state[:clicked] = true
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
button "Secondary", style: :secondary do |state|
|
|
143
|
+
# Secondary styling
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Display-only button (no server request, for previews)
|
|
147
|
+
button "Preview Button", submit: false
|
|
148
|
+
button "Cancel", style: :secondary, submit: false
|
|
149
|
+
|
|
150
|
+
# Stable identity for buttons in loops
|
|
151
|
+
voices.each do |v|
|
|
152
|
+
button "Test", key: v[:name] do |state|
|
|
153
|
+
speak(v[:name]) # Each button triggers its own callback
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Verbatim id when other code targets the element by selector
|
|
158
|
+
button "Delete", id: "delete-voice-#{v[:name]}" do |state| ... end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Identity precedence: `id:` > `key:` > auto-derivation.** Auto-derived ids hash
|
|
162
|
+
the label plus the block's source location, so loop iterations derive the same
|
|
163
|
+
id — StreamWeaver auto-disambiguates each repeat occurrence (`-dup-N`) and warns
|
|
164
|
+
once, so every button dispatches its own callback with no author intervention.
|
|
165
|
+
Those suffixes are position-stable, not content-stable: inserting or deleting an
|
|
166
|
+
earlier item shifts the later ones. Pass `key: record_id` (String/Symbol/Integer
|
|
167
|
+
only — anything else raises) when identity must survive reordering or deletion,
|
|
168
|
+
and `id:` when you need the emitted DOM id verbatim. `strict_ids: true` (per app,
|
|
169
|
+
`StreamWeaver.strict_ids = true` globally, or `SW_STRICT_IDS=1`) turns the
|
|
170
|
+
warning into a raise in dev/test — production still only warns. Full write-up in
|
|
171
|
+
`llms.txt` under "Interactive IDs and keying".
|
|
172
|
+
|
|
173
|
+
## Layout Containers
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
# Generic div
|
|
177
|
+
div class: "my-class" do
|
|
178
|
+
text "Nested content"
|
|
179
|
+
text_field :nested_field
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Card (styled container)
|
|
183
|
+
card do
|
|
184
|
+
header3 "Card Title"
|
|
185
|
+
text "Card content here"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Structured card with header, body, footer
|
|
189
|
+
card do
|
|
190
|
+
card_header "Card Title"
|
|
191
|
+
card_body do
|
|
192
|
+
text "Main content goes here"
|
|
193
|
+
text_field :field
|
|
194
|
+
end
|
|
195
|
+
card_footer do
|
|
196
|
+
button "Cancel", style: :secondary
|
|
197
|
+
button "Save"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Card header with badge + right-aligned meta (flex row: badge | title | meta)
|
|
202
|
+
card do
|
|
203
|
+
card_header "C1 — Title", badge: "C1", meta: "scheduler secretary"
|
|
204
|
+
card_body do
|
|
205
|
+
text "Main content goes here"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Stacking Layouts (VStack / HStack)
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
# Vertical stack with spacing
|
|
214
|
+
vstack spacing: :lg do
|
|
215
|
+
text "Item 1"
|
|
216
|
+
text "Item 2"
|
|
217
|
+
text "Item 3"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# VStack with dividers between items
|
|
221
|
+
vstack spacing: :md, divider: true do
|
|
222
|
+
text "Item with divider below"
|
|
223
|
+
text "Another item"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Horizontal stack
|
|
227
|
+
hstack spacing: :md, align: :center do
|
|
228
|
+
button "Action 1"
|
|
229
|
+
button "Action 2", style: :secondary
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# HStack with justify (spread content)
|
|
233
|
+
hstack justify: :between do
|
|
234
|
+
text "Left"
|
|
235
|
+
text "Right"
|
|
236
|
+
end
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Spacing options: `:xs`, `:sm`, `:md`, `:lg`, `:xl`
|
|
240
|
+
Align options: `:start`, `:center`, `:end`
|
|
241
|
+
Justify options (hstack only): `:start`, `:center`, `:end`, `:between`, `:around`
|
|
242
|
+
|
|
243
|
+
## Grid Layouts
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
# Fixed 3-column grid
|
|
247
|
+
grid columns: 3, gap: :md do
|
|
248
|
+
card { text "Item 1" }
|
|
249
|
+
card { text "Item 2" }
|
|
250
|
+
card { text "Item 3" }
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Responsive grid: 1 col mobile, 2 tablet, 3 desktop
|
|
254
|
+
grid columns: [1, 2, 3], gap: :lg do
|
|
255
|
+
items.each do |item|
|
|
256
|
+
card { text item.name }
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Columns (Multi-Column Layout)
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
# Equal-width columns (default)
|
|
265
|
+
columns do
|
|
266
|
+
column do
|
|
267
|
+
text "Left column"
|
|
268
|
+
end
|
|
269
|
+
column do
|
|
270
|
+
text "Right column"
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Custom widths (sidebar + main content)
|
|
275
|
+
columns widths: ['30%', '70%'] do
|
|
276
|
+
column do
|
|
277
|
+
header4 "Sidebar"
|
|
278
|
+
text "Navigation here"
|
|
279
|
+
end
|
|
280
|
+
column do
|
|
281
|
+
header4 "Main Content"
|
|
282
|
+
text "Primary content here"
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Custom gap between columns
|
|
287
|
+
columns gap: "2rem" do
|
|
288
|
+
column { text "More space" }
|
|
289
|
+
column { text "Between columns" }
|
|
290
|
+
end
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Columns stack vertically on mobile (< 768px) for responsive design.
|
|
294
|
+
|
|
295
|
+
## Collapsible Sections
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
# Collapsed by default
|
|
299
|
+
collapsible "Show Details" do
|
|
300
|
+
text "Hidden content revealed on click"
|
|
301
|
+
text "Can contain any components"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Start expanded
|
|
305
|
+
collapsible "View Context", expanded: true do
|
|
306
|
+
text "This content is visible initially"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# With a subtitle and badge in the header
|
|
310
|
+
collapsible "Activity Log", subtitle: "Last 30 days", badge_text: "12 new", badge_variant: :success do
|
|
311
|
+
text "Recent activity details"
|
|
312
|
+
end
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`collapsible` is client-side only — the server never learns the expanded state. If you need the server to know whether a section is expanded (e.g. to bind it to `state`), use `expandable_card` instead.
|
|
316
|
+
|
|
317
|
+
## Form Blocks (Deferred Submission)
|
|
318
|
+
|
|
319
|
+
Group multiple form elements for batch editing. Fields use client-side only state until submit.
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
form :edit_person do
|
|
323
|
+
text_field :name, placeholder: 'Name'
|
|
324
|
+
select :status, %w[active paused archived]
|
|
325
|
+
text_area :notes, placeholder: 'Notes...', rows: 3
|
|
326
|
+
|
|
327
|
+
submit 'Save' do |form_values|
|
|
328
|
+
# form_values = { name: "...", status: "...", notes: "..." }
|
|
329
|
+
# state[:edit_person] already updated at this point
|
|
330
|
+
api.save_person(form_values) # optional side effects
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
cancel 'Cancel' # resets to original values, no server request
|
|
334
|
+
end
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**Key behaviors:**
|
|
338
|
+
- State stored as nested hash: `state[:edit_person][:name]` (Rails-style)
|
|
339
|
+
- Form reads initial values from `state[:form_name]`
|
|
340
|
+
- On submit: state auto-updates, THEN submit block runs
|
|
341
|
+
- On cancel: Alpine.js resets to original values (client-side)
|
|
342
|
+
|
|
343
|
+
## Tabs
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
tabs :settings_tabs do
|
|
347
|
+
tab "General" do
|
|
348
|
+
text_field :app_name
|
|
349
|
+
checkbox :dark_mode, "Enable dark mode"
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
tab "Notifications" do
|
|
353
|
+
checkbox :email_notifications, "Email notifications"
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
# state[:settings_tabs] = 0 (active tab index)
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Tab variants:
|
|
360
|
+
```ruby
|
|
361
|
+
tabs :demo, variant: :line do ... end # Underline (default)
|
|
362
|
+
tabs :demo, variant: :enclosed do ... end # Boxed tabs
|
|
363
|
+
tabs :demo, variant: :"soft-rounded" do ... end # Pill-style
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
An out-of-range index (a stale session value, or a group that shrank) renders tab 0
|
|
367
|
+
rather than blanking every panel.
|
|
368
|
+
|
|
369
|
+
**Deprecated: `tabs :key, lazy: true`.** Lazy tabs defer evaluating inactive panels and
|
|
370
|
+
fetch the newly-active one with a server round-trip, which a canvas page has no route
|
|
371
|
+
for — inactive panels there stay empty forever (`stream_weaver-pkh`). The mode still
|
|
372
|
+
works exactly as before and warns once per process. Migrate to route tabs (`url: true`)
|
|
373
|
+
below; a lazy route-tab mode will replace this one.
|
|
374
|
+
|
|
375
|
+
### Route tabs (`url: true`)
|
|
376
|
+
|
|
377
|
+
```ruby
|
|
378
|
+
tabs :view, url: true do
|
|
379
|
+
tab("Overview") { text "..." }
|
|
380
|
+
tab("Findings") { text "..." }
|
|
381
|
+
end
|
|
382
|
+
# active tab lives in ?view=<index> — bookmarkable, back/forward-aware
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Switching tabs is client-side (History API `pushState`/`popstate`) — zero requests.
|
|
386
|
+
Multiple `url: true` groups compose, one param per key: `/?view=2&panel=1`.
|
|
387
|
+
|
|
388
|
+
**The URL is authoritative on a full GET.** Param present and valid → that index.
|
|
389
|
+
Absent or invalid → tab 0, *never* the session value. The same URL always renders the
|
|
390
|
+
same tabs.
|
|
391
|
+
|
|
392
|
+
Invalid values degrade instead of erroring — `?view=999`, `?view=abc`, and `?view[]=`
|
|
393
|
+
all return 200 with tab 0 active. Integer strings (`?view=2`) are valid deep links.
|
|
394
|
+
Build-time `ArgumentError`s: a reserved request param as the key (`app_id`, `splat`,
|
|
395
|
+
`captures`, `button_id`), two groups claiming the same key, or `url: true` with
|
|
396
|
+
`lazy: true`.
|
|
397
|
+
|
|
398
|
+
**Gotcha — read `state[:view]` *below* the declaration.** Above it, the key still holds
|
|
399
|
+
the raw pre-authority value (the param string, or a stale session index); the resolved
|
|
400
|
+
integer only exists once the group has applied URL authority:
|
|
401
|
+
|
|
402
|
+
```ruby
|
|
403
|
+
tabs :view, url: true do ... end
|
|
404
|
+
md "Active view: #{state[:view]}" # resolved Integer
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Gotcha — a route-tab group cannot be server-side preset.** `state[:view] ||= 1` above
|
|
408
|
+
the declaration is overwritten by URL authority (tab 0 on a bare GET). That is the
|
|
409
|
+
authority rule working as designed; use a deep link (`/?view=1`) instead.
|
|
410
|
+
|
|
411
|
+
On canvas pages `url:` is ignored — the group renders as plain client-side tabs and one
|
|
412
|
+
warning is logged per render pass, since a canvas page has no app URL to carry the tab.
|
|
413
|
+
|
|
414
|
+
See `examples/layout/route_tabs_demo.rb` for a two-group demo that exercises both
|
|
415
|
+
gotchas.
|
|
416
|
+
|
|
417
|
+
## Breadcrumbs
|
|
418
|
+
|
|
419
|
+
```ruby
|
|
420
|
+
breadcrumbs do
|
|
421
|
+
crumb "Home", href: "/"
|
|
422
|
+
crumb "Products", href: "/products"
|
|
423
|
+
crumb "Current" # No href = current page
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Custom separator
|
|
427
|
+
breadcrumbs separator: ">" do
|
|
428
|
+
crumb "Dashboard", href: "/dashboard"
|
|
429
|
+
crumb "Settings"
|
|
430
|
+
end
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## Navbar
|
|
434
|
+
|
|
435
|
+
Cross-app horizontal navigation bar. Active item renders as bold text; others as links.
|
|
436
|
+
|
|
437
|
+
```ruby
|
|
438
|
+
navbar do
|
|
439
|
+
nav_item "Home", href: "/", active: true # current page — bold, non-clickable
|
|
440
|
+
nav_item "Settings", href: "/settings" # link
|
|
441
|
+
nav_item "Help", href: "/help"
|
|
442
|
+
end
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Link
|
|
446
|
+
|
|
447
|
+
Inline anchor element.
|
|
448
|
+
|
|
449
|
+
```ruby
|
|
450
|
+
link_to "Visit docs", href: "https://example.com"
|
|
451
|
+
link_to "Internal page", href: "/dashboard"
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
## Dropdown Menu
|
|
455
|
+
|
|
456
|
+
```ruby
|
|
457
|
+
dropdown do
|
|
458
|
+
trigger do
|
|
459
|
+
button "Actions"
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
menu do
|
|
463
|
+
menu_item "Edit" do |s|
|
|
464
|
+
s[:editing] = true
|
|
465
|
+
end
|
|
466
|
+
menu_item "Duplicate" do |s|
|
|
467
|
+
s[:items] << s[:items].last.dup
|
|
468
|
+
end
|
|
469
|
+
menu_divider
|
|
470
|
+
menu_item "Delete", style: :destructive do |s|
|
|
471
|
+
s[:items].pop
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
## Modal Dialogs
|
|
478
|
+
|
|
479
|
+
```ruby
|
|
480
|
+
# Open modal via button
|
|
481
|
+
button "Open Settings" do |s|
|
|
482
|
+
s[:settings_open] = true
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Define modal (state key is :{name}_open)
|
|
486
|
+
modal :settings, title: "Settings", size: :md do
|
|
487
|
+
text_field :setting_value
|
|
488
|
+
text "Configure your preferences here."
|
|
489
|
+
|
|
490
|
+
modal_footer do
|
|
491
|
+
button "Cancel", style: :secondary do |s|
|
|
492
|
+
s[:settings_open] = false
|
|
493
|
+
end
|
|
494
|
+
button "Save" do |s|
|
|
495
|
+
# save logic
|
|
496
|
+
s[:settings_open] = false
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Modal sizes: `:sm` (400px), `:md` (560px default), `:lg` (800px), `:xl` (1140px)
|
|
503
|
+
|
|
504
|
+
Close methods: click backdrop, press Escape, or button action setting `s[:modal_key_open] = false`
|
|
505
|
+
|
|
506
|
+
## Status Badge
|
|
507
|
+
|
|
508
|
+
```ruby
|
|
509
|
+
status_badge :strong, "Perfect match"
|
|
510
|
+
status_badge :maybe, "Good fit, but..."
|
|
511
|
+
status_badge :skip, "Not recommended"
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Renders visual indicators:
|
|
515
|
+
- Strong = green background
|
|
516
|
+
- Maybe = yellow background
|
|
517
|
+
- Skip = red background
|
|
518
|
+
|
|
519
|
+
## Tag Buttons
|
|
520
|
+
|
|
521
|
+
```ruby
|
|
522
|
+
# Default style
|
|
523
|
+
tag_buttons :category, ["Fiction", "Non-fiction", "Mystery"]
|
|
524
|
+
|
|
525
|
+
# Destructive style
|
|
526
|
+
tag_buttons :eliminate_reason, ["Too dark", "Wrong genre"], style: :destructive
|
|
527
|
+
# state[:eliminate_reason] = "too_dark" (normalized)
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
Single-select: clicking a tag selects it and deselects others.
|
|
531
|
+
|
|
532
|
+
## External Link Button
|
|
533
|
+
|
|
534
|
+
```ruby
|
|
535
|
+
# Just open link
|
|
536
|
+
external_link_button "View on Amazon", url: "https://amazon.com/dp/B0XXX"
|
|
537
|
+
|
|
538
|
+
# Submit form AND open link (for agentic mode)
|
|
539
|
+
external_link_button "Get it!", url: book[:amazon_url], submit: true
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
## Copy Button
|
|
543
|
+
|
|
544
|
+
Client-side clipboard-copy trigger for a fixed text payload -- no server round-trip.
|
|
545
|
+
|
|
546
|
+
```ruby
|
|
547
|
+
copy_button "Copy summary", text: report_text
|
|
548
|
+
|
|
549
|
+
# Custom label shown briefly after a successful copy
|
|
550
|
+
copy_button "Copy", text: api_key, copied_label: "Copied to clipboard!"
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Uses `navigator.clipboard` on secure origins (HTTPS or localhost) and automatically
|
|
554
|
+
falls back to a hidden-textarea + `execCommand('copy')` approach otherwise -- this
|
|
555
|
+
matters when StreamWeaver is served over plain HTTP to a LAN IP, where
|
|
556
|
+
`navigator.clipboard` is unavailable because the page isn't a secure context.
|
|
557
|
+
|
|
558
|
+
## Code Block
|
|
559
|
+
|
|
560
|
+
Syntax-highlighted code display.
|
|
561
|
+
|
|
562
|
+
```ruby
|
|
563
|
+
code_block "def hello\n puts 'hi'\nend", lang: "ruby"
|
|
564
|
+
|
|
565
|
+
# With a copy affordance in the header (off by default)
|
|
566
|
+
code_block source_code, lang: "ruby", copy: true
|
|
567
|
+
|
|
568
|
+
# With a filename shown in the header
|
|
569
|
+
code_block source_code, lang: "ruby", file: "app.rb"
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
`copy:` reuses the same clipboard mechanism as `copy_button` -- the header gains
|
|
573
|
+
a small Copy button that always copies the full code text, even if the block is
|
|
574
|
+
truncated for display via `truncate:`.
|
|
575
|
+
|
|
576
|
+
## Table
|
|
577
|
+
|
|
578
|
+
Display tabular data with smart data inference, formatters, and interactive features.
|
|
579
|
+
|
|
580
|
+
### Basic Usage
|
|
581
|
+
|
|
582
|
+
```ruby
|
|
583
|
+
# Original API - explicit headers and rows
|
|
584
|
+
table headers: ["Name", "Size"], rows: [["app.rb", "12kb"], ["cli.rb", "8kb"]]
|
|
585
|
+
|
|
586
|
+
# Array of hashes - headers auto-inferred from keys
|
|
587
|
+
table [
|
|
588
|
+
{ name: "Alice", age: 30, role: "Engineer" },
|
|
589
|
+
{ name: "Bob", age: 25, role: "Designer" }
|
|
590
|
+
]
|
|
591
|
+
# Headers become: "Name", "Age", "Role" (titleized keys)
|
|
592
|
+
|
|
593
|
+
# Hash of arrays - keys become columns
|
|
594
|
+
table({ name: ["Alice", "Bob"], age: [30, 25] })
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Data Sources
|
|
598
|
+
|
|
599
|
+
```ruby
|
|
600
|
+
# State binding - reads from state[:users]
|
|
601
|
+
table data: :users
|
|
602
|
+
|
|
603
|
+
# File loading (YAML or JSON)
|
|
604
|
+
table file: "data/users.yaml"
|
|
605
|
+
table file: "data.json", path: "results.users"
|
|
606
|
+
|
|
607
|
+
# Transform block for file data
|
|
608
|
+
table file: "raw.yaml" do |data|
|
|
609
|
+
data.map { |r| { name: r[:n], value: r[:v] } }
|
|
610
|
+
end
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### Column DSL with Formatters
|
|
614
|
+
|
|
615
|
+
```ruby
|
|
616
|
+
table users do
|
|
617
|
+
column :name
|
|
618
|
+
column :email, header: "E-mail"
|
|
619
|
+
column :balance, format: :currency, align: :right
|
|
620
|
+
column :joined, format: :date
|
|
621
|
+
column(:active) { |u| u.active ? "Yes" : "No" } # Computed column
|
|
622
|
+
end
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
**Built-in formatters:**
|
|
626
|
+
|
|
627
|
+
| Format | Example Input | Output |
|
|
628
|
+
|--------|---------------|--------|
|
|
629
|
+
| `:date` | `Date.today` | "Jan 20, 2026" |
|
|
630
|
+
| `:datetime` | `Time.now` | "Jan 20, 2026 3:30 PM" |
|
|
631
|
+
| `:currency` | `1234.56` | "$1,234.56" |
|
|
632
|
+
| `:number` | `1234567` | "1,234,567" |
|
|
633
|
+
| `:percent` | `0.42` | "42%" |
|
|
634
|
+
|
|
635
|
+
Custom formatter with Proc:
|
|
636
|
+
```ruby
|
|
637
|
+
column :balance, format: ->(v) { v > 1000 ? "#{(v/1000.0).round(1)}k" : v.to_s }
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### Interactive Features
|
|
641
|
+
|
|
642
|
+
```ruby
|
|
643
|
+
# Sortable - click headers to sort (handles text and numbers)
|
|
644
|
+
table data, sortable: true
|
|
645
|
+
|
|
646
|
+
# Sticky header - header stays visible when scrolling
|
|
647
|
+
table data, sticky_header: true
|
|
648
|
+
|
|
649
|
+
# Combined
|
|
650
|
+
table data, sortable: true, sticky_header: true, striped: true
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### Markdown Links in Cells
|
|
654
|
+
|
|
655
|
+
```ruby
|
|
656
|
+
# Enable markdown link parsing with markdown: true
|
|
657
|
+
table [
|
|
658
|
+
{ issue: "[JIRA-123](https://jira.example.com/browse/JIRA-123)", status: "Open" },
|
|
659
|
+
{ issue: "[JIRA-456](https://jira.example.com/browse/JIRA-456)", status: "Closed" }
|
|
660
|
+
], markdown: true
|
|
661
|
+
|
|
662
|
+
# Default (markdown: false) shows literal text: "[text](url)"
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Styling Options
|
|
666
|
+
|
|
667
|
+
```ruby
|
|
668
|
+
table data,
|
|
669
|
+
striped: true, # Alternate row colors
|
|
670
|
+
bordered: true, # Cell borders
|
|
671
|
+
hoverable: true, # Highlight rows on hover (default: true)
|
|
672
|
+
compact: true, # Reduced padding
|
|
673
|
+
sortable: true, # Client-side sorting
|
|
674
|
+
sticky_header: true, # Header stays visible on scroll
|
|
675
|
+
markdown: true, # Parse [text](url) as clickable links
|
|
676
|
+
caption: "Title" # Table caption above
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
### Cell Style Escape Hatches
|
|
680
|
+
|
|
681
|
+
By default, the first column renders in an accent monospace style (legacy behavior, unchanged). To control this per-cell:
|
|
682
|
+
|
|
683
|
+
```ruby
|
|
684
|
+
# Column DSL: style: accepts a static String or a per-row Proc
|
|
685
|
+
table users do
|
|
686
|
+
column :name, style: "font-weight: 600;"
|
|
687
|
+
column :balance, style: ->(u) { u.balance.negative? ? "color: red;" : nil }
|
|
688
|
+
|
|
689
|
+
# id_style: overrides the default first-column accent styling, true/false, on any column
|
|
690
|
+
column :id, id_style: false # never accent this column
|
|
691
|
+
column :sku, id_style: true # always accent this column, regardless of position
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
# Raw headers:/rows: tables: id_column: picks which column (by index) gets the
|
|
695
|
+
# accent styling instead of the column-0 default, or disables it entirely
|
|
696
|
+
table headers: ["SKU", "Name"], rows: [...], id_column: 0 # explicit column 0
|
|
697
|
+
table headers: ["SKU", "Name"], rows: [...], id_column: false # no accent anywhere
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
`style:` is appended after any built-in styling (including the accent styling above), so it always wins on conflicting CSS properties without needing `!important`.
|
|
701
|
+
|
|
702
|
+
## Score Table
|
|
703
|
+
|
|
704
|
+
```ruby
|
|
705
|
+
score_table scores: [
|
|
706
|
+
{ label: "Novelty", value: 8, max: 10 },
|
|
707
|
+
{ label: "Quality", value: 5, max: 10 },
|
|
708
|
+
{ label: "Impact", value: 3, max: 10 }
|
|
709
|
+
]
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
Color coding: Green (>=70%), Yellow (40-69%), Red (<40%)
|
|
713
|
+
|
|
714
|
+
## Charts
|
|
715
|
+
|
|
716
|
+
Data visualization using Chart.js (loaded via CDN only when charts are present):
|
|
717
|
+
|
|
718
|
+
```ruby
|
|
719
|
+
# Bar charts
|
|
720
|
+
bar_chart data: { calendar: 45, news: 120, tasks: 30 }
|
|
721
|
+
hbar_chart data: { "Phase A" => 25, "Phase B" => 45 } # horizontal
|
|
722
|
+
|
|
723
|
+
# Line charts
|
|
724
|
+
line_chart data: [12, 19, 8, 15, 22] # array = sequential x-axis
|
|
725
|
+
line_chart data: { Mon: 5, Tue: 12, Wed: 8 }, fill: true
|
|
726
|
+
|
|
727
|
+
# Sparklines (minimal, inline trend indicators)
|
|
728
|
+
sparkline data: [45, 52, 48, 61, 55, 67, 72]
|
|
729
|
+
|
|
730
|
+
# File-based data
|
|
731
|
+
bar_chart file: "~/metrics/timing.yaml", path: "entries.-1.phases"
|
|
732
|
+
|
|
733
|
+
# State-bound
|
|
734
|
+
bar_chart data: :metrics
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
**Bar chart options:** `horizontal:`, `show_values:`
|
|
738
|
+
|
|
739
|
+
**Line chart options:** `fill:`, `smooth:`, `points:`, `begin_at_zero:`
|
|
740
|
+
|
|
741
|
+
**Pie/Doughnut:**
|
|
742
|
+
```ruby
|
|
743
|
+
pie_chart data: { sales: 100, costs: 60, profit: 40 }
|
|
744
|
+
doughnut_chart data: { frontend: 40, backend: 35, devops: 25 }
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
**Stacked bar:**
|
|
748
|
+
```ruby
|
|
749
|
+
stacked_bar_chart data: [
|
|
750
|
+
{ label: "Mon", sales: 100, costs: 60 },
|
|
751
|
+
{ label: "Tue", sales: 120, costs: 70 }
|
|
752
|
+
]
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
**Common options:** `data:`, `file:`, `path:`, `title:`, `height:`, `colors:`, `show_legend:`
|
|
756
|
+
|
|
757
|
+
## Mermaid Diagrams
|
|
758
|
+
|
|
759
|
+
Renders Mermaid.js diagrams. Mermaid.js 11 (ESM) is loaded lazily from CDN — only injected when a `mermaid` component is present on the page.
|
|
760
|
+
|
|
761
|
+
```ruby
|
|
762
|
+
# Basic — any valid Mermaid diagram type
|
|
763
|
+
mermaid <<~MERMAID
|
|
764
|
+
graph LR
|
|
765
|
+
A[Start] --> B{Decision}
|
|
766
|
+
B -- Yes --> C[Ship it]
|
|
767
|
+
B -- No --> D[Debug]
|
|
768
|
+
D --> A
|
|
769
|
+
MERMAID
|
|
770
|
+
|
|
771
|
+
# zoom: true — adds +/−/reset buttons and Ctrl+scroll zoom
|
|
772
|
+
mermaid diagram_code, zoom: true
|
|
773
|
+
|
|
774
|
+
# compact: true — reduced padding for embedding inside a card
|
|
775
|
+
card do
|
|
776
|
+
mermaid diagram_code, compact: true
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
# layout: :elk — ELK layout engine (better for dense graphs, loads separately from CDN)
|
|
780
|
+
mermaid diagram_code, layout: :elk
|
|
781
|
+
|
|
782
|
+
# theme_vars: — override Mermaid themeVariables per diagram
|
|
783
|
+
mermaid diagram_code, theme_vars: {
|
|
784
|
+
primaryColor: "#6366f1",
|
|
785
|
+
primaryTextColor: "#ffffff",
|
|
786
|
+
primaryBorderColor: "#4f46e5",
|
|
787
|
+
lineColor: "#6366f1"
|
|
788
|
+
}
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
**Options:**
|
|
792
|
+
|
|
793
|
+
| Option | Type | Default | Description |
|
|
794
|
+
|---|---|---|---|
|
|
795
|
+
| `zoom:` | Boolean | `false` | Adds in-place zoom/pan controls (+/−/reset) and Ctrl+scroll |
|
|
796
|
+
| `compact:` | Boolean | `false` | Reduces padding — use when embedding inside a card |
|
|
797
|
+
| `layout:` | Symbol | `:default` | Layout engine: `:default` (Dagre) or `:elk` (ELK) |
|
|
798
|
+
| `theme_vars:` | Hash | `nil` | Per-diagram Mermaid `themeVariables` overrides |
|
|
799
|
+
|
|
800
|
+
**Supported diagram types:** flowchart, sequence, pie, gantt, gitgraph, classDiagram, stateDiagram, erDiagram, and any other type supported by Mermaid.js 11.
|
|
801
|
+
|
|
802
|
+
**Expand to full screen.** Every diagram, regardless of `zoom:`, gets an ⛶-style expand button that opens it full-viewport with no width constraint — a doc column's max-width shrinks a wide diagram's fixed-px labels proportionally no matter how the layout is tuned, so this is the actual fix for an illegible complex diagram, not just a bigger version of `zoom: true`. Scroll (or drag) to pan, Ctrl+scroll to zoom further, Escape/click the backdrop/the close button to exit. No opt-in needed and no extra dependency — it's client-side only, so it works the same in the live canvas, `canvas-read`, and any exported doc (including an `--offline` one).
|
|
803
|
+
|
|
804
|
+
**Theme awareness:** Mermaid diagrams automatically re-render when the page theme changes (dark/light). Use `theme_vars:` for brand-specific color overrides.
|
|
805
|
+
|
|
806
|
+
**Examples:** `examples/components/mermaid_demo.rb` (all options), `examples/canvas/mermaid_canvas_demo.sh` (canvas-push).
|
|
807
|
+
|
|
808
|
+
## Educational Content (Glossary/Tooltips)
|
|
809
|
+
|
|
810
|
+
```ruby
|
|
811
|
+
glossary = {
|
|
812
|
+
"term" => {
|
|
813
|
+
simple: "Short definition on hover",
|
|
814
|
+
detailed: "Longer explanation on click"
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
# String syntax - terms in {braces}
|
|
819
|
+
lesson_text "This has a {term}.", glossary: glossary
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
## Alerts
|
|
823
|
+
|
|
824
|
+
```ruby
|
|
825
|
+
alert(variant: :info) do
|
|
826
|
+
text "Informational message."
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
alert(variant: :success, title: "Success!") do
|
|
830
|
+
text "Your changes have been saved."
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
alert(variant: :warning, title: "Warning") do
|
|
834
|
+
text "Your session will expire soon."
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
alert(variant: :error, title: "Error") do
|
|
838
|
+
text "Unable to connect to server."
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
# Dismissible
|
|
842
|
+
alert(variant: :info, dismissible: true) do
|
|
843
|
+
text "Click X to dismiss."
|
|
844
|
+
end
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
Variants: `:info`, `:success`, `:warning`, `:error`
|
|
848
|
+
|
|
849
|
+
## Toast Notifications
|
|
850
|
+
|
|
851
|
+
```ruby
|
|
852
|
+
# Add toast container
|
|
853
|
+
toast_container position: :top_right, duration: 5000
|
|
854
|
+
|
|
855
|
+
# Trigger toasts from button actions
|
|
856
|
+
button "Save" do |s|
|
|
857
|
+
show_toast("Saved successfully!", variant: :success)
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
# Clear all toasts
|
|
861
|
+
button "Clear" do |s|
|
|
862
|
+
clear_toasts
|
|
863
|
+
end
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
Positions: `:top_right`, `:top_left`, `:bottom_right`, `:bottom_left`
|
|
867
|
+
Variants: `:info`, `:success`, `:warning`, `:error`
|
|
868
|
+
Duration: milliseconds (0 = no auto-dismiss)
|
|
869
|
+
|
|
870
|
+
## Progress Bar
|
|
871
|
+
|
|
872
|
+
```ruby
|
|
873
|
+
progress_bar value: 75
|
|
874
|
+
progress_bar value: 65, show_label: true
|
|
875
|
+
progress_bar value: 100, variant: :success
|
|
876
|
+
progress_bar value: 80, animated: true
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
Variants: `:default`, `:success`, `:warning`, `:error`
|
|
880
|
+
|
|
881
|
+
## Spinner
|
|
882
|
+
|
|
883
|
+
```ruby
|
|
884
|
+
spinner
|
|
885
|
+
spinner size: :sm
|
|
886
|
+
spinner size: :md # default
|
|
887
|
+
spinner size: :lg
|
|
888
|
+
spinner size: :md, label: "Loading data..."
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
Sizes: `:sm`, `:md`, `:lg`
|
|
892
|
+
|
|
893
|
+
## Dashboard Components
|
|
894
|
+
|
|
895
|
+
Dashboard-style components for operations dashboards, control panels, and status displays. Best used with `theme: :dark`.
|
|
896
|
+
|
|
897
|
+
### Status Dot
|
|
898
|
+
|
|
899
|
+
Colored indicator dots with optional glow effect, pulse animation, and labels:
|
|
900
|
+
|
|
901
|
+
```ruby
|
|
902
|
+
status_dot status: :red # Red with glow
|
|
903
|
+
status_dot status: :yellow # Yellow with glow
|
|
904
|
+
status_dot status: :green # Green with glow
|
|
905
|
+
status_dot status: :gray # Gray (inactive)
|
|
906
|
+
|
|
907
|
+
# Sizes
|
|
908
|
+
status_dot status: :green, size: :sm # 6px
|
|
909
|
+
status_dot status: :green, size: :md # 10px (default)
|
|
910
|
+
status_dot status: :green, size: :lg # 14px
|
|
911
|
+
|
|
912
|
+
# Pulse animation
|
|
913
|
+
status_dot status: :green, pulse: true
|
|
914
|
+
|
|
915
|
+
# With label (displayed below the dot)
|
|
916
|
+
status_dot status: :green, label: "user.rb"
|
|
917
|
+
status_dot status: :green, pulse: true, label: "processing..."
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
### Badge
|
|
921
|
+
|
|
922
|
+
Count/label badges in various color variants:
|
|
923
|
+
|
|
924
|
+
```ruby
|
|
925
|
+
badge "5" # Default gray
|
|
926
|
+
badge "3", variant: :danger # Red
|
|
927
|
+
badge "12", variant: :warning # Yellow
|
|
928
|
+
badge "OK", variant: :success # Green
|
|
929
|
+
badge "new", variant: :info # Blue
|
|
930
|
+
|
|
931
|
+
# Sizes
|
|
932
|
+
badge "5", size: :sm # Smaller
|
|
933
|
+
badge "5", size: :md # Default
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
### Stat Display
|
|
937
|
+
|
|
938
|
+
Large metric numbers with labels:
|
|
939
|
+
|
|
940
|
+
```ruby
|
|
941
|
+
stat_display value: 42, label: "TASKS"
|
|
942
|
+
stat_display value: 7, label: "PENDING", color: :blue
|
|
943
|
+
stat_display value: 5, label: "BLOCKED", color: :red
|
|
944
|
+
stat_display value: 12, label: "DONE", color: :purple
|
|
945
|
+
|
|
946
|
+
# Sizes
|
|
947
|
+
stat_display value: 99, label: "TOTAL", size: :sm
|
|
948
|
+
stat_display value: 99, label: "TOTAL", size: :md # Default
|
|
949
|
+
stat_display value: 99, label: "TOTAL", size: :lg
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
Colors: `:default`, `:blue`, `:purple`, `:red`
|
|
953
|
+
|
|
954
|
+
### Type Tag
|
|
955
|
+
|
|
956
|
+
Activity type badges:
|
|
957
|
+
|
|
958
|
+
```ruby
|
|
959
|
+
type_tag :research # Blue
|
|
960
|
+
type_tag :task # Purple
|
|
961
|
+
type_tag :escalation # Red
|
|
962
|
+
type_tag :communication # Green
|
|
963
|
+
type_tag :warning # Yellow
|
|
964
|
+
type_tag :info # Gray
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
### Pulse Indicator
|
|
968
|
+
|
|
969
|
+
Animated status indicator with label (for headers):
|
|
970
|
+
|
|
971
|
+
```ruby
|
|
972
|
+
pulse_indicator color: :green, label: "System Active"
|
|
973
|
+
pulse_indicator color: :red, label: "Alert"
|
|
974
|
+
pulse_indicator color: :yellow, label: "Degraded"
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
### Priority Item
|
|
978
|
+
|
|
979
|
+
Items with priority-colored left border and hover slide effect:
|
|
980
|
+
|
|
981
|
+
```ruby
|
|
982
|
+
priority_item priority: :critical, title: "Database at capacity",
|
|
983
|
+
description: "Primary DB at 92% storage. Need immediate action.",
|
|
984
|
+
meta_left: "ops", meta_right: "Expand storage"
|
|
985
|
+
|
|
986
|
+
priority_item priority: :urgent, title: "API rate limited",
|
|
987
|
+
description: "Third-party integration hitting 429 errors."
|
|
988
|
+
|
|
989
|
+
priority_item priority: :high, title: "Security patch needed"
|
|
990
|
+
priority_item priority: :normal, title: "Documentation update"
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
Priorities: `:critical` (red), `:urgent` (orange), `:high` (yellow), `:normal` (gray)
|
|
994
|
+
|
|
995
|
+
### Activity Item
|
|
996
|
+
|
|
997
|
+
Activity feed items with time, title, summary, and type badge:
|
|
998
|
+
|
|
999
|
+
```ruby
|
|
1000
|
+
activity_item time: "15:00", title: "Performance analysis",
|
|
1001
|
+
summary: "Identified 3 slow queries, recommended indexes",
|
|
1002
|
+
type: :research
|
|
1003
|
+
|
|
1004
|
+
activity_item time: "14:30", title: "Deployment complete",
|
|
1005
|
+
summary: "All services updated successfully",
|
|
1006
|
+
type: :task
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
Types: `:research`, `:task`, `:escalation`, `:communication`
|
|
1010
|
+
|
|
1011
|
+
## Dashboard Layouts
|
|
1012
|
+
|
|
1013
|
+
### App Shell
|
|
1014
|
+
|
|
1015
|
+
Two-column layout with main content and sidebar:
|
|
1016
|
+
|
|
1017
|
+
```ruby
|
|
1018
|
+
app_shell sidebar_width: "320px" do
|
|
1019
|
+
main do
|
|
1020
|
+
header2 "Dashboard"
|
|
1021
|
+
# Main content here
|
|
1022
|
+
end
|
|
1023
|
+
|
|
1024
|
+
sidebar header: "Alerts" do
|
|
1025
|
+
# Sidebar content here
|
|
1026
|
+
end
|
|
1027
|
+
end
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
The sidebar is sticky and scrolls independently. Layout stacks on mobile.
|
|
1031
|
+
|
|
1032
|
+
### Expandable Card
|
|
1033
|
+
|
|
1034
|
+
Cards that expand/collapse on click with smooth transitions:
|
|
1035
|
+
|
|
1036
|
+
```ruby
|
|
1037
|
+
expandable_card key: :team_details,
|
|
1038
|
+
title: "Engineering",
|
|
1039
|
+
subtitle: "Product Development",
|
|
1040
|
+
badge_text: "5 activities",
|
|
1041
|
+
status: :green,
|
|
1042
|
+
initially_expanded: true do
|
|
1043
|
+
# Expanded content here
|
|
1044
|
+
stat_display value: 3, label: "TASKS"
|
|
1045
|
+
activity_item time: "15:00", title: "Code review"
|
|
1046
|
+
end
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
Options:
|
|
1050
|
+
- `key:` - State key for expansion state (required)
|
|
1051
|
+
- `title:` - Card header title
|
|
1052
|
+
- `subtitle:` - Secondary text
|
|
1053
|
+
- `badge_text:` - Text in top-right badge
|
|
1054
|
+
- `status:` - Status dot color (`:red`, `:yellow`, `:green`)
|
|
1055
|
+
- `initially_expanded:` - Start expanded (default: false)
|
|
1056
|
+
|
|
1057
|
+
## Live Streaming (SSE Timers)
|
|
1058
|
+
|
|
1059
|
+
Push live updates to the browser without polling. The `every` DSL registers periodic timers that fire server-side and push DOM updates via Server-Sent Events.
|
|
1060
|
+
|
|
1061
|
+
### `every(seconds)`
|
|
1062
|
+
|
|
1063
|
+
```ruby
|
|
1064
|
+
app "Live", theme: :dark do
|
|
1065
|
+
div id: "clock" do
|
|
1066
|
+
text "..."
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
every(1) do |streamer|
|
|
1070
|
+
streamer.replace("#clock") do
|
|
1071
|
+
div id: "clock" do
|
|
1072
|
+
text Time.now.strftime("%H:%M:%S")
|
|
1073
|
+
end
|
|
1074
|
+
end
|
|
1075
|
+
end
|
|
1076
|
+
end.run!
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
### Streamer Actions
|
|
1080
|
+
|
|
1081
|
+
The `streamer` object passed to `every` blocks supports:
|
|
1082
|
+
|
|
1083
|
+
```ruby
|
|
1084
|
+
# Replace element content (block or string)
|
|
1085
|
+
streamer.replace("#target") do
|
|
1086
|
+
div id: "target" do
|
|
1087
|
+
stat_display value: "42", label: "COUNT", color: :green
|
|
1088
|
+
end
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1091
|
+
# Append/prepend content
|
|
1092
|
+
streamer.prepend("#feed") do
|
|
1093
|
+
div style: "padding:4px" do
|
|
1094
|
+
text "New entry at #{Time.now}"
|
|
1095
|
+
end
|
|
1096
|
+
end
|
|
1097
|
+
|
|
1098
|
+
streamer.append("#log", "<p>Raw HTML also works</p>")
|
|
1099
|
+
|
|
1100
|
+
# CSS class manipulation
|
|
1101
|
+
streamer.add_class("#card-cpu", "highlight")
|
|
1102
|
+
streamer.remove_class("#card-cpu", "highlight")
|
|
1103
|
+
|
|
1104
|
+
# Remove element entirely
|
|
1105
|
+
streamer.remove("#temporary-banner")
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
### CSS Injection Pattern
|
|
1109
|
+
|
|
1110
|
+
To inject custom CSS for class effects, use a hidden placeholder with `replace` (not `append("head")` which creates duplicates):
|
|
1111
|
+
|
|
1112
|
+
```ruby
|
|
1113
|
+
div id: "custom-css", style: "display:none"
|
|
1114
|
+
|
|
1115
|
+
every(5) do |streamer|
|
|
1116
|
+
streamer.replace("#custom-css", "<style>.glow{box-shadow:0 0 12px red}</style>")
|
|
1117
|
+
streamer.add_class("#card-alert", "glow")
|
|
1118
|
+
end
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
### Multiple Timers
|
|
1122
|
+
|
|
1123
|
+
Multiple `every` blocks run independently. Use closure variables to share state:
|
|
1124
|
+
|
|
1125
|
+
```ruby
|
|
1126
|
+
latest = { cpu: 0.0 }
|
|
1127
|
+
|
|
1128
|
+
every(3) do |streamer|
|
|
1129
|
+
latest[:cpu] = read_cpu
|
|
1130
|
+
streamer.replace("#cpu-metric") { ... }
|
|
1131
|
+
end
|
|
1132
|
+
|
|
1133
|
+
every(10) do |streamer|
|
|
1134
|
+
if latest[:cpu] > threshold
|
|
1135
|
+
streamer.prepend("#alerts") { ... }
|
|
1136
|
+
end
|
|
1137
|
+
end
|
|
1138
|
+
```
|
|
1139
|
+
|
|
1140
|
+
### Targeting `expandable_card`
|
|
1141
|
+
|
|
1142
|
+
`expandable_card key: :foo` generates `id="card-foo"` on the outer container. Target it with `#card-foo` for class manipulation:
|
|
1143
|
+
|
|
1144
|
+
```ruby
|
|
1145
|
+
expandable_card key: :server, title: "Server", status: :green do
|
|
1146
|
+
div id: "server-stats" do
|
|
1147
|
+
stat_display value: "\u2014", label: "LOAD"
|
|
1148
|
+
end
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
every(5) do |streamer|
|
|
1152
|
+
streamer.replace("#server-stats") { ... }
|
|
1153
|
+
streamer.add_class("#card-server", "alert-ring") if overloaded
|
|
1154
|
+
end
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
### Accessing `state` Inside Replace Blocks
|
|
1158
|
+
|
|
1159
|
+
`streamer.replace` blocks run in a `FeedBuilder` context, not the App context. `state` is a method on App — it is **not available** inside replace blocks unless you pass it explicitly via the `state:` keyword.
|
|
1160
|
+
|
|
1161
|
+
```ruby
|
|
1162
|
+
# WRONG — raises NameError in timer thread at runtime
|
|
1163
|
+
every(30) do |streamer|
|
|
1164
|
+
streamer.replace("#panel") do
|
|
1165
|
+
text "Mode: #{state[:mode]}" # NameError: undefined method `state'
|
|
1166
|
+
end
|
|
1167
|
+
end
|
|
1168
|
+
|
|
1169
|
+
# CORRECT
|
|
1170
|
+
every(30) do |streamer|
|
|
1171
|
+
streamer.replace("#panel", state: state) do
|
|
1172
|
+
text "Mode: #{state[:mode]}" # works
|
|
1173
|
+
end
|
|
1174
|
+
end
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
This applies to `replace`, `append`, and `prepend`. The error appears in the server log as `[StreamWeaver] Timer error: NameError` — it will not raise on startup, only when the timer fires.
|
|
1178
|
+
|
|
1179
|
+
### Helper Methods and Replace Blocks
|
|
1180
|
+
|
|
1181
|
+
Top-level `def` helper methods are accessible inside replace blocks (they're global). If those helpers use `state`, pass `state:` to `replace` so it is available:
|
|
1182
|
+
|
|
1183
|
+
```ruby
|
|
1184
|
+
def render_dashboard(metrics:, state:)
|
|
1185
|
+
columns do
|
|
1186
|
+
column do
|
|
1187
|
+
stat_display value: metrics[:count], label: "Total"
|
|
1188
|
+
end
|
|
1189
|
+
column do
|
|
1190
|
+
text "Filter: #{state[:filter]}"
|
|
1191
|
+
end
|
|
1192
|
+
end
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
app "Dashboard" do
|
|
1196
|
+
div id: "dash" do
|
|
1197
|
+
render_dashboard(metrics: load_metrics, state: state)
|
|
1198
|
+
end
|
|
1199
|
+
|
|
1200
|
+
every(10) do |streamer|
|
|
1201
|
+
streamer.replace("#dash", state: state) do
|
|
1202
|
+
render_dashboard(metrics: load_metrics, state: state)
|
|
1203
|
+
end
|
|
1204
|
+
end
|
|
1205
|
+
end.run!
|
|
1206
|
+
```
|
|
1207
|
+
|
|
1208
|
+
## Dark Theme
|
|
1209
|
+
|
|
1210
|
+
Use `theme: :dark` for a dark color scheme optimized for dashboards:
|
|
1211
|
+
|
|
1212
|
+
```ruby
|
|
1213
|
+
app "Operations Dashboard", theme: :dark do
|
|
1214
|
+
# components...
|
|
1215
|
+
end
|
|
1216
|
+
```
|
|
1217
|
+
|
|
1218
|
+
Dark theme provides:
|
|
1219
|
+
- Deep dark backgrounds (`#0a0e14`, `#131820`)
|
|
1220
|
+
- Soft borders and elevated surfaces
|
|
1221
|
+
- Light text with proper contrast
|
|
1222
|
+
- Glow effects on status indicators
|
|
1223
|
+
- Hover lift and slide animations
|