diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f3fcfa..f022f77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,5 +20,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Multiple layout engines: ELK (hierarchical/layered/force), webcola, Smart Layout - PNG/SVG export, JSON save/load - Electron desktop app + browser mode +- Mockup element (UX) with screen / route, design link and a low-fi SVG wireframe — generated by AI from the linked requirements and scenarios, sanitised and rendered as an image; shown on the canvas, in the properties panel and on Wiki pages +- Mockup relations: Illustrates (→ requirement / scenario), Presented by (→ web app / container), Navigates to (→ mockup) for screen flows +- Radical Forge: Mockups stage before C4 with batch wireframe generation, "Finish here" at every stage with a summary step, and a single footer action bar +- Hub blueprints: per-actor screen flows (79 mockups in 19 flows) and named views — canvas: System context, Containers, Governance map, Mockups, technical flow, one per screen flow; wiki: Governance, Requirements; a per-mode view selector and `/cv/` deep links in the Hub; views are imported with the concept + +### Fixed +- Hub concept files' `views` were ignored by the viewer and on import +- Importing from the Hub no longer stacks one "Imported …" notification per concept over the template-parameter form [Unreleased]: https://github.com/radicaltools/radical.tools/compare/HEAD diff --git a/README.md b/README.md index 957522d..6ef6276 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,12 @@ An open-source software architecture design studio — diagram, document, and ev - **C4 model** — Person, Software System, Container, Component, Database, Web App, Queue, Relation - **Domain & Governance elements** — Domain, ADR, Fitness Function, Requirement, Blueprint +- **UI mockups** — Mockup element with a design link or an AI-generated low-fi wireframe, linked to requirements and scenarios (*illustrates*), to the container that renders it (*presented by*) and to other screens (*navigates to*) to model screen flows - **Smart Layout** — SA-based auto-layout with crossing minimisation, edge-length optimisation, aspect-ratio penalty, and compound parent fitting - **Multiple layout engines** — ELK (hierarchical/layered/force), webcola (live physics), custom Smart Layout pipeline - **AI assistant** — chat with OpenAI / Anthropic / Gemini / Ollama to generate and modify diagrams -- **Architecture Hub** — browse and import 120+ curated architecture concepts (patterns, fitness functions, ADRs, requirements, blueprints) from [hub.radical.tools](https://hub.radical.tools) +- **Radical Forge** — turn a system description into requirements, fitness functions, Gherkin scenarios, UI mockups and a C4 model, one reviewable stage at a time +- **Architecture Hub** — browse and import 120+ curated architecture concepts (patterns, fitness functions, ADRs, requirements, blueprints) from [hub.radical.tools](https://hub.radical.tools); blueprints include per-actor screen flows and named views - **Multiple views** — Canvas, Matrix, Sequence, Treemap, Table, Wiki per diagram - **Presentation mode** — fullscreen slides with navigation bar - **Metamodel editor** — customise node types, relation types, and constraints diff --git a/hub/blueprint/bp-ai-assistant-platform.radical b/hub/blueprint/bp-ai-assistant-platform.radical index 98f9ab2..d853dbf 100644 --- a/hub/blueprint/bp-ai-assistant-platform.radical +++ b/hub/blueprint/bp-ai-assistant-platform.radical @@ -1267,7 +1267,7 @@ }, { "id": "view-governance", - "name": "Governance", + "name": "Governance map", "kind": "static", "nodeIds": [ "req-escalation-pickup", @@ -1385,6 +1385,106 @@ } } }, + { + "id": "view-mockups", + "name": "Mockups", + "kind": "static", + "nodeIds": [ + "flow-customer", + "flow-human-agent", + "flow-assistant-ops", + "mockup-chat-start", + "mockup-chat-answer", + "mockup-support-chat", + "mockup-chat-human", + "mockup-escalation-queue", + "mockup-case-review", + "mockup-case-resolved", + "mockup-case-audit", + "mockup-kb-sources", + "mockup-quality-drift" + ], + "positions": { + "flow-assistant-ops": { + "x": 0, + "y": 2904, + "width": 600, + "height": 330 + }, + "flow-customer": { + "x": 0, + "y": 2084, + "width": 1240, + "height": 330 + }, + "flow-human-agent": { + "x": 0, + "y": 2494, + "width": 1240, + "height": 330 + }, + "mockup-case-audit": { + "x": 990, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-case-resolved": { + "x": 670, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-case-review": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-chat-answer": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-chat-human": { + "x": 990, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-chat-start": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-escalation-queue": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-kb-sources": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-quality-drift": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-support-chat": { + "x": 670, + "y": 110, + "width": 220, + "height": 190 + } + } + }, { "id": "view-flow-customer", "name": "Customer journey", @@ -1505,6 +1605,37 @@ "height": 190 } } + }, + { + "id": "view-wiki-governance", + "name": "Governance", + "kind": "wiki", + "nodeIds": [ + "req-escalation-pickup", + "req-extraction-review", + "req-kb-freshness", + "req-case-audit-trail", + "ff-escalation-backlog-guard", + "ff-extraction-drift-monitor", + "ff-cost-per-case-guard", + "adr-confidence-gated-autonomy", + "adr-retrieval-over-finetuning" + ], + "positions": {}, + "wikiFocusId": null + }, + { + "id": "view-wiki-requirements", + "name": "Requirements", + "kind": "wiki", + "nodeIds": [ + "req-escalation-pickup", + "req-extraction-review", + "req-kb-freshness", + "req-case-audit-trail" + ], + "positions": {}, + "wikiFocusId": null } ] } diff --git a/hub/blueprint/bp-analytical-data-platform.radical b/hub/blueprint/bp-analytical-data-platform.radical index b573db0..23e52d3 100644 --- a/hub/blueprint/bp-analytical-data-platform.radical +++ b/hub/blueprint/bp-analytical-data-platform.radical @@ -1099,7 +1099,7 @@ }, { "id": "view-governance", - "name": "Governance", + "name": "Governance map", "kind": "static", "nodeIds": [ "req-freshness", @@ -1210,6 +1210,92 @@ } } }, + { + "id": "view-mockups", + "name": "Mockups", + "kind": "static", + "nodeIds": [ + "flow-analyst", + "flow-data-engineer", + "mockup-bi-home", + "mockup-sales-dashboard", + "mockup-explore", + "mockup-dataset-lineage", + "mockup-access-request", + "mockup-pipelines", + "mockup-quality-runs", + "mockup-quarantine", + "mockup-freshness" + ], + "positions": { + "flow-analyst": { + "x": 0, + "y": 1534, + "width": 1560, + "height": 330 + }, + "flow-data-engineer": { + "x": 0, + "y": 1944, + "width": 1240, + "height": 330 + }, + "mockup-access-request": { + "x": 1310, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-bi-home": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-dataset-lineage": { + "x": 990, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-explore": { + "x": 670, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-freshness": { + "x": 990, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-pipelines": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-quality-runs": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-quarantine": { + "x": 670, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-sales-dashboard": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + } + } + }, { "id": "view-flow-analyst", "name": "Analyst journey", @@ -1306,6 +1392,37 @@ "height": 190 } } + }, + { + "id": "view-wiki-governance", + "name": "Governance", + "kind": "wiki", + "nodeIds": [ + "req-freshness", + "req-quality-gate", + "req-rls", + "req-lineage", + "ff-freshness", + "ff-quality", + "ff-cost", + "adr-lakehouse", + "adr-dbt" + ], + "positions": {}, + "wikiFocusId": null + }, + { + "id": "view-wiki-requirements", + "name": "Requirements", + "kind": "wiki", + "nodeIds": [ + "req-freshness", + "req-quality-gate", + "req-rls", + "req-lineage" + ], + "positions": {}, + "wikiFocusId": null } ] } diff --git a/hub/blueprint/bp-ecommerce-platform.radical b/hub/blueprint/bp-ecommerce-platform.radical index a367375..812ea92 100644 --- a/hub/blueprint/bp-ecommerce-platform.radical +++ b/hub/blueprint/bp-ecommerce-platform.radical @@ -1596,7 +1596,7 @@ }, { "id": "view-governance", - "name": "Governance", + "name": "Governance map", "kind": "static", "nodeIds": [ "req-checkout-latency", @@ -1701,6 +1701,127 @@ } } }, + { + "id": "view-mockups", + "name": "Mockups", + "kind": "static", + "nodeIds": [ + "flow-shopper", + "flow-operations", + "mockup-home", + "mockup-search-results", + "mockup-product-page", + "mockup-cart", + "mockup-checkout-address", + "mockup-checkout-shipping", + "mockup-checkout", + "mockup-payment-declined", + "mockup-order-confirmation", + "mockup-order-tracking", + "mockup-bo-catalogue", + "mockup-bo-product-editor", + "mockup-bo-orders", + "mockup-bo-order-detail" + ], + "positions": { + "flow-operations": { + "x": 0, + "y": 2794, + "width": 1240, + "height": 330 + }, + "flow-shopper": { + "x": 0, + "y": 2084, + "width": 1560, + "height": 630 + }, + "mockup-bo-catalogue": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-bo-order-detail": { + "x": 990, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-bo-orders": { + "x": 670, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-bo-product-editor": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-cart": { + "x": 990, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-checkout": { + "x": 350, + "y": 410, + "width": 220, + "height": 190 + }, + "mockup-checkout-address": { + "x": 1310, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-checkout-shipping": { + "x": 30, + "y": 410, + "width": 220, + "height": 190 + }, + "mockup-home": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-order-confirmation": { + "x": 990, + "y": 410, + "width": 220, + "height": 190 + }, + "mockup-order-tracking": { + "x": 1310, + "y": 410, + "width": 220, + "height": 190 + }, + "mockup-payment-declined": { + "x": 670, + "y": 410, + "width": 220, + "height": 190 + }, + "mockup-product-page": { + "x": 670, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-search-results": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + } + } + }, { "id": "view-flow-shopper", "name": "Shopper journey", @@ -1832,6 +1953,37 @@ "height": 190 } } + }, + { + "id": "view-wiki-governance", + "name": "Governance", + "kind": "wiki", + "nodeIds": [ + "req-checkout-latency", + "req-payment-once", + "req-stock-reservation", + "req-pci-scope", + "ff-checkout-conversion", + "ff-payment-reconciliation", + "ff-search-freshness", + "adr-hosted-payments", + "adr-database-per-service" + ], + "positions": {}, + "wikiFocusId": null + }, + { + "id": "view-wiki-requirements", + "name": "Requirements", + "kind": "wiki", + "nodeIds": [ + "req-checkout-latency", + "req-payment-once", + "req-stock-reservation", + "req-pci-scope" + ], + "positions": {}, + "wikiFocusId": null } ] } diff --git a/hub/blueprint/bp-fintech-ledger-platform.radical b/hub/blueprint/bp-fintech-ledger-platform.radical index cd8f430..d171e03 100644 --- a/hub/blueprint/bp-fintech-ledger-platform.radical +++ b/hub/blueprint/bp-fintech-ledger-platform.radical @@ -1306,7 +1306,7 @@ }, { "id": "view-governance", - "name": "Governance", + "name": "Governance map", "kind": "static", "nodeIds": [ "req-balanced-entry-enforcement", @@ -1417,6 +1417,120 @@ } } }, + { + "id": "view-mockups", + "name": "Mockups", + "kind": "static", + "nodeIds": [ + "flow-customer", + "flow-finance-ops", + "flow-compliance", + "mockup-accounts-overview", + "mockup-statement", + "mockup-transaction-detail", + "mockup-dispute", + "mockup-dispute-status", + "mockup-reconciliation-exceptions", + "mockup-exception-detail", + "mockup-compensating-entry", + "mockup-entry-approval", + "mockup-journal-entry", + "mockup-audit-trail", + "mockup-regulatory-reports" + ], + "positions": { + "flow-compliance": { + "x": 0, + "y": 2904, + "width": 600, + "height": 330 + }, + "flow-customer": { + "x": 0, + "y": 2084, + "width": 1560, + "height": 330 + }, + "flow-finance-ops": { + "x": 0, + "y": 2494, + "width": 1560, + "height": 330 + }, + "mockup-accounts-overview": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-audit-trail": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-compensating-entry": { + "x": 670, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-dispute": { + "x": 990, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-dispute-status": { + "x": 1310, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-entry-approval": { + "x": 990, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-exception-detail": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-journal-entry": { + "x": 1310, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-reconciliation-exceptions": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-regulatory-reports": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-statement": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-transaction-detail": { + "x": 670, + "y": 110, + "width": 220, + "height": 190 + } + } + }, { "id": "view-flow-customer", "name": "Customer journey", @@ -1551,6 +1665,37 @@ "height": 190 } } + }, + { + "id": "view-wiki-governance", + "name": "Governance", + "kind": "wiki", + "nodeIds": [ + "req-balanced-entry-enforcement", + "req-reconciliation-freshness", + "req-statement-accuracy-cutoff", + "req-dispute-investigation-sla", + "ff-settlement-backlog-guard", + "ff-manual-adjustment-rate-guard", + "ff-regulatory-report-timeliness", + "adr-suspense-account-for-unmatched", + "adr-ledger-as-system-of-record" + ], + "positions": {}, + "wikiFocusId": null + }, + { + "id": "view-wiki-requirements", + "name": "Requirements", + "kind": "wiki", + "nodeIds": [ + "req-balanced-entry-enforcement", + "req-reconciliation-freshness", + "req-statement-accuracy-cutoff", + "req-dispute-investigation-sla" + ], + "positions": {}, + "wikiFocusId": null } ] } diff --git a/hub/blueprint/bp-marketplace-platform.radical b/hub/blueprint/bp-marketplace-platform.radical index c8c592c..9b16558 100644 --- a/hub/blueprint/bp-marketplace-platform.radical +++ b/hub/blueprint/bp-marketplace-platform.radical @@ -1383,7 +1383,7 @@ }, { "id": "view-governance", - "name": "Governance", + "name": "Governance map", "kind": "static", "nodeIds": [ "req-listing-moderation", @@ -1494,6 +1494,127 @@ } } }, + { + "id": "view-mockups", + "name": "Mockups", + "kind": "static", + "nodeIds": [ + "flow-buyer", + "flow-seller", + "flow-trust-safety", + "mockup-mp-search", + "mockup-mp-listing", + "mockup-mp-checkout", + "mockup-order-dispute", + "mockup-mp-open-dispute", + "mockup-mp-dispute-status", + "mockup-seller-dashboard", + "mockup-new-listing", + "mockup-my-listings", + "mockup-seller-payouts", + "mockup-listing-review-queue", + "mockup-seller-investigation", + "mockup-ts-dispute-case" + ], + "positions": { + "flow-buyer": { + "x": 0, + "y": 2084, + "width": 1560, + "height": 630 + }, + "flow-seller": { + "x": 0, + "y": 2794, + "width": 1240, + "height": 330 + }, + "flow-trust-safety": { + "x": 0, + "y": 3204, + "width": 920, + "height": 330 + }, + "mockup-listing-review-queue": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-mp-checkout": { + "x": 670, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-mp-dispute-status": { + "x": 30, + "y": 410, + "width": 220, + "height": 190 + }, + "mockup-mp-listing": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-mp-open-dispute": { + "x": 1310, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-mp-search": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-my-listings": { + "x": 670, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-new-listing": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-order-dispute": { + "x": 990, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-seller-dashboard": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-seller-investigation": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-seller-payouts": { + "x": 990, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-ts-dispute-case": { + "x": 670, + "y": 110, + "width": 220, + "height": 190 + } + } + }, { "id": "view-flow-buyer", "name": "Buyer journey", @@ -1635,6 +1756,37 @@ "height": 190 } } + }, + { + "id": "view-wiki-governance", + "name": "Governance", + "kind": "wiki", + "nodeIds": [ + "req-listing-moderation", + "req-commission-accuracy", + "req-dispute-window-hold", + "req-delisting-suppression", + "ff-payout-reconciliation-guard", + "ff-chargeback-rate-guard", + "ff-search-relevance-guard", + "adr-centralised-search-index", + "adr-suspend-first-moderation" + ], + "positions": {}, + "wikiFocusId": null + }, + { + "id": "view-wiki-requirements", + "name": "Requirements", + "kind": "wiki", + "nodeIds": [ + "req-listing-moderation", + "req-commission-accuracy", + "req-dispute-window-hold", + "req-delisting-suppression" + ], + "positions": {}, + "wikiFocusId": null } ] } diff --git a/hub/blueprint/bp-realtime-collaboration.radical b/hub/blueprint/bp-realtime-collaboration.radical index 70d210e..393042c 100644 --- a/hub/blueprint/bp-realtime-collaboration.radical +++ b/hub/blueprint/bp-realtime-collaboration.radical @@ -1092,7 +1092,7 @@ }, { "id": "view-governance", - "name": "Governance", + "name": "Governance map", "kind": "static", "nodeIds": [ "req-offline-merge", @@ -1203,6 +1203,92 @@ } } }, + { + "id": "view-mockups", + "name": "Mockups", + "kind": "static", + "nodeIds": [ + "flow-collaborator", + "flow-workspace-admin", + "mockup-doc-list", + "mockup-document-editor", + "mockup-reconnect-merge", + "mockup-version-history", + "mockup-restore-version", + "mockup-share-dialog", + "mockup-workspace-members", + "mockup-remove-member", + "mockup-access-revoked" + ], + "positions": { + "flow-collaborator": { + "x": 0, + "y": 2084, + "width": 1560, + "height": 330 + }, + "flow-workspace-admin": { + "x": 0, + "y": 2494, + "width": 1240, + "height": 330 + }, + "mockup-access-revoked": { + "x": 990, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-doc-list": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-document-editor": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-reconnect-merge": { + "x": 670, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-remove-member": { + "x": 670, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-restore-version": { + "x": 1310, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-share-dialog": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-version-history": { + "x": 990, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-workspace-members": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + } + } + }, { "id": "view-flow-collaborator", "name": "Collaborator journey", @@ -1299,6 +1385,37 @@ "height": 190 } } + }, + { + "id": "view-wiki-governance", + "name": "Governance", + "kind": "wiki", + "nodeIds": [ + "req-offline-merge", + "req-room-capacity", + "req-access-revocation", + "req-snapshot-recovery", + "ff-presence-fanout-guard", + "ff-snapshot-compaction-lag", + "ff-reconnect-success-rate", + "adr-crdt-over-ot", + "adr-stateless-gateway-fleet" + ], + "positions": {}, + "wikiFocusId": null + }, + { + "id": "view-wiki-requirements", + "name": "Requirements", + "kind": "wiki", + "nodeIds": [ + "req-offline-merge", + "req-room-capacity", + "req-access-revocation", + "req-snapshot-recovery" + ], + "positions": {}, + "wikiFocusId": null } ] } diff --git a/hub/blueprint/bp-saas-b2b-platform.radical b/hub/blueprint/bp-saas-b2b-platform.radical index 1581387..187e39f 100644 --- a/hub/blueprint/bp-saas-b2b-platform.radical +++ b/hub/blueprint/bp-saas-b2b-platform.radical @@ -1335,7 +1335,7 @@ }, { "id": "view-governance", - "name": "Governance", + "name": "Governance map", "kind": "static", "nodeIds": [ "req-tenant-isolation", @@ -1405,6 +1405,127 @@ } } }, + { + "id": "view-mockups", + "name": "Mockups", + "kind": "static", + "nodeIds": [ + "flow-onboarding", + "flow-tenant-user", + "flow-tenant-admin", + "flow-platform-ops", + "mockup-signup", + "mockup-provisioning", + "mockup-invite-team", + "mockup-workspace-dashboard", + "mockup-project", + "mockup-read-only", + "mockup-billing-usage", + "mockup-update-payment", + "mockup-sso-settings", + "mockup-users-roles", + "mockup-tenant-list", + "mockup-tenant-detail" + ], + "positions": { + "flow-onboarding": { + "x": 0, + "y": 2324, + "width": 920, + "height": 330 + }, + "flow-platform-ops": { + "x": 0, + "y": 3554, + "width": 600, + "height": 330 + }, + "flow-tenant-admin": { + "x": 0, + "y": 3144, + "width": 1240, + "height": 330 + }, + "flow-tenant-user": { + "x": 0, + "y": 2734, + "width": 920, + "height": 330 + }, + "mockup-billing-usage": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-invite-team": { + "x": 670, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-project": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-provisioning": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-read-only": { + "x": 670, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-signup": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-sso-settings": { + "x": 670, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-tenant-detail": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-tenant-list": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-update-payment": { + "x": 350, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-users-roles": { + "x": 990, + "y": 110, + "width": 220, + "height": 190 + }, + "mockup-workspace-dashboard": { + "x": 30, + "y": 110, + "width": 220, + "height": 190 + } + } + }, { "id": "view-flow-onboarding", "name": "Onboarding journey", @@ -1556,6 +1677,37 @@ "height": 190 } } + }, + { + "id": "view-wiki-governance", + "name": "Governance", + "kind": "wiki", + "nodeIds": [ + "req-tenant-isolation", + "req-tenant-provisioning-time", + "req-subscription-enforcement", + "req-usage-metering-accuracy", + "ff-tenant-isolation-fuzz", + "ff-noisy-neighbor-guard", + "ff-billing-reconciliation", + "adr-tenancy-model", + "adr-sso-federation" + ], + "positions": {}, + "wikiFocusId": null + }, + { + "id": "view-wiki-requirements", + "name": "Requirements", + "kind": "wiki", + "nodeIds": [ + "req-tenant-isolation", + "req-tenant-provisioning-time", + "req-subscription-enforcement", + "req-usage-metering-accuracy" + ], + "positions": {}, + "wikiFocusId": null } ] } diff --git a/src/renderer/src/hub/HubApp.tsx b/src/renderer/src/hub/HubApp.tsx index c8d414b..66cdbe4 100644 --- a/src/renderer/src/hub/HubApp.tsx +++ b/src/renderer/src/hub/HubApp.tsx @@ -23,6 +23,7 @@ import { conceptToDiagramData, defaultViewKind, kindForViewId, + isSyntheticViewId, viewIdForKind, type HubViewKind, } from './conceptToDiagram' @@ -263,9 +264,9 @@ function HubAppInner(): React.ReactElement { const [theme, setTheme] = useState<'dark' | 'light'>(() => (localStorage.getItem(LS_THEME) as 'dark' | 'light') || 'dark') const [collapsedGroups, setCollapsedGroups] = useState>(() => new Set()) const [tagsExpanded, setTagsExpanded] = useState(false) - // Pending view kind (and named canvas view) from a deep link, applied once the concept is loaded. + // Pending view kind (and named view) from a deep link, applied once the concept is loaded. const pendingView = useRef(parseHubHash(window.location.hash).view) - const pendingCanvasView = useRef(parseHubHash(window.location.hash).canvasView) + const pendingNamedView = useRef(parseHubHash(window.location.hash).namedView) const views = useDiagramStore((s) => s.views) useEffect(() => { @@ -292,14 +293,15 @@ function HubAppInner(): React.ReactElement { [concept, concepts], ) const [conceptError, setConceptError] = useState(null) - const viewKind = kindForViewId(activeViewId) - // The concept's own named canvas views ("Containers", a screen flow, …); - // `null` = the full model ("All elements"). - const canvasViews = useMemo( - () => Object.values(views).filter((v) => kindForViewId(v.id) === 'canvas'), - [views], + const viewKind = kindForViewId(activeViewId, views) + // The concept's own named views for the current mode ("Containers" or a + // screen flow on the canvas, "Governance" in the wiki, …); `null` = all + // elements (the default canvas / the synthetic wiki and table views). + const namedViews = useMemo( + () => Object.values(views).filter((v) => !isSyntheticViewId(v.id) && kindForViewId(v.id, views) === viewKind), + [views, viewKind], ) - const canvasViewId = viewKind === 'canvas' ? activeViewId : null + const namedViewId = activeViewId && !isSyntheticViewId(activeViewId) ? activeViewId : null // Fetch the selected concept's .radical file once the index knows about it. useEffect(() => { @@ -316,13 +318,16 @@ function HubAppInner(): React.ReactElement { useEffect(() => { if (!concept) return loadDiagram(conceptToDiagramData(concept)) - const kind = pendingView.current ?? defaultViewKind(concept) - const named = pendingCanvasView.current + const named = pendingNamedView.current + const namedDef = named ? (concept.views ?? []).find((v) => v.id === named) : undefined + const namedKind: HubViewKind | undefined = namedDef + ? namedDef.kind === 'wiki' ? 'wiki' : namedDef.kind === 'table' ? 'table' : 'canvas' + : undefined + const kind = namedKind ?? pendingView.current ?? defaultViewKind(concept) pendingView.current = undefined - pendingCanvasView.current = undefined - const hasNamed = kind === 'canvas' && !!named && (concept.views ?? []).some((v) => v.id === named) - if (hasNamed) loadDiagram(conceptToDiagramData(concept, { layoutFrom: named })) - setActiveView(hasNamed ? named! : viewIdForKind(kind)) + pendingNamedView.current = undefined + if (namedKind === 'canvas') loadDiagram(conceptToDiagramData(concept, { layoutFrom: named })) + setActiveView(namedKind ? named! : viewIdForKind(kind)) if (kind === 'canvas') return scheduleFit(fitAll) }, [concept, loadDiagram, setActiveView, fitAll]) @@ -341,7 +346,7 @@ function HubAppInner(): React.ReactElement { browse: browsing, concept: concept?.id, view: concept ? viewKind : undefined, - canvasView: concept && canvasViewId ? canvasViewId : undefined, + namedView: concept && namedViewId ? namedViewId : undefined, category: activeCategory ?? undefined, tag: setToCsv(activeTags), status: setToCsv(activeStatuses), @@ -353,16 +358,15 @@ function HubAppInner(): React.ReactElement { const url = hash || window.location.pathname if (changed) history.pushState(null, '', url) else history.replaceState(null, '', url) - }, [browsing, concept, viewKind, canvasViewId, activeCategory, activeTags, activeStatuses, sortBy]) + }, [browsing, concept, viewKind, namedViewId, activeCategory, activeTags, activeStatuses, sortBy]) // URL → state (back/forward, pasted links). useEffect(() => { const onHash = () => { const r = parseHubHash(window.location.hash) setBrowsing(!!r.browse) - if (r.concept !== conceptId) { pendingView.current = r.view; pendingCanvasView.current = r.canvasView; setConceptId(r.concept) } - else if (r.view === 'canvas' && (r.canvasView ?? null) !== canvasViewId) switchCanvasViewRef.current(r.canvasView ?? null) - else if (r.view && r.view !== viewKind) setActiveView(viewIdForKind(r.view)) + if (r.concept !== conceptId) { pendingView.current = r.view; pendingNamedView.current = r.namedView; setConceptId(r.concept) } + else if (r.view && (r.view !== viewKind || (r.namedView ?? null) !== namedViewId)) switchNamedViewRef.current(r.view, r.namedView ?? null) setCategory(r.category ?? null) setTags(csvToSet(r.tag)) setStatuses(csvToSet(r.status)) @@ -374,7 +378,7 @@ function HubAppInner(): React.ReactElement { window.removeEventListener('hashchange', onHash) window.removeEventListener('popstate', onHash) } - }, [conceptId, viewKind, canvasViewId, setActiveView, setCategory, setTags, setStatuses, setSortBy]) + }, [conceptId, viewKind, namedViewId, setActiveView, setCategory, setTags, setStatuses, setSortBy]) // Reload the concept laid out for the chosen view (null = the full model // in its default layout) — see conceptToDiagramData's `layoutFrom`. @@ -384,13 +388,15 @@ function HubAppInner(): React.ReactElement { setActiveView(viewId) scheduleFit(fitAll) }, [concept, loadDiagram, setActiveView, fitAll]) - const switchCanvasViewRef = useRef(switchCanvasView) - switchCanvasViewRef.current = switchCanvasView - - const switchView = useCallback((kind: HubViewKind) => { - if (kind === 'canvas') return switchCanvasView(null) - setActiveView(viewIdForKind(kind)) + // Any mode + optional named view; canvas goes through the reload above. + const switchNamedView = useCallback((kind: HubViewKind, viewId: string | null) => { + if (kind === 'canvas') return switchCanvasView(viewId) + setActiveView(viewId ?? viewIdForKind(kind)) }, [setActiveView, switchCanvasView]) + const switchNamedViewRef = useRef(switchNamedView) + switchNamedViewRef.current = switchNamedView + + const switchView = useCallback((kind: HubViewKind) => switchNamedView(kind, null), [switchNamedView]) const toggleSelect = useCallback((id: string) => { setSelectedIds((prev) => toggleInSet(prev, id)) @@ -687,21 +693,21 @@ function HubAppInner(): React.ReactElement { ))} + {namedViews.length > 0 && ( + + )} {viewKind === 'canvas' && ( <> - {canvasViews.length > 0 && ( - - )} diff --git a/src/renderer/src/hub/conceptToDiagram.ts b/src/renderer/src/hub/conceptToDiagram.ts index 74b38c3..3fa18fa 100644 --- a/src/renderer/src/hub/conceptToDiagram.ts +++ b/src/renderer/src/hub/conceptToDiagram.ts @@ -32,12 +32,22 @@ export function viewIdForKind(kind: HubViewKind): string | null { return null } -export function kindForViewId(viewId: string | null): HubViewKind { +/** Mode a view belongs to. Pass the loaded views so the concept's own named + * wiki / table views are recognised (everything else is a canvas view). */ +export function kindForViewId(viewId: string | null, views?: Record): HubViewKind { if (viewId === HUB_WIKI_VIEW_ID) return 'wiki' if (viewId === HUB_TABLE_VIEW_ID) return 'table' + const kind = viewId ? views?.[viewId]?.kind : undefined + if (kind === 'wiki') return 'wiki' + if (kind === 'table') return 'table' return 'canvas' } +/** True for the viewer's synthetic all-elements wiki / table views. */ +export function isSyntheticViewId(viewId: string | null): boolean { + return viewId === HUB_WIKI_VIEW_ID || viewId === HUB_TABLE_VIEW_ID +} + /** Fill `{{KEY}}` placeholders with each parameter's default, but keep the * `{{…}}` wrapper around the substituted value (rather than dropping it) so * a reader browsing the hub can still tell which words in the prose are a diff --git a/src/renderer/src/hub/conceptViews.ts b/src/renderer/src/hub/conceptViews.ts index ffab088..279a597 100644 --- a/src/renderer/src/hub/conceptViews.ts +++ b/src/renderer/src/hub/conceptViews.ts @@ -5,8 +5,8 @@ // as-is) and both studio import paths (ids remapped to the freshly created // nodes / relations / sequences) — so it lives here once. // -// Only canvas kinds are taken over: the Hub builds its own wiki / table views -// and the studio has its defaults. A view keeps only the elements that exist +// Canvas (static / dynamic), wiki and table views are taken over; the Hub +// adds its own all-elements wiki / table views on top. A view keeps only the elements that exist // on the receiving side, so a partial blueprint import still gets sensible // views, and a view left with no nodes is dropped. A dynamic view whose // sequence did not survive falls back to a static view of the same nodes. @@ -71,7 +71,9 @@ export function conceptViewsToDiagram( ): DiagramView[] { const out: DiagramView[] = [] for (const [i, raw] of (rawViews ?? []).entries()) { - const kind = raw.kind === 'dynamic' ? 'dynamic' : raw.kind === undefined || raw.kind === 'static' ? 'static' : null + const kind = raw.kind === undefined ? 'static' + : raw.kind === 'static' || raw.kind === 'dynamic' || raw.kind === 'wiki' || raw.kind === 'table' ? raw.kind + : null if (!kind) continue const nodeIds = mapIds(raw.nodeIds, maps.node) if (nodeIds.length === 0) continue @@ -80,11 +82,17 @@ export function conceptViewsToDiagram( const view: DiagramView = { id: opts.newId ? opts.newId() : typeof raw.id === 'string' ? raw.id : `view-${i}`, name: opts.namePrefix ? `${opts.namePrefix} — ${name}` : name, - kind: sequenceId ? 'dynamic' : 'static', + kind: kind === 'dynamic' ? (sequenceId ? 'dynamic' : 'static') : kind, nodeIds, positions: opts.keepPositions ? mapPositions(raw.positions, maps.node) : {}, } if (sequenceId) view.sequenceId = sequenceId + if (kind === 'wiki') { + const focus = typeof raw.wikiFocusId === 'string' ? maps.node(raw.wikiFocusId) : undefined + view.wikiFocusId = focus ?? null + if (raw.wikiPageMode === 'single' || raw.wikiPageMode === 'multi') view.wikiPageMode = raw.wikiPageMode + } + if (kind === 'table' && typeof raw.tableActiveTab === 'string') view.tableActiveTab = raw.tableActiveTab const collapsed = mapIds(raw.collapsedNodeIds, maps.node) if (collapsed.length) view.collapsedNodeIds = collapsed const expanded = mapIds(raw.expandedNodeIds, maps.node) diff --git a/src/renderer/src/hub/hubRoute.ts b/src/renderer/src/hub/hubRoute.ts index 7a28030..dc61530 100644 --- a/src/renderer/src/hub/hubRoute.ts +++ b/src/renderer/src/hub/hubRoute.ts @@ -4,7 +4,7 @@ // the catalogue: // // #/browse -// #/c/[/v/][/cv/][/cat/][/tag/][/status/][/sort/] +// #/c/[/v/][/cv/][/cat/][/tag/][/status/][/sort/] // // `c` selects the concept shown in the viewer, `v` its presentation, `cat` / // `tag` / `status` / `sort` the catalogue filters. `tag` and `status` hold a @@ -21,8 +21,8 @@ export interface HubRoute { browse?: boolean concept?: string view?: HubViewKind - /** Named canvas view of the concept (only with view = canvas). */ - canvasView?: string + /** One of the concept's named views, in the mode given by `view`. */ + namedView?: string category?: string /** Comma-separated tag list. */ tag?: string @@ -51,7 +51,7 @@ export function parseHubHash(hash: string): HubRoute { const view = (VIEW_KINDS as readonly string[]).includes(map.v) ? (map.v as HubViewKind) : undefined const sort = (SORT_KEYS as readonly string[]).includes(map.sort) ? (map.sort as HubSortKey) : undefined const concept = map.c || undefined - const canvasView = view === 'canvas' || !view ? map.cv || undefined : undefined + const namedView = map.cv || undefined const category = map.cat || undefined const tag = map.tag || undefined const status = map.status || undefined @@ -60,7 +60,7 @@ export function parseHubHash(hash: string): HubRoute { ...(browse ? { browse: true } : {}), concept, view, - ...(canvasView ? { canvasView } : {}), + ...(namedView ? { namedView } : {}), category, tag, status, @@ -72,7 +72,7 @@ export function formatHubHash(route: HubRoute): string { const segs: string[] = [] if (route.concept) segs.push('c', encodeURIComponent(route.concept)) if (route.concept && route.view) segs.push('v', route.view) - if (route.concept && route.view === 'canvas' && route.canvasView) segs.push('cv', encodeURIComponent(route.canvasView)) + if (route.concept && route.view && route.namedView) segs.push('cv', encodeURIComponent(route.namedView)) if (route.category) segs.push('cat', encodeURIComponent(route.category)) if (route.tag) segs.push('tag', encodeURIComponent(route.tag)) if (route.status) segs.push('status', encodeURIComponent(route.status)) diff --git a/tests/hubCatalogue.test.ts b/tests/hubCatalogue.test.ts index 59c4d08..ed02ced 100644 --- a/tests/hubCatalogue.test.ts +++ b/tests/hubCatalogue.test.ts @@ -7,8 +7,9 @@ * - blueprint mockups form per-actor screen flows (flow groups linked by * navigates-to), use metamodel-valid relations and carry wireframes that * are already sanitised - * - blueprints ship named views (context, containers, governance, one per - * screen flow) over sequences whose steps all exist + * - blueprints ship named views (context, containers, governance map, + * mockups, one per screen flow, governance / requirements wikis) over + * sequences whose steps all exist */ import { describe, it, expect } from 'vitest' import { resolve } from 'node:path' @@ -138,7 +139,7 @@ describe('blueprint views and sequences', () => { expect(s.stepDescriptions ?? []).toHaveLength(s.relationIds.length) } for (const v of views) { - expect(['static', 'dynamic']).toContain(v.kind) + expect(['static', 'dynamic', 'wiki']).toContain(v.kind) for (const id of v.nodeIds) expect(nodeIds.has(id), `${v.id}: unknown node ${id}`).toBe(true) if (v.kind === 'dynamic') expect(seqIds.has(v.sequenceId!), `${v.id}: unknown sequence`).toBe(true) for (const [id, p] of Object.entries(v.positions)) { @@ -148,7 +149,14 @@ describe('blueprint views and sequences', () => { } const ids = views.map((v) => v.id) - expect(ids).toEqual(expect.arrayContaining(['view-context', 'view-containers', 'view-governance'])) + expect(ids).toEqual(expect.arrayContaining([ + 'view-context', 'view-containers', 'view-governance', 'view-mockups', 'view-wiki-governance', 'view-wiki-requirements', + ])) + const byId = Object.fromEntries(views.map((v) => [v.id, v])) + expect(byId['view-mockups'].kind).toBe('static') + expect(byId['view-wiki-governance'].kind).toBe('wiki') + const reqs = doc.nodes.filter((n) => n.type === 'requirement').map((n) => String(n.id)) + expect([...byId['view-wiki-requirements'].nodeIds].sort()).toEqual([...reqs].sort()) expect(views.some((v) => v.kind === 'dynamic' && !v.id.startsWith('view-flow-')), 'technical flow').toBe(true) for (const g of doc.nodes.filter((n) => n.type === 'group' && String(n.id).startsWith('flow-'))) { expect(ids, `view for ${g.id}`).toContain(`view-${g.id}`) diff --git a/tests/hubViewer.test.ts b/tests/hubViewer.test.ts index 8ddd555..53ddde7 100644 --- a/tests/hubViewer.test.ts +++ b/tests/hubViewer.test.ts @@ -12,6 +12,7 @@ import { describe, it, expect } from 'vitest' import { conceptToDiagramData, defaultViewKind, + kindForViewId, substituteTemplateDefaults, HUB_WIKI_VIEW_ID, HUB_TABLE_VIEW_ID, @@ -137,13 +138,14 @@ describe('hub route', () => { expect(parseHubHash(hash)).toEqual(r) }) - it('round-trips a named canvas view, and only on the canvas', () => { - const r = { browse: true, concept: 'bp-x', view: 'canvas' as const, canvasView: 'view-flow-shopper' } + it('round-trips a named view in any mode', () => { + const r = { browse: true, concept: 'bp-x', view: 'canvas' as const, namedView: 'view-flow-shopper' } const hash = formatHubHash(r) expect(hash).toBe('#/c/bp-x/v/canvas/cv/view-flow-shopper') expect(parseHubHash(hash)).toEqual(r) - expect(formatHubHash({ ...r, view: 'wiki' })).toBe('#/c/bp-x/v/wiki') - expect(parseHubHash('#/c/bp-x/v/wiki/cv/view-flow-shopper').canvasView).toBeUndefined() + const wiki = { ...r, view: 'wiki' as const, namedView: 'view-wiki-governance' } + expect(parseHubHash(formatHubHash(wiki))).toEqual(wiki) + expect(formatHubHash({ concept: 'bp-x', namedView: 'v' })).toBe('#/c/bp-x') }) it('omits view without a concept and ignores unknown view kinds', () => { @@ -182,14 +184,19 @@ describe('concept views', () => { views: [ { id: 'v-flow', name: 'Write flow', kind: 'dynamic', sequenceId: 's1', nodeIds: ['sys', 'cmd'], positions: { sys: { x: 5, y: 6, width: 360, height: 260 }, cmd: { x: 1, y: 2 } } }, - { id: 'v-wiki', name: 'Not a canvas view', kind: 'wiki', nodeIds: ['sys'] }, + { id: 'v-wiki', name: 'Docs', kind: 'wiki', nodeIds: ['sys'], wikiFocusId: 'sys' }, + { id: 'v-matrix', name: 'Unsupported kind', kind: 'matrix', nodeIds: ['sys'] }, { id: 'v-empty', name: 'Only ghosts', kind: 'static', nodeIds: ['ghost'] }, ], } - it('shows the concept\'s canvas views in the viewer, before wiki / table, with complete positions only', () => { + it('shows the concept\'s canvas and wiki views in the viewer, before the synthetic wiki / table, with complete positions only', () => { const views = conceptToDiagramData(withViews).views! - expect(views.map((v) => v.id)).toEqual(['v-flow', HUB_WIKI_VIEW_ID, HUB_TABLE_VIEW_ID]) + expect(views.map((v) => v.id)).toEqual(['v-flow', 'v-wiki', HUB_WIKI_VIEW_ID, HUB_TABLE_VIEW_ID]) + expect(views[1]).toMatchObject({ kind: 'wiki', wikiFocusId: 'sys', nodeIds: ['sys'] }) + const byId = Object.fromEntries(views.map((v) => [v.id, v])) + expect(kindForViewId('v-wiki', byId)).toBe('wiki') + expect(kindForViewId('v-flow', byId)).toBe('canvas') expect(views[0]).toMatchObject({ kind: 'dynamic', sequenceId: 's1', nodeIds: ['sys', 'cmd'] }) expect(views[0].positions).toEqual({ sys: { x: 5, y: 6, width: 360, height: 260 } }) }) @@ -203,6 +210,7 @@ describe('concept views', () => { ) expect(views).toEqual([ { id: 'id-1', name: 'CQRS — Write flow', kind: 'static', nodeIds: ['new-sys'], positions: {} }, + { id: 'id-2', name: 'CQRS — Docs', kind: 'wiki', nodeIds: ['new-sys'], positions: {}, wikiFocusId: 'new-sys' }, ]) }) }) diff --git a/website/manual.html b/website/manual.html index 63ea44f..318ccc5 100644 --- a/website/manual.html +++ b/website/manual.html @@ -4,7 +4,7 @@ Docs — Radical.Tools - + @@ -49,6 +49,7 @@
  • Elements
  • Relations
  • Properties & governance
  • +
  • Mockups & screen flows
  • Working with selections
  • @@ -78,6 +79,7 @@
  • Metamodel editor
  • Concept Hub
  • AI assistant
  • +
  • Radical Forge
  • @@ -229,6 +231,32 @@

    ADRs and fitness functions

    +
    +

    Mockups & screen flows

    +
    + The Shopper journey screen flow from the E-commerce blueprint: mockups with low-fi wireframes linked by labelled navigates-to relations; the right panel shows the selected Checkout mockup with its wireframe + The Shopper journey screen flow from the E-commerce blueprint: mockups with low-fi wireframes linked by labelled navigates-to relations; the right panel shows the selected Checkout mockup with its wireframe +
    A screen flow: each mockup carries a low-fi wireframe, and Navigates to relations are labelled with the action that moves the user on.
    +
    +

    + A Mockup (palette group UX) is one screen of the product. Beside label and description it has a Screen / route and an optional Design link to an external design (Figma, Penpot, …). Link it to the rest of the model with three relations: +

    +
      +
    • Illustrates — mockup → the requirements and scenarios the screen shows.
    • +
    • Presented by — mockup → the web app or container that renders it.
    • +
    • Navigates to — mockup → mockup, labelled with the triggering action (Pay, Add to cart). Chains of these are screen flows.
    • +
    +

    Wireframes

    +

    + The Wireframe section of a mockup's properties (and of its Wiki page) shows a low-fi wireframe. With AI enabled, Generate wireframe drafts one from the model around the screen: its description and route, the EARS requirements and Gherkin scenarios it illustrates, the container that presents it and the screens it navigates to. Regenerate and Remove do what they say; Open design follows the design link. Wireframes appear as thumbnails on the canvas and on Wiki overview cards. +

    +

    A wireframe is plain SVG stored on the element. Scripts, event handlers and external references are stripped, and it is only ever displayed as an image, so it can never run code or load anything.

    +

    Screen flows

    +

    + Group the mockups of one actor's journey in a Group (“Shopper journey”) and link the steps with Navigates to, including branches and returns (Payment → Payment declined → try again). Record the steps as a sequence to replay the flow screen by screen in a Flow view or in Presenter. +

    +
    +

    Working with selections

    @@ -441,7 +469,7 @@

    Concept Hub

    • Templates — concepts with parameters (a latency budget, an endpoint, a data class) ask for values on import; the Hub template section in the element's properties lets you change them later.
    • -
    • Blueprints — pick which elements and which referenced concepts to bring in.
    • +
    • Blueprints — pick which elements and which referenced concepts to bring in. Every blueprint ships one screen flow per actor (mockups with wireframes) and named views: on the canvas System context, Containers, Governance map, Mockups (every screen flow with its wireframes), its technical flow and one Flow view per screen flow; in the wiki Governance and Requirements. In the Hub, the view selector next to Canvas / Wiki / Table lists the views of the current mode; on import they arrive as views of your model, limited to the elements you picked.
    • Drop target — with a compatible container selected, imported elements become its children.
    @@ -466,6 +494,17 @@

    AI assistant

    +
    +

    Radical Forge

    +

    + Radical Forge (Start with Radical Forge on the welcome screen, or Radical menu → Radical Forge…) turns a plain-language description of a system into a model, one reviewable stage at a time: Requirements (EARS) → Fitness functions → Gherkin scenarios → Mockups → C4 model. Each stage may first ask a few clarifying questions, suggests matching Hub concepts, and can be regenerated before you continue. +

    +
      +
    • Mockups come before C4: the stage creates the user-facing screens with Illustrates and Navigates to links, then Generate wireframes draws a wireframe for every mockup that has none. The C4 stage then designs the front-ends the screens need and links each mockup to its container with Presented by.
    • +
    • Finish here ends the run at any stage and keeps what has been generated. The final step summarises every stage (what was added, what was skipped) and exports the scenarios as .feature files; ← Back resumes where you stopped.
    • +
    +
    +
    @@ -502,7 +541,7 @@

    Deep links

    f element page (Wiki views) s active milestone p / sl presentation and 0-based slide while presenting d ls:<uuid> or fs:<path> — which model (local to this machine) -

    The Hub uses #/c/<conceptId>/v/canvas|wiki|table and studio.radical.tools/?hub=id1,id2 opens Studio with those concepts queued for import.

    +

    The Hub uses #/c/<conceptId>/v/canvas|wiki|table (plus /cv/<viewId> for one of a concept's named views) and studio.radical.tools/?hub=id1,id2 opens Studio with those concepts queued for import.

    diff --git a/website/screenshots/mockups-dark.png b/website/screenshots/mockups-dark.png new file mode 100644 index 0000000..8995500 Binary files /dev/null and b/website/screenshots/mockups-dark.png differ diff --git a/website/screenshots/mockups-light.png b/website/screenshots/mockups-light.png new file mode 100644 index 0000000..04e5403 Binary files /dev/null and b/website/screenshots/mockups-light.png differ