@a83/orbiter-admin 0.3.39 → 0.3.41

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.39",
3
+ "version": "0.3.41",
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,47 @@ 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-new {
2067
+ display: block; padding: 6px 12px; font-size: 10px;
2068
+ color: var(--accent); text-decoration: none; border-top: 1px solid var(--line);
2069
+ transition: background .1s;
2070
+ }
2071
+ .xfce-preview-new:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
2048
2072
 
2049
2073
  /* ── Command Palette ─────────────────────────────────────── */
2050
2074
  .xfce-palette {
@@ -2115,6 +2139,68 @@ a.xfce-sb-logo:hover { opacity: .8; }
2115
2139
  .xfce-pal-muted { color: var(--muted); opacity: .7; }
2116
2140
  .xfce-pal-cmd-hint { color: var(--accent); }
2117
2141
 
2142
+ /* search result with snippet */
2143
+ .xfce-pal-item--rich { align-items: flex-start; }
2144
+ .xfce-pal-item--rich .xfce-pal-icon { margin-top: 2px; }
2145
+ .xfce-pal-item-body { flex: 1; display: flex; flex-direction: column; gap: 1px; min-width: 0; }
2146
+ .xfce-pal-snippet { font-size: 10px; color: var(--muted); font-family: var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
2147
+
2148
+ /* ── Status bar build indicator ──────────────────────────── */
2149
+ .xfce-sb-build { font-size: 9px; font-family: var(--mono); color: var(--muted); white-space: nowrap; }
2150
+ .xfce-sb-g-ind { font-size: 10px; font-family: var(--mono); color: var(--accent); font-weight: 600; letter-spacing: .05em; animation: xfce-blink .7s step-end infinite; }
2151
+ @keyframes xfce-blink { 0%,100% { opacity:1; } 50% { opacity:.3; } }
2152
+
2153
+ /* ── Left dock mode ───────────────────────────────────────── */
2154
+ html[data-style="xfce"][data-dock-pos="left"] .main {
2155
+ padding-bottom: 20px !important;
2156
+ padding-left: 100px !important;
2157
+ }
2158
+ html[data-style="xfce"][data-dock-pos="left"] .xfce-dock {
2159
+ bottom: auto; left: 16px; top: 50%;
2160
+ transform: translateY(-50%);
2161
+ padding: 10px 6px;
2162
+ align-items: center;
2163
+ }
2164
+ html[data-style="xfce"][data-dock-pos="left"] .xfce-dock-inner {
2165
+ flex-direction: column; align-items: center; gap: 4px;
2166
+ }
2167
+ html[data-style="xfce"][data-dock-pos="left"] .xfce-dock-group {
2168
+ flex-direction: column; align-items: center; gap: 4px;
2169
+ }
2170
+ html[data-style="xfce"][data-dock-pos="left"] .xfce-dock-sep {
2171
+ width: 28px; height: 1px; margin: 2px 0;
2172
+ }
2173
+ html[data-style="xfce"][data-dock-pos="left"] .xfce-dock-item {
2174
+ transform-origin: left center;
2175
+ }
2176
+ html[data-style="xfce"][data-dock-pos="left"] .xfce-toast-host {
2177
+ bottom: 20px; left: 110px; transform: none;
2178
+ }
2179
+ html[data-style="xfce"][data-dock-pos="left"] .xfce-palette {
2180
+ padding-bottom: 20px; padding-left: 100px; align-items: center;
2181
+ }
2182
+
2183
+ /* ── Dock context menu ───────────────────────────────────── */
2184
+ .xfce-ctx-menu {
2185
+ display: none; position: fixed; z-index: 100000;
2186
+ background: var(--bg1); border: 1px solid var(--line);
2187
+ border-top: 2px solid var(--accent);
2188
+ border-radius: 8px; padding: 4px 0;
2189
+ box-shadow: 0 4px 24px rgba(0,0,0,.25);
2190
+ min-width: 160px;
2191
+ opacity: 0; transform: scale(.96); transform-origin: top left;
2192
+ transition: opacity .1s, transform .1s;
2193
+ }
2194
+ .xfce-ctx-menu.open { opacity: 1; transform: scale(1); }
2195
+ .xfce-ctx-item {
2196
+ display: flex; align-items: center; gap: 10px;
2197
+ width: 100%; background: none; border: none; cursor: pointer;
2198
+ padding: 7px 14px; font-size: 12px; font-family: var(--mono);
2199
+ color: var(--body); text-align: left; transition: background .1s;
2200
+ }
2201
+ .xfce-ctx-item:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--heading); }
2202
+ .xfce-ctx-icon { width: 16px; text-align: center; color: var(--accent); flex-shrink: 0; }
2203
+
2118
2204
  /* ── Toast host (above dock) ─────────────────────────────── */
