@lotics/ui 5.6.1 → 5.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/AGENTS.md +44 -27
  2. package/examples/tpl_approvals.tsx +10 -10
  3. package/examples/tpl_crm_desk.tsx +3 -3
  4. package/examples/tpl_dieline.tsx +7 -6
  5. package/examples/tpl_directory.tsx +3 -3
  6. package/examples/tpl_dossier.tsx +7 -7
  7. package/examples/tpl_draft.tsx +4 -4
  8. package/examples/tpl_extract.tsx +7 -7
  9. package/examples/tpl_inventory.tsx +3 -3
  10. package/examples/tpl_lookup.tsx +4 -3
  11. package/examples/tpl_match.tsx +1 -1
  12. package/examples/tpl_ratedesk.tsx +11 -11
  13. package/examples/tpl_report.tsx +3 -3
  14. package/examples/tpl_stock.tsx +3 -3
  15. package/examples/tpl_triage.tsx +1 -1
  16. package/package.json +12 -3
  17. package/src/agent_progress.tsx +14 -12
  18. package/src/agent_run.tsx +248 -86
  19. package/src/back_button.tsx +3 -2
  20. package/src/badge.tsx +2 -1
  21. package/src/button.tsx +11 -32
  22. package/src/button_colors.ts +23 -0
  23. package/src/calendar/calendar_view.tsx +2 -2
  24. package/src/card_select_item.tsx +2 -1
  25. package/src/change_review.tsx +5 -6
  26. package/src/{pill_button.tsx → chip.tsx} +7 -7
  27. package/src/chip_group.tsx +2 -2
  28. package/src/column_filter.tsx +4 -4
  29. package/src/combobox.tsx +2 -9
  30. package/src/composer.tsx +4 -3
  31. package/src/control_surface.ts +15 -15
  32. package/src/date_field.tsx +2 -1
  33. package/src/date_filter.tsx +2 -1
  34. package/src/date_range_filter_field.tsx +2 -1
  35. package/src/dialog.tsx +2 -2
  36. package/src/discrepancy.tsx +1 -1
  37. package/src/drawer.tsx +1 -2
  38. package/src/{filter_pill.tsx → filter_chip.tsx} +8 -8
  39. package/src/finding.tsx +1 -1
  40. package/src/gantt/gantt_view.tsx +2 -2
  41. package/src/icon_button.tsx +38 -14
  42. package/src/inline_edit.tsx +2 -2
  43. package/src/link_button.tsx +2 -1
  44. package/src/markdown.css +209 -0
  45. package/src/markdown.tsx +17 -0
  46. package/src/markdown.web.tsx +69 -0
  47. package/src/menu_button.tsx +2 -1
  48. package/src/menu_list_item.tsx +2 -1
  49. package/src/number_input.tsx +2 -1
  50. package/src/picker.tsx +3 -3
  51. package/src/popover_header.tsx +2 -2
  52. package/src/primitives_purity.test.ts +13 -7
  53. package/src/radio_picker.tsx +2 -1
  54. package/src/range_slider.tsx +1 -1
  55. package/src/review_card.tsx +2 -2
  56. package/src/scan_field.tsx +2 -1
  57. package/src/scored_option.tsx +0 -1
  58. package/src/search_input.tsx +3 -2
  59. package/src/segmented_control.tsx +2 -1
  60. package/src/status_badge.tsx +2 -1
  61. package/src/stepper.tsx +3 -2
  62. package/src/switch_button.tsx +2 -1
  63. package/src/switcher.tsx +3 -2
  64. package/src/tabs.tsx +2 -1
  65. package/src/tag_input.tsx +2 -1
  66. package/src/text_input_field.tsx +2 -1
  67. package/src/time_field.tsx +2 -1
  68. package/src/time_picker.tsx +2 -1
  69. package/src/triage_row.tsx +3 -3
package/AGENTS.md CHANGED
@@ -22,9 +22,11 @@ typeahead, async search, virtualization, and a11y the primitive already ships.
22
22
 
23
23
  Pick by capability, not by name. (→ the source file for the API.)
24
24
 
25
- - **Actions** — `Button` (labelled; `color` = emphasis/risk, `shape` pill|rounded),
26
- `IconButton` (icon-only, needs `accessibilityLabel`/`tooltip`), `LinkButton` (quiet ghost
27
- action Clear / Select all), `PillButton` (dismissible facet chip). A button is never a raw
25
+ - **Actions** — `Button` (labelled; `title` is MANDATORY; `color` = emphasis/risk),
26
+ `IconButton` (icon-ONLY — the circular affordance; an icon-only Button is a type error, so reach
27
+ here. `size` `lg` 40px / `md` 28px / `sm` 24px, the SAME `color` palette as Button, plus `loading`;
28
+ needs `accessibilityLabel`/`tooltip`), `LinkButton` (quiet ghost
29
+ action — Clear / Select all), `Chip` (dismissible facet chip). A button is never a raw
28
30
  `Pressable`. For a link OUT (a URL / record / document) use `Link` — underline+blue, the
29
31
  destination signal; the opposite of `LinkButton` (a quiet in-app action, never underlined).
