@a83/orbiter-admin 0.3.33 → 0.3.35

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.33",
3
+ "version": "0.3.35",
4
4
  "description": "Standalone admin server for Orbiter CMS",
5
5
  "type": "module",
6
6
  "main": "./src/server.js",
package/public/style.css CHANGED
@@ -2009,8 +2009,13 @@ a.xfce-sb-logo:hover { opacity: .8; }
2009
2009
  font-family: var(--mono); transition: color .15s;
2010
2010
  }
2011
2011
  .xfce-sb-logout:hover { color: var(--red); }
2012
- .xfce-sb-palette-btn { background: none; border: none; cursor: pointer; padding: 0 3px; font-size: 14px; color: var(--mid); line-height: 1; font-family: var(--mono); transition: color .15s; }
2013
- .xfce-sb-palette-btn:hover { color: var(--accent); }
2012
+ .xfce-sb-palette-btn {
2013
+ background: none; border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
2014
+ border-radius: 4px; cursor: pointer; padding: 1px 6px;
2015
+ font-size: 13px; color: var(--accent); line-height: 1.4;
2016
+ font-family: var(--mono); transition: background .15s;
2017
+ }
2018
+ .xfce-sb-palette-btn:hover { background: color-mix(in srgb, var(--accent) 14%, transparent); }
2014
2019
 
2015
2020
  /* Draft badge on dock items */
2016
2021
  .xfce-dock-badge {
@@ -2041,54 +2046,73 @@ a.xfce-sb-logo:hover { opacity: .8; }
2041
2046
  opacity: 1; transform: translateX(-50%) translateY(0); pointer-events: auto;
2042
2047
  }
2043
2048
 
