@lotics/ui 22.2.0 → 23.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -2
- package/MIGRATION.md +142 -2
- package/docs/ai_patterns.md +27 -25
- package/docs/catalog.md +119 -98
- package/docs/composition.md +134 -42
- package/docs/data_entry.md +67 -31
- package/docs/templates.md +86 -68
- package/examples/tpl_allocate.tsx +5 -5
- package/examples/tpl_attendance.tsx +2 -2
- package/examples/tpl_calendar.tsx +6 -6
- package/examples/tpl_dashboard.tsx +9 -9
- package/examples/tpl_dieline.tsx +2 -2
- package/examples/tpl_item_list.tsx +64 -37
- package/examples/tpl_lookup.tsx +5 -5
- package/examples/tpl_pick.tsx +6 -6
- package/examples/tpl_pivot.tsx +3 -3
- package/examples/tpl_record.tsx +919 -611
- package/examples/tpl_report.tsx +2 -2
- package/examples/tpl_rollup.tsx +5 -5
- package/examples/tpl_shifts.tsx +5 -5
- package/examples/tpl_statements.tsx +7 -7
- package/examples/tpl_stock.tsx +2 -2
- package/examples/tpl_task_board.tsx +42 -26
- package/examples/tpl_tower.tsx +6 -6
- package/package.json +3 -2
- package/src/agent_run.tsx +40 -25
- package/src/breakdown.tsx +1 -1
- package/src/calendar/calendar_view.tsx +1 -1
- package/src/change_review.tsx +9 -8
- package/src/chip_group.tsx +12 -2
- package/src/choice_list.tsx +2 -2
- package/src/confidence.tsx +2 -2
- package/src/data_grid.tsx +1 -1
- package/src/detail_row.tsx +50 -58
- package/src/file_dropzone.tsx +1 -1
- package/src/file_gallery_modal.tsx +3 -3
- package/src/file_rows.tsx +1 -1
- package/src/finding.tsx +4 -4
- package/src/form_field.tsx +1 -1
- package/src/format_date.ts +2 -2
- package/src/heatmap.tsx +1 -1
- package/src/inline_button.tsx +84 -0
- package/src/inline_date_picker.tsx +17 -10
- package/src/inline_edit.tsx +298 -59
- package/src/inline_member_select.tsx +8 -3
- package/src/inline_number_input.tsx +11 -4
- package/src/inline_select.tsx +26 -13
- package/src/inline_text_input.tsx +12 -4
- package/src/inline_time_picker.tsx +10 -4
- package/src/ledger.tsx +2 -2
- package/src/locale.tsx +7 -3
- package/src/matrix.tsx +1 -1
- package/src/number_input.tsx +18 -7
- package/src/pipeline.tsx +1 -1
- package/src/popover.tsx +1 -1
- package/src/press_door.tsx +1 -1
- package/src/pressable_highlight.tsx +1 -1
- package/src/progress_bar.tsx +3 -3
- package/src/record_summary.tsx +2 -2
- package/src/result_header.tsx +2 -2
- package/src/sequence.tsx +170 -0
- package/src/share_or_download.ts +2 -2
- package/src/step_progress.tsx +7 -5
- package/src/stepper.tsx +1 -1
- package/src/task.tsx +6 -6
- package/src/text_input_field.tsx +19 -1
- package/src/text_utils.ts +1 -1
- package/src/linked_record_box.tsx +0 -157
package/examples/tpl_report.tsx
CHANGED
|
@@ -21,7 +21,7 @@ import { formatMoney } from "@lotics/ui/format_money";
|
|
|
21
21
|
import { formatDate, parseDate } from "@lotics/ui/format_date";
|
|
22
22
|
|
|
23
23
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
-
// Template
|
|
24
|
+
// Template, Lookup report — answer a question by SCOPING a dataset, not by
|
|
25
25
|
// browsing it. The scope bar leads: a period (header band) + ONE search key
|
|
26
26
|
// chosen from several mutually-exclusive dimensions (SegmentedControl → the
|
|
27
27
|
// matching picker; switching the key resets the value). That scope drives
|
|
@@ -363,7 +363,7 @@ export function TplReport() {
|
|
|
363
363
|
|
|
364
364
|
<CardFooter>
|
|
365
365
|
<Text size="xs" color="muted" tabular style={{ flex: 1 }}>
|
|
366
|
-
{`${rows.length.toLocaleString("en-US")} payments
|
|
366
|
+
{`${rows.length.toLocaleString("en-US")} payments totalling ${formatMoney(total, { compact: true })}${chips.length > 0 ? " (filtered)" : ""}`}
|
|
367
367
|
</Text>
|
|
368
368
|
<Pagination
|
|
369
369
|
page={page}
|
package/examples/tpl_rollup.tsx
CHANGED
|
@@ -12,7 +12,7 @@ import { PressableHighlight } from "@lotics/ui/pressable_highlight";
|
|
|
12
12
|
import { Sparkline } from "@lotics/ui/sparkline";
|
|
13
13
|
|
|
14
14
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
-
// Template
|
|
15
|
+
// Template, Hierarchy rollup — hierarchical totals with drill: region →
|
|
16
16
|
// site → lane, every level carrying the SAME aligned metric columns so a
|
|
17
17
|
// bad number can be chased down the tree ("which branch drags the
|
|
18
18
|
// total?"). Pure composition: nested Accordions + a fixed-width column
|
|
@@ -163,7 +163,7 @@ export function TplRollup() {
|
|
|
163
163
|
<Card style={{ padding: 0 }}>
|
|
164
164
|
<CardHeader>
|
|
165
165
|
<CardHeaderTitle info="The same three columns at every level — totals are volume-weighted, so a flagged region always traces to flagged lanes inside it.">
|
|
166
|
-
By region
|
|
166
|
+
By region, site, lane
|
|
167
167
|
</CardHeaderTitle>
|
|
168
168
|
</CardHeader>
|
|
169
169
|
|
|
@@ -239,7 +239,7 @@ export function TplRollup() {
|
|
|
239
239
|
|
|
240
240
|
<CardFooter>
|
|
241
241
|
<Text size="xs" color="muted" tabular style={{ flex: 1 }}>
|
|
242
|
-
{`Network
|
|
242
|
+
{`Network: ${NETWORK.volume.toLocaleString("en-US")} units, ${NETWORK.ontime}% on-time, ${NETWORK.margin}% margin`}
|
|
243
243
|
</Text>
|
|
244
244
|
</CardFooter>
|
|
245
245
|
</Card>
|
|
@@ -263,14 +263,14 @@ export function TplRollup() {
|
|
|
263
263
|
<Divider />
|
|
264
264
|
<KVRow label="On-time">
|
|
265
265
|
<Text size="sm" tabular color={openLane.ontime < ONTIME_TARGET ? "danger" : "default"}>
|
|
266
|
-
{`${openLane.ontime}%
|
|
266
|
+
{`${openLane.ontime}% (target ${ONTIME_TARGET}%)`}
|
|
267
267
|
</Text>
|
|
268
268
|
</KVRow>
|
|
269
269
|
<Divider />
|
|
270
270
|
<KVRow label="Margin"><Text size="sm" tabular>{`${openLane.margin}%`}</Text></KVRow>
|
|
271
271
|
</View>
|
|
272
272
|
<View style={{ gap: 8 }}>
|
|
273
|
-
<Text size="xs" color="muted" weight="medium">Volume
|
|
273
|
+
<Text size="xs" color="muted" weight="medium">Volume (last 12 weeks)</Text>
|
|
274
274
|
<Sparkline
|
|
275
275
|
data={Array.from({ length: 12 }, (_, w) => openLane.volume / 4 + (hash(w, openLane.volume) - 500) * (openLane.volume / 12000))}
|
|
276
276
|
width={200}
|
package/examples/tpl_shifts.tsx
CHANGED
|
@@ -20,7 +20,7 @@ import { ProgressBar } from "@lotics/ui/progress_bar";
|
|
|
20
20
|
import { Screen } from "@lotics/ui/screen_router";
|
|
21
21
|
|
|
22
22
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
-
// Template
|
|
23
|
+
// Template, Shift staffing — two-sided allocation: members SIGN UP, the
|
|
24
24
|
// manager DECIDES and FILLS. Three bands reuse the execution grammar:
|
|
25
25
|
// the coverage board is the overview (shifts × days, each cell its
|
|
26
26
|
// filled/required state — press to open the slot), the signup rail is the
|
|
@@ -109,7 +109,7 @@ const INITIAL_SIGNUPS: Signup[] = [
|
|
|
109
109
|
|
|
110
110
|
const dayOf = (k: DayKey) => DAYS.find((d) => d.key === k)!;
|
|
111
111
|
const shiftOf = (k: ShiftKey) => SHIFTS.find((s) => s.key === k)!;
|
|
112
|
-
const slotLabel = (d: DayKey, s: ShiftKey) => `${dayOf(d).label} ${dayOf(d).date}
|
|
112
|
+
const slotLabel = (d: DayKey, s: ShiftKey) => `${dayOf(d).label} ${dayOf(d).date}, ${shiftOf(s).label} ${shiftOf(s).hours}`;
|
|
113
113
|
|
|
114
114
|
function KVRow({ label, children }: { label: string; children: React.ReactNode }) {
|
|
115
115
|
return (
|
|
@@ -185,7 +185,7 @@ export function TplShifts() {
|
|
|
185
185
|
<View style={{ width: "100%", maxWidth: 1100, alignSelf: "center", gap: 16 }}>
|
|
186
186
|
{/* header band */}
|
|
187
187
|
<View style={{ gap: 2 }}>
|
|
188
|
-
<Text size="xxl" weight="semibold">Shift staffing
|
|
188
|
+
<Text size="xxl" weight="semibold">Shift staffing (week 15–21/06)</Text>
|
|
189
189
|
<Text size="sm" color="muted">Members sign up, you allocate — fill every slot without burning anyone out</Text>
|
|
190
190
|
</View>
|
|
191
191
|
|
|
@@ -254,7 +254,7 @@ export function TplShifts() {
|
|
|
254
254
|
</View>
|
|
255
255
|
<CardFooter>
|
|
256
256
|
<Text size="xs" color="muted" tabular style={{ flex: 1 }}>
|
|
257
|
-
{`${totalFilled} of ${totalRequired} positions filled
|
|
257
|
+
{`${totalFilled} of ${totalRequired} positions filled; accepting a signup or adding a member updates the board live`}
|
|
258
258
|
</Text>
|
|
259
259
|
</CardFooter>
|
|
260
260
|
</Card>
|
|
@@ -312,7 +312,7 @@ export function TplShifts() {
|
|
|
312
312
|
)}
|
|
313
313
|
<CardFooter>
|
|
314
314
|
<Text size="xs" color="muted" tabular style={{ flex: 1 }}>
|
|
315
|
-
{`${signups.length} pending
|
|
315
|
+
{`${signups.length} pending; first come, first served unless coverage says otherwise`}
|
|
316
316
|
</Text>
|
|
317
317
|
</CardFooter>
|
|
318
318
|
</Card>
|
|
@@ -6,7 +6,7 @@ import { Button } from "@lotics/ui/button";
|
|
|
6
6
|
import { Divider } from "@lotics/ui/divider";
|
|
7
7
|
|
|
8
8
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
-
// Template
|
|
9
|
+
// Template, Financial statements — the income statement, the balance sheet,
|
|
10
10
|
// and the cash flow statement, all speaking ONE statement grammar: column
|
|
11
11
|
// captions right-aligned over fixed money columns, items indented under their
|
|
12
12
|
// group, a hairline rule above every subtotal, the grand total DOUBLE-RULED
|
|
@@ -35,8 +35,8 @@ interface Line {
|
|
|
35
35
|
const COL_WIDTH = 124;
|
|
36
36
|
|
|
37
37
|
/** One statement: right-aligned column captions, then the lines. The grammar —
|
|
38
|
-
* header (group label, no values)
|
|
39
|
-
* above, semibold)
|
|
38
|
+
* header (group label, no values), item (indented, plain), subtotal (rule
|
|
39
|
+
* above, semibold), total (DOUBLE rule above, semibold) — covers all three
|
|
40
40
|
* statements; only the data differs. */
|
|
41
41
|
function Statement({ columns, lines }: { columns: string[]; lines: Line[] }) {
|
|
42
42
|
const cells = (l: Line, weight?: "medium" | "semibold") =>
|
|
@@ -110,7 +110,7 @@ function Statement({ columns, lines }: { columns: string[]; lines: Line[] }) {
|
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
/** The statement page: title + the one meta line (entity
|
|
113
|
+
/** The statement page: title + the one meta line (entity, period, basis,
|
|
114
114
|
* currency, stated ONCE so cells stay currency-free) + Export, then the
|
|
115
115
|
* statement column. */
|
|
116
116
|
function StatementPage({ title, meta, children }: { title: string; meta: string; children: ReactNode }) {
|
|
@@ -158,7 +158,7 @@ export function TplIncomeStatement() {
|
|
|
158
158
|
{ kind: "total", label: "Net income", values: [81_840 * K, 70_160 * K] },
|
|
159
159
|
];
|
|
160
160
|
return (
|
|
161
|
-
<StatementPage title="Income statement" meta="VITTORIA Logistics
|
|
161
|
+
<StatementPage title="Income statement" meta="VITTORIA Logistics, month ended 30/06/2026, VND">
|
|
162
162
|
<Statement columns={["Jun 2026", "May 2026"]} lines={lines} />
|
|
163
163
|
</StatementPage>
|
|
164
164
|
);
|
|
@@ -187,7 +187,7 @@ export function TplBalanceSheet() {
|
|
|
187
187
|
{ kind: "total", label: "Total liabilities & equity", values: [971_900 * K, 926_800 * K] },
|
|
188
188
|
];
|
|
189
189
|
return (
|
|
190
|
-
<StatementPage title="Balance sheet" meta="VITTORIA Logistics
|
|
190
|
+
<StatementPage title="Balance sheet" meta="VITTORIA Logistics, as at 30/06/2026, VND">
|
|
191
191
|
<Statement columns={["30/06/2026", "31/05/2026"]} lines={lines} />
|
|
192
192
|
</StatementPage>
|
|
193
193
|
);
|
|
@@ -214,7 +214,7 @@ export function TplCashflow() {
|
|
|
214
214
|
{ kind: "total", label: "Cash at 30/06/2026", values: [400_800 * K] },
|
|
215
215
|
];
|
|
216
216
|
return (
|
|
217
|
-
<StatementPage title="Cash flow statement" meta="VITTORIA Logistics
|
|
217
|
+
<StatementPage title="Cash flow statement" meta="VITTORIA Logistics, month ended 30/06/2026, VND">
|
|
218
218
|
<Statement columns={["Jun 2026"]} lines={lines} />
|
|
219
219
|
</StatementPage>
|
|
220
220
|
);
|
package/examples/tpl_stock.tsx
CHANGED
|
@@ -18,7 +18,7 @@ import { SearchInput } from "@lotics/ui/search_input";
|
|
|
18
18
|
import { formatMoney } from "@lotics/ui/format_money";
|
|
19
19
|
|
|
20
20
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
-
// Template
|
|
21
|
+
// Template, Drill-down overview — monitoring a population too large to browse
|
|
22
22
|
// (10,000+ units). The funnel: KPI strip (population health) → Breakdown
|
|
23
23
|
// cards (composition by dimension; pressing a segment is the drill-down) →
|
|
24
24
|
// dismissible facet chips + search → PAGINATED register (the data is the
|
|
@@ -334,7 +334,7 @@ export function TplStock() {
|
|
|
334
334
|
|
|
335
335
|
<CardFooter>
|
|
336
336
|
<Text size="xs" color="muted" tabular style={{ flex: 1 }}>
|
|
337
|
-
{`${filtered.length.toLocaleString("en-US")} units in this slice
|
|
337
|
+
{`${filtered.length.toLocaleString("en-US")} units in this slice, worth ${formatMoney(filtered.reduce((s, u) => s + u.value, 0), { compact: true })}`}
|
|
338
338
|
</Text>
|
|
339
339
|
<Pagination
|
|
340
340
|
page={page}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
-
import { Pressable, ScrollView, StyleSheet, View } from "react-native";
|
|
2
|
+
import { Pressable, ScrollView, StyleSheet, View, type ViewStyle } from "react-native";
|
|
3
3
|
import { Text } from "@lotics/ui/text";
|
|
4
4
|
import { TextLink } from "@lotics/ui/text_link";
|
|
5
5
|
import { colors, solid, asColorName } from "@lotics/ui/colors";
|
|
@@ -24,7 +24,8 @@ import { Popover, PopoverTrigger, PopoverContent } from "@lotics/ui/popover";
|
|
|
24
24
|
import { FilesEditor, FilesEditorBar, FilesEditorBarSpacer, FilesEditorRemove, FilesEditorSelect, FilesEditorUpload } from "@lotics/ui/files_editor";
|
|
25
25
|
import { FileThumbnail, type DisplayFile } from "@lotics/ui/file_thumbnail";
|
|
26
26
|
import { DataGrid, gridRowStyle, type DataGridColumn, type DataGridGroup } from "@lotics/ui/data_grid";
|
|
27
|
-
import { CONTROL_RADIUS } from "@lotics/ui/control_surface";
|
|
27
|
+
import { CONTROL_RADIUS, HOVER_BORDER } from "@lotics/ui/control_surface";
|
|
28
|
+
import { INLINE_CONTROL_HEIGHT } from "@lotics/ui/inline_edit";
|
|
28
29
|
import { cycleSort, sortBy, type SortState } from "@lotics/ui/sort_header";
|
|
29
30
|
import { Composer } from "@lotics/ui/composer";
|
|
30
31
|
import { AgentRun } from "@lotics/ui/agent_run";
|
|
@@ -40,7 +41,7 @@ type Part = UIMessagePart<UIDataTypes, UITools>;
|
|
|
40
41
|
type TagOption = { value: string; label: string };
|
|
41
42
|
|
|
42
43
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
-
// Template
|
|
44
|
+
// Template, Task board — the manager's view, built on the `DataGrid` primitive
|
|
44
45
|
// (the inline-managed grouped table). The board OWNS its data, state, toolbar,
|
|
45
46
|
// columns and per-group add; `DataGrid` owns the sortable header, the collapsible
|
|
46
47
|
// grouped sections and the aligned rows.
|
|
@@ -170,13 +171,13 @@ function sortValue(t: Task, key: string): string | number {
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
|
|
173
|
-
// ── AI
|
|
174
|
+
// ── AI, draft tasks from notes — agent proposes, the manager keeps/edits/drops ──
|
|
174
175
|
type Phase = "idle" | "reading" | "review" | "done";
|
|
175
176
|
interface Proposal { id: string; title: string; due: string | null; confidence: ConfidenceLevel; source: string }
|
|
176
177
|
const DRAFT_STEPS: { label: string; detail: string }[] = [
|
|
177
178
|
{ label: "Split the notes into candidate tasks", detail: "6 lines" },
|
|
178
179
|
{ label: "Drop greetings and lines that aren't actions", detail: "kept 4" },
|
|
179
|
-
{ label: "Infer a due date from each phrase", detail: "2 dated
|
|
180
|
+
{ label: "Infer a due date from each phrase", detail: "2 dated, 2 left open" },
|
|
180
181
|
];
|
|
181
182
|
const DRAFTED: Proposal[] = [
|
|
182
183
|
{ id: "p1", title: "Send VITTORIA the revised quote", due: day(1), confidence: "high", source: "“…told VITTORIA they'd have the updated quote by tomorrow.”" },
|
|
@@ -269,10 +270,10 @@ export function TplTaskBoard() {
|
|
|
269
270
|
}
|
|
270
271
|
|
|
271
272
|
const propertyCols: DataGridColumn<Task>[] = [
|
|
272
|
-
{ key: "assignee", label: "Assignee", width: 160, sortable: true, cell: (t) => <InlineMemberSelect variant="
|
|
273
|
-
{ key: "due", label: "Due", width: 140, sortable: true, cell: (t) => <InlineDatePicker variant="
|
|
274
|
-
{ key: "status", label: "Status", width: 130, sortable: true, cell: (t) => <InlineSelect variant="
|
|
275
|
-
{ key: "tags", label: "Tags", width: 168, cell: (t) => <InlineSelect multi searchable allowCustom variant="
|
|
273
|
+
{ key: "assignee", label: "Assignee", width: 160, sortable: true, cell: (t) => <InlineMemberSelect variant="bare" members={MEMBERS} value={t.ownerId} onSave={(id) => patch(t.id, { ownerId: id })} onClear={() => patch(t.id, { ownerId: null })} placeholder="Unassigned" accessibilityLabel="Assignee" /> },
|
|
274
|
+
{ key: "due", label: "Due", width: 140, sortable: true, cell: (t) => <InlineDatePicker variant="bare" value={t.due} optionalTime onSave={(v) => patch(t.id, { due: v })} onClear={() => patch(t.id, { due: null })} placeholder="No date" accessibilityLabel="Due date" /> },
|
|
275
|
+
{ key: "status", label: "Status", width: 130, sortable: true, cell: (t) => <InlineSelect variant="bare" value={t.status} options={STATUS_OPTIONS} onSave={(s) => patch(t.id, { status: s })} renderSelected={renderStatusBadge} renderOptionContent={renderStatusBadge} accessibilityLabel="Status" /> },
|
|
276
|
+
{ key: "tags", label: "Tags", width: 168, cell: (t) => <InlineSelect multi searchable allowCustom variant="bare" value={t.tags.map((tag) => tag.value)} onSave={(next) => patch(t.id, { tags: next.map(tagOf) })} options={TAG_OPTIONS} renderSelected={renderTagBadge} renderOptionContent={renderTagBadge} placeholder="Add tags" accessibilityLabel="Tags" /> },
|
|
276
277
|
];
|
|
277
278
|
const filesCol: DataGridColumn<Task> = {
|
|
278
279
|
key: "files", label: "Files", width: 100, sortable: false,
|
|
@@ -299,7 +300,7 @@ export function TplTaskBoard() {
|
|
|
299
300
|
const visibleProps = propertyCols.filter((c) => c.key !== groupBy);
|
|
300
301
|
const addCols = [...visibleProps, filesCol, actionCol, menuCol];
|
|
301
302
|
const columns: DataGridColumn<Task>[] = [
|
|
302
|
-
{ key: "title", label: "Task", sortable: true, cell: (t) => <InlineTextInput variant="
|
|
303
|
+
{ key: "title", label: "Task", sortable: true, cell: (t) => <InlineTextInput variant="bare" value={t.title} onSave={(v) => patch(t.id, { title: v })} struck={t.status === "done"} accessibilityLabel="Title" /> },
|
|
303
304
|
...addCols,
|
|
304
305
|
];
|
|
305
306
|
|
|
@@ -446,11 +447,11 @@ function TaskProposal({ proposal, onEdit }: { proposal: Proposal; onEdit: (id: s
|
|
|
446
447
|
<View style={{ gap: 10 }}>
|
|
447
448
|
<View style={{ flexDirection: "row", alignItems: "flex-start", gap: 12 }}>
|
|
448
449
|
<View style={{ flex: 1 }}>
|
|
449
|
-
<InlineTextInput variant="
|
|
450
|
+
<InlineTextInput variant="bare" value={proposal.title} onSave={(v) => onEdit(proposal.id, { title: v })} accessibilityLabel="Task title" />
|
|
450
451
|
</View>
|
|
451
452
|
</View>
|
|
452
453
|
<View style={{ width: 220 }}>
|
|
453
|
-
<InlineDatePicker variant="
|
|
454
|
+
<InlineDatePicker variant="bare" value={proposal.due} optionalTime onSave={(v) => onEdit(proposal.id, { due: v })} placeholder="No date" accessibilityLabel="Due date" />
|
|
454
455
|
</View>
|
|
455
456
|
<View style={styles.source}><Text size="xs" color="muted">{proposal.source}</Text></View>
|
|
456
457
|
</View>
|
|
@@ -515,13 +516,19 @@ function RowActionCell({ task, result, onAttach, onApprove }: { task: Task; resu
|
|
|
515
516
|
</View>
|
|
516
517
|
);
|
|
517
518
|
}
|
|
518
|
-
//
|
|
519
|
-
//
|
|
519
|
+
// NO CELL TREATMENT. Every other column here is a VALUE and wears the `bare`
|
|
520
|
+
// field surface; an action is not a value, so it gets no frame, no hover fill
|
|
521
|
+
// and no cell-sized hit area — otherwise the row shows two things that look
|
|
522
|
+
// editable and only one is.
|
|
523
|
+
//
|
|
524
|
+
// The LINK owns the press, which is also what puts the pointer cursor on the
|
|
525
|
+
// WORDS rather than across the whole column: a cell-wide pressable makes the
|
|
526
|
+
// cursor change over empty space, promising a target that isn't there.
|
|
520
527
|
const onPress = a.kind === "attach" ? () => onAttach(task.id) : a.kind === "approve" ? () => onApprove(task.id) : () => {};
|
|
521
528
|
return (
|
|
522
|
-
<
|
|
523
|
-
<TextLink size="sm" numberOfLines={1}>{a.label}</TextLink>
|
|
524
|
-
</
|
|
529
|
+
<View style={styles.action}>
|
|
530
|
+
<TextLink size="sm" numberOfLines={1} onPress={onPress}>{a.label}</TextLink>
|
|
531
|
+
</View>
|
|
525
532
|
);
|
|
526
533
|
}
|
|
527
534
|
|
|
@@ -546,10 +553,10 @@ function TaskAddRow({ preset, columns, onAdd }: { preset: Partial<Task>; columns
|
|
|
546
553
|
// Files / action / ⋯ act on a task that already exists, so the add row leaves
|
|
547
554
|
// those columns empty — the widths are still reserved so the columns stay aligned.
|
|
548
555
|
const editor = (key: string) =>
|
|
549
|
-
key === "assignee" ? <InlineMemberSelect variant="
|
|
550
|
-
: key === "due" ? <InlineDatePicker variant="
|
|
551
|
-
: key === "status" ? <InlineSelect variant="
|
|
552
|
-
: key === "tags" ? <InlineSelect multi searchable allowCustom variant="
|
|
556
|
+
key === "assignee" ? <InlineMemberSelect variant="bare" members={MEMBERS} value={ownerId} onSave={setOwnerId} placeholder="Assignee" accessibilityLabel="Assignee" />
|
|
557
|
+
: key === "due" ? <InlineDatePicker variant="bare" value={due} optionalTime onSave={setDue} placeholder="No date" accessibilityLabel="Due date" />
|
|
558
|
+
: key === "status" ? <InlineSelect variant="bare" value={status} options={STATUS_OPTIONS} onSave={setStatus} renderSelected={renderStatusBadge} renderOptionContent={renderStatusBadge} accessibilityLabel="Status" />
|
|
559
|
+
: key === "tags" ? <InlineSelect multi searchable allowCustom variant="bare" value={tags.map((tag) => tag.value)} onSave={(next) => setTags(next.map(tagOf))} options={TAG_OPTIONS} renderSelected={renderTagBadge} renderOptionContent={renderTagBadge} placeholder="Add tags" accessibilityLabel="Tags" />
|
|
553
560
|
: null;
|
|
554
561
|
|
|
555
562
|
return (
|
|
@@ -576,15 +583,24 @@ const styles = StyleSheet.create({
|
|
|
576
583
|
addField: { flex: 1, flexDirection: "row", alignItems: "center", gap: 8, paddingRight: 4 },
|
|
577
584
|
// Match the CELL height (40) so the files field lines up with the others — and that
|
|
578
585
|
// height affords a larger thumbnail. Hover = the background-tint WASH (zinc-100), the
|
|
579
|
-
// standard CELL language (same as the variant="
|
|
580
|
-
|
|
581
|
-
|
|
586
|
+
// standard CELL language (same as the variant="bare" editors beside these).
|
|
587
|
+
// The files cell is a VALUE like every other cell in the row, so it wears the
|
|
588
|
+
// `bare` editors' surface exactly: nothing at rest, the kit's border on hover,
|
|
589
|
+
// and the ARROW cursor — a pointer would read as a button and single this cell
|
|
590
|
+
// out from the editors either side of it.
|
|
591
|
+
clip: {
|
|
592
|
+
flexDirection: "row", alignItems: "center", gap: 6, minHeight: INLINE_CONTROL_HEIGHT,
|
|
593
|
+
paddingHorizontal: 8, borderRadius: CONTROL_RADIUS,
|
|
594
|
+
borderWidth: 1, borderColor: "transparent",
|
|
595
|
+
...({ cursor: "auto", transitionProperty: "border-color", transitionDuration: "0.12s" } as ViewStyle),
|
|
596
|
+
},
|
|
597
|
+
clipHover: { borderColor: HOVER_BORDER },
|
|
582
598
|
clipPreview: { flexDirection: "row", alignItems: "center", gap: 6 },
|
|
583
599
|
summary: { flexDirection: "row", alignItems: "center", gap: 4 },
|
|
584
600
|
summaryDot: { width: 8, height: 8, borderRadius: 999 },
|
|
585
601
|
result: { flexDirection: "row", alignItems: "center", gap: 6, minHeight: 40 },
|
|
586
602
|
// Fills the cell (height + width) so the link's hit area matches the cells; wash hover.
|
|
587
|
-
|
|
588
|
-
|
|
603
|
+
// Alignment only — the action column paints nothing.
|
|
604
|
+
action: { justifyContent: "center", minHeight: INLINE_CONTROL_HEIGHT, flexDirection: "row" },
|
|
589
605
|
source: { borderLeftWidth: 2, borderLeftColor: colors.zinc[200], paddingLeft: 10 },
|
|
590
606
|
});
|
package/examples/tpl_tower.tsx
CHANGED
|
@@ -15,7 +15,7 @@ import { StatusGrid, StatusLegend } from "@lotics/ui/status_grid";
|
|
|
15
15
|
import { Timeline } from "@lotics/ui/timeline";
|
|
16
16
|
|
|
17
17
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
-
// Template
|
|
18
|
+
// Template, Status grid — continuous monitoring of hundreds of live
|
|
19
19
|
// units (machines, gates, chargers, sensors). Three bands answer the three
|
|
20
20
|
// ops questions in order: KPI strip + StatusGrid = "is everything OK right
|
|
21
21
|
// now?" (scan for red; the legend drills a state), exceptions rail =
|
|
@@ -262,7 +262,7 @@ export function TplTower() {
|
|
|
262
262
|
</View>
|
|
263
263
|
<CardFooter>
|
|
264
264
|
<Text size="xs" color="muted" tabular style={{ flex: 1 }}>
|
|
265
|
-
{`12 of ${EXCEPTIONS.length}
|
|
265
|
+
{`12 of ${EXCEPTIONS.length} — the drawer's › walks the full queue`}
|
|
266
266
|
</Text>
|
|
267
267
|
<Button title="View all" color="muted" onPress={() => {}} />
|
|
268
268
|
</CardFooter>
|
|
@@ -273,7 +273,7 @@ export function TplTower() {
|
|
|
273
273
|
<Card style={{ padding: 0 }}>
|
|
274
274
|
<CardHeader>
|
|
275
275
|
<CardHeaderTitle info="Alarm count by site and day. Intensity scales to the worst cell — press one to see what drove that day.">
|
|
276
|
-
Alarm pattern
|
|
276
|
+
Alarm pattern (last 14 days)
|
|
277
277
|
</CardHeaderTitle>
|
|
278
278
|
</CardHeader>
|
|
279
279
|
<View style={{ paddingHorizontal: 20, paddingVertical: 16, gap: 12 }}>
|
|
@@ -288,8 +288,8 @@ export function TplTower() {
|
|
|
288
288
|
<Text size="sm" color="muted" tabular>
|
|
289
289
|
{hotCell
|
|
290
290
|
? hotValue === 0
|
|
291
|
-
? `${siteOf(hotCell.row as SiteKey).label}
|
|
292
|
-
: `${siteOf(hotCell.row as SiteKey).label}
|
|
291
|
+
? `${siteOf(hotCell.row as SiteKey).label}, ${hotCell.col} — no alarms`
|
|
292
|
+
: `${siteOf(hotCell.row as SiteKey).label}, ${hotCell.col} — ${hotValue} alarms; most frequent: ${WARN_MSGS[hash(DAYS.indexOf(hotCell.col), 8) % WARN_MSGS.length]}`
|
|
293
293
|
: "Press a cell to see what drove that day"}
|
|
294
294
|
</Text>
|
|
295
295
|
</View>
|
|
@@ -312,7 +312,7 @@ export function TplTower() {
|
|
|
312
312
|
<Badge label={siteOf(unit.site).label} />
|
|
313
313
|
</View>
|
|
314
314
|
{unit.message ? (
|
|
315
|
-
<Text size="sm" color="danger">{`${unit.message}
|
|
315
|
+
<Text size="sm" color="danger">{`${unit.message} (${formatAge(unit.ageMin ?? 0)} ago)`}</Text>
|
|
316
316
|
) : null}
|
|
317
317
|
<View style={{ gap: 6 }}>
|
|
318
318
|
<KVRow label="Site"><Text size="sm">{siteOf(unit.site).label}</Text></KVRow>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "23.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./vite": {
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
"./badge": "./src/badge.tsx",
|
|
138
138
|
"./callout": "./src/callout.tsx",
|
|
139
139
|
"./inset": "./src/inset.tsx",
|
|
140
|
-
"./
|
|
140
|
+
"./inline_button": "./src/inline_button.tsx",
|
|
141
141
|
"./inline_edit": "./src/inline_edit.tsx",
|
|
142
142
|
"./inline_static": "./src/inline_static.tsx",
|
|
143
143
|
"./inline_text_input": "./src/inline_text_input.tsx",
|
|
@@ -199,6 +199,7 @@
|
|
|
199
199
|
"./list_item": "./src/list_item.tsx",
|
|
200
200
|
"./section_heading": "./src/section_heading.tsx",
|
|
201
201
|
"./section_stack": "./src/section_stack.tsx",
|
|
202
|
+
"./sequence": "./src/sequence.tsx",
|
|
202
203
|
"./separator": "./src/separator.tsx",
|
|
203
204
|
"./stack": "./src/stack.tsx",
|
|
204
205
|
"./status_badge": "./src/status_badge.tsx",
|
package/src/agent_run.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { colors } from "./colors";
|
|
|
4
4
|
import { Text } from "./text";
|
|
5
5
|
import { Button } from "./button";
|
|
6
6
|
import { Icon, type IconName } from "./icon";
|
|
7
|
+
import { Callout, CalloutActions, CalloutText } from "./callout";
|
|
7
8
|
import { Markdown } from "./markdown";
|
|
8
9
|
import { JsonPanel, stringifyData } from "./json_panel";
|
|
9
10
|
import { PressableHighlight } from "./pressable_highlight";
|
|
@@ -139,7 +140,7 @@ const INK = colors.zinc[700];
|
|
|
139
140
|
* tools it calls, in the order they happened. While the agent is mid-tools the
|
|
140
141
|
* tail group is a SINGLE pulsing row whose label swaps in place as each call
|
|
141
142
|
* fires (not a growing stack of dots); once prose resumes the group settles into
|
|
142
|
-
* one row — "{final action}
|
|
143
|
+
* one row — "{final action} ({n} steps)" — that EXPANDS on press to the steps in
|
|
143
144
|
* between. Text segments render as prose. The run always ends on the agent's text.
|
|
144
145
|
* Pair with `Composer` + `ChangeReview`.
|
|
145
146
|
*/
|
|
@@ -187,27 +188,44 @@ export function AgentRun(props: AgentRunProps) {
|
|
|
187
188
|
/>
|
|
188
189
|
);
|
|
189
190
|
})}
|
|
190
|
-
{/* A run-level BREAKING error terminates the feed
|
|
191
|
-
|
|
192
|
-
A
|
|
191
|
+
{/* A run-level BREAKING error terminates the feed. It lives outside `parts`
|
|
192
|
+
(the stream failed), so the caller passes it explicitly.
|
|
193
|
+
A CONTAINED surface, not another step row: the run didn't advance, it
|
|
194
|
+
stopped, and a tinted panel says that at a glance where a bare red line
|
|
195
|
+
in the step gutter reads as one more step that happened to fail. It also
|
|
196
|
+
gives the retry a container — free-floating, a full-weight button in a
|
|
197
|
+
column of text reads as a stray control — and `Callout` carries
|
|
198
|
+
`role="alert"`, which the plain row did not.
|
|
199
|
+
FULL WIDTH, unlike `reasoning` / `rowDetail`: those indent to a step's
|
|
200
|
+
text column because they are that step's sub-content. This hangs under
|
|
201
|
+
no step — it is the whole run ending — so indenting it would make a
|
|
202
|
+
run-level failure read as the last step's detail. */}
|
|
193
203
|
{props.error ? (
|
|
194
|
-
<
|
|
195
|
-
<
|
|
196
|
-
<View style={styles.dotCol}>
|
|
197
|
-
<Icon name="circle-alert" size={17} color={colors.red[500]} />
|
|
198
|
-
</View>
|
|
199
|
-
<View style={styles.rowBody}>
|
|
200
|
-
<Text size="sm" color="danger">
|
|
201
|
-
{props.error}
|
|
202
|
-
</Text>
|
|
203
|
-
</View>
|
|
204
|
-
</View>
|
|
204
|
+
<Callout tone="error">
|
|
205
|
+
<CalloutText>{props.error}</CalloutText>
|
|
205
206
|
{onRetry ? (
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
<CalloutActions>
|
|
208
|
+
{/* PRIMARY, deliberately. `Callout` is a calm panel by design (a
|
|
209
|
+
50 tint behind a soft 100 border), so a filled button inside it
|
|
210
|
+
is not emphasis twice — it is the one action on a quiet
|
|
211
|
+
surface. Every unfilled variant loses here: default and muted
|
|
212
|
+
hover to zinc-100, a grey wash over the red tint, and
|
|
213
|
+
danger-secondary puts red ink on a red-50 ground, the weakest
|
|
214
|
+
contrast of the options. The run just died; the only way out
|
|
215
|
+
should be the most legible thing in the panel.
|
|
216
|
+
Still a Button, never a link: a link navigates, this re-fires a
|
|
217
|
+
run, and that distinction is what a screen reader and the
|
|
218
|
+
keyboard both rely on. */}
|
|
219
|
+
<Button
|
|
220
|
+
title={locale.agentRun.retry}
|
|
221
|
+
color="primary"
|
|
222
|
+
icon="rotate-ccw"
|
|
223
|
+
alignSelf="flex-start"
|
|
224
|
+
onPress={onRetry}
|
|
225
|
+
/>
|
|
226
|
+
</CalloutActions>
|
|
209
227
|
) : null}
|
|
210
|
-
</
|
|
228
|
+
</Callout>
|
|
211
229
|
) : null}
|
|
212
230
|
</View>
|
|
213
231
|
);
|
|
@@ -244,7 +262,7 @@ function ActivityRow(props: {
|
|
|
244
262
|
return <View style={styles.row}>{inner}</View>;
|
|
245
263
|
}
|
|
246
264
|
|
|
247
|
-
// The step label, plus a muted "
|
|
265
|
+
// The step label, plus a muted "(awaiting …)" qualifier when the call is parked
|
|
248
266
|
// on a human decision (its amber dot already signals the state; the word names it).
|
|
249
267
|
function StepBody({ label, awaiting }: { label: string; awaiting?: string }) {
|
|
250
268
|
return (
|
|
@@ -252,7 +270,7 @@ function StepBody({ label, awaiting }: { label: string; awaiting?: string }) {
|
|
|
252
270
|
{label}
|
|
253
271
|
{awaiting ? (
|
|
254
272
|
<Text size="sm" color="muted">
|
|
255
|
-
{"
|
|
273
|
+
{" (" + awaiting + ")"}
|
|
256
274
|
</Text>
|
|
257
275
|
) : null}
|
|
258
276
|
</Text>
|
|
@@ -435,7 +453,7 @@ function ToolGroup(props: {
|
|
|
435
453
|
);
|
|
436
454
|
}
|
|
437
455
|
|
|
438
|
-
// SETTLED, expandable — a persistent "{final action}
|
|
456
|
+
// SETTLED, expandable — a persistent "{final action} ({n} steps)" HEADER that
|
|
439
457
|
// STAYS PUT and rolls the steps out BELOW it (in order) on press, so the row you
|
|
440
458
|
// pressed never moves. The header wears the `complete` terminal dot — an outlined
|
|
441
459
|
// ring + check, a differentiator from the filled `done` step dots; every row
|
|
@@ -486,7 +504,4 @@ const styles = StyleSheet.create({
|
|
|
486
504
|
reasoning: { paddingLeft: 28, paddingBottom: 4 },
|
|
487
505
|
// A step's expanded I/O panels — same left edge as the reasoning body.
|
|
488
506
|
rowDetail: { paddingLeft: 28, paddingBottom: 6, paddingTop: 2 },
|
|
489
|
-
// The retry action under the terminal error — aligned to the message body
|
|
490
|
-
// (past the dot column + its gap), so it reads as a response to that row.
|
|
491
|
-
retry: { paddingLeft: 28 },
|
|
492
507
|
});
|
package/src/breakdown.tsx
CHANGED
|
@@ -46,7 +46,7 @@ const DEFAULT_LABELS: BreakdownLabels = { more: (hidden) => `Show ${hidden} more
|
|
|
46
46
|
/**
|
|
47
47
|
* Composition browser for LARGE populations — the answer to "what is my
|
|
48
48
|
* stock made of, and let me drill into a slice". A stacked bar shows the
|
|
49
|
-
* shares; below it, one pressable row per segment (swatch
|
|
49
|
+
* shares; below it, one pressable row per segment (swatch, label, count,
|
|
50
50
|
* share). Selecting a segment dims the rest and (in the host) filters the
|
|
51
51
|
* register underneath — the facet pattern for 10,000-record monitoring.
|
|
52
52
|
* Several Breakdowns side by side (by type, by status, by site) compose a
|
|
@@ -27,7 +27,7 @@ export interface CalendarViewProps<T = unknown> {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Full calendar: a toolbar (prev / today / next + month
|
|
30
|
+
* Full calendar: a toolbar (prev / today / next + month/week/day switch) over
|
|
31
31
|
* the month grid or the week/day time grid. Date + view are owned here so the
|
|
32
32
|
* primitive is drop-in; the consumer only supplies events + callbacks.
|
|
33
33
|
*/
|
package/src/change_review.tsx
CHANGED
|
@@ -43,7 +43,7 @@ export type ChangeStatus = "pending" | "accepted" | "rejected";
|
|
|
43
43
|
// context only mirrors it so the header counter, Keep-all, and the
|
|
44
44
|
// apply-disabled logic derive without the host re-wiring them. An add, an
|
|
45
45
|
// update, a removal and a source conflict are compositions of ONE grammar:
|
|
46
|
-
// `ChangeFields` + `ChangeField` (the − band
|
|
46
|
+
// `ChangeFields` + `ChangeField` (the − band, the + value, candidate rows)
|
|
47
47
|
// — not separate components; `ChangeRecord` wraps the same grammar in the
|
|
48
48
|
// per-item card for record SETS.
|
|
49
49
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -241,7 +241,7 @@ export function ChangeFields({ children }: { children: ReactNode }) {
|
|
|
241
241
|
export interface ChangeRecordProps {
|
|
242
242
|
/** Stable identity — registers in the review context like a `Change`. */
|
|
243
243
|
id: string;
|
|
244
|
-
/** "add" (+, green)
|
|
244
|
+
/** "add" (+, green), "remove" (−, red), "edit" (±, neutral — only the
|
|
245
245
|
* changed fields, each with its own mini-diff via `before`). */
|
|
246
246
|
tone: "add" | "remove" | "edit";
|
|
247
247
|
/** The card header title (the item's name). */
|
|
@@ -290,16 +290,17 @@ export function ChangeRecord(props: ChangeRecordProps) {
|
|
|
290
290
|
: { glyph: "±", word: loc.opEdit, color: colors.zinc[600], bg: colors.white, header: colors.zinc[50], border: colors.border };
|
|
291
291
|
return (
|
|
292
292
|
<View style={[styles.recordCard, { backgroundColor: mark.bg, borderColor: mark.border }]}>
|
|
293
|
-
{/* The op reads on the WHOLE card — a quiet tone wash (50 body
|
|
294
|
-
100 header
|
|
293
|
+
{/* The op reads on the WHOLE card — a quiet tone wash (50 body,
|
|
294
|
+
100 header, 200 border); an edit stays white, its diffs carry it. */}
|
|
295
295
|
<View style={[styles.recordCardHeader, { backgroundColor: mark.header }]}>
|
|
296
296
|
<Text size="sm" weight="semibold" tabular style={{ color: mark.color, width: 14 }}>
|
|
297
297
|
{mark.glyph}
|
|
298
298
|
</Text>
|
|
299
|
+
{/* The op word RUNS IN to the title it names, so it takes the colon and
|
|
300
|
+
the title follows on the row's own gap. */}
|
|
299
301
|
<Text size="sm" weight="semibold" style={{ color: mark.color }}>
|
|
300
|
-
{mark.word}
|
|
302
|
+
{mark.word}:
|
|
301
303
|
</Text>
|
|
302
|
-
<Text size="sm" color="muted">·</Text>
|
|
303
304
|
<Text size="sm" weight="semibold" numberOfLines={1} style={{ flexShrink: 1 }}>
|
|
304
305
|
{props.title}
|
|
305
306
|
</Text>
|
|
@@ -380,9 +381,9 @@ export interface ChangeFieldProps {
|
|
|
380
381
|
* (when replacing/removing), the + value (editable, read-only on a conflict,
|
|
381
382
|
* or absent on a pure removal), conflict candidates + the type-another-value
|
|
382
383
|
* third option, the quiet reasoning — deciding for ITSELF via Keep/Drop.
|
|
383
|
-
* A plain field (no diff, no decision) is the inline label
|
|
384
|
+
* A plain field (no diff, no decision) is the inline label, value row,
|
|
384
385
|
* press-to-edit when `onChangeText` is given. Decided → ONE quiet collapsed
|
|
385
|
-
* row: mark
|
|
386
|
+
* row: mark, label → value, Undo.
|
|
386
387
|
*/
|
|
387
388
|
export function ChangeField(props: ChangeFieldProps) {
|
|
388
389
|
const loc = useLoticsLocale().changeReview;
|