@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.
Files changed (58) hide show
  1. package/AGENTS.md +177 -70
  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 -7
  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 +25 -0
  33. package/src/page_header.tsx +0 -2
  34. package/src/popover_nav.tsx +40 -0
  35. package/src/progress_bar.tsx +32 -1
  36. package/src/record_summary.tsx +101 -0
  37. package/src/section_heading.tsx +16 -8
  38. package/src/suggestion_chip.tsx +47 -0
  39. package/src/trend_footer.tsx +3 -1
  40. package/src/use_screen_size.ts +1 -1
  41. package/src/use_section_nav.test.ts +69 -0
  42. package/src/use_section_nav.ts +59 -0
  43. package/examples/tpl_billing.tsx +0 -344
  44. package/examples/tpl_detail.tsx +0 -232
  45. package/examples/tpl_directory.tsx +0 -260
  46. package/examples/tpl_intake.tsx +0 -206
  47. package/examples/tpl_order.tsx +0 -482
  48. package/examples/tpl_quick.tsx +0 -211
  49. package/examples/tpl_record_plain.tsx +0 -259
  50. package/examples/tpl_settings.tsx +0 -178
  51. package/examples/tpl_timeline.tsx +0 -244
  52. package/examples/tpl_wizard.tsx +0 -223
  53. package/src/animation_horizontal_slide.tsx +0 -75
  54. package/src/form_time_picker.tsx +0 -22
  55. package/src/highlighted_text.tsx +0 -92
  56. package/src/menu_title.tsx +0 -15
  57. package/src/pager_view.tsx +0 -167
  58. package/src/popover_header.tsx +0 -38
