@lotics/ui 8.0.0 → 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 (48) hide show
  1. package/AGENTS.md +171 -68
  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_item_list.tsx +1015 -124
  7. package/examples/tpl_pick.tsx +3 -3
  8. package/examples/tpl_pivot.tsx +1 -1
  9. package/examples/tpl_record.tsx +1354 -0
  10. package/examples/tpl_report.tsx +7 -7
  11. package/examples/tpl_rollup.tsx +6 -6
  12. package/examples/tpl_shifts.tsx +2 -2
  13. package/examples/tpl_statements.tsx +221 -0
  14. package/examples/tpl_stock.tsx +7 -7
  15. package/examples/tpl_task_board.tsx +16 -13
  16. package/examples/tpl_tasks.tsx +15 -28
  17. package/examples/tpl_tower.tsx +2 -2
  18. package/package.json +8 -1
  19. package/src/capture_row.tsx +59 -0
  20. package/src/checklist.tsx +104 -0
  21. package/src/chip.tsx +12 -3
  22. package/src/detail_row.tsx +137 -10
  23. package/src/inline_date_picker.tsx +8 -3
  24. package/src/inline_edit.tsx +40 -10
  25. package/src/inline_member_select.tsx +3 -0
  26. package/src/inline_number_input.tsx +5 -2
  27. package/src/inline_select.tsx +8 -3
  28. package/src/inline_tag_select.tsx +140 -0
  29. package/src/inline_text_input.tsx +5 -2
  30. package/src/inline_time_picker.tsx +5 -2
  31. package/src/ledger.tsx +220 -0
  32. package/src/locale.tsx +21 -0
  33. package/src/progress_bar.tsx +32 -1
  34. package/src/record_summary.tsx +101 -0
  35. package/src/section_heading.tsx +16 -8
  36. package/src/suggestion_chip.tsx +47 -0
  37. package/src/use_section_nav.test.ts +69 -0
  38. package/src/use_section_nav.ts +59 -0
  39. package/examples/tpl_billing.tsx +0 -344
  40. package/examples/tpl_detail.tsx +0 -232
  41. package/examples/tpl_directory.tsx +0 -260
  42. package/examples/tpl_intake.tsx +0 -206
  43. package/examples/tpl_order.tsx +0 -482
  44. package/examples/tpl_quick.tsx +0 -211
  45. package/examples/tpl_record_plain.tsx +0 -259
  46. package/examples/tpl_settings.tsx +0 -178
  47. package/examples/tpl_timeline.tsx +0 -244
  48. package/examples/tpl_wizard.tsx +0 -223
