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:
2026-10-19 12:53:45 +03:00
parent 57914d435c
commit 38cdc7604f
6 changed files with 102 additions and 17 deletions
+45
View File
@@ -55,6 +55,42 @@ export interface Registry {
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 {
return (fileName.split(/[\\/]/).pop() ?? '') === MAIN_FILE;
}
@@ -172,6 +208,8 @@ export interface ResolveOptions {
* Expands `ref` components from the registry and strips what Archify does
* not know. Component order and indexes are kept, so Archify's diagnostics
* (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 {
const problems: ModelProblem[] = [];
@@ -229,6 +267,13 @@ export function resolveRefs(doc: unknown, options: ResolveOptions): ResolvedDiag
if (link) links.set(id, link);
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 };
}
+33 -17
View File
@@ -183,6 +183,9 @@ function bridgeScript(options: PageOptions): string {
}
}, 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) {
var node = document.createElement(tag);
if (className) node.className = className;
@@ -223,30 +226,34 @@ function bridgeScript(options: PageOptions): string {
});
body.appendChild(list);
});
// Registry elements as Archify info cards: the same cards a diagram shows below itself.
if (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) {
var row = el('tr');
var name = el('td', 'archify-model-element');
name.appendChild(el('strong', '', element.label || element.id));
name.appendChild(el('span', 'archify-model-item-meta', element.id + (element.type ? ' · ' + element.type : '')));
row.appendChild(name);
row.appendChild(el('td', 'archify-model-element-description', element.description || ''));
var used = el('td', 'archify-model-used');
var card = el('div', 'card archify-model-card');
var header = el('div', 'card-header');
header.appendChild(el('div', 'card-dot ' + dotForType(element.type)));
header.appendChild(el('h3', '', element.label || element.id));
card.appendChild(header);
var list = el('ul');
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) {
element.usedIn.forEach(function (path) {
var link = el('button', 'archify-model-used-link', path.replace(/\.archify$/, ''));
link.type = 'button';
link.title = path;
link.title = 'Open ' + path;
link.addEventListener('click', function () { post({ type: 'openDiagram', path: path }); });
used.appendChild(link);
});
} else used.appendChild(el('span', 'archify-model-item-meta', 'not used yet'));
row.appendChild(used);
table.appendChild(row);
list.appendChild(used);
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);
}
@@ -290,11 +297,20 @@ const MODEL_STYLE = `
.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-empty { color: var(--m-muted); }
.archify-model-registry { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.archify-model-registry td { padding: 0.55rem 0.6rem; border-top: 1px solid var(--m-border); vertical-align: top; }
.archify-model-element { width: 22%; } .archify-model-element strong { display: block; }
.archify-model-element-description { color: var(--m-muted); line-height: 1.45; }
.archify-model-used { width: 28%; display: flex; flex-wrap: wrap; gap: 0.3rem; }
/* Registry cards: Archify's own .card look, with fallbacks for the page without a diagram. */
.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-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr)); gap: 1rem; margin-top: 0.75rem; }
.archify-model-card { background: var(--m-panel); border: 1px solid var(--m-border); border-radius: 0.75rem; padding: 1.125rem 1.25rem; }
.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:hover { border-color: var(--m-accent); }
`;