@a83/orbiter-admin 0.3.14 → 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/package.json +6 -3
- package/public/build.html +1 -1
- package/public/collections.html +1 -1
- package/public/dashboard.html +30 -3
- package/public/import.html +1 -1
- package/public/media.html +1 -1
- package/public/settings.html +27 -3
- package/public/sidebar.js +11 -0
- package/public/style.css +1142 -0
- package/public/theme.js +2 -1
- package/public/users.html +1 -1
- package/public/xfce.js +697 -0
- package/src/cli.js +30 -0
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
|
+
})();
|