@fanzeal-intl/ui
v2.0.0
Published
React + Tailwind CSS v4 component library implementing the Fanzeal design system.
Readme
@fanzeal-intl/ui
React + Tailwind CSS v4 component library implementing the Fanzeal design system.
Design source of truth: Fanzeal Design System (Figma).
Requirements
- Node >= 22
- pnpm
Development
pnpm install
pnpm storybook # live component playground at http://localhost:6006
pnpm test # run the test suite once
pnpm test:watch # run tests in watch mode
pnpm lint # ESLint
pnpm typecheck # tsc --noEmit
pnpm build # build the library to dist/
pnpm build-storybook # static Storybook buildUsage
Published as @fanzeal-intl/ui.
import { Button } from '@fanzeal-intl/ui';
function Example() {
return <Button variant="primary">Continue</Button>;
}src/styles/tokens.css is raw, unprocessed Tailwind v4 source (@import 'tailwindcss', @theme, @layer components) — it is not a stylesheet you side-effect-import from a .tsx file. Import it from your own app's CSS entry file instead, i.e. wherever you already run Tailwind (e.g. app/globals.css):
/* app/globals.css (or wherever your Tailwind entry file lives) */
@import '@fanzeal-intl/ui/styles/tokens.css';Treat @fanzeal-intl/ui/styles/tokens.css as your Tailwind entry point, not an addition to one. It already does @import 'tailwindcss' internally, so if your own entry file also imports tailwindcss separately, you'll get Tailwind's base styles and utilities twice (duplicated resets, bloated output, and possible specificity surprises). Import Tailwind exactly once — through this file.
The design system defaults to dark mode. Add a .light class to <html> (or any ancestor) to opt into light mode.
Architecture
Tokens:
src/styles/tokens.css— Tailwind v4@theme, mirrors Figma variable names exactly.Components:
src/components/<name>/— each ships a component file, a colocated Code Connect mapping (.figma.tsx), a Storybook story, and tests.Component API convention: Figma visual-style/size variants become CVA-driven props; Figma interaction-state variants (Hover/Disabled) become real CSS, never props; Figma
TEXTcontent properties becomechildren; FigmaBOOLEAN+INSTANCE_SWAPslot pairs become a single optional prop.asChild+ slot content requiresSlottable. Any component that combines anasChild-style polymorphic prop (via@radix-ui/react-slot) with optional sibling slot content (e.g. Button'sicon) must wrapchildrenin Radix'sSlottable, not a plain<>...</>Fragment.Slot'sSlotCloneclones the merged className/ref/handlers onto whatever occupies theSlottableposition; a bare Fragment only forwardskey/children, so those merged props are silently dropped and the consumer's rendered element (e.g. an<a>fromasChild) loses its styling and ref. Seesrc/components/button/button.tsxfor the reference implementation. Expect this to recur in every future component that pairsasChildwith slot content — build it in from the start rather than rediscovering it via a failing test.
See docs/superpowers/specs/2026-08-07-ui-library-foundation-design.md for the full design rationale.
