@lotics/ui 43.7.1 → 44.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/MIGRATION.md +38 -0
- package/docs/ai_patterns.md +18 -11
- package/docs/catalog.md +12 -9
- package/examples/tpl_record.tsx +10 -13
- package/package.json +1 -1
- package/src/diff_value.tsx +1 -1
- package/src/finding.tsx +147 -94
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,44 @@ Breaking changes, newest first — normally per major, plus the rare minor that
|
|
|
4
4
|
anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
|
|
5
5
|
this file exists only to move an app from one release to the next.
|
|
6
6
|
|
|
7
|
+
## 44.0.0 — `Finding` is horizontal, and `FindingComparison` is gone
|
|
8
|
+
|
|
9
|
+
`Finding` stacked six blocks — badge, title, detail, a labelled row per side, a hairline, the
|
|
10
|
+
delta, then `Sources` chips. It spent roughly 200px to say one sentence and one figure, buried
|
|
11
|
+
the delta in the middle where nothing scans it, and named every source twice: once in the
|
|
12
|
+
comparison rows, again in the chips below them.
|
|
13
|
+
|
|
14
|
+
It is now two lines: a severity dot, the title, and the delta right-aligned on that line, over
|
|
15
|
+
one dim line of readings. **`FindingComparison` is removed** — its job is the new `readings`
|
|
16
|
+
prop, and the readings line carries the provenance, so `sources`/`onOpenSource` on `Finding` are
|
|
17
|
+
replaced by `onOpenSource(source: string)` against a reading's own name.
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
// before
|
|
21
|
+
<Finding severity="critical" title="Quantity disagrees" detail="…"
|
|
22
|
+
sources={[{ id: "inv", label: "invoice.pdf", kind: "document" }]}>
|
|
23
|
+
<FindingComparison
|
|
24
|
+
values={[{ label: "invoice.pdf", value: "480 pcs" },
|
|
25
|
+
{ label: "packing-list.pdf", value: "440 pcs" }]}
|
|
26
|
+
delta="−40 pcs" />
|
|
27
|
+
</Finding>
|
|
28
|
+
|
|
29
|
+
// after
|
|
30
|
+
<Finding
|
|
31
|
+
severity="critical"
|
|
32
|
+
title="Quantity disagrees"
|
|
33
|
+
detail="…"
|
|
34
|
+
readings={[{ source: "invoice.pdf", value: "480 pcs" },
|
|
35
|
+
{ source: "packing-list.pdf", value: "440 pcs" }]}
|
|
36
|
+
delta="−40 pcs"
|
|
37
|
+
/>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`metric`/`metricCaption` are gone with it: a one-number finding is a `delta` with no readings.
|
|
41
|
+
The `children` slot is unchanged, so a `Table`, `ProgressBar` or `Confidence` body still
|
|
42
|
+
composes. Readings render at `sm` rather than `xs` — they are the values the finding rests on,
|
|
43
|
+
and hierarchy comes from weight and colour, never from shrinking the evidence.
|
|
44
|
+
|
|
7
45
|
## 43.5.0 — registers, type and theming all change appearance
|
|
8
46
|
|
|
9
47
|
No API is removed and nothing throws, but four DEFAULTS moved, so every register and every avatar
|
package/docs/ai_patterns.md
CHANGED
|
@@ -550,17 +550,24 @@ fail → low) — never render a fabricated score.
|
|
|
550
550
|
## Findings — evidence, not writes
|
|
551
551
|
|
|
552
552
|
`Finding` (`@lotics/ui/finding`) is one ranked insight from an AI check — a cross-check
|
|
553
|
-
discrepancy, an audit observation, a briefing item
|
|
554
|
-
`title`,
|
|
555
|
-
`
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
553
|
+
discrepancy, an audit observation, a briefing item — laid out HORIZONTALLY, in the order a reader
|
|
554
|
+
needs it: a severity dot, the `title`, and the `delta` right-aligned on that same line, then one
|
|
555
|
+
dim line of `readings` (`source value · source value`) beneath. Two lines carry all four things a
|
|
556
|
+
reader wants, and scanning a stack gives severity down the left edge and magnitude down the
|
|
557
|
+
right. `detail` is the CONSEQUENCE and is optional — a second sentence restating the title in
|
|
558
|
+
longer words costs a line and adds nothing.
|
|
559
|
+
|
|
560
|
+
BOTH readings stay on screen and NEITHER is struck through or marked wrong: a finding reports
|
|
561
|
+
that two sources disagree, and which one is right is the reader's call. Omit `delta` where the
|
|
562
|
+
fields are not commensurable — two spellings of a name have no difference to compute. The
|
|
563
|
+
readings ARE the provenance, so a `Sources` chip row underneath would name every document a
|
|
564
|
+
second time; chips belong on surfaces that cite a source without quoting it.
|
|
565
|
+
|
|
566
|
+
Severity → colour: `critical` red, `warning` amber, `info` zinc ("Note"), `positive` emerald
|
|
567
|
+
("On track"); words come from the `finding` locale slice and reach assistive tech as the dot's
|
|
568
|
+
label, since colour alone carries nothing. **Display-only**: a finding informs the action the
|
|
569
|
+
human takes in the app; it decides nothing itself — no phantom "record verdict" write (a
|
|
570
|
+
persisted check-status goes stale on the next edit). Stack several most-severe first.
|
|
564
571
|
|
|
565
572
|
## Session, not chat
|
|
566
573
|
|
package/docs/catalog.md
CHANGED
|
@@ -350,11 +350,13 @@ is a `DiffValue` over `CardSelectItem` candidates — and the ROW around any of
|
|
|
350
350
|
your screen. See the AI-patterns doc indexed in [AGENTS.md](../AGENTS.md) for the laws; `Clarify` (the agent asks back — selectable
|
|
351
351
|
`ChoiceList` options), `Sources` (provenance chips for AI output — at review scale,
|
|
352
352
|
`label={null}` slots the chips at a section's bottom), `Finding` (one ranked insight from an
|
|
353
|
-
AI check
|
|
354
|
-
|
|
355
|
-
the
|
|
356
|
-
|
|
357
|
-
|
|
353
|
+
AI check, laid out HORIZONTALLY: a severity dot, the title, and the `delta` right-aligned on
|
|
354
|
+
that same line, over one dim line of `readings` — `source value · source value`, both kept on
|
|
355
|
+
screen with NEITHER marked as the wrong one, since a finding reports that two sources disagree
|
|
356
|
+
and which is right is the reader's call. `detail` is the optional consequence. A reader scanning
|
|
357
|
+
a stack gets severity down the left edge and magnitude down the right. The readings ARE the
|
|
358
|
+
provenance, so there are no `Sources` chips under it repeating the same names. The children slot
|
|
359
|
+
composes ANY visual result — a compact `Table` for per-line detail (danger color on the offending cells),
|
|
358
360
|
`ProgressBar` for consumption-toward-a-cliff (free time, credit), dot `Badge`s for a
|
|
359
361
|
present/missing checklist, `Confidence` for judgment calls. Display-only — it informs the
|
|
360
362
|
verdict the host records; `finding` locale slice). `ApprovalPrompt` (the surface that ANSWERS
|
|
@@ -1915,7 +1917,7 @@ component rather than showing it at zero.
|
|
|
1915
1917
|
**`before === after` collapses to ONE plain value** —
|
|
1916
1918
|
agreement is not a change, and a document confirming what the record already says is the
|
|
1917
1919
|
GOOD case, not a diff of a value against itself. **`delta`** (a host-formatted string, the
|
|
1918
|
-
same contract as `
|
|
1920
|
+
same contract as `Finding.delta`) prints HOW FAR it moved, because
|
|
1919
1921
|
`1.600.000 → 1.481.481` otherwise makes the reader subtract to find out whether a
|
|
1920
1922
|
correction is trivial or alarming. `layout` stacked (a column of figures) | inline (prose, dense
|
|
1921
1923
|
rows); `align`/`tabular` for money; `tone` for whether the change is good news. Omit `before`
|
|
@@ -1990,9 +1992,10 @@ component rather than showing it at zero.
|
|
|
1990
1992
|
text IS the value (a value not in `options` reads as the custom answer).
|
|
1991
1993
|
- **`sources`** — `Sources` + `SourceRef`/`SourceKind` (record | document | table | web |
|
|
1992
1994
|
knowledge): provenance chips, per-kind glyphs.
|
|
1993
|
-
- **`finding`** — `Finding` + `
|
|
1994
|
-
|
|
1995
|
-
|
|
1995
|
+
- **`finding`** — `Finding` + `FindingReading` + `FindingSeverity`/`FindingLabels`: one ranked
|
|
1996
|
+
AI-check insight on two lines — severity dot, title, right-aligned `delta`, then the
|
|
1997
|
+
`readings` with the source each came from; optional `detail` consequence, children slot, and
|
|
1998
|
+
the `finding` locale slice.
|
|
1996
1999
|
- **`result_header`** — `ResultHeader` (+ `ResultTone`): the save-direct RECEIPT's outcome
|
|
1997
2000
|
strip, on the page grid — tone mark (`ok`/`attention`/`error`/`skipped`) inline on the
|
|
1998
2001
|
title row, outcome-first title, right-slot `action` (the open-record jump / a retry),
|
package/examples/tpl_record.tsx
CHANGED
|
@@ -63,7 +63,7 @@ import { pickFiles } from "@lotics/ui/file_picker";
|
|
|
63
63
|
import { FileDropTarget } from "@lotics/ui/file_drop_target";
|
|
64
64
|
import { Table, TableRow, TableCell, type TableColumn } from "@lotics/ui/table";
|
|
65
65
|
import { cycleSort, sortBy, type SortState } from "@lotics/ui/sort_header";
|
|
66
|
-
import { Finding
|
|
66
|
+
import { Finding } from "@lotics/ui/finding";
|
|
67
67
|
import { FormTextInput } from "@lotics/ui/form_text_input";
|
|
68
68
|
import { CheckboxInput } from "@lotics/ui/checkbox_input";
|
|
69
69
|
import { ChipGroup, type ChipOption } from "@lotics/ui/chip_group";
|
|
@@ -76,7 +76,6 @@ import { CardSelectItem } from "@lotics/ui/card_select_item";
|
|
|
76
76
|
import { AgentRun } from "@lotics/ui/agent_run";
|
|
77
77
|
import { FollowScroll } from "@lotics/ui/follow_scroll";
|
|
78
78
|
import { useContainerSize } from "@lotics/ui/size_boundary";
|
|
79
|
-
import { type SourceRef } from "@lotics/ui/sources";
|
|
80
79
|
import { DiffValue } from "@lotics/ui/diff_value";
|
|
81
80
|
import { DiffMark } from "@lotics/ui/diff_mark";
|
|
82
81
|
import { useChangeSet, type ChangeSet } from "@lotics/ui/use_change_set";
|
|
@@ -1371,18 +1370,17 @@ const UNTOUCHED_LINES: { id: string; item: string; qty: string; doc?: LineDoc }[
|
|
|
1371
1370
|
// Cross-check findings — severity reads through ONE colored dot badge (red /
|
|
1372
1371
|
// amber / zinc), the rest stays calm text.
|
|
1373
1372
|
type Severity = "critical" | "warning" | "info";
|
|
1374
|
-
interface Check { id: string; severity: Severity; title: string; detail?: string;
|
|
1373
|
+
interface Check { id: string; severity: Severity; title: string; detail?: string; readings?: { source: string; value: string }[]; delta?: string }
|
|
1375
1374
|
const FINDINGS: Check[] = [
|
|
1376
1375
|
{
|
|
1377
1376
|
id: "q1", severity: "critical", title: "Quantity disagrees between the invoice and the packing list",
|
|
1378
1377
|
detail: "Short-shipping against the invoice risks a customs query and a client claim.",
|
|
1379
|
-
|
|
1380
|
-
sources: ["invoice.pdf", "packing-list.pdf"],
|
|
1378
|
+
readings: [{ source: "invoice.pdf", value: "480 pcs" }, { source: "packing-list.pdf", value: "440 pcs" }], delta: "−40 pcs",
|
|
1381
1379
|
},
|
|
1382
1380
|
{
|
|
1383
1381
|
id: "q2", severity: "warning", title: "Consignee differs between the invoice and the booking",
|
|
1384
1382
|
detail: "The invoice names the buyer's own warehouse; the booking routes delivery through a distribution partner. One of them files wrong.",
|
|
1385
|
-
|
|
1383
|
+
readings: [{ source: "invoice.pdf", value: "Buyer warehouse" }, { source: "booking-confirmation.pdf", value: "Distribution partner" }],
|
|
1386
1384
|
},
|
|
1387
1385
|
{
|
|
1388
1386
|
id: "q4", severity: "warning", title: "No certificate of origin among the documents",
|
|
@@ -1391,7 +1389,7 @@ const FINDINGS: Check[] = [
|
|
|
1391
1389
|
{
|
|
1392
1390
|
id: "q3", severity: "info", title: "Booking cut-off is earlier than the invoiced ship week",
|
|
1393
1391
|
detail: "Documents close 14 Jul on the booking; the invoice quotes shipment in the week of 18 Jul. No conflict if the cargo is ready.",
|
|
1394
|
-
|
|
1392
|
+
readings: [{ source: "booking-confirmation.pdf", value: "Cut-off 14 Jul" }, { source: "invoice.pdf", value: "Ships w/c 18 Jul" }],
|
|
1395
1393
|
},
|
|
1396
1394
|
];
|
|
1397
1395
|
|
|
@@ -5048,8 +5046,8 @@ export function TplRecord({ chrome = "page", code = "RC-2026-0418", openSection
|
|
|
5048
5046
|
<View style={{ gap: 14 }}>
|
|
5049
5047
|
<Text size="md" weight="semibold">Findings</Text>
|
|
5050
5048
|
{/* Display-only: findings inform the verdict the footer records.
|
|
5051
|
-
The kit `Finding` owns severity
|
|
5052
|
-
|
|
5049
|
+
The kit `Finding` owns the severity dot, the title, the delta
|
|
5050
|
+
and the readings line; hairlines separate them. */}
|
|
5053
5051
|
{FINDINGS.map((c, i) => (
|
|
5054
5052
|
<Fragment key={c.id}>
|
|
5055
5053
|
{i > 0 ? <Divider /> : null}
|
|
@@ -5057,11 +5055,10 @@ export function TplRecord({ chrome = "page", code = "RC-2026-0418", openSection
|
|
|
5057
5055
|
severity={c.severity}
|
|
5058
5056
|
title={c.title}
|
|
5059
5057
|
detail={c.detail}
|
|
5060
|
-
|
|
5058
|
+
readings={c.readings}
|
|
5059
|
+
delta={c.delta}
|
|
5061
5060
|
onOpenSource={() => { /* preview stub */ }}
|
|
5062
|
-
|
|
5063
|
-
{c.comparison ? <FindingComparison values={c.comparison.values} delta={c.comparison.delta} /> : null}
|
|
5064
|
-
</Finding>
|
|
5061
|
+
/>
|
|
5065
5062
|
</Fragment>
|
|
5066
5063
|
))}
|
|
5067
5064
|
</View>
|
package/package.json
CHANGED
package/src/diff_value.tsx
CHANGED
|
@@ -74,7 +74,7 @@ export interface DiffValueProps {
|
|
|
74
74
|
* a correction is trivial or alarming; the delta is the thing they were going
|
|
75
75
|
* to work out anyway. A string, not a number, because only the host knows the
|
|
76
76
|
* unit, the sign convention and the rounding — the same contract
|
|
77
|
-
* `
|
|
77
|
+
* `Finding.delta` already uses.
|
|
78
78
|
*/
|
|
79
79
|
delta?: string;
|
|
80
80
|
style?: StyleProp<ViewStyle>;
|
package/src/finding.tsx
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
1
|
import { StyleSheet, View, type StyleProp, type ViewStyle } from "react-native";
|
|
2
2
|
import { type ReactNode } from "react";
|
|
3
|
-
import { Badge } from "./badge";
|
|
4
3
|
import { Text } from "./text";
|
|
5
|
-
import {
|
|
4
|
+
import { PressableHighlight } from "./pressable_highlight";
|
|
6
5
|
import { useLoticsLocale } from "./locale";
|
|
7
|
-
import {
|
|
6
|
+
import { solid, type ColorName } from "./colors";
|
|
8
7
|
|
|
9
8
|
export type FindingSeverity = "critical" | "warning" | "info" | "positive";
|
|
10
9
|
|
|
11
10
|
/** The localized strings — the `finding` LoticsLocale slice. */
|
|
12
11
|
export type FindingLabels = Record<FindingSeverity, string> & {
|
|
13
|
-
/** The delta
|
|
12
|
+
/** The delta's label, read to assistive tech beside the figure. */
|
|
14
13
|
difference: string;
|
|
15
14
|
};
|
|
16
15
|
|
|
16
|
+
/**
|
|
17
|
+
* The dot's offset is COMPUTED, not eyeballed: centre it in the title's line
|
|
18
|
+
* box, then add where that box starts inside the row — RN-Web's `Text` sits a
|
|
19
|
+
* couple of px below its parent's top, and ignoring that left the dot 2px high
|
|
20
|
+
* even once the centring was right. Both numbers were measured off the rendered
|
|
21
|
+
* row rather than guessed; a guess put it 4.5px out.
|
|
22
|
+
*/
|
|
23
|
+
const TITLE_LINE = 24;
|
|
24
|
+
const TITLE_INSET = 2;
|
|
25
|
+
const DOT = 7;
|
|
26
|
+
|
|
17
27
|
const SEVERITY_COLOR: Record<FindingSeverity, ColorName> = {
|
|
18
28
|
critical: "red",
|
|
19
29
|
warning: "amber",
|
|
@@ -21,121 +31,164 @@ const SEVERITY_COLOR: Record<FindingSeverity, ColorName> = {
|
|
|
21
31
|
positive: "emerald",
|
|
22
32
|
};
|
|
23
33
|
|
|
34
|
+
/** One reading: what a source says, and which source said it. */
|
|
35
|
+
export interface FindingReading {
|
|
36
|
+
/** Where it came from — a document, a table, a rate card. */
|
|
37
|
+
source: string;
|
|
38
|
+
/** What that source says the value is. */
|
|
39
|
+
value: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
24
42
|
export interface FindingProps {
|
|
25
|
-
/** Ranked severity — the dot
|
|
43
|
+
/** Ranked severity — sets the dot's colour and its localized word. */
|
|
26
44
|
severity: FindingSeverity;
|
|
27
|
-
/** What
|
|
45
|
+
/** What was found, in one line, in the reader's own nouns. */
|
|
28
46
|
title: string;
|
|
29
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* The readings that disagree, in source order. Rendered inline beneath the
|
|
49
|
+
* title as `source value · source value`.
|
|
50
|
+
*
|
|
51
|
+
* NEITHER is marked as the wrong one. A finding reports that two sources
|
|
52
|
+
* disagree; which one is right is the reader's call, and a struck-through or
|
|
53
|
+
* red-inked side would make that call for them.
|
|
54
|
+
*/
|
|
55
|
+
readings?: FindingReading[];
|
|
56
|
+
/**
|
|
57
|
+
* The computed difference — "+170 USD", "−40 pcs", "3 days late".
|
|
58
|
+
*
|
|
59
|
+
* The most actionable thing on a finding, which is why it sits right-aligned
|
|
60
|
+
* on the title's own line rather than below the readings: it is the figure
|
|
61
|
+
* the reader acts on, and `1,450 → 1,280` makes them do the subtraction
|
|
62
|
+
* themselves. Omit where the fields are not commensurable — two spellings of
|
|
63
|
+
* a name have no delta.
|
|
64
|
+
*/
|
|
65
|
+
delta?: string;
|
|
66
|
+
/**
|
|
67
|
+
* The CONSEQUENCE, when the title does not already carry it — what happens if
|
|
68
|
+
* this ships. Optional on purpose: a second sentence that restates the title
|
|
69
|
+
* in longer words costs a line and adds nothing.
|
|
70
|
+
*/
|
|
30
71
|
detail?: string;
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
/** What the metric compares ("invoice vs packing list"). */
|
|
35
|
-
metricCaption?: string;
|
|
36
|
-
/** Provenance — the one `Sources` idiom, chips at the bottom. */
|
|
37
|
-
sources?: SourceRef[];
|
|
38
|
-
onOpenSource?: (source: SourceRef) => void;
|
|
39
|
-
/** Compose anything extra between the body and the sources — a band pair,
|
|
40
|
-
* an action row, a custom block. */
|
|
72
|
+
/** Makes each source name pressable — the host navigates to it. */
|
|
73
|
+
onOpenSource?: (source: string) => void;
|
|
74
|
+
/** Anything richer than readings — a table, a chart, a confidence bar. */
|
|
41
75
|
children?: ReactNode;
|
|
42
76
|
style?: StyleProp<ViewStyle>;
|
|
43
77
|
}
|
|
44
78
|
|
|
45
79
|
/**
|
|
46
|
-
*
|
|
47
|
-
* observation, a briefing item.
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
80
|
+
* ONE RANKED INSIGHT from an AI check — a cross-check discrepancy, an audit
|
|
81
|
+
* observation, a briefing item.
|
|
82
|
+
*
|
|
83
|
+
* The readings sit at `sm`, the same size as the title. They were `xs` — a
|
|
84
|
+
* miniature that said "secondary" by being hard to read, which is the wrong
|
|
85
|
+
* trade for the values the whole finding rests on. Hierarchy comes from WEIGHT
|
|
86
|
+
* and colour here, never from shrinking the evidence.
|
|
87
|
+
*
|
|
88
|
+
* Laid out HORIZONTALLY, in the order a reader needs it: the dot says whether
|
|
89
|
+
* to care, the title says what is wrong, the delta on that same line says how
|
|
90
|
+
* far off, and one dim line underneath says who said what. Two lines carry all
|
|
91
|
+
* four, so a reader scanning a stack reads severity down the left edge and
|
|
92
|
+
* magnitude down the right.
|
|
93
|
+
*
|
|
94
|
+
* The predecessor stacked six blocks — badge, title, detail, a labelled row per
|
|
95
|
+
* side, a hairline, the delta, then source chips. It spent roughly 200px to say
|
|
96
|
+
* one sentence and one figure, buried the delta in the middle where nothing
|
|
97
|
+
* scans it, and named every source twice: once in the comparison rows, again in
|
|
98
|
+
* the chips below them. The readings line IS the provenance, so the chips went
|
|
99
|
+
* with it, and `FindingComparison` is gone — its job is the `readings` prop.
|
|
100
|
+
*
|
|
101
|
+
* **Display-only**: a finding informs the action the human takes; it decides
|
|
102
|
+
* nothing itself. Stack several most-severe first.
|
|
52
103
|
*/
|
|
53
104
|
export function Finding(props: FindingProps) {
|
|
54
105
|
const words = useLoticsLocale().finding;
|
|
106
|
+
const readings = props.readings ?? [];
|
|
107
|
+
|
|
55
108
|
return (
|
|
56
109
|
<View style={[styles.root, props.style]}>
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
{props.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
{props.metric ? (
|
|
67
|
-
<View style={styles.metricBlock}>
|
|
68
|
-
<Text size="lg" weight="semibold" tabular>
|
|
69
|
-
{props.metric}
|
|
110
|
+
<View
|
|
111
|
+
style={[styles.dot, { backgroundColor: solid(SEVERITY_COLOR[props.severity]) }]}
|
|
112
|
+
accessibilityLabel={words[props.severity]}
|
|
113
|
+
/>
|
|
114
|
+
|
|
115
|
+
<View style={styles.body}>
|
|
116
|
+
<View style={styles.headline}>
|
|
117
|
+
<Text size="sm" weight="medium" style={styles.title}>
|
|
118
|
+
{props.title}
|
|
70
119
|
</Text>
|
|
71
|
-
{props.
|
|
72
|
-
<Text
|
|
73
|
-
|
|
120
|
+
{props.delta ? (
|
|
121
|
+
<Text
|
|
122
|
+
size="md"
|
|
123
|
+
weight="semibold"
|
|
124
|
+
tabular
|
|
125
|
+
style={[styles.delta, { color: solid(SEVERITY_COLOR[props.severity]) }]}
|
|
126
|
+
accessibilityLabel={`${words.difference}: ${props.delta}`}
|
|
127
|
+
>
|
|
128
|
+
{props.delta}
|
|
74
129
|
</Text>
|
|
75
130
|
) : null}
|
|
76
131
|
</View>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
132
|
+
|
|
133
|
+
{readings.length > 0 ? (
|
|
134
|
+
<View style={styles.readings}>
|
|
135
|
+
{readings.map((reading, index) => (
|
|
136
|
+
<View key={`${reading.source}-${index}`} style={styles.reading}>
|
|
137
|
+
{index > 0 ? (
|
|
138
|
+
<Text size="sm" color="muted">
|
|
139
|
+
·
|
|
140
|
+
</Text>
|
|
141
|
+
) : null}
|
|
142
|
+
<SourceName label={reading.source} onOpen={props.onOpenSource} />
|
|
143
|
+
<Text size="sm" weight="medium" tabular numberOfLines={1}>
|
|
144
|
+
{reading.value}
|
|
145
|
+
</Text>
|
|
146
|
+
</View>
|
|
147
|
+
))}
|
|
148
|
+
</View>
|
|
149
|
+
) : null}
|
|
150
|
+
|
|
151
|
+
{props.detail ? (
|
|
152
|
+
<Text size="sm" color="muted">
|
|
153
|
+
{props.detail}
|
|
154
|
+
</Text>
|
|
155
|
+
) : null}
|
|
156
|
+
|
|
157
|
+
{props.children}
|
|
158
|
+
</View>
|
|
82
159
|
</View>
|
|
83
160
|
);
|
|
84
161
|
}
|
|
85
162
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* The expected-vs-actual body of a `Finding` — each side a labeled row
|
|
98
|
-
* (source, value), a hairline, then the DELTA emphasized. Compose it as the
|
|
99
|
-
* finding's children; it replaces ad-hoc metric text for any
|
|
100
|
-
* one-value-disagrees insight (quantities, totals, dates).
|
|
101
|
-
*/
|
|
102
|
-
export function FindingComparison(props: FindingComparisonProps) {
|
|
103
|
-
const words = useLoticsLocale().finding;
|
|
163
|
+
/** The source behind a reading. Pressable only when the host can navigate to
|
|
164
|
+
* it — an affordance on something that goes nowhere is a broken promise. */
|
|
165
|
+
function SourceName({ label, onOpen }: { label: string; onOpen?: (source: string) => void }) {
|
|
166
|
+
const name = (
|
|
167
|
+
<Text size="sm" color="muted" numberOfLines={1}>
|
|
168
|
+
{label}
|
|
169
|
+
</Text>
|
|
170
|
+
);
|
|
171
|
+
if (!onOpen) return name;
|
|
104
172
|
return (
|
|
105
|
-
<
|
|
106
|
-
{
|
|
107
|
-
|
|
108
|
-
<Text size="sm" color="muted" style={styles.comparisonLabel} numberOfLines={1}>
|
|
109
|
-
{v.label}
|
|
110
|
-
</Text>
|
|
111
|
-
<Text size="sm" weight="semibold" tabular numberOfLines={1} style={{ flexShrink: 1 }}>
|
|
112
|
-
{v.value}
|
|
113
|
-
</Text>
|
|
114
|
-
</View>
|
|
115
|
-
))}
|
|
116
|
-
{props.delta ? (
|
|
117
|
-
<>
|
|
118
|
-
<View style={styles.comparisonRule} />
|
|
119
|
-
<View style={styles.comparisonRow}>
|
|
120
|
-
<Text size="sm" color="muted" style={styles.comparisonLabel} numberOfLines={1}>
|
|
121
|
-
{props.deltaLabel ?? words.difference}
|
|
122
|
-
</Text>
|
|
123
|
-
<Text size="lg" weight="semibold" tabular>
|
|
124
|
-
{props.delta}
|
|
125
|
-
</Text>
|
|
126
|
-
</View>
|
|
127
|
-
</>
|
|
128
|
-
) : null}
|
|
129
|
-
</View>
|
|
173
|
+
<PressableHighlight onPress={() => onOpen(label)} accessibilityRole="link" accessibilityLabel={label}>
|
|
174
|
+
{name}
|
|
175
|
+
</PressableHighlight>
|
|
130
176
|
);
|
|
131
177
|
}
|
|
132
178
|
|
|
133
179
|
const styles = StyleSheet.create({
|
|
134
|
-
root: { gap:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
180
|
+
root: { flexDirection: "row", gap: 8, alignItems: "flex-start" },
|
|
181
|
+
// See TITLE_LINE above — centred on the title's line box, plus where that box
|
|
182
|
+
// begins in the row.
|
|
183
|
+
dot: { width: DOT, height: DOT, borderRadius: DOT / 2, marginTop: TITLE_INSET + (TITLE_LINE - DOT) / 2 },
|
|
184
|
+
body: { flex: 1, gap: 3, minWidth: 0 },
|
|
185
|
+
headline: { flexDirection: "row", alignItems: "baseline", gap: 12 },
|
|
186
|
+
title: { flex: 1, minWidth: 0 },
|
|
187
|
+
// Never shrinks: the figure is the point of the row, and a clipped delta is
|
|
188
|
+
// worse than a title that wraps. Its COLOUR is the severity's, set on the
|
|
189
|
+
// instance — the dot marks the left edge and the figure the right, so a
|
|
190
|
+
// reader scanning a stack gets rank and magnitude in one pass.
|
|
191
|
+
delta: { flexShrink: 0 },
|
|
192
|
+
readings: { flexDirection: "row", flexWrap: "wrap", alignItems: "baseline", gap: 6 },
|
|
193
|
+
reading: { flexDirection: "row", alignItems: "baseline", gap: 5, minWidth: 0 },
|
|
140
194
|
});
|
|
141
|
-
|