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.
package/cli/commands/update.ts
CHANGED
|
@@ -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
package/supervisor/widget.js
CHANGED
|
@@ -6,11 +6,9 @@
|
|
|
6
6
|
// ── Styles ──
|
|
7
7
|
var style = document.createElement('style');
|
|
8
8
|
style.textContent = [
|
|
9
|
-
'#fluxy-widget-bubble{
|
|
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
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
var
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
398
|
+
canvas.style.display = isOpen ? 'none' : 'block';
|
|
79
399
|
}
|
|
80
400
|
|
|
81
|
-
bubble.addEventListener('click',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<!--
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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 = '
|
|
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='
|
|
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
|
|
44
|
+
// Show the dark background (used before reloads and on resume)
|
|
45
45
|
function showSplash() {
|
|
46
46
|
if (!splash) return;
|
|
47
|
-
splash.style.display = '
|
|
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
|
-
//
|
|
13
|
-
//
|
|
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
|
-
|
|
18
|
-
|
|
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
|
}));
|