@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,22 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import { SearchInput } from '../input/search-input';
|
|
4
|
+
|
|
5
|
+
describe('SearchInput', () => {
|
|
6
|
+
it('renders with placeholder text', () => {
|
|
7
|
+
render(<SearchInput value="" onChangeText={() => {}} placeholder="Search products..." />);
|
|
8
|
+
expect(screen.getByPlaceholderText('Search products...')).toBeDefined();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('displays the current value', () => {
|
|
12
|
+
render(<SearchInput value="espresso" onChangeText={() => {}} />);
|
|
13
|
+
expect(screen.getByDisplayValue('espresso')).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('calls onChangeText when text changes', () => {
|
|
17
|
+
const onChangeText = vi.fn();
|
|
18
|
+
render(<SearchInput value="" onChangeText={onChangeText} />);
|
|
19
|
+
fireEvent.change(screen.getByPlaceholderText('Search...'), { target: { value: 'latte' } });
|
|
20
|
+
expect(onChangeText).toHaveBeenCalledWith('latte');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
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 CartLineItem {
|
|
7
|
+
/** The raw product document (connector-specific shape) */
|
|
8
|
+
doc: any;
|
|
9
|
+
/** Quantity in cart */
|
|
10
|
+
quantity: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CartLineProps extends Omit<ViewProps, 'children'> {
|
|
14
|
+
/** The cart line item */
|
|
15
|
+
item: CartLineItem;
|
|
16
|
+
/** Currency symbol (defaults to '$') */
|
|
17
|
+
currencySymbol?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Displays a single cart line item: product name, quantity, and line total.
|
|
23
|
+
*
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <CartLine item={{ doc: productDocument, quantity: 2 }} />
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function CartLine({ item, currencySymbol = '$', className, ...viewProps }: CartLineProps) {
|
|
29
|
+
const { getName, getPrice } = useProductTraits();
|
|
30
|
+
const name = getName(item.doc);
|
|
31
|
+
const unitPrice = getPrice(item.doc);
|
|
32
|
+
const lineTotal = unitPrice
|
|
33
|
+
? (parseFloat(unitPrice) * item.quantity).toFixed(2)
|
|
34
|
+
: null;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<View className={cn('flex-row items-center gap-3 px-3 py-2.5', className)} {...viewProps}>
|
|
38
|
+
<View className="flex-1 gap-0.5">
|
|
39
|
+
<Text className="text-sm font-medium text-foreground" numberOfLines={1}>{name}</Text>
|
|
40
|
+
<Text className="text-xs text-muted">
|
|
41
|
+
{unitPrice ? `${currencySymbol}${unitPrice} × ${item.quantity}` : `× ${item.quantity}`}
|
|
42
|
+
</Text>
|
|
43
|
+
</View>
|
|
44
|
+
<Text className="text-sm font-semibold text-foreground">
|
|
45
|
+
{lineTotal ? `${currencySymbol}${lineTotal}` : '—'}
|
|
46
|
+
</Text>
|
|
47
|
+
</View>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { ScrollView, View, type ViewProps } from 'react-native';
|
|
3
|
+
import { cn } from '@tallyui/theme';
|
|
4
|
+
import type { CartLineItem } from './cart-line';
|
|
5
|
+
|
|
6
|
+
export interface CartPanelProps extends Omit<ViewProps, 'children'> {
|
|
7
|
+
/** Cart line items to render */
|
|
8
|
+
items: CartLineItem[];
|
|
9
|
+
/** Render function for each cart line */
|
|
10
|
+
renderItem: (item: CartLineItem, index: number) => ReactNode;
|
|
11
|
+
/** Header slot (e.g. customer info) */
|
|
12
|
+
header?: ReactNode;
|
|
13
|
+
/** Footer slot (e.g. totals, checkout button) */
|
|
14
|
+
footer?: ReactNode;
|
|
15
|
+
/** Content shown when items is empty */
|
|
16
|
+
emptyState?: ReactNode;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A scrollable cart panel with header/footer slots.
|
|
22
|
+
*
|
|
23
|
+
* Composes CartLine (or custom renderItem) into a scrollable list
|
|
24
|
+
* with optional header, footer, and empty state.
|
|
25
|
+
*
|
|
26
|
+
* ```tsx
|
|
27
|
+
* <CartPanel
|
|
28
|
+
* items={cartItems}
|
|
29
|
+
* renderItem={(item) => <CartLine item={item} />}
|
|
30
|
+
* header={<CustomerCard doc={customer} />}
|
|
31
|
+
* footer={<CartTotal items={cartItems} />}
|
|
32
|
+
* />
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function CartPanel({
|
|
36
|
+
items,
|
|
37
|
+
renderItem,
|
|
38
|
+
header,
|
|
39
|
+
footer,
|
|
40
|
+
emptyState,
|
|
41
|
+
className,
|
|
42
|
+
...viewProps
|
|
43
|
+
}: CartPanelProps) {
|
|
44
|
+
const hasItems = items.length > 0;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<View className={cn('flex-1', className)} {...viewProps}>
|
|
48
|
+
{header && <View className="border-b border-border px-3 py-2">{header}</View>}
|
|
49
|
+
|
|
50
|
+
{hasItems ? (
|
|
51
|
+
<ScrollView className="flex-1">
|
|
52
|
+
{items.map((item, index) => (
|
|
53
|
+
<View key={item.doc?.id ?? index}>
|
|
54
|
+
{renderItem(item, index)}
|
|
55
|
+
</View>
|
|
56
|
+
))}
|
|
57
|
+
</ScrollView>
|
|
58
|
+
) : (
|
|
59
|
+
emptyState ?? null
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
{footer && <View className="border-t border-border px-3 py-2">{footer}</View>}
|
|
63
|
+
</View>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Text, View, type ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { useProductTraits } from '@tallyui/core';
|
|
4
|
+
import { cn } from '@tallyui/theme';
|
|
5
|
+
import type { CartLineItem } from './cart-line';
|
|
6
|
+
|
|
7
|
+
export interface CartTotalProps extends Omit<ViewProps, 'children'> {
|
|
8
|
+
/** All cart line items */
|
|
9
|
+
items: CartLineItem[];
|
|
10
|
+
/** Currency symbol (defaults to '$') */
|
|
11
|
+
currencySymbol?: string;
|
|
12
|
+
/** Tax rate as a decimal (e.g. 0.1 for 10%). Defaults to 0. */
|
|
13
|
+
taxRate?: number;
|
|
14
|
+
/** Label for the tax line (defaults to 'Tax') */
|
|
15
|
+
taxLabel?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Displays a cart summary with subtotal, optional tax, and total.
|
|
21
|
+
*
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <CartTotal items={cartItems} taxRate={0.1} />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function CartTotal({
|
|
27
|
+
items,
|
|
28
|
+
currencySymbol = '$',
|
|
29
|
+
taxRate = 0,
|
|
30
|
+
taxLabel = 'Tax',
|
|
31
|
+
className,
|
|
32
|
+
...viewProps
|
|
33
|
+
}: CartTotalProps) {
|
|
34
|
+
const { getPrice } = useProductTraits();
|
|
35
|
+
|
|
36
|
+
const subtotal = items.reduce((sum, item) => {
|
|
37
|
+
const price = getPrice(item.doc);
|
|
38
|
+
return sum + (price ? parseFloat(price) * item.quantity : 0);
|
|
39
|
+
}, 0);
|
|
40
|
+
|
|
41
|
+
const tax = subtotal * taxRate;
|
|
42
|
+
const total = subtotal + tax;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<View className={cn('gap-1.5 px-3 py-3', className)} {...viewProps}>
|
|
46
|
+
<View className="flex-row items-center justify-between">
|
|
47
|
+
<Text className="text-sm text-muted">Subtotal</Text>
|
|
48
|
+
<Text className="text-sm text-foreground">{currencySymbol}{subtotal.toFixed(2)}</Text>
|
|
49
|
+
</View>
|
|
50
|
+
|
|
51
|
+
{taxRate > 0 && (
|
|
52
|
+
<View className="flex-row items-center justify-between">
|
|
53
|
+
<Text className="text-sm text-muted">{taxLabel}</Text>
|
|
54
|
+
<Text className="text-sm text-foreground">{currencySymbol}{tax.toFixed(2)}</Text>
|
|
55
|
+
</View>
|
|
56
|
+
)}
|
|
57
|
+
|
|
58
|
+
<View className="mt-1.5 flex-row items-center justify-between border-t border-border pt-2">
|
|
59
|
+
<Text className="text-base font-bold text-foreground">Total</Text>
|
|
60
|
+
<Text className="text-base font-bold text-foreground">{currencySymbol}{total.toFixed(2)}</Text>
|
|
61
|
+
</View>
|
|
62
|
+
</View>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { PaymentMethodCard, type PaymentMethodCardProps } from './payment-method-card';
|
|
2
|
+
export { OrderSummaryLine, type OrderSummaryLineProps } from './order-summary-line';
|
|
3
|
+
export { PaymentSelector, type PaymentSelectorProps, type PaymentMethod } from './payment-selector';
|
|
4
|
+
export { OrderSummary, type OrderSummaryProps, type OrderSummaryPayment } from './order-summary';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Text, View, type ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@tallyui/theme';
|
|
4
|
+
|
|
5
|
+
export interface OrderSummaryLineProps extends Omit<ViewProps, 'children'> {
|
|
6
|
+
label: string;
|
|
7
|
+
amount: string;
|
|
8
|
+
bold?: boolean;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function OrderSummaryLine({ label, amount, bold = false, className, ...viewProps }: OrderSummaryLineProps) {
|
|
13
|
+
return (
|
|
14
|
+
<View className={cn('flex-row items-center justify-between', className)} {...viewProps}>
|
|
15
|
+
<Text className={cn('text-sm', bold ? 'font-bold text-foreground' : 'text-muted')}>
|
|
16
|
+
{label}
|
|
17
|
+
</Text>
|
|
18
|
+
<Text className={cn('text-sm', bold ? 'font-bold text-foreground' : 'text-foreground')}>
|
|
19
|
+
{amount}
|
|
20
|
+
</Text>
|
|
21
|
+
</View>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import { cn } from '@tallyui/theme';
|
|
4
|
+
import { OrderSummaryLine } from './order-summary-line';
|
|
5
|
+
|
|
6
|
+
export interface OrderSummaryPayment {
|
|
7
|
+
label: string;
|
|
8
|
+
amount: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface OrderSummaryProps extends Omit<ViewProps, 'children'> {
|
|
12
|
+
/** Formatted subtotal string (e.g. "$1199.98") */
|
|
13
|
+
subtotal: string;
|
|
14
|
+
/** Formatted tax amount (optional) */
|
|
15
|
+
tax?: string;
|
|
16
|
+
/** Label for the tax line (defaults to "Tax") */
|
|
17
|
+
taxLabel?: string;
|
|
18
|
+
/** Formatted total string */
|
|
19
|
+
total: string;
|
|
20
|
+
/** Payment lines to display (e.g. cash tendered, change due) */
|
|
21
|
+
payments?: OrderSummaryPayment[];
|
|
22
|
+
/** Header slot (e.g. customer info, order number) */
|
|
23
|
+
header?: ReactNode;
|
|
24
|
+
/** Footer slot (e.g. action buttons) */
|
|
25
|
+
footer?: ReactNode;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Displays an order summary with subtotal, tax, total, and optional
|
|
31
|
+
* payment breakdown.
|
|
32
|
+
*
|
|
33
|
+
* Uses OrderSummaryLine internally for each row. Accepts header and
|
|
34
|
+
* footer slots for additional context like customer info or action buttons.
|
|
35
|
+
*
|
|
36
|
+
* ```tsx
|
|
37
|
+
* <OrderSummary
|
|
38
|
+
* subtotal="$1199.98"
|
|
39
|
+
* tax="$120.00"
|
|
40
|
+
* taxLabel="GST"
|
|
41
|
+
* total="$1319.98"
|
|
42
|
+
* payments={[{ label: 'Cash', amount: '$1400.00' }, { label: 'Change', amount: '$80.02' }]}
|
|
43
|
+
* header={<CustomerCard doc={customer} />}
|
|
44
|
+
* />
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function OrderSummary({
|
|
48
|
+
subtotal,
|
|
49
|
+
tax,
|
|
50
|
+
taxLabel = 'Tax',
|
|
51
|
+
total,
|
|
52
|
+
payments,
|
|
53
|
+
header,
|
|
54
|
+
footer,
|
|
55
|
+
className,
|
|
56
|
+
...viewProps
|
|
57
|
+
}: OrderSummaryProps) {
|
|
58
|
+
return (
|
|
59
|
+
<View className={cn('gap-1.5 px-3 py-3', className)} {...viewProps}>
|
|
60
|
+
{header && <View className="mb-2">{header}</View>}
|
|
61
|
+
|
|
62
|
+
<OrderSummaryLine label="Subtotal" amount={subtotal} />
|
|
63
|
+
{tax && <OrderSummaryLine label={taxLabel} amount={tax} />}
|
|
64
|
+
<OrderSummaryLine label="Total" amount={total} bold />
|
|
65
|
+
|
|
66
|
+
{payments && payments.length > 0 && (
|
|
67
|
+
<View className="mt-2 gap-1.5 border-t border-border pt-2">
|
|
68
|
+
{payments.map((payment) => (
|
|
69
|
+
<OrderSummaryLine key={payment.label} label={payment.label} amount={payment.amount} />
|
|
70
|
+
))}
|
|
71
|
+
</View>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
{footer && <View className="mt-2">{footer}</View>}
|
|
75
|
+
</View>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Pressable, Text, View, type ViewProps } from 'react-native';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@tallyui/theme';
|
|
5
|
+
|
|
6
|
+
export interface PaymentMethodCardProps extends Omit<ViewProps, 'children'> {
|
|
7
|
+
label: string;
|
|
8
|
+
selected: boolean;
|
|
9
|
+
onPress: () => void;
|
|
10
|
+
icon?: ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function PaymentMethodCard({ label, selected, onPress, icon, className, ...viewProps }: PaymentMethodCardProps) {
|
|
15
|
+
return (
|
|
16
|
+
<Pressable onPress={onPress} {...viewProps}>
|
|
17
|
+
<View
|
|
18
|
+
className={cn(
|
|
19
|
+
'flex-row items-center gap-3 rounded-lg border px-4 py-3',
|
|
20
|
+
selected ? 'border-primary bg-primary/5' : 'border-border bg-surface',
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
>
|
|
24
|
+
{icon && <View>{icon}</View>}
|
|
25
|
+
<Text className={cn('flex-1 text-sm font-medium', selected ? 'text-primary' : 'text-foreground')}>
|
|
26
|
+
{label}
|
|
27
|
+
</Text>
|
|
28
|
+
{selected && (
|
|
29
|
+
<View className="h-5 w-5 items-center justify-center rounded-full bg-primary">
|
|
30
|
+
<Text className="text-xs text-primary-foreground">{'\u2713'}</Text>
|
|
31
|
+
</View>
|
|
32
|
+
)}
|
|
33
|
+
</View>
|
|
34
|
+
</Pressable>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import { cn } from '@tallyui/theme';
|
|
4
|
+
import { PaymentMethodCard } from './payment-method-card';
|
|
5
|
+
|
|
6
|
+
export interface PaymentMethod {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PaymentSelectorProps extends Omit<ViewProps, 'children'> {
|
|
13
|
+
/** Available payment methods */
|
|
14
|
+
methods: PaymentMethod[];
|
|
15
|
+
/** ID of the currently selected method */
|
|
16
|
+
selected: string;
|
|
17
|
+
/** Called when a method is selected */
|
|
18
|
+
onSelect: (id: string) => void;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A list of payment method cards where one can be selected at a time.
|
|
24
|
+
*
|
|
25
|
+
* Wraps PaymentMethodCard for each method and manages the selected state.
|
|
26
|
+
*
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <PaymentSelector
|
|
29
|
+
* methods={[{ id: 'cash', label: 'Cash' }, { id: 'card', label: 'Credit Card' }]}
|
|
30
|
+
* selected={selectedMethod}
|
|
31
|
+
* onSelect={setSelectedMethod}
|
|
32
|
+
* />
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function PaymentSelector({
|
|
36
|
+
methods,
|
|
37
|
+
selected,
|
|
38
|
+
onSelect,
|
|
39
|
+
className,
|
|
40
|
+
...viewProps
|
|
41
|
+
}: PaymentSelectorProps) {
|
|
42
|
+
return (
|
|
43
|
+
<View className={cn('gap-2', className)} {...viewProps}>
|
|
44
|
+
{methods.map((method) => (
|
|
45
|
+
<PaymentMethodCard
|
|
46
|
+
key={method.id}
|
|
47
|
+
label={method.label}
|
|
48
|
+
selected={method.id === selected}
|
|
49
|
+
onPress={() => onSelect(method.id)}
|
|
50
|
+
icon={method.icon}
|
|
51
|
+
/>
|
|
52
|
+
))}
|
|
53
|
+
</View>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Text, View, type ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { useCustomerTraits } from '@tallyui/core';
|
|
4
|
+
import { cn } from '@tallyui/theme';
|
|
5
|
+
|
|
6
|
+
export interface CustomerCardProps extends Omit<ViewProps, 'children'> {
|
|
7
|
+
doc: any;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function CustomerCard({ doc, className, ...viewProps }: CustomerCardProps) {
|
|
12
|
+
const traits = useCustomerTraits();
|
|
13
|
+
if (!traits) return null;
|
|
14
|
+
|
|
15
|
+
const name = traits.getName(doc);
|
|
16
|
+
const email = traits.getEmail(doc);
|
|
17
|
+
const address = traits.getAddressSummary(doc);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<View className={cn('gap-0.5', className)} {...viewProps}>
|
|
21
|
+
<Text className="text-sm font-semibold text-foreground">{name}</Text>
|
|
22
|
+
{email && <Text className="text-xs text-muted">{email}</Text>}
|
|
23
|
+
{address && <Text className="text-xs text-muted">{address}</Text>}
|
|
24
|
+
</View>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Pressable, ScrollView, Text, View, type ViewProps } from 'react-native';
|
|
2
|
+
import { useCustomerTraits } from '@tallyui/core';
|
|
3
|
+
import { cn } from '@tallyui/theme';
|
|
4
|
+
import { CustomerCard } from './customer-card';
|
|
5
|
+
|
|
6
|
+
export interface CustomerSelectProps extends Omit<ViewProps, 'children'> {
|
|
7
|
+
/** Customer documents to display as options */
|
|
8
|
+
customers: any[];
|
|
9
|
+
/** Currently selected customer (if any) */
|
|
10
|
+
selected?: any;
|
|
11
|
+
/** Called when a customer is selected */
|
|
12
|
+
onSelect: (customer: any) => void;
|
|
13
|
+
/** Called when the search text changes */
|
|
14
|
+
onSearch: (query: string) => void;
|
|
15
|
+
/** Placeholder text for the search input */
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A customer picker that displays a list of customer cards.
|
|
22
|
+
*
|
|
23
|
+
* When a customer is selected, their name is shown as a summary.
|
|
24
|
+
* The parent is responsible for filtering — this component just
|
|
25
|
+
* calls onSearch with the query text and renders whatever customers
|
|
26
|
+
* are passed in.
|
|
27
|
+
*
|
|
28
|
+
* ```tsx
|
|
29
|
+
* <CustomerSelect
|
|
30
|
+
* customers={filteredCustomers}
|
|
31
|
+
* selected={selectedCustomer}
|
|
32
|
+
* onSelect={setSelectedCustomer}
|
|
33
|
+
* onSearch={setSearchQuery}
|
|
34
|
+
* />
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function CustomerSelect({
|
|
38
|
+
customers,
|
|
39
|
+
selected,
|
|
40
|
+
onSelect,
|
|
41
|
+
onSearch,
|
|
42
|
+
placeholder = 'Search customers...',
|
|
43
|
+
className,
|
|
44
|
+
...viewProps
|
|
45
|
+
}: CustomerSelectProps) {
|
|
46
|
+
const traits = useCustomerTraits();
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<View className={cn('gap-2', className)} {...viewProps}>
|
|
50
|
+
{selected && traits && (
|
|
51
|
+
<View className="rounded-lg border border-primary bg-primary/5 px-3 py-2">
|
|
52
|
+
<CustomerCard doc={selected} />
|
|
53
|
+
</View>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{customers.length > 0 && (
|
|
57
|
+
<ScrollView className="max-h-60">
|
|
58
|
+
{customers.map((customer, index) => {
|
|
59
|
+
const id = traits?.getId(customer) ?? index;
|
|
60
|
+
return (
|
|
61
|
+
<Pressable key={id} onPress={() => onSelect(customer)}>
|
|
62
|
+
<View className="border-b border-border px-3 py-2">
|
|
63
|
+
<CustomerCard doc={customer} />
|
|
64
|
+
</View>
|
|
65
|
+
</Pressable>
|
|
66
|
+
);
|
|
67
|
+
})}
|
|
68
|
+
</ScrollView>
|
|
69
|
+
)}
|
|
70
|
+
</View>
|
|
71
|
+
);
|
|
72
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Product components
|
|
2
|
+
export {
|
|
3
|
+
ProductTitle,
|
|
4
|
+
ProductPrice,
|
|
5
|
+
ProductImage,
|
|
6
|
+
ProductSku,
|
|
7
|
+
ProductStockBadge,
|
|
8
|
+
ProductCard,
|
|
9
|
+
ProductGrid,
|
|
10
|
+
type ProductTitleProps,
|
|
11
|
+
type ProductPriceProps,
|
|
12
|
+
type ProductImageProps,
|
|
13
|
+
type ProductSkuProps,
|
|
14
|
+
type ProductStockBadgeProps,
|
|
15
|
+
type ProductCardProps,
|
|
16
|
+
type ProductGridProps,
|
|
17
|
+
} from './product';
|
|
18
|
+
|
|
19
|
+
// Cart components
|
|
20
|
+
export {
|
|
21
|
+
CartLine,
|
|
22
|
+
CartTotal,
|
|
23
|
+
CartPanel,
|
|
24
|
+
type CartLineProps,
|
|
25
|
+
type CartLineItem,
|
|
26
|
+
type CartTotalProps,
|
|
27
|
+
type CartPanelProps,
|
|
28
|
+
} from './cart';
|
|
29
|
+
|
|
30
|
+
// Input components
|
|
31
|
+
export {
|
|
32
|
+
SearchInput,
|
|
33
|
+
type SearchInputProps,
|
|
34
|
+
FilterChip,
|
|
35
|
+
FilterChipGroup,
|
|
36
|
+
type FilterChipProps,
|
|
37
|
+
type FilterChipGroupProps,
|
|
38
|
+
type ChipItem,
|
|
39
|
+
} from './input';
|
|
40
|
+
|
|
41
|
+
// Customer components
|
|
42
|
+
export {
|
|
43
|
+
CustomerCard,
|
|
44
|
+
CustomerSelect,
|
|
45
|
+
type CustomerCardProps,
|
|
46
|
+
type CustomerSelectProps,
|
|
47
|
+
} from './customer';
|
|
48
|
+
|
|
49
|
+
// Checkout components
|
|
50
|
+
export {
|
|
51
|
+
PaymentMethodCard,
|
|
52
|
+
PaymentSelector,
|
|
53
|
+
type PaymentMethodCardProps,
|
|
54
|
+
type PaymentSelectorProps,
|
|
55
|
+
type PaymentMethod,
|
|
56
|
+
OrderSummaryLine,
|
|
57
|
+
OrderSummary,
|
|
58
|
+
type OrderSummaryLineProps,
|
|
59
|
+
type OrderSummaryProps,
|
|
60
|
+
type OrderSummaryPayment,
|
|
61
|
+
} from './checkout';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Pressable, ScrollView, Text, type ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@tallyui/theme';
|
|
4
|
+
|
|
5
|
+
export interface FilterChipProps {
|
|
6
|
+
label: string;
|
|
7
|
+
active: boolean;
|
|
8
|
+
onPress: () => void;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function FilterChip({ label, active, onPress, className }: FilterChipProps) {
|
|
13
|
+
return (
|
|
14
|
+
<Pressable
|
|
15
|
+
onPress={onPress}
|
|
16
|
+
className={cn(
|
|
17
|
+
'rounded-full px-3 py-1.5',
|
|
18
|
+
active ? 'bg-primary' : 'bg-surface-alt',
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
>
|
|
22
|
+
<Text className={cn('text-xs font-semibold', active ? 'text-primary-foreground' : 'text-muted')}>
|
|
23
|
+
{label}
|
|
24
|
+
</Text>
|
|
25
|
+
</Pressable>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ChipItem {
|
|
30
|
+
label: string;
|
|
31
|
+
active: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FilterChipGroupProps extends Omit<ViewProps, 'children'> {
|
|
35
|
+
chips: ChipItem[];
|
|
36
|
+
onChipPress: (index: number) => void;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function FilterChipGroup({ chips, onChipPress, className, ...viewProps }: FilterChipGroupProps) {
|
|
41
|
+
return (
|
|
42
|
+
<ScrollView
|
|
43
|
+
horizontal
|
|
44
|
+
showsHorizontalScrollIndicator={false}
|
|
45
|
+
contentContainerClassName={cn('flex-row gap-2', className)}
|
|
46
|
+
{...viewProps}
|
|
47
|
+
>
|
|
48
|
+
{chips.map((chip, index) => (
|
|
49
|
+
<FilterChip
|
|
50
|
+
key={chip.label}
|
|
51
|
+
label={chip.label}
|
|
52
|
+
active={chip.active}
|
|
53
|
+
onPress={() => onChipPress(index)}
|
|
54
|
+
/>
|
|
55
|
+
))}
|
|
56
|
+
</ScrollView>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { TextInput, View, Text, type TextInputProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@tallyui/theme';
|
|
4
|
+
|
|
5
|
+
export interface SearchInputProps extends Omit<TextInputProps, 'value' | 'onChangeText'> {
|
|
6
|
+
value: string;
|
|
7
|
+
onChangeText: (text: string) => void;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function SearchInput({ value, onChangeText, className, placeholder = 'Search...', ...props }: SearchInputProps) {
|
|
12
|
+
return (
|
|
13
|
+
<View className={cn('flex-row items-center gap-2 rounded-lg border border-border bg-surface px-3 py-2', className)}>
|
|
14
|
+
<Text className="text-muted">{'\u2315'}</Text>
|
|
15
|
+
<TextInput
|
|
16
|
+
role="searchbox"
|
|
17
|
+
value={value}
|
|
18
|
+
onChangeText={onChangeText}
|
|
19
|
+
placeholder={placeholder}
|
|
20
|
+
placeholderTextColor="#9ca3af"
|
|
21
|
+
className="flex-1 text-sm text-foreground"
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
</View>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { ProductTitle, type ProductTitleProps } from './product-title';
|
|
2
|
+
export { ProductPrice, type ProductPriceProps } from './product-price';
|
|
3
|
+
export { ProductImage, type ProductImageProps } from './product-image';
|
|
4
|
+
export { ProductSku, type ProductSkuProps } from './product-sku';
|
|
5
|
+
export { ProductStockBadge, type ProductStockBadgeProps } from './product-stock-badge';
|
|
6
|
+
export { ProductCard, type ProductCardProps } from './product-card';
|
|
7
|
+
export { ProductGrid, type ProductGridProps } from './product-grid';
|