@lotics/ui 7.19.3 → 8.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.
@@ -1,79 +0,0 @@
1
- import { type ReactNode } from "react";
2
- import { StyleSheet, View } from "react-native";
3
- import { colors } from "./colors";
4
- import { Text } from "./text";
5
- import { Icon } from "./icon";
6
-
7
- export interface MatchSideTextProps {
8
- /** Primary line — the record name / id. */
9
- title: string;
10
- /** Secondary line — amount, date, customer. */
11
- detail?: string;
12
- /** Right-align for the proposed (right) side of a pairing. */
13
- align?: "left" | "right";
14
- }
15
-
16
- /**
17
- * The DEFAULT side content — a title + a muted detail line. Pass it into a
18
- * `MatchSides` slot for the common case; drop a different node in when a side
19
- * needs more (a `Peek` to preview the record, an interactive card, a thumbnail).
20
- */
21
- export function MatchSideText({ title, detail, align }: MatchSideTextProps) {
22
- const right = align === "right";
23
- return (
24
- <>
25
- <Text size="sm" weight="semibold" numberOfLines={1} align={right ? "right" : undefined}>
26
- {title}
27
- </Text>
28
- {detail ? (
29
- <Text size="xs" color="muted" numberOfLines={1} align={right ? "right" : undefined}>
30
- {detail}
31
- </Text>
32
- ) : null}
33
- </>
34
- );
35
- }
36
-
37
- export interface MatchSidesProps {
38
- /** Left content — ANY node. `MatchSideText` for the default, or a `Peek` /
39
- * interactive card / thumbnail when a side needs to do more. */
40
- source: ReactNode;
41
- /** Right content. Omit → the "no confident match" placeholder. */
42
- match?: ReactNode;
43
- /** The connector between the sides. Default: a right arrow. Override to carry
44
- * a swap control, a label, or a different glyph. */
45
- connector?: ReactNode;
46
- }
47
-
48
- /**
49
- * The two-sided PAIRING — a left slot, a connector, a right slot. A pure LAYOUT
50
- * primitive: it owns the two-column arrangement and the connector, the CONTENT
51
- * of each side is yours (`MatchSideText`, a `Peek`, anything). The hero of a
52
- * reconciliation; the confidence, reason, and decision come from the `ReviewCard`.
53
- */
54
- export function MatchSides(props: MatchSidesProps) {
55
- return (
56
- <View style={styles.pair}>
57
- <View style={styles.side}>{props.source}</View>
58
- {props.connector ?? <Icon name="arrow-right" size={16} color={colors.zinc[400]} />}
59
- <View style={[styles.side, styles.sideRight]}>
60
- {props.match ?? (
61
- <>
62
- <Text size="sm" weight="medium" color="muted" align="right" numberOfLines={1}>
63
- No confident match
64
- </Text>
65
- <Text size="xs" color="muted" align="right" numberOfLines={1}>
66
- Pick a counterpart
67
- </Text>
68
- </>
69
- )}
70
- </View>
71
- </View>
72
- );
73
- }
74
-
75
- const styles = StyleSheet.create({
76
- pair: { flexDirection: "row", alignItems: "center", gap: 14 },
77
- side: { flex: 1, gap: 3, minWidth: 0 },
78
- sideRight: { alignItems: "flex-end" },
79
- });
@@ -1,68 +0,0 @@
1
- import { StyleSheet, View } from "react-native";
2
- import { solid } from "./colors";
3
- import { Text } from "./text";
4
- import { InlineTextInput } from "./inline_text_input";
5
-
6
- export interface RecordField {
7
- label: string;
8
- value: string;
9
- /** Unit suffix shown after the value (₫, kg, pcs). */
10
- unit?: string;
11
- /** Mark a low-confidence field worth double-checking — a subtle amber dot. */
12
- uncertain?: boolean;
13
- }
14
-
15
- export interface RecordFieldsProps {
16
- fields: RecordField[];
17
- /** Edit a field — fired on save of the inline editor (blur / Enter). When set,
18
- * each value is click-to-edit; omit for a read-only record. */
19
- onEditField?: (index: number, value: string) => void;
20
- }
21
-
22
- /**
23
- * The BODY of an extracted record — a label/value list, each value click-to-edit
24
- * (`InlineTextInput`) or read-only. A pure body atom: drop it into a `ReviewCard`
25
- * to review-before-save (a confidence + Confirm / Remove, stacked under a Save-all
26
- * → `tpl_extract`), or use it standalone as a live-edit field card (a params
27
- * panel). The review MECHANICS belong to the host / `ReviewCard`, not here.
28
- */
29
- export function RecordFields(props: RecordFieldsProps) {
30
- return (
31
- <View style={styles.fields}>
32
- {props.fields.map((f, i) => (
33
- <View key={`${f.label}-${i}`} style={styles.fieldRow}>
34
- <View style={styles.labelCol}>
35
- {f.uncertain ? <View style={styles.uncertainDot} /> : null}
36
- <Text size="sm" color="muted" numberOfLines={1}>
37
- {f.label}
38
- </Text>
39
- </View>
40
- <View style={styles.valueCol}>
41
- <View style={{ flex: 1 }}>
42
- {props.onEditField ? (
43
- <InlineTextInput value={f.value} accessibilityLabel={`Edit ${f.label}`} onSave={(next) => props.onEditField?.(i, next)} />
44
- ) : (
45
- <Text size="sm" weight="medium" tabular>
46
- {f.value}
47
- </Text>
48
- )}
49
- </View>
50
- {f.unit ? (
51
- <Text size="sm" color="muted">
52
- {f.unit}
53
- </Text>
54
- ) : null}
55
- </View>
56
- </View>
57
- ))}
58
- </View>
59
- );
60
- }
61
-
62
- const styles = StyleSheet.create({
63
- fields: { gap: 2 },
64
- fieldRow: { flexDirection: "row", alignItems: "center", gap: 16, minHeight: 34 },
65
- labelCol: { flexDirection: "row", alignItems: "center", gap: 6, width: 132 },
66
- uncertainDot: { width: 6, height: 6, borderRadius: 999, backgroundColor: solid("amber") },
67
- valueCol: { flex: 1, flexDirection: "row", alignItems: "center", gap: 6 },
68
- });
@@ -1,172 +0,0 @@
1
- import { type ReactNode } from "react";
2
- import { StyleSheet, View } from "react-native";
3
- import { colors, solid } from "./colors";
4
- import { reviewCardStyle } from "./control_surface";
5
- import { Text } from "./text";
6
- import { Icon } from "./icon";
7
- import { Button } from "./button";
8
- import { Confidence, type ConfidenceLevel } from "./confidence";
9
-
10
- export interface ReviewAction {
11
- label: string;
12
- onPress: () => void;
13
- /** Visual weight. Default: the LAST action reads "primary" (the affirmative),
14
- * the rest "muted". Use "secondary" for a non-destructive middle action
15
- * (Edit / Reassign). */
16
- kind?: "primary" | "secondary" | "muted";
17
- }
18
-
19
- export interface ReviewCardProps {
20
- /** Header-first: a small muted eyebrow above the body — e.g. "Proposed". */
21
- eyebrow?: string;
22
- /** Header-first: a headline above the body — a record name, a proposal. */
23
- title?: string;
24
- confidence?: ConfidenceLevel;
25
- confidenceScore?: number;
26
- /** One line of WHY — the rationale the human weighs. */
27
- rationale?: string;
28
- /** The proposal BODY — render it however fits: a `RecordFields`, a `MatchSides`
29
- * pairing, a spec stack, a chip row. Omit for a title-only proposal. */
30
- children?: ReactNode;
31
- /** Decision actions, left→right (the last reads as the primary affirmative):
32
- * Accept / Edit / Dismiss, Confirm / Remove, Accept / Reassign / Not-a-match —
33
- * whatever the proposal's verbs are. Nothing applies until the host acts. */
34
- actions?: ReviewAction[];
35
- /** Resolved → the card COLLAPSES to a quiet one-line row (a check or ✕ + a
36
- * summary + Undo), so a stack reads as a tidy checklist of what's decided. */
37
- status?: "open" | "accepted" | "dismissed";
38
- /** The summary text in the resolved row (defaults to `title`; required when
39
- * body-first, since the body is arbitrary). */
40
- summary?: string;
41
- /** Resolved outcome words. Default "Accepted" / "Dismissed"; set per surface
42
- * ("Confirmed" / "Removed", `Matched · …` / "Dismissed"). */
43
- acceptedLabel?: string;
44
- dismissedLabel?: string;
45
- onUndo?: () => void;
46
- }
47
-
48
- /**
49
- * THE single-proposal review card — an AI proposal the human accepts, edits, or
50
- * dismisses, never auto-applied. It owns the chrome (the shared `reviewCardStyle`
51
- * surface, `Confidence` + rationale, the decision footer, the collapse-to-a-row
52
- * resolved state); the proposal BODY is `children` — a `RecordFields` (an
53
- * extracted record to confirm), a `MatchSides` (a pairing to reconcile), a spec
54
- * stack, anything.
55
- *
56
- * Layout follows the props: give it an `eyebrow`/`title` and the chrome LEADS
57
- * (header-first — a value or record proposal); give it only `children` and the
58
- * BODY leads, the confidence + reason sitting beneath it (body-first — a pairing,
59
- * where the body is the hero). For a BATCH you decide each then commit, use the
60
- * `ChangeReview` engine instead.
61
- */
62
- export function ReviewCard(props: ReviewCardProps) {
63
- const status = props.status ?? "open";
64
-
65
- if (status === "accepted" || status === "dismissed") {
66
- const accepted = status === "accepted";
67
- return (
68
- <View style={styles.resolvedRow}>
69
- <Icon name={accepted ? "check" : "x"} size={15} color={accepted ? solid("emerald") : colors.zinc[400]} />
70
- <Text
71
- size="sm"
72
- weight="medium"
73
- color={accepted ? "default" : "muted"}
74
- numberOfLines={1}
75
- style={[{ flex: 1 }, accepted ? null : styles.strike]}
76
- >
77
- {props.summary ?? props.title ?? ""}
78
- </Text>
79
- <Text size="sm" weight="medium" color="muted" numberOfLines={1}>
80
- {accepted ? props.acceptedLabel ?? "Accepted" : props.dismissedLabel ?? "Dismissed"}
81
- </Text>
82
- {props.onUndo ? <Button title="Undo" color="muted" onPress={props.onUndo} /> : null}
83
- </View>
84
- );
85
- }
86
-
87
- const headed = !!(props.eyebrow || props.title);
88
- const hasConfidence = props.confidence != null || props.confidenceScore != null;
89
- const confidence = hasConfidence ? <Confidence level={props.confidence} score={props.confidenceScore} /> : null;
90
- const actions = props.actions ?? [];
91
-
92
- return (
93
- <View style={reviewCardStyle}>
94
- {/* Header-first: eyebrow + confidence, title, rationale ABOVE the body. */}
95
- {props.eyebrow ? (
96
- <View style={styles.headRow}>
97
- <Text size="xs" color="muted" weight="medium" style={{ flex: 1 }}>
98
- {props.eyebrow}
99
- </Text>
100
- {confidence}
101
- </View>
102
- ) : null}
103
- {props.title ? (
104
- <View style={styles.headRow}>
105
- <Text size="sm" weight="semibold" style={{ flex: 1 }} numberOfLines={2}>
106
- {props.title}
107
- </Text>
108
- {!props.eyebrow ? confidence : null}
109
- </View>
110
- ) : null}
111
- {headed && props.rationale ? (
112
- <Text size="sm" color="muted">
113
- {props.rationale}
114
- </Text>
115
- ) : null}
116
-
117
- {props.children ? <View>{props.children}</View> : null}
118
-
119
- {/* Body-first: the confidence + reason sit BENEATH the body, as one unit. */}
120
- {!headed && (hasConfidence || props.rationale) ? (
121
- <View style={styles.basis}>
122
- {confidence}
123
- {props.rationale ? (
124
- <Text size="sm" color="muted">
125
- {props.rationale}
126
- </Text>
127
- ) : null}
128
- </View>
129
- ) : null}
130
-
131
- {actions.length > 0 ? (
132
- <View style={styles.footer}>
133
- {actions.map((a, i) => {
134
- const kind = a.kind ?? (i === actions.length - 1 ? "primary" : "muted");
135
- return <Button key={a.label} title={a.label} color={kind} onPress={a.onPress} />;
136
- })}
137
- </View>
138
- ) : null}
139
- </View>
140
- );
141
- }
142
-
143
- const styles = StyleSheet.create({
144
- headRow: { flexDirection: "row", alignItems: "center", gap: 10 },
145
- // Body-first: confidence on top, the reason beneath — "this sure, because…".
146
- basis: { alignItems: "flex-start", gap: 6 },
147
- // The decisions sit below a hairline so the CTA row reads as a deliberate
148
- // footer, not buttons crowding the body.
149
- footer: {
150
- flexDirection: "row",
151
- alignItems: "center",
152
- justifyContent: "flex-end",
153
- gap: 8,
154
- borderTopWidth: 1,
155
- borderTopColor: colors.zinc[100],
156
- paddingTop: 14,
157
- },
158
- resolvedRow: {
159
- flexDirection: "row",
160
- alignItems: "center",
161
- gap: 10,
162
- backgroundColor: colors.white,
163
- borderWidth: 1,
164
- borderColor: colors.zinc[100],
165
- borderRadius: 10,
166
- paddingLeft: 12,
167
- paddingRight: 8,
168
- paddingVertical: 6,
169
- minHeight: 44,
170
- },
171
- strike: { textDecorationLine: "line-through" },
172
- });
@@ -1,138 +0,0 @@
1
- import { StyleSheet, View, type ViewStyle } from "react-native";
2
- import { colors } from "./colors";
3
- import { Text } from "./text";
4
- import { Button } from "./button";
5
- import { SpecList, type SpecRow } from "./spec_list";
6
-
7
- export interface ScoredOptionProps {
8
- /** Rank in the shortlist (1 = top). */
9
- rank?: number;
10
- title: string;
11
- /** Secondary line — the provider, the route summary. */
12
- subtitle?: string;
13
- /** 0–1 score the agent assigned — drives the score bar + figure. */
14
- score?: number;
15
- /** One line on why it ranks where it does. */
16
- rationale?: string;
17
- /** Key specs (price, transit, …) — rendered as a dense `SpecList`. */
18
- specs?: SpecRow[];
19
- /** The agent's top pick — a "RECOMMENDED" label + a heavier border. */
20
- recommended?: boolean;
21
- selected?: boolean;
22
- onSelect?: () => void;
23
- selectLabel?: string;
24
- }
25
-
26
- /**
27
- * An AI-RANKED candidate in a shortlist — rank, score, the agent's one-line
28
- * reason (a margin note), and the key specs — that the human picks from. The
29
- * agent scores and orders; the choice stays the human's. Mark the agent's top
30
- * pick `recommended` (a label + heavier border, no colour). The unit of an AI
31
- * compare / shortlist / recommend surface (quotes, carriers, suppliers, plans).
32
- * Monochrome and iconless. Stack several, highest rank first.
33
- */
34
- export function ScoredOption(props: ScoredOptionProps) {
35
- const { rank, title, subtitle, score, rationale, specs, recommended, selected, onSelect, selectLabel } = props;
36
- const pct = score != null ? Math.round(score * 100) : null;
37
- return (
38
- <View style={[styles.card, recommended ? styles.recCard : null, selected ? styles.selected : null]}>
39
- {recommended ? (
40
- <Text size="xs" color="default" weight="semibold">
41
- Recommended
42
- </Text>
43
- ) : null}
44
-
45
- <View style={styles.head}>
46
- {rank != null ? (
47
- <View style={styles.rank}>
48
- <Text size="sm" weight="semibold" style={{ color: colors.zinc[700] }}>
49
- {rank}
50
- </Text>
51
- </View>
52
- ) : null}
53
- <View style={styles.titleCol}>
54
- <Text size="md" weight="semibold" numberOfLines={1}>
55
- {title}
56
- </Text>
57
- {subtitle ? (
58
- <Text size="xs" color="muted" numberOfLines={1}>
59
- {subtitle}
60
- </Text>
61
- ) : null}
62
- </View>
63
- {pct != null ? (
64
- <View style={styles.scoreCol}>
65
- <Text size="lg" weight="semibold" tabular>
66
- {pct}
67
- </Text>
68
- <Text size="xs" color="muted">
69
- score
70
- </Text>
71
- </View>
72
- ) : null}
73
- </View>
74
-
75
- {pct != null ? (
76
- <View style={styles.track}>
77
- <View style={[styles.fill, { width: `${pct}%` }]} />
78
- </View>
79
- ) : null}
80
-
81
- {rationale ? (
82
- <View style={styles.note}>
83
- <Text size="sm" color="muted">
84
- {rationale}
85
- </Text>
86
- </View>
87
- ) : null}
88
-
89
- {specs && specs.length > 0 ? <SpecList rows={specs} dense /> : null}
90
-
91
- {onSelect ? (
92
- <View style={styles.footer}>
93
- <Button
94
- title={selected ? "Selected" : selectLabel ?? "Choose"}
95
- icon={selected ? "check" : undefined}
96
- color={selected ? "secondary" : "primary"}
97
- onPress={onSelect}
98
- />
99
- </View>
100
- ) : null}
101
- </View>
102
- );
103
- }
104
-
105
- const RING: ViewStyle = { boxShadow: `0 0 0 2px ${colors.zinc[900]}` } as ViewStyle;
106
-
107
- const styles = StyleSheet.create({
108
- card: {
109
- borderWidth: 1,
110
- borderColor: colors.border,
111
- backgroundColor: colors.white,
112
- borderRadius: 12,
113
- padding: 16,
114
- gap: 12,
115
- },
116
- recCard: { borderColor: colors.zinc[400] },
117
- selected: RING,
118
- footer: { flexDirection: "row", justifyContent: "flex-end" },
119
- head: { flexDirection: "row", alignItems: "center", gap: 12 },
120
- rank: {
121
- width: 28,
122
- height: 28,
123
- borderRadius: 8,
124
- alignItems: "center",
125
- justifyContent: "center",
126
- backgroundColor: colors.zinc[100],
127
- },
128
- titleCol: { flex: 1, gap: 1 },
129
- scoreCol: { alignItems: "flex-end" },
130
- track: {
131
- height: 6,
132
- borderRadius: 999,
133
- backgroundColor: colors.zinc[100],
134
- overflow: "hidden",
135
- },
136
- fill: { height: "100%", borderRadius: 999, backgroundColor: colors.zinc[900] },
137
- note: { borderLeftWidth: 2, borderLeftColor: colors.zinc[300], paddingLeft: 10 },
138
- });
package/src/spec_list.tsx DELETED
@@ -1,81 +0,0 @@
1
- import { StyleSheet, View } from "react-native";
2
- import { Text } from "./text";
3
- import { Divider } from "./divider";
4
-
5
- export interface SpecRow {
6
- /** The line label (left). */
7
- label: string;
8
- /** The value (right). A number renders grouped + tabular. */
9
- value: string | number;
10
- /** Unit after the value (%, mm, kg, ₫). */
11
- unit?: string;
12
- /** A heavier value — a headline figure or a sub-total inside the list. */
13
- emphasis?: boolean;
14
- }
15
-
16
- export interface SpecListProps {
17
- rows: SpecRow[];
18
- /** A pinned summary row, set off by a divider above and a heavier weight —
19
- * the total of a duty / cost / dimension breakdown. */
20
- total?: SpecRow;
21
- /** Tighter rows + xs labels for a dense side panel. */
22
- dense?: boolean;
23
- }
24
-
25
- /**
26
- * A labeled-value breakdown — the structured "spec sheet": a duty table, a
27
- * dimensions list, a cost split, the exact numbers behind an answer. Each row is
28
- * just label → value (· unit); an optional `total` pins the sum under a divider.
29
- * Deliberately minimal — two columns, hierarchy from weight and tabular figures.
30
- * The right-hand panel of a lookup / answer surface, the specs of a compared
31
- * option.
32
- */
33
- export function SpecList({ rows, total, dense }: SpecListProps) {
34
- return (
35
- <View>
36
- {rows.map((r, i) => (
37
- <SpecLine key={`${r.label}-${i}`} row={r} dense={dense} />
38
- ))}
39
- {total ? (
40
- <>
41
- <Divider paddingVertical={dense ? 5 : 7} />
42
- <SpecLine row={{ ...total, emphasis: true }} dense={dense} isTotal />
43
- </>
44
- ) : null}
45
- </View>
46
- );
47
- }
48
-
49
- function SpecLine({ row, dense, isTotal }: { row: SpecRow; dense?: boolean; isTotal?: boolean }) {
50
- const { label, value, unit, emphasis } = row;
51
- const display = typeof value === "number" ? value.toLocaleString("en-US") : value;
52
- return (
53
- <View style={[styles.row, { minHeight: dense ? 26 : 32 }]}>
54
- <Text
55
- size={dense ? "xs" : "sm"}
56
- color={isTotal ? "default" : "muted"}
57
- weight={isTotal ? "semibold" : "regular"}
58
- numberOfLines={1}
59
- style={styles.labelText}
60
- >
61
- {label}
62
- </Text>
63
- <View style={styles.valueRow}>
64
- <Text size={dense && !isTotal ? "sm" : "md"} weight={emphasis ? "semibold" : "medium"} tabular>
65
- {display}
66
- </Text>
67
- {unit ? (
68
- <Text size={dense ? "xs" : "sm"} color="muted">
69
- {unit}
70
- </Text>
71
- ) : null}
72
- </View>
73
- </View>
74
- );
75
- }
76
-
77
- const styles = StyleSheet.create({
78
- row: { flexDirection: "row", alignItems: "baseline", justifyContent: "space-between", gap: 16 },
79
- labelText: { flexShrink: 1 },
80
- valueRow: { flexDirection: "row", alignItems: "baseline", gap: 4 },
81
- });
@@ -1,99 +0,0 @@
1
- import { StyleSheet, View } from "react-native";
2
- import { colors } from "./colors";
3
- import { Text } from "./text";
4
- import { Button } from "./button";
5
- import { Badge } from "./badge";
6
- import { Confidence, type ConfidenceLevel } from "./confidence";
7
-
8
- export interface TriageRowProps {
9
- /** The incoming item's headline — a subject, a lead name, a ticket. */
10
- title: string;
11
- /** A one-line preview / snippet of the item. */
12
- preview?: string;
13
- /** Right meta — time, sender, channel. */
14
- meta?: string;
15
- /** The AI's classification, rendered as a neutral badge on the call line. */
16
- category?: { label: string };
17
- /** The AI's suggested action — the specific task to take ("Assign to Nguyen",
18
- * "Apply the payment"). Pairs with `category`, doesn't repeat it. */
19
- suggestedAction?: string;
20
- confidence?: ConfidenceLevel;
21
- confidenceScore?: number;
22
- /** Accept the AI's call — its classification + action. */
23
- onAccept?: () => void;
24
- /** Override — the host opens a reclassify control. */
25
- onOverride?: () => void;
26
- onDismiss?: () => void;
27
- /** Resolved → settles to a quiet outcome line. */
28
- status?: "open" | "accepted" | "dismissed";
29
- }
30
-
31
- /**
32
- * An incoming item the agent has TRIAGED — the item up top (title · preview ·
33
- * time), then the agent's CALL (a classification badge + the suggested action +
34
- * confidence), then the decision (accept / reclassify / dismiss). High-confidence
35
- * items batch-accept from the host. The unit of an AI inbox / intake / routing
36
- * queue. The classification is the agent's; the decision stays the human's.
37
- */
38
- export function TriageRow(props: TriageRowProps) {
39
- const status = props.status ?? "open";
40
- const resolved = status !== "open";
41
- const hasConfidence = props.confidence != null || props.confidenceScore != null;
42
- return (
43
- <View style={[styles.card, resolved ? styles.resolved : null]}>
44
- <View style={styles.titleRow}>
45
- <Text size="sm" weight="semibold" style={{ flex: 1 }} numberOfLines={1}>
46
- {props.title}
47
- </Text>
48
- {props.meta ? (
49
- <Text size="xs" color="muted" numberOfLines={1}>
50
- {props.meta}
51
- </Text>
52
- ) : null}
53
- </View>
54
-
55
- {props.preview ? (
56
- <Text size="sm" color="muted" numberOfLines={1}>
57
- {props.preview}
58
- </Text>
59
- ) : null}
60
-
61
- <View style={styles.callRow}>
62
- {props.category ? <Badge label={props.category.label} /> : null}
63
- {props.suggestedAction ? (
64
- <Text size="sm" weight="medium" numberOfLines={1} style={{ flexShrink: 1 }}>
65
- {props.suggestedAction}
66
- </Text>
67
- ) : null}
68
- {hasConfidence && !resolved ? <Confidence level={props.confidence} score={props.confidenceScore} /> : null}
69
- </View>
70
-
71
- {resolved ? (
72
- <Text size="xs" color="muted" weight="medium">
73
- {status === "accepted" ? "Accepted" : "Dismissed"}
74
- </Text>
75
- ) : (
76
- <View style={styles.footer}>
77
- {props.onOverride ? <Button title="Reclassify" color="muted" onPress={props.onOverride} /> : null}
78
- {props.onDismiss ? <Button title="Dismiss" color="muted" onPress={props.onDismiss} /> : null}
79
- {props.onAccept ? <Button title="Accept" color="primary" onPress={props.onAccept} /> : null}
80
- </View>
81
- )}
82
- </View>
83
- );
84
- }
85
-
86
- const styles = StyleSheet.create({
87
- card: {
88
- borderWidth: 1,
89
- borderColor: colors.border,
90
- backgroundColor: colors.white,
91
- borderRadius: 12,
92
- padding: 16,
93
- gap: 10,
94
- },
95
- resolved: { backgroundColor: colors.zinc[50] },
96
- titleRow: { flexDirection: "row", alignItems: "center", gap: 10 },
97
- callRow: { flexDirection: "row", alignItems: "center", gap: 10 },
98
- footer: { flexDirection: "row", alignItems: "center", justifyContent: "flex-end", gap: 8 },
99
- });