fluxy-bot 0.10.16 → 0.10.18

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.
@@ -88,6 +88,15 @@ export function registerUpdateCommand(program: Command) {
88
88
  fs.cpSync(wsSrc, path.join(DATA_DIR, 'workspace'), { recursive: true });
89
89
  }
90
90
 
91
+ // Always update index.html — contains splash screen, SW registration,
92
+ // and meta tags that ship with each version. Safe to overwrite since
93
+ // users don't edit this file (their code lives in src/ and backend/).
94
+ const indexSrc = path.join(extracted, 'workspace', 'client', 'index.html');
95
+ const indexDst = path.join(DATA_DIR, 'workspace', 'client', 'index.html');
96
+ if (fs.existsSync(indexSrc)) {
97
+ fs.cpSync(indexSrc, indexDst, { force: true });
98
+ }
99
+
91
100
  for (const file of ['package.json', 'vite.config.ts', 'vite.fluxy.config.ts', 'tsconfig.json', 'postcss.config.js', 'components.json']) {
92
101
  const src = path.join(extracted, file);
93
102
  if (fs.existsSync(src)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluxy-bot",
3
- "version": "0.10.16",
3
+ "version": "0.10.18",
4
4
  "releaseNotes": [
5
5
  "Adding a way for users to claim their fluxies on the fluxy.bot dashboard",
6
6
  "2. ",
@@ -6,11 +6,9 @@
6
6
  // ── Styles ──
7
7
  var style = document.createElement('style');
8
8
  style.textContent = [
9
- '#fluxy-widget-bubble{position:fixed;bottom:24px;right:24px;z-index:99998;cursor:pointer;width:60px;height:60px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:transform .15s ease;-webkit-tap-highlight-color:transparent}',
9
+ '#fluxy-widget-bubble{transition:transform .15s ease;-webkit-tap-highlight-color:transparent}',
10
10
  '#fluxy-widget-bubble:hover{transform:scale(1.1)}',
11
11
  '#fluxy-widget-bubble:active{transform:scale(0.95)}',
12
- '#fluxy-widget-bubble video{height:60px;width:auto;pointer-events:none;-webkit-user-drag:none}',
13
- '#fluxy-widget-bubble img{width:60px;height:60px;object-fit:contain;pointer-events:none;-webkit-user-drag:none}',
14
12
  '#fluxy-widget-backdrop{position:fixed;inset:0;z-index:99998;background:rgba(0,0,0,0.4);opacity:0;transition:opacity .2s ease;pointer-events:none}',
15
13
  '#fluxy-widget-backdrop.open{opacity:1;pointer-events:auto}',
16
14
  '#fluxy-widget-panel{position:fixed;top:0;right:0;bottom:0;z-index:99999;width:' + PANEL_WIDTH + ';max-width:100vw;transform:translateX(100%);transition:transform .25s cubic-bezier(.4,0,.2,1);box-shadow:-4px 0 24px rgba(0,0,0,0.3);border-left:1px solid #3a3a3a;overflow:hidden}',
@@ -35,50 +33,360 @@
35
33
  iframe.setAttribute('loading', 'lazy');
36
34
  panel.appendChild(iframe);
37
35
 
38
- // ── Bubble ──
39
- var bubble = document.createElement('div');
40
- bubble.id = 'fluxy-widget-bubble';
41
- bubble.setAttribute('role', 'button');
42
- bubble.setAttribute('aria-label', 'Open Fluxy chat');
43
-
44
- // Safari doesn't support WebM alpha — fall back to poster image
45
- var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent)
46
- || (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream);
47
-
48
- if (isSafari) {
49
- var img = document.createElement('img');
50
- img.src = '/fluxy_frame1.png';
51
- img.alt = 'Fluxy';
52
- img.draggable = false;
53
- bubble.appendChild(img);
54
- } else {
55
- var video = document.createElement('video');
56
- video.src = '/fluxy_tilts.webm';
57
- video.poster = '/fluxy_frame1.png';
58
- video.autoplay = true;
59
- video.loop = true;
60
- video.muted = true;
61
- video.playsInline = true;
62
- video.setAttribute('playsinline', '');
63
- video.draggable = false;
64
- bubble.appendChild(video);
36
+ // ══════════════════════════════════════════════════════════════════
37
+ // ── Sprite Sheet Animation (Splash + Bubble) ─────────────────────
38
+ // ══════════════════════════════════════════════════════════════════
39
+
40
+ // ── Sprite config ──
41
+ var COLS = 16;
42
+ var FRAME_W = 125;
43
+ var FRAME_H = 120;
44
+ var DISPLAY_H = 58;
45
+ var DISPLAY_W = DISPLAY_H * (FRAME_W / FRAME_H);
46
+
47
+ // Frame ranges (0-indexed)
48
+ var IDLE_START = 0, IDLE_END = 29;
49
+ var MELT_START = 30, MELT_END = 52;
50
+ var TRAVEL_START = 52, TRAVEL_END = 84;
51
+ var REFORM_START = 84, REFORM_END = 191;
52
+
53
+ // Timing
54
+ var FPS = 29;
55
+ var FRAME_MS = 1000 / FPS;
56
+ var IDLE_FPS = 22;
57
+ var IDLE_FRAME_MS = 1000 / IDLE_FPS;
58
+ var REFORM_FPS = 70;
59
+ var REFORM_FRAME_MS = 1000 / REFORM_FPS;
60
+
61
+ var TRAVEL_PX_PER_MS = 0.65;
62
+ var TRAVEL_MIN = 385;
63
+ var TRAVEL_MAX = 1150;
64
+ var MIN_SPLASH_MS = 2000;
65
+ var BUBBLE_SIZE = 60;
66
+ var BUBBLE_MARGIN = 24;
67
+
68
+ // ── Easing ──
69
+ function easeInOutCubic(t) { return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2; }
70
+
71
+ // ── Canvas setup (starts fullscreen for splash) ──
72
+ var canvas = document.createElement('canvas');
73
+ canvas.id = 'fluxy-widget-bubble';
74
+ canvas.setAttribute('role', 'button');
75
+ canvas.setAttribute('aria-label', 'Open Fluxy chat');
76
+ canvas.dataset.fluxyWidget = '1';
77
+ canvas.style.cssText = 'position:fixed;inset:0;width:100vw;height:100dvh;z-index:9999;pointer-events:none;';
78
+ document.body.appendChild(canvas);
79
+
80
+ var bubble = canvas;
81
+ var ctx = canvas.getContext('2d');
82
+ var dpr = window.devicePixelRatio || 1;
83
+ var W = 0, H = 0;
84
+
85
+ // ── Canvas phase: 'splash' | 'transitioning' | 'bubble' | 'disabled' ──
86
+ var canvasPhase = 'splash';
87
+ var canvasCreatedAt = Date.now();
88
+ var appReady = false;
89
+ var hideAfterTransition = false;
90
+ var skipSplash = false;
91
+
92
+ function resizeCanvas() {
93
+ if (canvasPhase === 'bubble') {
94
+ W = BUBBLE_SIZE;
95
+ H = BUBBLE_SIZE;
96
+ } else {
97
+ W = window.innerWidth;
98
+ H = window.innerHeight;
99
+ }
100
+ canvas.width = W * dpr;
101
+ canvas.height = H * dpr;
102
+ canvas.style.width = W + 'px';
103
+ canvas.style.height = H + 'px';
104
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
105
+ }
106
+ resizeCanvas();
107
+ window.addEventListener('resize', function () {
108
+ resizeCanvas();
109
+ if (canvasPhase === 'splash' && animState === 'idle') {
110
+ center.x = Math.round(W / 2);
111
+ center.y = Math.round(H / 2);
112
+ }
113
+ });
114
+
115
+ // ── Animation state ──
116
+ var spriteSheet = null;
117
+ var animState = 'loading';
118
+ var currentFrame = 0;
119
+ var idleDirection = 1;
120
+ var lastFrameTime = 0;
121
+ var travelDuration = 0;
122
+ var travelStartTime = 0;
123
+ var travelAngle = 0;
124
+ var travelDrawAngle = 0;
125
+ var travelFlip = 1;
126
+
127
+ var center = { x: 0, y: 0 };
128
+ var travelA = { x: 0, y: 0 };
129
+ var target = { x: 0, y: 0 };
130
+
131
+ // ── Load sprite sheet ──
132
+ function loadSprite(onDone, onFail) {
133
+ var img = new Image();
134
+ img.onload = function () {
135
+ spriteSheet = img;
136
+ center.x = Math.round(W / 2);
137
+ center.y = Math.round(H / 2);
138
+ currentFrame = IDLE_START;
139
+ animState = 'idle';
140
+
141
+ // Hide the HTML splash fallback
142
+ var splash = document.getElementById('splash');
143
+ if (splash) splash.style.display = 'none';
144
+
145
+ onDone();
146
+ };
147
+ img.onerror = function () {
148
+ if (onFail) onFail();
149
+ };
150
+ img.src = '/spritesheet.webp';
151
+ }
152
+
153
+ // ── Trigger move ──
154
+ function moveTo(tx, ty) {
155
+ if (animState !== 'idle') return;
156
+ target.x = tx;
157
+ target.y = ty;
158
+ travelA.x = center.x;
159
+ travelA.y = center.y;
160
+
161
+ var dx = target.x - center.x;
162
+ var dy = target.y - center.y;
163
+ var dist = Math.sqrt(dx * dx + dy * dy);
164
+
165
+ travelAngle = Math.atan2(dy, dx);
166
+ travelDrawAngle = travelAngle;
167
+ travelFlip = 1;
168
+ if (travelDrawAngle > Math.PI / 2) { travelDrawAngle -= Math.PI; travelFlip = -1; }
169
+ else if (travelDrawAngle < -Math.PI / 2) { travelDrawAngle += Math.PI; travelFlip = -1; }
170
+ travelDuration = Math.min(TRAVEL_MAX, Math.max(TRAVEL_MIN, dist / TRAVEL_PX_PER_MS));
171
+
172
+ currentFrame = MELT_START;
173
+ lastFrameTime = performance.now();
174
+ animState = 'melting';
175
+ }
176
+
177
+ // ── Update ──
178
+ function update(now) {
179
+ // Smooth position update during travel (every rAF)
180
+ if (animState === 'traveling') {
181
+ var elapsed = now - travelStartTime;
182
+ var progress = Math.min(1, elapsed / travelDuration);
183
+ var e = easeInOutCubic(progress);
184
+ center.x = travelA.x + (target.x - travelA.x) * e;
185
+ center.y = travelA.y + (target.y - travelA.y) * e;
186
+
187
+ if (progress >= 1) {
188
+ center.x = target.x;
189
+ center.y = target.y;
190
+ animState = 'reforming';
191
+ currentFrame = REFORM_START;
192
+ lastFrameTime = now;
193
+ return;
194
+ }
195
+ }
196
+
197
+ // Sprite frame advance (at configured FPS)
198
+ var frameInterval = animState === 'idle' ? IDLE_FRAME_MS
199
+ : animState === 'reforming' ? REFORM_FRAME_MS
200
+ : FRAME_MS;
201
+ var frameDelta = now - lastFrameTime;
202
+ if (frameDelta < frameInterval) return;
203
+ lastFrameTime = now - (frameDelta % frameInterval);
204
+
205
+ if (animState === 'idle') {
206
+ currentFrame += idleDirection;
207
+ if (currentFrame >= IDLE_END) { currentFrame = IDLE_END; idleDirection = -1; }
208
+ else if (currentFrame <= IDLE_START) { currentFrame = IDLE_START; idleDirection = 1; }
209
+ } else if (animState === 'melting') {
210
+ currentFrame++;
211
+ if (currentFrame > MELT_END) {
212
+ animState = 'traveling';
213
+ currentFrame = TRAVEL_START;
214
+ travelStartTime = now;
215
+ }
216
+ } else if (animState === 'traveling') {
217
+ currentFrame++;
218
+ if (currentFrame > TRAVEL_END) currentFrame = TRAVEL_START;
219
+ } else if (animState === 'reforming') {
220
+ currentFrame++;
221
+ if (currentFrame > REFORM_END) {
222
+ animState = 'idle';
223
+ currentFrame = IDLE_START;
224
+ idleDirection = 1;
225
+ // Transition canvas to bubble mode
226
+ if (canvasPhase === 'transitioning') {
227
+ canvasPhase = 'bubble';
228
+ canvas.style.cssText = 'position:fixed;bottom:' + BUBBLE_MARGIN + 'px;right:' + BUBBLE_MARGIN + 'px;width:' + BUBBLE_SIZE + 'px;height:' + BUBBLE_SIZE + 'px;z-index:99998;cursor:pointer;border-radius:50%;transition:transform .15s ease;-webkit-tap-highlight-color:transparent;';
229
+ W = BUBBLE_SIZE;
230
+ H = BUBBLE_SIZE;
231
+ canvas.width = BUBBLE_SIZE * dpr;
232
+ canvas.height = BUBBLE_SIZE * dpr;
233
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
234
+ center.x = BUBBLE_SIZE / 2;
235
+ center.y = BUBBLE_SIZE / 2;
236
+ if (hideAfterTransition) {
237
+ canvas.style.display = 'none';
238
+ }
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ // ── Draw ──
245
+ function drawFrame(frame, x, y, rotate, flipX) {
246
+ if (!spriteSheet) return;
247
+ var col = frame % COLS;
248
+ var row = Math.floor(frame / COLS);
249
+ var sx = col * FRAME_W;
250
+ var sy = row * FRAME_H;
251
+
252
+ ctx.save();
253
+ ctx.translate(x, y);
254
+ if (rotate) ctx.rotate(rotate);
255
+ if (flipX === -1) ctx.scale(-1, 1);
256
+ ctx.imageSmoothingEnabled = true;
257
+ ctx.imageSmoothingQuality = 'high';
258
+ ctx.drawImage(
259
+ spriteSheet,
260
+ sx, sy, FRAME_W, FRAME_H,
261
+ -DISPLAY_W / 2, -DISPLAY_H / 2,
262
+ DISPLAY_W, DISPLAY_H
263
+ );
264
+ ctx.restore();
65
265
  }
66
266
 
67
- // Mark widget present
68
- bubble.dataset.fluxyWidget = '1';
69
- document.body.appendChild(bubble);
267
+ function draw(now) {
268
+ ctx.clearRect(0, 0, W, H);
269
+ if (!spriteSheet || animState === 'loading') return;
270
+
271
+ if (animState === 'melting') {
272
+ var p = (currentFrame - MELT_START) / (MELT_END - MELT_START);
273
+ var eased = p * p;
274
+ drawFrame(currentFrame, center.x, center.y, travelDrawAngle * eased, travelFlip);
275
+ } else if (animState === 'traveling') {
276
+ drawFrame(currentFrame, center.x, center.y, travelDrawAngle, travelFlip);
277
+ } else if (animState === 'reforming') {
278
+ var p2 = (currentFrame - REFORM_START) / (REFORM_END - REFORM_START);
279
+ var eased2 = 1 - (1 - p2) * (1 - p2);
280
+ drawFrame(currentFrame, center.x, center.y, travelDrawAngle * (1 - eased2));
281
+ } else {
282
+ drawFrame(currentFrame, center.x, center.y);
283
+ }
284
+ }
285
+
286
+ // ── Animation loop ──
287
+ function loop(now) {
288
+ if (animState !== 'loading') {
289
+ update(now);
290
+ draw(now);
291
+ }
292
+ requestAnimationFrame(loop);
293
+ }
294
+
295
+ // ── App-ready coordination ──
296
+ function maybeTransition() {
297
+ if (!appReady || animState !== 'idle' || canvasPhase !== 'splash') return;
298
+ var elapsed = Date.now() - canvasCreatedAt;
299
+ if (elapsed < MIN_SPLASH_MS) {
300
+ setTimeout(maybeTransition, MIN_SPLASH_MS - elapsed);
301
+ return;
302
+ }
303
+ triggerTransition();
304
+ }
305
+
306
+ function triggerTransition() {
307
+ canvasPhase = 'transitioning';
308
+ var tx = W - BUBBLE_MARGIN - BUBBLE_SIZE / 2;
309
+ var ty = H - BUBBLE_MARGIN - BUBBLE_SIZE / 2;
310
+ moveTo(tx, ty);
311
+ }
312
+
313
+ // Skip splash and go straight to bubble mode (for onboard / instant load)
314
+ function skipToBubble() {
315
+ canvasPhase = 'bubble';
316
+ canvas.style.cssText = 'position:fixed;bottom:' + BUBBLE_MARGIN + 'px;right:' + BUBBLE_MARGIN + 'px;width:' + BUBBLE_SIZE + 'px;height:' + BUBBLE_SIZE + 'px;z-index:99998;cursor:pointer;border-radius:50%;transition:transform .15s ease;-webkit-tap-highlight-color:transparent;';
317
+ W = BUBBLE_SIZE;
318
+ H = BUBBLE_SIZE;
319
+ canvas.width = BUBBLE_SIZE * dpr;
320
+ canvas.height = BUBBLE_SIZE * dpr;
321
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
322
+ center.x = BUBBLE_SIZE / 2;
323
+ center.y = BUBBLE_SIZE / 2;
324
+ currentFrame = IDLE_START;
325
+ idleDirection = 1;
326
+ animState = 'idle';
327
+ // Hide the HTML splash fallback
328
+ var splash = document.getElementById('splash');
329
+ if (splash) splash.style.display = 'none';
330
+ }
331
+
332
+ window.addEventListener('fluxy:app-ready', function () {
333
+ appReady = true;
334
+ maybeTransition();
335
+ });
336
+ if (window.__fluxyAppReady) {
337
+ appReady = true;
338
+ }
339
+
340
+ // ── Check onboard status, then start ──
341
+ // First-time users get the onboard wizard — skip the splash animation.
342
+ var onboardActive = false;
343
+ fetch('/api/settings')
344
+ .then(function (r) { return r.json(); })
345
+ .then(function (s) {
346
+ if (s.onboard_complete !== 'true') {
347
+ onboardActive = true;
348
+ skipSplash = true;
349
+ }
350
+ startAnimation();
351
+ })
352
+ .catch(function () {
353
+ startAnimation();
354
+ });
355
+
356
+ function startAnimation() {
357
+ loadSprite(
358
+ function onSuccess() {
359
+ if (skipSplash) {
360
+ skipToBubble();
361
+ if (onboardActive) canvas.style.display = 'none';
362
+ }
363
+ requestAnimationFrame(loop);
364
+ maybeTransition();
365
+ },
366
+ function onFail() {
367
+ canvas.style.display = 'none';
368
+ canvasPhase = 'disabled';
369
+ }
370
+ );
371
+ }
372
+
373
+ // ══════════════════════════════════════════════════════════════════
374
+ // ── Widget Interaction (Panel, Toggle, Messages) ─────────────────
375
+ // ══════════════════════════════════════════════════════════════════
70
376
 
71
- // ── Toggle ──
72
377
  var isOpen = false;
73
378
 
74
379
  function toggle() {
380
+ if (canvasPhase !== 'bubble') return;
75
381
  isOpen = !isOpen;
76
382
  panel.classList.toggle('open', isOpen);
77
383
  backdrop.classList.toggle('open', isOpen);
78
- bubble.style.display = isOpen ? 'none' : 'flex';
384
+ canvas.style.display = isOpen ? 'none' : 'block';
79
385
  }
80
386
 
81
- bubble.addEventListener('click', toggle);
387
+ bubble.addEventListener('click', function () {
388
+ if (canvasPhase === 'bubble') toggle();
389
+ });
82
390
  backdrop.addEventListener('click', toggle);
83
391
 
84
392
  // Close on Escape
@@ -97,10 +405,8 @@
97
405
  window.addEventListener('message', function (e) {
98
406
  if (!e.data || !e.data.type) return;
99
407
 
100
- // Close chat panel
101
408
  if (e.data.type === 'fluxy:close' && isOpen) toggle();
102
409
 
103
- // Install App request from chat iframe
104
410
  if (e.data.type === 'fluxy:install-app') {
105
411
  if (deferredInstallPrompt) {
106
412
  deferredInstallPrompt.prompt();
@@ -108,66 +414,28 @@
108
414
  if (result.outcome === 'accepted') deferredInstallPrompt = null;
109
415
  });
110
416
  } else {
111
- // No native prompt available — tell chat to show instructions modal
112
417
  iframe.contentWindow.postMessage({ type: 'fluxy:show-ios-install' }, '*');
113
418
  }
114
419
  }
420
+
421
+ // Onboard complete — show the bubble
422
+ if (e.data.type === 'fluxy:onboard-complete') {
423
+ onboardActive = false;
424
+ hideAfterTransition = false;
425
+ canvas.style.display = 'block';
426
+ }
115
427
  });
116
428
 
117
- // Restore open state after HMR reload (so chat isn't disrupted)
429
+ // Restore open state after HMR reload
118
430
  try {
119
431
  if (sessionStorage.getItem('fluxy_widget_open') === '1') {
120
432
  sessionStorage.removeItem('fluxy_widget_open');
121
- toggle();
433
+ if (canvasPhase === 'bubble') toggle();
122
434
  }
123
435
  } catch (e) {}
124
436
 
125
- // Hide widget during initial onboard
126
- try {
127
- fetch('/api/settings')
128
- .then(function (r) { return r.json(); })
129
- .then(function (s) {
130
- if (s.onboard_complete !== 'true') {
131
- bubble.style.display = 'none';
132
- window.addEventListener('message', function onMsg(e) {
133
- if (e.data && e.data.type === 'fluxy:onboard-complete') {
134
- bubble.style.display = 'flex';
135
- window.removeEventListener('message', onMsg);
136
- }
137
- });
138
- }
139
- })
140
- .catch(function () {});
141
- } catch (e) {}
142
-
143
437
  // Inject app-ws.js — proxies /app/api/* fetch calls through WebSocket
144
- // (works around POST request failures through Cloudflare tunnel)
145
438
  var awsScript = document.createElement('script');
146
439
  awsScript.src = '/fluxy/app-ws.js';
147
440
  document.head.appendChild(awsScript);
148
-
149
- // ── Splash / Service Worker upgrade logic ──────────────────────────
150
- // This runs from the supervisor (always updated), so it fixes existing
151
- // installs whose workspace/client/index.html is still old.
152
-
153
- // Re-show splash before page unloads (manual refresh)
154
- window.addEventListener('beforeunload', function () {
155
- console.log('[widget] beforeunload — showing splash');
156
- var s = document.getElementById('splash');
157
- if (s) { s.style.transition = 'none'; s.style.display = 'flex'; s.style.opacity = '1'; }
158
- });
159
-
160
- // When a new SW takes control, show splash and reload so the new
161
- // caching strategy (stale-while-revalidate) kicks in immediately.
162
- if ('serviceWorker' in navigator) {
163
- var swRefreshing = false;
164
- navigator.serviceWorker.addEventListener('controllerchange', function () {
165
- console.log('[widget] controllerchange — new SW took control, refreshing:', swRefreshing);
166
- if (swRefreshing) return;
167
- swRefreshing = true;
168
- var s = document.getElementById('splash');
169
- if (s) { s.style.transition = 'none'; s.style.display = 'flex'; s.style.opacity = '1'; }
170
- location.reload();
171
- });
172
- }
173
441
  })();
@@ -10,18 +10,11 @@
10
10
  <link rel="apple-touch-icon" href="/fluxy-icon-192.png" />
11
11
  <link rel="manifest" href="/manifest.json" />
12
12
  <title>Fluxy</title>
13
- <style>
14
- @keyframes _fs{to{transform:rotate(360deg)}}
15
- </style>
16
13
  </head>
17
14
  <body class="bg-background text-foreground" style="background:#222122">
18
- <!-- App shell splash — visible instantly, no JS needed.
19
- Covers the screen during reload/restore so there's never a white flash.
20
- React hides it on mount; shown again before any reload. -->
21
- <div id="splash" style="background:#222122;color:#fff;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100dvh;width:100vw;position:fixed;inset:0;z-index:9999;font-family:system-ui,-apple-system,sans-serif;transition:opacity .25s ease-out">
22
- <img src="/fluxy-icon-192.png" width="56" height="56" style="border-radius:14px;margin-bottom:20px" alt="" />
23
- <div style="width:18px;height:18px;border:2px solid rgba(255,255,255,0.12);border-top-color:rgba(255,255,255,0.7);border-radius:50%;animation:_fs .6s linear infinite"></div>
24
- </div>
15
+ <!-- Dark background fallback — visible instantly while widget.js loads.
16
+ The canvas animation (in widget.js) takes over as the actual splash. -->
17
+ <div id="splash" style="background:#222122;position:fixed;inset:0;z-index:9998;transition:opacity .25s ease-out"></div>
25
18
 
26
19
  <div id="root"></div>
27
20
 
@@ -42,13 +35,10 @@
42
35
  });
43
36
  </script>
44
37
  <script>
45
- // Re-show splash before page unloads (manual refresh / navigation).
46
- // The last painted frame before teardown will be the dark splash
47
- // instead of a white gap.
38
+ // Re-show dark background before page unloads (manual refresh / navigation).
48
39
  window.addEventListener('beforeunload', function () {
49
- console.log('[splash] beforeunload — showing splash');
50
40
  var s = document.getElementById('splash');
51
- if (s) { s.style.transition = 'none'; s.style.display = 'flex'; s.style.opacity = '1'; }
41
+ if (s) { s.style.transition = 'none'; s.style.display = 'block'; s.style.opacity = '1'; }
52
42
  });
53
43
  </script>
54
44
  <script type="module" src="/src/main.tsx"></script>
@@ -63,7 +53,7 @@
63
53
  if(swRefreshing)return;
64
54
  swRefreshing=true;
65
55
  var s=document.getElementById('splash');
66
- if(s){s.style.transition='none';s.style.display='flex';s.style.opacity='1'}
56
+ if(s){s.style.transition='none';s.style.display='block';s.style.opacity='1'}
67
57
  console.log('[sw-reg] reloading after new SW took control');
68
58
  location.reload();
69
59
  });
