bloby-bot 0.51.0 → 0.51.3

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.
@@ -1 +1 @@
1
- import{c as e,r as t,t as n}from"./jsx-runtime-C0W9Wf2W.js";import{n as r,r as i,t as a}from"./bloby-B2Y96IF2.js";var o=e(t(),1),s=n(),c=({code:e,language:t,raw:n,className:c,startLine:l,lineNumbers:u,...d})=>{let{shikiTheme:f}=(0,o.useContext)(i),p=r(),[m,h]=(0,o.useState)(n);return(0,o.useEffect)(()=>{if(!p){h(n);return}let r=p.highlight({code:e,language:t,themes:f},e=>{h(e)});r&&h(r)},[e,t,f,p,n]),(0,s.jsx)(a,{className:c,language:t,lineNumbers:u,result:m,startLine:l,...d})};export{c as HighlightedCodeBlockBody};
1
+ import{c as e,r as t,t as n}from"./jsx-runtime-C0W9Wf2W.js";import{n as r,r as i,t as a}from"./bloby-vi0Xitb-.js";var o=e(t(),1),s=n(),c=({code:e,language:t,raw:n,className:c,startLine:l,lineNumbers:u,...d})=>{let{shikiTheme:f}=(0,o.useContext)(i),p=r(),[m,h]=(0,o.useState)(n);return(0,o.useEffect)(()=>{if(!p){h(n);return}let r=p.highlight({code:e,language:t,themes:f},e=>{h(e)});r&&h(r)},[e,t,f,p,n]),(0,s.jsx)(a,{className:c,language:t,lineNumbers:u,result:m,startLine:l,...d})};export{c as HighlightedCodeBlockBody};
@@ -0,0 +1 @@
1
+ import{i as e}from"./bloby-vi0Xitb-.js";export{e as Mermaid};
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content" />
6
6
  <title>Bloby Chat</title>
7
- <script type="module" crossorigin src="/bloby/assets/bloby-B2Y96IF2.js"></script>
7
+ <script type="module" crossorigin src="/bloby/assets/bloby-vi0Xitb-.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="/bloby/assets/jsx-runtime-C0W9Wf2W.js">
9
9
  <link rel="modulepreload" crossorigin href="/bloby/assets/globals-DNO3ilRx.js">
10
10
  <link rel="stylesheet" crossorigin href="/bloby/assets/globals-D60b-8LY.css">
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "bloby-bot",
3
- "version": "0.51.0",
3
+ "version": "0.51.3",
4
4
  "releaseNotes": [
5
- "1. Something great..",
6
- "2. ",
7
- "3. ",
5
+ "1. New Morphy animation system: config-driven sprites loaded from /morphy/*.json",
6
+ "2. Swapped teleporting (splash) and headphones (bubble + chat) to the new format",
7
+ "3. SW cache bumped to v16 to clear old assets",
8
8
  "4. "
9
9
  ],
10
10
  "description": "Self-hosted, self-evolving AI agent with its own dashboard.",
@@ -32,7 +32,6 @@ function BlobyApp() {
32
32
 
33
33
  // Recording state (for header animation)
34
34
  const [chatRecording, setChatRecording] = useState(false);
35
- const [showHpAnim, setShowHpAnim] = useState(false);
36
35
 
37
36
  // Push notifications
38
37
  const [pushState, setPushState] = useState<'loading' | 'unsupported' | 'denied' | 'subscribed' | 'unsubscribed'>('loading');
@@ -361,15 +360,7 @@ function BlobyApp() {
361
360
 
362
361
  {/* Mascot / headphones animation */}
363
362
  <div className="relative ml-1.5 shrink-0">
364
- {showHpAnim ? (
365
- <HeadphonesAnimation
366
- recording={chatRecording}
367
- height={36}
368
- onDone={() => setShowHpAnim(false)}
369
- />
370
- ) : (
371
- <img src="/bloby.png" alt={botName} className="h-9 w-auto" />
372
- )}
363
+ <HeadphonesAnimation recording={chatRecording} height={36} />
373
364
  </div>
374
365
 
375
366
  {/* Name + status line */}
@@ -529,10 +520,7 @@ function BlobyApp() {
529
520
  });
530
521
  }}
531
522
  onAudioReady={addPendingAudio}
532
- onRecordingChange={(r) => {
533
- setChatRecording(r);
534
- if (r) setShowHpAnim(true);
535
- }}
523
+ onRecordingChange={setChatRecording}
536
524
  />