@@ -1,244 +0,0 @@
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 { ActionMenu, type ActionMenuItem } from "@lotics/ui/action_menu";
6
- import { Badge } from "@lotics/ui/badge";
7
- import { Card, CardHeader, CardHeaderTitle, CardHeaderMeta } from "@lotics/ui/card";
8
- import { Icon, type IconName } from "@lotics/ui/icon";
9
- import { ChipGroup } from "@lotics/ui/chip_group";
10
- import { Timeline, type TimelineItem } from "@lotics/ui/timeline";
11
- import { KPICard } from "@lotics/ui/kpi_card";
12
- import { formatMoney } from "@lotics/ui/format_money";
13
-
14
- // ─────────────────────────────────────────────────────────────────────────────
15
- // Template · Activity log — the audit feed of ONE record (an order).
16
- // Grammar: zinc-50 canvas · header band · filter pills (ChipGroup) · one Card
17
- // per DAY, each card = date band → Divider → Timeline. Icon color = event
18
- // category accent (blue = documents, emerald = payments, zinc = system,
19
- // red = warnings). Times right-aligned tabular; expandable details on the
20
- // events that carry numbers. No dead rows: events without inline details
21
- // carry an ActionMenu (⋯) with the event's real operations — open/download
22
- // the document, view the workflow run, re-send the reminder.
23
- // ─────────────────────────────────────────────────────────────────────────────
24
-
25
- type Cat = "chungtu" | "thanhtoan" | "hethong";
26
-
27
- interface MockEvent {
28
- id: string;
29
- cat: Cat;
30
- icon: IconName;
31
- color: string;
32
- label: string;
33
- time: string; // HH:mm
34
- stats?: { label: string; value: string; tone?: "danger" }[];
35
- file?: string;
36
- note?: string;
37
- actions?: ActionMenuItem[];
38
- }
39
-
40
- interface DayGroup {
41
- id: string;
42
- label: string; // "Today · 12/06"
43
- events: MockEvent[];
44
- }
45
-
46
- const DAYS: DayGroup[] = [
47
- {
48
- id: "d0",
49
- label: "Today · 12/06",
50
- events: [
51
- {
52
- id: "e1", cat: "thanhtoan", icon: "circle-check", color: solid("emerald"),
53
- label: "Accounting confirmed installment 2 — order settled", time: "14:32",
54
- stats: [
55
- { label: "Amount", value: formatMoney(12_600_000) },
56
- { label: "Method", value: "Bank transfer" },
57
- { label: "Outstanding", value: formatMoney(0) },
58
- ],
59
- note: "Reference TRX-0612-114 · First National Bank",
60
- },
61
- {
62
- id: "e2", cat: "chungtu", icon: "file-up", color: solid("blue"),
63
- label: "Nina Adams uploaded the signed delivery note", time: "11:05",
64
- file: "DeliveryNote_DN-0042_signed.pdf",
65
- actions: [
66
- { key: "xem", label: "View document", icon: "eye", onPress: () => {} },
67
- { key: "tai", label: "Download", icon: "download", onPress: () => {} },
68
- ],
69
- },
70
- {
71
- id: "e3", cat: "hethong", icon: "bolt", color: colors.zinc[500],
72
- label: "Delivery-reconciliation workflow ran — quantities match the note", time: "11:06",
73
- actions: [
74
- { key: "chay", label: "View run", icon: "history", onPress: () => {} },
75
- { key: "mo", label: "Open workflow", icon: "external-link", onPress: () => {} },
76
- ],
77
- },
78
- {
79
- id: "e4", cat: "hethong", icon: "bell", color: colors.zinc[500],
80
- label: "Reminded Accounting: order awaiting invoice", time: "11:06",
81
- actions: [
82
- { key: "xem", label: "View notification", icon: "bell", onPress: () => {} },
83
- { key: "nhac", label: "Remind again", icon: "repeat", onPress: () => {} },
84
- ],
85
- },
86
- ],
87
- },
88
- {
89
- id: "d1",
90
- label: "Yesterday · 11/06",
91
- events: [
92
- {
93
- id: "e5", cat: "chungtu", icon: "file-text", color: solid("blue"),
94
- label: "VAT invoice issued from the Sales invoice template", time: "16:40",
95
- file: "Invoice_INV-2026-0584.pdf",
96
- actions: [
97
- { key: "xem", label: "View invoice", icon: "eye", onPress: () => {} },
98
- { key: "tai", label: "Download", icon: "download", onPress: () => {} },
99
- { key: "gui", label: "Send to customer", icon: "send", onPress: () => {} },
100
- ],
101
- },
102
- {
103
- id: "e6", cat: "thanhtoan", icon: "credit-card", color: solid("emerald"),
104
- label: "Installment 1 recorded (50% per contract)", time: "10:18",
105
- stats: [
106
- { label: "Amount", value: formatMoney(12_600_000) },
107
- { label: "Outstanding", value: formatMoney(12_600_000), tone: "danger" },
108
- ],
109
- },
110
- {
111
- id: "e7", cat: "hethong", icon: "circle-alert", color: solid("red"),
112
- label: "Warning: delivered 2 days past the needed-by date", time: "08:00",
113
- note: "Needed by 09/06 — truck actually delivered 11/06. Logged against the delivery KPI.",
114
- },
115
- ],
116
- },
117
- {
118
- id: "d2",
119
- label: "Tuesday · 09/06",
120
- events: [
121
- {
122
- id: "e8", cat: "hethong", icon: "pencil", color: colors.zinc[500],
123
- label: "Mark Vu edited the Expected delivery date field", time: "15:22",
124
- stats: [
125
- { label: "Before", value: "09/06" },
126
- { label: "After", value: "11/06" },
127
- ],
128
- note: "Reason: gluing line jammed at the plant — customer notified by chat.",
129
- },
130
- {
131
- id: "e9", cat: "chungtu", icon: "send", color: solid("blue"),
132
- label: "Approved quote emailed to the customer", time: "09:47",
133
- file: "Quote_QT-2026-0042.pdf",
134
- actions: [
135
- { key: "xem", label: "View quote", icon: "eye", onPress: () => {} },
136
- { key: "tai", label: "Download", icon: "download", onPress: () => {} },
137
- { key: "gui", label: "Re-send email", icon: "send", onPress: () => {} },
138
- ],
139
- },
140
- {
141
- id: "e10", cat: "hethong", icon: "circle-check", color: solid("emerald"),
142
- label: "Director approved the order — moved to Production order", time: "09:30",
143
- actions: [
144
- { key: "duyet", label: "View approval", icon: "eye", onPress: () => {} },
145
- { key: "lien-ket", label: "Copy link", icon: "copy", onPress: () => {} },
146
- ],
147
- },
148
- ],
149
- },
150
- ];
151
-
152
- const FILTERS = [
153
- { value: "all", label: "All" },
154
- { value: "chungtu", label: "Documents" },
155
- { value: "thanhtoan", label: "Payments" },
156
- { value: "hethong", label: "System" },
157
- ] as const;
158
-
159
- function EventDetails({ ev }: { ev: MockEvent }) {
160
- return (
161
- <View style={{ gap: 8, paddingVertical: 4 }}>
162
- {ev.stats ? (
163
- <View style={{ flexDirection: "row", gap: 28 }}>
164
- {ev.stats.map((s) => (
165
- <KPICard key={s.label} label={s.label} value={s.value} format="none" size="sm" tone={s.tone} />
166
- ))}
167
- </View>
168
- ) : null}
169
- {ev.note ? <Text size="xs" color="muted">{ev.note}</Text> : null}
170
- </View>
171
- );
172
- }
173
-
174
- function toTimelineItem(ev: MockEvent): TimelineItem {
175
- const hasDetails = !!ev.stats || !!ev.note;
176
- return {
177
- id: ev.id,
178
- icon: ev.icon,
179
- iconColor: ev.color,
180
- label: ev.label,
181
- right: (
182
- <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
183
- {ev.file ? (
184
- <View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
185
- <Icon name="file" size={12} color={colors.zinc[400]} />
186
- <Text size="xs" color="muted" numberOfLines={1}>{ev.file}</Text>
187
- </View>
188
- ) : null}
189
- <Text size="xs" color="muted" tabular>{ev.time}</Text>
190
- {ev.actions ? (
191
- <ActionMenu items={ev.actions} accessibilityLabel={`Actions: ${ev.label}`} />
192
- ) : null}
193
- </View>
194
- ),
195
- details: hasDetails ? <EventDetails ev={ev} /> : undefined,
196
- };
197
- }
198
-
199
- export function TplTimeline() {
200
- const [filter, setFilter] = useState<string>("all");
201
-
202
- const all = DAYS.flatMap((d) => d.events);
203
- const countOf = (v: string) => (v === "all" ? all.length : all.filter((e) => e.cat === v).length);
204
- const days = DAYS
205
- .map((d) => ({ ...d, events: filter === "all" ? d.events : d.events.filter((e) => e.cat === filter) }))
206
- .filter((d) => d.events.length > 0);
207
-
208
- return (
209
- <ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
210
- <View style={{ width: "100%", maxWidth: 880, alignSelf: "center", gap: 16 }}>
211
- {/* header band */}
212
- <View style={{ flexDirection: "row", alignItems: "flex-end", gap: 12 }}>
213
- <View style={{ gap: 2, flex: 1 }}>
214
- <Text size="xl" weight="semibold">Activity log</Text>
215
- <Text size="sm" color="muted">Order SO-2026-0042 · NEWTECONS — who did what, when, with the paper trail</Text>
216
- </View>
217
- <Badge label="Settled" color="emerald" />
218
- </View>
219
-
220
- {/* filter band — pills narrow the feed by event type */}
221
- <ChipGroup
222
- accessibilityLabel="Filter activity"
223
- options={FILTERS.map((f) => ({ label: `${f.label} · ${countOf(f.value)}`, value: f.value }))}
224
- value={filter}
225
- onValueChange={setFilter}
226
- />
227
-
228
- {/* one card per day: date band → hairline → timeline */}
229
- {days.map((d) => (
230
- <Card key={d.id} style={{ padding: 0 }}>
231
- <CardHeader>
232
- <CardHeaderTitle>{d.label}</CardHeaderTitle>
233
- <CardHeaderMeta>{`${d.events.length} events`}</CardHeaderMeta>
234
- </CardHeader>
235
- {/* Last Timeline row carries its own 14px tail — trim the band's bottom to keep 16px optical. */}
236
- <View style={{ paddingTop: 16, paddingHorizontal: 16, paddingBottom: 2 }}>
237
- <Timeline items={d.events.map(toTimelineItem)} />
238
- </View>
239
- </Card>
240
- ))}
241
- </View>
242
- </ScrollView>
243
- );
244
- }
@@ -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
- }