clauddy 1.22.0 → 1.23.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/package.json +1 -1
- package/renderer/index.html +146 -196
- package/renderer/motion.css +531 -0
- package/renderer/pet.js +67 -68
- package/renderer/style.css +514 -684
package/renderer/pet.js
CHANGED
|
@@ -18,6 +18,17 @@ const SPRITE = [
|
|
|
18
18
|
const body = el('body')
|
|
19
19
|
const eyes = el('eyes')
|
|
20
20
|
const C = 10
|
|
21
|
+
// Each pixel is its own <rect>, so neighbours share an edge. Under the mood
|
|
22
|
+
// animations the sprite is scaled by fractions, that edge lands between device
|
|
23
|
+
// pixels, and the card shows through as a hairline grid — crispEdges cannot
|
|
24
|
+
// help, it rounds in local space, before the transform. So a cell is grown to
|
|
25
|
+
// overlap the neighbour it actually has: same fill, invisible seam, and the
|
|
26
|
+
// silhouette stays exact because edge cells are left alone.
|
|
27
|
+
const SEAM = 0.5
|
|
28
|
+
const filled = (r, c) => {
|
|
29
|
+
const ch = SPRITE[r]?.[c]
|
|
30
|
+
return ch !== undefined && ch !== '.'
|
|
31
|
+
}
|
|
21
32
|
SPRITE.forEach((row, r) => {
|
|
22
33
|
for (let c = 0; c < row.length; c++) {
|
|
23
34
|
const ch = row[c]
|
|
@@ -25,8 +36,8 @@ const SPRITE = [
|
|
|
25
36
|
const rect = document.createElementNS(SVGNS, 'rect')
|
|
26
37
|
rect.setAttribute('x', c * C)
|
|
27
38
|
rect.setAttribute('y', r * C)
|
|
28
|
-
rect.setAttribute('width', C)
|
|
29
|
-
rect.setAttribute('height', C)
|
|
39
|
+
rect.setAttribute('width', C + (filled(r, c + 1) ? SEAM : 0))
|
|
40
|
+
rect.setAttribute('height', C + (filled(r + 1, c) ? SEAM : 0))
|
|
30
41
|
body.appendChild(rect)
|
|
31
42
|
if (ch === 'o') {
|
|
32
43
|
const eye = document.createElementNS(SVGNS, 'rect')
|
|
@@ -40,39 +51,6 @@ const SPRITE = [
|
|
|
40
51
|
})
|
|
41
52
|
})()
|
|
42
53
|
|
|
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
|
-
|
|
76
54
|
// night scene backdrop (clouds, crescent moon, stars, dotted ground/sky)
|
|
77
55
|
;(function buildScene() {
|
|
78
56
|
const s = el('scene')
|
|
@@ -375,8 +353,6 @@ function renderHeat(days) {
|
|
|
375
353
|
// only comparable if every track starts and ends at the same x. So it is sized
|
|
376
354
|
// to the widest label actually present, clamped so a long path cannot squeeze
|
|
377
355
|
// the bars into stubs, and anything past the clamp is clipped with an ellipsis.
|
|
378
|
-
const NAME_MIN = 64
|
|
379
|
-
const BAR_MIN = 96 // room left for the track and the token count
|
|
380
356
|
|
|
381
357
|
function renderBars(boxId, list, limit) {
|
|
382
358
|
const box = el(boxId)
|
|
@@ -398,24 +374,9 @@ function renderBars(boxId, list, limit) {
|
|
|
398
374
|
box.innerHTML = '<div class="mrow" style="opacity:.5">no activity</div>'
|
|
399
375
|
return
|
|
400
376
|
}
|
|
401
|
-
fitNames(box)
|
|
402
377
|
}
|
|
403
378
|
|
|
404
379
|
// measure the labels unconstrained, then lock the column to the widest one
|
|
405
|
-
function fitNames(box) {
|
|
406
|
-
const names = [...box.querySelectorAll('.mname')]
|
|
407
|
-
// a collapsed section measures zero — it re-fits when it opens
|
|
408
|
-
if (!names.length || !box.offsetWidth) return
|
|
409
|
-
box.style.setProperty('--name-w', 'auto')
|
|
410
|
-
// offsetWidth, not getBoundingClientRect: the width we hand back is a layout px,
|
|
411
|
-
// so it has to be measured in layout px too. A CSS transform on an ancestor
|
|
412
|
-
// (the video stage scales the card) inflates the rect and squeezes the bars away.
|
|
413
|
-
let widest = 0
|
|
414
|
-
for (const n of names) widest = Math.max(widest, n.offsetWidth)
|
|
415
|
-
const room = box.offsetWidth - BAR_MIN
|
|
416
|
-
const w = Math.max(NAME_MIN, Math.min(widest + 2, room))
|
|
417
|
-
box.style.setProperty('--name-w', `${w}px`)
|
|
418
|
-
}
|
|
419
380
|
|
|
420
381
|
// per-model weekly limits (e.g. "Fable" on Max) — one meter each, in the same
|
|
421
382
|
// shape as the all-models one. The token count pairs the limit with the local
|
|
@@ -431,13 +392,21 @@ function renderScoped(list, byModel) {
|
|
|
431
392
|
const m = document.createElement('div')
|
|
432
393
|
m.className = 'meter'
|
|
433
394
|
m.innerHTML =
|
|
434
|
-
`<div class="meter-top"><span>weekly · ${esc(key)}</span><span>${Math.round(s.pct)}%</span></div>` +
|
|
435
|
-
`<div class="track"><div class="fill week${s.pct >= 80 ? ' high' : ''}" style="width:${s.pct}%"></div></div>` +
|
|
395
|
+
`<div class="meter-top"><span>weekly · ${esc(key)}</span><span class="${levelOf(s.pct)}">${Math.round(s.pct)}%</span></div>` +
|
|
396
|
+
`<div class="track"><div class="fill week${s.pct >= 80 ? ' high' : ''} ${levelOf(s.pct)}" style="width:${s.pct}%"></div></div>` +
|
|
436
397
|
`<div class="sub">${s.resetMs != null ? `resets in ${fmtResetIn(s.resetMs)} · ` : ''}${fmtTokens(tokens)} tokens</div>`
|
|
437
398
|
box.appendChild(m)
|
|
438
399
|
}
|
|
439
400
|
}
|
|
440
401
|
|
|
402
|
+
// One ramp for every meter: plain below 60, warm to 85, hot past it. The number
|
|
403
|
+
// and its bar always read the same level.
|
|
404
|
+
const levelOf = (pct) => (pct >= 85 ? 'hot' : pct >= 60 ? 'mid' : '')
|
|
405
|
+
function setLevel(node, pct) {
|
|
406
|
+
node.classList.toggle('mid', levelOf(pct) === 'mid')
|
|
407
|
+
node.classList.toggle('hot', levelOf(pct) === 'hot')
|
|
408
|
+
}
|
|
409
|
+
|
|
441
410
|
// by model (7 days)
|
|
442
411
|
function renderModels(list) {
|
|
443
412
|
renderBars('bymodel-list', list, 4)
|
|
@@ -576,6 +545,7 @@ function render(d) {
|
|
|
576
545
|
if (liveOn && prevPct != null && sessActive && prevPct - sessPct > 25) celebrate()
|
|
577
546
|
prevPct = sessPct
|
|
578
547
|
el('session-pct').textContent = `${Math.round(sessPct)}%`
|
|
548
|
+
setLevel(el('session-pct'), liveOn ? sessPct : 0)
|
|
579
549
|
const mini = el('mini-pct')
|
|
580
550
|
mini.textContent = liveOn ? `${Math.round(sessPct)}%` : '—'
|
|
581
551
|
mini.classList.toggle('high', liveOn && sessPct >= 80)
|
|
@@ -589,6 +559,7 @@ function render(d) {
|
|
|
589
559
|
const sf = el('session-fill')
|
|
590
560
|
sf.style.width = `${sessPct}%`
|
|
591
561
|
sf.classList.toggle('high', sessPct >= 80)
|
|
562
|
+
setLevel(sf, sessPct)
|
|
592
563
|
el('session-sub').textContent = sessActive
|
|
593
564
|
? `resets in ${fmtResetIn(sessReset)} · ${fmtTokens(d.session.tokens)} tokens`
|
|
594
565
|
: 'no active session'
|
|
@@ -606,9 +577,11 @@ function render(d) {
|
|
|
606
577
|
}
|
|
607
578
|
|
|
608
579
|
el('week-pct').textContent = `${Math.round(wkPct)}%`
|
|
580
|
+
setLevel(el('week-pct'), wkPct)
|
|
609
581
|
const wf = el('week-fill')
|
|
610
582
|
wf.style.width = `${wkPct}%`
|
|
611
583
|
wf.classList.toggle('high', wkPct >= 80)
|
|
584
|
+
setLevel(wf, wkPct)
|
|
612
585
|
el('week-sub').textContent =
|
|
613
586
|
wkReset != null
|
|
614
587
|
? `resets in ${fmtResetIn(wkReset)} · ${fmtTokens(d.week.tokens)} tokens`
|
|
@@ -655,7 +628,7 @@ function fitSize() {
|
|
|
655
628
|
}
|
|
656
629
|
|
|
657
630
|
// widget scale, applied as a CSS zoom (not transform) so offsetWidth/Height
|
|
658
|
-
// keep reflecting it —
|
|
631
|
+
// keep reflecting it — offsetHeight is a layout px, so it must be measured as one
|
|
659
632
|
function applyZoom(z) {
|
|
660
633
|
document.body.style.zoom = (z || 100) / 100
|
|
661
634
|
}
|
|
@@ -715,8 +688,11 @@ function paintChip() {
|
|
|
715
688
|
const a = lastAccounts
|
|
716
689
|
const active = (a?.accounts || []).find((x) => x.id === a?.active)
|
|
717
690
|
const email = p?.email || (active?.connected ? active.label : null)
|
|
718
|
-
// the settings
|
|
719
|
-
|
|
691
|
+
// the settings block says who is connected, not just that someone is: the
|
|
692
|
+
// avatar carries the initial, the name line the email, the sub line the plan
|
|
693
|
+
el('acc-ok').textContent = email || 'Connected'
|
|
694
|
+
el('acc-sub').textContent = p?.plan || 'your real usage is live'
|
|
695
|
+
el('acc-avatar').textContent = email ? email[0].toUpperCase() : '●'
|
|
720
696
|
const chip = el('account-chip')
|
|
721
697
|
const mini = el('mini-acct')
|
|
722
698
|
if (!email) {
|
|
@@ -944,11 +920,21 @@ function toggleSection(id, force) {
|
|
|
944
920
|
sec.classList.toggle('folded', folded)
|
|
945
921
|
const head = sec.querySelector('.sec-head')
|
|
946
922
|
if (head) head.setAttribute('aria-expanded', String(!folded))
|
|
947
|
-
// the column width could not be measured while hidden
|
|
948
|
-
if (!folded) fitNames(sec.querySelector('.sec-body'))
|
|
949
923
|
// the card just changed height, and the next usage poll is seconds away —
|
|
950
|
-
// without this the window keeps its old size and clips the content
|
|
924
|
+
// without this the window keeps its old size and clips the content. The body
|
|
925
|
+
// slides now rather than snapping, so size it again once that settles.
|
|
951
926
|
fitSize()
|
|
927
|
+
// ...and again when the slide lands. The listener has to check what finished:
|
|
928
|
+
// the bar widths inside the section transition too, and those events bubble up
|
|
929
|
+
// here — resizing on one of them catches the card mid-slide.
|
|
930
|
+
const body = sec.querySelector('.sec-body')
|
|
931
|
+
if (!body) return
|
|
932
|
+
const settle = (e) => {
|
|
933
|
+
if (e.target !== body || e.propertyName !== 'grid-template-rows') return
|
|
934
|
+
body.removeEventListener('transitionend', settle)
|
|
935
|
+
fitSize()
|
|
936
|
+
}
|
|
937
|
+
body.addEventListener('transitionend', settle)
|
|
952
938
|
}
|
|
953
939
|
|
|
954
940
|
for (const head of document.querySelectorAll('.sec-head')) {
|
|
@@ -1111,7 +1097,26 @@ function openSettings() {
|
|
|
1111
1097
|
document.body.classList.add('settings-open')
|
|
1112
1098
|
fitSize()
|
|
1113
1099
|
}
|
|
1114
|
-
|
|
1100
|
+
|
|
1101
|
+
// Leaving settings plays the panel out to the right while home slides back in.
|
|
1102
|
+
// The panel is lifted out of the flow for those few frames (see .settings-closing
|
|
1103
|
+
// in the stylesheet), so the card can already be measured at its home height.
|
|
1104
|
+
let closingTimer = null
|
|
1105
|
+
function closeSettings() {
|
|
1106
|
+
if (!document.body.classList.contains('settings-open')) return
|
|
1107
|
+
abandonLogin()
|
|
1108
|
+
document.body.classList.remove('settings-open')
|
|
1109
|
+
document.body.classList.add('settings-closing')
|
|
1110
|
+
clearTimeout(closingTimer)
|
|
1111
|
+
closingTimer = setTimeout(() => document.body.classList.remove('settings-closing'), 600)
|
|
1112
|
+
clearSaveDirty()
|
|
1113
|
+
applyZoom(currentConfig?.zoom != null ? currentConfig.zoom : 100) // undo the preview
|
|
1114
|
+
fitSize()
|
|
1115
|
+
}
|
|
1116
|
+
el('gear').addEventListener('click', () => {
|
|
1117
|
+
if (document.body.classList.contains('settings-open')) closeSettings()
|
|
1118
|
+
else openSettings()
|
|
1119
|
+
})
|
|
1115
1120
|
// the "connect" placeholder jumps straight to settings
|
|
1116
1121
|
el('limits-connect').addEventListener('click', openSettings)
|
|
1117
1122
|
// custom number steppers (▲ / ▼)
|
|
@@ -1140,13 +1145,7 @@ for (const b of document.querySelectorAll('#set-mode .seg-btn')) {
|
|
|
1140
1145
|
refreshSaveDirty()
|
|
1141
1146
|
})
|
|
1142
1147
|
}
|
|
1143
|
-
el('set-cancel').addEventListener('click',
|
|
1144
|
-
abandonLogin()
|
|
1145
|
-
document.body.classList.remove('settings-open')
|
|
1146
|
-
clearSaveDirty()
|
|
1147
|
-
applyZoom(currentConfig?.zoom != null ? currentConfig.zoom : 100) // undo the preview
|
|
1148
|
-
fitSize()
|
|
1149
|
-
})
|
|
1148
|
+
el('set-cancel').addEventListener('click', closeSettings)
|
|
1150
1149
|
el('set-save').addEventListener('click', () => {
|
|
1151
1150
|
abandonLogin() // leaving the panel gives up on a login waiting for its code
|
|
1152
1151
|
const num = (id) => parseFloat(el(id).value)
|