+
Semantic passport
@@ -5501,8 +5397,8 @@
hwp-cli Rust Workspace
- Route probe
- Choose a start node
+ Path
+ Click where the path starts
@@ -5510,8 +5406,8 @@ hwp-cli Rust Workspace
-
-
Pick two semantic nodes on the diagram
+
+ Click two nodes on the diagram
@@ -5522,7 +5418,7 @@
hwp-cli Rust Workspace
-
Choose the source, then the destination. Direction matters.
+
Click a start, then an end. Paths follow the arrows.
@@ -5554,18 +5450,24 @@
hwp-cli Rust Workspace
Click nodeDrag to pan
Semantic radar needs more MAP space.
+
-
+
+
+
+
@@ -5861,8 +5774,8 @@
MCP and trust
var MOTION_FPS = 30;
var SHARE_CARD_WIDTH = 1200;
var SHARE_CARD_HEIGHT = 630;
- var SHARE_CARD_PADDING = 36;
- var SHARE_CARD_HEADER = 112;
+ var SHARE_CARD_PADDING = 40;
+ var SHARE_CARD_HEADER = 124;
function exportError(key, values) {
var error = new Error(viewerText(key, values));
@@ -5911,26 +5824,6 @@
MCP and trust
clone.removeAttribute('data-route-journey');
clone.removeAttribute('data-share-route');
clone.removeAttribute('data-share-reach');
- clone.removeAttribute('data-story-active');
- clone.removeAttribute('data-story-playing');
- clone.removeAttribute('data-story-beat');
- clone.removeAttribute('data-story-next');
- clone.removeAttribute('data-story-follow');
- clone.removeAttribute('data-chapter-handoff');
- clone.removeAttribute('data-chapter-anchor');
- clone.removeAttribute('data-chapter-preview');
- Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-handoff-overlay]'), function (el) {
- el.remove();
- });
- Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-role]'), function (el) {
- el.removeAttribute('data-chapter-role');
- });
- Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-preview-role]'), function (el) {
- el.removeAttribute('data-chapter-preview-role');
- });
- Array.prototype.forEach.call(clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay]'), function (el) {
- el.remove();
- });
Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-overlay]'), function (el) {
el.remove();
});
@@ -5981,12 +5874,6 @@
MCP and trust
el.removeAttribute('data-source-evidence-count');
el.removeAttribute('data-source-evidence-original-label');
});
- Array.prototype.forEach.call(clone.querySelectorAll('[data-story-step], [data-story-beat-state], [data-story-beat-step]'), function (el) {
- el.removeAttribute('data-story-step');
- el.removeAttribute('data-story-beat-state');
- el.removeAttribute('data-story-beat-step');
- el.style.removeProperty('--story-step');
- });
Array.prototype.forEach.call(clone.querySelectorAll('[data-focus-match], [data-focus-selected]'), function (el) {
el.removeAttribute('data-focus-match');
el.removeAttribute('data-focus-selected');
@@ -6060,17 +5947,9 @@
MCP and trust
!clone.hasAttribute('data-route-journey') &&
!clone.hasAttribute('data-share-route') &&
!clone.hasAttribute('data-share-reach') &&
- !clone.hasAttribute('data-story-active') &&
- !clone.hasAttribute('data-story-playing') &&
- !clone.hasAttribute('data-story-beat') &&
- !clone.hasAttribute('data-story-next') &&
- !clone.hasAttribute('data-story-follow') &&
- !clone.hasAttribute('data-chapter-handoff') &&
- !clone.hasAttribute('data-chapter-anchor') &&
- !clone.hasAttribute('data-chapter-preview') &&
!clone.style.getPropertyValue('transform') &&
!clone.style.getPropertyValue('clip-path') &&
- clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay], [data-story-carrier-token], [data-story-step], [data-story-beat-state], [data-story-beat-step], [data-chapter-handoff-overlay], [data-chapter-role], [data-chapter-preview-role], [data-focus-match], [data-focus-selected], [data-reach-match], [data-reach-origin], [data-reach-depth], [data-semantic-lens-overlay], [data-lens-match], [data-lens-selected], [data-lens-peer], [data-legend-bridge], [data-legend-kind], [data-legend-bridge-runtime], [data-legend-count], [data-legend-zero], [data-legend-selected], [data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer], [data-relationship-hit-overlay], [data-relationship-pulse-overlay], [data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target], [data-intent-trace-overlay], [data-intent-trace-match], [data-intent-trace-selected], [data-route-probe-overlay], [data-route-journey-overlay], [data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current], [data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle], [data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth], [data-source-evidence-beacon], [data-source-evidence-count], [data-source-evidence-original-label], [data-detail], [data-detail-anchor]').length === 0;
+ clone.querySelectorAll('[data-focus-match], [data-focus-selected], [data-reach-match], [data-reach-origin], [data-reach-depth], [data-semantic-lens-overlay], [data-lens-match], [data-lens-selected], [data-lens-peer], [data-legend-bridge], [data-legend-kind], [data-legend-bridge-runtime], [data-legend-count], [data-legend-zero], [data-legend-selected], [data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer], [data-relationship-hit-overlay], [data-relationship-pulse-overlay], [data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target], [data-intent-trace-overlay], [data-intent-trace-match], [data-intent-trace-selected], [data-route-probe-overlay], [data-route-journey-overlay], [data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current], [data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle], [data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth], [data-source-evidence-beacon], [data-source-evidence-count], [data-source-evidence-original-label], [data-detail], [data-detail-anchor]').length === 0;
}
@@ -6083,10 +5962,12 @@
MCP and trust
* (PNG/JPEG/WebP/clipboard) because canvas rasterization needs
* deterministic colors; a raster cannot react to
* prefers-color-scheme after encoding.
- * - autoTheme=true — emits BOTH dark and light variable sets plus a
+ * - theme=auto — emits BOTH dark and light variable sets plus a
* `@media (prefers-color-scheme)` rule so the resulting SVG
* self-themes when embedded in GitHub READMEs or other hosts that
- * expose a color scheme. Used for "Download SVG".
+ * expose a color scheme. Used for the default "Download SVG".
+ * - theme=light|dark — locks the standalone SVG to the requested
+ * theme, independently of its source Viewer and host OS themes.
*/
function applyRouteSnapshot(clone, snapshot) {
if (!snapshot || !Array.isArray(snapshot.nodeIds) || !Array.isArray(snapshot.edges) ||
@@ -6251,11 +6132,15 @@
MCP and trust
}
function serializeSvg(scale, opts) {
- // scale: integer multiplier for intrinsic SVG pixel dimensions used by
+ // scale: multiplier (integer unless the figure is oversized) for intrinsic SVG pixel dimensions used by
// the raster path. Defaults to 1 (natural size) for SVG download.
scale = scale || 1;
opts = opts || {};
- var autoTheme = opts.autoTheme === true;
+ var requestedTheme = opts.theme || (opts.autoTheme === true ? 'auto' : null);
+ if (requestedTheme && requestedTheme !== 'auto' && requestedTheme !== 'light' && requestedTheme !== 'dark') {
+ throw new Error('Unsupported SVG theme: ' + requestedTheme);
+ }
+ var autoTheme = requestedTheme === 'auto';
var svg = document.querySelector('.diagram-container svg');
var clone = svg.cloneNode(true);
@@ -6276,6 +6161,10 @@
MCP and trust
// canvas upscaling.
clone.setAttribute('width', vb.width * scale);
clone.setAttribute('height', vb.height * scale);
+ // Keep copied Viewer layout rules from overriding the export's size.
+ clone.style.width = vb.width * scale + 'px';
+ clone.style.height = vb.height * scale + 'px';
+ clone.style.minWidth = '0';
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
// Only the SVG-relevant rules: semantic classes, markers, and the
@@ -6296,6 +6185,17 @@
MCP and trust
var sel = rule.selectorText || '';
if (/(^|,)\s*(svg|:root|\[data-theme|\[data-preset|\.c-|\.t-|\.a-|\.m-)/.test(sel)) {
out.push(rule.cssText);
+ } else if (opts.figure && sel.indexOf('.diagram-container > svg') !== -1) {
+ // Figures look like the Viewer canvas: keep its preset/theme paint
+ // (quiet grid, lifted nodes) by rescoping matching rules to svg.
+ var scoped = sel.split(',').map(function (part) {
+ var at = part.indexOf('.diagram-container > svg');
+ if (at === -1) return null;
+ var prefix = part.slice(0, at).trim();
+ try { if (prefix && !document.documentElement.matches(prefix)) return null; } catch (_) { return null; }
+ return 'svg' + part.slice(at + '.diagram-container > svg'.length);
+ }).filter(Boolean);
+ if (scoped.length) out.push(scoped.join(', ') + ' { ' + rule.style.cssText + ' }');
}
});
});
@@ -6324,9 +6224,12 @@
MCP and trust
document.body.appendChild(probe);
try {
var c = getComputedStyle(probe);
- return varNames.map(function (n) {
- return n + ': ' + c.getPropertyValue(n).trim() + ';';
- }).join(' ');
+ return {
+ background: c.getPropertyValue('--bg').trim(),
+ vars: varNames.map(function (n) {
+ return n + ': ' + c.getPropertyValue(n).trim() + ';';
+ }).join(' ')
+ };
} finally {
document.body.removeChild(probe);
}
@@ -6338,25 +6241,29 @@
MCP and trust
var style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
var bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
- bgRect.setAttribute('width', '100%');
- bgRect.setAttribute('height', '100%');
+ // Cover the real viewBox: auto-cropped canvases start below y=0, and a
+ // 100% rect anchored at the origin left an unpainted band at the bottom.
+ bgRect.setAttribute('x', vb.x);
+ bgRect.setAttribute('y', vb.y);
+ bgRect.setAttribute('width', vb.width);
+ bgRect.setAttribute('height', vb.height);
if (autoTheme) {
// Dual-theme SVG. Dark is the default (so hosts without
// prefers-color-scheme still render), light swaps in via media
// query, and svg[data-theme="..."] still lets downstream
// consumers force a specific theme.
- var darkVars = resolveVars('dark');
- var lightVars = resolveVars('light');
+ var darkTheme = resolveVars('dark');
+ var lightTheme = resolveVars('light');
style.textContent =
fontCss + "\n" +
"svg { font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, 'DejaVu Sans Mono', 'Liberation Mono', 'Noto Sans Mono CJK SC', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', monospace; }\n" +
hostStyle + "\n" +
- ":root, svg { " + darkVars + " }\n" +
- "@media (prefers-color-scheme: light) { :root, svg { " + lightVars + " } }\n" +
- "svg[data-theme=\"light\"] { " + lightVars + " }\n" +
- "svg[data-theme=\"dark\"] { " + darkVars + " }\n" +
+ ":root, svg { " + darkTheme.vars + " }\n" +
+ "@media (prefers-color-scheme: light) { :root, svg { " + lightTheme.vars + " } }\n" +
+ "svg[data-theme=\"light\"] { " + lightTheme.vars + " }\n" +
+ "svg[data-theme=\"dark\"] { " + darkTheme.vars + " }\n" +
"rect.c-bg-rect { fill: var(--bg); }\n";
// Don't lock the serialized SVG to the viewer's current theme.
@@ -6365,11 +6272,12 @@
MCP and trust
// swaps with the media query.
bgRect.setAttribute('class', 'c-bg-rect');
} else {
- // Raster path: lock to the viewer's current theme.
- var theme = document.documentElement.getAttribute('data-theme') || 'dark';
- var themeHost = document.querySelector('[data-theme="' + theme + '"]') || document.documentElement;
- var computed = getComputedStyle(themeHost);
- var vars = varNames.map(function (n) {
+ // Raster exports keep the viewer's current theme. Explicit SVG
+ // exports instead select their own stable light/dark source.
+ var lockedTheme = requestedTheme || document.documentElement.getAttribute('data-theme') || 'dark';
+ var resolvedTheme = requestedTheme && resolveVars(lockedTheme);
+ var computed = getComputedStyle(document.documentElement);
+ var vars = resolvedTheme ? resolvedTheme.vars : varNames.map(function (n) {
return n + ': ' + computed.getPropertyValue(n).trim() + ';';
}).join(' ');
@@ -6383,30 +6291,37 @@
MCP and trust
hostStyle + "\n" +
":root, svg { " + vars + " }\n";
- bgRect.setAttribute('fill', computed.getPropertyValue('--bg').trim() || '#ffffff');
+ if (requestedTheme) clone.setAttribute('data-theme', lockedTheme);
+ bgRect.setAttribute('fill', (resolvedTheme ? resolvedTheme.background : computed.getPropertyValue('--bg').trim()) || '#ffffff');
}
if (opts.routeSnapshot) {
- style.textContent += "\nsvg[data-share-route] [data-node-id], svg[data-share-route] [data-edge-from] { opacity: 0.18; }\n" +
- "svg[data-share-route] [data-share-route-match] { opacity: 1; }\n" +
+ style.textContent += "\nsvg[data-share-route] [data-node-id], svg[data-share-route] [data-edge-from], svg[data-share-route] [data-graph-role=\"automatic-crossover-underlay\"] { opacity: 0.18; }\n" +
+ "svg[data-share-route] [data-share-route-match], svg[data-share-route] [data-graph-role=\"automatic-crossover\"]:has(> [data-share-route-match]) > [data-graph-role=\"automatic-crossover-underlay\"] { opacity: 1; }\n" +
"svg[data-share-route] [data-share-route-start] > :is(rect, circle, polygon):not(.c-mask) { stroke-width: 3; stroke-dasharray: 5 3; }\n" +
"svg[data-share-route] [data-share-route-middle] > :is(rect, circle, polygon):not(.c-mask) { stroke-width: 2.2; }\n" +
"svg[data-share-route] [data-share-route-end] > :is(rect, circle, polygon):not(.c-mask) { stroke-width: 3.4; stroke-dasharray: 1 0; }\n";
}
if (opts.reachSnapshot) {
- style.textContent += "\nsvg[data-share-reach] [data-node-id], svg[data-share-reach] [data-edge-from] { opacity: 0.14; }\n" +
- "svg[data-share-reach] [data-share-reach-match] { opacity: 1; }\n" +
+ style.textContent += "\nsvg[data-share-reach] [data-node-id], svg[data-share-reach] [data-edge-from], svg[data-share-reach] [data-graph-role=\"automatic-crossover-underlay\"] { opacity: 0.14; }\n" +
+ "svg[data-share-reach] [data-share-reach-match], svg[data-share-reach] [data-graph-role=\"automatic-crossover\"]:has(> [data-share-reach-match]) > [data-graph-role=\"automatic-crossover-underlay\"] { opacity: 1; }\n" +
"svg[data-share-reach] [data-edge-from][data-share-reach-match] { stroke-width: 1.55; }\n" +
"svg[data-share-reach=\"upstream\"] [data-share-reach-origin] > :is(rect, circle, polygon):not(.c-mask) { stroke: var(--database-stroke); stroke-width: 3.4; stroke-dasharray: 5 3; }\n" +
"svg[data-share-reach=\"downstream\"] [data-share-reach-origin] > :is(rect, circle, polygon):not(.c-mask) { stroke: var(--backend-stroke); stroke-width: 3.4; stroke-dasharray: 1 0; }\n" +
"svg[data-preset=\"blueprint\"][data-share-reach] [data-share-reach-origin], svg[data-preset=\"blueprint\"][data-share-reach] [data-edge-from][data-share-reach-match] { filter: none; }\n";
}
+ // Figures draw the diagram on a painted canvas card, like the Viewer.
+ if (opts.figure) bgRect.setAttribute('fill', 'none');
+
clone.insertBefore(style, clone.firstChild);
clone.insertBefore(bgRect, style.nextSibling);
+ // The XML declaration pins UTF-8: without it, consumers that guess an
+ // encoding instead of defaulting to UTF-8 mangle non-ASCII text.
return {
- svgString: new XMLSerializer().serializeToString(clone),
+ svgString: '\n' +
+ new XMLSerializer().serializeToString(clone),
width: vb.width * scale,
height: vb.height * scale,
canonicalStateClean: canonicalStateClean,
@@ -6430,14 +6345,114 @@
MCP and trust
// silently produces a blank canvas above ~16 Mpx; Chrome / Firefox /
// desktop Safari are far higher but start failing on memory-constrained
// devices. We pick the largest integer scale in {4,3,2,1} whose target
- // pixel count fits under this cap.
+ // pixel count fits under this cap; a figure too large even at 1x is
+ // downscaled to fit rather than allocating an oversized canvas.
var MAX_CANVAS_PIXELS = 16 * 1024 * 1024;
function pickSafeScale(vbW, vbH) {
for (var s = RASTER_SCALE; s >= 1; s--) {
if (vbW * s * vbH * s <= MAX_CANVAS_PIXELS) return s;
}
- return 1;
+ return Math.sqrt(MAX_CANVAS_PIXELS / (vbW * vbH)) * 0.999;
+ }
+
+ // Raster exports reproduce the Viewer page without its controls: the
+ // title row with its accent dot, then the diagram on the same rounded
+ // canvas card (fill, hairline, shadow, dot field). CSS pixels.
+ var FIGURE_MARGIN = 28;
+ var FIGURE_CARD_PADDING = 24;
+ var FIGURE_TITLE_SIZE = 24;
+ var FIGURE_SUBTITLE_SIZE = 14;
+
+ function figureLayout(vb) {
+ var titleNode = document.querySelector('.header h1');
+ var subtitleNode = document.querySelector('.header .subtitle');
+ var container = document.querySelector('.diagram-container');
+ var root = getComputedStyle(document.documentElement);
+ var card = container ? getComputedStyle(container) : null;
+ var title = titleNode ? titleNode.textContent.trim() : '';
+ var subtitle = subtitleNode ? subtitleNode.textContent.trim() : '';
+ var header = title ? 32 + (subtitle ? 24 : 0) + 18 : 0;
+ var cardWidth = vb.width + FIGURE_CARD_PADDING * 2;
+ var cardHeight = vb.height + FIGURE_CARD_PADDING * 2;
+ var cardFill = card && card.backgroundColor && !/rgba\(0, 0, 0, 0\)|transparent/.test(card.backgroundColor)
+ ? card.backgroundColor : (root.getPropertyValue('--panel').trim() || 'transparent');
+ return {
+ title: title,
+ subtitle: subtitle,
+ family: titleNode ? getComputedStyle(titleNode).fontFamily : 'sans-serif',
+ text: root.getPropertyValue('--text').trim() || '#111827',
+ muted: root.getPropertyValue('--text-muted').trim() || '#64748b',
+ accent: root.getPropertyValue('--frontend-stroke').trim() || '#0891b2',
+ bg: root.getPropertyValue('--bg').trim() || currentBg(),
+ cardFill: cardFill,
+ cardBorder: root.getPropertyValue('--panel-border').trim() || 'transparent',
+ dot: card && /radial-gradient/.test(card.backgroundImage) ? (root.getPropertyValue('--canvas-dot').trim() || '') : '',
+ light: (document.documentElement.getAttribute('data-theme') || 'dark') === 'light',
+ header: header,
+ cardWidth: cardWidth,
+ cardHeight: cardHeight,
+ width: cardWidth + FIGURE_MARGIN * 2,
+ height: cardHeight + FIGURE_MARGIN * 2 + header
+ };
+ }
+
+ function paintFigure(ctx, layout) {
+ var m = FIGURE_MARGIN;
+ ctx.fillStyle = layout.bg;
+ ctx.fillRect(0, 0, layout.width, layout.height);
+ if (layout.title) {
+ var titleMid = m + 16;
+ ctx.fillStyle = layout.accent;
+ ctx.globalAlpha = 0.16;
+ ctx.beginPath(); ctx.arc(m + 5, titleMid, 9, 0, Math.PI * 2); ctx.fill();
+ ctx.globalAlpha = 1;
+ ctx.beginPath(); ctx.arc(m + 5, titleMid, 5, 0, Math.PI * 2); ctx.fill();
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = layout.text;
+ ctx.fillText(fitCanvasText(ctx, layout.title, layout.width - m * 2 - 22, FIGURE_TITLE_SIZE, 14, '650', layout.family), m + 22, titleMid + 1);
+ if (layout.subtitle) {
+ ctx.fillStyle = layout.muted;
+ ctx.fillText(fitCanvasText(ctx, layout.subtitle, layout.width - m * 2, FIGURE_SUBTITLE_SIZE, 11, '400', layout.family), m, titleMid + 30);
+ }
+ ctx.textBaseline = 'alphabetic';
+ }
+ var x = m, y = m + layout.header, w = layout.cardWidth, h = layout.cardHeight, r = 16;
+ function cardPath() {
+ ctx.beginPath();
+ if (typeof ctx.roundRect === 'function') ctx.roundRect(x, y, w, h, r);
+ else ctx.rect(x, y, w, h);
+ }
+ ctx.save();
+ if (layout.light) {
+ ctx.shadowColor = 'rgba(15, 23, 42, 0.08)';
+ ctx.shadowBlur = 28;
+ ctx.shadowOffsetY = 10;
+ }
+ cardPath();
+ ctx.fillStyle = layout.bg;
+ ctx.fill();
+ ctx.restore();
+ cardPath();
+ ctx.fillStyle = layout.cardFill;
+ ctx.fill();
+ if (layout.dot) {
+ ctx.save();
+ cardPath();
+ ctx.clip();
+ ctx.fillStyle = layout.dot;
+ for (var dy = y + 1; dy < y + h; dy += 20) {
+ for (var dx = x + 1; dx < x + w; dx += 20) {
+ ctx.beginPath(); ctx.arc(dx, dy, 1, 0, Math.PI * 2); ctx.fill();
+ }
+ }
+ ctx.restore();
+ }
+ cardPath();
+ ctx.strokeStyle = layout.cardBorder;
+ ctx.lineWidth = 1;
+ ctx.stroke();
+ return { x: x + FIGURE_CARD_PADDING, y: y + FIGURE_CARD_PADDING };
}
function rasterize(format) {
@@ -6447,8 +6462,9 @@
MCP and trust
// size — no upsampling blur.
var svg = document.querySelector('.diagram-container svg');
var vb = svg.viewBox.baseVal;
- var scale = pickSafeScale(vb.width, vb.height);
- var data = serializeSvg(scale);
+ var layout = figureLayout(vb);
+ var scale = pickSafeScale(layout.width, layout.height);
+ var data = serializeSvg(scale, { figure: true });
var svgBlob = new Blob([data.svgString], { type: 'image/svg+xml;charset=utf-8' });
var svgUrl = URL.createObjectURL(svgBlob);
@@ -6457,15 +6473,15 @@
MCP and trust
img.onload = function () {
try {
var canvas = document.createElement('canvas');
- canvas.width = data.width;
- canvas.height = data.height;
+ canvas.width = Math.round(layout.width * scale);
+ canvas.height = Math.round(layout.height * scale);
var ctx = canvas2dOrThrow(canvas, format);
- if (format === 'jpeg') {
- ctx.fillStyle = currentBg();
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- }
- // Draw at natural size — SVG was already rasterized at target res.
- ctx.drawImage(img, 0, 0);
+ // Paint in CSS pixels; the SVG image is already rendered at scale,
+ // so drawing it at CSS size under this transform stays 1:1.
+ ctx.setTransform(scale, 0, 0, scale, 0, 0);
+ var origin = paintFigure(ctx, layout);
+ ctx.drawImage(img, origin.x, origin.y, vb.width, vb.height);
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
URL.revokeObjectURL(svgUrl);
var mime = format === 'jpeg' ? 'image/jpeg' :
format === 'webp' ? 'image/webp' : 'image/png';
@@ -6487,10 +6503,10 @@
MCP and trust
});
}
- function fitCanvasText(ctx, text, maxWidth, startSize, minSize, weight) {
+ function fitCanvasText(ctx, text, maxWidth, startSize, minSize, weight, fontFamily) {
var value = String(text || '').trim();
var size = startSize;
- var family = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
+ var family = fontFamily || "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
while (size > minSize) {
ctx.font = (weight || '600') + ' ' + size + 'px ' + family;
if (ctx.measureText(value).width <= maxWidth) return value;
@@ -6516,14 +6532,13 @@
MCP and trust
}
function renderShareCard(options) {
- options = options || {};
var routeSnapshot = options.routeSnapshot || null;
var reachSnapshot = options.reachSnapshot || null;
if (routeSnapshot && reachSnapshot) return Promise.reject(exportError('viewer.export.error.variantsCombined'));
var svg = document.querySelector('.diagram-container svg');
var vb = svg.viewBox.baseVal;
var sourceScale = Math.min(2, pickSafeScale(vb.width, vb.height));
- var data = serializeSvg(sourceScale, { routeSnapshot: routeSnapshot, reachSnapshot: reachSnapshot });
+ var data = serializeSvg(sourceScale, { routeSnapshot: routeSnapshot, reachSnapshot: reachSnapshot, figure: true });
if (!data.canonicalStateClean) return Promise.reject(exportError('viewer.export.error.viewerState'));
if (routeSnapshot && !data.routeStateClean) return Promise.reject(exportError('viewer.export.error.routeState'));
if (reachSnapshot && !data.reachStateClean) return Promise.reject(exportError('viewer.export.error.reachState'));
@@ -6548,7 +6563,6 @@
MCP and trust
: '--frontend-stroke';
var accent = computed.getPropertyValue(accentProperty).trim() || '#22d3ee';
var titleNode = document.querySelector('.header h1');
- var subtitleNode = document.querySelector('.header .subtitle');
var title = titleNode ? titleNode.textContent : document.title;
var directionLabel = reachSnapshot
? viewerText('viewer.export.direction.' + reachSnapshot.direction)
@@ -6558,62 +6572,65 @@
MCP and trust
source: routeSnapshot.source.label,
target: routeSnapshot.target.label
})
- : reachSnapshot
- ? viewerText('viewer.export.card.reachSummary', {
- direction: directionLabel,
- origin: reachSnapshot.origin.label,
- nodes: viewerCount('viewer.export.card.node', reachSnapshot.nodeIds.length - 1),
- links: viewerCount('viewer.export.card.link', reachSnapshot.edges.length),
- hops: viewerCount('viewer.export.card.hop', reachSnapshot.maxDepth)
- })
- : subtitleNode ? subtitleNode.textContent : '';
- var preset = document.documentElement.getAttribute('data-preset') || 'classic';
- var theme = document.documentElement.getAttribute('data-theme') || 'dark';
- var presetKey = preset === 'signal-flow'
- ? 'viewer.preset.flow.short'
- : 'viewer.preset.' + preset;
- var presetLabel = viewerText(presetKey).toUpperCase();
- var themeLabel = viewerText('viewer.theme.' + theme).toUpperCase();
+ : viewerText('viewer.export.card.reachSummary', {
+ direction: directionLabel,
+ origin: reachSnapshot.origin.label,
+ nodes: viewerCount('viewer.export.card.node', reachSnapshot.nodeIds.length - 1),
+ links: viewerCount('viewer.export.card.link', reachSnapshot.edges.length),
+ hops: viewerCount('viewer.export.card.hop', reachSnapshot.maxDepth)
+ });
var cardLabel = routeSnapshot
? viewerText('viewer.export.card.routeBadge', {
hops: viewerCount('viewer.export.card.hop', routeSnapshot.hops).toUpperCase()
})
- : reachSnapshot
- ? viewerText('viewer.export.card.reachBadge', { direction: directionLabel.toUpperCase() })
- : viewerText('viewer.export.card.defaultBadge', { preset: presetLabel, theme: themeLabel });
+ : viewerText('viewer.export.card.reachBadge', { direction: directionLabel.toUpperCase() });
+
+ var family = titleNode ? getComputedStyle(titleNode).fontFamily : 'sans-serif';
+ var panelFill = bg;
ctx.fillStyle = bg;
ctx.fillRect(0, 0, SHARE_CARD_WIDTH, SHARE_CARD_HEIGHT);
+ // Header: a short accent rule and badge over the title and summary.
ctx.fillStyle = accent;
- ctx.fillRect(SHARE_CARD_PADDING, 27, 42, 3);
-
+ ctx.fillRect(SHARE_CARD_PADDING, 34, 28, 3);
ctx.textBaseline = 'alphabetic';
+ ctx.font = '600 12px ' + family;
+ ctx.textAlign = 'right';
+ ctx.fillStyle = muted;
+ ctx.fillText(cardLabel, SHARE_CARD_WIDTH - SHARE_CARD_PADDING, 40);
+ ctx.textAlign = 'left';
+
ctx.fillStyle = text;
- var fittedTitle = fitCanvasText(ctx, title, SHARE_CARD_WIDTH - SHARE_CARD_PADDING * 2 - 330, 29, 18, '700');
- ctx.fillText(fittedTitle, SHARE_CARD_PADDING, 62);
+ var fittedTitle = fitCanvasText(ctx, title, SHARE_CARD_WIDTH - SHARE_CARD_PADDING * 2 - 260, 30, 18, '650', family);
+ ctx.fillText(fittedTitle, SHARE_CARD_PADDING, 74);
ctx.fillStyle = muted;
- var fittedSubtitle = fitCanvasText(ctx, subtitle, SHARE_CARD_WIDTH - SHARE_CARD_PADDING * 2 - 280, 13, 11, '500');
- ctx.fillText(fittedSubtitle, SHARE_CARD_PADDING, 87);
-
- ctx.font = "600 12px 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
- ctx.textAlign = 'right';
- ctx.fillStyle = accent;
- ctx.fillText(cardLabel, SHARE_CARD_WIDTH - SHARE_CARD_PADDING, 50);
- ctx.textAlign = 'left';
+ var fittedSubtitle = fitCanvasText(ctx, subtitle, SHARE_CARD_WIDTH - SHARE_CARD_PADDING * 2, 15, 12, '400', family);
+ ctx.fillText(fittedSubtitle, SHARE_CARD_PADDING, 99);
+
+ // The diagram sits on one soft rounded panel with an even inset.
+ var panelX = SHARE_CARD_PADDING;
+ var panelY = SHARE_CARD_HEADER;
+ var panelWidth = SHARE_CARD_WIDTH - SHARE_CARD_PADDING * 2;
+ var panelHeight = SHARE_CARD_HEIGHT - SHARE_CARD_HEADER - SHARE_CARD_PADDING;
+ var inset = 18;
+ ctx.beginPath();
+ if (typeof ctx.roundRect === 'function') ctx.roundRect(panelX, panelY, panelWidth, panelHeight, 14);
+ else ctx.rect(panelX, panelY, panelWidth, panelHeight);
+ ctx.fillStyle = panelFill;
+ ctx.fill();
+ ctx.strokeStyle = border;
+ ctx.lineWidth = 1;
+ ctx.stroke();
- var availableWidth = SHARE_CARD_WIDTH - SHARE_CARD_PADDING * 2;
- var availableHeight = SHARE_CARD_HEIGHT - SHARE_CARD_HEADER - SHARE_CARD_PADDING;
+ var availableWidth = panelWidth - inset * 2;
+ var availableHeight = panelHeight - inset * 2;
var fit = Math.min(availableWidth / data.width, availableHeight / data.height);
var drawWidth = data.width * fit;
var drawHeight = data.height * fit;
- var drawX = SHARE_CARD_PADDING + (availableWidth - drawWidth) / 2;
- var drawY = SHARE_CARD_HEADER + (availableHeight - drawHeight) / 2;
-
- ctx.strokeStyle = border;
- ctx.lineWidth = 1;
- ctx.strokeRect(drawX - 0.5, drawY - 0.5, drawWidth + 1, drawHeight + 1);
+ var drawX = panelX + inset + (availableWidth - drawWidth) / 2;
+ var drawY = panelY + inset + (availableHeight - drawHeight) / 2;
ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
URL.revokeObjectURL(svgUrl);
@@ -6636,7 +6653,6 @@
MCP and trust
function rasterizeShareCard(options) {
options = options || {};
- if (!options.variant) return renderShareCard();
if (options.variant !== 'route' && options.variant !== 'reach') {
return Promise.reject(exportError('viewer.export.unknownVariant', { variant: options.variant }));
}
@@ -6949,8 +6965,7 @@
MCP and trust
// canvas.toBlob('image/webp') silently returns a PNG on browsers without
// WebP encoding (older Safari), so detect explicitly.
function supports(format) {
- if (format === 'share-card') return true;
- if (format === 'svg' || format === 'png') return true;
+ if (format === 'svg' || format === 'svg-light' || format === 'svg-dark' || format === 'png') return true;
if (format === 'webm') return canRecordMotion();
var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp';
try {
@@ -6966,7 +6981,7 @@
MCP and trust
it.disabled = true;
it.title = viewerText('viewer.export.unsupported');
}
- if ((it.dataset.action === 'copy' || it.dataset.action === 'copy-share-card') && !canCopyImage()) {
+ if (it.dataset.action === 'copy' && !canCopyImage()) {
it.disabled = true;
it.title = viewerText('viewer.export.clipboardUnsupported');
}
@@ -7056,21 +7071,21 @@
MCP and trust
});
function runExport(format) {
+ if (['svg', 'svg-light', 'svg-dark', 'png', 'jpeg', 'webp', 'webm'].indexOf(format) === -1) {
+ return Promise.reject(exportError('viewer.export.unsupported'));
+ }
var base = diagramFilename();
+ var svgTheme = format === 'svg' ? 'auto' :
+ format === 'svg-light' ? 'light' :
+ format === 'svg-dark' ? 'dark' : null;
close(true);
clearExportReceipt();
if (format === 'webm') toast(viewerText('viewer.export.recording'));
- return (format === 'share-card'
- ? rasterizeShareCard().then(function (blob) {
- recordExportReceipt('share-card', blob, true, { width: SHARE_CARD_WIDTH, height: SHARE_CARD_HEIGHT });
- download(blob, base + '-share-card.png');
- toast(viewerText('viewer.export.downloadedShare'));
- })
- : format === 'svg'
- ? Promise.resolve(serializeSvg(1, { autoTheme: true })).then(function (d) {
+ return (svgTheme
+ ? Promise.resolve(serializeSvg(1, { theme: svgTheme })).then(function (d) {
var blob = new Blob([d.svgString], { type: 'image/svg+xml;charset=utf-8' });
recordExportReceipt('svg', blob, d.canonicalStateClean);
- download(blob, base + '.svg');
+ download(blob, base + (svgTheme === 'auto' ? '' : '-' + svgTheme) + '.svg');
})
: format === 'webm'
? recordWebm().then(function (blob) {
@@ -7206,29 +7221,6 @@
MCP and trust
}
}
- function runCopyShareCard() {
- close(true);
- clearExportReceipt();
- if (!canCopyImage()) {
- alert(viewerText('viewer.export.clipboardUnsupported.period'));
- return;
- }
- var blobPromise = rasterizeShareCard();
- return writePngToClipboard(blobPromise).then(function () {
- return blobPromise.then(function (blob) {
- recordExportReceipt('share-card', blob, true, { width: SHARE_CARD_WIDTH, height: SHARE_CARD_HEIGHT });
- toast(viewerText('viewer.export.copiedShare'));
- });
- }).catch(function (err) {
- console.error(err);
- var technicalMessage = err && err.message ? err.message : 'share-card';
- var message = exportMessage(err);
- document.documentElement.setAttribute('data-last-export-error-format', 'share-card');
- document.documentElement.setAttribute('data-last-export-error', technicalMessage);
- alert(viewerText('viewer.export.copyFailed', { message: message }));
- });
- }
-
function runCopy() {
close(true);
clearExportReceipt();
@@ -7254,9 +7246,6 @@
MCP and trust
var reachShareCardBtn = e.target.closest('button[data-action="reach-share-card"]');
if (reachShareCardBtn && !reachShareCardBtn.disabled && !reachShareCardBtn.hidden) { runReachShareCard(); return; }
- var copyShareCardBtn = e.target.closest('button[data-action="copy-share-card"]');
- if (copyShareCardBtn && !copyShareCardBtn.disabled) { runCopyShareCard(); return; }
-
var copyBtn = e.target.closest('button[data-action="copy"]');
if (copyBtn && !copyBtn.disabled) { runCopy(); return; }
@@ -7274,8 +7263,7 @@
MCP and trust
downloadRouteShareCard: runRouteShareCard,
downloadReachShareCard: runReachShareCard,
syncRouteShare: syncRouteShareItem,
- syncReachShare: syncReachShareItem,
- copyShareCard: runCopyShareCard
+ syncReachShare: syncReachShareItem
};
// Auto-open on page load for demo/screenshot purposes: ?openExport=1
@@ -7296,6 +7284,7 @@
MCP and trust
} catch (_) {}
})();
+
/* ============================================================
Motion Governor — one reader switch and one motion budget. Ambient
trace yields to the strongest semantic owner; Still also parks bounded
@@ -7369,10 +7358,6 @@
MCP and trust
return readerPaused || reducedMotion() || hasSuspension();
}
function ownerLabel(value) {
- if (value === 'story') return viewerText('viewer.owner.story');
- if (value === 'chapter') return viewerText('viewer.owner.chapter');
- if (value === 'chapter-preview') return viewerText('viewer.owner.chapterPreview');
- if (value === 'handoff') return viewerText('viewer.owner.handoff');
if (value === 'route') return viewerText('viewer.owner.route');
if (value === 'lens') return viewerText('viewer.owner.lens');
if (value === 'relationship') return viewerText('viewer.owner.relationship');
@@ -7394,12 +7379,6 @@
MCP and trust
btn.setAttribute('aria-pressed', paused ? 'false' : 'true');
btn.disabled = systemPaused;
label.textContent = viewerText(paused ? 'viewer.motion.still' : 'viewer.motion.live');
- if (paused && lastEffectivePaused !== true && Archify.guidedViews && Archify.guidedViews.isPlaying()) {
- Archify.guidedViews.pause();
- }
- if (paused && lastEffectivePaused !== true && Archify.guidedViews && Archify.guidedViews.settleHandoff) {
- Archify.guidedViews.settleHandoff(systemPaused ? 'reduced-motion' : (hasSuspension() ? 'hidden' : 'still'));
- }
if (paused && lastEffectivePaused !== true && Archify.routeProbe && Archify.routeProbe.isJourneyPlaying && Archify.routeProbe.isJourneyPlaying()) {
Archify.routeProbe.pauseJourney({ preserveElapsed: true, reason: systemPaused ? 'reduced-motion' : (hasSuspension() ? 'hidden' : 'still') });
}
@@ -7438,8 +7417,6 @@
MCP and trust
}
function deriveOwner() {
if (!svg) return '';
- if (svg.hasAttribute('data-story-playing') || svg.hasAttribute('data-story-follow')) return 'story';
- if (svg.hasAttribute('data-story-active')) return 'chapter';
if (svg.hasAttribute('data-route-picking') || svg.hasAttribute('data-route-active')) return 'route';
if (svg.hasAttribute('data-lens-active')) return 'lens';
if (svg.hasAttribute('data-relationship-preview-active')) return 'relationship';
@@ -7533,7 +7510,7 @@
MCP and trust
ownerObserver.observe(svg, {
attributes: true,
attributeFilter: [
- 'data-story-playing', 'data-story-follow', 'data-story-active', 'data-route-picking', 'data-route-active',
+ 'data-route-picking', 'data-route-active',
'data-lens-active', 'data-relationship-preview-active', 'data-intent-trace-active',
'data-focus-active', 'data-legend-preview-active'
]
@@ -7595,23 +7572,37 @@
MCP and trust
beacon.classList.add('source-evidence-beacon');
beacon.setAttribute('data-source-evidence-beacon', '');
beacon.setAttribute('aria-hidden', 'true');
- var brandOffset = node.hasAttribute('data-node-brand') ? 24 : 0;
- beacon.setAttribute('transform', 'translate(' + (box.x + box.width - 35 - brandOffset) + ' ' + (box.y + 5) + ')');
+ var brandOffset = node.hasAttribute('data-node-brand') ? 22 : 0;
var title = document.createElementNS(svgNamespace, 'title');
title.textContent = label;
var plate = document.createElementNS(svgNamespace, 'rect');
- plate.setAttribute('width', '30');
- plate.setAttribute('height', '12');
- plate.setAttribute('rx', '6');
+ plate.setAttribute('height', '13');
+ plate.setAttribute('rx', '6.5');
var text = document.createElementNS(svgNamespace, 'text');
- text.setAttribute('x', '15');
- text.setAttribute('y', '8.25');
+ text.setAttribute('y', '9.4');
text.textContent = viewerText('viewer.passport.sourceMarker') + ' ' + count;
beacon.appendChild(title);
beacon.appendChild(plate);
beacon.appendChild(text);
node.appendChild(beacon);
+ // Measure the real glyph advance, then clamp the plate between 24
+ // and 36px; a still-wider label is squeezed with textLength so the
+ // badge never overlaps the neighbouring brand mark or sigil.
+ var textWidth = 16;
+ try {
+ var measured = text.getComputedTextLength();
+ if (Number.isFinite(measured) && measured > 0) textWidth = measured;
+ } catch (_) {}
+ var plateWidth = Math.min(36, Math.max(24, textWidth + 8));
+ if (textWidth > plateWidth - 8) {
+ text.setAttribute('textLength', String(plateWidth - 8));
+ text.setAttribute('lengthAdjust', 'spacingAndGlyphs');
+ }
+ plate.setAttribute('width', String(plateWidth));
+ text.setAttribute('x', String(plateWidth / 2));
+ beacon.setAttribute('transform', 'translate(' + (box.x + box.width - 6 - brandOffset - plateWidth) + ' ' + (box.y + 4.5) + ')');
+
var originalLabel = node.getAttribute('aria-label') || '';
node.setAttribute('data-source-evidence-count', String(count));
node.setAttribute('data-source-evidence-original-label', originalLabel);
@@ -7662,6 +7653,7 @@
MCP and trust
var copyBtn = document.getElementById('btn-focus-copy');
var relationsBtn = document.getElementById('btn-focus-relations');
var clearBtn = document.getElementById('btn-focus-clear');
+ var moveBtn = document.getElementById('btn-focus-move');
var activeIds = [];
var hoveredRelationship = null;
var focusedRelationship = null;
@@ -7671,6 +7663,9 @@
MCP and trust
var relationshipHitOverlay = null;
var relationshipHitTargets = [];
var directPreviewTimer = null;
+ var lensDrag = null;
+ var lensDragClickPointer = null;
+ var manualLensPosition = null;
var reachabilityMode = null;
var activeReachability = null;
var svgNamespace = 'http://www.w3.org/2000/svg';
@@ -7809,9 +7804,6 @@
MCP and trust
}
var result = reachabilityFor(activeIds[0], direction);
if (!result || result.nodeIds.length <= 1) return false;
- if (Archify.guidedViews && typeof Archify.guidedViews.showAll === 'function') {
- Archify.guidedViews.showAll({ clearFocus: false, updateUrl: false, resetView: false });
- }
clearRelationshipPreview({ clearPin: true });
clearReachability({ updateUrl: false });
reachabilityMode = direction;
@@ -7865,7 +7857,7 @@
MCP and trust
if (options.reveal !== false && Archify.view && typeof Archify.view.reveal === 'function') {
Archify.view.reveal(result.nodeIds, { includeNeighbors: false, reason: 'reachability' });
}
- requestLensPlacement();
+ placeRelationshipLens();
return true;
}
function reachabilitySnapshot() {
@@ -8056,8 +8048,8 @@
MCP and trust
}
function relationshipHitRecords() {
var recordsByKey = {};
- var recordsById = {};
- var byId = {};
+ var recordsById = Object.create(null);
+ var byId = Object.create(null);
nodes().forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; });
var records = [];
edges().forEach(function (edge) {
@@ -8345,7 +8337,7 @@
MCP and trust
target.removeAttribute('data-preview-active');
});
renderRelationshipCopyAction();
- requestLensPlacement();
+ placeRelationshipLens();
}
function previewRelationship(button, options) {
options = options || {};
@@ -8372,7 +8364,7 @@
MCP and trust
if (!chip.hidden && options.direct !== true) chip.setAttribute('data-relationship-previewing', 'true');
activeRelationshipPreview = button;
renderRelationshipPulse(key);
- requestLensPlacement();
+ placeRelationshipLens();
}
function syncRelationshipPreview() {
var next = pinnedRelationship || focusedRelationship || hoveredRelationship;
@@ -8384,11 +8376,9 @@
MCP and trust
html.getAttribute('data-guide-open') === 'true' ||
container.classList.contains('is-panning') ||
(activeIds.length > 0 && !pinnedRelationshipKey) ||
- svg.hasAttribute('data-story-active') ||
svg.hasAttribute('data-route-picking') ||
svg.hasAttribute('data-route-active') ||
- svg.hasAttribute('data-lens-active') ||
- svg.hasAttribute('data-chapter-preview');
+ svg.hasAttribute('data-lens-active');
}
function scheduleDirectRelationshipPreview(target) {
if (directPreviewTimer) window.clearTimeout(directPreviewTimer);
@@ -8427,9 +8417,6 @@
MCP and trust
}
var record = relationshipRecordForKey(key);
if (!record) return false;
- if (Archify.guidedViews && typeof Archify.guidedViews.showAll === 'function') {
- Archify.guidedViews.showAll({ clearFocus: false, updateUrl: false });
- }
set(record.from, { toggle: false, updateUrl: false });
var row = Array.prototype.slice.call(relationshipList.querySelectorAll('[data-relationship-key]')).find(function (candidate) {
return candidate.getAttribute('data-relationship-key') === key;
@@ -8651,9 +8638,148 @@
MCP and trust
relationshipList.appendChild(section);
});
}
+ function lensPlacementBounds() {
+ var containerRect = container.getBoundingClientRect();
+ if (containerRect.width <= 0 || containerRect.height <= 0 || chip.offsetWidth <= 0 || chip.offsetHeight <= 0) return null;
+ var padding = window.innerWidth <= 720 ? 8 : 16;
+ var visibleLeft = Math.max(padding, -containerRect.left + padding);
+ var visibleRight = Math.min(
+ container.clientWidth - padding,
+ window.innerWidth - containerRect.left - padding
+ );
+ var visibleTop = Math.max(padding, -containerRect.top + padding);
+ var visibleBottom = Math.min(
+ container.clientHeight - padding,
+ window.innerHeight - containerRect.top - padding
+ );
+ var minLeft = visibleLeft;
+ var minTop = Math.min(visibleTop, Math.max(padding, visibleBottom - chip.offsetHeight));
+ return {
+ containerRect: containerRect,
+ minLeft: minLeft,
+ maxLeft: Math.max(minLeft, visibleRight - chip.offsetWidth),
+ minTop: minTop,
+ maxTop: Math.max(minTop, visibleBottom - chip.offsetHeight)
+ };
+ }
+ function manualLensPlacementAvailable() {
+ return window.innerWidth > 720 && (!finePointerQuery || finePointerQuery.matches);
+ }
+ function clampLensPosition(position, bounds) {
+ if (!position || !bounds) return null;
+ return {
+ left: Math.max(bounds.minLeft, Math.min(bounds.maxLeft, position.left)),
+ top: Math.max(bounds.minTop, Math.min(bounds.maxTop, position.top))
+ };
+ }
+ function currentLensPosition() {
+ var bounds = lensPlacementBounds();
+ if (!bounds) return null;
+ var rect = chip.getBoundingClientRect();
+ return clampLensPosition({
+ left: rect.left - bounds.containerRect.left,
+ top: rect.top - bounds.containerRect.top
+ }, bounds);
+ }
+ function applyManualLensPosition(position) {
+ if (!manualLensPlacementAvailable()) return false;
+ var bounds = lensPlacementBounds();
+ var clamped = clampLensPosition(position, bounds);
+ if (!clamped) return false;
+ manualLensPosition = clamped;
+ chip.setAttribute('data-manual-placement', 'true');
+ chip.style.left = Math.round(clamped.left) + 'px';
+ chip.style.top = Math.round(clamped.top) + 'px';
+ if (Archify.radar && typeof Archify.radar.sync === 'function') Archify.radar.sync();
+ return true;
+ }
+ function resetLensPlacement(options) {
+ options = options || {};
+ manualLensPosition = null;
+ chip.removeAttribute('data-manual-placement');
+ chip.style.removeProperty('left');
+ chip.style.removeProperty('top');
+ if (options.reposition !== false && !chip.hidden) requestLensPlacement();
+ }
+ function beginLensDrag(event) {
+ if (!manualLensPlacementAvailable() || event.button !== 0 || lensDrag) return;
+ var start = currentLensPosition();
+ if (!start) return;
+ event.preventDefault();
+ event.stopPropagation();
+ lensDrag = {
+ pointerId: event.pointerId,
+ originX: event.clientX,
+ originY: event.clientY,
+ start: start,
+ previousManual: manualLensPosition ? { left: manualLensPosition.left, top: manualLensPosition.top } : null,
+ moved: false
+ };
+ chip.setAttribute('data-panel-dragging', 'true');
+ try { moveBtn.setPointerCapture(event.pointerId); } catch (_) {}
+ }
+ function moveLensDrag(event) {
+ if (!lensDrag || lensDrag.pointerId !== event.pointerId) return;
+ var dx = event.clientX - lensDrag.originX;
+ var dy = event.clientY - lensDrag.originY;
+ if (!lensDrag.moved && Math.hypot(dx, dy) <= 3) return;
+ lensDrag.moved = true;
+ event.preventDefault();
+ event.stopPropagation();
+ applyManualLensPosition({ left: lensDrag.start.left + dx, top: lensDrag.start.top + dy });
+ }
+ function finishLensDrag(event, cancel) {
+ if (!lensDrag || lensDrag.pointerId !== event.pointerId) return;
+ event.preventDefault();
+ event.stopPropagation();
+ var activeDrag = lensDrag;
+ lensDrag = null;
+ if (activeDrag.moved) lensDragClickPointer = activeDrag.pointerId;
+ chip.removeAttribute('data-panel-dragging');
+ try { moveBtn.releasePointerCapture(event.pointerId); } catch (_) {}
+ if (!cancel) return;
+ manualLensPosition = activeDrag.previousManual
+ ? { left: activeDrag.previousManual.left, top: activeDrag.previousManual.top }
+ : null;
+ if (manualLensPosition && manualLensPlacementAvailable()) {
+ applyManualLensPosition(manualLensPosition);
+ } else {
+ chip.removeAttribute('data-manual-placement');
+ chip.style.removeProperty('left');
+ chip.style.removeProperty('top');
+ if (!chip.hidden) requestLensPlacement();
+ }
+ }
+ function moveLensWithKeyboard(event) {
+ if (event.key === 'Home') {
+ event.preventDefault();
+ resetLensPlacement();
+ return;
+ }
+ if (event.metaKey || event.ctrlKey || event.altKey || !manualLensPlacementAvailable()) return;
+ var directions = {
+ ArrowLeft: [-1, 0],
+ ArrowRight: [1, 0],
+ ArrowUp: [0, -1],
+ ArrowDown: [0, 1]
+ };
+ var direction = directions[event.key];
+ if (!direction) return;
+ event.preventDefault();
+ var current = manualLensPosition || currentLensPosition();
+ if (!current) return;
+ var distance = event.shiftKey ? 4 : 16;
+ applyManualLensPosition({
+ left: current.left + direction[0] * distance,
+ top: current.top + direction[1] * distance
+ });
+ }
var lensFrame = 0;
function placeRelationshipLens() {
- lensFrame = 0;
+ if (lensFrame) {
+ cancelAnimationFrame(lensFrame);
+ lensFrame = 0;
+ }
if (chip.hidden || activeIds.length !== 1) return;
var node = svg.querySelector('[data-node-id="' + activeIds[0] + '"]');
if (!node) return;
@@ -8665,8 +8791,15 @@
MCP and trust
var visibleBottom = Math.min(containerRect.height - padding, window.innerHeight - containerRect.top - padding);
var maxTop = Math.max(padding, visibleBottom - chip.offsetHeight);
var minTop = Math.min(visibleTop, maxTop);
- var nodeCenter = nodeRect.top - containerRect.top + nodeRect.height / 2;
var mobile = window.innerWidth <= 720;
+ if (!manualLensPlacementAvailable()) {
+ chip.removeAttribute('data-manual-placement');
+ chip.style.removeProperty('left');
+ } else if (manualLensPosition) {
+ applyManualLensPosition(manualLensPosition);
+ return;
+ }
+ var nodeCenter = nodeRect.top - containerRect.top + nodeRect.height / 2;
var previewingOnMobile = mobile && chip.getAttribute('data-relationship-previewing') === 'true';
var compactOnMobile = mobile && chip.getAttribute('data-relations-expanded') !== 'true';
var preferred;
@@ -8737,6 +8870,12 @@
MCP and trust
}
function clear(options) {
options = options || {};
+ if (lensDrag) {
+ var dragPointerId = lensDrag.pointerId;
+ lensDrag = null;
+ chip.removeAttribute('data-panel-dragging');
+ try { moveBtn.releasePointerCapture(dragPointerId); } catch (_) {}
+ }
var restoreNode = options.restoreFocus === true && activeIds.length === 1
? svg.querySelector('[data-node-id="' + activeIds[0] + '"]')
: null;
@@ -8784,7 +8923,7 @@
MCP and trust
relationsBtn.setAttribute('aria-label', viewerText('viewer.passport.relations.show'));
relationsBtn.setAttribute('aria-expanded', 'false');
chip.removeAttribute('data-relations-expanded');
- chip.style.removeProperty('top');
+ if (options.preserveLensPlacement !== true) resetLensPlacement({ reposition: false });
if (options.preserveView !== true && Archify.view && typeof Archify.view.reset === 'function') {
Archify.view.reset({ automatic: true });
}
@@ -8807,7 +8946,7 @@
MCP and trust
Archify.routeProbe.clear({ updateUrl: false, restoreFocus: false });
}
var nodeList = nodes();
- var byId = {};
+ var byId = Object.create(null);
nodeList.forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; });
var normalized = [];
(ids || []).forEach(function (id) {
@@ -8819,10 +8958,11 @@
MCP and trust
return true;
}
- clear({ updateUrl: false, preserveView: true });
+ var preserveLensPlacement = activeIds.length === 1 && normalized.length === 1 && !chip.hidden;
+ clear({ updateUrl: false, preserveView: true, preserveLensPlacement: preserveLensPlacement });
activeIds = normalized;
- var selected = {};
- var related = {};
+ var selected = Object.create(null);
+ var related = Object.create(null);
var seenEdges = {};
var matchedEdges = 0;
normalized.forEach(function (id) { selected[id] = true; related[id] = true; });
@@ -8849,12 +8989,12 @@
MCP and trust
svg.setAttribute('data-focus-active', normalized.join(' '));
var defaultLabel = normalized.length === 1
? nodeLabel(byId[normalized[0]], normalized[0])
- : viewerText('viewer.guided.chapter.selectedNodes', { count: normalized.length });
+ : viewerText('viewer.focus.selectedNodes', { count: normalized.length });
label.textContent = options.label || defaultLabel;
chip.hidden = options.hideChip === true || normalized.length !== 1 || selectionMode;
if (!chip.hidden) {
renderRelationshipLens(normalized[0], byId);
- requestLensPlacement();
+ placeRelationshipLens();
}
if (options.updateUrl !== false) {
var key = options.urlKey || 'focus';
@@ -8928,6 +9068,17 @@
MCP and trust
Archify.view.reveal([id], { includeNeighbors: true, reason: 'focus' });
}
});
+ moveBtn.addEventListener('pointerdown', beginLensDrag);
+ moveBtn.addEventListener('pointermove', moveLensDrag);
+ moveBtn.addEventListener('pointerup', function (event) { finishLensDrag(event, false); });
+ moveBtn.addEventListener('pointercancel', function (event) { finishLensDrag(event, true); });
+ moveBtn.addEventListener('lostpointercapture', function (event) { finishLensDrag(event, true); });
+ moveBtn.addEventListener('dblclick', function (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ resetLensPlacement();
+ });
+ moveBtn.addEventListener('keydown', moveLensWithKeyboard);
clearBtn.addEventListener('click', function () { clear({ restoreFocus: true }); });
copyBtn.addEventListener('click', copyFocusLink);
upstreamBtn.addEventListener('click', function () { applyReachability('upstream'); });
@@ -8940,15 +9091,12 @@
MCP and trust
relationsBtn.setAttribute('aria-label', viewerText(expanded
? 'viewer.passport.relations.show'
: 'viewer.passport.relations.hide'));
- requestLensPlacement();
+ placeRelationshipLens();
});
relationshipList.addEventListener('click', function (event) {
var button = event.target.closest('[data-relationship-target]');
if (!button) return;
var id = button.getAttribute('data-relationship-target');
- if (Archify.guidedViews && typeof Archify.guidedViews.showAll === 'function') {
- Archify.guidedViews.showAll({ clearFocus: false, updateUrl: false });
- }
set(id, { toggle: false });
if (Archify.view && typeof Archify.view.reveal === 'function') {
Archify.view.reveal([id], { includeNeighbors: true, reason: 'relationship' });
@@ -8997,7 +9145,20 @@
MCP and trust
event.preventDefault();
buttons[index].focus();
});
+ document.addEventListener('pointerdown', function () {
+ lensDragClickPointer = null;
+ }, true);
document.addEventListener('click', function (event) {
+ // Losing capture (for example when a resize hides the handle) can
+ // retarget this drag's final click to the page. It is not dismissal.
+ // A new pointerdown releases the guard; keyboard clicks remain live.
+ if (lensDragClickPointer != null && event.detail > 0 &&
+ (event.pointerId == null || event.pointerId === lensDragClickPointer)) {
+ lensDragClickPointer = null;
+ event.preventDefault();
+ event.stopPropagation();
+ return;
+ }
var target = event.target;
if (chip.hidden || !target || typeof target.closest !== 'function' || chip.contains(target)) return;
if (container.getAttribute('data-just-panned') === 'true') return;
@@ -9039,7 +9200,7 @@
MCP and trust
applyReachability(reach, { updateUrl: false, toggle: false, reveal: false });
}
}
- else if (!params.get('view')) clear({ updateUrl: false });
+ else clear({ updateUrl: false });
} catch (_) {}
}
@@ -9056,7 +9217,7 @@
MCP and trust
reachabilitySnapshot: reachabilitySnapshot,
inspectRelationship: inspectRelationship,
inspectRelationshipById: inspectRelationshipById,
- reposition: requestLensPlacement,
+ reposition: placeRelationshipLens,
relationship: function () {
var record = pinnedRelationshipRecord();
return record ? { id: record.id || null, key: record.key, from: record.from, to: record.to, label: record.label } : null;
@@ -9074,6 +9235,7 @@
MCP and trust
};
})();
+
/* ============================================================
Intent Trace — temporary one-hop topology before durable focus.
Fine-pointer hover and keyboard focus share the same stable-ID preview;
@@ -9111,7 +9273,6 @@
MCP and trust
html.getAttribute('data-guide-open') === 'true' ||
container.classList.contains('is-panning') ||
svg.hasAttribute('data-lens-active') ||
- svg.hasAttribute('data-story-active') ||
svg.hasAttribute('data-relationship-preview-active') ||
!!(Archify.routeProbe && typeof Archify.routeProbe.active === 'function' && Archify.routeProbe.active()) ||
!!(Archify.focus && typeof Archify.focus.active === 'function' && Archify.focus.active());
@@ -9173,7 +9334,7 @@
MCP and trust
clear({ announce: false });
var nodeList = nodes();
var edgeList = edges();
- var byId = {};
+ var byId = Object.create(null);
nodeList.forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; });
var selected = byId[id];
if (!selected) return false;
@@ -9182,7 +9343,7 @@
MCP and trust
overlay.setAttribute('class', 'intent-trace-overlay');
overlay.setAttribute('data-intent-trace-overlay', '');
overlay.setAttribute('aria-hidden', 'true');
- var related = {};
+ var related = Object.create(null);
var seen = {};
var counts = { out: 0, in: 0, loop: 0 };
related[id] = true;
@@ -9197,1836 +9358,107 @@
MCP and trust
if (!seen[key]) {
seen[key] = true;
counts[direction] += 1;
- }
- related[from] = true;
- related[to] = true;
- edge.setAttribute('data-intent-trace-match', '');
- var wrapper = document.createElementNS(namespace, 'g');
- if (edge.hasAttribute('transform')) wrapper.setAttribute('transform', edge.getAttribute('transform'));
- edgeShapes(edge).forEach(function (shape) {
- wrapper.appendChild(traceGeometry(shape, direction));
- });
- if (wrapper.childNodes.length) overlay.appendChild(wrapper);
- });
-
- nodeList.forEach(function (node) {
- var nodeId = node.getAttribute('data-node-id');
- if (related[nodeId]) node.setAttribute('data-intent-trace-match', '');
- if (nodeId === id) node.setAttribute('data-intent-trace-selected', '');
- });
- if (overlay.childNodes.length) {
- var firstEdge = edgeList[0];
- var firstNode = svg.querySelector('[data-node-id]');
- if (firstEdge && firstEdge.parentNode) firstEdge.parentNode.insertBefore(overlay, firstEdge);
- else if (firstNode) svg.insertBefore(overlay, firstNode);
- else svg.appendChild(overlay);
- }
- activeId = id;
- svg.setAttribute('data-intent-trace-active', id);
- if (options.announce === true) {
- var total = counts.out + counts.in + counts.loop;
- status.textContent = viewerText('viewer.intent.summary', {
- label: nodeLabel(selected, id),
- out: counts.out,
- in: counts.in,
- loops: counts.loop ? viewerText('viewer.intent.loops', { count: counts.loop }) : '',
- total: total
- });
- }
- return true;
- }
- function schedule(node) {
- if (enterTimer) window.clearTimeout(enterTimer);
- enterTimer = window.setTimeout(function () {
- enterTimer = null;
- if (hoveredNode === node) show(node.getAttribute('data-node-id'), { announce: false });
- }, reducedMotion() ? 0 : 90);
- }
- function sync() {
- var candidate = focusedNode || hoveredNode;
- if (!candidate) {
- clear();
- return;
- }
- show(candidate.getAttribute('data-node-id'), { announce: candidate === focusedNode });
- }
-
- svg.addEventListener('pointerover', function (event) {
- var node = event.target.closest('[data-node-id]');
- if (!node || !finePointer() || event.pointerType === 'touch') return;
- if (event.relatedTarget && node.contains(event.relatedTarget)) return;
- hoveredNode = node;
- schedule(node);
- });
- svg.addEventListener('pointerout', function (event) {
- var node = event.target.closest('[data-node-id]');
- if (!node || (event.relatedTarget && node.contains(event.relatedTarget))) return;
- if (hoveredNode === node) hoveredNode = null;
- sync();
- });
- svg.addEventListener('focusin', function (event) {
- var node = event.target.closest('[data-node-id]');
- if (!node) return;
- focusedNode = node;
- show(node.getAttribute('data-node-id'), { announce: true });
- });
- svg.addEventListener('focusout', function (event) {
- var node = event.target.closest('[data-node-id]');
- if (!node || (event.relatedTarget && node.contains(event.relatedTarget))) return;
- if (focusedNode === node) focusedNode = null;
- sync();
- });
- container.addEventListener('pointerdown', function (event) {
- if (!event.target.closest('[data-node-id]')) clear({ announce: false });
- });
- window.addEventListener('blur', function () { clear({ announce: false }); });
-
- return {
- show: show,
- clear: clear,
- active: function () { return activeId; }
- };
- })();
-
- Archify.guidedViews = (function () {
- var data = document.getElementById('archify-guided-views-data');
- var panel = document.getElementById('guided-views');
- var prev = document.getElementById('guided-view-prev');
- var next = document.getElementById('guided-view-next');
- var all = document.getElementById('guided-view-all');
- var play = document.getElementById('guided-view-play');
- var playIcon = document.getElementById('guided-view-play-icon');
- var playLabel = document.getElementById('guided-view-play-label');
- var beatLink = document.getElementById('guided-view-beat-link');
- var beatLinkLabel = document.getElementById('guided-view-beat-link-label');
- var progressBar = document.getElementById('guided-view-progress-bar');
- var count = document.getElementById('guided-view-count');
- var handoffReceipt = document.getElementById('guided-view-handoff');
- var label = document.getElementById('guided-view-label');
- var note = document.getElementById('guided-view-note');
- var trail = document.getElementById('guided-view-trail');
- var storyCaption = document.getElementById('guided-story-caption');
- var storyCaptionIndex = document.getElementById('guided-story-caption-index');
- var storyCaptionRoute = document.getElementById('guided-story-caption-route');
- var storyCaptionDetail = document.getElementById('guided-story-caption-detail');
- var storyCaptionNext = document.getElementById('guided-story-caption-next');
- var storyCaptionNextLabel = document.getElementById('guided-story-caption-next-label');
- var chapterIndex = document.getElementById('guided-view-index');
- var chapterList = document.getElementById('guided-view-chapters');
- var shareCue = document.getElementById('share-chapter-cue');
- var shareCueState = document.getElementById('share-chapter-state');
- var shareCueCount = document.getElementById('share-chapter-count');
- var shareCueLabel = document.getElementById('share-chapter-label');
- var shareCueNote = document.getElementById('share-chapter-note');
- var shareCueRoute = document.getElementById('share-chapter-route');
- var shareCueProgress = document.getElementById('share-chapter-progress-bar');
- var svg = document.querySelector('.diagram-container svg');
- var views = [];
- var activeIndex = -1;
- var playing = false;
- var storyBeatTimer = null;
- var storyBeatIndex = -1;
- var storyBeatStartedAt = 0;
- var storyBeatElapsedMs = 0;
- var storyBeatDwellMs = 0;
- var storyPlaybackGeneration = 0;
- var storyFollowGeneration = 0;
- var storyPlaybackScope = 'story';
- var storyPlaybackComplete = false;
- var beatLinkFeedbackTimer = null;
- var momentRestoreGeneration = 0;
- var storySteps = [];
- var storyPulseOwnerToken = 0;
- var storyPulseGeneration = 0;
- var autoplayPending = false;
- var VIEW_INTERVAL_MS = 3200;
- var STORY_FOLLOW_MIN_DWELL_MS = 1100;
- var STORY_FOLLOW_DURATION_MS = 320;
- var chapterButtons = [];
- var handoffGeneration = 0;
- var currentHandoff = null;
- var previewGeneration = 0;
- var pointerPreviewIntent = null;
- var focusPreviewIntent = null;
- var activePreviewIndex = -1;
- var previewOwnerToken = 0;
-
- try { views = JSON.parse(data.textContent || '[]'); } catch (_) { views = []; }
- if (!views.length) return { count: 0, active: function () { return null; } };
- var canonicalNodeIds = {};
- Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id]'), function (node) {
- canonicalNodeIds[node.getAttribute('data-node-id')] = true;
- });
- views.forEach(function (view) {
- var seen = {};
- view.focus = (view.focus || []).filter(function (id) {
- if (!canonicalNodeIds[id] || seen[id]) return false;
- seen[id] = true;
- return true;
- });
- });
- // Establish the compact cold state before exposing the panel so the
- // full Director cannot flash during first paint. Hash restoration runs
- // in the same task and replaces this state before paint when valid.
- panel.setAttribute('data-active-view', 'all');
- panel.hidden = false;
- panel.setAttribute('data-view-interval-ms', String(VIEW_INTERVAL_MS));
- panel.setAttribute('data-story-follow-min-dwell-ms', String(STORY_FOLLOW_MIN_DWELL_MS));
- buildChapterIndex();
-
- function reducedMotion() {
- return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
- }
-
- function findNode(id) {
- return Array.prototype.find.call(svg.querySelectorAll('[data-node-id]'), function (node) {
- return node.getAttribute('data-node-id') === id;
- }) || null;
- }
-
- function chapterDelta(previous, destination) {
- var previousFocus = previous ? previous.focus : [];
- var destinationFocus = destination ? destination.focus : [];
- var previousIds = {};
- var destinationIds = {};
- previousFocus.forEach(function (id) { previousIds[id] = true; });
- destinationFocus.forEach(function (id) { destinationIds[id] = true; });
- return {
- stay: previousFocus.filter(function (id) { return destinationIds[id]; }),
- enter: destinationFocus.filter(function (id) { return !previousIds[id]; }),
- leave: previousFocus.filter(function (id) { return !destinationIds[id]; })
- };
- }
-
- function chapterAnchor(previous, destination, outgoingBeatIndex, delta) {
- if (!previous || !destination) return '';
- var stayIds = {};
- delta.stay.forEach(function (id) { stayIds[id] = true; });
- var activeBeat = outgoingBeatIndex >= 0 ? previous.focus[outgoingBeatIndex] : '';
- if (activeBeat && stayIds[activeBeat]) return activeBeat;
- for (var index = previous.focus.length - 1; index >= 0; index -= 1) {
- if (stayIds[previous.focus[index]]) return previous.focus[index];
- }
- return '';
- }
-
- function handoffMotionAllowed() {
- if (document.hidden || reducedMotion()) return false;
- if (document.documentElement.getAttribute('data-embed') === 'true') return false;
- if (window.matchMedia && window.matchMedia('print').matches) return false;
- return !(Archify.motionGovernor && Archify.motionGovernor.capable && Archify.motionGovernor.isPaused());
- }
-
- function classifyHandoff(delta, anchor) {
- var roles = {};
- delta.stay.forEach(function (id) { roles[id] = 'stay'; });
- delta.enter.forEach(function (id) { roles[id] = 'enter'; });
- delta.leave.forEach(function (id) { roles[id] = 'leave'; });
- Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id]'), function (node) {
- var id = node.getAttribute('data-node-id');
- var role = roles[id] || '';
- if (role) node.setAttribute('data-chapter-role', role);
- else node.removeAttribute('data-chapter-role');
- });
- svg.setAttribute('data-chapter-handoff', anchor ? 'anchor' : 'no-anchor');
- if (anchor) svg.setAttribute('data-chapter-anchor', anchor);
- else svg.removeAttribute('data-chapter-anchor');
- }
-
- function drawHandoffAnchor(id) {
- var node = findNode(id);
- if (!node) return null;
- var box;
- try { box = node.getBBox(); } catch (_) { return null; }
- var overlay = document.createElementNS('http://www.w3.org/2000/svg', 'g');
- var ring = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
- overlay.setAttribute('class', 'chapter-handoff-overlay');
- overlay.setAttribute('data-chapter-handoff-overlay', '');
- overlay.setAttribute('aria-hidden', 'true');
- ring.setAttribute('class', 'chapter-handoff-anchor');
- ring.setAttribute('x', String(box.x - 8));
- ring.setAttribute('y', String(box.y - 8));
- ring.setAttribute('width', String(box.width + 16));
- ring.setAttribute('height', String(box.height + 16));
- ring.setAttribute('rx', '10');
- overlay.appendChild(ring);
- svg.appendChild(overlay);
- return overlay;
- }
-
- function clearHandoffPresentation() {
- svg.removeAttribute('data-chapter-handoff');
- svg.removeAttribute('data-chapter-anchor');
- Array.prototype.forEach.call(svg.querySelectorAll('[data-chapter-handoff-overlay]'), function (overlay) { overlay.remove(); });
- Array.prototype.forEach.call(svg.querySelectorAll('[data-chapter-role]'), function (node) { node.removeAttribute('data-chapter-role'); });
- if (handoffReceipt) {
- handoffReceipt.hidden = true;
- handoffReceipt.textContent = '';
- }
- }
-
- function completeHandoff(handoff, outcome) {
- if (!handoff || currentHandoff !== handoff) return false;
- if (handoff.holdTimer) clearTimeout(handoff.holdTimer);
- handoff.holdTimer = null;
- currentHandoff = null;
- clearHandoffPresentation();
- if (handoff.ownerToken && Archify.motionGovernor) Archify.motionGovernor.release(handoff.ownerToken);
- handoff.resolve({ id: handoff.id, state: outcome || 'complete', anchor: handoff.anchor || null });
- syncStoryControlsDisabled();
- syncChapterPreview();
- return true;
- }
-
- function settleHandoff(reason) {
- var handoff = currentHandoff;
- if (!handoff) return false;
- if (handoff.holdTimer) clearTimeout(handoff.holdTimer);
- handoff.holdTimer = null;
- if (handoff.camera && handoff.camera.cancel) handoff.camera.cancel(reason || 'settled', true);
- else if (Archify.view && Archify.view.reveal) Archify.view.reveal(handoff.focus, { instant: true, reason: reason || 'settled' });
- completeHandoff(handoff, reason || 'settled');
- return true;
- }
-
- function cancelHandoff(reason) {
- var handoff = currentHandoff;
- if (!handoff) return false;
- if (handoff.holdTimer) clearTimeout(handoff.holdTimer);
- handoff.holdTimer = null;
- if (handoff.camera && handoff.camera.cancel) handoff.camera.cancel(reason || 'manual', false);
- completeHandoff(handoff, reason || 'manual');
- return true;
- }
-
- function afterHandoff(callback) {
- if (!currentHandoff) { callback(); return; }
- currentHandoff.finished.then(callback);
- }
-
- function beginHandoff(previousIndex, nextIndex, previous, destination, outgoingBeatIndex, reason) {
- if (!Archify.view || !Archify.view.reveal) return null;
- if (!previous || previousIndex === nextIndex) {
- Archify.view.reveal(destination.focus, { reason: reason });
- return null;
- }
- var delta = chapterDelta(previous, destination);
- var anchor = chapterAnchor(previous, destination, outgoingBeatIndex, delta);
- if (!handoffMotionAllowed()) {
- Archify.view.reveal(destination.focus, { instant: true, reason: reason });
- return null;
- }
- var resolver;
- var handoff = {
- id: ++handoffGeneration,
- mode: anchor ? 'holding' : 'no-anchor',
- anchor: anchor,
- focus: destination.focus.slice(),
- holdTimer: null,
- camera: null,
- ownerToken: 0,
- finished: new Promise(function (resolve) { resolver = resolve; }),
- resolve: resolver
- };
- currentHandoff = handoff;
- classifyHandoff(delta, anchor);
- if (anchor) {
- drawHandoffAnchor(anchor);
- var node = findNode(anchor);
- var nodeLabel = node ? (node.getAttribute('data-node-label') || anchor) : anchor;
- handoffReceipt.textContent = viewerText('viewer.guided.handoff', {
- from: (previousIndex + 1 < 10 ? '0' : '') + (previousIndex + 1),
- to: (nextIndex + 1 < 10 ? '0' : '') + (nextIndex + 1),
- label: nodeLabel
- });
- handoffReceipt.hidden = false;
- }
- if (Archify.motionGovernor) {
- handoff.ownerToken = Archify.motionGovernor.claim('handoff', function () {
- if (currentHandoff === handoff) settleHandoff('preempted');
- });
- }
- var startCamera = function () {
- if (currentHandoff !== handoff) return;
- handoff.holdTimer = null;
- handoff.mode = 'settling';
- svg.setAttribute('data-chapter-handoff', anchor ? 'settling' : 'no-anchor');
- handoff.camera = Archify.view.reveal(destination.focus, { reason: reason, duration: 420 });
- if (handoff.camera && handoff.camera.finished) {
- handoff.camera.finished.then(function (outcome) {
- if (currentHandoff === handoff) completeHandoff(handoff, outcome && outcome.state || 'complete');
- });
- } else {
- completeHandoff(handoff, 'complete');
- }
- };
- if (anchor) handoff.holdTimer = setTimeout(startCamera, 110);
- else startCamera();
- return handoff;
- }
-
- function centerChapterButton(button) {
- if (!button || !chapterList) return false;
- var target = Math.max(0, button.offsetLeft - (chapterList.clientWidth - button.offsetWidth) / 2);
- if (typeof chapterList.scrollTo === 'function') chapterList.scrollTo({ left: target, behavior: 'auto' });
- else chapterList.scrollLeft = target;
- return true;
- }
-
- function focusChapterButton(index) {
- if (!chapterButtons.length) return false;
- index = Math.max(0, Math.min(chapterButtons.length - 1, index));
- chapterButtons.forEach(function (button, buttonIndex) {
- button.setAttribute('tabindex', buttonIndex === index ? '0' : '-1');
- });
- chapterButtons[index].focus();
- centerChapterButton(chapterButtons[index]);
- return true;
- }
-
- function buildChapterIndex() {
- chapterButtons = [];
- chapterList.textContent = '';
- views.forEach(function (view, index) {
- var item = document.createElement('li');
- var button = document.createElement('button');
- button.type = 'button';
- button.className = 'guided-view-chapter';
- button.setAttribute('data-guided-view-id', view.id);
- button.setAttribute('aria-pressed', 'false');
- button.setAttribute('tabindex', index === 0 ? '0' : '-1');
- button.setAttribute('aria-label', viewerText('viewer.guided.chapter.open', {
- index: index + 1,
- total: views.length,
- label: view.label,
- count: view.focus.length
- }));
- button.title = view.label + ' — ' + (view.note || viewerText('viewer.guided.chapter.selectedNodes', { count: view.focus.length }));
- var position = document.createElement('span');
- position.className = 'guided-view-chapter-index';
- position.setAttribute('aria-hidden', 'true');
- position.textContent = (index + 1 < 10 ? '0' : '') + (index + 1);
- var title = document.createElement('span');
- title.className = 'guided-view-chapter-title';
- title.textContent = view.label;
- var stops = document.createElement('em');
- stops.className = 'guided-view-chapter-count';
- stops.textContent = viewerCount('viewer.guided.chapter.stop', view.focus.length);
- var delta = document.createElement('span');
- delta.className = 'guided-view-chapter-delta';
- delta.hidden = true;
- delta.setAttribute('aria-hidden', 'true');
- ['stay', 'enter', 'leave'].forEach(function (kind) {
- var value = document.createElement('span');
- value.setAttribute('data-delta-kind', kind);
- delta.appendChild(value);
- });
- button.appendChild(position);
- button.appendChild(title);
- button.appendChild(stops);
- button.appendChild(delta);
- item.appendChild(button);
- chapterList.appendChild(item);
- chapterButtons.push(button);
- });
- }
-
- function syncChapterIndex() {
- chapterButtons.forEach(function (button, index) {
- var current = index === activeIndex;
- var stops = button.querySelector('.guided-view-chapter-count');
- var deltaLabel = button.querySelector('.guided-view-chapter-delta');
- var position = activeIndex < 0 ? 'available' : (current ? 'current' : (index < activeIndex ? 'before' : 'after'));
- button.setAttribute('data-chapter-position', position);
- button.setAttribute('aria-pressed', current ? 'true' : 'false');
- button.setAttribute('tabindex', current || (activeIndex < 0 && index === 0) ? '0' : '-1');
- if (current) {
- stops.hidden = false;
- deltaLabel.hidden = true;
- button.removeAttribute('data-chapter-delta');
- button.setAttribute('aria-label', viewerText('viewer.guided.chapter.current', {
- index: index + 1,
- total: views.length,
- label: views[index].label,
- count: views[index].focus.length
- }));
- button.title = viewerText('viewer.guided.chapter.current.title', {
- label: views[index].label,
- count: views[index].focus.length
- });
- } else {
- var delta = chapterDelta(activeIndex >= 0 ? views[activeIndex] : null, views[index]);
- var compact = '=' + delta.stay.length + ' +' + delta.enter.length + ' \u2212' + delta.leave.length;
- var expanded = viewerText('viewer.guided.chapter.delta.expanded', {
- stay: delta.stay.length,
- enter: delta.enter.length,
- leave: delta.leave.length
- });
- stops.hidden = true;
- deltaLabel.hidden = false;
- deltaLabel.querySelector('[data-delta-kind="stay"]').textContent = '=' + delta.stay.length;
- deltaLabel.querySelector('[data-delta-kind="enter"]').textContent = '+' + delta.enter.length;
- deltaLabel.querySelector('[data-delta-kind="leave"]').textContent = '\u2212' + delta.leave.length;
- button.setAttribute('data-chapter-delta', compact);
- button.setAttribute('aria-label', viewerText('viewer.guided.chapter.delta.aria', {
- index: index + 1,
- total: views.length,
- label: views[index].label,
- delta: expanded
- }));
- button.title = viewerText('viewer.guided.chapter.delta.title', {
- label: views[index].label,
- delta: compact
- });
- }
- if (current) button.setAttribute('aria-current', 'step');
- else button.removeAttribute('aria-current');
- if (button.parentElement) button.parentElement.setAttribute('data-chapter-position', position);
- });
- if (activeIndex >= 0) centerChapterButton(chapterButtons[activeIndex]);
- }
-
- function chapterPreviewBlocked() {
- if (document.hidden || playing || currentHandoff) return true;
- if (document.documentElement.getAttribute('data-embed') === 'true') return true;
- if (window.matchMedia && window.matchMedia('print').matches) return true;
- if (svg.hasAttribute('data-route-picking') || svg.hasAttribute('data-route-active')) return true;
- if (svg.hasAttribute('data-lens-active') || svg.hasAttribute('data-legend-preview-active')) return true;
- if (svg.hasAttribute('data-relationship-preview-active') || svg.hasAttribute('data-intent-trace-active')) return true;
- return activeIndex < 0 && svg.hasAttribute('data-focus-active');
- }
-
- function clearChapterPreviewPresentation() {
- svg.removeAttribute('data-chapter-preview');
- Array.prototype.forEach.call(svg.querySelectorAll('[data-chapter-preview-role]'), function (node) {
- node.removeAttribute('data-chapter-preview-role');
- });
- chapterButtons.forEach(function (button) { button.removeAttribute('data-preview-active'); });
- activePreviewIndex = -1;
- if (previewOwnerToken && Archify.motionGovernor) {
- var token = previewOwnerToken;
- previewOwnerToken = 0;
- Archify.motionGovernor.release(token);
- }
- }
-
- function clearChapterPreview(options) {
- options = options || {};
- previewGeneration += 1;
- if (options.clearIntents !== false) {
- pointerPreviewIntent = null;
- focusPreviewIntent = null;
- }
- clearChapterPreviewPresentation();
- return true;
- }
-
- function winningChapterPreviewIntent() {
- return [pointerPreviewIntent, focusPreviewIntent].filter(function (intent) {
- return intent && intent.index >= 0 && intent.index < views.length && intent.index !== activeIndex;
- }).sort(function (left, right) { return right.generation - left.generation; })[0] || null;
- }
-
- function showChapterPreview(index) {
- if (index < 0 || index >= views.length || index === activeIndex || chapterPreviewBlocked()) {
- clearChapterPreviewPresentation();
- return false;
- }
- if (activePreviewIndex === index && svg.getAttribute('data-chapter-preview') === views[index].id) return true;
- clearChapterPreviewPresentation();
- var delta = chapterDelta(activeIndex >= 0 ? views[activeIndex] : null, views[index]);
- var roles = {};
- delta.stay.forEach(function (id) { roles[id] = 'stay'; });
- delta.enter.forEach(function (id) { roles[id] = 'enter'; });
- delta.leave.forEach(function (id) { roles[id] = 'leave'; });
- Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id]'), function (node) {
- var role = roles[node.getAttribute('data-node-id')];
- if (role) node.setAttribute('data-chapter-preview-role', role);
- });
- svg.setAttribute('data-chapter-preview', views[index].id);
- chapterButtons[index].setAttribute('data-preview-active', 'true');
- activePreviewIndex = index;
- if (Archify.motionGovernor && Archify.motionGovernor.capable) {
- previewOwnerToken = Archify.motionGovernor.claim('chapter-preview', function () {
- previewOwnerToken = 0;
- clearChapterPreviewPresentation();
- });
- }
- return true;
- }
-
- function syncChapterPreview() {
- var intent = winningChapterPreviewIntent();
- if (!intent || chapterPreviewBlocked()) {
- clearChapterPreviewPresentation();
- return false;
- }
- return showChapterPreview(intent.index);
- }
-
- function setChapterPreviewIntent(source, index) {
- var intent = { index: index, generation: ++previewGeneration };
- if (source === 'pointer') pointerPreviewIntent = intent;
- else focusPreviewIntent = intent;
- if (playing) pausePlayback();
- return syncChapterPreview();
- }
-
- function clearChapterPreviewIntent(source, index) {
- var intent = source === 'pointer' ? pointerPreviewIntent : focusPreviewIntent;
- if (intent && (typeof index !== 'number' || intent.index === index)) {
- if (source === 'pointer') pointerPreviewIntent = null;
- else focusPreviewIntent = null;
- previewGeneration += 1;
- }
- return syncChapterPreview();
- }
-
- function hoverCapable() {
- return !!(window.matchMedia && window.matchMedia('(hover: hover)').matches);
- }
-
- function sharePlaybackRequested() {
- try { return new URLSearchParams(location.search).get('play') === '1'; }
- catch (_) { return false; }
- }
-
- autoplayPending = sharePlaybackRequested();
- if (autoplayPending) {
- document.documentElement.setAttribute('data-share-playback', 'true');
- setAutoplayState('pending');
- }
-
- function setAutoplayState(state) {
- if (state) panel.setAttribute('data-autoplay', state);
- else panel.removeAttribute('data-autoplay');
- renderShareCue();
- }
-
- function shareCueStatus(state) {
- return {
- pending: viewerText('viewer.guided.state.ready'),
- playing: viewerText('viewer.guided.state.playing'),
- complete: viewerText('viewer.guided.state.settled'),
- interrupted: viewerText('viewer.guided.state.paused'),
- pinned: viewerText('viewer.guided.state.pinned'),
- 'reduced-motion': viewerText('viewer.guided.state.still')
- }[state] || viewerText('viewer.guided.state.ready');
- }
-
- function shareCueBeatCopy(state, view, stops) {
- var total = stops.length;
- var activeStop = storyBeatIndex >= 0 ? stops[storyBeatIndex] : null;
- if ((state === 'playing' || state === 'interrupted') && activeStop) {
- return viewerText('viewer.guided.share.step', {
- index: (storyBeatIndex + 1 < 10 ? '0' : '') + (storyBeatIndex + 1),
- total: (total < 10 ? '0' : '') + total,
- label: activeStop.textContent
- });
- }
- if (state === 'pinned' && activeStop) {
- return storyBeatCopy(storySteps[storyBeatIndex], total);
- }
- if (state === 'reduced-motion' && activeStop && hashBeatMatchesCurrent()) {
- return viewerText('viewer.guided.share.staticMoment', {
- step: viewerText('viewer.guided.share.step', {
- index: (storyBeatIndex + 1 < 10 ? '0' : '') + (storyBeatIndex + 1),
- total: (total < 10 ? '0' : '') + total,
- label: activeStop.textContent
- })
- });
- }
- if (state === 'complete') return viewerText('viewer.guided.share.complete', {
- count: total,
- note: view.note || viewerText('viewer.guided.share.settled')
- });
- if (state === 'reduced-motion') return viewerText('viewer.guided.share.staticPath', { count: total });
- if (state === 'pending') return viewerText('viewer.guided.share.ready', { count: total });
- return view.note || viewerText('viewer.guided.chapter.selectedNodes', { count: view.focus.length });
- }
-
- function renderShareCue() {
- if (!shareCue) return;
- var view = activeIndex >= 0 ? views[activeIndex] : null;
- var shareMode = document.documentElement.getAttribute('data-share-playback') === 'true';
- var embedMode = document.documentElement.getAttribute('data-embed') === 'true';
- var pinnedMode = !shareMode && embedMode && hashBeatMatchesCurrent();
- if (pinnedMode) document.documentElement.setAttribute('data-share-moment', 'true');
- else document.documentElement.removeAttribute('data-share-moment');
- shareCue.hidden = !(embedMode && view && (shareMode || pinnedMode));
- if (shareCue.hidden) return;
- var state = pinnedMode ? 'pinned' : (panel.getAttribute('data-autoplay') || 'pending');
- var stops = Array.prototype.slice.call(trail.querySelectorAll('[data-story-node]'));
- var route = stops.map(function (stop, index) {
- if (index === 0) return stop.textContent;
- var kind = stop.getAttribute('data-story-link');
- var separator = kind === 'forward' ? '\u2192' : (kind === 'reverse' ? '\u2190' : (kind === 'multiple' ? '\u21c4' : '\u00b7'));
- return separator + ' ' + stop.textContent;
- }).join(' ');
- var beatCopy = shareCueBeatCopy(state, view, stops);
- shareCue.setAttribute('data-state', state);
- shareCue.setAttribute('aria-live', state === 'playing' ? 'off' : 'polite');
- shareCueState.textContent = shareCueStatus(state);
- shareCueCount.textContent = viewerText('viewer.guided.share.chapter', {
- index: (activeIndex + 1 < 10 ? '0' : '') + (activeIndex + 1),
- total: (views.length < 10 ? '0' : '') + views.length
- });
- shareCueLabel.textContent = view.label;
- shareCueNote.textContent = beatCopy;
- shareCueRoute.textContent = route;
- shareCue.setAttribute('aria-label', viewerText('viewer.guided.share.aria', {
- state: shareCueStatus(state),
- index: activeIndex + 1,
- total: views.length,
- label: view.label,
- beat: beatCopy,
- route: route
- }));
- }
-
- function setShareCueProgress(fraction) {
- if (!shareCueProgress) return;
- shareCueProgress.style.animation = 'none';
- shareCueProgress.style.setProperty('--guided-progress-start', String(Math.max(0, Math.min(1, fraction))));
- shareCueProgress.style.transform = 'scaleX(' + Math.max(0, Math.min(1, fraction)) + ')';
- }
-
- function startShareCueProgress(fraction, duration) {
- if (!shareCueProgress) return;
- setShareCueProgress(fraction || 0);
- void shareCueProgress.offsetWidth;
- shareCueProgress.style.animation = 'archify-guided-progress ' + Math.max(1, duration || VIEW_INTERVAL_MS) + 'ms linear forwards';
- }
-
- function storyNodeLabel(node, fallback) {
- if (!node) return fallback;
- return node.getAttribute('data-node-label') ||
- (node.getAttribute('aria-label') || fallback).replace(/^Focus\s+/, '');
- }
-
- function storyEdgeKey(edge) {
- return edge.getAttribute('data-edge-key') || (
- edge.getAttribute('data-edge-from') + '\u0000' +
- edge.getAttribute('data-edge-to') + '\u0000' +
- (edge.getAttribute('data-edge-label') || '')
- );
- }
-
- function uniqueStoryEdges(edgeList) {
- var positions = {};
- var unique = [];
- edgeList.forEach(function (edge) {
- var key = storyEdgeKey(edge);
- if (!Object.prototype.hasOwnProperty.call(positions, key)) {
- positions[key] = unique.length;
- unique.push(edge);
- return;
- }
- var index = positions[key];
- if (!storyGeometry(unique[index]).length && storyGeometry(edge).length) unique[index] = edge;
- });
- return unique;
- }
-
- function storyStep(view, index, edgeList, byId) {
- var id = view.focus[index];
- var node = byId[id];
- var previousId = index > 0 ? view.focus[index - 1] : '';
- var forward = [];
- var reverse = [];
- if (previousId) {
- edgeList.forEach(function (edge) {
- var from = edge.getAttribute('data-edge-from');
- var to = edge.getAttribute('data-edge-to');
- if (from === previousId && to === id) forward.push(edge);
- else if (from === id && to === previousId) reverse.push(edge);
- });
- }
- forward = uniqueStoryEdges(forward);
- reverse = uniqueStoryEdges(reverse);
- var edges = forward.concat(reverse).sort(function (left, right) {
- return edgeList.indexOf(left) - edgeList.indexOf(right);
- });
- var relation = index === 0 ? 'start' : (!edges.length ? 'group' : (
- edges.length === 1 && forward.length === 1 ? 'forward' :
- (edges.length === 1 && reverse.length === 1 ? 'reverse' : 'multiple')
- ));
- return {
- index: index,
- nodeId: id,
- nodeLabel: storyNodeLabel(byId[id], id),
- previousId: previousId,
- previousLabel: previousId ? storyNodeLabel(byId[previousId], previousId) : '',
- relation: relation,
- edges: edges,
- edgeKeys: edges.map(storyEdgeKey),
- edgeLabels: edges.map(function (edge) { return edge.getAttribute('data-edge-label') || ''; }).filter(function (value, edgeIndex, values) {
- return value && values.indexOf(value) === edgeIndex;
- }),
- responsibility: node ? (node.getAttribute('data-node-sublabel') || '') : '',
- context: node ? (node.getAttribute('data-node-context') || '') : ''
- };
- }
-
- function storyBeatCopy(step, total) {
- if (!step) return '';
- var position = (step.index + 1 < 10 ? '0' : '') + (step.index + 1);
- var countValue = (total < 10 ? '0' : '') + total;
- if (step.relation === 'start') return viewerText('viewer.guided.beat.start', { index: position, total: countValue, label: step.nodeLabel });
- if (step.relation === 'forward') return viewerText('viewer.guided.beat.forward', { index: position, total: countValue, from: step.previousLabel, to: step.nodeLabel });
- if (step.relation === 'reverse') return viewerText('viewer.guided.beat.reverse', { index: position, total: countValue, from: step.nodeLabel, to: step.previousLabel });
- if (step.relation === 'multiple') return viewerText('viewer.guided.beat.multiple', { index: position, total: countValue, from: step.previousLabel, to: step.nodeLabel, count: step.edges.length });
- return viewerText('viewer.guided.beat.group', { index: position, total: countValue, from: step.previousLabel, to: step.nodeLabel });
- }
-
- function storyBeatAria(step, total) {
- var prefix = viewerText('viewer.guided.beat.aria.prefix', { index: step.index + 1, total: total, label: step.nodeLabel });
- if (step.relation === 'start') return prefix + viewerText('viewer.guided.beat.aria.start');
- if (step.relation === 'forward') return prefix + viewerText('viewer.guided.beat.aria.forward', { from: step.previousLabel });
- if (step.relation === 'reverse') return prefix + viewerText('viewer.guided.beat.aria.reverse', { from: step.previousLabel, to: step.nodeLabel });
- if (step.relation === 'multiple') return prefix + viewerText('viewer.guided.beat.aria.multiple', { from: step.previousLabel, count: step.edges.length });
- return prefix + viewerText('viewer.guided.beat.aria.group', { from: step.previousLabel });
- }
-
- function storyCaptionRouteCopy(step) {
- if (step.relation === 'start') return step.nodeLabel + ' · ' + viewerText('viewer.guided.caption.start');
- if (step.relation === 'reverse') return step.previousLabel + ' ← ' + step.nodeLabel;
- if (step.relation === 'multiple') return step.previousLabel + ' ⇄ ' + step.nodeLabel;
- if (step.relation === 'group') return step.previousLabel + ' · ' + step.nodeLabel;
- return step.previousLabel + ' → ' + step.nodeLabel;
- }
-
- function storyCaptionDetailCopy(step) {
- var facts = [];
- if (step.relation === 'group') facts.push(viewerText('viewer.guided.caption.grouped'));
- else if (step.edgeLabels.length) facts.push(step.edgeLabels.slice(0, 3).join(' + ') + (step.edgeLabels.length > 3 ? viewerText('viewer.guided.caption.more', { count: step.edgeLabels.length - 3 }) : ''));
- else if (step.relation === 'reverse') facts.push(viewerText('viewer.guided.caption.reverse'));
- else if (step.relation === 'multiple') facts.push(viewerText('viewer.guided.caption.relationships', { count: step.edges.length }));
- else if (step.relation !== 'start') facts.push(viewerText('viewer.guided.caption.relationship'));
- if (step.relation === 'reverse') facts.push(viewerText('viewer.guided.caption.direction', { from: step.nodeLabel, to: step.previousLabel }));
- if (step.responsibility) facts.push(step.responsibility);
- if (step.context) facts.push(step.context);
- if (!facts.length) facts.push(viewerText('viewer.guided.caption.starting'));
- return facts.join(' · ');
- }
-
- function renderStoryCaption(step, total, nextStep) {
- if (!storyCaption) return;
- if (!step) {
- storyCaption.hidden = true;
- storyCaption.removeAttribute('data-story-caption');
- storyCaptionNext.hidden = true;
- storyCaptionNextLabel.textContent = '';
- return;
- }
- storyCaption.hidden = false;
- storyCaption.setAttribute('data-story-caption', step.relation);
- storyCaption.setAttribute('aria-live', playing ? 'off' : 'polite');
- storyCaptionIndex.textContent = (step.index + 1 < 10 ? '0' : '') + (step.index + 1) + ' / ' + (total < 10 ? '0' : '') + total;
- storyCaptionRoute.textContent = storyCaptionRouteCopy(step);
- storyCaptionDetail.textContent = storyCaptionDetailCopy(step);
- storyCaptionNext.hidden = !nextStep;
- storyCaptionNextLabel.textContent = nextStep
- ? ((nextStep.index + 1 < 10 ? '0' : '') + (nextStep.index + 1) + ' · ' + nextStep.nodeLabel)
- : '';
- storyCaption.style.animation = 'none';
- void storyCaption.offsetWidth;
- storyCaption.style.removeProperty('animation');
- }
-
- function storyMomentLink() {
- var view = activeIndex >= 0 ? views[activeIndex] : null;
- var step = storyBeatIndex >= 0 ? storySteps[storyBeatIndex] : null;
- if (!view || !step) return '';
- var url = new URL(location.href);
- url.searchParams.delete('play');
- url.hash = 'view=' + encodeURIComponent(view.id) + '&beat=' + encodeURIComponent(step.nodeId);
- return url.href;
- }
-
- function fallbackCopyStoryMoment(value) {
- var field = document.createElement('textarea');
- field.value = value;
- field.setAttribute('readonly', '');
- field.style.position = 'fixed';
- field.style.opacity = '0';
- document.body.appendChild(field);
- field.select();
- var copied = false;
- try { copied = document.execCommand('copy'); } catch (_) {}
- field.remove();
- return copied;
- }
-
- function resetBeatLinkFeedback() {
- if (beatLinkFeedbackTimer) clearTimeout(beatLinkFeedbackTimer);
- beatLinkFeedbackTimer = null;
- beatLink.removeAttribute('data-copy-state');
- beatLinkLabel.textContent = viewerText('viewer.guided.copyMoment');
- }
-
- function syncBeatLink() {
- var step = storyBeatIndex >= 0 ? storySteps[storyBeatIndex] : null;
- var available = !!(activeIndex >= 0 && step && !currentHandoff);
- beatLink.disabled = !available;
- if (!beatLink.hasAttribute('data-copy-state')) beatLinkLabel.textContent = viewerText('viewer.guided.copyMoment');
- var description = available
- ? viewerText('viewer.guided.beatLink', { index: step.index + 1, total: storySteps.length, label: step.nodeLabel })
- : viewerText('viewer.guided.selectBeatLink');
- beatLink.setAttribute('aria-label', description);
- beatLink.title = description;
- }
-
- function setBeatLinkFeedback(copied) {
- resetBeatLinkFeedback();
- beatLink.setAttribute('data-copy-state', copied ? 'copied' : 'failed');
- beatLinkLabel.textContent = viewerText(copied ? 'viewer.guided.copied' : 'viewer.guided.copyFailed');
- beatLink.setAttribute('aria-label', viewerText(copied ? 'viewer.guided.momentCopied' : 'viewer.guided.momentCopyFailed'));
- beatLinkFeedbackTimer = setTimeout(function () {
- resetBeatLinkFeedback();
- syncBeatLink();
- }, 1600);
- }
-
- function copyStoryMomentLink() {
- if (playing) pausePlayback();
- var value = storyMomentLink();
- if (!value) return Promise.resolve(false);
- var copy = navigator.clipboard && typeof navigator.clipboard.writeText === 'function'
- ? navigator.clipboard.writeText(value).then(function () { return true; }).catch(function () { return fallbackCopyStoryMoment(value); })
- : Promise.resolve(fallbackCopyStoryMoment(value));
- return copy.then(function (copied) {
- setBeatLinkFeedback(copied);
- return copied;
- });
- }
-
- function hashBeatMatchesCurrent() {
- var view = activeIndex >= 0 ? views[activeIndex] : null;
- var step = storyBeatIndex >= 0 ? storySteps[storyBeatIndex] : null;
- if (!view || !step) return false;
- try {
- var params = new URLSearchParams(location.hash.replace(/^#/, ''));
- return params.get('view') === view.id && params.get('beat') === step.nodeId;
- } catch (_) { return false; }
- }
-
- function storyMotionAllowed() {
- if (document.hidden || reducedMotion()) return false;
- if (document.documentElement.getAttribute('data-motion') !== 'live') return false;
- if (document.documentElement.getAttribute('data-embed') === 'true' &&
- document.documentElement.getAttribute('data-share-playback') !== 'true') return false;
- if (window.matchMedia && window.matchMedia('print').matches) return false;
- return !(Archify.motionGovernor && Archify.motionGovernor.isPaused());
- }
-
- function storyAutomaticPlaybackAllowed() {
- if (document.hidden || reducedMotion()) return false;
- if (window.matchMedia && window.matchMedia('print').matches) return false;
- if (Archify.motionGovernor && Archify.motionGovernor.capable) return !Archify.motionGovernor.isPaused();
- return true;
- }
-
- function clearStoryPulse(options) {
- options = options || {};
- storyPulseGeneration += 1;
- Array.prototype.forEach.call(svg.querySelectorAll('.story-trail-flow[data-story-pulse]'), function (flow) {
- flow.removeAttribute('data-story-pulse');
- });
- Array.prototype.forEach.call(svg.querySelectorAll('[data-story-carrier-overlay]'), function (overlay) {
- overlay.remove();
- });
- if (storyPulseOwnerToken && options.release !== false && Archify.motionGovernor) {
- var token = storyPulseOwnerToken;
- storyPulseOwnerToken = 0;
- Archify.motionGovernor.release(token);
- } else if (options.release === false) storyPulseOwnerToken = 0;
- }
-
- function pulseStoryStep(step) {
- clearStoryPulse();
- if (!step || (step.relation !== 'forward' && step.relation !== 'reverse') || step.edges.length !== 1 || !storyMotionAllowed()) return false;
- var flows = Array.prototype.slice.call(svg.querySelectorAll('.story-trail-flow[data-story-beat-step="' + step.index + '"]'));
- if (!flows.length) return false;
- var pulseGeneration = storyPulseGeneration;
- if (Archify.motionGovernor && Archify.motionGovernor.capable) {
- storyPulseOwnerToken = Archify.motionGovernor.claim('story', function () {
- clearStoryPulse({ release: false });
- });
- }
- flows.forEach(function (flow) { flow.setAttribute('data-story-pulse', 'true'); });
- if (Archify.flowTokens && typeof Archify.flowTokens.create === 'function') {
- var edge = step.edges[0];
- var shapes = storyGeometry(edge);
- var carrier = shapes.length ? Archify.flowTokens.create(edge, shapes[0], {
- className: 'story-flow-token',
- duration: '0.78s'
- }) : null;
- if (carrier) {
- var carrierOverlay = document.createElementNS('http://www.w3.org/2000/svg', 'g');
- var carrierWrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
- carrierOverlay.setAttribute('class', 'story-carrier-overlay');
- carrierOverlay.setAttribute('data-story-carrier-overlay', '');
- carrierOverlay.setAttribute('aria-hidden', 'true');
- if (edge.hasAttribute('transform')) carrierWrapper.setAttribute('transform', edge.getAttribute('transform'));
- carrier.setAttribute('data-story-carrier-token', '');
- carrier.setAttribute('data-story-beat-step', String(step.index));
- carrierWrapper.appendChild(carrier);
- carrierOverlay.appendChild(carrierWrapper);
- var firstNode = svg.querySelector('[data-node-id]');
- while (firstNode && firstNode.parentNode !== svg) firstNode = firstNode.parentNode;
- if (firstNode) svg.insertBefore(carrierOverlay, firstNode);
- else svg.appendChild(carrierOverlay);
- }
- }
- void flows[0].getBoundingClientRect();
- flows[0].addEventListener('animationend', function () {
- if (pulseGeneration === storyPulseGeneration) clearStoryPulse();
- }, { once: true });
- return true;
- }
-
- function stopStoryBeatTimer(options) {
- options = options || {};
- storyPlaybackGeneration += 1;
- if (storyBeatTimer && options.preserveElapsed === true && storyBeatStartedAt) {
- storyBeatElapsedMs = Math.min(storyBeatDwellMs, storyBeatElapsedMs + Math.max(0, Date.now() - storyBeatStartedAt));
- }
- if (storyBeatTimer) clearTimeout(storyBeatTimer);
- storyBeatTimer = null;
- storyBeatStartedAt = 0;
- }
-
- function centerStoryStop(stop) {
- if (!stop || !trail) return false;
- var bounds = stop.getBoundingClientRect();
- var viewportLeft = trail.getBoundingClientRect().left + trail.clientLeft;
- var target = Math.max(0, trail.scrollLeft + bounds.left - viewportLeft - (trail.clientWidth - bounds.width) / 2);
- trail.scrollLeft = target;
- return true;
- }
-
- function storyBeatDwell(total) {
- return Math.max(STORY_FOLLOW_MIN_DWELL_MS, VIEW_INTERVAL_MS / Math.max(1, total));
- }
-
- function clearStoryFollow() {
- storyFollowGeneration += 1;
- svg.removeAttribute('data-story-follow');
- panel.removeAttribute('data-story-follow');
- panel.removeAttribute('data-story-follow-node');
- }
-
- function storyFrameIds(step) {
- if (!step) return [];
- var ids = [];
- if (step.index > 0 && storySteps[step.index - 1]) ids.push(storySteps[step.index - 1].nodeId);
- ids.push(step.nodeId);
- if (step.index + 1 < storySteps.length) ids.push(storySteps[step.index + 1].nodeId);
- return ids;
- }
-
- function followStoryStep(step, options) {
- options = options || {};
- if (!step || document.hidden) return false;
- if (!Archify.view || typeof Archify.view.reveal !== 'function') {
- var deferredIndex = step.index;
- var deferredGeneration = ++storyFollowGeneration;
- requestAnimationFrame(function () {
- if (deferredGeneration !== storyFollowGeneration || storyBeatIndex !== deferredIndex) return;
- if (Archify.view && typeof Archify.view.reveal === 'function') followStoryStep(step, options);
- });
- return true;
- }
- if (window.matchMedia && window.matchMedia('print').matches) return false;
- var embed = document.documentElement.getAttribute('data-embed') === 'true';
- var explicitEmbedPlayback = document.documentElement.getAttribute('data-share-playback') === 'true';
- if (embed && !explicitEmbedPlayback && options.linked !== true) return false;
- var ids = storyFrameIds(step);
- var generation = ++storyFollowGeneration;
- svg.setAttribute('data-story-follow', step.nodeId);
- panel.setAttribute('data-story-follow', 'moving');
- panel.setAttribute('data-story-follow-node', step.nodeId);
- var receipt = Archify.view.reveal(ids, {
- reason: options.manual === true ? 'story-beat' : 'story-follow',
- padding: 64,
- maxScale: 1.65,
- duration: STORY_FOLLOW_DURATION_MS,
- instant: options.instant === true || reducedMotion() || document.documentElement.getAttribute('data-motion') !== 'live'
- });
- if (receipt && receipt.finished && typeof receipt.finished.then === 'function') {
- receipt.finished.then(function (result) {
- if (generation !== storyFollowGeneration || storyBeatIndex !== step.index) return;
- svg.removeAttribute('data-story-follow');
- panel.setAttribute('data-story-follow', result && result.state === 'complete' ? 'settled' : 'interrupted');
- });
- } else if (generation === storyFollowGeneration) {
- panel.setAttribute('data-story-follow', 'settled');
- }
- return receipt;
- }
-
- function setStoryBeat(index, options) {
- options = options || {};
- var stops = Array.prototype.slice.call(trail.querySelectorAll('[data-story-node]'));
- if (!stops.length) return false;
- resetBeatLinkFeedback();
- storyBeatIndex = Math.max(0, Math.min(stops.length - 1, index));
- var nextStep = storyBeatIndex + 1 < storySteps.length ? storySteps[storyBeatIndex + 1] : null;
- svg.setAttribute('data-story-beat', (storyBeatIndex + 1) + '/' + stops.length);
- panel.setAttribute('data-story-beat', (storyBeatIndex + 1) + '/' + stops.length);
- if (nextStep) {
- svg.setAttribute('data-story-next', nextStep.nodeId);
- panel.setAttribute('data-story-next', nextStep.nodeId);
- } else {
- svg.removeAttribute('data-story-next');
- panel.removeAttribute('data-story-next');
- }
- function storyBeatState(step) {
- if (step < storyBeatIndex) return 'past';
- if (step === storyBeatIndex) return 'active';
- if (step === storyBeatIndex + 1) return 'next';
- return 'pending';
- }
- Array.prototype.forEach.call(svg.querySelectorAll('[data-story-step]'), function (node) {
- var step = Number(node.getAttribute('data-story-step'));
- node.setAttribute('data-story-beat-state', storyBeatState(step));
- });
- Array.prototype.forEach.call(svg.querySelectorAll('[data-story-beat-step]'), function (edge) {
- var step = Number(edge.getAttribute('data-story-beat-step'));
- edge.setAttribute('data-story-beat-state', storyBeatState(step));
- });
- stops.forEach(function (stop, step) {
- stop.setAttribute('data-story-beat-state', storyBeatState(step));
- if (step === storyBeatIndex) stop.setAttribute('aria-current', 'step');
- else stop.removeAttribute('aria-current');
- });
- note.setAttribute('aria-live', 'off');
- note.textContent = storyBeatCopy(storySteps[storyBeatIndex], stops.length);
- renderStoryCaption(storySteps[storyBeatIndex], stops.length, nextStep);
- if (options.center === true) centerStoryStop(stops[storyBeatIndex]);
- if (options.pulse === true) pulseStoryStep(storySteps[storyBeatIndex]);
- else clearStoryPulse();
- if (options.follow === true) followStoryStep(storySteps[storyBeatIndex], {
- manual: options.manual === true,
- linked: options.linked === true,
- instant: options.followInstant === true
- });
- renderShareCue();
- syncBeatLink();
- return true;
- }
-
- function settleStoryBeats() {
- stopStoryBeatTimer();
- clearStoryPulse();
- clearStoryFollow();
- resetBeatLinkFeedback();
- storyBeatIndex = -1;
- storyBeatElapsedMs = 0;
- storyBeatDwellMs = 0;
- svg.removeAttribute('data-story-beat');
- svg.removeAttribute('data-story-next');
- panel.removeAttribute('data-story-beat');
- panel.removeAttribute('data-story-next');
- Array.prototype.forEach.call(svg.querySelectorAll('[data-story-beat-state]'), function (el) {
- el.removeAttribute('data-story-beat-state');
- });
- Array.prototype.forEach.call(trail.querySelectorAll('[data-story-beat-state]'), function (stop) {
- stop.removeAttribute('data-story-beat-state');
- stop.removeAttribute('aria-current');
- });
- note.setAttribute('aria-live', 'polite');
- if (activeIndex >= 0) note.textContent = views[activeIndex].note || viewerText('viewer.guided.chapter.selectedNodes', { count: views[activeIndex].focus.length });
- renderStoryCaption(null, 0);
- renderShareCue();
- syncBeatLink();
- }
-
- function clearStoryTrail() {
- stopStoryBeatTimer();
- clearStoryPulse();
- clearStoryFollow();
- resetBeatLinkFeedback();
- storyBeatIndex = -1;
- storyBeatElapsedMs = 0;
- storyBeatDwellMs = 0;
- storySteps = [];
- svg.removeAttribute('data-story-active');
- svg.removeAttribute('data-story-playing');
- svg.removeAttribute('data-story-beat');
- svg.removeAttribute('data-story-next');
- panel.removeAttribute('data-story-beat');
- panel.removeAttribute('data-story-next');
- Array.prototype.forEach.call(svg.querySelectorAll('[data-story-overlay]'), function (overlay) {
- overlay.remove();
- });
- Array.prototype.forEach.call(svg.querySelectorAll('[data-story-step], [data-story-beat-state]'), function (node) {
- node.removeAttribute('data-story-step');
- node.removeAttribute('data-story-beat-state');
- node.style.removeProperty('--story-step');
- });
- Array.prototype.forEach.call(svg.querySelectorAll('[data-edge-from][data-story-beat-step]'), function (edge) {
- edge.removeAttribute('data-story-beat-step');
- edge.removeAttribute('data-story-beat-state');
- });
- trail.textContent = '';
- trail.hidden = true;
- trail.removeAttribute('aria-label');
- renderStoryCaption(null, 0);
- syncBeatLink();
- }
-
- function storyGeometry(edge) {
- if (/^(path|line|polyline)$/i.test(edge.tagName)) return [edge];
- return Array.prototype.slice.call(edge.querySelectorAll('path, line, polyline'));
- }
-
- function renderStoryTrail(view) {
- if (Archify.semanticLens && typeof Archify.semanticLens.clearPreview === 'function') Archify.semanticLens.clearPreview();
- clearStoryTrail();
- if (!view) return;
-
- var nodeList = Array.prototype.slice.call(svg.querySelectorAll('[data-node-id]'));
- var edgeList = Array.prototype.slice.call(svg.querySelectorAll('[data-edge-from][data-edge-to]'));
- var byId = {};
- nodeList.forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; });
- storySteps = view.focus.map(function (_, index) { return storyStep(view, index, edgeList, byId); });
- var labels = [];
- storySteps.forEach(function (step, index) {
- var id = step.nodeId;
- var node = byId[id];
- if (!node) return;
- node.setAttribute('data-story-step', String(index));
- node.style.setProperty('--story-step', String(index));
- var nodeLabel = step.nodeLabel;
- labels.push(nodeLabel);
- var stop = document.createElement('button');
- stop.type = 'button';
- stop.className = 'guided-view-stop';
- stop.setAttribute('data-story-node', id);
- stop.setAttribute('data-story-index', String(index));
- stop.setAttribute('data-story-relation', step.relation);
- stop.setAttribute('data-story-number', (index + 1 < 10 ? '0' : '') + (index + 1));
- if (index > 0) stop.setAttribute('data-story-link', step.relation);
- stop.style.setProperty('--story-step', String(index));
- stop.setAttribute('aria-label', storyBeatAria(step, storySteps.length));
- stop.title = storyBeatCopy(step, storySteps.length);
- stop.textContent = nodeLabel;
- trail.appendChild(stop);
- });
- trail.hidden = labels.length === 0;
- trail.setAttribute('aria-label', viewerText('viewer.guided.storyTrail', { label: view.label, count: labels.length }));
- trail.scrollLeft = 0;
- svg.setAttribute('data-story-active', view.id);
-
- var overlay = document.createElementNS('http://www.w3.org/2000/svg', 'g');
- overlay.setAttribute('class', 'story-trail-overlay');
- overlay.setAttribute('data-story-overlay', '');
- overlay.setAttribute('aria-hidden', 'true');
- var copied = 0;
- storySteps.forEach(function (step) {
- step.edges.forEach(function (edge) {
- var edgeBeat = step.index;
- edge.setAttribute('data-story-beat-step', String(edgeBeat));
- var wrapper = document.createElementNS('http://www.w3.org/2000/svg', 'g');
- if (edge.hasAttribute('transform')) wrapper.setAttribute('transform', edge.getAttribute('transform'));
- storyGeometry(edge).forEach(function (shape) {
- var clone = shape.cloneNode(false);
- clone.removeAttribute('id');
- clone.removeAttribute('marker-start');
- clone.removeAttribute('marker-mid');
- clone.removeAttribute('marker-end');
- clone.removeAttribute('aria-label');
- clone.removeAttribute('role');
- clone.removeAttribute('data-animate');
- clone.removeAttribute('data-edge-from');
- clone.removeAttribute('data-edge-to');
- clone.removeAttribute('data-edge-key');
- clone.removeAttribute('data-edge-id');
- clone.removeAttribute('data-edge-label');
- clone.setAttribute('class', 'story-trail-flow');
- clone.setAttribute('data-story-beat-step', String(edgeBeat));
- clone.style.setProperty('--story-step', String(edgeBeat));
- wrapper.appendChild(clone);
- copied += 1;
- });
- if (wrapper.childNodes.length) overlay.appendChild(wrapper);
+ }
+ related[from] = true;
+ related[to] = true;
+ edge.setAttribute('data-intent-trace-match', '');
+ var wrapper = document.createElementNS(namespace, 'g');
+ if (edge.hasAttribute('transform')) wrapper.setAttribute('transform', edge.getAttribute('transform'));
+ edgeShapes(edge).forEach(function (shape) {
+ wrapper.appendChild(traceGeometry(shape, direction));
});
+ if (wrapper.childNodes.length) overlay.appendChild(wrapper);
+ });
+
+ nodeList.forEach(function (node) {
+ var nodeId = node.getAttribute('data-node-id');
+ if (related[nodeId]) node.setAttribute('data-intent-trace-match', '');
+ if (nodeId === id) node.setAttribute('data-intent-trace-selected', '');
});
- if (copied) {
+ if (overlay.childNodes.length) {
var firstEdge = edgeList[0];
+ var firstNode = svg.querySelector('[data-node-id]');
if (firstEdge && firstEdge.parentNode) firstEdge.parentNode.insertBefore(overlay, firstEdge);
- else svg.insertBefore(overlay, svg.firstChild);
- }
- }
-
- function syncStoryPlayback() {
- var shouldPlay = playing && activeIndex >= 0;
- if (shouldPlay && svg.getAttribute('data-story-playing') !== 'true') svg.setAttribute('data-story-playing', 'true');
- else if (!shouldPlay && svg.hasAttribute('data-story-playing')) svg.removeAttribute('data-story-playing');
- }
-
- function resetProgress(fraction) {
- fraction = Math.max(0, Math.min(1, Number(fraction) || 0));
- progressBar.style.animation = 'none';
- progressBar.style.setProperty('--guided-progress-start', String(fraction));
- progressBar.style.transform = 'scaleX(' + fraction + ')';
- }
-
- function startProgress(fraction, duration) {
- resetProgress(fraction);
- void progressBar.offsetWidth;
- progressBar.style.animation = 'archify-guided-progress ' + Math.max(1, duration || VIEW_INTERVAL_MS) + 'ms linear forwards';
- }
-
- function currentStoryProgress() {
- var total = storySteps.length;
- if (!total || storyBeatIndex < 0) return 0;
- var elapsed = storyBeatElapsedMs;
- if (playing && storyBeatTimer && storyBeatStartedAt) elapsed += Math.max(0, Date.now() - storyBeatStartedAt);
- var dwell = storyBeatDwellMs || storyBeatDwell(total);
- return Math.max(0, Math.min(1, (storyBeatIndex + Math.min(1, elapsed / dwell)) / total));
- }
-
- function renderPlayback() {
- var automaticPlaybackAllowed = storyAutomaticPlaybackAllowed();
- panel.setAttribute('data-playing', playing ? 'true' : 'false');
- syncStoryPlayback();
- play.disabled = !playing && !automaticPlaybackAllowed;
- play.setAttribute('aria-pressed', playing ? 'true' : 'false');
- play.setAttribute('aria-label', viewerText(playing
- ? 'viewer.guided.pause'
- : !automaticPlaybackAllowed
- ? 'viewer.guided.motionUnavailable'
- : storyPlaybackComplete
- ? 'viewer.guided.replay'
- : 'viewer.guided.play'));
- play.title = viewerText(playing
- ? 'viewer.guided.pause.title'
- : !automaticPlaybackAllowed
- ? 'viewer.guided.enableMotion'
- : storyPlaybackComplete
- ? 'viewer.guided.replay.title'
- : 'viewer.guided.play.title');
- playIcon.textContent = playing ? '\u2016' : '\u25b6';
- playLabel.textContent = viewerText(playing
- ? 'viewer.guided.pauseStory'
- : storyPlaybackComplete
- ? 'viewer.guided.replayStory'
- : 'viewer.guided.playStory');
- if (storyCaption && !storyCaption.hidden) storyCaption.setAttribute('aria-live', playing ? 'off' : 'polite');
- }
-
- function render() {
- var view = views[activeIndex];
- count.textContent = activeIndex < 0 ? '0 / ' + views.length : (activeIndex + 1) + ' / ' + views.length;
- label.textContent = view ? view.label : viewerText('viewer.guided.explore');
- note.setAttribute('aria-live', storyBeatIndex >= 0 ? 'off' : 'polite');
- note.textContent = storyBeatIndex >= 0
- ? storyBeatCopy(storySteps[storyBeatIndex], storySteps.length)
- : (view
- ? (view.note || viewerText('viewer.guided.chapter.selectedNodes', { count: view.focus.length }))
- : viewerText('viewer.guided.intro'));
- prev.disabled = activeIndex < 0;
- next.disabled = activeIndex >= views.length - 1;
- all.disabled = activeIndex < 0;
- panel.setAttribute('data-active-view', view ? view.id : 'all');
- syncChapterIndex();
- renderShareCue();
- renderPlayback();
- syncBeatLink();
- }
-
- function pausePlayback(options) {
- options = options || {};
- var progress = options.complete === true ? 1 : currentStoryProgress();
- if (panel.getAttribute('data-autoplay') === 'playing') {
- setShareCueProgress(progress);
- setAutoplayState(options.complete === true ? 'complete' : 'interrupted');
- }
- stopStoryBeatTimer({ preserveElapsed: options.complete !== true });
- clearStoryPulse();
- clearStoryFollow();
- playing = false;
- storyPlaybackComplete = options.complete === true;
- resetProgress(progress);
- renderPlayback();
- renderShareCue();
- }
-
- function finishStoryChapter() {
- if (!playing) return false;
- if (storyPlaybackScope === 'chapter') {
- pausePlayback({ complete: true });
- return true;
+ else if (firstNode) svg.insertBefore(overlay, firstNode);
+ else svg.appendChild(overlay);
}
- if (activeIndex < views.length - 1) {
- var destinationIndex = activeIndex + 1;
- activate(destinationIndex, { playback: true });
- afterHandoff(function () {
- if (!playing || activeIndex !== destinationIndex) return;
- storyBeatElapsedMs = 0;
- setStoryBeat(0, { pulse: true, follow: true });
- scheduleStoryPlayback();
+ activeId = id;
+ svg.setAttribute('data-intent-trace-active', id);
+ if (options.announce === true) {
+ var total = counts.out + counts.in + counts.loop;
+ status.textContent = viewerText('viewer.intent.summary', {
+ label: nodeLabel(selected, id),
+ out: counts.out,
+ in: counts.in,
+ loops: counts.loop ? viewerText('viewer.intent.loops', { count: counts.loop }) : '',
+ total: total
});
- return true;
- }
- pausePlayback({ complete: true });
- return true;
- }
-
- function scheduleStoryPlayback() {
- var total = storySteps.length;
- if (!playing || !total || reducedMotion()) {
- if (playing) pausePlayback();
- return false;
- }
- storyBeatDwellMs = storyBeatDwell(total);
- if (storyBeatIndex < 0) {
- storyBeatElapsedMs = 0;
- setStoryBeat(0, { pulse: true, follow: true });
- }
- storyBeatElapsedMs = Math.max(0, Math.min(storyBeatDwellMs, storyBeatElapsedMs));
- var remainingDwell = Math.max(1, storyBeatDwellMs - storyBeatElapsedMs);
- var progress = (storyBeatIndex + storyBeatElapsedMs / storyBeatDwellMs) / total;
- var remainingChapter = remainingDwell + Math.max(0, total - storyBeatIndex - 1) * storyBeatDwellMs;
- startProgress(progress, remainingChapter);
- if (panel.getAttribute('data-autoplay') === 'playing') startShareCueProgress(progress, remainingChapter);
- var generation = ++storyPlaybackGeneration;
- storyBeatStartedAt = Date.now();
- storyBeatTimer = setTimeout(function () {
- if (!playing || generation !== storyPlaybackGeneration) return;
- storyBeatTimer = null;
- storyBeatStartedAt = 0;
- storyBeatElapsedMs = 0;
- if (storyBeatIndex < total - 1) {
- setStoryBeat(storyBeatIndex + 1, { pulse: true, follow: true });
- scheduleStoryPlayback();
- } else finishStoryChapter();
- }, remainingDwell);
- return true;
- }
-
- function startPlayback() {
- if (!storyAutomaticPlaybackAllowed()) {
- renderPlayback();
- return false;
- }
- autoplayPending = false;
- setAutoplayState(null);
- setShareCueProgress(0);
- document.documentElement.removeAttribute('data-share-playback');
- storyPlaybackScope = 'story';
- if (storyPlaybackComplete) showAll({ updateUrl: false });
- storyPlaybackComplete = false;
- if (activeIndex < 0) activate(0, { playback: true });
- playing = true;
- renderPlayback();
- afterHandoff(function () {
- if (!playing) return;
- if (storyBeatIndex >= 0) followStoryStep(storySteps[storyBeatIndex]);
- scheduleStoryPlayback();
- });
- return true;
- }
-
- function startCurrentViewPlayback() {
- if (document.hidden) return false;
- autoplayPending = false;
- document.documentElement.setAttribute('data-share-playback', 'true');
- storyPlaybackScope = 'chapter';
- storyPlaybackComplete = false;
- if (activeIndex < 0) activate(0, { playback: true, updateUrl: false });
- if (!storyAutomaticPlaybackAllowed()) {
- var preserveMoment = hashBeatMatchesCurrent();
- setShareCueProgress(preserveMoment && storySteps.length ? (storyBeatIndex + 1) / storySteps.length : 1);
- setAutoplayState('reduced-motion');
- playing = false;
- if (preserveMoment) {
- clearStoryPulse();
- renderShareCue();
- } else settleStoryBeats();
- renderPlayback();
- return false;
}
- playing = true;
- setAutoplayState('playing');
- renderPlayback();
- afterHandoff(function () {
- if (!playing) return;
- if (storyBeatIndex >= 0) followStoryStep(storySteps[storyBeatIndex]);
- scheduleStoryPlayback();
- });
- return true;
- }
-
- function maybeStartSharePlayback() {
- if (!autoplayPending || document.hidden) return false;
- return startCurrentViewPlayback();
- }
-
- function togglePlayback() {
- return playing ? (pausePlayback(), false) : startPlayback();
- }
-
- function syncStoryControlsDisabled() {
- Array.prototype.forEach.call(trail.querySelectorAll('[data-story-node]'), function (stop) {
- stop.disabled = !!currentHandoff;
- });
- syncBeatLink();
- }
-
- function selectStoryBeat(index) {
- if (currentHandoff || activeIndex < 0 || index < 0 || index >= storySteps.length) return false;
- momentRestoreGeneration += 1;
- clearChapterPreview({ clearIntents: true });
- if (playing) pausePlayback();
- else {
- stopStoryBeatTimer();
- clearStoryPulse();
- }
- storyPlaybackComplete = false;
- storyPlaybackScope = 'story';
- storyBeatElapsedMs = 0;
- storyBeatDwellMs = storyBeatDwell(storySteps.length);
- var view = views[activeIndex];
- Archify.focus.setMany(view.focus, {
- toggle: false,
- updateUrl: false,
- mode: 'selection',
- label: view.label,
- hideChip: true
- });
- setStoryBeat(index, { manual: true, center: true, pulse: true, follow: true });
- resetProgress(index / storySteps.length);
- renderPlayback();
- return true;
- }
-
- function selectStoryBeatById(id, options) {
- options = options || {};
- var index = storySteps.findIndex(function (step) { return step.nodeId === id; });
- if (index < 0) return false;
- if (playing) pausePlayback();
- else {
- stopStoryBeatTimer();
- clearStoryPulse();
- }
- storyPlaybackComplete = false;
- storyPlaybackScope = 'story';
- storyBeatElapsedMs = 0;
- storyBeatDwellMs = storyBeatDwell(storySteps.length);
- setStoryBeat(index, {
- manual: false,
- center: true,
- pulse: false,
- follow: options.follow === true,
- linked: options.linked === true,
- followInstant: options.followInstant === true
- });
- resetProgress(index / storySteps.length);
- renderPlayback();
return true;
}
-
- function updateUrl(view) {
- try {
- history.replaceState(null, '', location.pathname + location.search + (view ? '#view=' + encodeURIComponent(view.id) : ''));
- } catch (_) {}
+ function schedule(node) {
+ if (enterTimer) window.clearTimeout(enterTimer);
+ enterTimer = window.setTimeout(function () {
+ enterTimer = null;
+ if (hoveredNode === node) show(node.getAttribute('data-node-id'), { announce: false });
+ }, reducedMotion() ? 0 : 90);
}
-
- function showAll(options) {
- options = options || {};
- if (options.restore !== true) momentRestoreGeneration += 1;
- clearChapterPreview({ clearIntents: true });
- if (playing && options.playback !== true) pausePlayback();
- if (options.playback !== true) storyPlaybackComplete = false;
- cancelHandoff('overview');
- activeIndex = -1;
- renderStoryTrail(null);
- if (options.clearFocus !== false) Archify.focus.clear({ updateUrl: false });
- if (options.resetView !== false && Archify.view && typeof Archify.view.reset === 'function') {
- Archify.view.reset({ automatic: true });
+ function sync() {
+ var candidate = focusedNode || hoveredNode;
+ if (!candidate) {
+ clear();
+ return;
}
- render();
- if (options.updateUrl !== false) updateUrl(null);
- }
-
- function activate(index, options) {
- options = options || {};
- if (options.restore !== true) momentRestoreGeneration += 1;
- if (playing && options.playback !== true) pausePlayback();
- if (options.playback !== true) storyPlaybackComplete = false;
- if (index < 0) { showAll(options); return true; }
- if (index >= views.length) return false;
- clearChapterPreview({ clearIntents: true });
- var previousIndex = activeIndex;
- var previous = previousIndex >= 0 ? views[previousIndex] : null;
- var outgoingBeatIndex = storyBeatIndex;
- cancelHandoff('replaced');
- activeIndex = index;
- var view = views[index];
- Archify.focus.setMany(view.focus, {
- toggle: false,
- updateUrl: false,
- mode: 'selection',
- label: view.label,
- hideChip: true
- });
- renderStoryTrail(view);
- beginHandoff(previousIndex, index, previous, view, outgoingBeatIndex, options.playback === true ? 'playback' : 'guided');
- syncStoryControlsDisabled();
- render();
- if (options.updateUrl !== false) updateUrl(view);
- return true;
- }
-
- function activateById(id, options) {
- var index = views.findIndex(function (view) { return view.id === id; });
- return index >= 0 && activate(index, options);
+ show(candidate.getAttribute('data-node-id'), { announce: candidate === focusedNode });
}
- prev.addEventListener('click', function () { activate(activeIndex - 1); });
- next.addEventListener('click', function () { activate(activeIndex + 1); });
- all.addEventListener('click', function () { showAll(); });
- play.addEventListener('click', togglePlayback);
- beatLink.addEventListener('click', copyStoryMomentLink);
- trail.addEventListener('focusin', function (event) {
- if (!event.target.closest('[data-story-node]')) return;
- if (playing) pausePlayback();
- });
- trail.addEventListener('click', function (event) {
- var stop = event.target.closest('[data-story-node]');
- if (!stop || stop.disabled) return;
- selectStoryBeat(Number(stop.getAttribute('data-story-index')));
- });
- trail.addEventListener('keydown', function (event) {
- if (event.key !== 'Enter' && event.key !== ' ') return;
- var stop = event.target.closest('[data-story-node]');
- if (!stop || stop.disabled) return;
- event.preventDefault();
- selectStoryBeat(Number(stop.getAttribute('data-story-index')));
- });
- chapterList.addEventListener('click', function (event) {
- var button = event.target.closest('[data-guided-view-id]');
- if (!button) return;
- activateById(button.getAttribute('data-guided-view-id'));
- });
- chapterList.addEventListener('pointerover', function (event) {
- var button = event.target.closest('[data-guided-view-id]');
- if (!button || !hoverCapable() || event.pointerType === 'touch') return;
- if (event.relatedTarget && button.contains(event.relatedTarget)) return;
- setChapterPreviewIntent('pointer', chapterButtons.indexOf(button));
- });
- chapterList.addEventListener('pointerout', function (event) {
- var button = event.target.closest('[data-guided-view-id]');
- if (!button || (event.relatedTarget && button.contains(event.relatedTarget))) return;
- clearChapterPreviewIntent('pointer', chapterButtons.indexOf(button));
- });
- chapterIndex.addEventListener('focusin', function (event) {
- var button = event.target.closest('[data-guided-view-id]');
- if (!button) return;
- setChapterPreviewIntent('focus', chapterButtons.indexOf(button));
+ svg.addEventListener('pointerover', function (event) {
+ var node = event.target.closest('[data-node-id]');
+ if (!node || !finePointer() || event.pointerType === 'touch') return;
+ if (event.relatedTarget && node.contains(event.relatedTarget)) return;
+ hoveredNode = node;
+ schedule(node);
});
- chapterIndex.addEventListener('focusout', function (event) {
- var button = event.target.closest('[data-guided-view-id]');
- if (!button || (event.relatedTarget && button.contains(event.relatedTarget))) return;
- clearChapterPreviewIntent('focus', chapterButtons.indexOf(button));
+ svg.addEventListener('pointerout', function (event) {
+ var node = event.target.closest('[data-node-id]');
+ if (!node || (event.relatedTarget && node.contains(event.relatedTarget))) return;
+ if (hoveredNode === node) hoveredNode = null;
+ sync();
});
- chapterList.addEventListener('keydown', function (event) {
- var button = event.target.closest('[data-guided-view-id]');
- var index = chapterButtons.indexOf(button);
- if (index < 0) return;
- var target = null;
- if (event.key === 'ArrowRight') target = Math.min(chapterButtons.length - 1, index + 1);
- else if (event.key === 'ArrowLeft') target = Math.max(0, index - 1);
- else if (event.key === 'Home') target = 0;
- else if (event.key === 'End') target = chapterButtons.length - 1;
- if (target === null) return;
- event.preventDefault();
- focusChapterButton(target);
+ svg.addEventListener('focusin', function (event) {
+ var node = event.target.closest('[data-node-id]');
+ if (!node) return;
+ focusedNode = node;
+ show(node.getAttribute('data-node-id'), { announce: true });
});
-
- // A direct node exploration supersedes the curated view. Capture phase
- // releases the view before the focus explorer writes its own deep link.
- function releaseForNode(event) {
- if (activeIndex >= 0 && event.target.closest('[data-node-id]')) {
- pausePlayback();
- showAll({ clearFocus: false, updateUrl: false });
- }
- }
- svg.addEventListener('click', releaseForNode, true);
- svg.addEventListener('keydown', function (event) {
- if (event.key === 'Enter' || event.key === ' ') releaseForNode(event);
- }, true);
-
- document.addEventListener('keydown', function (event) {
- if (event.metaKey || event.ctrlKey || event.altKey) return;
- if (document.documentElement.getAttribute('data-guide-open') === 'true') return;
- var target = event.target;
- if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) return;
- if (event.key === ']') {
- event.preventDefault();
- if (activeIndex < views.length - 1) activate(activeIndex + 1);
- } else if (event.key === '[') {
- event.preventDefault();
- if (activeIndex >= 0) activate(activeIndex - 1);
- } else if (event.key.toLowerCase() === 'p') {
- event.preventDefault();
- togglePlayback();
- } else if (event.key === 'Escape' && activePreviewIndex >= 0) {
- event.preventDefault();
- event.stopImmediatePropagation();
- clearChapterPreview({ clearIntents: true });
- } else if (event.key === 'Escape' && activeIndex >= 0) {
- event.preventDefault();
- showAll();
- }
- }, true);
-
- function syncViewFromHash() {
- try {
- var params = new URLSearchParams(location.hash.replace(/^#/, ''));
- var initial = params.get('view');
- var requestedBeat = params.get('beat');
- var restoreGeneration = ++momentRestoreGeneration;
- if (initial) {
- var activated = activateById(initial, { updateUrl: false, restore: true });
- if (!activated) {
- showAll({ updateUrl: false, restore: true });
- return;
- }
- if (requestedBeat) afterHandoff(function () {
- if (restoreGeneration !== momentRestoreGeneration) return;
- var latest = new URLSearchParams(location.hash.replace(/^#/, ''));
- if (latest.get('view') !== initial || latest.get('beat') !== requestedBeat) return;
- if (activeIndex < 0 || views[activeIndex].id !== initial) return;
- selectStoryBeatById(requestedBeat, { linked: true, follow: true, followInstant: true });
- });
- } else if (params.get('focus') || params.get('relation')) {
- activeIndex = -1;
- render();
- } else {
- showAll({ updateUrl: false, restore: true });
- }
- } catch (_) { showAll({ updateUrl: false, restore: true }); }
- }
-
- window.addEventListener('hashchange', syncViewFromHash);
- document.addEventListener('visibilitychange', function () {
- if (document.hidden) {
- clearChapterPreview({ clearIntents: true });
- if (playing) pausePlayback();
- else clearStoryPulse();
- settleHandoff('hidden');
- }
- else if (!document.hidden) maybeStartSharePlayback();
+ svg.addEventListener('focusout', function (event) {
+ var node = event.target.closest('[data-node-id]');
+ if (!node || (event.relatedTarget && node.contains(event.relatedTarget))) return;
+ if (focusedNode === node) focusedNode = null;
+ sync();
});
- if (window.matchMedia) {
- var guidedMotionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
- var onGuidedMotionChange = function () {
- if (guidedMotionQuery.matches) {
- if (playing) pausePlayback();
- else clearStoryPulse();
- settleHandoff('reduced-motion');
- }
- };
- if (typeof guidedMotionQuery.addEventListener === 'function') guidedMotionQuery.addEventListener('change', onGuidedMotionChange);
- else if (typeof guidedMotionQuery.addListener === 'function') guidedMotionQuery.addListener(onGuidedMotionChange);
- }
- if (document.documentElement.getAttribute('data-embed') !== 'true' && typeof MutationObserver !== 'undefined' && typeof Node !== 'undefined' && svg instanceof Node && document.documentElement instanceof Node) {
- var chapterPreviewOwnerObserver = new MutationObserver(syncChapterPreview);
- chapterPreviewOwnerObserver.observe(svg, {
- attributes: true,
- attributeFilter: [
- 'data-route-picking', 'data-route-active', 'data-lens-active',
- 'data-legend-preview-active', 'data-relationship-preview-active',
- 'data-intent-trace-active', 'data-focus-active'
- ]
- });
- var storyMotionObserver = new MutationObserver(function () {
- if (document.documentElement.getAttribute('data-motion') !== 'live') clearStoryPulse();
- renderPlayback();
- });
- storyMotionObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-motion'] });
- }
- if (typeof ResizeObserver === 'function') {
- var chapterViewportWidth = chapterList.clientWidth;
- var trailViewportWidth = trail.clientWidth;
- var storyScrollObserver = new ResizeObserver(function () {
- var chapterWidth = chapterList.clientWidth;
- var trailWidth = trail.clientWidth;
- if (chapterWidth !== chapterViewportWidth && activeIndex >= 0) centerChapterButton(chapterButtons[activeIndex]);
- if (trailWidth !== trailViewportWidth && storyBeatIndex >= 0) {
- centerStoryStop(trail.querySelectorAll('[data-story-node]')[storyBeatIndex]);
- }
- chapterViewportWidth = chapterWidth;
- trailViewportWidth = trailWidth;
- });
- storyScrollObserver.observe(chapterList);
- storyScrollObserver.observe(trail);
- }
- window.addEventListener('beforeprint', function () {
- clearChapterPreview({ clearIntents: true });
- if (playing) pausePlayback();
- else clearStoryPulse();
- settleHandoff('print');
+ container.addEventListener('pointerdown', function (event) {
+ if (!event.target.closest('[data-node-id]')) clear({ announce: false });
});
- syncViewFromHash();
- if (autoplayPending) {
- requestAnimationFrame(function () {
- requestAnimationFrame(maybeStartSharePlayback);
- });
- }
+ window.addEventListener('blur', function () { clear({ announce: false }); });
return {
- count: views.length,
- activate: activateById,
- showAll: showAll,
- play: startPlayback,
- playCurrent: startCurrentViewPlayback,
- pause: pausePlayback,
- beatLink: storyMomentLink,
- copyBeatLink: copyStoryMomentLink,
- cancelHandoff: cancelHandoff,
- settleHandoff: settleHandoff,
- clearPreview: function () { return clearChapterPreview({ clearIntents: true }); },
- isPlaying: function () { return playing; },
- handoff: function () {
- return currentHandoff ? { id: currentHandoff.id, mode: currentHandoff.mode, anchor: currentHandoff.anchor || null } : null;
- },
- active: function () { return activeIndex < 0 ? null : views[activeIndex].id; },
- preview: function () { return activePreviewIndex < 0 ? null : views[activePreviewIndex].id; },
- delta: function (id) {
- var index = views.findIndex(function (view) { return view.id === id; });
- if (index < 0) return null;
- var value = chapterDelta(activeIndex >= 0 ? views[activeIndex] : null, views[index]);
- return { stay: value.stay.slice(), enter: value.enter.slice(), leave: value.leave.slice() };
- },
- beat: function () {
- var step = storyBeatIndex >= 0 ? storySteps[storyBeatIndex] : null;
- return step ? {
- index: step.index,
- position: step.index + 1,
- total: storySteps.length,
- nodeId: step.nodeId,
- relation: step.relation,
- edgeKeys: step.edgeKeys.slice()
- } : null;
- },
- focus: function () { return activeIndex < 0 ? [] : views[activeIndex].focus.slice(); }
+ show: show,
+ clear: clear,
+ active: function () { return activeId; }
};
})();
/* ============================================================
Adaptive Reader Shell — one diagram across laptop and monitor.
The canonical SVG and viewBox never change. On ordinary desktop pages,
- wide diagrams receive only enough outer width to use the available
+ wide diagrams and measured intrinsic tall workflows receive enough outer width to use the available
height without pushing summary cards below the viewport. Mobile,
- embed, presentation, print, and non-wide diagrams retain their own
+ embed, presentation, print, and unmarked non-wide diagrams retain their own
established layout contracts.
============================================================ */
+ // Fixed script-private participants; the CLI sees only the joint wait below.
+ const archifyLayoutOwners = { reader: null, viewerChrome: null };
Archify.waitForStableLayout = function (options) {
options = options || {};
var maximumFrames = Math.max(1, Number(options.maximumFrames) || 240);
@@ -11074,18 +9506,54 @@
MCP and trust
var diagram = document.querySelector('.diagram-container');
var svg = diagram && diagram.querySelector(':scope > svg');
var header = shell && shell.querySelector('.header');
- var guided = shell && shell.querySelector('.guided-views');
var cards = shell && shell.querySelector('.cards');
var viewBox = svg && svg.viewBox && svg.viewBox.baseVal;
var ratio = viewBox && viewBox.height > 0 ? viewBox.width / viewBox.height : 0;
+ var measuredHeightFit = svg && svg.getAttribute('data-reader-fit') === 'intrinsic-height';
var frame = 0;
var settleFrame = 0;
var lastWidth = 0;
+ // Widest reader the overflow settle has accepted for this viewport (0 =
+ // uncapped). Card copy rewraps as the reader narrows, so recomputing
+ // from fixed heights alone would widen again and oscillate; only a
+ // resize lifts the cap.
+ var settledCap = 0;
var WIDE_RATIO = 1.55;
var MIN_DESKTOP_WIDTH = 1024;
var MIN_READER_WIDTH = 960;
var MAX_READER_WIDTH = 1920;
+ var MIN_PROJECTED_NODE_TEXT_PX = 6;
+ var declaredMinimumText = svg ? parseFloat(svg.getAttribute('data-reader-min-text') || '') : null;
+ var requestedMinimumText = Number.isFinite(declaredMinimumText)
+ ? Math.max(MIN_PROJECTED_NODE_TEXT_PX, declaredMinimumText)
+ : MIN_PROJECTED_NODE_TEXT_PX;
+ var declaredPrimaryText = svg ? parseFloat(svg.getAttribute('data-reader-primary-text') || '') : null;
var SAFE_BOTTOM_GAP = 12;
+ // Wide viewports move summary cards beside the diagram, so the height
+ // budget belongs to the diagram and spare width holds the notes.
+ var RAIL_MIN_VIEWPORT = 1280;
+ var RAIL_WIDTH = 288;
+ var RAIL_GAP = 20;
+ // A default rail must leave primary node labels comfortably readable;
+ // otherwise it starts collapsed and the reader can still open it.
+ var RAIL_COMFORT_PRIMARY_PX = 12;
+ var railPanel = shell && shell.querySelector('.reader-rail');
+ var railReveal = document.getElementById('rail-reveal');
+ var railCollapse = document.getElementById('rail-collapse');
+ var railPlacement = document.getElementById('rail-placement');
+ var RAIL_COLLAPSED_KEY = 'archify-rail-collapsed';
+ var RAIL_PLACEMENT_KEY = 'archify-rail-placement';
+ // Memory is the page's source of truth; storage only carries the choice
+ // to later artifacts, so a blocked or full localStorage cannot freeze it.
+ var preferences = Object.create(null);
+ function readPreference(key) {
+ if (key in preferences) return preferences[key];
+ try { return localStorage.getItem(key); } catch (_) { return null; }
+ }
+ function writePreference(key, value) {
+ preferences[key] = value;
+ try { localStorage.setItem(key, value); } catch (_) {}
+ }
if (diagram && ratio >= WIDE_RATIO) {
diagram.setAttribute('data-wide-diagram', 'true');
@@ -11104,9 +9572,58 @@
MCP and trust
var style = window.getComputedStyle(element);
return element.getBoundingClientRect().height + number(style.marginTop) + number(style.marginBottom);
}
+ // Smallest scale that keeps node text at the requested floor and
+ // context relationship labels readable. A declared wide reader holds
+ // relationship labels to the requested floor too; every other reader
+ // still keeps them at the 6px hard floor the browser gate enforces, so
+ // a first-screen fit can never push them below it.
+ function minimumReadableScale() {
+ var nodeMinimum = null;
+ var edgeMinimum = null;
+ var selectors = 'text[data-node-label], text[data-boundary-label], text[data-detail="context"], g[data-detail="context"] text';
+ Array.from(svg.querySelectorAll(selectors)).forEach(function (text) {
+ if (text.getAttribute('data-detail') === 'fine' || text.closest('[data-detail="fine"]')) return;
+ var sourceFontPx = parseFloat(text.getAttribute('font-size') || '');
+ if (!Number.isFinite(sourceFontPx)) return;
+ var primary = text.hasAttribute('data-node-label') || text.hasAttribute('data-boundary-label');
+ var context = text.getAttribute('data-detail') === 'context' || Boolean(text.closest('g[data-detail="context"]'));
+ if (!primary && context && text.closest('[data-edge-from][data-edge-to]')) {
+ edgeMinimum = edgeMinimum == null ? sourceFontPx : Math.min(edgeMinimum, sourceFontPx);
+ } else if (primary || text.closest('[data-node-id]')) {
+ nodeMinimum = nodeMinimum == null ? sourceFontPx : Math.min(nodeMinimum, sourceFontPx);
+ }
+ });
+ var edgeTarget = measuredHeightFit && ratio >= WIDE_RATIO && Number.isFinite(declaredMinimumText)
+ ? requestedMinimumText
+ : MIN_PROJECTED_NODE_TEXT_PX;
+ var scale = 0;
+ if (nodeMinimum != null) scale = Math.max(scale, requestedMinimumText / nodeMinimum);
+ if (edgeMinimum != null) scale = Math.max(scale, edgeTarget / edgeMinimum);
+ return scale > 0 ? Math.min(1, scale) : 1;
+ }
+ var sourcePrimary = null;
+ if (svg) Array.from(svg.querySelectorAll('text[data-node-label]')).forEach(function (text) {
+ var size = parseFloat(text.getAttribute('font-size') || '');
+ if (Number.isFinite(size) && size > 0) sourcePrimary = sourcePrimary == null ? size : Math.max(sourcePrimary, size);
+ });
+ // SVG width at which the largest node label renders at targetPx. Every
+ // renderer writes label font sizes, so the rail comfort check works for
+ // all diagram types; only the renderer-declared floor needs metadata.
+ function labelWidth(targetPx) {
+ return sourcePrimary == null || !viewBox ? 0 : viewBox.width * targetPx / sourcePrimary;
+ }
+ function primaryReadingWidth() {
+ if (!measuredHeightFit || !Number.isFinite(declaredPrimaryText) || declaredPrimaryText <= 0) return 0;
+ // Primary labels should remain comfortable to read when cards or
+ // auxiliary rows make a one-screen fit too small. Ordinary page
+ // scroll preserves that reading size; viewport width still caps it.
+ // A long title may already use a smaller fitted font. Preserve that
+ // hierarchy rather than enlarging every other node to compensate.
+ return labelWidth(declaredPrimaryText);
+ }
function eligible() {
return Boolean(
- shell && diagram && svg && ratio >= WIDE_RATIO &&
+ shell && diagram && svg && (ratio >= WIDE_RATIO || measuredHeightFit) &&
window.innerWidth >= MIN_DESKTOP_WIDTH &&
html.getAttribute('data-embed') !== 'true' &&
html.getAttribute('data-present') !== 'true' &&
@@ -11115,9 +9632,56 @@
MCP and trust
}
function clear() {
html.style.removeProperty('--archify-reader-width');
+ html.style.removeProperty('--archify-diagram-max-width');
+ html.removeAttribute('data-reader-narrow');
html.removeAttribute('data-reader-layout');
html.removeAttribute('data-reader-overflow');
+ setRail(false);
lastWidth = 0;
+ settledCap = 0;
+ }
+ // Rail modes: "true" docks beside the diagram, "collapsed" leaves only the
+ // reveal control, "overlay" opens a drawer when docking would break the
+ // readable floor, and "bottom" stacks notes and index below the diagram.
+ function setRail(mode) {
+ if (mode) {
+ html.setAttribute('data-reader-rail', mode);
+ html.style.setProperty('--archify-rail-width', RAIL_WIDTH + 'px');
+ html.style.setProperty('--archify-rail-gap', RAIL_GAP + 'px');
+ } else {
+ html.removeAttribute('data-reader-rail');
+ html.style.removeProperty('--archify-rail-width');
+ html.style.removeProperty('--archify-rail-gap');
+ }
+ if (railReveal) railReveal.hidden = mode !== 'collapsed';
+ if (railCollapse) {
+ railCollapse.hidden = mode !== 'true' && mode !== 'overlay';
+ railCollapse.setAttribute('aria-expanded', String(mode === 'true' || mode === 'overlay'));
+ }
+ if (railPlacement) {
+ railPlacement.hidden = !mode || mode === 'collapsed' || (mode === 'bottom' && window.innerWidth < RAIL_MIN_VIEWPORT);
+ railPlacement.setAttribute('data-placement', mode === 'bottom' ? 'bottom' : 'right');
+ railPlacement.setAttribute('aria-label', viewerText(mode === 'bottom' ? 'viewer.rail.right' : 'viewer.rail.bottom'));
+ railPlacement.title = railPlacement.getAttribute('aria-label');
+ }
+ }
+ var outline = document.getElementById('node-outline');
+ function hasCards() {
+ return Boolean((cards && cards.children.length && !cards.hidden) || (outline && !outline.hidden));
+ }
+ // Bottom notes and index are reading material below the fold: the first
+ // screen belongs to the interactive diagram and its controls.
+ function belowFold() {
+ return html.getAttribute('data-reader-rail') === 'bottom' ? outerHeight(railPanel) : 0;
+ }
+ // A docked rail may run past a short diagram down to the viewport floor,
+ // so the index uses that space instead of scrolling inside the diagram's
+ // height; it never pushes the page into overflow.
+ function fitDockedRail() {
+ if (html.getAttribute('data-reader-rail') !== 'true' || !railPanel) return;
+ var top = railPanel.getBoundingClientRect().top + window.scrollY;
+ var floor = window.innerHeight - number(window.getComputedStyle(body).paddingBottom) - top;
+ html.style.setProperty('--archify-rail-max', Math.max(diagram.getBoundingClientRect().height, floor) + 'px');
}
function chromeMetrics() {
var bodyStyle = window.getComputedStyle(body);
@@ -11131,11 +9695,25 @@
MCP and trust
number(diagramStyle.borderTopWidth) + number(diagramStyle.borderBottomWidth)
};
}
- function applyWidth(width) {
- var rounded = Math.round(width);
+ // `width` is the diagram's reading width. The page shell never narrows
+ // below the desktop reader floor, so a narrow, tall diagram keeps a
+ // usable header, toolbar, and controls; its SVG is centred instead.
+ function applyWidth(width, minWidth) {
+ var rounded = Math.max(Math.ceil(minWidth || 0), Math.round(width));
if (Math.abs(rounded - lastWidth) < 1) return false;
lastWidth = rounded;
- html.style.setProperty('--archify-reader-width', rounded + 'px');
+ var shellFloor = Math.min(MIN_READER_WIDTH, Math.max(0, window.innerWidth - chromeMetrics().bodyX));
+ html.style.setProperty('--archify-reader-width', Math.max(rounded, shellFloor) + 'px');
+ if (rounded < shellFloor) {
+ // `rounded` already includes a docked rail and its gap; the SVG cap
+ // is only the diagram's share, not the space beside it.
+ var railShare = html.getAttribute('data-reader-rail') === 'true' ? RAIL_WIDTH + RAIL_GAP : 0;
+ html.style.setProperty('--archify-diagram-max-width', Math.max(1, rounded - railShare - chromeMetrics().diagramX) + 'px');
+ html.setAttribute('data-reader-narrow', 'true');
+ } else {
+ html.style.removeProperty('--archify-diagram-max-width');
+ html.removeAttribute('data-reader-narrow');
+ }
html.setAttribute('data-reader-layout', 'adaptive');
return true;
}
@@ -11144,12 +9722,14 @@
MCP and trust
settleFrame = requestAnimationFrame(function () {
settleFrame = 0;
if (!eligible() || !lastWidth) return;
+ fitDockedRail();
var overflow = Math.max(
document.documentElement.scrollHeight,
document.body.scrollHeight
- ) - window.innerHeight;
- if (overflow > 1 && lastWidth > minWidth) {
- applyWidth(Math.max(minWidth, lastWidth - overflow * ratio - 4));
+ ) - window.innerHeight - belowFold();
+ if (overflow > 1 && lastWidth > Math.ceil(minWidth)) {
+ applyWidth(Math.max(minWidth, lastWidth - overflow * ratio - 4), minWidth);
+ settledCap = lastWidth;
html.setAttribute('data-reader-overflow', 'reduced');
} else if (overflow > 1) {
html.setAttribute('data-reader-overflow', 'authored');
@@ -11166,18 +9746,59 @@
MCP and trust
}
var chrome = chromeMetrics();
var viewportCap = Math.max(0, window.innerWidth - chrome.bodyX);
- var minWidth = Math.min(MIN_READER_WIDTH, viewportCap);
+ var readableWidth = viewBox && viewBox.width > 0
+ ? viewBox.width * minimumReadableScale() + chrome.diagramX
+ : MIN_READER_WIDTH;
var maxWidth = Math.min(MAX_READER_WIDTH, viewportCap);
+ var readableMinimumWidth = measuredHeightFit && ratio < WIDE_RATIO
+ ? readableWidth
+ : measuredHeightFit && ratio >= WIDE_RATIO && Number.isFinite(declaredMinimumText)
+ ? Math.max(MIN_READER_WIDTH, readableWidth)
+ : MIN_READER_WIDTH;
+ var minWidth;
+ if (measuredHeightFit && ratio < WIDE_RATIO) {
+ minWidth = Math.min(readableMinimumWidth, viewportCap);
+ } else if (measuredHeightFit && ratio >= WIDE_RATIO && Number.isFinite(declaredMinimumText)) {
+ minWidth = Math.min(readableMinimumWidth, maxWidth);
+ } else {
+ minWidth = Math.min(readableMinimumWidth, viewportCap);
+ }
+ var primaryWidth = primaryReadingWidth();
+ var railExtra = RAIL_WIDTH + RAIL_GAP;
+ var mode = null;
+ // Notes default below the diagram; the right rail is the reader's
+ // opt-in and needs a wide viewport.
+ if (hasCards() && (readPreference(RAIL_PLACEMENT_KEY) !== 'right' || window.innerWidth < RAIL_MIN_VIEWPORT)) {
+ mode = 'bottom';
+ } else if (hasCards()) {
+ var fitsReadable = readableMinimumWidth + railExtra <= maxWidth;
+ var comfortWidth = labelWidth(RAIL_COMFORT_PRIMARY_PX);
+ var comfortable = fitsReadable && (!comfortWidth || comfortWidth + chrome.diagramX + railExtra <= maxWidth);
+ var collapsedPreference = readPreference(RAIL_COLLAPSED_KEY);
+ if (collapsedPreference === '1' || (collapsedPreference !== '0' && !comfortable)) mode = 'collapsed';
+ else mode = fitsReadable ? 'true' : 'overlay';
+ }
+ // With notes below the fold, the first screen belongs to the whole
+ // diagram and its controls: it may shrink past the comfortable primary
+ // size down to the renderer's readable text floor, and zoom restores
+ // detail. Only a graph taller than that floor allows still scrolls.
+ if (mode === 'bottom') primaryWidth = 0;
+ if (primaryWidth > 0) minWidth = Math.max(minWidth, Math.min(maxWidth, primaryWidth + chrome.diagramX));
+ var docked = mode === 'true';
+ setRail(mode);
+ chrome = chromeMetrics();
+ if (docked) minWidth = Math.min(maxWidth, minWidth + railExtra);
+ var stackedBelow = mode === 'bottom' ? 0 : docked ? 0 : outerHeight(cards);
var fixedHeight = chrome.bodyY + chrome.diagramY + SAFE_BOTTOM_GAP +
- outerHeight(header) + outerHeight(guided) + outerHeight(cards);
+ outerHeight(header) + stackedBelow;
var availableSvgHeight = Math.max(1, window.innerHeight - fixedHeight);
- var desiredWidth = availableSvgHeight * ratio + chrome.diagramX;
- var width = Math.max(minWidth, Math.min(maxWidth, desiredWidth));
- applyWidth(width);
+ var desiredWidth = availableSvgHeight * ratio + chrome.diagramX + (docked ? railExtra : 0);
+ var width = Math.max(minWidth, Math.min(maxWidth, desiredWidth, settledCap || desiredWidth));
+ applyWidth(width, minWidth);
settleOverflow(minWidth);
return {
ratio: ratio,
- width: Math.round(width),
+ width: lastWidth,
availableSvgHeight: Math.round(availableSvgHeight),
fixedHeight: Math.round(fixedHeight)
};
@@ -11203,28 +9824,58 @@
MCP and trust
Math.round(diagramRect.height * 100) / 100
].join('|');
}
+ function layoutPending() { return Boolean(frame || settleFrame); }
function whenStable() {
return Archify.waitForStableLayout({
schedule: schedule,
- pending: function () { return Boolean(frame || settleFrame); },
+ pending: layoutPending,
snapshot: stableSnapshot,
timeoutMessage: 'Adaptive reader layout did not reach stable dimensions.'
});
}
+ archifyLayoutOwners.reader = { schedule: schedule, pending: layoutPending, snapshot: stableSnapshot };
- window.addEventListener('resize', schedule, { passive: true });
+ window.addEventListener('resize', function () {
+ settledCap = 0;
+ schedule();
+ }, { passive: true });
window.addEventListener('load', schedule, { once: true });
if (document.fonts && document.fonts.ready) document.fonts.ready.then(schedule).catch(function () {});
if (typeof ResizeObserver === 'function') {
var resizeObserver = new ResizeObserver(schedule);
- [header, guided, cards].forEach(function (element) { if (element) resizeObserver.observe(element); });
+ [header, cards].forEach(function (element) { if (element) resizeObserver.observe(element); });
}
if (typeof MutationObserver === 'function') {
var contentObserver = new MutationObserver(schedule);
- if (guided) contentObserver.observe(guided, { attributes: true, childList: true, subtree: true });
if (cards) contentObserver.observe(cards, { attributes: true, childList: true, subtree: true });
contentObserver.observe(html, { attributes: true, attributeFilter: ['data-embed', 'data-present'] });
}
+ // Reader choices persist across artifacts; a resize-style remeasure
+ // applies them without reloading.
+ function chooseRail(key, value) {
+ writePreference(key, value);
+ settledCap = 0;
+ schedule();
+ }
+ function collapseRail() {
+ chooseRail(RAIL_COLLAPSED_KEY, '1');
+ if (railReveal) requestAnimationFrame(function () { try { railReveal.focus({ preventScroll: true }); } catch (_) {} });
+ }
+ if (railReveal) railReveal.addEventListener('click', function () { chooseRail(RAIL_COLLAPSED_KEY, '0'); });
+ if (railCollapse) railCollapse.addEventListener('click', collapseRail);
+ if (railPlacement) railPlacement.addEventListener('click', function () {
+ var toBottom = html.getAttribute('data-reader-rail') !== 'bottom';
+ if (!toBottom) writePreference(RAIL_COLLAPSED_KEY, '0');
+ chooseRail(RAIL_PLACEMENT_KEY, toBottom ? 'bottom' : 'right');
+ });
+ document.addEventListener('keydown', function (event) {
+ if (event.key === 'Escape' && html.getAttribute('data-reader-rail') === 'overlay') collapseRail();
+ });
+ document.addEventListener('pointerdown', function (event) {
+ if (html.getAttribute('data-reader-rail') !== 'overlay' || !railPanel) return;
+ if (!railPanel.contains(event.target) && !(railReveal && railReveal.contains(event.target))) chooseRail(RAIL_COLLAPSED_KEY, '1');
+ });
+ if (typeof ResizeObserver === 'function' && railPanel) new ResizeObserver(schedule).observe(railPanel);
schedule();
return {
@@ -11379,8 +10030,36 @@
MCP and trust
};
return lastReceipt;
}
+ /* When the stage runs below the viewport, the dock lifts to the viewport
+ floor so its controls stay reachable while reading. Stage-rail
+ decisions and receipts use its resting position, so the lift never
+ feeds back into layout. */
+ var lift = 0;
+ var liftFrame = 0;
+ function restingNavRect() {
+ var rect = nav.getBoundingClientRect();
+ if (!lift) return rect;
+ return { x: rect.x, y: rect.y + lift, left: rect.left, right: rect.right, top: rect.top + lift, bottom: rect.bottom + lift, width: rect.width, height: rect.height };
+ }
+ function updateLift() {
+ liftFrame = 0;
+ var next = 0;
+ if (eligible()) {
+ var rest = restingNavRect();
+ var box = container.getBoundingClientRect();
+ next = Math.round(Math.max(0, Math.min(rest.bottom - (window.innerHeight - 16), rest.top - box.top - 16)));
+ }
+ if (next === lift) return;
+ lift = next;
+ if (lift) container.style.setProperty('--archify-dock-lift', lift + 'px');
+ else container.style.removeProperty('--archify-dock-lift');
+ }
+ function scheduleLift() {
+ if (!liftFrame) liftFrame = requestAnimationFrame(updateLift);
+ }
function measure() {
frame = 0;
+ scheduleLift();
if (probingBaseline) return null;
if (!eligible()) return clear({ preserveBaseline: !cameraAtBaseline() });
@@ -11394,7 +10073,7 @@
MCP and trust
if (reserve === 0 && restorableReserve > 0) {
if (writeReserve(restorableReserve)) return null;
}
- var cameraNavRect = nav.getBoundingClientRect();
+ var cameraNavRect = restingNavRect();
var cameraLegendRect = visible(legend) ? legend.getBoundingClientRect() : null;
var cameraStageRect = protectedStageRect();
var cameraIntersectionArea = usable(cameraLegendRect) && intersectionArea(cameraNavRect, cameraStageRect) > 0
@@ -11414,7 +10093,7 @@
MCP and trust
return lastReceipt;
}
- var navRect = nav.getBoundingClientRect();
+ var navRect = restingNavRect();
var legendRect = visible(legend) ? legend.getBoundingClientRect() : null;
var stageRect = protectedStageRect();
if (!usable(navRect) || !usable(stageRect)) return clear();
@@ -11436,7 +10115,7 @@
MCP and trust
if (writeReserve(reserve + remaining)) return null;
}
- navRect = nav.getBoundingClientRect();
+ navRect = restingNavRect();
legendRect = visible(legend) ? legend.getBoundingClientRect() : null;
stageRect = protectedStageRect();
stageGap = navRect.top - stageRect.bottom;
@@ -11491,7 +10170,7 @@
MCP and trust
}
function stableSnapshot() {
var containerRect = container ? container.getBoundingClientRect() : { width: 0, height: 0 };
- var navRect = nav ? nav.getBoundingClientRect() : { top: 0, left: 0 };
+ var navRect = nav ? restingNavRect() : { top: 0, left: 0 };
var legendRect = legend ? legend.getBoundingClientRect() : { top: 0, left: 0 };
return [
reserve,
@@ -11505,16 +10184,19 @@
MCP and trust
lastReceipt ? lastReceipt.stageGap : ''
].join('|');
}
+ function layoutPending() { return Boolean(frame || settleFrame || probingBaseline); }
function whenStable() {
return Archify.waitForStableLayout({
schedule: schedule,
- pending: function () { return Boolean(frame || settleFrame || probingBaseline); },
+ pending: layoutPending,
snapshot: stableSnapshot,
timeoutMessage: 'Viewer chrome layout did not reach stable dimensions.'
});
}
+ archifyLayoutOwners.viewerChrome = { schedule: schedule, pending: layoutPending, snapshot: stableSnapshot };
window.addEventListener('resize', reprobe, { passive: true });
+ window.addEventListener('scroll', scheduleLift, { passive: true });
window.addEventListener('load', schedule, { once: true });
window.addEventListener('beforeprint', schedule);
window.addEventListener('afterprint', reprobe);
@@ -11525,7 +10207,11 @@
MCP and trust
}
if (typeof MutationObserver === 'function') {
var contentObserver = new MutationObserver(function (records) {
- var viewerModeChanged = records.some(function (record) { return record.target === html; });
+ // A theme switch changes paint, not the stage geometry. Reprobing
+ // drops the bottom rail for several frames and makes the diagram jump.
+ var viewerModeChanged = records.some(function (record) {
+ return record.target === html && record.attributeName !== 'data-theme';
+ });
if (viewerModeChanged) reprobe();
else schedule();
});
@@ -11543,11 +10229,55 @@
MCP and trust
reprobe: reprobe,
whenStable: whenStable,
stageRect: protectedStageRect,
+ dockRect: function () { return nav ? restingNavRect() : null; },
active: function () { return reserve > 0; },
receipt: function () { return lastReceipt || measure(); }
};
})();
+ Archify.layoutStability = {
+ whenStable: function () {
+ var reader = archifyLayoutOwners.reader;
+ var chrome = archifyLayoutOwners.viewerChrome;
+ if (!reader || !chrome) {
+ return Promise.reject(new Error('Joint layout owners are unavailable.'));
+ }
+ var observedFontsReady = document.fonts && document.fonts.ready;
+ var fontCyclePending = false;
+ var fontGeneration = 0;
+ function scheduleOwners() { reader.schedule(); chrome.schedule(); }
+ function fontsPending() {
+ var fonts = document.fonts;
+ var ready = fonts && fonts.ready;
+ if (ready && (ready !== observedFontsReady || (fonts.status === 'loading' && !fontCyclePending))) {
+ observedFontsReady = ready;
+ fontCyclePending = true;
+ var generation = ++fontGeneration;
+ ready.catch(function () {}).then(function () {
+ if (generation !== fontGeneration) return;
+ fontCyclePending = false;
+ scheduleOwners();
+ });
+ }
+ return fontCyclePending || Boolean(fonts && fonts.status === 'loading');
+ }
+ return Archify.waitForStableLayout({
+ maximumFrames: 960,
+ schedule: scheduleOwners,
+ pending: function () {
+ // Recapture readiness for font cycles that begin during sampling.
+ return fontsPending() || reader.pending() || chrome.pending();
+ },
+ snapshot: function () {
+ return JSON.stringify([
+ ['reader', reader.snapshot()],
+ ['viewerChrome', chrome.snapshot()]
+ ]);
+ },
+ timeoutMessage: 'Joint reader and viewer chrome layout did not reach stable dimensions.'
+ });
+ }
+ };
Archify.view = (function () {
var container = document.querySelector('.diagram-container');
var svg = container.querySelector('svg');
@@ -11769,18 +10499,12 @@
MCP and trust
container.removeAttribute('data-camera-transaction');
}
function interruptCamera(reason) {
- if (Archify.guidedViews && Archify.guidedViews.cancelHandoff) {
- Archify.guidedViews.cancelHandoff(reason || 'manual');
- }
var rendered = sampleRenderedState();
stopCameraMotion(reason || 'manual', false);
state = rendered;
state.mode = 'manual';
apply();
renderControls();
- if (Archify.guidedViews && Archify.guidedViews.isPlaying && Archify.guidedViews.isPlaying()) {
- Archify.guidedViews.pause();
- }
if (Archify.routeProbe && Archify.routeProbe.isJourneyPlaying && Archify.routeProbe.isJourneyPlaying()) {
Archify.routeProbe.pauseJourney({ preserveElapsed: true, reason: reason || 'manual' });
}
@@ -11840,8 +10564,8 @@
MCP and trust
return true;
}
function semanticIds(ids, includeNeighbors) {
- var seeds = {};
- var wanted = {};
+ var seeds = Object.create(null);
+ var wanted = Object.create(null);
(ids || []).forEach(function (id) { seeds[id] = true; wanted[id] = true; });
if (includeNeighbors) {
Array.prototype.forEach.call(svg.querySelectorAll('[data-edge-from][data-edge-to]'), function (edge) {
@@ -11875,8 +10599,8 @@
MCP and trust
var maxX = Math.max.apply(Math, boxes.map(function (box) { return box.x + box.width; }));
var maxY = Math.max.apply(Math, boxes.map(function (box) { return box.y + box.height; }));
var bounds = {
- x: contentOffsetX + minX * contentScale,
- y: contentOffsetY + minY * contentScale,
+ x: contentOffsetX + (minX - viewBox.x) * contentScale,
+ y: contentOffsetY + (minY - viewBox.y) * contentScale,
width: Math.max(1, (maxX - minX) * contentScale),
height: Math.max(1, (maxY - minY) * contentScale)
};
@@ -11980,7 +10704,7 @@
MCP and trust
if (!boxes.length || !viewBox || viewBox.width <= 0) return false;
var minX = Math.min.apply(Math, boxes.map(function (box) { return box.x; }));
var maxX = Math.max.apply(Math, boxes.map(function (box) { return box.x + box.width; }));
- var center = ((minX + maxX) / 2 / viewBox.width) * (svg.clientWidth || 1);
+ var center = (((minX + maxX) / 2 - viewBox.x) / viewBox.width) * (svg.clientWidth || 1);
var target = Math.max(0, Math.min(svg.clientWidth - container.clientWidth, center - container.clientWidth / 2));
var transaction = cameraReceipt({ scrollLeft: target }, options);
cameraTransaction = transaction;
@@ -11998,9 +10722,6 @@
MCP and trust
return transaction;
}
function syncSemantic() {
- var guided = Archify.guidedViews && typeof Archify.guidedViews.focus === 'function'
- ? Archify.guidedViews.focus() : [];
- if (guided && guided.length) return reveal(guided, { reason: 'guided-sync' });
var active = Archify.focus && typeof Archify.focus.active === 'function' ? Archify.focus.active() : null;
if (typeof active === 'string') return reveal([active], { includeNeighbors: true, reason: 'focus-sync' });
if (Array.isArray(active) && active.length) return reveal(active, { reason: 'selection-sync' });
@@ -12457,8 +11178,7 @@
MCP and trust
: viewerText('viewer.radar.viewport.scale', { percent: Math.round(visible.scale * 100) }));
status.textContent = viewerText('viewer.radar.status', { count: nodes.length, viewport: viewportCopy });
nodes.forEach(function (item) {
- var active = item.node.hasAttribute('data-focus-selected') ||
- item.node.getAttribute('data-story-beat-state') === 'active';
+ var active = item.node.hasAttribute('data-focus-selected');
if (active) item.rect.setAttribute('data-radar-active', 'true');
else item.rect.removeAttribute('data-radar-active');
});
@@ -12482,6 +11202,7 @@
MCP and trust
if (next) {
clearSpaceRetry();
spaceRetryCount = 0;
+ trigger.setAttribute('data-radar-requested', 'true');
build();
attemptRequestedOpen(options);
} else {
@@ -12494,11 +11215,13 @@
MCP and trust
resetDockingStyles();
restorePassport();
feedback.hidden = true;
+ // Focus before collapsing: the dock hides this trigger at 100% unless it holds focus.
+ if (options.restoreFocus === true) trigger.focus();
trigger.setAttribute('aria-expanded', 'false');
+ trigger.removeAttribute('data-radar-requested');
trigger.removeAttribute('data-radar-space-limited');
trigger.setAttribute('aria-label', viewerText('viewer.nav.radar'));
trigger.title = viewerText('viewer.nav.radar.title');
- if (options.restoreFocus === true) trigger.focus();
}
return next;
}
@@ -12519,9 +11242,6 @@
MCP and trust
function focusNode(id) {
var main = diagram.querySelector('[data-node-id="' + id + '"]');
if (!main) return false;
- if (Archify.guidedViews && typeof Archify.guidedViews.showAll === 'function') {
- Archify.guidedViews.showAll({ clearFocus: false, updateUrl: false });
- }
if (Archify.focus && typeof Archify.focus.set === 'function') {
Archify.focus.set(id, { toggle: false });
}
@@ -12706,8 +11426,8 @@
MCP and trust
/* ============================================================
Presentation Stage — a shareable, viewport-filling live view.
It changes only HTML layout and URL state; SVG semantics and export
- serialization remain untouched. Escape first clears an active guided
- view/focus, then exits the stage.
+ serialization remain untouched. Escape first clears an active
+ focus, then exits the stage.
============================================================ */
Archify.presentation = (function () {
var html = document.documentElement;
@@ -12780,7 +11500,7 @@
MCP and trust
/* ============================================================
Node Finder — stable-ID search over the existing semantic SVG.
Search never changes the IR or SVG geometry: selecting a result resets
- the viewport, releases a guided view, and delegates to semantic focus.
+ the viewport and delegates to semantic focus.
============================================================ */
Archify.finder = (function () {
var html = document.documentElement;
@@ -12902,9 +11622,6 @@
MCP and trust
try { item.node.focus({ preventScroll: true }); } catch (_) { try { item.node.focus(); } catch (_) {} }
return true;
}
- if (Archify.guidedViews && typeof Archify.guidedViews.showAll === 'function') {
- Archify.guidedViews.showAll({ clearFocus: false, updateUrl: false });
- }
if (Archify.view && typeof Archify.view.reset === 'function') Archify.view.reset({ automatic: true });
Archify.focus.set(id, { toggle: false });
if (Archify.view && typeof Archify.view.reveal === 'function') {
@@ -13043,6 +11760,145 @@
MCP and trust
};
})();
+ /* ============================================================
+ Node Outline — a rail index of the semantic nodes, grouped by their
+ authored context. Hover previews neighbors through Intent Trace;
+ selection delegates to the Finder's focus-and-reveal path. It reads
+ the canonical SVG and never changes geometry or export state.
+ ============================================================ */
+ Archify.outline = (function () {
+ var panel = document.getElementById('node-outline');
+ var list = document.getElementById('node-outline-list');
+ var count = document.getElementById('node-outline-count');
+ var svg = document.querySelector('.diagram-container > svg');
+ if (!panel || !list || !svg) return { count: 0 };
+ var TONES = ['frontend', 'backend', 'database', 'cloud', 'security', 'messagebus', 'external'];
+ var nodes = Array.from(svg.querySelectorAll('[data-node-id]')).filter(function (node) {
+ return !node.closest('[data-legend-hit], .legend') && node.getAttribute('data-node-label');
+ });
+ var buttons = Object.create(null);
+
+ function tone(node) {
+ var shape = node.querySelector('[class*="c-"]:not(.c-mask)');
+ var match = shape && /(?:^|\s)c-([a-z]+)/.exec(shape.getAttribute('class') || '');
+ return match && TONES.indexOf(match[1]) !== -1 ? match[1] : 'external';
+ }
+ function preview(id) {
+ if (Archify.intentTrace && typeof Archify.intentTrace.show === 'function') Archify.intentTrace.show(id);
+ }
+ function clearPreview() {
+ if (Archify.intentTrace && typeof Archify.intentTrace.clear === 'function') Archify.intentTrace.clear({ announce: false });
+ }
+ function select(id) {
+ clearPreview();
+ var finder = Archify.finder;
+ if (finder && typeof finder.select === 'function') {
+ if (finder.select(id)) return;
+ var routeMode = Archify.routeProbe && typeof Archify.routeProbe.active === 'function' ? Archify.routeProbe.active() : 'idle';
+ // Only an active endpoint picker owns a refused selection.
+ if (routeMode === 'source' || routeMode === 'target') return;
+ }
+ if (Archify.focus && typeof Archify.focus.set === 'function') Archify.focus.set(id, { toggle: false });
+ }
+
+ var groups = [];
+ var byContext = Object.create(null);
+ nodes.forEach(function (node) {
+ // Group by the outermost context (lane or stage); deeper segments
+ // such as workflow groups and columns would split the index into
+ // one-node sections.
+ var context = (node.getAttribute('data-node-context') || '').split(' \u203a ')[0];
+ if (!byContext[context]) {
+ byContext[context] = { label: context, nodes: [] };
+ groups.push(byContext[context]);
+ }
+ byContext[context].nodes.push(node);
+ });
+
+ groups.forEach(function (group) {
+ var section = document.createElement('li');
+ section.className = 'node-outline-group';
+ if (groups.length > 1 && group.label) {
+ var heading = document.createElement('span');
+ heading.className = 'node-outline-heading';
+ var headingLabel = document.createElement('span');
+ headingLabel.textContent = group.label;
+ var headingCount = document.createElement('small');
+ headingCount.textContent = String(group.nodes.length);
+ heading.appendChild(headingLabel);
+ heading.appendChild(headingCount);
+ section.appendChild(heading);
+ }
+ var items = document.createElement('ul');
+ group.nodes.forEach(function (node) {
+ var id = node.getAttribute('data-node-id');
+ var item = document.createElement('li');
+ var button = document.createElement('button');
+ button.type = 'button';
+ button.className = 'node-outline-item';
+ button.setAttribute('data-outline-node', id);
+ button.style.setProperty('--outline-tone', 'var(--' + tone(node) + '-stroke)');
+ var dot = document.createElement('span');
+ dot.className = 'node-outline-dot';
+ dot.setAttribute('aria-hidden', 'true');
+ var label = document.createElement('strong');
+ label.textContent = node.getAttribute('data-node-label');
+ button.appendChild(dot);
+ button.appendChild(label);
+ var sublabel = node.getAttribute('data-node-sublabel');
+ if (sublabel) {
+ var detail = document.createElement('small');
+ detail.textContent = sublabel;
+ button.appendChild(detail);
+ }
+ button.addEventListener('pointerenter', function () { preview(id); });
+ button.addEventListener('focus', function () { preview(id); });
+ button.addEventListener('pointerleave', clearPreview);
+ button.addEventListener('blur', clearPreview);
+ button.addEventListener('click', function () { select(id); });
+ buttons[id] = button;
+ item.appendChild(button);
+ items.appendChild(item);
+ });
+ section.appendChild(items);
+ list.appendChild(section);
+ });
+
+ var previewing = null;
+ function syncCurrent() {
+ nodes.forEach(function (node) {
+ var button = buttons[node.getAttribute('data-node-id')];
+ if (!button) return;
+ if (node.getAttribute('aria-pressed') === 'true') button.setAttribute('aria-current', 'true');
+ else button.removeAttribute('aria-current');
+ });
+ // Mirror a diagram-side hover so the index answers "where is this?".
+ var active = buttons[svg.getAttribute('data-intent-trace-active') || ''] || null;
+ if (active === previewing) return;
+ if (previewing) previewing.removeAttribute('data-previewing');
+ previewing = active;
+ if (!active) return;
+ active.setAttribute('data-previewing', '');
+ if (!active.matches(':hover') && panel.offsetParent) {
+ var box = list.getBoundingClientRect();
+ var item = active.getBoundingClientRect();
+ if (item.top < box.top || item.bottom > box.bottom - 24) {
+ list.scrollTop += item.top - box.top - box.height / 2 + item.height / 2;
+ }
+ }
+ }
+ if (typeof MutationObserver === 'function') {
+ new MutationObserver(syncCurrent).observe(svg, { attributes: true, subtree: true, attributeFilter: ['aria-pressed', 'data-intent-trace-active'] });
+ }
+ if (count) count.textContent = String(nodes.length);
+ panel.hidden = nodes.length < 2;
+ syncCurrent();
+ if (Archify.readerLayout && typeof Archify.readerLayout.schedule === 'function') Archify.readerLayout.schedule();
+
+ return { count: nodes.length, select: select };
+ })();
+
+
/* ============================================================
Route Probe — shortest directed path over compiled semantics.
The graph is read from renderer-owned stable IDs and relationship
@@ -13309,7 +12165,7 @@
MCP and trust
}
function outgoingByNode() {
var byId = nodesById();
- var outgoing = {};
+ var outgoing = Object.create(null);
edges().forEach(function (edge) {
var from = edge.getAttribute('data-edge-from');
var to = edge.getAttribute('data-edge-to');
@@ -13321,7 +12177,7 @@
MCP and trust
}
function reachableFrom(source) {
var outgoing = outgoingByNode();
- var reached = {};
+ var reached = Object.create(null);
var queue = [source];
reached[source] = true;
for (var cursor = 0; cursor < queue.length; cursor += 1) {
@@ -13336,7 +12192,7 @@
MCP and trust
}
function hopDistancesFrom(source) {
var outgoing = outgoingByNode();
- var distances = {};
+ var distances = Object.create(null);
var queue = [source];
distances[source] = 0;
for (var cursor = 0; cursor < queue.length; cursor += 1) {
@@ -13352,7 +12208,7 @@
MCP and trust
function shortestDirectedPath(source, target) {
if (source === target) return null;
var outgoing = outgoingByNode();
- var previous = {};
+ var previous = Object.create(null);
var queue = [source];
previous[source] = null;
for (var cursor = 0; cursor < queue.length && !Object.prototype.hasOwnProperty.call(previous, target); cursor += 1) {
@@ -13839,9 +12695,6 @@
MCP and trust
if (Array.isArray(focused)) focused = null;
clear({ updateUrl: false, preserveView: true, restoreFocus: false });
if (Archify.intentTrace && typeof Archify.intentTrace.clear === 'function') Archify.intentTrace.clear({ announce: false });
- if (Archify.guidedViews && typeof Archify.guidedViews.showAll === 'function') {
- Archify.guidedViews.showAll({ clearFocus: false, updateUrl: false });
- }
if (Archify.focus && typeof Archify.focus.clear === 'function') {
Archify.focus.clear({ updateUrl: false, preserveView: true });
}
@@ -13882,7 +12735,7 @@
MCP and trust
var sourceIds = nodes().map(function (node) { return node.getAttribute('data-node-id'); }).filter(function (id) {
return outgoing[id] && outgoing[id].length;
});
- var sourceBadges = {};
+ var sourceBadges = Object.create(null);
sourceIds.forEach(function (id) { sourceBadges[id] = viewerText('viewer.route.finder.source.badge'); });
return {
kind: 'route-source',
@@ -13898,7 +12751,7 @@
MCP and trust
if (mode === 'target' && startId && byId[startId]) {
var distances = hopDistancesFrom(startId);
var targetIds = Object.keys(distances).filter(function (id) { return id !== startId && byId[id]; });
- var targetBadges = {};
+ var targetBadges = Object.create(null);
targetIds.forEach(function (id) {
targetBadges[id] = viewerCount('viewer.route.hop', distances[id]);
});
@@ -14107,7 +12960,7 @@
MCP and trust
var MAX_LENS_FLOW_EDGES = 24;
function nodesById() {
- var byId = {};
+ var byId = Object.create(null);
Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id][data-node-kind]'), function (node) {
var id = node.getAttribute('data-node-id');
if (id && !byId[id]) byId[id] = node;
@@ -14263,7 +13116,7 @@
MCP and trust
return selectedKinds.length > 0 || !panel.hidden || html.getAttribute('data-present') === 'true' ||
svg.hasAttribute('data-focus-active') || svg.hasAttribute('data-intent-trace-active') ||
svg.hasAttribute('data-route-picking') || svg.hasAttribute('data-route-active') ||
- svg.hasAttribute('data-story-active') || svg.hasAttribute('data-relationship-preview-active');
+ svg.hasAttribute('data-relationship-preview-active');
}
function previewLegendKind(entry) {
clearLegendPreview();
@@ -14274,7 +13127,7 @@
MCP and trust
return (byId[id].getAttribute('data-node-kind') || 'neutral') === kind;
});
if (!matches.length) return false;
- var chosen = {};
+ var chosen = Object.create(null);
matches.forEach(function (id) {
chosen[id] = true;
byId[id].setAttribute('data-legend-preview-match', '');
@@ -14450,7 +13303,7 @@
MCP and trust
options = options || {};
cleanSvgState();
var byId = nodesById();
- var nodeKind = {};
+ var nodeKind = Object.create(null);
Object.keys(byId).forEach(function (id) { nodeKind[id] = byId[id].getAttribute('data-node-kind') || 'neutral'; });
if (!selectedKinds.length) {
status.textContent = viewerText('viewer.lens.choose');
@@ -14535,9 +13388,6 @@
MCP and trust
if (Archify.routeProbe && typeof Archify.routeProbe.clear === 'function') {
Archify.routeProbe.clear({ updateUrl: false, restoreFocus: false });
}
- if (Archify.guidedViews && typeof Archify.guidedViews.showAll === 'function') {
- Archify.guidedViews.showAll({ clearFocus: false, updateUrl: false });
- }
if (Archify.intentTrace && typeof Archify.intentTrace.clear === 'function') {
Archify.intentTrace.clear({ announce: false });
}
@@ -14757,7 +13607,7 @@
MCP and trust
/* ============================================================
Diagram Guide — a factual command deck over existing interactions.
Counts come from compiled semantics; actions delegate to Finder, Route
- Probe, Semantic Radar, Semantic Lens, Story Trail, Presentation, theme, export, and
+ Probe, Semantic Radar, Semantic Lens, Presentation, theme, export, and
camera without adding a second command implementation or SVG state.
============================================================ */
Archify.guide = (function () {
@@ -14770,13 +13620,9 @@
MCP and trust
var stats = document.getElementById('diagram-guide-stats');
var actions = document.getElementById('diagram-guide-actions');
var feedback = document.getElementById('diagram-guide-feedback');
- var storyBtn = actions.querySelector('[data-guide-action="story"]');
- var storyCopy = document.getElementById('diagram-guide-story-copy');
var routePanel = document.getElementById('route-probe');
+ var returnFocus = null;
- function viewCount() {
- return Archify.guidedViews && Number(Archify.guidedViews.count) || 0;
- }
function relationshipCount() {
var seen = {};
Array.prototype.forEach.call(svg.querySelectorAll('[data-edge-from][data-edge-to]'), function (edge) {
@@ -14792,17 +13638,10 @@
MCP and trust
function renderFacts() {
var nodes = svg.querySelectorAll('[data-node-id]').length;
var relationships = relationshipCount();
- var views = viewCount();
stats.textContent = viewerText('viewer.guide.facts', {
nodes: viewerCount('viewer.guide.fact.node', nodes),
- relationships: viewerCount('viewer.guide.fact.relationship', relationships),
- views: viewerCount('viewer.guide.fact.view', views)
+ relationships: viewerCount('viewer.guide.fact.relationship', relationships)
});
- storyBtn.disabled = views === 0;
- storyBtn.setAttribute('aria-disabled', views === 0 ? 'true' : 'false');
- storyCopy.textContent = views
- ? viewerCount('viewer.guide.story.available', views)
- : viewerText('viewer.guide.story.unavailable');
}
function actionButtons() {
return Array.prototype.slice.call(actions.querySelectorAll('.diagram-guide-action:not(:disabled)'));
@@ -14815,7 +13654,10 @@
MCP and trust
trigger.setAttribute('aria-expanded', 'false');
trigger.setAttribute('aria-label', viewerText('viewer.guide.open'));
feedback.textContent = '';
- if (options.restoreFocus !== false) trigger.focus();
+ if (options.restoreFocus !== false) {
+ var target = trigger.hidden ? returnFocus : trigger;
+ if (target && typeof target.focus === 'function') target.focus();
+ }
return false;
}
function open() {
@@ -14825,12 +13667,10 @@
MCP and trust
if (Archify.finder && Archify.finder.isOpen()) Archify.finder.close({ restoreFocus: false });
if (Archify.radar && Archify.radar.isOpen()) Archify.radar.close({ restoreFocus: false });
if (Archify.semanticLens && Archify.semanticLens.isOpen()) Archify.semanticLens.close({ restoreFocus: false });
- if (Archify.guidedViews && Archify.guidedViews.isPlaying && Archify.guidedViews.isPlaying()) {
- Archify.guidedViews.pause();
- }
if (Archify.routeProbe && Archify.routeProbe.isJourneyPlaying && Archify.routeProbe.isJourneyPlaying()) {
Archify.routeProbe.pauseJourney({ preserveElapsed: true, reason: 'guide' });
}
+ returnFocus = document.activeElement;
renderFacts();
panel.hidden = false;
html.setAttribute('data-guide-open', 'true');
@@ -14847,16 +13687,11 @@
MCP and trust
function toggle() { return panel.hidden ? open() : close(); }
function execute(action) {
feedback.textContent = '';
- if (action === 'story' && !viewCount()) {
- feedback.textContent = viewerText('viewer.guide.noStory');
- return false;
- }
close({ restoreFocus: false });
if (action === 'find') return Archify.finder.open();
if (action === 'route') return Archify.routeProbe.begin({ focusNode: true });
if (action === 'map') return Archify.radar.open();
if (action === 'lens') return Archify.semanticLens.open();
- if (action === 'story') return Archify.guidedViews.play();
if (action === 'present') return Archify.presentation.enter();
if (action === 'export') return Archify.exportMenu.open();
if (action === 'theme') return Archify.theme.toggle();
@@ -14872,7 +13707,6 @@
MCP and trust
r: 'route',
m: 'map',
l: 'lens',
- p: 'story',
f: 'present',
e: 'export',
t: 'theme',
@@ -14934,8 +13768,7 @@
MCP and trust
facts: function () {
return {
nodes: svg.querySelectorAll('[data-node-id]').length,
- relationships: relationshipCount(),
- views: viewCount()
+ relationships: relationshipCount()
};
}
};
diff --git a/docs/architecture/hwp-cli-rendered.visual-check.1440x900.dark.png b/docs/architecture/hwp-cli-rendered.visual-check.1440x900.dark.png
index 2b35df79..b568a71a 100644
Binary files a/docs/architecture/hwp-cli-rendered.visual-check.1440x900.dark.png and b/docs/architecture/hwp-cli-rendered.visual-check.1440x900.dark.png differ
diff --git a/docs/architecture/hwp-cli-rendered.visual-check.1440x900.light.png b/docs/architecture/hwp-cli-rendered.visual-check.1440x900.light.png
index a140a8bd..0ef8548d 100644
Binary files a/docs/architecture/hwp-cli-rendered.visual-check.1440x900.light.png and b/docs/architecture/hwp-cli-rendered.visual-check.1440x900.light.png differ
diff --git a/docs/architecture/hwp-cli-rendered.visual-check.2048x1320.dark.png b/docs/architecture/hwp-cli-rendered.visual-check.2048x1320.dark.png
index 186b8f0a..19086bde 100644
Binary files a/docs/architecture/hwp-cli-rendered.visual-check.2048x1320.dark.png and b/docs/architecture/hwp-cli-rendered.visual-check.2048x1320.dark.png differ
diff --git a/docs/architecture/hwp-cli-rendered.visual-check.2048x1320.light.png b/docs/architecture/hwp-cli-rendered.visual-check.2048x1320.light.png
index 724b7d31..21f7e818 100644
Binary files a/docs/architecture/hwp-cli-rendered.visual-check.2048x1320.light.png and b/docs/architecture/hwp-cli-rendered.visual-check.2048x1320.light.png differ
diff --git a/docs/architecture/hwp-cli-rendered.visual-check.json b/docs/architecture/hwp-cli-rendered.visual-check.json
index 5fe17914..5c11e7e9 100644
--- a/docs/architecture/hwp-cli-rendered.visual-check.json
+++ b/docs/architecture/hwp-cli-rendered.visual-check.json
@@ -5,10 +5,12 @@
"evidenceKind": "automated-browser",
"status": "pass",
"visualReview": "pending",
+ "provenance": "current",
+ "deliveryReceiptId": "2a291fda-1d4d-4534-a74b-5c8cd8d99ef5",
"artifact": {
- "path": "hwp-cli-rendered.html",
- "sha256": "6c7ee779b1f2505a01c0d10ab254447de869793d5f0464fd90519df94de64808",
- "bytes": 821258
+ "path": "docs/architecture/hwp-cli-rendered.html",
+ "sha256": "511cd77a51b0283eb6a36fa3bc55af3436c7c97a34a9e817e656627901bd6e73",
+ "bytes": 779693
},
"state": {
"detail": "read",
@@ -21,6 +23,7 @@
"diagnostics": [],
"containment": {
"status": "pass",
+ "policy": "fit-or-reader-declared-readable-vertical-scroll",
"viewports": [
{
"width": 1440,
@@ -32,23 +35,44 @@
"scrollHeight": 900,
"overflowX": false,
"overflowY": false,
+ "verticalScrollAccepted": false,
+ "overflowDisposition": "contained",
+ "readerLayout": "adaptive",
+ "readerOverflow": null,
+ "readerFit": "intrinsic-height",
"ok": true,
- "readerWidth": 1094,
- "diagramWidth": 1064,
- "viewBoxWidth": 1376,
- "minimumProjectedNodeTextPx": 6.864516902288073,
- "minimumProjectedNodeText": "Checked paths: refusal before any file IO",
- "minimumProjectedNodeTextDetail": "boundary",
+ "readerWidth": 1276,
+ "diagramWidth": 1246,
+ "pageComposition": {
+ "bodyPaddingPx": 12,
+ "headerPx": 52,
+ "diagramChromePx": 78,
+ "svgPx": 746,
+ "cardsPx": 0,
+ "viewBoxHeight": 728
+ },
+ "viewBoxWidth": 1216,
+ "minimumProjectedNodeTextPx": 8,
+ "minimumProjectedNonEdgeTextPx": 9,
+ "minimumProjectedEdgeTextPx": 8,
+ "minimumProjectedNodeText": "hwp
",
+ "minimumProjectedNodeTextDetail": "edge",
+ "minimumProjectedNodeTextOwner": {
+ "kind": "edge",
+ "id": "user-to-cli",
+ "from": "user",
+ "to": "cli"
+ },
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
- "dockStageGap": 10.21875,
+ "dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
- "viewerChromeReserve": 51,
+ "viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
@@ -63,23 +87,44 @@
"scrollHeight": 1000,
"overflowX": false,
"overflowY": false,
+ "verticalScrollAccepted": false,
+ "overflowDisposition": "contained",
+ "readerLayout": "adaptive",
+ "readerOverflow": null,
+ "readerFit": "intrinsic-height",
"ok": true,
- "readerWidth": 1166,
- "diagramWidth": 1136,
- "viewBoxWidth": 1376,
- "minimumProjectedNodeTextPx": 7.329033083645912,
- "minimumProjectedNodeText": "Checked paths: refusal before any file IO",
- "minimumProjectedNodeTextDetail": "boundary",
+ "readerWidth": 1333,
+ "diagramWidth": 1303,
+ "pageComposition": {
+ "bodyPaddingPx": 64,
+ "headerPx": 60,
+ "diagramChromePx": 84,
+ "svgPx": 780,
+ "cardsPx": 0,
+ "viewBoxHeight": 728
+ },
+ "viewBoxWidth": 1216,
+ "minimumProjectedNodeTextPx": 8,
+ "minimumProjectedNonEdgeTextPx": 9,
+ "minimumProjectedEdgeTextPx": 8,
+ "minimumProjectedNodeText": "hwp ",
+ "minimumProjectedNodeTextDetail": "edge",
+ "minimumProjectedNodeTextOwner": {
+ "kind": "edge",
+ "id": "user-to-cli",
+ "from": "user",
+ "to": "cli"
+ },
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
- "dockStageGap": 10.21875,
+ "dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
- "viewerChromeReserve": 51,
+ "viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
@@ -94,23 +139,44 @@
"scrollHeight": 1080,
"overflowX": false,
"overflowY": false,
+ "verticalScrollAccepted": false,
+ "overflowDisposition": "contained",
+ "readerLayout": "adaptive",
+ "readerOverflow": null,
+ "readerFit": "intrinsic-height",
"ok": true,
- "readerWidth": 1336,
- "diagramWidth": 1306,
- "viewBoxWidth": 1376,
- "minimumProjectedNodeTextPx": 8.42580740074081,
- "minimumProjectedNodeText": "Checked paths: refusal before any file IO",
- "minimumProjectedNodeTextDetail": "boundary",
+ "readerWidth": 1466,
+ "diagramWidth": 1436,
+ "pageComposition": {
+ "bodyPaddingPx": 64,
+ "headerPx": 60,
+ "diagramChromePx": 84,
+ "svgPx": 860,
+ "cardsPx": 0,
+ "viewBoxHeight": 728
+ },
+ "viewBoxWidth": 1216,
+ "minimumProjectedNodeTextPx": 8,
+ "minimumProjectedNonEdgeTextPx": 9,
+ "minimumProjectedEdgeTextPx": 8,
+ "minimumProjectedNodeText": "hwp ",
+ "minimumProjectedNodeTextDetail": "edge",
+ "minimumProjectedNodeTextOwner": {
+ "kind": "edge",
+ "id": "user-to-cli",
+ "from": "user",
+ "to": "cli"
+ },
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
- "dockStageGap": 10.21875,
+ "dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
- "viewerChromeReserve": 51,
+ "viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
@@ -125,29 +191,97 @@
"scrollHeight": 1320,
"overflowX": false,
"overflowY": false,
+ "verticalScrollAccepted": false,
+ "overflowDisposition": "contained",
+ "readerLayout": "adaptive",
+ "readerOverflow": null,
+ "readerFit": "intrinsic-height",
"ok": true,
- "readerWidth": 1810,
- "diagramWidth": 1760,
- "viewBoxWidth": 1376,
- "minimumProjectedNodeTextPx": 8.87742035483871,
- "minimumProjectedNodeText": "Checked paths: refusal before any file IO",
- "minimumProjectedNodeTextDetail": "boundary",
+ "readerWidth": 1857,
+ "diagramWidth": 1807,
+ "pageComposition": {
+ "bodyPaddingPx": 64,
+ "headerPx": 68,
+ "diagramChromePx": 94,
+ "svgPx": 1082,
+ "cardsPx": 0,
+ "viewBoxHeight": 728
+ },
+ "viewBoxWidth": 1216,
+ "minimumProjectedNodeTextPx": 8,
+ "minimumProjectedNonEdgeTextPx": 9,
+ "minimumProjectedEdgeTextPx": 8,
+ "minimumProjectedNodeText": "hwp ",
+ "minimumProjectedNodeTextDetail": "edge",
+ "minimumProjectedNodeTextOwner": {
+ "kind": "edge",
+ "id": "user-to-cli",
+ "from": "user",
+ "to": "cli"
+ },
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
- "dockStageGap": 10.21875,
+ "dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
- "viewerChromeReserve": 41,
+ "viewerChromeReserve": 44,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
}
]
},
+ "themeStates": {
+ "status": "pass",
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 900,
+ "requestedTheme": "light",
+ "resolvedTheme": "light",
+ "ok": true
+ },
+ {
+ "width": 1600,
+ "height": 1000,
+ "requestedTheme": "light",
+ "resolvedTheme": "light",
+ "ok": true
+ },
+ {
+ "width": 1920,
+ "height": 1080,
+ "requestedTheme": "light",
+ "resolvedTheme": "light",
+ "ok": true
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "requestedTheme": "light",
+ "resolvedTheme": "light",
+ "ok": true
+ },
+ {
+ "width": 1440,
+ "height": 900,
+ "requestedTheme": "dark",
+ "resolvedTheme": "dark",
+ "ok": true
+ },
+ {
+ "width": 2048,
+ "height": 1320,
+ "requestedTheme": "dark",
+ "resolvedTheme": "dark",
+ "ok": true
+ }
+ ]
+ },
"readability": {
"status": "pass",
"minimumProjectedNodeTextPx": 6,
@@ -162,23 +296,44 @@
"scrollHeight": 900,
"overflowX": false,
"overflowY": false,
+ "verticalScrollAccepted": false,
+ "overflowDisposition": "contained",
+ "readerLayout": "adaptive",
+ "readerOverflow": null,
+ "readerFit": "intrinsic-height",
"ok": true,
- "readerWidth": 1094,
- "diagramWidth": 1064,
- "viewBoxWidth": 1376,
- "minimumProjectedNodeTextPx": 6.864516902288073,
- "minimumProjectedNodeText": "Checked paths: refusal before any file IO",
- "minimumProjectedNodeTextDetail": "boundary",
+ "readerWidth": 1276,
+ "diagramWidth": 1246,
+ "pageComposition": {
+ "bodyPaddingPx": 12,
+ "headerPx": 52,
+ "diagramChromePx": 78,
+ "svgPx": 746,
+ "cardsPx": 0,
+ "viewBoxHeight": 728
+ },
+ "viewBoxWidth": 1216,
+ "minimumProjectedNodeTextPx": 8,
+ "minimumProjectedNonEdgeTextPx": 9,
+ "minimumProjectedEdgeTextPx": 8,
+ "minimumProjectedNodeText": "hwp ",
+ "minimumProjectedNodeTextDetail": "edge",
+ "minimumProjectedNodeTextOwner": {
+ "kind": "edge",
+ "id": "user-to-cli",
+ "from": "user",
+ "to": "cli"
+ },
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
- "dockStageGap": 10.21875,
+ "dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
- "viewerChromeReserve": 51,
+ "viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
@@ -193,23 +348,44 @@
"scrollHeight": 1000,
"overflowX": false,
"overflowY": false,
+ "verticalScrollAccepted": false,
+ "overflowDisposition": "contained",
+ "readerLayout": "adaptive",
+ "readerOverflow": null,
+ "readerFit": "intrinsic-height",
"ok": true,
- "readerWidth": 1166,
- "diagramWidth": 1136,
- "viewBoxWidth": 1376,
- "minimumProjectedNodeTextPx": 7.329033083645912,
- "minimumProjectedNodeText": "Checked paths: refusal before any file IO",
- "minimumProjectedNodeTextDetail": "boundary",
+ "readerWidth": 1333,
+ "diagramWidth": 1303,
+ "pageComposition": {
+ "bodyPaddingPx": 64,
+ "headerPx": 60,
+ "diagramChromePx": 84,
+ "svgPx": 780,
+ "cardsPx": 0,
+ "viewBoxHeight": 728
+ },
+ "viewBoxWidth": 1216,
+ "minimumProjectedNodeTextPx": 8,
+ "minimumProjectedNonEdgeTextPx": 9,
+ "minimumProjectedEdgeTextPx": 8,
+ "minimumProjectedNodeText": "hwp ",
+ "minimumProjectedNodeTextDetail": "edge",
+ "minimumProjectedNodeTextOwner": {
+ "kind": "edge",
+ "id": "user-to-cli",
+ "from": "user",
+ "to": "cli"
+ },
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
- "dockStageGap": 10.21875,
+ "dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
- "viewerChromeReserve": 51,
+ "viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
@@ -224,23 +400,44 @@
"scrollHeight": 1080,
"overflowX": false,
"overflowY": false,
+ "verticalScrollAccepted": false,
+ "overflowDisposition": "contained",
+ "readerLayout": "adaptive",
+ "readerOverflow": null,
+ "readerFit": "intrinsic-height",
"ok": true,
- "readerWidth": 1336,
- "diagramWidth": 1306,
- "viewBoxWidth": 1376,
- "minimumProjectedNodeTextPx": 8.42580740074081,
- "minimumProjectedNodeText": "Checked paths: refusal before any file IO",
- "minimumProjectedNodeTextDetail": "boundary",
+ "readerWidth": 1466,
+ "diagramWidth": 1436,
+ "pageComposition": {
+ "bodyPaddingPx": 64,
+ "headerPx": 60,
+ "diagramChromePx": 84,
+ "svgPx": 860,
+ "cardsPx": 0,
+ "viewBoxHeight": 728
+ },
+ "viewBoxWidth": 1216,
+ "minimumProjectedNodeTextPx": 8,
+ "minimumProjectedNonEdgeTextPx": 9,
+ "minimumProjectedEdgeTextPx": 8,
+ "minimumProjectedNodeText": "hwp ",
+ "minimumProjectedNodeTextDetail": "edge",
+ "minimumProjectedNodeTextOwner": {
+ "kind": "edge",
+ "id": "user-to-cli",
+ "from": "user",
+ "to": "cli"
+ },
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
- "dockStageGap": 10.21875,
+ "dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
- "viewerChromeReserve": 51,
+ "viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
@@ -255,23 +452,44 @@
"scrollHeight": 1320,
"overflowX": false,
"overflowY": false,
+ "verticalScrollAccepted": false,
+ "overflowDisposition": "contained",
+ "readerLayout": "adaptive",
+ "readerOverflow": null,
+ "readerFit": "intrinsic-height",
"ok": true,
- "readerWidth": 1810,
- "diagramWidth": 1760,
- "viewBoxWidth": 1376,
- "minimumProjectedNodeTextPx": 8.87742035483871,
- "minimumProjectedNodeText": "Checked paths: refusal before any file IO",
- "minimumProjectedNodeTextDetail": "boundary",
+ "readerWidth": 1857,
+ "diagramWidth": 1807,
+ "pageComposition": {
+ "bodyPaddingPx": 64,
+ "headerPx": 68,
+ "diagramChromePx": 94,
+ "svgPx": 1082,
+ "cardsPx": 0,
+ "viewBoxHeight": 728
+ },
+ "viewBoxWidth": 1216,
+ "minimumProjectedNodeTextPx": 8,
+ "minimumProjectedNonEdgeTextPx": 9,
+ "minimumProjectedEdgeTextPx": 8,
+ "minimumProjectedNodeText": "hwp ",
+ "minimumProjectedNodeTextDetail": "edge",
+ "minimumProjectedNodeTextOwner": {
+ "kind": "edge",
+ "id": "user-to-cli",
+ "from": "user",
+ "to": "cli"
+ },
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
- "dockStageGap": 10.21875,
+ "dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
- "viewerChromeReserve": 41,
+ "viewerChromeReserve": 44,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
@@ -291,23 +509,44 @@
"scrollHeight": 900,
"overflowX": false,
"overflowY": false,
+ "verticalScrollAccepted": false,
+ "overflowDisposition": "contained",
+ "readerLayout": "adaptive",
+ "readerOverflow": null,
+ "readerFit": "intrinsic-height",
"ok": true,
- "readerWidth": 1094,
- "diagramWidth": 1064,
- "viewBoxWidth": 1376,
- "minimumProjectedNodeTextPx": 6.864516902288073,
- "minimumProjectedNodeText": "Checked paths: refusal before any file IO",
- "minimumProjectedNodeTextDetail": "boundary",
+ "readerWidth": 1276,
+ "diagramWidth": 1246,
+ "pageComposition": {
+ "bodyPaddingPx": 12,
+ "headerPx": 52,
+ "diagramChromePx": 78,
+ "svgPx": 746,
+ "cardsPx": 0,
+ "viewBoxHeight": 728
+ },
+ "viewBoxWidth": 1216,
+ "minimumProjectedNodeTextPx": 8,
+ "minimumProjectedNonEdgeTextPx": 9,
+ "minimumProjectedEdgeTextPx": 8,
+ "minimumProjectedNodeText": "hwp ",
+ "minimumProjectedNodeTextDetail": "edge",
+ "minimumProjectedNodeTextOwner": {
+ "kind": "edge",
+ "id": "user-to-cli",
+ "from": "user",
+ "to": "cli"
+ },
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
- "dockStageGap": 10.21875,
+ "dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
- "viewerChromeReserve": 51,
+ "viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
@@ -322,23 +561,44 @@
"scrollHeight": 1000,
"overflowX": false,
"overflowY": false,
+ "verticalScrollAccepted": false,
+ "overflowDisposition": "contained",
+ "readerLayout": "adaptive",
+ "readerOverflow": null,
+ "readerFit": "intrinsic-height",
"ok": true,
- "readerWidth": 1166,
- "diagramWidth": 1136,
- "viewBoxWidth": 1376,
- "minimumProjectedNodeTextPx": 7.329033083645912,
- "minimumProjectedNodeText": "Checked paths: refusal before any file IO",
- "minimumProjectedNodeTextDetail": "boundary",
+ "readerWidth": 1333,
+ "diagramWidth": 1303,
+ "pageComposition": {
+ "bodyPaddingPx": 64,
+ "headerPx": 60,
+ "diagramChromePx": 84,
+ "svgPx": 780,
+ "cardsPx": 0,
+ "viewBoxHeight": 728
+ },
+ "viewBoxWidth": 1216,
+ "minimumProjectedNodeTextPx": 8,
+ "minimumProjectedNonEdgeTextPx": 9,
+ "minimumProjectedEdgeTextPx": 8,
+ "minimumProjectedNodeText": "hwp ",
+ "minimumProjectedNodeTextDetail": "edge",
+ "minimumProjectedNodeTextOwner": {
+ "kind": "edge",
+ "id": "user-to-cli",
+ "from": "user",
+ "to": "cli"
+ },
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
- "dockStageGap": 10.21875,
+ "dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
- "viewerChromeReserve": 51,
+ "viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
@@ -353,23 +613,44 @@
"scrollHeight": 1080,
"overflowX": false,
"overflowY": false,
+ "verticalScrollAccepted": false,
+ "overflowDisposition": "contained",
+ "readerLayout": "adaptive",
+ "readerOverflow": null,
+ "readerFit": "intrinsic-height",
"ok": true,
- "readerWidth": 1336,
- "diagramWidth": 1306,
- "viewBoxWidth": 1376,
- "minimumProjectedNodeTextPx": 8.42580740074081,
- "minimumProjectedNodeText": "Checked paths: refusal before any file IO",
- "minimumProjectedNodeTextDetail": "boundary",
+ "readerWidth": 1466,
+ "diagramWidth": 1436,
+ "pageComposition": {
+ "bodyPaddingPx": 64,
+ "headerPx": 60,
+ "diagramChromePx": 84,
+ "svgPx": 860,
+ "cardsPx": 0,
+ "viewBoxHeight": 728
+ },
+ "viewBoxWidth": 1216,
+ "minimumProjectedNodeTextPx": 8,
+ "minimumProjectedNonEdgeTextPx": 9,
+ "minimumProjectedEdgeTextPx": 8,
+ "minimumProjectedNodeText": "hwp ",
+ "minimumProjectedNodeTextDetail": "edge",
+ "minimumProjectedNodeTextOwner": {
+ "kind": "edge",
+ "id": "user-to-cli",
+ "from": "user",
+ "to": "cli"
+ },
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
- "dockStageGap": 10.21875,
+ "dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
- "viewerChromeReserve": 51,
+ "viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
@@ -384,23 +665,44 @@
"scrollHeight": 1320,
"overflowX": false,
"overflowY": false,
+ "verticalScrollAccepted": false,
+ "overflowDisposition": "contained",
+ "readerLayout": "adaptive",
+ "readerOverflow": null,
+ "readerFit": "intrinsic-height",
"ok": true,
- "readerWidth": 1810,
- "diagramWidth": 1760,
- "viewBoxWidth": 1376,
- "minimumProjectedNodeTextPx": 8.87742035483871,
- "minimumProjectedNodeText": "Checked paths: refusal before any file IO",
- "minimumProjectedNodeTextDetail": "boundary",
+ "readerWidth": 1857,
+ "diagramWidth": 1807,
+ "pageComposition": {
+ "bodyPaddingPx": 64,
+ "headerPx": 68,
+ "diagramChromePx": 94,
+ "svgPx": 1082,
+ "cardsPx": 0,
+ "viewBoxHeight": 728
+ },
+ "viewBoxWidth": 1216,
+ "minimumProjectedNodeTextPx": 8,
+ "minimumProjectedNonEdgeTextPx": 9,
+ "minimumProjectedEdgeTextPx": 8,
+ "minimumProjectedNodeText": "hwp ",
+ "minimumProjectedNodeTextDetail": "edge",
+ "minimumProjectedNodeTextOwner": {
+ "kind": "edge",
+ "id": "user-to-cli",
+ "from": "user",
+ "to": "cli"
+ },
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
- "dockStageGap": 10.21875,
+ "dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
- "viewerChromeReserve": 41,
+ "viewerChromeReserve": 44,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light"
@@ -420,23 +722,44 @@
"scrollHeight": 900,
"overflowX": false,
"overflowY": false,
+ "verticalScrollAccepted": false,
+ "overflowDisposition": "contained",
+ "readerLayout": "adaptive",
+ "readerOverflow": null,
+ "readerFit": "intrinsic-height",
"ok": true,
- "readerWidth": 1094,
- "diagramWidth": 1064,
- "viewBoxWidth": 1376,
- "minimumProjectedNodeTextPx": 6.864516902288073,
- "minimumProjectedNodeText": "Checked paths: refusal before any file IO",
- "minimumProjectedNodeTextDetail": "boundary",
+ "readerWidth": 1276,
+ "diagramWidth": 1246,
+ "pageComposition": {
+ "bodyPaddingPx": 12,
+ "headerPx": 52,
+ "diagramChromePx": 78,
+ "svgPx": 746,
+ "cardsPx": 0,
+ "viewBoxHeight": 728
+ },
+ "viewBoxWidth": 1216,
+ "minimumProjectedNodeTextPx": 8,
+ "minimumProjectedNonEdgeTextPx": 9,
+ "minimumProjectedEdgeTextPx": 8,
+ "minimumProjectedNodeText": "hwp ",
+ "minimumProjectedNodeTextDetail": "edge",
+ "minimumProjectedNodeTextOwner": {
+ "kind": "edge",
+ "id": "user-to-cli",
+ "from": "user",
+ "to": "cli"
+ },
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
- "dockStageGap": 10.21875,
+ "dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
- "viewerChromeReserve": 51,
+ "viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light",
@@ -452,23 +775,44 @@
"scrollHeight": 900,
"overflowX": false,
"overflowY": false,
+ "verticalScrollAccepted": false,
+ "overflowDisposition": "contained",
+ "readerLayout": "adaptive",
+ "readerOverflow": null,
+ "readerFit": "intrinsic-height",
"ok": true,
- "readerWidth": 1094,
- "diagramWidth": 1064,
- "viewBoxWidth": 1376,
- "minimumProjectedNodeTextPx": 6.864516902288073,
- "minimumProjectedNodeText": "Checked paths: refusal before any file IO",
- "minimumProjectedNodeTextDetail": "boundary",
+ "readerWidth": 1276,
+ "diagramWidth": 1246,
+ "pageComposition": {
+ "bodyPaddingPx": 12,
+ "headerPx": 52,
+ "diagramChromePx": 78,
+ "svgPx": 746,
+ "cardsPx": 0,
+ "viewBoxHeight": 728
+ },
+ "viewBoxWidth": 1216,
+ "minimumProjectedNodeTextPx": 8,
+ "minimumProjectedNonEdgeTextPx": 9,
+ "minimumProjectedEdgeTextPx": 8,
+ "minimumProjectedNodeText": "hwp ",
+ "minimumProjectedNodeTextDetail": "edge",
+ "minimumProjectedNodeTextOwner": {
+ "kind": "edge",
+ "id": "user-to-cli",
+ "from": "user",
+ "to": "cli"
+ },
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
- "dockStageGap": 10.21875,
+ "dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
- "viewerChromeReserve": 51,
+ "viewerChromeReserve": 54,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "dark",
@@ -484,23 +828,44 @@
"scrollHeight": 1320,
"overflowX": false,
"overflowY": false,
+ "verticalScrollAccepted": false,
+ "overflowDisposition": "contained",
+ "readerLayout": "adaptive",
+ "readerOverflow": null,
+ "readerFit": "intrinsic-height",
"ok": true,
- "readerWidth": 1810,
- "diagramWidth": 1760,
- "viewBoxWidth": 1376,
- "minimumProjectedNodeTextPx": 8.87742035483871,
- "minimumProjectedNodeText": "Checked paths: refusal before any file IO",
- "minimumProjectedNodeTextDetail": "boundary",
+ "readerWidth": 1857,
+ "diagramWidth": 1807,
+ "pageComposition": {
+ "bodyPaddingPx": 64,
+ "headerPx": 68,
+ "diagramChromePx": 94,
+ "svgPx": 1082,
+ "cardsPx": 0,
+ "viewBoxHeight": 728
+ },
+ "viewBoxWidth": 1216,
+ "minimumProjectedNodeTextPx": 8,
+ "minimumProjectedNonEdgeTextPx": 9,
+ "minimumProjectedEdgeTextPx": 8,
+ "minimumProjectedNodeText": "hwp ",
+ "minimumProjectedNodeTextDetail": "edge",
+ "minimumProjectedNodeTextOwner": {
+ "kind": "edge",
+ "id": "user-to-cli",
+ "from": "user",
+ "to": "cli"
+ },
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
- "dockStageGap": 10.21875,
+ "dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
- "viewerChromeReserve": 41,
+ "viewerChromeReserve": 44,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "light",
@@ -516,23 +881,44 @@
"scrollHeight": 1320,
"overflowX": false,
"overflowY": false,
+ "verticalScrollAccepted": false,
+ "overflowDisposition": "contained",
+ "readerLayout": "adaptive",
+ "readerOverflow": null,
+ "readerFit": "intrinsic-height",
"ok": true,
- "readerWidth": 1810,
- "diagramWidth": 1760,
- "viewBoxWidth": 1376,
- "minimumProjectedNodeTextPx": 8.87742035483871,
- "minimumProjectedNodeText": "Checked paths: refusal before any file IO",
- "minimumProjectedNodeTextDetail": "boundary",
+ "readerWidth": 1857,
+ "diagramWidth": 1807,
+ "pageComposition": {
+ "bodyPaddingPx": 64,
+ "headerPx": 68,
+ "diagramChromePx": 94,
+ "svgPx": 1082,
+ "cardsPx": 0,
+ "viewBoxHeight": 728
+ },
+ "viewBoxWidth": 1216,
+ "minimumProjectedNodeTextPx": 8,
+ "minimumProjectedNonEdgeTextPx": 9,
+ "minimumProjectedEdgeTextPx": 8,
+ "minimumProjectedNodeText": "hwp ",
+ "minimumProjectedNodeTextDetail": "edge",
+ "minimumProjectedNodeTextOwner": {
+ "kind": "edge",
+ "id": "user-to-cli",
+ "from": "user",
+ "to": "cli"
+ },
"minimumRequiredNodeTextPx": 6,
"readabilityOk": true,
"hasLegend": true,
"hasNavigationDock": true,
"legendDockIntersectionArea": 0,
"dockStageIntersectionArea": 0,
- "dockStageGap": 10.21875,
+ "dockStageGap": 10,
"requiredDockStageGap": 10,
"viewerChromeStageOk": true,
- "viewerChromeReserve": 41,
+ "viewerChromeReserve": 44,
"viewerChromeActive": true,
"viewerChromeOk": true,
"resolvedTheme": "dark",
@@ -543,6 +929,33 @@
},
"sidecars": {
"receipt": "hwp-cli-rendered.visual-check.json",
- "contactSheet": "hwp-cli-rendered.visual-check.html"
+ "contactSheet": "hwp-cli-rendered.visual-check.html",
+ "files": [
+ {
+ "sha256": "05ee9b90ebd059232f1b88c1916b1aa20928537543f6da31d9f2bc1eed12e452",
+ "bytes": 1940,
+ "file": "hwp-cli-rendered.visual-check.html"
+ },
+ {
+ "sha256": "d42aee6008b251993a472cb949d41474342d1a92ae6a2473686d6c205e5b2fc8",
+ "bytes": 112861,
+ "file": "hwp-cli-rendered.visual-check.1440x900.light.png"
+ },
+ {
+ "sha256": "696a8422d35921debe966d092800abc8b4c2e3cf5baa476b8f767631837a6d7d",
+ "bytes": 121793,
+ "file": "hwp-cli-rendered.visual-check.1440x900.dark.png"
+ },
+ {
+ "sha256": "adbe8473b45c7b9c6efa96ead5ac55694f5768c7f15aff28346e39f38ea63161",
+ "bytes": 170740,
+ "file": "hwp-cli-rendered.visual-check.2048x1320.light.png"
+ },
+ {
+ "sha256": "3c55911f66c6b7e82ac41e99305376b7d7bd6de24767b1a12d1f33b79f6b77c3",
+ "bytes": 185202,
+ "file": "hwp-cli-rendered.visual-check.2048x1320.dark.png"
+ }
+ ]
}
}
diff --git a/docs/architecture/hwp-cli.architecture.json b/docs/architecture/hwp-cli.architecture.json
index 4c79263d..f3115146 100644
--- a/docs/architecture/hwp-cli.architecture.json
+++ b/docs/architecture/hwp-cli.architecture.json
@@ -6,308 +6,210 @@
"locale": "en",
"quality_profile": "showcase",
"output": "hwp-cli-rendered.html",
- "viewBox": [1376, 648],
"repository": {
"url": "https://github.com/STAIxBWLB/hwp-cli",
"provider": "github",
- "revision": "982c3e63557546b8721009405d87d5b6af7c64de"
- },
- "views": [
- {
- "id": "read-path",
- "label": "Read and convert path",
- "focus": ["user", "cli", "codecs", "model", "convert", "fs"],
- "note": "From hwp/hwpx input to IR and converted artifacts."
- },
- {
- "id": "render-path",
- "label": "Render path",
- "focus": ["model", "render", "fs"],
- "note": "IR to glyph shaping, layout, page output and layout geometry."
- },
- {
- "id": "mcp-path",
- "label": "MCP deployment",
- "focus": ["cli", "mcp", "edge", "fs"],
- "note": "22-tool stdio server and the POST /mcp edge hop."
- }
- ],
- "legend": {
- "mode": "all"
+ "revision": "1989573f62ae1db4176caa43d232c8094dc0673a"
}
},
"components": [
{
"id": "user",
"type": "external",
- "label": "Users / AI Agents",
- "sublabel": "CLI + MCP clients",
- "pos": [40, 330],
- "size": [130, 60],
+ "label": "Users / AI agents",
+ "sublabel": "shell, MCP clients",
+ "pos": [30, 250],
+ "size": [130, 64],
"sources": [
- { "path": "crates/hwp-cli/src/cli.rs", "line": 68, "label": "clap derive Cmd enum" }
+ { "path": "crates/hwp-cli/src/cli.rs", "line": 68, "label": "clap Cmd enum users invoke" },
+ { "path": "deploy/cloudflare/src/index.ts", "line": 10, "end_line": 17, "label": "remote MCP clients connect" }
]
},
{
"id": "cli",
"type": "backend",
- "label": "hwp CLI Entry",
- "sublabel": "crates/hwp-cli, clap derive",
+ "label": "hwp CLI entry",
+ "sublabel": "clap Cmd enum",
"tag": "26 commands",
- "pos": [250, 330],
- "size": [160, 64],
+ "pos": [260, 250],
+ "size": [140, 64],
"sources": [
- { "path": "crates/hwp-cli/src/cli.rs", "line": 68, "end_line": 580, "label": "26-variant Cmd enum" },
- { "path": "crates/hwp-cli/src/main.rs", "line": 1, "label": "hwp binary entrypoint" }
+ { "path": "crates/hwp-cli/src/cli.rs", "line": 68, "end_line": 586, "label": "26-variant Cmd enum" },
+ { "path": "crates/hwp-cli/src/main.rs", "line": 46, "end_line": 361, "label": "dispatch to commands" }
]
},
{
- "id": "codecs",
+ "id": "commands",
"type": "backend",
- "label": "Document Codecs",
- "sublabel": "hwp5 + hwpx crates",
- "tag": "CFB + OWPML",
- "pos": [250, 110],
- "size": [160, 60],
+ "label": "Command layer",
+ "sublabel": "hwp-cli/src/commands",
+ "tag": "shared by CLI + MCP",
+ "pos": [480, 250],
+ "size": [150, 64],
"sources": [
- { "path": "crates/hwp5/src/lib.rs", "line": 1, "label": "CFB record stream codec" },
- { "path": "crates/hwpx/src/lib.rs", "line": 1, "label": "OWPML package codec" }
+ { "path": "crates/hwp-cli/src/commands/cat.rs", "line": 177, "end_line": 210, "label": "load_document picks a codec" },
+ { "path": "crates/hwp-cli/src/commands/output.rs", "line": 17, "end_line": 40, "label": "write_validated atomic publish" },
+ { "path": "crates/hwp-cli/src/commands/render.rs", "line": 640, "end_line": 672, "label": "JPEG/WebP encoded from raster" }
]
},
{
- "id": "model",
+ "id": "mcp",
"type": "backend",
- "label": "hwp-model IR",
- "sublabel": "sections, paragraphs",
- "tag": "shared document model",
- "pos": [480, 330],
- "size": [150, 60],
+ "label": "MCP server",
+ "sublabel": "stdio + POST /mcp",
+ "tag": "22 tools",
+ "pos": [260, 430],
+ "size": [140, 60],
"sources": [
- { "path": "crates/hwp-model/src/lib.rs", "line": 1, "label": "L1 semantic IR, serde only" }
+ { "path": "crates/hwp-cli/src/commands/mcp/mod.rs", "line": 2346, "end_line": 2891, "label": "tool_defs: 22 tools" },
+ { "path": "crates/hwp-cli/src/commands/mcp/mod.rs", "line": 1, "end_line": 16, "label": "core; stdio + http adapters" },
+ { "path": "crates/hwp-cli/src/commands/mcp/http.rs", "line": 364, "end_line": 456, "label": "/files writes root.join(name)" }
]
},
{
- "id": "render",
+ "id": "edge",
+ "type": "cloud",
+ "label": "Remote edge",
+ "sublabel": "Worker or AgentCore",
+ "tag": "auth + TLS",
+ "pos": [30, 430],
+ "size": [130, 60],
+ "sources": [
+ { "path": "deploy/cloudflare/src/index.ts", "line": 18, "end_line": 37, "label": "Worker OAuth provider + PAT" },
+ { "path": "deploy/cloudflare/src/mcp-api.ts", "line": 190, "end_line": 196, "label": "forward to container /mcp" },
+ { "path": "deploy/aws/Dockerfile.agentcore", "line": 82, "end_line": 87, "label": "AgentCore runs hwp serve" }
+ ]
+ },
+ {
+ "id": "codecs",
"type": "backend",
- "label": "hwp-render",
- "sublabel": "rustybuzz, tiny-skia",
- "tag": "PNG/SVG/PDF/JPEG/WebP",
- "pos": [695, 110],
- "size": [170, 60],
+ "label": "Document codecs",
+ "sublabel": "hwp5 CFB + hwpx OWPML",
+ "tag": "unedited hwp5 lossless",
+ "pos": [760, 430],
+ "size": [160, 60],
"sources": [
- { "path": "crates/hwp-render/src/lib.rs", "line": 1, "label": "IR to page raster/vector pipeline" },
- { "path": "crates/hwp-render/src/segment_map.rs", "line": 1, "label": "render-layout-v1 geometry rows" }
+ { "path": "crates/hwp5/src/lib.rs", "line": 1, "end_line": 16, "label": "HWP 5.0 CFB reader/writer" },
+ { "path": "crates/hwpx/src/lib.rs", "line": 1, "end_line": 9, "label": "HWPX ZIP + XML reader/writer" },
+ { "path": "crates/hwp5/tests/identity.rs", "line": 1, "end_line": 5, "label": "identity gate, decompressed streams" }
]
},
{
- "id": "convert",
+ "id": "model",
"type": "backend",
- "label": "hwp-convert",
- "sublabel": "markdown/HTML/DOCX/ODT + lint",
- "tag": "10 lint rules",
- "pos": [695, 330],
- "size": [170, 64],
+ "label": "hwp-model IR",
+ "sublabel": "serde-only hub",
+ "tag": "slot grammar",
+ "pos": [1030, 250],
+ "size": [130, 64],
"sources": [
- { "path": "crates/hwp-convert/src/lint.rs", "line": 76, "end_line": 85, "label": "10 stable rule ids" },
- { "path": "crates/hwp-convert/src/official.rs", "line": 9, "label": "6 official-document profiles" }
+ { "path": "crates/hwp-model/src/lib.rs", "line": 1, "end_line": 27, "label": "L1 semantic IR, serde only" },
+ { "path": "crates/hwp-model/src/slot.rs", "line": 1, "end_line": 7, "label": "shared {{ name }} grammar" }
]
},
{
- "id": "fs",
- "type": "database",
- "label": "Local Filesystem",
- "sublabel": "hwp/hwpx in, artifacts out",
- "tag": "atomic report publish",
- "pos": [930, 330],
- "size": [150, 60],
+ "id": "convert",
+ "type": "backend",
+ "label": "hwp-convert",
+ "sublabel": "md/HTML/DOCX/ODT, edit",
+ "tag": "lint + forms",
+ "pos": [760, 250],
+ "size": [160, 64],
"sources": [
- { "path": "crates/hwp-cli/src/certification.rs", "line": 2964, "label": "atomic write_artifact staging" }
+ { "path": "crates/hwp-convert/src/form.rs", "line": 1, "end_line": 9, "label": "Korean form fields (kordoc port)" },
+ { "path": "crates/hwp-convert/src/lint.rs", "line": 76, "end_line": 85, "label": "10 lint rule ids" },
+ { "path": "crates/hwp-convert/src/official.rs", "line": 7, "end_line": 16, "label": "6 official-document profiles" }
]
},
{
- "id": "mcp",
+ "id": "render",
"type": "backend",
- "label": "MCP Server",
- "sublabel": "22 tools, no MCP SDK",
- "tag": "stdio + HTTP",
- "pos": [250, 500],
+ "label": "hwp-render",
+ "sublabel": "rustybuzz + tiny-skia",
+ "tag": "PNG/SVG/PDF pages",
+ "pos": [760, 80],
"size": [160, 60],
"sources": [
- { "path": "crates/hwp-cli/src/commands/mcp/mod.rs", "line": 2302, "label": "tool_defs: 22 tool table" },
- { "path": "crates/hwp-cli/src/commands/mcp/stdio.rs", "line": 4, "label": "per-call password zeroize" },
- { "path": "crates/hwp-cli/src/commands/mcp/authority.rs", "line": 273, "label": "checked_read_path / checked_write_path" }
+ { "path": "crates/hwp-render/src/lib.rs", "line": 1, "end_line": 5, "label": "IR to layout to backends" },
+ { "path": "crates/hwp-render/src/segment_map.rs", "line": 1, "end_line": 12, "label": "per-page segment geometry rows" },
+ { "path": "crates/hwp-render/src/segment_map.rs", "line": 58, "end_line": 62, "label": "drawing-object text rows" }
]
},
{
- "id": "edge",
- "type": "cloud",
- "label": "Remote Edge",
- "sublabel": "Cloudflare OAuth/PAT",
- "tag": "POST /mcp",
- "pos": [40, 500],
- "size": [130, 60],
+ "id": "fs",
+ "type": "database",
+ "label": "Local filesystem",
+ "sublabel": "inputs + staged outputs",
+ "tag": "stage, fsync, rename",
+ "pos": [760, 600],
+ "size": [160, 60],
"sources": [
- { "path": "crates/hwp-cli/src/commands/mcp/http.rs", "line": 1, "label": "close-per-request POST /mcp adapter" },
- { "path": "crates/hwp-cli/src/commands/mcp/wire.rs", "line": 1, "label": "httparse head parsing, bounded framing" },
- { "path": "docs/design/22-remote-mcp-deployment.md", "line": 85, "label": "private hop behind a trusted edge" }
+ { "path": "crates/hwp-cli/src/commands/output.rs", "line": 1, "end_line": 40, "label": "stage beside target, verify, rename" },
+ { "path": "crates/hwp5/src/write.rs", "line": 75, "label": "hwp5 write_document(path)" },
+ { "path": "crates/hwp-cli/src/commands/mcp/mod.rs", "line": 1028, "end_line": 1060, "label": "hwp_get_file reads directly" }
]
}
],
"boundaries": [
{
"kind": "region",
- "label": "Single Rust binary - no Hancom Office / COM",
- "wraps": ["cli", "codecs", "model", "render", "convert", "mcp"],
- "pad": 14
+ "label": "One hwp binary: 6 Rust crates, no external HWP library",
+ "wraps": ["cli", "commands", "mcp", "codecs", "model", "convert", "render"],
+ "pad": 16
},
{
"kind": "security-group",
- "label": "Checked paths: refusal before any file IO",
+ "label": "--root gate",
"wraps": ["mcp"],
- "pad": 14
+ "pad": 12
}
],
"connections": [
- {
- "id": "user-to-cli",
- "from": "user",
- "to": "cli",
- "label": "hwp",
- "variant": "emphasis",
- "fromSide": "right",
- "toSide": "left"
- },
- {
- "id": "cli-to-codecs",
- "from": "cli",
- "to": "codecs",
- "label": "read + write",
- "fromSide": "top",
- "toSide": "bottom",
- "labelDx": -70
- },
- {
- "id": "codecs-to-model",
- "from": "codecs",
- "to": "model",
- "label": "build IR",
- "fromSide": "bottom",
- "toSide": "top",
- "labelAt": [440, 240]
- },
- {
- "id": "cli-to-model",
- "from": "cli",
- "to": "model",
- "label": "IR",
- "fromSide": "right",
- "toSide": "left"
- },
- {
- "id": "cli-to-convert",
- "from": "cli",
- "to": "convert",
- "label": "serialize",
- "fromSide": "bottom",
- "toSide": "bottom",
- "via": [[330, 445], [780, 445]]
- },
- {
- "id": "cli-to-mcp",
- "from": "cli",
- "to": "mcp",
- "label": "hwp mcp / serve",
- "fromSide": "bottom",
- "toSide": "top",
- "labelAt": [260, 408]
- },
- {
- "id": "mcp-to-fs",
- "from": "mcp",
- "to": "fs",
- "label": "checked IO",
- "fromSide": "right",
- "toSide": "bottom",
- "labelAt": [700, 570]
- },
- {
- "id": "model-to-convert",
- "from": "model",
- "to": "convert",
- "label": "IR out",
- "fromSide": "right",
- "toSide": "left"
- },
- {
- "id": "convert-to-fs",
- "from": "convert",
- "to": "fs",
- "label": "write",
- "fromSide": "right",
- "toSide": "left"
- },
- {
- "id": "model-to-render",
- "from": "model",
- "to": "render",
- "label": "render",
- "fromSide": "top",
- "toSide": "bottom",
- "labelAt": [670, 250]
- },
- {
- "id": "render-to-fs",
- "from": "render",
- "to": "fs",
- "label": "pages + layout",
- "fromSide": "right",
- "toSide": "top",
- "labelAt": [935, 140]
- },
- {
- "id": "user-to-edge",
- "from": "user",
- "to": "edge",
- "label": "POST /mcp",
- "fromSide": "bottom",
- "toSide": "top",
- "labelAt": [105, 404]
- },
- {
- "id": "edge-to-mcp",
- "from": "edge",
- "to": "mcp",
- "label": "hop",
- "fromSide": "right",
- "toSide": "left"
- }
+ { "id": "user-to-cli", "from": "user", "to": "cli", "label": "hwp ", "variant": "emphasis" },
+ { "id": "cli-to-commands", "from": "cli", "to": "commands", "label": "dispatch" },
+ { "id": "commands-to-convert", "from": "commands", "to": "convert", "label": "convert, fill" },
+ { "id": "convert-to-model", "from": "convert", "to": "model", "label": "edit + read" },
+ { "id": "commands-to-render", "from": "commands", "to": "render", "label": "render", "fromSide": "right", "toSide": "left" },
+ { "id": "commands-to-codecs", "from": "commands", "to": "codecs", "label": "load + write", "fromSide": "right", "toSide": "left" },
+ { "id": "render-to-model", "from": "render", "to": "model", "label": "read IR", "fromSide": "right", "toSide": "top" },
+ { "id": "codecs-to-model", "from": "codecs", "to": "model", "label": "build IR", "fromSide": "right", "toSide": "bottom" },
+ { "id": "codecs-to-fs", "from": "codecs", "to": "fs", "label": "read + write", "fromSide": "bottom", "toSide": "top" },
+ { "id": "commands-to-fs", "from": "commands", "to": "fs", "label": "atomic publish", "fromSide": "bottom", "toSide": "left" },
+ { "id": "cli-to-mcp", "from": "cli", "to": "mcp", "label": "hwp mcp | serve", "fromSide": "bottom", "toSide": "top" },
+ { "id": "mcp-to-fs", "from": "mcp", "to": "fs", "label": "/files, get_file", "fromSide": "bottom", "toSide": "left" },
+ { "id": "mcp-to-commands", "from": "mcp", "to": "commands", "label": "tools/call", "fromSide": "right", "toSide": "bottom" },
+ { "id": "user-to-edge", "from": "user", "to": "edge", "label": "remote MCP", "fromSide": "bottom", "toSide": "top" },
+ { "id": "edge-to-mcp", "from": "edge", "to": "mcp", "label": "POST /mcp", "variant": "security" }
],
"cards": [
{
"dot": "cyan",
"title": "Core pipeline",
"items": [
- "6-crate workspace at v1.0.0, edition 2024; SemVer covers the CLI, MCP tools and schemas",
- "Codecs build the shared hwp-model IR; every format surface reads from it",
- "hwp5 re-serialization stays byte-exact; HWPX parsing is depth-bounded on quick-xml 0.41"
+ "6-crate workspace at v1.3.1, edition 2024; SemVer covers commands, flags, MCP tools and schemas",
+ "hwp5 and hwpx never depend on each other: both go through the hwp-model IR",
+ "Unedited hwp-origin files re-serialize byte-identically (decompressed streams); edited or hwpx/markdown input is rewritten semantically equivalent",
+ "Codecs open inputs by path; command outputs and hwp_put_file are staged, verified, renamed; serve /files writes in place"
]
},
{
"dot": "violet",
- "title": "Formats and rules",
+ "title": "Formats and forms",
"items": [
- "Reads to plain/markdown/HTML/JSON/CSV; writes DOCX, ODT, hwp, hwpx",
- "Renders PNG/SVG/PDF/JPEG/WebP plus layout geometry JSON for editors",
- "26 commands incl. lint (10 rules); schema ids on hwp-cli.staix.net"
+ "cat: plain/markdown/JSON/HTML/CSV; convert to hwp, hwpx, md, json, html, pdf, odt, txt, csv, docx",
+ "hwp-render draws PNG/SVG/PDF; the command layer encodes JPEG/WebP from the raster; render-layout-v1 rows include drawing-object text",
+ "slots --forms and fill --forms: Korean form fields (label cells, label: value, blanks, checkboxes)",
+ "lint: 10 rule ids; 6 official-document profiles"
]
},
{
"dot": "emerald",
- "title": "MCP and trust",
+ "title": "MCP and deployment",
"items": [
- "22-tool stdio MCP server; per-call passwords zeroized before the next request",
- "hwp serve: close-per-request POST /mcp on httparse behind a trusted edge, 1MiB cap",
- "Tier A live on v1.0.0; checked paths refuse IO outside --root"
+ "22 tools reuse the command modules' execute and publish helpers (not their stdout run paths); stdio and HTTP share one handle_request core",
+ "hwp serve: close-per-request POST /mcp on httparse, 1 MiB body cap, private hop behind a trusted edge",
+ "hwp mcp without --root is unrestricted (warns); hwp serve always requires --root",
+ "Tier A Cloudflare (OAuth or PAT, /files sideband) and Tier B AgentCore (/mcp only) images pin v1.3.1"
]
}
]