@snowcone-app/ui 0.4.13 → 0.8.0
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/CHANGELOG.md +47 -0
- package/dist/index.cjs +143 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -4
- package/dist/index.d.ts +59 -4
- package/dist/index.js +143 -28
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/CanvasIsolationBoundary.tsx +6 -0
- package/src/composed/HeroProductImage.tsx +15 -0
- package/src/ecommerce/stories/PDPInfoPanel.stories.tsx +2 -1
- package/src/ecommerce/stories/PDPLayout.stories.tsx +2 -1
- package/src/patterns/RealtimeProvider.tsx +84 -3
- package/src/primitives/stories/Accordion.stories.tsx +2 -2
- package/src/services/CanvasExportService.ts +97 -28
- package/src/services/__tests__/CanvasExportService.leadingEdge.test.ts +132 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#2249](https://github.com/snowcone-app/snowcone-monorepo/pull/2249) [`19f7ee5`](https://github.com/snowcone-app/snowcone-monorepo/commit/19f7ee583638ccedd8fd5f309ad3e5ba6a06c0f9) Thanks [@kevinsproles](https://github.com/kevinsproles)! - A mockup whose scene failed render parity is refused by the server (`E_MOCKUP_NOT_BLESSED`) instead of silently rendering on the slow path. `RealtimeRenderError` gains `mockupId` for per-mockup errors; `RealtimeProvider` exposes `unavailableMockups` (mockupId → error) and `HeroProductImage` shows that mockup as unavailable (`data-mockup-unavailable`) while the others keep streaming.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`19f7ee5`](https://github.com/snowcone-app/snowcone-monorepo/commit/19f7ee583638ccedd8fd5f309ad3e5ba6a06c0f9)]:
|
|
12
|
+
- @snowcone-app/sdk@0.23.0
|
|
13
|
+
|
|
14
|
+
## 0.7.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- [#2240](https://github.com/snowcone-app/snowcone-monorepo/pull/2240) [`61c4826`](https://github.com/snowcone-app/snowcone-monorepo/commit/61c48267d77b93628ef525df5a6aee66421d67f7) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Realtime send policy: pipeline depth two. `CanvasExportService` now keeps up to two edits in flight (`maxInFlight`, default 2) before holding the latest state — the box's window is ~30 ms of a ~96 ms edit→frame, so one in flight left it idle ~70% of every drag cycle. `RealtimeProvider` settles once per landed send (every mockup of a `requestVersion`) instead of on the first frame after any send, using the SDK's new `getLastSentVersion()` on the realtime service and hook.
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- Updated dependencies [[`61c4826`](https://github.com/snowcone-app/snowcone-monorepo/commit/61c48267d77b93628ef525df5a6aee66421d67f7)]:
|
|
23
|
+
- @snowcone-app/sdk@0.22.0
|
|
24
|
+
|
|
25
|
+
## 0.6.0
|
|
26
|
+
|
|
27
|
+
### Minor Changes
|
|
28
|
+
|
|
29
|
+
- [#2231](https://github.com/snowcone-app/snowcone-monorepo/pull/2231) [`ecc5bde`](https://github.com/snowcone-app/snowcone-monorepo/commit/ecc5bde83c091a5c0de179903b18c67d7c674fc0) Thanks [@kevinsproles](https://github.com/kevinsproles)! - `RealtimeProvider` accepts `initialGrant` — a session grant minted server-side while the page rendered. The first connect spends it (if it has more than 30 s of life left) instead of paying a grant round trip; renewals and reconnects still go through `grantUrl`.
|
|
30
|
+
|
|
31
|
+
## 0.5.0
|
|
32
|
+
|
|
33
|
+
### Minor Changes
|
|
34
|
+
|
|
35
|
+
- [#2230](https://github.com/snowcone-app/snowcone-monorepo/pull/2230) [`9f99c8a`](https://github.com/snowcone-app/snowcone-monorepo/commit/9f99c8a362f38b26d57c62a80a3845080338478f) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Realtime editor send policy is now one-in-flight: a change goes out immediately when no frame is in flight; while a frame is in flight the latest state is held and sent the moment that frame lands (`subscribePipelineSettled`), so the render pipeline is never idle during a drag and never more than one edit behind. `handleInteractionEnd()` (exported from the canvas isolation boundary) flushes a held state on pointer release. Replaces the fixed 300 ms settle / 1 s max-wait debounce.
|
|
36
|
+
|
|
37
|
+
## 0.4.15
|
|
38
|
+
|
|
39
|
+
### Patch Changes
|
|
40
|
+
|
|
41
|
+
- Updated dependencies [[`aaa73cc`](https://github.com/snowcone-app/snowcone-monorepo/commit/aaa73ccb4b2d4c7c107b66f18be29f67032422b8)]:
|
|
42
|
+
- @snowcone-app/sdk@0.21.0
|
|
43
|
+
|
|
44
|
+
## 0.4.14
|
|
45
|
+
|
|
46
|
+
### Patch Changes
|
|
47
|
+
|
|
48
|
+
- [#1638](https://github.com/snowcone-app/snowcone-monorepo/pull/1638) [`045645a`](https://github.com/snowcone-app/snowcone-monorepo/commit/045645ae6d9d7683bc0cb8b487da9440af151ef2) Thanks [@kevinsproles](https://github.com/kevinsproles)! - CanvasExportService: the first canvas change after idle is sent to the realtime renderer immediately (leading edge) instead of waiting for the 300ms settle. Editor actions emit two state changes ~250ms apart, so the trailing-only debounce left ~550ms of dead time before anything was sent; the settled state still follows as before.
|
|
49
|
+
|
|
3
50
|
## 0.4.13
|
|
4
51
|
|
|
5
52
|
### Patch Changes
|
package/dist/index.cjs
CHANGED
|
@@ -412,7 +412,7 @@ var brand_default = {
|
|
|
412
412
|
display_name: "Snowcone",
|
|
413
413
|
tagline: "Your art, on everything.",
|
|
414
414
|
seo_title: "Snowcone \u2014 Your art, on everything.",
|
|
415
|
-
legal_entity: "
|
|
415
|
+
legal_entity: "Art Shop AI LLC",
|
|
416
416
|
lang: "en",
|
|
417
417
|
wordmark: "/assets/wordmark-en.svg"
|
|
418
418
|
},
|
|
@@ -422,7 +422,7 @@ var brand_default = {
|
|
|
422
422
|
display_name: "\u96EA\u7ED8 (Snowcone)",
|
|
423
423
|
tagline: "\u5728\u96EA\u767D\u4E4B\u4E0A\uFF0C\u7ED8\u51FA\u4F60\u7684\u8272\u5F69\u3002",
|
|
424
424
|
seo_title: "\u96EA\u7ED8 (Snowcone) \u2014 \u5728\u96EA\u767D\u4E4B\u4E0A\uFF0C\u7ED8\u51FA\u4F60\u7684\u8272\u5F69",
|
|
425
|
-
legal_entity: "
|
|
425
|
+
legal_entity: "Art Shop AI LLC",
|
|
426
426
|
lang: "zh",
|
|
427
427
|
wordmark: "/assets/wordmark-zh.svg"
|
|
428
428
|
},
|
|
@@ -432,7 +432,7 @@ var brand_default = {
|
|
|
432
432
|
display_name: "Snowcone",
|
|
433
433
|
tagline: "Tu arte, en todo.",
|
|
434
434
|
seo_title: "Snowcone \u2014 Tu arte, en todo.",
|
|
435
|
-
legal_entity: "
|
|
435
|
+
legal_entity: "Art Shop AI LLC",
|
|
436
436
|
lang: "es",
|
|
437
437
|
wordmark: "/assets/wordmark-en.svg"
|
|
438
438
|
},
|
|
@@ -442,7 +442,7 @@ var brand_default = {
|
|
|
442
442
|
display_name: "Snowcone",
|
|
443
443
|
tagline: "Votre art, sur tout.",
|
|
444
444
|
seo_title: "Snowcone \u2014 Votre art, sur tout.",
|
|
445
|
-
legal_entity: "
|
|
445
|
+
legal_entity: "Art Shop AI LLC",
|
|
446
446
|
lang: "fr",
|
|
447
447
|
wordmark: "/assets/wordmark-en.svg"
|
|
448
448
|
}
|
|
@@ -6006,13 +6006,18 @@ function RealtimeProvider({
|
|
|
6006
6006
|
autoEnable = false,
|
|
6007
6007
|
shop,
|
|
6008
6008
|
grantUrl,
|
|
6009
|
+
initialGrant,
|
|
6009
6010
|
delivery = "cropped"
|
|
6010
6011
|
}) {
|
|
6011
6012
|
const productContext = useProductOptional();
|
|
6012
6013
|
const mockupBg = useMockupBackgroundOptional();
|
|
6013
6014
|
const [isEnabled, setIsEnabled] = (0, import_react25.useState)(autoEnable);
|
|
6014
6015
|
const useSessionGrant = Boolean(shop && grantUrl);
|
|
6016
|
+
const initialGrantRef = (0, import_react25.useRef)(initialGrant);
|
|
6015
6017
|
const getToken = (0, import_react25.useCallback)(async () => {
|
|
6018
|
+
const minted = initialGrantRef.current;
|
|
6019
|
+
initialGrantRef.current = void 0;
|
|
6020
|
+
if (minted && minted.expiresAt * 1e3 - Date.now() > 3e4) return minted;
|
|
6016
6021
|
const res = await fetch(grantUrl, {
|
|
6017
6022
|
method: "POST",
|
|
6018
6023
|
headers: { "Content-Type": "application/json" },
|
|
@@ -6035,6 +6040,9 @@ function RealtimeProvider({
|
|
|
6035
6040
|
const [spreadCropsVersion, setSpreadCropsVersion] = (0, import_react25.useState)(0);
|
|
6036
6041
|
const lastSentSpreadCropsRef = (0, import_react25.useRef)("null");
|
|
6037
6042
|
const lastSentColorsRef = (0, import_react25.useRef)({});
|
|
6043
|
+
const [unavailableMockups, setUnavailableMockups] = (0, import_react25.useState)(
|
|
6044
|
+
() => /* @__PURE__ */ new Map()
|
|
6045
|
+
);
|
|
6038
6046
|
const realtimeMockup = (0, import_react26.useRealtimeMockup)({
|
|
6039
6047
|
wsUrl,
|
|
6040
6048
|
getToken: useSessionGrant ? getToken : void 0,
|
|
@@ -6064,6 +6072,10 @@ function RealtimeProvider({
|
|
|
6064
6072
|
},
|
|
6065
6073
|
onError: (error, detail) => {
|
|
6066
6074
|
console.error(`[RealtimeProvider] Render error [${detail.code}]:`, error);
|
|
6075
|
+
if (detail.mockupId) {
|
|
6076
|
+
const id = detail.mockupId;
|
|
6077
|
+
setUnavailableMockups((prev) => prev.get(id)?.code === detail.code ? prev : new Map(prev).set(id, detail));
|
|
6078
|
+
}
|
|
6067
6079
|
}
|
|
6068
6080
|
});
|
|
6069
6081
|
const subscribeRTCTiming = (0, import_react25.useCallback)((_callback) => {
|
|
@@ -6084,7 +6096,8 @@ function RealtimeProvider({
|
|
|
6084
6096
|
connect,
|
|
6085
6097
|
disconnect,
|
|
6086
6098
|
flushPendingBlobs,
|
|
6087
|
-
hasPendingBlobs
|
|
6099
|
+
hasPendingBlobs,
|
|
6100
|
+
getLastSentVersion
|
|
6088
6101
|
} = realtimeMockup;
|
|
6089
6102
|
const mockupResultsRef = (0, import_react25.useRef)([]);
|
|
6090
6103
|
const mockupSubscribersRef = (0, import_react25.useRef)(
|
|
@@ -6106,6 +6119,8 @@ function RealtimeProvider({
|
|
|
6106
6119
|
const blobQueuedGenRef = (0, import_react25.useRef)(0);
|
|
6107
6120
|
const hasBlobInFlightRef = (0, import_react25.useRef)(false);
|
|
6108
6121
|
const pipelineResetCooldownRef = (0, import_react25.useRef)(false);
|
|
6122
|
+
const dueByVersionRef = (0, import_react25.useRef)(/* @__PURE__ */ new Map());
|
|
6123
|
+
const configuredMockupIdsRef = (0, import_react25.useRef)([]);
|
|
6109
6124
|
const mockupUrlCacheRef = (0, import_react25.useRef)(/* @__PURE__ */ new Map());
|
|
6110
6125
|
const registeredBlobUrlsRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
|
|
6111
6126
|
const pendingPlacementsRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
|
|
@@ -6224,7 +6239,29 @@ function RealtimeProvider({
|
|
|
6224
6239
|
console.log(`[RTP] No subscribers for ${result.mockupId} \u2014 result received but nobody listening`);
|
|
6225
6240
|
}
|
|
6226
6241
|
});
|
|
6227
|
-
|
|
6242
|
+
let settledVersions = 0;
|
|
6243
|
+
const due = dueByVersionRef.current;
|
|
6244
|
+
results.forEach((result) => {
|
|
6245
|
+
const v = result.requestVersion;
|
|
6246
|
+
if (v === void 0) return;
|
|
6247
|
+
const entry = due.get(v);
|
|
6248
|
+
if (!entry) return;
|
|
6249
|
+
if (entry.due <= 1) {
|
|
6250
|
+
due.delete(v);
|
|
6251
|
+
settledVersions += 1;
|
|
6252
|
+
} else {
|
|
6253
|
+
entry.due -= 1;
|
|
6254
|
+
}
|
|
6255
|
+
});
|
|
6256
|
+
due.forEach((entry, v) => {
|
|
6257
|
+
if (now - entry.at > 1e4) due.delete(v);
|
|
6258
|
+
});
|
|
6259
|
+
if (settledVersions > 0) {
|
|
6260
|
+
if (!pipelineResetCooldownRef.current) {
|
|
6261
|
+
hasBlobInFlightRef.current = false;
|
|
6262
|
+
for (let i = 0; i < settledVersions; i++) pipelineSettledSubscribersRef.current.forEach((cb) => cb());
|
|
6263
|
+
}
|
|
6264
|
+
} else if (due.size === 0 && hasBlobInFlightRef.current && !hasPendingBlobs() && !pipelineResetCooldownRef.current) {
|
|
6228
6265
|
hasBlobInFlightRef.current = false;
|
|
6229
6266
|
pipelineSettledSubscribersRef.current.forEach((cb) => cb());
|
|
6230
6267
|
}
|
|
@@ -6353,11 +6390,17 @@ function RealtimeProvider({
|
|
|
6353
6390
|
canvasBlobsSentRef.current += 1;
|
|
6354
6391
|
lastBlobSentTimeRef.current = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
6355
6392
|
isPendingMockupsRef.current = true;
|
|
6393
|
+
const version = getLastSentVersion();
|
|
6394
|
+
if (version > 0) {
|
|
6395
|
+
const configured = configuredMockupIdsRef.current.length;
|
|
6396
|
+
const expected = Math.max(1, Math.min(configured || Infinity, mockupCount || Infinity));
|
|
6397
|
+
dueByVersionRef.current.set(version, { due: Number.isFinite(expected) ? expected : 1, at: Date.now() });
|
|
6398
|
+
}
|
|
6356
6399
|
pendingBlobSubscribersRef.current.forEach((cb) => cb(placement));
|
|
6357
6400
|
}
|
|
6358
6401
|
return result;
|
|
6359
6402
|
},
|
|
6360
|
-
[sendCanvasStateRaw]
|
|
6403
|
+
[sendCanvasStateRaw, getLastSentVersion]
|
|
6361
6404
|
);
|
|
6362
6405
|
const sendColorBlobTracked = (0, import_react25.useCallback)(
|
|
6363
6406
|
(placement, color) => {
|
|
@@ -6377,6 +6420,8 @@ function RealtimeProvider({
|
|
|
6377
6420
|
return () => {
|
|
6378
6421
|
disconnect();
|
|
6379
6422
|
configSentRef.current = false;
|
|
6423
|
+
dueByVersionRef.current.clear();
|
|
6424
|
+
setUnavailableMockups((prev) => prev.size ? /* @__PURE__ */ new Map() : prev);
|
|
6380
6425
|
lastSentVariantIdRef.current = null;
|
|
6381
6426
|
lastSentBgRef.current = void 0;
|
|
6382
6427
|
lastSentSpreadCropsRef.current = "null";
|
|
@@ -6420,6 +6465,7 @@ function RealtimeProvider({
|
|
|
6420
6465
|
}
|
|
6421
6466
|
});
|
|
6422
6467
|
}
|
|
6468
|
+
configuredMockupIdsRef.current = mockupIds;
|
|
6423
6469
|
const configPayload = useSessionGrant ? {
|
|
6424
6470
|
productId,
|
|
6425
6471
|
mockupIds,
|
|
@@ -6635,6 +6681,7 @@ function RealtimeProvider({
|
|
|
6635
6681
|
() => {
|
|
6636
6682
|
pipelineGenRef.current++;
|
|
6637
6683
|
hasBlobInFlightRef.current = false;
|
|
6684
|
+
dueByVersionRef.current.clear();
|
|
6638
6685
|
pipelineResetCooldownRef.current = true;
|
|
6639
6686
|
requestAnimationFrame(() => {
|
|
6640
6687
|
pipelineResetCooldownRef.current = false;
|
|
@@ -6773,6 +6820,7 @@ function RealtimeProvider({
|
|
|
6773
6820
|
isConnected,
|
|
6774
6821
|
isConfigured,
|
|
6775
6822
|
renderError,
|
|
6823
|
+
unavailableMockups,
|
|
6776
6824
|
// Read from ref - this is a snapshot at render time, NOT reactive
|
|
6777
6825
|
// For reactive updates, use getMockupResultsImmediate() or subscribe functions
|
|
6778
6826
|
get mockupResults() {
|
|
@@ -6818,6 +6866,7 @@ function RealtimeProvider({
|
|
|
6818
6866
|
isConnected,
|
|
6819
6867
|
isConfigured,
|
|
6820
6868
|
renderError,
|
|
6869
|
+
unavailableMockups,
|
|
6821
6870
|
// mockupResults removed from deps - it's a getter that reads from ref
|
|
6822
6871
|
canvasExportSize,
|
|
6823
6872
|
mockupWidth,
|
|
@@ -8658,9 +8707,18 @@ var CanvasExportService = class _CanvasExportService {
|
|
|
8658
8707
|
// Debounce state for handleStateChange
|
|
8659
8708
|
stateChangeThrottleTimeout = null;
|
|
8660
8709
|
stateChangeLastSendTime = 0;
|
|
8661
|
-
/**
|
|
8662
|
-
|
|
8663
|
-
|
|
8710
|
+
/** Send policy: send times of the edits whose frames have not landed yet, oldest first. */
|
|
8711
|
+
inFlightSentAt = [];
|
|
8712
|
+
maxInFlight = 2;
|
|
8713
|
+
// CanvasExportServiceConfig.maxInFlight
|
|
8714
|
+
subscribePipelineSettled = null;
|
|
8715
|
+
unsubscribePipelineSettled = null;
|
|
8716
|
+
/**
|
|
8717
|
+
* Safety net for the one-in-flight policy: a held state never waits longer
|
|
8718
|
+
* than this for the in-flight frame (a swallowed render error would
|
|
8719
|
+
* otherwise hold the pipeline forever), and a change arriving while a
|
|
8720
|
+
* frame has been in flight this long is sent regardless.
|
|
8721
|
+
*/
|
|
8664
8722
|
static MAX_WAIT_MS = 1e3;
|
|
8665
8723
|
// Internal state for tracking exports
|
|
8666
8724
|
lastExportedArtwork = null;
|
|
@@ -8702,6 +8760,14 @@ var CanvasExportService = class _CanvasExportService {
|
|
|
8702
8760
|
if (config.disableRealtime !== void 0) {
|
|
8703
8761
|
this.disableRealtime = config.disableRealtime;
|
|
8704
8762
|
}
|
|
8763
|
+
if (config.maxInFlight !== void 0) {
|
|
8764
|
+
this.maxInFlight = config.maxInFlight;
|
|
8765
|
+
}
|
|
8766
|
+
if (config.subscribePipelineSettled !== void 0 && config.subscribePipelineSettled !== this.subscribePipelineSettled) {
|
|
8767
|
+
this.unsubscribePipelineSettled?.();
|
|
8768
|
+
this.subscribePipelineSettled = config.subscribePipelineSettled;
|
|
8769
|
+
this.unsubscribePipelineSettled = config.subscribePipelineSettled?.(this.onPipelineSettled) ?? null;
|
|
8770
|
+
}
|
|
8705
8771
|
if (config.clearImageBitmapCache !== void 0) {
|
|
8706
8772
|
this.clearImageBitmapCache = config.clearImageBitmapCache;
|
|
8707
8773
|
}
|
|
@@ -8905,32 +8971,62 @@ var CanvasExportService = class _CanvasExportService {
|
|
|
8905
8971
|
console.log(`[CES] handleStateChange #${callId}: not ready (realtime=${this.isRealtimeEnabled} sendState=${!!this.sendCanvasState} serialize=${!!this.serializeStateForServer}) \u2014 state cached`);
|
|
8906
8972
|
return;
|
|
8907
8973
|
}
|
|
8908
|
-
|
|
8909
|
-
const
|
|
8910
|
-
|
|
8911
|
-
|
|
8912
|
-
|
|
8913
|
-
if (isActiveDrag && timeSinceLastSend >= maxWaitMs) {
|
|
8914
|
-
console.log(`[CES] handleStateChange #${callId}: MAX-WAIT FLUSH (${timeSinceLastSend}ms >= ${maxWaitMs}ms)`);
|
|
8915
|
-
if (this.stateChangeThrottleTimeout) {
|
|
8916
|
-
clearTimeout(this.stateChangeThrottleTimeout);
|
|
8917
|
-
this.stateChangeThrottleTimeout = null;
|
|
8918
|
-
}
|
|
8974
|
+
this.dropStalledSends(now);
|
|
8975
|
+
const inFlight = this.inFlightSentAt.length;
|
|
8976
|
+
if (inFlight < this.maxInFlight) {
|
|
8977
|
+
console.log(`[CES] handleStateChange #${callId}: SEND now (${inFlight} in flight)`);
|
|
8978
|
+
this.clearFlushTimer();
|
|
8919
8979
|
this.sendStateNow(state);
|
|
8920
8980
|
return;
|
|
8921
8981
|
}
|
|
8922
|
-
|
|
8923
|
-
|
|
8982
|
+
const oldestMs = now - this.inFlightSentAt[0];
|
|
8983
|
+
console.log(`[CES] handleStateChange #${callId}: HELD (${inFlight} in flight, oldest ${oldestMs}ms) \u2014 sends when one lands`);
|
|
8984
|
+
this.armFlushTimer(_CanvasExportService.MAX_WAIT_MS - oldestMs);
|
|
8985
|
+
};
|
|
8986
|
+
/** A send landed (every mockup of it): free its slot, release the held state if any. */
|
|
8987
|
+
onPipelineSettled = () => {
|
|
8988
|
+
this.inFlightSentAt.shift();
|
|
8989
|
+
this.clearFlushTimer();
|
|
8990
|
+
if (this.pendingCanvasState && this.isRealtimeEnabled) {
|
|
8991
|
+
console.log(`[CES] pipeline settled \u2192 sending held state`);
|
|
8992
|
+
this.sendStateNow(this.pendingCanvasState);
|
|
8924
8993
|
}
|
|
8925
|
-
|
|
8994
|
+
};
|
|
8995
|
+
/**
|
|
8996
|
+
* The pointer was released (drag end, slider release, key up). Send the held
|
|
8997
|
+
* state NOW instead of waiting for the in-flight frame: the server coalesces
|
|
8998
|
+
* (latest wins), and the "let go" frame is the one the shopper waits for.
|
|
8999
|
+
* A no-op when nothing is held, so it is safe on every pointerup.
|
|
9000
|
+
*/
|
|
9001
|
+
handleInteractionEnd = () => {
|
|
9002
|
+
if (!this.pendingCanvasState || !this.isRealtimeEnabled || !this.sendCanvasState || !this.serializeStateForServer) return;
|
|
9003
|
+
if (this.inFlightSentAt.length < this.maxInFlight) return;
|
|
9004
|
+
console.log(`[CES] interaction end \u2192 flushing held state`);
|
|
9005
|
+
this.clearFlushTimer();
|
|
9006
|
+
this.sendStateNow(this.pendingCanvasState);
|
|
9007
|
+
};
|
|
9008
|
+
armFlushTimer(ms) {
|
|
9009
|
+
this.clearFlushTimer();
|
|
8926
9010
|
this.stateChangeThrottleTimeout = setTimeout(() => {
|
|
8927
9011
|
this.stateChangeThrottleTimeout = null;
|
|
8928
|
-
console.log(`[CES] handleStateChange #${callId}: SETTLE FIRED at ${Date.now()} (${Date.now() - now}ms after call)`);
|
|
8929
9012
|
if (this.pendingCanvasState) {
|
|
9013
|
+
console.log(`[CES] flush timer \u2192 sending held state (no frame landed within ${_CanvasExportService.MAX_WAIT_MS}ms)`);
|
|
9014
|
+
this.dropStalledSends(Date.now());
|
|
8930
9015
|
this.sendStateNow(this.pendingCanvasState);
|
|
8931
9016
|
}
|
|
8932
|
-
},
|
|
8933
|
-
}
|
|
9017
|
+
}, Math.max(0, ms));
|
|
9018
|
+
}
|
|
9019
|
+
/** A send whose frame has not landed within MAX_WAIT_MS no longer occupies a slot. */
|
|
9020
|
+
dropStalledSends(now) {
|
|
9021
|
+
if (this.inFlightSentAt.length === 0) return;
|
|
9022
|
+
this.inFlightSentAt = this.inFlightSentAt.filter((t) => now - t < _CanvasExportService.MAX_WAIT_MS);
|
|
9023
|
+
}
|
|
9024
|
+
clearFlushTimer() {
|
|
9025
|
+
if (this.stateChangeThrottleTimeout) {
|
|
9026
|
+
clearTimeout(this.stateChangeThrottleTimeout);
|
|
9027
|
+
this.stateChangeThrottleTimeout = null;
|
|
9028
|
+
}
|
|
9029
|
+
}
|
|
8934
9030
|
sendStateNow(state) {
|
|
8935
9031
|
if (!this.sendCanvasState || !this.serializeStateForServer) {
|
|
8936
9032
|
console.log(`[CES] sendStateNow SKIPPED: sendCanvasState=${!!this.sendCanvasState} serializeStateForServer=${!!this.serializeStateForServer}`);
|
|
@@ -8955,6 +9051,7 @@ var CanvasExportService = class _CanvasExportService {
|
|
|
8955
9051
|
if (sent) {
|
|
8956
9052
|
this.pendingCanvasState = null;
|
|
8957
9053
|
this.jsonModeSentSuccessfully = true;
|
|
9054
|
+
this.inFlightSentAt.push(Date.now());
|
|
8958
9055
|
console.log(`[CES] JSON state sent for ${artboards.length} artboard(s)`);
|
|
8959
9056
|
} else {
|
|
8960
9057
|
console.log(`[CES] JSON state send FAILED for all ${artboards.length} artboard(s) \u2014 state remains pending, jsonEverSent=${this.jsonModeSentSuccessfully}`);
|
|
@@ -8976,6 +9073,7 @@ var CanvasExportService = class _CanvasExportService {
|
|
|
8976
9073
|
this.stateChangeThrottleTimeout = null;
|
|
8977
9074
|
}
|
|
8978
9075
|
this.stateChangeLastSendTime = 0;
|
|
9076
|
+
this.inFlightSentAt = [];
|
|
8979
9077
|
this.jsonModeSentSuccessfully = false;
|
|
8980
9078
|
this.pendingCanvasState = null;
|
|
8981
9079
|
if (this.clearImageBitmapCache) {
|
|
@@ -9011,7 +9109,10 @@ function CanvasIsolationConfigurator({
|
|
|
9011
9109
|
// Wire CES's "I sent nothing" signal to the realtime context's
|
|
9012
9110
|
// cancellation broadcaster. Roll back any optimistic
|
|
9013
9111
|
// notifyPendingExport that didn't end up producing a real export.
|
|
9014
|
-
notifyExportSkipped: realtimeContext?.notifyPendingExportCancelled
|
|
9112
|
+
notifyExportSkipped: realtimeContext?.notifyPendingExportCancelled,
|
|
9113
|
+
// One-in-flight send policy: the held state goes out when the last
|
|
9114
|
+
// send's frame lands.
|
|
9115
|
+
subscribePipelineSettled: realtimeContext?.subscribePipelineSettled
|
|
9015
9116
|
});
|
|
9016
9117
|
}, [
|
|
9017
9118
|
realtimeContext?.sendCanvasBlob,
|
|
@@ -9023,6 +9124,7 @@ function CanvasIsolationConfigurator({
|
|
|
9023
9124
|
realtimeContext?.enableRealtime,
|
|
9024
9125
|
realtimeContext?.disableRealtime,
|
|
9025
9126
|
realtimeContext?.notifyPendingExportCancelled,
|
|
9127
|
+
realtimeContext?.subscribePipelineSettled,
|
|
9026
9128
|
placementNameProp,
|
|
9027
9129
|
onArtworkChange,
|
|
9028
9130
|
clearImageBitmapCache
|
|
@@ -9068,6 +9170,8 @@ function useIsolatedCanvas() {
|
|
|
9068
9170
|
handleExport: canvasExportService.handleExport,
|
|
9069
9171
|
/** Stable state change handler - pass to SnowconeCanvas onChange for JSON mode */
|
|
9070
9172
|
handleStateChange: canvasExportService.handleStateChange,
|
|
9173
|
+
/** Pointer released (drag end): flush a state held behind an in-flight frame. Safe on every pointerup. */
|
|
9174
|
+
handleInteractionEnd: canvasExportService.handleInteractionEnd,
|
|
9071
9175
|
/** Call when entering editor mode */
|
|
9072
9176
|
onEnterEditorMode: () => canvasExportService.onEnterEditorMode(),
|
|
9073
9177
|
/** Call when exiting editor mode */
|
|
@@ -10327,6 +10431,7 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10327
10431
|
const actualMockupId = (0, import_react46.useMemo)(() => {
|
|
10328
10432
|
return (0, import_sdk10.resolveMockupId)(context, actualGvid, mockupId);
|
|
10329
10433
|
}, [mockupId, actualGvid, context]);
|
|
10434
|
+
const unavailable = actualMockupId ? realtimeContext?.unavailableMockups?.get(actualMockupId) ?? null : null;
|
|
10330
10435
|
const artworkSrc = artwork?.src;
|
|
10331
10436
|
const selectedArtworkSrc = designContext?.selectedArtwork?.src;
|
|
10332
10437
|
const effectiveArtworkSrc = artworkSrc || selectedArtworkSrc;
|
|
@@ -10775,7 +10880,17 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
|
|
|
10775
10880
|
className: `relative overflow-hidden ${className || ""}`,
|
|
10776
10881
|
style,
|
|
10777
10882
|
"data-hero-image": "true",
|
|
10883
|
+
"data-mockup-unavailable": unavailable ? unavailable.code : void 0,
|
|
10778
10884
|
children: [
|
|
10885
|
+
unavailable && /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
|
|
10886
|
+
"div",
|
|
10887
|
+
{
|
|
10888
|
+
role: "status",
|
|
10889
|
+
title: unavailable.message,
|
|
10890
|
+
className: "absolute inset-0 z-20 flex items-center justify-center bg-white/80 text-sm text-neutral-700",
|
|
10891
|
+
children: "Mockup unavailable"
|
|
10892
|
+
}
|
|
10893
|
+
),
|
|
10779
10894
|
renderedUrl && !imgFailed && /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
|
|
10780
10895
|
"img",
|
|
10781
10896
|
{
|