14848 lines
700 KiB
HTML
14848 lines
700 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="en" data-theme="dark" data-preset="classic">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<meta name="generator" content="archify 2.16.0">
|
|||
|
|
<title>LazyBoy:從一句話到完成任務 Diagram</title>
|
|||
|
|
<script>
|
|||
|
|
// Resolve the theme before first paint so light-preference users don't
|
|||
|
|
// see a dark flash. The toolbar script below re-applies with UI state.
|
|||
|
|
(function () {
|
|||
|
|
try {
|
|||
|
|
var theme = null;
|
|||
|
|
try {
|
|||
|
|
var param = new URLSearchParams(window.location.search).get('theme');
|
|||
|
|
if (param === 'light' || param === 'dark') theme = param;
|
|||
|
|
if (new URLSearchParams(window.location.search).get('embed') === '1') {
|
|||
|
|
document.documentElement.setAttribute('data-embed', 'true');
|
|||
|
|
}
|
|||
|
|
if (new URLSearchParams(window.location.search).get('present') === '1') {
|
|||
|
|
document.documentElement.setAttribute('data-present', 'true');
|
|||
|
|
}
|
|||
|
|
} catch (_) {}
|
|||
|
|
if (!theme) {
|
|||
|
|
try { theme = localStorage.getItem('archify-theme'); } catch (_) {}
|
|||
|
|
}
|
|||
|
|
if (theme !== 'light' && theme !== 'dark') {
|
|||
|
|
theme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|||
|
|
}
|
|||
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|||
|
|
} catch (_) {}
|
|||
|
|
})();
|
|||
|
|
</script>
|
|||
|
|
<!-- Async font load: a blackholed network must not block first paint.
|
|||
|
|
The body font stack falls back to system monospace until it lands. -->
|
|||
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|||
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap"
|
|||
|
|
rel="stylesheet" media="print" onload="this.media='all'">
|
|||
|
|
<noscript>
|
|||
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|||
|
|
</noscript>
|
|||
|
|
<style>
|
|||
|
|
/* ==========================================================
|
|||
|
|
THEME VARIABLES — switch by toggling [data-theme] on <html>
|
|||
|
|
========================================================== */
|
|||
|
|
:root,
|
|||
|
|
[data-theme="dark"] {
|
|||
|
|
--bg: #020617;
|
|||
|
|
--grid: #1e293b;
|
|||
|
|
--text: #ffffff;
|
|||
|
|
--text-muted: #94a3b8;
|
|||
|
|
--text-dim: #475569;
|
|||
|
|
/* UI chrome and menu hints need more contrast than SVG .t-dim accents */
|
|||
|
|
--text-faint: #7d8da1;
|
|||
|
|
|
|||
|
|
--panel: rgba(15, 23, 42, 0.5);
|
|||
|
|
--panel-border: #1e293b;
|
|||
|
|
--lane-fill: rgba(15, 23, 42, 0.22);
|
|||
|
|
--lane-stroke: #334155;
|
|||
|
|
|
|||
|
|
--arrow: #64748b;
|
|||
|
|
--arrow-emphasis: #34d399;
|
|||
|
|
|
|||
|
|
/* Opaque mask color used behind semi-transparent component fills
|
|||
|
|
so the arrows drawn underneath are properly hidden. */
|
|||
|
|
--mask: #0f172a;
|
|||
|
|
|
|||
|
|
--frontend-fill: rgba(8, 51, 68, 0.4);
|
|||
|
|
--frontend-stroke: #22d3ee;
|
|||
|
|
--backend-fill: rgba(6, 78, 59, 0.4);
|
|||
|
|
--backend-stroke: #34d399;
|
|||
|
|
--database-fill: rgba(76, 29, 149, 0.4);
|
|||
|
|
--database-stroke: #a78bfa;
|
|||
|
|
--cloud-fill: rgba(120, 53, 15, 0.3);
|
|||
|
|
--cloud-stroke: #fbbf24;
|
|||
|
|
--security-fill: rgba(136, 19, 55, 0.4);
|
|||
|
|
--security-stroke: #fb7185;
|
|||
|
|
--messagebus-fill: rgba(251, 146, 60, 0.3);
|
|||
|
|
--messagebus-stroke:#fb923c;
|
|||
|
|
--external-fill: rgba(30, 41, 59, 0.5);
|
|||
|
|
--external-stroke: #94a3b8;
|
|||
|
|
|
|||
|
|
--toolbar-bg: rgba(15, 23, 42, 0.8);
|
|||
|
|
--toolbar-border: #334155;
|
|||
|
|
--toolbar-text: #e2e8f0;
|
|||
|
|
--toolbar-hover: rgba(15, 23, 42, 0.95);
|
|||
|
|
--toolbar-menu-bg: #0f172a;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[data-theme="light"] {
|
|||
|
|
--bg: #f8fafc;
|
|||
|
|
--grid: #e2e8f0;
|
|||
|
|
--text: #0f172a;
|
|||
|
|
--text-muted: #64748b;
|
|||
|
|
--text-dim: #94a3b8;
|
|||
|
|
--text-faint: #64748b;
|
|||
|
|
|
|||
|
|
--panel: #ffffff;
|
|||
|
|
--panel-border: #e2e8f0;
|
|||
|
|
--lane-fill: rgba(248, 250, 252, 0.65);
|
|||
|
|
--lane-stroke: #cbd5e1;
|
|||
|
|
|
|||
|
|
--arrow: #94a3b8;
|
|||
|
|
--arrow-emphasis: #059669;
|
|||
|
|
|
|||
|
|
--mask: #ffffff;
|
|||
|
|
|
|||
|
|
--frontend-fill: rgba(34, 211, 238, 0.15);
|
|||
|
|
--frontend-stroke: #0891b2;
|
|||
|
|
--backend-fill: rgba(52, 211, 153, 0.18);
|
|||
|
|
--backend-stroke: #059669;
|
|||
|
|
--database-fill: rgba(167, 139, 250, 0.2);
|
|||
|
|
--database-stroke: #7c3aed;
|
|||
|
|
--cloud-fill: rgba(251, 191, 36, 0.18);
|
|||
|
|
--cloud-stroke: #d97706;
|
|||
|
|
--security-fill: rgba(251, 113, 133, 0.15);
|
|||
|
|
--security-stroke: #e11d48;
|
|||
|
|
--messagebus-fill: rgba(251, 146, 60, 0.15);
|
|||
|
|
--messagebus-stroke:#ea580c;
|
|||
|
|
--external-fill: rgba(148, 163, 184, 0.18);
|
|||
|
|
--external-stroke: #64748b;
|
|||
|
|
|
|||
|
|
--toolbar-bg: rgba(255, 255, 255, 0.92);
|
|||
|
|
--toolbar-border: #cbd5e1;
|
|||
|
|
--toolbar-text: #334155;
|
|||
|
|
--toolbar-hover: #ffffff;
|
|||
|
|
--toolbar-menu-bg: #ffffff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ==========================================================
|
|||
|
|
SIGNAL FLOW — an opt-in, motion-forward visual preset.
|
|||
|
|
The variables also target a standalone exported SVG, whose
|
|||
|
|
root carries data-preset and data-theme directly.
|
|||
|
|
========================================================== */
|
|||
|
|
[data-preset="signal-flow"][data-theme="dark"] {
|
|||
|
|
--bg: #030711;
|
|||
|
|
--grid: #15233a;
|
|||
|
|
--panel: rgba(6, 14, 28, 0.78);
|
|||
|
|
--panel-border: #1d3350;
|
|||
|
|
--lane-fill: rgba(9, 22, 40, 0.5);
|
|||
|
|
--lane-stroke: #2c4564;
|
|||
|
|
--text: #f5fbff;
|
|||
|
|
--text-muted: #9eb0c7;
|
|||
|
|
--text-dim: #52667f;
|
|||
|
|
--text-faint: #7890ad;
|
|||
|
|
--mask: #07101e;
|
|||
|
|
--frontend-fill: rgba(6, 182, 212, 0.14);
|
|||
|
|
--frontend-stroke: #67e8f9;
|
|||
|
|
--backend-fill: rgba(16, 185, 129, 0.14);
|
|||
|
|
--backend-stroke: #5eead4;
|
|||
|
|
--database-fill: rgba(139, 92, 246, 0.16);
|
|||
|
|
--database-stroke: #c4b5fd;
|
|||
|
|
--cloud-fill: rgba(245, 158, 11, 0.13);
|
|||
|
|
--cloud-stroke: #fcd34d;
|
|||
|
|
--security-fill: rgba(244, 63, 94, 0.13);
|
|||
|
|
--security-stroke: #fda4af;
|
|||
|
|
--messagebus-fill: rgba(249, 115, 22, 0.13);
|
|||
|
|
--messagebus-stroke: #fdba74;
|
|||
|
|
--external-fill: rgba(71, 85, 105, 0.24);
|
|||
|
|
--external-stroke: #a5b4c7;
|
|||
|
|
--arrow: #7890ad;
|
|||
|
|
--arrow-emphasis: #2dd4bf;
|
|||
|
|
--toolbar-bg: rgba(7, 16, 30, 0.86);
|
|||
|
|
--toolbar-border: #29415f;
|
|||
|
|
--toolbar-menu-bg: #07101e;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[data-preset="signal-flow"][data-theme="light"] {
|
|||
|
|
--bg: #f4f9fc;
|
|||
|
|
--grid: #d4e5ee;
|
|||
|
|
--panel: rgba(255, 255, 255, 0.82);
|
|||
|
|
--panel-border: #bfd5e2;
|
|||
|
|
--lane-fill: rgba(232, 243, 248, 0.62);
|
|||
|
|
--lane-stroke: #a9c5d5;
|
|||
|
|
--text: #102638;
|
|||
|
|
--text-muted: #587287;
|
|||
|
|
--text-dim: #8aa2b4;
|
|||
|
|
--text-faint: #668397;
|
|||
|
|
--mask: #ffffff;
|
|||
|
|
--frontend-fill: rgba(6, 182, 212, 0.09);
|
|||
|
|
--frontend-stroke: #0789a1;
|
|||
|
|
--backend-fill: rgba(5, 150, 105, 0.09);
|
|||
|
|
--backend-stroke: #087f69;
|
|||
|
|
--database-fill: rgba(124, 58, 237, 0.09);
|
|||
|
|
--database-stroke: #7254c7;
|
|||
|
|
--cloud-fill: rgba(217, 119, 6, 0.08);
|
|||
|
|
--cloud-stroke: #b9670b;
|
|||
|
|
--security-fill: rgba(225, 29, 72, 0.08);
|
|||
|
|
--security-stroke: #c53a59;
|
|||
|
|
--messagebus-fill: rgba(234, 88, 12, 0.08);
|
|||
|
|
--messagebus-stroke: #c65f27;
|
|||
|
|
--external-fill: rgba(100, 116, 139, 0.1);
|
|||
|
|
--external-stroke: #607a8c;
|
|||
|
|
--arrow: #7b97aa;
|
|||
|
|
--arrow-emphasis: #0d9488;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ==========================================================
|
|||
|
|
BLUEPRINT — a review-first drafting preset for deployment,
|
|||
|
|
infrastructure, and technical design documentation. It keeps
|
|||
|
|
the same semantic palette and geometry while trading glow for
|
|||
|
|
precise grid contrast, squared materials, and drafting marks.
|
|||
|
|
========================================================== */
|
|||
|
|
[data-preset="blueprint"][data-theme="dark"] {
|
|||
|
|
--bg: #06131f;
|
|||
|
|
--grid: #17425a;
|
|||
|
|
--panel: rgba(7, 27, 43, 0.9);
|
|||
|
|
--panel-border: #27627f;
|
|||
|
|
--lane-fill: rgba(10, 43, 66, 0.36);
|
|||
|
|
--lane-stroke: #34799a;
|
|||
|
|
--text: #e3f6ff;
|
|||
|
|
--text-muted: #91b8ca;
|
|||
|
|
--text-dim: #557e92;
|
|||
|
|
--text-faint: #739caf;
|
|||
|
|
--mask: #0a2031;
|
|||
|
|
--frontend-fill: rgba(26, 157, 193, 0.14);
|
|||
|
|
--frontend-stroke: #66d9ef;
|
|||
|
|
--backend-fill: rgba(31, 155, 124, 0.13);
|
|||
|
|
--backend-stroke: #69dfbd;
|
|||
|
|
--database-fill: rgba(120, 105, 196, 0.14);
|
|||
|
|
--database-stroke: #b4a8ff;
|
|||
|
|
--cloud-fill: rgba(197, 145, 43, 0.13);
|
|||
|
|
--cloud-stroke: #ffd166;
|
|||
|
|
--security-fill: rgba(199, 76, 104, 0.13);
|
|||
|
|
--security-stroke: #ff8da1;
|
|||
|
|
--messagebus-fill: rgba(207, 112, 49, 0.13);
|
|||
|
|
--messagebus-stroke: #ffad66;
|
|||
|
|
--external-fill: rgba(84, 119, 139, 0.18);
|
|||
|
|
--external-stroke: #a7cad9;
|
|||
|
|
--arrow: #78a3b7;
|
|||
|
|
--arrow-emphasis: #64dfc1;
|
|||
|
|
--toolbar-bg: rgba(7, 28, 45, 0.92);
|
|||
|
|
--toolbar-border: #34799a;
|
|||
|
|
--toolbar-text: #d8f3ff;
|
|||
|
|
--toolbar-hover: rgba(18, 58, 83, 0.95);
|
|||
|
|
--toolbar-menu-bg: #0a2031;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[data-preset="blueprint"][data-theme="light"] {
|
|||
|
|
--bg: #edf7fa;
|
|||
|
|
--grid: #b5d5e1;
|
|||
|
|
--panel: rgba(249, 253, 255, 0.94);
|
|||
|
|
--panel-border: #78aabd;
|
|||
|
|
--lane-fill: rgba(210, 232, 240, 0.42);
|
|||
|
|
--lane-stroke: #83b2c4;
|
|||
|
|
--text: #123344;
|
|||
|
|
--text-muted: #4e7486;
|
|||
|
|
--text-dim: #86a6b4;
|
|||
|
|
--text-faint: #668c9d;
|
|||
|
|
--mask: #f9fdff;
|
|||
|
|
--frontend-fill: rgba(8, 145, 178, 0.08);
|
|||
|
|
--frontend-stroke: #087f9c;
|
|||
|
|
--backend-fill: rgba(5, 128, 101, 0.08);
|
|||
|
|
--backend-stroke: #08755f;
|
|||
|
|
--database-fill: rgba(100, 75, 180, 0.08);
|
|||
|
|
--database-stroke: #6757a8;
|
|||
|
|
--cloud-fill: rgba(181, 120, 15, 0.08);
|
|||
|
|
--cloud-stroke: #a86609;
|
|||
|
|
--security-fill: rgba(190, 48, 80, 0.07);
|
|||
|
|
--security-stroke: #b32f50;
|
|||
|
|
--messagebus-fill: rgba(196, 81, 22, 0.07);
|
|||
|
|
--messagebus-stroke: #b65120;
|
|||
|
|
--external-fill: rgba(79, 112, 128, 0.08);
|
|||
|
|
--external-stroke: #506f7e;
|
|||
|
|
--arrow: #6d93a5;
|
|||
|
|
--arrow-emphasis: #087f69;
|
|||
|
|
--toolbar-bg: rgba(247, 252, 254, 0.94);
|
|||
|
|
--toolbar-border: #8ab5c5;
|
|||
|
|
--toolbar-text: #294f61;
|
|||
|
|
--toolbar-hover: #ffffff;
|
|||
|
|
--toolbar-menu-bg: #f9fdff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ==========================================================
|
|||
|
|
EDITORIAL — a warm, publication-minded preset for design
|
|||
|
|
reviews, launch notes, and documentation. The diagram keeps
|
|||
|
|
the same semantic palette and exact geometry, but trades
|
|||
|
|
software chrome for paper, ink, ruled structure, and one
|
|||
|
|
restrained vermilion accent.
|
|||
|
|
========================================================== */
|
|||
|
|
[data-preset="editorial"][data-theme="dark"] {
|
|||
|
|
--bg: #181611;
|
|||
|
|
--grid: #39342a;
|
|||
|
|
--panel: rgba(35, 31, 24, 0.96);
|
|||
|
|
--panel-border: #625a4a;
|
|||
|
|
--lane-fill: rgba(52, 46, 35, 0.46);
|
|||
|
|
--lane-stroke: #726957;
|
|||
|
|
--text: #f4eddf;
|
|||
|
|
--text-muted: #b9ae9b;
|
|||
|
|
--text-dim: #776e60;
|
|||
|
|
--text-faint: #9d917e;
|
|||
|
|
--mask: #231f18;
|
|||
|
|
--frontend-fill: rgba(43, 133, 142, 0.16);
|
|||
|
|
--frontend-stroke: #7fc6c7;
|
|||
|
|
--backend-fill: rgba(63, 132, 92, 0.16);
|
|||
|
|
--backend-stroke: #8fc29e;
|
|||
|
|
--database-fill: rgba(117, 91, 141, 0.17);
|
|||
|
|
--database-stroke: #c0a4d0;
|
|||
|
|
--cloud-fill: rgba(170, 119, 49, 0.16);
|
|||
|
|
--cloud-stroke: #d8ad68;
|
|||
|
|
--security-fill: rgba(157, 69, 65, 0.17);
|
|||
|
|
--security-stroke: #df9085;
|
|||
|
|
--messagebus-fill: rgba(174, 79, 42, 0.16);
|
|||
|
|
--messagebus-stroke: #df946f;
|
|||
|
|
--external-fill: rgba(126, 115, 96, 0.18);
|
|||
|
|
--external-stroke: #b8ad99;
|
|||
|
|
--arrow: #948978;
|
|||
|
|
--arrow-emphasis: #dd6b3d;
|
|||
|
|
--toolbar-bg: rgba(35, 31, 24, 0.92);
|
|||
|
|
--toolbar-border: #625a4a;
|
|||
|
|
--toolbar-text: #eee5d5;
|
|||
|
|
--toolbar-hover: #302a20;
|
|||
|
|
--toolbar-menu-bg: #231f18;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[data-preset="editorial"][data-theme="light"] {
|
|||
|
|
--bg: #f2eee5;
|
|||
|
|
--grid: #d8d0c2;
|
|||
|
|
--panel: rgba(251, 248, 241, 0.97);
|
|||
|
|
--panel-border: #c4b9a6;
|
|||
|
|
--lane-fill: rgba(229, 221, 207, 0.42);
|
|||
|
|
--lane-stroke: #b8aa94;
|
|||
|
|
--text: #242018;
|
|||
|
|
--text-muted: #6f6658;
|
|||
|
|
--text-dim: #a09788;
|
|||
|
|
--text-faint: #817767;
|
|||
|
|
--mask: #fbf8f1;
|
|||
|
|
--frontend-fill: rgba(31, 117, 126, 0.09);
|
|||
|
|
--frontend-stroke: #287e84;
|
|||
|
|
--backend-fill: rgba(42, 119, 75, 0.09);
|
|||
|
|
--backend-stroke: #397b53;
|
|||
|
|
--database-fill: rgba(105, 75, 130, 0.09);
|
|||
|
|
--database-stroke: #765d86;
|
|||
|
|
--cloud-fill: rgba(157, 103, 31, 0.1);
|
|||
|
|
--cloud-stroke: #9a671f;
|
|||
|
|
--security-fill: rgba(153, 58, 52, 0.09);
|
|||
|
|
--security-stroke: #9e463f;
|
|||
|
|
--messagebus-fill: rgba(182, 70, 27, 0.09);
|
|||
|
|
--messagebus-stroke: #ad4b25;
|
|||
|
|
--external-fill: rgba(101, 91, 75, 0.09);
|
|||
|
|
--external-stroke: #746b5e;
|
|||
|
|
--arrow: #8a806f;
|
|||
|
|
--arrow-emphasis: #bb4c23;
|
|||
|
|
--toolbar-bg: rgba(251, 248, 241, 0.94);
|
|||
|
|
--toolbar-border: #c4b9a6;
|
|||
|
|
--toolbar-text: #40392f;
|
|||
|
|
--toolbar-hover: #fffdf8;
|
|||
|
|
--toolbar-menu-bg: #fbf8f1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|||
|
|
|
|||
|
|
body {
|
|||
|
|
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;
|
|||
|
|
background: var(--bg);
|
|||
|
|
min-height: 100vh;
|
|||
|
|
padding: 2rem;
|
|||
|
|
color: var(--text);
|
|||
|
|
transition: background 0.2s ease, color 0.2s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
html[data-preset="signal-flow"] body {
|
|||
|
|
background-image:
|
|||
|
|
radial-gradient(circle at 18% -8%, rgba(34, 211, 238, 0.14), transparent 34rem),
|
|||
|
|
radial-gradient(circle at 92% 18%, rgba(139, 92, 246, 0.12), transparent 30rem);
|
|||
|
|
background-attachment: fixed;
|
|||
|
|
}
|
|||
|
|
html[data-preset="signal-flow"][data-theme="light"] body {
|
|||
|
|
background-image:
|
|||
|
|
radial-gradient(circle at 18% -8%, rgba(6, 182, 212, 0.12), transparent 34rem),
|
|||
|
|
radial-gradient(circle at 92% 18%, rgba(139, 92, 246, 0.08), transparent 30rem);
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] body {
|
|||
|
|
background-image:
|
|||
|
|
linear-gradient(var(--grid) 1px, transparent 1px),
|
|||
|
|
linear-gradient(90deg, var(--grid) 1px, transparent 1px);
|
|||
|
|
background-size: 32px 32px;
|
|||
|
|
background-position: -1px -1px;
|
|||
|
|
background-attachment: fixed;
|
|||
|
|
}
|
|||
|
|
html[data-preset="editorial"] body {
|
|||
|
|
background-image:
|
|||
|
|
linear-gradient(90deg, transparent 0, transparent 5.4rem, color-mix(in srgb, var(--security-stroke) 18%, transparent) 5.4rem, color-mix(in srgb, var(--security-stroke) 18%, transparent) calc(5.4rem + 1px), transparent calc(5.4rem + 1px)),
|
|||
|
|
repeating-linear-gradient(0deg, transparent 0, transparent 31px, color-mix(in srgb, var(--grid) 36%, transparent) 31px, color-mix(in srgb, var(--grid) 36%, transparent) 32px);
|
|||
|
|
background-attachment: fixed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Gallery/embed mode keeps the generated artifact as the source of truth
|
|||
|
|
while presenting only its diagram surface inside a proof card. */
|
|||
|
|
html[data-embed="true"] body {
|
|||
|
|
min-height: 0;
|
|||
|
|
padding: 0;
|
|||
|
|
overflow: hidden;
|
|||
|
|
background: var(--bg);
|
|||
|
|
background-image: none;
|
|||
|
|
}
|
|||
|
|
html[data-embed="true"] .toolbar,
|
|||
|
|
html[data-embed="true"] .header,
|
|||
|
|
html[data-embed="true"] .cards,
|
|||
|
|
html[data-embed="true"] .diagram-nav,
|
|||
|
|
html[data-embed="true"] .overview-map,
|
|||
|
|
html[data-embed="true"] .focus-chip,
|
|||
|
|
html[data-embed="true"] .route-probe,
|
|||
|
|
html[data-embed="true"] .semantic-lens,
|
|||
|
|
html[data-embed="true"] .diagram-guide,
|
|||
|
|
html[data-embed="true"] .node-finder,
|
|||
|
|
html[data-embed="true"] .guided-views { display: none !important; }
|
|||
|
|
html[data-embed="true"] .container { max-width: none; }
|
|||
|
|
html[data-embed="true"] .diagram-container {
|
|||
|
|
padding: 0.5rem;
|
|||
|
|
border: 0;
|
|||
|
|
border-radius: 0;
|
|||
|
|
box-shadow: none;
|
|||
|
|
}
|
|||
|
|
html[data-embed="true"] .diagram-container svg { min-width: 0; }
|
|||
|
|
|
|||
|
|
/* Share Chapter Cue lives in the HTML viewer layer, not inside the SVG.
|
|||
|
|
It gives one-shot embeds a title, authored note, truthful stop order,
|
|||
|
|
and lifecycle state while canonical diagram geometry stays untouched. */
|
|||
|
|
.share-chapter-cue { display: none; }
|
|||
|
|
html[data-embed="true"][data-share-playback="true"] .share-chapter-cue:not([hidden]),
|
|||
|
|
html[data-embed="true"][data-share-moment="true"] .share-chapter-cue:not([hidden]) {
|
|||
|
|
position: absolute;
|
|||
|
|
z-index: 18;
|
|||
|
|
top: 0.85rem;
|
|||
|
|
left: 50%;
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: auto minmax(9rem, 13.5rem) minmax(0, 1fr);
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.45rem 0.85rem;
|
|||
|
|
width: min(52rem, calc(100% - 1.5rem));
|
|||
|
|
min-height: 3.35rem;
|
|||
|
|
padding: 0.55rem 0.75rem 0.65rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--frontend-stroke) 42%, var(--toolbar-border));
|
|||
|
|
border-radius: 0.75rem;
|
|||
|
|
background:
|
|||
|
|
linear-gradient(115deg, color-mix(in srgb, var(--frontend-fill) 52%, transparent), transparent 45%),
|
|||
|
|
color-mix(in srgb, var(--toolbar-menu-bg) 92%, transparent);
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
box-shadow: 0 14px 42px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.045);
|
|||
|
|
backdrop-filter: blur(16px);
|
|||
|
|
pointer-events: none;
|
|||
|
|
animation: archify-share-cue-enter 280ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] .share-chapter-cue {
|
|||
|
|
border-radius: 0.2rem;
|
|||
|
|
background:
|
|||
|
|
linear-gradient(90deg, color-mix(in srgb, var(--frontend-fill) 46%, transparent), transparent 36%),
|
|||
|
|
var(--toolbar-menu-bg);
|
|||
|
|
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.2);
|
|||
|
|
backdrop-filter: none;
|
|||
|
|
}
|
|||
|
|
html[data-preset="editorial"] .share-chapter-cue {
|
|||
|
|
border-color: var(--panel-border);
|
|||
|
|
background:
|
|||
|
|
linear-gradient(90deg, color-mix(in srgb, var(--security-fill) 58%, transparent), transparent 42%),
|
|||
|
|
var(--toolbar-menu-bg);
|
|||
|
|
box-shadow: 0 10px 30px color-mix(in srgb, #000 18%, transparent);
|
|||
|
|
backdrop-filter: none;
|
|||
|
|
}
|
|||
|
|
html[data-preset="editorial"] .share-chapter-state { color: var(--arrow-emphasis); }
|
|||
|
|
html[data-preset="editorial"] .share-chapter-state::before {
|
|||
|
|
border-radius: 0;
|
|||
|
|
box-shadow: none;
|
|||
|
|
transform: rotate(45deg) scale(0.8);
|
|||
|
|
}
|
|||
|
|
.share-chapter-index {
|
|||
|
|
display: grid;
|
|||
|
|
gap: 0.12rem;
|
|||
|
|
min-width: 5rem;
|
|||
|
|
padding-right: 0.75rem;
|
|||
|
|
border-right: 1px solid var(--toolbar-border);
|
|||
|
|
}
|
|||
|
|
.share-chapter-state {
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.36rem;
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
font-weight: 800;
|
|||
|
|
letter-spacing: 0.14em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.share-chapter-state::before {
|
|||
|
|
width: 0.38rem;
|
|||
|
|
height: 0.38rem;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: currentColor;
|
|||
|
|
box-shadow: 0 0 10px currentColor;
|
|||
|
|
content: '';
|
|||
|
|
}
|
|||
|
|
.share-chapter-cue[data-state="playing"] .share-chapter-state::before {
|
|||
|
|
animation: archify-share-cue-pulse 1.15s ease-in-out infinite;
|
|||
|
|
}
|
|||
|
|
.share-chapter-count {
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
font-weight: 700;
|
|||
|
|
letter-spacing: 0.08em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.share-chapter-copy { min-width: 0; }
|
|||
|
|
.share-chapter-copy strong,
|
|||
|
|
.share-chapter-copy small,
|
|||
|
|
.share-chapter-route {
|
|||
|
|
display: block;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.share-chapter-copy strong {
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
font-size: 0.6875rem;
|
|||
|
|
line-height: 1.35;
|
|||
|
|
}
|
|||
|
|
.share-chapter-copy small {
|
|||
|
|
margin-top: 0.08rem;
|
|||
|
|
color: var(--text-muted);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
line-height: 1.35;
|
|||
|
|
}
|
|||
|
|
.share-chapter-route {
|
|||
|
|
color: color-mix(in srgb, var(--frontend-stroke) 76%, var(--text-muted));
|
|||
|
|
font-size: 0.5625rem;
|
|||
|
|
font-weight: 650;
|
|||
|
|
letter-spacing: 0.015em;
|
|||
|
|
text-align: right;
|
|||
|
|
}
|
|||
|
|
.share-chapter-progress {
|
|||
|
|
position: absolute;
|
|||
|
|
right: 0;
|
|||
|
|
bottom: 0;
|
|||
|
|
left: 0;
|
|||
|
|
height: 2px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
background: color-mix(in srgb, var(--toolbar-border) 72%, transparent);
|
|||
|
|
}
|
|||
|
|
.share-chapter-progress span {
|
|||
|
|
display: block;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
background: linear-gradient(90deg, var(--frontend-stroke), var(--database-stroke));
|
|||
|
|
box-shadow: 0 0 12px var(--frontend-stroke);
|
|||
|
|
transform: scaleX(0);
|
|||
|
|
transform-origin: left center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Presentation Stage is a viewer-only layer: it gives the live diagram
|
|||
|
|
the viewport while keeping guided views, theme, focus, and exports real.
|
|||
|
|
Embed mode wins when both query parameters are present. */
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) body {
|
|||
|
|
height: 100dvh;
|
|||
|
|
min-height: 0;
|
|||
|
|
padding: 1rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .container {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 0.75rem;
|
|||
|
|
width: 100%;
|
|||
|
|
max-width: none;
|
|||
|
|
height: calc(100dvh - 2rem);
|
|||
|
|
}
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .header {
|
|||
|
|
flex: none;
|
|||
|
|
min-width: 0;
|
|||
|
|
margin: 0;
|
|||
|
|
padding-right: 23rem;
|
|||
|
|
}
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .header-row { margin-bottom: 0.25rem; }
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .subtitle {
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .guided-views {
|
|||
|
|
flex: none;
|
|||
|
|
margin-bottom: 0;
|
|||
|
|
}
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .diagram-container {
|
|||
|
|
display: flex;
|
|||
|
|
flex: 1 1 auto;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
min-height: 0;
|
|||
|
|
padding: 0.75rem;
|
|||
|
|
padding-bottom: calc(0.75rem + var(--archify-nav-reserve));
|
|||
|
|
}
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .diagram-container > svg {
|
|||
|
|
flex: 1 1 auto;
|
|||
|
|
height: 100%;
|
|||
|
|
min-height: 0;
|
|||
|
|
width: 100%;
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .cards { display: none; }
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) #btn-present {
|
|||
|
|
border-color: var(--frontend-stroke);
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* The reader owns only the outer scale. Wide diagrams keep one authored
|
|||
|
|
SVG/viewBox while the runtime chooses a desktop width from the actual
|
|||
|
|
vertical budget. This avoids device-specific files and breakpoint
|
|||
|
|
jumps between a laptop and a tall monitor. */
|
|||
|
|
.container {
|
|||
|
|
width: 100%;
|
|||
|
|
max-width: var(--archify-reader-width, 1440px);
|
|||
|
|
margin: 0 auto;
|
|||
|
|
}
|
|||
|
|
@media (min-width: 768px) and (max-height: 920px) {
|
|||
|
|
.header { margin-bottom: 1rem; }
|
|||
|
|
.diagram-container { padding: 1rem; }
|
|||
|
|
.cards { margin-top: 1rem; }
|
|||
|
|
.card { padding: 1rem; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header { margin-bottom: 1.5rem; padding-right: 29.5rem; }
|
|||
|
|
|
|||
|
|
html[data-preset="signal-flow"] .header,
|
|||
|
|
html[data-preset="blueprint"] .header,
|
|||
|
|
html[data-preset="editorial"] .header { padding-right: 29.5rem; }
|
|||
|
|
|
|||
|
|
.header-row {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 1rem;
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.pulse-dot {
|
|||
|
|
width: 12px;
|
|||
|
|
height: 12px;
|
|||
|
|
background: var(--frontend-stroke);
|
|||
|
|
border-radius: 50%;
|
|||
|
|
animation: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
html[data-motion-capable="true"] .pulse-dot { animation: pulse 2s infinite; }
|
|||
|
|
|
|||
|
|
@keyframes pulse {
|
|||
|
|
0%, 100% { opacity: 1; }
|
|||
|
|
50% { opacity: 0.5; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
h1 {
|
|||
|
|
font-size: 1.5rem;
|
|||
|
|
font-weight: 700;
|
|||
|
|
letter-spacing: -0.025em;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.subtitle {
|
|||
|
|
color: var(--text-muted);
|
|||
|
|
font-size: 0.875rem;
|
|||
|
|
margin-left: 1.75rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
html[data-preset="signal-flow"] .header-row::after {
|
|||
|
|
content: attr(data-preset-badge-signal-flow);
|
|||
|
|
margin-left: auto;
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
border: 1px solid var(--frontend-stroke);
|
|||
|
|
border-radius: 999px;
|
|||
|
|
padding: 0.28rem 0.6rem;
|
|||
|
|
font-size: 0.625rem;
|
|||
|
|
letter-spacing: 0.12em;
|
|||
|
|
opacity: 0.82;
|
|||
|
|
box-shadow: 0 0 20px rgba(34, 211, 238, 0.12);
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
html[data-preset="blueprint"] .pulse-dot {
|
|||
|
|
width: 10px;
|
|||
|
|
height: 10px;
|
|||
|
|
border: 1px solid var(--frontend-stroke);
|
|||
|
|
border-radius: 1px;
|
|||
|
|
background: transparent;
|
|||
|
|
box-shadow: inset 0 0 0 2px var(--bg);
|
|||
|
|
transform: rotate(45deg);
|
|||
|
|
animation: none;
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] .header-row::after {
|
|||
|
|
content: attr(data-preset-badge-blueprint);
|
|||
|
|
margin-left: auto;
|
|||
|
|
padding: 0.28rem 0.55rem;
|
|||
|
|
border-top: 1px solid var(--frontend-stroke);
|
|||
|
|
border-bottom: 1px solid var(--frontend-stroke);
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
font-size: 0.5625rem;
|
|||
|
|
font-weight: 700;
|
|||
|
|
letter-spacing: 0.14em;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
html[data-preset="editorial"] h1,
|
|||
|
|
html[data-preset="editorial"] .card h3 {
|
|||
|
|
font-family: Georgia, 'Times New Roman', 'Songti SC', STSong, serif;
|
|||
|
|
letter-spacing: -0.02em;
|
|||
|
|
}
|
|||
|
|
html[data-preset="editorial"] h1 { font-size: 1.72rem; font-weight: 600; }
|
|||
|
|
html[data-preset="editorial"] .pulse-dot {
|
|||
|
|
width: 9px;
|
|||
|
|
height: 9px;
|
|||
|
|
border: 1px solid var(--arrow-emphasis);
|
|||
|
|
border-radius: 0;
|
|||
|
|
background: var(--arrow-emphasis);
|
|||
|
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--arrow-emphasis) 14%, transparent);
|
|||
|
|
transform: rotate(45deg);
|
|||
|
|
animation: none;
|
|||
|
|
}
|
|||
|
|
html[data-preset="editorial"] .header-row::after {
|
|||
|
|
content: attr(data-preset-badge-editorial);
|
|||
|
|
margin-left: auto;
|
|||
|
|
padding: 0.32rem 0 0.24rem;
|
|||
|
|
border-bottom: 2px solid var(--arrow-emphasis);
|
|||
|
|
color: var(--arrow-emphasis);
|
|||
|
|
font-family: Georgia, 'Times New Roman', serif;
|
|||
|
|
font-size: 0.625rem;
|
|||
|
|
font-style: italic;
|
|||
|
|
font-weight: 600;
|
|||
|
|
letter-spacing: 0.08em;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.diagram-container {
|
|||
|
|
--archify-nav-reserve: 0px;
|
|||
|
|
background: var(--panel);
|
|||
|
|
border-radius: 1rem;
|
|||
|
|
border: 1px solid var(--panel-border);
|
|||
|
|
padding: 1.5rem;
|
|||
|
|
padding-bottom: calc(1.5rem + var(--archify-nav-reserve));
|
|||
|
|
overflow: hidden;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
html[data-preset="signal-flow"] .diagram-container {
|
|||
|
|
position: relative;
|
|||
|
|
overflow: hidden;
|
|||
|
|
background: linear-gradient(180deg, rgba(8, 20, 38, 0.82), rgba(3, 8, 18, 0.94));
|
|||
|
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 28px 80px rgba(0, 0, 0, 0.34);
|
|||
|
|
}
|
|||
|
|
html[data-preset="signal-flow"][data-theme="light"] .diagram-container {
|
|||
|
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(237, 246, 250, 0.94));
|
|||
|
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72), 0 24px 60px rgba(45, 78, 99, 0.12);
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] .diagram-container {
|
|||
|
|
border-color: var(--panel-border);
|
|||
|
|
border-radius: 0.35rem;
|
|||
|
|
background: var(--panel);
|
|||
|
|
box-shadow:
|
|||
|
|
inset 0 0 0 1px color-mix(in srgb, var(--panel-border) 34%, transparent),
|
|||
|
|
0 18px 48px rgba(0, 0, 0, 0.16);
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] .diagram-container::after {
|
|||
|
|
content: "";
|
|||
|
|
position: absolute;
|
|||
|
|
z-index: 10;
|
|||
|
|
inset: 0.7rem;
|
|||
|
|
pointer-events: none;
|
|||
|
|
background:
|
|||
|
|
linear-gradient(var(--frontend-stroke), var(--frontend-stroke)) left top / 1.35rem 1px no-repeat,
|
|||
|
|
linear-gradient(var(--frontend-stroke), var(--frontend-stroke)) left top / 1px 1.35rem no-repeat,
|
|||
|
|
linear-gradient(var(--frontend-stroke), var(--frontend-stroke)) right top / 1.35rem 1px no-repeat,
|
|||
|
|
linear-gradient(var(--frontend-stroke), var(--frontend-stroke)) right top / 1px 1.35rem no-repeat,
|
|||
|
|
linear-gradient(var(--frontend-stroke), var(--frontend-stroke)) left bottom / 1.35rem 1px no-repeat,
|
|||
|
|
linear-gradient(var(--frontend-stroke), var(--frontend-stroke)) left bottom / 1px 1.35rem no-repeat,
|
|||
|
|
linear-gradient(var(--frontend-stroke), var(--frontend-stroke)) right bottom / 1.35rem 1px no-repeat,
|
|||
|
|
linear-gradient(var(--frontend-stroke), var(--frontend-stroke)) right bottom / 1px 1.35rem no-repeat;
|
|||
|
|
opacity: 0.58;
|
|||
|
|
}
|
|||
|
|
html[data-preset="editorial"] .diagram-container {
|
|||
|
|
overflow: hidden;
|
|||
|
|
border-color: var(--panel-border);
|
|||
|
|
border-radius: 0.38rem;
|
|||
|
|
background:
|
|||
|
|
linear-gradient(90deg, color-mix(in srgb, var(--security-fill) 58%, transparent), transparent 12%),
|
|||
|
|
var(--panel);
|
|||
|
|
box-shadow:
|
|||
|
|
0 1px 0 color-mix(in srgb, var(--text) 6%, transparent),
|
|||
|
|
0 18px 48px color-mix(in srgb, #000 19%, transparent);
|
|||
|
|
}
|
|||
|
|
html[data-preset="editorial"] .diagram-container::after {
|
|||
|
|
position: absolute;
|
|||
|
|
z-index: 10;
|
|||
|
|
top: 0.72rem;
|
|||
|
|
right: 1.25rem;
|
|||
|
|
padding: 0.2rem 0.52rem;
|
|||
|
|
border: 1px solid var(--panel-border);
|
|||
|
|
background: var(--panel);
|
|||
|
|
color: var(--arrow-emphasis);
|
|||
|
|
content: attr(data-preset-badge-editorial-plate);
|
|||
|
|
font-family: Georgia, 'Times New Roman', serif;
|
|||
|
|
font-size: 0.52rem;
|
|||
|
|
font-style: italic;
|
|||
|
|
font-weight: 600;
|
|||
|
|
letter-spacing: 0.09em;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
html[data-embed="true"][data-preset="editorial"] .diagram-container::after { display: none; }
|
|||
|
|
html[data-motion-capable="true"][data-preset="signal-flow"][data-ambient-motion="running"] .diagram-container::before {
|
|||
|
|
content: "";
|
|||
|
|
position: absolute;
|
|||
|
|
inset: 0;
|
|||
|
|
pointer-events: none;
|
|||
|
|
background: linear-gradient(115deg, transparent 28%, rgba(103, 232, 249, 0.035) 48%, transparent 68%);
|
|||
|
|
transform: translateX(-70%);
|
|||
|
|
animation: archify-signal-scan 4.8s ease-in-out 1 both;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Motion Governor gives the reader a real static state and gives the
|
|||
|
|
strongest semantic action the only motion budget. Atmosphere resets to
|
|||
|
|
its authored base instead of freezing mid-cycle. Static artifacts never
|
|||
|
|
start a decorative pulse or scan. */
|
|||
|
|
html:not([data-motion-capable="true"]) svg[data-animation="trace"] [data-animate] {
|
|||
|
|
animation: none !important;
|
|||
|
|
}
|
|||
|
|
html[data-motion="still"] .pulse-dot,
|
|||
|
|
html[data-motion="still"] .diagram-container::before,
|
|||
|
|
html[data-motion="still"] svg[data-animation="trace"] [data-animate],
|
|||
|
|
html[data-motion-owner] .pulse-dot,
|
|||
|
|
html[data-motion-owner] .diagram-container::before,
|
|||
|
|
html[data-motion-owner] svg[data-animation="trace"] [data-animate],
|
|||
|
|
html[data-embed="true"] .pulse-dot,
|
|||
|
|
html[data-embed="true"] .diagram-container::before,
|
|||
|
|
html[data-share-playback="true"] .pulse-dot,
|
|||
|
|
html[data-share-playback="true"] .diagram-container::before,
|
|||
|
|
html[data-document-hidden="true"] .pulse-dot,
|
|||
|
|
html[data-document-hidden="true"] .diagram-container::before,
|
|||
|
|
html[data-document-hidden="true"] svg[data-animation="trace"] [data-animate] {
|
|||
|
|
animation: none !important;
|
|||
|
|
}
|
|||
|
|
html[data-motion="still"] .diagram-container::before,
|
|||
|
|
html[data-motion-owner] .diagram-container::before,
|
|||
|
|
html[data-embed="true"] .diagram-container::before,
|
|||
|
|
html[data-share-playback="true"] .diagram-container::before {
|
|||
|
|
opacity: 0;
|
|||
|
|
}
|
|||
|
|
html[data-motion="still"] .guided-view-progress span,
|
|||
|
|
html[data-motion="still"] .guided-story-caption,
|
|||
|
|
html[data-motion="still"] .story-trail-flow,
|
|||
|
|
html[data-motion="still"] .intent-trace-flow,
|
|||
|
|
html[data-motion="still"] .route-probe-flow,
|
|||
|
|
html[data-motion="still"] .route-journey-flow,
|
|||
|
|
html[data-motion="still"] .semantic-lens-flow,
|
|||
|
|
html[data-motion="still"] .diagram-guide,
|
|||
|
|
html[data-motion="still"] [data-story-step],
|
|||
|
|
html[data-motion="still"] .overview-map-live,
|
|||
|
|
html[data-motion="still"] .guided-view-stop,
|
|||
|
|
html[data-motion="still"] .share-chapter-cue,
|
|||
|
|
html[data-motion="still"] .share-chapter-state::before,
|
|||
|
|
html[data-motion="still"] .share-chapter-progress span,
|
|||
|
|
html[data-document-hidden="true"] .guided-view-progress span,
|
|||
|
|
html[data-document-hidden="true"] .story-trail-flow,
|
|||
|
|
html[data-document-hidden="true"] .intent-trace-flow,
|
|||
|
|
html[data-document-hidden="true"] .route-probe-flow,
|
|||
|
|
html[data-document-hidden="true"] .route-journey-flow,
|
|||
|
|
html[data-document-hidden="true"] .semantic-lens-flow,
|
|||
|
|
html[data-document-hidden="true"] .diagram-guide,
|
|||
|
|
html[data-document-hidden="true"] [data-story-step],
|
|||
|
|
html[data-document-hidden="true"] .overview-map-live,
|
|||
|
|
html[data-document-hidden="true"] .guided-view-stop,
|
|||
|
|
html[data-document-hidden="true"] .share-chapter-cue,
|
|||
|
|
html[data-document-hidden="true"] .share-chapter-state::before,
|
|||
|
|
html[data-document-hidden="true"] .share-chapter-progress span {
|
|||
|
|
animation: none !important;
|
|||
|
|
}
|
|||
|
|
html[data-motion="still"] .guided-view-progress span {
|
|||
|
|
transform: scaleX(1) !important;
|
|||
|
|
}
|
|||
|
|
html[data-motion="still"] svg[data-story-playing="true"] .story-trail-flow {
|
|||
|
|
stroke-dasharray: none;
|
|||
|
|
stroke-dashoffset: 0;
|
|||
|
|
opacity: 0.55;
|
|||
|
|
}
|
|||
|
|
html[data-motion="still"] .intent-trace-flow,
|
|||
|
|
html[data-motion="still"] .route-probe-flow {
|
|||
|
|
stroke-dasharray: none;
|
|||
|
|
stroke-dashoffset: 0;
|
|||
|
|
opacity: 0.72;
|
|||
|
|
}
|
|||
|
|
html[data-motion="still"] .route-journey-overlay,
|
|||
|
|
html[data-document-hidden="true"] .route-journey-overlay { display: none !important; }
|
|||
|
|
html[data-motion="still"] .relationship-pulse-overlay,
|
|||
|
|
html[data-motion="still"] .story-carrier-overlay,
|
|||
|
|
html[data-document-hidden="true"] .story-carrier-overlay { display: none !important; }
|
|||
|
|
html[data-motion="still"] .chapter-handoff-overlay { display: none !important; }
|
|||
|
|
html[data-motion="still"] .chapter-handoff-anchor { animation: none !important; }
|
|||
|
|
html[data-motion="still"] .guided-view-chapter,
|
|||
|
|
html[data-motion="still"] svg [data-node-id],
|
|||
|
|
html[data-motion="still"] svg [data-edge-from],
|
|||
|
|
html[data-motion="still"] svg [data-detail],
|
|||
|
|
html[data-motion="still"] svg [data-detail-anchor],
|
|||
|
|
html[data-motion="still"] svg [data-legend-hit],
|
|||
|
|
html[data-motion="still"] svg {
|
|||
|
|
transition: none !important;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
svg {
|
|||
|
|
width: 100%;
|
|||
|
|
min-width: min(900px, 100%);
|
|||
|
|
display: block;
|
|||
|
|
transform-origin: 0 0;
|
|||
|
|
transition: transform 0.18s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.diagram-container.is-camera-moving svg {
|
|||
|
|
transition: transform 0.42s cubic-bezier(0.22, 1, 0.36, 1);
|
|||
|
|
}
|
|||
|
|
.diagram-container.is-camera-transaction svg { transition: none; }
|
|||
|
|
.diagram-container.is-pannable { cursor: grab; }
|
|||
|
|
.diagram-container.is-panning { cursor: grabbing; user-select: none; }
|
|||
|
|
.diagram-container.is-panning svg { transition: none; }
|
|||
|
|
|
|||
|
|
.diagram-nav {
|
|||
|
|
position: absolute;
|
|||
|
|
right: 1rem;
|
|||
|
|
bottom: 1rem;
|
|||
|
|
z-index: 12;
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
max-width: calc(100% - 2rem);
|
|||
|
|
padding: 0.15rem;
|
|||
|
|
gap: 0;
|
|||
|
|
overflow-x: auto;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--toolbar-border) 48%, transparent);
|
|||
|
|
border-radius: 0.58rem;
|
|||
|
|
background: color-mix(in srgb, var(--toolbar-bg) 82%, transparent);
|
|||
|
|
box-shadow: 0 5px 14px rgba(0, 0, 0, 0.07);
|
|||
|
|
backdrop-filter: blur(10px);
|
|||
|
|
scrollbar-width: none;
|
|||
|
|
}
|
|||
|
|
.diagram-nav::-webkit-scrollbar { display: none; }
|
|||
|
|
.diagram-nav button,
|
|||
|
|
.focus-chip button,
|
|||
|
|
.guided-views button {
|
|||
|
|
border: 0;
|
|||
|
|
background: transparent;
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
font: inherit;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
.diagram-nav button {
|
|||
|
|
min-width: 2rem;
|
|||
|
|
height: 2rem;
|
|||
|
|
padding: 0 0.35rem;
|
|||
|
|
border-radius: 0.35rem;
|
|||
|
|
color: color-mix(in srgb, var(--toolbar-text) 68%, transparent);
|
|||
|
|
font-size: 0.56rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
letter-spacing: 0.02em;
|
|||
|
|
transition: background 0.15s, color 0.15s;
|
|||
|
|
}
|
|||
|
|
.diagram-nav button:hover,
|
|||
|
|
.diagram-nav button:focus-visible,
|
|||
|
|
.focus-chip button:hover,
|
|||
|
|
.focus-chip button:focus-visible,
|
|||
|
|
.guided-views button:hover,
|
|||
|
|
.guided-views button:focus-visible { background: var(--toolbar-hover); }
|
|||
|
|
.diagram-nav button:focus-visible,
|
|||
|
|
.focus-chip button:focus-visible,
|
|||
|
|
.guided-views button:focus-visible {
|
|||
|
|
outline: 2px solid var(--frontend-stroke);
|
|||
|
|
outline-offset: 1px;
|
|||
|
|
}
|
|||
|
|
.diagram-nav button:disabled { opacity: 0.38; cursor: default; }
|
|||
|
|
.diagram-nav button:disabled:hover { background: transparent; }
|
|||
|
|
.diagram-nav #btn-route-probe,
|
|||
|
|
.diagram-nav #btn-overview-map,
|
|||
|
|
.diagram-nav #btn-semantic-lens {
|
|||
|
|
min-width: auto;
|
|||
|
|
padding-inline: 0.5rem;
|
|||
|
|
color: color-mix(in srgb, var(--toolbar-text) 86%, transparent);
|
|||
|
|
font-weight: 650;
|
|||
|
|
}
|
|||
|
|
.diagram-nav #btn-node-finder,
|
|||
|
|
.diagram-nav [data-view="out"] {
|
|||
|
|
position: relative;
|
|||
|
|
margin-left: 0.25rem;
|
|||
|
|
}
|
|||
|
|
.diagram-nav #btn-node-finder::before,
|
|||
|
|
.diagram-nav [data-view="out"]::before {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 0.4rem;
|
|||
|
|
bottom: 0.4rem;
|
|||
|
|
left: -0.13rem;
|
|||
|
|
width: 1px;
|
|||
|
|
background: color-mix(in srgb, var(--toolbar-border) 62%, transparent);
|
|||
|
|
content: "";
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
.diagram-nav-icon {
|
|||
|
|
display: block;
|
|||
|
|
width: 0.72rem;
|
|||
|
|
height: 0.72rem;
|
|||
|
|
margin: auto;
|
|||
|
|
background: currentColor;
|
|||
|
|
-webkit-mask-position: center;
|
|||
|
|
-webkit-mask-repeat: no-repeat;
|
|||
|
|
-webkit-mask-size: contain;
|
|||
|
|
mask-position: center;
|
|||
|
|
mask-repeat: no-repeat;
|
|||
|
|
mask-size: contain;
|
|||
|
|
}
|
|||
|
|
.diagram-nav-icon.find {
|
|||
|
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='8.5' cy='8.5' r='4.5' fill='none' stroke='black' stroke-width='1.8'/%3E%3Cpath d='m12 12 4 4' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
|
|||
|
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='8.5' cy='8.5' r='4.5' fill='none' stroke='black' stroke-width='1.8'/%3E%3Cpath d='m12 12 4 4' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
|
|||
|
|
}
|
|||
|
|
.diagram-nav-icon.guide {
|
|||
|
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='10' cy='10' r='7' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M7.8 7.3a2.4 2.4 0 014.6.9c0 1.8-2.4 2-2.4 3.7M10 14.9h.01' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E");
|
|||
|
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Ccircle cx='10' cy='10' r='7' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M7.8 7.3a2.4 2.4 0 014.6.9c0 1.8-2.4 2-2.4 3.7M10 14.9h.01' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E");
|
|||
|
|
}
|
|||
|
|
.diagram-nav-icon.minus {
|
|||
|
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M5 10h10' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
|
|||
|
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M5 10h10' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
|
|||
|
|
}
|
|||
|
|
.diagram-nav-icon.plus {
|
|||
|
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 5v10M5 10h10' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
|
|||
|
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 5v10M5 10h10' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E");
|
|||
|
|
}
|
|||
|
|
.diagram-nav [data-view="reset"] {
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
gap: 0.2rem;
|
|||
|
|
min-width: 3.1rem;
|
|||
|
|
padding-inline: 0.35rem;
|
|||
|
|
}
|
|||
|
|
.diagram-nav [data-view="reset"][data-detail-visible] {
|
|||
|
|
min-width: 3.8rem;
|
|||
|
|
}
|
|||
|
|
.diagram-nav-detail {
|
|||
|
|
color: color-mix(in srgb, var(--toolbar-text) 52%, transparent);
|
|||
|
|
font-size: 0.44rem;
|
|||
|
|
font-weight: 700;
|
|||
|
|
line-height: 1;
|
|||
|
|
letter-spacing: 0.07em;
|
|||
|
|
}
|
|||
|
|
.diagram-nav-percent {
|
|||
|
|
color: color-mix(in srgb, var(--toolbar-text) 84%, transparent);
|
|||
|
|
font-size: 0.55rem;
|
|||
|
|
font-weight: 650;
|
|||
|
|
line-height: 1;
|
|||
|
|
}
|
|||
|
|
.diagram-container[data-camera-indicator="true"] .diagram-nav-detail,
|
|||
|
|
.diagram-container[data-camera-indicator="true"] .diagram-nav-percent { color: var(--frontend-stroke); }
|
|||
|
|
.diagram-nav #btn-diagram-guide[aria-expanded="true"] {
|
|||
|
|
color: var(--cloud-stroke);
|
|||
|
|
background: var(--toolbar-hover);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Semantic Radar is a deliberately simplified, viewer-only overview. It
|
|||
|
|
uses authored node bounds and the live camera viewport rather than
|
|||
|
|
cloning the canonical SVG, so IDs, filters, animation, and export state
|
|||
|
|
stay singular and deterministic. */
|
|||
|
|
.overview-map {
|
|||
|
|
position: absolute;
|
|||
|
|
right: 1rem;
|
|||
|
|
bottom: calc(4.15rem + var(--archify-nav-reserve));
|
|||
|
|
z-index: 12;
|
|||
|
|
width: 11.25rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--frontend-stroke) 52%, var(--toolbar-border));
|
|||
|
|
border-radius: 0.78rem;
|
|||
|
|
background: var(--toolbar-menu-bg);
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
box-shadow: 0 16px 42px rgba(0, 0, 0, 0.3);
|
|||
|
|
backdrop-filter: blur(16px);
|
|||
|
|
font-size: 0.625rem;
|
|||
|
|
}
|
|||
|
|
.overview-map[hidden] { display: none; }
|
|||
|
|
.overview-map[data-docked="true"] {
|
|||
|
|
position: fixed;
|
|||
|
|
right: var(--archify-radar-right, 1rem);
|
|||
|
|
top: var(--archify-radar-top, 1rem);
|
|||
|
|
bottom: auto;
|
|||
|
|
}
|
|||
|
|
.overview-map[data-docked="true"][data-dock-side="left"] {
|
|||
|
|
right: auto;
|
|||
|
|
left: var(--archify-radar-left, 1rem);
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] .overview-map { border-radius: 0.28rem; }
|
|||
|
|
.overview-map-head {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.45rem;
|
|||
|
|
min-height: 2.25rem;
|
|||
|
|
padding: 0.42rem 0.48rem 0.38rem 0.55rem;
|
|||
|
|
border-bottom: 1px solid var(--toolbar-border);
|
|||
|
|
background: linear-gradient(110deg, color-mix(in srgb, var(--frontend-fill) 45%, transparent), transparent 68%);
|
|||
|
|
cursor: grab;
|
|||
|
|
touch-action: none;
|
|||
|
|
user-select: none;
|
|||
|
|
}
|
|||
|
|
.overview-map[data-panel-dragging="true"] .overview-map-head { cursor: grabbing; }
|
|||
|
|
.overview-map[data-placement-invalid="true"] {
|
|||
|
|
border-color: var(--security-stroke);
|
|||
|
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--security-stroke) 34%, transparent), 0 16px 42px rgba(0, 0, 0, 0.3);
|
|||
|
|
}
|
|||
|
|
.overview-map[data-compact="true"] {
|
|||
|
|
width: 7.4rem;
|
|||
|
|
}
|
|||
|
|
.overview-map[data-compact="true"] .overview-map-head {
|
|||
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|||
|
|
gap: 0.24rem;
|
|||
|
|
min-height: 1.5rem;
|
|||
|
|
padding: 0.05rem 0.16rem 0.05rem 0.32rem;
|
|||
|
|
border-bottom: 0;
|
|||
|
|
}
|
|||
|
|
.overview-map[data-compact="true"] .overview-map-live,
|
|||
|
|
.overview-map[data-compact="true"] .overview-map-copy,
|
|||
|
|
.overview-map[data-compact="true"] .overview-map-surface,
|
|||
|
|
.overview-map[data-compact="true"] .overview-map-hint {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
.overview-map[data-compact="true"] .overview-map-expand { display: inline-flex; }
|
|||
|
|
.overview-map[data-compact="true"] .overview-map-close {
|
|||
|
|
width: 1.15rem;
|
|||
|
|
height: 1.15rem;
|
|||
|
|
font-size: 0.72rem;
|
|||
|
|
}
|
|||
|
|
.overview-map-live {
|
|||
|
|
width: 0.42rem;
|
|||
|
|
height: 0.42rem;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: var(--backend-stroke);
|
|||
|
|
box-shadow: 0 0 0 0 color-mix(in srgb, var(--backend-stroke) 52%, transparent);
|
|||
|
|
animation: archify-radar-live 2.4s ease-out infinite;
|
|||
|
|
}
|
|||
|
|
.overview-map-copy { min-width: 0; line-height: 1.1; }
|
|||
|
|
.overview-map-copy strong {
|
|||
|
|
display: block;
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
font-size: 0.59rem;
|
|||
|
|
letter-spacing: 0.12em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
}
|
|||
|
|
.overview-map-copy small {
|
|||
|
|
display: block;
|
|||
|
|
margin-top: 0.18rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.52rem;
|
|||
|
|
letter-spacing: 0.04em;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
}
|
|||
|
|
.overview-map-expand {
|
|||
|
|
display: none;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
min-width: 0;
|
|||
|
|
height: 1.2rem;
|
|||
|
|
padding: 0 0.34rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 0;
|
|||
|
|
border-radius: 0.3rem;
|
|||
|
|
background: transparent;
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
font: inherit;
|
|||
|
|
font-size: 0.49rem;
|
|||
|
|
font-weight: 700;
|
|||
|
|
letter-spacing: 0.06em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
.overview-map-close {
|
|||
|
|
width: 1.55rem;
|
|||
|
|
height: 1.55rem;
|
|||
|
|
padding: 0;
|
|||
|
|
border: 0;
|
|||
|
|
border-radius: 0.34rem;
|
|||
|
|
background: transparent;
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
font: inherit;
|
|||
|
|
font-size: 0.9rem;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
.overview-map-expand:hover,
|
|||
|
|
.overview-map-expand:focus-visible,
|
|||
|
|
.overview-map-close:hover,
|
|||
|
|
.overview-map-close:focus-visible { background: var(--toolbar-hover); }
|
|||
|
|
.overview-map-expand:focus-visible,
|
|||
|
|
.overview-map-close:focus-visible,
|
|||
|
|
.overview-map-surface:focus-visible,
|
|||
|
|
.overview-map-node:focus-visible {
|
|||
|
|
outline: 2px solid var(--frontend-stroke);
|
|||
|
|
outline-offset: 1px;
|
|||
|
|
}
|
|||
|
|
.overview-map-surface {
|
|||
|
|
position: relative;
|
|||
|
|
height: 7rem;
|
|||
|
|
margin: 0.42rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--panel-border) 72%, transparent);
|
|||
|
|
border-radius: 0.42rem;
|
|||
|
|
background:
|
|||
|
|
linear-gradient(color-mix(in srgb, var(--panel-border) 26%, transparent) 1px, transparent 1px),
|
|||
|
|
linear-gradient(90deg, color-mix(in srgb, var(--panel-border) 26%, transparent) 1px, transparent 1px),
|
|||
|
|
color-mix(in srgb, var(--bg) 86%, transparent);
|
|||
|
|
background-size: 12px 12px;
|
|||
|
|
cursor: crosshair;
|
|||
|
|
touch-action: none;
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] .overview-map-surface { border-radius: 0.1rem; }
|
|||
|
|
.overview-map-surface > svg {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
min-width: 0;
|
|||
|
|
transform: none !important;
|
|||
|
|
transition: none !important;
|
|||
|
|
display: block;
|
|||
|
|
overflow: visible;
|
|||
|
|
}
|
|||
|
|
.overview-map-node {
|
|||
|
|
fill: var(--backend-stroke);
|
|||
|
|
fill-opacity: 0.52;
|
|||
|
|
stroke: var(--backend-stroke);
|
|||
|
|
stroke-opacity: 0.86;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: fill-opacity 140ms ease, stroke-width 140ms ease, filter 140ms ease;
|
|||
|
|
}
|
|||
|
|
.overview-map-node[data-kind="frontend"],
|
|||
|
|
.overview-map-node[data-kind="start"] { fill: var(--frontend-stroke); stroke: var(--frontend-stroke); }
|
|||
|
|
.overview-map-node[data-kind="database"],
|
|||
|
|
.overview-map-node[data-kind="success"] { fill: var(--database-stroke); stroke: var(--database-stroke); }
|
|||
|
|
.overview-map-node[data-kind="cloud"],
|
|||
|
|
.overview-map-node[data-kind="waiting"] { fill: var(--cloud-stroke); stroke: var(--cloud-stroke); }
|
|||
|
|
.overview-map-node[data-kind="messagebus"] { fill: var(--messagebus-stroke); stroke: var(--messagebus-stroke); }
|
|||
|
|
.overview-map-node[data-kind="security"],
|
|||
|
|
.overview-map-node[data-kind="decision"],
|
|||
|
|
.overview-map-node[data-kind="failure"] { fill: var(--security-stroke); stroke: var(--security-stroke); }
|
|||
|
|
.overview-map-node[data-kind="external"],
|
|||
|
|
.overview-map-node[data-kind="neutral"] { fill: var(--external-stroke); stroke: var(--external-stroke); }
|
|||
|
|
.overview-map-node[data-radar-active="true"],
|
|||
|
|
.overview-map-node:hover,
|
|||
|
|
.overview-map-node:focus-visible {
|
|||
|
|
fill-opacity: 0.92;
|
|||
|
|
stroke-width: 2.2;
|
|||
|
|
filter: drop-shadow(0 0 3px currentColor);
|
|||
|
|
}
|
|||
|
|
.overview-map-viewport {
|
|||
|
|
fill: color-mix(in srgb, var(--frontend-fill) 56%, transparent);
|
|||
|
|
stroke: var(--frontend-stroke);
|
|||
|
|
stroke-width: 2;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
pointer-events: none;
|
|||
|
|
filter: drop-shadow(0 0 3px color-mix(in srgb, var(--frontend-stroke) 54%, transparent));
|
|||
|
|
}
|
|||
|
|
.overview-map-hint {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
padding: 0 0.52rem 0.46rem;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.49rem;
|
|||
|
|
letter-spacing: 0.06em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.diagram-nav #btn-overview-map[aria-expanded="true"] {
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
background: var(--toolbar-hover);
|
|||
|
|
}
|
|||
|
|
.overview-map-feedback {
|
|||
|
|
position: absolute;
|
|||
|
|
right: 1rem;
|
|||
|
|
bottom: 4.15rem;
|
|||
|
|
z-index: 13;
|
|||
|
|
max-width: min(16rem, calc(100% - 2rem));
|
|||
|
|
padding: 0.38rem 0.55rem;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--frontend-stroke) 52%, var(--toolbar-border));
|
|||
|
|
border-radius: 0.5rem;
|
|||
|
|
background: var(--toolbar-menu-bg);
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
|||
|
|
font-size: 0.52rem;
|
|||
|
|
line-height: 1.35;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
.overview-map-feedback[hidden] { display: none; }
|
|||
|
|
.focus-chip[data-radar-yielded="true"] { display: none !important; }
|
|||
|
|
.diagram-nav #btn-route-probe[aria-pressed="true"] {
|
|||
|
|
color: var(--backend-stroke);
|
|||
|
|
background: var(--toolbar-hover);
|
|||
|
|
}
|
|||
|
|
.diagram-nav #btn-semantic-lens[aria-expanded="true"],
|
|||
|
|
.diagram-nav #btn-semantic-lens[aria-pressed="true"] {
|
|||
|
|
color: var(--database-stroke);
|
|||
|
|
background: var(--toolbar-hover);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Semantic Lens turns the compiled node-kind palette into an analytical
|
|||
|
|
legend. One kind reveals its real traffic; two kinds compare only their
|
|||
|
|
direct authored relationships. Geometry and the canonical SVG stay put. */
|
|||
|
|
.semantic-lens {
|
|||
|
|
position: absolute;
|
|||
|
|
right: 1rem;
|
|||
|
|
bottom: calc(4.15rem + var(--archify-nav-reserve));
|
|||
|
|
z-index: 24;
|
|||
|
|
width: min(20rem, calc(100% - 2rem));
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--database-stroke) 58%, var(--toolbar-border));
|
|||
|
|
border-radius: 0.82rem;
|
|||
|
|
background:
|
|||
|
|
linear-gradient(135deg, color-mix(in srgb, var(--database-fill) 34%, transparent), transparent 48%),
|
|||
|
|
var(--toolbar-menu-bg);
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
box-shadow: 0 18px 52px rgba(0, 0, 0, 0.36);
|
|||
|
|
backdrop-filter: blur(18px);
|
|||
|
|
animation: archify-lens-in 180ms cubic-bezier(0.22, 1, 0.36, 1);
|
|||
|
|
}
|
|||
|
|
.semantic-lens[hidden] { display: none; }
|
|||
|
|
.semantic-lens[data-dock-side="left"] { left: 1rem; right: auto; }
|
|||
|
|
.semantic-lens[data-dock-side="right"] { left: auto; right: 1rem; }
|
|||
|
|
html[data-preset="blueprint"] .semantic-lens { border-radius: 0.28rem; }
|
|||
|
|
.semantic-lens-head {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|||
|
|
align-items: start;
|
|||
|
|
gap: 0.75rem;
|
|||
|
|
padding: 0.72rem 0.78rem 0.62rem;
|
|||
|
|
border-bottom: 1px solid var(--toolbar-border);
|
|||
|
|
}
|
|||
|
|
.semantic-lens-eyebrow {
|
|||
|
|
display: block;
|
|||
|
|
color: var(--database-stroke);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
font-weight: 800;
|
|||
|
|
letter-spacing: 0.16em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.semantic-lens-title {
|
|||
|
|
display: block;
|
|||
|
|
margin-top: 0.16rem;
|
|||
|
|
color: var(--text);
|
|||
|
|
font-size: 0.82rem;
|
|||
|
|
letter-spacing: -0.02em;
|
|||
|
|
}
|
|||
|
|
.semantic-lens-close {
|
|||
|
|
width: 1.75rem;
|
|||
|
|
height: 1.75rem;
|
|||
|
|
padding: 0;
|
|||
|
|
border: 0;
|
|||
|
|
border-radius: 0.35rem;
|
|||
|
|
background: transparent;
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
font: inherit;
|
|||
|
|
font-size: 1rem;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
.semantic-lens-close:hover,
|
|||
|
|
.semantic-lens-close:focus-visible { background: var(--toolbar-hover); }
|
|||
|
|
.semantic-lens-instruction {
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 0.52rem 0.78rem 0.48rem;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.55rem;
|
|||
|
|
line-height: 1.45;
|
|||
|
|
}
|
|||
|
|
.semantic-lens-kinds {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1fr 1fr;
|
|||
|
|
gap: 0.3rem;
|
|||
|
|
max-height: min(15rem, 40vh);
|
|||
|
|
padding: 0 0.5rem 0.5rem;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
scrollbar-width: thin;
|
|||
|
|
}
|
|||
|
|
.semantic-lens-kind {
|
|||
|
|
--lens-color: var(--external-stroke);
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.42rem;
|
|||
|
|
min-width: 0;
|
|||
|
|
min-height: 2.15rem;
|
|||
|
|
padding: 0.35rem 0.42rem;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--lens-color) 24%, var(--toolbar-border));
|
|||
|
|
border-radius: 0.45rem;
|
|||
|
|
background: color-mix(in srgb, var(--panel) 76%, transparent);
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
font: inherit;
|
|||
|
|
text-align: left;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: border-color 140ms ease, background 140ms ease, opacity 140ms ease;
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] .semantic-lens-kind { border-radius: 0.12rem; }
|
|||
|
|
.semantic-lens-kind[data-kind="frontend"],
|
|||
|
|
.semantic-lens-kind[data-kind="start"] { --lens-color: var(--frontend-stroke); }
|
|||
|
|
.semantic-lens-kind[data-kind="backend"],
|
|||
|
|
.semantic-lens-kind[data-kind="active"] { --lens-color: var(--backend-stroke); }
|
|||
|
|
.semantic-lens-kind[data-kind="database"],
|
|||
|
|
.semantic-lens-kind[data-kind="success"] { --lens-color: var(--database-stroke); }
|
|||
|
|
.semantic-lens-kind[data-kind="cloud"],
|
|||
|
|
.semantic-lens-kind[data-kind="waiting"] { --lens-color: var(--cloud-stroke); }
|
|||
|
|
.semantic-lens-kind[data-kind="messagebus"] { --lens-color: var(--messagebus-stroke); }
|
|||
|
|
.semantic-lens-kind[data-kind="security"],
|
|||
|
|
.semantic-lens-kind[data-kind="decision"],
|
|||
|
|
.semantic-lens-kind[data-kind="failure"] { --lens-color: var(--security-stroke); }
|
|||
|
|
.semantic-lens-kind:hover,
|
|||
|
|
.semantic-lens-kind:focus-visible,
|
|||
|
|
.semantic-lens-kind[aria-pressed="true"] {
|
|||
|
|
border-color: color-mix(in srgb, var(--lens-color) 78%, var(--toolbar-border));
|
|||
|
|
background: color-mix(in srgb, var(--lens-color) 11%, var(--panel));
|
|||
|
|
}
|
|||
|
|
.semantic-lens-kind:focus-visible,
|
|||
|
|
.semantic-lens-close:focus-visible,
|
|||
|
|
.semantic-lens-actions button:focus-visible {
|
|||
|
|
outline: 2px solid var(--database-stroke);
|
|||
|
|
outline-offset: 1px;
|
|||
|
|
}
|
|||
|
|
.semantic-lens-kind:disabled { opacity: 0.34; cursor: default; }
|
|||
|
|
.semantic-lens-swatch {
|
|||
|
|
width: 0.48rem;
|
|||
|
|
height: 0.48rem;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: var(--lens-color);
|
|||
|
|
box-shadow: 0 0 8px color-mix(in srgb, var(--lens-color) 58%, transparent);
|
|||
|
|
}
|
|||
|
|
.semantic-lens-kind strong {
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: var(--text);
|
|||
|
|
font-size: 0.61rem;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.semantic-lens-kind em {
|
|||
|
|
color: var(--lens-color);
|
|||
|
|
font-size: 0.54rem;
|
|||
|
|
font-style: normal;
|
|||
|
|
font-weight: 800;
|
|||
|
|
}
|
|||
|
|
.semantic-lens-status {
|
|||
|
|
min-height: 2.5rem;
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 0.55rem 0.78rem;
|
|||
|
|
border-top: 1px solid var(--toolbar-border);
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.55rem;
|
|||
|
|
line-height: 1.45;
|
|||
|
|
}
|
|||
|
|
.semantic-lens-status strong { color: var(--database-stroke); font-weight: 800; }
|
|||
|
|
.semantic-lens-actions {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
gap: 0.28rem;
|
|||
|
|
padding: 0 0.62rem 0.62rem;
|
|||
|
|
}
|
|||
|
|
.semantic-lens-actions button {
|
|||
|
|
min-height: 1.8rem;
|
|||
|
|
padding: 0.28rem 0.48rem;
|
|||
|
|
border: 0;
|
|||
|
|
border-radius: 0.35rem;
|
|||
|
|
background: transparent;
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
font: inherit;
|
|||
|
|
font-size: 0.56rem;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
.semantic-lens-actions button:hover { background: var(--toolbar-hover); }
|
|||
|
|
.semantic-lens-actions #semantic-lens-copy { color: var(--database-stroke); }
|
|||
|
|
@keyframes archify-lens-in {
|
|||
|
|
from { opacity: 0; transform: translateY(5px) scale(0.985); }
|
|||
|
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Route Probe answers a reader question over the compiled semantic graph:
|
|||
|
|
choose two stable nodes and reveal the shortest authored directed route.
|
|||
|
|
The receipt and signal live entirely in the viewer layer. */
|
|||
|
|
.route-probe {
|
|||
|
|
position: absolute;
|
|||
|
|
left: 1rem;
|
|||
|
|
bottom: calc(1rem + var(--archify-nav-reserve));
|
|||
|
|
z-index: 12;
|
|||
|
|
width: min(46rem, calc(100% - 25rem));
|
|||
|
|
min-width: 18rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--backend-stroke) 62%, var(--toolbar-border));
|
|||
|
|
border-radius: 0.82rem;
|
|||
|
|
background:
|
|||
|
|
linear-gradient(115deg, color-mix(in srgb, var(--backend-fill) 50%, transparent), transparent 62%),
|
|||
|
|
var(--toolbar-menu-bg);
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.3);
|
|||
|
|
backdrop-filter: blur(16px);
|
|||
|
|
font-size: 0.625rem;
|
|||
|
|
}
|
|||
|
|
.route-probe[hidden] { display: none; }
|
|||
|
|
.route-probe[data-route-dock="top"] {
|
|||
|
|
top: 1rem;
|
|||
|
|
bottom: auto;
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] .route-probe { border-radius: 0.25rem; }
|
|||
|
|
.route-probe-head {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|||
|
|
align-items: start;
|
|||
|
|
gap: 0.65rem;
|
|||
|
|
padding: 0.52rem 0.58rem 0.44rem;
|
|||
|
|
border-bottom: 1px solid color-mix(in srgb, var(--toolbar-border) 80%, transparent);
|
|||
|
|
}
|
|||
|
|
.route-probe-copy { min-width: 0; }
|
|||
|
|
.route-probe-eyebrow {
|
|||
|
|
display: block;
|
|||
|
|
color: var(--backend-stroke);
|
|||
|
|
font-size: 0.48rem;
|
|||
|
|
font-weight: 800;
|
|||
|
|
letter-spacing: 0.15em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.route-probe-title {
|
|||
|
|
display: block;
|
|||
|
|
margin-top: 0.14rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
font-size: 0.72rem;
|
|||
|
|
line-height: 1.25;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
}
|
|||
|
|
.route-probe-actions { display: flex; gap: 0.2rem; }
|
|||
|
|
.route-probe button {
|
|||
|
|
min-height: 1.55rem;
|
|||
|
|
padding: 0.25rem 0.42rem;
|
|||
|
|
border: 0;
|
|||
|
|
border-radius: 0.36rem;
|
|||
|
|
background: transparent;
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
font: inherit;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
.route-probe button:hover,
|
|||
|
|
.route-probe button:focus-visible { background: var(--toolbar-hover); }
|
|||
|
|
.route-probe button:focus-visible {
|
|||
|
|
outline: 2px solid var(--backend-stroke);
|
|||
|
|
outline-offset: 1px;
|
|||
|
|
}
|
|||
|
|
.route-probe-copy-link { color: var(--frontend-stroke) !important; }
|
|||
|
|
.route-probe-find { color: var(--backend-stroke) !important; }
|
|||
|
|
.route-probe-copy-link[hidden] { display: none; }
|
|||
|
|
.route-probe-find[hidden] { display: none; }
|
|||
|
|
.route-probe-path {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
min-height: 2.2rem;
|
|||
|
|
padding: 0.42rem 0.58rem 0.24rem;
|
|||
|
|
overflow-x: auto;
|
|||
|
|
scrollbar-width: thin;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.route-probe-node,
|
|||
|
|
.route-probe button.route-probe-node {
|
|||
|
|
flex: none;
|
|||
|
|
max-width: 9.5rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
min-height: 1.75rem !important;
|
|||
|
|
padding: 0.22rem 0.38rem;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--backend-stroke) 46%, var(--toolbar-border));
|
|||
|
|
border-radius: 999px;
|
|||
|
|
background: color-mix(in srgb, var(--backend-fill) 52%, transparent);
|
|||
|
|
color: var(--text);
|
|||
|
|
font-size: 0.56rem;
|
|||
|
|
font-weight: 700;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.route-probe-node[data-endpoint="start"] {
|
|||
|
|
border-color: var(--frontend-stroke);
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
}
|
|||
|
|
.route-probe-node[data-endpoint="end"] {
|
|||
|
|
border-color: var(--security-stroke);
|
|||
|
|
color: var(--security-stroke);
|
|||
|
|
}
|
|||
|
|
.route-probe-node[data-route-journey-state="past"] { opacity: 0.66; }
|
|||
|
|
.route-probe-node[data-route-journey-state="future"] { opacity: 0.48; }
|
|||
|
|
.route-probe-node[aria-current="step"] {
|
|||
|
|
border-color: var(--backend-stroke);
|
|||
|
|
background: color-mix(in srgb, var(--backend-fill) 82%, var(--toolbar-menu-bg));
|
|||
|
|
color: var(--text);
|
|||
|
|
box-shadow: 0 0 0 1px color-mix(in srgb, var(--backend-stroke) 42%, transparent), 0 0 14px color-mix(in srgb, var(--backend-stroke) 34%, transparent);
|
|||
|
|
opacity: 1;
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] .route-probe-node[aria-current="step"] {
|
|||
|
|
border-style: double;
|
|||
|
|
border-width: 3px;
|
|||
|
|
border-radius: 0.2rem;
|
|||
|
|
box-shadow: none;
|
|||
|
|
}
|
|||
|
|
.route-probe-arrow {
|
|||
|
|
flex: none;
|
|||
|
|
padding: 0 0.28rem;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.65rem;
|
|||
|
|
}
|
|||
|
|
.route-probe-placeholder {
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.56rem;
|
|||
|
|
letter-spacing: 0.03em;
|
|||
|
|
}
|
|||
|
|
.route-probe-status {
|
|||
|
|
display: block;
|
|||
|
|
min-height: 1.45rem;
|
|||
|
|
padding: 0.08rem 0.58rem 0.44rem;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.52rem;
|
|||
|
|
line-height: 1.35;
|
|||
|
|
}
|
|||
|
|
.route-journey-controls {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 2rem minmax(5.8rem, auto) 2rem minmax(4.8rem, auto);
|
|||
|
|
justify-content: start;
|
|||
|
|
gap: 0.22rem;
|
|||
|
|
padding: 0.08rem 0.58rem 0.28rem;
|
|||
|
|
}
|
|||
|
|
.route-journey-controls[hidden] { display: none; }
|
|||
|
|
.route-journey-controls button {
|
|||
|
|
min-height: 1.8rem;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--toolbar-border) 86%, transparent);
|
|||
|
|
background: color-mix(in srgb, var(--toolbar-bg) 72%, transparent);
|
|||
|
|
font-size: 0.54rem;
|
|||
|
|
font-weight: 700;
|
|||
|
|
}
|
|||
|
|
.route-journey-controls button:disabled {
|
|||
|
|
cursor: default;
|
|||
|
|
opacity: 0.34;
|
|||
|
|
}
|
|||
|
|
.route-journey-play { color: var(--backend-stroke) !important; }
|
|||
|
|
.route-journey-overview { color: var(--frontend-stroke) !important; }
|
|||
|
|
.route-journey-controls[data-playing="true"] .route-journey-play {
|
|||
|
|
border-color: color-mix(in srgb, var(--backend-stroke) 64%, var(--toolbar-border));
|
|||
|
|
background: color-mix(in srgb, var(--backend-fill) 62%, transparent);
|
|||
|
|
}
|
|||
|
|
.route-probe[data-state="error"] {
|
|||
|
|
border-color: color-mix(in srgb, var(--security-stroke) 72%, var(--toolbar-border));
|
|||
|
|
}
|
|||
|
|
.route-probe[data-state="error"] .route-probe-status { color: var(--security-stroke); }
|
|||
|
|
|
|||
|
|
.focus-chip {
|
|||
|
|
position: absolute;
|
|||
|
|
left: 1rem;
|
|||
|
|
top: 1rem;
|
|||
|
|
z-index: 12;
|
|||
|
|
display: block;
|
|||
|
|
width: min(22rem, calc(100% - 13rem));
|
|||
|
|
max-width: calc(100% - 13rem);
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 1px solid var(--frontend-stroke);
|
|||
|
|
border-radius: 0.85rem;
|
|||
|
|
background: var(--toolbar-menu-bg);
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.3);
|
|||
|
|
backdrop-filter: blur(16px);
|
|||
|
|
font-size: 0.6875rem;
|
|||
|
|
}
|
|||
|
|
.focus-chip[hidden] { display: none; }
|
|||
|
|
html[data-preset="blueprint"] .focus-chip { border-radius: 0.3rem; }
|
|||
|
|
.focus-chip button {
|
|||
|
|
flex: none;
|
|||
|
|
padding: 0.3rem 0.45rem;
|
|||
|
|
border-radius: 0.35rem;
|
|||
|
|
font-size: 0.625rem;
|
|||
|
|
}
|
|||
|
|
.relationship-lens-head {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|||
|
|
align-items: start;
|
|||
|
|
gap: 0.75rem;
|
|||
|
|
padding: 0.7rem 0.75rem 0.65rem;
|
|||
|
|
border-bottom: 1px solid var(--toolbar-border);
|
|||
|
|
background: linear-gradient(120deg, color-mix(in srgb, var(--frontend-fill) 34%, transparent), transparent 72%);
|
|||
|
|
}
|
|||
|
|
.relationship-lens-copy { min-width: 0; }
|
|||
|
|
.relationship-lens-actions {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: flex-end;
|
|||
|
|
gap: 0.18rem;
|
|||
|
|
}
|
|||
|
|
.relationship-lens-actions button { white-space: nowrap; }
|
|||
|
|
.relationship-lens-actions #btn-focus-clear {
|
|||
|
|
display: grid;
|
|||
|
|
width: 1.75rem;
|
|||
|
|
height: 1.75rem;
|
|||
|
|
padding: 0;
|
|||
|
|
place-items: center;
|
|||
|
|
color: var(--text-muted);
|
|||
|
|
font-size: 1rem;
|
|||
|
|
line-height: 1;
|
|||
|
|
}
|
|||
|
|
.relationship-lens-actions #btn-focus-clear:hover,
|
|||
|
|
.relationship-lens-actions #btn-focus-clear:focus-visible {
|
|||
|
|
background: var(--toolbar-hover);
|
|||
|
|
color: var(--text);
|
|||
|
|
}
|
|||
|
|
.relationship-lens-actions #btn-focus-copy { color: var(--frontend-stroke); }
|
|||
|
|
.relationship-lens-actions #btn-focus-relations { display: none; }
|
|||
|
|
.relationship-lens-eyebrow {
|
|||
|
|
display: block;
|
|||
|
|
margin-bottom: 0.18rem;
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
font-weight: 800;
|
|||
|
|
letter-spacing: 0.14em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.relationship-lens-title {
|
|||
|
|
display: block;
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: var(--text);
|
|||
|
|
font-size: 0.8rem;
|
|||
|
|
line-height: 1.35;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-detail {
|
|||
|
|
display: block;
|
|||
|
|
margin-top: 0.12rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: var(--text-muted);
|
|||
|
|
font-size: 0.625rem;
|
|||
|
|
line-height: 1.4;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-detail[hidden] { display: none; }
|
|||
|
|
.semantic-passport-meta {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 0.24rem;
|
|||
|
|
margin-top: 0.42rem;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-meta span,
|
|||
|
|
.semantic-passport-meta code {
|
|||
|
|
max-width: 100%;
|
|||
|
|
overflow: hidden;
|
|||
|
|
padding: 0.14rem 0.34rem;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--toolbar-border) 82%, transparent);
|
|||
|
|
border-radius: 999px;
|
|||
|
|
background: color-mix(in srgb, var(--panel) 68%, transparent);
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font: inherit;
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
letter-spacing: 0.04em;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-meta [data-passport="kind"] {
|
|||
|
|
border-color: color-mix(in srgb, var(--frontend-stroke) 50%, var(--toolbar-border));
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
font-weight: 800;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-meta [data-passport="tag"] { color: var(--cloud-stroke); }
|
|||
|
|
.semantic-passport-meta [data-passport="brand"] {
|
|||
|
|
border-color: color-mix(in srgb, var(--messagebus-stroke) 52%, var(--toolbar-border));
|
|||
|
|
color: var(--messagebus-stroke);
|
|||
|
|
font-weight: 700;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-meta [hidden] { display: none; }
|
|||
|
|
html[data-preset="blueprint"] .semantic-passport-meta span,
|
|||
|
|
html[data-preset="blueprint"] .semantic-passport-meta code { border-radius: 0.1rem; }
|
|||
|
|
.semantic-passport-evidence {
|
|||
|
|
margin-top: 0.52rem;
|
|||
|
|
padding: 0.5rem;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--backend-stroke) 32%, var(--toolbar-border));
|
|||
|
|
border-radius: 0.55rem;
|
|||
|
|
background: color-mix(in srgb, var(--backend-fill) 18%, var(--panel));
|
|||
|
|
}
|
|||
|
|
.semantic-passport-evidence[hidden] { display: none; }
|
|||
|
|
html[data-preset="blueprint"] .semantic-passport-evidence { border-radius: 0.18rem; }
|
|||
|
|
.semantic-passport-evidence-head {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 0.55rem;
|
|||
|
|
margin-bottom: 0.35rem;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-evidence-status {
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.28rem;
|
|||
|
|
color: var(--backend-stroke);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
font-weight: 800;
|
|||
|
|
letter-spacing: 0.08em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-evidence-status::before {
|
|||
|
|
content: '';
|
|||
|
|
width: 0.38rem;
|
|||
|
|
height: 0.38rem;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: var(--backend-stroke);
|
|||
|
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--backend-stroke) 14%, transparent);
|
|||
|
|
}
|
|||
|
|
.semantic-passport-repository {
|
|||
|
|
min-width: 0;
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
text-decoration: none;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-repository:hover,
|
|||
|
|
.semantic-passport-repository:focus-visible { color: var(--backend-stroke); }
|
|||
|
|
.semantic-passport-evidence-links { display: grid; gap: 0.22rem; }
|
|||
|
|
.semantic-passport-source {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|||
|
|
gap: 0.15rem 0.45rem;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 0.34rem 0.4rem;
|
|||
|
|
border: 1px solid transparent;
|
|||
|
|
border-radius: 0.38rem;
|
|||
|
|
color: var(--text-muted);
|
|||
|
|
text-decoration: none;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-source:hover,
|
|||
|
|
.semantic-passport-source:focus-visible {
|
|||
|
|
border-color: color-mix(in srgb, var(--backend-stroke) 42%, transparent);
|
|||
|
|
background: color-mix(in srgb, var(--backend-fill) 32%, transparent);
|
|||
|
|
color: var(--text);
|
|||
|
|
}
|
|||
|
|
.semantic-passport-source strong {
|
|||
|
|
overflow: hidden;
|
|||
|
|
font-size: 0.5625rem;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-source code {
|
|||
|
|
color: var(--backend-stroke);
|
|||
|
|
font: inherit;
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-source small {
|
|||
|
|
grid-column: 1 / -1;
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.relationship-lens-summary {
|
|||
|
|
display: block;
|
|||
|
|
margin-top: 0.12rem;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.5625rem;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-reach {
|
|||
|
|
display: grid;
|
|||
|
|
gap: 0.28rem;
|
|||
|
|
margin-top: 0.52rem;
|
|||
|
|
padding-top: 0.48rem;
|
|||
|
|
border-top: 1px solid color-mix(in srgb, var(--toolbar-border) 76%, transparent);
|
|||
|
|
}
|
|||
|
|
.semantic-passport-reach[hidden] { display: none; }
|
|||
|
|
.semantic-passport-reach-label {
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
font-weight: 800;
|
|||
|
|
letter-spacing: 0.1em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-reach-actions {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|||
|
|
gap: 0.25rem;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-reach-actions button {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
min-width: 0;
|
|||
|
|
min-height: 2rem;
|
|||
|
|
padding: 0.32rem 0.42rem;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--toolbar-border) 86%, transparent);
|
|||
|
|
background: color-mix(in srgb, var(--panel) 70%, transparent);
|
|||
|
|
color: var(--text-muted);
|
|||
|
|
text-align: left;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-reach-actions button:hover:not(:disabled),
|
|||
|
|
.semantic-passport-reach-actions button:focus-visible {
|
|||
|
|
border-color: color-mix(in srgb, var(--frontend-stroke) 62%, var(--toolbar-border));
|
|||
|
|
color: var(--text);
|
|||
|
|
}
|
|||
|
|
.semantic-passport-reach-actions button:focus-visible {
|
|||
|
|
outline: 2px solid var(--frontend-stroke);
|
|||
|
|
outline-offset: 1px;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-reach-actions button:disabled {
|
|||
|
|
cursor: default;
|
|||
|
|
opacity: 0.5;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-reach-actions button[aria-pressed="true"] {
|
|||
|
|
border-color: color-mix(in srgb, var(--reach-color, var(--frontend-stroke)) 72%, var(--toolbar-border));
|
|||
|
|
background: color-mix(in srgb, var(--reach-fill, var(--frontend-fill)) 48%, transparent);
|
|||
|
|
color: var(--text);
|
|||
|
|
}
|
|||
|
|
#btn-reach-upstream {
|
|||
|
|
--reach-color: var(--database-stroke);
|
|||
|
|
--reach-fill: var(--database-fill);
|
|||
|
|
}
|
|||
|
|
#btn-reach-downstream {
|
|||
|
|
--reach-color: var(--backend-stroke);
|
|||
|
|
--reach-fill: var(--backend-fill);
|
|||
|
|
}
|
|||
|
|
.semantic-passport-reach-actions strong {
|
|||
|
|
color: var(--reach-color, var(--frontend-stroke));
|
|||
|
|
font-size: 0.625rem;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-reach-status {
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.525rem;
|
|||
|
|
line-height: 1.4;
|
|||
|
|
}
|
|||
|
|
.semantic-passport-reach-status[hidden] { display: none; }
|
|||
|
|
.relationship-lens-list {
|
|||
|
|
max-height: min(18rem, 46vh);
|
|||
|
|
overflow: auto;
|
|||
|
|
padding: 0.4rem;
|
|||
|
|
scrollbar-width: thin;
|
|||
|
|
scrollbar-color: var(--frontend-stroke) transparent;
|
|||
|
|
}
|
|||
|
|
.relationship-lens-list::-webkit-scrollbar { width: 4px; }
|
|||
|
|
.relationship-lens-list::-webkit-scrollbar-thumb {
|
|||
|
|
border-radius: 999px;
|
|||
|
|
background: color-mix(in srgb, var(--frontend-stroke) 68%, transparent);
|
|||
|
|
}
|
|||
|
|
.relationship-lens-group + .relationship-lens-group { margin-top: 0.35rem; }
|
|||
|
|
.relationship-lens-group-title {
|
|||
|
|
display: block;
|
|||
|
|
padding: 0.24rem 0.38rem 0.16rem;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
font-weight: 800;
|
|||
|
|
letter-spacing: 0.12em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.relationship-lens-row {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 2.1rem minmax(0, 1fr);
|
|||
|
|
gap: 0.06rem 0.45rem;
|
|||
|
|
width: 100%;
|
|||
|
|
padding: 0.45rem 0.5rem !important;
|
|||
|
|
text-align: left;
|
|||
|
|
}
|
|||
|
|
.relationship-lens-row:hover,
|
|||
|
|
.relationship-lens-row:focus-visible,
|
|||
|
|
.relationship-lens-row[data-preview-active="true"] {
|
|||
|
|
background: var(--toolbar-hover);
|
|||
|
|
box-shadow: inset 2px 0 0 var(--frontend-stroke);
|
|||
|
|
}
|
|||
|
|
.relationship-lens-row:focus-visible {
|
|||
|
|
outline: 2px solid var(--frontend-stroke);
|
|||
|
|
outline-offset: -2px;
|
|||
|
|
}
|
|||
|
|
.relationship-lens-direction {
|
|||
|
|
grid-row: 1 / 3;
|
|||
|
|
align-self: center;
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
font-weight: 800;
|
|||
|
|
letter-spacing: 0.08em;
|
|||
|
|
}
|
|||
|
|
.relationship-lens-row[data-direction="in"] .relationship-lens-direction { color: var(--database-stroke); }
|
|||
|
|
.relationship-lens-row[data-direction="loop"] .relationship-lens-direction { color: var(--security-stroke); }
|
|||
|
|
.relationship-lens-row strong {
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: var(--text);
|
|||
|
|
font-size: 0.6875rem;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.relationship-lens-row small {
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.5625rem;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.relationship-lens-empty {
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 0.75rem;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.625rem;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Diagram Guide keeps the artifact's growing reader vocabulary
|
|||
|
|
discoverable without widening the permanent control bar. It derives
|
|||
|
|
honest counts at runtime and delegates every action to an existing
|
|||
|
|
production interaction instead of creating a second command system. */
|
|||
|
|
.diagram-guide {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 1rem;
|
|||
|
|
right: 1rem;
|
|||
|
|
z-index: 26;
|
|||
|
|
width: min(32rem, calc(100% - 2rem));
|
|||
|
|
max-height: min(42rem, calc(100% - 2rem));
|
|||
|
|
overflow: auto;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--cloud-stroke) 58%, var(--toolbar-border));
|
|||
|
|
border-radius: 0.82rem;
|
|||
|
|
background:
|
|||
|
|
linear-gradient(135deg, color-mix(in srgb, var(--cloud-fill) 32%, transparent), transparent 42%),
|
|||
|
|
var(--toolbar-menu-bg);
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
box-shadow: 0 22px 64px rgba(0, 0, 0, 0.4);
|
|||
|
|
backdrop-filter: blur(18px);
|
|||
|
|
animation: archify-guide-in 180ms cubic-bezier(0.22, 1, 0.36, 1);
|
|||
|
|
}
|
|||
|
|
.diagram-guide[hidden] { display: none; }
|
|||
|
|
html[data-preset="blueprint"] .diagram-guide { border-radius: 0.28rem; }
|
|||
|
|
.diagram-guide-head {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|||
|
|
align-items: start;
|
|||
|
|
gap: 1rem;
|
|||
|
|
padding: 0.82rem 0.9rem 0.68rem;
|
|||
|
|
border-bottom: 1px solid var(--toolbar-border);
|
|||
|
|
}
|
|||
|
|
.diagram-guide-eyebrow {
|
|||
|
|
display: block;
|
|||
|
|
color: var(--cloud-stroke);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
font-weight: 800;
|
|||
|
|
letter-spacing: 0.16em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-title {
|
|||
|
|
display: block;
|
|||
|
|
margin-top: 0.16rem;
|
|||
|
|
color: var(--text);
|
|||
|
|
font-size: 0.86rem;
|
|||
|
|
letter-spacing: -0.02em;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-close {
|
|||
|
|
width: 1.8rem;
|
|||
|
|
height: 1.8rem;
|
|||
|
|
border: 0;
|
|||
|
|
border-radius: 0.35rem;
|
|||
|
|
background: transparent;
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
font: inherit;
|
|||
|
|
font-size: 1rem;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-close:hover,
|
|||
|
|
.diagram-guide-close:focus-visible { background: var(--toolbar-hover); }
|
|||
|
|
.diagram-guide-stats {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 0.54rem 0.9rem;
|
|||
|
|
border-bottom: 1px solid color-mix(in srgb, var(--toolbar-border) 74%, transparent);
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.56rem;
|
|||
|
|
letter-spacing: 0.025em;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-stats::before {
|
|||
|
|
content: '';
|
|||
|
|
width: 0.42rem;
|
|||
|
|
height: 0.42rem;
|
|||
|
|
flex: none;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: var(--backend-stroke);
|
|||
|
|
box-shadow: 0 0 10px color-mix(in srgb, var(--backend-stroke) 72%, transparent);
|
|||
|
|
}
|
|||
|
|
.diagram-guide-actions {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1fr 1fr;
|
|||
|
|
gap: 0.38rem;
|
|||
|
|
padding: 0.62rem;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-action {
|
|||
|
|
--guide-accent: var(--frontend-stroke);
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.55rem;
|
|||
|
|
min-height: 4.15rem;
|
|||
|
|
padding: 0.55rem 0.58rem;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--guide-accent) 22%, var(--toolbar-border));
|
|||
|
|
border-radius: 0.55rem;
|
|||
|
|
background: color-mix(in srgb, var(--panel) 78%, transparent);
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
font: inherit;
|
|||
|
|
text-align: left;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: border-color 140ms ease, background 140ms ease, transform 140ms ease;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-action[data-guide-action="route"] { --guide-accent: var(--backend-stroke); }
|
|||
|
|
.diagram-guide-action[data-guide-action="map"] { --guide-accent: var(--database-stroke); }
|
|||
|
|
.diagram-guide-action[data-guide-action="lens"] { --guide-accent: var(--messagebus-stroke); }
|
|||
|
|
.diagram-guide-action[data-guide-action="story"] { --guide-accent: var(--security-stroke); }
|
|||
|
|
.diagram-guide-action[data-guide-action="present"] {
|
|||
|
|
--guide-accent: var(--cloud-stroke);
|
|||
|
|
}
|
|||
|
|
.diagram-guide-action:hover,
|
|||
|
|
.diagram-guide-action:focus-visible {
|
|||
|
|
border-color: color-mix(in srgb, var(--guide-accent) 76%, var(--toolbar-border));
|
|||
|
|
background: color-mix(in srgb, var(--guide-accent) 9%, var(--panel));
|
|||
|
|
transform: translateY(-1px);
|
|||
|
|
}
|
|||
|
|
.diagram-guide-action:focus-visible,
|
|||
|
|
.diagram-guide-close:focus-visible {
|
|||
|
|
outline: 2px solid var(--cloud-stroke);
|
|||
|
|
outline-offset: 1px;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-action:disabled {
|
|||
|
|
opacity: 0.42;
|
|||
|
|
cursor: default;
|
|||
|
|
transform: none;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-index {
|
|||
|
|
display: grid;
|
|||
|
|
place-items: center;
|
|||
|
|
width: 1.65rem;
|
|||
|
|
height: 1.65rem;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--guide-accent) 64%, var(--toolbar-border));
|
|||
|
|
border-radius: 0.35rem;
|
|||
|
|
color: var(--guide-accent);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
font-weight: 800;
|
|||
|
|
letter-spacing: 0.06em;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-action-copy { min-width: 0; }
|
|||
|
|
.diagram-guide-action strong {
|
|||
|
|
display: block;
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: var(--text);
|
|||
|
|
font-size: 0.66rem;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-action small {
|
|||
|
|
display: -webkit-box;
|
|||
|
|
margin-top: 0.18rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
line-height: 1.35;
|
|||
|
|
-webkit-box-orient: vertical;
|
|||
|
|
-webkit-line-clamp: 2;
|
|||
|
|
}
|
|||
|
|
.diagram-guide kbd {
|
|||
|
|
align-self: start;
|
|||
|
|
min-width: 1.45rem;
|
|||
|
|
padding: 0.12rem 0.3rem;
|
|||
|
|
border: 1px solid var(--toolbar-border);
|
|||
|
|
border-radius: 0.25rem;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font: inherit;
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-shortcuts {
|
|||
|
|
display: flex;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
gap: 0.34rem 0.62rem;
|
|||
|
|
padding: 0.58rem 0.9rem 0.68rem;
|
|||
|
|
border-top: 1px solid var(--toolbar-border);
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-shortcuts span {
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.28rem;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-feedback {
|
|||
|
|
min-height: 1rem;
|
|||
|
|
margin: -0.18rem 0.9rem 0.62rem;
|
|||
|
|
color: var(--security-stroke);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-feedback:empty { display: none; }
|
|||
|
|
@keyframes archify-guide-in {
|
|||
|
|
from { opacity: 0; transform: translateY(-5px) scale(0.985); }
|
|||
|
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.node-finder {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 1rem;
|
|||
|
|
left: 1rem;
|
|||
|
|
z-index: 24;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
width: min(22rem, calc(100% - 2rem));
|
|||
|
|
max-height: calc(100% - 2rem);
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 1px solid var(--toolbar-border);
|
|||
|
|
border-radius: 0.85rem;
|
|||
|
|
background: var(--toolbar-menu-bg);
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.22);
|
|||
|
|
backdrop-filter: blur(16px);
|
|||
|
|
}
|
|||
|
|
.node-finder[hidden] { display: none; }
|
|||
|
|
.node-finder[data-context="route-source"] {
|
|||
|
|
border-color: color-mix(in srgb, var(--frontend-stroke) 68%, var(--toolbar-border));
|
|||
|
|
}
|
|||
|
|
.node-finder[data-context="route-target"] {
|
|||
|
|
border-color: color-mix(in srgb, var(--security-stroke) 68%, var(--toolbar-border));
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] .node-finder { border-radius: 0.3rem; }
|
|||
|
|
.node-finder-head {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 1rem;
|
|||
|
|
padding: 0.7rem 0.8rem 0.45rem;
|
|||
|
|
}
|
|||
|
|
.node-finder-head strong {
|
|||
|
|
color: var(--text);
|
|||
|
|
font-size: 0.6875rem;
|
|||
|
|
letter-spacing: 0.08em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.node-finder[data-context="route-source"] .node-finder-head strong { color: var(--frontend-stroke); }
|
|||
|
|
.node-finder[data-context="route-target"] .node-finder-head strong { color: var(--security-stroke); }
|
|||
|
|
.node-finder-close {
|
|||
|
|
width: 1.75rem;
|
|||
|
|
height: 1.75rem;
|
|||
|
|
border: 0;
|
|||
|
|
border-radius: 0.35rem;
|
|||
|
|
background: transparent;
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
font: inherit;
|
|||
|
|
font-size: 1rem;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
.node-finder-close:hover,
|
|||
|
|
.node-finder-close:focus-visible { background: var(--toolbar-hover); }
|
|||
|
|
.node-finder-close:focus-visible,
|
|||
|
|
.node-finder-result:focus-visible {
|
|||
|
|
outline: 2px solid var(--frontend-stroke);
|
|||
|
|
outline-offset: -2px;
|
|||
|
|
}
|
|||
|
|
.node-finder-search {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.55rem;
|
|||
|
|
margin: 0 0.55rem 0.55rem;
|
|||
|
|
padding: 0 0.7rem;
|
|||
|
|
min-height: 2.6rem;
|
|||
|
|
border: 1px solid var(--toolbar-border);
|
|||
|
|
border-radius: 0.55rem;
|
|||
|
|
background: color-mix(in srgb, var(--panel) 78%, transparent);
|
|||
|
|
transition: border-color 160ms ease-out, box-shadow 160ms ease-out;
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] .node-finder-search { border-radius: 0.18rem; }
|
|||
|
|
.node-finder-search > span { color: var(--frontend-stroke); font-size: 0.875rem; }
|
|||
|
|
.node-finder-search:focus-within {
|
|||
|
|
border-color: var(--frontend-stroke);
|
|||
|
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--frontend-stroke) 20%, transparent);
|
|||
|
|
}
|
|||
|
|
.node-finder-input {
|
|||
|
|
appearance: none;
|
|||
|
|
width: 100%;
|
|||
|
|
min-width: 0;
|
|||
|
|
border: 0;
|
|||
|
|
outline: 0;
|
|||
|
|
background: transparent;
|
|||
|
|
color: var(--text);
|
|||
|
|
font: inherit;
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
}
|
|||
|
|
.node-finder-input:focus-visible { outline: none; }
|
|||
|
|
.node-finder-input::placeholder { color: var(--text-faint); }
|
|||
|
|
.node-finder kbd {
|
|||
|
|
padding: 0.1rem 0.32rem;
|
|||
|
|
border: 1px solid var(--toolbar-border);
|
|||
|
|
border-radius: 0.25rem;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font: inherit;
|
|||
|
|
font-size: 0.5625rem;
|
|||
|
|
}
|
|||
|
|
.node-finder-results {
|
|||
|
|
display: grid;
|
|||
|
|
flex: 1 1 auto;
|
|||
|
|
gap: 0;
|
|||
|
|
min-height: 0;
|
|||
|
|
max-height: min(18rem, 48vh);
|
|||
|
|
margin: 0 0.55rem 0.45rem;
|
|||
|
|
padding: 0;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
overscroll-behavior: contain;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--toolbar-border) 78%, transparent);
|
|||
|
|
border-radius: 0.55rem;
|
|||
|
|
background: color-mix(in srgb, var(--panel) 54%, transparent);
|
|||
|
|
scroll-padding-block: 0.25rem;
|
|||
|
|
scrollbar-width: thin;
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] .node-finder-results { border-radius: 0.18rem; }
|
|||
|
|
.node-finder-result {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|||
|
|
gap: 0.2rem 0.75rem;
|
|||
|
|
width: 100%;
|
|||
|
|
min-height: 3.25rem;
|
|||
|
|
padding: 0.58rem 0.65rem;
|
|||
|
|
border: 0;
|
|||
|
|
border-radius: 0;
|
|||
|
|
background: transparent;
|
|||
|
|
color: var(--text);
|
|||
|
|
font: inherit;
|
|||
|
|
text-align: left;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: background-color 160ms ease-out;
|
|||
|
|
}
|
|||
|
|
.node-finder-result:not(:last-child) {
|
|||
|
|
border-bottom: 1px solid color-mix(in srgb, var(--toolbar-border) 62%, transparent);
|
|||
|
|
}
|
|||
|
|
.node-finder-result:hover,
|
|||
|
|
.node-finder-result:focus-visible {
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 1;
|
|||
|
|
background: color-mix(in srgb, var(--frontend-fill) 48%, var(--toolbar-hover));
|
|||
|
|
}
|
|||
|
|
.node-finder-result strong {
|
|||
|
|
overflow: hidden;
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
line-height: 1.25;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.node-finder-result small {
|
|||
|
|
grid-column: 1 / -1;
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.5625rem;
|
|||
|
|
letter-spacing: 0.015em;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.node-finder-result em {
|
|||
|
|
align-self: center;
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
font-size: 0.625rem;
|
|||
|
|
font-style: normal;
|
|||
|
|
}
|
|||
|
|
.node-finder[data-context^="route-"] .node-finder-result em {
|
|||
|
|
padding: 0.1rem 0.3rem;
|
|||
|
|
border: 1px solid color-mix(in srgb, currentColor 52%, transparent);
|
|||
|
|
border-radius: 999px;
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
font-weight: 800;
|
|||
|
|
letter-spacing: 0.05em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.node-finder[data-context="route-target"] .node-finder-result em { color: var(--security-stroke); }
|
|||
|
|
.node-finder-empty {
|
|||
|
|
padding: 1.1rem 0.75rem 1.25rem;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.6875rem;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
.node-finder-status {
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 0.1rem 0.8rem 0.65rem;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.5625rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.guided-views {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 2.75rem minmax(0, 1fr) 2.75rem auto;
|
|||
|
|
align-items: stretch;
|
|||
|
|
gap: 0.35rem;
|
|||
|
|
min-width: 0;
|
|||
|
|
margin: 0 0 0.75rem;
|
|||
|
|
padding: 0.35rem;
|
|||
|
|
border: 1px solid var(--toolbar-border);
|
|||
|
|
border-radius: 0.85rem;
|
|||
|
|
background: var(--toolbar-bg);
|
|||
|
|
background: linear-gradient(120deg, var(--toolbar-bg), color-mix(in srgb, var(--toolbar-bg) 82%, var(--frontend-fill)));
|
|||
|
|
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.16);
|
|||
|
|
backdrop-filter: blur(12px);
|
|||
|
|
}
|
|||
|
|
.guided-views[hidden] { display: none; }
|
|||
|
|
|
|||
|
|
html[data-preset="blueprint"] .guided-views {
|
|||
|
|
border-color: var(--panel-border);
|
|||
|
|
border-radius: 0.35rem;
|
|||
|
|
background:
|
|||
|
|
linear-gradient(90deg, color-mix(in srgb, var(--frontend-fill) 36%, transparent), transparent 24%),
|
|||
|
|
var(--toolbar-bg);
|
|||
|
|
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.12);
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] .guided-views button { border-radius: 0.2rem; }
|
|||
|
|
html[data-preset="blueprint"] .guided-view-progress { border-radius: 0; }
|
|||
|
|
html[data-preset="blueprint"] .guided-view-progress span { border-radius: 0; }
|
|||
|
|
.guided-views button {
|
|||
|
|
min-width: 0;
|
|||
|
|
padding: 0.45rem 0.6rem;
|
|||
|
|
border-radius: 0.55rem;
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
}
|
|||
|
|
.guided-views > #guided-view-prev,
|
|||
|
|
.guided-views > #guided-view-next { min-width: 2.75rem; min-height: 2.75rem; }
|
|||
|
|
.guided-views button:disabled { opacity: 0.3; cursor: default; }
|
|||
|
|
.guided-view-copy {
|
|||
|
|
min-width: 0;
|
|||
|
|
padding: 0.2rem 0.5rem;
|
|||
|
|
border-left: 1px solid var(--toolbar-border);
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
.guided-view-meta {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
min-height: 1rem;
|
|||
|
|
margin-bottom: 0.12rem;
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
font-size: 0.5625rem;
|
|||
|
|
font-weight: 700;
|
|||
|
|
letter-spacing: 0.12em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.guided-view-handoff {
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
height: 1rem;
|
|||
|
|
min-width: 0;
|
|||
|
|
max-width: 15rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
padding: 0 0.38rem;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--frontend-stroke) 46%, transparent);
|
|||
|
|
border-radius: 999px;
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
background: color-mix(in srgb, var(--frontend-fill) 44%, transparent);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
line-height: calc(1rem - 2px);
|
|||
|
|
letter-spacing: 0.06em;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.guided-view-handoff[hidden] { display: none; }
|
|||
|
|
html[data-preset="blueprint"] .guided-view-handoff { border-radius: 0.15rem; }
|
|||
|
|
.chapter-handoff-overlay { pointer-events: none; }
|
|||
|
|
.chapter-handoff-anchor {
|
|||
|
|
fill: color-mix(in srgb, var(--frontend-fill) 10%, transparent);
|
|||
|
|
stroke: var(--frontend-stroke);
|
|||
|
|
stroke-width: 2;
|
|||
|
|
stroke-dasharray: 4 4;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
opacity: 0.96;
|
|||
|
|
animation: archify-chapter-anchor 0.42s cubic-bezier(0.22, 1, 0.36, 1) both;
|
|||
|
|
}
|
|||
|
|
svg[data-chapter-handoff] [data-node-id][data-chapter-role="stay"] { opacity: 1; }
|
|||
|
|
svg[data-chapter-handoff] [data-node-id][data-chapter-role="enter"] { opacity: 0.78; }
|
|||
|
|
svg[data-chapter-handoff] [data-node-id][data-chapter-role="leave"] { opacity: 0.26; }
|
|||
|
|
@keyframes archify-chapter-anchor {
|
|||
|
|
0% { opacity: 0.35; stroke-dashoffset: 8; }
|
|||
|
|
45% { opacity: 1; }
|
|||
|
|
100% { opacity: 0.72; stroke-dashoffset: 0; }
|
|||
|
|
}
|
|||
|
|
.guided-view-progress {
|
|||
|
|
width: 3.5rem;
|
|||
|
|
height: 2px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border-radius: 999px;
|
|||
|
|
background: var(--toolbar-border);
|
|||
|
|
}
|
|||
|
|
.guided-view-progress span {
|
|||
|
|
display: block;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
border-radius: inherit;
|
|||
|
|
background: var(--frontend-stroke);
|
|||
|
|
transform: scaleX(0);
|
|||
|
|
transform-origin: left center;
|
|||
|
|
}
|
|||
|
|
.guided-view-copy strong,
|
|||
|
|
.guided-view-copy small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|||
|
|
.guided-view-copy strong { color: var(--toolbar-text); font-size: 0.75rem; line-height: 1.35; }
|
|||
|
|
.guided-view-copy small { margin-top: 0; color: var(--text-muted); font-size: 0.625rem; line-height: 1.35; }
|
|||
|
|
.guided-story-caption {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|||
|
|
gap: 0.12rem 0.55rem;
|
|||
|
|
align-items: center;
|
|||
|
|
min-width: 0;
|
|||
|
|
margin-top: 0.18rem;
|
|||
|
|
padding: 0.3rem 0.5rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--frontend-stroke) 32%, var(--toolbar-border));
|
|||
|
|
border-radius: 0.58rem;
|
|||
|
|
background: color-mix(in srgb, var(--frontend-fill) 26%, transparent);
|
|||
|
|
animation: archify-story-caption-in 140ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
|||
|
|
}
|
|||
|
|
.guided-story-caption[hidden] { display: none; }
|
|||
|
|
.guided-story-caption-index {
|
|||
|
|
grid-row: 1 / span 2;
|
|||
|
|
align-self: stretch;
|
|||
|
|
display: inline-grid;
|
|||
|
|
min-width: 3.3rem;
|
|||
|
|
place-items: center;
|
|||
|
|
padding-right: 0.52rem;
|
|||
|
|
border-right: 1px solid color-mix(in srgb, var(--frontend-stroke) 28%, var(--toolbar-border));
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
font-size: 0.5625rem;
|
|||
|
|
font-weight: 800;
|
|||
|
|
letter-spacing: 0.08em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.guided-story-caption strong,
|
|||
|
|
.guided-story-caption small {
|
|||
|
|
display: block;
|
|||
|
|
min-width: 0;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.guided-story-caption strong { color: var(--toolbar-text); font-size: 0.6875rem; line-height: 1.3; }
|
|||
|
|
.guided-story-caption small { color: var(--text-muted); font-size: 0.5625rem; line-height: 1.35; }
|
|||
|
|
.guided-story-caption-next {
|
|||
|
|
grid-column: 3;
|
|||
|
|
grid-row: 1 / span 2;
|
|||
|
|
display: grid;
|
|||
|
|
min-width: 6.4rem;
|
|||
|
|
max-width: 9.5rem;
|
|||
|
|
align-self: stretch;
|
|||
|
|
align-content: center;
|
|||
|
|
padding-left: 0.58rem;
|
|||
|
|
border-left: 1px solid color-mix(in srgb, var(--frontend-stroke) 22%, var(--toolbar-border));
|
|||
|
|
}
|
|||
|
|
.guided-story-caption-next[hidden] { display: none; }
|
|||
|
|
.guided-story-caption-next small {
|
|||
|
|
color: color-mix(in srgb, var(--frontend-stroke) 72%, var(--text-muted));
|
|||
|
|
font-weight: 800;
|
|||
|
|
letter-spacing: 0.09em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.guided-story-caption-next strong { color: var(--text-muted); font-weight: 700; }
|
|||
|
|
html[data-preset="blueprint"] .guided-story-caption { border-radius: 0.2rem; }
|
|||
|
|
.guided-views[data-story-beat] .guided-view-copy > #guided-view-label,
|
|||
|
|
.guided-views[data-story-beat] .guided-view-copy > #guided-view-note { display: none; }
|
|||
|
|
@keyframes archify-story-caption-in {
|
|||
|
|
from { opacity: 0; transform: translateY(3px); }
|
|||
|
|
to { opacity: 1; transform: translateY(0); }
|
|||
|
|
}
|
|||
|
|
.guided-view-trail {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.28rem;
|
|||
|
|
min-width: 0;
|
|||
|
|
min-height: 1.7rem;
|
|||
|
|
margin-top: 0;
|
|||
|
|
padding: 0;
|
|||
|
|
overflow-x: auto;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
overscroll-behavior-x: contain;
|
|||
|
|
touch-action: pan-x;
|
|||
|
|
scrollbar-width: none;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.guided-view-trail[hidden] { display: none; }
|
|||
|
|
.guided-view-trail::-webkit-scrollbar { display: none; }
|
|||
|
|
.guided-view-trail .guided-view-stop {
|
|||
|
|
appearance: none;
|
|||
|
|
box-sizing: border-box;
|
|||
|
|
display: inline-flex;
|
|||
|
|
flex: 0 0 auto;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.28rem;
|
|||
|
|
min-height: 1.5rem;
|
|||
|
|
padding: 0.1rem 0.3rem 0.1rem 0.18rem;
|
|||
|
|
border: 1px solid transparent;
|
|||
|
|
border-radius: 999px;
|
|||
|
|
background: transparent;
|
|||
|
|
color: var(--text-muted);
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-family: inherit;
|
|||
|
|
font-size: 0.5625rem;
|
|||
|
|
font-weight: 650;
|
|||
|
|
letter-spacing: 0.02em;
|
|||
|
|
line-height: 1;
|
|||
|
|
transition: background 140ms ease, border-color 140ms ease, color 140ms ease, opacity 140ms ease;
|
|||
|
|
}
|
|||
|
|
.guided-view-stop:hover {
|
|||
|
|
border-color: color-mix(in srgb, var(--frontend-stroke) 36%, transparent);
|
|||
|
|
background: color-mix(in srgb, var(--frontend-fill) 34%, transparent);
|
|||
|
|
}
|
|||
|
|
.guided-view-stop:focus-visible {
|
|||
|
|
outline: 2px solid var(--frontend-stroke);
|
|||
|
|
outline-offset: 1px;
|
|||
|
|
}
|
|||
|
|
.guided-view-stop:disabled { cursor: wait; }
|
|||
|
|
.guided-view-stop::before {
|
|||
|
|
display: inline-grid;
|
|||
|
|
width: 1rem;
|
|||
|
|
height: 1rem;
|
|||
|
|
place-items: center;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--frontend-stroke) 54%, var(--toolbar-border));
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: color-mix(in srgb, var(--frontend-fill) 58%, var(--toolbar-bg));
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
content: attr(data-story-number);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
line-height: 1;
|
|||
|
|
}
|
|||
|
|
.guided-view-stop + .guided-view-stop::after {
|
|||
|
|
order: -1;
|
|||
|
|
color: color-mix(in srgb, var(--frontend-stroke) 58%, var(--text-faint));
|
|||
|
|
content: '\00b7';
|
|||
|
|
font-size: 0.65rem;
|
|||
|
|
}
|
|||
|
|
.guided-view-stop[data-story-link="forward"]::after { content: '\2192'; }
|
|||
|
|
.guided-view-stop[data-story-link="reverse"]::after { content: '\2190'; }
|
|||
|
|
.guided-view-stop[data-story-link="multiple"]::after { content: '\21c4'; }
|
|||
|
|
.guided-view-stop[data-story-beat-state="pending"] { opacity: 0.38; }
|
|||
|
|
.guided-view-stop[data-story-beat-state="next"] {
|
|||
|
|
border-style: dashed;
|
|||
|
|
border-color: color-mix(in srgb, var(--frontend-stroke) 42%, var(--toolbar-border));
|
|||
|
|
color: color-mix(in srgb, var(--toolbar-text) 68%, var(--text-muted));
|
|||
|
|
opacity: 0.58;
|
|||
|
|
}
|
|||
|
|
.guided-view-stop[data-story-beat-state="past"] { color: var(--text-muted); opacity: 0.76; }
|
|||
|
|
.guided-view-stop[data-story-beat-state="active"] {
|
|||
|
|
border-color: color-mix(in srgb, var(--frontend-stroke) 72%, var(--toolbar-border));
|
|||
|
|
background: color-mix(in srgb, var(--frontend-fill) 42%, transparent);
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
font-weight: 800;
|
|||
|
|
opacity: 1;
|
|||
|
|
}
|
|||
|
|
.guided-view-stop[data-story-beat-state="active"]::before {
|
|||
|
|
border-color: var(--frontend-stroke);
|
|||
|
|
box-shadow: 0 0 10px color-mix(in srgb, var(--frontend-stroke) 54%, transparent);
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] .guided-view-stop,
|
|||
|
|
html[data-preset="blueprint"] .guided-view-stop::before { border-radius: 0.1rem; }
|
|||
|
|
html[data-preset="blueprint"] .guided-view-stop[data-story-beat-state="active"]::before { box-shadow: none; }
|
|||
|
|
.guided-view-actions { display: flex; align-items: stretch; gap: 0.2rem; }
|
|||
|
|
.guided-view-actions button { white-space: nowrap; }
|
|||
|
|
.guided-view-play {
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
gap: 0.38rem;
|
|||
|
|
min-width: 6.25rem !important;
|
|||
|
|
color: var(--frontend-stroke) !important;
|
|||
|
|
font-size: 0.625rem !important;
|
|||
|
|
}
|
|||
|
|
.guided-view-play[aria-pressed="true"] { background: var(--toolbar-hover); }
|
|||
|
|
.guided-view-all { min-width: 3.25rem !important; color: var(--text-muted) !important; font-size: 0.625rem !important; }
|
|||
|
|
.guided-view-beat-link {
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
gap: 0.3rem;
|
|||
|
|
min-width: 5.4rem !important;
|
|||
|
|
min-height: 1.5rem;
|
|||
|
|
color: var(--text-muted) !important;
|
|||
|
|
font-size: 0.5625rem !important;
|
|||
|
|
letter-spacing: 0.025em;
|
|||
|
|
transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
|
|||
|
|
}
|
|||
|
|
.guided-view-beat-link:not(:disabled):hover {
|
|||
|
|
border-color: color-mix(in srgb, var(--frontend-stroke) 52%, var(--toolbar-border));
|
|||
|
|
color: var(--frontend-stroke) !important;
|
|||
|
|
}
|
|||
|
|
.guided-view-beat-link[data-copy-state="copied"] {
|
|||
|
|
border-color: color-mix(in srgb, var(--backend-stroke) 58%, var(--toolbar-border));
|
|||
|
|
background: color-mix(in srgb, var(--backend-fill) 46%, transparent);
|
|||
|
|
color: var(--backend-stroke) !important;
|
|||
|
|
}
|
|||
|
|
.guided-view-index {
|
|||
|
|
grid-column: 1 / -1;
|
|||
|
|
min-width: 0;
|
|||
|
|
padding: 0.28rem 0.12rem 0.08rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border-top: 1px solid color-mix(in srgb, var(--toolbar-border) 72%, transparent);
|
|||
|
|
}
|
|||
|
|
@media (min-width: 721px) {
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .guided-views[data-playing="true"] .guided-view-index,
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .guided-views[data-playing="true"] .guided-view-beat-link,
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .guided-views[data-playing="true"] .guided-view-all,
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .guided-views[data-playing="true"] .guided-view-trail,
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .guided-views[data-playing="true"] .guided-view-copy > #guided-view-label,
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .guided-views[data-playing="true"] .guided-view-copy > #guided-view-note { display: none; }
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .guided-views[data-playing="true"] .guided-story-caption { margin-top: 0.08rem; }
|
|||
|
|
}
|
|||
|
|
.guided-view-chapters {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 0.28rem;
|
|||
|
|
min-width: 0;
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 0.08rem 0.05rem 0.2rem;
|
|||
|
|
overflow-x: auto;
|
|||
|
|
list-style: none;
|
|||
|
|
scroll-padding-inline: 0.05rem;
|
|||
|
|
scroll-snap-type: x proximity;
|
|||
|
|
scrollbar-width: none;
|
|||
|
|
}
|
|||
|
|
.guided-view-chapters::-webkit-scrollbar { display: none; }
|
|||
|
|
.guided-view-chapters li {
|
|||
|
|
display: flex;
|
|||
|
|
flex: 1 1 0;
|
|||
|
|
min-width: 9.5rem;
|
|||
|
|
scroll-snap-align: center;
|
|||
|
|
}
|
|||
|
|
.guided-view-chapter {
|
|||
|
|
position: relative;
|
|||
|
|
display: grid !important;
|
|||
|
|
grid-template-columns: 1.55rem minmax(0, 1fr) auto;
|
|||
|
|
align-items: center;
|
|||
|
|
width: 100%;
|
|||
|
|
min-height: 2.75rem;
|
|||
|
|
padding: 0.36rem 0.48rem !important;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--toolbar-border) 86%, transparent) !important;
|
|||
|
|
border-radius: 0.48rem !important;
|
|||
|
|
background: color-mix(in srgb, var(--toolbar-bg) 88%, transparent) !important;
|
|||
|
|
color: var(--text-muted) !important;
|
|||
|
|
text-align: left;
|
|||
|
|
transition: background 0.16s ease, border-color 0.16s ease, opacity 0.16s ease;
|
|||
|
|
}
|
|||
|
|
.guided-view-chapter:hover {
|
|||
|
|
border-color: color-mix(in srgb, var(--frontend-stroke) 58%, var(--toolbar-border)) !important;
|
|||
|
|
background: color-mix(in srgb, var(--frontend-fill) 42%, var(--toolbar-bg)) !important;
|
|||
|
|
}
|
|||
|
|
.guided-view-chapter:focus-visible {
|
|||
|
|
outline: 2px solid var(--frontend-stroke);
|
|||
|
|
outline-offset: 1px;
|
|||
|
|
}
|
|||
|
|
.guided-view-chapter-index {
|
|||
|
|
display: inline-grid;
|
|||
|
|
width: 1.32rem;
|
|||
|
|
height: 1.32rem;
|
|||
|
|
place-items: center;
|
|||
|
|
border: 1px solid color-mix(in srgb, var(--frontend-stroke) 48%, var(--toolbar-border));
|
|||
|
|
border-radius: 50%;
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
font-weight: 800;
|
|||
|
|
letter-spacing: 0.03em;
|
|||
|
|
}
|
|||
|
|
.guided-view-chapter-title {
|
|||
|
|
min-width: 0;
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
font-size: 0.625rem;
|
|||
|
|
font-weight: 700;
|
|||
|
|
line-height: 1.3;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.guided-view-chapter-count {
|
|||
|
|
grid-column: 3;
|
|||
|
|
grid-row: 1;
|
|||
|
|
margin-left: 0.38rem;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
font-style: normal;
|
|||
|
|
font-weight: 650;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.guided-view-chapter-delta {
|
|||
|
|
display: inline-flex;
|
|||
|
|
grid-column: 3;
|
|||
|
|
grid-row: 1;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.22rem;
|
|||
|
|
margin-left: 0.38rem;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
font-weight: 750;
|
|||
|
|
letter-spacing: 0.01em;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.guided-view-chapter-delta[hidden] { display: none; }
|
|||
|
|
.guided-view-chapter-delta [data-delta-kind="stay"] { color: var(--text-muted); }
|
|||
|
|
.guided-view-chapter-delta [data-delta-kind="enter"] { color: var(--frontend-stroke); }
|
|||
|
|
.guided-view-chapter-delta [data-delta-kind="leave"] { color: var(--security-stroke); }
|
|||
|
|
.guided-view-chapter[data-preview-active="true"] {
|
|||
|
|
border-color: color-mix(in srgb, var(--frontend-stroke) 72%, var(--toolbar-border)) !important;
|
|||
|
|
background: color-mix(in srgb, var(--frontend-fill) 54%, var(--toolbar-bg)) !important;
|
|||
|
|
}
|
|||
|
|
.guided-view-chapter[data-chapter-position="current"] {
|
|||
|
|
border: 2px solid var(--frontend-stroke) !important;
|
|||
|
|
padding: calc(0.36rem - 1px) calc(0.48rem - 1px) !important;
|
|||
|
|
box-shadow: inset 3px 0 0 var(--frontend-stroke);
|
|||
|
|
color: var(--toolbar-text) !important;
|
|||
|
|
}
|
|||
|
|
.guided-view-chapter[data-chapter-position="current"] .guided-view-chapter-index {
|
|||
|
|
background: var(--frontend-stroke);
|
|||
|
|
color: var(--bg);
|
|||
|
|
}
|
|||
|
|
.guided-view-chapter[data-chapter-position="before"] {
|
|||
|
|
border-style: solid !important;
|
|||
|
|
opacity: 0.72;
|
|||
|
|
}
|
|||
|
|
.guided-view-chapter[data-chapter-position="after"] { border-style: dashed !important; }
|
|||
|
|
html[data-preset="signal-flow"] .guided-view-chapter[data-chapter-position="current"] {
|
|||
|
|
box-shadow: inset 3px 0 0 var(--frontend-stroke), 0 0 18px color-mix(in srgb, var(--frontend-stroke) 18%, transparent);
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] .guided-view-chapter,
|
|||
|
|
html[data-preset="blueprint"] .guided-view-chapter-index { border-radius: 0.12rem !important; }
|
|||
|
|
|
|||
|
|
/* Story Shelf — keep every authored chapter and the Play action visible,
|
|||
|
|
but do not spend a second row on unavailable director controls before
|
|||
|
|
the reader enters a chapter. Existing data-active-view state expands
|
|||
|
|
the full guided surface without a new toggle or stored preference. */
|
|||
|
|
.guided-views[data-active-view="all"] {
|
|||
|
|
grid-template-columns: minmax(11rem, .72fr) auto minmax(0, 2fr);
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 0.28rem 0.35rem;
|
|||
|
|
}
|
|||
|
|
.guided-views[data-active-view="all"] > #guided-view-prev,
|
|||
|
|
.guided-views[data-active-view="all"] > #guided-view-next,
|
|||
|
|
.guided-views[data-active-view="all"] .guided-view-beat-link,
|
|||
|
|
.guided-views[data-active-view="all"] .guided-view-all { display: none; }
|
|||
|
|
.guided-views[data-active-view="all"] .guided-view-copy {
|
|||
|
|
grid-column: 1;
|
|||
|
|
grid-row: 1;
|
|||
|
|
padding: 0.1rem 0.55rem;
|
|||
|
|
}
|
|||
|
|
.guided-views[data-active-view="all"] .guided-view-copy > #guided-view-note { display: none; }
|
|||
|
|
.guided-views[data-active-view="all"] .guided-view-actions {
|
|||
|
|
display: block;
|
|||
|
|
grid-column: 2;
|
|||
|
|
grid-row: 1;
|
|||
|
|
}
|
|||
|
|
.guided-views[data-active-view="all"] .guided-view-play { min-height: 2.75rem; }
|
|||
|
|
.guided-views[data-active-view="all"] .guided-view-index {
|
|||
|
|
grid-column: 3;
|
|||
|
|
grid-row: 1;
|
|||
|
|
padding: 0 0.08rem 0 0.42rem;
|
|||
|
|
border-top: 0;
|
|||
|
|
border-left: 1px solid color-mix(in srgb, var(--toolbar-border) 72%, transparent);
|
|||
|
|
}
|
|||
|
|
.guided-views[data-active-view="all"] .guided-view-chapters {
|
|||
|
|
gap: 0.3rem;
|
|||
|
|
padding: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.cards {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|||
|
|
gap: 1rem;
|
|||
|
|
margin-top: 2rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card {
|
|||
|
|
background: var(--panel);
|
|||
|
|
border-radius: 0.75rem;
|
|||
|
|
border: 1px solid var(--panel-border);
|
|||
|
|
padding: 1.25rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
html[data-preset="signal-flow"] .card {
|
|||
|
|
background: linear-gradient(145deg, rgba(10, 24, 43, 0.86), rgba(5, 12, 24, 0.92));
|
|||
|
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.035);
|
|||
|
|
}
|
|||
|
|
html[data-preset="signal-flow"][data-theme="light"] .card {
|
|||
|
|
background: linear-gradient(145deg, rgba(255, 255, 255, 0.92), rgba(237, 246, 250, 0.9));
|
|||
|
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82), 0 12px 32px rgba(45, 78, 99, 0.08);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Preserve the one-screen guarantee on ordinary laptop displays. The
|
|||
|
|
high-screen expansion above must not trade a fuller canvas for page
|
|||
|
|
scrolling at 900–1080px heights. */
|
|||
|
|
@media (min-width: 768px) and (max-height: 1100px) {
|
|||
|
|
.header { margin-bottom: 1rem; }
|
|||
|
|
.diagram-container {
|
|||
|
|
padding: 0.875rem;
|
|||
|
|
padding-bottom: calc(0.875rem + var(--archify-nav-reserve));
|
|||
|
|
}
|
|||
|
|
.cards { margin-top: 1rem; }
|
|||
|
|
.card { padding: 1rem; }
|
|||
|
|
}
|
|||
|
|
@media (min-width: 768px) and (max-height: 920px) {
|
|||
|
|
body { padding-block: 1.25rem; }
|
|||
|
|
.header { margin-bottom: 0.875rem; }
|
|||
|
|
.cards { margin-top: 0.75rem; }
|
|||
|
|
html[data-nav-stage-rail="true"] body { padding-block: 0.375rem; }
|
|||
|
|
html[data-nav-stage-rail="true"] .header { margin-bottom: 0.5rem; }
|
|||
|
|
html[data-nav-stage-rail="true"] .guided-views { margin-bottom: 0.375rem; }
|
|||
|
|
html[data-nav-stage-rail="true"] .diagram-container[data-nav-stage-rail="true"] { padding-top: 0.5rem; }
|
|||
|
|
html[data-nav-stage-rail="true"] .cards { margin-top: 0.375rem; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
html[data-preset="blueprint"] .card {
|
|||
|
|
position: relative;
|
|||
|
|
border-color: var(--panel-border);
|
|||
|
|
border-radius: 0.3rem;
|
|||
|
|
background: var(--panel);
|
|||
|
|
box-shadow: none;
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] .card::before {
|
|||
|
|
content: "";
|
|||
|
|
position: absolute;
|
|||
|
|
top: -1px;
|
|||
|
|
left: 1rem;
|
|||
|
|
width: 2.5rem;
|
|||
|
|
border-top: 2px solid var(--frontend-stroke);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
html[data-preset="editorial"] .card {
|
|||
|
|
position: relative;
|
|||
|
|
border-radius: 0.28rem;
|
|||
|
|
background: var(--panel);
|
|||
|
|
box-shadow: 0 10px 28px color-mix(in srgb, #000 10%, transparent);
|
|||
|
|
}
|
|||
|
|
html[data-preset="editorial"] .card::before {
|
|||
|
|
position: absolute;
|
|||
|
|
top: -1px;
|
|||
|
|
left: 1.25rem;
|
|||
|
|
width: 3.25rem;
|
|||
|
|
border-top: 3px solid var(--arrow-emphasis);
|
|||
|
|
content: "";
|
|||
|
|
}
|
|||
|
|
html[data-preset="editorial"] .card-dot { border-radius: 0; transform: rotate(45deg) scale(0.78); }
|
|||
|
|
|
|||
|
|
.card-header {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
margin-bottom: 0.75rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card-dot {
|
|||
|
|
width: 8px;
|
|||
|
|
height: 8px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card-dot.cyan { background: var(--frontend-stroke); }
|
|||
|
|
.card-dot.emerald { background: var(--backend-stroke); }
|
|||
|
|
.card-dot.violet { background: var(--database-stroke); }
|
|||
|
|
.card-dot.amber { background: var(--cloud-stroke); }
|
|||
|
|
.card-dot.rose { background: var(--security-stroke); }
|
|||
|
|
.card-dot.orange { background: var(--messagebus-stroke); }
|
|||
|
|
.card-dot.slate { background: var(--external-stroke); }
|
|||
|
|
|
|||
|
|
.card h3 { font-size: 0.875rem; font-weight: 600; color: var(--text); }
|
|||
|
|
|
|||
|
|
.card ul {
|
|||
|
|
list-style: none;
|
|||
|
|
color: var(--text-muted);
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.card li { margin-bottom: 0.375rem; }
|
|||
|
|
|
|||
|
|
/* ==========================================================
|
|||
|
|
Print stylesheet — hide interactive controls, force light,
|
|||
|
|
drop the grid so the diagram doesn't waste ink, use landscape
|
|||
|
|
so wide diagrams fit, pack summary cards into 2 columns to
|
|||
|
|
avoid an orphan card on a trailing page.
|
|||
|
|
========================================================== */
|
|||
|
|
@page { size: landscape; margin: 1.5cm; }
|
|||
|
|
|
|||
|
|
@media print {
|
|||
|
|
/* Force the FULL light palette — including component fills/strokes and
|
|||
|
|
lane/arrow colors — so printing from dark theme doesn't put neon
|
|||
|
|
strokes and translucent dark fills on white paper. */
|
|||
|
|
:root, [data-theme="dark"], [data-theme="light"] {
|
|||
|
|
--bg: #ffffff;
|
|||
|
|
--grid: transparent;
|
|||
|
|
--panel: #ffffff;
|
|||
|
|
--panel-border: #e2e8f0;
|
|||
|
|
--text: #0f172a;
|
|||
|
|
--text-muted: #475569;
|
|||
|
|
--text-dim: #94a3b8;
|
|||
|
|
--text-faint: #64748b;
|
|||
|
|
--mask: #ffffff;
|
|||
|
|
--lane-fill: rgba(248, 250, 252, 0.65);
|
|||
|
|
--lane-stroke: #cbd5e1;
|
|||
|
|
--arrow: #94a3b8;
|
|||
|
|
--arrow-emphasis: #059669;
|
|||
|
|
--frontend-fill: rgba(34, 211, 238, 0.15);
|
|||
|
|
--frontend-stroke: #0891b2;
|
|||
|
|
--backend-fill: rgba(52, 211, 153, 0.18);
|
|||
|
|
--backend-stroke: #059669;
|
|||
|
|
--database-fill: rgba(167, 139, 250, 0.2);
|
|||
|
|
--database-stroke: #7c3aed;
|
|||
|
|
--cloud-fill: rgba(251, 191, 36, 0.18);
|
|||
|
|
--cloud-stroke: #d97706;
|
|||
|
|
--security-fill: rgba(251, 113, 133, 0.15);
|
|||
|
|
--security-stroke: #e11d48;
|
|||
|
|
--messagebus-fill: rgba(251, 146, 60, 0.15);
|
|||
|
|
--messagebus-stroke:#ea580c;
|
|||
|
|
--external-fill: rgba(148, 163, 184, 0.18);
|
|||
|
|
--external-stroke: #64748b;
|
|||
|
|
}
|
|||
|
|
body { background: #ffffff !important; padding: 0; }
|
|||
|
|
.container { max-width: none; }
|
|||
|
|
.toolbar, .diagram-nav, .focus-chip, .guided-views, .archify-toast, .no-print { display: none !important; }
|
|||
|
|
.chapter-handoff-overlay { display: none !important; }
|
|||
|
|
svg [data-node-id][data-chapter-role] { opacity: 1 !important; }
|
|||
|
|
svg[data-chapter-preview] [data-node-id],
|
|||
|
|
svg[data-chapter-preview] [data-edge-from] { opacity: 1 !important; filter: none !important; }
|
|||
|
|
.diagram-container, .card { box-shadow: none; border-color: #e2e8f0; }
|
|||
|
|
.diagram-container { --archify-nav-reserve: 0px !important; }
|
|||
|
|
html[data-preset="signal-flow"] .diagram-container,
|
|||
|
|
html[data-preset="signal-flow"] .card { background: #ffffff; }
|
|||
|
|
.cards { grid-template-columns: 1fr 1fr; }
|
|||
|
|
/* Ensure elements break sensibly across pages */
|
|||
|
|
.card { break-inside: avoid; page-break-inside: avoid; }
|
|||
|
|
.diagram-container { break-inside: avoid; page-break-inside: avoid; }
|
|||
|
|
.diagram-container svg [data-detail] {
|
|||
|
|
opacity: 1 !important;
|
|||
|
|
pointer-events: auto !important;
|
|||
|
|
}
|
|||
|
|
.diagram-container svg [data-detail-anchor] { transform: none !important; }
|
|||
|
|
.diagram-container svg[data-lens-active] [data-node-id],
|
|||
|
|
.diagram-container svg[data-lens-active] [data-edge-from],
|
|||
|
|
.diagram-container svg[data-legend-preview-active] [data-node-id],
|
|||
|
|
.diagram-container svg[data-legend-preview-active] [data-edge-from] {
|
|||
|
|
opacity: 1 !important;
|
|||
|
|
filter: none !important;
|
|||
|
|
}
|
|||
|
|
[data-legend-bridge-runtime] { display: none !important; }
|
|||
|
|
.semantic-lens-overlay { display: none !important; }
|
|||
|
|
.relationship-hit-overlay { display: none !important; }
|
|||
|
|
.relationship-pulse-overlay { display: none !important; }
|
|||
|
|
.route-probe-overlay, .route-journey-overlay { display: none !important; }
|
|||
|
|
.story-trail-overlay,
|
|||
|
|
.story-carrier-overlay { display: none !important; }
|
|||
|
|
.diagram-container svg[data-focus-active] [data-node-id],
|
|||
|
|
.diagram-container svg[data-focus-active] [data-edge-from],
|
|||
|
|
.diagram-container svg[data-reach-active] [data-node-id],
|
|||
|
|
.diagram-container svg[data-reach-active] [data-edge-from],
|
|||
|
|
.diagram-container svg[data-story-beat] [data-story-step],
|
|||
|
|
.diagram-container svg[data-story-beat] [data-edge-from][data-story-beat-step],
|
|||
|
|
.diagram-container svg[data-route-active] [data-node-id],
|
|||
|
|
.diagram-container svg[data-route-active] [data-edge-from] {
|
|||
|
|
opacity: 1 !important;
|
|||
|
|
filter: none !important;
|
|||
|
|
animation: none !important;
|
|||
|
|
}
|
|||
|
|
h1, .subtitle { color: #0f172a; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ==========================================================
|
|||
|
|
TOOLBAR (theme toggle + export menu)
|
|||
|
|
========================================================== */
|
|||
|
|
.toolbar {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 1rem;
|
|||
|
|
right: 1rem;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
padding: 0;
|
|||
|
|
border: 0;
|
|||
|
|
background: transparent;
|
|||
|
|
box-shadow: none;
|
|||
|
|
z-index: 100;
|
|||
|
|
}
|
|||
|
|
.toolbar button {
|
|||
|
|
background: var(--toolbar-bg);
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
border: 1px solid var(--toolbar-border);
|
|||
|
|
padding: 0.5rem 0.875rem;
|
|||
|
|
border-radius: 0.625rem;
|
|||
|
|
font-family: inherit;
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
font-weight: 500;
|
|||
|
|
cursor: pointer;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
backdrop-filter: blur(10px);
|
|||
|
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
|
|||
|
|
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.375rem;
|
|||
|
|
min-height: 2.75rem;
|
|||
|
|
}
|
|||
|
|
.toolbar button:hover {
|
|||
|
|
background: var(--toolbar-hover);
|
|||
|
|
border-color: color-mix(in srgb, var(--arrow) 62%, var(--toolbar-border));
|
|||
|
|
}
|
|||
|
|
.toolbar button[aria-expanded="true"],
|
|||
|
|
.toolbar button[aria-pressed="true"]:not(#btn-theme) {
|
|||
|
|
background: color-mix(in srgb, var(--frontend-stroke) 10%, var(--toolbar-hover));
|
|||
|
|
border-color: color-mix(in srgb, var(--frontend-stroke) 48%, var(--toolbar-border));
|
|||
|
|
}
|
|||
|
|
.toolbar #btn-export {
|
|||
|
|
padding-right: 0.82rem;
|
|||
|
|
padding-left: 0.82rem;
|
|||
|
|
}
|
|||
|
|
.toolbar #btn-export:hover,
|
|||
|
|
.toolbar #btn-export[aria-expanded="true"] {
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
background: color-mix(in srgb, var(--frontend-stroke) 10%, var(--toolbar-hover));
|
|||
|
|
border-color: color-mix(in srgb, var(--frontend-stroke) 54%, var(--toolbar-border));
|
|||
|
|
}
|
|||
|
|
.toolbar button:focus-visible {
|
|||
|
|
outline: 2px solid var(--frontend-stroke);
|
|||
|
|
outline-offset: 2px;
|
|||
|
|
}
|
|||
|
|
.toolbar #btn-motion[hidden] { display: none !important; }
|
|||
|
|
.toolbar #btn-motion { min-width: 4.4rem; }
|
|||
|
|
.toolbar #btn-motion:disabled { cursor: default; opacity: 0.58; }
|
|||
|
|
.toolbar .preset-wrap,
|
|||
|
|
.toolbar .export-wrap {
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
.toolbar-icon {
|
|||
|
|
display: inline-block;
|
|||
|
|
width: 0.95rem;
|
|||
|
|
height: 0.95rem;
|
|||
|
|
flex: 0 0 auto;
|
|||
|
|
background: currentColor;
|
|||
|
|
-webkit-mask-position: center;
|
|||
|
|
-webkit-mask-repeat: no-repeat;
|
|||
|
|
-webkit-mask-size: contain;
|
|||
|
|
mask-position: center;
|
|||
|
|
mask-repeat: no-repeat;
|
|||
|
|
mask-size: contain;
|
|||
|
|
}
|
|||
|
|
.toolbar-chevron {
|
|||
|
|
width: 0.65rem;
|
|||
|
|
height: 0.65rem;
|
|||
|
|
transition: transform 0.15s ease;
|
|||
|
|
}
|
|||
|
|
[aria-expanded="true"] > .toolbar-chevron { transform: rotate(180deg); }
|
|||
|
|
[data-theme="light"] #theme-icon {
|
|||
|
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'%3E%3Ccircle cx='10' cy='10' r='3.1'/%3E%3Cpath d='M10 2.2v1.4M10 16.4v1.4M2.2 10h1.4M16.4 10h1.4M4.5 4.5l1 1M14.5 14.5l1 1M15.5 4.5l-1 1M5.5 14.5l-1 1'/%3E%3C/g%3E%3C/svg%3E");
|
|||
|
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cg fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'%3E%3Ccircle cx='10' cy='10' r='3.1'/%3E%3Cpath d='M10 2.2v1.4M10 16.4v1.4M2.2 10h1.4M16.4 10h1.4M4.5 4.5l1 1M14.5 14.5l1 1M15.5 4.5l-1 1M5.5 14.5l-1 1'/%3E%3C/g%3E%3C/svg%3E");
|
|||
|
|
}
|
|||
|
|
[data-theme="dark"] #theme-icon {
|
|||
|
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M15.2 12.7A6.3 6.3 0 017.3 4.8a6.3 6.3 0 107.9 7.9Z' fill='none' stroke='black' stroke-width='1.7' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|||
|
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M15.2 12.7A6.3 6.3 0 017.3 4.8a6.3 6.3 0 107.9 7.9Z' fill='none' stroke='black' stroke-width='1.7' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|||
|
|
}
|
|||
|
|
#btn-present[aria-pressed="false"] #present-icon {
|
|||
|
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M7.2 3.2h-4v4M12.8 3.2h4v4M7.2 16.8h-4v-4M12.8 16.8h4v-4' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E");
|
|||
|
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M7.2 3.2h-4v4M12.8 3.2h4v4M7.2 16.8h-4v-4M12.8 16.8h4v-4' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E");
|
|||
|
|
}
|
|||
|
|
#btn-present[aria-pressed="true"] #present-icon {
|
|||
|
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M7.2 7.2h-4v-4M12.8 7.2h4v-4M7.2 12.8h-4v4M12.8 12.8h4v4' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E");
|
|||
|
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M7.2 7.2h-4v-4M12.8 7.2h4v-4M7.2 12.8h-4v4M12.8 12.8h4v4' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E");
|
|||
|
|
}
|
|||
|
|
.toolbar-chevron {
|
|||
|
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='m3 4.5 3 3 3-3' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|||
|
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='m3 4.5 3 3 3-3' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|||
|
|
}
|
|||
|
|
.toolbar #btn-preset { min-width: 5.4rem; }
|
|||
|
|
.preset-control-mark {
|
|||
|
|
width: 0.58rem;
|
|||
|
|
height: 0.58rem;
|
|||
|
|
flex: 0 0 auto;
|
|||
|
|
border: 1px solid var(--frontend-stroke);
|
|||
|
|
border-radius: 0.2rem;
|
|||
|
|
background: color-mix(in srgb, var(--frontend-stroke) 28%, transparent);
|
|||
|
|
transition: border-radius 0.15s, box-shadow 0.15s, transform 0.15s;
|
|||
|
|
}
|
|||
|
|
#btn-preset[data-preset-option="signal-flow"] .preset-control-mark {
|
|||
|
|
border-radius: 50%;
|
|||
|
|
box-shadow: 0 0 10px color-mix(in srgb, var(--frontend-stroke) 78%, transparent);
|
|||
|
|
}
|
|||
|
|
#btn-preset[data-preset-option="blueprint"] .preset-control-mark {
|
|||
|
|
border-radius: 0.06rem;
|
|||
|
|
background: transparent;
|
|||
|
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--frontend-stroke) 32%, transparent);
|
|||
|
|
transform: rotate(45deg) scale(0.84);
|
|||
|
|
}
|
|||
|
|
#btn-preset[data-preset-option="editorial"] .preset-control-mark {
|
|||
|
|
border-color: var(--arrow-emphasis);
|
|||
|
|
border-radius: 0;
|
|||
|
|
background: var(--arrow-emphasis);
|
|||
|
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--arrow-emphasis) 14%, transparent);
|
|||
|
|
transform: rotate(45deg) scale(0.72);
|
|||
|
|
}
|
|||
|
|
.toolbar .preset-menu {
|
|||
|
|
position: absolute;
|
|||
|
|
top: calc(100% + 0.375rem);
|
|||
|
|
right: 0;
|
|||
|
|
display: none;
|
|||
|
|
width: 18.5rem;
|
|||
|
|
max-width: calc(100vw - 2rem);
|
|||
|
|
padding: 0.45rem;
|
|||
|
|
border: 1px solid var(--toolbar-border);
|
|||
|
|
border-radius: 0.875rem;
|
|||
|
|
background: var(--toolbar-menu-bg);
|
|||
|
|
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.24);
|
|||
|
|
}
|
|||
|
|
.toolbar .preset-menu.open { display: block; }
|
|||
|
|
.toolbar .preset-menu.open,
|
|||
|
|
.toolbar .export-menu.open {
|
|||
|
|
animation: toolbar-menu-in 0.16s cubic-bezier(0.2, 0.8, 0.2, 1);
|
|||
|
|
transform-origin: top right;
|
|||
|
|
}
|
|||
|
|
@keyframes toolbar-menu-in {
|
|||
|
|
from { opacity: 0; transform: translateY(-0.25rem) scale(0.985); }
|
|||
|
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
|||
|
|
}
|
|||
|
|
.preset-menu-heading {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: baseline;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 1rem;
|
|||
|
|
padding: 0.42rem 0.6rem 0.55rem;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.56rem;
|
|||
|
|
letter-spacing: 0.13em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.preset-menu-heading span:last-child {
|
|||
|
|
font-size: 0.52rem;
|
|||
|
|
letter-spacing: 0.04em;
|
|||
|
|
text-transform: none;
|
|||
|
|
}
|
|||
|
|
.toolbar .preset-option {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 2rem minmax(0, 1fr) 1rem;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.68rem;
|
|||
|
|
width: 100%;
|
|||
|
|
min-height: 3.35rem;
|
|||
|
|
padding: 0.5rem 0.62rem;
|
|||
|
|
border: 0;
|
|||
|
|
border-radius: 0.5rem;
|
|||
|
|
background: transparent;
|
|||
|
|
box-shadow: none;
|
|||
|
|
backdrop-filter: none;
|
|||
|
|
text-align: left;
|
|||
|
|
}
|
|||
|
|
.toolbar .preset-option:hover,
|
|||
|
|
.toolbar .preset-option:focus-visible { background: var(--toolbar-hover); }
|
|||
|
|
.toolbar .preset-option[aria-checked="true"] {
|
|||
|
|
background: color-mix(in srgb, var(--frontend-stroke) 9%, transparent);
|
|||
|
|
border-color: color-mix(in srgb, var(--frontend-stroke) 36%, transparent);
|
|||
|
|
}
|
|||
|
|
.preset-option-swatch {
|
|||
|
|
position: relative;
|
|||
|
|
width: 2rem;
|
|||
|
|
height: 1.55rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
border: 1px solid #64748b;
|
|||
|
|
border-radius: 0.24rem;
|
|||
|
|
background: #0f172a;
|
|||
|
|
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.04);
|
|||
|
|
}
|
|||
|
|
.preset-option-swatch::before,
|
|||
|
|
.preset-option-swatch::after { position: absolute; content: ""; }
|
|||
|
|
.preset-option-swatch.classic::before {
|
|||
|
|
top: 0.42rem;
|
|||
|
|
right: 0.35rem;
|
|||
|
|
bottom: 0.42rem;
|
|||
|
|
left: 0.35rem;
|
|||
|
|
border: 1px solid #22d3ee;
|
|||
|
|
border-radius: 0.16rem;
|
|||
|
|
background: rgba(34, 211, 238, 0.14);
|
|||
|
|
}
|
|||
|
|
.preset-option-swatch.signal-flow {
|
|||
|
|
border-color: #29415f;
|
|||
|
|
background: linear-gradient(135deg, #07101e 10%, #10243c 100%);
|
|||
|
|
}
|
|||
|
|
.preset-option-swatch.signal-flow::before {
|
|||
|
|
top: 50%;
|
|||
|
|
right: 0.2rem;
|
|||
|
|
left: 0.2rem;
|
|||
|
|
height: 1px;
|
|||
|
|
background: linear-gradient(90deg, #67e8f9, #c4b5fd);
|
|||
|
|
box-shadow: 0 0 8px #67e8f9;
|
|||
|
|
}
|
|||
|
|
.preset-option-swatch.signal-flow::after {
|
|||
|
|
top: calc(50% - 0.13rem);
|
|||
|
|
right: 0.28rem;
|
|||
|
|
width: 0.26rem;
|
|||
|
|
height: 0.26rem;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: #67e8f9;
|
|||
|
|
box-shadow: 0 0 7px #67e8f9;
|
|||
|
|
}
|
|||
|
|
.preset-option-swatch.blueprint {
|
|||
|
|
border-color: #34799a;
|
|||
|
|
border-radius: 0.08rem;
|
|||
|
|
background-color: #0a2031;
|
|||
|
|
background-image:
|
|||
|
|
linear-gradient(rgba(102, 217, 239, 0.18) 1px, transparent 1px),
|
|||
|
|
linear-gradient(90deg, rgba(102, 217, 239, 0.18) 1px, transparent 1px);
|
|||
|
|
background-size: 0.42rem 0.42rem;
|
|||
|
|
}
|
|||
|
|
.preset-option-swatch.blueprint::before {
|
|||
|
|
top: 0.34rem;
|
|||
|
|
right: 0.38rem;
|
|||
|
|
bottom: 0.34rem;
|
|||
|
|
left: 0.38rem;
|
|||
|
|
border: 1px solid #66d9ef;
|
|||
|
|
}
|
|||
|
|
.preset-option-swatch.editorial {
|
|||
|
|
border-color: #b8aa94;
|
|||
|
|
border-radius: 0.08rem;
|
|||
|
|
background: repeating-linear-gradient(0deg, #f2eee5 0 0.34rem, #d8d0c2 0.36rem, #f2eee5 0.38rem);
|
|||
|
|
}
|
|||
|
|
.preset-option-swatch.editorial::before {
|
|||
|
|
top: 0;
|
|||
|
|
bottom: 0;
|
|||
|
|
left: 0.42rem;
|
|||
|
|
width: 1px;
|
|||
|
|
background: #c9502d;
|
|||
|
|
opacity: 0.72;
|
|||
|
|
}
|
|||
|
|
.preset-option-swatch.editorial::after {
|
|||
|
|
top: 0.52rem;
|
|||
|
|
right: 0.45rem;
|
|||
|
|
width: 0.38rem;
|
|||
|
|
height: 0.38rem;
|
|||
|
|
background: #c9502d;
|
|||
|
|
transform: rotate(45deg);
|
|||
|
|
}
|
|||
|
|
.preset-option-copy { min-width: 0; }
|
|||
|
|
.preset-option-copy strong,
|
|||
|
|
.preset-option-copy small { display: block; }
|
|||
|
|
.preset-option-copy strong {
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
font-size: 0.72rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
.preset-option-copy small {
|
|||
|
|
margin-top: 0.16rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.57rem;
|
|||
|
|
font-weight: 400;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
}
|
|||
|
|
.preset-option-check {
|
|||
|
|
display: inline-block;
|
|||
|
|
width: 0.9rem;
|
|||
|
|
height: 0.9rem;
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
background: currentColor;
|
|||
|
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m3 8.5 3 3 7-7' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|||
|
|
-webkit-mask-position: center;
|
|||
|
|
-webkit-mask-repeat: no-repeat;
|
|||
|
|
-webkit-mask-size: contain;
|
|||
|
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m3 8.5 3 3 7-7' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|||
|
|
mask-position: center;
|
|||
|
|
mask-repeat: no-repeat;
|
|||
|
|
mask-size: contain;
|
|||
|
|
opacity: 0;
|
|||
|
|
}
|
|||
|
|
.preset-option[aria-checked="true"] .preset-option-check { opacity: 1; }
|
|||
|
|
html[data-preset="editorial"] .toolbar button,
|
|||
|
|
html[data-preset="editorial"] .preset-menu,
|
|||
|
|
html[data-preset="editorial"] .export-menu,
|
|||
|
|
html[data-preset="editorial"] .overview-map,
|
|||
|
|
html[data-preset="editorial"] .semantic-lens,
|
|||
|
|
html[data-preset="editorial"] .route-probe,
|
|||
|
|
html[data-preset="editorial"] .focus-chip,
|
|||
|
|
html[data-preset="editorial"] .diagram-guide,
|
|||
|
|
html[data-preset="editorial"] .node-finder,
|
|||
|
|
html[data-preset="editorial"] .guided-views,
|
|||
|
|
html[data-preset="editorial"] .share-chapter-cue {
|
|||
|
|
border-radius: 0.3rem;
|
|||
|
|
}
|
|||
|
|
.motion-control-dot {
|
|||
|
|
width: 0.46rem;
|
|||
|
|
height: 0.46rem;
|
|||
|
|
flex: 0 0 auto;
|
|||
|
|
border: 1px solid var(--frontend-stroke);
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: var(--frontend-stroke);
|
|||
|
|
box-shadow: 0 0 9px color-mix(in srgb, var(--frontend-stroke) 72%, transparent);
|
|||
|
|
}
|
|||
|
|
#btn-motion[aria-pressed="false"] .motion-control-dot {
|
|||
|
|
background: transparent;
|
|||
|
|
box-shadow: none;
|
|||
|
|
}
|
|||
|
|
.toolbar .export-menu {
|
|||
|
|
position: absolute;
|
|||
|
|
top: calc(100% + 0.375rem);
|
|||
|
|
right: 0;
|
|||
|
|
background: var(--toolbar-menu-bg);
|
|||
|
|
border: 1px solid var(--toolbar-border);
|
|||
|
|
width: 19rem;
|
|||
|
|
max-width: calc(100vw - 2rem);
|
|||
|
|
max-height: calc(100vh - 5.5rem);
|
|||
|
|
overflow-y: auto;
|
|||
|
|
border-radius: 0.875rem;
|
|||
|
|
padding: 0.55rem;
|
|||
|
|
display: none;
|
|||
|
|
flex-direction: column;
|
|||
|
|
box-shadow: 0 18px 48px rgba(0,0,0,0.24);
|
|||
|
|
}
|
|||
|
|
.toolbar .export-menu.open { display: flex; }
|
|||
|
|
.export-menu-header {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
gap: 1rem;
|
|||
|
|
padding: 0.5rem 0.55rem 0.72rem;
|
|||
|
|
}
|
|||
|
|
.export-menu-header strong,
|
|||
|
|
.export-menu-header span { display: block; }
|
|||
|
|
.export-menu-header strong {
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
font-size: 0.78rem;
|
|||
|
|
font-weight: 650;
|
|||
|
|
letter-spacing: -0.015em;
|
|||
|
|
}
|
|||
|
|
.export-menu-header span {
|
|||
|
|
margin-top: 0.16rem;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.56rem;
|
|||
|
|
}
|
|||
|
|
.export-menu-header kbd {
|
|||
|
|
padding: 0.16rem 0.36rem;
|
|||
|
|
border: 1px solid var(--toolbar-border);
|
|||
|
|
border-radius: 0.3rem;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
background: color-mix(in srgb, var(--toolbar-border) 16%, transparent);
|
|||
|
|
font-family: inherit;
|
|||
|
|
font-size: 0.54rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
.export-menu-section {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: minmax(0, 1fr);
|
|||
|
|
gap: 0.18rem;
|
|||
|
|
}
|
|||
|
|
.export-menu-section + .export-menu-section {
|
|||
|
|
margin-top: 0.42rem;
|
|||
|
|
padding-top: 0.42rem;
|
|||
|
|
border-top: 1px solid color-mix(in srgb, var(--toolbar-border) 68%, transparent);
|
|||
|
|
}
|
|||
|
|
.export-menu-heading {
|
|||
|
|
grid-column: 1 / -1;
|
|||
|
|
display: block;
|
|||
|
|
padding: 0.24rem 0.55rem 0.2rem;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.54rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
letter-spacing: 0.12em;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
.toolbar .export-menu button {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 1.15rem minmax(0, 1fr);
|
|||
|
|
align-items: center;
|
|||
|
|
column-gap: 0.52rem;
|
|||
|
|
background: transparent;
|
|||
|
|
border: 1px solid transparent;
|
|||
|
|
box-shadow: none;
|
|||
|
|
backdrop-filter: none;
|
|||
|
|
width: 100%;
|
|||
|
|
min-height: 3rem;
|
|||
|
|
text-align: left;
|
|||
|
|
padding: 0.48rem 0.55rem;
|
|||
|
|
border-radius: 0.5rem;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.export-item-copy {
|
|||
|
|
display: block;
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
.export-item-copy strong,
|
|||
|
|
.export-item-copy small { display: block; }
|
|||
|
|
.export-item-copy strong {
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: inherit;
|
|||
|
|
font-size: 0.7rem;
|
|||
|
|
font-weight: 550;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
}
|
|||
|
|
.export-item-copy small {
|
|||
|
|
margin-top: 0.12rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.55rem;
|
|||
|
|
font-weight: 450;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
}
|
|||
|
|
.toolbar .export-menu button::before {
|
|||
|
|
width: 1rem;
|
|||
|
|
height: 1rem;
|
|||
|
|
background: currentColor;
|
|||
|
|
content: "";
|
|||
|
|
opacity: 0.72;
|
|||
|
|
-webkit-mask-position: center;
|
|||
|
|
-webkit-mask-repeat: no-repeat;
|
|||
|
|
-webkit-mask-size: contain;
|
|||
|
|
mask-position: center;
|
|||
|
|
mask-repeat: no-repeat;
|
|||
|
|
mask-size: contain;
|
|||
|
|
}
|
|||
|
|
.toolbar .export-menu button[data-format="share-card"]::before,
|
|||
|
|
.toolbar .export-menu button[data-action="route-share-card"]::before,
|
|||
|
|
.toolbar .export-menu button[data-action="reach-share-card"]::before {
|
|||
|
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='black' stroke-width='1.7' d='M3 4.5h14v11H3zM6 12l2.5-2.5 2 2 1.5-1.5 2 2'/%3E%3Ccircle cx='13.8' cy='7.3' r='1.2' fill='black'/%3E%3C/svg%3E");
|
|||
|
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='black' stroke-width='1.7' d='M3 4.5h14v11H3zM6 12l2.5-2.5 2 2 1.5-1.5 2 2'/%3E%3Ccircle cx='13.8' cy='7.3' r='1.2' fill='black'/%3E%3C/svg%3E");
|
|||
|
|
}
|
|||
|
|
.toolbar .export-menu button[data-action^="copy"]::before {
|
|||
|
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='6' y='6' width='10' height='10' rx='1.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M4 13H3.5A1.5 1.5 0 012 11.5v-8A1.5 1.5 0 013.5 2h8A1.5 1.5 0 0113 3.5V4' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E");
|
|||
|
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Crect x='6' y='6' width='10' height='10' rx='1.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M4 13H3.5A1.5 1.5 0 012 11.5v-8A1.5 1.5 0 013.5 2h8A1.5 1.5 0 0113 3.5V4' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E");
|
|||
|
|
}
|
|||
|
|
.toolbar .export-menu button[data-format="png"]::before,
|
|||
|
|
.toolbar .export-menu button[data-format="jpeg"]::before,
|
|||
|
|
.toolbar .export-menu button[data-format="webp"]::before {
|
|||
|
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M5 2.5h7l3 3v12H5zM12 2.5v3h3' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M7.5 14l2-2 1.5 1.5 1.5-1.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E");
|
|||
|
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M5 2.5h7l3 3v12H5zM12 2.5v3h3' fill='none' stroke='black' stroke-width='1.7'/%3E%3Cpath d='M7.5 14l2-2 1.5 1.5 1.5-1.5' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E");
|
|||
|
|
}
|
|||
|
|
.toolbar .export-menu button[data-format="svg"]::before,
|
|||
|
|
.toolbar .export-menu button[data-format="webm"]::before {
|
|||
|
|
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E");
|
|||
|
|
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M10 2.5l7 4v7l-7 4-7-4v-7zM3 6.5l7 4 7-4M10 10.5v7' fill='none' stroke='black' stroke-width='1.7'/%3E%3C/svg%3E");
|
|||
|
|
}
|
|||
|
|
.toolbar .export-menu button[data-format="share-card"],
|
|||
|
|
.toolbar .export-menu button[data-action="route-share-card"],
|
|||
|
|
.toolbar .export-menu button[data-action="reach-share-card"] {
|
|||
|
|
min-height: 3.35rem;
|
|||
|
|
color: var(--toolbar-text);
|
|||
|
|
background: color-mix(in srgb, var(--frontend-stroke) 8%, transparent);
|
|||
|
|
border-color: color-mix(in srgb, var(--frontend-stroke) 24%, var(--toolbar-border));
|
|||
|
|
}
|
|||
|
|
.toolbar .export-menu button[hidden] { display: none; }
|
|||
|
|
.toolbar .export-menu button:hover {
|
|||
|
|
background: var(--toolbar-hover);
|
|||
|
|
}
|
|||
|
|
.toolbar .export-menu button:focus-visible {
|
|||
|
|
background: var(--toolbar-hover);
|
|||
|
|
outline-offset: -2px;
|
|||
|
|
}
|
|||
|
|
.toolbar .export-menu button:disabled {
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
background: color-mix(in srgb, var(--toolbar-border) 18%, transparent);
|
|||
|
|
cursor: not-allowed;
|
|||
|
|
}
|
|||
|
|
.toolbar .export-menu button:disabled:hover {
|
|||
|
|
border-color: transparent;
|
|||
|
|
}
|
|||
|
|
.toolbar .export-menu .hint {
|
|||
|
|
color: var(--text-faint);
|
|||
|
|
font-size: 0.55rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Toast — brief feedback for actions like "copied" */
|
|||
|
|
.archify-toast {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 1rem;
|
|||
|
|
left: 50%;
|
|||
|
|
transform: translateX(-50%) translateY(-8px);
|
|||
|
|
background: var(--toolbar-menu-bg);
|
|||
|
|
color: var(--text);
|
|||
|
|
border: 1px solid var(--toolbar-border);
|
|||
|
|
padding: 0.5rem 1rem;
|
|||
|
|
border-radius: 0.5rem;
|
|||
|
|
font-family: inherit;
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
opacity: 0;
|
|||
|
|
transition: opacity 0.2s, transform 0.2s;
|
|||
|
|
z-index: 200;
|
|||
|
|
pointer-events: none;
|
|||
|
|
box-shadow: 0 4px 20px rgba(0,0,0,0.25);
|
|||
|
|
}
|
|||
|
|
.archify-toast.show {
|
|||
|
|
opacity: 1;
|
|||
|
|
transform: translateX(-50%) translateY(0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 720px) {
|
|||
|
|
body { padding: 1rem; }
|
|||
|
|
.toolbar .preset-menu,
|
|||
|
|
.toolbar .export-menu {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 4.5rem;
|
|||
|
|
right: 1rem;
|
|||
|
|
left: 1rem;
|
|||
|
|
width: auto;
|
|||
|
|
max-width: none;
|
|||
|
|
}
|
|||
|
|
.toolbar {
|
|||
|
|
position: relative;
|
|||
|
|
justify-content: flex-end;
|
|||
|
|
width: max-content;
|
|||
|
|
max-width: 100%;
|
|||
|
|
margin-left: auto;
|
|||
|
|
margin-bottom: 1rem;
|
|||
|
|
}
|
|||
|
|
.header { margin-bottom: 1.25rem; padding-right: 0; }
|
|||
|
|
html[data-preset="signal-flow"] .header,
|
|||
|
|
html[data-preset="blueprint"] .header,
|
|||
|
|
html[data-preset="editorial"] .header { padding-right: 0; }
|
|||
|
|
.header-row {
|
|||
|
|
gap: 0.75rem;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
}
|
|||
|
|
html[data-preset="signal-flow"] .header-row,
|
|||
|
|
html[data-preset="blueprint"] .header-row,
|
|||
|
|
html[data-preset="editorial"] .header-row { flex-wrap: wrap; }
|
|||
|
|
html[data-preset="signal-flow"] .header-row::after {
|
|||
|
|
margin-left: 0;
|
|||
|
|
order: 3;
|
|||
|
|
}
|
|||
|
|
html[data-preset="blueprint"] .header-row::after {
|
|||
|
|
margin-left: 0;
|
|||
|
|
order: 3;
|
|||
|
|
}
|
|||
|
|
html[data-preset="editorial"] .header-row::after {
|
|||
|
|
margin-left: 0;
|
|||
|
|
order: 3;
|
|||
|
|
}
|
|||
|
|
h1 {
|
|||
|
|
font-size: 1.25rem;
|
|||
|
|
line-height: 1.25;
|
|||
|
|
}
|
|||
|
|
.subtitle {
|
|||
|
|
margin-left: 0;
|
|||
|
|
font-size: 0.8125rem;
|
|||
|
|
line-height: 1.55;
|
|||
|
|
}
|
|||
|
|
.diagram-container {
|
|||
|
|
padding: 0.75rem;
|
|||
|
|
border-radius: 0.75rem;
|
|||
|
|
}
|
|||
|
|
html:not([data-embed="true"]) .diagram-container {
|
|||
|
|
padding: 0.75rem 0.75rem 4.25rem;
|
|||
|
|
}
|
|||
|
|
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] {
|
|||
|
|
overflow-x: auto;
|
|||
|
|
overflow-y: hidden;
|
|||
|
|
overscroll-behavior-x: contain;
|
|||
|
|
scrollbar-width: thin;
|
|||
|
|
scroll-behavior: smooth;
|
|||
|
|
}
|
|||
|
|
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] > svg {
|
|||
|
|
min-width: 720px;
|
|||
|
|
}
|
|||
|
|
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] .diagram-nav,
|
|||
|
|
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] .overview-map,
|
|||
|
|
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] .focus-chip,
|
|||
|
|
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] .route-probe,
|
|||
|
|
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] .semantic-lens,
|
|||
|
|
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] .diagram-guide,
|
|||
|
|
html:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] .node-finder {
|
|||
|
|
transform: translateX(var(--archify-scroll-x, 0px));
|
|||
|
|
}
|
|||
|
|
.diagram-nav { right: 0.5rem; bottom: 0.5rem; }
|
|||
|
|
.diagram-nav button {
|
|||
|
|
min-width: 2.75rem;
|
|||
|
|
height: 2.75rem;
|
|||
|
|
}
|
|||
|
|
.diagram-nav [data-view="reset"] { min-width: 3.75rem; }
|
|||
|
|
.diagram-nav [data-view="reset"][data-detail-visible] { min-width: 4.35rem; }
|
|||
|
|
.overview-map {
|
|||
|
|
right: 0.5rem;
|
|||
|
|
bottom: 3.7rem;
|
|||
|
|
width: min(10.25rem, calc(100% - 1rem));
|
|||
|
|
}
|
|||
|
|
.overview-map[data-docked="true"] {
|
|||
|
|
right: var(--archify-radar-right, 0.5rem);
|
|||
|
|
top: var(--archify-radar-top, 0.5rem);
|
|||
|
|
bottom: auto;
|
|||
|
|
transform: none !important;
|
|||
|
|
}
|
|||
|
|
.overview-map[data-docked="true"][data-dock-side="left"] {
|
|||
|
|
right: auto;
|
|||
|
|
left: var(--archify-radar-left, 0.5rem);
|
|||
|
|
}
|
|||
|
|
.overview-map-surface { height: 4rem; }
|
|||
|
|
.overview-map-feedback { right: 0.5rem; bottom: 3.7rem; }
|
|||
|
|
.semantic-lens {
|
|||
|
|
right: 0.5rem;
|
|||
|
|
bottom: 3.7rem;
|
|||
|
|
width: calc(100% - 1rem);
|
|||
|
|
max-height: calc(100% - 4.2rem);
|
|||
|
|
}
|
|||
|
|
.semantic-lens[data-dock-side] { left: auto; right: 0.5rem; }
|
|||
|
|
.semantic-lens-kinds { max-height: min(12rem, 34vh); }
|
|||
|
|
.focus-chip {
|
|||
|
|
left: 0.5rem;
|
|||
|
|
top: 0.5rem;
|
|||
|
|
width: calc(100% - 1rem);
|
|||
|
|
max-width: none;
|
|||
|
|
}
|
|||
|
|
.relationship-lens-list { max-height: min(12rem, 38vh); }
|
|||
|
|
.relationship-lens-actions #btn-focus-relations { display: block; color: var(--database-stroke); }
|
|||
|
|
.focus-chip:not([data-relations-expanded="true"]) .relationship-lens-head { border-bottom-color: transparent; }
|
|||
|
|
.focus-chip:not([data-relations-expanded="true"]) .relationship-lens-list { display: none; }
|
|||
|
|
/* While a mobile reader previews one relationship, turn the full Lens
|
|||
|
|
into a compact peek card. This keeps the exact source and target on
|
|||
|
|
canvas instead of covering them with a long, scrollable list. */
|
|||
|
|
.focus-chip[data-relationship-previewing="true"] .relationship-lens-list {
|
|||
|
|
max-height: none;
|
|||
|
|
padding: 0.35rem;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
.focus-chip[data-relationship-previewing="true"] .relationship-lens-group { margin: 0; }
|
|||
|
|
.focus-chip[data-relationship-previewing="true"] .relationship-lens-group-title,
|
|||
|
|
.focus-chip[data-relationship-previewing="true"] .relationship-lens-row:not([data-preview-active="true"]) {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
.node-finder {
|
|||
|
|
top: 0.5rem;
|
|||
|
|
left: 0.5rem;
|
|||
|
|
width: min(22rem, calc(100% - 1rem));
|
|||
|
|
max-height: calc(100% - 1rem);
|
|||
|
|
}
|
|||
|
|
.diagram-guide {
|
|||
|
|
top: 0.5rem;
|
|||
|
|
right: 0.5rem;
|
|||
|
|
left: 0.5rem;
|
|||
|
|
width: auto;
|
|||
|
|
max-height: calc(100% - 1rem);
|
|||
|
|
}
|
|||
|
|
.diagram-guide-head { padding: 0.62rem 0.7rem 0.52rem; }
|
|||
|
|
.diagram-guide-stats { padding: 0.42rem 0.7rem; }
|
|||
|
|
.diagram-guide-actions {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
gap: 0.28rem;
|
|||
|
|
padding: 0.42rem;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-action {
|
|||
|
|
min-height: 3.1rem;
|
|||
|
|
padding: 0.42rem 0.48rem;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-action small {
|
|||
|
|
display: block;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-index {
|
|||
|
|
width: 1.5rem;
|
|||
|
|
height: 1.5rem;
|
|||
|
|
}
|
|||
|
|
.diagram-guide-shortcuts { padding: 0.46rem 0.7rem 0.52rem; }
|
|||
|
|
.diagram-guide-action[data-guide-action="present"] { grid-column: auto; }
|
|||
|
|
.route-probe[data-guide-open="true"] {
|
|||
|
|
visibility: hidden;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
.node-finder-results { max-height: min(16rem, 38vh); }
|
|||
|
|
.route-probe {
|
|||
|
|
left: 0.5rem;
|
|||
|
|
bottom: 3.7rem;
|
|||
|
|
width: calc(100% - 1rem);
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
.route-probe[data-route-dock="top"] {
|
|||
|
|
top: 0.5rem;
|
|||
|
|
bottom: auto;
|
|||
|
|
}
|
|||
|
|
.route-probe[data-finder-open="true"] {
|
|||
|
|
visibility: hidden;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
.route-probe-head { padding: 0.45rem 0.48rem 0.38rem; }
|
|||
|
|
.route-probe-path { padding: 0.35rem 0.48rem 0.2rem; }
|
|||
|
|
.route-probe-node { min-height: 2.75rem !important; }
|
|||
|
|
.route-journey-controls {
|
|||
|
|
grid-template-columns: 2.75rem minmax(0, 1fr) 2.75rem minmax(0, 1fr);
|
|||
|
|
padding: 0.08rem 0.48rem 0.32rem;
|
|||
|
|
}
|
|||
|
|
.route-journey-controls button { min-height: 2.75rem; }
|
|||
|
|
.route-probe-status { padding: 0.05rem 0.48rem 0.38rem; }
|
|||
|
|
.route-probe-node { max-width: 7.5rem; }
|
|||
|
|
.guided-views {
|
|||
|
|
grid-template-columns: 2.75rem minmax(0, 1fr) 2.75rem;
|
|||
|
|
gap: 0.2rem;
|
|||
|
|
padding: 0.25rem;
|
|||
|
|
}
|
|||
|
|
.guided-view-actions {
|
|||
|
|
grid-column: 1 / -1;
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|||
|
|
}
|
|||
|
|
.guided-views > #guided-view-prev,
|
|||
|
|
.guided-views > #guided-view-next {
|
|||
|
|
align-self: center;
|
|||
|
|
height: 2.75rem;
|
|||
|
|
}
|
|||
|
|
.guided-view-play,
|
|||
|
|
.guided-view-all,
|
|||
|
|
.guided-view-beat-link { min-height: 2.75rem; }
|
|||
|
|
.guided-view-copy small { white-space: normal; }
|
|||
|
|
.guided-story-caption { padding: 0.32rem 0.45rem; }
|
|||
|
|
.guided-story-caption-index { min-width: 2.9rem; padding-right: 0.42rem; }
|
|||
|
|
.guided-story-caption strong,
|
|||
|
|
.guided-story-caption small { white-space: normal; }
|
|||
|
|
.guided-story-caption-next { display: none; }
|
|||
|
|
.guided-view-trail { min-height: 2rem; }
|
|||
|
|
.guided-view-trail .guided-view-stop {
|
|||
|
|
min-height: 2rem;
|
|||
|
|
padding: 0.22rem 0.42rem 0.22rem 0.28rem;
|
|||
|
|
}
|
|||
|
|
.guided-view-handoff { max-width: min(9rem, 32vw); }
|
|||
|
|
.guided-view-index { padding-top: 0.35rem; }
|
|||
|
|
.guided-view-chapters {
|
|||
|
|
gap: 0.35rem;
|
|||
|
|
scroll-padding-inline: 0.2rem;
|
|||
|
|
}
|
|||
|
|
.guided-view-chapters li {
|
|||
|
|
flex: 0 0 min(14rem, 78vw);
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
.guided-view-chapter { min-height: 2.75rem; }
|
|||
|
|
.guided-views[data-active-view="all"] {
|
|||
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|||
|
|
gap: 0.2rem;
|
|||
|
|
padding: 0.25rem;
|
|||
|
|
}
|
|||
|
|
.guided-views[data-active-view="all"] .guided-view-copy {
|
|||
|
|
grid-column: 1;
|
|||
|
|
grid-row: 1;
|
|||
|
|
}
|
|||
|
|
.guided-views[data-active-view="all"] .guided-view-actions {
|
|||
|
|
grid-column: 2;
|
|||
|
|
grid-row: 1;
|
|||
|
|
}
|
|||
|
|
.guided-views[data-active-view="all"] .guided-view-play {
|
|||
|
|
min-width: 6rem !important;
|
|||
|
|
min-height: 2.75rem;
|
|||
|
|
}
|
|||
|
|
.guided-views[data-active-view="all"] .guided-view-index {
|
|||
|
|
grid-column: 1 / -1;
|
|||
|
|
grid-row: 2;
|
|||
|
|
padding: 0.3rem 0.08rem 0;
|
|||
|
|
border-top: 1px solid color-mix(in srgb, var(--toolbar-border) 72%, transparent);
|
|||
|
|
border-left: 0;
|
|||
|
|
}
|
|||
|
|
html[data-embed="true"][data-share-playback="true"] .share-chapter-cue:not([hidden]),
|
|||
|
|
html[data-embed="true"][data-share-moment="true"] .share-chapter-cue:not([hidden]) {
|
|||
|
|
top: 0.5rem;
|
|||
|
|
grid-template-columns: auto minmax(0, 1fr);
|
|||
|
|
gap: 0.28rem 0.55rem;
|
|||
|
|
width: calc(100% - 1rem);
|
|||
|
|
min-height: 0;
|
|||
|
|
padding: 0.45rem 0.55rem 0.55rem;
|
|||
|
|
}
|
|||
|
|
html[data-embed="true"][data-share-playback="true"] .diagram-container {
|
|||
|
|
padding-top: 4.25rem;
|
|||
|
|
}
|
|||
|
|
html[data-embed="true"][data-share-moment="true"] .diagram-container {
|
|||
|
|
padding-top: 4.25rem;
|
|||
|
|
}
|
|||
|
|
.share-chapter-index {
|
|||
|
|
min-width: 4.4rem;
|
|||
|
|
padding-right: 0.5rem;
|
|||
|
|
}
|
|||
|
|
.share-chapter-copy small { font-size: 0.46rem; }
|
|||
|
|
.share-chapter-route {
|
|||
|
|
grid-column: 1 / -1;
|
|||
|
|
font-size: 0.5rem;
|
|||
|
|
text-align: left;
|
|||
|
|
}
|
|||
|
|
.cards {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
margin-top: 1rem;
|
|||
|
|
}
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) body { padding: 0.5rem; }
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .container {
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
height: calc(100dvh - 1rem);
|
|||
|
|
}
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .toolbar {
|
|||
|
|
position: fixed;
|
|||
|
|
top: 0.5rem;
|
|||
|
|
right: 0.5rem;
|
|||
|
|
margin: 0;
|
|||
|
|
gap: 0.25rem;
|
|||
|
|
}
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .toolbar button {
|
|||
|
|
min-height: 2.75rem;
|
|||
|
|
padding: 0.42rem 0.58rem;
|
|||
|
|
}
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .header {
|
|||
|
|
min-height: 2.25rem;
|
|||
|
|
padding: 3.5rem 0 0;
|
|||
|
|
}
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .header-row {
|
|||
|
|
min-height: 2.25rem;
|
|||
|
|
margin: 0;
|
|||
|
|
align-items: center;
|
|||
|
|
flex-wrap: nowrap;
|
|||
|
|
}
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .header-row::after,
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .pulse-dot,
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .subtitle { display: none; }
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) h1 {
|
|||
|
|
flex: 1 1 auto;
|
|||
|
|
min-width: 0;
|
|||
|
|
max-width: 100%;
|
|||
|
|
overflow: hidden;
|
|||
|
|
font-size: 0.875rem;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
}
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .guided-views {
|
|||
|
|
padding: 0.2rem;
|
|||
|
|
}
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .guided-view-copy small { display: none; }
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .guided-story-caption small { display: block; }
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .diagram-container { padding: 0.5rem 0.5rem 3.75rem; }
|
|||
|
|
html[data-present="true"]:not([data-embed="true"]) .diagram-container[data-wide-diagram="true"] > svg {
|
|||
|
|
width: 720px;
|
|||
|
|
height: auto;
|
|||
|
|
min-width: 720px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 420px) {
|
|||
|
|
.export-menu-section { grid-template-columns: 1fr; }
|
|||
|
|
.export-menu-heading,
|
|||
|
|
.toolbar .export-menu button[data-format="share-card"],
|
|||
|
|
.toolbar .export-menu button[data-action="route-share-card"],
|
|||
|
|
.toolbar .export-menu button[data-action="reach-share-card"] { grid-column: 1; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (prefers-reduced-motion: reduce) {
|
|||
|
|
.guided-story-caption { animation: none !important; }
|
|||
|
|
.toolbar .preset-menu.open,
|
|||
|
|
.toolbar .export-menu.open { animation: none; }
|
|||
|
|
.toolbar-chevron { transition: none; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 360px) {
|
|||
|
|
.toolbar { gap: 0.25rem; }
|
|||
|
|
.toolbar button { padding-right: 0.58rem; padding-left: 0.58rem; }
|
|||
|
|
.toolbar #btn-motion { min-width: 4.4rem; padding-right: 0.58rem; padding-left: 0.58rem; }
|
|||
|
|
#theme-label, #preset-label, #present-label { display: none; }
|
|||
|
|
.guided-view-handoff { max-width: 6.5rem; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ==========================================================
|
|||
|
|
SVG SEMANTIC CLASSES — use these instead of inline fill/stroke
|
|||
|
|
========================================================== */
|
|||
|
|
.c-grid { stroke: var(--grid); fill: none; }
|
|||
|
|
.c-mask { fill: var(--mask); stroke: none; }
|
|||
|
|
|
|||
|
|
.c-frontend { fill: var(--frontend-fill); stroke: var(--frontend-stroke); }
|
|||
|
|
.c-backend { fill: var(--backend-fill); stroke: var(--backend-stroke); }
|
|||
|
|
.c-database { fill: var(--database-fill); stroke: var(--database-stroke); }
|
|||
|
|
.c-cloud { fill: var(--cloud-fill); stroke: var(--cloud-stroke); }
|
|||
|
|
.c-security { fill: var(--security-fill); stroke: var(--security-stroke); }
|
|||
|
|
.c-messagebus { fill: var(--messagebus-fill); stroke: var(--messagebus-stroke); }
|
|||
|
|
.c-external { fill: var(--external-fill); stroke: var(--external-stroke); }
|
|||
|
|
|
|||
|
|
/* Text helpers */
|
|||
|
|
.t-primary { fill: var(--text); }
|
|||
|
|
.t-muted { fill: var(--text-muted); }
|
|||
|
|
.t-dim { fill: var(--text-dim); }
|
|||
|
|
.t-frontend { fill: var(--frontend-stroke); }
|
|||
|
|
.t-backend { fill: var(--backend-stroke); }
|
|||
|
|
.t-database { fill: var(--database-stroke); }
|
|||
|
|
.t-cloud { fill: var(--cloud-stroke); }
|
|||
|
|
.t-security { fill: var(--security-stroke); }
|
|||
|
|
.t-messagebus { fill: var(--messagebus-stroke); }
|
|||
|
|
.t-external { fill: var(--external-stroke); }
|
|||
|
|
|
|||
|
|
/* Semantic Sigils are small authored role stamps, not icons from a brand
|
|||
|
|
pack and not viewer controls. Prefix selectors with svg so the canonical
|
|||
|
|
export's SVG-only stylesheet collector keeps them automatically. */
|
|||
|
|
svg .semantic-sigil {
|
|||
|
|
fill: none;
|
|||
|
|
stroke: currentColor;
|
|||
|
|
stroke-width: 1.35;
|
|||
|
|
stroke-linecap: round;
|
|||
|
|
stroke-linejoin: round;
|
|||
|
|
opacity: 0.76;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
svg .semantic-sigil > * { vector-effect: non-scaling-stroke; }
|
|||
|
|
svg .semantic-sigil .sigil-fill { fill: currentColor; stroke: none; }
|
|||
|
|
svg .s-frontend { color: var(--frontend-stroke); }
|
|||
|
|
svg .s-backend { color: var(--backend-stroke); }
|
|||
|
|
svg .s-database { color: var(--database-stroke); }
|
|||
|
|
svg .s-cloud { color: var(--cloud-stroke); }
|
|||
|
|
svg .s-security { color: var(--security-stroke); }
|
|||
|
|
svg .s-messagebus { color: var(--messagebus-stroke); }
|
|||
|
|
svg .s-external { color: var(--external-stroke); }
|
|||
|
|
|
|||
|
|
/* Brand Marks are authored identity badges. Their color is contained
|
|||
|
|
inside one neutral plate so vendor color never replaces Archify's
|
|||
|
|
semantic node and relationship vocabulary. */
|
|||
|
|
svg .brand-mark { pointer-events: none; }
|
|||
|
|
svg .brand-mark-badge { fill: #fff; stroke: none; }
|
|||
|
|
svg .brand-mark-frame {
|
|||
|
|
fill: none;
|
|||
|
|
stroke: #cbd5e1;
|
|||
|
|
stroke-width: 0.8;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
}
|
|||
|
|
svg .brand-mark-fallback {
|
|||
|
|
fill: none;
|
|||
|
|
stroke: #475569;
|
|||
|
|
stroke-width: 1.15;
|
|||
|
|
stroke-linecap: round;
|
|||
|
|
stroke-linejoin: round;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"] .brand-mark-badge,
|
|||
|
|
svg[data-preset="blueprint"] .brand-mark-frame { rx: 1px; }
|
|||
|
|
|
|||
|
|
/* Reading Depth is a viewer-only level-of-detail layer. MAP preserves the
|
|||
|
|
authored structure and primary labels, READ restores relationship labels
|
|||
|
|
and node context, and FULL restores tags and fine annotations. Semantic
|
|||
|
|
intent can reveal the relevant detail without moving any geometry. */
|
|||
|
|
svg [data-detail] { transition: opacity 160ms ease; }
|
|||
|
|
svg [data-detail-anchor] {
|
|||
|
|
transform-box: fill-box;
|
|||
|
|
transform-origin: center;
|
|||
|
|
transition: transform 160ms ease;
|
|||
|
|
}
|
|||
|
|
.diagram-container[data-detail-level="map"] svg [data-detail="context"],
|
|||
|
|
.diagram-container[data-detail-level="map"] svg [data-detail="fine"],
|
|||
|
|
.diagram-container[data-detail-level="read"] svg [data-detail="fine"] {
|
|||
|
|
opacity: 0;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
.diagram-container[data-detail-level="map"] svg [data-detail-anchor] {
|
|||
|
|
transform: translateY(8px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Focus, lens, preview, route, and story states are stronger reader intent than
|
|||
|
|
the global zoom level. Reveal only their exact semantic matches. */
|
|||
|
|
svg[data-focus-active] [data-focus-match][data-detail],
|
|||
|
|
svg[data-focus-active] [data-focus-match] [data-detail],
|
|||
|
|
svg[data-reach-active] [data-reach-match][data-detail],
|
|||
|
|
svg[data-reach-active] [data-reach-match] [data-detail],
|
|||
|
|
svg[data-lens-active] [data-lens-match][data-detail],
|
|||
|
|
svg[data-lens-active] [data-lens-match] [data-detail],
|
|||
|
|
svg[data-legend-preview-active] [data-legend-preview-match][data-detail],
|
|||
|
|
svg[data-legend-preview-active] [data-legend-preview-match] [data-detail],
|
|||
|
|
svg[data-intent-trace-active] [data-intent-trace-match][data-detail],
|
|||
|
|
svg[data-intent-trace-active] [data-intent-trace-match] [data-detail],
|
|||
|
|
svg[data-route-active] [data-route-match][data-detail],
|
|||
|
|
svg[data-route-active] [data-route-match] [data-detail],
|
|||
|
|
svg[data-story-active] [data-story-step][data-detail],
|
|||
|
|
svg[data-story-active] [data-story-step] [data-detail],
|
|||
|
|
svg[data-relationship-preview-active] [data-relationship-preview][data-detail],
|
|||
|
|
svg[data-relationship-preview-active] [data-relationship-preview] [data-detail],
|
|||
|
|
svg [data-node-id]:hover [data-detail],
|
|||
|
|
svg [data-node-id]:focus-visible [data-detail] {
|
|||
|
|
opacity: 1;
|
|||
|
|
pointer-events: auto;
|
|||
|
|
}
|
|||
|
|
svg[data-focus-active] [data-focus-match] [data-detail-anchor],
|
|||
|
|
svg[data-reach-active] [data-reach-match] [data-detail-anchor],
|
|||
|
|
svg[data-lens-active] [data-lens-match] [data-detail-anchor],
|
|||
|
|
svg[data-legend-preview-active] [data-legend-preview-match] [data-detail-anchor],
|
|||
|
|
svg[data-intent-trace-active] [data-intent-trace-match] [data-detail-anchor],
|
|||
|
|
svg[data-route-active] [data-route-match] [data-detail-anchor],
|
|||
|
|
svg[data-story-active] [data-story-step] [data-detail-anchor],
|
|||
|
|
svg [data-node-id]:hover [data-detail-anchor],
|
|||
|
|
svg [data-node-id]:focus-visible [data-detail-anchor] { transform: none; }
|
|||
|
|
|
|||
|
|
/* Arrows */
|
|||
|
|
.a-default { stroke: var(--arrow); fill: none; }
|
|||
|
|
.a-emphasis { stroke: var(--arrow-emphasis); fill: none; }
|
|||
|
|
.a-security { stroke: var(--security-stroke); fill: none; stroke-dasharray: 5,5; }
|
|||
|
|
.a-dashed { stroke: var(--database-stroke); fill: none; stroke-dasharray: 4,4; }
|
|||
|
|
|
|||
|
|
/* Arrowhead markers reference these */
|
|||
|
|
.m-default { fill: var(--arrow); }
|
|||
|
|
.m-emphasis { fill: var(--arrow-emphasis); }
|
|||
|
|
.m-security { fill: var(--security-stroke); }
|
|||
|
|
.m-dashed { fill: var(--database-stroke); }
|
|||
|
|
|
|||
|
|
/* Stable semantic exploration hooks emitted by every renderer. */
|
|||
|
|
svg [data-node-id] {
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: opacity 0.18s ease, filter 0.18s ease;
|
|||
|
|
}
|
|||
|
|
svg [data-edge-from] { transition: opacity 0.18s ease, filter 0.18s ease; }
|
|||
|
|
svg [data-node-id]:hover,
|
|||
|
|
svg [data-node-id]:focus-visible {
|
|||
|
|
filter: drop-shadow(0 0 7px var(--frontend-stroke));
|
|||
|
|
outline: none;
|
|||
|
|
}
|
|||
|
|
/* Verified Source Beacons are installed at runtime only for nodes whose
|
|||
|
|
repository evidence passed the local revision/blob/line checks. They
|
|||
|
|
live inside the owning node so every focus, route, story, and guided
|
|||
|
|
view keeps one coherent opacity state. Export serialization removes
|
|||
|
|
them before producing the canonical SVG or any derived visual. */
|
|||
|
|
.source-evidence-beacon {
|
|||
|
|
pointer-events: none;
|
|||
|
|
opacity: 0.76;
|
|||
|
|
transition: opacity 160ms ease, filter 160ms ease;
|
|||
|
|
}
|
|||
|
|
.source-evidence-beacon rect {
|
|||
|
|
fill: color-mix(in srgb, var(--mask) 92%, var(--backend-stroke));
|
|||
|
|
stroke: color-mix(in srgb, var(--backend-stroke) 82%, var(--panel-border));
|
|||
|
|
stroke-width: 1;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
}
|
|||
|
|
.source-evidence-beacon text {
|
|||
|
|
fill: var(--backend-stroke);
|
|||
|
|
font-size: 5.5px;
|
|||
|
|
font-weight: 800;
|
|||
|
|
letter-spacing: 0.06em;
|
|||
|
|
text-anchor: middle;
|
|||
|
|
text-transform: uppercase;
|
|||
|
|
}
|
|||
|
|
svg [data-node-id]:hover .source-evidence-beacon,
|
|||
|
|
svg [data-node-id]:focus-visible .source-evidence-beacon,
|
|||
|
|
svg [data-node-id][data-focus-selected] .source-evidence-beacon {
|
|||
|
|
opacity: 1;
|
|||
|
|
filter: drop-shadow(0 0 4px color-mix(in srgb, var(--backend-stroke) 72%, transparent));
|
|||
|
|
}
|
|||
|
|
svg[data-preset="signal-flow"] .source-evidence-beacon { opacity: 0.9; }
|
|||
|
|
svg[data-preset="blueprint"] .source-evidence-beacon rect {
|
|||
|
|
rx: 0.8px;
|
|||
|
|
filter: none;
|
|||
|
|
}
|
|||
|
|
svg [data-legend-kind][role="button"] { cursor: pointer; }
|
|||
|
|
svg [data-legend-kind]:focus { outline: none; }
|
|||
|
|
svg [data-legend-hit] {
|
|||
|
|
fill: transparent;
|
|||
|
|
stroke: transparent;
|
|||
|
|
stroke-width: 1.4;
|
|||
|
|
pointer-events: all;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
transition: stroke 150ms ease, stroke-width 150ms ease, filter 150ms ease;
|
|||
|
|
}
|
|||
|
|
svg [data-legend-count-badge] { pointer-events: none; }
|
|||
|
|
svg [data-legend-count-badge] rect {
|
|||
|
|
fill: var(--mask);
|
|||
|
|
stroke: var(--panel-border);
|
|||
|
|
stroke-width: 1;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
}
|
|||
|
|
svg [data-legend-count-badge] text {
|
|||
|
|
fill: var(--text-muted);
|
|||
|
|
font-size: 8px;
|
|||
|
|
font-weight: 700;
|
|||
|
|
text-anchor: middle;
|
|||
|
|
}
|
|||
|
|
svg [data-legend-kind][role="button"]:hover [data-legend-hit] {
|
|||
|
|
stroke: color-mix(in srgb, var(--database-stroke) 58%, transparent);
|
|||
|
|
}
|
|||
|
|
svg [data-legend-kind][role="button"]:focus-visible [data-legend-hit] {
|
|||
|
|
stroke: var(--frontend-stroke);
|
|||
|
|
stroke-width: 1.8;
|
|||
|
|
filter: drop-shadow(0 0 3px var(--frontend-stroke));
|
|||
|
|
}
|
|||
|
|
svg [data-legend-kind][data-legend-selected] [data-legend-hit] {
|
|||
|
|
stroke: var(--database-stroke);
|
|||
|
|
stroke-width: 2.2;
|
|||
|
|
stroke-dasharray: 3 2;
|
|||
|
|
}
|
|||
|
|
svg [data-legend-kind][data-legend-selected] [data-legend-count-badge] rect {
|
|||
|
|
stroke: var(--database-stroke);
|
|||
|
|
stroke-width: 1.8;
|
|||
|
|
}
|
|||
|
|
svg [data-legend-kind][data-legend-zero] { opacity: 0.44; }
|
|||
|
|
svg[data-preset="signal-flow"] [data-legend-kind][data-legend-selected] [data-legend-hit] {
|
|||
|
|
filter: drop-shadow(0 0 4px var(--database-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"] [data-legend-hit],
|
|||
|
|
svg[data-preset="blueprint"] [data-legend-count-badge] rect { rx: 0; filter: none; }
|
|||
|
|
svg[data-legend-preview-active] [data-node-id],
|
|||
|
|
svg[data-legend-preview-active] [data-edge-from] { opacity: 0.11; }
|
|||
|
|
svg[data-legend-preview-active] [data-legend-preview-match] { opacity: 1; }
|
|||
|
|
svg[data-legend-preview-active] [data-legend-preview-peer] { opacity: 0.62; }
|
|||
|
|
svg[data-legend-preview-active] [data-legend-preview-selected] {
|
|||
|
|
filter: drop-shadow(0 0 8px var(--database-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-lens-active] [data-node-id],
|
|||
|
|
svg[data-lens-active] [data-edge-from] { opacity: 0.11; }
|
|||
|
|
svg[data-lens-active] [data-lens-match] { opacity: 1; }
|
|||
|
|
svg[data-lens-active] [data-lens-peer] { opacity: 0.62; }
|
|||
|
|
svg[data-lens-active] [data-lens-selected] {
|
|||
|
|
filter: drop-shadow(0 0 10px var(--database-stroke));
|
|||
|
|
}
|
|||
|
|
.semantic-lens-overlay { pointer-events: none; }
|
|||
|
|
.semantic-lens-flow {
|
|||
|
|
fill: none;
|
|||
|
|
stroke-width: 3.05;
|
|||
|
|
stroke-linecap: round;
|
|||
|
|
stroke-dasharray: 0.075 0.925;
|
|||
|
|
stroke-dashoffset: 0;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
opacity: 0.9;
|
|||
|
|
animation: archify-semantic-lens-flow 1.35s linear 1 both;
|
|||
|
|
animation-delay: var(--lens-flow-delay, 0s);
|
|||
|
|
}
|
|||
|
|
.semantic-lens-flow[data-direction="out"],
|
|||
|
|
.semantic-lens-flow[data-direction="forward"] {
|
|||
|
|
stroke: var(--frontend-stroke);
|
|||
|
|
filter: drop-shadow(0 0 3px var(--frontend-stroke));
|
|||
|
|
}
|
|||
|
|
.semantic-lens-flow[data-direction="in"],
|
|||
|
|
.semantic-lens-flow[data-direction="reverse"] {
|
|||
|
|
stroke: var(--database-stroke);
|
|||
|
|
filter: drop-shadow(0 0 3px var(--database-stroke));
|
|||
|
|
}
|
|||
|
|
.semantic-lens-flow[data-direction="within"] {
|
|||
|
|
stroke: var(--messagebus-stroke);
|
|||
|
|
filter: drop-shadow(0 0 3px var(--messagebus-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-preset="signal-flow"] .semantic-lens-flow {
|
|||
|
|
stroke-width: 3.7;
|
|||
|
|
opacity: 1;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="signal-flow"] .semantic-lens-flow[data-direction="out"],
|
|||
|
|
svg[data-preset="signal-flow"] .semantic-lens-flow[data-direction="forward"] {
|
|||
|
|
filter: drop-shadow(0 0 6px var(--frontend-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-preset="signal-flow"] .semantic-lens-flow[data-direction="in"],
|
|||
|
|
svg[data-preset="signal-flow"] .semantic-lens-flow[data-direction="reverse"] {
|
|||
|
|
filter: drop-shadow(0 0 6px var(--database-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-preset="signal-flow"] .semantic-lens-flow[data-direction="within"] {
|
|||
|
|
filter: drop-shadow(0 0 6px var(--messagebus-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"] .semantic-lens-flow {
|
|||
|
|
stroke-width: 2.45;
|
|||
|
|
stroke-linecap: square;
|
|||
|
|
filter: none;
|
|||
|
|
opacity: 0.78;
|
|||
|
|
}
|
|||
|
|
html[data-embed="true"] .semantic-lens-overlay { display: none; }
|
|||
|
|
svg[data-focus-active] [data-node-id],
|
|||
|
|
svg[data-focus-active] [data-edge-from] { opacity: 0.13; }
|
|||
|
|
svg[data-focus-active] [data-focus-match] { opacity: 1; }
|
|||
|
|
svg[data-focus-active] [data-focus-selected] {
|
|||
|
|
filter: drop-shadow(0 0 10px var(--frontend-stroke));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Authored Reachability is a bounded graph query over the relationships
|
|||
|
|
already present in the artifact. Upstream walks incoming edges;
|
|||
|
|
downstream walks outgoing edges. It never claims runtime causality and
|
|||
|
|
changes only viewer emphasis, not canonical geometry or export bytes. */
|
|||
|
|
svg[data-reach-active] [data-node-id],
|
|||
|
|
svg[data-reach-active] [data-edge-from] { opacity: 0.09; }
|
|||
|
|
svg[data-reach-active] [data-reach-match] { opacity: 1; }
|
|||
|
|
svg[data-reach-active="upstream"] [data-reach-origin] {
|
|||
|
|
filter: drop-shadow(0 0 11px var(--database-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-reach-active="downstream"] [data-reach-origin] {
|
|||
|
|
filter: drop-shadow(0 0 11px var(--backend-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-reach-active="upstream"] [data-edge-from][data-reach-match] {
|
|||
|
|
filter: drop-shadow(0 0 4px color-mix(in srgb, var(--database-stroke) 72%, transparent));
|
|||
|
|
}
|
|||
|
|
svg[data-reach-active="downstream"] [data-edge-from][data-reach-match] {
|
|||
|
|
filter: drop-shadow(0 0 4px color-mix(in srgb, var(--backend-stroke) 72%, transparent));
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"][data-reach-active] [data-reach-origin],
|
|||
|
|
svg[data-preset="blueprint"][data-reach-active] [data-edge-from][data-reach-match] {
|
|||
|
|
filter: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Direct Relationship Explorer makes authored paths a real input surface
|
|||
|
|
without changing their visible stroke or canonical SVG. A transparent
|
|||
|
|
runtime rail supplies forgiving pointer geometry; the exact authored
|
|||
|
|
path and endpoints continue to carry every visible state. */
|
|||
|
|
.relationship-hit-overlay { pointer-events: none; }
|
|||
|
|
.relationship-hit-target { outline: none; }
|
|||
|
|
.relationship-hit-rail {
|
|||
|
|
fill: none;
|
|||
|
|
stroke: transparent;
|
|||
|
|
stroke-width: 24;
|
|||
|
|
stroke-linecap: round;
|
|||
|
|
stroke-linejoin: round;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
pointer-events: stroke;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
.relationship-focus-rail {
|
|||
|
|
fill: none;
|
|||
|
|
stroke: color-mix(in srgb, var(--frontend-stroke) 34%, transparent);
|
|||
|
|
stroke-width: 6;
|
|||
|
|
stroke-dasharray: 2 4;
|
|||
|
|
stroke-linecap: round;
|
|||
|
|
stroke-linejoin: round;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
pointer-events: none;
|
|||
|
|
opacity: 0;
|
|||
|
|
}
|
|||
|
|
.relationship-hit-target:focus-visible .relationship-focus-rail {
|
|||
|
|
opacity: 1;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"] .relationship-hit-target:focus-visible .relationship-focus-rail {
|
|||
|
|
stroke-linecap: square;
|
|||
|
|
filter: none;
|
|||
|
|
}
|
|||
|
|
svg[data-relationship-direct-active] [data-node-id],
|
|||
|
|
svg[data-relationship-direct-active] [data-edge-from] { opacity: 0.16; }
|
|||
|
|
svg[data-relationship-direct-active] [data-relationship-preview],
|
|||
|
|
svg[data-relationship-direct-active] [data-relationship-preview-node] { opacity: 1; }
|
|||
|
|
svg[data-focus-active]:not([data-relationship-pin-active]) .relationship-hit-rail,
|
|||
|
|
svg[data-story-active] .relationship-hit-rail,
|
|||
|
|
svg[data-route-picking] .relationship-hit-rail,
|
|||
|
|
svg[data-route-active] .relationship-hit-rail,
|
|||
|
|
svg[data-lens-active] .relationship-hit-rail,
|
|||
|
|
svg[data-chapter-preview] .relationship-hit-rail { pointer-events: none; }
|
|||
|
|
html[data-embed="true"] .relationship-hit-overlay { display: none; }
|
|||
|
|
@media (hover: none), (pointer: coarse) {
|
|||
|
|
.relationship-hit-rail { stroke-width: 24; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Relationship Preview is temporary exploration state layered on top of
|
|||
|
|
neighborhood focus. It changes emphasis only; exported geometry and the
|
|||
|
|
relationship's original solid/dashed visual language stay untouched. */
|
|||
|
|
svg[data-relationship-preview-active] [data-focus-match] { opacity: 0.18; }
|
|||
|
|
svg[data-relationship-preview-active] [data-relationship-preview],
|
|||
|
|
svg[data-relationship-preview-active] [data-relationship-preview-node] { opacity: 1; }
|
|||
|
|
svg[data-relationship-preview-active] [data-relationship-preview] {
|
|||
|
|
filter: drop-shadow(0 0 5px var(--arrow-emphasis));
|
|||
|
|
}
|
|||
|
|
svg[data-relationship-preview-active] [data-relationship-preview] path,
|
|||
|
|
svg[data-relationship-preview-active] path[data-relationship-preview],
|
|||
|
|
svg[data-relationship-preview-active] [data-relationship-preview] line,
|
|||
|
|
svg[data-relationship-preview-active] line[data-relationship-preview],
|
|||
|
|
svg[data-relationship-preview-active] [data-relationship-preview] polyline,
|
|||
|
|
svg[data-relationship-preview-active] polyline[data-relationship-preview] {
|
|||
|
|
stroke-width: 2.75;
|
|||
|
|
}
|
|||
|
|
svg[data-relationship-preview-active] [data-relationship-preview-source] {
|
|||
|
|
filter: drop-shadow(0 0 7px var(--frontend-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-relationship-preview-active] [data-relationship-preview-target] {
|
|||
|
|
filter: drop-shadow(0 0 7px var(--database-stroke));
|
|||
|
|
}
|
|||
|
|
.relationship-pulse-overlay { pointer-events: none; }
|
|||
|
|
.relationship-flow-pulse {
|
|||
|
|
fill: none;
|
|||
|
|
stroke: var(--arrow-emphasis);
|
|||
|
|
stroke-width: 3.35;
|
|||
|
|
stroke-linecap: round;
|
|||
|
|
stroke-dasharray: 0.085 0.915;
|
|||
|
|
stroke-dashoffset: 0;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
opacity: 0;
|
|||
|
|
filter: drop-shadow(0 0 4px var(--arrow-emphasis));
|
|||
|
|
animation: archify-relationship-pulse 1.2s linear 1 both;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="signal-flow"] .relationship-flow-pulse {
|
|||
|
|
stroke: var(--frontend-stroke);
|
|||
|
|
stroke-width: 3.85;
|
|||
|
|
filter: drop-shadow(0 0 7px var(--frontend-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"] .relationship-flow-pulse {
|
|||
|
|
stroke-width: 2.5;
|
|||
|
|
stroke-linecap: square;
|
|||
|
|
filter: none;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="editorial"] .relationship-flow-pulse {
|
|||
|
|
stroke: var(--arrow-emphasis);
|
|||
|
|
stroke-width: 2.75;
|
|||
|
|
filter: none;
|
|||
|
|
}
|
|||
|
|
.semantic-flow-token {
|
|||
|
|
color: var(--frontend-stroke);
|
|||
|
|
opacity: 0;
|
|||
|
|
pointer-events: none;
|
|||
|
|
}
|
|||
|
|
.relationship-flow-token {
|
|||
|
|
animation: archify-relationship-token-life 1.2s linear 1 both;
|
|||
|
|
}
|
|||
|
|
.story-flow-token {
|
|||
|
|
animation: archify-relationship-token-life 0.78s linear 1 both;
|
|||
|
|
}
|
|||
|
|
.semantic-flow-token-halo {
|
|||
|
|
fill: var(--mask);
|
|||
|
|
stroke: currentColor;
|
|||
|
|
stroke-width: 1.1;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
opacity: 0.96;
|
|||
|
|
}
|
|||
|
|
.relationship-flow-token-shape {
|
|||
|
|
fill: var(--mask);
|
|||
|
|
stroke: currentColor;
|
|||
|
|
stroke-width: 1.35;
|
|||
|
|
stroke-linecap: round;
|
|||
|
|
stroke-linejoin: round;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
}
|
|||
|
|
.relationship-flow-token-ink {
|
|||
|
|
fill: none;
|
|||
|
|
stroke: currentColor;
|
|||
|
|
stroke-width: 1.05;
|
|||
|
|
stroke-linecap: round;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
}
|
|||
|
|
.relationship-flow-token-dot { fill: currentColor; stroke: none; }
|
|||
|
|
.semantic-flow-token[data-token-kind="data"] { color: var(--database-stroke); }
|
|||
|
|
.semantic-flow-token[data-token-kind="event"] { color: var(--messagebus-stroke); }
|
|||
|
|
.semantic-flow-token[data-token-kind="security"] { color: var(--security-stroke); }
|
|||
|
|
.semantic-flow-token[data-token-kind="state"] { color: var(--cloud-stroke); }
|
|||
|
|
svg[data-preset="signal-flow"] .semantic-flow-token {
|
|||
|
|
filter: drop-shadow(0 0 5px currentColor);
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"] .semantic-flow-token { filter: none; }
|
|||
|
|
svg[data-preset="editorial"] .semantic-flow-token { filter: none; }
|
|||
|
|
html[data-embed="true"] .relationship-pulse-overlay { display: none; }
|
|||
|
|
|
|||
|
|
/* Intent Trace is a temporary, pre-click exploration layer. Related
|
|||
|
|
topology stays readable while a short viewer-only signal runs over
|
|||
|
|
cloned edge geometry; the authored edge remains untouched underneath. */
|
|||
|
|
svg[data-intent-trace-active] [data-node-id],
|
|||
|
|
svg[data-intent-trace-active] [data-edge-from] { opacity: 0.2; }
|
|||
|
|
svg[data-intent-trace-active] [data-intent-trace-match] { opacity: 1; }
|
|||
|
|
svg[data-intent-trace-active] [data-intent-trace-selected] {
|
|||
|
|
filter: drop-shadow(0 0 10px var(--frontend-stroke));
|
|||
|
|
}
|
|||
|
|
.intent-trace-overlay { pointer-events: none; }
|
|||
|
|
.intent-trace-flow {
|
|||
|
|
fill: none;
|
|||
|
|
stroke-width: 3.1;
|
|||
|
|
stroke-linecap: round;
|
|||
|
|
stroke-dasharray: 0.1 0.9;
|
|||
|
|
stroke-dashoffset: 0;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
opacity: 0.94;
|
|||
|
|
animation: archify-intent-trace-flow 1.15s linear 1 both;
|
|||
|
|
}
|
|||
|
|
/* Direction names describe each edge relative to the focused node only.
|
|||
|
|
The cloned geometry already runs from the authored source to target, so
|
|||
|
|
incoming and outgoing signals must share the same playback direction. */
|
|||
|
|
.intent-trace-flow[data-direction="out"] {
|
|||
|
|
stroke: var(--frontend-stroke);
|
|||
|
|
filter: drop-shadow(0 0 4px var(--frontend-stroke));
|
|||
|
|
}
|
|||
|
|
.intent-trace-flow[data-direction="in"] {
|
|||
|
|
stroke: var(--database-stroke);
|
|||
|
|
filter: drop-shadow(0 0 4px var(--database-stroke));
|
|||
|
|
animation-direction: normal;
|
|||
|
|
}
|
|||
|
|
.intent-trace-flow[data-direction="loop"] {
|
|||
|
|
stroke: var(--security-stroke);
|
|||
|
|
filter: drop-shadow(0 0 4px var(--security-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"] [data-intent-trace-selected],
|
|||
|
|
svg[data-preset="blueprint"] .intent-trace-flow {
|
|||
|
|
filter: none;
|
|||
|
|
stroke-linecap: square;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="editorial"] [data-intent-trace-selected],
|
|||
|
|
svg[data-preset="editorial"] .intent-trace-flow { filter: none; }
|
|||
|
|
.intent-trace-status {
|
|||
|
|
position: absolute;
|
|||
|
|
width: 1px;
|
|||
|
|
height: 1px;
|
|||
|
|
padding: 0;
|
|||
|
|
margin: -1px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
clip: rect(0, 0, 0, 0);
|
|||
|
|
clip-path: inset(50%);
|
|||
|
|
white-space: nowrap;
|
|||
|
|
border: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Route Probe separates a durable two-point query from selection and
|
|||
|
|
one-hop hover. Only the chosen shortest directed path stays prominent;
|
|||
|
|
the traveling signal is cloned viewer geometry and never authored SVG. */
|
|||
|
|
svg[data-route-picking="target"] [data-node-id] { opacity: 0.24; }
|
|||
|
|
svg[data-route-picking="target"] [data-route-candidate],
|
|||
|
|
svg[data-route-picking="target"] [data-route-start] { opacity: 1; }
|
|||
|
|
svg[data-route-picking] [data-node-id] { cursor: crosshair; }
|
|||
|
|
svg[data-route-picking] [data-route-start] {
|
|||
|
|
filter: drop-shadow(0 0 10px var(--frontend-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-route-active] [data-node-id],
|
|||
|
|
svg[data-route-active] [data-edge-from] { opacity: 0.11; }
|
|||
|
|
svg[data-route-active] [data-route-match] { opacity: 1; }
|
|||
|
|
svg[data-route-active] [data-route-start] {
|
|||
|
|
filter: drop-shadow(0 0 11px var(--frontend-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-route-active] [data-route-end] {
|
|||
|
|
filter: drop-shadow(0 0 11px var(--security-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-route-active] [data-route-step]:not([data-route-start]):not([data-route-end]) {
|
|||
|
|
filter: drop-shadow(0 0 7px var(--backend-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-route-journey] [data-route-match][data-route-journey-state="past"] { opacity: 0.62; }
|
|||
|
|
svg[data-route-journey] [data-route-match][data-route-journey-state="future"] { opacity: 0.34; }
|
|||
|
|
svg[data-route-journey] [data-route-match][data-route-journey-state="current"] { opacity: 1; }
|
|||
|
|
svg[data-route-journey] [data-node-id][data-route-journey-current] {
|
|||
|
|
filter: drop-shadow(0 0 10px var(--backend-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-route-journey] [data-edge-from][data-route-journey-current] {
|
|||
|
|
opacity: 1;
|
|||
|
|
filter: drop-shadow(0 0 6px var(--backend-stroke));
|
|||
|
|
}
|
|||
|
|
.route-probe-overlay { pointer-events: none; }
|
|||
|
|
.route-journey-overlay { pointer-events: none; }
|
|||
|
|
.route-probe-flow {
|
|||
|
|
fill: none;
|
|||
|
|
stroke: var(--backend-stroke);
|
|||
|
|
stroke-width: 3.25;
|
|||
|
|
stroke-linecap: round;
|
|||
|
|
stroke-dasharray: 0.085 0.915;
|
|||
|
|
stroke-dashoffset: 0;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
opacity: 0.96;
|
|||
|
|
filter: drop-shadow(0 0 5px var(--backend-stroke));
|
|||
|
|
animation: archify-route-probe-flow 1.1s cubic-bezier(0.22, 1, 0.36, 1) 1 both;
|
|||
|
|
animation-delay: calc(var(--route-step, 0) * 0.16s);
|
|||
|
|
}
|
|||
|
|
.route-journey-flow {
|
|||
|
|
fill: none;
|
|||
|
|
stroke: var(--backend-stroke);
|
|||
|
|
stroke-width: 3.5;
|
|||
|
|
stroke-linecap: round;
|
|||
|
|
stroke-dasharray: 0.1 0.9;
|
|||
|
|
stroke-dashoffset: 0;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
opacity: 0;
|
|||
|
|
filter: drop-shadow(0 0 6px var(--backend-stroke));
|
|||
|
|
animation: archify-route-journey-flow 780ms cubic-bezier(0.22, 1, 0.36, 1) 1 both;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"] [data-route-start],
|
|||
|
|
svg[data-preset="blueprint"] [data-route-end],
|
|||
|
|
svg[data-preset="blueprint"] [data-route-step],
|
|||
|
|
svg[data-preset="blueprint"] .route-probe-flow {
|
|||
|
|
filter: none;
|
|||
|
|
stroke-linecap: square;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"] .route-journey-flow {
|
|||
|
|
filter: none;
|
|||
|
|
stroke-linecap: square;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="editorial"] [data-route-start],
|
|||
|
|
svg[data-preset="editorial"] [data-route-end],
|
|||
|
|
svg[data-preset="editorial"] [data-route-step],
|
|||
|
|
svg[data-preset="editorial"] .route-probe-flow,
|
|||
|
|
svg[data-preset="editorial"] .route-journey-flow { filter: none; }
|
|||
|
|
|
|||
|
|
svg[data-preset="editorial"] .c-grid {
|
|||
|
|
stroke-dasharray: 1 4;
|
|||
|
|
opacity: 0.48;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="editorial"] .c-region {
|
|||
|
|
fill: color-mix(in srgb, var(--cloud-fill) 48%, transparent);
|
|||
|
|
stroke-dasharray: 9 4;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="editorial"] [data-node-id]:hover,
|
|||
|
|
svg[data-preset="editorial"] [data-node-id]:focus-visible,
|
|||
|
|
svg[data-preset="editorial"][data-focus-active] [data-focus-selected] {
|
|||
|
|
filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--text) 18%, transparent));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
svg[data-preset="blueprint"] .c-grid {
|
|||
|
|
stroke-dasharray: 1 3;
|
|||
|
|
opacity: 0.78;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"] .c-region {
|
|||
|
|
fill: color-mix(in srgb, var(--cloud-fill) 58%, transparent);
|
|||
|
|
stroke-dasharray: 12 4 2 4;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"] .c-security-group,
|
|||
|
|
svg[data-preset="blueprint"] .c-lane { stroke-dasharray: 7 3; }
|
|||
|
|
svg[data-preset="blueprint"] [data-node-id]:hover,
|
|||
|
|
svg[data-preset="blueprint"] [data-node-id]:focus-visible {
|
|||
|
|
filter: drop-shadow(0 0 3px var(--frontend-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"][data-focus-active] [data-focus-selected] {
|
|||
|
|
filter: drop-shadow(0 0 3px var(--frontend-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"][data-focus-active][data-reach-active] [data-focus-selected][data-reach-origin] {
|
|||
|
|
filter: none;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"][data-relationship-preview-active] [data-relationship-preview],
|
|||
|
|
svg[data-preset="blueprint"][data-relationship-preview-active] [data-relationship-preview-source],
|
|||
|
|
svg[data-preset="blueprint"][data-relationship-preview-active] [data-relationship-preview-target] {
|
|||
|
|
filter: drop-shadow(0 0 3px var(--frontend-stroke));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Story Trail is a viewer-only overlay derived from the ordered focus IDs
|
|||
|
|
of the active guided view. The authored edge stays underneath with its
|
|||
|
|
original color, dash pattern, marker, and geometry intact. */
|
|||
|
|
.story-trail-overlay { pointer-events: none; }
|
|||
|
|
.story-carrier-overlay { pointer-events: none; }
|
|||
|
|
.story-trail-flow {
|
|||
|
|
fill: none;
|
|||
|
|
stroke: var(--frontend-stroke);
|
|||
|
|
stroke-width: 2.8;
|
|||
|
|
stroke-linecap: round;
|
|||
|
|
vector-effect: non-scaling-stroke;
|
|||
|
|
opacity: 0.34;
|
|||
|
|
filter: drop-shadow(0 0 2px var(--frontend-stroke));
|
|||
|
|
}
|
|||
|
|
svg[data-story-beat] [data-story-step] {
|
|||
|
|
opacity: 0.22;
|
|||
|
|
filter: saturate(0.42);
|
|||
|
|
transition: opacity 180ms ease, filter 180ms ease;
|
|||
|
|
}
|
|||
|
|
svg[data-story-beat] [data-story-step][data-story-beat-state="past"] {
|
|||
|
|
opacity: 0.72;
|
|||
|
|
filter: saturate(0.82);
|
|||
|
|
}
|
|||
|
|
svg[data-story-beat] [data-story-step][data-story-beat-state="next"] {
|
|||
|
|
opacity: 0.5;
|
|||
|
|
filter: saturate(0.66);
|
|||
|
|
}
|
|||
|
|
svg[data-story-beat] [data-story-step][data-story-beat-state="active"] {
|
|||
|
|
opacity: 1;
|
|||
|
|
filter: drop-shadow(0 0 7px var(--frontend-stroke));
|
|||
|
|
animation: archify-story-beat-node 360ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
|||
|
|
}
|
|||
|
|
svg[data-story-beat] [data-edge-from][data-story-beat-step] {
|
|||
|
|
opacity: 0.16;
|
|||
|
|
transition: opacity 160ms ease;
|
|||
|
|
}
|
|||
|
|
svg[data-story-beat] [data-edge-from][data-story-beat-step][data-story-beat-state="past"] { opacity: 0.58; }
|
|||
|
|
svg[data-story-beat] [data-edge-from][data-story-beat-step][data-story-beat-state="next"] { opacity: 0.34; }
|
|||
|
|
svg[data-story-beat] [data-edge-from][data-story-beat-step][data-story-beat-state="active"] { opacity: 1; }
|
|||
|
|
svg[data-story-beat] .story-trail-flow { opacity: 0; transition: opacity 160ms ease; }
|
|||
|
|
svg[data-story-beat] .story-trail-flow[data-story-beat-state="past"] { opacity: 0.34; }
|
|||
|
|
svg[data-story-beat] .story-trail-flow[data-story-beat-state="next"] { opacity: 0.2; }
|
|||
|
|
svg[data-story-beat] .story-trail-flow[data-story-beat-state="active"] { opacity: 0.92; }
|
|||
|
|
svg[data-story-beat] .story-trail-flow[data-story-pulse="true"] {
|
|||
|
|
stroke-dasharray: 2 13;
|
|||
|
|
stroke-dashoffset: 30;
|
|||
|
|
opacity: 0.92;
|
|||
|
|
animation: archify-story-flow 0.78s linear 1 both;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"] .story-trail-flow {
|
|||
|
|
stroke-linecap: square;
|
|||
|
|
filter: none;
|
|||
|
|
opacity: 0.5;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"][data-story-beat] [data-story-step][data-story-beat-state="active"] {
|
|||
|
|
filter: none;
|
|||
|
|
animation: none;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="editorial"] .story-trail-flow { filter: none; }
|
|||
|
|
svg[data-preset="editorial"][data-story-beat] [data-story-step][data-story-beat-state="active"] {
|
|||
|
|
filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--text) 16%, transparent));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Chapter Delta Preview is a static, pre-commit comparison between two
|
|||
|
|
chapter focus sets. Symbols in the rail carry the meaning without color;
|
|||
|
|
these SVG rules only make exact stable-ID membership easier to scan. */
|
|||
|
|
svg[data-chapter-preview] [data-node-id],
|
|||
|
|
svg[data-chapter-preview] [data-edge-from] {
|
|||
|
|
opacity: 0.1 !important;
|
|||
|
|
filter: none !important;
|
|||
|
|
transition: none !important;
|
|||
|
|
}
|
|||
|
|
svg[data-chapter-preview] .story-trail-overlay { opacity: 0.08 !important; }
|
|||
|
|
svg[data-chapter-preview] [data-node-id][data-chapter-preview-role="stay"] {
|
|||
|
|
opacity: 0.76 !important;
|
|||
|
|
filter: saturate(0.7) !important;
|
|||
|
|
}
|
|||
|
|
svg[data-chapter-preview] [data-node-id][data-chapter-preview-role="enter"] {
|
|||
|
|
opacity: 1 !important;
|
|||
|
|
filter: saturate(1.2) !important;
|
|||
|
|
}
|
|||
|
|
svg[data-chapter-preview] [data-node-id][data-chapter-preview-role="leave"] {
|
|||
|
|
opacity: 0.28 !important;
|
|||
|
|
filter: grayscale(0.72) !important;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"][data-chapter-preview] [data-node-id] { filter: none !important; }
|
|||
|
|
|
|||
|
|
/* Dashed boundary variants */
|
|||
|
|
.c-security-group { fill: transparent; stroke: var(--security-stroke); stroke-dasharray: 4,4; }
|
|||
|
|
.c-region { fill: rgba(251, 191, 36, 0.05); stroke: var(--cloud-stroke); stroke-dasharray: 8,4; }
|
|||
|
|
.c-lane { fill: var(--lane-fill); stroke: var(--lane-stroke); stroke-dasharray: 6,6; }
|
|||
|
|
|
|||
|
|
/* Optional trace animation, enabled only by meta.animation = "trace".
|
|||
|
|
The ordinary viewer performs one ambient pass and then restores the
|
|||
|
|
authored solid/security/async line language. WebM samples the same
|
|||
|
|
authored geometry into an explicit canvas timeline. */
|
|||
|
|
html[data-ambient-motion="running"] svg[data-animation="trace"] [data-animate="edge"] {
|
|||
|
|
animation: archify-edge-flow 2.4s linear 1;
|
|||
|
|
animation-delay: calc(var(--step, 0) * 160ms);
|
|||
|
|
}
|
|||
|
|
html[data-ambient-motion="running"] svg[data-animation="trace"] [data-animate="node"] {
|
|||
|
|
transform-box: fill-box;
|
|||
|
|
transform-origin: center;
|
|||
|
|
animation: archify-node-pulse 3.6s ease-in-out 1;
|
|||
|
|
animation-delay: calc(var(--step, 0) * 160ms);
|
|||
|
|
}
|
|||
|
|
svg[data-preset="signal-flow"][data-animation="trace"] [data-animate="edge"] {
|
|||
|
|
stroke-linecap: round;
|
|||
|
|
filter: drop-shadow(0 0 3px var(--arrow-emphasis));
|
|||
|
|
animation-duration: 1.75s;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="signal-flow"][data-animation="trace"] [data-animate="node"] {
|
|||
|
|
animation-duration: 3.1s;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"][data-animation="trace"] [data-animate="edge"] {
|
|||
|
|
stroke-linecap: square;
|
|||
|
|
filter: none;
|
|||
|
|
animation-duration: 2.15s;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="blueprint"][data-animation="trace"] [data-animate="node"] {
|
|||
|
|
animation-name: archify-blueprint-node-pulse;
|
|||
|
|
animation-duration: 3.8s;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="editorial"][data-animation="trace"] [data-animate="edge"] {
|
|||
|
|
filter: none;
|
|||
|
|
animation-duration: 2.2s;
|
|||
|
|
}
|
|||
|
|
svg[data-preset="editorial"][data-animation="trace"] [data-animate="node"] {
|
|||
|
|
animation-name: archify-editorial-node-pulse;
|
|||
|
|
animation-duration: 3.5s;
|
|||
|
|
}
|
|||
|
|
/* Embedded proofs are static until a named Story Trail chapter is played.
|
|||
|
|
This keeps proof grids calm and ensures ?play=1 stops all viewer motion
|
|||
|
|
after one 3.2 second chapter instead of leaving ambient trace loops on. */
|
|||
|
|
html[data-embed="true"] svg[data-animation="trace"] [data-animate],
|
|||
|
|
html[data-share-playback="true"] svg[data-animation="trace"] [data-animate] {
|
|||
|
|
animation: none !important;
|
|||
|
|
stroke-dashoffset: 0;
|
|||
|
|
}
|
|||
|
|
@keyframes archify-edge-flow {
|
|||
|
|
0% { stroke-dasharray: 10 8; stroke-dashoffset: 54; opacity: 0.42; }
|
|||
|
|
88% { stroke-dasharray: 10 8; stroke-dashoffset: 0; opacity: 1; }
|
|||
|
|
99.9% { stroke-dasharray: 10 8; stroke-dashoffset: 0; opacity: 1; }
|
|||
|
|
100% { stroke-dashoffset: 0; opacity: 1; }
|
|||
|
|
}
|
|||
|
|
@keyframes archify-node-pulse {
|
|||
|
|
0%, 72%, 100% { filter: none; stroke-width: 1.5; }
|
|||
|
|
18%, 36% {
|
|||
|
|
filter: drop-shadow(0 0 8px var(--arrow-emphasis));
|
|||
|
|
stroke-width: 2.4;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
@keyframes archify-blueprint-node-pulse {
|
|||
|
|
0%, 72%, 100% { opacity: 1; filter: none; }
|
|||
|
|
18%, 36% { opacity: 0.72; filter: drop-shadow(0 0 2px var(--frontend-stroke)); }
|
|||
|
|
}
|
|||
|
|
@keyframes archify-editorial-node-pulse {
|
|||
|
|
0%, 72%, 100% { opacity: 1; filter: none; }
|
|||
|
|
18%, 36% { opacity: 0.78; filter: drop-shadow(0 2px 2px color-mix(in srgb, var(--text) 18%, transparent)); }
|
|||
|
|
}
|
|||
|
|
@keyframes archify-signal-scan {
|
|||
|
|
0%, 18% { opacity: 1; transform: translateX(-70%); }
|
|||
|
|
70% { opacity: 1; transform: translateX(70%); }
|
|||
|
|
100% { opacity: 0; transform: translateX(70%); }
|
|||
|
|
}
|
|||
|
|
@keyframes archify-radar-live {
|
|||
|
|
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--backend-stroke) 52%, transparent); }
|
|||
|
|
55%, 100% { box-shadow: 0 0 0 0.32rem transparent; }
|
|||
|
|
}
|
|||
|
|
@keyframes archify-guided-progress {
|
|||
|
|
from { transform: scaleX(var(--guided-progress-start, 0)); }
|
|||
|
|
to { transform: scaleX(1); }
|
|||
|
|
}
|
|||
|
|
@keyframes archify-story-flow {
|
|||
|
|
to { stroke-dashoffset: 0; }
|
|||
|
|
}
|
|||
|
|
@keyframes archify-intent-trace-flow {
|
|||
|
|
0% { opacity: 0.28; stroke-dashoffset: 0; }
|
|||
|
|
12%, 78% { opacity: 0.94; }
|
|||
|
|
100% { opacity: 0; stroke-dashoffset: -1; }
|
|||
|
|
}
|
|||
|
|
@keyframes archify-route-probe-flow {
|
|||
|
|
0% { stroke-dashoffset: 0; opacity: 0.96; }
|
|||
|
|
78% { stroke-dashoffset: -1; opacity: 0.96; }
|
|||
|
|
100% { stroke-dasharray: none; stroke-dashoffset: 0; opacity: 0.58; }
|
|||
|
|
}
|
|||
|
|
@keyframes archify-route-journey-flow {
|
|||
|
|
0% { opacity: 0.16; stroke-dashoffset: 0; }
|
|||
|
|
14%, 76% { opacity: 1; }
|
|||
|
|
100% { opacity: 0; stroke-dashoffset: -1; }
|
|||
|
|
}
|
|||
|
|
@keyframes archify-semantic-lens-flow {
|
|||
|
|
0% { opacity: 0.2; stroke-dashoffset: 0; }
|
|||
|
|
12%, 78% { opacity: 0.9; }
|
|||
|
|
100% { opacity: 0; stroke-dashoffset: -1; }
|
|||
|
|
}
|
|||
|
|
@keyframes archify-relationship-pulse {
|
|||
|
|
0% { opacity: 0.18; stroke-dashoffset: 0; }
|
|||
|
|
10%, 76% { opacity: 0.98; }
|
|||
|
|
100% { opacity: 0; stroke-dashoffset: -1; }
|
|||
|
|
}
|
|||
|
|
@keyframes archify-relationship-token-life {
|
|||
|
|
0%, 7% { opacity: 0; }
|
|||
|
|
13%, 82% { opacity: 1; }
|
|||
|
|
100% { opacity: 0; }
|
|||
|
|
}
|
|||
|
|
@keyframes archify-story-beat-node {
|
|||
|
|
from { opacity: 0.48; filter: saturate(0.6); }
|
|||
|
|
58% { opacity: 1; filter: drop-shadow(0 0 10px var(--frontend-stroke)); }
|
|||
|
|
to { opacity: 1; filter: drop-shadow(0 0 7px var(--frontend-stroke)); }
|
|||
|
|
}
|
|||
|
|
@keyframes archify-share-cue-enter {
|
|||
|
|
from { opacity: 0; transform: translate(-50%, -0.45rem); }
|
|||
|
|
to { opacity: 1; transform: translate(-50%, 0); }
|
|||
|
|
}
|
|||
|
|
@keyframes archify-share-cue-pulse {
|
|||
|
|
0%, 100% { opacity: 1; transform: scale(1); }
|
|||
|
|
50% { opacity: 0.48; transform: scale(0.78); }
|
|||
|
|
}
|
|||
|
|
@media (prefers-reduced-motion: reduce) {
|
|||
|
|
html[data-preset="signal-flow"] .diagram-container::before {
|
|||
|
|
animation: none !important;
|
|||
|
|
}
|
|||
|
|
.pulse-dot { animation: none !important; }
|
|||
|
|
svg[data-animation="trace"] [data-animate] {
|
|||
|
|
animation: none !important;
|
|||
|
|
stroke-dashoffset: 0;
|
|||
|
|
}
|
|||
|
|
.guided-view-progress span {
|
|||
|
|
animation: none !important;
|
|||
|
|
transform: scaleX(1) !important;
|
|||
|
|
}
|
|||
|
|
.story-trail-flow,
|
|||
|
|
.intent-trace-flow,
|
|||
|
|
.route-probe-flow,
|
|||
|
|
.semantic-lens,
|
|||
|
|
.diagram-guide,
|
|||
|
|
[data-story-step],
|
|||
|
|
.overview-map-live,
|
|||
|
|
.guided-view-stop,
|
|||
|
|
.share-chapter-cue,
|
|||
|
|
.chapter-handoff-anchor,
|
|||
|
|
.share-chapter-state::before,
|
|||
|
|
.share-chapter-progress span { animation: none !important; }
|
|||
|
|
.guided-view-chapter,
|
|||
|
|
svg[data-chapter-preview] [data-node-id],
|
|||
|
|
svg[data-chapter-preview] [data-edge-from] { transition: none !important; }
|
|||
|
|
svg[data-story-playing="true"] .story-trail-flow {
|
|||
|
|
stroke-dasharray: none;
|
|||
|
|
stroke-dashoffset: 0;
|
|||
|
|
opacity: 0.55;
|
|||
|
|
}
|
|||
|
|
.intent-trace-flow {
|
|||
|
|
animation: none !important;
|
|||
|
|
stroke-dasharray: none;
|
|||
|
|
stroke-dashoffset: 0;
|
|||
|
|
opacity: 0.72;
|
|||
|
|
}
|
|||
|
|
.route-probe-flow {
|
|||
|
|
animation: none !important;
|
|||
|
|
stroke-dasharray: none;
|
|||
|
|
stroke-dashoffset: 0;
|
|||
|
|
opacity: 0.78;
|
|||
|
|
}
|
|||
|
|
.route-journey-overlay,
|
|||
|
|
.story-carrier-overlay { display: none !important; }
|
|||
|
|
.semantic-lens-flow {
|
|||
|
|
animation: none !important;
|
|||
|
|
stroke-dasharray: none;
|
|||
|
|
stroke-width: 2.2;
|
|||
|
|
opacity: 0.34;
|
|||
|
|
filter: none;
|
|||
|
|
}
|
|||
|
|
.relationship-pulse-overlay { display: none !important; }
|
|||
|
|
.guided-view-chapter { transition: none !important; }
|
|||
|
|
.source-evidence-beacon,
|
|||
|
|
svg [data-node-id], svg [data-edge-from], svg [data-detail], svg [data-detail-anchor], svg [data-legend-hit], svg { transition: none !important; }
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<!-- Toolbar: theme, live visual style, trace motion, presentation stage, and export.
|
|||
|
|
Motion appears only for trace-enabled artifacts and has no shortcut;
|
|||
|
|
Keyboard: T toggles theme, S cycles style, F toggles the stage, E opens export. -->
|
|||
|
|
<div class="toolbar" role="toolbar" aria-label="Diagram actions">
|
|||
|
|
<button id="btn-theme" type="button"
|
|||
|
|
title="Toggle theme (T)"
|
|||
|
|
aria-label="Toggle color theme"
|
|||
|
|
aria-pressed="false">
|
|||
|
|
<span id="theme-icon" class="toolbar-icon" aria-hidden="true"></span>
|
|||
|
|
<span id="theme-label">Dark</span>
|
|||
|
|
</button>
|
|||
|
|
<div class="preset-wrap">
|
|||
|
|
<button id="btn-preset" type="button"
|
|||
|
|
title="Choose visual style (S cycles)"
|
|||
|
|
aria-label="Choose visual style"
|
|||
|
|
aria-haspopup="menu"
|
|||
|
|
aria-expanded="false"
|
|||
|
|
aria-controls="preset-menu"
|
|||
|
|
aria-keyshortcuts="S">
|
|||
|
|
<span class="preset-control-mark" aria-hidden="true"></span>
|
|||
|
|
<span id="preset-label">Style</span>
|
|||
|
|
<span class="toolbar-icon toolbar-chevron" aria-hidden="true"></span>
|
|||
|
|
</button>
|
|||
|
|
<div class="preset-menu" id="preset-menu" role="menu" aria-label="Visual style">
|
|||
|
|
<div class="preset-menu-heading" role="presentation"><span>Visual identity</span><span>S cycles</span></div>
|
|||
|
|
<button class="preset-option" data-preset-value="classic" type="button" role="menuitemradio" aria-checked="false" tabindex="-1">
|
|||
|
|
<span class="preset-option-swatch classic" aria-hidden="true"></span>
|
|||
|
|
<span class="preset-option-copy"><strong>Classic</strong><small>Stable technical default</small></span>
|
|||
|
|
<span class="preset-option-check" aria-hidden="true"></span>
|
|||
|
|
</button>
|
|||
|
|
<button class="preset-option" data-preset-value="signal-flow" type="button" role="menuitemradio" aria-checked="false" tabindex="-1">
|
|||
|
|
<span class="preset-option-swatch signal-flow" aria-hidden="true"></span>
|
|||
|
|
<span class="preset-option-copy"><strong>Signal Flow</strong><small>Motion-forward presentation</small></span>
|
|||
|
|
<span class="preset-option-check" aria-hidden="true"></span>
|
|||
|
|
</button>
|
|||
|
|
<button class="preset-option" data-preset-value="blueprint" type="button" role="menuitemradio" aria-checked="false" tabindex="-1">
|
|||
|
|
<span class="preset-option-swatch blueprint" aria-hidden="true"></span>
|
|||
|
|
<span class="preset-option-copy"><strong>Blueprint</strong><small>Engineering review</small></span>
|
|||
|
|
<span class="preset-option-check" aria-hidden="true"></span>
|
|||
|
|
</button>
|
|||
|
|
<button class="preset-option" data-preset-value="editorial" type="button" role="menuitemradio" aria-checked="false" tabindex="-1">
|
|||
|
|
<span class="preset-option-swatch editorial" aria-hidden="true"></span>
|
|||
|
|
<span class="preset-option-copy"><strong>Editorial</strong><small>Publication and launch notes</small></span>
|
|||
|
|
<span class="preset-option-check" aria-hidden="true"></span>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<button id="btn-motion" type="button" hidden
|
|||
|
|
title="Pause motion"
|
|||
|
|
aria-label="Pause motion"
|
|||
|
|
aria-pressed="true">
|
|||
|
|
<span class="motion-control-dot" aria-hidden="true"></span>
|
|||
|
|
<span id="motion-label">Live</span>
|
|||
|
|
</button>
|
|||
|
|
<button id="btn-present" type="button"
|
|||
|
|
title="Presentation stage (F)"
|
|||
|
|
aria-label="Enter presentation stage"
|
|||
|
|
aria-pressed="false">
|
|||
|
|
<span id="present-icon" class="toolbar-icon" aria-hidden="true"></span>
|
|||
|
|
<span id="present-label">Present</span>
|
|||
|
|
</button>
|
|||
|
|
<div class="export-wrap">
|
|||
|
|
<button id="btn-export" type="button"
|
|||
|
|
title="Export diagram (E)"
|
|||
|
|
aria-label="Export diagram"
|
|||
|
|
aria-haspopup="menu"
|
|||
|
|
aria-expanded="false"
|
|||
|
|
aria-controls="export-menu">
|
|||
|
|
Export <span class="toolbar-icon toolbar-chevron" aria-hidden="true"></span>
|
|||
|
|
</button>
|
|||
|
|
<div class="export-menu" id="export-menu" role="menu" aria-label="Export">
|
|||
|
|
<div class="export-menu-header" role="presentation">
|
|||
|
|
<div><strong>Export diagram</strong><span>Portable, clean outputs</span></div>
|
|||
|
|
<kbd>E</kbd>
|
|||
|
|
</div>
|
|||
|
|
<div class="export-menu-section" role="group" aria-label="Share">
|
|||
|
|
<span class="export-menu-heading" aria-hidden="true">Share</span>
|
|||
|
|
<button data-format="share-card" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>Share Card</strong><small class="hint">1200×630 PNG</small></span></button>
|
|||
|
|
<button data-action="route-share-card" type="button" role="menuitem" tabindex="-1" hidden disabled><span class="export-item-copy"><strong>Route Share Card</strong><small class="hint">1200×630 PNG</small></span></button>
|
|||
|
|
<button data-action="reach-share-card" type="button" role="menuitem" tabindex="-1" hidden disabled><span class="export-item-copy"><strong>Reach Share Card</strong><small class="hint">1200×630 PNG</small></span></button>
|
|||
|
|
<button data-action="copy-share-card" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>Copy Share Card</strong><small class="hint">PNG to clipboard</small></span></button>
|
|||
|
|
<button data-action="copy" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>Copy diagram</strong><small class="hint">PNG to clipboard</small></span></button>
|
|||
|
|
</div>
|
|||
|
|
<div class="export-menu-section" role="group" aria-label="Raster images">
|
|||
|
|
<span class="export-menu-heading" aria-hidden="true">Image</span>
|
|||
|
|
<button data-format="png" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>PNG</strong><small class="hint">Lossless image</small></span></button>
|
|||
|
|
<button data-format="jpeg" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>JPEG</strong><small class="hint">Compact image</small></span></button>
|
|||
|
|
<button data-format="webp" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>WebP</strong><small class="hint">Modern image</small></span></button>
|
|||
|
|
</div>
|
|||
|
|
<div class="export-menu-section" role="group" aria-label="Vector and motion">
|
|||
|
|
<span class="export-menu-heading" aria-hidden="true">Vector & motion</span>
|
|||
|
|
<button data-format="svg" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>SVG</strong><small class="hint">Editable vector</small></span></button>
|
|||
|
|
<button data-format="webm" type="button" role="menuitem" tabindex="-1"><span class="export-item-copy"><strong>WebM</strong><small class="hint">6s motion</small></span></button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="container">
|
|||
|
|
<!-- Header -->
|
|||
|
|
<div class="header">
|
|||
|
|
<div class="header-row"
|
|||
|
|
data-preset-badge-signal-flow="SIGNAL FLOW"
|
|||
|
|
data-preset-badge-blueprint="BLUEPRINT / REV 01"
|
|||
|
|
data-preset-badge-editorial="EDITORIAL / FIELD NOTE">
|
|||
|
|
<div class="pulse-dot"></div>
|
|||
|
|
<h1>LazyBoy:從一句話到完成任務</h1>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script id="archify-guided-views-data" type="application/json">[{"id":"task-flow","label":"任務主流程","focus":["workspace","api","supervisor","desktop","result"],"note":"沿著主路徑查看指令如何成為可觀察、可接管的桌面操作。"},{"id":"memory-schedule","label":"記憶與排程","focus":["api","memory","schedule"],"note":"長期記憶與排程會在需要時補充情境並重新啟動任務。"},{"id":"safety","label":"隔離與憑證","focus":["vault","supervisor","desktop"],"note":"登入資料經加密保存,桌面工作負載在獨立容器中執行。"}]</script>
|
|||
|
|
|
|||
|
|
<script id="archify-i18n-data" type="application/json">{"locale":"en","messages":{"viewer.kind.frontend":"Frontend","viewer.kind.backend":"Backend","viewer.kind.database":"Database","viewer.kind.cloud":"Cloud","viewer.kind.security":"Security","viewer.kind.messagebus":"Message bus","viewer.kind.external":"External","viewer.kind.neutral":"Neutral","viewer.kind.node":"Node","viewer.kind.start":"Start","viewer.kind.active":"Active","viewer.kind.waiting":"Waiting","viewer.kind.decision":"Decision","viewer.kind.success":"Success","viewer.kind.failure":"Failure","viewer.toolbar.actions":"Diagram actions","viewer.theme.toggle.title":"Toggle theme (T)","viewer.theme.toggle":"Toggle color theme","viewer.theme.dark":"Dark","viewer.theme.light":"Light","viewer.preset.choose.title":"Choose visual style (S cycles)","viewer.preset.choose":"Choose visual style","viewer.preset.style":"Style","viewer.preset.menu":"Visual style","viewer.preset.identity":"Visual identity","viewer.preset.cycles":"S cycles","viewer.preset.classic":"Classic","viewer.preset.classic.short":"Classic","viewer.preset.classic.hint":"Stable technical default","viewer.preset.flow":"Signal Flow","viewer.preset.flow.short":"Flow","viewer.preset.flow.hint":"Motion-forward presentation","viewer.preset.blueprint":"Blueprint","viewer.preset.blueprint.hint":"Engineering review","viewer.preset.editorial":"Editorial","viewer.preset.editorial.hint":"Publication and launch notes","viewer.preset.badge.signalFlow":"SIGNAL FLOW","viewer.preset.badge.blueprint":"BLUEPRINT / REV 01","viewer.preset.badge.editorial":"EDITORIAL / FIELD NOTE","viewer.preset.badge.editorialPlate":"ARCHIFY / PLATE 04","viewer.preset.current":"Visual style: {style}. Choose visual style","viewer.motion.live":"Live","viewer.motion.still":"Still","viewer.motion.pause":"Pause motion","viewer.motion.resume":"Resume motion","viewer.motion.reduced":"Motion paused by reduced-motion preference","viewer.motion.hidden":"Motion paused while this page is hidden","viewer.motion.yielding":"Pause motion; currently yielding to {owner}","viewer.motion.yielding.title":"Live preview enabled · yielding to {owner}","viewer.owner.story":"the guided story","viewer.owner.chapter":"the active chapter","viewer.owner.chapterPreview":"the chapter delta preview","viewer.owner.handoff":"the chapter handoff","viewer.owner.route":"Route Probe","viewer.owner.lens":"Semantic Lens","viewer.owner.relationship":"Relationship Preview","viewer.owner.intent":"Intent Trace","viewer.owner.focus":"semantic focus","viewer.owner.legend":"legend preview","viewer.owner.reader":"reader interaction","viewer.present.enter":"Enter presentation stage","viewer.present.enter.title":"Presentation stage (F)","viewer.present.exit":"Exit presentation stage","viewer.present.exit.title":"Exit presentation stage (F or Escape)","viewer.present.present":"Present","viewer.present.exit.label":"Exit","viewer.export.button":"Export","viewer.export.button.title":"Export diagram (E)","viewer.export.diagram":"Export diagram","viewer.export.menu":"Export","viewer.export.subtitle":"Portable, clean outputs","viewer.export.share":"Share","viewer.export.shareCard":"Share Card","viewer.export.routeShareCard":"Route Share Card","viewer.export.reachShareCard":"Reach Share Card","viewer.export.copyShareCard":"Copy Share Card","viewer.export.copyDiagram":"Copy diagram","viewer.export.clipboardPng":"PNG to clipboard","viewer.export.raster":"Raster images","viewer.export.image":"Image","viewer.export.lossless":"Lossless image","viewer.export.compact":"Compact image","viewer.export.modern":"Modern image","viewer.export.vectorMotion":"Vector and motion","viewer.export.vectorMotion.heading":"Vector \u0026 motion","viewer.export.editable":"Editable vector","viewer.export.motion6s":"6s motion","viewer.export.unsupported":"Not supported by this browser","viewer.export.clipboardUnsupported":"Clipboard image write not supported by this browser","viewer.export.clipboardUnsupported.period":"Clipboard image write not supported by this browser.","viewer.export.clipboardUnsupported.short":"Clip
|
|||
|
|
<div class="guided-views no-print" id="guided-views" hidden aria-label="Guided diagram views">
|
|||
|
|
<button id="guided-view-prev" type="button" aria-label="Previous guided view" title="Previous guided view ([)">←</button>
|
|||
|
|
<div class="guided-view-copy">
|
|||
|
|
<div class="guided-view-meta">
|
|||
|
|
<span>Guided views</span>
|
|||
|
|
<span id="guided-view-count"></span>
|
|||
|
|
<span class="guided-view-progress" aria-hidden="true"><span id="guided-view-progress-bar"></span></span>
|
|||
|
|
<span class="guided-view-handoff" id="guided-view-handoff" hidden aria-hidden="true"></span>
|
|||
|
|
</div>
|
|||
|
|
<strong id="guided-view-label">Explore this system</strong>
|
|||
|
|
<small id="guided-view-note" aria-live="polite" aria-atomic="true">Step through curated paths without changing the source diagram.</small>
|
|||
|
|
<div class="guided-view-trail" id="guided-view-trail" hidden role="group" aria-label="Story trail"></div>
|
|||
|
|
<div class="guided-story-caption" id="guided-story-caption" hidden aria-live="polite" aria-atomic="true">
|
|||
|
|
<span class="guided-story-caption-index" id="guided-story-caption-index">Beat</span>
|
|||
|
|
<strong id="guided-story-caption-route"></strong>
|
|||
|
|
<small id="guided-story-caption-detail"></small>
|
|||
|
|
<span class="guided-story-caption-next" id="guided-story-caption-next" hidden aria-hidden="true">
|
|||
|
|
<small>Next</small>
|
|||
|
|
<strong id="guided-story-caption-next-label"></strong>
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<button id="guided-view-next" type="button" aria-label="Next guided view" title="Next guided view (])">→</button>
|
|||
|
|
<div class="guided-view-actions">
|
|||
|
|
<button class="guided-view-play" id="guided-view-play" type="button" aria-label="Play guided story" aria-pressed="false" title="Play guided story (P)">
|
|||
|
|
<span id="guided-view-play-icon" aria-hidden="true">▶</span>
|
|||
|
|
<span id="guided-view-play-label">Play story</span>
|
|||
|
|
</button>
|
|||
|
|
<button class="guided-view-beat-link" id="guided-view-beat-link" type="button" aria-label="Select a Story Beat to copy its exact link" title="Select a Story Beat to copy its exact link" disabled>
|
|||
|
|
<span aria-hidden="true">#</span>
|
|||
|
|
<span id="guided-view-beat-link-label">Copy moment</span>
|
|||
|
|
</button>
|
|||
|
|
<button class="guided-view-all" id="guided-view-all" type="button" aria-label="Show entire diagram">Show all</button>
|
|||
|
|
</div>
|
|||
|
|
<nav class="guided-view-index" id="guided-view-index" aria-label="Story chapters">
|
|||
|
|
<ol class="guided-view-chapters" id="guided-view-chapters"></ol>
|
|||
|
|
</nav>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Main Diagram -->
|
|||
|
|
<div class="diagram-container" data-detail-level="read"
|
|||
|
|
data-preset-badge-editorial-plate="ARCHIFY / PLATE 04">
|
|||
|
|
<svg viewBox="0 0 1233 542" role="img" lang="en" aria-labelledby="archify-diagram-title archify-diagram-description" data-preset="classic" data-quality-profile="showcase">
|
|||
|
|
<title id="archify-diagram-title">LazyBoy:從一句話到完成任務</title>
|
|||
|
|
<desc id="archify-diagram-description">A workflow diagram generated by Archify.</desc>
|
|||
|
|
<!-- Definitions -->
|
|||
|
|
<defs>
|
|||
|
|
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|||
|
|
<polygon points="0 0, 10 3.5, 0 7" class="m-default" />
|
|||
|
|
</marker>
|
|||
|
|
<marker id="arrowhead-emphasis" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|||
|
|
<polygon points="0 0, 10 3.5, 0 7" class="m-emphasis" />
|
|||
|
|
</marker>
|
|||
|
|
<marker id="arrowhead-security" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|||
|
|
<polygon points="0 0, 10 3.5, 0 7" class="m-security" />
|
|||
|
|
</marker>
|
|||
|
|
<marker id="arrowhead-dashed" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
|||
|
|
<polygon points="0 0, 10 3.5, 0 7" class="m-dashed" />
|
|||
|
|
</marker>
|
|||
|
|
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
|||
|
|
<path d="M 40 0 L 0 0 0 40" class="c-grid" stroke-width="0.5"/>
|
|||
|
|
</pattern>
|
|||
|
|
</defs>
|
|||
|
|
|
|||
|
|
<!-- Background Grid -->
|
|||
|
|
<rect width="100%" height="100%" fill="url(#grid)" />
|
|||
|
|
|
|||
|
|
<!-- Swimlanes -->
|
|||
|
|
<rect data-graph-role="structural-frame" data-composition-frame-kind="lane" data-composition-frame-id="lane-0" x="40" y="52" width="1177" height="114" rx="10" class="c-lane" stroke-width="1"/>
|
|||
|
|
<text x="54" y="74" class="t-dim" font-size="10" font-weight="600">01 / 任務主流程</text>
|
|||
|
|
|
|||
|
|
<rect data-graph-role="structural-frame" data-composition-frame-kind="lane" data-composition-frame-id="lane-1" x="40" y="186" width="1177" height="106" rx="10" class="c-lane" stroke-width="1"/>
|
|||
|
|
<text x="54" y="208" class="t-dim" font-size="10" font-weight="600">02 / 持續自動化</text>
|
|||
|
|
|
|||
|
|
<rect data-graph-role="structural-frame" data-composition-frame-kind="lane" data-composition-frame-id="lane-2" x="40" y="312" width="1177" height="106" rx="10" class="c-lane" stroke-width="1"/>
|
|||
|
|
<rect data-graph-role="structural-frame" data-composition-frame-kind="exception-lane" data-composition-frame-id="lane-2-exception" x="46" y="318" width="1165" height="94" rx="8" class="c-security-group" stroke-width="1"/>
|
|||
|
|
<text x="54" y="334" class="t-security" font-size="10" font-weight="600">EX / 資料與安全</text>
|
|||
|
|
|
|||
|
|
<!-- Phase headers -->
|
|||
|
|
<line x1="77" y1="35" x2="411.8" y2="35" class="a-default" stroke-width="1.1"/>
|
|||
|
|
<rect x="77" y="27" width="334.8" height="16" rx="4" class="c-mask"/>
|
|||
|
|
<text x="244.4" y="39" class="t-muted" font-size="8" font-weight="600" text-anchor="middle">說明目標</text>
|
|||
|
|
<line x1="541.3999999999999" y1="35" x2="874.1999999999999" y2="35" class="a-emphasis" stroke-width="1.1"/>
|
|||
|
|
<rect x="541.3999999999999" y="27" width="332.80000000000007" height="16" rx="4" class="c-mask"/>
|
|||
|
|
<text x="707.8" y="39" class="t-backend" font-size="8" font-weight="600" text-anchor="middle">理解與編排</text>
|
|||
|
|
<line x1="992.2" y1="35" x2="1204.2" y2="35" class="a-dashed" stroke-width="1.1"/>
|
|||
|
|
<rect x="992.2" y="27" width="212" height="16" rx="4" class="c-mask"/>
|
|||
|
|
<text x="1098.2" y="39" class="t-messagebus" font-size="8" font-weight="600" text-anchor="middle">執行與回傳</text>
|
|||
|
|
|
|||
|
|
<!-- Workflow groups -->
|
|||
|
|
<rect data-graph-role="structural-frame" data-composition-frame-kind="group" data-composition-frame-id="group-0" x="288.8" y="90" width="375.59999999999997" height="72" rx="9" class="c-lane" stroke-width="1"/>
|
|||
|
|
<text x="298.8" y="88" class="t-backend" font-size="7" font-weight="600">Rust Agent Runtime</text>
|
|||
|
|
<rect data-graph-role="structural-frame" data-composition-frame-kind="group" data-composition-frame-id="group-1" x="510.4" y="90" width="394.80000000000007" height="72" rx="9" class="c-security-group" stroke-width="1"/>
|
|||
|
|
<text x="520.4" y="88" class="t-security" font-size="7" font-weight="600">Docker 隔離邊界</text>
|
|||
|
|
<rect data-graph-role="structural-frame" data-composition-frame-kind="group" data-composition-frame-id="group-2" x="284.8" y="350" width="618.4000000000001" height="64" rx="9" class="c-lane" stroke-width="1"/>
|
|||
|
|
<text x="294.8" y="348" class="t-messagebus" font-size="7" font-weight="600">排程、記憶與登入資料</text>
|
|||
|
|
|
|||
|
|
<!-- Edge paths -->
|
|||
|
|
<path data-edge-from="api" data-edge-to="memory" data-edge-label="檢索與寫入" data-edge-key="0" data-edge-id="api-memory" data-composition-points="292.8,127;276.8,127;276.8,434;365.8,434;365.8,406" d="M 292.8 127 L 276.8 127 L 276.8 434 L 365.8 434 L 365.8 406" class="a-dashed" stroke-width="1.4" marker-end="url(#arrowhead-dashed)"/>
|
|||
|
|
<path data-edge-from="api" data-edge-to="supervisor" data-edge-label="受控工具呼叫" data-edge-key="1" data-edge-id="api-supervisor" data-composition-points="365.8,161;365.8,182;587.4,182;587.4,153" d="M 365.8 161 L 365.8 182 L 587.4 182 L 587.4 153" class="a-security" stroke-width="1.4" marker-end="url(#arrowhead-security)"/>
|
|||
|
|
<path data-edge-from="desktop" data-edge-to="result" data-edge-label="螢幕與事件" data-edge-key="2" data-edge-id="desktop-result" data-composition-points="901.2,127;967.2,127" d="M 901.2 127 L 967.2 127" class="a-emphasis" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
|
|||
|
|
<path data-edge-from="schedule" data-edge-to="api" data-edge-label="Cron 觸發" data-edge-key="3" data-edge-id="schedule-api" data-composition-points="365.8,228;365.8,161" d="M 365.8 228 L 365.8 161" class="a-dashed" stroke-width="1.4" marker-end="url(#arrowhead-dashed)"/>
|
|||
|
|
<path data-edge-from="supervisor" data-edge-to="desktop" data-edge-label="啟動・暫停・恢復" data-edge-key="4" data-edge-id="supervisor-desktop" data-composition-points="660.4,127;755.2,127" d="M 660.4 127 L 755.2 127" class="a-security" stroke-width="1.4" marker-end="url(#arrowhead-security)"/>
|
|||
|
|
<path data-edge-from="vault" data-edge-to="desktop" data-edge-label="僅匹配 HTTPS 網域" data-edge-key="5" data-edge-id="vault-desktop" data-composition-points="828.2,354;828.2,161" d="M 828.2 354 L 828.2 161" class="a-security" stroke-width="1.4" marker-end="url(#arrowhead-security)"/>
|
|||
|
|
<path data-edge-from="workspace" data-edge-to="api" data-edge-label="HTTP / WebSocket" data-edge-key="6" data-edge-id="workspace-api" data-composition-points="123,161;123,182;276.8,182;276.8,127;292.8,127" d="M 123 161 L 123 182 L 276.8 182 L 276.8 127 L 292.8 127" class="a-emphasis" stroke-width="1.8" marker-end="url(#arrowhead-emphasis)"/>
|
|||
|
|
|
|||
|
|
<!-- Nodes -->
|
|||
|
|
<g id="node-workspace" data-node-id="workspace" data-node-label="React 工作空間" tabindex="0" role="button" aria-label="Focus React 工作空間, 對話・電腦・記憶, 任務主流程 › 說明目標" aria-pressed="false" data-node-kind="frontend" data-node-sublabel="對話・電腦・記憶" data-node-tag="zh-TW / EN" data-node-context="任務主流程 › 說明目標">
|
|||
|
|
<title>React 工作空間 · 對話・電腦・記憶 · 任務主流程 › 說明目標 · zh-TW / EN</title>
|
|||
|
|
<rect x="48" y="93" width="150" height="68" rx="6" class="c-mask"/>
|
|||
|
|
<rect x="48" y="93" width="150" height="68" rx="6" class="c-frontend" stroke-width="1.5"/>
|
|||
|
|
<g aria-hidden="true" data-semantic-sigil="frontend" class="semantic-sigil s-frontend" transform="translate(54 99) scale(0.6875)">
|
|||
|
|
<rect x="2" y="3" width="12" height="10" rx="2"/>
|
|||
|
|
<path d="M2 6.5h12"/>
|
|||
|
|
<circle cx="4.1" cy="4.8" r=".7" class="sigil-fill"/>
|
|||
|
|
<circle cx="6.3" cy="4.8" r=".7" class="sigil-fill"/>
|
|||
|
|
</g>
|
|||
|
|
<text data-node-label="" data-detail-anchor="" x="123" y="114" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">React 工作空間</text>
|
|||
|
|
<text data-detail="context" x="123" y="131" class="t-muted" font-size="8" text-anchor="middle">對話・電腦・記憶</text>
|
|||
|
|
<text data-detail="fine" x="123" y="149" class="t-frontend" font-size="7" text-anchor="middle">zh-TW / EN</text>
|
|||
|
|
</g>
|
|||
|
|
|
|||
|
|
<g id="node-api" data-node-id="api" data-node-label="API + Agent" tabindex="0" role="button" aria-label="Focus API + Agent, 模型、工具與技能, 任務主流程 › Rust Agent Runtime › 說明目標" aria-pressed="false" data-node-kind="backend" data-node-sublabel="模型、工具與技能" data-node-tag="Rust / Axum" data-node-context="任務主流程 › Rust Agent Runtime › 說明目標">
|
|||
|
|
<title>API + Agent · 模型、工具與技能 · 任務主流程 › Rust Agent Runtime › 說明目標 · Rust / Axum</title>
|
|||
|
|
<rect x="292.8" y="93" width="146" height="68" rx="6" class="c-mask"/>
|
|||
|
|
<rect x="292.8" y="93" width="146" height="68" rx="6" class="c-backend" stroke-width="1.5"/>
|
|||
|
|
<g aria-hidden="true" data-semantic-sigil="backend" class="semantic-sigil s-backend" transform="translate(298.8 99) scale(0.6875)">
|
|||
|
|
<path d="M6 3 3 8l3 5M10 3l3 5-3 5"/>
|
|||
|
|
</g>
|
|||
|
|
<text data-node-label="" data-detail-anchor="" x="365.8" y="114" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">API + Agent</text>
|
|||
|
|
<text data-detail="context" x="365.8" y="131" class="t-muted" font-size="8" text-anchor="middle">模型、工具與技能</text>
|
|||
|
|
<text data-detail="fine" x="365.8" y="149" class="t-backend" font-size="7" text-anchor="middle">Rust / Axum</text>
|
|||
|
|
</g>
|
|||
|
|
|
|||
|
|
<g id="node-supervisor" data-node-id="supervisor" data-node-label="Supervisor" tabindex="0" role="button" aria-label="Focus Supervisor, 資源限制與生命週期, 任務主流程 › Rust Agent Runtime › 理解與編排" aria-pressed="false" data-node-kind="security" data-node-sublabel="資源限制與生命週期" data-node-context="任務主流程 › Rust Agent Runtime › 理解與編排">
|
|||
|
|
<title>Supervisor · 資源限制與生命週期 · 任務主流程 › Rust Agent Runtime › 理解與編排</title>
|
|||
|
|
<rect x="514.4" y="101" width="146" height="52" rx="6" class="c-mask"/>
|
|||
|
|
<rect x="514.4" y="101" width="146" height="52" rx="6" class="c-security" stroke-width="1.5"/>
|
|||
|
|
<g aria-hidden="true" data-semantic-sigil="security" class="semantic-sigil s-security" transform="translate(520.4 107) scale(0.6875)">
|
|||
|
|
<path d="M8 2.2 13 4v3.5c0 3.1-1.8 5.4-5 6.5-3.2-1.1-5-3.4-5-6.5V4Z"/>
|
|||
|
|
<path d="m5.8 8 1.5 1.5 3-3"/>
|
|||
|
|
</g>
|
|||
|
|
<text data-node-label="" data-detail-anchor="" x="587.4" y="122" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">Supervisor</text>
|
|||
|
|
<text data-detail="context" x="587.4" y="139" class="t-muted" font-size="8" text-anchor="middle">資源限制與生命週期</text>
|
|||
|
|
</g>
|
|||
|
|
|
|||
|
|
<g id="node-desktop" data-node-id="desktop" data-node-label="隔離電腦" tabindex="0" role="button" aria-label="Focus 隔離電腦, 瀏覽器、檔案與輸入, 任務主流程 › Docker 隔離邊界 › 理解與編排" aria-pressed="false" data-node-kind="cloud" data-node-sublabel="瀏覽器、檔案與輸入" data-node-tag="noVNC" data-node-context="任務主流程 › Docker 隔離邊界 › 理解與編排">
|
|||
|
|
<title>隔離電腦 · 瀏覽器、檔案與輸入 · 任務主流程 › Docker 隔離邊界 › 理解與編排 · noVNC</title>
|
|||
|
|
<rect x="755.2" y="93" width="146" height="68" rx="6" class="c-mask"/>
|
|||
|
|
<rect x="755.2" y="93" width="146" height="68" rx="6" class="c-cloud" stroke-width="1.5"/>
|
|||
|
|
<g aria-hidden="true" data-semantic-sigil="cloud" class="semantic-sigil s-cloud" transform="translate(761.2 99) scale(0.6875)">
|
|||
|
|
<path d="M4.3 12.5h7.3a2.4 2.4 0 0 0 .2-4.8 4 4 0 0 0-7.5-1.3A3.1 3.1 0 0 0 4.3 12.5Z"/>
|
|||
|
|
</g>
|
|||
|
|
<text data-node-label="" data-detail-anchor="" x="828.2" y="114" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">隔離電腦</text>
|
|||
|
|
<text data-detail="context" x="828.2" y="131" class="t-muted" font-size="8" text-anchor="middle">瀏覽器、檔案與輸入</text>
|
|||
|
|
<text data-detail="fine" x="828.2" y="149" class="t-cloud" font-size="7" text-anchor="middle">noVNC</text>
|
|||
|
|
</g>
|
|||
|
|
|
|||
|
|
<g id="node-result" data-node-id="result" data-node-label="即時結果" tabindex="0" role="button" aria-label="Focus 即時結果, 畫面、訊息與狀態, 任務主流程 › 執行與回傳" aria-pressed="false" data-node-kind="frontend" data-node-sublabel="畫面、訊息與狀態" data-node-context="任務主流程 › 執行與回傳">
|
|||
|
|
<title>即時結果 · 畫面、訊息與狀態 · 任務主流程 › 執行與回傳</title>
|
|||
|
|
<rect x="967.2" y="101" width="142" height="52" rx="6" class="c-mask"/>
|
|||
|
|
<rect x="967.2" y="101" width="142" height="52" rx="6" class="c-frontend" stroke-width="1.5"/>
|
|||
|
|
<g aria-hidden="true" data-semantic-sigil="frontend" class="semantic-sigil s-frontend" transform="translate(973.2 107) scale(0.6875)">
|
|||
|
|
<rect x="2" y="3" width="12" height="10" rx="2"/>
|
|||
|
|
<path d="M2 6.5h12"/>
|
|||
|
|
<circle cx="4.1" cy="4.8" r=".7" class="sigil-fill"/>
|
|||
|
|
<circle cx="6.3" cy="4.8" r=".7" class="sigil-fill"/>
|
|||
|
|
</g>
|
|||
|
|
<text data-node-label="" data-detail-anchor="" x="1038.2" y="122" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">即時結果</text>
|
|||
|
|
<text data-detail="context" x="1038.2" y="139" class="t-muted" font-size="8" text-anchor="middle">畫面、訊息與狀態</text>
|
|||
|
|
</g>
|
|||
|
|
|
|||
|
|
<g id="node-schedule" data-node-id="schedule" data-node-label="排程" tabindex="0" role="button" aria-label="Focus 排程, 到點自動執行, 持續自動化 › 說明目標" aria-pressed="false" data-node-kind="messagebus" data-node-sublabel="到點自動執行" data-node-context="持續自動化 › 說明目標">
|
|||
|
|
<title>排程 · 到點自動執行 · 持續自動化 › 說明目標</title>
|
|||
|
|
<rect x="300.8" y="228" width="130" height="52" rx="6" class="c-mask"/>
|
|||
|
|
<rect x="300.8" y="228" width="130" height="52" rx="6" class="c-messagebus" stroke-width="1.5"/>
|
|||
|
|
<g aria-hidden="true" data-semantic-sigil="messagebus" class="semantic-sigil s-messagebus" transform="translate(306.8 234) scale(0.6875)">
|
|||
|
|
<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/>
|
|||
|
|
<circle cx="5" cy="4.5" r="1" class="sigil-fill"/>
|
|||
|
|
<circle cx="10.5" cy="8" r="1" class="sigil-fill"/>
|
|||
|
|
<circle cx="7" cy="11.5" r="1" class="sigil-fill"/>
|
|||
|
|
</g>
|
|||
|
|
<text data-node-label="" data-detail-anchor="" x="365.8" y="249" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">排程</text>
|
|||
|
|
<text data-detail="context" x="365.8" y="266" class="t-muted" font-size="8" text-anchor="middle">到點自動執行</text>
|
|||
|
|
</g>
|
|||
|
|
|
|||
|
|
<g id="node-memory" data-node-id="memory" data-node-label="記憶與事件" tabindex="0" role="button" aria-label="Focus 記憶與事件, PostgreSQL + pgvector, 資料與安全 › 排程、記憶與登入資料 › 說明目標" aria-pressed="false" data-node-kind="database" data-node-sublabel="PostgreSQL + pgvector" data-node-context="資料與安全 › 排程、記憶與登入資料 › 說明目標">
|
|||
|
|
<title>記憶與事件 · PostgreSQL + pgvector · 資料與安全 › 排程、記憶與登入資料 › 說明目標</title>
|
|||
|
|
<rect x="288.8" y="354" width="154" height="52" rx="6" class="c-mask"/>
|
|||
|
|
<rect x="288.8" y="354" width="154" height="52" rx="6" class="c-database" stroke-width="1.5"/>
|
|||
|
|
<g aria-hidden="true" data-semantic-sigil="database" class="semantic-sigil s-database" transform="translate(294.8 360) scale(0.6875)">
|
|||
|
|
<ellipse cx="8" cy="4" rx="5" ry="2"/>
|
|||
|
|
<path d="M3 4v8c0 1.1 2.2 2 5 2s5-.9 5-2V4M3 8c0 1.1 2.2 2 5 2s5-.9 5-2"/>
|
|||
|
|
</g>
|
|||
|
|
<text data-node-label="" data-detail-anchor="" x="365.8" y="375" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">記憶與事件</text>
|
|||
|
|
<text data-detail="context" x="365.8" y="392" class="t-muted" font-size="8" text-anchor="middle">PostgreSQL + pgvector</text>
|
|||
|
|
</g>
|
|||
|
|
|
|||
|
|
<g id="node-vault" data-node-id="vault" data-node-label="加密憑證庫" tabindex="0" role="button" aria-label="Focus 加密憑證庫, 網站登入資料, 資料與安全 › 排程、記憶與登入資料 › 理解與編排" aria-pressed="false" data-node-kind="security" data-node-sublabel="網站登入資料" data-node-context="資料與安全 › 排程、記憶與登入資料 › 理解與編排">
|
|||
|
|
<title>加密憑證庫 · 網站登入資料 · 資料與安全 › 排程、記憶與登入資料 › 理解與編排</title>
|
|||
|
|
<rect x="757.2" y="354" width="142" height="52" rx="6" class="c-mask"/>
|
|||
|
|
<rect x="757.2" y="354" width="142" height="52" rx="6" class="c-security" stroke-width="1.5"/>
|
|||
|
|
<g aria-hidden="true" data-semantic-sigil="security" class="semantic-sigil s-security" transform="translate(763.2 360) scale(0.6875)">
|
|||
|
|
<path d="M8 2.2 13 4v3.5c0 3.1-1.8 5.4-5 6.5-3.2-1.1-5-3.4-5-6.5V4Z"/>
|
|||
|
|
<path d="m5.8 8 1.5 1.5 3-3"/>
|
|||
|
|
</g>
|
|||
|
|
<text data-node-label="" data-detail-anchor="" x="828.2" y="375" class="t-primary" font-size="11" font-weight="600" text-anchor="middle">加密憑證庫</text>
|
|||
|
|
<text data-detail="context" x="828.2" y="392" class="t-muted" font-size="8" text-anchor="middle">網站登入資料</text>
|
|||
|
|
</g>
|
|||
|
|
|
|||
|
|
<!-- Edge labels -->
|
|||
|
|
<g data-detail="context" data-edge-from="api" data-edge-to="memory" data-edge-label="檢索與寫入" data-edge-key="0" data-edge-id="api-memory">
|
|||
|
|
<rect x="292.3" y="414" width="58" height="14" rx="3" class="c-mask"/>
|
|||
|
|
<text x="321.3" y="424" class="t-database" font-size="8" text-anchor="middle">檢索與寫入</text>
|
|||
|
|
</g>
|
|||
|
|
<g data-detail="context" data-edge-from="api" data-edge-to="supervisor" data-edge-label="受控工具呼叫" data-edge-key="1" data-edge-id="api-supervisor">
|
|||
|
|
<rect x="442.8" y="162" width="67.6" height="14" rx="3" class="c-mask"/>
|
|||
|
|
<text x="476.6" y="172" class="t-security" font-size="8" text-anchor="middle">受控工具呼叫</text>
|
|||
|
|
</g>
|
|||
|
|
<g data-detail="context" data-edge-from="desktop" data-edge-to="result" data-edge-label="螢幕與事件" data-edge-key="2" data-edge-id="desktop-result">
|
|||
|
|
<rect x="905.2" y="107" width="58" height="14" rx="3" class="c-mask"/>
|
|||
|
|
<text x="934.2" y="117" class="t-backend" font-size="8" text-anchor="middle">螢幕與事件</text>
|
|||
|
|
</g>
|
|||
|
|
<g data-detail="context" data-edge-from="schedule" data-edge-to="api" data-edge-label="Cron 觸發" data-edge-key="3" data-edge-id="schedule-api">
|
|||
|
|
<rect x="339.2" y="208" width="53.199999999999996" height="14" rx="3" class="c-mask"/>
|
|||
|
|
<text x="365.8" y="218" class="t-database" font-size="8" text-anchor="middle">Cron 觸發</text>
|
|||
|
|
</g>
|
|||
|
|
<g data-detail="context" data-edge-from="supervisor" data-edge-to="desktop" data-edge-label="啟動・暫停・恢復" data-edge-key="4" data-edge-id="supervisor-desktop">
|
|||
|
|
<rect x="664.4" y="107" width="86.8" height="14" rx="3" class="c-mask"/>
|
|||
|
|
<text x="707.8" y="117" class="t-security" font-size="8" text-anchor="middle">啟動・暫停・恢復</text>
|
|||
|
|
</g>
|
|||
|
|
<g data-detail="context" data-edge-from="vault" data-edge-to="desktop" data-edge-label="僅匹配 HTTPS 網域" data-edge-key="5" data-edge-id="vault-desktop">
|
|||
|
|
<rect x="782.4000000000001" y="334" width="91.6" height="14" rx="3" class="c-mask"/>
|
|||
|
|
<text x="828.2" y="344" class="t-security" font-size="8" text-anchor="middle">僅匹配 HTTPS 網域</text>
|
|||
|
|
</g>
|
|||
|
|
<g data-detail="context" data-edge-from="workspace" data-edge-to="api" data-edge-label="HTTP / WebSocket" data-edge-key="6" data-edge-id="workspace-api">
|
|||
|
|
<rect x="156.5" y="162" width="86.8" height="14" rx="3" class="c-mask"/>
|
|||
|
|
<text x="199.9" y="172" class="t-backend" font-size="8" text-anchor="middle">HTTP / WebSocket</text>
|
|||
|
|
</g>
|
|||
|
|
|
|||
|
|
<!-- Legend -->
|
|||
|
|
<g data-legend="" data-legend-bridge="">
|
|||
|
|
<text x="20" y="442" class="t-primary" font-size="12" font-weight="650">Legend</text>
|
|||
|
|
<g data-legend-semantic-kind="frontend" data-legend-kind="frontend" data-legend-label="User UI" data-legend-x="20" data-legend-baseline="462" data-legend-width="74">
|
|||
|
|
<rect x="20" y="454" width="14" height="9" rx="2" class="c-frontend" stroke-width="1"/>
|
|||
|
|
<text x="42" y="462" class="t-muted" font-size="7.5" font-weight="500">User UI</text>
|
|||
|
|
</g>
|
|||
|
|
<g data-legend-semantic-kind="backend" data-legend-kind="backend" data-legend-label="Agent logic" data-legend-x="101" data-legend-baseline="462" data-legend-width="91">
|
|||
|
|
<rect x="101" y="454" width="14" height="9" rx="2" class="c-backend" stroke-width="1"/>
|
|||
|
|
<text x="123" y="462" class="t-muted" font-size="7.5" font-weight="500">Agent logic</text>
|
|||
|
|
</g>
|
|||
|
|
<g data-legend-semantic-kind="security" data-legend-kind="security" data-legend-label="Policy" data-legend-x="199" data-legend-baseline="462" data-legend-width="70">
|
|||
|
|
<rect x="199" y="454" width="14" height="9" rx="2" class="c-security" stroke-width="1"/>
|
|||
|
|
<text x="221" y="462" class="t-muted" font-size="7.5" font-weight="500">Policy</text>
|
|||
|
|
</g>
|
|||
|
|
<g data-legend-semantic-kind="messagebus" data-legend-kind="messagebus" data-legend-label="Tool action" data-legend-x="276" data-legend-baseline="462" data-legend-width="91">
|
|||
|
|
<rect x="276" y="454" width="14" height="9" rx="2" class="c-messagebus" stroke-width="1"/>
|
|||
|
|
<text x="298" y="462" class="t-muted" font-size="7.5" font-weight="500">Tool action</text>
|
|||
|
|
</g>
|
|||
|
|
<g data-legend-semantic-kind="database" data-legend-kind="database" data-legend-label="Context / trace" data-legend-x="374" data-legend-baseline="462" data-legend-width="109">
|
|||
|
|
<rect x="374" y="454" width="14" height="9" rx="2" class="c-database" stroke-width="1"/>
|
|||
|
|
<text x="396" y="462" class="t-muted" font-size="7.5" font-weight="500">Context / trace</text>
|
|||
|
|
</g>
|
|||
|
|
<g data-legend-semantic-kind="cloud" data-legend-kind="cloud" data-legend-label="Cloud service" data-legend-x="490" data-legend-baseline="462" data-legend-width="100">
|
|||
|
|
<rect x="490" y="454" width="14" height="9" rx="2" class="c-cloud" stroke-width="1"/>
|
|||
|
|
<text x="512" y="462" class="t-muted" font-size="7.5" font-weight="500">Cloud service</text>
|
|||
|
|
</g>
|
|||
|
|
</g>
|
|||
|
|
</svg>
|
|||
|
|
<p class="intent-trace-status no-print" id="intent-trace-status" role="status" aria-live="polite" aria-atomic="true"></p>
|
|||
|
|
<div class="share-chapter-cue no-print" id="share-chapter-cue" hidden role="status" aria-live="polite">
|
|||
|
|
<div class="share-chapter-index">
|
|||
|
|
<span class="share-chapter-state" id="share-chapter-state">Ready</span>
|
|||
|
|
<span class="share-chapter-count" id="share-chapter-count">Chapter 01 / 01</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="share-chapter-copy">
|
|||
|
|
<strong id="share-chapter-label">Guided chapter</strong>
|
|||
|
|
<small id="share-chapter-note"></small>
|
|||
|
|
</div>
|
|||
|
|
<span class="share-chapter-route" id="share-chapter-route"></span>
|
|||
|
|
<span class="share-chapter-progress" aria-hidden="true"><span id="share-chapter-progress-bar"></span></span>
|
|||
|
|
</div>
|
|||
|
|
<div class="diagram-guide no-print" id="diagram-guide" hidden role="dialog" aria-modal="false" aria-labelledby="diagram-guide-title">
|
|||
|
|
<div class="diagram-guide-head">
|
|||
|
|
<span>
|
|||
|
|
<span class="diagram-guide-eyebrow">Diagram guide</span>
|
|||
|
|
<strong class="diagram-guide-title" id="diagram-guide-title">Explore this system</strong>
|
|||
|
|
</span>
|
|||
|
|
<button class="diagram-guide-close" id="diagram-guide-close" type="button" aria-label="Close diagram guide">×</button>
|
|||
|
|
</div>
|
|||
|
|
<p class="diagram-guide-stats" id="diagram-guide-stats">Inspecting compiled semantics</p>
|
|||
|
|
<div class="diagram-guide-actions" id="diagram-guide-actions" role="group" aria-label="Diagram exploration actions">
|
|||
|
|
<button class="diagram-guide-action" type="button" data-guide-action="find" aria-label="Find any node">
|
|||
|
|
<span class="diagram-guide-index" aria-hidden="true">01</span>
|
|||
|
|
<span class="diagram-guide-action-copy"><strong>Find any node</strong><small>Search labels, responsibilities, kinds, and stable IDs.</small></span>
|
|||
|
|
<kbd aria-hidden="true">/</kbd>
|
|||
|
|
</button>
|
|||
|
|
<button class="diagram-guide-action" type="button" data-guide-action="route" aria-label="Trace a directed route">
|
|||
|
|
<span class="diagram-guide-index" aria-hidden="true">02</span>
|
|||
|
|
<span class="diagram-guide-action-copy"><strong>Trace a route</strong><small>Ask how two semantic nodes connect in authored direction.</small></span>
|
|||
|
|
<kbd aria-hidden="true">R</kbd>
|
|||
|
|
</button>
|
|||
|
|
<button class="diagram-guide-action" type="button" data-guide-action="map" aria-label="See the whole system">
|
|||
|
|
<span class="diagram-guide-index" aria-hidden="true">03</span>
|
|||
|
|
<span class="diagram-guide-action-copy"><strong>See the whole system</strong><small>Open Semantic Radar with a live viewport and stable nodes.</small></span>
|
|||
|
|
<kbd aria-hidden="true">M</kbd>
|
|||
|
|
</button>
|
|||
|
|
<button class="diagram-guide-action" type="button" data-guide-action="lens" aria-label="Compare semantic kinds">
|
|||
|
|
<span class="diagram-guide-index" aria-hidden="true">04</span>
|
|||
|
|
<span class="diagram-guide-action-copy"><strong>Compare semantic kinds</strong><small>Count roles, reveal their traffic, and compare direct authored links.</small></span>
|
|||
|
|
<kbd aria-hidden="true">L</kbd>
|
|||
|
|
</button>
|
|||
|
|
<button class="diagram-guide-action" type="button" data-guide-action="story" aria-label="Play the guided story">
|
|||
|
|
<span class="diagram-guide-index" aria-hidden="true">05</span>
|
|||
|
|
<span class="diagram-guide-action-copy"><strong>Play the guided story</strong><small id="diagram-guide-story-copy">Walk the authored chapters and real relationships.</small></span>
|
|||
|
|
<kbd aria-hidden="true">P</kbd>
|
|||
|
|
</button>
|
|||
|
|
<button class="diagram-guide-action" type="button" data-guide-action="present" aria-label="Enter presentation stage">
|
|||
|
|
<span class="diagram-guide-index" aria-hidden="true">06</span>
|
|||
|
|
<span class="diagram-guide-action-copy"><strong>Enter Presentation Stage</strong><small>Give the live diagram the viewport without changing export.</small></span>
|
|||
|
|
<kbd aria-hidden="true">F</kbd>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="diagram-guide-shortcuts" aria-label="Additional keyboard shortcuts">
|
|||
|
|
<span><kbd>E</kbd> Export</span><span><kbd>T</kbd> Theme</span><span><kbd>S</kbd> Style</span><span><kbd>0</kbd> Reset</span><span><kbd>+</kbd> Zoom in</span><span><kbd>-</kbd> Zoom out</span><span><kbd>Esc</kbd> Close</span>
|
|||
|
|
</div>
|
|||
|
|
<p class="diagram-guide-feedback" id="diagram-guide-feedback" aria-live="polite"></p>
|
|||
|
|
</div>
|
|||
|
|
<div class="node-finder no-print" id="node-finder" hidden role="dialog" aria-modal="false" aria-labelledby="node-finder-title">
|
|||
|
|
<div class="node-finder-head">
|
|||
|
|
<strong id="node-finder-title">Find a node</strong>
|
|||
|
|
<button class="node-finder-close" id="node-finder-close" type="button" aria-label="Close node finder">×</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="node-finder-search">
|
|||
|
|
<span aria-hidden="true">⌕</span>
|
|||
|
|
<input class="node-finder-input" id="node-finder-input" type="search" autocomplete="off" spellcheck="false" placeholder="Search labels or IDs" aria-label="Search diagram nodes" aria-describedby="node-finder-status">
|
|||
|
|
<kbd aria-hidden="true">/</kbd>
|
|||
|
|
</div>
|
|||
|
|
<div class="node-finder-results" id="node-finder-results" role="group" aria-label="Diagram nodes"></div>
|
|||
|
|
<p class="node-finder-empty" id="node-finder-empty" hidden>No matching nodes</p>
|
|||
|
|
<p class="node-finder-status" id="node-finder-status" aria-live="polite"></p>
|
|||
|
|
</div>
|
|||
|
|
<div class="focus-chip relationship-lens no-print" id="focus-chip" hidden role="region" aria-labelledby="relationship-lens-title">
|
|||
|
|
<div class="relationship-lens-head">
|
|||
|
|
<div class="relationship-lens-copy">
|
|||
|
|
<span class="relationship-lens-eyebrow">Semantic passport</span>
|
|||
|
|
<strong class="relationship-lens-title" id="relationship-lens-title"><span id="focus-label"></span></strong>
|
|||
|
|
<span class="semantic-passport-detail" id="focus-detail" hidden></span>
|
|||
|
|
<div class="semantic-passport-meta" id="focus-passport-meta" aria-label="Node metadata">
|
|||
|
|
<span id="focus-kind" data-passport="kind"></span>
|
|||
|
|
<span id="focus-context" data-passport="context" hidden></span>
|
|||
|
|
<span id="focus-tag" data-passport="tag" hidden></span>
|
|||
|
|
<span id="focus-brand" data-passport="brand" hidden></span>
|
|||
|
|
<code id="focus-id" data-passport="id"></code>
|
|||
|
|
</div>
|
|||
|
|
<div class="semantic-passport-evidence" id="focus-evidence" hidden aria-label="Verified source evidence">
|
|||
|
|
<div class="semantic-passport-evidence-head">
|
|||
|
|
<span class="semantic-passport-evidence-status">Verified source</span>
|
|||
|
|
<a class="semantic-passport-repository" id="focus-repository" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer"></a>
|
|||
|
|
</div>
|
|||
|
|
<div class="semantic-passport-evidence-links" id="focus-evidence-links"></div>
|
|||
|
|
</div>
|
|||
|
|
<span class="relationship-lens-summary" id="focus-summary" aria-live="polite"></span>
|
|||
|
|
<div class="semantic-passport-reach" id="focus-reach" hidden>
|
|||
|
|
<span class="semantic-passport-reach-label">Authored reach</span>
|
|||
|
|
<div class="semantic-passport-reach-actions" role="group" aria-label="Trace authored reachability">
|
|||
|
|
<button id="btn-reach-upstream" type="button" aria-label="Trace upstream authored reachability" aria-pressed="false">
|
|||
|
|
<span>Upstream</span><strong id="focus-reach-upstream-count">0</strong>
|
|||
|
|
</button>
|
|||
|
|
<button id="btn-reach-downstream" type="button" aria-label="Trace downstream authored reachability" aria-pressed="false">
|
|||
|
|
<span>Downstream</span><strong id="focus-reach-downstream-count">0</strong>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<small class="semantic-passport-reach-status" id="focus-reach-status" hidden aria-live="polite"></small>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="relationship-lens-actions">
|
|||
|
|
<button id="btn-focus-clear" type="button" aria-label="Close semantic passport" title="Close">×</button>
|
|||
|
|
<button id="btn-focus-copy" type="button" aria-label="Copy link to focused node">Copy link</button>
|
|||
|
|
<button id="btn-focus-relations" type="button" aria-label="Show connected relationships" aria-expanded="false" aria-controls="relationship-lens-list">Relations</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="relationship-lens-list" id="relationship-lens-list" aria-label="Connected relationships"></div>
|
|||
|
|
</div>
|
|||
|
|
<div class="route-probe no-print" id="route-probe" hidden role="region" aria-labelledby="route-probe-title" data-state="idle">
|
|||
|
|
<div class="route-probe-head">
|
|||
|
|
<span class="route-probe-copy">
|
|||
|
|
<span class="route-probe-eyebrow">Route probe</span>
|
|||
|
|
<strong class="route-probe-title" id="route-probe-title">Choose a start node</strong>
|
|||
|
|
</span>
|
|||
|
|
<span class="route-probe-actions">
|
|||
|
|
<button class="route-probe-find" id="route-probe-find" type="button" aria-label="Find a route start" data-node-finder-trigger>Find start</button>
|
|||
|
|
<button class="route-probe-copy-link" id="route-probe-copy" type="button" aria-label="Copy link to traced route" hidden>Copy link</button>
|
|||
|
|
<button id="route-probe-clear" type="button" aria-label="Clear route probe">Clear</button>
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="route-probe-path" id="route-probe-path" aria-label="Traced route">
|
|||
|
|
<span class="route-probe-placeholder">Pick two semantic nodes on the diagram</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="route-journey-controls" id="route-journey-controls" hidden role="group" aria-label="Route journey controls" data-playing="false">
|
|||
|
|
<button id="route-journey-prev" type="button" aria-label="Previous route position" title="Previous route position">←</button>
|
|||
|
|
<button class="route-journey-play" id="route-journey-play" type="button" aria-label="Play route journey" aria-pressed="false" title="Play route journey">
|
|||
|
|
<span id="route-journey-play-icon" aria-hidden="true">▶</span>
|
|||
|
|
<span id="route-journey-play-label">Journey</span>
|
|||
|
|
</button>
|
|||
|
|
<button id="route-journey-next" type="button" aria-label="Next route position" title="Next route position">→</button>
|
|||
|
|
<button class="route-journey-overview" id="route-journey-overview" type="button" aria-label="Show complete route overview" title="Show complete route overview">Overview</button>
|
|||
|
|
</div>
|
|||
|
|
<small class="route-probe-status" id="route-probe-status" aria-live="polite">Choose the source, then the destination. Direction matters.</small>
|
|||
|
|
</div>
|
|||
|
|
<div class="semantic-lens no-print" id="semantic-lens" hidden role="dialog" aria-modal="false" aria-labelledby="semantic-lens-title">
|
|||
|
|
<div class="semantic-lens-head">
|
|||
|
|
<span>
|
|||
|
|
<span class="semantic-lens-eyebrow">Semantic lens</span>
|
|||
|
|
<strong class="semantic-lens-title" id="semantic-lens-title">Compare system roles</strong>
|
|||
|
|
</span>
|
|||
|
|
<button class="semantic-lens-close" id="semantic-lens-close" type="button" aria-label="Close semantic lens">×</button>
|
|||
|
|
</div>
|
|||
|
|
<p class="semantic-lens-instruction">Choose up to two semantic kinds. One reveals its real traffic; two compare only direct authored relationships.</p>
|
|||
|
|
<div class="semantic-lens-kinds" id="semantic-lens-kinds" role="group" aria-label="Semantic kinds"></div>
|
|||
|
|
<p class="semantic-lens-status" id="semantic-lens-status" aria-live="polite">Choose a kind to inspect its nodes and touching relationships.</p>
|
|||
|
|
<div class="semantic-lens-actions">
|
|||
|
|
<button id="semantic-lens-copy" type="button" aria-label="Copy link to semantic lens" hidden>Copy link</button>
|
|||
|
|
<button id="semantic-lens-clear" type="button" aria-label="Clear semantic lens">Clear</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="overview-map no-print" id="overview-map" hidden role="region" aria-labelledby="overview-map-title">
|
|||
|
|
<div class="overview-map-head">
|
|||
|
|
<span class="overview-map-live" aria-hidden="true"></span>
|
|||
|
|
<span class="overview-map-copy">
|
|||
|
|
<strong id="overview-map-title">Semantic radar</strong>
|
|||
|
|
<small id="overview-map-status" aria-live="polite">Building overview</small>
|
|||
|
|
</span>
|
|||
|
|
<button class="overview-map-expand" id="overview-map-expand" type="button" aria-label="Open full semantic radar">Open radar</button>
|
|||
|
|
<button class="overview-map-close" id="overview-map-close" type="button" aria-label="Close semantic radar">×</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="overview-map-surface" id="overview-map-surface" tabindex="0" role="group" aria-label="Diagram overview. Click a node to focus it, or use arrow keys to pan."></div>
|
|||
|
|
<div class="overview-map-hint" aria-hidden="true"><span>Click node</span><span>Drag to pan</span></div>
|
|||
|
|
</div>
|
|||
|
|
<p class="overview-map-feedback no-print" id="overview-map-feedback" role="status" aria-live="polite" hidden>Semantic radar needs more MAP space.</p>
|
|||
|
|
<div class="diagram-nav no-print" role="toolbar" aria-label="Diagram view controls">
|
|||
|
|
<button id="btn-route-probe" type="button" aria-label="Trace a directed route" aria-pressed="false" aria-controls="route-probe" title="Trace route (R)">PATH</button>
|
|||
|
|
<button id="btn-overview-map" type="button" aria-label="Open semantic radar" aria-expanded="false" aria-controls="overview-map" title="Semantic radar (M)">MAP</button>
|
|||
|
|
<button id="btn-semantic-lens" type="button" aria-label="Open semantic lens" aria-haspopup="dialog" aria-expanded="false" aria-pressed="false" aria-controls="semantic-lens" title="Semantic lens (L)">LENS</button>
|
|||
|
|
<button id="btn-node-finder" type="button" aria-label="Find a node" aria-haspopup="dialog" aria-expanded="false" aria-controls="node-finder" title="Find a node (/)" data-node-finder-trigger><span class="diagram-nav-icon find" aria-hidden="true"></span></button>
|
|||
|
|
<button id="btn-diagram-guide" type="button" aria-label="Open diagram guide" aria-haspopup="dialog" aria-expanded="false" aria-controls="diagram-guide" title="Diagram guide (?)"><span class="diagram-nav-icon guide" aria-hidden="true"></span></button>
|
|||
|
|
<button data-view="out" type="button" aria-label="Zoom out" title="Zoom out (-)"><span class="diagram-nav-icon minus" aria-hidden="true"></span></button>
|
|||
|
|
<button data-view="reset" type="button" aria-label="Reset diagram view" title="Reset view (0)"><span class="diagram-nav-detail" data-view-detail hidden>READ</span><span class="diagram-nav-percent" data-view-percent>100%</span></button>
|
|||
|
|
<button data-view="in" type="button" aria-label="Zoom in" title="Zoom in (+)"><span class="diagram-nav-icon plus" aria-hidden="true"></span></button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Info Cards -->
|
|||
|
|
<div class="cards">
|
|||
|
|
<div class="card">
|
|||
|
|
<div class="card-header">
|
|||
|
|
<div class="card-dot emerald"></div>
|
|||
|
|
<h3>看得見,也接得回來</h3>
|
|||
|
|
</div>
|
|||
|
|
<ul>
|
|||
|
|
<li>• 即時觀看 Agent 桌面與目前步驟</li>
|
|||
|
|
<li>• 需要時由使用者接管,再交還給 Agent</li>
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="card">
|
|||
|
|
<div class="card-header">
|
|||
|
|
<div class="card-dot violet"></div>
|
|||
|
|
<h3>從一次任務到持續自動化</h3>
|
|||
|
|
</div>
|
|||
|
|
<ul>
|
|||
|
|
<li>• 示範操作可整理成可重用技能</li>
|
|||
|
|
<li>• 排程、長期記憶與 MCP 延伸工作能力</li>
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
var Archify = {};
|
|||
|
|
var archifyI18nData = (function () {
|
|||
|
|
var node = document.getElementById('archify-i18n-data');
|
|||
|
|
try { return JSON.parse(node ? node.textContent : '{}'); }
|
|||
|
|
catch (_) { return { locale: 'en', messages: {} }; }
|
|||
|
|
})();
|
|||
|
|
Archify.locale = archifyI18nData.locale || 'en';
|
|||
|
|
|
|||
|
|
function viewerText(key, values) {
|
|||
|
|
var messages = archifyI18nData.messages || {};
|
|||
|
|
var template = Object.prototype.hasOwnProperty.call(messages, key) ? messages[key] : key;
|
|||
|
|
return String(template).replace(/\{([a-zA-Z0-9_]+)\}/g, function (match, name) {
|
|||
|
|
return values && Object.prototype.hasOwnProperty.call(values, name) ? String(values[name]) : match;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function viewerCount(key, count, values) {
|
|||
|
|
var suffix = Number(count) === 1 ? '.one' : '.other';
|
|||
|
|
var payload = Object.assign({}, values || {}, { count: count });
|
|||
|
|
return viewerText(key + suffix, payload);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function viewerKindLabel(value) {
|
|||
|
|
var normalized = String(value || 'node').toLowerCase();
|
|||
|
|
var knownKey = 'viewer.kind.' + normalized;
|
|||
|
|
var known = viewerText(knownKey);
|
|||
|
|
if (known !== knownKey) return known;
|
|||
|
|
return normalized
|
|||
|
|
.replace(/messagebus/gi, 'message bus')
|
|||
|
|
.replace(/[-_]+/g, ' ')
|
|||
|
|
.replace(/\b\w/g, function (letter) { return letter.toUpperCase(); });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function hasDrawableGeometry(element) {
|
|||
|
|
if (!element) return false;
|
|||
|
|
var geometries = /^(path|line|polyline)$/i.test(element.tagName)
|
|||
|
|
? [element]
|
|||
|
|
: Array.prototype.slice.call(element.querySelectorAll('path, line, polyline'));
|
|||
|
|
return geometries.some(function (geometry) {
|
|||
|
|
var source = geometry.tagName.toLowerCase() === 'path'
|
|||
|
|
? geometry.getAttribute('d')
|
|||
|
|
: geometry.tagName.toLowerCase() === 'polyline'
|
|||
|
|
? geometry.getAttribute('points')
|
|||
|
|
: [geometry.getAttribute('x1'), geometry.getAttribute('y1'), geometry.getAttribute('x2'), geometry.getAttribute('y2')].join(' ');
|
|||
|
|
if (!source || /(?:^|[^a-z])(?:nan|infinity)(?:[^a-z]|$)/i.test(source) || typeof geometry.getTotalLength !== 'function') return false;
|
|||
|
|
try {
|
|||
|
|
var length = Number(geometry.getTotalLength());
|
|||
|
|
return Number.isFinite(length) && length > 0;
|
|||
|
|
} catch (_) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ============================================================
|
|||
|
|
Visual style try-on — one topology, four bundled presets.
|
|||
|
|
The reader's choice is intentionally session-only: it updates the
|
|||
|
|
live page and canonical SVG together, but never rewrites the source,
|
|||
|
|
URL, or a later diagram's authored default.
|
|||
|
|
============================================================ */
|
|||
|
|
Archify.preset = (function () {
|
|||
|
|
var PRESETS = ['classic', 'signal-flow', 'blueprint', 'editorial'];
|
|||
|
|
var LABELS = {
|
|||
|
|
classic: viewerText('viewer.preset.classic.short'),
|
|||
|
|
'signal-flow': viewerText('viewer.preset.flow.short'),
|
|||
|
|
blueprint: viewerText('viewer.preset.blueprint'),
|
|||
|
|
editorial: viewerText('viewer.preset.editorial')
|
|||
|
|
};
|
|||
|
|
var html = document.documentElement;
|
|||
|
|
var svg = document.querySelector('.diagram-container svg');
|
|||
|
|
var btn = document.getElementById('btn-preset');
|
|||
|
|
var label = document.getElementById('preset-label');
|
|||
|
|
var menu = document.getElementById('preset-menu');
|
|||
|
|
var options = function () {
|
|||
|
|
return Array.prototype.slice.call(menu.querySelectorAll('[data-preset-value]'));
|
|||
|
|
};
|
|||
|
|
var authored = PRESETS.indexOf(html.getAttribute('data-preset')) >= 0
|
|||
|
|
? html.getAttribute('data-preset')
|
|||
|
|
: 'classic';
|
|||
|
|
|
|||
|
|
function current() {
|
|||
|
|
var value = html.getAttribute('data-preset');
|
|||
|
|
return PRESETS.indexOf(value) >= 0 ? value : authored;
|
|||
|
|
}
|
|||
|
|
function nextAfter(preset) {
|
|||
|
|
return PRESETS[(PRESETS.indexOf(preset) + 1) % PRESETS.length];
|
|||
|
|
}
|
|||
|
|
function apply(preset) {
|
|||
|
|
if (html.getAttribute('data-embed') === 'true') return false;
|
|||
|
|
if (PRESETS.indexOf(preset) < 0) return false;
|
|||
|
|
html.setAttribute('data-preset', preset);
|
|||
|
|
svg.setAttribute('data-preset', preset);
|
|||
|
|
btn.setAttribute('data-preset-option', preset);
|
|||
|
|
label.textContent = LABELS[preset];
|
|||
|
|
btn.setAttribute('aria-label', viewerText('viewer.preset.current', { style: LABELS[preset] }));
|
|||
|
|
btn.title = viewerText('viewer.preset.choose.title');
|
|||
|
|
options().forEach(function (option) {
|
|||
|
|
var selected = option.getAttribute('data-preset-value') === preset;
|
|||
|
|
option.setAttribute('aria-checked', String(selected));
|
|||
|
|
});
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function cycle() { return apply(nextAfter(current())); }
|
|||
|
|
|
|||
|
|
function isOpen() { return menu.classList.contains('open'); }
|
|||
|
|
function open(focusLast) {
|
|||
|
|
if (html.getAttribute('data-embed') === 'true') return false;
|
|||
|
|
if (Archify.exportMenu && Archify.exportMenu.isOpen()) Archify.exportMenu.close(false);
|
|||
|
|
menu.classList.add('open');
|
|||
|
|
btn.setAttribute('aria-expanded', 'true');
|
|||
|
|
var available = options();
|
|||
|
|
var selected = available.find(function (option) { return option.getAttribute('aria-checked') === 'true'; });
|
|||
|
|
var target = focusLast ? available[available.length - 1] : (selected || available[0]);
|
|||
|
|
if (target) target.focus();
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function close(focusTrigger) {
|
|||
|
|
menu.classList.remove('open');
|
|||
|
|
btn.setAttribute('aria-expanded', 'false');
|
|||
|
|
if (focusTrigger) btn.focus();
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
apply(authored);
|
|||
|
|
btn.addEventListener('click', function (e) {
|
|||
|
|
e.stopPropagation();
|
|||
|
|
if (isOpen()) close(false);
|
|||
|
|
else open(false);
|
|||
|
|
});
|
|||
|
|
btn.addEventListener('keydown', function (e) {
|
|||
|
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
if (!isOpen()) open(e.key === 'ArrowUp');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
document.addEventListener('click', function (e) {
|
|||
|
|
if (!menu.contains(e.target) && e.target !== btn) close(false);
|
|||
|
|
});
|
|||
|
|
menu.addEventListener('click', function (e) {
|
|||
|
|
var option = e.target.closest('[data-preset-value]');
|
|||
|
|
if (!option || !menu.contains(option)) return;
|
|||
|
|
if (apply(option.getAttribute('data-preset-value'))) close(true);
|
|||
|
|
});
|
|||
|
|
menu.addEventListener('keydown', function (e) {
|
|||
|
|
if (e.key === 'Escape') { e.preventDefault(); close(true); return; }
|
|||
|
|
if (e.key === 'Tab') { close(false); return; }
|
|||
|
|
var available = options();
|
|||
|
|
var active = available.indexOf(document.activeElement);
|
|||
|
|
switch (e.key) {
|
|||
|
|
case 'ArrowDown':
|
|||
|
|
e.preventDefault();
|
|||
|
|
if (available.length) available[(active + 1 + available.length) % available.length].focus();
|
|||
|
|
break;
|
|||
|
|
case 'ArrowUp':
|
|||
|
|
e.preventDefault();
|
|||
|
|
if (available.length) available[(active - 1 + available.length) % available.length].focus();
|
|||
|
|
break;
|
|||
|
|
case 'Home':
|
|||
|
|
e.preventDefault();
|
|||
|
|
if (available[0]) available[0].focus();
|
|||
|
|
break;
|
|||
|
|
case 'End':
|
|||
|
|
e.preventDefault();
|
|||
|
|
if (available.length) available[available.length - 1].focus();
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
return { cycle: cycle, apply: apply, current: current, authored: authored, open: open, close: close, isOpen: isOpen };
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* ============================================================
|
|||
|
|
Theme toggle — persists to localStorage, respects system pref
|
|||
|
|
============================================================ */
|
|||
|
|
|
|||
|
|
Archify.theme = (function () {
|
|||
|
|
var STORAGE_KEY = 'archify-theme';
|
|||
|
|
var html = document.documentElement;
|
|||
|
|
var btn = document.getElementById('btn-theme');
|
|||
|
|
var label = document.getElementById('theme-label');
|
|||
|
|
|
|||
|
|
// localStorage can throw (blocked cookies, sandboxed iframes); the whole
|
|||
|
|
// script element dies on an uncaught error, so guard every access.
|
|||
|
|
function readStored() {
|
|||
|
|
try { return localStorage.getItem(STORAGE_KEY); } catch (_) { return null; }
|
|||
|
|
}
|
|||
|
|
function writeStored(value) {
|
|||
|
|
try { localStorage.setItem(STORAGE_KEY, value); } catch (_) {}
|
|||
|
|
}
|
|||
|
|
function urlOverride() {
|
|||
|
|
// URL param wins (useful for deterministic screenshots / share links)
|
|||
|
|
try {
|
|||
|
|
var param = new URLSearchParams(window.location.search).get('theme');
|
|||
|
|
if (param === 'light' || param === 'dark') return param;
|
|||
|
|
} catch (_) {}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resolveInitial() {
|
|||
|
|
var fromUrl = urlOverride();
|
|||
|
|
if (fromUrl) return fromUrl;
|
|||
|
|
var saved = readStored();
|
|||
|
|
if (saved === 'light' || saved === 'dark') return saved;
|
|||
|
|
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function apply(theme) {
|
|||
|
|
html.setAttribute('data-theme', theme);
|
|||
|
|
label.textContent = viewerText(theme === 'dark' ? 'viewer.theme.dark' : 'viewer.theme.light');
|
|||
|
|
btn.setAttribute('aria-pressed', theme === 'light' ? 'true' : 'false');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function toggle() {
|
|||
|
|
var next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
|||
|
|
apply(next);
|
|||
|
|
writeStored(next);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
apply(resolveInitial());
|
|||
|
|
btn.addEventListener('click', toggle);
|
|||
|
|
|
|||
|
|
// Follow live OS theme changes while the user has no explicit preference.
|
|||
|
|
try {
|
|||
|
|
var media = window.matchMedia('(prefers-color-scheme: light)');
|
|||
|
|
var onChange = function (e) {
|
|||
|
|
var saved = readStored();
|
|||
|
|
if (urlOverride() || saved === 'light' || saved === 'dark') return;
|
|||
|
|
apply(e.matches ? 'light' : 'dark');
|
|||
|
|
};
|
|||
|
|
if (media.addEventListener) media.addEventListener('change', onChange);
|
|||
|
|
else if (media.addListener) media.addListener(onChange);
|
|||
|
|
} catch (_) {}
|
|||
|
|
|
|||
|
|
return { toggle: toggle };
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* ============================================================
|
|||
|
|
Export — Share Card / PNG / JPEG / WebP / SVG / WebM and clipboard
|
|||
|
|
|
|||
|
|
Raster exports are always rendered at 4x source resolution for
|
|||
|
|
maximum sharpness. The trick: we set the serialized SVG's
|
|||
|
|
`width`/`height` to viewBox * 4 so the browser rasterizes the
|
|||
|
|
vectors at that resolution natively. drawImage then draws at
|
|||
|
|
the image's natural size (no upscaling = no blur).
|
|||
|
|
|
|||
|
|
JPEG/WebP paint the current theme's background explicitly since
|
|||
|
|
those formats have no alpha channel.
|
|||
|
|
============================================================ */
|
|||
|
|
(function () {
|
|||
|
|
var RASTER_SCALE = 4;
|
|||
|
|
var MOTION_DURATION = 6000;
|
|||
|
|
var MOTION_FPS = 30;
|
|||
|
|
var SHARE_CARD_WIDTH = 1200;
|
|||
|
|
var SHARE_CARD_HEIGHT = 630;
|
|||
|
|
var SHARE_CARD_PADDING = 36;
|
|||
|
|
var SHARE_CARD_HEADER = 112;
|
|||
|
|
|
|||
|
|
function exportError(key, values) {
|
|||
|
|
var error = new Error(viewerText(key, values));
|
|||
|
|
error.archifyViewerMessage = true;
|
|||
|
|
return error;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function exportMessage(error) {
|
|||
|
|
return error && error.archifyViewerMessage
|
|||
|
|
? error.message
|
|||
|
|
: viewerText('viewer.export.unknown');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function diagramFilename() {
|
|||
|
|
var title = (document.title || 'diagram')
|
|||
|
|
.replace(/\s+Architecture(\s+Diagram)?$/i, '')
|
|||
|
|
.replace(/\s+Diagram$/i, '')
|
|||
|
|
.trim();
|
|||
|
|
return title.replace(/[^a-z0-9_\-]+/gi, '-')
|
|||
|
|
.toLowerCase()
|
|||
|
|
.replace(/^-+|-+$/g, '') || 'diagram';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function currentBg() {
|
|||
|
|
return getComputedStyle(document.body).backgroundColor || '#ffffff';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Serialize the main SVG into a standalone document.
|
|||
|
|
*
|
|||
|
|
* Two modes:
|
|||
|
|
* - Default (opts.autoTheme=false) — locks the serialized SVG to the
|
|||
|
|
* viewer's current theme. Used by the raster pipeline
|
|||
|
|
* (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
|
|||
|
|
* `@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".
|
|||
|
|
*/
|
|||
|
|
function applyRouteSnapshot(clone, snapshot) {
|
|||
|
|
if (!snapshot || !Array.isArray(snapshot.nodeIds) || !Array.isArray(snapshot.edges) ||
|
|||
|
|
snapshot.nodeIds.length < 2 || snapshot.edges.length !== snapshot.nodeIds.length - 1 ||
|
|||
|
|
snapshot.hops !== snapshot.edges.length ||
|
|||
|
|
!snapshot.source || !snapshot.target ||
|
|||
|
|
snapshot.source.id !== snapshot.nodeIds[0] ||
|
|||
|
|
snapshot.target.id !== snapshot.nodeIds[snapshot.nodeIds.length - 1]) return false;
|
|||
|
|
|
|||
|
|
clone.removeAttribute('data-animation');
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-animate]'), function (element) {
|
|||
|
|
element.removeAttribute('data-animate');
|
|||
|
|
element.style.removeProperty('--step');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
var cloneNodes = Array.prototype.slice.call(clone.querySelectorAll('[data-node-id]'));
|
|||
|
|
var cloneEdges = Array.prototype.slice.call(clone.querySelectorAll('[data-edge-key]'));
|
|||
|
|
var nodeIds = Object.create(null);
|
|||
|
|
var edgeKeys = Object.create(null);
|
|||
|
|
var nodeMatches = [];
|
|||
|
|
var edgeMatches = [];
|
|||
|
|
|
|||
|
|
for (var nodeIndex = 0; nodeIndex < snapshot.nodeIds.length; nodeIndex++) {
|
|||
|
|
var nodeId = snapshot.nodeIds[nodeIndex];
|
|||
|
|
if (typeof nodeId !== 'string' || !nodeId || nodeIds[nodeId]) return false;
|
|||
|
|
var matchedNodes = cloneNodes.filter(function (candidate) {
|
|||
|
|
return candidate.getAttribute('data-node-id') === nodeId;
|
|||
|
|
});
|
|||
|
|
if (matchedNodes.length !== 1) return false;
|
|||
|
|
nodeIds[nodeId] = true;
|
|||
|
|
nodeMatches.push(matchedNodes);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for (var edgeIndex = 0; edgeIndex < snapshot.edges.length; edgeIndex++) {
|
|||
|
|
var edge = snapshot.edges[edgeIndex];
|
|||
|
|
var fromId = snapshot.nodeIds[edgeIndex];
|
|||
|
|
var toId = snapshot.nodeIds[edgeIndex + 1];
|
|||
|
|
if (!edge || typeof edge.key !== 'string' || !edge.key || edgeKeys[edge.key] ||
|
|||
|
|
edge.from !== fromId || edge.to !== toId) return false;
|
|||
|
|
var matchedEdges = cloneEdges.filter(function (candidate) {
|
|||
|
|
return candidate.getAttribute('data-edge-key') === edge.key;
|
|||
|
|
});
|
|||
|
|
var drawableMatches = matchedEdges.filter(hasDrawableGeometry);
|
|||
|
|
if (!matchedEdges.length || drawableMatches.length !== 1 || !matchedEdges.every(function (candidate) {
|
|||
|
|
return candidate.getAttribute('data-edge-from') === edge.from &&
|
|||
|
|
candidate.getAttribute('data-edge-to') === edge.to &&
|
|||
|
|
(candidate.getAttribute('data-edge-id') || '') === (edge.id || '');
|
|||
|
|
})) return false;
|
|||
|
|
edgeKeys[edge.key] = true;
|
|||
|
|
edgeMatches.push(matchedEdges);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
clone.setAttribute('data-share-route', '');
|
|||
|
|
nodeMatches.forEach(function (matches, step) {
|
|||
|
|
matches.forEach(function (element) {
|
|||
|
|
element.setAttribute('data-share-route-match', '');
|
|||
|
|
element.setAttribute('data-share-route-step', String(step));
|
|||
|
|
if (step === 0) element.setAttribute('data-share-route-start', '');
|
|||
|
|
else if (step === snapshot.nodeIds.length - 1) element.setAttribute('data-share-route-end', '');
|
|||
|
|
else element.setAttribute('data-share-route-middle', '');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
edgeMatches.forEach(function (matches, step) {
|
|||
|
|
matches.forEach(function (element) {
|
|||
|
|
element.setAttribute('data-share-route-match', '');
|
|||
|
|
element.setAttribute('data-share-route-step', String(step));
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return clone.hasAttribute('data-share-route') &&
|
|||
|
|
!clone.hasAttribute('data-animation') &&
|
|||
|
|
!clone.hasAttribute('data-route-active') &&
|
|||
|
|
!clone.hasAttribute('data-route-journey') &&
|
|||
|
|
clone.querySelectorAll('[data-animate], [data-route-match], [data-route-step], [data-route-start], [data-route-end], [data-route-journey-state], [data-route-journey-current], [data-route-journey-overlay]').length === 0 &&
|
|||
|
|
clone.querySelectorAll('[data-share-route-match]').length ===
|
|||
|
|
nodeMatches.reduce(function (count, matches) { return count + matches.length; }, 0) +
|
|||
|
|
edgeMatches.reduce(function (count, matches) { return count + matches.length; }, 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function applyReachSnapshot(clone, snapshot) {
|
|||
|
|
if (!snapshot || (snapshot.direction !== 'upstream' && snapshot.direction !== 'downstream') ||
|
|||
|
|
!snapshot.origin || typeof snapshot.origin.id !== 'string' || !snapshot.origin.id ||
|
|||
|
|
typeof snapshot.origin.label !== 'string' || !snapshot.origin.label.trim() ||
|
|||
|
|
!Array.isArray(snapshot.nodeIds) || snapshot.nodeIds.length < 2 ||
|
|||
|
|
!Array.isArray(snapshot.edges) || !snapshot.edges.length ||
|
|||
|
|
!snapshot.depths || typeof snapshot.depths !== 'object' ||
|
|||
|
|
!Number.isInteger(snapshot.maxDepth) || snapshot.maxDepth < 1 ||
|
|||
|
|
snapshot.nodeIds[0] !== snapshot.origin.id) return false;
|
|||
|
|
|
|||
|
|
clone.removeAttribute('data-animation');
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-animate]'), function (element) {
|
|||
|
|
element.removeAttribute('data-animate');
|
|||
|
|
element.style.removeProperty('--step');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
var cloneNodes = Array.prototype.slice.call(clone.querySelectorAll('[data-node-id]'));
|
|||
|
|
var cloneEdges = Array.prototype.slice.call(clone.querySelectorAll('[data-edge-key]'));
|
|||
|
|
var nodeIds = Object.create(null);
|
|||
|
|
var edgeKeys = Object.create(null);
|
|||
|
|
var nodeMatches = [];
|
|||
|
|
var edgeMatches = [];
|
|||
|
|
var measuredMaxDepth = 0;
|
|||
|
|
|
|||
|
|
for (var nodeIndex = 0; nodeIndex < snapshot.nodeIds.length; nodeIndex++) {
|
|||
|
|
var nodeId = snapshot.nodeIds[nodeIndex];
|
|||
|
|
var depth = snapshot.depths[nodeId];
|
|||
|
|
if (typeof nodeId !== 'string' || !nodeId || nodeIds[nodeId] ||
|
|||
|
|
!Number.isInteger(depth) || depth < 0 || depth > snapshot.maxDepth ||
|
|||
|
|
(nodeId === snapshot.origin.id ? depth !== 0 : depth < 1)) return false;
|
|||
|
|
var matchedNodes = cloneNodes.filter(function (candidate) {
|
|||
|
|
return candidate.getAttribute('data-node-id') === nodeId;
|
|||
|
|
});
|
|||
|
|
if (matchedNodes.length !== 1) return false;
|
|||
|
|
nodeIds[nodeId] = true;
|
|||
|
|
measuredMaxDepth = Math.max(measuredMaxDepth, depth);
|
|||
|
|
nodeMatches.push({ elements: matchedNodes, id: nodeId, depth: depth });
|
|||
|
|
}
|
|||
|
|
if (measuredMaxDepth !== snapshot.maxDepth) return false;
|
|||
|
|
|
|||
|
|
for (var edgeIndex = 0; edgeIndex < snapshot.edges.length; edgeIndex++) {
|
|||
|
|
var edge = snapshot.edges[edgeIndex];
|
|||
|
|
if (!edge || typeof edge.key !== 'string' || !edge.key || edgeKeys[edge.key] ||
|
|||
|
|
!nodeIds[edge.from] || !nodeIds[edge.to] ||
|
|||
|
|
!Number.isInteger(edge.depth) || edge.depth < 1 || edge.depth > snapshot.maxDepth ||
|
|||
|
|
edge.depth !== Math.max(snapshot.depths[edge.from], snapshot.depths[edge.to])) return false;
|
|||
|
|
var matchedEdges = cloneEdges.filter(function (candidate) {
|
|||
|
|
return candidate.getAttribute('data-edge-key') === edge.key;
|
|||
|
|
});
|
|||
|
|
var drawableMatches = matchedEdges.filter(hasDrawableGeometry);
|
|||
|
|
if (!matchedEdges.length || drawableMatches.length !== 1 || !matchedEdges.every(function (candidate) {
|
|||
|
|
return candidate.getAttribute('data-edge-from') === edge.from &&
|
|||
|
|
candidate.getAttribute('data-edge-to') === edge.to &&
|
|||
|
|
(candidate.getAttribute('data-edge-id') || '') === (edge.id || '');
|
|||
|
|
})) return false;
|
|||
|
|
edgeKeys[edge.key] = true;
|
|||
|
|
edgeMatches.push({ elements: matchedEdges, depth: edge.depth });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
clone.setAttribute('data-share-reach', snapshot.direction);
|
|||
|
|
nodeMatches.forEach(function (match) {
|
|||
|
|
match.elements.forEach(function (element) {
|
|||
|
|
element.setAttribute('data-share-reach-match', '');
|
|||
|
|
element.setAttribute('data-share-reach-depth', String(match.depth));
|
|||
|
|
if (match.id === snapshot.origin.id) element.setAttribute('data-share-reach-origin', '');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
edgeMatches.forEach(function (match) {
|
|||
|
|
match.elements.forEach(function (element) {
|
|||
|
|
element.setAttribute('data-share-reach-match', '');
|
|||
|
|
element.setAttribute('data-share-reach-depth', String(match.depth));
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return clone.getAttribute('data-share-reach') === snapshot.direction &&
|
|||
|
|
!clone.hasAttribute('data-animation') &&
|
|||
|
|
!clone.hasAttribute('data-reach-active') &&
|
|||
|
|
clone.querySelectorAll('[data-animate], [data-reach-match], [data-reach-origin], [data-reach-depth]').length === 0 &&
|
|||
|
|
clone.querySelectorAll('[data-share-reach-origin]').length === 1 &&
|
|||
|
|
clone.querySelectorAll('[data-share-reach-match]').length ===
|
|||
|
|
nodeMatches.reduce(function (count, match) { return count + match.elements.length; }, 0) +
|
|||
|
|
edgeMatches.reduce(function (count, match) { return count + match.elements.length; }, 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function serializeSvg(scale, opts) {
|
|||
|
|
// scale: integer multiplier 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 svg = document.querySelector('.diagram-container svg');
|
|||
|
|
var clone = svg.cloneNode(true);
|
|||
|
|
|
|||
|
|
// View transforms and neighborhood focus are HTML exploration state,
|
|||
|
|
// never part of a downloaded full-diagram artifact.
|
|||
|
|
clone.style.removeProperty('transform');
|
|||
|
|
clone.style.removeProperty('clip-path');
|
|||
|
|
clone.removeAttribute('data-view-scale');
|
|||
|
|
clone.removeAttribute('data-focus-active');
|
|||
|
|
clone.removeAttribute('data-reach-active');
|
|||
|
|
clone.removeAttribute('data-lens-active');
|
|||
|
|
clone.removeAttribute('data-lens-flow-count');
|
|||
|
|
clone.removeAttribute('data-lens-flow-density');
|
|||
|
|
clone.removeAttribute('data-legend-preview-active');
|
|||
|
|
clone.removeAttribute('data-relationship-preview-active');
|
|||
|
|
clone.removeAttribute('data-relationship-direct-active');
|
|||
|
|
clone.removeAttribute('data-relationship-pin-active');
|
|||
|
|
clone.removeAttribute('data-intent-trace-active');
|
|||
|
|
clone.removeAttribute('data-route-picking');
|
|||
|
|
clone.removeAttribute('data-route-active');
|
|||
|
|
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();
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-route-probe-overlay]'), function (el) {
|
|||
|
|
el.remove();
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-route-journey-overlay]'), function (el) {
|
|||
|
|
el.remove();
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-semantic-lens-overlay]'), function (el) {
|
|||
|
|
el.remove();
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge-runtime]'), function (el) {
|
|||
|
|
el.remove();
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-kind]'), function (el) {
|
|||
|
|
el.removeAttribute('data-legend-kind');
|
|||
|
|
el.removeAttribute('data-legend-label');
|
|||
|
|
el.removeAttribute('data-legend-count');
|
|||
|
|
el.removeAttribute('data-legend-zero');
|
|||
|
|
el.removeAttribute('data-legend-selected');
|
|||
|
|
el.removeAttribute('role');
|
|||
|
|
el.removeAttribute('tabindex');
|
|||
|
|
el.removeAttribute('aria-label');
|
|||
|
|
el.removeAttribute('aria-pressed');
|
|||
|
|
el.removeAttribute('aria-haspopup');
|
|||
|
|
el.removeAttribute('aria-controls');
|
|||
|
|
el.removeAttribute('aria-expanded');
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge]'), function (el) {
|
|||
|
|
el.removeAttribute('data-legend-bridge');
|
|||
|
|
el.removeAttribute('role');
|
|||
|
|
el.removeAttribute('aria-label');
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-pulse-overlay]'), function (el) {
|
|||
|
|
el.remove();
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-hit-overlay]'), function (el) {
|
|||
|
|
el.remove();
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-beacon]'), function (el) {
|
|||
|
|
el.remove();
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-count]'), function (el) {
|
|||
|
|
var originalLabel = el.getAttribute('data-source-evidence-original-label');
|
|||
|
|
if (originalLabel == null || originalLabel === '') el.removeAttribute('aria-label');
|
|||
|
|
else el.setAttribute('aria-label', originalLabel);
|
|||
|
|
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');
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-reach-match], [data-reach-origin], [data-reach-depth]'), function (el) {
|
|||
|
|
el.removeAttribute('data-reach-match');
|
|||
|
|
el.removeAttribute('data-reach-origin');
|
|||
|
|
el.removeAttribute('data-reach-depth');
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-lens-match], [data-lens-selected], [data-lens-peer]'), function (el) {
|
|||
|
|
el.removeAttribute('data-lens-match');
|
|||
|
|
el.removeAttribute('data-lens-selected');
|
|||
|
|
el.removeAttribute('data-lens-peer');
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer]'), function (el) {
|
|||
|
|
el.removeAttribute('data-legend-preview-match');
|
|||
|
|
el.removeAttribute('data-legend-preview-selected');
|
|||
|
|
el.removeAttribute('data-legend-preview-peer');
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target]'), function (el) {
|
|||
|
|
el.removeAttribute('data-relationship-preview');
|
|||
|
|
el.removeAttribute('data-relationship-preview-node');
|
|||
|
|
el.removeAttribute('data-relationship-preview-source');
|
|||
|
|
el.removeAttribute('data-relationship-preview-target');
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-match], [data-intent-trace-selected]'), function (el) {
|
|||
|
|
el.removeAttribute('data-intent-trace-match');
|
|||
|
|
el.removeAttribute('data-intent-trace-selected');
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current]'), function (el) {
|
|||
|
|
el.removeAttribute('data-route-match');
|
|||
|
|
el.removeAttribute('data-route-start');
|
|||
|
|
el.removeAttribute('data-route-end');
|
|||
|
|
el.removeAttribute('data-route-step');
|
|||
|
|
el.removeAttribute('data-route-candidate');
|
|||
|
|
el.removeAttribute('data-route-journey-state');
|
|||
|
|
el.removeAttribute('data-route-journey-current');
|
|||
|
|
el.style.removeProperty('--route-step');
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle]'), function (el) {
|
|||
|
|
el.removeAttribute('data-share-route-match');
|
|||
|
|
el.removeAttribute('data-share-route-step');
|
|||
|
|
el.removeAttribute('data-share-route-start');
|
|||
|
|
el.removeAttribute('data-share-route-end');
|
|||
|
|
el.removeAttribute('data-share-route-middle');
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth]'), function (el) {
|
|||
|
|
el.removeAttribute('data-share-reach-match');
|
|||
|
|
el.removeAttribute('data-share-reach-origin');
|
|||
|
|
el.removeAttribute('data-share-reach-depth');
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-detail], [data-detail-anchor]'), function (el) {
|
|||
|
|
el.removeAttribute('data-detail');
|
|||
|
|
el.removeAttribute('data-detail-anchor');
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(clone.querySelectorAll('[data-node-id][aria-pressed]'), function (el) {
|
|||
|
|
el.setAttribute('aria-pressed', 'false');
|
|||
|
|
});
|
|||
|
|
var canonicalStateClean = !clone.hasAttribute('data-view-scale') &&
|
|||
|
|
!clone.hasAttribute('data-focus-active') &&
|
|||
|
|
!clone.hasAttribute('data-reach-active') &&
|
|||
|
|
!clone.hasAttribute('data-lens-active') &&
|
|||
|
|
!clone.hasAttribute('data-lens-flow-count') &&
|
|||
|
|
!clone.hasAttribute('data-lens-flow-density') &&
|
|||
|
|
!clone.hasAttribute('data-legend-preview-active') &&
|
|||
|
|
!clone.hasAttribute('data-relationship-preview-active') &&
|
|||
|
|
!clone.hasAttribute('data-relationship-direct-active') &&
|
|||
|
|
!clone.hasAttribute('data-intent-trace-active') &&
|
|||
|
|
!clone.hasAttribute('data-route-picking') &&
|
|||
|
|
!clone.hasAttribute('data-route-active') &&
|
|||
|
|
!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;
|
|||
|
|
|
|||
|
|
var vb = svg.viewBox.baseVal;
|
|||
|
|
var finiteSvgDimensions = Number.isFinite(vb.x) && Number.isFinite(vb.y) &&
|
|||
|
|
Number.isFinite(vb.width) && Number.isFinite(vb.height) && vb.width > 0 && vb.height > 0;
|
|||
|
|
var routeStateClean = opts.routeSnapshot
|
|||
|
|
? canonicalStateClean && finiteSvgDimensions && applyRouteSnapshot(clone, opts.routeSnapshot)
|
|||
|
|
: null;
|
|||
|
|
var reachStateClean = opts.reachSnapshot
|
|||
|
|
? canonicalStateClean && finiteSvgDimensions && !opts.routeSnapshot && applyReachSnapshot(clone, opts.reachSnapshot)
|
|||
|
|
: null;
|
|||
|
|
// Scale width/height so the browser rasterizes the vectors at target
|
|||
|
|
// resolution directly. viewBox stays unchanged so coordinates don't
|
|||
|
|
// shift. This is the key to sharp rasters — do NOT scale later via
|
|||
|
|
// canvas upscaling.
|
|||
|
|
clone.setAttribute('width', vb.width * scale);
|
|||
|
|
clone.setAttribute('height', vb.height * scale);
|
|||
|
|
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|||
|
|
|
|||
|
|
// Only the SVG-relevant rules: semantic classes, markers, and the
|
|||
|
|
// theme variable blocks. Toolbar/cards/print CSS can never apply
|
|||
|
|
// inside a standalone SVG and would only bloat the export.
|
|||
|
|
var hostStyle = (function () {
|
|||
|
|
var out = [];
|
|||
|
|
Array.prototype.forEach.call(document.styleSheets, function (sheet) {
|
|||
|
|
var rules;
|
|||
|
|
try { rules = sheet.cssRules; } catch (_) { return; } // cross-origin
|
|||
|
|
if (!rules) return;
|
|||
|
|
Array.prototype.forEach.call(rules, function (rule) {
|
|||
|
|
if (rule.type === 7 && /^archify-/.test(rule.name || '')) {
|
|||
|
|
out.push(rule.cssText);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (rule.type !== 1) return; // plain style rules only
|
|||
|
|
var sel = rule.selectorText || '';
|
|||
|
|
if (/(^|,)\s*(svg|:root|\[data-theme|\[data-preset|\.c-|\.t-|\.a-|\.m-)/.test(sel)) {
|
|||
|
|
out.push(rule.cssText);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
return out.join('\n');
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
// Derive the variable list from the stylesheet so newly added theme
|
|||
|
|
// variables can never be missed by the export pipeline again
|
|||
|
|
// (--lane-fill/--lane-stroke once were).
|
|||
|
|
var varNames = (function () {
|
|||
|
|
var seen = {};
|
|||
|
|
var names = [];
|
|||
|
|
(hostStyle.match(/--[a-zA-Z0-9-]+(?=\s*:)/g) || []).forEach(function (n) {
|
|||
|
|
if (!seen[n]) { seen[n] = true; names.push(n); }
|
|||
|
|
});
|
|||
|
|
return names;
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
// Resolve the full variable set for a given data-theme via an
|
|||
|
|
// off-DOM probe, independent of what the viewer is currently set to.
|
|||
|
|
function resolveVars(themeAttr) {
|
|||
|
|
var probe = document.createElement('div');
|
|||
|
|
probe.setAttribute('data-theme', themeAttr);
|
|||
|
|
probe.setAttribute('data-preset', document.documentElement.getAttribute('data-preset') || 'classic');
|
|||
|
|
probe.style.cssText = 'position:absolute;width:0;height:0;visibility:hidden;';
|
|||
|
|
document.body.appendChild(probe);
|
|||
|
|
try {
|
|||
|
|
var c = getComputedStyle(probe);
|
|||
|
|
return varNames.map(function (n) {
|
|||
|
|
return n + ': ' + c.getPropertyValue(n).trim() + ';';
|
|||
|
|
}).join(' ');
|
|||
|
|
} finally {
|
|||
|
|
document.body.removeChild(probe);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Prepend a local()-only @font-face block for each weight so that the
|
|||
|
|
// sandboxed image-rendering context used during PNG/JPEG/WebP export
|
|||
|
|
// can find JetBrains Mono if the user has it installed locally, and
|
|||
|
|
// falls through cleanly to the monospace stack otherwise. The usual
|
|||
|
|
// Google-Fonts <link> in the host document is unreachable from the
|
|||
|
|
// serialized SVG, so url()-sourced faces would just hang.
|
|||
|
|
var fontFallback = [400, 500, 600, 700].map(function (w) {
|
|||
|
|
return "@font-face { font-family: 'JetBrains Mono'; font-weight: " + w +
|
|||
|
|
"; src: local('JetBrains Mono'), local('JetBrainsMono-Regular'); }";
|
|||
|
|
}).join('\n');
|
|||
|
|
|
|||
|
|
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%');
|
|||
|
|
|
|||
|
|
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');
|
|||
|
|
|
|||
|
|
style.textContent =
|
|||
|
|
fontFallback + "\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" +
|
|||
|
|
"rect.c-bg-rect { fill: var(--bg); }\n";
|
|||
|
|
|
|||
|
|
// Don't lock the serialized SVG to the viewer's current theme.
|
|||
|
|
clone.removeAttribute('data-theme');
|
|||
|
|
// Background follows the CSS variable, not a fixed color, so it
|
|||
|
|
// 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) {
|
|||
|
|
return n + ': ' + computed.getPropertyValue(n).trim() + ';';
|
|||
|
|
}).join(' ');
|
|||
|
|
|
|||
|
|
// IMPORTANT: inject the resolved variables AFTER hostStyle,
|
|||
|
|
// otherwise hostStyle's ":root, [data-theme=\"dark\"] { ... }" rule
|
|||
|
|
// overrides our chosen theme via later-in-cascade equal-specificity.
|
|||
|
|
// Keep this order.
|
|||
|
|
style.textContent =
|
|||
|
|
fontFallback + "\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 { " + vars + " }\n";
|
|||
|
|
|
|||
|
|
bgRect.setAttribute('fill', 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" +
|
|||
|
|
"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" +
|
|||
|
|
"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";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
clone.insertBefore(style, clone.firstChild);
|
|||
|
|
clone.insertBefore(bgRect, style.nextSibling);
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
svgString: new XMLSerializer().serializeToString(clone),
|
|||
|
|
width: vb.width * scale,
|
|||
|
|
height: vb.height * scale,
|
|||
|
|
canonicalStateClean: canonicalStateClean,
|
|||
|
|
routeStateClean: routeStateClean,
|
|||
|
|
reachStateClean: reachStateClean
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function download(blob, filename) {
|
|||
|
|
var url = URL.createObjectURL(blob);
|
|||
|
|
var a = document.createElement('a');
|
|||
|
|
a.href = url;
|
|||
|
|
a.download = filename;
|
|||
|
|
document.body.appendChild(a);
|
|||
|
|
a.click();
|
|||
|
|
a.remove();
|
|||
|
|
setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Conservative upper bound on total canvas pixels. Older Safari on iOS
|
|||
|
|
// 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.
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function rasterize(format) {
|
|||
|
|
// Serialize at a safe scale (RASTER_SCALE=4 by default, reduced if the
|
|||
|
|
// resulting canvas would exceed MAX_CANVAS_PIXELS). The SVG itself is
|
|||
|
|
// rasterized at target resolution natively; drawImage draws at natural
|
|||
|
|
// 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 svgBlob = new Blob([data.svgString], { type: 'image/svg+xml;charset=utf-8' });
|
|||
|
|
var svgUrl = URL.createObjectURL(svgBlob);
|
|||
|
|
|
|||
|
|
return new Promise(function (resolve, reject) {
|
|||
|
|
var img = new Image();
|
|||
|
|
img.onload = function () {
|
|||
|
|
try {
|
|||
|
|
var canvas = document.createElement('canvas');
|
|||
|
|
canvas.width = data.width;
|
|||
|
|
canvas.height = data.height;
|
|||
|
|
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);
|
|||
|
|
URL.revokeObjectURL(svgUrl);
|
|||
|
|
var mime = format === 'jpeg' ? 'image/jpeg' :
|
|||
|
|
format === 'webp' ? 'image/webp' : 'image/png';
|
|||
|
|
var quality = format === 'png' ? undefined : 0.95;
|
|||
|
|
canvas.toBlob(function (blob) {
|
|||
|
|
if (!blob) reject(exportError('viewer.export.error.toBlobNull', { label: format }));
|
|||
|
|
else resolve(blob);
|
|||
|
|
}, mime, quality);
|
|||
|
|
} catch (error) {
|
|||
|
|
URL.revokeObjectURL(svgUrl);
|
|||
|
|
reject(error);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
img.onerror = function (e) {
|
|||
|
|
URL.revokeObjectURL(svgUrl);
|
|||
|
|
reject(e);
|
|||
|
|
};
|
|||
|
|
img.src = svgUrl;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function fitCanvasText(ctx, text, maxWidth, startSize, minSize, weight) {
|
|||
|
|
var value = String(text || '').trim();
|
|||
|
|
var size = startSize;
|
|||
|
|
var family = "'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;
|
|||
|
|
size -= 1;
|
|||
|
|
}
|
|||
|
|
ctx.font = (weight || '600') + ' ' + minSize + 'px ' + family;
|
|||
|
|
if (ctx.measureText(value).width <= maxWidth) return value;
|
|||
|
|
var suffix = '\u2026';
|
|||
|
|
while (value.length > 1 && ctx.measureText(value + suffix).width > maxWidth) {
|
|||
|
|
value = value.slice(0, -1);
|
|||
|
|
}
|
|||
|
|
return value + suffix;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function canvas2dOrThrow(canvas, label) {
|
|||
|
|
if (!canvas || typeof canvas.getContext !== 'function') {
|
|||
|
|
throw exportError('viewer.export.error.canvasUnavailable', { label: label });
|
|||
|
|
}
|
|||
|
|
var ctx = canvas.getContext('2d');
|
|||
|
|
if (!ctx) throw exportError('viewer.export.error.contextUnavailable', { label: label });
|
|||
|
|
if (typeof canvas.toBlob !== 'function') throw exportError('viewer.export.error.toBlobUnavailable', { label: label });
|
|||
|
|
return ctx;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 });
|
|||
|
|
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'));
|
|||
|
|
var svgBlob = new Blob([data.svgString], { type: 'image/svg+xml;charset=utf-8' });
|
|||
|
|
var svgUrl = URL.createObjectURL(svgBlob);
|
|||
|
|
|
|||
|
|
return new Promise(function (resolve, reject) {
|
|||
|
|
var img = new Image();
|
|||
|
|
img.onload = function () {
|
|||
|
|
try {
|
|||
|
|
var canvas = document.createElement('canvas');
|
|||
|
|
canvas.width = SHARE_CARD_WIDTH;
|
|||
|
|
canvas.height = SHARE_CARD_HEIGHT;
|
|||
|
|
var ctx = canvas2dOrThrow(canvas, viewerText('viewer.export.shareCard'));
|
|||
|
|
var computed = getComputedStyle(document.documentElement);
|
|||
|
|
var bg = computed.getPropertyValue('--bg').trim() || currentBg();
|
|||
|
|
var text = computed.getPropertyValue('--text').trim() || '#ffffff';
|
|||
|
|
var muted = computed.getPropertyValue('--text-muted').trim() || '#94a3b8';
|
|||
|
|
var border = computed.getPropertyValue('--panel-border').trim() || '#334155';
|
|||
|
|
var accentProperty = reachSnapshot
|
|||
|
|
? (reachSnapshot.direction === 'upstream' ? '--database-stroke' : '--backend-stroke')
|
|||
|
|
: '--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)
|
|||
|
|
: '';
|
|||
|
|
var subtitle = routeSnapshot
|
|||
|
|
? viewerCount('viewer.export.card.routeSummary', routeSnapshot.hops, {
|
|||
|
|
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();
|
|||
|
|
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 });
|
|||
|
|
|
|||
|
|
ctx.fillStyle = bg;
|
|||
|
|
ctx.fillRect(0, 0, SHARE_CARD_WIDTH, SHARE_CARD_HEIGHT);
|
|||
|
|
|
|||
|
|
ctx.fillStyle = accent;
|
|||
|
|
ctx.fillRect(SHARE_CARD_PADDING, 27, 42, 3);
|
|||
|
|
|
|||
|
|
ctx.textBaseline = 'alphabetic';
|
|||
|
|
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);
|
|||
|
|
|
|||
|
|
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 availableWidth = SHARE_CARD_WIDTH - SHARE_CARD_PADDING * 2;
|
|||
|
|
var availableHeight = SHARE_CARD_HEIGHT - SHARE_CARD_HEADER - SHARE_CARD_PADDING;
|
|||
|
|
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);
|
|||
|
|
ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
|
|||
|
|
URL.revokeObjectURL(svgUrl);
|
|||
|
|
|
|||
|
|
canvas.toBlob(function (blob) {
|
|||
|
|
if (!blob) reject(exportError('viewer.export.error.toBlobNull', { label: viewerText('viewer.export.shareCard') }));
|
|||
|
|
else resolve(blob);
|
|||
|
|
}, 'image/png');
|
|||
|
|
} catch (error) {
|
|||
|
|
URL.revokeObjectURL(svgUrl);
|
|||
|
|
reject(error);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
img.onerror = function (e) {
|
|||
|
|
URL.revokeObjectURL(svgUrl);
|
|||
|
|
reject(e);
|
|||
|
|
};
|
|||
|
|
img.src = svgUrl;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 }));
|
|||
|
|
}
|
|||
|
|
if (options.variant === 'route') {
|
|||
|
|
var routeSnapshot = Archify.routeProbe && Archify.routeProbe.exportSnapshot();
|
|||
|
|
if (!routeSnapshot) return Promise.reject(exportError('viewer.export.routeRequired'));
|
|||
|
|
return renderShareCard({ routeSnapshot: routeSnapshot });
|
|||
|
|
}
|
|||
|
|
var snapshot = Archify.focus && typeof Archify.focus.reachabilitySnapshot === 'function'
|
|||
|
|
? Archify.focus.reachabilitySnapshot()
|
|||
|
|
: null;
|
|||
|
|
if (!snapshot) return Promise.reject(exportError('viewer.export.reachRequired'));
|
|||
|
|
return renderShareCard({ reachSnapshot: snapshot });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function motionMimeType() {
|
|||
|
|
if (typeof MediaRecorder === 'undefined') return '';
|
|||
|
|
var candidates = [
|
|||
|
|
'video/webm;codecs=vp9',
|
|||
|
|
'video/webm;codecs=vp8',
|
|||
|
|
'video/webm'
|
|||
|
|
];
|
|||
|
|
for (var i = 0; i < candidates.length; i++) {
|
|||
|
|
if (!MediaRecorder.isTypeSupported || MediaRecorder.isTypeSupported(candidates[i])) return candidates[i];
|
|||
|
|
}
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function canRecordMotion() {
|
|||
|
|
var svg = document.querySelector('.diagram-container svg');
|
|||
|
|
return !!(svg && svg.getAttribute('data-animation') === 'trace' &&
|
|||
|
|
typeof MediaRecorder !== 'undefined' && motionMimeType() &&
|
|||
|
|
typeof HTMLCanvasElement !== 'undefined' &&
|
|||
|
|
typeof HTMLCanvasElement.prototype.captureStream === 'function');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Record the live CSS animation without Puppeteer, ffmpeg, or a network
|
|||
|
|
// dependency. SVG files loaded through Image are commonly rasterized as
|
|||
|
|
// one cached bitmap, so repeatedly drawing that Image does not reliably
|
|||
|
|
// advance its CSS animation. Keep one crisp static SVG background, then
|
|||
|
|
// render an explicit time-varying signal scene over the real authored
|
|||
|
|
// relationship geometry on every captured canvas frame.
|
|||
|
|
function recordWebm(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (!canRecordMotion()) {
|
|||
|
|
return Promise.reject(exportError('viewer.export.error.webmRequirements'));
|
|||
|
|
}
|
|||
|
|
var duration = Math.max(250, Number(options.duration) || MOTION_DURATION);
|
|||
|
|
var fps = Math.max(1, Number(options.fps) || MOTION_FPS);
|
|||
|
|
var svg = document.querySelector('.diagram-container svg');
|
|||
|
|
var vb = svg.viewBox.baseVal;
|
|||
|
|
var scale = Math.min(1, 1280 / vb.width);
|
|||
|
|
var data = serializeSvg(scale);
|
|||
|
|
var sourceUrl = URL.createObjectURL(new Blob([data.svgString], { type: 'image/svg+xml;charset=utf-8' }));
|
|||
|
|
|
|||
|
|
function createMotionScene(root) {
|
|||
|
|
var rootMatrix = root.getCTM ? root.getCTM() : null;
|
|||
|
|
|
|||
|
|
function pointInRoot(element, point) {
|
|||
|
|
if (!rootMatrix || !element.getCTM || !point.matrixTransform) return { x: point.x, y: point.y };
|
|||
|
|
try {
|
|||
|
|
var elementMatrix = element.getCTM();
|
|||
|
|
if (!elementMatrix) return { x: point.x, y: point.y };
|
|||
|
|
var mapped = point.matrixTransform(rootMatrix.inverse().multiply(elementMatrix));
|
|||
|
|
return { x: mapped.x, y: mapped.y };
|
|||
|
|
} catch (_) {
|
|||
|
|
return { x: point.x, y: point.y };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function samplesFor(element) {
|
|||
|
|
if (typeof element.getTotalLength !== 'function' || typeof element.getPointAtLength !== 'function') return [];
|
|||
|
|
var length;
|
|||
|
|
try { length = element.getTotalLength(); } catch (_) { return []; }
|
|||
|
|
if (!Number.isFinite(length) || length <= 0) return [];
|
|||
|
|
var count = Math.max(12, Math.min(72, Math.ceil(length / 12)));
|
|||
|
|
var points = [];
|
|||
|
|
for (var i = 0; i <= count; i++) {
|
|||
|
|
points.push(pointInRoot(element, element.getPointAtLength(length * i / count)));
|
|||
|
|
}
|
|||
|
|
return points;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function authoredStep(element, fallback) {
|
|||
|
|
var raw = element.style.getPropertyValue('--step') || getComputedStyle(element).getPropertyValue('--step');
|
|||
|
|
var value = Number(raw);
|
|||
|
|
return Number.isFinite(value) ? value : fallback;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var edges = Array.prototype.slice.call(root.querySelectorAll('[data-animate="edge"]')).map(function (element, index) {
|
|||
|
|
var computed = getComputedStyle(element);
|
|||
|
|
return {
|
|||
|
|
points: samplesFor(element),
|
|||
|
|
color: computed.stroke && computed.stroke !== 'none' ? computed.stroke : '#22d3ee',
|
|||
|
|
width: Math.max(1.5, parseFloat(computed.strokeWidth) || 1.5),
|
|||
|
|
delay: Math.min(12, authoredStep(element, index)) * 0.16
|
|||
|
|
};
|
|||
|
|
}).filter(function (edge) { return edge.points.length > 1; });
|
|||
|
|
|
|||
|
|
var nodes = Array.prototype.slice.call(root.querySelectorAll('[data-node-id][data-animate="node"]')).map(function (element, index) {
|
|||
|
|
var box;
|
|||
|
|
try { box = element.getBBox(); } catch (_) { return null; }
|
|||
|
|
var painted = element.querySelector('[class*="c-"]') || element;
|
|||
|
|
var computed = getComputedStyle(painted);
|
|||
|
|
return {
|
|||
|
|
x: box.x,
|
|||
|
|
y: box.y,
|
|||
|
|
width: box.width,
|
|||
|
|
height: box.height,
|
|||
|
|
color: computed.stroke && computed.stroke !== 'none' ? computed.stroke : '#22d3ee',
|
|||
|
|
delay: Math.min(12, authoredStep(element, index)) * 0.16
|
|||
|
|
};
|
|||
|
|
}).filter(Boolean);
|
|||
|
|
|
|||
|
|
return { edges: edges, nodes: nodes, x: vb.x, y: vb.y, width: vb.width, height: vb.height };
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function pointAlong(points, progress) {
|
|||
|
|
var scaled = Math.max(0, Math.min(1, progress)) * (points.length - 1);
|
|||
|
|
var index = Math.min(points.length - 2, Math.floor(scaled));
|
|||
|
|
var mix = scaled - index;
|
|||
|
|
return {
|
|||
|
|
x: points[index].x + (points[index + 1].x - points[index].x) * mix,
|
|||
|
|
y: points[index].y + (points[index + 1].y - points[index].y) * mix
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function roundedRectPath(ctx, x, y, width, height, radius) {
|
|||
|
|
var r = Math.max(0, Math.min(radius, width / 2, height / 2));
|
|||
|
|
ctx.beginPath();
|
|||
|
|
ctx.moveTo(x + r, y);
|
|||
|
|
ctx.lineTo(x + width - r, y);
|
|||
|
|
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
|
|||
|
|
ctx.lineTo(x + width, y + height - r);
|
|||
|
|
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
|
|||
|
|
ctx.lineTo(x + r, y + height);
|
|||
|
|
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
|
|||
|
|
ctx.lineTo(x, y + r);
|
|||
|
|
ctx.quadraticCurveTo(x, y, x + r, y);
|
|||
|
|
ctx.closePath();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function drawMotionFrame(ctx, backgroundImage, motionScene, elapsed) {
|
|||
|
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|||
|
|
ctx.drawImage(backgroundImage, 0, 0, ctx.canvas.width, ctx.canvas.height);
|
|||
|
|
ctx.save();
|
|||
|
|
ctx.scale(ctx.canvas.width / motionScene.width, ctx.canvas.height / motionScene.height);
|
|||
|
|
ctx.translate(-motionScene.x, -motionScene.y);
|
|||
|
|
|
|||
|
|
motionScene.edges.forEach(function (edge) {
|
|||
|
|
var duration = 1.75;
|
|||
|
|
var progress = (elapsed - edge.delay) / duration;
|
|||
|
|
if (progress < 0 || progress > 1) return;
|
|||
|
|
var head = Math.max(0, Math.min(1, progress));
|
|||
|
|
var tail = Math.max(0, head - 0.24);
|
|||
|
|
var opacity = Math.sin(Math.PI * Math.min(1, progress));
|
|||
|
|
|
|||
|
|
ctx.save();
|
|||
|
|
ctx.strokeStyle = edge.color;
|
|||
|
|
ctx.fillStyle = edge.color;
|
|||
|
|
ctx.lineCap = 'round';
|
|||
|
|
ctx.lineJoin = 'round';
|
|||
|
|
ctx.lineWidth = Math.max(3.2, edge.width * 2.2);
|
|||
|
|
ctx.globalAlpha = 0.42 + opacity * 0.5;
|
|||
|
|
ctx.shadowColor = edge.color;
|
|||
|
|
ctx.shadowBlur = 10;
|
|||
|
|
ctx.beginPath();
|
|||
|
|
for (var i = 0; i <= 14; i++) {
|
|||
|
|
var trailPoint = pointAlong(edge.points, tail + (head - tail) * i / 14);
|
|||
|
|
if (i === 0) ctx.moveTo(trailPoint.x, trailPoint.y);
|
|||
|
|
else ctx.lineTo(trailPoint.x, trailPoint.y);
|
|||
|
|
}
|
|||
|
|
ctx.stroke();
|
|||
|
|
|
|||
|
|
var point = pointAlong(edge.points, head);
|
|||
|
|
ctx.globalAlpha = 0.95;
|
|||
|
|
ctx.beginPath();
|
|||
|
|
ctx.arc(point.x, point.y, 4.8, 0, Math.PI * 2);
|
|||
|
|
ctx.fill();
|
|||
|
|
ctx.restore();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
motionScene.nodes.forEach(function (node) {
|
|||
|
|
var progress = (elapsed - node.delay) / 2.6;
|
|||
|
|
if (progress < 0 || progress > 1) return;
|
|||
|
|
var pulse = Math.sin(Math.PI * progress);
|
|||
|
|
if (pulse <= 0.02) return;
|
|||
|
|
ctx.save();
|
|||
|
|
ctx.strokeStyle = node.color;
|
|||
|
|
ctx.lineWidth = 1.4 + pulse * 1.8;
|
|||
|
|
ctx.globalAlpha = pulse * 0.5;
|
|||
|
|
ctx.shadowColor = node.color;
|
|||
|
|
ctx.shadowBlur = 12 * pulse;
|
|||
|
|
var inset = 2 + pulse * 3;
|
|||
|
|
roundedRectPath(ctx, node.x - inset, node.y - inset, node.width + inset * 2, node.height + inset * 2, 8);
|
|||
|
|
ctx.stroke();
|
|||
|
|
ctx.restore();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
ctx.restore();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var motionScene = createMotionScene(svg);
|
|||
|
|
|
|||
|
|
return new Promise(function (resolve, reject) {
|
|||
|
|
var backgroundImage = new Image();
|
|||
|
|
backgroundImage.onload = function () {
|
|||
|
|
var canvas = document.createElement('canvas');
|
|||
|
|
canvas.width = Math.max(2, Math.round(data.width / 2) * 2);
|
|||
|
|
canvas.height = Math.max(2, Math.round(data.height / 2) * 2);
|
|||
|
|
var ctx = canvas.getContext('2d');
|
|||
|
|
var stream = canvas.captureStream(fps);
|
|||
|
|
var mime = motionMimeType();
|
|||
|
|
var recorder;
|
|||
|
|
try {
|
|||
|
|
recorder = new MediaRecorder(stream, { mimeType: mime, videoBitsPerSecond: 6000000 });
|
|||
|
|
} catch (err) {
|
|||
|
|
URL.revokeObjectURL(sourceUrl);
|
|||
|
|
stream.getTracks().forEach(function (track) { track.stop(); });
|
|||
|
|
reject(err);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
var chunks = [];
|
|||
|
|
var raf = 0;
|
|||
|
|
var stopped = false;
|
|||
|
|
var startedAt = performance.now();
|
|||
|
|
function cleanup() {
|
|||
|
|
if (stopped) return;
|
|||
|
|
stopped = true;
|
|||
|
|
cancelAnimationFrame(raf);
|
|||
|
|
URL.revokeObjectURL(sourceUrl);
|
|||
|
|
stream.getTracks().forEach(function (track) { track.stop(); });
|
|||
|
|
}
|
|||
|
|
function draw(now) {
|
|||
|
|
var elapsed = Math.max(0, ((Number(now) || performance.now()) - startedAt) / 1000);
|
|||
|
|
drawMotionFrame(ctx, backgroundImage, motionScene, elapsed);
|
|||
|
|
raf = requestAnimationFrame(draw);
|
|||
|
|
}
|
|||
|
|
recorder.ondataavailable = function (event) {
|
|||
|
|
if (event.data && event.data.size) chunks.push(event.data);
|
|||
|
|
};
|
|||
|
|
recorder.onerror = function (event) {
|
|||
|
|
cleanup();
|
|||
|
|
reject(event.error || exportError('viewer.export.error.mediaRecorder'));
|
|||
|
|
};
|
|||
|
|
recorder.onstop = function () {
|
|||
|
|
cleanup();
|
|||
|
|
var blob = new Blob(chunks, { type: recorder.mimeType || mime });
|
|||
|
|
if (!blob.size) reject(exportError('viewer.export.error.emptyWebm'));
|
|||
|
|
else resolve(blob);
|
|||
|
|
};
|
|||
|
|
drawMotionFrame(ctx, backgroundImage, motionScene, 0);
|
|||
|
|
recorder.start(250);
|
|||
|
|
startedAt = performance.now();
|
|||
|
|
raf = requestAnimationFrame(draw);
|
|||
|
|
setTimeout(function () {
|
|||
|
|
if (recorder.state === 'inactive') return;
|
|||
|
|
// Chromium normally emits the final chunk during stop(), but
|
|||
|
|
// some embedded browser shells need an explicit encoder flush
|
|||
|
|
// first. Keep the short grace window bounded and harmless for
|
|||
|
|
// browsers that already delivered timeslice chunks.
|
|||
|
|
try { recorder.requestData(); } catch (_) {}
|
|||
|
|
setTimeout(function () {
|
|||
|
|
if (recorder.state !== 'inactive') recorder.stop();
|
|||
|
|
}, 120);
|
|||
|
|
}, duration);
|
|||
|
|
};
|
|||
|
|
backgroundImage.onerror = function () {
|
|||
|
|
URL.revokeObjectURL(sourceUrl);
|
|||
|
|
reject(exportError('viewer.export.error.webmBackground'));
|
|||
|
|
};
|
|||
|
|
backgroundImage.src = sourceUrl;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var menu = document.getElementById('export-menu');
|
|||
|
|
var btn = document.getElementById('btn-export');
|
|||
|
|
var routeShareItem = menu.querySelector('button[data-action="route-share-card"]');
|
|||
|
|
var reachShareItem = menu.querySelector('button[data-action="reach-share-card"]');
|
|||
|
|
var items = function () {
|
|||
|
|
return Array.prototype.slice.call(menu.querySelectorAll('button[role="menuitem"]'));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
function syncRouteShareItem() {
|
|||
|
|
var snapshot = Archify.routeProbe && typeof Archify.routeProbe.exportSnapshot === 'function'
|
|||
|
|
? Archify.routeProbe.exportSnapshot()
|
|||
|
|
: null;
|
|||
|
|
routeShareItem.hidden = !snapshot;
|
|||
|
|
routeShareItem.disabled = !snapshot;
|
|||
|
|
return snapshot;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function syncReachShareItem() {
|
|||
|
|
var snapshot = Archify.focus && typeof Archify.focus.reachabilitySnapshot === 'function'
|
|||
|
|
? Archify.focus.reachabilitySnapshot()
|
|||
|
|
: null;
|
|||
|
|
reachShareItem.hidden = !snapshot;
|
|||
|
|
reachShareItem.disabled = !snapshot;
|
|||
|
|
return snapshot;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- Clipboard support ----------------------------------------------
|
|||
|
|
function canCopyImage() {
|
|||
|
|
return typeof ClipboardItem !== 'undefined' &&
|
|||
|
|
navigator.clipboard &&
|
|||
|
|
typeof navigator.clipboard.write === 'function';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---- Raster format detection ----------------------------------------
|
|||
|
|
// 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 === 'webm') return canRecordMotion();
|
|||
|
|
var mime = format === 'jpeg' ? 'image/jpeg' : 'image/webp';
|
|||
|
|
try {
|
|||
|
|
var c = document.createElement('canvas');
|
|||
|
|
c.width = c.height = 2;
|
|||
|
|
return c.toDataURL(mime).indexOf('data:' + mime) === 0;
|
|||
|
|
} catch (_) { return false; }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Gray out unsupported items.
|
|||
|
|
items().forEach(function (it) {
|
|||
|
|
if (it.dataset.format && !supports(it.dataset.format)) {
|
|||
|
|
it.disabled = true;
|
|||
|
|
it.title = viewerText('viewer.export.unsupported');
|
|||
|
|
}
|
|||
|
|
if ((it.dataset.action === 'copy' || it.dataset.action === 'copy-share-card') && !canCopyImage()) {
|
|||
|
|
it.disabled = true;
|
|||
|
|
it.title = viewerText('viewer.export.clipboardUnsupported');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// ---- Toast ----------------------------------------------------------
|
|||
|
|
// A live region only announces CHANGES to an existing node, so the toast
|
|||
|
|
// element is created once up front and updated in place.
|
|||
|
|
var toastEl = document.createElement('div');
|
|||
|
|
toastEl.className = 'archify-toast';
|
|||
|
|
toastEl.setAttribute('role', 'status');
|
|||
|
|
document.body.appendChild(toastEl);
|
|||
|
|
var toastTimer = null;
|
|||
|
|
|
|||
|
|
function toast(msg) {
|
|||
|
|
toastEl.textContent = '';
|
|||
|
|
requestAnimationFrame(function () {
|
|||
|
|
toastEl.textContent = msg;
|
|||
|
|
toastEl.classList.add('show');
|
|||
|
|
});
|
|||
|
|
clearTimeout(toastTimer);
|
|||
|
|
toastTimer = setTimeout(function () { toastEl.classList.remove('show'); }, 1500);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function open(focusLast) {
|
|||
|
|
if (Archify.preset && Archify.preset.isOpen()) Archify.preset.close(false);
|
|||
|
|
if (Archify.semanticLens && typeof Archify.semanticLens.clearPreview === 'function') Archify.semanticLens.clearPreview();
|
|||
|
|
if (Archify.semanticLens && Archify.semanticLens.isOpen()) Archify.semanticLens.close({ restoreFocus: false });
|
|||
|
|
syncRouteShareItem();
|
|||
|
|
syncReachShareItem();
|
|||
|
|
menu.classList.add('open');
|
|||
|
|
btn.setAttribute('aria-expanded', 'true');
|
|||
|
|
var available = items().filter(function (i) { return !i.hidden && !i.disabled; });
|
|||
|
|
var target = focusLast ? available[available.length - 1] : available[0];
|
|||
|
|
if (target) target.focus();
|
|||
|
|
}
|
|||
|
|
function close(focusTrigger) {
|
|||
|
|
menu.classList.remove('open');
|
|||
|
|
btn.setAttribute('aria-expanded', 'false');
|
|||
|
|
if (focusTrigger) btn.focus();
|
|||
|
|
}
|
|||
|
|
function isOpen() { return menu.classList.contains('open'); }
|
|||
|
|
|
|||
|
|
btn.addEventListener('click', function (e) {
|
|||
|
|
e.stopPropagation();
|
|||
|
|
if (isOpen()) close(false);
|
|||
|
|
else open();
|
|||
|
|
});
|
|||
|
|
// APG menu-button pattern: ArrowDown/ArrowUp on the trigger opens the menu.
|
|||
|
|
btn.addEventListener('keydown', function (e) {
|
|||
|
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
if (!isOpen()) open(e.key === 'ArrowUp');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
document.addEventListener('click', function (e) {
|
|||
|
|
if (!menu.contains(e.target) && e.target !== btn) close(false);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Keyboard navigation inside the menu.
|
|||
|
|
// Menu items: Up/Down, Home/End.
|
|||
|
|
// Esc closes + returns focus to trigger; Tab closes.
|
|||
|
|
menu.addEventListener('keydown', function (e) {
|
|||
|
|
if (e.key === 'Escape') { e.preventDefault(); close(true); return; }
|
|||
|
|
if (e.key === 'Tab') { close(false); return; }
|
|||
|
|
|
|||
|
|
var available = items().filter(function (i) { return !i.hidden && !i.disabled; });
|
|||
|
|
var current = available.indexOf(document.activeElement);
|
|||
|
|
switch (e.key) {
|
|||
|
|
case 'ArrowDown':
|
|||
|
|
e.preventDefault();
|
|||
|
|
if (available.length) available[(current + 1 + available.length) % available.length].focus();
|
|||
|
|
break;
|
|||
|
|
case 'ArrowUp':
|
|||
|
|
e.preventDefault();
|
|||
|
|
if (available.length) available[(current - 1 + available.length) % available.length].focus();
|
|||
|
|
break;
|
|||
|
|
case 'Home':
|
|||
|
|
e.preventDefault();
|
|||
|
|
if (available[0]) available[0].focus();
|
|||
|
|
break;
|
|||
|
|
case 'End':
|
|||
|
|
e.preventDefault();
|
|||
|
|
if (available.length) available[available.length - 1].focus();
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function runExport(format) {
|
|||
|
|
var base = diagramFilename();
|
|||
|
|
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) {
|
|||
|
|
var blob = new Blob([d.svgString], { type: 'image/svg+xml;charset=utf-8' });
|
|||
|
|
recordExportReceipt('svg', blob, d.canonicalStateClean);
|
|||
|
|
download(blob, base + '.svg');
|
|||
|
|
})
|
|||
|
|
: format === 'webm'
|
|||
|
|
? recordWebm().then(function (blob) {
|
|||
|
|
document.documentElement.setAttribute('data-last-motion-bytes', String(blob.size));
|
|||
|
|
recordExportReceipt('webm', blob, true);
|
|||
|
|
download(blob, base + '.webm');
|
|||
|
|
toast(viewerText('viewer.export.downloadedWebm'));
|
|||
|
|
})
|
|||
|
|
: rasterize(format).then(function (blob) {
|
|||
|
|
recordExportReceipt(format, blob, true);
|
|||
|
|
download(blob, base + '.' + format);
|
|||
|
|
})
|
|||
|
|
).catch(function (err) {
|
|||
|
|
console.error(err);
|
|||
|
|
var technicalMessage = err && err.message ? err.message : format;
|
|||
|
|
var message = exportMessage(err);
|
|||
|
|
document.documentElement.setAttribute('data-last-export-error-format', format);
|
|||
|
|
document.documentElement.setAttribute('data-last-export-error', technicalMessage);
|
|||
|
|
if (format === 'webm') {
|
|||
|
|
var motionItem = menu.querySelector('button[data-format="webm"]');
|
|||
|
|
if (motionItem) {
|
|||
|
|
motionItem.disabled = true;
|
|||
|
|
motionItem.title = viewerText('viewer.export.motionUnavailable');
|
|||
|
|
motionItem.style.opacity = '0.5';
|
|||
|
|
}
|
|||
|
|
toast(viewerText('viewer.export.webmUnavailable'));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
alert(viewerText('viewer.export.failed', { message: message }));
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function runRouteShareCard() {
|
|||
|
|
close(false);
|
|||
|
|
clearExportReceipt();
|
|||
|
|
var snapshot = Archify.routeProbe && Archify.routeProbe.exportSnapshot();
|
|||
|
|
var blobPromise = snapshot
|
|||
|
|
? renderShareCard({ routeSnapshot: snapshot })
|
|||
|
|
: Promise.reject(exportError('viewer.export.routeRequired'));
|
|||
|
|
return blobPromise.then(function (blob) {
|
|||
|
|
recordExportReceipt('share-card', blob, false, { width: SHARE_CARD_WIDTH, height: SHARE_CARD_HEIGHT }, 'route', true);
|
|||
|
|
download(blob, diagramFilename() + '-route-share-card.png');
|
|||
|
|
toast(viewerText('viewer.export.downloadedRoute'));
|
|||
|
|
return blob;
|
|||
|
|
}).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.routeFailed', { message: message }));
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function runReachShareCard() {
|
|||
|
|
close(false);
|
|||
|
|
clearExportReceipt();
|
|||
|
|
var snapshot = Archify.focus && typeof Archify.focus.reachabilitySnapshot === 'function'
|
|||
|
|
? Archify.focus.reachabilitySnapshot()
|
|||
|
|
: null;
|
|||
|
|
var blobPromise = snapshot
|
|||
|
|
? renderShareCard({ reachSnapshot: snapshot })
|
|||
|
|
: Promise.reject(exportError('viewer.export.reachRequired'));
|
|||
|
|
return blobPromise.then(function (blob) {
|
|||
|
|
recordExportReceipt('share-card', blob, false, { width: SHARE_CARD_WIDTH, height: SHARE_CARD_HEIGHT }, 'reach', false, true);
|
|||
|
|
download(blob, diagramFilename() + '-' + snapshot.direction + '-reach-share-card.png');
|
|||
|
|
toast(viewerText('viewer.export.downloadedReach'));
|
|||
|
|
return blob;
|
|||
|
|
}).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.reachFailed', { message: message }));
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// A compact, machine-readable receipt for local QA and generated proof
|
|||
|
|
// galleries. It exposes no diagram contents, only the completed format,
|
|||
|
|
// byte count, and whether temporary viewer state was excluded.
|
|||
|
|
function recordExportReceipt(format, blob, canonical, dimensions, variant, routeStateClean, reachStateClean) {
|
|||
|
|
document.documentElement.setAttribute('data-last-export-format', format);
|
|||
|
|
document.documentElement.setAttribute('data-last-export-bytes', String(blob.size));
|
|||
|
|
document.documentElement.setAttribute('data-last-export-canonical', canonical ? 'true' : 'false');
|
|||
|
|
if (variant) {
|
|||
|
|
document.documentElement.setAttribute('data-last-export-variant', variant);
|
|||
|
|
} else {
|
|||
|
|
document.documentElement.removeAttribute('data-last-export-variant');
|
|||
|
|
}
|
|||
|
|
if (routeStateClean === true) {
|
|||
|
|
document.documentElement.setAttribute('data-last-export-route-state-clean', 'true');
|
|||
|
|
} else {
|
|||
|
|
document.documentElement.removeAttribute('data-last-export-route-state-clean');
|
|||
|
|
}
|
|||
|
|
if (reachStateClean === true) {
|
|||
|
|
document.documentElement.setAttribute('data-last-export-reach-state-clean', 'true');
|
|||
|
|
} else {
|
|||
|
|
document.documentElement.removeAttribute('data-last-export-reach-state-clean');
|
|||
|
|
}
|
|||
|
|
if (dimensions) {
|
|||
|
|
document.documentElement.setAttribute('data-last-export-width', String(dimensions.width));
|
|||
|
|
document.documentElement.setAttribute('data-last-export-height', String(dimensions.height));
|
|||
|
|
} else {
|
|||
|
|
document.documentElement.removeAttribute('data-last-export-width');
|
|||
|
|
document.documentElement.removeAttribute('data-last-export-height');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function clearExportReceipt() {
|
|||
|
|
document.documentElement.removeAttribute('data-last-export-format');
|
|||
|
|
document.documentElement.removeAttribute('data-last-export-bytes');
|
|||
|
|
document.documentElement.removeAttribute('data-last-export-canonical');
|
|||
|
|
document.documentElement.removeAttribute('data-last-export-width');
|
|||
|
|
document.documentElement.removeAttribute('data-last-export-height');
|
|||
|
|
document.documentElement.removeAttribute('data-last-export-variant');
|
|||
|
|
document.documentElement.removeAttribute('data-last-export-route-state-clean');
|
|||
|
|
document.documentElement.removeAttribute('data-last-export-reach-state-clean');
|
|||
|
|
document.documentElement.removeAttribute('data-last-export-error-format');
|
|||
|
|
document.documentElement.removeAttribute('data-last-export-error');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function writePngToClipboard(blobPromise) {
|
|||
|
|
// WebKit requires ClipboardItem to be constructed synchronously inside
|
|||
|
|
// the user gesture. Chromium accepts the same pending Promise<Blob>;
|
|||
|
|
// engines that reject promise values fall back to an awaited Blob.
|
|||
|
|
try {
|
|||
|
|
return navigator.clipboard.write([new ClipboardItem({ 'image/png': blobPromise })]);
|
|||
|
|
} catch (_) {
|
|||
|
|
return blobPromise.then(function (blob) {
|
|||
|
|
return navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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();
|
|||
|
|
if (!canCopyImage()) {
|
|||
|
|
alert(viewerText('viewer.export.clipboardUnsupported.short'));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
var blobPromise = rasterize('png');
|
|||
|
|
return writePngToClipboard(blobPromise).then(function () {
|
|||
|
|
toast(viewerText('viewer.export.copiedPng'));
|
|||
|
|
}).catch(function (err) {
|
|||
|
|
console.error(err);
|
|||
|
|
alert(viewerText('viewer.export.copyFailed', {
|
|||
|
|
message: exportMessage(err)
|
|||
|
|
}));
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
menu.addEventListener('click', function (e) {
|
|||
|
|
var routeShareCardBtn = e.target.closest('button[data-action="route-share-card"]');
|
|||
|
|
if (routeShareCardBtn && !routeShareCardBtn.disabled && !routeShareCardBtn.hidden) { runRouteShareCard(); return; }
|
|||
|
|
|
|||
|
|
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; }
|
|||
|
|
|
|||
|
|
var formatBtn = e.target.closest('button[data-format]');
|
|||
|
|
if (formatBtn && !formatBtn.disabled) { runExport(formatBtn.dataset.format); }
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
Archify.motion = { canRecord: canRecordMotion, recordWebm: recordWebm };
|
|||
|
|
Archify.exportMenu = {
|
|||
|
|
open: open,
|
|||
|
|
close: close,
|
|||
|
|
isOpen: isOpen,
|
|||
|
|
run: runExport,
|
|||
|
|
shareCard: rasterizeShareCard,
|
|||
|
|
downloadRouteShareCard: runRouteShareCard,
|
|||
|
|
downloadReachShareCard: runReachShareCard,
|
|||
|
|
syncRouteShare: syncRouteShareItem,
|
|||
|
|
syncReachShare: syncReachShareItem,
|
|||
|
|
copyShareCard: runCopyShareCard
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Auto-open on page load for demo/screenshot purposes: ?openExport=1
|
|||
|
|
// Wait for fonts (so the menu doesn't flash before typography lands)
|
|||
|
|
// and paint before opening. Fallback timeout for browsers without the
|
|||
|
|
// Font Loading API.
|
|||
|
|
try {
|
|||
|
|
if (new URLSearchParams(window.location.search).get('openExport') === '1') {
|
|||
|
|
var openWhenReady = function () {
|
|||
|
|
requestAnimationFrame(function () { requestAnimationFrame(open); });
|
|||
|
|
};
|
|||
|
|
if (document.fonts && document.fonts.ready) {
|
|||
|
|
document.fonts.ready.then(openWhenReady);
|
|||
|
|
} else {
|
|||
|
|
setTimeout(openWhenReady, 200);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (_) {}
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* ============================================================
|
|||
|
|
Motion Governor — one reader switch and one motion budget. Ambient
|
|||
|
|
trace yields to the strongest semantic owner; Still also parks bounded
|
|||
|
|
viewer signals without discarding their static meaning.
|
|||
|
|
============================================================ */
|
|||
|
|
Archify.motionGovernor = (function () {
|
|||
|
|
var STORAGE_KEY = 'archify-motion';
|
|||
|
|
var html = document.documentElement;
|
|||
|
|
var svg = document.querySelector('.diagram-container svg');
|
|||
|
|
var btn = document.getElementById('btn-motion');
|
|||
|
|
var label = document.getElementById('motion-label');
|
|||
|
|
var motionQuery = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;
|
|||
|
|
var capable = !!(svg && svg.getAttribute('data-animation') === 'trace');
|
|||
|
|
var readerPaused = false;
|
|||
|
|
var suspensions = Object.create(null);
|
|||
|
|
var explicitOwner = '';
|
|||
|
|
var owner = '';
|
|||
|
|
var ownerToken = 0;
|
|||
|
|
var ownerCleanup = null;
|
|||
|
|
var lastEffectivePaused = null;
|
|||
|
|
var ambientStarted = false;
|
|||
|
|
var ambientPending = new Set();
|
|||
|
|
|
|||
|
|
function detachAmbientBoundary() {
|
|||
|
|
if (!svg) return;
|
|||
|
|
svg.removeEventListener('animationend', onAmbientBoundary, true);
|
|||
|
|
svg.removeEventListener('animationcancel', onAmbientBoundary, true);
|
|||
|
|
}
|
|||
|
|
function settleAmbient(reason) {
|
|||
|
|
if (!capable) return false;
|
|||
|
|
ambientStarted = true;
|
|||
|
|
ambientPending.clear();
|
|||
|
|
detachAmbientBoundary();
|
|||
|
|
html.setAttribute('data-ambient-motion', 'settled');
|
|||
|
|
html.setAttribute('data-ambient-settle-reason', reason || 'complete');
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function onAmbientBoundary(event) {
|
|||
|
|
if (!ambientPending.has(event.target)) return;
|
|||
|
|
ambientPending.delete(event.target);
|
|||
|
|
if (!ambientPending.size) settleAmbient('complete');
|
|||
|
|
}
|
|||
|
|
function startAmbient() {
|
|||
|
|
if (ambientStarted || !capable) return false;
|
|||
|
|
ambientStarted = true;
|
|||
|
|
ambientPending = new Set(Array.prototype.slice.call(svg.querySelectorAll('[data-animate="edge"], [data-animate="node"]')));
|
|||
|
|
if (!ambientPending.size) return settleAmbient('empty');
|
|||
|
|
svg.addEventListener('animationend', onAmbientBoundary, true);
|
|||
|
|
svg.addEventListener('animationcancel', onAmbientBoundary, true);
|
|||
|
|
html.setAttribute('data-ambient-motion', 'running');
|
|||
|
|
html.removeAttribute('data-ambient-settle-reason');
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function readStored() {
|
|||
|
|
try { return localStorage.getItem(STORAGE_KEY); } catch (_) { return null; }
|
|||
|
|
}
|
|||
|
|
function writeStored() {
|
|||
|
|
try {
|
|||
|
|
if (readerPaused) localStorage.setItem(STORAGE_KEY, 'still');
|
|||
|
|
else localStorage.removeItem(STORAGE_KEY);
|
|||
|
|
} catch (_) {}
|
|||
|
|
}
|
|||
|
|
function reducedMotion() {
|
|||
|
|
return !!(motionQuery && motionQuery.matches);
|
|||
|
|
}
|
|||
|
|
function hasSuspension() {
|
|||
|
|
return Object.keys(suspensions).length > 0;
|
|||
|
|
}
|
|||
|
|
function effectivePaused() {
|
|||
|
|
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');
|
|||
|
|
if (value === 'intent') return viewerText('viewer.owner.intent');
|
|||
|
|
if (value === 'focus') return viewerText('viewer.owner.focus');
|
|||
|
|
if (value === 'legend') return viewerText('viewer.owner.legend');
|
|||
|
|
return viewerText('viewer.owner.reader');
|
|||
|
|
}
|
|||
|
|
function render() {
|
|||
|
|
if (!capable) return;
|
|||
|
|
var systemPaused = reducedMotion();
|
|||
|
|
var paused = effectivePaused();
|
|||
|
|
html.setAttribute('data-motion', paused ? 'still' : 'live');
|
|||
|
|
if (paused || owner || html.hasAttribute('data-embed') || html.hasAttribute('data-share-playback') || html.hasAttribute('data-document-hidden')) {
|
|||
|
|
settleAmbient('suppressed');
|
|||
|
|
} else if (!ambientStarted) {
|
|||
|
|
startAmbient();
|
|||
|
|
}
|
|||
|
|
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') });
|
|||
|
|
}
|
|||
|
|
lastEffectivePaused = paused;
|
|||
|
|
if (Archify.routeProbe && typeof Archify.routeProbe.syncMotion === 'function') Archify.routeProbe.syncMotion();
|
|||
|
|
if (systemPaused) {
|
|||
|
|
btn.setAttribute('aria-label', viewerText('viewer.motion.reduced'));
|
|||
|
|
btn.title = viewerText('viewer.motion.reduced');
|
|||
|
|
} else if (hasSuspension()) {
|
|||
|
|
btn.setAttribute('aria-label', viewerText('viewer.motion.hidden'));
|
|||
|
|
btn.title = viewerText('viewer.motion.hidden');
|
|||
|
|
} else if (paused) {
|
|||
|
|
btn.setAttribute('aria-label', viewerText('viewer.motion.resume'));
|
|||
|
|
btn.title = viewerText('viewer.motion.resume');
|
|||
|
|
} else if (owner) {
|
|||
|
|
btn.setAttribute('aria-label', viewerText('viewer.motion.yielding', { owner: ownerLabel(owner) }));
|
|||
|
|
btn.title = viewerText('viewer.motion.yielding.title', { owner: ownerLabel(owner) });
|
|||
|
|
} else {
|
|||
|
|
btn.setAttribute('aria-label', viewerText('viewer.motion.pause'));
|
|||
|
|
btn.title = viewerText('viewer.motion.pause');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
function setPaused(next, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
readerPaused = !!next;
|
|||
|
|
if (options.persist !== false) writeStored();
|
|||
|
|
render();
|
|||
|
|
return readerPaused;
|
|||
|
|
}
|
|||
|
|
function publishOwner() {
|
|||
|
|
owner = explicitOwner || deriveOwner();
|
|||
|
|
if (owner) html.setAttribute('data-motion-owner', owner);
|
|||
|
|
else html.removeAttribute('data-motion-owner');
|
|||
|
|
render();
|
|||
|
|
return owner;
|
|||
|
|
}
|
|||
|
|
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';
|
|||
|
|
if (svg.hasAttribute('data-intent-trace-active')) return 'intent';
|
|||
|
|
if (svg.hasAttribute('data-focus-active')) return 'focus';
|
|||
|
|
if (svg.hasAttribute('data-legend-preview-active')) return 'legend';
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
function clearClaim(preempted) {
|
|||
|
|
var cleanup = ownerCleanup;
|
|||
|
|
ownerCleanup = null;
|
|||
|
|
explicitOwner = '';
|
|||
|
|
if (preempted && cleanup) {
|
|||
|
|
try { cleanup(); } catch (_) {}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
function claim(next, cleanup) {
|
|||
|
|
if (!capable || !next) return 0;
|
|||
|
|
clearClaim(true);
|
|||
|
|
ownerToken += 1;
|
|||
|
|
explicitOwner = next;
|
|||
|
|
ownerCleanup = typeof cleanup === 'function' ? cleanup : null;
|
|||
|
|
publishOwner();
|
|||
|
|
return ownerToken;
|
|||
|
|
}
|
|||
|
|
function release(token) {
|
|||
|
|
if (!capable || token !== ownerToken || !explicitOwner) return false;
|
|||
|
|
clearClaim(false);
|
|||
|
|
ownerToken += 1;
|
|||
|
|
publishOwner();
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function suspend(reason) {
|
|||
|
|
var key = String(reason || 'runtime');
|
|||
|
|
var active = true;
|
|||
|
|
suspensions[key] = (suspensions[key] || 0) + 1;
|
|||
|
|
render();
|
|||
|
|
return function () {
|
|||
|
|
if (!active) return false;
|
|||
|
|
active = false;
|
|||
|
|
if (suspensions[key] > 1) suspensions[key] -= 1;
|
|||
|
|
else delete suspensions[key];
|
|||
|
|
render();
|
|||
|
|
return true;
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
function syncVisibility() {
|
|||
|
|
if (document.hidden) {
|
|||
|
|
suspensions.visibility = true;
|
|||
|
|
html.setAttribute('data-document-hidden', 'true');
|
|||
|
|
} else {
|
|||
|
|
delete suspensions.visibility;
|
|||
|
|
html.removeAttribute('data-document-hidden');
|
|||
|
|
}
|
|||
|
|
render();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!capable) {
|
|||
|
|
btn.hidden = true;
|
|||
|
|
html.removeAttribute('data-motion-capable');
|
|||
|
|
html.removeAttribute('data-motion');
|
|||
|
|
html.removeAttribute('data-motion-owner');
|
|||
|
|
html.removeAttribute('data-ambient-motion');
|
|||
|
|
html.removeAttribute('data-ambient-settle-reason');
|
|||
|
|
return {
|
|||
|
|
capable: false,
|
|||
|
|
pause: function () { return false; },
|
|||
|
|
resume: function () { return false; },
|
|||
|
|
toggle: function () { return false; },
|
|||
|
|
setMode: function () { return 'still'; },
|
|||
|
|
mode: function () { return 'still'; },
|
|||
|
|
claim: function () { return 0; },
|
|||
|
|
release: function () { return false; },
|
|||
|
|
suspend: function () { return function () { return false; }; },
|
|||
|
|
isPaused: function () { return true; },
|
|||
|
|
owner: function () { return ''; }
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
html.setAttribute('data-motion-capable', 'true');
|
|||
|
|
btn.hidden = false;
|
|||
|
|
readerPaused = readStored() === 'still';
|
|||
|
|
btn.addEventListener('click', function () { setPaused(!readerPaused); });
|
|||
|
|
if (motionQuery) {
|
|||
|
|
if (typeof motionQuery.addEventListener === 'function') motionQuery.addEventListener('change', render);
|
|||
|
|
else if (typeof motionQuery.addListener === 'function') motionQuery.addListener(render);
|
|||
|
|
}
|
|||
|
|
document.addEventListener('visibilitychange', syncVisibility);
|
|||
|
|
if (document.documentElement.getAttribute('data-embed') !== 'true' && typeof MutationObserver !== 'undefined' && typeof Node !== 'undefined' && svg instanceof Node) {
|
|||
|
|
var ownerObserver = new MutationObserver(function () { publishOwner(); });
|
|||
|
|
ownerObserver.observe(svg, {
|
|||
|
|
attributes: true,
|
|||
|
|
attributeFilter: [
|
|||
|
|
'data-story-playing', 'data-story-follow', 'data-story-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'
|
|||
|
|
]
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
syncVisibility();
|
|||
|
|
publishOwner();
|
|||
|
|
render();
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
capable: true,
|
|||
|
|
pause: function () { return setPaused(true); },
|
|||
|
|
resume: function () { return setPaused(false); },
|
|||
|
|
toggle: function () { return setPaused(!readerPaused); },
|
|||
|
|
setMode: function (next, options) {
|
|||
|
|
setPaused(next === 'still', options);
|
|||
|
|
return effectivePaused() ? 'still' : 'live';
|
|||
|
|
},
|
|||
|
|
mode: function () { return effectivePaused() ? 'still' : 'live'; },
|
|||
|
|
claim: claim,
|
|||
|
|
release: release,
|
|||
|
|
suspend: suspend,
|
|||
|
|
isPaused: effectivePaused,
|
|||
|
|
owner: function () { return owner; }
|
|||
|
|
};
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* Verified repository evidence is emitted only after the renderer checks
|
|||
|
|
the authored GitHub revision, source blobs, and optional line ranges
|
|||
|
|
against the explicit --repo-root. It remains outside the SVG so every
|
|||
|
|
canonical visual export stays free of repository paths. */
|
|||
|
|
Archify.sourceEvidence = (function () {
|
|||
|
|
var element = document.getElementById('archify-source-evidence-data');
|
|||
|
|
var payload = null;
|
|||
|
|
var svgNamespace = 'http://www.w3.org/2000/svg';
|
|||
|
|
if (element) {
|
|||
|
|
try {
|
|||
|
|
var parsed = JSON.parse(element.textContent || 'null');
|
|||
|
|
if (parsed && parsed.verified === true && parsed.repository && parsed.nodes) payload = parsed;
|
|||
|
|
} catch (_) {}
|
|||
|
|
}
|
|||
|
|
function installBeacons() {
|
|||
|
|
if (!payload) return 0;
|
|||
|
|
var svg = document.querySelector('.diagram-container svg');
|
|||
|
|
if (!svg) return 0;
|
|||
|
|
var installed = 0;
|
|||
|
|
Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id]'), function (node) {
|
|||
|
|
var id = node.getAttribute('data-node-id');
|
|||
|
|
var sources = payload.nodes[id];
|
|||
|
|
if (!Array.isArray(sources) || !sources.length || node.querySelector('[data-source-evidence-beacon]')) return;
|
|||
|
|
var shape = node.querySelector('[data-animate="node"]') || node.querySelector('[class*="c-"]');
|
|||
|
|
var box;
|
|||
|
|
try { box = (shape || node).getBBox(); } catch (_) { return; }
|
|||
|
|
if (!box || !Number.isFinite(box.x) || !Number.isFinite(box.y) || !Number.isFinite(box.width) || box.width < 36) return;
|
|||
|
|
|
|||
|
|
var count = sources.length;
|
|||
|
|
var label = viewerCount('viewer.passport.beacon', count);
|
|||
|
|
var beacon = document.createElementNS(svgNamespace, 'g');
|
|||
|
|
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 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');
|
|||
|
|
var text = document.createElementNS(svgNamespace, 'text');
|
|||
|
|
text.setAttribute('x', '15');
|
|||
|
|
text.setAttribute('y', '8.25');
|
|||
|
|
text.textContent = viewerText('viewer.passport.sourceMarker') + ' ' + count;
|
|||
|
|
beacon.appendChild(title);
|
|||
|
|
beacon.appendChild(plate);
|
|||
|
|
beacon.appendChild(text);
|
|||
|
|
node.appendChild(beacon);
|
|||
|
|
|
|||
|
|
var originalLabel = node.getAttribute('aria-label') || '';
|
|||
|
|
node.setAttribute('data-source-evidence-count', String(count));
|
|||
|
|
node.setAttribute('data-source-evidence-original-label', originalLabel);
|
|||
|
|
node.setAttribute('aria-label', (originalLabel ? originalLabel + ', ' : '') + viewerCount('viewer.passport.sourceCount', count));
|
|||
|
|
installed += 1;
|
|||
|
|
});
|
|||
|
|
return installed;
|
|||
|
|
}
|
|||
|
|
return {
|
|||
|
|
available: function () { return Boolean(payload); },
|
|||
|
|
repository: function () { return payload ? payload.repository : null; },
|
|||
|
|
node: function (id) {
|
|||
|
|
var sources = payload && payload.nodes ? payload.nodes[id] : null;
|
|||
|
|
return Array.isArray(sources) ? sources.slice() : [];
|
|||
|
|
},
|
|||
|
|
installBeacons: installBeacons
|
|||
|
|
};
|
|||
|
|
})();
|
|||
|
|
Archify.sourceEvidence.installBeacons();
|
|||
|
|
|
|||
|
|
/* ============================================================
|
|||
|
|
Explore — stable-ID neighborhood focus + dependency-free pan/zoom.
|
|||
|
|
Renderer IDs become deep-linkable semantic hooks without turning the
|
|||
|
|
standalone artifact into a canvas editor.
|
|||
|
|
============================================================ */
|
|||
|
|
Archify.focus = (function () {
|
|||
|
|
var html = document.documentElement;
|
|||
|
|
var container = document.querySelector('.diagram-container');
|
|||
|
|
var svg = container.querySelector('svg');
|
|||
|
|
var chip = document.getElementById('focus-chip');
|
|||
|
|
var label = document.getElementById('focus-label');
|
|||
|
|
var detail = document.getElementById('focus-detail');
|
|||
|
|
var kind = document.getElementById('focus-kind');
|
|||
|
|
var context = document.getElementById('focus-context');
|
|||
|
|
var tag = document.getElementById('focus-tag');
|
|||
|
|
var semanticId = document.getElementById('focus-id');
|
|||
|
|
var evidence = document.getElementById('focus-evidence');
|
|||
|
|
var repositoryLink = document.getElementById('focus-repository');
|
|||
|
|
var evidenceLinks = document.getElementById('focus-evidence-links');
|
|||
|
|
var summary = document.getElementById('focus-summary');
|
|||
|
|
var reachSection = document.getElementById('focus-reach');
|
|||
|
|
var reachStatus = document.getElementById('focus-reach-status');
|
|||
|
|
var upstreamBtn = document.getElementById('btn-reach-upstream');
|
|||
|
|
var downstreamBtn = document.getElementById('btn-reach-downstream');
|
|||
|
|
var upstreamCount = document.getElementById('focus-reach-upstream-count');
|
|||
|
|
var downstreamCount = document.getElementById('focus-reach-downstream-count');
|
|||
|
|
var relationshipList = document.getElementById('relationship-lens-list');
|
|||
|
|
var copyBtn = document.getElementById('btn-focus-copy');
|
|||
|
|
var relationsBtn = document.getElementById('btn-focus-relations');
|
|||
|
|
var clearBtn = document.getElementById('btn-focus-clear');
|
|||
|
|
var activeIds = [];
|
|||
|
|
var hoveredRelationship = null;
|
|||
|
|
var focusedRelationship = null;
|
|||
|
|
var pinnedRelationship = null;
|
|||
|
|
var pinnedRelationshipKey = null;
|
|||
|
|
var activeRelationshipPreview = null;
|
|||
|
|
var relationshipHitOverlay = null;
|
|||
|
|
var relationshipHitTargets = [];
|
|||
|
|
var directPreviewTimer = null;
|
|||
|
|
var reachabilityMode = null;
|
|||
|
|
var activeReachability = null;
|
|||
|
|
var svgNamespace = 'http://www.w3.org/2000/svg';
|
|||
|
|
var reducedMotionQuery = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)') : null;
|
|||
|
|
var finePointerQuery = window.matchMedia ? window.matchMedia('(hover: hover) and (pointer: fine)') : null;
|
|||
|
|
|
|||
|
|
function nodes() {
|
|||
|
|
return Array.prototype.slice.call(svg.querySelectorAll('[data-node-id]'));
|
|||
|
|
}
|
|||
|
|
function edges() {
|
|||
|
|
return Array.prototype.slice.call(svg.querySelectorAll('[data-edge-from][data-edge-to]'));
|
|||
|
|
}
|
|||
|
|
function nodeLabel(node, fallback) {
|
|||
|
|
return node.getAttribute('data-node-label') || (node.getAttribute('aria-label') || fallback).replace(/^Focus\s+/, '');
|
|||
|
|
}
|
|||
|
|
function reachabilityRelationships() {
|
|||
|
|
var seen = Object.create(null);
|
|||
|
|
var relationships = [];
|
|||
|
|
edges().forEach(function (edge) {
|
|||
|
|
var from = edge.getAttribute('data-edge-from');
|
|||
|
|
var to = edge.getAttribute('data-edge-to');
|
|||
|
|
var key = edge.getAttribute('data-edge-key') || (from + '\u0000' + to + '\u0000' + (edge.getAttribute('data-edge-label') || ''));
|
|||
|
|
if (!from || !to || seen[key]) return;
|
|||
|
|
seen[key] = true;
|
|||
|
|
relationships.push({ key: key, from: from, to: to });
|
|||
|
|
});
|
|||
|
|
return relationships;
|
|||
|
|
}
|
|||
|
|
function computeReachability(originId, direction, relationships) {
|
|||
|
|
if (typeof originId !== 'string' || !originId ||
|
|||
|
|
(direction !== 'upstream' && direction !== 'downstream') ||
|
|||
|
|
!Array.isArray(relationships)) return null;
|
|||
|
|
var records = [];
|
|||
|
|
var seenKeys = Object.create(null);
|
|||
|
|
relationships.forEach(function (relationship, index) {
|
|||
|
|
if (!relationship || typeof relationship.from !== 'string' || !relationship.from ||
|
|||
|
|
typeof relationship.to !== 'string' || !relationship.to) return;
|
|||
|
|
var key = typeof relationship.key === 'string' && relationship.key
|
|||
|
|
? relationship.key : String(index);
|
|||
|
|
if (seenKeys[key]) return;
|
|||
|
|
seenKeys[key] = true;
|
|||
|
|
records.push({ key: key, from: relationship.from, to: relationship.to });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
var depths = Object.create(null);
|
|||
|
|
var order = [];
|
|||
|
|
var queue = [originId];
|
|||
|
|
depths[originId] = 0;
|
|||
|
|
for (var cursor = 0; cursor < queue.length; cursor += 1) {
|
|||
|
|
var current = queue[cursor];
|
|||
|
|
records.forEach(function (relationship) {
|
|||
|
|
var next = null;
|
|||
|
|
if (direction === 'downstream' && relationship.from === current) next = relationship.to;
|
|||
|
|
else if (direction === 'upstream' && relationship.to === current) next = relationship.from;
|
|||
|
|
if (next == null || Object.prototype.hasOwnProperty.call(depths, next)) return;
|
|||
|
|
depths[next] = depths[current] + 1;
|
|||
|
|
queue.push(next);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
queue.forEach(function (id) { order.push(id); });
|
|||
|
|
|
|||
|
|
var edgeKeys = [];
|
|||
|
|
records.forEach(function (relationship) {
|
|||
|
|
if (Object.prototype.hasOwnProperty.call(depths, relationship.from) &&
|
|||
|
|
Object.prototype.hasOwnProperty.call(depths, relationship.to)) edgeKeys.push(relationship.key);
|
|||
|
|
});
|
|||
|
|
var maxDepth = order.reduce(function (maximum, id) {
|
|||
|
|
return Math.max(maximum, depths[id]);
|
|||
|
|
}, 0);
|
|||
|
|
return {
|
|||
|
|
direction: direction,
|
|||
|
|
originId: originId,
|
|||
|
|
nodeIds: order,
|
|||
|
|
edgeKeys: edgeKeys,
|
|||
|
|
depths: depths,
|
|||
|
|
maxDepth: maxDepth
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
function reachabilityFor(id, direction) {
|
|||
|
|
return computeReachability(id, direction, reachabilityRelationships());
|
|||
|
|
}
|
|||
|
|
function resetReachabilityButtons() {
|
|||
|
|
upstreamBtn.setAttribute('aria-pressed', 'false');
|
|||
|
|
downstreamBtn.setAttribute('aria-pressed', 'false');
|
|||
|
|
}
|
|||
|
|
function clearReachability(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
reachabilityMode = null;
|
|||
|
|
activeReachability = null;
|
|||
|
|
svg.removeAttribute('data-reach-active');
|
|||
|
|
chip.removeAttribute('data-reach-mode');
|
|||
|
|
nodes().forEach(function (node) {
|
|||
|
|
node.removeAttribute('data-reach-match');
|
|||
|
|
node.removeAttribute('data-reach-origin');
|
|||
|
|
node.removeAttribute('data-reach-depth');
|
|||
|
|
});
|
|||
|
|
edges().forEach(function (edge) {
|
|||
|
|
edge.removeAttribute('data-reach-match');
|
|||
|
|
edge.removeAttribute('data-reach-depth');
|
|||
|
|
});
|
|||
|
|
resetReachabilityButtons();
|
|||
|
|
reachStatus.textContent = '';
|
|||
|
|
reachStatus.hidden = true;
|
|||
|
|
if (Archify.exportMenu && typeof Archify.exportMenu.syncReachShare === 'function') {
|
|||
|
|
Archify.exportMenu.syncReachShare();
|
|||
|
|
}
|
|||
|
|
if (options.updateUrl === true && activeIds.length === 1) {
|
|||
|
|
try {
|
|||
|
|
history.replaceState(null, '', location.pathname + location.search + '#focus=' + encodeURIComponent(activeIds[0]));
|
|||
|
|
} catch (_) {}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
function renderReachabilityControls(id) {
|
|||
|
|
var upstream = reachabilityFor(id, 'upstream');
|
|||
|
|
var downstream = reachabilityFor(id, 'downstream');
|
|||
|
|
var upstreamReach = upstream ? Math.max(0, upstream.nodeIds.length - 1) : 0;
|
|||
|
|
var downstreamReach = downstream ? Math.max(0, downstream.nodeIds.length - 1) : 0;
|
|||
|
|
upstreamCount.textContent = String(upstreamReach);
|
|||
|
|
downstreamCount.textContent = String(downstreamReach);
|
|||
|
|
upstreamBtn.disabled = upstreamReach === 0;
|
|||
|
|
downstreamBtn.disabled = downstreamReach === 0;
|
|||
|
|
upstreamBtn.setAttribute('aria-label', upstreamReach
|
|||
|
|
? viewerCount('viewer.passport.reach.upstream', upstreamReach)
|
|||
|
|
: viewerText('viewer.passport.reach.noUpstream'));
|
|||
|
|
downstreamBtn.setAttribute('aria-label', downstreamReach
|
|||
|
|
? viewerCount('viewer.passport.reach.downstream', downstreamReach)
|
|||
|
|
: viewerText('viewer.passport.reach.noDownstream'));
|
|||
|
|
reachSection.hidden = false;
|
|||
|
|
}
|
|||
|
|
function applyReachability(direction, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (activeIds.length !== 1 || (direction !== 'upstream' && direction !== 'downstream')) return false;
|
|||
|
|
if (reachabilityMode === direction && options.toggle !== false) {
|
|||
|
|
clearReachability({ updateUrl: options.updateUrl !== false });
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
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;
|
|||
|
|
activeReachability = result;
|
|||
|
|
var edgeKeySet = Object.create(null);
|
|||
|
|
result.edgeKeys.forEach(function (key) { edgeKeySet[key] = true; });
|
|||
|
|
svg.setAttribute('data-reach-active', direction);
|
|||
|
|
chip.setAttribute('data-reach-mode', direction);
|
|||
|
|
nodes().forEach(function (node) {
|
|||
|
|
var id = node.getAttribute('data-node-id');
|
|||
|
|
if (!Object.prototype.hasOwnProperty.call(result.depths, id)) return;
|
|||
|
|
node.setAttribute('data-reach-match', '');
|
|||
|
|
node.setAttribute('data-reach-depth', String(result.depths[id]));
|
|||
|
|
if (id === result.originId) node.setAttribute('data-reach-origin', '');
|
|||
|
|
});
|
|||
|
|
edges().forEach(function (edge) {
|
|||
|
|
var key = edge.getAttribute('data-edge-key') || (
|
|||
|
|
edge.getAttribute('data-edge-from') + '\u0000' +
|
|||
|
|
edge.getAttribute('data-edge-to') + '\u0000' +
|
|||
|
|
(edge.getAttribute('data-edge-label') || '')
|
|||
|
|
);
|
|||
|
|
if (!edgeKeySet[key]) return;
|
|||
|
|
edge.setAttribute('data-reach-match', '');
|
|||
|
|
var fromDepth = result.depths[edge.getAttribute('data-edge-from')];
|
|||
|
|
var toDepth = result.depths[edge.getAttribute('data-edge-to')];
|
|||
|
|
edge.setAttribute('data-reach-depth', String(Math.max(fromDepth || 0, toDepth || 0)));
|
|||
|
|
});
|
|||
|
|
resetReachabilityButtons();
|
|||
|
|
var activeButton = direction === 'upstream' ? upstreamBtn : downstreamBtn;
|
|||
|
|
activeButton.setAttribute('aria-pressed', 'true');
|
|||
|
|
var reachableCount = result.nodeIds.length - 1;
|
|||
|
|
var directionLabel = viewerText(direction === 'upstream'
|
|||
|
|
? 'viewer.passport.upstream'
|
|||
|
|
: 'viewer.passport.downstream');
|
|||
|
|
reachStatus.textContent = viewerText('viewer.passport.reach.status', {
|
|||
|
|
direction: directionLabel,
|
|||
|
|
nodes: reachableCount,
|
|||
|
|
links: result.edgeKeys.length,
|
|||
|
|
hops: result.maxDepth
|
|||
|
|
});
|
|||
|
|
reachStatus.hidden = false;
|
|||
|
|
if (Archify.exportMenu && typeof Archify.exportMenu.syncReachShare === 'function') {
|
|||
|
|
Archify.exportMenu.syncReachShare();
|
|||
|
|
}
|
|||
|
|
if (options.updateUrl !== false) {
|
|||
|
|
try {
|
|||
|
|
history.replaceState(null, '', location.pathname + location.search + '#focus=' +
|
|||
|
|
encodeURIComponent(activeIds[0]) + '&reach=' + direction);
|
|||
|
|
} catch (_) {}
|
|||
|
|
}
|
|||
|
|
if (options.reveal !== false && Archify.view && typeof Archify.view.reveal === 'function') {
|
|||
|
|
Archify.view.reveal(result.nodeIds, { includeNeighbors: false, reason: 'reachability' });
|
|||
|
|
}
|
|||
|
|
requestLensPlacement();
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function reachabilitySnapshot() {
|
|||
|
|
if (!activeReachability || activeIds.length !== 1 ||
|
|||
|
|
(reachabilityMode !== 'upstream' && reachabilityMode !== 'downstream') ||
|
|||
|
|
activeReachability.direction !== reachabilityMode ||
|
|||
|
|
activeReachability.originId !== activeIds[0] ||
|
|||
|
|
svg.getAttribute('data-reach-active') !== reachabilityMode) return null;
|
|||
|
|
|
|||
|
|
var originId = activeReachability.originId;
|
|||
|
|
var nodeIds = activeReachability.nodeIds.slice();
|
|||
|
|
var edgeKeys = activeReachability.edgeKeys.slice();
|
|||
|
|
if (nodeIds.length < 2 || nodeIds[0] !== originId || !edgeKeys.length ||
|
|||
|
|
!activeReachability.depths || !Number.isInteger(activeReachability.maxDepth) ||
|
|||
|
|
activeReachability.maxDepth < 1) return null;
|
|||
|
|
|
|||
|
|
var allNodes = nodes();
|
|||
|
|
var allEdges = edges();
|
|||
|
|
var seenNodeIds = Object.create(null);
|
|||
|
|
var seenEdgeKeys = Object.create(null);
|
|||
|
|
var nodeIdSet = Object.create(null);
|
|||
|
|
var depths = Object.create(null);
|
|||
|
|
var originNode = null;
|
|||
|
|
var measuredMaxDepth = 0;
|
|||
|
|
|
|||
|
|
if (nodeIds.some(function (id) {
|
|||
|
|
var depth = activeReachability.depths[id];
|
|||
|
|
var matches = allNodes.filter(function (node) {
|
|||
|
|
return node.getAttribute('data-node-id') === id;
|
|||
|
|
});
|
|||
|
|
if (typeof id !== 'string' || !id || seenNodeIds[id] || matches.length !== 1 ||
|
|||
|
|
!matches[0].hasAttribute('data-reach-match') ||
|
|||
|
|
!Number.isInteger(depth) || depth < 0 || depth > activeReachability.maxDepth ||
|
|||
|
|
(id === originId
|
|||
|
|
? (!matches[0].hasAttribute('data-reach-origin') || depth !== 0)
|
|||
|
|
: depth < 1)) return true;
|
|||
|
|
seenNodeIds[id] = true;
|
|||
|
|
nodeIdSet[id] = true;
|
|||
|
|
depths[id] = depth;
|
|||
|
|
measuredMaxDepth = Math.max(measuredMaxDepth, depth);
|
|||
|
|
if (id === originId) originNode = matches[0];
|
|||
|
|
return false;
|
|||
|
|
}) || !originNode || measuredMaxDepth !== activeReachability.maxDepth ||
|
|||
|
|
allNodes.filter(function (node) { return node.hasAttribute('data-reach-match'); }).length !== nodeIds.length) return null;
|
|||
|
|
|
|||
|
|
var edgeRecords = [];
|
|||
|
|
if (edgeKeys.some(function (key) {
|
|||
|
|
if (typeof key !== 'string' || !key || seenEdgeKeys[key]) return true;
|
|||
|
|
var fragments = allEdges.filter(function (edge) {
|
|||
|
|
return edge.getAttribute('data-edge-key') === key;
|
|||
|
|
});
|
|||
|
|
var drawableFragments = fragments.filter(hasDrawableGeometry);
|
|||
|
|
if (!fragments.length || drawableFragments.length !== 1 ||
|
|||
|
|
!fragments.every(function (fragment) { return fragment.hasAttribute('data-reach-match'); })) return true;
|
|||
|
|
var first = fragments[0];
|
|||
|
|
var from = first.getAttribute('data-edge-from');
|
|||
|
|
var to = first.getAttribute('data-edge-to');
|
|||
|
|
var id = first.getAttribute('data-edge-id') || '';
|
|||
|
|
var labelValue = first.getAttribute('data-edge-label') || '';
|
|||
|
|
if (!nodeIdSet[from] || !nodeIdSet[to] || !fragments.every(function (fragment) {
|
|||
|
|
return fragment.getAttribute('data-edge-from') === from &&
|
|||
|
|
fragment.getAttribute('data-edge-to') === to &&
|
|||
|
|
(fragment.getAttribute('data-edge-id') || '') === id;
|
|||
|
|
})) return true;
|
|||
|
|
seenEdgeKeys[key] = true;
|
|||
|
|
edgeRecords.push({
|
|||
|
|
key: key,
|
|||
|
|
id: id,
|
|||
|
|
from: from,
|
|||
|
|
to: to,
|
|||
|
|
label: labelValue,
|
|||
|
|
depth: Math.max(depths[from], depths[to])
|
|||
|
|
});
|
|||
|
|
return false;
|
|||
|
|
})) return null;
|
|||
|
|
|
|||
|
|
var liveEdgeKeys = Object.create(null);
|
|||
|
|
if (allEdges.some(function (edge) {
|
|||
|
|
if (!edge.hasAttribute('data-reach-match')) return false;
|
|||
|
|
var key = edge.getAttribute('data-edge-key');
|
|||
|
|
if (!key || !seenEdgeKeys[key]) return true;
|
|||
|
|
liveEdgeKeys[key] = true;
|
|||
|
|
return false;
|
|||
|
|
}) || Object.keys(liveEdgeKeys).length !== edgeKeys.length) return null;
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
direction: reachabilityMode,
|
|||
|
|
origin: { id: originId, label: nodeLabel(originNode, originId) },
|
|||
|
|
nodeIds: nodeIds,
|
|||
|
|
depths: depths,
|
|||
|
|
maxDepth: activeReachability.maxDepth,
|
|||
|
|
edges: edgeRecords
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
function setPassportValue(element, value) {
|
|||
|
|
var normalized = value == null ? '' : String(value).trim();
|
|||
|
|
element.textContent = normalized;
|
|||
|
|
element.hidden = !normalized;
|
|||
|
|
}
|
|||
|
|
function renderSourceEvidence(id) {
|
|||
|
|
evidenceLinks.textContent = '';
|
|||
|
|
repositoryLink.removeAttribute('href');
|
|||
|
|
repositoryLink.textContent = '';
|
|||
|
|
var sources = Archify.sourceEvidence.node(id);
|
|||
|
|
var repository = Archify.sourceEvidence.repository();
|
|||
|
|
if (!repository || !sources.length) {
|
|||
|
|
evidence.hidden = true;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
var slug = repository.url.replace(/^https:\/\/github\.com\//, '').replace(/\/$/, '');
|
|||
|
|
repositoryLink.href = repository.url + '/tree/' + repository.revision;
|
|||
|
|
repositoryLink.textContent = slug + ' @ ' + repository.shortRevision;
|
|||
|
|
repositoryLink.setAttribute('aria-label', viewerText('viewer.passport.repository.open', { revision: repository.revision }));
|
|||
|
|
sources.forEach(function (source) {
|
|||
|
|
var link = document.createElement('a');
|
|||
|
|
link.className = 'semantic-passport-source';
|
|||
|
|
link.href = source.href;
|
|||
|
|
link.target = '_blank';
|
|||
|
|
link.rel = 'noopener noreferrer';
|
|||
|
|
link.referrerPolicy = 'no-referrer';
|
|||
|
|
link.setAttribute('aria-label', viewerText('viewer.passport.source.open', { path: source.path, revision: repository.shortRevision }));
|
|||
|
|
var name = document.createElement('strong');
|
|||
|
|
name.textContent = source.label || source.path.split('/').pop() || source.path;
|
|||
|
|
var location = document.createElement('code');
|
|||
|
|
location.textContent = source.line
|
|||
|
|
? 'L' + source.line + (source.endLine && source.endLine !== source.line ? '–' + source.endLine : '') + ' ↗'
|
|||
|
|
: viewerText('viewer.passport.source.openLink');
|
|||
|
|
var sourcePath = document.createElement('small');
|
|||
|
|
sourcePath.textContent = source.path;
|
|||
|
|
link.appendChild(name);
|
|||
|
|
link.appendChild(location);
|
|||
|
|
link.appendChild(sourcePath);
|
|||
|
|
evidenceLinks.appendChild(link);
|
|||
|
|
});
|
|||
|
|
evidence.hidden = false;
|
|||
|
|
}
|
|||
|
|
function renderPassport(id, node) {
|
|||
|
|
setPassportValue(detail, node.getAttribute('data-node-sublabel'));
|
|||
|
|
setPassportValue(kind, viewerKindLabel(node.getAttribute('data-node-kind') || 'node'));
|
|||
|
|
setPassportValue(context, node.getAttribute('data-node-context'));
|
|||
|
|
setPassportValue(tag, node.getAttribute('data-node-tag'));
|
|||
|
|
setPassportValue(document.getElementById('focus-brand'), node.getAttribute('data-node-brand'));
|
|||
|
|
semanticId.textContent = id;
|
|||
|
|
semanticId.hidden = false;
|
|||
|
|
renderSourceEvidence(id);
|
|||
|
|
}
|
|||
|
|
function relationshipsFor(id, byId) {
|
|||
|
|
var seen = {};
|
|||
|
|
var relationships = [];
|
|||
|
|
edges().forEach(function (edge) {
|
|||
|
|
var from = edge.getAttribute('data-edge-from');
|
|||
|
|
var to = edge.getAttribute('data-edge-to');
|
|||
|
|
if (from !== id && to !== id) return;
|
|||
|
|
var edgeLabel = edge.getAttribute('data-edge-label') || '';
|
|||
|
|
var edgeKey = edge.getAttribute('data-edge-key') || (from + '\u0000' + to + '\u0000' + edgeLabel);
|
|||
|
|
var edgeId = edge.getAttribute('data-edge-id') || '';
|
|||
|
|
if (seen[edgeKey]) return;
|
|||
|
|
seen[edgeKey] = true;
|
|||
|
|
var direction = from === id && to === id ? 'loop' : (from === id ? 'out' : 'in');
|
|||
|
|
var neighborId = direction === 'in' ? from : to;
|
|||
|
|
var neighbor = byId[neighborId];
|
|||
|
|
relationships.push({
|
|||
|
|
key: edgeKey,
|
|||
|
|
id: edgeId,
|
|||
|
|
from: from,
|
|||
|
|
to: to,
|
|||
|
|
direction: direction,
|
|||
|
|
neighborId: neighborId,
|
|||
|
|
neighborLabel: neighbor ? nodeLabel(neighbor, neighborId) : neighborId,
|
|||
|
|
label: edgeLabel || viewerText(direction === 'loop'
|
|||
|
|
? 'viewer.passport.relationship.loopsBack'
|
|||
|
|
: direction === 'out'
|
|||
|
|
? 'viewer.passport.relationship.connectsTo'
|
|||
|
|
: 'viewer.passport.relationship.connectsFrom')
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
return relationships;
|
|||
|
|
}
|
|||
|
|
function relationshipEdgeShapes(edge) {
|
|||
|
|
if (!edge) return [];
|
|||
|
|
if (/^(path|line|polyline)$/i.test(edge.tagName || '')) return [edge];
|
|||
|
|
return Array.prototype.slice.call(edge.querySelectorAll('path, line, polyline'));
|
|||
|
|
}
|
|||
|
|
function relationshipHitRecords() {
|
|||
|
|
var recordsByKey = {};
|
|||
|
|
var recordsById = {};
|
|||
|
|
var byId = {};
|
|||
|
|
nodes().forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; });
|
|||
|
|
var records = [];
|
|||
|
|
edges().forEach(function (edge) {
|
|||
|
|
var shapes = relationshipEdgeShapes(edge);
|
|||
|
|
if (!shapes.length) return;
|
|||
|
|
var from = edge.getAttribute('data-edge-from');
|
|||
|
|
var to = edge.getAttribute('data-edge-to');
|
|||
|
|
var labelValue = edge.getAttribute('data-edge-label') || '';
|
|||
|
|
var edgeId = edge.getAttribute('data-edge-id') || '';
|
|||
|
|
var key = edge.getAttribute('data-edge-key') || (from + '\u0000' + to + '\u0000' + labelValue);
|
|||
|
|
var existing = recordsByKey[key];
|
|||
|
|
if (existing) {
|
|||
|
|
if (existing.from !== from || existing.to !== to || existing.labelValue !== labelValue || existing.id !== edgeId) {
|
|||
|
|
existing.invalid = true;
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
shapes.forEach(function (shape) {
|
|||
|
|
if (existing.shapes.indexOf(shape) === -1) existing.shapes.push(shape);
|
|||
|
|
});
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
var record = {
|
|||
|
|
key: key,
|
|||
|
|
id: edgeId,
|
|||
|
|
from: from,
|
|||
|
|
to: to,
|
|||
|
|
fromLabel: byId[from] ? nodeLabel(byId[from], from) : from,
|
|||
|
|
toLabel: byId[to] ? nodeLabel(byId[to], to) : to,
|
|||
|
|
labelValue: labelValue,
|
|||
|
|
label: labelValue || viewerText(from === to
|
|||
|
|
? 'viewer.passport.relationship.loopsBack'
|
|||
|
|
: 'viewer.passport.relationship.connectsTo'),
|
|||
|
|
edge: edge,
|
|||
|
|
shapes: shapes,
|
|||
|
|
invalid: false
|
|||
|
|
};
|
|||
|
|
recordsByKey[key] = record;
|
|||
|
|
if (edgeId) {
|
|||
|
|
if (recordsById[edgeId]) {
|
|||
|
|
recordsById[edgeId].invalid = true;
|
|||
|
|
record.invalid = true;
|
|||
|
|
} else {
|
|||
|
|
recordsById[edgeId] = record;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
records.push(record);
|
|||
|
|
});
|
|||
|
|
return records.filter(function (record) { return !record.invalid && record.from && record.to; });
|
|||
|
|
}
|
|||
|
|
function relationshipRecordForKey(key) {
|
|||
|
|
return relationshipHitRecords().find(function (record) { return record.key === key; }) || null;
|
|||
|
|
}
|
|||
|
|
function pinnedRelationshipRecord() {
|
|||
|
|
return pinnedRelationshipKey ? relationshipRecordForKey(pinnedRelationshipKey) : null;
|
|||
|
|
}
|
|||
|
|
function renderRelationshipCopyAction() {
|
|||
|
|
var record = pinnedRelationshipRecord();
|
|||
|
|
if (record && record.id) {
|
|||
|
|
copyBtn.textContent = viewerText('viewer.passport.copyRelation');
|
|||
|
|
copyBtn.setAttribute('aria-label', viewerText('viewer.passport.copyPinned'));
|
|||
|
|
} else if (pinnedRelationshipKey) {
|
|||
|
|
copyBtn.textContent = viewerText('viewer.passport.copyNode');
|
|||
|
|
copyBtn.setAttribute('aria-label', viewerText('viewer.passport.copySource'));
|
|||
|
|
} else {
|
|||
|
|
copyBtn.textContent = viewerText('viewer.passport.copy');
|
|||
|
|
copyBtn.setAttribute('aria-label', viewerText('viewer.passport.copy.focus'));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
function relationshipHitGeometry(shape, className) {
|
|||
|
|
var clone = shape.cloneNode(false);
|
|||
|
|
clone.removeAttribute('id');
|
|||
|
|
clone.removeAttribute('class');
|
|||
|
|
clone.removeAttribute('style');
|
|||
|
|
clone.removeAttribute('filter');
|
|||
|
|
clone.removeAttribute('marker-start');
|
|||
|
|
clone.removeAttribute('marker-mid');
|
|||
|
|
clone.removeAttribute('marker-end');
|
|||
|
|
clone.removeAttribute('role');
|
|||
|
|
clone.removeAttribute('tabindex');
|
|||
|
|
clone.removeAttribute('aria-label');
|
|||
|
|
clone.removeAttribute('aria-labelledby');
|
|||
|
|
clone.removeAttribute('aria-hidden');
|
|||
|
|
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', className || 'relationship-hit-rail');
|
|||
|
|
return clone;
|
|||
|
|
}
|
|||
|
|
function removeRelationshipPulse() {
|
|||
|
|
Array.prototype.forEach.call(svg.querySelectorAll('[data-relationship-pulse-overlay]'), function (element) {
|
|||
|
|
element.remove();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function relationshipPulseGeometry(shape) {
|
|||
|
|
var clone = shape.cloneNode(false);
|
|||
|
|
clone.removeAttribute('id');
|
|||
|
|
clone.removeAttribute('class');
|
|||
|
|
clone.removeAttribute('style');
|
|||
|
|
clone.removeAttribute('transform');
|
|||
|
|
clone.removeAttribute('filter');
|
|||
|
|
clone.removeAttribute('marker-start');
|
|||
|
|
clone.removeAttribute('marker-mid');
|
|||
|
|
clone.removeAttribute('marker-end');
|
|||
|
|
clone.removeAttribute('role');
|
|||
|
|
clone.removeAttribute('tabindex');
|
|||
|
|
clone.removeAttribute('aria-label');
|
|||
|
|
clone.removeAttribute('aria-hidden');
|
|||
|
|
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.removeAttribute('data-focus-match');
|
|||
|
|
clone.removeAttribute('data-relationship-preview');
|
|||
|
|
clone.setAttribute('class', 'relationship-flow-pulse');
|
|||
|
|
clone.setAttribute('pathLength', '1');
|
|||
|
|
return clone;
|
|||
|
|
}
|
|||
|
|
function relationshipTokenKind(edge) {
|
|||
|
|
var shapes = relationshipEdgeShapes(edge);
|
|||
|
|
var classEvidence = [edge.getAttribute('class') || ''].concat(shapes.map(function (shape) {
|
|||
|
|
return shape.getAttribute('class') || '';
|
|||
|
|
})).join(' ');
|
|||
|
|
var from = edge.getAttribute('data-edge-from') || '';
|
|||
|
|
var to = edge.getAttribute('data-edge-to') || '';
|
|||
|
|
var source = nodes().find(function (node) { return node.getAttribute('data-node-id') === from; });
|
|||
|
|
var target = nodes().find(function (node) { return node.getAttribute('data-node-id') === to; });
|
|||
|
|
var sourceKind = source ? source.getAttribute('data-node-kind') || 'neutral' : 'neutral';
|
|||
|
|
var targetKind = target ? target.getAttribute('data-node-kind') || 'neutral' : 'neutral';
|
|||
|
|
if (/\ba-security\b/.test(classEvidence) || sourceKind === 'security' || targetKind === 'security' || targetKind === 'failure') return 'security';
|
|||
|
|
if (/\ba-dashed\b/.test(classEvidence) || sourceKind === 'messagebus' || targetKind === 'messagebus') return 'event';
|
|||
|
|
if (sourceKind === 'database' || targetKind === 'database') return 'data';
|
|||
|
|
if (targetKind === 'waiting' || targetKind === 'success') return 'state';
|
|||
|
|
return 'call';
|
|||
|
|
}
|
|||
|
|
function relationshipTokenPath(shape) {
|
|||
|
|
if (!shape) return '';
|
|||
|
|
var tagName = String(shape.tagName || '').toLowerCase();
|
|||
|
|
if (tagName === 'path') return shape.getAttribute('d') || '';
|
|||
|
|
if (tagName === 'line') {
|
|||
|
|
return 'M ' + shape.getAttribute('x1') + ' ' + shape.getAttribute('y1') +
|
|||
|
|
' L ' + shape.getAttribute('x2') + ' ' + shape.getAttribute('y2');
|
|||
|
|
}
|
|||
|
|
if (tagName === 'polyline' && shape.points && shape.points.numberOfItems > 1) {
|
|||
|
|
var commands = [];
|
|||
|
|
for (var i = 0; i < shape.points.numberOfItems; i += 1) {
|
|||
|
|
var point = shape.points.getItem(i);
|
|||
|
|
commands.push((i === 0 ? 'M ' : 'L ') + point.x + ' ' + point.y);
|
|||
|
|
}
|
|||
|
|
return commands.join(' ');
|
|||
|
|
}
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
function relationshipTokenPart(tagName, className, attrs) {
|
|||
|
|
var part = document.createElementNS(svgNamespace, tagName);
|
|||
|
|
part.setAttribute('class', className);
|
|||
|
|
Object.keys(attrs || {}).forEach(function (name) { part.setAttribute(name, attrs[name]); });
|
|||
|
|
return part;
|
|||
|
|
}
|
|||
|
|
function relationshipTokenGeometry(shape, kind, key, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
var pathData = relationshipTokenPath(shape);
|
|||
|
|
if (!pathData) return null;
|
|||
|
|
var token = document.createElementNS(svgNamespace, 'g');
|
|||
|
|
token.setAttribute('class', 'semantic-flow-token ' + (options.className || 'relationship-flow-token'));
|
|||
|
|
token.setAttribute('data-token-kind', kind);
|
|||
|
|
token.setAttribute('data-token-edge-key', key);
|
|||
|
|
token.setAttribute('aria-hidden', 'true');
|
|||
|
|
token.appendChild(relationshipTokenPart('circle', 'semantic-flow-token-halo', { cx: '0', cy: '0', r: '7' }));
|
|||
|
|
if (kind === 'data') {
|
|||
|
|
token.appendChild(relationshipTokenPart('rect', 'relationship-flow-token-shape', { x: '-5', y: '-4', width: '10', height: '8', rx: '2' }));
|
|||
|
|
token.appendChild(relationshipTokenPart('path', 'relationship-flow-token-ink', { d: 'M -2.8 -1.2 h 5.6 M -2.8 1.4 h 3.8' }));
|
|||
|
|
} else if (kind === 'event') {
|
|||
|
|
token.appendChild(relationshipTokenPart('rect', 'relationship-flow-token-shape', { x: '-7', y: '-3', width: '4', height: '6', rx: '1' }));
|
|||
|
|
token.appendChild(relationshipTokenPart('rect', 'relationship-flow-token-shape', { x: '-2', y: '-3', width: '4', height: '6', rx: '1' }));
|
|||
|
|
token.appendChild(relationshipTokenPart('rect', 'relationship-flow-token-shape', { x: '3', y: '-3', width: '4', height: '6', rx: '1' }));
|
|||
|
|
} else if (kind === 'security') {
|
|||
|
|
token.appendChild(relationshipTokenPart('path', 'relationship-flow-token-shape', { d: 'M 0 -5 L 4 -3.4 V 0 c 0 3 -1.6 4.5 -4 5.5 C -2.4 4.5 -4 3 -4 0 v -3.4 Z' }));
|
|||
|
|
token.appendChild(relationshipTokenPart('path', 'relationship-flow-token-ink', { d: 'm -2 .2 1.4 1.4 L 2 -1.4' }));
|
|||
|
|
} else if (kind === 'state') {
|
|||
|
|
token.appendChild(relationshipTokenPart('circle', 'relationship-flow-token-shape', { cx: '0', cy: '0', r: '5' }));
|
|||
|
|
token.appendChild(relationshipTokenPart('circle', 'relationship-flow-token-dot', { cx: '0', cy: '0', r: '1.35' }));
|
|||
|
|
} else {
|
|||
|
|
token.appendChild(relationshipTokenPart('path', 'relationship-flow-token-ink', { d: 'M -5 -3 L -1 0 L -5 3 M 0 -3 L 4 0 L 0 3' }));
|
|||
|
|
}
|
|||
|
|
var motion = document.createElementNS(svgNamespace, 'animateMotion');
|
|||
|
|
motion.setAttribute('path', pathData);
|
|||
|
|
motion.setAttribute('dur', options.duration || '1.2s');
|
|||
|
|
motion.setAttribute('begin', '0s');
|
|||
|
|
motion.setAttribute('fill', 'freeze');
|
|||
|
|
motion.setAttribute('rotate', 'auto');
|
|||
|
|
motion.setAttribute('calcMode', 'spline');
|
|||
|
|
motion.setAttribute('keyTimes', '0;1');
|
|||
|
|
motion.setAttribute('keySplines', '.2 0 .2 1');
|
|||
|
|
token.appendChild(motion);
|
|||
|
|
return token;
|
|||
|
|
}
|
|||
|
|
function createSemanticFlowToken(edge, shape, options) {
|
|||
|
|
if (!edge || !shape) return null;
|
|||
|
|
var key = edge.getAttribute('data-edge-key') || (
|
|||
|
|
edge.getAttribute('data-edge-from') + '\u0000' +
|
|||
|
|
edge.getAttribute('data-edge-to') + '\u0000' +
|
|||
|
|
(edge.getAttribute('data-edge-label') || '')
|
|||
|
|
);
|
|||
|
|
return relationshipTokenGeometry(shape, relationshipTokenKind(edge), key, options);
|
|||
|
|
}
|
|||
|
|
Archify.flowTokens = {
|
|||
|
|
create: createSemanticFlowToken,
|
|||
|
|
kind: function (edge) { return relationshipTokenKind(edge); },
|
|||
|
|
path: relationshipTokenPath
|
|||
|
|
};
|
|||
|
|
function renderRelationshipPulse(key) {
|
|||
|
|
removeRelationshipPulse();
|
|||
|
|
if (!key || document.documentElement.getAttribute('data-embed') === 'true') return false;
|
|||
|
|
if (document.hidden || (Archify.motionGovernor && Archify.motionGovernor.isPaused())) return false;
|
|||
|
|
if (reducedMotionQuery && reducedMotionQuery.matches) return false;
|
|||
|
|
var matchingEdges = edges().filter(function (edge) {
|
|||
|
|
return edge.getAttribute('data-edge-key') === key;
|
|||
|
|
});
|
|||
|
|
if (!matchingEdges.length) return false;
|
|||
|
|
var overlay = document.createElementNS(svgNamespace, 'g');
|
|||
|
|
overlay.setAttribute('class', 'relationship-pulse-overlay');
|
|||
|
|
overlay.setAttribute('data-relationship-pulse-overlay', '');
|
|||
|
|
overlay.setAttribute('data-relationship-pulse-key', key);
|
|||
|
|
overlay.setAttribute('aria-hidden', 'true');
|
|||
|
|
var tokenAdded = false;
|
|||
|
|
matchingEdges.forEach(function (edge) {
|
|||
|
|
var wrapper = document.createElementNS(svgNamespace, 'g');
|
|||
|
|
if (edge.hasAttribute('transform')) wrapper.setAttribute('transform', edge.getAttribute('transform'));
|
|||
|
|
var shapes = relationshipEdgeShapes(edge);
|
|||
|
|
shapes.forEach(function (shape) {
|
|||
|
|
wrapper.appendChild(relationshipPulseGeometry(shape));
|
|||
|
|
});
|
|||
|
|
if (!tokenAdded && shapes.length) {
|
|||
|
|
var tokenKind = relationshipTokenKind(edge);
|
|||
|
|
var token = relationshipTokenGeometry(shapes[0], tokenKind, key);
|
|||
|
|
if (token) {
|
|||
|
|
wrapper.appendChild(token);
|
|||
|
|
overlay.setAttribute('data-relationship-token-kind', tokenKind);
|
|||
|
|
tokenAdded = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (wrapper.childNodes.length) overlay.appendChild(wrapper);
|
|||
|
|
});
|
|||
|
|
if (!overlay.childNodes.length) return false;
|
|||
|
|
var finishPulse = function () {
|
|||
|
|
if (overlay.parentNode) overlay.remove();
|
|||
|
|
};
|
|||
|
|
overlay.addEventListener('animationend', finishPulse, { once: true });
|
|||
|
|
overlay.addEventListener('animationcancel', finishPulse, { once: true });
|
|||
|
|
var firstNode = svg.querySelector('[data-node-id]');
|
|||
|
|
if (firstNode) svg.insertBefore(overlay, firstNode);
|
|||
|
|
else svg.appendChild(overlay);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function clearRelationshipPreview(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (directPreviewTimer) window.clearTimeout(directPreviewTimer);
|
|||
|
|
directPreviewTimer = null;
|
|||
|
|
removeRelationshipPulse();
|
|||
|
|
activeRelationshipPreview = null;
|
|||
|
|
svg.removeAttribute('data-relationship-preview-active');
|
|||
|
|
svg.removeAttribute('data-relationship-direct-active');
|
|||
|
|
if (options.clearPin === true) {
|
|||
|
|
pinnedRelationship = null;
|
|||
|
|
pinnedRelationshipKey = null;
|
|||
|
|
svg.removeAttribute('data-relationship-pin-active');
|
|||
|
|
}
|
|||
|
|
chip.removeAttribute('data-relationship-previewing');
|
|||
|
|
edges().forEach(function (edge) { edge.removeAttribute('data-relationship-preview'); });
|
|||
|
|
nodes().forEach(function (node) {
|
|||
|
|
node.removeAttribute('data-relationship-preview-node');
|
|||
|
|
node.removeAttribute('data-relationship-preview-source');
|
|||
|
|
node.removeAttribute('data-relationship-preview-target');
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(relationshipList.querySelectorAll('[data-preview-active]'), function (button) {
|
|||
|
|
button.removeAttribute('data-preview-active');
|
|||
|
|
});
|
|||
|
|
relationshipHitTargets.forEach(function (target) {
|
|||
|
|
target.setAttribute('aria-pressed', pinnedRelationshipKey && target.getAttribute('data-relationship-key') === pinnedRelationshipKey ? 'true' : 'false');
|
|||
|
|
target.removeAttribute('data-preview-active');
|
|||
|
|
});
|
|||
|
|
renderRelationshipCopyAction();
|
|||
|
|
requestLensPlacement();
|
|||
|
|
}
|
|||
|
|
function previewRelationship(button, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (pinnedRelationshipKey && pinnedRelationship && button !== pinnedRelationship) return;
|
|||
|
|
clearRelationshipPreview();
|
|||
|
|
if (!button) return;
|
|||
|
|
var key = button.getAttribute('data-relationship-key');
|
|||
|
|
var from = button.getAttribute('data-relationship-from');
|
|||
|
|
var to = button.getAttribute('data-relationship-to');
|
|||
|
|
if (!key || !from || !to) return;
|
|||
|
|
svg.setAttribute('data-relationship-preview-active', key);
|
|||
|
|
if (options.direct === true) svg.setAttribute('data-relationship-direct-active', key);
|
|||
|
|
edges().forEach(function (edge) {
|
|||
|
|
if (edge.getAttribute('data-edge-key') === key) edge.setAttribute('data-relationship-preview', '');
|
|||
|
|
});
|
|||
|
|
nodes().forEach(function (node) {
|
|||
|
|
var id = node.getAttribute('data-node-id');
|
|||
|
|
if (id !== from && id !== to) return;
|
|||
|
|
node.setAttribute('data-relationship-preview-node', '');
|
|||
|
|
if (id === from) node.setAttribute('data-relationship-preview-source', '');
|
|||
|
|
if (id === to) node.setAttribute('data-relationship-preview-target', '');
|
|||
|
|
});
|
|||
|
|
button.setAttribute('data-preview-active', 'true');
|
|||
|
|
if (!chip.hidden && options.direct !== true) chip.setAttribute('data-relationship-previewing', 'true');
|
|||
|
|
activeRelationshipPreview = button;
|
|||
|
|
renderRelationshipPulse(key);
|
|||
|
|
requestLensPlacement();
|
|||
|
|
}
|
|||
|
|
function syncRelationshipPreview() {
|
|||
|
|
var next = pinnedRelationship || focusedRelationship || hoveredRelationship;
|
|||
|
|
if (next === activeRelationshipPreview) return;
|
|||
|
|
previewRelationship(next, { direct: !!(next && next.hasAttribute('data-relationship-hit-key')) });
|
|||
|
|
}
|
|||
|
|
function directRelationshipBlocked() {
|
|||
|
|
return html.getAttribute('data-embed') === 'true' ||
|
|||
|
|
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');
|
|||
|
|
}
|
|||
|
|
function scheduleDirectRelationshipPreview(target) {
|
|||
|
|
if (directPreviewTimer) window.clearTimeout(directPreviewTimer);
|
|||
|
|
if (pinnedRelationshipKey) return;
|
|||
|
|
directPreviewTimer = window.setTimeout(function () {
|
|||
|
|
directPreviewTimer = null;
|
|||
|
|
if (pinnedRelationshipKey || hoveredRelationship !== target || directRelationshipBlocked()) return;
|
|||
|
|
previewRelationship(target, { direct: true });
|
|||
|
|
}, reducedMotionQuery && reducedMotionQuery.matches ? 0 : 90);
|
|||
|
|
}
|
|||
|
|
function relationshipHitTarget(key) {
|
|||
|
|
return relationshipHitTargets.find(function (target) {
|
|||
|
|
return target.getAttribute('data-relationship-key') === key;
|
|||
|
|
}) || null;
|
|||
|
|
}
|
|||
|
|
function revealPinnedRelationship(record) {
|
|||
|
|
function reveal() {
|
|||
|
|
if (!record || pinnedRelationshipKey !== record.key) return true;
|
|||
|
|
if (!Archify.view || typeof Archify.view.reveal !== 'function') return false;
|
|||
|
|
Archify.view.reveal([record.from, record.to], { reason: 'relationship-direct' });
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
if (!reveal()) requestAnimationFrame(reveal);
|
|||
|
|
}
|
|||
|
|
function inspectRelationship(key, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (html.getAttribute('data-embed') === 'true') return false;
|
|||
|
|
if (directPreviewTimer) window.clearTimeout(directPreviewTimer);
|
|||
|
|
directPreviewTimer = null;
|
|||
|
|
hoveredRelationship = null;
|
|||
|
|
focusedRelationship = null;
|
|||
|
|
if (pinnedRelationshipKey === key) {
|
|||
|
|
if (options.toggle === false) return true;
|
|||
|
|
clear({ updateUrl: options.updateUrl !== false });
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
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;
|
|||
|
|
});
|
|||
|
|
if (!row) return false;
|
|||
|
|
previewRelationship(row);
|
|||
|
|
pinnedRelationship = row;
|
|||
|
|
pinnedRelationshipKey = key;
|
|||
|
|
svg.setAttribute('data-relationship-pin-active', key);
|
|||
|
|
var target = relationshipHitTarget(key);
|
|||
|
|
if (target) target.setAttribute('aria-pressed', 'true');
|
|||
|
|
renderRelationshipCopyAction();
|
|||
|
|
summary.textContent = viewerText('viewer.passport.relationship.pinned', {
|
|||
|
|
from: record.fromLabel,
|
|||
|
|
to: record.toLabel,
|
|||
|
|
label: record.label
|
|||
|
|
});
|
|||
|
|
revealPinnedRelationship(record);
|
|||
|
|
if (options.updateUrl !== false && record.id) {
|
|||
|
|
try { history.replaceState(null, '', location.pathname + location.search + '#relation=' + encodeURIComponent(record.id)); } catch (_) {}
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function inspectRelationshipById(id, options) {
|
|||
|
|
var record = relationshipHitRecords().find(function (item) { return item.id === id; });
|
|||
|
|
return record ? inspectRelationship(record.key, options) : false;
|
|||
|
|
}
|
|||
|
|
function installRelationshipHitTargets() {
|
|||
|
|
if (html.getAttribute('data-embed') === 'true') return 0;
|
|||
|
|
var records = relationshipHitRecords();
|
|||
|
|
if (!records.length) return 0;
|
|||
|
|
relationshipHitOverlay = document.createElementNS(svgNamespace, 'g');
|
|||
|
|
relationshipHitOverlay.setAttribute('class', 'relationship-hit-overlay');
|
|||
|
|
relationshipHitOverlay.setAttribute('data-relationship-hit-overlay', '');
|
|||
|
|
relationshipHitOverlay.setAttribute('role', 'group');
|
|||
|
|
relationshipHitOverlay.setAttribute('aria-label', viewerText('viewer.passport.relationship.explorer'));
|
|||
|
|
var relationshipHelp = document.createElementNS(svgNamespace, 'desc');
|
|||
|
|
relationshipHelp.id = 'archify-relationship-help';
|
|||
|
|
relationshipHelp.textContent = viewerText('viewer.passport.relationship.help');
|
|||
|
|
relationshipHitOverlay.appendChild(relationshipHelp);
|
|||
|
|
records.forEach(function (record, index) {
|
|||
|
|
var target = document.createElementNS(svgNamespace, 'g');
|
|||
|
|
target.setAttribute('class', 'relationship-hit-target');
|
|||
|
|
target.setAttribute('data-relationship-hit-key', record.key);
|
|||
|
|
target.setAttribute('data-relationship-key', record.key);
|
|||
|
|
target.setAttribute('data-relationship-from', record.from);
|
|||
|
|
target.setAttribute('data-relationship-to', record.to);
|
|||
|
|
if (record.id) target.setAttribute('data-relationship-id', record.id);
|
|||
|
|
target.setAttribute('role', 'button');
|
|||
|
|
target.setAttribute('tabindex', index === 0 ? '0' : '-1');
|
|||
|
|
target.setAttribute('aria-pressed', 'false');
|
|||
|
|
target.setAttribute('aria-describedby', relationshipHelp.id);
|
|||
|
|
var description = viewerText('viewer.passport.relationship.inspect', {
|
|||
|
|
index: index + 1,
|
|||
|
|
total: records.length,
|
|||
|
|
from: record.fromLabel,
|
|||
|
|
to: record.toLabel,
|
|||
|
|
label: record.label
|
|||
|
|
});
|
|||
|
|
target.setAttribute('aria-label', description);
|
|||
|
|
var title = document.createElementNS(svgNamespace, 'title');
|
|||
|
|
title.textContent = record.fromLabel + ' \u2192 ' + record.toLabel + ' \u00b7 ' + record.label;
|
|||
|
|
target.appendChild(title);
|
|||
|
|
record.shapes.forEach(function (shape) {
|
|||
|
|
target.appendChild(relationshipHitGeometry(shape));
|
|||
|
|
target.appendChild(relationshipHitGeometry(shape, 'relationship-focus-rail'));
|
|||
|
|
});
|
|||
|
|
if (target.childNodes.length > 1) {
|
|||
|
|
relationshipHitTargets.push(target);
|
|||
|
|
relationshipHitOverlay.appendChild(target);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
if (!relationshipHitTargets.length) return 0;
|
|||
|
|
var firstNode = svg.querySelector('[data-node-id]');
|
|||
|
|
var nodeLayer = firstNode;
|
|||
|
|
while (nodeLayer && nodeLayer.parentNode && nodeLayer.parentNode !== svg) nodeLayer = nodeLayer.parentNode;
|
|||
|
|
if (nodeLayer && nodeLayer.parentNode === svg) svg.insertBefore(relationshipHitOverlay, nodeLayer);
|
|||
|
|
else svg.appendChild(relationshipHitOverlay);
|
|||
|
|
|
|||
|
|
relationshipHitOverlay.addEventListener('pointerdown', function (event) {
|
|||
|
|
if (event.target.closest('[data-relationship-hit-key]')) event.stopPropagation();
|
|||
|
|
});
|
|||
|
|
relationshipHitOverlay.addEventListener('pointerover', function (event) {
|
|||
|
|
if (event.pointerType === 'touch') return;
|
|||
|
|
if (finePointerQuery && !finePointerQuery.matches) return;
|
|||
|
|
var target = event.target.closest('[data-relationship-hit-key]');
|
|||
|
|
if (!target || directRelationshipBlocked() || pinnedRelationshipKey) return;
|
|||
|
|
if (event.relatedTarget && target.contains(event.relatedTarget)) return;
|
|||
|
|
hoveredRelationship = target;
|
|||
|
|
scheduleDirectRelationshipPreview(target);
|
|||
|
|
});
|
|||
|
|
relationshipHitOverlay.addEventListener('pointerout', function (event) {
|
|||
|
|
var target = event.target.closest('[data-relationship-hit-key]');
|
|||
|
|
if (!target || (event.relatedTarget && target.contains(event.relatedTarget))) return;
|
|||
|
|
if (hoveredRelationship === target) hoveredRelationship = null;
|
|||
|
|
syncRelationshipPreview();
|
|||
|
|
});
|
|||
|
|
relationshipHitOverlay.addEventListener('focusin', function (event) {
|
|||
|
|
var target = event.target.closest('[data-relationship-hit-key]');
|
|||
|
|
if (!target || directRelationshipBlocked()) return;
|
|||
|
|
focusedRelationship = target;
|
|||
|
|
if (!pinnedRelationshipKey) previewRelationship(target, { direct: true });
|
|||
|
|
});
|
|||
|
|
relationshipHitOverlay.addEventListener('focusout', function (event) {
|
|||
|
|
var target = event.target.closest('[data-relationship-hit-key]');
|
|||
|
|
if (!target || (event.relatedTarget && target.contains(event.relatedTarget))) return;
|
|||
|
|
if (focusedRelationship === target) focusedRelationship = null;
|
|||
|
|
syncRelationshipPreview();
|
|||
|
|
});
|
|||
|
|
relationshipHitOverlay.addEventListener('click', function (event) {
|
|||
|
|
var target = event.target.closest('[data-relationship-hit-key]');
|
|||
|
|
if (!target || directRelationshipBlocked()) return;
|
|||
|
|
event.preventDefault();
|
|||
|
|
event.stopPropagation();
|
|||
|
|
focusedRelationship = null;
|
|||
|
|
hoveredRelationship = null;
|
|||
|
|
inspectRelationship(target.getAttribute('data-relationship-key'));
|
|||
|
|
});
|
|||
|
|
relationshipHitOverlay.addEventListener('keydown', function (event) {
|
|||
|
|
var target = event.target.closest('[data-relationship-hit-key]');
|
|||
|
|
if (!target) return;
|
|||
|
|
if (event.key === 'Escape' && pinnedRelationshipKey) {
|
|||
|
|
event.preventDefault();
|
|||
|
|
clear({ updateUrl: false });
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|||
|
|
event.preventDefault();
|
|||
|
|
focusedRelationship = null;
|
|||
|
|
hoveredRelationship = null;
|
|||
|
|
inspectRelationship(target.getAttribute('data-relationship-key'));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (event.key !== 'ArrowRight' && event.key !== 'ArrowLeft' && event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'Home' && event.key !== 'End') return;
|
|||
|
|
var index = relationshipHitTargets.indexOf(target);
|
|||
|
|
if (event.key === 'Home') index = 0;
|
|||
|
|
else if (event.key === 'End') index = relationshipHitTargets.length - 1;
|
|||
|
|
else if (event.key === 'ArrowRight' || event.key === 'ArrowDown') index = (index + 1) % relationshipHitTargets.length;
|
|||
|
|
else index = (index - 1 + relationshipHitTargets.length) % relationshipHitTargets.length;
|
|||
|
|
event.preventDefault();
|
|||
|
|
relationshipHitTargets.forEach(function (item, itemIndex) { item.setAttribute('tabindex', itemIndex === index ? '0' : '-1'); });
|
|||
|
|
try { relationshipHitTargets[index].focus({ preventScroll: true }); }
|
|||
|
|
catch (_) { try { relationshipHitTargets[index].focus(); } catch (_) {} }
|
|||
|
|
});
|
|||
|
|
return relationshipHitTargets.length;
|
|||
|
|
}
|
|||
|
|
function renderRelationshipLens(id, byId) {
|
|||
|
|
hoveredRelationship = null;
|
|||
|
|
focusedRelationship = null;
|
|||
|
|
clearRelationshipPreview({ clearPin: true });
|
|||
|
|
renderPassport(id, byId[id]);
|
|||
|
|
var relationships = relationshipsFor(id, byId);
|
|||
|
|
chip.removeAttribute('data-relations-expanded');
|
|||
|
|
relationsBtn.setAttribute('aria-expanded', 'false');
|
|||
|
|
relationsBtn.textContent = viewerCount('viewer.passport.relationship.count', relationships.length);
|
|||
|
|
relationsBtn.setAttribute('aria-label', viewerCount('viewer.passport.relationship.show', relationships.length));
|
|||
|
|
var counts = { out: 0, in: 0, loop: 0 };
|
|||
|
|
relationships.forEach(function (relationship) { counts[relationship.direction] += 1; });
|
|||
|
|
summary.textContent = viewerText('viewer.passport.relationship.summary', {
|
|||
|
|
out: counts.out,
|
|||
|
|
in: counts.in,
|
|||
|
|
loops: counts.loop ? viewerText('viewer.passport.relationship.loops', { count: counts.loop }) : ''
|
|||
|
|
});
|
|||
|
|
renderReachabilityControls(id);
|
|||
|
|
relationshipList.textContent = '';
|
|||
|
|
if (!relationships.length) {
|
|||
|
|
var empty = document.createElement('p');
|
|||
|
|
empty.className = 'relationship-lens-empty';
|
|||
|
|
empty.textContent = viewerText('viewer.passport.relationship.none');
|
|||
|
|
relationshipList.appendChild(empty);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[
|
|||
|
|
{ id: 'out', label: viewerText('viewer.passport.relationship.group.out') },
|
|||
|
|
{ id: 'in', label: viewerText('viewer.passport.relationship.group.in') },
|
|||
|
|
{ id: 'loop', label: viewerText('viewer.passport.relationship.group.loop') }
|
|||
|
|
].forEach(function (group) {
|
|||
|
|
var items = relationships.filter(function (relationship) { return relationship.direction === group.id; });
|
|||
|
|
if (!items.length) return;
|
|||
|
|
var section = document.createElement('div');
|
|||
|
|
section.className = 'relationship-lens-group';
|
|||
|
|
var heading = document.createElement('span');
|
|||
|
|
heading.className = 'relationship-lens-group-title';
|
|||
|
|
heading.textContent = group.label + ' · ' + items.length;
|
|||
|
|
section.appendChild(heading);
|
|||
|
|
items.forEach(function (relationship) {
|
|||
|
|
var button = document.createElement('button');
|
|||
|
|
button.type = 'button';
|
|||
|
|
button.className = 'relationship-lens-row';
|
|||
|
|
button.setAttribute('data-direction', relationship.direction);
|
|||
|
|
button.setAttribute('data-relationship-target', relationship.neighborId);
|
|||
|
|
button.setAttribute('data-relationship-key', relationship.key);
|
|||
|
|
button.setAttribute('data-relationship-from', relationship.from);
|
|||
|
|
button.setAttribute('data-relationship-to', relationship.to);
|
|||
|
|
if (relationship.id) button.setAttribute('data-relationship-id', relationship.id);
|
|||
|
|
button.setAttribute('aria-label', viewerText('viewer.passport.relationship.row', {
|
|||
|
|
group: group.label,
|
|||
|
|
relationship: relationship.label,
|
|||
|
|
neighbor: relationship.neighborLabel
|
|||
|
|
}));
|
|||
|
|
var direction = document.createElement('span');
|
|||
|
|
direction.className = 'relationship-lens-direction';
|
|||
|
|
direction.setAttribute('aria-hidden', 'true');
|
|||
|
|
direction.textContent = viewerText(relationship.direction === 'out'
|
|||
|
|
? 'viewer.passport.relationship.direction.out'
|
|||
|
|
: relationship.direction === 'in'
|
|||
|
|
? 'viewer.passport.relationship.direction.in'
|
|||
|
|
: 'viewer.passport.relationship.direction.loop');
|
|||
|
|
var target = document.createElement('strong');
|
|||
|
|
target.textContent = relationship.neighborLabel;
|
|||
|
|
var relation = document.createElement('small');
|
|||
|
|
relation.textContent = relationship.label;
|
|||
|
|
button.appendChild(direction);
|
|||
|
|
button.appendChild(target);
|
|||
|
|
button.appendChild(relation);
|
|||
|
|
section.appendChild(button);
|
|||
|
|
});
|
|||
|
|
relationshipList.appendChild(section);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
var lensFrame = 0;
|
|||
|
|
function placeRelationshipLens() {
|
|||
|
|
lensFrame = 0;
|
|||
|
|
if (chip.hidden || activeIds.length !== 1) return;
|
|||
|
|
var node = svg.querySelector('[data-node-id="' + activeIds[0] + '"]');
|
|||
|
|
if (!node) return;
|
|||
|
|
var containerRect = container.getBoundingClientRect();
|
|||
|
|
var nodeRect = node.getBoundingClientRect();
|
|||
|
|
if (containerRect.bottom <= 0 || containerRect.top >= window.innerHeight) return;
|
|||
|
|
var padding = window.innerWidth <= 720 ? 8 : 16;
|
|||
|
|
var visibleTop = Math.max(padding, -containerRect.top + padding);
|
|||
|
|
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;
|
|||
|
|
var previewingOnMobile = mobile && chip.getAttribute('data-relationship-previewing') === 'true';
|
|||
|
|
var compactOnMobile = mobile && chip.getAttribute('data-relations-expanded') !== 'true';
|
|||
|
|
var preferred;
|
|||
|
|
if (compactOnMobile) {
|
|||
|
|
var nodeTop = nodeRect.top - containerRect.top;
|
|||
|
|
var nodeBottom = nodeRect.bottom - containerRect.top;
|
|||
|
|
var gap = 10;
|
|||
|
|
var above = nodeTop - chip.offsetHeight - gap;
|
|||
|
|
var below = nodeBottom + gap;
|
|||
|
|
if (above >= visibleTop) preferred = above;
|
|||
|
|
else if (below + chip.offsetHeight <= visibleBottom - 56) preferred = below;
|
|||
|
|
else preferred = nodeCenter < (visibleTop + visibleBottom) / 2
|
|||
|
|
? Math.max(minTop, visibleBottom - chip.offsetHeight - 56)
|
|||
|
|
: visibleTop;
|
|||
|
|
} else if (previewingOnMobile) {
|
|||
|
|
var pinnedTop = visibleTop;
|
|||
|
|
var pinnedBottom = Math.max(minTop, visibleBottom - chip.offsetHeight - 56);
|
|||
|
|
preferred = nodeCenter < (visibleTop + visibleBottom) / 2 ? pinnedBottom : pinnedTop;
|
|||
|
|
} else {
|
|||
|
|
preferred = nodeCenter - chip.offsetHeight / 2;
|
|||
|
|
}
|
|||
|
|
var top = Math.max(minTop, Math.min(maxTop, preferred));
|
|||
|
|
var chipRect = chip.getBoundingClientRect();
|
|||
|
|
var safeGap = 10;
|
|||
|
|
var protectViewerChrome = !mobile || compactOnMobile || previewingOnMobile;
|
|||
|
|
var protectedRects = (protectViewerChrome
|
|||
|
|
? [svg.querySelector('[data-legend]'), container.querySelector('.diagram-nav')]
|
|||
|
|
: [])
|
|||
|
|
.filter(function (element) {
|
|||
|
|
if (!element || element.hidden) return false;
|
|||
|
|
var style = window.getComputedStyle(element);
|
|||
|
|
return style.display !== 'none' && style.visibility !== 'hidden';
|
|||
|
|
})
|
|||
|
|
.map(function (element) { return element.getBoundingClientRect(); })
|
|||
|
|
.filter(function (rect) {
|
|||
|
|
return rect.width > 0 && rect.height > 0 &&
|
|||
|
|
chipRect.left < rect.right + safeGap && chipRect.right > rect.left - safeGap;
|
|||
|
|
});
|
|||
|
|
if (protectedRects.length) {
|
|||
|
|
var candidates = [top, minTop, maxTop];
|
|||
|
|
protectedRects.forEach(function (rect) {
|
|||
|
|
candidates.push(
|
|||
|
|
rect.top - containerRect.top - chip.offsetHeight - safeGap,
|
|||
|
|
rect.bottom - containerRect.top + safeGap
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
var valid = candidates.map(function (candidate) {
|
|||
|
|
return Math.max(minTop, Math.min(maxTop, candidate));
|
|||
|
|
}).filter(function (candidate, index, all) {
|
|||
|
|
if (all.indexOf(candidate) !== index) return false;
|
|||
|
|
var candidateTop = containerRect.top + candidate;
|
|||
|
|
var candidateBottom = candidateTop + chip.offsetHeight;
|
|||
|
|
return protectedRects.every(function (rect) {
|
|||
|
|
return candidateBottom <= rect.top - safeGap || candidateTop >= rect.bottom + safeGap;
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
valid.sort(function (first, second) {
|
|||
|
|
return Math.abs(first - preferred) - Math.abs(second - preferred);
|
|||
|
|
});
|
|||
|
|
if (valid.length) top = valid[0];
|
|||
|
|
}
|
|||
|
|
chip.style.top = Math.round(top) + 'px';
|
|||
|
|
if (Archify.radar && typeof Archify.radar.sync === 'function') Archify.radar.sync();
|
|||
|
|
}
|
|||
|
|
function requestLensPlacement() {
|
|||
|
|
if (lensFrame) return;
|
|||
|
|
lensFrame = requestAnimationFrame(placeRelationshipLens);
|
|||
|
|
}
|
|||
|
|
function clear(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
var restoreNode = options.restoreFocus === true && activeIds.length === 1
|
|||
|
|
? svg.querySelector('[data-node-id="' + activeIds[0] + '"]')
|
|||
|
|
: null;
|
|||
|
|
clearReachability({ updateUrl: false });
|
|||
|
|
if (Archify.intentTrace && typeof Archify.intentTrace.clear === 'function') {
|
|||
|
|
Archify.intentTrace.clear({ announce: false });
|
|||
|
|
}
|
|||
|
|
hoveredRelationship = null;
|
|||
|
|
focusedRelationship = null;
|
|||
|
|
clearRelationshipPreview({ clearPin: true });
|
|||
|
|
activeIds = [];
|
|||
|
|
svg.removeAttribute('data-focus-active');
|
|||
|
|
nodes().forEach(function (node) {
|
|||
|
|
node.removeAttribute('data-focus-match');
|
|||
|
|
node.removeAttribute('data-focus-selected');
|
|||
|
|
node.setAttribute('aria-pressed', 'false');
|
|||
|
|
});
|
|||
|
|
edges().forEach(function (edge) { edge.removeAttribute('data-focus-match'); });
|
|||
|
|
chip.hidden = true;
|
|||
|
|
label.textContent = '';
|
|||
|
|
detail.textContent = '';
|
|||
|
|
detail.hidden = true;
|
|||
|
|
kind.textContent = '';
|
|||
|
|
kind.hidden = true;
|
|||
|
|
context.textContent = '';
|
|||
|
|
context.hidden = true;
|
|||
|
|
tag.textContent = '';
|
|||
|
|
tag.hidden = true;
|
|||
|
|
semanticId.textContent = '';
|
|||
|
|
semanticId.hidden = true;
|
|||
|
|
evidence.hidden = true;
|
|||
|
|
evidenceLinks.textContent = '';
|
|||
|
|
repositoryLink.removeAttribute('href');
|
|||
|
|
repositoryLink.textContent = '';
|
|||
|
|
summary.textContent = '';
|
|||
|
|
reachSection.hidden = true;
|
|||
|
|
upstreamCount.textContent = '0';
|
|||
|
|
downstreamCount.textContent = '0';
|
|||
|
|
upstreamBtn.disabled = true;
|
|||
|
|
downstreamBtn.disabled = true;
|
|||
|
|
relationshipList.textContent = '';
|
|||
|
|
copyBtn.textContent = viewerText('viewer.passport.copy');
|
|||
|
|
copyBtn.setAttribute('aria-label', viewerText('viewer.passport.copy.focus'));
|
|||
|
|
relationsBtn.textContent = viewerText('viewer.passport.relations');
|
|||
|
|
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.preserveView !== true && Archify.view && typeof Archify.view.reset === 'function') {
|
|||
|
|
Archify.view.reset({ automatic: true });
|
|||
|
|
}
|
|||
|
|
if (options.updateUrl !== false) {
|
|||
|
|
try { history.replaceState(null, '', location.pathname + location.search); } catch (_) {}
|
|||
|
|
}
|
|||
|
|
if (restoreNode) {
|
|||
|
|
try { restoreNode.focus({ preventScroll: true }); }
|
|||
|
|
catch (_) { try { restoreNode.focus(); } catch (_) {} }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setMany(ids, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (Archify.semanticLens && typeof Archify.semanticLens.clearPreview === 'function') Archify.semanticLens.clearPreview();
|
|||
|
|
if (Archify.semanticLens && Archify.semanticLens.active()) {
|
|||
|
|
Archify.semanticLens.clear({ updateUrl: false, preserveView: true, closePanel: true });
|
|||
|
|
}
|
|||
|
|
if (options.preserveRoute !== true && Archify.routeProbe && typeof Archify.routeProbe.clear === 'function') {
|
|||
|
|
Archify.routeProbe.clear({ updateUrl: false, restoreFocus: false });
|
|||
|
|
}
|
|||
|
|
var nodeList = nodes();
|
|||
|
|
var byId = {};
|
|||
|
|
nodeList.forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; });
|
|||
|
|
var normalized = [];
|
|||
|
|
(ids || []).forEach(function (id) {
|
|||
|
|
if (byId[id] && normalized.indexOf(id) === -1) normalized.push(id);
|
|||
|
|
});
|
|||
|
|
if (!normalized.length) return false;
|
|||
|
|
if (normalized.length === activeIds.length && normalized.every(function (id, index) { return activeIds[index] === id; }) && options.toggle !== false) {
|
|||
|
|
clear();
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
clear({ updateUrl: false, preserveView: true });
|
|||
|
|
activeIds = normalized;
|
|||
|
|
var selected = {};
|
|||
|
|
var related = {};
|
|||
|
|
var seenEdges = {};
|
|||
|
|
var matchedEdges = 0;
|
|||
|
|
normalized.forEach(function (id) { selected[id] = true; related[id] = true; });
|
|||
|
|
var selectionMode = options.mode === 'selection' || normalized.length > 1;
|
|||
|
|
|
|||
|
|
edges().forEach(function (edge) {
|
|||
|
|
var from = edge.getAttribute('data-edge-from');
|
|||
|
|
var to = edge.getAttribute('data-edge-to');
|
|||
|
|
var match = selectionMode ? selected[from] && selected[to] : selected[from] || selected[to];
|
|||
|
|
if (!match) return;
|
|||
|
|
edge.setAttribute('data-focus-match', '');
|
|||
|
|
if (!selectionMode) { related[from] = true; related[to] = true; }
|
|||
|
|
var edgeKey = edge.getAttribute('data-edge-key') || (from + '\u0000' + to + '\u0000' + (edge.getAttribute('data-edge-label') || ''));
|
|||
|
|
if (!seenEdges[edgeKey]) { seenEdges[edgeKey] = true; matchedEdges += 1; }
|
|||
|
|
});
|
|||
|
|
nodeList.forEach(function (node) {
|
|||
|
|
var nodeId = node.getAttribute('data-node-id');
|
|||
|
|
if (related[nodeId]) node.setAttribute('data-focus-match', '');
|
|||
|
|
if (selected[nodeId]) {
|
|||
|
|
node.setAttribute('data-focus-selected', '');
|
|||
|
|
node.setAttribute('aria-pressed', 'true');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
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 });
|
|||
|
|
label.textContent = options.label || defaultLabel;
|
|||
|
|
chip.hidden = options.hideChip === true || normalized.length !== 1 || selectionMode;
|
|||
|
|
if (!chip.hidden) {
|
|||
|
|
renderRelationshipLens(normalized[0], byId);
|
|||
|
|
requestLensPlacement();
|
|||
|
|
}
|
|||
|
|
if (options.updateUrl !== false) {
|
|||
|
|
var key = options.urlKey || 'focus';
|
|||
|
|
var value = options.urlValue || normalized[0];
|
|||
|
|
try { history.replaceState(null, '', location.pathname + location.search + '#' + key + '=' + encodeURIComponent(value)); } catch (_) {}
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function set(id, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
options.mode = 'neighborhood';
|
|||
|
|
return setMany([id], options);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function fallbackCopy(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 copyFocusLink() {
|
|||
|
|
if (activeIds.length !== 1) return Promise.resolve(false);
|
|||
|
|
var record = pinnedRelationshipRecord();
|
|||
|
|
var relationId = record && record.id;
|
|||
|
|
var value = location.href.replace(/#.*$/, '') + (relationId
|
|||
|
|
? '#relation=' + encodeURIComponent(relationId)
|
|||
|
|
: '#focus=' + encodeURIComponent(activeIds[0]) + (reachabilityMode ? '&reach=' + reachabilityMode : ''));
|
|||
|
|
var copy = navigator.clipboard && typeof navigator.clipboard.writeText === 'function'
|
|||
|
|
? navigator.clipboard.writeText(value).then(function () { return true; }).catch(function () { return fallbackCopy(value); })
|
|||
|
|
: Promise.resolve(fallbackCopy(value));
|
|||
|
|
return copy.then(function (copied) {
|
|||
|
|
copyBtn.textContent = viewerText(copied ? 'viewer.common.copied' : 'viewer.common.copyFailed');
|
|||
|
|
copyBtn.setAttribute('aria-label', copied
|
|||
|
|
? viewerText(relationId ? 'viewer.passport.copy.pinned.success' : 'viewer.passport.copy.focused.success')
|
|||
|
|
: viewerText(relationId ? 'viewer.passport.copy.pinned.failed' : 'viewer.passport.copy.focused.failed'));
|
|||
|
|
window.setTimeout(function () {
|
|||
|
|
renderRelationshipCopyAction();
|
|||
|
|
}, 1600);
|
|||
|
|
return copied;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
svg.addEventListener('click', function (event) {
|
|||
|
|
if (container.getAttribute('data-just-panned') === 'true') return;
|
|||
|
|
var node = event.target.closest('[data-node-id]');
|
|||
|
|
if (node) {
|
|||
|
|
var id = node.getAttribute('data-node-id');
|
|||
|
|
set(id);
|
|||
|
|
if (activeIds.indexOf(id) !== -1 && Archify.view && typeof Archify.view.reveal === 'function') {
|
|||
|
|
Archify.view.reveal([id], { includeNeighbors: true, reason: 'focus' });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
else if (activeIds.length) clear();
|
|||
|
|
});
|
|||
|
|
svg.addEventListener('keydown', function (event) {
|
|||
|
|
var node = event.target.closest('[data-node-id]');
|
|||
|
|
if (!node || (event.key !== 'Enter' && event.key !== ' ')) return;
|
|||
|
|
event.preventDefault();
|
|||
|
|
var id = node.getAttribute('data-node-id');
|
|||
|
|
set(id);
|
|||
|
|
if (activeIds.indexOf(id) !== -1 && Archify.view && typeof Archify.view.reveal === 'function') {
|
|||
|
|
Archify.view.reveal([id], { includeNeighbors: true, reason: 'focus' });
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
clearBtn.addEventListener('click', function () { clear({ restoreFocus: true }); });
|
|||
|
|
copyBtn.addEventListener('click', copyFocusLink);
|
|||
|
|
upstreamBtn.addEventListener('click', function () { applyReachability('upstream'); });
|
|||
|
|
downstreamBtn.addEventListener('click', function () { applyReachability('downstream'); });
|
|||
|
|
relationsBtn.addEventListener('click', function () {
|
|||
|
|
var expanded = chip.getAttribute('data-relations-expanded') === 'true';
|
|||
|
|
if (expanded) chip.removeAttribute('data-relations-expanded');
|
|||
|
|
else chip.setAttribute('data-relations-expanded', 'true');
|
|||
|
|
relationsBtn.setAttribute('aria-expanded', expanded ? 'false' : 'true');
|
|||
|
|
relationsBtn.setAttribute('aria-label', viewerText(expanded
|
|||
|
|
? 'viewer.passport.relations.show'
|
|||
|
|
: 'viewer.passport.relations.hide'));
|
|||
|
|
requestLensPlacement();
|
|||
|
|
});
|
|||
|
|
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' });
|
|||
|
|
}
|
|||
|
|
var node = svg.querySelector('[data-node-id="' + id + '"]');
|
|||
|
|
if (node) {
|
|||
|
|
try { node.focus({ preventScroll: true }); } catch (_) { try { node.focus(); } catch (_) {} }
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
relationshipList.addEventListener('pointerover', function (event) {
|
|||
|
|
if (event.pointerType === 'touch') return;
|
|||
|
|
if (finePointerQuery && !finePointerQuery.matches) return;
|
|||
|
|
var button = event.target.closest('[data-relationship-key]');
|
|||
|
|
if (!button || !relationshipList.contains(button)) return;
|
|||
|
|
if (event.relatedTarget && button.contains(event.relatedTarget)) return;
|
|||
|
|
hoveredRelationship = button;
|
|||
|
|
syncRelationshipPreview();
|
|||
|
|
});
|
|||
|
|
relationshipList.addEventListener('pointerout', function (event) {
|
|||
|
|
var button = event.target.closest('[data-relationship-key]');
|
|||
|
|
if (!button || (event.relatedTarget && button.contains(event.relatedTarget))) return;
|
|||
|
|
if (hoveredRelationship === button) hoveredRelationship = null;
|
|||
|
|
syncRelationshipPreview();
|
|||
|
|
});
|
|||
|
|
relationshipList.addEventListener('focusin', function (event) {
|
|||
|
|
var button = event.target.closest('[data-relationship-key]');
|
|||
|
|
if (!button) return;
|
|||
|
|
focusedRelationship = button;
|
|||
|
|
syncRelationshipPreview();
|
|||
|
|
});
|
|||
|
|
relationshipList.addEventListener('focusout', function (event) {
|
|||
|
|
var button = event.target.closest('[data-relationship-key]');
|
|||
|
|
if (!button || (event.relatedTarget && button.contains(event.relatedTarget))) return;
|
|||
|
|
if (focusedRelationship === button) focusedRelationship = null;
|
|||
|
|
syncRelationshipPreview();
|
|||
|
|
});
|
|||
|
|
relationshipList.addEventListener('keydown', function (event) {
|
|||
|
|
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'Home' && event.key !== 'End') return;
|
|||
|
|
var buttons = Array.prototype.slice.call(relationshipList.querySelectorAll('[data-relationship-target]'));
|
|||
|
|
if (!buttons.length) return;
|
|||
|
|
var index = buttons.indexOf(document.activeElement);
|
|||
|
|
if (event.key === 'Home') index = 0;
|
|||
|
|
else if (event.key === 'End') index = buttons.length - 1;
|
|||
|
|
else if (event.key === 'ArrowDown') index = Math.min(buttons.length - 1, Math.max(0, index + 1));
|
|||
|
|
else index = Math.max(0, index < 0 ? 0 : index - 1);
|
|||
|
|
event.preventDefault();
|
|||
|
|
buttons[index].focus();
|
|||
|
|
});
|
|||
|
|
document.addEventListener('click', function (event) {
|
|||
|
|
var target = event.target;
|
|||
|
|
if (chip.hidden || !target || typeof target.closest !== 'function' || chip.contains(target)) return;
|
|||
|
|
if (container.getAttribute('data-just-panned') === 'true') return;
|
|||
|
|
if (target.closest('[data-node-id], [data-relationship-hit-key], .overview-map')) return;
|
|||
|
|
clear();
|
|||
|
|
}, true);
|
|||
|
|
window.addEventListener('scroll', requestLensPlacement, { passive: true });
|
|||
|
|
window.addEventListener('resize', requestLensPlacement);
|
|||
|
|
container.addEventListener('scroll', requestLensPlacement, { passive: true });
|
|||
|
|
document.addEventListener('visibilitychange', function () {
|
|||
|
|
if (document.hidden) removeRelationshipPulse();
|
|||
|
|
});
|
|||
|
|
function syncRelationshipMotionPreference(event) {
|
|||
|
|
if (event.matches) removeRelationshipPulse();
|
|||
|
|
}
|
|||
|
|
if (reducedMotionQuery) {
|
|||
|
|
if (typeof reducedMotionQuery.addEventListener === 'function') {
|
|||
|
|
reducedMotionQuery.addEventListener('change', syncRelationshipMotionPreference);
|
|||
|
|
} else if (typeof reducedMotionQuery.addListener === 'function') {
|
|||
|
|
reducedMotionQuery.addListener(syncRelationshipMotionPreference);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
installRelationshipHitTargets();
|
|||
|
|
|
|||
|
|
function syncFocusFromHash() {
|
|||
|
|
try {
|
|||
|
|
var params = new URLSearchParams(location.hash.replace(/^#/, ''));
|
|||
|
|
var relation = params.get('relation');
|
|||
|
|
var initial = params.get('focus');
|
|||
|
|
var reach = params.get('reach');
|
|||
|
|
if (relation) {
|
|||
|
|
if (html.getAttribute('data-embed') === 'true' ||
|
|||
|
|
!inspectRelationshipById(relation, { updateUrl: false, toggle: false })) clear({ updateUrl: false });
|
|||
|
|
}
|
|||
|
|
else if (initial) {
|
|||
|
|
if (set(initial, { updateUrl: false, toggle: false }) &&
|
|||
|
|
(reach === 'upstream' || reach === 'downstream')) {
|
|||
|
|
applyReachability(reach, { updateUrl: false, toggle: false, reveal: false });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
else if (!params.get('view')) clear({ updateUrl: false });
|
|||
|
|
} catch (_) {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
window.addEventListener('hashchange', syncFocusFromHash);
|
|||
|
|
syncFocusFromHash();
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
set: set,
|
|||
|
|
setMany: setMany,
|
|||
|
|
clear: clear,
|
|||
|
|
copyLink: copyFocusLink,
|
|||
|
|
reach: applyReachability,
|
|||
|
|
clearReach: clearReachability,
|
|||
|
|
reachabilitySnapshot: reachabilitySnapshot,
|
|||
|
|
inspectRelationship: inspectRelationship,
|
|||
|
|
inspectRelationshipById: inspectRelationshipById,
|
|||
|
|
reposition: requestLensPlacement,
|
|||
|
|
relationship: function () {
|
|||
|
|
var record = pinnedRelationshipRecord();
|
|||
|
|
return record ? { id: record.id || null, key: record.key, from: record.from, to: record.to, label: record.label } : null;
|
|||
|
|
},
|
|||
|
|
reachability: function () {
|
|||
|
|
return activeReachability ? {
|
|||
|
|
direction: activeReachability.direction,
|
|||
|
|
originId: activeReachability.originId,
|
|||
|
|
nodeIds: activeReachability.nodeIds.slice(),
|
|||
|
|
edgeKeys: activeReachability.edgeKeys.slice(),
|
|||
|
|
maxDepth: activeReachability.maxDepth
|
|||
|
|
} : null;
|
|||
|
|
},
|
|||
|
|
active: function () { return activeIds.length === 0 ? null : (activeIds.length === 1 ? activeIds[0] : activeIds.slice()); }
|
|||
|
|
};
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* ============================================================
|
|||
|
|
Intent Trace — temporary one-hop topology before durable focus.
|
|||
|
|
Fine-pointer hover and keyboard focus share the same stable-ID preview;
|
|||
|
|
touch continues directly to the existing click-to-focus contract.
|
|||
|
|
============================================================ */
|
|||
|
|
Archify.intentTrace = (function () {
|
|||
|
|
var html = document.documentElement;
|
|||
|
|
var container = document.querySelector('.diagram-container');
|
|||
|
|
var svg = container.querySelector(':scope > svg');
|
|||
|
|
var status = document.getElementById('intent-trace-status');
|
|||
|
|
var namespace = 'http://www.w3.org/2000/svg';
|
|||
|
|
var activeId = null;
|
|||
|
|
var hoveredNode = null;
|
|||
|
|
var focusedNode = null;
|
|||
|
|
var enterTimer = null;
|
|||
|
|
|
|||
|
|
function nodes() {
|
|||
|
|
return Array.prototype.slice.call(svg.querySelectorAll('[data-node-id]'));
|
|||
|
|
}
|
|||
|
|
function edges() {
|
|||
|
|
return Array.prototype.slice.call(svg.querySelectorAll('[data-edge-from][data-edge-to]'));
|
|||
|
|
}
|
|||
|
|
function nodeLabel(node, fallback) {
|
|||
|
|
return node.getAttribute('data-node-label') ||
|
|||
|
|
(node.getAttribute('aria-label') || fallback).replace(/^Focus\s+/, '').split(',')[0];
|
|||
|
|
}
|
|||
|
|
function finePointer() {
|
|||
|
|
return !window.matchMedia || window.matchMedia('(hover: hover) and (pointer: fine)').matches;
|
|||
|
|
}
|
|||
|
|
function reducedMotion() {
|
|||
|
|
return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
|
|||
|
|
}
|
|||
|
|
function blocked() {
|
|||
|
|
return html.getAttribute('data-embed') === 'true' ||
|
|||
|
|
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());
|
|||
|
|
}
|
|||
|
|
function edgeShapes(edge) {
|
|||
|
|
if (/^(path|line|polyline)$/i.test(edge.tagName)) return [edge];
|
|||
|
|
return Array.prototype.slice.call(edge.querySelectorAll('path, line, polyline'));
|
|||
|
|
}
|
|||
|
|
function removeOverlay() {
|
|||
|
|
Array.prototype.forEach.call(svg.querySelectorAll('[data-intent-trace-overlay]'), function (overlay) {
|
|||
|
|
overlay.remove();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function clear(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (enterTimer) window.clearTimeout(enterTimer);
|
|||
|
|
enterTimer = null;
|
|||
|
|
activeId = null;
|
|||
|
|
svg.removeAttribute('data-intent-trace-active');
|
|||
|
|
removeOverlay();
|
|||
|
|
nodes().forEach(function (node) {
|
|||
|
|
node.removeAttribute('data-intent-trace-match');
|
|||
|
|
node.removeAttribute('data-intent-trace-selected');
|
|||
|
|
});
|
|||
|
|
edges().forEach(function (edge) { edge.removeAttribute('data-intent-trace-match'); });
|
|||
|
|
if (options.announce !== false) status.textContent = '';
|
|||
|
|
}
|
|||
|
|
function traceGeometry(shape, direction) {
|
|||
|
|
var clone = shape.cloneNode(false);
|
|||
|
|
clone.removeAttribute('id');
|
|||
|
|
clone.removeAttribute('class');
|
|||
|
|
clone.removeAttribute('style');
|
|||
|
|
clone.removeAttribute('marker-start');
|
|||
|
|
clone.removeAttribute('marker-mid');
|
|||
|
|
clone.removeAttribute('marker-end');
|
|||
|
|
clone.removeAttribute('role');
|
|||
|
|
clone.removeAttribute('aria-label');
|
|||
|
|
clone.removeAttribute('aria-labelledby');
|
|||
|
|
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.removeAttribute('data-intent-trace-match');
|
|||
|
|
clone.removeAttribute('data-intent-trace-selected');
|
|||
|
|
clone.setAttribute('class', 'intent-trace-flow');
|
|||
|
|
clone.setAttribute('data-direction', direction);
|
|||
|
|
clone.setAttribute('pathLength', '1');
|
|||
|
|
return clone;
|
|||
|
|
}
|
|||
|
|
function show(id, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (!id || blocked()) {
|
|||
|
|
clear({ announce: false });
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
if (activeId === id) return true;
|
|||
|
|
clear({ announce: false });
|
|||
|
|
var nodeList = nodes();
|
|||
|
|
var edgeList = edges();
|
|||
|
|
var byId = {};
|
|||
|
|
nodeList.forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; });
|
|||
|
|
var selected = byId[id];
|
|||
|
|
if (!selected) return false;
|
|||
|
|
|
|||
|
|
var overlay = document.createElementNS(namespace, 'g');
|
|||
|
|
overlay.setAttribute('class', 'intent-trace-overlay');
|
|||
|
|
overlay.setAttribute('data-intent-trace-overlay', '');
|
|||
|
|
overlay.setAttribute('aria-hidden', 'true');
|
|||
|
|
var related = {};
|
|||
|
|
var seen = {};
|
|||
|
|
var counts = { out: 0, in: 0, loop: 0 };
|
|||
|
|
related[id] = true;
|
|||
|
|
|
|||
|
|
edgeList.forEach(function (edge) {
|
|||
|
|
var from = edge.getAttribute('data-edge-from');
|
|||
|
|
var to = edge.getAttribute('data-edge-to');
|
|||
|
|
if (from !== id && to !== id) return;
|
|||
|
|
var direction = from === id && to === id ? 'loop' : (from === id ? 'out' : 'in');
|
|||
|
|
var key = edge.getAttribute('data-edge-key') ||
|
|||
|
|
(from + '\u0000' + to + '\u0000' + (edge.getAttribute('data-edge-label') || ''));
|
|||
|
|
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 target = Math.max(0, stop.offsetLeft - (trail.clientWidth - stop.offsetWidth) / 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);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
if (copied) {
|
|||
|
|
var firstEdge = edgeList[0];
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
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();
|
|||
|
|
});
|
|||
|
|
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 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 });
|
|||
|
|
}
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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));
|
|||
|
|
});
|
|||
|
|
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));
|
|||
|
|
});
|
|||
|
|
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);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 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();
|
|||
|
|
});
|
|||
|
|
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'] });
|
|||
|
|
}
|
|||
|
|
window.addEventListener('beforeprint', function () {
|
|||
|
|
clearChapterPreview({ clearIntents: true });
|
|||
|
|
if (playing) pausePlayback();
|
|||
|
|
else clearStoryPulse();
|
|||
|
|
settleHandoff('print');
|
|||
|
|
});
|
|||
|
|
syncViewFromHash();
|
|||
|
|
if (autoplayPending) {
|
|||
|
|
requestAnimationFrame(function () {
|
|||
|
|
requestAnimationFrame(maybeStartSharePlayback);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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(); }
|
|||
|
|
};
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* ============================================================
|
|||
|
|
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
|
|||
|
|
height without pushing summary cards below the viewport. Mobile,
|
|||
|
|
embed, presentation, print, and non-wide diagrams retain their own
|
|||
|
|
established layout contracts.
|
|||
|
|
============================================================ */
|
|||
|
|
Archify.waitForStableLayout = function (options) {
|
|||
|
|
options = options || {};
|
|||
|
|
var maximumFrames = Math.max(1, Number(options.maximumFrames) || 240);
|
|||
|
|
var fontsReady = document.fonts && document.fonts.ready
|
|||
|
|
? document.fonts.ready.catch(function () {})
|
|||
|
|
: Promise.resolve();
|
|||
|
|
return fontsReady.then(function () {
|
|||
|
|
if (typeof options.schedule === 'function') options.schedule();
|
|||
|
|
return new Promise(function (resolve, reject) {
|
|||
|
|
var previous = '';
|
|||
|
|
var stableFrames = 0;
|
|||
|
|
var sampledFrames = 0;
|
|||
|
|
function sample() {
|
|||
|
|
sampledFrames += 1;
|
|||
|
|
if (typeof options.pending === 'function' && options.pending()) {
|
|||
|
|
previous = '';
|
|||
|
|
stableFrames = 0;
|
|||
|
|
} else {
|
|||
|
|
var current = typeof options.snapshot === 'function' ? options.snapshot() : '';
|
|||
|
|
if (current === previous) stableFrames += 1;
|
|||
|
|
else {
|
|||
|
|
previous = current;
|
|||
|
|
stableFrames = 0;
|
|||
|
|
}
|
|||
|
|
if (stableFrames >= 3) {
|
|||
|
|
resolve({ stable: true, snapshot: current, sampledFrames: sampledFrames });
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (sampledFrames >= maximumFrames) {
|
|||
|
|
reject(new Error(options.timeoutMessage || 'Layout did not reach stable dimensions.'));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
requestAnimationFrame(sample);
|
|||
|
|
}
|
|||
|
|
requestAnimationFrame(sample);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
Archify.readerLayout = (function () {
|
|||
|
|
var html = document.documentElement;
|
|||
|
|
var body = document.body;
|
|||
|
|
var shell = document.querySelector('.container');
|
|||
|
|
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 frame = 0;
|
|||
|
|
var settleFrame = 0;
|
|||
|
|
var lastWidth = 0;
|
|||
|
|
var WIDE_RATIO = 1.55;
|
|||
|
|
var MIN_DESKTOP_WIDTH = 1024;
|
|||
|
|
var MIN_READER_WIDTH = 960;
|
|||
|
|
var MAX_READER_WIDTH = 1920;
|
|||
|
|
var SAFE_BOTTOM_GAP = 12;
|
|||
|
|
|
|||
|
|
if (diagram && ratio >= WIDE_RATIO) {
|
|||
|
|
diagram.setAttribute('data-wide-diagram', 'true');
|
|||
|
|
html.setAttribute('data-diagram-shape', 'wide');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function number(value) {
|
|||
|
|
var parsed = parseFloat(value);
|
|||
|
|
return Number.isFinite(parsed) ? parsed : 0;
|
|||
|
|
}
|
|||
|
|
function visible(element) {
|
|||
|
|
return Boolean(element && !element.hidden && window.getComputedStyle(element).display !== 'none');
|
|||
|
|
}
|
|||
|
|
function outerHeight(element) {
|
|||
|
|
if (!visible(element)) return 0;
|
|||
|
|
var style = window.getComputedStyle(element);
|
|||
|
|
return element.getBoundingClientRect().height + number(style.marginTop) + number(style.marginBottom);
|
|||
|
|
}
|
|||
|
|
function eligible() {
|
|||
|
|
return Boolean(
|
|||
|
|
shell && diagram && svg && ratio >= WIDE_RATIO &&
|
|||
|
|
window.innerWidth >= MIN_DESKTOP_WIDTH &&
|
|||
|
|
html.getAttribute('data-embed') !== 'true' &&
|
|||
|
|
html.getAttribute('data-present') !== 'true' &&
|
|||
|
|
(!window.matchMedia || !window.matchMedia('print').matches)
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
function clear() {
|
|||
|
|
html.style.removeProperty('--archify-reader-width');
|
|||
|
|
html.removeAttribute('data-reader-layout');
|
|||
|
|
html.removeAttribute('data-reader-overflow');
|
|||
|
|
lastWidth = 0;
|
|||
|
|
}
|
|||
|
|
function chromeMetrics() {
|
|||
|
|
var bodyStyle = window.getComputedStyle(body);
|
|||
|
|
var diagramStyle = window.getComputedStyle(diagram);
|
|||
|
|
return {
|
|||
|
|
bodyX: number(bodyStyle.paddingLeft) + number(bodyStyle.paddingRight),
|
|||
|
|
bodyY: number(bodyStyle.paddingTop) + number(bodyStyle.paddingBottom),
|
|||
|
|
diagramX: number(diagramStyle.paddingLeft) + number(diagramStyle.paddingRight) +
|
|||
|
|
number(diagramStyle.borderLeftWidth) + number(diagramStyle.borderRightWidth),
|
|||
|
|
diagramY: number(diagramStyle.paddingTop) + number(diagramStyle.paddingBottom) +
|
|||
|
|
number(diagramStyle.borderTopWidth) + number(diagramStyle.borderBottomWidth)
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
function applyWidth(width) {
|
|||
|
|
var rounded = Math.round(width);
|
|||
|
|
if (Math.abs(rounded - lastWidth) < 1) return false;
|
|||
|
|
lastWidth = rounded;
|
|||
|
|
html.style.setProperty('--archify-reader-width', rounded + 'px');
|
|||
|
|
html.setAttribute('data-reader-layout', 'adaptive');
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function settleOverflow(minWidth) {
|
|||
|
|
if (settleFrame) cancelAnimationFrame(settleFrame);
|
|||
|
|
settleFrame = requestAnimationFrame(function () {
|
|||
|
|
settleFrame = 0;
|
|||
|
|
if (!eligible() || !lastWidth) return;
|
|||
|
|
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));
|
|||
|
|
html.setAttribute('data-reader-overflow', 'reduced');
|
|||
|
|
} else if (overflow > 1) {
|
|||
|
|
html.setAttribute('data-reader-overflow', 'authored');
|
|||
|
|
} else {
|
|||
|
|
html.removeAttribute('data-reader-overflow');
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function measure() {
|
|||
|
|
frame = 0;
|
|||
|
|
if (!eligible()) {
|
|||
|
|
clear();
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
var chrome = chromeMetrics();
|
|||
|
|
var viewportCap = Math.max(0, window.innerWidth - chrome.bodyX);
|
|||
|
|
var minWidth = Math.min(MIN_READER_WIDTH, viewportCap);
|
|||
|
|
var maxWidth = Math.min(MAX_READER_WIDTH, viewportCap);
|
|||
|
|
var fixedHeight = chrome.bodyY + chrome.diagramY + SAFE_BOTTOM_GAP +
|
|||
|
|
outerHeight(header) + outerHeight(guided) + outerHeight(cards);
|
|||
|
|
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);
|
|||
|
|
settleOverflow(minWidth);
|
|||
|
|
return {
|
|||
|
|
ratio: ratio,
|
|||
|
|
width: Math.round(width),
|
|||
|
|
availableSvgHeight: Math.round(availableSvgHeight),
|
|||
|
|
fixedHeight: Math.round(fixedHeight)
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
function schedule() {
|
|||
|
|
if (frame) return;
|
|||
|
|
frame = requestAnimationFrame(measure);
|
|||
|
|
}
|
|||
|
|
function stableSnapshot() {
|
|||
|
|
var shellRect = shell ? shell.getBoundingClientRect() : { width: 0, height: 0 };
|
|||
|
|
var diagramRect = diagram ? diagram.getBoundingClientRect() : { width: 0, height: 0 };
|
|||
|
|
return [
|
|||
|
|
lastWidth,
|
|||
|
|
html.getAttribute('data-reader-layout') || '',
|
|||
|
|
html.getAttribute('data-reader-overflow') || '',
|
|||
|
|
Math.ceil(document.documentElement.scrollWidth),
|
|||
|
|
Math.ceil(document.documentElement.scrollHeight),
|
|||
|
|
Math.ceil(document.body.scrollWidth),
|
|||
|
|
Math.ceil(document.body.scrollHeight),
|
|||
|
|
Math.round(shellRect.width * 100) / 100,
|
|||
|
|
Math.round(shellRect.height * 100) / 100,
|
|||
|
|
Math.round(diagramRect.width * 100) / 100,
|
|||
|
|
Math.round(diagramRect.height * 100) / 100
|
|||
|
|
].join('|');
|
|||
|
|
}
|
|||
|
|
function whenStable() {
|
|||
|
|
return Archify.waitForStableLayout({
|
|||
|
|
schedule: schedule,
|
|||
|
|
pending: function () { return Boolean(frame || settleFrame); },
|
|||
|
|
snapshot: stableSnapshot,
|
|||
|
|
timeoutMessage: 'Adaptive reader layout did not reach stable dimensions.'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
window.addEventListener('resize', 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); });
|
|||
|
|
}
|
|||
|
|
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'] });
|
|||
|
|
}
|
|||
|
|
schedule();
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
measure: measure,
|
|||
|
|
schedule: schedule,
|
|||
|
|
whenStable: whenStable,
|
|||
|
|
active: function () { return html.getAttribute('data-reader-layout') === 'adaptive'; },
|
|||
|
|
receipt: function () { return { ratio: ratio, width: lastWidth }; }
|
|||
|
|
};
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* ============================================================
|
|||
|
|
Viewer Chrome Layout — keep HTML controls clear of the canonical SVG
|
|||
|
|
stage without changing authored geometry. The dock keeps its floating
|
|||
|
|
appearance while a small bottom stage rail becomes part of the reader
|
|||
|
|
budget whenever its zero-reserve position would enter the stage.
|
|||
|
|
============================================================ */
|
|||
|
|
Archify.viewerChromeLayout = (function () {
|
|||
|
|
var html = document.documentElement;
|
|||
|
|
var container = document.querySelector('.diagram-container');
|
|||
|
|
var svg = container && container.querySelector(':scope > svg');
|
|||
|
|
var nav = container && container.querySelector('.diagram-nav');
|
|||
|
|
var legend = svg && svg.querySelector('[data-legend]');
|
|||
|
|
var frame = 0;
|
|||
|
|
var settleFrame = 0;
|
|||
|
|
var reserve = 0;
|
|||
|
|
var railLatched = false;
|
|||
|
|
var probingBaseline = false;
|
|||
|
|
var probePromise = null;
|
|||
|
|
var baselineIntersectionArea = 0;
|
|||
|
|
var baselineStageGap = null;
|
|||
|
|
var restorableReserve = 0;
|
|||
|
|
var probeFallbackReserve = 0;
|
|||
|
|
var lastReceipt = null;
|
|||
|
|
var SAFE_GAP = 10;
|
|||
|
|
|
|||
|
|
function visible(element) {
|
|||
|
|
if (!element || element.hidden) return false;
|
|||
|
|
var style = window.getComputedStyle(element);
|
|||
|
|
return style.display !== 'none' && style.visibility !== 'hidden';
|
|||
|
|
}
|
|||
|
|
function usable(rect) {
|
|||
|
|
return Boolean(rect && rect.width > 0 && rect.height > 0);
|
|||
|
|
}
|
|||
|
|
function intersectionArea(a, b) {
|
|||
|
|
var width = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left));
|
|||
|
|
var height = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));
|
|||
|
|
return width * height;
|
|||
|
|
}
|
|||
|
|
function protectedStageRect() {
|
|||
|
|
if (!svg) return null;
|
|||
|
|
var rect = svg.getBoundingClientRect();
|
|||
|
|
var transform = '';
|
|||
|
|
try { transform = window.getComputedStyle(svg).transform || ''; } catch (_) {}
|
|||
|
|
var scaleX = 1;
|
|||
|
|
var scaleY = 1;
|
|||
|
|
var translateX = 0;
|
|||
|
|
var translateY = 0;
|
|||
|
|
var matrix = transform.match(/^matrix\(([^)]+)\)$/);
|
|||
|
|
var matrix3d = transform.match(/^matrix3d\(([^)]+)\)$/);
|
|||
|
|
if (matrix) {
|
|||
|
|
var values = matrix[1].split(',').map(Number);
|
|||
|
|
if (values.length === 6 && values.every(Number.isFinite)) {
|
|||
|
|
scaleX = Math.abs(values[0]) || 1;
|
|||
|
|
scaleY = Math.abs(values[3]) || 1;
|
|||
|
|
translateX = values[4];
|
|||
|
|
translateY = values[5];
|
|||
|
|
}
|
|||
|
|
} else if (matrix3d) {
|
|||
|
|
var values3d = matrix3d[1].split(',').map(Number);
|
|||
|
|
if (values3d.length === 16 && values3d.every(Number.isFinite)) {
|
|||
|
|
scaleX = Math.abs(values3d[0]) || 1;
|
|||
|
|
scaleY = Math.abs(values3d[5]) || 1;
|
|||
|
|
translateX = values3d[12];
|
|||
|
|
translateY = values3d[13];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
var width = rect.width / scaleX;
|
|||
|
|
var height = rect.height / scaleY;
|
|||
|
|
var left = rect.left - translateX;
|
|||
|
|
var top = rect.top - translateY;
|
|||
|
|
return {
|
|||
|
|
x: left,
|
|||
|
|
y: top,
|
|||
|
|
left: left,
|
|||
|
|
top: top,
|
|||
|
|
right: left + width,
|
|||
|
|
bottom: top + height,
|
|||
|
|
width: width,
|
|||
|
|
height: height
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
function eligible() {
|
|||
|
|
return Boolean(
|
|||
|
|
container && svg && nav &&
|
|||
|
|
window.innerWidth > 720 &&
|
|||
|
|
html.getAttribute('data-embed') !== 'true' &&
|
|||
|
|
(!window.matchMedia || !window.matchMedia('print').matches) &&
|
|||
|
|
visible(nav)
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
function cameraAtBaseline() {
|
|||
|
|
var scale = Number(svg && svg.getAttribute('data-view-scale'));
|
|||
|
|
return !Number.isFinite(scale) || Math.abs(scale - 1) < 0.001;
|
|||
|
|
}
|
|||
|
|
function writeReserve(next, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
next = Math.max(0, Math.ceil(next));
|
|||
|
|
if (Math.abs(next - reserve) < 1) return false;
|
|||
|
|
reserve = next;
|
|||
|
|
if (reserve) {
|
|||
|
|
if (options.remember !== false) restorableReserve = reserve;
|
|||
|
|
container.style.setProperty('--archify-nav-reserve', reserve + 'px');
|
|||
|
|
container.setAttribute('data-nav-stage-rail', 'true');
|
|||
|
|
html.setAttribute('data-nav-stage-rail', 'true');
|
|||
|
|
} else {
|
|||
|
|
container.style.removeProperty('--archify-nav-reserve');
|
|||
|
|
container.removeAttribute('data-nav-stage-rail');
|
|||
|
|
html.removeAttribute('data-nav-stage-rail');
|
|||
|
|
}
|
|||
|
|
if (Archify.readerLayout && typeof Archify.readerLayout.schedule === 'function') {
|
|||
|
|
Archify.readerLayout.schedule();
|
|||
|
|
}
|
|||
|
|
if (options.quiet !== true) {
|
|||
|
|
if (settleFrame) cancelAnimationFrame(settleFrame);
|
|||
|
|
settleFrame = requestAnimationFrame(function () {
|
|||
|
|
settleFrame = 0;
|
|||
|
|
schedule();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function clear(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
railLatched = false;
|
|||
|
|
if (options.preserveBaseline !== true) {
|
|||
|
|
baselineIntersectionArea = 0;
|
|||
|
|
baselineStageGap = null;
|
|||
|
|
restorableReserve = 0;
|
|||
|
|
}
|
|||
|
|
writeReserve(0, { quiet: probingBaseline });
|
|||
|
|
lastReceipt = {
|
|||
|
|
eligible: false,
|
|||
|
|
active: false,
|
|||
|
|
reserve: 0,
|
|||
|
|
gap: SAFE_GAP,
|
|||
|
|
baselineStageGap: null,
|
|||
|
|
stageGap: null,
|
|||
|
|
stageIntersectionArea: 0,
|
|||
|
|
baselineIntersectionArea: 0,
|
|||
|
|
intersectionArea: 0
|
|||
|
|
};
|
|||
|
|
return lastReceipt;
|
|||
|
|
}
|
|||
|
|
function measure() {
|
|||
|
|
frame = 0;
|
|||
|
|
if (probingBaseline) return null;
|
|||
|
|
if (!eligible()) return clear({ preserveBaseline: !cameraAtBaseline() });
|
|||
|
|
|
|||
|
|
/* Camera transforms enlarge and translate authored paint inside the
|
|||
|
|
fixed, clipped root SVG viewport. They must not redefine the
|
|||
|
|
reader's baseline rail.
|
|||
|
|
Restore that baseline after temporary mobile/embed/print states.
|
|||
|
|
Camera Reset preserves the established rail while viewport, mode,
|
|||
|
|
and content changes remain responsible for baseline reprobes. */
|
|||
|
|
if (!cameraAtBaseline()) {
|
|||
|
|
if (reserve === 0 && restorableReserve > 0) {
|
|||
|
|
if (writeReserve(restorableReserve)) return null;
|
|||
|
|
}
|
|||
|
|
var cameraNavRect = nav.getBoundingClientRect();
|
|||
|
|
var cameraLegendRect = visible(legend) ? legend.getBoundingClientRect() : null;
|
|||
|
|
var cameraStageRect = protectedStageRect();
|
|||
|
|
var cameraIntersectionArea = usable(cameraLegendRect) && intersectionArea(cameraNavRect, cameraStageRect) > 0
|
|||
|
|
? intersectionArea(cameraNavRect, cameraLegendRect)
|
|||
|
|
: 0;
|
|||
|
|
lastReceipt = {
|
|||
|
|
eligible: true,
|
|||
|
|
active: reserve > 0,
|
|||
|
|
reserve: reserve,
|
|||
|
|
gap: SAFE_GAP,
|
|||
|
|
baselineStageGap: baselineStageGap == null ? null : Math.round(baselineStageGap * 100) / 100,
|
|||
|
|
stageGap: Math.round((cameraNavRect.top - cameraStageRect.bottom) * 100) / 100,
|
|||
|
|
stageIntersectionArea: Math.round(intersectionArea(cameraNavRect, cameraStageRect) * 100) / 100,
|
|||
|
|
baselineIntersectionArea: Math.round(baselineIntersectionArea * 100) / 100,
|
|||
|
|
intersectionArea: Math.round(cameraIntersectionArea * 100) / 100
|
|||
|
|
};
|
|||
|
|
return lastReceipt;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var navRect = nav.getBoundingClientRect();
|
|||
|
|
var legendRect = visible(legend) ? legend.getBoundingClientRect() : null;
|
|||
|
|
var stageRect = protectedStageRect();
|
|||
|
|
if (!usable(navRect) || !usable(stageRect)) return clear();
|
|||
|
|
|
|||
|
|
var actualIntersectionArea = usable(legendRect) ? intersectionArea(navRect, legendRect) : 0;
|
|||
|
|
var stageGap = navRect.top - stageRect.bottom;
|
|||
|
|
if (!railLatched && reserve === 0) {
|
|||
|
|
baselineIntersectionArea = actualIntersectionArea;
|
|||
|
|
baselineStageGap = stageGap;
|
|||
|
|
if (stageGap < SAFE_GAP) {
|
|||
|
|
railLatched = true;
|
|||
|
|
if (writeReserve(Math.max(0, SAFE_GAP - stageGap))) return null;
|
|||
|
|
}
|
|||
|
|
} else if (railLatched && reserve > 0 && stageGap < SAFE_GAP) {
|
|||
|
|
/* Keep the decision latched while Adaptive Reader incorporates the
|
|||
|
|
rail. Presentation and rounded layout values can require one
|
|||
|
|
bounded follow-up before the full stage gap is established. */
|
|||
|
|
var remaining = Math.max(1, SAFE_GAP - stageGap);
|
|||
|
|
if (writeReserve(reserve + remaining)) return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
navRect = nav.getBoundingClientRect();
|
|||
|
|
legendRect = visible(legend) ? legend.getBoundingClientRect() : null;
|
|||
|
|
stageRect = protectedStageRect();
|
|||
|
|
stageGap = navRect.top - stageRect.bottom;
|
|||
|
|
lastReceipt = {
|
|||
|
|
eligible: true,
|
|||
|
|
active: reserve > 0,
|
|||
|
|
reserve: reserve,
|
|||
|
|
gap: SAFE_GAP,
|
|||
|
|
baselineStageGap: baselineStageGap == null ? null : Math.round(baselineStageGap * 100) / 100,
|
|||
|
|
stageGap: Math.round(stageGap * 100) / 100,
|
|||
|
|
stageIntersectionArea: Math.round(intersectionArea(navRect, stageRect) * 100) / 100,
|
|||
|
|
baselineIntersectionArea: Math.round(baselineIntersectionArea * 100) / 100,
|
|||
|
|
intersectionArea: Math.round((usable(legendRect) ? intersectionArea(navRect, legendRect) : 0) * 100) / 100
|
|||
|
|
};
|
|||
|
|
return lastReceipt;
|
|||
|
|
}
|
|||
|
|
function reprobe() {
|
|||
|
|
if (probingBaseline) return probePromise || Promise.resolve(false);
|
|||
|
|
if (!cameraAtBaseline()) {
|
|||
|
|
schedule();
|
|||
|
|
return Promise.resolve(false);
|
|||
|
|
}
|
|||
|
|
if (!reserve && !railLatched && !restorableReserve) {
|
|||
|
|
schedule();
|
|||
|
|
return Promise.resolve(false);
|
|||
|
|
}
|
|||
|
|
probeFallbackReserve = restorableReserve || reserve;
|
|||
|
|
probingBaseline = true;
|
|||
|
|
railLatched = false;
|
|||
|
|
baselineIntersectionArea = 0;
|
|||
|
|
baselineStageGap = null;
|
|||
|
|
restorableReserve = 0;
|
|||
|
|
writeReserve(0, { quiet: true });
|
|||
|
|
var readerReady = Archify.readerLayout && typeof Archify.readerLayout.whenStable === 'function'
|
|||
|
|
? Archify.readerLayout.whenStable().catch(function () {})
|
|||
|
|
: Promise.resolve();
|
|||
|
|
probePromise = readerReady.then(function () {
|
|||
|
|
probingBaseline = false;
|
|||
|
|
probePromise = null;
|
|||
|
|
if (!cameraAtBaseline() && probeFallbackReserve > 0) {
|
|||
|
|
restorableReserve = probeFallbackReserve;
|
|||
|
|
}
|
|||
|
|
probeFallbackReserve = 0;
|
|||
|
|
schedule();
|
|||
|
|
return true;
|
|||
|
|
});
|
|||
|
|
return probePromise;
|
|||
|
|
}
|
|||
|
|
function schedule() {
|
|||
|
|
if (frame) return;
|
|||
|
|
frame = requestAnimationFrame(measure);
|
|||
|
|
}
|
|||
|
|
function stableSnapshot() {
|
|||
|
|
var containerRect = container ? container.getBoundingClientRect() : { width: 0, height: 0 };
|
|||
|
|
var navRect = nav ? nav.getBoundingClientRect() : { top: 0, left: 0 };
|
|||
|
|
var legendRect = legend ? legend.getBoundingClientRect() : { top: 0, left: 0 };
|
|||
|
|
return [
|
|||
|
|
reserve,
|
|||
|
|
Math.round(containerRect.width * 100) / 100,
|
|||
|
|
Math.round(containerRect.height * 100) / 100,
|
|||
|
|
Math.round(navRect.left * 100) / 100,
|
|||
|
|
Math.round(navRect.top * 100) / 100,
|
|||
|
|
Math.round(legendRect.left * 100) / 100,
|
|||
|
|
Math.round(legendRect.top * 100) / 100,
|
|||
|
|
railLatched ? 'latched' : 'clear',
|
|||
|
|
lastReceipt ? lastReceipt.stageGap : ''
|
|||
|
|
].join('|');
|
|||
|
|
}
|
|||
|
|
function whenStable() {
|
|||
|
|
return Archify.waitForStableLayout({
|
|||
|
|
schedule: schedule,
|
|||
|
|
pending: function () { return Boolean(frame || settleFrame || probingBaseline); },
|
|||
|
|
snapshot: stableSnapshot,
|
|||
|
|
timeoutMessage: 'Viewer chrome layout did not reach stable dimensions.'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
window.addEventListener('resize', reprobe, { passive: true });
|
|||
|
|
window.addEventListener('load', schedule, { once: true });
|
|||
|
|
window.addEventListener('beforeprint', schedule);
|
|||
|
|
window.addEventListener('afterprint', reprobe);
|
|||
|
|
if (document.fonts && document.fonts.ready) document.fonts.ready.then(reprobe).catch(function () {});
|
|||
|
|
if (typeof ResizeObserver === 'function') {
|
|||
|
|
var resizeObserver = new ResizeObserver(schedule);
|
|||
|
|
[nav, svg, legend].forEach(function (element) { if (element) resizeObserver.observe(element); });
|
|||
|
|
}
|
|||
|
|
if (typeof MutationObserver === 'function') {
|
|||
|
|
var contentObserver = new MutationObserver(function (records) {
|
|||
|
|
var viewerModeChanged = records.some(function (record) { return record.target === html; });
|
|||
|
|
if (viewerModeChanged) reprobe();
|
|||
|
|
else schedule();
|
|||
|
|
});
|
|||
|
|
if (legend) contentObserver.observe(legend, { attributes: true, childList: true, subtree: true });
|
|||
|
|
contentObserver.observe(html, {
|
|||
|
|
attributes: true,
|
|||
|
|
attributeFilter: ['data-embed', 'data-present', 'data-preset', 'data-theme']
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
schedule();
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
measure: measure,
|
|||
|
|
schedule: schedule,
|
|||
|
|
reprobe: reprobe,
|
|||
|
|
whenStable: whenStable,
|
|||
|
|
stageRect: protectedStageRect,
|
|||
|
|
active: function () { return reserve > 0; },
|
|||
|
|
receipt: function () { return lastReceipt || measure(); }
|
|||
|
|
};
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
Archify.view = (function () {
|
|||
|
|
var container = document.querySelector('.diagram-container');
|
|||
|
|
var svg = container.querySelector('svg');
|
|||
|
|
var outBtn = container.querySelector('[data-view="out"]');
|
|||
|
|
var resetBtn = container.querySelector('[data-view="reset"]');
|
|||
|
|
var resetDetailLabel = resetBtn.querySelector('[data-view-detail]');
|
|||
|
|
var resetPercentLabel = resetBtn.querySelector('[data-view-percent]');
|
|||
|
|
var inBtn = container.querySelector('[data-view="in"]');
|
|||
|
|
var state = { scale: 1, x: 0, y: 0, mode: 'overview' };
|
|||
|
|
var drag = null;
|
|||
|
|
var cameraTimer = null;
|
|||
|
|
var cameraFrame = null;
|
|||
|
|
var cameraGeneration = 0;
|
|||
|
|
var cameraTransaction = null;
|
|||
|
|
var clipFrame = 0;
|
|||
|
|
var resizeFrame = 0;
|
|||
|
|
var autoScrollUntil = 0;
|
|||
|
|
|
|||
|
|
var viewBox = svg.viewBox && svg.viewBox.baseVal;
|
|||
|
|
|
|||
|
|
function clamp() {
|
|||
|
|
var width = svg.clientWidth || 1;
|
|||
|
|
var height = svg.clientHeight || 1;
|
|||
|
|
state.x = Math.min(0, Math.max(width - width * state.scale, state.x));
|
|||
|
|
state.y = Math.min(0, Math.max(height - height * state.scale, state.y));
|
|||
|
|
}
|
|||
|
|
function reducedMotion() {
|
|||
|
|
return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|||
|
|
}
|
|||
|
|
function contentMetrics() {
|
|||
|
|
if (!viewBox || viewBox.width <= 0 || viewBox.height <= 0) return null;
|
|||
|
|
var width = svg.clientWidth || 1;
|
|||
|
|
var height = svg.clientHeight || 1;
|
|||
|
|
var scale = Math.min(width / viewBox.width, height / viewBox.height);
|
|||
|
|
return {
|
|||
|
|
width: width,
|
|||
|
|
height: height,
|
|||
|
|
scale: scale,
|
|||
|
|
offsetX: (width - viewBox.width * scale) / 2,
|
|||
|
|
offsetY: (height - viewBox.height * scale) / 2
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
function logicalViewport() {
|
|||
|
|
var metrics = contentMetrics();
|
|||
|
|
if (!metrics) return null;
|
|||
|
|
var x;
|
|||
|
|
var y;
|
|||
|
|
var width;
|
|||
|
|
var height;
|
|||
|
|
if (window.innerWidth <= 720 && container.hasAttribute('data-wide-diagram')) {
|
|||
|
|
x = viewBox.x + container.scrollLeft / metrics.scale;
|
|||
|
|
y = viewBox.y;
|
|||
|
|
width = Math.min(viewBox.width, Math.max(1, container.clientWidth / metrics.scale));
|
|||
|
|
height = viewBox.height;
|
|||
|
|
} else {
|
|||
|
|
x = viewBox.x + ((-state.x / state.scale) - metrics.offsetX) / metrics.scale;
|
|||
|
|
y = viewBox.y + ((-state.y / state.scale) - metrics.offsetY) / metrics.scale;
|
|||
|
|
width = Math.min(viewBox.width, metrics.width / state.scale / metrics.scale);
|
|||
|
|
height = Math.min(viewBox.height, metrics.height / state.scale / metrics.scale);
|
|||
|
|
}
|
|||
|
|
width = Math.max(1, Math.min(viewBox.width, width));
|
|||
|
|
height = Math.max(1, Math.min(viewBox.height, height));
|
|||
|
|
x = Math.max(viewBox.x, Math.min(viewBox.x + viewBox.width - width, x));
|
|||
|
|
y = Math.max(viewBox.y, Math.min(viewBox.y + viewBox.height - height, y));
|
|||
|
|
return { x: x, y: y, width: width, height: height, scale: state.scale };
|
|||
|
|
}
|
|||
|
|
function detailLevel() {
|
|||
|
|
if (state.mode === 'semantic') return 'full';
|
|||
|
|
if (state.scale >= 1.75) return 'full';
|
|||
|
|
if (state.scale >= 1) return 'read';
|
|||
|
|
return 'map';
|
|||
|
|
}
|
|||
|
|
function renderControls() {
|
|||
|
|
var semantic = state.mode === 'semantic' && state.scale > 1.01;
|
|||
|
|
var detail = detailLevel();
|
|||
|
|
var percent = Math.round(state.scale * 100) + '%';
|
|||
|
|
var levelLabel = viewerText('viewer.nav.level.' + detail);
|
|||
|
|
var detailHint = detail === 'map'
|
|||
|
|
? viewerText('viewer.nav.detail.map')
|
|||
|
|
: detail === 'read'
|
|||
|
|
? viewerText('viewer.nav.detail.read')
|
|||
|
|
: viewerText('viewer.nav.detail.full');
|
|||
|
|
var resolvedLevel = semantic ? viewerText('viewer.nav.level.auto') : levelLabel;
|
|||
|
|
var showDetailLevel = semantic || detail !== 'read';
|
|||
|
|
if (resetDetailLabel) {
|
|||
|
|
resetDetailLabel.textContent = resolvedLevel;
|
|||
|
|
resetDetailLabel.hidden = !showDetailLevel;
|
|||
|
|
}
|
|||
|
|
if (resetPercentLabel) resetPercentLabel.textContent = percent;
|
|||
|
|
resetBtn.toggleAttribute('data-detail-visible', showDetailLevel);
|
|||
|
|
resetBtn.title = viewerText('viewer.nav.camera.title', {
|
|||
|
|
semantic: semantic ? viewerText('viewer.nav.camera.semantic') : '',
|
|||
|
|
hint: detailHint
|
|||
|
|
});
|
|||
|
|
resetBtn.setAttribute('aria-label', viewerText('viewer.nav.camera', { hint: detailHint }));
|
|||
|
|
resetBtn.setAttribute('data-detail-level', detail);
|
|||
|
|
container.setAttribute('data-detail-level', detail);
|
|||
|
|
container.setAttribute('data-camera-mode', state.mode);
|
|||
|
|
container.setAttribute('data-camera-indicator', semantic ? 'true' : 'false');
|
|||
|
|
}
|
|||
|
|
function clipToViewport(camera) {
|
|||
|
|
camera = camera || state;
|
|||
|
|
if (camera.scale <= 1.001) {
|
|||
|
|
svg.style.removeProperty('clip-path');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
var width = svg.clientWidth || 1;
|
|||
|
|
var height = svg.clientHeight || 1;
|
|||
|
|
var scale = camera.scale;
|
|||
|
|
var top = Math.max(0, Math.min(height, -camera.y / scale));
|
|||
|
|
var left = Math.max(0, Math.min(width, -camera.x / scale));
|
|||
|
|
var right = Math.max(0, Math.min(width, width - (width - camera.x) / scale));
|
|||
|
|
var bottom = Math.max(0, Math.min(height, height - (height - camera.y) / scale));
|
|||
|
|
svg.style.clipPath = 'inset(' + [top, right, bottom, left].map(function (value) {
|
|||
|
|
return Math.round(value * 1000) / 1000 + 'px';
|
|||
|
|
}).join(' ') + ')';
|
|||
|
|
}
|
|||
|
|
function cameraSettled(rendered) {
|
|||
|
|
return Math.abs(rendered.scale - state.scale) < 0.001 &&
|
|||
|
|
Math.abs(rendered.x - state.x) < 0.05 &&
|
|||
|
|
Math.abs(rendered.y - state.y) < 0.05;
|
|||
|
|
}
|
|||
|
|
function syncViewportClip() {
|
|||
|
|
if (clipFrame) cancelAnimationFrame(clipFrame);
|
|||
|
|
clipFrame = 0;
|
|||
|
|
function sample() {
|
|||
|
|
clipFrame = 0;
|
|||
|
|
var rendered = sampleRenderedState();
|
|||
|
|
clipToViewport(rendered);
|
|||
|
|
if (!cameraSettled(rendered)) clipFrame = requestAnimationFrame(sample);
|
|||
|
|
}
|
|||
|
|
sample();
|
|||
|
|
}
|
|||
|
|
function apply() {
|
|||
|
|
clamp();
|
|||
|
|
svg.style.transform = 'translate(' + state.x + 'px,' + state.y + 'px) scale(' + state.scale + ')';
|
|||
|
|
syncViewportClip();
|
|||
|
|
renderControls();
|
|||
|
|
outBtn.disabled = state.scale <= 1;
|
|||
|
|
inBtn.disabled = state.scale >= 3;
|
|||
|
|
container.classList.toggle('is-pannable', state.scale > 1);
|
|||
|
|
svg.setAttribute('data-view-scale', String(state.scale));
|
|||
|
|
if (Archify.radar && typeof Archify.radar.sync === 'function') Archify.radar.sync();
|
|||
|
|
if (Archify.viewerChromeLayout && typeof Archify.viewerChromeLayout.schedule === 'function') {
|
|||
|
|
Archify.viewerChromeLayout.schedule();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
function sampleRenderedState() {
|
|||
|
|
var transform = '';
|
|||
|
|
try { transform = getComputedStyle(svg).transform || ''; } catch (_) {}
|
|||
|
|
var match = transform.match(/^matrix\(([^)]+)\)$/);
|
|||
|
|
if (!match) return { scale: state.scale, x: state.x, y: state.y, mode: state.mode };
|
|||
|
|
var values = match[1].split(',').map(Number);
|
|||
|
|
if (values.length !== 6 || !values.every(Number.isFinite)) {
|
|||
|
|
return { scale: state.scale, x: state.x, y: state.y, mode: state.mode };
|
|||
|
|
}
|
|||
|
|
return { scale: values[0], x: values[4], y: values[5], mode: state.mode };
|
|||
|
|
}
|
|||
|
|
function finishCameraTransaction(transaction, outcome) {
|
|||
|
|
if (!transaction || transaction.settled) return false;
|
|||
|
|
transaction.settled = true;
|
|||
|
|
transaction.state = outcome || 'complete';
|
|||
|
|
if (transaction.frame) cancelAnimationFrame(transaction.frame);
|
|||
|
|
if (transaction.timer) clearTimeout(transaction.timer);
|
|||
|
|
transaction.frame = null;
|
|||
|
|
transaction.timer = null;
|
|||
|
|
if (cameraTransaction === transaction) cameraTransaction = null;
|
|||
|
|
cameraFrame = null;
|
|||
|
|
cameraTimer = null;
|
|||
|
|
container.classList.remove('is-camera-moving');
|
|||
|
|
container.classList.remove('is-camera-transaction');
|
|||
|
|
container.removeAttribute('data-camera-transaction');
|
|||
|
|
if (Archify.focus && Archify.focus.reposition) Archify.focus.reposition();
|
|||
|
|
transaction.resolve({ id: transaction.id, state: transaction.state });
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function cameraReceipt(target, options) {
|
|||
|
|
var resolver;
|
|||
|
|
var transaction = {
|
|||
|
|
id: ++cameraGeneration,
|
|||
|
|
state: 'running',
|
|||
|
|
target: target,
|
|||
|
|
settled: false,
|
|||
|
|
frame: null,
|
|||
|
|
timer: null,
|
|||
|
|
finished: new Promise(function (resolve) { resolver = resolve; }),
|
|||
|
|
resolve: resolver,
|
|||
|
|
cancel: function (reason, commitTarget) {
|
|||
|
|
if (transaction.settled) return false;
|
|||
|
|
if (commitTarget && transaction.target) {
|
|||
|
|
if (Object.prototype.hasOwnProperty.call(transaction.target, 'scrollLeft')) {
|
|||
|
|
container.scrollLeft = transaction.target.scrollLeft;
|
|||
|
|
} else {
|
|||
|
|
state = {
|
|||
|
|
scale: transaction.target.scale,
|
|||
|
|
x: transaction.target.x,
|
|||
|
|
y: transaction.target.y,
|
|||
|
|
mode: transaction.target.mode
|
|||
|
|
};
|
|||
|
|
apply();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return finishCameraTransaction(transaction, reason || 'cancelled');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
return transaction;
|
|||
|
|
}
|
|||
|
|
function stopCameraMotion(reason, commitTarget) {
|
|||
|
|
if (cameraTransaction && !cameraTransaction.settled) {
|
|||
|
|
cameraTransaction.cancel(reason || 'cancelled', commitTarget === true);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (cameraTimer) clearTimeout(cameraTimer);
|
|||
|
|
if (cameraFrame) cancelAnimationFrame(cameraFrame);
|
|||
|
|
cameraTimer = null;
|
|||
|
|
cameraFrame = null;
|
|||
|
|
container.classList.remove('is-camera-moving');
|
|||
|
|
container.classList.remove('is-camera-transaction');
|
|||
|
|
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' });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
function zoom(next, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (options.manual !== false) interruptCamera();
|
|||
|
|
var previous = state.scale;
|
|||
|
|
next = Math.max(1, Math.min(3, Math.round(next * 4) / 4));
|
|||
|
|
if (next === previous) return;
|
|||
|
|
var centerX = (svg.clientWidth || 1) / 2;
|
|||
|
|
var centerY = (svg.clientHeight || 1) / 2;
|
|||
|
|
var contentX = (centerX - state.x) / previous;
|
|||
|
|
var contentY = (centerY - state.y) / previous;
|
|||
|
|
state.scale = next;
|
|||
|
|
state.x = centerX - contentX * next;
|
|||
|
|
state.y = centerY - contentY * next;
|
|||
|
|
apply();
|
|||
|
|
}
|
|||
|
|
function reset(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (options.automatic !== true) interruptCamera();
|
|||
|
|
else stopCameraMotion('reset', false);
|
|||
|
|
state = { scale: 1, x: 0, y: 0, mode: 'overview' };
|
|||
|
|
apply();
|
|||
|
|
}
|
|||
|
|
function centerAt(logicalX, logicalY, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
logicalX = Number(logicalX);
|
|||
|
|
logicalY = Number(logicalY);
|
|||
|
|
var metrics = contentMetrics();
|
|||
|
|
if (!metrics || !Number.isFinite(logicalX) || !Number.isFinite(logicalY)) return false;
|
|||
|
|
interruptCamera();
|
|||
|
|
if (window.innerWidth <= 720 && container.hasAttribute('data-wide-diagram')) {
|
|||
|
|
state.scale = 1;
|
|||
|
|
state.x = 0;
|
|||
|
|
state.y = 0;
|
|||
|
|
state.mode = 'manual';
|
|||
|
|
apply();
|
|||
|
|
var mobileTarget = (logicalX - viewBox.x) * metrics.scale - container.clientWidth / 2;
|
|||
|
|
mobileTarget = Math.max(0, Math.min(svg.clientWidth - container.clientWidth, mobileTarget));
|
|||
|
|
autoScrollUntil = Date.now() + 80;
|
|||
|
|
try { container.scrollTo({ left: mobileTarget, behavior: options.instant ? 'auto' : 'smooth' }); }
|
|||
|
|
catch (_) { container.scrollLeft = mobileTarget; }
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
var minimumScale = Math.max(1, Math.min(3, Number(options.minimumScale) || 1));
|
|||
|
|
var requestedScale = Number(options.scale);
|
|||
|
|
state.scale = Math.max(minimumScale, Math.min(3, Number.isFinite(requestedScale) ? requestedScale : state.scale));
|
|||
|
|
var contentX = metrics.offsetX + (logicalX - viewBox.x) * metrics.scale;
|
|||
|
|
var contentY = metrics.offsetY + (logicalY - viewBox.y) * metrics.scale;
|
|||
|
|
state.x = metrics.width / 2 - contentX * state.scale;
|
|||
|
|
state.y = metrics.height / 2 - contentY * state.scale;
|
|||
|
|
state.mode = 'manual';
|
|||
|
|
apply();
|
|||
|
|
if (Archify.focus && Archify.focus.reposition) Archify.focus.reposition();
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function semanticIds(ids, includeNeighbors) {
|
|||
|
|
var seeds = {};
|
|||
|
|
var wanted = {};
|
|||
|
|
(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) {
|
|||
|
|
var from = edge.getAttribute('data-edge-from');
|
|||
|
|
var to = edge.getAttribute('data-edge-to');
|
|||
|
|
if (seeds[from] || seeds[to]) { wanted[from] = true; wanted[to] = true; }
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
return wanted;
|
|||
|
|
}
|
|||
|
|
function boxesFor(ids, includeNeighbors) {
|
|||
|
|
var wanted = semanticIds(ids, includeNeighbors);
|
|||
|
|
return Array.prototype.slice.call(svg.querySelectorAll('[data-node-id]'))
|
|||
|
|
.filter(function (node) { return wanted[node.getAttribute('data-node-id')]; })
|
|||
|
|
.map(function (node) {
|
|||
|
|
try { return node.getBBox(); } catch (_) { return null; }
|
|||
|
|
})
|
|||
|
|
.filter(Boolean);
|
|||
|
|
}
|
|||
|
|
function frameDesktop(ids, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
var boxes = boxesFor(ids, options.includeNeighbors === true);
|
|||
|
|
if (!boxes.length || !viewBox || viewBox.width <= 0 || viewBox.height <= 0) return false;
|
|||
|
|
var svgWidth = svg.clientWidth || 1;
|
|||
|
|
var svgHeight = svg.clientHeight || 1;
|
|||
|
|
var contentScale = Math.min(svgWidth / viewBox.width, svgHeight / viewBox.height);
|
|||
|
|
var contentOffsetX = (svgWidth - viewBox.width * contentScale) / 2;
|
|||
|
|
var contentOffsetY = (svgHeight - viewBox.height * contentScale) / 2;
|
|||
|
|
var minX = Math.min.apply(Math, boxes.map(function (box) { return box.x; }));
|
|||
|
|
var minY = Math.min.apply(Math, boxes.map(function (box) { return box.y; }));
|
|||
|
|
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,
|
|||
|
|
width: Math.max(1, (maxX - minX) * contentScale),
|
|||
|
|
height: Math.max(1, (maxY - minY) * contentScale)
|
|||
|
|
};
|
|||
|
|
var padding = options.padding || 48;
|
|||
|
|
var left = padding;
|
|||
|
|
var right = svgWidth - padding;
|
|||
|
|
var top = padding;
|
|||
|
|
var bottom = svgHeight - Math.max(padding, 72);
|
|||
|
|
var containerRect = container.getBoundingClientRect();
|
|||
|
|
var visibleTop = Math.max(0, -containerRect.top);
|
|||
|
|
var visibleBottom = Math.min(svgHeight, window.innerHeight - containerRect.top);
|
|||
|
|
if (visibleBottom - visibleTop >= 240) {
|
|||
|
|
top = Math.max(top, visibleTop + padding);
|
|||
|
|
bottom = Math.min(bottom, visibleBottom - Math.max(padding, 72));
|
|||
|
|
}
|
|||
|
|
var chip = document.getElementById('focus-chip');
|
|||
|
|
if (chip && !chip.hidden) {
|
|||
|
|
var lensEnd = chip.offsetLeft + chip.offsetWidth + 24 - (svg.offsetLeft || 0);
|
|||
|
|
left = Math.max(left, Math.min(svgWidth * 0.42, lensEnd));
|
|||
|
|
}
|
|||
|
|
var routeReceipt = document.getElementById('route-probe');
|
|||
|
|
if (routeReceipt && !routeReceipt.hidden && routeReceipt.hasAttribute('data-route-journey')) {
|
|||
|
|
var receiptTop = routeReceipt.offsetTop;
|
|||
|
|
var receiptBottom = receiptTop + routeReceipt.offsetHeight;
|
|||
|
|
if (receiptTop < svgHeight / 2) top = Math.max(top, receiptBottom + 24);
|
|||
|
|
else bottom = Math.min(bottom, receiptTop - 24);
|
|||
|
|
}
|
|||
|
|
if (right <= left || bottom <= top) return false;
|
|||
|
|
var maxScale = options.maxScale || (options.includeNeighbors ? 1.9 : 2.15);
|
|||
|
|
var targetScale = Math.min((right - left) / bounds.width, (bottom - top) / bounds.height) * 0.9;
|
|||
|
|
targetScale = Math.max(1, Math.min(maxScale, targetScale));
|
|||
|
|
if (targetScale < 1.08) targetScale = 1;
|
|||
|
|
var target = {
|
|||
|
|
scale: Math.round(targetScale * 100) / 100,
|
|||
|
|
x: 0,
|
|||
|
|
y: 0,
|
|||
|
|
mode: 'semantic'
|
|||
|
|
};
|
|||
|
|
target.x = (left + right) / 2 - (bounds.x + bounds.width / 2) * target.scale;
|
|||
|
|
target.y = (top + bottom) / 2 - (bounds.y + bounds.height / 2) * target.scale;
|
|||
|
|
var start = sampleRenderedState();
|
|||
|
|
stopCameraMotion('replaced', false);
|
|||
|
|
var transaction = cameraReceipt(target, options);
|
|||
|
|
cameraTransaction = transaction;
|
|||
|
|
var instant = options.instant === true || reducedMotion() || document.hidden;
|
|||
|
|
if (instant) {
|
|||
|
|
state = target;
|
|||
|
|
apply();
|
|||
|
|
finishCameraTransaction(transaction, reducedMotion() ? 'reduced-motion' : (document.hidden ? 'hidden' : 'complete'));
|
|||
|
|
return transaction;
|
|||
|
|
}
|
|||
|
|
var duration = Math.max(180, Math.min(520, Number(options.duration) || 420));
|
|||
|
|
var startedAt = 0;
|
|||
|
|
state = start;
|
|||
|
|
state.mode = 'semantic';
|
|||
|
|
apply();
|
|||
|
|
container.classList.add('is-camera-moving');
|
|||
|
|
container.classList.add('is-camera-transaction');
|
|||
|
|
container.setAttribute('data-camera-transaction', String(transaction.id));
|
|||
|
|
var step = function (timestamp) {
|
|||
|
|
if (cameraTransaction !== transaction || transaction.settled) return;
|
|||
|
|
if (!startedAt) startedAt = timestamp;
|
|||
|
|
var fraction = Math.max(0, Math.min(1, (timestamp - startedAt) / duration));
|
|||
|
|
var eased = 1 - Math.pow(1 - fraction, 3);
|
|||
|
|
state = {
|
|||
|
|
scale: start.scale + (target.scale - start.scale) * eased,
|
|||
|
|
x: start.x + (target.x - start.x) * eased,
|
|||
|
|
y: start.y + (target.y - start.y) * eased,
|
|||
|
|
mode: 'semantic'
|
|||
|
|
};
|
|||
|
|
apply();
|
|||
|
|
if (fraction < 1) {
|
|||
|
|
transaction.frame = requestAnimationFrame(step);
|
|||
|
|
cameraFrame = transaction.frame;
|
|||
|
|
} else {
|
|||
|
|
state = target;
|
|||
|
|
apply();
|
|||
|
|
finishCameraTransaction(transaction, 'complete');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
transaction.frame = requestAnimationFrame(step);
|
|||
|
|
cameraFrame = transaction.frame;
|
|||
|
|
return transaction;
|
|||
|
|
}
|
|||
|
|
function reveal(ids, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (window.innerWidth > 720) return frameDesktop(ids, options);
|
|||
|
|
stopCameraMotion('replaced', false);
|
|||
|
|
state.scale = 1;
|
|||
|
|
state.x = 0;
|
|||
|
|
state.y = 0;
|
|||
|
|
state.mode = 'semantic';
|
|||
|
|
apply();
|
|||
|
|
if (!container.hasAttribute('data-wide-diagram')) {
|
|||
|
|
var contained = cameraReceipt({ scale: 1, x: 0, y: 0, mode: 'semantic' }, options);
|
|||
|
|
cameraTransaction = contained;
|
|||
|
|
finishCameraTransaction(contained, 'complete');
|
|||
|
|
return contained;
|
|||
|
|
}
|
|||
|
|
var boxes = boxesFor(ids, options.includeNeighbors === true);
|
|||
|
|
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 target = Math.max(0, Math.min(svg.clientWidth - container.clientWidth, center - container.clientWidth / 2));
|
|||
|
|
var transaction = cameraReceipt({ scrollLeft: target }, options);
|
|||
|
|
cameraTransaction = transaction;
|
|||
|
|
var instant = options.instant === true || reducedMotion() || document.hidden;
|
|||
|
|
autoScrollUntil = Date.now() + (instant ? 50 : 470);
|
|||
|
|
try { container.scrollTo({ left: target, behavior: instant ? 'auto' : 'smooth' }); }
|
|||
|
|
catch (_) { container.scrollLeft = target; }
|
|||
|
|
if (instant) finishCameraTransaction(transaction, reducedMotion() ? 'reduced-motion' : (document.hidden ? 'hidden' : 'complete'));
|
|||
|
|
else {
|
|||
|
|
transaction.timer = setTimeout(function () { finishCameraTransaction(transaction, 'complete'); }, 460);
|
|||
|
|
cameraTimer = transaction.timer;
|
|||
|
|
container.classList.add('is-camera-moving');
|
|||
|
|
container.setAttribute('data-camera-transaction', String(transaction.id));
|
|||
|
|
}
|
|||
|
|
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' });
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
function pinControls() {
|
|||
|
|
container.style.setProperty('--archify-scroll-x', container.scrollLeft + 'px');
|
|||
|
|
}
|
|||
|
|
function onScroll() {
|
|||
|
|
pinControls();
|
|||
|
|
if (Archify.radar && typeof Archify.radar.sync === 'function') Archify.radar.sync();
|
|||
|
|
if (window.innerWidth <= 720 && container.hasAttribute('data-wide-diagram') && Date.now() > autoScrollUntil) {
|
|||
|
|
interruptCamera();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
function onPointerEnd(event) {
|
|||
|
|
if (!drag) return;
|
|||
|
|
var moved = drag.moved;
|
|||
|
|
drag = null;
|
|||
|
|
container.classList.remove('is-panning');
|
|||
|
|
try { container.releasePointerCapture(event.pointerId); } catch (_) {}
|
|||
|
|
if (moved) {
|
|||
|
|
container.setAttribute('data-just-panned', 'true');
|
|||
|
|
setTimeout(function () { container.removeAttribute('data-just-panned'); }, 80);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
inBtn.addEventListener('click', function () { zoom(state.scale + 0.25); });
|
|||
|
|
outBtn.addEventListener('click', function () { zoom(state.scale - 0.25); });
|
|||
|
|
resetBtn.addEventListener('click', reset);
|
|||
|
|
container.addEventListener('pointerdown', function (event) {
|
|||
|
|
if (state.scale <= 1 || event.button !== 0 || event.target.closest('.diagram-nav, .focus-chip, .node-finder, .diagram-guide, .overview-map, .route-probe, .semantic-lens') || event.target.closest('[data-node-id]') || event.target.closest('[data-relationship-hit-key]')) return;
|
|||
|
|
interruptCamera();
|
|||
|
|
drag = { startX: event.clientX, startY: event.clientY, x: state.x, y: state.y, moved: false };
|
|||
|
|
container.classList.add('is-panning');
|
|||
|
|
try { container.setPointerCapture(event.pointerId); } catch (_) {}
|
|||
|
|
});
|
|||
|
|
container.addEventListener('pointermove', function (event) {
|
|||
|
|
if (!drag) return;
|
|||
|
|
var dx = event.clientX - drag.startX;
|
|||
|
|
var dy = event.clientY - drag.startY;
|
|||
|
|
if (Math.abs(dx) + Math.abs(dy) > 3) drag.moved = true;
|
|||
|
|
state.x = drag.x + dx;
|
|||
|
|
state.y = drag.y + dy;
|
|||
|
|
apply();
|
|||
|
|
});
|
|||
|
|
container.addEventListener('pointerup', onPointerEnd);
|
|||
|
|
container.addEventListener('pointercancel', onPointerEnd);
|
|||
|
|
container.addEventListener('scroll', onScroll, { passive: true });
|
|||
|
|
window.addEventListener('resize', function () {
|
|||
|
|
if (resizeFrame) cancelAnimationFrame(resizeFrame);
|
|||
|
|
resizeFrame = requestAnimationFrame(function () {
|
|||
|
|
resizeFrame = 0;
|
|||
|
|
if (state.mode === 'semantic') syncSemantic();
|
|||
|
|
else apply();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
window.addEventListener('hashchange', function () { requestAnimationFrame(syncSemantic); });
|
|||
|
|
apply();
|
|||
|
|
pinControls();
|
|||
|
|
requestAnimationFrame(syncSemantic);
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
zoomIn: function () { zoom(state.scale + 0.25); },
|
|||
|
|
zoomOut: function () { zoom(state.scale - 0.25); },
|
|||
|
|
reset: reset,
|
|||
|
|
reveal: reveal,
|
|||
|
|
centerAt: centerAt,
|
|||
|
|
logicalViewport: logicalViewport,
|
|||
|
|
sync: syncSemantic,
|
|||
|
|
state: function () { return { scale: state.scale, x: state.x, y: state.y, mode: state.mode }; }
|
|||
|
|
};
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* ============================================================
|
|||
|
|
Semantic Radar — simplified semantic bounds + live viewport.
|
|||
|
|
The radar is built at runtime so the checked artifact still contains
|
|||
|
|
one canonical SVG block, and export serialization remains untouched.
|
|||
|
|
============================================================ */
|
|||
|
|
Archify.radar = (function () {
|
|||
|
|
var container = document.querySelector('.diagram-container');
|
|||
|
|
var diagram = container.querySelector(':scope > svg');
|
|||
|
|
var panel = document.getElementById('overview-map');
|
|||
|
|
var panelHead = panel.querySelector('.overview-map-head');
|
|||
|
|
var surface = document.getElementById('overview-map-surface');
|
|||
|
|
var status = document.getElementById('overview-map-status');
|
|||
|
|
var trigger = document.getElementById('btn-overview-map');
|
|||
|
|
var closeBtn = document.getElementById('overview-map-close');
|
|||
|
|
var expandBtn = document.getElementById('overview-map-expand');
|
|||
|
|
var feedback = document.getElementById('overview-map-feedback');
|
|||
|
|
var navigation = container.querySelector('.diagram-nav');
|
|||
|
|
var passport = document.getElementById('focus-chip');
|
|||
|
|
var namespace = 'http://www.w3.org/2000/svg';
|
|||
|
|
var viewBox = diagram.viewBox && diagram.viewBox.baseVal;
|
|||
|
|
var mapSvg = document.createElementNS(namespace, 'svg');
|
|||
|
|
var nodeLayer = document.createElementNS(namespace, 'g');
|
|||
|
|
var viewport = document.createElementNS(namespace, 'rect');
|
|||
|
|
var nodes = [];
|
|||
|
|
var viewportDrag = null;
|
|||
|
|
var panelDrag = null;
|
|||
|
|
var manualPosition = null;
|
|||
|
|
var lastPlacement = null;
|
|||
|
|
var requestedOpen = false;
|
|||
|
|
var passportYielded = false;
|
|||
|
|
var passportPreviousAriaHidden = null;
|
|||
|
|
var syncFrame = 0;
|
|||
|
|
var spaceRetryTimer = 0;
|
|||
|
|
var spaceRetryCount = 0;
|
|||
|
|
var placementGap = 16;
|
|||
|
|
|
|||
|
|
mapSvg.setAttribute('role', 'group');
|
|||
|
|
mapSvg.setAttribute('aria-label', viewerText('viewer.radar.nodes'));
|
|||
|
|
mapSvg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
|||
|
|
nodeLayer.setAttribute('class', 'overview-map-nodes');
|
|||
|
|
viewport.setAttribute('class', 'overview-map-viewport');
|
|||
|
|
viewport.setAttribute('rx', '3');
|
|||
|
|
viewport.setAttribute('ry', '3');
|
|||
|
|
mapSvg.appendChild(nodeLayer);
|
|||
|
|
mapSvg.appendChild(viewport);
|
|||
|
|
surface.appendChild(mapSvg);
|
|||
|
|
|
|||
|
|
function nodeLabel(node, fallback) {
|
|||
|
|
return node.getAttribute('data-node-label') ||
|
|||
|
|
(node.getAttribute('aria-label') || fallback).replace(/^Focus\s+/, '').split(',')[0];
|
|||
|
|
}
|
|||
|
|
function build() {
|
|||
|
|
if (!viewBox || viewBox.width <= 0 || viewBox.height <= 0) return false;
|
|||
|
|
mapSvg.setAttribute('viewBox', [viewBox.x, viewBox.y, viewBox.width, viewBox.height].join(' '));
|
|||
|
|
nodeLayer.textContent = '';
|
|||
|
|
nodes = [];
|
|||
|
|
Array.prototype.forEach.call(diagram.querySelectorAll('[data-node-id]'), function (node) {
|
|||
|
|
var box;
|
|||
|
|
try { box = node.getBBox(); } catch (_) { box = null; }
|
|||
|
|
if (!box || box.width <= 0 || box.height <= 0) return;
|
|||
|
|
var id = node.getAttribute('data-node-id');
|
|||
|
|
var rect = document.createElementNS(namespace, 'rect');
|
|||
|
|
rect.setAttribute('class', 'overview-map-node');
|
|||
|
|
rect.setAttribute('x', String(box.x));
|
|||
|
|
rect.setAttribute('y', String(box.y));
|
|||
|
|
rect.setAttribute('width', String(Math.max(3, box.width)));
|
|||
|
|
rect.setAttribute('height', String(Math.max(3, box.height)));
|
|||
|
|
rect.setAttribute('rx', String(Math.max(2, Math.min(8, Math.min(box.width, box.height) * 0.1))));
|
|||
|
|
rect.setAttribute('data-radar-node-id', id);
|
|||
|
|
rect.setAttribute('data-kind', node.getAttribute('data-node-kind') || 'neutral');
|
|||
|
|
rect.setAttribute('tabindex', '0');
|
|||
|
|
rect.setAttribute('role', 'button');
|
|||
|
|
rect.setAttribute('aria-label', viewerText('viewer.radar.focus', { label: nodeLabel(node, id) }));
|
|||
|
|
nodeLayer.appendChild(rect);
|
|||
|
|
nodes.push({ id: id, node: node, rect: rect });
|
|||
|
|
});
|
|||
|
|
status.textContent = viewerText('viewer.radar.fullMap', { count: nodes.length });
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function visibleRect(element) {
|
|||
|
|
if (!element || element.hidden) return null;
|
|||
|
|
var rect = element.getBoundingClientRect();
|
|||
|
|
if (rect.width <= 0 || rect.height <= 0 || rect.right <= 0 || rect.bottom <= 0 || rect.left >= window.innerWidth || rect.top >= window.innerHeight) return null;
|
|||
|
|
return { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, width: rect.width, height: rect.height };
|
|||
|
|
}
|
|||
|
|
function panelRectAt(position) {
|
|||
|
|
return {
|
|||
|
|
left: position.left,
|
|||
|
|
top: position.top,
|
|||
|
|
right: position.left + panel.offsetWidth,
|
|||
|
|
bottom: position.top + panel.offsetHeight,
|
|||
|
|
width: panel.offsetWidth,
|
|||
|
|
height: panel.offsetHeight
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
function rectsIntersect(first, second, gap) {
|
|||
|
|
gap = Number(gap) || 0;
|
|||
|
|
return first.left < second.right + gap &&
|
|||
|
|
first.right > second.left - gap &&
|
|||
|
|
first.top < second.bottom + gap &&
|
|||
|
|
first.bottom > second.top - gap;
|
|||
|
|
}
|
|||
|
|
function intersectionArea(first, second) {
|
|||
|
|
var width = Math.max(0, Math.min(first.right, second.right) - Math.max(first.left, second.left));
|
|||
|
|
var height = Math.max(0, Math.min(first.bottom, second.bottom) - Math.max(first.top, second.top));
|
|||
|
|
return width * height;
|
|||
|
|
}
|
|||
|
|
function clamp(value, minimum, maximum) {
|
|||
|
|
if (maximum < minimum) return minimum;
|
|||
|
|
return Math.max(minimum, Math.min(maximum, value));
|
|||
|
|
}
|
|||
|
|
function placementContext() {
|
|||
|
|
var containerRect = visibleRect(container);
|
|||
|
|
if (!containerRect) return null;
|
|||
|
|
var controlRect = visibleRect(navigation);
|
|||
|
|
var left = Math.max(placementGap, containerRect.left + placementGap);
|
|||
|
|
var top = Math.max(placementGap, containerRect.top + placementGap);
|
|||
|
|
var right = Math.min(window.innerWidth - placementGap, containerRect.right - placementGap);
|
|||
|
|
var bottom = Math.min(window.innerHeight - placementGap, containerRect.bottom - placementGap);
|
|||
|
|
if (controlRect) bottom = Math.min(bottom, controlRect.top - placementGap);
|
|||
|
|
var lens = document.getElementById('focus-chip');
|
|||
|
|
var lensRect = visibleRect(lens);
|
|||
|
|
var legendRect = visibleRect(diagram.querySelector('[data-legend]'));
|
|||
|
|
var active = diagram.querySelector('[data-focus-selected]');
|
|||
|
|
var activeRect = visibleRect(active);
|
|||
|
|
return {
|
|||
|
|
bounds: { left: left, top: top, right: right, bottom: bottom },
|
|||
|
|
hardBlockers: [lensRect, controlRect, legendRect].filter(Boolean),
|
|||
|
|
softBlockers: [activeRect].filter(Boolean),
|
|||
|
|
preferredSide: !activeRect || activeRect.left + activeRect.width / 2 > window.innerWidth / 2 ? 'left' : 'right'
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
function positionIsValid(position, context) {
|
|||
|
|
if (!position || !context) return false;
|
|||
|
|
var rect = panelRectAt(position);
|
|||
|
|
var bounds = context.bounds;
|
|||
|
|
if (rect.left < bounds.left || rect.top < bounds.top || rect.right > bounds.right || rect.bottom > bounds.bottom) return false;
|
|||
|
|
return context.hardBlockers.every(function (blocker) { return !rectsIntersect(rect, blocker, placementGap); });
|
|||
|
|
}
|
|||
|
|
function cornerCandidates(context) {
|
|||
|
|
var bounds = context.bounds;
|
|||
|
|
var left = bounds.left;
|
|||
|
|
var right = Math.max(left, bounds.right - panel.offsetWidth);
|
|||
|
|
var top = bounds.top;
|
|||
|
|
var bottom = Math.max(top, bounds.bottom - panel.offsetHeight);
|
|||
|
|
var preferredLeft = context.preferredSide === 'left' ? left : right;
|
|||
|
|
var alternateLeft = context.preferredSide === 'left' ? right : left;
|
|||
|
|
return [
|
|||
|
|
{ left: preferredLeft, top: bottom },
|
|||
|
|
{ left: alternateLeft, top: bottom },
|
|||
|
|
{ left: preferredLeft, top: top },
|
|||
|
|
{ left: alternateLeft, top: top }
|
|||
|
|
].filter(function (candidate, index, all) {
|
|||
|
|
return all.findIndex(function (other) { return other.left === candidate.left && other.top === candidate.top; }) === index;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function nearbyCandidates(context, reference) {
|
|||
|
|
var bounds = context.bounds;
|
|||
|
|
var maximumLeft = bounds.right - panel.offsetWidth;
|
|||
|
|
var maximumTop = bounds.bottom - panel.offsetHeight;
|
|||
|
|
var requested = reference ? {
|
|||
|
|
left: clamp(reference.left, bounds.left, maximumLeft),
|
|||
|
|
top: clamp(reference.top, bounds.top, maximumTop)
|
|||
|
|
} : null;
|
|||
|
|
var horizontal = [bounds.left, maximumLeft];
|
|||
|
|
var vertical = [bounds.top, maximumTop];
|
|||
|
|
if (requested) {
|
|||
|
|
horizontal.push(requested.left);
|
|||
|
|
vertical.push(requested.top);
|
|||
|
|
}
|
|||
|
|
context.hardBlockers.forEach(function (blocker) {
|
|||
|
|
horizontal.push(
|
|||
|
|
blocker.left - placementGap - panel.offsetWidth,
|
|||
|
|
blocker.right + placementGap
|
|||
|
|
);
|
|||
|
|
vertical.push(
|
|||
|
|
blocker.top - placementGap - panel.offsetHeight,
|
|||
|
|
blocker.bottom + placementGap
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
horizontal = horizontal.map(function (left) { return clamp(left, bounds.left, maximumLeft); });
|
|||
|
|
vertical = vertical.map(function (top) { return clamp(top, bounds.top, maximumTop); });
|
|||
|
|
var candidates = [];
|
|||
|
|
horizontal.forEach(function (left) {
|
|||
|
|
vertical.forEach(function (top) { candidates.push({ left: left, top: top }); });
|
|||
|
|
});
|
|||
|
|
return candidates.filter(function (candidate, index, all) {
|
|||
|
|
return all.findIndex(function (other) { return other.left === candidate.left && other.top === candidate.top; }) === index;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function placementScore(position, context, reference, softWeight) {
|
|||
|
|
var rect = panelRectAt(position);
|
|||
|
|
var referenceLeft = reference ? reference.left : (context.preferredSide === 'left' ? context.bounds.left : context.bounds.right - panel.offsetWidth);
|
|||
|
|
var referenceTop = reference ? reference.top : context.bounds.bottom - panel.offsetHeight;
|
|||
|
|
var distance = Math.pow(position.left - referenceLeft, 2) + Math.pow(position.top - referenceTop, 2);
|
|||
|
|
var softOverlap = context.softBlockers.reduce(function (total, blocker) { return total + intersectionArea(rect, blocker); }, 0);
|
|||
|
|
return distance + softOverlap * softWeight;
|
|||
|
|
}
|
|||
|
|
function chooseRadarPlacement(context, reference, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
var softWeight = Number.isFinite(options.softWeight) ? options.softWeight : 100;
|
|||
|
|
var candidates = nearbyCandidates(context, reference).concat(cornerCandidates(context));
|
|||
|
|
var valid = candidates.filter(function (candidate) { return positionIsValid(candidate, context); });
|
|||
|
|
valid.sort(function (first, second) {
|
|||
|
|
return placementScore(first, context, reference, softWeight) - placementScore(second, context, reference, softWeight);
|
|||
|
|
});
|
|||
|
|
return valid.length ? valid[0] : null;
|
|||
|
|
}
|
|||
|
|
function applyPlacement(position, remember) {
|
|||
|
|
var useLeft = position.left + panel.offsetWidth / 2 <= window.innerWidth / 2;
|
|||
|
|
panel.setAttribute('data-docked', 'true');
|
|||
|
|
panel.setAttribute('data-dock-side', useLeft ? 'left' : 'right');
|
|||
|
|
if (useLeft) {
|
|||
|
|
panel.style.setProperty('--archify-radar-left', Math.round(position.left) + 'px');
|
|||
|
|
panel.style.removeProperty('--archify-radar-right');
|
|||
|
|
} else {
|
|||
|
|
panel.style.setProperty('--archify-radar-right', Math.round(window.innerWidth - position.left - panel.offsetWidth) + 'px');
|
|||
|
|
panel.style.removeProperty('--archify-radar-left');
|
|||
|
|
}
|
|||
|
|
panel.style.setProperty('--archify-radar-top', Math.round(position.top) + 'px');
|
|||
|
|
if (remember !== false) lastPlacement = { left: position.left, top: position.top };
|
|||
|
|
}
|
|||
|
|
function resetDockingStyles() {
|
|||
|
|
panel.removeAttribute('data-docked');
|
|||
|
|
panel.removeAttribute('data-dock-side');
|
|||
|
|
panel.removeAttribute('data-panel-dragging');
|
|||
|
|
panel.removeAttribute('data-placement-invalid');
|
|||
|
|
panel.removeAttribute('data-placement-degraded');
|
|||
|
|
panel.removeAttribute('data-placement-unavailable');
|
|||
|
|
panel.removeAttribute('data-compact');
|
|||
|
|
panel.removeAttribute('title');
|
|||
|
|
panel.style.removeProperty('visibility');
|
|||
|
|
panel.style.removeProperty('--archify-radar-right');
|
|||
|
|
panel.style.removeProperty('--archify-radar-left');
|
|||
|
|
panel.style.removeProperty('--archify-radar-top');
|
|||
|
|
}
|
|||
|
|
function updateDocking() {
|
|||
|
|
var options = arguments[0] || {};
|
|||
|
|
if (panel.hidden) return false;
|
|||
|
|
if (panelDrag) return true;
|
|||
|
|
panel.removeAttribute('data-compact');
|
|||
|
|
panel.removeAttribute('data-placement-degraded');
|
|||
|
|
panel.removeAttribute('data-placement-invalid');
|
|||
|
|
panel.removeAttribute('data-placement-unavailable');
|
|||
|
|
panel.removeAttribute('title');
|
|||
|
|
panel.style.removeProperty('visibility');
|
|||
|
|
var context = placementContext();
|
|||
|
|
if (!context) {
|
|||
|
|
resetDockingStyles();
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
var reference = manualPosition || lastPlacement;
|
|||
|
|
var placementOptions = { softWeight: manualPosition ? 0 : 100 };
|
|||
|
|
var placement = manualPosition && positionIsValid(manualPosition, context)
|
|||
|
|
? manualPosition
|
|||
|
|
: chooseRadarPlacement(context, reference, placementOptions);
|
|||
|
|
var compact = false;
|
|||
|
|
if (!placement && options.allowCompact !== false) {
|
|||
|
|
compact = true;
|
|||
|
|
panel.setAttribute('data-compact', 'true');
|
|||
|
|
panel.setAttribute('data-placement-degraded', 'true');
|
|||
|
|
panel.setAttribute('title', viewerText('viewer.radar.compacted'));
|
|||
|
|
context = placementContext();
|
|||
|
|
placement = chooseRadarPlacement(context, reference, placementOptions);
|
|||
|
|
}
|
|||
|
|
if (!placement) {
|
|||
|
|
resetDockingStyles();
|
|||
|
|
panel.setAttribute('data-placement-unavailable', 'true');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
if (manualPosition && !compact) manualPosition = { left: placement.left, top: placement.top };
|
|||
|
|
applyPlacement(placement, true);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function clearSpaceRetry() {
|
|||
|
|
if (spaceRetryTimer) window.clearTimeout(spaceRetryTimer);
|
|||
|
|
spaceRetryTimer = 0;
|
|||
|
|
}
|
|||
|
|
function yieldPassport() {
|
|||
|
|
if (!passport || passport.hidden || passportYielded) return passportYielded;
|
|||
|
|
passportPreviousAriaHidden = passport.getAttribute('aria-hidden');
|
|||
|
|
passportYielded = true;
|
|||
|
|
passport.setAttribute('data-radar-yielded', 'true');
|
|||
|
|
passport.setAttribute('aria-hidden', 'true');
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function restorePassport() {
|
|||
|
|
if (!passport || !passportYielded) return;
|
|||
|
|
passportYielded = false;
|
|||
|
|
passport.removeAttribute('data-radar-yielded');
|
|||
|
|
if (passportPreviousAriaHidden === null) passport.removeAttribute('aria-hidden');
|
|||
|
|
else passport.setAttribute('aria-hidden', passportPreviousAriaHidden);
|
|||
|
|
passportPreviousAriaHidden = null;
|
|||
|
|
}
|
|||
|
|
function reflectVisible() {
|
|||
|
|
panel.hidden = false;
|
|||
|
|
panel.removeAttribute('data-placement-unavailable');
|
|||
|
|
trigger.setAttribute('aria-expanded', 'true');
|
|||
|
|
trigger.removeAttribute('data-radar-space-limited');
|
|||
|
|
trigger.setAttribute('aria-label', viewerText('viewer.radar.close'));
|
|||
|
|
trigger.title = viewerText('viewer.nav.radar.title');
|
|||
|
|
feedback.hidden = true;
|
|||
|
|
}
|
|||
|
|
function scheduleSpaceRetry() {
|
|||
|
|
if (!requestedOpen || spaceRetryTimer || spaceRetryCount >= 4) return;
|
|||
|
|
spaceRetryCount += 1;
|
|||
|
|
spaceRetryTimer = window.setTimeout(function () {
|
|||
|
|
spaceRetryTimer = 0;
|
|||
|
|
attemptRequestedOpen();
|
|||
|
|
}, 60);
|
|||
|
|
}
|
|||
|
|
function reflectUnavailable() {
|
|||
|
|
restorePassport();
|
|||
|
|
panel.hidden = true;
|
|||
|
|
panel.setAttribute('data-placement-unavailable', 'true');
|
|||
|
|
trigger.setAttribute('aria-expanded', 'false');
|
|||
|
|
trigger.setAttribute('aria-label', viewerText('viewer.radar.cancelWaiting'));
|
|||
|
|
trigger.setAttribute('data-radar-space-limited', 'true');
|
|||
|
|
trigger.title = viewerText('viewer.radar.needsSpace');
|
|||
|
|
feedback.hidden = false;
|
|||
|
|
scheduleSpaceRetry();
|
|||
|
|
}
|
|||
|
|
function attemptRequestedOpen(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (!requestedOpen) return false;
|
|||
|
|
panel.hidden = false;
|
|||
|
|
if (!updateDocking(options)) {
|
|||
|
|
reflectUnavailable();
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
clearSpaceRetry();
|
|||
|
|
spaceRetryCount = 0;
|
|||
|
|
reflectVisible();
|
|||
|
|
sync();
|
|||
|
|
if (options.focus === true && !panel.hasAttribute('data-compact')) surface.focus();
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function expandCompactRadar() {
|
|||
|
|
if (!requestedOpen || panel.hidden || !panel.hasAttribute('data-compact')) return false;
|
|||
|
|
yieldPassport();
|
|||
|
|
if (!updateDocking({ allowCompact: false })) {
|
|||
|
|
restorePassport();
|
|||
|
|
if (!updateDocking()) {
|
|||
|
|
reflectUnavailable();
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
reflectVisible();
|
|||
|
|
sync();
|
|||
|
|
if (!panel.hasAttribute('data-compact')) surface.focus();
|
|||
|
|
return !panel.hasAttribute('data-compact');
|
|||
|
|
}
|
|||
|
|
function syncNow() {
|
|||
|
|
syncFrame = 0;
|
|||
|
|
if (panel.hidden || !Archify.view || typeof Archify.view.logicalViewport !== 'function') return;
|
|||
|
|
if (!updateDocking()) {
|
|||
|
|
reflectUnavailable();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (passportYielded && panel.hasAttribute('data-compact')) {
|
|||
|
|
restorePassport();
|
|||
|
|
if (!updateDocking()) {
|
|||
|
|
reflectUnavailable();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
reflectVisible();
|
|||
|
|
var visible = Archify.view.logicalViewport();
|
|||
|
|
if (!visible) return;
|
|||
|
|
viewport.setAttribute('x', String(visible.x));
|
|||
|
|
viewport.setAttribute('y', String(visible.y));
|
|||
|
|
viewport.setAttribute('width', String(visible.width));
|
|||
|
|
viewport.setAttribute('height', String(visible.height));
|
|||
|
|
var full = visible.width >= viewBox.width * 0.98 && visible.height >= viewBox.height * 0.98;
|
|||
|
|
var mobileWide = window.innerWidth <= 720 && container.hasAttribute('data-wide-diagram');
|
|||
|
|
var viewportCopy = full
|
|||
|
|
? viewerText('viewer.radar.viewport.full')
|
|||
|
|
: (mobileWide
|
|||
|
|
? viewerText('viewer.radar.viewport.width', { percent: Math.round(visible.width / viewBox.width * 100) })
|
|||
|
|
: 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';
|
|||
|
|
if (active) item.rect.setAttribute('data-radar-active', 'true');
|
|||
|
|
else item.rect.removeAttribute('data-radar-active');
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function sync() {
|
|||
|
|
if (requestedOpen && panel.hidden) {
|
|||
|
|
attemptRequestedOpen();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (syncFrame) return;
|
|||
|
|
syncFrame = requestAnimationFrame(syncNow);
|
|||
|
|
}
|
|||
|
|
function setOpen(next, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
next = Boolean(next);
|
|||
|
|
if (next && Archify.semanticLens && typeof Archify.semanticLens.clearPreview === 'function') Archify.semanticLens.clearPreview();
|
|||
|
|
if (next && Archify.semanticLens && Archify.semanticLens.isOpen()) {
|
|||
|
|
Archify.semanticLens.close({ restoreFocus: false });
|
|||
|
|
}
|
|||
|
|
requestedOpen = next;
|
|||
|
|
if (next) {
|
|||
|
|
clearSpaceRetry();
|
|||
|
|
spaceRetryCount = 0;
|
|||
|
|
build();
|
|||
|
|
attemptRequestedOpen(options);
|
|||
|
|
} else {
|
|||
|
|
clearSpaceRetry();
|
|||
|
|
spaceRetryCount = 0;
|
|||
|
|
viewportDrag = null;
|
|||
|
|
panelDrag = null;
|
|||
|
|
container.classList.remove('is-panning');
|
|||
|
|
panel.hidden = true;
|
|||
|
|
resetDockingStyles();
|
|||
|
|
restorePassport();
|
|||
|
|
feedback.hidden = true;
|
|||
|
|
trigger.setAttribute('aria-expanded', 'false');
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
function toggle() { return setOpen(!requestedOpen, { focus: false }); }
|
|||
|
|
function close(options) { return setOpen(false, options); }
|
|||
|
|
function bringNodeIntoWindow(node) {
|
|||
|
|
var delay = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 540;
|
|||
|
|
window.setTimeout(function () {
|
|||
|
|
var rect = node.getBoundingClientRect();
|
|||
|
|
var safeTop = 64;
|
|||
|
|
var safeBottom = window.innerHeight - 64;
|
|||
|
|
if (rect.top >= safeTop && rect.bottom <= safeBottom) return;
|
|||
|
|
var top = Math.max(0, window.scrollY + rect.top + rect.height / 2 - window.innerHeight / 2);
|
|||
|
|
try { window.scrollTo({ top: top, behavior: delay ? 'smooth' : 'auto' }); }
|
|||
|
|
catch (_) { window.scrollTo(0, top); }
|
|||
|
|
}, delay);
|
|||
|
|
}
|
|||
|
|
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 });
|
|||
|
|
}
|
|||
|
|
if (Archify.view && typeof Archify.view.reveal === 'function') {
|
|||
|
|
Archify.view.reveal([id], { includeNeighbors: true, reason: 'radar' });
|
|||
|
|
}
|
|||
|
|
bringNodeIntoWindow(main);
|
|||
|
|
try { main.focus({ preventScroll: true }); } catch (_) { try { main.focus(); } catch (_) {} }
|
|||
|
|
sync();
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function diagramPoint(event) {
|
|||
|
|
var matrix = mapSvg.getScreenCTM();
|
|||
|
|
if (!matrix) return null;
|
|||
|
|
var point = mapSvg.createSVGPoint();
|
|||
|
|
point.x = event.clientX;
|
|||
|
|
point.y = event.clientY;
|
|||
|
|
return point.matrixTransform(matrix.inverse());
|
|||
|
|
}
|
|||
|
|
function navigate(event) {
|
|||
|
|
var point = diagramPoint(event);
|
|||
|
|
if (!point || !Archify.view || typeof Archify.view.centerAt !== 'function') return;
|
|||
|
|
Archify.view.centerAt(point.x, point.y, { minimumScale: 1.5, instant: true });
|
|||
|
|
sync();
|
|||
|
|
}
|
|||
|
|
function endViewportDrag(event) {
|
|||
|
|
if (!viewportDrag) return;
|
|||
|
|
viewportDrag = null;
|
|||
|
|
panel.removeAttribute('data-dragging');
|
|||
|
|
container.classList.remove('is-panning');
|
|||
|
|
try { surface.releasePointerCapture(event.pointerId); } catch (_) {}
|
|||
|
|
}
|
|||
|
|
function beginPanelDrag(event) {
|
|||
|
|
if (event.button !== 0 || event.target.closest('button, a, input, [role="button"]')) return;
|
|||
|
|
var context = placementContext();
|
|||
|
|
if (!context) return;
|
|||
|
|
var rect = panel.getBoundingClientRect();
|
|||
|
|
event.preventDefault();
|
|||
|
|
event.stopPropagation();
|
|||
|
|
panelDrag = {
|
|||
|
|
pointerId: event.pointerId,
|
|||
|
|
originX: event.clientX,
|
|||
|
|
originY: event.clientY,
|
|||
|
|
start: { left: rect.left, top: rect.top },
|
|||
|
|
current: { left: rect.left, top: rect.top },
|
|||
|
|
previousManual: manualPosition ? { left: manualPosition.left, top: manualPosition.top } : null
|
|||
|
|
};
|
|||
|
|
panel.setAttribute('data-panel-dragging', 'true');
|
|||
|
|
try { panelHead.setPointerCapture(event.pointerId); } catch (_) {}
|
|||
|
|
}
|
|||
|
|
function movePanelDrag(event) {
|
|||
|
|
if (!panelDrag || panelDrag.pointerId !== event.pointerId) return;
|
|||
|
|
var context = placementContext();
|
|||
|
|
if (!context) return;
|
|||
|
|
event.preventDefault();
|
|||
|
|
event.stopPropagation();
|
|||
|
|
var bounds = context.bounds;
|
|||
|
|
var position = {
|
|||
|
|
left: clamp(panelDrag.start.left + event.clientX - panelDrag.originX, bounds.left, bounds.right - panel.offsetWidth),
|
|||
|
|
top: clamp(panelDrag.start.top + event.clientY - panelDrag.originY, bounds.top, bounds.bottom - panel.offsetHeight)
|
|||
|
|
};
|
|||
|
|
panelDrag.current = position;
|
|||
|
|
if (positionIsValid(position, context)) panel.removeAttribute('data-placement-invalid');
|
|||
|
|
else panel.setAttribute('data-placement-invalid', 'true');
|
|||
|
|
applyPlacement(position, false);
|
|||
|
|
}
|
|||
|
|
function finishPanelDrag(event, cancel) {
|
|||
|
|
if (!panelDrag || panelDrag.pointerId !== event.pointerId) return;
|
|||
|
|
event.preventDefault();
|
|||
|
|
event.stopPropagation();
|
|||
|
|
var activeDrag = panelDrag;
|
|||
|
|
panelDrag = null;
|
|||
|
|
panel.removeAttribute('data-panel-dragging');
|
|||
|
|
panel.removeAttribute('data-placement-invalid');
|
|||
|
|
try { panelHead.releasePointerCapture(event.pointerId); } catch (_) {}
|
|||
|
|
var context = placementContext();
|
|||
|
|
if (!context) return;
|
|||
|
|
if (cancel) {
|
|||
|
|
manualPosition = activeDrag.previousManual;
|
|||
|
|
lastPlacement = { left: activeDrag.start.left, top: activeDrag.start.top };
|
|||
|
|
updateDocking();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
var requested = activeDrag.current;
|
|||
|
|
manualPosition = { left: requested.left, top: requested.top };
|
|||
|
|
updateDocking();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
trigger.addEventListener('click', toggle);
|
|||
|
|
closeBtn.addEventListener('click', function () { close({ restoreFocus: true }); });
|
|||
|
|
expandBtn.addEventListener('click', expandCompactRadar);
|
|||
|
|
panelHead.addEventListener('pointerdown', beginPanelDrag);
|
|||
|
|
panelHead.addEventListener('pointermove', movePanelDrag);
|
|||
|
|
panelHead.addEventListener('pointerup', function (event) { finishPanelDrag(event, false); });
|
|||
|
|
panelHead.addEventListener('pointercancel', function (event) { finishPanelDrag(event, true); });
|
|||
|
|
surface.addEventListener('pointerdown', function (event) {
|
|||
|
|
if (event.button !== 0 || event.target.closest('[data-radar-node-id]')) return;
|
|||
|
|
event.preventDefault();
|
|||
|
|
viewportDrag = { pointerId: event.pointerId };
|
|||
|
|
panel.setAttribute('data-dragging', 'true');
|
|||
|
|
container.classList.add('is-panning');
|
|||
|
|
try { surface.setPointerCapture(event.pointerId); } catch (_) {}
|
|||
|
|
navigate(event);
|
|||
|
|
});
|
|||
|
|
surface.addEventListener('pointermove', function (event) {
|
|||
|
|
if (!viewportDrag || viewportDrag.pointerId !== event.pointerId) return;
|
|||
|
|
navigate(event);
|
|||
|
|
});
|
|||
|
|
surface.addEventListener('pointerup', endViewportDrag);
|
|||
|
|
surface.addEventListener('pointercancel', endViewportDrag);
|
|||
|
|
surface.addEventListener('click', function (event) {
|
|||
|
|
var node = event.target.closest('[data-radar-node-id]');
|
|||
|
|
if (node) focusNode(node.getAttribute('data-radar-node-id'));
|
|||
|
|
});
|
|||
|
|
surface.addEventListener('keydown', function (event) {
|
|||
|
|
if (event.key === 'Escape') {
|
|||
|
|
event.preventDefault();
|
|||
|
|
event.stopPropagation();
|
|||
|
|
close({ restoreFocus: true });
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
var node = event.target.closest('[data-radar-node-id]');
|
|||
|
|
if (node && (event.key === 'Enter' || event.key === ' ')) {
|
|||
|
|
event.preventDefault();
|
|||
|
|
focusNode(node.getAttribute('data-radar-node-id'));
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight' && event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;
|
|||
|
|
var visible = Archify.view && Archify.view.logicalViewport ? Archify.view.logicalViewport() : null;
|
|||
|
|
if (!visible) return;
|
|||
|
|
event.preventDefault();
|
|||
|
|
var x = visible.x + visible.width / 2;
|
|||
|
|
var y = visible.y + visible.height / 2;
|
|||
|
|
var stepX = Math.max(12, visible.width * 0.24);
|
|||
|
|
var stepY = Math.max(12, visible.height * 0.24);
|
|||
|
|
if (event.key === 'ArrowLeft') x -= stepX;
|
|||
|
|
else if (event.key === 'ArrowRight') x += stepX;
|
|||
|
|
else if (event.key === 'ArrowUp') y -= stepY;
|
|||
|
|
else y += stepY;
|
|||
|
|
Archify.view.centerAt(x, y, { minimumScale: 1.5, instant: true });
|
|||
|
|
sync();
|
|||
|
|
});
|
|||
|
|
document.addEventListener('keydown', function (event) {
|
|||
|
|
if (!panelDrag || event.key !== 'Escape') return;
|
|||
|
|
finishPanelDrag({
|
|||
|
|
pointerId: panelDrag.pointerId,
|
|||
|
|
preventDefault: function () { event.preventDefault(); },
|
|||
|
|
stopPropagation: function () { event.stopPropagation(); }
|
|||
|
|
}, true);
|
|||
|
|
}, true);
|
|||
|
|
function reflow() {
|
|||
|
|
if (!requestedOpen) return;
|
|||
|
|
clearSpaceRetry();
|
|||
|
|
spaceRetryCount = 0;
|
|||
|
|
sync();
|
|||
|
|
}
|
|||
|
|
window.addEventListener('resize', reflow);
|
|||
|
|
window.addEventListener('scroll', reflow, { passive: true });
|
|||
|
|
if (typeof ResizeObserver === 'function') {
|
|||
|
|
var radarResizeObserver = new ResizeObserver(function (entries) {
|
|||
|
|
if (!requestedOpen) return;
|
|||
|
|
if (panel.hidden && entries.every(function (entry) { return entry.target === panel; })) return;
|
|||
|
|
reflow();
|
|||
|
|
});
|
|||
|
|
[container, navigation, document.getElementById('focus-chip'), panel].filter(Boolean).forEach(function (element) {
|
|||
|
|
radarResizeObserver.observe(element);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
requestAnimationFrame(build);
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
open: function () { return setOpen(true); },
|
|||
|
|
close: close,
|
|||
|
|
toggle: toggle,
|
|||
|
|
sync: sync,
|
|||
|
|
focus: focusNode,
|
|||
|
|
isOpen: function () { return requestedOpen; },
|
|||
|
|
count: function () { return nodes.length; }
|
|||
|
|
};
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* ============================================================
|
|||
|
|
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.
|
|||
|
|
============================================================ */
|
|||
|
|
Archify.presentation = (function () {
|
|||
|
|
var html = document.documentElement;
|
|||
|
|
var btn = document.getElementById('btn-present');
|
|||
|
|
var label = document.getElementById('present-label');
|
|||
|
|
var previousScrollY = 0;
|
|||
|
|
|
|||
|
|
function active() { return html.getAttribute('data-present') === 'true'; }
|
|||
|
|
|
|||
|
|
function updateUrl(next) {
|
|||
|
|
try {
|
|||
|
|
var url = new URL(window.location.href);
|
|||
|
|
if (next) url.searchParams.set('present', '1');
|
|||
|
|
else url.searchParams.delete('present');
|
|||
|
|
history.replaceState(null, '', url.pathname + url.search + url.hash);
|
|||
|
|
} catch (_) {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function render(next) {
|
|||
|
|
btn.setAttribute('aria-pressed', next ? 'true' : 'false');
|
|||
|
|
btn.setAttribute('aria-label', viewerText(next ? 'viewer.present.exit' : 'viewer.present.enter'));
|
|||
|
|
btn.title = viewerText(next ? 'viewer.present.exit.title' : 'viewer.present.enter.title');
|
|||
|
|
label.textContent = viewerText(next ? 'viewer.present.exit.label' : 'viewer.present.present');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function setActive(next, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
next = Boolean(next);
|
|||
|
|
if (next === active()) {
|
|||
|
|
render(next);
|
|||
|
|
return next;
|
|||
|
|
}
|
|||
|
|
if (next) {
|
|||
|
|
if (Archify.semanticLens && typeof Archify.semanticLens.clearPreview === 'function') Archify.semanticLens.clearPreview();
|
|||
|
|
previousScrollY = window.scrollY || 0;
|
|||
|
|
html.setAttribute('data-present', 'true');
|
|||
|
|
try { window.scrollTo(0, 0); } catch (_) {}
|
|||
|
|
} else {
|
|||
|
|
html.removeAttribute('data-present');
|
|||
|
|
}
|
|||
|
|
render(next);
|
|||
|
|
if (options.updateUrl !== false) updateUrl(next);
|
|||
|
|
requestAnimationFrame(function () {
|
|||
|
|
if (Archify.view && typeof Archify.view.reset === 'function') {
|
|||
|
|
Archify.view.reset({ automatic: true });
|
|||
|
|
requestAnimationFrame(function () {
|
|||
|
|
if (Archify.view && typeof Archify.view.sync === 'function') Archify.view.sync();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
if (!next && previousScrollY) {
|
|||
|
|
try { window.scrollTo(0, previousScrollY); } catch (_) {}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
return next;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function toggle() { return setActive(!active()); }
|
|||
|
|
|
|||
|
|
render(active());
|
|||
|
|
btn.addEventListener('click', toggle);
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
enter: function () { return setActive(true); },
|
|||
|
|
exit: function () { return setActive(false); },
|
|||
|
|
toggle: toggle,
|
|||
|
|
active: active
|
|||
|
|
};
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* ============================================================
|
|||
|
|
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.
|
|||
|
|
============================================================ */
|
|||
|
|
Archify.finder = (function () {
|
|||
|
|
var html = document.documentElement;
|
|||
|
|
var container = document.querySelector('.diagram-container');
|
|||
|
|
var svg = container.querySelector('svg');
|
|||
|
|
var trigger = document.getElementById('btn-node-finder');
|
|||
|
|
var panel = document.getElementById('node-finder');
|
|||
|
|
var heading = document.getElementById('node-finder-title');
|
|||
|
|
var closeBtn = document.getElementById('node-finder-close');
|
|||
|
|
var input = document.getElementById('node-finder-input');
|
|||
|
|
var results = document.getElementById('node-finder-results');
|
|||
|
|
var empty = document.getElementById('node-finder-empty');
|
|||
|
|
var status = document.getElementById('node-finder-status');
|
|||
|
|
var visibleItems = [];
|
|||
|
|
|
|||
|
|
function defaultContext() {
|
|||
|
|
return {
|
|||
|
|
kind: 'focus',
|
|||
|
|
title: viewerText('viewer.finder.title'),
|
|||
|
|
placeholder: viewerText('viewer.finder.placeholder'),
|
|||
|
|
empty: viewerText('viewer.finder.empty'),
|
|||
|
|
resultsLabel: viewerText('viewer.finder.results'),
|
|||
|
|
availableNoun: viewerText('viewer.finder.noun.nodes'),
|
|||
|
|
allowedIds: null,
|
|||
|
|
badges: null
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var context = defaultContext();
|
|||
|
|
|
|||
|
|
function semanticType(node) {
|
|||
|
|
var authored = node.getAttribute('data-node-kind');
|
|||
|
|
if (authored) return authored;
|
|||
|
|
var types = ['frontend', 'backend', 'database', 'cloud', 'security', 'messagebus', 'external'];
|
|||
|
|
return types.find(function (type) { return node.querySelector('.c-' + type); }) || 'node';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function connectionsFor(id) {
|
|||
|
|
var count = 0;
|
|||
|
|
var seen = {};
|
|||
|
|
Array.prototype.forEach.call(svg.querySelectorAll('[data-edge-from][data-edge-to]'), function (edge) {
|
|||
|
|
var from = edge.getAttribute('data-edge-from');
|
|||
|
|
var to = edge.getAttribute('data-edge-to');
|
|||
|
|
var key = from + '\u0000' + to;
|
|||
|
|
if ((from === id || to === id) && !seen[key]) {
|
|||
|
|
seen[key] = true;
|
|||
|
|
count += 1;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
return count;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var items = Array.prototype.map.call(svg.querySelectorAll('[data-node-id]'), function (node) {
|
|||
|
|
var id = node.getAttribute('data-node-id');
|
|||
|
|
var label = node.getAttribute('data-node-label') || (node.getAttribute('aria-label') || id).replace(/^Focus\s+/, '');
|
|||
|
|
var text = (node.textContent || '').replace(/\s+/g, ' ').trim();
|
|||
|
|
var sublabel = node.getAttribute('data-node-sublabel') || '';
|
|||
|
|
var context = node.getAttribute('data-node-context') || '';
|
|||
|
|
var tag = node.getAttribute('data-node-tag') || '';
|
|||
|
|
var brand = node.getAttribute('data-node-brand') || '';
|
|||
|
|
var type = semanticType(node);
|
|||
|
|
var sources = Archify.sourceEvidence.node(id);
|
|||
|
|
var sourceSearch = sources.map(function (source) {
|
|||
|
|
return [source.path, source.label, source.line, source.endLine].filter(Boolean).join(' ');
|
|||
|
|
}).join(' ');
|
|||
|
|
return {
|
|||
|
|
id: id,
|
|||
|
|
label: label,
|
|||
|
|
type: type,
|
|||
|
|
sublabel: sublabel,
|
|||
|
|
context: context,
|
|||
|
|
tag: tag,
|
|||
|
|
brand: brand,
|
|||
|
|
sources: sources,
|
|||
|
|
links: connectionsFor(id),
|
|||
|
|
search: (id + ' ' + label + ' ' + type + ' ' + sublabel + ' ' + context + ' ' + tag + ' ' + sourceSearch + ' ' + text).toLowerCase() + ' ' + brand.toLowerCase(),
|
|||
|
|
node: node
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function resultButtons() {
|
|||
|
|
return Array.prototype.slice.call(results.querySelectorAll('.node-finder-result'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function resolveContext(options) {
|
|||
|
|
var requested = options && options.context ? options.context : null;
|
|||
|
|
if (!requested && Archify.routeProbe && typeof Archify.routeProbe.finderContext === 'function') {
|
|||
|
|
requested = Archify.routeProbe.finderContext();
|
|||
|
|
}
|
|||
|
|
if (!requested) return defaultContext();
|
|||
|
|
var resolved = defaultContext();
|
|||
|
|
Object.keys(requested).forEach(function (key) { resolved[key] = requested[key]; });
|
|||
|
|
return resolved;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function availableItems() {
|
|||
|
|
if (!context.allowedIds) return items.slice();
|
|||
|
|
return items.filter(function (item) { return context.allowedIds.indexOf(item.id) !== -1; });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function applyContext() {
|
|||
|
|
panel.setAttribute('data-context', context.kind);
|
|||
|
|
heading.textContent = context.title;
|
|||
|
|
input.placeholder = context.placeholder;
|
|||
|
|
empty.textContent = context.empty;
|
|||
|
|
results.setAttribute('aria-label', context.resultsLabel);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function select(id) {
|
|||
|
|
var item = items.find(function (candidate) { return candidate.id === id; });
|
|||
|
|
if (!item) return false;
|
|||
|
|
var routeSelection = context.kind === 'route-source' || context.kind === 'route-target';
|
|||
|
|
if (routeSelection) {
|
|||
|
|
if (!Archify.routeProbe || typeof Archify.routeProbe.choose !== 'function' || !Archify.routeProbe.choose(id)) return false;
|
|||
|
|
if (Archify.routeProbe.active() === 'target' && Archify.view && typeof Archify.view.reveal === 'function') {
|
|||
|
|
Archify.view.reveal([id], { includeNeighbors: true, reason: 'route-pick' });
|
|||
|
|
}
|
|||
|
|
close({ restoreFocus: false });
|
|||
|
|
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') {
|
|||
|
|
Archify.view.reveal([id], { includeNeighbors: true, reason: 'finder' });
|
|||
|
|
}
|
|||
|
|
close({ restoreFocus: false });
|
|||
|
|
try { item.node.focus({ preventScroll: true }); } catch (_) { try { item.node.focus(); } catch (_) {} }
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function render(query) {
|
|||
|
|
query = (query || '').trim().toLowerCase();
|
|||
|
|
var available = availableItems();
|
|||
|
|
visibleItems = available.filter(function (item) { return !query || item.search.indexOf(query) !== -1; });
|
|||
|
|
results.textContent = '';
|
|||
|
|
visibleItems.forEach(function (item) {
|
|||
|
|
var button = document.createElement('button');
|
|||
|
|
button.type = 'button';
|
|||
|
|
button.className = 'node-finder-result';
|
|||
|
|
button.setAttribute('data-node-id', item.id);
|
|||
|
|
var badge = context.badges && context.badges[item.id]
|
|||
|
|
? context.badges[item.id]
|
|||
|
|
: context.kind === 'focus'
|
|||
|
|
? viewerCount('viewer.finder.link', item.links)
|
|||
|
|
: String(item.links);
|
|||
|
|
var action = context.kind === 'route-source'
|
|||
|
|
? viewerText('viewer.finder.result.routeStart', { label: item.label })
|
|||
|
|
: context.kind === 'route-target'
|
|||
|
|
? viewerText('viewer.finder.result.routeTarget', { label: item.label, links: badge })
|
|||
|
|
: viewerCount('viewer.finder.result.focus', item.links, { label: item.label });
|
|||
|
|
button.setAttribute('aria-label', action);
|
|||
|
|
|
|||
|
|
var name = document.createElement('strong');
|
|||
|
|
name.textContent = item.label;
|
|||
|
|
var links = document.createElement('em');
|
|||
|
|
links.textContent = badge;
|
|||
|
|
links.title = context.kind === 'focus' ? badge : action;
|
|||
|
|
var meta = document.createElement('small');
|
|||
|
|
meta.textContent = [viewerKindLabel(item.type), item.id, item.sublabel, item.tag].filter(Boolean).join(' \u00b7 ');
|
|||
|
|
meta.title = [viewerKindLabel(item.type), item.id, item.context, item.sublabel, item.tag].filter(Boolean).join(' \u00b7 ');
|
|||
|
|
button.appendChild(name);
|
|||
|
|
button.appendChild(links);
|
|||
|
|
button.appendChild(meta);
|
|||
|
|
button.addEventListener('click', function () { select(item.id); });
|
|||
|
|
results.appendChild(button);
|
|||
|
|
});
|
|||
|
|
empty.hidden = visibleItems.length !== 0;
|
|||
|
|
status.textContent = query
|
|||
|
|
? viewerText('viewer.finder.status.filtered', {
|
|||
|
|
visible: visibleItems.length,
|
|||
|
|
available: available.length,
|
|||
|
|
noun: context.availableNoun
|
|||
|
|
})
|
|||
|
|
: viewerText('viewer.finder.status.all', { available: available.length, noun: context.availableNoun });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function open(options) {
|
|||
|
|
if (html.getAttribute('data-embed') === 'true') return false;
|
|||
|
|
if (Archify.semanticLens && typeof Archify.semanticLens.clearPreview === 'function') Archify.semanticLens.clearPreview();
|
|||
|
|
if (Archify.exportMenu && Archify.exportMenu.isOpen()) Archify.exportMenu.close(false);
|
|||
|
|
if (Archify.semanticLens && Archify.semanticLens.isOpen()) Archify.semanticLens.close({ restoreFocus: false });
|
|||
|
|
context = resolveContext(options || {});
|
|||
|
|
applyContext();
|
|||
|
|
panel.hidden = false;
|
|||
|
|
trigger.setAttribute('aria-expanded', 'true');
|
|||
|
|
if (context.kind.indexOf('route-') === 0 && Archify.routeProbe && typeof Archify.routeProbe.finderOpening === 'function') {
|
|||
|
|
Archify.routeProbe.finderOpening();
|
|||
|
|
}
|
|||
|
|
input.value = '';
|
|||
|
|
render('');
|
|||
|
|
requestAnimationFrame(function () { input.focus(); });
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function close(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
var routeContext = context.kind.indexOf('route-') === 0;
|
|||
|
|
panel.hidden = true;
|
|||
|
|
trigger.setAttribute('aria-expanded', 'false');
|
|||
|
|
input.value = '';
|
|||
|
|
if (routeContext && Archify.routeProbe && typeof Archify.routeProbe.finderClosed === 'function') {
|
|||
|
|
Archify.routeProbe.finderClosed({ restoreFocus: options.restoreFocus !== false });
|
|||
|
|
} else if (options.restoreFocus !== false) {
|
|||
|
|
trigger.focus();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function toggle() { return panel.hidden ? open() : (close(), false); }
|
|||
|
|
|
|||
|
|
trigger.addEventListener('click', toggle);
|
|||
|
|
closeBtn.addEventListener('click', function () { close(); });
|
|||
|
|
input.addEventListener('input', function () { render(input.value); });
|
|||
|
|
input.addEventListener('keydown', function (event) {
|
|||
|
|
var buttons = resultButtons();
|
|||
|
|
if (event.key === 'ArrowDown' && buttons.length) {
|
|||
|
|
event.preventDefault();
|
|||
|
|
buttons[0].focus();
|
|||
|
|
} else if (event.key === 'Enter' && visibleItems.length) {
|
|||
|
|
event.preventDefault();
|
|||
|
|
select(visibleItems[0].id);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
results.addEventListener('keydown', function (event) {
|
|||
|
|
var buttons = resultButtons();
|
|||
|
|
var index = buttons.indexOf(document.activeElement);
|
|||
|
|
if (index < 0 || !buttons.length) return;
|
|||
|
|
var next = null;
|
|||
|
|
if (event.key === 'ArrowDown') next = (index + 1) % buttons.length;
|
|||
|
|
else if (event.key === 'ArrowUp') next = (index - 1 + buttons.length) % buttons.length;
|
|||
|
|
else if (event.key === 'Home') next = 0;
|
|||
|
|
else if (event.key === 'End') next = buttons.length - 1;
|
|||
|
|
if (next !== null) {
|
|||
|
|
event.preventDefault();
|
|||
|
|
buttons[next].focus();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
panel.addEventListener('keydown', function (event) {
|
|||
|
|
if (event.key !== 'Escape') return;
|
|||
|
|
event.preventDefault();
|
|||
|
|
event.stopPropagation();
|
|||
|
|
close();
|
|||
|
|
});
|
|||
|
|
document.addEventListener('click', function (event) {
|
|||
|
|
if (!panel.hidden && !panel.contains(event.target) && !event.target.closest('[data-node-finder-trigger]')) close({ restoreFocus: false });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
render('');
|
|||
|
|
return {
|
|||
|
|
open: open,
|
|||
|
|
close: close,
|
|||
|
|
toggle: toggle,
|
|||
|
|
select: select,
|
|||
|
|
isOpen: function () { return !panel.hidden; },
|
|||
|
|
context: function () { return context.kind; },
|
|||
|
|
count: items.length
|
|||
|
|
};
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* ============================================================
|
|||
|
|
Route Probe — shortest directed path over compiled semantics.
|
|||
|
|
The graph is read from renderer-owned stable IDs and relationship
|
|||
|
|
endpoints. BFS order follows authored DOM edge order, so equal-length
|
|||
|
|
choices are deterministic without adding weights or a graph runtime.
|
|||
|
|
============================================================ */
|
|||
|
|
Archify.routeProbe = (function () {
|
|||
|
|
var html = document.documentElement;
|
|||
|
|
var container = document.querySelector('.diagram-container');
|
|||
|
|
var svg = container.querySelector(':scope > svg');
|
|||
|
|
var trigger = document.getElementById('btn-route-probe');
|
|||
|
|
var panel = document.getElementById('route-probe');
|
|||
|
|
var title = document.getElementById('route-probe-title');
|
|||
|
|
var path = document.getElementById('route-probe-path');
|
|||
|
|
var status = document.getElementById('route-probe-status');
|
|||
|
|
var findBtn = document.getElementById('route-probe-find');
|
|||
|
|
var copyBtn = document.getElementById('route-probe-copy');
|
|||
|
|
var clearBtn = document.getElementById('route-probe-clear');
|
|||
|
|
var journeyControls = document.getElementById('route-journey-controls');
|
|||
|
|
var journeyPrevBtn = document.getElementById('route-journey-prev');
|
|||
|
|
var journeyPlayBtn = document.getElementById('route-journey-play');
|
|||
|
|
var journeyPlayIcon = document.getElementById('route-journey-play-icon');
|
|||
|
|
var journeyPlayLabel = document.getElementById('route-journey-play-label');
|
|||
|
|
var journeyNextBtn = document.getElementById('route-journey-next');
|
|||
|
|
var journeyOverviewBtn = document.getElementById('route-journey-overview');
|
|||
|
|
var namespace = 'http://www.w3.org/2000/svg';
|
|||
|
|
var mode = 'idle';
|
|||
|
|
var startId = null;
|
|||
|
|
var endId = null;
|
|||
|
|
var activeNodeIds = [];
|
|||
|
|
var activeEdges = [];
|
|||
|
|
var journeyIndex = -1;
|
|||
|
|
var journeyPlaying = false;
|
|||
|
|
var journeyComplete = false;
|
|||
|
|
var journeyGeneration = 0;
|
|||
|
|
var journeyTimer = null;
|
|||
|
|
var journeyStartedAt = 0;
|
|||
|
|
var journeyElapsedMs = 0;
|
|||
|
|
var journeyOwnerToken = 0;
|
|||
|
|
var JOURNEY_DWELL_MS = 1100;
|
|||
|
|
|
|||
|
|
function nodes() {
|
|||
|
|
return Array.prototype.slice.call(svg.querySelectorAll('[data-node-id]'));
|
|||
|
|
}
|
|||
|
|
function edges() {
|
|||
|
|
return Array.prototype.slice.call(svg.querySelectorAll('[data-edge-from][data-edge-to]'));
|
|||
|
|
}
|
|||
|
|
function nodesById() {
|
|||
|
|
var out = Object.create(null);
|
|||
|
|
nodes().forEach(function (node) { out[node.getAttribute('data-node-id')] = node; });
|
|||
|
|
return out;
|
|||
|
|
}
|
|||
|
|
function nodeLabel(node, fallback) {
|
|||
|
|
return node.getAttribute('data-node-label') ||
|
|||
|
|
(node.getAttribute('aria-label') || fallback).replace(/^Focus\s+/, '').split(',')[0];
|
|||
|
|
}
|
|||
|
|
function exportSnapshot() {
|
|||
|
|
if (mode !== 'result' || activeNodeIds.length < 2 || activeEdges.length !== activeNodeIds.length - 1) return null;
|
|||
|
|
var allNodes = nodes();
|
|||
|
|
var byId = nodesById();
|
|||
|
|
var seenNodeIds = Object.create(null);
|
|||
|
|
var seenEdgeKeys = Object.create(null);
|
|||
|
|
if (startId !== activeNodeIds[0] || endId !== activeNodeIds[activeNodeIds.length - 1] ||
|
|||
|
|
activeNodeIds.some(function (id) {
|
|||
|
|
if (seenNodeIds[id] || allNodes.filter(function (node) {
|
|||
|
|
return node.getAttribute('data-node-id') === id;
|
|||
|
|
}).length !== 1) return true;
|
|||
|
|
seenNodeIds[id] = true;
|
|||
|
|
return !byId[id];
|
|||
|
|
}) ||
|
|||
|
|
activeEdges.some(function (edge, index) {
|
|||
|
|
if (!edge || !svg.contains(edge)) return true;
|
|||
|
|
var edgeKey = edge.getAttribute('data-edge-key');
|
|||
|
|
var edgeId = edge.getAttribute('data-edge-id') || '';
|
|||
|
|
if (!edgeKey || seenEdgeKeys[edgeKey]) return true;
|
|||
|
|
seenEdgeKeys[edgeKey] = true;
|
|||
|
|
var fragments = Array.prototype.slice.call(svg.querySelectorAll('[data-edge-key]')).filter(function (candidate) {
|
|||
|
|
return candidate.getAttribute('data-edge-key') === edgeKey;
|
|||
|
|
});
|
|||
|
|
var drawableFragments = fragments.filter(hasDrawableGeometry);
|
|||
|
|
return !fragments.length || !fragments.every(function (fragment) {
|
|||
|
|
return fragment.getAttribute('data-edge-from') === activeNodeIds[index] &&
|
|||
|
|
fragment.getAttribute('data-edge-to') === activeNodeIds[index + 1] &&
|
|||
|
|
(fragment.getAttribute('data-edge-id') || '') === edgeId;
|
|||
|
|
}) || drawableFragments.length !== 1 || drawableFragments[0] !== edge ||
|
|||
|
|
edge.getAttribute('data-edge-from') !== activeNodeIds[index] ||
|
|||
|
|
edge.getAttribute('data-edge-to') !== activeNodeIds[index + 1];
|
|||
|
|
})) return null;
|
|||
|
|
return {
|
|||
|
|
source: { id: startId, label: nodeLabel(byId[startId], startId) },
|
|||
|
|
target: { id: endId, label: nodeLabel(byId[endId], endId) },
|
|||
|
|
nodeIds: activeNodeIds.slice(),
|
|||
|
|
hops: activeEdges.length,
|
|||
|
|
edges: activeEdges.map(function (edge) {
|
|||
|
|
return {
|
|||
|
|
key: edge.getAttribute('data-edge-key'),
|
|||
|
|
id: edge.getAttribute('data-edge-id') || '',
|
|||
|
|
from: edge.getAttribute('data-edge-from'),
|
|||
|
|
to: edge.getAttribute('data-edge-to'),
|
|||
|
|
label: edge.getAttribute('data-edge-label') || ''
|
|||
|
|
};
|
|||
|
|
})
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
function edgeShapes(edge) {
|
|||
|
|
if (/^(path|line|polyline)$/i.test(edge.tagName)) return [edge];
|
|||
|
|
return Array.prototype.slice.call(edge.querySelectorAll('path, line, polyline'));
|
|||
|
|
}
|
|||
|
|
function removeOverlay() {
|
|||
|
|
Array.prototype.forEach.call(svg.querySelectorAll('[data-route-probe-overlay]'), function (overlay) {
|
|||
|
|
overlay.remove();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function removeJourneyPulse(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
Array.prototype.forEach.call(svg.querySelectorAll('[data-route-journey-overlay]'), function (overlay) {
|
|||
|
|
overlay.remove();
|
|||
|
|
});
|
|||
|
|
if (journeyOwnerToken && options.release !== false && Archify.motionGovernor) {
|
|||
|
|
var token = journeyOwnerToken;
|
|||
|
|
journeyOwnerToken = 0;
|
|||
|
|
Archify.motionGovernor.release(token);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
function stopJourneyTimer(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
journeyGeneration += 1;
|
|||
|
|
if (journeyTimer) {
|
|||
|
|
if (options.preserveElapsed === true && journeyStartedAt) {
|
|||
|
|
journeyElapsedMs = Math.min(JOURNEY_DWELL_MS, journeyElapsedMs + Math.max(0, Date.now() - journeyStartedAt));
|
|||
|
|
}
|
|||
|
|
window.clearTimeout(journeyTimer);
|
|||
|
|
}
|
|||
|
|
journeyTimer = null;
|
|||
|
|
journeyStartedAt = 0;
|
|||
|
|
if (options.preserveElapsed !== true) journeyElapsedMs = 0;
|
|||
|
|
}
|
|||
|
|
function clearJourneyPresentation(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
removeJourneyPulse();
|
|||
|
|
svg.removeAttribute('data-route-journey');
|
|||
|
|
panel.removeAttribute('data-route-journey');
|
|||
|
|
nodes().forEach(function (node) {
|
|||
|
|
node.removeAttribute('data-route-journey-state');
|
|||
|
|
node.removeAttribute('data-route-journey-current');
|
|||
|
|
});
|
|||
|
|
edges().forEach(function (edge) {
|
|||
|
|
edge.removeAttribute('data-route-journey-state');
|
|||
|
|
edge.removeAttribute('data-route-journey-current');
|
|||
|
|
});
|
|||
|
|
Array.prototype.forEach.call(path.querySelectorAll('[data-route-journey-index]'), function (button, index) {
|
|||
|
|
button.removeAttribute('data-route-journey-state');
|
|||
|
|
button.removeAttribute('aria-current');
|
|||
|
|
button.setAttribute('tabindex', index === 0 ? '0' : '-1');
|
|||
|
|
});
|
|||
|
|
if (options.keepControls !== true) journeyControls.hidden = true;
|
|||
|
|
journeyControls.setAttribute('data-playing', 'false');
|
|||
|
|
}
|
|||
|
|
function resetJourneyState() {
|
|||
|
|
stopJourneyTimer();
|
|||
|
|
journeyPlaying = false;
|
|||
|
|
journeyComplete = false;
|
|||
|
|
journeyIndex = -1;
|
|||
|
|
clearJourneyPresentation();
|
|||
|
|
}
|
|||
|
|
function cleanSvgState() {
|
|||
|
|
resetJourneyState();
|
|||
|
|
svg.removeAttribute('data-route-picking');
|
|||
|
|
svg.removeAttribute('data-route-active');
|
|||
|
|
removeOverlay();
|
|||
|
|
nodes().forEach(function (node) {
|
|||
|
|
node.removeAttribute('data-route-match');
|
|||
|
|
node.removeAttribute('data-route-start');
|
|||
|
|
node.removeAttribute('data-route-end');
|
|||
|
|
node.removeAttribute('data-route-step');
|
|||
|
|
node.removeAttribute('data-route-candidate');
|
|||
|
|
node.style.removeProperty('--route-step');
|
|||
|
|
});
|
|||
|
|
edges().forEach(function (edge) {
|
|||
|
|
edge.removeAttribute('data-route-match');
|
|||
|
|
edge.removeAttribute('data-route-step');
|
|||
|
|
edge.style.removeProperty('--route-step');
|
|||
|
|
});
|
|||
|
|
panel.removeAttribute('data-route-dock');
|
|||
|
|
}
|
|||
|
|
function replaceRouteHash(value) {
|
|||
|
|
try {
|
|||
|
|
history.replaceState(null, '', location.pathname + location.search + (value ? '#route=' + value : ''));
|
|||
|
|
} catch (_) {}
|
|||
|
|
}
|
|||
|
|
function renderPlaceholder(copy) {
|
|||
|
|
path.textContent = '';
|
|||
|
|
var placeholder = document.createElement('span');
|
|||
|
|
placeholder.className = 'route-probe-placeholder';
|
|||
|
|
placeholder.textContent = copy;
|
|||
|
|
path.appendChild(placeholder);
|
|||
|
|
}
|
|||
|
|
function renderPath(ids, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
var byId = nodesById();
|
|||
|
|
path.textContent = '';
|
|||
|
|
ids.forEach(function (id, index) {
|
|||
|
|
if (index) {
|
|||
|
|
var arrow = document.createElement('span');
|
|||
|
|
arrow.className = 'route-probe-arrow';
|
|||
|
|
arrow.setAttribute('aria-hidden', 'true');
|
|||
|
|
arrow.textContent = '\u2192';
|
|||
|
|
path.appendChild(arrow);
|
|||
|
|
}
|
|||
|
|
var item = document.createElement(options.interactive === true ? 'button' : 'span');
|
|||
|
|
if (options.interactive === true) {
|
|||
|
|
item.type = 'button';
|
|||
|
|
item.setAttribute('data-route-journey-index', String(index));
|
|||
|
|
item.setAttribute('data-route-node-id', id);
|
|||
|
|
item.setAttribute('tabindex', index === 0 ? '0' : '-1');
|
|||
|
|
item.setAttribute('aria-label', viewerText('viewer.route.position', {
|
|||
|
|
index: index + 1,
|
|||
|
|
total: ids.length,
|
|||
|
|
label: nodeLabel(byId[id], id)
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
item.className = 'route-probe-node';
|
|||
|
|
item.textContent = nodeLabel(byId[id], id);
|
|||
|
|
item.title = nodeLabel(byId[id], id) + ' · ' + id;
|
|||
|
|
if (index === 0) item.setAttribute('data-endpoint', 'start');
|
|||
|
|
if (index === ids.length - 1 && ids.length > 1) item.setAttribute('data-endpoint', 'end');
|
|||
|
|
path.appendChild(item);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function setTrigger(active) {
|
|||
|
|
trigger.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|||
|
|
trigger.setAttribute('aria-label', viewerText(active ? 'viewer.route.trigger.clear' : 'viewer.guide.route.aria'));
|
|||
|
|
}
|
|||
|
|
function clear(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
var wasActive = mode !== 'idle';
|
|||
|
|
if (Archify.finder && Archify.finder.isOpen() && typeof Archify.finder.context === 'function' && Archify.finder.context().indexOf('route-') === 0) {
|
|||
|
|
Archify.finder.close({ restoreFocus: false });
|
|||
|
|
}
|
|||
|
|
mode = 'idle';
|
|||
|
|
startId = null;
|
|||
|
|
endId = null;
|
|||
|
|
activeNodeIds = [];
|
|||
|
|
activeEdges = [];
|
|||
|
|
cleanSvgState();
|
|||
|
|
panel.hidden = true;
|
|||
|
|
panel.setAttribute('data-state', 'idle');
|
|||
|
|
panel.removeAttribute('data-finder-open');
|
|||
|
|
title.textContent = viewerText('viewer.route.start');
|
|||
|
|
renderPlaceholder(viewerText('viewer.route.pickTwo'));
|
|||
|
|
status.textContent = viewerText('viewer.route.instructions');
|
|||
|
|
findBtn.hidden = true;
|
|||
|
|
findBtn.textContent = viewerText('viewer.route.start.find');
|
|||
|
|
findBtn.setAttribute('aria-label', viewerText('viewer.route.start.find.aria'));
|
|||
|
|
copyBtn.hidden = true;
|
|||
|
|
copyBtn.textContent = viewerText('viewer.route.copy');
|
|||
|
|
if (Archify.exportMenu && typeof Archify.exportMenu.syncRouteShare === 'function') Archify.exportMenu.syncRouteShare();
|
|||
|
|
setTrigger(false);
|
|||
|
|
if (wasActive && options.preserveView !== true && Archify.view && typeof Archify.view.reset === 'function') {
|
|||
|
|
Archify.view.reset({ automatic: true });
|
|||
|
|
}
|
|||
|
|
if (options.updateUrl !== false) replaceRouteHash('');
|
|||
|
|
if (options.restoreFocus === true) trigger.focus();
|
|||
|
|
}
|
|||
|
|
function outgoingByNode() {
|
|||
|
|
var byId = nodesById();
|
|||
|
|
var outgoing = {};
|
|||
|
|
edges().forEach(function (edge) {
|
|||
|
|
var from = edge.getAttribute('data-edge-from');
|
|||
|
|
var to = edge.getAttribute('data-edge-to');
|
|||
|
|
if (!byId[from] || !byId[to] || from === to) return;
|
|||
|
|
if (!outgoing[from]) outgoing[from] = [];
|
|||
|
|
outgoing[from].push({ edge: edge, to: to });
|
|||
|
|
});
|
|||
|
|
return outgoing;
|
|||
|
|
}
|
|||
|
|
function reachableFrom(source) {
|
|||
|
|
var outgoing = outgoingByNode();
|
|||
|
|
var reached = {};
|
|||
|
|
var queue = [source];
|
|||
|
|
reached[source] = true;
|
|||
|
|
for (var cursor = 0; cursor < queue.length; cursor += 1) {
|
|||
|
|
var links = outgoing[queue[cursor]] || [];
|
|||
|
|
links.forEach(function (link) {
|
|||
|
|
if (reached[link.to]) return;
|
|||
|
|
reached[link.to] = true;
|
|||
|
|
queue.push(link.to);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
return reached;
|
|||
|
|
}
|
|||
|
|
function hopDistancesFrom(source) {
|
|||
|
|
var outgoing = outgoingByNode();
|
|||
|
|
var distances = {};
|
|||
|
|
var queue = [source];
|
|||
|
|
distances[source] = 0;
|
|||
|
|
for (var cursor = 0; cursor < queue.length; cursor += 1) {
|
|||
|
|
var links = outgoing[queue[cursor]] || [];
|
|||
|
|
links.forEach(function (link) {
|
|||
|
|
if (Object.prototype.hasOwnProperty.call(distances, link.to)) return;
|
|||
|
|
distances[link.to] = distances[queue[cursor]] + 1;
|
|||
|
|
queue.push(link.to);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
return distances;
|
|||
|
|
}
|
|||
|
|
function shortestDirectedPath(source, target) {
|
|||
|
|
if (source === target) return null;
|
|||
|
|
var outgoing = outgoingByNode();
|
|||
|
|
var previous = {};
|
|||
|
|
var queue = [source];
|
|||
|
|
previous[source] = null;
|
|||
|
|
for (var cursor = 0; cursor < queue.length && !Object.prototype.hasOwnProperty.call(previous, target); cursor += 1) {
|
|||
|
|
var links = outgoing[queue[cursor]] || [];
|
|||
|
|
links.some(function (link) {
|
|||
|
|
if (Object.prototype.hasOwnProperty.call(previous, link.to)) return false;
|
|||
|
|
previous[link.to] = { from: queue[cursor], edge: link.edge };
|
|||
|
|
queue.push(link.to);
|
|||
|
|
return link.to === target;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
if (!Object.prototype.hasOwnProperty.call(previous, target)) return null;
|
|||
|
|
var nodeIds = [target];
|
|||
|
|
var routeEdges = [];
|
|||
|
|
var current = target;
|
|||
|
|
while (current !== source) {
|
|||
|
|
var step = previous[current];
|
|||
|
|
if (!step) return null;
|
|||
|
|
routeEdges.unshift(step.edge);
|
|||
|
|
nodeIds.unshift(step.from);
|
|||
|
|
current = step.from;
|
|||
|
|
}
|
|||
|
|
return { nodes: nodeIds, edges: routeEdges };
|
|||
|
|
}
|
|||
|
|
function traceGeometry(shape, step) {
|
|||
|
|
var clone = shape.cloneNode(false);
|
|||
|
|
clone.removeAttribute('id');
|
|||
|
|
clone.removeAttribute('class');
|
|||
|
|
clone.removeAttribute('style');
|
|||
|
|
clone.removeAttribute('marker-start');
|
|||
|
|
clone.removeAttribute('marker-mid');
|
|||
|
|
clone.removeAttribute('marker-end');
|
|||
|
|
clone.removeAttribute('role');
|
|||
|
|
clone.removeAttribute('aria-label');
|
|||
|
|
clone.removeAttribute('aria-labelledby');
|
|||
|
|
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.removeAttribute('data-route-match');
|
|||
|
|
clone.removeAttribute('data-route-step');
|
|||
|
|
clone.setAttribute('class', 'route-probe-flow');
|
|||
|
|
clone.setAttribute('pathLength', '1');
|
|||
|
|
clone.style.setProperty('--route-step', String(step));
|
|||
|
|
return clone;
|
|||
|
|
}
|
|||
|
|
function renderOverlay(routeEdges) {
|
|||
|
|
removeOverlay();
|
|||
|
|
if (!routeEdges.length) return;
|
|||
|
|
var overlay = document.createElementNS(namespace, 'g');
|
|||
|
|
overlay.setAttribute('class', 'route-probe-overlay');
|
|||
|
|
overlay.setAttribute('data-route-probe-overlay', '');
|
|||
|
|
overlay.setAttribute('aria-hidden', 'true');
|
|||
|
|
routeEdges.forEach(function (edge, step) {
|
|||
|
|
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, step)); });
|
|||
|
|
if (wrapper.childNodes.length) overlay.appendChild(wrapper);
|
|||
|
|
});
|
|||
|
|
var firstNode = svg.querySelector('[data-node-id]');
|
|||
|
|
if (firstNode) svg.insertBefore(overlay, firstNode);
|
|||
|
|
else svg.appendChild(overlay);
|
|||
|
|
}
|
|||
|
|
function journeyButtons() {
|
|||
|
|
return Array.prototype.slice.call(path.querySelectorAll('[data-route-journey-index]'));
|
|||
|
|
}
|
|||
|
|
function journeyMotionAllowed() {
|
|||
|
|
return html.getAttribute('data-embed') !== 'true' &&
|
|||
|
|
!document.hidden &&
|
|||
|
|
Archify.motionGovernor &&
|
|||
|
|
Archify.motionGovernor.capable === true &&
|
|||
|
|
!Archify.motionGovernor.isPaused();
|
|||
|
|
}
|
|||
|
|
function routeOverviewStatus() {
|
|||
|
|
return viewerText('viewer.route.overview.status', {
|
|||
|
|
nodes: viewerCount('viewer.route.overview.node', activeNodeIds.length),
|
|||
|
|
hops: viewerCount('viewer.route.overview.hop', activeEdges.length)
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function centerJourneyButton(button) {
|
|||
|
|
if (!button) return;
|
|||
|
|
var target = button.offsetLeft - Math.max(0, (path.clientWidth - button.offsetWidth) / 2);
|
|||
|
|
path.scrollLeft = Math.max(0, target);
|
|||
|
|
}
|
|||
|
|
function focusJourneyButton(index) {
|
|||
|
|
var buttons = journeyButtons();
|
|||
|
|
if (!buttons.length) return false;
|
|||
|
|
var next = Math.max(0, Math.min(buttons.length - 1, index));
|
|||
|
|
buttons.forEach(function (button, buttonIndex) {
|
|||
|
|
button.setAttribute('tabindex', buttonIndex === next ? '0' : '-1');
|
|||
|
|
});
|
|||
|
|
try { buttons[next].focus({ preventScroll: true }); } catch (_) { buttons[next].focus(); }
|
|||
|
|
centerJourneyButton(buttons[next]);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function renderJourneyControls() {
|
|||
|
|
var hasRoute = mode === 'result' && activeNodeIds.length > 1;
|
|||
|
|
var canPlay = hasRoute && journeyMotionAllowed();
|
|||
|
|
journeyControls.hidden = !hasRoute;
|
|||
|
|
journeyControls.setAttribute('data-playing', journeyPlaying ? 'true' : 'false');
|
|||
|
|
journeyPrevBtn.disabled = !hasRoute || journeyIndex <= 0;
|
|||
|
|
journeyNextBtn.disabled = !hasRoute || journeyIndex >= activeNodeIds.length - 1;
|
|||
|
|
journeyOverviewBtn.disabled = !hasRoute || journeyIndex < 0;
|
|||
|
|
journeyPlayBtn.disabled = !canPlay;
|
|||
|
|
journeyPlayBtn.setAttribute('aria-pressed', journeyPlaying ? 'true' : 'false');
|
|||
|
|
if (journeyPlaying) {
|
|||
|
|
journeyPlayIcon.textContent = '\u275a\u275a';
|
|||
|
|
journeyPlayLabel.textContent = viewerText('viewer.route.pause.label');
|
|||
|
|
journeyPlayBtn.setAttribute('aria-label', viewerText('viewer.route.pause'));
|
|||
|
|
journeyPlayBtn.title = viewerText('viewer.route.pause');
|
|||
|
|
} else if (journeyComplete || journeyIndex === activeNodeIds.length - 1) {
|
|||
|
|
journeyPlayIcon.textContent = '\u21bb';
|
|||
|
|
journeyPlayLabel.textContent = viewerText('viewer.route.replay.label');
|
|||
|
|
journeyPlayBtn.setAttribute('aria-label', viewerText('viewer.route.replay'));
|
|||
|
|
journeyPlayBtn.title = viewerText(canPlay ? 'viewer.route.replay' : 'viewer.route.motionRequired');
|
|||
|
|
} else {
|
|||
|
|
journeyPlayIcon.textContent = '\u25b6';
|
|||
|
|
journeyPlayLabel.textContent = viewerText('viewer.route.journey');
|
|||
|
|
journeyPlayBtn.setAttribute('aria-label', viewerText('viewer.route.play'));
|
|||
|
|
journeyPlayBtn.title = viewerText(canPlay ? 'viewer.route.play' : 'viewer.route.motionRequired');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
function journeyGeometry(shape) {
|
|||
|
|
var clone = shape.cloneNode(false);
|
|||
|
|
clone.removeAttribute('id');
|
|||
|
|
clone.removeAttribute('class');
|
|||
|
|
clone.removeAttribute('style');
|
|||
|
|
clone.removeAttribute('marker-start');
|
|||
|
|
clone.removeAttribute('marker-mid');
|
|||
|
|
clone.removeAttribute('marker-end');
|
|||
|
|
clone.removeAttribute('role');
|
|||
|
|
clone.removeAttribute('aria-label');
|
|||
|
|
clone.removeAttribute('aria-labelledby');
|
|||
|
|
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.removeAttribute('data-route-match');
|
|||
|
|
clone.removeAttribute('data-route-step');
|
|||
|
|
clone.removeAttribute('data-route-journey-state');
|
|||
|
|
clone.removeAttribute('data-route-journey-current');
|
|||
|
|
clone.setAttribute('class', 'route-journey-flow');
|
|||
|
|
clone.setAttribute('pathLength', '1');
|
|||
|
|
return clone;
|
|||
|
|
}
|
|||
|
|
function renderJourneyPulse(edge) {
|
|||
|
|
removeJourneyPulse();
|
|||
|
|
if (!edge || !journeyMotionAllowed()) return false;
|
|||
|
|
var overlay = document.createElementNS(namespace, 'g');
|
|||
|
|
overlay.setAttribute('class', 'route-journey-overlay');
|
|||
|
|
overlay.setAttribute('data-route-journey-overlay', '');
|
|||
|
|
overlay.setAttribute('aria-hidden', 'true');
|
|||
|
|
var wrapper = document.createElementNS(namespace, 'g');
|
|||
|
|
if (edge.hasAttribute('transform')) wrapper.setAttribute('transform', edge.getAttribute('transform'));
|
|||
|
|
edgeShapes(edge).forEach(function (shape) { wrapper.appendChild(journeyGeometry(shape)); });
|
|||
|
|
if (!wrapper.childNodes.length) return false;
|
|||
|
|
overlay.appendChild(wrapper);
|
|||
|
|
var firstNode = svg.querySelector('[data-node-id]');
|
|||
|
|
if (firstNode) svg.insertBefore(overlay, firstNode);
|
|||
|
|
else svg.appendChild(overlay);
|
|||
|
|
if (Archify.motionGovernor && Archify.motionGovernor.capable) {
|
|||
|
|
var token = 0;
|
|||
|
|
token = Archify.motionGovernor.claim('route', function () {
|
|||
|
|
if (journeyOwnerToken === token) journeyOwnerToken = 0;
|
|||
|
|
removeJourneyPulse({ release: false });
|
|||
|
|
});
|
|||
|
|
journeyOwnerToken = token;
|
|||
|
|
}
|
|||
|
|
overlay.addEventListener('animationend', function () {
|
|||
|
|
if (overlay.isConnected) removeJourneyPulse();
|
|||
|
|
}, { once: true });
|
|||
|
|
window.setTimeout(function () {
|
|||
|
|
if (overlay.isConnected) removeJourneyPulse();
|
|||
|
|
}, 860);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function applyJourneyState(index, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (mode !== 'result' || !activeNodeIds.length) return false;
|
|||
|
|
var byId = nodesById();
|
|||
|
|
journeyIndex = Math.max(0, Math.min(activeNodeIds.length - 1, index));
|
|||
|
|
removeJourneyPulse();
|
|||
|
|
svg.setAttribute('data-route-journey', (journeyIndex + 1) + '/' + activeNodeIds.length);
|
|||
|
|
panel.setAttribute('data-route-journey', String(journeyIndex));
|
|||
|
|
activeNodeIds.forEach(function (id, step) {
|
|||
|
|
var node = byId[id];
|
|||
|
|
if (!node) return;
|
|||
|
|
var state = step < journeyIndex ? 'past' : (step === journeyIndex ? 'current' : 'future');
|
|||
|
|
node.setAttribute('data-route-journey-state', state);
|
|||
|
|
if (step === journeyIndex) node.setAttribute('data-route-journey-current', '');
|
|||
|
|
else node.removeAttribute('data-route-journey-current');
|
|||
|
|
});
|
|||
|
|
activeEdges.forEach(function (edge, step) {
|
|||
|
|
var destination = step + 1;
|
|||
|
|
var state = destination < journeyIndex ? 'past' : (destination === journeyIndex ? 'current' : 'future');
|
|||
|
|
edge.setAttribute('data-route-journey-state', state);
|
|||
|
|
if (destination === journeyIndex) edge.setAttribute('data-route-journey-current', '');
|
|||
|
|
else edge.removeAttribute('data-route-journey-current');
|
|||
|
|
});
|
|||
|
|
var buttons = journeyButtons();
|
|||
|
|
buttons.forEach(function (button, step) {
|
|||
|
|
var state = step < journeyIndex ? 'past' : (step === journeyIndex ? 'current' : 'future');
|
|||
|
|
button.setAttribute('data-route-journey-state', state);
|
|||
|
|
button.setAttribute('tabindex', step === journeyIndex ? '0' : '-1');
|
|||
|
|
if (step === journeyIndex) button.setAttribute('aria-current', 'step');
|
|||
|
|
else button.removeAttribute('aria-current');
|
|||
|
|
});
|
|||
|
|
if (options.center !== false) centerJourneyButton(buttons[journeyIndex]);
|
|||
|
|
var phase = viewerText(journeyPlaying
|
|||
|
|
? 'viewer.route.phase.playing'
|
|||
|
|
: journeyComplete
|
|||
|
|
? 'viewer.route.phase.complete'
|
|||
|
|
: 'viewer.route.phase.inspecting');
|
|||
|
|
status.textContent = viewerText('viewer.route.step', {
|
|||
|
|
index: journeyIndex + 1,
|
|||
|
|
total: activeNodeIds.length,
|
|||
|
|
phase: phase,
|
|||
|
|
label: nodeLabel(byId[activeNodeIds[journeyIndex]], activeNodeIds[journeyIndex])
|
|||
|
|
});
|
|||
|
|
if (options.pulse === true && journeyIndex > 0) renderJourneyPulse(activeEdges[journeyIndex - 1]);
|
|||
|
|
if (options.reveal !== false && Archify.view && typeof Archify.view.reveal === 'function') {
|
|||
|
|
Archify.view.reveal(activeNodeIds.slice(Math.max(0, journeyIndex - 1), Math.min(activeNodeIds.length, journeyIndex + 2)), {
|
|||
|
|
includeNeighbors: false,
|
|||
|
|
reason: 'route-journey',
|
|||
|
|
maxScale: 1.65,
|
|||
|
|
padding: 64,
|
|||
|
|
duration: 360,
|
|||
|
|
instant: !journeyMotionAllowed()
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
renderJourneyControls();
|
|||
|
|
requestDocking();
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function pauseJourney(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (!journeyPlaying && options.complete !== true) {
|
|||
|
|
renderJourneyControls();
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
stopJourneyTimer({ preserveElapsed: options.complete !== true && options.preserveElapsed !== false });
|
|||
|
|
journeyPlaying = false;
|
|||
|
|
journeyComplete = options.complete === true;
|
|||
|
|
if (journeyComplete) journeyElapsedMs = 0;
|
|||
|
|
removeJourneyPulse();
|
|||
|
|
if (journeyIndex >= 0) applyJourneyState(journeyIndex, { center: false, pulse: false, reveal: false });
|
|||
|
|
else renderJourneyControls();
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function selectJourneyIndex(index) {
|
|||
|
|
if (mode !== 'result') return false;
|
|||
|
|
stopJourneyTimer();
|
|||
|
|
journeyPlaying = false;
|
|||
|
|
journeyComplete = false;
|
|||
|
|
return applyJourneyState(index, { pulse: true, reveal: true });
|
|||
|
|
}
|
|||
|
|
function showJourneyOverview(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (mode !== 'result') return false;
|
|||
|
|
stopJourneyTimer();
|
|||
|
|
journeyPlaying = false;
|
|||
|
|
journeyComplete = false;
|
|||
|
|
journeyIndex = -1;
|
|||
|
|
clearJourneyPresentation({ keepControls: true });
|
|||
|
|
status.textContent = routeOverviewStatus();
|
|||
|
|
renderJourneyControls();
|
|||
|
|
if (options.reveal !== false && Archify.view && typeof Archify.view.reveal === 'function') {
|
|||
|
|
Archify.view.reveal(activeNodeIds, { includeNeighbors: false, reason: 'route', instant: !journeyMotionAllowed() });
|
|||
|
|
}
|
|||
|
|
requestDocking();
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function scheduleJourney() {
|
|||
|
|
if (!journeyPlaying || !journeyMotionAllowed()) {
|
|||
|
|
pauseJourney({ preserveElapsed: true });
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
var generation = ++journeyGeneration;
|
|||
|
|
var remaining = Math.max(0, JOURNEY_DWELL_MS - journeyElapsedMs);
|
|||
|
|
journeyStartedAt = Date.now();
|
|||
|
|
journeyTimer = window.setTimeout(function () {
|
|||
|
|
if (generation !== journeyGeneration || !journeyPlaying) return;
|
|||
|
|
journeyTimer = null;
|
|||
|
|
journeyStartedAt = 0;
|
|||
|
|
journeyElapsedMs = 0;
|
|||
|
|
if (journeyIndex >= activeNodeIds.length - 1) {
|
|||
|
|
pauseJourney({ complete: true, preserveElapsed: false });
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
applyJourneyState(journeyIndex + 1, { pulse: true, reveal: true });
|
|||
|
|
// Camera and motion-owner handoff are allowed to synchronously
|
|||
|
|
// settle the previous step. The new step always starts with a fresh
|
|||
|
|
// dwell; never carry cleanup time into the next scheduler lease.
|
|||
|
|
journeyElapsedMs = 0;
|
|||
|
|
journeyStartedAt = 0;
|
|||
|
|
scheduleJourney();
|
|||
|
|
}, remaining);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function playJourney() {
|
|||
|
|
if (mode !== 'result' || activeNodeIds.length < 2 || !journeyMotionAllowed()) {
|
|||
|
|
renderJourneyControls();
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
if (journeyPlaying) return true;
|
|||
|
|
if (journeyComplete || journeyIndex >= activeNodeIds.length - 1) {
|
|||
|
|
journeyIndex = -1;
|
|||
|
|
journeyElapsedMs = 0;
|
|||
|
|
journeyComplete = false;
|
|||
|
|
}
|
|||
|
|
journeyPlaying = true;
|
|||
|
|
if (journeyIndex < 0) applyJourneyState(0, { pulse: false, reveal: true });
|
|||
|
|
else applyJourneyState(journeyIndex, { center: false, pulse: false, reveal: false });
|
|||
|
|
scheduleJourney();
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function toggleJourney() {
|
|||
|
|
return journeyPlaying ? pauseJourney({ preserveElapsed: true }) : playJourney();
|
|||
|
|
}
|
|||
|
|
function previousJourney() {
|
|||
|
|
return selectJourneyIndex(Math.max(0, journeyIndex < 0 ? 0 : journeyIndex - 1));
|
|||
|
|
}
|
|||
|
|
function nextJourney() {
|
|||
|
|
return selectJourneyIndex(Math.min(activeNodeIds.length - 1, journeyIndex + 1));
|
|||
|
|
}
|
|||
|
|
function overlapArea(a, b) {
|
|||
|
|
var width = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left));
|
|||
|
|
var height = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));
|
|||
|
|
return width * height;
|
|||
|
|
}
|
|||
|
|
function updateDocking() {
|
|||
|
|
if (panel.hidden) {
|
|||
|
|
panel.removeAttribute('data-route-dock');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
var containerRect = container.getBoundingClientRect();
|
|||
|
|
var width = panel.offsetWidth;
|
|||
|
|
var height = panel.offsetHeight;
|
|||
|
|
if (!width || !height) return;
|
|||
|
|
var currentRect = panel.getBoundingClientRect();
|
|||
|
|
var left = currentRect.left;
|
|||
|
|
var topCandidate = {
|
|||
|
|
left: left,
|
|||
|
|
right: left + width,
|
|||
|
|
top: containerRect.top + 8,
|
|||
|
|
bottom: containerRect.top + 8 + height
|
|||
|
|
};
|
|||
|
|
var nav = container.querySelector('.diagram-nav');
|
|||
|
|
var navRect = nav ? nav.getBoundingClientRect() : null;
|
|||
|
|
var bottom = navRect ? navRect.top - 9 : containerRect.bottom - 8;
|
|||
|
|
var bottomCandidate = {
|
|||
|
|
left: left,
|
|||
|
|
right: left + width,
|
|||
|
|
top: bottom - height,
|
|||
|
|
bottom: bottom
|
|||
|
|
};
|
|||
|
|
var byId = nodesById();
|
|||
|
|
var relevant = activeNodeIds.length ? activeNodeIds : (startId ? [startId] : []);
|
|||
|
|
var blockers = relevant.map(function (id) { return byId[id]; }).filter(Boolean).map(function (node) {
|
|||
|
|
return node.getBoundingClientRect();
|
|||
|
|
});
|
|||
|
|
function score(candidate) {
|
|||
|
|
var value = blockers.reduce(function (sum, rect) { return sum + overlapArea(candidate, rect); }, 0);
|
|||
|
|
if (navRect) value += overlapArea(candidate, navRect) * 4;
|
|||
|
|
if (candidate.top < containerRect.top || candidate.bottom > containerRect.bottom) value += 1000000;
|
|||
|
|
return value;
|
|||
|
|
}
|
|||
|
|
if (score(topCandidate) <= score(bottomCandidate)) panel.setAttribute('data-route-dock', 'top');
|
|||
|
|
else panel.setAttribute('data-route-dock', 'bottom');
|
|||
|
|
}
|
|||
|
|
function requestDocking() {
|
|||
|
|
requestAnimationFrame(updateDocking);
|
|||
|
|
window.setTimeout(updateDocking, 120);
|
|||
|
|
window.setTimeout(updateDocking, 560);
|
|||
|
|
}
|
|||
|
|
function chooseStart(id) {
|
|||
|
|
var byId = nodesById();
|
|||
|
|
if (!byId[id]) return false;
|
|||
|
|
cleanSvgState();
|
|||
|
|
startId = id;
|
|||
|
|
endId = null;
|
|||
|
|
mode = 'target';
|
|||
|
|
var reached = reachableFrom(id);
|
|||
|
|
byId[id].setAttribute('data-route-start', '');
|
|||
|
|
byId[id].setAttribute('data-route-match', '');
|
|||
|
|
Object.keys(reached).forEach(function (candidateId) {
|
|||
|
|
if (candidateId !== id && byId[candidateId]) byId[candidateId].setAttribute('data-route-candidate', '');
|
|||
|
|
});
|
|||
|
|
svg.setAttribute('data-route-picking', 'target');
|
|||
|
|
panel.setAttribute('data-state', 'target');
|
|||
|
|
title.textContent = viewerText('viewer.route.destination', { label: nodeLabel(byId[id], id) });
|
|||
|
|
renderPath([id]);
|
|||
|
|
var count = Math.max(0, Object.keys(reached).length - 1);
|
|||
|
|
status.textContent = count
|
|||
|
|
? viewerCount('viewer.route.destination.count', count)
|
|||
|
|
: viewerText('viewer.route.noOutgoing');
|
|||
|
|
findBtn.hidden = false;
|
|||
|
|
findBtn.textContent = viewerText('viewer.route.destination.find');
|
|||
|
|
findBtn.setAttribute('aria-label', viewerText('viewer.route.destination.find.aria'));
|
|||
|
|
requestDocking();
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function showResult(result, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
var byId = nodesById();
|
|||
|
|
cleanSvgState();
|
|||
|
|
mode = 'result';
|
|||
|
|
startId = result.nodes[0];
|
|||
|
|
endId = result.nodes[result.nodes.length - 1];
|
|||
|
|
activeNodeIds = result.nodes.slice();
|
|||
|
|
activeEdges = result.edges.slice();
|
|||
|
|
result.nodes.forEach(function (id, step) {
|
|||
|
|
var node = byId[id];
|
|||
|
|
if (!node) return;
|
|||
|
|
node.setAttribute('data-route-match', '');
|
|||
|
|
node.setAttribute('data-route-step', String(step));
|
|||
|
|
node.style.setProperty('--route-step', String(step));
|
|||
|
|
if (step === 0) node.setAttribute('data-route-start', '');
|
|||
|
|
if (step === result.nodes.length - 1) node.setAttribute('data-route-end', '');
|
|||
|
|
});
|
|||
|
|
result.edges.forEach(function (edge, step) {
|
|||
|
|
edge.setAttribute('data-route-match', '');
|
|||
|
|
edge.setAttribute('data-route-step', String(step));
|
|||
|
|
edge.style.setProperty('--route-step', String(step));
|
|||
|
|
});
|
|||
|
|
svg.setAttribute('data-route-active', startId + '~' + endId);
|
|||
|
|
renderOverlay(result.edges);
|
|||
|
|
renderPath(result.nodes, { interactive: true });
|
|||
|
|
panel.setAttribute('data-state', 'result');
|
|||
|
|
panel.hidden = false;
|
|||
|
|
title.textContent = viewerText('viewer.route.result.title', {
|
|||
|
|
source: nodeLabel(byId[startId], startId),
|
|||
|
|
target: nodeLabel(byId[endId], endId)
|
|||
|
|
});
|
|||
|
|
findBtn.hidden = true;
|
|||
|
|
copyBtn.hidden = false;
|
|||
|
|
setTrigger(true);
|
|||
|
|
if (Archify.exportMenu && typeof Archify.exportMenu.syncRouteShare === 'function') Archify.exportMenu.syncRouteShare();
|
|||
|
|
if (options.updateUrl !== false) {
|
|||
|
|
replaceRouteHash(encodeURIComponent(startId) + '~' + encodeURIComponent(endId));
|
|||
|
|
}
|
|||
|
|
showJourneyOverview({ reveal: false });
|
|||
|
|
if (Archify.view && typeof Archify.view.reveal === 'function') {
|
|||
|
|
Archify.view.reveal(result.nodes, { includeNeighbors: false, reason: 'route' });
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function choose(id, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
var byId = nodesById();
|
|||
|
|
if (!byId[id]) return false;
|
|||
|
|
if (mode === 'source') return chooseStart(id);
|
|||
|
|
if (mode !== 'target') return false;
|
|||
|
|
if (id === startId) {
|
|||
|
|
panel.setAttribute('data-state', 'error');
|
|||
|
|
title.textContent = viewerText('viewer.route.differentDestination');
|
|||
|
|
status.textContent = viewerText('viewer.route.distinct');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
var result = shortestDirectedPath(startId, id);
|
|||
|
|
if (!result) {
|
|||
|
|
panel.setAttribute('data-state', 'error');
|
|||
|
|
title.textContent = viewerText('viewer.route.unreachable', { label: nodeLabel(byId[id], id) });
|
|||
|
|
status.textContent = viewerText('viewer.route.unreachable.detail', {
|
|||
|
|
target: nodeLabel(byId[id], id),
|
|||
|
|
source: nodeLabel(byId[startId], startId)
|
|||
|
|
});
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
return showResult(result, options);
|
|||
|
|
}
|
|||
|
|
function begin(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (html.getAttribute('data-embed') === 'true') return false;
|
|||
|
|
if (Archify.semanticLens && typeof Archify.semanticLens.clearPreview === 'function') Archify.semanticLens.clearPreview();
|
|||
|
|
if (Archify.semanticLens && Archify.semanticLens.active()) {
|
|||
|
|
Archify.semanticLens.clear({ updateUrl: false, preserveView: true, closePanel: true });
|
|||
|
|
}
|
|||
|
|
var focused = options.source || (Archify.focus && typeof Archify.focus.active === 'function' ? Archify.focus.active() : null);
|
|||
|
|
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 });
|
|||
|
|
}
|
|||
|
|
if (Archify.finder && Archify.finder.isOpen()) Archify.finder.close({ restoreFocus: false });
|
|||
|
|
if (Archify.radar && Archify.radar.isOpen()) Archify.radar.close({ restoreFocus: false });
|
|||
|
|
mode = 'source';
|
|||
|
|
panel.hidden = false;
|
|||
|
|
panel.setAttribute('data-state', 'source');
|
|||
|
|
svg.setAttribute('data-route-picking', 'source');
|
|||
|
|
title.textContent = viewerText('viewer.route.start');
|
|||
|
|
renderPlaceholder(viewerText('viewer.route.pickOne'));
|
|||
|
|
status.textContent = viewerText('viewer.route.start.instructions');
|
|||
|
|
findBtn.hidden = false;
|
|||
|
|
findBtn.textContent = viewerText('viewer.route.start.find');
|
|||
|
|
findBtn.setAttribute('aria-label', viewerText('viewer.route.start.find.aria'));
|
|||
|
|
copyBtn.hidden = true;
|
|||
|
|
setTrigger(true);
|
|||
|
|
if (focused && nodesById()[focused]) chooseStart(focused);
|
|||
|
|
if (options.focusNode === true && !focused) {
|
|||
|
|
var first = nodes()[0];
|
|||
|
|
if (first) {
|
|||
|
|
try { first.focus({ preventScroll: true }); } catch (_) { try { first.focus(); } catch (_) {} }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function toggle(options) {
|
|||
|
|
if (mode !== 'idle') {
|
|||
|
|
clear({ restoreFocus: true });
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
return begin(options);
|
|||
|
|
}
|
|||
|
|
function finderContext() {
|
|||
|
|
var byId = nodesById();
|
|||
|
|
if (mode === 'source') {
|
|||
|
|
var outgoing = outgoingByNode();
|
|||
|
|
var sourceIds = nodes().map(function (node) { return node.getAttribute('data-node-id'); }).filter(function (id) {
|
|||
|
|
return outgoing[id] && outgoing[id].length;
|
|||
|
|
});
|
|||
|
|
var sourceBadges = {};
|
|||
|
|
sourceIds.forEach(function (id) { sourceBadges[id] = viewerText('viewer.route.finder.source.badge'); });
|
|||
|
|
return {
|
|||
|
|
kind: 'route-source',
|
|||
|
|
title: viewerText('viewer.route.finder.source.title'),
|
|||
|
|
placeholder: viewerText('viewer.route.finder.source.placeholder'),
|
|||
|
|
empty: viewerText('viewer.route.finder.source.empty'),
|
|||
|
|
resultsLabel: viewerText('viewer.route.finder.source.results'),
|
|||
|
|
availableNoun: viewerText('viewer.route.finder.source.noun'),
|
|||
|
|
allowedIds: sourceIds,
|
|||
|
|
badges: sourceBadges
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
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 = {};
|
|||
|
|
targetIds.forEach(function (id) {
|
|||
|
|
targetBadges[id] = viewerCount('viewer.route.hop', distances[id]);
|
|||
|
|
});
|
|||
|
|
return {
|
|||
|
|
kind: 'route-target',
|
|||
|
|
title: viewerText('viewer.route.finder.target.title', { label: nodeLabel(byId[startId], startId) }),
|
|||
|
|
placeholder: viewerText('viewer.route.finder.target.placeholder'),
|
|||
|
|
empty: viewerText('viewer.route.finder.target.empty'),
|
|||
|
|
resultsLabel: viewerText('viewer.route.finder.target.results'),
|
|||
|
|
availableNoun: viewerText('viewer.route.finder.target.noun'),
|
|||
|
|
allowedIds: targetIds,
|
|||
|
|
badges: targetBadges
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
function finderOpening() {
|
|||
|
|
panel.setAttribute('data-finder-open', 'true');
|
|||
|
|
}
|
|||
|
|
function finderClosed(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
panel.removeAttribute('data-finder-open');
|
|||
|
|
requestDocking();
|
|||
|
|
if (options.restoreFocus === true && !findBtn.hidden) findBtn.focus();
|
|||
|
|
}
|
|||
|
|
function openFinder() {
|
|||
|
|
var context = finderContext();
|
|||
|
|
if (!context || !Archify.finder) return false;
|
|||
|
|
return Archify.finder.open({ context: context });
|
|||
|
|
}
|
|||
|
|
function fallbackCopy(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 copyLink() {
|
|||
|
|
if (mode !== 'result') return Promise.resolve(false);
|
|||
|
|
var value = location.href.replace(/#.*$/, '') + '#route=' + encodeURIComponent(startId) + '~' + encodeURIComponent(endId);
|
|||
|
|
var copy = navigator.clipboard && typeof navigator.clipboard.writeText === 'function'
|
|||
|
|
? navigator.clipboard.writeText(value).then(function () { return true; }).catch(function () { return fallbackCopy(value); })
|
|||
|
|
: Promise.resolve(fallbackCopy(value));
|
|||
|
|
return copy.then(function (copied) {
|
|||
|
|
copyBtn.textContent = viewerText(copied ? 'viewer.common.copied' : 'viewer.common.copyFailed');
|
|||
|
|
copyBtn.setAttribute('aria-label', viewerText(copied ? 'viewer.route.copy.success' : 'viewer.route.copy.failed'));
|
|||
|
|
window.setTimeout(function () {
|
|||
|
|
copyBtn.textContent = viewerText('viewer.route.copy');
|
|||
|
|
copyBtn.setAttribute('aria-label', viewerText('viewer.route.copy.aria'));
|
|||
|
|
}, 1600);
|
|||
|
|
return copied;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function interceptSelection(event) {
|
|||
|
|
if (mode !== 'source' && mode !== 'target') return;
|
|||
|
|
if (container.getAttribute('data-just-panned') === 'true') return;
|
|||
|
|
var node = event.target.closest('[data-node-id]');
|
|||
|
|
if (!node) return;
|
|||
|
|
if (event.type === 'keydown' && event.key !== 'Enter' && event.key !== ' ') return;
|
|||
|
|
event.preventDefault();
|
|||
|
|
event.stopImmediatePropagation();
|
|||
|
|
choose(node.getAttribute('data-node-id'));
|
|||
|
|
}
|
|||
|
|
function syncFromHash() {
|
|||
|
|
try {
|
|||
|
|
var params = new URLSearchParams(location.hash.replace(/^#/, ''));
|
|||
|
|
var route = params.get('route');
|
|||
|
|
if (!route) {
|
|||
|
|
if (mode !== 'idle') clear({ updateUrl: false, restoreFocus: false });
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
var parts = route.split('~');
|
|||
|
|
if (parts.length !== 2) return;
|
|||
|
|
var byId = nodesById();
|
|||
|
|
if (!byId[parts[0]] || !byId[parts[1]]) return;
|
|||
|
|
begin({ source: parts[0], focusNode: false });
|
|||
|
|
choose(parts[1], { updateUrl: false });
|
|||
|
|
} catch (_) {}
|
|||
|
|
}
|
|||
|
|
function escapeRoute(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (journeyPlaying) {
|
|||
|
|
pauseJourney({ preserveElapsed: true });
|
|||
|
|
if (options.restoreFocus === true) journeyPlayBtn.focus();
|
|||
|
|
return 'paused';
|
|||
|
|
}
|
|||
|
|
if (journeyIndex >= 0) {
|
|||
|
|
showJourneyOverview({ reveal: true });
|
|||
|
|
if (options.restoreFocus === true) journeyOverviewBtn.focus();
|
|||
|
|
return 'overview';
|
|||
|
|
}
|
|||
|
|
clear({ restoreFocus: options.restoreFocus === true });
|
|||
|
|
return 'cleared';
|
|||
|
|
}
|
|||
|
|
function syncJourneyMotion() {
|
|||
|
|
if (journeyPlaying && !journeyMotionAllowed()) pauseJourney({ preserveElapsed: true });
|
|||
|
|
renderJourneyControls();
|
|||
|
|
return journeyMotionAllowed();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
trigger.addEventListener('click', function () { toggle({ focusNode: false }); });
|
|||
|
|
findBtn.addEventListener('click', openFinder);
|
|||
|
|
clearBtn.addEventListener('click', function () { clear({ restoreFocus: true }); });
|
|||
|
|
copyBtn.addEventListener('click', copyLink);
|
|||
|
|
journeyPrevBtn.addEventListener('click', previousJourney);
|
|||
|
|
journeyPlayBtn.addEventListener('click', toggleJourney);
|
|||
|
|
journeyNextBtn.addEventListener('click', nextJourney);
|
|||
|
|
journeyOverviewBtn.addEventListener('click', function () { showJourneyOverview({ reveal: true }); });
|
|||
|
|
path.addEventListener('click', function (event) {
|
|||
|
|
var button = event.target.closest('[data-route-journey-index]');
|
|||
|
|
if (!button) return;
|
|||
|
|
selectJourneyIndex(Number(button.getAttribute('data-route-journey-index')));
|
|||
|
|
});
|
|||
|
|
path.addEventListener('focusin', function (event) {
|
|||
|
|
if (journeyPlaying && event.target.closest('[data-route-journey-index]')) pauseJourney({ preserveElapsed: true });
|
|||
|
|
});
|
|||
|
|
path.addEventListener('keydown', function (event) {
|
|||
|
|
var button = event.target.closest('[data-route-journey-index]');
|
|||
|
|
if (!button) return;
|
|||
|
|
var index = Number(button.getAttribute('data-route-journey-index'));
|
|||
|
|
var next = null;
|
|||
|
|
if (event.key === 'ArrowRight') next = Math.min(activeNodeIds.length - 1, index + 1);
|
|||
|
|
else if (event.key === 'ArrowLeft') next = Math.max(0, index - 1);
|
|||
|
|
else if (event.key === 'Home') next = 0;
|
|||
|
|
else if (event.key === 'End') next = activeNodeIds.length - 1;
|
|||
|
|
if (next !== null) {
|
|||
|
|
event.preventDefault();
|
|||
|
|
focusJourneyButton(next);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|||
|
|
event.preventDefault();
|
|||
|
|
selectJourneyIndex(index);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
svg.addEventListener('click', interceptSelection, true);
|
|||
|
|
svg.addEventListener('keydown', interceptSelection, true);
|
|||
|
|
container.addEventListener('scroll', updateDocking, { passive: true });
|
|||
|
|
window.addEventListener('resize', requestDocking);
|
|||
|
|
window.addEventListener('beforeprint', function () {
|
|||
|
|
if (journeyPlaying) pauseJourney({ preserveElapsed: true, reason: 'print' });
|
|||
|
|
else removeJourneyPulse();
|
|||
|
|
});
|
|||
|
|
window.addEventListener('hashchange', function () { requestAnimationFrame(syncFromHash); });
|
|||
|
|
syncFromHash();
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
begin: begin,
|
|||
|
|
choose: choose,
|
|||
|
|
clear: clear,
|
|||
|
|
toggle: toggle,
|
|||
|
|
escape: escapeRoute,
|
|||
|
|
copyLink: copyLink,
|
|||
|
|
playJourney: playJourney,
|
|||
|
|
pauseJourney: pauseJourney,
|
|||
|
|
showOverview: showJourneyOverview,
|
|||
|
|
selectJourneyIndex: selectJourneyIndex,
|
|||
|
|
syncMotion: syncJourneyMotion,
|
|||
|
|
isJourneyPlaying: function () { return journeyPlaying; },
|
|||
|
|
finderContext: finderContext,
|
|||
|
|
finderOpening: finderOpening,
|
|||
|
|
finderClosed: finderClosed,
|
|||
|
|
openFinder: openFinder,
|
|||
|
|
exportSnapshot: exportSnapshot,
|
|||
|
|
active: function () { return mode === 'idle' ? null : mode; },
|
|||
|
|
result: function () {
|
|||
|
|
return mode === 'result'
|
|||
|
|
? { source: startId, target: endId, nodes: activeNodeIds.slice(), hops: activeEdges.length, journey: journeyIndex, playing: journeyPlaying }
|
|||
|
|
: null;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* ============================================================
|
|||
|
|
Semantic Lens — a counted, viewer-only legend over data-node-kind.
|
|||
|
|
One selected kind reveals every authored relationship touching it.
|
|||
|
|
Two selected kinds compare only direct cross-kind relationships while
|
|||
|
|
preserving the full diagram as a dimmed spatial reference.
|
|||
|
|
============================================================ */
|
|||
|
|
Archify.semanticLens = (function () {
|
|||
|
|
var html = document.documentElement;
|
|||
|
|
var container = document.querySelector('.diagram-container');
|
|||
|
|
var svg = container.querySelector(':scope > svg');
|
|||
|
|
var trigger = document.getElementById('btn-semantic-lens');
|
|||
|
|
var panel = document.getElementById('semantic-lens');
|
|||
|
|
var closeBtn = document.getElementById('semantic-lens-close');
|
|||
|
|
var kindsRoot = document.getElementById('semantic-lens-kinds');
|
|||
|
|
var status = document.getElementById('semantic-lens-status');
|
|||
|
|
var copyBtn = document.getElementById('semantic-lens-copy');
|
|||
|
|
var clearBtn = document.getElementById('semantic-lens-clear');
|
|||
|
|
var legendBridge = svg.querySelector('[data-legend-bridge]');
|
|||
|
|
var legendEntries = [];
|
|||
|
|
var hoveredLegendEntry = null;
|
|||
|
|
var focusedLegendEntry = null;
|
|||
|
|
var activeLegendPreview = null;
|
|||
|
|
var lensOpener = trigger;
|
|||
|
|
var selectedKinds = [];
|
|||
|
|
var namespace = 'http://www.w3.org/2000/svg';
|
|||
|
|
var finePointerQuery = window.matchMedia ? window.matchMedia('(hover: hover) and (pointer: fine)') : null;
|
|||
|
|
var MAX_LENS_FLOW_EDGES = 24;
|
|||
|
|
|
|||
|
|
function nodesById() {
|
|||
|
|
var byId = {};
|
|||
|
|
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;
|
|||
|
|
});
|
|||
|
|
return byId;
|
|||
|
|
}
|
|||
|
|
function collectKinds() {
|
|||
|
|
var kinds = {};
|
|||
|
|
var byId = nodesById();
|
|||
|
|
Object.keys(byId).forEach(function (id) {
|
|||
|
|
var node = byId[id];
|
|||
|
|
var value = node.getAttribute('data-node-kind') || 'neutral';
|
|||
|
|
var kind = kinds[value] || (kinds[value] = { id: value, label: viewerKindLabel(value), nodes: [] });
|
|||
|
|
kind.nodes.push(node);
|
|||
|
|
});
|
|||
|
|
return Object.keys(kinds).map(function (key) { return kinds[key]; }).sort(function (a, b) {
|
|||
|
|
return b.nodes.length - a.nodes.length || a.label.localeCompare(b.label);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function edgeGroups() {
|
|||
|
|
var groups = {};
|
|||
|
|
Array.prototype.forEach.call(svg.querySelectorAll('[data-edge-from][data-edge-to]'), function (edge) {
|
|||
|
|
var from = edge.getAttribute('data-edge-from');
|
|||
|
|
var to = edge.getAttribute('data-edge-to');
|
|||
|
|
var key = edge.getAttribute('data-edge-key') || [from, to, edge.getAttribute('data-edge-label') || ''].join('\u0000');
|
|||
|
|
if (!groups[key]) groups[key] = { key: key, from: from, to: to, members: [] };
|
|||
|
|
groups[key].members.push(edge);
|
|||
|
|
});
|
|||
|
|
return Object.keys(groups).map(function (key) { return groups[key]; });
|
|||
|
|
}
|
|||
|
|
function edgeShapes(edge) {
|
|||
|
|
if (!edge) return [];
|
|||
|
|
if (/^(path|line|polyline)$/i.test(edge.tagName || '')) return [edge];
|
|||
|
|
return Array.prototype.slice.call(edge.querySelectorAll('path, line, polyline'));
|
|||
|
|
}
|
|||
|
|
function legendSourceBounds(entry) {
|
|||
|
|
var bounds = null;
|
|||
|
|
Array.prototype.forEach.call(entry.children, function (child) {
|
|||
|
|
if (child.hasAttribute('data-legend-bridge-runtime')) return;
|
|||
|
|
var box;
|
|||
|
|
try { box = child.getBBox(); } catch (_) { return; }
|
|||
|
|
if (!box || !Number.isFinite(box.x) || !Number.isFinite(box.y)) return;
|
|||
|
|
var next = { left: box.x, top: box.y, right: box.x + box.width, bottom: box.y + box.height };
|
|||
|
|
if (!bounds) bounds = next;
|
|||
|
|
else {
|
|||
|
|
bounds.left = Math.min(bounds.left, next.left);
|
|||
|
|
bounds.top = Math.min(bounds.top, next.top);
|
|||
|
|
bounds.right = Math.max(bounds.right, next.right);
|
|||
|
|
bounds.bottom = Math.max(bounds.bottom, next.bottom);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
return bounds;
|
|||
|
|
}
|
|||
|
|
function layoutLegendEntry(entry) {
|
|||
|
|
var bounds = legendSourceBounds(entry);
|
|||
|
|
var hit = entry.querySelector('[data-legend-hit]');
|
|||
|
|
var badge = entry.querySelector('[data-legend-count-badge]');
|
|||
|
|
if (!bounds || !hit || !badge) return false;
|
|||
|
|
var count = entry.getAttribute('data-legend-count') || '0';
|
|||
|
|
var centerY = (bounds.top + bounds.bottom) / 2;
|
|||
|
|
var badgeWidth = Math.max(14, count.length * 5 + 8);
|
|||
|
|
var badgeX = bounds.right + 3;
|
|||
|
|
var hitX = bounds.left - 5;
|
|||
|
|
var hitRight = badgeX + badgeWidth + 4;
|
|||
|
|
hit.setAttribute('x', hitX.toFixed(2));
|
|||
|
|
hit.setAttribute('y', (centerY - 12).toFixed(2));
|
|||
|
|
hit.setAttribute('width', Math.max(24, hitRight - hitX).toFixed(2));
|
|||
|
|
hit.setAttribute('height', '24');
|
|||
|
|
hit.setAttribute('rx', '4');
|
|||
|
|
var badgeRect = badge.querySelector('rect');
|
|||
|
|
var badgeText = badge.querySelector('text');
|
|||
|
|
badgeRect.setAttribute('x', badgeX.toFixed(2));
|
|||
|
|
badgeRect.setAttribute('y', (centerY - 7).toFixed(2));
|
|||
|
|
badgeRect.setAttribute('width', badgeWidth.toFixed(2));
|
|||
|
|
badgeRect.setAttribute('height', '14');
|
|||
|
|
badgeRect.setAttribute('rx', '7');
|
|||
|
|
badgeText.setAttribute('x', (badgeX + badgeWidth / 2).toFixed(2));
|
|||
|
|
badgeText.setAttribute('y', (centerY + 2.5).toFixed(2));
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function layoutLegendBridge() {
|
|||
|
|
legendEntries.forEach(layoutLegendEntry);
|
|||
|
|
if (!legendBridge) return;
|
|||
|
|
Array.prototype.forEach.call(legendBridge.querySelectorAll('[data-legend-zero]'), layoutLegendEntry);
|
|||
|
|
}
|
|||
|
|
function decorateLegendBridge() {
|
|||
|
|
legendEntries = [];
|
|||
|
|
if (!legendBridge || html.getAttribute('data-embed') === 'true') return false;
|
|||
|
|
var facts = {};
|
|||
|
|
collectKinds().forEach(function (kind) { facts[kind.id] = kind; });
|
|||
|
|
Array.prototype.forEach.call(legendBridge.querySelectorAll('[data-legend-kind]'), function (entry) {
|
|||
|
|
var kind = entry.getAttribute('data-legend-kind');
|
|||
|
|
var fact = facts[kind];
|
|||
|
|
var count = fact ? fact.nodes.length : 0;
|
|||
|
|
entry.setAttribute('data-legend-count', String(count));
|
|||
|
|
var hit = document.createElementNS(namespace, 'rect');
|
|||
|
|
hit.setAttribute('data-legend-bridge-runtime', '');
|
|||
|
|
hit.setAttribute('data-legend-hit', '');
|
|||
|
|
hit.setAttribute('aria-hidden', 'true');
|
|||
|
|
entry.insertBefore(hit, entry.firstChild);
|
|||
|
|
var badge = document.createElementNS(namespace, 'g');
|
|||
|
|
badge.setAttribute('data-legend-bridge-runtime', '');
|
|||
|
|
badge.setAttribute('data-legend-count-badge', '');
|
|||
|
|
badge.setAttribute('aria-hidden', 'true');
|
|||
|
|
badge.appendChild(document.createElementNS(namespace, 'rect'));
|
|||
|
|
var countText = document.createElementNS(namespace, 'text');
|
|||
|
|
countText.textContent = String(count);
|
|||
|
|
badge.appendChild(countText);
|
|||
|
|
entry.appendChild(badge);
|
|||
|
|
if (!fact || !count) {
|
|||
|
|
entry.setAttribute('data-legend-zero', '');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
entry.setAttribute('role', 'button');
|
|||
|
|
entry.setAttribute('tabindex', legendEntries.length ? '-1' : '0');
|
|||
|
|
var visibleLabel = entry.getAttribute('data-legend-label') || fact.label;
|
|||
|
|
entry.setAttribute('aria-label', viewerCount('viewer.lens.legend.inspect', count, { label: visibleLabel }));
|
|||
|
|
entry.setAttribute('aria-pressed', 'false');
|
|||
|
|
entry.setAttribute('aria-haspopup', 'dialog');
|
|||
|
|
entry.setAttribute('aria-controls', 'semantic-lens');
|
|||
|
|
entry.setAttribute('aria-expanded', 'false');
|
|||
|
|
legendEntries.push(entry);
|
|||
|
|
});
|
|||
|
|
if (!legendEntries.length) return false;
|
|||
|
|
legendBridge.setAttribute('role', legendEntries.length >= 3 ? 'toolbar' : 'group');
|
|||
|
|
legendBridge.setAttribute('aria-label', viewerText('viewer.lens.legend'));
|
|||
|
|
syncLegendBridge();
|
|||
|
|
layoutLegendBridge();
|
|||
|
|
try {
|
|||
|
|
if (document.fonts && document.fonts.ready) document.fonts.ready.then(layoutLegendBridge);
|
|||
|
|
} catch (_) {}
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function syncLegendBridge() {
|
|||
|
|
legendEntries.forEach(function (entry) {
|
|||
|
|
var selected = selectedKinds.indexOf(entry.getAttribute('data-legend-kind')) >= 0;
|
|||
|
|
entry.setAttribute('aria-pressed', selected ? 'true' : 'false');
|
|||
|
|
entry.setAttribute('aria-expanded', !panel.hidden && lensOpener === entry ? 'true' : 'false');
|
|||
|
|
if (selected) entry.setAttribute('data-legend-selected', '');
|
|||
|
|
else entry.removeAttribute('data-legend-selected');
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function clearLegendPreview() {
|
|||
|
|
activeLegendPreview = null;
|
|||
|
|
svg.removeAttribute('data-legend-preview-active');
|
|||
|
|
Array.prototype.forEach.call(svg.querySelectorAll('[data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer]'), function (element) {
|
|||
|
|
element.removeAttribute('data-legend-preview-match');
|
|||
|
|
element.removeAttribute('data-legend-preview-selected');
|
|||
|
|
element.removeAttribute('data-legend-preview-peer');
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function strongerLegendOwnerActive() {
|
|||
|
|
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');
|
|||
|
|
}
|
|||
|
|
function previewLegendKind(entry) {
|
|||
|
|
clearLegendPreview();
|
|||
|
|
if (!entry || strongerLegendOwnerActive()) return false;
|
|||
|
|
var kind = entry.getAttribute('data-legend-kind');
|
|||
|
|
var byId = nodesById();
|
|||
|
|
var matches = Object.keys(byId).filter(function (id) {
|
|||
|
|
return (byId[id].getAttribute('data-node-kind') || 'neutral') === kind;
|
|||
|
|
});
|
|||
|
|
if (!matches.length) return false;
|
|||
|
|
var chosen = {};
|
|||
|
|
matches.forEach(function (id) {
|
|||
|
|
chosen[id] = true;
|
|||
|
|
byId[id].setAttribute('data-legend-preview-match', '');
|
|||
|
|
byId[id].setAttribute('data-legend-preview-selected', '');
|
|||
|
|
});
|
|||
|
|
edgeGroups().forEach(function (edge) {
|
|||
|
|
if (!chosen[edge.from] && !chosen[edge.to]) return;
|
|||
|
|
edge.members.forEach(function (member) { member.setAttribute('data-legend-preview-match', ''); });
|
|||
|
|
[edge.from, edge.to].forEach(function (id) {
|
|||
|
|
if (!byId[id] || chosen[id]) return;
|
|||
|
|
byId[id].setAttribute('data-legend-preview-match', '');
|
|||
|
|
byId[id].setAttribute('data-legend-preview-peer', '');
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
svg.setAttribute('data-legend-preview-active', kind);
|
|||
|
|
activeLegendPreview = entry;
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function syncLegendPreview() {
|
|||
|
|
var next = focusedLegendEntry || hoveredLegendEntry;
|
|||
|
|
if (next === activeLegendPreview) return;
|
|||
|
|
clearLegendPreview();
|
|||
|
|
if (next) previewLegendKind(next);
|
|||
|
|
}
|
|||
|
|
function activateLegendEntry(entry) {
|
|||
|
|
if (!entry || entry.getAttribute('role') !== 'button') return false;
|
|||
|
|
clearLegendPreview();
|
|||
|
|
select(entry.getAttribute('data-legend-kind'));
|
|||
|
|
return open({ opener: entry });
|
|||
|
|
}
|
|||
|
|
function removeFlowOverlay() {
|
|||
|
|
Array.prototype.forEach.call(svg.querySelectorAll('[data-semantic-lens-overlay]'), function (element) {
|
|||
|
|
element.remove();
|
|||
|
|
});
|
|||
|
|
svg.removeAttribute('data-lens-flow-count');
|
|||
|
|
svg.removeAttribute('data-lens-flow-density');
|
|||
|
|
}
|
|||
|
|
function flowGeometry(shape, direction, step) {
|
|||
|
|
var clone = shape.cloneNode(false);
|
|||
|
|
clone.removeAttribute('id');
|
|||
|
|
clone.removeAttribute('class');
|
|||
|
|
clone.removeAttribute('style');
|
|||
|
|
clone.removeAttribute('marker-start');
|
|||
|
|
clone.removeAttribute('marker-mid');
|
|||
|
|
clone.removeAttribute('marker-end');
|
|||
|
|
clone.removeAttribute('role');
|
|||
|
|
clone.removeAttribute('aria-label');
|
|||
|
|
clone.removeAttribute('aria-hidden');
|
|||
|
|
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.removeAttribute('data-lens-match');
|
|||
|
|
clone.setAttribute('class', 'semantic-lens-flow');
|
|||
|
|
clone.setAttribute('data-direction', direction);
|
|||
|
|
clone.setAttribute('pathLength', '1');
|
|||
|
|
clone.style.setProperty('--lens-flow-delay', (step * 0.08).toFixed(2) + 's');
|
|||
|
|
return clone;
|
|||
|
|
}
|
|||
|
|
function renderFlowOverlay(entries) {
|
|||
|
|
removeFlowOverlay();
|
|||
|
|
svg.setAttribute('data-lens-flow-count', String(entries.length));
|
|||
|
|
if (!entries.length || html.getAttribute('data-embed') === 'true') return false;
|
|||
|
|
if (entries.length > MAX_LENS_FLOW_EDGES) {
|
|||
|
|
svg.setAttribute('data-lens-flow-density', 'quiet');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
var overlay = document.createElementNS(namespace, 'g');
|
|||
|
|
overlay.setAttribute('class', 'semantic-lens-overlay');
|
|||
|
|
overlay.setAttribute('data-semantic-lens-overlay', '');
|
|||
|
|
overlay.setAttribute('aria-hidden', 'true');
|
|||
|
|
entries.forEach(function (entry, step) {
|
|||
|
|
var wrapper = document.createElementNS(namespace, 'g');
|
|||
|
|
if (entry.edge.members[0].hasAttribute('transform')) {
|
|||
|
|
wrapper.setAttribute('transform', entry.edge.members[0].getAttribute('transform'));
|
|||
|
|
}
|
|||
|
|
edgeShapes(entry.edge.members[0]).forEach(function (shape) {
|
|||
|
|
wrapper.appendChild(flowGeometry(shape, entry.direction, step));
|
|||
|
|
});
|
|||
|
|
if (wrapper.childNodes.length) overlay.appendChild(wrapper);
|
|||
|
|
});
|
|||
|
|
if (!overlay.childNodes.length) return false;
|
|||
|
|
var firstNode = svg.querySelector('[data-node-id]');
|
|||
|
|
if (firstNode) svg.insertBefore(overlay, firstNode);
|
|||
|
|
else svg.appendChild(overlay);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function cleanSvgState() {
|
|||
|
|
removeFlowOverlay();
|
|||
|
|
svg.removeAttribute('data-lens-active');
|
|||
|
|
Array.prototype.forEach.call(svg.querySelectorAll('[data-lens-match], [data-lens-selected], [data-lens-peer]'), function (element) {
|
|||
|
|
element.removeAttribute('data-lens-match');
|
|||
|
|
element.removeAttribute('data-lens-selected');
|
|||
|
|
element.removeAttribute('data-lens-peer');
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function renderKinds() {
|
|||
|
|
kindsRoot.textContent = '';
|
|||
|
|
collectKinds().forEach(function (kind) {
|
|||
|
|
var button = document.createElement('button');
|
|||
|
|
button.type = 'button';
|
|||
|
|
button.className = 'semantic-lens-kind';
|
|||
|
|
button.setAttribute('data-kind', kind.id);
|
|||
|
|
button.setAttribute('aria-pressed', selectedKinds.indexOf(kind.id) >= 0 ? 'true' : 'false');
|
|||
|
|
button.setAttribute('aria-label', viewerCount('viewer.lens.kind.count', kind.nodes.length, { label: kind.label }));
|
|||
|
|
button.disabled = selectedKinds.length >= 2 && selectedKinds.indexOf(kind.id) === -1;
|
|||
|
|
var swatch = document.createElement('span');
|
|||
|
|
swatch.className = 'semantic-lens-swatch';
|
|||
|
|
swatch.setAttribute('aria-hidden', 'true');
|
|||
|
|
var label = document.createElement('strong');
|
|||
|
|
label.textContent = kind.label;
|
|||
|
|
var count = document.createElement('em');
|
|||
|
|
count.textContent = String(kind.nodes.length);
|
|||
|
|
button.appendChild(swatch);
|
|||
|
|
button.appendChild(label);
|
|||
|
|
button.appendChild(count);
|
|||
|
|
kindsRoot.appendChild(button);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function updateHash() {
|
|||
|
|
try {
|
|||
|
|
var hash = selectedKinds.length
|
|||
|
|
? '#lens=' + selectedKinds.map(encodeURIComponent).join('~')
|
|||
|
|
: '';
|
|||
|
|
history.replaceState(null, '', location.pathname + location.search + hash);
|
|||
|
|
} catch (_) {}
|
|||
|
|
}
|
|||
|
|
function updateTrigger() {
|
|||
|
|
var active = selectedKinds.length > 0;
|
|||
|
|
trigger.setAttribute('aria-pressed', active ? 'true' : 'false');
|
|||
|
|
trigger.setAttribute('aria-label', viewerText(active ? 'viewer.lens.openActive' : 'viewer.lens.open'));
|
|||
|
|
copyBtn.hidden = !active;
|
|||
|
|
syncLegendBridge();
|
|||
|
|
}
|
|||
|
|
function overlapArea(a, b) {
|
|||
|
|
var width = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left));
|
|||
|
|
var height = Math.max(0, Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top));
|
|||
|
|
return width * height;
|
|||
|
|
}
|
|||
|
|
function dockPanel(byId) {
|
|||
|
|
panel.removeAttribute('data-dock-side');
|
|||
|
|
if (panel.hidden || window.innerWidth <= 720) return 'right';
|
|||
|
|
var panelRect = panel.getBoundingClientRect();
|
|||
|
|
var containerRect = container.getBoundingClientRect();
|
|||
|
|
if (!panelRect.width || !panelRect.height) return 'right';
|
|||
|
|
var selectedRects = Object.keys(byId).filter(function (id) {
|
|||
|
|
return selectedKinds.indexOf(byId[id].getAttribute('data-node-kind') || 'neutral') >= 0;
|
|||
|
|
}).map(function (id) { return byId[id].getBoundingClientRect(); });
|
|||
|
|
var top = panelRect.top;
|
|||
|
|
var width = panelRect.width;
|
|||
|
|
var leftCandidate = { left: containerRect.left + 16, right: containerRect.left + 16 + width, top: top, bottom: panelRect.bottom };
|
|||
|
|
var rightCandidate = { left: containerRect.right - 16 - width, right: containerRect.right - 16, top: top, bottom: panelRect.bottom };
|
|||
|
|
var leftScore = selectedRects.reduce(function (score, rect) { return score + overlapArea(leftCandidate, rect); }, 0);
|
|||
|
|
var rightScore = selectedRects.reduce(function (score, rect) { return score + overlapArea(rightCandidate, rect); }, 0);
|
|||
|
|
var legend = svg.querySelector('[data-legend]');
|
|||
|
|
var nav = container.querySelector('.diagram-nav');
|
|||
|
|
var protectedRects = [legend, nav].filter(function (element) {
|
|||
|
|
return element && !element.hidden && window.getComputedStyle(element).display !== 'none';
|
|||
|
|
}).map(function (element) { return element.getBoundingClientRect(); });
|
|||
|
|
leftScore += protectedRects.reduce(function (score, rect) {
|
|||
|
|
return score + overlapArea(leftCandidate, rect) * 1000;
|
|||
|
|
}, 0);
|
|||
|
|
rightScore += protectedRects.reduce(function (score, rect) {
|
|||
|
|
return score + overlapArea(rightCandidate, rect) * 1000;
|
|||
|
|
}, 0);
|
|||
|
|
var side = leftScore < rightScore ? 'left' : 'right';
|
|||
|
|
panel.setAttribute('data-dock-side', side);
|
|||
|
|
return side;
|
|||
|
|
}
|
|||
|
|
function applySelection(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
cleanSvgState();
|
|||
|
|
var byId = nodesById();
|
|||
|
|
var nodeKind = {};
|
|||
|
|
Object.keys(byId).forEach(function (id) { nodeKind[id] = byId[id].getAttribute('data-node-kind') || 'neutral'; });
|
|||
|
|
if (!selectedKinds.length) {
|
|||
|
|
status.textContent = viewerText('viewer.lens.choose');
|
|||
|
|
updateTrigger();
|
|||
|
|
renderKinds();
|
|||
|
|
dockPanel(byId);
|
|||
|
|
if (options.updateUrl !== false) updateHash();
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var chosen = {};
|
|||
|
|
selectedKinds.forEach(function (kind) { chosen[kind] = true; });
|
|||
|
|
Object.keys(byId).forEach(function (id) {
|
|||
|
|
if (!chosen[nodeKind[id]]) return;
|
|||
|
|
byId[id].setAttribute('data-lens-match', '');
|
|||
|
|
byId[id].setAttribute('data-lens-selected', '');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
var touching = 0;
|
|||
|
|
var forward = 0;
|
|||
|
|
var reverse = 0;
|
|||
|
|
var crossKind = selectedKinds.length === 2;
|
|||
|
|
var matchedFlow = [];
|
|||
|
|
edgeGroups().forEach(function (edge) {
|
|||
|
|
var fromKind = nodeKind[edge.from];
|
|||
|
|
var toKind = nodeKind[edge.to];
|
|||
|
|
var match = false;
|
|||
|
|
if (crossKind) {
|
|||
|
|
if (fromKind === selectedKinds[0] && toKind === selectedKinds[1]) { match = true; forward += 1; }
|
|||
|
|
if (fromKind === selectedKinds[1] && toKind === selectedKinds[0]) { match = true; reverse += 1; }
|
|||
|
|
} else if (fromKind === selectedKinds[0] || toKind === selectedKinds[0]) {
|
|||
|
|
match = true;
|
|||
|
|
touching += 1;
|
|||
|
|
}
|
|||
|
|
if (!match) return;
|
|||
|
|
var direction = 'within';
|
|||
|
|
if (crossKind) {
|
|||
|
|
direction = fromKind === selectedKinds[0] ? 'forward' : 'reverse';
|
|||
|
|
} else {
|
|||
|
|
direction = fromKind === selectedKinds[0] && toKind !== selectedKinds[0] ? 'out'
|
|||
|
|
: (toKind === selectedKinds[0] && fromKind !== selectedKinds[0] ? 'in' : 'within');
|
|||
|
|
}
|
|||
|
|
matchedFlow.push({ edge: edge, direction: direction });
|
|||
|
|
edge.members.forEach(function (member) { member.setAttribute('data-lens-match', ''); });
|
|||
|
|
if (!crossKind) {
|
|||
|
|
[edge.from, edge.to].forEach(function (id) {
|
|||
|
|
if (!byId[id]) return;
|
|||
|
|
byId[id].setAttribute('data-lens-match', '');
|
|||
|
|
if (!chosen[nodeKind[id]]) byId[id].setAttribute('data-lens-peer', '');
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
svg.setAttribute('data-lens-active', selectedKinds.join(' '));
|
|||
|
|
renderFlowOverlay(matchedFlow);
|
|||
|
|
if (crossKind) {
|
|||
|
|
var total = forward + reverse;
|
|||
|
|
status.textContent = viewerCount('viewer.lens.compare', total, {
|
|||
|
|
first: viewerKindLabel(selectedKinds[0]),
|
|||
|
|
second: viewerKindLabel(selectedKinds[1]),
|
|||
|
|
forward: forward,
|
|||
|
|
reverse: reverse
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
var nodeCount = collectKinds().filter(function (kind) { return kind.id === selectedKinds[0]; })[0].nodes.length;
|
|||
|
|
status.textContent = viewerText('viewer.lens.single', {
|
|||
|
|
nodes: viewerCount('viewer.lens.node', nodeCount, { label: viewerKindLabel(selectedKinds[0]) }),
|
|||
|
|
relationships: viewerCount('viewer.lens.relationship', touching)
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
updateTrigger();
|
|||
|
|
renderKinds();
|
|||
|
|
dockPanel(byId);
|
|||
|
|
if (options.updateUrl !== false) updateHash();
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function prepareForLens() {
|
|||
|
|
clearLegendPreview();
|
|||
|
|
if (Archify.focus && typeof Archify.focus.clear === 'function') {
|
|||
|
|
Archify.focus.clear({ updateUrl: false, preserveView: true });
|
|||
|
|
}
|
|||
|
|
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 });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
function select(kind, options) {
|
|||
|
|
options = options || {};
|
|||
|
|
clearLegendPreview();
|
|||
|
|
var exists = collectKinds().some(function (entry) { return entry.id === kind; });
|
|||
|
|
if (!exists) return false;
|
|||
|
|
var index = selectedKinds.indexOf(kind);
|
|||
|
|
if (index >= 0) selectedKinds.splice(index, 1);
|
|||
|
|
else {
|
|||
|
|
if (selectedKinds.length >= 2) return false;
|
|||
|
|
if (!selectedKinds.length) prepareForLens();
|
|||
|
|
selectedKinds.push(kind);
|
|||
|
|
}
|
|||
|
|
return applySelection(options);
|
|||
|
|
}
|
|||
|
|
function close(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
panel.hidden = true;
|
|||
|
|
trigger.setAttribute('aria-expanded', 'false');
|
|||
|
|
updateTrigger();
|
|||
|
|
if (options.restoreFocus !== false && lensOpener && typeof lensOpener.focus === 'function') lensOpener.focus();
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
function open(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
if (html.getAttribute('data-embed') === 'true') return false;
|
|||
|
|
lensOpener = options.opener || trigger;
|
|||
|
|
if (Archify.exportMenu && Archify.exportMenu.isOpen()) Archify.exportMenu.close(false);
|
|||
|
|
if (Archify.finder && Archify.finder.isOpen()) Archify.finder.close({ restoreFocus: false });
|
|||
|
|
if (Archify.radar && Archify.radar.isOpen()) Archify.radar.close({ restoreFocus: false });
|
|||
|
|
if (Archify.guide && Archify.guide.isOpen()) Archify.guide.close({ restoreFocus: false });
|
|||
|
|
renderKinds();
|
|||
|
|
panel.hidden = false;
|
|||
|
|
trigger.setAttribute('aria-expanded', 'true');
|
|||
|
|
updateTrigger();
|
|||
|
|
requestAnimationFrame(function () {
|
|||
|
|
dockPanel(nodesById());
|
|||
|
|
var activeButton = kindsRoot.querySelector('[aria-pressed="true"]');
|
|||
|
|
var firstButton = activeButton || kindsRoot.querySelector('button:not(:disabled)');
|
|||
|
|
if (firstButton) firstButton.focus();
|
|||
|
|
});
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
function toggle() { return panel.hidden ? open({ opener: trigger }) : close(); }
|
|||
|
|
function clear(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
selectedKinds = [];
|
|||
|
|
cleanSvgState();
|
|||
|
|
panel.removeAttribute('data-dock-side');
|
|||
|
|
updateTrigger();
|
|||
|
|
renderKinds();
|
|||
|
|
status.textContent = viewerText('viewer.lens.choose');
|
|||
|
|
if (options.updateUrl !== false) updateHash();
|
|||
|
|
if (options.preserveView !== true && Archify.view && typeof Archify.view.reset === 'function') {
|
|||
|
|
Archify.view.reset({ automatic: true });
|
|||
|
|
}
|
|||
|
|
if (options.closePanel === true) close({ restoreFocus: false });
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
function fallbackCopy(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 copyLink() {
|
|||
|
|
if (!selectedKinds.length) return Promise.resolve(false);
|
|||
|
|
var value = location.href.replace(/#.*$/, '') + '#lens=' + selectedKinds.map(encodeURIComponent).join('~');
|
|||
|
|
var copy = navigator.clipboard && typeof navigator.clipboard.writeText === 'function'
|
|||
|
|
? navigator.clipboard.writeText(value).then(function () { return true; }).catch(function () { return fallbackCopy(value); })
|
|||
|
|
: Promise.resolve(fallbackCopy(value));
|
|||
|
|
return copy.then(function (copied) {
|
|||
|
|
copyBtn.textContent = viewerText(copied ? 'viewer.common.copied' : 'viewer.common.copyFailed');
|
|||
|
|
window.setTimeout(function () { copyBtn.textContent = viewerText('viewer.common.copyLink'); }, 1600);
|
|||
|
|
return copied;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
function syncFromHash() {
|
|||
|
|
try {
|
|||
|
|
var params = new URLSearchParams(location.hash.replace(/^#/, ''));
|
|||
|
|
var value = params.get('lens');
|
|||
|
|
if (!value) {
|
|||
|
|
if (selectedKinds.length) clear({ updateUrl: false, preserveView: true });
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
var available = collectKinds().map(function (kind) { return kind.id; });
|
|||
|
|
var requested = value.split('~').filter(function (kind, index, list) {
|
|||
|
|
return available.indexOf(kind) >= 0 && list.indexOf(kind) === index;
|
|||
|
|
}).slice(0, 2);
|
|||
|
|
if (!requested.length) return;
|
|||
|
|
prepareForLens();
|
|||
|
|
selectedKinds = requested;
|
|||
|
|
applySelection({ updateUrl: false });
|
|||
|
|
} catch (_) {}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
trigger.addEventListener('click', toggle);
|
|||
|
|
if (decorateLegendBridge()) {
|
|||
|
|
legendBridge.addEventListener('click', function (event) {
|
|||
|
|
var entry = event.target.closest('[data-legend-kind][role="button"]');
|
|||
|
|
if (entry) activateLegendEntry(entry);
|
|||
|
|
});
|
|||
|
|
legendBridge.addEventListener('pointerover', function (event) {
|
|||
|
|
if (event.pointerType === 'touch' || (finePointerQuery && !finePointerQuery.matches)) return;
|
|||
|
|
var entry = event.target.closest('[data-legend-kind][role="button"]');
|
|||
|
|
if (!entry || (event.relatedTarget && entry.contains(event.relatedTarget))) return;
|
|||
|
|
hoveredLegendEntry = entry;
|
|||
|
|
syncLegendPreview();
|
|||
|
|
});
|
|||
|
|
legendBridge.addEventListener('pointerout', function (event) {
|
|||
|
|
var entry = event.target.closest('[data-legend-kind][role="button"]');
|
|||
|
|
if (!entry || (event.relatedTarget && entry.contains(event.relatedTarget))) return;
|
|||
|
|
if (hoveredLegendEntry === entry) hoveredLegendEntry = null;
|
|||
|
|
syncLegendPreview();
|
|||
|
|
});
|
|||
|
|
legendBridge.addEventListener('focusin', function (event) {
|
|||
|
|
var entry = event.target.closest('[data-legend-kind][role="button"]');
|
|||
|
|
if (!entry) return;
|
|||
|
|
focusedLegendEntry = entry;
|
|||
|
|
legendEntries.forEach(function (candidate) {
|
|||
|
|
candidate.setAttribute('tabindex', candidate === entry ? '0' : '-1');
|
|||
|
|
});
|
|||
|
|
syncLegendPreview();
|
|||
|
|
});
|
|||
|
|
legendBridge.addEventListener('focusout', function (event) {
|
|||
|
|
var entry = event.target.closest('[data-legend-kind][role="button"]');
|
|||
|
|
if (!entry || (event.relatedTarget && entry.contains(event.relatedTarget))) return;
|
|||
|
|
if (focusedLegendEntry === entry) focusedLegendEntry = null;
|
|||
|
|
syncLegendPreview();
|
|||
|
|
});
|
|||
|
|
legendBridge.addEventListener('keydown', function (event) {
|
|||
|
|
var entry = event.target.closest('[data-legend-kind][role="button"]');
|
|||
|
|
if (!entry) return;
|
|||
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|||
|
|
event.preventDefault();
|
|||
|
|
activateLegendEntry(entry);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
var index = legendEntries.indexOf(entry);
|
|||
|
|
var next = null;
|
|||
|
|
if (event.key === 'ArrowRight') next = (index + 1) % legendEntries.length;
|
|||
|
|
else if (event.key === 'ArrowLeft') next = (index - 1 + legendEntries.length) % legendEntries.length;
|
|||
|
|
else if (event.key === 'Home') next = 0;
|
|||
|
|
else if (event.key === 'End') next = legendEntries.length - 1;
|
|||
|
|
if (next === null) return;
|
|||
|
|
event.preventDefault();
|
|||
|
|
legendEntries.forEach(function (candidate, candidateIndex) {
|
|||
|
|
candidate.setAttribute('tabindex', candidateIndex === next ? '0' : '-1');
|
|||
|
|
});
|
|||
|
|
legendEntries[next].focus();
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
closeBtn.addEventListener('click', function () { close(); });
|
|||
|
|
clearBtn.addEventListener('click', function () { clear({ preserveView: true }); });
|
|||
|
|
copyBtn.addEventListener('click', copyLink);
|
|||
|
|
kindsRoot.addEventListener('click', function (event) {
|
|||
|
|
var button = event.target.closest('[data-kind]');
|
|||
|
|
if (!button || button.disabled) return;
|
|||
|
|
select(button.getAttribute('data-kind'));
|
|||
|
|
});
|
|||
|
|
kindsRoot.addEventListener('keydown', function (event) {
|
|||
|
|
var buttons = Array.prototype.slice.call(kindsRoot.querySelectorAll('button:not(:disabled)'));
|
|||
|
|
var index = buttons.indexOf(document.activeElement);
|
|||
|
|
var next = null;
|
|||
|
|
if (index >= 0 && (event.key === 'ArrowRight' || event.key === 'ArrowDown')) next = (index + 1) % buttons.length;
|
|||
|
|
else if (index >= 0 && (event.key === 'ArrowLeft' || event.key === 'ArrowUp')) next = (index - 1 + buttons.length) % buttons.length;
|
|||
|
|
else if (event.key === 'Home' && buttons.length) next = 0;
|
|||
|
|
else if (event.key === 'End' && buttons.length) next = buttons.length - 1;
|
|||
|
|
if (next !== null) { event.preventDefault(); buttons[next].focus(); }
|
|||
|
|
});
|
|||
|
|
panel.addEventListener('keydown', function (event) {
|
|||
|
|
if (event.key !== 'Escape') return;
|
|||
|
|
event.preventDefault();
|
|||
|
|
event.stopPropagation();
|
|||
|
|
close();
|
|||
|
|
});
|
|||
|
|
document.addEventListener('click', function (event) {
|
|||
|
|
var eventPath = typeof event.composedPath === 'function' ? event.composedPath() : [];
|
|||
|
|
var clickedInside = eventPath.indexOf(panel) >= 0 || panel.contains(event.target);
|
|||
|
|
var clickedLauncher = event.target === trigger || legendEntries.some(function (entry) {
|
|||
|
|
return event.target === entry || entry.contains(event.target);
|
|||
|
|
});
|
|||
|
|
if (!panel.hidden && !clickedInside && !clickedLauncher) close({ restoreFocus: false });
|
|||
|
|
});
|
|||
|
|
window.addEventListener('hashchange', syncFromHash);
|
|||
|
|
window.addEventListener('resize', function () {
|
|||
|
|
if (!panel.hidden && selectedKinds.length) dockPanel(nodesById());
|
|||
|
|
layoutLegendBridge();
|
|||
|
|
});
|
|||
|
|
renderKinds();
|
|||
|
|
syncFromHash();
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
open: open,
|
|||
|
|
close: close,
|
|||
|
|
toggle: toggle,
|
|||
|
|
clear: clear,
|
|||
|
|
clearPreview: clearLegendPreview,
|
|||
|
|
select: select,
|
|||
|
|
copyLink: copyLink,
|
|||
|
|
isOpen: function () { return !panel.hidden; },
|
|||
|
|
active: function () { return selectedKinds.length ? selectedKinds.slice() : null; },
|
|||
|
|
kinds: function () { return collectKinds().map(function (kind) { return { id: kind.id, count: kind.nodes.length }; }); }
|
|||
|
|
};
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* ============================================================
|
|||
|
|
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
|
|||
|
|
camera without adding a second command implementation or SVG state.
|
|||
|
|
============================================================ */
|
|||
|
|
Archify.guide = (function () {
|
|||
|
|
var html = document.documentElement;
|
|||
|
|
var container = document.querySelector('.diagram-container');
|
|||
|
|
var svg = container.querySelector(':scope > svg');
|
|||
|
|
var trigger = document.getElementById('btn-diagram-guide');
|
|||
|
|
var panel = document.getElementById('diagram-guide');
|
|||
|
|
var closeBtn = document.getElementById('diagram-guide-close');
|
|||
|
|
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');
|
|||
|
|
|
|||
|
|
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) {
|
|||
|
|
var key = edge.getAttribute('data-edge-key') || [
|
|||
|
|
edge.getAttribute('data-edge-from'),
|
|||
|
|
edge.getAttribute('data-edge-to'),
|
|||
|
|
edge.getAttribute('data-edge-label') || ''
|
|||
|
|
].join('\u0000');
|
|||
|
|
seen[key] = true;
|
|||
|
|
});
|
|||
|
|
return Object.keys(seen).length;
|
|||
|
|
}
|
|||
|
|
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)
|
|||
|
|
});
|
|||
|
|
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)'));
|
|||
|
|
}
|
|||
|
|
function close(options) {
|
|||
|
|
options = options || {};
|
|||
|
|
panel.hidden = true;
|
|||
|
|
html.removeAttribute('data-guide-open');
|
|||
|
|
routePanel.removeAttribute('data-guide-open');
|
|||
|
|
trigger.setAttribute('aria-expanded', 'false');
|
|||
|
|
trigger.setAttribute('aria-label', viewerText('viewer.guide.open'));
|
|||
|
|
feedback.textContent = '';
|
|||
|
|
if (options.restoreFocus !== false) trigger.focus();
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
function open() {
|
|||
|
|
if (html.getAttribute('data-embed') === 'true') return false;
|
|||
|
|
if (Archify.semanticLens && typeof Archify.semanticLens.clearPreview === 'function') Archify.semanticLens.clearPreview();
|
|||
|
|
if (Archify.exportMenu && Archify.exportMenu.isOpen()) Archify.exportMenu.close(false);
|
|||
|
|
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' });
|
|||
|
|
}
|
|||
|
|
renderFacts();
|
|||
|
|
panel.hidden = false;
|
|||
|
|
html.setAttribute('data-guide-open', 'true');
|
|||
|
|
routePanel.setAttribute('data-guide-open', 'true');
|
|||
|
|
trigger.setAttribute('aria-expanded', 'true');
|
|||
|
|
trigger.setAttribute('aria-label', viewerText('viewer.guide.close'));
|
|||
|
|
feedback.textContent = '';
|
|||
|
|
requestAnimationFrame(function () {
|
|||
|
|
var first = actionButtons()[0];
|
|||
|
|
if (first) first.focus();
|
|||
|
|
});
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
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();
|
|||
|
|
if (action === 'preset') return Archify.preset.cycle();
|
|||
|
|
if (action === 'reset') return Archify.view.reset();
|
|||
|
|
if (action === 'zoom-in') return Archify.view.zoomIn();
|
|||
|
|
if (action === 'zoom-out') return Archify.view.zoomOut();
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
function actionForKey(key) {
|
|||
|
|
return {
|
|||
|
|
'/': 'find',
|
|||
|
|
r: 'route',
|
|||
|
|
m: 'map',
|
|||
|
|
l: 'lens',
|
|||
|
|
p: 'story',
|
|||
|
|
f: 'present',
|
|||
|
|
e: 'export',
|
|||
|
|
t: 'theme',
|
|||
|
|
s: 'preset',
|
|||
|
|
'0': 'reset',
|
|||
|
|
'+': 'zoom-in',
|
|||
|
|
'=': 'zoom-in',
|
|||
|
|
'-': 'zoom-out'
|
|||
|
|
}[key] || null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
trigger.addEventListener('click', toggle);
|
|||
|
|
closeBtn.addEventListener('click', function () { close(); });
|
|||
|
|
actions.addEventListener('click', function (event) {
|
|||
|
|
var button = event.target.closest('[data-guide-action]');
|
|||
|
|
if (!button || button.disabled) return;
|
|||
|
|
event.stopPropagation();
|
|||
|
|
execute(button.getAttribute('data-guide-action'));
|
|||
|
|
});
|
|||
|
|
panel.addEventListener('keydown', function (event) {
|
|||
|
|
if (event.key === 'Escape' || event.key === '?') {
|
|||
|
|
event.preventDefault();
|
|||
|
|
event.stopPropagation();
|
|||
|
|
close();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
var buttons = actionButtons();
|
|||
|
|
var index = buttons.indexOf(document.activeElement);
|
|||
|
|
var next = null;
|
|||
|
|
if (index >= 0 && event.key === 'ArrowRight') next = (index + 1) % buttons.length;
|
|||
|
|
else if (index >= 0 && event.key === 'ArrowDown') next = (index + 1) % buttons.length;
|
|||
|
|
else if (index >= 0 && event.key === 'ArrowLeft') next = (index - 1 + buttons.length) % buttons.length;
|
|||
|
|
else if (index >= 0 && event.key === 'ArrowUp') next = (index - 1 + buttons.length) % buttons.length;
|
|||
|
|
else if (event.key === 'Home' && buttons.length) next = 0;
|
|||
|
|
else if (event.key === 'End' && buttons.length) next = buttons.length - 1;
|
|||
|
|
if (next !== null) {
|
|||
|
|
event.preventDefault();
|
|||
|
|
event.stopPropagation();
|
|||
|
|
buttons[next].focus();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
var action = actionForKey(event.key.length === 1 ? event.key.toLowerCase() : event.key);
|
|||
|
|
if (!action) return;
|
|||
|
|
event.preventDefault();
|
|||
|
|
event.stopPropagation();
|
|||
|
|
execute(action);
|
|||
|
|
});
|
|||
|
|
document.addEventListener('click', function (event) {
|
|||
|
|
if (!panel.hidden && !panel.contains(event.target) && event.target !== trigger) close({ restoreFocus: false });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
renderFacts();
|
|||
|
|
return {
|
|||
|
|
open: open,
|
|||
|
|
close: close,
|
|||
|
|
toggle: toggle,
|
|||
|
|
execute: execute,
|
|||
|
|
isOpen: function () { return !panel.hidden; },
|
|||
|
|
facts: function () {
|
|||
|
|
return {
|
|||
|
|
nodes: svg.querySelectorAll('[data-node-id]').length,
|
|||
|
|
relationships: relationshipCount(),
|
|||
|
|
views: viewCount()
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
})();
|
|||
|
|
|
|||
|
|
/* ============================================================
|
|||
|
|
Global keyboard shortcuts
|
|||
|
|
? -> open Diagram Guide
|
|||
|
|
T -> toggle theme
|
|||
|
|
S -> cycle visual style
|
|||
|
|
E -> open export menu
|
|||
|
|
F -> toggle presentation stage
|
|||
|
|
M -> toggle Semantic Radar
|
|||
|
|
L -> toggle Semantic Lens
|
|||
|
|
R -> start/clear Route Probe
|
|||
|
|
/ -> open node finder or the active route endpoint picker
|
|||
|
|
+ / - -> zoom, 0 -> reset view
|
|||
|
|
Escape -> clear temporary trace/focus/view first, then exit presentation
|
|||
|
|
Ignored when focus is inside an input/textarea/contenteditable.
|
|||
|
|
============================================================ */
|
|||
|
|
document.addEventListener('keydown', function (e) {
|
|||
|
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|||
|
|
if (e.defaultPrevented) return;
|
|||
|
|
var t = e.target;
|
|||
|
|
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
|
|||
|
|
if (Archify.preset && Archify.preset.isOpen() && e.key !== 'Escape' && e.key !== 's' && e.key !== 'S') {
|
|||
|
|
Archify.preset.close(false);
|
|||
|
|
}
|
|||
|
|
if (e.key === '?') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.guide.toggle();
|
|||
|
|
} else if (e.key === '/') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.finder.open();
|
|||
|
|
} else if (e.key === 't' || e.key === 'T') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.theme.toggle();
|
|||
|
|
} else if (e.key === 's' || e.key === 'S') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.preset.cycle();
|
|||
|
|
} else if (e.key === 'e' || e.key === 'E') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
if (!Archify.exportMenu.isOpen()) Archify.exportMenu.open();
|
|||
|
|
} else if (e.key === 'f' || e.key === 'F') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.presentation.toggle();
|
|||
|
|
} else if (e.key === 'm' || e.key === 'M') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.radar.toggle();
|
|||
|
|
} else if (e.key === 'l' || e.key === 'L') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.semanticLens.toggle();
|
|||
|
|
} else if (e.key === 'r' || e.key === 'R') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.routeProbe.toggle({ focusNode: true });
|
|||
|
|
} else if (e.key === '+' || e.key === '=') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.view.zoomIn();
|
|||
|
|
} else if (e.key === '-') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.view.zoomOut();
|
|||
|
|
} else if (e.key === '0') {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.view.reset();
|
|||
|
|
} else if (e.key === 'Escape' && Archify.preset.isOpen()) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.preset.close(true);
|
|||
|
|
} else if (e.key === 'Escape' && Archify.semanticLens.isOpen()) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.semanticLens.close({ restoreFocus: true });
|
|||
|
|
} else if (e.key === 'Escape' && Archify.semanticLens.active()) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.semanticLens.clear({ preserveView: true });
|
|||
|
|
} else if (e.key === 'Escape' && Archify.guide.isOpen()) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.guide.close({ restoreFocus: true });
|
|||
|
|
} else if (e.key === 'Escape' && Archify.radar.isOpen()) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.radar.close({ restoreFocus: true });
|
|||
|
|
} else if (e.key === 'Escape' && Archify.routeProbe.active()) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.routeProbe.escape({ restoreFocus: true });
|
|||
|
|
} else if (e.key === 'Escape' && Archify.intentTrace.active()) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.intentTrace.clear();
|
|||
|
|
} else if (e.key === 'Escape' && Archify.focus.active()) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.focus.clear({ restoreFocus: true });
|
|||
|
|
} else if (e.key === 'Escape' && Archify.presentation.active()) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
Archify.presentation.exit();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|