@lotics/ui 5.7.0 → 5.9.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 +102 -42
- package/examples/tpl_approvals.tsx +10 -10
- package/examples/tpl_convert.tsx +11 -23
- 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_order.tsx +1 -1
- package/examples/tpl_ratedesk.tsx +12 -12
- package/examples/tpl_record.tsx +1 -1
- 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 -4
- package/src/agent_progress.tsx +14 -12
- package/src/agent_run.tsx +253 -66
- 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/file_gallery_modal.tsx +220 -133
- package/src/file_grid.tsx +184 -0
- package/src/file_preview.web.tsx +329 -6
- package/src/file_preview_types.ts +40 -0
- package/src/file_row.tsx +114 -0
- package/src/file_rows.tsx +105 -0
- package/src/file_thumbnail.tsx +121 -79
- package/src/file_thumbnail_grid.tsx +130 -55
- 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 +49 -14
- package/src/image_gallery.tsx +0 -4
- package/src/inline_edit.tsx +2 -2
- package/src/link_button.tsx +2 -1
- package/src/menu_button.tsx +2 -1
- package/src/menu_list_item.tsx +2 -1
- package/src/number_input.tsx +2 -1
- package/src/pdfjs_worker.d.ts +5 -0
- package/src/picker.tsx +3 -3
- package/src/popover_header.tsx +2 -2
- 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/src/uploading_thumbnail.tsx +212 -0
- package/src/hover_action.tsx +0 -54
package/src/agent_run.tsx
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { type ReactNode } from "react";
|
|
2
|
-
import { View } from "react-native";
|
|
1
|
+
import { useState, type ReactNode } from "react";
|
|
2
|
+
import { StyleSheet, View } from "react-native";
|
|
3
|
+
import { colors } from "./colors";
|
|
3
4
|
import { Text } from "./text";
|
|
4
|
-
import { type IconName } from "./icon";
|
|
5
|
+
import { Icon, type IconName } from "./icon";
|
|
5
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,66 +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;
|
|
147
|
+
|
|
148
|
+
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
|
149
|
+
const toggle = (id: string) => setExpanded((e) => ({ ...e, [id]: !e[id] }));
|
|
96
150
|
|
|
97
151
|
return (
|
|
98
|
-
<View accessibilityLabel={accessibilityLabel} style={{ gap:
|
|
99
|
-
{
|
|
100
|
-
|
|
101
|
-
<Stepper orientation="vertical" live={streaming}>
|
|
102
|
-
{steps.map((s, i) => {
|
|
103
|
-
// Tools the SDK feeds arrive already "done"; show the most recent as
|
|
104
|
-
// the pulsing current while the run is still live.
|
|
105
|
-
const status: StepStatus =
|
|
106
|
-
s.status === "error"
|
|
107
|
-
? "warning"
|
|
108
|
-
: s.status === "running" || (streaming && i === last)
|
|
109
|
-
? "current"
|
|
110
|
-
: "done";
|
|
111
|
-
const label = s.kind === "tool" ? resolveToolMeta(s.label, labelForTool).label : s.label;
|
|
112
|
-
const body = (
|
|
113
|
-
<View style={{ gap: 1 }}>
|
|
114
|
-
<Text size="sm">{label}</Text>
|
|
115
|
-
{s.detail ? (
|
|
116
|
-
<Text size="xs" color={s.status === "error" ? "danger" : "muted"}>
|
|
117
|
-
{s.detail}
|
|
118
|
-
</Text>
|
|
119
|
-
) : null}
|
|
120
|
-
</View>
|
|
121
|
-
);
|
|
152
|
+
<View accessibilityLabel={accessibilityLabel} style={{ gap: 10 }}>
|
|
153
|
+
{segments.map((seg, i) => {
|
|
154
|
+
if (seg.kind === "text") {
|
|
122
155
|
return (
|
|
123
|
-
<
|
|
124
|
-
{
|
|
125
|
-
|
|
126
|
-
{body}
|
|
127
|
-
</Peek>
|
|
128
|
-
) : (
|
|
129
|
-
body
|
|
130
|
-
)}
|
|
131
|
-
</Step>
|
|
156
|
+
<View key={seg.id} style={styles.narration}>
|
|
157
|
+
<Markdown>{streaming && i === lastIndex ? `${seg.text} ▍` : seg.text}</Markdown>
|
|
158
|
+
</View>
|
|
132
159
|
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
})}
|
|
142
173
|
</View>
|
|
143
174
|
);
|
|
144
175
|
}
|
|
176
|
+
|
|
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>
|
|
203
|
+
);
|
|
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
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const styles = StyleSheet.create({
|
|
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 },
|
|
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) {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ButtonColor } from "./button";
|
|
2
|
+
import { colors } from "./colors";
|
|
3
|
+
|
|
4
|
+
/** The icon/glyph ink for each button colour — shared by `Button` and
|
|
5
|
+
* `IconButton` so an icon-only action migrated to `IconButton` keeps the EXACT
|
|
6
|
+
* same colour. Lives in its own module (not `button.tsx`) so a test that mocks
|
|
7
|
+
* `@lotics/ui/button` can't leave `IconButton` without it. */
|
|
8
|
+
export function getButtonIconColor(color?: ButtonColor, disabled?: boolean): string {
|
|
9
|
+
switch (color) {
|
|
10
|
+
case "primary":
|
|
11
|
+
return colors.white;
|
|
12
|
+
case "secondary":
|
|
13
|
+
return colors.zinc["900"];
|
|
14
|
+
case "danger":
|
|
15
|
+
return colors.red["900"];
|
|
16
|
+
case "danger-secondary":
|
|
17
|
+
return colors.red["900"];
|
|
18
|
+
case "muted":
|
|
19
|
+
return colors.zinc["600"];
|
|
20
|
+
default:
|
|
21
|
+
return disabled ? colors.zinc["300"] : colors.zinc["950"];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -125,7 +125,7 @@ const styles = StyleSheet.create({
|
|
|
125
125
|
iconBtn: { width: 30, height: 30, borderRadius: 6, alignItems: "center", justifyContent: "center" },
|
|
126
126
|
todayBtn: { paddingHorizontal: 12, height: 30, borderRadius: 6, borderWidth: 1, borderColor: colors.border, alignItems: "center", justifyContent: "center" },
|
|
127
127
|
title: { flex: 1, textAlign: "center" },
|
|
128
|
-
viewSwitch: { flexDirection: "row", backgroundColor: colors.zinc[100], borderRadius:
|
|
129
|
-
viewBtn: { paddingHorizontal: 12, paddingVertical: 5, borderRadius:
|
|
128
|
+
viewSwitch: { flexDirection: "row", backgroundColor: colors.zinc[100], borderRadius: 10, padding: 2 },
|
|
129
|
+
viewBtn: { paddingHorizontal: 12, paddingVertical: 5, borderRadius: 8 },
|
|
130
130
|
viewBtnActive: { backgroundColor: colors.white },
|
|
131
131
|
});
|
package/src/card_select_item.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PressableStateCallbackType, StyleProp, StyleSheet, ViewStyle } from "react-native";
|
|
2
2
|
import { colors } from "./colors";
|
|
3
|
+
import { CONTROL_RADIUS } from "./control_surface";
|
|
3
4
|
import { PressableHighlight } from "./pressable_highlight";
|
|
4
5
|
|
|
5
6
|
interface CardSelectItemProps {
|
|
@@ -45,7 +46,7 @@ export function CardSelectItem(props: CardSelectItemProps) {
|
|
|
45
46
|
const styles = StyleSheet.create({
|
|
46
47
|
container: {
|
|
47
48
|
padding: 16,
|
|
48
|
-
borderRadius:
|
|
49
|
+
borderRadius: CONTROL_RADIUS,
|
|
49
50
|
backgroundColor: colors.background,
|
|
50
51
|
borderColor: colors.border,
|
|
51
52
|
borderWidth: 1,
|
package/src/change_review.tsx
CHANGED
|
@@ -173,8 +173,8 @@ export function ChangeReview<T>(props: ChangeReviewProps<T>) {
|
|
|
173
173
|
<View key={props.getKey(item, i)} style={styles.itemCard}>
|
|
174
174
|
{props.renderItem(item, i)}
|
|
175
175
|
<View style={styles.itemActions}>
|
|
176
|
-
<Button title={props.rejectLabel ?? "Drop"} color="muted"
|
|
177
|
-
<Button title={props.acceptLabel ?? "Keep"} color="secondary"
|
|
176
|
+
<Button title={props.rejectLabel ?? "Drop"} color="muted" onPress={() => props.onRejectItem?.(i)} />
|
|
177
|
+
<Button title={props.acceptLabel ?? "Keep"} color="secondary" onPress={() => props.onAcceptItem?.(i)} />
|
|
178
178
|
</View>
|
|
179
179
|
</View>
|
|
180
180
|
);
|
|
@@ -193,7 +193,7 @@ export function ChangeReview<T>(props: ChangeReviewProps<T>) {
|
|
|
193
193
|
)}
|
|
194
194
|
</View>
|
|
195
195
|
{props.onUndoItem ? (
|
|
196
|
-
<Button title={L.undo} color="muted"
|
|
196
|
+
<Button title={L.undo} color="muted" onPress={() => props.onUndoItem?.(i)} />
|
|
197
197
|
) : null}
|
|
198
198
|
</View>
|
|
199
199
|
);
|
|
@@ -243,14 +243,13 @@ export function ChangeReviewActions<T>(props: ChangeReviewActionsProps<T>) {
|
|
|
243
243
|
});
|
|
244
244
|
return (
|
|
245
245
|
<View style={styles.actionsRow}>
|
|
246
|
-
{perItem ? <Button title={props.acceptAllLabel ?? "Accept all"} color="muted"
|
|
246
|
+
{perItem ? <Button title={props.acceptAllLabel ?? "Accept all"} color="muted" onPress={acceptAll} /> : null}
|
|
247
247
|
<View style={{ flex: 1 }} />
|
|
248
|
-
{props.onDiscard ? <Button title={props.discardLabel ?? "Discard"} color="muted"
|
|
248
|
+
{props.onDiscard ? <Button title={props.discardLabel ?? "Discard"} color="muted" onPress={props.onDiscard} /> : null}
|
|
249
249
|
{props.onApply ? (
|
|
250
250
|
<Button
|
|
251
251
|
title={props.applyLabel ?? (perItem ? "Apply kept" : "Apply")}
|
|
252
252
|
color="primary"
|
|
253
|
-
shape="rounded"
|
|
254
253
|
disabled={perItem && keptCount === 0}
|
|
255
254
|
onPress={props.onApply}
|
|
256
255
|
/>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Ref } from "react";
|
|
2
2
|
import { IconButton } from "./icon_button";
|
|
3
3
|
import { PressableHighlight } from "./pressable_highlight";
|
|
4
|
-
import {
|
|
4
|
+
import { chipSurfaceStyle } from "./control_surface";
|
|
5
5
|
import { StyleSheet, View } from "react-native";
|
|
6
6
|
|
|
7
|
-
interface
|
|
7
|
+
interface ChipProps {
|
|
8
8
|
testID?: string;
|
|
9
9
|
children: React.ReactNode;
|
|
10
10
|
onPress?: () => void;
|
|
@@ -13,7 +13,7 @@ interface PillButtonProps {
|
|
|
13
13
|
ref?: Ref<View>;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function
|
|
16
|
+
export function Chip(props: ChipProps) {
|
|
17
17
|
const { testID, children, onPress, onDismiss, dismissTooltip, ref } = props;
|
|
18
18
|
|
|
19
19
|
return (
|
|
@@ -22,12 +22,12 @@ export function PillButton(props: PillButtonProps) {
|
|
|
22
22
|
<PressableHighlight
|
|
23
23
|
testID={testID}
|
|
24
24
|
onPress={onPress}
|
|
25
|
-
style={(state) => [
|
|
25
|
+
style={(state) => [chipSurfaceStyle(state), styles.pillLayout, onDismiss && styles.pillWithDismiss]}
|
|
26
26
|
>
|
|
27
27
|
{children}
|
|
28
28
|
</PressableHighlight>
|
|
29
29
|
) : (
|
|
30
|
-
<View style={[
|
|
30
|
+
<View style={[chipSurfaceStyle({}), styles.pillLayout, onDismiss && styles.pillWithDismiss]}>{children}</View>
|
|
31
31
|
)}
|
|
32
32
|
{onDismiss && (
|
|
33
33
|
<View style={styles.dismissButton}>
|
|
@@ -45,8 +45,8 @@ export function PillButton(props: PillButtonProps) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const styles = StyleSheet.create({
|
|
48
|
-
// Surface (height/border/radius/white + hover) comes from
|
|
49
|
-
//
|
|
48
|
+
// Surface (height/border/radius/white + hover) comes from chipSurfaceStyle;
|
|
49
|
+
// Chip owns only its row layout.
|
|
50
50
|
pillLayout: {
|
|
51
51
|
flexDirection: "row",
|
|
52
52
|
alignItems: "center",
|
package/src/chip_group.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { View } from "react-native";
|
|
2
2
|
import { Text } from "./text";
|
|
3
3
|
import { PressableHighlight } from "./pressable_highlight";
|
|
4
|
-
import {
|
|
4
|
+
import { chipSurfaceStyle } from "./control_surface";
|
|
5
5
|
|
|
6
6
|
// One-of-N chips: every option visible, one tap to switch, the row wraps on
|
|
7
7
|
// narrow widths. Quiet zinc styling — bordered white at rest, dark fill when
|
|
@@ -55,7 +55,7 @@ export function ChipGroup<T extends string = string>(props: ChipGroupProps<T>) {
|
|
|
55
55
|
// The shared pill-surface contract (height/border/radius/white +
|
|
56
56
|
// hover/press wash + the selected box-shadow ring); the chip only
|
|
57
57
|
// adds its horizontal padding.
|
|
58
|
-
style={(state) => [
|
|
58
|
+
style={(state) => [chipSurfaceStyle(state, { selected: active }), { paddingHorizontal: 14 }]}
|
|
59
59
|
>
|
|
60
60
|
<Text size="sm" weight={active ? "semibold" : "medium"} color={active ? "default" : "muted"}>
|
|
61
61
|
{option.label}
|