@lotics/ui 46.2.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 +38 -1
- package/MIGRATION.md +87 -0
- package/docs/ai_patterns.md +11 -0
- package/docs/catalog.md +217 -21
- package/docs/composition.md +74 -6
- package/docs/data_entry.md +56 -2
- package/docs/reviewing.md +34 -0
- package/docs/templates.md +93 -30
- package/docs/testing.md +6 -0
- package/examples/tpl_board.tsx +257 -0
- package/examples/tpl_money.tsx +1027 -0
- package/package.json +261 -258
- package/src/accordion.tsx +7 -1
- package/src/alert.css +0 -1
- package/src/alert.tsx +8 -0
- package/src/axis_label_indices.ts +84 -0
- package/src/bar_chart.tsx +137 -16
- 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/dialog.tsx +46 -24
- package/src/drawer.tsx +21 -2
- package/src/file_gallery_modal.tsx +3 -0
- 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/line_chart.tsx +2 -2
- package/src/locale.tsx +26 -1
- package/src/matrix.tsx +23 -8
- package/src/modal.tsx +23 -3
- package/src/overlay_layer.ts +65 -0
- package/src/page_content.tsx +8 -22
- package/src/page_header.tsx +60 -11
- package/src/popover.tsx +29 -5
- package/src/reference_field.tsx +36 -13
- package/src/skip_link.tsx +2 -1
- package/src/stacked_bar_chart.tsx +31 -1
- package/src/table.tsx +6 -1
- package/src/tabs.tsx +1 -1
- package/src/text.tsx +21 -0
- package/src/tooltip.tsx +2 -1
- package/src/use_change_set.ts +66 -17
- package/src/use_scroll_seam.ts +79 -0
- package/examples/tpl_report.tsx +0 -410
- package/examples/tpl_statements.tsx +0 -221
- package/src/line_chart_labels.ts +0 -32
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
|
|
@@ -69,6 +87,28 @@ the subject into rows.
|
|
|
69
87
|
the same left edge as the title above it and the actions below it. Never hand-pad inside one,
|
|
70
88
|
and never hand-roll one: the three surfaces disagree on the number (24 / responsive / 20) and
|
|
71
89
|
only the surface knows which it is.
|
|
90
|
+
- **ANY scroller whose body is SWAPPED opens the new content at the top.** Stated as a rule and
|
|
91
|
+
not as a count of the surfaces that happen to have one today: `ModalBody`,
|
|
92
|
+
`DialogScrollArea` and `DrawerScrollArea` each take a `scrollKey` — the content's identity (a
|
|
93
|
+
record id, a step name) — and the kit's routed `Popover` takes the same seam with no prop at
|
|
94
|
+
all, because its ROUTE already is that identity. A scroller you add that swaps its body joins
|
|
95
|
+
the rule rather than being added to a list. `scrollKey={openChild?.id}` is the right shape:
|
|
96
|
+
an ABSENT key names the root content, not an opt-out, so the offset comes back when the child
|
|
97
|
+
closes. Opting out needs no signal — a surface whose key never changes never scrolls. Without it the container keeps the offset it
|
|
98
|
+
was holding for content that no longer exists, so a swapped-in record opens part-way down
|
|
99
|
+
itself with its heading off-screen above — invisible until the first list long enough to
|
|
100
|
+
scroll, which is the same list that made the swap worth having. The key opens unseen content
|
|
101
|
+
at the top and restores the offset when it comes BACK, which is what a React `key` on the
|
|
102
|
+
scroll area cannot do: that rebuilds the container, resetting the parent's place along with
|
|
103
|
+
the child's. Omit it where the body is one thing, and inside a `Screen`-routed dialog, where
|
|
104
|
+
a stacked screen stays mounted and already keeps its own offset.
|
|
105
|
+
- **Overlays paint in the order they were OPENED, and no call site has to know it.** Mount them
|
|
106
|
+
wherever composition wants — a `Dialog` sitting always-mounted beside the `Drawer` whose row
|
|
107
|
+
opens it is the ordinary shape, and it is the one that used to fail: the dialog claimed its DOM
|
|
108
|
+
slot on the app's first paint, the drawer landed after it, and the dialog then rendered
|
|
109
|
+
perfectly and read correctly with every control under the panel DEAD. Whatever opens last is on
|
|
110
|
+
top, both ways round, so a drawer opened from a dialog covers it too. `Alert` and `Tooltip` sit
|
|
111
|
+
above every overlay at any depth — they are ABOUT the surface under them.
|
|
72
112
|
- **The responsive one is READ, never re-derived — `useDialogGutter()`, no argument.** Inside
|
|
73
113
|
a `Dialog` that is what the header, the scroll area, the footer and any pane a caller drops
|
|
74
114
|
in all call. The `Dialog` resolves `small ? 16 : 24` ONCE, off the SCREEN, and publishes it.
|
|
@@ -819,6 +859,14 @@ surfaces consult `pressSelectedText()` instead, so a release that ENDED a select
|
|
|
819
859
|
open the record and throw that selection away. Only a real, non-empty selection suppresses: a
|
|
820
860
|
swallowed ordinary click would leave the row impossible to open.
|
|
821
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
|
+
|
|
822
870
|
Selection covers every value at once, so reach for **`CopyButton`** only where one value is copied
|
|
823
871
|
often enough to earn a control (a phone number a rep dials, a reference quoted into a message) —
|
|
824
872
|
and put it ON that value inside its cell, never in the row's trailing column, which has nothing to
|
|
@@ -829,7 +877,11 @@ unconditionally and let an empty `value` disable it, rather than revealing it on
|
|
|
829
877
|
|
|
830
878
|
Pressing a PRIMARY entity row opens the record workspace in a `Drawer` with `onPrev`/`onNext`/
|
|
831
879
|
`position` ("3/24") over the visible ordering (←/→ arrow keys are built in). Key the drawer body by
|
|
832
|
-
record id so per-record state resets on step.
|
|
880
|
+
record id so per-record state resets on step. A drawer that instead REPLACES its body with a child
|
|
881
|
+
record — the shape where a row inside the panel opens a second record rather than stacking a
|
|
882
|
+
second drawer — passes that id as `DrawerScrollArea`'s `scrollKey` (`scrollKey={openChild?.id}` —
|
|
883
|
+
the undefined leg is the list's own identity), so the child opens at the top and the list keeps its
|
|
884
|
+
place on the way back. Facts are `DetailRow`s; the commit bar is a
|
|
833
885
|
`DrawerFooter` (a hairline-topped band pinned to the panel bottom — render it as the LAST child,
|
|
834
886
|
after the scroll body; actions sit right, a leading `<Text style={{ flex: 1 }}>` hint pushes them
|
|
835
887
|
there). The open row shows a `selected` highlight (and `marked` for a bulk-ticked row). `Peek` is
|
|
@@ -1318,6 +1370,15 @@ colours, same shapes. Re-rendering it as plain text makes the reader translate b
|
|
|
1318
1370
|
presentations of one field, and the two drift the first time either side gains a value. This holds
|
|
1319
1371
|
for every custom-rendered value, not just status.
|
|
1320
1372
|
|
|
1373
|
+
**The same rule reaches the CHART beside the table.** One entity, one rendering, everywhere inside
|
|
1374
|
+
one card: if the table's first cell carries the entity's mark (`BrandMark`, `Avatar`, a status
|
|
1375
|
+
dot), the chart's rows carry it too — `StackedBarRow` and `BarChartItem` each take a `leading`
|
|
1376
|
+
slot for exactly that, laid out on the label's own line box. It is worst where the mark carries
|
|
1377
|
+
most (a channel, a platform, a person), and the two ways out that are not the slot are both
|
|
1378
|
+
losses: dropping the mark from the table to match makes *both* halves anonymous, and hand-drawing
|
|
1379
|
+
the chart to get one slot loses the component. A LEGEND row is the exception and needs no slot —
|
|
1380
|
+
its subject is a category, and the colour swatch already is that category's identity.
|
|
1381
|
+
|
|
1321
1382
|
**Group by what implies a different ACTION, never by a category the reader can already see.**
|
|
1322
1383
|
Bands like *Waiting on you* / *Gone quiet* / *Open* / *Closed* each name a different response, so
|
|
1323
1384
|
the grouping tells the reader something no column does. Grouping by a value that is already a
|
|
@@ -1780,13 +1841,20 @@ complaint it was meant to fix, because a reader who cannot read a label reports
|
|
|
1780
1841
|
cluttered rather than as small. Recede with INK, at the same rung. `xs` is for genuine meta (a
|
|
1781
1842
|
timestamp, a count), not for a label that a person actually has to read.
|
|
1782
1843
|
|
|
1783
|
-
**A LABEL sharing a row with a flexible VALUE must be told which one
|
|
1844
|
+
**A LABEL sharing a row with a flexible VALUE must be told which one HOLDS.** Two items in one
|
|
1784
1845
|
flex row, and whichever cannot shrink forces the other to. It is invisible wherever the values
|
|
1785
1846
|
happen to fit, which is usually the width it was built at, and it appears the first time a real
|
|
1786
|
-
record carries a long one.
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1847
|
+
record carries a long one. Say which one stays whole — `flexShrink: 0` on a fixed-length key, on a
|
|
1848
|
+
figure, on a control — and pick by which is still USEFUL truncated: `RC-2026-0…` is unusable,
|
|
1849
|
+
`(555) 384-7…` is still recognisably a phone number.
|
|
1850
|
+
|
|
1851
|
+
The clipping half needs only **`numberOfLines`**, which now brings its own `minWidth: 0` +
|
|
1852
|
+
`flexShrink: 1`: a clamp that cannot shrink cannot clamp, so the prop that says "cut this to fit"
|
|
1853
|
+
is what makes it possible. (Neither platform gave it for free — on web a clamped `Text` is
|
|
1854
|
+
`white-space: nowrap`, so `min-width: auto` floors it at the WHOLE string; on native a `Text` is
|
|
1855
|
+
`flexShrink: 0`. A row of two clamped values laid out at intrinsic width and ran off the frame.)
|
|
1856
|
+
Both are no-ops until a row actually overflows, and a `style` on the call site still wins — which
|
|
1857
|
+
is how a figure keeps holding over its own label.
|
|
1790
1858
|
|
|
1791
1859
|
|
|
1792
1860
|
|
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
|
|
@@ -450,7 +458,9 @@ The corollaries, each of which a register is routinely missing:
|
|
|
450
458
|
DRAFTS; it does not write. Every drafted record then renders in full — all four fields as
|
|
451
459
|
they will be stored, each editable in place, a `DiffMark` per card, Keep/Drop via
|
|
452
460
|
`useChangeSet` (`initial: "accepted"` — the operator drops the exceptions rather than
|
|
453
|
-
approving six identical records
|
|
461
|
+
approving six identical records; a set that also OVERWRITES values a human set passes a
|
|
462
|
+
`Map<id, decision>` instead, so that half arrives dropped) — and ONE commit named for its outcome
|
|
463
|
+
creates them. The
|
|
454
464
|
manual form beside it skips all of that and saves direct. Only the DOCUMENT path needs the
|
|
455
465
|
gate. The receipt still follows the write — it states the outcome, ROUTES, and carries the
|
|
456
466
|
deterministic checks (a checksum, a count reconciliation).
|
|
@@ -890,7 +900,32 @@ a REGISTER (`tpl_item_list`) with a derived stage column — the same ladder mod
|
|
|
890
900
|
so a row's badge and its filter bucket can never disagree. Rows that are structurally identical
|
|
891
901
|
and tickable in any order are a `tpl_task_board`, not a pipeline.
|
|
892
902
|
|
|
893
|
-
### `
|
|
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.
|
|
894
929
|
|
|
895
930
|
The manager's board: a search, group-by, filter toolbar over a grouped, sortable grid of
|
|
896
931
|
LIVE editable cells — assignee/due/status/tags ALL via `Inline*` editors in **`variant="bare"`**
|
|
@@ -921,31 +956,59 @@ only real dimensions, ✕ clears to ungrouped (no "Nothing" option).
|
|
|
921
956
|
manage in view (group/sort/filter/edit-in-place; it renders ALL rows). For thousands+ you
|
|
922
957
|
BROWSE — that's `tpl_item_list`'s paginated register, not this.
|
|
923
958
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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.
|
|
949
1012
|
|
|
950
1013
|
### `tpl_calendar` — the week desk
|
|
951
1014
|
|
package/docs/testing.md
CHANGED
|
@@ -46,6 +46,12 @@ Assert two things, not one: that the content appeared, and that it **dismisses**
|
|
|
46
46
|
backdrop, so clicking the trigger a second time is often intercepted; click the
|
|
47
47
|
backdrop or press `Escape`.
|
|
48
48
|
|
|
49
|
+
Two overlays open at once stack by the order they were OPENED, so the newest one
|
|
50
|
+
takes the press. If a driver reports `subtree intercepts pointer events` on a
|
|
51
|
+
control of the overlay you just opened, that is a real defect and not this
|
|
52
|
+
anatomy: check with `document.elementFromPoint` at the control's centre, which
|
|
53
|
+
names whatever is actually on top.
|
|
54
|
+
|
|
49
55
|
## Custom pointer drag is not `dragTo`
|
|
50
56
|
|
|
51
57
|
The calendar and gantt drags are built on `use_pointer_drag`, which listens for
|