@lotics/ui 3.6.0 → 4.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 (54) hide show
  1. package/AGENTS.md +323 -0
  2. package/examples/app_orders.tsx +405 -0
  3. package/examples/tpl_allocate.tsx +120 -0
  4. package/examples/tpl_approvals.tsx +375 -0
  5. package/examples/tpl_attendance.tsx +355 -0
  6. package/examples/tpl_batch.tsx +234 -0
  7. package/examples/tpl_calendar.tsx +288 -0
  8. package/examples/tpl_callsheet.tsx +481 -0
  9. package/examples/tpl_convert.tsx +490 -0
  10. package/examples/tpl_crm_desk.tsx +541 -0
  11. package/examples/tpl_dashboard.tsx +554 -0
  12. package/examples/tpl_detail.tsx +232 -0
  13. package/examples/tpl_directory.tsx +263 -0
  14. package/examples/tpl_dispatch.tsx +289 -0
  15. package/examples/tpl_dossier.tsx +431 -0
  16. package/examples/tpl_intake.tsx +206 -0
  17. package/examples/tpl_inventory.tsx +299 -0
  18. package/examples/tpl_order.tsx +483 -0
  19. package/examples/tpl_pick.tsx +240 -0
  20. package/examples/tpl_quick.tsx +210 -0
  21. package/examples/tpl_reconcile.tsx +275 -0
  22. package/examples/tpl_record.tsx +301 -0
  23. package/examples/tpl_record_plain.tsx +154 -0
  24. package/examples/tpl_rollup.tsx +300 -0
  25. package/examples/tpl_run.tsx +235 -0
  26. package/examples/tpl_settings.tsx +178 -0
  27. package/examples/tpl_shifts.tsx +421 -0
  28. package/examples/tpl_stock.tsx +387 -0
  29. package/examples/tpl_timeline.tsx +244 -0
  30. package/examples/tpl_tower.tsx +356 -0
  31. package/examples/tpl_wizard.tsx +223 -0
  32. package/package.json +11 -2
  33. package/src/bar_chart.tsx +5 -0
  34. package/src/combobox.tsx +22 -6
  35. package/src/form_date_picker.tsx +2 -0
  36. package/src/form_picker.tsx +1 -0
  37. package/src/form_switch.tsx +1 -0
  38. package/src/form_text_input.tsx +2 -0
  39. package/src/icon.tsx +2 -0
  40. package/src/icon_button.tsx +5 -2
  41. package/src/inline_date_picker.tsx +110 -0
  42. package/src/inline_edit.tsx +228 -0
  43. package/src/inline_number_input.tsx +70 -0
  44. package/src/inline_select.tsx +91 -0
  45. package/src/inline_text_input.tsx +71 -0
  46. package/src/inline_time_picker.tsx +64 -0
  47. package/src/line_chart.tsx +4 -0
  48. package/src/list_item.tsx +5 -0
  49. package/src/number_input.tsx +12 -1
  50. package/src/page_content.tsx +5 -0
  51. package/src/section_heading.tsx +43 -29
  52. package/src/tag_input.tsx +202 -0
  53. package/src/time_picker.tsx +15 -3
  54. package/src/tooltip.tsx +19 -0
