# @lotics/ui — migration notes

**Crossing into 72.0.0.** Every breaking change in this major, with the call site before and after.
The current contract is `llms.txt` + `AGENTS.md` + `docs/` — this file exists only to move an app
from the release before this one to this one, and it is the one file in the tarball a reader opens
once. Older majors are `MIGRATION_ARCHIVE.md`, which stays in the repo: an app on 70 upgrades
through 71 first, and reads those notes there.

## 72.0.0

### Removed exports and peers

| Gone | Now | Why |
| --- | --- | --- |
| `FACE_BADGE_WIDTH` (`@lotics/ui/file_badge`) | `FileBadge.size`, the `MarkSize` square the page stands in; `tile` insets it in a slot-sized tile | A badge is sized by the mark scale, never a pixel width of its own. |
| `MediaViewLabels`, `LoticsLocale.mediaView` | `GalleryLabels` (`@lotics/ui/file_preview`) | A frame's file acts are the gallery's: one label set for open, rename, replace, remove and download. |
| `laneTiers` (`@lotics/ui/lane_board_layout`) | `blockTiers` | Blocks stack by their drawn width, so a widened short block pushes the one it would cover down instead of hiding it. |
| `useLeadRung` (`@lotics/ui/avatar`) | nothing: the kit's lists (`Table`, `FileRows`, `FilesEditor`) hold one lead slot, and each mark takes its natural size inside it | A mark no longer reads the list's size; the list keeps the names at one x. |

### The role becomes the name

| Was | Now | Why |
| --- | --- | --- |
| `AvatarSize`, `AVATAR_PX` (`@lotics/ui/avatar`) | `MarkSize`, `MARK_PX` (`@lotics/ui/mark`) | One scale for every lead mark — a face, a picture, a document's tile — not an avatar's alone. |

### Changed contracts

