@lepsto/ui
v9.0.0
Published
Lepsto design system — shared UI primitives, tokens, and theme
Readme
@lepsto/ui
The Lepsto design system as an installable package: shared UI primitives, design tokens, theme and a Tailwind preset.
Install
Published to the public npm registry — no scope or registry configuration needed:
npm install @lepsto/uiContract: the package name, its exports and its peer dependencies — all declared in
package.json— are a cross-project contract. Do not change them without coordinating the workspace host and every federation remote (see Federated remotes).scripts/check-package-contract.mjsrefuses a change to any of them, and says the same thing when it does.
What's inside
- Components: shadcn/radix primitives themed by Lepsto tokens (Button,
Dialog, Select, Tabs, …) plus layout:
Grid/Col(the adaptive 4 / 8 / 12 column grid) andGridOverlay(a design-aid overlay). The full catalog lives in Storybook at ui.lessly.com/storybook. - Theme:
useThemehook +ThemeToggle(toggles thelightclass on<html>;:rootis dark,.lightis light). - Tokens: the
tailwind-presetre-exports the token vocabulary from@lepsto/tokens(generated upstream inlepsto/design-system) withdarkMode: 'class'added, andstyles.cssbundles the package'stheme.css(CSS variables,.lightoverrides, typography classes) plus this library's base layer, and a@themeblock that declares every colour token to Tailwind v4. It declares no font face: the brand faces live in@lepsto/tokens/fonts.css, which the kit re-exports as@lepsto/ui/fonts.css, a second import beside it.styles-federated.cssisstyles.csswithout the@layer baseblock and without the@themecolour bridge, and with its variables declared on:where(:root)/:where(.light)so a shell's own tokens outrank the remote's whatever loaded last, for remotes that must not restyle the page they load into. The bridge ships as its own sheet,theme-federated.css, which a remote imports withtheme(reference)beside it. Token values are never edited here — they change by bumping@lepsto/tokens.
Usage
import { Button, useTheme } from '@lepsto/ui';
import '@lepsto/ui/styles.css';
import '@lepsto/ui/fonts.css';The second stylesheet is what loads the brand faces — Inter in nine weights,
Fira Mono in three, Sora in two — from woff2 files inside @lepsto/tokens, at
relative paths your bundler resolves out of node_modules. That package reaches
you as a dependency of @lepsto/ui, and the kit resolves it, so there is nothing
to install beside it and the version you get is the one the kit was built
against. Miss the import and every role renders in a system fallback, with no
error and no warning.
The four components that need a router — AppShell, AppSidebar, SidebarNav
and ExtensionLink — come from @lepsto/ui/router instead, and only that entry
imports react-router. The main entry never does, which is what makes
react-router an optional peer: an app that navigates with plain hrefs can
use the rest of the kit without installing one.
import { AppShell, SidebarNav } from '@lepsto/ui/router';Tailwind is what turns the kit's class names into CSS. The package ships no
compiled utility CSS — every consumer runs its own Tailwind over the token
preset (tailwindcss >= 3.4, an optional peer dependency, required only if you
consume the preset).
Tailwind v4 (CSS-first)
Everything is declared in your CSS entry. The minimum is four lines:
@import 'tailwindcss';
/* Token variables, `.light` overrides, base layer — and a `@theme` block
declaring every colour token, so `bg-bg-surface`, `text-text-primary` and
`ring-border-selected` compile from this import alone. */
@import '@lepsto/ui/styles.css';
/* The brand faces. No sheet above declares one, and nothing warns when they are
missing — the page just renders in a system fallback. */
@import '@lepsto/ui/fonts.css';
/* Scan the library. v4 does not scan `node_modules` on its own, and the kit's
components (Grid, Col, …) carry literal utilities (`md:col-span-5`,
`gap-gutter`) as strings inside `dist/index.js`. The glob matters: a bare
directory path is skipped, because Tailwind honours your `.gitignore` and
`node_modules/` is in it. */
@source './node_modules/@lepsto/ui/dist/**/*.js';The rest of the token vocabulary is not colour and does not travel in CSS: the
spacing scale, the grid gutter (gap-gutter), radii, shadows, z-index and
durations come from the JS preset, and v4 has no presets: key of its own, so a
config file is what loads it:
@config './tailwind.config.ts';with that file holding nothing but the preset:
import type { Config } from 'tailwindcss';
import { lepstoPreset } from '@lepsto/ui/tailwind-preset';
export default { presets: [lepstoPreset] } satisfies Config;Paths in @source and @config are relative to the CSS file. If your CSS entry
sits in src/, they read '../tailwind.config.ts' and
'../node_modules/@lepsto/ui/dist/**/*.js'.
Tailwind v3
The preset and the library scan both go in the JS config:
import { lepstoPreset } from '@lepsto/ui/tailwind-preset';
export default {
presets: [lepstoPreset],
// Scan the library too — its components (Grid, Col, …) use literal token
// utilities (`md:col-span-5`, `gap-gutter`) that Tailwind must see to generate.
content: ['./src/**/*.{ts,tsx}', './node_modules/@lepsto/ui/dist/**/*.js'],
};CSS variables use the upstream unprefixed names (--bg-primary,
--text-primary, …). The old --color-* names still resolve through a
deprecated alias layer in styles.css and will be removed in the next major —
migrate any direct var(--color-*) reads.
Federated remotes
A Module Federation remote loads its CSS after the shell has painted, so it must
not restyle the live page: a @layer base body rule re-applied on top of a
rendered page shifts font metrics on every cold load. Import the
federation-safe sheet instead — same tokens, aliases, ui variables and
animation layer, without that block — paired with the colour bridge, which
ships as its own sheet loaded first in reference mode:
@import '@lepsto/ui/theme-federated.css' theme(reference);
@import '@lepsto/ui/styles-federated.css';The reference line is required: miss it and the bridge compiles to a bare
:root,:host block that outranks the shell, so the palette is dropped rather
than inherited. styles.css keeps its bridge inline, so a standalone consumer
imports one sheet as before. The shell keeps importing @lepsto/ui/styles.css,
and it is the shell that imports @lepsto/ui/fonts.css: a remote declares no
face and no base layer.
The preset is not optional on this path either, and installing it is not the
same as reaching it: Tailwind v4 has no presets: key, so both entries —
federation and standalone — need @config, and one that lacks it compiles to
Tailwind's defaults with no error anywhere. What each sheet carries, why the
federated one declares its variables at zero specificity, and why
theme(reference) is not the fix are in
docs/federated-styles.md.
Storybook
pnpm storybook # dev server on http://localhost:6006
pnpm build-storybook # static build → storybook-static/The Storybook toolbar has a global Dark/Light toggle driven by the package's own
useTheme hook.
Where a component goes
This kit is not the only place in lepsto a shared component can live.
design-system holds the token and asset layer this package installs as
@lepsto/tokens and no component
implementations at all; landing-ui is the reference catalog for the public site,
before the login; and a product's own app holds what only that product uses.
docs/component-home.md records what each one holds today
and the test that tells them apart.
Deployment (ui.lessly.com)
ui.lessly.com is served by a Lepsto platform service that builds the repo's root
Dockerfile from main. The image runs the zero-dependency site/server.mjs, which
serves the landing page (site/) at /, the built Storybook (storybook-static/) at
/storybook, and a health check at /healthz. The Docker build runs pnpm build,
pnpm build-storybook, and pnpm build-site in turn.
The npm package build is pnpm build (tsup → dist/), which the release flow uses; the
npm package published to npmjs always contains the package, never the site or Storybook.
Scripts
| Script | Description |
| --- | --- |
| pnpm build | tsup package build → dist/ (+ assembles styles.css from the @lepsto/tokens theme, deprecated --color-* aliases, ui-local pieces, and the animation layer, the federation-safe styles-federated.css beside it, and the theme-federated.css colour bridge a remote pairs with it) — the npm package published to npmjs |
| pnpm build-storybook | Storybook static site → storybook-static/ — served at ui.lessly.com/storybook by site/server.mjs |
| pnpm build-site | Landing app (site/) → site/dist/ — served at ui.lessly.com/ by site/server.mjs |
| pnpm storybook | Storybook dev server on http://localhost:6006 |
| pnpm check | Vitest, only what your change reaches |
| pnpm test | Vitest, everything but the guard meta-tests |
| pnpm test:gates | Vitest, the gates-bite-* meta-tests |
| pnpm test:all | Vitest, both projects — the merge gate |
| pnpm type-check | tsc --noEmit |
| pnpm lint | ESLint |
| pnpm changeset / pnpm release:version | Versioning via Changesets (describe change → apply bump + changelog) — see RELEASING.md |
| pnpm release:publish | CI-only publish wrapper (publish to npmjs if needed + changeset tag) — used by .github/workflows/version.yml |
Releasing
Add a changeset (pnpm changeset) to each significant PR; merging the
auto-generated "Release: version packages" PR publishes to the public npm
registry and tags v<version>. See RELEASING.md.
