@lotics/ui 47.6.0 → 47.6.1
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 +22 -3
- package/MIGRATION.md +61 -62
- package/docs/ai_patterns.md +9 -2
- package/docs/catalog.md +37 -3
- package/docs/composition.md +148 -10
- package/docs/data_entry.md +8 -0
- package/docs/reviewing.md +51 -2
- package/examples/tpl_attendance.tsx +1 -1
- package/package.json +3 -1
- package/src/alert.tsx +1 -1
- package/src/avatar.tsx +1 -1
- package/src/avatar.web.tsx +1 -1
- package/src/avatar_group.tsx +1 -1
- package/src/bar_chart.tsx +10 -1
- package/src/board.tsx +1 -1
- package/src/button.tsx +4 -4
- package/src/cell_stack.tsx +140 -0
- package/src/column_filter.tsx +1 -1
- package/src/combobox.tsx +2 -2
- package/src/comments_thread.tsx +2 -2
- package/src/count.tsx +1 -1
- package/src/date_calendar.tsx +2 -2
- package/src/date_filter.tsx +1 -1
- package/src/diff_value.tsx +2 -2
- package/src/eyebrow.tsx +1 -1
- package/src/file_thumbnail.tsx +2 -2
- package/src/file_thumbnail_grid.tsx +1 -1
- package/src/filter_chip.tsx +5 -2
- package/src/finding.tsx +19 -2
- package/src/form_field.tsx +1 -1
- package/src/inline_date_picker.tsx +1 -1
- package/src/inline_edit.tsx +2 -1
- package/src/line_chart.tsx +3 -1
- package/src/list_item.tsx +1 -1
- package/src/loading.tsx +1 -1
- package/src/matrix.tsx +1 -1
- package/src/member_chip.tsx +1 -1
- package/src/member_profile_card.tsx +1 -1
- package/src/menu_list_item.tsx +2 -2
- package/src/metric.tsx +1 -1
- package/src/option_list.tsx +1 -1
- package/src/page_header.tsx +26 -25
- package/src/pagination.tsx +1 -1
- package/src/pie_chart.tsx +4 -2
- package/src/radio_picker.tsx +3 -3
- package/src/section_heading.tsx +74 -5
- package/src/segmented_control.tsx +1 -1
- package/src/select.tsx +1 -1
- package/src/shortcut_badge.tsx +1 -1
- package/src/stacked_bar_chart.tsx +1 -1
- package/src/switcher.tsx +1 -1
- package/src/table.tsx +46 -8
- package/src/tabs.tsx +2 -2
- package/src/text.tsx +2 -7
- package/src/text_ink.ts +96 -0
- package/src/text_link.tsx +1 -1
- package/src/text_utils.ts +1 -46
- package/src/thumbnail_stack.tsx +1 -1
- package/src/time_picker.tsx +1 -1
- package/src/timeline.tsx +1 -1
- package/src/tokens.ts +28 -4
- package/src/tooltip.tsx +1 -1
- package/src/uploading_thumbnail.tsx +1 -1
package/docs/catalog.md
CHANGED
|
@@ -443,6 +443,11 @@ controls is `PressableRow` + **`PressDoor`** — never `PressableHighlight`, whi
|
|
|
443
443
|
button and wraps its children (a button must not contain interactive descendants). `Table`/
|
|
444
444
|
`TableRow` does exactly this internally, so a columnar register needs no assembly.
|
|
445
445
|
|
|
446
|
+
A subject with ONE qualifying line under it — in a table cell, a row, or beside a mark — is
|
|
447
|
+
**`CellStack`** (`title` + `caption`), never two `Text`s in a `View`. It owns the three-part
|
|
448
|
+
stacked-pair rule that composition.md §Typography states: which rung the caption takes, which
|
|
449
|
+
lines are tightened, and the zero gap. See §"Lists, tables & registers".
|
|
450
|
+
|
|
446
451
|
### Filters & view controls
|
|
447
452
|
|
|
448
453
|
`SearchInput`, `ChipGroup`, `FilterChip` (+ `Slider range`, `Counter`), `Chip`,
|
|
@@ -463,6 +468,16 @@ passes through — `Skeleton` / `Loading` (in flight) → `ErrorState` (failed)
|
|
|
463
468
|
(succeeded, nothing) → `CompletionState` (done). Pick by what the region can ASSERT: an empty
|
|
464
469
|
says the read succeeded and found nothing, a failed read knows neither.
|
|
465
470
|
|
|
471
|
+
**An EXCEPTION a check produced is `Finding`, not a `Callout` — and it does not matter who ran
|
|
472
|
+
the check.** The discriminator is the SHAPE of what was found, never its author: two readings
|
|
473
|
+
that disagree, with a magnitude between them, is a `Finding` (severity ranks it, `readings` name
|
|
474
|
+
what each side says, `delta` says how far apart) — a rules engine comparing two stored figures
|
|
475
|
+
qualifies exactly as an extraction comparing two documents does. A `Callout` is for a STATE with
|
|
476
|
+
no second reading to contradict: a record past its validity, a row that will not bill. Reaching
|
|
477
|
+
for a Callout on a genuine contradiction throws away the ranking, the magnitude and the
|
|
478
|
+
provenance, and there is no way to get them back at the call site.
|
|
479
|
+
→ `src/finding.tsx`, whose docstring owns this rule.
|
|
480
|
+
|
|
466
481
|
### Files
|
|
467
482
|
|
|
468
483
|
**Pick the surface FIRST, and pick exactly one per set.** Documents get a LIST (`FileRows`),
|
|
@@ -526,8 +541,9 @@ layout). An add is `after` alone, a removal is `before` alone, an update is both
|
|
|
526
541
|
is a `DiffValue` over `CardSelectItem` candidates — and the ROW around any of them belongs to
|
|
527
542
|
your screen. See the AI-patterns doc indexed in [AGENTS.md](../AGENTS.md) for the laws; `Clarify` (the agent asks back — selectable
|
|
528
543
|
`ChoiceList` options), `Sources` (provenance chips for AI output — at review scale,
|
|
529
|
-
`label={null}` slots the chips at a section's bottom), `Finding` (one ranked
|
|
530
|
-
|
|
544
|
+
`label={null}` slots the chips at a section's bottom), `Finding` (one ranked discrepancy from a
|
|
545
|
+
cross-check — AI or a rules engine, the component does not care; see §"Status / feedback" for
|
|
546
|
+
the Finding-vs-Callout call — laid out HORIZONTALLY: a severity dot, the title, and the `delta` right-aligned on
|
|
531
547
|
that same line, over the `readings` stacked ONE PER LINE — each `source value`, no separator
|
|
532
548
|
joining them (a middot claims a relation it will not name, and is the one mark a screen reader
|
|
533
549
|
drops), both kept on
|
|
@@ -1660,6 +1676,23 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1660
1676
|
A `description` passed as a NODE keeps its own leading — the component only sets it on the
|
|
1661
1677
|
string branch, because forcing a line box on somebody else's subtree re-spaces content that
|
|
1662
1678
|
may not be a line of text at all.
|
|
1679
|
+
- **`cell_stack`** — `CellStack`: a SUBJECT over one qualifying line, as a value primitive —
|
|
1680
|
+
the commonest cell in any register, and the shape most often hand-rolled wrong. `title` +
|
|
1681
|
+
`caption`, plus `weight` / `marked` / `size` / `color` / `tabular` / `align` /
|
|
1682
|
+
`numberOfLines`.
|
|
1683
|
+
It OWNS the three-part stacked-pair rule that [composition.md](./composition.md) §Typography
|
|
1684
|
+
states, so a call site never re-derives it: the caption's rung follows **what else already
|
|
1685
|
+
separates the pair** (a `medium` subject or a leading identity mark ⇒ the caption keeps body
|
|
1686
|
+
size, because that line is often the disambiguator; both lines regular ⇒ it drops to `xs` or
|
|
1687
|
+
the two read as peers); both body-rung lines take `leading="tight"`, an `xs` caption being
|
|
1688
|
+
tight already; and the stack's `gap` is **0**, because each tight box already contributes
|
|
1689
|
+
3px a side.
|
|
1690
|
+
A LONE `title` is deliberately NOT tightened — tight leading closes a PAIR up, and there is
|
|
1691
|
+
no second line to close up to. A `caption` passed as a NODE keeps its own leading and ink,
|
|
1692
|
+
for the reason `ListItem`'s `description` does.
|
|
1693
|
+
Reach for it over two `Text`s in a `View`: the excess height in a hand-rolled pair comes
|
|
1694
|
+
from the LINE BOX, so no margin and no gap appears anywhere to explain it and no spacing
|
|
1695
|
+
probe can see it — it is reported as "too far apart", never as its cause.
|
|
1663
1696
|
- **`table`** — `Table` + `TableRow` + `TableCell`: the paginated high-volume register
|
|
1664
1697
|
(columns defined once, `sortLabels` localizable; rows are `PressableRow`-based).
|
|
1665
1698
|
CONTAINER-RESPONSIVE with no prop (measures itself, like `Breakdown`/`DetailTable`): when
|
|
@@ -2589,7 +2622,8 @@ component rather than showing it at zero.
|
|
|
2589
2622
|
- **`sources`** — `Sources` + `SourceRef`/`SourceKind` (record | document | table | web |
|
|
2590
2623
|
knowledge): provenance chips, per-kind glyphs.
|
|
2591
2624
|
- **`finding`** — `Finding` + `FindingReading` + `FindingSeverity`/`FindingLabels`: one ranked
|
|
2592
|
-
|
|
2625
|
+
discrepancy on two lines, from a cross-check by WHOEVER ran it — an AI extraction, a rules
|
|
2626
|
+
engine, an audit — severity dot, title, right-aligned `delta`, then the
|
|
2593
2627
|
`readings` with the source each came from; optional `detail` consequence, children slot, and
|
|
2594
2628
|
the `finding` locale slice.
|
|
2595
2629
|
- **`result_header`** — `ResultHeader` (+ `ResultTone`): the save-direct RECEIPT's outcome
|
package/docs/composition.md
CHANGED
|
@@ -318,6 +318,14 @@ verb is off-screen, and on an empty list there is no last row to sit under. The
|
|
|
318
318
|
section's control line — it already carries the meta and the view controls — so the add belongs on
|
|
319
319
|
it, in the same spot whether the section holds nought or forty.
|
|
320
320
|
|
|
321
|
+
**The heading's FIRST child is the title — a mark for the subject goes in
|
|
322
|
+
`SectionHeadingTitle`'s `leading` slot, never beside it.** `SectionHeading` reads child
|
|
323
|
+
one as the title and groups everything after it, so an `Avatar` passed as a sibling
|
|
324
|
+
becomes the title and the real title joins the trailing group — which is content-sized,
|
|
325
|
+
so the row stops pushing the action to its right edge. Nothing clips and nothing errors;
|
|
326
|
+
the action simply sits mid-row. `icon` is for a CATEGORY (an `IconName`); `leading` is for
|
|
327
|
+
an IDENTITY — a company's avatar, a person's, a logo.
|
|
328
|
+
|
|
321
329
|
**When the row runs out of width, the VERB wraps — the title never gives way.** The title names
|
|
322
330
|
the section; the verb is the thing that can move. Getting this backwards is easy to ship because
|
|
323
331
|
RN-Web resolves `flex: 1` to `flex-basis: 0%` with `min-width: 0`, so a title beside a button that
|
|
@@ -674,6 +682,23 @@ by — its key, code, or id — because that is what they are scanning against a
|
|
|
674
682
|
or someone on the phone. No other column can take its place: a name repeats, a status is shared by
|
|
675
683
|
dozens.
|
|
676
684
|
|
|
685
|
+
**A set of records the reader COMPARES on a value is a register, and a `ListItem` list cannot be
|
|
686
|
+
one.** `ListItem` gives a title, one description string and a right slot, so every field after the
|
|
687
|
+
first is prose joined by separators — and a value inside prose cannot be scanned down a column,
|
|
688
|
+
which is the whole act the reader came to perform. The tell is a **superlative badge**: a "cheapest",
|
|
689
|
+
"best" or "fastest" mark appears exactly when there is no column to read the answer off, so it is a
|
|
690
|
+
workaround for the missing form rather than a feature, and it disappears the moment the value gets
|
|
691
|
+
a column. The other tell is a per-row badge repeating a value the rows are already SORTED by — that
|
|
692
|
+
is a section heading printed once, not a pill printed on every row.
|
|
693
|
+
|
|
694
|
+
Reach for `ListItem` where the rows are heterogeneous and the reader picks one by NAME — settings,
|
|
695
|
+
a menu of destinations, a detail row. Reach for a register wherever they share a field set.
|
|
696
|
+
|
|
697
|
+
**Sibling surfaces must be the same form.** Two tabs of one dialog, or two panes of one screen,
|
|
698
|
+
showing the same KIND of subject in different forms reads as an accident: a reader parses shape
|
|
699
|
+
before meaning, and the difference tells them the two are unrelated when they are the same thing
|
|
700
|
+
from two sources.
|
|
701
|
+
|
|
677
702
|
Every listed record is actionable. PRIMARY entity rows press-open the workspace `Drawer`
|
|
678
703
|
(sequenced — see [master-detail](#master-detail--list--workspace-drawer-with-sequencing));
|
|
679
704
|
read-only drill-downs expand via `Accordion` or glance via `Peek`; every other row gets an
|
|
@@ -692,6 +717,29 @@ not a row count, it is whether the neighbouring rows survive the expansion.
|
|
|
692
717
|
| `bleed` (legacy) | square wash, `Divider`-separated | only an edge-to-edge data grid that genuinely wants hard rules |
|
|
693
718
|
| `inset` | rounded (radius 8) | grouped lists in a padded container (Accordion drill-downs) |
|
|
694
719
|
|
|
720
|
+
**A leading MARK in a column reserves its slot on every row, including the empty ones.** An
|
|
721
|
+
avatar, a thumbnail or a status dot that only some rows carry gives that column two left edges —
|
|
722
|
+
the marked rows indent by the mark's width, the placeholder sits on the column's own edge — and a
|
|
723
|
+
column is scannable precisely because its values start in the same place. Hold the gutter open
|
|
724
|
+
with an empty box of the mark's width. The header stays on the column edge, which is what a
|
|
725
|
+
column with a leading mark looks like everywhere.
|
|
726
|
+
|
|
727
|
+
**A register centres its cells, and that is right until a TRAILING CONTROL has to line up
|
|
728
|
+
with one of them.** With a control in the row's gutter, centring puts it wherever the tallest
|
|
729
|
+
cell leaves it — 10px below the field on one row and 27px on the next, so the controls stop
|
|
730
|
+
forming a column; that register passes `align="top"`. Without one, keep the default: centring
|
|
731
|
+
lets each cell's block sit in the middle of the row, so a one-line value beside a two-line
|
|
732
|
+
neighbour reads as balanced rather than stranded at the top. **Removing the control is a reason
|
|
733
|
+
to drop `top` again** — nothing breaks when you forget, it just looks slightly wrong forever.
|
|
734
|
+
|
|
735
|
+
**The mechanism, for the `top` case.** Centring resolves per cell: the moment one cell is two lines its
|
|
736
|
+
single-line siblings sit at a different height, and the trailing control — centred
|
|
737
|
+
against the tallest cell — drops below the field it acts on, by an amount that varies
|
|
738
|
+
with the data, so the controls stop forming a column at all. A caller cannot fix this
|
|
739
|
+
(`alignSelf` on the control reaches the slot's own centring, one level below the row's),
|
|
740
|
+
which is why it is a `Table` prop. A one-line row is pixel-identical either way, so
|
|
741
|
+
passing it costs nothing and omitting it is invisible until a value grows a qualifier.
|
|
742
|
+
|
|
695
743
|
**A washed row BLEEDS its wash; it never insets its content.** All three variants
|
|
696
744
|
carry `marginHorizontal: -ROW_WASH_BLEED` absorbed by an equal `paddingHorizontal`
|
|
697
745
|
— net zero for layout — so a row's content sits on its container's own edge,
|
|
@@ -947,6 +995,17 @@ action goes to the referenced record's own page; a local `Drawer` that re-shows
|
|
|
947
995
|
just showed makes "Open" mean *read that again, wider*, and leaves the real record unreachable
|
|
948
996
|
from the only control that promised it.
|
|
949
997
|
|
|
998
|
+
**A detail surface never contains a register of its own SIBLINGS.** The list it was opened from
|
|
999
|
+
is still on screen behind it, and `onPrev`/`onNext` already walk that list — so a "related records"
|
|
1000
|
+
table inside the panel is the third rendering of one set, and the reader has to work out whether
|
|
1001
|
+
the three agree. The trap is that it can be RIGHT when it is built and rot into this later: a
|
|
1002
|
+
register that folds many children into one row (a lane, an order, a person) genuinely hides them,
|
|
1003
|
+
and a table in the detail is then the only way to see them. Flatten that register to one row per
|
|
1004
|
+
child — usually a good change on its own — and the table silently becomes a copy of the rows it is
|
|
1005
|
+
covering, with nothing failing. **When a list changes what a row stands for, re-read every detail
|
|
1006
|
+
panel it opens.** Keep the VERBS such a section carried (a bulk action over the group is real work
|
|
1007
|
+
the register may not offer) and move them to the section they act on.
|
|
1008
|
+
|
|
950
1009
|
**A record surface answers *which record is this* first — and the block that does it is SHORT.**
|
|
951
1010
|
Identity is the handful of facts someone would say aloud to name this record (its number and its
|
|
952
1011
|
counterparty, usually two); everything else is an ATTRIBUTE and belongs below the answer the
|
|
@@ -968,6 +1027,14 @@ ONE at a time, because two open details push the next row off screen. The bounda
|
|
|
968
1027
|
weight, not its field count: read or light-edit (independent inline commits) expands, a real form
|
|
969
1028
|
with a submit and cross-field validation still earns its own surface.
|
|
970
1029
|
|
|
1030
|
+
**A column of always-open inputs makes a list look like a form.** An inline editor is a framed
|
|
1031
|
+
box, so a register that puts one in every row of its NAME column renders eight boxes down the left
|
|
1032
|
+
edge and reads as eight fields waiting to be filled rather than eight records to scan. It also
|
|
1033
|
+
gives the column two left edges — the frame's and the text's — which no amount of aligning the
|
|
1034
|
+
rest of the row recovers. Put the editors on the values a reader actually revises in place (a
|
|
1035
|
+
quantity, a price) and leave the SUBJECT as text; it edits in the expansion, which is where
|
|
1036
|
+
editing lives anyway. The tell is a screenshot: count the frames before you count the rows.
|
|
1037
|
+
|
|
971
1038
|
The expansion carries **EVERY** field, including the ones already in columns. Column cells are
|
|
972
1039
|
read-only, and `table_fit` tier 2 drops droppable columns on the explicit promise that a door
|
|
973
1040
|
reaches the dropped values — the expansion IS that door, so anything it omits is unreachable.
|
|
@@ -1159,6 +1226,13 @@ by what KIND of thing they are.
|
|
|
1159
1226
|
rides its section's HEADING row at the right edge — see § The add-placement law for why, and
|
|
1160
1227
|
for the one-verb-one-weight rule that keeps it `primary` wherever it renders. (The
|
|
1161
1228
|
icon-only `+` `IconButton` — a composer's attach — is a different thing and keeps its glyph.)
|
|
1229
|
+
- **Edit is never an icon.** A pencil is not a word: it does not say WHAT it edits, it reads as
|
|
1230
|
+
decoration beside real controls, and in a row it competes with the values it sits next to. Use
|
|
1231
|
+
a `secondary` `Button` with the verb. `IconButton` is for a verb whose glyph IS the universal
|
|
1232
|
+
name of the act *and* whose surface has no room for a word — a close ✕, an overflow ⋯, a
|
|
1233
|
+
composer's attach. Reserve the row's action gutter wide enough for the label rather than
|
|
1234
|
+
shrinking the verb to fit the gutter; a control that toggles a disclosure names both states
|
|
1235
|
+
(`Sửa` → `Xong`), because a label that stays put while its effect reverses is a lie.
|
|
1162
1236
|
|
|
1163
1237
|
## The action-gating law — disabled + the reason as a co-located Callout
|
|
1164
1238
|
|
|
@@ -1656,6 +1730,13 @@ tight, so only the subject carries the prop. Then the stack's own `gap` goes to
|
|
|
1656
1730
|
line box already contributes 3px above and below, giving 6px between the two, and a `gap` on top
|
|
1657
1731
|
is a third source of space between lines that are meant to read as one.
|
|
1658
1732
|
|
|
1733
|
+
**All three pieces above are one component: reach for `CellStack` (`title` + `caption`) rather
|
|
1734
|
+
than assembling a pair by hand.** They have to agree with each other, and a hand-rolled stack gets
|
|
1735
|
+
one of them wrong silently — the excess height comes from the LINE BOX, so there is no margin and
|
|
1736
|
+
no gap anywhere to explain it, and every spacing probe passes. It surfaces as "too far apart" and
|
|
1737
|
+
gets fixed by tightening a gap, which is a fourth wrong number. Two `Text`s in a `View` is the
|
|
1738
|
+
tell. → [catalog.md §"Lists, tables & registers"](./catalog.md).
|
|
1739
|
+
|
|
1659
1740
|
Running text keeps prose leading. The default is for paragraphs, and `.ui-markdown` renders at it.
|
|
1660
1741
|
|
|
1661
1742
|
**A CONTROL riding a text line must not resize that line.** A control is taller than the text it
|
|
@@ -1687,6 +1768,16 @@ row that must not look the same on every row. A register of twenty people in one
|
|
|
1687
1768
|
twenty identical circles, with the largest and most colourful thing in the row carrying no
|
|
1688
1769
|
information.
|
|
1689
1770
|
|
|
1771
|
+
- **The GLYPH is derived from the DISTINGUISHING part of the name, which is not always the
|
|
1772
|
+
first word.** `avatarInitials` takes a name's first (and last) letters, which is right for a
|
|
1773
|
+
person and wrong for an ORGANIZATION: an organization's name opens with its legal form, so a
|
|
1774
|
+
register of companies marks the same letter on every row — and it is the same failure the derived
|
|
1775
|
+
hue was written to fix, arriving through the glyph instead of the ground. Strip the form from
|
|
1776
|
+
both ends before the name reaches the mark, and use the stripped name for the row's TITLE too:
|
|
1777
|
+
whatever eats the first letters of a mark is also eating the first 60px of the column beside it,
|
|
1778
|
+
where the words that tell two customers apart fall off the end. The legal name belongs on the
|
|
1779
|
+
record the row opens, not in the list. **A hue tally cannot see this** — the ground is seeded
|
|
1780
|
+
from the whole name, so a screen of identical letters still measures as N distinct colours.
|
|
1690
1781
|
- **The hue is derived, never passed.** A per-app or per-screen map is a second copy of something
|
|
1691
1782
|
the name already determines, and it drifts. `avatar_tone` is pure, so one person is one colour
|
|
1692
1783
|
on every screen and in every session — the value is never stored.
|
|
@@ -1876,7 +1967,22 @@ selection washes).
|
|
|
1876
1967
|
|
|
1877
1968
|
**Count the chromatic grounds on a finished screen — the tally is the check.** One or two hues used
|
|
1878
1969
|
many times reads calm and deliberate; ZERO reads like a spreadsheet; ten used once each reads
|
|
1879
|
-
cheap.
|
|
1970
|
+
cheap.
|
|
1971
|
+
|
|
1972
|
+
**A tonal column cannot encode more than about two categories, and the limit is measurable rather
|
|
1973
|
+
than a matter of taste.** The tints are the 50-step, and 50-steps of NEIGHBOURING families are
|
|
1974
|
+
nearly the same colour: sky/blue/indigo measure 1.9–4.5 ΔE apart, against a just-noticeable
|
|
1975
|
+
difference of roughly 2.3. So a hue per value across five or six values ships a column that LOOKS
|
|
1976
|
+
coded and is not — the reader still reads every label, and the palette has spent itself saying
|
|
1977
|
+
nothing. The families far enough apart to actually separate are the warm ones, which already mean
|
|
1978
|
+
danger and waiting, so reaching for them to distinguish a category makes every red on the screen
|
|
1979
|
+
ambiguous.
|
|
1980
|
+
|
|
1981
|
+
Give colour the ONE split in the set that a reader must not get wrong — the one where two values
|
|
1982
|
+
are not interchangeable — and let the label carry the rest. Two hues used many times is the
|
|
1983
|
+
disciplined tally above; it is also the most the medium can carry. Where the values are ORDERED
|
|
1984
|
+
rather than categorical, `ramp` is the tool: one family, shaded, where the eye reads sequence
|
|
1985
|
+
without having to resolve hue at all. A hue with a SINGLE member, from a family used nowhere else on the screen, is a library
|
|
1880
1986
|
default nobody chose — the giveaway is that the call site mentions no colour at all, so the shade
|
|
1881
1987
|
arrived from a component's fallback rather than from a decision. Either give it siblings or take it
|
|
1882
1988
|
back to a neutral.
|
|
@@ -1889,12 +1995,43 @@ colours on one screen: collect every element's `color` grouped by what it MEANS,
|
|
|
1889
1995
|
holding more than one value — or any value serving more than one meaning — is the finding, whatever
|
|
1890
1996
|
the sizes are.
|
|
1891
1997
|
|
|
1892
|
-
**Supporting text is `muted`, and
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1998
|
+
**Supporting text is `muted`, and there is no raw neutral to reach for instead.** `Text`'s `color`
|
|
1999
|
+
is a set of ROLES — `default`, `muted`, `inactive`, `inverted`, `danger`, `warning`, `success` —
|
|
2000
|
+
and carries no palette names at all. That is not tidiness; a shade name in this position is wrong
|
|
2001
|
+
three ways.
|
|
2002
|
+
|
|
2003
|
+
*It cannot know its ground.* A call site names a colour without knowing what will be painted under
|
|
2004
|
+
it, and the kit paints several things under text: white, a `zinc-50` hover, a `zinc-100` selected
|
|
2005
|
+
row. The retired `zinc-500` cleared AA on white by 0.33 (4.83:1) and **failed on the selected row**
|
|
2006
|
+
at 4.40:1, for text at `sm`/`xs` where the bar is 4.5. A role is resolved once, centrally, against
|
|
2007
|
+
the darkest ground the kit permits — which is why `muted` is `zinc-600` (7.03:1 at worst) and not
|
|
2008
|
+
the lighter step that looks tempting in isolation.
|
|
2009
|
+
|
|
2010
|
+
*It splits one role across two inks.* Supporting lines under a VALUE were `muted` while a heading's
|
|
2011
|
+
`description` was a raw shade — one structural role, "the line that supports the thing above it",
|
|
2012
|
+
rendering in two colours decided by what happened to sit above it.
|
|
2013
|
+
|
|
2014
|
+
*And it cannot be re-pointed.* An inverted ground, a denser surface, a dark theme — a role name
|
|
2015
|
+
moves, a name that states its own value becomes a lie. `inverted` is the proof the kit already
|
|
2016
|
+
leans on: if shade names were right it would be called `white`.
|
|
2017
|
+
|
|
2018
|
+
**The vocabulary is TWO LAYERS, and only one is a decision.** `colors.zinc[600]` is a primitive —
|
|
2019
|
+
a value, owned by nobody, the same ramp anyone could paste. `TEXT_INK` (`@lotics/ui/text_ink`) is
|
|
2020
|
+
the semantic layer that says `muted` → `zinc.600`, and it is where the rule lives. Components
|
|
2021
|
+
address ink only through the second.
|
|
2022
|
+
|
|
2023
|
+
Keep them separate even when the design tool wants shade names. A designer picking `zinc-500` and
|
|
2024
|
+
an author typing `zinc-500` look like one source of truth, but what they agree on is a THIRD-PARTY
|
|
2025
|
+
RAMP — the decision itself is written in neither, so it gets re-made at every call site, which is
|
|
2026
|
+
precisely how one role came to render in two inks. `textInkTokens()` emits both layers in the W3C
|
|
2027
|
+
Design Tokens format with the alias intact (`{color.zinc.600}`, never a flattened hex), which is
|
|
2028
|
+
what a design tool imports as an ALIAS variable. One file, both tools, and re-pointing a role moves
|
|
2029
|
+
them together.
|
|
2030
|
+
|
|
2031
|
+
`inactive` is the one ink below the AA floor, deliberately — WCAG 1.4.3 exempts text in an inactive
|
|
2032
|
+
component, and a disabled label or an absent value is not content. Everything else is asserted by
|
|
2033
|
+
`text_ink_contrast.test.ts`, which iterates the vocabulary the `TextColor` type is DERIVED from, so
|
|
2034
|
+
a newly added ink is checked automatically rather than when someone remembers to add a case.
|
|
1898
2035
|
|
|
1899
2036
|
**TEXT never wears `solid()`** — the 500 is a FILL shade (dots, series). A status word/number uses
|
|
1900
2037
|
`Text`'s valence tokens: `color="danger"` (red-900) / `"warning"` (amber-700) / `"success"`
|
|
@@ -2061,9 +2198,10 @@ file and wrong on the screen. If something cannot be wrapped in `Text` (a raw `T
|
|
|
2061
2198
|
axis), take the rung from `type_ramp` rather than picking a number.
|
|
2062
2199
|
|
|
2063
2200
|
Weights: `regular` / `medium` /
|
|
2064
|
-
`semibold`. Colors: `default
|
|
2065
|
-
|
|
2066
|
-
|
|
2201
|
+
`semibold`. Colors are ROLES and there are seven: `default`, `muted` (all supporting text),
|
|
2202
|
+
`inactive` (a disabled control's label, and a value that is not there yet), `onInverse` (requires a
|
|
2203
|
+
dark ground — illegal on the canvas), and the valence set (`danger`/`warning`/`success`). No
|
|
2204
|
+
palette names — see §"Supporting text is `muted`" for why a shade in this position is wrong.
|
|
2067
2205
|
|
|
2068
2206
|
## Touch & whitespace
|
|
2069
2207
|
|
package/docs/data_entry.md
CHANGED
|
@@ -385,6 +385,14 @@ where it must stay strictly inside its box — that is `framed`. A DISABLED `fra
|
|
|
385
385
|
box: it drops the border and fill so it cannot promise a press, but it stays in line with the enabled
|
|
386
386
|
fields beside it.
|
|
387
387
|
|
|
388
|
+
**A `framed` field in a RIGHT-ALIGNED column must be told to fill its cell.** It sizes to its
|
|
389
|
+
content, and the column's alignment then pins each box's RIGHT edge while its left edge lands
|
|
390
|
+
wherever that row's value happened to end — so a column of amounts reads as a ragged stack of
|
|
391
|
+
boxes, worse the more the values differ in length. Nothing measures wrong: every box is correct
|
|
392
|
+
on its own, no text clips, and the values ARE right-aligned. Give the cell's editor a full-width
|
|
393
|
+
wrapper so one box width serves the column. (`bare` has the mirror constraint above and cannot be
|
|
394
|
+
right-aligned at all.)
|
|
395
|
+
|
|
388
396
|
**`bare` puts its whole affordance in the hover edge, so nothing may outrank that edge.** Pass the
|
|
389
397
|
variant as a PROP; never express it as a resting `borderColor: "transparent"` in `style`, which is
|
|
390
398
|
applied after the hover rule and erases the edge one line after it is computed. The failure is
|
package/docs/reviewing.md
CHANGED
|
@@ -553,6 +553,50 @@ back-load the meaning, so the half that survives is the half the reader already
|
|
|
553
553
|
**Source-side signature:** a fit or stack decision that reaches for a control/usability constant
|
|
554
554
|
(`MIN_CONTROL_WIDTH`, a flex minimum) where the thing being measured holds prose.
|
|
555
555
|
|
|
556
|
+
### 8h. Which columns SURVIVE the narrow width
|
|
557
|
+
**Collect** the header labels a register still renders at 375, and diff them against 1280.
|
|
558
|
+
|
|
559
|
+
A register sheds columns by rank rather than overflowing, so the question is never whether it
|
|
560
|
+
fits — it always does — but WHICH columns it kept. The signature of a wrong rank is a survivor
|
|
561
|
+
list missing the column the register exists for: an amount register that kept a name and a status
|
|
562
|
+
and dropped the amount. It is invisible at desk width, which is where it gets reviewed, and no
|
|
563
|
+
probe that measures a wide screen can see it.
|
|
564
|
+
|
|
565
|
+
Read the survivor list, not the ranks in source: an unannotated column takes a rank ABOVE every
|
|
566
|
+
explicit one, so a set where "the important ones are annotated" sheds the unannotated column
|
|
567
|
+
first — annotating some columns and not others is the failure, not the fix. If the register has
|
|
568
|
+
more than one column, either all of them carry a rank or none do.
|
|
569
|
+
|
|
570
|
+
### 8h-bis. A cell that grows a second line lifts its first
|
|
571
|
+
**Collect** one column's first-line `y` MINUS its row's top, for every row; report the SET.
|
|
572
|
+
|
|
573
|
+
A register centres its cells, so a cell's content straddles the row's middle: one line sits ON the
|
|
574
|
+
centre, two lines straddle it, and the two states put the first line at different heights. A column
|
|
575
|
+
whose cell is sometimes one line and sometimes two therefore does not form a line down the page —
|
|
576
|
+
its figures drift by half a line on exactly the rows that grew a caption, which are the rows that
|
|
577
|
+
had something worth saying. A constant non-zero offset is fine (a one-line column centred in a
|
|
578
|
+
two-line row is correct); a SET with more than one value is the finding.
|
|
579
|
+
|
|
580
|
+
Fixing the cell that grew only MOVES it — the neighbour that did not grow is now the odd one. Fix
|
|
581
|
+
it by making the varying cell's height constant: render the optional line always, empty when there
|
|
582
|
+
is nothing to say. It costs no row height wherever some other cell is already two lines tall.
|
|
583
|
+
|
|
584
|
+
*Two traps in the fix.* A lone `" "` collapses in the web renderer and the reserved line silently
|
|
585
|
+
loses its height — use a non-breaking space. And a caption WIDER than its column does not clip: it
|
|
586
|
+
overflows into the neighbour, so two adjacent captions close up and read as one sentence. A column
|
|
587
|
+
holding a sentence is sized by the sentence, never by the figure above it.
|
|
588
|
+
|
|
589
|
+
### 8i. Row controls against a multi-line cell
|
|
590
|
+
**Collect** each row's trailing control top and the top of the field it acts on; report
|
|
591
|
+
the SET of deltas, not the average.
|
|
592
|
+
|
|
593
|
+
A register's rows centre their cells, so a cell that grew a second line leaves its
|
|
594
|
+
siblings and the trailing control at a different height. The signature is a delta set
|
|
595
|
+
with more than one value — 10px on one row and 27px on another means the offset tracks
|
|
596
|
+
the data, which is worse than a constant one because the controls no longer form a
|
|
597
|
+
column. Non-zero-but-constant is the same defect with luckier data.
|
|
598
|
+
|
|
599
|
+
|
|
556
600
|
### 9. The absence pass — what catches "bland"
|
|
557
601
|
Every probe above reads an element that EXISTS. None fires on a screen where nothing is wrong and
|
|
558
602
|
nothing is right, which is the screen reported as bland. Defect-removal converges on *inoffensive*;
|
|
@@ -563,7 +607,10 @@ this converges on *distinctive*.
|
|
|
563
607
|
ten used once each reads cheap.
|
|
564
608
|
- **Identity marks** — on a screen whose subject is an entity, count them. Zero is the finding,
|
|
565
609
|
and so is N identical: a mark that looks the same on every row carries nothing, and it is the
|
|
566
|
-
largest, brightest thing there.
|
|
610
|
+
largest, brightest thing there. **Tally the GLYPH and the GROUND separately**, and report the
|
|
611
|
+
size of the largest GROUP rather than the count of distinct values — the two halves are derived
|
|
612
|
+
by different rules and fail independently, so a set of marks can be five distinct colours and
|
|
613
|
+
five copies of one letter, which "5 distinct marks" reports as healthy.
|
|
567
614
|
- **What does the screen DECIDE?** A register listing every row at equal weight has no point of
|
|
568
615
|
view. Usually the real answer when treatment fixes keep not landing.
|
|
569
616
|
- **What is inherently visual in THIS data?** Usually the data itself: identity marks, status
|
|
@@ -684,7 +731,9 @@ instead of one blended string.
|
|
|
684
731
|
```js
|
|
685
732
|
() => {
|
|
686
733
|
const SZ = { '12px':'xs','14px':'sm','16px':'md','18px':'lg','20px':'lg','22px':'xl','24px':'xl','28px':'xxl','32px':'xxl/xxxl','48px':'xxxl' };
|
|
687
|
-
|
|
734
|
+
// An ink the kit no longer has a NAME for is a finding, not a label: the
|
|
735
|
+
// vocabulary is default / muted / inactive / inverted + the valence set.
|
|
736
|
+
const INK = { 'rgb(24, 24, 27)':'default','rgb(82, 82, 91)':'muted','rgb(161, 161, 170)':'inactive','rgb(255, 255, 255)':'onInverse','rgb(113, 113, 122)':'OFF-SYSTEM zinc-500 (retired)','rgb(63, 63, 70)':'OFF-SYSTEM zinc-700 (retired)' };
|
|
688
737
|
const out = [];
|
|
689
738
|
const walk = (n) => {
|
|
690
739
|
if (n.nodeType === 3 && n.textContent.trim()) {
|
|
@@ -143,7 +143,7 @@ function HomNayRow({ nv, selected, onPress }: { nv: NhanVien; selected: boolean;
|
|
|
143
143
|
<Avatar name={nv.ten} size="md" />
|
|
144
144
|
<View style={{ flex: 1, gap: 2, minWidth: 0 }}>
|
|
145
145
|
<Text weight="medium" numberOfLines={1}>{nv.ten}</Text>
|
|
146
|
-
<Text size="sm" color="
|
|
146
|
+
<Text size="sm" color="muted" numberOfLines={1}>{nv.chucVu}</Text>
|
|
147
147
|
</View>
|
|
148
148
|
</View>
|
|
149
149
|
</TableCell>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "47.6.
|
|
3
|
+
"version": "47.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./accordion": "./src/accordion.tsx",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"./capture_row": "./src/capture_row.tsx",
|
|
36
36
|
"./card": "./src/card.tsx",
|
|
37
37
|
"./card_select_item": "./src/card_select_item.tsx",
|
|
38
|
+
"./cell_stack": "./src/cell_stack.tsx",
|
|
38
39
|
"./charge_lines": "./src/charge_lines.tsx",
|
|
39
40
|
"./check_circle": "./src/check_circle.tsx",
|
|
40
41
|
"./checkbox": "./src/checkbox.tsx",
|
|
@@ -275,6 +276,7 @@
|
|
|
275
276
|
"./tabs": "./src/tabs.tsx",
|
|
276
277
|
"./text": "./src/text.tsx",
|
|
277
278
|
"./text_disclosure": "./src/text_disclosure.tsx",
|
|
279
|
+
"./text_ink": "./src/text_ink.ts",
|
|
278
280
|
"./text_input_field": "./src/text_input_field.tsx",
|
|
279
281
|
"./text_link": "./src/text_link.tsx",
|
|
280
282
|
"./text_utils": "./src/text_utils.ts",
|
package/src/alert.tsx
CHANGED
package/src/avatar.tsx
CHANGED
package/src/avatar.web.tsx
CHANGED
package/src/avatar_group.tsx
CHANGED
|
@@ -146,7 +146,7 @@ export function AvatarGroup(props: AvatarGroupProps) {
|
|
|
146
146
|
>
|
|
147
147
|
{/* The rung's own type — the same one initials take. A fixed size here
|
|
148
148
|
is how a stack ends up with a 14px count beside 12px initials. */}
|
|
149
|
-
<Text size={AVATAR_TEXT[size]} weight="medium" color="
|
|
149
|
+
<Text size={AVATAR_TEXT[size]} weight="medium" color="muted" aria-hidden>
|
|
150
150
|
{`+${hidden}`}
|
|
151
151
|
</Text>
|
|
152
152
|
</View>
|
package/src/bar_chart.tsx
CHANGED
|
@@ -314,8 +314,17 @@ export function BarChart(props: BarChartProps) {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
const styles = StyleSheet.create({
|
|
317
|
+
// CONTENT-SIZED; the HOST decides whether a chart stretches.
|
|
318
|
+
//
|
|
319
|
+
// This was `flex: 1`, which RN-Web resolves to a zero basis on the parent's
|
|
320
|
+
// MAIN axis. A card body is a vertical stack, so the chart's height resolved to
|
|
321
|
+
// nothing and it painted its fixed-height content centred on that — outside the
|
|
322
|
+
// card. Every consumer then had to neutralise it by wrapping the chart in a row,
|
|
323
|
+
// and getting that wrapper wrong is silent at one width and broken at another.
|
|
324
|
+
// `Funnel` and `Breakdown` were content-sized all along, so the family did not
|
|
325
|
+
// even agree with itself. A host that genuinely wants a chart to fill a box
|
|
326
|
+
// still says so, on the box.
|
|
317
327
|
chartContainer: {
|
|
318
|
-
flex: 1,
|
|
319
328
|
justifyContent: "center",
|
|
320
329
|
},
|
|
321
330
|
verticalChartWrapper: {
|
package/src/board.tsx
CHANGED
|
@@ -290,7 +290,7 @@ export function BoardColumn(props: BoardColumnProps) {
|
|
|
290
290
|
<View ref={ref} style={[styles.column, over && styles.columnOver]}>
|
|
291
291
|
<View style={styles.columnHeading}>
|
|
292
292
|
<View style={styles.columnIdentity}>{heading}</View>
|
|
293
|
-
<Text size="sm" color="
|
|
293
|
+
<Text size="sm" color="muted" tabular>
|
|
294
294
|
{count}
|
|
295
295
|
</Text>
|
|
296
296
|
<View style={styles.columnAction}>{action}</View>
|
package/src/button.tsx
CHANGED
|
@@ -13,7 +13,7 @@ import { colors } from "./colors";
|
|
|
13
13
|
import { CONTROL_RADIUS, FOCUS_RING } from "./control_surface";
|
|
14
14
|
import { Icon, IconName } from "./icon";
|
|
15
15
|
import { Ref } from "react";
|
|
16
|
-
import { TextColor } from "./
|
|
16
|
+
import { TextColor } from "./text_ink";
|
|
17
17
|
import { useTooltip, UseTooltipOptions } from "./tooltip";
|
|
18
18
|
import { useFocusRing, composeHandler } from "./use_focus_ring";
|
|
19
19
|
import { getButtonIconColor } from "./button_colors";
|
|
@@ -119,7 +119,7 @@ export function Button(props: ButtonProps) {
|
|
|
119
119
|
weight="medium"
|
|
120
120
|
// `inverted` — the kit's own white-on-dark ink, the same token the primary
|
|
121
121
|
// uses, so a brand button and the primary read as one family.
|
|
122
|
-
color={brand && !disabledOrLoading ? "
|
|
122
|
+
color={brand && !disabledOrLoading ? "onInverse" : getButtonTextColor(color, disabledOrLoading)}
|
|
123
123
|
userSelect="none"
|
|
124
124
|
>
|
|
125
125
|
{title}
|
|
@@ -206,10 +206,10 @@ export function getButtonTextColor(color?: ButtonColor, disabled?: boolean): Tex
|
|
|
206
206
|
// Disabled fades the label to the zinc-400 disabled ink on EVERY variant —
|
|
207
207
|
// the washed background alone is a one-step tint nobody reads as disabled.
|
|
208
208
|
// (The primary keeps inverted ink; its zinc-300 surface carries the wash.)
|
|
209
|
-
if (disabled) return color === "primary" ? "
|
|
209
|
+
if (disabled) return color === "primary" ? "onInverse" : "inactive";
|
|
210
210
|
switch (color) {
|
|
211
211
|
case "primary":
|
|
212
|
-
return "
|
|
212
|
+
return "onInverse";
|
|
213
213
|
case "secondary":
|
|
214
214
|
return "default";
|
|
215
215
|
case "danger":
|