@lotics/ui 8.0.0 → 10.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 +177 -70
- 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_item_list.tsx +1015 -124
- 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 +16 -13
- package/examples/tpl_tasks.tsx +15 -28
- package/examples/tpl_tower.tsx +2 -2
- package/package.json +8 -7
- package/src/capture_row.tsx +59 -0
- package/src/checklist.tsx +104 -0
- package/src/chip.tsx +12 -3
- package/src/detail_row.tsx +137 -10
- 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 +25 -0
- package/src/page_header.tsx +0 -2
- package/src/popover_nav.tsx +40 -0
- package/src/progress_bar.tsx +32 -1
- package/src/record_summary.tsx +101 -0
- package/src/section_heading.tsx +16 -8
- package/src/suggestion_chip.tsx +47 -0
- package/src/trend_footer.tsx +3 -1
- package/src/use_screen_size.ts +1 -1
- package/src/use_section_nav.test.ts +69 -0
- package/src/use_section_nav.ts +59 -0
- package/examples/tpl_billing.tsx +0 -344
- package/examples/tpl_detail.tsx +0 -232
- package/examples/tpl_directory.tsx +0 -260
- package/examples/tpl_intake.tsx +0 -206
- package/examples/tpl_order.tsx +0 -482
- package/examples/tpl_quick.tsx +0 -211
- 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_wizard.tsx +0 -223
- package/src/animation_horizontal_slide.tsx +0 -75
- package/src/form_time_picker.tsx +0 -22
- package/src/highlighted_text.tsx +0 -92
- package/src/menu_title.tsx +0 -15
- package/src/pager_view.tsx +0 -167
- package/src/popover_header.tsx +0 -38
|
@@ -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
|
-
}
|
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
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useRef } from "react";
|
|
2
|
-
import { Animated, StyleProp, ViewStyle } from "react-native";
|
|
3
|
-
|
|
4
|
-
interface AnimationHorizontalSlideProps {
|
|
5
|
-
width: number;
|
|
6
|
-
visible: boolean;
|
|
7
|
-
children?: React.ReactNode;
|
|
8
|
-
onSlideAnimationEnd?: () => void;
|
|
9
|
-
onCollapseAnimationEnd?: () => void;
|
|
10
|
-
style?: StyleProp<ViewStyle>;
|
|
11
|
-
contentContainerStyle?: StyleProp<ViewStyle>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function AnimationHorizontalSlide(props: AnimationHorizontalSlideProps) {
|
|
15
|
-
const {
|
|
16
|
-
width: intrinsicWidth,
|
|
17
|
-
visible,
|
|
18
|
-
children,
|
|
19
|
-
onSlideAnimationEnd,
|
|
20
|
-
onCollapseAnimationEnd,
|
|
21
|
-
style,
|
|
22
|
-
contentContainerStyle,
|
|
23
|
-
} = props;
|
|
24
|
-
const widthAnim = useRef(new Animated.Value(visible ? intrinsicWidth : 0)).current;
|
|
25
|
-
const opacityAnim = useRef(new Animated.Value(visible ? 1 : 0)).current;
|
|
26
|
-
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
Animated.parallel([
|
|
29
|
-
Animated.spring(widthAnim, {
|
|
30
|
-
toValue: visible ? intrinsicWidth : 0,
|
|
31
|
-
bounciness: 0,
|
|
32
|
-
useNativeDriver: false,
|
|
33
|
-
}),
|
|
34
|
-
Animated.spring(opacityAnim, {
|
|
35
|
-
toValue: visible ? 1 : 0,
|
|
36
|
-
bounciness: 0,
|
|
37
|
-
useNativeDriver: false,
|
|
38
|
-
}),
|
|
39
|
-
]).start((animation) => {
|
|
40
|
-
if (animation.finished) {
|
|
41
|
-
if (visible) {
|
|
42
|
-
onSlideAnimationEnd?.();
|
|
43
|
-
} else {
|
|
44
|
-
onCollapseAnimationEnd?.();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
}, [
|
|
49
|
-
widthAnim,
|
|
50
|
-
opacityAnim,
|
|
51
|
-
visible,
|
|
52
|
-
intrinsicWidth,
|
|
53
|
-
onSlideAnimationEnd,
|
|
54
|
-
onCollapseAnimationEnd,
|
|
55
|
-
]);
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<Animated.View style={[{ width: widthAnim, opacity: opacityAnim }, style]}>
|
|
59
|
-
<Animated.View
|
|
60
|
-
style={[
|
|
61
|
-
{
|
|
62
|
-
display: visible ? "flex" : "none",
|
|
63
|
-
position: "absolute",
|
|
64
|
-
top: 0,
|
|
65
|
-
bottom: 0,
|
|
66
|
-
width: intrinsicWidth,
|
|
67
|
-
},
|
|
68
|
-
contentContainerStyle,
|
|
69
|
-
]}
|
|
70
|
-
>
|
|
71
|
-
{children}
|
|
72
|
-
</Animated.View>
|
|
73
|
-
</Animated.View>
|
|
74
|
-
);
|
|
75
|
-
}
|
package/src/form_time_picker.tsx
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { FormField, FormFieldProps } from "./form_field";
|
|
2
|
-
import { TimePicker, TimePickerProps } from "./time_picker";
|
|
3
|
-
|
|
4
|
-
export interface FormTimePickerProps
|
|
5
|
-
extends Omit<FormFieldProps, "style">,
|
|
6
|
-
TimePickerProps {}
|
|
7
|
-
|
|
8
|
-
export function FormTimePicker(props: FormTimePickerProps) {
|
|
9
|
-
const { label, description, optional, optionalLabel, error, ...timePickerProps } = props;
|
|
10
|
-
|
|
11
|
-
return (
|
|
12
|
-
<FormField
|
|
13
|
-
label={label}
|
|
14
|
-
description={description}
|
|
15
|
-
error={error}
|
|
16
|
-
optional={optional}
|
|
17
|
-
optionalLabel={optionalLabel}
|
|
18
|
-
>
|
|
19
|
-
<TimePicker {...timePickerProps} />
|
|
20
|
-
</FormField>
|
|
21
|
-
);
|
|
22
|
-
}
|
package/src/highlighted_text.tsx
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Text, TextProps } from "./text";
|
|
3
|
-
import { colors } from "./colors";
|
|
4
|
-
import { View, StyleSheet } from "react-native";
|
|
5
|
-
|
|
6
|
-
interface HighlightedTextProps extends Omit<TextProps, "children"> {
|
|
7
|
-
text: string;
|
|
8
|
-
searchTerm: string;
|
|
9
|
-
singleLine?: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function HighlightedText({
|
|
13
|
-
text,
|
|
14
|
-
searchTerm,
|
|
15
|
-
singleLine = false,
|
|
16
|
-
...textProps
|
|
17
|
-
}: HighlightedTextProps) {
|
|
18
|
-
const parts = highlightSearchTerm(text || "", searchTerm || "");
|
|
19
|
-
|
|
20
|
-
// Use numberOfLines prop for single line display instead of custom CSS
|
|
21
|
-
const numberOfLines = singleLine ? 1 : undefined;
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<Text {...textProps} numberOfLines={numberOfLines}>
|
|
25
|
-
{parts.map((part, index) => (
|
|
26
|
-
<Text key={index} style={part.isHighlighted ? styles.highlightedText : undefined}>
|
|
27
|
-
{part.text}
|
|
28
|
-
</Text>
|
|
29
|
-
))}
|
|
30
|
-
</Text>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface TextPart {
|
|
35
|
-
text: string;
|
|
36
|
-
isHighlighted: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function highlightSearchTerm(text: string, searchTerm: string): TextPart[] {
|
|
40
|
-
if (!searchTerm.trim()) {
|
|
41
|
-
return [{ text, isHighlighted: false }];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const searchRegex = new RegExp(`(${escapeRegExp(searchTerm.trim())})`, "gi");
|
|
45
|
-
const parts: TextPart[] = [];
|
|
46
|
-
let lastIndex = 0;
|
|
47
|
-
|
|
48
|
-
text.replace(searchRegex, (match, ...args) => {
|
|
49
|
-
const index = args[args.length - 2] as number; // offset index from regex replace
|
|
50
|
-
|
|
51
|
-
// Add text before the match
|
|
52
|
-
if (index > lastIndex) {
|
|
53
|
-
parts.push({
|
|
54
|
-
text: text.slice(lastIndex, index),
|
|
55
|
-
isHighlighted: false,
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Add the highlighted match
|
|
60
|
-
parts.push({
|
|
61
|
-
text: match,
|
|
62
|
-
isHighlighted: true,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
lastIndex = index + match.length;
|
|
66
|
-
return match;
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
// Add any remaining text after the last match
|
|
70
|
-
if (lastIndex < text.length) {
|
|
71
|
-
parts.push({
|
|
72
|
-
text: text.slice(lastIndex),
|
|
73
|
-
isHighlighted: false,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return parts.length > 0 ? parts : [{ text, isHighlighted: false }];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function escapeRegExp(string: string): string {
|
|
81
|
-
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const styles = StyleSheet.create({
|
|
85
|
-
highlightedText: {
|
|
86
|
-
backgroundColor: colors.yellow[100],
|
|
87
|
-
color: colors.yellow[900],
|
|
88
|
-
fontWeight: "bold" as const,
|
|
89
|
-
paddingHorizontal: 2,
|
|
90
|
-
borderRadius: 2,
|
|
91
|
-
},
|
|
92
|
-
});
|
package/src/menu_title.tsx
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { Text } from "./text";
|
|
2
|
-
|
|
3
|
-
export interface MenuTitleProps {
|
|
4
|
-
title: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function MenuTitle(props: MenuTitleProps) {
|
|
8
|
-
const { title } = props;
|
|
9
|
-
|
|
10
|
-
return (
|
|
11
|
-
<Text size="lg" weight="semibold" style={{ paddingLeft: 8, lineHeight: 32, paddingBottom: 16 }}>
|
|
12
|
-
{title}
|
|
13
|
-
</Text>
|
|
14
|
-
);
|
|
15
|
-
}
|