@snowcone-app/ui 0.4.14 → 0.8.1

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 CHANGED
@@ -1,5 +1,53 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`4b68ab9`](https://github.com/snowcone-app/snowcone-monorepo/commit/4b68ab971b99e0dfa4e6f096e50bb0e5ceb2ec70)]:
8
+ - @snowcone-app/sdk@0.24.0
9
+
10
+ ## 0.8.0
11
+
12
+ ### Minor Changes
13
+
14
+ - [#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.
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [[`19f7ee5`](https://github.com/snowcone-app/snowcone-monorepo/commit/19f7ee583638ccedd8fd5f309ad3e5ba6a06c0f9)]:
19
+ - @snowcone-app/sdk@0.23.0
20
+
21
+ ## 0.7.0
22
+
23
+ ### Minor Changes
24
+
25
+ - [#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.
26
+
27
+ ### Patch Changes
28
+
29
+ - Updated dependencies [[`61c4826`](https://github.com/snowcone-app/snowcone-monorepo/commit/61c48267d77b93628ef525df5a6aee66421d67f7)]:
30
+ - @snowcone-app/sdk@0.22.0
31
+
32
+ ## 0.6.0
33
+
34
+ ### Minor Changes
35
+
36
+ - [#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`.
37
+
38
+ ## 0.5.0
39
+
40
+ ### Minor Changes
41
+
42
+ - [#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.
43
+
44
+ ## 0.4.15
45
+
46
+ ### Patch Changes
47
+
48
+ - Updated dependencies [[`aaa73cc`](https://github.com/snowcone-app/snowcone-monorepo/commit/aaa73ccb4b2d4c7c107b66f18be29f67032422b8)]:
49
+ - @snowcone-app/sdk@0.21.0
50
+
3
51
  ## 0.4.14
4
52
 
5
53
  ### 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: "Snowcone Inc.",
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: "\u96EA\u7ED8\u79D1\u6280\u6709\u9650\u516C\u53F8",
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: "Snowcone Inc.",
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: "Snowcone Inc.",
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
- if (hasBlobInFlightRef.current && !hasPendingBlobs() && !pipelineResetCooldownRef.current) {
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
- /** How long to wait after last change before sending (quick discrete actions) */
8662
- static SETTLE_MS = 300;
8663
- /** During continuous activity (drag), force a send at least this often */
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,42 +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
- const settleMs = _CanvasExportService.SETTLE_MS;
8909
- const maxWaitMs = _CanvasExportService.MAX_WAIT_MS;
8910
- const hasSentBefore = this.stateChangeLastSendTime > 0;
8911
- const timeSinceLastSend = hasSentBefore ? now - this.stateChangeLastSendTime : 0;
8912
- const isActiveDrag = hasSentBefore && timeSinceLastSend < maxWaitMs * 3;
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
- const isIdle = !this.stateChangeThrottleTimeout && (!hasSentBefore || timeSinceLastSend >= settleMs);
8923
- if (isIdle) {
8924
- console.log(`[CES] handleStateChange #${callId}: LEADING send (idle ${hasSentBefore ? timeSinceLastSend : "forever"}ms)`);
8925
- this.sendStateNow(state);
8926
- this.stateChangeThrottleTimeout = setTimeout(() => {
8927
- this.stateChangeThrottleTimeout = null;
8928
- if (this.pendingCanvasState) this.sendStateNow(this.pendingCanvasState);
8929
- }, settleMs);
8930
- return;
8931
- }
8932
- if (this.stateChangeThrottleTimeout) {
8933
- clearTimeout(this.stateChangeThrottleTimeout);
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);
8934
8993
  }
8935
- console.log(`[CES] handleStateChange #${callId}: SETTLE scheduled ${settleMs}ms`);
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();
8936
9010
  this.stateChangeThrottleTimeout = setTimeout(() => {
8937
9011
  this.stateChangeThrottleTimeout = null;
8938
- console.log(`[CES] handleStateChange #${callId}: SETTLE FIRED at ${Date.now()} (${Date.now() - now}ms after call)`);
8939
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());
8940
9015
  this.sendStateNow(this.pendingCanvasState);
8941
9016
  }
8942
- }, settleMs);
8943
- };
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
+ }
8944
9030
  sendStateNow(state) {
8945
9031
  if (!this.sendCanvasState || !this.serializeStateForServer) {
8946
9032
  console.log(`[CES] sendStateNow SKIPPED: sendCanvasState=${!!this.sendCanvasState} serializeStateForServer=${!!this.serializeStateForServer}`);
@@ -8965,6 +9051,7 @@ var CanvasExportService = class _CanvasExportService {
8965
9051
  if (sent) {
8966
9052
  this.pendingCanvasState = null;
8967
9053
  this.jsonModeSentSuccessfully = true;
9054
+ this.inFlightSentAt.push(Date.now());
8968
9055
  console.log(`[CES] JSON state sent for ${artboards.length} artboard(s)`);
8969
9056
  } else {
8970
9057
  console.log(`[CES] JSON state send FAILED for all ${artboards.length} artboard(s) \u2014 state remains pending, jsonEverSent=${this.jsonModeSentSuccessfully}`);
@@ -8986,6 +9073,7 @@ var CanvasExportService = class _CanvasExportService {
8986
9073
  this.stateChangeThrottleTimeout = null;
8987
9074
  }
8988
9075
  this.stateChangeLastSendTime = 0;
9076
+ this.inFlightSentAt = [];
8989
9077
  this.jsonModeSentSuccessfully = false;
8990
9078
  this.pendingCanvasState = null;
8991
9079
  if (this.clearImageBitmapCache) {
@@ -9021,7 +9109,10 @@ function CanvasIsolationConfigurator({
9021
9109
  // Wire CES's "I sent nothing" signal to the realtime context's
9022
9110
  // cancellation broadcaster. Roll back any optimistic
9023
9111
  // notifyPendingExport that didn't end up producing a real export.
9024
- 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
9025
9116
  });
9026
9117
  }, [
9027
9118
  realtimeContext?.sendCanvasBlob,
@@ -9033,6 +9124,7 @@ function CanvasIsolationConfigurator({
9033
9124
  realtimeContext?.enableRealtime,
9034
9125
  realtimeContext?.disableRealtime,
9035
9126
  realtimeContext?.notifyPendingExportCancelled,
9127
+ realtimeContext?.subscribePipelineSettled,
9036
9128
  placementNameProp,
9037
9129
  onArtworkChange,
9038
9130
  clearImageBitmapCache
@@ -9078,6 +9170,8 @@ function useIsolatedCanvas() {
9078
9170
  handleExport: canvasExportService.handleExport,
9079
9171
  /** Stable state change handler - pass to SnowconeCanvas onChange for JSON mode */
9080
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,
9081
9175
  /** Call when entering editor mode */
9082
9176
  onEnterEditorMode: () => canvasExportService.onEnterEditorMode(),
9083
9177
  /** Call when exiting editor mode */
@@ -10337,6 +10431,7 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
10337
10431
  const actualMockupId = (0, import_react46.useMemo)(() => {
10338
10432
  return (0, import_sdk10.resolveMockupId)(context, actualGvid, mockupId);
10339
10433
  }, [mockupId, actualGvid, context]);
10434
+ const unavailable = actualMockupId ? realtimeContext?.unavailableMockups?.get(actualMockupId) ?? null : null;
10340
10435
  const artworkSrc = artwork?.src;
10341
10436
  const selectedArtworkSrc = designContext?.selectedArtwork?.src;
10342
10437
  const effectiveArtworkSrc = artworkSrc || selectedArtworkSrc;
@@ -10785,7 +10880,17 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
10785
10880
  className: `relative overflow-hidden ${className || ""}`,
10786
10881
  style,
10787
10882
  "data-hero-image": "true",
10883
+ "data-mockup-unavailable": unavailable ? unavailable.code : void 0,
10788
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
+ ),
10789
10894
  renderedUrl && !imgFailed && /* @__PURE__ */ (0, import_jsx_runtime61.jsx)(
10790
10895
  "img",
10791
10896
  {