@a83/orbiter-admin 0.3.20 → 0.3.22
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 +1 -1
- package/public/style.css +97 -0
- package/public/xfce.js +245 -56
- package/src/middleware/csrf.js +37 -0
- package/src/routes/info.js +5 -4
- package/src/server.js +3 -0
package/package.json
CHANGED
package/public/style.css
CHANGED
|
@@ -1995,6 +1995,103 @@ html[data-style="xfce"] .xfce-in-focus .xfce-focus-trigger::after {
|
|
|
1995
1995
|
flex-shrink: 0;
|
|
1996
1996
|
}
|
|
1997
1997
|
|
|
1998
|
+
/* Status bar links & logout */
|
|
1999
|
+
a.xfce-sb-logo { text-decoration: none; color: var(--accent); }
|
|
2000
|
+
a.xfce-sb-logo:hover { opacity: .8; }
|
|
2001
|
+
.xfce-sb-user-link { color: var(--mid); text-decoration: none; font-size: 9px; font-family: var(--mono); }
|
|
2002
|
+
.xfce-sb-user-link:hover { color: var(--text); }
|
|
2003
|
+
.xfce-sb-logout {
|
|
2004
|
+
background: none; border: none; cursor: pointer;
|
|
2005
|
+
font-size: 10px; color: var(--muted); padding: 0; line-height: 1;
|
|
2006
|
+
font-family: var(--mono); transition: color .15s;
|
|
2007
|
+
}
|
|
2008
|
+
.xfce-sb-logout:hover { color: var(--red); }
|
|
2009
|
+
|
|
2010
|
+
/* Draft badge on dock items */
|
|
2011
|
+
.xfce-dock-badge {
|
|
2012
|
+
position: absolute; top: 2px; right: 2px;
|
|
2013
|
+
background: var(--gold); color: #000;
|
|
2014
|
+
font-size: 7px; font-family: var(--mono); font-weight: 700;
|
|
2015
|
+
min-width: 12px; height: 12px; border-radius: 6px;
|
|
2016
|
+
display: flex; align-items: center; justify-content: center;
|
|
2017
|
+
padding: 0 3px; pointer-events: none; line-height: 1;
|
|
2018
|
+
}
|
|
2019
|
+
.xfce-dock-item { position: relative; }
|
|
2020
|
+
|
|
2021
|
+
/* Quick-create button */
|
|
2022
|
+
.xfce-dock-create .xfce-dock-icon {
|
|
2023
|
+
font-size: 16px !important; font-family: inherit !important;
|
|
2024
|
+
color: var(--accent); font-weight: 300;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
/* ── Command Palette ─────────────────────────────────────── */
|
|
2028
|
+
.xfce-palette {
|
|
2029
|
+
display: none; position: fixed; inset: 0; z-index: 10000;
|
|
2030
|
+
background: rgba(0,0,0,.55); backdrop-filter: blur(4px);
|
|
2031
|
+
align-items: flex-start; justify-content: center;
|
|
2032
|
+
padding-top: 12vh;
|
|
2033
|
+
}
|
|
2034
|
+
.xfce-palette.open { display: flex; }
|
|
2035
|
+
|
|
2036
|
+
.xfce-palette-inner {
|
|
2037
|
+
width: min(560px, 92vw);
|
|
2038
|
+
background: var(--glass-bg, color-mix(in srgb, var(--bg1) 90%, transparent));
|
|
2039
|
+
border: 1px solid var(--line);
|
|
2040
|
+
border-radius: 12px;
|
|
2041
|
+
box-shadow: 0 24px 60px rgba(0,0,0,.5), 0 0 0 1px color-mix(in srgb,var(--accent) 20%,transparent);
|
|
2042
|
+
overflow: hidden;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
.xfce-palette-bar {
|
|
2046
|
+
display: flex; align-items: center; gap: 8px;
|
|
2047
|
+
padding: 10px 14px; border-bottom: 1px solid var(--line);
|
|
2048
|
+
}
|
|
2049
|
+
.xfce-palette-cmd { color: var(--accent); font-size: 13px; flex-shrink: 0; }
|
|
2050
|
+
.xfce-palette-inp {
|
|
2051
|
+
flex: 1; background: none; border: none; outline: none;
|
|
2052
|
+
font-family: var(--mono); font-size: 13px; color: var(--heading);
|
|
2053
|
+
}
|
|
2054
|
+
.xfce-palette-inp::placeholder { color: var(--muted); }
|
|
2055
|
+
.xfce-palette-hint { font-size: 8px; color: var(--muted); font-family: var(--mono); white-space: nowrap; }
|
|
2056
|
+
|
|
2057
|
+
.xfce-palette-results {
|
|
2058
|
+
max-height: 360px; overflow-y: auto; padding: 6px 0;
|
|
2059
|
+
}
|
|
2060
|
+
.xfce-pal-group {
|
|
2061
|
+
font-size: 8px; font-family: var(--mono); color: var(--muted);
|
|
2062
|
+
letter-spacing: .08em; text-transform: uppercase;
|
|
2063
|
+
padding: 8px 14px 4px;
|
|
2064
|
+
}
|
|
2065
|
+
.xfce-pal-item {
|
|
2066
|
+
display: flex; align-items: center; gap: 10px;
|
|
2067
|
+
padding: 7px 14px; cursor: pointer; transition: background .1s;
|
|
2068
|
+
}
|
|
2069
|
+
.xfce-pal-item:hover, .xfce-pal-item.pal-active {
|
|
2070
|
+
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
|
2071
|
+
}
|
|
2072
|
+
.xfce-pal-icon { width: 20px; text-align: center; font-size: 13px; color: var(--accent); flex-shrink: 0; }
|
|
2073
|
+
.xfce-pal-label { font-size: 12px; color: var(--heading); font-family: var(--mono); }
|
|
2074
|
+
.xfce-pal-empty { padding: 20px 14px; color: var(--muted); font-size: 11px; font-family: var(--mono); }
|
|
2075
|
+
|
|
2076
|
+
/* ── Toast host (above dock) ─────────────────────────────── */
|
|
2077
|
+
.xfce-toast-host {
|
|
2078
|
+
position: fixed; bottom: 90px; left: 50%; transform: translateX(-50%);
|
|
2079
|
+
z-index: 9990; display: flex; flex-direction: column; align-items: center; gap: 6px;
|
|
2080
|
+
pointer-events: none;
|
|
2081
|
+
}
|
|
2082
|
+
.xfce-toast {
|
|
2083
|
+
background: var(--bg2); border: 1px solid var(--line);
|
|
2084
|
+
border-radius: 8px; padding: 6px 14px;
|
|
2085
|
+
font-family: var(--mono); font-size: 10px; color: var(--text);
|
|
2086
|
+
box-shadow: 0 4px 16px rgba(0,0,0,.3);
|
|
2087
|
+
opacity: 0; transform: translateY(8px);
|
|
2088
|
+
transition: opacity .2s, transform .2s;
|
|
2089
|
+
white-space: nowrap;
|
|
2090
|
+
}
|
|
2091
|
+
.xfce-toast.show { opacity: 1; transform: translateY(0); }
|
|
2092
|
+
.xfce-toast.xfce-toast-success { border-color: var(--jade); color: var(--jade); }
|
|
2093
|
+
.xfce-toast.xfce-toast-error { border-color: var(--red); color: var(--red); }
|
|
2094
|
+
|
|
1998
2095
|
/* ════════════════════════════════════════════════════════════
|
|
1999
2096
|
XFCE — MOBILE (≤ 640px)
|
|
2000
2097
|
════════════════════════════════════════════════════════════ */
|
package/public/xfce.js
CHANGED
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
{ icon: '☑', label: 'To-do', pane: 'todos' },
|
|
29
29
|
];
|
|
30
30
|
|
|
31
|
+
// palette items populated after /api/info loads
|
|
32
|
+
var _palItems = NAV.concat(TOOLS).map(function (n) {
|
|
33
|
+
return { icon: n.icon, label: n.label, href: n.href, group: n.key in { schema:1, build:1, import:1 } ? 'Tools' : 'Nav' };
|
|
34
|
+
});
|
|
35
|
+
|
|
31
36
|
// ── Helpers ───────────────────────────────────────────────────────────
|
|
32
37
|
function el(tag, cls, html) {
|
|
33
38
|
var e = document.createElement(tag);
|
|
@@ -36,31 +41,38 @@
|
|
|
36
41
|
return e;
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
function isEditing(target) {
|
|
45
|
+
var t = target.tagName;
|
|
46
|
+
return t === 'INPUT' || t === 'TEXTAREA' || t === 'SELECT' || target.isContentEditable;
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
// ── Status Bar ────────────────────────────────────────────────────────
|
|
40
50
|
function buildStatusBar() {
|
|
41
51
|
var sb = el('div', 'xfce-sb');
|
|
42
52
|
sb.innerHTML = [
|
|
43
53
|
'<div class="xfce-sb-left">',
|
|
44
|
-
'<
|
|
54
|
+
'<a class="xfce-sb-logo" href="/dashboard.html">',
|
|
45
55
|
'<svg viewBox="0 0 20 20" width="12" height="12" fill="none" style="margin-right:5px;vertical-align:middle">',
|
|
46
56
|
'<circle cx="10" cy="10" r="4.5" fill="currentColor" opacity=".9"/>',
|
|
47
57
|
'<ellipse cx="10" cy="10" rx="9" ry="3.2" stroke="currentColor" stroke-width="1" opacity=".5" transform="rotate(-22 10 10)"/>',
|
|
48
58
|
'</svg>ORBITER',
|
|
49
|
-
'</
|
|
59
|
+
'</a>',
|
|
50
60
|
'<span class="xfce-sb-div">·</span>',
|
|
51
61
|
'<span id="xfce-sb-site">—</span>',
|
|
52
62
|
'</div>',
|
|
53
63
|
'<div class="xfce-sb-center" id="xfce-sb-title"></div>',
|
|
54
64
|
'<div class="xfce-sb-right">',
|
|
55
|
-
'<
|
|
65
|
+
'<a id="xfce-sb-user" href="/account.html" class="xfce-sb-user-link"></a>',
|
|
66
|
+
'<span class="xfce-sb-div">·</span>',
|
|
67
|
+
'<button id="xfce-sb-logout" class="xfce-sb-logout" title="Log out">⏻</button>',
|
|
56
68
|
'<span class="xfce-sb-div">·</span>',
|
|
57
69
|
'<span id="xfce-sb-clock"></span>',
|
|
58
70
|
'</div>',
|
|
59
71
|
].join('');
|
|
60
72
|
document.body.insertBefore(sb, document.body.firstChild);
|
|
61
73
|
|
|
62
|
-
// Page title from document.title
|
|
63
|
-
var title
|
|
74
|
+
// Page title from document.title
|
|
75
|
+
var title = document.title.replace(/\s*—\s*Orbiter.*$/, '').trim();
|
|
64
76
|
var titleEl = document.getElementById('xfce-sb-title');
|
|
65
77
|
if (titleEl && title) titleEl.textContent = title;
|
|
66
78
|
|
|
@@ -72,6 +84,12 @@
|
|
|
72
84
|
}
|
|
73
85
|
tick();
|
|
74
86
|
setInterval(tick, 15000);
|
|
87
|
+
|
|
88
|
+
// Logout
|
|
89
|
+
document.getElementById('xfce-sb-logout').addEventListener('click', function () {
|
|
90
|
+
fetch('/api/auth/logout', { method: 'POST', credentials: 'include' })
|
|
91
|
+
.finally(function () { location.href = '/login.html'; });
|
|
92
|
+
});
|
|
75
93
|
}
|
|
76
94
|
|
|
77
95
|
// ── HUD Meta Panel ────────────────────────────────────────────────────
|
|
@@ -100,7 +118,6 @@
|
|
|
100
118
|
metaPanel.classList.remove('open');
|
|
101
119
|
});
|
|
102
120
|
|
|
103
|
-
// Nav links inside HUD (all items including tools)
|
|
104
121
|
var navWrap = document.getElementById('xfce-hud-nav');
|
|
105
122
|
if (navWrap) {
|
|
106
123
|
NAV.concat(TOOLS).forEach(function (n) {
|
|
@@ -148,6 +165,152 @@
|
|
|
148
165
|
toolsPopup.classList.toggle('open');
|
|
149
166
|
}
|
|
150
167
|
|
|
168
|
+
// ── Command Palette ───────────────────────────────────────────────────
|
|
169
|
+
var palette, paletteInp, paletteResults, palActive = -1;
|
|
170
|
+
|
|
171
|
+
function buildPalette() {
|
|
172
|
+
palette = el('div', 'xfce-palette');
|
|
173
|
+
palette.id = 'xfce-palette';
|
|
174
|
+
palette.innerHTML = [
|
|
175
|
+
'<div class="xfce-palette-inner">',
|
|
176
|
+
'<div class="xfce-palette-bar">',
|
|
177
|
+
'<span class="xfce-palette-cmd">⌘</span>',
|
|
178
|
+
'<input id="xfce-palette-inp" class="xfce-palette-inp" placeholder="Go to page or collection…" autocomplete="off" spellcheck="false" />',
|
|
179
|
+
'<span class="xfce-palette-hint">ESC to close</span>',
|
|
180
|
+
'</div>',
|
|
181
|
+
'<div id="xfce-palette-results" class="xfce-palette-results"></div>',
|
|
182
|
+
'</div>',
|
|
183
|
+
].join('');
|
|
184
|
+
document.body.appendChild(palette);
|
|
185
|
+
|
|
186
|
+
paletteInp = document.getElementById('xfce-palette-inp');
|
|
187
|
+
paletteResults = document.getElementById('xfce-palette-results');
|
|
188
|
+
|
|
189
|
+
paletteInp.addEventListener('input', function () {
|
|
190
|
+
palActive = -1;
|
|
191
|
+
renderPalette(paletteInp.value);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
paletteInp.addEventListener('keydown', function (e) {
|
|
195
|
+
var items = paletteResults.querySelectorAll('.xfce-pal-item');
|
|
196
|
+
if (e.key === 'ArrowDown') {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
palActive = Math.min(palActive + 1, items.length - 1);
|
|
199
|
+
updatePalActive(items);
|
|
200
|
+
} else if (e.key === 'ArrowUp') {
|
|
201
|
+
e.preventDefault();
|
|
202
|
+
palActive = Math.max(palActive - 1, 0);
|
|
203
|
+
updatePalActive(items);
|
|
204
|
+
} else if (e.key === 'Enter') {
|
|
205
|
+
var active = paletteResults.querySelector('.xfce-pal-item.pal-active');
|
|
206
|
+
if (active) { location.href = active.dataset.href; closePalette(); }
|
|
207
|
+
else {
|
|
208
|
+
var first = paletteResults.querySelector('.xfce-pal-item');
|
|
209
|
+
if (first) { location.href = first.dataset.href; closePalette(); }
|
|
210
|
+
}
|
|
211
|
+
} else if (e.key === 'Escape') {
|
|
212
|
+
closePalette();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
palette.addEventListener('click', function (e) {
|
|
217
|
+
if (e.target === palette) closePalette();
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function updatePalActive(items) {
|
|
222
|
+
items.forEach(function (it, i) {
|
|
223
|
+
it.classList.toggle('pal-active', i === palActive);
|
|
224
|
+
if (i === palActive) it.scrollIntoView({ block: 'nearest' });
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function renderPalette(q) {
|
|
229
|
+
q = (q || '').toLowerCase().trim();
|
|
230
|
+
var filtered = q
|
|
231
|
+
? _palItems.filter(function (it) { return it.label.toLowerCase().includes(q); })
|
|
232
|
+
: _palItems;
|
|
233
|
+
|
|
234
|
+
if (!filtered.length) {
|
|
235
|
+
paletteResults.innerHTML = '<div class="xfce-pal-empty">No results</div>';
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Group items
|
|
240
|
+
var groups = {};
|
|
241
|
+
filtered.forEach(function (it) {
|
|
242
|
+
if (!groups[it.group]) groups[it.group] = [];
|
|
243
|
+
groups[it.group].push(it);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
var html = '';
|
|
247
|
+
Object.keys(groups).forEach(function (g) {
|
|
248
|
+
html += '<div class="xfce-pal-group">' + g + '</div>';
|
|
249
|
+
groups[g].forEach(function (it) {
|
|
250
|
+
html += '<div class="xfce-pal-item" data-href="' + it.href + '">'
|
|
251
|
+
+ '<span class="xfce-pal-icon">' + it.icon + '</span>'
|
|
252
|
+
+ '<span class="xfce-pal-label">' + it.label + '</span>'
|
|
253
|
+
+ '</div>';
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
paletteResults.innerHTML = html;
|
|
257
|
+
|
|
258
|
+
paletteResults.querySelectorAll('.xfce-pal-item').forEach(function (item) {
|
|
259
|
+
item.addEventListener('click', function () {
|
|
260
|
+
location.href = item.dataset.href;
|
|
261
|
+
closePalette();
|
|
262
|
+
});
|
|
263
|
+
item.addEventListener('mouseenter', function () {
|
|
264
|
+
var items = paletteResults.querySelectorAll('.xfce-pal-item');
|
|
265
|
+
palActive = Array.from(items).indexOf(item);
|
|
266
|
+
updatePalActive(items);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function openPalette() {
|
|
272
|
+
if (!palette) buildPalette();
|
|
273
|
+
palette.classList.add('open');
|
|
274
|
+
paletteInp.value = '';
|
|
275
|
+
palActive = -1;
|
|
276
|
+
renderPalette('');
|
|
277
|
+
setTimeout(function () { paletteInp.focus(); }, 30);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function closePalette() {
|
|
281
|
+
if (palette) palette.classList.remove('open');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Toast system ──────────────────────────────────────────────────────
|
|
285
|
+
function buildToastHost() {
|
|
286
|
+
var host = el('div', 'xfce-toast-host');
|
|
287
|
+
host.id = 'xfce-toast-host';
|
|
288
|
+
document.body.appendChild(host);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
window.xfceToast = function (msg, type) {
|
|
292
|
+
var host = document.getElementById('xfce-toast-host');
|
|
293
|
+
if (!host) return;
|
|
294
|
+
var t = el('div', 'xfce-toast' + (type ? ' xfce-toast-' + type : ''));
|
|
295
|
+
t.textContent = msg;
|
|
296
|
+
host.appendChild(t);
|
|
297
|
+
requestAnimationFrame(function () { t.classList.add('show'); });
|
|
298
|
+
setTimeout(function () {
|
|
299
|
+
t.classList.remove('show');
|
|
300
|
+
setTimeout(function () { t.remove(); }, 300);
|
|
301
|
+
}, 2500);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
function observeSavedFlash() {
|
|
305
|
+
var flash = document.getElementById('saved-flash');
|
|
306
|
+
if (!flash) return;
|
|
307
|
+
new MutationObserver(function () {
|
|
308
|
+
if (flash.style.display !== 'none' && flash.textContent.trim()) {
|
|
309
|
+
window.xfceToast(flash.textContent.trim(), 'success');
|
|
310
|
+
}
|
|
311
|
+
}).observe(flash, { attributes: true, attributeFilter: ['style'] });
|
|
312
|
+
}
|
|
313
|
+
|
|
151
314
|
// ── Workspace overlay (Notes + To-do) ────────────────────────────────
|
|
152
315
|
var wsOverlay, wsActivePane = 'notes', wsNotesTimer, wsTodosData = [];
|
|
153
316
|
|
|
@@ -186,12 +349,10 @@
|
|
|
186
349
|
].join('');
|
|
187
350
|
document.body.appendChild(wsOverlay);
|
|
188
351
|
|
|
189
|
-
// Tabs
|
|
190
352
|
wsOverlay.querySelectorAll('.xfce-ws-tab').forEach(function (btn) {
|
|
191
353
|
btn.addEventListener('click', function () { switchWsPane(btn.dataset.pane); });
|
|
192
354
|
});
|
|
193
355
|
|
|
194
|
-
// Close
|
|
195
356
|
document.getElementById('xfce-ws-close').addEventListener('click', closeWorkspace);
|
|
196
357
|
document.addEventListener('keydown', function (e) {
|
|
197
358
|
if (e.key === 'Escape' && wsOverlay.classList.contains('open')) closeWorkspace();
|
|
@@ -202,7 +363,6 @@
|
|
|
202
363
|
closeWorkspace();
|
|
203
364
|
});
|
|
204
365
|
|
|
205
|
-
// Notes auto-save
|
|
206
366
|
var notesEl = document.getElementById('xfce-ws-notes');
|
|
207
367
|
var ind = document.getElementById('xfce-ws-ind');
|
|
208
368
|
notesEl.addEventListener('input', function () {
|
|
@@ -221,9 +381,8 @@
|
|
|
221
381
|
}, 1200);
|
|
222
382
|
});
|
|
223
383
|
|
|
224
|
-
// Todo add
|
|
225
384
|
function addTodo() {
|
|
226
|
-
var inp
|
|
385
|
+
var inp = document.getElementById('xfce-ws-todo-inp');
|
|
227
386
|
var text = inp.value.trim();
|
|
228
387
|
if (!text) return;
|
|
229
388
|
wsTodosData.push({ text: text, done: false });
|
|
@@ -236,13 +395,11 @@
|
|
|
236
395
|
if (e.key === 'Enter') addTodo();
|
|
237
396
|
});
|
|
238
397
|
|
|
239
|
-
// Clear done
|
|
240
398
|
document.getElementById('xfce-ws-todo-clear').addEventListener('click', function () {
|
|
241
399
|
wsTodosData = wsTodosData.filter(function (t) { return !t.done; });
|
|
242
400
|
renderTodos(); saveTodos();
|
|
243
401
|
});
|
|
244
402
|
|
|
245
|
-
// Export .md
|
|
246
403
|
document.getElementById('xfce-ws-export').addEventListener('click', function () {
|
|
247
404
|
var date = new Date().toISOString().slice(0, 10);
|
|
248
405
|
var text, filename;
|
|
@@ -306,7 +463,6 @@
|
|
|
306
463
|
}
|
|
307
464
|
|
|
308
465
|
function loadWsData() {
|
|
309
|
-
// On dashboard, read directly from the page's own elements if available
|
|
310
466
|
var dashNotes = document.getElementById('notes-area');
|
|
311
467
|
if (dashNotes) {
|
|
312
468
|
var ta = document.getElementById('xfce-ws-notes');
|
|
@@ -347,13 +503,12 @@
|
|
|
347
503
|
|
|
348
504
|
function closeWorkspace() {
|
|
349
505
|
if (wsOverlay) wsOverlay.classList.remove('open');
|
|
350
|
-
// Sync indicator on dock buttons
|
|
351
506
|
document.querySelectorAll('[data-wspane]').forEach(function (btn) {
|
|
352
507
|
btn.classList.remove('active');
|
|
353
508
|
});
|
|
354
509
|
}
|
|
355
510
|
|
|
356
|
-
// ── Focus mode
|
|
511
|
+
// ── Focus mode ────────────────────────────────────────────────────────
|
|
357
512
|
var focusedEl = null;
|
|
358
513
|
var focusOrigStyle = null;
|
|
359
514
|
|
|
@@ -368,11 +523,9 @@
|
|
|
368
523
|
|
|
369
524
|
function enterFocusMode(target) {
|
|
370
525
|
if (focusedEl) return;
|
|
371
|
-
|
|
372
526
|
var dim = document.getElementById('xfce-focus-dim') || buildFocusDim();
|
|
373
527
|
var rect = target.getBoundingClientRect();
|
|
374
528
|
|
|
375
|
-
// Capture current computed inline style so we can fully restore it
|
|
376
529
|
focusOrigStyle = {
|
|
377
530
|
position: target.style.position || '',
|
|
378
531
|
left: target.style.left || '',
|
|
@@ -391,7 +544,6 @@
|
|
|
391
544
|
var maxH = Math.round(window.innerHeight * 0.86);
|
|
392
545
|
var tl = Math.round((window.innerWidth - tw) / 2);
|
|
393
546
|
|
|
394
|
-
// Fix position and set target width — let height be auto so it fits content
|
|
395
547
|
target.style.transition = 'none';
|
|
396
548
|
target.style.position = 'fixed';
|
|
397
549
|
target.style.left = rect.left + 'px';
|
|
@@ -403,11 +555,9 @@
|
|
|
403
555
|
target.style.overflow = 'auto';
|
|
404
556
|
target.classList.add('xfce-in-focus');
|
|
405
557
|
|
|
406
|
-
// Reflow so the browser computes auto height at the new width
|
|
407
558
|
var actualH = Math.min(target.scrollHeight, maxH);
|
|
408
559
|
var tt = Math.round((window.innerHeight - actualH) / 2);
|
|
409
560
|
|
|
410
|
-
// Animate only position — height stays auto (content-driven)
|
|
411
561
|
target.style.transition = [
|
|
412
562
|
'left .32s cubic-bezier(.34,1.15,.64,1)',
|
|
413
563
|
'top .32s cubic-bezier(.34,1.15,.64,1)',
|
|
@@ -430,7 +580,6 @@
|
|
|
430
580
|
if (dim) dim.classList.remove('active');
|
|
431
581
|
document.removeEventListener('keydown', onFocusKey);
|
|
432
582
|
|
|
433
|
-
// Fade out, restore, fade back in
|
|
434
583
|
target.style.transition = 'opacity .18s';
|
|
435
584
|
target.style.opacity = '0';
|
|
436
585
|
|
|
@@ -493,7 +642,6 @@
|
|
|
493
642
|
});
|
|
494
643
|
dockInner.appendChild(navGroup);
|
|
495
644
|
|
|
496
|
-
// Separator
|
|
497
645
|
dockInner.appendChild(el('div', 'xfce-dock-sep'));
|
|
498
646
|
|
|
499
647
|
// Collections group (populated by /api/info)
|
|
@@ -501,10 +649,9 @@
|
|
|
501
649
|
colGroup.id = 'xfce-dock-cols';
|
|
502
650
|
dockInner.appendChild(colGroup);
|
|
503
651
|
|
|
504
|
-
// Separator
|
|
505
652
|
dockInner.appendChild(el('div', 'xfce-dock-sep'));
|
|
506
653
|
|
|
507
|
-
// Workspace group
|
|
654
|
+
// Workspace group
|
|
508
655
|
var wsGroup = el('div', 'xfce-dock-group');
|
|
509
656
|
WORKSPACE.forEach(function (w) {
|
|
510
657
|
var btn = makeDockItem(w.icon, w.label, null, false, true);
|
|
@@ -517,10 +664,9 @@
|
|
|
517
664
|
});
|
|
518
665
|
dockInner.appendChild(wsGroup);
|
|
519
666
|
|
|
520
|
-
// Separator
|
|
521
667
|
dockInner.appendChild(el('div', 'xfce-dock-sep'));
|
|
522
668
|
|
|
523
|
-
// Tools popup button
|
|
669
|
+
// Tools popup button
|
|
524
670
|
var toolsActive = TOOLS.some(function (t) { return t.key === page; });
|
|
525
671
|
var toolsBtn = makeDockItem('⚒', 'Tools', null, toolsActive, true);
|
|
526
672
|
toolsBtn.id = 'xfce-tools-btn';
|
|
@@ -530,16 +676,15 @@
|
|
|
530
676
|
});
|
|
531
677
|
dockInner.appendChild(toolsBtn);
|
|
532
678
|
|
|
533
|
-
// Separator
|
|
534
679
|
dockInner.appendChild(el('div', 'xfce-dock-sep'));
|
|
535
680
|
|
|
536
|
-
// HUD toggle
|
|
681
|
+
// HUD toggle
|
|
537
682
|
var hudBtn = makeDockItem('▣', 'HUD', null, false, true);
|
|
538
683
|
hudBtn.addEventListener('click', toggleHUD);
|
|
539
684
|
dockInner.appendChild(hudBtn);
|
|
540
685
|
|
|
541
686
|
// Scheme toggle
|
|
542
|
-
var schemeBtn
|
|
687
|
+
var schemeBtn = document.getElementById('scheme-toggle');
|
|
543
688
|
var schemeClone = makeDockItem('◐', 'Scheme', null, false, true);
|
|
544
689
|
schemeClone.addEventListener('click', function () {
|
|
545
690
|
if (schemeBtn) schemeBtn.click();
|
|
@@ -551,8 +696,6 @@
|
|
|
551
696
|
|
|
552
697
|
document.body.appendChild(dock);
|
|
553
698
|
|
|
554
|
-
// Any click inside the dock exits focus mode (capture phase runs
|
|
555
|
-
// before stopPropagation on individual buttons can block it)
|
|
556
699
|
dock.addEventListener('click', function () {
|
|
557
700
|
if (focusedEl) exitFocusMode();
|
|
558
701
|
}, true);
|
|
@@ -584,7 +727,7 @@
|
|
|
584
727
|
.then(function (info) {
|
|
585
728
|
if (!info) return;
|
|
586
729
|
|
|
587
|
-
// Site name
|
|
730
|
+
// Site name in status bar
|
|
588
731
|
var siteEl = document.getElementById('xfce-sb-site');
|
|
589
732
|
if (siteEl) siteEl.textContent = info.siteName || info.podPath.split('/').pop().replace('.pod', '');
|
|
590
733
|
|
|
@@ -592,7 +735,7 @@
|
|
|
592
735
|
var colGroup = document.getElementById('xfce-dock-cols');
|
|
593
736
|
if (colGroup) {
|
|
594
737
|
var topLevel = (info.collections || []).filter(function (c) { return !c.parent; });
|
|
595
|
-
topLevel.forEach(function (col) {
|
|
738
|
+
topLevel.forEach(function (col, idx) {
|
|
596
739
|
var isSingleton = !!col.singleton;
|
|
597
740
|
var href = isSingleton
|
|
598
741
|
? '/editor.html?collection=' + encodeURIComponent(col.id) + '&singleton=1'
|
|
@@ -600,12 +743,32 @@
|
|
|
600
743
|
var isActive = isSingleton
|
|
601
744
|
? page === 'editor' && activeCol === col.id
|
|
602
745
|
: page === 'entries' && activeCol === col.id;
|
|
603
|
-
// Abbreviation icon (first char of label)
|
|
604
746
|
var abbr = col.label.substring(0, 2);
|
|
605
747
|
var item = makeDockItem(abbr, col.label, href, isActive, false);
|
|
606
748
|
item.querySelector('.xfce-dock-icon').style.cssText = 'font-size:9px;font-family:var(--mono);letter-spacing:-.02em;line-height:1;';
|
|
749
|
+
item.dataset.dockIdx = idx + 1;
|
|
750
|
+
|
|
751
|
+
// Draft badge
|
|
752
|
+
if (col.drafts > 0) {
|
|
753
|
+
var badge = el('span', 'xfce-dock-badge');
|
|
754
|
+
badge.textContent = col.drafts;
|
|
755
|
+
badge.title = col.drafts + ' draft' + (col.drafts === 1 ? '' : 's');
|
|
756
|
+
item.appendChild(badge);
|
|
757
|
+
}
|
|
758
|
+
|
|
607
759
|
colGroup.appendChild(item);
|
|
760
|
+
|
|
761
|
+
// Add to palette
|
|
762
|
+
_palItems.push({ icon: abbr, label: col.label, href: href, group: 'Collections' });
|
|
608
763
|
});
|
|
764
|
+
|
|
765
|
+
// Quick-create: + button when viewing a collection's entries
|
|
766
|
+
if (page === 'entries' && activeCol) {
|
|
767
|
+
var createHref = '/editor.html?collection=' + encodeURIComponent(activeCol);
|
|
768
|
+
var createBtn = makeDockItem('+', 'New entry', createHref, false, false);
|
|
769
|
+
createBtn.classList.add('xfce-dock-create');
|
|
770
|
+
colGroup.appendChild(createBtn);
|
|
771
|
+
}
|
|
609
772
|
}
|
|
610
773
|
|
|
611
774
|
// HUD pod section
|
|
@@ -613,11 +776,11 @@
|
|
|
613
776
|
if (hudPod) {
|
|
614
777
|
var total = (info.collections || []).reduce(function (s, c) { return s + (c.total || 0); }, 0);
|
|
615
778
|
hudPod.innerHTML = [
|
|
616
|
-
hudRow('File',
|
|
617
|
-
hudRow('Format',
|
|
618
|
-
hudRow('Admin',
|
|
779
|
+
hudRow('File', info.podPath.split('/').pop()),
|
|
780
|
+
hudRow('Format', 'v' + info.formatVersion),
|
|
781
|
+
hudRow('Admin', 'v' + info.adminVersion),
|
|
619
782
|
hudRow('Collections', info.collections.length),
|
|
620
|
-
hudRow('
|
|
783
|
+
hudRow('Published', total),
|
|
621
784
|
].join('');
|
|
622
785
|
}
|
|
623
786
|
|
|
@@ -625,7 +788,8 @@
|
|
|
625
788
|
var hudCols = document.getElementById('xfce-hud-cols');
|
|
626
789
|
if (hudCols) {
|
|
627
790
|
hudCols.innerHTML = (info.collections || []).map(function (col) {
|
|
628
|
-
|
|
791
|
+
var draftTxt = col.drafts > 0 ? ', ' + col.drafts + ' draft' + (col.drafts === 1 ? '' : 's') : '';
|
|
792
|
+
return hudRow(col.label, col.total + ' published' + draftTxt);
|
|
629
793
|
}).join('');
|
|
630
794
|
}
|
|
631
795
|
})
|
|
@@ -641,18 +805,15 @@
|
|
|
641
805
|
})
|
|
642
806
|
.catch(function () {});
|
|
643
807
|
|
|
644
|
-
//
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
var
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
sbUser.textContent = topbarUser.textContent;
|
|
654
|
-
}
|
|
655
|
-
}, 300);
|
|
808
|
+
// Current user
|
|
809
|
+
fetch('/api/auth/me', { credentials: 'include' })
|
|
810
|
+
.then(function (r) { return r.ok ? r.json() : null; })
|
|
811
|
+
.then(function (d) {
|
|
812
|
+
if (!d || !d.user) return;
|
|
813
|
+
var sbUser = document.getElementById('xfce-sb-user');
|
|
814
|
+
if (sbUser) sbUser.textContent = d.user.username;
|
|
815
|
+
})
|
|
816
|
+
.catch(function () {});
|
|
656
817
|
}
|
|
657
818
|
|
|
658
819
|
function hudRow(label, value) {
|
|
@@ -663,18 +824,44 @@
|
|
|
663
824
|
function bindKeys() {
|
|
664
825
|
document.addEventListener('keydown', function (e) {
|
|
665
826
|
var mod = e.metaKey || e.ctrlKey;
|
|
666
|
-
if (!mod || !e.shiftKey) return;
|
|
667
827
|
|
|
668
|
-
|
|
828
|
+
// ⌘K — command palette
|
|
829
|
+
if (mod && !e.shiftKey && (e.key === 'k' || e.key === 'K')) {
|
|
830
|
+
e.preventDefault();
|
|
831
|
+
if (palette && palette.classList.contains('open')) closePalette();
|
|
832
|
+
else openPalette();
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// ⌘⇧D — toggle HUD
|
|
837
|
+
if (mod && e.shiftKey && (e.key === 'd' || e.key === 'D')) {
|
|
669
838
|
e.preventDefault();
|
|
670
839
|
toggleHUD();
|
|
840
|
+
return;
|
|
671
841
|
}
|
|
672
842
|
|
|
673
|
-
// ⌘⇧L —
|
|
674
|
-
if (e.key === 'l' || e.key === 'L') {
|
|
843
|
+
// ⌘⇧L — switch back to glass mode
|
|
844
|
+
if (mod && e.shiftKey && (e.key === 'l' || e.key === 'L')) {
|
|
675
845
|
e.preventDefault();
|
|
676
846
|
localStorage.setItem('orb_style', 'glass');
|
|
677
847
|
location.reload();
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// 1–9 — jump to nth dock link (no modifier, not in input)
|
|
852
|
+
if (!mod && !e.shiftKey && !e.altKey && !isEditing(e.target)) {
|
|
853
|
+
var n = parseInt(e.key);
|
|
854
|
+
if (n >= 1 && n <= 9) {
|
|
855
|
+
var links = dockInner
|
|
856
|
+
? Array.from(dockInner.querySelectorAll('a.xfce-dock-item')).filter(function (a) {
|
|
857
|
+
return !a.classList.contains('xfce-dock-create');
|
|
858
|
+
})
|
|
859
|
+
: [];
|
|
860
|
+
if (links[n - 1]) {
|
|
861
|
+
e.preventDefault();
|
|
862
|
+
location.href = links[n - 1].href;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
678
865
|
}
|
|
679
866
|
});
|
|
680
867
|
}
|
|
@@ -684,9 +871,11 @@
|
|
|
684
871
|
buildStatusBar();
|
|
685
872
|
buildMetaPanel();
|
|
686
873
|
buildDock();
|
|
874
|
+
buildToastHost();
|
|
687
875
|
loadInfo();
|
|
688
876
|
bindKeys();
|
|
689
877
|
initFocusMode();
|
|
878
|
+
observeSavedFlash();
|
|
690
879
|
}
|
|
691
880
|
|
|
692
881
|
if (document.readyState === 'loading') {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSRF protection via Origin / Referer header validation.
|
|
3
|
+
*
|
|
4
|
+
* sameSite: 'Strict' on the session cookie already blocks most CSRF vectors.
|
|
5
|
+
* This middleware adds a second layer: mutating requests (POST/PUT/PATCH/DELETE)
|
|
6
|
+
* must carry an Origin (or Referer) header that matches ALLOWED_ORIGINS.
|
|
7
|
+
*
|
|
8
|
+
* Browser fetch/XHR always sends Origin on cross-origin requests; same-origin
|
|
9
|
+
* requests send it on POST but not on GET. We only check mutating methods, so
|
|
10
|
+
* the case where Origin is absent on a same-site POST is fine — a same-site
|
|
11
|
+
* request is already allowed.
|
|
12
|
+
*
|
|
13
|
+
* Requests with no Origin AND no Referer (e.g. curl without headers, Postman)
|
|
14
|
+
* are allowed — they can't carry the session cookie cross-site anyway.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const MUTATING = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
18
|
+
|
|
19
|
+
export function csrfMiddleware(allowedOrigins) {
|
|
20
|
+
return async (c, next) => {
|
|
21
|
+
if (!MUTATING.has(c.req.method)) return next();
|
|
22
|
+
|
|
23
|
+
const origin = c.req.header('origin');
|
|
24
|
+
const referer = c.req.header('referer');
|
|
25
|
+
|
|
26
|
+
// No origin/referer — non-browser client; skip check
|
|
27
|
+
if (!origin && !referer) return next();
|
|
28
|
+
|
|
29
|
+
const candidate = origin ?? new URL(referer).origin;
|
|
30
|
+
|
|
31
|
+
if (!allowedOrigins.includes(candidate)) {
|
|
32
|
+
return c.json({ error: 'CSRF check failed' }, 403);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return next();
|
|
36
|
+
};
|
|
37
|
+
}
|
package/src/routes/info.js
CHANGED
|
@@ -16,10 +16,11 @@ infoRoutes.get('/', (c) => {
|
|
|
16
16
|
const podPath = c.get('podPath');
|
|
17
17
|
const db = openPod(podPath);
|
|
18
18
|
const cols = db.getCollections().map(col => ({
|
|
19
|
-
id:
|
|
20
|
-
label:
|
|
21
|
-
total:
|
|
22
|
-
|
|
19
|
+
id: col.id,
|
|
20
|
+
label: col.label,
|
|
21
|
+
total: db.getEntries(col.id, { status: 'published' }).length,
|
|
22
|
+
drafts: db.getEntries(col.id, { status: 'draft' }).length,
|
|
23
|
+
parent: db.getMeta(`collection.${col.id}.parent`) ?? null,
|
|
23
24
|
singleton: !!col.singleton,
|
|
24
25
|
}));
|
|
25
26
|
const version = db.getMeta('format_version') ?? '1';
|
package/src/server.js
CHANGED
|
@@ -25,6 +25,7 @@ import { importRoutes } from './routes/import.js';
|
|
|
25
25
|
import { commentRoutes } from './routes/comments.js';
|
|
26
26
|
import { lockRoutes } from './routes/locks.js';
|
|
27
27
|
import { requireAuth } from './middleware/auth.js';
|
|
28
|
+
import { csrfMiddleware } from './middleware/csrf.js';
|
|
28
29
|
|
|
29
30
|
const { version: adminVersion } = JSON.parse(
|
|
30
31
|
readFileSync(join(__dirname, '../package.json'), 'utf8')
|
|
@@ -56,6 +57,8 @@ export function createApp(podPath) {
|
|
|
56
57
|
credentials: true,
|
|
57
58
|
}));
|
|
58
59
|
|
|
60
|
+
app.use('/api/*', csrfMiddleware(ALLOWED_ORIGINS));
|
|
61
|
+
|
|
59
62
|
// Public routes
|
|
60
63
|
app.route('/api/auth', authRoutes);
|
|
61
64
|
|