30
32
  - **Pick from a list** — `Picker` (known options, native typeahead, single/multi/custom-render,
@@ -66,8 +68,8 @@ Pick by capability, not by name. (→ the source file for the API.)
66
68
  - **Rows & registers** — `PressableRow` (THE register row), `ListItem`, `MenuButton`,
67
69
  `MenuListItem`, `DetailRow` (label+value for drawers/peeks), `ActionMenu` (⋯), `FloatingActionBar`
68
70
  (bulk-select bar).
69
- - **Filters & view controls** — `SearchInput`, `ChipGroup`, `FilterPill` (+ `RangeSlider`,
70
- `Counter`), `PillButton`.
71
+ - **Filters & view controls** — `SearchInput`, `ChipGroup`, `FilterChip` (+ `RangeSlider`,
72
+ `Counter`), `Chip`.
71
73
  - **Overlays** — `Dialog`, `Drawer` (+ `DrawerFooter`), `Popover`, `Tooltip`, `CommandMenu`,
72
74
  `Alert` (the blocking confirm).
73
75
  - **Status / feedback** — `Badge` / `StatusBadge`, `Callout` (inline status), `EmptyState`,
@@ -270,15 +272,20 @@ Compose the surfaces as a loop, and reach for the right one by job:
270
272
  attaching HOLDS the file for review; the human presses Send. The canvas (`tpl_dieline`) uses it this
271
273
  way; while running the host swaps `Composer` for `AgentProgress` (same pill geometry, so it reads as
272
274
  a morph).
273
- - **Show the work** — `AgentRun`: a live feed of the agent's work its streamed narration (with a
274
- blinking caret) + tool calls as a vertical `Stepper` of neutral dots (the latest pulsing while live,
275
- a white-check dot once done). No title, no avatar just the work. A terminal "Done" step (the
276
- `complete` node) appears ONLY once the run finishes. A step can carry `peek` content to glance into
277
- via a popover. Tool labels resolve from the raw tool name via a built-in default map + an optional
278
- `labelForTool` override (localize THERE the kit stays English). `doneLabel` localizes the terminal.
279
- Transparent work, NEVER a bare spinner — the
280
- intelligence is legible. On a canvas/composer app reach for `AgentProgress` `AgentRun`
281
- collapsed into a floating pill (avatar + current step) that EXPANDS on press; the composer
275
+ - **Show the work** — `AgentRun`: a live feed of the agent's work as a TIMELINE. The prop is an
276
+ ORDERED `items` array `{type:"text"}` prose (rendered as `Markdown`, a trailing while live) interleaved with `{type:"step"}` tool/step calls, in
277
+ the order they happened (a real run is text a burst of calls more text more calls, NOT all
278
+ text on top of a flat step list). Consecutive `step` items fold into ONE activity group: while the
279
+ agent is mid-tools the tail group is a SINGLE pulsing row whose label swaps in place as each call
280
+ fires (no growing stack of dots); once prose resumes the group settles into a persistent "{final
281
+ action} · {n} steps" HEADER (a `complete` terminal dot distinct from the filled `done` step dots)
282
+ that STAYS PUT and rolls the calls out BELOW it on press (a `step` can carry `peek` content;
283
+ `detail` is OPTIONAL real tool I/O, never invented prose). The run ALWAYS ends on the agent's
284
+ text (no global terminal node). A `step` with
285
+ `kind:"tool"` carries the RAW tool name, resolved via a built-in map + an optional `labelForTool`
286
+ override (localize THERE — the kit stays English); `stepsLabel` localizes the "{n} steps" suffix.
287
+ Transparent work, NEVER a bare spinner. On a canvas/composer app reach for `AgentProgress` —
288
+ `AgentRun` collapsed into a floating pill (avatar + current step) that EXPANDS on press; the composer
282
289
  morphs into it while running, and reveals again when done.
283
290
  - **Review before apply — ONE card + the batch engine, the BODY is compositional.** Every "the agent
284
291
  proposes → the human accepts / edits / dismisses → nothing auto-applies" surface wears the same
@@ -378,8 +385,8 @@ controls — while **status/severity/diffs use functional colour** the way the r
378
385
  - **Stepper / AgentRun nodes** — progress dots on a spine: `current` a **pulsing accent ring** (white
379
386
  centre), `done` a filled accent dot + **white check**, `upcoming` a faint **grey** ring, the terminal
380
387
  `complete` a **blackish ring + black check**, `warning` **amber**. Default accent is neutral ink
381
- (`color` themes it); `AgentRun` keeps the neutral default + appends the `complete` "Done" node only
382
- when the run finishes.
388
+ (`color` themes it); `AgentRun` keeps the neutral default an active group's tail pulses (`current`),
389
+ a settled group is a `done`/`warning` row, and there is no global terminal node (the run ends on text).
383
390
  - **ChangeReview diffs** — the old value struck in **red**, the new value in **green**.
384
391
  - **Discrepancy** — the agent's pick gets a **blue** highlight; `Mismatch` reads amber, `Resolved`
385
392
  emerald.
@@ -478,25 +485,33 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
478
485
  step. Facts are `DetailRow`s; the commit bar is a `DrawerFooter`. The open row shows a selected
479
486
  highlight (and `marked` — a resting blue tint — for a bulk-ticked row). `Peek` is ONLY for
480
487
  secondary references, never the primary row press.
488
+ - **One control radius** — every interactive control (`Button`, the inputs/selects/pickers,
489
+ `SearchInput`, `MenuButton`, `Tabs`, the `SegmentedControl` track, `ChipGroup`/`FilterChip`/
490
+ `Chip`) wears `CONTROL_RADIUS` (10, from `control_surface.ts`) — never a literal. Containers
491
+ are deliberately larger (`Card` 16, the review card 12). An element NESTED inside a padded control
492
+ (a segmented thumb, a combobox chip, an ✕ on a chip) takes `CONTROL_RADIUS − padding` so its corner
493
+ stays CONCENTRIC with the parent — set it equal and the inner corner bulges past the outer curve.
494
+ Round-by-function controls (avatars, status dots, switches, sliders, progress bars, `IconButton`)
495
+ stay full (999); a small square toggle (`Checkbox`, 24px) keeps a proportional small radius (6 —
496
+ the same fraction of its box that 10 is of a 40px control), not the band radius. No capsules —
497
+ `Button` has no pill `shape`.
481
498
  - **One view-control vocabulary** (all 40px, `sm` labels, in one wrapping band):
