@tallyui/components 1.0.0 → 2.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 (136) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +1076 -76
  3. package/dist/index.js +4662 -355
  4. package/package.json +27 -5
  5. package/src/cart/cart-line-actions.tsx +40 -0
  6. package/src/cart/cart-line.tsx +18 -32
  7. package/src/cart/cart-note-input.tsx +46 -0
  8. package/src/cart/cart-panel.tsx +39 -26
  9. package/src/cart/cart-total.tsx +43 -47
  10. package/src/cart/discount-badge.tsx +19 -0
  11. package/src/cart/index.ts +4 -1
  12. package/src/checkout/cash-tendered.tsx +101 -0
  13. package/src/checkout/change-display.tsx +19 -0
  14. package/src/checkout/index.ts +3 -0
  15. package/src/checkout/order-summary-line.tsx +7 -8
  16. package/src/checkout/order-summary.tsx +8 -7
  17. package/src/checkout/payment-method-card.tsx +10 -9
  18. package/src/checkout/payment-selector.tsx +5 -5
  19. package/src/checkout/receipt-preview.tsx +78 -0
  20. package/src/customer/customer-card.tsx +8 -9
  21. package/src/customer/customer-form.tsx +70 -0
  22. package/src/customer/customer-order-history.tsx +34 -0
  23. package/src/customer/customer-select.tsx +63 -24
  24. package/src/customer/index.ts +2 -0
  25. package/src/index.ts +112 -1
  26. package/src/input/filter-chip.tsx +85 -27
  27. package/src/input/index.ts +1 -0
  28. package/src/input/quantity-stepper.tsx +64 -0
  29. package/src/input/search-input.tsx +30 -7
  30. package/src/layout/checkout-layout.tsx +46 -0
  31. package/src/layout/index.ts +5 -0
  32. package/src/layout/live-tab-screen.tsx +54 -0
  33. package/src/layout/pos-layout.tsx +64 -0
  34. package/src/layout/settings-layout.tsx +58 -0
  35. package/src/layout/store-settings-choice-screen.tsx +142 -0
  36. package/src/order/index.ts +4 -0
  37. package/src/order/order-card.tsx +48 -0
  38. package/src/order/order-detail.tsx +82 -0
  39. package/src/order/order-list.tsx +39 -0
  40. package/src/order/order-status-badge.tsx +32 -0
  41. package/src/product/category-nav.tsx +70 -0
  42. package/src/product/index.ts +3 -0
  43. package/src/product/product-card.tsx +36 -8
  44. package/src/product/product-grid.tsx +25 -6
  45. package/src/product/product-image.tsx +28 -7
  46. package/src/product/product-list.tsx +44 -0
  47. package/src/product/product-price.tsx +77 -22
  48. package/src/product/product-sku.tsx +5 -5
  49. package/src/product/product-stock-badge.tsx +41 -28
  50. package/src/product/product-title.tsx +2 -3
  51. package/src/product/product-variant-picker.tsx +61 -0
  52. package/src/register/cash-count-input.tsx +85 -0
  53. package/src/register/closure-sheet.tsx +155 -0
  54. package/src/register/index.ts +12 -0
  55. package/src/register/movement-sheet.tsx +118 -0
  56. package/src/register/open-register-card.tsx +109 -0
  57. package/src/register/register-bar.helpers.ts +57 -0
  58. package/src/register/register-bar.tsx +85 -0
  59. package/src/register/register-column.tsx +117 -0
  60. package/src/register/register-count.tsx +330 -0
  61. package/src/register/register-open-close.tsx +52 -0
  62. package/src/register/register-panel.tsx +151 -0
  63. package/src/register/register-picker.tsx +60 -0
  64. package/src/register/register-summary.tsx +72 -0
  65. package/src/sale/cart-bar.tsx +18 -0
  66. package/src/sale/cart.tsx +66 -0
  67. package/src/sale/catalogue.tsx +131 -0
  68. package/src/sale/discount-form.tsx +85 -0
  69. package/src/sale/index.ts +9 -0
  70. package/src/sale/orders-list.tsx +66 -0
  71. package/src/sale/print-style.ts +11 -0
  72. package/src/sale/receipt.tsx +61 -0
  73. package/src/sale/sync-status.tsx +20 -0
  74. package/src/sale/tender.tsx +35 -0
  75. package/src/settings/connector-status.tsx +65 -0
  76. package/src/settings/index.ts +3 -0
  77. package/src/settings/settings-group.tsx +23 -0
  78. package/src/settings/settings-row.tsx +23 -0
  79. package/src/ui/accordion/index.tsx +59 -0
  80. package/src/ui/alert-dialog/index.tsx +91 -0
  81. package/src/ui/avatar/index.tsx +50 -0
  82. package/src/ui/badge/index.tsx +60 -0
  83. package/src/ui/button/index.tsx +90 -0
  84. package/src/ui/card/index.tsx +60 -0
  85. package/src/ui/checkbox/index.tsx +29 -0
  86. package/src/ui/collapsible/index.tsx +24 -0
  87. package/src/ui/combobox/index.tsx +98 -0
  88. package/src/ui/context-menu/index.tsx +159 -0
  89. package/src/ui/dialog/index.tsx +92 -0
  90. package/src/ui/dropdown-menu/index.tsx +161 -0
  91. package/src/ui/hover-card/index.tsx +31 -0
  92. package/src/ui/hstack/index.tsx +43 -0
  93. package/src/ui/icon/index.tsx +44 -0
  94. package/src/ui/icon/search-icon.tsx +9 -0
  95. package/src/ui/icon/svg-icon.tsx +36 -0
  96. package/src/ui/icon-button/index.tsx +58 -0
  97. package/src/ui/index.ts +99 -0
  98. package/src/ui/input/index.tsx +100 -0
  99. package/src/ui/label/index.tsx +26 -0
  100. package/src/ui/loader/index.tsx +37 -0
  101. package/src/ui/popover/index.tsx +31 -0
  102. package/src/ui/progress/index.tsx +28 -0
  103. package/src/ui/radio-group/index.tsx +38 -0
  104. package/src/ui/select/index.tsx +96 -0
  105. package/src/ui/separator/index.tsx +29 -0
  106. package/src/ui/slider/index.tsx +28 -0
  107. package/src/ui/switch/index.tsx +31 -0
  108. package/src/ui/table/index.tsx +59 -0
  109. package/src/ui/tabs/index.tsx +53 -0
  110. package/src/ui/text/index.tsx +36 -0
  111. package/src/ui/textarea/index.tsx +29 -0
  112. package/src/ui/toast/index.tsx +52 -0
  113. package/src/ui/toggle/index.tsx +69 -0
  114. package/src/ui/toggle-group/index.tsx +66 -0
  115. package/src/ui/tooltip/index.tsx +30 -0
  116. package/src/ui/vstack/index.tsx +43 -0
  117. package/src/uniwind-env.d.ts +9 -1
  118. package/src/__tests__/cart-line.test.tsx +0 -53
  119. package/src/__tests__/cart-panel.test.tsx +0 -55
  120. package/src/__tests__/cart-total.test.tsx +0 -60
  121. package/src/__tests__/customer-card.test.tsx +0 -47
  122. package/src/__tests__/customer-select.test.tsx +0 -33
  123. package/src/__tests__/filter-chip.test.tsx +0 -40
  124. package/src/__tests__/helpers.ts +0 -111
  125. package/src/__tests__/order-summary-line.test.tsx +0 -18
  126. package/src/__tests__/order-summary.test.tsx +0 -32
  127. package/src/__tests__/payment-method-card.test.tsx +0 -24
  128. package/src/__tests__/payment-selector.test.tsx +0 -23
  129. package/src/__tests__/product-card.test.tsx +0 -31
  130. package/src/__tests__/product-grid.test.tsx +0 -37
  131. package/src/__tests__/product-image.test.tsx +0 -54
  132. package/src/__tests__/product-price.test.tsx +0 -68
  133. package/src/__tests__/product-sku.test.tsx +0 -47
  134. package/src/__tests__/product-stock-badge.test.tsx +0 -58
  135. package/src/__tests__/product-title.test.tsx +0 -38
  136. package/src/__tests__/search-input.test.tsx +0 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyui/components",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "description": "Universal POS UI components for Tally UI",
