@incorta/ui
v1.3.2
Published
Incorta design system — shadcn components on Base UI, Tailwind v4, React 19
Readme
@incorta/ui
Incorta's design system: shadcn components built on Base UI, styled with Tailwind CSS v4, targeting React 19.
Published to npm as @incorta/ui.
Installation
pnpm add @incorta/uiPeer dependencies: react@^19 and react-dom@^19. Styling requires Tailwind CSS v4 in the
consuming app (the package ships design tokens and unstyled class names, not compiled CSS).
Usage
Components are imported via their subpath, not a barrel (avoids name collisions across 60+ components and keeps bundles lean):
import { Button } from '@incorta/ui/components/button';
import { Card, CardContent, CardHeader } from '@incorta/ui/components/card';
import { useIsMobile } from '@incorta/ui/hooks/use-mobile';
import { cn } from '@incorta/ui'; // the only root exportTailwind setup
In your app's Tailwind v4 CSS entry, import the package stylesheet. It brings in Tailwind,
the Incorta design tokens, and an @source directive that makes Tailwind scan this package
for class usage:
@import '@incorta/ui/globals.css';If your app manages its own Tailwind layers (e.g. scoped/unlayered setups inside a microfrontend), import only the tokens and register the package as a source yourself:
@import 'tailwindcss';
@import '@incorta/ui/tokens.css';
@source '../node_modules/@incorta/ui/dist';Dark mode follows the shadcn convention: add the dark class to an ancestor element.
Development
pnpm install
pnpm typecheck
pnpm build # emits ESM + d.ts to dist/ via tsc, copies stylesNew components are added with the shadcn CLI (configured by components.json):
pnpm dlx shadcn@latest add <component>Testing changes locally in a consumer app
To try an unpublished change in a real app (e.g. incorta-apps) without cutting a release,
symlink the consumer's node_modules/@incorta/ui straight at this repo, so rebuilding here
is immediately visible there — no publish/push step per change.
Deliberately don't use pnpm link/npm link for this: in a pnpm workspace, pnpm link
<path> doesn't just touch node_modules — it recorded the link as a persistent overrides
entry in the workspace root package.json/pnpm-workspace.yaml as a side effect (observed
with incorta-apps on pnpm 10.26.1), which is exactly the kind of change that could get
committed by accident. A plain filesystem symlink (ln -s) achieves the identical resolution
result without touching any file pnpm or git track.
A raw symlink means this repo's own node_modules/react (installed here to build/test the
library) sits alongside the consumer's, which can cause duplicate-React "Invalid hook call"
errors. Consumers that link this package must add dedupe: ['react', 'react-dom'] to their
bundler's resolve config (e.g. Vite's resolve.dedupe) to force a single instance — see
incorta-apps/apps/web/vite.config.ts for a working example.
In this repo, build once (and keep rebuilding on change):
pnpm run build # one-off
pnpm run build:watch # or leave this running while you iterateIn the consumer app, wire up a pair of scripts like incorta-apps/apps/web/package.json's
ui:local/ui:remote:
"ui:local": "target=$(cd <relative-path-to-this-repo> && pwd) && ln -sfn \"$target\" node_modules/@incorta/ui",
"ui:remote": "pnpm install"Resolve the path via cd ... && pwd rather than passing a relative path straight to ln:
a relative symlink target resolves relative to the symlink's own directory (easy to miscount
when it's nested several node_modules levels deep), and — critically — piping a failed cd
straight into ln's argument via ln -sfn "$(cd bad/path && pwd)" ... fails silently: the
substitution returns an empty string but ln still runs and happily creates a broken symlink
pointing nowhere. Assigning to a variable first (target=$(cd ... && pwd) && ln ...) makes
the cd failure propagate through the &&, so a wrong path aborts loudly instead of quietly
corrupting node_modules/@incorta/ui.
ui:local swaps in the symlink; ui:remote reinstalls from the lockfile to restore the
real published dependency. Neither touches package.json or the lockfile — nothing to
accidentally commit.
The build is bundler-targeted ESM: import specifiers are left extensionless and internal
#components/* / #lib/* / #hooks/* aliases are resolved through the package.json
imports field, so consumers need a bundler (Vite, webpack, Next.js, …).
Releases
Releases are fully automated with semantic-release
(modeled on layout-dev/Layout): every push to main runs the
Release workflow, which determines the next version from
Conventional Commits, publishes to npm, and creates
the GitHub release. main is a protected branch, so nothing is committed back to it: the
changelog lives on GitHub Releases, and the
version field in the committed package.json is a placeholder that gets stamped with the
real version only in the published tarball.
feat:→ minor,fix:/docs:/chore:/refactor:/… → patch,feat!:or aBREAKING CHANGE:footer → major.- Commit messages are enforced locally by commitlint (husky
commit-msghook). - npm auth uses Trusted Publishing (OIDC): the
workflow authenticates as
Incorta/[email protected]directly — no long-lived npm token. Provenance attestations are off: npm only supports them for public source repositories, and this repository is private. - One-time bootstrap: npm cannot trust-publish a package that doesn't exist yet, so the
first-ever publish needs a short-lived npmjs granular token in the
NPM_TOKENrepo secret. After that release, configure the trusted publisher on the package's npmjs Settings page (GitHub Actions /Incorta/ui/release.yml) and delete the secret.
There is no manual version bumping: package.json#version is managed by the release
pipeline.