537
525
  </div>
538
526
 
@@ -1,26 +1,40 @@
1
1
  import { useEffect, useRef, useCallback } from 'react';
2
2
 
3
- // v2 sprite config
4
- const HP_COLS = 16;
5
- const HP_FRAME_W = 126;
6
- const HP_FRAME_H = 84;
7
- const HP_IDLE_FRAME = 10;
8
- const HP_ACTIVATE_START = 11;
9
- const HP_ACTIVATE_END = 40;
10
- const HP_RECORD_START = 40;
11
- const HP_RECORD_END = 85;
12
- const HP_BASE_FPS = 24;
13
- const HP_FAST_FPS = 48;
3
+ // Config-driven. Loaded from /morphy/headphones.json.
4
+ // Clips map to states:
5
+ // idle (hold) → 'idle'
6
+ // enter (forward) → 'activating'
7
+ // active (pingpong)→ 'recording'
8
+ // exit (forward) → 'deactivating'
9
+ const HP_CONFIG_URL = '/morphy/headphones.json';
10
+ const HP_ASSETS_DIR = '/morphy/';
11
+
12
+ interface HpConfig {
13
+ spritesheet: string;
14
+ frame: { w: number; h: number };
15
+ grid: { cols: number; rows: number };
16
+ fps: number;
17
+ clips: {
18
+ idle: { from: number; to: number; mode: string; fps?: number };
19
+ enter: { from: number; to: number; mode: string; fps?: number };
20
+ active: { from: number; to: number; mode: string; fps?: number };
21
+ exit: { from: number; to: number; mode: string; fps?: number };
22
+ };
23
+ }
14
24
 
15
25
  let cachedSprite: HTMLImageElement | null = null;