2044
- /* ── Command Palette ─────────────────────────────────────── */
2045
- .xfce-palette {
2049
+ /* ── Orbiter Terminal ────────────────────────────────────── */
2050
+ .xfce-terminal {
2046
2051
  display: none; position: fixed; inset: 0; z-index: 10000;
2047
- background: rgba(0,0,0,.55); backdrop-filter: blur(4px);
2048
- align-items: flex-start; justify-content: center;
2049
- padding-top: 12vh;
2052
+ background: rgba(0,0,0,0);
2053
+ align-items: flex-end; justify-content: center;
2054
+ padding-bottom: calc(var(--dock-h, 76px) + 20px);
2055
+ transition: background .18s;
2056
+ }
2057
+ .xfce-terminal.open {
2058
+ display: flex;
2059
+ background: rgba(0,0,0,.28);
2050
2060
  }
2051
- .xfce-palette.open { display: flex; }
2052
2061
 
2053
- .xfce-palette-inner {
2054
- width: min(560px, 92vw);
2055
- background: var(--glass-bg, color-mix(in srgb, var(--bg1) 90%, transparent));
2056
- border: 1px solid var(--line);
2062
+ .xfce-term-inner {
2063
+ width: min(680px, 92vw); max-height: min(480px, 60vh);
2064
+ background: #0a0a14;
2065
+ border: 1px solid rgba(120,90,210,.25);
2066
+ border-top: 2px solid var(--accent, #7c6fcd);
2057
2067
  border-radius: 12px;
2058
- box-shadow: 0 24px 60px rgba(0,0,0,.5), 0 0 0 1px color-mix(in srgb,var(--accent) 20%,transparent);
2068
+ box-shadow: 0 -2px 48px rgba(0,0,0,.5);
2059
2069
  overflow: hidden;
2070
+ display: flex; flex-direction: column;
2071
+ transform: translateY(14px); opacity: 0;
2072
+ transition: transform .2s cubic-bezier(.34,1.15,.64,1), opacity .15s;
2073
+ }
2074
+ .xfce-terminal.open .xfce-term-inner {
2075
+ transform: translateY(0); opacity: 1;
2060
2076
  }
2061
2077
 
2062
- .xfce-palette-bar {
2063
- display: flex; align-items: center; gap: 8px;
2064
- padding: 10px 14px; border-bottom: 1px solid var(--line);
2078
+ .xfce-term-bar {
2079
+ display: flex; align-items: center; justify-content: space-between;
2080
+ padding: 8px 14px; border-bottom: 1px solid rgba(255,255,255,.06); flex-shrink: 0;
2065
2081
  }
2066
- .xfce-palette-cmd { color: var(--accent); font-size: 13px; flex-shrink: 0; }
2067
- .xfce-palette-inp {
2068
- flex: 1; background: none; border: none; outline: none;
2069
- font-family: var(--mono); font-size: 13px; color: var(--heading);
2082
+ .xfce-term-title {
2083
+ font-family: var(--mono); font-size: 9px; letter-spacing: .12em;
2084
+ text-transform: uppercase; color: var(--accent, #7c6fcd);
2085
+ }
2086
+ .xfce-term-esc {
2087
+ font-family: var(--mono); font-size: 8px; color: rgba(150,150,200,.5);
2088
+ background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.1);
2089
+ border-radius: 3px; padding: 1px 5px;
2070
2090
  }
2071
- .xfce-palette-inp::placeholder { color: var(--muted); }
2072
- .xfce-palette-hint { font-size: 8px; color: var(--muted); font-family: var(--mono); white-space: nowrap; }
2073
2091
 
2074
- .xfce-palette-results {
2075
- max-height: 360px; overflow-y: auto; padding: 6px 0;
2092
+ .xfce-term-output {
2093
+ flex: 1; overflow-y: auto; padding: 10px 0;
2094
+ font-family: var(--mono); font-size: 11px; line-height: 1.7;
2076
2095
  }
2077
- .xfce-pal-group {
2078
- font-size: 8px; font-family: var(--mono); color: var(--muted);
2079
- letter-spacing: .08em; text-transform: uppercase;
2080
- padding: 8px 14px 4px;
2096
+ .xfce-term-line { padding: 0 16px; color: rgba(200,200,230,.75); white-space: pre; }
2097
+ .xfce-term-cmd { color: rgba(200,200,230,.9); }
2098
+ .xfce-term-ok { color: #5dc97e; }
2099
+ .xfce-term-err { color: #e06c6c; }
2100
+ .xfce-term-dim { color: rgba(150,150,190,.6); }
2101
+ .xfce-term-muted { color: rgba(120,120,170,.45); }
2102
+
2103
+ .xfce-term-input-row {
2104
+ display: flex; align-items: center; gap: 8px;
2105
+ padding: 10px 16px; border-top: 1px solid rgba(255,255,255,.06); flex-shrink: 0;
2081
2106
  }
2082
- .xfce-pal-item {
2083
- display: flex; align-items: center; gap: 10px;
2084
- padding: 7px 14px; cursor: pointer; transition: background .1s;
2107
+ .xfce-term-prompt {
2108
+ font-family: var(--mono); font-size: 11px;
2109
+ color: var(--accent, #7c6fcd); white-space: nowrap; flex-shrink: 0;
2085
2110
  }
2086
- .xfce-pal-item:hover, .xfce-pal-item.pal-active {
2087
- background: color-mix(in srgb, var(--accent) 12%, transparent);
2111
+ .xfce-term-input {
2112
+ flex: 1; background: none; border: none; outline: none;
2113
+ font-family: var(--mono); font-size: 11px; color: rgba(220,220,255,.9);
2114
+ caret-color: var(--accent, #7c6fcd);
2088
2115
  }
2089
- .xfce-pal-icon { width: 20px; text-align: center; font-size: 13px; color: var(--accent); flex-shrink: 0; }
2090
- .xfce-pal-label { font-size: 12px; color: var(--heading); font-family: var(--mono); }
2091
- .xfce-pal-empty { padding: 20px 14px; color: var(--muted); font-size: 11px; font-family: var(--mono); }
2092
2116
 
2093
2117
  /* ── Toast host (above dock) ─────────────────────────────── */
2094
2118
  .xfce-toast-host {
package/public/xfce.js CHANGED
@@ -28,10 +28,8 @@
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
- });
31
+ // collections loaded by /api/info — used by terminal
32
+ var _termCols = [];
35
33
 
36
34
  // ── Helpers ───────────────────────────────────────────────────────────
37
35
  function el(tag, cls, html) {
@@ -96,7 +94,7 @@
96
94
  // Palette trigger in status bar
97
95
  document.getElementById('xfce-sb-palette-btn').addEventListener('click', function (e) {
98
96
  e.stopPropagation();
99
- openPalette();
97
+ openTerminal();
100
98
  });
101
99
  }
102
100
 
@@ -162,7 +160,7 @@
162
160
  palBtn.addEventListener('click', function (e) {
163
161
  e.stopPropagation();
164
162
  toolsPopup.classList.remove('open');
165
- openPalette();
163
+ openTerminal();
166
164
  });
167
165
  toolsPopup.appendChild(palBtn);
168
166
  document.body.appendChild(toolsPopup);
@@ -212,120 +210,278 @@
212
210
  if (colCreateEl) colCreateEl.classList.remove('visible');
213
211
  }
214
212
 
215
- // ── Command Palette ───────────────────────────────────────────────────
216
- var palette, paletteInp, paletteResults, palActive = -1;
213
+ // ── Orbiter Terminal ─────────────────────────────────────────────────
214
+ var terminal, termOutput, termInput;
215
+ var termHistory = [], termHistIdx = -1;
216
+ var TERM_CMDS = ['help','clear','info','ls','go','new','search','build','export'];
217
+ var NAV_DEST = {
218
+ dashboard: '/dashboard.html', media: '/media.html', settings: '/settings.html',
219
+ users: '/users.html', schema: '/schema.html', build: '/build.html',
220
+ import: '/import.html', account: '/account.html',
221
+ };
217
222
 
218
- function buildPalette() {
219
- palette = el('div', 'xfce-palette');
220
- palette.id = 'xfce-palette';
221
- palette.innerHTML = [
222
- '<div class="xfce-palette-inner">',
223
- '<div class="xfce-palette-bar">',
224
- '<span class="xfce-palette-cmd">⌘</span>',
225
- '<input id="xfce-palette-inp" class="xfce-palette-inp" placeholder="Go to page or collection…" autocomplete="off" spellcheck="false" />',
226
- '<span class="xfce-palette-hint">ESC to close</span>',
223
+ function buildTerminal() {
224
+ terminal = el('div', 'xfce-terminal');
225
+ terminal.id = 'xfce-terminal';
226
+ terminal.innerHTML = [
227
+ '<div class="xfce-term-inner">',
228
+ '<div class="xfce-term-bar">',
229
+ '<span class="xfce-term-title">◈ orbiter terminal</span>',
230
+ '<kbd class="xfce-term-esc">ESC</kbd>',
231
+ '</div>',
232
+ '<div id="xfce-term-output" class="xfce-term-output"></div>',
233
+ '<div class="xfce-term-input-row">',
234
+ '<span class="xfce-term-prompt">orbiter $</span>',
235
+ '<input id="xfce-term-input" class="xfce-term-input" autocomplete="off" spellcheck="false" />',
227
236
  '</div>',
228
- '<div id="xfce-palette-results" class="xfce-palette-results"></div>',
229
237
  '</div>',
230
238
  ].join('');
231
- document.body.appendChild(palette);
232
-
233
- paletteInp = document.getElementById('xfce-palette-inp');
234
- paletteResults = document.getElementById('xfce-palette-results');
235
-
236
- paletteInp.addEventListener('input', function () {
237
- palActive = -1;
238
- renderPalette(paletteInp.value);
239
- });
240
-
241
- paletteInp.addEventListener('keydown', function (e) {
242
- var items = paletteResults.querySelectorAll('.xfce-pal-item');
243
- if (e.key === 'ArrowDown') {
244
- e.preventDefault();
245
- palActive = Math.min(palActive + 1, items.length - 1);
246
- updatePalActive(items);
239
+ document.body.appendChild(terminal);
240
+
241
+ termOutput = document.getElementById('xfce-term-output');
242
+ termInput = document.getElementById('xfce-term-input');
243
+
244
+ termPrint('Orbiter Admin · type \'help\' for available commands', 'muted');
245
+ termPrint('', '');
246
+
247
+ termInput.addEventListener('keydown', function (e) {
248
+ if (e.key === 'Enter') {
249
+ var line = termInput.value.trim();
250
+ termInput.value = '';
251
+ if (!line) return;
252
+ termHistIdx = -1;
253
+ if (!termHistory.length || termHistory[0] !== line) termHistory.unshift(line);
254
+ if (termHistory.length > 50) termHistory.pop();
255
+ termPrint('orbiter $ ' + line, 'cmd');
256
+ execCmd(line);
247
257
  } else if (e.key === 'ArrowUp') {
248
258
  e.preventDefault();
249
- palActive = Math.max(palActive - 1, 0);
250
- updatePalActive(items);
251
- } else if (e.key === 'Enter') {
252
- var active = paletteResults.querySelector('.xfce-pal-item.pal-active');
253
- if (active) { location.href = active.dataset.href; closePalette(); }
254
- else {
255
- var first = paletteResults.querySelector('.xfce-pal-item');
256
- if (first) { location.href = first.dataset.href; closePalette(); }
257
- }
259
+ if (termHistIdx < termHistory.length - 1) { termHistIdx++; termInput.value = termHistory[termHistIdx]; }
260
+ } else if (e.key === 'ArrowDown') {
261
+ e.preventDefault();
262
+ if (termHistIdx > 0) { termHistIdx--; termInput.value = termHistory[termHistIdx]; }
263
+ else { termHistIdx = -1; termInput.value = ''; }
264
+ } else if (e.key === 'Tab') {
265
+ e.preventDefault();
266
+ termTabComplete();
258
267
  } else if (e.key === 'Escape') {
259
- closePalette();
268
+ closeTerminal();
260
269
  }
261
270
  });
262
271
 
263
- palette.addEventListener('click', function (e) {
264
- if (e.target === palette) closePalette();
272
+ terminal.addEventListener('click', function (e) {
273
+ if (e.target === terminal) closeTerminal();
265
274
  });
266
275
  }
267
276
 
268
- function updatePalActive(items) {
269
- items.forEach(function (it, i) {
270
- it.classList.toggle('pal-active', i === palActive);
271
- if (i === palActive) it.scrollIntoView({ block: 'nearest' });
272
- });
277
+ function termPrint(text, cls) {
278
+ var line = document.createElement('div');
279
+ line.className = 'xfce-term-line' + (cls ? ' xfce-term-' + cls : '');
280
+ line.textContent = text;
281
+ termOutput.appendChild(line);
282
+ termOutput.scrollTop = termOutput.scrollHeight;
273
283
  }
274
284
 
275
- function renderPalette(q) {
276
- q = (q || '').toLowerCase().trim();
277
- var filtered = q
278
- ? _palItems.filter(function (it) { return it.label.toLowerCase().includes(q); })
279
- : _palItems;
280
-
281
- if (!filtered.length) {
282
- paletteResults.innerHTML = '<div class="xfce-pal-empty">No results</div>';
283
- return;
285
+ function termClear() { termOutput.innerHTML = ''; }
286
+
287
+ function execCmd(raw) {
288
+ var parts = raw.trim().split(/\s+/);
289
+ var cmd = parts[0].toLowerCase();
290
+ var args = parts.slice(1);
291
+ switch (cmd) {
292
+ case 'help': termHelp(); break;
293
+ case 'clear': termClear(); break;
294
+ case 'info': termInfo(); break;
295
+ case 'ls': termLs(args); break;
296
+ case 'go': termGo(args); break;
297
+ case 'new': termNew(args); break;
298
+ case 'search': termSearch(args.join(' ')); break;
299
+ case 'build': termBuild(); break;
300
+ case 'export': termExport(args); break;
301
+ default: termPrint('command not found: ' + cmd + ' (try \'help\')', 'err');
284
302
  }
303
+ termPrint('', '');
304
+ }
285
305
 
286
- // Group items
287
- var groups = {};
288
- filtered.forEach(function (it) {
289
- if (!groups[it.group]) groups[it.group] = [];
290
- groups[it.group].push(it);
291
- });
306
+ function termHelp() {
307
+ termPrint('', '');
308
+ [
309
+ ' go <page|collection> navigate to a page or collection',
310
+ ' new <collection> open editor for a new entry',
311
+ ' ls [collection] list collections or recent entries',
312
+ ' search <term> full-text search across all content',
313
+ ' info show pod and version info',
314
+ ' build trigger deploy webhook',
315
+ ' export <col> [flags] download entries flags: --md --drafts',
316
+ ' clear clear terminal',
317
+ ' help show this message',
318
+ '',
319
+ ' shortcuts: ↑↓ history · Tab completion · ⌘K toggle',
320
+ ].forEach(function (l) { termPrint(l, 'dim'); });
321
+ }
292
322
 
293
- var html = '';
294
- Object.keys(groups).forEach(function (g) {
295
- html += '<div class="xfce-pal-group">' + g + '</div>';
296
- groups[g].forEach(function (it) {
297
- html += '<div class="xfce-pal-item" data-href="' + it.href + '">'
298
- + '<span class="xfce-pal-icon">' + it.icon + '</span>'
299
- + '<span class="xfce-pal-label">' + it.label + '</span>'
300
- + '</div>';
323
+ function termInfo() {
324
+ fetch('/api/info', { credentials: 'include' })
325
+ .then(function (r) { return r.ok ? r.json() : null; })
326
+ .then(function (d) {
327
+ if (!d) { termPrint('error fetching info', 'err'); return; }
328
+ var total = (d.collections || []).reduce(function (s, c) { return s + (c.total || 0); }, 0);
329
+ termPrint('', '');
330
+ termPrint(' pod ' + d.podPath.split('/').pop(), 'dim');
331
+ termPrint(' format v' + d.formatVersion, 'dim');
332
+ termPrint(' admin v' + d.adminVersion, 'dim');
333
+ termPrint(' collections ' + (d.collections || []).length, 'dim');
334
+ termPrint(' published ' + total + ' entries', 'dim');
301
335
  });
302
- });
303
- paletteResults.innerHTML = html;
336
+ }
304
337
 
305
- paletteResults.querySelectorAll('.xfce-pal-item').forEach(function (item) {
306
- item.addEventListener('click', function () {
307
- location.href = item.dataset.href;
308
- closePalette();
338
+ function termLs(args) {
339
+ if (!args.length) {
340
+ if (!_termCols.length) { termPrint('no collections loaded yet', 'muted'); return; }
341
+ termPrint('', '');
342
+ _termCols.forEach(function (col) {
343
+ var info = col.total + ' entr' + (col.total === 1 ? 'y' : 'ies');
344
+ if (col.drafts > 0) info += ' ' + col.drafts + ' draft' + (col.drafts === 1 ? '' : 's');
345
+ termPrint(' ' + col.id.padEnd(22) + info, 'dim');
309
346
  });
310
- item.addEventListener('mouseenter', function () {
311
- var items = paletteResults.querySelectorAll('.xfce-pal-item');
312
- palActive = Array.from(items).indexOf(item);
313
- updatePalActive(items);
347
+ return;
348
+ }
349
+ var colId = args[0];
350
+ fetch('/api/collections/' + encodeURIComponent(colId) + '/entries?status=published&limit=25', { credentials: 'include' })
351
+ .then(function (r) { return r.ok ? r.json() : null; })
352
+ .then(function (d) {
353
+ if (!d) { termPrint('collection not found: ' + colId, 'err'); return; }
354
+ var entries = d.entries || (Array.isArray(d) ? d : []);
355
+ termPrint('', '');
356
+ if (!entries.length) { termPrint(' (no published entries)', 'muted'); return; }
357
+ entries.forEach(function (e) {
358
+ var date = (e.updated_at || e.created_at || '').substring(0, 10);
359
+ termPrint(' ' + (e.slug || e.id || '').padEnd(36) + date, 'dim');
360
+ });
361
+ if (d.total > entries.length) termPrint(' … ' + (d.total - entries.length) + ' more', 'muted');
314
362
  });
315
- });
316
363
  }
317
364
 
318
- function openPalette() {
319
- if (!palette) buildPalette();
320
- palette.classList.add('open');
321
- paletteInp.value = '';
322
- palActive = -1;
323
- renderPalette('');
324
- setTimeout(function () { paletteInp.focus(); }, 30);
365
+ function termGo(args) {
366
+ if (!args.length) { termPrint('usage: go <page|collection>', 'err'); return; }
367
+ var dest = args[0].toLowerCase();
368
+ if (NAV_DEST[dest]) {
369
+ termPrint('→ ' + dest, 'ok');
370
+ closeTerminal();
371
+ setTimeout(function () { location.href = NAV_DEST[dest]; }, 180);
372
+ return;
373
+ }
374
+ var col = _termCols.find(function (c) { return c.id === dest || c.label.toLowerCase() === dest; });
375
+ if (col) {
376
+ termPrint('→ ' + col.label, 'ok');
377
+ closeTerminal();
378
+ var href = col.singleton
379
+ ? '/editor.html?collection=' + encodeURIComponent(col.id) + '&singleton=1'
380
+ : '/entries.html?col=' + encodeURIComponent(col.id) + '&label=' + encodeURIComponent(col.label);
381
+ setTimeout(function () { location.href = href; }, 180);
382
+ return;
383
+ }
384
+ termPrint('not found: ' + dest, 'err');
385
+ }
386
+
387
+ function termNew(args) {
388
+ if (!args.length) { termPrint('usage: new <collection>', 'err'); return; }
389
+ var dest = args[0].toLowerCase();
390
+ var col = _termCols.find(function (c) { return c.id === dest || c.label.toLowerCase() === dest; });
391
+ if (!col) { termPrint('collection not found: ' + dest, 'err'); return; }
392
+ termPrint('→ new entry in ' + col.label, 'ok');
393
+ closeTerminal();
394
+ setTimeout(function () { location.href = '/editor.html?collection=' + encodeURIComponent(col.id); }, 180);
395
+ }
396
+
397
+ function termSearch(term) {
398
+ if (!term) { termPrint('usage: search <term>', 'err'); return; }
399
+ termPrint('searching…', 'muted');
400
+ fetch('/api/search?q=' + encodeURIComponent(term), { credentials: 'include' })
401
+ .then(function (r) { return r.ok ? r.json() : null; })
402
+ .then(function (d) {
403
+ if (!d) { termPrint('search error', 'err'); return; }
404
+ var results = d.results || (Array.isArray(d) ? d : []);
405
+ termPrint('', '');
406
+ if (!results.length) { termPrint(' no results for "' + term + '"', 'muted'); return; }
407
+ results.slice(0, 15).forEach(function (r) {
408
+ termPrint(' › ' + (r.collection || '') + '/' + (r.slug || r.id || ''), 'ok');
409
+ if (r.title) termPrint(' ' + r.title, 'dim');
410
+ });
411
+ if (results.length > 15) termPrint(' … ' + (results.length - 15) + ' more', 'muted');
412
+ });
413
+ }
414
+
415
+ function termBuild() {
416
+ termPrint('triggering build…', 'muted');
417
+ fetch('/api/build', { method: 'POST', credentials: 'include' })
418
+ .then(function (r) { return r.json(); })
419
+ .then(function (d) {
420
+ if (d.ok || d.message) termPrint('✓ ' + (d.message || 'build triggered'), 'ok');
421
+ else termPrint('build: ' + (d.error || JSON.stringify(d)), 'err');
422
+ })
423
+ .catch(function () { termPrint('build request failed', 'err'); });
424
+ }
425
+
426
+ function termExport(args) {
427
+ if (!args.length) { termPrint('usage: export <collection> [--md] [--drafts]', 'err'); return; }
428
+ var colId = args[0];
429
+ var format = args.indexOf('--md') !== -1 ? 'md' : 'json';
430
+ var drafts = args.indexOf('--drafts') !== -1 ? '1' : '0';
431
+ var col = _termCols.find(function (c) { return c.id === colId; });
432
+ if (!col) { termPrint('collection not found: ' + colId, 'err'); return; }
433
+ termPrint('exporting ' + col.label + ' as ' + format + '…', 'muted');
434
+ fetch('/api/terminal/export?col=' + encodeURIComponent(colId) + '&format=' + format + '&drafts=' + drafts, { credentials: 'include' })
435
+ .then(function (r) {
436
+ if (!r.ok) return r.json().then(function (e) { throw new Error(e.error || r.status); });
437
+ return r.blob().then(function (blob) {
438
+ var a = document.createElement('a');
439
+ a.href = URL.createObjectURL(blob);
440
+ a.download = colId + '.' + format;
441
+ document.body.appendChild(a); a.click(); a.remove();
442
+ termPrint('✓ downloaded ' + colId + '.' + format, 'ok');
443
+ });
444
+ })
445
+ .catch(function (err) { termPrint('export failed: ' + err.message, 'err'); });
446
+ }
447
+
448
+ function termTabComplete() {
449
+ var val = termInput.value;
450
+ var parts = val.split(/\s+/);
451
+ if (parts.length === 1) {
452
+ var p = parts[0].toLowerCase();
453
+ var m = TERM_CMDS.filter(function (c) { return c.startsWith(p); });
454
+ if (m.length === 1) termInput.value = m[0] + ' ';
455
+ else if (m.length > 1) termPrint(m.join(' '), 'muted');
456
+ } else if (parts.length === 2) {
457
+ var cmd2 = parts[0].toLowerCase(), p2 = parts[1].toLowerCase();
458
+ var pool = _termCols.map(function (c) { return c.id; });
459
+ if (cmd2 === 'go') pool = pool.concat(Object.keys(NAV_DEST));
460
+ if (['go','new','ls','export'].indexOf(cmd2) !== -1) {
461
+ var m2 = pool.filter(function (c) { return c.startsWith(p2); });
462
+ if (m2.length === 1) termInput.value = parts[0] + ' ' + m2[0];
463
+ else if (m2.length > 1) termPrint(m2.join(' '), 'muted');
464
+ }
465
+ }
325
466
  }
326
467
 
327
- function closePalette() {
328
- if (palette) palette.classList.remove('open');
468
+ function openTerminal() {
469
+ if (!terminal) buildTerminal();
470
+ terminal.style.display = 'flex';
471
+ requestAnimationFrame(function () {
472
+ requestAnimationFrame(function () {
473
+ terminal.classList.add('open');
474
+ setTimeout(function () { termInput.focus(); }, 40);
475
+ });
476
+ });
477
+ }
478
+
479
+ function closeTerminal() {
480
+ if (!terminal) return;
481
+ terminal.classList.remove('open');
482
+ setTimeout(function () {
483
+ if (!terminal.classList.contains('open')) terminal.style.display = 'none';
484
+ }, 200);
329
485
  }
330
486
 
331
487
  // ── Toast system ──────────────────────────────────────────────────────
@@ -812,8 +968,8 @@
812
968
 
813
969
  colGroup.appendChild(item);
814
970
 
815
- // Add to palette
816
- _palItems.push({ icon: abbr, label: col.label, href: href, group: 'Collections' });
971
+ // Add to terminal collection list
972
+ _termCols.push({ id: col.id, label: col.label, total: col.total || 0, drafts: col.drafts || 0, singleton: !!col.singleton });
817
973
  });
818
974
  }
819
975
 
@@ -871,11 +1027,11 @@
871
1027
  document.addEventListener('keydown', function (e) {
872
1028
  var mod = e.metaKey || e.ctrlKey;
873
1029
 
874
- // ⌘K — command palette
1030
+ // ⌘K — terminal
875
1031
  if (mod && !e.shiftKey && (e.key === 'k' || e.key === 'K')) {
876
1032
  e.preventDefault();
877
- if (palette && palette.classList.contains('open')) closePalette();
878
- else openPalette();
1033
+ if (terminal && terminal.classList.contains('open')) closeTerminal();
1034
+ else openTerminal();
879
1035
  return;
880
1036
  }
881
1037
 
@@ -0,0 +1,42 @@
1
+ import { Hono } from 'hono';
2
+ import { openPod } from '@a83/orbiter-core';
3
+
4
+ export const terminalRoutes = new Hono();
5
+
6
+ // GET /api/terminal/export?col=<id>&format=json|md&drafts=0|1
7
+ terminalRoutes.get('/export', (c) => {
8
+ const podPath = c.get('podPath');
9
+ const colId = c.req.query('col');
10
+ const format = c.req.query('format') || 'json';
11
+ const inclDrafts = c.req.query('drafts') === '1';
12
+
13
+ if (!colId) return c.json({ error: 'col is required' }, 400);
14
+
15
+ const db = openPod(podPath);
16
+ const col = db.getCollections().find(col => col.id === colId);
17
+ if (!col) { db.close(); return c.json({ error: 'Collection not found' }, 404); }
18
+
19
+ const entries = inclDrafts
20
+ ? db.getEntries(colId)
21
+ : db.getEntries(colId, { status: 'published' });
22
+ db.close();
23
+
24
+ if (format === 'md') {
25
+ const md = entries.map(e =>
26
+ `---\nslug: ${JSON.stringify(e.slug)}\nstatus: ${JSON.stringify(e.status)}\n---\n\n${e.content || ''}`
27
+ ).join('\n\n---\n\n');
28
+ return new Response(md, {
29
+ headers: {
30
+ 'Content-Type': 'text/markdown; charset=utf-8',
31
+ 'Content-Disposition': `attachment; filename="${colId}.md"`,
32
+ },
33
+ });
34
+ }
35
+
36
+ return new Response(JSON.stringify({ collection: colId, entries }, null, 2), {
37
+ headers: {
38
+ 'Content-Type': 'application/json',
39
+ 'Content-Disposition': `attachment; filename="${colId}.json"`,
40
+ },
41
+ });
42
+ });
package/src/server.js CHANGED
@@ -24,6 +24,7 @@ import { infoRoutes } from './routes/info.js';
24
24
  import { importRoutes } from './routes/import.js';
25
25
  import { commentRoutes } from './routes/comments.js';
26
26
  import { lockRoutes } from './routes/locks.js';
27
+ import { terminalRoutes } from './routes/terminal.js';
27
28
  import { requireAuth } from './middleware/auth.js';
28
29
  import { csrfMiddleware } from './middleware/csrf.js';
29
30
 
@@ -78,6 +79,7 @@ export function createApp(podPath) {
78
79
  api.route('/collections', commentRoutes);
79
80
  api.route('/', commentRoutes);
80
81
  api.route('/locks', lockRoutes);
82
+ api.route('/terminal', terminalRoutes);
81
83
 
82
84
  app.route('/api', api);
83
85