@lotics/ui 5.6.1 → 5.8.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 +44 -27
- package/examples/tpl_approvals.tsx +10 -10
- package/examples/tpl_crm_desk.tsx +3 -3
- package/examples/tpl_dieline.tsx +7 -6
- package/examples/tpl_directory.tsx +3 -3
- package/examples/tpl_dossier.tsx +7 -7
- package/examples/tpl_draft.tsx +4 -4
- package/examples/tpl_extract.tsx +7 -7
- package/examples/tpl_inventory.tsx +3 -3
- package/examples/tpl_lookup.tsx +4 -3
- package/examples/tpl_match.tsx +1 -1
- package/examples/tpl_ratedesk.tsx +11 -11
- package/examples/tpl_report.tsx +3 -3
- package/examples/tpl_stock.tsx +3 -3
- package/examples/tpl_triage.tsx +1 -1
- package/package.json +12 -3
- package/src/agent_progress.tsx +14 -12
- package/src/agent_run.tsx +248 -86
- package/src/back_button.tsx +3 -2
- package/src/badge.tsx +2 -1
- package/src/button.tsx +11 -32
- package/src/button_colors.ts +23 -0
- package/src/calendar/calendar_view.tsx +2 -2
- package/src/card_select_item.tsx +2 -1
- package/src/change_review.tsx +5 -6
- package/src/{pill_button.tsx → chip.tsx} +7 -7
- package/src/chip_group.tsx +2 -2
- package/src/column_filter.tsx +4 -4
- package/src/combobox.tsx +2 -9
- package/src/composer.tsx +4 -3
- package/src/control_surface.ts +15 -15
- package/src/date_field.tsx +2 -1
- package/src/date_filter.tsx +2 -1
- package/src/date_range_filter_field.tsx +2 -1
- package/src/dialog.tsx +2 -2
- package/src/discrepancy.tsx +1 -1
- package/src/drawer.tsx +1 -2
- package/src/{filter_pill.tsx → filter_chip.tsx} +8 -8
- package/src/finding.tsx +1 -1
- package/src/gantt/gantt_view.tsx +2 -2
- package/src/icon_button.tsx +38 -14
- package/src/inline_edit.tsx +2 -2
- package/src/link_button.tsx +2 -1
- package/src/markdown.css +209 -0
- package/src/markdown.tsx +17 -0
- package/src/markdown.web.tsx +69 -0
- package/src/menu_button.tsx +2 -1
- package/src/menu_list_item.tsx +2 -1
- package/src/number_input.tsx +2 -1
- package/src/picker.tsx +3 -3
- package/src/popover_header.tsx +2 -2
- package/src/primitives_purity.test.ts +13 -7
- package/src/radio_picker.tsx +2 -1
- package/src/range_slider.tsx +1 -1
- package/src/review_card.tsx +2 -2
- package/src/scan_field.tsx +2 -1
- package/src/scored_option.tsx +0 -1
- package/src/search_input.tsx +3 -2
- package/src/segmented_control.tsx +2 -1
- package/src/status_badge.tsx +2 -1
- package/src/stepper.tsx +3 -2
- package/src/switch_button.tsx +2 -1
- package/src/switcher.tsx +3 -2
- package/src/tabs.tsx +2 -1
- package/src/tag_input.tsx +2 -1
- package/src/text_input_field.tsx +2 -1
- package/src/time_field.tsx +2 -1
- package/src/time_picker.tsx +2 -1
- package/src/triage_row.tsx +3 -3
package/examples/tpl_stock.tsx
CHANGED
|
@@ -12,7 +12,7 @@ import { Drawer } from "@lotics/ui/drawer";
|
|
|
12
12
|
import { EmptyState } from "@lotics/ui/empty_state";
|
|
13
13
|
import { KPIStrip } from "@lotics/ui/kpi_strip";
|
|
14
14
|
import { Pagination } from "@lotics/ui/pagination";
|
|
15
|
-
import {
|
|
15
|
+
import { Chip } from "@lotics/ui/chip";
|
|
16
16
|
import { PressableRow } from "@lotics/ui/pressable_row";
|
|
17
17
|
import { SearchInput } from "@lotics/ui/search_input";
|
|
18
18
|
import { formatMoney } from "@lotics/ui/format_money";
|
|
@@ -266,9 +266,9 @@ export function TplStock() {
|
|
|
266
266
|
</CardHeaderTitle>
|
|
267
267
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
|
268
268
|
{chips.map((c) => (
|
|
269
|
-
<
|
|
269
|
+
<Chip key={c.key} onDismiss={c.clear} dismissTooltip="Clear filter">
|
|
270
270
|
<Text size="xs" weight="medium" color="muted">{c.label}</Text>
|
|
271
|
-
</
|
|
271
|
+
</Chip>
|
|
272
272
|
))}
|
|
273
273
|
<View style={{ width: 240 }}>
|
|
274
274
|
<SearchInput
|
package/examples/tpl_triage.tsx
CHANGED
|
@@ -62,7 +62,7 @@ export function TplTriage() {
|
|
|
62
62
|
<Text size="sm" color="muted">The agent classifies and routes each item — accept the call, override, or dismiss</Text>
|
|
63
63
|
</View>
|
|
64
64
|
{tab === "open" && highOpen > 0 ? (
|
|
65
|
-
<Button title={`Accept ${highOpen} high-confidence`} color="secondary"
|
|
65
|
+
<Button title={`Accept ${highOpen} high-confidence`} color="secondary" onPress={acceptAllHigh} />
|
|
66
66
|
) : null}
|
|
67
67
|
</View>
|
|
68
68
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./tokens": "./src/tokens.ts",
|
|
@@ -63,6 +63,11 @@
|
|
|
63
63
|
"./activity_indicator": "./src/activity_indicator.tsx",
|
|
64
64
|
"./agent_run": "./src/agent_run.tsx",
|
|
65
65
|
"./agent_progress": "./src/agent_progress.tsx",
|
|
66
|
+
"./markdown": {
|
|
67
|
+
"react-native": "./src/markdown.tsx",
|
|
68
|
+
"default": "./src/markdown.web.tsx"
|
|
69
|
+
},
|
|
70
|
+
"./markdown.css": "./src/markdown.css",
|
|
66
71
|
"./confidence": "./src/confidence.tsx",
|
|
67
72
|
"./review_card": "./src/review_card.tsx",
|
|
68
73
|
"./change_review": "./src/change_review.tsx",
|
|
@@ -128,8 +133,8 @@
|
|
|
128
133
|
"./radio_picker": "./src/radio_picker.tsx",
|
|
129
134
|
"./form_text_input": "./src/form_text_input.tsx",
|
|
130
135
|
"./form_switch": "./src/form_switch.tsx",
|
|
131
|
-
"./
|
|
132
|
-
"./
|
|
136
|
+
"./chip": "./src/chip.tsx",
|
|
137
|
+
"./filter_chip": "./src/filter_chip.tsx",
|
|
133
138
|
"./range_slider": "./src/range_slider.tsx",
|
|
134
139
|
"./counter": "./src/counter.tsx",
|
|
135
140
|
"./link": "./src/link.tsx",
|
|
@@ -217,6 +222,10 @@
|
|
|
217
222
|
"directory": "packages/ui"
|
|
218
223
|
},
|
|
219
224
|
"license": "MIT",
|
|
225
|
+
"dependencies": {
|
|
226
|
+
"react-markdown": "^10.0.0",
|
|
227
|
+
"remark-gfm": "^4.0.0"
|
|
228
|
+
},
|
|
220
229
|
"peerDependencies": {
|
|
221
230
|
"@lotics/docx": "^0.1.0",
|
|
222
231
|
"@lotics/xlsx": "^0.1.0",
|
package/src/agent_progress.tsx
CHANGED
|
@@ -4,19 +4,20 @@ import { colors } from "./colors";
|
|
|
4
4
|
import { Text } from "./text";
|
|
5
5
|
import { WaveAvatar } from "./wave_avatar";
|
|
6
6
|
import { PressableHighlight } from "./pressable_highlight";
|
|
7
|
-
import { AgentRun, resolveToolMeta, type
|
|
7
|
+
import { AgentRun, resolveToolMeta, type AgentRunItem } from "./agent_run";
|
|
8
8
|
|
|
9
9
|
export interface AgentProgressProps {
|
|
10
|
-
|
|
10
|
+
/** The ordered transcript — same shape `AgentRun` takes. */
|
|
11
|
+
items: AgentRunItem[];
|
|
11
12
|
state?: "streaming" | "done" | "error";
|
|
12
13
|
/** The collapsed-pill label. Defaults to the running step's label (or
|
|
13
14
|
* "Working…" / "Done"). */
|
|
14
15
|
label?: string;
|
|
15
|
-
/** The agent's streamed text — forwarded to the expanded `AgentRun`. */
|
|
16
|
-
text?: string;
|
|
17
16
|
/** Localize / override a tool step's label by raw tool name — applied to both
|
|
18
17
|
* the collapsed pill and the expanded feed. */
|
|
19
18
|
labelForTool?: (toolName: string) => string | undefined;
|
|
19
|
+
/** Localized "{n} steps" suffix, forwarded to the expanded feed. */
|
|
20
|
+
stepsLabel?: (n: number) => string;
|
|
20
21
|
defaultExpanded?: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -29,15 +30,16 @@ export interface AgentProgressProps {
|
|
|
29
30
|
* label, so the surface stays calm until you ask to see the steps.
|
|
30
31
|
*/
|
|
31
32
|
export function AgentProgress(props: AgentProgressProps) {
|
|
32
|
-
const {
|
|
33
|
+
const { items, state = "streaming", label, labelForTool, stepsLabel, defaultExpanded } = props;
|
|
33
34
|
const [expanded, setExpanded] = useState(defaultExpanded ?? false);
|
|
34
35
|
|
|
35
|
-
const running =
|
|
36
|
-
const runningLabel =
|
|
37
|
-
|
|
38
|
-
?
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
const running = [...items].reverse().find((it) => it.type === "step" && it.status === "running");
|
|
37
|
+
const runningLabel =
|
|
38
|
+
running && running.type === "step"
|
|
39
|
+
? running.kind === "tool"
|
|
40
|
+
? resolveToolMeta(running.label, labelForTool).label
|
|
41
|
+
: running.label
|
|
42
|
+
: undefined;
|
|
41
43
|
const compact = label ?? runningLabel ?? (state === "done" ? "Done" : state === "error" ? "Stopped" : "Working…");
|
|
42
44
|
const streaming = state === "streaming";
|
|
43
45
|
|
|
@@ -46,7 +48,7 @@ export function AgentProgress(props: AgentProgressProps) {
|
|
|
46
48
|
{expanded ? (
|
|
47
49
|
<View style={styles.panel}>
|
|
48
50
|
<ScrollView style={{ maxHeight: 260 }} contentContainerStyle={{ padding: 16 }}>
|
|
49
|
-
<AgentRun
|
|
51
|
+
<AgentRun items={items} state={state} labelForTool={labelForTool} stepsLabel={stepsLabel} />
|
|
50
52
|
</ScrollView>
|
|
51
53
|
</View>
|
|
52
54
|
) : null}
|
package/src/agent_run.tsx
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { colors
|
|
1
|
+
import { useState, type ReactNode } from "react";
|
|
2
|
+
import { StyleSheet, View } from "react-native";
|
|
3
|
+
import { colors } from "./colors";
|
|
4
4
|
import { Text } from "./text";
|
|
5
|
-
import { type IconName } from "./icon";
|
|
5
|
+
import { Icon, type IconName } from "./icon";
|
|
6
|
+
import { Markdown } from "./markdown";
|
|
6
7
|
import { Peek } from "./peek";
|
|
7
|
-
import {
|
|
8
|
+
import { PressableHighlight } from "./pressable_highlight";
|
|
9
|
+
import { Marker, type StepStatus } from "./stepper";
|
|
10
|
+
import { AnimationFadeIn } from "./animation_fade_in";
|
|
11
|
+
import { CONTROL_HEIGHT, CONTROL_RADIUS } from "./control_surface";
|
|
8
12
|
|
|
9
13
|
export type AgentStepStatus = "running" | "done" | "error";
|
|
10
14
|
|
|
@@ -14,29 +18,41 @@ export interface AgentRunStep {
|
|
|
14
18
|
* it to a human label via the default map / `labelForTool`. A `step` carries
|
|
15
19
|
* an already-human label. */
|
|
16
20
|
label: string;
|
|
17
|
-
/** A note under the label (
|
|
21
|
+
/** A note under the label — should be REAL tool I/O (an arg, a count, a result
|
|
22
|
+
* summary), never invented prose. Muted. */
|
|
18
23
|
detail?: string;
|
|
19
24
|
status: AgentStepStatus;
|
|
20
25
|
kind?: "step" | "tool";
|
|
21
|
-
/** Optional content shown in a `Peek` popover when the step is
|
|
22
|
-
* glance into what the step did (the tool input, a result summary). */
|
|
26
|
+
/** Optional content shown in a `Peek` popover when the (expanded) step is
|
|
27
|
+
* pressed — glance into what the step did (the tool input, a result summary). */
|
|
23
28
|
peek?: ReactNode;
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
/**
|
|
32
|
+
* The ordered transcript of a run, in the order it happened: prose the agent
|
|
33
|
+
* streams (`text`) interleaved with the tools/steps it runs between prose
|
|
34
|
+
* (`step`). A real run is a timeline — text, a burst of tool calls, more text,
|
|
35
|
+
* more calls — so the feed is an ordered array, NOT a text block over a flat
|
|
36
|
+
* step list. Consecutive `step` items collapse into ONE activity group.
|
|
37
|
+
*/
|
|
38
|
+
export type AgentRunItem =
|
|
39
|
+
| { type: "text"; id: string; text: string }
|
|
40
|
+
| ({ type: "step" } & AgentRunStep);
|
|
41
|
+
|
|
26
42
|
export interface AgentRunProps {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
* pulsing; `done`/`error` settle it. Defaults to `streaming` while a step runs. */
|
|
43
|
+
/** The ordered transcript — text + step items in the order they occurred. */
|
|
44
|
+
items: AgentRunItem[];
|
|
45
|
+
/** Whole-run state. `streaming` keeps the trailing text's caret blinking and
|
|
46
|
+
* the tail activity group pulsing; `done`/`error` settle every group. Defaults
|
|
47
|
+
* to `streaming` while any step is `running`. */
|
|
33
48
|
state?: "streaming" | "done" | "error";
|
|
34
49
|
/** Localize / override a TOOL step's display label by its raw tool name
|
|
35
50
|
* (`update_records` → "Đang cập nhật dữ liệu"). Return `undefined` to fall
|
|
36
51
|
* back to the built-in label. */
|
|
37
52
|
labelForTool?: (toolName: string) => string | undefined;
|
|
38
|
-
/**
|
|
39
|
-
|
|
53
|
+
/** Localized "{n} steps" suffix on a collapsed activity group (the kit is
|
|
54
|
+
* locale-neutral). Default `"{n} steps"`. */
|
|
55
|
+
stepsLabel?: (n: number) => string;
|
|
40
56
|
accessibilityLabel?: string;
|
|
41
57
|
}
|
|
42
58
|
|
|
@@ -79,91 +95,237 @@ export function resolveToolMeta(
|
|
|
79
95
|
};
|
|
80
96
|
}
|
|
81
97
|
|
|
98
|
+
/** Resolve a step's display label — a `tool` maps its raw name, a `step` is
|
|
99
|
+
* already human. */
|
|
100
|
+
function stepLabel(s: AgentRunStep, labelForTool?: (toolName: string) => string | undefined): string {
|
|
101
|
+
return s.kind === "tool" ? resolveToolMeta(s.label, labelForTool).label : s.label;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// A run is a timeline of segments: prose, then a group of consecutive tool/step
|
|
105
|
+
// calls, then more prose. Folding the flat item list into segments is what lets
|
|
106
|
+
// the feed interleave instead of forcing all text to the top.
|
|
107
|
+
type Segment =
|
|
108
|
+
| { kind: "text"; id: string; text: string }
|
|
109
|
+
| { kind: "group"; id: string; steps: AgentRunStep[] };
|
|
110
|
+
|
|
111
|
+
function toSegments(items: AgentRunItem[]): Segment[] {
|
|
112
|
+
const segments: Segment[] = [];
|
|
113
|
+
for (const item of items) {
|
|
114
|
+
if (item.type === "text") {
|
|
115
|
+
segments.push({ kind: "text", id: item.id, text: item.text });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const { type: _t, ...step } = item;
|
|
119
|
+
const tail = segments[segments.length - 1];
|
|
120
|
+
if (tail && tail.kind === "group") {
|
|
121
|
+
segments[segments.length - 1] = { ...tail, steps: [...tail.steps, step] };
|
|
122
|
+
} else {
|
|
123
|
+
segments.push({ kind: "group", id: item.id, steps: [step] });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return segments;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const INK = colors.zinc[700];
|
|
130
|
+
|
|
82
131
|
/**
|
|
83
|
-
* A live feed of an AI agent's work — the prose it streams
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
132
|
+
* A live feed of an AI agent's work as a TIMELINE — the prose it streams and the
|
|
133
|
+
* tools it calls, in the order they happened. While the agent is mid-tools the
|
|
134
|
+
* tail group is a SINGLE pulsing row whose label swaps in place as each call
|
|
135
|
+
* fires (not a growing stack of dots); once prose resumes the group settles into
|
|
136
|
+
* one row — "{final action} · {n} steps" — that EXPANDS on press to the steps in
|
|
137
|
+
* between. Text segments render as prose, the last one carrying a live caret. The
|
|
138
|
+
* run always ends on the agent's text. Pair with `Composer` + `ReviewCard`.
|
|
89
139
|
*/
|
|
90
140
|
export function AgentRun(props: AgentRunProps) {
|
|
91
|
-
const {
|
|
92
|
-
const anyRunning =
|
|
141
|
+
const { items, labelForTool, stepsLabel = (n) => `${n} steps`, accessibilityLabel } = props;
|
|
142
|
+
const anyRunning = items.some((it) => it.type === "step" && it.status === "running");
|
|
93
143
|
const state = props.state ?? (anyRunning ? "streaming" : "done");
|
|
94
144
|
const streaming = state === "streaming";
|
|
95
|
-
const
|
|
145
|
+
const segments = toSegments(items);
|
|
146
|
+
const lastIndex = segments.length - 1;
|
|
96
147
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
{text ? (
|
|
100
|
-
<Text size="sm" style={styles.narration}>
|
|
101
|
-
{text}
|
|
102
|
-
{streaming ? <Caret /> : null}
|
|
103
|
-
</Text>
|
|
104
|
-
) : null}
|
|
148
|
+
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
|
149
|
+
const toggle = (id: string) => setExpanded((e) => ({ ...e, [id]: !e[id] }));
|
|
105
150
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const status: StepStatus =
|
|
111
|
-
s.status === "error"
|
|
112
|
-
? "warning"
|
|
113
|
-
: s.status === "running" || (streaming && i === last)
|
|
114
|
-
? "current"
|
|
115
|
-
: "done";
|
|
116
|
-
const label = s.kind === "tool" ? resolveToolMeta(s.label, labelForTool).label : s.label;
|
|
117
|
-
const body = (
|
|
118
|
-
<View style={{ gap: 1 }}>
|
|
119
|
-
<Text size="sm">{label}</Text>
|
|
120
|
-
{s.detail ? (
|
|
121
|
-
<Text size="xs" color={s.status === "error" ? "danger" : "muted"}>
|
|
122
|
-
{s.detail}
|
|
123
|
-
</Text>
|
|
124
|
-
) : null}
|
|
125
|
-
</View>
|
|
126
|
-
);
|
|
151
|
+
return (
|
|
152
|
+
<View accessibilityLabel={accessibilityLabel} style={{ gap: 10 }}>
|
|
153
|
+
{segments.map((seg, i) => {
|
|
154
|
+
if (seg.kind === "text") {
|
|
127
155
|
return (
|
|
128
|
-
<
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
{body}
|
|
132
|
-
</Peek>
|
|
133
|
-
) : (
|
|
134
|
-
body
|
|
135
|
-
)}
|
|
136
|
-
</Step>
|
|
156
|
+
<View key={seg.id} style={styles.narration}>
|
|
157
|
+
<Markdown>{streaming && i === lastIndex ? `${seg.text} ▍` : seg.text}</Markdown>
|
|
158
|
+
</View>
|
|
137
159
|
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
160
|
+
}
|
|
161
|
+
return (
|
|
162
|
+
<ToolGroup
|
|
163
|
+
key={seg.id}
|
|
164
|
+
steps={seg.steps}
|
|
165
|
+
active={streaming && i === lastIndex}
|
|
166
|
+
expanded={!!expanded[seg.id]}
|
|
167
|
+
onToggle={() => toggle(seg.id)}
|
|
168
|
+
labelForTool={labelForTool}
|
|
169
|
+
stepsLabel={stepsLabel}
|
|
170
|
+
/>
|
|
171
|
+
);
|
|
172
|
+
})}
|
|
147
173
|
</View>
|
|
148
174
|
);
|
|
149
175
|
}
|
|
150
176
|
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
177
|
+
// One activity row: [dot] [body] … [chevron], dot vertically centred to the body,
|
|
178
|
+
// the whole row a CONTROL_HEIGHT pressable when collapsible (no indent — every row
|
|
179
|
+
// shares the same left edge, collapsed or expanded).
|
|
180
|
+
function ActivityRow(props: {
|
|
181
|
+
markerStatus: StepStatus;
|
|
182
|
+
live?: boolean;
|
|
183
|
+
onPress?: () => void;
|
|
184
|
+
accessibilityLabel?: string;
|
|
185
|
+
trailing?: ReactNode;
|
|
186
|
+
children: ReactNode;
|
|
187
|
+
}) {
|
|
188
|
+
const { markerStatus, live, onPress, accessibilityLabel, trailing, children } = props;
|
|
189
|
+
const inner = (
|
|
190
|
+
<>
|
|
191
|
+
<View style={styles.dotCol}>
|
|
192
|
+
<Marker status={markerStatus} color={INK} live={!!live} />
|
|
193
|
+
</View>
|
|
194
|
+
<View style={styles.rowBody}>{children}</View>
|
|
195
|
+
{trailing ?? null}
|
|
196
|
+
</>
|
|
197
|
+
);
|
|
198
|
+
if (onPress) {
|
|
199
|
+
return (
|
|
200
|
+
<PressableHighlight onPress={onPress} accessibilityRole="button" accessibilityLabel={accessibilityLabel} style={styles.row}>
|
|
201
|
+
{inner}
|
|
202
|
+
</PressableHighlight>
|
|
160
203
|
);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
204
|
+
}
|
|
205
|
+
return <View style={styles.row}>{inner}</View>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function StepBody({ label, detail, status }: { label: string; detail?: string; status: AgentStepStatus }) {
|
|
209
|
+
return (
|
|
210
|
+
<View style={{ gap: 1 }}>
|
|
211
|
+
<Text size="sm">{label}</Text>
|
|
212
|
+
{detail ? (
|
|
213
|
+
<Text size="xs" color={status === "error" ? "danger" : "muted"}>
|
|
214
|
+
{detail}
|
|
215
|
+
</Text>
|
|
216
|
+
) : null}
|
|
217
|
+
</View>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function SummaryLabel({ label, count, stepsLabel }: { label: string; count: number; stepsLabel: (n: number) => string }) {
|
|
222
|
+
return (
|
|
223
|
+
<Text size="sm" weight="medium" numberOfLines={1}>
|
|
224
|
+
{label}
|
|
225
|
+
<Text size="sm" color="muted">
|
|
226
|
+
{" · " + stepsLabel(count)}
|
|
227
|
+
</Text>
|
|
228
|
+
</Text>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function chevron(dir: "down" | "up") {
|
|
233
|
+
return <Icon name={dir === "down" ? "chevron-down" : "chevron-up"} size={16} color={colors.zinc[400]} />;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function ToolGroup(props: {
|
|
237
|
+
steps: AgentRunStep[];
|
|
238
|
+
active: boolean;
|
|
239
|
+
expanded: boolean;
|
|
240
|
+
onToggle: () => void;
|
|
241
|
+
labelForTool?: (toolName: string) => string | undefined;
|
|
242
|
+
stepsLabel: (n: number) => string;
|
|
243
|
+
}) {
|
|
244
|
+
const { steps, active, expanded, onToggle, labelForTool, stepsLabel } = props;
|
|
245
|
+
const resolve = (s: AgentRunStep) => stepLabel(s, labelForTool);
|
|
246
|
+
|
|
247
|
+
// ACTIVE — one pulsing row whose label swaps in place as each call fires (the
|
|
248
|
+
// label is keyed by the current step's id, so a new call rises + fades into the
|
|
249
|
+
// SAME row instead of stacking a new dot).
|
|
250
|
+
if (active) {
|
|
251
|
+
const current = steps[steps.length - 1];
|
|
252
|
+
return (
|
|
253
|
+
<View style={styles.group}>
|
|
254
|
+
<ActivityRow markerStatus="current" live>
|
|
255
|
+
<AnimationFadeIn key={current.id} translateY={4}>
|
|
256
|
+
<StepBody label={resolve(current)} detail={current.detail} status={current.status} />
|
|
257
|
+
</AnimationFadeIn>
|
|
258
|
+
</ActivityRow>
|
|
259
|
+
</View>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const errored = steps.some((s) => s.status === "error");
|
|
264
|
+
const final = steps[steps.length - 1];
|
|
265
|
+
const expandable = steps.length > 1;
|
|
266
|
+
|
|
267
|
+
// SETTLED, single call — just the one done/warning row (nothing to expand).
|
|
268
|
+
if (!expandable) {
|
|
269
|
+
return (
|
|
270
|
+
<View style={styles.group}>
|
|
271
|
+
<ActivityRow markerStatus={final.status === "error" ? "warning" : "done"}>
|
|
272
|
+
{final.peek ? (
|
|
273
|
+
<Peek accessibilityLabel={resolve(final)} content={final.peek}>
|
|
274
|
+
<StepBody label={resolve(final)} detail={final.detail} status={final.status} />
|
|
275
|
+
</Peek>
|
|
276
|
+
) : (
|
|
277
|
+
<StepBody label={resolve(final)} detail={final.detail} status={final.status} />
|
|
278
|
+
)}
|
|
279
|
+
</ActivityRow>
|
|
280
|
+
</View>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// SETTLED, expandable — a persistent "{final action} · {n} steps" HEADER that
|
|
285
|
+
// STAYS PUT and rolls the steps out BELOW it (in order) on press, so the row you
|
|
286
|
+
// pressed never moves. The header wears the `complete` terminal dot — an outlined
|
|
287
|
+
// ring + check, a differentiator from the filled `done` step dots; every row
|
|
288
|
+
// shares one left edge (no indent).
|
|
289
|
+
return (
|
|
290
|
+
<View style={styles.group}>
|
|
291
|
+
<ActivityRow
|
|
292
|
+
markerStatus={errored ? "warning" : "complete"}
|
|
293
|
+
onPress={onToggle}
|
|
294
|
+
accessibilityLabel={`${resolve(final)} — ${stepsLabel(steps.length)}`}
|
|
295
|
+
trailing={chevron(expanded ? "up" : "down")}
|
|
296
|
+
>
|
|
297
|
+
<SummaryLabel label={resolve(final)} count={steps.length} stepsLabel={stepsLabel} />
|
|
298
|
+
</ActivityRow>
|
|
299
|
+
{expanded
|
|
300
|
+
? steps.map((s) => (
|
|
301
|
+
<AnimationFadeIn key={s.id} translateY={4}>
|
|
302
|
+
<ActivityRow markerStatus={s.status === "error" ? "warning" : "done"}>
|
|
303
|
+
{s.peek ? (
|
|
304
|
+
<Peek accessibilityLabel={resolve(s)} content={s.peek}>
|
|
305
|
+
<StepBody label={resolve(s)} detail={s.detail} status={s.status} />
|
|
306
|
+
</Peek>
|
|
307
|
+
) : (
|
|
308
|
+
<StepBody label={resolve(s)} detail={s.detail} status={s.status} />
|
|
309
|
+
)}
|
|
310
|
+
</ActivityRow>
|
|
311
|
+
</AnimationFadeIn>
|
|
312
|
+
))
|
|
313
|
+
: null}
|
|
314
|
+
</View>
|
|
315
|
+
);
|
|
165
316
|
}
|
|
166
317
|
|
|
167
318
|
const styles = StyleSheet.create({
|
|
168
|
-
narration: {
|
|
319
|
+
narration: { paddingHorizontal: 8 },
|
|
320
|
+
group: { gap: 2 },
|
|
321
|
+
row: {
|
|
322
|
+
flexDirection: "row",
|
|
323
|
+
alignItems: "center",
|
|
324
|
+
gap: 10,
|
|
325
|
+
minHeight: CONTROL_HEIGHT,
|
|
326
|
+
paddingHorizontal: 8,
|
|
327
|
+
borderRadius: CONTROL_RADIUS,
|
|
328
|
+
},
|
|
329
|
+
dotCol: { width: 18, alignItems: "center" },
|
|
330
|
+
rowBody: { flex: 1 },
|
|
169
331
|
});
|
package/src/back_button.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IconButton } from "./icon_button";
|
|
2
2
|
import { View } from "react-native";
|
|
3
3
|
|
|
4
4
|
interface BackButtonProps {
|
|
@@ -11,8 +11,9 @@ export function BackButton(props: BackButtonProps) {
|
|
|
11
11
|
const { onPress, accessibilityLabel = "Back" } = props;
|
|
12
12
|
return (
|
|
13
13
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
14
|
-
<
|
|
14
|
+
<IconButton
|
|
15
15
|
icon="chevron-left"
|
|
16
|
+
size="lg"
|
|
16
17
|
color="secondary"
|
|
17
18
|
accessibilityLabel={accessibilityLabel}
|
|
18
19
|
onPress={onPress}
|
package/src/badge.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { View, StyleSheet, StyleProp, ViewStyle } from "react-native";
|
|
3
3
|
import { Text } from "./text";
|
|
4
4
|
import { colors, type ColorName } from "./colors";
|
|
5
|
+
import { CONTROL_RADIUS } from "./control_surface";
|
|
5
6
|
import { useTooltip } from "./tooltip";
|
|
6
7
|
|
|
7
8
|
interface BadgeProps {
|
|
@@ -62,7 +63,7 @@ export function Badge(props: BadgeProps) {
|
|
|
62
63
|
|
|
63
64
|
const styles = StyleSheet.create({
|
|
64
65
|
base: {
|
|
65
|
-
borderRadius:
|
|
66
|
+
borderRadius: CONTROL_RADIUS,
|
|
66
67
|
paddingVertical: 4,
|
|
67
68
|
paddingHorizontal: 8,
|
|
68
69
|
flexDirection: "row",
|
package/src/button.tsx
CHANGED
|
@@ -9,10 +9,12 @@ import {
|
|
|
9
9
|
} from "react-native";
|
|
10
10
|
import { Text } from "./text";
|
|
11
11
|
import { colors } from "./colors";
|
|
12
|
+
import { CONTROL_RADIUS } from "./control_surface";
|
|
12
13
|
import { Icon, IconName } from "./icon";
|
|
13
14
|
import { Ref, useCallback } from "react";
|
|
14
15
|
import { TextColor } from "./text_utils";
|
|
15
16
|
import { useTooltip, UseTooltipOptions } from "./tooltip";
|
|
17
|
+
import { getButtonIconColor } from "./button_colors";
|
|
16
18
|
export type ButtonColor = "primary" | "secondary" | "danger" | "muted" | "danger-secondary";
|
|
17
19
|
export type ButtonIconPosition = "left" | "right";
|
|
18
20
|
|
|
@@ -22,10 +24,6 @@ interface ButtonPropsBase {
|
|
|
22
24
|
icon?: IconName;
|
|
23
25
|
alignSelf?: "flex-start" | "flex-end" | "center" | "stretch" | "auto";
|
|
24
26
|
color?: ButtonColor;
|
|
25
|
-
/** Corner shape. `pill` (default) is the fully-rounded capsule — keep it for
|
|
26
|
-
* icon-only and toolbar buttons, and to preserve existing screens. `rounded`
|
|
27
|
-
* is a 10px radius — use it for labelled CTAs / action buttons. */
|
|
28
|
-
shape?: "pill" | "rounded";
|
|
29
27
|
loading?: boolean;
|
|
30
28
|
disabled?: boolean;
|
|
31
29
|
tooltip?: string | UseTooltipOptions;
|
|
@@ -34,17 +32,15 @@ interface ButtonPropsBase {
|
|
|
34
32
|
}
|
|
35
33
|
|
|
36
34
|
/**
|
|
37
|
-
* A
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* runtime derives `accessibilityLabel` in that precedence order.
|
|
35
|
+
* A `Button` is a LABELLED action — `title` is MANDATORY and is its accessible
|
|
36
|
+
* name. For an icon-only affordance use `IconButton` (circular); the compiler
|
|
37
|
+
* blocks a title-less Button so icon-only actions never render as a rounded box.
|
|
41
38
|
*/
|
|
42
|
-
export type ButtonProps = ButtonPropsBase &
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
);
|
|
39
|
+
export type ButtonProps = ButtonPropsBase & {
|
|
40
|
+
title: string;
|
|
41
|
+
accessibilityLabel?: string;
|
|
42
|
+
tooltip?: string | UseTooltipOptions;
|
|
43
|
+
};
|
|
48
44
|
|
|
49
45
|
export function Button(props: ButtonProps) {
|
|
50
46
|
const {
|
|
@@ -53,7 +49,6 @@ export function Button(props: ButtonProps) {
|
|
|
53
49
|
alignSelf,
|
|
54
50
|
title,
|
|
55
51
|
color,
|
|
56
|
-
shape = "pill",
|
|
57
52
|
style,
|
|
58
53
|
disabled,
|
|
59
54
|
tooltip,
|
|
@@ -117,7 +112,7 @@ export function Button(props: ButtonProps) {
|
|
|
117
112
|
const restPrimary = isPrimary && !pressed && !hovered;
|
|
118
113
|
return [
|
|
119
114
|
{
|
|
120
|
-
borderRadius:
|
|
115
|
+
borderRadius: CONTROL_RADIUS,
|
|
121
116
|
backgroundColor: disabled
|
|
122
117
|
? getButtonDisabledBackgroundColor(color)
|
|
123
118
|
: pressed
|
|
@@ -145,22 +140,6 @@ export function Button(props: ButtonProps) {
|
|
|
145
140
|
);
|
|
146
141
|
}
|
|
147
142
|
|
|
148
|
-
export function getButtonIconColor(color?: ButtonColor, disabled?: boolean): string {
|
|
149
|
-
switch (color) {
|
|
150
|
-
case "primary":
|
|
151
|
-
return colors.white;
|
|
152
|
-
case "secondary":
|
|
153
|
-
return colors.zinc["900"];
|
|
154
|
-
case "danger":
|
|
155
|
-
return colors.red["900"];
|
|
156
|
-
case "danger-secondary":
|
|
157
|
-
return colors.red["900"];
|
|
158
|
-
case "muted":
|
|
159
|
-
return colors.zinc["600"];
|
|
160
|
-
default:
|
|
161
|
-
return disabled ? colors.zinc["300"] : colors.zinc["950"];
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
143
|
|
|
165
144
|
export function getButtonTextColor(color?: ButtonColor, disabled?: boolean): TextColor {
|
|
166
145
|
switch (color) {
|