@a83/orbiter-admin 0.3.13 → 0.3.15

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/public/xfce.js ADDED
@@ -0,0 +1,697 @@
1
+ /**
2
+ * xfce.js — Space Station dock mode for Orbiter Admin.
3
+ * Auto-loaded by sidebar.js when orb_style === 'xfce'.
4
+ * Creates: status bar (top) + floating dock (bottom center) + HUD panel (slide-right).
5
+ */
6
+ (function () {
7
+ 'use strict';
8
+
9
+ var page = location.pathname.split('/').pop().replace('.html', '');
10
+ var params = new URLSearchParams(location.search);
11
+ var activeCol = params.get('col') || params.get('collection');
12
+
13
+ var NAV = [
14
+ { icon: '⬡', label: 'Dashboard', href: '/dashboard.html', key: 'dashboard' },
15
+ { icon: '◫', label: 'Media', href: '/media.html', key: 'media' },
16
+ { icon: '⚙', label: 'Settings', href: '/settings.html', key: 'settings' },
17
+ { icon: '⊛', label: 'Users', href: '/users.html', key: 'users' },
18
+ ];
19
+
20
+ var TOOLS = [
21
+ { icon: '▦', label: 'Schema', href: '/schema.html', key: 'schema' },
22
+ { icon: '◉', label: 'Build', href: '/build.html', key: 'build' },
23
+ { icon: '↓', label: 'Import', href: '/import.html', key: 'import' },
24
+ ];
25
+
26
+ var WORKSPACE = [
27
+ { icon: '✎', label: 'Notes', pane: 'notes' },
28
+ { icon: '☑', label: 'To-do', pane: 'todos' },
29
+ ];
30
+
31
+ // ── Helpers ───────────────────────────────────────────────────────────
32
+ function el(tag, cls, html) {
33
+ var e = document.createElement(tag);
34
+ if (cls) e.className = cls;
35
+ if (html) e.innerHTML = html;
36
+ return e;
37
+ }
38
+
39
+ // ── Status Bar ────────────────────────────────────────────────────────
40
+ function buildStatusBar() {
41
+ var sb = el('div', 'xfce-sb');
42
+ sb.innerHTML = [
43
+ '<div class="xfce-sb-left">',
44
+ '<span class="xfce-sb-logo">',
45
+ '<svg viewBox="0 0 20 20" width="12" height="12" fill="none" style="margin-right:5px;vertical-align:middle">',
46
+ '<circle cx="10" cy="10" r="4.5" fill="currentColor" opacity=".9"/>',
47
+ '<ellipse cx="10" cy="10" rx="9" ry="3.2" stroke="currentColor" stroke-width="1" opacity=".5" transform="rotate(-22 10 10)"/>',
48
+ '</svg>ORBITER',
49
+ '</span>',
50
+ '<span class="xfce-sb-div">·</span>',
51
+ '<span id="xfce-sb-site">—</span>',
52
+ '</div>',
53
+ '<div class="xfce-sb-center" id="xfce-sb-title"></div>',
54
+ '<div class="xfce-sb-right">',
55
+ '<span id="xfce-sb-user"></span>',
56
+ '<span class="xfce-sb-div">·</span>',
57
+ '<span id="xfce-sb-clock"></span>',
58
+ '</div>',
59
+ ].join('');
60
+ document.body.insertBefore(sb, document.body.firstChild);
61
+
62
+ // Page title from document.title (strip " — Orbiter")
63
+ var title = document.title.replace(/\s*—\s*Orbiter.*$/, '').trim();
64
+ var titleEl = document.getElementById('xfce-sb-title');
65
+ if (titleEl && title) titleEl.textContent = title;
66
+
67
+ // Clock
68
+ function tick() {
69
+ var c = document.getElementById('xfce-sb-clock');
70
+ if (!c) return;
71
+ c.textContent = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
72
+ }
73
+ tick();
74
+ setInterval(tick, 15000);
75
+ }
76
+
77
+ // ── HUD Meta Panel ────────────────────────────────────────────────────
78
+ var metaPanel;
79
+
80
+ function buildMetaPanel() {
81
+ metaPanel = el('div', 'xfce-hud');
82
+ metaPanel.id = 'xfce-hud';
83
+ metaPanel.innerHTML = [
84
+ '<div class="xfce-hud-bar">',
85
+ '<span class="xfce-hud-title">◈ System HUD</span>',
86
+ '<button class="xfce-hud-close" id="xfce-hud-close" title="Close">✕</button>',
87
+ '</div>',
88
+ '<div class="xfce-hud-body">',
89
+ '<div class="xfce-hud-section-label">Pod</div>',
90
+ '<div id="xfce-hud-pod" class="xfce-hud-rows"></div>',
91
+ '<div class="xfce-hud-section-label" style="margin-top:16px">Collections</div>',
92
+ '<div id="xfce-hud-cols" class="xfce-hud-rows"></div>',
93
+ '<div class="xfce-hud-section-label" style="margin-top:16px">Navigation</div>',
94
+ '<div class="xfce-hud-nav-links" id="xfce-hud-nav"></div>',
95
+ '</div>',
96
+ ].join('');
97
+ document.body.appendChild(metaPanel);
98
+
99
+ document.getElementById('xfce-hud-close').addEventListener('click', function () {
100
+ metaPanel.classList.remove('open');
101
+ });
102
+
103
+ // Nav links inside HUD (all items including tools)
104
+ var navWrap = document.getElementById('xfce-hud-nav');
105
+ if (navWrap) {
106
+ NAV.concat(TOOLS).forEach(function (n) {
107
+ var a = el('a', 'xfce-hud-nav-item' + (page === n.key ? ' active' : ''));
108
+ a.href = n.href;
109
+ a.innerHTML = '<span>' + n.icon + '</span><span>' + n.label + '</span>';
110
+ navWrap.appendChild(a);
111
+ });
112
+ }
113
+ }
114
+
115
+ function toggleHUD() {
116
+ if (!metaPanel) return;
117
+ metaPanel.classList.toggle('open');
118
+ }
119
+
120
+ // ── Tools popup ───────────────────────────────────────────────────────
121
+ var toolsPopup;
122
+
123
+ function buildToolsPopup() {
124
+ toolsPopup = el('div', 'xfce-tools-popup');
125
+ toolsPopup.id = 'xfce-tools-popup';
126
+ TOOLS.forEach(function (t) {
127
+ var a = el('a', 'xfce-tools-item' + (page === t.key ? ' active' : ''));
128
+ a.href = t.href;
129
+ a.innerHTML = '<span class="xfce-tools-icon">' + t.icon + '</span><span>' + t.label + '</span>';
130
+ toolsPopup.appendChild(a);
131
+ });
132
+ document.body.appendChild(toolsPopup);
133
+ document.addEventListener('click', function () {
134
+ toolsPopup.classList.remove('open');
135
+ });
136
+ document.addEventListener('keydown', function (e) {
137
+ if (e.key === 'Escape') toolsPopup.classList.remove('open');
138
+ });
139
+ }
140
+
141
+ function toggleToolsPopup() {
142
+ if (!toolsPopup) buildToolsPopup();
143
+ var btn = document.getElementById('xfce-tools-btn');
144
+ if (btn) {
145
+ var rect = btn.getBoundingClientRect();
146
+ toolsPopup.style.left = Math.round(rect.left + rect.width / 2) + 'px';
147
+ }
148
+ toolsPopup.classList.toggle('open');
149
+ }
150
+
151
+ // ── Workspace overlay (Notes + To-do) ────────────────────────────────
152
+ var wsOverlay, wsActivePane = 'notes', wsNotesTimer, wsTodosData = [];
153
+
154
+ function buildWorkspaceOverlay() {
155
+ wsOverlay = el('div', 'xfce-ws-overlay');
156
+ wsOverlay.id = 'xfce-ws-overlay';
157
+ wsOverlay.innerHTML = [
158
+ '<div class="xfce-ws-bar">',
159
+ '<div class="xfce-ws-tabs">',
160
+ '<button class="xfce-ws-tab active" data-pane="notes">✎ Notes</button>',
161
+ '<button class="xfce-ws-tab" data-pane="todos">☑ To-do</button>',
162
+ '</div>',
163
+ '<div style="display:flex;align-items:center;gap:8px">',
164
+ '<span id="xfce-ws-ind" class="xfce-ws-ind"></span>',
165
+ '<button id="xfce-ws-export" class="xfce-ws-export" title="Download as Markdown">↓ .md</button>',
166
+ '<button id="xfce-ws-close" class="xfce-ws-close">✕</button>',
167
+ '</div>',
168
+ '</div>',
169
+ '<div class="xfce-ws-body">',
170
+ '<div id="xfce-ws-notes-pane" class="xfce-ws-pane">',
171
+ '<textarea id="xfce-ws-notes" class="xfce-ws-textarea" placeholder="Jot something down…"></textarea>',
172
+ '</div>',
173
+ '<div id="xfce-ws-todos-pane" class="xfce-ws-pane" style="display:none">',
174
+ '<div class="xfce-ws-todo-row">',
175
+ '<span class="xfce-ws-todo-prompt">›</span>',
176
+ '<input id="xfce-ws-todo-inp" class="xfce-ws-todo-inp" placeholder="Add a task…" type="text" />',
177
+ '<button id="xfce-ws-todo-add" class="xfce-ws-todo-add">↵</button>',
178
+ '</div>',
179
+ '<div id="xfce-ws-todo-list" class="xfce-ws-todo-list"></div>',
180
+ '<div class="xfce-ws-todo-footer">',
181
+ '<span id="xfce-ws-todo-count"></span>',
182
+ '<button id="xfce-ws-todo-clear" style="display:none;background:none;border:none;font-family:var(--mono);font-size:9px;color:var(--muted);cursor:pointer">Clear done</button>',
183
+ '</div>',
184
+ '</div>',
185
+ '</div>',
186
+ ].join('');
187
+ document.body.appendChild(wsOverlay);
188
+
189
+ // Tabs
190
+ wsOverlay.querySelectorAll('.xfce-ws-tab').forEach(function (btn) {
191
+ btn.addEventListener('click', function () { switchWsPane(btn.dataset.pane); });
192
+ });
193
+
194
+ // Close
195
+ document.getElementById('xfce-ws-close').addEventListener('click', closeWorkspace);
196
+ document.addEventListener('keydown', function (e) {
197
+ if (e.key === 'Escape' && wsOverlay.classList.contains('open')) closeWorkspace();
198
+ });
199
+ document.addEventListener('click', function (e) {
200
+ if (!wsOverlay.classList.contains('open')) return;
201
+ if (wsOverlay.contains(e.target)) return;
202
+ closeWorkspace();
203
+ });
204
+
205
+ // Notes auto-save
206
+ var notesEl = document.getElementById('xfce-ws-notes');
207
+ var ind = document.getElementById('xfce-ws-ind');
208
+ notesEl.addEventListener('input', function () {
209
+ clearTimeout(wsNotesTimer);
210
+ ind.textContent = '● unsaved'; ind.style.color = 'var(--gold)';
211
+ wsNotesTimer = setTimeout(function () {
212
+ ind.textContent = '↑ saving…'; ind.style.color = 'var(--muted)';
213
+ fetch('/api/meta', {
214
+ method: 'PUT', credentials: 'include',
215
+ headers: { 'Content-Type': 'application/json' },
216
+ body: JSON.stringify({ 'dashboard.notes': notesEl.value }),
217
+ }).then(function () {
218
+ ind.textContent = '✓ saved'; ind.style.color = 'var(--jade)';
219
+ setTimeout(function () { ind.textContent = ''; }, 2000);
220
+ });
221
+ }, 1200);
222
+ });
223
+
224
+ // Todo add
225
+ function addTodo() {
226
+ var inp = document.getElementById('xfce-ws-todo-inp');
227
+ var text = inp.value.trim();
228
+ if (!text) return;
229
+ wsTodosData.push({ text: text, done: false });
230
+ inp.value = '';
231
+ renderTodos();
232
+ saveTodos();
233
+ }
234
+ document.getElementById('xfce-ws-todo-add').addEventListener('click', addTodo);
235
+ document.getElementById('xfce-ws-todo-inp').addEventListener('keydown', function (e) {
236
+ if (e.key === 'Enter') addTodo();
237
+ });
238
+
239
+ // Clear done
240
+ document.getElementById('xfce-ws-todo-clear').addEventListener('click', function () {
241
+ wsTodosData = wsTodosData.filter(function (t) { return !t.done; });
242
+ renderTodos(); saveTodos();
243
+ });
244
+
245
+ // Export .md
246
+ document.getElementById('xfce-ws-export').addEventListener('click', function () {
247
+ var date = new Date().toISOString().slice(0, 10);
248
+ var text, filename;
249
+ if (wsActivePane === 'notes') {
250
+ text = document.getElementById('xfce-ws-notes').value;
251
+ filename = 'notes-' + date + '.md';
252
+ } else {
253
+ text = wsTodosData.map(function (t) { return (t.done ? '- [x] ' : '- [ ] ') + t.text; }).join('\n');
254
+ filename = 'todos-' + date + '.md';
255
+ }
256
+ var blob = new Blob([text], { type: 'text/markdown' });
257
+ var a = document.createElement('a');
258
+ a.href = URL.createObjectURL(blob);
259
+ a.download = filename;
260
+ a.click();
261
+ URL.revokeObjectURL(a.href);
262
+ });
263
+ }
264
+
265
+ function switchWsPane(pane) {
266
+ wsActivePane = pane;
267
+ wsOverlay.querySelectorAll('.xfce-ws-tab').forEach(function (b) { b.classList.remove('active'); });
268
+ wsOverlay.querySelector('[data-pane="' + pane + '"]').classList.add('active');
269
+ document.getElementById('xfce-ws-notes-pane').style.display = pane === 'notes' ? '' : 'none';
270
+ document.getElementById('xfce-ws-todos-pane').style.display = pane === 'todos' ? '' : 'none';
271
+ }
272
+
273
+ function renderTodos() {
274
+ var list = document.getElementById('xfce-ws-todo-list');
275
+ var count = document.getElementById('xfce-ws-todo-count');
276
+ var clear = document.getElementById('xfce-ws-todo-clear');
277
+ if (!list) return;
278
+ var done = wsTodosData.filter(function (t) { return t.done; }).length;
279
+ if (count) count.textContent = done + '/' + wsTodosData.length + ' done';
280
+ if (clear) clear.style.display = done > 0 ? '' : 'none';
281
+ list.innerHTML = wsTodosData.length === 0
282
+ ? '<div class="xfce-ws-todo-empty">No tasks yet</div>'
283
+ : '';
284
+ wsTodosData.forEach(function (t, i) {
285
+ var row = document.createElement('label');
286
+ row.className = 'xfce-ws-todo-item' + (t.done ? ' done' : '');
287
+ var cb = document.createElement('input');
288
+ cb.type = 'checkbox'; cb.checked = !!t.done;
289
+ cb.addEventListener('change', function () {
290
+ wsTodosData[i].done = cb.checked;
291
+ renderTodos(); saveTodos();
292
+ });
293
+ var span = document.createElement('span');
294
+ span.textContent = t.text;
295
+ row.appendChild(cb); row.appendChild(span);
296
+ list.appendChild(row);
297
+ });
298
+ }
299
+
300
+ function saveTodos() {
301
+ fetch('/api/meta', {
302
+ method: 'PUT', credentials: 'include',
303
+ headers: { 'Content-Type': 'application/json' },
304
+ body: JSON.stringify({ 'dashboard.todos': JSON.stringify(wsTodosData) }),
305
+ });
306
+ }
307
+
308
+ function loadWsData() {
309
+ // On dashboard, read directly from the page's own elements if available
310
+ var dashNotes = document.getElementById('notes-area');
311
+ if (dashNotes) {
312
+ var ta = document.getElementById('xfce-ws-notes');
313
+ if (ta) ta.value = dashNotes.value;
314
+ } else {
315
+ fetch('/api/meta/dashboard~notes', { credentials: 'include' })
316
+ .then(function (r) { return r.ok ? r.json() : null; })
317
+ .then(function (d) {
318
+ var ta = document.getElementById('xfce-ws-notes');
319
+ if (ta && d && d.value != null) ta.value = d.value;
320
+ });
321
+ }
322
+ fetch('/api/meta/dashboard~todos', { credentials: 'include' })
323
+ .then(function (r) { return r.ok ? r.json() : null; })
324
+ .then(function (d) {
325
+ try { wsTodosData = JSON.parse(d && d.value ? d.value : '[]'); } catch (e) { wsTodosData = []; }
326
+ if (!Array.isArray(wsTodosData)) wsTodosData = [];
327
+ renderTodos();
328
+ });
329
+ }
330
+
331
+ function openWorkspace(pane) {
332
+ if (!wsOverlay) buildWorkspaceOverlay();
333
+ var isOpen = wsOverlay.classList.contains('open');
334
+ if (isOpen && wsActivePane === pane) { closeWorkspace(); return; }
335
+ switchWsPane(pane);
336
+ if (!isOpen) {
337
+ wsOverlay.classList.add('open');
338
+ loadWsData();
339
+ }
340
+ setTimeout(function () {
341
+ var focus = pane === 'notes'
342
+ ? document.getElementById('xfce-ws-notes')
343
+ : document.getElementById('xfce-ws-todo-inp');
344
+ if (focus) focus.focus();
345
+ }, 60);
346
+ }
347
+
348
+ function closeWorkspace() {
349
+ if (wsOverlay) wsOverlay.classList.remove('open');
350
+ // Sync indicator on dock buttons
351
+ document.querySelectorAll('[data-wspane]').forEach(function (btn) {
352
+ btn.classList.remove('active');
353
+ });
354
+ }
355
+
356
+ // ── Focus mode (spotlight on recently edited) ─────────────────────────
357
+ var focusedEl = null;
358
+ var focusOrigStyle = null;
359
+
360
+ function buildFocusDim() {
361
+ var dim = document.createElement('div');
362
+ dim.id = 'xfce-focus-dim';
363
+ dim.className = 'xfce-focus-dim';
364
+ dim.addEventListener('click', exitFocusMode);
365
+ document.body.appendChild(dim);
366
+ return dim;
367
+ }
368
+
369
+ function enterFocusMode(target) {
370
+ if (focusedEl) return;
371
+
372
+ var dim = document.getElementById('xfce-focus-dim') || buildFocusDim();
373
+ var rect = target.getBoundingClientRect();
374
+
375
+ // Capture current computed inline style so we can fully restore it
376
+ focusOrigStyle = {
377
+ position: target.style.position || '',
378
+ left: target.style.left || '',
379
+ top: target.style.top || '',
380
+ width: target.style.width || '',
381
+ height: target.style.height || '',
382
+ zIndex: target.style.zIndex || '',
383
+ overflow: target.style.overflow || '',
384
+ maxHeight: target.style.maxHeight || '',
385
+ boxShadow: target.style.boxShadow || '',
386
+ transition:target.style.transition|| '',
387
+ };
388
+ focusedEl = target;
389
+
390
+ var tw = Math.round(window.innerWidth * 0.92);
391
+ var maxH = Math.round(window.innerHeight * 0.86);
392
+ var tl = Math.round((window.innerWidth - tw) / 2);
393
+
394
+ // Fix position and set target width — let height be auto so it fits content
395
+ target.style.transition = 'none';
396
+ target.style.position = 'fixed';
397
+ target.style.left = rect.left + 'px';
398
+ target.style.top = rect.top + 'px';
399
+ target.style.width = tw + 'px';
400
+ target.style.height = 'auto';
401
+ target.style.maxHeight = maxH + 'px';
402
+ target.style.zIndex = '9995';
403
+ target.style.overflow = 'auto';
404
+ target.classList.add('xfce-in-focus');
405
+
406
+ // Reflow so the browser computes auto height at the new width
407
+ var actualH = Math.min(target.scrollHeight, maxH);
408
+ var tt = Math.round((window.innerHeight - actualH) / 2);
409
+
410
+ // Animate only position — height stays auto (content-driven)
411
+ target.style.transition = [
412
+ 'left .32s cubic-bezier(.34,1.15,.64,1)',
413
+ 'top .32s cubic-bezier(.34,1.15,.64,1)',
414
+ 'width .28s cubic-bezier(.4,0,.2,1)',
415
+ 'box-shadow .28s',
416
+ ].join(',');
417
+ target.style.left = tl + 'px';
418
+ target.style.top = tt + 'px';
419
+ target.style.boxShadow = '0 32px 80px rgba(0,0,0,.5), 0 0 0 1px color-mix(in srgb,var(--accent) 28%,transparent)';
420
+
421
+ dim.classList.add('active');
422
+ document.addEventListener('keydown', onFocusKey);
423
+ }
424
+
425
+ function exitFocusMode() {
426
+ if (!focusedEl) return;
427
+ var target = focusedEl;
428
+ var dim = document.getElementById('xfce-focus-dim');
429
+
430
+ if (dim) dim.classList.remove('active');
431
+ document.removeEventListener('keydown', onFocusKey);
432
+
433
+ // Fade out, restore, fade back in
434
+ target.style.transition = 'opacity .18s';
435
+ target.style.opacity = '0';
436
+
437
+ setTimeout(function () {
438
+ target.classList.remove('xfce-in-focus');
439
+ Object.keys(focusOrigStyle).forEach(function (k) {
440
+ target.style[k] = focusOrigStyle[k];
441
+ });
442
+ target.style.opacity = '0';
443
+ target.getBoundingClientRect();
444
+ target.style.transition = 'opacity .15s';
445
+ target.style.opacity = '1';
446
+ setTimeout(function () {
447
+ target.style.transition = '';
448
+ target.style.opacity = '';
449
+ }, 160);
450
+ focusedEl = null;
451
+ focusOrigStyle = null;
452
+ }, 180);
453
+ }
454
+
455
+ function onFocusKey(e) {
456
+ if (e.key === 'Escape') exitFocusMode();
457
+ }
458
+
459
+ function initFocusMode() {
460
+ var dashContent = document.querySelector('.dash-content');
461
+ if (!dashContent) return;
462
+ var head = dashContent.querySelector('.section-head');
463
+ if (!head) return;
464
+ head.classList.add('xfce-focus-trigger');
465
+ head.title = 'Click to focus';
466
+ head.addEventListener('click', function () { enterFocusMode(dashContent); });
467
+ }
468
+
469
+ // ── Dock ──────────────────────────────────────────────────────────────
470
+ var dockInner;
471
+
472
+ function makeDockItem(icon, label, hrefOrNull, isActive, isBtn) {
473
+ var item = isBtn ? el('button') : el('a');
474
+ item.className = 'xfce-dock-item' + (isActive ? ' active' : '');
475
+ if (!isBtn) item.href = hrefOrNull;
476
+ item.setAttribute('aria-label', label);
477
+ item.dataset.label = label;
478
+ item.innerHTML = '<span class="xfce-dock-icon">' + icon + '</span><span class="xfce-dock-lbl">' + label + '</span>';
479
+ item.style.setProperty('--ds', '1');
480
+ return item;
481
+ }
482
+
483
+ function buildDock() {
484
+ var dock = el('div', 'xfce-dock');
485
+ dock.id = 'xfce-dock';
486
+ dockInner = el('div', 'xfce-dock-inner');
487
+ dock.appendChild(dockInner);
488
+
489
+ // Nav items group
490
+ var navGroup = el('div', 'xfce-dock-group');
491
+ NAV.forEach(function (n) {
492
+ navGroup.appendChild(makeDockItem(n.icon, n.label, n.href, page === n.key, false));
493
+ });
494
+ dockInner.appendChild(navGroup);
495
+
496
+ // Separator
497
+ dockInner.appendChild(el('div', 'xfce-dock-sep'));
498
+
499
+ // Collections group (populated by /api/info)
500
+ var colGroup = el('div', 'xfce-dock-group');
501
+ colGroup.id = 'xfce-dock-cols';
502
+ dockInner.appendChild(colGroup);
503
+
504
+ // Separator
505
+ dockInner.appendChild(el('div', 'xfce-dock-sep'));
506
+
507
+ // Workspace group: Notes + To-do (open overlay)
508
+ var wsGroup = el('div', 'xfce-dock-group');
509
+ WORKSPACE.forEach(function (w) {
510
+ var btn = makeDockItem(w.icon, w.label, null, false, true);
511
+ btn.dataset.wspane = w.pane;
512
+ btn.addEventListener('click', function (e) {
513
+ e.stopPropagation();
514
+ openWorkspace(w.pane);
515
+ });
516
+ wsGroup.appendChild(btn);
517
+ });
518
+ dockInner.appendChild(wsGroup);
519
+
520
+ // Separator
521
+ dockInner.appendChild(el('div', 'xfce-dock-sep'));
522
+
523
+ // Tools popup button (Schema, Build, Import)
524
+ var toolsActive = TOOLS.some(function (t) { return t.key === page; });
525
+ var toolsBtn = makeDockItem('⚒', 'Tools', null, toolsActive, true);
526
+ toolsBtn.id = 'xfce-tools-btn';
527
+ toolsBtn.addEventListener('click', function (e) {
528
+ e.stopPropagation();
529
+ toggleToolsPopup();
530
+ });
531
+ dockInner.appendChild(toolsBtn);
532
+
533
+ // Separator
534
+ dockInner.appendChild(el('div', 'xfce-dock-sep'));
535
+
536
+ // HUD toggle button
537
+ var hudBtn = makeDockItem('▣', 'HUD', null, false, true);
538
+ hudBtn.addEventListener('click', toggleHUD);
539
+ dockInner.appendChild(hudBtn);
540
+
541
+ // Scheme toggle
542
+ var schemeBtn = document.getElementById('scheme-toggle');
543
+ var schemeClone = makeDockItem('◐', 'Scheme', null, false, true);
544
+ schemeClone.addEventListener('click', function () {
545
+ if (schemeBtn) schemeBtn.click();
546
+ setTimeout(function () {
547
+ schemeClone.querySelector('.xfce-dock-icon').textContent = schemeBtn ? schemeBtn.textContent : '◐';
548
+ }, 50);
549
+ });
550
+ dockInner.appendChild(schemeClone);
551
+
552
+ document.body.appendChild(dock);
553
+
554
+ // Any click inside the dock exits focus mode (capture phase runs
555
+ // before stopPropagation on individual buttons can block it)
556
+ dock.addEventListener('click', function () {
557
+ if (focusedEl) exitFocusMode();
558
+ }, true);
559
+
560
+ // ── Magnification ─────────────────────────────────────────────────
561
+ function applyMag(cx) {
562
+ var items = dockInner.querySelectorAll('.xfce-dock-item');
563
+ items.forEach(function (item) {
564
+ var r = item.getBoundingClientRect();
565
+ var mid = r.left + r.width / 2;
566
+ var d = Math.abs(cx - mid);
567
+ var s = d < 96 ? 1 + (1 - d / 96) * 0.95 : 1;
568
+ item.style.setProperty('--ds', s.toFixed(3));
569
+ });
570
+ }
571
+
572
+ dock.addEventListener('mousemove', function (e) { applyMag(e.clientX); });
573
+ dock.addEventListener('mouseleave', function () {
574
+ dockInner.querySelectorAll('.xfce-dock-item').forEach(function (item) {
575
+ item.style.setProperty('--ds', '1');
576
+ });
577
+ });
578
+ }
579
+
580
+ // ── Load /api/info ────────────────────────────────────────────────────
581
+ function loadInfo() {
582
+ fetch('/api/info', { credentials: 'include' })
583
+ .then(function (r) { return r.ok ? r.json() : null; })
584
+ .then(function (info) {
585
+ if (!info) return;
586
+
587
+ // Site name
588
+ var siteEl = document.getElementById('xfce-sb-site');
589
+ if (siteEl) siteEl.textContent = info.siteName || info.podPath.split('/').pop().replace('.pod', '');
590
+
591
+ // Collections in dock
592
+ var colGroup = document.getElementById('xfce-dock-cols');
593
+ if (colGroup) {
594
+ var topLevel = (info.collections || []).filter(function (c) { return !c.parent; });
595
+ topLevel.forEach(function (col) {
596
+ var isSingleton = !!col.singleton;
597
+ var href = isSingleton
598
+ ? '/editor.html?collection=' + encodeURIComponent(col.id) + '&singleton=1'
599
+ : '/entries.html?col=' + encodeURIComponent(col.id) + '&label=' + encodeURIComponent(col.label);
600
+ var isActive = isSingleton
601
+ ? page === 'editor' && activeCol === col.id
602
+ : page === 'entries' && activeCol === col.id;
603
+ // Abbreviation icon (first char of label)
604
+ var abbr = col.label.substring(0, 2);
605
+ var item = makeDockItem(abbr, col.label, href, isActive, false);
606
+ item.querySelector('.xfce-dock-icon').style.cssText = 'font-size:9px;font-family:var(--mono);letter-spacing:-.02em;line-height:1;';
607
+ colGroup.appendChild(item);
608
+ });
609
+ }
610
+
611
+ // HUD pod section
612
+ var hudPod = document.getElementById('xfce-hud-pod');
613
+ if (hudPod) {
614
+ var total = (info.collections || []).reduce(function (s, c) { return s + (c.total || 0); }, 0);
615
+ hudPod.innerHTML = [
616
+ hudRow('File', info.podPath.split('/').pop()),
617
+ hudRow('Format', 'v' + info.formatVersion),
618
+ hudRow('Admin', 'v' + info.adminVersion),
619
+ hudRow('Collections', info.collections.length),
620
+ hudRow('Entries', total),
621
+ ].join('');
622
+ }
623
+
624
+ // HUD collections section
625
+ var hudCols = document.getElementById('xfce-hud-cols');
626
+ if (hudCols) {
627
+ hudCols.innerHTML = (info.collections || []).map(function (col) {
628
+ return hudRow(col.label, col.total + ' entr' + (col.total === 1 ? 'y' : 'ies'));
629
+ }).join('');
630
+ }
631
+ })
632
+ .catch(function () {});
633
+
634
+ // Site meta
635
+ fetch('/api/meta/site~name', { credentials: 'include' })
636
+ .then(function (r) { return r.ok ? r.json() : null; })
637
+ .then(function (d) {
638
+ if (!d || !d.value) return;
639
+ var siteEl = document.getElementById('xfce-sb-site');
640
+ if (siteEl) siteEl.textContent = d.value;
641
+ })
642
+ .catch(function () {});
643
+
644
+ // User from topbar (wait for other scripts to populate it)
645
+ setTimeout(function () {
646
+ var topbarUser = document.getElementById('topbar-user');
647
+ var sbUser = document.getElementById('xfce-sb-user');
648
+ if (sbUser && topbarUser) {
649
+ var observer = new MutationObserver(function () {
650
+ sbUser.textContent = topbarUser.textContent;
651
+ });
652
+ observer.observe(topbarUser, { childList: true, characterData: true, subtree: true });
653
+ sbUser.textContent = topbarUser.textContent;
654
+ }
655
+ }, 300);
656
+ }
657
+
658
+ function hudRow(label, value) {
659
+ return '<div class="xfce-hud-row"><span>' + label + '</span><span>' + value + '</span></div>';
660
+ }
661
+
662
+ // ── Keyboard shortcuts ────────────────────────────────────────────────
663
+ function bindKeys() {
664
+ document.addEventListener('keydown', function (e) {
665
+ var mod = e.metaKey || e.ctrlKey;
666
+ if (!mod || !e.shiftKey) return;
667
+
668
+ if (e.key === 'd' || e.key === 'D') {
669
+ e.preventDefault();
670
+ toggleHUD();
671
+ }
672
+
673
+ // ⌘⇧L — cycle back to glass
674
+ if (e.key === 'l' || e.key === 'L') {
675
+ e.preventDefault();
676
+ localStorage.setItem('orb_style', 'glass');
677
+ location.reload();
678
+ }
679
+ });
680
+ }
681
+
682
+ // ── Init ──────────────────────────────────────────────────────────────
683
+ function init() {
684
+ buildStatusBar();
685
+ buildMetaPanel();
686
+ buildDock();
687
+ loadInfo();
688
+ bindKeys();
689
+ initFocusMode();
690
+ }
691
+
692
+ if (document.readyState === 'loading') {
693
+ document.addEventListener('DOMContentLoaded', init);
694
+ } else {
695
+ init();
696
+ }
697
+ })();