@lotics/ui 47.13.1 → 47.14.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 +16 -2
- package/docs/catalog.md +225 -27
- package/docs/composition.md +83 -4
- package/docs/data_entry.md +13 -6
- package/docs/reviewing.md +30 -0
- package/docs/templates.md +1 -1
- package/examples/tpl_attendance.tsx +51 -80
- package/examples/tpl_pivot.tsx +10 -1
- package/examples/tpl_record.tsx +33 -107
- package/package.json +9 -1
- package/src/avatar_size.ts +9 -8
- package/src/axis_ticks.ts +63 -0
- package/src/bar_chart.tsx +69 -50
- package/src/cell_stack.tsx +43 -7
- package/src/chip_group.tsx +12 -6
- package/src/danger_zone.tsx +23 -10
- package/src/date_filter.tsx +17 -7
- package/src/date_filter_presets.ts +209 -1
- package/src/date_range_filter_field.tsx +14 -4
- package/src/detail_row.tsx +18 -18
- package/src/drawer.tsx +66 -28
- package/src/format_date.ts +31 -3
- package/src/index.css +18 -0
- package/src/ledger.tsx +23 -15
- package/src/line_chart.tsx +194 -61
- package/src/list_item.tsx +22 -3
- package/src/locale.tsx +25 -0
- package/src/matrix.tsx +116 -16
- package/src/matrix_totals.ts +71 -3
- package/src/month_math.ts +16 -0
- package/src/month_stepper.tsx +62 -0
- package/src/organization_name.ts +94 -0
- package/src/search_match.ts +19 -0
- package/src/section_nav.tsx +163 -0
- package/src/state_matrix.tsx +265 -0
- package/src/table.tsx +67 -5
- package/src/timetable.tsx +167 -0
- package/src/toggle_strip.tsx +259 -0
package/AGENTS.md
CHANGED
|
@@ -17,7 +17,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
17
17
|
| [docs/catalog.md](./docs/catalog.md) | **The complete inventory** — incl. `just_arrived` (`useJustArrived` + `TableRow`'s `justArrived`: the one-shot wash a row wears when it LANDS, so a realtime push is visible rather than one row of two hundred silently changing) and `SplitRow`/`SplitPane` (the responsive pane row — a weight that survives into a stacked column divides HEIGHT and clips a pane) — Reach-by-role (each data role → the ONE canonical component) + every `@lotics/ui/<module>` entry point (incl. `@lotics/ui/vite`'s `loticsOptimizeDeps` + `loticsResolve()` — the pre-bundle list and the whole `resolve` block a custom-code app's `vite.config.ts` imports rather than hand-carries, dev-link included). Read before building any screen; reuse first. |
|
|
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 (`Finding` is filed here because AI produces most of them, NOT because it is AI-only — a rules-derived contradiction is as in scope as an extraction's); **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
|
-
| [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), **a width floor answers can this be READ and never can this be operated** (the operating question passes a row whose every string is already cut), **a status column earns its slot by carrying a REASON rather than a state** (the floor asks for the fact to be visible, not for a badge), **an identity mark's GLYPH is derived from the name's distinguishing part** (an organization's first word is its legal form, so untreated every row marks one letter), **a row subject's supporting line takes a REFERENCE and never a sentence** (an explanation truncates to the clause the reader already knew), **a list stays fast by keeping IDENTITY rather than memoizing** (key by the record not the index; `memo` is the last resort and does nothing against a freshly-built prop), and where a commit sits. |
|
|
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), **a width floor answers can this be READ and never can this be operated** (the operating question passes a row whose every string is already cut), **a status column earns its slot by carrying a REASON rather than a state** (the floor asks for the fact to be visible, not for a badge), **an identity mark's GLYPH is derived from the name's distinguishing part** (an organization's first word is its legal form, so untreated every row marks one letter), **a row subject's supporting line takes a REFERENCE and never a sentence** (an explanation truncates to the clause the reader already knew), **a list stays fast by keeping IDENTITY rather than memoizing** (key by the record not the index; `memo` is the last resort and does nothing against a freshly-built prop), **a surface keyed by a whole MONTH takes a `MonthStepper`, never a range field** (its `max` bound is the contract), **a subject-by-date sheet of STATES is a `StateMatrix`** (a glyph per cell, the legend derived from the same list; not a `Matrix`, where a blank means zero), **a report's comparator is DERIVED from the selected range with `previousPeriod`, never a second control** (show it on the figure — `KPIStripItem.trend`/`caption`, `Metric previousValue`, a second `LineChart` series; a grain `SegmentedControl` beside the date field re-adds the mode the field removed), and where a commit sits. |
|
|
21
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 the probes, each naming what to COLLECT and the SIGNATURE in the numbers — including the truncation sweep (a row whose halves both measure fine and whose text is cut) the summary-vs-facet check (a strip that keeps describing the unfiltered set) and the derivable-column test (a status badge its neighbour already answers) the first-line drift a column shows when its cell is sometimes one line and sometimes two, the cross-FRAME check (what the screen destroys when a row arrives — the one defect that exists in no single frame), and the semantic-section check (a danger or empty section drawn as an ordinary one, and the primitive nobody reached for). 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. |
|
|
@@ -87,7 +87,9 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
87
87
|
identity. And a radius is a PROPORTION of the box (`proportionalRadius`), because a component
|
|
88
88
|
sized by data renders as a pill at one height and barely a corner at another.
|
|
89
89
|
→ [composition.md §"A body, not an edge"](./docs/composition.md).
|
|
90
|
-
- **A subject over one qualifying line is `CellStack`, never two `Text`s in a `View`.**
|
|
90
|
+
- **A subject over one qualifying line is `CellStack`, never two `Text`s in a `View`.** Pass a
|
|
91
|
+
whitespace `caption` to RESERVE the line on a row with nothing to say — it is normalised to a
|
|
92
|
+
non-breaking space, so the column keeps one first-line height. It owns
|
|
91
93
|
the whole stacked-pair rule — which rung the caption takes (from what ELSE separates the pair),
|
|
92
94
|
which lines are tightened, and the zero gap. A hand-rolled pair gets one of the three wrong
|
|
93
95
|
silently: the excess comes from the LINE BOX, so no margin and no gap exists to explain it and
|
|
@@ -107,6 +109,11 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
107
109
|
- **A countdown ANNOTATES its date field — never a trailing badge, and never hand-rolled.**
|
|
108
110
|
Reach for `@lotics/ui/deadline` and spread `deadlineAnnotation` onto the
|
|
109
111
|
`DetailRow`. → [catalog.md §"Dates & times"](./docs/catalog.md).
|
|
112
|
+
- **A record surface's outline is `SectionNav` + `useSectionNav`, never hand-rolled rows.** The
|
|
113
|
+
hook spies the scroll, the component draws the rail AND the narrow bar that replaces it, and
|
|
114
|
+
the item's `issue` dot is what makes the outline say where the problem is. A hand-rolled rail
|
|
115
|
+
loses the live re-measure, the last-section rule and the narrow form.
|
|
116
|
+
→ [catalog.md §"Overlays & navigation"](./docs/catalog.md).
|
|
110
117
|
- **A third-party channel takes `BrandMark`, our own glyphs take `Icon`.** The names are an
|
|
111
118
|
allowlist, so a channel with no mark (a phone call, an event, a website) fails to typecheck
|
|
112
119
|
and falls back to `Icon` instead of borrowing another company's logo.
|
|
@@ -146,6 +153,13 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
146
153
|
gets `<GroupLead>` (`sm` medium, page ink). Neither takes `size`/`weight`/`transform` — the
|
|
147
154
|
omitted props are the rule. →
|
|
148
155
|
[composition.md §"The heading ramp"](./docs/composition.md).
|
|
156
|
+
- **N booleans across FIXED positions are a `ToggleStrip`, never a row of hand-rolled cells.**
|
|
157
|
+
A week's running days, a gate's open hours, a notice's channels: a compact strip at
|
|
158
|
+
`CONTROL_CONTENT_HEIGHT`, each cell a `checkbox`, `minOne` where the empty set is a different
|
|
159
|
+
reading rather than a neutral one, and `toggleStripWidth(n)` for the column that seats it.
|
|
160
|
+
Selected wears `chipSurfaceStyle`'s doubled dark edge — a brand ground on a selection is the
|
|
161
|
+
decision the kit already reversed once.
|
|
162
|
+
→ [catalog.md §"Choice controls"](./docs/catalog.md).
|
|
149
163
|
- **Pure primitives only** — no i18n, analytics, or domain types in `src/` (pass `labels`,
|
|
150
164
|
callbacks); Lotics-coupled UI belongs in `@lotics/ui-internal`.
|
|
151
165
|
- **A button never contains a control** — a row that presses open AND carries its own controls
|
package/docs/catalog.md
CHANGED
|
@@ -387,7 +387,9 @@ Worked example: [`tpl_board`](../examples/tpl_board.tsx).
|
|
|
387
387
|
`KPIStrip` (the dashboard stat band), `SummaryLine` (the light inline register/list summary
|
|
388
388
|
— below the toolbar, from the filtered rows), `KPICard` / `Metric` (headline figures),
|
|
389
389
|
`TrendChip` (delta), `Sparkline`, `BarChart` / `LineChart` / `PieChart` (the canonical SVG
|
|
390
|
-
set — no recharts
|
|
390
|
+
set — no recharts; `LineChart` takes `points` for one line or `series` for several on one
|
|
391
|
+
shared scale, which is where a "this period vs last" comparison goes), `ProgressRing`,
|
|
392
|
+
`ProgressBar` (its `compact` prop = ONE row, track + a
|
|
391
393
|
plain sm tabular count beside it — the cell/heading/peek-trigger meter. **The track clamps at
|
|
392
394
|
100%, the caption does not** — over its max it reads `2,100 / 2,000 (105%)`. **`max: null` is an
|
|
393
395
|
unbounded meter** — the caption states the value and the caller's `unboundedLabel`
|
|
@@ -413,7 +415,9 @@ stays solid, others dim — the caller renders the records). The subset/drop-off
|
|
|
413
415
|
split across stages), `StatusGrid` + `StatusLegend`, `Heatmap`
|
|
414
416
|
(density: colour-only, "where does it cluster"), `Matrix` (the PIVOT cross-tab: the NUMBER
|
|
415
417
|
in each cell — optionally a heat wash behind it — plus row/column/grand totals; press a cell
|
|
416
|
-
to drill)
|
|
418
|
+
to drill), `StateMatrix` (the CATEGORICAL cross-tab: subjects down the side, dated positions
|
|
419
|
+
across the top, a STATE glyph in every cell — an attendance sheet, a fleet's month; a blank
|
|
420
|
+
cell means "nothing was due", never zero).
|
|
417
421
|
|
|
418
422
|
### Surfaces & layout
|
|
419
423
|
|
|
@@ -533,7 +537,8 @@ patterns doc indexed in [AGENTS.md](../AGENTS.md)).
|
|
|
533
537
|
upcoming, horizontal OR vertical), `RemainderMeter` + `AllocationRow` (allocation),
|
|
534
538
|
`Timeline` (a heterogeneous event LOG — icons + expandable details, not progress),
|
|
535
539
|
`CalendarView` (the `calendar` module — days are the axis), `GanttView` (RESOURCES are the
|
|
536
|
-
axis: a channel, an owner, a vehicle), `
|
|
540
|
+
axis: a channel, an owner, a vehicle), `Timetable` (a repeating PATTERN with no dates — time ×
|
|
541
|
+
weekday, a slot per row, today a tinted column), `comments_thread`, `SocialPostPreview` (a post as ONE
|
|
537
542
|
network's feed shows it, with the count against that network's limit and every rule its API
|
|
538
543
|
would refuse — the publish gate reads the same `socialPostIssues` the preview draws).
|
|
539
544
|
|
|
@@ -641,7 +646,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
641
646
|
`react-markdown` instance the kit's renderer cannot serve. Never `remark-gfm`: it ships a
|
|
642
647
|
regex-lookbehind literal, which WebKit older than Safari 16.4 rejects while parsing the bundle.
|
|
643
648
|
- **`markdown.css`** — import once for the web markdown styling.
|
|
644
|
-
- **`format_date`** — `formatDate` / `parseDate` / `toISODate` + `DateFormatStyle
|
|
649
|
+
- **`format_date`** — `formatDate` / `parseDate` / `toISODate` + `DateFormatStyle` (incl.
|
|
650
|
+
`monthYear` and `quarterYear`, the two period labels).
|
|
645
651
|
- **`format_money`** — `formatMoney` / `formatCompactNumber`.
|
|
646
652
|
- **`text_utils`** — text/typography plumbing: `getTextColor` (the TextColor→hex map incl.
|
|
647
653
|
the AA-cleared valence set), the Inter `fontFamily*` stacks, and `getInputTextStyle` /
|
|
@@ -770,6 +776,12 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
770
776
|
- **`group_avatar`** — `GroupAvatar`: a name's initials in a zinc rounded square (`size` — the
|
|
771
777
|
same rung scale AND the same initials rule as `Avatar`, default `md`); the avatar for
|
|
772
778
|
image-less entities — groups, organizations. A person → `Avatar`/`MemberChip`.
|
|
779
|
+
- **`organization_name`** — `stripLegalForm(name)`: an organization's name without the legal
|
|
780
|
+
form it opens or closes with ("Công ty TNHH", "HTX", "Hộ kinh doanh", "Ltd.", "LLC"…). The
|
|
781
|
+
name a mark derives its initials from AND the title a register row shows — composition.md
|
|
782
|
+
§Identity marks says why: the form puts the same first letters on every row and spends the
|
|
783
|
+
first 60px of the column on words that tell no two rows apart. The legal name belongs on the
|
|
784
|
+
record the row opens. A name that is nothing but a form comes back unchanged.
|
|
773
785
|
- **`thumbnail_stack`** — `ThumbnailStack`: the same capped stack over THINGS rather than
|
|
774
786
|
parties — a set of product, document or unit pictures answering "an order of what?" in one
|
|
775
787
|
register cell. Takes `{ id, label, image? }`, shows `max` (default 3) and makes the rest the
|
|
@@ -952,6 +964,13 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
952
964
|
destructive action slot (children, e.g. a `danger` Button); sits APART at the bottom of a
|
|
953
965
|
record/settings surface. The heading defaults to the locale's `dangerZone.title`
|
|
954
966
|
("Danger zone" / "Vùng nguy hiểm") — pass `title` only to override.
|
|
967
|
+
ONE LEFT EDGE, and it is `Callout`'s anatomy that gets it: the hazard mark sits in an outer
|
|
968
|
+
ROW beside a column holding title, consequence and actions, so all three derive from the same
|
|
969
|
+
edge. Put the mark INSIDE the heading row instead — the obvious way to write it, and how this
|
|
970
|
+
shipped — and only the TITLE moves right, so the loudest line in the block sits 22px past the
|
|
971
|
+
sentence explaining it and the button under both starts somewhere else again. Three edges
|
|
972
|
+
inside one 16px-padded box, on the primitive that exists so a hazard is not hand-rolled. Any
|
|
973
|
+
block with this anatomy (mark, title, body, actions) is laid out the same way.
|
|
955
974
|
- **`landmark`** — `Landmark`: the semantic region wrapper — `kind`
|
|
956
975
|
banner|navigation|main|complementary|contentinfo|region maps to the matching HTML element
|
|
957
976
|
on web for screen-reader landmark navigation, `accessibilityRole` on native;
|
|
@@ -1208,7 +1227,13 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1208
1227
|
choice. → `data_entry.md` §"Choosing a CHOICE control" — and check the states really ARE
|
|
1209
1228
|
exclusive: independent capabilities that happen to be exclusive today bake today's
|
|
1210
1229
|
combinations into the type.
|
|
1211
|
-
- **`chip_group`** — `ChipGroup`: pill single-select over a small visible set. The
|
|
1230
|
+
- **`chip_group`** — `ChipGroup`: pill single-select over a small visible set. The ACTIVE chip
|
|
1231
|
+
steps up in INK, never into a heavier weight: both states are `medium`, and what says "this
|
|
1232
|
+
one is on" is the doubled zinc-900 edge plus default ink. Semibold is a separate, genuinely
|
|
1233
|
+
heavier font FILE and the heading ramp is semibold at every rung, so an active chip in
|
|
1234
|
+
semibold rendered a filter value at heading weight — and made itself a singleton treatment on
|
|
1235
|
+
every screen carrying one, while `Tabs`, which answers the same question, steps
|
|
1236
|
+
regular → medium. The row wraps by
|
|
1212
1237
|
default; `overflow="scroll"` keeps it to ONE row for a register FILTER, where six options
|
|
1213
1238
|
carrying counts wrap to three rows — 136px on a phone, more than the search band and the view
|
|
1214
1239
|
switcher combined. Deliberately NOT a screen-width branch: width is not the question, the job
|
|
@@ -1216,6 +1241,35 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1216
1241
|
chips are worth more than a `Picker` there only because every option is visible, and a swipe
|
|
1217
1242
|
is exactly what takes that away. → `data_entry.md` §"Choosing a CHOICE control" for which
|
|
1218
1243
|
control the count and the job call for.
|
|
1244
|
+
- **`toggle_strip`** — `ToggleStrip` (+ `toggleStripWidth`): N INDEPENDENT booleans across FIXED
|
|
1245
|
+
positions, drawn as one compact strip — which days a departure runs, which hours a gate is
|
|
1246
|
+
open, which channels a notice goes out on. Sits inside a register row or a field at
|
|
1247
|
+
`CONTROL_CONTENT_HEIGHT`, not on a control band.
|
|
1248
|
+
Not `ChipGroup` (one choice, and chips WRAP — which destroys the reading this exists for:
|
|
1249
|
+
a column of strips lets the eye run down position 3 and see the shape of the week), and not
|
|
1250
|
+
`SegmentedControl` (an exclusive choice, a radiogroup — each cell here is a `checkbox`).
|
|
1251
|
+
`minOne` refuses to turn the LAST cell off, for a set where empty is a DIFFERENT reading
|
|
1252
|
+
rather than a neutral one. Each option carries a `label` (the one or two characters shown)
|
|
1253
|
+
AND a `name` (what a screen reader says) — a two-letter cell announces as nonsense, and seven
|
|
1254
|
+
of them in a register are told apart only by voice. Budget the table column with
|
|
1255
|
+
`toggleStripWidth(n)`, never a guess: a guessed column is one the fit sheds for the wrong
|
|
1256
|
+
reason. Selected wears `chipSurfaceStyle`'s doubled dark EDGE, never a brand ground — a ground
|
|
1257
|
+
is the row's language and this is a control, and seven filled cells per row made the screen's
|
|
1258
|
+
one primary button the twenty-third copy of its own ground.
|
|
1259
|
+
**`variant="mark"`** drops the letters for a bare square (filled = on) — for a COLUMN of strips
|
|
1260
|
+
whose positions a header names once, which is what `Timetable` does; `current` tints the cell
|
|
1261
|
+
of the position the reader stands in (today). `TOGGLE_STRIP_CELL_WIDTH` / `_GAP` are the
|
|
1262
|
+
geometry a header lays its labels on.
|
|
1263
|
+
- **`timetable`** — `Timetable`: slots down the side, a FIXED repeating axis across the top, a
|
|
1264
|
+
mark wherever a slot is on — a departure board (time × weekday), a shift roster, a lane's
|
|
1265
|
+
opening hours. `slots` are the positions (`label` shown once in the header, `name` for the
|
|
1266
|
+
cells' announcement), `current` is today's column (tinted, `aria-current="date"`), `rows` carry
|
|
1267
|
+
a `time`, the `active` positions, a `subject` node (put the identity mark here), and optionally
|
|
1268
|
+
`status`, `trailing` (an `ActionMenu`), and `detail` + `expanded` + `onPress` — the row's time and
|
|
1269
|
+
subject are TEXT and edit in the expansion, since a timetable is read daily and changed a few
|
|
1270
|
+
times a month; the pattern is the one thing edited in place. Not `CalendarView`: nothing has a
|
|
1271
|
+
DATE, so there is no week to page to and no duration to draw. Not a `Table` of `ToggleStrip`s
|
|
1272
|
+
with letters in every cell — that names the axis on every row.
|
|
1219
1273
|
- **`checkbox`** — `Checkbox`: the bare square check control. Bare means UNLABELLED — reach for
|
|
1220
1274
|
`CheckboxInput` unless something else already names it (a table's select-all, a row's
|
|
1221
1275
|
leading slot). A square means CHOSEN; a ring means DONE, and spending the wrong one teaches a
|
|
@@ -1309,6 +1363,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1309
1363
|
the next click produces; hovering backwards from the start previews just as well.
|
|
1310
1364
|
- **`date_filter`** — `DateFilter`: the date+time period filter panel — presets
|
|
1311
1365
|
(`PresetId`), calendar, optional time segments; the body `DateRangeFilterField` opens.
|
|
1366
|
+
The presets run day → week → month → quarter → year (`today`, `yesterday`, `tomorrow`,
|
|
1367
|
+
`this_week`, `this_month`, `last_month`, `this_quarter`, `last_quarter`, `this_year`,
|
|
1368
|
+
`last_year`), shortest grain first, so a reader scans past the days to the grain they mean.
|
|
1312
1369
|
**Every preset SETS a range** — the list holds no mode and no null case, so a hand-picked
|
|
1313
1370
|
range selects nothing. The
|
|
1314
1371
|
presets are a `listbox` of `MenuButton` options: selection is the kit's `zinc.100` row
|
|
@@ -1321,7 +1378,38 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1321
1378
|
renders it as a single date). Both bounds always show, so a half-picked range names the
|
|
1322
1379
|
end that is still open.
|
|
1323
1380
|
- **`date_range_filter_field`** — `DateRangeFilterField`: the register's period filter field
|
|
1324
|
-
(presets + footer + time-segment a11y; localized via the `dateRange` locale slice).
|
|
1381
|
+
(presets + footer + time-segment a11y; localized via the `dateRange` locale slice). **A range
|
|
1382
|
+
that IS a whole period reads as that period** on the trigger — a whole month "Tháng 9 năm
|
|
1383
|
+
2026", a whole quarter "Quý 3 năm 2026", a whole year "2026", one day one date; only a range
|
|
1384
|
+
that is no period falls back to a date pair. That is what lets one field serve a dashboard
|
|
1385
|
+
that lives in period rhythm, so never put a grain `SegmentedControl` beside it. For a
|
|
1386
|
+
surface keyed by a whole MONTH — a plan, a roster — reach for `month_stepper` instead: a
|
|
1387
|
+
month-keyed record cannot take "the 3rd to the 19th", so a range field offers moves that do
|
|
1388
|
+
not exist.
|
|
1389
|
+
- **`date_filter_presets`** — `PRESET_IDS` / `getPresetValue(id, now)` / **`previousPeriod(value)`** /
|
|
1390
|
+
**`periodToDate(value, now)`**;
|
|
1391
|
+
the panel's range math, React-free and computed off an INJECTED `now` so a host can resolve or
|
|
1392
|
+
compare a period without rendering anything. `previousPeriod` is the comparator a report means
|
|
1393
|
+
by "so với kỳ trước": a whole calendar month, quarter or year steps back one WHOLE period
|
|
1394
|
+
(February against March is 28 days against 31 — sliding a 31-day window back would compare
|
|
1395
|
+
March against three days of January), any other complete range steps back by its own length
|
|
1396
|
+
ending the day before it starts, and an open range returns `null` because it names no period.
|
|
1397
|
+
Bounds come back day-aligned and untimed, like a preset's. Feed the result to the same query
|
|
1398
|
+
the selected range feeds and show the delta ON the figure — never as a second control beside
|
|
1399
|
+
the field. `periodToDate` is what a report keyed by a period actually reads: while the period
|
|
1400
|
+
is still RUNNING it cuts both sides to the days elapsed — the selection ending today, the
|
|
1401
|
+
comparator cut to the same count from its own first day and clamped to that period's end — and
|
|
1402
|
+
reports `running` / `elapsedDays` / `totalDays` for the caption. Reach for it over a bare
|
|
1403
|
+
`previousPeriod` on any figure the reader may open mid-period, or six days of this month get
|
|
1404
|
+
compared against a whole month and the headline states a collapse that never happened.
|
|
1405
|
+
→ [composition.md §"Period filters for time-constrained data"](./composition.md).
|
|
1406
|
+
- **`month_stepper`** — `MonthStepper`: ‹ Tháng 9 năm 2026 › — `value` (any day of the month, as
|
|
1407
|
+
`yyyy-MM-dd`), `onChange` (always the month's first day), `max` / `min` (the last / first
|
|
1408
|
+
month that may be stepped INTO — pass this month on a surface about what has happened, or
|
|
1409
|
+
the reader walks into a month nobody has planned), `labels` (the two arrows' names; the
|
|
1410
|
+
`monthStepper` locale slice). The month name comes from `formatDate`'s `monthYear`.
|
|
1411
|
+
- **`month_math`** — `monthStart` / `shiftMonth`: the stepper's arithmetic on `yyyy-MM-dd`
|
|
1412
|
+
strings, React-free, for a host that keys its own state by month.
|
|
1325
1413
|
- **`time_picker`** — `TimePicker`: the time-of-day FIELD. The value on a pressable
|
|
1326
1414
|
control (the bordered surface a `Select` uses, so it reads as pickable by the same
|
|
1327
1415
|
shape as every other field that opens something), and `TimeColumns` behind it.
|
|
@@ -1513,8 +1601,12 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1513
1601
|
control line while a
|
|
1514
1602
|
one-line label still centers on it (both modes, no prop). The FIELD-ANNOTATION vocabulary (same
|
|
1515
1603
|
names + meanings as `FormField`), always EXPLICIT — a row never hides guidance behind an ⓘ:
|
|
1516
|
-
**`description`** = a fact / persistent guidance (muted), under the VALUE
|
|
1517
|
-
|
|
1604
|
+
**`description`** = a fact / persistent guidance (muted), under the VALUE at BOTH widths —
|
|
1605
|
+
stacked mode wears the form LOOK (a `FormField`-weight label) but keeps the RECORD's reading
|
|
1606
|
+
order, label then value then annotation. Not the form ORDER: a form's description is guidance
|
|
1607
|
+
you need BEFORE typing, a record's qualifies a value that already exists, so ahead of it the
|
|
1608
|
+
reader gets "còn 116 ngày" before any date. Both modes render the shared `FieldAnnotations`
|
|
1609
|
+
anatomy, so a fault says itself one way at both widths; **`warning`** = a consequence to weigh
|
|
1518
1610
|
before acting (amber, announced); **`error`** = field-level failure under the value, danger +
|
|
1519
1611
|
alert semantics (the `Inline*` editors already render their own transient save errors — don't
|
|
1520
1612
|
wire both); invalid/cross-field STATE = a co-located `Callout`; **`flat`**
|
|
@@ -1582,7 +1674,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1582
1674
|
rows), `LedgerTotal` = the divider-set emphasized close with `zeroLabel` for settled. **`meta` is a
|
|
1583
1675
|
neutral QUALIFIER** (a date, a method) at caption weight — never a problem or a state: a
|
|
1584
1676
|
caption is the wrong weight for something wanting action. A problem goes on the row
|
|
1585
|
-
that can FIX it.
|
|
1677
|
+
that can FIX it. **The label outranks the caption when the row runs out of width**: the
|
|
1678
|
+
caption is sized off what the label and the figure leave, so it clips and is down to
|
|
1679
|
+
nothing before the label gives up a word — and under a 320px ledger the row drops it
|
|
1680
|
+
outright, since a caption clipped to two syllables is worth less than the space.
|
|
1586
1681
|
**Pick the shape by what the statement IS**, because there are two and they read differently:
|
|
1587
1682
|
- **`charges → total`** — `LedgerGroup`s, each **closed by its own sum** ("Charges" vs
|
|
1588
1683
|
"Received"), giving three ascending rungs: row `sm/regular`, subtotal `sm/medium`, total
|
|
@@ -1732,10 +1827,28 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1732
1827
|
A `description` passed as a NODE keeps its own leading — the component only sets it on the
|
|
1733
1828
|
string branch, because forcing a line box on somebody else's subtree re-spaces content that
|
|
1734
1829
|
may not be a line of text at all.
|
|
1830
|
+
**`selected` outranks hover, and the two never paint the same grey.** The ladder is
|
|
1831
|
+
`PressableRow`'s, verbatim — zinc-50 hovered, zinc-100 selected, zinc-200 pressed, one
|
|
1832
|
+
neutral step apart with HOVER LIGHTER — so pointing at a neighbour cannot erase which row is
|
|
1833
|
+
open. The two used to share `zinc-100`, which is invisible to every probe that reads a
|
|
1834
|
+
RESTING screen and fails hardest on a navigation pane, where the selection is the only thing
|
|
1835
|
+
tying a chooser to the detail it opened.
|
|
1735
1836
|
- **`cell_stack`** — `CellStack`: a SUBJECT over one qualifying line, as a value primitive —
|
|
1736
1837
|
the commonest cell in any register, and the shape most often hand-rolled wrong. `title` +
|
|
1737
1838
|
`caption`, plus `weight` / `marked` / `size` / `color` / `tabular` / `align` /
|
|
1738
|
-
`numberOfLines`.
|
|
1839
|
+
`numberOfLines`. `leading` draws the identity mark (an `Avatar`, a `FileBadge`) before the
|
|
1840
|
+
pair and owns the mark-to-text gap — the subject cell of a register is `CellStack leading`,
|
|
1841
|
+
never a hand-rolled row of mark + stack; the column still budgets the mark's width. That gap
|
|
1842
|
+
is exported as `LEADING_GAP`, the ONE number for mark-to-label anywhere the kit draws the pair
|
|
1843
|
+
(`Matrix` and `StateMatrix` rows read it too) — a caller drawing its own pair reads it rather
|
|
1844
|
+
than picking a number.
|
|
1845
|
+
To RESERVE the caption line on a row that has nothing to say — which is how a column whose
|
|
1846
|
+
cells are sometimes two lines keeps ONE first-line height down the page — pass a whitespace
|
|
1847
|
+
string. `" "` is what everybody writes and the web renderer collapses it, so `CellStack`
|
|
1848
|
+
normalises whitespace to a non-breaking space itself: without that the cell drops to one line
|
|
1849
|
+
and its first line rides half a line above the rows that DID grow a caption, while the ROW
|
|
1850
|
+
height, set by whichever sibling is two lines tall, never moves — so no spacing probe sees it.
|
|
1851
|
+
(One app wrote the bare space twelve times.)
|
|
1739
1852
|
It OWNS the three-part stacked-pair rule that [composition.md](./composition.md) §Typography
|
|
1740
1853
|
states, so a call site never re-derives it: the caption's rung follows **what else already
|
|
1741
1854
|
separates the pair** (a `medium` subject or a leading identity mark ⇒ the caption keeps body
|
|
@@ -1750,15 +1863,22 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1750
1863
|
from the LINE BOX, so no margin and no gap appears anywhere to explain it and no spacing
|
|
1751
1864
|
probe can see it — it is reported as "too far apart", never as its cause.
|
|
1752
1865
|
- **`table`** — `Table` + `TableRow` + `TableCell`: the paginated high-volume register
|
|
1753
|
-
(columns defined once, `sortLabels` localizable; rows are `PressableRow`-based).
|
|
1866
|
+
(columns defined once, `sortLabels` localizable; rows are `PressableRow`-based). A column
|
|
1867
|
+
whose header is a set of POSITIONS rather than a word passes `header` (a node) beside its
|
|
1868
|
+
`label` — `Timetable`'s seven weekdays — never as a way to restyle a label.
|
|
1754
1869
|
CONTAINER-RESPONSIVE with no prop (measures itself, like `Breakdown`/`DetailTable`): when
|
|
1755
1870
|
the width can't fit every column it hides columns by `TableColumn.priority` (**`1` = highest,
|
|
1756
1871
|
as in P1/P2/P3** — the smallest number is the last to go; default = column order, rightmost
|
|
1757
1872
|
first; column 0 — the identity — never drops), and
|
|
1758
1873
|
below the two-column floor every row STACKS: the identity cell (column 0) HEADS the pile
|
|
1759
1874
|
unlabelled — its column name would only restate the value — and each REMAINING cell renders
|
|
1760
|
-
as a `DetailRow
|
|
1761
|
-
|
|
1875
|
+
as a `DetailRow` inside a `DetailTable`, so the field lines wear the FORM grammar: one fixed
|
|
1876
|
+
label column, every value on ONE left edge, one vocabulary with the drawer's detail rows. The
|
|
1877
|
+
lines sit on the identity column's text edge, never under the checkbox.
|
|
1878
|
+
**A SPREAD row is right for one detail value and wrong for a register of them** — that is
|
|
1879
|
+
what the narrow fork used to render, and repeated down twenty-one rows the same label string
|
|
1880
|
+
measured seven distinct widths while its value started at a different x on every row, so the
|
|
1881
|
+
fork had no value column at all. Invisible at desk width, where the fork never runs;
|
|
1762
1882
|
the header band gives way, so sorting is a wide-container affordance. Rows carry TWO right-side slots: `action`
|
|
1763
1883
|
(the primary CTA `Button` — register: in the trailing gutter; stacked: closes the content,
|
|
1764
1884
|
right-aligned) and `trailing` (the ⋯ overflow — stays beside the identity on the top line).
|
|
@@ -1965,6 +2085,13 @@ a child of the app root, which every overlay's body-level box follows.
|
|
|
1965
2085
|
and a PINNED one (a heading or summary above a scrolling list), which puts the exported
|
|
1966
2086
|
`DRAWER_GUTTER` on the pinned part and on the scroller's `contentContainerStyle` rather than
|
|
1967
2087
|
copying `20`.
|
|
2088
|
+
**`position` + `onPrev`/`onNext` make it a RECORD navigator, and below the split width that
|
|
2089
|
+
navigation drops to its own line** so the title keeps the full bar. The title names the record
|
|
2090
|
+
— whose file is open — and it is the only element on that bar that CAN give way, since the
|
|
2091
|
+
pager and the three icon buttons are all fixed widths; sharing one line at 375 cut a record's
|
|
2092
|
+
own name at 206 of the 261px it needed while the chrome beside it sat whole. Pass a NODE title
|
|
2093
|
+
(name over a qualifying line) and give its first line `lg` — that is the rung a string title
|
|
2094
|
+
already takes, and at `md` a record reads as a peer of the section headings inside it.
|
|
1968
2095
|
- **`modal`** — `Modal` + `ModalHeader` + `ModalBody` + `ModalFooter` — the full-bleed,
|
|
1969
2096
|
edge-to-edge takeover: an OPAQUE surface that COVERS THE WHOLE SCREEN, so unlike Dialog
|
|
1970
2097
|
(centered card WITH scrim) and Drawer (docked panel WITH scrim) there is nothing behind it
|
|
@@ -2053,10 +2180,19 @@ a child of the app root, which every overlay's body-level box follows.
|
|
|
2053
2180
|
jump both re-measure the live DOM on web, so content loading in above a section never
|
|
2054
2181
|
strands them. Reaching the END of the scroll activates the LAST key regardless of offsets —
|
|
2055
2182
|
a final section shorter than the viewport never scrolls its top past the trigger line.
|
|
2056
|
-
|
|
2057
|
-
the rail becomes a PINNED bar naming the CURRENT section that opens a full-page
|
|
2058
|
-
section-picker `Modal` — never a horizontal tab strip. Worked example:
|
|
2183
|
+
Pair it with `SectionNav`, which draws both forms. Worked example:
|
|
2059
2184
|
[`tpl_record`](../examples/tpl_record.tsx).
|
|
2185
|
+
- **`section_nav`** — `SectionNav`: the CHROME half of the outline, in its two forms — a RAIL
|
|
2186
|
+
in a gutter, or a pinned bar naming the CURRENT section that opens the same list as a picker,
|
|
2187
|
+
never a horizontal tab strip. `wide` picks the form and the CALLER measures its own container
|
|
2188
|
+
for it (a window read gives a drawer the page's answer); `items` carry an `issue` dot, which is
|
|
2189
|
+
what makes the outline answer "where is the problem" and not merely "where am I" and which is
|
|
2190
|
+
ANNOUNCED as well as coloured, since it is the only thing marking the section; `back` leads
|
|
2191
|
+
both forms, omitted where the container already has an exit. The active entry is `current`
|
|
2192
|
+
(→ `aria-current`), never `selected` — a rail item is navigation, not listbox selection. The
|
|
2193
|
+
picker anchors to the bar's own button, so it stays inside whatever container that bar sits in.
|
|
2194
|
+
It owns neither the gutter the rail floats in, nor the width threshold, nor which sections are
|
|
2195
|
+
in trouble.
|
|
2060
2196
|
- **`scroll_to_bottom`** — `ScrollToBottom`: the floating jump-to-latest circle button for a
|
|
2061
2197
|
chat/feed; ONLY the affordance — the caller owns positioning, visibility, and the actual
|
|
2062
2198
|
scroll.
|
|
@@ -2095,7 +2231,24 @@ component rather than showing it at zero.
|
|
|
2095
2231
|
it carries SHAPE (rising, spiky, flat) and no readable values, which is exactly what a
|
|
2096
2232
|
register wants. Reach for a real chart the moment someone needs to read a value off it.
|
|
2097
2233
|
- **`bar_chart`** / **`line_chart`** / **`pie_chart`** — `BarChart` / `LineChart` /
|
|
2098
|
-
`PieChart`: the canonical SVG chart set (no recharts).
|
|
2234
|
+
`PieChart`: the canonical SVG chart set (no recharts). `LineChart` takes EITHER `points`
|
|
2235
|
+
(one line, its own `lineColor`) OR **`series: { key, label, color?, points }[]`** — never
|
|
2236
|
+
both; the union makes passing two, or neither, a compile error. **Several series share ONE
|
|
2237
|
+
y-scale**, which is the whole reason they are one chart: per-series scales would draw two
|
|
2238
|
+
different quantities at the same height and the picture would state nothing. So a
|
|
2239
|
+
COMPARISON — this period against last, this depot against that one — is two series on one
|
|
2240
|
+
chart, never two charts side by side and never a control that swaps one for the other. The
|
|
2241
|
+
x axis is shared too: position `i` is position `i` for every series, and the label comes
|
|
2242
|
+
from whichever series reaches it, so a shorter series just ends early. **`legend` is on by
|
|
2243
|
+
default once there is more than one series** (several unnamed lines are a picture nobody can
|
|
2244
|
+
read) and off for a single one, which the card around it already names; the anatomy is the
|
|
2245
|
+
same `LegendItem` every other chart annotates itself with. A series that names no `color`
|
|
2246
|
+
takes the next one off the chart's own order, so four series are four distinguishable lines
|
|
2247
|
+
with nothing to pass. Each named series is a `role="img"` carrying its label — a line
|
|
2248
|
+
announces as nothing otherwise. The area wash and the per-reading markers are SINGLE-line
|
|
2249
|
+
devices: stacked washes muddy into a colour belonging to no series, and four lines' markers
|
|
2250
|
+
read as a field of dots, so several series draw bare with only their latest reading marked.
|
|
2251
|
+
**A categorical axis prints as many
|
|
2099
2252
|
labels as the track fits and thins the rest** — `LineChart` and `BarChart`'s vertical
|
|
2100
2253
|
orientation both, because it is the axis's rule and not one chart's. It is anchored on the LAST
|
|
2101
2254
|
position: the newest reading is the one a reader looks up, and anchoring there is what keeps the
|
|
@@ -2112,7 +2265,18 @@ component rather than showing it at zero.
|
|
|
2112
2265
|
`Table` or the horizontal orientation, which gives each entity its own row and its own value
|
|
2113
2266
|
column. `BarChart`'s horizontal orientation is one row per entity: **`labelWidth`**
|
|
2114
2267
|
sizes the fixed label column (default 80, which fits a date and not a company name), and each
|
|
2115
|
-
item's **`leading`** slot carries that entity's own mark ahead of its name.
|
|
2268
|
+
item's **`leading`** slot carries that entity's own mark ahead of its name. **The value axis
|
|
2269
|
+
reads the VALUES, not just their maximum, so an axis over whole things never prints a half** —
|
|
2270
|
+
a series of counts gets whole-number gridlines however small its maximum, and only genuinely
|
|
2271
|
+
fractional data gets fractional ones. Nothing to pass; it follows the data. **That axis thins
|
|
2272
|
+
its labels too**, by the same rule the categorical one runs: its ticks are positioned by
|
|
2273
|
+
fraction, so a narrow track does not crowd them, it STACKS them into an unreadable smudge, and
|
|
2274
|
+
the tick marks stay because a 1px line collides with nothing. And **`labelWidth` is a REQUEST,
|
|
2275
|
+
not a reservation**: the label and value columns are fixed, so in a narrow pane they would take
|
|
2276
|
+
the whole width and leave every bar on its 2px floor — a row of identical stubs claiming every
|
|
2277
|
+
entity is equal — so the label column gives way first and the plot keeps a readable minimum. A
|
|
2278
|
+
trimmed name beside a real bar says more than a full name beside a stub; ask for the width the
|
|
2279
|
+
names want and let the chart settle it.
|
|
2116
2280
|
- **`progress_bar`** — `ProgressBar`: the determinate meter; `compact` = ONE row, track + a
|
|
2117
2281
|
plain sm tabular count beside it; `max: null` + `unboundedLabel` = the same meter with no bound.
|
|
2118
2282
|
- **`progress_ring`** — `ProgressRing`: the same meter in a circle, and the ONLY circular one. Same API shape as the bar — a real `value`/`max` rather than a
|
|
@@ -2168,26 +2332,56 @@ component rather than showing it at zero.
|
|
|
2168
2332
|
`onSelect`/`selectedKey` press-to-drill; the subset/drop-off sibling of
|
|
2169
2333
|
`stacked_progress_bar`'s whole-split.
|
|
2170
2334
|
- **`status_grid`** — `StatusGrid` + `StatusLegend`: the per-slot state grid (occupancy,
|
|
2171
|
-
availability) and its legend.
|
|
2335
|
+
availability) and its legend. A PILE of units with no axes — when the reader compares a
|
|
2336
|
+
subject against a date, that is `state_matrix`.
|
|
2337
|
+
- **`state_matrix`** — `StateMatrix`: the CATEGORICAL cross-tab — `rows` (a subject each:
|
|
2338
|
+
`label`, an identity `leading`, a pinned `total`, one cell per column), `columns` (dated
|
|
2339
|
+
positions named ONCE in the header, `caption` for a second line, `current` for the one the
|
|
2340
|
+
reader stands in — tinted down the grid, `aria-current`), `states` (key, label, palette
|
|
2341
|
+
`color`, `shape` filled | hollow | dot | none — the legend derives from this list, so it can
|
|
2342
|
+
never name a glyph the cells do not draw). A cell carries a `name` (a glyph announces as
|
|
2343
|
+
nothing) and is a `button` only when `pressable` — `onPressCell(rowKey, colKey)` opens what
|
|
2344
|
+
it has to say. The label block is PINNED outside the grid's own horizontal scroll. Not
|
|
2345
|
+
`Matrix` (nothing here is a number; blank is "not due", never zero), not `Timetable` (a
|
|
2346
|
+
timetable's positions are a dateless pattern and every mark is its own editor). Worked
|
|
2347
|
+
example: `examples/tpl_attendance.tsx` §This week.
|
|
2172
2348
|
- **`heatmap`** — `Heatmap`: density cross-tab — colour-only, no numbers; "where does it
|
|
2173
2349
|
cluster".
|
|
2174
2350
|
- **`matrix`** — `Matrix`: the PIVOT cross-tab — band-compound `Matrix` root +
|
|
2175
2351
|
`Matrix.Header` (corner + axis labels) + `Matrix.Grid` (`display` number|heat|both — the
|
|
2176
2352
|
cells, pressable, the value IN the cell) + `Matrix.Totals` (row + column + grand) +
|
|
2177
|
-
`Matrix.Legend`. Pick over `Heatmap` when the NUMBER and totals matter.
|
|
2353
|
+
`Matrix.Legend`. Pick over `Heatmap` when the NUMBER and totals matter. **The heat is a RANK
|
|
2354
|
+
scale behind numbers** (`heatScale`, default `rank` for `display="both"`, `linear` for a pure
|
|
2355
|
+
heatmap): a linear map of value ÷ largest-cell spends the whole band on the one outlier every
|
|
2356
|
+
real sheet has and leaves the rest inside a step the eye cannot resolve — a white grid with one
|
|
2357
|
+
dark square. Pass `heatScale="linear"` only where the wash is the sole statement of MAGNITUDE.
|
|
2358
|
+
`Matrix.Legend` samples the swatches out of that same scale rather than listing its own alphas,
|
|
2359
|
+
so it cannot advertise a shade no cell can wear, and it renders nothing when no cell is painted.
|
|
2360
|
+
**An empty interior cell says ZERO, not "no data"** — the grid has no third state, so a column
|
|
2361
|
+
the source has not reached yet is the caller's to leave out. Figures are
|
|
2178
2362
|
RIGHT-aligned — they are `tabular`, and centring them defeats the aligned column — and row
|
|
2179
|
-
labels carry default ink, because a row's label is its identity, not chrome. **
|
|
2363
|
+
labels carry default ink, because a row's label is its identity, not chrome. **A ROW may carry
|
|
2364
|
+
a `leading` identity mark** (an `Avatar`, a status dot), drawn inside `rowLabelWidth` so it eats
|
|
2365
|
+
the label's width rather than widening the column — which is how a matrix shows the same mark
|
|
2366
|
+
the chart beside it shows. Rows only: a column is a period or a category and has no identity, so
|
|
2367
|
+
`Matrix.Header` ignores `leading`, and the totals band carries no mark either. **The totals column
|
|
2180
2368
|
takes one flex share like a data column**, because a total is the longest figure in its row: on a
|
|
2181
2369
|
fixed width it is the FIRST thing to clip, and a clamped figure is whole in the DOM, so nothing
|
|
2182
|
-
but a rendered screen sees it.
|
|
2183
|
-
|
|
2184
|
-
its
|
|
2370
|
+
but a rendered screen sees it. **The grid owns its own horizontal scroll**: past the width where
|
|
2371
|
+
every cell still meets its floor it scrolls rather than compressing, so a matrix with many columns
|
|
2372
|
+
keeps its row labels and its totals column instead of squeezing the label to one glyph and pushing
|
|
2373
|
+
the totals off the edge. That is the component's answer to a wide column axis — never the page
|
|
2374
|
+
body, and never a caller's own wrapper. A date axis is still the caller's to BOUND: a sheet that
|
|
2375
|
+
reaches past the last day with data buys empty columns that the ones with numbers then pay for.
|
|
2376
|
+
`MATRIX_COL_GAP` is exported for a caller budgeting its own column widths. **A money matrix states its unit once** — in the card's
|
|
2185
2377
|
`CardHeaderMeta`, cells bare (`formatCompactNumber`, not `formatMoney`), per
|
|
2186
2378
|
[composition.md](./composition.md) §"A column states its unit ONCE": one currency glyph per cell
|
|
2187
2379
|
is the same word fifty times and it is what pushes the totals column past its share.
|
|
2188
2380
|
- **`matrix_totals`** — the data layer for `Matrix`: `matrixTotals` (the cross-tab
|
|
2189
|
-
aggregation) + `
|
|
2190
|
-
|
|
2381
|
+
aggregation) + `matrixHeatScale` (value → alpha, and the legend sampled from that same
|
|
2382
|
+
function) + `MatrixAxisItem` (`key`, `label`, and a rows-only `leading` mark — a TYPE-only
|
|
2383
|
+
node, so nothing here runs) / `MatrixCellRef` / `MatrixTotalsResult` / `MatrixHeatScale`;
|
|
2384
|
+
React-free, so a KPI can be driven off the same numbers the grid shows.
|
|
2191
2385
|
- **`highlight_segments`** — `splitHighlightSegments`: cuts a string into plain and
|
|
2192
2386
|
highlighted runs for `Composer`'s `highlightRanges`. React-free: it feeds a mirror sitting
|
|
2193
2387
|
behind a real text field, so a boundary off by one paints the tint adrift while everything
|
|
@@ -2764,7 +2958,11 @@ component rather than showing it at zero.
|
|
|
2764
2958
|
`cafe` finds `Café` and `da giac` finds `Đa Giác`. ONE definition, and every list that filters
|
|
2765
2959
|
uses it — two rules is not a style problem: a reader who found a row by typing unaccented text
|
|
2766
2960
|
in one list and finds nothing in the next reads the second as missing data. `OptionList`,
|
|
2767
|
-
`Combobox` and `Switcher` all match through it.
|
|
2961
|
+
`Combobox` and `Switcher` all match through it. `codeKey(s)` / `codeMatches(query, ...fields)`:
|
|
2962
|
+
the same fold for a CODE — a plate, a container number, a voucher number — dropping every
|
|
2963
|
+
separator on both sides, so `14b031` and `031.02` both find `14B-031.02`; still a substring
|
|
2964
|
+
test, never a subsequence one. A register searched by an identifier matches through this,
|
|
2965
|
+
never through its own regex.
|
|
2768
2966
|
- **`use_async_fn`** — `useAsyncFn`: wrap an async function into a manual-trigger mutation —
|
|
2769
2967
|
`[run, {loading, data, error}]`, unmount-safe, the error lands in state AND rethrows; the
|
|
2770
2968
|
pending-state engine for a submit/download/upload action.
|
package/docs/composition.md
CHANGED
|
@@ -398,6 +398,38 @@ badge. Every period-dependent number MUST follow the selection. Pass `includeTim
|
|
|
398
398
|
time-of-day matters: the trigger previews the chosen time (locale-aware) and the `labels` prop
|
|
399
399
|
translates the presets, footer buttons, placeholder, and the time-segment editors.
|
|
400
400
|
|
|
401
|
+
**One field, every grain — a `SegmentedControl` of grains beside it re-adds the mode the field
|
|
402
|
+
removed.** The presets already run day → week → month → quarter → year, and the trigger folds a
|
|
403
|
+
whole period back to its own name ("Tháng 9 năm 2026", "Quý 3 năm 2026", "2026"), so the grain is
|
|
404
|
+
something the reader SETS and READS in one control. Putting *Tháng | Quý | Năm* next to it splits
|
|
405
|
+
one fact across two controls that can disagree — pick "Quý" beside a range of eleven days and the
|
|
406
|
+
screen states a grain nothing on it is computed at.
|
|
407
|
+
|
|
408
|
+
**A report's comparator is DERIVED from the selected range, never chosen.** "So với kỳ trước" is
|
|
409
|
+
`previousPeriod(value)` from `@lotics/ui/date_filter_presets` — a whole month steps back a whole
|
|
410
|
+
month, a quarter a quarter, a year a year, and anything else steps back by its own length. Feed
|
|
411
|
+
that range to the same query the selection feeds and put the result ON the figure: a
|
|
412
|
+
`KPIStripItem`'s `trend` / `caption`, `Metric`'s `previousValue`, a second `LineChart` series
|
|
413
|
+
(`series` is what makes two periods one chart on one scale), `TrendFooter` under a chart card. A
|
|
414
|
+
second date field, a "compare to" picker or a comparison toggle beside the filter is the same
|
|
415
|
+
mistake in three shapes — it lets a reader select a comparator that is not the period they are
|
|
416
|
+
looking at, and every figure on the screen then answers a question nobody asked. `null` back means
|
|
417
|
+
the range is open-ended: show the figure with no delta rather than inventing one.
|
|
418
|
+
|
|
419
|
+
**A period still RUNNING is compared TO DATE — `periodToDate(value, now)`.** Six days of this
|
|
420
|
+
month set beside a whole previous month reads as a collapse that never happened, and it lands on
|
|
421
|
+
the first figure anyone looks at, so both sides are cut to the days that have elapsed: it hands
|
|
422
|
+
back the selection ending today, the comparator cut to the same count from its own first day
|
|
423
|
+
(clamped to that period's end, so 30 days of March meet all 28 of February), and
|
|
424
|
+
`running` / `elapsedDays` / `totalDays` for the caption that says how far in the reader is. A
|
|
425
|
+
closed period keeps both sides whole.
|
|
426
|
+
|
|
427
|
+
**A surface keyed by a whole month takes a `MonthStepper`, not a range field.** A monthly plan, a
|
|
428
|
+
roster, a month's attendance cannot take "the 3rd to the 19th", so a range control offers moves
|
|
429
|
+
that do not exist. The stepper's bound is its contract: pass `max` as this month on any surface
|
|
430
|
+
about what has happened, or the reader walks into a month nobody has planned and reads it as a
|
|
431
|
+
plan. Two hand-rolled steppers in one app disagreed on exactly this.
|
|
432
|
+
|
|
401
433
|
## A calendar answers WHEN — and only when the position on a timeline is the question
|
|
402
434
|
|
|
403
435
|
A calendar is not a prettier register. Reach for one when the reader's question is *when does
|
|
@@ -406,6 +438,19 @@ at the same time. When the question is *which of these needs me*, the answer is
|
|
|
406
438
|
a date column, and a calendar buries it: thirty rows of a table are scannable, thirty rows
|
|
407
439
|
scattered across a month grid are a search.
|
|
408
440
|
|
|
441
|
+
**A weekly PATTERN with no dates is a `Timetable`, not a calendar.** Which days a departure
|
|
442
|
+
runs, which hours a lane is open: nothing has a date to be placed on, so a week view pages
|
|
443
|
+
between identical weeks and draws the same four facts seven times. Rows are the slots, the
|
|
444
|
+
fixed positions are columns named once in the header, today is a tinted column, and a mark is
|
|
445
|
+
its own editor.
|
|
446
|
+
|
|
447
|
+
**A subject-by-date sheet of STATES is a `StateMatrix`.** Who was in on which day, which vehicle
|
|
448
|
+
ran on the 14th: the reader compares a subject against a date and reads a category, not a
|
|
449
|
+
number. Rows are the subjects with a pinned name and figure, the dates are columns named once,
|
|
450
|
+
a glyph per cell says the state, and the legend is derived from the same list the cells draw
|
|
451
|
+
from. A calendar would scatter thirty subjects across a month grid; a `Matrix` would put a
|
|
452
|
+
number where a blank means "nothing was due".
|
|
453
|
+
|
|
409
454
|
Three rules follow from that, and each of them is about legibility rather than taste.
|
|
410
455
|
|
|
411
456
|
**A grid is unreadable in two directions.** Seven equal columns need roughly 110px each to hold
|
|
@@ -647,6 +692,11 @@ form: per container, collect its own `row-gap` and the **max `row-gap` of its vi
|
|
|
647
692
|
different quantity. `ChargeLines` takes `formatMoney` and `formatQuantity` for this reason, both
|
|
648
693
|
at the BAND so two lines cannot disagree, and threads them to the locked value and the inline
|
|
649
694
|
editor alike. Any new component that displays a caller's number owes the same hook.
|
|
695
|
+
- **An axis over whole things has no half.** A count of vehicles, contracts or trips cannot be
|
|
696
|
+
0,5, so gridlines at `0 · 0,5 · 1 · 1,5 · 2` state something false about the SUBJECT rather than
|
|
697
|
+
about the layout, and they appear exactly where the numbers are smallest — a five-tick target
|
|
698
|
+
starts reaching below the unit at a maximum of two. The kit's charts derive this from the values
|
|
699
|
+
themselves; anywhere you draw a scale by hand, the step comes from what the data is made of.
|
|
650
700
|
- **A column states its unit ONCE — which is only possible if it holds ONE unit.** State it in the
|
|
651
701
|
header band and leave the cells bare. The corollary is the one that bites: a label/value list
|
|
652
702
|
whose rows are a date range, a sum of money and a COUNT has no single unit to state, so each
|
|
@@ -667,7 +717,8 @@ form: per container, collect its own `row-gap` and the **max `row-gap` of its vi
|
|
|
667
717
|
| `medium` | `22 thg 5, 2026` |
|
|
668
718
|
| `long` | `22 tháng 5, 2026` |
|
|
669
719
|
| `dayMonth` | `22 thg 5` |
|
|
670
|
-
| `monthYear` | `Tháng 5 2026` |
|
|
720
|
+
| `monthYear` | `Tháng 5 năm 2026` |
|
|
721
|
+
| `quarterYear` | `Quý 2 năm 2026` (`Q2 2026` outside Vietnamese) |
|
|
671
722
|
|
|
672
723
|
**`time: true` is orthogonal** — it prepends the 24h time to ANY style (`14:30 22/05/2026`,
|
|
673
724
|
`14:30 22 tháng 5, 2026`); `compact` drops the year on the `date` style; `emptyLabel` renders
|
|
@@ -1123,6 +1174,15 @@ When a region genuinely must sit beside the list (a source/remainder summary, a
|
|
|
1123
1174
|
make it a TOP summary header or move it into a `Popover`/sheet at the decision point — not a
|
|
1124
1175
|
standing column.
|
|
1125
1176
|
|
|
1177
|
+
**A screen is cut by the JOB, never by the subject.** Two jobs done at different hours — watching
|
|
1178
|
+
exceptions all day, signing a count once at its end — are two surfaces even when both are about
|
|
1179
|
+
the same day; a screen that stacks them puts the act done once under the list read least, and the
|
|
1180
|
+
one fact they share travels as a single line. The work screen opens on what the person DOES, then
|
|
1181
|
+
what they look up; an end-of-period act sits with the book it lands in — the register of periods
|
|
1182
|
+
above, the picked period's form below. A working screen summarises with `SummaryLine`; `KPIStrip`
|
|
1183
|
+
is a dashboard band. And a screen keyed by a period is named for the period it shows ("Trong
|
|
1184
|
+
ngày"), never for the default its filter happens to hold ("Hôm nay").
|
|
1185
|
+
|
|
1126
1186
|
## One control radius
|
|
1127
1187
|
|
|
1128
1188
|
Every interactive control (`Button`, the inputs/selects/pickers, `SearchInput`, `MenuButton`,
|
|
@@ -1594,6 +1654,19 @@ next step. When the state is already derivable from a neighbour, the badge is a
|
|
|
1594
1654
|
fact, free to disagree with the first, and it costs a column on every screen to say what the
|
|
1595
1655
|
reader already read.
|
|
1596
1656
|
|
|
1657
|
+
**A state the system DERIVES from an absence is pending until a person confirms it.** No chuyến
|
|
1658
|
+
past the slot, no reply past the deadline, no scan past the cut-off: the system can say the thing
|
|
1659
|
+
did not happen, never WHY, and the why is the only fact anyone acts on. Label such a row as the
|
|
1660
|
+
question ("Chưa xác nhận"), tone it as attention rather than failure, and put the confirming act
|
|
1661
|
+
on the row; the verdict word ("Nghỉ") appears only once the reason is on record. Painting the
|
|
1662
|
+
derived absence with the verdict spends the register's one alarm colour on the commonest row of
|
|
1663
|
+
the morning and accuses a row nobody has looked at yet.
|
|
1664
|
+
|
|
1665
|
+
**A strip whose every option shows the same register with fewer rows is a FILTER**, so it is
|
|
1666
|
+
`ChipGroup` with counts and `overflow="scroll"`, opening on the partition that owes the act.
|
|
1667
|
+
`Tabs` is for sections whose content differs; `PipelineTabs` for stages that change what the
|
|
1668
|
+
register under them IS.
|
|
1669
|
+
|
|
1597
1670
|
The clean discriminator is what the badge would say that the row does not:
|
|
1598
1671
|
|
|
1599
1672
|
- A gate register whose status reads *"paperwork not closed"* keeps its column. No other cell
|
|
@@ -1867,7 +1940,7 @@ information.
|
|
|
1867
1940
|
first word.** `avatarInitials` takes a name's first (and last) letters, which is right for a
|
|
1868
1941
|
person and wrong for an ORGANIZATION: an organization's name opens with its legal form, so a
|
|
1869
1942
|
register of companies marks the same letter on every row — and it is the same failure the derived
|
|
1870
|
-
hue was written to fix, arriving through the glyph instead of the ground. Strip the form from
|
|
1943
|
+
hue was written to fix, arriving through the glyph instead of the ground. Strip the form (`stripLegalForm`, `@lotics/ui/organization_name`) from
|
|
1871
1944
|
both ends before the name reaches the mark, and use the stripped name for the row's TITLE too:
|
|
1872
1945
|
whatever eats the first letters of a mark is also eating the first 60px of the column beside it,
|
|
1873
1946
|
where the words that tell two customers apart fall off the end. The legal name belongs on the
|
|
@@ -1904,8 +1977,14 @@ information.
|
|
|
1904
1977
|
subject. Keep the ordinal where order is the thing being EDITED, and then as text in the
|
|
1905
1978
|
supporting line — never as the mark. A surface that needs to state position has its own readout
|
|
1906
1979
|
(a drawer's `3/8`, a stepper's rail).
|
|
1907
|
-
- **The rung follows what the
|
|
1908
|
-
|
|
1980
|
+
- **The rung follows what the reader AIMS AT**: `lg` where the mark is the thing the eye lands on
|
|
1981
|
+
— the row's subject, or an ORGANIZATION in its own column, whose logo is what a reader scans a
|
|
1982
|
+
book of movements by. `md` only where the mark is incidental to a value the reader came for (see
|
|
1983
|
+
`avatar_size`). Read as "subject ⇒ `lg`, field column ⇒ `md`" this drew a 28px disc in a carrier
|
|
1984
|
+
column and the register was reported as flat — the same complaint the leading-mark rule above
|
|
1985
|
+
names, arriving through the rung instead of the absence. The budget is the real constraint: `lg`
|
|
1986
|
+
is 12px wider and takes them from the flexible column the name is in, so a column too tight to
|
|
1987
|
+
seat one takes `md` and says so.
|
|
1909
1988
|
- **A column of faces must not draw an UNBOUNDED audience as faces.** Where a cell answers "who
|
|
1910
1989
|
can reach this", the commonest state is often *everyone* — and three sampled avatars assert
|
|
1911
1990
|
something false about it. Draw the
|