@tallyui/components 1.0.0 → 3.0.0-next.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 +1113 -83
- package/dist/index.js +4936 -353
- 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 +16 -12
- package/src/customer/customer-form.tsx +72 -0
- package/src/customer/customer-order-history.tsx +34 -0
- package/src/customer/customer-picker.tsx +124 -0
- package/src/customer/customer-select.tsx +80 -31
- package/src/customer/index.ts +3 -0
- package/src/index.ts +114 -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 +46 -13
- 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 +161 -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 +139 -0
- package/src/sale/discount-form.tsx +85 -0
- package/src/sale/index.ts +9 -0
- package/src/sale/orders-list.tsx +108 -0
- package/src/sale/print-style.ts +11 -0
- package/src/sale/receipt.tsx +62 -0
- package/src/sale/sync-status.tsx +141 -0
- package/src/sale/tender.tsx +38 -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/src/index.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/// <reference path="./uniwind-env.d.ts" />
|
|
2
|
+
|
|
3
|
+
// Generic UI components
|
|
4
|
+
export * from './ui';
|
|
5
|
+
|
|
1
6
|
// Product components
|
|
2
7
|
export {
|
|
3
8
|
ProductTitle,
|
|
@@ -14,6 +19,14 @@ export {
|
|
|
14
19
|
type ProductStockBadgeProps,
|
|
15
20
|
type ProductCardProps,
|
|
16
21
|
type ProductGridProps,
|
|
22
|
+
ProductList,
|
|
23
|
+
type ProductListProps,
|
|
24
|
+
CategoryNav,
|
|
25
|
+
type CategoryNavProps,
|
|
26
|
+
type CategoryItem,
|
|
27
|
+
ProductVariantPicker,
|
|
28
|
+
type ProductVariantPickerProps,
|
|
29
|
+
type VariantOption,
|
|
17
30
|
} from './product';
|
|
18
31
|
|
|
19
32
|
// Cart components
|
|
@@ -21,10 +34,16 @@ export {
|
|
|
21
34
|
CartLine,
|
|
22
35
|
CartTotal,
|
|
23
36
|
CartPanel,
|
|
37
|
+
DiscountBadge,
|
|
38
|
+
CartNoteInput,
|
|
24
39
|
type CartLineProps,
|
|
25
|
-
type CartLineItem,
|
|
26
40
|
type CartTotalProps,
|
|
27
41
|
type CartPanelProps,
|
|
42
|
+
type DiscountBadgeProps,
|
|
43
|
+
type CartNoteInputProps,
|
|
44
|
+
CartLineActions,
|
|
45
|
+
type CartLineActionsProps,
|
|
46
|
+
type CartAction,
|
|
28
47
|
} from './cart';
|
|
29
48
|
|
|
30
49
|
// Input components
|
|
@@ -36,14 +55,23 @@ export {
|
|
|
36
55
|
type FilterChipProps,
|
|
37
56
|
type FilterChipGroupProps,
|
|
38
57
|
type ChipItem,
|
|
58
|
+
QuantityStepper,
|
|
59
|
+
type QuantityStepperProps,
|
|
39
60
|
} from './input';
|
|
40
61
|
|
|
41
62
|
// Customer components
|
|
42
63
|
export {
|
|
43
64
|
CustomerCard,
|
|
44
65
|
CustomerSelect,
|
|
66
|
+
CustomerPicker,
|
|
67
|
+
type CustomerPickerProps,
|
|
45
68
|
type CustomerCardProps,
|
|
46
69
|
type CustomerSelectProps,
|
|
70
|
+
CustomerForm,
|
|
71
|
+
type CustomerFormProps,
|
|
72
|
+
type CustomerFormValues,
|
|
73
|
+
CustomerOrderHistory,
|
|
74
|
+
type CustomerOrderHistoryProps,
|
|
47
75
|
} from './customer';
|
|
48
76
|
|
|
49
77
|
// Checkout components
|
|
@@ -58,4 +86,89 @@ export {
|
|
|
58
86
|
type OrderSummaryLineProps,
|
|
59
87
|
type OrderSummaryProps,
|
|
60
88
|
type OrderSummaryPayment,
|
|
89
|
+
ChangeDisplay,
|
|
90
|
+
type ChangeDisplayProps,
|
|
91
|
+
CashTendered,
|
|
92
|
+
type CashTenderedProps,
|
|
93
|
+
ReceiptPreview,
|
|
94
|
+
type ReceiptPreviewProps,
|
|
95
|
+
type ReceiptItem,
|
|
61
96
|
} from './checkout';
|
|
97
|
+
|
|
98
|
+
// Order components
|
|
99
|
+
export {
|
|
100
|
+
OrderStatusBadge,
|
|
101
|
+
type OrderStatusBadgeProps,
|
|
102
|
+
type OrderStatus,
|
|
103
|
+
OrderCard,
|
|
104
|
+
type OrderCardProps,
|
|
105
|
+
OrderList,
|
|
106
|
+
type OrderListProps,
|
|
107
|
+
OrderDetail,
|
|
108
|
+
type OrderDetailProps,
|
|
109
|
+
type OrderDetailLineItem,
|
|
110
|
+
} from './order';
|
|
111
|
+
|
|
112
|
+
// Register components
|
|
113
|
+
export {
|
|
114
|
+
CashCountInput,
|
|
115
|
+
type CashCountInputProps,
|
|
116
|
+
type Denomination,
|
|
117
|
+
RegisterSummary,
|
|
118
|
+
type RegisterSummaryProps,
|
|
119
|
+
type TransactionSummary,
|
|
120
|
+
RegisterOpenClose,
|
|
121
|
+
type RegisterOpenCloseProps,
|
|
122
|
+
RegisterPicker,
|
|
123
|
+
type RegisterPickerProps,
|
|
124
|
+
type RegisterPickerRegister,
|
|
125
|
+
OpenRegisterCard,
|
|
126
|
+
type OpenRegisterCardProps,
|
|
127
|
+
RegisterBar,
|
|
128
|
+
type RegisterBarProps,
|
|
129
|
+
describeRegisterBarPill,
|
|
130
|
+
type RegisterBarPill,
|
|
131
|
+
MovementSheet,
|
|
132
|
+
type MovementSheetProps,
|
|
133
|
+
RegisterPanel,
|
|
134
|
+
type RegisterPanelProps,
|
|
135
|
+
RegisterColumn,
|
|
136
|
+
type RegisterColumnProps,
|
|
137
|
+
RegisterCount,
|
|
138
|
+
APPROVAL_REQUIRED_TEXT,
|
|
139
|
+
type RegisterCountProps,
|
|
140
|
+
ClosureSheet,
|
|
141
|
+
type ClosureSheetProps,
|
|
142
|
+
} from './register';
|
|
143
|
+
|
|
144
|
+
// Settings components
|
|
145
|
+
export {
|
|
146
|
+
SettingsRow,
|
|
147
|
+
type SettingsRowProps,
|
|
148
|
+
SettingsGroup,
|
|
149
|
+
type SettingsGroupProps,
|
|
150
|
+
ConnectorStatus,
|
|
151
|
+
type ConnectorStatusProps,
|
|
152
|
+
type ConnectionStatus,
|
|
153
|
+
} from './settings';
|
|
154
|
+
|
|
155
|
+
// Sale components
|
|
156
|
+
export {
|
|
157
|
+
Cart, CartBar, Tender, DiscountForm, DiscountChips, parseDiscount, discountLabel,
|
|
158
|
+
Catalogue, formatStockSyncTime, Receipt, injectPrintStyle, SyncStatus, OrdersList,
|
|
159
|
+
} from './sale';
|
|
160
|
+
|
|
161
|
+
// Layout components
|
|
162
|
+
export {
|
|
163
|
+
POSLayout,
|
|
164
|
+
type POSLayoutProps,
|
|
165
|
+
CheckoutLayout,
|
|
166
|
+
type CheckoutLayoutProps,
|
|
167
|
+
SettingsLayout,
|
|
168
|
+
type SettingsLayoutProps,
|
|
169
|
+
LiveTabScreen,
|
|
170
|
+
type LiveTabScreenProps,
|
|
171
|
+
type LiveTabScreenState,
|
|
172
|
+
StoreSettingsChoiceScreen,
|
|
173
|
+
type StoreSettingsChoiceScreenProps,
|
|
174
|
+
} from './layout';
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { Pressable, ScrollView, type ViewProps } from 'react-native';
|
|
3
|
+
import { ToggleGroup } from '@tallyui/primitives';
|
|
3
4
|
import { cn } from '@tallyui/theme';
|
|
5
|
+
import { Text } from '../ui';
|
|
4
6
|
|
|
5
7
|
export interface FilterChipProps {
|
|
6
8
|
label: string;
|
|
@@ -9,20 +11,33 @@ export interface FilterChipProps {
|
|
|
9
11
|
className?: string;
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
/**
|
|
15
|
+
* A single toggle chip. When used standalone, renders as a simple
|
|
16
|
+
* ToggleGroup with one item.
|
|
17
|
+
*/
|
|
12
18
|
export function FilterChip({ label, active, onPress, className }: FilterChipProps) {
|
|
19
|
+
const value = useMemo(() => (active ? [label] : []), [active, label]);
|
|
20
|
+
|
|
13
21
|
return (
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
active ? 'bg-primary' : 'bg-surface-alt',
|
|
19
|
-
className,
|
|
20
|
-
)}
|
|
22
|
+
<ToggleGroup.Root
|
|
23
|
+
type="multiple"
|
|
24
|
+
value={value}
|
|
25
|
+
onValueChange={() => onPress()}
|
|
21
26
|
>
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
<ToggleGroup.Item value={label} asChild>
|
|
28
|
+
<Pressable
|
|
29
|
+
className={cn(
|
|
30
|
+
'rounded-full px-3 py-1.5',
|
|
31
|
+
active ? 'bg-primary' : 'bg-accent',
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
<Text className={cn('text-xs font-semibold', active ? 'text-primary-foreground' : 'text-muted-foreground')}>
|
|
36
|
+
{label}
|
|
37
|
+
</Text>
|
|
38
|
+
</Pressable>
|
|
39
|
+
</ToggleGroup.Item>
|
|
40
|
+
</ToggleGroup.Root>
|
|
26
41
|
);
|
|
27
42
|
}
|
|
28
43
|
|
|
@@ -37,22 +52,65 @@ export interface FilterChipGroupProps extends Omit<ViewProps, 'children'> {
|
|
|
37
52
|
className?: string;
|
|
38
53
|
}
|
|
39
54
|
|
|
55
|
+
/**
|
|
56
|
+
* A horizontal scrollable row of toggleable filter chips.
|
|
57
|
+
*
|
|
58
|
+
* Built on top of the ToggleGroup primitive for proper `aria-pressed`
|
|
59
|
+
* semantics and accessible toggle group behavior.
|
|
60
|
+
*/
|
|
40
61
|
export function FilterChipGroup({ chips, onChipPress, className, ...viewProps }: FilterChipGroupProps) {
|
|
62
|
+
// Derive the controlled value from which chips are active
|
|
63
|
+
const activeValues = useMemo(
|
|
64
|
+
() => chips.filter((c) => c.active).map((c) => c.label),
|
|
65
|
+
[chips],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Build a label-to-index lookup so we can translate back to onChipPress(index)
|
|
69
|
+
const labelToIndex = useMemo(() => {
|
|
70
|
+
const map = new Map<string, number>();
|
|
71
|
+
chips.forEach((chip, i) => map.set(chip.label, i));
|
|
72
|
+
return map;
|
|
73
|
+
}, [chips]);
|
|
74
|
+
|
|
75
|
+
function handleValueChange(newValues: string[]) {
|
|
76
|
+
// Find which chip was toggled by diffing against current active set
|
|
77
|
+
const added = newValues.find((v) => !activeValues.includes(v));
|
|
78
|
+
const removed = activeValues.find((v) => !newValues.includes(v));
|
|
79
|
+
const toggledLabel = added ?? removed;
|
|
80
|
+
if (toggledLabel !== undefined) {
|
|
81
|
+
const index = labelToIndex.get(toggledLabel);
|
|
82
|
+
if (index !== undefined) onChipPress(index);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
41
86
|
return (
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
87
|
+
<ToggleGroup.Root
|
|
88
|
+
type="multiple"
|
|
89
|
+
value={activeValues}
|
|
90
|
+
onValueChange={handleValueChange}
|
|
91
|
+
asChild
|
|
47
92
|
>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
93
|
+
<ScrollView
|
|
94
|
+
horizontal
|
|
95
|
+
showsHorizontalScrollIndicator={false}
|
|
96
|
+
contentContainerClassName={cn('flex-row gap-2', className)}
|
|
97
|
+
{...viewProps}
|
|
98
|
+
>
|
|
99
|
+
{chips.map((chip) => (
|
|
100
|
+
<ToggleGroup.Item key={chip.label} value={chip.label} asChild>
|
|
101
|
+
<Pressable
|
|
102
|
+
className={cn(
|
|
103
|
+
'rounded-full px-3 py-1.5',
|
|
104
|
+
chip.active ? 'bg-primary' : 'bg-accent',
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
<Text className={cn('text-xs font-semibold', chip.active ? 'text-primary-foreground' : 'text-muted-foreground')}>
|
|
108
|
+
{chip.label}
|
|
109
|
+
</Text>
|
|
110
|
+
</Pressable>
|
|
111
|
+
</ToggleGroup.Item>
|
|
112
|
+
))}
|
|
113
|
+
</ScrollView>
|
|
114
|
+
</ToggleGroup.Root>
|
|
57
115
|
);
|
|
58
116
|
}
|
package/src/input/index.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { SearchInput, type SearchInputProps } from './search-input';
|
|
2
2
|
export { FilterChip, FilterChipGroup, type FilterChipProps, type FilterChipGroupProps, type ChipItem } from './filter-chip';
|
|
3
|
+
export { QuantityStepper, type QuantityStepperProps } from './quantity-stepper';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Pressable, Text, View, type ViewProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@tallyui/theme';
|
|
5
|
+
|
|
6
|
+
export interface QuantityStepperProps extends Omit<ViewProps, 'children'> {
|
|
7
|
+
defaultQuantity?: number;
|
|
8
|
+
quantity?: number;
|
|
9
|
+
onChangeQuantity?: (quantity: number) => void;
|
|
10
|
+
min?: number;
|
|
11
|
+
max?: number;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function QuantityStepper({
|
|
16
|
+
defaultQuantity = 1,
|
|
17
|
+
quantity: controlledQuantity,
|
|
18
|
+
onChangeQuantity,
|
|
19
|
+
min = 1,
|
|
20
|
+
max = 999,
|
|
21
|
+
className,
|
|
22
|
+
...viewProps
|
|
23
|
+
}: QuantityStepperProps) {
|
|
24
|
+
const [internalQuantity, setInternalQuantity] = useState(defaultQuantity);
|
|
25
|
+
const quantity = controlledQuantity ?? internalQuantity;
|
|
26
|
+
|
|
27
|
+
const handleChange = (next: number) => {
|
|
28
|
+
if (next < min || next > max) return;
|
|
29
|
+
if (controlledQuantity === undefined) setInternalQuantity(next);
|
|
30
|
+
onChangeQuantity?.(next);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<View className={cn('flex-row items-center gap-2', className)} {...viewProps}>
|
|
35
|
+
<Pressable
|
|
36
|
+
onPress={() => handleChange(quantity - 1)}
|
|
37
|
+
disabled={quantity <= min}
|
|
38
|
+
className={cn(
|
|
39
|
+
'h-8 w-8 items-center justify-center rounded-lg',
|
|
40
|
+
quantity <= min ? 'bg-muted/50' : 'bg-muted',
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
<Text className={cn('text-base font-semibold', quantity <= min ? 'text-muted-foreground' : 'text-foreground')}>
|
|
44
|
+
{'\u2212'}
|
|
45
|
+
</Text>
|
|
46
|
+
</Pressable>
|
|
47
|
+
<Text className="min-w-[2rem] text-center text-base font-semibold text-foreground">
|
|
48
|
+
{quantity}
|
|
49
|
+
</Text>
|
|
50
|
+
<Pressable
|
|
51
|
+
onPress={() => handleChange(quantity + 1)}
|
|
52
|
+
disabled={quantity >= max}
|
|
53
|
+
className={cn(
|
|
54
|
+
'h-8 w-8 items-center justify-center rounded-lg',
|
|
55
|
+
quantity >= max ? 'bg-muted/50' : 'bg-muted',
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
<Text className={cn('text-base font-semibold', quantity >= max ? 'text-muted-foreground' : 'text-foreground')}>
|
|
59
|
+
+
|
|
60
|
+
</Text>
|
|
61
|
+
</Pressable>
|
|
62
|
+
</View>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { useId, useState } from 'react';
|
|
2
|
+
import { TextInput, View, type TextInputProps } from 'react-native';
|
|
3
|
+
import { Label } from '@tallyui/primitives';
|
|
3
4
|
import { cn } from '@tallyui/theme';
|
|
5
|
+
import { SearchIcon, HStack } from '../ui';
|
|
4
6
|
|
|
5
7
|
export interface SearchInputProps extends Omit<TextInputProps, 'value' | 'onChangeText'> {
|
|
6
8
|
value: string;
|
|
@@ -8,19 +10,40 @@ export interface SearchInputProps extends Omit<TextInputProps, 'value' | 'onChan
|
|
|
8
10
|
className?: string;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
|
-
export function SearchInput({ value, onChangeText, className, placeholder = 'Search...', ...props }: SearchInputProps) {
|
|
13
|
+
export function SearchInput({ value, onChangeText, className, placeholder = 'Search...', style, onFocus, onBlur, ...props }: SearchInputProps) {
|
|
14
|
+
const labelId = useId();
|
|
15
|
+
const [focused, setFocused] = useState(false);
|
|
16
|
+
|
|
12
17
|
return (
|
|
13
|
-
<
|
|
14
|
-
|
|
18
|
+
<HStack space="sm" className={cn(
|
|
19
|
+
'rounded-lg border bg-card px-3 py-2.5',
|
|
20
|
+
focused ? 'border-ring' : 'border-border',
|
|
21
|
+
className,
|
|
22
|
+
)}>
|
|
23
|
+
<Label.Root nativeID={labelId} asChild>
|
|
24
|
+
<View aria-hidden className="text-muted-foreground">
|
|
25
|
+
<SearchIcon size={16} />
|
|
26
|
+
</View>
|
|
27
|
+
</Label.Root>
|
|
15
28
|
<TextInput
|
|
16
29
|
role="searchbox"
|
|
30
|
+
aria-labelledby={labelId}
|
|
17
31
|
value={value}
|
|
18
32
|
onChangeText={onChangeText}
|
|
19
33
|
placeholder={placeholder}
|
|
20
|
-
|
|
34
|
+
placeholderTextColorClassName="accent-muted-foreground"
|
|
21
35
|
className="flex-1 text-sm text-foreground"
|
|
36
|
+
style={[{ outlineStyle: 'none' } as any, style]}
|
|
37
|
+
onFocus={(event) => {
|
|
38
|
+
setFocused(true);
|
|
39
|
+
onFocus?.(event);
|
|
40
|
+
}}
|
|
41
|
+
onBlur={(event) => {
|
|
42
|
+
setFocused(false);
|
|
43
|
+
onBlur?.(event);
|
|
44
|
+
}}
|
|
22
45
|
{...props}
|
|
23
46
|
/>
|
|
24
|
-
</
|
|
47
|
+
</HStack>
|
|
25
48
|
);
|
|
26
49
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { ScrollView, View, useWindowDimensions, type ViewProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@tallyui/theme';
|
|
5
|
+
|
|
6
|
+
export interface CheckoutLayoutProps extends Omit<ViewProps, 'children'> {
|
|
7
|
+
summarySlot: ReactNode;
|
|
8
|
+
paymentSlot: ReactNode;
|
|
9
|
+
headerSlot?: ReactNode;
|
|
10
|
+
layout?: 'split' | 'stacked';
|
|
11
|
+
breakpoint?: number;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function CheckoutLayout({
|
|
16
|
+
summarySlot,
|
|
17
|
+
paymentSlot,
|
|
18
|
+
headerSlot,
|
|
19
|
+
layout: layoutProp,
|
|
20
|
+
breakpoint = 768,
|
|
21
|
+
className,
|
|
22
|
+
...viewProps
|
|
23
|
+
}: CheckoutLayoutProps) {
|
|
24
|
+
const { width } = useWindowDimensions();
|
|
25
|
+
const layout = layoutProp ?? (width >= breakpoint ? 'split' : 'stacked');
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<View className={cn('flex-1 bg-bg', className)} {...viewProps}>
|
|
29
|
+
{headerSlot && <View className="border-b border-border">{headerSlot}</View>}
|
|
30
|
+
|
|
31
|
+
{layout === 'split' ? (
|
|
32
|
+
<View className="flex-1 flex-row">
|
|
33
|
+
<View className="flex-1 border-r border-border">{summarySlot}</View>
|
|
34
|
+
<View className="flex-1">{paymentSlot}</View>
|
|
35
|
+
</View>
|
|
36
|
+
) : (
|
|
37
|
+
<ScrollView className="flex-1">
|
|
38
|
+
<View className="gap-4 p-4">
|
|
39
|
+
{summarySlot}
|
|
40
|
+
{paymentSlot}
|
|
41
|
+
</View>
|
|
42
|
+
</ScrollView>
|
|
43
|
+
)}
|
|
44
|
+
</View>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { POSLayout, type POSLayoutProps } from './pos-layout';
|
|
2
|
+
export { CheckoutLayout, type CheckoutLayoutProps } from './checkout-layout';
|
|
3
|
+
export { SettingsLayout, type SettingsLayoutProps } from './settings-layout';
|
|
4
|
+
export { LiveTabScreen, type LiveTabScreenProps, type LiveTabScreenState } from './live-tab-screen';
|
|
5
|
+
export { StoreSettingsChoiceScreen, type StoreSettingsChoiceScreenProps } from './store-settings-choice-screen';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Pressable, Text, View, type ViewProps } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@tallyui/theme';
|
|
4
|
+
|
|
5
|
+
/** Mirrors `LiveTabState` from `@tallyui/database`, without depending on that package. */
|
|
6
|
+
export type LiveTabScreenState = 'acquiring' | 'live' | 'parked' | 'blocked';
|
|
7
|
+
|
|
8
|
+
export interface LiveTabScreenProps extends Omit<ViewProps, 'children'> {
|
|
9
|
+
state: LiveTabScreenState;
|
|
10
|
+
/** Called when the parked screen's "Use here" button is pressed. */
|
|
11
|
+
onUseHere: () => void;
|
|
12
|
+
/** Called when the blocked screen's "Reload" button is pressed. */
|
|
13
|
+
onReload: () => void;
|
|
14
|
+
/** Title for both the parked and the blocked screen. */
|
|
15
|
+
parkedTitle?: string;
|
|
16
|
+
parkedBody?: string;
|
|
17
|
+
useHereLabel?: string;
|
|
18
|
+
blockedBody?: string;
|
|
19
|
+
reloadLabel?: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* ADR-061: what a tab shows once it is no longer the live one. Renders
|
|
25
|
+
* nothing for `acquiring` and `live`.
|
|
26
|
+
*/
|
|
27
|
+
export function LiveTabScreen({
|
|
28
|
+
state,
|
|
29
|
+
onUseHere,
|
|
30
|
+
onReload,
|
|
31
|
+
parkedTitle = 'POS is open in another tab',
|
|
32
|
+
parkedBody = 'This tab stopped so the other one can work. Use it here instead?',
|
|
33
|
+
useHereLabel = 'Use here',
|
|
34
|
+
blockedBody = 'The POS is open in another tab. Close that tab to use it here, or reload this one.',
|
|
35
|
+
reloadLabel = 'Reload',
|
|
36
|
+
className,
|
|
37
|
+
...viewProps
|
|
38
|
+
}: LiveTabScreenProps) {
|
|
39
|
+
if (state !== 'parked' && state !== 'blocked') return null;
|
|
40
|
+
|
|
41
|
+
const body = state === 'parked' ? parkedBody : blockedBody;
|
|
42
|
+
const buttonLabel = state === 'parked' ? useHereLabel : reloadLabel;
|
|
43
|
+
const onPress = state === 'parked' ? onUseHere : onReload;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<View className={cn('flex-1 items-center justify-center gap-4 bg-bg p-6', className)} {...viewProps}>
|
|
47
|
+
<Text className="text-center text-lg font-bold text-foreground">{parkedTitle}</Text>
|
|
48
|
+
<Text className="text-center text-sm text-muted-foreground">{body}</Text>
|
|
49
|
+
<Pressable onPress={onPress} className="items-center rounded-lg bg-primary px-4 py-3">
|
|
50
|
+
<Text className="text-sm font-semibold text-primary-foreground">{buttonLabel}</Text>
|
|
51
|
+
</Pressable>
|
|
52
|
+
</View>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useState, type ReactNode } from 'react';
|
|
2
|
+
import { Pressable, Text, View, useWindowDimensions, type ViewProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@tallyui/theme';
|
|
5
|
+
|
|
6
|
+
export interface POSLayoutProps extends Omit<ViewProps, 'children'> {
|
|
7
|
+
browseSlot: ReactNode;
|
|
8
|
+
cartSlot: ReactNode;
|
|
9
|
+
headerSlot?: ReactNode;
|
|
10
|
+
layout?: 'split' | 'stacked';
|
|
11
|
+
breakpoint?: number;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function POSLayout({
|
|
16
|
+
browseSlot,
|
|
17
|
+
cartSlot,
|
|
18
|
+
headerSlot,
|
|
19
|
+
layout: layoutProp,
|
|
20
|
+
breakpoint = 768,
|
|
21
|
+
className,
|
|
22
|
+
...viewProps
|
|
23
|
+
}: POSLayoutProps) {
|
|
24
|
+
const { width } = useWindowDimensions();
|
|
25
|
+
const layout = layoutProp ?? (width >= breakpoint ? 'split' : 'stacked');
|
|
26
|
+
const [activeTab, setActiveTab] = useState<'browse' | 'cart'>('browse');
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<View className={cn('flex-1 bg-bg', className)} {...viewProps}>
|
|
30
|
+
{headerSlot && <View className="border-b border-border">{headerSlot}</View>}
|
|
31
|
+
|
|
32
|
+
{layout === 'split' ? (
|
|
33
|
+
<View className="flex-1 flex-row">
|
|
34
|
+
<View className="flex-[3]">{browseSlot}</View>
|
|
35
|
+
<View className="flex-[2] border-l border-border">{cartSlot}</View>
|
|
36
|
+
</View>
|
|
37
|
+
) : (
|
|
38
|
+
<>
|
|
39
|
+
<View className="flex-row border-b border-border">
|
|
40
|
+
<Pressable
|
|
41
|
+
onPress={() => setActiveTab('browse')}
|
|
42
|
+
className={cn('flex-1 py-3', activeTab === 'browse' && 'border-b-2 border-primary')}
|
|
43
|
+
>
|
|
44
|
+
<Text className={cn('text-center text-sm font-semibold', activeTab === 'browse' ? 'text-primary' : 'text-muted-foreground')}>
|
|
45
|
+
Browse
|
|
46
|
+
</Text>
|
|
47
|
+
</Pressable>
|
|
48
|
+
<Pressable
|
|
49
|
+
onPress={() => setActiveTab('cart')}
|
|
50
|
+
className={cn('flex-1 py-3', activeTab === 'cart' && 'border-b-2 border-primary')}
|
|
51
|
+
>
|
|
52
|
+
<Text className={cn('text-center text-sm font-semibold', activeTab === 'cart' ? 'text-primary' : 'text-muted-foreground')}>
|
|
53
|
+
Cart
|
|
54
|
+
</Text>
|
|
55
|
+
</Pressable>
|
|
56
|
+
</View>
|
|
57
|
+
<View className="flex-1">
|
|
58
|
+
{activeTab === 'browse' ? browseSlot : cartSlot}
|
|
59
|
+
</View>
|
|
60
|
+
</>
|
|
61
|
+
)}
|
|
62
|
+
</View>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useState, type ReactNode } from 'react';
|
|
2
|
+
import { Pressable, Text, View, useWindowDimensions, type ViewProps } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@tallyui/theme';
|
|
5
|
+
|
|
6
|
+
export interface SettingsLayoutProps extends Omit<ViewProps, 'children'> {
|
|
7
|
+
navSlot: ReactNode;
|
|
8
|
+
contentSlot: ReactNode;
|
|
9
|
+
headerSlot?: ReactNode;
|
|
10
|
+
layout?: 'split' | 'stacked';
|
|
11
|
+
breakpoint?: number;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function SettingsLayout({
|
|
16
|
+
navSlot,
|
|
17
|
+
contentSlot,
|
|
18
|
+
headerSlot,
|
|
19
|
+
layout: layoutProp,
|
|
20
|
+
breakpoint = 768,
|
|
21
|
+
className,
|
|
22
|
+
...viewProps
|
|
23
|
+
}: SettingsLayoutProps) {
|
|
24
|
+
const { width } = useWindowDimensions();
|
|
25
|
+
const layout = layoutProp ?? (width >= breakpoint ? 'split' : 'stacked');
|
|
26
|
+
const [showNav, setShowNav] = useState(false);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<View className={cn('flex-1 bg-bg', className)} {...viewProps}>
|
|
30
|
+
{headerSlot && <View className="border-b border-border">{headerSlot}</View>}
|
|
31
|
+
|
|
32
|
+
{layout === 'split' ? (
|
|
33
|
+
<View className="flex-1 flex-row">
|
|
34
|
+
<View className="w-64 border-r border-border">{navSlot}</View>
|
|
35
|
+
<View className="flex-1">{contentSlot}</View>
|
|
36
|
+
</View>
|
|
37
|
+
) : (
|
|
38
|
+
<View className="flex-1">
|
|
39
|
+
{showNav ? (
|
|
40
|
+
<View className="flex-1">
|
|
41
|
+
{navSlot}
|
|
42
|
+
<Pressable onPress={() => setShowNav(false)} className="border-t border-border p-3">
|
|
43
|
+
<Text className="text-center text-sm font-medium text-primary">Done</Text>
|
|
44
|
+
</Pressable>
|
|
45
|
+
</View>
|
|
46
|
+
) : (
|
|
47
|
+
<View className="flex-1">
|
|
48
|
+
<Pressable onPress={() => setShowNav(true)} className="border-b border-border px-4 py-2">
|
|
49
|
+
<Text className="text-sm font-medium text-primary">Menu</Text>
|
|
50
|
+
</Pressable>
|
|
51
|
+
{contentSlot}
|
|
52
|
+
</View>
|
|
53
|
+
)}
|
|
54
|
+
</View>
|
|
55
|
+
)}
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
}
|