@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/dist/index.d.cts CHANGED
@@ -1510,7 +1510,7 @@ interface UseProductGalleryReturn {
1510
1510
  * ```tsx
1511
1511
  * // Fetch specific products
1512
1512
  * const { products, loading, error, refetch } = useProductGallery(
1513
- * ['BEEB77', 'CAXHQC'],
1513
+ * ['BC43BE', 'CAXHQC'],
1514
1514
  * { endpoint: 'http://localhost:3000', mode: 'mock' }
1515
1515
  * );
1516
1516
  *
@@ -1599,7 +1599,7 @@ interface ProductGalleryProps extends UseProductGalleryOptions {
1599
1599
  * 2. List all products: Pass empty array or omit productIds
1600
1600
  *
1601
1601
  * @example Specific products
1602
- * <ProductGallery productIds={["BEEB77", "CAXHQC", "KMYKUK"]}>
1602
+ * <ProductGallery productIds={["BC43BE", "CAXHQC", "XTNA6J"]}>
1603
1603
  * {(products, loading, error) => (
1604
1604
  * <div className="grid grid-cols-3 gap-4">
1605
1605
  * {products.map(product => (
@@ -1917,7 +1917,7 @@ interface CurrentSelectionDisplayProps {
1917
1917
  * ```tsx
1918
1918
  * import { Product, ProductOptions, CurrentSelectionDisplay } from '@yourorg/ui-react';
1919
1919
  *
1920
- * <Product productId="BEEB77">
1920
+ * <Product productId="BC43BE">
1921
1921
  * <ProductOptions />
1922
1922
  * <CurrentSelectionDisplay />
1923
1923
  * </Product>
@@ -2928,6 +2928,8 @@ declare function useShopOptional(): ShopContextValue | undefined;
2928
2928
  interface MockupResult {
2929
2929
  mockupId: string;
2930
2930
  imageUrl: string;
2931
+ /** The canvas version this frame was rendered from (see getCanvasVersion). */
2932
+ requestVersion?: number;
2931
2933
  }
2932
2934
  type RealtimeState = RealtimeContextValue;
2933
2935
  interface RealtimeContextValue {
@@ -3043,6 +3045,13 @@ interface RealtimeContextValue {
3043
3045
  * subsequent blobs to the same placement.
3044
3046
  */
3045
3047
  subscribeBlobSent: (callback: (placement: string) => void) => () => void;
3048
+ /**
3049
+ * The canvas version the session last sent (0 before the first edit
3050
+ * reaches the server). Every rendered result carries the `requestVersion`
3051
+ * it was rendered from: a picture whose version is below this one shows an
3052
+ * older design. Read it after a `subscribeBlobSent` notification.
3053
+ */
3054
+ getCanvasVersion: () => number;
3046
3055
  flushPendingBlobs: () => void;
3047
3056
  hasPendingBlobs: () => boolean;
3048
3057
  subscribePipelineSettled: (callback: () => void) => () => void;
@@ -4265,7 +4274,7 @@ type ArtworkCustomizerProps = {
4265
4274
  * // CORRECT: Use with ArtSelector to manage artwork via context
4266
4275
  * import { Product, ArtSelector, ArtworkCustomizer } from '@snowcone-app/ui';
4267
4276
  *
4268
- * <Product productId="BEEB77">
4277
+ * <Product productId="BC43BE">
4269
4278
  * <ArtSelector artworks={[
4270
4279
  * { type: 'regular', src: 'https://example.com/photo.jpg' },
4271
4280
  * { type: 'pattern', src: 'https://example.com/pattern.jpg', tileCount: 1 }
@@ -4279,7 +4288,7 @@ type ArtworkCustomizerProps = {
4279
4288
  * // CORRECT: Full example with ProductImage
4280
4289
  * import { Product, ProductImage, ArtSelector, ArtworkCustomizer } from '@snowcone-app/ui';
4281
4290
  *
4282
- * <Product productId="BEEB77">
4291
+ * <Product productId="BC43BE">
4283
4292
  * <div className="grid grid-cols-2 gap-8">
4284
4293
  * <ProductImage />
4285
4294
  * <div>
package/dist/index.d.ts CHANGED
@@ -1510,7 +1510,7 @@ interface UseProductGalleryReturn {
1510
1510
  * ```tsx
1511
1511
  * // Fetch specific products
1512
1512
  * const { products, loading, error, refetch } = useProductGallery(
1513
- * ['BEEB77', 'CAXHQC'],
1513
+ * ['BC43BE', 'CAXHQC'],
1514
1514
  * { endpoint: 'http://localhost:3000', mode: 'mock' }
1515
1515
  * );
1516
1516
  *
@@ -1599,7 +1599,7 @@ interface ProductGalleryProps extends UseProductGalleryOptions {
1599
1599
  * 2. List all products: Pass empty array or omit productIds
1600
1600
  *
1601
1601
  * @example Specific products
1602
- * <ProductGallery productIds={["BEEB77", "CAXHQC", "KMYKUK"]}>
1602
+ * <ProductGallery productIds={["BC43BE", "CAXHQC", "XTNA6J"]}>
1603
1603
  * {(products, loading, error) => (
1604
1604
  * <div className="grid grid-cols-3 gap-4">
1605
1605
  * {products.map(product => (
@@ -1917,7 +1917,7 @@ interface CurrentSelectionDisplayProps {
1917
1917
  * ```tsx
1918
1918
  * import { Product, ProductOptions, CurrentSelectionDisplay } from '@yourorg/ui-react';
1919
1919
  *
1920
- * <Product productId="BEEB77">
1920
+ * <Product productId="BC43BE">
1921
1921
  * <ProductOptions />
1922
1922
  * <CurrentSelectionDisplay />
1923
1923
  * </Product>
@@ -2928,6 +2928,8 @@ declare function useShopOptional(): ShopContextValue | undefined;
2928
2928
  interface MockupResult {
2929
2929
  mockupId: string;
2930
2930
  imageUrl: string;
2931
+ /** The canvas version this frame was rendered from (see getCanvasVersion). */
2932
+ requestVersion?: number;
2931
2933
  }
2932
2934
  type RealtimeState = RealtimeContextValue;
2933
2935
  interface RealtimeContextValue {
@@ -3043,6 +3045,13 @@ interface RealtimeContextValue {
3043
3045
  * subsequent blobs to the same placement.
3044
3046
  */
3045
3047
  subscribeBlobSent: (callback: (placement: string) => void) => () => void;
3048
+ /**
3049
+ * The canvas version the session last sent (0 before the first edit
3050
+ * reaches the server). Every rendered result carries the `requestVersion`
3051
+ * it was rendered from: a picture whose version is below this one shows an
3052
+ * older design. Read it after a `subscribeBlobSent` notification.
3053
+ */
3054
+ getCanvasVersion: () => number;
3046
3055
  flushPendingBlobs: () => void;
3047
3056
  hasPendingBlobs: () => boolean;
3048
3057
  subscribePipelineSettled: (callback: () => void) => () => void;
@@ -4265,7 +4274,7 @@ type ArtworkCustomizerProps = {
4265
4274
  * // CORRECT: Use with ArtSelector to manage artwork via context
4266
4275
  * import { Product, ArtSelector, ArtworkCustomizer } from '@snowcone-app/ui';
4267
4276
  *
4268
- * <Product productId="BEEB77">
4277
+ * <Product productId="BC43BE">
4269
4278
  * <ArtSelector artworks={[
4270
4279
  * { type: 'regular', src: 'https://example.com/photo.jpg' },
4271
4280
  * { type: 'pattern', src: 'https://example.com/pattern.jpg', tileCount: 1 }
@@ -4279,7 +4288,7 @@ type ArtworkCustomizerProps = {
4279
4288
  * // CORRECT: Full example with ProductImage
4280
4289
  * import { Product, ProductImage, ArtSelector, ArtworkCustomizer } from '@snowcone-app/ui';
4281
4290
  *
4282
- * <Product productId="BEEB77">
4291
+ * <Product productId="BC43BE">
4283
4292
  * <div className="grid grid-cols-2 gap-8">
4284
4293
  * <ProductImage />
4285
4294
  * <div>
package/dist/index.js CHANGED
@@ -5703,6 +5703,11 @@ async function compositeCroppedFrame(args) {
5703
5703
  import { jsx as jsx52 } from "react/jsx-runtime";
5704
5704
  var DEFAULT_ARTBOARD_SIZE = 2e3;
5705
5705
  var MAX_CONCURRENT_BLOBS = 5;
5706
+ function mergeMockupIds(current, extra) {
5707
+ const out = [...current];
5708
+ for (const id of extra) if (!out.includes(id)) out.push(id);
5709
+ return out;
5710
+ }
5706
5711
  var RealtimeContext = createContext8(
5707
5712
  void 0
5708
5713
  );
@@ -5843,6 +5848,15 @@ function RealtimeProvider({
5843
5848
  const compositeQueueRef = useRef12(/* @__PURE__ */ new Map());
5844
5849
  const compositeBusyRef = useRef12(/* @__PURE__ */ new Set());
5845
5850
  const compositeCanvasRef = useRef12(null);
5851
+ const requestedMockupIdsRef = useRef12([]);
5852
+ const fullFrameRerenderIdsRef = useRef12(/* @__PURE__ */ new Set());
5853
+ const fullFrameRerenderTimerRef = useRef12(null);
5854
+ useEffect17(
5855
+ () => () => {
5856
+ if (fullFrameRerenderTimerRef.current) clearTimeout(fullFrameRerenderTimerRef.current);
5857
+ },
5858
+ []
5859
+ );
5846
5860
  const registerBlobUrl = useCallback10((blobOrUrl, placementName) => {
5847
5861
  let url;
5848
5862
  if (blobOrUrl instanceof Blob) {
@@ -5874,6 +5888,7 @@ function RealtimeProvider({
5874
5888
  const base = lastConfigBaseRef.current;
5875
5889
  if (!base) return;
5876
5890
  croppedActiveRef.current = withCrop;
5891
+ requestedMockupIdsRef.current = base.mockupIds ?? [];
5877
5892
  sendConfigRaw({
5878
5893
  ...base,
5879
5894
  placementSettings: { ...placementSettingsOverridesRef.current },
@@ -5886,6 +5901,44 @@ function RealtimeProvider({
5886
5901
  if (!croppedActiveRef.current) return;
5887
5902
  sendCropConfig(false);
5888
5903
  }, [sendCropConfig]);
5904
+ const rerenderFullFrame = useCallback10(
5905
+ (mockupId) => {
5906
+ fullFrameRerenderIdsRef.current.add(mockupId);
5907
+ if (fullFrameRerenderTimerRef.current) return;
5908
+ fullFrameRerenderTimerRef.current = setTimeout(() => {
5909
+ fullFrameRerenderTimerRef.current = null;
5910
+ const failed = [...fullFrameRerenderIdsRef.current];
5911
+ fullFrameRerenderIdsRef.current.clear();
5912
+ const base = lastConfigBaseRef.current;
5913
+ if (!base || failed.length === 0) return;
5914
+ const previous = requestedMockupIdsRef.current;
5915
+ const ids = mergeMockupIds(previous, failed);
5916
+ croppedActiveRef.current = false;
5917
+ requestedMockupIdsRef.current = ids;
5918
+ const send = (mockupIds) => sendConfigRaw({
5919
+ ...base,
5920
+ mockupIds,
5921
+ placementSettings: { ...placementSettingsOverridesRef.current }
5922
+ });
5923
+ const fingerprint = (list) => [...list].sort().join(",");
5924
+ if (fingerprint(ids) !== fingerprint(previous)) {
5925
+ send(ids);
5926
+ return;
5927
+ }
5928
+ const step = fingerprint(failed) !== fingerprint(previous) ? failed : ids.slice(0, -1);
5929
+ if (step.length === 0) {
5930
+ console.error(
5931
+ `[RealtimeProvider] E_OVERLAY_RERENDER_UNAVAILABLE: cannot force a full-frame re-render of ${failed.join(", ")} (single-mockup session); it updates on the next edit.`
5932
+ );
5933
+ send(ids);
5934
+ return;
5935
+ }
5936
+ send(step);
5937
+ send(ids);
5938
+ }, 0);
5939
+ },
5940
+ [sendConfigRaw]
5941
+ );
5889
5942
  const maybeFlipCrop = useCallback10(() => {
5890
5943
  if (!croppedDeliveryEnabled || !cropCapableRef.current) return;
5891
5944
  if (croppedActiveRef.current) return;
@@ -5985,8 +6038,7 @@ function RealtimeProvider({
5985
6038
  console.error(
5986
6039
  `[RealtimeProvider] E_OVERLAY_NO_BACKGROUND: cropped frame for ${result.mockupId} has no frozen background \u2014 falling back to full-frame and re-rendering it.`
5987
6040
  );
5988
- unflipCrop();
5989
- updateMockupIdsRaw([result.mockupId]);
6041
+ rerenderFullFrame(result.mockupId);
5990
6042
  return;
5991
6043
  }
5992
6044
  try {
@@ -6004,16 +6056,18 @@ function RealtimeProvider({
6004
6056
  if (gen !== sceneGenRef.current) return;
6005
6057
  deliverResults([{ ...result, imageUrl: registerBlobUrl(blob) }]);
6006
6058
  } catch (err) {
6059
+ if (gen !== sceneGenRef.current || compositeQueueRef.current.has(result.mockupId) || processedFrameRef.current.get(result.mockupId) !== result.imageUrl) {
6060
+ return;
6061
+ }
6007
6062
  console.error(
6008
6063
  "[RealtimeProvider] E_OVERLAY_COMPOSITE_FAILED: cropped-frame composite failed \u2014 full-frame delivery for the rest of this session.",
6009
6064
  err
6010
6065
  );
6011
6066
  cropCapableRef.current = false;
6012
- unflipCrop();
6013
- updateMockupIdsRaw([result.mockupId]);
6067
+ rerenderFullFrame(result.mockupId);
6014
6068
  }
6015
6069
  },
6016
- [deliverResults, registerBlobUrl, unflipCrop, updateMockupIdsRaw]
6070
+ [deliverResults, registerBlobUrl, rerenderFullFrame]
6017
6071
  );
6018
6072
  const pumpCompositeQueue = useCallback10(
6019
6073
  (mockupId) => {
@@ -6201,6 +6255,7 @@ function RealtimeProvider({
6201
6255
  });
6202
6256
  }
6203
6257
  lastConfigBaseRef.current = configPayload;
6258
+ requestedMockupIdsRef.current = mockupIds;
6204
6259
  sendConfigRaw(
6205
6260
  croppedActiveRef.current ? { ...configPayload, overlayCrop: "auto" } : configPayload
6206
6261
  );
@@ -6260,6 +6315,7 @@ function RealtimeProvider({
6260
6315
  if (croppedActiveRef.current && mockupIds.some((id) => !backgroundsRef.current.has(id))) {
6261
6316
  unflipCrop();
6262
6317
  }
6318
+ requestedMockupIdsRef.current = [...mockupIds];
6263
6319
  return updateMockupIdsRaw(mockupIds);
6264
6320
  },
6265
6321
  [updateMockupIdsRaw, unflipCrop]
@@ -6516,6 +6572,9 @@ function RealtimeProvider({
6516
6572
  },
6517
6573
  []
6518
6574
  );
6575
+ const getLastSentVersionRef = useRef12(getLastSentVersion);
6576
+ getLastSentVersionRef.current = getLastSentVersion;
6577
+ const getCanvasVersion = useCallback10(() => getLastSentVersionRef.current?.() ?? 0, []);
6519
6578
  const stableSubscribePipelineSettled = useCallback10(
6520
6579
  (callback) => {
6521
6580
  return subscribePipelineSettledRef.current(callback);
@@ -6564,6 +6623,7 @@ function RealtimeProvider({
6564
6623
  // Centralized URL management
6565
6624
  getMockupUrl: getMockupUrl3,
6566
6625
  getMockupUrls,
6626
+ getCanvasVersion,
6567
6627
  isMockupLoading,
6568
6628
  registerBlobUrl,
6569
6629
  // WebRTC streaming
@@ -6595,6 +6655,7 @@ function RealtimeProvider({
6595
6655
  hasPendingBlobs,
6596
6656
  getMockupUrl3,
6597
6657
  getMockupUrls,
6658
+ getCanvasVersion,
6598
6659
  isMockupLoading,
6599
6660
  registerBlobUrl
6600
6661
  ]
@@ -7471,17 +7532,33 @@ function MockupPriorityProvider({ children }) {
7471
7532
  if (!realtimeContext?.updateMockupIds) {
7472
7533
  return;
7473
7534
  }
7535
+ const mobile = mobileCarouselRef.current;
7536
+ const universe = mobile && mobile.mockupIds.length > 0 ? mobile.mockupIds : registrationOrderRef.current;
7537
+ const inView = [];
7538
+ if (level1MockupRef.current) {
7539
+ inView.push(level1MockupRef.current);
7540
+ if (!mobile || mobile.mockupIds.length === 0) {
7541
+ visibilityMapRef.current.forEach((info, id) => {
7542
+ if (info.visibleArea > 0 && !inView.includes(id)) inView.push(id);
7543
+ });
7544
+ }
7545
+ }
7474
7546
  let mockupIds;
7475
7547
  switch (level) {
7476
7548
  case 1:
7477
- mockupIds = level1MockupRef.current ? [level1MockupRef.current] : [];
7549
+ mockupIds = inView;
7478
7550
  break;
7479
7551
  case 2:
7480
- mockupIds = [...level2MockupsRef.current];
7552
+ mockupIds = level2MockupsRef.current.filter((id) => !inView.includes(id));
7481
7553
  break;
7482
- case 3:
7483
- mockupIds = [...level3QueueRef.current];
7554
+ case 3: {
7555
+ const earlier = /* @__PURE__ */ new Set([...inView, ...level2MockupsRef.current]);
7556
+ mockupIds = [
7557
+ ...level3QueueRef.current.filter((id) => !earlier.has(id)),
7558
+ ...universe.filter((id) => !earlier.has(id) && !level3QueueRef.current.includes(id))
7559
+ ];
7484
7560
  break;
7561
+ }
7485
7562
  default:
7486
7563
  mockupIds = [];
7487
7564
  }
@@ -7506,6 +7583,16 @@ function MockupPriorityProvider({ children }) {
7506
7583
  const isStillDragging = timeSinceLastPendingBlob < DRAG_STOP_DEBOUNCE_MS;
7507
7584
  if (currentLevel === 1 && isStillDragging) {
7508
7585
  currentRenderingLevelRef.current = null;
7586
+ if (levelAdvanceTimeoutRef.current) {
7587
+ clearTimeout(levelAdvanceTimeoutRef.current);
7588
+ }
7589
+ levelAdvanceTimeoutRef.current = setTimeout(() => {
7590
+ levelAdvanceTimeoutRef.current = null;
7591
+ const quiet = Date.now() - lastPendingBlobTimeRef.current >= DRAG_STOP_DEBOUNCE_MS;
7592
+ if (quiet && currentRenderingLevelRef.current === null && pendingMockupsRef.current.size === 0) {
7593
+ requestMockupsForLevel(2);
7594
+ }
7595
+ }, DRAG_STOP_DEBOUNCE_MS - timeSinceLastPendingBlob + 10);
7509
7596
  return;
7510
7597
  }
7511
7598
  if (isStillDragging) {
@@ -10363,6 +10450,52 @@ var HeroProductImage = memo3(function HeroProductImage2({
10363
10450
  unsubscribe?.();
10364
10451
  };
10365
10452
  }, [mode, realtimeContext?.subscribePendingBlob, placement]);
10453
+ const renderVersionRef = useRef23(0);
10454
+ const refusalCodeRef = useRef23(null);
10455
+ refusalCodeRef.current = unavailable?.code ?? null;
10456
+ const sessionErrorCodeRef = useRef23(null);
10457
+ sessionErrorCodeRef.current = realtimeContext?.renderError?.code ?? null;
10458
+ const getCanvasVersion = realtimeContext?.getCanvasVersion;
10459
+ const applyRenderStamp = useCallback18(() => {
10460
+ const node = containerRef.current;
10461
+ if (!node) return;
10462
+ const canvas = getCanvasVersion?.() ?? 0;
10463
+ if (canvas <= 0) {
10464
+ delete node.dataset.canvasVersion;
10465
+ delete node.dataset.renderVersion;
10466
+ delete node.dataset.renderState;
10467
+ return;
10468
+ }
10469
+ const rendered = renderVersionRef.current;
10470
+ const behind = rendered < canvas;
10471
+ node.dataset.canvasVersion = String(canvas);
10472
+ node.dataset.renderVersion = String(rendered);
10473
+ node.dataset.renderState = refusalCodeRef.current || behind && sessionErrorCodeRef.current ? "refused" : behind ? "stale" : "current";
10474
+ }, [getCanvasVersion]);
10475
+ useEffect32(() => {
10476
+ applyRenderStamp();
10477
+ });
10478
+ useEffect32(() => {
10479
+ const subscribeSent = realtimeContext?.subscribeBlobSent;
10480
+ const subscribeResult = realtimeContext?.subscribeMockupResultById;
10481
+ if (!actualMockupId || !subscribeSent || !subscribeResult) return;
10482
+ const offSent = subscribeSent(() => applyRenderStamp());
10483
+ const offResult = subscribeResult(actualMockupId, (result) => {
10484
+ if (typeof result?.requestVersion === "number" && result.requestVersion > renderVersionRef.current) {
10485
+ renderVersionRef.current = result.requestVersion;
10486
+ }
10487
+ applyRenderStamp();
10488
+ });
10489
+ return () => {
10490
+ offSent?.();
10491
+ offResult?.();
10492
+ };
10493
+ }, [
10494
+ actualMockupId,
10495
+ realtimeContext?.subscribeBlobSent,
10496
+ realtimeContext?.subscribeMockupResultById,
10497
+ applyRenderStamp
10498
+ ]);
10366
10499
  useEffect32(() => {
10367
10500
  if (!priorityContext || !actualMockupId || !placement) return;
10368
10501
  priorityContext.registerMockup(actualMockupId, placement, initialPriority);