@shopify/shop-minis-react 0.0.0-snapshot.20251215171026 → 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.
Files changed (71) hide show
  1. package/dist/components/atoms/alert-dialog.js.map +1 -1
  2. package/dist/components/atoms/button.js.map +1 -1
  3. package/dist/components/atoms/icon-button.js.map +1 -1
  4. package/dist/components/atoms/image.js +65 -51
  5. package/dist/components/atoms/image.js.map +1 -1
  6. package/dist/components/atoms/list.js.map +1 -1
  7. package/dist/components/atoms/product-variant-price.js +1 -1
  8. package/dist/components/atoms/product-variant-price.js.map +1 -1
  9. package/dist/components/atoms/text-input.js.map +1 -1
  10. package/dist/components/atoms/touchable.js.map +1 -1
  11. package/dist/components/atoms/video-player.js +1 -1
  12. package/dist/components/atoms/video-player.js.map +1 -1
  13. package/dist/components/commerce/add-to-cart.js.map +1 -1
  14. package/dist/components/commerce/buy-now.js.map +1 -1
  15. package/dist/components/commerce/favorite-button.js +1 -4
  16. package/dist/components/commerce/favorite-button.js.map +1 -1
  17. package/dist/components/commerce/merchant-card.js.map +1 -1
  18. package/dist/components/commerce/product-card.js +1 -1
  19. package/dist/components/commerce/product-card.js.map +1 -1
  20. package/dist/components/commerce/product-link.js +1 -1
  21. package/dist/components/commerce/product-link.js.map +1 -1
  22. package/dist/components/commerce/quantity-selector.js.map +1 -1
  23. package/dist/components/content/image-content-wrapper.js.map +1 -1
  24. package/dist/components/navigation/minis-router.js.map +1 -1
  25. package/dist/components/navigation/transition-link.js.map +1 -1
  26. package/dist/components/ui/alert.js.map +1 -1
  27. package/dist/components/ui/badge.js.map +1 -1
  28. package/dist/components/ui/input.js.map +1 -1
  29. package/dist/hooks/storage/useImageUpload.js +32 -24
  30. package/dist/hooks/storage/useImageUpload.js.map +1 -1
  31. package/dist/index.js +92 -88
  32. package/dist/index.js.map +1 -1
  33. package/dist/mocks.js +70 -50
  34. package/dist/mocks.js.map +1 -1
  35. package/dist/utils/formatMoney.js.map +1 -0
  36. package/dist/utils/image.js +44 -24
  37. package/dist/utils/image.js.map +1 -1
  38. package/eslint/rules/validate-manifest.cjs +91 -41
  39. package/package.json +2 -2
  40. package/src/components/atoms/alert-dialog.tsx +3 -3
  41. package/src/components/atoms/button.tsx +22 -0
  42. package/src/components/atoms/icon-button.tsx +16 -8
  43. package/src/components/atoms/image.tsx +41 -8
  44. package/src/components/atoms/list.tsx +25 -2
  45. package/src/components/atoms/product-variant-price.test.tsx +1 -1
  46. package/src/components/atoms/product-variant-price.tsx +1 -1
  47. package/src/components/atoms/text-input.tsx +3 -1
  48. package/src/components/atoms/touchable.tsx +15 -4
  49. package/src/components/atoms/video-player.tsx +16 -6
  50. package/src/components/commerce/add-to-cart.tsx +7 -11
  51. package/src/components/commerce/buy-now.tsx +7 -10
  52. package/src/components/commerce/favorite-button.tsx +6 -5
  53. package/src/components/commerce/merchant-card.tsx +4 -0
  54. package/src/components/commerce/product-card.test.tsx +1 -1
  55. package/src/components/commerce/product-card.tsx +1 -1
  56. package/src/components/commerce/product-link.test.tsx +1 -1
  57. package/src/components/commerce/product-link.tsx +16 -1
  58. package/src/components/commerce/quantity-selector.tsx +6 -1
  59. package/src/components/content/image-content-wrapper.tsx +16 -1
  60. package/src/components/navigation/minis-router.tsx +2 -2
  61. package/src/components/navigation/transition-link.tsx +11 -1
  62. package/src/components/ui/alert.tsx +7 -0
  63. package/src/components/ui/badge.tsx +9 -0
  64. package/src/components/ui/input.tsx +15 -0
  65. package/src/hooks/storage/useImageUpload.ts +13 -0
  66. package/src/mocks.ts +48 -19
  67. package/src/utils/image.ts +38 -0
  68. package/src/utils/index.ts +1 -0
  69. package/dist/lib/formatMoney.js.map +0 -1
  70. /package/dist/{lib → utils}/formatMoney.js +0 -0
  71. /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 {getThumbhashDataURL, getResizedImageUrl} from '../../utils'
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 thumbhashDataURL = useMemo(
56
- () => getThumbhashDataURL(thumbhash ?? undefined),
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 Props<T = any>
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
- }: Props<T>) {
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('../../lib/formatMoney', () => ({
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'
@@ -1,5 +1,5 @@
1
- import {formatMoney} from '../../lib/formatMoney'
2
1
  import {cn} from '../../lib/utils'
2
+ import {formatMoney} from '../../utils/formatMoney'
3
3
 
4
4
  export interface ProductVariantPriceProps {
5
5
  amount: number | string
@@ -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
- function TextInput({...props}: React.ComponentProps<'input'>) {
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
- }: HTMLMotionProps<'div'> & {
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 = 'false',
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 function FavoriteButton({
6
- onClick,
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('../../lib/formatMoney', () => ({
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('../../lib/formatMoney', () => ({
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
- type ImageContentWrapperProps = (
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
- type ShopMinisRouterProps = BrowserRouterProps & {
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
- }: ShopMinisRouterProps) {
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
- type TransitionLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
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
- title,
18
- price: {amount: price, currencyCode: 'USD'},
19
- ...(compareAtPrice && {
20
- compareAtPrice: {amount: compareAtPrice, currencyCode: 'USD'},
21
- }),
22
- reviewAnalytics: {averageRating: 4.5, reviewCount: 10},
23
- shop: createShop('shop1', 'Mock Shop'),
24
- defaultVariantId: `variant-${id}`,
25
- isFavorited: false,
26
- featuredImage: {
27
- url: `https://cdn.shopify.com/static/sample-images/teapot.jpg`,
28
- altText: title,
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({ok: true as const, data: result})
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: {url: 'https://example.com/image.jpg'},
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: '/mock-initial-url',
588
+ initialUrl: '/',
560
589
  platform: 'web',
561
590
  }
562
591
  }