@shopify/shop-minis-react 0.0.32 → 0.0.34

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 (50) hide show
  1. package/dist/_virtual/index2.js +4 -4
  2. package/dist/_virtual/index3.js +4 -4
  3. package/dist/_virtual/index4.js +2 -2
  4. package/dist/_virtual/index5.js +3 -2
  5. package/dist/_virtual/index5.js.map +1 -1
  6. package/dist/_virtual/index6.js +2 -2
  7. package/dist/_virtual/index7.js +2 -3
  8. package/dist/_virtual/index7.js.map +1 -1
  9. package/dist/_virtual/index8.js +2 -2
  10. package/dist/_virtual/index9.js +2 -2
  11. package/dist/components/atoms/image.js +52 -0
  12. package/dist/components/atoms/image.js.map +1 -0
  13. package/dist/components/commerce/merchant-card.js +188 -245
  14. package/dist/components/commerce/merchant-card.js.map +1 -1
  15. package/dist/components/commerce/product-card.js +11 -11
  16. package/dist/components/commerce/product-card.js.map +1 -1
  17. package/dist/components/content/image-content-wrapper.js +29 -22
  18. package/dist/components/content/image-content-wrapper.js.map +1 -1
  19. package/dist/hooks/content/useCreateImageContent.js +16 -22
  20. package/dist/hooks/content/useCreateImageContent.js.map +1 -1
  21. package/dist/hooks/storage/useImageUpload.js +36 -37
  22. package/dist/hooks/storage/useImageUpload.js.map +1 -1
  23. package/dist/index.js +252 -246
  24. package/dist/shop-minis-platform/src/types/content.js.map +1 -1
  25. package/dist/shop-minis-react/node_modules/.pnpm/@radix-ui_react-use-is-hydrated@0.1.0_@types_react@19.1.6_react@19.1.0/node_modules/@radix-ui/react-use-is-hydrated/dist/index.js +1 -1
  26. package/dist/shop-minis-react/node_modules/.pnpm/@videojs_xhr@2.7.0/node_modules/@videojs/xhr/lib/index.js +1 -1
  27. package/dist/shop-minis-react/node_modules/.pnpm/@xmldom_xmldom@0.8.10/node_modules/@xmldom/xmldom/lib/index.js +1 -1
  28. package/dist/shop-minis-react/node_modules/.pnpm/color-string@1.9.1/node_modules/color-string/index.js +1 -1
  29. package/dist/shop-minis-react/node_modules/.pnpm/mpd-parser@1.3.1/node_modules/mpd-parser/dist/mpd-parser.es.js +1 -1
  30. package/dist/shop-minis-react/node_modules/.pnpm/querystringify@2.2.0/node_modules/querystringify/index.js +1 -1
  31. package/dist/shop-minis-react/node_modules/.pnpm/video.js@8.23.3/node_modules/video.js/dist/video.es.js +1 -1
  32. package/dist/utils/colors.js +1 -1
  33. package/dist/utils/image.js +45 -9
  34. package/dist/utils/image.js.map +1 -1
  35. package/package.json +2 -2
  36. package/src/components/atoms/{thumbhash-image.tsx → image.tsx} +14 -14
  37. package/src/components/commerce/merchant-card.tsx +224 -225
  38. package/src/components/commerce/product-card.tsx +2 -2
  39. package/src/components/content/image-content-wrapper.tsx +9 -2
  40. package/src/components/index.ts +1 -1
  41. package/src/hooks/content/useCreateImageContent.ts +1 -7
  42. package/src/hooks/storage/useImageUpload.ts +22 -20
  43. package/src/stories/MerchantCard.stories.tsx +0 -3
  44. package/src/utils/image.ts +72 -0
  45. package/src/utils/index.ts +1 -1
  46. package/dist/components/atoms/thumbhash-image.js +0 -54
  47. package/dist/components/atoms/thumbhash-image.js.map +0 -1
  48. package/dist/utils/imageToDataUri.js +0 -10
  49. package/dist/utils/imageToDataUri.js.map +0 -1
  50. package/src/utils/imageToDataUri.ts +0 -8
@@ -1,9 +1,8 @@
1
1
  import * as React from 'react'
2
+ import {createContext, useCallback, useContext, useMemo} from 'react'
2
3
 
