@eocrm/design-system
React 19 design system for the EOCRM. Ships compiled ESM (dist/: JavaScript, .d.ts types and plain CSS) — no Sass, TypeScript or transpile configuration needed in the consumer.
Its web tokens are generated and versioned by @eocrm/design-tokens; the
compiled token CSS is bundled into this package, so consumers install only
@eocrm/design-system.
Component contracts live in docs/components/<Name>.md (props table, anti-patterns). AI agents consuming this package should read AI-PRIMER.md first, ideally through the MCP server (see AI agents).
Live playground (every component, demoed against main): https://eocrm.github.io/design-system/components
Install
1. Registry
@eocrm/design-system is public on npmjs, so no .npmrc or token is needed.
If your .npmrc still maps @eocrm to another registry, remove that line.
2. Install
npm install @eocrm/design-system
3. Use
// Once, at the app's root (e.g. main.tsx):
import '@eocrm/design-system/styles/global.css';
// Anywhere:
import {
Avatar,
Badge,
Button,
Card,
Cluster,
DropdownMenu,
Input,
Stack,
Tabs,
} from '@eocrm/design-system';
What global.css does
Importing @eocrm/design-system/styles/global.css once at your app root applies three things:
- Tokens — defines every
--color-*,--space-*,--radius-*,--font-*,--shadow-*etc. on:root. Required. - Modern reset — including
* { margin: 0 }. All default browser margins on every element are zeroed. Headings, paragraphs, lists have no intrinsic vertical rhythm; space them viaStack/Clusteror with the parent's own margin in CSS. The document background uses--document-background(defaultvar(--color-bg-subtle)) so route transitions between full-screen surfaces do not flash a different base color; override that token after the global import for a different application canvas. The reset also appliesscrollbar-width: thin+ a token-coloredscrollbar-colorto every element, so every scroll container — library components and your own — gets a thin themed scrollbar instead of the OS default. (One deliberate exception: a collapsed<Rail>hides its bar entirely, since a gutter is about a quarter of the 56px rail's inner width.) It's declared at specificity 0-0-0; opt a scroller out with its ownscrollbar-width: auto/scrollbar-color: auto. Note that Chromium ignores::-webkit-scrollbarrules on any scroller where these standard properties are set — if you already style scrollbars that way, they'll stop applying. - Base typography —
bodyfont + size,h1–h4sizes, link color. Global rules, not scoped.
If you need only the tokens (you're providing your own reset), import @eocrm/design-system/styles/tokens.css directly. Sass variables: @eocrm/design-tokens. mixins.scss was removed with no replacement.
Accent
The accent family defaults to blue. To switch the whole app to another accent, set data-accent on <html> (omit the attribute for blue):
<html data-accent="tealpine" data-theme="dark"></html>
data-accent | Hue | Compose objects |
|---|---|---|
| (none) | Blue (default) | EocrmLightTokens / EocrmDarkTokens |
tealpine | Teal pine | EocrmTealPineLightTokens / EocrmTealPineDarkTokens |
indigo | Indigo | EocrmIndigoLightTokens / EocrmIndigoDarkTokens |
grape | Purple | EocrmGrapeLightTokens / EocrmGrapeDarkTokens |
berry | Magenta-pink | EocrmBerryLightTokens / EocrmBerryDarkTokens |
slate | Neutral graphite | EocrmSlateLightTokens / EocrmSlateDarkTokens |
ocean | Cyan-blue | EocrmOceanLightTokens / EocrmOceanDarkTokens |
moss | Olive green | EocrmMossLightTokens / EocrmMossDarkTokens |
Each accent swaps --color-accent, -hover, -pressed, -fg, -bg-subtle, -subtle-bg and --ring-accent, plus the default Link colour (--color-link-fg / -hover), in light, forced dark (data-theme="dark") and OS dark. They ship in the existing tokens.css / global.css, so there is no new import. AppProvider's tokens / darkTokens overrides win over data-accent in every theme (see docs/setup.md). Info colours stay blue under any accent; DropdownMenu checked items, the OptionsPicker selected row and StagePath done states follow the accent. Chart colours (--color-chart-series-1…8, the Brand palette) keep the same eight hues but re-order so slot 1 matches the accent, so Chart's first series and FunnelChart's ramp follow it: blue and Slate lead with blue, Indigo and Grape with violet, Berry with magenta, Ocean and Teal pine with teal, Moss with green. The full orders are in docs/components/Chart.md; under Moss the first three slots separate less for deutan viewers, an accepted exception like the success-colour note below. In light theme, Teal pine's default Links use a slightly brighter teal than the accent (#007676, AA on every grey surface) at medium weight (not inside headings, <strong> or <th>), because the accent alone sits too close to the navy body text to read as a link. Slate's default Links are medium weight for the same reason. Indigo and Berry also set a slightly different link colour from their accent (#4342c4, #a82c68) to keep links at AA on every grey surface.
Compose: each accent's Light / Dark objects sit beside EocrmLightTokens / EocrmDarkTokens; only accent colours differ. The link colour and chart series tokens are web-only (Compose has no Link or Chart), so Compose links keep the accent colour.
There is deliberately no red, orange or yellow accent: a primary button in those hues reads as a danger or warning action.
Known issue: in light theme, Teal pine and Moss sit close to --color-success in hue. Review success/accent pairings visually before switching the CRM to either.
Bundler notes
The package ships compiled ESM (dist/) with .d.ts types and plain CSS. No Sass, TypeScript or transpile configuration is needed.
- CSS imports: every component's JS chunk imports its own
.css, so the bundler must handle CSS imports. Next.js, Vite, Rsbuild and CRA do out of the box; plain webpack 5 needscss-loaderplusstyle-loaderormini-css-extract-plugin; Rollup needs a CSS plugin. Import@eocrm/design-system/styles/global.cssonce at the app root. - One way to load component CSS: use either
styles.css(a single<link>, for setups whose CSS pipeline can't take imports from JS) or the JS imports, not both — loading both duplicates every rule. The JS itself always needs a bundler (it imports CSS and readsprocess.env.NODE_ENV). - Next.js (app router): works as-is, no
transpilePackages. Client-bound modules carry'use client', so plain components (Button,Select,Badge,Input, …) can be rendered directly from Server Components. Three limits of the Server/Client boundary:- Providers: the
rucatalog contains functions, and functions cannot be passed from a Server Component to a Client Component. MountAppProvider/I18nProviderfrom your own'use client'providers file that importsru(example below), not fromapp/layout.tsxdirectly. - Compound parts (
Breadcrumb.Item,Modal.Header,Accordion.Item,List.Item, …) cannot be reached from a Server Component — a client module can't be dotted into on the server, soX.Itemisundefinedthere and rendering fails. Render compound components from a'use client'file. - Controlled overlays (
Modal,Drawer) need state and a functiononOpenChange, so render them from a client component.
- Providers: the
- Set
moduleResolution: "bundler"(or"node16") in yourtsconfig.jsonif TypeScript can't resolve types. Iftscrejects the side-effectimport '…/styles/global.css'(TS2882) and your framework doesn't declare CSS modules for you (Vite'svite/clientand Next'snext-env.d.tsdo), adddeclare module '*.css';to a.d.tsfile.
// app/providers.tsx
'use client';
import { AppProvider, ru } from '@eocrm/design-system';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<AppProvider locale="ru" messages={ru}>
{children}
</AppProvider>
);
}
// app/layout.tsx (Server Component)
import '@eocrm/design-system/styles/global.css';
import { Providers } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ru">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
Tests. Vitest externalizes node_modules and Node can't import the components' .css (TypeError: Unknown file extension ".css"), so inline this package (add css: true only if you assert on styles):
// vitest.config.ts
test: { server: { deps: { inline: [/@eocrm\/design-system/] } } },
Jest (CommonJS) needs to transform this ESM package and its ESM-only dependencies (d3-*, internmap, used by Chart), and stub .css imports:
// jest.config.js — the pattern covers npm/yarn and pnpm (node_modules/.pnpm/<name>@<version>/…) layouts
transformIgnorePatterns: [
'/node_modules/(?!(\\.pnpm/)?(@eocrm[/+]design-system|d3-[^/@]+|internmap)[/@])',
],
moduleNameMapper: { '\\.css$': '<rootDir>/emptyModule.js' },
emptyModule.jsis an empty file (module.exports = {};).- The transformer must convert ESM to CommonJS inside
node_modules:babel-jestwith a rootbabel.config.js(a.babelrcdoes not apply tonode_modules) using@babel/preset-env(targets: { node: 'current' }), orts-jestwithpreset: 'ts-jest/presets/js-with-ts',allowJs: trueandmodule: 'commonjs'.
Components
Grouped for navigation. Per-component contracts are in docs/components/<Name>.md; AI-PRIMER.md indexes them with the tokens table.
Typography
Title— semantic headingText— body / inline textCode— inline<code>chipKbd— keyboard key hint
Actions
Button— action triggersButtonGroup— joined Buttons + segmented controlSocialButton— provider sign-in buttonLink— polymorphic styled anchorDropdownMenu— action menu from a trigger
Form controls
Input— single-line textTextarea— multi-line textOtpInput— one-time-code entry, single-character boxesPasswordInput— password field with eye toggle + optional warningsPasswordStrengthMeter— 4-segment strength visualizationSelect— value picker (single, multi, searchable, async, creatable)OptionsPicker— filter picker (multi/single, grouped, searchable)Checkbox— checkbox with native input + custom paintRadio/RadioGroup— single radio + fieldset wrapperSwitch— binary toggleSlider— controlled slider (single + range, horizontal + vertical)ColorPicker— controlled HEX color picker (popover + inline)IconPicker— controlled icon picker with a popover radio gridFileUpload— controlled file picker with dropzoneImageCrop— controlled image cropper
Form layout
Field— labeled-control unitFormSection— titled group of fieldsFormRow— fields side by sideSettingRow/SettingRow.List— settings-screen row with a shared label column, adornments and a footer slot
Date & time
Calendar— month / week / day / agenda viewsDatePicker/InlineDatePicker— single-date input + popover, or in flowDateRangePicker/InlineDateRangePicker— date-range input + popover, or in flowTimeField— standalone time-of-day input
Layout & structure
Stack— vertical layoutCluster— horizontal layout that wrapsGrid— 2D layout primitiveMasonry— height-balanced masonry layoutConstrain— width / height / flex constraintDivider— separator primitivePage— page-root layout primitiveScreen— full-bleed / centered screen layoutAppLayout— viewport-filling app shellPageHeader— top-of-page heading area
Navigation & app chrome
Rail— collapsible left-side navigationTopBar— sticky application top barTabs— horizontal tab stripTabNav— tab-styled route navigation (links, not tabs)Breadcrumb— navigation trailPagination— numbered nav with windowingCursorPagination— prev / next for streams without a total
Overlays
Modal— focus-locked dialogDrawer— edge-anchored slide-in panelPopover— non-modal floating panel for interactive contentConfirmationPopover— opinionated "Are you sure?" presetTooltip— small floating label on hover / keyboard focus
Feedback & status
Alert— persistent in-flow notificationToast(ToastViewport+toast) — transient notificationsProgress— linear progress barCircularProgress— circular progress / spinnerSkeleton— loading placeholderEmptyState— "nothing here" containerErrorState— page-level status / result screenBadge— status / category pillFilterChip— dismissible "active filter" pill
Data display
Card— bordered containerLinkCard— clickable, full-surface CardDefinitionList— semantic key/value pairs (dl/dt/dd)Table— tabular data primitiveDataTable— server-driven data table with column featuresAccordion— vertically-stacked collapsible panelsKanban— multi-column board (drag-to-reorder + cross-column drag)Sortable— drag-to-reorder list (single column)Image— image with loading + error states
Identity & media
Avatar/AvatarGroup— profile circle / Slack-style stacked rowPersonDisplay— Avatar + name (+ optional description lines)Logo— brand logo lockupBrandIcon— third-party brand marksIconTile— palette-colored icon frame
The library also ships an
AppProvider(theme + i18n + locale wiring),LocaleProvider/I18nProvider, theuseTranslation/useLocalehooks, thepalettehelpers, and Calendar primitive hooks (useMonth,useWeek,useDay,useAgenda). See AI-PRIMER.md for those.
Migrating to 0.7
- Stylesheet:
@eocrm/design-system/styles/global.scssis now@eocrm/design-system/styles/global.css.styles.cssbundles every component's CSS; per-component CSS is also imported automatically by the JS chunks — use one or the other, not both. - Removed subpaths:
styles/tokens.scss,styles/reset.scss,styles/typography.scssandstyles/mixins.scssare gone. Usestyles/tokens.cssfor tokens only. Sass variables:@eocrm/design-tokens(@useit directly).mixins.scsswas removed with no replacement. - Compiled output: the package no longer ships
src/; droptranspilePackagesand any Sass/TSX loader rules for it. - i18n:
I18nProvider/AppProviderno longer bundle Russian. Pass the catalog:<I18nProvider locale="ru" messages={ru}>withimport { ru } from '@eocrm/design-system'. A non-enlocale withoutmessageslogs aconsole.errorand renders English. In a Next.js app router, passrufrom a'use client'providers file (see Bundler notes) — a Server Component cannot pass its function leaves.
Releasing a new version
Releases are automatic. Merging to main triggers the Release workflow (.github/workflows/release.yml), which:
- Runs the quality gate (typecheck, test, lint, build, tarball-contents check).
- If the library actually changed, computes the next version from the latest
v*git tag (patch bump by default) and refuses if that tag already exists. - Synchronizes
@eocrm/design-tokens,@eocrm/design-system, and the generated token contract to one version. - Publishes the token npm package, then the design-system npm package, to npmjs (trusted publishing), then
com.eocrm.design:design-tokens-composeto GitHub Packages (Maven). - Verifies all three registry versions before creating and pushing the
v<version>git tag. - Re-deploys the playground (see below).
There is no manual "Run workflow" button — merging to main is the only way to release. Playground-only changes skip the publish step (no version bump for a demo tweak) but still redeploy the playground.
Bundle-size budgets: size-budgets.json caps gzip JS/CSS per consumer scenario (npm run size, run by Quality test-ds); raise a budget deliberately in the PR that grows the bundle.
Forcing a minor/major bump: edit BUMP (patch → minor / major) in release.yml on a branch and merge. There is no other override by design.
One-time repo setup: Settings → Actions → General → Workflow permissions → "Read and write permissions".
The package name @eocrm/design-system must match an organization (or user) you have publish access to on GitHub. If your org isn't eocrm, rename the package in package.json and the scope: in the workflow.
Playground deployment
The playground (live component gallery) deploys to GitHub Pages automatically as the final step of the Release workflow, so the live site at https://eocrm.github.io/design-system/ always reflects main. It redeploys whenever the quality gate passes — even on playground-only changes, and even if a publish hiccups.
This repo and its Pages site are public. The playground bakes the library's source into its bundle via ?raw imports, so the source is world-readable — that's intentional for an open-source library.
One-time setup: Settings → Pages → Source = "GitHub Actions" (not "Deploy from a branch").
AI agents
@eocrm/design-system-mcp is an MCP server that serves the docs of the @eocrm/design-system version your project has installed. One-time, user-scope install for Claude Code:
claude mcp add --scope user eocrm-design-system -- npx -y @eocrm/design-system-mcp
Codex, Cursor, VS Code and generic setups, plus npx -y @eocrm/design-system-mcp install-skill: see its README.
Where docs live (so you don't have to grep)
- README.md (this file) — install, setup, bundler notes, publishing/deploy ops.
- AI-PRIMER.md — concise primer for AI coding agents. Token table and component index.
docs/components/<Name>.md— per-component contract: props table, examples, anti-patterns. Alsodocs/setup.md,docs/theming.md,docs/transient-state.md.- CLAUDE.md — rules for someone modifying this library (not for consumers).