@t2000/ui
v0.1.4
Published
t2000 design system — Geist Design System tokens + shadcn/ui primitives themed for the t2000 brand family.
Maintainers
Readme
@t2000/ui
t2000 design system — Geist Design System tokens + shadcn/ui primitives themed for the t2000 brand family.
Consumed by:
t2000.ai—apps/web(accent:#0072F3blue)mpp.t2000.ai—apps/gateway(accent:#12A594teal)suimpp.dev— separate repo (accent: monochrome)developers.t2000.ai—apps/docsMintlify (token hex values synced manually)
Install
pnpm add @t2000/uiRequired Tailwind plugin
Several primitives (Dialog, DropdownMenu, Sheet, Tooltip) ship with data-[state=open]:animate-in / animate-out utility classes for their open/close transitions. You must install the matching plugin or those primitives will render with no animation (functional, but visually janky):
| Your Tailwind version | Plugin to install | How it's wired |
|---|---|---|
| Tailwind v4 | tw-animate-css | @import "tw-animate-css"; in globals.css (top, alongside @import "tailwindcss";) |
| Tailwind v3 | tailwindcss-animate | plugins: [require('tailwindcss-animate')] in tailwind.config.ts |
# Tailwind v4 consumers
pnpm add -D tw-animate-css
# Tailwind v3 consumers
pnpm add -D tailwindcss-animateQuickstart
Pick one path based on which major version of Tailwind you run. The two paths are mutually exclusive — don't combine them.
Path A — Tailwind v4 (recommended; current major version)
/* app/globals.css */
@import "tailwindcss";
@import "tw-animate-css"; /* required for primitive animations */
@import "@t2000/ui/tokens"; /* --ds-* + --fg-* primitives */
@import "@t2000/ui/tokens/page"; /* optional: t2k-* utility classes */
@import "@t2000/ui/tokens/responsive";/* optional: responsive helpers */
@import "@t2000/ui/tokens/theme"; /* @theme block — registers utilities */
:root {
--t2k-accent: var(--ds-blue-700);
--t2k-accent-hover: var(--ds-blue-800); /* tier-down for accent buttons */
}You don't need a tailwind.config.ts with v4 — the @theme block in tokens/theme registers bg-background, text-foreground, font-sans, etc.
Path B — Tailwind v3 (legacy)
// app/layout.tsx
import '@t2000/ui/tokens';
import '@t2000/ui/tokens/page';
import '@t2000/ui/tokens/responsive';
import './globals.css'; // your --t2k-accent override// tailwind.config.ts
import t2000UiPreset from '@t2000/ui/tailwind-preset';
import tailwindcssAnimate from 'tailwindcss-animate';
export default {
presets: [t2000UiPreset],
plugins: [tailwindcssAnimate], // required for primitive animations
content: [
'./app/**/*.{ts,tsx}',
'./node_modules/@t2000/ui/dist/**/*.js',
],
};/* app/globals.css */
:root {
--t2k-accent: var(--ds-blue-700);
--t2k-accent-hover: var(--ds-blue-800); /* tier-down for accent buttons */
}Usage (both paths)
import { Button, Card, cn } from '@t2000/ui';
export function Hero() {
return (
<Card className={cn('p-6')}>
<Button>Get started</Button>
</Card>
);
}Primitives
15 themed shadcn/ui primitives ship today. 14 from the main barrel: Button, Card, Badge, Table, Separator, Dialog, Sheet, DropdownMenu, Tabs, Tooltip, Accordion, ScrollArea, Command, Skeleton. One from a client-only sub-entry: Toaster (Sonner) from @t2000/ui/toaster.
Skeleton
Loading placeholders, per CURSOR.md §9 "Skeleton states". Use shapes that match the eventual content (not generic spinners):
import { Skeleton } from '@t2000/ui';
<Skeleton className="h-4 w-32" /> {/* metric label */}
<Skeleton className="h-8 w-48 mt-2" /> {/* metric value */}The pulse animation is Tailwind's built-in animate-pulse (honors prefers-reduced-motion).
Toaster (Sonner)
Import from the @t2000/ui/toaster sub-entry, not the main barrel. Sonner uses client-only React hooks at the top of its render function; the sub-entry ships with 'use client' baked in so it works when imported from a server component (e.g. your root layout). The main @t2000/ui barrel stays RSC-friendly so server components can import Card / Badge / Table without paying client-bundle cost.
Mount once at your app root; trigger from anywhere via the re-exported toast:
// app/layout.tsx (server component — fine, the sub-entry is 'use client')
import { Toaster } from '@t2000/ui/toaster';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Toaster />
</body>
</html>
);
}
// any-client-component.tsx (must already have 'use client' to call toast())
'use client';
import { Button } from '@t2000/ui';
import { toast } from '@t2000/ui/toaster';
<Button onClick={() => toast.success('Copied to clipboard')}>Copy</Button>Defaults: theme="dark", position="bottom-right". Override either as a <Toaster theme="light" position="top-center" /> prop. Sonner v2 inherits --popover + --popover-foreground tokens via the bg-popover / text-popover-foreground Tailwind classes in classNames.toast, which @t2000/ui/tokens/theme maps to --bg-elevated (Geist surface).
Utility classes
Optional helpers from @t2000/ui/tokens/page — opt-in via class name (not applied to every element by default):
| Class | Purpose | Pattern (per CURSOR.md §9) |
|---|---|---|
| .t2k-card-hover | Interactive Card hover. Border darkens to --ds-gray-alpha-500, bg stays flat. | Border-strengthen |
| .t2k-list-row | Row in a list (accordion trigger, sidebar item). Bg lifts to --ds-gray-alpha-100 on hover. | Surface-lift |
| .t2k-link | Inline link in body copy. Color shifts to --t2k-accent on hover, underline tracks. | Color-shift |
| .t2k-hero-headline | Text overlaying a glow/gradient. Forces GPU compositing to avoid fringing. | (anti-aliasing fix) |
| .t2k-code | Code blocks (Geist Mono). Forces line-height: 1.75 — Tailwind's leading-relaxed is under-leaded for Geist Mono. | (line-height fix) |
| .t2k-tabular | Re-assert tabular-nums inside an element that opted out via .t2k-prose. | (numeric grid) |
| .t2k-prose | Opt OUT of global tabular-nums (body copy, long-form, FAQ answers). | (prose readability) |
Global defaults (no class needed): dark color-scheme, -webkit-font-smoothing: antialiased, text-rendering: optimizeLegibility, font-feature-settings: "ss01" "cv11", font-variant-numeric: tabular-nums, 4px Geist-blue focus ring on every <button> / <a> / <input> / <textarea> / <select> / [tabindex] via low-specificity :where() rule. Per CURSOR.md §9 "Anti-aliasing & rendering" + "Focus rings" + "Dark mode is the default".
Per-property accent
Every primitive references var(--t2k-accent). Each consumer redefines it (plus --t2k-accent-hover for the tier-down on accent buttons) in its own globals.css:
| Property | Accent | Override |
|---|---|---|
| t2000.ai + developers.t2000.ai | #0072F3 blue | --t2k-accent: var(--ds-blue-700); --t2k-accent-hover: var(--ds-blue-800); |
| mpp.t2000.ai | #12A594 teal | --t2k-accent: var(--ds-teal-700); --t2k-accent-hover: var(--ds-teal-800); |
| suimpp.dev | monochrome (links only) | --t2k-accent: var(--ds-blue-700); --t2k-accent-hover: var(--ds-blue-800); (no fills) |
--t2k-accent-hover is the tier-down per CURSOR.md §9 "Tier-down" hover discipline — one ramp tier darker than the resting accent. If you omit it, the bg-accent-hover Tailwind utility falls back to --t2k-accent (accent buttons will appear static on hover instead of darkening).
Dark / light mode
Default theme is dark (matches Geist). To switch to light, set data-theme="light" on <html> (or data-mode="light" on any ancestor):
// app/layout.tsx — server-rendered light mode
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" data-theme="light">
<body>{children}</body>
</html>
);
}// Client-side toggle
'use client';
import { useEffect, useState } from 'react';
export function ThemeToggle() {
const [theme, setTheme] = useState<'dark' | 'light'>('dark');
useEffect(() => {
document.documentElement.dataset.theme = theme;
}, [theme]);
return (
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
{theme === 'dark' ? 'Switch to light' : 'Switch to dark'}
</button>
);
}Tokens
CSS variables only — port of Vercel's Geist Design System. 314 custom properties under --ds-* (primitive scales) and --fg-* (semantic foregrounds) namespaces.
Canonical hex values synced to apps/docs/docs.json (Mintlify cannot import this package; sync is manual per token edit):
| Var | Hex | Used by |
|---|---|---|
| --ds-blue-700 | #0072F3 | t2000.ai, developers.t2000.ai, suimpp.dev link color |
| --ds-teal-700 | #12A594 | mpp.t2000.ai |
| --ds-background-100 | #0a0a0a | All surfaces (dark mode default) |
| --ds-background-200 | #000000 | All page backdrops |
Primitive vs alias names
Seven Tailwind-reserved names (--font-sans, --font-mono, --font-display, --radius-sm, --radius-md, --radius-lg, --ease-out) collide with Tailwind v4's @theme namespaces. To avoid CSS custom-property cycles, these are stored under namespaced primitives and re-exported as bare-name aliases:
| Primitive (canonical) | Alias (consumer-facing) |
|---|---|
| --ds-font-sans | --font-sans |
| --ds-font-mono | --font-mono |
| --ds-font-display | --font-display |
| --ds-radius-sm | --radius-sm |
| --ds-radius-md | --radius-md |
| --ds-radius-lg | --radius-lg |
| --ds-ease-out | --ease-out |
You can reference either form. The Tailwind v3 preset and page-level utility classes use the bare aliases; the Tailwind v4 @theme block uses the --ds-* primitives.
Fonts
Consumers wire Geist Sans + Geist Mono via next/font/google in their root layout. @t2000/ui tokens reference the resulting CSS variables (--font-sans, --font-mono).
import { GeistSans } from 'geist/font/sans';
import { GeistMono } from 'geist/font/mono';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={`${GeistSans.variable} ${GeistMono.variable}`}>
<body>{children}</body>
</html>
);
}License
MIT