16
- function getSprite(): Promise<HTMLImageElement> {
17
- if (cachedSprite) return Promise.resolve(cachedSprite);
18
- return new Promise((resolve, reject) => {
19
- const img = new Image();
20
- img.onload = () => { cachedSprite = img; resolve(img); };
21
- img.onerror = reject;
22
- img.src = '/headphones_spritesheet.webp';
23
- });
26
+ let cachedConfig: HpConfig | null = null;
27
+
28
+ function loadConfigAndSprite(): Promise<{ img: HTMLImageElement; cfg: HpConfig }> {
29
+ if (cachedSprite && cachedConfig) return Promise.resolve({ img: cachedSprite, cfg: cachedConfig });
30
+ return fetch(HP_CONFIG_URL)
31
+ .then((r) => { if (!r.ok) throw new Error('hp config ' + r.status); return r.json() as Promise<HpConfig>; })
32
+ .then((cfg) => new Promise<{ img: HTMLImageElement; cfg: HpConfig }>((resolve, reject) => {
33
+ const img = new Image();
34
+ img.onload = () => { cachedSprite = img; cachedConfig = cfg; resolve({ img, cfg }); };
35
+ img.onerror = reject;
36
+ img.src = HP_ASSETS_DIR + cfg.spritesheet;
37
+ }));
24
38
  }
25
39
 
26
40
  interface Props {
@@ -34,8 +48,9 @@ type HpState = 'idle' | 'activating' | 'activating_then_deactivate' | 'recording
34
48
  export default function HeadphonesAnimation({ recording, height = 36, onDone }: Props) {
35
49
  const canvasRef = useRef<HTMLCanvasElement>(null);
36
50
  const spriteRef = useRef<HTMLImageElement | null>(cachedSprite);
51
+ const configRef = useRef<HpConfig | null>(cachedConfig);
37
52
  const stateRef = useRef<HpState>('idle');
38
- const frameRef = useRef(HP_ACTIVATE_START);
53
+ const frameRef = useRef(0);
39
54
  const pingPongRef = useRef(1);
40
55
  const lastRef = useRef(0);
41
56
  const rafRef = useRef(0);
@@ -43,16 +58,28 @@ export default function HeadphonesAnimation({ recording, height = 36, onDone }:
43
58
  const onDoneRef = useRef(onDone);
44
59
  onDoneRef.current = onDone;
45
60
 
61
+ // Aspect ratio from config (defaults to 1:1 — matches new sprite).
62
+ const cfg = configRef.current;
63
+ const aspectW = cfg ? cfg.frame.w : 180;
64
+ const aspectH = cfg ? cfg.frame.h : 180;
46
65
  const displayH = height;
47
- const displayW = Math.round(displayH * (HP_FRAME_W / HP_FRAME_H));
66
+ const displayW = Math.round(displayH * (aspectW / aspectH));
48
67
 
49
- useEffect(() => { getSprite().then((img) => { spriteRef.current = img; }); }, []);
68
+ useEffect(() => {
69
+ loadConfigAndSprite().then(({ img, cfg }) => {
70
+ spriteRef.current = img;
71
+ configRef.current = cfg;
72
+ if (stateRef.current === 'idle') frameRef.current = cfg.clips.idle.from;
73
+ });
74
+ }, []);
50
75
 
51
76
  // React to recording prop
52
77
  useEffect(() => {
78
+ const cfg = configRef.current;
79
+ if (!cfg) return;
53
80
  if (recording && !prevRecording.current) {
54
81
  stateRef.current = 'activating';
55
- frameRef.current = HP_ACTIVATE_START;
82
+ frameRef.current = cfg.clips.enter.from;
56
83
  pingPongRef.current = 1;
57
84
  lastRef.current = performance.now();
58
85
  } else if (!recording && prevRecording.current) {
@@ -60,7 +87,7 @@ export default function HeadphonesAnimation({ recording, height = 36, onDone }:
60
87
  stateRef.current = 'activating_then_deactivate';
61
88
  } else if (stateRef.current === 'recording') {
62
89
  stateRef.current = 'deactivating';
63
- frameRef.current = HP_ACTIVATE_END;
90
+ frameRef.current = cfg.clips.exit.from;
64
91
  lastRef.current = performance.now();
65
92
  }
66
93
  }
@@ -70,17 +97,40 @@ export default function HeadphonesAnimation({ recording, height = 36, onDone }:
70
97
  const tick = useCallback((now: number) => {
71
98
  const canvas = canvasRef.current;
72
99
  const sprite = spriteRef.current;
100
+ const cfg = configRef.current;
73
101
  if (!canvas) { rafRef.current = requestAnimationFrame(tick); return; }
74
102
  const ctx = canvas.getContext('2d')!;
75
103
  const state = stateRef.current;
76
104
 
77
- if (state === 'idle' || !sprite) {
105
+ if (!sprite || !cfg) {
106
+ ctx.clearRect(0, 0, displayW, displayH);
107
+ rafRef.current = requestAnimationFrame(tick);
108
+ return;
109
+ }
110
+
111
+ if (state === 'idle') {
112
+ // Hold the configured idle frame so the chat header shows a real visual at rest.
113
+ const cols = cfg.grid.cols;
114
+ const fw = cfg.frame.w;
115
+ const fh = cfg.frame.h;
116
+ const idleFrame = cfg.clips.idle.from;
117
+ const col = idleFrame % cols;
118
+ const row = Math.floor(idleFrame / cols);
78
119
  ctx.clearRect(0, 0, displayW, displayH);
120
+ ctx.imageSmoothingEnabled = true;
121
+ ctx.imageSmoothingQuality = 'high';
122
+ ctx.drawImage(sprite, col * fw, row * fh, fw, fh, 0, 0, displayW, displayH);
79
123
  rafRef.current = requestAnimationFrame(tick);
80
124
  return;
81
125
  }
82
126
 
83
- const fps = (state === 'activating' || state === 'activating_then_deactivate' || state === 'deactivating') ? HP_FAST_FPS : HP_BASE_FPS;
127
+ const baseFps = cfg.fps;
128
+ const fps =
129
+ state === 'activating' || state === 'activating_then_deactivate'
130
+ ? (cfg.clips.enter.fps ?? baseFps)
131
+ : state === 'deactivating'
132
+ ? (cfg.clips.exit.fps ?? baseFps)
133
+ : (cfg.clips.active.fps ?? baseFps);
84
134
  const frameMs = 1000 / fps;
85
135
  const delta = now - lastRef.current;
86
136
  if (delta >= frameMs) {
@@ -88,24 +138,25 @@ export default function HeadphonesAnimation({ recording, height = 36, onDone }:
88
138
 
89
139
  if (state === 'activating' || state === 'activating_then_deactivate') {
90
140
  frameRef.current++;
91
- if (frameRef.current > HP_ACTIVATE_END) {
141
+ if (frameRef.current > cfg.clips.enter.to) {
92
142
  if (state === 'activating_then_deactivate') {
93
143
  stateRef.current = 'deactivating';
94
- frameRef.current = HP_ACTIVATE_END;
144
+ frameRef.current = cfg.clips.exit.from;
95
145
  } else {
96
146
  stateRef.current = 'recording';
97
- frameRef.current = HP_RECORD_START;
147
+ frameRef.current = cfg.clips.active.from;
98
148
  pingPongRef.current = 1;
99
149
  }
100
150
  }
101
151
  } else if (state === 'recording') {
102
152
  frameRef.current += pingPongRef.current;
103
- if (frameRef.current >= HP_RECORD_END) { frameRef.current = HP_RECORD_END; pingPongRef.current = -1; }
104
- else if (frameRef.current <= HP_RECORD_START) { frameRef.current = HP_RECORD_START; pingPongRef.current = 1; }
153
+ if (frameRef.current >= cfg.clips.active.to) { frameRef.current = cfg.clips.active.to; pingPongRef.current = -1; }
154
+ else if (frameRef.current <= cfg.clips.active.from) { frameRef.current = cfg.clips.active.from; pingPongRef.current = 1; }
105
155
  } else if (state === 'deactivating') {
106
- frameRef.current--;
107
- if (frameRef.current < HP_ACTIVATE_START) {
156
+ frameRef.current++;
157
+ if (frameRef.current > cfg.clips.exit.to) {
108
158
  stateRef.current = 'idle';
159
+ frameRef.current = cfg.clips.idle.from;
109
160
  ctx.clearRect(0, 0, displayW, displayH);
110
161
  onDoneRef.current?.();
111
162
  rafRef.current = requestAnimationFrame(tick);
@@ -114,12 +165,15 @@ export default function HeadphonesAnimation({ recording, height = 36, onDone }:
114
165
  }
115
166
  }
116
167
 
117
- const col = frameRef.current % HP_COLS;
118
- const row = Math.floor(frameRef.current / HP_COLS);
168
+ const cols = cfg.grid.cols;
169
+ const fw = cfg.frame.w;
170
+ const fh = cfg.frame.h;
171
+ const col = frameRef.current % cols;
172
+ const row = Math.floor(frameRef.current / cols);
119
173
  ctx.clearRect(0, 0, displayW, displayH);
120
174
  ctx.imageSmoothingEnabled = true;
121
175
  ctx.imageSmoothingQuality = 'high';
122
- ctx.drawImage(sprite, col * HP_FRAME_W, row * HP_FRAME_H, HP_FRAME_W, HP_FRAME_H, 0, 0, displayW, displayH);
176
+ ctx.drawImage(sprite, col * fw, row * fh, fw, fh, 0, 0, displayW, displayH);
123
177
 
124
178
  rafRef.current = requestAnimationFrame(tick);
125
179
  }, [displayW, displayH]);
@@ -90,7 +90,7 @@ const SW_JS = `// Service worker — app-shell caching + push notifications
90
90
  // JS/CSS modules → stale-while-revalidate
91
91
  // API, WebSocket, Vite internals → network-only (no cache)
92
92
 
93
- var CACHE = 'bloby-v15';
93
+ var CACHE = 'bloby-v16';
94
94
  var HASHED_RE = new RegExp('/assets/.+-[a-zA-Z0-9]{6,}[.](js|css)$');
95
95
 
96
96
  // Precache the HTML shell on install so the cache is never empty.
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "headphones",
3
+ "spritesheet": "headphones.png",
4
+ "frame": {
5
+ "w": 174,
6
+ "h": 180
7
+ },
8
+ "grid": {
9
+ "cols": 16,
10
+ "rows": 8
11
+ },
12
+ "totalFrames": 121,
13
+ "fps": 24,
14
+ "clips": {
15
+ "idle": {
16
+ "from": 15,
17
+ "to": 15,
18
+ "mode": "hold",
19
+ "fps": 38
20
+ },
21
+ "enter": {
22
+ "from": 16,
23
+ "to": 25,
24
+ "mode": "forward",
25
+ "fps": 48,
26
+ "next": "active"
27
+ },
28
+ "active": {
29
+ "from": 26,
30
+ "to": 94,
31
+ "mode": "pingpong"
32
+ },
33
+ "exit": {
34
+ "from": 96,
35
+ "to": 105,
36
+ "mode": "forward",
37
+ "next": "idle"
38
+ }
39
+ }
40
+ }
@@ -77,24 +77,31 @@
77
77
  document.body.appendChild(badgeEl);
78
78
 
79
79
  // ══════════════════════════════════════════════════════════════════
80
- // ── Headphones Sprite Sheet Config (v2 bubble + recording) ─────
80
+ // ── Headphones Sprite Sheet Config (loaded from JSON) ────────────
81
+ // Clips map to hpStates:
82
+ // idle (hold) → hpState 'idle'
83
+ // enter (forward) → hpState 'activating'
84
+ // active (pingpong)→ hpState 'recording'
85
+ // exit (forward) → hpState 'deactivating'
81
86
  // ══════════════════════════════════════════════════════════════════
82
87
 
88
+ var HP_CONFIG_URL = '/morphy/headphones.json';
89
+
90
+ // Defaults mirror the shipped JSON so animation works even if fetch is cached.
83
91
  var HP_COLS = 16;
84
- var HP_FRAME_W = 126;
85
- var HP_FRAME_H = 84;
86
- // Bubble display: fill 60px width exactly
92
+ var HP_FRAME_W = 180;
93
+ var HP_FRAME_H = 180;
87
94
  var HP_BUBBLE_H = 40;
88
- var HP_BUBBLE_W = HP_BUBBLE_H * (HP_FRAME_W / HP_FRAME_H); // 60
95
+ var HP_BUBBLE_W = HP_BUBBLE_H * (HP_FRAME_W / HP_FRAME_H);
89
96
 
90
- // v2 frame ranges
91
- var HP_IDLE_FRAME = 10;
92
- var HP_ACTIVATE_START = 11, HP_ACTIVATE_END = 40;
93
- var HP_RECORD_START = 40, HP_RECORD_END = 85;
97
+ var HP_IDLE_FRAME = 15;
98
+ var HP_ACTIVATE_START = 16, HP_ACTIVATE_END = 25;
99
+ var HP_RECORD_START = 26, HP_RECORD_END = 94;
100
+ var HP_EXIT_START = 96, HP_EXIT_END = 105;
94
101
 
95
- // v2 FPS: 2x speed for activate/deactivate, normal for recording
96
- var HP_BASE_FPS = 24;
97
- var HP_FAST_FPS = 48;
102
+ var HP_BASE_FPS = 24; // active / record default
103
+ var HP_ACTIVATE_FPS = 48; // enter clip fps
104
+ var HP_EXIT_FPS = 24; // exit clip fps
98
105
 
99
106
  // Long-press threshold
100
107
  var HP_HOLD_MS = 500;
@@ -229,13 +236,33 @@
229
236
  .catch(function () { if (onFail) onFail(); });
230
237
  }
231
238
 
232
- // ── Load headphones sprite sheet ──
239
+ // ── Load headphones sprite sheet (config + image) ──
240
+ function applyHpConfig(cfg) {
241
+ HP_COLS = cfg.grid.cols;
242
+ HP_FRAME_W = cfg.frame.w;
243
+ HP_FRAME_H = cfg.frame.h;
244
+ HP_BUBBLE_W = HP_BUBBLE_H * (HP_FRAME_W / HP_FRAME_H);
245
+ HP_IDLE_FRAME = cfg.clips.idle.from;
246
+ HP_ACTIVATE_START = cfg.clips.enter.from; HP_ACTIVATE_END = cfg.clips.enter.to;
247
+ HP_RECORD_START = cfg.clips.active.from; HP_RECORD_END = cfg.clips.active.to;
248
+ HP_EXIT_START = cfg.clips.exit.from; HP_EXIT_END = cfg.clips.exit.to;
249
+ HP_BASE_FPS = (cfg.clips.active.fps != null ? cfg.clips.active.fps : cfg.fps);
250
+ HP_ACTIVATE_FPS = (cfg.clips.enter.fps != null ? cfg.clips.enter.fps : cfg.fps);
251
+ HP_EXIT_FPS = (cfg.clips.exit.fps != null ? cfg.clips.exit.fps : cfg.fps);
252
+ }
253
+
233
254
  function loadHpSprite(cb) {
234
255
  if (hpSpriteSheet) { if (cb) cb(); return; }
235
- var img = new Image();
236
- img.onload = function () { hpSpriteSheet = img; if (cb) cb(); };
237
- img.onerror = function () { console.error('[widget] headphones sprite failed'); };
238
- img.src = '/headphones_spritesheet.webp';
256
+ fetch(HP_CONFIG_URL)
257
+ .then(function (r) { if (!r.ok) throw new Error('hp config ' + r.status); return r.json(); })
258
+ .then(function (cfg) {
259
+ applyHpConfig(cfg);
260
+ var img = new Image();
261
+ img.onload = function () { hpSpriteSheet = img; hpFrame = HP_IDLE_FRAME; if (cb) cb(); };
262
+ img.onerror = function () { console.error('[widget] headphones sprite failed'); };
263
+ img.src = BLOB_ASSETS_DIR + cfg.spritesheet;
264
+ })
265
+ .catch(function (err) { console.error('[widget] headphones config failed', err); });
239
266
  }
240
267
 
241
268
  // ── Trigger blob move (splash) ──
@@ -261,7 +288,8 @@
261
288
  // ══════════════════════════════════════════════════════════════════
262
289
 
263
290
  function hpFps() {
264
- if (hpState === 'activating' || hpState === 'activating_then_deactivate' || hpState === 'deactivating') return HP_FAST_FPS;
291
+ if (hpState === 'activating' || hpState === 'activating_then_deactivate') return HP_ACTIVATE_FPS;
292
+ if (hpState === 'deactivating') return HP_EXIT_FPS;
265
293
  return HP_BASE_FPS;
266
294
  }
267
295
 
@@ -277,9 +305,8 @@
277
305
  hpFrame++;
278
306
  if (hpFrame > HP_ACTIVATE_END) {
279
307
  if (hpState === 'activating_then_deactivate') {
280
- // Reverse back to idle
281
308
  hpState = 'deactivating';
282
- hpFrame = HP_ACTIVATE_END;
309
+ hpFrame = HP_EXIT_START;
283
310
  } else {
284
311
  hpState = 'recording';
285
312
  hpFrame = HP_RECORD_START;
@@ -291,9 +318,8 @@
291
318
  if (hpFrame >= HP_RECORD_END) { hpFrame = HP_RECORD_END; hpPingPong = -1; }
292
319
  else if (hpFrame <= HP_RECORD_START) { hpFrame = HP_RECORD_START; hpPingPong = 1; }
293
320
  } else if (hpState === 'deactivating') {
294
- // v2: reverse 40→11
295
- hpFrame--;
296
- if (hpFrame < HP_ACTIVATE_START) {
321
+ hpFrame++;
322
+ if (hpFrame > HP_EXIT_END) {
297
323
  hpState = 'idle';
298
324
  hpFrame = HP_IDLE_FRAME;
299
325
  }
@@ -572,12 +598,11 @@
572
598
  canvas.style.transition = 'transform 0.2s ease';
573
599
  canvas.style.transform = '';
574
600
 
575
- // v2 deactivation: reverse 40→11 (activate played backwards)
576
601
  if (hpState === 'activating') {
577
602
  hpState = 'activating_then_deactivate';
578
603
  } else if (hpState === 'recording') {
579
604
  hpState = 'deactivating';
580
- hpFrame = HP_ACTIVATE_END;
605
+ hpFrame = HP_EXIT_START;
581
606
  hpLastFrameTime = performance.now();
582
607
  }
583
608
  }
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "headphones",
3
+ "spritesheet": "headphones.png",
4
+ "frame": {
5
+ "w": 174,
6
+ "h": 180
7
+ },
8
+ "grid": {
9
+ "cols": 16,
10
+ "rows": 8
11
+ },
12
+ "totalFrames": 121,
13
+ "fps": 24,
14
+ "clips": {
15
+ "idle": {
16
+ "from": 15,
17
+ "to": 15,
18
+ "mode": "hold",
19
+ "fps": 38
20
+ },
21
+ "enter": {
22
+ "from": 16,
23
+ "to": 25,
24
+ "mode": "forward",
25
+ "fps": 48,
26
+ "next": "active"
27
+ },
28
+ "active": {
29
+ "from": 26,
30
+ "to": 94,
31
+ "mode": "pingpong"
32
+ },
33
+ "exit": {
34
+ "from": 96,
35
+ "to": 105,
36
+ "mode": "forward",
37
+ "next": "idle"
38
+ }
39
+ }
40
+ }
@@ -6,7 +6,7 @@
6
6
  // JS/CSS modules → stale-while-revalidate
7
7
  // API, WebSocket, Vite internals → network-only (no cache)
8
8
 
9
- const CACHE = 'bloby-v6';
9
+ const CACHE = 'bloby-v7';
10
10
 
11
11
  // Precache the HTML shell on install so the cache is never empty.
12
12
  // Without this, the first navigation isn't intercepted (SW wasn't
@@ -1 +0,0 @@
1
- import{i as e}from"./bloby-B2Y96IF2.js";export{e as Mermaid};