@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,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,3 @@
1
+ export { CartLine, type CartLineProps, type CartLineItem } from './cart-line';
2
+ export { CartTotal, type CartTotalProps } from './cart-total';
3
+ export { CartPanel, type CartPanelProps } from './cart-panel';
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export { CustomerCard, type CustomerCardProps } from './customer-card';
2
+ export { CustomerSelect, type CustomerSelectProps } from './customer-select';
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,2 @@
1
+ export { SearchInput, type SearchInputProps } from './search-input';
2
+ export { FilterChip, FilterChipGroup, type FilterChipProps, type FilterChipGroupProps, type ChipItem } from './filter-chip';
@@ -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';