@a83/orbiter-admin 0.3.47 → 0.3.48
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/README.md +88 -2
- package/package.json +1 -1
- package/public/account.html +287 -0
package/README.md
CHANGED
|
@@ -165,9 +165,95 @@ For **External links**, use the External link tab in the image picker. Orbiter m
|
|
|
165
165
|
|
|
166
166
|
---
|
|
167
167
|
|
|
168
|
-
##
|
|
168
|
+
## Space Station mode
|
|
169
|
+
|
|
170
|
+
A distinct admin layout — dark glassmorphism, floating magnification dock, and a full keyboard-driven interface. Enable in **Settings → Interface → Layout → Space Station**.
|
|
171
|
+
|
|
172
|
+
### Command palette `⌘K` or `/`
|
|
173
|
+
|
|
174
|
+
Opens full-screen over any page. Shows the 7 most recent entries on open (no typing required). Type to fuzzy-search pages and collections.
|
|
175
|
+
|
|
176
|
+
**Command mode** — prefix with `>`:
|
|
177
|
+
|
|
178
|
+
| Command | Action |
|
|
179
|
+
|---------|--------|
|
|
180
|
+
| `> ls` | List all collections |
|
|
181
|
+
| `> go <page\|collection>` | Navigate |
|
|
182
|
+
| `> new <collection>` | Create new entry |
|
|
183
|
+
| `> search <term>` | Full-text search |
|
|
184
|
+
| `> build` | Trigger site build |
|
|
185
|
+
| `> export <col> [--md] [--drafts]` | Download collection |
|
|
186
|
+
| `> random` | Jump to a random published entry |
|
|
187
|
+
| `> = <expr>` | Evaluate a math expression (`2^10 * 3`) |
|
|
188
|
+
| `> info` | Pod & version info |
|
|
189
|
+
| `> help` | Show all commands |
|
|
190
|
+
|
|
191
|
+
Command history with ↑/↓. Output rendered inline in the palette.
|
|
192
|
+
|
|
193
|
+
### Keyboard shortcuts
|
|
194
|
+
|
|
195
|
+
| Key | Action |
|
|
196
|
+
|-----|--------|
|
|
197
|
+
| `⌘K` / `/` | Open command palette |
|
|
198
|
+
| `?` | Shortcut cheatsheet |
|
|
199
|
+
| `⌘⇧F` | Zen / focus mode (hides dock + status bar) |
|
|
200
|
+
| `⌘⇧L` | Switch back to Glass mode |
|
|
201
|
+
| `1`–`9` | Jump to nth dock item |
|
|
202
|
+
| `g` + `d` | Dashboard |
|
|
203
|
+
| `g` + `m` | Media |
|
|
204
|
+
| `g` + `u` | Users |
|
|
205
|
+
| `g` + `s` | Settings |
|
|
206
|
+
| `g` + `b` | Build |
|
|
207
|
+
| `g` + `i` | Import |
|
|
208
|
+
| `g` + `c` | Schema |
|
|
209
|
+
| `g` + `h` | Toggle HUD panel |
|
|
210
|
+
| `g` + `a` | Account |
|
|
211
|
+
|
|
212
|
+
A `g ›` badge pulses in the status bar while waiting for the second key.
|
|
213
|
+
|
|
214
|
+
### Status bar
|
|
215
|
+
|
|
216
|
+
- **Left** — Orbiter logo, site name
|
|
217
|
+
- **Center** — page title; breadcrumb (`Collection › Entries`) when inside a collection
|
|
218
|
+
- **Right** — vim `g ›` indicator · bell (notification center) · `?` cheatsheet · `⌘` palette · last build time · username · logout · clock
|
|
219
|
+
|
|
220
|
+
Build indicator shows `◉ building…` with a pulse animation while a build runs, polling `/api/build/status` every 4 seconds.
|
|
221
|
+
|
|
222
|
+
### Notification center
|
|
169
223
|
|
|
170
|
-
|
|
224
|
+
Every save, build trigger, and export is logged automatically. Click the bell `○` in the status bar to open the dropdown. Unread count badge clears on open. "Clear all" resets the log.
|
|
225
|
+
|
|
226
|
+
### HUD panel `g h`
|
|
227
|
+
|
|
228
|
+
Side panel with:
|
|
229
|
+
- **Pod** — file name, versions, collection count
|
|
230
|
+
- **Collections** — published/draft counts per collection
|
|
231
|
+
- **Drafts** — last 10 draft entries, clickable to editor
|
|
232
|
+
- **Activity** — last 8 events (saves, builds, exports) as a live timeline
|
|
233
|
+
- **Navigation** — all pages
|
|
234
|
+
|
|
235
|
+
### Dock
|
|
236
|
+
|
|
237
|
+
Floating bottom bar (or left sidebar — toggle in the Tools popup). Magnification effect on hover. Items:
|
|
238
|
+
|
|
239
|
+
- **Nav group** — Dashboard, Media, Users
|
|
240
|
+
- **Collections group** — one item per collection; draft count badge; hover shows preview card with 3 recent entries and quick-action buttons
|
|
241
|
+
- **Workspace** — Notes scratchpad, To-do list
|
|
242
|
+
- **Tools** — Schema, Build, Import (popup)
|
|
243
|
+
- **Settings** — direct link
|
|
244
|
+
- **HUD** — toggle button
|
|
245
|
+
|
|
246
|
+
### Hover preview cards
|
|
247
|
+
|
|
248
|
+
Hovering a collection dock item after 280 ms shows a card with the 3 most recent entries (title + date) and an action row: `+ new entry · ◫ view all · ↓ export`.
|
|
249
|
+
|
|
250
|
+
### Zen mode `⌘⇧F`
|
|
251
|
+
|
|
252
|
+
Hides the dock and status bar entirely. Smooth CSS transition. Persists across reloads (stored in `localStorage`). A toast hint shows "Focus mode on — ⌘⇧F to exit" on enter.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Themes
|
|
171
257
|
|
|
172
258
|
| Theme | Dark | Light |
|
|
173
259
|
|-------|------|-------|
|
package/package.json
CHANGED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Orbiter Admin — Account</title>
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&family=Space+Grotesk:wght@300;400;500;600&family=Noto+Serif+JP:wght@200;300&family=DM+Mono:wght@300;400&display=swap" rel="stylesheet">
|
|
10
|
+
<link rel="stylesheet" href="/style.css" />
|
|
11
|
+
<script src="/theme.js"></script>
|
|
12
|
+
<style>
|
|
13
|
+
.account-wrap { max-width: 640px; padding-bottom: 40px; }
|
|
14
|
+
|
|
15
|
+
.settings-group {
|
|
16
|
+
background: var(--bg2);
|
|
17
|
+
border: 1px solid var(--line);
|
|
18
|
+
border-radius: 10px;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
margin-bottom: 12px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.group-header {
|
|
24
|
+
padding: 10px 24px;
|
|
25
|
+
font-size: 10px;
|
|
26
|
+
letter-spacing: 0.18em;
|
|
27
|
+
text-transform: uppercase;
|
|
28
|
+
color: var(--muted);
|
|
29
|
+
border-bottom: 1px solid var(--line);
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: 7px;
|
|
33
|
+
}
|
|
34
|
+
.group-header::before { content: "◆"; color: var(--gold); font-size: 5px; line-height: 1; }
|
|
35
|
+
|
|
36
|
+
.setting-row {
|
|
37
|
+
display: grid;
|
|
38
|
+
grid-template-columns: 1fr 252px;
|
|
39
|
+
align-items: center;
|
|
40
|
+
gap: 24px;
|
|
41
|
+
padding: 18px 24px;
|
|
42
|
+
border-bottom: 1px solid var(--line2);
|
|
43
|
+
}
|
|
44
|
+
.setting-row:last-child { border-bottom: none; }
|
|
45
|
+
.setting-label { font-size: 13px; color: var(--text); margin-bottom: 3px; }
|
|
46
|
+
.setting-desc { font-size: 11px; color: var(--muted); line-height: 1.55; }
|
|
47
|
+
|
|
48
|
+
.account-wrap .input {
|
|
49
|
+
width: 100%;
|
|
50
|
+
background: var(--bg0);
|
|
51
|
+
border: 1px solid var(--line);
|
|
52
|
+
border-radius: 6px;
|
|
53
|
+
padding: 7px 11px;
|
|
54
|
+
color: var(--heading);
|
|
55
|
+
font-family: var(--mono);
|
|
56
|
+
font-size: 12px;
|
|
57
|
+
outline: none;
|
|
58
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
59
|
+
}
|
|
60
|
+
.account-wrap .input:focus {
|
|
61
|
+
border-color: var(--accent);
|
|
62
|
+
box-shadow: 0 0 0 3px var(--accent-bg);
|
|
63
|
+
}
|
|
64
|
+
.account-wrap .input[readonly] {
|
|
65
|
+
color: var(--muted);
|
|
66
|
+
cursor: default;
|
|
67
|
+
background: var(--bg1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.save-row {
|
|
71
|
+
display: flex;
|
|
72
|
+
justify-content: flex-end;
|
|
73
|
+
align-items: center;
|
|
74
|
+
padding: 14px 24px;
|
|
75
|
+
border-top: 1px solid var(--line);
|
|
76
|
+
gap: 10px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.banner {
|
|
80
|
+
padding: 10px 20px;
|
|
81
|
+
font-size: 11px;
|
|
82
|
+
display: none;
|
|
83
|
+
align-items: center;
|
|
84
|
+
gap: 8px;
|
|
85
|
+
border-radius: 8px;
|
|
86
|
+
margin-bottom: 12px;
|
|
87
|
+
}
|
|
88
|
+
.banner-ok { background: var(--jade-bg); color: var(--jade); border: 1px solid rgba(45,139,106,.2); }
|
|
89
|
+
.banner-err { background: var(--red-bg); color: var(--red); border: 1px solid rgba(240,112,128,.18); }
|
|
90
|
+
|
|
91
|
+
.role-badge {
|
|
92
|
+
display: inline-block;
|
|
93
|
+
font-family: var(--mono);
|
|
94
|
+
font-size: 9px;
|
|
95
|
+
letter-spacing: 0.1em;
|
|
96
|
+
text-transform: uppercase;
|
|
97
|
+
padding: 2px 8px;
|
|
98
|
+
border-radius: 4px;
|
|
99
|
+
background: var(--accent-bg);
|
|
100
|
+
color: var(--accent);
|
|
101
|
+
border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@media (max-width: 600px) {
|
|
105
|
+
.setting-row { grid-template-columns: 1fr; gap: 10px; }
|
|
106
|
+
}
|
|
107
|
+
</style>
|
|
108
|
+
</head>
|
|
109
|
+
<body>
|
|
110
|
+
<div class="app">
|
|
111
|
+
<header class="topbar">
|
|
112
|
+
<a class="logo" href="/dashboard.html"><div class="logo-mark">OR</div>Orbiter</a>
|
|
113
|
+
<div class="topbar-right">
|
|
114
|
+
<button class="search-trigger" id="search-btn" title="Search (⌘K)">
|
|
115
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
|
116
|
+
Search <kbd>⌘K</kbd>
|
|
117
|
+
</button>
|
|
118
|
+
<button class="scheme-toggle" id="scheme-toggle" title="Toggle scheme">◐</button>
|
|
119
|
+
<span class="user" id="topbar-user"></span>
|
|
120
|
+
<span class="logout" id="logout-btn">Sign out</span>
|
|
121
|
+
</div>
|
|
122
|
+
</header>
|
|
123
|
+
<nav class="sidebar">
|
|
124
|
+
<div class="nav-section">Content</div>
|
|
125
|
+
<a class="nav-item" href="/dashboard.html"><span class="nav-icon">◈</span>Dashboard</a>
|
|
126
|
+
<a class="nav-item" href="/collections.html"><span class="nav-icon">⊞</span>Collections</a>
|
|
127
|
+
<div class="nav-section">Assets</div>
|
|
128
|
+
<a class="nav-item" href="/media.html"><span class="nav-icon">⊟</span>Media</a>
|
|
129
|
+
<div class="nav-section">System</div>
|
|
130
|
+
<a class="nav-item" href="/schema.html"><span class="nav-icon">◈</span>Schema</a>
|
|
131
|
+
<a class="nav-item" href="/build.html"><span class="nav-icon">▲</span>Build</a>
|
|
132
|
+
<a class="nav-item" href="/settings.html"><span class="nav-icon">◎</span>Settings</a>
|
|
133
|
+
<a class="nav-item admin-only" href="/users.html" style="display:none"><span class="nav-icon">◉</span>Users</a>
|
|
134
|
+
<div class="sidebar-footer">
|
|
135
|
+
<div class="pod-name" id="pod-name">content.pod</div>
|
|
136
|
+
<div class="pod-info" id="pod-info"></div>
|
|
137
|
+
<div class="pod-version" id="pod-version"></div>
|
|
138
|
+
<div class="pod-status"><span class="pod-dot"></span>pod synced</div>
|
|
139
|
+
</div>
|
|
140
|
+
</nav>
|
|
141
|
+
<main class="main">
|
|
142
|
+
<div class="page-header glass-card">
|
|
143
|
+
<h1 class="page-title">Account</h1>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div class="account-wrap">
|
|
147
|
+
|
|
148
|
+
<!-- Profile -->
|
|
149
|
+
<div id="banner-profile" class="banner"></div>
|
|
150
|
+
<div class="settings-group">
|
|
151
|
+
<div class="group-header">Profile</div>
|
|
152
|
+
<div class="setting-row">
|
|
153
|
+
<div>
|
|
154
|
+
<div class="setting-label">Role</div>
|
|
155
|
+
<div class="setting-desc">Your permission level in this pod.</div>
|
|
156
|
+
</div>
|
|
157
|
+
<div><span class="role-badge" id="role-badge">—</span></div>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="setting-row">
|
|
160
|
+
<div>
|
|
161
|
+
<div class="setting-label">Username</div>
|
|
162
|
+
<div class="setting-desc">The name you log in with. Must be unique.</div>
|
|
163
|
+
</div>
|
|
164
|
+
<input class="input" id="new-username" type="text" autocomplete="username" spellcheck="false" />
|
|
165
|
+
</div>
|
|
166
|
+
<div class="setting-row">
|
|
167
|
+
<div>
|
|
168
|
+
<div class="setting-label">Current password</div>
|
|
169
|
+
<div class="setting-desc">Required to confirm the username change.</div>
|
|
170
|
+
</div>
|
|
171
|
+
<input class="input" id="pw-for-username" type="password" autocomplete="current-password" />
|
|
172
|
+
</div>
|
|
173
|
+
<div class="save-row">
|
|
174
|
+
<button class="btn-save" id="save-username-btn">Save username</button>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<!-- Password -->
|
|
179
|
+
<div id="banner-password" class="banner"></div>
|
|
180
|
+
<div class="settings-group">
|
|
181
|
+
<div class="group-header">Change password</div>
|
|
182
|
+
<div class="setting-row">
|
|
183
|
+
<div>
|
|
184
|
+
<div class="setting-label">Current password</div>
|
|
185
|
+
</div>
|
|
186
|
+
<input class="input" id="cur-password" type="password" autocomplete="current-password" />
|
|
187
|
+
</div>
|
|
188
|
+
<div class="setting-row">
|
|
189
|
+
<div>
|
|
190
|
+
<div class="setting-label">New password</div>
|
|
191
|
+
<div class="setting-desc">Minimum 8 characters.</div>
|
|
192
|
+
</div>
|
|
193
|
+
<input class="input" id="new-password" type="password" autocomplete="new-password" />
|
|
194
|
+
</div>
|
|
195
|
+
<div class="setting-row">
|
|
196
|
+
<div>
|
|
197
|
+
<div class="setting-label">Confirm new password</div>
|
|
198
|
+
</div>
|
|
199
|
+
<input class="input" id="confirm-password" type="password" autocomplete="new-password" />
|
|
200
|
+
</div>
|
|
201
|
+
<div class="save-row">
|
|
202
|
+
<button class="btn-save" id="save-password-btn">Change password</button>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
</div><!-- /.account-wrap -->
|
|
207
|
+
</main>
|
|
208
|
+
</div><!-- /.app -->
|
|
209
|
+
|
|
210
|
+
<script type="module">
|
|
211
|
+
const me = await fetch('/api/auth/me', { credentials: 'include' }).then(r => r.json()).catch(() => null);
|
|
212
|
+
if (!me?.user) { location.replace('/login.html'); }
|
|
213
|
+
|
|
214
|
+
const { username, role } = me.user;
|
|
215
|
+
document.getElementById('topbar-user').textContent = username;
|
|
216
|
+
document.getElementById('new-username').value = username;
|
|
217
|
+
document.getElementById('role-badge').textContent = role;
|
|
218
|
+
if (role === 'admin') document.querySelectorAll('.admin-only').forEach(el => el.style.display = '');
|
|
219
|
+
|
|
220
|
+
document.getElementById('logout-btn').addEventListener('click', async () => {
|
|
221
|
+
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
|
|
222
|
+
location.replace('/login.html');
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
function showBanner(id, ok, text) {
|
|
226
|
+
const el = document.getElementById(id);
|
|
227
|
+
el.className = 'banner ' + (ok ? 'banner-ok' : 'banner-err');
|
|
228
|
+
el.textContent = (ok ? '✓ ' : '✕ ') + text;
|
|
229
|
+
el.style.display = 'flex';
|
|
230
|
+
setTimeout(() => el.style.display = 'none', 3500);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
document.getElementById('save-username-btn').addEventListener('click', async () => {
|
|
234
|
+
const newUsername = document.getElementById('new-username').value.trim();
|
|
235
|
+
const currentPassword = document.getElementById('pw-for-username').value;
|
|
236
|
+
if (!newUsername || !currentPassword) {
|
|
237
|
+
showBanner('banner-profile', false, 'Fill in username and current password.');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const res = await fetch('/api/account/username', {
|
|
241
|
+
method: 'PUT', credentials: 'include',
|
|
242
|
+
headers: { 'Content-Type': 'application/json' },
|
|
243
|
+
body: JSON.stringify({ newUsername, currentPassword }),
|
|
244
|
+
}).then(r => r.json()).catch(() => ({ error: 'Network error' }));
|
|
245
|
+
|
|
246
|
+
if (res.ok) {
|
|
247
|
+
document.getElementById('topbar-user').textContent = newUsername;
|
|
248
|
+
document.getElementById('pw-for-username').value = '';
|
|
249
|
+
showBanner('banner-profile', true, 'Username updated.');
|
|
250
|
+
} else {
|
|
251
|
+
showBanner('banner-profile', false, res.error || 'Failed to update username.');
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
document.getElementById('save-password-btn').addEventListener('click', async () => {
|
|
256
|
+
const currentPassword = document.getElementById('cur-password').value;
|
|
257
|
+
const newPassword = document.getElementById('new-password').value;
|
|
258
|
+
const confirm = document.getElementById('confirm-password').value;
|
|
259
|
+
if (!currentPassword || !newPassword) {
|
|
260
|
+
showBanner('banner-password', false, 'Fill in all password fields.');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (newPassword !== confirm) {
|
|
264
|
+
showBanner('banner-password', false, 'New passwords do not match.');
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const res = await fetch('/api/account/password', {
|
|
268
|
+
method: 'PUT', credentials: 'include',
|
|
269
|
+
headers: { 'Content-Type': 'application/json' },
|
|
270
|
+
body: JSON.stringify({ currentPassword, newPassword }),
|
|
271
|
+
}).then(r => r.json()).catch(() => ({ error: 'Network error' }));
|
|
272
|
+
|
|
273
|
+
if (res.ok) {
|
|
274
|
+
document.getElementById('cur-password').value = '';
|
|
275
|
+
document.getElementById('new-password').value = '';
|
|
276
|
+
document.getElementById('confirm-password').value = '';
|
|
277
|
+
showBanner('banner-password', true, 'Password changed.');
|
|
278
|
+
} else {
|
|
279
|
+
showBanner('banner-password', false, res.error || 'Failed to change password.');
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
</script>
|
|
283
|
+
<script src="/sidebar.js"></script>
|
|
284
|
+
<script src="/search.js"></script>
|
|
285
|
+
<script src="/xfce.js" type="module"></script>
|
|
286
|
+
</body>
|
|
287
|
+
</html>
|