| Was | Now | Why |
| --- | --- | --- |
| On a phone `FilterBand` wrapped every chip onto the lines under the search | the line is the search, one "Filters" press counting what narrows (`FilterBandLabels`, `LoticsLocale.filterBand`) and the action; under it what narrows in words; the press opens a `Dialog` sheet of the status, the filters, the grouping and the order, each its whole control under its name, "Apply" closing it. A desk is unchanged. A chip reads where it is drawn through `useFilterBandPlace()` (`line`, `sheet`, `summary`): a `FilterChip` — and so `StatusChips`, `GroupByMenu`, `SortMenu` — draws itself for each; a chip of your own reads it too | Chips wrapped across a phone read as a second toolbar, and the one a reader wanted was often off screen. |
| An `OptionList` capped its list at 320 wherever it stood | a container holding it whole sets `--lotics-option-list-reach` (a filter band's sheet sets `none`) | A list scrolling inside a scrolling sheet hid options with no edge to say so. |
| A drawer led only back to the rows | `Drawer.onOpenPage` / `RecordFrame.onOpenPage` put "Open page" (`LoticsLocale.drawer.openPage`) before the close | A board opens its rows in a drawer to keep its place; the record's page is one press further. |
| `CardGrid` kept its cards' doors to itself | `CardGrid.doorRef(row)` hands a card's door back, as `TableRow.doorRef` does | Back from a row's page, the row the reader opened takes focus. |
| A `LoticsLocale` held no `filterBand` slice, and `drawer` no `openPage` | both are required: a pack of your own states them | Every rendered string is the pack's. |
| A `SectionNav` item that is no step drew its name alone | `SectionNavItem.icon`, `RecordFrameSection.icon` and `RecordFrameCompanion.icon` lead the item with a kit glyph in muted ink, in the step marker's box; a step keeps its mark | A rail of names with marks on some and gaps on others read as unfinished. |
| A drawer's section tabs were words alone | `TabOption.lead` stands a mark before a tab's label; `RecordFrame` leads each tab with the mark its section leads a rail with | A drawer's tabs read as a page's rail does. |
| A `Status` label read medium wherever it stood | medium unless its container sets `--lotics-status-weight`; a `FormField`'s control and an option list's row set it to the value's weight | An option's word in a field read heavier than a typed one beside it. |
| An option list's picked row stood on the `--accent` ground | a picked row is its tick (the checkbox in a set, the check in a single list) and no ground; the highlight keeps its wash | Every pick of a set on grey under a black box read as blocks. |
| `MemberSelect` drew its person as an inline `MemberChip` (a medium name, an `md` face) | as `MemberChip chrome="cell"`, at rest and in its list — a read field's face and a value's weight | A person picked read heavier than the same person read. |
| A multi `Select` field's values stood 4 apart, the chevron centred on the list | 8 apart (a group's rhythm), the first line where a one-line field's words stand, the chevron on it; a `cell` select keeps 4 | Chosen values read cramped. |
| A date field's separators, a range's dash and its prompt were `Text size="sm"` | set in the segments' own rung, which grows on a phone | 16px digits and 14px slashes read as two types. |
| `StateMatrixCell.state` and `.words`, and a glyph cell's `value` as its count; `onPressCell(rowKey, colKey)` | `entries: { key, state, words? }[]` — a mark per entry in its state's colour, side by side, three then "+N", the one whose state is latest in `states` (list them calmest first) leading and grounding the cell; a worded cell says its first entry's `words`; a pressed cell of several opens the list of its entries; `onPressCell(rowKey, colKey, entryKey)`; the legend counts each entry by its state | A cell of several drew one state's dot and a bare count, hiding a missed one beside one done. |
| A dated `StateMatrix`'s cells stood 2 apart in rows exactly their height | 8 apart, each row the band and 6 of air above and below its cells, the name filling the row | A month read as one cluttered block. |
| `BarChartItem.leading`, `MatrixAxisItem.leading` (rows) and `StackedBarRow.leading` drew the caller's mark at its own size, only on the data that had one; a matrix's column heads dropped theirs | a datum's `leading` — now on `PieChartSlice`, `StackedBarSeries`, `LegendItem` and a matrix's column heads too — stands in a slot of a line's mark (16): an unsized `Avatar` takes it, and every datum of the run holds the slot where one has a mark, so the names start at one x. `barChartInk`, `pieChartInk` and `matrixFloor` (its `seat.rows`) budget it, and a matrix's row names' column widens by it. Pass the mark unsized | A chart named people by bare names beside rows that drew their faces. |
| A `PieChart` legend's counts stood in a 40px column | at the widest count's ink, as a `BarChart`'s figures; the share keeps its floor, and `pieChartInk` budgets both so | A one-digit count held a name's room, and a name beside its face wrapped on a phone. |
| A `FilesEditor` document row carried a ✕ where `onRemove` was passed, and nothing where it was not; a picture tile its ✕ | every file — row and tile — carries its ⋯: Open and Download always; `onRename(id, filename)` and `onReplace(id, picked)` add Rename (the extension kept) and Replace, `onRemove` a confirmed Remove. `FileRows` takes `onRename(file, filename)` and `onReplace(file, picked)` the same way; `FileThumbnailGrid` takes `fileActs(file)` for its tiles' ⋯; `FileActs` (`@lotics/ui/file_acts`) is the ⋯ for a row that IS one file, joining its own `items` | A file drawn with no menu could be neither renamed nor downloaded where it stood. |
| A `FilesEditor` holding files drew a compact drop row under the set | the set itself takes a drop, and its add stands at the END of its head line; `addAt="host"` hands it to a heading the host draws (`FileAddButton`) or a bar's `upload` verb | An add under a list read as another row of it; every block's add stands in its heading. |
| `MediaView` took `labels: Partial<MediaViewLabels>` for its replace press; `LoticsLocale.mediaView` | its ⋯ over the frame's corner (`onRename`, `onRemove`, `onDownload` beside `onReplace`) takes `labels: Partial<GalleryLabels>`; a document is its row, never a card of its page. `MediaViewLabels` and `LoticsLocale.mediaView` are gone | One file's acts, said once. |
| `GalleryLabels` (and `LoticsLocale.gallery`) held the viewer's words | it also holds `open`, `rename`, `replace`, `renameTitle(filename)`, `name`, `save` and `cancel` — a locale pack of your own adds them | The ⋯ and its rename dialog speak the viewer's words. |
| A PDF at a tile's size drew its first page; a `FileRow` placeholder among pictures was a picture-sized dashed square; `FileThumbnailGrid` took `ghosts` | a document is its type at every size — its badge in a face or a mark, its card with its name in a tile — and an expected file is the document badge's ghost; `ghosts` and `FileThumbnail.credentials` are gone | A page shrunk to a tile read as a photo, and an owed paper as an owed picture. |
| A `FileBadge` narrower than 26 drew a bare page with its type's band; a `MarkStack` holding several documents drew three overlapping pages | under 26 the badge is its type's glyph in its type's ink; a set's documents are ONE mark (their shared type, else a plain file) before its pictures and the count | A 13px page with a red band said nothing a reader could read. |
| A compact figure alone (`formatMoney({ compact })`, `formatCompactNumber`, a `Metric`'s `compact`) rounded millions whole and billions to two places, and outside `vi` took Intl's compact rounding | every compact figure keeps a tenth under ten of its unit and a hundredth under one, alone or at a chart's `scale`: "2,7 tr ₫", "1,3 tỷ", "$13M" | A tile read "3 tr ₫" beside a bar reading "2,7 tr" for the same money. |
| A `ReadingGrid form="band"` figure stood at its words' width | each figure is a unit of the band's line — a quarter, less its gaps, never under a strip column's 180; half the line in a phone's column — its history drawn across it | A 150px number beside a chart read as a sliver. |
| A `Metric` history of periods that each held nothing drew a rule along the tile's foot; a flat `Sparkline` lay on its box's floor | no history is drawn where every period held nothing; a flat series is drawn across its box's middle | A bare rule read as a chart that failed. |
| A `TotalsLine` item's `trend` read green on every rise | `TotalsLineItem.goodDirection` tones it as `Trend`'s | A count of faults that grew read as good news. |
| A `LaneBoard` block only opened; its lane changed nowhere on the board | `LaneBoardBlock.move` (`LaneBoardMove`: `name`, `to: { lane, loads, name }[]`, `refused?`, `onMove(lane)`) stands a ⇅ at the end of a block wide enough for it — a phone's row at its right — opening a menu of the lanes offered, each drawn by its face and name with the loads it would carry; `refused` reads every lane and takes none | Choosing a lane is choosing where the work fits: the reader reads each lane's room before it moves. |
| `LaneBoardBlock.tier`, and `laneTiers(spans)` (`@lotics/ui/lane_board_layout`) to compute it; an hour's mark stood 96 wide, so a working day scrolled on a desk; free stretches drew a full dashed edge and muted words | the board stacks a lane's blocks itself, where they are DRAWN: a block stands at least a name's run wide (72, never past one mark), kept inside the axis, and one it would cover takes the next stripe (`blockTiers(boxes, least)`); an hour's mark stands 56 wide; a free stretch is a faint edge and quiet words, its edge whole on hover. Drop `tier` | A half-hour's booking read "Lê …", the afternoon ran off the desk's edge, and the free ground read louder than the work on it. |
| A `FilesEditor`'s add stood on the set's own first line — under the label where a field's label stands over its value | `FormFieldActs` (`@lotics/ui/form_field`): a value states its own acts inside it and its `FormField` stands them at the END of the label's line where the label is over the value, else on the value's first line, level with a label beside it or with no field; `FilesEditor`'s `addAt="head"` add is its field's acts, and the `lotics-files-editor__head` part is gone | On a phone a list's add stood on a line of its own between its label and the list, read as one more row. |
| A picked value in a field's box — a `Select` option or person, a `Combobox`'s chosen row or prompt, a field `MarkdownEditor`'s prose — read `sm` (14) at every width | the box re-points its `sm` rung at the input rung (`--lotics-input-text`, `-leading`, `-tracking` and the new `--lotics-input-leading-tight`): 16 on a phone, as a typed value; a read value stays 14 | A picked value read smaller than the typed one under it, and Safari zooms a focused 14. |
| `AvatarSize`, `AVATAR_PX`, `identityRung`, `IdentityBlock`, `IdentitySetting` and `ROW_PICTURE` from `@lotics/ui/avatar`; the `--lotics-avatar-xs` … `-xxl` tokens; `useLeadRung` | `MarkSize`, `MARK_PX`, `identityRung`, `IdentityBlock` (with `datum`), `IdentitySetting`, `ROW_PICTURE`, `markSizeWithin(px)`, `LeadMark`, `leadSlot(tiled)`, `markInSlot(mark, slot)` and `tileBadge(px)` from `@lotics/ui/mark`; `--lotics-mark-xs` … `-xxl`; `useLeadRung` is the kit's own. `Avatar`'s `size` is a `MarkSize` | The scale every leading mark — a face, a picture, a badge, a placeholder — is drawn at is one, and is no avatar's. |
| `FileBadge.size` was the page's width in pixels, default `FACE_BADGE_WIDTH` (gone); `fileChipWidth(filename, box)` took the mark's box in pixels | `FileBadge.size` is the `MarkSize` square the page stands in, as tall as it (default `lg`); `fileChipWidth(filename, mark: MarkSize)`. `FileBadge.tile` makes that square a tile — a picture's frame on a quiet ground, the page a fifth in from each edge (`tileBadge`), dashed with the page's ghost where `placeholder`: a document leading beside pictures and faces is one — a `FileRow`'s, an `Avatar`'s file, a stack's in a lead, a compact `FileThumbnail`'s | A 32px page among 56px pictures read as a smaller thing, and a page filling a picture's square edge to edge as one crammed into it. |
| A list's leads (`Table`, `FileRows`) stood in a slot as wide as its widest mark, each at its own rung: a picture 56, initials, a face, a badge or a placeholder 40 | every lead of a list stands in ONE slot, square and centred (`leadSlot`) — `ROW_PICTURE` where any row holds a picture or a document, a pair's where none does; a phone steps it down to a pair's — each mark at its own size in it (`markInSlot`): a picture fills it, a document is its tile, a file not held that tile dashed, a face (an `Avatar` with a `subject`, or initials) a pair's. A list of documents stands in a picture's slot, a list of placeholders alone in a face's. `FileRow`'s `mark` part is gone (`face` is its square) | Initials grown to 56 read as a poster, and a paper edge to edge in 56 as crammed; the slot, not the mark, keeps the names at one x. |
| A row's stage and figure rode its title's line on a pair, and its acts joined them there (round 22) | stage, figure and acts centre on the row across every line the pair holds, a register's phone row's ⋯ too; a `TableGroup` record heading's file fills the list's slot — a document its tile, holding the slot a picture's as a row's does; none filed yet, the dashed tile — standing a row's height; a band's `face` is a name's (`md`), never a line's | A status and a figure riding the title's line floated over the row's middle, a heading's page floated in a picture's square, and a 16px face over 56px pictures read as a speck. |
| A `LinkValueImage` (`LinkValue`, `RecordHeader`, a `Combobox` option's row) without `src` drew initials | `LinkValueImage.subject` (`"person"` a disc, `"organization"` a rounded square) is someone: their picture, else their initials. Without it the row is a thing: its picture, else the dashed tile of the picture not held (none on a line). State `subject` where initials are meant | "SO" for order SO-1044 read a thing as a person. |
| A `MarkStack` thing mark with no `image` drew the neutral parcel | it draws the dashed square of the picture not held, the picture's glyph in it — the page's ghost is a paper owed (`FileBadge placeholder`); `icon` still draws its kind's glyph | A parcel said nothing of a picture owed. |
| A `RecordFrameSection` holding nothing — no `fields`, `blocks`, `acts`, `note` or `menu` — drew its heading over nothing | it draws none: its navigation item still marks its step, and a press on it lands on the next section that draws | A step the work has nothing at yet read as a section left blank. |
| A `TableGroup` that is an option named it in words alone | `TableGroup.mark` (`OptionMark`) leads the label with the option's own mark in `color`'s ink, in the dot's place | A band per payment method read by words beside cells that drew each method's mark. |
| `rowGutterWidth(verb, menu, glyph)` budgeted a row's verb at its word's ink however long | `rowGutterWidth(verb, menu, glyph, width)`: a verb wider than a third of its line `width` (`rowVerbDraws(verb, glyph, width)` false) stands as no button, the gutter budgeting the ⋯ it waits in. Name such a verb in the row's ⋯ | One long label widened every line's seat until the register piled. |
| A `DatePicker`'s `actions` rode inside its field beside the date wherever it stood | under a `FormField` label standing over the date they stand at the end of the label's line (`FormFieldActs`); beside a label, inside the field as before | A date and its hour hold their width: at a phone's 16px a verb beside them was drawn over the hour. |
| A list (`Table`, `FileRows`, `CardGrid`) drew a thing's picture not held on every row that lacked one | it draws that placeholder only where a row in view holds a picture or a document; where none does no row draws one, and where every lead is one the list keeps no lead slot. `LeadMark` gains `lacking`, the picture not held; `placeholder` is a paper owed, drawn wherever it stands | A column of empty tiles marked nothing. |
| A `Timeline` entry's `mark: { image }` was always a thing's | `mark: { image, kind? }`: a `person` or `group` is drawn by a face, its initials where `image` is `null` | An agenda of customers' visits led each with a thing's placeholder. |
| A chart datum's face (`DatumLead`) stood at a line's 16 | at `identityRung("datum")`, 24 — the smallest whose initial reads; `barChartInk`, `pieChartInk` and `matrixFloor` budget it | "T" and "P" at 16 read as specks. |
| `RecordFrameBlock.keepsTitle` kept a lone block's heading under its section's; only a lone block under no field was headed by the section's title | gone: the one block a heading would name in a section of no field — a block that `namesItself` beside it — is headed by the section's title, its count and add there; its own title passes to its body, where an empty `FilesEditor` well names what it takes | "Dọn phòng" over "Ảnh sau khi dọn" said the section twice. |
| A staged rail marked the section in view from the head, beside the step the work stands at | at the head, while a step is `current`, `RecordFrame` marks no place (`useSectionNav` hands `moved`, the reader having scrolled or jumped); `RecordFrameSection.exit` / `SectionNavItem.exit` draw an end the work takes instead of its path — never a step, led by its glyph, set apart after the steps | Two items stood out at once, and a cancellation read as a fifth step. |
