@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.
Files changed (47) hide show
  1. package/dist/index.d.ts +364 -0
  2. package/dist/index.js +451 -0
  3. package/package.json +35 -0
  4. package/src/__tests__/cart-line.test.tsx +53 -0
  5. package/src/__tests__/cart-panel.test.tsx +55 -0
  6. package/src/__tests__/cart-total.test.tsx +60 -0
  7. package/src/__tests__/customer-card.test.tsx +47 -0
  8. package/src/__tests__/customer-select.test.tsx +33 -0
  9. package/src/__tests__/filter-chip.test.tsx +40 -0
  10. package/src/__tests__/helpers.ts +111 -0
  11. package/src/__tests__/order-summary-line.test.tsx +18 -0
  12. package/src/__tests__/order-summary.test.tsx +32 -0
  13. package/src/__tests__/payment-method-card.test.tsx +24 -0
  14. package/src/__tests__/payment-selector.test.tsx +23 -0
  15. package/src/__tests__/product-card.test.tsx +31 -0
  16. package/src/__tests__/product-grid.test.tsx +37 -0
  17. package/src/__tests__/product-image.test.tsx +54 -0
  18. package/src/__tests__/product-price.test.tsx +68 -0
  19. package/src/__tests__/product-sku.test.tsx +47 -0
  20. package/src/__tests__/product-stock-badge.test.tsx +58 -0
  21. package/src/__tests__/product-title.test.tsx +38 -0
  22. package/src/__tests__/search-input.test.tsx +22 -0
  23. package/src/cart/cart-line.tsx +49 -0
  24. package/src/cart/cart-panel.tsx +65 -0
  25. package/src/cart/cart-total.tsx +64 -0
  26. package/src/cart/index.ts +3 -0
  27. package/src/checkout/index.ts +4 -0
  28. package/src/checkout/order-summary-line.tsx +23 -0
  29. package/src/checkout/order-summary.tsx +77 -0
  30. package/src/checkout/payment-method-card.tsx +36 -0
  31. package/src/checkout/payment-selector.tsx +55 -0
  32. package/src/customer/customer-card.tsx +26 -0
  33. package/src/customer/customer-select.tsx +72 -0
  34. package/src/customer/index.ts +2 -0
  35. package/src/index.ts +61 -0
  36. package/src/input/filter-chip.tsx +58 -0
  37. package/src/input/index.ts +2 -0
  38. package/src/input/search-input.tsx +26 -0
  39. package/src/product/index.ts +7 -0
  40. package/src/product/product-card.tsx +28 -0
  41. package/src/product/product-grid.tsx +66 -0
  42. package/src/product/product-image.tsx +41 -0
  43. package/src/product/product-price.tsx +53 -0
  44. package/src/product/product-sku.tsx +33 -0
  45. package/src/product/product-stock-badge.tsx +73 -0
  46. package/src/product/product-title.tsx +30 -0
  47. 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
+ });