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,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
module Components
|
|
5
|
+
module Deck
|
|
6
|
+
# Auto-generated summary slide appended to the end of a design deck.
|
|
7
|
+
# Reads DeckState to display:
|
|
8
|
+
# - Each slide's title, selected option label, aside text, and notes
|
|
9
|
+
# - A "Still need: Slide X, Slide Y" message when selections are incomplete
|
|
10
|
+
# - A final notes textarea for overall comments
|
|
11
|
+
# - A Submit button gated on all slides having a selection
|
|
12
|
+
#
|
|
13
|
+
# Submit sets state[:_result] with { deck_selections: {...}, deck_notes: {...} }
|
|
14
|
+
# which triggers run_once! to unblock.
|
|
15
|
+
#
|
|
16
|
+
# sw- CSS classes:
|
|
17
|
+
# sw-deck-summary - summary slide wrapper
|
|
18
|
+
# sw-deck-summary__title - summary heading
|
|
19
|
+
# sw-deck-summary__cards - grid of summary cards
|
|
20
|
+
# sw-deck-summary__card - individual summary card
|
|
21
|
+
# sw-deck-summary__card-title - slide title in card
|
|
22
|
+
# sw-deck-summary__card-label - selected option label
|
|
23
|
+
# sw-deck-summary__card-aside - aside text
|
|
24
|
+
# sw-deck-summary__card-notes - notes text
|
|
25
|
+
# sw-deck-summary__card--empty - card with no selection
|
|
26
|
+
# sw-deck-summary__missing - "still need" message
|
|
27
|
+
# sw-deck-summary__final-notes - final notes textarea container
|
|
28
|
+
# sw-deck-summary__submit - submit button
|
|
29
|
+
# sw-deck-summary__submit--disabled - disabled submit button
|
|
30
|
+
#
|
|
31
|
+
# @example Auto-appended by design_deck DSL method (no manual usage needed)
|
|
32
|
+
class DeckSummary < Base
|
|
33
|
+
attr_accessor :deck_slides
|
|
34
|
+
|
|
35
|
+
def initialize(**options)
|
|
36
|
+
@deck_slides = []
|
|
37
|
+
super(**options)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def render(view, state)
|
|
41
|
+
view.adapter.render_deck_summary(view, self, state)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# CSS class list
|
|
45
|
+
def css_classes
|
|
46
|
+
"sw-deck-summary"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Check if all slides have selections
|
|
50
|
+
#
|
|
51
|
+
# @param deck_state [DeckState, nil] The deck state instance
|
|
52
|
+
# @return [Boolean] true if every slide has a selection
|
|
53
|
+
def all_selected?(deck_state)
|
|
54
|
+
return false unless deck_state
|
|
55
|
+
return true if @deck_slides.empty?
|
|
56
|
+
|
|
57
|
+
@deck_slides.all? { |slide| deck_state.selection(slide.id) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get list of slide titles that still need selections
|
|
61
|
+
#
|
|
62
|
+
# @param deck_state [DeckState, nil] The deck state instance
|
|
63
|
+
# @return [Array<String>] titles of slides missing selections
|
|
64
|
+
def missing_slides(deck_state)
|
|
65
|
+
return @deck_slides.map { |s| s.title || s.id } unless deck_state
|
|
66
|
+
|
|
67
|
+
@deck_slides.reject { |slide| deck_state.selection(slide.id) }
|
|
68
|
+
.map { |s| s.title || s.id }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
module Components
|
|
5
|
+
module Deck
|
|
6
|
+
# Top-level orchestrator for a design deck.
|
|
7
|
+
# Contains DeckSlide children, wraps in SlideContainer with :swap mode,
|
|
8
|
+
# validates uniqueness of slide IDs, and auto-appends navigation.
|
|
9
|
+
#
|
|
10
|
+
# NOT an App subclass -- it is a component composed via DSL methods on App.
|
|
11
|
+
#
|
|
12
|
+
# sw- CSS classes:
|
|
13
|
+
# sw-deck - outermost deck wrapper
|
|
14
|
+
# sw-deck__title - deck title heading
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# design_deck "Architecture Direction" do
|
|
18
|
+
# slide "arch", "System Architecture" do
|
|
19
|
+
# option "Monolith" do
|
|
20
|
+
# code_block "app.listen(3000)", lang: "ts"
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
class DesignDeck < Base
|
|
25
|
+
attr_reader :title
|
|
26
|
+
attr_accessor :children
|
|
27
|
+
|
|
28
|
+
# @param title [String] Deck title
|
|
29
|
+
# @param options [Hash] Additional options
|
|
30
|
+
def initialize(title, **options)
|
|
31
|
+
@title = title
|
|
32
|
+
@children = []
|
|
33
|
+
super(**options)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def render(view, state)
|
|
37
|
+
view.adapter.render_design_deck(view, self, state)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Validate the deck structure.
|
|
41
|
+
# Raises if duplicate slide IDs are found.
|
|
42
|
+
def validate!
|
|
43
|
+
ids = @children
|
|
44
|
+
.select { |c| c.is_a?(DeckSlide) }
|
|
45
|
+
.map(&:id)
|
|
46
|
+
|
|
47
|
+
dupes = ids.group_by(&:itself).select { |_k, v| v.size > 1 }.keys
|
|
48
|
+
unless dupes.empty?
|
|
49
|
+
raise ArgumentError, "Duplicate slide IDs in design_deck: #{dupes.join(', ')}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# CSS class list
|
|
54
|
+
def css_classes
|
|
55
|
+
"sw-deck"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
module Components
|
|
5
|
+
module Deck
|
|
6
|
+
# Generate-more controls rendered within a deck slide footer.
|
|
7
|
+
# Provides prompt input, count dropdown, generate/cancel buttons,
|
|
8
|
+
# and status display during generation.
|
|
9
|
+
#
|
|
10
|
+
# sw- CSS classes:
|
|
11
|
+
# sw-generate-more - controls wrapper
|
|
12
|
+
# sw-generate-more__form - form with prompt + count + button
|
|
13
|
+
# sw-generate-more__prompt - prompt text input
|
|
14
|
+
# sw-generate-more__count - count dropdown
|
|
15
|
+
# sw-generate-more__btn - generate button
|
|
16
|
+
# sw-generate-more__btn--cancel - cancel button
|
|
17
|
+
# sw-generate-more__status - status banner during generation
|
|
18
|
+
# sw-generate-more__status-dot - animated pulse dot
|
|
19
|
+
# sw-generate-more__status-text - status text
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# GenerateMoreControls.new("arch", generate_state: gen_state)
|
|
23
|
+
class GenerateMoreControls < Base
|
|
24
|
+
attr_reader :slide_id
|
|
25
|
+
|
|
26
|
+
# @param slide_id [String] Slide this controls belong to
|
|
27
|
+
# @param generate_state [Hash] Current generation state from DeckState
|
|
28
|
+
# @param timeout [Integer] Timeout in seconds (default: 15)
|
|
29
|
+
def initialize(slide_id, generate_state: {}, timeout: 15, **options)
|
|
30
|
+
@slide_id = slide_id.to_s
|
|
31
|
+
@generate_state = generate_state
|
|
32
|
+
@timeout = timeout
|
|
33
|
+
super(**options)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def render(view, state)
|
|
37
|
+
view.adapter.render_generate_more_controls(view, self, state)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Current generation status
|
|
41
|
+
# @return [Symbol] :idle, :generating, :timed_out, or :cancelled
|
|
42
|
+
def status
|
|
43
|
+
(@generate_state["status"] || "idle").to_sym
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def generating?
|
|
47
|
+
status == :generating
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def timed_out?
|
|
51
|
+
status == :timed_out
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def requested_count
|
|
55
|
+
@generate_state["requested_count"] || 0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def received_count
|
|
59
|
+
@generate_state["received_count"] || 0
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def remaining_count
|
|
63
|
+
generating? ? [requested_count - received_count, 0].max : 0
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def prompt
|
|
67
|
+
@generate_state["prompt"]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def request_id
|
|
71
|
+
@generate_state["request_id"]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def timeout
|
|
75
|
+
@timeout
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def css_classes
|
|
79
|
+
"sw-generate-more"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
module Components
|
|
5
|
+
module Deck
|
|
6
|
+
# AI model picker for generate-more functionality.
|
|
7
|
+
# Renders provider filter pills and a model list.
|
|
8
|
+
# Selected model is stored in DeckState for use by generate-more.
|
|
9
|
+
#
|
|
10
|
+
# sw- CSS classes:
|
|
11
|
+
# sw-model-selector - outermost wrapper
|
|
12
|
+
# sw-model-selector__pills - provider filter pill row
|
|
13
|
+
# sw-model-selector__pill - individual provider pill
|
|
14
|
+
# sw-model-selector__pill--active - active provider pill
|
|
15
|
+
# sw-model-selector__list - model list container
|
|
16
|
+
# sw-model-selector__item - individual model item
|
|
17
|
+
# sw-model-selector__item--selected - selected model item
|
|
18
|
+
# sw-model-selector__name - model display name
|
|
19
|
+
# sw-model-selector__provider - model provider label
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# model_selector(
|
|
23
|
+
# models: [
|
|
24
|
+
# { id: "claude-3", name: "Claude 3 Opus", provider: "Anthropic" },
|
|
25
|
+
# { id: "gpt-4", name: "GPT-4", provider: "OpenAI" }
|
|
26
|
+
# ],
|
|
27
|
+
# default_model: "claude-3"
|
|
28
|
+
# )
|
|
29
|
+
class ModelSelector < Base
|
|
30
|
+
attr_reader :models, :default_model
|
|
31
|
+
|
|
32
|
+
# @param models [Array<Hash>] Array of { id:, name:, provider: } hashes
|
|
33
|
+
# @param default_model [String, nil] ID of the default selected model
|
|
34
|
+
# @param options [Hash] Additional options
|
|
35
|
+
def initialize(models: [], default_model: nil, **options)
|
|
36
|
+
@models = normalize_models(models)
|
|
37
|
+
@default_model = default_model || @models.first&.dig(:id)
|
|
38
|
+
super(**options)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def render(view, state)
|
|
42
|
+
view.adapter.render_model_selector(view, self, state)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# All unique provider names from the model list, sorted.
|
|
46
|
+
# @return [Array<String>]
|
|
47
|
+
def providers
|
|
48
|
+
@models.map { |m| m[:provider] }.uniq.sort
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Models filtered by provider (nil returns all).
|
|
52
|
+
# @param provider [String, nil] Provider name or nil for all
|
|
53
|
+
# @return [Array<Hash>]
|
|
54
|
+
def models_for_provider(provider)
|
|
55
|
+
return @models if provider.nil? || provider == "All"
|
|
56
|
+
@models.select { |m| m[:provider] == provider }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Whether to show the selector (hidden when fewer than 2 models).
|
|
60
|
+
# @return [Boolean]
|
|
61
|
+
def visible?
|
|
62
|
+
@models.length >= 2
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# CSS class list
|
|
66
|
+
def css_classes
|
|
67
|
+
"sw-model-selector"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def normalize_models(models)
|
|
73
|
+
models.map do |m|
|
|
74
|
+
{
|
|
75
|
+
id: (m[:id] || m["id"]).to_s,
|
|
76
|
+
name: (m[:name] || m["name"]).to_s,
|
|
77
|
+
provider: (m[:provider] || m["provider"]).to_s
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
module Components
|
|
5
|
+
module Deck
|
|
6
|
+
# Shimmer animation placeholder shown while options are being generated.
|
|
7
|
+
# Rendered for (requested_count - received_count) placeholders.
|
|
8
|
+
#
|
|
9
|
+
# sw- CSS classes:
|
|
10
|
+
# sw-skeleton - skeleton card wrapper
|
|
11
|
+
# sw-skeleton__line - shimmer line element
|
|
12
|
+
# sw-skeleton__line--title - wider title line
|
|
13
|
+
# sw-skeleton__line--body - narrower body line
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# SkeletonPlaceholder.new(index: 0)
|
|
17
|
+
class SkeletonPlaceholder < Base
|
|
18
|
+
attr_reader :index
|
|
19
|
+
|
|
20
|
+
# @param index [Integer] Index of this skeleton (for staggered animation)
|
|
21
|
+
def initialize(index: 0, **options)
|
|
22
|
+
@index = index
|
|
23
|
+
super(**options)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render(view, state)
|
|
27
|
+
view.adapter.render_skeleton_placeholder(view, self, state)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def css_classes
|
|
31
|
+
"sw-skeleton"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Diffy shells out to the system diff(1) binary, so it exists only on the
|
|
4
|
+
# server. In the browser the equivalent work is done by jsdiff -- see
|
|
5
|
+
# #unified_diff below.
|
|
6
|
+
require "diffy" unless RUBY_ENGINE == "opal"
|
|
7
|
+
|
|
8
|
+
module StreamWeaver
|
|
9
|
+
module Components
|
|
10
|
+
# Unified diff viewer: removed lines red, added green, context neutral.
|
|
11
|
+
# Two-column line-number gutter (before | after). Prism.js highlights per-line
|
|
12
|
+
# fragments — multi-line constructs (heredocs, block comments) highlight each line
|
|
13
|
+
# independently, same limitation as AnnotatedCode.
|
|
14
|
+
class DiffBlock < Base
|
|
15
|
+
DiffLine = Struct.new(:type, :old_num, :new_num, :prefix, :content, keyword_init: true)
|
|
16
|
+
|
|
17
|
+
attr_accessor :before_code, :after_code
|
|
18
|
+
attr_reader :language
|
|
19
|
+
|
|
20
|
+
def initialize(language: nil, **options)
|
|
21
|
+
@language = language
|
|
22
|
+
@before_code = ""
|
|
23
|
+
@after_code = ""
|
|
24
|
+
@options = options
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def language_class
|
|
28
|
+
language ? "language-#{language}" : "language-none"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parsed_lines
|
|
32
|
+
before = before_code.to_s
|
|
33
|
+
after = after_code.to_s
|
|
34
|
+
return [] if before.empty? && after.empty?
|
|
35
|
+
|
|
36
|
+
before += "\n" unless before.end_with?("\n")
|
|
37
|
+
after += "\n" unless after.end_with?("\n")
|
|
38
|
+
|
|
39
|
+
parse_unified_diff(unified_diff(before, after))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def render(view, state)
|
|
43
|
+
view.adapter.render_diff_block(view, self, state)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
CONTEXT_LINES = 3
|
|
49
|
+
|
|
50
|
+
if RUBY_ENGINE == "opal"
|
|
51
|
+
# jsdiff, so the browser never needs a diff(1) binary it cannot have.
|
|
52
|
+
#
|
|
53
|
+
# structuredPatch rather than createPatch: the latter prepends an
|
|
54
|
+
# Index:/=== header that the server-side output has no equivalent for,
|
|
55
|
+
# and stripping a fixed number of lines back off is the kind of thing
|
|
56
|
+
# that breaks quietly. Building the unified text from the hunks keeps
|
|
57
|
+
# both engines feeding #parse_unified_diff the same shape.
|
|
58
|
+
#
|
|
59
|
+
# "" markers are dropped -- they are
|
|
60
|
+
# annotations rather than content, and parse_unified_diff would read
|
|
61
|
+
# the leading backslash as a context line.
|
|
62
|
+
def unified_diff(before, after)
|
|
63
|
+
# :nocov:
|
|
64
|
+
%x{
|
|
65
|
+
var patch = Diff.structuredPatch("", "", #{before}, #{after}, "", "",
|
|
66
|
+
{ context: #{CONTEXT_LINES} });
|
|
67
|
+
var out = [];
|
|
68
|
+
patch.hunks.forEach(function(h) {
|
|
69
|
+
out.push("@@ -" + h.oldStart + "," + h.oldLines +
|
|
70
|
+
" +" + h.newStart + "," + h.newLines + " @@");
|
|
71
|
+
h.lines.forEach(function(l) {
|
|
72
|
+
if (l.charAt(0) !== "\\\\") out.push(l);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
return out.length ? out.join("\n") + "\n" : "";
|
|
76
|
+
}
|
|
77
|
+
# :nocov:
|
|
78
|
+
end
|
|
79
|
+
else
|
|
80
|
+
def unified_diff(before, after)
|
|
81
|
+
Diffy::Diff.new(before, after, context: CONTEXT_LINES).to_s(:text)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
HUNK_HEADER_RE = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.freeze
|
|
86
|
+
|
|
87
|
+
def parse_unified_diff(text)
|
|
88
|
+
lines = []
|
|
89
|
+
old_line = 1
|
|
90
|
+
new_line = 1
|
|
91
|
+
|
|
92
|
+
text.each_line do |raw_line|
|
|
93
|
+
line = raw_line.chomp
|
|
94
|
+
|
|
95
|
+
if (m = HUNK_HEADER_RE.match(line))
|
|
96
|
+
old_line = m[1].to_i
|
|
97
|
+
new_line = m[2].to_i
|
|
98
|
+
lines << DiffLine.new(type: :hunk_header, old_num: nil, new_num: nil, prefix: "@@", content: line)
|
|
99
|
+
next
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
next if line.start_with?("---", "+++")
|
|
103
|
+
|
|
104
|
+
prefix = line[0] || " "
|
|
105
|
+
content = line[1..] || ""
|
|
106
|
+
|
|
107
|
+
case prefix
|
|
108
|
+
when "-"
|
|
109
|
+
lines << DiffLine.new(type: :removed, old_num: old_line, new_num: nil, prefix: "-", content: content)
|
|
110
|
+
old_line += 1
|
|
111
|
+
when "+"
|
|
112
|
+
lines << DiffLine.new(type: :added, old_num: nil, new_num: new_line, prefix: "+", content: content)
|
|
113
|
+
new_line += 1
|
|
114
|
+
else
|
|
115
|
+
lines << DiffLine.new(type: :context, old_num: old_line, new_num: new_line, prefix: " ", content: content)
|
|
116
|
+
old_line += 1
|
|
117
|
+
new_line += 1
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
lines
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
module Components
|
|
5
|
+
# Document-level header with eyebrow label, serif title, and meta pill row.
|
|
6
|
+
# Used to open long-form documents: PRDs, reports, explainers.
|
|
7
|
+
#
|
|
8
|
+
# Pills are an array of mixed items:
|
|
9
|
+
# - String → rendered as plain meta text
|
|
10
|
+
# - Hash { text:, variant: } → colored pill (:default, :warn, :good)
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# doc_header(
|
|
14
|
+
# eyebrow: "cultiv-ai · Personal OS",
|
|
15
|
+
# title: "Calendar-Driven Travel State",
|
|
16
|
+
# pills: [
|
|
17
|
+
# { text: "Draft" },
|
|
18
|
+
# "June 25, 2026",
|
|
19
|
+
# "Author: Forrest Chang"
|
|
20
|
+
# ]
|
|
21
|
+
# )
|
|
22
|
+
class DocHeader < Base
|
|
23
|
+
attr_reader :eyebrow, :title, :pills
|
|
24
|
+
|
|
25
|
+
def initialize(eyebrow: nil, title:, pills: [], **options)
|
|
26
|
+
@eyebrow = eyebrow
|
|
27
|
+
@title = title
|
|
28
|
+
@pills = Array(pills)
|
|
29
|
+
@options = options
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def render(view, state)
|
|
33
|
+
view.adapter.render_doc_header(view, self, state)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Numbered section eyebrow + heading pair for document sections.
|
|
38
|
+
# Renders a monospace number badge and an h2 heading with a decorative line.
|
|
39
|
+
#
|
|
40
|
+
# @example
|
|
41
|
+
# doc_section_header "01", "Problem Statement", id: "problem"
|
|
42
|
+
class DocSectionHeader < Base
|
|
43
|
+
attr_reader :number, :title, :anchor_id
|
|
44
|
+
|
|
45
|
+
def initialize(number, title, id: nil, **options)
|
|
46
|
+
@number = number.to_s
|
|
47
|
+
@title = title
|
|
48
|
+
@anchor_id = id
|
|
49
|
+
@options = options
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def render(view, state)
|
|
53
|
+
view.adapter.render_doc_section_header(view, self, state)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
|
|
5
|
+
module StreamWeaver
|
|
6
|
+
module Components
|
|
7
|
+
# Image display component with optional caption and base64 export support.
|
|
8
|
+
#
|
|
9
|
+
# Renders an image with optional caption text below it.
|
|
10
|
+
# Supports local file paths, URLs, and base64 data URI conversion
|
|
11
|
+
# for self-contained HTML export.
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# image_block("photo.png", alt: "A photo")
|
|
15
|
+
#
|
|
16
|
+
# @example With caption
|
|
17
|
+
# image_block("diagram.svg", caption: "Figure 1: Architecture")
|
|
18
|
+
#
|
|
19
|
+
# @example Base64 export mode
|
|
20
|
+
# image_block("local/image.png", base64: true)
|
|
21
|
+
class ImageBlock < Base
|
|
22
|
+
attr_reader :src, :alt, :caption, :base64
|
|
23
|
+
|
|
24
|
+
# @param src [String] Image source (URL or file path)
|
|
25
|
+
# @param alt [String] Alt text for accessibility (default: "")
|
|
26
|
+
# @param caption [String, nil] Caption text to display below the image
|
|
27
|
+
# @param base64 [Boolean] Convert local file to base64 data URI (default: false)
|
|
28
|
+
# @param options [Hash] Additional options
|
|
29
|
+
def initialize(src, alt: "", caption: nil, base64: false, **options)
|
|
30
|
+
@src = src.to_s
|
|
31
|
+
@alt = alt.to_s
|
|
32
|
+
@caption = caption
|
|
33
|
+
@base64 = base64
|
|
34
|
+
@options = options
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Resolve the image source, converting to data URI if base64 mode is enabled.
|
|
38
|
+
# @return [String] The resolved src attribute value
|
|
39
|
+
def resolved_src
|
|
40
|
+
return @src unless @base64 && File.exist?(@src)
|
|
41
|
+
|
|
42
|
+
mime = mime_type(@src)
|
|
43
|
+
data = Base64.strict_encode64(File.binread(@src))
|
|
44
|
+
"data:#{mime};base64,#{data}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def render(view, state)
|
|
48
|
+
view.adapter.render_image_block(view, self, state)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Detect MIME type from file extension
|
|
54
|
+
# @param path [String] File path
|
|
55
|
+
# @return [String] MIME type
|
|
56
|
+
def mime_type(path)
|
|
57
|
+
case File.extname(path).downcase
|
|
58
|
+
when ".png" then "image/png"
|
|
59
|
+
when ".jpg", ".jpeg" then "image/jpeg"
|
|
60
|
+
when ".gif" then "image/gif"
|
|
61
|
+
when ".svg" then "image/svg+xml"
|
|
62
|
+
when ".webp" then "image/webp"
|
|
63
|
+
when ".bmp" then "image/bmp"
|
|
64
|
+
when ".ico" then "image/x-icon"
|
|
65
|
+
else "application/octet-stream"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StreamWeaver
|
|
4
|
+
module Components
|
|
5
|
+
# File-path-to-rationale mapping component for pre-flight planning.
|
|
6
|
+
# Shows what files will be touched and why, before writing any code.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# implementation_map(files: [
|
|
10
|
+
# { path: "lib/foo.rb", note: "Add the new method" },
|
|
11
|
+
# { path: "spec/foo_spec.rb", note: "Cover edge cases" }
|
|
12
|
+
# ])
|
|
13
|
+
class ImplementationMap < Base
|
|
14
|
+
attr_reader :files
|
|
15
|
+
|
|
16
|
+
# @param files [Array<Hash>] Array of {path:, note:} or {"path"=>,"note"=>} hashes
|
|
17
|
+
def initialize(files: [], **options)
|
|
18
|
+
@files = files.map { |f| normalize(f) }
|
|
19
|
+
@options = options
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render(view, state)
|
|
23
|
+
view.adapter.render_implementation_map(view, self, state)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def normalize(entry)
|
|
29
|
+
{ path: entry[:path] || entry["path"] || "", note: entry[:note] || entry["note"] || "" }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|