@snowcone-app/ui 0.9.1 → 0.9.3

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.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [#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.
8
+
9
+ - [#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()`.
10
+
11
+ ## 0.9.2
12
+
13
+ ### Patch Changes
14
+
15
+ - [#4283](https://github.com/snowcone-app/snowcone-monorepo/pull/4283) [`4fbaaf1`](https://github.com/snowcone-app/snowcone-monorepo/commit/4fbaaf161d0c580a1c56790a9a4c54f676a92abc) Thanks [@kevinsproles](https://github.com/kevinsproles)! - Examples and tool descriptions use live demo products: the iPhone 17 Pro Max Case (`BC43BE`), the Canvas Tote (`CAXHQC`) and the All-Over Print Zip Hoodie (`XTNA6J`), in place of the Framed Canvas (`BEEB77`) and Women's Tee (`KMYKUK`), which are no longer for sale. No API change.
16
+
17
+ - Updated dependencies [[`4fbaaf1`](https://github.com/snowcone-app/snowcone-monorepo/commit/4fbaaf161d0c580a1c56790a9a4c54f676a92abc)]:
18
+ - @snowcone-app/sdk@0.24.4
19
+
3
20
  ## 0.9.1
4
21
 
5
22
  ### Patch Changes
package/README.md CHANGED
@@ -38,7 +38,7 @@ import { Shop, Product, ArtSelector, ProductImage, AddToCart } from '@snowcone-a
38
38
  function MyStore() {
39
39
  return (
40
40
  <Shop mode="mock">
41
- <Product productId="BEEB77">
41
+ <Product productId="BC43BE">
42
42
  {/* ArtSelector sets the artwork; ProductImage reads it from context. */}
43
43
  <ArtSelector artworks={['https://example.com/art.png']} />
44
44
  <ProductImage />
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
  );
@@ -6202,6 +6207,15 @@ function RealtimeProvider({
6202
6207
  const compositeQueueRef = (0, import_react25.useRef)(/* @__PURE__ */ new Map());
6203
6208
  const compositeBusyRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
6204
6209
  const compositeCanvasRef = (0, import_react25.useRef)(null);
6210
+ const requestedMockupIdsRef = (0, import_react25.useRef)([]);
6211
+ const fullFrameRerenderIdsRef = (0, import_react25.useRef)(/* @__PURE__ */ new Set());
6212
+ const fullFrameRerenderTimerRef = (0, import_react25.useRef)(null);
6213
+ (0, import_react25.useEffect)(
6214
+ () => () => {
6215
+ if (fullFrameRerenderTimerRef.current) clearTimeout(fullFrameRerenderTimerRef.current);
6216
+ },
6217
+ []
6218
+ );
6205
6219
  const registerBlobUrl = (0, import_react25.useCallback)((blobOrUrl, placementName) => {
6206
6220
  let url;
6207
6221
  if (blobOrUrl instanceof Blob) {
@@ -6233,6 +6247,7 @@ function RealtimeProvider({
6233
6247
  const base = lastConfigBaseRef.current;
6234
6248
  if (!base) return;
6235
6249
  croppedActiveRef.current = withCrop;
6250
+ requestedMockupIdsRef.current = base.mockupIds ?? [];
6236
6251
  sendConfigRaw({
6237
6252
  ...base,
6238
6253
  placementSettings: { ...placementSettingsOverridesRef.current },
@@ -6245,6 +6260,44 @@ function RealtimeProvider({
6245
6260
  if (!croppedActiveRef.current) return;
6246
6261
  sendCropConfig(false);
6247
6262
  }, [sendCropConfig]);
6263
+ const rerenderFullFrame = (0, import_react25.useCallback)(
6264
+ (mockupId) => {
6265
+ fullFrameRerenderIdsRef.current.add(mockupId);
6266
+ if (fullFrameRerenderTimerRef.current) return;
6267
+ fullFrameRerenderTimerRef.current = setTimeout(() => {
6268
+ fullFrameRerenderTimerRef.current = null;
6269
+ const failed = [...fullFrameRerenderIdsRef.current];
6270
+ fullFrameRerenderIdsRef.current.clear();
6271
+ const base = lastConfigBaseRef.current;
6272
+ if (!base || failed.length === 0) return;
6273
+ const previous = requestedMockupIdsRef.current;
6274
+ const ids = mergeMockupIds(previous, failed);
6275
+ croppedActiveRef.current = false;
6276
+ requestedMockupIdsRef.current = ids;
6277
+ const send = (mockupIds) => sendConfigRaw({
6278
+ ...base,
6279
+ mockupIds,
6280
+ placementSettings: { ...placementSettingsOverridesRef.current }
6281
+ });
6282
+ const fingerprint = (list) => [...list].sort().join(",");
6283
+ if (fingerprint(ids) !== fingerprint(previous)) {
6284
+ send(ids);
6285
+ return;
6286
+ }
6287
+ const step = fingerprint(failed) !== fingerprint(previous) ? failed : ids.slice(0, -1);
6288
+ if (step.length === 0) {
6289
+ console.error(
6290
+ `[RealtimeProvider] E_OVERLAY_RERENDER_UNAVAILABLE: cannot force a full-frame re-render of ${failed.join(", ")} (single-mockup session); it updates on the next edit.`
6291
+ );
6292
+ send(ids);
6293
+ return;
6294
+ }
6295
+ send(step);
6296
+ send(ids);
6297
+ }, 0);
6298
+ },
6299
+ [sendConfigRaw]
6300
+ );
6248
6301
  const maybeFlipCrop = (0, import_react25.useCallback)(() => {
6249
6302
  if (!croppedDeliveryEnabled || !cropCapableRef.current) return;
6250
6303
  if (croppedActiveRef.current) return;
@@ -6344,8 +6397,7 @@ function RealtimeProvider({
6344
6397
  console.error(
6345
6398
  `[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
6399
  );
6347
- unflipCrop();
6348
- updateMockupIdsRaw([result.mockupId]);
6400
+ rerenderFullFrame(result.mockupId);
6349
6401
  return;
6350
6402
  }
6351
6403
  try {
@@ -6363,16 +6415,18 @@ function RealtimeProvider({
6363
6415
  if (gen !== sceneGenRef.current) return;
6364
6416
  deliverResults([{ ...result, imageUrl: registerBlobUrl(blob) }]);
6365
6417
  } catch (err) {
6418
+ if (gen !== sceneGenRef.current || compositeQueueRef.current.has(result.mockupId) || processedFrameRef.current.get(result.mockupId) !== result.imageUrl) {
6419
+ return;
6420
+ }
6366
6421
  console.error(
6367
6422
  "[RealtimeProvider] E_OVERLAY_COMPOSITE_FAILED: cropped-frame composite failed \u2014 full-frame delivery for the rest of this session.",
6368
6423
  err
6369
6424
  );
6370
6425
  cropCapableRef.current = false;
6371
- unflipCrop();
6372
- updateMockupIdsRaw([result.mockupId]);
6426
+ rerenderFullFrame(result.mockupId);
6373
6427
  }
6374
6428
  },
6375
- [deliverResults, registerBlobUrl, unflipCrop, updateMockupIdsRaw]
6429
+ [deliverResults, registerBlobUrl, rerenderFullFrame]
6376
6430
  );
6377
6431
  const pumpCompositeQueue = (0, import_react25.useCallback)(
6378
6432
  (mockupId) => {
@@ -6560,6 +6614,7 @@ function RealtimeProvider({
6560
6614
  });
6561
6615
  }
6562
6616
  lastConfigBaseRef.current = configPayload;
6617
+ requestedMockupIdsRef.current = mockupIds;
6563
6618
  sendConfigRaw(
6564
6619
  croppedActiveRef.current ? { ...configPayload, overlayCrop: "auto" } : configPayload
6565
6620
  );
@@ -6619,6 +6674,7 @@ function RealtimeProvider({
6619
6674
  if (croppedActiveRef.current && mockupIds.some((id) => !backgroundsRef.current.has(id))) {
6620
6675
  unflipCrop();
6621
6676
  }
6677
+ requestedMockupIdsRef.current = [...mockupIds];
6622
6678
  return updateMockupIdsRaw(mockupIds);
6623
6679
  },
6624
6680
  [updateMockupIdsRaw, unflipCrop]
@@ -6875,6 +6931,9 @@ function RealtimeProvider({
6875
6931
  },
6876
6932
  []
6877
6933
  );
6934
+ const getLastSentVersionRef = (0, import_react25.useRef)(getLastSentVersion);
6935
+ getLastSentVersionRef.current = getLastSentVersion;
6936
+ const getCanvasVersion = (0, import_react25.useCallback)(() => getLastSentVersionRef.current?.() ?? 0, []);
6878
6937
  const stableSubscribePipelineSettled = (0, import_react25.useCallback)(
6879
6938
  (callback) => {
6880
6939
  return subscribePipelineSettledRef.current(callback);
@@ -6923,6 +6982,7 @@ function RealtimeProvider({
6923
6982
  // Centralized URL management
6924
6983
  getMockupUrl: getMockupUrl3,
6925
6984
  getMockupUrls,
6985
+ getCanvasVersion,
6926
6986
  isMockupLoading,
6927
6987
  registerBlobUrl,
6928
6988
  // WebRTC streaming
@@ -6954,6 +7014,7 @@ function RealtimeProvider({
6954
7014
  hasPendingBlobs,
6955
7015
  getMockupUrl3,
6956
7016
  getMockupUrls,
7017
+ getCanvasVersion,
6957
7018
  isMockupLoading,
6958
7019
  registerBlobUrl
6959
7020
  ]
@@ -7822,17 +7883,33 @@ function MockupPriorityProvider({ children }) {
7822
7883
  if (!realtimeContext?.updateMockupIds) {
7823
7884
  return;
7824
7885
  }
7886
+ const mobile = mobileCarouselRef.current;
7887
+ const universe = mobile && mobile.mockupIds.length > 0 ? mobile.mockupIds : registrationOrderRef.current;
7888
+ const inView = [];
7889
+ if (level1MockupRef.current) {
7890
+ inView.push(level1MockupRef.current);
7891
+ if (!mobile || mobile.mockupIds.length === 0) {
7892
+ visibilityMapRef.current.forEach((info, id) => {
7893
+ if (info.visibleArea > 0 && !inView.includes(id)) inView.push(id);
7894
+ });
7895
+ }
7896
+ }
7825
7897
  let mockupIds;
7826
7898
  switch (level) {
7827
7899
  case 1:
7828
- mockupIds = level1MockupRef.current ? [level1MockupRef.current] : [];
7900
+ mockupIds = inView;
7829
7901
  break;
7830
7902
  case 2:
7831
- mockupIds = [...level2MockupsRef.current];
7903
+ mockupIds = level2MockupsRef.current.filter((id) => !inView.includes(id));
7832
7904
  break;
7833
- case 3:
7834
- mockupIds = [...level3QueueRef.current];
7905
+ case 3: {
7906
+ const earlier = /* @__PURE__ */ new Set([...inView, ...level2MockupsRef.current]);
7907
+ mockupIds = [
7908
+ ...level3QueueRef.current.filter((id) => !earlier.has(id)),
7909
+ ...universe.filter((id) => !earlier.has(id) && !level3QueueRef.current.includes(id))
7910
+ ];
7835
7911
  break;
7912
+ }
7836
7913
  default:
7837
7914
  mockupIds = [];
7838
7915
  }
@@ -7857,6 +7934,16 @@ function MockupPriorityProvider({ children }) {
7857
7934
  const isStillDragging = timeSinceLastPendingBlob < DRAG_STOP_DEBOUNCE_MS;
7858
7935
  if (currentLevel === 1 && isStillDragging) {
7859
7936
  currentRenderingLevelRef.current = null;
7937
+ if (levelAdvanceTimeoutRef.current) {
7938
+ clearTimeout(levelAdvanceTimeoutRef.current);
7939
+ }
7940
+ levelAdvanceTimeoutRef.current = setTimeout(() => {
7941
+ levelAdvanceTimeoutRef.current = null;
7942
+ const quiet = Date.now() - lastPendingBlobTimeRef.current >= DRAG_STOP_DEBOUNCE_MS;
7943
+ if (quiet && currentRenderingLevelRef.current === null && pendingMockupsRef.current.size === 0) {
7944
+ requestMockupsForLevel(2);
7945
+ }
7946
+ }, DRAG_STOP_DEBOUNCE_MS - timeSinceLastPendingBlob + 10);
7860
7947
  return;
7861
7948
  }
7862
7949
  if (isStillDragging) {
@@ -10681,6 +10768,52 @@ var HeroProductImage = (0, import_react46.memo)(function HeroProductImage2({
10681
10768
  unsubscribe?.();
10682
10769
  };
10683
10770
  }, [mode, realtimeContext?.subscribePendingBlob, placement]);
10771
+ const renderVersionRef = (0, import_react46.useRef)(0);
10772
+ const refusalCodeRef = (0, import_react46.useRef)(null);
10773
+ refusalCodeRef.current = unavailable?.code ?? null;
10774
+ const sessionErrorCodeRef = (0, import_react46.useRef)(null);
10775
+ sessionErrorCodeRef.current = realtimeContext?.renderError?.code ?? null;
10776
+ const getCanvasVersion = realtimeContext?.getCanvasVersion;
10777
+ const applyRenderStamp = (0, import_react46.useCallback)(() => {
10778
+ const node = containerRef.current;
10779
+ if (!node) return;
10780
+ const canvas = getCanvasVersion?.() ?? 0;
10781
+ if (canvas <= 0) {
10782
+ delete node.dataset.canvasVersion;
10783
+ delete node.dataset.renderVersion;
10784
+ delete node.dataset.renderState;
10785
+ return;
10786
+ }
10787
+ const rendered = renderVersionRef.current;
10788
+ const behind = rendered < canvas;
10789
+ node.dataset.canvasVersion = String(canvas);
10790
+ node.dataset.renderVersion = String(rendered);
10791
+ node.dataset.renderState = refusalCodeRef.current || behind && sessionErrorCodeRef.current ? "refused" : behind ? "stale" : "current";
10792
+ }, [getCanvasVersion]);
10793
+ (0, import_react46.useEffect)(() => {
10794
+ applyRenderStamp();
10795
+ });
10796
+ (0, import_react46.useEffect)(() => {
10797
+ const subscribeSent = realtimeContext?.subscribeBlobSent;
10798
+ const subscribeResult = realtimeContext?.subscribeMockupResultById;
10799
+ if (!actualMockupId || !subscribeSent || !subscribeResult) return;
10800
+ const offSent = subscribeSent(() => applyRenderStamp());
10801
+ const offResult = subscribeResult(actualMockupId, (result) => {
10802
+ if (typeof result?.requestVersion === "number" && result.requestVersion > renderVersionRef.current) {
10803
+ renderVersionRef.current = result.requestVersion;
10804
+ }
10805
+ applyRenderStamp();
10806
+ });
10807
+ return () => {
10808
+ offSent?.();
10809
+ offResult?.();
10810
+ };
10811
+ }, [
10812
+ actualMockupId,
10813
+ realtimeContext?.subscribeBlobSent,
10814
+ realtimeContext?.subscribeMockupResultById,
10815
+ applyRenderStamp
10816
+ ]);
10684
10817
  (0, import_react46.useEffect)(() => {
10685
10818
  if (!priorityContext || !actualMockupId || !placement) return;
10686
10819
  priorityContext.registerMockup(actualMockupId, placement, initialPriority);