@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.
Files changed (47) hide show
  1. package/dist/index.d.ts +364 -0
  2. package/dist/index.js +451 -0
  3. package/package.json +35 -0
  4. package/src/__tests__/cart-line.test.tsx +53 -0
  5. package/src/__tests__/cart-panel.test.tsx +55 -0
  6. package/src/__tests__/cart-total.test.tsx +60 -0
  7. package/src/__tests__/customer-card.test.tsx +47 -0
  8. package/src/__tests__/customer-select.test.tsx +33 -0
  9. package/src/__tests__/filter-chip.test.tsx +40 -0
  10. package/src/__tests__/helpers.ts +111 -0
  11. package/src/__tests__/order-summary-line.test.tsx +18 -0
  12. package/src/__tests__/order-summary.test.tsx +32 -0
  13. package/src/__tests__/payment-method-card.test.tsx +24 -0
  14. package/src/__tests__/payment-selector.test.tsx +23 -0
  15. package/src/__tests__/product-card.test.tsx +31 -0
  16. package/src/__tests__/product-grid.test.tsx +37 -0
  17. package/src/__tests__/product-image.test.tsx +54 -0
  18. package/src/__tests__/product-price.test.tsx +68 -0
  19. package/src/__tests__/product-sku.test.tsx +47 -0
  20. package/src/__tests__/product-stock-badge.test.tsx +58 -0
  21. package/src/__tests__/product-title.test.tsx +38 -0
  22. package/src/__tests__/search-input.test.tsx +22 -0
  23. package/src/cart/cart-line.tsx +49 -0
  24. package/src/cart/cart-panel.tsx +65 -0
  25. package/src/cart/cart-total.tsx +64 -0
  26. package/src/cart/index.ts +3 -0
  27. package/src/checkout/index.ts +4 -0
  28. package/src/checkout/order-summary-line.tsx +23 -0
  29. package/src/checkout/order-summary.tsx +77 -0
  30. package/src/checkout/payment-method-card.tsx +36 -0
  31. package/src/checkout/payment-selector.tsx +55 -0
  32. package/src/customer/customer-card.tsx +26 -0
  33. package/src/customer/customer-select.tsx +72 -0
  34. package/src/customer/index.ts +2 -0
  35. package/src/index.ts +61 -0
  36. package/src/input/filter-chip.tsx +58 -0
  37. package/src/input/index.ts +2 -0
  38. package/src/input/search-input.tsx +26 -0
  39. package/src/product/index.ts +7 -0
  40. package/src/product/product-card.tsx +28 -0
  41. package/src/product/product-grid.tsx +66 -0
  42. package/src/product/product-image.tsx +41 -0
  43. package/src/product/product-price.tsx +53 -0
  44. package/src/product/product-sku.tsx +33 -0
  45. package/src/product/product-stock-badge.tsx +73 -0
  46. package/src/product/product-title.tsx +30 -0
  47. 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
+ }