@snowcone-app/sdk 0.1.10

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/dist/react.cjs ADDED
@@ -0,0 +1,755 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/react.ts
21
+ var react_exports = {};
22
+ __export(react_exports, {
23
+ ReactAdapter: () => ReactAdapter,
24
+ createReactComponent: () => createReactComponent,
25
+ useReactAdapter: () => useFrameworkAdapter,
26
+ useRealtimeMockup: () => useRealtimeMockup
27
+ });
28
+ module.exports = __toCommonJS(react_exports);
29
+
30
+ // src/realtime/react.ts
31
+ var import_react = require("react");
32
+
33
+ // src/realtime/websocket.ts
34
+ var RealtimeMockupService = class {
35
+ constructor(wsUrl = "wss://WS_URL_NOT_CONFIGURED.invalid/realtime") {
36
+ this.wsUrl = wsUrl;
37
+ }
38
+ ws = null;
39
+ config = null;
40
+ configSent = false;
41
+ sessionId = null;
42
+ isConfigured = false;
43
+ mockupResults = [];
44
+ status = "Disconnected";
45
+ logs = [];
46
+ lastError = null;
47
+ callbacks = {};
48
+ canvasBlobs = /* @__PURE__ */ new Map();
49
+ colors = /* @__PURE__ */ new Map();
50
+ lastSendTime = {};
51
+ throttleTimeouts = {};
52
+ // Request versioning to detect stale responses
53
+ // Increments each time we send a blob, so we can ignore old server responses
54
+ requestVersion = 0;
55
+ lastSentVersion = 0;
56
+ // Track latest sent version per placement to detect stale responses
57
+ latestSentVersionByPlacement = {};
58
+ // Feature flag: server now supports version in blob message
59
+ sendVersionInBlob = true;
60
+ setCallbacks(callbacks) {
61
+ this.callbacks = callbacks;
62
+ }
63
+ getState() {
64
+ return {
65
+ isConnected: this.ws?.readyState === WebSocket.OPEN,
66
+ sessionId: this.sessionId,
67
+ isConfigured: this.isConfigured,
68
+ mockupResults: [...this.mockupResults],
69
+ status: this.status,
70
+ logs: [...this.logs],
71
+ lastError: this.lastError
72
+ };
73
+ }
74
+ addLog(message, type = "info") {
75
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
76
+ const prefix = type === "sent" ? "\u2192 SENT: " : type === "received" ? "\u2190 RECEIVED: " : "\u2022 ";
77
+ const logMessage = `[${timestamp}] ${prefix}${message}`;
78
+ this.logs.push(logMessage);
79
+ this.callbacks.onLog?.(message, type);
80
+ }
81
+ connect() {
82
+ if (this.ws?.readyState === WebSocket.OPEN) {
83
+ return;
84
+ }
85
+ this.addLog(`Connecting to ${this.wsUrl}...`);
86
+ this.ws = new WebSocket(this.wsUrl);
87
+ this.ws.onopen = () => {
88
+ this.addLog("WebSocket connection opened");
89
+ this.status = "Connected";
90
+ };
91
+ this.ws.onmessage = (event) => {
92
+ try {
93
+ const data = JSON.parse(event.data);
94
+ this.addLog(JSON.stringify(data, null, 2), "received");
95
+ this.handleMessage(data);
96
+ } catch (error) {
97
+ this.addLog(`Received non-JSON message: ${event.data}`, "received");
98
+ }
99
+ };
100
+ this.ws.onclose = (event) => {
101
+ this.addLog(`WebSocket connection closed (code: ${event.code})`);
102
+ this.status = "Disconnected";
103
+ this.sessionId = null;
104
+ this.isConfigured = false;
105
+ this.configSent = false;
106
+ this.callbacks.onDisconnected?.();
107
+ };
108
+ this.ws.onerror = (error) => {
109
+ this.addLog(`WebSocket error: ${error}`);
110
+ this.status = "Disconnected";
111
+ };
112
+ }
113
+ handleMessage(data) {
114
+ switch (data.type) {
115
+ case "connected":
116
+ this.sessionId = data.sessionId || null;
117
+ this.status = `Connected (Session: ${data.sessionId || "N/A"})`;
118
+ this.addLog(`\u2705 Session established: ${data.sessionId}`);
119
+ if (data.sessionId) {
120
+ this.callbacks.onConnected?.(data.sessionId);
121
+ }
122
+ if (this.config && !this.configSent) {
123
+ this.addLog("\u{1F4E4} Auto-sending cached config after connection...");
124
+ setTimeout(() => {
125
+ if (this.config && !this.configSent) {
126
+ this.sendConfig(this.config);
127
+ }
128
+ }, 100);
129
+ }
130
+ break;
131
+ case "config_received":
132
+ case "configured":
133
+ this.isConfigured = true;
134
+ this.status = "Configured";
135
+ this.addLog("\u2705 Configuration accepted! You can now send blobs.");
136
+ this.callbacks.onConfigReceived?.();
137
+ const cachedCanvasCount = this.canvasBlobs.size;
138
+ const cachedColorCount = this.colors.size;
139
+ if (cachedCanvasCount > 0 || cachedColorCount > 0) {
140
+ this.addLog(`\u{1F4E6} Auto-sending cached data: ${cachedCanvasCount} canvas blobs, ${cachedColorCount} colors`);
141
+ setTimeout(() => {
142
+ this.canvasBlobs.forEach((blob, placement) => {
143
+ this.sendCanvasBlob(placement, blob, 1, 0, false);
144
+ });
145
+ this.colors.forEach((color, placement) => {
146
+ this.sendColorBlob(placement, color);
147
+ });
148
+ }, 100);
149
+ }
150
+ break;
151
+ case "blob_received":
152
+ const placementName = data.placement || "unknown";
153
+ const missingCount = data.missingPlacements?.length || 0;
154
+ this.addLog(`\u2705 Blob received for "${placementName}" - ${missingCount} placements still needed`);
155
+ this.callbacks.onBlobReceived?.(placementName);
156
+ break;
157
+ case "have_all_blobs":
158
+ this.addLog("\u{1F389} All blobs received! Starting render...");
159
+ break;
160
+ case "rendering_started":
161
+ this.addLog("\u{1F3A8} Mockup rendering has started...");
162
+ break;
163
+ case "mockup_rendered":
164
+ if (data.imageUrl && data.mockupId) {
165
+ const responseVersion = data.requestVersion;
166
+ const responsePlacement = data.placement;
167
+ if (responseVersion !== void 0 && responsePlacement) {
168
+ const latestVersion = this.latestSentVersionByPlacement[responsePlacement];
169
+ if (latestVersion !== void 0 && responseVersion < latestVersion) {
170
+ this.addLog(`\u23ED\uFE0F Ignoring stale mockup v${responseVersion} for "${responsePlacement}" (latest sent: v${latestVersion})`);
171
+ break;
172
+ }
173
+ }
174
+ const mockupResult = {
175
+ mockupId: data.mockupId,
176
+ imageUrl: data.imageUrl,
177
+ renderUrl: data.renderUrl || data.imageUrl,
178
+ imageSize: data.imageSize || 0,
179
+ requestVersion: responseVersion,
180
+ placement: responsePlacement
181
+ };
182
+ const existingIndex = this.mockupResults.findIndex((m) => m.mockupId === data.mockupId);
183
+ if (existingIndex >= 0) {
184
+ this.mockupResults[existingIndex] = mockupResult;
185
+ } else {
186
+ this.mockupResults.push(mockupResult);
187
+ }
188
+ this.addLog(`\u2705 Mockup rendered v${responseVersion ?? "?"} for "${responsePlacement ?? "?"}" (${data.imageSize} bytes)`);
189
+ this.callbacks.onMockupRendered?.(mockupResult);
190
+ } else {
191
+ const missing = [
192
+ !data.imageUrl && "imageUrl",
193
+ !data.mockupId && "mockupId"
194
+ ].filter(Boolean);
195
+ this.addLog(`\u26A0\uFE0F mockup_rendered message dropped: missing required fields [${missing.join(", ")}]. Full data keys: [${Object.keys(data).join(", ")}]`);
196
+ }
197
+ break;
198
+ case "all_mockups_rendered":
199
+ if (data.mockups) {
200
+ const freshMockups = data.mockups.filter((mockup) => {
201
+ if (mockup.requestVersion !== void 0 && mockup.placement) {
202
+ const latestVersion = this.latestSentVersionByPlacement[mockup.placement];
203
+ if (latestVersion !== void 0 && mockup.requestVersion < latestVersion) {
204
+ this.addLog(`\u23ED\uFE0F Filtering stale mockup v${mockup.requestVersion} for "${mockup.placement}" (latest: v${latestVersion})`);
205
+ return false;
206
+ }
207
+ }
208
+ return true;
209
+ });
210
+ this.mockupResults = freshMockups;
211
+ this.addLog(`\u{1F389} All mockups rendered: ${freshMockups.length} fresh (${data.mockups.length - freshMockups.length} stale filtered)`);
212
+ this.callbacks.onAllMockupsRendered?.(freshMockups);
213
+ } else {
214
+ this.addLog(`\u26A0\uFE0F all_mockups_rendered message dropped: missing 'mockups' array. Full data keys: [${Object.keys(data).join(", ")}]`);
215
+ }
216
+ break;
217
+ case "error":
218
+ const errorMessage = data.message || "Unknown error occurred";
219
+ this.lastError = errorMessage;
220
+ this.addLog(`\u274C Server Error: ${errorMessage}`);
221
+ this.callbacks.onError?.(errorMessage);
222
+ break;
223
+ default:
224
+ this.addLog(`\u{1F914} Unknown message type: ${data.type}`, "received");
225
+ break;
226
+ }
227
+ }
228
+ disconnect() {
229
+ if (this.ws) {
230
+ this.ws.close();
231
+ this.ws = null;
232
+ }
233
+ }
234
+ sendConfig(config) {
235
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
236
+ this.addLog("WebSocket not connected, caching config");
237
+ this.config = config;
238
+ this.configSent = false;
239
+ return false;
240
+ }
241
+ if (!this.sessionId) {
242
+ this.addLog("WebSocket connected but no session yet, caching config");
243
+ this.config = config;
244
+ this.configSent = false;
245
+ return false;
246
+ }
247
+ const hasConfigChanged = !this.config || JSON.stringify(this.config) !== JSON.stringify(config);
248
+ if (this.configSent && !hasConfigChanged) {
249
+ this.addLog("Config already sent and unchanged, skipping duplicate");
250
+ return false;
251
+ }
252
+ if (this.configSent && hasConfigChanged) {
253
+ const isOnlyMockupIdsChange = this.config && this.config.productId === config.productId && this.config.variantId === config.variantId;
254
+ if (isOnlyMockupIdsChange) {
255
+ this.addLog("\u{1F504} MockupIds changed, keeping cached blobs (server will reuse)");
256
+ this.isConfigured = false;
257
+ this.mockupResults = [];
258
+ } else {
259
+ this.addLog("\u{1F504} Product/variant changed, full reset");
260
+ this.isConfigured = false;
261
+ this.mockupResults = [];
262
+ this.canvasBlobs.clear();
263
+ this.colors.clear();
264
+ this.lastSendTime = {};
265
+ Object.values(this.throttleTimeouts).forEach((timeout) => clearTimeout(timeout));
266
+ this.throttleTimeouts = {};
267
+ this.requestVersion = 0;
268
+ this.lastSentVersion = 0;
269
+ this.latestSentVersionByPlacement = {};
270
+ this.addLog("\u{1F9F9} Cleared all cached canvas/color data for new product");
271
+ }
272
+ }
273
+ this.config = config;
274
+ this.configSent = true;
275
+ const message = {
276
+ type: "config",
277
+ config
278
+ };
279
+ const messageStr = JSON.stringify(message);
280
+ this.addLog(`\u{1F4E4} Sending config: ${JSON.stringify(message, null, 2)}`, "sent");
281
+ this.ws.send(messageStr);
282
+ return true;
283
+ }
284
+ /**
285
+ * Update only the mockupIds without changing other config.
286
+ * Server will use already-cached blobs to render the requested mockups.
287
+ * This is the preferred method for priority-based rendering.
288
+ */
289
+ updateMockupIds(mockupIds) {
290
+ if (!this.config) {
291
+ this.addLog("Cannot update mockupIds: no config set");
292
+ return false;
293
+ }
294
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
295
+ this.addLog("Cannot update mockupIds: WebSocket not connected");
296
+ return false;
297
+ }
298
+ const updatedConfig = {
299
+ ...this.config,
300
+ mockupIds
301
+ };
302
+ this.addLog(`\u{1F3AF} Updating mockupIds to: [${mockupIds.join(", ")}]`);
303
+ return this.sendConfig(updatedConfig);
304
+ }
305
+ /**
306
+ * Update placementSettings without changing other config.
307
+ * Used to override scaleMode when canvas editor is active.
308
+ */
309
+ updatePlacementSettings(placementSettings) {
310
+ if (!this.config) {
311
+ this.addLog("Cannot update placementSettings: no config set");
312
+ return false;
313
+ }
314
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
315
+ this.addLog("Cannot update placementSettings: WebSocket not connected");
316
+ return false;
317
+ }
318
+ const updatedConfig = {
319
+ ...this.config,
320
+ placementSettings
321
+ };
322
+ return this.sendConfig(updatedConfig);
323
+ }
324
+ sendCanvasBlob(placement, blob, mockupCount = 1, baseThrottleMs = 1e3, notifyCallback = true) {
325
+ this.canvasBlobs.set(placement, blob);
326
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
327
+ return false;
328
+ }
329
+ if (baseThrottleMs <= 0) {
330
+ this.sendBlobImmediately(placement, blob, notifyCallback);
331
+ this.lastSendTime[placement] = Date.now();
332
+ return true;
333
+ }
334
+ const throttleMs = baseThrottleMs * mockupCount;
335
+ const now = Date.now();
336
+ const lastSendTime = this.lastSendTime[placement] || 0;
337
+ const timeSinceLastSend = now - lastSendTime;
338
+ const hasNeverSent = lastSendTime === 0;
339
+ if (hasNeverSent || timeSinceLastSend >= throttleMs) {
340
+ this.sendBlobImmediately(placement, blob, notifyCallback);
341
+ this.lastSendTime[placement] = now;
342
+ } else if (!this.throttleTimeouts[placement]) {
343
+ const delayTime = throttleMs - timeSinceLastSend;
344
+ this.throttleTimeouts[placement] = setTimeout(() => {
345
+ const latestBlob = this.canvasBlobs.get(placement);
346
+ if (latestBlob && this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
347
+ this.sendBlobImmediately(placement, latestBlob, notifyCallback);
348
+ this.lastSendTime[placement] = Date.now();
349
+ }
350
+ delete this.throttleTimeouts[placement];
351
+ }, delayTime);
352
+ }
353
+ return true;
354
+ }
355
+ sendBlobImmediately(placement, blob, notifyCallback = true) {
356
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
357
+ this.lastSentVersion = ++this.requestVersion;
358
+ this.latestSentVersionByPlacement[placement] = this.lastSentVersion;
359
+ const versionToSend = this.lastSentVersion;
360
+ const reader = new FileReader();
361
+ reader.onload = (e) => {
362
+ if (e.target?.result && this.ws) {
363
+ const imageBytes = new Uint8Array(e.target.result);
364
+ let combined;
365
+ if (this.sendVersionInBlob) {
366
+ const headerBytes = new TextEncoder().encode(`${placement}
367
+ ${versionToSend}
368
+ `);
369
+ combined = new Uint8Array(headerBytes.length + imageBytes.length);
370
+ combined.set(headerBytes, 0);
371
+ combined.set(imageBytes, headerBytes.length);
372
+ } else {
373
+ const headerBytes = new TextEncoder().encode(`${placement}
374
+ `);
375
+ combined = new Uint8Array(headerBytes.length + imageBytes.length);
376
+ combined.set(headerBytes, 0);
377
+ combined.set(imageBytes, headerBytes.length);
378
+ }
379
+ this.ws.send(combined.buffer);
380
+ this.addLog(`Sent canvas blob for placement "${placement}" (${imageBytes.length} bytes, v${versionToSend})`, "sent");
381
+ if (notifyCallback) {
382
+ this.callbacks.onBlobSent?.(placement);
383
+ }
384
+ }
385
+ };
386
+ reader.readAsArrayBuffer(blob);
387
+ }
388
+ /**
389
+ * Enable sending version number in blob messages.
390
+ * Call this once the server supports the new format: <placement>\n<version>\n<blob>
391
+ */
392
+ enableVersionInBlob(enabled = true) {
393
+ this.sendVersionInBlob = enabled;
394
+ this.addLog(`Version in blob ${enabled ? "enabled" : "disabled"}`);
395
+ }
396
+ /**
397
+ * Flush all pending throttled blobs immediately.
398
+ * Call this when the user finishes an action (e.g., mouse up after drag/flip)
399
+ * to ensure the final state is sent without waiting for throttle.
400
+ */
401
+ flushPendingBlobs() {
402
+ Object.keys(this.throttleTimeouts).forEach((placement) => {
403
+ clearTimeout(this.throttleTimeouts[placement]);
404
+ delete this.throttleTimeouts[placement];
405
+ });
406
+ if (this.ws?.readyState === WebSocket.OPEN && this.isConfigured) {
407
+ this.canvasBlobs.forEach((blob, placement) => {
408
+ this.sendBlobImmediately(placement, blob);
409
+ this.lastSendTime[placement] = Date.now();
410
+ });
411
+ this.addLog(`\u{1F680} Flushed ${this.canvasBlobs.size} pending blob(s)`);
412
+ }
413
+ }
414
+ /**
415
+ * Check if there are pending blobs waiting to be sent (in throttle queue)
416
+ */
417
+ hasPendingBlobs() {
418
+ return Object.keys(this.throttleTimeouts).length > 0;
419
+ }
420
+ sendColorBlob(placement, hexColor) {
421
+ this.colors.set(placement, hexColor);
422
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !this.isConfigured) {
423
+ return false;
424
+ }
425
+ const message = placement + "\ncolor:" + hexColor;
426
+ const messageBytes = new TextEncoder().encode(message);
427
+ this.ws.send(messageBytes.buffer);
428
+ this.addLog(`Sent color "${hexColor}" for placement "${placement}"`, "sent");
429
+ return true;
430
+ }
431
+ createEmptyCanvasBlob(width = 400, height = 400) {
432
+ return new Promise((resolve, reject) => {
433
+ const canvas = document.createElement("canvas");
434
+ canvas.width = width;
435
+ canvas.height = height;
436
+ const ctx = canvas.getContext("2d");
437
+ if (!ctx) {
438
+ reject(new Error("Could not get canvas context"));
439
+ return;
440
+ }
441
+ ctx.fillStyle = "white";
442
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
443
+ canvas.toBlob(
444
+ (blob) => {
445
+ if (blob) {
446
+ resolve(blob);
447
+ } else {
448
+ reject(new Error("Could not create blob from canvas"));
449
+ }
450
+ },
451
+ "image/png",
452
+ 0.8
453
+ );
454
+ });
455
+ }
456
+ async sendInitialEmptyCanvases(placements) {
457
+ this.addLog(`\u{1F3A8} Creating initial empty canvases for ${placements.length} placements...`);
458
+ let emptyCanvasCount = 0;
459
+ let preservedCanvasCount = 0;
460
+ for (const placement of placements) {
461
+ try {
462
+ const existingBlob = this.canvasBlobs.get(placement.label);
463
+ if (existingBlob) {
464
+ this.addLog(`\u{1F3AF} Preserving existing canvas for "${placement.label}" (${existingBlob.size} bytes)`);
465
+ preservedCanvasCount++;
466
+ if (this.isConfigured) {
467
+ this.sendCanvasBlob(placement.label, existingBlob);
468
+ }
469
+ } else {
470
+ const emptyBlob = await this.createEmptyCanvasBlob(
471
+ placement.width || 400,
472
+ placement.height || 400
473
+ );
474
+ const blobCachedDuringAwait = this.canvasBlobs.get(placement.label);
475
+ if (blobCachedDuringAwait) {
476
+ this.addLog(`\u{1F3AF} Skipping empty canvas for "${placement.label}" \u2014 real blob was cached during creation`);
477
+ preservedCanvasCount++;
478
+ if (this.isConfigured) {
479
+ this.sendCanvasBlob(placement.label, blobCachedDuringAwait);
480
+ }
481
+ } else {
482
+ this.canvasBlobs.set(placement.label, emptyBlob);
483
+ this.addLog(`\u2705 Created empty canvas for "${placement.label}" (${emptyBlob.size} bytes)`);
484
+ emptyCanvasCount++;
485
+ if (this.isConfigured) {
486
+ this.sendCanvasBlob(placement.label, emptyBlob);
487
+ }
488
+ }
489
+ }
490
+ } catch (error) {
491
+ this.addLog(`\u274C Failed to process canvas for "${placement.label}": ${error}`);
492
+ }
493
+ }
494
+ this.addLog(`\u{1F389} Canvas processing complete: ${preservedCanvasCount} preserved, ${emptyCanvasCount} created empty`);
495
+ }
496
+ setInitialData(canvasBlobs, colors) {
497
+ const existingCanvasCount = this.canvasBlobs.size;
498
+ if (existingCanvasCount === 0) {
499
+ this.canvasBlobs = new Map(canvasBlobs);
500
+ this.addLog(`Set initial canvas blobs: ${canvasBlobs.size} new blobs`);
501
+ } else {
502
+ this.addLog(`Preserving existing canvas blobs: ${existingCanvasCount} blobs (ignoring ${canvasBlobs.size} new empty blobs)`);
503
+ }
504
+ this.colors = new Map(colors);
505
+ this.addLog(`Set initial colors: ${colors.size} colors`);
506
+ }
507
+ clearLogs() {
508
+ this.logs = [];
509
+ }
510
+ clearMockups() {
511
+ this.mockupResults = [];
512
+ }
513
+ };
514
+
515
+ // src/realtime/react.ts
516
+ function useRealtimeMockup(options = {}) {
517
+ const serviceRef = (0, import_react.useRef)(null);
518
+ const [state, setState] = (0, import_react.useState)({
519
+ isConnected: false,
520
+ sessionId: null,
521
+ isConfigured: false,
522
+ mockupResults: [],
523
+ status: "Disconnected",
524
+ logs: [],
525
+ lastError: null
526
+ });
527
+ (0, import_react.useEffect)(() => {
528
+ const service = new RealtimeMockupService(options.wsUrl);
529
+ serviceRef.current = service;
530
+ service.setCallbacks({
531
+ onConnected: (sessionId) => {
532
+ setState(service.getState());
533
+ options.onConnected?.(sessionId);
534
+ },
535
+ onDisconnected: () => {
536
+ setState(service.getState());
537
+ options.onDisconnected?.();
538
+ },
539
+ onConfigReceived: () => {
540
+ setState(service.getState());
541
+ options.onConfigReceived?.();
542
+ },
543
+ onBlobReceived: (placement) => {
544
+ options.onBlobReceived?.(placement);
545
+ },
546
+ onBlobSent: (placement) => {
547
+ options.onBlobSent?.(placement);
548
+ },
549
+ onMockupRendered: (result) => {
550
+ setState(service.getState());
551
+ options.onMockupRendered?.(result);
552
+ },
553
+ onAllMockupsRendered: (results) => {
554
+ setState(service.getState());
555
+ options.onAllMockupsRendered?.(results);
556
+ },
557
+ onError: (error) => {
558
+ setState(service.getState());
559
+ options.onError?.(error);
560
+ }
561
+ // REMOVED: onLog callback was causing setState on every log message,
562
+ // which triggered re-renders during canvas drag operations
563
+ });
564
+ return () => {
565
+ service.disconnect();
566
+ };
567
+ }, [options.wsUrl]);
568
+ const connect = (0, import_react.useCallback)(() => {
569
+ serviceRef.current?.connect();
570
+ }, []);
571
+ const disconnect = (0, import_react.useCallback)(() => {
572
+ serviceRef.current?.disconnect();
573
+ }, []);
574
+ const sendConfig = (0, import_react.useCallback)((config) => {
575
+ return serviceRef.current?.sendConfig(config) || false;
576
+ }, []);
577
+ const updateMockupIds = (0, import_react.useCallback)((mockupIds) => {
578
+ return serviceRef.current?.updateMockupIds(mockupIds) || false;
579
+ }, []);
580
+ const updatePlacementSettings = (0, import_react.useCallback)((settings) => {
581
+ return serviceRef.current?.updatePlacementSettings(settings) || false;
582
+ }, []);
583
+ const sendCanvasBlob = (0, import_react.useCallback)((placement, blob, mockupCount, baseThrottleMs) => {
584
+ return serviceRef.current?.sendCanvasBlob(placement, blob, mockupCount, baseThrottleMs) || false;
585
+ }, []);
586
+ const sendColorBlob = (0, import_react.useCallback)((placement, hexColor) => {
587
+ return serviceRef.current?.sendColorBlob(placement, hexColor) || false;
588
+ }, []);
589
+ const createEmptyCanvasBlob = (0, import_react.useCallback)((width, height) => {
590
+ return serviceRef.current?.createEmptyCanvasBlob(width, height) || Promise.reject(new Error("Service not initialized"));
591
+ }, []);
592
+ const sendInitialEmptyCanvases = (0, import_react.useCallback)(async (placements) => {
593
+ await serviceRef.current?.sendInitialEmptyCanvases(placements);
594
+ }, []);
595
+ const setInitialData = (0, import_react.useCallback)((canvasBlobs, colors) => {
596
+ serviceRef.current?.setInitialData(canvasBlobs, colors);
597
+ }, []);
598
+ const clearLogs = (0, import_react.useCallback)(() => {
599
+ serviceRef.current?.clearLogs();
600
+ setState(serviceRef.current?.getState() || state);
601
+ }, [state]);
602
+ const clearMockups = (0, import_react.useCallback)(() => {
603
+ serviceRef.current?.clearMockups();
604
+ setState(serviceRef.current?.getState() || state);
605
+ }, [state]);
606
+ const flushPendingBlobs = (0, import_react.useCallback)(() => {
607
+ serviceRef.current?.flushPendingBlobs();
608
+ }, []);
609
+ const hasPendingBlobs = (0, import_react.useCallback)(() => {
610
+ return serviceRef.current?.hasPendingBlobs() || false;
611
+ }, []);
612
+ return {
613
+ ...state,
614
+ connect,
615
+ disconnect,
616
+ sendConfig,
617
+ updateMockupIds,
618
+ updatePlacementSettings,
619
+ sendCanvasBlob,
620
+ sendColorBlob,
621
+ createEmptyCanvasBlob,
622
+ sendInitialEmptyCanvases,
623
+ setInitialData,
624
+ clearLogs,
625
+ clearMockups,
626
+ flushPendingBlobs,
627
+ hasPendingBlobs
628
+ };
629
+ }
630
+
631
+ // src/framework/adapters/react.ts
632
+ var ReactAdapter = class {
633
+ name = "react";
634
+ React;
635
+ hooks;
636
+ constructor(React, hooks) {
637
+ this.React = React;
638
+ this.hooks = hooks || React;
639
+ }
640
+ /**
641
+ * Create a stateful value using useState
642
+ */
643
+ createState(initialValue) {
644
+ const [value, setValue] = this.hooks.useState(initialValue);
645
+ return {
646
+ get: () => value,
647
+ set: (newValue) => {
648
+ setValue(newValue);
649
+ },
650
+ subscribe: (callback) => {
651
+ this.hooks.useEffect(() => {
652
+ callback(value);
653
+ }, [value]);
654
+ return () => {
655
+ };
656
+ }
657
+ };
658
+ }
659
+ /**
660
+ * Create a context provider/consumer
661
+ */
662
+ createContext(name) {
663
+ const Context = this.React.createContext(void 0);
664
+ return {
665
+ provide: (value) => {
666
+ return this.React.createElement(Context.Provider, { value });
667
+ },
668
+ consume: () => {
669
+ return this.hooks.useContext(Context);
670
+ },
671
+ subscribe: (callback) => {
672
+ const value = this.hooks.useContext(Context);
673
+ this.hooks.useEffect(() => {
674
+ if (value !== void 0) {
675
+ callback(value);
676
+ }
677
+ }, [value]);
678
+ return () => {
679
+ };
680
+ }
681
+ };
682
+ }
683
+ /**
684
+ * Register lifecycle hooks
685
+ */
686
+ useLifecycle(lifecycle) {
687
+ this.hooks.useEffect(() => {
688
+ lifecycle.onMount?.();
689
+ return () => {
690
+ lifecycle.onUnmount?.();
691
+ };
692
+ }, []);
693
+ this.hooks.useEffect(() => {
694
+ lifecycle.onUpdate?.({});
695
+ });
696
+ }
697
+ /**
698
+ * Register event handlers
699
+ */
700
+ useEvents(handlers) {
701
+ handlers.forEach(({ name, handler }) => {
702
+ this.hooks.useCallback(handler, []);
703
+ });
704
+ }
705
+ /**
706
+ * Create a ref to a DOM element
707
+ */
708
+ createRef() {
709
+ return this.hooks.useRef(null);
710
+ }
711
+ /**
712
+ * Render a component
713
+ */
714
+ render(component, props) {
715
+ return component.render(this);
716
+ }
717
+ /**
718
+ * Get React-specific utilities
719
+ */
720
+ getUtilities() {
721
+ return {
722
+ batchUpdates: (callback) => {
723
+ callback();
724
+ },
725
+ nextTick: (callback) => {
726
+ Promise.resolve().then(callback);
727
+ },
728
+ computed: (deps, compute) => {
729
+ return this.hooks.useMemo(compute, deps);
730
+ },
731
+ memo: (value, deps) => {
732
+ return this.hooks.useMemo(() => value, deps);
733
+ }
734
+ };
735
+ }
736
+ };
737
+ function createReactComponent(descriptor, React) {
738
+ return function Component(props) {
739
+ const adapter = new ReactAdapter(React, React);
740
+ if (descriptor.lifecycle) {
741
+ adapter.useLifecycle(descriptor.lifecycle);
742
+ }
743
+ return adapter.render(descriptor, props);
744
+ };
745
+ }
746
+ function useFrameworkAdapter(React) {
747
+ return new ReactAdapter(React, React);
748
+ }
749
+ // Annotate the CommonJS export names for ESM import in node:
750
+ 0 && (module.exports = {
751
+ ReactAdapter,
752
+ createReactComponent,
753
+ useReactAdapter,
754
+ useRealtimeMockup
755
+ });