ovtools 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +121 -2
- package/assets/common/bond-inspect.sh +368 -0
- package/assets/common/config.sh +263 -0
- package/assets/common/extract-data.sh +197 -0
- package/assets/common/harden-host.sh +37 -0
- package/assets/common/install-deps.sh +93 -0
- package/assets/common/node-enroll.sh +65 -0
- package/assets/common/node-setup.sh +150 -0
- package/assets/common/node-start.sh +90 -0
- package/assets/common/node-stop.sh +18 -0
- package/assets/common/node-upgrade.sh +45 -0
- package/assets/common/pre-run.sh +497 -0
- package/assets/common/promote-validator.sh +104 -0
- package/assets/deploy/10-prepare-host.sh +98 -0
- package/assets/deploy/20-install-toolchain.sh +93 -0
- package/assets/deploy/30-setup-node.sh +82 -0
- package/assets/deploy/40-start-node.sh +75 -0
- package/assets/deploy/50-verify.sh +259 -0
- package/assets/deploy/60-enroll.sh +304 -0
- package/assets/deploy/70-promote.sh +152 -0
- package/assets/deploy/lib.sh +398 -0
- package/assets/manage/10-bond-increase.sh +149 -0
- package/assets/manage/10-diagnose-node.sh +169 -0
- package/assets/manage/20-exit-validator.sh +259 -0
- package/assets/manage/25-recover-node.sh +291 -0
- package/assets/manage/30-status.sh +64 -0
- package/assets/manage/40-exit-withdraw.sh +133 -0
- package/assets/manage/bond_inc.sh +318 -0
- package/assets/manage/dashboard_monitor/app.js +1544 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/base.css +113 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/content.css +133 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/controls.css +229 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/data.css +374 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/icons.css +35 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/motion.css +57 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/shell.css +51 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/sidebar.css +242 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/components/topbar.css +138 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/fonts.css +30 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/synteq-ui-v2.css +25 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/theme-dark.css +38 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/theme-light.css +36 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/css/tokens.css +61 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-Medium.ttf +0 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-Regular.ttf +0 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-SemiBold.ttf +0 -0
- package/assets/manage/dashboard_monitor/assets/v2.2/js/synteq-ui-v2.js +869 -0
- package/assets/manage/dashboard_monitor/deploy-dashboard.sh +230 -0
- package/assets/manage/dashboard_monitor/favicon.svg +13 -0
- package/assets/manage/dashboard_monitor/index.html +1197 -0
- package/assets/manage/dashboard_monitor/nginx-octra.conf +76 -0
- package/assets/manage/dashboard_monitor/octra-dashboard.service +29 -0
- package/assets/manage/dashboard_monitor/octra-node-prune.cron +12 -0
- package/assets/manage/dashboard_monitor/octra-node-prune.service +16 -0
- package/assets/manage/dashboard_monitor/octra-node-prune.timer +12 -0
- package/assets/manage/dashboard_monitor/prune-node-junk.sh +329 -0
- package/assets/manage/dashboard_monitor/server.py +1887 -0
- package/assets/manage/dashboard_monitor/ssl-certbot.sh +90 -0
- package/assets/manage/dashboard_monitor/styles.css +327 -0
- package/assets/manage/lib.sh +347 -0
- package/assets/manifest.json +143 -0
- package/assets/migrate/00-backup-identity.sh +67 -0
- package/assets/migrate/01-prepare-target.sh +145 -0
- package/assets/migrate/02-authorize-pull.sh +43 -0
- package/assets/migrate/03-pull-state.sh +102 -0
- package/assets/migrate/04-stop-source.sh +85 -0
- package/assets/migrate/05-finalize-target.sh +140 -0
- package/assets/migrate/06-start-target.sh +57 -0
- package/assets/migrate/07-verify.sh +237 -0
- package/assets/migrate/08-rollback.sh +54 -0
- package/assets/migrate/lib.sh +149 -0
- package/assets/upgrade/10-preflight.sh +120 -0
- package/assets/upgrade/20-upgrade.sh +142 -0
- package/assets/upgrade/30-manual-upgrade.sh +129 -0
- package/assets/upgrade/40-storage-prune.sh +65 -0
- package/assets/upgrade/50-status.sh +58 -0
- package/assets/upgrade/lib.sh +232 -0
- package/bin/ovtools.js +32 -0
- package/dist/api/checks.d.ts +6 -0
- package/dist/api/checks.d.ts.map +1 -0
- package/dist/api/checks.js +173 -0
- package/dist/api/checks.js.map +1 -0
- package/dist/api/hostkeys.d.ts +10 -0
- package/dist/api/hostkeys.d.ts.map +1 -0
- package/dist/api/hostkeys.js +58 -0
- package/dist/api/hostkeys.js.map +1 -0
- package/dist/api/jobs.d.ts +33 -0
- package/dist/api/jobs.d.ts.map +1 -0
- package/dist/api/jobs.js +483 -0
- package/dist/api/jobs.js.map +1 -0
- package/dist/api/nodes.d.ts +16 -0
- package/dist/api/nodes.d.ts.map +1 -0
- package/dist/api/nodes.js +126 -0
- package/dist/api/nodes.js.map +1 -0
- package/dist/api/server.d.ts +3 -0
- package/dist/api/server.d.ts.map +1 -0
- package/dist/api/server.js +443 -0
- package/dist/api/server.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +301 -0
- package/dist/cli.js.map +1 -0
- package/dist/constants.d.ts +76 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +150 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator/base.d.ts +52 -0
- package/dist/orchestrator/base.d.ts.map +1 -0
- package/dist/orchestrator/base.js +219 -0
- package/dist/orchestrator/base.js.map +1 -0
- package/dist/orchestrator/deploy.d.ts +33 -0
- package/dist/orchestrator/deploy.d.ts.map +1 -0
- package/dist/orchestrator/deploy.js +313 -0
- package/dist/orchestrator/deploy.js.map +1 -0
- package/dist/orchestrator/manage.d.ts +28 -0
- package/dist/orchestrator/manage.d.ts.map +1 -0
- package/dist/orchestrator/manage.js +267 -0
- package/dist/orchestrator/manage.js.map +1 -0
- package/dist/orchestrator/migrate.d.ts +31 -0
- package/dist/orchestrator/migrate.d.ts.map +1 -0
- package/dist/orchestrator/migrate.js +442 -0
- package/dist/orchestrator/migrate.js.map +1 -0
- package/dist/ssh/client.d.ts +39 -0
- package/dist/ssh/client.d.ts.map +1 -0
- package/dist/ssh/client.js +418 -0
- package/dist/ssh/client.js.map +1 -0
- package/dist/test/core.test.d.ts +2 -0
- package/dist/test/core.test.d.ts.map +1 -0
- package/dist/test/core.test.js +188 -0
- package/dist/test/core.test.js.map +1 -0
- package/dist/types.d.ts +267 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/assets.d.ts +28 -0
- package/dist/utils/assets.d.ts.map +1 -0
- package/dist/utils/assets.js +583 -0
- package/dist/utils/assets.js.map +1 -0
- package/dist/utils/browser.d.ts +2 -0
- package/dist/utils/browser.d.ts.map +1 -0
- package/dist/utils/browser.js +23 -0
- package/dist/utils/browser.js.map +1 -0
- package/dist/utils/dashboard.d.ts +16 -0
- package/dist/utils/dashboard.d.ts.map +1 -0
- package/dist/utils/dashboard.js +177 -0
- package/dist/utils/dashboard.js.map +1 -0
- package/dist/utils/helpers.d.ts +29 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +204 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/scripts.d.ts +60 -0
- package/dist/utils/scripts.d.ts.map +1 -0
- package/dist/utils/scripts.js +174 -0
- package/dist/utils/scripts.js.map +1 -0
- package/dist/utils/state.d.ts +12 -0
- package/dist/utils/state.d.ts.map +1 -0
- package/dist/utils/state.js +44 -0
- package/dist/utils/state.js.map +1 -0
- package/dist/utils/tar.d.ts +11 -0
- package/dist/utils/tar.d.ts.map +1 -0
- package/dist/utils/tar.js +93 -0
- package/dist/utils/tar.js.map +1 -0
- package/dist/utils/update.d.ts +9 -0
- package/dist/utils/update.d.ts.map +1 -0
- package/dist/utils/update.js +134 -0
- package/dist/utils/update.js.map +1 -0
- package/dist/utils/websocket.d.ts +10 -0
- package/dist/utils/websocket.d.ts.map +1 -0
- package/dist/utils/websocket.js +203 -0
- package/dist/utils/websocket.js.map +1 -0
- package/package.json +55 -4
- package/public/app.js +2712 -0
- package/public/assets/v2.2/css/base.css +113 -0
- package/public/assets/v2.2/css/components/content.css +133 -0
- package/public/assets/v2.2/css/components/controls.css +229 -0
- package/public/assets/v2.2/css/components/data.css +373 -0
- package/public/assets/v2.2/css/components/icons.css +35 -0
- package/public/assets/v2.2/css/components/motion.css +57 -0
- package/public/assets/v2.2/css/components/shell.css +51 -0
- package/public/assets/v2.2/css/components/sidebar.css +242 -0
- package/public/assets/v2.2/css/components/topbar.css +138 -0
- package/public/assets/v2.2/css/fonts.css +30 -0
- package/public/assets/v2.2/css/synteq-ui-v2.css +25 -0
- package/public/assets/v2.2/css/theme-dark.css +38 -0
- package/public/assets/v2.2/css/theme-light.css +36 -0
- package/public/assets/v2.2/css/tokens.css +61 -0
- package/public/assets/v2.2/fonts/GoogleSansCode-Medium.ttf +0 -0
- package/public/assets/v2.2/fonts/GoogleSansCode-Regular.ttf +0 -0
- package/public/assets/v2.2/fonts/GoogleSansCode-SemiBold.ttf +0 -0
- package/public/assets/v2.2/js/synteq-ui-v2.js +869 -0
- package/public/favicon.svg +7 -0
- package/public/index.html +1080 -0
- package/public/styles.css +552 -0
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synteq UI V2 Runtime (V2/js/synteq-ui-v2.js)
|
|
3
|
+
* Zero-dependency vanilla JS helpers:
|
|
4
|
+
* - Theme toggle (dark / light, persisted via localStorage)
|
|
5
|
+
* - Sticky bottom navigation dock toggle and persisted visibility
|
|
6
|
+
* - Smooth gauge fill (width-based, uses CSS transition)
|
|
7
|
+
* - Mini bar chart renderer (network I/O or any dual-series data)
|
|
8
|
+
* - SVG sparkline renderer
|
|
9
|
+
* - Topbar scroll-shadow
|
|
10
|
+
* - Reduced-motion guard on all animations
|
|
11
|
+
* - Stagger mount trigger (IntersectionObserver)
|
|
12
|
+
* - Nav scroll + scroll-spy (BoundingClientRect-based, works inside
|
|
13
|
+
* any scrollable .v2-content container)
|
|
14
|
+
* - Clipboard copy buttons [data-v2-copy]
|
|
15
|
+
*/
|
|
16
|
+
(function (global) {
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
/* ---- Helpers ---- */
|
|
20
|
+
const $ = (sel, ctx) => (ctx || document).querySelector(sel);
|
|
21
|
+
const $$ = (sel, ctx) => Array.from((ctx || document).querySelectorAll(sel));
|
|
22
|
+
const prefersReduced = () =>
|
|
23
|
+
typeof matchMedia !== 'undefined' &&
|
|
24
|
+
matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
25
|
+
|
|
26
|
+
/* ======================================================
|
|
27
|
+
THEME
|
|
28
|
+
====================================================== */
|
|
29
|
+
function setTheme(theme) {
|
|
30
|
+
const t = theme === 'light' ? 'light' : 'dark';
|
|
31
|
+
const root = document.documentElement;
|
|
32
|
+
root.setAttribute('data-theme', t);
|
|
33
|
+
root.classList.remove('theme-dark', 'theme-light');
|
|
34
|
+
root.classList.add('theme-' + t);
|
|
35
|
+
try { localStorage.setItem('sq-v2-theme', t); } catch (_) {}
|
|
36
|
+
return t;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getTheme() {
|
|
40
|
+
return document.documentElement.getAttribute('data-theme') || 'dark';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function toggleTheme() {
|
|
44
|
+
return setTheme(getTheme() === 'dark' ? 'light' : 'dark');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function initTheme() {
|
|
48
|
+
let saved;
|
|
49
|
+
try { saved = localStorage.getItem('sq-v2-theme'); } catch (_) {}
|
|
50
|
+
if (saved) { setTheme(saved); return; }
|
|
51
|
+
const sys = typeof matchMedia !== 'undefined' && matchMedia('(prefers-color-scheme: light)').matches;
|
|
52
|
+
setTheme(sys ? 'light' : 'dark');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ======================================================
|
|
56
|
+
GAUGE — sets .v2-gauge__fill width via data-value
|
|
57
|
+
Usage: renderGauge(el, 72.5) // thresholds
|
|
58
|
+
renderGauge(el, 100, { kind:'brand' }) // quorum-style
|
|
59
|
+
renderGauge(el, 42, { kind:'neutral' })// no state colour
|
|
60
|
+
or set data-v2-gauge="72.5" and call initGauges()
|
|
61
|
+
====================================================== */
|
|
62
|
+
function renderGauge(fillEl, pct, opts) {
|
|
63
|
+
if (!fillEl) return;
|
|
64
|
+
var kind = (opts && opts.kind) || 'auto';
|
|
65
|
+
var clamped = Math.max(0, Math.min(100, pct));
|
|
66
|
+
// Remove state classes then apply the requested one
|
|
67
|
+
fillEl.classList.remove('v2-gauge__fill--ok', 'v2-gauge__fill--warn', 'v2-gauge__fill--err', 'v2-gauge__fill--brand');
|
|
68
|
+
if (kind === 'brand') {
|
|
69
|
+
fillEl.classList.add('v2-gauge__fill--brand');
|
|
70
|
+
} else if (kind !== 'neutral') {
|
|
71
|
+
if (clamped >= 90) fillEl.classList.add('v2-gauge__fill--err');
|
|
72
|
+
else if (clamped >= 75) fillEl.classList.add('v2-gauge__fill--warn');
|
|
73
|
+
else fillEl.classList.add('v2-gauge__fill--ok');
|
|
74
|
+
}
|
|
75
|
+
// Delay so CSS transition fires after paint
|
|
76
|
+
requestAnimationFrame(function () {
|
|
77
|
+
requestAnimationFrame(function () { fillEl.style.width = clamped + '%'; });
|
|
78
|
+
});
|
|
79
|
+
// Update aria on parent track
|
|
80
|
+
var track = fillEl.closest('[role="progressbar"]') || fillEl.parentElement;
|
|
81
|
+
if (track) track.setAttribute('aria-valuenow', String(Math.round(clamped)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function initGauges() {
|
|
85
|
+
$$('[data-v2-gauge]').forEach(function (track) {
|
|
86
|
+
const fill = track.querySelector('.v2-gauge__fill');
|
|
87
|
+
if (!fill) return;
|
|
88
|
+
const pct = parseFloat(track.getAttribute('data-v2-gauge')) || 0;
|
|
89
|
+
renderGauge(fill, pct);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* ======================================================
|
|
94
|
+
BARS — dual-series bar chart (in / out)
|
|
95
|
+
stageEl : .v2-bars container
|
|
96
|
+
samples : Array of [inPct, outPct] each 0–100
|
|
97
|
+
opts : { minCols, stepSec, emptyLabel }
|
|
98
|
+
minCols pad with empty columns so a short series still
|
|
99
|
+
renders slim bars instead of a few fat ones
|
|
100
|
+
stepSec seconds per sample, used for the tooltip label
|
|
101
|
+
====================================================== */
|
|
102
|
+
function renderBars(stageEl, samples, opts) {
|
|
103
|
+
if (!stageEl) return;
|
|
104
|
+
opts = opts || {};
|
|
105
|
+
samples = samples || [];
|
|
106
|
+
|
|
107
|
+
var minCols = Math.max(0, opts.minCols || 0);
|
|
108
|
+
var padCols = Math.max(0, minCols - samples.length);
|
|
109
|
+
var total = padCols + samples.length;
|
|
110
|
+
|
|
111
|
+
stageEl.innerHTML = '';
|
|
112
|
+
stageEl.style.setProperty('--v2-bars-cols', Math.max(1, total));
|
|
113
|
+
stageEl.classList.toggle('is-empty', samples.length === 0);
|
|
114
|
+
if (samples.length === 0) stageEl.setAttribute('data-v2-bars-empty', 'true');
|
|
115
|
+
else stageEl.removeAttribute('data-v2-bars-empty');
|
|
116
|
+
|
|
117
|
+
// Leading empty columns keep the chart geometry stable while data fills in
|
|
118
|
+
for (var p = 0; p < padCols; p++) {
|
|
119
|
+
var padCol = document.createElement('div');
|
|
120
|
+
padCol.className = 'v2-bars__col v2-bars__col--pad';
|
|
121
|
+
padCol.setAttribute('aria-hidden', 'true');
|
|
122
|
+
stageEl.appendChild(padCol);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
samples.forEach(function (pair, idx) {
|
|
126
|
+
var inPct = Math.max(0, Math.min(100, pair[0] || 0));
|
|
127
|
+
var outPct = Math.max(0, Math.min(100, pair[1] || 0));
|
|
128
|
+
var col = document.createElement('div');
|
|
129
|
+
col.className = 'v2-bars__col';
|
|
130
|
+
|
|
131
|
+
var offset = samples.length - idx; // 1 = newest
|
|
132
|
+
var label = opts.stepSec
|
|
133
|
+
? ('T-' + (offset * opts.stepSec) + 's')
|
|
134
|
+
: ('Sample -' + offset);
|
|
135
|
+
col.setAttribute('title',
|
|
136
|
+
label + ' ↓ ' + inPct.toFixed(1) + '% ↑ ' + outPct.toFixed(1) + '%');
|
|
137
|
+
|
|
138
|
+
var segOut = document.createElement('div');
|
|
139
|
+
segOut.className = 'v2-bars__seg v2-bars__seg--out';
|
|
140
|
+
segOut.style.height = outPct + '%';
|
|
141
|
+
|
|
142
|
+
var segIn = document.createElement('div');
|
|
143
|
+
segIn.className = 'v2-bars__seg v2-bars__seg--in';
|
|
144
|
+
segIn.style.height = inPct + '%';
|
|
145
|
+
|
|
146
|
+
col.appendChild(segOut);
|
|
147
|
+
col.appendChild(segIn);
|
|
148
|
+
stageEl.appendChild(col);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ======================================================
|
|
153
|
+
SPARKLINE — draws an SVG polyline inside .v2-spark
|
|
154
|
+
sparkEl : SVG element with class .v2-spark
|
|
155
|
+
values : Array of numbers (raw values, auto-scaled)
|
|
156
|
+
====================================================== */
|
|
157
|
+
function renderSparkline(sparkEl, values) {
|
|
158
|
+
if (!sparkEl) return;
|
|
159
|
+
if (!values || values.length < 2) {
|
|
160
|
+
// Not enough samples yet — clear the previous path and publish an empty
|
|
161
|
+
// state so consumers can render a placeholder instead of stale data.
|
|
162
|
+
while (sparkEl.firstChild) sparkEl.removeChild(sparkEl.firstChild);
|
|
163
|
+
sparkEl.__v2SparkValues = [];
|
|
164
|
+
sparkEl.setAttribute('data-v2-spark-empty', 'true');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
sparkEl.removeAttribute('data-v2-spark-empty');
|
|
168
|
+
// Clear previous
|
|
169
|
+
while (sparkEl.firstChild) sparkEl.removeChild(sparkEl.firstChild);
|
|
170
|
+
|
|
171
|
+
// Draw in viewBox units, NOT rendered pixels. Using clientWidth here made
|
|
172
|
+
// every point overflow a 200-unit viewBox (the path bled far outside the
|
|
173
|
+
// SVG, which showed up as a blank / clipped chart in the explorer).
|
|
174
|
+
const vb = sparkEl.viewBox && sparkEl.viewBox.baseVal;
|
|
175
|
+
const W = (vb && vb.width) || 200;
|
|
176
|
+
const H = (vb && vb.height) || 40;
|
|
177
|
+
if (!vb || !vb.width) sparkEl.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
|
|
178
|
+
|
|
179
|
+
// Remember the series so the sparkline can be redrawn on resize
|
|
180
|
+
sparkEl.__v2SparkValues = values.slice();
|
|
181
|
+
|
|
182
|
+
// Pad keeps the 1.5px stroke and the 2.5px end dot inside the viewBox
|
|
183
|
+
const pad = 4;
|
|
184
|
+
const min = Math.min.apply(null, values);
|
|
185
|
+
const max = Math.max.apply(null, values);
|
|
186
|
+
const range = max - min || 1;
|
|
187
|
+
|
|
188
|
+
function sx(i) { return pad + (i / (values.length - 1)) * (W - pad * 2); }
|
|
189
|
+
function sy(v) { return pad + (1 - (v - min) / range) * (H - pad * 2); }
|
|
190
|
+
|
|
191
|
+
const pts = values.map(function (v, i) { return sx(i) + ',' + sy(v); }).join(' ');
|
|
192
|
+
|
|
193
|
+
// Filled area
|
|
194
|
+
const first = sx(0) + ',' + sy(values[0]);
|
|
195
|
+
const last = sx(values.length - 1) + ',' + sy(values[values.length - 1]);
|
|
196
|
+
const area = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
|
|
197
|
+
area.setAttribute('class', 'v2-spark__area');
|
|
198
|
+
area.setAttribute('points',
|
|
199
|
+
first + ' ' + pts + ' ' + last + ' ' +
|
|
200
|
+
sx(values.length - 1) + ',' + H + ' ' + sx(0) + ',' + H);
|
|
201
|
+
sparkEl.appendChild(area);
|
|
202
|
+
|
|
203
|
+
// Line
|
|
204
|
+
const line = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
|
|
205
|
+
line.setAttribute('class', 'v2-spark__line');
|
|
206
|
+
line.setAttribute('points', pts);
|
|
207
|
+
sparkEl.appendChild(line);
|
|
208
|
+
|
|
209
|
+
// Latest-point dot
|
|
210
|
+
const lastIdx = values.length - 1;
|
|
211
|
+
const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
212
|
+
dot.setAttribute('class', 'v2-spark__dot');
|
|
213
|
+
dot.setAttribute('cx', sx(lastIdx));
|
|
214
|
+
dot.setAttribute('cy', sy(values[lastIdx]));
|
|
215
|
+
dot.setAttribute('r', '2.5');
|
|
216
|
+
sparkEl.appendChild(dot);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* Draws every .v2-spark from its remembered series (or data-v2-spark="1,2,3")
|
|
220
|
+
and redraws them once the viewport is resized. */
|
|
221
|
+
function initSparklines() {
|
|
222
|
+
$$('.v2-spark').forEach(function (el) {
|
|
223
|
+
if (!el.__v2SparkValues) {
|
|
224
|
+
var raw = el.getAttribute('data-v2-spark');
|
|
225
|
+
if (raw) {
|
|
226
|
+
el.__v2SparkValues = raw.split(',')
|
|
227
|
+
.map(function (n) { return parseFloat(n); })
|
|
228
|
+
.filter(function (n) { return !isNaN(n); });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (el.__v2SparkValues) renderSparkline(el, el.__v2SparkValues);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (initSparklines.bound) return;
|
|
235
|
+
initSparklines.bound = true;
|
|
236
|
+
var timer;
|
|
237
|
+
window.addEventListener('resize', function () {
|
|
238
|
+
clearTimeout(timer);
|
|
239
|
+
timer = setTimeout(function () {
|
|
240
|
+
$$('.v2-spark').forEach(function (el) {
|
|
241
|
+
if (el.__v2SparkValues) renderSparkline(el, el.__v2SparkValues);
|
|
242
|
+
});
|
|
243
|
+
}, 120);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function sidebarOpen() {
|
|
248
|
+
var app = document.getElementById('app') || $('.v2-app');
|
|
249
|
+
if (!app) return;
|
|
250
|
+
app.classList.remove('nav-collapsed');
|
|
251
|
+
$$('[data-v2-sidebar-toggle]').forEach(function (btn) { btn.setAttribute('aria-expanded', 'true'); });
|
|
252
|
+
try { localStorage.setItem('sq-v2-sidebar', 'open'); } catch (_) {}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function sidebarClose() {
|
|
256
|
+
var app = document.getElementById('app') || $('.v2-app');
|
|
257
|
+
if (!app) return;
|
|
258
|
+
app.classList.add('nav-collapsed');
|
|
259
|
+
$$('[data-v2-sidebar-toggle]').forEach(function (btn) { btn.setAttribute('aria-expanded', 'false'); });
|
|
260
|
+
try { localStorage.setItem('sq-v2-sidebar', 'closed'); } catch (_) {}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function sidebarToggle() {
|
|
264
|
+
var app = document.getElementById('app') || $('.v2-app');
|
|
265
|
+
if (!app) return;
|
|
266
|
+
app.classList.contains('nav-collapsed') ? sidebarOpen() : sidebarClose();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function initSidebar() {
|
|
270
|
+
$$('[data-v2-sidebar-toggle]').forEach(function (btn) {
|
|
271
|
+
btn.addEventListener('click', function (e) { e.stopPropagation(); sidebarToggle(); });
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
var app = document.getElementById('app') || $('.v2-app');
|
|
275
|
+
var saved; try { saved = localStorage.getItem('sq-v2-sidebar'); } catch (_) {}
|
|
276
|
+
if (saved === 'closed') sidebarClose(); else if (app) sidebarOpen();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* ======================================================
|
|
280
|
+
TOPBAR SCROLL SHADOW — adds .is-scrolled to .v2-topbar
|
|
281
|
+
when the in-content region scrolls past 4px
|
|
282
|
+
====================================================== */
|
|
283
|
+
function initScrollShadow() {
|
|
284
|
+
$$('.v2-content').forEach(function (content) {
|
|
285
|
+
var topbar = $('.v2-topbar');
|
|
286
|
+
if (!topbar) return;
|
|
287
|
+
content.addEventListener('scroll', function () {
|
|
288
|
+
topbar.classList.toggle('is-scrolled', content.scrollTop > 4);
|
|
289
|
+
}, { passive: true });
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* ======================================================
|
|
294
|
+
STAGGER MOUNT — triggers .v2-stagger animation when
|
|
295
|
+
the element scrolls into view (IntersectionObserver).
|
|
296
|
+
Skips animation if reduced-motion is active.
|
|
297
|
+
====================================================== */
|
|
298
|
+
function initStagger() {
|
|
299
|
+
if (prefersReduced()) return;
|
|
300
|
+
if (typeof IntersectionObserver === 'undefined') return;
|
|
301
|
+
var io = new IntersectionObserver(function (entries) {
|
|
302
|
+
entries.forEach(function (entry) {
|
|
303
|
+
if (entry.isIntersecting) {
|
|
304
|
+
entry.target.classList.add('v2-stagger');
|
|
305
|
+
io.unobserve(entry.target);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}, { threshold: 0.1 });
|
|
309
|
+
$$('[data-v2-stagger]').forEach(function (el) { io.observe(el); });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* ======================================================
|
|
313
|
+
THEME TOGGLE BUTTON — wire all [data-v2-toggle-theme]
|
|
314
|
+
====================================================== */
|
|
315
|
+
function initThemeToggles() {
|
|
316
|
+
$$('[data-v2-toggle-theme]').forEach(function (btn) {
|
|
317
|
+
btn.addEventListener('click', function () { toggleTheme(); });
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* ======================================================
|
|
322
|
+
TIP — transient anchored label ("Copied", "Paused", …)
|
|
323
|
+
tip(el, 'Copied') // ok, 1.5 s
|
|
324
|
+
tip(el, 'Copy failed', { kind: 'err' }) // warn | err | ok
|
|
325
|
+
Auto-positions above the anchor (below when there is no room) and removes
|
|
326
|
+
itself. Only one tip exists at a time, so rapid clicks never stack up.
|
|
327
|
+
====================================================== */
|
|
328
|
+
var _tipTimer = null;
|
|
329
|
+
|
|
330
|
+
function tip(anchor, message, opts) {
|
|
331
|
+
if (!anchor || !message || typeof document === 'undefined') return null;
|
|
332
|
+
opts = opts || {};
|
|
333
|
+
|
|
334
|
+
var existing = $('.v2-tip');
|
|
335
|
+
if (existing) existing.remove();
|
|
336
|
+
|
|
337
|
+
var el = document.createElement('div');
|
|
338
|
+
el.className = 'v2-tip' + (opts.kind ? ' v2-tip--' + opts.kind : '');
|
|
339
|
+
el.setAttribute('role', 'status');
|
|
340
|
+
el.textContent = String(message);
|
|
341
|
+
document.body.appendChild(el);
|
|
342
|
+
|
|
343
|
+
var rect = anchor.getBoundingClientRect();
|
|
344
|
+
var gap = 8;
|
|
345
|
+
var w = el.offsetWidth;
|
|
346
|
+
var h = el.offsetHeight;
|
|
347
|
+
var above = rect.top - h - gap;
|
|
348
|
+
var top = above > gap ? above : rect.bottom + gap;
|
|
349
|
+
var left = rect.left + rect.width / 2 - w / 2;
|
|
350
|
+
var maxLeft = Math.max(gap, window.innerWidth - w - gap);
|
|
351
|
+
left = Math.min(Math.max(gap, left), maxLeft);
|
|
352
|
+
|
|
353
|
+
el.style.top = Math.round(top) + 'px';
|
|
354
|
+
el.style.left = Math.round(left) + 'px';
|
|
355
|
+
|
|
356
|
+
if (_tipTimer) clearTimeout(_tipTimer);
|
|
357
|
+
_tipTimer = setTimeout(function () {
|
|
358
|
+
el.classList.add('is-out');
|
|
359
|
+
setTimeout(function () { el.remove(); }, 220);
|
|
360
|
+
}, opts.duration || 1500);
|
|
361
|
+
|
|
362
|
+
return el;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/* ======================================================
|
|
366
|
+
CLIPBOARD — delegated, with a fallback for insecure origins
|
|
367
|
+
Any element carrying [data-v2-copy] copies its value (or the text of its
|
|
368
|
+
previous sibling when the attribute is empty). Delegation means buttons
|
|
369
|
+
added or swapped in later keep working without re-initialisation.
|
|
370
|
+
Feedback: .v2-copy--ok + a "Copied" tip + aria-label swap.
|
|
371
|
+
====================================================== */
|
|
372
|
+
function copyToClipboard(text) {
|
|
373
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
374
|
+
return navigator.clipboard.writeText(text);
|
|
375
|
+
}
|
|
376
|
+
// http:// origins are not secure contexts: fall back to a temp field
|
|
377
|
+
return new Promise(function (resolve, reject) {
|
|
378
|
+
try {
|
|
379
|
+
var area = document.createElement('textarea');
|
|
380
|
+
area.value = text;
|
|
381
|
+
area.setAttribute('readonly', '');
|
|
382
|
+
area.style.position = 'fixed';
|
|
383
|
+
area.style.top = '-1000px';
|
|
384
|
+
area.style.opacity = '0';
|
|
385
|
+
document.body.appendChild(area);
|
|
386
|
+
area.select();
|
|
387
|
+
var ok = document.execCommand('copy');
|
|
388
|
+
document.body.removeChild(area);
|
|
389
|
+
if (ok) resolve(); else reject(new Error('copy-failed'));
|
|
390
|
+
} catch (err) {
|
|
391
|
+
reject(err);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function initCopyButtons() {
|
|
397
|
+
if (document.__v2CopyBound) return;
|
|
398
|
+
document.__v2CopyBound = true;
|
|
399
|
+
|
|
400
|
+
document.addEventListener('click', function (event) {
|
|
401
|
+
var btn = event.target && event.target.closest
|
|
402
|
+
? event.target.closest('[data-v2-copy]')
|
|
403
|
+
: null;
|
|
404
|
+
if (!btn) return;
|
|
405
|
+
|
|
406
|
+
var text = btn.getAttribute('data-v2-copy');
|
|
407
|
+
if (!text) {
|
|
408
|
+
var prev = btn.previousElementSibling;
|
|
409
|
+
text = prev ? prev.textContent : '';
|
|
410
|
+
}
|
|
411
|
+
text = (text || '').trim();
|
|
412
|
+
if (!text) return;
|
|
413
|
+
|
|
414
|
+
var label = btn.getAttribute('data-v2-copy-label') || 'Copied';
|
|
415
|
+
|
|
416
|
+
copyToClipboard(text).then(function () {
|
|
417
|
+
var orig = btn.getAttribute('aria-label') || 'Copy';
|
|
418
|
+
btn.setAttribute('aria-label', label);
|
|
419
|
+
btn.classList.add('v2-copy--ok');
|
|
420
|
+
tip(btn, label, { kind: 'ok' });
|
|
421
|
+
setTimeout(function () {
|
|
422
|
+
btn.setAttribute('aria-label', orig);
|
|
423
|
+
btn.classList.remove('v2-copy--ok');
|
|
424
|
+
}, 1800);
|
|
425
|
+
}).catch(function () {
|
|
426
|
+
tip(btn, 'Copy failed', { kind: 'err' });
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function toast(message) {
|
|
432
|
+
var region = $('[data-v2-toast-region]');
|
|
433
|
+
if (!region) {
|
|
434
|
+
region = document.createElement('div');
|
|
435
|
+
region.className = 'v2-toast-region';
|
|
436
|
+
region.setAttribute('data-v2-toast-region', '');
|
|
437
|
+
region.setAttribute('role', 'status');
|
|
438
|
+
region.setAttribute('aria-live', 'polite');
|
|
439
|
+
document.body.appendChild(region);
|
|
440
|
+
}
|
|
441
|
+
var item = document.createElement('div');
|
|
442
|
+
item.className = 'v2-toast';
|
|
443
|
+
item.textContent = message;
|
|
444
|
+
region.appendChild(item);
|
|
445
|
+
window.setTimeout(function () { item.remove(); }, 3200);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function initProductionControls() {
|
|
449
|
+
$$('[data-v2-toast]').forEach(function (button) {
|
|
450
|
+
button.addEventListener('click', function () { toast(button.getAttribute('data-v2-toast') || button.textContent.trim() + ' complete'); });
|
|
451
|
+
});
|
|
452
|
+
$$('[data-v2-dialog-open]').forEach(function (button) {
|
|
453
|
+
button.addEventListener('click', function () {
|
|
454
|
+
var dialog = document.getElementById(button.getAttribute('data-v2-dialog-open'));
|
|
455
|
+
if (dialog && typeof dialog.showModal === 'function') dialog.showModal();
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
$$('[data-v2-dialog-close]').forEach(function (button) {
|
|
459
|
+
button.addEventListener('click', function () { var dialog = button.closest('dialog'); if (dialog) dialog.close(); });
|
|
460
|
+
});
|
|
461
|
+
$$('[data-v2-search-trigger]').forEach(function (button) { button.addEventListener('click', function () { var input = $('[data-v2-search-input]'); if (input) { var dialog = input.closest('dialog'); if (dialog && !dialog.open) dialog.showModal(); input.focus(); } }); });
|
|
462
|
+
document.addEventListener('keydown', function (event) {
|
|
463
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
|
|
464
|
+
var trigger = $('[data-v2-search-trigger]');
|
|
465
|
+
if (trigger) { event.preventDefault(); trigger.click(); }
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
$$('[data-v2-search-input]').forEach(function (input) {
|
|
469
|
+
input.addEventListener('input', function () {
|
|
470
|
+
var query = input.value.trim().toLowerCase();
|
|
471
|
+
$$('.v2-command [data-v2-command]').forEach(function (item) { item.hidden = query && !item.textContent.toLowerCase().includes(query); });
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
$$('[data-v2-command]').forEach(function (item) {
|
|
475
|
+
item.addEventListener('click', function () {
|
|
476
|
+
var target = item.getAttribute('data-v2-command');
|
|
477
|
+
var dialog = item.closest('dialog');
|
|
478
|
+
if (dialog) dialog.close();
|
|
479
|
+
if (target && target.charAt(0) === '#') {
|
|
480
|
+
var section = document.querySelector(target);
|
|
481
|
+
var content = $('.v2-content');
|
|
482
|
+
if (section && content) {
|
|
483
|
+
// Rect-based math: offsetTop is relative to offsetParent, not the
|
|
484
|
+
// scroll container, and lands on the wrong section.
|
|
485
|
+
var delta = section.getBoundingClientRect().top - content.getBoundingClientRect().top + content.scrollTop - 16;
|
|
486
|
+
content.scrollTo({ top: Math.max(0, delta), behavior: 'smooth' });
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
else toast(item.textContent.trim() + ' selected');
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* ======================================================
|
|
495
|
+
BOTTOM DOCK — drag-to-scroll + "More" drop-up
|
|
496
|
+
• Items are safe-centred by CSS; when they overflow the strip can be
|
|
497
|
+
dragged (mouse / pen), wheel-scrolled horizontally, and a More button
|
|
498
|
+
opens a drop-up with every item that is currently out of view.
|
|
499
|
+
• Keeps the active item visible when the scroll-spy changes.
|
|
500
|
+
====================================================== */
|
|
501
|
+
function initNavDock() {
|
|
502
|
+
var aside = $('.v2-aside');
|
|
503
|
+
if (!aside) return;
|
|
504
|
+
var nav = $('.v2-nav', aside);
|
|
505
|
+
if (!nav) return;
|
|
506
|
+
if (nav.getAttribute('data-v2-dock-ready') === 'true') {
|
|
507
|
+
// Re-scan after dynamic nav changes (new destinations added/removed)
|
|
508
|
+
if (typeof nav.__v2DockRefresh === 'function') nav.__v2DockRefresh();
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
nav.setAttribute('data-v2-dock-ready', 'true');
|
|
512
|
+
|
|
513
|
+
var isActive = function (el) {
|
|
514
|
+
return el.classList.contains('is-active') || el.classList.contains('v2-nav__item--active');
|
|
515
|
+
};
|
|
516
|
+
var navItems = function () { return $$('.v2-nav__item', nav); };
|
|
517
|
+
var overflowing = function () { return nav.scrollWidth - nav.clientWidth > 1; };
|
|
518
|
+
|
|
519
|
+
/* ---- More trigger + drop-up shell ---- */
|
|
520
|
+
var more = document.createElement('button');
|
|
521
|
+
more.type = 'button';
|
|
522
|
+
more.className = 'v2-nav__more';
|
|
523
|
+
more.hidden = true;
|
|
524
|
+
more.setAttribute('aria-haspopup', 'menu');
|
|
525
|
+
more.setAttribute('aria-expanded', 'false');
|
|
526
|
+
more.setAttribute('aria-label', 'More navigation items');
|
|
527
|
+
more.innerHTML = '<svg class="v2-icon v2-icon--18" viewBox="0 0 24 24" aria-hidden="true"><circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/></svg><span aria-hidden="true">More</span>';
|
|
528
|
+
|
|
529
|
+
var menu = document.createElement('div');
|
|
530
|
+
menu.className = 'v2-nav__menu';
|
|
531
|
+
menu.hidden = true;
|
|
532
|
+
menu.setAttribute('role', 'menu');
|
|
533
|
+
menu.setAttribute('aria-label', 'All navigation items');
|
|
534
|
+
|
|
535
|
+
nav.parentNode.insertBefore(more, nav.nextSibling);
|
|
536
|
+
aside.appendChild(menu);
|
|
537
|
+
|
|
538
|
+
function closeMenu() {
|
|
539
|
+
if (menu.hidden) return;
|
|
540
|
+
menu.hidden = true;
|
|
541
|
+
more.setAttribute('aria-expanded', 'false');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function buildMenu() {
|
|
545
|
+
while (menu.firstChild) menu.removeChild(menu.firstChild);
|
|
546
|
+
var navRect = nav.getBoundingClientRect();
|
|
547
|
+
var left = navRect.left + 2;
|
|
548
|
+
var right = navRect.right - 2;
|
|
549
|
+
var outOfView = navItems().filter(function (item) {
|
|
550
|
+
var r = item.getBoundingClientRect();
|
|
551
|
+
return r.left < left - 1 || r.right > right + 1;
|
|
552
|
+
});
|
|
553
|
+
outOfView.forEach(function (src) {
|
|
554
|
+
var clone = src.cloneNode(true);
|
|
555
|
+
clone.classList.remove('v2-nav__item');
|
|
556
|
+
clone.classList.remove('v2-nav__item--active');
|
|
557
|
+
clone.classList.remove('is-active');
|
|
558
|
+
clone.classList.add('v2-nav__menu-item');
|
|
559
|
+
if (isActive(src)) clone.classList.add('is-active');
|
|
560
|
+
clone.removeAttribute('aria-current');
|
|
561
|
+
clone.setAttribute('role', 'menuitem');
|
|
562
|
+
clone.addEventListener('click', function (e) {
|
|
563
|
+
e.preventDefault();
|
|
564
|
+
closeMenu();
|
|
565
|
+
// Delegate to the real item so scroll-spy + smooth scroll stay in sync
|
|
566
|
+
src.click();
|
|
567
|
+
ensureVisible(src);
|
|
568
|
+
});
|
|
569
|
+
menu.appendChild(clone);
|
|
570
|
+
});
|
|
571
|
+
return outOfView.length;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function openMenu() {
|
|
575
|
+
if (!buildMenu()) return;
|
|
576
|
+
menu.hidden = false;
|
|
577
|
+
more.setAttribute('aria-expanded', 'true');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
more.addEventListener('click', function (e) {
|
|
581
|
+
e.stopPropagation();
|
|
582
|
+
menu.hidden ? openMenu() : closeMenu();
|
|
583
|
+
});
|
|
584
|
+
document.addEventListener('click', function (e) {
|
|
585
|
+
if (menu.hidden) return;
|
|
586
|
+
if (menu.contains(e.target) || more.contains(e.target)) return;
|
|
587
|
+
closeMenu();
|
|
588
|
+
});
|
|
589
|
+
document.addEventListener('keydown', function (e) {
|
|
590
|
+
if (e.key !== 'Escape' || menu.hidden) return;
|
|
591
|
+
var hadFocus = menu.contains(document.activeElement);
|
|
592
|
+
closeMenu();
|
|
593
|
+
if (hadFocus) more.focus();
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
/* ---- Keep the active item inside the visible strip ---- */
|
|
597
|
+
function ensureVisible(item) {
|
|
598
|
+
if (!item || !overflowing()) return;
|
|
599
|
+
var navRect = nav.getBoundingClientRect();
|
|
600
|
+
var r = item.getBoundingClientRect();
|
|
601
|
+
var pad = 8;
|
|
602
|
+
// Already comfortably in view — leave the strip alone
|
|
603
|
+
if (r.left >= navRect.left + pad && r.right <= navRect.right - pad) return;
|
|
604
|
+
// Otherwise centre it so the neighbouring destinations stay discoverable
|
|
605
|
+
var max = nav.scrollWidth - nav.clientWidth;
|
|
606
|
+
var centered = nav.scrollLeft + (r.left - navRect.left) - (navRect.width - r.width) / 2;
|
|
607
|
+
nav.scrollTo({
|
|
608
|
+
left: Math.max(0, Math.min(max, centered)),
|
|
609
|
+
behavior: prefersReduced() ? 'auto' : 'smooth'
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/* ---- Overflow state (More button + edge shadows) ---- */
|
|
614
|
+
function updateOverflow() {
|
|
615
|
+
var canOverflow = overflowing();
|
|
616
|
+
more.hidden = !canOverflow;
|
|
617
|
+
nav.classList.toggle('has-overflow', canOverflow);
|
|
618
|
+
if (!canOverflow) closeMenu();
|
|
619
|
+
var max = nav.scrollWidth - nav.clientWidth;
|
|
620
|
+
nav.setAttribute('data-overflow-start', String(max > 1 && nav.scrollLeft > 1));
|
|
621
|
+
nav.setAttribute('data-overflow-end', String(max > 1 && nav.scrollLeft < max - 1));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
var frame = 0;
|
|
625
|
+
nav.addEventListener('scroll', function () {
|
|
626
|
+
if (frame) return;
|
|
627
|
+
frame = requestAnimationFrame(function () { frame = 0; updateOverflow(); });
|
|
628
|
+
}, { passive: true });
|
|
629
|
+
|
|
630
|
+
/* ---- Drag-to-scroll (mouse / pen) ---- */
|
|
631
|
+
var drag = { active: false, moved: false, startX: 0, startLeft: 0 };
|
|
632
|
+
nav.addEventListener('pointerdown', function (e) {
|
|
633
|
+
if (e.pointerType === 'touch' || e.button !== 0 || !overflowing()) return;
|
|
634
|
+
drag.active = true;
|
|
635
|
+
drag.moved = false;
|
|
636
|
+
drag.startX = e.clientX;
|
|
637
|
+
drag.startLeft = nav.scrollLeft;
|
|
638
|
+
nav.classList.add('is-dragging');
|
|
639
|
+
});
|
|
640
|
+
document.addEventListener('pointermove', function (e) {
|
|
641
|
+
if (!drag.active) return;
|
|
642
|
+
var dx = e.clientX - drag.startX;
|
|
643
|
+
if (!drag.moved && Math.abs(dx) < 4) return;
|
|
644
|
+
drag.moved = true;
|
|
645
|
+
nav.scrollLeft = drag.startLeft - dx;
|
|
646
|
+
e.preventDefault();
|
|
647
|
+
});
|
|
648
|
+
document.addEventListener('pointerup', function () {
|
|
649
|
+
if (!drag.active) return;
|
|
650
|
+
drag.active = false;
|
|
651
|
+
nav.classList.remove('is-dragging');
|
|
652
|
+
});
|
|
653
|
+
nav.addEventListener('click', function (e) {
|
|
654
|
+
if (!drag.moved) return;
|
|
655
|
+
e.preventDefault();
|
|
656
|
+
e.stopPropagation();
|
|
657
|
+
drag.moved = false;
|
|
658
|
+
}, true);
|
|
659
|
+
nav.addEventListener('dragstart', function (e) { e.preventDefault(); });
|
|
660
|
+
|
|
661
|
+
/* ---- Vertical wheel → horizontal strip ---- */
|
|
662
|
+
nav.addEventListener('wheel', function (e) {
|
|
663
|
+
if (!overflowing() || Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
|
|
664
|
+
var max = nav.scrollWidth - nav.clientWidth;
|
|
665
|
+
var step = e.deltaMode === 1 ? e.deltaY * 16 : e.deltaY;
|
|
666
|
+
if ((step < 0 && nav.scrollLeft <= 0) || (step > 0 && nav.scrollLeft >= max - 1)) return;
|
|
667
|
+
nav.scrollLeft += step;
|
|
668
|
+
e.preventDefault();
|
|
669
|
+
}, { passive: false });
|
|
670
|
+
|
|
671
|
+
/* ---- React to scroll-spy changes ---- */
|
|
672
|
+
document.addEventListener('v2:navchange', function (e) {
|
|
673
|
+
var item = e.detail && e.detail.item;
|
|
674
|
+
if (item) ensureVisible(item);
|
|
675
|
+
updateOverflow();
|
|
676
|
+
if (!menu.hidden) openMenu();
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
680
|
+
new ResizeObserver(function () { updateOverflow(); }).observe(nav);
|
|
681
|
+
}
|
|
682
|
+
window.addEventListener('resize', updateOverflow);
|
|
683
|
+
|
|
684
|
+
nav.__v2DockRefresh = function () {
|
|
685
|
+
updateOverflow();
|
|
686
|
+
ensureVisible(nav.querySelector('.is-active, .v2-nav__item--active'));
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
requestAnimationFrame(function () {
|
|
690
|
+
updateOverflow();
|
|
691
|
+
ensureVisible(nav.querySelector('.is-active, .v2-nav__item--active'));
|
|
692
|
+
});
|
|
693
|
+
if (document.fonts && document.fonts.ready) {
|
|
694
|
+
document.fonts.ready.then(function () {
|
|
695
|
+
updateOverflow();
|
|
696
|
+
ensureVisible(nav.querySelector('.is-active, .v2-nav__item--active'));
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/* ======================================================
|
|
702
|
+
NAV SCROLL & SCROLL-SPY
|
|
703
|
+
- Click nav item → smooth scroll to target section
|
|
704
|
+
- Scroll-spy: highlight nav item when section in view
|
|
705
|
+
Uses getBoundingClientRect for accurate scroll detection
|
|
706
|
+
inside any scrollable container (.v2-content).
|
|
707
|
+
====================================================== */
|
|
708
|
+
|
|
709
|
+
/* ======================================================
|
|
710
|
+
SECTION SCROLL — shared by nav clicks and #deep links
|
|
711
|
+
scrollToSection('logs') / scrollToSection('#logs')
|
|
712
|
+
====================================================== */
|
|
713
|
+
function scrollToSection(id, opts) {
|
|
714
|
+
var content = $('.v2-content');
|
|
715
|
+
if (!content || !id) return false;
|
|
716
|
+
var target = document.getElementById(String(id).replace(/^#/, ''));
|
|
717
|
+
if (!target) return false;
|
|
718
|
+
|
|
719
|
+
var behavior = (opts && opts.behavior) || (prefersReduced() ? 'auto' : 'smooth');
|
|
720
|
+
var containerTop = content.getBoundingClientRect().top;
|
|
721
|
+
var targetTop = target.getBoundingClientRect().top;
|
|
722
|
+
content.scrollTo({
|
|
723
|
+
top: Math.max(0, content.scrollTop + targetTop - containerTop - 16),
|
|
724
|
+
behavior: behavior
|
|
725
|
+
});
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/* Applies #section deep links on load and on hash change. */
|
|
730
|
+
function initHashSection() {
|
|
731
|
+
function apply(behavior) {
|
|
732
|
+
var id = (window.location.hash || '').replace('#', '');
|
|
733
|
+
if (id) scrollToSection(id, { behavior: behavior });
|
|
734
|
+
}
|
|
735
|
+
if (window.location.hash) {
|
|
736
|
+
// Defer a tick so layout (and any first render) settles first.
|
|
737
|
+
setTimeout(function () { apply('auto'); }, 0);
|
|
738
|
+
}
|
|
739
|
+
window.addEventListener('hashchange', function () { apply('smooth'); });
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function initNavScroll() {
|
|
743
|
+
var content = $('.v2-content');
|
|
744
|
+
if (!content) return;
|
|
745
|
+
if (content.dataset.v2NavReady === 'true') return;
|
|
746
|
+
content.dataset.v2NavReady = 'true';
|
|
747
|
+
|
|
748
|
+
// Build map of nav items → sections via href="#id"
|
|
749
|
+
var navItems = $$('.v2-nav__item[href^="#"]');
|
|
750
|
+
var sections = [];
|
|
751
|
+
var app = document.getElementById('app') || $('.v2-app');
|
|
752
|
+
|
|
753
|
+
navItems.forEach(function (item) {
|
|
754
|
+
var href = item.getAttribute('href');
|
|
755
|
+
if (!href || href === '#') return;
|
|
756
|
+
var id = href.slice(1);
|
|
757
|
+
var section = document.getElementById(id);
|
|
758
|
+
if (section) {
|
|
759
|
+
sections.push({ item: item, section: section });
|
|
760
|
+
|
|
761
|
+
// Click: scroll section into view inside container
|
|
762
|
+
item.addEventListener('click', function (e) {
|
|
763
|
+
e.preventDefault();
|
|
764
|
+
e.stopPropagation();
|
|
765
|
+
scrollToSection(id);
|
|
766
|
+
// Keep the URL shareable without triggering a native jump
|
|
767
|
+
try { history.replaceState(null, '', '#' + id); } catch (_) {}
|
|
768
|
+
// Immediate active update on click
|
|
769
|
+
navItems.forEach(function (n) {
|
|
770
|
+
n.classList.remove('is-active', 'v2-nav__item--active');
|
|
771
|
+
n.removeAttribute('aria-current');
|
|
772
|
+
});
|
|
773
|
+
item.classList.add('is-active', 'v2-nav__item--active');
|
|
774
|
+
item.setAttribute('aria-current', 'page');
|
|
775
|
+
if (app && app.classList.contains('nav-collapsed')) sidebarOpen();
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
if (sections.length === 0) return;
|
|
781
|
+
|
|
782
|
+
// Scroll-spy: update active item as user scrolls
|
|
783
|
+
var lastActiveItem = null;
|
|
784
|
+
function updateActive() {
|
|
785
|
+
var containerTop = content.getBoundingClientRect().top;
|
|
786
|
+
var threshold = 60; // px from container top edge
|
|
787
|
+
var activeIdx = 0;
|
|
788
|
+
|
|
789
|
+
for (var i = 0; i < sections.length; i++) {
|
|
790
|
+
var rect = sections[i].section.getBoundingClientRect();
|
|
791
|
+
if (rect.top - containerTop <= threshold) activeIdx = i;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
var activeItem = null;
|
|
795
|
+
sections.forEach(function (pair, idx) {
|
|
796
|
+
var active = idx === activeIdx;
|
|
797
|
+
pair.item.classList.toggle('is-active', active);
|
|
798
|
+
pair.item.classList.toggle('v2-nav__item--active', active);
|
|
799
|
+
if (active) {
|
|
800
|
+
pair.item.setAttribute('aria-current', 'page');
|
|
801
|
+
activeItem = pair.item;
|
|
802
|
+
}
|
|
803
|
+
else pair.item.removeAttribute('aria-current');
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// Let the dock keep the active item in view / sync the More drop-up
|
|
807
|
+
if (activeItem && activeItem !== lastActiveItem) {
|
|
808
|
+
lastActiveItem = activeItem;
|
|
809
|
+
document.dispatchEvent(new CustomEvent('v2:navchange', { detail: { item: activeItem } }));
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
content.addEventListener('scroll', updateActive, { passive: true });
|
|
814
|
+
// Run once on init to set correct initial state
|
|
815
|
+
updateActive();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/* ======================================================
|
|
819
|
+
AUTO-INIT — runs on DOMContentLoaded
|
|
820
|
+
====================================================== */
|
|
821
|
+
function init() {
|
|
822
|
+
initTheme();
|
|
823
|
+
initThemeToggles();
|
|
824
|
+
initSidebar();
|
|
825
|
+
initGauges();
|
|
826
|
+
initSparklines();
|
|
827
|
+
initScrollShadow();
|
|
828
|
+
initStagger();
|
|
829
|
+
initCopyButtons();
|
|
830
|
+
initProductionControls();
|
|
831
|
+
initNavDock();
|
|
832
|
+
initNavScroll();
|
|
833
|
+
initHashSection();
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (document.readyState === 'loading') {
|
|
837
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
838
|
+
} else {
|
|
839
|
+
init();
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/* ======================================================
|
|
843
|
+
PUBLIC API
|
|
844
|
+
====================================================== */
|
|
845
|
+
global.SynteqV2 = {
|
|
846
|
+
// Theme
|
|
847
|
+
setTheme: setTheme,
|
|
848
|
+
getTheme: getTheme,
|
|
849
|
+
toggleTheme: toggleTheme,
|
|
850
|
+
// Sidebar
|
|
851
|
+
sidebarToggle: sidebarToggle,
|
|
852
|
+
sidebarOpen: sidebarOpen,
|
|
853
|
+
sidebarClose: sidebarClose,
|
|
854
|
+
// Data viz
|
|
855
|
+
renderGauge: renderGauge,
|
|
856
|
+
renderBars: renderBars,
|
|
857
|
+
renderSparkline: renderSparkline,
|
|
858
|
+
toast: toast,
|
|
859
|
+
tip: tip,
|
|
860
|
+
// Navigation
|
|
861
|
+
scrollToSection: scrollToSection,
|
|
862
|
+
// Init helpers (call after dynamic content changes)
|
|
863
|
+
initGauges: initGauges,
|
|
864
|
+
initSparklines: initSparklines,
|
|
865
|
+
initNavDock: initNavDock,
|
|
866
|
+
initNavScroll: initNavScroll,
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
})(window);
|