@lotics/ui 11.3.0 → 11.5.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 +904 -0
- package/docs/composition.md +439 -0
- package/docs/data_entry.md +398 -0
- package/docs/templates.md +357 -0
- package/package.json +3 -2
- package/src/popover.tsx +79 -28
|
@@ -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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.5.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"
|
package/src/popover.tsx
CHANGED
|
@@ -188,7 +188,7 @@ export interface PopoverContentProps {
|
|
|
188
188
|
* restores it on close. Set false for an anchored panel whose trigger must
|
|
189
189
|
* keep focus — e.g. a search-as-you-type combobox whose input lives outside
|
|
190
190
|
* the popover and drives the list via `aria-activedescendant`. Focus then
|
|
191
|
-
* stays on the trigger; keyboard still reaches
|
|
191
|
+
* stays on the trigger; keyboard still reaches the content. */
|
|
192
192
|
manageFocus?: boolean;
|
|
193
193
|
}
|
|
194
194
|
|
|
@@ -212,9 +212,9 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
212
212
|
const [isBottomSheetShown, setIsBottomSheetShown] = useState(false);
|
|
213
213
|
const returnFocusRef = useRef<HTMLElement | null>(null);
|
|
214
214
|
// Last known trigger geometry. The trigger can unmount while the popover is
|
|
215
|
-
// open — a hover-revealed menu button stops being rendered
|
|
216
|
-
//
|
|
217
|
-
//
|
|
215
|
+
// open — a hover-revealed menu button stops being rendered once its row loses
|
|
216
|
+
// hover/focus — so positioning cannot rely on `triggerRef.current` still being
|
|
217
|
+
// live when it runs.
|
|
218
218
|
const triggerRectRef = useRef<DOMRect | null>(null);
|
|
219
219
|
|
|
220
220
|
const handleClose = useCallback(() => {
|
|
@@ -224,9 +224,9 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
224
224
|
|
|
225
225
|
// Snapshot the trigger geometry synchronously the moment we open, while it is
|
|
226
226
|
// guaranteed to still be mounted. `calculatePosition` runs later in a rAF, by
|
|
227
|
-
// which point a hover-revealed trigger may already be gone (
|
|
228
|
-
//
|
|
229
|
-
// anchored to where the trigger was instead of stranding it off-screen.
|
|
227
|
+
// which point a hover-revealed trigger may already be gone (a hover-revealed row
|
|
228
|
+
// button can unmount once hover/focus leaves the row). Capturing here keeps the
|
|
229
|
+
// popover anchored to where the trigger was instead of stranding it off-screen.
|
|
230
230
|
useLayoutEffect(() => {
|
|
231
231
|
if (!open || small) return;
|
|
232
232
|
if (triggerRef.current) {
|
|
@@ -249,7 +249,10 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
249
249
|
const firstFocusable = content.querySelector<HTMLElement>(
|
|
250
250
|
'input, select, textarea, button, [role="button"], [role="menuitem"], [role="option"], [tabindex]:not([tabindex="-1"])',
|
|
251
251
|
);
|
|
252
|
-
|
|
252
|
+
// preventScroll: focusing the content must never scroll an ancestor into
|
|
253
|
+
// view — that scroll would trip the scroll-to-dismiss listener and close the
|
|
254
|
+
// popover the instant it opens.
|
|
255
|
+
(firstFocusable ?? content).focus({ preventScroll: true });
|
|
253
256
|
});
|
|
254
257
|
|
|
255
258
|
return () => {
|
|
@@ -262,11 +265,11 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
262
265
|
// popover (a Picker/InlineSelect/menu) it is <body>; restoring there strands
|
|
263
266
|
// focus and the next Tab jumps to the page's first focusable. Fall back to
|
|
264
267
|
// the prior element only if the trigger is gone (e.g. a hover-revealed menu
|
|
265
|
-
// button that unmounted
|
|
268
|
+
// button that unmounted once its row lost hover).
|
|
266
269
|
const trigger = triggerRef.current;
|
|
267
270
|
const target = trigger && trigger.isConnected ? trigger : prior;
|
|
268
271
|
if (target && typeof target.focus === "function") {
|
|
269
|
-
target.focus();
|
|
272
|
+
target.focus({ preventScroll: true });
|
|
270
273
|
}
|
|
271
274
|
};
|
|
272
275
|
}, [open, manageFocus]);
|
|
@@ -313,6 +316,34 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
313
316
|
};
|
|
314
317
|
}, [open, small, onOpenChange]);
|
|
315
318
|
|
|
319
|
+
// Outside-dismiss (NON-MODAL anchored popover): with no blocking overlay, the
|
|
320
|
+
// rest of the page stays interactive, so we close on a click that lands outside
|
|
321
|
+
// this popover's content AND its trigger. We listen on `click` (not pointerdown)
|
|
322
|
+
// in the CAPTURE phase so that clicking ANOTHER field's trigger completes that
|
|
323
|
+
// trigger's own press — the old popover dismisses AND the new one opens in a
|
|
324
|
+
// single click, instead of the press being cancelled mid-gesture. A click INSIDE
|
|
325
|
+
// a deeper (nested) popover keeps this one open — a Select opened inside a filter
|
|
326
|
+
// popover must not dismiss the filter. The bottom sheet (`small`) is modal and
|
|
327
|
+
// dismisses via its scrim, so it opts out here.
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
if (!open || small) return;
|
|
330
|
+
const onOutsideClick = (e: MouseEvent) => {
|
|
331
|
+
const node = e.target instanceof Node ? e.target : null;
|
|
332
|
+
if (!node) return;
|
|
333
|
+
if (popoverRef.current?.contains(node)) return; // inside my content
|
|
334
|
+
if (triggerRef.current?.contains(node)) return; // on my trigger — its own press toggles
|
|
335
|
+
// Keep open when the click is inside a DEEPER popover (e.g. a Select opened
|
|
336
|
+
// inside this one). `data-popover-level` is the same nesting depth as z-index.
|
|
337
|
+
const myLevel = Number(popoverRef.current?.getAttribute("data-popover-level") ?? "0");
|
|
338
|
+
const el = node instanceof Element ? node : node.parentElement;
|
|
339
|
+
const hit = el?.closest("[data-popover]");
|
|
340
|
+
if (hit && Number(hit.getAttribute("data-popover-level") ?? "0") > myLevel) return; // deeper popover
|
|
341
|
+
onOpenChange(false);
|
|
342
|
+
};
|
|
343
|
+
document.addEventListener("click", onOutsideClick, true);
|
|
344
|
+
return () => document.removeEventListener("click", onOutsideClick, true);
|
|
345
|
+
}, [open, small, onOpenChange, triggerRef]);
|
|
346
|
+
|
|
316
347
|
const calculatePosition = useCallback(() => {
|
|
317
348
|
if (!popoverRef.current) return;
|
|
318
349
|
if (small) return;
|
|
@@ -476,11 +507,26 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
476
507
|
}
|
|
477
508
|
});
|
|
478
509
|
|
|
510
|
+
// Non-modal: the page can now scroll UNDER an open popover. A scroll of the
|
|
511
|
+
// page or any ancestor is a "moved on" signal, so we DISMISS (as native
|
|
512
|
+
// <select> and most menu systems do) rather than chase the trigger and risk a
|
|
513
|
+
// panel detached from an off-screen anchor. Scrolling the popover's OWN content
|
|
514
|
+
// (a long option list) must NOT close it. Capture catches nested scrollers.
|
|
515
|
+
const onScroll = (e: Event) => {
|
|
516
|
+
if (e.target instanceof Node && popoverRef.current?.contains(e.target)) return;
|
|
517
|
+
onOpenChange(false);
|
|
518
|
+
};
|
|
519
|
+
window.addEventListener("scroll", onScroll, true);
|
|
520
|
+
// A window resize just re-anchors (the layout moved, the intent didn't).
|
|
521
|
+
window.addEventListener("resize", calculatePosition);
|
|
522
|
+
|
|
479
523
|
return () => {
|
|
480
524
|
cancelAnimationFrame(frame);
|
|
481
525
|
resizeObserver?.disconnect();
|
|
526
|
+
window.removeEventListener("scroll", onScroll, true);
|
|
527
|
+
window.removeEventListener("resize", calculatePosition);
|
|
482
528
|
};
|
|
483
|
-
}, [open, calculatePosition, small]);
|
|
529
|
+
}, [open, calculatePosition, small, onOpenChange]);
|
|
484
530
|
|
|
485
531
|
const handleOverlayClick = useCallback(
|
|
486
532
|
(e: React.MouseEvent) => {
|
|
@@ -501,27 +547,32 @@ export function PopoverContent(props: PopoverContentProps) {
|
|
|
501
547
|
|
|
502
548
|
return (
|
|
503
549
|
<Portal>
|
|
504
|
-
{/*
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
550
|
+
{/* Modal scrim — ONLY for the bottom-sheet (`small`) mode, which IS modal.
|
|
551
|
+
The anchored popover is NON-MODAL: no overlay, so the rest of the page
|
|
552
|
+
stays interactive; outside-dismiss is the click listener above. */}
|
|
553
|
+
{small && (
|
|
554
|
+
<div
|
|
555
|
+
style={{
|
|
556
|
+
position: "fixed",
|
|
557
|
+
top: 0,
|
|
558
|
+
left: 0,
|
|
559
|
+
right: 0,
|
|
560
|
+
bottom: 0,
|
|
561
|
+
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
562
|
+
opacity: isBottomSheetShown ? 1 : 0,
|
|
563
|
+
transition: "opacity 0.3s ease",
|
|
564
|
+
zIndex: overlayZIndex,
|
|
565
|
+
pointerEvents: "auto",
|
|
566
|
+
}}
|
|
567
|
+
onClick={handleOverlayClick}
|
|
568
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
569
|
+
/>
|
|
570
|
+
)}
|
|
521
571
|
{/* Popover */}
|
|
522
572
|
<div
|
|
523
573
|
ref={popoverRef}
|
|
524
574
|
data-popover="true"
|
|
575
|
+
data-popover-level={nestingLevel}
|
|
525
576
|
data-testid={testID}
|
|
526
577
|
role="dialog"
|
|
527
578
|
aria-modal={small ? true : undefined}
|