0.9.2: Source view, schema checks without the YAML extension
Every Archify page can show its file's YAML in place. Diagrams get Diagram | Graph | Source (Graph only inside a model), main.archify Model | Graph | Source, registry and decision pages View | Source, and the Graph page a Source segment. 2D | 3D moves left of that switch and hides while Source shows. The YAML opens in a CodeMirror editor inside the page (dist/source.js, loaded on first use), themed with the page's colours. Edits go to the VS Code document as WorkspaceEdits; outside changes come back to the page. Undo and redo are the editor's own, since VS Code sends ⌘Z in a focused webview to the webview; save and find stay VS Code's. Problems come from the host only (what the Problems panel holds for the file) and are counted on the Source segment; completion and hover come from the schema, folded into one draft-07 document per diagram type. The host now checks .archify files against the extension's schema with ajv on the YAML as written, before archify validate, and checks .archify-registry files against the registry schema for the first time. Registry and decision pages become text editor providers. While Source shows, new renders wait and appear when the picture comes back. Show Source switches the page to Source; Open in Text Editor (the editor title button) opens the text beside it, as Show Source did. The decision schema is no longer given to the YAML extension. Fixed: YAML errors in registry and decision files were reported on line 1; "Webview is disposed" when a diagram closed mid-render; the model heading under the toolbar in narrow editors.
This commit is contained in:
1 parent
5bcd34b839
commit
27d4794ff8
32 files changed
+2486
-127
No files matched your search
+118
-34
@@ -28,6 +28,8 @@ export interface PageOptions {
|
||||
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 };
|
||||
/** Pages of a file that can be edited in place: the Source view's script, and the name of the page's own view. */
|
||||
source?: { scriptUri: string; left: 'Model' | 'Diagram' | 'View' };
|
||||
/** 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 };
|
||||
}
|
||||
@@ -93,6 +95,7 @@ function bridgeScript(options: PageOptions): string {
|
||||
decisions: options.decisions || [],
|
||||
descriptions: options.descriptions || {},
|
||||
graphSwitch: options.graphSwitch,
|
||||
source: options.source,
|
||||
graph: options.graph ? { graph: options.graph.graph, query: options.graph.query } : undefined,
|
||||
};
|
||||
return `(function () {
|
||||
@@ -676,35 +679,97 @@ 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.
|
||||
// What the page shows: Diagram | Graph | Source (Model | Graph | Source on
|
||||
// main.archify, View | Source on registry and decision pages). It sits
|
||||
// left of Export in Archify's toolbar, after 2D | 3D, which only says how
|
||||
// the picture is drawn; on our own pages it sits top right. Graph opens the
|
||||
// model's Graph page; Source puts the file's YAML in place of the picture.
|
||||
var sourceOn = false;
|
||||
var syncModeSwitch = function () {};
|
||||
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 src = config.source;
|
||||
if ((!sw && !src) || document.getElementById('archify-mode-switch')) return;
|
||||
var group = el('div', 'archify-mode-switch');
|
||||
group.id = 'archify-mode-switch';
|
||||
group.setAttribute('role', 'group');
|
||||
group.setAttribute('aria-label', 'View');
|
||||
function segment(label, title, action) {
|
||||
var button = el('button', 'archify-mode-seg', label);
|
||||
button.type = 'button';
|
||||
button.title = title;
|
||||
button.addEventListener('click', function (event) { event.stopPropagation(); action(); });
|
||||
group.appendChild(button);
|
||||
return button;
|
||||
}
|
||||
var name = (src && src.left) || sw.left;
|
||||
var left = segment(name, name === 'View' ? 'Show the page' : 'Show the ' + name.toLowerCase(), function () { setSource(false); });
|
||||
if (sw) segment('Graph', 'Open the model as one graph, with a query box', function () { post({ type: 'showGraph', query: sw.query }); });
|
||||
var source = src ? segment('Source', 'Edit the YAML of this file, checked as you type', function () { setSource(true); }) : null;
|
||||
if (source) {
|
||||
var badge = el('span', 'archify-source-badge');
|
||||
badge.id = 'archify-source-badge';
|
||||
badge.hidden = true;
|
||||
source.appendChild(badge);
|
||||
}
|
||||
syncModeSwitch = function () {
|
||||
left.classList.toggle('on', !sourceOn);
|
||||
left.setAttribute('aria-pressed', String(!sourceOn));
|
||||
if (source) {
|
||||
source.classList.toggle('on', sourceOn);
|
||||
source.setAttribute('aria-pressed', String(sourceOn));
|
||||
}
|
||||
};
|
||||
syncModeSwitch();
|
||||
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) {
|
||||
if (exportWrap && exportWrap.parentNode) exportWrap.parentNode.insertBefore(group, exportWrap);
|
||||
else {
|
||||
var row = el('div', 'archify-model-switch-row');
|
||||
row.appendChild(button);
|
||||
head.insertBefore(row, head.firstChild);
|
||||
} else document.body.appendChild(button);
|
||||
row.appendChild(group);
|
||||
if (head) head.insertBefore(row, head.firstChild);
|
||||
else document.body.insertBefore(row, document.body.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// The Source view lives in dist/source.js, loaded the first time it is chosen.
|
||||
function setSource(on) {
|
||||
if (!config.source || on === sourceOn) return;
|
||||
sourceOn = on;
|
||||
vscode.setState(Object.assign({}, vscode.getState() || {}, { sourceView: on }));
|
||||
syncModeSwitch();
|
||||
if (!on) {
|
||||
if (window.__archifySource) window.__archifySource.close();
|
||||
return;
|
||||
}
|
||||
if (window.__archifySource) {
|
||||
window.__archifySource.open();
|
||||
return;
|
||||
}
|
||||
var script = document.createElement('script');
|
||||
script.src = config.source.scriptUri;
|
||||
script.onload = function () { if (sourceOn && window.__archifySource) window.__archifySource.open(); };
|
||||
script.onerror = function () {
|
||||
sourceOn = false;
|
||||
syncModeSwitch();
|
||||
post({ type: 'error', message: 'The Source view could not be loaded.' });
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
function showProblemCount(count) {
|
||||
var badge = document.getElementById('archify-source-badge');
|
||||
if (!badge) return;
|
||||
badge.hidden = !count;
|
||||
badge.textContent = count ? String(count) : '';
|
||||
badge.title = count ? count + (count === 1 ? ' problem' : ' problems') + ' in the source' : '';
|
||||
}
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
var message = event.data || {};
|
||||
if (message.type === 'notice') showNotice(message.notice);
|
||||
if (message.type === 'sourceDiagnostics') showProblemCount(message.count);
|
||||
if (message.type === 'setView') setSource(message.view === 'source');
|
||||
});
|
||||
function onReady(fn) {
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
|
||||
@@ -717,6 +782,11 @@ function bridgeScript(options: PageOptions): string {
|
||||
onReady(renderDecisionsRail);
|
||||
onReady(describeElements);
|
||||
onReady(renderModeSwitch);
|
||||
if (config.source) {
|
||||
// The host answers with the problem count for the Source badge.
|
||||
post({ type: 'sourceReady' });
|
||||
if ((vscode.getState() || {}).sourceView) onReady(function () { setSource(true); });
|
||||
}
|
||||
if (config.notice) {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function () { showNotice(config.notice); });
|
||||
@@ -731,6 +801,8 @@ const MODEL_STYLE = `
|
||||
.archify-model { --m-text: var(--text, var(--vscode-foreground, CanvasText)); --m-muted: var(--text-muted, var(--vscode-descriptionForeground, GrayText)); --m-panel: var(--panel, var(--vscode-editorWidget-background, Canvas)); --m-border: var(--panel-border, var(--vscode-editorWidget-border, rgba(128,128,128,.35))); --m-accent: var(--frontend-stroke, var(--vscode-textLink-foreground));
|
||||
max-width: 1400px; margin: 0 auto; padding: 1.25rem 2rem; color: var(--m-text); font-family: var(--vscode-font-family, system-ui, sans-serif); }
|
||||
.archify-model-head { padding-top: 1.5rem; }
|
||||
/* Under Archify's fixed toolbar (diagram pages of main.archify), start below it. */
|
||||
.toolbar ~ .archify-model-head { padding-top: 4.25rem; }
|
||||
.archify-model-kicker { font-size: 0.6875rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--m-accent); }
|
||||
.archify-model-name { margin: 0.25rem 0 0.5rem; font-size: 1.5rem; font-weight: 700; letter-spacing: -0.02em; }
|
||||
.archify-model-description { margin: 0; max-width: 72ch; font-size: 0.9375rem; line-height: 1.55; color: var(--m-muted); white-space: pre-line; }
|
||||
@@ -815,15 +887,19 @@ const MODEL_STYLE = `
|
||||
|
||||
/* 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; }
|
||||
.archify-mode-switch { display: inline-flex; align-items: center; padding: 0 0.25rem; gap: 0; color: var(--text-muted, var(--vscode-descriptionForeground)); 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; }
|
||||
/* Anchored on the id: Archify's .toolbar button rules (one of them with :not(#btn-theme)) would win otherwise. */
|
||||
#archify-mode-switch .archify-mode-seg { position: relative; display: inline-flex; align-items: center; height: 1.75rem; padding: 0 0.7rem; border: 0; border-radius: 0.5rem; background: transparent; color: inherit; font: inherit; font-weight: 550; font-size: 0.8125rem; cursor: pointer; transition: background 0.15s, color 0.15s; }
|
||||
#archify-mode-switch .archify-mode-seg.on[aria-pressed="true"] { 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 .archify-mode-seg:hover:not(.on) { color: var(--text, var(--vscode-foreground)); }
|
||||
#archify-mode-switch .archify-mode-seg:focus-visible { outline: 2px solid var(--frontend-stroke, var(--vscode-focusBorder)); outline-offset: 1px; }
|
||||
.archify-source-badge { display: inline-block; min-width: 1rem; margin-left: 0.35rem; padding: 0 0.3rem; border-radius: 999px; background: var(--security-stroke, var(--vscode-editorError-foreground, #f14c4c)); color: var(--bg, #fff); font-size: 0.6875rem; font-weight: 700; line-height: 1rem; text-align: center; }
|
||||
.archify-source-badge[hidden] { display: none; }
|
||||
.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; }
|
||||
body > .archify-model-switch-row { position: relative; z-index: 2; margin: 1rem 1.25rem 0 1rem; }
|
||||
.archify-model-switch-row .archify-mode-switch { border: 1px solid var(--m-border, var(--vscode-editorWidget-border, rgba(128,128,128,.35))); border-radius: 0.75rem; background: var(--m-panel, var(--vscode-editorWidget-background)); padding: 0.1rem 0.25rem; }
|
||||
`;
|
||||
|
||||
/* Connectors rail panel: highlight of the hovered or selected edge in the SVG. */
|
||||
@@ -895,8 +971,8 @@ 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: {}, graphSwitch: { left: 'Model' } });
|
||||
export function modelPage(options: Pick<PageOptions, 'cspSource' | 'nonce' | 'state' | 'source'>, model: ModelPanel): string {
|
||||
const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, model, links: {}, graphSwitch: { left: 'Model' }, source: options.source });
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -935,8 +1011,8 @@ export function graphPage(options: Pick<PageOptions, 'cspSource' | 'nonce' | 'st
|
||||
}
|
||||
|
||||
/** The registry page for one .archify-registry file. */
|
||||
export function registryPage(options: Pick<PageOptions, 'cspSource' | 'nonce' | 'state'>, registry: RegistryPanel): string {
|
||||
const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, registry, links: {} });
|
||||
export function registryPage(options: Pick<PageOptions, 'cspSource' | 'nonce' | 'state' | 'source'>, registry: RegistryPanel): string {
|
||||
const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, registry, links: {}, source: options.source });
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -947,6 +1023,8 @@ export function registryPage(options: Pick<PageOptions, 'cspSource' | 'nonce' |
|
||||
<style>
|
||||
body { margin: 0; color: var(--vscode-foreground); background: var(--vscode-editor-background); }
|
||||
${MODEL_STYLE}
|
||||
${SWITCH_STYLE}
|
||||
${NOTICE_STYLE}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
@@ -954,8 +1032,8 @@ export function registryPage(options: Pick<PageOptions, 'cspSource' | 'nonce' |
|
||||
}
|
||||
|
||||
/** The decision page for one .archify-decision file. */
|
||||
export function decisionPage(options: Pick<PageOptions, 'cspSource' | 'nonce' | 'state'>, decision: DecisionPanel): string {
|
||||
const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, decision, links: {} });
|
||||
export function decisionPage(options: Pick<PageOptions, 'cspSource' | 'nonce' | 'state' | 'source'>, decision: DecisionPanel): string {
|
||||
const bridge = bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, decision, links: {}, source: options.source });
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -966,28 +1044,34 @@ export function decisionPage(options: Pick<PageOptions, 'cspSource' | 'nonce' |
|
||||
<style>
|
||||
body { margin: 0; color: var(--vscode-foreground); background: var(--vscode-editor-background); }
|
||||
${MODEL_STYLE}
|
||||
${SWITCH_STYLE}
|
||||
${NOTICE_STYLE}
|
||||
</style>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
/** A small VS Code-styled page for loading and error states. */
|
||||
/** A small VS Code-styled page for loading and error states; with `source`, the YAML can be fixed in place. */
|
||||
export function messagePage(
|
||||
options: Pick<PageOptions, 'cspSource' | 'nonce' | 'state'>,
|
||||
options: Pick<PageOptions, 'cspSource' | 'nonce' | 'state' | 'source'>,
|
||||
title: string,
|
||||
body: string,
|
||||
detail?: string,
|
||||
): string {
|
||||
const script = options.source
|
||||
? bridgeScript({ cspSource: options.cspSource, nonce: options.nonce, theme: undefined, state: options.state, links: {}, source: options.source })
|
||||
: `(function () { var vscode = acquireVsCodeApi(); var state = ${scriptJson(options.state)}; if (state) vscode.setState(state); })();`;
|
||||
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}">(function () { var vscode = acquireVsCodeApi(); var state = ${scriptJson(options.state)}; if (state) vscode.setState(state); })();</script>
|
||||
<script nonce="${options.nonce}">${script}</script>
|
||||
<style>
|
||||
body { font-family: var(--vscode-font-family); color: var(--vscode-foreground); background: var(--vscode-editor-background); padding: 24px 28px; }
|
||||
${options.source ? SWITCH_STYLE + NOTICE_STYLE : ''}
|
||||
h2 { font-weight: 600; font-size: 15px; margin: 0 0 8px; }
|
||||
p { margin: 0 0 12px; color: var(--vscode-descriptionForeground); }
|
||||
pre { font-family: var(--vscode-editor-font-family); font-size: 12px; white-space: pre-wrap; word-break: break-word;
|
||||
|
||||
Reference in new issue
Block a user