fluxy-bot 0.10.16 → 0.10.17

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.17",
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,374 @@
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
+ // ── Canvas Particle Animation (Splash + Bubble) ──────────────────
38
+ // ══════════════════════════════════════════════════════════════════
39
+
40
+ // ── Config ──
41
+ var TARGET_HEIGHT = 40;
42
+ var SAMPLE_RES = 60;
43
+ var MELT_DURATION = 750;
44
+ var REFORM_DURATION = 850;
45
+ var TRAVEL_MIN = 400;
46
+ var TRAVEL_MAX = 1100;
47
+ var TRAVEL_PX_PER_MS = 0.9;
48
+ var MIN_SPLASH_MS = 2000;
49
+ var BUBBLE_SIZE = 60;
50
+ var BUBBLE_MARGIN = 24;
51
+
52
+ // ── Easing ──
53
+ function easeInCubic(t) { return t * t * t; }
54
+ function easeInOutCubic(t) { return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t + 2, 3) / 2; }
55
+ function easeOutElastic(t) {
56
+ if (t === 0 || t === 1) return t;
57
+ return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * (2 * Math.PI / 3)) + 1;
58
+ }
59
+
60
+ // ── Canvas setup (starts fullscreen for splash) ──
61
+ var canvas = document.createElement('canvas');
62
+ canvas.id = 'fluxy-widget-bubble';
63
+ canvas.setAttribute('role', 'button');
64
+ canvas.setAttribute('aria-label', 'Open Fluxy chat');
65
+ canvas.dataset.fluxyWidget = '1';
66
+ canvas.style.cssText = 'position:fixed;inset:0;width:100vw;height:100dvh;z-index:9999;pointer-events:none;';
67
+ document.body.appendChild(canvas);
68
+
69
+ var bubble = canvas; // alias for toggle/click compatibility
70
+ var ctx = canvas.getContext('2d');
71
+ var dpr = window.devicePixelRatio || 1;
72
+ var W = 0, H = 0;
73
+
74
+ // ── Canvas phase: 'splash' | 'transitioning' | 'bubble' ──
75
+ var canvasPhase = 'splash';
76
+ var canvasCreatedAt = Date.now();
77
+ var appReady = false;
78
+ var hideAfterTransition = false;
79
+
80
+ function resizeCanvas() {
81
+ if (canvasPhase === 'bubble') {
82
+ W = BUBBLE_SIZE;
83
+ H = BUBBLE_SIZE;
84
+ } else {
85
+ W = window.innerWidth;
86
+ H = window.innerHeight;
87
+ }
88
+ canvas.width = W * dpr;
89
+ canvas.height = H * dpr;
90
+ canvas.style.width = W + 'px';
91
+ canvas.style.height = H + 'px';
92
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
93
+ }
94
+ resizeCanvas();
95
+ window.addEventListener('resize', function () {
96
+ resizeCanvas();
97
+ // Recalculate center position during splash
98
+ if (canvasPhase === 'splash' && animState === 'idle') {
99
+ center.x = Math.round(W / 2);
100
+ center.y = Math.round(H / 2);
101
+ }
102
+ });
103
+
104
+ // ── Particle state ──
105
+ var particles = [];
106
+ var preRendered = null;
107
+ var displayW = 0, displayH = 0;
108
+ var particleRadius = 0;
109
+ var animState = 'loading'; // particle animation state
110
+ var phaseStart = 0;
111
+ var travelDuration = 0;
112
+
113
+ var center = { x: 0, y: 0 };
114
+ var travelA = { x: 0, y: 0 };
115
+ var target = { x: 0, y: 0 };
116
+
117
+ // ── Particle constructor ──
118
+ function Particle(localX, localY, r, g, b, a) {
119
+ this.localX = localX;
120
+ this.localY = localY;
121
+ this.r = r;
122
+ this.g = g;
123
+ this.b = b;
124
+ this.a = a;
125
+ this.x = 0;
126
+ this.y = 0;
127
+
128
+ var normalizedY = (localY + displayH / 2) / displayH;
129
+
130
+ // Melt into an elliptical blob
131
+ var angle = Math.random() * Math.PI * 2;
132
+ var dist = Math.sqrt(Math.random());
133
+ var blobRX = displayW * 0.28;
134
+ var blobRY = displayH * 0.18;
135
+ this.meltLocalX = Math.cos(angle) * dist * blobRX;
136
+ this.meltLocalY = Math.sin(angle) * dist * blobRY + displayH * 0.3;
137
+
138
+ // Stagger: top particles melt first
139
+ this.meltDelay = normalizedY * 0.35;
140
+
141
+ this.wobbleAmp = 0.5 + Math.random() * 1.5;
142
+ this.wobbleFreq = 3 + Math.random() * 4;
143
+ this.wobblePhase = Math.random() * Math.PI * 2;
144
+ }
145
+
146
+ // ── Load mascot ──
147
+ function loadMascot(onDone, onFail) {
148
+ var img = new Image();
149
+ img.onload = function () {
150
+ var aspect = img.width / img.height;
151
+ displayH = TARGET_HEIGHT;
152
+ displayW = TARGET_HEIGHT * aspect;
153
+
154
+ // Pre-render at high DPR for crisp display
155
+ var preScale = dpr * 2;
156
+ var pre = document.createElement('canvas');
157
+ var preCtx = pre.getContext('2d');
158
+ pre.width = Math.round(displayW * preScale);
159
+ pre.height = Math.round(displayH * preScale);
160
+ preCtx.imageSmoothingEnabled = true;
161
+ preCtx.imageSmoothingQuality = 'high';
162
+ preCtx.drawImage(img, 0, 0, pre.width, pre.height);
163
+ preRendered = pre;
164
+
165
+ // Sample pixels for particles
166
+ var sampleH = SAMPLE_RES;
167
+ var sampleW = Math.round(SAMPLE_RES * aspect);
168
+
169
+ var off = document.createElement('canvas');
170
+ var offCtx = off.getContext('2d');
171
+ off.width = sampleW;
172
+ off.height = sampleH;
173
+ offCtx.imageSmoothingEnabled = true;
174
+ offCtx.imageSmoothingQuality = 'high';
175
+ offCtx.drawImage(img, 0, 0, sampleW, sampleH);
176
+
177
+ var data = offCtx.getImageData(0, 0, sampleW, sampleH).data;
178
+ var scaleX = displayW / sampleW;
179
+ var scaleY = displayH / sampleH;
180
+ particleRadius = (scaleX + scaleY) / 2 * 0.7;
181
+
182
+ for (var y = 0; y < sampleH; y++) {
183
+ for (var x = 0; x < sampleW; x++) {
184
+ var i = (y * sampleW + x) * 4;
185
+ if (data[i + 3] > 50) {
186
+ var lx = (x - sampleW / 2) * scaleX;
187
+ var ly = (y - sampleH / 2) * scaleY;
188
+ particles.push(new Particle(lx, ly, data[i], data[i+1], data[i+2], data[i+3] / 255));
189
+ }
190
+ }
191
+ }
192
+
193
+ // Start idle in center
194
+ center.x = Math.round(W / 2);
195
+ center.y = Math.round(H / 2);
196
+ animState = 'idle';
197
+
198
+ // Hide the HTML splash fallback
199
+ var splash = document.getElementById('splash');
200
+ if (splash) splash.style.display = 'none';
201
+
202
+ onDone();
203
+ };
204
+ img.onerror = function () {
205
+ if (onFail) onFail();
206
+ };
207
+ img.src = '/fluxy.png';
208
+ }
209
+
210
+ // ── Trigger move ──
211
+ function moveTo(tx, ty) {
212
+ if (animState !== 'idle') return;
213
+ target.x = tx;
214
+ target.y = ty;
215
+ travelA.x = center.x;
216
+ travelA.y = center.y;
217
+ animState = 'melting';
218
+ phaseStart = performance.now();
219
+ }
220
+
221
+ // ── Update ──
222
+ function update(now) {
223
+ var elapsed = now - phaseStart;
224
+ var i, p, t, e, cx, cy, wx, wy;
225
+
226
+ if (animState === 'melting') {
227
+ var progress = elapsed / MELT_DURATION;
228
+ if (progress >= 1) {
229
+ progress = 1;
230
+ var dx = target.x - travelA.x;
231
+ var dy = target.y - travelA.y;
232
+ var dist = Math.sqrt(dx*dx + dy*dy);
233
+ travelDuration = Math.min(TRAVEL_MAX, Math.max(TRAVEL_MIN, dist / TRAVEL_PX_PER_MS));
234
+ animState = 'traveling';
235
+ phaseStart = now;
236
+ }
237
+ for (i = 0; i < particles.length; i++) {
238
+ p = particles[i];
239
+ t = Math.max(0, Math.min(1, (progress - p.meltDelay) / (1 - p.meltDelay)));
240
+ e = easeInCubic(t);
241
+ p.x = center.x + p.localX + (p.meltLocalX - p.localX) * e;
242
+ p.y = center.y + p.localY + (p.meltLocalY - p.localY) * e;
243
+ }
244
+ return;
245
+ }
246
+
247
+ if (animState === 'traveling') {
248
+ var progress2 = elapsed / travelDuration;
249
+ if (progress2 >= 1) {
250
+ progress2 = 1;
251
+ center.x = target.x;
252
+ center.y = target.y;
253
+ animState = 'reforming';
254
+ phaseStart = now;
255
+ }
256
+ e = easeInOutCubic(progress2);
257
+ cx = travelA.x + (target.x - travelA.x) * e;
258
+ cy = travelA.y + (target.y - travelA.y) * e;
259
+ for (i = 0; i < particles.length; i++) {
260
+ p = particles[i];
261
+ wx = Math.sin(now * 0.008 * p.wobbleFreq + p.wobblePhase) * p.wobbleAmp;
262
+ wy = Math.cos(now * 0.006 * p.wobbleFreq + p.wobblePhase) * p.wobbleAmp * 0.5;
263
+ p.x = cx + p.meltLocalX + wx;
264
+ p.y = cy + p.meltLocalY + wy;
265
+ }
266
+ return;
267
+ }
268
+
269
+ if (animState === 'reforming') {
270
+ var progress3 = elapsed / REFORM_DURATION;
271
+ if (progress3 >= 1) {
272
+ progress3 = 1;
273
+ animState = 'idle';
274
+ // Transition canvas to bubble mode
275
+ if (canvasPhase === 'transitioning') {
276
+ canvasPhase = 'bubble';
277
+ 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%;';
278
+ W = BUBBLE_SIZE;
279
+ H = BUBBLE_SIZE;
280
+ canvas.width = BUBBLE_SIZE * dpr;
281
+ canvas.height = BUBBLE_SIZE * dpr;
282
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
283
+ // Center is now the center of the bubble canvas
284
+ center.x = BUBBLE_SIZE / 2;
285
+ center.y = BUBBLE_SIZE / 2;
286
+ // Handle onboard hide
287
+ if (hideAfterTransition) {
288
+ canvas.style.display = 'none';
289
+ }
290
+ }
291
+ }
292
+ e = easeOutElastic(progress3);
293
+ for (i = 0; i < particles.length; i++) {
294
+ p = particles[i];
295
+ p.x = center.x + p.meltLocalX + (p.localX - p.meltLocalX) * e;
296
+ p.y = center.y + p.meltLocalY + (p.localY - p.meltLocalY) * e;
297
+ }
298
+ }
299
+ }
300
+
301
+ // ── Draw ──
302
+ function draw(now) {
303
+ ctx.clearRect(0, 0, W, H);
304
+
305
+ if (animState === 'idle' && preRendered) {
306
+ var bob = Math.sin(now * 0.002) * 1;
307
+ ctx.imageSmoothingEnabled = true;
308
+ ctx.imageSmoothingQuality = 'high';
309
+ ctx.drawImage(
310
+ preRendered,
311
+ center.x - displayW / 2,
312
+ center.y - displayH / 2 + bob,
313
+ displayW,
314
+ displayH
315
+ );
316
+ } else if (animState !== 'loading') {
317
+ var r = particleRadius;
318
+ for (var i = 0; i < particles.length; i++) {
319
+ var p = particles[i];
320
+ // In transitioning/bubble phase, offset particles relative to canvas
321
+ var px = p.x;
322
+ var py = p.y;
323
+ if (canvasPhase === 'splash' || canvasPhase === 'transitioning') {
324
+ // Particles are in screen coordinates during splash/transition
325
+ }
326
+ ctx.globalAlpha = p.a;
327
+ ctx.fillStyle = 'rgb(' + p.r + ',' + p.g + ',' + p.b + ')';
328
+ ctx.beginPath();
329
+ ctx.arc(px, py, r, 0, Math.PI * 2);
330
+ ctx.fill();
331
+ }
332
+ ctx.globalAlpha = 1;
333
+ }
334
+ }
335
+
336
+ // ── Animation loop ──
337
+ function loop(now) {
338
+ if (animState !== 'loading') {
339
+ update(now);
340
+ draw(now);
341
+ }
342
+ requestAnimationFrame(loop);
343
+ }
344
+
345
+ // ── App-ready coordination ──
346
+ function maybeTransition() {
347
+ if (!appReady || animState !== 'idle' || canvasPhase !== 'splash') return;
348
+ var elapsed = Date.now() - canvasCreatedAt;
349
+ if (elapsed < MIN_SPLASH_MS) {
350
+ setTimeout(maybeTransition, MIN_SPLASH_MS - elapsed);
351
+ return;
352
+ }
353
+ triggerTransition();
354
+ }
355
+
356
+ function triggerTransition() {
357
+ canvasPhase = 'transitioning';
358
+ // Target: center of the 60x60 bubble area at bottom-right
359
+ var tx = W - BUBBLE_MARGIN - BUBBLE_SIZE / 2;
360
+ var ty = H - BUBBLE_MARGIN - BUBBLE_SIZE / 2;
361
+ moveTo(tx, ty);
362
+ }
363
+
364
+ window.addEventListener('fluxy:app-ready', function () {
365
+ appReady = true;
366
+ maybeTransition();
367
+ });
368
+ // Race condition guard: check if main.tsx already fired before we registered
369
+ if (window.__fluxyAppReady) {
370
+ appReady = true;
65
371
  }
