@lotics/ui 11.4.0 → 11.6.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 +41 -1100
- package/docs/ai_patterns.md +374 -0
- package/docs/catalog.md +920 -0
- package/docs/composition.md +439 -0
- package/docs/data_entry.md +398 -0
- package/docs/templates.md +357 -0
- package/examples/tpl_item_list.tsx +2 -6
- package/package.json +3 -2
- package/src/table.test.ts +90 -0
- package/src/table.tsx +200 -43
- package/src/table_fit.ts +88 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# Templates — the worked example screens
|
|
2
|
+
|
|
3
|
+
The package ships full worked-example screens in `examples/tpl_*.tsx` — each a complete,
|
|
4
|
+
runnable recipe for one screen **job**, built purely from `@lotics/ui` components plus mock
|
|
5
|
+
data. This doc is the map: what shape each template solves, what it owns versus what its
|
|
6
|
+
primitives own, and which one to start from. Read it when starting any new screen, then read
|
|
7
|
+
the chosen template's source before writing code. Component contracts live in
|
|
8
|
+
[the catalog](./catalog.md), the layout/color laws in [the composition grammar](./composition.md);
|
|
9
|
+
the package index is [../AGENTS.md](../AGENTS.md).
|
|
10
|
+
|
|
11
|
+
## How to use an example
|
|
12
|
+
|
|
13
|
+
- **Read the source** at `node_modules/@lotics/ui/examples/<name>.tsx`. Each file is
|
|
14
|
+
self-contained: `react` + `react-native` + `@lotics/ui/<module>` imports only, mock data
|
|
15
|
+
constants at the top, one exported screen component.
|
|
16
|
+
- **Copy and adapt — never import.** The `examples/` directory ships as `.tsx` source and is
|
|
17
|
+
**not** in the package's `exports` map: `import … from "@lotics/ui/examples/…"` fails module
|
|
18
|
+
resolution by design. Copy the bands you need into your app, keep the component imports,
|
|
19
|
+
and replace the mock constants with real reads.
|
|
20
|
+
- **Pick by the job, not the domain.** Templates are scenario-flavoured (a pick run, cash
|
|
21
|
+
application, a delivery week) but generic-purpose — they teach the shape. Your orders /
|
|
22
|
+
items / customers screen starts from the template whose *job* matches, whatever the domain.
|
|
23
|
+
- The templates encode the composition grammar (band order, toolbar law, color discipline,
|
|
24
|
+
status weight) as working code — when a template and your instinct disagree, the template
|
|
25
|
+
wins.
|
|
26
|
+
|
|
27
|
+
## Picking by the job
|
|
28
|
+
|
|
29
|
+
| You are building | Start from |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| Executive overview — KPIs, trends, an attention list | `tpl_dashboard` |
|
|
32
|
+
| Monitoring a population too large to browse (drill-down → faceted register) | `tpl_stock` |
|
|
33
|
+
| A live wallboard of hundreds of unit states | `tpl_tower` |
|
|
34
|
+
| One dimension crossed against another, with drill | `tpl_pivot` |
|
|
35
|
+
| Hierarchical totals chased down a tree | `tpl_rollup` |
|
|
36
|
+
| A register / consolidated work-list (browse, per-row action, bulk action) | `tpl_item_list` |
|
|
37
|
+
| A guided sequence of physical tasks (scan, confirm, next) | `tpl_pick` |
|
|
38
|
+
| Splitting one source amount across many targets | `tpl_allocate` |
|
|
39
|
+
| A record's create/edit surface — also the settings shape | `tpl_record` |
|
|
40
|
+
| A personal to-do list | `tpl_tasks` |
|
|
41
|
+
| A team task board (inline-managed grouped table) | `tpl_task_board` |
|
|
42
|
+
| Financial statements | `tpl_statements` |
|
|
43
|
+
| A scoped lookup report with export | `tpl_report` |
|
|
44
|
+
| A week calendar + agenda | `tpl_calendar` |
|
|
45
|
+
| An attendance desk | `tpl_attendance` |
|
|
46
|
+
| Shift signup + staffing | `tpl_shifts` |
|
|
47
|
+
| AI produces a visual artifact on a canvas | `tpl_dieline` |
|
|
48
|
+
| AI ranks answers — look-up-and-explain | `tpl_lookup` |
|
|
49
|
+
| AI over a record's documents (extract / cross-check / generate) | `tpl_documents` |
|
|
50
|
+
|
|
51
|
+
## Analytics
|
|
52
|
+
|
|
53
|
+
Read-mostly screens that answer a question about a population, then open doors into the
|
|
54
|
+
records behind each number.
|
|
55
|
+
|
|
56
|
+
### `tpl_dashboard` — the executive overview
|
|
57
|
+
|
|
58
|
+
One screen answering "how is this period going, where is it stuck, who pays the bills".
|
|
59
|
+
Bands: header + period · `KPIStrip` · a revenue chart (`BarChart`, emerald = money) beside
|
|
60
|
+
the pipeline (`StackedProgressBar`, blue = pipeline) · a **Needs attention** `Accordion`
|
|
61
|
+
(icon + label + count `Badge`; expanding opens the records behind the count, each row
|
|
62
|
+
carrying its domain `⋯ ActionMenu`) · top customers (`Avatar` + revenue + margin badge; the
|
|
63
|
+
name `Peek`s the dossier). Also exercises the wider chart family (`LineChart`, `PieChart`,
|
|
64
|
+
`RingGauge`, `Sparkline`, `TrendFooter`, `Funnel`) and `DateRangeFilterField` — the date
|
|
65
|
+
filter actually filters the KPI datasets. Start here for any "state of the operation"
|
|
66
|
+
landing screen.
|
|
67
|
+
|
|
68
|
+
### `tpl_stock` — the drill-down overview (large populations)
|
|
69
|
+
|
|
70
|
+
Monitoring a population too large to browse (10,000+ units). The funnel: `KPIStrip`
|
|
71
|
+
(population health) → `Breakdown` cards (composition by dimension; **pressing a segment IS
|
|
72
|
+
the drill-down**) → dismissible facet chips + search → a **paginated** register (the data
|
|
73
|
+
volume is the reason `Pagination` exists) → row press opens the sequenced unit `Drawer`, `⋯`
|
|
74
|
+
carries quick operations. Facets combine across dimensions and each `Breakdown`'s counts
|
|
75
|
+
respect the *other* dimensions' selections — true faceted search. Also the reference for
|
|
76
|
+
color discipline: coherent dimensions take one hue family shaded by `ramp`; status colors
|
|
77
|
+
carry meaning, one `ColorName` per state, no hand-picked hex.
|
|
78
|
+
|
|
79
|
+
### `tpl_tower` — the status-grid wallboard
|
|
80
|
+
|
|
81
|
+
Continuous monitoring of hundreds of live units (machines, gates, sensors). Three bands
|
|
82
|
+
answer the three ops questions in order: `KPIStrip` + `StatusGrid` = "is everything OK right
|
|
83
|
+
now?" (scan for red; the `StatusLegend` drills a state) · an exceptions rail = "where do I
|
|
84
|
+
look first?" (severity, then longest unresolved) · a `Heatmap` = "when/where does it
|
|
85
|
+
cluster?" (pressing a cell explains the day). Cells and exception rows open the SAME
|
|
86
|
+
sequenced unit `Drawer` (with a `Timeline` of the unit's events). One semantic color
|
|
87
|
+
reference per state — every weight (grid cell, legend dot, drawer badge) derives from that
|
|
88
|
+
family name.
|
|
89
|
+
|
|
90
|
+
### `tpl_pivot` — the cross-tab desk
|
|
91
|
+
|
|
92
|
+
One dimension crossed against another, then drill. The `Matrix` is the hero: the number IN
|
|
93
|
+
each cell plus a heat wash behind it so concentration reads at a glance, row/column/grand
|
|
94
|
+
totals down the edges. **Pressing a cell is a door** — the register below filters to exactly
|
|
95
|
+
that intersection; a row there opens the item drawer. The pattern for "how does X distribute
|
|
96
|
+
across Y, and what's behind each bucket?".
|
|
97
|
+
|
|
98
|
+
### `tpl_rollup` — hierarchical totals
|
|
99
|
+
|
|
100
|
+
Totals with drill: three levels (e.g. region → site → lane), every level carrying the SAME
|
|
101
|
+
aligned metric columns so a bad number can be chased down the tree ("which branch drags the
|
|
102
|
+
total?"). Pure composition: nested `Accordion`s + a fixed-width column map; under-target
|
|
103
|
+
branches are flagged at every level so the drill path is visible before expanding. Leaf rows
|
|
104
|
+
open a sequenced `Drawer`.
|
|
105
|
+
|
|
106
|
+
## Work
|
|
107
|
+
|
|
108
|
+
Screens where the user executes: browse → act. **Every work screen is FLAT (no `Card`) and
|
|
109
|
+
ONE of two shapes**: a register/list you WORK EACH of (press a row → workspace `Drawer`) or
|
|
110
|
+
a list you ACT ON MANY of (tick rows → `FloatingActionBar`) — never a side-by-side
|
|
111
|
+
master-detail panel.
|
|
112
|
+
|
|
113
|
+
### `tpl_item_list` — THE canonical register
|
|
114
|
+
|
|
115
|
+
The one work-execution list shape; it subsumes approvals, dispatch, batch-building, and run
|
|
116
|
+
screens — register, per-row action, gated selection, and act-on-many in one. The page:
|
|
117
|
+
|
|
118
|
+
- **One toolbar row** — search + a status `Select` + facet `FilterChip`s LEFT, the New CTA
|
|
119
|
+
RIGHT; a light `SummaryLine` of the filtered view below.
|
|
120
|
+
- **A sortable, `Divider`-separated `Table`** where every row carries a leading checkbox; a
|
|
121
|
+
row that can't take the bulk action gets a **disabled** checkbox (the same gating as any
|
|
122
|
+
blocked line). A per-row action `Button` (here Print) is the row's primary action in the
|
|
123
|
+
trailing column, `⋯` its overflow. A select-all band, footer totals + `Pagination`, and a
|
|
124
|
+
`FloatingActionBar` carrying the bulk action while rows are ticked.
|
|
125
|
+
- **A row press opens the PRODUCTION workspace `Drawer`**, which demonstrates the full
|
|
126
|
+
record-workspace anatomy section by section:
|
|
127
|
+
- **Details `DetailTable`** showing every inline field type — text, `InlineMemberSelect`,
|
|
128
|
+
dates incl. optional time, dot-select, money number, the `InlineTagSelect` tag field,
|
|
129
|
+
and `InlineStatic` + a System badge for read-only values. Detail rows earn trailing CTAs
|
|
130
|
+
where deserved (Phone → Call, Due → Today) — the case where `trailingWidth` IS set.
|
|
131
|
+
- **Linked records** — the customer's other records as `ListItem`s (title + description at
|
|
132
|
+
the column edge, badge · amount · chevron); pressing one PUSHES an **editable** workspace
|
|
133
|
+
for that record inside the drawer via the hosted `ScreenRouter` — while `canGoBack` the
|
|
134
|
+
drawer header swaps to a back button + the pushed id and the ◀ ▶ sequencer hides; back
|
|
135
|
+
pops with scroll preserved.
|
|
136
|
+
- **Files** with its own Add CTA — `FileRows` CRUD via `pickFiles`, plus an EXPECTED
|
|
137
|
+
document as a ghost `FileRow placeholder` with a Request action.
|
|
138
|
+
- **Payment** — the `Ledger` (charge/received groups with sums, peekable fee rows,
|
|
139
|
+
`LedgerTotal`) + a Record-payment popover that appends a receipt.
|
|
140
|
+
- **Activity** — the CRM touch-log shape: optional outcome pills (`ChipGroup`,
|
|
141
|
+
tap-to-deselect) over a MULTILINE note (the note IS the record and the one requirement),
|
|
142
|
+
an always-visible optional follow-up `InlineDatePicker`, an "Attach files" secondary
|
|
143
|
+
(`pickFiles` → removable `FileThumbnailGrid` in the composer), and the primary Log button
|
|
144
|
+
saying WHY when disabled. Below, a labelled History `Timeline` of outcome-typed entries,
|
|
145
|
+
newest first — an entry's attachments ride its expandable details as a thumbnail grid
|
|
146
|
+
whose press opens the shared `FileGalleryModal`.
|
|
147
|
+
- A closing `DangerZone`.
|
|
148
|
+
|
|
149
|
+
### `tpl_pick` — the guided queue
|
|
150
|
+
|
|
151
|
+
Work that is a SEQUENCE of physical tasks, not a list to browse: the run hands the operator
|
|
152
|
+
the next location, they scan to verify (`ScanField`), confirm the count (`Counter`), and the
|
|
153
|
+
next task takes its place. A single-focus column — the CURRENT task is the only thing in view;
|
|
154
|
+
the whole path is a collapsible `Accordion` below, collapsed by default, so position is one
|
|
155
|
+
tap away and never competes with the task. Can't-skip ordering, `Stepper` + `ProgressBar`
|
|
156
|
+
progress, and a short-pick exception that flags-and-advances without stalling, ending in a
|
|
157
|
+
`CompletionState`.
|
|
158
|
+
|
|
159
|
+
### `tpl_allocate` — the split
|
|
160
|
+
|
|
161
|
+
Apply ONE source across MANY targets until the remainder is zero (cash application,
|
|
162
|
+
stock-to-orders, budget distribution). The inverse of a batch builder: batch SUMS parts up to
|
|
163
|
+
a total; allocation SPLITS a fixed total DOWN with a remainder that must hit zero. The
|
|
164
|
+
`RemainderMeter` is the spine — under (left on account) · exact (apply) · over (blocked).
|
|
165
|
+
Oldest-first auto-allocates; each `AllocationRow` can be filled or typed.
|
|
166
|
+
|
|
167
|
+
### `tpl_record` — THE record surface
|
|
168
|
+
|
|
169
|
+
Create-then-refine taken all the way: **"New" is ONE CLICK** (no dialog, no form) — it
|
|
170
|
+
creates a fresh Draft and the surface IS the editor; everything refines in place. It absorbed
|
|
171
|
+
the old order-form, inline-record, intake, settings, billing, and quick-capture templates.
|
|
172
|
+
Top → bottom:
|
|
173
|
+
|
|
174
|
+
- A `RecordSummary` header (identity · status · the one metric) over its key-fact
|
|
175
|
+
`DetailTable`, then a Details `DetailTable` — one shared column grid, auto-stacking when
|
|
176
|
+
narrow.
|
|
177
|
+
- **The customer section — TWO states, no swap mode.** Attached = the read-only-first card:
|
|
178
|
+
Edit/Done live in the SECTION HEADING and swap the fields to inline chips, where a trailing
|
|
179
|
+
Fetch fills contact + city from a registry lookup keyed by tax ID, un-gating billing;
|
|
180
|
+
Remove (danger) sits low and detaches the link (the customer stays in the book) → the
|
|
181
|
+
find-or-create search, whose custom row opens the create `Dialog` rendering the SAME
|
|
182
|
+
inline-chip table + Fetch.
|
|
183
|
+
- **Files** — `FileDropzone` add, gallery preview, delete.
|
|
184
|
+
- **Billing, owned by a later desk** (gated until the record reaches that stage), in the
|
|
185
|
+
inline vocabulary: each invoice is a hairline-set band; charge amounts are chips with the
|
|
186
|
+
list price as ghost placeholder + a one-tap "Standard …" suggestion pill while unset;
|
|
187
|
+
"How paid…" select chips; NO per-band totals — the collect band owns the number. The band's
|
|
188
|
+
action row sits BOTTOM-RIGHT as *hint → status dot → Issue*, the hint naming exactly what
|
|
189
|
+
blocks (fees → method → the customer gate). An ISSUED invoice keeps its lines editable and
|
|
190
|
+
offers Re-issue — a new lookup code replaces the old, confirmed in the same `Dialog`. Plus
|
|
191
|
+
a validated receipt and a separate refundable deposit.
|
|
192
|
+
- **The lifecycle is a HANDOFF CHAIN** (e.g. Sales → Operations → Accounting → Closed): a
|
|
193
|
+
Tasks section speaking the full task-list grammar — a clearable Group-by `FilterChip`
|
|
194
|
+
(Desk/Assignee/Status) plus assignee/status filter chips derive the groups (empty groups
|
|
195
|
+
drop, EXCEPT desk groups — the journey stays visible; desk heads keep their owner dots,
|
|
196
|
+
assignee heads are `MemberChip`s), the add-a-task `CaptureRow` at the TOP landing on the
|
|
197
|
+
current desk, rows riding the `Checklist`/`ChecklistRow` compound with per-task
|
|
198
|
+
`InlineMemberSelect` assignees and a per-row `menu` (move to next desk · danger Delete),
|
|
199
|
+
and suggested tasks as `SuggestionChip`s. EVERY desk's rows edit — the stage gates the
|
|
200
|
+
handoff and Billing, never task editing (planning ahead on a later desk is normal work).
|
|
201
|
+
**Per-stage handoff CTAs are NEVER blocked** — open tasks warn and carry over.
|
|
202
|
+
- Also the settings shape: Preferences switch rows + a `DangerZone`.
|
|
203
|
+
- A LEFT OUTLINE RAIL (`MenuButton` + `useSectionNav`) jumps between the sections and
|
|
204
|
+
scroll-spies the active one; on narrow containers it becomes the pinned current-section
|
|
205
|
+
bar opening a section-picker `Modal`.
|
|
206
|
+
|
|
207
|
+
### `tpl_tasks` — the quick list
|
|
208
|
+
|
|
209
|
+
The personal to-do shape: quiet `CheckCircle` rows you tick to finish and tap to **expand in
|
|
210
|
+
place** to edit (`Inline*` editors; the row is stable — the meta summary never shifts on
|
|
211
|
+
expand), colour-dot tags (`OptionBadge`), an attachments field (`FilesEditor` — thumbnail
|
|
212
|
+
grid, tap a tile for the gallery with download + confirmed remove, plus its Add button),
|
|
213
|
+
search + a clearable group-by (✕ → ungrouped) + tag filter chips, and a primary
|
|
214
|
+
Draft-from-notes CTA (`Composer` → `AgentRun` → `ChangeReview`). No assignee — the list is
|
|
215
|
+
yours.
|
|
216
|
+
|
|
217
|
+
**Warning — never a button in a button.** There is deliberately NO `Task` component: a task
|
|
218
|
+
row varies too much to bake into one, so rows compose `CheckCircle` + `InlineTextInput` +
|
|
219
|
+
inline-editor cells as **sibling** controls in a plain `View`. NEVER wrap the whole row in a
|
|
220
|
+
`Pressable accessibilityRole="button"` (for tap-to-expand) with those controls nested inside:
|
|
221
|
+
React Native Web renders the row AND each control as a `<button>`, so you get `<button>`s
|
|
222
|
+
inside a `<button>` — invalid DOM, and the inner controls drop out of the keyboard tab order.
|
|
223
|
+
Give "expand" its own affordance (a trailing `Pressable` over the meta/chevron), as
|
|
224
|
+
`tpl_tasks` does.
|
|
225
|
+
|
|
226
|
+
### `tpl_task_board` — the columns shape
|
|
227
|
+
|
|
228
|
+
The manager's board: a search · group-by · filter toolbar over a grouped, sortable grid of
|
|
229
|
+
LIVE inline-editable cells — assignee/due/status/tags set directly in the cell. It builds on
|
|
230
|
+
the `DataGrid` primitive (the inline-managed grouped-table shape) and **owns only its data,
|
|
231
|
+
toolbar, columns, and per-group add**; `DataGrid` owns the sortable header, the collapsible
|
|
232
|
+
grouped sections, and the aligned rows. It demonstrates:
|
|
233
|
+
|
|
234
|
+
- A **files cell** — thumbnail glance in the cell → a `Popover` hosting the full
|
|
235
|
+
`FilesEditor` → gallery.
|
|
236
|
+
- A **dynamic action column** — a per-row underlined action link (`TextLink`) driven by each
|
|
237
|
+
row's `action` descriptor (attach / approve / open…), each wired in a real app to its OWN
|
|
238
|
+
workflow: one board, a different action per row; the link's pressable fills the cell to
|
|
239
|
+
match the inline editors.
|
|
240
|
+
- A trailing `ActionMenu` (the `⋯` overflow — a `danger` Remove behind a confirm `Alert`).
|
|
241
|
+
- CUSTOM pressable cells (files, the action link) hover with the control-surface BORDER
|
|
242
|
+
reveal (`HOVER_BORDER` from `@lotics/ui/control_surface`) — the same language as the
|
|
243
|
+
inline-editor cells beside them, never their own background wash.
|
|
244
|
+
- Per-group add rows that pre-set the group's field (align with the exported `gridRowStyle`),
|
|
245
|
+
and the same Draft-from-notes CTA as `tpl_tasks`.
|
|
246
|
+
|
|
247
|
+
Both `tpl_tasks` and `tpl_task_board`: filters/search left, primary CTA right, one toolbar
|
|
248
|
+
row; group-by holds only real dimensions, ✕ clears to ungrouped (no "Nothing" option).
|
|
249
|
+
|
|
250
|
+
**Altitude rule**: `tpl_task_board` is the inline-managed grouped table — MODERATE data you
|
|
251
|
+
manage in view (group/sort/filter/edit-in-place; it renders ALL rows). For thousands+ you
|
|
252
|
+
BROWSE — that's `tpl_item_list`'s paginated register, not this.
|
|
253
|
+
|
|
254
|
+
## Finance
|
|
255
|
+
|
|
256
|
+
### `tpl_statements` — the statement grammar
|
|
257
|
+
|
|
258
|
+
The income statement, balance sheet, and cash flow statement in ONE statement grammar:
|
|
259
|
+
right-aligned column captions over fixed money columns, items indented under group headers, a
|
|
260
|
+
hairline rule above every subtotal, the grand total DOUBLE-RULED (the accounting convention),
|
|
261
|
+
negatives in accounting parentheses, per-cell currency-free (the meta line names the currency
|
|
262
|
+
once), no bars, no charts — the numbers are the interface. Composed from `Text` + `Divider`
|
|
263
|
+
alone: it teaches a grammar, not a component. The three statements TIE — net income flows
|
|
264
|
+
into retained earnings, closing cash IS the balance sheet's cash, the loan repayment moves
|
|
265
|
+
the debt line. Keep your books reconciled when adapting the data, or the screen teaches the
|
|
266
|
+
wrong thing.
|
|
267
|
+
|
|
268
|
+
### `tpl_report` — the scope-first lookup report
|
|
269
|
+
|
|
270
|
+
Answer a question by SCOPING a dataset, not by browsing it. The scope bar leads: a header
|
|
271
|
+
period (`DateRangeFilterField`) + ONE search key chosen from several mutually-exclusive
|
|
272
|
+
dimensions (`SegmentedControl` swaps the matching picker; switching the key resets the
|
|
273
|
+
value). That scope drives everything below — KPI totals → a row of pressable `Breakdown`
|
|
274
|
+
facets (the long-tail one folds behind `maxRows`) → the paginated register of matching lines
|
|
275
|
+
→ an Export button. Leave the search empty and the period alone gives the whole-period
|
|
276
|
+
report. The fee / transaction / usage lookup desk.
|
|
277
|
+
|
|
278
|
+
## Scheduling
|
|
279
|
+
|
|
280
|
+
### `tpl_calendar` — the week desk
|
|
281
|
+
|
|
282
|
+
One real `CalendarView` week grid over the current week's events, then a "Today" agenda card
|
|
283
|
+
of pressable rows. Every slot is a door: pressing a grid event or an agenda row opens the
|
|
284
|
+
sequenced workspace `Drawer`; agenda rows also carry the `⋯` quick-actions menu. Event colors
|
|
285
|
+
carry meaning (one `ColorName` per event kind). The mock data anchors to the real current
|
|
286
|
+
week so the copied screen is evergreen.
|
|
287
|
+
|
|
288
|
+
### `tpl_attendance` — the attendance desk
|
|
289
|
+
|
|
290
|
+
Who is in, who is late, who is out, plus the weekly per-person grid. Header + month `Picker`
|
|
291
|
+
· `KPIStrip` (the four counts) · `Tabs` Today / This week. "Today" = one banded roster
|
|
292
|
+
`Card` — each row press-opens the person's workspace `Drawer` (◀ ▶ sequencing) and carries a
|
|
293
|
+
`⋯ ActionMenu` with the day's quick fixes. "This week" = per-person `Accordion` rows: day
|
|
294
|
+
markers expanding to the day-by-day detail in place. Presence is one accent; badge colors
|
|
295
|
+
carry the status meaning.
|
|
296
|
+
|
|
297
|
+
### `tpl_shifts` — two-sided staffing
|
|
298
|
+
|
|
299
|
+
Members SIGN UP, the manager DECIDES and FILLS. Three bands reuse the execution grammar: the
|
|
300
|
+
coverage board is the overview (shifts × days, each cell its filled/required state — press to
|
|
301
|
+
open the slot), the signup rail is the APPROVALS pattern (in-hours = one-click Accept;
|
|
302
|
+
overtime = a consequence `Dialog`; decline behind `⋯`), and the slot drawer fills gaps with
|
|
303
|
+
the DISPATCH fit-gate (every candidate names their hours; same-slot members are excluded,
|
|
304
|
+
double-shift days flagged). Accepting and assigning move the board live.
|
|
305
|
+
|
|
306
|
+
## Agents
|
|
307
|
+
|
|
308
|
+
Screens where AI does the work and the human reviews. `tpl_dieline` *produces* an artifact;
|
|
309
|
+
`tpl_lookup` and `tpl_documents` *structure* information for a decision. The AI component
|
|
310
|
+
contracts and the propose→review→apply loop live in [ai_patterns.md](./ai_patterns.md).
|
|
311
|
+
|
|
312
|
+
### `tpl_dieline` — the design canvas
|
|
313
|
+
|
|
314
|
+
The page IS the design: a parametric drawing fills the centre of a pannable/zoomable surface,
|
|
315
|
+
a FLOATING `Composer` sits at the bottom, and a pinned PARAMS PANEL (a hand-composed
|
|
316
|
+
label/value field card in live-edit mode) floats centre-right. Empty → drop a photo in the
|
|
317
|
+
composer → the centre processes while the composer MORPHS into a progress pill
|
|
318
|
+
(`AgentProgress`) → the drawing reveals, the params panel appears, the composer returns. Two
|
|
319
|
+
ways to change it: prompt the agent ("5 mm taller") or edit a param directly — either
|
|
320
|
+
re-flows the design in place. The geometry is deterministic (the app owns it); the agent only
|
|
321
|
+
proposes parameters. The panel carries the single Download.
|
|
322
|
+
|
|
323
|
+
### `tpl_lookup` — the answer desk
|
|
324
|
+
|
|
325
|
+
Look-up-and-explain (classification, tariff/fee lookup, policy Q&A, a spec/compliance desk).
|
|
326
|
+
Describe the subject on the LEFT (`Composer` + `AgentRun`), the agent RANKS the matching
|
|
327
|
+
candidates (nearest matches, the top one wearing a Recommended `Badge`), you pick one and its
|
|
328
|
+
structured ANSWER pins on the RIGHT — the code, the exact breakdown as hand-composed spec
|
|
329
|
+
rows, policy `Callout`s, `Confidence`, and `Sources`. The flow is input → matches → pick, not
|
|
330
|
+
a single confident verdict: classification is ambiguous, so the alternatives are first-class.
|
|
331
|
+
Refine the description in a follow-up and the ranking updates.
|
|
332
|
+
|
|
333
|
+
### `tpl_documents` — the document desk
|
|
334
|
+
|
|
335
|
+
THE worked example for everything document-driven on a record. A record's files block feeds
|
|
336
|
+
ONE **"Use AI"** entry — the one AI entry point, never per-row AI buttons (the per-row `⋯`
|
|
337
|
+
menu holds only Rename/Download). Inside its dialog the run FORKS into three tasks as
|
|
338
|
+
`CardSelectItem`s (select → a confirming footer CTA, never one-click):
|
|
339
|
+
|
|
340
|
+
- **Extract** (read the documents, fill the record) — the open `ChangeFields` review: ONE
|
|
341
|
+
`Change` section per record whose body stacks a `ChangeField` per proposed value (label ·
|
|
342
|
+
the − band when replacing · the editable + value). **Editing IS the review**; an add has no
|
|
343
|
+
before, a removal is the − band alone, and a conflict shows the read-only outcome band over
|
|
344
|
+
candidate rows + a type-another-value third option. Proposed line items arrive as
|
|
345
|
+
`ChangeRecord` cards; Keep-all + one outcome-named Apply commit.
|
|
346
|
+
- **Cross-check** (compare the documents against the record and each other) — ranked,
|
|
347
|
+
display-only `Finding`s the human acts on, closing in a recorded verdict.
|
|
348
|
+
- **Edit with AI** — the `askAi` handoff: dialogue-shaped file iteration happens in the
|
|
349
|
+
platform chat (the documents attached to a fresh thread), not in the app.
|
|
350
|
+
|
|
351
|
+
Separately, a **Create documents** action opens its own dialog: a readiness checklist
|
|
352
|
+
(hand-composed rows — per-document missing inputs open a fill screen, saved values recompute
|
|
353
|
+
readiness) → generation runs as a visible `AgentRun`, never a teleport → generated files stay
|
|
354
|
+
PENDING (previewable) until the user commits them onto the record.
|
|
355
|
+
|
|
356
|
+
All mock — `useState` plus a timer streams the `AgentRun`; a real app drives the same
|
|
357
|
+
components from its live agent run.
|
|
@@ -390,12 +390,8 @@ function HoSoRow({ hs, daThu, tasks, onTasksChange, suggestions, onDismissSugges
|
|
|
390
390
|
minHeight={56}
|
|
391
391
|
accessibilityLabel={`Open record ${hs.ma} — ${hs.khach}`}
|
|
392
392
|
leading={<CheckboxInput accessibilityLabel={selectable ? `Select ${hs.ma}` : `${hs.ma} — awaiting docs, not ready to export`} checked={marked} disabled={!selectable} onChange={onToggle} />}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
<Button title="Print" icon={docFor(hs, daThu).icon} color="secondary" onPress={() => {}} />
|
|
396
|
-
<ActionMenu items={menuFor(hs, daThu)} accessibilityLabel={`Actions for ${hs.ma}`} />
|
|
397
|
-
</View>
|
|
398
|
-
}
|
|
393
|
+
action={<Button title="Print" icon={docFor(hs, daThu).icon} color="secondary" onPress={() => {}} />}
|
|
394
|
+
trailing={<ActionMenu items={menuFor(hs, daThu)} accessibilityLabel={`Actions for ${hs.ma}`} />}
|
|
399
395
|
>
|
|
400
396
|
<TableCell>
|
|
401
397
|
<Text size="sm" weight="semibold" tabular>{hs.ma}</Text>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./tokens": "./src/tokens.ts",
|
|
@@ -223,7 +223,8 @@
|
|
|
223
223
|
"files": [
|
|
224
224
|
"src",
|
|
225
225
|
"examples",
|
|
226
|
-
"AGENTS.md"
|
|
226
|
+
"AGENTS.md",
|
|
227
|
+
"docs"
|
|
227
228
|
],
|
|
228
229
|
"publishConfig": {
|
|
229
230
|
"access": "public"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { computeTableFit, type TableFitColumn } from "./table_fit";
|
|
3
|
+
|
|
4
|
+
// The canonical register's columns (tpl_item_list): a fixed identity column,
|
|
5
|
+
// a flexible name column, then fixed data columns — plus a 24px leading
|
|
6
|
+
// checkbox gutter and a 132px trailing actions gutter.
|
|
7
|
+
const REGISTER: TableFitColumn[] = [
|
|
8
|
+
{ key: "ma", width: 112 },
|
|
9
|
+
{ key: "khach" }, // flex
|
|
10
|
+
{ key: "trangThai", width: 112 },
|
|
11
|
+
{ key: "viec", width: 82 },
|
|
12
|
+
{ key: "phi", width: 116 },
|
|
13
|
+
{ key: "ngayNhan", width: 72 },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function visible(fit: ReturnType<typeof computeTableFit>): string[] {
|
|
17
|
+
return REGISTER.filter((c) => fit.visibleKeys.has(c.key)).map((c) => c.key);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("computeTableFit", () => {
|
|
21
|
+
it("keeps every column when the container fits them all", () => {
|
|
22
|
+
const fit = computeTableFit(REGISTER, 24, 132, 1040);
|
|
23
|
+
expect(fit.stacked).toBe(false);
|
|
24
|
+
expect(visible(fit)).toEqual(["ma", "khach", "trangThai", "viec", "phi", "ngayNhan"]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("drops columns right-to-left by default, one at a time, until the rest fit", () => {
|
|
28
|
+
// 700px: the full set (908px) and the set minus "ngayNhan" (822px) overflow;
|
|
29
|
+
// minus "phi" too (692px) fits.
|
|
30
|
+
const fit = computeTableFit(REGISTER, 24, 132, 700);
|
|
31
|
+
expect(fit.stacked).toBe(false);
|
|
32
|
+
expect(visible(fit)).toEqual(["ma", "khach", "trangThai", "viec"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("keeps a high-priority column alive while lower-priority ones drop", () => {
|
|
36
|
+
const withPriority = REGISTER.map((c) => (c.key === "phi" ? { ...c, priority: 1 } : c));
|
|
37
|
+
const fit = computeTableFit(withPriority, 24, 132, 700);
|
|
38
|
+
expect(fit.stacked).toBe(false);
|
|
39
|
+
// "phi" (priority 1) outlives "trangThai"/"viec"/"ngayNhan" (default = index).
|
|
40
|
+
expect(fit.visibleKeys.has("phi")).toBe(true);
|
|
41
|
+
expect(fit.visibleKeys.has("trangThai")).toBe(false);
|
|
42
|
+
expect(fit.visibleKeys.has("viec")).toBe(false);
|
|
43
|
+
expect(fit.visibleKeys.has("ngayNhan")).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("never drops the first column", () => {
|
|
47
|
+
// Wide enough for exactly the identity + flex pair, no more.
|
|
48
|
+
const fit = computeTableFit(REGISTER, 0, 0, 320);
|
|
49
|
+
expect(fit.stacked).toBe(false);
|
|
50
|
+
expect(fit.visibleKeys.has("ma")).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("never drops the first column even when it carries the worst priority", () => {
|
|
54
|
+
const hostile = REGISTER.map((c) => (c.key === "ma" ? { ...c, priority: 99 } : c));
|
|
55
|
+
const fit = computeTableFit(hostile, 24, 132, 700);
|
|
56
|
+
expect(fit.stacked).toBe(false);
|
|
57
|
+
expect(fit.visibleKeys.has("ma")).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("stacks when even the minimum column set overflows (the phone case)", () => {
|
|
61
|
+
// A 390px phone minus the page padding: the canonical register's identity +
|
|
62
|
+
// customer + gutters still need ~470px.
|
|
63
|
+
const fit = computeTableFit(REGISTER, 24, 132, 334);
|
|
64
|
+
expect(fit.stacked).toBe(true);
|
|
65
|
+
// Stacked mode shows every column — vertical space is free.
|
|
66
|
+
expect(visible(fit)).toEqual(["ma", "khach", "trangThai", "viec", "phi", "ngayNhan"]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("counts the leading/trailing gutters and gaps in the fit math", () => {
|
|
70
|
+
// Two fixed columns: 40 padding + 100 + 100 + 1 gap (14) = 254 exactly.
|
|
71
|
+
const two: TableFitColumn[] = [
|
|
72
|
+
{ key: "a", width: 100 },
|
|
73
|
+
{ key: "b", width: 100 },
|
|
74
|
+
];
|
|
75
|
+
expect(computeTableFit(two, 0, 0, 254).stacked).toBe(false);
|
|
76
|
+
expect(computeTableFit(two, 0, 0, 253).stacked).toBe(true);
|
|
77
|
+
// Adding a 24px leading gutter adds the gutter and one more gap.
|
|
78
|
+
expect(computeTableFit(two, 24, 0, 292).stacked).toBe(false);
|
|
79
|
+
expect(computeTableFit(two, 24, 0, 291).stacked).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("a single-column table fits until the gutters overflow, then stacks", () => {
|
|
83
|
+
const one: TableFitColumn[] = [{ key: "a" }];
|
|
84
|
+
// 40 padding + 120 flex minimum = 160.
|
|
85
|
+
expect(computeTableFit(one, 0, 0, 200).stacked).toBe(false);
|
|
86
|
+
const tiny = computeTableFit(one, 0, 0, 100);
|
|
87
|
+
expect(tiny.stacked).toBe(true);
|
|
88
|
+
expect(tiny.visibleKeys.has("a")).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|