@snowcone-app/ui 0.4.7 → 0.4.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowcone-app/ui",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "React components for merchandise visualization and customization",
5
5
  "keywords": [
6
6
  "react",
@@ -103,7 +103,7 @@
103
103
  "react-instantsearch": "^7.15.5",
104
104
  "react-zoom-pan-pinch": "^3.6.4",
105
105
  "tailwind-merge": "^3.0.0",
106
- "@snowcone-app/sdk": "0.20.0"
106
+ "@snowcone-app/sdk": "0.20.2"
107
107
  },
108
108
  "devDependencies": {
109
109
  "@chromatic-com/storybook": "^4.1.2",
@@ -42,6 +42,7 @@ import type { RealtimeRenderError } from "@snowcone-app/sdk";
42
42
  import { readEnv } from "../lib/env";
43
43
  import { useProductOptional } from "./Product";
44
44
  import type { ReactProductContext } from "./Product";
45
+ import { useMockupBackgroundOptional } from "./MockupBackground";
45
46
 
46
47
  // Default artboard size for export calculations (matches @snowcone-app/canvas default)
47
48
  const DEFAULT_ARTBOARD_SIZE = 2000;
@@ -126,6 +127,26 @@ export interface RealtimeContextValue {
126
127
  */
127
128
  updatePlacementSettings: (settings: Record<string, { scaleMode?: 'fill' | 'fit'; tiles?: number; alignment?: string }>) => boolean;
128
129
 
130
+ /**
131
+ * Declare spread-mode crop geometry (ADR-0060) for canvases that run
132
+ * combined spread/group artboards: artboard name → artboard dims + piece
133
+ * rects keyed by the product's placement labels. Sent on the realtime
134
+ * config (re-sent when it changes) so the server can slice each rendered
135
+ * spread canvas_state into per-placement images. Pass undefined to clear.
136
+ */
137
+ setSpreadCrops: (
138
+ crops:
139
+ | Record<
140
+ string,
141
+ {
142
+ width: number;
143
+ height: number;
144
+ pieces: Array<{ placement: string; x: number; y: number; width: number; height: number }>;
145
+ }
146
+ >
147
+ | undefined
148
+ ) => void;
149
+
129
150
  // PERFORMANCE: Direct access without state updates
130
151
  getMockupResultsImmediate: () => MockupResult[];
131
152
 
@@ -273,6 +294,9 @@ export function RealtimeProvider({
273
294
  grantUrl,
274
295
  }: RealtimeProviderProps) {
275
296
  const productContext = useProductOptional();
297
+ // Page background for renders (see MockupBackground) — sent on the config so
298
+ // realtime results match static `bg=` mockup URLs.
299
+ const mockupBg = useMockupBackgroundOptional();
276
300
 
277
301
  const [isEnabled, setIsEnabled] = useState(autoEnable);
278
302
 
@@ -305,6 +329,22 @@ export function RealtimeProvider({
305
329
 
306
330
  // Track last sent variantId to detect when we need to re-send config
307
331
  const lastSentVariantIdRef = useRef<string | null>(null);
332
+ const lastSentBgRef = useRef<string | undefined>(undefined);
333
+ // Spread crop geometry (ADR-0060) declared via setSpreadCrops. Ref holds
334
+ // the payload; the version state re-runs the config effect on change.
335
+ const spreadCropsRef = useRef<
336
+ | Record<
337
+ string,
338
+ {
339
+ width: number;
340
+ height: number;
341
+ pieces: Array<{ placement: string; x: number; y: number; width: number; height: number }>;
342
+ }
343
+ >
344
+ | undefined
345
+ >(undefined);
346
+ const [spreadCropsVersion, setSpreadCropsVersion] = useState(0);
347
+ const lastSentSpreadCropsRef = useRef<string>("null");
308
348
 
309
349
  // Track last sent colors to prevent duplicates
310
350
  const lastSentColorsRef = useRef<Record<string, string>>({});
@@ -577,6 +617,8 @@ export function RealtimeProvider({
577
617
  disconnect();
578
618
  configSentRef.current = false;
579
619
  lastSentVariantIdRef.current = null;
620
+ lastSentBgRef.current = undefined;
621
+ lastSentSpreadCropsRef.current = "null";
580
622
  lastSentColorsRef.current = {};
581
623
  };
582
624
  }
@@ -611,8 +653,14 @@ export function RealtimeProvider({
611
653
  const isFirstSend = !configSentRef.current;
612
654
  const variantChanged = lastSentVariantIdRef.current !== null &&
613
655
  lastSentVariantIdRef.current !== variantId;
614
-
615
- if (!isFirstSend && !variantChanged) {
656
+ // 3. Background changed (theme flip swaps the MockupBackground hex)
657
+ const bgChanged = configSentRef.current && lastSentBgRef.current !== mockupBg;
658
+ // 4. Spread crop geometry changed (spread-mode editor mounted/unmounted)
659
+ const spreadCropsJson = JSON.stringify(spreadCropsRef.current ?? null);
660
+ const spreadCropsChanged =
661
+ configSentRef.current && lastSentSpreadCropsRef.current !== spreadCropsJson;
662
+
663
+ if (!isFirstSend && !variantChanged && !bgChanged && !spreadCropsChanged) {
616
664
  return; // Nothing changed, skip
617
665
  }
618
666
 
@@ -637,6 +685,8 @@ export function RealtimeProvider({
637
685
  variantId,
638
686
  shop: shop as string,
639
687
  width: mockupWidth,
688
+ ...(mockupBg ? { bg: mockupBg } : {}),
689
+ ...(spreadCropsRef.current ? { spreadCrops: spreadCropsRef.current } : {}),
640
690
  placementSettings: { ...placementSettingsOverridesRef.current },
641
691
  }
642
692
  : {
@@ -646,6 +696,8 @@ export function RealtimeProvider({
646
696
  shop: "hTidnpNnNa",
647
697
  seal: "bypass-sig-for-k6-load-test",
648
698
  width: mockupWidth,
699
+ ...(mockupBg ? { bg: mockupBg } : {}),
700
+ ...(spreadCropsRef.current ? { spreadCrops: spreadCropsRef.current } : {}),
649
701
  placementSettings: { ...placementSettingsOverridesRef.current },
650
702
  };
651
703
 
@@ -662,6 +714,8 @@ export function RealtimeProvider({
662
714
 
663
715
  configSentRef.current = true;
664
716
  lastSentVariantIdRef.current = variantId;
717
+ lastSentBgRef.current = mockupBg;
718
+ lastSentSpreadCropsRef.current = spreadCropsJson;
665
719
 
666
720
  // Send default colors for color placements
667
721
  setTimeout(() => {
@@ -682,6 +736,8 @@ export function RealtimeProvider({
682
736
  sendConfigRaw,
683
737
  sendColorBlobTracked,
684
738
  mockupWidth,
739
+ mockupBg,
740
+ spreadCropsVersion,
685
741
  ]);
686
742
 
687
743
  // Watch for color placement changes
@@ -725,6 +781,32 @@ export function RealtimeProvider({
725
781
  [updatePlacementSettingsRaw]
726
782
  );
727
783
 
784
+ // Declare spread crop geometry (ADR-0060). Stored in a ref; bumping the
785
+ // version state re-runs the config effect, which re-sends the config when
786
+ // the serialized geometry actually changed.
787
+ const setSpreadCrops = useCallback(
788
+ (
789
+ crops:
790
+ | Record<
791
+ string,
792
+ {
793
+ width: number;
794
+ height: number;
795
+ pieces: Array<{ placement: string; x: number; y: number; width: number; height: number }>;
796
+ }
797
+ >
798
+ | undefined
799
+ ) => {
800
+ const next = crops && Object.keys(crops).length > 0 ? crops : undefined;
801
+ if (JSON.stringify(next ?? null) === JSON.stringify(spreadCropsRef.current ?? null)) {
802
+ return;
803
+ }
804
+ spreadCropsRef.current = next;
805
+ setSpreadCropsVersion((v) => v + 1);
806
+ },
807
+ []
808
+ );
809
+
728
810
  const enableRealtime = useCallback(() => {
729
811
  setIsEnabled(true);
730
812
  }, []);
@@ -1099,6 +1181,7 @@ export function RealtimeProvider({
1099
1181
  sendCanvasState,
1100
1182
  updateMockupIds: updateMockupIdsRaw,
1101
1183
  updatePlacementSettings: updatePlacementSettingsWrapped,
1184
+ setSpreadCrops,
1102
1185
  getMockupResultsImmediate: () => mockupResultsRef.current,
1103
1186
  subscribeMockupResults: stableSubscribeMockupResults,
1104
1187
  subscribeMockupResultById: stableSubscribeMockupResultById,
@@ -1135,6 +1218,7 @@ export function RealtimeProvider({
1135
1218
  sendCanvasState,
1136
1219
  updateMockupIdsRaw,
1137
1220
  updatePlacementSettingsWrapped,
1221
+ setSpreadCrops,
1138
1222
  stableSubscribeMockupResults,
1139
1223
  stableSubscribeMockupResultById,
1140
1224
  notifyPendingExport,