@geonosis/ui
v0.2.1
Published
The atoms and molecules floor: a component library with zero domain knowledge, themed entirely through @geonosis/themekit's CSS variables.
Maintainers
Readme
@geonosis/ui
The atoms and molecules floor: a component library with zero domain knowledge, themed entirely
through @geonosis/themekit's CSS variables. 19 atoms, 16 molecules, ported faithfully from a
1,913-line reference tree with two changes — every colour, radius, font and spacing the reference
spelled as a raw Tailwind value that themekit has a token for now reads that token's CSS variable,
and every brand or product word is gone.
The door is per component, not per domain
D-056's "one door per domain" is the rule for a domain package (@geonosis/document, one door
plus ./ui). A component library's door is different by convention: every atom and molecule is
its own exports subpath (./atoms/button, ./molecules/editable-field, …), so a consumer's
bundler tree-shakes an unused component out entirely and an import is a straight find-and-replace
of the scope from whatever library you are migrating off of. There is no root . export — nothing
composes an atom or a molecule for you, and nothing forces you to load one to get another.
Peers
reactandreact-dom(>=19) are required peers — this package is nothing without them — declared throughpeerDependenciesMetaso an install without them warns rather than errors.lucide-reactandframer-motionare optional peers. Onlymolecules/save-indicator.tsxreadsframer-motion, and onlymolecules/empty-state.tsx,molecules/error-boundary.tsx,molecules/save-indicator.tsx,molecules/select.tsxandmolecules/view-toggle.tsxreadlucide-react— a consumer who never imports one of those five subpaths never installs the optional peer its bundler would otherwise ask for.
Tokens
Colours, the control radius and the md/lg/xl shadow steps read @geonosis/themekit's
default emission (packages/themekit/src/emit.ts's defaultEmission(), prefix --theme-) —
var(--theme-primary), var(--theme-border), var(--theme-radius-control),
var(--theme-shadow-md), the --theme-<status>-surface / --theme-on-<status>-surface pairs for
badges and the error boundary. A consumer wires its own theme through emitVariables into a
:root block (or its own emission map, if its variables are spelled differently) — this package
never emits, it only reads.
Ten of those roles arrived in themekit 2.11 because this package needed them and had been leaving
raw Tailwind in their place: surface-subtle / on-surface-subtle, surface-hover,
control-surface, control-track / control-track-on / on-control-track, label,
muted-subtle and grip.
They are OPTIONAL in the contract, so every call site names a fallback — a theme that declares none
of them still renders, in the colours it had before.
A hover, active or subdued state is a role of its own, not a mix of another one
Never a fixed neutral, and never a second dark: class — the token already carries the mode.
Every surface-tokenized fill (Input and Select's terminal mode, Textarea,
molecules/select.tsx's trigger, CardButton's unselected state, the table row and cell in
constants.ts) hovers to var(--theme-surface-hover), which a theme STATES.
It used to be color-mix(in srgb, var(--theme-surface) 96%, black), and that was wrong twice: it
reads as "one step darker", which is backwards in a dark mode where one step is lighter, and the
same shape on Button's primary — color-mix(… var(--theme-primary) 90%, black) — renders
black on black in a theme whose primary is black, so the button lost its hover entirely. primary
now hovers with opacity-90, which no theme can defeat. A text-only tint (destructive) still
mixes toward transparent, which has no such trap.
buttonVariants for primary, destructive, ghost and secondary is asserted to carry no
neutral- class (src/atoms/atoms.test.tsx); control is exempt and says why below.
Not yet a token
themekit's contract has no role for these, so they stay raw Tailwind — each is a genuine gap in the token contract, not an oversight in the port:
- The document sticky-controls chrome —
Button'scontrolvariant (bg-neutral-800 text-white … dark:text-neutral-300) is deliberately the same dark chrome in every theme mode, not a themed surface. This one is a decision, not a gap. SkeletonLine's shimmer gradient — afrom/via/togradient has no single-token shape.Card'smenuvariant background/border — kept raw pending a distinct "popover" surface role (its shadow step IS tokenized,--theme-shadow-xl).HideButton's compact overlay chip — its background/border/text triad doesn't map cleanly onto one role.AddItemButton's tertiary, near-invisible text weight — a shade dimmer thanmuted, with no themekit role betweenmutedand fully hidden.Indicator'sdefaultdot (bg-neutral-400/dark:bg-neutral-500) — a neutral status, which the status roles do not model besidesuccess,warninganddanger.
Five entries left this list when themekit 2.11 added the roles they were waiting for: the hover
tints became surfaceHover, the toggle tracks became controlTrack/controlTrackOn/
onControlTrack, and the subdued pills, ViewToggle's active segment and SectionBox's panel
became surfaceSubtle/onSurfaceSubtle.
Tailwind
Tailwind v4 only scans files it can see from the consumer's own tree. Add this package's built
output to the consumer's own stylesheet or its @source never runs and every class in it purges
to nothing:
@source "../node_modules/@geonosis/ui/dist";postcss
./postcss re-exports this package's own postcss.config.js (the @tailwindcss/postcss plugin
entry) for a consumer that wants to compose it into its own PostCSS config.
