@lotics/ui 7.19.3 → 9.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 (75) hide show
  1. package/AGENTS.md +298 -159
  2. package/examples/tpl_allocate.tsx +2 -2
  3. package/examples/tpl_attendance.tsx +2 -2
  4. package/examples/tpl_calendar.tsx +1 -1
  5. package/examples/tpl_dashboard.tsx +1 -1
  6. package/examples/tpl_dieline.tsx +3 -3
  7. package/examples/tpl_documents.tsx +790 -0
  8. package/examples/tpl_item_list.tsx +1015 -124
  9. package/examples/tpl_lookup.tsx +37 -10
  10. package/examples/tpl_pick.tsx +3 -3
  11. package/examples/tpl_pivot.tsx +1 -1
  12. package/examples/tpl_record.tsx +1354 -0
  13. package/examples/tpl_report.tsx +7 -7
  14. package/examples/tpl_rollup.tsx +6 -6
  15. package/examples/tpl_shifts.tsx +2 -2
  16. package/examples/tpl_statements.tsx +221 -0
  17. package/examples/tpl_stock.tsx +7 -7
  18. package/examples/tpl_task_board.tsx +48 -32
  19. package/examples/tpl_tasks.tsx +47 -47
  20. package/examples/tpl_tower.tsx +2 -2
  21. package/package.json +9 -9
  22. package/src/agent_run.tsx +1 -1
  23. package/src/capture_row.tsx +59 -0
  24. package/src/change_review.tsx +732 -236
  25. package/src/checklist.tsx +104 -0
  26. package/src/chip.tsx +12 -3
  27. package/src/composer.tsx +22 -1
  28. package/src/confidence.tsx +1 -1
  29. package/src/control_surface.ts +0 -14
  30. package/src/detail_row.tsx +137 -10
  31. package/src/finding.tsx +112 -80
  32. package/src/floating_action_bar.tsx +1 -1
  33. package/src/inline_date_picker.tsx +8 -3
  34. package/src/inline_edit.tsx +40 -10
  35. package/src/inline_member_select.tsx +3 -0
  36. package/src/inline_number_input.tsx +5 -2
  37. package/src/inline_select.tsx +8 -3
  38. package/src/inline_tag_select.tsx +140 -0
  39. package/src/inline_text_input.tsx +5 -2
  40. package/src/inline_time_picker.tsx +5 -2
  41. package/src/ledger.tsx +220 -0
  42. package/src/locale.tsx +43 -16
  43. package/src/progress_bar.tsx +32 -1
  44. package/src/record_summary.tsx +101 -0
  45. package/src/section_heading.tsx +16 -8
  46. package/src/sources.tsx +8 -5
  47. package/src/suggestion_chip.tsx +47 -0
  48. package/src/use_section_nav.test.ts +69 -0
  49. package/src/use_section_nav.ts +59 -0
  50. package/examples/tpl_assistant.tsx +0 -174
  51. package/examples/tpl_billing.tsx +0 -344
  52. package/examples/tpl_briefing.tsx +0 -121
  53. package/examples/tpl_compare.tsx +0 -133
  54. package/examples/tpl_crosscheck.tsx +0 -120
  55. package/examples/tpl_detail.tsx +0 -232
  56. package/examples/tpl_directory.tsx +0 -260
  57. package/examples/tpl_draft.tsx +0 -163
  58. package/examples/tpl_extract.tsx +0 -193
  59. package/examples/tpl_intake.tsx +0 -206
  60. package/examples/tpl_match.tsx +0 -134
  61. package/examples/tpl_order.tsx +0 -482
  62. package/examples/tpl_quick.tsx +0 -211
  63. package/examples/tpl_ratedesk.tsx +0 -386
  64. package/examples/tpl_record_plain.tsx +0 -259
  65. package/examples/tpl_settings.tsx +0 -178
  66. package/examples/tpl_timeline.tsx +0 -244
  67. package/examples/tpl_triage.tsx +0 -112
  68. package/examples/tpl_wizard.tsx +0 -223
  69. package/src/discrepancy.tsx +0 -114
  70. package/src/match_sides.tsx +0 -79
  71. package/src/record_fields.tsx +0 -68
  72. package/src/review_card.tsx +0 -172
  73. package/src/scored_option.tsx +0 -138
  74. package/src/spec_list.tsx +0 -81
  75. package/src/triage_row.tsx +0 -99
