clauddy 1.10.0 → 1.11.0
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 +2 -0
- package/main.js +64 -0
- package/package.json +4 -4
- package/preload.js +4 -0
- package/renderer/index.html +51 -34
- package/renderer/pet.js +45 -0
- package/renderer/style.css +150 -15
package/README.md
CHANGED
|
@@ -87,6 +87,8 @@ curl -fsSL https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/main
|
|
|
87
87
|
|
|
88
88
|
**Clauddy is free and open source** — the command above just downloads the latest release from this repo and drops it in `/Applications`, nothing else (you can read [`install.sh`](install.sh) first if you'd like).
|
|
89
89
|
|
|
90
|
+
Once installed, you can update in-app: **⚙ Settings → Check for updates → Update now** runs the same installer and relaunches the new build.
|
|
91
|
+
|
|
90
92
|
Why not a normal download? macOS blocks **unsigned** apps downloaded through a browser with a scary *"damaged, move to Trash"* warning — even when they're perfectly safe. It's a false alarm: the only way to silence it is to pay Apple **$99/year** to sign + notarize, which a free hobby app skips. Files fetched with `curl` aren't flagged, so this method simply **lets your Mac open the app** without the block. It then registers in **Login Items** and starts with your Mac — set it and forget it.
|
|
91
93
|
|
|
92
94
|
#### 2. Run it via `bunx` (no install)
|
package/main.js
CHANGED
|
@@ -12,9 +12,12 @@ const {
|
|
|
12
12
|
const path = require('node:path')
|
|
13
13
|
const fs = require('node:fs')
|
|
14
14
|
const os = require('node:os')
|
|
15
|
+
const { spawn } = require('node:child_process')
|
|
15
16
|
const { getUsage } = require('./usage')
|
|
16
17
|
const auth = require('./auth')
|
|
17
18
|
|
|
19
|
+
const REPO = 'renatoaug/claude-usage-monitor'
|
|
20
|
+
|
|
18
21
|
// data dir: kept outside the project folder so moving the repo doesn't break it.
|
|
19
22
|
// when CLAUDE_CONFIG_DIR is set (e.g. via direnv for multi-account setups), nest
|
|
20
23
|
// the widget's data under that account's Claude config dir so each account gets
|
|
@@ -163,12 +166,15 @@ function createWindow() {
|
|
|
163
166
|
win.webContents.once('did-finish-load', () => {
|
|
164
167
|
tick()
|
|
165
168
|
win.webContents.send('config', publicConfig(config))
|
|
169
|
+
win.webContents.send('version', app.getVersion())
|
|
166
170
|
win.webContents.send('auth-state', { connected: auth.isConnected() })
|
|
167
171
|
pollTimer = setInterval(tick, config.pollIntervalMs)
|
|
168
172
|
startUsagePoll()
|
|
169
173
|
sendProfile()
|
|
170
174
|
watchDebug()
|
|
171
175
|
applyMode(config.mode) // reveal the widget, or set up the tray + popover
|
|
176
|
+
setTimeout(autoUpdateCheck, 8000) // check once shortly after launch…
|
|
177
|
+
setInterval(autoUpdateCheck, 6 * 60 * 60 * 1000) // …then every 6h
|
|
172
178
|
})
|
|
173
179
|
}
|
|
174
180
|
|
|
@@ -412,6 +418,64 @@ ipcMain.on('save-config', (_e, patch) => {
|
|
|
412
418
|
applyMode(config.mode) // switch between floating widget and menu-bar popover live
|
|
413
419
|
})
|
|
414
420
|
|
|
421
|
+
// ---- self-update (checks the latest GitHub release, runs install.sh) ---------
|
|
422
|
+
async function fetchLatestTag() {
|
|
423
|
+
const res = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, {
|
|
424
|
+
headers: { 'User-Agent': 'Clauddy', Accept: 'application/vnd.github+json' },
|
|
425
|
+
})
|
|
426
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
427
|
+
const j = await res.json()
|
|
428
|
+
return j.tag_name // e.g. "v1.9.1"
|
|
429
|
+
}
|
|
430
|
+
// compare two "1.2.3" versions; >0 if a is newer than b
|
|
431
|
+
function cmpVer(a, b) {
|
|
432
|
+
const pa = String(a).replace(/^v/, '').split('.').map(Number)
|
|
433
|
+
const pb = String(b).replace(/^v/, '').split('.').map(Number)
|
|
434
|
+
for (let i = 0; i < 3; i++) {
|
|
435
|
+
const d = (pa[i] || 0) - (pb[i] || 0)
|
|
436
|
+
if (d) return d > 0 ? 1 : -1
|
|
437
|
+
}
|
|
438
|
+
return 0
|
|
439
|
+
}
|
|
440
|
+
async function computeUpdate() {
|
|
441
|
+
const latest = await fetchLatestTag()
|
|
442
|
+
return { latest, available: cmpVer(latest, app.getVersion()) > 0 }
|
|
443
|
+
}
|
|
444
|
+
// manual check (from the Settings button): shows checking / result / error
|
|
445
|
+
ipcMain.on('check-updates', async () => {
|
|
446
|
+
if (!win || win.isDestroyed()) return
|
|
447
|
+
const send = (s) => !win.isDestroyed() && win.webContents.send('update-status', s)
|
|
448
|
+
send({ state: 'checking' })
|
|
449
|
+
try {
|
|
450
|
+
const u = await computeUpdate()
|
|
451
|
+
send(u.available ? { state: 'available', latest: u.latest } : { state: 'uptodate' })
|
|
452
|
+
} catch (e) {
|
|
453
|
+
send({ state: 'error', message: String(e?.message || e) })
|
|
454
|
+
}
|
|
455
|
+
})
|
|
456
|
+
// silent background check: only speaks up when there's actually an update, so
|
|
457
|
+
// the renderer can badge the gear without any UI churn on the common case
|
|
458
|
+
async function autoUpdateCheck() {
|
|
459
|
+
if (!win || win.isDestroyed()) return
|
|
460
|
+
try {
|
|
461
|
+
const u = await computeUpdate()
|
|
462
|
+
if (u.available) win.webContents.send('update-status', { state: 'available', latest: u.latest })
|
|
463
|
+
} catch {} // offline / rate-limited: stay quiet, try again on the next tick
|
|
464
|
+
}
|
|
465
|
+
ipcMain.on('do-update', () => {
|
|
466
|
+
// Windows/Linux have no install.sh: send them to the releases page instead.
|
|
467
|
+
if (process.platform !== 'darwin') {
|
|
468
|
+
shell.openExternal(`https://github.com/${REPO}/releases/latest`)
|
|
469
|
+
return
|
|
470
|
+
}
|
|
471
|
+
if (win && !win.isDestroyed()) win.webContents.send('update-status', { state: 'updating' })
|
|
472
|
+
const cmd = `curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | bash`
|
|
473
|
+
const child = spawn('/bin/bash', ['-lc', cmd], { detached: true, stdio: 'ignore' })
|
|
474
|
+
child.unref()
|
|
475
|
+
// quit so the installer can replace the running .app and relaunch the new build
|
|
476
|
+
setTimeout(() => app.quit(), 1500)
|
|
477
|
+
})
|
|
478
|
+
|
|
415
479
|
ipcMain.on('quit', () => app.quit())
|
|
416
480
|
|
|
417
481
|
// Electron's login-item API only covers macOS (SMAppService) and Windows (the
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clauddy",
|
|
3
3
|
"desktopName": "clauddy.desktop",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.11.0",
|
|
5
5
|
"description": "A cute desktop pet that tracks your Claude Code usage",
|
|
6
6
|
"main": "main.js",
|
|
7
7
|
"bin": {
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"scripts": {
|
|
20
20
|
"start": "electron .",
|
|
21
21
|
"pack": "node scripts/build-app.js --dir",
|
|
22
|
-
"dist": "node scripts/build-app.js --mac zip",
|
|
23
|
-
"dist:win": "node scripts/build-app.js --win zip",
|
|
24
|
-
"dist:linux": "node scripts/build-app.js --linux",
|
|
22
|
+
"dist": "node scripts/build-app.js --mac zip --publish never",
|
|
23
|
+
"dist:win": "node scripts/build-app.js --win zip --publish never",
|
|
24
|
+
"dist:linux": "node scripts/build-app.js --linux --publish never",
|
|
25
25
|
"icon": "bash build-icon.sh",
|
|
26
26
|
"gifs": "electron tools/capture/capture.js",
|
|
27
27
|
"format": "biome format --write .",
|
package/preload.js
CHANGED
|
@@ -15,5 +15,9 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
15
15
|
authCode: (code) => ipcRenderer.send('auth-code', code),
|
|
16
16
|
authLogout: () => ipcRenderer.send('auth-logout'),
|
|
17
17
|
onDebugState: (cb) => ipcRenderer.on('debug-state', (_e, s) => cb(s)),
|
|
18
|
+
onVersion: (cb) => ipcRenderer.on('version', (_e, v) => cb(v)),
|
|
19
|
+
onUpdateStatus: (cb) => ipcRenderer.on('update-status', (_e, s) => cb(s)),
|
|
20
|
+
checkUpdates: () => ipcRenderer.send('check-updates'),
|
|
21
|
+
doUpdate: () => ipcRenderer.send('do-update'),
|
|
18
22
|
quit: () => ipcRenderer.send('quit'),
|
|
19
23
|
})
|
package/renderer/index.html
CHANGED
|
@@ -305,55 +305,72 @@
|
|
|
305
305
|
</div>
|
|
306
306
|
</div>
|
|
307
307
|
|
|
308
|
-
<div class="set-
|
|
309
|
-
|
|
308
|
+
<div class="set-section">
|
|
309
|
+
<div class="set-sec-title">Display</div>
|
|
310
310
|
<span class="seg" id="set-mode">
|
|
311
311
|
<button type="button" class="seg-btn" data-mode="floating">Floating pet</button>
|
|
312
312
|
<button type="button" class="seg-btn" data-mode="menubar">Menu bar</button>
|
|
313
313
|
</span>
|
|
314
|
-
</
|
|
314
|
+
</div>
|
|
315
315
|
|
|
316
|
-
<
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
316
|
+
<div class="set-section">
|
|
317
|
+
<div class="set-sec-title">
|
|
318
|
+
Alerts
|
|
319
|
+
<input id="set-alerts" class="set-toggle" type="checkbox" aria-label="Enable alerts" />
|
|
320
|
+
</div>
|
|
321
|
+
<div class="set-thresholds">
|
|
322
|
+
<span class="thr">
|
|
323
|
+
<span class="thr-cap">heads-up</span>
|
|
324
|
+
<span class="num">
|
|
325
|
+
<input id="set-t1" type="number" min="1" max="100" />
|
|
326
|
+
<span class="num-spin">
|
|
327
|
+
<button type="button" class="num-btn" data-for="set-t1" data-step="1">▲</button>
|
|
328
|
+
<button type="button" class="num-btn" data-for="set-t1" data-step="-1">▼</button>
|
|
329
|
+
</span>
|
|
325
330
|
</span>
|
|
326
331
|
</span>
|
|
327
|
-
<span class="
|
|
328
|
-
<
|
|
329
|
-
<span class="num
|
|
330
|
-
<
|
|
331
|
-
<
|
|
332
|
+
<span class="thr">
|
|
333
|
+
<span class="thr-cap">near limit</span>
|
|
334
|
+
<span class="num">
|
|
335
|
+
<input id="set-t2" type="number" min="1" max="100" />
|
|
336
|
+
<span class="num-spin">
|
|
337
|
+
<button type="button" class="num-btn" data-for="set-t2" data-step="1">▲</button>
|
|
338
|
+
<button type="button" class="num-btn" data-for="set-t2" data-step="-1">▼</button>
|
|
339
|
+
</span>
|
|
332
340
|
</span>
|
|
333
341
|
</span>
|
|
334
|
-
</
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
</
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<div class="set-section">
|
|
346
|
+
<div class="set-sec-title">Catch fire</div>
|
|
347
|
+
<div class="set-thresholds">
|
|
348
|
+
<span class="thr">
|
|
349
|
+
<span class="thr-cap">session %</span>
|
|
350
|
+
<span class="num">
|
|
351
|
+
<input id="set-fire" type="number" min="1" max="99" />
|
|
352
|
+
<span class="num-spin">
|
|
353
|
+
<button type="button" class="num-btn" data-for="set-fire" data-step="1">▲</button>
|
|
354
|
+
<button type="button" class="num-btn" data-for="set-fire" data-step="-1">▼</button>
|
|
355
|
+
</span>
|
|
348
356
|
</span>
|
|
349
357
|
</span>
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
353
361
|
<div class="set-actions">
|
|
354
362
|
<button id="set-cancel">Cancel</button>
|
|
355
363
|
<button id="set-save">Save</button>
|
|
356
364
|
</div>
|
|
365
|
+
|
|
366
|
+
<div class="set-updates">
|
|
367
|
+
<span class="upd-ver">Clauddy v<span id="app-version">—</span></span>
|
|
368
|
+
<span class="upd-right">
|
|
369
|
+
<span id="upd-status"></span>
|
|
370
|
+
<button type="button" id="upd-check" class="acc-link">Check for updates</button>
|
|
371
|
+
<button type="button" id="upd-now" class="upd-btn" hidden>Update now</button>
|
|
372
|
+
</span>
|
|
373
|
+
</div>
|
|
357
374
|
</div>
|
|
358
375
|
</div>
|
|
359
376
|
<script src="pet.js"></script>
|
package/renderer/pet.js
CHANGED
|
@@ -640,6 +640,51 @@ el('acc-confirm').addEventListener('click', () => {
|
|
|
640
640
|
})
|
|
641
641
|
el('acc-logout').addEventListener('click', () => window.api.authLogout())
|
|
642
642
|
|
|
643
|
+
// self-update: show the version, check the latest release, one-click update
|
|
644
|
+
window.api.onVersion((v) => {
|
|
645
|
+
if (v) el('app-version').textContent = v
|
|
646
|
+
})
|
|
647
|
+
window.api.onUpdateStatus((s) => {
|
|
648
|
+
const status = el('upd-status')
|
|
649
|
+
const now = el('upd-now')
|
|
650
|
+
const check = el('upd-check')
|
|
651
|
+
// reset to the idle look, then layer each state on top
|
|
652
|
+
now.hidden = true
|
|
653
|
+
check.hidden = false
|
|
654
|
+
status.textContent = ''
|
|
655
|
+
status.className = ''
|
|
656
|
+
const state = s?.state
|
|
657
|
+
if (state === 'checking') {
|
|
658
|
+
status.textContent = 'Checking…'
|
|
659
|
+
check.hidden = true
|
|
660
|
+
} else if (state === 'uptodate') {
|
|
661
|
+
status.textContent = "You're up to date"
|
|
662
|
+
status.className = 'ok'
|
|
663
|
+
} else if (state === 'available') {
|
|
664
|
+
// the button carries the version, so no separate status text is needed
|
|
665
|
+
now.textContent = `Update to ${s.latest}`
|
|
666
|
+
now.hidden = false
|
|
667
|
+
check.hidden = true
|
|
668
|
+
} else if (state === 'updating') {
|
|
669
|
+
status.textContent = 'Updating… the app will restart'
|
|
670
|
+
check.hidden = true
|
|
671
|
+
} else if (state === 'error') {
|
|
672
|
+
status.textContent = 'Check failed'
|
|
673
|
+
status.className = 'err'
|
|
674
|
+
}
|
|
675
|
+
// pulse a dot on the gear whenever an update is waiting (cleared once it's
|
|
676
|
+
// up to date or actively updating) — so it's noticeable without opening Settings
|
|
677
|
+
if (state === 'available') document.body.classList.add('has-update')
|
|
678
|
+
else if (state === 'uptodate' || state === 'updating')
|
|
679
|
+
document.body.classList.remove('has-update')
|
|
680
|
+
if (document.body.classList.contains('settings-open')) fitSize()
|
|
681
|
+
})
|
|
682
|
+
el('upd-check').addEventListener('click', () => window.api.checkUpdates())
|
|
683
|
+
el('upd-now').addEventListener('click', () => {
|
|
684
|
+
el('upd-now').disabled = true
|
|
685
|
+
window.api.doUpdate()
|
|
686
|
+
})
|
|
687
|
+
|
|
643
688
|
// settings panel
|
|
644
689
|
// snapshot of the editable fields, to tell whether there are unsaved changes
|
|
645
690
|
let settingsBaseline = null
|
package/renderer/style.css
CHANGED
|
@@ -104,6 +104,38 @@ body {
|
|
|
104
104
|
#gear {
|
|
105
105
|
font-size: 10px;
|
|
106
106
|
}
|
|
107
|
+
/* "update available" cue: a soft pulsing coral dot on the gear's corner, so a
|
|
108
|
+
new version is noticeable without opening Settings */
|
|
109
|
+
body.has-update #gear {
|
|
110
|
+
position: relative;
|
|
111
|
+
overflow: visible;
|
|
112
|
+
}
|
|
113
|
+
body.has-update #gear::after {
|
|
114
|
+
content: "";
|
|
115
|
+
position: absolute;
|
|
116
|
+
top: -2px;
|
|
117
|
+
right: -2px;
|
|
118
|
+
width: 7px;
|
|
119
|
+
height: 7px;
|
|
120
|
+
border-radius: 50%;
|
|
121
|
+
background: var(--coral);
|
|
122
|
+
box-shadow:
|
|
123
|
+
0 0 0 1.5px #2a1f19,
|
|
124
|
+
0 0 5px 1px rgba(217, 119, 87, 0.6);
|
|
125
|
+
animation: upd-badge 1.6s ease-in-out infinite;
|
|
126
|
+
pointer-events: none;
|
|
127
|
+
}
|
|
128
|
+
@keyframes upd-badge {
|
|
129
|
+
0%,
|
|
130
|
+
100% {
|
|
131
|
+
transform: scale(1);
|
|
132
|
+
opacity: 0.9;
|
|
133
|
+
}
|
|
134
|
+
50% {
|
|
135
|
+
transform: scale(1.3);
|
|
136
|
+
opacity: 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
107
139
|
/* minimize icon swaps to an "expand" icon when collapsed */
|
|
108
140
|
.ic-max {
|
|
109
141
|
display: none;
|
|
@@ -232,8 +264,8 @@ body.settings-open #settings {
|
|
|
232
264
|
border-color: var(--coral);
|
|
233
265
|
}
|
|
234
266
|
/* hide the ugly native number spinners (custom ones below) */
|
|
235
|
-
|
|
236
|
-
|
|
267
|
+
input[type="number"]::-webkit-inner-spin-button,
|
|
268
|
+
input[type="number"]::-webkit-outer-spin-button {
|
|
237
269
|
-webkit-appearance: none;
|
|
238
270
|
appearance: none;
|
|
239
271
|
margin: 0;
|
|
@@ -270,28 +302,73 @@ body.settings-open #settings {
|
|
|
270
302
|
body.is-menubar #min {
|
|
271
303
|
display: none;
|
|
272
304
|
}
|
|
273
|
-
|
|
305
|
+
/* settings sections: Display / Alerts / Catch fire — each with a standardized
|
|
306
|
+
title, separated by a hairline divider */
|
|
307
|
+
.set-section {
|
|
308
|
+
margin-bottom: 10px;
|
|
309
|
+
}
|
|
310
|
+
.set-section + .set-section {
|
|
311
|
+
border-top: 0.5px solid var(--hair);
|
|
312
|
+
padding-top: 10px;
|
|
313
|
+
}
|
|
314
|
+
.set-sec-title {
|
|
274
315
|
display: flex;
|
|
275
|
-
|
|
316
|
+
align-items: center;
|
|
317
|
+
justify-content: space-between;
|
|
318
|
+
font-size: 11px;
|
|
319
|
+
font-weight: 600;
|
|
320
|
+
color: var(--text);
|
|
321
|
+
margin-bottom: 8px;
|
|
276
322
|
}
|
|
277
|
-
/*
|
|
278
|
-
.set-
|
|
279
|
-
margin
|
|
323
|
+
/* alerts on/off toggle, sitting to the right of the section title */
|
|
324
|
+
.set-toggle {
|
|
325
|
+
margin: 0;
|
|
326
|
+
width: 13px;
|
|
327
|
+
height: 13px;
|
|
328
|
+
accent-color: var(--coral);
|
|
329
|
+
cursor: pointer;
|
|
280
330
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
331
|
+
/* threshold fields: caption on top, number input below — same shape for all
|
|
332
|
+
three (heads-up, near limit, catch fire) so they read as one set */
|
|
333
|
+
.set-thresholds {
|
|
334
|
+
display: flex;
|
|
335
|
+
gap: 12px;
|
|
336
|
+
}
|
|
337
|
+
.thr {
|
|
338
|
+
display: flex;
|
|
339
|
+
flex-direction: column;
|
|
340
|
+
gap: 4px;
|
|
341
|
+
}
|
|
342
|
+
.thr-cap {
|
|
343
|
+
font-size: 9px;
|
|
284
344
|
color: var(--muted);
|
|
285
|
-
|
|
345
|
+
height: 12px;
|
|
346
|
+
line-height: 12px;
|
|
286
347
|
}
|
|
287
|
-
/* custom
|
|
348
|
+
/* number field + custom stepper */
|
|
288
349
|
.num {
|
|
289
350
|
position: relative;
|
|
290
|
-
flex:
|
|
351
|
+
flex: none;
|
|
352
|
+
width: 66px;
|
|
291
353
|
}
|
|
292
354
|
.num input {
|
|
293
355
|
width: 100%;
|
|
294
|
-
|
|
356
|
+
background: rgba(255, 255, 255, 0.06);
|
|
357
|
+
border: 0.5px solid var(--hair);
|
|
358
|
+
border-radius: 8px;
|
|
359
|
+
color: var(--text);
|
|
360
|
+
font-size: 13px;
|
|
361
|
+
font-weight: 600;
|
|
362
|
+
font-variant-numeric: tabular-nums;
|
|
363
|
+
padding: 6px 24px 6px 10px;
|
|
364
|
+
outline: none;
|
|
365
|
+
transition:
|
|
366
|
+
border-color 0.15s,
|
|
367
|
+
background 0.15s;
|
|
368
|
+
}
|
|
369
|
+
.num input:focus {
|
|
370
|
+
border-color: var(--coral);
|
|
371
|
+
background: rgba(255, 255, 255, 0.09);
|
|
295
372
|
}
|
|
296
373
|
.num-spin {
|
|
297
374
|
position: absolute;
|
|
@@ -387,6 +464,61 @@ body.is-menubar #min {
|
|
|
387
464
|
}
|
|
388
465
|
}
|
|
389
466
|
|
|
467
|
+
/* updates footer */
|
|
468
|
+
.set-updates {
|
|
469
|
+
display: flex;
|
|
470
|
+
align-items: center;
|
|
471
|
+
justify-content: space-between;
|
|
472
|
+
gap: 8px;
|
|
473
|
+
margin-top: 11px;
|
|
474
|
+
padding-top: 9px;
|
|
475
|
+
border-top: 0.5px solid var(--hair);
|
|
476
|
+
}
|
|
477
|
+
.upd-ver {
|
|
478
|
+
font-size: 10px;
|
|
479
|
+
color: var(--muted);
|
|
480
|
+
}
|
|
481
|
+
.upd-right {
|
|
482
|
+
display: flex;
|
|
483
|
+
align-items: center;
|
|
484
|
+
gap: 8px;
|
|
485
|
+
}
|
|
486
|
+
#upd-status {
|
|
487
|
+
font-size: 10px;
|
|
488
|
+
color: var(--muted);
|
|
489
|
+
}
|
|
490
|
+
#upd-status.ok {
|
|
491
|
+
color: var(--green);
|
|
492
|
+
}
|
|
493
|
+
#upd-status.err {
|
|
494
|
+
color: var(--coral);
|
|
495
|
+
}
|
|
496
|
+
/* compact update button — lighter than the full-width .acc-btn */
|
|
497
|
+
.upd-btn {
|
|
498
|
+
border: none;
|
|
499
|
+
border-radius: 7px;
|
|
500
|
+
padding: 4px 9px;
|
|
501
|
+
font-size: 10px;
|
|
502
|
+
font-weight: 600;
|
|
503
|
+
color: #fff;
|
|
504
|
+
background: var(--coral);
|
|
505
|
+
cursor: pointer;
|
|
506
|
+
-webkit-app-region: no-drag;
|
|
507
|
+
transition:
|
|
508
|
+
background 0.18s,
|
|
509
|
+
transform 0.1s;
|
|
510
|
+
}
|
|
511
|
+
.upd-btn:hover {
|
|
512
|
+
background: #e2876a;
|
|
513
|
+
}
|
|
514
|
+
.upd-btn:active {
|
|
515
|
+
transform: scale(0.97);
|
|
516
|
+
}
|
|
517
|
+
.upd-btn:disabled {
|
|
518
|
+
opacity: 0.6;
|
|
519
|
+
cursor: default;
|
|
520
|
+
}
|
|
521
|
+
|
|
390
522
|
/* live tag */
|
|
391
523
|
.live-only {
|
|
392
524
|
display: none;
|
|
@@ -402,6 +534,8 @@ body.live .live-only {
|
|
|
402
534
|
/* account / auth */
|
|
403
535
|
#account {
|
|
404
536
|
margin-bottom: 10px;
|
|
537
|
+
padding-bottom: 10px;
|
|
538
|
+
border-bottom: 0.5px solid var(--hair); /* divider before the first section */
|
|
405
539
|
}
|
|
406
540
|
.acc-btn {
|
|
407
541
|
width: 100%;
|
|
@@ -434,12 +568,13 @@ body.live .live-only {
|
|
|
434
568
|
color: var(--muted);
|
|
435
569
|
font-size: 10px;
|
|
436
570
|
cursor: pointer;
|
|
437
|
-
text-decoration:
|
|
571
|
+
text-decoration: none;
|
|
438
572
|
-webkit-app-region: no-drag;
|
|
439
573
|
transition: color 0.15s;
|
|
440
574
|
}
|
|
441
575
|
.acc-link:hover {
|
|
442
576
|
color: var(--text);
|
|
577
|
+
text-decoration: underline;
|
|
443
578
|
}
|
|
444
579
|
.acc-ok {
|
|
445
580
|
color: var(--green);
|