@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.
- package/LICENSE +21 -0
- package/dist/index.d.ts +1076 -76
- package/dist/index.js +4662 -355
- package/package.json +27 -5
- package/src/cart/cart-line-actions.tsx +40 -0
- package/src/cart/cart-line.tsx +18 -32
- package/src/cart/cart-note-input.tsx +46 -0
- package/src/cart/cart-panel.tsx +39 -26
- package/src/cart/cart-total.tsx +43 -47
- package/src/cart/discount-badge.tsx +19 -0
- package/src/cart/index.ts +4 -1
- package/src/checkout/cash-tendered.tsx +101 -0
- package/src/checkout/change-display.tsx +19 -0
- package/src/checkout/index.ts +3 -0
- package/src/checkout/order-summary-line.tsx +7 -8
- package/src/checkout/order-summary.tsx +8 -7
- package/src/checkout/payment-method-card.tsx +10 -9
- package/src/checkout/payment-selector.tsx +5 -5
- package/src/checkout/receipt-preview.tsx +78 -0
- package/src/customer/customer-card.tsx +8 -9
- package/src/customer/customer-form.tsx +70 -0
- package/src/customer/customer-order-history.tsx +34 -0
- package/src/customer/customer-select.tsx +63 -24
- package/src/customer/index.ts +2 -0
- package/src/index.ts +112 -1
- package/src/input/filter-chip.tsx +85 -27
- package/src/input/index.ts +1 -0
- package/src/input/quantity-stepper.tsx +64 -0
- package/src/input/search-input.tsx +30 -7
- package/src/layout/checkout-layout.tsx +46 -0
- package/src/layout/index.ts +5 -0
- package/src/layout/live-tab-screen.tsx +54 -0
- package/src/layout/pos-layout.tsx +64 -0
- package/src/layout/settings-layout.tsx +58 -0
- package/src/layout/store-settings-choice-screen.tsx +142 -0
- package/src/order/index.ts +4 -0
- package/src/order/order-card.tsx +48 -0
- package/src/order/order-detail.tsx +82 -0
- package/src/order/order-list.tsx +39 -0
- package/src/order/order-status-badge.tsx +32 -0
- package/src/product/category-nav.tsx +70 -0
- package/src/product/index.ts +3 -0
- package/src/product/product-card.tsx +36 -8
- package/src/product/product-grid.tsx +25 -6
- package/src/product/product-image.tsx +28 -7
- package/src/product/product-list.tsx +44 -0
- package/src/product/product-price.tsx +77 -22
- package/src/product/product-sku.tsx +5 -5
- package/src/product/product-stock-badge.tsx +41 -28
- package/src/product/product-title.tsx +2 -3
- package/src/product/product-variant-picker.tsx +61 -0
- package/src/register/cash-count-input.tsx +85 -0
- package/src/register/closure-sheet.tsx +155 -0
- package/src/register/index.ts +12 -0
- package/src/register/movement-sheet.tsx +118 -0
- package/src/register/open-register-card.tsx +109 -0
- package/src/register/register-bar.helpers.ts +57 -0
- package/src/register/register-bar.tsx +85 -0
- package/src/register/register-column.tsx +117 -0
- package/src/register/register-count.tsx +330 -0
- package/src/register/register-open-close.tsx +52 -0
- package/src/register/register-panel.tsx +151 -0
- package/src/register/register-picker.tsx +60 -0
- package/src/register/register-summary.tsx +72 -0
- package/src/sale/cart-bar.tsx +18 -0
- package/src/sale/cart.tsx +66 -0
- package/src/sale/catalogue.tsx +131 -0
- package/src/sale/discount-form.tsx +85 -0
- package/src/sale/index.ts +9 -0
- package/src/sale/orders-list.tsx +66 -0
- package/src/sale/print-style.ts +11 -0
- package/src/sale/receipt.tsx +61 -0
- package/src/sale/sync-status.tsx +20 -0
- package/src/sale/tender.tsx +35 -0
- package/src/settings/connector-status.tsx +65 -0
- package/src/settings/index.ts +3 -0
- package/src/settings/settings-group.tsx +23 -0
- package/src/settings/settings-row.tsx +23 -0
- package/src/ui/accordion/index.tsx +59 -0
- package/src/ui/alert-dialog/index.tsx +91 -0
- package/src/ui/avatar/index.tsx +50 -0
- package/src/ui/badge/index.tsx +60 -0
- package/src/ui/button/index.tsx +90 -0
- package/src/ui/card/index.tsx +60 -0
- package/src/ui/checkbox/index.tsx +29 -0
- package/src/ui/collapsible/index.tsx +24 -0
- package/src/ui/combobox/index.tsx +98 -0
- package/src/ui/context-menu/index.tsx +159 -0
- package/src/ui/dialog/index.tsx +92 -0
- package/src/ui/dropdown-menu/index.tsx +161 -0
- package/src/ui/hover-card/index.tsx +31 -0
- package/src/ui/hstack/index.tsx +43 -0
- package/src/ui/icon/index.tsx +44 -0
- package/src/ui/icon/search-icon.tsx +9 -0
- package/src/ui/icon/svg-icon.tsx +36 -0
- package/src/ui/icon-button/index.tsx +58 -0
- package/src/ui/index.ts +99 -0
- package/src/ui/input/index.tsx +100 -0
- package/src/ui/label/index.tsx +26 -0
- package/src/ui/loader/index.tsx +37 -0
- package/src/ui/popover/index.tsx +31 -0
- package/src/ui/progress/index.tsx +28 -0
- package/src/ui/radio-group/index.tsx +38 -0
- package/src/ui/select/index.tsx +96 -0
- package/src/ui/separator/index.tsx +29 -0
- package/src/ui/slider/index.tsx +28 -0
- package/src/ui/switch/index.tsx +31 -0
- package/src/ui/table/index.tsx +59 -0
- package/src/ui/tabs/index.tsx +53 -0
- package/src/ui/text/index.tsx +36 -0
- package/src/ui/textarea/index.tsx +29 -0
- package/src/ui/toast/index.tsx +52 -0
- package/src/ui/toggle/index.tsx +69 -0
- package/src/ui/toggle-group/index.tsx +66 -0
- package/src/ui/tooltip/index.tsx +30 -0
- package/src/ui/vstack/index.tsx +43 -0
- package/src/uniwind-env.d.ts +9 -1
- package/src/__tests__/cart-line.test.tsx +0 -53
- package/src/__tests__/cart-panel.test.tsx +0 -55
- package/src/__tests__/cart-total.test.tsx +0 -60
- package/src/__tests__/customer-card.test.tsx +0 -47
- package/src/__tests__/customer-select.test.tsx +0 -33
- package/src/__tests__/filter-chip.test.tsx +0 -40
- package/src/__tests__/helpers.ts +0 -111
- package/src/__tests__/order-summary-line.test.tsx +0 -18
- package/src/__tests__/order-summary.test.tsx +0 -32
- package/src/__tests__/payment-method-card.test.tsx +0 -24
- package/src/__tests__/payment-selector.test.tsx +0 -23
- package/src/__tests__/product-card.test.tsx +0 -31
- package/src/__tests__/product-grid.test.tsx +0 -37
- package/src/__tests__/product-image.test.tsx +0 -54
- package/src/__tests__/product-price.test.tsx +0 -68
- package/src/__tests__/product-sku.test.tsx +0 -47
- package/src/__tests__/product-stock-badge.test.tsx +0 -58
- package/src/__tests__/product-title.test.tsx +0 -38
- 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": "
|
|
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
|
-
"
|
|
25
|
-
|
|
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
|
-
"@
|
|
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
|
+
}
|
package/src/cart/cart-line.tsx
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
|
-
import {
|
|
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
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
18
|
+
* <CartLine name="Coffee" quantity={2} unitPrice={unitPrice} lineTotal={lineTotal} />
|
|
26
19
|
* ```
|
|
27
20
|
*/
|
|
28
|
-
export function CartLine({
|
|
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
|
-
<
|
|
38
|
-
<
|
|
39
|
-
<Text className="text-sm font-medium
|
|
40
|
-
<Text className="text-xs text-muted">
|
|
41
|
-
{
|
|
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
|
-
</
|
|
44
|
-
<Text className="text-sm font-semibold
|
|
45
|
-
{lineTotal
|
|
29
|
+
</VStack>
|
|
30
|
+
<Text className="text-sm font-semibold">
|
|
31
|
+
{formatMoney(lineTotal, locale) ?? '—'}
|
|
46
32
|
</Text>
|
|
47
|
-
</
|
|
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
|
+
}
|
package/src/cart/cart-panel.tsx
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
2
|
-
import { ScrollView, View
|
|
1
|
+
import type { JSX, ReactNode } from 'react';
|
|
2
|
+
import { ScrollView, View } from 'react-native';
|
|
3
3
|
import { cn } from '@tallyui/theme';
|
|
4
|
-
import type
|
|
4
|
+
import { VStack, type VStackProps } from '../ui';
|
|
5
5
|
|
|
6
|
-
export interface CartPanelProps extends Omit<
|
|
6
|
+
export interface CartPanelProps<T> extends Omit<VStackProps, 'children'> {
|
|
7
7
|
/** Cart line items to render */
|
|
8
|
-
items:
|
|
8
|
+
items: T[];
|
|
9
9
|
/** Render function for each cart line */
|
|
10
|
-
renderItem: (item:
|
|
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
|
-
*
|
|
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
|
|
34
|
+
* renderItem={(item) => <CartLine {...item} />}
|
|
35
|
+
* keyExtractor={(line) => line.id}
|
|
30
36
|
* header={<CustomerCard doc={customer} />}
|
|
31
|
-
*
|
|
37
|
+
* afterItems={<DiscountChips discounts={discounts} />}
|
|
38
|
+
* footer={<CartTotal subtotal={subtotal} total={total} />}
|
|
32
39
|
* />
|
|
33
40
|
* ```
|
|
34
41
|
*/
|
|
35
|
-
|
|
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
|
-
...
|
|
43
|
-
}: CartPanelProps) {
|
|
56
|
+
...props
|
|
57
|
+
}: CartPanelProps<T>): JSX.Element {
|
|
44
58
|
const hasItems = items.length > 0;
|
|
45
59
|
|
|
46
60
|
return (
|
|
47
|
-
<
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
</
|
|
76
|
+
</VStack>
|
|
64
77
|
);
|
|
65
78
|
}
|
package/src/cart/cart-total.tsx
CHANGED
|
@@ -1,64 +1,60 @@
|
|
|
1
|
-
import {
|
|
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
|
|
6
|
-
|
|
7
|
-
export interface CartTotalProps extends Omit<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
17
|
+
* Displays a cart summary: subtotal, optional discount, optional tax, and total, in the receipt's order.
|
|
21
18
|
*
|
|
22
19
|
* ```tsx
|
|
23
|
-
* <CartTotal
|
|
20
|
+
* <CartTotal subtotal={subtotal} discount={discount} taxLines={taxLines} total={total} />
|
|
24
21
|
* ```
|
|
25
22
|
*/
|
|
26
23
|
export function CartTotal({
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
subtotal,
|
|
25
|
+
taxLines,
|
|
26
|
+
discount,
|
|
27
|
+
total,
|
|
28
|
+
taxInclusive,
|
|
29
|
+
locale,
|
|
31
30
|
className,
|
|
32
|
-
...
|
|
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
|
-
<
|
|
46
|
-
<
|
|
47
|
-
<Text className="text-sm text-muted">Subtotal</Text>
|
|
48
|
-
<Text className="text-sm
|
|
49
|
-
</
|
|
50
|
-
|
|
51
|
-
{
|
|
52
|
-
<
|
|
53
|
-
<Text className="text-sm text-muted">
|
|
54
|
-
<Text className="text-sm
|
|
55
|
-
</
|
|
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
|
-
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
+
}
|
package/src/checkout/index.ts
CHANGED
|
@@ -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<
|
|
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, ...
|
|
11
|
+
export function OrderSummaryLine({ label, amount, bold = false, className, ...props }: OrderSummaryLineProps) {
|
|
13
12
|
return (
|
|
14
|
-
<
|
|
15
|
-
<Text className={cn('text-sm', bold ? 'font-bold
|
|
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
|
|
17
|
+
<Text className={cn('text-sm', bold && 'font-bold')}>
|
|
19
18
|
{amount}
|
|
20
19
|
</Text>
|
|
21
|
-
</
|
|
20
|
+
</HStack>
|
|
22
21
|
);
|
|
23
22
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import { View
|
|
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<
|
|
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
|
-
...
|
|
57
|
+
...props
|
|
57
58
|
}: OrderSummaryProps) {
|
|
58
59
|
return (
|
|
59
|
-
<
|
|
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
|
-
<
|
|
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
|
-
</
|
|
72
|
+
</VStack>
|
|
72
73
|
)}
|
|
73
74
|
|
|
74
75
|
{footer && <View className="mt-2">{footer}</View>}
|
|
75
|
-
</
|
|
76
|
+
</VStack>
|
|
76
77
|
);
|
|
77
78
|
}
|