Model graph: the whole model as one graph with queries; 0.9.0

- Model | Graph switch on main.archify (Diagram | Graph on diagram pages, narrowed to that diagram)
- union graph of every element and connection, registry and inline, built from the model index
- query box: text, type:, diagram:, decision:, uses:, near:, from: to:, unused, shared, -shared
- side panel per element and connection with diagrams, registry line and decisions one click away
- legend chips, Fit, Layout, Labels, Export PNG; Cytoscape.js bundled as dist/graph.js
- command Archify | Open Model Graph; integration check; unit tests for graph and queries
This commit is contained in:
2026-10-21 15:47:00 +03:00
parent 265f8c57f6
commit c8645b228b
20 changed files with 1551 additions and 30 deletions
+72 -3
View File
@@ -26,8 +26,13 @@ export interface PageOptions {
decision?: DecisionPanel;
/** Diagram page: decisions of this diagram, for the Decisions rail panel. */
decisions?: DiagramDecision[];
/** Pages inside a model get a switch to the model's Graph page; `left` names the page they are on. */
graphSwitch?: { left: 'Model' | 'Diagram'; query?: string };
/** The Graph page: the model as one graph, the bundled script that draws it, and the query to start with. */
graph?: { graph: ModelGraph; query?: string; scriptUri: string };
}
import type { ModelGraph } from '../model/graph';
import type { DecisionPanel, ModelPanel, RegistryPanel } from '../model/panel';
import type { DiagramDecision } from '../model/prepare';
@@ -87,6 +92,8 @@ function bridgeScript(options: PageOptions): string {
decision: options.decision,
decisions: options.decisions || [],
descriptions: options.descriptions || {},
graphSwitch: options.graphSwitch,
graph: options.graph ? { graph: options.graph.graph, query: options.graph.query } : undefined,
};
return `(function () {
var config = ${scriptJson(config)};
@@ -110,7 +117,8 @@ function bridgeScript(options: PageOptions): string {
defaultView: config.defaultView || '2d',
post: post,
getState: function () { return vscode.getState() || {}; },
setState: function (patch) { vscode.setState(Object.assign({}, vscode.getState() || {}, patch)); }
setState: function (patch) { vscode.setState(Object.assign({}, vscode.getState() || {}, patch)); },
graph: config.graph
};
function saveBlobUrl(url, filename) {
@@ -668,6 +676,32 @@ function bridgeScript(options: PageOptions): string {
if (anchor && anchor.parentNode) anchor.parentNode.insertBefore(body, anchor.nextSibling); else document.body.appendChild(body);
}
// Model | Graph (or Diagram | Graph): Export's twin in Archify's toolbar,
// or at the top of the model page. Graph opens the model's Graph page.
function renderModeSwitch() {
var sw = config.graphSwitch;
if (!sw || document.getElementById('archify-mode-switch')) return;
var button = el('button', 'archify-mode-switch');
button.id = 'archify-mode-switch';
button.type = 'button';
button.title = 'Open the model as one graph, with a query box';
button.setAttribute('aria-label', sw.left + ' view; switch to the model graph');
button.appendChild(el('span', 'archify-mode-seg on', sw.left));
button.appendChild(el('span', 'archify-mode-seg', 'Graph'));
button.addEventListener('click', function (event) {
event.stopPropagation();
post({ type: 'showGraph', query: sw.query });
});
var exportWrap = document.querySelector('.toolbar .export-wrap');
var head = document.querySelector('.archify-model-head');
if (exportWrap && exportWrap.parentNode) exportWrap.parentNode.insertBefore(button, exportWrap);
else if (head) {
var row = el('div', 'archify-model-switch-row');
row.appendChild(button);
head.insertBefore(row, head.firstChild);
} else document.body.appendChild(button);
}
window.addEventListener('message', function (event) {
var message = event.data || {};
if (message.type === 'notice') showNotice(message.notice);
@@ -682,6 +716,7 @@ function bridgeScript(options: PageOptions): string {
onReady(renderConnectors);
onReady(renderDecisionsRail);
onReady(describeElements);
onReady(renderModeSwitch);
if (config.notice) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { showNotice(config.notice); });
@@ -778,6 +813,19 @@ const MODEL_STYLE = `
.archify-model-used-link:hover { border-color: var(--m-accent); }
`;
/* The Model | Graph switch: in Archify's toolbar it is the 2D | 3D switch's twin; on the model page it sits top right. */
const SWITCH_STYLE = `
.archify-mode-switch { display: inline-flex; align-items: center; padding: 0 0.25rem; gap: 0; border: 0; background: transparent; color: var(--text-muted, var(--vscode-descriptionForeground)); font: inherit; cursor: pointer; position: relative; }
.toolbar .archify-mode-switch::before { content: ""; position: absolute; inset: 0.25rem 0; border-radius: 0.6875rem; background: color-mix(in srgb, var(--panel) 78%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--panel-border) 85%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); backdrop-filter: blur(12px); }
.toolbar .archify-mode-switch:hover::before { box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 22%, transparent), 0 1px 2px rgba(15, 23, 42, 0.04); }
.archify-mode-seg { position: relative; display: inline-flex; align-items: center; height: 1.75rem; padding: 0 0.7rem; border-radius: 0.5rem; font-weight: 550; font-size: 0.8125rem; transition: background 0.15s, color 0.15s; }
.archify-mode-seg.on { color: var(--bg, var(--vscode-editor-background)); background: var(--text, var(--vscode-foreground)); box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12), 0 6px 16px -8px rgba(15, 23, 42, 0.4); }
.archify-mode-switch:hover .archify-mode-seg:not(.on) { color: var(--text, var(--vscode-foreground)); }
.archify-mode-switch:focus-visible { outline: 2px solid var(--frontend-stroke, var(--vscode-focusBorder)); outline-offset: 2px; border-radius: 0.75rem; }
.archify-model-switch-row { float: right; margin: 1.5rem 0 0 1rem; }
.archify-model-switch-row .archify-mode-switch { border: 1px solid var(--m-border); border-radius: 0.75rem; background: var(--m-panel); padding: 0.1rem 0.25rem; }
`;
/* Connectors rail panel: highlight of the hovered or selected edge in the SVG. */
const CONNECTOR_STYLE = `
.diagram-container svg [data-edge-key][data-archify-hit] path, .diagram-container svg [data-edge-key][data-archify-selected] path { stroke-width: 3px; filter: drop-shadow(0 0 3px currentColor); }
@@ -820,7 +868,7 @@ export function prepareDiagramHtml(html: string, options: PageOptions): string {
const head =
`<meta http-equiv="Content-Security-Policy" content="${escapeHtml(contentSecurityPolicy(options.cspSource, options.nonce))}">\n` +
`<script nonce="${options.nonce}">${bridgeScript(options)}</script>\n` +
`<style>${NOTICE_STYLE}${CONNECTOR_STYLE}${options.model ? MODEL_STYLE : ''}</style>\n`;
`<style>${NOTICE_STYLE}${CONNECTOR_STYLE}${SWITCH_STYLE}${options.model ? MODEL_STYLE : ''}</style>\n`;
let page = html
// Archify calls its node rail "Node index"; next to the registry it reads better as Elements.
@@ -848,7 +896,7 @@ export function prepareDiagramHtml(html: string, options: PageOptions): string {
/** The model page for a main.archify without a diagram of its own. */
export function modelPage(options: Pick<PageOptions, 'cspSource' | 'nonce' | 'state'>, model: ModelPanel): string {
const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, model, links: {} });
const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, model, links: {}, graphSwitch: { left: 'Model' } });
return `<!DOCTYPE html>
<html lang="en">
<head>
@@ -859,6 +907,27 @@ export function modelPage(options: Pick<PageOptions, 'cspSource' | 'nonce' | 'st
<style>
body { margin: 0; color: var(--vscode-foreground); background: var(--vscode-editor-background); }
${MODEL_STYLE}
${SWITCH_STYLE}
</style>
</head>
<body></body>
</html>`;
}
/** The Graph page of a model: the bundled script builds everything from the host's graph data. */
export function graphPage(options: Pick<PageOptions, 'cspSource' | 'nonce' | 'state'>, graph: NonNullable<PageOptions['graph']>): string {
const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, links: {}, graph });
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="${escapeHtml(contentSecurityPolicy(options.cspSource, options.nonce))}">
<script nonce="${options.nonce}">${bridge}</script>
<script nonce="${options.nonce}" defer src="${escapeHtml(graph.scriptUri)}"></script>
<style>
html, body { margin: 0; height: 100%; color: var(--vscode-foreground); background: var(--vscode-editor-background); }
${NOTICE_STYLE}
</style>
</head>
<body></body>