@@ -0,0 +1,232 @@
1
+ import { useState } from "react";
2
+ import { ScrollView, View } from "react-native";
3
+ import { Text } from "@lotics/ui/text";
4
+ import { colors, solid } from "@lotics/ui/colors";
5
+ import { Badge } from "@lotics/ui/badge";
6
+ import { Button } from "@lotics/ui/button";
7
+ import { Card, CardBody } from "@lotics/ui/card";
8
+ import { Divider } from "@lotics/ui/divider";
9
+ import { FileBadge } from "@lotics/ui/file_badge";
10
+ import { formatMoney } from "@lotics/ui/format_money";
11
+ import { KPICard } from "@lotics/ui/kpi_card";
12
+ import { Stepper } from "@lotics/ui/stepper";
13
+ import { Tabs } from "@lotics/ui/tabs";
14
+ import { Timeline, TimelineItem } from "@lotics/ui/timeline";
15
+
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+ // Template · Record detail — a single order's detail screen. Bands:
18
+ // breadcrumb line · header (id + status + actions) · stat rail (one card,
19
+ // five hairline columns) · Tabs with real switching: Overview (pipeline +
20
+ // key-value list) / Documents (file rows) / Audit log (timeline — every event
21
+ // expands in place; document events link to the Documents tab).
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+
24
+ const PIPELINE = ["Intake", "Order", "Prod", "Ship", "Recon", "Paid"];
25
+ const PIPE_AT = 3;
26
+
27
+ const FIELDS: { label: string; value: string }[] = [
28
+ { label: "Order received", value: "12/05" },
29
+ { label: "Customer PO", value: "NEW-2668" },
30
+ { label: "Specification", value: "500×300×200 · 5-ply BC" },
31
+ { label: "Unit price", value: `${formatMoney(14_000)}/box` },
32
+ { label: "Delivered", value: "1,800 / 1,800" },
33
+ { label: "Payment terms", value: "Net 30" },
34
+ ];
35
+
36
+ const FILES: { name: string; mime: string; size: string }[] = [
37
+ { name: "Quote_QT-2026-0042.pdf", mime: "application/pdf", size: "182 KB" },
38
+ { name: "ProductionOrder_MO-2026-0042.pdf", mime: "application/pdf", size: "96 KB" },
39
+ { name: "DeliveryNote_DN-0042.xlsx", mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", size: "24 KB" },
40
+ ];
41
+
42
+ // Every log event expands in place (Timeline rows become pressable when they
43
+ // carry details) — the read-only drill-down door. Events that produced a
44
+ // a document also carry the real path to it (the Documents tab).
45
+ const LOG: {
46
+ id: string;
47
+ icon: TimelineItem["icon"];
48
+ iconColor: string;
49
+ label: string;
50
+ time: string;
51
+ detail: string;
52
+ chungTu?: string;
53
+ }[] = [
54
+ {
55
+ id: "log-5",
56
+ icon: "circle-check",
57
+ iconColor: solid("emerald"),
58
+ label: "Delivered in full 1,800/1,800 — awaiting note reconciliation",
59
+ time: "04/06 16:40",
60
+ detail: "Customer signed for all 1,800 boxes · reconcile against DN-0042 before closing receivables.",
61
+ },
62
+ {
63
+ id: "log-4",
64
+ icon: "package",
65
+ iconColor: solid("blue"),
66
+ label: "Shipped 1,800 boxes — loaded on truck 29C-123.45",
67
+ time: "04/06 07:15",
68
+ detail: "Dispatched from finished-goods warehouse · truck 29C-123.45 · driver Ben Tran.",
69
+ chungTu: "DeliveryNote_DN-0042.xlsx",
70
+ },
71
+ {
72
+ id: "log-3",
73
+ icon: "check",
74
+ iconColor: solid("blue"),
75
+ label: "Production complete — moved to finished goods",
76
+ time: "02/06 17:05",
77
+ detail: "Completed at the Eastside plant · 1,800 boxes received into finished goods.",
78
+ },
79
+ {
80
+ id: "log-2",
81
+ icon: "file-text",
82
+ iconColor: solid("blue"),
83
+ label: "Production order MO-2026-0042 issued to the plant",
84
+ time: "26/05 09:20",
85
+ detail: "Sent to the Eastside plant · 5-ply BC blanks · die KB-118 on hand.",
86
+ chungTu: "ProductionOrder_MO-2026-0042.pdf",
87
+ },
88
+ {
89
+ id: "log-1",
90
+ icon: "plus",
91
+ iconColor: colors.zinc[400],
92
+ label: "Order created from PO NEW-2668 — Tessa Tran",
93
+ time: "12/05 10:02",
94
+ detail: "Created from PO NEW-2668 received by email · created by Tessa Tran.",
95
+ },
96
+ ];
97
+
98
+ const STATS = [
99
+ { label: "Customer", value: "NEWTECONS" },
100
+ { label: "Quantity", value: "1,800" },
101
+ { label: "Value", value: formatMoney(25_200_000) },
102
+ { label: "Needed by", value: "28/05", tone: "danger" as const },
103
+ { label: "Owner", value: "Tessa Tran" },
104
+ ];
105
+
106
+ const TABS = [
107
+ { label: "Overview", value: "tongquan" },
108
+ { label: "Documents", value: "chungtu" },
109
+ { label: "Audit log", value: "nhatky" },
110
+ ];
111
+
112
+ function TongQuan() {
113
+ return (
114
+ <View style={{ gap: 12 }}>
115
+ <Card style={{ padding: 0 }}>
116
+ <CardBody style={{ gap: 12 }}>
117
+ <Text size="xs" color="muted" transform="uppercase">
118
+ Pipeline
119
+ </Text>
120
+ {/* a detail screen has room for the journey itself — every
121
+ milestone labeled, the current one marked */}
122
+ <Stepper steps={PIPELINE} current={PIPE_AT} color={solid("blue")} />
123
+ <Text size="xs" color="muted">
124
+ Delivered in full — awaiting reconciliation
125
+ </Text>
126
+ </CardBody>
127
+ </Card>
128
+ <Card style={{ padding: 0 }}>
129
+ {FIELDS.map((f, i) => (
130
+ <View key={f.label}>
131
+ {i > 0 ? <Divider /> : null}
132
+ <View style={{ flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingVertical: 12, gap: 16 }}>
133
+ <Text size="sm" color="muted">{f.label}</Text>
134
+ <View style={{ flex: 1 }} />
135
+ <Text size="sm" weight="medium" tabular align="right">
136
+ {f.value}
137
+ </Text>
138
+ </View>
139
+ </View>
140
+ ))}
141
+ </Card>
142
+ </View>
143
+ );
144
+ }
145
+
146
+ function ChungTu() {
147
+ return (
148
+ <Card style={{ padding: 0 }}>
149
+ {FILES.map((f, i) => (
150
+ <View key={f.name}>
151
+ {i > 0 ? <Divider /> : null}
152
+ <View style={{ flexDirection: "row", alignItems: "center", paddingHorizontal: 16, paddingVertical: 12, gap: 12 }}>
153
+ <FileBadge mimeType={f.mime} size={24} />
154
+ <Text size="sm" weight="medium" numberOfLines={1} style={{ flexShrink: 1 }}>{f.name}</Text>
155
+ <Text size="xs" color="muted" tabular>{f.size}</Text>
156
+ <View style={{ flex: 1 }} />
157
+ <Button title="Download" color="muted" onPress={() => {}} />
158
+ </View>
159
+ </View>
160
+ ))}
161
+ </Card>
162
+ );
163
+ }
164
+
165
+ function NhatKy({ onMoChungTu }: { onMoChungTu: () => void }) {
166
+ const items: TimelineItem[] = LOG.map((e) => ({
167
+ id: e.id,
168
+ icon: e.icon,
169
+ iconColor: e.iconColor,
170
+ label: e.label,
171
+ right: <Text size="xs" color="muted" tabular>{e.time}</Text>,
172
+ details: (
173
+ <View style={{ gap: 8 }}>
174
+ <Text size="xs" color="muted">{e.detail}</Text>
175
+ {e.chungTu ? (
176
+ <View style={{ flexDirection: "row" }}>
177
+ <Button title="Open document" color="muted" onPress={onMoChungTu} />
178
+ </View>
179
+ ) : null}
180
+ </View>
181
+ ),
182
+ }));
183
+ return (
184
+ <Card style={{ padding: 16 }}>
185
+ <Timeline items={items} />
186
+ </Card>
187
+ );
188
+ }
189
+
190
+ export function TplDetail() {
191
+ const [tab, setTab] = useState<string>("tongquan");
192
+
193
+ return (
194
+ <ScrollView style={{ flex: 1, backgroundColor: colors.zinc[50] }} contentContainerStyle={{ padding: 28 }}>
195
+ <View style={{ width: "100%", maxWidth: 880, alignSelf: "center", gap: 16 }}>
196
+ {/* breadcrumb line */}
197
+ <Text size="xs" color="muted">Orders / SO-2026-0042</Text>
198
+
199
+ {/* header band */}
200
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
201
+ <Text size="xl" weight="semibold" tabular>SO-2026-0042</Text>
202
+ <Badge label="Shipping" color="blue" />
203
+ <Badge label="Overdue" color="red" />
204
+ <View style={{ flex: 1 }} />
205
+ <Button title="Print documents" color="secondary" onPress={() => {}} />
206
+ <Button title="Update" color="primary" onPress={() => {}} />
207
+ </View>
208
+ <Text size="sm" color="muted">Carton box NEW 2668 500×300×200 — NEWTECONS</Text>
209
+
210
+ {/* stat rail: one card, five KPI columns */}
211
+ <Card style={{ padding: 0 }}>
212
+ <View style={{ flexDirection: "row", paddingVertical: 14, paddingHorizontal: 16, gap: 16 }}>
213
+ {STATS.map((s) => (
214
+ <KPICard key={s.label} label={s.label} value={s.value} tone={s.tone} size="sm" style={{ flex: 1 }} />
215
+ ))}
216
+ </View>
217
+ </Card>
218
+
219
+ <Tabs
220
+ accessibilityLabel="Record sections"
221
+ options={TABS}
222
+ selectedTab={tab}
223
+ onSelectTab={setTab}
224
+ />
225
+
226
+ {tab === "tongquan" ? <TongQuan /> : null}
227
+ {tab === "chungtu" ? <ChungTu /> : null}
228
+ {tab === "nhatky" ? <NhatKy onMoChungTu={() => setTab("chungtu")} /> : null}
229
+ </View>
230
+ </ScrollView>
231
+ );
232
+ }
@@ -0,0 +1,263 @@
1
+ import { useState } from "react";
2
+ import { Pressable, ScrollView, View } from "react-native";
3
+ import { Text } from "@lotics/ui/text";
4
+ import { colors } from "@lotics/ui/colors";
5
+ import { ActionMenu } from "@lotics/ui/action_menu";
6
+ import { Avatar } from "@lotics/ui/avatar";
7
+ import { Badge } from "@lotics/ui/badge";
8
+ import { Button } from "@lotics/ui/button";
9
+ import { ChipGroup } from "@lotics/ui/chip_group";
10
+ import { FilterPill } from "@lotics/ui/filter_pill";
11
+ import { Counter } from "@lotics/ui/counter";
12
+ import { Card } from "@lotics/ui/card";
13
+ import { Divider } from "@lotics/ui/divider";
14
+ import { Drawer } from "@lotics/ui/drawer";
15
+ import { EmptyState } from "@lotics/ui/empty_state";
16
+ import { formatMoney } from "@lotics/ui/format_money";
17
+ import { StatusBadge } from "@lotics/ui/status_badge";
18
+ import { PressableRow } from "@lotics/ui/pressable_row";
19
+ import { SearchInput } from "@lotics/ui/search_input";
20
+
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+ // Template · Directory — the partner directory every trading/logistics desk
23
+ // needs: one searchable, filterable register of customers and suppliers with
24
+ // the relationship status and volume at a glance.
25
+ //
26
+ // Grammar: zinc-50 canvas · one header band (count + ONE primary action) ·
27
+ // toolbar (live search + type tabs) · ONE banded card of rows separated by
28
+ // hairlines: Avatar → name + MST → type badge → tabular volume → StatusBadge.
29
+ // Doors: a partner row is the PRIMARY entity here — press opens the partner
30
+ // workspace Drawer (◀ ▶ / ←→ sequenced over the visible filtered ordering);
31
+ // ⋯ holds the row's quick operations (destructive last).
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+
34
+ type PartnerKind = "khach" | "ncc";
35
+
36
+ interface Partner {
37
+ id: string;
38
+ ten: string;
39
+ mst: string;
40
+ kind: PartnerKind;
41
+ donHang: number;
42
+ doanhSo: number;
43
+ active: boolean;
44
+ }
45
+
46
+ const PARTNERS: Partner[] = [
47
+ { id: "p1", ten: "Apex Plastics", mst: "0301452786", kind: "khach", donHang: 24, doanhSo: 860_000_000, active: true },
48
+ { id: "p2", ten: "Eastland Packaging", mst: "0312908455", kind: "ncc", donHang: 18, doanhSo: 412_000_000, active: true },
49
+ { id: "p3", ten: "Vista Paper Mills", mst: "0309977120", kind: "ncc", donHang: 31, doanhSo: 1_240_000_000, active: true },
50
+ { id: "p4", ten: "Crestline Foods", mst: "0107643298", kind: "khach", donHang: 12, doanhSo: 340_000_000, active: true },
51
+ { id: "p5", ten: "Harbor Engineering", mst: "0203318874", kind: "khach", donHang: 7, doanhSo: 156_000_000, active: false },
52
+ { id: "p6", ten: "Summit Freight", mst: "0314226901", kind: "ncc", donHang: 42, doanhSo: 980_000_000, active: true },
53
+ { id: "p7", ten: "Northway Appliances", mst: "0108812346", kind: "khach", donHang: 19, doanhSo: 624_000_000, active: true },
54
+ { id: "p8", ten: "Brightline Inks", mst: "0305570213", kind: "ncc", donHang: 9, doanhSo: 87_000_000, active: false },
55
+ { id: "p9", ten: "Phoenix Textiles", mst: "0300523847", kind: "khach", donHang: 28, doanhSo: 1_030_000_000, active: true },
56
+ { id: "p10", ten: "Delta Chemicals", mst: "0204461185", kind: "ncc", donHang: 15, doanhSo: 295_000_000, active: true },
57
+ ];
58
+
59
+ const KIND_TABS = [
60
+ { value: "all", label: "All" },
61
+ { value: "khach", label: "Customers" },
62
+ { value: "ncc", label: "Suppliers" },
63
+ ] as const;
64
+
65
+ type KindTab = (typeof KIND_TABS)[number]["value"];
66
+
67
+ const kindLabel = (kind: PartnerKind) => (kind === "khach" ? "Customer" : "Supplier");
68
+
69
+ function PartnerRow({ p, selected, onPress }: { p: Partner; selected: boolean; onPress: () => void }) {
70
+ return (
71
+ <PressableRow onPress={onPress} selected={selected} style={{ minHeight: 64 }}>
72
+ <Pressable
73
+ accessibilityRole="button"
74
+ accessibilityLabel={`Open partner ${p.ten}`}
75
+ onPress={onPress}
76
+ style={{ flex: 1, flexDirection: "row", alignItems: "center", gap: 12 }}
77
+ >
78
+ <Avatar size={36} name={p.ten} />
79
+ <View style={{ flex: 1, gap: 2 }}>
80
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
81
+ <Text size="sm" weight="medium" numberOfLines={1}>{p.ten}</Text>
82
+ <Badge label={kindLabel(p.kind)} color={p.kind === "khach" ? "blue" : undefined} />
83
+ </View>
84
+ <Text size="xs" color="muted" tabular>MST {p.mst}</Text>
85
+ </View>
86
+ <Text size="sm" color="muted" tabular>
87
+ {p.donHang} orders · {formatMoney(p.doanhSo)}
88
+ </Text>
89
+ <StatusBadge enabled={p.active} label={p.active ? "Active" : "Paused"} />
90
+ </Pressable>
91
+ <ActionMenu
92
+ accessibilityLabel={`Actions for ${p.ten}`}
93
+ items={[
94
+ { key: "don", label: "Create order", icon: "file-text", onPress: () => {} },
95
+ { key: "sua", label: "Edit details", icon: "pencil", onPress: () => {} },
96
+ p.active
97
+ ? { key: "dung", label: "Pause partnership", icon: "pause", danger: true, onPress: () => {} }
98
+ : { key: "molai", label: "Resume partnership", icon: "history", onPress: () => {} },
99
+ ]}
100
+ />
101
+ </PressableRow>
102
+ );
103
+ }
104
+
105
+ function KVRow({ label, children }: { label: string; children: React.ReactNode }) {
106
+ return (
107
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 12, minHeight: 24 }}>
108
+ <Text size="sm" color="muted" style={{ width: 110 }}>{label}</Text>
109
+ {typeof children === "string" ? <Text size="sm">{children}</Text> : children}
110
+ </View>
111
+ );
112
+ }
113
+
114
+ // The drawer body — compact partner workspace: identity, the key facts already
115
+ // on the row, and the two real next steps. Keyed by partner id by the caller
116
+ // so nothing leaks across ◀ ▶ steps.
117
+ function PartnerWorkspace({ p }: { p: Partner }) {
118
+ return (
119
+ <>
120
+ <ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 24, gap: 20 }}>
121
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
122
+ <Avatar size={44} name={p.ten} />
123
+ <View style={{ gap: 4, flex: 1 }}>
124
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
125
+ <Badge label={kindLabel(p.kind)} color={p.kind === "khach" ? "blue" : undefined} />
126
+ <StatusBadge enabled={p.active} label={p.active ? "Active" : "Paused"} />
127
+ </View>
128
+ <Text size="xs" color="muted" tabular userSelect="auto">MST {p.mst}</Text>
129
+ </View>
130
+ </View>
131
+
132
+ <Divider />
133
+
134
+ <View style={{ gap: 6 }}>
135
+ <KVRow label="Partner type">{kindLabel(p.kind)}</KVRow>
136
+ <KVRow label="Tax ID"><Text size="sm" tabular userSelect="auto">{p.mst}</Text></KVRow>
137
+ <KVRow label="Orders"><Text size="sm" tabular>{`${p.donHang} orders`}</Text></KVRow>
138
+ <KVRow label="Revenue"><Text size="sm" tabular>{formatMoney(p.doanhSo)}</Text></KVRow>
139
+ </View>
140
+ </ScrollView>
141
+
142
+ {/* pinned footer — the workspace's escalation, commit action at the right edge */}
143
+ <View style={{ borderTopWidth: 1, borderTopColor: colors.border, paddingHorizontal: 20, paddingVertical: 14, flexDirection: "row", alignItems: "center", gap: 12 }}>
144
+ <Text size="xs" color="muted" style={{ flex: 1 }}>New orders attach to this partner — track them in the Orders module.</Text>
145
+ <Button title="Edit details" color="secondary" onPress={() => {}} />
146
+ <Button title="Create order" color="primary" onPress={() => {}} />
147
+ </View>
148
+ </>
149
+ );
150
+ }
151
+
152
+ export function TplDirectory() {
153
+ const [search, setSearch] = useState("");
154
+ const [tab, setTab] = useState<KindTab>("all");
155
+ const [openId, setOpenId] = useState<string | null>(null);
156
+ const [minOrders, setMinOrders] = useState(0);
157
+
158
+ const q = search.trim().toLowerCase();
159
+ const matches = PARTNERS.filter(
160
+ (p) =>
161
+ (tab === "all" || p.kind === tab) &&
162
+ p.donHang >= minOrders &&
163
+ (q === "" || p.ten.toLowerCase().includes(q) || p.mst.includes(q)),
164
+ );
165
+
166
+ // The drawer sequences over the visible filtered ordering. When the open
167
+ // partner falls out of it (search/tab changed), the chevrons disappear but
168
+ // the drawer stays on the record.
169
+ const openPartner = PARTNERS.find((p) => p.id === openId) ?? null;
170
+ const seqIndex = openPartner ? matches.findIndex((p) => p.id === openPartner.id) : -1;
171
+
172
+ return (
173
+ <View style={{ flex: 1, backgroundColor: colors.zinc[50] }}>
174
+ <ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 28 }}>
175
+ <View style={{ width: "100%", maxWidth: 880, alignSelf: "center", gap: 16 }}>
176
+ {/* header band */}
177
+ <View style={{ flexDirection: "row", alignItems: "flex-end", gap: 12 }}>
178
+ <View style={{ gap: 2, flex: 1 }}>
179
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
180
+ <Text size="xl" weight="semibold">Directory</Text>
181
+ </View>
182
+ <Text size="sm" color="muted">Customers and suppliers — look anyone up by name or tax ID</Text>
183
+ </View>
184
+ <Button title="Add partner" color="primary" onPress={() => {}} />
185
+ </View>
186
+
187
+ {/* toolbar: live search + a partner-type lens. Wraps when narrow;
188
+ every control is 40px so a wrapped band reads as clean rows. */}
189
+ <View style={{ flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 8 }}>
190
+ <View style={{ flexGrow: 1, flexBasis: 240, minWidth: 200, maxWidth: 360 }}>
191
+ <SearchInput
192
+ placeholder="Search by name or tax ID…"
193
+ accessibilityLabel="Search partners"
194
+ value={search}
195
+ onChangeText={setSearch}
196
+ />
197
+ </View>
198
+ <ChipGroup
199
+ accessibilityLabel="Partner type"
200
+ options={KIND_TABS.map((t) => ({
201
+ label: `${t.label} · ${PARTNERS.filter((p) => t.value === "all" || p.kind === t.value).length}`,
202
+ value: t.value,
203
+ }))}
204
+ value={tab}
205
+ onValueChange={setTab}
206
+ />
207
+ <FilterPill
208
+ label="Min orders"
209
+ summary={minOrders > 0 ? `≥ ${minOrders}` : undefined}
210
+ onClear={() => setMinOrders(0)}
211
+ clearLabel="Clear min-orders filter"
212
+ >
213
+ <View style={{ gap: 10 }}>
214
+ <Text size="sm" color="muted">Show partners with at least</Text>
215
+ <Counter
216
+ value={minOrders}
217
+ min={0}
218
+ max={50}
219
+ step={5}
220
+ onValueChange={setMinOrders}
221
+ accessibilityLabel="minimum orders"
222
+ format={(n) => `${n} orders`}
223
+ />
224
+ </View>
225
+ </FilterPill>
226
+ </View>
227
+
228
+ {/* the register: one card, hairline-separated rows */}
229
+ <Card style={{ padding: 0 }}>
230
+ {matches.length === 0 ? (
231
+ <EmptyState
232
+ message="No partners match"
233
+ hint="Try another keyword or switch the type filter"
234
+ />
235
+ ) : (
236
+ <View style={{ paddingVertical: 6 }}>
237
+ {matches.map((p, i) => (
238
+ <View key={p.id}>
239
+ {i > 0 ? <Divider /> : null}
240
+ <PartnerRow p={p} selected={p.id === openId} onPress={() => setOpenId(p.id)} />
241
+ </View>
242
+ ))}
243
+ </View>
244
+ )}
245
+ </Card>
246
+ </View>
247
+ </ScrollView>
248
+
249
+ {/* the partner workspace — body keyed by partner id so per-record state
250
+ resets when ◀ ▶ (or ←/→) steps through the visible list */}
251
+ {openPartner ? (
252
+ <Drawer
253
+ open onOpenChange={(o) => !o && setOpenId(null)} title={openPartner.ten} width={480}
254
+ onPrev={seqIndex > 0 ? () => setOpenId(matches[seqIndex - 1].id) : undefined}
255
+ onNext={seqIndex >= 0 && seqIndex < matches.length - 1 ? () => setOpenId(matches[seqIndex + 1].id) : undefined}
256
+ position={seqIndex >= 0 ? `${seqIndex + 1}/${matches.length}` : undefined}
257
+ >
258
+ <PartnerWorkspace key={openPartner.id} p={openPartner} />
259
+ </Drawer>
260
+ ) : null}
261
+ </View>
262
+ );
263
+ }