@@ -41,10 +41,10 @@ export default function App() {
41
41
  const splash = document.getElementById('splash');
42
42
  let hiddenAt = 0;
43
43
 
44
- // Show the splash screen (used before reloads and on resume)
44
+ // Show the dark background (used before reloads and on resume)
45
45
  function showSplash() {
46
46
  if (!splash) return;
47
- splash.style.display = 'flex';
47
+ splash.style.display = 'block';
48
48
  splash.style.opacity = '1';
49
49
  }
50
50
 
@@ -9,18 +9,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
9
9
  </React.StrictMode>,
10
10
  );
11
11
 
12
- // Fade out the HTML splash screen once React has actually painted.
13
- // React 18's createRoot().render() is async wait for the next frame
14
- // to ensure the app is visible before removing the splash.
15
- console.log('[main] React render called, waiting for paint to hide splash');
12
+ // Signal that the app has painted the canvas animation in widget.js
13
+ // will trigger the melt travel reform transition to bubble mode.
16
14
  requestAnimationFrame(() => requestAnimationFrame(() => {
17
- console.log('[main] double-rAF fired hiding splash');
18
- const splash = document.getElementById('splash');
19
- if (splash) {
20
- splash.style.opacity = '0';
21
- splash.addEventListener('transitionend', () => {
22
- splash.style.display = 'none';
23
- console.log('[main] splash hidden');
24
- }, { once: true });
25
- }
15
+ (window as any).__fluxyAppReady = true;
16
+ window.dispatchEvent(new Event('fluxy:app-ready'));
26
17
  }));