@lotics/ui 46.2.0 → 46.8.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 (49) hide show
  1. package/AGENTS.md +38 -1
  2. package/MIGRATION.md +87 -0
  3. package/docs/ai_patterns.md +11 -0
  4. package/docs/catalog.md +217 -21
  5. package/docs/composition.md +74 -6
  6. package/docs/data_entry.md +56 -2
  7. package/docs/reviewing.md +34 -0
  8. package/docs/templates.md +93 -30
  9. package/docs/testing.md +6 -0
  10. package/examples/tpl_board.tsx +257 -0
  11. package/examples/tpl_money.tsx +1027 -0
  12. package/package.json +261 -258
  13. package/src/accordion.tsx +7 -1
  14. package/src/alert.css +0 -1
  15. package/src/alert.tsx +8 -0
  16. package/src/axis_label_indices.ts +84 -0
  17. package/src/bar_chart.tsx +137 -16
  18. package/src/board.tsx +611 -0
  19. package/src/card.tsx +7 -1
  20. package/src/charge_lines.tsx +373 -0
  21. package/src/chip_group.tsx +57 -1
  22. package/src/dialog.tsx +46 -24
  23. package/src/drawer.tsx +21 -2
  24. package/src/file_gallery_modal.tsx +3 -0
  25. package/src/file_row.tsx +98 -5
  26. package/src/icon.tsx +6 -0
  27. package/src/inline_edit.tsx +54 -10
  28. package/src/inline_number_input.tsx +5 -1
  29. package/src/inline_text_input.tsx +1 -1
  30. package/src/line_chart.tsx +2 -2
  31. package/src/locale.tsx +26 -1
  32. package/src/matrix.tsx +23 -8
  33. package/src/modal.tsx +23 -3
  34. package/src/overlay_layer.ts +65 -0
  35. package/src/page_content.tsx +8 -22
  36. package/src/page_header.tsx +60 -11
  37. package/src/popover.tsx +29 -5
  38. package/src/reference_field.tsx +36 -13
  39. package/src/skip_link.tsx +2 -1
  40. package/src/stacked_bar_chart.tsx +31 -1
  41. package/src/table.tsx +6 -1
  42. package/src/tabs.tsx +1 -1
  43. package/src/text.tsx +21 -0
  44. package/src/tooltip.tsx +2 -1
  45. package/src/use_change_set.ts +66 -17
  46. package/src/use_scroll_seam.ts +79 -0
  47. package/examples/tpl_report.tsx +0 -410
  48. package/examples/tpl_statements.tsx +0 -221
  49. package/src/line_chart_labels.ts +0 -32
@@ -1,221 +0,0 @@
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="xxl" 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
- }
@@ -1,32 +0,0 @@
1
- /**
2
- * Which of a line chart's x positions get a printed label.
3
- *
4
- * A label is roughly `minLabelWidth` wide, so only so many fit across the
5
- * track; the rest are thinned out. The rule that matters is WHERE the thinning
6
- * is anchored: stepping forwards from the first point and then forcing the last
7
- * one in collides whenever the series length minus one is not a multiple of the
8
- * step — the newest label lands a few pixels from the one before it while every
9
- * other pair is a full step apart. Anchoring on the LAST point makes the spacing
10
- * uniform by construction, and the last point is the one a reader looks up
11
- * first. The first point is then kept only when it clears the same distance.
12
- */
13
- export function lineChartLabelIndices(
14
- count: number,
15
- chartWidth: number,
16
- minLabelWidth = 50,
17
- ): number[] {
18
- if (count <= 0 || chartWidth <= 0) return [];
19
- if (count === 1) return [0];
20
-
21
- const last = count - 1;
22
- const maxLabels = Math.max(2, Math.floor(chartWidth / minLabelWidth));
23
- const step = Math.max(1, Math.ceil(count / maxLabels));
24
-
25
- const kept: number[] = [];
26
- for (let i = last; i >= 0; i -= step) kept.unshift(i);
27
-
28
- const firstKept = kept[0] ?? 0;
29
- if (firstKept > 0 && (firstKept / last) * chartWidth >= minLabelWidth) kept.unshift(0);
30
-
31
- return kept;
32
- }