@shopify/shop-minis-react 0.0.0-snapshot.20251216112959 → 0.0.0-snapshot.20251216185714
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/components/atoms/alert-dialog.js.map +1 -1
- package/dist/components/atoms/button.js.map +1 -1
- package/dist/components/atoms/icon-button.js.map +1 -1
- package/dist/components/atoms/image.js +65 -51
- package/dist/components/atoms/image.js.map +1 -1
- package/dist/components/atoms/list.js.map +1 -1
- package/dist/components/atoms/product-variant-price.js +1 -1
- package/dist/components/atoms/product-variant-price.js.map +1 -1
- package/dist/components/atoms/text-input.js.map +1 -1
- package/dist/components/atoms/touchable.js.map +1 -1
- package/dist/components/atoms/video-player.js +1 -1
- package/dist/components/atoms/video-player.js.map +1 -1
- package/dist/components/commerce/add-to-cart.js.map +1 -1
- package/dist/components/commerce/buy-now.js.map +1 -1
- package/dist/components/commerce/favorite-button.js +1 -4
- package/dist/components/commerce/favorite-button.js.map +1 -1
- package/dist/components/commerce/merchant-card.js.map +1 -1
- package/dist/components/commerce/product-card.js +1 -1
- package/dist/components/commerce/product-card.js.map +1 -1
- package/dist/components/commerce/product-link.js +1 -1
- package/dist/components/commerce/product-link.js.map +1 -1
- package/dist/components/commerce/quantity-selector.js.map +1 -1
- package/dist/components/content/image-content-wrapper.js.map +1 -1
- package/dist/components/navigation/minis-router.js.map +1 -1
- package/dist/components/navigation/transition-link.js.map +1 -1
- package/dist/components/ui/alert.js.map +1 -1
- package/dist/components/ui/badge.js.map +1 -1
- package/dist/components/ui/input.js.map +1 -1
- package/dist/hooks/storage/useImageUpload.js +32 -24
- package/dist/hooks/storage/useImageUpload.js.map +1 -1
- package/dist/index.js +92 -88
- package/dist/index.js.map +1 -1
- package/dist/mocks.js +70 -50
- package/dist/mocks.js.map +1 -1
- package/dist/utils/formatMoney.js.map +1 -0
- package/dist/utils/image.js +44 -24
- package/dist/utils/image.js.map +1 -1
- package/eslint/rules/validate-manifest.cjs +91 -41
- package/package.json +2 -2
- package/src/components/atoms/alert-dialog.tsx +3 -3
- package/src/components/atoms/button.tsx +22 -0
- package/src/components/atoms/icon-button.tsx +16 -8
- package/src/components/atoms/image.tsx +41 -8
- package/src/components/atoms/list.tsx +25 -2
- package/src/components/atoms/product-variant-price.test.tsx +1 -1
- package/src/components/atoms/product-variant-price.tsx +1 -1
- package/src/components/atoms/text-input.tsx +3 -1
- package/src/components/atoms/touchable.tsx +15 -4
- package/src/components/atoms/video-player.tsx +16 -6
- package/src/components/commerce/add-to-cart.tsx +7 -11
- package/src/components/commerce/buy-now.tsx +7 -10
- package/src/components/commerce/favorite-button.tsx +6 -5
- package/src/components/commerce/merchant-card.tsx +4 -0
- package/src/components/commerce/product-card.test.tsx +1 -1
- package/src/components/commerce/product-card.tsx +1 -1
- package/src/components/commerce/product-link.test.tsx +1 -1
- package/src/components/commerce/product-link.tsx +16 -1
- package/src/components/commerce/quantity-selector.tsx +6 -1
- package/src/components/content/image-content-wrapper.tsx +16 -1
- package/src/components/navigation/minis-router.tsx +2 -2
- package/src/components/navigation/transition-link.tsx +11 -1
- package/src/components/ui/alert.tsx +7 -0
- package/src/components/ui/badge.tsx +9 -0
- package/src/components/ui/input.tsx +15 -0
- package/src/hooks/storage/useImageUpload.ts +13 -0
- package/src/mocks.ts +48 -19
- package/src/utils/image.ts +38 -0
- package/src/utils/index.ts +1 -0
- package/dist/lib/formatMoney.js.map +0 -1
- /package/dist/{lib → utils}/formatMoney.js +0 -0
- /package/src/{lib → utils}/formatMoney.ts +0 -0
|
@@ -10,7 +10,24 @@ import {
|
|
|
10
10
|
} from 'react'
|
|
11
11
|
|
|
12
12
|
import {cn} from '../../lib/utils'
|
|
13
|
-
import {
|
|
13
|
+
import {getThumbhashBlobURL, getResizedImageUrl} from '../../utils'
|
|
14
|
+
|
|
15
|
+
export interface ImageDocProps {
|
|
16
|
+
/** Remote image URL */
|
|
17
|
+
src?: string
|
|
18
|
+
/** File object from useImagePicker (auto-manages blob URL lifecycle) */
|
|
19
|
+
file?: File
|
|
20
|
+
/** Thumbhash string for progressive loading placeholder */
|
|
21
|
+
thumbhash?: string | null
|
|
22
|
+
/** Aspect ratio (e.g., 16/9, "4/3", or "auto") */
|
|
23
|
+
aspectRatio?: number | string
|
|
24
|
+
/** How the image should fit within its container */
|
|
25
|
+
objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | 'none'
|
|
26
|
+
/** Alt text for accessibility */
|
|
27
|
+
alt?: string
|
|
28
|
+
/** Callback when image finishes loading */
|
|
29
|
+
onLoad?: () => void
|
|
30
|
+
}
|
|
14
31
|
|
|
15
32
|
type ImageProps = ImgHTMLAttributes<HTMLImageElement> & {
|
|
16
33
|
src?: string
|
|
@@ -52,11 +69,20 @@ export const Image = memo(function Image(props: ImageProps) {
|
|
|
52
69
|
}
|
|
53
70
|
}, [file])
|
|
54
71
|
|
|
55
|
-
const
|
|
56
|
-
() =>
|
|
72
|
+
const thumbhashBlobUrl = useMemo(
|
|
73
|
+
() => getThumbhashBlobURL(thumbhash ?? undefined),
|
|
57
74
|
[thumbhash]
|
|
58
75
|
)
|
|
59
76
|
|
|
77
|
+
// Cleanup blob URL when it changes or component unmounts
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
return () => {
|
|
80
|
+
if (thumbhashBlobUrl) {
|
|
81
|
+
URL.revokeObjectURL(thumbhashBlobUrl)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}, [thumbhashBlobUrl])
|
|
85
|
+
|
|
60
86
|
const handleLoad = useCallback(
|
|
61
87
|
(event: React.SyntheticEvent<HTMLImageElement, Event>) => {
|
|
62
88
|
setIsLoaded(true)
|
|
@@ -77,13 +103,20 @@ export const Image = memo(function Image(props: ImageProps) {
|
|
|
77
103
|
style={{
|
|
78
104
|
...style,
|
|
79
105
|
...(aspectRatio !== 'auto' && {aspectRatio}),
|
|
80
|
-
backgroundImage: thumbhashDataURL
|
|
81
|
-
? `url(${thumbhashDataURL})`
|
|
82
|
-
: undefined,
|
|
83
|
-
backgroundSize: 'cover',
|
|
84
|
-
backgroundPosition: 'center',
|
|
85
106
|
}}
|
|
86
107
|
>
|
|
108
|
+
{thumbhashBlobUrl && !isLoaded && (
|
|
109
|
+
<img
|
|
110
|
+
className={cn(
|
|
111
|
+
aspectRatio === 'auto'
|
|
112
|
+
? 'w-full h-auto'
|
|
113
|
+
: 'absolute inset-0 size-full',
|
|
114
|
+
'object-cover'
|
|
115
|
+
)}
|
|
116
|
+
src={thumbhashBlobUrl}
|
|
117
|
+
aria-hidden="true"
|
|
118
|
+
/>
|
|
119
|
+
)}
|
|
87
120
|
<img
|
|
88
121
|
className={cn(
|
|
89
122
|
aspectRatio === 'auto'
|
|
@@ -12,7 +12,30 @@ import {Skeleton} from '../ui/skeleton'
|
|
|
12
12
|
const DEFAULT_REFRESH_PULL_THRESHOLD = 200
|
|
13
13
|
const ELEMENT_BIND_DELAY = 100
|
|
14
14
|
|
|
15
|
-
interface
|
|
15
|
+
export interface ListDocProps<T = any> {
|
|
16
|
+
/** Array of items to render */
|
|
17
|
+
items: T[]
|
|
18
|
+
/** Function to render each item */
|
|
19
|
+
renderItem: (item: T, index: number) => React.ReactNode
|
|
20
|
+
/** Height of the list container */
|
|
21
|
+
height?: string | number
|
|
22
|
+
/** Show scrollbar (default: false) */
|
|
23
|
+
showScrollbar?: boolean
|
|
24
|
+
/** Header element rendered at the top of the list */
|
|
25
|
+
header?: React.ReactNode
|
|
26
|
+
/** Callback to fetch more items when scrolled to bottom */
|
|
27
|
+
fetchMore?: () => Promise<void>
|
|
28
|
+
/** Custom loading component shown while fetching more */
|
|
29
|
+
loadingComponent?: React.ReactNode
|
|
30
|
+
/** Callback for pull-to-refresh */
|
|
31
|
+
onRefresh?: () => Promise<void>
|
|
32
|
+
/** Whether the list is currently refreshing */
|
|
33
|
+
refreshing?: boolean
|
|
34
|
+
/** Enable pull-to-refresh gesture (default: true) */
|
|
35
|
+
enablePullToRefresh?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ListProps<T = any>
|
|
16
39
|
extends Omit<
|
|
17
40
|
VirtuosoProps<T, unknown>,
|
|
18
41
|
'data' | 'itemContent' | 'endReached'
|
|
@@ -41,7 +64,7 @@ export function List<T = any>({
|
|
|
41
64
|
refreshing,
|
|
42
65
|
enablePullToRefresh = true,
|
|
43
66
|
...virtuosoProps
|
|
44
|
-
}:
|
|
67
|
+
}: ListProps<T>) {
|
|
45
68
|
const inFlightFetchMoreRef = useRef<Promise<void> | null>(null)
|
|
46
69
|
const virtuosoRef = useRef<any>(null)
|
|
47
70
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
@@ -5,7 +5,7 @@ import {render, screen} from '../../test-utils'
|
|
|
5
5
|
import {ProductVariantPrice} from './product-variant-price'
|
|
6
6
|
|
|
7
7
|
// Mock formatMoney function
|
|
8
|
-
vi.mock('../../
|
|
8
|
+
vi.mock('../../utils/formatMoney', () => ({
|
|
9
9
|
formatMoney: vi.fn((amount: string, currencyCode: string) => {
|
|
10
10
|
const numAmount = parseFloat(amount)
|
|
11
11
|
return currencyCode === 'USD'
|
|
@@ -3,7 +3,9 @@ import * as React from 'react'
|
|
|
3
3
|
import {useKeyboardAvoidingView} from '../../hooks'
|
|
4
4
|
import {Input} from '../ui/input'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
export type TextInputProps = React.ComponentProps<'input'>
|
|
7
|
+
|
|
8
|
+
function TextInput({...props}: TextInputProps) {
|
|
7
9
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
8
10
|
const {onBlur, onFocus} = useKeyboardAvoidingView()
|
|
9
11
|
|
|
@@ -2,15 +2,26 @@ import * as React from 'react'
|
|
|
2
2
|
|
|
3
3
|
import {motion, HTMLMotionProps, useAnimationControls} from 'motion/react'
|
|
4
4
|
|
|
5
|
+
export interface TouchableDocProps {
|
|
6
|
+
/** Click handler */
|
|
7
|
+
onClick?: React.MouseEventHandler<HTMLDivElement>
|
|
8
|
+
/** Prevent click event from bubbling to parent elements */
|
|
9
|
+
stopPropagation?: boolean
|
|
10
|
+
/** Content to render inside the touchable area */
|
|
11
|
+
children?: React.ReactNode
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TouchableProps extends HTMLMotionProps<'div'> {
|
|
15
|
+
onClick?: React.MouseEventHandler<HTMLDivElement>
|
|
16
|
+
stopPropagation?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
5
19
|
export const Touchable = ({
|
|
6
20
|
children,
|
|
7
21
|
onClick,
|
|
8
22
|
stopPropagation = false,
|
|
9
23
|
...props
|
|
10
|
-
}:
|
|
11
|
-
onClick?: React.MouseEventHandler<HTMLDivElement>
|
|
12
|
-
stopPropagation?: boolean
|
|
13
|
-
}) => {
|
|
24
|
+
}: TouchableProps) => {
|
|
14
25
|
const ref = React.useRef<HTMLDivElement>(null)
|
|
15
26
|
const controls = useAnimationControls()
|
|
16
27
|
|
|
@@ -17,24 +17,34 @@ export interface VideoPlayerRef {
|
|
|
17
17
|
pause: () => void
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
interface VideoPlayerProps {
|
|
20
|
+
export interface VideoPlayerProps {
|
|
21
|
+
/** The video source URL */
|
|
21
22
|
src: string
|
|
22
|
-
/**
|
|
23
|
-
* The format/MIME type of the video.
|
|
24
|
-
* @default 'video/mp4'
|
|
25
|
-
*/
|
|
23
|
+
/** The format/MIME type of the video (default: 'video/mp4') */
|
|
26
24
|
format?: string
|
|
25
|
+
/** Whether the video should be muted */
|
|
27
26
|
muted?: boolean
|
|
27
|
+
/** URL for the poster image shown before playback */
|
|
28
28
|
poster?: string
|
|
29
|
+
/** Whether the video should autoplay */
|
|
29
30
|
autoplay?: boolean
|
|
31
|
+
/** Preload behavior: 'none', 'metadata', or 'auto' */
|
|
30
32
|
preload?: 'none' | 'metadata' | 'auto'
|
|
33
|
+
/** Whether the video should loop */
|
|
31
34
|
loop?: boolean
|
|
35
|
+
/** Video width in pixels */
|
|
32
36
|
width?: number
|
|
37
|
+
/** Video height in pixels */
|
|
33
38
|
height?: number
|
|
39
|
+
/** Custom play button component */
|
|
34
40
|
playButtonComponent?: React.ReactNode
|
|
41
|
+
/** Callback when video starts playing */
|
|
35
42
|
onPlay?: () => void
|
|
43
|
+
/** Callback when video is paused */
|
|
36
44
|
onPause?: () => void
|
|
45
|
+
/** Callback when video ends */
|
|
37
46
|
onEnded?: () => void
|
|
47
|
+
/** Callback when video player is ready */
|
|
38
48
|
onReady?: () => void
|
|
39
49
|
}
|
|
40
50
|
|
|
@@ -49,7 +59,7 @@ export const VideoPlayer: React.ForwardRefExoticComponent<
|
|
|
49
59
|
muted,
|
|
50
60
|
autoplay,
|
|
51
61
|
preload = 'auto',
|
|
52
|
-
loop =
|
|
62
|
+
loop = false,
|
|
53
63
|
width,
|
|
54
64
|
height,
|
|
55
65
|
playButtonComponent,
|
|
@@ -10,22 +10,18 @@ import {useShopCartActions} from '../../internal/useShopCartActions'
|
|
|
10
10
|
import {cn} from '../../lib/utils'
|
|
11
11
|
import {Button} from '../atoms/button'
|
|
12
12
|
|
|
13
|
-
interface AddToCartButtonProps {
|
|
13
|
+
export interface AddToCartButtonProps {
|
|
14
|
+
/** Whether the button is disabled */
|
|
14
15
|
disabled?: boolean
|
|
16
|
+
/** CSS class name */
|
|
15
17
|
className?: string
|
|
18
|
+
/** Button size variant */
|
|
16
19
|
size?: 'default' | 'sm' | 'lg'
|
|
17
|
-
/**
|
|
18
|
-
* The discount codes to apply to the cart.
|
|
19
|
-
*/
|
|
20
|
+
/** The discount codes to apply to the cart */
|
|
20
21
|
discountCodes?: string[]
|
|
21
|
-
/**
|
|
22
|
-
* The GID of the product variant. E.g. `gid://shopify/ProductVariant/456`.
|
|
23
|
-
*/
|
|
22
|
+
/** The GID of the product variant. E.g. `gid://shopify/ProductVariant/456` */
|
|
24
23
|
productVariantId: string
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* The product to add to the cart.
|
|
28
|
-
*/
|
|
24
|
+
/** The product to add to the cart */
|
|
29
25
|
product?: Product
|
|
30
26
|
}
|
|
31
27
|
|
|
@@ -8,21 +8,18 @@ import {useShopCartActions} from '../../internal/useShopCartActions'
|
|
|
8
8
|
import {cn} from '../../lib/utils'
|
|
9
9
|
import {Button} from '../atoms/button'
|
|
10
10
|
|
|
11
|
-
interface BuyNowButtonProps {
|
|
11
|
+
export interface BuyNowButtonProps {
|
|
12
|
+
/** Whether the button is disabled */
|
|
12
13
|
disabled?: boolean
|
|
14
|
+
/** CSS class name */
|
|
13
15
|
className?: string
|
|
16
|
+
/** Button size variant */
|
|
14
17
|
size?: 'default' | 'sm' | 'lg'
|
|
15
|
-
/**
|
|
16
|
-
* The discount code to apply to the purchase.
|
|
17
|
-
*/
|
|
18
|
+
/** The discount code to apply to the purchase */
|
|
18
19
|
discountCode?: string
|
|
19
|
-
/**
|
|
20
|
-
* The GID of the product variant. E.g. `gid://shopify/ProductVariant/456`.
|
|
21
|
-
*/
|
|
20
|
+
/** The GID of the product variant. E.g. `gid://shopify/ProductVariant/456` */
|
|
22
21
|
productVariantId: string
|
|
23
|
-
/**
|
|
24
|
-
* The product to buy now.
|
|
25
|
-
*/
|
|
22
|
+
/** The product to buy now */
|
|
26
23
|
product?: Product
|
|
27
24
|
}
|
|
28
25
|
|
|
@@ -2,13 +2,14 @@ import {Heart} from 'lucide-react'
|
|
|
2
2
|
|
|
3
3
|
import {IconButton} from '../atoms/icon-button'
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
filled = false,
|
|
8
|
-
}: {
|
|
5
|
+
export interface FavoriteButtonProps {
|
|
6
|
+
/** Click handler for toggling favorite state */
|
|
9
7
|
onClick?: () => void
|
|
8
|
+
/** Whether the product is currently favorited */
|
|
10
9
|
filled?: boolean
|
|
11
|
-
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function FavoriteButton({onClick, filled = false}: FavoriteButtonProps) {
|
|
12
13
|
return (
|
|
13
14
|
<IconButton
|
|
14
15
|
Icon={Heart}
|
|
@@ -365,9 +365,13 @@ function MerchantCardHeader({
|
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
export interface MerchantCardProps {
|
|
368
|
+
/** The shop/merchant to display */
|
|
368
369
|
shop: Shop
|
|
370
|
+
/** Whether the card is tappable to navigate to shop (default: true) */
|
|
369
371
|
touchable?: boolean
|
|
372
|
+
/** Maximum number of featured product images to show (default: 4) */
|
|
370
373
|
featuredImagesLimit?: number
|
|
374
|
+
/** Custom content to render inside the card */
|
|
371
375
|
children?: React.ReactNode
|
|
372
376
|
}
|
|
373
377
|
|
|
@@ -40,7 +40,7 @@ vi.mock('../../hooks/user/useSavedProductsActions', () => ({
|
|
|
40
40
|
}))
|
|
41
41
|
|
|
42
42
|
// Mock formatMoney
|
|
43
|
-
vi.mock('../../
|
|
43
|
+
vi.mock('../../utils/formatMoney', () => ({
|
|
44
44
|
formatMoney: vi.fn((amount: string, currencyCode: string) => {
|
|
45
45
|
const numAmount = parseFloat(amount)
|
|
46
46
|
return currencyCode === 'USD'
|
|
@@ -6,8 +6,8 @@ import {type Product, type ProductVariant} from '@shopify/shop-minis-platform'
|
|
|
6
6
|
import {useShopNavigation} from '../../hooks/navigation/useShopNavigation'
|
|
7
7
|
import {useSavedProductsActions} from '../../hooks/user/useSavedProductsActions'
|
|
8
8
|
import {ProductReviewStars} from '../../internal/components/product-review-stars'
|
|
9
|
-
import {formatMoney} from '../../lib/formatMoney'
|
|
10
9
|
import {cn} from '../../lib/utils'
|
|
10
|
+
import {formatMoney} from '../../utils/formatMoney'
|
|
11
11
|
import {Image} from '../atoms/image'
|
|
12
12
|
import {ProductVariantPrice} from '../atoms/product-variant-price'
|
|
13
13
|
import {Touchable} from '../atoms/touchable'
|
|
@@ -28,7 +28,7 @@ vi.mock('../../hooks/user/useSavedProductsActions', () => ({
|
|
|
28
28
|
}))
|
|
29
29
|
|
|
30
30
|
// Mock formatMoney
|
|
31
|
-
vi.mock('../../
|
|
31
|
+
vi.mock('../../utils/formatMoney', () => ({
|
|
32
32
|
formatMoney: vi.fn((amount: string, currencyCode: string) => {
|
|
33
33
|
const numAmount = parseFloat(amount)
|
|
34
34
|
return currencyCode === 'USD'
|
|
@@ -7,8 +7,8 @@ import {Slot as SlotPrimitive} from 'radix-ui'
|
|
|
7
7
|
import {useShopNavigation} from '../../hooks/navigation/useShopNavigation'
|
|
8
8
|
import {useSavedProductsActions} from '../../hooks/user/useSavedProductsActions'
|
|
9
9
|
import {ProductReviewStars} from '../../internal/components/product-review-stars'
|
|
10
|
-
import {formatMoney} from '../../lib/formatMoney'
|
|
11
10
|
import {cn} from '../../lib/utils'
|
|
11
|
+
import {formatMoney} from '../../utils/formatMoney'
|
|
12
12
|
import {Touchable} from '../atoms/touchable'
|
|
13
13
|
import {Card, CardContent, CardAction} from '../ui/card'
|
|
14
14
|
|
|
@@ -226,6 +226,21 @@ function ProductLinkActions({
|
|
|
226
226
|
)
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
export interface ProductLinkDocProps {
|
|
230
|
+
/** The product to display */
|
|
231
|
+
product: Product
|
|
232
|
+
/** Hide the favorite/save button */
|
|
233
|
+
hideFavoriteAction?: boolean
|
|
234
|
+
/** Callback when the product link is clicked */
|
|
235
|
+
onClick?: (product: Product) => void
|
|
236
|
+
/** Hide the review stars */
|
|
237
|
+
reviewsDisabled?: boolean
|
|
238
|
+
/** Custom action element to replace the favorite button. Must be provided with `onCustomActionClick`. */
|
|
239
|
+
customAction?: React.ReactNode
|
|
240
|
+
/** Callback when the custom action is clicked. Must be provided with `customAction`. */
|
|
241
|
+
onCustomActionClick?: () => void
|
|
242
|
+
}
|
|
243
|
+
|
|
229
244
|
export type ProductLinkProps = {
|
|
230
245
|
product: Product
|
|
231
246
|
hideFavoriteAction?: boolean
|
|
@@ -5,11 +5,16 @@ import {Minus, Plus} from 'lucide-react'
|
|
|
5
5
|
import {cn} from '../../lib/utils'
|
|
6
6
|
import {IconButton} from '../atoms/icon-button'
|
|
7
7
|
|
|
8
|
-
interface QuantitySelectorProps {
|
|
8
|
+
export interface QuantitySelectorProps {
|
|
9
|
+
/** Current quantity value */
|
|
9
10
|
quantity: number
|
|
11
|
+
/** Callback when quantity changes */
|
|
10
12
|
onQuantityChange: (quantity: number) => void
|
|
13
|
+
/** Maximum allowed quantity */
|
|
11
14
|
maxQuantity: number
|
|
15
|
+
/** Minimum allowed quantity (default: 1) */
|
|
12
16
|
minQuantity?: number
|
|
17
|
+
/** Whether the selector is disabled */
|
|
13
18
|
disabled?: boolean
|
|
14
19
|
}
|
|
15
20
|
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import {ContentWrapper} from '../atoms/content-wrapper'
|
|
2
2
|
import {Image} from '../atoms/image'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
export interface ImageContentWrapperDocProps {
|
|
5
|
+
/** The public ID of the uploaded image (use this OR externalId) */
|
|
6
|
+
publicId?: string
|
|
7
|
+
/** The external ID of the uploaded image (use this OR publicId) */
|
|
8
|
+
externalId?: string
|
|
9
|
+
/** Callback when the image loads */
|
|
10
|
+
onLoad?: () => void
|
|
11
|
+
/** Image width */
|
|
12
|
+
width?: number
|
|
13
|
+
/** Image height */
|
|
14
|
+
height?: number
|
|
15
|
+
/** Loading placeholder */
|
|
16
|
+
Loader?: React.ReactNode | string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ImageContentWrapperProps = (
|
|
5
20
|
| {publicId: string; externalId?: never}
|
|
6
21
|
| {externalId: string; publicId?: never}
|
|
7
22
|
) & {
|
|
@@ -2,7 +2,7 @@ import {BrowserRouter, BrowserRouterProps} from 'react-router'
|
|
|
2
2
|
|
|
3
3
|
import {TransitionContainer} from './transition-container'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
export interface MinisRouterProps extends BrowserRouterProps {
|
|
6
6
|
viewTransitions?: boolean
|
|
7
7
|
}
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ export function MinisRouter({
|
|
|
10
10
|
children,
|
|
11
11
|
viewTransitions = false,
|
|
12
12
|
...props
|
|
13
|
-
}:
|
|
13
|
+
}: MinisRouterProps) {
|
|
14
14
|
if (viewTransitions) {
|
|
15
15
|
return (
|
|
16
16
|
<BrowserRouter {...props}>
|
|
@@ -4,7 +4,17 @@ import {useHref} from 'react-router'
|
|
|
4
4
|
|
|
5
5
|
import {useNavigateWithTransition} from '../../hooks/navigation/useNavigateWithTransition'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
export interface TransitionLinkDocProps {
|
|
8
|
+
/** The target path to navigate to */
|
|
9
|
+
to: string
|
|
10
|
+
/** Click handler called before navigation */
|
|
11
|
+
onClick?: React.MouseEventHandler<HTMLAnchorElement>
|
|
12
|
+
/** Content to render inside the link */
|
|
13
|
+
children?: React.ReactNode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TransitionLinkProps
|
|
17
|
+
extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
8
18
|
to: string
|
|
9
19
|
}
|
|
10
20
|
|
|
@@ -4,6 +4,13 @@ import {cva, type VariantProps} from 'class-variance-authority'
|
|
|
4
4
|
|
|
5
5
|
import {cn} from '../../lib/utils'
|
|
6
6
|
|
|
7
|
+
export interface AlertDocProps {
|
|
8
|
+
/** Visual style variant */
|
|
9
|
+
variant?: 'default' | 'destructive'
|
|
10
|
+
/** Content to render inside */
|
|
11
|
+
children?: React.ReactNode
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
const alertVariants = cva(
|
|
8
15
|
'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
|
|
9
16
|
{
|
|
@@ -5,6 +5,15 @@ import {Slot as SlotPrimitive} from 'radix-ui'
|
|
|
5
5
|
|
|
6
6
|
import {cn} from '../../lib/utils'
|
|
7
7
|
|
|
8
|
+
export interface BadgeDocProps {
|
|
9
|
+
/** Visual style variant */
|
|
10
|
+
variant?: 'primary' | 'secondary' | 'destructive' | 'outline' | 'none'
|
|
11
|
+
/** Render as child element instead of span */
|
|
12
|
+
asChild?: boolean
|
|
13
|
+
/** Content to render inside */
|
|
14
|
+
children?: React.ReactNode
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
const badgeVariants = cva(
|
|
9
18
|
'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none transition-[color,box-shadow] overflow-hidden',
|
|
10
19
|
{
|
|
@@ -2,6 +2,21 @@ import * as React from 'react'
|
|
|
2
2
|
|
|
3
3
|
import {cn} from '../../lib/utils'
|
|
4
4
|
|
|
5
|
+
export interface InputDocProps {
|
|
6
|
+
/** Ref to the input element (use instead of ref) */
|
|
7
|
+
innerRef?: React.Ref<HTMLInputElement>
|
|
8
|
+
/** Input type (text, email, password, etc.) */
|
|
9
|
+
type?: string
|
|
10
|
+
/** Placeholder text */
|
|
11
|
+
placeholder?: string
|
|
12
|
+
/** Current value */
|
|
13
|
+
value?: string
|
|
14
|
+
/** Change handler */
|
|
15
|
+
onChange?: React.ChangeEventHandler<HTMLInputElement>
|
|
16
|
+
/** Whether the input is disabled */
|
|
17
|
+
disabled?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
// using the default ref doesn't seem to set the parent's ref object when mounted
|
|
6
21
|
// Since this is a shadCN component, we need to make sure to add back the innerRef prop,
|
|
7
22
|
// whenever the component is updated.
|
|
@@ -107,6 +107,19 @@ export const useImageUpload = (): UseImageUploadReturns => {
|
|
|
107
107
|
throw new Error(links.error.message)
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
if (links.mocked) {
|
|
111
|
+
// Skip upload and return mock data
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
114
|
+
id: 'uploaded-image-id',
|
|
115
|
+
imageUrl:
|
|
116
|
+
'https://cdn.shopify.com/s/files/1/0621/0463/3599/files/Mr._Bean_2007_800x800.jpg?v=1763126175',
|
|
117
|
+
resourceUrl:
|
|
118
|
+
'https://cdn.shopify.com/s/files/1/0621/0463/3599/files/Mr._Bean_2007_800x800.jpg?v=1763126175',
|
|
119
|
+
},
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
|
|
110
123
|
// Upload single file to GCS
|
|
111
124
|
const {error: uploadError} = await uploadFileToGCS(
|
|
112
125
|
processedImageParams,
|
package/src/mocks.ts
CHANGED
|
@@ -6,28 +6,50 @@ import {
|
|
|
6
6
|
} from '@shopify/shop-minis-platform'
|
|
7
7
|
import {ShopActions} from '@shopify/shop-minis-platform/actions'
|
|
8
8
|
|
|
9
|
+
const SAMPLE_IMAGE_NAMES = [
|
|
10
|
+
'garnished.jpeg',
|
|
11
|
+
'bath.jpeg',
|
|
12
|
+
'teapot.jpg',
|
|
13
|
+
'shoes.jpeg',
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
// Simple hash function to get a deterministic index from a string
|
|
17
|
+
const hashString = (str: string): number => {
|
|
18
|
+
let hash = 0
|
|
19
|
+
for (let i = 0; i < str.length; i++) {
|
|
20
|
+
hash = (hash << 5) - hash + str.charCodeAt(i)
|
|
21
|
+
hash |= 0
|
|
22
|
+
}
|
|
23
|
+
return Math.abs(hash)
|
|
24
|
+
}
|
|
25
|
+
|
|
9
26
|
// Helper functions for common data structures
|
|
10
27
|
export const createProduct = (
|
|
11
28
|
id: string,
|
|
12
29
|
title: string,
|
|
13
30
|
price = '99.99',
|
|
14
31
|
compareAtPrice?: string
|
|
15
|
-
): Product =>
|
|
16
|
-
id
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
): Product => {
|
|
33
|
+
const imageIndex = hashString(id) % SAMPLE_IMAGE_NAMES.length
|
|
34
|
+
const imageName = SAMPLE_IMAGE_NAMES[imageIndex]
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
id,
|
|
38
|
+
title,
|
|
39
|
+
price: {amount: price, currencyCode: 'USD'},
|
|
40
|
+
...(compareAtPrice && {
|
|
41
|
+
compareAtPrice: {amount: compareAtPrice, currencyCode: 'USD'},
|
|
42
|
+
}),
|
|
43
|
+
reviewAnalytics: {averageRating: 4.5, reviewCount: 10},
|
|
44
|
+
shop: createShop('shop1', 'Mock Shop'),
|
|
45
|
+
defaultVariantId: `variant-${id}`,
|
|
46
|
+
isFavorited: false,
|
|
47
|
+
featuredImage: {
|
|
48
|
+
url: `https://cdn.shopify.com/static/sample-images/${imageName}`,
|
|
49
|
+
altText: title,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
31
53
|
|
|
32
54
|
export const createShop = (
|
|
33
55
|
id: string,
|
|
@@ -202,7 +224,11 @@ function makeMockMethod<K extends keyof ShopActions>(
|
|
|
202
224
|
): ShopActions[K] {
|
|
203
225
|
return ((params: Parameters<ShopActions[K]>[0]) => {
|
|
204
226
|
logMockAction(String(key), params)
|
|
205
|
-
return Promise.resolve({
|
|
227
|
+
return Promise.resolve({
|
|
228
|
+
ok: true as const,
|
|
229
|
+
data: result,
|
|
230
|
+
mocked: true,
|
|
231
|
+
})
|
|
206
232
|
}) as ShopActions[K]
|
|
207
233
|
}
|
|
208
234
|
|
|
@@ -249,6 +275,7 @@ export function makeMockActions(): ShopActions {
|
|
|
249
275
|
navigateToOrder: undefined,
|
|
250
276
|
navigateToCheckout: undefined,
|
|
251
277
|
createImageUploadLink: {
|
|
278
|
+
// This action is mocked in the actual hook. See `useImageUpload` for more details.
|
|
252
279
|
targets: [
|
|
253
280
|
{
|
|
254
281
|
url: 'https://example.com/upload',
|
|
@@ -262,7 +289,9 @@ export function makeMockActions(): ShopActions {
|
|
|
262
289
|
{
|
|
263
290
|
id: 'file-123',
|
|
264
291
|
fileStatus: 'READY',
|
|
265
|
-
image: {
|
|
292
|
+
image: {
|
|
293
|
+
url: 'https://example.com/image.jpg',
|
|
294
|
+
},
|
|
266
295
|
},
|
|
267
296
|
],
|
|
268
297
|
},
|
|
@@ -556,7 +585,7 @@ export const injectMocks = ({force}: {force?: boolean} = {}) => {
|
|
|
556
585
|
window.minisSDK = makeMockActions()
|
|
557
586
|
window.minisParams = {
|
|
558
587
|
handle: 'mock-handle',
|
|
559
|
-
initialUrl: '/
|
|
588
|
+
initialUrl: '/',
|
|
560
589
|
platform: 'web',
|
|
561
590
|
}
|
|
562
591
|
}
|