@shopify/shop-minis-react 0.0.25 → 0.0.27

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 (58) hide show
  1. package/dist/_virtual/index10.js +2 -2
  2. package/dist/_virtual/index4.js +2 -3
  3. package/dist/_virtual/index4.js.map +1 -1
  4. package/dist/_virtual/index7.js +3 -2
  5. package/dist/_virtual/index7.js.map +1 -1
  6. package/dist/_virtual/index9.js +2 -2
  7. package/dist/components/atoms/product-variant-price.js +61 -0
  8. package/dist/components/atoms/product-variant-price.js.map +1 -0
  9. package/dist/components/commerce/product-card.js +120 -153
  10. package/dist/components/commerce/product-card.js.map +1 -1
  11. package/dist/components/commerce/product-link-skeleton.js +30 -0
  12. package/dist/components/commerce/product-link-skeleton.js.map +1 -0
  13. package/dist/components/commerce/product-link.js +73 -77
  14. package/dist/components/commerce/product-link.js.map +1 -1
  15. package/dist/components/commerce/search.js +144 -0
  16. package/dist/components/commerce/search.js.map +1 -0
  17. package/dist/components/content/content-monitor.js +17 -0
  18. package/dist/components/content/content-monitor.js.map +1 -0
  19. package/dist/components/content/content-wrapper.js +17 -0
  20. package/dist/components/content/content-wrapper.js.map +1 -0
  21. package/dist/components/ui/input.js +3 -3
  22. package/dist/components/ui/input.js.map +1 -1
  23. package/dist/hooks/content/useContent.js +24 -0
  24. package/dist/hooks/content/useContent.js.map +1 -0
  25. package/dist/hooks/content/useCreateImageContent.js +21 -18
  26. package/dist/hooks/content/useCreateImageContent.js.map +1 -1
  27. package/dist/hooks/product/useProductSearch.js +24 -23
  28. package/dist/hooks/product/useProductSearch.js.map +1 -1
  29. package/dist/index.js +230 -221
  30. package/dist/index.js.map +1 -1
  31. package/dist/mocks.js +21 -6
  32. package/dist/mocks.js.map +1 -1
  33. package/dist/shop-minis-platform/src/types/content.js +5 -0
  34. package/dist/shop-minis-platform/src/types/content.js.map +1 -0
  35. 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
  36. package/dist/shop-minis-react/node_modules/.pnpm/@xmldom_xmldom@0.8.10/node_modules/@xmldom/xmldom/lib/index.js +1 -1
  37. package/dist/shop-minis-react/node_modules/.pnpm/lucide-react@0.513.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/search.js +16 -0
  38. package/dist/shop-minis-react/node_modules/.pnpm/lucide-react@0.513.0_react@19.1.0/node_modules/lucide-react/dist/esm/icons/search.js.map +1 -0
  39. package/dist/shop-minis-react/node_modules/.pnpm/querystringify@2.2.0/node_modules/querystringify/index.js +1 -1
  40. package/dist/shop-minis-react/node_modules/.pnpm/use-sync-external-store@1.5.0_react@19.1.0/node_modules/use-sync-external-store/shim/index.js +1 -1
  41. package/dist/shop-minis-react.css +1 -1
  42. package/package.json +5 -4
  43. package/src/components/atoms/product-variant-price.tsx +74 -0
  44. package/src/components/commerce/product-card.tsx +7 -56
  45. package/src/components/commerce/product-link-skeleton.tsx +30 -0
  46. package/src/components/commerce/product-link.tsx +8 -7
  47. package/src/components/commerce/search.tsx +264 -0
  48. package/src/components/content/content-monitor.tsx +23 -0
  49. package/src/components/content/content-wrapper.tsx +56 -0
  50. package/src/components/index.ts +3 -0
  51. package/src/components/ui/input.tsx +1 -1
  52. package/src/hooks/content/useContent.ts +50 -0
  53. package/src/hooks/content/useCreateImageContent.ts +20 -5
  54. package/src/hooks/product/useProductSearch.ts +10 -1
  55. package/src/mocks.ts +15 -0
  56. package/src/stories/ProductVariantPrice.stories.tsx +73 -0
  57. package/src/stories/Toaster.stories.tsx +2 -2
  58. package/src/styles/utilities.css +9 -0
