@lotics/ui 5.2.0 → 5.4.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 +13 -7
- package/examples/tpl_assistant.tsx +36 -24
- package/examples/tpl_ratedesk.tsx +23 -18
- package/package.json +1 -1
- package/src/change_review.tsx +128 -80
- package/src/remainder_meter.tsx +29 -3
package/AGENTS.md
CHANGED
|
@@ -13,8 +13,8 @@ typeahead, async search, virtualization, and a11y the primitive already ships.
|
|
|
13
13
|
- RN-Web only: `View`/`ScrollView` from `react-native`, the `Text` primitive (no raw
|
|
14
14
|
`div`/`span`, no raw `fontSize`/`fontWeight`); styles are RN objects.
|
|
15
15
|
- i18n: most components are string-free; the few that emit their own user-facing strings take
|
|
16
|
-
a `labels` prop (`DateRangeFilterField`, `Pagination`, `SortHeader`/`Table.sortLabels
|
|
17
|
-
localize there.
|
|
16
|
+
a `labels` prop (`DateRangeFilterField`, `Pagination`, `SortHeader`/`Table.sortLabels`,
|
|
17
|
+
`RemainderMeter`) — localize there.
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -83,8 +83,9 @@ Pick by capability, not by name. (→ the source file for the API.)
|
|
|
83
83
|
`AgentRun` (the live streaming work feed) + `AgentProgress` (its compact, floating, expandable
|
|
84
84
|
form — a composer's "working" state), `ReviewCard` (THE single-proposal review card — compositional
|
|
85
85
|
body via `children`: a `RecordFields` record (header-first) or a `MatchSides` pairing (body-first))
|
|
86
|
-
+ `Confidence`; `ChangeReview` (the BATCH review
|
|
87
|
-
`renderItem`, with `ChangeDiff` for the before→after body),
|
|
86
|
+
+ `Confidence`; `ChangeReview` (the BATCH review list — whole-set OR 1-by-1; compositional via
|
|
87
|
+
`renderItem`, with `ChangeDiff` for the before→after body) + `ChangeReviewActions` (its commit bar,
|
|
88
|
+
pinned by the host in a `DialogFooter`/`DrawerFooter` or inline),
|
|
88
89
|
`Clarify` (the agent asks back — selectable `ChoiceList` options),
|
|
89
90
|
`Sources` (provenance chips for AI output). For STRUCTURED AI output the review
|
|
90
91
|
surfaces above don't cover: `SpecList` (the "exact breakdown" — a labeled-value spec sheet, the
|
|
@@ -281,8 +282,13 @@ Compose the surfaces as a loop, and reach for the right one by job:
|
|
|
281
282
|
→ `tpl_match`). Stack several (rank by confidence) for a shortlist or a document's records.
|
|
282
283
|
- **`ChangeReview`** — THE batch engine for a SET you decide each then commit together. `renderItem`
|
|
283
284
|
draws each proposal (a `ChangeDiff` before→after body, a record, an editable form), 1-by-1
|
|
284
|
-
Keep/Drop collapses each
|
|
285
|
-
|
|
285
|
+
Keep/Drop collapses each (e.g. ingested rate lines — `tpl_ratedesk`); whole-set mode reviews the
|
|
286
|
+
lot. It is the LIST only — the commit bar (Accept all / Apply / Discard) is the SEPARATE
|
|
287
|
+
**`ChangeReviewActions`**, which the host places where commit bars belong (a `DialogFooter` /
|
|
288
|
+
`DrawerFooter`, or inline under the list). Don't bundle a commit into a scroll body — like every
|
|
289
|
+
other footer in the kit it's the container's to pin (`tpl_ratedesk` puts it in the `DialogFooter`).
|
|
290
|
+
`ChangeReviewActions` reads the same `items`/`statusOf`, so it owns Accept-all + the kept count —
|
|
291
|
+
never re-derive them in the host.
|
|
286
292
|
The body atoms (`RecordFields`, `MatchSides`, `ChangeDiff`) are pure presentation — usable inside
|
|
287
293
|
either engine OR standalone (a live-edit field card). The decide / accept-all / apply mechanics are
|
|
288
294
|
never re-implemented per surface.
|
|
@@ -590,7 +596,7 @@ spacer · stack · section_card · page_header · page_content · calendar (cale
|
|
|
590
596
|
comments_thread · agent_run (live streaming work feed) · agent_progress (its compact floating
|
|
591
597
|
expandable form — a composer's working state) · composer (Composer — the adaptive command/chat
|
|
592
598
|
composer: a compact pill that expands for long text + attachments) · review_card (ReviewCard — THE single-proposal review card; compositional body via children + a generic actions footer; header-first with a title/eyebrow, body-first without) · confidence (calibrated high/med/low) ·
|
|
593
|
-
record_fields (RecordFields — the editable extracted-record BODY for a ReviewCard, or a standalone live-edit field card) · change_review (ChangeReview — the BATCH review
|
|
599
|
+
record_fields (RecordFields — the editable extracted-record BODY for a ReviewCard, or a standalone live-edit field card) · change_review (ChangeReview — the BATCH review LIST, whole-set OR 1-by-1; compositional `renderItem` body + the `ChangeDiff` before→after helper, `labels` localizes Undo/Applied/Discarded/counter — PLUS `ChangeReviewActions`, the separate commit bar the host pins in a `DialogFooter`/`DrawerFooter` or inline so it never scrolls away) ·
|
|
594
600
|
clarify (the agent asks back, via ChoiceList) · choice_list (ChoiceList — selectable answer options,
|
|
595
601
|
the agent's quick-reply surface) · sources (provenance chips, per-kind glyphs) · spec_list (SpecList — the exact
|
|
596
602
|
labeled-value breakdown, the panel of an answer) · match_sides (MatchSides — the two-sided pairing BODY for a body-first ReviewCard reconciliation; composable side slots + the MatchSideText default) ·
|
|
@@ -4,7 +4,7 @@ import { colors } from "@lotics/ui/colors";
|
|
|
4
4
|
import { Text } from "@lotics/ui/text";
|
|
5
5
|
import { DotsIndicator } from "@lotics/ui/dots_indicator";
|
|
6
6
|
import { Composer } from "@lotics/ui/composer";
|
|
7
|
-
import { ChangeReview, ChangeDiff, type ChangeReviewItemStatus } from "@lotics/ui/change_review";
|
|
7
|
+
import { ChangeReview, ChangeReviewActions, ChangeDiff, type ChangeReviewItemStatus } from "@lotics/ui/change_review";
|
|
8
8
|
|
|
9
9
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
10
10
|
// Template · AI assistant — a CHAT workflow, a different shape from the design
|
|
@@ -115,30 +115,42 @@ export function TplAssistant() {
|
|
|
115
115
|
) : null}
|
|
116
116
|
{m.text ? <Text size="sm">{m.text}</Text> : null}
|
|
117
117
|
{m.changes ? (
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
<
|
|
131
|
-
|
|
118
|
+
<View style={{ gap: 14 }}>
|
|
119
|
+
<ChangeReview
|
|
120
|
+
items={m.changes}
|
|
121
|
+
getKey={(c, i) => `${m.id}:${i}`}
|
|
122
|
+
statusOf={(_, i) => decisions[`${m.id}:${i}`] ?? "pending"}
|
|
123
|
+
renderItem={(c) => (
|
|
124
|
+
<>
|
|
125
|
+
<Text size="xs" color="muted" weight="medium">{c.label}</Text>
|
|
126
|
+
<ChangeDiff before={c.before} after={c.after} />
|
|
127
|
+
</>
|
|
128
|
+
)}
|
|
129
|
+
renderSummary={(c, kept) => (
|
|
130
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
|
131
|
+
<Text size="sm" weight="medium" numberOfLines={1}>{c.label}</Text>
|
|
132
|
+
<Text size="sm" numberOfLines={1} color={kept ? "default" : "muted"} style={[{ flex: 1 }, kept ? undefined : { textDecorationLine: "line-through" }]}>{kept ? c.after : (c.before ?? c.after)}</Text>
|
|
133
|
+
</View>
|
|
134
|
+
)}
|
|
135
|
+
status={m.applied ? "applied" : "open"}
|
|
136
|
+
onAcceptItem={active ? (i) => decide(m.id, i, "accepted") : undefined}
|
|
137
|
+
onRejectItem={active ? (i) => decide(m.id, i, "rejected") : undefined}
|
|
138
|
+
onUndoItem={active ? (i) => setDecisions((d) => { const n = { ...d }; delete n[`${m.id}:${i}`]; return n; }) : undefined}
|
|
139
|
+
/>
|
|
140
|
+
{/* Inline (no dialog) — the commit bar sits under the list behind a divider. */}
|
|
141
|
+
{active && !m.applied ? (
|
|
142
|
+
<View style={{ borderTopWidth: 1, borderTopColor: colors.zinc[200], paddingTop: 14, flexDirection: "row" }}>
|
|
143
|
+
<ChangeReviewActions
|
|
144
|
+
items={m.changes}
|
|
145
|
+
statusOf={(_, i) => decisions[`${m.id}:${i}`] ?? "pending"}
|
|
146
|
+
applyLabel="Apply accepted"
|
|
147
|
+
onAcceptItem={(i) => decide(m.id, i, "accepted")}
|
|
148
|
+
onApply={() => applyMsg(m.id)}
|
|
149
|
+
onDiscard={() => setMessages((ms) => ms.map((x) => (x.id === m.id ? { ...x, applied: true } : x)))}
|
|
150
|
+
/>
|
|
132
151
|
</View>
|
|
133
|
-
)}
|
|
134
|
-
|
|
135
|
-
applyLabel="Apply accepted"
|
|
136
|
-
onAcceptItem={active ? (i) => decide(m.id, i, "accepted") : undefined}
|
|
137
|
-
onRejectItem={active ? (i) => decide(m.id, i, "rejected") : undefined}
|
|
138
|
-
onUndoItem={active ? (i) => setDecisions((d) => { const n = { ...d }; delete n[`${m.id}:${i}`]; return n; }) : undefined}
|
|
139
|
-
onApply={active ? () => applyMsg(m.id) : undefined}
|
|
140
|
-
onDiscard={active ? () => setMessages((ms) => ms.map((x) => (x.id === m.id ? { ...x, applied: true } : x))) : undefined}
|
|
141
|
-
/>
|
|
152
|
+
) : null}
|
|
153
|
+
</View>
|
|
142
154
|
) : null}
|
|
143
155
|
</View>
|
|
144
156
|
</View>
|
|
@@ -17,7 +17,7 @@ import { AgentRun, type AgentRunStep } from "@lotics/ui/agent_run";
|
|
|
17
17
|
import { Table, TableRow, TableCell, type TableColumn } from "@lotics/ui/table";
|
|
18
18
|
import { InlineNumberInput } from "@lotics/ui/inline_number_input";
|
|
19
19
|
import { CompletionState } from "@lotics/ui/completion_state";
|
|
20
|
-
import { ChangeReview, type ChangeReviewItemStatus } from "@lotics/ui/change_review";
|
|
20
|
+
import { ChangeReview, ChangeReviewActions, type ChangeReviewItemStatus } from "@lotics/ui/change_review";
|
|
21
21
|
import { Dialog, DialogHeader, DialogHeaderTitle, DialogScrollArea, DialogFooter } from "@lotics/ui/dialog";
|
|
22
22
|
import { formatDate } from "@lotics/ui/format_date";
|
|
23
23
|
import { colors, solid, type ColorName } from "@lotics/ui/colors";
|
|
@@ -254,12 +254,9 @@ export function TplRatedesk() {
|
|
|
254
254
|
<Text size="xs" color="muted" numberOfLines={1} tabular style={[{ flex: 1 }, kept ? undefined : { textDecorationLine: "line-through" }]}>{l.carrier} · {l.unit} · sell {usd(l.sell)}</Text>
|
|
255
255
|
</View>
|
|
256
256
|
)}
|
|
257
|
-
applyLabel={`Apply ${lines.filter((l) => decisions[l.id] === "accepted").length} kept`}
|
|
258
257
|
onAcceptItem={(i) => decide(lines[i].id, "accepted")}
|
|
259
258
|
onRejectItem={(i) => decide(lines[i].id, "rejected")}
|
|
260
259
|
onUndoItem={(i) => decide(lines[i].id, null)}
|
|
261
|
-
onApply={applyAccepted}
|
|
262
|
-
onDiscard={reset}
|
|
263
260
|
/>
|
|
264
261
|
) : null}
|
|
265
262
|
|
|
@@ -268,21 +265,29 @@ export function TplRatedesk() {
|
|
|
268
265
|
) : null}
|
|
269
266
|
</DialogScrollArea>
|
|
270
267
|
|
|
271
|
-
{/* idle
|
|
272
|
-
|
|
273
|
-
|
|
268
|
+
{/* idle / review / done carry footer actions; reading just streams. The
|
|
269
|
+
review commit bar pins HERE via ChangeReviewActions — it never scrolls
|
|
270
|
+
away inside the list. */}
|
|
271
|
+
{phase === "idle" ? (
|
|
274
272
|
<DialogFooter>
|
|
275
|
-
{
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
273
|
+
{files.length > 0 ? <Button title="Clear" color="muted" shape="rounded" onPress={() => setFiles([])} /> : null}
|
|
274
|
+
<Button title={files.length ? `Read ${files.length} sheets` : "Read sheets"} color="primary" shape="rounded" disabled={!files.length} onPress={() => { setRevealed(0); setPhase("reading"); }} />
|
|
275
|
+
</DialogFooter>
|
|
276
|
+
) : phase === "review" ? (
|
|
277
|
+
<DialogFooter>
|
|
278
|
+
<ChangeReviewActions
|
|
279
|
+
items={lines}
|
|
280
|
+
statusOf={(l) => decisions[l.id] ?? "pending"}
|
|
281
|
+
onAcceptItem={(i) => decide(lines[i].id, "accepted")}
|
|
282
|
+
onApply={applyAccepted}
|
|
283
|
+
onDiscard={reset}
|
|
284
|
+
applyLabel={`Apply ${lines.filter((l) => decisions[l.id] === "accepted").length} kept`}
|
|
285
|
+
/>
|
|
286
|
+
</DialogFooter>
|
|
287
|
+
) : phase === "done" ? (
|
|
288
|
+
<DialogFooter>
|
|
289
|
+
<Button title="Import more" color="secondary" shape="rounded" onPress={reset} />
|
|
290
|
+
<Button title="Done" color="primary" shape="rounded" onPress={closeImport} />
|
|
286
291
|
</DialogFooter>
|
|
287
292
|
) : null}
|
|
288
293
|
</Dialog>
|
package/package.json
CHANGED
package/src/change_review.tsx
CHANGED
|
@@ -32,22 +32,45 @@ export interface ChangeReviewProps<T> {
|
|
|
32
32
|
summary?: string;
|
|
33
33
|
/** Card heading. Default "Suggested edits". */
|
|
34
34
|
title?: string;
|
|
35
|
-
/** Providing either flips
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
/** Providing either flips into 1-BY-1 mode: each proposal is its own card with
|
|
36
|
+
* Keep / Drop; deciding COLLAPSES it to a preview the host can `onUndoItem`.
|
|
37
|
+
* Omit both for whole-set (the list, reviewed together). The commit bar
|
|
38
|
+
* (Accept all / Apply / Discard) is NOT here — it's `ChangeReviewActions`,
|
|
39
|
+
* which the host places in a `DialogFooter` / `DrawerFooter` / inline. */
|
|
39
40
|
onAcceptItem?: (index: number) => void;
|
|
40
41
|
onRejectItem?: (index: number) => void;
|
|
41
42
|
/** Revert a decided item back to pending (the Undo on a collapsed card). */
|
|
42
43
|
onUndoItem?: (index: number) => void;
|
|
43
|
-
onApply?: () => void;
|
|
44
|
-
onDiscard?: () => void;
|
|
45
|
-
applyLabel?: string;
|
|
46
44
|
/** 1-by-1 per-item action labels. Default "Keep" / "Drop". */
|
|
47
45
|
acceptLabel?: string;
|
|
48
46
|
rejectLabel?: string;
|
|
47
|
+
/** Once resolved the list settles to a quiet outcome line. */
|
|
48
|
+
status?: "open" | "applied" | "discarded";
|
|
49
|
+
/** Localize the chrome the LIST emits itself (Keep/Drop take their own props;
|
|
50
|
+
* the commit-bar labels live on `ChangeReviewActions`). */
|
|
51
|
+
labels?: {
|
|
52
|
+
undo?: string;
|
|
53
|
+
applied?: string;
|
|
54
|
+
discarded?: string;
|
|
55
|
+
/** The "N of M kept" counter — default `${kept} of ${total} kept`. */
|
|
56
|
+
keptCount?: (kept: number, total: number) => string;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ChangeReviewActionsProps<T> {
|
|
61
|
+
/** The same items + per-item status the list reads, so Accept-all knows which
|
|
62
|
+
* are pending and Apply knows the kept count. */
|
|
63
|
+
items: T[];
|
|
64
|
+
statusOf?: (item: T, index: number) => ChangeReviewItemStatus;
|
|
65
|
+
/** Presence enables 1-by-1: shows Accept-all and disables Apply at 0 kept. Omit
|
|
66
|
+
* for whole-set (no Accept-all; Apply commits the lot). */
|
|
67
|
+
onAcceptItem?: (index: number) => void;
|
|
68
|
+
onApply?: () => void;
|
|
69
|
+
onDiscard?: () => void;
|
|
70
|
+
applyLabel?: string;
|
|
49
71
|
acceptAllLabel?: string;
|
|
50
|
-
|
|
72
|
+
discardLabel?: string;
|
|
73
|
+
/** Self-hides unless open — the host can always render it. */
|
|
51
74
|
status?: "open" | "applied" | "discarded";
|
|
52
75
|
}
|
|
53
76
|
|
|
@@ -82,13 +105,15 @@ export function ChangeDiff({ before, after }: { before?: string; after: string }
|
|
|
82
105
|
|
|
83
106
|
/**
|
|
84
107
|
* An agent-proposed set of changes, reviewed before it lands — never
|
|
85
|
-
* auto-applied.
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
108
|
+
* auto-applied. This is the LIST half: it owns the review MECHANICS (per-item
|
|
109
|
+
* decide + collapse, the kept counter), `renderItem` owns how each proposal
|
|
110
|
+
* looks. The COMMIT BAR is `ChangeReviewActions` — a separate piece the host
|
|
111
|
+
* places where commit bars belong (a `DialogFooter`, a `DrawerFooter`, or
|
|
112
|
+
* inline under the list), so the commit never scrolls away inside a dialog.
|
|
113
|
+
* Two modes. Whole-set: the list, reviewed together. 1-BY-1 (when the host
|
|
114
|
+
* passes `onAcceptItem`/`onRejectItem`): each proposal is its own card with
|
|
115
|
+
* Keep / Drop; deciding COLLAPSES it to a preview with Undo. Where a
|
|
116
|
+
* `ReviewCard` reviews a SINGLE proposal, this is the BATCH engine.
|
|
92
117
|
*/
|
|
93
118
|
export function ChangeReview<T>(props: ChangeReviewProps<T>) {
|
|
94
119
|
const status = props.status ?? "open";
|
|
@@ -97,11 +122,13 @@ export function ChangeReview<T>(props: ChangeReviewProps<T>) {
|
|
|
97
122
|
const statusFor = (item: T, i: number) => props.statusOf?.(item, i) ?? "pending";
|
|
98
123
|
const keptCount = props.items.filter((it, i) => statusFor(it, i) === "accepted").length;
|
|
99
124
|
const title = props.title ?? "Suggested edits";
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
125
|
+
const L = {
|
|
126
|
+
undo: "Undo",
|
|
127
|
+
applied: "Applied",
|
|
128
|
+
discarded: "Discarded",
|
|
129
|
+
keptCount: (kept: number, total: number) => `${kept} of ${total} kept`,
|
|
130
|
+
...props.labels,
|
|
131
|
+
};
|
|
105
132
|
|
|
106
133
|
const summaryNote = props.summary ? (
|
|
107
134
|
<View style={styles.note}>
|
|
@@ -111,6 +138,13 @@ export function ChangeReview<T>(props: ChangeReviewProps<T>) {
|
|
|
111
138
|
</View>
|
|
112
139
|
) : null;
|
|
113
140
|
|
|
141
|
+
const resolvedLine =
|
|
142
|
+
status !== "open" ? (
|
|
143
|
+
<Text size="xs" color="muted" weight="medium">
|
|
144
|
+
{status === "applied" ? L.applied : L.discarded}
|
|
145
|
+
</Text>
|
|
146
|
+
) : null;
|
|
147
|
+
|
|
114
148
|
// ── 1-BY-1: each proposal a bordered card; deciding collapses it to a quiet,
|
|
115
149
|
// lighter row — visual weight tracks whether it still needs you ──────────
|
|
116
150
|
if (perItem) {
|
|
@@ -122,68 +156,55 @@ export function ChangeReview<T>(props: ChangeReviewProps<T>) {
|
|
|
122
156
|
</Text>
|
|
123
157
|
{status === "open" ? (
|
|
124
158
|
<Text size="xs" color="muted" tabular>
|
|
125
|
-
{keptCount
|
|
159
|
+
{L.keptCount(keptCount, total)}
|
|
126
160
|
</Text>
|
|
127
161
|
) : null}
|
|
128
162
|
</View>
|
|
129
163
|
{summaryNote}
|
|
130
164
|
|
|
131
165
|
{status !== "open" ? (
|
|
132
|
-
|
|
133
|
-
{status === "applied" ? "Applied" : "Discarded"}
|
|
134
|
-
</Text>
|
|
166
|
+
resolvedLine
|
|
135
167
|
) : (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (st === "pending") {
|
|
141
|
-
return (
|
|
142
|
-
<View key={props.getKey(item, i)} style={styles.itemCard}>
|
|
143
|
-
{props.renderItem(item, i)}
|
|
144
|
-
<View style={styles.itemActions}>
|
|
145
|
-
<Button title={props.rejectLabel ?? "Drop"} color="muted" shape="rounded" onPress={() => props.onRejectItem?.(i)} />
|
|
146
|
-
<Button title={props.acceptLabel ?? "Keep"} color="secondary" shape="rounded" onPress={() => props.onAcceptItem?.(i)} />
|
|
147
|
-
</View>
|
|
148
|
-
</View>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
const kept = st === "accepted";
|
|
168
|
+
<View style={styles.list}>
|
|
169
|
+
{props.items.map((item, i) => {
|
|
170
|
+
const st = statusFor(item, i);
|
|
171
|
+
if (st === "pending") {
|
|
152
172
|
return (
|
|
153
|
-
<View key={props.getKey(item, i)} style={
|
|
154
|
-
|
|
155
|
-
<View style={
|
|
156
|
-
{props.
|
|
157
|
-
|
|
158
|
-
) : (
|
|
159
|
-
<Text size="sm" color={kept ? "default" : "muted"} numberOfLines={1} style={kept ? null : styles.strike}>
|
|
160
|
-
{props.getTitle?.(item, i) ?? ""}
|
|
161
|
-
</Text>
|
|
162
|
-
)}
|
|
173
|
+
<View key={props.getKey(item, i)} style={styles.itemCard}>
|
|
174
|
+
{props.renderItem(item, i)}
|
|
175
|
+
<View style={styles.itemActions}>
|
|
176
|
+
<Button title={props.rejectLabel ?? "Drop"} color="muted" shape="rounded" onPress={() => props.onRejectItem?.(i)} />
|
|
177
|
+
<Button title={props.acceptLabel ?? "Keep"} color="secondary" shape="rounded" onPress={() => props.onAcceptItem?.(i)} />
|
|
163
178
|
</View>
|
|
164
|
-
{props.onUndoItem ? (
|
|
165
|
-
<Button title="Undo" color="muted" shape="rounded" onPress={() => props.onUndoItem?.(i)} />
|
|
166
|
-
) : null}
|
|
167
179
|
</View>
|
|
168
180
|
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
}
|
|
182
|
+
const kept = st === "accepted";
|
|
183
|
+
return (
|
|
184
|
+
<View key={props.getKey(item, i)} style={[styles.decidedRow, kept ? null : styles.decidedDrop]}>
|
|
185
|
+
<Icon name={kept ? "check" : "x"} size={15} color={kept ? colors.emerald[600] : colors.zinc[400]} />
|
|
186
|
+
<View style={{ flex: 1 }}>
|
|
187
|
+
{props.renderSummary ? (
|
|
188
|
+
props.renderSummary(item, kept, i)
|
|
189
|
+
) : (
|
|
190
|
+
<Text size="sm" color={kept ? "default" : "muted"} numberOfLines={1} style={kept ? null : styles.strike}>
|
|
191
|
+
{props.getTitle?.(item, i) ?? ""}
|
|
192
|
+
</Text>
|
|
193
|
+
)}
|
|
194
|
+
</View>
|
|
195
|
+
{props.onUndoItem ? (
|
|
196
|
+
<Button title={L.undo} color="muted" shape="rounded" onPress={() => props.onUndoItem?.(i)} />
|
|
197
|
+
) : null}
|
|
198
|
+
</View>
|
|
199
|
+
);
|
|
200
|
+
})}
|
|
201
|
+
</View>
|
|
181
202
|
)}
|
|
182
203
|
</View>
|
|
183
204
|
);
|
|
184
205
|
}
|
|
185
206
|
|
|
186
|
-
// ── WHOLE-SET: one bordered card, review the lot
|
|
207
|
+
// ── WHOLE-SET: one bordered card, review the lot together ──────────────────
|
|
187
208
|
return (
|
|
188
209
|
<View style={[reviewCardStyle, status !== "open" ? reviewResolvedStyle : null]}>
|
|
189
210
|
<Text size="xs" color="muted" weight="medium">
|
|
@@ -197,19 +218,43 @@ export function ChangeReview<T>(props: ChangeReviewProps<T>) {
|
|
|
197
218
|
</View>
|
|
198
219
|
))}
|
|
199
220
|
</View>
|
|
200
|
-
{
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
221
|
+
{resolvedLine}
|
|
222
|
+
</View>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The commit bar for a `ChangeReview` — Accept all (1-by-1) · Discard · Apply.
|
|
228
|
+
* SEPARATE from the list so the host pins it where commit bars belong: drop it
|
|
229
|
+
* into a `DialogFooter` / `DrawerFooter`, or render it inline under the list.
|
|
230
|
+
* It reads the same `items` + `statusOf` the list does, so it owns Accept-all
|
|
231
|
+
* and the apply-disabled-at-0-kept logic — the host never re-derives them.
|
|
232
|
+
* Renders a right-aligned button row (no divider — the footer container or an
|
|
233
|
+
* inline wrapper provides the chrome); self-hides once `status` isn't open.
|
|
234
|
+
*/
|
|
235
|
+
export function ChangeReviewActions<T>(props: ChangeReviewActionsProps<T>) {
|
|
236
|
+
if ((props.status ?? "open") !== "open") return null;
|
|
237
|
+
const perItem = props.onAcceptItem != null;
|
|
238
|
+
const statusFor = (item: T, i: number) => props.statusOf?.(item, i) ?? "pending";
|
|
239
|
+
const keptCount = props.items.filter((it, i) => statusFor(it, i) === "accepted").length;
|
|
240
|
+
const acceptAll = () =>
|
|
241
|
+
props.items.forEach((it, i) => {
|
|
242
|
+
if (statusFor(it, i) === "pending") props.onAcceptItem?.(i);
|
|
243
|
+
});
|
|
244
|
+
return (
|
|
245
|
+
<View style={styles.actionsRow}>
|
|
246
|
+
{perItem ? <Button title={props.acceptAllLabel ?? "Accept all"} color="muted" shape="rounded" onPress={acceptAll} /> : null}
|
|
247
|
+
<View style={{ flex: 1 }} />
|
|
248
|
+
{props.onDiscard ? <Button title={props.discardLabel ?? "Discard"} color="muted" shape="rounded" onPress={props.onDiscard} /> : null}
|
|
249
|
+
{props.onApply ? (
|
|
250
|
+
<Button
|
|
251
|
+
title={props.applyLabel ?? (perItem ? "Apply kept" : "Apply")}
|
|
252
|
+
color="primary"
|
|
253
|
+
shape="rounded"
|
|
254
|
+
disabled={perItem && keptCount === 0}
|
|
255
|
+
onPress={props.onApply}
|
|
256
|
+
/>
|
|
257
|
+
) : null}
|
|
213
258
|
</View>
|
|
214
259
|
);
|
|
215
260
|
}
|
|
@@ -246,7 +291,10 @@ const styles = {
|
|
|
246
291
|
diff: { gap: 3 },
|
|
247
292
|
diffLine: { flexDirection: "row", alignItems: "baseline", gap: 8 },
|
|
248
293
|
strike: { textDecorationLine: "line-through" },
|
|
249
|
-
|
|
294
|
+
// The commit bar: a plain right-aligned row. The container (DialogFooter / an
|
|
295
|
+
// inline wrapper) owns any divider — the bar itself stays chrome-less so it
|
|
296
|
+
// nests cleanly in a footer without double borders. flex:1 fills the row.
|
|
297
|
+
actionsRow: { flexDirection: "row", alignItems: "center", gap: 8, flex: 1 },
|
|
250
298
|
}),
|
|
251
299
|
marker: (color: string) => ({ color, width: 12 }),
|
|
252
300
|
value: (color: string) => ({ color }),
|
package/src/remainder_meter.tsx
CHANGED
|
@@ -2,6 +2,26 @@ import { StyleSheet, View, type DimensionValue } from "react-native";
|
|
|
2
2
|
import { colors, solid } from "./colors";
|
|
3
3
|
import { Text } from "./text";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Override the meter's captions for localization. Each key is optional and
|
|
7
|
+
* falls back to English (mirrors `PaginationLabels` / `DateRangeFilterField`).
|
|
8
|
+
*/
|
|
9
|
+
export interface RemainderMeterLabels {
|
|
10
|
+
/** The left caption. Default: `{allocated} of {total} applied`. */
|
|
11
|
+
applied?: (allocated: string, total: string) => string;
|
|
12
|
+
/** UNDER state (a remainder still to place). Default: `{remainder} unapplied`. */
|
|
13
|
+
remaining?: (remainder: string) => string;
|
|
14
|
+
/** OVER state (over-allocated). Default: `Over by {amount}`. */
|
|
15
|
+
over?: (amount: string) => string;
|
|
16
|
+
/** EXACT state. Default: `Fully applied`. */
|
|
17
|
+
exact?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const defaultApplied = (allocated: string, total: string) => `${allocated} of ${total} applied`;
|
|
21
|
+
const defaultRemaining = (remainder: string) => `${remainder} unapplied`;
|
|
22
|
+
const defaultOver = (amount: string) => `Over by ${amount}`;
|
|
23
|
+
const DEFAULT_EXACT = "Fully applied";
|
|
24
|
+
|
|
5
25
|
export interface RemainderMeterProps {
|
|
6
26
|
/** The source amount being distributed — the payment, the available stock. */
|
|
7
27
|
total: number;
|
|
@@ -9,6 +29,8 @@ export interface RemainderMeterProps {
|
|
|
9
29
|
allocated: number;
|
|
10
30
|
/** Format amounts (money / units). Defaults to a plain locale string. */
|
|
11
31
|
format?: (n: number) => string;
|
|
32
|
+
/** Localize the captions; each key falls back to English. */
|
|
33
|
+
labels?: RemainderMeterLabels;
|
|
12
34
|
}
|
|
13
35
|
|
|
14
36
|
/**
|
|
@@ -20,17 +42,21 @@ export interface RemainderMeterProps {
|
|
|
20
42
|
* distribution, budgeting.
|
|
21
43
|
*/
|
|
22
44
|
export function RemainderMeter(props: RemainderMeterProps) {
|
|
23
|
-
const { total, allocated, format = (n) => n.toLocaleString() } = props;
|
|
45
|
+
const { total, allocated, format = (n) => n.toLocaleString(), labels } = props;
|
|
24
46
|
const remainder = total - allocated;
|
|
25
47
|
const state = remainder > 0 ? "under" : remainder < 0 ? "over" : "exact";
|
|
26
48
|
const pct: DimensionValue = `${total <= 0 ? 0 : Math.min(100, (allocated / total) * 100)}%`;
|
|
27
49
|
const barColor = state === "over" ? solid("red") : state === "exact" ? solid("emerald") : solid("blue");
|
|
28
50
|
const right =
|
|
29
|
-
state === "exact"
|
|
51
|
+
state === "exact"
|
|
52
|
+
? (labels?.exact ?? DEFAULT_EXACT)
|
|
53
|
+
: state === "over"
|
|
54
|
+
? (labels?.over ?? defaultOver)(format(-remainder))
|
|
55
|
+
: (labels?.remaining ?? defaultRemaining)(format(remainder));
|
|
30
56
|
return (
|
|
31
57
|
<View style={{ gap: 8 }}>
|
|
32
58
|
<View style={{ flexDirection: "row", alignItems: "baseline", gap: 8 }}>
|
|
33
|
-
<Text size="sm" color="muted" style={{ flex: 1 }}>{
|
|
59
|
+
<Text size="sm" color="muted" style={{ flex: 1 }}>{(labels?.applied ?? defaultApplied)(format(allocated), format(total))}</Text>
|
|
34
60
|
<Text size="sm" weight="medium" tabular color={state === "over" ? "danger" : state === "exact" ? "success" : "default"}>
|
|
35
61
|
{right}
|
|
36
62
|
</Text>
|