@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.
- package/AGENTS.md +298 -159
- package/examples/tpl_allocate.tsx +2 -2
- package/examples/tpl_attendance.tsx +2 -2
- package/examples/tpl_calendar.tsx +1 -1
- package/examples/tpl_dashboard.tsx +1 -1
- package/examples/tpl_dieline.tsx +3 -3
- package/examples/tpl_documents.tsx +790 -0
- package/examples/tpl_item_list.tsx +1015 -124
- package/examples/tpl_lookup.tsx +37 -10
- package/examples/tpl_pick.tsx +3 -3
- package/examples/tpl_pivot.tsx +1 -1
- package/examples/tpl_record.tsx +1354 -0
- package/examples/tpl_report.tsx +7 -7
- package/examples/tpl_rollup.tsx +6 -6
- package/examples/tpl_shifts.tsx +2 -2
- package/examples/tpl_statements.tsx +221 -0
- package/examples/tpl_stock.tsx +7 -7
- package/examples/tpl_task_board.tsx +48 -32
- package/examples/tpl_tasks.tsx +47 -47
- package/examples/tpl_tower.tsx +2 -2
- package/package.json +9 -9
- package/src/agent_run.tsx +1 -1
- package/src/capture_row.tsx +59 -0
- package/src/change_review.tsx +732 -236
- package/src/checklist.tsx +104 -0
- package/src/chip.tsx +12 -3
- package/src/composer.tsx +22 -1
- package/src/confidence.tsx +1 -1
- package/src/control_surface.ts +0 -14
- package/src/detail_row.tsx +137 -10
- package/src/finding.tsx +112 -80
- package/src/floating_action_bar.tsx +1 -1
- package/src/inline_date_picker.tsx +8 -3
- package/src/inline_edit.tsx +40 -10
- package/src/inline_member_select.tsx +3 -0
- package/src/inline_number_input.tsx +5 -2
- package/src/inline_select.tsx +8 -3
- package/src/inline_tag_select.tsx +140 -0
- package/src/inline_text_input.tsx +5 -2
- package/src/inline_time_picker.tsx +5 -2
- package/src/ledger.tsx +220 -0
- package/src/locale.tsx +43 -16
- package/src/progress_bar.tsx +32 -1
- package/src/record_summary.tsx +101 -0
- package/src/section_heading.tsx +16 -8
- package/src/sources.tsx +8 -5
- package/src/suggestion_chip.tsx +47 -0
- package/src/use_section_nav.test.ts +69 -0
- package/src/use_section_nav.ts +59 -0
- package/examples/tpl_assistant.tsx +0 -174
- package/examples/tpl_billing.tsx +0 -344
- package/examples/tpl_briefing.tsx +0 -121
- package/examples/tpl_compare.tsx +0 -133
- package/examples/tpl_crosscheck.tsx +0 -120
- package/examples/tpl_detail.tsx +0 -232
- package/examples/tpl_directory.tsx +0 -260
- package/examples/tpl_draft.tsx +0 -163
- package/examples/tpl_extract.tsx +0 -193
- package/examples/tpl_intake.tsx +0 -206
- package/examples/tpl_match.tsx +0 -134
- package/examples/tpl_order.tsx +0 -482
- package/examples/tpl_quick.tsx +0 -211
- package/examples/tpl_ratedesk.tsx +0 -386
- package/examples/tpl_record_plain.tsx +0 -259
- package/examples/tpl_settings.tsx +0 -178
- package/examples/tpl_timeline.tsx +0 -244
- package/examples/tpl_triage.tsx +0 -112
- package/examples/tpl_wizard.tsx +0 -223
- package/src/discrepancy.tsx +0 -114
- package/src/match_sides.tsx +0 -79
- package/src/record_fields.tsx +0 -68
- package/src/review_card.tsx +0 -172
- package/src/scored_option.tsx +0 -138
- package/src/spec_list.tsx +0 -81
- package/src/triage_row.tsx +0 -99
package/examples/tpl_triage.tsx
DELETED
|
@@ -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
|
-
}
|
package/examples/tpl_wizard.tsx
DELETED
|
@@ -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
|
-
}
|
package/src/discrepancy.tsx
DELETED
|
@@ -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
|
-
});
|
package/src/match_sides.tsx
DELETED
|
@@ -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
|
-
});
|
package/src/record_fields.tsx
DELETED
|
@@ -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
|
-
});
|