@@ -0,0 +1,30 @@
1
+ import * as React from 'react'
2
+
3
+ import {cn} from '../../lib/utils'
4
+ import {Skeleton} from '../ui/skeleton'
5
+
6
+ function ProductLinkSkeleton({
7
+ className,
8
+ ...props
9
+ }: React.ComponentProps<'div'>) {
10
+ return (
11
+ <div
12
+ className={cn(
13
+ 'relative w-full shadow-sm rounded-lg p-4 items-center',
14
+ className
15
+ )}
16
+ {...props}
17
+ >
18
+ <div className="flex flex-row items-center justify-center w-full">
19
+ <Skeleton className="aspect-square w-15 h-15" />
20
+ <div className="flex flex-col justify-center items-start ml-2 w-full pt-2">
21
+ <Skeleton className="mb-3 h-3 w-3/4" />
22
+ <Skeleton className="mb-2 h-3 w-1/4" />
23
+ <Skeleton className="mb-2 h-3 w-1/3" />
24
+ </div>
25
+ </div>
26
+ </div>
27
+ )
28
+ }
29
+
30
+ export {ProductLinkSkeleton}
@@ -222,10 +222,11 @@ function ProductLinkActions({
222
222
 
223
223
  export interface ProductLinkProps {
224
224
  product: Product
225
+ hideFavoriteAction?: boolean
225
226
  }
226
227
 
227
228
  // Composed ProductLink component
228
- function ProductLink({product}: ProductLinkProps) {
229
+ function ProductLink({product, hideFavoriteAction = false}: ProductLinkProps) {
229
230
  const {navigateToProduct} = useShopNavigation()
230
231
  const {saveProduct, unsaveProduct} = useSavedProductsActions()
231
232
 
@@ -247,7 +248,6 @@ function ProductLink({product}: ProductLinkProps) {
247
248
 
248
249
  const averageRating = reviewAnalytics?.averageRating
249
250
  const reviewCount = reviewAnalytics?.reviewCount
250
- const currencyCode = price?.currencyCode
251
251
  const amount = price?.amount
252
252
  ? formatMoney(price?.amount, price?.currencyCode)
253
253
  : undefined
@@ -256,7 +256,6 @@ function ProductLink({product}: ProductLinkProps) {
256
256
  const compareAtPriceAmount = compareAtPrice?.amount
257
257
  ? formatMoney(compareAtPrice?.amount, compareAtPrice?.currencyCode)
258
258
  : undefined
259
- const compareAtPriceCurrencyCode = compareAtPrice?.currencyCode
260
259
  const hasDiscount = compareAtPriceAmount && compareAtPriceAmount !== amount
261
260
 
262
261
  const handlePress = React.useCallback(() => {
@@ -360,10 +359,12 @@ function ProductLink({product}: ProductLinkProps) {
360
359
  </ProductLinkPrice>
361
360
  </ProductLinkInfo>
362
361
 
363
- <ProductLinkActions
364
- filled={isFavoritedLocal}
365
- onPress={handleActionPress}
366
- />
362
+ {hideFavoriteAction ? null : (
363
+ <ProductLinkActions
364
+ filled={isFavoritedLocal}
365
+ onPress={handleActionPress}
366
+ />
367
+ )}
367
368
  </ProductLinkRoot>
368
369
  )
369
370
  }
@@ -0,0 +1,264 @@
1
+ import * as React from 'react'
2
+ import {createContext, useContext, useState, useCallback} from 'react'
3
+
4
+ import {SearchIcon, X} from 'lucide-react'
5
+
6
+ import {useProductSearch} from '../../hooks/product/useProductSearch'
7
+ import {cn} from '../../lib/utils'
8
+ import {type Product} from '../../types'
9
+ import {IconButton} from '../atoms/icon-button'
10
+ import {List} from '../atoms/list'
11
+ import {Input} from '../ui/input'
12
+
13
+ import {ProductLink} from './product-link'
14
+ import {ProductLinkSkeleton} from './product-link-skeleton'
15
+
16
+ const ESTIMATED_PRODUCT_LINK_HEIGHT = 100
17
+
18
+ interface SearchContextValue {
19
+ query: string
20
+ setQuery: (query: string) => void
21
+ products: Product[] | null
22
+ loading: boolean
23
+ error: Error | null
24
+ fetchMore?: () => void
25
+ hasNextPage: boolean
26
+ isTyping: boolean
27
+ }
28
+
29
+ const SearchContext = createContext<SearchContextValue | null>(null)
30
+
31
+ function useSearchContext() {
32
+ const context = useContext(SearchContext)
33
+ if (!context) {
34
+ throw new Error('useSearchContext must be used within a SearchProvider')
35
+ }
36
+ return context
37
+ }
38
+
39
+ export interface SearchProviderProps {
40
+ initialQuery?: string
41
+ children: React.ReactNode
42
+ }
43
+
44
+ function SearchProvider({initialQuery = '', children}: SearchProviderProps) {
45
+ const [query, setQueryState] = useState(initialQuery)
46
+
47
+ const {products, loading, error, fetchMore, hasNextPage, isTyping} =
48
+ useProductSearch({
49
+ query,
50
+ fetchPolicy: 'network-only',
51
+ })
52
+
53
+ const setQuery = useCallback((newQuery: string) => {
54
+ setQueryState(newQuery)
55
+ }, [])
56
+
57
+ const contextValue: SearchContextValue = {
58
+ query,
59
+ setQuery,
60
+ products,
61
+ loading,
62
+ error,
63
+ fetchMore,
64
+ hasNextPage,
65
+ isTyping,
66
+ }
67
+
68
+ return (
69
+ <SearchContext.Provider value={contextValue}>
70
+ {children}
71
+ </SearchContext.Provider>
72
+ )
73
+ }
74
+
75
+ export interface SearchInputProps {
76
+ placeholder?: string
77
+ className?: string
78
+ inputProps?: React.ComponentProps<'input'>
79
+ }
80
+
81
+ function SearchInput({
82
+ placeholder = 'Search products...',
83
+ className,
84
+ inputProps,
85
+ }: SearchInputProps) {
86
+ const {query, setQuery} = useSearchContext()
87
+
88
+ const handleQueryChange = useCallback(
89
+ (event: React.ChangeEvent<HTMLInputElement>) => {
90
+ setQuery(event.target.value)
91
+ inputProps?.onChange?.(event)
92
+ },
93
+ [inputProps, setQuery]
94
+ )
95
+
96
+ return (
97
+ <div className="relative flex flex-1 items-center rounded-full pl-4 pr-2 py-1 bg-gray-100">
98
+ <div className="relative flex items-center">
99
+ <SearchIcon
100
+ size={18}
101
+ className={cn('text-accent-foreground opacity-60')}
102
+ />
103
+ </div>
104
+ <div className="relative flex-1 flex items-center mx-2">
105
+ <Input
106
+ name="search"
107
+ onChange={handleQueryChange}
108
+ placeholder={placeholder}
109
+ type="search"
110
+ role="searchbox"
111
+ autoComplete="off"
112
+ value={query}
113
+ data-testid="search-input"
114
+ {...inputProps}
115
+ className={cn(
116
+ `w-full flex overflow-hidden rounded-radius-28 border-none py-4 px-0 text-text placeholder:text-text placeholder:opacity-60`,
117
+ className
118
+ )}
119
+ />
120
+ </div>
121
+ <div className="relative flex items-center">
122
+ {query === '' ? null : (
123
+ <IconButton
124
+ Icon={X}
125
+ size="sm"
126
+ filled={false}
127
+ iconStyles=""
128
+ onClick={() => setQuery('')}
129
+ buttonStyles="flex items-center rounded-radius-max bg-[var(--grayscale-l20)]"
130
+ />
131
+ )}
132
+ </div>
133
+ </div>
134
+ )
135
+ }
136
+
137
+ export interface SearchResultsListProps {
138
+ renderItem?: (product: Product, index: number) => React.ReactNode
139
+ height?: number
140
+ itemHeight?: number
141
+ initialStateComponent?: React.JSX.Element
142
+ showScrollbar?: boolean
143
+ overscanCount?: number
144
+ }
145
+
146
+ function SearchResultsList({
147
+ height = window.innerHeight,
148
+ renderItem,
149
+ itemHeight = ESTIMATED_PRODUCT_LINK_HEIGHT,
150
+ initialStateComponent,
151
+ showScrollbar,
152
+ overscanCount = 5,
153
+ }: SearchResultsListProps) {
154
+ const {query, products, loading, fetchMore, hasNextPage, isTyping} =
155
+ useSearchContext()
156
+
157
+ const _renderItem = (product: Product, index: number) => {
158
+ if (renderItem) {
159
+ return renderItem(product, index)
160
+ }
161
+
162
+ return (
163
+ <div className="p-2">
164
+ <ProductLink key={product.id} product={product} hideFavoriteAction />
165
+ </div>
166
+ )
167
+ }
168
+
169
+ const shouldShowStartingState = query.trim().length === 0
170
+ const shouldShowLoading =
171
+ (!products || products.length === 0) && (loading || isTyping)
172
+ const shouldShowEmptyState = (!products || products.length === 0) && !loading
173
+
174
+ if (shouldShowStartingState) {
175
+ return (
176
+ initialStateComponent || (
177
+ <div className="flex items-center justify-center h-32 text-gray-500">
178
+ Start typing to search for products
179
+ </div>
180
+ )
181
+ )
182
+ }
183
+
184
+ if (shouldShowLoading) {
185
+ return (
186
+ <div className="flex flex-col px-4 py-4">
187
+ <ProductLinkSkeleton className="mb-4" />
188
+ <ProductLinkSkeleton className="mb-4" />
189
+ <ProductLinkSkeleton className="mb-4" />
190
+ <ProductLinkSkeleton className="mb-4" />
191
+ </div>
192
+ )
193
+ }
194
+
195
+ if (shouldShowEmptyState) {
196
+ return (
197
+ <div className="flex items-center justify-center h-32 text-gray-500">
198
+ {`No products found for "${query}"`}
199
+ </div>
200
+ )
201
+ }
202
+
203
+ return (
204
+ <List
205
+ items={products || []}
206
+ height={height}
207
+ renderItem={_renderItem}
208
+ itemSizeForRow={() => itemHeight}
209
+ fetchMore={hasNextPage ? fetchMore : undefined}
210
+ showScrollbar={showScrollbar}
211
+ overscanCount={overscanCount}
212
+ />
213
+ )
214
+ }
215
+
216
+ interface SearchProviderPropsWithoutChildren
217
+ extends Omit<SearchProviderProps, 'children'> {}
218
+ export interface SearchResultsProps
219
+ extends SearchProviderPropsWithoutChildren,
220
+ SearchInputProps,
221
+ SearchResultsListProps {
222
+ showSearchInput?: boolean
223
+ }
224
+
225
+ function Search({
226
+ initialQuery,
227
+ placeholder,
228
+ inputProps,
229
+ height,
230
+ className,
231
+ renderItem,
232
+ itemHeight,
233
+ }: SearchResultsProps) {
234
+ const _renderItem = (product: Product, index: number) => {
235
+ if (renderItem) {
236
+ return renderItem(product, index)
237
+ }
238
+
239
+ return (
240
+ <div className="p-2">
241
+ <ProductLink key={product.id} product={product} hideFavoriteAction />
242
+ </div>
243
+ )
244
+ }
245
+
246
+ return (
247
+ <SearchProvider initialQuery={initialQuery}>
248
+ <div className={cn('flex flex-col ', className)}>
249
+ <div className="fixed top-0 left-0 right-0 p-4 w-full z-20 bg-background">
250
+ <SearchInput placeholder={placeholder} inputProps={inputProps} />
251
+ </div>
252
+ <div className="h-14" />
253
+ <SearchResultsList
254
+ height={height}
255
+ renderItem={_renderItem}
256
+ itemHeight={itemHeight}
257
+ showScrollbar
258
+ />
259
+ </div>
260
+ </SearchProvider>
261
+ )
262
+ }
263
+
264
+ export {SearchProvider, SearchInput, SearchResultsList, Search}
@@ -0,0 +1,23 @@
1
+ // import {useShopActions} from '../../internal/useShopActions'
2
+ import {Touchable} from '../atoms/touchable'
3
+
4
+ export function ContentMonitor({
5
+ // publicId,
6
+ children,
7
+ }: {
8
+ publicId: string
9
+ children: React.ReactNode
10
+ }) {
11
+ // const {showFeedbackSheet} = useShopActions()
12
+
13
+ return (
14
+ <Touchable
15
+ // TODO: Add long press support to Touchable
16
+ // onLongPress={() => {
17
+ // showFeedbackSheet({publicId})
18
+ // }}
19
+ >
20
+ {children}
21
+ </Touchable>
22
+ )
23
+ }
@@ -0,0 +1,56 @@
1
+ import {useContent} from '../../hooks/content/useContent'
2
+ import {Content} from '../../types'
3
+
4
+ import {ContentMonitor} from './content-monitor'
5
+
6
+ interface BaseContentWrapperProps {
7
+ children: ({
8
+ content,
9
+ loading,
10
+ }: {
11
+ content?: Content
12
+ loading: boolean
13
+ }) => JSX.Element | null
14
+ }
15
+
16
+ interface PublicIdContentWrapperProps extends BaseContentWrapperProps {
17
+ publicId: string
18
+ externalId?: never
19
+ }
20
+
21
+ interface ExternalIdContentWrapperProps extends BaseContentWrapperProps {
22
+ externalId: string
23
+ publicId?: never
24
+ }
25
+
26
+ type ContentWrapperProps =
27
+ | PublicIdContentWrapperProps
28
+ | ExternalIdContentWrapperProps
29
+
30
+ // It's too messy in the docs to show the complete types here so we show a simplified version
31
+ export interface ContentWrapperPropsForDocs extends BaseContentWrapperProps {
32
+ publicId?: string
33
+ externalId?: string
34
+ }
35
+
36
+ export function ContentWrapper({
37
+ publicId,
38
+ externalId,
39
+ children,
40
+ }: ContentWrapperProps) {
41
+ const {content, loading} = useContent({
42
+ identifiers: [{publicId, externalId}],
43
+ })
44
+
45
+ const contentItem = content?.[0]
46
+
47
+ if (loading || !contentItem) {
48
+ return children({loading})
49
+ }
50
+
51
+ return (
52
+ <ContentMonitor publicId={contentItem.publicId}>
53
+ {children({content: contentItem, loading})}
54
+ </ContentMonitor>
55
+ )
56
+ }
@@ -6,6 +6,9 @@ export * from './commerce/merchant-card'
6
6
  export * from './commerce/product-card-skeleton'
7
7
  export * from './commerce/merchant-card-skeleton'
8
8
  export * from './commerce/quantity-selector'
9
+ export * from './commerce/search'
10
+
11
+ export * from './content/content-wrapper'
9
12
 
10
13
  export * from './navigation/transition-container'
11
14
  export * from './navigation/transition-link'
@@ -9,7 +9,7 @@ function Input({className, type, ...props}: React.ComponentProps<'input'>) {
9
9
  data-slot="input"
10
10
  className={cn(
11
11
  'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
12
- 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
12
+ 'focus:outline-none focus:ring-0 focus-visible:ring-0 focus-visible:outline-none',
13
13
  'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
14
14
  className
15
15
  )}
@@ -0,0 +1,50 @@
1
+ import {useMemo} from 'react'
2
+
3
+ import {useShopActions} from '../../internal/useShopActions'
4
+ import {useShopActionsDataFetching} from '../../internal/useShopActionsDataFetching'
5
+ import {
6
+ Content,
7
+ ContentIdentifierInput,
8
+ DataHookOptionsBase,
9
+ DataHookReturnsBase,
10
+ } from '../../types'
11
+
12
+ export interface UseContentParams extends DataHookOptionsBase {
13
+ /**
14
+ * The identifiers of the content to fetch.
15
+ */
16
+ identifiers: ContentIdentifierInput | ContentIdentifierInput[]
17
+ }
18
+
19
+ interface UseContentReturns extends DataHookReturnsBase {
20
+ /**
21
+ * The content returned from the query.
22
+ */
23
+ content: Content[] | null
24
+ }
25
+
26
+ export const useContent = (params: UseContentParams): UseContentReturns => {
27
+ const {getContent} = useShopActions()
28
+ const {identifiers, skip = false, ...restParams} = params
29
+
30
+ const {data, ...rest} = useShopActionsDataFetching(
31
+ getContent,
32
+ {
33
+ identifiers,
34
+ ...restParams,
35
+ },
36
+ {
37
+ skip,
38
+ hook: 'useContent',
39
+ }
40
+ )
41
+
42
+ const content = useMemo(() => {
43
+ return data ?? null
44
+ }, [data])
45
+
46
+ return {
47
+ ...rest,
48
+ content,
49
+ }
50
+ }
@@ -1,7 +1,10 @@
1
- import {useCallback} from 'react'
1
+ import {useCallback, useState} from 'react'
2
2
 
3
- import {ContentVisibility} from '@shopify/shop-minis-platform'
4
- import {CreateContentResponse} from '@shopify/shop-minis-platform/actions'
3
+ import {
4
+ ContentVisibility,
5
+ Content,
6
+ ContentCreateUserErrors,
7
+ } from '@shopify/shop-minis-platform'
5
8
 
6
9
  import {useHandleAction} from '../../internal/useHandleAction'
7
10
  import {useShopActions} from '../../internal/useShopActions'
@@ -20,15 +23,22 @@ interface UseCreateImageContentReturns {
20
23
  */
21
24
  createImageContent: (
22
25
  params: CreateImageContentParams
23
- ) => Promise<CreateContentResponse>
26
+ ) => Promise<{data: Content; userErrors?: ContentCreateUserErrors[]}>
27
+ /**
28
+ * Whether the content is being created.
29
+ */
30
+ loading: boolean
24
31
  }
25
32
 
26
33
  export const useCreateImageContent = (): UseCreateImageContentReturns => {
27
34
  const {createContent} = useShopActions()
28
35
  const {uploadImage} = useImageUpload()
36
+ const [loading, setLoading] = useState(false)
29
37
 
30
38
  const createImageContent = useCallback(
31
39
  async (params: CreateImageContentParams) => {
40
+ setLoading(true)
41
+
32
42
  const {image, contentTitle, visibility} = params
33
43
 
34
44
  if (!image.type) {
@@ -50,16 +60,21 @@ export const useCreateImageContent = (): UseCreateImageContentReturns => {
50
60
  throw new Error('Image upload failed')
51
61
  }
52
62
 
53
- return createContent({
63
+ const createContentResult = await createContent({
54
64
  title: contentTitle,
55
65
  imageUrl: uploadImageUrl,
56
66
  visibility,
57
67
  })
68
+
69
+ setLoading(false)
70
+
71
+ return createContentResult
58
72
  },
59
73
  [createContent, uploadImage]
60
74
  )
61
75
 
62
76
  return {
63
77
  createImageContent: useHandleAction(createImageContent),
78
+ loading,
64
79
  }
65
80
  }
@@ -36,6 +36,10 @@ interface UseProductSearchReturns extends PaginatedDataHookReturnsBase {
36
36
  * The products returned from the query.
37
37
  */
38
38
  products: Product[] | null
39
+ /**
40
+ * Whether the user is typing.
41
+ */
42
+ isTyping: boolean
39
43
  }
40
44
 
41
45
  export const useProductSearch = (
@@ -85,11 +89,16 @@ export const useProductSearch = (
85
89
  )
86
90
 
87
91
  const products = useMemo(() => {
92
+ if (debouncedQuery.trim().length === 0) {
93
+ return null
94
+ }
95
+
88
96
  return data ?? null
89
- }, [data])
97
+ }, [data, debouncedQuery])
90
98
 
91
99
  return {
92
100
  ...rest,
93
101
  products,
102
+ isTyping: debouncedQuery !== query,
94
103
  }
95
104
  }
package/src/mocks.ts CHANGED
@@ -422,6 +422,21 @@ function makeMockActions(): ShopActions {
422
422
  products: null,
423
423
  },
424
424
  },
425
+ getContent: {
426
+ data: [
427
+ {
428
+ publicId: 'content-123',
429
+ image: {
430
+ id: 'img-123',
431
+ url: 'https://example.com/content-image.jpg',
432
+ width: 800,
433
+ height: 600,
434
+ },
435
+ title: 'Mock Content',
436
+ visibility: ['DISCOVERABLE'],
437
+ },
438
+ ],
439
+ },
425
440
  } as const
426
441
 
427
442
  const mock: Partial<ShopActions> = {}
@@ -0,0 +1,73 @@
1
+ import {ProductVariantPrice} from '../components/atoms/product-variant-price'
2
+
3
+ import type {Meta, StoryObj} from '@storybook/react-vite'
4
+
5
+ type ProductVariantPriceProps = React.ComponentProps<typeof ProductVariantPrice>
6
+
7
+ const meta = {
8
+ title: 'Atoms/ProductVariantPrice',
9
+ component: ProductVariantPrice,
10
+ parameters: {
11
+ layout: 'padded',
12
+ },
13
+ argTypes: {
14
+ amount: {
15
+ control: 'text',
16
+ },
17
+ currencyCode: {
18
+ control: 'select',
19
+ options: ['USD', 'CAD', 'EUR', 'GBP', 'JPY'],
20
+ },
21
+ compareAtPriceAmount: {
22
+ control: 'text',
23
+ },
24
+ compareAtPriceCurrencyCode: {
25
+ control: 'select',
26
+ options: ['USD', 'CAD', 'EUR', 'GBP', 'JPY'],
27
+ },
28
+ className: {
29
+ control: 'text',
30
+ },
31
+ currentPriceClassName: {
32
+ control: 'text',
33
+ },
34
+ originalPriceClassName: {
35
+ control: 'text',
36
+ },
37
+ containerClassName: {
38
+ control: 'text',
39
+ },
40
+ },
41
+ tags: ['autodocs'],
42
+ } satisfies Meta<ProductVariantPriceProps>
43
+
44
+ export default meta
45
+ type Story = StoryObj<typeof meta>
46
+
47
+ export const Default: Story = {
48
+ args: {
49
+ amount: '29.99',
50
+ currencyCode: 'USD',
51
+ },
52
+ }
53
+
54
+ export const WithDiscount: Story = {
55
+ name: 'With Discount',
56
+ args: {
57
+ amount: '24.99',
58
+ currencyCode: 'USD',
59
+ compareAtPriceAmount: '39.99',
60
+ },
61
+ }
62
+
63
+ export const CustomStyling: Story = {
64
+ name: 'Custom Styling',
65
+ args: {
66
+ amount: '89.99',
67
+ currencyCode: 'USD',
68
+ compareAtPriceAmount: '119.99',
69
+ currentPriceClassName: 'text-2xl font-bold text-green-600',
70
+ originalPriceClassName: 'text-lg text-red-500 line-through',
71
+ containerClassName: 'gap-3 p-4 bg-gray-50 rounded-lg',
72
+ },
73
+ }
@@ -26,7 +26,7 @@ type Story = StoryObj<typeof meta>
26
26
 
27
27
  export const SuccessToast: Story = {
28
28
  decorators: [
29
- Story => (
29
+ () => (
30
30
  <Button onClick={() => toast.success('Success toast!')}>
31
31
  Show success Toast
32
32
  </Button>
@@ -37,7 +37,7 @@ export const SuccessToast: Story = {
37
37
 
38
38
  export const ErrorToast: Story = {
39
39
  decorators: [
40
- Story => (
40
+ () => (
41
41
  <Button onClick={() => toast.error('Error toast!')}>
42
42
  Show error Toast
43
43
  </Button>