66
372
 
67
- // Mark widget present
68
- bubble.dataset.fluxyWidget = '1';
69
- document.body.appendChild(bubble);
373
+ // ── Start ──
374
+ loadMascot(
375
+ function onSuccess() {
376
+ requestAnimationFrame(loop);
377
+ // Check if app is already ready
378
+ maybeTransition();
379
+ },
380
+ function onFail() {
381
+ // Image failed — leave HTML splash visible, hide canvas
382
+ canvas.style.display = 'none';
383
+ canvasPhase = 'bubble'; // prevent further transitions
384
+ }
385
+ );
386
+
387
+ // ══════════════════════════════════════════════════════════════════
388
+ // ── Widget Interaction (Panel, Toggle, Messages) ─────────────────
389
+ // ══════════════════════════════════════════════════════════════════
70
390
 
71
- // ── Toggle ──
72
391
  var isOpen = false;
73
392
 
74
393
  function toggle() {
394
+ if (canvasPhase !== 'bubble') return; // only allow toggle after animation settles
75
395
  isOpen = !isOpen;
76
396
  panel.classList.toggle('open', isOpen);
77
397
  backdrop.classList.toggle('open', isOpen);
78
- bubble.style.display = isOpen ? 'none' : 'flex';
398
+ canvas.style.display = isOpen ? 'none' : 'block';
79
399
  }
