@snowcone-app/ui 0.9.2 → 0.9.4

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,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [#4344](https://github.com/snowcone-app/snowcone-monorepo/pull/4344) [`379a042`](https://github.com/snowcone-app/snowcone-monorepo/commit/379a042eb69a26f98fe0050f694c4ccf342e55b8) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Per-placement render versions and an explicit re-render. SDK: `MockupResult.versions` (every placement folded into a frame, from boxes that stamp it), `getLatestSentVersions()`, and `rerender(mockupIds)` (answered by `rerender_queued`; refusals are typed `E_RERENDER_*` errors). UI: a picture's `data-render-state` is `current` only when every placement is at its latest sent version, and a picture whose cropped frame failed is re-rendered with `rerender` once the box stamps versions (falling back to forcing it with configs on an `E_RERENDER_*` refusal or an older box).
8
+
9
+ - Updated dependencies [[`379a042`](https://github.com/snowcone-app/snowcone-monorepo/commit/379a042eb69a26f98fe0050f694c4ccf342e55b8)]:
10
+ - @snowcone-app/sdk@0.25.0
11
+
12
+ ## 0.9.3
13
+
14
+ ### Patch Changes
15
+
16
+ - [#4338](https://github.com/snowcone-app/snowcone-monorepo/pull/4338) [`80ec516`](https://github.com/snowcone-app/snowcone-monorepo/commit/80ec5166f3b2d9e52a9791ae54c8d67e8a217cae) Thanks [@kevinsproles](https://github.com/kevinsproles)! - The full-frame fallback really re-renders: when a cropped frame can't be composited, the realtime session now sends a config that differs from the one before it and then the full set, so the render server (which ignores overlayCrop and sorts mockupIds when deciding whether a config changed anything) re-renders the failed pictures instead of leaving them on their old render until the next edit.
17
+
18
+ - [#4307](https://github.com/snowcone-app/snowcone-monorepo/pull/4307) [`1b510d0`](https://github.com/snowcone-app/snowcone-monorepo/commit/1b510d08c9e2c4399fd7731d3032b7e71abbfe9f) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Live pictures update on every edit: a cropped frame that can't be shown no longer narrows the realtime session to one mockup (it re-renders full-frame with the whole set); a frame superseded mid-drag is dropped instead of killing cropped delivery; the priority provider requests every picture in view as one set and, after the edit stops, every registered picture. `HeroProductImage` exposes `data-canvas-version`, `data-render-version` and `data-render-state` (current | stale | refused); the realtime context adds `getCanvasVersion()`.
19
+
3
20
  ## 0.9.2
4
21
 
5
22
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -6062,6 +6062,11 @@ async function compositeCroppedFrame(args) {
6062
6062
  var import_jsx_runtime52 = require("react/jsx-runtime");
6063
6063
  var DEFAULT_ARTBOARD_SIZE = 2e3;
6064
6064
  var MAX_CONCURRENT_BLOBS = 5;
6065
+ function mergeMockupIds(current, extra) {
6066
+ const out = [...current];
6067
+ for (const id of extra) if (!out.includes(id)) out.push(id);
6068
+ return out;
6069
+ }
6065
6070
  var RealtimeContext = (0, import_react25.createContext)(
6066
6071
  void 0
6067
6072
  );
@@ -6140,6 +6145,12 @@ function RealtimeProvider({
6140
6145
  },
6141
6146
  onError: (error, detail) => {
6142
6147
  console.error(`[RealtimeProvider] Render error [${detail.code}]:`, error);
6148
+ if (detail.code.startsWith("E_RERENDER_") && pendingRerenderRef.current.length > 0) {
6149
+ const ids = pendingRerenderRef.current;
6150
+ pendingRerenderRef.current = [];
6151
+ forceRerenderByConfigRef.current?.(ids);
6152
+ return;
6153
+ }
6143
6154
  if (detail.mockupId) {
6144
6155
  const id = detail.mockupId;
6145
6156
  setUnavailableMockups((prev) => prev.get(id)?.code === detail.code ? prev : new Map(prev).set(id, detail));
@@ -6165,7 +6176,9 @@ function RealtimeProvider({
6165
6176
  disconnect,
6166
6177
  flushPendingBlobs,
6167
6178
  hasPendingBlobs,
6168
- getLastSentVersion
6179
+ getLastSentVersion,
6180
+ getLatestSentVersions,
6181
+ rerender: rerenderRaw
6169
6182
  } = realtimeMockup;
6170
6183
  const mockupResultsRef = (0, import_react25.useRef)([]);
6171
6184
  const mockupSubscribersRef = (0, import_react25.useRef)(
@@ -6202,6 +6215,18 @@ function RealtimeProvider({
6202
6215
  const compositeQueueRef = (0, import_react25.useRef)(/* @__PURE__ */ new Map());
6203
6216
  const compositeBusyRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
6204
6217
  const compositeCanvasRef = (0, import_react25.useRef)(null);
6218
+ const requestedMockupIdsRef = (0, import_react25.useRef)([]);
6219
+ const serverStampsVersionsRef = (0, import_react25.useRef)(false);
6220
+ const pendingRerenderRef = (0, import_react25.useRef)([]);
6221
+ const forceRerenderByConfigRef = (0, import_react25.useRef)(null);
6222
+ const fullFrameRerenderIdsRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
6223
+ const fullFrameRerenderTimerRef = (0, import_react25.useRef)(null);
6224
+ (0, import_react25.useEffect)(
6225
+ () => () => {
6226
+ if (fullFrameRerenderTimerRef.current) clearTimeout(fullFrameRerenderTimerRef.current);
6227
+ },
6228
+ []
6229
+ );
6205
6230
  const registerBlobUrl = (0, import_react25.useCallback)((blobOrUrl, placementName) => {
6206
6231
  let url;
6207
6232
  if (blobOrUrl instanceof Blob) {
@@ -6233,6 +6258,7 @@ function RealtimeProvider({
6233
6258
  const base = lastConfigBaseRef.current;
6234
6259
  if (!base) return;
6235
6260
  croppedActiveRef.current = withCrop;
6261
+ requestedMockupIdsRef.current = base.mockupIds ?? [];
6236
6262
  sendConfigRaw({
6237
6263
  ...base,
6238
6264
  placementSettings: { ...placementSettingsOverridesRef.current },
@@ -6245,6 +6271,65 @@ function RealtimeProvider({
6245
6271
  if (!croppedActiveRef.current) return;
6246
6272
  sendCropConfig(false);
6247
6273
  }, [sendCropConfig]);
6274
+ const sendFullFrameConfig = (0, import_react25.useCallback)(
6275
+ (mockupIds) => {
6276
+ const base = lastConfigBaseRef.current;
6277
+ if (!base) return;
6278
+ sendConfigRaw({
6279
+ ...base,
6280
+ mockupIds,
6281
+ placementSettings: { ...placementSettingsOverridesRef.current }
6282
+ });
6283
+ },
6284
+ [sendConfigRaw]
6285
+ );
6286
+ const forceRerenderByConfig = (0, import_react25.useCallback)(
6287
+ (failed) => {
6288
+ const inForce = requestedMockupIdsRef.current;
6289
+ const fp = (list) => [...list].sort().join(",");
6290
+ const step = fp(failed) !== fp(inForce) ? failed : inForce.slice(0, -1);
6291
+ if (step.length === 0) {
6292
+ console.error(
6293
+ `[RealtimeProvider] E_OVERLAY_RERENDER_UNAVAILABLE: cannot force a full-frame re-render of ${failed.join(", ")} (single-mockup session on a box without \`rerender\`); it updates on the next edit.`
6294
+ );
6295
+ sendFullFrameConfig(inForce);
6296
+ return;
6297
+ }
6298
+ sendFullFrameConfig(step);
6299
+ sendFullFrameConfig(inForce);
6300
+ },
6301
+ [sendFullFrameConfig]
6302
+ );
6303
+ forceRerenderByConfigRef.current = forceRerenderByConfig;
6304
+ const rerenderFullFrame = (0, import_react25.useCallback)(
6305
+ (mockupId) => {
6306
+ fullFrameRerenderIdsRef.current.add(mockupId);
6307
+ if (fullFrameRerenderTimerRef.current) return;
6308
+ fullFrameRerenderTimerRef.current = setTimeout(() => {
6309
+ fullFrameRerenderTimerRef.current = null;
6310
+ const failed = [...fullFrameRerenderIdsRef.current];
6311
+ fullFrameRerenderIdsRef.current.clear();
6312
+ if (!lastConfigBaseRef.current || failed.length === 0) return;
6313
+ const previous = requestedMockupIdsRef.current;
6314
+ const ids = mergeMockupIds(previous, failed);
6315
+ croppedActiveRef.current = false;
6316
+ requestedMockupIdsRef.current = ids;
6317
+ const fp = (list) => [...list].sort().join(",");
6318
+ if (fp(ids) !== fp(previous)) {
6319
+ sendFullFrameConfig(ids);
6320
+ return;
6321
+ }
6322
+ if (serverStampsVersionsRef.current) {
6323
+ sendFullFrameConfig(ids);
6324
+ pendingRerenderRef.current = failed;
6325
+ if (rerenderRaw(failed)) return;
6326
+ pendingRerenderRef.current = [];
6327
+ }
6328
+ forceRerenderByConfig(failed);
6329
+ }, 0);
6330
+ },
6331
+ [sendFullFrameConfig, forceRerenderByConfig, rerenderRaw]
6332
+ );
6248
6333
  const maybeFlipCrop = (0, import_react25.useCallback)(() => {
6249
6334
  if (!croppedDeliveryEnabled || !cropCapableRef.current) return;
6250
6335
  if (croppedActiveRef.current) return;
@@ -6278,6 +6363,10 @@ function RealtimeProvider({
6278
6363
  else merged.push(result);
6279
6364
  });
6280
6365
  mockupResultsRef.current = merged;
6366
+ if (pendingRerenderRef.current.length > 0) {
6367
+ const delivered = new Set(results.map((r) => r.mockupId));
6368
+ pendingRerenderRef.current = pendingRerenderRef.current.filter((id) => !delivered.has(id));
6369
+ }
6281
6370
  const clearedPlacements = /* @__PURE__ */ new Set();
6282
6371
  results.forEach((result) => {
6283
6372
  const wasPending = pendingMockupIdsRef.current.has(result.mockupId);
@@ -6344,8 +6433,7 @@ function RealtimeProvider({
6344
6433
  console.error(
6345
6434
  `[RealtimeProvider] E_OVERLAY_NO_BACKGROUND: cropped frame for ${result.mockupId} has no frozen background \u2014 falling back to full-frame and re-rendering it.`
6346
6435
  );
6347
- unflipCrop();
6348
- updateMockupIdsRaw([result.mockupId]);
6436
+ rerenderFullFrame(result.mockupId);
6349
6437
  return;
6350
6438
  }
6351
6439
  try {
@@ -6363,16 +6451,18 @@ function RealtimeProvider({
6363
6451
  if (gen !== sceneGenRef.current) return;
6364
6452
  deliverResults([{ ...result, imageUrl: registerBlobUrl(blob) }]);
6365
6453
  } catch (err) {
6454
+ if (gen !== sceneGenRef.current || compositeQueueRef.current.has(result.mockupId) || processedFrameRef.current.get(result.mockupId) !== result.imageUrl) {
6455
+ return;
6456
+ }
6366
6457
  console.error(
6367
6458
  "[RealtimeProvider] E_OVERLAY_COMPOSITE_FAILED: cropped-frame composite failed \u2014 full-frame delivery for the rest of this session.",
6368
6459
  err
6369
6460
  );
6370
6461
  cropCapableRef.current = false;
6371
- unflipCrop();
6372
- updateMockupIdsRaw([result.mockupId]);
6462
+ rerenderFullFrame(result.mockupId);
6373
6463
  }
6374
6464
  },
6375
- [deliverResults, registerBlobUrl, unflipCrop, updateMockupIdsRaw]
6465
+ [deliverResults, registerBlobUrl, rerenderFullFrame]
6376
6466
  );
6377
6467
  const pumpCompositeQueue = (0, import_react25.useCallback)(
6378
6468
  (mockupId) => {
@@ -6398,6 +6488,9 @@ function RealtimeProvider({
6398
6488
  }
6399
6489
  console.log(`[RTP] rawMockupResults changed: ${rawMockupResults.length} result(s)`, rawMockupResults.map((r) => `${r.mockupId}${r.imageUrl ? " \u2713url" : " \u2717NO-URL"}${r.cropRect ? " [crop]" : ""}`));
6400
6490
  const displayable = [];
6491
+ if (!serverStampsVersionsRef.current && rawMockupResults.some((r) => r.versions && typeof r.versions === "object")) {
6492
+ serverStampsVersionsRef.current = true;
6493
+ }
6401
6494
  rawMockupResults.forEach((result) => {
6402
6495
  if (!result.imageUrl) {
6403
6496
  console.log(`[RTP] Result for ${result.mockupId} has NO imageUrl \u2014 not caching, pending NOT cleared`);
@@ -6560,6 +6653,7 @@ function RealtimeProvider({
6560
6653
  });
6561
6654
  }
6562
6655
  lastConfigBaseRef.current = configPayload;
6656
+ requestedMockupIdsRef.current = mockupIds;
6563
6657
  sendConfigRaw(
6564
6658
  croppedActiveRef.current ? { ...configPayload, overlayCrop: "auto" } : configPayload
6565
6659
  );
@@ -6619,6 +6713,7 @@ function RealtimeProvider({
6619
6713
  if (croppedActiveRef.current && mockupIds.some((id) => !backgroundsRef.current.has(id))) {
6620
6714
  unflipCrop();
6621
6715
  }
6716
+ requestedMockupIdsRef.current = [...mockupIds];
6622
6717
  return updateMockupIdsRaw(mockupIds);
6623
6718
  },
6624
6719
  [updateMockupIdsRaw, unflipCrop]
@@ -6875,6 +6970,12 @@ function RealtimeProvider({
6875
6970
  },
6876
6971
  []
6877
6972
  );
6973
+ const getLastSentVersionRef = (0, import_react25.useRef)(getLastSentVersion);
6974
+ getLastSentVersionRef.current = getLastSentVersion;
6975
+ const getCanvasVersion = (0, import_react25.useCallback)(() => getLastSentVersionRef.current?.() ?? 0, []);
6976
+ const getLatestSentVersionsRef = (0, import_react25.useRef)(getLatestSentVersions);
6977
+ getLatestSentVersionsRef.current = getLatestSentVersions;
6978
+ const getLatestSentVersionsStable = (0, import_react25.useCallback)(() => getLatestSentVersionsRef.current?.() ?? {}, []);
6878
6979
  const stableSubscribePipelineSettled = (0, import_react25.useCallback)(
6879
6980
  (callback) => {
6880
6981
  return subscribePipelineSettledRef.current(callback);
@@ -6923,6 +7024,8 @@ function RealtimeProvider({
6923
7024
  // Centralized URL management
6924
7025
  getMockupUrl: getMockupUrl3,
6925
7026
  getMockupUrls,
7027
+ getCanvasVersion,
7028
+ getLatestSentVersions: getLatestSentVersionsStable,
6926
7029
  isMockupLoading,
6927
7030
  registerBlobUrl,
6928
7031
  // WebRTC streaming
@@ -6954,6 +7057,8 @@ function RealtimeProvider({
6954
7057
  hasPendingBlobs,
6955
7058
  getMockupUrl3,
6956
7059
  getMockupUrls,
7060
+ getCanvasVersion,
7061
+ getLatestSentVersionsStable,
6957
7062
  isMockupLoading,
6958
7063
  registerBlobUrl
6959
7064
  ]
@@ -7822,17 +7927,33 @@ function MockupPriorityProvider({ children }) {
7822
7927
  if (!realtimeContext?.updateMockupIds) {
7823
7928
  return;
7824
7929
  }
7930
+ const mobile = mobileCarouselRef.current;
7931
+ const universe = mobile && mobile.mockupIds.length > 0 ? mobile.mockupIds : registrationOrderRef.current;
7932
+ const inView = [];
7933
+ if (level1MockupRef.current) {
7934
+ inView.push(level1MockupRef.current);
7935
+ if (!mobile || mobile.mockupIds.length === 0) {
7936
+ visibilityMapRef.current.forEach((info, id) => {
7937
+ if (info.visibleArea > 0 && !inView.includes(id)) inView.push(id);
7938
+ });
7939
+ }
7940
+ }
7825
7941
  let mockupIds;
7826
7942
  switch (level) {
7827
7943
  case 1:
7828
- mockupIds = level1MockupRef.current ? [level1MockupRef.current] : [];
7944
+ mockupIds = inView;
7829
7945
  break;
7830
7946
  case 2:
7831
- mockupIds = [...level2MockupsRef.current];
7947
+ mockupIds = level2MockupsRef.current.filter((id) => !inView.includes(id));
7832
7948
  break;
7833
- case 3:
7834
- mockupIds = [...level3QueueRef.current];
7949
+ case 3: {
7950
+ const earlier = /* @__PURE__ */ new Set([...inView, ...level2MockupsRef.current]);
7951
+ mockupIds = [
7952
+ ...level3QueueRef.current.filter((id) => !earlier.has(id)),
7953
+ ...universe.filter((id) => !earlier.has(id) && !level3QueueRef.current.includes(id))
7954
+ ];
7835
7955
  break;
7956
+ }
7836
7957
  default:
7837
7958
  mockupIds = [];
7838
7959
  }
@@ -7857,6 +7978,16 @@ function MockupPriorityProvider({ children }) {
7857
7978
  const isStillDragging = timeSinceLastPendingBlob < DRAG_STOP_DEBOUNCE_MS;
7858
7979
  if (currentLevel === 1 && isStillDragging) {
7859
7980
  currentRenderingLevelRef.current = null;
7981
+ if (levelAdvanceTimeoutRef.current) {
7982
+ clearTimeout(levelAdvanceTimeoutRef.current);
7983
+ }
7984
+ levelAdvanceTimeoutRef.current = setTimeout(() => {
7985
+ levelAdvanceTimeoutRef.current = null;
7986
+ const quiet = Date.now() - lastPendingBlobTimeRef.current >= DRAG_STOP_DEBOUNCE_MS;
7987
+ if (quiet && currentRenderingLevelRef.current === null && pendingMockupsRef.current.size === 0) {
7988
+ requestMockupsForLevel(2);
7989
+ }
7990
+ }, DRAG_STOP_DEBOUNCE_MS - timeSinceLastPendingBlob + 10);
7860
7991
  return;
7861
7992
  }
7862
7993
  if (isStillDragging) {
@@ -10681,6 +10812,57 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
10681
10812
  unsubscribe?.();
10682
10813
  };
10683
10814
  }, [mode, realtimeContext?.subscribePendingBlob, placement]);
10815
+ const renderVersionRef = (0, import_react46.useRef)(0);
10816
+ const renderVersionsRef = (0, import_react46.useRef)(null);
10817
+ const getLatestSentVersions = realtimeContext?.getLatestSentVersions;
10818
+ const refusalCodeRef = (0, import_react46.useRef)(null);
10819
+ refusalCodeRef.current = unavailable?.code ?? null;
10820
+ const sessionErrorCodeRef = (0, import_react46.useRef)(null);
10821
+ sessionErrorCodeRef.current = realtimeContext?.renderError?.code ?? null;
10822
+ const getCanvasVersion = realtimeContext?.getCanvasVersion;
10823
+ const applyRenderStamp = (0, import_react46.useCallback)(() => {
10824
+ const node = containerRef.current;
10825
+ if (!node) return;
10826
+ const canvas = getCanvasVersion?.() ?? 0;
10827
+ if (canvas <= 0) {
10828
+ delete node.dataset.canvasVersion;
10829
+ delete node.dataset.renderVersion;
10830
+ delete node.dataset.renderState;
10831
+ return;
10832
+ }
10833
+ const rendered = renderVersionRef.current;
10834
+ const versions = renderVersionsRef.current;
10835
+ const sent = versions ? getLatestSentVersions?.() : void 0;
10836
+ const behind = versions && sent ? Object.entries(sent).some(([placement2, v]) => (versions[placement2] ?? -1) < v) : rendered < canvas;
10837
+ node.dataset.canvasVersion = String(canvas);
10838
+ node.dataset.renderVersion = String(rendered);
10839
+ node.dataset.renderState = refusalCodeRef.current || behind && sessionErrorCodeRef.current ? "refused" : behind ? "stale" : "current";
10840
+ }, [getCanvasVersion, getLatestSentVersions]);
10841
+ (0, import_react46.useEffect)(() => {
10842
+ applyRenderStamp();
10843
+ });
10844
+ (0, import_react46.useEffect)(() => {
10845
+ const subscribeSent = realtimeContext?.subscribeBlobSent;
10846
+ const subscribeResult = realtimeContext?.subscribeMockupResultById;
10847
+ if (!actualMockupId || !subscribeSent || !subscribeResult) return;
10848
+ const offSent = subscribeSent(() => applyRenderStamp());
10849
+ const offResult = subscribeResult(actualMockupId, (result) => {
10850
+ if (typeof result?.requestVersion === "number" && result.requestVersion >= renderVersionRef.current) {
10851
+ renderVersionRef.current = result.requestVersion;
10852
+ renderVersionsRef.current = result.versions ?? null;
10853
+ }
10854
+ applyRenderStamp();
10855
+ });
10856
+ return () => {
10857
+ offSent?.();
10858
+ offResult?.();
10859
+ };
10860
+ }, [
10861
+ actualMockupId,
10862
+ realtimeContext?.subscribeBlobSent,
10863
+ realtimeContext?.subscribeMockupResultById,
10864
+ applyRenderStamp
10865
+ ]);
10684
10866
  (0, import_react46.useEffect)(() => {
10685
10867
  if (!priorityContext || !actualMockupId || !placement) return;
10686
10868
  priorityContext.registerMockup(actualMockupId, placement, initialPriority);