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,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'set'
|
|
5
|
+
|
|
6
|
+
module StreamWeaver
|
|
7
|
+
# Registry for files served via the /sw-asset/ route.
|
|
8
|
+
# Maps a short SHA key → absolute path so only pre-registered files are served.
|
|
9
|
+
module ComponentAssets
|
|
10
|
+
@file_registry = {}
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Register a file and return its URL key.
|
|
14
|
+
def register_file(abs_path)
|
|
15
|
+
key = Digest::SHA256.hexdigest(abs_path)[0, 16]
|
|
16
|
+
@file_registry[key] = abs_path
|
|
17
|
+
key
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Resolve a key back to an absolute path, or nil if unknown.
|
|
21
|
+
def resolve_file(key)
|
|
22
|
+
@file_registry[key]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Return the key for an already-registered path (registers if new).
|
|
26
|
+
def file_key(abs_path)
|
|
27
|
+
key = Digest::SHA256.hexdigest(abs_path)[0, 16]
|
|
28
|
+
@file_registry[key] ||= abs_path
|
|
29
|
+
key
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Walk a component tree and collect unique CSS/JS declarations.
|
|
33
|
+
# Returns [css_strings, css_paths, js_paths] — all deduped by component class.
|
|
34
|
+
def collect(components, seen_classes = Set.new)
|
|
35
|
+
css_strings = []
|
|
36
|
+
css_paths = []
|
|
37
|
+
js_paths = []
|
|
38
|
+
|
|
39
|
+
components.each do |c|
|
|
40
|
+
klass = c.class
|
|
41
|
+
unless seen_classes.include?(klass)
|
|
42
|
+
seen_classes << klass
|
|
43
|
+
if klass.respond_to?(:component_css_strings)
|
|
44
|
+
css_strings.concat(klass.component_css_strings)
|
|
45
|
+
end
|
|
46
|
+
if klass.respond_to?(:component_css_path) && klass.component_css_path
|
|
47
|
+
css_paths << klass.component_css_path
|
|
48
|
+
end
|
|
49
|
+
if klass.respond_to?(:component_js_path) && klass.component_js_path
|
|
50
|
+
js_paths << klass.component_js_path
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if c.respond_to?(:children) && !c.children.empty?
|
|
55
|
+
child_css_s, child_css_p, child_js_p = collect(c.children, seen_classes)
|
|
56
|
+
css_strings.concat(child_css_s)
|
|
57
|
+
css_paths.concat(child_css_p)
|
|
58
|
+
js_paths.concat(child_js_p)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
[css_strings, css_paths, js_paths]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def reset!
|
|
66
|
+
@file_registry.clear
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
# Registry for third-party component classes.
|
|
5
|
+
#
|
|
6
|
+
# Allows downstream gems to contribute DSL methods without monkey-patching App or DisplayDSL.
|
|
7
|
+
# Registered components are available globally on every app instance via DisplayDSL.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# StreamWeaver.register_component(MyGem::Banner, dsl: :banner)
|
|
11
|
+
#
|
|
12
|
+
# Then in any app block:
|
|
13
|
+
# banner(text: "Hello") { ... }
|
|
14
|
+
module ComponentRegistry
|
|
15
|
+
@registry = {}
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def register(klass, dsl:, adapter: :alpinejs)
|
|
19
|
+
dsl_name = dsl.to_sym
|
|
20
|
+
@registry[dsl_name] = { class: klass, adapter: adapter }
|
|
21
|
+
|
|
22
|
+
# Install the DSL method on DisplayDSL so every App picks it up immediately.
|
|
23
|
+
# with_container handles both leaf (no block) and container (block) usage.
|
|
24
|
+
DisplayDSL.module_eval do
|
|
25
|
+
define_method(dsl_name) do |*args, **kwargs, &block|
|
|
26
|
+
with_container(klass.new(*args, **kwargs), &block)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def [](dsl_name)
|
|
32
|
+
@registry[dsl_name.to_sym]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def registered?(dsl_name)
|
|
36
|
+
@registry.key?(dsl_name.to_sym)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def each(&block)
|
|
40
|
+
@registry.each(&block)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def all
|
|
44
|
+
@registry.dup
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Remove all registrations (intended for test isolation only).
|
|
48
|
+
def reset!
|
|
49
|
+
@registry.each_key do |dsl_name|
|
|
50
|
+
DisplayDSL.undef_method(dsl_name) if DisplayDSL.method_defined?(dsl_name)
|
|
51
|
+
end
|
|
52
|
+
@registry.clear
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class << self
|
|
58
|
+
# Register a component class with a DSL method name.
|
|
59
|
+
#
|
|
60
|
+
# @param klass [Class] A class that inherits from Components::Base
|
|
61
|
+
# @param dsl [Symbol, String] The DSL method name to define on App/DisplayDSL
|
|
62
|
+
# @param adapter [Symbol] Adapter hint (:alpinejs, :opal) — informational for now
|
|
63
|
+
def register_component(klass, dsl:, adapter: :alpinejs)
|
|
64
|
+
ComponentRegistry.register(klass, dsl: dsl, adapter: adapter)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
# Renders StreamWeaver components to HTML strings via Phlex.
|
|
5
|
+
# Used by Feed and Streamer to turn component DSL blocks into HTML
|
|
6
|
+
# that can be pushed via SSE or POST /stream/push.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# components = [Components::StatDisplay.new(value: 42, label: "RPS", color: :blue)]
|
|
10
|
+
# html = ComponentRenderer.render_html(adapter, components)
|
|
11
|
+
class ComponentRenderer < Phlex::HTML
|
|
12
|
+
attr_reader :adapter
|
|
13
|
+
|
|
14
|
+
def initialize(adapter, components, state = {})
|
|
15
|
+
@adapter = adapter
|
|
16
|
+
@components = Array(components)
|
|
17
|
+
@state = state
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def view_template
|
|
21
|
+
@components.each { |c| c.render(self, @state) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Fragment scope tracking, matching Views::AppContentView. Components::Fragment
|
|
25
|
+
# asks every view it renders into for this, so a renderer without it raises
|
|
26
|
+
# NoMethodError on any app that uses `fragment`.
|
|
27
|
+
attr_reader :current_fragment_id
|
|
28
|
+
|
|
29
|
+
def with_fragment(id)
|
|
30
|
+
previous = @current_fragment_id
|
|
31
|
+
@current_fragment_id = id
|
|
32
|
+
yield
|
|
33
|
+
ensure
|
|
34
|
+
@current_fragment_id = previous
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Render components to an HTML string
|
|
38
|
+
#
|
|
39
|
+
# @param adapter [StreamWeaver::Adapter::Base] The adapter (default: AlpineJS)
|
|
40
|
+
# @param components [Array<Components::Base>] Components to render
|
|
41
|
+
# @param state [Hash] State hash (default: empty)
|
|
42
|
+
# @return [String] Rendered HTML
|
|
43
|
+
def self.render_html(adapter = nil, components = [], state = {})
|
|
44
|
+
adapter ||= Adapter::AlpineJS.new
|
|
45
|
+
new(adapter, components, state).call
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
module Components
|
|
5
|
+
# Code block with line-number-pinned annotation bubbles in a side panel.
|
|
6
|
+
# Uses Prism.js for syntax highlighting. Note: highlighting is applied per line,
|
|
7
|
+
# so multi-line constructs (heredocs, block comments) may highlight each line
|
|
8
|
+
# independently — this component optimises for annotation alignment, not perfect highlighting.
|
|
9
|
+
# Renders a side-by-side layout: code pane left, annotation panel right.
|
|
10
|
+
# Each annotation is aligned to its target line; annotated lines get a subtle highlight.
|
|
11
|
+
# Annotations are positioned by line number (1-based, in-range). Out-of-range lines
|
|
12
|
+
# produce dangling bubbles with no highlighted counterpart — that is visible authoring feedback.
|
|
13
|
+
class AnnotatedCode < Base
|
|
14
|
+
Annotation = Struct.new(:line, :note, keyword_init: true)
|
|
15
|
+
|
|
16
|
+
attr_reader :language, :annotations
|
|
17
|
+
|
|
18
|
+
def initialize(language: nil, annotations: [], **options)
|
|
19
|
+
@language = language
|
|
20
|
+
@annotations = annotations.map { |a| Annotation.new(line: a[:line], note: a[:note]) }
|
|
21
|
+
@options = options
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def code=(value)
|
|
25
|
+
@code = value.to_s
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def code
|
|
29
|
+
@code || ""
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def lines
|
|
33
|
+
code.lines
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def line_count
|
|
37
|
+
lines.length
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def language_class
|
|
41
|
+
language ? "language-#{language}" : "language-none"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def annotated_lines
|
|
45
|
+
@annotated_lines ||= Set.new(annotations.map(&:line))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render(view, state)
|
|
49
|
+
view.adapter.render_annotated_code(view, self, state)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
module Components
|
|
5
|
+
# Single API endpoint spec card: method badge, path, description, params table, response schema.
|
|
6
|
+
# DSL: api_endpoint(method: "POST", path: "/users", description: "Create user",
|
|
7
|
+
# params: [{name: "email", type: "string", required: true}],
|
|
8
|
+
# response: {id: "integer", email: "string"})
|
|
9
|
+
class ApiEndpoint < Base
|
|
10
|
+
DEFAULT_BADGE_COLOR = "#6b7280"
|
|
11
|
+
BADGE_COLORS = {
|
|
12
|
+
"GET" => "#2563eb",
|
|
13
|
+
"POST" => "#16a34a",
|
|
14
|
+
"PUT" => "#d97706",
|
|
15
|
+
"PATCH" => "#d97706",
|
|
16
|
+
"DELETE" => "#dc2626",
|
|
17
|
+
"HEAD" => DEFAULT_BADGE_COLOR,
|
|
18
|
+
"OPTIONS" => DEFAULT_BADGE_COLOR
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :http_method, :path, :description, :params, :response
|
|
22
|
+
|
|
23
|
+
def initialize(method:, path:, description: nil, params: [], response: {}, **options)
|
|
24
|
+
@http_method = method.to_s.upcase
|
|
25
|
+
@path = path.to_s
|
|
26
|
+
@description = description
|
|
27
|
+
@params = Array(params)
|
|
28
|
+
@response = response.is_a?(Hash) ? response : {}
|
|
29
|
+
@options = options
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def badge_color = BADGE_COLORS.fetch(@http_method, DEFAULT_BADGE_COLOR)
|
|
33
|
+
def description? = @description && !@description.empty?
|
|
34
|
+
def params? = @params.any?
|
|
35
|
+
def response? = !@response.empty?
|
|
36
|
+
|
|
37
|
+
def render(view, state)
|
|
38
|
+
view.adapter.render_api_endpoint(view, self, state)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
module Components
|
|
5
|
+
# Non-dismissible callout box with colored left border and icon area.
|
|
6
|
+
#
|
|
7
|
+
# Unlike Alert (which is dismissible), Callout is a static content box
|
|
8
|
+
# used for tips, warnings, notes, etc. in explainer documents.
|
|
9
|
+
#
|
|
10
|
+
# Variants: :info (blue), :warning (amber), :success (green),
|
|
11
|
+
# :error (red), :tip (purple)
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# callout(variant: :warning, title: "Caution") do
|
|
15
|
+
# text "Be careful with this API."
|
|
16
|
+
# end
|
|
17
|
+
class Callout < Base
|
|
18
|
+
VARIANTS = %i[info warning success error tip decision risk].freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :variant, :title
|
|
21
|
+
attr_accessor :children
|
|
22
|
+
|
|
23
|
+
# @param variant [Symbol] Callout type (:info, :warning, :success, :error, :tip)
|
|
24
|
+
# @param title [String, nil] Optional callout title
|
|
25
|
+
# @param options [Hash] Additional options
|
|
26
|
+
def initialize(variant: :info, title: nil, **options)
|
|
27
|
+
@variant = VARIANTS.include?(variant) ? variant : :info
|
|
28
|
+
@title = title
|
|
29
|
+
@options = options
|
|
30
|
+
@children = []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Icon character for each variant
|
|
34
|
+
# @return [String] Unicode icon
|
|
35
|
+
def icon
|
|
36
|
+
case @variant
|
|
37
|
+
when :info then "\u2139\uFE0F" # info
|
|
38
|
+
when :warning then "\u26A0\uFE0F" # warning
|
|
39
|
+
when :success then "\u2705" # check
|
|
40
|
+
when :error then "\u274C" # cross
|
|
41
|
+
when :tip then "\u{1F4A1}" # lightbulb
|
|
42
|
+
when :decision then "\u2696\uFE0F" # scales (decision)
|
|
43
|
+
when :risk then "\u{1F53A}" # red triangle up (risk)
|
|
44
|
+
else "\u2139\uFE0F"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# CSS modifier class for the variant
|
|
49
|
+
# @return [String]
|
|
50
|
+
def variant_class
|
|
51
|
+
"sw-callout--#{@variant}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def render(view, state)
|
|
55
|
+
view.adapter.render_callout(view, self, state)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module StreamWeaver
|
|
6
|
+
module Components
|
|
7
|
+
# Chart.js wrapper component.
|
|
8
|
+
# Renders a Chart.js chart (bar, line, pie, doughnut, radar).
|
|
9
|
+
# CDN loads lazily -- Chart.js 4 is only loaded when this component is used.
|
|
10
|
+
# Dark mode aware: reads --sw-text and --sw-border for grid/text colors.
|
|
11
|
+
#
|
|
12
|
+
# sw- CSS classes:
|
|
13
|
+
# sw-chart - outer container
|
|
14
|
+
# sw-chart__canvas - the canvas element
|
|
15
|
+
#
|
|
16
|
+
# @example Bar chart
|
|
17
|
+
# chart type: :bar, data: {
|
|
18
|
+
# labels: ["A", "B", "C"],
|
|
19
|
+
# datasets: [{ label: "Values", data: [1, 2, 3] }]
|
|
20
|
+
# }
|
|
21
|
+
#
|
|
22
|
+
# @example Line chart with custom height
|
|
23
|
+
# chart type: :line, data: { labels: [...], datasets: [...] }, height: 400
|
|
24
|
+
#
|
|
25
|
+
# @example Pie chart
|
|
26
|
+
# chart type: :pie, data: { labels: ["X", "Y"], datasets: [{ data: [60, 40] }] }
|
|
27
|
+
class Chart < Base
|
|
28
|
+
VALID_TYPES = %i[bar line pie doughnut radar].freeze
|
|
29
|
+
CHART_JS_CDN = "https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"
|
|
30
|
+
|
|
31
|
+
attr_reader :chart_type, :data, :chart_options, :height
|
|
32
|
+
|
|
33
|
+
# @param type [Symbol] Chart type (:bar, :line, :pie, :doughnut, :radar)
|
|
34
|
+
# @param data [Hash] Chart.js data config (labels, datasets)
|
|
35
|
+
# @param options [Hash] Chart.js options config
|
|
36
|
+
# @param height [Integer] Canvas height in pixels (default: 300)
|
|
37
|
+
def initialize(type:, data:, options: {}, height: 300, **extra)
|
|
38
|
+
@chart_type = type.to_sym
|
|
39
|
+
unless VALID_TYPES.include?(@chart_type)
|
|
40
|
+
raise ArgumentError, "Invalid chart type: #{@chart_type}. Must be one of: #{VALID_TYPES.join(', ')}"
|
|
41
|
+
end
|
|
42
|
+
@data = data
|
|
43
|
+
@chart_options = options
|
|
44
|
+
@height = height
|
|
45
|
+
super(**extra)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render(view, state)
|
|
49
|
+
view.adapter.render_chartjs(view, self, state)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Generate a unique canvas ID for this chart instance
|
|
53
|
+
def canvas_id
|
|
54
|
+
@canvas_id ||= "sw-chart-#{object_id}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Serialize data to JSON for the JS initializer
|
|
58
|
+
def data_json
|
|
59
|
+
JSON.generate(deep_stringify(@data))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Serialize options to JSON for the JS initializer
|
|
63
|
+
def options_json
|
|
64
|
+
JSON.generate(deep_stringify(@chart_options))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Deep-convert symbol keys to strings for JSON compatibility
|
|
70
|
+
def deep_stringify(obj)
|
|
71
|
+
case obj
|
|
72
|
+
when Hash
|
|
73
|
+
obj.each_with_object({}) do |(k, v), h|
|
|
74
|
+
h[k.to_s] = deep_stringify(v)
|
|
75
|
+
end
|
|
76
|
+
when Array
|
|
77
|
+
obj.map { |v| deep_stringify(v) }
|
|
78
|
+
else
|
|
79
|
+
obj
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
module Components
|
|
5
|
+
# Syntax-highlighted code block component using Prism.js CDN.
|
|
6
|
+
#
|
|
7
|
+
# Renders code with optional language-based syntax highlighting,
|
|
8
|
+
# file path header, line truncation for thumbnails, and scrollable container.
|
|
9
|
+
#
|
|
10
|
+
# Uses Prism.js with autoloader plugin for lazy per-language loading.
|
|
11
|
+
# CDN scripts are only included when this component is used.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# code_block("puts 'hello'", lang: "ruby")
|
|
15
|
+
#
|
|
16
|
+
# @example With file header
|
|
17
|
+
# code_block(code, file: "src/app.rb", lang: "ruby")
|
|
18
|
+
#
|
|
19
|
+
# @example Truncated for thumbnail
|
|
20
|
+
# code_block(code, truncate: 10, lang: "ruby")
|
|
21
|
+
class CodeBlock < Base
|
|
22
|
+
attr_reader :code, :lang, :file, :truncate, :scroll, :copy
|
|
23
|
+
|
|
24
|
+
# @param code [String] The source code to display
|
|
25
|
+
# @param lang [String, nil] Language for syntax highlighting (e.g. "ruby", "javascript")
|
|
26
|
+
# @param file [String, nil] File path to show in header bar
|
|
27
|
+
# @param truncate [Integer, nil] Maximum number of lines to show (nil = show all)
|
|
28
|
+
# @param scroll [Boolean] Whether to enable scrolling for long code (default: true)
|
|
29
|
+
# @param copy [Boolean] Show a copy-to-clipboard affordance in the header (default: false)
|
|
30
|
+
# @param options [Hash] Additional options
|
|
31
|
+
def initialize(code, lang: nil, file: nil, truncate: nil, scroll: true, copy: false, **options)
|
|
32
|
+
@code = code.to_s
|
|
33
|
+
@lang = lang
|
|
34
|
+
@file = file
|
|
35
|
+
@truncate = truncate
|
|
36
|
+
@scroll = scroll
|
|
37
|
+
@copy = copy
|
|
38
|
+
@options = options
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Return the display code, truncated if needed
|
|
42
|
+
# @return [String] The code to render
|
|
43
|
+
def display_code
|
|
44
|
+
return @code unless @truncate
|
|
45
|
+
|
|
46
|
+
lines = @code.lines
|
|
47
|
+
return @code if lines.length <= @truncate
|
|
48
|
+
|
|
49
|
+
lines.first(@truncate).join
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Whether the code was truncated
|
|
53
|
+
# @return [Boolean]
|
|
54
|
+
def truncated?
|
|
55
|
+
return false unless @truncate
|
|
56
|
+
|
|
57
|
+
@code.lines.length > @truncate
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Total line count of original code
|
|
61
|
+
# @return [Integer]
|
|
62
|
+
def total_lines
|
|
63
|
+
@code.lines.length
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Prism.js language class (e.g. "language-ruby")
|
|
67
|
+
# @return [String]
|
|
68
|
+
def language_class
|
|
69
|
+
@lang ? "language-#{@lang}" : "language-none"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def render(view, state)
|
|
73
|
+
view.adapter.render_code_block(view, self, state)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
module Components
|
|
5
|
+
# Side-by-side comparison panels with "before" and "after" named regions.
|
|
6
|
+
#
|
|
7
|
+
# Each panel renders its children independently. On narrow viewports
|
|
8
|
+
# (<768px), panels stack vertically.
|
|
9
|
+
#
|
|
10
|
+
# Uses named block DSL: `before { ... }` and `after { ... }` inside
|
|
11
|
+
# the comparison block populate the two panels.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# comparison(before_label: "Old", after_label: "New") do
|
|
15
|
+
# before { text "Version 1" }
|
|
16
|
+
# after { text "Version 2" }
|
|
17
|
+
# end
|
|
18
|
+
class Comparison < Base
|
|
19
|
+
attr_reader :before_label, :after_label
|
|
20
|
+
attr_accessor :children, :before_children, :after_children
|
|
21
|
+
|
|
22
|
+
# @param before_label [String] Label for the "before" panel
|
|
23
|
+
# @param after_label [String] Label for the "after" panel
|
|
24
|
+
# @param options [Hash] Additional options
|
|
25
|
+
def initialize(before_label: "Before", after_label: "After", **options)
|
|
26
|
+
@before_label = before_label
|
|
27
|
+
@after_label = after_label
|
|
28
|
+
@options = options
|
|
29
|
+
@children = []
|
|
30
|
+
@before_children = []
|
|
31
|
+
@after_children = []
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def render(view, state)
|
|
35
|
+
view.adapter.render_comparison(view, self, state)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
module Components
|
|
5
|
+
# Architecture decision block with labeled options and a recommended flag.
|
|
6
|
+
# Renders a question heading with option cards; recommended option gets a badge,
|
|
7
|
+
# non-recommended options are visually de-emphasized.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# decision(question: "Which database should we use?") do
|
|
11
|
+
# option(id: :pg, label: "PostgreSQL", detail: "Full ACID, rich extensions", recommended: true)
|
|
12
|
+
# option(id: :sqlite, label: "SQLite", detail: "Zero-dependency, great for dev")
|
|
13
|
+
# end
|
|
14
|
+
class Decision < Base
|
|
15
|
+
Option = Struct.new(:id, :label, :detail, :recommended, keyword_init: true)
|
|
16
|
+
|
|
17
|
+
attr_reader :question
|
|
18
|
+
|
|
19
|
+
def initialize(question:, **options)
|
|
20
|
+
@question = question
|
|
21
|
+
@options = options
|
|
22
|
+
@decision_options = []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def options
|
|
26
|
+
@decision_options
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def add_option(id:, label:, detail:, recommended: false)
|
|
30
|
+
@decision_options << Option.new(id: id, label: label, detail: detail, recommended: recommended)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def render(view, state)
|
|
34
|
+
view.adapter.render_decision(view, self, state)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
module Components
|
|
5
|
+
module Deck
|
|
6
|
+
# Full-screen overlay shown after deck submit or cancel.
|
|
7
|
+
# Displays a status message with blur backdrop and optional auto-close tab.
|
|
8
|
+
#
|
|
9
|
+
# sw- CSS classes:
|
|
10
|
+
# sw-close-overlay - full-screen overlay wrapper
|
|
11
|
+
# sw-close-overlay--submitted - green status (submitted)
|
|
12
|
+
# sw-close-overlay--cancelled - amber status (cancelled)
|
|
13
|
+
# sw-close-overlay__backdrop - blur backdrop layer
|
|
14
|
+
# sw-close-overlay__content - centered content box
|
|
15
|
+
# sw-close-overlay__icon - status icon (checkmark or X)
|
|
16
|
+
# sw-close-overlay__message - status message text
|
|
17
|
+
# sw-close-overlay__countdown - auto-close countdown display
|
|
18
|
+
# sw-close-overlay__close-btn - manual close button
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# close_overlay(status: :submitted, message: "Deck submitted successfully!")
|
|
22
|
+
# close_overlay(status: :cancelled, message: "Deck cancelled.")
|
|
23
|
+
class CloseOverlay < Base
|
|
24
|
+
attr_reader :status, :message, :auto_close_delay
|
|
25
|
+
|
|
26
|
+
VALID_STATUSES = [:submitted, :cancelled].freeze
|
|
27
|
+
|
|
28
|
+
# @param status [Symbol] Status type (:submitted or :cancelled)
|
|
29
|
+
# @param message [String] Status message to display
|
|
30
|
+
# @param auto_close_delay [Integer] Delay before auto-closing tab in ms (default: 800)
|
|
31
|
+
# @param options [Hash] Additional options
|
|
32
|
+
def initialize(status:, message:, auto_close_delay: 800, **options)
|
|
33
|
+
@status = validate_status(status)
|
|
34
|
+
@message = message
|
|
35
|
+
@auto_close_delay = auto_close_delay
|
|
36
|
+
super(**options)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def render(view, state)
|
|
40
|
+
view.adapter.render_close_overlay(view, self, state)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Whether this is a submitted status.
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
def submitted?
|
|
46
|
+
@status == :submitted
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Whether this is a cancelled status.
|
|
50
|
+
# @return [Boolean]
|
|
51
|
+
def cancelled?
|
|
52
|
+
@status == :cancelled
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Status-specific CSS modifier class.
|
|
56
|
+
# @return [String]
|
|
57
|
+
def status_modifier
|
|
58
|
+
"sw-close-overlay--#{@status}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# CSS class list
|
|
62
|
+
def css_classes
|
|
63
|
+
"sw-close-overlay #{status_modifier}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Status icon character.
|
|
67
|
+
# @return [String]
|
|
68
|
+
def icon
|
|
69
|
+
submitted? ? "✓" : "✕"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def validate_status(status)
|
|
75
|
+
status = status.to_sym
|
|
76
|
+
unless VALID_STATUSES.include?(status)
|
|
77
|
+
raise ArgumentError, "Invalid status '#{status}'. Must be one of: #{VALID_STATUSES.join(', ')}"
|
|
78
|
+
end
|
|
79
|
+
status
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|