@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 +17 -0
- package/README.md +1 -1
- package/dist/index.cjs +142 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -5
- package/dist/index.d.ts +14 -5
- package/dist/index.js +142 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/composed/ArtworkCustomizer.tsx +2 -2
- package/src/composed/CurrentSelectionDisplay.tsx +1 -1
- package/src/composed/HeroProductImage.tsx +59 -0
- package/src/composed/ProductGallery.tsx +1 -1
- package/src/hooks/useProductGallery.ts +1 -1
- package/src/patterns/MockupPriorityProvider.tsx +49 -7
- package/src/patterns/RealtimeProvider.tsx +123 -5
- package/src/patterns/__tests__/MockupPriorityProvider.dragStop.test.tsx +112 -0
- package/src/patterns/__tests__/RealtimeProvider.cropDelivery.test.tsx +103 -3
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="
|
|
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
|
-
|
|
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
|
-
|
|
6372
|
-
updateMockupIdsRaw([result.mockupId]);
|
|
6426
|
+
rerenderFullFrame(result.mockupId);
|
|
6373
6427
|
}
|
|
6374
6428
|
},
|
|
6375
|
-
[deliverResults, registerBlobUrl,
|
|
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 =
|
|
7900
|
+
mockupIds = inView;
|
|
7829
7901
|
break;
|
|
7830
7902
|
case 2:
|
|
7831
|
-
mockupIds =
|
|
7903
|
+
mockupIds = level2MockupsRef.current.filter((id) => !inView.includes(id));
|
|
7832
7904
|
break;
|
|
7833
|
-
case 3:
|
|
7834
|
-
|
|
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);
|