@lotics/ui 5.8.0 → 5.10.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 +184 -56
- package/examples/tpl_approvals.tsx +3 -3
- package/examples/tpl_convert.tsx +11 -23
- package/examples/tpl_dossier.tsx +5 -4
- package/examples/tpl_order.tsx +1 -1
- package/examples/tpl_pipeline.tsx +400 -0
- package/examples/tpl_ratedesk.tsx +1 -1
- package/examples/tpl_record.tsx +6 -5
- package/examples/tpl_shifts.tsx +7 -5
- package/examples/tpl_task_board.tsx +473 -0
- package/examples/tpl_tasks.tsx +440 -0
- package/package.json +19 -6
- package/src/accordion.tsx +1 -1
- package/src/agent_progress.tsx +1 -1
- package/src/agent_run.tsx +1 -1
- package/src/breakdown.tsx +2 -2
- package/src/button.tsx +12 -3
- package/src/calendar/calendar_view.tsx +10 -10
- package/src/calendar/month_view.tsx +30 -11
- package/src/calendar/time_grid_view.tsx +25 -7
- package/src/card_select_item.tsx +11 -13
- package/src/check_circle.tsx +88 -0
- package/src/checkbox_input.tsx +5 -1
- package/src/chip.tsx +1 -1
- package/src/chip_group.tsx +1 -1
- package/src/column_filter.tsx +3 -2
- package/src/combobox.tsx +84 -219
- package/src/confidence.tsx +14 -3
- package/src/control_surface.ts +22 -7
- package/src/data_grid.tsx +159 -0
- package/src/date_calendar.tsx +46 -23
- package/src/date_field.tsx +10 -2
- package/src/date_filter.tsx +5 -9
- package/src/date_picker.tsx +78 -26
- package/src/date_picker_value.ts +11 -0
- package/src/date_range_filter_field.tsx +6 -4
- package/src/drawer.tsx +1 -1
- package/src/file_dropzone.tsx +5 -0
- package/src/file_gallery_modal.tsx +227 -139
- package/src/file_grid.tsx +184 -0
- package/src/file_preview.web.tsx +349 -12
- package/src/file_preview_types.ts +49 -0
- package/src/file_row.tsx +118 -0
- package/src/file_rows.tsx +109 -0
- package/src/file_thumbnail.tsx +132 -81
- package/src/file_thumbnail_grid.tsx +135 -55
- package/src/files_editor.tsx +232 -0
- package/src/filter_chip.tsx +25 -12
- package/src/focus_ring_pressable.tsx +34 -0
- package/src/form_picker.tsx +3 -3
- package/src/gantt/gantt_view.tsx +25 -7
- package/src/heatmap.tsx +5 -1
- package/src/icon_button.tsx +14 -3
- package/src/image_gallery.tsx +8 -14
- package/src/index.css +8 -12
- package/src/inline_date_picker.tsx +11 -11
- package/src/inline_edit.tsx +22 -6
- package/src/inline_member_select.tsx +1 -1
- package/src/inline_select.tsx +9 -8
- package/src/inline_text_input.tsx +4 -1
- package/src/link.tsx +11 -1
- package/src/link_button.tsx +1 -1
- package/src/list_item.tsx +2 -2
- package/src/member_select.tsx +9 -4
- package/src/menu_button.tsx +2 -2
- package/src/menu_list_item.tsx +2 -2
- package/src/number_input.tsx +11 -4
- package/src/option_list.tsx +211 -0
- package/src/pdfjs_worker.d.ts +5 -0
- package/src/peek.tsx +1 -1
- package/src/picker.tsx +24 -213
- package/src/pressable_highlight.tsx +42 -23
- package/src/radio_picker.tsx +2 -2
- package/src/range_slider.tsx +35 -7
- package/src/react_native.d.ts +3 -0
- package/src/scroll_to_bottom.tsx +1 -1
- package/src/section.tsx +7 -2
- package/src/segmented_control.tsx +2 -2
- package/src/select.tsx +299 -0
- package/src/sort_header.tsx +1 -1
- package/src/sources.tsx +1 -1
- package/src/spreadsheet_view.tsx +43 -17
- package/src/status_grid.tsx +4 -3
- package/src/stepper.tsx +1 -1
- package/src/switch.tsx +5 -1
- package/src/switch_button.tsx +1 -1
- package/src/switcher.tsx +1 -1
- package/src/table.tsx +5 -1
- package/src/tabs.tsx +2 -2
- package/src/text_input_field.tsx +15 -4
- package/src/time_picker.tsx +11 -4
- package/src/timeline.tsx +1 -1
- package/src/uploading_thumbnail.tsx +212 -0
- package/src/use_focus_ring.ts +80 -0
- package/src/use_hover.ts +26 -0
- package/src/use_list_keyboard_nav.test.ts +71 -0
- package/src/use_list_keyboard_nav.ts +52 -5
- package/src/use_option_list.test.ts +193 -0
- package/src/use_option_list.ts +354 -0
- package/src/command_menu.tsx +0 -205
- package/src/hover_action.tsx +0 -54
- package/src/picker_menu.tsx +0 -355
- package/src/tag_input.tsx +0 -203
- package/src/time_field.tsx +0 -297
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { Pressable, ScrollView, StyleSheet, View } from "react-native";
|
|
3
|
+
import { Text } from "@lotics/ui/text";
|
|
4
|
+
import { colors, solid, asColorName } from "@lotics/ui/colors";
|
|
5
|
+
import { Icon } from "@lotics/ui/icon";
|
|
6
|
+
import { CheckCircle } from "@lotics/ui/check_circle";
|
|
7
|
+
import { OptionBadge, type OptionValue } from "@lotics/ui/option_badge";
|
|
8
|
+
import { SearchInput } from "@lotics/ui/search_input";
|
|
9
|
+
import { FilterChip } from "@lotics/ui/filter_chip";
|
|
10
|
+
import { OptionList } from "@lotics/ui/option_list";
|
|
11
|
+
import { InlineTextInput } from "@lotics/ui/inline_text_input";
|
|
12
|
+
import { InlineDatePicker } from "@lotics/ui/inline_date_picker";
|
|
13
|
+
import { InlineSelect } from "@lotics/ui/inline_select";
|
|
14
|
+
import { Select } from "@lotics/ui/select";
|
|
15
|
+
import { TextInputField } from "@lotics/ui/text_input_field";
|
|
16
|
+
import { Button } from "@lotics/ui/button";
|
|
17
|
+
import { FilesEditor } from "@lotics/ui/files_editor";
|
|
18
|
+
import type { DisplayFile } from "@lotics/ui/file_thumbnail";
|
|
19
|
+
import { Composer } from "@lotics/ui/composer";
|
|
20
|
+
import { AgentRun, type AgentRunItem } from "@lotics/ui/agent_run";
|
|
21
|
+
import { ChangeReview, ChangeReviewActions, type ChangeReviewItemStatus } from "@lotics/ui/change_review";
|
|
22
|
+
import { Dialog, DialogHeader, DialogHeaderTitle, DialogScrollArea, DialogFooter } from "@lotics/ui/dialog";
|
|
23
|
+
import { CompletionState } from "@lotics/ui/completion_state";
|
|
24
|
+
import { Confidence, type ConfidenceLevel } from "@lotics/ui/confidence";
|
|
25
|
+
import { formatDate, toISODate } from "@lotics/ui/format_date";
|
|
26
|
+
|
|
27
|
+
type TagOption = { value: string; label: string };
|
|
28
|
+
|
|
29
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
// Template · Tasks (quick list) — the Apple-Reminders shape for PERSONAL lists.
|
|
31
|
+
// There's no Task component: a row is composed directly from primitives
|
|
32
|
+
// (CheckCircle + an inline-editable title + a quiet meta line), which is the most
|
|
33
|
+
// flexible way to build a task surface. Quiet, scannable rows you tick to finish
|
|
34
|
+
// and tap to expand in place; the note sits below the title in a light tone. No
|
|
35
|
+
// assignee (the list is yours). The sibling tpl_task_board is the Linear columns
|
|
36
|
+
// shape for a manager. Tick the ring → done; tap the row → expand; click the title
|
|
37
|
+
// → rename in place.
|
|
38
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
type Status = "todo" | "doing" | "done";
|
|
41
|
+
const STATUS: Record<Status, OptionValue> = {
|
|
42
|
+
todo: { key: "todo", label: "To do", color: null },
|
|
43
|
+
doing: { key: "doing", label: "Doing", color: "amber" },
|
|
44
|
+
done: { key: "done", label: "Done", color: "emerald" },
|
|
45
|
+
};
|
|
46
|
+
const STATUS_OPTIONS = (Object.keys(STATUS) as Status[]).map((s) => ({ value: s, label: STATUS[s].label }));
|
|
47
|
+
|
|
48
|
+
const TAGS: Record<string, OptionValue> = {
|
|
49
|
+
client: { key: "client", label: "Client", color: "blue" },
|
|
50
|
+
design: { key: "design", label: "Design", color: "cyan" },
|
|
51
|
+
print: { key: "print", label: "Print", color: "amber" },
|
|
52
|
+
urgent: { key: "urgent", label: "Urgent", color: "rose" },
|
|
53
|
+
};
|
|
54
|
+
const TAG_OPTIONS: TagOption[] = Object.values(TAGS).map((t) => ({ value: String(t.key), label: t.label }));
|
|
55
|
+
const tagOf = (k: string): TagOption => ({ value: k, label: TAGS[k]?.label ?? k });
|
|
56
|
+
const tagBadge = (t: TagOption): OptionValue => TAGS[t.value] ?? { key: t.value, label: t.label, color: null };
|
|
57
|
+
const tagColor = (t: TagOption): string => solid(asColorName(TAGS[t.value]?.color));
|
|
58
|
+
const renderTagBadge = (o: { value: string; label?: string }) => <OptionBadge value={tagBadge({ value: o.value, label: o.label ?? o.value })} variant="dot" />;
|
|
59
|
+
|
|
60
|
+
interface Task {
|
|
61
|
+
id: string;
|
|
62
|
+
title: string;
|
|
63
|
+
due: string | null;
|
|
64
|
+
status: Status;
|
|
65
|
+
tags: TagOption[];
|
|
66
|
+
note: string;
|
|
67
|
+
files: DisplayFile[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const day = (n: number): string => {
|
|
71
|
+
const d = new Date();
|
|
72
|
+
d.setDate(d.getDate() + n);
|
|
73
|
+
return toISODate(d);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// A self-contained SVG data-URI image so seeds preview offline in the gallery.
|
|
77
|
+
const sampleImage = (label: string, fill: string): DisplayFile => ({
|
|
78
|
+
id: `seed-${label}`,
|
|
79
|
+
filename: `${label}.png`,
|
|
80
|
+
mimeType: "image/png",
|
|
81
|
+
url: "data:image/svg+xml," + encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="320" height="240"><rect width="320" height="240" fill="${fill}"/><text x="160" y="128" font-family="sans-serif" font-size="22" fill="white" text-anchor="middle">${label}</text></svg>`),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// New attachments map a picked browser File → a DisplayFile (local object URL).
|
|
85
|
+
// A host wires its own upload here; the template just previews in place.
|
|
86
|
+
let fileSeq = 0;
|
|
87
|
+
const toDisplayFiles = (files: File[]): DisplayFile[] =>
|
|
88
|
+
files.map((f) => ({ id: `f${++fileSeq}`, filename: f.name, mimeType: f.type || "application/octet-stream", url: URL.createObjectURL(f) }));
|
|
89
|
+
|
|
90
|
+
const SEED: Task[] = [
|
|
91
|
+
{ id: "t1", title: "Send VITTORIA their quote", due: day(-2), status: "doing", tags: [tagOf("client"), tagOf("urgent")], note: "Waiting on production to confirm quantities.", files: [sampleImage("quote-draft", "#3b82f6")] },
|
|
92
|
+
{ id: "t2", title: "Finalize the gift-box packaging design", due: day(0), status: "doing", tags: [tagOf("design")], note: "", files: [] },
|
|
93
|
+
{ id: "t3", title: "Sign off the die-cut sample", due: `${day(0)}T15:00`, status: "todo", tags: [tagOf("design"), tagOf("print")], note: "Workshop visit booked for 3pm.", files: [sampleImage("die-cut", "#10b981"), sampleImage("sample-2", "#f59e0b")] },
|
|
94
|
+
{ id: "t4", title: "Order 300g ivory board stock", due: day(3), status: "todo", tags: [tagOf("print")], note: "", files: [] },
|
|
95
|
+
{ id: "t5", title: "Schedule the first delivery batch", due: day(9), status: "todo", tags: [], note: "", files: [] },
|
|
96
|
+
{ id: "t6", title: "Draft the 2026 print contract", due: null, status: "todo", tags: [tagOf("client")], note: "", files: [] },
|
|
97
|
+
{ id: "t7", title: "Update the Q3 price list", due: day(-5), status: "done", tags: [], note: "Sent to the sales team.", files: [] },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
type Group = "due" | "status";
|
|
101
|
+
const GROUP_OPTIONS: { value: Group; label: string }[] = [
|
|
102
|
+
{ value: "due", label: "Due date" },
|
|
103
|
+
{ value: "status", label: "Status" },
|
|
104
|
+
];
|
|
105
|
+
type SectionKey = "overdue" | "today" | "week" | "later" | "none" | "done" | Status;
|
|
106
|
+
const DUE_SECTIONS: { key: SectionKey; label: string }[] = [
|
|
107
|
+
{ key: "overdue", label: "Overdue" },
|
|
108
|
+
{ key: "today", label: "Today" },
|
|
109
|
+
{ key: "week", label: "This week" },
|
|
110
|
+
{ key: "later", label: "Later" },
|
|
111
|
+
{ key: "none", label: "No date" },
|
|
112
|
+
{ key: "done", label: "Done" },
|
|
113
|
+
];
|
|
114
|
+
const STATUS_SECTIONS: { key: SectionKey; label: string }[] = [
|
|
115
|
+
{ key: "todo", label: "To do" },
|
|
116
|
+
{ key: "doing", label: "Doing" },
|
|
117
|
+
{ key: "done", label: "Done" },
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
function dueBucket(due: string | null): SectionKey {
|
|
121
|
+
if (!due) return "none";
|
|
122
|
+
const d = due.slice(0, 10);
|
|
123
|
+
const today = day(0);
|
|
124
|
+
if (d < today) return "overdue";
|
|
125
|
+
if (d === today) return "today";
|
|
126
|
+
if (d <= day(7)) return "week";
|
|
127
|
+
return "later";
|
|
128
|
+
}
|
|
129
|
+
function dueInfo(due: string | null): { label: string; overdue: boolean } | null {
|
|
130
|
+
if (!due) return null;
|
|
131
|
+
return { label: formatDate(due, { format: "dayMonth" }), overdue: due.slice(0, 10) < day(0) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── AI · draft tasks from notes — agent proposes, you keep/edit/drop ──────────
|
|
135
|
+
type Phase = "idle" | "reading" | "review" | "done";
|
|
136
|
+
interface Proposal { id: string; title: string; due: string | null; confidence: ConfidenceLevel; source: string }
|
|
137
|
+
const DRAFT_STEPS: { label: string; detail: string }[] = [
|
|
138
|
+
{ label: "Split the notes into candidate items", detail: "6 lines" },
|
|
139
|
+
{ label: "Drop greetings and lines that aren't actions", detail: "kept 4" },
|
|
140
|
+
{ label: "Infer a due date from each phrase", detail: "2 dated · 2 left open" },
|
|
141
|
+
];
|
|
142
|
+
const DRAFTED: Proposal[] = [
|
|
143
|
+
{ id: "d1", title: "Send VITTORIA the revised quote", due: day(1), confidence: "high", source: "“…told VITTORIA they'd have the updated quote by tomorrow.”" },
|
|
144
|
+
{ id: "d2", title: "Re-check the die-cut sample size with the workshop", due: day(3), confidence: "medium", source: "“…workshop to measure the sample again, maybe Thursday.”" },
|
|
145
|
+
{ id: "d3", title: "Order 300g ivory board for the gift-box run", due: null, confidence: "high", source: "“…order the ivory stock this week.”" },
|
|
146
|
+
{ id: "d4", title: "Sit in on the Tet carton press check", due: null, confidence: "low", source: "“…someone should be there for the Tet print run.”" },
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
export function TplTasks() {
|
|
150
|
+
const [tasks, setTasks] = useState<Task[]>(SEED);
|
|
151
|
+
const [group, setGroup] = useState<Group | null>("due"); // null = ungrouped (cleared)
|
|
152
|
+
const [expandedIds, setExpandedIds] = useState<Set<string>>(() => new Set());
|
|
153
|
+
const toggleExpand = (id: string, open: boolean) =>
|
|
154
|
+
setExpandedIds((s) => { const n = new Set(s); if (open) n.add(id); else n.delete(id); return n; });
|
|
155
|
+
const [query, setQuery] = useState("");
|
|
156
|
+
const [tagFilter, setTagFilter] = useState<string[]>([]);
|
|
157
|
+
const [draft, setDraft] = useState("");
|
|
158
|
+
|
|
159
|
+
const [draftOpen, setDraftOpen] = useState(false);
|
|
160
|
+
const [phase, setPhase] = useState<Phase>("idle");
|
|
161
|
+
const [revealed, setRevealed] = useState(0);
|
|
162
|
+
const [proposals, setProposals] = useState<Proposal[]>([]);
|
|
163
|
+
const [decisions, setDecisions] = useState<Record<string, ChangeReviewItemStatus>>({});
|
|
164
|
+
const [addedCount, setAddedCount] = useState(0);
|
|
165
|
+
|
|
166
|
+
const patch = (id: string, next: Partial<Task>) =>
|
|
167
|
+
setTasks((ts) => ts.map((t) => (t.id === id ? { ...t, ...next } : t)));
|
|
168
|
+
const add = () => {
|
|
169
|
+
const title = draft.trim();
|
|
170
|
+
if (!title) return;
|
|
171
|
+
setTasks((ts) => [...ts, { id: `n${ts.length}-${title.length}`, title, due: null, status: "todo", tags: [], note: "", files: [] }]);
|
|
172
|
+
setDraft("");
|
|
173
|
+
};
|
|
174
|
+
const addFiles = (id: string, picked: File[]) =>
|
|
175
|
+
setTasks((ts) => ts.map((t) => (t.id === id ? { ...t, files: [...t.files, ...toDisplayFiles(picked)] } : t)));
|
|
176
|
+
const removeFile = (id: string, fileId: string) =>
|
|
177
|
+
setTasks((ts) => ts.map((t) => (t.id === id ? { ...t, files: t.files.filter((f) => f.id !== fileId) } : t)));
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
const startDraft = useCallback(() => { setRevealed(0); setPhase("reading"); }, []);
|
|
181
|
+
const resetDraft = useCallback(() => { setProposals([]); setDecisions({}); setRevealed(0); setPhase("idle"); }, []);
|
|
182
|
+
const closeDraft = useCallback(() => { setDraftOpen(false); resetDraft(); }, [resetDraft]);
|
|
183
|
+
const editProposal = useCallback((id: string, p: Partial<Proposal>) =>
|
|
184
|
+
setProposals((ps) => ps.map((x) => (x.id === id ? { ...x, ...p } : x))), []);
|
|
185
|
+
const decide = useCallback((id: string, status: ChangeReviewItemStatus | null) =>
|
|
186
|
+
setDecisions((dec) => { const n = { ...dec }; if (status) n[id] = status; else delete n[id]; return n; }), []);
|
|
187
|
+
const acceptedCount = proposals.filter((p) => decisions[p.id] === "accepted").length;
|
|
188
|
+
const addAccepted = useCallback(() => {
|
|
189
|
+
const accepted = proposals.filter((p) => decisions[p.id] === "accepted");
|
|
190
|
+
setTasks((ts) => [...ts, ...accepted.map((p) => ({ id: `d-${p.id}`, title: p.title, due: p.due, status: "todo" as Status, tags: [], note: "", files: [] }))]);
|
|
191
|
+
setAddedCount(accepted.length);
|
|
192
|
+
setPhase("done");
|
|
193
|
+
}, [proposals, decisions]);
|
|
194
|
+
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (phase !== "reading") return;
|
|
197
|
+
if (revealed >= DRAFT_STEPS.length) {
|
|
198
|
+
const t = setTimeout(() => { setProposals(DRAFTED); setPhase("review"); }, 850);
|
|
199
|
+
return () => clearTimeout(t);
|
|
200
|
+
}
|
|
201
|
+
const t = setTimeout(() => setRevealed((r) => r + 1), revealed === 0 ? 280 : 680);
|
|
202
|
+
return () => clearTimeout(t);
|
|
203
|
+
}, [phase, revealed]);
|
|
204
|
+
|
|
205
|
+
const runItems: AgentRunItem[] = [
|
|
206
|
+
{ type: "text", id: "intro", text: "I read your notes and pulled out the work." },
|
|
207
|
+
...DRAFT_STEPS.slice(0, Math.min(revealed + 1, DRAFT_STEPS.length)).map((s, i): AgentRunItem => ({
|
|
208
|
+
type: "step", id: `s${i}`, label: s.label, detail: i < revealed ? s.detail : undefined, status: i < revealed ? "done" : "running",
|
|
209
|
+
})),
|
|
210
|
+
];
|
|
211
|
+
if (revealed >= DRAFT_STEPS.length) {
|
|
212
|
+
runItems.push({ type: "text", id: "outro", text: `Drafted ${DRAFTED.length} tasks — keep the ones that look right, edit any before adding.` });
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const sections = useMemo(() => {
|
|
216
|
+
const q = query.trim().toLowerCase();
|
|
217
|
+
const pool = tasks.filter((t) => {
|
|
218
|
+
if (q && !(t.title.toLowerCase().includes(q) || t.tags.some((tag) => tag.label.toLowerCase().includes(q)))) return false;
|
|
219
|
+
if (tagFilter.length > 0 && !t.tags.some((tag) => tagFilter.includes(tag.value))) return false;
|
|
220
|
+
return true;
|
|
221
|
+
});
|
|
222
|
+
const byDue = (a: Task, b: Task) => (a.due ?? "9999").localeCompare(b.due ?? "9999");
|
|
223
|
+
if (!group) {
|
|
224
|
+
return [{ key: "all" as SectionKey, label: "", items: [...pool].sort(byDue) }];
|
|
225
|
+
}
|
|
226
|
+
const defs = group === "due" ? DUE_SECTIONS : STATUS_SECTIONS;
|
|
227
|
+
const inBucket = (t: Task, key: SectionKey) =>
|
|
228
|
+
group === "due" ? (t.status === "done" ? key === "done" : dueBucket(t.due) === key) : t.status === key;
|
|
229
|
+
return defs
|
|
230
|
+
.map((s) => ({ ...s, items: pool.filter((t) => inBucket(t, s.key)).sort(byDue) }))
|
|
231
|
+
.filter((s) => s.items.length > 0);
|
|
232
|
+
}, [tasks, group, query, tagFilter]);
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<>
|
|
236
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ paddingVertical: 28, paddingHorizontal: 20, paddingBottom: 96 }}>
|
|
237
|
+
<View style={{ maxWidth: 680, width: "100%", alignSelf: "center", gap: 14 }}>
|
|
238
|
+
<Text size="xl" weight="semibold">Tasks</Text>
|
|
239
|
+
|
|
240
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
241
|
+
<View style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", gap: 8, flex: 1 }}>
|
|
242
|
+
<View style={{ flexGrow: 1, flexBasis: 240, minWidth: 200, maxWidth: 360 }}>
|
|
243
|
+
<SearchInput value={query} onChangeText={setQuery} placeholder="Search tasks" accessibilityLabel="Search tasks" />
|
|
244
|
+
</View>
|
|
245
|
+
<FilterChip label="Group by" summary={group ? GROUP_OPTIONS.find((g) => g.value === group)?.label : undefined} onClear={() => setGroup(null)} clearLabel="Clear grouping">
|
|
246
|
+
{({ close }) => <OptionList search={{ mode: "none" }} options={GROUP_OPTIONS} value={group ?? undefined} onValueChange={(v) => v && setGroup(v)} onRequestClose={close} />}
|
|
247
|
+
</FilterChip>
|
|
248
|
+
<FilterChip
|
|
249
|
+
label="Tag"
|
|
250
|
+
summary={tagFilter.length > 0 ? <View style={styles.dots}>{tagFilter.map((v) => <View key={v} style={[styles.dot, { backgroundColor: tagColor(tagOf(v)) }]} />)}</View> : undefined}
|
|
251
|
+
onClear={() => setTagFilter([])}
|
|
252
|
+
clearLabel="Clear tag filter"
|
|
253
|
+
>
|
|
254
|
+
<OptionList search={{ mode: "none" }} multi options={TAG_OPTIONS} value={tagFilter} onValueChange={setTagFilter} renderOptionContent={(o) => <OptionBadge value={tagBadge({ value: o.value, label: o.label ?? o.value })} variant="dot" />} />
|
|
255
|
+
</FilterChip>
|
|
256
|
+
</View>
|
|
257
|
+
<Button title="Draft from notes" color="primary" onPress={() => setDraftOpen(true)} />
|
|
258
|
+
</View>
|
|
259
|
+
|
|
260
|
+
{/* Capture — type to add; the submit button lives inside the field on the right. */}
|
|
261
|
+
<View style={styles.capture}>
|
|
262
|
+
<View style={[styles.row, { gap: 12 }]}>
|
|
263
|
+
<View style={styles.emptyRing} />
|
|
264
|
+
<View style={styles.captureField}>
|
|
265
|
+
<View style={{ flex: 1 }}>
|
|
266
|
+
<TextInputField value={draft} onChangeText={setDraft} onSubmitEditing={add} placeholder="Add a task…" style={styles.titleInput} accessibilityLabel="Add a task" />
|
|
267
|
+
</View>
|
|
268
|
+
{draft.trim() ? <Button title="Save" color="primary" onPress={add} /> : null}
|
|
269
|
+
</View>
|
|
270
|
+
</View>
|
|
271
|
+
</View>
|
|
272
|
+
|
|
273
|
+
<View>
|
|
274
|
+
{sections.map((s) => (
|
|
275
|
+
<View key={s.key} style={{ marginBottom: 16 }}>
|
|
276
|
+
{s.label ? (
|
|
277
|
+
<View style={styles.sectionHead}>
|
|
278
|
+
<Text size="xs" weight="semibold" color={s.key === "overdue" ? "danger" : "muted"} style={styles.sectionLabel}>{s.label}</Text>
|
|
279
|
+
<Text size="xs" color="muted" tabular>{s.items.length}</Text>
|
|
280
|
+
</View>
|
|
281
|
+
) : null}
|
|
282
|
+
{s.items.map((t) => {
|
|
283
|
+
const open = expandedIds.has(t.id);
|
|
284
|
+
const done = t.status === "done";
|
|
285
|
+
const d = dueInfo(t.due);
|
|
286
|
+
return (
|
|
287
|
+
<View key={t.id}>
|
|
288
|
+
<Pressable onPress={() => toggleExpand(t.id, !open)} accessibilityRole="button" accessibilityLabel={t.title} style={({ hovered }: { hovered?: boolean }) => [styles.row, hovered ? styles.rowHover : null]}>
|
|
289
|
+
<CheckCircle checked={done} onChange={(on) => patch(t.id, { status: on ? "done" : "todo" })} accessibilityLabel={t.title} />
|
|
290
|
+
<View style={{ flex: 1 }}>
|
|
291
|
+
<InlineTextInput value={t.title} onSave={(v) => patch(t.id, { title: v })} struck={done} accessibilityLabel="Title" />
|
|
292
|
+
</View>
|
|
293
|
+
<View style={styles.meta}>
|
|
294
|
+
{t.files.length > 0 ? (
|
|
295
|
+
<View style={styles.clip}>
|
|
296
|
+
<Icon name="paperclip" size={13} color={colors.zinc[400]} />
|
|
297
|
+
<Text size="xs" color="muted" tabular>{t.files.length}</Text>
|
|
298
|
+
</View>
|
|
299
|
+
) : null}
|
|
300
|
+
{t.tags.length > 0 ? (
|
|
301
|
+
<View style={styles.dots}>{t.tags.map((tag) => <View key={tag.value} style={[styles.dot, { backgroundColor: tagColor(tag) }]} />)}</View>
|
|
302
|
+
) : null}
|
|
303
|
+
{d ? <Text size="xs" weight="medium" color={d.overdue && !done ? "danger" : "muted"}>{d.label}</Text> : null}
|
|
304
|
+
</View>
|
|
305
|
+
<Icon name={open ? "chevron-up" : "chevron-down"} size={16} color={colors.zinc[300]} />
|
|
306
|
+
</Pressable>
|
|
307
|
+
{open ? (
|
|
308
|
+
<View style={styles.detail}>
|
|
309
|
+
<Field label="Due">
|
|
310
|
+
<InlineDatePicker value={t.due} optionalTime onSave={(v) => patch(t.id, { due: v })} placeholder="Set a due date" accessibilityLabel="Due date" />
|
|
311
|
+
</Field>
|
|
312
|
+
<Field label="Status">
|
|
313
|
+
<InlineSelect value={t.status} options={STATUS_OPTIONS} onSave={(st) => patch(t.id, { status: st })} renderSelected={(o) => <OptionBadge value={STATUS[o.value as Status]} variant="dot" />} renderOptionContent={(o) => <OptionBadge value={STATUS[o.value as Status]} variant="dot" />} accessibilityLabel="Status" />
|
|
314
|
+
</Field>
|
|
315
|
+
<Field label="Tags">
|
|
316
|
+
<Select multi searchable allowCustom value={t.tags.map((tag) => tag.value)} onValueChange={(next) => patch(t.id, { tags: next.map(tagOf) })} options={TAG_OPTIONS} renderSelected={renderTagBadge} renderOptionContent={renderTagBadge} style={{ borderColor: "transparent" }} placeholder="Add tags" accessibilityLabel="Tags" />
|
|
317
|
+
</Field>
|
|
318
|
+
<Field label="Note">
|
|
319
|
+
<InlineTextInput value={t.note} onSave={(v) => patch(t.id, { note: v })} placeholder="Add a note…" accessibilityLabel="Note" />
|
|
320
|
+
</Field>
|
|
321
|
+
<Field label="Files" top>
|
|
322
|
+
<FilesEditor files={t.files} itemSize={72} onAdd={(picked) => addFiles(t.id, picked)} onRemove={(id) => removeFile(t.id, id)} />
|
|
323
|
+
</Field>
|
|
324
|
+
</View>
|
|
325
|
+
) : null}
|
|
326
|
+
</View>
|
|
327
|
+
);
|
|
328
|
+
})}
|
|
329
|
+
</View>
|
|
330
|
+
))}
|
|
331
|
+
</View>
|
|
332
|
+
</View>
|
|
333
|
+
</ScrollView>
|
|
334
|
+
|
|
335
|
+
{/* Draft from notes — Composer → AgentRun → ChangeReview → commit. */}
|
|
336
|
+
<Dialog open={draftOpen} onOpenChange={(o) => { if (!o) closeDraft(); }} maxWidth={620}>
|
|
337
|
+
<DialogHeader><DialogHeaderTitle>Draft from notes</DialogHeaderTitle></DialogHeader>
|
|
338
|
+
<DialogScrollArea>
|
|
339
|
+
{phase === "idle" ? (
|
|
340
|
+
<View style={{ gap: 12 }}>
|
|
341
|
+
<Text size="sm" color="muted">Paste meeting notes, a brief, or an email. Lotics drafts tasks from it — nothing is added until you review and keep them.</Text>
|
|
342
|
+
<Composer onSend={startDraft} placeholder="Paste your notes…" autoFocus sendLabel="Draft tasks" />
|
|
343
|
+
</View>
|
|
344
|
+
) : null}
|
|
345
|
+
{phase === "reading" ? <AgentRun items={runItems} state={revealed >= DRAFT_STEPS.length ? "done" : "streaming"} /> : null}
|
|
346
|
+
{phase === "review" ? (
|
|
347
|
+
<ChangeReview
|
|
348
|
+
title="Drafted tasks"
|
|
349
|
+
summary={`${proposals.length} from your notes — keep, edit, or drop each. A “Low” mark is worth a glance.`}
|
|
350
|
+
items={proposals}
|
|
351
|
+
getKey={(p) => p.id}
|
|
352
|
+
statusOf={(p) => decisions[p.id] ?? "pending"}
|
|
353
|
+
renderItem={(p) => <TaskProposal proposal={p} onEdit={editProposal} />}
|
|
354
|
+
renderSummary={(p, kept) => (
|
|
355
|
+
<Text size="sm" numberOfLines={1} color={kept ? "default" : "muted"} style={kept ? undefined : { textDecorationLine: "line-through" }}>{p.title}</Text>
|
|
356
|
+
)}
|
|
357
|
+
onAcceptItem={(i) => decide(proposals[i].id, "accepted")}
|
|
358
|
+
onRejectItem={(i) => decide(proposals[i].id, "rejected")}
|
|
359
|
+
onUndoItem={(i) => decide(proposals[i].id, null)}
|
|
360
|
+
/>
|
|
361
|
+
) : null}
|
|
362
|
+
{phase === "done" ? (
|
|
363
|
+
<CompletionState title={`${addedCount} ${addedCount === 1 ? "task" : "tasks"} added`} summary="They're in your list, grouped by due date." />
|
|
364
|
+
) : null}
|
|
365
|
+
</DialogScrollArea>
|
|
366
|
+
{phase === "review" ? (
|
|
367
|
+
<DialogFooter>
|
|
368
|
+
<ChangeReviewActions items={proposals} statusOf={(p) => decisions[p.id] ?? "pending"} onAcceptItem={(i) => decide(proposals[i].id, "accepted")} onApply={addAccepted} onDiscard={resetDraft} applyLabel={`Add ${acceptedCount} ${acceptedCount === 1 ? "task" : "tasks"}`} />
|
|
369
|
+
</DialogFooter>
|
|
370
|
+
) : phase === "done" ? (
|
|
371
|
+
<DialogFooter>
|
|
372
|
+
<Button title="Draft more" color="secondary" onPress={resetDraft} />
|
|
373
|
+
<Button title="Done" color="primary" onPress={closeDraft} />
|
|
374
|
+
</DialogFooter>
|
|
375
|
+
) : null}
|
|
376
|
+
</Dialog>
|
|
377
|
+
</>
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function TaskProposal({ proposal, onEdit }: { proposal: Proposal; onEdit: (id: string, p: Partial<Proposal>) => void }) {
|
|
382
|
+
return (
|
|
383
|
+
<View style={{ gap: 10 }}>
|
|
384
|
+
<View style={{ flexDirection: "row", alignItems: "flex-start", gap: 12 }}>
|
|
385
|
+
<View style={{ flex: 1 }}>
|
|
386
|
+
<InlineTextInput value={proposal.title} onSave={(v) => onEdit(proposal.id, { title: v })} accessibilityLabel="Task title" />
|
|
387
|
+
</View>
|
|
388
|
+
<View style={{ paddingTop: 10 }}><Confidence level={proposal.confidence} /></View>
|
|
389
|
+
</View>
|
|
390
|
+
<View style={{ width: 200 }}>
|
|
391
|
+
<InlineDatePicker value={proposal.due} onSave={(v) => onEdit(proposal.id, { due: v })} placeholder="No date" accessibilityLabel="Due date" />
|
|
392
|
+
</View>
|
|
393
|
+
<View style={styles.source}><Text size="xs" color="muted">{proposal.source}</Text></View>
|
|
394
|
+
</View>
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function Field(props: { label: string; children: React.ReactNode; top?: boolean }) {
|
|
399
|
+
return (
|
|
400
|
+
<View style={{ flexDirection: "row", alignItems: props.top ? "flex-start" : "center", minHeight: 36 }}>
|
|
401
|
+
<Text size="sm" color="muted" style={{ width: 116, paddingTop: props.top ? 8 : 0 }}>{props.label}</Text>
|
|
402
|
+
<View style={{ flex: 1 }}>{props.children}</View>
|
|
403
|
+
</View>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const styles = StyleSheet.create({
|
|
408
|
+
row: {
|
|
409
|
+
flexDirection: "row",
|
|
410
|
+
alignItems: "center",
|
|
411
|
+
gap: 12,
|
|
412
|
+
minHeight: 44,
|
|
413
|
+
paddingHorizontal: 8,
|
|
414
|
+
borderRadius: 10,
|
|
415
|
+
},
|
|
416
|
+
rowHover: { backgroundColor: colors.zinc[50] },
|
|
417
|
+
capture: {
|
|
418
|
+
borderBottomWidth: 1,
|
|
419
|
+
borderBottomColor: colors.zinc[100],
|
|
420
|
+
paddingBottom: 6,
|
|
421
|
+
},
|
|
422
|
+
// Borderless title input that matches an InlineTextInput's resting inset (8),
|
|
423
|
+
// so the capture title lines up with the task titles below it.
|
|
424
|
+
titleInput: {
|
|
425
|
+
borderWidth: 0,
|
|
426
|
+
backgroundColor: "transparent",
|
|
427
|
+
paddingVertical: 0,
|
|
428
|
+
},
|
|
429
|
+
captureField: { flex: 1, flexDirection: "row", alignItems: "center", gap: 8, paddingRight: 4 },
|
|
430
|
+
sectionHead: { flexDirection: "row", alignItems: "center", gap: 8, paddingBottom: 6 },
|
|
431
|
+
sectionLabel: { letterSpacing: 0.3, textTransform: "uppercase" },
|
|
432
|
+
meta: { flexDirection: "row", alignItems: "center", gap: 10 },
|
|
433
|
+
clip: { flexDirection: "row", alignItems: "center", gap: 2 },
|
|
434
|
+
emptyRing: { width: 20, height: 20, borderRadius: 10, borderWidth: 1.5, borderColor: colors.zinc[300], borderStyle: "dashed" },
|
|
435
|
+
dots: { flexDirection: "row", alignItems: "center", gap: 4 },
|
|
436
|
+
dot: { width: 8, height: 8, borderRadius: 999 },
|
|
437
|
+
// ring (20) + gap (12) + the title's 8 inset → labels line up with the title text
|
|
438
|
+
detail: { paddingLeft: 48, paddingRight: 8, paddingTop: 2, paddingBottom: 14, gap: 8 },
|
|
439
|
+
source: { borderLeftWidth: 2, borderLeftColor: colors.zinc[200], paddingLeft: 10 },
|
|
440
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./tokens": "./src/tokens.ts",
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
"./file_badge": "./src/file_badge.tsx",
|
|
19
19
|
"./file_dropzone": "./src/file_dropzone.tsx",
|
|
20
20
|
"./file_thumbnail": "./src/file_thumbnail.tsx",
|
|
21
|
+
"./file_row": "./src/file_row.tsx",
|
|
22
|
+
"./file_rows": "./src/file_rows.tsx",
|
|
21
23
|
"./file_preview": {
|
|
22
24
|
"react-native": "./src/file_preview.tsx",
|
|
23
25
|
"default": "./src/file_preview.web.tsx"
|
|
@@ -29,6 +31,9 @@
|
|
|
29
31
|
"./use_image_rotation": "./src/use_image_rotation.ts",
|
|
30
32
|
"./use_selection_mode": "./src/use_selection_mode.ts",
|
|
31
33
|
"./file_thumbnail_grid": "./src/file_thumbnail_grid.tsx",
|
|
34
|
+
"./uploading_thumbnail": "./src/uploading_thumbnail.tsx",
|
|
35
|
+
"./file_grid": "./src/file_grid.tsx",
|
|
36
|
+
"./files_editor": "./src/files_editor.tsx",
|
|
32
37
|
"./pagination": "./src/pagination.tsx",
|
|
33
38
|
"./bar_chart": "./src/bar_chart.tsx",
|
|
34
39
|
"./line_chart": "./src/line_chart.tsx",
|
|
@@ -58,7 +63,8 @@
|
|
|
58
63
|
"./form_time_picker": "./src/form_time_picker.tsx",
|
|
59
64
|
"./form_picker": "./src/form_picker.tsx",
|
|
60
65
|
"./picker": "./src/picker.tsx",
|
|
61
|
-
"./
|
|
66
|
+
"./select": "./src/select.tsx",
|
|
67
|
+
"./option_list": "./src/option_list.tsx",
|
|
62
68
|
"./text": "./src/text.tsx",
|
|
63
69
|
"./activity_indicator": "./src/activity_indicator.tsx",
|
|
64
70
|
"./agent_run": "./src/agent_run.tsx",
|
|
@@ -91,7 +97,6 @@
|
|
|
91
97
|
"./button": "./src/button.tsx",
|
|
92
98
|
"./checkbox": "./src/checkbox.tsx",
|
|
93
99
|
"./combobox": "./src/combobox.tsx",
|
|
94
|
-
"./tag_input": "./src/tag_input.tsx",
|
|
95
100
|
"./composer": "./src/composer.tsx",
|
|
96
101
|
"./portal": "./src/portal.tsx",
|
|
97
102
|
"./popover_nav": "./src/popover_nav.tsx",
|
|
@@ -123,6 +128,10 @@
|
|
|
123
128
|
"./keyboard": "./src/keyboard.ts",
|
|
124
129
|
"./use_screen_size": "./src/use_screen_size.ts",
|
|
125
130
|
"./use_auto_grow_height": "./src/use_auto_grow_height.ts",
|
|
131
|
+
"./use_focus_ring": "./src/use_focus_ring.ts",
|
|
132
|
+
"./use_hover": "./src/use_hover.ts",
|
|
133
|
+
"./focus_ring_pressable": "./src/focus_ring_pressable.tsx",
|
|
134
|
+
"./control_surface": "./src/control_surface.ts",
|
|
126
135
|
"./shortcut_badge": "./src/shortcut_badge.tsx",
|
|
127
136
|
"./text_input_field": "./src/text_input_field.tsx",
|
|
128
137
|
"./number_input": "./src/number_input.tsx",
|
|
@@ -130,6 +139,7 @@
|
|
|
130
139
|
"./switch": "./src/switch.tsx",
|
|
131
140
|
"./form_field": "./src/form_field.tsx",
|
|
132
141
|
"./checkbox_input": "./src/checkbox_input.tsx",
|
|
142
|
+
"./check_circle": "./src/check_circle.tsx",
|
|
133
143
|
"./radio_picker": "./src/radio_picker.tsx",
|
|
134
144
|
"./form_text_input": "./src/form_text_input.tsx",
|
|
135
145
|
"./form_switch": "./src/form_switch.tsx",
|
|
@@ -142,6 +152,7 @@
|
|
|
142
152
|
"./sort_header": "./src/sort_header.tsx",
|
|
143
153
|
"./skeleton": "./src/skeleton.tsx",
|
|
144
154
|
"./table": "./src/table.tsx",
|
|
155
|
+
"./data_grid": "./src/data_grid.tsx",
|
|
145
156
|
"./detail_row": "./src/detail_row.tsx",
|
|
146
157
|
"./scan_field": "./src/scan_field.tsx",
|
|
147
158
|
"./completion_state": "./src/completion_state.tsx",
|
|
@@ -175,7 +186,6 @@
|
|
|
175
186
|
"./date_filter": "./src/date_filter.tsx",
|
|
176
187
|
"./date_range_filter_field": "./src/date_range_filter_field.tsx",
|
|
177
188
|
"./time_picker": "./src/time_picker.tsx",
|
|
178
|
-
"./time_field": "./src/time_field.tsx",
|
|
179
189
|
"./date_calendar": "./src/date_calendar.tsx",
|
|
180
190
|
"./dialog": "./src/dialog.tsx",
|
|
181
191
|
"./drawer": "./src/drawer.tsx",
|
|
@@ -192,14 +202,12 @@
|
|
|
192
202
|
},
|
|
193
203
|
"./use_async_fn": "./src/use_async_fn.ts",
|
|
194
204
|
"./use_form": "./src/use_form.ts",
|
|
195
|
-
"./hover_action": "./src/hover_action.tsx",
|
|
196
205
|
"./scroll_to_bottom": "./src/scroll_to_bottom.tsx",
|
|
197
206
|
"./animation_fade_in": "./src/animation_fade_in.tsx",
|
|
198
207
|
"./json_panel": "./src/json_panel.tsx",
|
|
199
208
|
"./timeline": "./src/timeline.tsx",
|
|
200
209
|
"./loading": "./src/loading.tsx",
|
|
201
210
|
"./popover_header": "./src/popover_header.tsx",
|
|
202
|
-
"./command_menu": "./src/command_menu.tsx",
|
|
203
211
|
"./switch_button": "./src/switch_button.tsx",
|
|
204
212
|
"./alert": "./src/alert.tsx",
|
|
205
213
|
"./landmark": "./src/landmark.tsx",
|
|
@@ -233,6 +241,7 @@
|
|
|
233
241
|
"expo-image": ">=3.0.0",
|
|
234
242
|
"lucide-react": ">=0.460.0",
|
|
235
243
|
"lucide-react-native": ">=0.460.0",
|
|
244
|
+
"pdfjs-dist": ">=6.0.0",
|
|
236
245
|
"react": "^19.2.0",
|
|
237
246
|
"react-dom": "^19.2.0",
|
|
238
247
|
"react-native": ">=0.85.0",
|
|
@@ -246,6 +255,9 @@
|
|
|
246
255
|
"@lotics/xlsx": {
|
|
247
256
|
"optional": true
|
|
248
257
|
},
|
|
258
|
+
"pdfjs-dist": {
|
|
259
|
+
"optional": true
|
|
260
|
+
},
|
|
249
261
|
"expo-image": {
|
|
250
262
|
"optional": true
|
|
251
263
|
},
|
|
@@ -271,6 +283,7 @@
|
|
|
271
283
|
"@types/react-dom": "~19.2.2",
|
|
272
284
|
"@vitejs/plugin-react": "^4.3.4",
|
|
273
285
|
"lucide-react": "^0.562.0",
|
|
286
|
+
"pdfjs-dist": "^6.0.227",
|
|
274
287
|
"react-dom": "^19.2.0",
|
|
275
288
|
"react-native-web": "^0.21.0",
|
|
276
289
|
"vite": "^7.2.4"
|
package/src/accordion.tsx
CHANGED
|
@@ -99,7 +99,7 @@ export function AccordionHeader(props: AccordionHeaderProps) {
|
|
|
99
99
|
|
|
100
100
|
return (
|
|
101
101
|
<PressableHighlight
|
|
102
|
-
onPress={toggle}
|
|
102
|
+
focusRing onPress={toggle}
|
|
103
103
|
accessibilityRole="button"
|
|
104
104
|
accessibilityState={{ expanded }}
|
|
105
105
|
accessibilityLabel={accessibilityLabel}
|
package/src/agent_progress.tsx
CHANGED
|
@@ -54,7 +54,7 @@ export function AgentProgress(props: AgentProgressProps) {
|
|
|
54
54
|
) : null}
|
|
55
55
|
|
|
56
56
|
<PressableHighlight
|
|
57
|
-
onPress={() => setExpanded((e) => !e)}
|
|
57
|
+
focusRing onPress={() => setExpanded((e) => !e)}
|
|
58
58
|
style={styles.pill}
|
|
59
59
|
accessibilityLabel={expanded ? "Hide the agent's steps" : "Show the agent's steps"}
|
|
60
60
|
>
|
package/src/agent_run.tsx
CHANGED
|
@@ -197,7 +197,7 @@ function ActivityRow(props: {
|
|
|
197
197
|
);
|
|
198
198
|
if (onPress) {
|
|
199
199
|
return (
|
|
200
|
-
<PressableHighlight onPress={onPress} accessibilityRole="button" accessibilityLabel={accessibilityLabel} style={styles.row}>
|
|
200
|
+
<PressableHighlight focusRing onPress={onPress} accessibilityRole="button" accessibilityLabel={accessibilityLabel} style={styles.row}>
|
|
201
201
|
{inner}
|
|
202
202
|
</PressableHighlight>
|
|
203
203
|
);
|
package/src/breakdown.tsx
CHANGED
|
@@ -95,7 +95,7 @@ export function Breakdown(props: BreakdownProps) {
|
|
|
95
95
|
);
|
|
96
96
|
return onSelect ? (
|
|
97
97
|
<PressableHighlight
|
|
98
|
-
key={item.key}
|
|
98
|
+
focusRing key={item.key}
|
|
99
99
|
accessibilityRole="button"
|
|
100
100
|
accessibilityState={{ selected }}
|
|
101
101
|
accessibilityLabel={`${item.label}: ${formatValue(item.value)}`}
|
|
@@ -112,7 +112,7 @@ export function Breakdown(props: BreakdownProps) {
|
|
|
112
112
|
})}
|
|
113
113
|
{collapsible ? (
|
|
114
114
|
<PressableHighlight
|
|
115
|
-
accessibilityRole="button"
|
|
115
|
+
focusRing accessibilityRole="button"
|
|
116
116
|
accessibilityState={{ expanded }}
|
|
117
117
|
accessibilityLabel={expanded ? labels.less : labels.more(hidden)}
|
|
118
118
|
onPress={() => setExpanded((e) => !e)}
|