6
6
  "main": "./dist/index.js",
@@ -15,17 +15,39 @@
15
15
  },
16
16
  "files": [
17
17
  "dist",
18
- "src"
18
+ "src",
19
+ "!src/**/*.test.ts",
20
+ "!src/**/*.test.tsx",
21
+ "!src/**/__tests__/**"
19
22
  ],
20
23
  "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/TallyUI/tallyui.git",
27
+ "directory": "packages/components"
28
+ },
21
29
  "peerDependencies": {
30
+ "@tallyui/core": "2.0.0",
31
+ "@tallyui/primitives": "2.0.0",
32
+ "@tallyui/theme": "2.0.0",
22
33
  "react": ">=18",
23
34
  "react-native": ">=0.74",
24
- "@tallyui/core": "0.2.0",
25
- "@tallyui/theme": "0.2.0"
35
+ "react-native-svg": ">=15"
36
+ },
37
+ "dependencies": {
38
+ "class-variance-authority": "^0.7.1",
39
+ "@tallyui/pos": "2.0.0"
26
40
  },
27
41
  "devDependencies": {
28
- "@types/react": ">=18"
42
+ "@tallyui/connector-medusa": "2.0.0",
43
+ "@tallyui/connector-vendure": "2.0.0",
44
+ "@tallyui/connector-woocommerce": "2.0.0",
45
+ "@tallyui/mock-api": "2.0.0",
46
+ "@types/react": ">=18",
47
+ "react": "^19.2.4",
48
+ "react-native-svg": "15.12.1",
49
+ "rxdb": "16.21.1",
50
+ "rxjs": "7.8.2"
29
51
  },
