@lotics/ui 46.3.0 → 46.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.
- package/AGENTS.md +14 -1
- package/docs/catalog.md +118 -6
- package/docs/composition.md +26 -0
- package/docs/data_entry.md +56 -2
- package/docs/reviewing.md +34 -0
- package/docs/templates.md +90 -29
- package/examples/tpl_board.tsx +257 -0
- package/examples/tpl_money.tsx +1027 -0
- package/package.json +261 -259
- package/src/accordion.tsx +7 -1
- package/src/board.tsx +611 -0
- package/src/card.tsx +7 -1
- package/src/charge_lines.tsx +373 -0
- package/src/chip_group.tsx +57 -1
- package/src/file_row.tsx +98 -5
- package/src/icon.tsx +6 -0
- package/src/inline_edit.tsx +54 -10
- package/src/inline_number_input.tsx +5 -1
- package/src/inline_text_input.tsx +1 -1
- package/src/locale.tsx +26 -1
- package/src/matrix.tsx +23 -8
- package/src/reference_field.tsx +36 -13
- package/src/table.tsx +6 -1
- package/src/tabs.tsx +1 -1
- package/examples/tpl_report.tsx +0 -410
- package/examples/tpl_statements.tsx +0 -221
package/AGENTS.md
CHANGED
|
@@ -18,7 +18,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
18
18
|
| [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, choosing a CHOICE control by option count, find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (`InlineFiles` for a record ROW, the `FilesEditor` COMPOUND for a whole section, plus the three-way file INTAKE), stage gates, and the commit-on-blur vs action-press ordering law. |
|
|
19
19
|
| [docs/ai_patterns.md](./docs/ai_patterns.md) | AI acts, the human stays in charge — composer, live run feed (`AgentRun`), the one law's split — it turns on WHO supplied the values; findings, provenance, confidence; **after the run** — a stored record that fills up from several writers; the whole run in a dialog, **stopping**; **review surfaces compose from atoms** — `DiffValue`, `DiffMark`, `useChangeSet`. |
|
|
20
20
|
| [docs/composition.md](./docs/composition.md) | The design-language contract — **the form comes before the treatment** (name what the subject IS before reaching for parts), canvas + content column, heading altitude, register vs inset rows, the button ladder, master-detail `Drawer`, the register's rhythm, craft and TRIAGE bands, where the accent goes, theming as an app-level ESCAPE HATCH, color discipline, typography, **one fact one surface** (a chooser over the thing it chooses is a SELECTOR, not a second card grid; a control that only swaps one number for another is replaced by showing both), and where a commit sits. |
|
|
21
|
-
| [docs/reviewing.md](./docs/reviewing.md) | **Reviewing a screen you built** — the other docs say what good looks like; this one says how to find out whether you achieved it. GATES that outrank every treatment question, then 11 probes each naming what to COLLECT and the SIGNATURE in the numbers. Plus the extraction snippets, what measurement cannot see, and how to act on a finding. |
|
|
21
|
+
| [docs/reviewing.md](./docs/reviewing.md) | **Reviewing a screen you built — and the answer when one "looks bad"** — the other docs say what good looks like; this one says how to find out whether you achieved it. Run it BEFORE the deploy: a typecheck, a lint, a test and `app check` are all blind to how a screen reads. GATES that outrank every treatment question, then 11 probes each naming what to COLLECT and the SIGNATURE in the numbers. Plus the extraction snippets, what measurement cannot see, and how to act on a finding. |
|
|
22
22
|
| [docs/testing.md](./docs/testing.md) | Driving the kit in a browser — the three anatomies where the a11y tree says one thing and a driver must do another: a `PressDoor` row whose named button always intercepts pointer events (by design), portalled overlays that render at the top of the DOM, and custom pointer drag that `dragTo` cannot move. |
|
|
23
23
|
| [docs/templates.md](./docs/templates.md) | The map of `examples/tpl_*.tsx` — what shape each template solves and which to start from (copy + adapt, never import) — plus the record-surface composition rules (pipeline order, static shape, decision budget) and the ACTIVITY shape — a communications feed where the row's label is the GIST and the body varies by medium, one anatomy rather than a row type per kind. |
|
|
24
24
|
|
|
@@ -114,6 +114,19 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
114
114
|
(the tab stop, name, focus ring), never `PressableHighlight`, which wraps its children in the
|
|
115
115
|
button. `Table`/`TableRow` already does this.
|
|
116
116
|
→ [composition.md §"Row actions are always-visible siblings"](./docs/composition.md).
|
|
117
|
+
- **Piles you MOVE records between are a `Board`; bands of rows are not.** `Board` +
|
|
118
|
+
`BoardColumn` + `BoardCard` (`@lotics/ui/board`) — columns are one field's values, and a
|
|
119
|
+
card's `moves` drive a KEYBOARD menu first, with pointer drag layered on the same list.
|
|
120
|
+
`tpl_task_board` is a `DataGrid` of ROWS despite its name; a view built on it and labelled
|
|
121
|
+
Kanban is mislabelled.
|
|
122
|
+
→ [catalog.md §"Records in PILES you move them between"](./docs/catalog.md),
|
|
123
|
+
[templates.md §"`Board` vs `tpl_task_board`"](./docs/templates.md).
|
|
124
|
+
- **Money you are PRICING is `ChargeLines`; money you are READING is `Ledger`.** A charge is ONE
|
|
125
|
+
line — `quantity × unitPrice` reading across it and ENDING at the amount, never a derived figure
|
|
126
|
+
hung underneath — and the band's total lands on that same amount column. Below its fork width
|
|
127
|
+
the row takes two lines and the arithmetic RIGHT-ALIGNS to the band, so the amounts keep the
|
|
128
|
+
total's edge. → [catalog.md §"Money you are PRICING"](./docs/catalog.md),
|
|
129
|
+
[templates.md §"`tpl_money`"](./docs/templates.md).
|
|
117
130
|
- **A fact every row needs is a COLUMN; a fact one row needs expands in place.** `TableRow`'s
|
|
118
131
|
`detail` + `expanded` reveal the detail beneath the row; keep the drawer for a heavy form.
|
|
119
132
|
→ [composition.md §"No dead rows"](./docs/composition.md),
|
package/docs/catalog.md
CHANGED
|
@@ -151,8 +151,8 @@ reader must read WHOLE rather than recognise at a glance: a payment term, an add
|
|
|
151
151
|
Above 1 the resting box RESERVES that many lines — the full editor height, not the height this
|
|
152
152
|
particular value needs — so clicking in moves neither the field nor anything below it; Enter
|
|
153
153
|
then inserts a newline and the value commits on blur.
|
|
154
|
-
**`autoGrow`** makes that budget a MINIMUM and fits the value instead
|
|
155
|
-
[data_entry.md §Open prose grows](./data_entry.md). **`link`** marks a URL value with
|
|
154
|
+
**`autoGrow`** makes that budget a MINIMUM and fits the value instead, and a NOTE field always
|
|
155
|
+
takes it — see [data_entry.md §Open prose grows](./data_entry.md). **`link`** marks a URL value with
|
|
156
156
|
the navigation ink as a TREATMENT on the same single element — never a swapped-in
|
|
157
157
|
`TextLink`, which would reintroduce the sub-pixel jump this field stays one
|
|
158
158
|
`<input>` to avoid; pressing still edits, so pair it with an `Open` `InlineButton`
|
|
@@ -182,6 +182,33 @@ field rows lives in
|
|
|
182
182
|
`DetailTable` + `DetailRow`; the record's identity band is `RecordSummary`; its money
|
|
183
183
|
summary is `Ledger`.
|
|
184
184
|
|
|
185
|
+
### Money you are PRICING — the charge band
|
|
186
|
+
|
|
187
|
+
`ChargeLines` + `ChargeLine` (`@lotics/ui/charge_lines`) is the WRITE side of a record's money:
|
|
188
|
+
what it charges, priced per line, closed by its own total. `Ledger` is the READ side of the same
|
|
189
|
+
subject and the two are not interchangeable — a ledger has nothing to type into, and this has no
|
|
190
|
+
notion of adjustments or of three sides.
|
|
191
|
+
|
|
192
|
+
- **A charge is ONE line.** The arithmetic reads across it — `quantity × unitPrice`, ending at the
|
|
193
|
+
amount. Hanging the derived figure on a second line doubles every row's height and leaves the
|
|
194
|
+
answer in no column, so the total below closes nothing.
|
|
195
|
+
- **One money column.** Quantity, unit price and amount all right-align and the total lands on the
|
|
196
|
+
amount column, so the band adds up by eye. The typed figures right-align through
|
|
197
|
+
`InlineNumberInput`'s `align`; without it a typed price and a derived one drift apart, which is
|
|
198
|
+
probe 6 in [reviewing.md](./reviewing.md).
|
|
199
|
+
- **The total is the sum of what is on screen.** Never a figure computed off a different array: a
|
|
200
|
+
total that is not the sum of something visible is the one figure a reader cannot check.
|
|
201
|
+
- **The row's verb sits in a fixed slot**, so the money column does not shift between a row that
|
|
202
|
+
can be removed and one that cannot. `locked` turns the editors into values for a settled charge.
|
|
203
|
+
- **Below its fork width** the name takes its own line and the arithmetic sits under it — a
|
|
204
|
+
two-line row ON PURPOSE, measured on the band's own container because it lives in drawers as
|
|
205
|
+
often as on pages. The arithmetic RIGHT-ALIGNS to the band there: laid out from the left it
|
|
206
|
+
would land the amounts wherever the fixed slot widths happen to end, which coincides with the
|
|
207
|
+
closing total only when the container is exactly as wide as the row. The row's money verb moves
|
|
208
|
+
up to the name line in the fork, because the arithmetic line has no width to reserve for it.
|
|
209
|
+
- **The field names are locale-wired** — `LoticsLocale.chargeLines` gives the quantity, unit-price
|
|
210
|
+
and amount editors their screen-reader names, each qualified by the charge it belongs to.
|
|
211
|
+
|
|
185
212
|
### Tasks, to-dos, phased work — ONE compound, two axes
|
|
186
213
|
|
|
187
214
|
Anything a reader marks is `checklist` — `Checklist` + `ChecklistGroup` + `ChecklistItem`. Two
|
|
@@ -217,7 +244,9 @@ same record, so it follows automatically; for a surface whose SUBJECT is tasks s
|
|
|
217
244
|
|
|
218
245
|
### Tabular data — pick by SCALE + intent
|
|
219
246
|
|
|
220
|
-
Two columnar shapes, and the choice is about data size
|
|
247
|
+
Two columnar shapes, and the choice is about data size. (A third shape divides the same records
|
|
248
|
+
into **piles you move them between** — that is the board, below, and it is picked by INTENT, not
|
|
249
|
+
by size.)
|
|
221
250
|
|
|
222
251
|
- **High-volume register** (thousands+ you BROWSE) — `Table` (columns defined once; sortable
|
|
223
252
|
headers via `SortHeader`; paired with `Pagination`) + rows that either open a `Drawer` to
|
|
@@ -253,6 +282,56 @@ Two columnar shapes, and the choice is about data size:
|
|
|
253
282
|
grouping + pagination/infinite don't compose; use the register instead. Worked example:
|
|
254
283
|
[`tpl_task_board`](../examples/tpl_task_board.tsx) (a `CheckCircle` leading).
|
|
255
284
|
|
|
285
|
+
### Records in PILES you move them between — the column board
|
|
286
|
+
|
|
287
|
+
`Board` + `BoardColumn` + `BoardCard` (`@lotics/ui/board`). Columns are the values of ONE
|
|
288
|
+
field; cards are records; the act the shape exists for is sending a card to another column.
|
|
289
|
+
Reach for it when the reader ADVANCES work and the pile sizes are themselves the report — not
|
|
290
|
+
when they compare a row's measures (`DataGrid`) or browse thousands (`Table`).
|
|
291
|
+
|
|
292
|
+
Worked example: [`tpl_board`](../examples/tpl_board.tsx).
|
|
293
|
+
|
|
294
|
+
- **Order WITHIN a column is deliberately not offered.** The board moves a card BETWEEN piles;
|
|
295
|
+
it does not let you drag one above another in the same pile. Ordering that survives a reload
|
|
296
|
+
needs a persisted rank field on the record, and without one the reader arranges a pile, leaves,
|
|
297
|
+
and finds it rearranged — which is worse than never offering it. A pile is sorted by a column
|
|
298
|
+
the data already carries (a deadline, a priority), stated once. An earlier implementation shipped
|
|
299
|
+
an insert-at-index affordance with no rank behind it; it is gone.
|
|
300
|
+
|
|
301
|
+
- **A column states its identity and its SIZE separately.** `heading` is a node — the component
|
|
302
|
+
the value's data role owns (`OptionBadge` for a select, `MemberChip` for a person) — and
|
|
303
|
+
`count` is a PROP. Formatted into the heading the number becomes part of the column's NAME.
|
|
304
|
+
- **The move is keyboard-first.** A card's `moves` (`{ key, label }[]`) name its destinations,
|
|
305
|
+
and the card renders a focusable control whose menu items are those labels. Pointer drag is
|
|
306
|
+
layered on the SAME list: the grip drags, the drop resolves to the column under the pointer,
|
|
307
|
+
and a destination the menu would refuse is refused there too. Omit `moves` and the card
|
|
308
|
+
carries no move control — the honest rendering of a card that cannot go anywhere.
|
|
309
|
+
- **The board owns its horizontal scroll**, so the page body never scrolls sideways and the
|
|
310
|
+
shape survives 375. **Columns grow and the page scrolls** — a bounded board height is a number
|
|
311
|
+
nobody can derive, which is how `height: 560` gets hand-picked into an app.
|
|
312
|
+
- **A card's controls sit in the CARD's top-right corner, and they are always visible.** Pinned
|
|
313
|
+
to the card rather than carried by the title row, because a title that wraps to two lines
|
|
314
|
+
otherwise pushes them down and a control that moves with the length of the text beside it is one
|
|
315
|
+
the reader hunts for on every card. Always visible rather than revealed on hover: hover-reveal is
|
|
316
|
+
the exception a DENSE register earns, where forty controls at rest drown the rows — a board is
|
|
317
|
+
already sparse, and hiding the one act the shape exists for leaves a pointer user with a card
|
|
318
|
+
that looks inert. (Revealing on focus only is the same bug wearing a keyboard: the pointer, which
|
|
319
|
+
is how most people drive a board, never triggers it.)
|
|
320
|
+
- **The card face states the record; the MOVE MENU states the destinations.** A badge on the card
|
|
321
|
+
repeating the column it already sits in prints one fact twice. The same badge in the menu is new
|
|
322
|
+
information — it names where the card would GO, in that column's own vocabulary — so the menu
|
|
323
|
+
takes each destination's heading node and cannot drift from the pile it points at.
|
|
324
|
+
- **A card darkens under the pointer and its geometry does not move.** The wash is the card's own,
|
|
325
|
+
one neutral step; a border appearing on hover adds an edge to a surface already full of them.
|
|
326
|
+
- **A drop must not open the record.** The browser ends a drag with a press on the row the grip
|
|
327
|
+
sits in, and that row is a door — so without a guard every drag dumps the reader into a detail
|
|
328
|
+
surface they never asked for. Suppress the press for the tick after a drop. The case that
|
|
329
|
+
reproduces it is a SHORT drag, where the release lands back inside the card it started in.
|
|
330
|
+
- `BOARD_COLUMN_WIDTH` is the kit's column width. Do not re-pick it per app.
|
|
331
|
+
- Not the same thing as [`tpl_task_board`](../examples/tpl_task_board.tsx), whose name says
|
|
332
|
+
columns and whose body is a `DataGrid` of ROWS — see
|
|
333
|
+
[templates.md §"`Board` vs `tpl_task_board`"](./templates.md).
|
|
334
|
+
|
|
256
335
|
### Numbers & charts
|
|
257
336
|
|
|
258
337
|
`KPIStrip` (the dashboard stat band), `SummaryLine` (the light inline register/list summary
|
|
@@ -1154,7 +1233,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1154
1233
|
a list too small for its own row.
|
|
1155
1234
|
Pass `actions` UNCONDITIONALLY and `disabled` the verb when it has nothing to act on: a slot
|
|
1156
1235
|
that appears once the value is non-empty resizes the field as the user types.
|
|
1157
|
-
- **`inline_text_input`**, **`inline_number_input`** (`format` for currency/units
|
|
1236
|
+
- **`inline_text_input`**, **`inline_number_input`** (`format` for currency/units, `align="right"`
|
|
1237
|
+
to put the RESTING figure on its slot's far edge so a typed money column shares one edge —
|
|
1238
|
+
data_entry.md §Billing),
|
|
1158
1239
|
**`inline_select`**, **`inline_member_select`**, **`inline_date_picker`**
|
|
1159
1240
|
(`format="datetime"`, `optionalTime`; keyboard focus opens the TYPED segmented `DateField`
|
|
1160
1241
|
— locale field order, separator advances, Alt+ArrowDown floats the calendar; click still
|
|
@@ -1302,6 +1383,20 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1302
1383
|
line box, so the band is the same height either way and a loading record's header does not
|
|
1303
1384
|
move when the number lands. Still ONE number: a node is a stand-in for it, not a licence to
|
|
1304
1385
|
put a control in the slot.
|
|
1386
|
+
- **`charge_lines`** — `ChargeLines` + `ChargeLine`: the editable money band — what a record
|
|
1387
|
+
CHARGES, priced line by line, closed by its own total. The write side of `Ledger`'s subject,
|
|
1388
|
+
and not interchangeable with it: a ledger has nothing to type into and this has no notion of
|
|
1389
|
+
adjustments. A charge is ONE line — `quantity`/`unitPrice` derive the amount and the amount is
|
|
1390
|
+
never typed; leave both out and the `amount` itself becomes the editor (`amountActions` puts a
|
|
1391
|
+
list price one tap away, `unitPriceActions` a rate). Verbs sit BESIDE the field, never inside
|
|
1392
|
+
it, and the row's own verb (`action`, usually removal) sits in a slot held open on every row —
|
|
1393
|
+
both so the money column cannot move between rows. `extra` carries a second control belonging
|
|
1394
|
+
to the charge (how it was paid); `warning` names a problem on the line that has it; `locked`
|
|
1395
|
+
turns the editors into values for a settled band; `empty` speaks for a record nobody has priced
|
|
1396
|
+
yet. `formatMoney` is the BAND's, so two lines cannot disagree about a currency, and the `total`
|
|
1397
|
+
you pass must be the sum of the lines on screen. Below its fork width the row takes two lines
|
|
1398
|
+
with the arithmetic right-aligned to the band — see § "Money you are PRICING" for the full
|
|
1399
|
+
rules. Worked example: [`tpl_money`](../examples/tpl_money.tsx).
|
|
1305
1400
|
- **`ledger`** — `Ledger` + `LedgerGroup` + `LedgerBasis` + `LedgerRow` + `LedgerTotal` — the
|
|
1306
1401
|
record-level money list: every figure on ONE right-aligned tabular column, `peek` turns a row
|
|
1307
1402
|
into a pressable door floating its particulars in an anchored popover (put links INSIDE the
|
|
@@ -1514,6 +1609,13 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1514
1609
|
- **`data_grid`** — `DataGrid` + `gridRowStyle`: the inline-managed grouped table (see
|
|
1515
1610
|
[Tabular data](#tabular-data--pick-by-scale--intent)); `labels` localizes the sort-header
|
|
1516
1611
|
a11y via `SortHeaderLabels`.
|
|
1612
|
+
- **`board`** — `Board` + `BoardColumn` + `BoardCard` + `BoardMove` + `BOARD_COLUMN_WIDTH`:
|
|
1613
|
+
the column board — piles you MOVE records between (see
|
|
1614
|
+
[Records in PILES](#records-in-piles-you-move-them-between--the-column-board)). The board
|
|
1615
|
+
owns the horizontal scroll; a column takes a `heading` node, a `columnKey` and a `count`
|
|
1616
|
+
prop; a card takes `moves` + `onMove`, which drive the keyboard menu AND the pointer drop.
|
|
1617
|
+
Strings resolve through the `board` locale slice (`open` / `move` / `moveTo` / `empty`),
|
|
1618
|
+
overridable per card with `openLabel` / `moveLabel` and per column with `emptyMessage`.
|
|
1517
1619
|
- **`pagination`** — `Pagination` (+ `PaginationLabels`): the register's pager — a range and two
|
|
1518
1620
|
arrows, no page index. ONE per register, ABOVE the rows on the `SummaryLine` row; its range is
|
|
1519
1621
|
the register's count (pair with `Table` `counted`), and `onPageChange` owes a scroll reset the
|
|
@@ -1857,9 +1959,15 @@ component rather than showing it at zero.
|
|
|
1857
1959
|
cells, pressable, the value IN the cell) + `Matrix.Totals` (row + column + grand) +
|
|
1858
1960
|
`Matrix.Legend`. Pick over `Heatmap` when the NUMBER and totals matter. Figures are
|
|
1859
1961
|
RIGHT-aligned — they are `tabular`, and centring them defeats the aligned column — and row
|
|
1860
|
-
labels carry default ink, because a row's label is its identity, not chrome.
|
|
1962
|
+
labels carry default ink, because a row's label is its identity, not chrome. **The totals column
|
|
1963
|
+
takes one flex share like a data column**, because a total is the longest figure in its row: on a
|
|
1964
|
+
fixed width it is the FIRST thing to clip, and a clamped figure is whole in the DOM, so nothing
|
|
1965
|
+
but a rendered screen sees it. `MATRIX_COL_GAP` is
|
|
1861
1966
|
exported for a caller budgeting
|
|
1862
|
-
its own column widths.
|
|
1967
|
+
its own column widths. **A money matrix states its unit once** — in the card's
|
|
1968
|
+
`CardHeaderMeta`, cells bare (`formatCompactNumber`, not `formatMoney`), per
|
|
1969
|
+
[composition.md](./composition.md) §"A column states its unit ONCE": one currency glyph per cell
|
|
1970
|
+
is the same word fifty times and it is what pushes the totals column past its share.
|
|
1863
1971
|
- **`matrix_totals`** — the data layer for `Matrix`: `matrixTotals` (the cross-tab
|
|
1864
1972
|
aggregation) + `MatrixAxisItem` / `MatrixCellRef` / `MatrixTotalsResult`; React-free, so a
|
|
1865
1973
|
KPI can be driven off the same numbers the grid shows.
|
|
@@ -2070,6 +2178,10 @@ component rather than showing it at zero.
|
|
|
2070
2178
|
NODE** as well as a string, because a second line can itself be carrying a change (a file
|
|
2071
2179
|
being RECLASSIFIED is the same bytes filed differently — a `DiffValue` on the property); a
|
|
2072
2180
|
node renders outside the muted single-line `Text`, which would clip it and fight its colour.
|
|
2181
|
+
It also takes the **field annotations** (`description` / `warning` / `error`) — an expected
|
|
2182
|
+
document is a value the reader supplies, so a row whose document FAILS says so on the row, in
|
|
2183
|
+
the same block `DetailRow` renders; without it a fault could only be demoted into a callout
|
|
2184
|
+
about the whole section, which says something different.
|
|
2073
2185
|
Both `sm` variants share ONE beat (48px min + 6px vertical padding).
|
|
2074
2186
|
- **`file_rows`** — `FileRows`: batteries-included file list — row press → built-in gallery
|
|
2075
2187
|
+ a ⋯ Download/Open-external/Remove menu; composes `FileRow` + `ActionMenu` +
|
package/docs/composition.md
CHANGED
|
@@ -8,6 +8,24 @@ full worked-example screens ship as source in `examples/tpl_*.tsx` (picked by jo
|
|
|
8
8
|
[templates](./templates.md)). Every rule here is enforced by the primitives' defaults; fighting
|
|
9
9
|
them produces off-system UI.
|
|
10
10
|
|
|
11
|
+
**Arrived here because a screen LOOKS wrong?** The visual law sits in the last fifth of this file,
|
|
12
|
+
behind the structural law — so the sections that answer that complaint are listed here rather than
|
|
13
|
+
found by reading forward:
|
|
14
|
+
|
|
15
|
+
| The report | The section |
|
|
16
|
+
|---|---|
|
|
17
|
+
| "it looks like a spreadsheet" | [The register's own craft](#the-registers-own-craft--what-it-looks-like-a-spreadsheet-actually-is) |
|
|
18
|
+
| "it looks plain / flat / unfinished" | [Identity marks](#identity-marks--a-mark-that-is-the-same-on-every-row-carries-nothing) — a register whose rows open with text starts flat, and the symptom is reported as anything but the cause |
|
|
19
|
+
| "it's bland" after every defect is fixed | [Character comes from the DATA](#character-comes-from-the-data--the-answer-when-a-screen-reads-bland) |
|
|
20
|
+
| colour, accent, type, density | [Color discipline](#color-discipline--solid--tint--ramp--one-accent) · [Where the accent goes](#where-the-accent-goes--where-you-are-and-nothing-else) · [Typography](#typography) · [Touch & whitespace](#touch--whitespace) |
|
|
21
|
+
|
|
22
|
+
Those are the reports these sections were written to answer, in their own words — not the whole
|
|
23
|
+
visual law, which is the rest of the file.
|
|
24
|
+
|
|
25
|
+
**Reading them is not the same as checking them.** [reviewing.md](./reviewing.md) is how you find
|
|
26
|
+
out whether a screen you built actually holds these — render, look, measure — and it belongs
|
|
27
|
+
before a deploy rather than after a complaint.
|
|
28
|
+
|
|
11
29
|
## The form comes before the treatment
|
|
12
30
|
|
|
13
31
|
**If the domain already has a standard shape, that is the answer — build it.** A parcel's journey
|
|
@@ -841,6 +859,14 @@ surfaces consult `pressSelectedText()` instead, so a release that ENDED a select
|
|
|
841
859
|
open the record and throw that selection away. Only a real, non-empty selection suppresses: a
|
|
842
860
|
swallowed ordinary click would leave the row impossible to open.
|
|
843
861
|
|
|
862
|
+
**Every gesture that ends on a row owes the same guard.** Selection is one source; a DRAG that
|
|
863
|
+
ends inside the row it started in is another, and the next one will not be on this list. A row
|
|
864
|
+
carrying both a door and a gesture must suppress the press for that gesture's release — and the
|
|
865
|
+
release is what to watch, not the movement: a long drag ends far from where it began and never
|
|
866
|
+
reaches the door, so the case that reproduces the bug is the SHORT one. Suppress on the specific
|
|
867
|
+
gesture having just finished, never by disabling the press while a gesture is possible, which
|
|
868
|
+
takes the door away from every reader who was only ever going to click.
|
|
869
|
+
|
|
844
870
|
Selection covers every value at once, so reach for **`CopyButton`** only where one value is copied
|
|
845
871
|
often enough to earn a control (a phone number a rep dials, a reference quoted into a message) —
|
|
846
872
|
and put it ON that value inside its cell, never in the row's trailing column, which has nothing to
|
package/docs/data_entry.md
CHANGED
|
@@ -300,6 +300,13 @@ The box is drawn at the budget and the value simply exceeds it: no ellipsis, no
|
|
|
300
300
|
scrollbar, nothing to scroll. So the test is not "is this field long" but **"who decides the
|
|
301
301
|
length"** — the field, or whoever is typing.
|
|
302
302
|
|
|
303
|
+
**One field kind answers that question the same way every time: the NOTE.** A remark, a comment,
|
|
304
|
+
a "ghi chú", an internal note — whatever a surface calls the box a person types free prose into,
|
|
305
|
+
its length is the author's by definition, so it takes `autoGrow` without the test being re-run.
|
|
306
|
+
It is named here because the abstract test kept being answered wrong for it: a note reads as
|
|
307
|
+
short (most are one line), so it gets the default budget, and the one entry that actually
|
|
308
|
+
mattered — the sentence explaining the exception — is the one silently cut.
|
|
309
|
+
|
|
303
310
|
**And it gets worse as the surface narrows**: a budget is a count of LINES and the wrap point
|
|
304
311
|
moves, so the narrower column wraps the same value into more hidden lines. A fixed reserve
|
|
305
312
|
verified on a wide screen is not verified.
|
|
@@ -341,6 +348,17 @@ on top would make inline fields the one control in the kit with a second hover l
|
|
|
341
348
|
reader crossing a record surface would meet both. Open adds the 2px ring. A `disabled` editor rests FLAT and borderless
|
|
342
349
|
automatically — the surface is the editability promise, and an inert field must not make it.
|
|
343
350
|
|
|
351
|
+
**The surface is not the only promise, and dropping it alone makes things worse.** A select's
|
|
352
|
+
chevron and a date field's calendar glyph each say "something opens here"; once the frame and the
|
|
353
|
+
hover are gone, that glyph is the ONLY mark left calling the row a control, so a value you cannot
|
|
354
|
+
change reads as an editable one that ignores the click. A disabled field therefore renders no
|
|
355
|
+
trailing affordance either — what is left is plain text, which is what a read-only value is.
|
|
356
|
+
The same applies to the PLACEHOLDER: "Choose a…" on an inert field invites an act that cannot
|
|
357
|
+
happen, so a disabled empty field says what is true ("None recorded") instead of what to do.
|
|
358
|
+
Check it by disabling one field in a stack and looking down the column — every field that cannot
|
|
359
|
+
be edited should be indistinguishable from a static value, and one that still carries a glyph or
|
|
360
|
+
an inviting placeholder is the odd one out.
|
|
361
|
+
|
|
344
362
|
**`variant` decides how much of that frame shows AT REST — an axis of WEIGHT, not of use.**
|
|
345
363
|
`"framed"` (default) keeps the surface visible: required wherever editable and static values MIX,
|
|
346
364
|
because the frame is the only thing saying which values you can change. `"bare"` shows nothing at
|
|
@@ -423,6 +441,26 @@ surface look cluttered — not the number of rows.
|
|
|
423
441
|
| More options, or long labels, or the value is set once and read after | **`InlineSelect`** | scales to any count; costs two presses (open, pick) and hides the alternatives until you do — put each option's gloss in `renderOptionContent`, where it is needed while CHOOSING rather than on every later read |
|
|
424
442
|
| The choice IS the task — a wizard step, a short focused form | **`RadioPicker`** | full-width choices with their descriptions permanently visible. In a grid of many fields this is ~4 rows of height for one value; never use it as a row in a record's field table |
|
|
425
443
|
|
|
444
|
+
**A chip may carry an ICON, and it earns one only when the set differs in KIND.** Payment
|
|
445
|
+
methods, channels, document types — a glyph plus `iconColor` separates *the money is here* from
|
|
446
|
+
*it is not* before the labels are read. On a set that differs in DEGREE (Low / Medium / High) the
|
|
447
|
+
same glyphs are decoration every chip pays width for. Never icon-only: the label stays, because a
|
|
448
|
+
chip that is a bare glyph has a value nobody can read aloud.
|
|
449
|
+
|
|
450
|
+
**Appending a line to a list is create-then-refine too, even when the new line must pick its
|
|
451
|
+
KIND first.** A dialog that exists only to choose one of N and press Save is three presses for one
|
|
452
|
+
act, and it leaves the reader looking at a form instead of the list they were reading. Put the
|
|
453
|
+
choice ON the add control — a `Popover` of the kinds not already present — and let the chosen line
|
|
454
|
+
land in the list with its defaults, refined in place like any other row.
|
|
455
|
+
|
|
456
|
+
Where that control sits follows the list's own shape: **at the END of the rows when the list is
|
|
457
|
+
read in order** (a `DetailTable` of lines, where the next line is what comes after the last one and
|
|
458
|
+
the section heading carries the section's identity, not a per-list verb), and **on the section
|
|
459
|
+
heading when the list is a `Table`** (which already owns a header row and a toolbar for acts that
|
|
460
|
+
are about the whole set). One primary either way — if the section already has a committing act,
|
|
461
|
+
the add is secondary, because appending a rare line must not outrank the act the section exists
|
|
462
|
+
for.
|
|
463
|
+
|
|
426
464
|
**A choice that GATES the form is a fourth answer, and the answer is usually "don't ask".** The
|
|
427
465
|
table above is for a choice that IS a value. A choice about how to proceed — which sign-in method,
|
|
428
466
|
which import format, which template — is not a field at all, and putting it on its own screen in
|
|
@@ -730,8 +768,24 @@ line saying what's needed; the payment-method picker turns required the instant
|
|
|
730
768
|
amount. Issuing a real e-invoice is irreversible → confirm in a `Dialog` (stage gate). A
|
|
731
769
|
grand-total **receipt** validates first — surface the EXACT missing methods (`Alert.alert` listing
|
|
732
770
|
each) rather than a vague "incomplete." A refundable **deposit** is its own card and its own
|
|
733
|
-
receipt — never folded into the total due.
|
|
734
|
-
|
|
771
|
+
receipt — never folded into the total due. Where the band's lines are PRICED — a quantity against
|
|
772
|
+
a rate — that band is `ChargeLines` + `ChargeLine` (`@lotics/ui/charge_lines`), which owns the
|
|
773
|
+
one-line charge, the two money columns and the closing total; hand-rolling it is what produced the
|
|
774
|
+
defects listed in [catalog.md §"Money you are PRICING"](./catalog.md). Worked examples:
|
|
775
|
+
`tpl_money`, and `tpl_record`'s Billing section.
|
|
776
|
+
|
|
777
|
+
**An EDITABLE amount still owes the reader a money column.** An `Inline*` editor fills the value
|
|
778
|
+
slot and left-aligns its text, while the figures derived from it — a line's extended amount, the
|
|
779
|
+
total — right-align like money always does. Left alone that is three ink positions for three
|
|
780
|
+
numbers the reader is meant to add up, and no width fixes it because the two are aligned on
|
|
781
|
+
opposite edges. Give the editor a FIXED width and `align="right"` — the `Inline*` prop that moves
|
|
782
|
+
the RESTING value onto its slot's far edge, which is the half a width alone cannot buy: the
|
|
783
|
+
amounts you type form one column, the amounts the system computes form the next, and both close on
|
|
784
|
+
the same edge as the total beneath them. Measure it by collecting the right edge of every money
|
|
785
|
+
string on the surface — a correct one shows two values, not one per row. A one-tap verb beside the
|
|
786
|
+
figure (a standard rate, a list price) is a THIRD edge and not a defect; what it must never do is
|
|
787
|
+
sit INSIDE the editor in a money column, where it eats the field's width and slides the figure off
|
|
788
|
+
the column its neighbours are on. In a form row whose value column grows, on the field is fine.
|
|
735
789
|
|
|
736
790
|
## Fee summary — `Ledger`
|
|
737
791
|
|
package/docs/reviewing.md
CHANGED
|
@@ -225,6 +225,17 @@ spacing distinguishes them.
|
|
|
225
225
|
fixed neighbouring shade under the pointer. Watch for an opaque overlay hiding a themed ground.
|
|
226
226
|
- **A prop's EFFECT must match its claim** — render each value and diff what actually moved
|
|
227
227
|
against what the prop says it changes. The fix is in the component, never the caller.
|
|
228
|
+
- **A prop with NO effect is invisible to that diff**, because every value renders the same
|
|
229
|
+
screen. It typechecks, it lints, the caller reads correctly, the docs describe it — and it does
|
|
230
|
+
nothing, sometimes for releases. Read it off the SOURCE instead of the screen: a name that
|
|
231
|
+
appears in the props interface and in the destructure and **nowhere else in the render** is
|
|
232
|
+
dead. Wire it, then cover it with a test that goes red when the render line is deleted — a
|
|
233
|
+
component test asserting the passed node reaches the output, since nothing about the type system
|
|
234
|
+
will ever notice. **Three shapes look dead and are not**, so check them before reporting: a rest
|
|
235
|
+
spread (`const { a, ...inline } = props`) forwards a prop without ever naming it; a renamed
|
|
236
|
+
destructure (`slices: chartData`) uses it under the new name; and a platform fallback file
|
|
237
|
+
deliberately ignores a prop its `.web` twin honours. Sweep a component's WHOLE prop list at
|
|
238
|
+
once — a verb prop that shipped dead usually has a twin on the sibling field, dead the same way.
|
|
228
239
|
- **Measure a component against ITSELF under each optional prop** that should not change size — a
|
|
229
240
|
file row measured 37px static and 49px with an `onPress`, because only the pressable variant
|
|
230
241
|
carried the padding its wash needed. Same for a slot that may be empty.
|
|
@@ -240,6 +251,29 @@ spacing distinguishes them.
|
|
|
240
251
|
become double-corrections the moment the row is right. Two exceptions are legitimate: a
|
|
241
252
|
self-contained overlay has nothing to align with, and an indent something VISIBLE occupies (a
|
|
242
253
|
checkbox, an ordinal, a mark) is explained.
|
|
254
|
+
- **A number you can EDIT and a number you can only READ drift apart on a money column.** An
|
|
255
|
+
inline editor fills its slot and left-aligns; the figures derived from it right-align. Collect
|
|
256
|
+
the right edge of every money string on the surface — a sound one lands on TWO values (the typed
|
|
257
|
+
column, the computed one), a broken one lands on as many values as there are rows, and the
|
|
258
|
+
reader can no longer add the column up by eye. The band's TOTAL is part of the computed column
|
|
259
|
+
and must be counted; a one-tap verb beside a field (a standard rate) is a third edge and is not a
|
|
260
|
+
defect. **Re-run the probe at a width where the row FORKS to two lines** — a forked row is laid
|
|
261
|
+
out by different rules and can land its amounts on an edge the total is not on, which is the one
|
|
262
|
+
failure the wide measurement cannot see. Rule: data_entry.md §Billing.
|
|
263
|
+
- **A derived result must END the expression it derives from.** Where a row spells out a
|
|
264
|
+
calculation — a quantity, an operator, a rate — collect the right edge of the RESULT and of the
|
|
265
|
+
last operand. Equal is sound; anything else makes the eye read the expression left-to-right and
|
|
266
|
+
then jump backwards to find its answer, and putting the result in a label/description slot is
|
|
267
|
+
the worst version (it lands at the START of the line). The near-miss has one cause worth naming:
|
|
268
|
+
the result was positioned by a HAND-ADDED width, and one term of the sum — usually the operator
|
|
269
|
+
glyph, whose width the font decides — was guessed. Give that glyph an explicit width and build
|
|
270
|
+
both offsets from the same named constants, or the two edges agree only at the font size you
|
|
271
|
+
happened to screenshot.
|
|
272
|
+
- **A DISABLED control that still wears its affordance.** Query every field for
|
|
273
|
+
`aria-disabled="true"`, then ask what separates it from an editable neighbour. Frame gone but
|
|
274
|
+
chevron/calendar glyph still drawn, or an inviting placeholder ("Choose a…") on an inert field,
|
|
275
|
+
is a value that reads as editable and ignores the click — the complaint arrives as "I can't edit
|
|
276
|
+
this and I don't know why". Rule: data_entry.md §The editability affordance.
|
|
243
277
|
- **A CENTRED child hides its own drift.** Centring puts a child's top at `(container − child)/2`,
|
|
244
278
|
so a child whose size varies with DATA moves its own first line between instances while the
|
|
245
279
|
container measures identical every time. It takes a container with SLACK to bite. Wherever a
|
package/docs/templates.md
CHANGED
|
@@ -40,10 +40,11 @@ composition grammar](./composition.md); the package index is [../AGENTS.md](../A
|
|
|
40
40
|
| A guided sequence of physical tasks (scan, confirm, next) | `tpl_pick` |
|
|
41
41
|
| Splitting one source amount across many targets | `tpl_allocate` |
|
|
42
42
|
| A record's create/edit surface — also the settings shape | `tpl_record` |
|
|
43
|
-
| A surface whose SUBJECT is tasks
|
|
43
|
+
| A surface whose SUBJECT is tasks, managed in a grid of live cells | `tpl_task_board` |
|
|
44
|
+
| Advancing records between STAGES | `tpl_board` |
|
|
45
|
+
| Records as CARDS in columns, moved between them — the column board | `Board` (`@lotics/ui/board`) |
|
|
44
46
|
| One record HANDED between desks — stages that each own their controls | `tpl_record` (its Progress section) |
|
|
45
|
-
|
|
|
46
|
-
| A scoped lookup report with export | `tpl_report` |
|
|
47
|
+
| ONE record's money — what it charges, costs, bills and collects | `tpl_money` |
|
|
47
48
|
| A week calendar + agenda | `tpl_calendar` |
|
|
48
49
|
| An attendance desk | `tpl_attendance` |
|
|
49
50
|
| Shift signup + staffing | `tpl_shifts` |
|
|
@@ -387,6 +388,13 @@ The corollaries, each of which a register is routinely missing:
|
|
|
387
388
|
|
|
388
389
|
- **One toolbar row** — search + a status `Select` + facet `FilterChip`s LEFT, the New CTA
|
|
389
390
|
RIGHT; then a light `SummaryLine` of the filtered view BELOW it, above the rows.
|
|
391
|
+
**Search is ALWAYS the leftmost control**, and the order after it is stated rather than left to
|
|
392
|
+
the author: search, then the scope the rest of the row filters WITHIN (a site, an account, a
|
|
393
|
+
period), then the facets. Search is the one control every register carries and the one a reader
|
|
394
|
+
reaches for without looking, so it holds the same spot on every screen in the product; a scope
|
|
395
|
+
`Select` placed before it moves the anchor per screen and costs the reader a scan each time.
|
|
396
|
+
This is the register's counterpart to the add-placement law — the position is the affordance,
|
|
397
|
+
and an author choosing it per screen is the defect.
|
|
390
398
|
- **A sortable `Table`** — rows separated by their own height and the hover wash, with the one
|
|
391
399
|
hairline under the column band. It sets no row height: the register's rhythm is the kit's. →
|
|
392
400
|
[composition.md](./composition.md) §"The register's rhythm". The record's SUBJECT is the first
|
|
@@ -892,7 +900,32 @@ a REGISTER (`tpl_item_list`) with a derived stage column — the same ladder mod
|
|
|
892
900
|
so a row's badge and its filter bucket can never disagree. Rows that are structurally identical
|
|
893
901
|
and tickable in any order are a `tpl_task_board`, not a pipeline.
|
|
894
902
|
|
|
895
|
-
### `
|
|
903
|
+
### `tpl_board` — the column board
|
|
904
|
+
|
|
905
|
+
Columns are the STAGES of a pipeline, cards are records, and the only act is sending a card
|
|
906
|
+
onward. Reach for it when the question is *"what is stuck, and what moves next?"* — the pile sizes
|
|
907
|
+
are themselves the report, which is the thing a register at equal weight cannot say.
|
|
908
|
+
|
|
909
|
+
- **The move is KEYBOARD-first.** Each card carries a control whose items name their destinations;
|
|
910
|
+
pointer drag is layered on that same declared list, so the two paths cannot disagree about what
|
|
911
|
+
is legal. An act reachable only by drag is unreachable for a keyboard user and untestable by a
|
|
912
|
+
driver.
|
|
913
|
+
- **A column states its size as a PROP**, never formatted into its heading.
|
|
914
|
+
- **The BOARD owns the horizontal scroll.** The page never does.
|
|
915
|
+
- **A card is a DOOR** into the same record surface a register opens — the board is another way in,
|
|
916
|
+
never a second detail view.
|
|
917
|
+
|
|
918
|
+
**Not [`tpl_task_board`](../examples/tpl_task_board.tsx)**, which is a `DataGrid` of ROWS with an
|
|
919
|
+
inline editor in every cell: you ADVANCE work here and COMPARE-and-correct it there. The name is
|
|
920
|
+
the trap — read the source.
|
|
921
|
+
|
|
922
|
+
### `tpl_task_board` — the inline-managed task grid
|
|
923
|
+
|
|
924
|
+
**It is not a column board, and the name is the trap.** This template is a `DataGrid`:
|
|
925
|
+
collapsible BANDS OF ROWS with inline-editable cells. Nothing is arranged in columns of cards
|
|
926
|
+
and nothing is dragged between them. A view built on it and labelled *Kanban* is mislabelled —
|
|
927
|
+
for the column shape reach for **`Board`** (`@lotics/ui/board`), and read
|
|
928
|
+
§"`Board` vs `tpl_task_board`" below before choosing.
|
|
896
929
|
|
|
897
930
|
The manager's board: a search, group-by, filter toolbar over a grouped, sortable grid of
|
|
898
931
|
LIVE editable cells — assignee/due/status/tags ALL via `Inline*` editors in **`variant="bare"`**
|
|
@@ -923,31 +956,59 @@ only real dimensions, ✕ clears to ungrouped (no "Nothing" option).
|
|
|
923
956
|
manage in view (group/sort/filter/edit-in-place; it renders ALL rows). For thousands+ you
|
|
924
957
|
BROWSE — that's `tpl_item_list`'s paginated register, not this.
|
|
925
958
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
959
|
+
### `Board` vs `tpl_task_board` — the same data, two different jobs
|
|
960
|
+
|
|
961
|
+
Both divide one field's values into piles. They are not variants of each other, and the
|
|
962
|
+
discriminator is what the reader came to DO.
|
|
963
|
+
|
|
964
|
+
- **`Board`** (`@lotics/ui/board`) when the job is to ADVANCE work — the pile sizes are the
|
|
965
|
+
report, and moving a card from one pile to the next is the act the screen exists for. Columns
|
|
966
|
+
are the field's values, cards are records, and each card carries a `moves` list naming its
|
|
967
|
+
destinations. That list is the keyboard path (a menu) AND the pointer path (a drag drops onto
|
|
968
|
+
the column under the pointer and is refused unless the destination is in the same list), so the
|
|
969
|
+
two can never offer different answers. Compare across cards is what it is BAD at: only what
|
|
970
|
+
fits on a card is visible, and nothing lines up.
|
|
971
|
+
- **`tpl_task_board`** when the job is to COMPARE and EDIT — assignee against due date against
|
|
972
|
+
priority, twenty rows at a time, each cell an inline editor. Bands, not columns; the field is
|
|
973
|
+
changed in its own cell rather than by moving the row.
|
|
974
|
+
|
|
975
|
+
Grouped rows answer "what is in each pile" and stop there. **A view labelled Kanban over bands
|
|
976
|
+
of rows is the wrong FORM, not a smaller version of the right one** — the move is the whole
|
|
977
|
+
shape, and a grid hands it back as a cell edit two screens from the pile it belongs to.
|
|
978
|
+
|
|
979
|
+
## Money
|
|
980
|
+
|
|
981
|
+
### `tpl_money` — THE money surface for one record
|
|
982
|
+
|
|
983
|
+
Everything one record's money does, on one page, in the four shapes it takes — and the split that
|
|
984
|
+
governs it: **`ChargeLines` is what you are PRICING and `Ledger` is what you are READING**, a band
|
|
985
|
+
you type into versus a statement you check.
|
|
986
|
+
|
|
987
|
+
- **Fees** — the DETAILED view, both directions (charge / cost), each fee carrying its party, due
|
|
988
|
+
date, supplier original and paid state. A register whose every row EXPANDS in place to its full
|
|
989
|
+
nine-field detail, one at a time: a child collection inside a record never docks a drawer,
|
|
990
|
+
because the page already IS the context the drawer would recreate. The section's Add rides the
|
|
991
|
+
heading row, where it does not move with the row count.
|
|
992
|
+
- **Billing** — the invoice DOCUMENTS. Each invoice costs ONE line of chrome (its name, its
|
|
993
|
+
total, its reference once issued — the reference in a HELD-OPEN slot, or it drags the totals off
|
|
994
|
+
the column the statement below closes) over its own charge rows, with the issue act at the end
|
|
995
|
+
of what it commits. A problem rides the CHARGE ROW that has it, never a callout counting
|
|
996
|
+
offenders. The section closes on a three-sided `Ledger` — billed, less credited, less received —
|
|
997
|
+
and the refundable deposit, which is deliberately outside the total to collect.
|
|
998
|
+
- **The charge bands** — `ChargeLines` in every shape: priced lines (`quantity × unitPrice`, the
|
|
999
|
+
amount never typed), flat amounts (the amount IS the editor, with the payment method riding the
|
|
1000
|
+
charge's own line via `extra`), mixed kinds landing on one column, `locked` once collected, the
|
|
1001
|
+
empty band, and the narrow fork — the SAME band rendered in a 360 box, because the fork is
|
|
1002
|
+
measured on the band's container and not on the window.
|
|
1003
|
+
|
|
1004
|
+
**The law the page is checked against:** every band lands on TWO money edges — the typed column
|
|
1005
|
+
and the amount column, with the band's total in the amount column. A one-tap verb (a standard
|
|
1006
|
+
rate) is not a column figure and may sit elsewhere. Verify by measuring: collect the right edge of
|
|
1007
|
+
every money string per band at a wide and a narrow width (→
|
|
1008
|
+
[reviewing.md](./reviewing.md), and [data_entry.md §Billing](./data_entry.md)).
|
|
1009
|
+
|
|
1010
|
+
Its Fees and Billing sections are the same two sections `tpl_record` carries — read them there
|
|
1011
|
+
when you want them in the context of a whole record surface, and here when money is the subject.
|
|
951
1012
|
|
|
952
1013
|
### `tpl_calendar` — the week desk
|
|
953
1014
|
|