@tallyui/components 1.0.0
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/index.d.ts +364 -0
- package/dist/index.js +451 -0
- package/package.json +35 -0
- package/src/__tests__/cart-line.test.tsx +53 -0
- package/src/__tests__/cart-panel.test.tsx +55 -0
- package/src/__tests__/cart-total.test.tsx +60 -0
- package/src/__tests__/customer-card.test.tsx +47 -0
- package/src/__tests__/customer-select.test.tsx +33 -0
- package/src/__tests__/filter-chip.test.tsx +40 -0
- package/src/__tests__/helpers.ts +111 -0
- package/src/__tests__/order-summary-line.test.tsx +18 -0
- package/src/__tests__/order-summary.test.tsx +32 -0
- package/src/__tests__/payment-method-card.test.tsx +24 -0
- package/src/__tests__/payment-selector.test.tsx +23 -0
- package/src/__tests__/product-card.test.tsx +31 -0
- package/src/__tests__/product-grid.test.tsx +37 -0
- package/src/__tests__/product-image.test.tsx +54 -0
- package/src/__tests__/product-price.test.tsx +68 -0
- package/src/__tests__/product-sku.test.tsx +47 -0
- package/src/__tests__/product-stock-badge.test.tsx +58 -0
- package/src/__tests__/product-title.test.tsx +38 -0
- package/src/__tests__/search-input.test.tsx +22 -0
- package/src/cart/cart-line.tsx +49 -0
- package/src/cart/cart-panel.tsx +65 -0
- package/src/cart/cart-total.tsx +64 -0
- package/src/cart/index.ts +3 -0
- package/src/checkout/index.ts +4 -0
- package/src/checkout/order-summary-line.tsx +23 -0
- package/src/checkout/order-summary.tsx +77 -0
- package/src/checkout/payment-method-card.tsx +36 -0
- package/src/checkout/payment-selector.tsx +55 -0
- package/src/customer/customer-card.tsx +26 -0
- package/src/customer/customer-select.tsx +72 -0
- package/src/customer/index.ts +2 -0
- package/src/index.ts +61 -0
- package/src/input/filter-chip.tsx +58 -0
- package/src/input/index.ts +2 -0
- package/src/input/search-input.tsx +26 -0
- package/src/product/index.ts +7 -0
- package/src/product/product-card.tsx +28 -0
- package/src/product/product-grid.tsx +66 -0
- package/src/product/product-image.tsx +41 -0
- package/src/product/product-price.tsx +53 -0
- package/src/product/product-sku.tsx +33 -0
- package/src/product/product-stock-badge.tsx +73 -0
- package/src/product/product-title.tsx +30 -0
- package/src/uniwind-env.d.ts +31 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Pressable, View, type ViewProps } from 'react-native';
|
|
2
|
+
import { cn } from '@tallyui/theme';
|
|
3
|
+
import { ProductImage } from './product-image';
|
|
4
|
+
import { ProductTitle } from './product-title';
|
|
5
|
+
import { ProductPrice } from './product-price';
|
|
6
|
+
|
|
7
|
+
export interface ProductCardProps extends Omit<ViewProps, 'children'> {
|
|
8
|
+
doc: any;
|
|
9
|
+
onPress?: () => void;
|
|
10
|
+
imageSize?: number;
|
|
11
|
+
currencySymbol?: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ProductCard({ doc, onPress, imageSize = 80, currencySymbol, className, ...viewProps }: ProductCardProps) {
|
|
16
|
+
const content = (
|
|
17
|
+
<View className={cn('items-center gap-2 rounded-lg bg-surface p-3', className)} {...viewProps}>
|
|
18
|
+
<ProductImage doc={doc} size={imageSize} className="rounded-md" />
|
|
19
|
+
<ProductTitle doc={doc} className="text-sm" numberOfLines={2} />
|
|
20
|
+
<ProductPrice doc={doc} currencySymbol={currencySymbol} />
|
|
21
|
+
</View>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (onPress) {
|
|
25
|
+
return <Pressable onPress={onPress}>{content}</Pressable>;
|
|
26
|
+
}
|
|
27
|
+
return content;
|
|
28
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { ScrollView, View, type ViewProps } from 'react-native';
|
|
3
|
+
import { cn } from '@tallyui/theme';
|
|
4
|
+
|
|
5
|
+
export interface ProductGridProps extends Omit<ViewProps, 'children'> {
|
|
6
|
+
/** Array of product documents to render */
|
|
7
|
+
items: any[];
|
|
8
|
+
/** Render function for each product */
|
|
9
|
+
renderItem: (item: any, index: number) => ReactNode;
|
|
10
|
+
/** Number of columns in the grid (defaults to 2) */
|
|
11
|
+
numColumns?: number;
|
|
12
|
+
/** Search input slot rendered above the grid */
|
|
13
|
+
searchSlot?: ReactNode;
|
|
14
|
+
/** Filter slot rendered between search and grid */
|
|
15
|
+
filterSlot?: ReactNode;
|
|
16
|
+
/** Content shown when items is empty */
|
|
17
|
+
emptyState?: ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A scrollable product grid with optional search and filter slots.
|
|
23
|
+
*
|
|
24
|
+
* Uses ScrollView + flexWrap for cross-platform grid layout rather than
|
|
25
|
+
* FlatList numColumns, which keeps things predictable in both native and web.
|
|
26
|
+
*
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <ProductGrid
|
|
29
|
+
* items={products}
|
|
30
|
+
* renderItem={(doc) => <ProductCard doc={doc} />}
|
|
31
|
+
* searchSlot={<SearchInput value={q} onChangeText={setQ} />}
|
|
32
|
+
* numColumns={3}
|
|
33
|
+
* />
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function ProductGrid({
|
|
37
|
+
items,
|
|
38
|
+
renderItem,
|
|
39
|
+
numColumns = 2,
|
|
40
|
+
searchSlot,
|
|
41
|
+
filterSlot,
|
|
42
|
+
emptyState,
|
|
43
|
+
className,
|
|
44
|
+
...viewProps
|
|
45
|
+
}: ProductGridProps) {
|
|
46
|
+
const hasItems = items.length > 0;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<View className={cn('flex-1', className)} {...viewProps}>
|
|
50
|
+
{searchSlot && <View className="px-3 py-2">{searchSlot}</View>}
|
|
51
|
+
{filterSlot && <View className="px-3 pb-2">{filterSlot}</View>}
|
|
52
|
+
|
|
53
|
+
{hasItems ? (
|
|
54
|
+
<ScrollView contentContainerClassName="flex-row flex-wrap p-1">
|
|
55
|
+
{items.map((item, index) => (
|
|
56
|
+
<View key={item.id ?? index} style={{ width: `${100 / numColumns}%` }} className="p-1">
|
|
57
|
+
{renderItem(item, index)}
|
|
58
|
+
</View>
|
|
59
|
+
))}
|
|
60
|
+
</ScrollView>
|
|
61
|
+
) : (
|
|
62
|
+
emptyState ?? null
|
|
63
|
+
)}
|
|
64
|
+
</View>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Image, type ImageProps, type ImageStyle, type StyleProp } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { useProductTraits } from '@tallyui/core';
|
|
4
|
+
import { cn } from '@tallyui/theme';
|
|
5
|
+
|
|
6
|
+
export interface ProductImageProps extends Omit<ImageProps, 'source'> {
|
|
7
|
+
/** The raw RxDB product document */
|
|
8
|
+
doc: any;
|
|
9
|
+
/** Image dimensions */
|
|
10
|
+
size?: number;
|
|
11
|
+
/** Style overrides (for dynamic sizing) */
|
|
12
|
+
style?: StyleProp<ImageStyle>;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Displays a product's primary image.
|
|
18
|
+
*
|
|
19
|
+
* Falls back to null if no image is available.
|
|
20
|
+
*
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <ProductImage doc={productDocument} size={80} className="rounded-lg" />
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function ProductImage({ doc, size = 60, style, className, ...imageProps }: ProductImageProps) {
|
|
26
|
+
const { getImageUrl } = useProductTraits();
|
|
27
|
+
const imageUrl = getImageUrl(doc);
|
|
28
|
+
|
|
29
|
+
if (!imageUrl) {
|
|
30
|
+
return null; // TODO: placeholder image
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Image
|
|
35
|
+
source={{ uri: imageUrl }}
|
|
36
|
+
className={cn('rounded', className)}
|
|
37
|
+
style={[{ width: size, height: size }, style]}
|
|
38
|
+
{...imageProps}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Text, type TextProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { useProductTraits } from '@tallyui/core';
|
|
4
|
+
import { cn } from '@tallyui/theme';
|
|
5
|
+
|
|
6
|
+
export interface ProductPriceProps extends Omit<TextProps, 'children'> {
|
|
7
|
+
/** The raw RxDB product document (connector-specific shape) */
|
|
8
|
+
doc: any;
|
|
9
|
+
/** Currency symbol to prepend (defaults to '$') */
|
|
10
|
+
currencySymbol?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Displays a product's price.
|
|
16
|
+
*
|
|
17
|
+
* Works with any connector — extracts price via traits regardless
|
|
18
|
+
* of whether it's a string field (WooCommerce) or derived from
|
|
19
|
+
* variant pricing (Medusa).
|
|
20
|
+
*
|
|
21
|
+
* ```tsx
|
|
22
|
+
* <ProductPrice doc={productDocument} currencySymbol="$" />
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function ProductPrice({ doc, currencySymbol = '$', className, ...textProps }: ProductPriceProps) {
|
|
26
|
+
const { getPrice, isOnSale, getSalePrice, getRegularPrice } = useProductTraits();
|
|
27
|
+
const price = getPrice(doc);
|
|
28
|
+
|
|
29
|
+
if (!price) {
|
|
30
|
+
return (
|
|
31
|
+
<Text className={cn('text-sm text-muted', className)} {...textProps}>
|
|
32
|
+
-
|
|
33
|
+
</Text>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (isOnSale(doc)) {
|
|
38
|
+
const salePrice = getSalePrice(doc);
|
|
39
|
+
const regularPrice = getRegularPrice(doc);
|
|
40
|
+
return (
|
|
41
|
+
<Text className={cn('text-sm font-medium text-sale', className)} {...textProps}>
|
|
42
|
+
{currencySymbol}{salePrice ?? price}
|
|
43
|
+
{regularPrice ? ` (was ${currencySymbol}${regularPrice})` : ''}
|
|
44
|
+
</Text>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Text className={cn('text-sm font-medium text-price', className)} {...textProps}>
|
|
50
|
+
{currencySymbol}{price}
|
|
51
|
+
</Text>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Text, type TextProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { useProductTraits } from '@tallyui/core';
|
|
4
|
+
import { cn } from '@tallyui/theme';
|
|
5
|
+
|
|
6
|
+
export interface ProductSkuProps extends Omit<TextProps, 'children'> {
|
|
7
|
+
/** The raw RxDB product document (connector-specific shape) */
|
|
8
|
+
doc: any;
|
|
9
|
+
/** Text to show when SKU is missing (defaults to '—') */
|
|
10
|
+
fallback?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Displays a product's SKU.
|
|
16
|
+
*
|
|
17
|
+
* Reads the SKU from whatever field the active connector stores it in —
|
|
18
|
+
* `doc.sku` for WooCommerce, `doc.variants[0].sku` for Medusa, etc.
|
|
19
|
+
*
|
|
20
|
+
* ```tsx
|
|
21
|
+
* <ProductSku doc={productDocument} />
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function ProductSku({ doc, fallback = '—', className, ...textProps }: ProductSkuProps) {
|
|
25
|
+
const { getSku } = useProductTraits();
|
|
26
|
+
const sku = getSku(doc);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Text className={cn('text-xs text-muted', className)} {...textProps}>
|
|
30
|
+
{sku ?? fallback}
|
|
31
|
+
</Text>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Text, View, type ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { useProductTraits } from '@tallyui/core';
|
|
4
|
+
import { cn } from '@tallyui/theme';
|
|
5
|
+
|
|
6
|
+
export interface ProductStockBadgeProps extends Omit<ViewProps, 'children'> {
|
|
7
|
+
/** The raw RxDB product document (connector-specific shape) */
|
|
8
|
+
doc: any;
|
|
9
|
+
/** Whether to show the quantity alongside the status */
|
|
10
|
+
showQuantity?: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const statusLabels: Record<string, string> = {
|
|
15
|
+
instock: 'In Stock',
|
|
16
|
+
outofstock: 'Out of Stock',
|
|
17
|
+
onbackorder: 'On Backorder',
|
|
18
|
+
unknown: 'Unknown',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const STATUS_STYLES: Record<string, { badge: string; dot: string; text: string }> = {
|
|
22
|
+
instock: {
|
|
23
|
+
badge: 'bg-success/15',
|
|
24
|
+
dot: 'bg-success',
|
|
25
|
+
text: 'text-success',
|
|
26
|
+
},
|
|
27
|
+
outofstock: {
|
|
28
|
+
badge: 'bg-danger/15',
|
|
29
|
+
dot: 'bg-danger',
|
|
30
|
+
text: 'text-danger',
|
|
31
|
+
},
|
|
32
|
+
onbackorder: {
|
|
33
|
+
badge: 'bg-warning/15',
|
|
34
|
+
dot: 'bg-warning',
|
|
35
|
+
text: 'text-warning',
|
|
36
|
+
},
|
|
37
|
+
unknown: {
|
|
38
|
+
badge: 'bg-muted/15',
|
|
39
|
+
dot: 'bg-muted',
|
|
40
|
+
text: 'text-muted',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Displays a visual stock status badge for a product.
|
|
46
|
+
*
|
|
47
|
+
* Shows a colored label (In Stock / Out of Stock / On Backorder)
|
|
48
|
+
* with an optional quantity count.
|
|
49
|
+
*
|
|
50
|
+
* ```tsx
|
|
51
|
+
* <ProductStockBadge doc={productDocument} showQuantity />
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function ProductStockBadge({ doc, showQuantity = false, className, ...viewProps }: ProductStockBadgeProps) {
|
|
55
|
+
const { getStockStatus, getStockQuantity } = useProductTraits();
|
|
56
|
+
const status = getStockStatus(doc);
|
|
57
|
+
const quantity = getStockQuantity(doc);
|
|
58
|
+
const label = statusLabels[status] ?? statusLabels.unknown;
|
|
59
|
+
const styles = STATUS_STYLES[status] ?? STATUS_STYLES.unknown;
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<View
|
|
63
|
+
className={cn('flex-row items-center self-start gap-1.5 rounded-xl px-2 py-1', styles.badge, className)}
|
|
64
|
+
{...viewProps}
|
|
65
|
+
>
|
|
66
|
+
<View className={cn('h-1.5 w-1.5 rounded-full', styles.dot)} />
|
|
67
|
+
<Text className={cn('text-xs font-semibold', styles.text)}>
|
|
68
|
+
{label}
|
|
69
|
+
{showQuantity && quantity != null ? ` (${quantity})` : ''}
|
|
70
|
+
</Text>
|
|
71
|
+
</View>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Text, type TextProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { useProductTraits } from '@tallyui/core';
|
|
4
|
+
import { cn } from '@tallyui/theme';
|
|
5
|
+
|
|
6
|
+
export interface ProductTitleProps extends Omit<TextProps, 'children'> {
|
|
7
|
+
/** The raw RxDB product document (connector-specific shape) */
|
|
8
|
+
doc: any;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Displays a product's name.
|
|
14
|
+
*
|
|
15
|
+
* Works with any connector — it uses the active connector's product traits
|
|
16
|
+
* to extract the name from the document, regardless of whether the underlying
|
|
17
|
+
* field is called `name` (WooCommerce) or `title` (Medusa).
|
|
18
|
+
*
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <ProductTitle doc={productDocument} className="text-lg font-bold" />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function ProductTitle({ doc, className, ...textProps }: ProductTitleProps) {
|
|
24
|
+
const { getName } = useProductTraits();
|
|
25
|
+
return (
|
|
26
|
+
<Text className={cn('text-base font-semibold text-foreground', className)} {...textProps}>
|
|
27
|
+
{getName(doc)}
|
|
28
|
+
</Text>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type augmentations for React Native components to support className prop.
|
|
3
|
+
* Uniwind provides these at runtime — this file ensures TypeScript accepts them.
|
|
4
|
+
*/
|
|
5
|
+
export {};
|
|
6
|
+
|
|
7
|
+
declare module 'react-native' {
|
|
8
|
+
interface ViewProps {
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface TextProps {
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ImagePropsBase {
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface PressableProps {
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface FlatListProps<ItemT> {
|
|
25
|
+
contentContainerClassName?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ScrollViewProps {
|
|
29
|
+
contentContainerClassName?: string;
|
|
30
|
+
}
|
|
31
|
+
}
|