clauddy 1.5.1 → 1.7.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 +26 -5
- package/config.json +1 -0
- package/main.js +5 -0
- package/package.json +15 -1
- package/renderer/index.html +70 -1
- package/renderer/pet.js +65 -1
- package/renderer/style.css +70 -1
package/README.md
CHANGED
|
@@ -50,9 +50,13 @@ Plus a welcome **wave** on launch. You can [poke the pet from the terminal](#pla
|
|
|
50
50
|
|
|
51
51
|
## Install
|
|
52
52
|
|
|
53
|
-
macOS (Apple Silicon).
|
|
53
|
+
**macOS (Apple Silicon)** is the first-class build. **Windows (x64)** works too, and **Linux** is next. The `bunx`/`npx` route below runs on all of them today.
|
|
54
54
|
|
|
55
|
-
###
|
|
55
|
+
### macOS
|
|
56
|
+
|
|
57
|
+
Two ways, depending on what you want:
|
|
58
|
+
|
|
59
|
+
#### 1. Install as an app — opens at login (recommended)
|
|
56
60
|
|
|
57
61
|
One command — it downloads the latest release and drops it in `/Applications`:
|
|
58
62
|
|
|
@@ -64,7 +68,7 @@ curl -fsSL https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/main
|
|
|
64
68
|
|
|
65
69
|
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.
|
|
66
70
|
|
|
67
|
-
|
|
71
|
+
#### 2. Run it via `bunx` (no install)
|
|
68
72
|
|
|
69
73
|
Needs [Bun](https://bun.sh) (or use `npx` with Node 24):
|
|
70
74
|
|
|
@@ -74,7 +78,23 @@ bunx clauddy
|
|
|
74
78
|
|
|
75
79
|
The first run downloads Electron, so give it a moment. Handy for a quick run, but it stays up **only while that command is open** and won't start on its own. Quit it with the **×** button.
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
### Windows (x64)
|
|
82
|
+
|
|
83
|
+
The quickest path works the same as macOS — with [Bun](https://bun.sh) or Node 24 installed:
|
|
84
|
+
|
|
85
|
+
```powershell
|
|
86
|
+
bunx clauddy # or: npx clauddy
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Prefer a standalone app with no Node/Bun? Grab the **portable zip** (`Clauddy-<version>-win.zip`) from the [latest release](https://github.com/renatoaug/claude-usage-monitor/releases), unzip it anywhere, and run `Clauddy.exe`. Because the app is unsigned, Windows **SmartScreen** shows a "Windows protected your PC" prompt the first time — click **More info → Run anyway**. From then on it starts with Windows.
|
|
90
|
+
|
|
91
|
+
> Windows builds are produced by the **Build** workflow (Actions ▸ Build) — attaching them to every release automatically is on the roadmap.
|
|
92
|
+
|
|
93
|
+
### Linux
|
|
94
|
+
|
|
95
|
+
Coming soon — the build is already wired for `tar.gz` and AppImage. In the meantime, `bunx clauddy` / `npx clauddy` works today.
|
|
96
|
+
|
|
97
|
+
> The app keeps its data in `~/.claude-usage-monitor`, regardless of platform or how you run it.
|
|
78
98
|
|
|
79
99
|
## Controls
|
|
80
100
|
|
|
@@ -95,7 +115,8 @@ Settings saved from the UI live in `~/.claude-usage-monitor/config.json`, so you
|
|
|
95
115
|
```jsonc
|
|
96
116
|
{
|
|
97
117
|
"alerts": true, // macOS notifications on/off
|
|
98
|
-
"alertThresholds": [80, 95], // notify when session/week cross these %
|
|
118
|
+
"alertThresholds": [80, 95], // notify when session/week cross these % (two levels)
|
|
119
|
+
"fireThreshold": 90, // session % at which the pet catches fire (maxed out stays 100)
|
|
99
120
|
"pollIntervalMs": 4000, // how often local logs are re-read
|
|
100
121
|
"activeThresholdMs": 8000, // "working" if Claude was active within this window
|
|
101
122
|
"sleepThresholdMs": 300000, // "sleeping" after this much idle time (5 min)
|
package/config.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"_note": "Defaults bundled with the app. User settings are saved to ~/.claude-usage-monitor/config.json. Session/weekly % come from your account (log in via Settings).",
|
|
3
3
|
"alerts": true,
|
|
4
4
|
"alertThresholds": [80, 95],
|
|
5
|
+
"fireThreshold": 90,
|
|
5
6
|
"pollIntervalMs": 4000,
|
|
6
7
|
"activeThresholdMs": 8000,
|
|
7
8
|
"sleepThresholdMs": 300000
|
package/main.js
CHANGED
|
@@ -38,6 +38,7 @@ function publicConfig(c) {
|
|
|
38
38
|
weeklyAnchorIso: c.weeklyAnchorIso,
|
|
39
39
|
alerts: c.alerts,
|
|
40
40
|
alertThresholds: c.alertThresholds,
|
|
41
|
+
fireThreshold: c.fireThreshold,
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -49,6 +50,7 @@ function loadConfig() {
|
|
|
49
50
|
weeklyAnchorIso: null,
|
|
50
51
|
alerts: true,
|
|
51
52
|
alertThresholds: [80, 95],
|
|
53
|
+
fireThreshold: 90, // session % at which the pet catches fire (tired still fixed at 100)
|
|
52
54
|
pollIntervalMs: 4000,
|
|
53
55
|
activeThresholdMs: 8000,
|
|
54
56
|
sleepThresholdMs: 300000,
|
|
@@ -101,6 +103,7 @@ function createWindow() {
|
|
|
101
103
|
y: workAreaSize.height - H - 24,
|
|
102
104
|
frame: false,
|
|
103
105
|
transparent: true,
|
|
106
|
+
backgroundColor: '#00000000', // fully transparent — Windows needs this or the window paints black
|
|
104
107
|
resizable: false,
|
|
105
108
|
alwaysOnTop: true,
|
|
106
109
|
skipTaskbar: true,
|
|
@@ -265,6 +268,8 @@ ipcMain.on('save-config', (_e, patch) => {
|
|
|
265
268
|
ipcMain.on('quit', () => app.quit())
|
|
266
269
|
|
|
267
270
|
app.whenReady().then(() => {
|
|
271
|
+
// Windows toast notifications need an explicit AppUserModelID to show reliably
|
|
272
|
+
if (process.platform === 'win32') app.setAppUserModelId('app.clauddy')
|
|
268
273
|
createWindow()
|
|
269
274
|
// open at login (packaged app only)
|
|
270
275
|
if (app.isPackaged) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clauddy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "A cute desktop pet that tracks your Claude Code usage",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"start": "electron .",
|
|
20
20
|
"pack": "node scripts/build-app.js --dir",
|
|
21
21
|
"dist": "node scripts/build-app.js --mac zip",
|
|
22
|
+
"dist:win": "node scripts/build-app.js --win zip",
|
|
23
|
+
"dist:linux": "node scripts/build-app.js --linux",
|
|
22
24
|
"icon": "bash build-icon.sh",
|
|
23
25
|
"gifs": "electron tools/capture/capture.js",
|
|
24
26
|
"format": "biome format --write .",
|
|
@@ -73,6 +75,18 @@
|
|
|
73
75
|
"target": "dir",
|
|
74
76
|
"identity": null,
|
|
75
77
|
"icon": "build/icon.icns"
|
|
78
|
+
},
|
|
79
|
+
"win": {
|
|
80
|
+
"target": "zip",
|
|
81
|
+
"icon": "build/icon.ico"
|
|
82
|
+
},
|
|
83
|
+
"linux": {
|
|
84
|
+
"target": [
|
|
85
|
+
"tar.gz",
|
|
86
|
+
"AppImage"
|
|
87
|
+
],
|
|
88
|
+
"category": "Utility",
|
|
89
|
+
"icon": "build/icon.png"
|
|
76
90
|
}
|
|
77
91
|
},
|
|
78
92
|
"dependencies": {
|
package/renderer/index.html
CHANGED
|
@@ -31,6 +31,43 @@
|
|
|
31
31
|
|
|
32
32
|
<!-- Claude pixel pet -->
|
|
33
33
|
<div id="stage">
|
|
34
|
+
<!-- reusable gear shape -->
|
|
35
|
+
<svg width="0" height="0" aria-hidden="true" style="position: absolute">
|
|
36
|
+
<defs>
|
|
37
|
+
<g id="gear-unit">
|
|
38
|
+
<g id="gear-teeth">
|
|
39
|
+
<rect x="10.5" y="0.5" width="3" height="5" rx="0.5" />
|
|
40
|
+
<rect x="10.5" y="18.5" width="3" height="5" rx="0.5" />
|
|
41
|
+
<rect x="0.5" y="10.5" width="5" height="3" rx="0.5" />
|
|
42
|
+
<rect x="18.5" y="10.5" width="5" height="3" rx="0.5" />
|
|
43
|
+
</g>
|
|
44
|
+
<use href="#gear-teeth" transform="rotate(45 12 12)" />
|
|
45
|
+
<circle cx="12" cy="12" r="7.5" />
|
|
46
|
+
<circle cx="12" cy="12" r="2.6" fill="#0a0706" />
|
|
47
|
+
</g>
|
|
48
|
+
</defs>
|
|
49
|
+
</svg>
|
|
50
|
+
<!-- loose gears turning on the ground behind the pet (settings only) -->
|
|
51
|
+
<svg id="gears" viewBox="0 0 216 135" preserveAspectRatio="none">
|
|
52
|
+
<g transform="translate(150 118) scale(1.35)">
|
|
53
|
+
<g>
|
|
54
|
+
<use href="#gear-unit" x="-12" y="-12" />
|
|
55
|
+
<animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="3s" repeatCount="indefinite" />
|
|
56
|
+
</g>
|
|
57
|
+
</g>
|
|
58
|
+
<g transform="translate(188 124) scale(0.78)">
|
|
59
|
+
<g>
|
|
60
|
+
<use href="#gear-unit" x="-12" y="-12" />
|
|
61
|
+
<animateTransform attributeName="transform" type="rotate" from="360" to="0" dur="2.3s" repeatCount="indefinite" />
|
|
62
|
+
</g>
|
|
63
|
+
</g>
|
|
64
|
+
<g transform="translate(118 125) scale(0.6)">
|
|
65
|
+
<g>
|
|
66
|
+
<use href="#gear-unit" x="-12" y="-12" />
|
|
67
|
+
<animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="1.8s" repeatCount="indefinite" />
|
|
68
|
+
</g>
|
|
69
|
+
</g>
|
|
70
|
+
</svg>
|
|
34
71
|
<svg id="scene" viewBox="0 0 216 135" preserveAspectRatio="none"></svg>
|
|
35
72
|
<div id="shadow"></div>
|
|
36
73
|
<div id="pet">
|
|
@@ -38,6 +75,21 @@
|
|
|
38
75
|
<g id="body"></g>
|
|
39
76
|
<g id="eyes"></g>
|
|
40
77
|
<rect id="mouth" x="38" y="54" width="24" height="10" rx="2" />
|
|
78
|
+
<!-- mechanic mode (settings): hard hat + screwdriver -->
|
|
79
|
+
<g id="robot">
|
|
80
|
+
<!-- yellow hard hat: pixel-art bitmap, built in pet.js (buildHat) -->
|
|
81
|
+
<g id="hardhat"></g>
|
|
82
|
+
<!-- screwdriver held at an angle to the side, tapping away -->
|
|
83
|
+
<g id="driver">
|
|
84
|
+
<rect class="dr-tip" x="90" y="39" width="6" height="4" />
|
|
85
|
+
<rect class="dr-shaft" x="91" y="43" width="4" height="19" />
|
|
86
|
+
<rect class="dr-hi" x="91" y="43" width="1.6" height="19" />
|
|
87
|
+
<rect class="dr-collar" x="87" y="61" width="12" height="5" rx="1" />
|
|
88
|
+
<rect class="dr-handle" x="84" y="66" width="18" height="24" rx="8" />
|
|
89
|
+
<rect class="dr-handle-hi" x="86.5" y="69" width="2.6" height="17" rx="1.3" />
|
|
90
|
+
<animateTransform attributeName="transform" type="rotate" values="-32 93 70; -26 93 70; -32 93 70" dur="1s" repeatCount="indefinite" />
|
|
91
|
+
</g>
|
|
92
|
+
</g>
|
|
41
93
|
</svg>
|
|
42
94
|
<svg id="zzz" viewBox="0 0 44 44" width="44" height="44">
|
|
43
95
|
<text x="2" y="34" class="z1">Z</text>
|
|
@@ -141,7 +193,7 @@
|
|
|
141
193
|
</div>
|
|
142
194
|
|
|
143
195
|
<label class="set-check"><input id="set-alerts" type="checkbox" /> Alerts</label>
|
|
144
|
-
<div class="set-hint set-hint-left">macOS
|
|
196
|
+
<div class="set-hint set-hint-left">A macOS notification when your session or weekly usage crosses each level.</div>
|
|
145
197
|
<label class="set-field">Alert thresholds (%)
|
|
146
198
|
<span class="set-two">
|
|
147
199
|
<span class="num">
|
|
@@ -159,6 +211,23 @@
|
|
|
159
211
|
</span>
|
|
160
212
|
</span>
|
|
161
213
|
</span>
|
|
214
|
+
<span class="set-two set-caps">
|
|
215
|
+
<span class="num-cap">heads-up</span>
|
|
216
|
+
<span class="num-cap">near limit</span>
|
|
217
|
+
</span>
|
|
218
|
+
</label>
|
|
219
|
+
<div class="set-hint set-hint-left">The pet catches fire past this session % (it's maxed out at 100%).</div>
|
|
220
|
+
<label class="set-field">Catch fire at (%)
|
|
221
|
+
<span class="set-two">
|
|
222
|
+
<span class="num">
|
|
223
|
+
<input id="set-fire" type="number" min="1" max="99" />
|
|
224
|
+
<span class="num-spin">
|
|
225
|
+
<button type="button" class="num-btn" data-for="set-fire" data-step="1">▲</button>
|
|
226
|
+
<button type="button" class="num-btn" data-for="set-fire" data-step="-1">▼</button>
|
|
227
|
+
</span>
|
|
228
|
+
</span>
|
|
229
|
+
<span class="num" aria-hidden="true"></span>
|
|
230
|
+
</span>
|
|
162
231
|
</label>
|
|
163
232
|
<div class="set-actions">
|
|
164
233
|
<button id="set-cancel">Cancel</button>
|
package/renderer/pet.js
CHANGED
|
@@ -40,6 +40,39 @@ const SPRITE = [
|
|
|
40
40
|
})
|
|
41
41
|
})()
|
|
42
42
|
|
|
43
|
+
// hard hat (mechanic mode): pixel-art bitmap so it matches the pet's blocky look
|
|
44
|
+
;(function buildHat() {
|
|
45
|
+
const g = el('hardhat')
|
|
46
|
+
if (!g) return
|
|
47
|
+
// L = highlight, Y = main, D = shadow rim, B = brim
|
|
48
|
+
const HAT = [
|
|
49
|
+
'.....YLLY.....',
|
|
50
|
+
'...YYYLLYYY...',
|
|
51
|
+
'..YYYYLLYYYY..',
|
|
52
|
+
'.YYYYYLLYYYYY.',
|
|
53
|
+
'.DDDDDDDDDDDD.',
|
|
54
|
+
'BBBBBBBBBBBBBB',
|
|
55
|
+
]
|
|
56
|
+
const COL = { L: '#ffe27a', Y: '#f5c518', D: '#d99a00', B: '#c98f14' }
|
|
57
|
+
const C = 8 // cell size in svg units
|
|
58
|
+
const cols = HAT[0].length
|
|
59
|
+
const ox = 50 - (cols * C) / 2 // centered on the head (center x = 50)
|
|
60
|
+
const oy = -24 // rises above the head, brim ends just over the eyes
|
|
61
|
+
HAT.forEach((row, r) => {
|
|
62
|
+
for (let c = 0; c < row.length; c++) {
|
|
63
|
+
const ch = row[c]
|
|
64
|
+
if (ch === '.') continue
|
|
65
|
+
const rect = document.createElementNS(SVGNS, 'rect')
|
|
66
|
+
rect.setAttribute('x', ox + c * C)
|
|
67
|
+
rect.setAttribute('y', oy + r * C)
|
|
68
|
+
rect.setAttribute('width', C)
|
|
69
|
+
rect.setAttribute('height', C)
|
|
70
|
+
rect.setAttribute('fill', COL[ch])
|
|
71
|
+
g.appendChild(rect)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
})()
|
|
75
|
+
|
|
43
76
|
// night scene backdrop (clouds, crescent moon, stars, dotted ground/sky)
|
|
44
77
|
;(function buildScene() {
|
|
45
78
|
const s = el('scene')
|
|
@@ -369,12 +402,13 @@ function render(d) {
|
|
|
369
402
|
const wkPct = liveOn ? realUsage.week.pct : 0
|
|
370
403
|
const wkReset = liveOn ? realUsage.week.resetMs : null
|
|
371
404
|
|
|
405
|
+
const fireAt = currentConfig.fireThreshold ?? 90
|
|
372
406
|
let st =
|
|
373
407
|
liveOn && sessPct >= 100
|
|
374
408
|
? 'tired'
|
|
375
409
|
: d.active
|
|
376
410
|
? 'working'
|
|
377
|
-
: liveOn && sessPct >=
|
|
411
|
+
: liveOn && sessPct >= fireAt
|
|
378
412
|
? 'stressed'
|
|
379
413
|
: d.sleeping
|
|
380
414
|
? 'sleeping'
|
|
@@ -567,12 +601,32 @@ el('acc-confirm').addEventListener('click', () => {
|
|
|
567
601
|
el('acc-logout').addEventListener('click', () => window.api.authLogout())
|
|
568
602
|
|
|
569
603
|
// settings panel
|
|
604
|
+
// snapshot of the editable fields, to tell whether there are unsaved changes
|
|
605
|
+
let settingsBaseline = null
|
|
606
|
+
function snapshotSettings() {
|
|
607
|
+
return JSON.stringify({
|
|
608
|
+
alerts: el('set-alerts').checked,
|
|
609
|
+
t1: el('set-t1').value,
|
|
610
|
+
t2: el('set-t2').value,
|
|
611
|
+
fire: el('set-fire').value,
|
|
612
|
+
})
|
|
613
|
+
}
|
|
614
|
+
function refreshSaveDirty() {
|
|
615
|
+
if (settingsBaseline == null) return
|
|
616
|
+
el('set-save').classList.toggle('dirty', snapshotSettings() !== settingsBaseline)
|
|
617
|
+
}
|
|
618
|
+
function clearSaveDirty() {
|
|
619
|
+
settingsBaseline = snapshotSettings()
|
|
620
|
+
el('set-save').classList.remove('dirty')
|
|
621
|
+
}
|
|
570
622
|
function populateSettings() {
|
|
571
623
|
const c = currentConfig || {}
|
|
572
624
|
el('set-alerts').checked = c.alerts !== false
|
|
573
625
|
const th = c.alertThresholds || [80, 95]
|
|
574
626
|
el('set-t1').value = th[0] != null ? th[0] : 80
|
|
575
627
|
el('set-t2').value = th[1] != null ? th[1] : 95
|
|
628
|
+
el('set-fire').value = c.fireThreshold != null ? c.fireThreshold : 90
|
|
629
|
+
clearSaveDirty() // fields now match the saved config
|
|
576
630
|
}
|
|
577
631
|
function openSettings() {
|
|
578
632
|
document.body.classList.remove('collapsed')
|
|
@@ -591,20 +645,30 @@ for (const b of document.querySelectorAll('.num-btn')) {
|
|
|
591
645
|
const max = Number(input.max) || 100
|
|
592
646
|
const next = (Number.parseInt(input.value, 10) || 0) + Number(b.dataset.step)
|
|
593
647
|
input.value = Math.min(max, Math.max(min, next))
|
|
648
|
+
refreshSaveDirty() // steppers change the value without an 'input' event
|
|
594
649
|
})
|
|
595
650
|
}
|
|
651
|
+
// light up Save whenever an editable field changes
|
|
652
|
+
for (const id of ['set-alerts', 'set-t1', 'set-t2', 'set-fire']) {
|
|
653
|
+
el(id).addEventListener('input', refreshSaveDirty)
|
|
654
|
+
el(id).addEventListener('change', refreshSaveDirty)
|
|
655
|
+
}
|
|
596
656
|
el('set-cancel').addEventListener('click', () => {
|
|
597
657
|
document.body.classList.remove('settings-open')
|
|
658
|
+
clearSaveDirty()
|
|
598
659
|
fitSize()
|
|
599
660
|
})
|
|
600
661
|
el('set-save').addEventListener('click', () => {
|
|
601
662
|
const num = (id) => parseFloat(el(id).value)
|
|
663
|
+
const fire = num('set-fire')
|
|
602
664
|
window.api.saveConfig({
|
|
603
665
|
alerts: el('set-alerts').checked,
|
|
604
666
|
alertThresholds: [num('set-t1'), num('set-t2')]
|
|
605
667
|
.filter((n) => n >= 1 && n <= 100)
|
|
606
668
|
.sort((a, b) => a - b),
|
|
669
|
+
fireThreshold: fire >= 1 && fire <= 99 ? fire : 90,
|
|
607
670
|
})
|
|
671
|
+
clearSaveDirty()
|
|
608
672
|
document.body.classList.remove('settings-open')
|
|
609
673
|
fitSize()
|
|
610
674
|
})
|
package/renderer/style.css
CHANGED
|
@@ -242,6 +242,16 @@ body.settings-open #settings {
|
|
|
242
242
|
display: flex;
|
|
243
243
|
gap: 6px;
|
|
244
244
|
}
|
|
245
|
+
/* small captions under each threshold box */
|
|
246
|
+
.set-caps {
|
|
247
|
+
margin-top: 3px;
|
|
248
|
+
}
|
|
249
|
+
.num-cap {
|
|
250
|
+
flex: 1;
|
|
251
|
+
font-size: 8.5px;
|
|
252
|
+
color: var(--muted);
|
|
253
|
+
padding-left: 2px;
|
|
254
|
+
}
|
|
245
255
|
/* custom number stepper */
|
|
246
256
|
.num {
|
|
247
257
|
position: relative;
|
|
@@ -324,12 +334,26 @@ body.settings-open #settings {
|
|
|
324
334
|
background: rgba(255, 255, 255, 0.2);
|
|
325
335
|
}
|
|
326
336
|
#set-save {
|
|
327
|
-
background:
|
|
337
|
+
background: rgba(217, 119, 87, 0.4); /* calm until there are unsaved changes */
|
|
328
338
|
color: #fff;
|
|
329
339
|
}
|
|
330
340
|
#set-save:hover {
|
|
331
341
|
background: #e2876a;
|
|
332
342
|
}
|
|
343
|
+
/* unsaved changes: light up and pulse so you don't forget to save */
|
|
344
|
+
#set-save.dirty {
|
|
345
|
+
background: var(--coral);
|
|
346
|
+
animation: save-pulse 1.6s ease-in-out infinite;
|
|
347
|
+
}
|
|
348
|
+
@keyframes save-pulse {
|
|
349
|
+
0%,
|
|
350
|
+
100% {
|
|
351
|
+
box-shadow: 0 0 0 0 rgba(217, 119, 87, 0);
|
|
352
|
+
}
|
|
353
|
+
50% {
|
|
354
|
+
box-shadow: 0 0 9px 2px rgba(217, 119, 87, 0.6);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
333
357
|
|
|
334
358
|
/* live tag */
|
|
335
359
|
.live-only {
|
|
@@ -667,6 +691,51 @@ body.collapsed #shadow {
|
|
|
667
691
|
transform-origin: center;
|
|
668
692
|
animation: blink 5.5s infinite;
|
|
669
693
|
}
|
|
694
|
+
/* mechanic mode (settings): hard hat + screwdriver on the pet, loose gears
|
|
695
|
+
turning behind it. Hidden otherwise. */
|
|
696
|
+
#robot {
|
|
697
|
+
opacity: 0;
|
|
698
|
+
pointer-events: none;
|
|
699
|
+
transition: opacity 0.2s ease;
|
|
700
|
+
}
|
|
701
|
+
/* hard hat colors live inline (built pixel-by-pixel in pet.js buildHat) */
|
|
702
|
+
.dr-handle {
|
|
703
|
+
fill: #c0392b;
|
|
704
|
+
}
|
|
705
|
+
.dr-handle-hi {
|
|
706
|
+
fill: #e0685a;
|
|
707
|
+
}
|
|
708
|
+
.dr-collar {
|
|
709
|
+
fill: #9a9a9a;
|
|
710
|
+
}
|
|
711
|
+
.dr-tip,
|
|
712
|
+
.dr-shaft {
|
|
713
|
+
fill: #d0d0d0;
|
|
714
|
+
}
|
|
715
|
+
.dr-hi {
|
|
716
|
+
fill: #f2f2f2;
|
|
717
|
+
}
|
|
718
|
+
#gears {
|
|
719
|
+
position: absolute;
|
|
720
|
+
inset: 0;
|
|
721
|
+
width: 100%;
|
|
722
|
+
height: 100%;
|
|
723
|
+
z-index: 1;
|
|
724
|
+
fill: #7a6a5c;
|
|
725
|
+
opacity: 0;
|
|
726
|
+
pointer-events: none;
|
|
727
|
+
transition: opacity 0.2s ease;
|
|
728
|
+
}
|
|
729
|
+
body.settings-open #robot,
|
|
730
|
+
body.settings-open #gears {
|
|
731
|
+
opacity: 1;
|
|
732
|
+
}
|
|
733
|
+
/* declutter the scene while configuring */
|
|
734
|
+
body.settings-open #zzz,
|
|
735
|
+
body.settings-open #flames,
|
|
736
|
+
body.settings-open #sweat {
|
|
737
|
+
display: none;
|
|
738
|
+
}
|
|
670
739
|
#mouth {
|
|
671
740
|
fill: var(--eye);
|
|
672
741
|
transform-box: fill-box;
|