@snowcone-app/ui 0.1.42 → 0.2.0

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.
Files changed (192) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +18 -4
  3. package/package.json +9 -5
  4. package/src/components/CanvasIsolationBoundary.tsx +202 -0
  5. package/src/components/LoadingOverlayPrism.tsx +251 -0
  6. package/src/composed/AddToCart.tsx +229 -0
  7. package/src/composed/ArtAlignment.tsx +703 -0
  8. package/src/composed/ArtSelector.tsx +290 -0
  9. package/src/composed/ArtworkCustomizer.tsx +212 -0
  10. package/src/composed/CanvasEditor.tsx +79 -0
  11. package/src/composed/ColorPicker.tsx +111 -0
  12. package/src/composed/CurrentSelectionDisplay.tsx +86 -0
  13. package/src/composed/HeroProductImage.tsx +1071 -0
  14. package/src/composed/Lightbox.index.ts +2 -0
  15. package/src/composed/Lightbox.tsx +230 -0
  16. package/src/composed/PlacementClipShapeSelector.tsx +88 -0
  17. package/src/composed/PlacementTabs.tsx +179 -0
  18. package/src/composed/ProductCard.tsx +298 -0
  19. package/src/composed/ProductGallery.tsx +54 -0
  20. package/src/composed/ProductImage.tsx +129 -0
  21. package/src/composed/ProductList.tsx +147 -0
  22. package/src/composed/ProductOptions.tsx +305 -0
  23. package/src/composed/RealtimeMockup.tsx +121 -0
  24. package/src/composed/TileCount.tsx +348 -0
  25. package/src/composed/carousels/HeroCarousel.tsx +240 -0
  26. package/src/composed/carousels/MobileProductCarousel.tsx +1002 -0
  27. package/src/composed/carousels/index.ts +11 -0
  28. package/src/composed/carousels/types.ts +58 -0
  29. package/src/composed/grids/MasonryGrid.tsx +238 -0
  30. package/src/composed/grids/index.ts +9 -0
  31. package/src/composed/search/CurrentRefinements.tsx +80 -0
  32. package/src/composed/search/Filters.tsx +49 -0
  33. package/src/composed/search/FiltersButton.tsx +57 -0
  34. package/src/composed/search/FiltersDrawer.tsx +375 -0
  35. package/src/composed/search/ProductGrid.tsx +118 -0
  36. package/src/composed/search/ProductHit.tsx +56 -0
  37. package/src/composed/search/SearchBox.tsx +109 -0
  38. package/src/composed/search/SearchProvider.tsx +136 -0
  39. package/src/composed/search/facetConfig.ts +16 -0
  40. package/src/composed/search/index.ts +22 -0
  41. package/src/composed/search/meilisearchAdapter.ts +20 -0
  42. package/src/composed/search/types.ts +22 -0
  43. package/src/composed/zoom/EnhancedImageViewer.tsx +505 -0
  44. package/src/composed/zoom/ResponsiveZoom.tsx +134 -0
  45. package/src/composed/zoom/ZoomOverlay.tsx +194 -0
  46. package/src/composed/zoom/index.ts +12 -0
  47. package/src/composed/zoom/types.ts +12 -0
  48. package/src/design-system/ColorPalette.tsx +126 -0
  49. package/src/design-system/ColorSwatch.tsx +49 -0
  50. package/src/design-system/DesignSystemPage.tsx +130 -0
  51. package/src/design-system/ThemeSwitcher.tsx +181 -0
  52. package/src/design-system/TypographyScale.tsx +106 -0
  53. package/src/design-system/index.ts +5 -0
  54. package/src/ecommerce/stories/HeroProductImage.stories.tsx +66 -0
  55. package/src/ecommerce/stories/PDPHeroGallery.stories.tsx +105 -0
  56. package/src/ecommerce/stories/PDPInfoPanel.stories.tsx +472 -0
  57. package/src/ecommerce/stories/PDPLayout.stories.tsx +365 -0
  58. package/src/hooks/useBrand.ts +41 -0
  59. package/src/hooks/useCanvasContext.ts +127 -0
  60. package/src/hooks/useDeviceDetection.ts +64 -0
  61. package/src/hooks/useFocusTrap.ts +70 -0
  62. package/src/hooks/useImagePreloader.ts +268 -0
  63. package/src/hooks/useImageTransition.ts +608 -0
  64. package/src/hooks/usePlacementsProcessor.ts +74 -0
  65. package/src/hooks/useProductGallery.ts +193 -0
  66. package/src/hooks/useProductPage.ts +467 -0
  67. package/src/hooks/useRenderGuard.ts +96 -0
  68. package/src/hooks/useScrollDirection.ts +196 -0
  69. package/src/hooks/viewport/index.ts +25 -0
  70. package/src/hooks/viewport/useContainerWidth.ts +59 -0
  71. package/src/hooks/viewport/useMediaQuery.ts +52 -0
  72. package/src/hooks/viewport/useResponsiveImageCap.ts +149 -0
  73. package/src/hooks/viewport/useViewportDimensions.ts +135 -0
  74. package/src/hooks/viewport/useWideMonitorMode.ts +150 -0
  75. package/src/hooks/visibility/index.ts +15 -0
  76. package/src/hooks/visibility/observerPool.ts +150 -0
  77. package/src/index.ts +240 -0
  78. package/src/layouts/hero-zoom/HeroShrinkLayout.tsx +209 -0
  79. package/src/layouts/hero-zoom/HeroZoomLayout.tsx +351 -0
  80. package/src/layouts/hero-zoom/index.ts +30 -0
  81. package/src/layouts/hero-zoom/stories/HeroZoomLayout.stories.tsx +350 -0
  82. package/src/layouts/hero-zoom/types.ts +113 -0
  83. package/src/layouts/hero-zoom/useHeroZoomScales.ts +156 -0
  84. package/src/layouts/index.ts +9 -0
  85. package/src/layouts/pdp/EdgeBlurBox.tsx +210 -0
  86. package/src/layouts/pdp/ImageBlurExtension.tsx +215 -0
  87. package/src/layouts/pdp/ImageEdgeBlur.tsx +215 -0
  88. package/src/layouts/pdp/PDPLayout.tsx +246 -0
  89. package/src/layouts/pdp/SimpleImageBlur.tsx +140 -0
  90. package/src/layouts/pdp/index.ts +40 -0
  91. package/src/lib/env.ts +15 -0
  92. package/src/lib/locale.ts +167 -0
  93. package/src/lib/router.tsx +46 -0
  94. package/src/lib/utils.ts +6 -0
  95. package/src/lightbox/README.md +77 -0
  96. package/src/next/index.tsx +26 -0
  97. package/src/patterns/MockupPriorityProvider.tsx +1014 -0
  98. package/src/patterns/Product.tsx +850 -0
  99. package/src/patterns/ProductPageProvider.tsx +224 -0
  100. package/src/patterns/RealtimeProvider.tsx +1162 -0
  101. package/src/patterns/ShopProvider.tsx +603 -0
  102. package/src/personalization/PersonalizationBridge.tsx +235 -0
  103. package/src/personalization/PersonalizationContext.ts +29 -0
  104. package/src/personalization/PersonalizationInputs.tsx +110 -0
  105. package/src/personalization/PersonalizationProvider.tsx +407 -0
  106. package/src/personalization/canvas-stub.d.ts +22 -0
  107. package/src/personalization/index.ts +43 -0
  108. package/src/personalization/types.ts +48 -0
  109. package/src/personalization/usePersonalization.ts +32 -0
  110. package/src/personalization/usePersonalizationShimmer.ts +159 -0
  111. package/src/personalization/utils.ts +59 -0
  112. package/src/primitives/BrandLogo.tsx +65 -0
  113. package/src/primitives/BrandName.tsx +51 -0
  114. package/src/primitives/Button.tsx +123 -0
  115. package/src/primitives/ColorSwatch.tsx +221 -0
  116. package/src/primitives/DragHintAnimation.tsx +190 -0
  117. package/src/primitives/EdgeSwipeGuards.tsx +60 -0
  118. package/src/primitives/FloatingActionGroup.tsx +176 -0
  119. package/src/primitives/ProductPrice.tsx +171 -0
  120. package/src/primitives/ProgressiveBlur.tsx +295 -0
  121. package/src/primitives/ThemeToggle.tsx +125 -0
  122. package/src/primitives/__tests__/story-coverage.test.ts +98 -0
  123. package/src/primitives/accordion.tsx +280 -0
  124. package/src/primitives/badge.tsx +137 -0
  125. package/src/primitives/card.tsx +61 -0
  126. package/src/primitives/checkbox.tsx +56 -0
  127. package/src/primitives/collapsible.tsx +51 -0
  128. package/src/primitives/drawer.tsx +828 -0
  129. package/src/primitives/dropdown-menu.tsx +197 -0
  130. package/src/primitives/fieldset.tsx +73 -0
  131. package/src/primitives/index.ts +138 -0
  132. package/src/primitives/input.tsx +91 -0
  133. package/src/primitives/kbd.tsx +130 -0
  134. package/src/primitives/label.tsx +20 -0
  135. package/src/primitives/link.tsx +182 -0
  136. package/src/primitives/popover.tsx +80 -0
  137. package/src/primitives/radio-group.tsx +79 -0
  138. package/src/primitives/scroll-fade.tsx +159 -0
  139. package/src/primitives/select.tsx +170 -0
  140. package/src/primitives/separator.tsx +25 -0
  141. package/src/primitives/slider.tsx +221 -0
  142. package/src/primitives/spinner.tsx +72 -0
  143. package/src/primitives/stories/Accordion.stories.tsx +121 -0
  144. package/src/primitives/stories/Badge.stories.tsx +221 -0
  145. package/src/primitives/stories/Button.stories.tsx +185 -0
  146. package/src/primitives/stories/Card.stories.tsx +171 -0
  147. package/src/primitives/stories/Checkbox.stories.tsx +214 -0
  148. package/src/primitives/stories/Collapsible.stories.tsx +230 -0
  149. package/src/primitives/stories/Drawer.stories.tsx +378 -0
  150. package/src/primitives/stories/DropdownMenu.stories.tsx +182 -0
  151. package/src/primitives/stories/Fieldset.stories.tsx +212 -0
  152. package/src/primitives/stories/Input.stories.tsx +172 -0
  153. package/src/primitives/stories/Kbd.stories.tsx +183 -0
  154. package/src/primitives/stories/Label.stories.tsx +98 -0
  155. package/src/primitives/stories/Link.stories.tsx +260 -0
  156. package/src/primitives/stories/Popover.stories.tsx +178 -0
  157. package/src/primitives/stories/RadioGroup.stories.tsx +205 -0
  158. package/src/primitives/stories/Select.stories.tsx +222 -0
  159. package/src/primitives/stories/Separator.stories.tsx +134 -0
  160. package/src/primitives/stories/Slider.stories.tsx +203 -0
  161. package/src/primitives/stories/Spinner.stories.tsx +142 -0
  162. package/src/primitives/stories/Surface.stories.tsx +257 -0
  163. package/src/primitives/stories/Switch.stories.tsx +131 -0
  164. package/src/primitives/stories/Tabs.stories.tsx +275 -0
  165. package/src/primitives/stories/TextField.stories.tsx +139 -0
  166. package/src/primitives/stories/Textarea.stories.tsx +148 -0
  167. package/src/primitives/stories/Tooltip.stories.tsx +119 -0
  168. package/src/primitives/surface.tsx +86 -0
  169. package/src/primitives/switch.tsx +35 -0
  170. package/src/primitives/tabs.tsx +206 -0
  171. package/src/primitives/text-field.tsx +84 -0
  172. package/src/primitives/textarea.tsx +50 -0
  173. package/src/primitives/tooltip.tsx +58 -0
  174. package/src/services/CanvasExportService.ts +518 -0
  175. package/src/styles/base.css +380 -0
  176. package/src/styles/defaults.css +280 -0
  177. package/src/styles/globals.css +1242 -0
  178. package/src/styles/index.css +17 -0
  179. package/src/styles/ne-themes.css +4740 -0
  180. package/src/styles/tailwind.css +11 -0
  181. package/src/styles/tokens.css +117 -0
  182. package/src/styles/utilities.css +188 -0
  183. package/src/themes/apply-theme.ts +449 -0
  184. package/src/themes/getThemeStyles.ts +454 -0
  185. package/src/themes/index.ts +48 -0
  186. package/src/themes/oklch-theme.ts +283 -0
  187. package/src/themes/presets.ts +989 -0
  188. package/src/themes/types.ts +386 -0
  189. package/src/themes/useTheme.tsx +450 -0
  190. package/src/utils/dev-warnings.ts +161 -0
  191. package/src/utils/devWarnings.ts +153 -0
  192. package/dist/styles.css +0 -1
