@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
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
|
-
import { ScrollView, View } from "react-native";
|
|
3
|
-
import { Text } from "@lotics/ui/text";
|
|
4
|
-
import { colors, type ColorName } from "@lotics/ui/colors";
|
|
5
|
-
import { SectionHeading, SectionHeadingTitle } from "@lotics/ui/section_heading";
|
|
6
|
-
import { Divider } from "@lotics/ui/divider";
|
|
7
|
-
import { DetailRow } from "@lotics/ui/detail_row";
|
|
8
|
-
import { Badge } from "@lotics/ui/badge";
|
|
9
|
-
import { Button } from "@lotics/ui/button";
|
|
10
|
-
import { DangerZone } from "@lotics/ui/danger_zone";
|
|
11
|
-
import { InlineStatic } from "@lotics/ui/inline_static";
|
|
12
|
-
import { InlineTextInput } from "@lotics/ui/inline_text_input";
|
|
13
|
-
import { InlineNumberInput } from "@lotics/ui/inline_number_input";
|
|
14
|
-
import { InlineSelect } from "@lotics/ui/inline_select";
|
|
15
|
-
import { InlineDatePicker } from "@lotics/ui/inline_date_picker";
|
|
16
|
-
import { InlineTimePicker } from "@lotics/ui/inline_time_picker";
|
|
17
|
-
|
|
18
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
-
// Template · Inline record (plain) — a full record DRAWER, card-less. It reads
|
|
20
|
-
// top → bottom like a real drawer: a HEADER surfacing the record's identity +
|
|
21
|
-
// status + key facts, then the in-place inline fields, then a DANGER ZONE for
|
|
22
|
-
// the destructive action. Sections are set off by a `SectionHeading` + hairline
|
|
23
|
-
// and generous rhythm, not card chrome — the lighter look for a single
|
|
24
|
-
// full-screen record where banded cards would be visual noise.
|
|
25
|
-
//
|
|
26
|
-
// It's also the reference for the THREE-PART ROW — label · inline editor ·
|
|
27
|
-
// optional `trailing` (an action / badge / unit) — and for read-only alignment:
|
|
28
|
-
// an `InlineStatic` value sits PIXEL-FOR-PIXEL in the same column as the editors
|
|
29
|
-
// (see the "Row anatomy" section).
|
|
30
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
const PLAN = [
|
|
33
|
-
{ value: "starter", label: "Starter" },
|
|
34
|
-
{ value: "growth", label: "Growth" },
|
|
35
|
-
{ value: "enterprise", label: "Enterprise" },
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
const HEALTH = [
|
|
39
|
-
{ value: "healthy", label: "Healthy" },
|
|
40
|
-
{ value: "at_risk", label: "At risk" },
|
|
41
|
-
{ value: "churning", label: "Churning" },
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
const HEALTH_DOT: Record<string, string> = {
|
|
45
|
-
healthy: colors.emerald[500],
|
|
46
|
-
at_risk: colors.amber[500],
|
|
47
|
-
churning: colors.red[500],
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// The same health states as a Badge color family (the header's status chip).
|
|
51
|
-
const HEALTH_BADGE: Record<string, ColorName> = {
|
|
52
|
-
healthy: "emerald",
|
|
53
|
-
at_risk: "amber",
|
|
54
|
-
churning: "red",
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const STAGE = [
|
|
58
|
-
{ value: "lead", label: "Lead" },
|
|
59
|
-
{ value: "qualified", label: "Qualified" },
|
|
60
|
-
{ value: "proposal", label: "Proposal" },
|
|
61
|
-
{ value: "won", label: "Won" },
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
function persist<T>(set: (v: T) => void) {
|
|
65
|
-
return (v: T) =>
|
|
66
|
-
new Promise<void>((resolve) => {
|
|
67
|
-
setTimeout(() => {
|
|
68
|
-
set(v);
|
|
69
|
-
resolve();
|
|
70
|
-
}, 350);
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function Field({ label, children, trailing }: { label: string; children: React.ReactNode; trailing?: React.ReactNode }) {
|
|
75
|
-
return (
|
|
76
|
-
<DetailRow label={label} labelWidth={130} minHeight={40} trailing={trailing}>
|
|
77
|
-
<View style={{ flex: 1 }}>{children}</View>
|
|
78
|
-
</DetailRow>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
|
83
|
-
return (
|
|
84
|
-
<View style={{ gap: 8 }}>
|
|
85
|
-
<SectionHeading>
|
|
86
|
-
<SectionHeadingTitle>{title}</SectionHeadingTitle>
|
|
87
|
-
</SectionHeading>
|
|
88
|
-
<Divider />
|
|
89
|
-
<View style={{ gap: 2 }}>{children}</View>
|
|
90
|
-
</View>
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// A compact label-over-value pair for the header's key-facts strip.
|
|
95
|
-
function HeaderStat({ label, value }: { label: string; value: string }) {
|
|
96
|
-
return (
|
|
97
|
-
<View style={{ gap: 2 }}>
|
|
98
|
-
<Text size="xs" color="muted" transform="uppercase">
|
|
99
|
-
{label}
|
|
100
|
-
</Text>
|
|
101
|
-
<Text size="sm" weight="medium" tabular>
|
|
102
|
-
{value}
|
|
103
|
-
</Text>
|
|
104
|
-
</View>
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function TplRecordPlain() {
|
|
109
|
-
const [name, setName] = useState("Northwind Traders");
|
|
110
|
-
const [contact, setContact] = useState("Mara Lindqvist");
|
|
111
|
-
const [email, setEmail] = useState("mara@northwind.co");
|
|
112
|
-
const [phone, setPhone] = useState("");
|
|
113
|
-
const [city, setCity] = useState("Gothenburg");
|
|
114
|
-
const [value, setValue] = useState<number | null>(48000);
|
|
115
|
-
const [plan, setPlan] = useState("growth");
|
|
116
|
-
const [health, setHealth] = useState("healthy");
|
|
117
|
-
const [renews, setRenews] = useState("2026-05-22");
|
|
118
|
-
const [review, setReview] = useState("2026-04-10T14:30");
|
|
119
|
-
const [opens, setOpens] = useState("09:00");
|
|
120
|
-
// "Row anatomy" fields.
|
|
121
|
-
const [taxId, setTaxId] = useState("SE556677-8899");
|
|
122
|
-
const [stage, setStage] = useState("proposal");
|
|
123
|
-
const [seats, setSeats] = useState<number | null>(25);
|
|
124
|
-
|
|
125
|
-
const money = (v: number | null) => (v == null ? "" : `$${v.toLocaleString("en-US")}`);
|
|
126
|
-
const planLabel = PLAN.find((p) => p.value === plan)?.label ?? "—";
|
|
127
|
-
const healthLabel = HEALTH.find((h) => h.value === health)?.label ?? "";
|
|
128
|
-
// Read-only, system-owned values that still align with the editors above.
|
|
129
|
-
const accountId = "ACC-4821";
|
|
130
|
-
const lifetimeValue = 128400;
|
|
131
|
-
|
|
132
|
-
return (
|
|
133
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
134
|
-
<View style={{ width: "100%", maxWidth: 560, alignSelf: "center", gap: 28 }}>
|
|
135
|
-
{/* Record header — identity + status + the facts you'd want at a glance,
|
|
136
|
-
composed in-place (not worth a primitive until a 2nd screen needs it). */}
|
|
137
|
-
<View style={{ gap: 12 }}>
|
|
138
|
-
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
|
139
|
-
<Text size="xl" weight="semibold">
|
|
140
|
-
{name}
|
|
141
|
-
</Text>
|
|
142
|
-
<Badge label={healthLabel} color={HEALTH_BADGE[health]} />
|
|
143
|
-
<View style={{ flex: 1 }} />
|
|
144
|
-
<Text size="xs" color="muted">
|
|
145
|
-
Click any value to edit
|
|
146
|
-
</Text>
|
|
147
|
-
</View>
|
|
148
|
-
<Text size="sm" color="muted">
|
|
149
|
-
{contact} · {email}
|
|
150
|
-
</Text>
|
|
151
|
-
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 28 }}>
|
|
152
|
-
<HeaderStat label="Plan" value={planLabel} />
|
|
153
|
-
<HeaderStat label="Annual value" value={money(value) || "—"} />
|
|
154
|
-
<HeaderStat label="City" value={city || "—"} />
|
|
155
|
-
</View>
|
|
156
|
-
</View>
|
|
157
|
-
|
|
158
|
-
<Section title="Contact">
|
|
159
|
-
<Field label="Account name">
|
|
160
|
-
<InlineTextInput value={name} onSave={persist(setName)} accessibilityLabel="Account name" />
|
|
161
|
-
</Field>
|
|
162
|
-
<Field label="Primary contact">
|
|
163
|
-
<InlineTextInput value={contact} onSave={persist(setContact)} accessibilityLabel="Primary contact" />
|
|
164
|
-
</Field>
|
|
165
|
-
<Field label="Email">
|
|
166
|
-
<InlineTextInput value={email} onSave={persist(setEmail)} placeholder="Add email…" accessibilityLabel="Email" />
|
|
167
|
-
</Field>
|
|
168
|
-
<Field label="Phone">
|
|
169
|
-
<InlineTextInput value={phone} onSave={persist(setPhone)} placeholder="Add phone…" accessibilityLabel="Phone" />
|
|
170
|
-
</Field>
|
|
171
|
-
<Field label="City">
|
|
172
|
-
<InlineTextInput value={city} onSave={persist(setCity)} accessibilityLabel="City" />
|
|
173
|
-
</Field>
|
|
174
|
-
</Section>
|
|
175
|
-
|
|
176
|
-
<Section title="Commercial">
|
|
177
|
-
<Field label="Annual value">
|
|
178
|
-
<InlineNumberInput value={value} onSave={persist(setValue)} format={money} accessibilityLabel="Annual value" />
|
|
179
|
-
</Field>
|
|
180
|
-
<Field label="Plan">
|
|
181
|
-
<InlineSelect value={plan} onSave={persist(setPlan)} options={PLAN} accessibilityLabel="Plan" />
|
|
182
|
-
</Field>
|
|
183
|
-
<Field label="Health">
|
|
184
|
-
<InlineSelect
|
|
185
|
-
value={health}
|
|
186
|
-
onSave={persist(setHealth)}
|
|
187
|
-
options={HEALTH}
|
|
188
|
-
accessibilityLabel="Health"
|
|
189
|
-
renderOptionContent={(o) => (
|
|
190
|
-
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
191
|
-
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: HEALTH_DOT[o.value] }} />
|
|
192
|
-
<Text size="sm">{o.label}</Text>
|
|
193
|
-
</View>
|
|
194
|
-
)}
|
|
195
|
-
/>
|
|
196
|
-
</Field>
|
|
197
|
-
</Section>
|
|
198
|
-
|
|
199
|
-
<Section title="Schedule">
|
|
200
|
-
<Field label="Renews on">
|
|
201
|
-
<InlineDatePicker value={renews} onSave={persist(setRenews)} locale="en-US" accessibilityLabel="Renews on" />
|
|
202
|
-
</Field>
|
|
203
|
-
<Field label="Next review">
|
|
204
|
-
<InlineDatePicker value={review} onSave={persist(setReview)} format="datetime" locale="en-US" accessibilityLabel="Next review" />
|
|
205
|
-
</Field>
|
|
206
|
-
<Field label="Support opens">
|
|
207
|
-
<InlineTimePicker value={opens} onSave={persist(setOpens)} accessibilityLabel="Support opens" />
|
|
208
|
-
</Field>
|
|
209
|
-
</Section>
|
|
210
|
-
|
|
211
|
-
{/* Row anatomy — the three-part row (label · editor · trailing) and
|
|
212
|
-
read-only alignment, ALL in one 130px-label column so the editors and
|
|
213
|
-
the InlineStatic values line up to the pixel. Copy these shapes. */}
|
|
214
|
-
<Section title="Row anatomy">
|
|
215
|
-
{/* editor + a trailing ACTION */}
|
|
216
|
-
<Field
|
|
217
|
-
label="Tax ID"
|
|
218
|
-
trailing={<Button title="Verify" color="secondary" onPress={() => {}} />}
|
|
219
|
-
>
|
|
220
|
-
<InlineTextInput value={taxId} onSave={persist(setTaxId)} placeholder="Add tax ID…" accessibilityLabel="Tax ID" />
|
|
221
|
-
</Field>
|
|
222
|
-
{/* editor + a trailing STATUS dot */}
|
|
223
|
-
<Field
|
|
224
|
-
label="Pipeline stage"
|
|
225
|
-
trailing={<Badge variant="dot" color="emerald" label="Active" />}
|
|
226
|
-
>
|
|
227
|
-
<InlineSelect value={stage} onSave={persist(setStage)} options={STAGE} accessibilityLabel="Pipeline stage" />
|
|
228
|
-
</Field>
|
|
229
|
-
{/* editor + a trailing UNIT label */}
|
|
230
|
-
<Field
|
|
231
|
-
label="Licensed seats"
|
|
232
|
-
trailing={
|
|
233
|
-
<Text size="sm" color="muted">
|
|
234
|
-
seats
|
|
235
|
-
</Text>
|
|
236
|
-
}
|
|
237
|
-
>
|
|
238
|
-
<InlineNumberInput value={seats} onSave={persist(setSeats)} min={0} accessibilityLabel="Licensed seats" />
|
|
239
|
-
</Field>
|
|
240
|
-
{/* READ-ONLY value — aligns with the editors above, no input chrome */}
|
|
241
|
-
<Field label="Account ID">
|
|
242
|
-
<InlineStatic value={accountId} tabular />
|
|
243
|
-
</Field>
|
|
244
|
-
{/* READ-ONLY value + a trailing MARK */}
|
|
245
|
-
<Field label="Lifetime value" trailing={<Badge label="Computed" color="zinc" />}>
|
|
246
|
-
<InlineStatic value={money(lifetimeValue)} tabular />
|
|
247
|
-
</Field>
|
|
248
|
-
</Section>
|
|
249
|
-
|
|
250
|
-
<DangerZone
|
|
251
|
-
title="Delete account"
|
|
252
|
-
description="Permanently remove this account and all of its contacts, deals, and history. This can't be undone."
|
|
253
|
-
>
|
|
254
|
-
<Button title="Delete account" color="danger" onPress={() => {}} />
|
|
255
|
-
</DangerZone>
|
|
256
|
-
</View>
|
|
257
|
-
</ScrollView>
|
|
258
|
-
);
|
|
259
|
-
}
|
|
@@ -1,178 +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, CardFooter, CardHeader, CardHeaderTitle } from "@lotics/ui/card";
|
|
7
|
-
import { Picker } from "@lotics/ui/picker";
|
|
8
|
-
import { Switch } from "@lotics/ui/switch";
|
|
9
|
-
import { Divider } from "@lotics/ui/divider";
|
|
10
|
-
|
|
11
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
|
-
// Template · Settings — the workspace settings screen; the Action Layout doc
|
|
13
|
-
// made visible. Three banded cards on the zinc-50 canvas:
|
|
14
|
-
// General → Divider-separated rows, label left / control right (text+Edit, Pickers)
|
|
15
|
-
// Notifications → title+description left, live Switch right
|
|
16
|
-
// Danger zone → separated red heading + description + ONE labeled
|
|
17
|
-
// danger button (never icon-only, never next to Save)
|
|
18
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
const NGON_NGU = [
|
|
21
|
-
{ label: "Vietnamese", value: "vi" },
|
|
22
|
-
{ label: "English", value: "en" },
|
|
23
|
-
];
|
|
24
|
-
|
|
25
|
-
const MUI_GIO = [
|
|
26
|
-
{ label: "(GMT+7) Ho Chi Minh City", value: "Asia/Ho_Chi_Minh" },
|
|
27
|
-
{ label: "(GMT+7) Bangkok", value: "Asia/Bangkok" },
|
|
28
|
-
{ label: "(GMT+8) Singapore", value: "Asia/Singapore" },
|
|
29
|
-
{ label: "(GMT+9) Tokyo", value: "Asia/Tokyo" },
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
const THONG_BAO = [
|
|
33
|
-
{
|
|
34
|
-
id: "donHang",
|
|
35
|
-
label: "New orders",
|
|
36
|
-
description: "Notify immediately when an order is created in this workspace.",
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
id: "nhacViec",
|
|
40
|
-
label: "Due-task reminders",
|
|
41
|
-
description: "Send an 8:00 AM reminder for tasks due today.",
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: "tomTat",
|
|
45
|
-
label: "Weekly digest",
|
|
46
|
-
description: "A summary of the whole team's activity, sent Monday morning.",
|
|
47
|
-
},
|
|
48
|
-
] as const;
|
|
49
|
-
|
|
50
|
-
type ThongBaoId = (typeof THONG_BAO)[number]["id"];
|
|
51
|
-
|
|
52
|
-
/** One hairline settings row: label (+ optional description) left, control right. */
|
|
53
|
-
function SettingRow({
|
|
54
|
-
label,
|
|
55
|
-
description,
|
|
56
|
-
children,
|
|
57
|
-
}: {
|
|
58
|
-
label: string;
|
|
59
|
-
description?: string;
|
|
60
|
-
children: React.ReactNode;
|
|
61
|
-
}) {
|
|
62
|
-
return (
|
|
63
|
-
<View
|
|
64
|
-
style={{
|
|
65
|
-
paddingHorizontal: 20,
|
|
66
|
-
paddingVertical: 14,
|
|
67
|
-
flexDirection: "row",
|
|
68
|
-
alignItems: "center",
|
|
69
|
-
gap: 16,
|
|
70
|
-
}}
|
|
71
|
-
>
|
|
72
|
-
<View style={{ flex: 1, gap: 2 }}>
|
|
73
|
-
<Text size="sm" weight="medium">{label}</Text>
|
|
74
|
-
{description ? <Text size="xs" color="muted">{description}</Text> : null}
|
|
75
|
-
</View>
|
|
76
|
-
{children}
|
|
77
|
-
</View>
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function TplSettings() {
|
|
82
|
-
const [ngonNgu, setNgonNgu] = useState("vi");
|
|
83
|
-
const [muiGio, setMuiGio] = useState("Asia/Ho_Chi_Minh");
|
|
84
|
-
const [thongBao, setThongBao] = useState<Record<ThongBaoId, boolean>>({
|
|
85
|
-
donHang: true,
|
|
86
|
-
nhacViec: true,
|
|
87
|
-
tomTat: false,
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
return (
|
|
91
|
-
<ScrollView
|
|
92
|
-
style={{ flex: 1, backgroundColor: colors.white }}
|
|
93
|
-
contentContainerStyle={{ padding: 28 }}
|
|
94
|
-
>
|
|
95
|
-
<View style={{ width: "100%", maxWidth: 880, alignSelf: "center", gap: 16 }}>
|
|
96
|
-
{/* header band */}
|
|
97
|
-
<View style={{ gap: 2 }}>
|
|
98
|
-
<Text size="xl" weight="semibold">Settings</Text>
|
|
99
|
-
<Text size="sm" color="muted">
|
|
100
|
-
General settings, notifications and the workspace danger zone
|
|
101
|
-
</Text>
|
|
102
|
-
</View>
|
|
103
|
-
|
|
104
|
-
{/* General */}
|
|
105
|
-
<Card style={{ padding: 0 }}>
|
|
106
|
-
<CardHeader>
|
|
107
|
-
<CardHeaderTitle description="Display name, language and timezone applied to every member">
|
|
108
|
-
General
|
|
109
|
-
</CardHeaderTitle>
|
|
110
|
-
</CardHeader>
|
|
111
|
-
<SettingRow label="Workspace name">
|
|
112
|
-
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
|
113
|
-
<Text size="sm">Crestline Logistics</Text>
|
|
114
|
-
<Button title="Edit" color="muted" onPress={() => {}} />
|
|
115
|
-
</View>
|
|
116
|
-
</SettingRow>
|
|
117
|
-
<Divider />
|
|
118
|
-
<SettingRow label="Language" description="Interface language and outgoing email">
|
|
119
|
-
<Picker
|
|
120
|
-
options={NGON_NGU}
|
|
121
|
-
value={ngonNgu}
|
|
122
|
-
onValueChange={setNgonNgu}
|
|
123
|
-
style={{ width: 220 }}
|
|
124
|
-
/>
|
|
125
|
-
</SettingRow>
|
|
126
|
-
<Divider />
|
|
127
|
-
<SettingRow label="Timezone" description="Used for due dates, calendars and reminders">
|
|
128
|
-
<Picker
|
|
129
|
-
options={MUI_GIO}
|
|
130
|
-
value={muiGio}
|
|
131
|
-
onValueChange={setMuiGio}
|
|
132
|
-
style={{ width: 220 }}
|
|
133
|
-
/>
|
|
134
|
-
</SettingRow>
|
|
135
|
-
</Card>
|
|
136
|
-
|
|
137
|
-
{/* Notifications */}
|
|
138
|
-
<Card style={{ padding: 0 }}>
|
|
139
|
-
<CardHeader>
|
|
140
|
-
<CardHeaderTitle description="Pick what deserves a notification — everything else stays in-app">
|
|
141
|
-
Notifications
|
|
142
|
-
</CardHeaderTitle>
|
|
143
|
-
</CardHeader>
|
|
144
|
-
{THONG_BAO.map((tb, i) => (
|
|
145
|
-
<View key={tb.id}>
|
|
146
|
-
{i > 0 ? <Divider /> : null}
|
|
147
|
-
<SettingRow label={tb.label} description={tb.description}>
|
|
148
|
-
<Switch
|
|
149
|
-
accessibilityLabel={tb.label}
|
|
150
|
-
value={thongBao[tb.id]}
|
|
151
|
-
onChange={(value) => setThongBao((prev) => ({ ...prev, [tb.id]: value }))}
|
|
152
|
-
/>
|
|
153
|
-
</SettingRow>
|
|
154
|
-
</View>
|
|
155
|
-
))}
|
|
156
|
-
</Card>
|
|
157
|
-
|
|
158
|
-
{/* Danger zone — separated, explicit label, confirm before executing */}
|
|
159
|
-
<Card style={{ padding: 0 }}>
|
|
160
|
-
<CardHeader>
|
|
161
|
-
{/* bespoke title node: CardHeaderTitle has no danger color, and the
|
|
162
|
-
red heading is the point of this band */}
|
|
163
|
-
<View style={{ flex: 1, gap: 2 }}>
|
|
164
|
-
<Text size="sm" weight="semibold" color="danger">Danger zone</Text>
|
|
165
|
-
<Text size="xs" color="muted">
|
|
166
|
-
Permanently deletes the workspace with every table, record and attachment. This
|
|
167
|
-
cannot be undone.
|
|
168
|
-
</Text>
|
|
169
|
-
</View>
|
|
170
|
-
</CardHeader>
|
|
171
|
-
<CardFooter>
|
|
172
|
-
<Button title="Delete workspace" color="danger" onPress={() => {}} />
|
|
173
|
-
</CardFooter>
|
|
174
|
-
</Card>
|
|
175
|
-
</View>
|
|
176
|
-
</ScrollView>
|
|
177
|
-
);
|
|
178
|
-
}
|
|
@@ -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
|
-
}
|