30
52
  "scripts": {
31
53
  "build": "tsup",
@@ -0,0 +1,40 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Pressable, Text, View, type ViewProps } from 'react-native';
3
+
4
+ import { cn } from '@tallyui/theme';
5
+
6
+ export interface CartAction {
7
+ id: string;
8
+ label: string;
9
+ onPress: () => void;
10
+ color?: string;
11
+ iconSlot?: ReactNode;
12
+ }
13
+
14
+ export interface CartLineActionsProps extends Omit<ViewProps, 'children'> {
15
+ children: ReactNode;
16
+ actions: CartAction[];
17
+ className?: string;
18
+ }
19
+
20
+ export function CartLineActions({ children, actions, className, ...viewProps }: CartLineActionsProps) {
21
+ return (
22
+ <View className={cn('flex-row items-center', className)} {...viewProps}>
23
+ <View className="flex-1">{children}</View>
24
+ <View className="flex-row gap-1 px-2">
25
+ {actions.map((action) => (
26
+ <Pressable
27
+ key={action.id}
28
+ onPress={action.onPress}
29
+ className="rounded-md px-2 py-1.5"
30
+ >
31
+ {action.iconSlot}
32
+ <Text className={cn('text-xs font-medium', action.color ?? 'text-destructive')}>
33
+ {action.label}
34
+ </Text>
35
+ </Pressable>
36
+ ))}
37
+ </View>
38
+ </View>
39
+ );
40
+ }
@@ -1,20 +1,13 @@
1
- import { Text, View, type ViewProps } from 'react-native';
2
-
3
- import { useProductTraits } from '@tallyui/core';
1
+ import { formatMoney, type Money } from '@tallyui/core';
4
2
  import { cn } from '@tallyui/theme';
3
+ import { Text, HStack, VStack, type HStackProps } from '../ui';
5
4
 
