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,414 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# stream_weaver-oeo proof: Forrest's direction was "duplicate the sinatra
|
|
5
|
+
# apps' theming niceness (semantic classes + ONE user stylesheet owning
|
|
6
|
+
# the look, zero framework fights) instead of the intrusive escape-hatch
|
|
7
|
+
# path the pixel slice used." examples/parity/tyrion_warroom_slice.rb
|
|
8
|
+
# proved pixel-identical parity is achievable, but only by inventing
|
|
9
|
+
# tyrion's own class names from scratch with primitive div/phrase/header
|
|
10
|
+
# calls and never touching a real StreamWeaver component (Board, Lane,
|
|
11
|
+
# Sidebar, Navbar, ... were all deliberately unused there, see that
|
|
12
|
+
# file's own header comment).
|
|
13
|
+
#
|
|
14
|
+
# This slice is the other half of the bet: same fixture data, same two
|
|
15
|
+
# routes (/ and /stories/:id), but built with the PRETTY DSL --
|
|
16
|
+
# app_shell/sidebar/navbar/nav_item/board/lane/board_card/clickable/card
|
|
17
|
+
# -- styled by exactly ONE stylesheet (tyrion_components.css) that
|
|
18
|
+
# targets ONLY the sw- hook classes documented in docs/theming-hooks.md
|
|
19
|
+
# (plus its own tc- prefixed classes for the handful of elements with no
|
|
20
|
+
# dedicated component -- topbar branding, war-table hero background,
|
|
21
|
+
# story-detail page chrome; see design-parity-fights.md findings #2 and
|
|
22
|
+
# #6, both still-open gaps this slice routes around the same way the
|
|
23
|
+
# pixel slice did).
|
|
24
|
+
#
|
|
25
|
+
# Because every framework style now lives in @layer stream-weaver
|
|
26
|
+
# (stream_weaver-oeo commit 1), tyrion_components.css needs zero
|
|
27
|
+
# specificity workarounds anywhere -- no `body` prefix, no !important,
|
|
28
|
+
# nothing the pixel slice's CSS had to resort to.
|
|
29
|
+
#
|
|
30
|
+
# Palette/type values below were re-derived by reading (READ-ONLY, never
|
|
31
|
+
# copied) ~/work/tyrion/web/public/shared.css's :root custom properties --
|
|
32
|
+
# every rule in tyrion_components.css is hand-written against those
|
|
33
|
+
# values, not copy-pasted, and never references SW_PARITY_CSS (this slice
|
|
34
|
+
# doesn't load the real stylesheet at all -- that's the whole point).
|
|
35
|
+
#
|
|
36
|
+
# Fixture data only -- invented story titles, no real Tyrion project data.
|
|
37
|
+
#
|
|
38
|
+
# Env vars (optional; the slice degrades gracefully without it):
|
|
39
|
+
# SW_PARITY_ASSETS=/path/to/tyrion/web/public/assets
|
|
40
|
+
# Real crest/dragon/lantern/map art via a local-file endpoint. Unset:
|
|
41
|
+
# unicode glyph fallbacks (๐ฆ/๐/๐ฎ) and no war-table photo (the
|
|
42
|
+
# gradient-only fallback already baked into tyrion_components.css).
|
|
43
|
+
#
|
|
44
|
+
# Run: SW_PARITY_ASSETS=... ruby examples/parity/tyrion_warroom_components.rb
|
|
45
|
+
|
|
46
|
+
require_relative "../../lib/stream_weaver"
|
|
47
|
+
require "base64"
|
|
48
|
+
require "time"
|
|
49
|
+
|
|
50
|
+
PARITY_ASSETS_DIR = ENV["SW_PARITY_ASSETS"]
|
|
51
|
+
COMPONENTS_CSS_PATH = File.expand_path("tyrion_components.css", __dir__)
|
|
52
|
+
|
|
53
|
+
def parity_art_url(basename)
|
|
54
|
+
return nil unless PARITY_ASSETS_DIR
|
|
55
|
+
|
|
56
|
+
path = File.join(PARITY_ASSETS_DIR, basename)
|
|
57
|
+
return nil unless File.exist?(path)
|
|
58
|
+
|
|
59
|
+
"/sw-asset/#{StreamWeaver::ComponentAssets.register_file(File.expand_path(path))}/#{basename}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
CREST_URL = parity_art_url("LionCrest.png")
|
|
63
|
+
DRAGON_URL = parity_art_url("dragon.png")
|
|
64
|
+
LANTERN_URL = parity_art_url("lantern.png")
|
|
65
|
+
MAP_URL = parity_art_url("strategy_map.png")
|
|
66
|
+
|
|
67
|
+
# Same seam tyrion_warroom_slice.rb's own header comment documents: the
|
|
68
|
+
# real .topbar/.wr-board background-image is a *relative*
|
|
69
|
+
# url("assets/strategy_map.png") in the real CSS, so there's no `class:`
|
|
70
|
+
# hook that can carry a resolved /sw-asset/ URL -- it has to be a CSS
|
|
71
|
+
# background-image rule. This slice never loads the real CSS at all
|
|
72
|
+
# (that's the whole point), so it's a small inline override targeting
|
|
73
|
+
# this slice's OWN hook classes (.sw-topbar, .tc-board-hero), appended
|
|
74
|
+
# after tyrion_components.css so it wins ties. Unset MAP_URL: no override,
|
|
75
|
+
# tyrion_components.css's own gradient-only fallback stands as-is.
|
|
76
|
+
stylesheet_list = [COMPONENTS_CSS_PATH]
|
|
77
|
+
if MAP_URL
|
|
78
|
+
bg_css = <<~CSS
|
|
79
|
+
.sw-topbar { background-image: linear-gradient(to bottom, rgba(0,0,0,.48), rgba(13,10,7,.88)), url('#{MAP_URL}'); background-size: cover; background-position: center 35%; }
|
|
80
|
+
.tc-board-hero { background-image: linear-gradient(180deg, rgba(8,10,11,.18), rgba(8,10,11,.28)), url('#{MAP_URL}'); background-size: cover; background-position: center; }
|
|
81
|
+
CSS
|
|
82
|
+
stylesheet_list << "data:text/css;base64,#{Base64.strict_encode64(bg_css)}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Store: identical fixture data to tyrion_warroom_slice.rb (same shape,
|
|
87
|
+
# same mirrored lane counts curled from the real war room -- see that
|
|
88
|
+
# file's StoryStore comment for the full provenance note).
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
module StoryStore
|
|
91
|
+
DEFS = [
|
|
92
|
+
{ slug: "forge-the-beacon-relay", epic_slug: "field-ops", status: :done,
|
|
93
|
+
context: "Beacon-relay handshake wired between both outposts.",
|
|
94
|
+
intent: "Two outposts need a shared signal before the muster begins.",
|
|
95
|
+
next_action: nil,
|
|
96
|
+
criteria: [["Relay reaches both towers", :met], ["Signal survives storm interference", :met], ["Watch rotation briefed", :met]],
|
|
97
|
+
notes: [[:progress, "First handshake test succeeded at half range."], [:progress, "Full 4-league range held through the night watch."]],
|
|
98
|
+
stale: false },
|
|
99
|
+
{ slug: "mend-the-gate-hinge", epic_slug: "field-ops", status: :done,
|
|
100
|
+
context: "Gate hinge replaced and load-tested.",
|
|
101
|
+
intent: "Keep the east gate from seizing before winter.",
|
|
102
|
+
next_action: nil,
|
|
103
|
+
criteria: [["Hinge replaced", :met], ["Load test passed", :met]],
|
|
104
|
+
notes: [[:progress, "New hinge forged and fitted."], [:progress, "Load-tested to double the rated weight."]],
|
|
105
|
+
stale: false },
|
|
106
|
+
{ slug: "seal-the-old-cistern", epic_slug: "river-trade", status: :done,
|
|
107
|
+
context: "Cistern sealed; pressure test passed.",
|
|
108
|
+
intent: "Stop the slow leak before the dry season.",
|
|
109
|
+
next_action: nil,
|
|
110
|
+
criteria: [["Cistern sealed", :met], ["Pressure test passed", :met]],
|
|
111
|
+
notes: [[:progress, "Seal held at full pressure for a full day."]], stale: false },
|
|
112
|
+
{ slug: "clear-the-ashwood-pass", epic_slug: "field-ops", status: :done,
|
|
113
|
+
context: "Riders confirmed the pass safe for the muster.",
|
|
114
|
+
intent: "Confirm the ashwood pass is clear before the muster marches through.",
|
|
115
|
+
next_action: nil,
|
|
116
|
+
criteria: [["Riders dispatched", :met], ["Pass condition confirmed", :met]],
|
|
117
|
+
notes: [[:progress, "Riders returned at dusk -- pass reported clear."]], stale: false },
|
|
118
|
+
{ slug: "repair-the-north-bridge", epic_slug: "field-ops", status: :done,
|
|
119
|
+
context: "Crossbeams placed; load-bearing test passed.",
|
|
120
|
+
intent: "The north bridge must hold before the autumn floods.",
|
|
121
|
+
next_action: nil,
|
|
122
|
+
criteria: [["Timber delivered", :met], ["Crossbeams placed", :met], ["Load-bearing test", :met]],
|
|
123
|
+
notes: [[:progress, "Timber arrived a day ahead of schedule."], [:progress, "Load test held at double the rated weight."]],
|
|
124
|
+
stale: false },
|
|
125
|
+
{ slug: "translate-the-sunken-tablet", epic_slug: "river-trade", status: :done,
|
|
126
|
+
context: "Scriptorium translator resolved the old script.",
|
|
127
|
+
intent: "The tablet may name the vault's true owner.",
|
|
128
|
+
next_action: nil,
|
|
129
|
+
criteria: [["Tablet recovered", :met], ["Translator found", :met]],
|
|
130
|
+
notes: [[:progress, "Scriptorium translator confirmed the vault's owner."]], stale: false },
|
|
131
|
+
{ slug: "negotiate-toll-rights", epic_slug: "river-trade", status: :done,
|
|
132
|
+
context: "Guild council approved the toll rights.",
|
|
133
|
+
intent: "Toll rights unlock the southern supply route.",
|
|
134
|
+
next_action: nil,
|
|
135
|
+
criteria: [["Guild proposal submitted", :met], ["Council vote scheduled", :met]],
|
|
136
|
+
notes: [[:progress, "Council vote passed on the second reading."]], stale: false },
|
|
137
|
+
{ slug: "draft-the-harvest-ledger", epic_slug: "field-ops", status: :done,
|
|
138
|
+
context: "Ledger reconciled against the quartermaster's count.",
|
|
139
|
+
intent: "The harvest ledger must balance before winter stores are sealed.",
|
|
140
|
+
next_action: nil,
|
|
141
|
+
criteria: [["Ledger drafted", :met], ["Counts reconciled", :met]],
|
|
142
|
+
notes: [[:progress, "Ledger balanced on the first reconciliation pass."]], stale: false },
|
|
143
|
+
{ slug: "chart-the-southern-vault", epic_slug: "field-ops", status: :pending,
|
|
144
|
+
context: "Survey pass before the vault dig begins.",
|
|
145
|
+
intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
|
|
146
|
+
{ slug: "restock-the-armory", epic_slug: "field-ops", status: :pending,
|
|
147
|
+
context: "Quartermaster count due before the next campaign.",
|
|
148
|
+
intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
|
|
149
|
+
{ slug: "draft-the-spring-muster", epic_slug: "field-ops", status: :pending,
|
|
150
|
+
context: "First pass on troop assignments.",
|
|
151
|
+
intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
|
|
152
|
+
{ slug: "ready-the-siege-engines", epic_slug: "field-ops", status: :pending,
|
|
153
|
+
context: "Inventory pass before the engines are wheeled out.",
|
|
154
|
+
intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
|
|
155
|
+
{ slug: "survey-the-flooded-cellar", epic_slug: "river-trade", status: :pending,
|
|
156
|
+
context: "Waiting on a dry spell before the survey crew goes down.",
|
|
157
|
+
intent: nil, next_action: nil, criteria: [], notes: [], stale: false },
|
|
158
|
+
{ slug: "catalog-the-relic-hoard", epic_slug: "river-trade", status: :pending,
|
|
159
|
+
context: "First pass on the recovered hoard, pending an appraiser.",
|
|
160
|
+
intent: nil, next_action: nil, criteria: [], notes: [], stale: false }
|
|
161
|
+
].freeze
|
|
162
|
+
|
|
163
|
+
EMPTY_LANE = ENV["SW_PARITY_EMPTY_LANE"]&.to_sym
|
|
164
|
+
|
|
165
|
+
@stories = DEFS.each_with_index.map do |d, i|
|
|
166
|
+
d.merge(
|
|
167
|
+
id: i + 1,
|
|
168
|
+
last_note_at: d[:status] == :in_progress ? (d[:stale] ? Time.now - (9 * 3600) : Time.now - (i + 1) * 240) : nil,
|
|
169
|
+
completed_at: d[:status] == :done ? Time.now - ((i + 1) * 3600 * 9) : nil
|
|
170
|
+
)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
class << self
|
|
174
|
+
def all = @stories
|
|
175
|
+
def find(id) = @stories.find { |s| s[:id] == id.to_i }
|
|
176
|
+
def by_status(status) = status == EMPTY_LANE ? [] : @stories.select { |s| s[:status] == status }
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def relative_time(t)
|
|
181
|
+
return "" unless t
|
|
182
|
+
mins = ((Time.now - t) / 60).round
|
|
183
|
+
return "#{mins}m ago" if mins < 60
|
|
184
|
+
hours = mins / 60
|
|
185
|
+
return "#{hours}h ago" if hours < 24
|
|
186
|
+
"#{hours / 24}d ago"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
STATUS_LABEL = { pending: "pending", in_progress: "in progress", done: "done", blocked: "blocked" }.freeze
|
|
190
|
+
# Lane tone: -> board's own semantic tokens (--sw-success/warning/error/info),
|
|
191
|
+
# not tyrion's literal hex values -- the right abstraction level for a
|
|
192
|
+
# reusable option (design-parity-fights.md's own "not attempted" note #2).
|
|
193
|
+
LANE_TONE = { pending: :neutral, in_progress: :warning, blocked: :error, done: :success }.freeze
|
|
194
|
+
|
|
195
|
+
App = StreamWeaver::App.new(
|
|
196
|
+
"Tyrion War Room (components slice)",
|
|
197
|
+
chrome: false,
|
|
198
|
+
theme: :dashboard,
|
|
199
|
+
theme_overrides: { color_bg: "#0D0A07", color_text: "#F5E6C8" },
|
|
200
|
+
fonts: ["Cinzel:wght@400;600;700", "IBM+Plex+Mono:wght@300;400", "Lora:ital,wght@0,400;1,400;1,600"],
|
|
201
|
+
# Local-path auto-detection (App#resolve_stylesheet_href) serves this
|
|
202
|
+
# file via /sw-asset/... -- no hand-rolled endpoint needed, unlike the
|
|
203
|
+
# pixel slice's SW_PARITY_CSS proxy for the (much larger, real) shared.css.
|
|
204
|
+
stylesheets: stylesheet_list,
|
|
205
|
+
assets_dirs: [PARITY_ASSETS_DIR].compact
|
|
206
|
+
) do
|
|
207
|
+
def art_glyph(url, glyph, css_class:)
|
|
208
|
+
if url
|
|
209
|
+
div(class: "#{css_class} tc-art-img", style: "background-image:url('#{url}');")
|
|
210
|
+
else
|
|
211
|
+
phrase(glyph, class: "#{css_class} tc-art-glyph")
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
current_action = state[StreamWeaver::Resource::StateKeys::ACTION]
|
|
216
|
+
current_story = current_action.is_a?(Symbol) && current_action.to_s.start_with?("story_") ? StoryStore.find(current_action.to_s.delete_prefix("story_")) : nil
|
|
217
|
+
|
|
218
|
+
# =========================================================================
|
|
219
|
+
# Topbar chrome: the real `topbar` component (stream_weaver-oeo) --
|
|
220
|
+
# design-parity-fights.md finding #6 closed, no more hand-assembled
|
|
221
|
+
# div/phrase soup here.
|
|
222
|
+
# =========================================================================
|
|
223
|
+
topbar(icon: CREST_URL || "๐ฆ", wordmark: "TYRION",
|
|
224
|
+
breadcrumbs: ["field-ops", current_story ? current_story[:slug] : "warroom-components"]) do
|
|
225
|
+
phrase("โ parity/warroom-components", class: "tc-pill")
|
|
226
|
+
phrase("โ 2", class: "tc-pill tc-pill--amber")
|
|
227
|
+
end
|
|
228
|
+
navbar(class: "tc-navbar") do
|
|
229
|
+
nav_item("โ War Room", href: "/", active: current_story.nil?, close: current_story.nil?)
|
|
230
|
+
nav_item("๐บ Roadmap", href: "#")
|
|
231
|
+
nav_item("๐ Active Story", href: "#")
|
|
232
|
+
nav_item("๐ Global View", href: "#")
|
|
233
|
+
nav_item("๐ก Discoveries", href: "#")
|
|
234
|
+
nav_item("โ About Tyrion", href: "#")
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# =========================================================================
|
|
238
|
+
# app_shell owns the sidebar + main-content split -- the one real
|
|
239
|
+
# StreamWeaver layout primitive that maps directly onto tyrion's chrome.
|
|
240
|
+
# =========================================================================
|
|
241
|
+
app_shell(sidebar_position: :left, sidebar_width: "240px", gap: "0", class: "tc-shell") do
|
|
242
|
+
sidebar(sticky: true, class: "tc-sidebar") do
|
|
243
|
+
div(class: "tc-sidebar-path") { phrase("field-ops โบ warroom-components") }
|
|
244
|
+
div(class: "sw-sidebar-section") { phrase("Stories ยท warroom-components") }
|
|
245
|
+
|
|
246
|
+
StoryStore.all.each do |story|
|
|
247
|
+
icon_glyph = story[:status] == :pending ? "โ" : "โ"
|
|
248
|
+
clickable(href: "/stories/#{story[:id]}", class: "tc-story-row tc-story-row--#{story[:status]}") do
|
|
249
|
+
phrase(icon_glyph, class: "tc-story-icon")
|
|
250
|
+
phrase(story[:slug], class: "tc-story-name")
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
div(class: "tc-discoveries") do
|
|
255
|
+
div(class: "sw-sidebar-section") { phrase("Discoveries") }
|
|
256
|
+
clickable(href: "#", class: "tc-disc-row") do
|
|
257
|
+
phrase("spike", class: "tc-disc-pill tc-disc-pill--spike")
|
|
258
|
+
phrase("the flooded cellar timing", class: "tc-disc-label")
|
|
259
|
+
end
|
|
260
|
+
clickable(href: "#", class: "tc-disc-row") do
|
|
261
|
+
phrase("5 marks", class: "tc-disc-pill tc-disc-pill--mark")
|
|
262
|
+
phrase("unformalized", class: "tc-disc-label")
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
main do
|
|
268
|
+
# ======================================================================
|
|
269
|
+
# War Room board -- the actual Board/Lane/BoardCard proof.
|
|
270
|
+
# ======================================================================
|
|
271
|
+
page(:warroom, "/") do
|
|
272
|
+
div(class: "tc-board-hero") do
|
|
273
|
+
div(class: "tc-dragon-zone") do
|
|
274
|
+
art_glyph(DRAGON_URL, "๐", css_class: "tc-dragon-glyph")
|
|
275
|
+
header3("Here Be Dragons")
|
|
276
|
+
text("Uncertain requirements")
|
|
277
|
+
text("External dependency")
|
|
278
|
+
text("Needs human decision")
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
board(pinned_headers: true) do
|
|
282
|
+
[[:pending, "Queue", "Pending"], [:in_progress, "Active Campaign", "In Progress"],
|
|
283
|
+
[:blocked, "Blocked Frontier", "Blocked"], [:done, "Shipped Keep", "Done"]].each do |status, title, sub|
|
|
284
|
+
stories = StoryStore.by_status(status)
|
|
285
|
+
# lane's icon: slot (stream_weaver-oeo) replaces the
|
|
286
|
+
# title-prefix workaround design-parity-fights.md catalogued.
|
|
287
|
+
lane(title, tone: LANE_TONE.fetch(status), subtitle: sub, icon: status == :blocked ? (DRAGON_URL || "๐") : nil) do
|
|
288
|
+
stories.each { |story| render_wr_card(story) }
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# ======================================================================
|
|
296
|
+
# Story detail -- no dedicated "story detail" component exists
|
|
297
|
+
# (same gap as the pixel slice); built from card/card_header/card_body,
|
|
298
|
+
# which do have sw- hooks, plus tc- classes for the parts that don't
|
|
299
|
+
# map onto any existing primitive (hero status line, resume strip).
|
|
300
|
+
# ======================================================================
|
|
301
|
+
StoryStore.all.each do |story|
|
|
302
|
+
page(:"story_#{story[:id]}", "/stories/#{story[:id]}") do
|
|
303
|
+
div(class: "tc-story-page") do
|
|
304
|
+
clickable(href: "/", class: "tc-back-link") { phrase("โ War Room") }
|
|
305
|
+
|
|
306
|
+
card(class: "tc-story-hero") do
|
|
307
|
+
card_body do
|
|
308
|
+
div(class: "tc-hero-status") do
|
|
309
|
+
phrase(STATUS_LABEL.fetch(story[:status]), class: "sw-status-badge sw-status-badge--#{story[:status] == :in_progress ? 'maybe' : (story[:status] == :done ? 'strong' : 'skip')}")
|
|
310
|
+
if story[:last_note_at]
|
|
311
|
+
phrase("last note #{relative_time(story[:last_note_at])}", class: "tc-hero-meta")
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
header1(story[:slug], class: "tc-hero-title")
|
|
315
|
+
|
|
316
|
+
if story[:intent]
|
|
317
|
+
div(class: "tc-brief") do
|
|
318
|
+
phrase("MISSION BRIEF", class: "tc-brief-label")
|
|
319
|
+
text(story[:intent], class: "tc-brief-text")
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
card do
|
|
326
|
+
card_header("Current Context")
|
|
327
|
+
card_body { text(story[:context]) }
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
card do
|
|
331
|
+
card_header("Next Action")
|
|
332
|
+
card_body { text(story[:next_action] || "(not set)") }
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
div(class: "tc-story-split") do
|
|
336
|
+
card do
|
|
337
|
+
card_header("Criteria")
|
|
338
|
+
card_body do
|
|
339
|
+
if story[:criteria].any?
|
|
340
|
+
story[:criteria].each do |crit_text, status|
|
|
341
|
+
div(class: "tc-crit-row tc-crit-row--#{status}") do
|
|
342
|
+
phrase(status == :met ? "โ" : "โ", class: "tc-crit-check")
|
|
343
|
+
phrase(crit_text, class: "tc-crit-text")
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
else
|
|
347
|
+
text("No criteria yet")
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
card do
|
|
353
|
+
card_header("Notes")
|
|
354
|
+
card_body do
|
|
355
|
+
note_anchor = story[:last_note_at] || story[:completed_at] || Time.now
|
|
356
|
+
story[:notes].each_with_index do |(kind, body), idx|
|
|
357
|
+
div(class: "tc-note-entry") do
|
|
358
|
+
phrase("#{kind} ยท #{relative_time(note_anchor - (idx * 3600))}", class: "tc-note-meta")
|
|
359
|
+
text(body, class: "tc-note-body")
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
if story[:status] == :in_progress || story[:next_action] || story[:last_note_at]
|
|
367
|
+
div(class: "tc-resume-strip") do
|
|
368
|
+
div(class: "tc-rs-panel") do
|
|
369
|
+
phrase("LAST NOTE", class: "tc-rs-label")
|
|
370
|
+
text(story[:notes].first ? "#{relative_time(story[:last_note_at])} โ \"#{story[:notes].first[1].to_s[0, 60]}\"" : "no notes yet", class: "tc-rs-text")
|
|
371
|
+
end
|
|
372
|
+
div(class: "tc-rs-panel tc-rs-panel--beacon") do
|
|
373
|
+
art_glyph(LANTERN_URL, "๐ฎ", css_class: "tc-lantern-glyph")
|
|
374
|
+
div do
|
|
375
|
+
phrase("RESUME POINT", class: "tc-rs-label")
|
|
376
|
+
met = story[:criteria].count { |_, s| s == :met }
|
|
377
|
+
text(story[:criteria].any? ? "#{story[:slug]} ยท #{met}/#{story[:criteria].size} criteria met" : story[:slug], class: "tc-rs-text")
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
div(class: "tc-rs-panel") do
|
|
381
|
+
phrase("NEXT ACTION", class: "tc-rs-label")
|
|
382
|
+
text(story[:next_action] || "(not set)", class: "tc-rs-text")
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def render_wr_card(story)
|
|
394
|
+
card_class = if story[:stale]
|
|
395
|
+
"tc-card--stale"
|
|
396
|
+
elsif story[:status] == :done
|
|
397
|
+
"tc-card--done"
|
|
398
|
+
end
|
|
399
|
+
clickable(href: "/stories/#{story[:id]}") do
|
|
400
|
+
board_card(tone: story[:status] == :blocked ? :error : nil, class: card_class) do
|
|
401
|
+
phrase("โ STALE #{relative_time(story[:last_note_at])}", class: "tc-stale-badge") if story[:stale]
|
|
402
|
+
phrase(story[:epic_slug], class: "tc-card-epic")
|
|
403
|
+
header4(story[:slug])
|
|
404
|
+
text(story[:context].to_s[0, 60], class: "tc-card-ctx") if story[:context] && story[:status] != :done
|
|
405
|
+
if story[:status] == :done
|
|
406
|
+
phrase("โ done ยท #{relative_time(story[:completed_at])}", class: "tc-card-done-meta")
|
|
407
|
+
else
|
|
408
|
+
badge(STATUS_LABEL.fetch(story[:status]), variant: :default, size: :sm)
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
App.generate.run! if __FILE__ == $0
|