3
4
  import {type Shop} from '@shopify/shop-minis-platform'
4
- import {cva, type VariantProps} from 'class-variance-authority'
5
5
  import {Star} from 'lucide-react'
6
- import {Slot as SlotPrimitive} from 'radix-ui'
7
6
 
8
7
  import {useShopNavigation} from '../../hooks/navigation/useShopNavigation'
9
8
  import {cn} from '../../lib/utils'
@@ -15,52 +14,62 @@ import {
15
14
  normalizeRating,
16
15
  } from '../../utils'
17
16
  import {isDarkColor} from '../../utils/colors'
18
- import {ThumbhashImage} from '../atoms/thumbhash-image'
17
+ import {Image} from '../atoms/image'
19
18
  import {Touchable} from '../atoms/touchable'
20
19
 
21
- const merchantCardVariants = cva(
22
- 'relative w-full overflow-hidden rounded-xl bg-white flex flex-col border border-gray-200',
23
- {
24
- variants: {
25
- touchable: {
26
- true: 'cursor-pointer',
27
- false: '',
28
- },
29
- },
30
- defaultVariants: {
31
- touchable: true,
32
- },
33
- }
20
+ interface MerchantCardContextValue {
21
+ // Core data
22
+ shop: Shop
23
+
24
+ // Derived data
25
+ cardTheme: ExtractedBrandTheme
26
+
27
+ // UI configuration
28
+ touchable: boolean
29
+ featuredImagesLimit: number
30
+
31
+ // Actions
32
+ onClick: () => void
33
+ }
34
+
35
+ const MerchantCardContext = createContext<MerchantCardContextValue | undefined>(
36
+ undefined
34
37
  )
35
38
 
36
- export interface MerchantCardRootProps
37
- extends React.ComponentProps<'div'>,
38
- VariantProps<typeof merchantCardVariants> {
39
- touchable?: boolean
40
- asChild?: boolean
41
- onPress?: () => void
39
+ function useMerchantCardContext() {
40
+ const context = useContext(MerchantCardContext)
41
+ if (!context) {
42
+ throw new Error(
43
+ 'useMerchantCardContext must be used within a MerchantCardProvider'
44
+ )
45
+ }
46
+ return context
42
47
  }
43
48
 
44
- function MerchantCardRoot({
49
+ function MerchantCardContainer({
45
50
  className,
46
- touchable = true,
47
- asChild = false,
48
- onPress,
49
51
  ...props
50
- }: MerchantCardRootProps) {
51
- const Comp = asChild ? SlotPrimitive.Slot : 'div'
52
+ }: React.ComponentProps<'div'>) {
53
+ const {touchable, cardTheme, onClick} = useMerchantCardContext()
52
54
 
53
55
  const content = (
54
- <Comp
55
- className={cn(merchantCardVariants({touchable}), className)}
56
+ <div
57
+ style={{
58
+ backgroundColor: cardTheme.backgroundColor,
59
+ }}
60
+ className={cn(
61
+ 'relative w-full overflow-hidden rounded-xl bg-white flex flex-col border border-gray-200 aspect-square',
62
+
63
+ className
64
+ )}
56
65
  {...props}
57
66
  />
58
67
  )
59
68
 
60
- if (touchable && onPress) {
69
+ if (touchable && onClick) {
61
70
  return (
62
71
  <Touchable
63
- onClick={onPress}
72
+ onClick={onClick}
64
73
  whileTap={{opacity: 0.7}}
65
74
  transition={{
66
75
  opacity: {type: 'tween', duration: 0.08, ease: 'easeInOut'},
@@ -74,19 +83,6 @@ function MerchantCardRoot({
74
83
  return content
75
84
  }
76
85
 
77
- function MerchantCardImageContainer({
78
- className,
79
- ...props
80
- }: React.ComponentProps<'div'>) {
81
- return (
82
- <div
83
- data-slot="merchant-card-image-container"
84
- className={cn('relative overflow-hidden w-full flex-grow', className)}
85
- {...props}
86
- />
87
- )
88
- }
89
-
90
86
  function MerchantCardImage({
91
87
  className,
92
88
  src,
@@ -104,12 +100,12 @@ function MerchantCardImage({
104
100
 
105
101
  if (thumbhash) {
106
102
  return (
107
- <ThumbhashImage
103
+ <Image
108
104
  data-slot="merchant-card-image"
109
105
  src={src}
110
106
  alt={alt}
111
107
  thumbhash={thumbhash}
112
- className={cn('w-full h-full object-cover', className)}
108
+ className={cn(className)}
113
109
  {...props}
114
110
  />
115
111
  )
@@ -120,23 +116,28 @@ function MerchantCardImage({
120
116
  data-slot="merchant-card-image"
121
117
  src={src}
122
118
  alt={alt}
123
- className={cn('w-full h-full object-cover', className)}
119
+ className={cn('size-full object-cover', className)}
124
120
  {...props}
125
121
  />
126
122
  )
127
123
  }
128
124
 
129
- function MerchantCardLogo({
130
- className,
131
- src,
132
- alt,
133
- shopName,
134
- ...props
135
- }: React.ComponentProps<'div'> & {
136
- src?: string
137
- alt?: string
138
- shopName?: string
139
- }) {
125
+ function MerchantCardLogo({className, ...props}: React.ComponentProps<'div'>) {
126
+ const {shop} = useMerchantCardContext()
127
+ const {name, visualTheme} = shop
128
+
129
+ const logoAverageColor = visualTheme?.brandSettings?.colors?.logoAverage
130
+ const logoDominantColor = visualTheme?.brandSettings?.colors?.logoDominant
131
+ const logoColor = logoAverageColor || logoDominantColor
132
+
133
+ const logoBackgroundClassName = useMemo(
134
+ () => (logoColor && isDarkColor(logoColor) ? 'bg-white' : 'bg-gray-800'),
135
+ [logoColor]
136
+ )
137
+
138
+ const logoUrl = visualTheme?.logoImage?.url
139
+ const altText = `${name} logo`
140
+
140
141
  return (
141
142
  <div
142
143
  data-slot="merchant-card-logo"
@@ -144,16 +145,21 @@ function MerchantCardLogo({
144
145
  'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-10',
145
146
  'w-16 h-16 rounded-xl bg-white border-2 border-white shadow-sm',
146
147
  'flex items-center justify-center overflow-hidden',
148
+ logoBackgroundClassName,
147
149
  className
148
150
  )}
149
151
  {...props}
150
152
  >
151
- {src ? (
152
- <img src={src} alt={alt} className="w-full h-full object-cover" />
153
+ {logoUrl ? (
154
+ <img
155
+ src={logoUrl}
156
+ alt={altText}
157
+ className="w-full h-full object-cover"
158
+ />
153
159
  ) : (
154
160
  <div className="w-full h-full bg-gray-200 flex items-center justify-center">
155
161
  <span className="text-gray-600 font-semibold text-lg">
156
- {shopName?.slice(0, 1)}
162
+ {name?.slice(0, 1)}
157
163
  </span>
158
164
  </div>
159
165
  )}
@@ -162,11 +168,23 @@ function MerchantCardLogo({
162
168
  }
163
169
 
164
170
  function MerchantCardInfo({className, ...props}: React.ComponentProps<'div'>) {
171
+ const {cardTheme} = useMerchantCardContext()
172
+
173
+ const isDarkTheme = useMemo(() => {
174
+ return (
175
+ cardTheme.backgroundColor !== 'white' &&
176
+ isDarkColor(cardTheme.backgroundColor)
177
+ )
178
+ }, [cardTheme.backgroundColor])
179
+
180
+ const textColor = isDarkTheme ? 'text-primary-foreground' : 'text-foreground'
181
+
165
182
  return (
166
183
  <div
167
184
  data-slot="merchant-card-info"
168
185
  className={cn(
169
- 'p-3 space-y-2 flex-shrink-0 flex items-center justify-between',
186
+ 'p-3 flex-shrink-0 flex flex-col min-w-0',
187
+ textColor,
170
188
  className
171
189
  )}
172
190
  {...props}
@@ -179,31 +197,36 @@ function MerchantCardName({
179
197
  children,
180
198
  ...props
181
199
  }: React.ComponentProps<'h3'>) {
200
+ const {shop} = useMerchantCardContext()
201
+ const {name} = shop
202
+ const nameContent = children ?? name
203
+
182
204
  return (
183
205
  <h3
184
206
  data-slot="merchant-card-name"
185
- className={cn(
186
- 'text-sm font-medium text-grayscale-d100',
187
- 'truncate overflow-hidden whitespace-nowrap text-ellipsis',
188
- className
189
- )}
207
+ className={cn('text-sm font-medium truncate', className)}
190
208
  {...props}
191
209
  >
192
- {children}
210
+ {nameContent}
193
211
  </h3>
194
212
  )
195
213
  }
196
214
 
197
215
  function MerchantCardRating({
198
216
  className,
199
- rating,
200
- reviewCount,
217
+
201
218
  ...props
202
219
  }: React.ComponentProps<'div'> & {
203
220
  rating?: number | null
204
221
  reviewCount?: number
205
222
  }) {
206
- if (!rating || !reviewCount) return null
223
+ const {shop} = useMerchantCardContext()
224
+
225
+ const {
226
+ reviewAnalytics: {averageRating, reviewCount},
227
+ } = shop
228
+
229
+ if (!averageRating || !reviewCount) return null
207
230
 
208
231
  return (
209
232
  <div
@@ -213,21 +236,61 @@ function MerchantCardRating({
213
236
  >
214
237
  <Star className="h-3 w-3 fill-current" />
215
238
  <span className="text-xs">
216
- {normalizeRating(rating)} ({formatReviewCount(reviewCount)})
239
+ {normalizeRating(averageRating)} ({formatReviewCount(reviewCount)})
217
240
  </span>
218
241
  </div>
219
242
  )
220
243
  }
221
244
 
222
- function MerchantCardBrandedHeader({
223
- shop,
224
- cardTheme,
225
- logoBackgroundClassName,
226
- }: {
227
- shop: Shop
228
- cardTheme: ExtractedBrandTheme
229
- logoBackgroundClassName?: string
230
- }) {
245
+ function MerchantCardDefaultHeader({withLogo = false}: {withLogo?: boolean}) {
246
+ const {shop, cardTheme, featuredImagesLimit} = useMerchantCardContext()
247
+ const {visualTheme} = shop
248
+
249
+ const featuredImages = useMemo(
250
+ () => getFeaturedImages(visualTheme, featuredImagesLimit),
251
+ [visualTheme, featuredImagesLimit]
252
+ )
253
+
254
+ const numberOfFeaturedImages = featuredImages?.length ?? 0
255
+
256
+ const displayDefaultCover = () => {
257
+ if (numberOfFeaturedImages > 0) {
258
+ const heightClass = numberOfFeaturedImages === 2 ? 'h-full' : 'h-1/2'
259
+ return featuredImages?.map((image, index) => (
260
+ <div className={`z-0 w-1/2 ${heightClass}`} key={image.url || index}>
261
+ <MerchantCardImage
262
+ src={image.url}
263
+ alt={image.altText ?? undefined}
264
+ thumbhash={image.thumbhash ?? undefined}
265
+ className="aspect-square"
266
+ />
267
+ </div>
268
+ ))
269
+ } else if (cardTheme.type === 'coverImage') {
270
+ return (
271
+ <MerchantCardImage
272
+ src={cardTheme.coverImageUrl}
273
+ thumbhash={cardTheme.coverImageThumbhash}
274
+ />
275
+ )
276
+ }
277
+ }
278
+
279
+ return (
280
+ <div className="w-full h-full bg-muted relative flex flex-wrap overflow-hidden">
281
+ {withLogo && (
282
+ <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-10">
283
+ <MerchantCardLogo />
284
+ </div>
285
+ )}
286
+
287
+ {displayDefaultCover()}
288
+ </div>
289
+ )
290
+ }
291
+
292
+ function MerchantCardBrandedHeader({withLogo = false}: {withLogo?: boolean}) {
293
+ const {shop, cardTheme} = useMerchantCardContext()
231
294
  const wordmarkImage = shop.visualTheme?.brandSettings?.headerTheme?.wordmark
232
295
 
233
296
  return (
@@ -238,7 +301,6 @@ function MerchantCardBrandedHeader({
238
301
  src={cardTheme.coverImageUrl}
239
302
  alt={shop.name}
240
303
  thumbhash={cardTheme.coverImageThumbhash ?? undefined}
241
- className="size-full"
242
304
  />
243
305
 
244
306
  <div className="absolute inset-0 z-[1] bg-black/20" />
@@ -252,175 +314,112 @@ function MerchantCardBrandedHeader({
252
314
  </>
253
315
  )}
254
316
 
255
- <div className="absolute inset-0 z-[1] flex items-center justify-center">
256
- {wordmarkImage ? (
257
- <img
258
- src={wordmarkImage.url}
259
- alt={wordmarkImage.altText || shop.name}
260
- className="max-h-16 min-h-10 max-w-28 object-contain"
261
- data-testid="store-data-wordmark"
262
- />
263
- ) : (
264
- <MerchantCardLogo
265
- src={shop.visualTheme?.logoImage?.url}
266
- alt={`${shop.name} logo`}
267
- shopName={shop.name}
268
- className={logoBackgroundClassName}
269
- />
270
- )}
271
- </div>
317
+ {withLogo && (
318
+ <div className="absolute inset-0 z-[1] flex items-center justify-center">
319
+ {wordmarkImage ? (
320
+ <img
321
+ src={wordmarkImage.url}
322
+ alt={wordmarkImage.altText || shop.name}
323
+ className="max-h-16 min-h-10 max-w-28 object-contain"
324
+ data-testid="store-data-wordmark"
325
+ />
326
+ ) : (
327
+ <MerchantCardLogo />
328
+ )}
329
+ </div>
330
+ )}
272
331
  </div>
273
332
  )
274
333
  }
334
+
335
+ interface MerchantCardHeaderProps {
336
+ isDefault?: boolean
337
+ withLogo?: boolean
338
+ }
339
+
340
+ function MerchantCardHeader({
341
+ isDefault,
342
+ withLogo,
343
+ className,
344
+ ...props
345
+ }: React.ComponentProps<'div'> & MerchantCardHeaderProps) {
346
+ const {cardTheme} = useMerchantCardContext()
347
+
348
+ const isBranded =
349
+ cardTheme.type === 'coverImage' || cardTheme.type === 'brandColor'
350
+
351
+ return (
352
+ <div
353
+ className={cn('relative overflow-hidden flex-1 flex-wrap', className)}
354
+ {...props}
355
+ >
356
+ {isBranded && !isDefault ? (
357
+ <MerchantCardBrandedHeader withLogo={withLogo} />
358
+ ) : (
359
+ <MerchantCardDefaultHeader withLogo={withLogo} />
360
+ )}
361
+ </div>
362
+ )
363
+ }
364
+
275
365
  export interface MerchantCardProps {
276
366
  shop: Shop
277
367
  touchable?: boolean
278
- fixedHeight?: boolean
279
368
  featuredImagesLimit?: number
369
+ children?: React.ReactNode
280
370
  }
281
371
 
282
372
  function MerchantCard({
283
373
  shop,
284
374
  touchable = true,
285
- fixedHeight = false,
286
375
  featuredImagesLimit = 4,
376
+ children,
287
377
  }: MerchantCardProps) {
288
378
  const {navigateToShop} = useShopNavigation()
289
379
 
290
- const {
291
- id,
292
- name,
293
- reviewAnalytics: {averageRating, reviewCount},
294
- visualTheme,
295
- } = shop
380
+ const {id, visualTheme} = shop
296
381
 
297
- const handlePress = React.useCallback(() => {
382
+ const handleClick = useCallback(() => {
298
383
  if (!touchable) return
299
384
  navigateToShop({shopId: id})
300
385
  }, [navigateToShop, id, touchable])
301
386
 
302
- const featuredImages = React.useMemo(
303
- () => getFeaturedImages(visualTheme, featuredImagesLimit),
304
- [visualTheme, featuredImagesLimit]
305
- )
306
-
307
- const numberOfFeaturedImages = featuredImages?.length ?? 0
308
-
309
- const logoAverageColor = visualTheme?.brandSettings?.colors?.logoAverage
310
- const logoDominantColor = visualTheme?.brandSettings?.colors?.logoDominant
311
- const logoColor = logoAverageColor || logoDominantColor
312
-
313
- const logoBackgroundClassName = React.useMemo(
314
- () => (logoColor && isDarkColor(logoColor) ? 'bg-white' : 'bg-gray-800'),
315
- [logoColor]
316
- )
317
-
318
- const cardTheme = React.useMemo(
387
+ const cardTheme = useMemo(
319
388
  () => extractBrandTheme(visualTheme?.brandSettings),
320
389
  [visualTheme?.brandSettings]
321
390
  )
322
391
 
323
- const isDarkTheme = React.useMemo(() => {
324
- return (
325
- cardTheme.backgroundColor !== 'white' &&
326
- isDarkColor(cardTheme.backgroundColor)
327
- )
328
- }, [cardTheme.backgroundColor])
329
-
330
- const textColor = isDarkTheme ? 'text-primary-foreground' : 'text-foreground'
331
-
332
- const hasBrandedHeader =
333
- cardTheme.type === 'coverImage' || cardTheme.type === 'brandColor'
392
+ const contextValue = useMemo<MerchantCardContextValue>(
393
+ () => ({
394
+ shop,
395
+ cardTheme,
396
+ touchable,
397
+ featuredImagesLimit,
398
+ onClick: handleClick,
399
+ }),
400
+ [shop, cardTheme, touchable, featuredImagesLimit, handleClick]
401
+ )
334
402
 
335
403
  return (
336
- <div className={cn('flex', fixedHeight ? '' : 'aspect-square')}>
337
- <MerchantCardRoot
338
- touchable={touchable}
339
- onPress={handlePress}
340
- style={{
341
- backgroundColor: cardTheme.backgroundColor,
342
- }}
343
- >
344
- <MerchantCardImageContainer
345
- className={cn(
346
- 'relative overflow-hidden w-full',
347
- fixedHeight ? 'h-[120px]' : 'flex-1'
348
- )}
349
- >
350
- {hasBrandedHeader ? (
351
- <MerchantCardBrandedHeader
352
- shop={shop}
353
- cardTheme={cardTheme}
354
- logoBackgroundClassName={logoBackgroundClassName}
355
- />
356
- ) : (
357
- <>
358
- <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-10">
359
- <MerchantCardLogo
360
- src={visualTheme?.logoImage?.url}
361
- alt={`${name} logo`}
362
- shopName={name}
363
- className={logoBackgroundClassName}
364
- data-testid="merchant-logo"
365
- />
366
- </div>
367
-
368
- {numberOfFeaturedImages > 0 ? (
369
- featuredImages?.map((image, index) => (
370
- <div
371
- className="z-0 flex h-full flex-1"
372
- key={image.url || index}
373
- >
374
- <MerchantCardImage
375
- src={image.url}
376
- alt={image.altText || ''}
377
- thumbhash={image.thumbhash ?? undefined}
378
- />
379
- </div>
380
- ))
381
- ) : (
382
- <div
383
- className="h-20 bg-gray-100"
384
- data-testid="image-fallback"
385
- />
386
- )}
387
- </>
388
- )}
389
- </MerchantCardImageContainer>
390
-
391
- <MerchantCardInfo className="flex items-center justify-between p-3">
392
- <div className="flex flex-col items-start gap-2">
393
- <div className="flex min-w-0 flex-1 flex-col">
394
- <MerchantCardName
395
- className={cn(
396
- 'line-clamp-1 overflow-hidden text-ellipsis',
397
- textColor
398
- )}
399
- >
400
- {name}
401
- </MerchantCardName>
402
-
403
- <MerchantCardRating
404
- rating={averageRating}
405
- reviewCount={reviewCount}
406
- className={textColor}
407
- />
408
- </div>
409
- </div>
410
- </MerchantCardInfo>
411
- </MerchantCardRoot>
412
- </div>
404
+ <MerchantCardContext.Provider value={contextValue}>
405
+ {children ?? (
406
+ <MerchantCardContainer>
407
+ <MerchantCardHeader withLogo />
408
+ <MerchantCardInfo>
409
+ <MerchantCardName />
410
+ <MerchantCardRating />
411
+ </MerchantCardInfo>
412
+ </MerchantCardContainer>
413
+ )}
414
+ </MerchantCardContext.Provider>
413
415
  )
414
416
  }
415
417
 
416
- export const MerchantCardPrimitive = Object.assign(MerchantCardRoot, {
417
- ImageContainer: MerchantCardImageContainer,
418
- Image: MerchantCardImage,
419
- Logo: MerchantCardLogo,
420
- Info: MerchantCardInfo,
421
- Name: MerchantCardName,
422
- Rating: MerchantCardRating,
423
- BrandedHeader: MerchantCardBrandedHeader,
424
- })
425
-
426
- export {MerchantCard}
418
+ export {
419
+ MerchantCard,
420
+ MerchantCardContainer,
421
+ MerchantCardHeader,
422
+ MerchantCardInfo,
423
+ MerchantCardName,
424
+ MerchantCardRating,
425
+ }
@@ -8,8 +8,8 @@ import {useSavedProductsActions} from '../../hooks/user/useSavedProductsActions'
8
8
  import {formatMoney} from '../../lib/formatMoney'
9
9
  import {cn} from '../../lib/utils'
10
10
  import {FavoriteButton} from '../atoms/favorite-button'
11
+ import {Image} from '../atoms/image'
11
12
  import {ProductVariantPrice} from '../atoms/product-variant-price'
12
- import {ThumbhashImage} from '../atoms/thumbhash-image'
13
13
  import {Touchable} from '../atoms/touchable'
14
14
  import {Badge} from '../ui/badge'
15
15
 
@@ -115,7 +115,7 @@ function ProductCardImage({className, ...props}: React.ComponentProps<'img'>) {
115
115
  const renderImageElement = useCallback(
116
116
  (src: string) => {
117
117
  const imageElement = thumbhash ? (
118
- <ThumbhashImage
118
+ <Image
119
119
  data-slot="product-card-image"
120
120
  src={src}
121
121
  alt={alt}
@@ -1,5 +1,5 @@
1
- /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
2
1
  import {ContentWrapper} from '../atoms/content-wrapper'
2
+ import {Image} from '../atoms/image'
3
3
 
4
4
  type ImageContentWrapperProps = (
5
5
  | {publicId: string; externalId?: never}
@@ -26,14 +26,21 @@ export function ImageContentWrapper({
26
26
  {({content, loading}) => {
27
27
  if (loading) return Loader ? <>{Loader}</> : null
28
28
 
29
+ const aspectRatio =
30
+ content?.image?.width && content?.image?.height
31
+ ? content.image.width / content.image.height
32
+ : undefined
33
+
29
34
  return (
30
- <img
35
+ <Image
31
36
  src={content?.image?.url}
37
+ thumbhash={content?.image?.thumbhash}
32
38
  width={width}
33
39
  height={height}
34
40
  alt={content?.title}
35
41
  onLoad={onLoad}
36
42
  className={className}
43
+ aspectRatio={aspectRatio}
37
44
  />
38
45
  )
39
46
  }}
@@ -16,7 +16,7 @@ export * from './navigation/transition-link'
16
16
  export * from './atoms/button'
17
17
  export * from './atoms/favorite-button'
18
18
  export * from './atoms/icon-button'
19
- export * from './atoms/thumbhash-image'
19
+ export * from './atoms/image'
20
20
  export * from './atoms/touchable'
21
21
  export * from './atoms/long-press-detector'
22
22
  export * from './atoms/alert-dialog'
@@ -8,7 +8,6 @@ import {
8
8
 
9
9
  import {useHandleAction} from '../../internal/useHandleAction'
10
10
  import {useShopActions} from '../../internal/useShopActions'
11
- import {fileToDataUri} from '../../utils'
12
11
  import {useImageUpload} from '../storage/useImageUpload'
13
12
 
14
13
  interface CreateImageContentParams {
@@ -48,12 +47,7 @@ export const useCreateImageContent = (): UseCreateImageContentReturns => {
48
47
  throw new Error('Invalid file type: must be an image')
49
48
  }
50
49
 
51
- const [uploadImageResult] = await uploadImage([
52
- {
53
- mimeType: image.type,
54
- uri: await fileToDataUri(image),
55
- },
56
- ])
50
+ const [uploadImageResult] = await uploadImage(image)
57
51
  const uploadImageUrl = uploadImageResult.imageUrl
58
52
 
59
53
  if (!uploadImageUrl) {