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.
Files changed (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -2
  3. package/assets/common/bond-inspect.sh +368 -0
  4. package/assets/common/config.sh +263 -0
  5. package/assets/common/extract-data.sh +197 -0
  6. package/assets/common/harden-host.sh +37 -0
  7. package/assets/common/install-deps.sh +93 -0
  8. package/assets/common/node-enroll.sh +65 -0
  9. package/assets/common/node-setup.sh +150 -0
  10. package/assets/common/node-start.sh +90 -0
  11. package/assets/common/node-stop.sh +18 -0
  12. package/assets/common/node-upgrade.sh +45 -0
  13. package/assets/common/pre-run.sh +497 -0
  14. package/assets/common/promote-validator.sh +104 -0
  15. package/assets/deploy/10-prepare-host.sh +98 -0
  16. package/assets/deploy/20-install-toolchain.sh +93 -0
  17. package/assets/deploy/30-setup-node.sh +82 -0
  18. package/assets/deploy/40-start-node.sh +75 -0
  19. package/assets/deploy/50-verify.sh +259 -0
  20. package/assets/deploy/60-enroll.sh +304 -0
  21. package/assets/deploy/70-promote.sh +152 -0
  22. package/assets/deploy/lib.sh +398 -0
  23. package/assets/manage/10-bond-increase.sh +149 -0
  24. package/assets/manage/10-diagnose-node.sh +169 -0
  25. package/assets/manage/20-exit-validator.sh +259 -0
  26. package/assets/manage/25-recover-node.sh +291 -0
  27. package/assets/manage/30-status.sh +64 -0
  28. package/assets/manage/40-exit-withdraw.sh +133 -0
  29. package/assets/manage/bond_inc.sh +318 -0
  30. package/assets/manage/dashboard_monitor/app.js +1544 -0
  31. package/assets/manage/dashboard_monitor/assets/v2.2/css/base.css +113 -0
  32. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/content.css +133 -0
  33. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/controls.css +229 -0
  34. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/data.css +374 -0
  35. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/icons.css +35 -0
  36. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/motion.css +57 -0
  37. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/shell.css +51 -0
  38. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/sidebar.css +242 -0
  39. package/assets/manage/dashboard_monitor/assets/v2.2/css/components/topbar.css +138 -0
  40. package/assets/manage/dashboard_monitor/assets/v2.2/css/fonts.css +30 -0
  41. package/assets/manage/dashboard_monitor/assets/v2.2/css/synteq-ui-v2.css +25 -0
  42. package/assets/manage/dashboard_monitor/assets/v2.2/css/theme-dark.css +38 -0
  43. package/assets/manage/dashboard_monitor/assets/v2.2/css/theme-light.css +36 -0
  44. package/assets/manage/dashboard_monitor/assets/v2.2/css/tokens.css +61 -0
  45. package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-Medium.ttf +0 -0
  46. package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-Regular.ttf +0 -0
  47. package/assets/manage/dashboard_monitor/assets/v2.2/fonts/GoogleSansCode-SemiBold.ttf +0 -0
  48. package/assets/manage/dashboard_monitor/assets/v2.2/js/synteq-ui-v2.js +869 -0
  49. package/assets/manage/dashboard_monitor/deploy-dashboard.sh +230 -0
  50. package/assets/manage/dashboard_monitor/favicon.svg +13 -0
  51. package/assets/manage/dashboard_monitor/index.html +1197 -0
  52. package/assets/manage/dashboard_monitor/nginx-octra.conf +76 -0
  53. package/assets/manage/dashboard_monitor/octra-dashboard.service +29 -0
  54. package/assets/manage/dashboard_monitor/octra-node-prune.cron +12 -0
  55. package/assets/manage/dashboard_monitor/octra-node-prune.service +16 -0
  56. package/assets/manage/dashboard_monitor/octra-node-prune.timer +12 -0
  57. package/assets/manage/dashboard_monitor/prune-node-junk.sh +329 -0
  58. package/assets/manage/dashboard_monitor/server.py +1887 -0
  59. package/assets/manage/dashboard_monitor/ssl-certbot.sh +90 -0
  60. package/assets/manage/dashboard_monitor/styles.css +327 -0
  61. package/assets/manage/lib.sh +347 -0
  62. package/assets/manifest.json +143 -0
  63. package/assets/migrate/00-backup-identity.sh +67 -0
  64. package/assets/migrate/01-prepare-target.sh +145 -0
  65. package/assets/migrate/02-authorize-pull.sh +43 -0
  66. package/assets/migrate/03-pull-state.sh +102 -0
  67. package/assets/migrate/04-stop-source.sh +85 -0
  68. package/assets/migrate/05-finalize-target.sh +140 -0
  69. package/assets/migrate/06-start-target.sh +57 -0
  70. package/assets/migrate/07-verify.sh +237 -0
  71. package/assets/migrate/08-rollback.sh +54 -0
  72. package/assets/migrate/lib.sh +149 -0
  73. package/assets/upgrade/10-preflight.sh +120 -0
  74. package/assets/upgrade/20-upgrade.sh +142 -0
  75. package/assets/upgrade/30-manual-upgrade.sh +129 -0
  76. package/assets/upgrade/40-storage-prune.sh +65 -0
  77. package/assets/upgrade/50-status.sh +58 -0
  78. package/assets/upgrade/lib.sh +232 -0
  79. package/bin/ovtools.js +32 -0
  80. package/dist/api/checks.d.ts +6 -0
  81. package/dist/api/checks.d.ts.map +1 -0
  82. package/dist/api/checks.js +173 -0
  83. package/dist/api/checks.js.map +1 -0
  84. package/dist/api/hostkeys.d.ts +10 -0
  85. package/dist/api/hostkeys.d.ts.map +1 -0
  86. package/dist/api/hostkeys.js +58 -0
  87. package/dist/api/hostkeys.js.map +1 -0
  88. package/dist/api/jobs.d.ts +33 -0
  89. package/dist/api/jobs.d.ts.map +1 -0
  90. package/dist/api/jobs.js +483 -0
  91. package/dist/api/jobs.js.map +1 -0
  92. package/dist/api/nodes.d.ts +16 -0
  93. package/dist/api/nodes.d.ts.map +1 -0
  94. package/dist/api/nodes.js +126 -0
  95. package/dist/api/nodes.js.map +1 -0
  96. package/dist/api/server.d.ts +3 -0
  97. package/dist/api/server.d.ts.map +1 -0
  98. package/dist/api/server.js +443 -0
  99. package/dist/api/server.js.map +1 -0
  100. package/dist/cli.d.ts +3 -0
  101. package/dist/cli.d.ts.map +1 -0
  102. package/dist/cli.js +301 -0
  103. package/dist/cli.js.map +1 -0
  104. package/dist/constants.d.ts +76 -0
  105. package/dist/constants.d.ts.map +1 -0
  106. package/dist/constants.js +150 -0
  107. package/dist/constants.js.map +1 -0
  108. package/dist/index.d.ts +18 -0
  109. package/dist/index.d.ts.map +1 -0
  110. package/dist/index.js +60 -0
  111. package/dist/index.js.map +1 -0
  112. package/dist/orchestrator/base.d.ts +52 -0
  113. package/dist/orchestrator/base.d.ts.map +1 -0
  114. package/dist/orchestrator/base.js +219 -0
  115. package/dist/orchestrator/base.js.map +1 -0
  116. package/dist/orchestrator/deploy.d.ts +33 -0
  117. package/dist/orchestrator/deploy.d.ts.map +1 -0
  118. package/dist/orchestrator/deploy.js +313 -0
  119. package/dist/orchestrator/deploy.js.map +1 -0
  120. package/dist/orchestrator/manage.d.ts +28 -0
  121. package/dist/orchestrator/manage.d.ts.map +1 -0
  122. package/dist/orchestrator/manage.js +267 -0
  123. package/dist/orchestrator/manage.js.map +1 -0
  124. package/dist/orchestrator/migrate.d.ts +31 -0
  125. package/dist/orchestrator/migrate.d.ts.map +1 -0
  126. package/dist/orchestrator/migrate.js +442 -0
  127. package/dist/orchestrator/migrate.js.map +1 -0
  128. package/dist/ssh/client.d.ts +39 -0
  129. package/dist/ssh/client.d.ts.map +1 -0
  130. package/dist/ssh/client.js +418 -0
  131. package/dist/ssh/client.js.map +1 -0
  132. package/dist/test/core.test.d.ts +2 -0
  133. package/dist/test/core.test.d.ts.map +1 -0
  134. package/dist/test/core.test.js +188 -0
  135. package/dist/test/core.test.js.map +1 -0
  136. package/dist/types.d.ts +267 -0
  137. package/dist/types.d.ts.map +1 -0
  138. package/dist/types.js +3 -0
  139. package/dist/types.js.map +1 -0
  140. package/dist/utils/assets.d.ts +28 -0
  141. package/dist/utils/assets.d.ts.map +1 -0
  142. package/dist/utils/assets.js +583 -0
  143. package/dist/utils/assets.js.map +1 -0
  144. package/dist/utils/browser.d.ts +2 -0
  145. package/dist/utils/browser.d.ts.map +1 -0
  146. package/dist/utils/browser.js +23 -0
  147. package/dist/utils/browser.js.map +1 -0
  148. package/dist/utils/dashboard.d.ts +16 -0
  149. package/dist/utils/dashboard.d.ts.map +1 -0
  150. package/dist/utils/dashboard.js +177 -0
  151. package/dist/utils/dashboard.js.map +1 -0
  152. package/dist/utils/helpers.d.ts +29 -0
  153. package/dist/utils/helpers.d.ts.map +1 -0
  154. package/dist/utils/helpers.js +204 -0
  155. package/dist/utils/helpers.js.map +1 -0
  156. package/dist/utils/scripts.d.ts +60 -0
  157. package/dist/utils/scripts.d.ts.map +1 -0
  158. package/dist/utils/scripts.js +174 -0
  159. package/dist/utils/scripts.js.map +1 -0
  160. package/dist/utils/state.d.ts +12 -0
  161. package/dist/utils/state.d.ts.map +1 -0
  162. package/dist/utils/state.js +44 -0
  163. package/dist/utils/state.js.map +1 -0
  164. package/dist/utils/tar.d.ts +11 -0
  165. package/dist/utils/tar.d.ts.map +1 -0
  166. package/dist/utils/tar.js +93 -0
  167. package/dist/utils/tar.js.map +1 -0
  168. package/dist/utils/update.d.ts +9 -0
  169. package/dist/utils/update.d.ts.map +1 -0
  170. package/dist/utils/update.js +134 -0
  171. package/dist/utils/update.js.map +1 -0
  172. package/dist/utils/websocket.d.ts +10 -0
  173. package/dist/utils/websocket.d.ts.map +1 -0
  174. package/dist/utils/websocket.js +203 -0
  175. package/dist/utils/websocket.js.map +1 -0
  176. package/package.json +55 -4
  177. package/public/app.js +2712 -0
  178. package/public/assets/v2.2/css/base.css +113 -0
  179. package/public/assets/v2.2/css/components/content.css +133 -0
  180. package/public/assets/v2.2/css/components/controls.css +229 -0
  181. package/public/assets/v2.2/css/components/data.css +373 -0
  182. package/public/assets/v2.2/css/components/icons.css +35 -0
  183. package/public/assets/v2.2/css/components/motion.css +57 -0
  184. package/public/assets/v2.2/css/components/shell.css +51 -0
  185. package/public/assets/v2.2/css/components/sidebar.css +242 -0
  186. package/public/assets/v2.2/css/components/topbar.css +138 -0
  187. package/public/assets/v2.2/css/fonts.css +30 -0
  188. package/public/assets/v2.2/css/synteq-ui-v2.css +25 -0
  189. package/public/assets/v2.2/css/theme-dark.css +38 -0
  190. package/public/assets/v2.2/css/theme-light.css +36 -0
  191. package/public/assets/v2.2/css/tokens.css +61 -0
  192. package/public/assets/v2.2/fonts/GoogleSansCode-Medium.ttf +0 -0
  193. package/public/assets/v2.2/fonts/GoogleSansCode-Regular.ttf +0 -0
  194. package/public/assets/v2.2/fonts/GoogleSansCode-SemiBold.ttf +0 -0
  195. package/public/assets/v2.2/js/synteq-ui-v2.js +869 -0
  196. package/public/favicon.svg +7 -0
  197. package/public/index.html +1080 -0
  198. 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);