@@ -1,112 +0,0 @@
1
- import { useState } from "react";
2
- import { ScrollView, View } from "react-native";
3
- import { Text } from "@lotics/ui/text";
4
- import { colors } from "@lotics/ui/colors";
5
- import { Button } from "@lotics/ui/button";
6
- import { KPIStrip } from "@lotics/ui/kpi_strip";
7
- import { SegmentedControl } from "@lotics/ui/segmented_control";
8
- import { EmptyState } from "@lotics/ui/empty_state";
9
- import { TriageRow } from "@lotics/ui/triage_row";
10
- import type { ConfidenceLevel } from "@lotics/ui/confidence";
11
-
12
- // ─────────────────────────────────────────────────────────────────────────────
13
- // Template · Triage — an inbox the agent has CLASSIFIED and routed. Each item
14
- // carries the agent's category and a suggested action with a confidence; the
15
- // human accepts the call, overrides it, or dismisses the item. High-confidence
16
- // items batch-accept so attention goes to the ambiguous ones. Leads, support
17
- // tickets, inbound documents, emails. The agent does the sorting; the human
18
- // keeps the decision.
19
- // ─────────────────────────────────────────────────────────────────────────────
20
-
21
- interface T {
22
- id: string;
23
- title: string;
24
- preview: string;
25
- meta: string;
26
- category: { label: string };
27
- suggestedAction: string;
28
- confidence: ConfidenceLevel;
29
- status: "open" | "accepted" | "dismissed";
30
- }
31
-
32
- const SEED: T[] = [
33
- { id: "t1", title: "RFQ — 2×40HC Cát Lái → Hamburg, reefer", preview: "Hi, we need a quote for two reefer containers of frozen shrimp departing next week…", meta: "08:12", category: { label: "Sales" }, suggestedAction: "Assign to Nguyen", confidence: "high", status: "open" },
34
- { id: "t2", title: "Where is my shipment HBL-4471?", preview: "Customer asking for an ETA update on the Hamburg booking, getting anxious about demurrage…", meta: "08:39", category: { label: "Support" }, suggestedAction: "Send ETA, open ticket", confidence: "high", status: "open" },
35
- { id: "t3", title: "Invoice INV-2026-0318 — payment confirmation", preview: "Attached the TT receipt for 41,300,000 ₫, please confirm and release the documents…", meta: "09:02", category: { label: "Billing" }, suggestedAction: "Apply the payment", confidence: "high", status: "open" },
36
- { id: "t4", title: "Re: partnership opportunity (sponsored)", preview: "Boost your logistics with our revolutionary platform, limited-time offer just for you…", meta: "09:15", category: { label: "Spam" }, suggestedAction: "Move to spam", confidence: "high", status: "open" },
37
- { id: "t5", title: "Complaint — damaged cartons on delivery", preview: "Three cartons arrived crushed, we need a claim opened and a replacement schedule…", meta: "09:41", category: { label: "Support" }, suggestedAction: "Open a damage claim", confidence: "medium", status: "open" },
38
- { id: "t6", title: "Quote request — air freight, urgent samples", preview: "Need 12 kg of samples to Rotterdam by Friday, what's the fastest option and cost…", meta: "10:03", category: { label: "Sales" }, suggestedAction: "Assign to the air desk", confidence: "medium", status: "open" },
39
- { id: "t7", title: "Updated bank details for remittance", preview: "Please update our account for future payments to the following beneficiary…", meta: "10:20", category: { label: "Billing" }, suggestedAction: "Hold — verify sender first", confidence: "low", status: "open" },
40
- { id: "t8", title: "Newsletter: port congestion outlook Q3", preview: "Our latest market report on Asia–Europe capacity and rate trends is now available…", meta: "Yesterday", category: { label: "Spam" }, suggestedAction: "Move to spam", confidence: "medium", status: "accepted" },
41
- ];
42
-
43
- export function TplTriage() {
44
- const [rows, setRows] = useState<T[]>(SEED);
45
- const [tab, setTab] = useState<"open" | "resolved">("open");
46
-
47
- const set = (id: string, status: T["status"]) => setRows((prev) => prev.map((r) => (r.id === id ? { ...r, status } : r)));
48
- const acceptAllHigh = () => setRows((prev) => prev.map((r) => (r.status === "open" && r.confidence === "high" ? { ...r, status: "accepted" } : r)));
49
-
50
- const open = rows.filter((r) => r.status === "open");
51
- const resolved = rows.filter((r) => r.status !== "open");
52
- const visible = tab === "open" ? open : resolved;
53
- const highOpen = open.filter((r) => r.confidence === "high").length;
54
- const review = open.filter((r) => r.confidence !== "high").length;
55
-
56
- return (
57
- <ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
58
- <View style={{ width: "100%", maxWidth: 820, alignSelf: "center", gap: 16 }}>
59
- <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 16 }}>
60
- <View style={{ flex: 1, gap: 2 }}>
61
- <Text size="xl" weight="semibold">Inbox triage</Text>
62
- <Text size="sm" color="muted">The agent classifies and routes each item — accept the call, override, or dismiss</Text>
63
- </View>
64
- {tab === "open" && highOpen > 0 ? (
65
- <Button title={`Accept ${highOpen} high-confidence`} color="secondary" onPress={acceptAllHigh} />
66
- ) : null}
67
- </View>
68
-
69
- <KPIStrip
70
- items={[
71
- { label: "New", value: open.length, format: "number", caption: "awaiting triage" },
72
- { label: "Ready to auto-route", value: highOpen, format: "number", info: "High-confidence items the agent is sure about — safe to accept in bulk." },
73
- { label: "Needs review", value: review, format: "number", tone: review > 0 ? "warning" : "default", info: "Medium / low confidence — the agent flagged these for a human to confirm the routing." },
74
- { label: "Handled", value: rows.filter((r) => r.status === "accepted").length, format: "number" },
75
- ]}
76
- />
77
-
78
- <SegmentedControl
79
- accessibilityLabel="Queue"
80
- options={[
81
- { label: `New · ${open.length}`, value: "open" },
82
- { label: `Handled · ${resolved.length}`, value: "resolved" },
83
- ]}
84
- value={tab}
85
- onValueChange={setTab}
86
- />
87
-
88
- {visible.length === 0 ? (
89
- <EmptyState icon="circle-check" message={tab === "open" ? "Inbox zero" : "Nothing handled yet"} hint={tab === "open" ? "Every item has been routed or dismissed" : undefined} />
90
- ) : (
91
- <View style={{ gap: 10 }}>
92
- {visible.map((r) => (
93
- <TriageRow
94
- key={r.id}
95
- title={r.title}
96
- preview={r.preview}
97
- meta={r.meta}
98
- category={r.category}
99
- suggestedAction={r.suggestedAction}
100
- confidence={r.confidence}
101
- status={r.status}
102
- onAccept={() => set(r.id, "accepted")}
103
- onOverride={() => {}}
104
- onDismiss={() => set(r.id, "dismissed")}
105
- />
106
- ))}
107
- </View>
108
- )}
109
- </View>
110
- </ScrollView>
111
- );
112
- }
@@ -1,223 +0,0 @@
1
- import { useState } from "react";
2
- import { ScrollView, View } from "react-native";
3
- import { Text } from "@lotics/ui/text";
4
- import { colors } from "@lotics/ui/colors";
5
- import { Button } from "@lotics/ui/button";
6
- import { Card, CardBody } from "@lotics/ui/card";
7
- import { Stepper } from "@lotics/ui/stepper";
8
- import { FormField } from "@lotics/ui/form_field";
9
- import { TextInputField } from "@lotics/ui/text_input_field";
10
- import { Picker } from "@lotics/ui/picker";
11
- import type { PickerOption } from "@lotics/ui/picker";
12
- import { NumberInput } from "@lotics/ui/number_input";
13
- import { Switch } from "@lotics/ui/switch";
14
- import { DetailRow } from "@lotics/ui/detail_row";
15
- import { Divider } from "@lotics/ui/divider";
16
- import { Callout, CalloutText } from "@lotics/ui/callout";
17
- import { formatMoney } from "@lotics/ui/format_money";
18
-
19
- // ─────────────────────────────────────────────────────────────────────────────
20
- // Template · Application wizard — a long form broken into ordered STEPS with a
21
- // `Stepper` header, one fieldset per step, and a REVIEW step that reads back
22
- // every value with an Edit jump to its section. Back / Next gate on the current
23
- // step's required fields; the last step submits. For a form too long to scan as
24
- // one page (onboarding, applications, KYC). Two-column grid throughout.
25
- // ─────────────────────────────────────────────────────────────────────────────
26
-
27
- const STEPS = ["Company", "Contact", "Terms", "Review"];
28
-
29
- const INDUSTRY: PickerOption[] = [
30
- { value: "manufacturing", label: "Manufacturing" },
31
- { value: "trading", label: "Trading" },
32
- { value: "construction", label: "Construction" },
33
- { value: "transport", label: "Transportation" },
34
- { value: "retail", label: "Retail" },
35
- ];
36
-
37
- const SIZE: PickerOption[] = [
38
- { value: "1-10", label: "1–10" },
39
- { value: "11-50", label: "11–50" },
40
- { value: "51-200", label: "51–200" },
41
- { value: "200+", label: "200+" },
42
- ];
43
-
44
- const TERMS: PickerOption[] = [
45
- { value: "receipt", label: "Due on receipt" },
46
- { value: "15", label: "Net 15" },
47
- { value: "30", label: "Net 30" },
48
- { value: "45", label: "Net 45" },
49
- ];
50
-
51
- const labelOf = (opts: PickerOption[], v: string) => opts.find((o) => o.value === v)?.label ?? "—";
52
-
53
- const half = { flexGrow: 1, flexBasis: 240 } as const;
54
- const full = { flexGrow: 1, flexBasis: "100%" } as const;
55
-
56
- export function TplWizard() {
57
- const [step, setStep] = useState(0);
58
-
59
- // Company
60
- const [companyName, setCompanyName] = useState("Northwind Packaging Co., Ltd.");
61
- const [taxId, setTaxId] = useState("0312456789");
62
- const [industry, setIndustry] = useState("manufacturing");
63
- const [regCity, setRegCity] = useState("Gothenburg");
64
- const [size, setSize] = useState("51-200");
65
-
66
- // Contact
67
- const [contactName, setContactName] = useState("Mara Lindqvist");
68
- const [role, setRole] = useState("Procurement lead");
69
- const [phone, setPhone] = useState("0903 558 214");
70
- const [email, setEmail] = useState("mara@northwind.co");
71
-
72
- // Terms
73
- const [terms, setTerms] = useState("30");
74
- const [creditLimit, setCreditLimit] = useState<number | null>(50000000);
75
- const [vatInvoice, setVatInvoice] = useState(true);
76
- const [notes, setNotes] = useState("");
77
-
78
- const stepValid = [
79
- companyName.trim() !== "" && taxId.trim() !== "",
80
- contactName.trim() !== "" && email.trim() !== "",
81
- terms !== "",
82
- true,
83
- ];
84
- const isLast = step === STEPS.length - 1;
85
-
86
- const goEdit = (s: number) => setStep(s);
87
-
88
- return (
89
- <ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
90
- <View style={{ width: "100%", maxWidth: 720, alignSelf: "center", gap: 16 }}>
91
- <View style={{ gap: 2 }}>
92
- <Text size="xl" weight="semibold">New partner application</Text>
93
- <Text size="sm" color="muted">Four short steps — we save as you go; review everything before submitting</Text>
94
- </View>
95
-
96
- <Card style={{ padding: 0 }}>
97
- <CardBody>
98
- <Stepper steps={STEPS} current={step} />
99
- </CardBody>
100
- <Divider />
101
-
102
- {/* step 0 · Company */}
103
- {step === 0 ? (
104
- <CardBody>
105
- <View style={{ flexDirection: "row", flexWrap: "wrap", columnGap: 16 }}>
106
- <FormField label="Legal name" style={full}>
107
- <TextInputField value={companyName} onChangeText={setCompanyName} placeholder="Name on the business registration" autoFocus />
108
- </FormField>
109
- <FormField label="Tax ID" style={half}>
110
- <TextInputField value={taxId} onChangeText={setTaxId} placeholder="10 or 13 digits" inputMode="numeric" />
111
- </FormField>
112
- <FormField label="Industry" style={half}>
113
- <Picker options={INDUSTRY} value={industry} onValueChange={setIndustry} placeholder="Select industry" accessibilityLabel="Industry" />
114
- </FormField>
115
- <FormField label="Registered city" style={half}>
116
- <TextInputField value={regCity} onChangeText={setRegCity} placeholder="City" />
117
- </FormField>
118
- <FormField label="Company size" style={half}>
119
- <Picker options={SIZE} value={size} onValueChange={setSize} placeholder="Headcount" accessibilityLabel="Company size" />
120
- </FormField>
121
- </View>
122
- </CardBody>
123
- ) : null}
124
-
125
- {/* step 1 · Contact */}
126
- {step === 1 ? (
127
- <CardBody>
128
- <View style={{ flexDirection: "row", flexWrap: "wrap", columnGap: 16 }}>
129
- <FormField label="Contact person" style={half}>
130
- <TextInputField value={contactName} onChangeText={setContactName} placeholder="Full name" autoFocus />
131
- </FormField>
132
- <FormField label="Role" style={half}>
133
- <TextInputField value={role} onChangeText={setRole} placeholder="Job title" />
134
- </FormField>
135
- <FormField label="Phone" style={half}>
136
- <TextInputField value={phone} onChangeText={setPhone} placeholder="Mobile number" inputMode="tel" />
137
- </FormField>
138
- <FormField label="Email" style={half}>
139
- <TextInputField value={email} onChangeText={setEmail} placeholder="name@company.com" inputMode="email" autoCapitalize="none" />
140
- </FormField>
141
- </View>
142
- </CardBody>
143
- ) : null}
144
-
145
- {/* step 2 · Terms */}
146
- {step === 2 ? (
147
- <CardBody>
148
- <View style={{ flexDirection: "row", flexWrap: "wrap", columnGap: 16 }}>
149
- <FormField label="Payment terms" style={half}>
150
- <Picker options={TERMS} value={terms} onValueChange={setTerms} placeholder="Select terms" accessibilityLabel="Payment terms" />
151
- </FormField>
152
- <FormField label="Requested credit limit" style={half}>
153
- <NumberInput value={creditLimit} onValueChange={setCreditLimit} min={0} accessibilityLabel="Requested credit limit" />
154
- </FormField>
155
- <FormField label="VAT invoice" style={full}>
156
- <View style={{ flexDirection: "row", alignItems: "center", gap: 10, minHeight: 40 }}>
157
- <Switch accessibilityLabel="VAT invoice" value={vatInvoice} onChange={setVatInvoice} />
158
- <Text size="sm" color="muted">{vatInvoice ? "Issue an invoice for every order" : "No invoices issued"}</Text>
159
- </View>
160
- </FormField>
161
- <FormField label="Notes" optional optionalLabel="Optional" style={full}>
162
- <TextInputField value={notes} onChangeText={setNotes} placeholder="Anything the credit team should know" numberOfLines={3} />
163
- </FormField>
164
- </View>
165
- </CardBody>
166
- ) : null}
167
-
168
- {/* step 3 · Review */}
169
- {step === 3 ? (
170
- <CardBody>
171
- <Callout tone="info"><CalloutText>Check every detail — once submitted, changes need a credit-team request.</CalloutText></Callout>
172
- <ReviewGroup title="Company" onEdit={() => goEdit(0)}>
173
- <DetailRow label="Legal name" labelWidth={150}><Text size="sm">{companyName}</Text></DetailRow>
174
- <DetailRow label="Tax ID" labelWidth={150}><Text size="sm" tabular>{taxId}</Text></DetailRow>
175
- <DetailRow label="Industry" labelWidth={150}><Text size="sm">{labelOf(INDUSTRY, industry)}</Text></DetailRow>
176
- <DetailRow label="Registered city" labelWidth={150}><Text size="sm">{regCity}</Text></DetailRow>
177
- <DetailRow label="Company size" labelWidth={150}><Text size="sm">{labelOf(SIZE, size)}</Text></DetailRow>
178
- </ReviewGroup>
179
- <Divider />
180
- <ReviewGroup title="Contact" onEdit={() => goEdit(1)}>
181
- <DetailRow label="Contact person" labelWidth={150}><Text size="sm">{contactName}</Text></DetailRow>
182
- <DetailRow label="Role" labelWidth={150}><Text size="sm">{role}</Text></DetailRow>
183
- <DetailRow label="Phone" labelWidth={150}><Text size="sm" tabular>{phone}</Text></DetailRow>
184
- <DetailRow label="Email" labelWidth={150}><Text size="sm">{email}</Text></DetailRow>
185
- </ReviewGroup>
186
- <Divider />
187
- <ReviewGroup title="Terms" onEdit={() => goEdit(2)}>
188
- <DetailRow label="Payment terms" labelWidth={150}><Text size="sm">{labelOf(TERMS, terms)}</Text></DetailRow>
189
- <DetailRow label="Credit limit" labelWidth={150}><Text size="sm" tabular>{creditLimit == null ? "—" : formatMoney(creditLimit)}</Text></DetailRow>
190
- <DetailRow label="VAT invoice" labelWidth={150}><Text size="sm">{vatInvoice ? "Yes" : "No"}</Text></DetailRow>
191
- <DetailRow label="Notes" labelWidth={150}><Text size="sm" color={notes ? "default" : "muted"}>{notes || "—"}</Text></DetailRow>
192
- </ReviewGroup>
193
- </CardBody>
194
- ) : null}
195
-
196
- <Divider />
197
- <View style={{ flexDirection: "row", alignItems: "center", paddingHorizontal: 20, paddingVertical: 16, gap: 8 }}>
198
- {step > 0 ? <Button title="Back" color="muted" icon="chevron-left" onPress={() => setStep((s) => s - 1)} /> : null}
199
- <View style={{ flex: 1 }} />
200
- <Text size="xs" color="muted">{`Step ${step + 1} of ${STEPS.length}`}</Text>
201
- {isLast ? (
202
- <Button title="Submit application" color="primary" onPress={() => {}} />
203
- ) : (
204
- <Button title="Next" color="primary" disabled={!stepValid[step]} onPress={() => setStep((s) => s + 1)} />
205
- )}
206
- </View>
207
- </Card>
208
- </View>
209
- </ScrollView>
210
- );
211
- }
212
-
213
- function ReviewGroup({ title, onEdit, children }: { title: string; onEdit: () => void; children: React.ReactNode }) {
214
- return (
215
- <View style={{ gap: 2 }}>
216
- <View style={{ flexDirection: "row", alignItems: "center", paddingBottom: 4 }}>
217
- <Text size="xs" color="muted" transform="uppercase" style={{ flex: 1 }}>{title}</Text>
218
- <Button title="Edit" color="muted" icon="pencil" onPress={onEdit} />
219
- </View>
220
- {children}
221
- </View>
222
- );
223
- }
@@ -1,114 +0,0 @@
1
- import { StyleSheet, View } from "react-native";
2
- import { colors, solid } from "./colors";
3
- import { Text } from "./text";
4
- import { Icon } from "./icon";
5
- import { Badge } from "./badge";
6
- import { Button } from "./button";
7
- import { CardSelectItem } from "./card_select_item";
8
-
9
- export interface DiscrepancyValue {
10
- /** Where this value came from — the document / record / system of record. */
11
- source: string;
12
- value: string;
13
- /** The agent's recommended truth among the conflicting values. */
14
- recommended?: boolean;
15
- }
16
-
17
- export interface DiscrepancyProps {
18
- /** The field that disagrees across sources. */
19
- field: string;
20
- /** The agent's explanation of the conflict — shown above the options so the
21
- * human reads WHY before picking. */
22
- note?: string;
23
- values: DiscrepancyValue[];
24
- /** Resolve the conflict by picking a value (index into `values`) — pressing a
25
- * card commits it; there is no separate confirm button. */
26
- onResolve?: (index: number) => void;
27
- /** Send for manual handling instead of picking. */
28
- onFlag?: () => void;
29
- /** Resolved → the chosen index settles the card. */
30
- resolvedIndex?: number;
31
- flagged?: boolean;
32
- }
33
-
34
- /**
35
- * A field whose value DISAGREES across sources — the unit of an AI cross-check
36
- * / audit. The agent's explanation sits up top; below it the conflicting values
37
- * are identical selectable cards (the global press/hover ring), the one the
38
- * agent believes carrying a neutral "Agent's pick" tag — NOT a pre-selected
39
- * highlight. Pressing a card resolves the conflict to it — no confirm step.
40
- * Symmetric, unlike `ChangeReview`'s before→after.
41
- */
42
- export function Discrepancy(props: DiscrepancyProps) {
43
- const { field, values, note, onResolve, onFlag, resolvedIndex, flagged } = props;
44
- const resolved = resolvedIndex != null || flagged === true;
45
-
46
- return (
47
- <View style={[styles.card, resolved ? styles.resolved : null]}>
48
- <Text size="sm" weight="semibold" numberOfLines={1}>
49
- {field}
50
- </Text>
51
-
52
- {resolved ? (
53
- <View style={styles.outcome}>
54
- <Icon name={flagged ? "triangle-alert" : "circle-check"} size={15} color={flagged ? solid("amber") : solid("emerald")} />
55
- <Text size="sm" color="muted">
56
- {flagged
57
- ? "Flagged for manual review"
58
- : `Resolved to ${values[resolvedIndex as number].source} — ${values[resolvedIndex as number].value}`}
59
- </Text>
60
- </View>
61
- ) : (
62
- <>
63
- {note ? (
64
- <Text size="sm" color="muted">
65
- {note}
66
- </Text>
67
- ) : null}
68
-
69
- <View style={styles.values}>
70
- {values.map((v, i) => (
71
- <CardSelectItem
72
- key={`${v.source}-${i}`}
73
- accessibilityLabel={`Resolve ${field} to ${v.source}: ${v.value}`}
74
- onPress={() => onResolve?.(i)}
75
- style={styles.valueBox}
76
- >
77
- <Text size="sm" color="muted" numberOfLines={1} style={{ flexShrink: 1 }}>
78
- {v.source}
79
- </Text>
80
- <View style={{ flex: 1 }} />
81
- {v.recommended ? <Badge label="Agent's pick" /> : null}
82
- <Text size="md" weight="semibold" tabular>
83
- {v.value}
84
- </Text>
85
- </CardSelectItem>
86
- ))}
87
- </View>
88
-
89
- {onFlag ? (
90
- <View style={styles.footer}>
91
- <Button title="Flag for review" color="muted" onPress={onFlag} />
92
- </View>
93
- ) : null}
94
- </>
95
- )}
96
- </View>
97
- );
98
- }
99
-
100
- const styles = StyleSheet.create({
101
- card: {
102
- borderWidth: 1,
103
- borderColor: colors.border,
104
- backgroundColor: colors.white,
105
- borderRadius: 12,
106
- padding: 16,
107
- gap: 12,
108
- },
109
- resolved: { backgroundColor: colors.zinc[50] },
110
- outcome: { flexDirection: "row", alignItems: "center", gap: 8 },
111
- values: { gap: 8 },
112
- valueBox: { flexDirection: "row", alignItems: "center", gap: 12, paddingVertical: 12, paddingHorizontal: 14 },
113
- footer: { flexDirection: "row", justifyContent: "flex-end" },
114
- });
@@ -1,79 +0,0 @@
1
- import { type ReactNode } from "react";
2
- import { StyleSheet, View } from "react-native";
3
- import { colors } from "./colors";
4
- import { Text } from "./text";
5
- import { Icon } from "./icon";
6
-
7
- export interface MatchSideTextProps {
8
- /** Primary line — the record name / id. */
9
- title: string;
10
- /** Secondary line — amount, date, customer. */
11
- detail?: string;
12
- /** Right-align for the proposed (right) side of a pairing. */
13
- align?: "left" | "right";
14
- }
15
-
16
- /**
17
- * The DEFAULT side content — a title + a muted detail line. Pass it into a
18
- * `MatchSides` slot for the common case; drop a different node in when a side
19
- * needs more (a `Peek` to preview the record, an interactive card, a thumbnail).
20
- */
21
- export function MatchSideText({ title, detail, align }: MatchSideTextProps) {
22
- const right = align === "right";
23
- return (
24
- <>
25
- <Text size="sm" weight="semibold" numberOfLines={1} align={right ? "right" : undefined}>
26
- {title}
27
- </Text>
28
- {detail ? (
29
- <Text size="xs" color="muted" numberOfLines={1} align={right ? "right" : undefined}>
30
- {detail}
31
- </Text>
32
- ) : null}
33
- </>
34
- );
35
- }
36
-
37
- export interface MatchSidesProps {
38
- /** Left content — ANY node. `MatchSideText` for the default, or a `Peek` /
39
- * interactive card / thumbnail when a side needs to do more. */
40
- source: ReactNode;
41
- /** Right content. Omit → the "no confident match" placeholder. */
42
- match?: ReactNode;
43
- /** The connector between the sides. Default: a right arrow. Override to carry
44
- * a swap control, a label, or a different glyph. */
45
- connector?: ReactNode;
46
- }
47
-
48
- /**
49
- * The two-sided PAIRING — a left slot, a connector, a right slot. A pure LAYOUT
50
- * primitive: it owns the two-column arrangement and the connector, the CONTENT
51
- * of each side is yours (`MatchSideText`, a `Peek`, anything). The hero of a
52
- * reconciliation; the confidence, reason, and decision come from the `ReviewCard`.
53
- */
54
- export function MatchSides(props: MatchSidesProps) {
55
- return (
56
- <View style={styles.pair}>
57
- <View style={styles.side}>{props.source}</View>
58
- {props.connector ?? <Icon name="arrow-right" size={16} color={colors.zinc[400]} />}
59
- <View style={[styles.side, styles.sideRight]}>
60
- {props.match ?? (
61
- <>
62
- <Text size="sm" weight="medium" color="muted" align="right" numberOfLines={1}>
63
- No confident match
64
- </Text>
65
- <Text size="xs" color="muted" align="right" numberOfLines={1}>
66
- Pick a counterpart
67
- </Text>
68
- </>
69
- )}
70
- </View>
71
- </View>
72
- );
73
- }
74
-
75
- const styles = StyleSheet.create({
76
- pair: { flexDirection: "row", alignItems: "center", gap: 14 },
77
- side: { flex: 1, gap: 3, minWidth: 0 },
78
- sideRight: { alignItems: "flex-end" },
79
- });
@@ -1,68 +0,0 @@
1
- import { StyleSheet, View } from "react-native";
2
- import { solid } from "./colors";
3
- import { Text } from "./text";
4
- import { InlineTextInput } from "./inline_text_input";
5
-
6
- export interface RecordField {
7
- label: string;
8
- value: string;
9
- /** Unit suffix shown after the value (₫, kg, pcs). */
10
- unit?: string;
11
- /** Mark a low-confidence field worth double-checking — a subtle amber dot. */
12
- uncertain?: boolean;
13
- }
14
-
15
- export interface RecordFieldsProps {
16
- fields: RecordField[];
17
- /** Edit a field — fired on save of the inline editor (blur / Enter). When set,
18
- * each value is click-to-edit; omit for a read-only record. */
19
- onEditField?: (index: number, value: string) => void;
20
- }
21
-
22
- /**
23
- * The BODY of an extracted record — a label/value list, each value click-to-edit
24
- * (`InlineTextInput`) or read-only. A pure body atom: drop it into a `ReviewCard`
25
- * to review-before-save (a confidence + Confirm / Remove, stacked under a Save-all
26
- * → `tpl_extract`), or use it standalone as a live-edit field card (a params
27
- * panel). The review MECHANICS belong to the host / `ReviewCard`, not here.
28
- */
29
- export function RecordFields(props: RecordFieldsProps) {
30
- return (
31
- <View style={styles.fields}>
32
- {props.fields.map((f, i) => (
33
- <View key={`${f.label}-${i}`} style={styles.fieldRow}>
34
- <View style={styles.labelCol}>
35
- {f.uncertain ? <View style={styles.uncertainDot} /> : null}
36
- <Text size="sm" color="muted" numberOfLines={1}>
37
- {f.label}
38
- </Text>
39
- </View>
40
- <View style={styles.valueCol}>
41
- <View style={{ flex: 1 }}>
42
- {props.onEditField ? (
43
- <InlineTextInput value={f.value} accessibilityLabel={`Edit ${f.label}`} onSave={(next) => props.onEditField?.(i, next)} />
44
- ) : (
45
- <Text size="sm" weight="medium" tabular>
46
- {f.value}
47
- </Text>
48
- )}
49
- </View>
50
- {f.unit ? (
51
- <Text size="sm" color="muted">
52
- {f.unit}
53
- </Text>
54
- ) : null}
55
- </View>
56
- </View>
57
- ))}
58
- </View>
59
- );
60
- }
61
-
62
- const styles = StyleSheet.create({
63
- fields: { gap: 2 },
64
- fieldRow: { flexDirection: "row", alignItems: "center", gap: 16, minHeight: 34 },
65
- labelCol: { flexDirection: "row", alignItems: "center", gap: 6, width: 132 },
66
- uncertainDot: { width: 6, height: 6, borderRadius: 999, backgroundColor: solid("amber") },
67
- valueCol: { flex: 1, flexDirection: "row", alignItems: "center", gap: 6 },
68
- });