Registry as Archify cards on the model page; cards under model diagrams from registry descriptions
- model page: registry elements are rendered as Archify info cards (type colour dot, label, id · type, description, used-in chips that open the diagram), replacing the table - resolveRefs: a diagram without cards of its own gets one card per drawn shared element that has a description, so the registry text shows under every model diagram as it does on hand-written diagrams - unit tests for generated cards, own cards winning and no cards without refs; docs and changelog
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
- **Models:** a folder with a `main.archify` at its top is a model, a set of diagrams that belong together. Opening `main.archify` shows the model page: name and description, the overview diagram if the file has one, the diagrams grouped by folder (click to open), and the registry with where each element is used.
|
- **Models:** a folder with a `main.archify` at its top is a model, a set of diagrams that belong together. Opening `main.archify` shows the model page: name and description, the overview diagram if the file has one, the diagrams grouped by folder (click to open), and the registry with where each element is used.
|
||||||
- **Registry:** `*.archify-registry` files define shared elements once; diagrams draw them with `ref: <id>` plus position, and may override `label`, `sublabel` and `tag`. Hover, go-to-definition and completion on `ref`; unknown names, forbidden overrides, duplicate ids and broken registries in Problems. Registry edits re-render open diagrams.
|
- **Registry:** `*.archify-registry` files define shared elements once; diagrams draw them with `ref: <id>` plus position, and may override `label`, `sublabel` and `tag`. Hover, go-to-definition and completion on `ref`; unknown names, forbidden overrides, duplicate ids and broken registries in Problems. Registry edits re-render open diagrams.
|
||||||
|
- **Cards:** a model diagram without `cards` of its own gets Archify cards from the registry descriptions of the elements it draws; the model page shows the registry as the same cards, each with the diagrams it is used in.
|
||||||
- **Links:** `diagram:` on a registry element or a component points at the diagram that explains it; double-click the node (2D view) to open it.
|
- **Links:** `diagram:` on a registry element or a component points at the diagram that explains it; double-click the node (2D view) to open it.
|
||||||
- **New Model…:** right-click a folder and choose *Archify | New Model…* (also in *File → New File…* and the Command Palette) to start from one of three complete C4 examples: **C4 - Standard Web App**, **C4 - E-Shop**, **C4 - Event-Based Integration**. Each creates a model folder with `main.archify`, a registry and context, container and component diagrams, named after the folder you choose, and opens the model page.
|
- **New Model…:** right-click a folder and choose *Archify | New Model…* (also in *File → New File…* and the Command Palette) to start from one of three complete C4 examples: **C4 - Standard Web App**, **C4 - E-Shop**, **C4 - Event-Based Integration**. Each creates a model folder with `main.archify`, a registry and context, container and component diagrams, named after the folder you choose, and opens the model page.
|
||||||
- New commands *Archify | Open Model* (editor title, Command Palette and the Explorer menu of `.archify` and `.archify-registry` files); *New Diagram… → Model (main.archify)* template for an empty model; editor schema for registries and the new keys; example model in `examples/online-shop`.
|
- New commands *Archify | Open Model* (editor title, Command Palette and the Explorer menu of `.archify` and `.archify-registry` files); *New Diagram… → Model (main.archify)* template for an empty model; editor schema for registries and the new keys; example model in `examples/online-shop`.
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ online-shop/
|
|||||||
- **The model page.** Click `main.archify` and you get the model's page: its name and description, the overview diagram if the file has one, the diagrams of the model grouped by folder (click one to open it), and the registry with "used in N diagrams".
|
- **The model page.** Click `main.archify` and you get the model's page: its name and description, the overview diagram if the file has one, the diagrams of the model grouped by folder (click one to open it), and the registry with "used in N diagrams".
|
||||||
- **Shared elements.** A registry file defines an element once (`type`, `label`, `sublabel`, `brand`, `description`, …). A diagram draws it with `ref: api` plus `pos` and `size`; it may override `label`, `sublabel` and `tag`, and may still define unshared elements inline. Hover a `ref` for the description, F12 jumps to the registry, completion after `ref:` lists the elements, unknown names appear in Problems.
|
- **Shared elements.** A registry file defines an element once (`type`, `label`, `sublabel`, `brand`, `description`, …). A diagram draws it with `ref: api` plus `pos` and `size`; it may override `label`, `sublabel` and `tag`, and may still define unshared elements inline. Hover a `ref` for the description, F12 jumps to the registry, completion after `ref:` lists the elements, unknown names appear in Problems.
|
||||||
- **Links.** A registry element (or a component) with `diagram: 0300-components/api.archify` points at the diagram that explains it: double-click the node in the 2D view to open it.
|
- **Links.** A registry element (or a component) with `diagram: 0300-components/api.archify` points at the diagram that explains it: double-click the node in the 2D view to open it.
|
||||||
|
- **Cards.** A model diagram without `cards` of its own shows one card per shared element it draws, from the registry descriptions. The model page shows the registry as the same cards, each with the diagrams it is used in.
|
||||||
- **Starting one.** *Archify | New Model…* (right-click a folder) copies one of the shipped examples into a new folder, named as you choose: **C4 - Standard Web App** (single-page app, API, database, cache, storage, identity provider), **C4 - E-Shop** (web shop, API, worker, queue, payment and email providers) or **C4 - Event-Based Integration** (event bus, router, dead-letter queue, event store, consumers). Each has a registry and context, container and component diagrams. For an empty model, *Archify | New Diagram… → Model* creates just a `main.archify`.
|
- **Starting one.** *Archify | New Model…* (right-click a folder) copies one of the shipped examples into a new folder, named as you choose: **C4 - Standard Web App** (single-page app, API, database, cache, storage, identity provider), **C4 - E-Shop** (web shop, API, worker, queue, payment and email providers) or **C4 - Event-Based Integration** (event bus, router, dead-letter queue, event store, consumers). Each has a registry and context, container and component diagrams. For an empty model, *Archify | New Diagram… → Model* creates just a `main.archify`.
|
||||||
- **Commands.** *Archify | Open Model* opens the model page of the current file, from the editor title, the Command Palette or the Explorer menu of a `.archify` or `.archify-registry` file.
|
- **Commands.** *Archify | Open Model* opens the model page of the current file, from the editor title, the Command Palette or the Explorer menu of a `.archify` or `.archify-registry` file.
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ components:
|
|||||||
- Before Archify runs, the extension replaces each `ref` component with the registry element plus the diagram's own fields, drops `description` and `diagram`, and hands Archify an ordinary component.
|
- Before Archify runs, the extension replaces each `ref` component with the registry element plus the diagram's own fields, drops `description` and `diagram`, and hands Archify an ordinary component.
|
||||||
- Connections, boundaries and everything else stay as they are and use the ids as before.
|
- Connections, boundaries and everything else stay as they are and use the ids as before.
|
||||||
|
|
||||||
|
## Cards under a model diagram
|
||||||
|
|
||||||
|
Archify shows `cards` (a coloured dot, a title, a few lines) below a diagram. A diagram inside a model that draws shared elements and defines no `cards` of its own gets one card per drawn element that has a `description` in the registry, in drawing order, with the dot colour of the element's type. Write your own `cards:` in the diagram to replace them.
|
||||||
|
|
||||||
## Links between diagrams
|
## Links between diagrams
|
||||||
|
|
||||||
A diagram may point at another diagram from a component, whether shared or inline:
|
A diagram may point at another diagram from a component, whether shared or inline:
|
||||||
|
|||||||
@@ -55,6 +55,42 @@ export interface Registry {
|
|||||||
problems: ModelProblem[];
|
problems: ModelProblem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Archify card dot colour for a component type (the colours Archify gives the nodes). */
|
||||||
|
const DOT_BY_TYPE: Record<string, string> = {
|
||||||
|
frontend: 'cyan',
|
||||||
|
backend: 'emerald',
|
||||||
|
database: 'violet',
|
||||||
|
cloud: 'amber',
|
||||||
|
security: 'rose',
|
||||||
|
messagebus: 'orange',
|
||||||
|
external: 'slate',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function dotForType(type?: string): string {
|
||||||
|
return DOT_BY_TYPE[type ?? ''] ?? 'slate';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Card {
|
||||||
|
dot: string;
|
||||||
|
title: string;
|
||||||
|
items: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Archify info cards for registry elements: one per element that has a
|
||||||
|
* description, titled by its label. What a diagram gets below itself when
|
||||||
|
* it draws shared elements and defines no cards of its own.
|
||||||
|
*/
|
||||||
|
export function cardsFromElements(elements: Iterable<RegistryElement>): Card[] {
|
||||||
|
const cards: Card[] = [];
|
||||||
|
for (const element of elements) {
|
||||||
|
if (!element.description) continue;
|
||||||
|
const items = [element.description.trim()];
|
||||||
|
cards.push({ dot: dotForType(element.type), title: element.label ?? element.id, items });
|
||||||
|
}
|
||||||
|
return cards;
|
||||||
|
}
|
||||||
|
|
||||||
export function isMainFile(fileName: string): boolean {
|
export function isMainFile(fileName: string): boolean {
|
||||||
return (fileName.split(/[\\/]/).pop() ?? '') === MAIN_FILE;
|
return (fileName.split(/[\\/]/).pop() ?? '') === MAIN_FILE;
|
||||||
}
|
}
|
||||||
@@ -172,6 +208,8 @@ export interface ResolveOptions {
|
|||||||
* Expands `ref` components from the registry and strips what Archify does
|
* Expands `ref` components from the registry and strips what Archify does
|
||||||
* not know. Component order and indexes are kept, so Archify's diagnostics
|
* not know. Component order and indexes are kept, so Archify's diagnostics
|
||||||
* (JSON pointers) still point at the right component in the source file.
|
* (JSON pointers) still point at the right component in the source file.
|
||||||
|
* A diagram without `cards` of its own gets cards from the descriptions of
|
||||||
|
* the shared elements it draws, so the registry text shows under the diagram.
|
||||||
*/
|
*/
|
||||||
export function resolveRefs(doc: unknown, options: ResolveOptions): ResolvedDiagram {
|
export function resolveRefs(doc: unknown, options: ResolveOptions): ResolvedDiagram {
|
||||||
const problems: ModelProblem[] = [];
|
const problems: ModelProblem[] = [];
|
||||||
@@ -229,6 +267,13 @@ export function resolveRefs(doc: unknown, options: ResolveOptions): ResolvedDiag
|
|||||||
if (link) links.set(id, link);
|
if (link) links.set(id, link);
|
||||||
return merged;
|
return merged;
|
||||||
});
|
});
|
||||||
|
if (diagram.cards === undefined && refs.size) {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const elements: RegistryElement[] = [];
|
||||||
|
for (const element of refs.values()) if (!seen.has(element.id)) { seen.add(element.id); elements.push(element); }
|
||||||
|
const cards = cardsFromElements(elements);
|
||||||
|
if (cards.length) diagram.cards = cards;
|
||||||
|
}
|
||||||
return { diagram, problems, refs, links, used };
|
return { diagram, problems, refs, links, used };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+33
-17
@@ -183,6 +183,9 @@ function bridgeScript(options: PageOptions): string {
|
|||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
var DOT_BY_TYPE = { frontend: 'cyan', backend: 'emerald', database: 'violet', cloud: 'amber', security: 'rose', messagebus: 'orange', external: 'slate' };
|
||||||
|
function dotForType(type) { return DOT_BY_TYPE[type || ''] || 'slate'; }
|
||||||
|
|
||||||
function el(tag, className, text) {
|
function el(tag, className, text) {
|
||||||
var node = document.createElement(tag);
|
var node = document.createElement(tag);
|
||||||
if (className) node.className = className;
|
if (className) node.className = className;
|
||||||
@@ -223,30 +226,34 @@ function bridgeScript(options: PageOptions): string {
|
|||||||
});
|
});
|
||||||
body.appendChild(list);
|
body.appendChild(list);
|
||||||
});
|
});
|
||||||
|
// Registry elements as Archify info cards: the same cards a diagram shows below itself.
|
||||||
if (model.registry.length) {
|
if (model.registry.length) {
|
||||||
body.appendChild(el('h2', 'archify-model-title', 'Registry · ' + model.registry.length));
|
body.appendChild(el('h2', 'archify-model-title', 'Registry · ' + model.registry.length));
|
||||||
var table = el('table', 'archify-model-registry');
|
var cards = el('div', 'cards archify-model-cards');
|
||||||
model.registry.forEach(function (element) {
|
model.registry.forEach(function (element) {
|
||||||
var row = el('tr');
|
var card = el('div', 'card archify-model-card');
|
||||||
var name = el('td', 'archify-model-element');
|
var header = el('div', 'card-header');
|
||||||
name.appendChild(el('strong', '', element.label || element.id));
|
header.appendChild(el('div', 'card-dot ' + dotForType(element.type)));
|
||||||
name.appendChild(el('span', 'archify-model-item-meta', element.id + (element.type ? ' · ' + element.type : '')));
|
header.appendChild(el('h3', '', element.label || element.id));
|
||||||
row.appendChild(name);
|
card.appendChild(header);
|
||||||
row.appendChild(el('td', 'archify-model-element-description', element.description || ''));
|
var list = el('ul');
|
||||||
var used = el('td', 'archify-model-used');
|
list.appendChild(el('li', 'archify-model-card-id', element.id + (element.type ? ' · ' + element.type : '')));
|
||||||
|
if (element.description) list.appendChild(el('li', '', element.description.trim()));
|
||||||
|
var used = el('li', 'archify-model-card-used');
|
||||||
if (element.usedIn.length) {
|
if (element.usedIn.length) {
|
||||||
element.usedIn.forEach(function (path) {
|
element.usedIn.forEach(function (path) {
|
||||||
var link = el('button', 'archify-model-used-link', path.replace(/\.archify$/, ''));
|
var link = el('button', 'archify-model-used-link', path.replace(/\.archify$/, ''));
|
||||||
link.type = 'button';
|
link.type = 'button';
|
||||||
link.title = path;
|
link.title = 'Open ' + path;
|
||||||
link.addEventListener('click', function () { post({ type: 'openDiagram', path: path }); });
|
link.addEventListener('click', function () { post({ type: 'openDiagram', path: path }); });
|
||||||
used.appendChild(link);
|
used.appendChild(link);
|
||||||
});
|
});
|
||||||
} else used.appendChild(el('span', 'archify-model-item-meta', 'not used yet'));
|
} else used.appendChild(el('span', 'archify-model-item-meta', 'not used yet'));
|
||||||
row.appendChild(used);
|
list.appendChild(used);
|
||||||
table.appendChild(row);
|
card.appendChild(list);
|
||||||
|
cards.appendChild(card);
|
||||||
});
|
});
|
||||||
body.appendChild(table);
|
body.appendChild(cards);
|
||||||
}
|
}
|
||||||
if (anchor && anchor.parentNode) anchor.parentNode.insertBefore(body, anchor.nextSibling); else document.body.appendChild(body);
|
if (anchor && anchor.parentNode) anchor.parentNode.insertBefore(body, anchor.nextSibling); else document.body.appendChild(body);
|
||||||
}
|
}
|
||||||
@@ -290,11 +297,20 @@ const MODEL_STYLE = `
|
|||||||
.archify-model-item-title { font-weight: 600; font-size: 0.9375rem; }
|
.archify-model-item-title { font-weight: 600; font-size: 0.9375rem; }
|
||||||
.archify-model-item-meta { font-size: 0.75rem; color: var(--m-muted); font-family: ui-monospace, Menlo, monospace; }
|
.archify-model-item-meta { font-size: 0.75rem; color: var(--m-muted); font-family: ui-monospace, Menlo, monospace; }
|
||||||
.archify-model-empty { color: var(--m-muted); }
|
.archify-model-empty { color: var(--m-muted); }
|
||||||
.archify-model-registry { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
|
/* Registry cards: Archify's own .card look, with fallbacks for the page without a diagram. */
|
||||||
.archify-model-registry td { padding: 0.55rem 0.6rem; border-top: 1px solid var(--m-border); vertical-align: top; }
|
.archify-model { --m-frontend: var(--frontend-stroke, #22d3ee); --m-backend: var(--backend-stroke, #34d399); --m-database: var(--database-stroke, #a78bfa); --m-cloud: var(--cloud-stroke, #fbbf24); --m-security: var(--security-stroke, #fb7185); --m-messagebus: var(--messagebus-stroke, #fb923c); --m-external: var(--external-stroke, #94a3b8); }
|
||||||
.archify-model-element { width: 22%; } .archify-model-element strong { display: block; }
|
.archify-model-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr)); gap: 1rem; margin-top: 0.75rem; }
|
||||||
.archify-model-element-description { color: var(--m-muted); line-height: 1.45; }
|
.archify-model-card { background: var(--m-panel); border: 1px solid var(--m-border); border-radius: 0.75rem; padding: 1.125rem 1.25rem; }
|
||||||
.archify-model-used { width: 28%; display: flex; flex-wrap: wrap; gap: 0.3rem; }
|
.archify-model-card .card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.6rem; }
|
||||||
|
.archify-model-card .card-dot { flex: none; width: 10px; height: 10px; border-radius: 50%; }
|
||||||
|
.archify-model-card .card-dot.cyan { background: var(--m-frontend); } .archify-model-card .card-dot.emerald { background: var(--m-backend); } .archify-model-card .card-dot.violet { background: var(--m-database); } .archify-model-card .card-dot.amber { background: var(--m-cloud); } .archify-model-card .card-dot.rose { background: var(--m-security); } .archify-model-card .card-dot.orange { background: var(--m-messagebus); } .archify-model-card .card-dot.slate { background: var(--m-external); }
|
||||||
|
.archify-model-card h3 { margin: 0; font-size: 0.9375rem; font-weight: 600; line-height: 1.4; color: var(--m-text); }
|
||||||
|
.archify-model-card ul { list-style: none; margin: 0; padding: 0; color: var(--m-muted); font-size: 0.8125rem; line-height: 1.6; }
|
||||||
|
.archify-model-card li { margin-bottom: 0.3rem; }
|
||||||
|
.archify-model-card li.archify-model-card-id, .archify-model-card li.archify-model-card-used { padding-left: 0; }
|
||||||
|
.archify-model-card li.archify-model-card-id::before, .archify-model-card li.archify-model-card-used::before { display: none; }
|
||||||
|
.archify-model-card-id { font-family: ui-monospace, Menlo, monospace; font-size: 0.75rem; }
|
||||||
|
.archify-model-card-used { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.5rem; }
|
||||||
.archify-model-used-link { font: inherit; font-size: 0.75rem; padding: 0.15rem 0.5rem; border-radius: 999px; border: 1px solid var(--m-border); background: transparent; color: var(--m-text); cursor: pointer; font-family: ui-monospace, Menlo, monospace; }
|
.archify-model-used-link { font: inherit; font-size: 0.75rem; padding: 0.15rem 0.5rem; border-radius: 999px; border: 1px solid var(--m-border); background: transparent; color: var(--m-text); cursor: pointer; font-family: ui-monospace, Menlo, monospace; }
|
||||||
.archify-model-used-link:hover { border-color: var(--m-accent); }
|
.archify-model-used-link:hover { border-color: var(--m-accent); }
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import fs from 'node:fs';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import {
|
import {
|
||||||
|
cardsFromElements,
|
||||||
|
dotForType,
|
||||||
groupOf,
|
groupOf,
|
||||||
hasDiagramBody,
|
hasDiagramBody,
|
||||||
isMainFile,
|
isMainFile,
|
||||||
@@ -81,6 +83,22 @@ test('resolve: refs become full components, links and usage are collected', () =
|
|||||||
assert.deepEqual([...r.used].sort(), ['api', 'cache', 'customer', 'db', 'queue', 'web', 'worker']);
|
assert.deepEqual([...r.used].sort(), ['api', 'cache', 'customer', 'db', 'queue', 'web', 'worker']);
|
||||||
assert.equal(r.refs.get(2)?.id, 'api');
|
assert.equal(r.refs.get(2)?.id, 'api');
|
||||||
assert.ok(!('model' in r.diagram));
|
assert.ok(!('model' in r.diagram));
|
||||||
|
// Cards from the registry descriptions, in drawing order, one per element.
|
||||||
|
const cards = r.diagram.cards as { dot: string; title: string; items: string[] }[];
|
||||||
|
assert.equal(cards.length, 7);
|
||||||
|
assert.deepEqual(cards[0], { dot: 'slate', title: 'Customer', items: ['Buys products; signs in with email or a social account.'] });
|
||||||
|
assert.equal(cards.find((c) => c.title === 'API')?.dot, 'emerald');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolve: own cards win, no cards without refs or descriptions', () => {
|
||||||
|
const registry = registryOfModel();
|
||||||
|
const own = resolveRefs({ components: [{ ref: 'api', pos: [0, 0] }], cards: [{ dot: 'rose', title: 'Mine', items: ['kept'] }] }, { registry, fileName: 'x.archify' });
|
||||||
|
assert.deepEqual(own.diagram.cards, [{ dot: 'rose', title: 'Mine', items: ['kept'] }]);
|
||||||
|
const inline = resolveRefs({ components: [{ id: 'a', type: 'backend', label: 'A', pos: [0, 0] }] }, { registry, fileName: 'x.archify' });
|
||||||
|
assert.ok(!('cards' in inline.diagram));
|
||||||
|
assert.equal(cardsFromElements([{ id: 'x', type: 'cloud', label: 'X', file: 'r', line: 0 }]).length, 0);
|
||||||
|
assert.equal(dotForType('messagebus'), 'orange');
|
||||||
|
assert.equal(dotForType(undefined), 'slate');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('resolve: overrides, second id for the same element, inline elements and links', () => {
|
test('resolve: overrides, second id for the same element, inline elements and links', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user