@@ -0,0 +1,193 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useMemo, useCallback } from "react";
4
+ import type { CatalogProduct } from "@snowcone-app/sdk";
5
+ import { getProduct, listProducts } from "@snowcone-app/sdk";
6
+ import { createDevFetcher } from "@snowcone-app/sdk/dev-fetcher";
7
+
8
+ export interface UseProductGalleryOptions {
9
+ endpoint?: string;
10
+ mode?: "mock" | "live";
11
+ limit?: number; // For listProducts when no productIds provided
12
+ }
13
+
14
+ export interface UseProductGalleryReturn {
15
+ products: CatalogProduct[];
16
+ loading: boolean;
17
+ error: string | null;
18
+ refetch: () => void;
19
+ }
20
+
21
+ /**
22
+ * useProductGallery - Fetch and manage product collections
23
+ *
24
+ * A React hook for fetching either specific products by ID or listing all
25
+ * available products. Handles loading states, errors, and provides refetch
26
+ * capability. Perfect for product catalogs, related products, and galleries.
27
+ *
28
+ * Features:
29
+ * - Fetch specific products by ID array
30
+ * - List all products (empty array)
31
+ * - Loading and error states
32
+ * - Refetch capability
33
+ * - Mock/live mode support
34
+ * - Automatic deduplication via memoization
35
+ * - Limit for product lists
36
+ *
37
+ * **Modes:**
38
+ * - Specific products: Pass array of IDs `['id1', 'id2']`
39
+ * - List all: Pass empty array `[]` or omit parameter
40
+ *
41
+ * **Use Cases:**
42
+ * - Product catalog pages
43
+ * - Related/recommended products
44
+ * - Search results
45
+ * - Recently viewed
46
+ * - User favorites
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * // Fetch specific products
51
+ * const { products, loading, error, refetch } = useProductGallery(
52
+ * ['BEEB77', 'AR2P3G'],
53
+ * { endpoint: 'http://localhost:3000', mode: 'mock' }
54
+ * );
55
+ *
56
+ * if (loading) return <Spinner />;
57
+ * if (error) return <Error message={error} onRetry={refetch} />;
58
+ *
59
+ * return (
60
+ * <div className="grid grid-cols-3 gap-4">
61
+ * {products.map(product => (
62
+ * <ProductCard key={product.id} product={product} />
63
+ * ))}
64
+ * </div>
65
+ * );
66
+ * ```
67
+ *
68
+ * @example
69
+ * ```tsx
70
+ * // List all products with limit
71
+ * const { products, loading } = useProductGallery(
72
+ * [], // Empty array = list all
73
+ * { mode: 'mock', limit: 12 }
74
+ * );
75
+ * ```
76
+ *
77
+ * @example
78
+ * ```tsx
79
+ * // Use in Remix loader
80
+ * export async function loader() {
81
+ * // Note: In loaders, use SDK functions directly
82
+ * // This hook is for client components
83
+ * const products = await listProducts({ ... });
84
+ * return json({ products });
85
+ * }
86
+ *
87
+ * // Then in component
88
+ * function ProductGallery() {
89
+ * const { products } = useLoaderData<typeof loader>();
90
+ * // Or use hook for client-side filtering
91
+ * const { products: filtered } = useProductGallery(
92
+ * products.map(p => p.id)
93
+ * );
94
+ * }
95
+ * ```
96
+ *
97
+ * @example
98
+ * ```tsx
99
+ * // With error handling and retry
100
+ * function ProductCatalog() {
101
+ * const { products, loading, error, refetch } = useProductGallery([], {
102
+ * mode: 'live',
103
+ * limit: 20
104
+ * });
105
+ *
106
+ * if (error) {
107
+ * return (
108
+ * <div>
109
+ * <p>Failed to load: {error}</p>
110
+ * <button onClick={refetch}>Retry</button>
111
+ * </div>
112
+ * );
113
+ * }
114
+ *
115
+ * return <ProductGrid products={products} loading={loading} />;
116
+ * }
117
+ * ```
118
+ *
119
+ * @param productIds - Array of product IDs to fetch (empty array = list all)
120
+ * @param options - Configuration options
121
+ * @param options.endpoint - API endpoint URL
122
+ * @param options.mode - Data source ("mock" or "live", default: "mock")
123
+ * @param options.limit - Max products to return when listing all (default: 10)
124
+ * @returns Object with products array, loading state, error, and refetch function
125
+ */
126
+ export function useProductGallery(
127
+ productIds: string[] = [], // Make optional - empty array means "list all products"
128
+ options: UseProductGalleryOptions = {}
129
+ ): UseProductGalleryReturn {
130
+ const { endpoint, mode = "mock", limit = 10 } = options;
131
+
132
+ const [products, setProducts] = useState<CatalogProduct[]>([]);
133
+ const [loading, setLoading] = useState(true);
134
+ const [error, setError] = useState<string | null>(null);
135
+
136
+ // Memoize the productIds array to prevent infinite loops from array reference changes
137
+ const memoizedProductIds = useMemo(
138
+ () => productIds,
139
+ [JSON.stringify(productIds)]
140
+ );
141
+
142
+ const fetchProducts = useCallback(async () => {
143
+ try {
144
+ setLoading(true);
145
+ setError(null);
146
+
147
+ const baseUrl =
148
+ endpoint ||
149
+ (window as any)?.snowcone?.endpoint ||
150
+ "http://192.168.4.32:5175";
151
+ const customFetcher =
152
+ mode === "mock" ? createDevFetcher(baseUrl) : undefined;
153
+
154
+ let fetchedProducts: CatalogProduct[];
155
+
156
+ if (memoizedProductIds.length === 0) {
157
+ // List all products when no specific IDs provided
158
+ const response = await listProducts({
159
+ baseUrl,
160
+ fetcher: customFetcher as any,
161
+ });
162
+ fetchedProducts = response.items.slice(0, limit); // Apply limit
163
+ } else {
164
+ // Fetch specific products by ID
165
+ const productPromises = memoizedProductIds.map((productId) =>
166
+ getProduct(productId, { baseUrl, fetcher: customFetcher as any })
167
+ );
168
+
169
+ const results = await Promise.all(productPromises);
170
+ fetchedProducts = results.filter(Boolean) as CatalogProduct[];
171
+ }
172
+
173
+ setProducts(fetchedProducts);
174
+ } catch (err: any) {
175
+ console.error("Failed to load products:", err);
176
+ setError(err.message || "Failed to load products");
177
+ } finally {
178
+ setLoading(false);
179
+ }
180
+ }, [memoizedProductIds, endpoint, mode, limit]);
181
+
182
+ useEffect(() => {
183
+ // Always fetch - either specific products or list all products
184
+ fetchProducts();
185
+ }, [fetchProducts]);
186
+
187
+ return {
188
+ products,
189
+ loading,
190
+ error,
191
+ refetch: fetchProducts,
192
+ };
193
+ }
@@ -0,0 +1,467 @@
1
+ "use client";
2
+
3
+ import { useCallback, useMemo } from "react";
4
+ import {
5
+ useProduct,
6
+ useProductOptional,
7
+ useDesignOptional,
8
+ type ReactProductContext,
9
+ type Artwork,
10
+ type PlacementDesign,
11
+ type ClipShape,
12
+ } from "../patterns/Product";
13
+ import {
14
+ useShop,
15
+ useShopOptional,
16
+ type ShopContextValue,
17
+ } from "../patterns/ShopProvider";
18
+ import {
19
+ useRealtimeOptional,
20
+ type RealtimeContextValue,
21
+ } from "../patterns/RealtimeProvider";
22
+ import {
23
+ useMockupPriorityOptional,
24
+ type MockupPriorityContextValue,
25
+ } from "../patterns/MockupPriorityProvider";
26
+ import type { CatalogProduct, OptionSelection, ImageAlignment } from "@snowcone-app/sdk";
27
+
28
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
29
+ // PROVIDER STATUS - For helpful error messages
30
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
31
+
32
+ export interface ProviderStatus {
33
+ hasShop: boolean;
34
+ hasProduct: boolean;
35
+ hasRealtime: boolean;
36
+ hasPriority: boolean;
37
+ hasArtwork: boolean;
38
+ missing: string[];
39
+ }
40
+
41
+ /**
42
+ * Check which providers are available in the current context.
43
+ * Useful for debugging and conditional rendering.
44
+ */
45
+ export function useProviderStatus(): ProviderStatus {
46
+ const shop = useShopOptional();
47
+ const product = useProductOptional();
48
+ const design = useDesignOptional();
49
+ const realtime = useRealtimeOptional();
50
+ const priority = useMockupPriorityOptional();
51
+
52
+ const missing: string[] = [];
53
+ if (!shop) missing.push("Shop");
54
+ if (!product) missing.push("Product");
55
+ if (!realtime) missing.push("RealtimeProvider");
56
+ if (!priority) missing.push("MockupPriorityProvider");
57
+ if (!design?.selectedArtwork) missing.push("ArtSelector or selectedArtwork");
58
+
59
+ return {
60
+ hasShop: !!shop,
61
+ hasProduct: !!product,
62
+ hasRealtime: !!realtime,
63
+ hasPriority: !!priority,
64
+ hasArtwork: !!design?.selectedArtwork,
65
+ missing,
66
+ };
67
+ }
68
+
69
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
70
+ // PRODUCT PAGE CONTEXT - Unified interface
71
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
72
+
73
+ export interface ProductPageContext {
74
+ // ── Product Data ──────────────────────────────────────────────────────────
75
+ /** The product being displayed */
76
+ product: CatalogProduct | undefined;
77
+ /** Whether product data is loading */
78
+ isLoading: boolean;
79
+ /** Current variant selection (size, color, etc.) */
80
+ selection: OptionSelection;
81
+ /** Update variant selection */
82
+ updateSelection: (selection: OptionSelection) => void;
83
+ /** Current price based on selection */
84
+ currentPrice: number | undefined;
85
+
86
+ // ── Artwork & Design ──────────────────────────────────────────────────────
87
+ /** Currently selected artwork */
88
+ selectedArtwork: Artwork | undefined;
89
+ /** Set the selected artwork */
90
+ setSelectedArtwork: (artwork: Artwork | undefined) => void;
91
+ /** All available artworks */
92
+ artworks: Artwork[];
93
+ /** Add an artwork to the collection */
94
+ addArtwork: (artwork: Artwork) => void;
95
+ /** Placement designs for current product */
96
+ placements: Record<string, PlacementDesign>;
97
+ /** Currently selected placement */
98
+ selectedPlacement: string | undefined;
99
+ /** Set selected placement */
100
+ setSelectedPlacement: (placement: string) => void;
101
+
102
+ // ── Design Methods ────────────────────────────────────────────────────────
103
+ /** Update a placement's design */
104
+ setPlacementDesign: (placement: string, design: Partial<PlacementDesign>) => void;
105
+ /** Get a placement's design */
106
+ getPlacementDesign: (placement: string) => PlacementDesign | undefined;
107
+ /** Apply artwork to a specific placement */
108
+ applyArtworkToPlacement: (placement: string, artwork?: Artwork) => void;
109
+ /** Set clip shape for a placement */
110
+ setPlacementClipShape: (placement: string, clipShape: ClipShape) => void;
111
+ /** Get clip shape for a placement */
112
+ getPlacementClipShape: (placement: string) => ClipShape;
113
+
114
+ // ── Realtime (Optional) ───────────────────────────────────────────────────
115
+ /** Realtime mockup context (if RealtimeProvider is present) */
116
+ realtime: RealtimeContextValue | undefined;
117
+ /** Whether realtime mockups are available */
118
+ hasRealtime: boolean;
119
+
120
+ // ── Priority (Optional) ───────────────────────────────────────────────────
121
+ /** Priority context (if MockupPriorityProvider is present) */
122
+ priority: MockupPriorityContextValue | undefined;
123
+ /** Whether priority system is available */
124
+ hasPriority: boolean;
125
+
126
+ // ── Shop ──────────────────────────────────────────────────────────────────
127
+ /** Queue an image update (throttled) */
128
+ queueImageUpdate: ShopContextValue["queueImageUpdate"];
129
+ /** Cancel a queued image update */
130
+ cancelImageUpdate: ShopContextValue["cancelImageUpdate"];
131
+ /** Get cached product data */
132
+ getCachedProduct: (productId: string) => CatalogProduct | undefined;
133
+
134
+ // ── Provider Status ───────────────────────────────────────────────────────
135
+ /** Check which providers are available */
136
+ providers: ProviderStatus;
137
+ }
138
+
139
+ /**
140
+ * useProductPage - Unified hook for building product pages
141
+ *
142
+ * Combines all necessary contexts (Product, Shop, Realtime, Priority) into
143
+ * a single, easy-to-use interface. This is the recommended way to access
144
+ * context in custom product page components.
145
+ *
146
+ * **Requirements:**
147
+ * - Must be used within a `<Shop>` provider
148
+ * - Must be used within a `<Product>` provider
149
+ * - RealtimeProvider and MockupPriorityProvider are optional
150
+ *
151
+ * @example
152
+ * ```tsx
153
+ * function MyProductPage() {
154
+ * const {
155
+ * product,
156
+ * selection,
157
+ * updateSelection,
158
+ * selectedArtwork,
159
+ * setSelectedArtwork,
160
+ * placements,
161
+ * realtime,
162
+ * } = useProductPage();
163
+ *
164
+ * if (!product) return <div>Loading...</div>;
165
+ *
166
+ * return (
167
+ * <div>
168
+ * <h1>{product.name}</h1>
169
+ * <button onClick={() => setSelectedArtwork(myArtwork)}>
170
+ * Set Artwork
171
+ * </button>
172
+ * </div>
173
+ * );
174
+ * }
175
+ * ```
176
+ *
177
+ * @throws Error if used outside of Shop or Product context
178
+ */
179
+ export function useProductPage(): ProductPageContext {
180
+ const shop = useShop(); // Required - will throw if missing
181
+ const productCtx = useProduct(); // Required - will throw if missing
182
+ const realtime = useRealtimeOptional();
183
+ const priority = useMockupPriorityOptional();
184
+
185
+ const providers = useProviderStatus();
186
+
187
+ return useMemo(
188
+ () => ({
189
+ // Product Data
190
+ product: productCtx.product,
191
+ isLoading: productCtx.loading ?? false,
192
+ selection: productCtx.selection ?? {},
193
+ updateSelection: productCtx.updateSelection ?? (() => {}),
194
+ currentPrice: productCtx.currentPrice,
195
+
196
+ // Artwork & Design
197
+ selectedArtwork: productCtx.selectedArtwork,
198
+ setSelectedArtwork: productCtx.setSelectedArtwork,
199
+ artworks: shop.artworks,
200
+ addArtwork: shop.addArtwork,
201
+ placements: productCtx.placements,
202
+ selectedPlacement: productCtx.selectedPlacement,
203
+ setSelectedPlacement: productCtx.setSelectedPlacement,
204
+
205
+ // Design Methods
206
+ setPlacementDesign: productCtx.setPlacementDesign,
207
+ getPlacementDesign: productCtx.getPlacementDesign,
208
+ applyArtworkToPlacement: productCtx.applyArtworkToPlacement,
209
+ setPlacementClipShape: productCtx.setPlacementClipShape,
210
+ getPlacementClipShape: productCtx.getPlacementClipShape,
211
+
212
+ // Realtime
213
+ realtime,
214
+ hasRealtime: !!realtime,
215
+
216
+ // Priority
217
+ priority,
218
+ hasPriority: !!priority,
219
+
220
+ // Shop
221
+ queueImageUpdate: shop.queueImageUpdate,
222
+ cancelImageUpdate: shop.cancelImageUpdate,
223
+ getCachedProduct: shop.getProduct,
224
+
225
+ // Provider Status
226
+ providers,
227
+ }),
228
+ [productCtx, shop, realtime, priority, providers]
229
+ );
230
+ }
231
+
232
+ /**
233
+ * useProductPageOptional - Like useProductPage but returns undefined if contexts missing
234
+ *
235
+ * Use this when you're not sure if you're inside the required providers.
236
+ * Returns undefined instead of throwing an error.
237
+ *
238
+ * @example
239
+ * ```tsx
240
+ * function MaybeProductComponent() {
241
+ * const page = useProductPageOptional();
242
+ *
243
+ * if (!page) {
244
+ * return <div>Not inside a product page</div>;
245
+ * }
246
+ *
247
+ * return <div>{page.product?.name}</div>;
248
+ * }
249
+ * ```
250
+ */
251
+ export function useProductPageOptional(): ProductPageContext | undefined {
252
+ const shop = useShopOptional();
253
+ const productCtx = useProductOptional();
254
+ const realtime = useRealtimeOptional();
255
+ const priority = useMockupPriorityOptional();
256
+
257
+ const providers = useProviderStatus();
258
+
259
+ if (!shop || !productCtx) {
260
+ return undefined;
261
+ }
262
+
263
+ return {
264
+ // Product Data
265
+ product: productCtx.product,
266
+ isLoading: productCtx.loading ?? false,
267
+ selection: productCtx.selection ?? {},
268
+ updateSelection: productCtx.updateSelection ?? (() => {}),
269
+ currentPrice: productCtx.currentPrice,
270
+
271
+ // Artwork & Design
272
+ selectedArtwork: productCtx.selectedArtwork,
273
+ setSelectedArtwork: productCtx.setSelectedArtwork,
274
+ artworks: shop.artworks,
275
+ addArtwork: shop.addArtwork,
276
+ placements: productCtx.placements,
277
+ selectedPlacement: productCtx.selectedPlacement,
278
+ setSelectedPlacement: productCtx.setSelectedPlacement,
279
+
280
+ // Design Methods
281
+ setPlacementDesign: productCtx.setPlacementDesign,
282
+ getPlacementDesign: productCtx.getPlacementDesign,
283
+ applyArtworkToPlacement: productCtx.applyArtworkToPlacement,
284
+ setPlacementClipShape: productCtx.setPlacementClipShape,
285
+ getPlacementClipShape: productCtx.getPlacementClipShape,
286
+
287
+ // Realtime
288
+ realtime,
289
+ hasRealtime: !!realtime,
290
+
291
+ // Priority
292
+ priority,
293
+ hasPriority: !!priority,
294
+
295
+ // Shop
296
+ queueImageUpdate: shop.queueImageUpdate,
297
+ cancelImageUpdate: shop.cancelImageUpdate,
298
+ getCachedProduct: shop.getProduct,
299
+
300
+ // Provider Status
301
+ providers,
302
+ };
303
+ }
304
+
305
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
306
+ // PROJECTION HOOKS - Simplified access to nested state
307
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
308
+
309
+ /**
310
+ * useCurrentPlacements - Get placements for the current product
311
+ *
312
+ * Simplified access to placement data without dealing with nested state.
313
+ *
314
+ * @example
315
+ * ```tsx
316
+ * const placements = useCurrentPlacements();
317
+ * // { front: { imageUrl: '...', alignment: {...} }, back: {...} }
318
+ * ```
319
+ */
320
+ export function useCurrentPlacements(): Record<string, PlacementDesign> {
321
+ const productCtx = useProductOptional();
322
+ return productCtx?.placements ?? {};
323
+ }
324
+
325
+ /**
326
+ * useArtworkAlignment - Get alignment for a specific artwork on a placement
327
+ *
328
+ * @param placement - The placement name (e.g., "front", "back")
329
+ * @returns The alignment or undefined
330
+ *
331
+ * @example
332
+ * ```tsx
333
+ * const alignment = useArtworkAlignment("front");
334
+ * // { x: 0.5, y: 0.5, scale: 1, rotation: 0 }
335
+ * ```
336
+ */
337
+ export function useArtworkAlignment(
338
+ placement: string
339
+ ): ImageAlignment | undefined {
340
+ const productCtx = useProductOptional();
341
+ return productCtx?.placements[placement]?.alignment;
342
+ }
343
+
344
+ /**
345
+ * useSetArtworkAlignment - Set alignment for a placement
346
+ *
347
+ * @returns A function to update alignment
348
+ *
349
+ * @example
350
+ * ```tsx
351
+ * const setAlignment = useSetArtworkAlignment();
352
+ * setAlignment("front", "center");
353
+ * setAlignment("back", "top");
354
+ * ```
355
+ */
356
+ export function useSetArtworkAlignment(): (
357
+ placement: string,
358
+ alignment: ImageAlignment
359
+ ) => void {
360
+ const productCtx = useProductOptional();
361
+
362
+ return useCallback(
363
+ (placement: string, alignment: ImageAlignment) => {
364
+ if (productCtx) {
365
+ const currentDesign = productCtx.placements[placement];
366
+ productCtx.setPlacementDesign(placement, {
367
+ ...currentDesign,
368
+ alignment,
369
+ });
370
+ }
371
+ },
372
+ [productCtx]
373
+ );
374
+ }
375
+
376
+ /**
377
+ * useSelectedArtwork - Get and set the selected artwork
378
+ *
379
+ * A simplified hook for artwork selection.
380
+ *
381
+ * @example
382
+ * ```tsx
383
+ * const [artwork, setArtwork] = useSelectedArtwork();
384
+ *
385
+ * return (
386
+ * <button onClick={() => setArtwork({ type: 'regular', src: url })}>
387
+ * Select Artwork
388
+ * </button>
389
+ * );
390
+ * ```
391
+ */
392
+ export function useSelectedArtwork(): [
393
+ Artwork | undefined,
394
+ (artwork: Artwork | undefined) => void
395
+ ] {
396
+ const productCtx = useProductOptional();
397
+ const shopCtx = useShopOptional();
398
+
399
+ // Prefer product context, fall back to shop context
400
+ const ctx = productCtx || shopCtx;
401
+
402
+ const setArtwork = useCallback(
403
+ (artwork: Artwork | undefined) => {
404
+ ctx?.setSelectedArtwork(artwork);
405
+ },
406
+ [ctx]
407
+ );
408
+
409
+ return [ctx?.selectedArtwork, setArtwork];
410
+ }
411
+
412
+ /**
413
+ * useProductSelection - Get and set variant selection
414
+ *
415
+ * @example
416
+ * ```tsx
417
+ * const [selection, updateSelection] = useProductSelection();
418
+ *
419
+ * return (
420
+ * <button onClick={() => updateSelection({ ...selection, size: 'L' })}>
421
+ * Large
422
+ * </button>
423
+ * );
424
+ * ```
425
+ */
426
+ export function useProductSelection(): [
427
+ OptionSelection,
428
+ (selection: OptionSelection) => void
429
+ ] {
430
+ const productCtx = useProductOptional();
431
+
432
+ const update = useCallback(
433
+ (selection: OptionSelection) => {
434
+ productCtx?.updateSelection?.(selection);
435
+ },
436
+ [productCtx]
437
+ );
438
+
439
+ return [productCtx?.selection ?? {}, update];
440
+ }
441
+
442
+ /**
443
+ * usePlacementClipShape - Get and set clip shape for a placement
444
+ *
445
+ * @example
446
+ * ```tsx
447
+ * const [clipShape, setClipShape] = usePlacementClipShape("front");
448
+ * // clipShape: 'rectangle' | 'circle' | 'custom'
449
+ * ```
450
+ */
451
+ export function usePlacementClipShape(
452
+ placement: string
453
+ ): [ClipShape, (shape: ClipShape) => void] {
454
+ const productCtx = useProductOptional();
455
+
456
+ const setShape = useCallback(
457
+ (shape: ClipShape) => {
458
+ productCtx?.setPlacementClipShape(placement, shape);
459
+ },
460
+ [productCtx, placement]
461
+ );
462
+
463
+ return [
464
+ productCtx?.getPlacementClipShape(placement) ?? "rectangle",
465
+ setShape,
466
+ ];
467
+ }