@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.
Files changed (75) hide show
  1. package/AGENTS.md +298 -159
  2. package/examples/tpl_allocate.tsx +2 -2
  3. package/examples/tpl_attendance.tsx +2 -2
  4. package/examples/tpl_calendar.tsx +1 -1
  5. package/examples/tpl_dashboard.tsx +1 -1
  6. package/examples/tpl_dieline.tsx +3 -3
  7. package/examples/tpl_documents.tsx +790 -0
  8. package/examples/tpl_item_list.tsx +1015 -124
  9. package/examples/tpl_lookup.tsx +37 -10
  10. package/examples/tpl_pick.tsx +3 -3
  11. package/examples/tpl_pivot.tsx +1 -1
  12. package/examples/tpl_record.tsx +1354 -0
  13. package/examples/tpl_report.tsx +7 -7
  14. package/examples/tpl_rollup.tsx +6 -6
  15. package/examples/tpl_shifts.tsx +2 -2
  16. package/examples/tpl_statements.tsx +221 -0
  17. package/examples/tpl_stock.tsx +7 -7
  18. package/examples/tpl_task_board.tsx +48 -32
  19. package/examples/tpl_tasks.tsx +47 -47
  20. package/examples/tpl_tower.tsx +2 -2
  21. package/package.json +9 -9
  22. package/src/agent_run.tsx +1 -1
  23. package/src/capture_row.tsx +59 -0
  24. package/src/change_review.tsx +732 -236
  25. package/src/checklist.tsx +104 -0
  26. package/src/chip.tsx +12 -3
  27. package/src/composer.tsx +22 -1
  28. package/src/confidence.tsx +1 -1
  29. package/src/control_surface.ts +0 -14
  30. package/src/detail_row.tsx +137 -10
  31. package/src/finding.tsx +112 -80
  32. package/src/floating_action_bar.tsx +1 -1
  33. package/src/inline_date_picker.tsx +8 -3
  34. package/src/inline_edit.tsx +40 -10
  35. package/src/inline_member_select.tsx +3 -0
  36. package/src/inline_number_input.tsx +5 -2
  37. package/src/inline_select.tsx +8 -3
  38. package/src/inline_tag_select.tsx +140 -0
  39. package/src/inline_text_input.tsx +5 -2
  40. package/src/inline_time_picker.tsx +5 -2
  41. package/src/ledger.tsx +220 -0
  42. package/src/locale.tsx +43 -16
  43. package/src/progress_bar.tsx +32 -1
  44. package/src/record_summary.tsx +101 -0
  45. package/src/section_heading.tsx +16 -8
  46. package/src/sources.tsx +8 -5
  47. package/src/suggestion_chip.tsx +47 -0
  48. package/src/use_section_nav.test.ts +69 -0
  49. package/src/use_section_nav.ts +59 -0
  50. package/examples/tpl_assistant.tsx +0 -174
  51. package/examples/tpl_billing.tsx +0 -344
  52. package/examples/tpl_briefing.tsx +0 -121
  53. package/examples/tpl_compare.tsx +0 -133
  54. package/examples/tpl_crosscheck.tsx +0 -120
  55. package/examples/tpl_detail.tsx +0 -232
  56. package/examples/tpl_directory.tsx +0 -260
  57. package/examples/tpl_draft.tsx +0 -163
  58. package/examples/tpl_extract.tsx +0 -193
  59. package/examples/tpl_intake.tsx +0 -206
  60. package/examples/tpl_match.tsx +0 -134
  61. package/examples/tpl_order.tsx +0 -482
  62. package/examples/tpl_quick.tsx +0 -211
  63. package/examples/tpl_ratedesk.tsx +0 -386
  64. package/examples/tpl_record_plain.tsx +0 -259
  65. package/examples/tpl_settings.tsx +0 -178
  66. package/examples/tpl_timeline.tsx +0 -244
  67. package/examples/tpl_triage.tsx +0 -112
  68. package/examples/tpl_wizard.tsx +0 -223
  69. package/src/discrepancy.tsx +0 -114
  70. package/src/match_sides.tsx +0 -79
  71. package/src/record_fields.tsx +0 -68
  72. package/src/review_card.tsx +0 -172
  73. package/src/scored_option.tsx +0 -138
  74. package/src/spec_list.tsx +0 -81
  75. package/src/triage_row.tsx +0 -99
@@ -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
- });