@bryan-cunningham-design/igenie-ui
v0.2.0
Published
i-Genie React component library — typed, accessible primitives with no copy of their own, themed entirely by CSS custom properties from @bryan-cunningham-design/igenie-tokens.
Maintainers
Readme
@bryan-cunningham-design/igenie-ui
React component library for i-Genie: typed, accessible primitives built on
Base UI. They carry no colour values and no user-facing
copy of their own — every surface, radius and shadow resolves to a token from
@bryan-cunningham-design/igenie-tokens, and every string arrives through
children or a prop. A new locale therefore costs nothing here.
Primitives only. The blocks are source you copy, not package API — see Looking for the blocks? below.
Install
pnpm add @bryan-cunningham-design/igenie-ui @bryan-cunningham-design/igenie-tokensOn the public npm registry — no token, no .npmrc. See
docs/guides/engineer.md for the CSS setup that follows.
CSS setup — two lines
Tailwind v4 detects source files relative to wherever @import "tailwindcss"
appears, so your app owns that import, not this package:
@import "tailwindcss";
@import "@bryan-cunningham-design/igenie-ui/index.css";That is the whole setup. index.css pulls in the tokens, the base-vega
variants, and an @source "./dist" that points Tailwind at this package's
compiled output so our class names survive your build. No node_modules paths
to write, no Tailwind config to extend.
Dark mode activates on either .dark or [data-theme="dark"] on an ancestor.
No Tailwind in your app? Take one line instead of two:
@import "@bryan-cunningham-design/igenie-ui/standalone.css";That is every component's CSS, compiled when this package was published, for an
app that resolves it from a CDN and has no node_modules to scan. Take one or
the other and never both: this one carries Tailwind's preflight, and it cannot
be extended with utilities of your own.
Usage
import { Button, Card, CardContent } from "@bryan-cunningham-design/igenie-ui"Every component is a named export from the package root, with one exception, so a component moving between files inside the library is never a breaking change for you.
The one exception: Toaster
Toaster is not on the root. It lives behind its own subpath, and it is the only
component that needs a package you install yourself:
pnpm add sonnerimport { Toaster } from "@bryan-cunningham-design/igenie-ui/sonner"That separation is deliberate rather than tidy-mindedness. toast() and
<Toaster> talk to each other through one module-level store, so the library
cannot ship its own copy of sonner without your toasts reaching a different
toaster than the one on your page. Putting it on the root instead would make
your bundler resolve sonner to build a Button, and break the build of anyone
who declined the install.
sonner is marked optional in peerDependenciesMeta, so skipping the install is
silent and costs nothing until you import that path. Nothing else in the library
is affected.
The full account — where to mount it, the three things that break it quietly, and
why richColors also wants a theme prop — is under Optional peers in
docs/guides/engineer.md.
The other subpaths, none of them a component
@bryan-cunningham-design/igenie-ui/hooks/use-mobile—useIsMobile(), amatchMediahook that returnstrueunder 768px. It is whatSidebaruses to become a drawer, and it carries no peer.@bryan-cunningham-design/igenie-ui/index.css— the stylesheet from the CSS setup above, and@bryan-cunningham-design/igenie-ui/standalone.css, its precompiled alternative.
SidebarProvider writes its open state to a cookie named sidebar_state,
with the widths 16rem expanded and 3rem collapsed-to-icons. Read that cookie
on the server and pass it as defaultOpen if you want the sidebar to render in
the right state before hydration.
Looking for the blocks?
LoginForm, FilterPanel and StatsDashboard are not published, and no
longer resolve from this package. A block is a whole screen — it decides where
state lives, what the labels say and how the fields are grouped — and none of
that can be reached from outside a subtree you do not render. So they are handed
over as source instead: open
the Blocks gallery and
copy the file. It imports this package by name, so it compiles unchanged. The
gallery sits behind a sign-in — the site is private. The way in is your address
in .github/access-readers.jsonc: a one-line pull request if you can see this
repository, and otherwise ask whoever gave you access to the package, since
they can open it for you.
Extending it in your own application
You never need to fork this package to get a variant it does not have. Three moves cover it, all in your application's own stylesheet or source:
- Redefine a semantic variable after the two imports, in both modes.
- Add your own
@themeblock. Tailwind v4 merges theme namespaces, so--color-brand-500mintsbg-brand-500for your app and no other. - Compose on an exported
cvaconfig.alertVariants,badgeVariants,buttonVariants,buttonGroupVariantsandtabsListVariantsare public; a variant layered on one keeps receiving upstream fixes.
Each is worked through on the docs site under Theming → Extending the system in your own application.
For maintainers of this library
Everything below is about producing this package, not consuming it. If you installed it, you are done above.
src/components/ is generated — do not edit it
src/components/*.tsx and styles/base.css are vendored from the shadcn
registry by scripts/vendor-ui.mjs. Local edits are overwritten on the next
re-vendor:
pnpm ui:vendorThat refetches every vendored component, rewrites their imports to local paths,
resolves icons against lucide-react, refetches the vendored hooks, and
regenerates styles/base.css. It is vendored rather than imported so the
library does not take a CLI as a runtime dependency. Review the diff before
committing — an upstream change to a component's DOM structure is a visual
change to every consuming product.
The component list lives in scripts/lib/registry.mjs; add a name there and
re-run the command to pull it in. To change how a component looks, either move
the token it resolves to, or wrap it in a new hand-written file — never patch
the vendored source.
Coverage against the upstream registry
pnpm parityDiffs the pinned @shadcn registry's component list against this library and
prints the table as markdown for a pull request description. The inventory has
exactly one definition — scripts/lib/registry.mjs — which is why the table
cannot drift, and the shipped column is derived from the site registry, so
nothing reads as implemented until it has a demo.
Four statuses, matching STATUS in scripts/parity.mjs: shipped,
deferred (would add a third-party runtime package not yet taken on),
recipe (a documented composition with nothing to install, left out of the
denominator) and planned.
This report is deliberately not published on the docs site. Coverage is a maintainer's progress bar; on a consumer page it invites the reader to model the library as a partial copy of something else.
Not every file in src/components/ is generated. A component whose banner
says FORKED has been ejected from the vendor set: ui:vendor skips it, upstream
fixes no longer arrive on their own, and the file is hand-owned from then on.
FORKED in scripts/lib/registry.mjs is the list, and it records the shadcn
version each fork happened at. Read the banner before you assume an edit will
survive.
Hand-written code lives in src/lib/ and index.css, and is safe to edit.
Seeing what upstream offers
pnpm parity reports coverage against the pinned @shadcn registry. To look
further — a namespaced or third-party registry the parity table knows nothing
about — search the CLI instead:
pnpm dlx [email protected] search @shadcn -q <term> -t ui --cwd packages/uiWith no registry argument it searches every registry listed in
components.json. Finding a name there is not an instruction to run add: the
route in is still COMPONENTS in scripts/lib/registry.mjs plus
pnpm ui:vendor --only <name>, which resolves the registry's import aliases and
icon placeholders for you.
One-off shadcn add
components.json is kept accurate so the CLI still works for a single item:
pnpm dlx [email protected] add <name>It writes @/lib/utils-style imports. Rewrite them to relative paths
(../lib/utils, ./button) before building — the package ships unbundled, so
an unresolved alias would reach consumers intact.
Keep components.json free of comments. Biome parses it as JSONC and will
not complain, but the shadcn CLI parses it as strict JSON and dies on the first
// — taking both commands above with it. The aliases in it exist only for
these one-offs; nothing in src/ resolves them.
Build
pnpm --filter @bryan-cunningham-design/igenie-ui buildtsdown emits per-module ESM plus declarations, unbundled and unminified. That
is deliberate: bundling would collapse the per-file "use client" directives
most components rely on, and minifying would mangle the class strings Tailwind
needs to scan.
Two settings in tsconfig.json exist only to serve the vendored code, and
cannot be commented inline because Biome parses this package's tsconfig.json
as strict JSON:
allowUmdGlobalAccess— the registry shipsskeleton.tsxusing theReactUMD global with no import. Accommodating it here means a re-vendor never needs a hand-edit.baseUrl/paths— resolves the@/aliases incomponents.jsonso a one-offshadcn addworks. Nothing insrc/uses them.