482
- · `SearchInput` is THE search box — a white `TextInputField` preset (rounded-8 like every other
483
- input, a thin zinc-300 border, the leading search glyph + a clear ✕). It reads as search by its
499
+ · `SearchInput` is THE search box — a white `TextInputField` preset (the shared `CONTROL_RADIUS`
500
+ like every other input, a thin zinc-300 border, the leading search glyph + a clear ✕). It reads as search by its
484
501
  ICON + white fill, not a distinct shape, so a toolbar of controls stays one consistent band.
485
502
  Never a bare `TextInputField` + search icon.
486
503
  · `ChipGroup` is THE one-of-N lens (≤ ~10 options the user flips between; include "All"; counts in
487
504
  the label). Filters ONE list to a SUBSET — including by process stage / lifecycle state.
488
- · `FilterPill` is THE secondary-dimension filter — a compact pill that opens a composed popover
505
+ · `FilterChip` is THE secondary-dimension filter — a compact chip that opens a composed popover
489
506
  editor (`PickerMenu multi` / `RangeSlider` / `Counter` / date range). Single-select closes on
490
507
  pick via the `{({close}) => …}` render prop.
491
508
  · `ColumnFilter` is the TYPED column filter — give it a `FilterableColumn` (`text` / `number` /
492
509
  `select`) + a controlled `ColumnFilterValue` and it renders the right editor (contains / range /
493
- multi-select) inside a `FilterPill`, with `columnFilterToConditions` mapping the value to query
494
- predicates. Reach for it — ONE compact pill per dimension — when a register filters on SEVERAL
510
+ multi-select) inside a `FilterChip`, with `columnFilterToConditions` mapping the value to query
511
+ predicates. Reach for it — ONE compact chip per dimension — when a register filters on SEVERAL
495
512
  columns (origin, destination, carrier, type…). Never hand-roll a row of bespoke chips/filters,
496
513
  and never use a permanently-expanded `ChipGroup` for a secondary dimension.
497
- · `Combobox pill` is the SEARCHABLE filter in the band `<Combobox pill>` wears the same white
498
- toolbar-pill surface as `SearchInput`/`FilterPill` (don't put a bare form-look Combobox in a
499
- toolbar — its transparent input reads grey on the zinc-50 canvas beside the white pills). Reach
514
+ · `Combobox` is the SEARCHABLE pickerreach
500
515
  for it over `ColumnFilter` when the value is ONE of a large/growing set you type to narrow (a
501
516
  port, a customer) rather than a short fixed list. Pass `recentOptions` (the same option list) so
502
517
  a click opens the full list, then typing filters. The **From → To route picker** is two of them
@@ -581,12 +596,14 @@ recipe for a screen JOB; copy and adapt). Pick by the job:
581
596
 
582
597
  ## Full component inventory (import as `@lotics/ui/<module>`)
583
598
 
584
- text · card (Card · CardHeader · CardHeaderTitle · CardHeaderMeta · CardBody · CardFooter) ·
599
+ text ·
600
+ markdown (Markdown — the single canonical markdown renderer for chat, apps, and `AgentRun`; rich on web via react-markdown/remark-gfm with copyable tables, plain-text on native; takes a markdown `children` string; import `@lotics/ui/markdown.css` once for styling) ·
601
+ card (Card · CardHeader · CardHeaderTitle · CardHeaderMeta · CardBody · CardFooter) ·
585
602
  section_heading (SectionHeading · SectionHeadingTitle — compound, owns no margin) · badge ·
586
603
  option_badge (OptionBadge — a select value as its configured colored badge) ·
587
604
  member_chip (MemberChip — avatar + name; the universal person render) ·
588
605
  member_select (MemberSelect — a Picker of MemberChip options; the member picker) ·
589
- status_badge · button · icon_button · link · link_button · pill_button · tabs · segmented_control ·
606
+ status_badge · button · icon_button · link · link_button · chip · tabs · segmented_control ·
590
607
  picker · combobox · tag_input (TagInput — chip box + Add-popover; for tags, not Combobox multi) ·
591
608
  text_input_field · number_input · search_input · form_field · checkbox · checkbox_input · switch ·
592
609
  radio_picker · counter · range_slider · date_picker · date_range_filter_field · time_picker ·
@@ -594,7 +611,7 @@ inline_text_input · inline_number_input · inline_select · inline_member_selec
594
611
  inline_time_picker (the Inline* family — per-field editors on `inline_edit`'s `useInlineEdit` +
595
612
  `InlineEditView`; `InlineSelect`/`InlineMemberSelect` take `renderValue` for a chip/badge at rest) ·
596
613
  list · list_item · menu_button · menu_list_item · detail_row · pressable_row · action_menu ·
597
- floating_action_bar · filter_pill · column_filter (ColumnFilter — the typed per-column filter pill +
614
+ floating_action_bar · filter_chip · column_filter (ColumnFilter — the typed per-column filter pill +
598
615
  columnFilterToConditions; for a register filtering on several columns) · chip_group · search_input ·
599
616
  sort_header · table · pagination · accordion · stepper (Stepper + Step — done/current/upcoming/warning/complete progress on a track (horizontal) or spine (vertical); compound `<Step status>children` OR data `steps[]`+`current`; the guided-run / agent-feed primitive — subsumes the old StepList) ·
600
617
  step_progress · timeline (heterogeneous event LOG — per-row icon + expandable details, models the past; NOT progress) · drawer (+ DrawerFooter) · dialog · popover · tooltip ·
@@ -13,7 +13,7 @@ import { Divider } from "@lotics/ui/divider";
13
13
  import { Drawer, DrawerFooter } from "@lotics/ui/drawer";
14
14
  import { EmptyState } from "@lotics/ui/empty_state";
15
15
  import { Counter } from "@lotics/ui/counter";
16
- import { FilterPill, selectSummary } from "@lotics/ui/filter_pill";
16
+ import { FilterChip, selectSummary } from "@lotics/ui/filter_chip";
17
17
  import { FloatingActionBar } from "@lotics/ui/floating_action_bar";
18
18
  import { FormField } from "@lotics/ui/form_field";
19
19
  import { KPIStrip } from "@lotics/ui/kpi_strip";
@@ -240,7 +240,7 @@ export function TplApprovals() {
240
240
  />
241
241
 
242
242
  {/* layered filter band — the hot dimension (type) inline as chips,
243
- secondary dimensions collapsed into FilterPills so the band stays
243
+ secondary dimensions collapsed into FilterChips so the band stays
244
244
  scannable as more facets are added */}
245
245
  <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8, alignItems: "center" }}>
