@ravnhq/efni
v0.3.0
Published
Efni — Ravn's design system: framework-free React components on Base UI, with the token layer they are built on.
Readme
@ravnhq/efni
Ravn's design system: framework-free React components on Base UI, built on a three-tier token layer.
Docs: https://ravnhq.github.io/efni-ds/
To work on Efni's components, tokens, or documentation site, clone
ravnhq/efni-ds and run pnpm dev.
Install
Efni is a public package on npm, under Ravn's @ravnhq org:
pnpm add @ravnhq/efniThe package ships compiled JavaScript and type declarations. Public npm access allows installation in local projects and CI.
Setup
Three things, and the package cannot do them for you.
1. The stylesheet. Replace the contents of app/globals.css with this one
line — do not add it alongside what is already there.
/* app/globals.css */
@import "@ravnhq/efni/styles.css";It brings Tailwind, the tokens, the shadcn bridge, and an @source pointing at
this package, since Tailwind v4 does not scan node_modules.
Replacing matters. create-next-app writes a globals.css that defines
--background and --foreground — the same names the shadcn bridge maps onto
Efni tokens — plus body { font-family: Arial, … } and its own @theme inline
font mapping. Keep them and they win: the page stays light with
data-theme="dark" set, and every component renders in Arial. Nothing errors,
so it is easy to miss.
2. The three font variables. The tokens map --font-sans, --font-mono and --font-display onto them, so without these you get no typography.
import { Hanken_Grotesk, Inter, JetBrains_Mono } from "next/font/google";
const display = Hanken_Grotesk({ variable: "--font-hanken-grotesk", subsets: ["latin"] });
const body = Inter({ variable: "--font-inter", subsets: ["latin"] });
const mono = JetBrains_Mono({ variable: "--font-jetbrains-mono", subsets: ["latin"] });3. The theme attributes on <html>. data-theme is required; data-tool only if you want an accent other than the default.
<html data-theme="light" className={`${display.variable} ${body.variable} ${mono.variable}`}>Use
One import per component:
import { Button } from "@ravnhq/efni/button";
import { Typography } from "@ravnhq/efni/typography";
<Typography variant="h1">Mission MB-4472</Typography>
<Button>Save</Button>Peer dependencies
react, react-dom and tailwindcss come with create-next-app.
lucide-react does not — install it:
pnpm add lucide-reactSixteen components import it directly, so you need it even if you never touch
Icon. It is a peer rather than a bundled dependency because Icon takes an
icon you pass in, which has to resolve from your tree:
import { Star } from "lucide-react";
import { Icon } from "@ravnhq/efni/icon";
<Icon icon={Star} />Four more are optional, needed only by the component that uses them — and needed directly in your project, because a component's own dependency is not importable from your code:
| Add this | If you use |
|---|---|
| pnpm add react-hook-form | Form — you call useForm yourself |
| pnpm add "@tanstack/react-table@^8" | DataTable with typed ColumnDef columns |
| pnpm add sonner | toast() — the package exports Toaster, you call toast |
| pnpm add react-day-picker | Calendar or DatePicker with typed props — Calendar types its props as ComponentProps<typeof DayPicker> |
The ^8 on react-table is not optional. Plain pnpm add @tanstack/react-table
installs v9, which renamed getCoreRowModel to createCoreRowModel and
useReactTable to ReactTable. DataTable is on v8 deliberately, so v9 breaks
the build with Export getCoreRowModel doesn't exist in target module.
If you see TypeScript errors inside @ravnhq/efni/dist, you are missing a
peer. The declarations reference the peers' types, so an absent peer surfaces
as Cannot find module inside our .d.ts files. Install the peer the first
error names.
Inside a monorepo, as a workspace package
A monorepo that wants to edit Efni alongside its apps — Mission Board is the
first — can bring packages/ui in as a workspace member instead of installing
from npm. Prefer a git submodule or subtree of ravnhq/efni-ds: it keeps the
history, and git submodule update is the upgrade. A copy of the directory
works too.
# pnpm-workspace.yaml
packages:
- "apps/*"
- "vendor/efni-ds/packages/ui" # wherever you put it// apps/<app>/package.json
"dependencies": { "@ravnhq/efni": "workspace:*" }In a workspace the package's exports point at its TypeScript source.
Edits show up on save through Turborepo's
Just-in-Time internal package
pattern:
- Your bundler compiles our source. In Next:
transpilePackages: ["@ravnhq/efni"]. - Peers resolve from your workspace — the same list as above.
With Turborepo. The package carries the check-types script the
Just-in-Time shape expects (tsc --noEmit), plus one it does not: build
(tsup), which produces what npm receives. A host with
build: { dependsOn: ["^build"] } will run that tsup before your app builds,
and check-types: { dependsOn: ["^check-types"] } will typecheck the package,
tests included. Both are unnecessary for you — your app compiles the source —
and both pass, because the directory is self-contained: its tsconfig.json
extends nothing, and everything its build and tests import — the peers included
— is declared in its own devDependencies, so it does not depend on your
auto-install-peers setting. Let them run; Turborepo caches them.
dist/ is gitignored here, so a submodule never carries one. A directory copy
taken after a local pnpm build does, and styles.css would scan those stale
files alongside src — delete dist when you copy.
Licence
MIT