6
- export interface CartLineItem {
7
- /** The raw product document (connector-specific shape) */
8
- doc: any;
9
- /** Quantity in cart */
5
+ export interface CartLineProps extends Omit<HStackProps, 'children'> {
6
+ name: string;
10
7
  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;
8
+ unitPrice: Money;
9
+ lineTotal: Money;
10
+ locale?: string;
18
11
  className?: string;
19
12
  }
20
13
 
@@ -22,28 +15,21 @@ export interface CartLineProps extends Omit<ViewProps, 'children'> {
22
15
  * Displays a single cart line item: product name, quantity, and line total.
23
16
  *
24
17
  * ```tsx
25
- * <CartLine item={{ doc: productDocument, quantity: 2 }} />
18
+ * <CartLine name="Coffee" quantity={2} unitPrice={unitPrice} lineTotal={lineTotal} />
26
19
  * ```
27
20
  */
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
-
21
+ export function CartLine({ name, quantity, unitPrice, lineTotal, locale, className, ...props }: CartLineProps) {
36
22
  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}`}
23
+ <HStack className={cn('px-3 py-2.5', className)} {...props}>
24
+ <VStack space="none" className="flex-1 gap-0.5">
25
+ <Text className="text-sm font-medium" numberOfLines={1}>{name}</Text>
26
+ <Text className="text-xs text-muted-foreground">
27
+ {`${formatMoney(unitPrice, locale) ?? '—'} × ${quantity}`}
42
28
  </Text>
43
- </View>
44
- <Text className="text-sm font-semibold text-foreground">
45
- {lineTotal ? `${currencySymbol}${lineTotal}` : '—'}
29
+ </VStack>
30
+ <Text className="text-sm font-semibold">
31
+ {formatMoney(lineTotal, locale) ?? '—'}
46
32
  </Text>
47
- </View>
33
+ </HStack>
48
34
  );
49
35
  }
@@ -0,0 +1,46 @@
1
+ import { useState } from 'react';
2
+ import { Pressable, Text, TextInput, View, type ViewProps } from 'react-native';
3
+
4
+ import { cn } from '@tallyui/theme';
5
+
6
+ export interface CartNoteInputProps extends Omit<ViewProps, 'children'> {
7
+ value: string;
8
+ onChangeText: (text: string) => void;
9
+ placeholder?: string;
10
+ className?: string;
11
+ }
12
+
13
+ export function CartNoteInput({
14
+ value,
15
+ onChangeText,
16
+ placeholder = 'Order note...',
17
+ className,
18
+ ...viewProps
19
+ }: CartNoteInputProps) {
20
+ const [expanded, setExpanded] = useState(value.length > 0);
21
+
22
+ if (!expanded) {
23
+ return (
24
+ <Pressable
25
+ onPress={() => setExpanded(true)}
26
+ className={cn('flex-row items-center gap-1.5 px-3 py-2', className)}
27
+ {...viewProps}
28
+ >
29
+ <Text className="text-xs text-primary">Add note</Text>
30
+ </Pressable>
31
+ );
32
+ }
33
+
34
+ return (
35
+ <View className={cn('px-3 py-2', className)} {...viewProps}>
36
+ <TextInput
37
+ value={value}
38
+ onChangeText={onChangeText}
39
+ placeholder={placeholder}
40
+ placeholderTextColorClassName="accent-muted-foreground"
41
+ multiline
42
+ className="min-h-[3rem] rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground"
43
+ />
44
+ </View>
45
+ );
46
+ }
@@ -1,15 +1,19 @@
1
- import type { ReactNode } from 'react';
2
- import { ScrollView, View, type ViewProps } from 'react-native';
1
+ import type { JSX, ReactNode } from 'react';
2
+ import { ScrollView, View } from 'react-native';
3
3
  import { cn } from '@tallyui/theme';
4
- import type { CartLineItem } from './cart-line';
4
+ import { VStack, type VStackProps } from '../ui';
5
5
 
6
- export interface CartPanelProps extends Omit<ViewProps, 'children'> {
6
+ export interface CartPanelProps<T> extends Omit<VStackProps, 'children'> {
7
7
  /** Cart line items to render */
8
- items: CartLineItem[];
8
+ items: T[];
9
9
  /** Render function for each cart line */
10
- renderItem: (item: CartLineItem, index: number) => ReactNode;
10
+ renderItem: (item: T, index: number) => ReactNode;
11
+ /** Row key per item; defaults to the item's string/number `id`, else its index. Override for items without `id`, or to key by something else. */
12
+ keyExtractor?: (item: T, index: number) => string;
11
13
  /** Header slot (e.g. customer info) */
12
14
  header?: ReactNode;
15
+ /** Content after the lines, inside the scrolling region (e.g. discount chips); shown even with no lines, so it isn't hidden by an empty cart */
16
+ afterItems?: ReactNode;
13
17
  /** Footer slot (e.g. totals, checkout button) */
14
18
  footer?: ReactNode;
15
19
  /** Content shown when items is empty */
@@ -20,46 +24,55 @@ export interface CartPanelProps extends Omit<ViewProps, 'children'> {
20
24
  /**
21
25
  * A scrollable cart panel with header/footer slots.
22
26
  *
23
- * Composes CartLine (or custom renderItem) into a scrollable list
24
- * with optional header, footer, and empty state.
27
+ * Composes CartLine (or custom renderItem) into a scrollable list with an
28
+ * optional header. The footer stays pinned at the bottom at every height;
29
+ * afterItems renders inside the scrolling region, after the lines.
25
30
  *
26
31
  * ```tsx
27
32
  * <CartPanel
28
33
  * items={cartItems}
29
- * renderItem={(item) => <CartLine item={item} />}
34
+ * renderItem={(item) => <CartLine {...item} />}
35
+ * keyExtractor={(line) => line.id}
30
36
  * header={<CustomerCard doc={customer} />}
31
- * footer={<CartTotal items={cartItems} />}
37
+ * afterItems={<DiscountChips discounts={discounts} />}
38
+ * footer={<CartTotal subtotal={subtotal} total={total} />}
32
39
  * />
33
40
  * ```
34
41
  */
35
- export function CartPanel({
42
+ function defaultKey(item: unknown, index: number): string {
43
+ const id = (item as { id?: unknown } | null)?.id;
44
+ return typeof id === 'string' || typeof id === 'number' ? String(id) : String(index);
45
+ }
46
+
47
+ export function CartPanel<T>({
36
48
  items,
37
49
  renderItem,
50
+ keyExtractor,
38
51
  header,
52
+ afterItems,
39
53
  footer,
40
54
  emptyState,
41
55
  className,
42
- ...viewProps
43
- }: CartPanelProps) {
56
+ ...props
57
+ }: CartPanelProps<T>): JSX.Element {
44
58
  const hasItems = items.length > 0;
45
59
 
46
60
  return (
47
- <View className={cn('flex-1', className)} {...viewProps}>
61
+ <VStack space="none" className={cn('flex-1 min-h-0', className)} {...props}>
48
62
  {header && <View className="border-b border-border px-3 py-2">{header}</View>}
49
63
 
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
- )}
64
+ <ScrollView className="flex-1 min-h-0" contentContainerClassName="flex-grow">
65
+ {hasItems
66
+ ? items.map((item, index) => (
67
+ <View key={keyExtractor ? keyExtractor(item, index) : defaultKey(item, index)}>
68
+ {renderItem(item, index)}
69
+ </View>
70
+ ))
71
+ : (emptyState ?? null)}
72
+ {afterItems}
73
+ </ScrollView>
61
74
 
62
75
  {footer && <View className="border-t border-border px-3 py-2">{footer}</View>}
63
- </View>
76
+ </VStack>
64
77
  );
65
78
  }
@@ -1,64 +1,60 @@
1
- import { Text, View, type ViewProps } from 'react-native';
2
-
3
- import { useProductTraits } from '@tallyui/core';
1
+ import { formatMoney, type Money } from '@tallyui/core';
4
2
  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;
3
+ import { Text, HStack, VStack, type VStackProps } from '../ui';
4
+
5
+ export interface CartTotalProps extends Omit<VStackProps, 'children'> {
6
+ subtotal: Money;
7
+ taxLines?: Array<{ label: string; amount: Money }>;
8
+ discount?: Money;
9
+ total: Money;
10
+ /** The tax is already in the subtotal (an inclusive display), so its rows read "incl." rather than add up. */
11
+ taxInclusive?: boolean;
12
+ locale?: string;
16
13
  className?: string;
17
14
  }
18
15
 
19
16
  /**
20
- * Displays a cart summary with subtotal, optional tax, and total.
17
+ * Displays a cart summary: subtotal, optional discount, optional tax, and total, in the receipt's order.
21
18
  *
22
19
  * ```tsx
23
- * <CartTotal items={cartItems} taxRate={0.1} />
20
+ * <CartTotal subtotal={subtotal} discount={discount} taxLines={taxLines} total={total} />
24
21
  * ```
25
22
  */
26
23
  export function CartTotal({
27
- items,
28
- currencySymbol = '$',
29
- taxRate = 0,
30
- taxLabel = 'Tax',
24
+ subtotal,
25
+ taxLines,
26
+ discount,
27
+ total,
28
+ taxInclusive,
29
+ locale,
31
30
  className,
32
- ...viewProps
31
+ ...props
33
32
  }: 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
33
  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>
34
+ <VStack space="none" className={cn('gap-1.5 px-3 py-3', className)} {...props}>
35
+ <HStack space="none" className="justify-between">
36
+ <Text className="text-sm text-muted-foreground">Subtotal</Text>
37
+ <Text className="text-sm">{formatMoney(subtotal, locale) ?? '—'}</Text>
38
+ </HStack>
39
+
40
+ {discount && discount.amount > 0 && (
41
+ <HStack space="none" className="justify-between">
42
+ <Text className="text-sm text-muted-foreground">Discount</Text>
43
+ <Text className="text-sm">−{formatMoney(discount, locale) ?? '—'}</Text>
44
+ </HStack>
56
45
  )}
57
46
 
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>
47
+ {taxLines?.map((line, index) => (
48
+ <HStack key={index} space="none" className="justify-between">
49
+ <Text className="text-sm text-muted-foreground">{taxInclusive ? `incl. ${line.label}` : line.label}</Text>
50
+ <Text className="text-sm">{formatMoney(line.amount, locale) ?? '—'}</Text>
51
+ </HStack>
52
+ ))}
53
+
54
+ <HStack space="none" className="mt-1.5 justify-between border-t border-border pt-2">
55
+ <Text className="font-bold">Total</Text>
56
+ <Text className="font-bold">{formatMoney(total, locale) ?? '—'}</Text>
57
+ </HStack>
58
+ </VStack>
63
59
  );
64
60
  }
@@ -0,0 +1,19 @@
1
+ import { Text, View, type ViewProps } from 'react-native';
2
+
3
+ import { cn } from '@tallyui/theme';
4
+
5
+ export interface DiscountBadgeProps extends Omit<ViewProps, 'children'> {
6
+ label: string;
7
+ className?: string;
8
+ }
9
+
10
+ export function DiscountBadge({ label, className, ...viewProps }: DiscountBadgeProps) {
11
+ return (
12
+ <View
13
+ className={cn('flex-row items-center self-start gap-1 rounded-md bg-sale/15 px-1.5 py-0.5', className)}
14
+ {...viewProps}
15
+ >
16
+ <Text className="text-xs font-semibold text-sale">{label}</Text>
17
+ </View>
18
+ );
19
+ }
package/src/cart/index.ts CHANGED
@@ -1,3 +1,6 @@
1
- export { CartLine, type CartLineProps, type CartLineItem } from './cart-line';
1
+ export { CartLine, type CartLineProps } from './cart-line';
2
2
  export { CartTotal, type CartTotalProps } from './cart-total';
3
3
  export { CartPanel, type CartPanelProps } from './cart-panel';
4
+ export { DiscountBadge, type DiscountBadgeProps } from './discount-badge';
5
+ export { CartNoteInput, type CartNoteInputProps } from './cart-note-input';
6
+ export { CartLineActions, type CartLineActionsProps, type CartAction } from './cart-line-actions';
@@ -0,0 +1,101 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { Pressable, Text, TextInput, View, type ViewProps } from 'react-native';
3
+
4
+ import { formatMoney, minorUnitDigits, moneyFromDecimalString, type Money } from '@tallyui/core';
5
+ import { cn } from '@tallyui/theme';
6
+
7
+ export interface CashTenderedProps extends Omit<ViewProps, 'children'> {
8
+ total: Money;
9
+ defaultAmount?: Money;
10
+ amount?: Money;
11
+ onChangeAmount?: (amount: Money) => void;
12
+ quickAmounts?: Money[];
13
+ locale?: string;
14
+ className?: string;
15
+ }
16
+
17
+ function defaultQuickAmounts(total: Money): Money[] {
18
+ const unit = 10 ** minorUnitDigits(total.currency);
19
+ const amounts = [
20
+ total.amount,
21
+ Math.ceil(total.amount / (5 * unit)) * 5 * unit,
22
+ Math.ceil(total.amount / (10 * unit)) * 10 * unit,
23
+ Math.ceil(total.amount / (20 * unit)) * 20 * unit,
24
+ ];
25
+ return [...new Set(amounts)].sort((a, b) => a - b).map((amount) => ({ amount, currency: total.currency }));
26
+ }
27
+
28
+ export function CashTendered({
29
+ total,
30
+ defaultAmount,
31
+ amount: controlledAmount,
32
+ onChangeAmount,
33
+ quickAmounts,
34
+ locale,
35
+ className,
36
+ ...viewProps
37
+ }: CashTenderedProps) {
38
+ const [internalAmount, setInternalAmount] = useState(defaultAmount ?? total);
39
+ const amount = controlledAmount ?? internalAmount;
40
+ const buttons = quickAmounts ?? defaultQuickAmounts(total);
41
+ const digits = minorUnitDigits(amount.currency);
42
+ const amountText = String(Math.abs(amount.amount)).padStart(digits + 1, '0');
43
+ const decimal = (amount.amount < 0 ? '-' : '') + (digits
44
+ ? `${amountText.slice(0, -digits)}.${amountText.slice(-digits)}` : amountText);
45
+ const [text, setText] = useState(decimal);
46
+ const lastTextAmount = useRef(amount);
47
+
48
+ useEffect(() => {
49
+ if (amount.amount !== lastTextAmount.current.amount || amount.currency !== lastTextAmount.current.currency) {
50
+ setText(decimal);
51
+ lastTextAmount.current = amount;
52
+ }
53
+ }, [amount, decimal]);
54
+
55
+ const handleChange = (value: Money) => {
56
+ if (controlledAmount === undefined) setInternalAmount(value);
57
+ onChangeAmount?.(value);
58
+ };
59
+
60
+ return (
61
+ <View className={cn('gap-3', className)} {...viewProps}>
62
+ <Text className="text-sm font-semibold text-foreground">Cash Tendered</Text>
63
+ <View className="flex-row flex-wrap gap-2">
64
+ {buttons.map((val) => (
65
+ <Pressable
66
+ key={`${val.currency}:${val.amount}`}
67
+ onPress={() => handleChange(val)}
68
+ className={cn(
69
+ 'rounded-lg border px-4 py-2',
70
+ amount.amount === val.amount && amount.currency === val.currency ? 'border-primary bg-primary' : 'border-input bg-card',
71
+ )}
72
+ >
73
+ <Text
74
+ className={cn(
75
+ 'text-sm font-semibold',
76
+ amount.amount === val.amount && amount.currency === val.currency ? 'text-primary-foreground' : 'text-foreground',
77
+ )}
78
+ >
79
+ {formatMoney(val, locale) ?? '—'}
80
+ </Text>
81
+ </Pressable>
82
+ ))}
83
+ </View>
84
+ <TextInput
85
+ value={text}
86
+ onChangeText={(text) => {
87
+ const parsed = moneyFromDecimalString(text, total.currency);
88
+ if (parsed) {
89
+ setText(text);
90
+ lastTextAmount.current = parsed;
91
+ handleChange(parsed);
92
+ } else if (text === '' || /^\d+\.$/.test(text)) {
93
+ setText(text);
94
+ }
95
+ }}
96
+ keyboardType="decimal-pad"
97
+ className="rounded-lg border border-border bg-card px-3 py-2 text-base text-foreground"
98
+ />
99
+ </View>
100
+ );
101
+ }
@@ -0,0 +1,19 @@
1
+ import { Text, View, type ViewProps } from 'react-native';
2
+
3
+ import { formatMoney, type Money } from '@tallyui/core';
4
+ import { cn } from '@tallyui/theme';
5
+
6
+ export interface ChangeDisplayProps extends Omit<ViewProps, 'children'> {
7
+ change: Money;
8
+ locale?: string;
9
+ className?: string;
10
+ }
11
+
12
+ export function ChangeDisplay({ change, locale, className, ...viewProps }: ChangeDisplayProps) {
13
+ return (
14
+ <View className={cn('items-center gap-1 rounded-xl bg-muted p-4', className)} {...viewProps}>
15
+ <Text className="text-sm text-muted-foreground">Change Due</Text>
16
+ <Text className="text-3xl font-bold text-success">{formatMoney(change, locale) ?? '—'}</Text>
17
+ </View>
18
+ );
19
+ }
@@ -2,3 +2,6 @@ export { PaymentMethodCard, type PaymentMethodCardProps } from './payment-method
2
2
  export { OrderSummaryLine, type OrderSummaryLineProps } from './order-summary-line';
3
3
  export { PaymentSelector, type PaymentSelectorProps, type PaymentMethod } from './payment-selector';
4
4
  export { OrderSummary, type OrderSummaryProps, type OrderSummaryPayment } from './order-summary';
5
+ export { ChangeDisplay, type ChangeDisplayProps } from './change-display';
6
+ export { CashTendered, type CashTenderedProps } from './cash-tendered';
7
+ export { ReceiptPreview, type ReceiptPreviewProps, type ReceiptItem } from './receipt-preview';
@@ -1,23 +1,22 @@
1
- import { Text, View, type ViewProps } from 'react-native';
2
-
3
1
  import { cn } from '@tallyui/theme';
2
+ import { Text, HStack, type HStackProps } from '../ui';
4
3
 
5
- export interface OrderSummaryLineProps extends Omit<ViewProps, 'children'> {
4
+ export interface OrderSummaryLineProps extends Omit<HStackProps, 'children'> {
6
5
  label: string;
7
6
  amount: string;
8
7
  bold?: boolean;
9
8
  className?: string;
10
9
  }
11
10
 
12
- export function OrderSummaryLine({ label, amount, bold = false, className, ...viewProps }: OrderSummaryLineProps) {
11
+ export function OrderSummaryLine({ label, amount, bold = false, className, ...props }: OrderSummaryLineProps) {
13
12
  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')}>
13
+ <HStack space="none" className={cn('justify-between', className)} {...props}>
14
+ <Text className={cn('text-sm', bold ? 'font-bold' : 'text-muted-foreground')}>
16
15
  {label}
17
16
  </Text>
18
- <Text className={cn('text-sm', bold ? 'font-bold text-foreground' : 'text-foreground')}>
17
+ <Text className={cn('text-sm', bold && 'font-bold')}>
19
18
  {amount}
20
19
  </Text>
21
- </View>
20
+ </HStack>
22
21
  );
23
22
  }
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
- import { View, type ViewProps } from 'react-native';
2
+ import { View } from 'react-native';
3
3
  import { cn } from '@tallyui/theme';
4
+ import { VStack, type VStackProps } from '../ui';
4
5
  import { OrderSummaryLine } from './order-summary-line';
5
6
 
6
7
  export interface OrderSummaryPayment {
@@ -8,7 +9,7 @@ export interface OrderSummaryPayment {
8
9
  amount: string;
9
10
  }
10
11
 
11
- export interface OrderSummaryProps extends Omit<ViewProps, 'children'> {
12
+ export interface OrderSummaryProps extends Omit<VStackProps, 'children'> {
12
13
  /** Formatted subtotal string (e.g. "$1199.98") */
13
14
  subtotal: string;
14
15
  /** Formatted tax amount (optional) */
@@ -53,10 +54,10 @@ export function OrderSummary({
53
54
  header,
54
55
  footer,
55
56
  className,
56
- ...viewProps
57
+ ...props
57
58
  }: OrderSummaryProps) {
58
59
  return (
59
- <View className={cn('gap-1.5 px-3 py-3', className)} {...viewProps}>
60
+ <VStack space="none" className={cn('gap-1.5 px-3 py-3', className)} {...props}>
60
61
  {header && <View className="mb-2">{header}</View>}
61
62
 
62
63
  <OrderSummaryLine label="Subtotal" amount={subtotal} />
@@ -64,14 +65,14 @@ export function OrderSummary({
64
65
  <OrderSummaryLine label="Total" amount={total} bold />
65
66
 
66
67
  {payments && payments.length > 0 && (
67
- <View className="mt-2 gap-1.5 border-t border-border pt-2">
68
+ <VStack space="none" className="mt-2 gap-1.5 border-t border-border pt-2">
68
69
  {payments.map((payment) => (
69
70
  <OrderSummaryLine key={payment.label} label={payment.label} amount={payment.amount} />
70
71
  ))}
71
- </View>
72
+ </VStack>
72
73
  )}
73
74
 
74
75
  {footer && <View className="mt-2">{footer}</View>}
75
- </View>
76
+ </VStack>
76
77
  );
77
78
  }