@lotics/ui 7.19.3 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +298 -159
- package/examples/tpl_allocate.tsx +2 -2
- package/examples/tpl_attendance.tsx +2 -2
- package/examples/tpl_calendar.tsx +1 -1
- package/examples/tpl_dashboard.tsx +1 -1
- package/examples/tpl_dieline.tsx +3 -3
- package/examples/tpl_documents.tsx +790 -0
- package/examples/tpl_item_list.tsx +1015 -124
- package/examples/tpl_lookup.tsx +37 -10
- package/examples/tpl_pick.tsx +3 -3
- package/examples/tpl_pivot.tsx +1 -1
- package/examples/tpl_record.tsx +1354 -0
- package/examples/tpl_report.tsx +7 -7
- package/examples/tpl_rollup.tsx +6 -6
- package/examples/tpl_shifts.tsx +2 -2
- package/examples/tpl_statements.tsx +221 -0
- package/examples/tpl_stock.tsx +7 -7
- package/examples/tpl_task_board.tsx +48 -32
- package/examples/tpl_tasks.tsx +47 -47
- package/examples/tpl_tower.tsx +2 -2
- package/package.json +9 -9
- package/src/agent_run.tsx +1 -1
- package/src/capture_row.tsx +59 -0
- package/src/change_review.tsx +732 -236
- package/src/checklist.tsx +104 -0
- package/src/chip.tsx +12 -3
- package/src/composer.tsx +22 -1
- package/src/confidence.tsx +1 -1
- package/src/control_surface.ts +0 -14
- package/src/detail_row.tsx +137 -10
- package/src/finding.tsx +112 -80
- package/src/floating_action_bar.tsx +1 -1
- package/src/inline_date_picker.tsx +8 -3
- package/src/inline_edit.tsx +40 -10
- package/src/inline_member_select.tsx +3 -0
- package/src/inline_number_input.tsx +5 -2
- package/src/inline_select.tsx +8 -3
- package/src/inline_tag_select.tsx +140 -0
- package/src/inline_text_input.tsx +5 -2
- package/src/inline_time_picker.tsx +5 -2
- package/src/ledger.tsx +220 -0
- package/src/locale.tsx +43 -16
- package/src/progress_bar.tsx +32 -1
- package/src/record_summary.tsx +101 -0
- package/src/section_heading.tsx +16 -8
- package/src/sources.tsx +8 -5
- package/src/suggestion_chip.tsx +47 -0
- package/src/use_section_nav.test.ts +69 -0
- package/src/use_section_nav.ts +59 -0
- package/examples/tpl_assistant.tsx +0 -174
- package/examples/tpl_billing.tsx +0 -344
- package/examples/tpl_briefing.tsx +0 -121
- package/examples/tpl_compare.tsx +0 -133
- package/examples/tpl_crosscheck.tsx +0 -120
- package/examples/tpl_detail.tsx +0 -232
- package/examples/tpl_directory.tsx +0 -260
- package/examples/tpl_draft.tsx +0 -163
- package/examples/tpl_extract.tsx +0 -193
- package/examples/tpl_intake.tsx +0 -206
- package/examples/tpl_match.tsx +0 -134
- package/examples/tpl_order.tsx +0 -482
- package/examples/tpl_quick.tsx +0 -211
- package/examples/tpl_ratedesk.tsx +0 -386
- package/examples/tpl_record_plain.tsx +0 -259
- package/examples/tpl_settings.tsx +0 -178
- package/examples/tpl_timeline.tsx +0 -244
- package/examples/tpl_triage.tsx +0 -112
- package/examples/tpl_wizard.tsx +0 -223
- package/src/discrepancy.tsx +0 -114
- package/src/match_sides.tsx +0 -79
- package/src/record_fields.tsx +0 -68
- package/src/review_card.tsx +0 -172
- package/src/scored_option.tsx +0 -138
- package/src/spec_list.tsx +0 -81
- package/src/triage_row.tsx +0 -99
package/examples/tpl_report.tsx
CHANGED
|
@@ -321,13 +321,13 @@ export function TplReport() {
|
|
|
321
321
|
|
|
322
322
|
{/* eyebrow columns */}
|
|
323
323
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 12, paddingHorizontal: 20, paddingVertical: 10 }}>
|
|
324
|
-
<Text size="xs" color="muted"
|
|
325
|
-
<Text size="xs" color="muted"
|
|
326
|
-
<Text size="xs" color="muted"
|
|
327
|
-
<Text size="xs" color="muted"
|
|
328
|
-
<Text size="xs" color="muted"
|
|
329
|
-
<Text size="xs" color="muted"
|
|
330
|
-
<Text size="xs" color="muted"
|
|
324
|
+
<Text size="xs" color="muted" weight="medium" style={{ width: 92 }}>Reference</Text>
|
|
325
|
+
<Text size="xs" color="muted" weight="medium" style={{ flex: 1.3 }}>Customer</Text>
|
|
326
|
+
<Text size="xs" color="muted" weight="medium" style={{ flex: 1 }}>Operator</Text>
|
|
327
|
+
<Text size="xs" color="muted" weight="medium" style={{ width: 70 }}>Date</Text>
|
|
328
|
+
<Text size="xs" color="muted" weight="medium" tabular align="right" style={{ width: 116 }}>Amount</Text>
|
|
329
|
+
<Text size="xs" color="muted" weight="medium" style={{ width: 92 }}>Receipt</Text>
|
|
330
|
+
<Text size="xs" color="muted" weight="medium" style={{ width: 96 }}>Status</Text>
|
|
331
331
|
</View>
|
|
332
332
|
<Divider />
|
|
333
333
|
|
package/examples/tpl_rollup.tsx
CHANGED
|
@@ -12,7 +12,7 @@ import { PressableHighlight } from "@lotics/ui/pressable_highlight";
|
|
|
12
12
|
import { Sparkline } from "@lotics/ui/sparkline";
|
|
13
13
|
|
|
14
14
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
-
// Template ·
|
|
15
|
+
// Template · Hierarchy rollup — hierarchical totals with drill: region →
|
|
16
16
|
// site → lane, every level carrying the SAME aligned metric columns so a
|
|
17
17
|
// bad number can be chased down the tree ("which branch drags the
|
|
18
18
|
// total?"). Pure composition: nested Accordions + a fixed-width column
|
|
@@ -169,10 +169,10 @@ export function TplRollup() {
|
|
|
169
169
|
|
|
170
170
|
{/* eyebrow columns — match the tree's column map exactly */}
|
|
171
171
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 12, paddingHorizontal: 20, paddingVertical: 10 }}>
|
|
172
|
-
<Text size="xs" color="muted"
|
|
173
|
-
<Text size="xs" color="muted"
|
|
174
|
-
<Text size="xs" color="muted"
|
|
175
|
-
<Text size="xs" color="muted"
|
|
172
|
+
<Text size="xs" color="muted" weight="medium" style={{ flex: 1 }}>Branch</Text>
|
|
173
|
+
<Text size="xs" color="muted" weight="medium" align="right" style={{ width: COLS.volume }}>Volume</Text>
|
|
174
|
+
<Text size="xs" color="muted" weight="medium" align="right" style={{ width: COLS.ontime }}>On-time</Text>
|
|
175
|
+
<Text size="xs" color="muted" weight="medium" align="right" style={{ width: COLS.margin }}>Margin</Text>
|
|
176
176
|
<View style={{ width: 16 }} />
|
|
177
177
|
</View>
|
|
178
178
|
<Divider />
|
|
@@ -270,7 +270,7 @@ export function TplRollup() {
|
|
|
270
270
|
<KVRow label="Margin"><Text size="sm" tabular>{`${openLane.margin}%`}</Text></KVRow>
|
|
271
271
|
</View>
|
|
272
272
|
<View style={{ gap: 8 }}>
|
|
273
|
-
<Text size="xs" color="muted"
|
|
273
|
+
<Text size="xs" color="muted" weight="medium">Volume · last 12 weeks</Text>
|
|
274
274
|
<Sparkline
|
|
275
275
|
data={Array.from({ length: 12 }, (_, w) => openLane.volume / 4 + (hash(w, openLane.volume) - 500) * (openLane.volume / 12000))}
|
|
276
276
|
width={200}
|
package/examples/tpl_shifts.tsx
CHANGED
|
@@ -210,7 +210,7 @@ export function TplShifts() {
|
|
|
210
210
|
<View style={{ width: 110 }} />
|
|
211
211
|
{DAYS.map((d) => (
|
|
212
212
|
<View key={d.key} style={{ flex: 1, alignItems: "center" }}>
|
|
213
|
-
<Text size="xs" color="muted"
|
|
213
|
+
<Text size="xs" color="muted" weight="medium">{d.label}</Text>
|
|
214
214
|
<Text size="xs" color="muted" tabular>{d.date}</Text>
|
|
215
215
|
</View>
|
|
216
216
|
))}
|
|
@@ -389,7 +389,7 @@ export function TplShifts() {
|
|
|
389
389
|
</PopoverTrigger>
|
|
390
390
|
<PopoverContent style={{ width: 320 }}>
|
|
391
391
|
<View style={{ paddingHorizontal: 12, paddingTop: 10, paddingBottom: 6 }}>
|
|
392
|
-
<Text size="xs" color="muted"
|
|
392
|
+
<Text size="xs" color="muted" weight="medium">{`Fill ${slotLabel(open.day, open.shift)}`}</Text>
|
|
393
393
|
</View>
|
|
394
394
|
<ScrollView style={{ maxHeight: 280 }} contentContainerStyle={{ paddingHorizontal: 6, paddingBottom: 8, gap: 2 }}>
|
|
395
395
|
{MEMBERS.filter((m) => !openAssigned.includes(m)).map((m) => {
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { ReactNode } 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 { Divider } from "@lotics/ui/divider";
|
|
7
|
+
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// Template · Financial statements — the income statement, the balance sheet,
|
|
10
|
+
// and the cash flow statement, all speaking ONE statement grammar: column
|
|
11
|
+
// captions right-aligned over fixed money columns, items indented under their
|
|
12
|
+
// group, a hairline rule above every subtotal, the grand total DOUBLE-RULED
|
|
13
|
+
// (the accounting convention), negatives in accounting parentheses, no bars,
|
|
14
|
+
// no charts — the numbers are the interface. The three statements TIE: net
|
|
15
|
+
// income flows into retained earnings, the cash flow's closing cash IS the
|
|
16
|
+
// balance sheet's cash, the loan repayment moves the debt line. Figures are
|
|
17
|
+
// per-cell currency-free (the meta line says "VND" once).
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/** Accounting formatting: thousands-dotted, negatives in parentheses. */
|
|
21
|
+
const num = (n: number) => {
|
|
22
|
+
const s = Math.abs(n).toLocaleString("vi-VN");
|
|
23
|
+
return n < 0 ? `(${s})` : s;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type LineKind = "header" | "item" | "subtotal" | "total";
|
|
27
|
+
|
|
28
|
+
interface Line {
|
|
29
|
+
kind: LineKind;
|
|
30
|
+
label: string;
|
|
31
|
+
/** One value per column; null renders an empty cell (headers usually). */
|
|
32
|
+
values?: (number | null)[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const COL_WIDTH = 124;
|
|
36
|
+
|
|
37
|
+
/** One statement: right-aligned column captions, then the lines. The grammar —
|
|
38
|
+
* header (group label, no values) · item (indented, plain) · subtotal (rule
|
|
39
|
+
* above, semibold) · total (DOUBLE rule above, semibold) — covers all three
|
|
40
|
+
* statements; only the data differs. */
|
|
41
|
+
function Statement({ columns, lines }: { columns: string[]; lines: Line[] }) {
|
|
42
|
+
const cells = (l: Line, weight?: "medium" | "semibold") =>
|
|
43
|
+
(l.values ?? columns.map(() => null)).map((v, i) => (
|
|
44
|
+
<Text key={i} size="sm" weight={weight} tabular align="right" style={{ width: COL_WIDTH }}>
|
|
45
|
+
{v == null ? "" : num(v)}
|
|
46
|
+
</Text>
|
|
47
|
+
));
|
|
48
|
+
return (
|
|
49
|
+
<View>
|
|
50
|
+
{/* column captions */}
|
|
51
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 12, paddingBottom: 6 }}>
|
|
52
|
+
<View style={{ flex: 1 }} />
|
|
53
|
+
{columns.map((c) => (
|
|
54
|
+
<Text key={c} size="xs" weight="medium" color="muted" align="right" style={{ width: COL_WIDTH }}>
|
|
55
|
+
{c}
|
|
56
|
+
</Text>
|
|
57
|
+
))}
|
|
58
|
+
</View>
|
|
59
|
+
<Divider />
|
|
60
|
+
<View style={{ paddingTop: 6, gap: 2 }}>
|
|
61
|
+
{lines.map((l, i) => {
|
|
62
|
+
if (l.kind === "header") {
|
|
63
|
+
return (
|
|
64
|
+
<Text key={i} size="sm" weight="semibold" style={{ paddingTop: i === 0 ? 0 : 10 }}>
|
|
65
|
+
{l.label}
|
|
66
|
+
</Text>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (l.kind === "item") {
|
|
70
|
+
return (
|
|
71
|
+
<View key={i} style={{ flexDirection: "row", alignItems: "center", gap: 12, minHeight: 26 }}>
|
|
72
|
+
<Text size="sm" style={{ flex: 1, paddingLeft: 12 }}>{l.label}</Text>
|
|
73
|
+
{cells(l)}
|
|
74
|
+
</View>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (l.kind === "subtotal") {
|
|
78
|
+
return (
|
|
79
|
+
<View key={i} style={{ gap: 2 }}>
|
|
80
|
+
<View style={{ flexDirection: "row", gap: 12 }}>
|
|
81
|
+
<View style={{ flex: 1 }} />
|
|
82
|
+
<View style={{ width: COL_WIDTH * columns.length + 12 * (columns.length - 1), borderTopWidth: 1, borderTopColor: colors.zinc[200] }} />
|
|
83
|
+
</View>
|
|
84
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 12, minHeight: 26 }}>
|
|
85
|
+
<Text size="sm" weight="medium" style={{ flex: 1 }}>{l.label}</Text>
|
|
86
|
+
{cells(l, "medium")}
|
|
87
|
+
</View>
|
|
88
|
+
</View>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
// total — the double rule
|
|
92
|
+
return (
|
|
93
|
+
<View key={i} style={{ gap: 2, paddingTop: 4 }}>
|
|
94
|
+
<View style={{ flexDirection: "row", gap: 12 }}>
|
|
95
|
+
<View style={{ flex: 1 }} />
|
|
96
|
+
<View style={{ width: COL_WIDTH * columns.length + 12 * (columns.length - 1), gap: 2 }}>
|
|
97
|
+
<View style={{ borderTopWidth: 1, borderTopColor: colors.zinc[400] }} />
|
|
98
|
+
<View style={{ borderTopWidth: 1, borderTopColor: colors.zinc[400] }} />
|
|
99
|
+
</View>
|
|
100
|
+
</View>
|
|
101
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 12, minHeight: 30 }}>
|
|
102
|
+
<Text size="sm" weight="semibold" style={{ flex: 1 }}>{l.label}</Text>
|
|
103
|
+
{cells(l, "semibold")}
|
|
104
|
+
</View>
|
|
105
|
+
</View>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
108
|
+
</View>
|
|
109
|
+
</View>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The statement page: title + the one meta line (entity · period · basis ·
|
|
114
|
+
* currency, stated ONCE so cells stay currency-free) + Export, then the
|
|
115
|
+
* statement column. */
|
|
116
|
+
function StatementPage({ title, meta, children }: { title: string; meta: string; children: ReactNode }) {
|
|
117
|
+
return (
|
|
118
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28, paddingBottom: 96 }}>
|
|
119
|
+
<View style={{ width: "100%", maxWidth: 680, alignSelf: "center", gap: 20 }}>
|
|
120
|
+
<View style={{ flexDirection: "row", alignItems: "flex-start", flexWrap: "wrap", columnGap: 12, rowGap: 8 }}>
|
|
121
|
+
<View style={{ flexGrow: 1, flexBasis: 240, gap: 2 }}>
|
|
122
|
+
<Text size="xl" weight="semibold">{title}</Text>
|
|
123
|
+
<Text size="sm" color="muted">{meta}</Text>
|
|
124
|
+
</View>
|
|
125
|
+
<Button title="Export" color="secondary" icon="file-down" onPress={() => {}} />
|
|
126
|
+
</View>
|
|
127
|
+
{children}
|
|
128
|
+
<Text size="xs" color="muted">Unaudited management accounts. Figures in VND.</Text>
|
|
129
|
+
</View>
|
|
130
|
+
</ScrollView>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── the data — internally exact AND tied across the three statements:
|
|
135
|
+
// net income 81.840 → retained earnings 203.600 + 81.840 − 40.740 dividends
|
|
136
|
+
// = 244.700; closing cash 400.800 IS the balance sheet's cash; the loan
|
|
137
|
+
// repayment takes long-term debt 90.000 → 80.000. (thousands of VND)
|
|
138
|
+
const K = 1000;
|
|
139
|
+
|
|
140
|
+
export function TplIncomeStatement() {
|
|
141
|
+
const lines: Line[] = [
|
|
142
|
+
{ kind: "header", label: "Revenue" },
|
|
143
|
+
{ kind: "item", label: "Service revenue", values: [842_500 * K, 781_200 * K] },
|
|
144
|
+
{ kind: "item", label: "Disbursement recharges", values: [128_400 * K, 118_900 * K] },
|
|
145
|
+
{ kind: "subtotal", label: "Total revenue", values: [970_900 * K, 900_100 * K] },
|
|
146
|
+
{ kind: "header", label: "Cost of services" },
|
|
147
|
+
{ kind: "item", label: "Carrier & handling", values: [-512_300 * K, -471_800 * K] },
|
|
148
|
+
{ kind: "item", label: "Customs & levies", values: [-148_200 * K, -139_600 * K] },
|
|
149
|
+
{ kind: "subtotal", label: "Gross profit", values: [310_400 * K, 288_700 * K] },
|
|
150
|
+
{ kind: "header", label: "Operating expenses" },
|
|
151
|
+
{ kind: "item", label: "Salaries & benefits", values: [-142_000 * K, -138_500 * K] },
|
|
152
|
+
{ kind: "item", label: "Office & software", values: [-38_400 * K, -37_200 * K] },
|
|
153
|
+
{ kind: "item", label: "Marketing", values: [-21_500 * K, -18_900 * K] },
|
|
154
|
+
{ kind: "subtotal", label: "Operating income", values: [108_500 * K, 94_100 * K] },
|
|
155
|
+
{ kind: "item", label: "Interest expense", values: [-6_200 * K, -6_400 * K] },
|
|
156
|
+
{ kind: "subtotal", label: "Profit before tax", values: [102_300 * K, 87_700 * K] },
|
|
157
|
+
{ kind: "item", label: "Income tax (20%)", values: [-20_460 * K, -17_540 * K] },
|
|
158
|
+
{ kind: "total", label: "Net income", values: [81_840 * K, 70_160 * K] },
|
|
159
|
+
];
|
|
160
|
+
return (
|
|
161
|
+
<StatementPage title="Income statement" meta="VITTORIA Logistics · month ended 30/06/2026 · VND">
|
|
162
|
+
<Statement columns={["Jun 2026", "May 2026"]} lines={lines} />
|
|
163
|
+
</StatementPage>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function TplBalanceSheet() {
|
|
168
|
+
const lines: Line[] = [
|
|
169
|
+
{ kind: "header", label: "Assets" },
|
|
170
|
+
{ kind: "item", label: "Cash & equivalents", values: [400_800 * K, 355_100 * K] },
|
|
171
|
+
{ kind: "item", label: "Accounts receivable", values: [386_200 * K, 401_600 * K] },
|
|
172
|
+
{ kind: "item", label: "Advances & deposits", values: [54_500 * K, 48_200 * K] },
|
|
173
|
+
{ kind: "subtotal", label: "Current assets", values: [841_500 * K, 804_900 * K] },
|
|
174
|
+
{ kind: "item", label: "Equipment, net", values: [130_400 * K, 121_900 * K] },
|
|
175
|
+
{ kind: "total", label: "Total assets", values: [971_900 * K, 926_800 * K] },
|
|
176
|
+
{ kind: "header", label: "Liabilities" },
|
|
177
|
+
{ kind: "item", label: "Accounts payable", values: [268_300 * K, 259_400 * K] },
|
|
178
|
+
{ kind: "item", label: "Taxes payable", values: [42_100 * K, 38_600 * K] },
|
|
179
|
+
{ kind: "item", label: "Accrued salaries", values: [36_800 * K, 35_200 * K] },
|
|
180
|
+
{ kind: "subtotal", label: "Current liabilities", values: [347_200 * K, 333_200 * K] },
|
|
181
|
+
{ kind: "item", label: "Long-term debt", values: [80_000 * K, 90_000 * K] },
|
|
182
|
+
{ kind: "subtotal", label: "Total liabilities", values: [427_200 * K, 423_200 * K] },
|
|
183
|
+
{ kind: "header", label: "Equity" },
|
|
184
|
+
{ kind: "item", label: "Contributed capital", values: [300_000 * K, 300_000 * K] },
|
|
185
|
+
{ kind: "item", label: "Retained earnings", values: [244_700 * K, 203_600 * K] },
|
|
186
|
+
{ kind: "subtotal", label: "Total equity", values: [544_700 * K, 503_600 * K] },
|
|
187
|
+
{ kind: "total", label: "Total liabilities & equity", values: [971_900 * K, 926_800 * K] },
|
|
188
|
+
];
|
|
189
|
+
return (
|
|
190
|
+
<StatementPage title="Balance sheet" meta="VITTORIA Logistics · as at 30/06/2026 · VND">
|
|
191
|
+
<Statement columns={["30/06/2026", "31/05/2026"]} lines={lines} />
|
|
192
|
+
</StatementPage>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function TplCashflow() {
|
|
197
|
+
const lines: Line[] = [
|
|
198
|
+
{ kind: "header", label: "Operating activities" },
|
|
199
|
+
{ kind: "item", label: "Net income", values: [81_840 * K] },
|
|
200
|
+
{ kind: "item", label: "Depreciation", values: [3_500 * K] },
|
|
201
|
+
{ kind: "item", label: "Decrease in accounts receivable", values: [15_400 * K] },
|
|
202
|
+
{ kind: "item", label: "Increase in accounts payable", values: [8_900 * K] },
|
|
203
|
+
{ kind: "item", label: "Other working-capital movements", values: [-1_200 * K] },
|
|
204
|
+
{ kind: "subtotal", label: "Net cash from operating activities", values: [108_440 * K] },
|
|
205
|
+
{ kind: "header", label: "Investing activities" },
|
|
206
|
+
{ kind: "item", label: "Purchases of equipment", values: [-12_000 * K] },
|
|
207
|
+
{ kind: "subtotal", label: "Net cash used in investing activities", values: [-12_000 * K] },
|
|
208
|
+
{ kind: "header", label: "Financing activities" },
|
|
209
|
+
{ kind: "item", label: "Repayment of long-term debt", values: [-10_000 * K] },
|
|
210
|
+
{ kind: "item", label: "Dividends paid", values: [-40_740 * K] },
|
|
211
|
+
{ kind: "subtotal", label: "Net cash used in financing activities", values: [-50_740 * K] },
|
|
212
|
+
{ kind: "subtotal", label: "Net increase in cash", values: [45_700 * K] },
|
|
213
|
+
{ kind: "item", label: "Cash at 01/06/2026", values: [355_100 * K] },
|
|
214
|
+
{ kind: "total", label: "Cash at 30/06/2026", values: [400_800 * K] },
|
|
215
|
+
];
|
|
216
|
+
return (
|
|
217
|
+
<StatementPage title="Cash flow statement" meta="VITTORIA Logistics · month ended 30/06/2026 · VND">
|
|
218
|
+
<Statement columns={["Jun 2026"]} lines={lines} />
|
|
219
|
+
</StatementPage>
|
|
220
|
+
);
|
|
221
|
+
}
|
package/examples/tpl_stock.tsx
CHANGED
|
@@ -18,7 +18,7 @@ import { SearchInput } from "@lotics/ui/search_input";
|
|
|
18
18
|
import { formatMoney } from "@lotics/ui/format_money";
|
|
19
19
|
|
|
20
20
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
-
// Template ·
|
|
21
|
+
// Template · Drill-down overview — monitoring a population too large to browse
|
|
22
22
|
// (10,000+ units). The funnel: KPI strip (population health) → Breakdown
|
|
23
23
|
// cards (composition by dimension; pressing a segment is the drill-down) →
|
|
24
24
|
// dismissible facet chips + search → PAGINATED register (the data is the
|
|
@@ -283,12 +283,12 @@ export function TplStock() {
|
|
|
283
283
|
|
|
284
284
|
{/* eyebrow columns */}
|
|
285
285
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 12, paddingHorizontal: 20, paddingVertical: 10 }}>
|
|
286
|
-
<Text size="xs" color="muted"
|
|
287
|
-
<Text size="xs" color="muted"
|
|
288
|
-
<Text size="xs" color="muted"
|
|
289
|
-
<Text size="xs" color="muted"
|
|
290
|
-
<Text size="xs" color="muted"
|
|
291
|
-
<Text size="xs" color="muted"
|
|
286
|
+
<Text size="xs" color="muted" weight="medium" style={{ width: 110 }}>Unit no</Text>
|
|
287
|
+
<Text size="xs" color="muted" weight="medium" style={{ flex: 1 }}>Type</Text>
|
|
288
|
+
<Text size="xs" color="muted" weight="medium" style={{ width: 96 }}>Depot</Text>
|
|
289
|
+
<Text size="xs" color="muted" weight="medium" style={{ width: 76 }}>Last move</Text>
|
|
290
|
+
<Text size="xs" color="muted" weight="medium" tabular align="right" style={{ width: 104 }}>Value</Text>
|
|
291
|
+
<Text size="xs" color="muted" weight="medium" style={{ width: 110 }}>Status</Text>
|
|
292
292
|
<View style={{ width: 28 }} />
|
|
293
293
|
</View>
|
|
294
294
|
<Divider />
|
|
@@ -25,10 +25,11 @@ import { Popover, PopoverTrigger, PopoverContent } from "@lotics/ui/popover";
|
|
|
25
25
|
import { FilesEditor } from "@lotics/ui/files_editor";
|
|
26
26
|
import { FileThumbnail, type DisplayFile } from "@lotics/ui/file_thumbnail";
|
|
27
27
|
import { DataGrid, gridRowStyle, type DataGridColumn, type DataGridGroup } from "@lotics/ui/data_grid";
|
|
28
|
+
import { CONTROL_RADIUS, HOVER_BORDER } from "@lotics/ui/control_surface";
|
|
28
29
|
import { cycleSort, sortBy, type SortState } from "@lotics/ui/sort_header";
|
|
29
30
|
import { Composer } from "@lotics/ui/composer";
|
|
30
31
|
import { AgentRun, type AgentRunItem } from "@lotics/ui/agent_run";
|
|
31
|
-
import { ChangeReview, ChangeReviewActions, type
|
|
32
|
+
import { Change, ChangeReview, ChangeReviewActions, ChangeReviewHeader, ChangeSummary, type ChangeStatus } from "@lotics/ui/change_review";
|
|
32
33
|
import { Dialog, DialogHeader, DialogHeaderTitle, DialogScrollArea, DialogFooter } from "@lotics/ui/dialog";
|
|
33
34
|
import { CompletionState } from "@lotics/ui/completion_state";
|
|
34
35
|
import { Confidence, type ConfidenceLevel } from "@lotics/ui/confidence";
|
|
@@ -225,7 +226,7 @@ export function TplTaskBoard() {
|
|
|
225
226
|
const [phase, setPhase] = useState<Phase>("idle");
|
|
226
227
|
const [revealed, setRevealed] = useState(0);
|
|
227
228
|
const [proposals, setProposals] = useState<Proposal[]>([]);
|
|
228
|
-
const [decisions, setDecisions] = useState<Record<string,
|
|
229
|
+
const [decisions, setDecisions] = useState<Record<string, ChangeStatus>>({});
|
|
229
230
|
const [addedCount, setAddedCount] = useState(0);
|
|
230
231
|
|
|
231
232
|
const startDraft = useCallback(() => { setRevealed(0); setPhase("reading"); }, []);
|
|
@@ -233,7 +234,7 @@ export function TplTaskBoard() {
|
|
|
233
234
|
const closeDraft = useCallback(() => { setDraftOpen(false); resetDraft(); }, [resetDraft]);
|
|
234
235
|
const editProposal = useCallback((id: string, p: Partial<Proposal>) =>
|
|
235
236
|
setProposals((ps) => ps.map((x) => (x.id === id ? { ...x, ...p } : x))), []);
|
|
236
|
-
const decide = useCallback((id: string, status:
|
|
237
|
+
const decide = useCallback((id: string, status: ChangeStatus | null) =>
|
|
237
238
|
setDecisions((dec) => { const n = { ...dec }; if (status) n[id] = status; else delete n[id]; return n; }), []);
|
|
238
239
|
const acceptedCount = proposals.filter((p) => decisions[p.id] === "accepted").length;
|
|
239
240
|
const addAccepted = useCallback(() => {
|
|
@@ -264,9 +265,9 @@ export function TplTaskBoard() {
|
|
|
264
265
|
}
|
|
265
266
|
|
|
266
267
|
const propertyCols: DataGridColumn<Task>[] = [
|
|
267
|
-
{ key: "assignee", label: "Assignee", width: 160, sortable: true, cell: (t) => <InlineMemberSelect members={MEMBERS} value={t.ownerId} onSave={(id) => patch(t.id, { ownerId: id })} placeholder="Unassigned" accessibilityLabel="Assignee" /> },
|
|
268
|
-
{ key: "due", label: "Due", width: 140, sortable: true, cell: (t) => <InlineDatePicker value={t.due} optionalTime onSave={(v) => patch(t.id, { due: v })} placeholder="No date" accessibilityLabel="Due date" /> },
|
|
269
|
-
{ key: "status", label: "Status", width: 130, sortable: true, cell: (t) => <InlineSelect value={t.status} options={STATUS_OPTIONS} onSave={(s) => patch(t.id, { status: s })} renderSelected={renderStatusBadge} renderOptionContent={renderStatusBadge} accessibilityLabel="Status" /> },
|
|
268
|
+
{ key: "assignee", label: "Assignee", width: 160, sortable: true, cell: (t) => <InlineMemberSelect background="transparent" members={MEMBERS} value={t.ownerId} onSave={(id) => patch(t.id, { ownerId: id })} placeholder="Unassigned" accessibilityLabel="Assignee" /> },
|
|
269
|
+
{ key: "due", label: "Due", width: 140, sortable: true, cell: (t) => <InlineDatePicker background="transparent" value={t.due} optionalTime onSave={(v) => patch(t.id, { due: v })} placeholder="No date" accessibilityLabel="Due date" /> },
|
|
270
|
+
{ key: "status", label: "Status", width: 130, sortable: true, cell: (t) => <InlineSelect background="transparent" value={t.status} options={STATUS_OPTIONS} onSave={(s) => patch(t.id, { status: s })} renderSelected={renderStatusBadge} renderOptionContent={renderStatusBadge} accessibilityLabel="Status" /> },
|
|
270
271
|
{ key: "tags", label: "Tags", width: 168, cell: (t) => <Select multi searchable allowCustom value={t.tags.map((tag) => tag.value)} onValueChange={(next) => patch(t.id, { tags: next.map(tagOf) })} options={TAG_OPTIONS} renderSelected={renderTagBadge} renderOptionContent={renderTagBadge} style={{ borderColor: "transparent" }} placeholder="Add tags" accessibilityLabel="Tags" /> },
|
|
271
272
|
];
|
|
272
273
|
const filesCol: DataGridColumn<Task> = {
|
|
@@ -294,7 +295,7 @@ export function TplTaskBoard() {
|
|
|
294
295
|
const visibleProps = propertyCols.filter((c) => c.key !== groupBy);
|
|
295
296
|
const addCols = [...visibleProps, filesCol, actionCol, menuCol];
|
|
296
297
|
const columns: DataGridColumn<Task>[] = [
|
|
297
|
-
{ key: "title", label: "Task", sortable: true, cell: (t) => <InlineTextInput value={t.title} onSave={(v) => patch(t.id, { title: v })} struck={t.status === "done"} accessibilityLabel="Title" /> },
|
|
298
|
+
{ key: "title", label: "Task", sortable: true, cell: (t) => <InlineTextInput background="transparent" value={t.title} onSave={(v) => patch(t.id, { title: v })} struck={t.status === "done"} accessibilityLabel="Title" /> },
|
|
298
299
|
...addCols,
|
|
299
300
|
];
|
|
300
301
|
|
|
@@ -371,7 +372,10 @@ export function TplTaskBoard() {
|
|
|
371
372
|
</View>
|
|
372
373
|
</ScrollView>
|
|
373
374
|
|
|
374
|
-
{/* Draft from notes — Composer → AgentRun → ChangeReview → commit (adds tasks).
|
|
375
|
+
{/* Draft from notes — Composer → AgentRun → ChangeReview → commit (adds tasks).
|
|
376
|
+
The review provider wraps the WHOLE dialog so the `Change`s (scroll area)
|
|
377
|
+
and the commit bar (`DialogFooter`) share one review context. */}
|
|
378
|
+
<ChangeReview>
|
|
375
379
|
<Dialog open={draftOpen} onOpenChange={(o) => { if (!o) closeDraft(); }} maxWidth={620}>
|
|
376
380
|
<DialogHeader><DialogHeaderTitle>Draft from notes</DialogHeaderTitle></DialogHeader>
|
|
377
381
|
<DialogScrollArea>
|
|
@@ -383,20 +387,29 @@ export function TplTaskBoard() {
|
|
|
383
387
|
) : null}
|
|
384
388
|
{phase === "reading" ? <AgentRun items={runItems} state={revealed >= DRAFT_STEPS.length ? "done" : "streaming"} /> : null}
|
|
385
389
|
{phase === "review" ? (
|
|
386
|
-
<
|
|
387
|
-
title="Drafted tasks"
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
390
|
+
<View style={{ gap: 8 }}>
|
|
391
|
+
<ChangeReviewHeader title="Drafted tasks" />
|
|
392
|
+
<Text size="sm" color="muted">{`${proposals.length} from your notes — keep, edit, or drop each. A “Low” mark is worth a glance.`}</Text>
|
|
393
|
+
{proposals.map((p) => {
|
|
394
|
+
const status = decisions[p.id] ?? "pending";
|
|
395
|
+
const kept = status === "accepted";
|
|
396
|
+
return (
|
|
397
|
+
<Change
|
|
398
|
+
key={p.id}
|
|
399
|
+
id={p.id}
|
|
400
|
+
status={status}
|
|
401
|
+
onAccept={() => decide(p.id, "accepted")}
|
|
402
|
+
onReject={() => decide(p.id, "rejected")}
|
|
403
|
+
onUndo={() => decide(p.id, null)}
|
|
404
|
+
>
|
|
405
|
+
<TaskProposal proposal={p} onEdit={editProposal} />
|
|
406
|
+
<ChangeSummary>
|
|
407
|
+
<Text size="sm" numberOfLines={1} color={kept ? "default" : "muted"} style={kept ? undefined : { textDecorationLine: "line-through" }}>{p.title}</Text>
|
|
408
|
+
</ChangeSummary>
|
|
409
|
+
</Change>
|
|
410
|
+
);
|
|
411
|
+
})}
|
|
412
|
+
</View>
|
|
400
413
|
) : null}
|
|
401
414
|
{phase === "done" ? (
|
|
402
415
|
<CompletionState title={`${addedCount} ${addedCount === 1 ? "task" : "tasks"} added`} summary="They're on the board now." />
|
|
@@ -404,7 +417,7 @@ export function TplTaskBoard() {
|
|
|
404
417
|
</DialogScrollArea>
|
|
405
418
|
{phase === "review" ? (
|
|
406
419
|
<DialogFooter>
|
|
407
|
-
<ChangeReviewActions
|
|
420
|
+
<ChangeReviewActions onApply={addAccepted} onDiscard={resetDraft} applyLabel={`Add ${acceptedCount} ${acceptedCount === 1 ? "task" : "tasks"}`} />
|
|
408
421
|
</DialogFooter>
|
|
409
422
|
) : phase === "done" ? (
|
|
410
423
|
<DialogFooter>
|
|
@@ -413,6 +426,7 @@ export function TplTaskBoard() {
|
|
|
413
426
|
</DialogFooter>
|
|
414
427
|
) : null}
|
|
415
428
|
</Dialog>
|
|
429
|
+
</ChangeReview>
|
|
416
430
|
</>
|
|
417
431
|
);
|
|
418
432
|
}
|
|
@@ -422,12 +436,12 @@ function TaskProposal({ proposal, onEdit }: { proposal: Proposal; onEdit: (id: s
|
|
|
422
436
|
<View style={{ gap: 10 }}>
|
|
423
437
|
<View style={{ flexDirection: "row", alignItems: "flex-start", gap: 12 }}>
|
|
424
438
|
<View style={{ flex: 1 }}>
|
|
425
|
-
<InlineTextInput value={proposal.title} onSave={(v) => onEdit(proposal.id, { title: v })} accessibilityLabel="Task title" />
|
|
439
|
+
<InlineTextInput background="transparent" value={proposal.title} onSave={(v) => onEdit(proposal.id, { title: v })} accessibilityLabel="Task title" />
|
|
426
440
|
</View>
|
|
427
441
|
<View style={{ paddingTop: 10 }}><Confidence level={proposal.confidence} /></View>
|
|
428
442
|
</View>
|
|
429
443
|
<View style={{ width: 220 }}>
|
|
430
|
-
<InlineDatePicker value={proposal.due} optionalTime onSave={(v) => onEdit(proposal.id, { due: v })} placeholder="No date" accessibilityLabel="Due date" />
|
|
444
|
+
<InlineDatePicker background="transparent" value={proposal.due} optionalTime onSave={(v) => onEdit(proposal.id, { due: v })} placeholder="No date" accessibilityLabel="Due date" />
|
|
431
445
|
</View>
|
|
432
446
|
<View style={styles.source}><Text size="xs" color="muted">{proposal.source}</Text></View>
|
|
433
447
|
</View>
|
|
@@ -514,9 +528,9 @@ function TaskAddRow({ preset, columns, onAdd }: { preset: Partial<Task>; columns
|
|
|
514
528
|
// Files / action / ⋯ act on a task that already exists, so the add row leaves
|
|
515
529
|
// those columns empty — the widths are still reserved so the columns stay aligned.
|
|
516
530
|
const editor = (key: string) =>
|
|
517
|
-
key === "assignee" ? <InlineMemberSelect members={MEMBERS} value={ownerId} onSave={setOwnerId} placeholder="Assignee" accessibilityLabel="Assignee" />
|
|
518
|
-
: key === "due" ? <InlineDatePicker value={due} optionalTime onSave={setDue} placeholder="No date" accessibilityLabel="Due date" />
|
|
519
|
-
: key === "status" ? <InlineSelect value={status} options={STATUS_OPTIONS} onSave={setStatus} renderSelected={renderStatusBadge} renderOptionContent={renderStatusBadge} accessibilityLabel="Status" />
|
|
531
|
+
key === "assignee" ? <InlineMemberSelect background="transparent" members={MEMBERS} value={ownerId} onSave={setOwnerId} placeholder="Assignee" accessibilityLabel="Assignee" />
|
|
532
|
+
: key === "due" ? <InlineDatePicker background="transparent" value={due} optionalTime onSave={setDue} placeholder="No date" accessibilityLabel="Due date" />
|
|
533
|
+
: key === "status" ? <InlineSelect background="transparent" value={status} options={STATUS_OPTIONS} onSave={setStatus} renderSelected={renderStatusBadge} renderOptionContent={renderStatusBadge} accessibilityLabel="Status" />
|
|
520
534
|
: key === "tags" ? <Select multi searchable allowCustom value={tags.map((tag) => tag.value)} onValueChange={(next) => setTags(next.map(tagOf))} options={TAG_OPTIONS} renderSelected={renderTagBadge} renderOptionContent={renderTagBadge} style={{ borderColor: "transparent" }} placeholder="Add tags" accessibilityLabel="Tags" />
|
|
521
535
|
: null;
|
|
522
536
|
|
|
@@ -544,14 +558,16 @@ const styles = StyleSheet.create({
|
|
|
544
558
|
addField: { flex: 1, flexDirection: "row", alignItems: "center", gap: 8, paddingRight: 4 },
|
|
545
559
|
// Match the inline-editor cells' resting box (INLINE_CONTROL_HEIGHT = 40) so the
|
|
546
560
|
// files field lines up with the others — and that height affords a larger thumbnail.
|
|
547
|
-
|
|
548
|
-
|
|
561
|
+
// Hover = the control-surface BORDER reveal, the same language as the
|
|
562
|
+
// inline-editor cells beside these — never a background wash of its own.
|
|
563
|
+
clip: { flexDirection: "row", alignItems: "center", gap: 6, minHeight: 40, paddingHorizontal: 8, borderRadius: CONTROL_RADIUS, borderWidth: 1, borderColor: "transparent", cursor: "pointer" },
|
|
564
|
+
clipHover: { borderColor: HOVER_BORDER },
|
|
549
565
|
clipPreview: { flexDirection: "row", alignItems: "center", gap: 6 },
|
|
550
566
|
summary: { flexDirection: "row", alignItems: "center", gap: 4 },
|
|
551
567
|
summaryDot: { width: 8, height: 8, borderRadius: 999 },
|
|
552
568
|
result: { flexDirection: "row", alignItems: "center", gap: 6, minHeight: 40 },
|
|
553
569
|
// Fills the cell (height + width) so the link's hit area matches the inline cells.
|
|
554
|
-
action: { justifyContent: "center", minHeight: 40, paddingHorizontal: 8, borderRadius:
|
|
555
|
-
actionHover: {
|
|
570
|
+
action: { justifyContent: "center", minHeight: 40, paddingHorizontal: 8, borderRadius: CONTROL_RADIUS, borderWidth: 1, borderColor: "transparent", cursor: "pointer" },
|
|
571
|
+
actionHover: { borderColor: HOVER_BORDER },
|
|
556
572
|
source: { borderLeftWidth: 2, borderLeftColor: colors.zinc[200], paddingLeft: 10 },
|
|
557
573
|
});
|