@snowcone-app/ui 0.1.43 → 0.2.1
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 +32 -0
- package/README.md +18 -4
- package/dist/index.cjs +5 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/package.json +9 -5
- package/src/components/CanvasIsolationBoundary.tsx +202 -0
- package/src/components/LoadingOverlayPrism.tsx +251 -0
- package/src/composed/AddToCart.tsx +229 -0
- package/src/composed/ArtAlignment.tsx +703 -0
- package/src/composed/ArtSelector.tsx +290 -0
- package/src/composed/ArtworkCustomizer.tsx +212 -0
- package/src/composed/CanvasEditor.tsx +79 -0
- package/src/composed/ColorPicker.tsx +111 -0
- package/src/composed/CurrentSelectionDisplay.tsx +86 -0
- package/src/composed/HeroProductImage.tsx +1079 -0
- package/src/composed/Lightbox.index.ts +2 -0
- package/src/composed/Lightbox.tsx +230 -0
- package/src/composed/PlacementClipShapeSelector.tsx +88 -0
- package/src/composed/PlacementTabs.tsx +179 -0
- package/src/composed/ProductCard.tsx +298 -0
- package/src/composed/ProductGallery.tsx +54 -0
- package/src/composed/ProductImage.tsx +129 -0
- package/src/composed/ProductList.tsx +147 -0
- package/src/composed/ProductOptions.tsx +305 -0
- package/src/composed/RealtimeMockup.tsx +121 -0
- package/src/composed/TileCount.tsx +348 -0
- package/src/composed/carousels/HeroCarousel.tsx +240 -0
- package/src/composed/carousels/MobileProductCarousel.tsx +1002 -0
- package/src/composed/carousels/index.ts +11 -0
- package/src/composed/carousels/types.ts +58 -0
- package/src/composed/grids/MasonryGrid.tsx +238 -0
- package/src/composed/grids/index.ts +9 -0
- package/src/composed/search/CurrentRefinements.tsx +80 -0
- package/src/composed/search/Filters.tsx +49 -0
- package/src/composed/search/FiltersButton.tsx +57 -0
- package/src/composed/search/FiltersDrawer.tsx +375 -0
- package/src/composed/search/ProductGrid.tsx +118 -0
- package/src/composed/search/ProductHit.tsx +56 -0
- package/src/composed/search/SearchBox.tsx +109 -0
- package/src/composed/search/SearchProvider.tsx +136 -0
- package/src/composed/search/facetConfig.ts +16 -0
- package/src/composed/search/index.ts +22 -0
- package/src/composed/search/meilisearchAdapter.ts +20 -0
- package/src/composed/search/types.ts +22 -0
- package/src/composed/zoom/EnhancedImageViewer.tsx +505 -0
- package/src/composed/zoom/ResponsiveZoom.tsx +134 -0
- package/src/composed/zoom/ZoomOverlay.tsx +194 -0
- package/src/composed/zoom/index.ts +12 -0
- package/src/composed/zoom/types.ts +12 -0
- package/src/design-system/ColorPalette.tsx +126 -0
- package/src/design-system/ColorSwatch.tsx +49 -0
- package/src/design-system/DesignSystemPage.tsx +130 -0
- package/src/design-system/ThemeSwitcher.tsx +181 -0
- package/src/design-system/TypographyScale.tsx +106 -0
- package/src/design-system/index.ts +5 -0
- package/src/ecommerce/stories/HeroProductImage.stories.tsx +66 -0
- package/src/ecommerce/stories/PDPHeroGallery.stories.tsx +105 -0
- package/src/ecommerce/stories/PDPInfoPanel.stories.tsx +472 -0
- package/src/ecommerce/stories/PDPLayout.stories.tsx +365 -0
- package/src/hooks/useBrand.ts +41 -0
- package/src/hooks/useCanvasContext.ts +127 -0
- package/src/hooks/useDeviceDetection.ts +64 -0
- package/src/hooks/useFocusTrap.ts +70 -0
- package/src/hooks/useImagePreloader.ts +268 -0
- package/src/hooks/useImageTransition.ts +608 -0
- package/src/hooks/usePlacementsProcessor.ts +74 -0
- package/src/hooks/useProductGallery.ts +193 -0
- package/src/hooks/useProductPage.ts +467 -0
- package/src/hooks/useRenderGuard.ts +96 -0
- package/src/hooks/useScrollDirection.ts +196 -0
- package/src/hooks/viewport/index.ts +25 -0
- package/src/hooks/viewport/useContainerWidth.ts +59 -0
- package/src/hooks/viewport/useMediaQuery.ts +52 -0
- package/src/hooks/viewport/useResponsiveImageCap.ts +149 -0
- package/src/hooks/viewport/useViewportDimensions.ts +135 -0
- package/src/hooks/viewport/useWideMonitorMode.ts +150 -0
- package/src/hooks/visibility/index.ts +15 -0
- package/src/hooks/visibility/observerPool.ts +150 -0
- package/src/index.ts +240 -0
- package/src/layouts/hero-zoom/HeroShrinkLayout.tsx +209 -0
- package/src/layouts/hero-zoom/HeroZoomLayout.tsx +351 -0
- package/src/layouts/hero-zoom/index.ts +30 -0
- package/src/layouts/hero-zoom/stories/HeroZoomLayout.stories.tsx +350 -0
- package/src/layouts/hero-zoom/types.ts +113 -0
- package/src/layouts/hero-zoom/useHeroZoomScales.ts +156 -0
- package/src/layouts/index.ts +9 -0
- package/src/layouts/pdp/EdgeBlurBox.tsx +210 -0
- package/src/layouts/pdp/ImageBlurExtension.tsx +215 -0
- package/src/layouts/pdp/ImageEdgeBlur.tsx +215 -0
- package/src/layouts/pdp/PDPLayout.tsx +246 -0
- package/src/layouts/pdp/SimpleImageBlur.tsx +140 -0
- package/src/layouts/pdp/index.ts +40 -0
- package/src/lib/env.ts +15 -0
- package/src/lib/locale.ts +167 -0
- package/src/lib/router.tsx +46 -0
- package/src/lib/utils.ts +6 -0
- package/src/lightbox/README.md +77 -0
- package/src/next/index.tsx +26 -0
- package/src/patterns/MockupPriorityProvider.tsx +1014 -0
- package/src/patterns/Product.tsx +850 -0
- package/src/patterns/ProductPageProvider.tsx +224 -0
- package/src/patterns/RealtimeProvider.tsx +1162 -0
- package/src/patterns/ShopProvider.tsx +603 -0
- package/src/personalization/PersonalizationBridge.tsx +235 -0
- package/src/personalization/PersonalizationContext.ts +29 -0
- package/src/personalization/PersonalizationInputs.tsx +110 -0
- package/src/personalization/PersonalizationProvider.tsx +407 -0
- package/src/personalization/canvas-stub.d.ts +22 -0
- package/src/personalization/index.ts +43 -0
- package/src/personalization/types.ts +48 -0
- package/src/personalization/usePersonalization.ts +32 -0
- package/src/personalization/usePersonalizationShimmer.ts +159 -0
- package/src/personalization/utils.ts +59 -0
- package/src/primitives/BrandLogo.tsx +65 -0
- package/src/primitives/BrandName.tsx +51 -0
- package/src/primitives/Button.tsx +123 -0
- package/src/primitives/ColorSwatch.tsx +221 -0
- package/src/primitives/DragHintAnimation.tsx +190 -0
- package/src/primitives/EdgeSwipeGuards.tsx +60 -0
- package/src/primitives/FloatingActionGroup.tsx +176 -0
- package/src/primitives/ProductPrice.tsx +171 -0
- package/src/primitives/ProgressiveBlur.tsx +295 -0
- package/src/primitives/ThemeToggle.tsx +125 -0
- package/src/primitives/__tests__/story-coverage.test.ts +98 -0
- package/src/primitives/accordion.tsx +280 -0
- package/src/primitives/badge.tsx +137 -0
- package/src/primitives/card.tsx +61 -0
- package/src/primitives/checkbox.tsx +56 -0
- package/src/primitives/collapsible.tsx +51 -0
- package/src/primitives/drawer.tsx +828 -0
- package/src/primitives/dropdown-menu.tsx +197 -0
- package/src/primitives/fieldset.tsx +73 -0
- package/src/primitives/index.ts +138 -0
- package/src/primitives/input.tsx +91 -0
- package/src/primitives/kbd.tsx +130 -0
- package/src/primitives/label.tsx +20 -0
- package/src/primitives/link.tsx +182 -0
- package/src/primitives/popover.tsx +80 -0
- package/src/primitives/radio-group.tsx +79 -0
- package/src/primitives/scroll-fade.tsx +159 -0
- package/src/primitives/select.tsx +170 -0
- package/src/primitives/separator.tsx +25 -0
- package/src/primitives/slider.tsx +221 -0
- package/src/primitives/spinner.tsx +72 -0
- package/src/primitives/stories/Accordion.stories.tsx +121 -0
- package/src/primitives/stories/Badge.stories.tsx +221 -0
- package/src/primitives/stories/Button.stories.tsx +185 -0
- package/src/primitives/stories/Card.stories.tsx +171 -0
- package/src/primitives/stories/Checkbox.stories.tsx +214 -0
- package/src/primitives/stories/Collapsible.stories.tsx +230 -0
- package/src/primitives/stories/Drawer.stories.tsx +378 -0
- package/src/primitives/stories/DropdownMenu.stories.tsx +182 -0
- package/src/primitives/stories/Fieldset.stories.tsx +212 -0
- package/src/primitives/stories/Input.stories.tsx +172 -0
- package/src/primitives/stories/Kbd.stories.tsx +183 -0
- package/src/primitives/stories/Label.stories.tsx +98 -0
- package/src/primitives/stories/Link.stories.tsx +260 -0
- package/src/primitives/stories/Popover.stories.tsx +178 -0
- package/src/primitives/stories/RadioGroup.stories.tsx +205 -0
- package/src/primitives/stories/Select.stories.tsx +222 -0
- package/src/primitives/stories/Separator.stories.tsx +134 -0
- package/src/primitives/stories/Slider.stories.tsx +203 -0
- package/src/primitives/stories/Spinner.stories.tsx +142 -0
- package/src/primitives/stories/Surface.stories.tsx +257 -0
- package/src/primitives/stories/Switch.stories.tsx +131 -0
- package/src/primitives/stories/Tabs.stories.tsx +275 -0
- package/src/primitives/stories/TextField.stories.tsx +139 -0
- package/src/primitives/stories/Textarea.stories.tsx +148 -0
- package/src/primitives/stories/Tooltip.stories.tsx +119 -0
- package/src/primitives/surface.tsx +86 -0
- package/src/primitives/switch.tsx +35 -0
- package/src/primitives/tabs.tsx +206 -0
- package/src/primitives/text-field.tsx +84 -0
- package/src/primitives/textarea.tsx +50 -0
- package/src/primitives/tooltip.tsx +58 -0
- package/src/services/CanvasExportService.ts +518 -0
- package/src/styles/base.css +380 -0
- package/src/styles/defaults.css +280 -0
- package/src/styles/globals.css +1242 -0
- package/src/styles/index.css +17 -0
- package/src/styles/ne-themes.css +4740 -0
- package/src/styles/tailwind.css +11 -0
- package/src/styles/tokens.css +117 -0
- package/src/styles/utilities.css +188 -0
- package/src/themes/apply-theme.ts +449 -0
- package/src/themes/getThemeStyles.ts +454 -0
- package/src/themes/index.ts +48 -0
- package/src/themes/oklch-theme.ts +283 -0
- package/src/themes/presets.ts +989 -0
- package/src/themes/types.ts +386 -0
- package/src/themes/useTheme.tsx +450 -0
- package/src/utils/dev-warnings.ts +161 -0
- package/src/utils/devWarnings.ts +153 -0
- package/dist/styles.css +0 -1
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, {
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
type CSSProperties,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { useDrag } from "@use-gesture/react";
|
|
11
|
+
import {
|
|
12
|
+
describeProductArtAlignment,
|
|
13
|
+
getSnapPoints,
|
|
14
|
+
type ProductArtAlignmentOptions,
|
|
15
|
+
type ProductArtAlignmentContext,
|
|
16
|
+
type ImageAlignment,
|
|
17
|
+
} from "@snowcone-app/sdk";
|
|
18
|
+
import { useProduct, useDesignOptional } from "../patterns/Product";
|
|
19
|
+
|
|
20
|
+
export interface ArtAlignmentProps extends ProductArtAlignmentOptions {
|
|
21
|
+
context?: ProductArtAlignmentContext;
|
|
22
|
+
placement?: string;
|
|
23
|
+
height?: string | number;
|
|
24
|
+
maxHeight?: string | number;
|
|
25
|
+
minHeight?: string | number;
|
|
26
|
+
artwork?: {
|
|
27
|
+
src: string;
|
|
28
|
+
// aspectRatio intentionally omitted - auto-detected from image
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* ArtAlignment - Interactive artwork positioning component
|
|
34
|
+
*
|
|
35
|
+
* Displays an artwork image with a draggable frame overlay for selecting the alignment
|
|
36
|
+
* position within a product placement. The frame snaps to predefined positions based on
|
|
37
|
+
* the available movement space.
|
|
38
|
+
*
|
|
39
|
+
* **🚨 DO NOT USE THIS COMPONENT DIRECTLY!**
|
|
40
|
+
* **Use `<ArtworkCustomizer />` instead - it automatically handles both regular artwork AND patterns.**
|
|
41
|
+
*
|
|
42
|
+
* **Why use ArtworkCustomizer:**
|
|
43
|
+
* - ✅ Single component for both regular artwork and patterns
|
|
44
|
+
* - ✅ TypeScript enforces correct props based on artwork type
|
|
45
|
+
* - ✅ Impossible to make mistakes (no manual conditionals needed)
|
|
46
|
+
* - ✅ Cleaner, simpler code
|
|
47
|
+
*
|
|
48
|
+
* **Only use ArtAlignment directly if:**
|
|
49
|
+
* - You are 100% certain you only have regular artwork (never patterns)
|
|
50
|
+
* - You are building a custom component that wraps ArtAlignment
|
|
51
|
+
* - You have read the ArtworkCustomizer docs and understand why you need direct access
|
|
52
|
+
*
|
|
53
|
+
* **⚠️ CRITICAL: If you use ArtAlignment directly:**
|
|
54
|
+
* - ONLY for regular artwork (photos, illustrations) - NEVER for seamless patterns
|
|
55
|
+
* - For seamless patterns, use `TileCount` component instead
|
|
56
|
+
* - NEVER show both ArtAlignment and TileCount at the same time
|
|
57
|
+
* - Check `artwork.type === 'seamless'` to decide which component to render
|
|
58
|
+
* - Pattern: `{artwork.type !== 'seamless' ? <ArtAlignment /> : <TileCount />}`
|
|
59
|
+
*
|
|
60
|
+
* **👉 See ArtworkCustomizer for the recommended approach!**
|
|
61
|
+
*
|
|
62
|
+
* **🎯 Automatic Aspect Ratio Detection:**
|
|
63
|
+
* - **DO NOT** pass `aspectRatio` prop - it is intentionally not supported
|
|
64
|
+
* - ArtAlignment automatically detects aspect ratio from the loaded image
|
|
65
|
+
* - This prevents layout issues from incorrect manual aspect ratios
|
|
66
|
+
* - The component will wait for the image to load before calculating dimensions
|
|
67
|
+
*
|
|
68
|
+
* **Smart Snap Points:**
|
|
69
|
+
* - 1 position (center only): When artwork and placement aspect ratios are nearly identical (< 5% movement)
|
|
70
|
+
* - 3 positions (far-left/top, center, far-right/bottom): Limited movement space (5-15%)
|
|
71
|
+
* - 5 positions (adds left/top, right/bottom): Significant movement space (≥ 15%)
|
|
72
|
+
*
|
|
73
|
+
* **Context Integration:**
|
|
74
|
+
* - Works within `Product` context to get placement dimensions
|
|
75
|
+
* - Integrates with `Design` context for artwork and alignment state
|
|
76
|
+
* - Automatically calculates mask aspect ratio from product placements
|
|
77
|
+
*
|
|
78
|
+
* **Features:**
|
|
79
|
+
* - Theme-aware borders using CSS variables
|
|
80
|
+
* - Blur effect outside the selected frame area
|
|
81
|
+
* - Touch and mouse drag support
|
|
82
|
+
* - Automatic aspect ratio detection from image
|
|
83
|
+
* - Smooth snapping to alignment positions
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```tsx
|
|
87
|
+
* // Basic usage with artwork prop (aspect ratio auto-detected)
|
|
88
|
+
* <Product productId="shirt-123">
|
|
89
|
+
* <ArtAlignment
|
|
90
|
+
* artwork={{ src: 'https://example.com/art.jpg' }}
|
|
91
|
+
* placement="Front"
|
|
92
|
+
* maxHeight={200}
|
|
93
|
+
* />
|
|
94
|
+
* </Product>
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```tsx
|
|
99
|
+
* // Using context-based artwork (from Design context)
|
|
100
|
+
* <Product productId="shirt-123">
|
|
101
|
+
* <ArtAlignment
|
|
102
|
+
* placement="Front"
|
|
103
|
+
* maxHeight={200}
|
|
104
|
+
* />
|
|
105
|
+
* </Product>
|
|
106
|
+
* ```
|
|
107
|
+
*
|
|
108
|
+
* @param artwork - Artwork object with src only (aspect ratio auto-detected from image)
|
|
109
|
+
* @param src - Image source URL (alternative to artwork prop, deprecated - use artwork.src instead)
|
|
110
|
+
* @param placement - Product placement label (e.g., "Front", "Back"). Updates Design context automatically when alignment changes
|
|
111
|
+
* @param maskAspectRatio - Manual mask aspect ratio (usually auto-detected from placement)
|
|
112
|
+
* @param alignment - Initial alignment position
|
|
113
|
+
* @param maxHeight - Maximum height constraint in pixels
|
|
114
|
+
* @param height - Fixed height in pixels
|
|
115
|
+
* @param minHeight - Minimum height constraint in pixels
|
|
116
|
+
* @param className - Additional CSS classes
|
|
117
|
+
*/
|
|
118
|
+
export function ArtAlignment({
|
|
119
|
+
src: propSrc,
|
|
120
|
+
artworkAspectRatio: propAspectRatio,
|
|
121
|
+
maskAspectRatio,
|
|
122
|
+
placement,
|
|
123
|
+
alignment: propAlignment,
|
|
124
|
+
className,
|
|
125
|
+
context,
|
|
126
|
+
height,
|
|
127
|
+
maxHeight,
|
|
128
|
+
minHeight,
|
|
129
|
+
artwork,
|
|
130
|
+
}: ArtAlignmentProps) {
|
|
131
|
+
// Get design context
|
|
132
|
+
const designContext = useDesignOptional();
|
|
133
|
+
|
|
134
|
+
// Get design for this placement if available
|
|
135
|
+
const placementDesign =
|
|
136
|
+
placement && designContext
|
|
137
|
+
? designContext.getPlacementDesign(placement)
|
|
138
|
+
: undefined;
|
|
139
|
+
|
|
140
|
+
// Use artwork from props first, then context/placement, then individual props
|
|
141
|
+
const src =
|
|
142
|
+
artwork?.src ||
|
|
143
|
+
placementDesign?.imageUrl ||
|
|
144
|
+
designContext?.selectedArtwork?.src ||
|
|
145
|
+
propSrc;
|
|
146
|
+
|
|
147
|
+
// IMPORTANT: aspectRatio is intentionally NOT used from props
|
|
148
|
+
// ArtAlignment auto-detects aspect ratio from the loaded image
|
|
149
|
+
// This prevents incorrect manual aspect ratios from breaking layout
|
|
150
|
+
const artworkAspectRatio = undefined; // Always auto-detect from image
|
|
151
|
+
|
|
152
|
+
// Try to get product from context - call hook unconditionally
|
|
153
|
+
let productCtx: any;
|
|
154
|
+
try {
|
|
155
|
+
productCtx = useProduct();
|
|
156
|
+
} catch {
|
|
157
|
+
// Not in a Product context, that's OK
|
|
158
|
+
productCtx = null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Build product context after hook calls
|
|
162
|
+
let productContext: ProductArtAlignmentContext | undefined = context;
|
|
163
|
+
if (!productContext && productCtx) {
|
|
164
|
+
// Get placements from product if available (they're added dynamically)
|
|
165
|
+
const product = productCtx.product as any;
|
|
166
|
+
productContext = {
|
|
167
|
+
product: {
|
|
168
|
+
placements: product?.placements,
|
|
169
|
+
},
|
|
170
|
+
selection: {},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
175
|
+
const imageRef = useRef<HTMLImageElement>(null);
|
|
176
|
+
|
|
177
|
+
// Track when position was last set by user drag to prevent sync effect from causing "boomerang"
|
|
178
|
+
// The boomerang happens because: drag sets position -> context updates async -> sync effect fires -> resets position
|
|
179
|
+
// We ignore context-driven updates for 500ms after a user drag
|
|
180
|
+
const lastUserDragTimeRef = useRef(0);
|
|
181
|
+
|
|
182
|
+
// All hooks must be called before any early returns
|
|
183
|
+
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
|
184
|
+
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
185
|
+
const [isReady, setIsReady] = useState(false);
|
|
186
|
+
const [detectedAspectRatio, setDetectedAspectRatio] = useState<number | undefined>();
|
|
187
|
+
|
|
188
|
+
// Use detected aspect ratio if context doesn't provide one
|
|
189
|
+
const effectiveAspectRatio = artworkAspectRatio || detectedAspectRatio;
|
|
190
|
+
|
|
191
|
+
// Derive alignment from context or props - don't use local state for this
|
|
192
|
+
const alignment = placementDesign?.alignment || propAlignment || "center";
|
|
193
|
+
|
|
194
|
+
const descriptor = useMemo(
|
|
195
|
+
() =>
|
|
196
|
+
describeProductArtAlignment(
|
|
197
|
+
{
|
|
198
|
+
src,
|
|
199
|
+
artworkAspectRatio: effectiveAspectRatio,
|
|
200
|
+
maskAspectRatio,
|
|
201
|
+
placement,
|
|
202
|
+
alignment: propAlignment,
|
|
203
|
+
className,
|
|
204
|
+
},
|
|
205
|
+
productContext
|
|
206
|
+
),
|
|
207
|
+
[
|
|
208
|
+
src,
|
|
209
|
+
effectiveAspectRatio,
|
|
210
|
+
maskAspectRatio,
|
|
211
|
+
placement,
|
|
212
|
+
propAlignment,
|
|
213
|
+
className,
|
|
214
|
+
productContext,
|
|
215
|
+
]
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Calculate container style with height constraints
|
|
219
|
+
const containerStyle = useMemo(() => {
|
|
220
|
+
const style: CSSProperties = {};
|
|
221
|
+
|
|
222
|
+
// When height is constrained, we need to calculate the width based on aspect ratio
|
|
223
|
+
if (height !== undefined) {
|
|
224
|
+
const heightValue =
|
|
225
|
+
typeof height === "number" ? height : parseFloat(height);
|
|
226
|
+
if (!isNaN(heightValue) && effectiveAspectRatio) {
|
|
227
|
+
style.height = `${heightValue}px`;
|
|
228
|
+
style.width = `${heightValue * effectiveAspectRatio}px`;
|
|
229
|
+
}
|
|
230
|
+
} else if (maxHeight !== undefined) {
|
|
231
|
+
const maxHeightValue =
|
|
232
|
+
typeof maxHeight === "number" ? maxHeight : parseFloat(maxHeight);
|
|
233
|
+
if (!isNaN(maxHeightValue) && effectiveAspectRatio) {
|
|
234
|
+
style.maxHeight = `${maxHeightValue}px`;
|
|
235
|
+
style.maxWidth = `${maxHeightValue * effectiveAspectRatio}px`;
|
|
236
|
+
style.width = "100%";
|
|
237
|
+
style.aspectRatio = String(effectiveAspectRatio);
|
|
238
|
+
}
|
|
239
|
+
} else if (effectiveAspectRatio) {
|
|
240
|
+
// Default behavior - use aspect ratio
|
|
241
|
+
style.aspectRatio = String(effectiveAspectRatio);
|
|
242
|
+
style.width = "100%";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (minHeight !== undefined) {
|
|
246
|
+
const minHeightValue =
|
|
247
|
+
typeof minHeight === "number" ? minHeight : parseFloat(minHeight);
|
|
248
|
+
if (!isNaN(minHeightValue)) {
|
|
249
|
+
style.minHeight = `${minHeightValue}px`;
|
|
250
|
+
if (effectiveAspectRatio) {
|
|
251
|
+
style.minWidth = `${minHeightValue * effectiveAspectRatio}px`;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return style;
|
|
257
|
+
}, [effectiveAspectRatio, height, maxHeight, minHeight]);
|
|
258
|
+
|
|
259
|
+
// Initialize container size measurement using ResizeObserver (avoids forced reflow)
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
if (!containerRef.current) return;
|
|
262
|
+
|
|
263
|
+
// Use ResizeObserver for all size measurements - it runs asynchronously
|
|
264
|
+
// and doesn't cause forced synchronous layout like getBoundingClientRect()
|
|
265
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
266
|
+
const entry = entries[0];
|
|
267
|
+
if (entry) {
|
|
268
|
+
// Use contentRect which is already computed - no forced reflow
|
|
269
|
+
const { width, height } = entry.contentRect;
|
|
270
|
+
setContainerSize((prev) => {
|
|
271
|
+
// Only update if size actually changed to avoid unnecessary re-renders
|
|
272
|
+
if (prev.width === width && prev.height === height) return prev;
|
|
273
|
+
return { width, height };
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
resizeObserver.observe(containerRef.current);
|
|
279
|
+
|
|
280
|
+
return () => {
|
|
281
|
+
resizeObserver.disconnect();
|
|
282
|
+
};
|
|
283
|
+
}, []);
|
|
284
|
+
|
|
285
|
+
// Handle image load
|
|
286
|
+
const handleImageLoad = () => {
|
|
287
|
+
setIsReady(true);
|
|
288
|
+
|
|
289
|
+
// Detect aspect ratio from loaded image
|
|
290
|
+
if (imageRef.current && !artworkAspectRatio) {
|
|
291
|
+
const { naturalWidth, naturalHeight } = imageRef.current;
|
|
292
|
+
if (naturalWidth && naturalHeight) {
|
|
293
|
+
setDetectedAspectRatio(naturalWidth / naturalHeight);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Note: Container size is handled by ResizeObserver - no need for getBoundingClientRect()
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Check if the image is already loaded when component mounts
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
if (!imageRef.current || !descriptor) return;
|
|
302
|
+
|
|
303
|
+
// If the image already has a complete property set to true,
|
|
304
|
+
// it means the image is already loaded (from cache)
|
|
305
|
+
if (imageRef.current.complete) {
|
|
306
|
+
setIsReady(true);
|
|
307
|
+
|
|
308
|
+
// Detect aspect ratio for cached images
|
|
309
|
+
if (!artworkAspectRatio) {
|
|
310
|
+
const { naturalWidth, naturalHeight } = imageRef.current;
|
|
311
|
+
if (naturalWidth && naturalHeight) {
|
|
312
|
+
setDetectedAspectRatio(naturalWidth / naturalHeight);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// Note: Container size is handled by ResizeObserver - no need for getBoundingClientRect()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Also try to load the image programmatically to handle both scenarios
|
|
319
|
+
const img = new Image();
|
|
320
|
+
img.crossOrigin = 'anonymous';
|
|
321
|
+
img.src = descriptor.src;
|
|
322
|
+
|
|
323
|
+
const handleLoad = () => {
|
|
324
|
+
setIsReady(true);
|
|
325
|
+
|
|
326
|
+
// Detect aspect ratio when programmatically loaded
|
|
327
|
+
if (!artworkAspectRatio && img.naturalWidth && img.naturalHeight) {
|
|
328
|
+
setDetectedAspectRatio(img.naturalWidth / img.naturalHeight);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
img.addEventListener("load", handleLoad);
|
|
333
|
+
|
|
334
|
+
return () => {
|
|
335
|
+
img.removeEventListener("load", handleLoad);
|
|
336
|
+
};
|
|
337
|
+
}, [descriptor?.src, artworkAspectRatio]);
|
|
338
|
+
|
|
339
|
+
// Calculate mask dimensions
|
|
340
|
+
const maskDimensions = useMemo(() => {
|
|
341
|
+
if (!isReady || containerSize.width === 0 || !descriptor) {
|
|
342
|
+
return { width: 0, height: 0 };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
width:
|
|
347
|
+
descriptor.effectiveAlignment === "horizontal"
|
|
348
|
+
? containerSize.height * descriptor.maskAspectRatio
|
|
349
|
+
: containerSize.width,
|
|
350
|
+
height:
|
|
351
|
+
descriptor.effectiveAlignment === "horizontal"
|
|
352
|
+
? containerSize.height
|
|
353
|
+
: containerSize.width / descriptor.maskAspectRatio,
|
|
354
|
+
};
|
|
355
|
+
}, [
|
|
356
|
+
containerSize.width,
|
|
357
|
+
containerSize.height,
|
|
358
|
+
descriptor?.maskAspectRatio,
|
|
359
|
+
descriptor?.effectiveAlignment,
|
|
360
|
+
isReady,
|
|
361
|
+
]);
|
|
362
|
+
|
|
363
|
+
// Pre-calculate clip-path coordinate helpers to avoid recalculation during drag
|
|
364
|
+
const clipPathCoords = useMemo(() => {
|
|
365
|
+
const halfMaskW = maskDimensions.width / 2;
|
|
366
|
+
const halfMaskH = maskDimensions.height / 2;
|
|
367
|
+
const halfContainerW = containerSize.width / 2;
|
|
368
|
+
const halfContainerH = containerSize.height / 2;
|
|
369
|
+
return { halfMaskW, halfMaskH, halfContainerW, halfContainerH };
|
|
370
|
+
}, [maskDimensions.width, maskDimensions.height, containerSize.width, containerSize.height]);
|
|
371
|
+
|
|
372
|
+
// Update position when alignment changes or component is ready
|
|
373
|
+
useEffect(() => {
|
|
374
|
+
// Skip if position was recently set by user drag to prevent "boomerang" effect
|
|
375
|
+
// Context updates can arrive with variable delay, so we use a 500ms window
|
|
376
|
+
if (Date.now() - lastUserDragTimeRef.current < 500) return;
|
|
377
|
+
if (!isReady || maskDimensions.width === 0 || !descriptor) return;
|
|
378
|
+
|
|
379
|
+
const points = getSnapPoints(
|
|
380
|
+
containerSize,
|
|
381
|
+
maskDimensions,
|
|
382
|
+
descriptor.effectiveAlignment
|
|
383
|
+
);
|
|
384
|
+
const newPosition = {
|
|
385
|
+
x:
|
|
386
|
+
descriptor.effectiveAlignment === "horizontal"
|
|
387
|
+
? points[alignment] || 0
|
|
388
|
+
: 0,
|
|
389
|
+
y:
|
|
390
|
+
descriptor.effectiveAlignment === "vertical"
|
|
391
|
+
? points[alignment] || 0
|
|
392
|
+
: 0,
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
setPosition(newPosition);
|
|
396
|
+
}, [
|
|
397
|
+
alignment,
|
|
398
|
+
descriptor?.effectiveAlignment,
|
|
399
|
+
containerSize.width,
|
|
400
|
+
containerSize.height,
|
|
401
|
+
maskDimensions.width,
|
|
402
|
+
maskDimensions.height,
|
|
403
|
+
isReady,
|
|
404
|
+
]);
|
|
405
|
+
|
|
406
|
+
// Memoize overlay style - uses pre-calculated coords for simpler template literal
|
|
407
|
+
const overlayStyle = useMemo((): CSSProperties => {
|
|
408
|
+
const { halfMaskW, halfMaskH, halfContainerW, halfContainerH } = clipPathCoords;
|
|
409
|
+
// Pre-calculate corner positions to reduce template literal complexity
|
|
410
|
+
const cx = position.x + halfContainerW;
|
|
411
|
+
const cy = position.y + halfContainerH;
|
|
412
|
+
const right = cx + halfMaskW;
|
|
413
|
+
const left = cx - halfMaskW;
|
|
414
|
+
const bottom = cy + halfMaskH;
|
|
415
|
+
const top = cy - halfMaskH;
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
position: "absolute",
|
|
419
|
+
top: 0,
|
|
420
|
+
left: 0,
|
|
421
|
+
right: 0,
|
|
422
|
+
bottom: 0,
|
|
423
|
+
backgroundColor: "rgba(0, 0, 0, 0.3)",
|
|
424
|
+
backdropFilter: "blur(3px)",
|
|
425
|
+
WebkitBackdropFilter: "blur(3px)",
|
|
426
|
+
zIndex: 1,
|
|
427
|
+
clipPath: isReady
|
|
428
|
+
? `polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%, 0% 0%, ${right}px ${bottom}px, ${right}px ${top}px, ${left}px ${top}px, ${left}px ${bottom}px, ${right}px ${bottom}px)`
|
|
429
|
+
: "none",
|
|
430
|
+
};
|
|
431
|
+
}, [isReady, position.x, position.y, clipPathCoords]);
|
|
432
|
+
|
|
433
|
+
// Memoize mask style - doesn't depend on position (transform applied separately in JSX)
|
|
434
|
+
const maskStyle = useMemo((): CSSProperties => ({
|
|
435
|
+
width: `${maskDimensions.width}px`,
|
|
436
|
+
height: `${maskDimensions.height}px`,
|
|
437
|
+
boxShadow: `0 0 0 2px var(--color-background), 0 0 0 4px var(--color-primary)`,
|
|
438
|
+
position: "absolute",
|
|
439
|
+
zIndex: 2,
|
|
440
|
+
boxSizing: "border-box",
|
|
441
|
+
cursor: "grab",
|
|
442
|
+
touchAction: "none",
|
|
443
|
+
top: "50%",
|
|
444
|
+
left: "50%",
|
|
445
|
+
willChange: "transform", // GPU acceleration for smoother drag
|
|
446
|
+
borderRadius: "var(--radius-image, 4px)",
|
|
447
|
+
}), [maskDimensions.width, maskDimensions.height]);
|
|
448
|
+
|
|
449
|
+
// Memoize clear image clip-path - reuses same coords as overlay
|
|
450
|
+
const clearImageClipPath = useMemo(() => {
|
|
451
|
+
if (!isReady) return "none";
|
|
452
|
+
const { halfMaskW, halfMaskH, halfContainerW, halfContainerH } = clipPathCoords;
|
|
453
|
+
const cx = position.x + halfContainerW;
|
|
454
|
+
const cy = position.y + halfContainerH;
|
|
455
|
+
const right = cx + halfMaskW;
|
|
456
|
+
const left = cx - halfMaskW;
|
|
457
|
+
const bottom = cy + halfMaskH;
|
|
458
|
+
const top = cy - halfMaskH;
|
|
459
|
+
return `polygon(${left}px ${top}px, ${right}px ${top}px, ${right}px ${bottom}px, ${left}px ${bottom}px)`;
|
|
460
|
+
}, [isReady, position.x, position.y, clipPathCoords]);
|
|
461
|
+
|
|
462
|
+
// Memoize mask transform - the dynamic position part
|
|
463
|
+
const maskTransform = useMemo(() => {
|
|
464
|
+
return descriptor?.effectiveAlignment === "horizontal"
|
|
465
|
+
? `translate(calc(-50% + ${position.x}px), -50%)`
|
|
466
|
+
: `translate(-50%, calc(-50% + ${position.y}px))`;
|
|
467
|
+
}, [descriptor?.effectiveAlignment, position.x, position.y]);
|
|
468
|
+
|
|
469
|
+
// Throttle context updates to prevent iOS Safari crashes
|
|
470
|
+
// Track last context update time and pending alignment
|
|
471
|
+
const lastContextUpdateTimeRef = useRef(0);
|
|
472
|
+
const pendingAlignmentRef = useRef<ImageAlignment | null>(null);
|
|
473
|
+
const throttleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
474
|
+
const THROTTLE_MS = 150; // Update context at most every 150ms during drag
|
|
475
|
+
|
|
476
|
+
const bind = useDrag(
|
|
477
|
+
({ offset: [ox, oy], last }: { offset: [number, number]; last: boolean }) => {
|
|
478
|
+
if (
|
|
479
|
+
maskDimensions.width === 0 ||
|
|
480
|
+
maskDimensions.height === 0 ||
|
|
481
|
+
!descriptor
|
|
482
|
+
) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const movement = descriptor.effectiveAlignment === "horizontal" ? ox : oy;
|
|
487
|
+
|
|
488
|
+
const maxDistance =
|
|
489
|
+
descriptor.effectiveAlignment === "horizontal"
|
|
490
|
+
? (containerSize.width - maskDimensions.width) / 2
|
|
491
|
+
: (containerSize.height - maskDimensions.height) / 2;
|
|
492
|
+
|
|
493
|
+
const clampedMovement = Math.max(
|
|
494
|
+
Math.min(movement, maxDistance),
|
|
495
|
+
-maxDistance
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
// Find closest snap point
|
|
499
|
+
const positions = Object.entries(
|
|
500
|
+
getSnapPoints(
|
|
501
|
+
containerSize,
|
|
502
|
+
maskDimensions,
|
|
503
|
+
descriptor.effectiveAlignment
|
|
504
|
+
)
|
|
505
|
+
);
|
|
506
|
+
const [closestPosition] = positions.reduce(
|
|
507
|
+
(nearest, [pos, value]) => {
|
|
508
|
+
const distance = Math.abs(clampedMovement - value);
|
|
509
|
+
return distance < nearest[1] ? [pos, distance] : nearest;
|
|
510
|
+
},
|
|
511
|
+
["center", Infinity]
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
// Get the actual position value for the closest snap point
|
|
515
|
+
const snapPoints = getSnapPoints(
|
|
516
|
+
containerSize,
|
|
517
|
+
maskDimensions,
|
|
518
|
+
descriptor.effectiveAlignment
|
|
519
|
+
);
|
|
520
|
+
const snappedPosition =
|
|
521
|
+
snapPoints[closestPosition as keyof typeof snapPoints];
|
|
522
|
+
|
|
523
|
+
// Record drag time to prevent sync effect from causing "boomerang"
|
|
524
|
+
lastUserDragTimeRef.current = Date.now();
|
|
525
|
+
|
|
526
|
+
// Calculate new position
|
|
527
|
+
const newPosition = {
|
|
528
|
+
x:
|
|
529
|
+
descriptor.effectiveAlignment === "horizontal"
|
|
530
|
+
? snappedPosition ?? 0
|
|
531
|
+
: 0,
|
|
532
|
+
y:
|
|
533
|
+
descriptor.effectiveAlignment === "vertical"
|
|
534
|
+
? snappedPosition ?? 0
|
|
535
|
+
: 0,
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// Only update position state if it actually changed (prevents unnecessary re-renders)
|
|
539
|
+
const positionChanged = newPosition.x !== position.x || newPosition.y !== position.y;
|
|
540
|
+
|
|
541
|
+
if (positionChanged) {
|
|
542
|
+
setPosition(newPosition);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Track alignment change for throttled context update
|
|
546
|
+
if (closestPosition !== alignment) {
|
|
547
|
+
pendingAlignmentRef.current = closestPosition as ImageAlignment;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Helper to commit alignment to context
|
|
551
|
+
const commitAlignment = () => {
|
|
552
|
+
if (pendingAlignmentRef.current !== null && placement && designContext) {
|
|
553
|
+
designContext.setPlacementDesign(placement, {
|
|
554
|
+
alignment: pendingAlignmentRef.current,
|
|
555
|
+
});
|
|
556
|
+
lastContextUpdateTimeRef.current = Date.now();
|
|
557
|
+
pendingAlignmentRef.current = null;
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
// On drag end, clear any pending timeout and commit immediately
|
|
562
|
+
if (last) {
|
|
563
|
+
if (throttleTimeoutRef.current) {
|
|
564
|
+
clearTimeout(throttleTimeoutRef.current);
|
|
565
|
+
throttleTimeoutRef.current = null;
|
|
566
|
+
}
|
|
567
|
+
commitAlignment();
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// During drag, throttle context updates to prevent iOS Safari crashes
|
|
572
|
+
if (pendingAlignmentRef.current !== null) {
|
|
573
|
+
const now = Date.now();
|
|
574
|
+
const timeSinceLastUpdate = now - lastContextUpdateTimeRef.current;
|
|
575
|
+
|
|
576
|
+
if (timeSinceLastUpdate >= THROTTLE_MS) {
|
|
577
|
+
// Enough time has passed, update immediately
|
|
578
|
+
commitAlignment();
|
|
579
|
+
} else if (!throttleTimeoutRef.current) {
|
|
580
|
+
// Schedule an update for when throttle period ends
|
|
581
|
+
throttleTimeoutRef.current = setTimeout(() => {
|
|
582
|
+
throttleTimeoutRef.current = null;
|
|
583
|
+
commitAlignment();
|
|
584
|
+
}, THROTTLE_MS - timeSinceLastUpdate);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
from: () => [position.x, position.y],
|
|
590
|
+
bounds: () => {
|
|
591
|
+
if (maskDimensions.width === 0 || maskDimensions.height === 0) {
|
|
592
|
+
return { left: 0, right: 0, top: 0, bottom: 0 };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const maxDistanceX = (containerSize.width - maskDimensions.width) / 2;
|
|
596
|
+
const maxDistanceY = (containerSize.height - maskDimensions.height) / 2;
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
left: -maxDistanceX,
|
|
600
|
+
right: maxDistanceX,
|
|
601
|
+
top: -maxDistanceY,
|
|
602
|
+
bottom: maxDistanceY,
|
|
603
|
+
};
|
|
604
|
+
},
|
|
605
|
+
}
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
// Early return AFTER all hooks
|
|
609
|
+
if (!descriptor) {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return (
|
|
614
|
+
<div
|
|
615
|
+
className={`relative ${className || ""}`}
|
|
616
|
+
style={{
|
|
617
|
+
display: "inline-block",
|
|
618
|
+
width: "fit-content",
|
|
619
|
+
maxWidth: "100%",
|
|
620
|
+
}}
|
|
621
|
+
>
|
|
622
|
+
<div className="overflow-hidden p-1">
|
|
623
|
+
<div ref={containerRef} className="relative" style={containerStyle}>
|
|
624
|
+
{/* Background image with blur */}
|
|
625
|
+
<img
|
|
626
|
+
ref={imageRef}
|
|
627
|
+
src={descriptor.src}
|
|
628
|
+
alt="Masked"
|
|
629
|
+
crossOrigin="anonymous"
|
|
630
|
+
className="h-full w-full block"
|
|
631
|
+
style={{
|
|
632
|
+
backgroundColor: "#fff",
|
|
633
|
+
backgroundPosition: "0 0, 10px 10px",
|
|
634
|
+
backgroundImage: `
|
|
635
|
+
linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5),
|
|
636
|
+
linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5)
|
|
637
|
+
`,
|
|
638
|
+
backgroundSize: "20px 20px, 20px 20px",
|
|
639
|
+
objectFit:
|
|
640
|
+
descriptor.effectiveAlignment === "vertical"
|
|
641
|
+
? "cover"
|
|
642
|
+
: "contain",
|
|
643
|
+
objectPosition: "center",
|
|
644
|
+
maskImage: "linear-gradient(circle, white 70%, transparent 71%)",
|
|
645
|
+
WebkitMaskImage:
|
|
646
|
+
"linear-gradient(circle, white 70%, transparent 71%)",
|
|
647
|
+
}}
|
|
648
|
+
onLoad={handleImageLoad}
|
|
649
|
+
loading="eager"
|
|
650
|
+
/>
|
|
651
|
+
|
|
652
|
+
{isReady && maskDimensions.width > 0 && (
|
|
653
|
+
<>
|
|
654
|
+
{/* Overlay with blur - excludes the selected area */}
|
|
655
|
+
<div style={overlayStyle} />
|
|
656
|
+
|
|
657
|
+
{/* Clear image copy for selected area only - sits on top */}
|
|
658
|
+
<img
|
|
659
|
+
src={descriptor.src}
|
|
660
|
+
alt="Selected area"
|
|
661
|
+
crossOrigin="anonymous"
|
|
662
|
+
className="h-full w-full block"
|
|
663
|
+
style={{
|
|
664
|
+
position: "absolute",
|
|
665
|
+
top: 0,
|
|
666
|
+
left: 0,
|
|
667
|
+
backgroundColor: "#fff",
|
|
668
|
+
backgroundPosition: "0 0, 10px 10px",
|
|
669
|
+
backgroundImage: `
|
|
670
|
+
linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5),
|
|
671
|
+
linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5)
|
|
672
|
+
`,
|
|
673
|
+
backgroundSize: "20px 20px, 20px 20px",
|
|
674
|
+
objectFit:
|
|
675
|
+
descriptor.effectiveAlignment === "vertical"
|
|
676
|
+
? "cover"
|
|
677
|
+
: "contain",
|
|
678
|
+
objectPosition: "center",
|
|
679
|
+
zIndex: 1,
|
|
680
|
+
maskImage:
|
|
681
|
+
"linear-gradient(circle, white 70%, transparent 71%)",
|
|
682
|
+
WebkitMaskImage:
|
|
683
|
+
"linear-gradient(circle, white 70%, transparent 71%)",
|
|
684
|
+
clipPath: clearImageClipPath,
|
|
685
|
+
}}
|
|
686
|
+
loading="eager"
|
|
687
|
+
/>
|
|
688
|
+
|
|
689
|
+
{/* Selection frame */}
|
|
690
|
+
<div
|
|
691
|
+
{...bind()}
|
|
692
|
+
style={{
|
|
693
|
+
...maskStyle,
|
|
694
|
+
transform: maskTransform,
|
|
695
|
+
}}
|
|
696
|
+
/>
|
|
697
|
+
</>
|
|
698
|
+
)}
|
|
699
|
+
</div>
|
|
700
|
+
</div>
|
|
701
|
+
</div>
|
|
702
|
+
);
|
|
703
|
+
}
|