@lovett/ui
Primitives, tokens, layouts and patterns: the component library ignite-workspace's apps,
Lightwork and ads-engine install. Its subsystems include thread, detail, filter-core,
data-grid, data-table, meta-previews, file-upload, icons and lib.
Canonical home: here, and only here (ADR-CE-0151, Accepted 2026-11-01)
This directory in edwinlov3tt/lovett-packages is the only source and the only publisher of
@lovett/ui. It was imported from ignite-workspace packages/ui with its full history at
ignite main @ 2675a1440c6e5b61da21659fd00e1251594fbd65 (2026-10-16), replacing the stale 0.0.9
fork that used to live here. The decision is ADR-CE-0151 in ~/engines/docs/decisions/_active/;
packages-pm releases it.
- Change a primitive here, publish, and consumers bump. A consumer never links this tree with
workspace:orfile:(ADR-CE-0041). - A release is a tag on
main(ui-v<version>), packed, the tarball installed and checked, then published. Never from a branch. Every publish must be above the registry's highest version, read at publish time (npm view @lovett/ui versions).
Cutover (ADR-CE-0151 D1, D4)
- Publish 0.6.0 from
mainhere. Done when npm'slatestequals apnpm packof the tag. - ignite-workspace stops publishing (its seat, discoverypro-pm): delete
.github/workflows/publish-ui.ymland move npm's trusted publisher off that repo. Until then aui-v*tag pushed there would publish, and npm would pointlatestat it. - Lightwork (lw-pm): retire
scripts/ui-release.shor point itsLOVETT_UI_DIRhere. - Each consumer moves to a family release at the version it chooses: Admium off its vendored tarball, the directory off npm's employer-built 0.5.0, ads-engine off 0.3.4.
Develop
pnpm install # own lockfile; not a pnpm workspace
pnpm typecheck
pnpm test # vitest, jsdom
pnpm build # tsup -> dist/ (ESM, split chunks, bundled .d.ts, CSS + fonts copied)
pnpm check:dist-esm # refuses a dist Node's ESM resolver cannot load; runs in prepublishOnly
The source exports (main/exports -> ./src) are what a source consumer resolves;
publishConfig swaps them to ./dist for the published tarball. Both maps must change together.
scripts/check-dist-esm.mjs scans every dist/*.js. It exists because a deep specifier without a
file extension (react-syntax-highlighter/dist/esm/styles/prism) builds green under Vite and fails
at import under Node. That shipped as 0.0.7 and 0.0.8, was fixed here at 0.0.9, was lost in ignite's
tree (the published 0.4.2 carries it), and is fixed again in 0.5.0.
Themes
One kit, several looks. A theme is a scoped stylesheet that re-points the kit's own token names;
no component knows which theme it is in. Import tokens.css and styles.css first, then the theme.
| Theme | Import | Opt in |
|---|---|---|
| kit (dark default, light) | @lovett/ui/tokens.css | data-theme="light" (or combo) on <html>; none means dark |
| v2 | @lovett/ui/theme-v2.css | data-ds="v2" on any element |
| Lovett (Lightwork) | @lovett/ui/theme-lovett.css | data-ds="lovett" below <html> |
| Admium (0.5.0) | @lovett/ui/theme-admium.css | data-ds="admium" on <html> or any element, under <html data-theme="light"> |
Admium is the warm palette with an orange accent (#EF6820 fill, #E04F16 pressed, #FFF0E5
tint, #C2410C orange text), approved by the owner on 2026-10-17 in Admium's lab. It is a token
override of the kit's light theme, so it applies only under data-theme="light"; under dark it
matches nothing. It adds three things beyond tokens, all scoped to it:
- The raised primary button: a 1px white inner highlight (12% to 0, top to bottom), a 1px ring
in its own orange, a
0 1px 2px rgba(13,18,27,0.24)shadow, a semibold label, and the same radius as every other button. No lift and no glow on hover. - Control hairlines: secondary and icon buttons and the search bar rest on
#E6E4E2(--border-control), while dividers stay#F2F1F0. - State chips re-resolved in scope, so their tinted borders follow the warm ink.
0.6.0: one tree, published from the family (2026-11-01)
No component changes its look for markup that existed in either 0.5.0. 0.6.0 is the union of the two different 0.5.0s (ADR-CE-0151 D2):
- The family's 0.5.0 (lovett-packages
feat/ui-homeb12a8d4, never published; Admium vendors it): the Admium theme and the owner's UI rules below. - npm's 0.5.0 (ignite-workspace
packages/ui8f3ae0d1, published 2026-10-18; the directory installs it): the at-risk desk's kit gaps. That is the only ignite commit to touchpackages/uiafter the split at 2675a144.
What a consumer must know when it moves:
- From npm's 0.5.0 (the directory): you gain the owner's UI rules below (no bold, no colour transitions, chip borders at 30% of their ink, concentric radii). Those are the owner's intended changes; check your screenshots for them. Every export you had is still there.
- From the family's 0.5.0 (Admium): you gain StateChip
iconandbare,DropdownMenuRadioGroupandDropdownMenuRadioItem, SegmentedPillariaLabelledBy,ariaDescribedByand itemtitle, Traysize="wide", Buttonvariant="danger-text",aria-disabledon Button and Switch,readOnlyon Switch and Checkbox, ToastercloseButtonLabelanddismissOnEscape. The Lovett-scope ink changes apply only underdata-ds="lovett". - Replace the dependency with
"@lovett/ui": "0.6.0". Tailwind is still needed over the package's source (@sourceatnode_modules/@lovett/ui/src); 0.7.0 removes that need.
Files the merge resolved. npm's commit applied without conflict onto the family tree, so every file carries both sides' changes:
| File | From the family (b12a8d4) | From npm (8f3ae0d1) |
|---|---|---|
src/badge.tsx | accent text reads --accent-ink | StateChip icon and bare |
src/checkbox.tsx | no colour transition | readOnly |
src/dropdown-button.tsx | menu padding 6px to 4px (concentric radii) | the danger-text variant's class |
src/dropdown-menu.tsx | menu padding, no colour transitions | radio group and radio item; the new radio item's transition-colors removed here, to match the owner's rule (the one edit 0.6.0 makes to npm's code) |
src/floating-drawer.tsx | no colour transition | Tray size="wide" |
src/segmented-pill.tsx | no colour transition | ARIA props, item title |
src/styles.css | no colour transitions on buttons, inputs and links | danger-text, aria-disabled and readOnly styles |
src/theme-lovett.css | search field transitions its shadow only | Lovett-scope inks, disabled primary, danger-text, 16px fields below 640px |
package.json | 0.5.0, the Admium theme export | 0.5.0 |
npm's commit alone also changed button.tsx, switch.tsx, toast.tsx and index.ts, and touched
nine test files (four of them new). The family's alone touched 73 other files: the rules below, and
the new theme-admium.css.
Also in 0.6.0, none of it visible:
- Lint runs.
eslint.config.jsextends@lovett/elint/migrating, withreact-hooksand the test tsconfig wired. Fixed to pass the hard rules: promise-returning handlers arevoided (calculator shells, code block, copy field, file drop zone, folder picker, profile section), and upload ids come fromcrypto.getRandomValuesinstead ofMath.random. - Tests: 1,793 pass, the token-shape test among them.
0.5.0: the owner's UI rules, fixed at the source (2026-10-17)
These change how every consumer renders once it upgrades to 0.5.0. Consumers pin versions, so
nothing moves until then. src/__tests__/ui-rules.test.ts greps src/ for the checkable ones so
they cannot come back.
- No bold.
font-bold/font-extrabold/font-weight: 700becamefont-semibold(600) on headings, titles, labels and figures, andfont-medium(500) on inline emphasis (the Markdownstrong, the floating status bar's label). Exempt:meta-previews/, which reproduces Facebook / Instagram / Messenger chrome, and the 700@font-facethose previews use. - No colour transitions.
transition-colorsand colour members oftransition-[...]lists are gone,transition-allnames the property it animated (width,opacity), and the CSS fallbacks for--btn-transition,--input-transition,--pager-transitionand--search-transitionkeep onlybox-shadow/transform(ornone). Colour, background, border-colour, fill and stroke now change immediately. Kept on purpose: geometry tweens (bar widths, chart paths, collapse rows) and the fading scrollbar'sscrollbar-color. - Tinted chip borders. A chip's border is its own ink at 30%, never a neutral grey: the
default
--state-*-bordertokens (were--border/--border-strong) andDeltaChip's neutral tone.BadgeandMetricCard'saccenttone now read--accent-ink(identical in the kit's light theme). - Concentric radii. Where a rounded element sits 8px or less inside another, the outer radius is
the inner radius plus the gap. Fixed in both the kit and the Lovett scope:
DropdownMenuContent,DropdownButton, the collapsed-sidebar flyout and theSelect/Comboboxlistbox pad 4px (was 6) around md rows; theFilterDropdownlist is lg / 4px / md (was md / 6px / 8px);Modal's tray is lg inside the xl panel (was md); the bubble-menu toolbar and the link group are lg around sm buttons (was sm, rounder inside than out); the data table's filter and column-manager popovers are xl (was md); board column head buttons are lg in the 2xl column; the file preview thumbnail,ChipNav's track (12 default, 10 compact),CopyField's button (now also centred),SegmentedChoice,FilterChip's remove button,TaskCardmedia and grip, thread suggestions and sort pills,TagChipInput/FolderTreePickeroptions and chip remove buttons,StatRow's inner panel, the owner strip,FolderCard's checkbox, the attachments drop overlay and the prompt ring's error edge usecalc(var(--radius-X) - gap)(or+) so they hold in both themes. Left alone on purpose: controls in a padded footer or field (thread composer footer, attachment chips, chips insideCombobox/TagChipInputfields), where the concentric answer is a square control; hover-only rows (FieldRow, filter-menu rows); and Lovett's search hint, whose concentric radius is a square keycap (Lightwork's call). - No conditionally rendered dialogs. The kit had no
{x && <Dialog|Modal|Sheet|Drawer>; the test keeps it that way.
Known gaps
TODO(ui-tray-exit):Tray(floating-drawer.tsx) has an enter animation but returnsnullwhenopenis false, so its close animation never runs;Modalalso unmounts when closed (it has no animation to lose). Both are driven by their open prop, which is what the dialog rule asks of consumers; keepingTraymounted through its exit is a separate behaviour change.TODO(ui-lint-backlog): lint runs on@lovett/elint's migrating preset since 0.6.0, with zero errors. Twelve strict rules over code written under ignite's looser config warn instead of failing; they are listed ineslint.config.jsand burn down release by release.- The
dsxuioverlap (60 names, six tokens with clashing formats) is sanctioned coexistence under ADR-CE-0034 D1. Converging the two is a separate decision.