@a83/orbiter-admin 0.3.40 → 0.3.42

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a83/orbiter-admin",
3
- "version": "0.3.40",
3
+ "version": "0.3.42",
4
4
  "description": "Standalone admin server for Orbiter CMS",
5
5
  "type": "module",
6
6
  "main": "./src/server.js",
package/public/style.css CHANGED
@@ -2028,23 +2028,52 @@ a.xfce-sb-logo:hover { opacity: .8; }
2028
2028
  }
2029
2029
  .xfce-dock-item { position: relative; }
2030
2030
 
2031
- /* Hover + badge above collection dock items */
2032
- .xfce-col-create {
2031
+ /* Hover preview card above collection dock items */
2032
+ .xfce-col-preview {
2033
2033
  position: fixed; z-index: 99990;
2034
- width: 22px; height: 22px; border-radius: 50%;
2035
- background: var(--accent);
2036
- color: color-mix(in srgb, var(--bg1) 15%, #000);
2037
- font-size: 17px; line-height: 22px; font-weight: 300;
2038
- text-align: center; text-decoration: none;
2039
2034
  transform: translateX(-50%) translateY(6px);
2040
2035
  opacity: 0; pointer-events: none;
2041
- transition: opacity .13s, transform .13s cubic-bezier(.34,1.5,.64,1);
2042
- box-shadow: 0 0 10px color-mix(in srgb, var(--accent) 55%, transparent);
2043
- user-select: none;
2036
+ transition: opacity .15s, transform .15s cubic-bezier(.34,1.4,.64,1);
2037
+ background: var(--bg1); border: 1px solid var(--line);
2038
+ border-top: 2px solid var(--accent);
2039
+ border-radius: 10px; min-width: 180px; max-width: 240px;
2040
+ box-shadow: 0 4px 24px rgba(0,0,0,.25);
2041
+ overflow: hidden; font-family: var(--mono);
2044
2042
  }
2045
- .xfce-col-create.visible {
2043
+ .xfce-col-preview.visible {
2046
2044
  opacity: 1; transform: translateX(-50%) translateY(0); pointer-events: auto;
2047
2045
  }
2046
+ html[data-dock-pos="left"] .xfce-col-preview {
2047
+ transform: translateX(0) translateY(-50%);
2048
+ }
2049
+ html[data-dock-pos="left"] .xfce-col-preview.visible {
2050
+ transform: translateX(6px) translateY(-50%);
2051
+ }
2052
+ .xfce-preview-head { padding: 7px 12px; border-bottom: 1px solid var(--line); }
2053
+ .xfce-preview-head a { font-size: 10px; font-weight: 600; color: var(--accent); text-decoration: none; text-transform: uppercase; letter-spacing: .06em; }
2054
+ .xfce-preview-head a:hover { text-decoration: underline; }
2055
+ .xfce-preview-entries { padding: 4px 0; }
2056
+ .xfce-preview-row {
2057
+ display: flex; align-items: center; justify-content: space-between;
2058
+ padding: 4px 12px; gap: 8px; text-decoration: none;
2059
+ transition: background .1s;
2060
+ }
2061
+ .xfce-preview-row:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
2062
+ .xfce-preview-slug { font-size: 10px; color: var(--body); flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
2063
+ .xfce-preview-date { font-size: 9px; color: var(--muted); flex-shrink: 0; }
2064
+ .xfce-preview-empty { padding: 6px 12px; font-size: 10px; color: var(--muted); }
2065
+ .xfce-preview-loading { padding: 10px 12px; font-size: 10px; color: var(--muted); }
2066
+ .xfce-preview-actions {
2067
+ display: flex; border-top: 1px solid var(--line);
2068
+ }
2069
+ .xfce-preview-action {
2070
+ flex: 1; padding: 6px 8px; font-size: 10px; font-family: var(--mono);
2071
+ color: var(--accent); text-decoration: none; text-align: center;
2072
+ background: none; border: none; cursor: pointer;
2073
+ transition: background .1s; white-space: nowrap;
2074
+ }
2075
+ .xfce-preview-action + .xfce-preview-action { border-left: 1px solid var(--line); }
2076
+ .xfce-preview-action:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
2048
2077
 
2049
2078
  /* ── Command Palette ─────────────────────────────────────── */
2050
2079
  .xfce-palette {
@@ -2123,27 +2152,49 @@ a.xfce-sb-logo:hover { opacity: .8; }
2123
2152
 
2124
2153
  /* ── Status bar build indicator ──────────────────────────── */
2125
2154
  .xfce-sb-build { font-size: 9px; font-family: var(--mono); color: var(--muted); white-space: nowrap; }
2126
-
2127
- /* ── Dock context menu ───────────────────────────────────── */
2128
- .xfce-ctx-menu {
2129
- display: none; position: fixed; z-index: 100000;
2130
- background: var(--bg1); border: 1px solid var(--line);
2131
- border-top: 2px solid var(--accent);
2132
- border-radius: 8px; padding: 4px 0;
2133
- box-shadow: 0 4px 24px rgba(0,0,0,.25);
2134
- min-width: 160px;
2135
- opacity: 0; transform: scale(.96); transform-origin: top left;
2136
- transition: opacity .1s, transform .1s;
2155
+ .xfce-sb-g-ind {
2156
+ font-size: 10px; font-family: var(--mono); font-weight: 700; white-space: nowrap;
2157
+ color: var(--bg1); background: var(--accent);
2158
+ border-radius: 4px; padding: 1px 6px; letter-spacing: .04em;
2159
+ animation: xfce-blink .6s step-end infinite;
2160
+ }
2161
+ @keyframes xfce-blink { 0%,100% { opacity:1; } 50% { opacity:.4; } }
2162
+
2163
+ /* ── Left dock mode ───────────────────────────────────────── */
2164
+ html[data-style="xfce"][data-dock-pos="left"] .main {
2165
+ padding-bottom: 20px !important;
2166
+ padding-left: 100px !important;
2167
+ }
2168
+ html[data-style="xfce"][data-dock-pos="left"] .xfce-dock {
2169
+ bottom: auto; left: 16px; top: 50%;
2170
+ transform: translateY(-50%);
2171
+ padding: 10px 6px;
2172
+ align-items: center;
2137
2173
  }
2138
- .xfce-ctx-menu.open { opacity: 1; transform: scale(1); }
2139
- .xfce-ctx-item {
2140
- display: flex; align-items: center; gap: 10px;
2141
- width: 100%; background: none; border: none; cursor: pointer;
2142
- padding: 7px 14px; font-size: 12px; font-family: var(--mono);
2143
- color: var(--body); text-align: left; transition: background .1s;
2174
+ html[data-style="xfce"][data-dock-pos="left"] .xfce-dock-inner {
2175
+ flex-direction: column; align-items: center; gap: 4px;
2176
+ }
2177
+ html[data-style="xfce"][data-dock-pos="left"] .xfce-dock-group {
2178
+ flex-direction: column; align-items: center; gap: 4px;
2179
+ }
2180
+ html[data-style="xfce"][data-dock-pos="left"] .xfce-dock-sep {
2181
+ width: 28px; height: 1px; margin: 2px 0;
2182
+ }
2183
+ html[data-style="xfce"][data-dock-pos="left"] .xfce-dock-item {
2184
+ transform-origin: left center;
2185
+ }
2186
+ html[data-style="xfce"][data-dock-pos="left"] .xfce-toast-host {
2187
+ bottom: 20px; left: 110px; transform: none;
2188
+ }
2189
+ html[data-style="xfce"][data-dock-pos="left"] .xfce-palette {
2190
+ padding-bottom: 20px; padding-left: 100px; align-items: center;
2191
+ }
2192
+ html[data-style="xfce"][data-dock-pos="left"] .xfce-tools-popup {
2193
+ bottom: auto;
2194
+ }
2195
+ html[data-style="xfce"][data-dock-pos="left"] .xfce-ws-overlay {
2196
+ bottom: auto; left: 110px; top: 50%; transform: translateY(-50%);
2144
2197
  }
2145
- .xfce-ctx-item:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--heading); }
2146
- .xfce-ctx-icon { width: 16px; text-align: center; color: var(--accent); flex-shrink: 0; }
2147
2198
 
2148
2199
  /* ── Toast host (above dock) ─────────────────────────────── */
2149
2200
  .xfce-toast-host {
package/public/xfce.js CHANGED
@@ -63,6 +63,7 @@
63
63
  '</div>',
64
64
  '<div class="xfce-sb-center" id="xfce-sb-title"></div>',
65
65
  '<div class="xfce-sb-right">',
66
+ '<span id="xfce-sb-g-ind" class="xfce-sb-g-ind" style="display:none" title="g mode: d=dashboard m=media s=settings u=users b=build i=import h=schema a=account">g ›</span>',
66
67
  '<button id="xfce-sb-palette-btn" class="xfce-sb-palette-btn" title="Command palette (⌘K)">⌘</button>',
67
68
  '<span class="xfce-sb-div">·</span>',
68
69
  '<span id="xfce-sb-build" class="xfce-sb-build" title="Last build"></span>',
@@ -168,6 +169,30 @@
168
169
  openPalette();
169
170
  });
170
171
  toolsPopup.appendChild(palBtn);
172
+
173
+ var dockSep2 = document.createElement('div');
174
+ dockSep2.className = 'xfce-tools-sep';
175
+ toolsPopup.appendChild(dockSep2);
176
+
177
+ var dockPosBtn = el('button', 'xfce-tools-item');
178
+ dockPosBtn.id = 'xfce-dock-pos-btn';
179
+ function updateDockPosLabel() {
180
+ var pos = document.documentElement.dataset.dockPos || 'bottom';
181
+ dockPosBtn.innerHTML = '<span class="xfce-tools-icon">' + (pos === 'left' ? '⬌' : '⬍') + '</span>'
182
+ + '<span>Dock: ' + (pos === 'left' ? 'left' : 'bottom') + '</span>';
183
+ }
184
+ updateDockPosLabel();
185
+ dockPosBtn.addEventListener('click', function (e) {
186
+ e.stopPropagation();
187
+ var cur = document.documentElement.dataset.dockPos || 'bottom';
188
+ var next = cur === 'bottom' ? 'left' : 'bottom';
189
+ localStorage.setItem('orb_dock_pos', next);
190
+ document.documentElement.dataset.dockPos = next;
191
+ updateDockPosLabel();
192
+ toolsPopup.classList.remove('open');
193
+ _previewCache = {};
194
+ });
195
+ toolsPopup.appendChild(dockPosBtn);
171
196
  document.body.appendChild(toolsPopup);
172
197
  document.addEventListener('click', function () {
173
198
  toolsPopup.classList.remove('open');
@@ -179,40 +204,123 @@
179
204
 
180
205
  function toggleToolsPopup() {
181
206
  if (!toolsPopup) buildToolsPopup();
182
- var btn = document.getElementById('xfce-tools-btn');
183
- if (btn) {
184
- var rect = btn.getBoundingClientRect();
185
- toolsPopup.style.left = Math.round(rect.left + rect.width / 2) + 'px';
207
+ var btn = document.getElementById('xfce-tools-btn');
208
+ var dock = document.getElementById('xfce-dock');
209
+ var isLeft = document.documentElement.dataset.dockPos === 'left';
210
+ if (btn && dock) {
211
+ var bRect = btn.getBoundingClientRect();
212
+ var dRect = dock.getBoundingClientRect();
213
+ if (isLeft) {
214
+ toolsPopup.style.left = Math.round(dRect.right + 10) + 'px';
215
+ toolsPopup.style.top = Math.round(bRect.top + bRect.height / 2) + 'px';
216
+ toolsPopup.style.bottom = 'auto';
217
+ toolsPopup.style.transform = 'translateY(-50%)';
218
+ } else {
219
+ toolsPopup.style.left = Math.round(bRect.left + bRect.width / 2) + 'px';
220
+ toolsPopup.style.top = '';
221
+ toolsPopup.style.bottom = '';
222
+ toolsPopup.style.transform = '';
223
+ }
186
224
  }
187
225
  toolsPopup.classList.toggle('open');
188
226
  }
189
227
 
190
- // ── Hover + badge above collection items ─────────────────────────────
191
- var colCreateEl, colCreateTimer;
228
+ // ── Hover preview card above collection items ────────────────────────
229
+ var _previewEl = null, _previewTimer = null, _previewCache = {};
192
230
 
193
- function buildColCreate() {
194
- colCreateEl = el('a', 'xfce-col-create');
195
- colCreateEl.id = 'xfce-col-create';
196
- colCreateEl.textContent = '+';
197
- colCreateEl.addEventListener('mouseenter', function () { clearTimeout(colCreateTimer); });
198
- colCreateEl.addEventListener('mouseleave', function () { colCreateTimer = setTimeout(hideColCreate, 120); });
199
- document.body.appendChild(colCreateEl);
231
+ function buildPreview() {
232
+ _previewEl = el('div', 'xfce-col-preview');
233
+ _previewEl.id = 'xfce-col-preview';
234
+ _previewEl.addEventListener('mouseenter', function () { clearTimeout(_previewTimer); });
235
+ _previewEl.addEventListener('mouseleave', function () { _previewTimer = setTimeout(hideColPreview, 150); });
236
+ document.body.appendChild(_previewEl);
200
237
  }
201
238
 
202
- function showColCreate(href, itemEl) {
203
- if (!colCreateEl) buildColCreate();
204
- clearTimeout(colCreateTimer);
205
- var dock = document.getElementById('xfce-dock');
206
- var dockTop = dock ? dock.getBoundingClientRect().top : 0;
207
- var itemRect = itemEl.getBoundingClientRect();
208
- colCreateEl.href = href;
209
- colCreateEl.style.left = Math.round(itemRect.left + itemRect.width / 2) + 'px';
210
- colCreateEl.style.top = Math.round(dockTop - 34) + 'px';
211
- colCreateEl.classList.add('visible');
239
+ function showColPreview(col, itemEl) {
240
+ if (!_previewEl) buildPreview();
241
+ clearTimeout(_previewTimer);
242
+ _previewTimer = setTimeout(function () { _renderPreview(col, itemEl); }, 280);
212
243
  }
213
244
 
214
- function hideColCreate() {
215
- if (colCreateEl) colCreateEl.classList.remove('visible');
245
+ function _renderPreview(col, itemEl) {
246
+ var dock = document.getElementById('xfce-dock');
247
+ var isLeft = document.documentElement.dataset.dockPos === 'left';
248
+ var dockR = dock ? dock.getBoundingClientRect() : { top: 0, right: 0 };
249
+ var itemR = itemEl.getBoundingClientRect();
250
+
251
+ function place() {
252
+ if (isLeft) {
253
+ _previewEl.style.left = Math.round(dockR.right + 10) + 'px';
254
+ _previewEl.style.top = Math.round(itemR.top + itemR.height / 2 - _previewEl.offsetHeight / 2) + 'px';
255
+ _previewEl.style.bottom = 'auto';
256
+ } else {
257
+ var cx = Math.round(itemR.left + itemR.width / 2);
258
+ _previewEl.style.left = cx + 'px';
259
+ _previewEl.style.bottom = Math.round(window.innerHeight - dockR.top + 10) + 'px';
260
+ _previewEl.style.top = 'auto';
261
+ }
262
+ }
263
+
264
+ var newHref = '/editor.html?collection=' + encodeURIComponent(col.id);
265
+ var entriesHref = col.singleton
266
+ ? newHref + '&singleton=1'
267
+ : '/entries.html?col=' + encodeURIComponent(col.id) + '&label=' + encodeURIComponent(col.label);
268
+
269
+ function render(entries) {
270
+ var rows = entries.length
271
+ ? entries.slice(0, 3).map(function (e) {
272
+ var slug = e.slug || e.id || '';
273
+ var date = (e.updated_at || e.created_at || '').substring(5, 10);
274
+ var href = '/editor.html?collection=' + encodeURIComponent(col.id) + '&slug=' + encodeURIComponent(slug);
275
+ return '<a class="xfce-preview-row" href="' + href + '">'
276
+ + '<span class="xfce-preview-slug">' + escHtml(slug) + '</span>'
277
+ + '<span class="xfce-preview-date">' + date + '</span>'
278
+ + '</a>';
279
+ }).join('')
280
+ : '<div class="xfce-preview-empty">no entries yet</div>';
281
+ _previewEl.innerHTML =
282
+ '<div class="xfce-preview-head"><a href="' + entriesHref + '">' + escHtml(col.label) + '</a></div>'
283
+ + '<div class="xfce-preview-entries">' + rows + '</div>'
284
+ + '<div class="xfce-preview-actions">'
285
+ + '<a class="xfce-preview-action" href="' + newHref + '">+ new entry</a>'
286
+ + '<a class="xfce-preview-action" href="' + entriesHref + '">◫ view all</a>'
287
+ + '<button class="xfce-preview-action xfce-preview-export" data-col="' + escHtml(col.id) + '">↓ export</button>'
288
+ + '</div>';
289
+ var expBtn = _previewEl.querySelector('.xfce-preview-export');
290
+ if (expBtn) expBtn.addEventListener('click', function (ev) {
291
+ ev.preventDefault();
292
+ fetch('/api/terminal/export?col=' + encodeURIComponent(col.id) + '&format=json&drafts=0', { credentials: 'include' })
293
+ .then(function (r) { return r.blob(); })
294
+ .then(function (blob) {
295
+ var a = document.createElement('a');
296
+ a.href = URL.createObjectURL(blob); a.download = col.id + '.json';
297
+ document.body.appendChild(a); a.click(); a.remove();
298
+ });
299
+ hideColPreview();
300
+ });
301
+ _previewEl.classList.add('visible');
302
+ place();
303
+ }
304
+
305
+ if (_previewCache[col.id]) {
306
+ render(_previewCache[col.id]);
307
+ return;
308
+ }
309
+ _previewEl.innerHTML = '<div class="xfce-preview-loading">…</div>';
310
+ _previewEl.classList.add('visible');
311
+ place();
312
+ fetch('/api/collections/' + encodeURIComponent(col.id) + '/entries?limit=3', { credentials: 'include' })
313
+ .then(function (r) { return r.ok ? r.json() : null; })
314
+ .then(function (d) {
315
+ var entries = d ? (d.entries || (Array.isArray(d) ? d : [])) : [];
316
+ _previewCache[col.id] = entries;
317
+ if (_previewEl.classList.contains('visible')) render(entries);
318
+ });
319
+ }
320
+
321
+ function hideColPreview() {
322
+ clearTimeout(_previewTimer);
323
+ if (_previewEl) _previewEl.classList.remove('visible');
216
324
  }
217
325
 
218
326
  // ── Command Palette ───────────────────────────────────────────────────
@@ -1000,19 +1108,25 @@
1000
1108
  if (focusedEl) exitFocusMode();
1001
1109
  }, true);
1002
1110
 
1111
+ // Apply saved dock position
1112
+ var _savedDockPos = localStorage.getItem('orb_dock_pos') || 'bottom';
1113
+ document.documentElement.dataset.dockPos = _savedDockPos;
1114
+
1003
1115
  // ── Magnification ─────────────────────────────────────────────────
1004
- function applyMag(cx) {
1005
- var items = dockInner.querySelectorAll('.xfce-dock-item');
1116
+ function applyMag(cx, cy) {
1117
+ var isLeft = document.documentElement.dataset.dockPos === 'left';
1118
+ var items = dockInner.querySelectorAll('.xfce-dock-item');
1006
1119
  items.forEach(function (item) {
1007
1120
  var r = item.getBoundingClientRect();
1008
- var mid = r.left + r.width / 2;
1009
- var d = Math.abs(cx - mid);
1121
+ var mid = isLeft ? (r.top + r.height / 2) : (r.left + r.width / 2);
1122
+ var pos = isLeft ? cy : cx;
1123
+ var d = Math.abs(pos - mid);
1010
1124
  var s = d < 80 ? 1 + (1 - d / 80) * 0.50 : 1;
1011
1125
  item.style.setProperty('--ds', s.toFixed(3));
1012
1126
  });
1013
1127
  }
1014
1128
 
1015
- dock.addEventListener('mousemove', function (e) { applyMag(e.clientX); });
1129
+ dock.addEventListener('mousemove', function (e) { applyMag(e.clientX, e.clientY); });
1016
1130
  dock.addEventListener('mouseleave', function () {
1017
1131
  dockInner.querySelectorAll('.xfce-dock-item').forEach(function (item) {
1018
1132
  item.style.setProperty('--ds', '1');
@@ -1056,15 +1170,9 @@
1056
1170
  item.appendChild(badge);
1057
1171
  }
1058
1172
 
1059
- // Hover shows + badge above this item
1060
- var createHref = col.singleton
1061
- ? '/editor.html?collection=' + encodeURIComponent(col.id) + '&singleton=1'
1062
- : '/editor.html?collection=' + encodeURIComponent(col.id);
1063
- item.addEventListener('mouseenter', function () { showColCreate(createHref, item); });
1064
- item.addEventListener('mouseleave', function () { colCreateTimer = setTimeout(hideColCreate, 120); });
1065
-
1066
- // Right-click context menu
1067
- addDockCtxMenu(item, col);
1173
+ // Hover shows preview card with recent entries
1174
+ item.addEventListener('mouseenter', function () { showColPreview(col, item); });
1175
+ item.addEventListener('mouseleave', function () { _previewTimer = setTimeout(hideColPreview, 150); });
1068
1176
 
1069
1177
  colGroup.appendChild(item);
1070
1178
 
@@ -1124,6 +1232,17 @@
1124
1232
  }
1125
1233
 
1126
1234
  // ── Keyboard shortcuts ────────────────────────────────────────────────
1235
+ var _gPending = false, _gTimer = null;
1236
+ var G_MAP = { d: '/dashboard.html', m: '/media.html', s: '/settings.html',
1237
+ u: '/users.html', b: '/build.html', i: '/import.html',
1238
+ h: '/schema.html', a: '/account.html' };
1239
+
1240
+ function setGMode(on) {
1241
+ _gPending = on;
1242
+ var ind = document.getElementById('xfce-sb-g-ind');
1243
+ if (ind) ind.style.display = on ? '' : 'none';
1244
+ }
1245
+
1127
1246
  function bindKeys() {
1128
1247
  // Capture-phase ⌘K: fires before admin-utils.js bubble-phase listener, stops it
1129
1248
  document.addEventListener('keydown', function (e) {
@@ -1146,11 +1265,26 @@
1146
1265
  // / — open palette (when not typing in an input; Shift+7 on DE keyboard also produces '/')
1147
1266
  if (!mod && !e.altKey && e.key === '/' && !isEditing(e.target)) {
1148
1267
  e.preventDefault();
1268
+ setGMode(false); clearTimeout(_gTimer);
1149
1269
  if (palette && palette.classList.contains('open')) closePalette();
1150
1270
  else openPalette();
1151
1271
  return;
1152
1272
  }
1153
1273
 
1274
+ // g — vim-style navigation prefix (g d = dashboard, g m = media, …)
1275
+ if (!mod && !e.shiftKey && !e.altKey && e.key === 'g' && !isEditing(e.target)) {
1276
+ e.preventDefault();
1277
+ setGMode(true);
1278
+ clearTimeout(_gTimer);
1279
+ _gTimer = setTimeout(function () { setGMode(false); }, 1500);
1280
+ return;
1281
+ }
1282
+ if (_gPending && !isEditing(e.target)) {
1283
+ clearTimeout(_gTimer); setGMode(false);
1284
+ if (G_MAP[e.key]) { e.preventDefault(); location.href = G_MAP[e.key]; }
1285
+ return;
1286
+ }
1287
+
1154
1288
  // ⌘⇧D — toggle HUD
1155
1289
  if (mod && e.shiftKey && (e.key === 'd' || e.key === 'D')) {
1156
1290
  e.preventDefault();
@@ -1206,81 +1340,6 @@
1206
1340
  });
1207
1341
  }
1208
1342
 
1209
- // ── Dock right-click context menu ─────────────────────────────────────
1210
- var _ctxMenu = null;
1211
-
1212
- function buildCtxMenu() {
1213
- _ctxMenu = document.createElement('div');
1214
- _ctxMenu.className = 'xfce-ctx-menu';
1215
- _ctxMenu.id = 'xfce-ctx-menu';
1216
- document.body.appendChild(_ctxMenu);
1217
- document.addEventListener('click', function (e) {
1218
- if (_ctxMenu && !_ctxMenu.contains(e.target)) closeCtxMenu();
1219
- }, true);
1220
- document.addEventListener('keydown', function (e) {
1221
- if (e.key === 'Escape') closeCtxMenu();
1222
- });
1223
- }
1224
-
1225
- function openCtxMenu(x, y, items) {
1226
- if (!_ctxMenu) buildCtxMenu();
1227
- _ctxMenu.innerHTML = items.map(function (it) {
1228
- return '<button class="xfce-ctx-item" data-href="' + (it.href || '') + '">'
1229
- + '<span class="xfce-ctx-icon">' + it.icon + '</span>'
1230
- + '<span>' + it.label + '</span>'
1231
- + '</button>';
1232
- }).join('');
1233
- _ctxMenu.querySelectorAll('.xfce-ctx-item').forEach(function (btn) {
1234
- btn.addEventListener('click', function () {
1235
- var href = btn.dataset.href;
1236
- closeCtxMenu();
1237
- if (href) location.href = href;
1238
- });
1239
- });
1240
- var vw = window.innerWidth, vh = window.innerHeight;
1241
- _ctxMenu.style.display = 'block';
1242
- var w = _ctxMenu.offsetWidth, h = _ctxMenu.offsetHeight;
1243
- _ctxMenu.style.left = Math.min(x, vw - w - 8) + 'px';
1244
- _ctxMenu.style.top = Math.min(y, vh - h - 8) + 'px';
1245
- _ctxMenu.classList.add('open');
1246
- }
1247
-
1248
- function closeCtxMenu() {
1249
- if (!_ctxMenu) return;
1250
- _ctxMenu.classList.remove('open');
1251
- setTimeout(function () { if (_ctxMenu) _ctxMenu.style.display = 'none'; }, 120);
1252
- }
1253
-
1254
- function addDockCtxMenu(item, col) {
1255
- item.addEventListener('contextmenu', function (e) {
1256
- e.preventDefault();
1257
- var entriesHref = col.singleton
1258
- ? '/editor.html?collection=' + encodeURIComponent(col.id) + '&singleton=1'
1259
- : '/entries.html?col=' + encodeURIComponent(col.id) + '&label=' + encodeURIComponent(col.label);
1260
- var newHref = '/editor.html?collection=' + encodeURIComponent(col.id);
1261
- openCtxMenu(e.clientX, e.clientY, [
1262
- { icon: '◫', label: 'View entries', href: entriesHref },
1263
- { icon: '+', label: 'New entry', href: newHref },
1264
- { icon: '↓', label: 'Export JSON', href: '' },
1265
- ]);
1266
- // Wire export separately (needs fetch, not href)
1267
- var exportBtn = _ctxMenu.querySelectorAll('.xfce-ctx-item')[2];
1268
- if (exportBtn) {
1269
- exportBtn.addEventListener('click', function (ev) {
1270
- ev.stopImmediatePropagation();
1271
- closeCtxMenu();
1272
- fetch('/api/terminal/export?col=' + encodeURIComponent(col.id) + '&format=json&drafts=0', { credentials: 'include' })
1273
- .then(function (r) { return r.blob(); })
1274
- .then(function (blob) {
1275
- var a = document.createElement('a');
1276
- a.href = URL.createObjectURL(blob);
1277
- a.download = col.id + '.json';
1278
- document.body.appendChild(a); a.click(); a.remove();
1279
- });
1280
- }, { once: true });
1281
- }
1282
- });
1283
- }
1284
1343
 
1285
1344
  // ── Init ──────────────────────────────────────────────────────────────
1286
1345
  function init() {