@@ -1,211 +0,0 @@
1
- import { useRef, useState } from "react";
2
- import { ScrollView, View, type TextInput as RNTextInput } 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 { Alert } from "@lotics/ui/alert";
7
- import { Card, CardBody, CardHeader, CardHeaderMeta, CardHeaderTitle } from "@lotics/ui/card";
8
- import { Divider } from "@lotics/ui/divider";
9
- import { Icon, type IconName } from "@lotics/ui/icon";
10
- import { FormField } from "@lotics/ui/form_field";
11
- import { TextInputField } from "@lotics/ui/text_input_field";
12
- import { Combobox, ComboboxInput, ComboboxContent } from "@lotics/ui/combobox";
13
- import type { PickerOption } from "@lotics/ui/picker";
14
- import { SegmentedControl } from "@lotics/ui/segmented_control";
15
- import { Counter } from "@lotics/ui/counter";
16
- import { EmptyState } from "@lotics/ui/empty_state";
17
-
18
- // ─────────────────────────────────────────────────────────────────────────────
19
- // Template · Quick capture — the SPEED surface, opposite of the heavy form: one
20
- // compact row of inputs, Enter to add, the field keeps focus and the contact
21
- // carries over so the next touchpoint is two keystrokes away. Every entry drops
22
- // into the log below (newest first, undoable). A `SegmentedControl` mode, a
23
- // `Combobox`-as-select, a `Counter` for the bounded duration. No screen-wide
24
- // Save — each entry commits on its own.
25
- // ─────────────────────────────────────────────────────────────────────────────
26
-
27
- type ActType = "call" | "email" | "meeting" | "note";
28
-
29
- const ACT: Record<ActType, { label: string; icon: IconName }> = {
30
- call: { label: "Call", icon: "phone" },
31
- email: { label: "Email", icon: "mail" },
32
- meeting: { label: "Meeting", icon: "calendar" },
33
- note: { label: "Note", icon: "message-square" },
34
- };
35
-
36
- const TYPE_OPTIONS = (Object.keys(ACT) as ActType[]).map((v) => ({ label: ACT[v].label, value: v }));
37
-
38
- interface Contact {
39
- id: string;
40
- name: string;
41
- company: string;
42
- }
43
-
44
- const CONTACTS: Contact[] = [
45
- { id: "c1", name: "Mara Lindqvist", company: "Northwind Traders" },
46
- { id: "c2", name: "Diego Alvarez", company: "Harbor Freight Lines" },
47
- { id: "c3", name: "Priya Nair", company: "Summit Packaging Co." },
48
- { id: "c4", name: "Tom Becker", company: "Atlas Distribution" },
49
- { id: "c5", name: "Lena Fischer", company: "Bluewater Logistics" },
50
- { id: "c6", name: "Sara Cohen", company: "Meridian Retail Group" },
51
- ];
52
-
53
- const CONTACT_OPTIONS: PickerOption<string, Contact>[] = CONTACTS.map((c) => ({
54
- value: c.id,
55
- label: c.name,
56
- data: c,
57
- }));
58
-
59
- interface Entry {
60
- id: string;
61
- type: ActType;
62
- contact: Contact;
63
- note: string;
64
- minutes: number;
65
- time: string;
66
- }
67
-
68
- const SEED: Entry[] = [
69
- { id: "e1", type: "call", contact: CONTACTS[2], note: "Reviewed Q3 volume, wants a revised quote by Friday", minutes: 15, time: "10:05" },
70
- { id: "e2", type: "email", contact: CONTACTS[0], note: "Sent updated pallet spec sheet", minutes: 0, time: "09:40" },
71
- { id: "e3", type: "meeting", contact: CONTACTS[4], note: "Warehouse walkthrough — flagged dock height", minutes: 45, time: "09:12" },
72
- ];
73
-
74
- const fmtDuration = (m: number) => (m === 0 ? "—" : m >= 60 ? `${Math.floor(m / 60)}h${m % 60 ? ` ${m % 60}m` : ""}` : `${m}m`);
75
-
76
- export function TplQuick() {
77
- const id = useRef(1);
78
- const noteRef = useRef<RNTextInput>(null);
79
-
80
- const [type, setType] = useState<ActType>("call");
81
- const [contact, setContact] = useState<PickerOption<string, Contact> | null>(null);
82
- const [note, setNote] = useState("");
83
- const [minutes, setMinutes] = useState(15);
84
- const [entries, setEntries] = useState<Entry[]>(SEED);
85
-
86
- const canAdd = contact !== null && note.trim().length > 0;
87
-
88
- const add = () => {
89
- if (!canAdd || !contact?.data) return;
90
- const entry: Entry = {
91
- id: `e_${(id.current += 1)}`,
92
- type,
93
- contact: contact.data,
94
- note: note.trim(),
95
- minutes,
96
- time: "Just now",
97
- };
98
- setEntries((prev) => [entry, ...prev]);
99
- // Keep type + contact for the next touchpoint; clear only the note + duration
100
- // and re-focus the note so logging again is immediate.
101
- setNote("");
102
- setMinutes(type === "email" ? 0 : 15);
103
- noteRef.current?.focus();
104
- };
105
-
106
- return (
107
- <ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
108
- <View style={{ width: "100%", maxWidth: 720, alignSelf: "center", gap: 16 }}>
109
- <View style={{ gap: 2 }}>
110
- <Text size="xl" weight="semibold">Log activity</Text>
111
- <Text size="sm" color="muted">Capture a touchpoint in seconds — pick the type, the contact, type the note, press Enter</Text>
112
- </View>
113
-
114
- {/* the capture row */}
115
- <Card style={{ padding: 0 }}>
116
- <CardBody>
117
- <SegmentedControl accessibilityLabel="Activity type" options={TYPE_OPTIONS} value={type} onValueChange={setType} style={{ marginBottom: 16 }} />
118
- <View style={{ flexDirection: "row", flexWrap: "wrap", columnGap: 16 }}>
119
- <FormField label="Contact" style={{ flexGrow: 1, flexBasis: 240 }}>
120
- <Combobox
121
- options={CONTACT_OPTIONS}
122
- value={contact}
123
- onValueChange={setContact}
124
- getOptionDescription={(o) => o.data?.company}
125
- >
126
- <ComboboxInput placeholder="Select a contact" accessibilityLabel="Contact" />
127
- <ComboboxContent />
128
- </Combobox>
129
- </FormField>
130
- <FormField label="Duration" style={{ flexGrow: 0, flexBasis: 180 }}>
131
- <View style={{ minHeight: 40, justifyContent: "center" }}>
132
- <Counter value={minutes} onValueChange={setMinutes} min={0} max={240} step={15} accessibilityLabel="Duration" format={fmtDuration} />
133
- </View>
134
- </FormField>
135
- </View>
136
- <FormField label="Note">
137
- <TextInputField
138
- ref={noteRef}
139
- value={note}
140
- onChangeText={setNote}
141
- placeholder="What happened? (press Enter to log)"
142
- autoFocus
143
- onKeyPress={(e: { nativeEvent: { key: string }; preventDefault: () => void }) => {
144
- if (e.nativeEvent.key === "Enter") {
145
- e.preventDefault();
146
- add();
147
- }
148
- }}
149
- />
150
- </FormField>
151
- </CardBody>
152
- <Divider />
153
- <View style={{ flexDirection: "row", alignItems: "center", paddingHorizontal: 20, paddingVertical: 12, gap: 8 }}>
154
- <Text size="xs" color="muted" style={{ flex: 1 }}>
155
- {contact?.data ? `${ACT[type].label} · ${contact.data.name}` : "Pick a contact to log against"}
156
- </Text>
157
- <Button title="Log activity" color="primary" icon="plus" disabled={!canAdd} onPress={add} />
158
- </View>
159
- </Card>
160
-
161
- {/* the running log */}
162
- <Card style={{ padding: 0 }}>
163
- <CardHeader>
164
- <CardHeaderTitle>Today</CardHeaderTitle>
165
- <CardHeaderMeta>{entries.length} logged</CardHeaderMeta>
166
- </CardHeader>
167
- {entries.length === 0 ? (
168
- <EmptyState icon="message-square" message="Nothing logged yet" hint="Your activity for the day appears here" />
169
- ) : (
170
- entries.map((e, i) => (
171
- <View key={e.id}>
172
- {i > 0 ? <Divider /> : null}
173
- <View style={{ flexDirection: "row", alignItems: "center", gap: 12, paddingHorizontal: 20, paddingVertical: 12 }}>
174
- <View style={{ width: 32, height: 32, borderRadius: 16, backgroundColor: colors.zinc[100], alignItems: "center", justifyContent: "center" }}>
175
- <Icon name={ACT[e.type].icon} size={16} color={colors.zinc[600]} />
176
- </View>
177
- <View style={{ flex: 1, gap: 1 }}>
178
- <View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
179
- <Text weight="medium" numberOfLines={1}>{e.contact.name}</Text>
180
- <Text size="xs" color="muted">· {e.contact.company}</Text>
181
- </View>
182
- <Text size="sm" color="muted" numberOfLines={1}>{e.note}</Text>
183
- </View>
184
- <View style={{ alignItems: "flex-end", width: 76 }}>
185
- <Text size="xs" color="muted" tabular>{e.time}</Text>
186
- {e.minutes > 0 ? <Text size="xs" color="muted" tabular>{fmtDuration(e.minutes)}</Text> : null}
187
- </View>
188
- <Button
189
- title="Delete"
190
- color="secondary"
191
- accessibilityLabel={`Delete ${e.contact.name} ${ACT[e.type].label.toLowerCase()}`}
192
- onPress={() =>
193
- Alert.alert(
194
- "Delete activity?",
195
- `Remove the ${ACT[e.type].label.toLowerCase()} logged for ${e.contact.name}. This can't be undone.`,
196
- [
197
- { text: "Cancel", style: "cancel" },
198
- { text: "Delete", style: "destructive", onPress: () => setEntries((prev) => prev.filter((x) => x.id !== e.id)) },
199
- ],
200
- )
201
- }
202
- />
203
- </View>
204
- </View>
205
- ))
206
- )}
207
- </Card>
208
- </View>
209
- </ScrollView>
210
- );
211
- }
@@ -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
- }