@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/src/ledger.tsx
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
+
import { StyleProp, StyleSheet, View, ViewStyle } from "react-native";
|
|
3
|
+
import { colors } from "./colors";
|
|
4
|
+
import { Divider } from "./divider";
|
|
5
|
+
import { Link } from "./link";
|
|
6
|
+
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
|
7
|
+
import { PressableHighlight } from "./pressable_highlight";
|
|
8
|
+
import { useLoticsLocale } from "./locale";
|
|
9
|
+
import { Text } from "./text";
|
|
10
|
+
|
|
11
|
+
// The record-level money list — a compact financial STATEMENT for one record's
|
|
12
|
+
// drawer/section: charge/receipt GROUPS with their sums in the group headers,
|
|
13
|
+
// every figure on ONE right-aligned tabular column, closed by a divider-set
|
|
14
|
+
// emphasized total. A row with `peek` is a door: pressing it floats the fee's
|
|
15
|
+
// PARTICULARS (basis, who charged it, the invoice it landed on) in an anchored
|
|
16
|
+
// popover — the list stays flat and scannable, the depth is on demand. A row
|
|
17
|
+
// with `reference` carries a trailing link instead (an issued invoice, a
|
|
18
|
+
// receipt no.). No bars, no charts — the numbers are the interface.
|
|
19
|
+
//
|
|
20
|
+
// <Ledger formatValue={formatMoney}>
|
|
21
|
+
// <LedgerGroup label="Charges" total={total}>
|
|
22
|
+
// <LedgerRow label="Service fee" value={fee} peek={<FeeParticulars … />} />
|
|
23
|
+
// <LedgerRow label="Levy" value={levy} reference={{ label: "INV-0414", onPress }} />
|
|
24
|
+
// </LedgerGroup>
|
|
25
|
+
// <LedgerGroup label="Received" total={-received}>
|
|
26
|
+
// <LedgerRow label="Deposit" meta="03/06 · Cash" value={-1_550_000} tone="success" />
|
|
27
|
+
// </LedgerGroup>
|
|
28
|
+
// <LedgerTotal label="Outstanding" value={due} tone="danger" zeroLabel="Paid in full" />
|
|
29
|
+
// </Ledger>
|
|
30
|
+
|
|
31
|
+
interface LedgerContextValue {
|
|
32
|
+
format: (n: number) => string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const LedgerContext = createContext<LedgerContextValue | null>(null);
|
|
36
|
+
|
|
37
|
+
function useLedger(): LedgerContextValue {
|
|
38
|
+
const ctx = useContext(LedgerContext);
|
|
39
|
+
if (!ctx) throw new Error("Ledger components must be used within a Ledger");
|
|
40
|
+
return ctx;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Signed display: negatives render as "− <abs>" (a receipt against charges).
|
|
44
|
+
* `n + 0` normalizes -0 (a zero receipts sum passed negated) to plain 0. */
|
|
45
|
+
function signed(format: (n: number) => string, n: number) {
|
|
46
|
+
return n < 0 ? `− ${format(-n)}` : format(n + 0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface LedgerProps {
|
|
50
|
+
/** Money formatting shared by every row, sum, and total (e.g. `formatMoney`). */
|
|
51
|
+
formatValue: (n: number) => string;
|
|
52
|
+
children: ReactNode;
|
|
53
|
+
style?: StyleProp<ViewStyle>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function Ledger(props: LedgerProps) {
|
|
57
|
+
const { formatValue, children, style } = props;
|
|
58
|
+
return (
|
|
59
|
+
<LedgerContext.Provider value={{ format: formatValue }}>
|
|
60
|
+
<View style={[styles.ledger, style]}>{children}</View>
|
|
61
|
+
</LedgerContext.Provider>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface LedgerGroupProps {
|
|
66
|
+
/** The group name ("Charges", "Received") — xs muted, with the sum opposite. */
|
|
67
|
+
label: string;
|
|
68
|
+
/** The group's sum, shown right-aligned in the header. Sign renders as
|
|
69
|
+
* "−" (pass receipts negative). Omit to leave the header sumless. */
|
|
70
|
+
total?: number;
|
|
71
|
+
children: ReactNode;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One side of the ledger — a labelled run of rows with its sum in the header. */
|
|
75
|
+
export function LedgerGroup(props: LedgerGroupProps) {
|
|
76
|
+
const { label, total, children } = props;
|
|
77
|
+
const { format } = useLedger();
|
|
78
|
+
return (
|
|
79
|
+
<View style={styles.group}>
|
|
80
|
+
<View style={styles.row}>
|
|
81
|
+
<Text size="xs" weight="medium" color="muted" style={styles.grow}>
|
|
82
|
+
{label}
|
|
83
|
+
</Text>
|
|
84
|
+
{total != null ? (
|
|
85
|
+
<Text size="xs" color="muted" tabular>
|
|
86
|
+
{signed(format, total)}
|
|
87
|
+
</Text>
|
|
88
|
+
) : null}
|
|
89
|
+
</View>
|
|
90
|
+
{children}
|
|
91
|
+
</View>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface LedgerRowProps {
|
|
96
|
+
/** The line's name — what was charged / received. */
|
|
97
|
+
label: string;
|
|
98
|
+
/** Inline context after the label — a date · method, a basis, a period. */
|
|
99
|
+
meta?: string;
|
|
100
|
+
/** The amount. Negative renders "− <abs>" (a receipt). */
|
|
101
|
+
value: number;
|
|
102
|
+
/** Valence of the amount (a receipt reads success). */
|
|
103
|
+
tone?: "default" | "success" | "danger";
|
|
104
|
+
/** The line's PARTICULARS — set it and the whole row becomes a pressable
|
|
105
|
+
* door that floats this node in an anchored popover. Put any links (the
|
|
106
|
+
* invoice, the tariff) INSIDE the peek: a row is never a button holding
|
|
107
|
+
* another button, so `reference` is ignored while `peek` is set. */
|
|
108
|
+
peek?: ReactNode;
|
|
109
|
+
/** Peek popover width. Default 300. */
|
|
110
|
+
peekWidth?: number;
|
|
111
|
+
/** A trailing reference link (an issued invoice, a receipt no.) for a row
|
|
112
|
+
* WITHOUT `peek`. */
|
|
113
|
+
reference?: { label: string; onPress: () => void };
|
|
114
|
+
/** Accessible name for a peekable row. Defaults to "<label> details". */
|
|
115
|
+
accessibilityLabel?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** One money line. Static by default; a door to its particulars with `peek`. */
|
|
119
|
+
export function LedgerRow(props: LedgerRowProps) {
|
|
120
|
+
const { label, meta, value, tone = "default", peek, peekWidth = 300, reference, accessibilityLabel } = props;
|
|
121
|
+
const rowDetails = useLoticsLocale().ledger.rowDetails;
|
|
122
|
+
const { format } = useLedger();
|
|
123
|
+
const content = (
|
|
124
|
+
<>
|
|
125
|
+
<Text size="sm" numberOfLines={1} style={styles.shrink}>
|
|
126
|
+
{label}
|
|
127
|
+
</Text>
|
|
128
|
+
{meta ? (
|
|
129
|
+
<Text size="xs" color="muted" numberOfLines={1}>
|
|
130
|
+
{meta}
|
|
131
|
+
</Text>
|
|
132
|
+
) : null}
|
|
133
|
+
<View style={styles.grow} />
|
|
134
|
+
{!peek && reference ? (
|
|
135
|
+
<Link size="xs" onPress={reference.onPress} accessibilityLabel={reference.label}>
|
|
136
|
+
{reference.label}
|
|
137
|
+
</Link>
|
|
138
|
+
) : null}
|
|
139
|
+
<Text size="sm" tabular color={tone === "default" ? undefined : tone}>
|
|
140
|
+
{signed(format, value)}
|
|
141
|
+
</Text>
|
|
142
|
+
</>
|
|
143
|
+
);
|
|
144
|
+
if (!peek) {
|
|
145
|
+
return <View style={styles.row}>{content}</View>;
|
|
146
|
+
}
|
|
147
|
+
return (
|
|
148
|
+
<Popover side="bottom" align="end">
|
|
149
|
+
<PopoverTrigger>
|
|
150
|
+
<PressableHighlight
|
|
151
|
+
focusRing
|
|
152
|
+
accessibilityRole="button"
|
|
153
|
+
accessibilityLabel={accessibilityLabel ?? rowDetails(label)}
|
|
154
|
+
style={(state) => [styles.row, styles.rowPress, state.hovered ? styles.rowHovered : null]}
|
|
155
|
+
>
|
|
156
|
+
{content}
|
|
157
|
+
</PressableHighlight>
|
|
158
|
+
</PopoverTrigger>
|
|
159
|
+
<PopoverContent style={{ width: peekWidth }} disableBodyScroll>
|
|
160
|
+
{peek}
|
|
161
|
+
</PopoverContent>
|
|
162
|
+
</Popover>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface LedgerTotalProps {
|
|
167
|
+
/** The closing question the ledger answers ("Outstanding", "Balance"). */
|
|
168
|
+
label: string;
|
|
169
|
+
value: number;
|
|
170
|
+
/** Valence of the closing figure. */
|
|
171
|
+
tone?: "default" | "success" | "danger";
|
|
172
|
+
/** Rendered instead of the figure when `value` is 0 ("Paid in full") — in
|
|
173
|
+
* success tone, since a settled ledger is the good outcome. */
|
|
174
|
+
zeroLabel?: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The divider-set closing line — the ledger's ONE emphasized number. */
|
|
178
|
+
export function LedgerTotal(props: LedgerTotalProps) {
|
|
179
|
+
const { label, value, tone = "default", zeroLabel } = props;
|
|
180
|
+
const { format } = useLedger();
|
|
181
|
+
const settled = value === 0 && zeroLabel != null;
|
|
182
|
+
return (
|
|
183
|
+
<View style={styles.total}>
|
|
184
|
+
<Divider />
|
|
185
|
+
<View style={styles.row}>
|
|
186
|
+
<Text size="sm" weight="semibold" style={styles.grow}>
|
|
187
|
+
{label}
|
|
188
|
+
</Text>
|
|
189
|
+
<Text size="lg" weight="semibold" tabular color={settled ? "success" : tone === "default" ? undefined : tone}>
|
|
190
|
+
{settled ? zeroLabel : signed(format, value)}
|
|
191
|
+
</Text>
|
|
192
|
+
</View>
|
|
193
|
+
</View>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const styles = StyleSheet.create({
|
|
198
|
+
// Outdent by the rows' 8px inset (the ListItem/Timeline technique) so row text +
|
|
199
|
+
// the money column align with the section heading and the container edges;
|
|
200
|
+
// the pressable door's wash bleeds into the gutter instead of squeezing text.
|
|
201
|
+
ledger: { gap: 12, marginHorizontal: -8 },
|
|
202
|
+
group: { gap: 4 },
|
|
203
|
+
// EVERY line (group header, rows, the total) shares the 8px text inset, so
|
|
204
|
+
// labels and the money column sit on one edge whether a row peeks or not —
|
|
205
|
+
// the pressable door's wash simply fills the same padded box.
|
|
206
|
+
row: {
|
|
207
|
+
flexDirection: "row",
|
|
208
|
+
alignItems: "center",
|
|
209
|
+
gap: 10,
|
|
210
|
+
minHeight: 28,
|
|
211
|
+
paddingHorizontal: 8,
|
|
212
|
+
},
|
|
213
|
+
rowPress: {
|
|
214
|
+
borderRadius: 8,
|
|
215
|
+
},
|
|
216
|
+
rowHovered: { backgroundColor: colors.zinc[50] },
|
|
217
|
+
grow: { flexGrow: 1, flexShrink: 1 },
|
|
218
|
+
shrink: { flexShrink: 1 },
|
|
219
|
+
total: { gap: 6 },
|
|
220
|
+
});
|
package/src/locale.tsx
CHANGED
|
@@ -2,10 +2,10 @@ import { createContext, useContext, type ReactNode } from "react";
|
|
|
2
2
|
import { type PaginationLabels } from "./pagination";
|
|
3
3
|
import { type SortHeaderLabels } from "./sort_header";
|
|
4
4
|
import { type ConfidenceLabels } from "./confidence";
|
|
5
|
-
import { type FindingLabels } from "./finding";
|
|
6
5
|
import { type RemainderMeterLabels } from "./remainder_meter";
|
|
7
6
|
import { type DateRangeFilterFieldLabels } from "./date_range_filter_field";
|
|
8
7
|
import { type GalleryLabels } from "./file_preview_types";
|
|
8
|
+
import { type FindingLabels } from "./finding";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* The kit's localizable strings, one slice per string-bearing component. A
|
|
@@ -36,27 +36,40 @@ export interface LoticsLocale {
|
|
|
36
36
|
formField: { optional: string };
|
|
37
37
|
/** `Drawer`: the record prev/next + close controls (screen-reader names). */
|
|
38
38
|
drawer: { previous: string; next: string; close: string };
|
|
39
|
+
/** The `Inline*` editor family: the shared save-error line and the
|
|
40
|
+
* text-editor Save/Cancel tooltips. */
|
|
41
|
+
inline: { saveError: string; save: string; cancel: string };
|
|
42
|
+
/** `Ledger`: the screen-reader name of a peekable row. */
|
|
43
|
+
ledger: { rowDetails: (label: string) => string };
|
|
44
|
+
/** `SectionHeadingTitle`: the info-popover trigger's screen-reader name. */
|
|
45
|
+
sectionHeading: { info: string };
|
|
46
|
+
/** `Chip`: the ✕ default name when no `dismissTooltip` is given. */
|
|
47
|
+
chip: { remove: string };
|
|
48
|
+
/** `SuggestionChip`: the press target's default name and the ✕ tooltip. */
|
|
49
|
+
suggestionChip: { add: (label: string) => string; dismiss: string };
|
|
39
50
|
/** `Confidence`: the full level phrase ("High confidence" …). */
|
|
40
51
|
confidence: ConfidenceLabels;
|
|
41
52
|
/** `Finding`: the severity badge word ("Critical" …). */
|
|
42
|
-
finding: FindingLabels;
|
|
43
53
|
/** `RemainderMeter`: the applied / remaining / over / exact captions. */
|
|
44
54
|
remainderMeter: Required<RemainderMeterLabels>;
|
|
45
55
|
/** `ChangeReview` (+ `ChangeReviewActions`): the review-card chrome — the
|
|
46
56
|
* Keep/Drop verdicts, Undo, the Applied/Discarded tags, the kept-counter,
|
|
47
57
|
* the title, and the commit-bar Accept-all / Discard. */
|
|
58
|
+
finding: FindingLabels;
|
|
48
59
|
changeReview: {
|
|
49
60
|
title: string;
|
|
50
61
|
accept: string;
|
|
51
62
|
reject: string;
|
|
52
63
|
undo: string;
|
|
53
|
-
applied: string;
|
|
54
|
-
discarded: string;
|
|
55
64
|
keptCount: (kept: number, total: number) => string;
|
|
56
65
|
acceptAll: string;
|
|
57
66
|
discard: string;
|
|
58
67
|
apply: string;
|
|
59
|
-
|
|
68
|
+
recommended: string;
|
|
69
|
+
customValue: string;
|
|
70
|
+
opAdd: string;
|
|
71
|
+
opEdit: string;
|
|
72
|
+
opRemove: string;
|
|
60
73
|
};
|
|
61
74
|
/** `DateRangeFilterField` (and the `DateFilter` panel it wraps): presets,
|
|
62
75
|
* from/to, the footer Clear/Done, the trigger placeholder, and the time-field
|
|
@@ -88,26 +101,33 @@ export const en: LoticsLocale = {
|
|
|
88
101
|
filterChip: { clear: "Clear" },
|
|
89
102
|
formField: { optional: "Optional" },
|
|
90
103
|
drawer: { previous: "Previous record", next: "Next record", close: "Close" },
|
|
104
|
+
inline: { saveError: "Couldn't save. Try again.", save: "Save", cancel: "Cancel" },
|
|
105
|
+
ledger: { rowDetails: (label) => `${label} details` },
|
|
106
|
+
sectionHeading: { info: "About this data" },
|
|
107
|
+
chip: { remove: "Remove" },
|
|
108
|
+
suggestionChip: { add: (label) => `Add: ${label}`, dismiss: "Dismiss suggestion" },
|
|
91
109
|
confidence: { high: "High confidence", medium: "Medium confidence", low: "Low confidence" },
|
|
92
|
-
finding: { critical: "Critical", warning: "Warning", info: "Note", positive: "On track" },
|
|
93
110
|
remainderMeter: {
|
|
94
111
|
applied: (allocated, total) => `${allocated} of ${total} applied`,
|
|
95
112
|
remaining: (remainder) => `${remainder} unapplied`,
|
|
96
113
|
over: (amount) => `Over by ${amount}`,
|
|
97
114
|
exact: "Fully applied",
|
|
98
115
|
},
|
|
116
|
+
finding: { critical: "Critical", warning: "Warning", info: "Note", positive: "On track", difference: "Difference" },
|
|
99
117
|
changeReview: {
|
|
100
118
|
title: "Suggested edits",
|
|
101
119
|
accept: "Keep",
|
|
102
120
|
reject: "Drop",
|
|
103
121
|
undo: "Undo",
|
|
104
|
-
applied: "Applied",
|
|
105
|
-
discarded: "Discarded",
|
|
106
122
|
keptCount: (kept, total) => `${kept} of ${total} kept`,
|
|
107
|
-
acceptAll: "
|
|
123
|
+
acceptAll: "Keep all",
|
|
108
124
|
discard: "Discard",
|
|
109
125
|
apply: "Apply",
|
|
110
|
-
|
|
126
|
+
recommended: "AI pick",
|
|
127
|
+
customValue: "Type another value",
|
|
128
|
+
opAdd: "Add",
|
|
129
|
+
opEdit: "Edit",
|
|
130
|
+
opRemove: "Delete",
|
|
111
131
|
},
|
|
112
132
|
dateRange: {
|
|
113
133
|
year: "Year", month: "Month", day: "Day", hour: "Hour", minute: "Minute", dayPeriod: "AM/PM",
|
|
@@ -157,26 +177,33 @@ export const vi: LoticsLocale = {
|
|
|
157
177
|
filterChip: { clear: "Xóa" },
|
|
158
178
|
formField: { optional: "Tùy chọn" },
|
|
159
179
|
drawer: { previous: "Bản ghi trước", next: "Bản ghi sau", close: "Đóng" },
|
|
180
|
+
inline: { saveError: "Không lưu được. Thử lại.", save: "Lưu", cancel: "Hủy" },
|
|
181
|
+
ledger: { rowDetails: (label) => `Chi tiết ${label}` },
|
|
182
|
+
sectionHeading: { info: "Giải thích dữ liệu" },
|
|
183
|
+
chip: { remove: "Xóa" },
|
|
184
|
+
suggestionChip: { add: (label) => `Thêm: ${label}`, dismiss: "Bỏ gợi ý" },
|
|
160
185
|
confidence: { high: "Độ tin cậy cao", medium: "Độ tin cậy trung bình", low: "Độ tin cậy thấp" },
|
|
161
|
-
finding: { critical: "Nghiêm trọng", warning: "Cảnh báo", info: "Ghi chú", positive: "Đúng tiến độ" },
|
|
162
186
|
remainderMeter: {
|
|
163
187
|
applied: (allocated, total) => `Đã phân bổ ${allocated}/${total}`,
|
|
164
188
|
remaining: (remainder) => `Còn ${remainder}`,
|
|
165
189
|
over: (amount) => `Vượt ${amount}`,
|
|
166
190
|
exact: "Đã phân bổ đủ",
|
|
167
191
|
},
|
|
192
|
+
finding: { critical: "Nghiêm trọng", warning: "Cảnh báo", info: "Ghi chú", positive: "Đúng tiến độ", difference: "Chênh lệch" },
|
|
168
193
|
changeReview: {
|
|
169
194
|
title: "Đề xuất chỉnh sửa",
|
|
170
195
|
accept: "Giữ",
|
|
171
196
|
reject: "Bỏ",
|
|
172
197
|
undo: "Hoàn tác",
|
|
173
|
-
|
|
174
|
-
discarded: "Đã bỏ",
|
|
175
|
-
keptCount: (kept, total) => `Giữ ${kept}/${total}`,
|
|
198
|
+
keptCount: (kept, total) => `Đã giữ ${kept}/${total}`,
|
|
176
199
|
acceptAll: "Giữ tất cả",
|
|
177
|
-
discard: "
|
|
200
|
+
discard: "Bỏ hết",
|
|
178
201
|
apply: "Áp dụng",
|
|
179
|
-
|
|
202
|
+
recommended: "AI đề xuất",
|
|
203
|
+
customValue: "Nhập giá trị khác",
|
|
204
|
+
opAdd: "Thêm",
|
|
205
|
+
opEdit: "Sửa",
|
|
206
|
+
opRemove: "Xóa",
|
|
180
207
|
},
|
|
181
208
|
dateRange: {
|
|
182
209
|
year: "Năm", month: "Tháng", day: "Ngày", hour: "Giờ", minute: "Phút", dayPeriod: "SA/CH",
|
package/src/progress_bar.tsx
CHANGED
|
@@ -16,6 +16,11 @@ export interface ProgressBarProps {
|
|
|
16
16
|
format?: ProgressBarFormat;
|
|
17
17
|
color?: string;
|
|
18
18
|
completeColor?: string;
|
|
19
|
+
/** COMPACT: one row — the track (flex) with a plain sm tabular count beside
|
|
20
|
+
* it ("4/5", or "80%" under format="percentage"). For cells, headings, and
|
|
21
|
+
* peek triggers where the stacked title-over-track anatomy is too tall and
|
|
22
|
+
* a floating caption reads misaligned. `title` is ignored. */
|
|
23
|
+
compact?: boolean;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
/**
|
|
@@ -32,11 +37,29 @@ export function ProgressBar(props: ProgressBarProps) {
|
|
|
32
37
|
format = "percentage",
|
|
33
38
|
color = colors.blue["500"],
|
|
34
39
|
completeColor = colors.green["500"],
|
|
40
|
+
compact = false,
|
|
35
41
|
} = props;
|
|
36
42
|
|
|
37
|
-
const percentage = Math.min(100, Math.max(0, (value / max) * 100));
|
|
43
|
+
const percentage = max > 0 ? Math.min(100, Math.max(0, (value / max) * 100)) : 0;
|
|
38
44
|
const isComplete = percentage >= 100;
|
|
39
45
|
|
|
46
|
+
if (compact) {
|
|
47
|
+
const label =
|
|
48
|
+
format === "percentage" ? `${Math.round(percentage)}%` : `${value.toLocaleString("vi-VN")}/${max.toLocaleString("vi-VN")}`;
|
|
49
|
+
return (
|
|
50
|
+
<View style={styles.compactRow}>
|
|
51
|
+
<View style={[styles.track, styles.compactTrack]}>
|
|
52
|
+
<View style={[styles.fill, { width: `${percentage}%`, backgroundColor: isComplete ? completeColor : color }]} />
|
|
53
|
+
</View>
|
|
54
|
+
{format === "none" ? null : (
|
|
55
|
+
<Text size="sm" tabular color={isComplete ? "success" : "muted"}>
|
|
56
|
+
{label}
|
|
57
|
+
</Text>
|
|
58
|
+
)}
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
40
63
|
const caption =
|
|
41
64
|
format === "fraction"
|
|
42
65
|
? `${value.toLocaleString("vi-VN")} / ${max.toLocaleString("vi-VN")} · ${Math.round(percentage)}%`
|
|
@@ -80,6 +103,14 @@ const styles = StyleSheet.create({
|
|
|
80
103
|
container: {
|
|
81
104
|
gap: 6,
|
|
82
105
|
},
|
|
106
|
+
compactRow: {
|
|
107
|
+
flexDirection: "row",
|
|
108
|
+
alignItems: "center",
|
|
109
|
+
gap: 8,
|
|
110
|
+
},
|
|
111
|
+
compactTrack: {
|
|
112
|
+
flex: 1,
|
|
113
|
+
},
|
|
83
114
|
header: {
|
|
84
115
|
flexDirection: "row",
|
|
85
116
|
alignItems: "baseline",
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { StyleSheet, View } from "react-native";
|
|
3
|
+
import { Text } from "./text";
|
|
4
|
+
|
|
5
|
+
export interface RecordSummaryMetric {
|
|
6
|
+
/** Caption above the number ("Outstanding", "Total value"). */
|
|
7
|
+
label: string;
|
|
8
|
+
/** The headline figure — rendered lg semibold tabular. */
|
|
9
|
+
value: string;
|
|
10
|
+
/** Valence of the figure (and its note) — the summary's ONE permitted accent. */
|
|
11
|
+
tone?: "default" | "danger" | "warning" | "success";
|
|
12
|
+
/** One short qualifier under the number ("Overdue 25 days"). */
|
|
13
|
+
note?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RecordSummaryProps {
|
|
17
|
+
/** The record's id/name — xl semibold, tabular so ids align across records. */
|
|
18
|
+
title: string;
|
|
19
|
+
/** Heading level of the title (`role="header"` + aria-level). Default 1 —
|
|
20
|
+
* the record page's one h1; pass 2 when the band sits inside a surface
|
|
21
|
+
* that already owns the h1 (a drawer with its own titled header). */
|
|
22
|
+
level?: 1 | 2 | 3;
|
|
23
|
+
/** Identity qualifiers on the title line ("HCM → Hamburg · Export · FCL") —
|
|
24
|
+
* sm muted; wraps under the title when narrow. */
|
|
25
|
+
subtitle?: string;
|
|
26
|
+
/** A status chip (a `Badge`) or any small node right after the title. */
|
|
27
|
+
status?: ReactNode;
|
|
28
|
+
/** ONE headline number with valence — pinned to the summary's top right.
|
|
29
|
+
* More than one figure belongs in a KPI strip, not the record header. */
|
|
30
|
+
metric?: RecordSummaryMetric;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The identity band of a single record's detail screen or drawer: ONE row —
|
|
35
|
+
* title · subtitle · status chip, with an optional headline metric pinned
|
|
36
|
+
* right. It exists to stop hand-rolled record headers drifting: mixed type
|
|
37
|
+
* scales, several competing figures, color noise.
|
|
38
|
+
*
|
|
39
|
+
* Color discipline: the header stays neutral; `metric.tone` is the one accent.
|
|
40
|
+
* The record's FIELDS never live in the header — compose them as
|
|
41
|
+
* `DetailTable`s in the sections below (`SectionHeading` + `Divider` rhythm) —
|
|
42
|
+
* see `examples/tpl_record.tsx`.
|
|
43
|
+
*/
|
|
44
|
+
export function RecordSummary(props: RecordSummaryProps) {
|
|
45
|
+
const { title, subtitle, status, metric, level = 1 } = props;
|
|
46
|
+
return (
|
|
47
|
+
<View style={styles.identityRow}>
|
|
48
|
+
<View style={styles.identity}>
|
|
49
|
+
<View style={styles.titleRow}>
|
|
50
|
+
<Text size="xl" weight="semibold" tabular level={level}>
|
|
51
|
+
{title}
|
|
52
|
+
</Text>
|
|
53
|
+
{status ?? null}
|
|
54
|
+
</View>
|
|
55
|
+
{subtitle ? (
|
|
56
|
+
<Text size="sm" color="muted">
|
|
57
|
+
{subtitle}
|
|
58
|
+
</Text>
|
|
59
|
+
) : null}
|
|
60
|
+
</View>
|
|
61
|
+
{metric ? (
|
|
62
|
+
<View style={styles.metric}>
|
|
63
|
+
<Text size="xs" color="muted" weight="medium" align="right">
|
|
64
|
+
{metric.label}
|
|
65
|
+
</Text>
|
|
66
|
+
<Text size="lg" weight="semibold" tabular align="right" color={metric.tone === "default" ? undefined : metric.tone}>
|
|
67
|
+
{metric.value}
|
|
68
|
+
</Text>
|
|
69
|
+
{metric.note ? (
|
|
70
|
+
<Text size="xs" align="right" weight={metric.tone && metric.tone !== "default" ? "medium" : "regular"} color={metric.tone && metric.tone !== "default" ? metric.tone : "muted"}>
|
|
71
|
+
{metric.note}
|
|
72
|
+
</Text>
|
|
73
|
+
) : null}
|
|
74
|
+
</View>
|
|
75
|
+
) : null}
|
|
76
|
+
</View>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const styles = StyleSheet.create({
|
|
81
|
+
identityRow: {
|
|
82
|
+
flexDirection: "row",
|
|
83
|
+
alignItems: "flex-start",
|
|
84
|
+
gap: 16,
|
|
85
|
+
},
|
|
86
|
+
identity: {
|
|
87
|
+
flex: 1,
|
|
88
|
+
gap: 2,
|
|
89
|
+
},
|
|
90
|
+
titleRow: {
|
|
91
|
+
flexDirection: "row",
|
|
92
|
+
alignItems: "center",
|
|
93
|
+
flexWrap: "wrap",
|
|
94
|
+
columnGap: 10,
|
|
95
|
+
rowGap: 4,
|
|
96
|
+
},
|
|
97
|
+
metric: {
|
|
98
|
+
alignItems: "flex-end",
|
|
99
|
+
gap: 1,
|
|
100
|
+
},
|
|
101
|
+
});
|
package/src/section_heading.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { View, type StyleProp, type ViewStyle } from "react-native";
|
|
|
2
2
|
import { Text, type HeadingLevel } from "./text";
|
|
3
3
|
import { Icon, type IconName } from "./icon";
|
|
4
4
|
import { InfoPopover } from "./info_popover";
|
|
5
|
+
import { useLoticsLocale } from "./locale";
|
|
5
6
|
|
|
6
7
|
// The card-less section header — the bare-canvas sibling of `CardHeader`, built
|
|
7
8
|
// the same compound way. A title (+ optional leading icon / description) on one
|
|
@@ -36,7 +37,12 @@ export interface SectionProps {
|
|
|
36
37
|
* </Section>
|
|
37
38
|
*/
|
|
38
39
|
export function Section(props: SectionProps) {
|
|
39
|
-
|
|
40
|
+
// gap 12 spaces the heading from its body. Space BETWEEN sections belongs to
|
|
41
|
+
// the page column: flat record/form pages use `gap: 32` on the content column
|
|
42
|
+
// with a bare `Divider` BETWEEN sections (never under the heading — the
|
|
43
|
+
// heading belongs to its content; the hairline separates it from the LAST
|
|
44
|
+
// section).
|
|
45
|
+
return <View style={[{ gap: 12 }, props.style]}>{props.children}</View>;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
export interface SectionHeadingProps {
|
|
@@ -58,26 +64,28 @@ export interface SectionHeadingTitleProps {
|
|
|
58
64
|
icon?: IconName;
|
|
59
65
|
/** Heading rank. Defaults to 2 — typical for page-level section titles. */
|
|
60
66
|
level?: HeadingLevel;
|
|
61
|
-
/** Title weight. Defaults to `
|
|
62
|
-
*
|
|
63
|
-
*
|
|
67
|
+
/** Title weight. Defaults to `semibold` — a section title must separate from
|
|
68
|
+
* the body rows at a glance (medium blends in; it remains as an opt-down for
|
|
69
|
+
* a surface where the heading competes with a stronger band above it). */
|
|
64
70
|
weight?: "medium" | "semibold";
|
|
65
71
|
/** An ⓘ popover after the title — a short "what this is / where it came from"
|
|
66
72
|
* gloss, mirroring `CardHeaderTitle`'s `info`. */
|
|
67
73
|
info?: string;
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
/** The title block —
|
|
76
|
+
/** The title block — lg semibold (a section must announce itself; md blends
|
|
77
|
+
* into control labels) + an optional muted `description` line. Grows to push
|
|
78
|
+
* any sibling actions to the right edge. */
|
|
71
79
|
export function SectionHeadingTitle(props: SectionHeadingTitleProps) {
|
|
72
|
-
const { children, description, icon, level = 2, weight = "
|
|
80
|
+
const { children, description, icon, level = 2, weight = "semibold", info } = props;
|
|
73
81
|
return (
|
|
74
82
|
<View style={{ flex: 1, gap: 2 }}>
|
|
75
83
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
|
76
84
|
{icon ? <Icon name={icon} size={18} /> : null}
|
|
77
|
-
<Text level={level} weight={weight} size="
|
|
85
|
+
<Text level={level} weight={weight} size="lg">
|
|
78
86
|
{children}
|
|
79
87
|
</Text>
|
|
80
|
-
{info ? <InfoPopover text={info} accessibilityLabel=
|
|
88
|
+
{info ? <InfoPopover text={info} accessibilityLabel={useLoticsLocale().sectionHeading.info} /> : null}
|
|
81
89
|
</View>
|
|
82
90
|
{description ? (
|
|
83
91
|
<Text color="zinc-500" size="sm">
|
package/src/sources.tsx
CHANGED
|
@@ -16,8 +16,9 @@ export interface SourceRef {
|
|
|
16
16
|
|
|
17
17
|
export interface SourcesProps {
|
|
18
18
|
sources: SourceRef[];
|
|
19
|
-
/** Eyebrow above the chips. Default "Sources"
|
|
20
|
-
|
|
19
|
+
/** Eyebrow above the chips. Default "Sources"; pass `null` for chips only —
|
|
20
|
+
* the form used INSIDE a `Change` (a review card's own header/body). */
|
|
21
|
+
label?: string | null;
|
|
21
22
|
/** Pass to make each chip openable (jump to the record / document / page). The
|
|
22
23
|
* chips then show a hover state + an open glyph; the host does the navigation. */
|
|
23
24
|
onOpen?: (source: SourceRef) => void;
|
|
@@ -45,9 +46,11 @@ export function Sources(props: SourcesProps) {
|
|
|
45
46
|
if (props.sources.length === 0) return null;
|
|
46
47
|
return (
|
|
47
48
|
<View style={{ gap: 8 }}>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
{props.label === null ? null : (
|
|
50
|
+
<Text size="xs" color="muted" weight="medium">
|
|
51
|
+
{props.label ?? "Sources"}
|
|
52
|
+
</Text>
|
|
53
|
+
)}
|
|
51
54
|
<View style={styles.row}>
|
|
52
55
|
{props.sources.map((s) =>
|
|
53
56
|
props.onOpen ? (
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { StyleSheet, View } from "react-native";
|
|
2
|
+
import { Chip } from "./chip";
|
|
3
|
+
import { colors } from "./colors";
|
|
4
|
+
import { Icon } from "./icon";
|
|
5
|
+
import { useLoticsLocale } from "./locale";
|
|
6
|
+
import { Text } from "./text";
|
|
7
|
+
|
|
8
|
+
export interface SuggestionChipProps {
|
|
9
|
+
/** The suggested item ("Verify the customer's tax ID"). */
|
|
10
|
+
label: string;
|
|
11
|
+
/** Materialize the suggestion. */
|
|
12
|
+
onAdd: () => void;
|
|
13
|
+
/** Refuse the suggestion (the ✕). Omit for take-it-or-leave-it pills. */
|
|
14
|
+
onDismiss?: () => void;
|
|
15
|
+
/** Accessible name of the press target; default "Add: <label>". */
|
|
16
|
+
accessibilityLabel?: string;
|
|
17
|
+
/** The ✕'s name; defaults to the locale's "Dismiss suggestion". */
|
|
18
|
+
dismissLabel?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A dismissible SUGGESTION pill — an item the record could have but doesn't
|
|
23
|
+
* yet (a common task, an expected line): a `Chip` whose press MATERIALIZES it
|
|
24
|
+
* and whose ✕ refuses it. A pill can never be mistaken for the real row it
|
|
25
|
+
* would become; suggestions never count in any total. Filter out labels the
|
|
26
|
+
* list already holds before rendering.
|
|
27
|
+
*/
|
|
28
|
+
export function SuggestionChip(props: SuggestionChipProps) {
|
|
29
|
+
const { label, onAdd, onDismiss, accessibilityLabel, dismissLabel } = props;
|
|
30
|
+
const labels = useLoticsLocale().suggestionChip;
|
|
31
|
+
return (
|
|
32
|
+
<Chip onPress={onAdd} accessibilityLabel={accessibilityLabel ?? labels.add(label)} onDismiss={onDismiss} dismissTooltip={dismissLabel ?? labels.dismiss}>
|
|
33
|
+
<View style={styles.content}>
|
|
34
|
+
<Icon name="plus" size={15} color={colors.zinc[500]} />
|
|
35
|
+
<Text size="sm">{label}</Text>
|
|
36
|
+
</View>
|
|
37
|
+
</Chip>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const styles = StyleSheet.create({
|
|
42
|
+
content: {
|
|
43
|
+
flexDirection: "row",
|
|
44
|
+
alignItems: "center",
|
|
45
|
+
gap: 6,
|
|
46
|
+
},
|
|
47
|
+
});
|