@lotics/ui 4.4.0 → 4.6.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,10 +1,8 @@
1
1
  import { ReactNode } from "react";
2
2
  import { StyleSheet, View } from "react-native";
3
- import { colors, solid, tint } from "./colors";
3
+ import { colors } from "./colors";
4
4
  import { Text } from "./text";
5
- import { Icon } from "./icon";
6
5
  import { Button } from "./button";
7
- import { LinkButton } from "./link_button";
8
6
  import { Confidence, type ConfidenceLevel } from "./confidence";
9
7
 
10
8
  export interface SuggestionProps {
@@ -28,57 +26,50 @@ export interface SuggestionProps {
28
26
 
29
27
  /**
30
28
  * The core review-for-approval surface: an AI proposal the human accepts,
31
- * edits, or dismisses — never auto-applied. The violet sparkle marks it as
32
- * AI-proposed; a `Confidence` chip says how sure; the body shows the proposed
33
- * value. The decision is the human's. For a shortlist, stack several (rank by
34
- * confidence, the top one first). Pair with `AgentRun` (what produced it) and
35
- * `ChangeReview` (when the proposal is an edit to existing state).
29
+ * edits, or dismisses — never auto-applied. A "PROPOSED" eyebrow marks it as
30
+ * the agent's, a `Confidence` meter says how sure, and the rationale is set as a
31
+ * left-ruled margin note the machine's reasoning, quoted and kept apart from
32
+ * the facts and your controls. The decision is the human's. For a shortlist,
33
+ * stack several (rank by confidence, the top one first). Pair with `AgentRun`
34
+ * (what produced it) and `ChangeReview` (when the proposal edits existing state).
36
35
  */
37
36
  export function Suggestion(props: SuggestionProps) {
38
37
  const status = props.status ?? "open";
39
38
  const hasConfidence = props.confidence != null || props.confidenceScore != null;
40
39
  return (
41
40
  <View style={[styles.card, status !== "open" ? styles.resolved : null]}>
42
- <View style={styles.header}>
43
- <Icon name="sparkles" size={14} color={solid("violet")} />
44
- <Text size="sm" weight="semibold" style={{ flex: 1 }} numberOfLines={2}>
45
- {props.title}
41
+ <View style={styles.eyebrow}>
42
+ <Text size="xs" color="muted" weight="medium" style={{ flex: 1 }}>
43
+ Proposed
46
44
  </Text>
47
- {status === "open" && hasConfidence ? (
48
- <Confidence level={props.confidence} score={props.confidenceScore} />
49
- ) : null}
45
+ {status === "open" && hasConfidence ? <Confidence level={props.confidence} score={props.confidenceScore} /> : null}
50
46
  </View>
51
47
 
48
+ <Text size="md" weight="semibold" numberOfLines={2}>
49
+ {props.title}
50
+ </Text>
51
+
52
52
  {props.rationale ? (
53
- <Text size="xs" color="muted">
54
- {props.rationale}
55
- </Text>
53
+ <View style={styles.note}>
54
+ <Text size="sm" color="muted">
55
+ {props.rationale}
56
+ </Text>
57
+ </View>
56
58
  ) : null}
57
59
 
58
60
  {props.children ? <View>{props.children}</View> : null}
59
61
 
60
62
  {status !== "open" ? (
61
- <View style={styles.outcome}>
62
- <Icon
63
- name={status === "accepted" ? "circle-check" : "x"}
64
- size={14}
65
- color={status === "accepted" ? solid("emerald") : colors.zinc[400]}
66
- />
67
- <Text size="xs" color="muted">
68
- {status === "accepted" ? "Accepted" : "Dismissed"}
69
- </Text>
70
- </View>
63
+ <Text size="xs" color="muted" weight="medium">
64
+ {status === "accepted" ? "Accepted" : "Dismissed"}
65
+ </Text>
71
66
  ) : (
72
67
  <View style={styles.footer}>
73
- {props.onDismiss ? <LinkButton title="Dismiss" onPress={props.onDismiss} /> : <View />}
74
- <View style={styles.actions}>
75
- {props.onEdit ? (
76
- <Button title="Edit" color="secondary" shape="rounded" icon="square-pen" onPress={props.onEdit} />
77
- ) : null}
78
- {props.onAccept ? (
79
- <Button title={props.acceptLabel ?? "Accept"} color="primary" shape="rounded" onPress={props.onAccept} />
80
- ) : null}
81
- </View>
68
+ {props.onDismiss ? <Button title="Dismiss" color="muted" shape="rounded" onPress={props.onDismiss} /> : null}
69
+ {props.onEdit ? <Button title="Edit" color="secondary" shape="rounded" onPress={props.onEdit} /> : null}
70
+ {props.onAccept ? (
71
+ <Button title={props.acceptLabel ?? "Accept"} color="primary" shape="rounded" onPress={props.onAccept} />
72
+ ) : null}
82
73
  </View>
83
74
  )}
84
75
  </View>
@@ -88,15 +79,14 @@ export function Suggestion(props: SuggestionProps) {
88
79
  const styles = StyleSheet.create({
89
80
  card: {
90
81
  borderWidth: 1,
91
- borderColor: tint("violet", 0.35),
82
+ borderColor: colors.border,
92
83
  backgroundColor: colors.white,
93
84
  borderRadius: 12,
94
- padding: 14,
95
- gap: 10,
85
+ padding: 16,
86
+ gap: 12,
96
87
  },
97
- resolved: { borderColor: colors.border, backgroundColor: colors.zinc[50] },
98
- header: { flexDirection: "row", alignItems: "center", gap: 8 },
99
- outcome: { flexDirection: "row", alignItems: "center", gap: 6 },
100
- footer: { flexDirection: "row", alignItems: "center", justifyContent: "space-between" },
101
- actions: { flexDirection: "row", alignItems: "center", gap: 8 },
88
+ resolved: { backgroundColor: colors.zinc[50] },
89
+ eyebrow: { flexDirection: "row", alignItems: "center", gap: 8 },
90
+ note: { borderLeftWidth: 2, borderLeftColor: colors.zinc[300], paddingLeft: 12 },
91
+ footer: { flexDirection: "row", alignItems: "center", justifyContent: "flex-end", gap: 8 },
102
92
  });
@@ -10,7 +10,6 @@ import {
10
10
  fieldOrder,
11
11
  from12h,
12
12
  getTimeLayout,
13
- placeholderFor,
14
13
  to12h,
15
14
  } from "./date_segments";
16
15
 
@@ -119,13 +118,13 @@ export function TimeField(props: TimeFieldProps) {
119
118
  "hour" | "minute" | "dayPeriod",
120
119
  { options: Option[]; onSelect: (v: string) => void; label: string; width: number }
121
120
  > = {
122
- hour: { options: hourOptions, onSelect: setHour, label: segmentLabels.hour, width: 58 },
123
- minute: { options: minuteOptions, onSelect: setMinute, label: segmentLabels.minute, width: 58 },
121
+ hour: { options: hourOptions, onSelect: setHour, label: segmentLabels.hour, width: 78 },
122
+ minute: { options: minuteOptions, onSelect: setMinute, label: segmentLabels.minute, width: 78 },
124
123
  dayPeriod: {
125
124
  options: periodOptions,
126
125
  onSelect: setPeriod,
127
126
  label: segmentLabels.dayPeriod,
128
- width: 66,
127
+ width: 78,
129
128
  },
130
129
  };
131
130
 
@@ -142,7 +141,7 @@ export function TimeField(props: TimeFieldProps) {
142
141
  options={c.options}
143
142
  onSelect={c.onSelect}
144
143
  accessibilityLabel={c.label}
145
- placeholder={placeholderFor(type)}
144
+ placeholder={c.label}
146
145
  disabled={disabled}
147
146
  width={c.width}
148
147
  />
@@ -0,0 +1,99 @@
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" shape="rounded" onPress={props.onOverride} /> : null}
78
+ {props.onDismiss ? <Button title="Dismiss" color="muted" shape="rounded" onPress={props.onDismiss} /> : null}
79
+ {props.onAccept ? <Button title="Accept" color="primary" shape="rounded" 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
+ });
@@ -1,93 +0,0 @@
1
- import { StyleSheet, View } from "react-native";
2
- import { solid, tint } from "./colors";
3
- import { Text } from "./text";
4
- import { Icon } from "./icon";
5
- import { IconButton } from "./icon_button";
6
- import { LinkButton } from "./link_button";
7
- import { Confidence, type ConfidenceLevel } from "./confidence";
8
-
9
- export interface AssistedFieldProps {
10
- label: string;
11
- /** The current value — an AI estimate until confirmed, or the final value. */
12
- value: string;
13
- /** Unit suffix shown after the value (mm, cm, kg). */
14
- unit?: string;
15
- confidence?: ConfidenceLevel;
16
- confidenceScore?: number;
17
- /** True while the value is still an unconfirmed AI estimate — it reads
18
- * amber-tinted with a Confidence chip and a Confirm action (visible
19
- * provenance). Confirm or override flips it to a plain confirmed value. */
20
- estimated?: boolean;
21
- onConfirm?: () => void;
22
- /** Override — the host swaps in its own inline input to edit the value. */
23
- onEdit?: () => void;
24
- }
25
-
26
- /**
27
- * A form value the AI pre-filled — shown as a tinted ESTIMATE the human
28
- * confirms (→ plain, checked) or overrides, never silently committed. The
29
- * provenance is always visible: an estimate looks different from a value a
30
- * person stood behind. Pair with `Confidence`; for a whole extracted record,
31
- * stack several and add one "Confirm all" at the form level.
32
- */
33
- export function AssistedField(props: AssistedFieldProps) {
34
- const { label, value, unit, estimated, confidence, confidenceScore, onConfirm, onEdit } = props;
35
- const display = unit ? `${value} ${unit}` : value;
36
- return (
37
- <View style={styles.row}>
38
- <Text size="sm" color="muted" style={styles.label}>
39
- {label}
40
- </Text>
41
- <View style={styles.valueCol}>
42
- <View style={styles.valueRow}>
43
- {estimated ? (
44
- <View style={styles.estimatePill}>
45
- <Icon name="sparkles" size={11} color={solid("violet")} />
46
- <Text size="sm" weight="medium" tabular>
47
- {display}
48
- </Text>
49
- </View>
50
- ) : (
51
- <View style={styles.confirmedRow}>
52
- <Text size="sm" weight="medium" tabular>
53
- {display}
54
- </Text>
55
- <Icon name="circle-check" size={13} color={solid("emerald")} />
56
- </View>
57
- )}
58
- {onEdit ? (
59
- <IconButton icon="square-pen" size="sm" color="none" accessibilityLabel={`Edit ${label}`} onPress={onEdit} />
60
- ) : null}
61
- </View>
62
- {estimated ? (
63
- <View style={styles.estimateMeta}>
64
- {confidence != null || confidenceScore != null ? (
65
- <Confidence level={confidence} score={confidenceScore} />
66
- ) : null}
67
- {onConfirm ? <LinkButton title="Confirm" onPress={onConfirm} /> : null}
68
- </View>
69
- ) : null}
70
- </View>
71
- </View>
72
- );
73
- }
74
-
75
- const styles = StyleSheet.create({
76
- row: { flexDirection: "row", alignItems: "flex-start", gap: 16, paddingVertical: 8 },
77
- label: { width: 132, paddingTop: 6 },
78
- valueCol: { flex: 1, gap: 6, alignItems: "flex-end" },
79
- valueRow: { flexDirection: "row", alignItems: "center", gap: 6 },
80
- estimatePill: {
81
- flexDirection: "row",
82
- alignItems: "center",
83
- gap: 6,
84
- paddingHorizontal: 10,
85
- paddingVertical: 5,
86
- borderRadius: 8,
87
- backgroundColor: tint("amber", 0.12),
88
- borderWidth: 1,
89
- borderColor: tint("amber", 0.35),
90
- },
91
- confirmedRow: { flexDirection: "row", alignItems: "center", gap: 6, paddingVertical: 5 },
92
- estimateMeta: { flexDirection: "row", alignItems: "center", gap: 10 },
93
- });