2119
2205
  .xfce-toast-host {
2120
2206
  position: fixed; bottom: 90px; left: 50%; transform: translateX(-50%);
package/public/xfce.js CHANGED
@@ -63,8 +63,11 @@
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 — type destination key">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>',
69
+ '<span id="xfce-sb-build" class="xfce-sb-build" title="Last build"></span>',
70
+ '<span id="xfce-sb-build-sep" class="xfce-sb-div" style="display:none">·</span>',
68
71
  '<a id="xfce-sb-user" href="/account.html" class="xfce-sb-user-link"></a>',
69
72
  '<span class="xfce-sb-div">·</span>',
70
73
  '<button id="xfce-sb-logout" class="xfce-sb-logout" title="Log out">⏻</button>',
@@ -166,6 +169,30 @@
166
169
  openPalette();
167
170
  });
168
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);
169
196
  document.body.appendChild(toolsPopup);
170
197
  document.addEventListener('click', function () {
171
198
  toolsPopup.classList.remove('open');
@@ -185,36 +212,91 @@
185
212
  toolsPopup.classList.toggle('open');
186
213
  }
187
214
 
188
- // ── Hover + badge above collection items ─────────────────────────────
189
- var colCreateEl, colCreateTimer;
215
+ // ── Hover preview card above collection items ────────────────────────
216
+ var _previewEl = null, _previewTimer = null, _previewCache = {};
217
+
218
+ function buildPreview() {
219
+ _previewEl = el('div', 'xfce-col-preview');
220
+ _previewEl.id = 'xfce-col-preview';
221
+ _previewEl.addEventListener('mouseenter', function () { clearTimeout(_previewTimer); });
222
+ _previewEl.addEventListener('mouseleave', function () { _previewTimer = setTimeout(hideColPreview, 150); });
223
+ document.body.appendChild(_previewEl);
224
+ }
190
225
 
191
- function buildColCreate() {
192
- colCreateEl = el('a', 'xfce-col-create');
193
- colCreateEl.id = 'xfce-col-create';
194
- colCreateEl.textContent = '+';
195
- colCreateEl.addEventListener('mouseenter', function () { clearTimeout(colCreateTimer); });
196
- colCreateEl.addEventListener('mouseleave', function () { colCreateTimer = setTimeout(hideColCreate, 120); });
197
- document.body.appendChild(colCreateEl);
226
+ function showColPreview(col, itemEl) {
227
+ if (!_previewEl) buildPreview();
228
+ clearTimeout(_previewTimer);
229
+ _previewTimer = setTimeout(function () { _renderPreview(col, itemEl); }, 280);
198
230
  }
199
231
 
200
- function showColCreate(href, itemEl) {
201
- if (!colCreateEl) buildColCreate();
202
- clearTimeout(colCreateTimer);
203
- var dock = document.getElementById('xfce-dock');
204
- var dockTop = dock ? dock.getBoundingClientRect().top : 0;
205
- var itemRect = itemEl.getBoundingClientRect();
206
- colCreateEl.href = href;
207
- colCreateEl.style.left = Math.round(itemRect.left + itemRect.width / 2) + 'px';
208
- colCreateEl.style.top = Math.round(dockTop - 34) + 'px';
209
- colCreateEl.classList.add('visible');
232
+ function _renderPreview(col, itemEl) {
233
+ var dock = document.getElementById('xfce-dock');
234
+ var isLeft = document.documentElement.dataset.dockPos === 'left';
235
+ var dockR = dock ? dock.getBoundingClientRect() : { top: 0, right: 0 };
236
+ var itemR = itemEl.getBoundingClientRect();
237
+
238
+ function place() {
239
+ if (isLeft) {
240
+ _previewEl.style.left = Math.round(dockR.right + 10) + 'px';
241
+ _previewEl.style.top = Math.round(itemR.top + itemR.height / 2 - _previewEl.offsetHeight / 2) + 'px';
242
+ _previewEl.style.bottom = 'auto';
243
+ } else {
244
+ var cx = Math.round(itemR.left + itemR.width / 2);
245
+ _previewEl.style.left = cx + 'px';
246
+ _previewEl.style.bottom = Math.round(window.innerHeight - dockR.top + 10) + 'px';
247
+ _previewEl.style.top = 'auto';
248
+ }
249
+ }
250
+
251
+ var newHref = '/editor.html?collection=' + encodeURIComponent(col.id);
252
+ var entriesHref = col.singleton
253
+ ? newHref + '&singleton=1'
254
+ : '/entries.html?col=' + encodeURIComponent(col.id) + '&label=' + encodeURIComponent(col.label);
255
+
256
+ function render(entries) {
257
+ var rows = entries.length
258
+ ? entries.slice(0, 3).map(function (e) {
259
+ var slug = e.slug || e.id || '';
260
+ var date = (e.updated_at || e.created_at || '').substring(5, 10);
261
+ var href = '/editor.html?collection=' + encodeURIComponent(col.id) + '&slug=' + encodeURIComponent(slug);
262
+ return '<a class="xfce-preview-row" href="' + href + '">'
263
+ + '<span class="xfce-preview-slug">' + escHtml(slug) + '</span>'
264
+ + '<span class="xfce-preview-date">' + date + '</span>'
265
+ + '</a>';
266
+ }).join('')
267
+ : '<div class="xfce-preview-empty">no entries yet</div>';
268
+ _previewEl.innerHTML =
269
+ '<div class="xfce-preview-head"><a href="' + entriesHref + '">' + escHtml(col.label) + '</a></div>'
270
+ + '<div class="xfce-preview-entries">' + rows + '</div>'
271
+ + '<a class="xfce-preview-new" href="' + newHref + '">+ new entry</a>';
272
+ _previewEl.classList.add('visible');
273
+ place();
274
+ }
275
+
276
+ if (_previewCache[col.id]) {
277
+ render(_previewCache[col.id]);
278
+ return;
279
+ }
280
+ _previewEl.innerHTML = '<div class="xfce-preview-loading">…</div>';
281
+ _previewEl.classList.add('visible');
282
+ place();
283
+ fetch('/api/collections/' + encodeURIComponent(col.id) + '/entries?limit=3', { credentials: 'include' })
284
+ .then(function (r) { return r.ok ? r.json() : null; })
285
+ .then(function (d) {
286
+ var entries = d ? (d.entries || (Array.isArray(d) ? d : [])) : [];
287
+ _previewCache[col.id] = entries;
288
+ if (_previewEl.classList.contains('visible')) render(entries);
289
+ });
210
290
  }
211
291
 
212
- function hideColCreate() {
213
- if (colCreateEl) colCreateEl.classList.remove('visible');
292
+ function hideColPreview() {
293
+ clearTimeout(_previewTimer);
294
+ if (_previewEl) _previewEl.classList.remove('visible');
214
295
  }
215
296
 
216
297
  // ── Command Palette ───────────────────────────────────────────────────
217
298
  var palette, paletteInp, paletteResults, palActive = -1;
299
+ var _cmdHistory = [], _cmdHistIdx = -1;
218
300
 
219
301
  var NAV_DEST = {
220
302
  dashboard: '/dashboard.html', media: '/media.html', settings: '/settings.html',
@@ -247,10 +329,18 @@
247
329
 
248
330
  paletteInp.addEventListener('keydown', function (e) {
249
331
  var val = paletteInp.value.trim();
332
+ var inCmdMode = val.startsWith('>');
333
+
250
334
  if (e.key === 'Enter') {
251
- if (val.startsWith('>')) {
335
+ if (inCmdMode) {
252
336
  e.preventDefault();
253
- execPaletteCmd(val.slice(1).trim());
337
+ var cmd = val.slice(1).trim();
338
+ if (cmd) {
339
+ if (!_cmdHistory.length || _cmdHistory[0] !== cmd) _cmdHistory.unshift(cmd);
340
+ if (_cmdHistory.length > 30) _cmdHistory.pop();
341
+ }
342
+ _cmdHistIdx = -1;
343
+ execPaletteCmd(cmd);
254
344
  } else {
255
345
  var active = paletteResults.querySelector('.xfce-pal-item.pal-active');
256
346
  if (active && active.dataset.href) { location.href = active.dataset.href; closePalette(); }
@@ -259,16 +349,28 @@
259
349
  if (first) { location.href = first.dataset.href; closePalette(); }
260
350
  }
261
351
  }
262
- } else if (e.key === 'ArrowDown') {
263
- e.preventDefault();
264
- var items = paletteResults.querySelectorAll('.xfce-pal-item[data-href]');
265
- palActive = Math.min(palActive + 1, items.length - 1);
266
- updatePalActive(items);
267
352
  } else if (e.key === 'ArrowUp') {
268
353
  e.preventDefault();
269
- var items = paletteResults.querySelectorAll('.xfce-pal-item[data-href]');
270
- palActive = Math.max(palActive - 1, 0);
271
- updatePalActive(items);
354
+ if (inCmdMode && _cmdHistory.length) {
355
+ _cmdHistIdx = Math.min(_cmdHistIdx + 1, _cmdHistory.length - 1);
356
+ paletteInp.value = '> ' + _cmdHistory[_cmdHistIdx];
357
+ renderPalette(paletteInp.value);
358
+ } else if (!inCmdMode) {
359
+ var items = paletteResults.querySelectorAll('.xfce-pal-item[data-href]');
360
+ palActive = Math.max(palActive - 1, 0);
361
+ updatePalActive(items);
362
+ }
363
+ } else if (e.key === 'ArrowDown') {
364
+ e.preventDefault();
365
+ if (inCmdMode && _cmdHistory.length) {
366
+ _cmdHistIdx = Math.max(_cmdHistIdx - 1, -1);
367
+ paletteInp.value = _cmdHistIdx < 0 ? '> ' : '> ' + _cmdHistory[_cmdHistIdx];
368
+ renderPalette(paletteInp.value);
369
+ } else if (!inCmdMode) {
370
+ var items = paletteResults.querySelectorAll('.xfce-pal-item[data-href]');
371
+ palActive = Math.min(palActive + 1, items.length - 1);
372
+ updatePalActive(items);
373
+ }
272
374
  } else if (e.key === 'Escape') {
273
375
  closePalette();
274
376
  }
@@ -480,11 +582,15 @@
480
582
  var results = d.results || (Array.isArray(d) ? d : []);
481
583
  if (!results.length) { palPrint('no results for “' + escHtml(term) + '”', 'muted'); return; }
482
584
  var html = results.slice(0, 15).map(function (r) {
483
- var href = '/editor.html?collection=' + encodeURIComponent(r.collection || '') + '&id=' + encodeURIComponent(r.id || '');
484
- return '<div class="xfce-pal-item" data-href="' + href + '">'
485
- + '<span class="xfce-pal-icon">⌕</span>'
486
- + '<span class="xfce-pal-label">' + escHtml(r.title || r.slug || r.id || '') + '</span>'
487
- + '<span class="xfce-pal-hint-r">' + escHtml(r.collection || '') + '</span>'
585
+ var href = '/editor.html?collection=' + encodeURIComponent(r.collection || '') + '&slug=' + encodeURIComponent(r.slug || '');
586
+ var snippet = r.snippet ? '<div class=”xfce-pal-snippet”>' + escHtml(r.snippet) + '</div>' : '';
587
+ return '<div class=”xfce-pal-item xfce-pal-item--rich” data-href=”' + href + '”>'
588
+ + '<span class=”xfce-pal-icon”>⌕</span>'
589
+ + '<span class=”xfce-pal-item-body”>'
590
+ + '<span class=”xfce-pal-label”>' + escHtml(r.title || r.slug || '') + '</span>'
591
+ + snippet
592
+ + '</span>'
593
+ + '<span class=”xfce-pal-hint-r”>' + escHtml(r.label || r.collection || '') + '</span>'
488
594
  + '</div>';
489
595
  }).join('');
490
596
  palSetItems(html);
@@ -494,7 +600,7 @@
494
600
 
495
601
  function palBuild() {
496
602
  palPrint('triggering build…', 'muted');
497
- fetch('/api/build', { method: 'POST', credentials: 'include' })
603
+ fetch('/api/build/trigger', { method: 'POST', credentials: 'include' })
498
604
  .then(function (r) { return r.json(); })
499
605
  .then(function (d) {
500
606
  paletteResults.innerHTML = '';
@@ -973,19 +1079,25 @@
973
1079
  if (focusedEl) exitFocusMode();
974
1080
  }, true);
975
1081
 
1082
+ // Apply saved dock position
1083
+ var _savedDockPos = localStorage.getItem('orb_dock_pos') || 'bottom';
1084
+ document.documentElement.dataset.dockPos = _savedDockPos;
1085
+
976
1086
  // ── Magnification ─────────────────────────────────────────────────
977
- function applyMag(cx) {
978
- var items = dockInner.querySelectorAll('.xfce-dock-item');
1087
+ function applyMag(cx, cy) {
1088
+ var isLeft = document.documentElement.dataset.dockPos === 'left';
1089
+ var items = dockInner.querySelectorAll('.xfce-dock-item');
979
1090
  items.forEach(function (item) {
980
1091
  var r = item.getBoundingClientRect();
981
- var mid = r.left + r.width / 2;
982
- var d = Math.abs(cx - mid);
1092
+ var mid = isLeft ? (r.top + r.height / 2) : (r.left + r.width / 2);
1093
+ var pos = isLeft ? cy : cx;
1094
+ var d = Math.abs(pos - mid);
983
1095
  var s = d < 80 ? 1 + (1 - d / 80) * 0.50 : 1;
984
1096
  item.style.setProperty('--ds', s.toFixed(3));
985
1097
  });
986
1098
  }
987
1099
 
988
- dock.addEventListener('mousemove', function (e) { applyMag(e.clientX); });
1100
+ dock.addEventListener('mousemove', function (e) { applyMag(e.clientX, e.clientY); });
989
1101
  dock.addEventListener('mouseleave', function () {
990
1102
  dockInner.querySelectorAll('.xfce-dock-item').forEach(function (item) {
991
1103
  item.style.setProperty('--ds', '1');
@@ -1029,12 +1141,12 @@
1029
1141
  item.appendChild(badge);
1030
1142
  }
1031
1143
 
1032
- // Hover shows + badge above this item
1033
- var createHref = col.singleton
1034
- ? '/editor.html?collection=' + encodeURIComponent(col.id) + '&singleton=1'
1035
- : '/editor.html?collection=' + encodeURIComponent(col.id);
1036
- item.addEventListener('mouseenter', function () { showColCreate(createHref, item); });
1037
- item.addEventListener('mouseleave', function () { colCreateTimer = setTimeout(hideColCreate, 120); });
1144
+ // Hover shows preview card with recent entries
1145
+ item.addEventListener('mouseenter', function () { showColPreview(col, item); });
1146
+ item.addEventListener('mouseleave', function () { _previewTimer = setTimeout(hideColPreview, 150); });
1147
+
1148
+ // Right-click context menu
1149
+ addDockCtxMenu(item, col);
1038
1150
 
1039
1151
  colGroup.appendChild(item);
1040
1152
 
@@ -1094,6 +1206,17 @@
1094
1206
  }
1095
1207
 
1096
1208
  // ── Keyboard shortcuts ────────────────────────────────────────────────
1209
+ var _gPending = false, _gTimer = null;
1210
+ var G_MAP = { d: '/dashboard.html', m: '/media.html', s: '/settings.html',
1211
+ u: '/users.html', b: '/build.html', i: '/import.html',
1212
+ h: '/schema.html', a: '/account.html' };
1213
+
1214
+ function setGMode(on) {
1215
+ _gPending = on;
1216
+ var ind = document.getElementById('xfce-sb-g-ind');
1217
+ if (ind) ind.style.display = on ? '' : 'none';
1218
+ }
1219
+
1097
1220
  function bindKeys() {
1098
1221
  // Capture-phase ⌘K: fires before admin-utils.js bubble-phase listener, stops it
1099
1222
  document.addEventListener('keydown', function (e) {
@@ -1116,11 +1239,26 @@
1116
1239
  // / — open palette (when not typing in an input; Shift+7 on DE keyboard also produces '/')
1117
1240
  if (!mod && !e.altKey && e.key === '/' && !isEditing(e.target)) {
1118
1241
  e.preventDefault();
1242
+ setGMode(false); clearTimeout(_gTimer);
1119
1243
  if (palette && palette.classList.contains('open')) closePalette();
1120
1244
  else openPalette();
1121
1245
  return;
1122
1246
  }
1123
1247
 
1248
+ // g — vim-style navigation prefix (g d = dashboard, g m = media, …)
1249
+ if (!mod && !e.shiftKey && !e.altKey && e.key === 'g' && !isEditing(e.target)) {
1250
+ e.preventDefault();
1251
+ setGMode(true);
1252
+ clearTimeout(_gTimer);
1253
+ _gTimer = setTimeout(function () { setGMode(false); }, 1500);
1254
+ return;
1255
+ }
1256
+ if (_gPending && !isEditing(e.target)) {
1257
+ clearTimeout(_gTimer); setGMode(false);
1258
+ if (G_MAP[e.key]) { e.preventDefault(); location.href = G_MAP[e.key]; }
1259
+ return;
1260
+ }
1261
+
1124
1262
  // ⌘⇧D — toggle HUD
1125
1263
  if (mod && e.shiftKey && (e.key === 'd' || e.key === 'D')) {
1126
1264
  e.preventDefault();
@@ -1154,6 +1292,104 @@
1154
1292
  });
1155
1293
  }
1156
1294
 
1295
+ // ── Build status in status bar ────────────────────────────────────────
1296
+ function loadBuildStatus() {
1297
+ fetch('/api/build/status', { credentials: 'include' })
1298
+ .then(function (r) { return r.ok ? r.json() : null; })
1299
+ .then(function (d) {
1300
+ if (!d || !d.lastTriggered) return;
1301
+ var el2 = document.getElementById('xfce-sb-build');
1302
+ var sep = document.getElementById('xfce-sb-build-sep');
1303
+ if (!el2) return;
1304
+ var dt = new Date(d.lastTriggered.replace(' ', 'T'));
1305
+ var now = new Date();
1306
+ var diffM = Math.floor((now - dt) / 60000);
1307
+ var label = diffM < 1 ? 'built now'
1308
+ : diffM < 60 ? 'built ' + diffM + 'm ago'
1309
+ : diffM < 1440 ? 'built ' + Math.floor(diffM / 60) + 'h ago'
1310
+ : 'built ' + dt.toLocaleDateString([], { month: 'short', day: 'numeric' });
1311
+ el2.textContent = '◉ ' + label;
1312
+ el2.title = 'Last build: ' + d.lastTriggered;
1313
+ if (sep) sep.style.display = '';
1314
+ });
1315
+ }
1316
+
1317
+ // ── Dock right-click context menu ─────────────────────────────────────
1318
+ var _ctxMenu = null;
1319
+
1320
+ function buildCtxMenu() {
1321
+ _ctxMenu = document.createElement('div');
1322
+ _ctxMenu.className = 'xfce-ctx-menu';
1323
+ _ctxMenu.id = 'xfce-ctx-menu';
1324
+ document.body.appendChild(_ctxMenu);
1325
+ document.addEventListener('click', function (e) {
1326
+ if (_ctxMenu && !_ctxMenu.contains(e.target)) closeCtxMenu();
1327
+ }, true);
1328
+ document.addEventListener('keydown', function (e) {
1329
+ if (e.key === 'Escape') closeCtxMenu();
1330
+ });
1331
+ }
1332
+
1333
+ function openCtxMenu(x, y, items) {
1334
+ if (!_ctxMenu) buildCtxMenu();
1335
+ _ctxMenu.innerHTML = items.map(function (it) {
1336
+ return '<button class="xfce-ctx-item" data-href="' + (it.href || '') + '">'
1337
+ + '<span class="xfce-ctx-icon">' + it.icon + '</span>'
1338
+ + '<span>' + it.label + '</span>'
1339
+ + '</button>';
1340
+ }).join('');
1341
+ _ctxMenu.querySelectorAll('.xfce-ctx-item').forEach(function (btn) {
1342
+ btn.addEventListener('click', function () {
1343
+ var href = btn.dataset.href;
1344
+ closeCtxMenu();
1345
+ if (href) location.href = href;
1346
+ });
1347
+ });
1348
+ var vw = window.innerWidth, vh = window.innerHeight;
1349
+ _ctxMenu.style.display = 'block';
1350
+ var w = _ctxMenu.offsetWidth, h = _ctxMenu.offsetHeight;
1351
+ _ctxMenu.style.left = Math.min(x, vw - w - 8) + 'px';
1352
+ _ctxMenu.style.top = Math.min(y, vh - h - 8) + 'px';
1353
+ _ctxMenu.classList.add('open');
1354
+ }
1355
+
1356
+ function closeCtxMenu() {
1357
+ if (!_ctxMenu) return;
1358
+ _ctxMenu.classList.remove('open');
1359
+ setTimeout(function () { if (_ctxMenu) _ctxMenu.style.display = 'none'; }, 120);
1360
+ }
1361
+
1362
+ function addDockCtxMenu(item, col) {
1363
+ item.addEventListener('contextmenu', function (e) {
1364
+ e.preventDefault();
1365
+ var entriesHref = col.singleton
1366
+ ? '/editor.html?collection=' + encodeURIComponent(col.id) + '&singleton=1'
1367
+ : '/entries.html?col=' + encodeURIComponent(col.id) + '&label=' + encodeURIComponent(col.label);
1368
+ var newHref = '/editor.html?collection=' + encodeURIComponent(col.id);
1369
+ openCtxMenu(e.clientX, e.clientY, [
1370
+ { icon: '◫', label: 'View entries', href: entriesHref },
1371
+ { icon: '+', label: 'New entry', href: newHref },
1372
+ { icon: '↓', label: 'Export JSON', href: '' },
1373
+ ]);
1374
+ // Wire export separately (needs fetch, not href)
1375
+ var exportBtn = _ctxMenu.querySelectorAll('.xfce-ctx-item')[2];
1376
+ if (exportBtn) {
1377
+ exportBtn.addEventListener('click', function (ev) {
1378
+ ev.stopImmediatePropagation();
1379
+ closeCtxMenu();
1380
+ fetch('/api/terminal/export?col=' + encodeURIComponent(col.id) + '&format=json&drafts=0', { credentials: 'include' })
1381
+ .then(function (r) { return r.blob(); })
1382
+ .then(function (blob) {
1383
+ var a = document.createElement('a');
1384
+ a.href = URL.createObjectURL(blob);
1385
+ a.download = col.id + '.json';
1386
+ document.body.appendChild(a); a.click(); a.remove();
1387
+ });
1388
+ }, { once: true });
1389
+ }
1390
+ });
1391
+ }
1392
+
1157
1393
  // ── Init ──────────────────────────────────────────────────────────────
1158
1394
  function init() {
1159
1395
  buildStatusBar();
@@ -1161,6 +1397,7 @@
1161
1397
  buildDock();
1162
1398
  buildToastHost();
1163
1399
  loadInfo();
1400
+ loadBuildStatus();
1164
1401
  bindKeys();
1165
1402
  initFocusMode();
1166
1403
  observeSavedFlash();
@@ -29,6 +29,16 @@ searchRoutes.get('/recent', (c) => {
29
29
  return c.json(results);
30
30
  });
31
31
 
32
+ function makeSnippet(body, q, maxLen) {
33
+ if (!body) return '';
34
+ const lower = body.toLowerCase();
35
+ const idx = lower.indexOf(q);
36
+ if (idx < 0) return body.slice(0, maxLen) + (body.length > maxLen ? '…' : '');
37
+ const start = Math.max(0, idx - 30);
38
+ const end = Math.min(body.length, idx + q.length + 60);
39
+ return (start > 0 ? '…' : '') + body.slice(start, end).trim() + (end < body.length ? '…' : '');
40
+ }
41
+
32
42
  // GET /api/search?q=
33
43
  searchRoutes.get('/', (c) => {
34
44
  const q = (c.req.query('q') ?? '').trim().toLowerCase();
@@ -42,8 +52,8 @@ searchRoutes.get('/', (c) => {
42
52
  const entries = db.getEntries(col.id);
43
53
  for (const entry of entries) {
44
54
  const title = (entry.data?.title ?? entry.slug ?? '').toLowerCase();
45
- const body = (entry.data?.body ?? '').toLowerCase();
46
- if (title.includes(q) || body.includes(q) || entry.slug.includes(q)) {
55
+ const body = entry.data?.body ?? '';
56
+ if (title.includes(q) || body.toLowerCase().includes(q) || entry.slug.includes(q)) {
47
57
  results.push({
48
58
  type: 'entry',
49
59
  collection: col.id,
@@ -51,6 +61,7 @@ searchRoutes.get('/', (c) => {
51
61
  slug: entry.slug,
52
62
  title: entry.data?.title ?? entry.slug,
53
63
  status: entry.status,
64
+ snippet: makeSnippet(body, q, 100),
54
65
  });
55
66
  if (results.length >= 20) break;
56
67
  }