@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.
Files changed (137) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +1113 -83
  3. package/dist/index.js +4936 -353
  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 +16 -12
  21. package/src/customer/customer-form.tsx +72 -0
  22. package/src/customer/customer-order-history.tsx +34 -0
  23. package/src/customer/customer-picker.tsx +124 -0
  24. package/src/customer/customer-select.tsx +80 -31
  25. package/src/customer/index.ts +3 -0
  26. package/src/index.ts +114 -1
  27. package/src/input/filter-chip.tsx +85 -27
  28. package/src/input/index.ts +1 -0
  29. package/src/input/quantity-stepper.tsx +64 -0
  30. package/src/input/search-input.tsx +30 -7
  31. package/src/layout/checkout-layout.tsx +46 -0
  32. package/src/layout/index.ts +5 -0
  33. package/src/layout/live-tab-screen.tsx +54 -0
  34. package/src/layout/pos-layout.tsx +64 -0
  35. package/src/layout/settings-layout.tsx +58 -0
  36. package/src/layout/store-settings-choice-screen.tsx +142 -0
  37. package/src/order/index.ts +4 -0
  38. package/src/order/order-card.tsx +48 -0
  39. package/src/order/order-detail.tsx +82 -0
  40. package/src/order/order-list.tsx +39 -0
  41. package/src/order/order-status-badge.tsx +32 -0
  42. package/src/product/category-nav.tsx +70 -0
  43. package/src/product/index.ts +3 -0
  44. package/src/product/product-card.tsx +36 -8
  45. package/src/product/product-grid.tsx +46 -13
  46. package/src/product/product-image.tsx +28 -7
  47. package/src/product/product-list.tsx +44 -0
  48. package/src/product/product-price.tsx +77 -22
  49. package/src/product/product-sku.tsx +5 -5
  50. package/src/product/product-stock-badge.tsx +41 -28
  51. package/src/product/product-title.tsx +2 -3
  52. package/src/product/product-variant-picker.tsx +61 -0
  53. package/src/register/cash-count-input.tsx +85 -0
  54. package/src/register/closure-sheet.tsx +161 -0
  55. package/src/register/index.ts +12 -0
  56. package/src/register/movement-sheet.tsx +118 -0
  57. package/src/register/open-register-card.tsx +109 -0
  58. package/src/register/register-bar.helpers.ts +57 -0
  59. package/src/register/register-bar.tsx +85 -0
  60. package/src/register/register-column.tsx +117 -0
  61. package/src/register/register-count.tsx +330 -0
  62. package/src/register/register-open-close.tsx +52 -0
  63. package/src/register/register-panel.tsx +151 -0
  64. package/src/register/register-picker.tsx +60 -0
  65. package/src/register/register-summary.tsx +72 -0
  66. package/src/sale/cart-bar.tsx +18 -0
  67. package/src/sale/cart.tsx +66 -0
  68. package/src/sale/catalogue.tsx +139 -0
  69. package/src/sale/discount-form.tsx +85 -0
  70. package/src/sale/index.ts +9 -0
  71. package/src/sale/orders-list.tsx +108 -0
  72. package/src/sale/print-style.ts +11 -0
  73. package/src/sale/receipt.tsx +62 -0
  74. package/src/sale/sync-status.tsx +141 -0
  75. package/src/sale/tender.tsx +38 -0
  76. package/src/settings/connector-status.tsx +65 -0
  77. package/src/settings/index.ts +3 -0
  78. package/src/settings/settings-group.tsx +23 -0
  79. package/src/settings/settings-row.tsx +23 -0
  80. package/src/ui/accordion/index.tsx +59 -0
  81. package/src/ui/alert-dialog/index.tsx +91 -0
  82. package/src/ui/avatar/index.tsx +50 -0
  83. package/src/ui/badge/index.tsx +60 -0
  84. package/src/ui/button/index.tsx +90 -0
  85. package/src/ui/card/index.tsx +60 -0
  86. package/src/ui/checkbox/index.tsx +29 -0
  87. package/src/ui/collapsible/index.tsx +24 -0
  88. package/src/ui/combobox/index.tsx +98 -0
  89. package/src/ui/context-menu/index.tsx +159 -0
  90. package/src/ui/dialog/index.tsx +92 -0
  91. package/src/ui/dropdown-menu/index.tsx +161 -0
  92. package/src/ui/hover-card/index.tsx +31 -0
  93. package/src/ui/hstack/index.tsx +43 -0
  94. package/src/ui/icon/index.tsx +44 -0
  95. package/src/ui/icon/search-icon.tsx +9 -0
  96. package/src/ui/icon/svg-icon.tsx +36 -0
  97. package/src/ui/icon-button/index.tsx +58 -0
  98. package/src/ui/index.ts +99 -0
  99. package/src/ui/input/index.tsx +100 -0
  100. package/src/ui/label/index.tsx +26 -0
  101. package/src/ui/loader/index.tsx +37 -0
  102. package/src/ui/popover/index.tsx +31 -0
  103. package/src/ui/progress/index.tsx +28 -0
  104. package/src/ui/radio-group/index.tsx +38 -0
  105. package/src/ui/select/index.tsx +96 -0
  106. package/src/ui/separator/index.tsx +29 -0
  107. package/src/ui/slider/index.tsx +28 -0
  108. package/src/ui/switch/index.tsx +31 -0
  109. package/src/ui/table/index.tsx +59 -0
  110. package/src/ui/tabs/index.tsx +53 -0
  111. package/src/ui/text/index.tsx +36 -0
  112. package/src/ui/textarea/index.tsx +29 -0
  113. package/src/ui/toast/index.tsx +52 -0
  114. package/src/ui/toggle/index.tsx +69 -0
  115. package/src/ui/toggle-group/index.tsx +66 -0
  116. package/src/ui/tooltip/index.tsx +30 -0
  117. package/src/ui/vstack/index.tsx +43 -0
  118. package/src/uniwind-env.d.ts +9 -1
  119. package/src/__tests__/cart-line.test.tsx +0 -53
  120. package/src/__tests__/cart-panel.test.tsx +0 -55
  121. package/src/__tests__/cart-total.test.tsx +0 -60
  122. package/src/__tests__/customer-card.test.tsx +0 -47
  123. package/src/__tests__/customer-select.test.tsx +0 -33
  124. package/src/__tests__/filter-chip.test.tsx +0 -40
  125. package/src/__tests__/helpers.ts +0 -111
  126. package/src/__tests__/order-summary-line.test.tsx +0 -18
  127. package/src/__tests__/order-summary.test.tsx +0 -32
  128. package/src/__tests__/payment-method-card.test.tsx +0 -24
  129. package/src/__tests__/payment-selector.test.tsx +0 -23
  130. package/src/__tests__/product-card.test.tsx +0 -31
  131. package/src/__tests__/product-grid.test.tsx +0 -37
  132. package/src/__tests__/product-image.test.tsx +0 -54
  133. package/src/__tests__/product-price.test.tsx +0 -68
  134. package/src/__tests__/product-sku.test.tsx +0 -47
  135. package/src/__tests__/product-stock-badge.test.tsx +0 -58
  136. package/src/__tests__/product-title.test.tsx +0 -38
  137. 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 { Pressable, ScrollView, Text, type ViewProps } from 'react-native';
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
- <Pressable
15
- onPress={onPress}
16
- className={cn(
17
- 'rounded-full px-3 py-1.5',
18
- active ? 'bg-primary' : 'bg-surface-alt',
19
- className,
20
- )}
22
+ <ToggleGroup.Root
23
+ type="multiple"
24
+ value={value}
25
+ onValueChange={() => onPress()}
21
26
  >
22
- <Text className={cn('text-xs font-semibold', active ? 'text-primary-foreground' : 'text-muted')}>
23
- {label}
24
- </Text>
25
- </Pressable>
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
- <ScrollView
43
- horizontal
44
- showsHorizontalScrollIndicator={false}
45
- contentContainerClassName={cn('flex-row gap-2', className)}
46
- {...viewProps}
87
+ <ToggleGroup.Root
88
+ type="multiple"
89
+ value={activeValues}
90
+ onValueChange={handleValueChange}
91
+ asChild
47
92
  >
48
- {chips.map((chip, index) => (
49
- <FilterChip
50
- key={chip.label}
51
- label={chip.label}
52
- active={chip.active}
53
- onPress={() => onChipPress(index)}
54
- />
55
- ))}
56
- </ScrollView>
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
  }
@@ -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 { TextInput, View, Text, type TextInputProps } from 'react-native';
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
- <View className={cn('flex-row items-center gap-2 rounded-lg border border-border bg-surface px-3 py-2', className)}>
14
- <Text className="text-muted">{'\u2315'}</Text>
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
- placeholderTextColor="#9ca3af"
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
- </View>
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
+ }