246
246
  <ChipGroup
@@ -254,13 +254,13 @@ export function TplApprovals() {
254
254
  value={tab}
255
255
  onValueChange={(t) => { setTab(t); setOpenId(null); }}
256
256
  />
257
- <FilterPill label="Requester" summary={reqSummary} onClear={() => setReqSel([])} clearLabel="Clear requester filter">
257
+ <FilterChip label="Requester" summary={reqSummary} onClear={() => setReqSel([])} clearLabel="Clear requester filter">
258
258
  <PickerMenu multi enableSelectAll options={REQUESTERS} value={reqSel} onValueChange={setReqSel} />
259
- </FilterPill>
260
- <FilterPill label="Role" summary={roleSummary} onClear={() => setRoleSel([])} clearLabel="Clear role filter">
259
+ </FilterChip>
260
+ <FilterChip label="Role" summary={roleSummary} onClear={() => setRoleSel([])} clearLabel="Clear role filter">
261
261
  <PickerMenu multi options={ROLES} value={roleSel} onValueChange={setRoleSel} />
262
- </FilterPill>
263
- <FilterPill label="Amount" summary={amtSummary} onClear={() => setAmt([AMT_MIN, AMT_MAX])} clearLabel="Clear amount filter">
262
+ </FilterChip>
263
+ <FilterChip label="Amount" summary={amtSummary} onClear={() => setAmt([AMT_MIN, AMT_MAX])} clearLabel="Clear amount filter">
264
264
  <RangeSlider
265
265
  min={AMT_MIN}
266
266
  max={AMT_MAX}
@@ -270,8 +270,8 @@ export function TplApprovals() {
270
270
  format={fmtAmt}
271
271
  accessibilityLabel="Amount"
272
272
  />
273
- </FilterPill>
274
- <FilterPill label="Waiting" summary={daysSummary} onClear={() => setMinDays(0)} clearLabel="Clear waiting filter">
273
+ </FilterChip>
274
+ <FilterChip label="Waiting" summary={daysSummary} onClear={() => setMinDays(0)} clearLabel="Clear waiting filter">
275
275
  <View style={{ gap: 10 }}>
276
276
  <Text size="sm" color="muted">Waiting at least</Text>
277
277
  <Counter
@@ -283,7 +283,7 @@ export function TplApprovals() {
283
283
  format={(n) => `${n} ${n === 1 ? "day" : "days"}`}
284
284
  />
285
285
  </View>
286
- </FilterPill>
286
+ </FilterChip>
287
287
  </View>
288
288
 
289
289
  <Card style={{ padding: 0 }}>
@@ -20,7 +20,7 @@ import { NumberInput } from "@lotics/ui/number_input";
20
20
  import { Peek } from "@lotics/ui/peek";
21
21
  import { ChipGroup } from "@lotics/ui/chip_group";
22
22
  import { DatePicker } from "@lotics/ui/date_picker";
23
- import { FilterPill } from "@lotics/ui/filter_pill";
23
+ import { FilterChip } from "@lotics/ui/filter_chip";
24
24
  import { Popover, PopoverContent, PopoverTrigger } from "@lotics/ui/popover";
25
25
  import { PressableRow } from "@lotics/ui/pressable_row";
26
26
  import { SearchInput } from "@lotics/ui/search_input";
@@ -158,7 +158,7 @@ function ChiTieuGate({ chiTieu, onSave }: { chiTieu: number; onSave: (n: number)
158
158
  onSave(draft); setApDung("In effect from today · set by you"); setOpen(false);
159
159
  };
160
160
  return (
161
- <FilterPill
161
+ <FilterChip
162
162
  label="Target"
163
163
  summary={String(chiTieu)}
164
164
  align="end"
@@ -181,7 +181,7 @@ function ChiTieuGate({ chiTieu, onSave }: { chiTieu: number; onSave: (n: number)
181
181
  </FormField>
182
182
  <Text size="xs" color="muted">{apDung}</Text>
183
183
  </View>
184
- </FilterPill>
184
+ </FilterChip>
185
185
  );
186
186
  }
187
187
 
@@ -10,7 +10,7 @@ import { InlineTextInput } from "@lotics/ui/inline_text_input";
10
10
  import { AgentProgress } from "@lotics/ui/agent_progress";
11
11
  import { Composer } from "@lotics/ui/composer";
12
12
  import { FileThumbnail, type DisplayFile } from "@lotics/ui/file_thumbnail";
13
- import { type AgentRunStep } from "@lotics/ui/agent_run";
13
+ import { type AgentRunStep, type AgentRunItem } from "@lotics/ui/agent_run";
14
14
 
15
15
  // A real app fills the attachment from `useFileUpload` (the uploaded file's URL);
16
16
  // this mock uses an inline placeholder so the example needs no network image.
@@ -105,8 +105,9 @@ export function TplDieline() {
105
105
  return () => clearTimeout(t);
106
106
  }, [run, revealed]);
107
107
 
108
- const liveSteps: AgentRunStep[] = run
108
+ const liveItems: AgentRunItem[] = run
109
109
  ? run.script.slice(0, revealed).map((s, i) => ({
110
+ type: "step",
110
111
  ...s,
111
112
  status: revealed < run.script.length && i === revealed - 1 ? "running" : "done",
112
113
  }))
@@ -167,7 +168,7 @@ export function TplDieline() {
167
168
  {/* slim top bar */}
168
169
  <View style={{ flexDirection: "row", alignItems: "center", gap: 12, paddingHorizontal: 20, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: colors.zinc[200], backgroundColor: colors.background }}>
169
170
  <Text size="sm" weight="semibold" style={{ flex: 1 }}>Carton design</Text>
170
- {phase !== "empty" ? <Button title="New" color="secondary" shape="rounded" icon="plus" onPress={newSession} /> : null}
171
+ {phase !== "empty" ? <Button title="New" color="secondary" icon="plus" onPress={newSession} /> : null}
171
172
  </View>
172
173
 
173
174
  {/* canvas region — a pannable + zoomable surface (DRAG to pan, the zoom pill
@@ -208,7 +209,7 @@ export function TplDieline() {
208
209
  <View style={{ position: "absolute", left: 0, right: 0, bottom: 24, alignItems: "center", paddingHorizontal: 16, pointerEvents: "none" }}>
209
210
  <View style={{ width: "100%", maxWidth: 620, pointerEvents: "auto" }}>
210
211
  {phase === "working" ? (
211
- <AgentProgress steps={liveSteps} state="streaming" />
212
+ <AgentProgress items={liveItems} state="streaming" />
212
213
  ) : (
213
214
  <Composer
214
215
  value={prompt}
@@ -217,7 +218,7 @@ export function TplDieline() {
217
218
  sendDisabled={prompt.trim().length === 0 && !attachment}
218
219
  placeholder={designReady ? "Describe a change — “make it 5 mm taller”" : "Attach a photo, add a note, then send…"}
219
220
  actionsButton={
220
- <Button icon="paperclip" color="secondary" accessibilityLabel="Attach a photo" onPress={() => setAttachment(MOCK_PHOTO)} />
221
+ <IconButton icon="paperclip" size="lg" color="secondary" accessibilityLabel="Attach a photo" onPress={() => setAttachment(MOCK_PHOTO)} />
221
222
  }
222
223
  files={attachment ? <FileThumbnail file={attachment} size={56} onRemove={() => setAttachment(null)} /> : null}
223
224
  />
@@ -272,7 +273,7 @@ function ParamsPanel({ dims, onEditParam, onDownload }: { dims: Dims; onEditPara
272
273
  ))}
273
274
  </View>
274
275
 
275
- <Button title="Download" color="primary" shape="rounded" icon="file-down" alignSelf="stretch" onPress={onDownload} />
276
+ <Button title="Download" color="primary" icon="file-down" alignSelf="stretch" onPress={onDownload} />
276
277
  </>
277
278
  ) : null}
278
279
  </View>
@@ -7,7 +7,7 @@ import { Avatar } from "@lotics/ui/avatar";
7
7
  import { Badge } from "@lotics/ui/badge";
8
8
  import { Button } from "@lotics/ui/button";
9
9
  import { ChipGroup } from "@lotics/ui/chip_group";
10
- import { FilterPill } from "@lotics/ui/filter_pill";
10
+ import { FilterChip } from "@lotics/ui/filter_chip";
11
11
  import { Counter } from "@lotics/ui/counter";
12
12
  import { Card } from "@lotics/ui/card";
13
13
  import { Divider } from "@lotics/ui/divider";
@@ -204,7 +204,7 @@ export function TplDirectory() {
204
204
  value={tab}
205
205
  onValueChange={setTab}
206
206
  />
207
- <FilterPill
207
+ <FilterChip
208
208
  label="Min orders"
209
209
  summary={minOrders > 0 ? `≥ ${minOrders}` : undefined}
210
210
  onClear={() => setMinOrders(0)}
@@ -222,7 +222,7 @@ export function TplDirectory() {
222
222
  format={(n) => `${n} orders`}
223
223
  />
224
224
  </View>
225
- </FilterPill>
225
+ </FilterChip>
226
226
  </View>
227
227
 
228
228
  {/* the register: one card, hairline-separated rows */}
@@ -20,7 +20,7 @@ import { Popover, PopoverContent, PopoverFooter, PopoverTrigger } from "@lotics/
20
20
  import { Table, TableRow, TableCell, type TableColumn } from "@lotics/ui/table";
21
21
  import { SearchInput } from "@lotics/ui/search_input";
22
22
  import { ChipGroup } from "@lotics/ui/chip_group";
23
- import { FilterPill, selectSummary } from "@lotics/ui/filter_pill";
23
+ import { FilterChip, selectSummary } from "@lotics/ui/filter_chip";
24
24
  import { PickerMenu } from "@lotics/ui/picker_menu";
25
25
  import { cycleSort, sortBy, type SortState } from "@lotics/ui/sort_header";
26
26
  import { Combobox } from "@lotics/ui/combobox";
@@ -101,7 +101,7 @@ const TABS: { value: Tab; label: string }[] = [
101
101
 
102
102
  const PAGE_SIZE = 8;
103
103
 
104
- // Distinct case handlers — the secondary filter dimension (a multi-select FilterPill).
104
+ // Distinct case handlers — the secondary filter dimension (a multi-select FilterChip).
105
105
  const ASSIGNEES = [...new Set(HO_SO.map((r) => r.phuTrach))].map((n) => ({ label: n, value: n }));
106
106
 
107
107
  // Columns defined ONCE — Table renders the header from these and each
@@ -331,7 +331,7 @@ export function TplDossier() {
331
331
  />
332
332
 
333
333
  {/* status chips (the hot dimension, inline) + a secondary Assignee
334
- multi-select FilterPill — the same faceted band as Approvals */}
334
+ multi-select FilterChip — the same faceted band as Approvals */}
335
335
  <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8, alignItems: "center" }}>
336
336
  <View style={{ flexGrow: 1, flexBasis: 240, minWidth: 200, maxWidth: 360 }}>
337
337
  <SearchInput
@@ -350,7 +350,7 @@ export function TplDossier() {
350
350
  value={tab}
351
351
  onValueChange={chonTab}
352
352
  />
353
- <FilterPill
353
+ <FilterChip
354
354
  label="Assignee"
355
355
  summary={assigneeSummary}
356
356
  onClear={() => { setAssignee([]); setPage(0); }}
@@ -363,10 +363,10 @@ export function TplDossier() {
363
363
  value={assignee}
364
364
  onValueChange={(v) => { setAssignee(v); setPage(0); }}
365
365
  />
366
- </FilterPill>
366
+ </FilterChip>
367
367
  {/* Single-select sibling — the render-prop `close` shuts the popover on
368
368
  pick (the multi above stays open to collect several values). */}
369
- <FilterPill
369
+ <FilterChip
370
370
  label="Fee"
371
371
  summary={feeStatus === "paid" ? "Paid" : feeStatus === "unpaid" ? "Unpaid" : undefined}
372
372
  onClear={() => { setFeeStatus(null); setPage(0); }}
@@ -380,7 +380,7 @@ export function TplDossier() {
380
380
  onRequestClose={close}
381
381
  />
382
382
  )}
383
- </FilterPill>
383
+ </FilterChip>
384
384
  </View>
385
385
 
386
386
  {/* the register — eyebrow band (Table header) → list rows → totals + paging */}
@@ -95,7 +95,7 @@ export function TplDraft() {
95
95
  {phase === "sent" ? (
96
96
  <Card>
97
97
  <CompletionState title="Reply sent" summary="Your edited draft was sent to Maya Tran.">
98
- <Button title="Draft another" color="secondary" shape="rounded" onPress={() => { setPhase("idle"); setDraft(""); }} />
98
+ <Button title="Draft another" color="secondary" onPress={() => { setPhase("idle"); setDraft(""); }} />
99
99
  </CompletionState>
100
100
  </Card>
101
101
  ) : (
@@ -129,7 +129,7 @@ export function TplDraft() {
129
129
 
130
130
  {phase === "idle" ? (
131
131
  <View style={{ alignItems: "flex-start", paddingTop: 4 }}>
132
- <Button title="Draft a reply" color="primary" shape="rounded" onPress={startDraft} />
132
+ <Button title="Draft a reply" color="primary" onPress={startDraft} />
133
133
  </View>
134
134
  ) : null}
135
135
 
@@ -151,8 +151,8 @@ export function TplDraft() {
151
151
  {phase === "ready" ? (
152
152
  <CardFooter>
153
153
  <View style={{ flex: 1 }} />
154
- <Button title="Regenerate" color="secondary" shape="rounded" icon="rotate-ccw" onPress={startDraft} />
155
- <Button title="Send reply" color="primary" shape="rounded" onPress={() => setPhase("sent")} />
154
+ <Button title="Regenerate" color="secondary" icon="rotate-ccw" onPress={startDraft} />
155
+ <Button title="Send reply" color="primary" onPress={() => setPhase("sent")} />
156
156
  </CardFooter>
157
157
  ) : null}
158
158
  </Card>
@@ -5,7 +5,7 @@ import { Text } from "@lotics/ui/text";
5
5
  import { Button } from "@lotics/ui/button";
6
6
  import { Card, CardBody, CardHeader, CardHeaderTitle } from "@lotics/ui/card";
7
7
  import { FileDropzone } from "@lotics/ui/file_dropzone";
8
- import { AgentRun, type AgentRunStep } from "@lotics/ui/agent_run";
8
+ import { AgentRun, type AgentRunStep, type AgentRunItem } from "@lotics/ui/agent_run";
9
9
  import { ReviewCard } from "@lotics/ui/review_card";
10
10
  import { RecordFields, type RecordField } from "@lotics/ui/record_fields";
11
11
  import { Sources } from "@lotics/ui/sources";
@@ -81,7 +81,8 @@ export function TplExtract() {
81
81
  return () => clearTimeout(t);
82
82
  }, [streaming, revealed]);
83
83
 
84
- const steps: AgentRunStep[] = EXTRACT.slice(0, revealed).map((s, i) => ({
84
+ const items: AgentRunItem[] = EXTRACT.slice(0, revealed).map((s, i) => ({
85
+ type: "step",
85
86
  ...s,
86
87
  status: streaming && revealed < EXTRACT.length && i === revealed - 1 ? "running" : "done",
87
88
  }));
@@ -119,7 +120,7 @@ export function TplExtract() {
119
120
  <Text size="xl" weight="semibold">Capture packing list</Text>
120
121
  <Text size="sm" color="muted">Drop a photo; the agent extracts each line, you review and edit, then save them all to the table.</Text>
121
122
  </View>
122
- {phase !== "intake" ? <Button title="Start over" color="muted" shape="rounded" onPress={reset} /> : null}
123
+ {phase !== "intake" ? <Button title="Start over" color="muted" onPress={reset} /> : null}
123
124
  </View>
124
125
 
125
126
  {phase === "intake" ? (
@@ -133,7 +134,7 @@ export function TplExtract() {
133
134
 
134
135
  {phase === "extracting" ? (
135
136
  <Card>
136
- <AgentRun steps={steps} state={streaming ? "streaming" : "done"} />
137
+ <AgentRun items={items} state={streaming ? "streaming" : "done"} />
137
138
  </Card>
138
139
  ) : null}
139
140
 
@@ -167,12 +168,11 @@ export function TplExtract() {
167
168
  ))}
168
169
  </View>
169
170
  <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
170
- <Button title="Confirm all" color="muted" shape="rounded" onPress={() => setStatuses(Object.fromEntries(RECORDS.map((r) => [r.id, "confirmed" as Status])))} />
171
+ <Button title="Confirm all" color="muted" onPress={() => setStatuses(Object.fromEntries(RECORDS.map((r) => [r.id, "confirmed" as Status])))} />
171
172
  <View style={{ flex: 1 }} />
172
173
  <Button
173
174
  title={`Save ${confirmedCount} ${confirmedCount === 1 ? "line" : "lines"}`}
174
175
  color="primary"
175
- shape="rounded"
176
176
  disabled={confirmedCount === 0}
177
177
  onPress={() => setPhase("done")}
178
178
  />
@@ -183,7 +183,7 @@ export function TplExtract() {
183
183
  {phase === "done" ? (
184
184
  <Card>
185
185
  <CompletionState title={`${confirmedCount} lines saved`} summary="The confirmed lines were written to the shipment table.">
186
- <Button title="Capture another" color="secondary" shape="rounded" onPress={reset} />
186
+ <Button title="Capture another" color="secondary" onPress={reset} />
187
187
  </CompletionState>
188
188
  </Card>
189
189
  ) : null}
@@ -5,7 +5,7 @@ import { colors, solid } from "@lotics/ui/colors";
5
5
  import { Badge } from "@lotics/ui/badge";
6
6
  import { Button } from "@lotics/ui/button";
7
7
  import { ChipGroup } from "@lotics/ui/chip_group";
8
- import { FilterPill } from "@lotics/ui/filter_pill";
8
+ import { FilterChip } from "@lotics/ui/filter_chip";
9
9
  import { RangeSlider, rangeSummary } from "@lotics/ui/range_slider";
10
10
  import { cycleSort, sortBy, type SortState } from "@lotics/ui/sort_header";
11
11
  import { Card } from "@lotics/ui/card";
@@ -259,9 +259,9 @@ export function TplInventory() {
259
259
  value={loc}
260
260
  onValueChange={setLoc}
261
261
  />
262
- <FilterPill label="Stock value" summary={amtSummary} onClear={() => setAmt([0, AMT_MAX])} clearLabel="Clear stock value filter">
262
+ <FilterChip label="Stock value" summary={amtSummary} onClear={() => setAmt([0, AMT_MAX])} clearLabel="Clear stock value filter">
263
263
  <RangeSlider min={0} max={AMT_MAX} step={5_000_000} value={amt} onValueChange={setAmt} format={fmtAmt} accessibilityLabel="Stock value" />
264
- </FilterPill>
264
+ </FilterChip>
265
265
  </View>
266
266
 
267
267
  {/* the stock register: eyebrow band → Divider-separated SKU rows */}
@@ -6,7 +6,7 @@ import { Divider } from "@lotics/ui/divider";
6
6
  import { Callout, CalloutText, CalloutTitle } from "@lotics/ui/callout";
7
7
  import { PressableHighlight } from "@lotics/ui/pressable_highlight";
8
8
  import { Composer } from "@lotics/ui/composer";
9
- import { AgentRun, type AgentRunStep } from "@lotics/ui/agent_run";
9
+ import { AgentRun, type AgentRunStep, type AgentRunItem } from "@lotics/ui/agent_run";
10
10
  import { SpecList, type SpecRow } from "@lotics/ui/spec_list";
11
11
  import { Confidence, type ConfidenceLevel } from "@lotics/ui/confidence";
12
12
  import { Badge } from "@lotics/ui/badge";
@@ -184,7 +184,8 @@ export function TplLookup() {
184
184
  };
185
185
 
186
186
  const streaming = phase === "running" && revealed < SCRIPT.length;
187
- const steps: AgentRunStep[] = SCRIPT.slice(0, revealed).map((s, i) => ({
187
+ const items: AgentRunItem[] = SCRIPT.slice(0, revealed).map((s, i) => ({
188
+ type: "step",
188
189
  ...s,
189
190
  status: streaming && i === revealed - 1 ? "running" : "done",
190
191
  }));
@@ -201,7 +202,7 @@ export function TplLookup() {
201
202
 
202
203
  <ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 16, gap: 16 }}>
203
204
  {/* current run */}
204
- <AgentRun steps={steps} state={streaming ? "streaming" : "done"} />
205
+ <AgentRun items={items} state={streaming ? "streaming" : "done"} />
205
206
 
206
207
  <View style={{ gap: 8 }}>
207
208
  <Text size="xs" color="muted" weight="medium">Nearest matches</Text>
@@ -72,7 +72,7 @@ export function TplMatch() {
72
72
  <Text size="sm" color="muted">The agent pairs incoming payments to open invoices — you confirm, reassign, or reject</Text>
73
73
  </View>
74
74
  {tab === "open" && highOpen > 0 ? (
75
- <Button title={`Accept ${highOpen} high-confidence`} color="secondary" shape="rounded" onPress={acceptAllHigh} />
75
+ <Button title={`Accept ${highOpen} high-confidence`} color="secondary" onPress={acceptAllHigh} />
76
76
  ) : null}
77
77
  </View>
78
78
 
@@ -13,7 +13,7 @@ import { FileDropzone } from "@lotics/ui/file_dropzone";
13
13
  import { FileThumbnailGrid } from "@lotics/ui/file_thumbnail_grid";
14
14
  import { FileGalleryModal } from "@lotics/ui/file_gallery_modal";
15
15
  import { type DisplayFile } from "@lotics/ui/file_thumbnail";
16
- import { AgentRun, type AgentRunStep } from "@lotics/ui/agent_run";
16
+ import { AgentRun, type AgentRunItem } from "@lotics/ui/agent_run";
17
17
  import { Table, TableRow, TableCell, type TableColumn } from "@lotics/ui/table";
18
18
  import { InlineNumberInput } from "@lotics/ui/inline_number_input";
19
19
  import { CompletionState } from "@lotics/ui/completion_state";
@@ -167,8 +167,8 @@ export function TplRatedesk() {
167
167
  setPhase("done");
168
168
  }, [lines, decisions]);
169
169
 
170
- const runSteps: AgentRunStep[] = READ_STEPS.slice(0, Math.min(revealed + 1, READ_STEPS.length)).map((s, i) => ({
171
- id: `r${i}`, label: s.label, kind: s.kind, status: i < revealed ? "done" : "running",
170
+ const runItems: AgentRunItem[] = READ_STEPS.slice(0, Math.min(revealed + 1, READ_STEPS.length)).map((s, i) => ({
171
+ type: "step", id: `r${i}`, label: s.label, kind: s.kind, status: i < revealed ? "done" : "running",
172
172
  }));
173
173
  const newCount = lines.filter((l) => l.status === "new").length;
174
174
 
@@ -181,7 +181,7 @@ export function TplRatedesk() {
181
181
  <Text size="xl" weight="semibold">Rate desk</Text>
182
182
  <Text size="sm" color="muted">Look up any route, carrier or container. Prices come from carrier sheets — re-ingest to refresh.</Text>
183
183
  </View>
184
- <Button title="Import prices" color="primary" shape="rounded" onPress={() => setImportOpen(true)} />
184
+ <Button title="Import prices" color="primary" onPress={() => setImportOpen(true)} />
185
185
  </View>
186
186
 
187
187
  {/* REGISTER — the live rate card, filter to anything */}
@@ -193,11 +193,11 @@ export function TplRatedesk() {
193
193
  <View style={{ flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 8 }}>
194
194
  <View style={{ flexDirection: "row", alignItems: "center", gap: 6, flexGrow: 1, flexBasis: 360, minWidth: 280 }}>
195
195
  <View style={{ flex: 1, minWidth: 120 }}>
196
- <Combobox pill icon="map-pin" options={originOpts} recentOptions={originOpts} recentsLabel="Origin" value={fromV} onValueChange={setFromV} clearable onClear={() => setFromV(null)} placeholder="From" accessibilityLabel="Origin port" />
196
+ <Combobox icon="map-pin" options={originOpts} recentOptions={originOpts} recentsLabel="Origin" value={fromV} onValueChange={setFromV} clearable onClear={() => setFromV(null)} placeholder="From" accessibilityLabel="Origin port" />
197
197
  </View>
198
198
  <Icon name="arrow-right" size={14} color={colors.zinc[400]} />
199
199
  <View style={{ flex: 1, minWidth: 120 }}>
200
- <Combobox pill icon="map-pin" options={destOpts} recentOptions={destOpts} recentsLabel="Destination" value={toV} onValueChange={setToV} clearable onClear={() => setToV(null)} placeholder="To" accessibilityLabel="Destination port" />
200
+ <Combobox icon="map-pin" options={destOpts} recentOptions={destOpts} recentsLabel="Destination" value={toV} onValueChange={setToV} clearable onClear={() => setToV(null)} placeholder="To" accessibilityLabel="Destination port" />
201
201
  </View>
202
202
  </View>
203
203
  <ColumnFilter column={carrierCol} value={carrierF} onChange={setCarrierF} clearLabel="Clear carrier" />
@@ -238,7 +238,7 @@ export function TplRatedesk() {
238
238
  </View>
239
239
  ) : null}
240
240
 
241
- {phase === "reading" ? <AgentRun steps={runSteps} state="streaming" /> : null}
241
+ {phase === "reading" ? <AgentRun items={runItems} state="streaming" /> : null}
242
242
 
243
243
  {phase === "review" ? (
244
244
  <ChangeReview
@@ -270,8 +270,8 @@ export function TplRatedesk() {
270
270
  away inside the list. */}
271
271
  {phase === "idle" ? (
272
272
  <DialogFooter>
273
- {files.length > 0 ? <Button title="Clear" color="muted" shape="rounded" onPress={() => setFiles([])} /> : null}
274
- <Button title={files.length ? `Read ${files.length} sheets` : "Read sheets"} color="primary" shape="rounded" disabled={!files.length} onPress={() => { setRevealed(0); setPhase("reading"); }} />
273
+ {files.length > 0 ? <Button title="Clear" color="muted" onPress={() => setFiles([])} /> : null}
274
+ <Button title={files.length ? `Read ${files.length} sheets` : "Read sheets"} color="primary" disabled={!files.length} onPress={() => { setRevealed(0); setPhase("reading"); }} />
275
275
  </DialogFooter>
276
276
  ) : phase === "review" ? (
277
277
  <DialogFooter>
@@ -286,8 +286,8 @@ export function TplRatedesk() {
286
286
  </DialogFooter>
287
287
  ) : phase === "done" ? (
288
288
  <DialogFooter>
289
- <Button title="Import more" color="secondary" shape="rounded" onPress={reset} />
290
- <Button title="Done" color="primary" shape="rounded" onPress={closeImport} />
289
+ <Button title="Import more" color="secondary" onPress={reset} />
290
+ <Button title="Done" color="primary" onPress={closeImport} />
291
291
  </DialogFooter>
292
292
  ) : null}
293
293
  </Dialog>
@@ -15,7 +15,7 @@ import { EmptyState } from "@lotics/ui/empty_state";
15
15
  import { KPIStrip } from "@lotics/ui/kpi_strip";
16
16
  import { Link } from "@lotics/ui/link";
17
17
  import { Pagination } from "@lotics/ui/pagination";
18
- import { PillButton } from "@lotics/ui/pill_button";
18
+ import { Chip } from "@lotics/ui/chip";
19
19
  import { SegmentedControl, type SegmentOption } from "@lotics/ui/segmented_control";
20
20
  import { formatMoney } from "@lotics/ui/format_money";
21
21
  import { formatDate, parseDate } from "@lotics/ui/format_date";
@@ -311,9 +311,9 @@ export function TplReport() {
311
311
  {chips.length > 0 ? (
312
312
  <View style={{ flexDirection: "row", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
313
313
  {chips.map((c) => (
314
- <PillButton key={c.key} onDismiss={c.clear} dismissTooltip="Clear filter">
314
+ <Chip key={c.key} onDismiss={c.clear} dismissTooltip="Clear filter">
315
315
  <Text size="xs" weight="medium" color="muted">{c.label}</Text>
316
- </PillButton>
316
+ </Chip>
317
317
  ))}
318
318
  </View>
319
319
  ) : null}