@tallyui/components 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +364 -0
- package/dist/index.js +451 -0
- package/package.json +35 -0
- package/src/__tests__/cart-line.test.tsx +53 -0
- package/src/__tests__/cart-panel.test.tsx +55 -0
- package/src/__tests__/cart-total.test.tsx +60 -0
- package/src/__tests__/customer-card.test.tsx +47 -0
- package/src/__tests__/customer-select.test.tsx +33 -0
- package/src/__tests__/filter-chip.test.tsx +40 -0
- package/src/__tests__/helpers.ts +111 -0
- package/src/__tests__/order-summary-line.test.tsx +18 -0
- package/src/__tests__/order-summary.test.tsx +32 -0
- package/src/__tests__/payment-method-card.test.tsx +24 -0
- package/src/__tests__/payment-selector.test.tsx +23 -0
- package/src/__tests__/product-card.test.tsx +31 -0
- package/src/__tests__/product-grid.test.tsx +37 -0
- package/src/__tests__/product-image.test.tsx +54 -0
- package/src/__tests__/product-price.test.tsx +68 -0
- package/src/__tests__/product-sku.test.tsx +47 -0
- package/src/__tests__/product-stock-badge.test.tsx +58 -0
- package/src/__tests__/product-title.test.tsx +38 -0
- package/src/__tests__/search-input.test.tsx +22 -0
- package/src/cart/cart-line.tsx +49 -0
- package/src/cart/cart-panel.tsx +65 -0
- package/src/cart/cart-total.tsx +64 -0
- package/src/cart/index.ts +3 -0
- package/src/checkout/index.ts +4 -0
- package/src/checkout/order-summary-line.tsx +23 -0
- package/src/checkout/order-summary.tsx +77 -0
- package/src/checkout/payment-method-card.tsx +36 -0
- package/src/checkout/payment-selector.tsx +55 -0
- package/src/customer/customer-card.tsx +26 -0
- package/src/customer/customer-select.tsx +72 -0
- package/src/customer/index.ts +2 -0
- package/src/index.ts +61 -0
- package/src/input/filter-chip.tsx +58 -0
- package/src/input/index.ts +2 -0
- package/src/input/search-input.tsx +26 -0
- package/src/product/index.ts +7 -0
- package/src/product/product-card.tsx +28 -0
- package/src/product/product-grid.tsx +66 -0
- package/src/product/product-image.tsx +41 -0
- package/src/product/product-price.tsx +53 -0
- package/src/product/product-sku.tsx +33 -0
- package/src/product/product-stock-badge.tsx +73 -0
- package/src/product/product-title.tsx +30 -0
- package/src/uniwind-env.d.ts +31 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
// src/product/product-title.tsx
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
import { useProductTraits } from "@tallyui/core";
|
|
4
|
+
import { cn } from "@tallyui/theme";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
function ProductTitle({ doc, className, ...textProps }) {
|
|
7
|
+
const { getName } = useProductTraits();
|
|
8
|
+
return /* @__PURE__ */ jsx(Text, { className: cn("text-base font-semibold text-foreground", className), ...textProps, children: getName(doc) });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/product/product-price.tsx
|
|
12
|
+
import { Text as Text2 } from "react-native";
|
|
13
|
+
import { useProductTraits as useProductTraits2 } from "@tallyui/core";
|
|
14
|
+
import { cn as cn2 } from "@tallyui/theme";
|
|
15
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
16
|
+
function ProductPrice({ doc, currencySymbol = "$", className, ...textProps }) {
|
|
17
|
+
const { getPrice, isOnSale, getSalePrice, getRegularPrice } = useProductTraits2();
|
|
18
|
+
const price = getPrice(doc);
|
|
19
|
+
if (!price) {
|
|
20
|
+
return /* @__PURE__ */ jsx2(Text2, { className: cn2("text-sm text-muted", className), ...textProps, children: "-" });
|
|
21
|
+
}
|
|
22
|
+
if (isOnSale(doc)) {
|
|
23
|
+
const salePrice = getSalePrice(doc);
|
|
24
|
+
const regularPrice = getRegularPrice(doc);
|
|
25
|
+
return /* @__PURE__ */ jsxs(Text2, { className: cn2("text-sm font-medium text-sale", className), ...textProps, children: [
|
|
26
|
+
currencySymbol,
|
|
27
|
+
salePrice ?? price,
|
|
28
|
+
regularPrice ? ` (was ${currencySymbol}${regularPrice})` : ""
|
|
29
|
+
] });
|
|
30
|
+
}
|
|
31
|
+
return /* @__PURE__ */ jsxs(Text2, { className: cn2("text-sm font-medium text-price", className), ...textProps, children: [
|
|
32
|
+
currencySymbol,
|
|
33
|
+
price
|
|
34
|
+
] });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/product/product-image.tsx
|
|
38
|
+
import { Image } from "react-native";
|
|
39
|
+
import { useProductTraits as useProductTraits3 } from "@tallyui/core";
|
|
40
|
+
import { cn as cn3 } from "@tallyui/theme";
|
|
41
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
42
|
+
function ProductImage({ doc, size = 60, style, className, ...imageProps }) {
|
|
43
|
+
const { getImageUrl } = useProductTraits3();
|
|
44
|
+
const imageUrl = getImageUrl(doc);
|
|
45
|
+
if (!imageUrl) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return /* @__PURE__ */ jsx3(
|
|
49
|
+
Image,
|
|
50
|
+
{
|
|
51
|
+
source: { uri: imageUrl },
|
|
52
|
+
className: cn3("rounded", className),
|
|
53
|
+
style: [{ width: size, height: size }, style],
|
|
54
|
+
...imageProps
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/product/product-sku.tsx
|
|
60
|
+
import { Text as Text3 } from "react-native";
|
|
61
|
+
import { useProductTraits as useProductTraits4 } from "@tallyui/core";
|
|
62
|
+
import { cn as cn4 } from "@tallyui/theme";
|
|
63
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
64
|
+
function ProductSku({ doc, fallback = "\u2014", className, ...textProps }) {
|
|
65
|
+
const { getSku } = useProductTraits4();
|
|
66
|
+
const sku = getSku(doc);
|
|
67
|
+
return /* @__PURE__ */ jsx4(Text3, { className: cn4("text-xs text-muted", className), ...textProps, children: sku ?? fallback });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/product/product-stock-badge.tsx
|
|
71
|
+
import { Text as Text4, View } from "react-native";
|
|
72
|
+
import { useProductTraits as useProductTraits5 } from "@tallyui/core";
|
|
73
|
+
import { cn as cn5 } from "@tallyui/theme";
|
|
74
|
+
import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
75
|
+
var statusLabels = {
|
|
76
|
+
instock: "In Stock",
|
|
77
|
+
outofstock: "Out of Stock",
|
|
78
|
+
onbackorder: "On Backorder",
|
|
79
|
+
unknown: "Unknown"
|
|
80
|
+
};
|
|
81
|
+
var STATUS_STYLES = {
|
|
82
|
+
instock: {
|
|
83
|
+
badge: "bg-success/15",
|
|
84
|
+
dot: "bg-success",
|
|
85
|
+
text: "text-success"
|
|
86
|
+
},
|
|
87
|
+
outofstock: {
|
|
88
|
+
badge: "bg-danger/15",
|
|
89
|
+
dot: "bg-danger",
|
|
90
|
+
text: "text-danger"
|
|
91
|
+
},
|
|
92
|
+
onbackorder: {
|
|
93
|
+
badge: "bg-warning/15",
|
|
94
|
+
dot: "bg-warning",
|
|
95
|
+
text: "text-warning"
|
|
96
|
+
},
|
|
97
|
+
unknown: {
|
|
98
|
+
badge: "bg-muted/15",
|
|
99
|
+
dot: "bg-muted",
|
|
100
|
+
text: "text-muted"
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
function ProductStockBadge({ doc, showQuantity = false, className, ...viewProps }) {
|
|
104
|
+
const { getStockStatus, getStockQuantity } = useProductTraits5();
|
|
105
|
+
const status = getStockStatus(doc);
|
|
106
|
+
const quantity = getStockQuantity(doc);
|
|
107
|
+
const label = statusLabels[status] ?? statusLabels.unknown;
|
|
108
|
+
const styles = STATUS_STYLES[status] ?? STATUS_STYLES.unknown;
|
|
109
|
+
return /* @__PURE__ */ jsxs2(
|
|
110
|
+
View,
|
|
111
|
+
{
|
|
112
|
+
className: cn5("flex-row items-center self-start gap-1.5 rounded-xl px-2 py-1", styles.badge, className),
|
|
113
|
+
...viewProps,
|
|
114
|
+
children: [
|
|
115
|
+
/* @__PURE__ */ jsx5(View, { className: cn5("h-1.5 w-1.5 rounded-full", styles.dot) }),
|
|
116
|
+
/* @__PURE__ */ jsxs2(Text4, { className: cn5("text-xs font-semibold", styles.text), children: [
|
|
117
|
+
label,
|
|
118
|
+
showQuantity && quantity != null ? ` (${quantity})` : ""
|
|
119
|
+
] })
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/product/product-card.tsx
|
|
126
|
+
import { Pressable, View as View2 } from "react-native";
|
|
127
|
+
import { cn as cn6 } from "@tallyui/theme";
|
|
128
|
+
import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
129
|
+
function ProductCard({ doc, onPress, imageSize = 80, currencySymbol, className, ...viewProps }) {
|
|
130
|
+
const content = /* @__PURE__ */ jsxs3(View2, { className: cn6("items-center gap-2 rounded-lg bg-surface p-3", className), ...viewProps, children: [
|
|
131
|
+
/* @__PURE__ */ jsx6(ProductImage, { doc, size: imageSize, className: "rounded-md" }),
|
|
132
|
+
/* @__PURE__ */ jsx6(ProductTitle, { doc, className: "text-sm", numberOfLines: 2 }),
|
|
133
|
+
/* @__PURE__ */ jsx6(ProductPrice, { doc, currencySymbol })
|
|
134
|
+
] });
|
|
135
|
+
if (onPress) {
|
|
136
|
+
return /* @__PURE__ */ jsx6(Pressable, { onPress, children: content });
|
|
137
|
+
}
|
|
138
|
+
return content;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/product/product-grid.tsx
|
|
142
|
+
import { ScrollView, View as View3 } from "react-native";
|
|
143
|
+
import { cn as cn7 } from "@tallyui/theme";
|
|
144
|
+
import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
145
|
+
function ProductGrid({
|
|
146
|
+
items,
|
|
147
|
+
renderItem,
|
|
148
|
+
numColumns = 2,
|
|
149
|
+
searchSlot,
|
|
150
|
+
filterSlot,
|
|
151
|
+
emptyState,
|
|
152
|
+
className,
|
|
153
|
+
...viewProps
|
|
154
|
+
}) {
|
|
155
|
+
const hasItems = items.length > 0;
|
|
156
|
+
return /* @__PURE__ */ jsxs4(View3, { className: cn7("flex-1", className), ...viewProps, children: [
|
|
157
|
+
searchSlot && /* @__PURE__ */ jsx7(View3, { className: "px-3 py-2", children: searchSlot }),
|
|
158
|
+
filterSlot && /* @__PURE__ */ jsx7(View3, { className: "px-3 pb-2", children: filterSlot }),
|
|
159
|
+
hasItems ? /* @__PURE__ */ jsx7(ScrollView, { contentContainerClassName: "flex-row flex-wrap p-1", children: items.map((item, index) => /* @__PURE__ */ jsx7(View3, { style: { width: `${100 / numColumns}%` }, className: "p-1", children: renderItem(item, index) }, item.id ?? index)) }) : emptyState ?? null
|
|
160
|
+
] });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/cart/cart-line.tsx
|
|
164
|
+
import { Text as Text5, View as View4 } from "react-native";
|
|
165
|
+
import { useProductTraits as useProductTraits6 } from "@tallyui/core";
|
|
166
|
+
import { cn as cn8 } from "@tallyui/theme";
|
|
167
|
+
import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
168
|
+
function CartLine({ item, currencySymbol = "$", className, ...viewProps }) {
|
|
169
|
+
const { getName, getPrice } = useProductTraits6();
|
|
170
|
+
const name = getName(item.doc);
|
|
171
|
+
const unitPrice = getPrice(item.doc);
|
|
172
|
+
const lineTotal = unitPrice ? (parseFloat(unitPrice) * item.quantity).toFixed(2) : null;
|
|
173
|
+
return /* @__PURE__ */ jsxs5(View4, { className: cn8("flex-row items-center gap-3 px-3 py-2.5", className), ...viewProps, children: [
|
|
174
|
+
/* @__PURE__ */ jsxs5(View4, { className: "flex-1 gap-0.5", children: [
|
|
175
|
+
/* @__PURE__ */ jsx8(Text5, { className: "text-sm font-medium text-foreground", numberOfLines: 1, children: name }),
|
|
176
|
+
/* @__PURE__ */ jsx8(Text5, { className: "text-xs text-muted", children: unitPrice ? `${currencySymbol}${unitPrice} \xD7 ${item.quantity}` : `\xD7 ${item.quantity}` })
|
|
177
|
+
] }),
|
|
178
|
+
/* @__PURE__ */ jsx8(Text5, { className: "text-sm font-semibold text-foreground", children: lineTotal ? `${currencySymbol}${lineTotal}` : "\u2014" })
|
|
179
|
+
] });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/cart/cart-total.tsx
|
|
183
|
+
import { Text as Text6, View as View5 } from "react-native";
|
|
184
|
+
import { useProductTraits as useProductTraits7 } from "@tallyui/core";
|
|
185
|
+
import { cn as cn9 } from "@tallyui/theme";
|
|
186
|
+
import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
187
|
+
function CartTotal({
|
|
188
|
+
items,
|
|
189
|
+
currencySymbol = "$",
|
|
190
|
+
taxRate = 0,
|
|
191
|
+
taxLabel = "Tax",
|
|
192
|
+
className,
|
|
193
|
+
...viewProps
|
|
194
|
+
}) {
|
|
195
|
+
const { getPrice } = useProductTraits7();
|
|
196
|
+
const subtotal = items.reduce((sum, item) => {
|
|
197
|
+
const price = getPrice(item.doc);
|
|
198
|
+
return sum + (price ? parseFloat(price) * item.quantity : 0);
|
|
199
|
+
}, 0);
|
|
200
|
+
const tax = subtotal * taxRate;
|
|
201
|
+
const total = subtotal + tax;
|
|
202
|
+
return /* @__PURE__ */ jsxs6(View5, { className: cn9("gap-1.5 px-3 py-3", className), ...viewProps, children: [
|
|
203
|
+
/* @__PURE__ */ jsxs6(View5, { className: "flex-row items-center justify-between", children: [
|
|
204
|
+
/* @__PURE__ */ jsx9(Text6, { className: "text-sm text-muted", children: "Subtotal" }),
|
|
205
|
+
/* @__PURE__ */ jsxs6(Text6, { className: "text-sm text-foreground", children: [
|
|
206
|
+
currencySymbol,
|
|
207
|
+
subtotal.toFixed(2)
|
|
208
|
+
] })
|
|
209
|
+
] }),
|
|
210
|
+
taxRate > 0 && /* @__PURE__ */ jsxs6(View5, { className: "flex-row items-center justify-between", children: [
|
|
211
|
+
/* @__PURE__ */ jsx9(Text6, { className: "text-sm text-muted", children: taxLabel }),
|
|
212
|
+
/* @__PURE__ */ jsxs6(Text6, { className: "text-sm text-foreground", children: [
|
|
213
|
+
currencySymbol,
|
|
214
|
+
tax.toFixed(2)
|
|
215
|
+
] })
|
|
216
|
+
] }),
|
|
217
|
+
/* @__PURE__ */ jsxs6(View5, { className: "mt-1.5 flex-row items-center justify-between border-t border-border pt-2", children: [
|
|
218
|
+
/* @__PURE__ */ jsx9(Text6, { className: "text-base font-bold text-foreground", children: "Total" }),
|
|
219
|
+
/* @__PURE__ */ jsxs6(Text6, { className: "text-base font-bold text-foreground", children: [
|
|
220
|
+
currencySymbol,
|
|
221
|
+
total.toFixed(2)
|
|
222
|
+
] })
|
|
223
|
+
] })
|
|
224
|
+
] });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/cart/cart-panel.tsx
|
|
228
|
+
import { ScrollView as ScrollView2, View as View6 } from "react-native";
|
|
229
|
+
import { cn as cn10 } from "@tallyui/theme";
|
|
230
|
+
import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
231
|
+
function CartPanel({
|
|
232
|
+
items,
|
|
233
|
+
renderItem,
|
|
234
|
+
header,
|
|
235
|
+
footer,
|
|
236
|
+
emptyState,
|
|
237
|
+
className,
|
|
238
|
+
...viewProps
|
|
239
|
+
}) {
|
|
240
|
+
const hasItems = items.length > 0;
|
|
241
|
+
return /* @__PURE__ */ jsxs7(View6, { className: cn10("flex-1", className), ...viewProps, children: [
|
|
242
|
+
header && /* @__PURE__ */ jsx10(View6, { className: "border-b border-border px-3 py-2", children: header }),
|
|
243
|
+
hasItems ? /* @__PURE__ */ jsx10(ScrollView2, { className: "flex-1", children: items.map((item, index) => /* @__PURE__ */ jsx10(View6, { children: renderItem(item, index) }, item.doc?.id ?? index)) }) : emptyState ?? null,
|
|
244
|
+
footer && /* @__PURE__ */ jsx10(View6, { className: "border-t border-border px-3 py-2", children: footer })
|
|
245
|
+
] });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/input/search-input.tsx
|
|
249
|
+
import { TextInput, View as View7, Text as Text7 } from "react-native";
|
|
250
|
+
import { cn as cn11 } from "@tallyui/theme";
|
|
251
|
+
import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
252
|
+
function SearchInput({ value, onChangeText, className, placeholder = "Search...", ...props }) {
|
|
253
|
+
return /* @__PURE__ */ jsxs8(View7, { className: cn11("flex-row items-center gap-2 rounded-lg border border-border bg-surface px-3 py-2", className), children: [
|
|
254
|
+
/* @__PURE__ */ jsx11(Text7, { className: "text-muted", children: "\u2315" }),
|
|
255
|
+
/* @__PURE__ */ jsx11(
|
|
256
|
+
TextInput,
|
|
257
|
+
{
|
|
258
|
+
role: "searchbox",
|
|
259
|
+
value,
|
|
260
|
+
onChangeText,
|
|
261
|
+
placeholder,
|
|
262
|
+
placeholderTextColor: "#9ca3af",
|
|
263
|
+
className: "flex-1 text-sm text-foreground",
|
|
264
|
+
...props
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
] });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/input/filter-chip.tsx
|
|
271
|
+
import { Pressable as Pressable2, ScrollView as ScrollView3, Text as Text8 } from "react-native";
|
|
272
|
+
import { cn as cn12 } from "@tallyui/theme";
|
|
273
|
+
import { jsx as jsx12 } from "react/jsx-runtime";
|
|
274
|
+
function FilterChip({ label, active, onPress, className }) {
|
|
275
|
+
return /* @__PURE__ */ jsx12(
|
|
276
|
+
Pressable2,
|
|
277
|
+
{
|
|
278
|
+
onPress,
|
|
279
|
+
className: cn12(
|
|
280
|
+
"rounded-full px-3 py-1.5",
|
|
281
|
+
active ? "bg-primary" : "bg-surface-alt",
|
|
282
|
+
className
|
|
283
|
+
),
|
|
284
|
+
children: /* @__PURE__ */ jsx12(Text8, { className: cn12("text-xs font-semibold", active ? "text-primary-foreground" : "text-muted"), children: label })
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
function FilterChipGroup({ chips, onChipPress, className, ...viewProps }) {
|
|
289
|
+
return /* @__PURE__ */ jsx12(
|
|
290
|
+
ScrollView3,
|
|
291
|
+
{
|
|
292
|
+
horizontal: true,
|
|
293
|
+
showsHorizontalScrollIndicator: false,
|
|
294
|
+
contentContainerClassName: cn12("flex-row gap-2", className),
|
|
295
|
+
...viewProps,
|
|
296
|
+
children: chips.map((chip, index) => /* @__PURE__ */ jsx12(
|
|
297
|
+
FilterChip,
|
|
298
|
+
{
|
|
299
|
+
label: chip.label,
|
|
300
|
+
active: chip.active,
|
|
301
|
+
onPress: () => onChipPress(index)
|
|
302
|
+
},
|
|
303
|
+
chip.label
|
|
304
|
+
))
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/customer/customer-card.tsx
|
|
310
|
+
import { Text as Text9, View as View8 } from "react-native";
|
|
311
|
+
import { useCustomerTraits } from "@tallyui/core";
|
|
312
|
+
import { cn as cn13 } from "@tallyui/theme";
|
|
313
|
+
import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
314
|
+
function CustomerCard({ doc, className, ...viewProps }) {
|
|
315
|
+
const traits = useCustomerTraits();
|
|
316
|
+
if (!traits) return null;
|
|
317
|
+
const name = traits.getName(doc);
|
|
318
|
+
const email = traits.getEmail(doc);
|
|
319
|
+
const address = traits.getAddressSummary(doc);
|
|
320
|
+
return /* @__PURE__ */ jsxs9(View8, { className: cn13("gap-0.5", className), ...viewProps, children: [
|
|
321
|
+
/* @__PURE__ */ jsx13(Text9, { className: "text-sm font-semibold text-foreground", children: name }),
|
|
322
|
+
email && /* @__PURE__ */ jsx13(Text9, { className: "text-xs text-muted", children: email }),
|
|
323
|
+
address && /* @__PURE__ */ jsx13(Text9, { className: "text-xs text-muted", children: address })
|
|
324
|
+
] });
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/customer/customer-select.tsx
|
|
328
|
+
import { Pressable as Pressable3, ScrollView as ScrollView4, View as View9 } from "react-native";
|
|
329
|
+
import { useCustomerTraits as useCustomerTraits2 } from "@tallyui/core";
|
|
330
|
+
import { cn as cn14 } from "@tallyui/theme";
|
|
331
|
+
import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
332
|
+
function CustomerSelect({
|
|
333
|
+
customers,
|
|
334
|
+
selected,
|
|
335
|
+
onSelect,
|
|
336
|
+
onSearch,
|
|
337
|
+
placeholder = "Search customers...",
|
|
338
|
+
className,
|
|
339
|
+
...viewProps
|
|
340
|
+
}) {
|
|
341
|
+
const traits = useCustomerTraits2();
|
|
342
|
+
return /* @__PURE__ */ jsxs10(View9, { className: cn14("gap-2", className), ...viewProps, children: [
|
|
343
|
+
selected && traits && /* @__PURE__ */ jsx14(View9, { className: "rounded-lg border border-primary bg-primary/5 px-3 py-2", children: /* @__PURE__ */ jsx14(CustomerCard, { doc: selected }) }),
|
|
344
|
+
customers.length > 0 && /* @__PURE__ */ jsx14(ScrollView4, { className: "max-h-60", children: customers.map((customer, index) => {
|
|
345
|
+
const id = traits?.getId(customer) ?? index;
|
|
346
|
+
return /* @__PURE__ */ jsx14(Pressable3, { onPress: () => onSelect(customer), children: /* @__PURE__ */ jsx14(View9, { className: "border-b border-border px-3 py-2", children: /* @__PURE__ */ jsx14(CustomerCard, { doc: customer }) }) }, id);
|
|
347
|
+
}) })
|
|
348
|
+
] });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/checkout/payment-method-card.tsx
|
|
352
|
+
import { Pressable as Pressable4, Text as Text11, View as View10 } from "react-native";
|
|
353
|
+
import { cn as cn15 } from "@tallyui/theme";
|
|
354
|
+
import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
355
|
+
function PaymentMethodCard({ label, selected, onPress, icon, className, ...viewProps }) {
|
|
356
|
+
return /* @__PURE__ */ jsx15(Pressable4, { onPress, ...viewProps, children: /* @__PURE__ */ jsxs11(
|
|
357
|
+
View10,
|
|
358
|
+
{
|
|
359
|
+
className: cn15(
|
|
360
|
+
"flex-row items-center gap-3 rounded-lg border px-4 py-3",
|
|
361
|
+
selected ? "border-primary bg-primary/5" : "border-border bg-surface",
|
|
362
|
+
className
|
|
363
|
+
),
|
|
364
|
+
children: [
|
|
365
|
+
icon && /* @__PURE__ */ jsx15(View10, { children: icon }),
|
|
366
|
+
/* @__PURE__ */ jsx15(Text11, { className: cn15("flex-1 text-sm font-medium", selected ? "text-primary" : "text-foreground"), children: label }),
|
|
367
|
+
selected && /* @__PURE__ */ jsx15(View10, { className: "h-5 w-5 items-center justify-center rounded-full bg-primary", children: /* @__PURE__ */ jsx15(Text11, { className: "text-xs text-primary-foreground", children: "\u2713" }) })
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
) });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/checkout/order-summary-line.tsx
|
|
374
|
+
import { Text as Text12, View as View11 } from "react-native";
|
|
375
|
+
import { cn as cn16 } from "@tallyui/theme";
|
|
376
|
+
import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
377
|
+
function OrderSummaryLine({ label, amount, bold = false, className, ...viewProps }) {
|
|
378
|
+
return /* @__PURE__ */ jsxs12(View11, { className: cn16("flex-row items-center justify-between", className), ...viewProps, children: [
|
|
379
|
+
/* @__PURE__ */ jsx16(Text12, { className: cn16("text-sm", bold ? "font-bold text-foreground" : "text-muted"), children: label }),
|
|
380
|
+
/* @__PURE__ */ jsx16(Text12, { className: cn16("text-sm", bold ? "font-bold text-foreground" : "text-foreground"), children: amount })
|
|
381
|
+
] });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/checkout/payment-selector.tsx
|
|
385
|
+
import { View as View12 } from "react-native";
|
|
386
|
+
import { cn as cn17 } from "@tallyui/theme";
|
|
387
|
+
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
388
|
+
function PaymentSelector({
|
|
389
|
+
methods,
|
|
390
|
+
selected,
|
|
391
|
+
onSelect,
|
|
392
|
+
className,
|
|
393
|
+
...viewProps
|
|
394
|
+
}) {
|
|
395
|
+
return /* @__PURE__ */ jsx17(View12, { className: cn17("gap-2", className), ...viewProps, children: methods.map((method) => /* @__PURE__ */ jsx17(
|
|
396
|
+
PaymentMethodCard,
|
|
397
|
+
{
|
|
398
|
+
label: method.label,
|
|
399
|
+
selected: method.id === selected,
|
|
400
|
+
onPress: () => onSelect(method.id),
|
|
401
|
+
icon: method.icon
|
|
402
|
+
},
|
|
403
|
+
method.id
|
|
404
|
+
)) });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/checkout/order-summary.tsx
|
|
408
|
+
import { View as View13 } from "react-native";
|
|
409
|
+
import { cn as cn18 } from "@tallyui/theme";
|
|
410
|
+
import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
411
|
+
function OrderSummary({
|
|
412
|
+
subtotal,
|
|
413
|
+
tax,
|
|
414
|
+
taxLabel = "Tax",
|
|
415
|
+
total,
|
|
416
|
+
payments,
|
|
417
|
+
header,
|
|
418
|
+
footer,
|
|
419
|
+
className,
|
|
420
|
+
...viewProps
|
|
421
|
+
}) {
|
|
422
|
+
return /* @__PURE__ */ jsxs13(View13, { className: cn18("gap-1.5 px-3 py-3", className), ...viewProps, children: [
|
|
423
|
+
header && /* @__PURE__ */ jsx18(View13, { className: "mb-2", children: header }),
|
|
424
|
+
/* @__PURE__ */ jsx18(OrderSummaryLine, { label: "Subtotal", amount: subtotal }),
|
|
425
|
+
tax && /* @__PURE__ */ jsx18(OrderSummaryLine, { label: taxLabel, amount: tax }),
|
|
426
|
+
/* @__PURE__ */ jsx18(OrderSummaryLine, { label: "Total", amount: total, bold: true }),
|
|
427
|
+
payments && payments.length > 0 && /* @__PURE__ */ jsx18(View13, { className: "mt-2 gap-1.5 border-t border-border pt-2", children: payments.map((payment) => /* @__PURE__ */ jsx18(OrderSummaryLine, { label: payment.label, amount: payment.amount }, payment.label)) }),
|
|
428
|
+
footer && /* @__PURE__ */ jsx18(View13, { className: "mt-2", children: footer })
|
|
429
|
+
] });
|
|
430
|
+
}
|
|
431
|
+
export {
|
|
432
|
+
CartLine,
|
|
433
|
+
CartPanel,
|
|
434
|
+
CartTotal,
|
|
435
|
+
CustomerCard,
|
|
436
|
+
CustomerSelect,
|
|
437
|
+
FilterChip,
|
|
438
|
+
FilterChipGroup,
|
|
439
|
+
OrderSummary,
|
|
440
|
+
OrderSummaryLine,
|
|
441
|
+
PaymentMethodCard,
|
|
442
|
+
PaymentSelector,
|
|
443
|
+
ProductCard,
|
|
444
|
+
ProductGrid,
|
|
445
|
+
ProductImage,
|
|
446
|
+
ProductPrice,
|
|
447
|
+
ProductSku,
|
|
448
|
+
ProductStockBadge,
|
|
449
|
+
ProductTitle,
|
|
450
|
+
SearchInput
|
|
451
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tallyui/components",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Universal POS UI components for Tally UI",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"source": "./src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"source": "./src/index.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"react": ">=18",
|
|
23
|
+
"react-native": ">=0.74",
|
|
24
|
+
"@tallyui/core": "0.2.0",
|
|
25
|
+
"@tallyui/theme": "0.2.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/react": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"test": "echo \"no tests yet\""
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { ConnectorProvider } from '@tallyui/core';
|
|
4
|
+
import { CartLine } from '../cart/cart-line';
|
|
5
|
+
import { createTestConnector, wooDoc, medusaDoc } from './helpers';
|
|
6
|
+
|
|
7
|
+
describe('CartLine', () => {
|
|
8
|
+
it('renders product name and line total for WooCommerce', () => {
|
|
9
|
+
const connector = createTestConnector('woo');
|
|
10
|
+
render(
|
|
11
|
+
<ConnectorProvider connector={connector}>
|
|
12
|
+
<CartLine item={{ doc: wooDoc, quantity: 2 }} />
|
|
13
|
+
</ConnectorProvider>
|
|
14
|
+
);
|
|
15
|
+
expect(screen.getByText('Espresso Machine Pro')).toBeDefined();
|
|
16
|
+
expect(screen.getByText('$1299.00 × 2')).toBeDefined();
|
|
17
|
+
expect(screen.getByText('$2598.00')).toBeDefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('renders product name and line total for Medusa', () => {
|
|
21
|
+
const connector = createTestConnector('medusa');
|
|
22
|
+
render(
|
|
23
|
+
<ConnectorProvider connector={connector}>
|
|
24
|
+
<CartLine item={{ doc: medusaDoc, quantity: 1 }} />
|
|
25
|
+
</ConnectorProvider>
|
|
26
|
+
);
|
|
27
|
+
expect(screen.getByText('Espresso Machine Pro')).toBeDefined();
|
|
28
|
+
expect(screen.getByText('$1299.00 × 1')).toBeDefined();
|
|
29
|
+
expect(screen.getByText('$1299.00')).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders custom currency symbol', () => {
|
|
33
|
+
const connector = createTestConnector('woo');
|
|
34
|
+
render(
|
|
35
|
+
<ConnectorProvider connector={connector}>
|
|
36
|
+
<CartLine item={{ doc: wooDoc, quantity: 1 }} currencySymbol="€" />
|
|
37
|
+
</ConnectorProvider>
|
|
38
|
+
);
|
|
39
|
+
expect(screen.getByText('€1299.00 × 1')).toBeDefined();
|
|
40
|
+
expect(screen.getByText('€1299.00')).toBeDefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('handles missing price gracefully', () => {
|
|
44
|
+
const connector = createTestConnector('woo');
|
|
45
|
+
render(
|
|
46
|
+
<ConnectorProvider connector={connector}>
|
|
47
|
+
<CartLine item={{ doc: {}, quantity: 3 }} />
|
|
48
|
+
</ConnectorProvider>
|
|
49
|
+
);
|
|
50
|
+
expect(screen.getByText('× 3')).toBeDefined();
|
|
51
|
+
expect(screen.getByText('—')).toBeDefined();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { Text } from 'react-native';
|
|
4
|
+
import { ConnectorProvider } from '@tallyui/core';
|
|
5
|
+
import { CartPanel } from '../cart/cart-panel';
|
|
6
|
+
import { CartLine } from '../cart/cart-line';
|
|
7
|
+
import { createTestConnector, wooDoc } from './helpers';
|
|
8
|
+
|
|
9
|
+
describe('CartPanel', () => {
|
|
10
|
+
const items = [
|
|
11
|
+
{ doc: wooDoc, quantity: 2 },
|
|
12
|
+
{ doc: { ...wooDoc, id: 2, name: 'Grinder', price: '199.99' }, quantity: 1 },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
it('renders cart line items', () => {
|
|
16
|
+
const connector = createTestConnector('woo');
|
|
17
|
+
render(
|
|
18
|
+
<ConnectorProvider connector={connector}>
|
|
19
|
+
<CartPanel items={items} renderItem={(item) => <CartLine item={item} />} />
|
|
20
|
+
</ConnectorProvider>
|
|
21
|
+
);
|
|
22
|
+
expect(screen.getByText('Espresso Machine Pro')).toBeDefined();
|
|
23
|
+
expect(screen.getByText('Grinder')).toBeDefined();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders header and footer slots', () => {
|
|
27
|
+
const connector = createTestConnector('woo');
|
|
28
|
+
render(
|
|
29
|
+
<ConnectorProvider connector={connector}>
|
|
30
|
+
<CartPanel
|
|
31
|
+
items={items}
|
|
32
|
+
renderItem={(item) => <CartLine item={item} />}
|
|
33
|
+
header={<Text>Customer: Jane</Text>}
|
|
34
|
+
footer={<Text>Total: $1399.97</Text>}
|
|
35
|
+
/>
|
|
36
|
+
</ConnectorProvider>
|
|
37
|
+
);
|
|
38
|
+
expect(screen.getByText('Customer: Jane')).toBeDefined();
|
|
39
|
+
expect(screen.getByText('Total: $1399.97')).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('renders emptyState when no items', () => {
|
|
43
|
+
const connector = createTestConnector('woo');
|
|
44
|
+
render(
|
|
45
|
+
<ConnectorProvider connector={connector}>
|
|
46
|
+
<CartPanel
|
|
47
|
+
items={[]}
|
|
48
|
+
renderItem={() => null}
|
|
49
|
+
emptyState={<Text>Cart is empty</Text>}
|
|
50
|
+
/>
|
|
51
|
+
</ConnectorProvider>
|
|
52
|
+
);
|
|
53
|
+
expect(screen.getByText('Cart is empty')).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { ConnectorProvider } from '@tallyui/core';
|
|
4
|
+
import { CartTotal } from '../cart/cart-total';
|
|
5
|
+
import { createTestConnector, wooDoc } from './helpers';
|
|
6
|
+
|
|
7
|
+
const cartItems = [
|
|
8
|
+
{ doc: wooDoc, quantity: 2 },
|
|
9
|
+
{ doc: { ...wooDoc, name: 'Milk Frother', price: '49.99' }, quantity: 1 },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
describe('CartTotal', () => {
|
|
13
|
+
it('renders subtotal and total labels', () => {
|
|
14
|
+
const connector = createTestConnector('woo');
|
|
15
|
+
const { container } = render(
|
|
16
|
+
<ConnectorProvider connector={connector}>
|
|
17
|
+
<CartTotal items={cartItems} />
|
|
18
|
+
</ConnectorProvider>
|
|
19
|
+
);
|
|
20
|
+
expect(screen.getByText('Subtotal')).toBeDefined();
|
|
21
|
+
expect(screen.getByText('Total')).toBeDefined();
|
|
22
|
+
// subtotal and total both $2647.99 (no tax), so 2 elements
|
|
23
|
+
expect(screen.getAllByText('$2647.99')).toHaveLength(2);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders tax when taxRate is provided', () => {
|
|
27
|
+
const connector = createTestConnector('woo');
|
|
28
|
+
render(
|
|
29
|
+
<ConnectorProvider connector={connector}>
|
|
30
|
+
<CartTotal items={cartItems} taxRate={0.1} />
|
|
31
|
+
</ConnectorProvider>
|
|
32
|
+
);
|
|
33
|
+
expect(screen.getByText('Tax')).toBeDefined();
|
|
34
|
+
expect(screen.getByText('$264.80')).toBeDefined();
|
|
35
|
+
expect(screen.getByText('$2912.79')).toBeDefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('uses custom currency symbol and tax label', () => {
|
|
39
|
+
const connector = createTestConnector('woo');
|
|
40
|
+
render(
|
|
41
|
+
<ConnectorProvider connector={connector}>
|
|
42
|
+
<CartTotal items={cartItems} currencySymbol="€" taxRate={0.2} taxLabel="VAT" />
|
|
43
|
+
</ConnectorProvider>
|
|
44
|
+
);
|
|
45
|
+
expect(screen.getByText('VAT')).toBeDefined();
|
|
46
|
+
// Multiple euro amounts present — just check at least one exists
|
|
47
|
+
expect(screen.getAllByText(/€/).length).toBeGreaterThan(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('handles empty cart', () => {
|
|
51
|
+
const connector = createTestConnector('woo');
|
|
52
|
+
render(
|
|
53
|
+
<ConnectorProvider connector={connector}>
|
|
54
|
+
<CartTotal items={[]} />
|
|
55
|
+
</ConnectorProvider>
|
|
56
|
+
);
|
|
57
|
+
// subtotal and total both $0.00
|
|
58
|
+
expect(screen.getAllByText('$0.00')).toHaveLength(2);
|
|
59
|
+
});
|
|
60
|
+
});
|