80
400
 
81
- bubble.addEventListener('click', toggle);
401
+ bubble.addEventListener('click', function () {
402
+ if (canvasPhase === 'bubble') toggle();
403
+ });
82
404
  backdrop.addEventListener('click', toggle);
83
405
 
84
406
  // Close on Escape
@@ -118,7 +440,8 @@
118
440
  try {
119
441
  if (sessionStorage.getItem('fluxy_widget_open') === '1') {
120
442
  sessionStorage.removeItem('fluxy_widget_open');
121
- toggle();
443
+ // Only restore if already in bubble phase
444
+ if (canvasPhase === 'bubble') toggle();
122
445
  }
123
446
  } catch (e) {}
124
447
 
@@ -128,10 +451,15 @@
128
451
  .then(function (r) { return r.json(); })
129
452
  .then(function (s) {
130
453
  if (s.onboard_complete !== 'true') {
131
- bubble.style.display = 'none';
454
+ if (canvasPhase === 'bubble') {
455
+ canvas.style.display = 'none';
456
+ } else {
457
+ hideAfterTransition = true;
458
+ }
132
459
  window.addEventListener('message', function onMsg(e) {
133
460
  if (e.data && e.data.type === 'fluxy:onboard-complete') {
134
- bubble.style.display = 'flex';
461
+ hideAfterTransition = false;
462
+ canvas.style.display = 'block';
135
463
  window.removeEventListener('message', onMsg);
136
464
  }
137
465
  });
@@ -145,29 +473,4 @@
145
473
  var awsScript = document.createElement('script');
146
474
  awsScript.src = '/fluxy/app-ws.js';
147
475
  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
476
  })();
@@ -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
  }));