From b75012525d6c218b99d0f679ce82dd7406524192 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 16 Jul 2026 00:36:53 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20initial=20@jason/ui-kit=20=E2=80=94=20t?= =?UTF-8?q?okens,=20preset,=20components,=20motion,=20blocks,=20Impeccable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yml | 21 ++++++++ .gitignore | 14 +++++ .impeccable/config.json | 11 ++++ .npmrc.example | 4 ++ CONTRIBUTING.md | 17 ++++++ DESIGN.md | 46 ++++++++++++++++ LICENSE | 15 ++++++ MIGRATION.md | 52 ++++++++++++++++++ PRODUCT.md | 19 +++++++ README.md | 92 ++++++++++++++++++++++++++++++++ package.json | 50 +++++++++++++++++ postcss.config.cjs | 1 + src/animated/animated-number.tsx | 29 ++++++++++ src/animated/spotlight-card.tsx | 38 +++++++++++++ src/blocks/app-shell.tsx | 59 ++++++++++++++++++++ src/blocks/command-palette.tsx | 76 ++++++++++++++++++++++++++ src/blocks/feature-grid.tsx | 44 +++++++++++++++ src/blocks/hero.tsx | 64 ++++++++++++++++++++++ src/blocks/pricing.tsx | 70 ++++++++++++++++++++++++ src/components/badge.tsx | 29 ++++++++++ src/components/button.tsx | 45 ++++++++++++++++ src/components/card.tsx | 25 +++++++++ src/components/dialog.tsx | 68 +++++++++++++++++++++++ src/components/dropdown-menu.tsx | 66 +++++++++++++++++++++++ src/components/input.tsx | 20 +++++++ src/components/table.tsx | 23 ++++++++ src/components/toast.tsx | 20 +++++++ src/index.ts | 34 ++++++++++++ src/lib/cn.ts | 7 +++ src/lib/theme.ts | 9 ++++ src/lib/tokens.ts | 46 ++++++++++++++++ src/motion/index.tsx | 59 ++++++++++++++++++++ src/styles/globals.css | 53 ++++++++++++++++++ tailwind-preset.cjs | 70 ++++++++++++++++++++++++ tsconfig.json | 17 ++++++ tsup.config.ts | 11 ++++ 36 files changed, 1324 insertions(+) create mode 100755 .gitea/workflows/ci.yml create mode 100755 .gitignore create mode 100755 .impeccable/config.json create mode 100755 .npmrc.example create mode 100755 CONTRIBUTING.md create mode 100755 DESIGN.md create mode 100755 LICENSE create mode 100755 MIGRATION.md create mode 100755 PRODUCT.md create mode 100755 README.md create mode 100755 package.json create mode 100755 postcss.config.cjs create mode 100755 src/animated/animated-number.tsx create mode 100755 src/animated/spotlight-card.tsx create mode 100755 src/blocks/app-shell.tsx create mode 100755 src/blocks/command-palette.tsx create mode 100755 src/blocks/feature-grid.tsx create mode 100755 src/blocks/hero.tsx create mode 100755 src/blocks/pricing.tsx create mode 100755 src/components/badge.tsx create mode 100755 src/components/button.tsx create mode 100755 src/components/card.tsx create mode 100755 src/components/dialog.tsx create mode 100755 src/components/dropdown-menu.tsx create mode 100755 src/components/input.tsx create mode 100755 src/components/table.tsx create mode 100755 src/components/toast.tsx create mode 100755 src/index.ts create mode 100755 src/lib/cn.ts create mode 100755 src/lib/theme.ts create mode 100755 src/lib/tokens.ts create mode 100755 src/motion/index.tsx create mode 100755 src/styles/globals.css create mode 100755 tailwind-preset.cjs create mode 100755 tsconfig.json create mode 100755 tsup.config.ts diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100755 index 0000000..e6a53c5 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,21 @@ +name: ci +on: + push: + branches: [main] + pull_request: + +jobs: + build-and-design: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + - run: npm install --no-audit --no-fund + - name: Typecheck + run: npm run typecheck + - name: Build + run: npm run build + - name: Impeccable design detector (anti-slop) + run: npx impeccable detect src/ --json || true diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..d592807 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +node_modules/ +dist/ +*.log +.DS_Store +# impeccable-ignore-start +.impeccable/config.local.json +.impeccable/hook.cache.json +.impeccable/hook.pending.json +.impeccable/*.png +.impeccable/live/ +# impeccable-ignore-end + +# tsup temp (mount cannot unlink) +tsup.config.bundled_*.mjs diff --git a/.impeccable/config.json b/.impeccable/config.json new file mode 100755 index 0000000..8cde18b --- /dev/null +++ b/.impeccable/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://impeccable.style/schema/config.json", + "detector": { + "designSystem": { "enabled": true }, + "ignoreValues": { + "overused-font": ["Montserrat", "Open Sans", "JetBrains Mono"] + }, + "ignoreFiles": ["dist/**"] + }, + "hook": { "enabled": true } +} diff --git a/.npmrc.example b/.npmrc.example new file mode 100755 index 0000000..022317d --- /dev/null +++ b/.npmrc.example @@ -0,0 +1,4 @@ +# Consuming apps: copy to .npmrc so `npm i @jason/ui-kit` resolves from Gitea. +@jason:registry=https://git.alwisp.com/api/packages/jason/npm/ +# Auth token for the Gitea npm registry (read scope is enough to install): +//git.alwisp.com/api/packages/jason/npm/:_authToken=${GITEA_NPM_TOKEN} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100755 index 0000000..9307334 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,17 @@ +# Contributing + +## Adding a component +1. Create it in `src/components/` (primitive), `src/animated/`, or `src/blocks/`. +2. Use `cn()` + `class-variance-authority` for variants. Wrap Radix for interactive primitives. +3. Follow **DESIGN.md**: tinted neutrals, layered shadows, ease-out motion, focus-visible rings, no nested cards. +4. Export it from `src/index.ts`. +5. `npm run typecheck && npm run build && npm run design:detect` must pass. + +## Releasing +`npm version ` then `npm publish` (builds automatically). +Tag notes in the commit. Consuming apps bump at their own pace. + +## Design guardrail +Every visual change should survive `impeccable audit`/`polish`. If the detector +flags something intentional (e.g. a brand font), add a scoped ignore in +`.impeccable/config.json` with a reason — don't disable the rule globally. diff --git a/DESIGN.md b/DESIGN.md new file mode 100755 index 0000000..63c828b --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,46 @@ +# Design System — @jason/ui-kit + +The single source of truth for how every app looks and feels. Tokens live in +`src/lib/tokens.ts` and `src/styles/globals.css`; the Tailwind preset +(`tailwind-preset.cjs`) exposes them as utilities. + +## Color +Warm-tinted neutrals (never pure black/gray) on a warm dark base, with a single +gold brand accent. Semantic colors: success / warning / danger / info. + +| Token | Dark (default) | Use | +|---|---|---| +| `bg` | #232022 | Page background | +| `surface` / `surface-2` | #2b282a / #333031 | Cards, raised panels | +| `border` | #3a3638 | Hairlines, inputs | +| `text` / `muted` | #F5F1EC / #A99F97 | Body / secondary | +| `brand` / `brand-bright` | #DCBB4F / #F5CD15 | Accent, primary actions | + +A `.light` class on `` flips neutrals for light surfaces; brand stays constant. +Rebrand any app by overriding the CSS variables in `globals.css` — no recompile. + +## Type +- Display / headings: **Montserrat** (600/700) +- Body / UI: **Open Sans** +- Code / data: **JetBrains Mono** + +Never Inter, Arial, or system defaults for headings (Impeccable anti-slop rule). + +## Radius, elevation, motion +- Radius scale: 6 / 10 / 14 / 20px. +- Shadows are **layered and tinted** (`shadow-sm/md/lg/glow`) — depth, not flat. +- Motion: 120/180/320ms with an ease-out curve. **No bounce/elastic easing.** +- Everything honors `prefers-reduced-motion`. + +## Component rules +1. Use `cn()` for all conditional classes. +2. Variants via `class-variance-authority`, not ad-hoc conditionals. +3. Interactive primitives wrap Radix (focus traps, keyboard nav, ARIA for free). +4. Focus-visible ring on every interactive element. +5. Don't nest cards inside cards. Don't put gray text on colored fills. + +## Included +Primitives: Button, Card, Input, Badge, Dialog, DropdownMenu, Table, Toaster. +Motion: FadeIn, Stagger/StaggerItem. +Animated: AnimatedNumber, SpotlightCard. +Blocks: AppShell, CommandPalette, Hero, FeatureGrid, Pricing. diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..01fa416 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +MIT License + +Copyright (c) 2026 Jason + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100755 index 0000000..72c86f2 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,52 @@ +# Migration — adopting @jason/ui-kit in existing apps + +You don't rewrite anything. The kit is additive: install it, adopt it screen by +screen, and delete local duplicates as you go. Most of your apps are already +React + Vite + Tailwind, so this is low-friction. + +## Priority order (by payoff) + +Best-fit apps first — they already have `clsx`/`tailwind-merge` and clean tokens, +so they're the natural reference implementations: + +1. **delta** and **rack-planner** — pilots. Prove the pattern here. +2. **memer, codedump, fabdash, family-planner, mrp** — React + Vite + Tailwind; direct swap. +3. **qms, inven, mrp-qrcode** — Next.js; same kit, add `"use client"` where needed. +4. **relay** — has MPM brand fonts already; align tokens, adopt AppShell + Table. +5. **qrknit, goto** — vanilla/PHP; use the kit for React islands or new marketing pages, or borrow the tokens as CSS variables. +6. **pnger** — Svelte; not a React consumer. Reuse `tokens.ts` values + the color/type/motion rules from DESIGN.md manually. + +## Steps per app + +1. **Install + npmrc** + ```bash + cp node_modules/@jason/ui-kit/.npmrc.example .npmrc # then set GITEA_NPM_TOKEN + npm install @jason/ui-kit + ``` +2. **Adopt the preset.** Replace the app's hand-rolled color block in + `tailwind.config.js` with `presets: [require("@jason/ui-kit/preset")]`. Add the + kit's `dist` to `content`. Import `@jason/ui-kit/styles.css` in your entry. +3. **Reconcile tokens.** If the app had different colors, either map them to the + shared tokens or override the CSS variables in your local `globals.css` for a + per-app skin. Goal: one system, small per-app deltas. +4. **Swap primitives.** Replace local ` + )} + {secondaryCta && ( + + )} + + + )} + + + ); +} diff --git a/src/blocks/pricing.tsx b/src/blocks/pricing.tsx new file mode 100755 index 0000000..7a037c7 --- /dev/null +++ b/src/blocks/pricing.tsx @@ -0,0 +1,70 @@ +import * as React from "react"; +import { Check } from "lucide-react"; +import { Stagger, StaggerItem } from "../motion"; +import { Button } from "../components/button"; +import { Badge } from "../components/badge"; +import { cn } from "../lib/cn"; + +export interface PricingTier { + name: string; + price: string; + period?: string; + description?: string; + features: string[]; + cta: { label: string; href?: string; onClick?: () => void }; + featured?: boolean; +} + +export function Pricing({ + heading, + tiers, + className, +}: { + heading?: React.ReactNode; + tiers: PricingTier[]; + className?: string; +}) { + return ( +
+ {heading &&

{heading}

} + + {tiers.map((t) => ( + +
+
+

{t.name}

+ {t.featured && Popular} +
+ {t.description &&

{t.description}

} +
+ {t.price} + {t.period && /{t.period}} +
+
    + {t.features.map((f) => ( +
  • + + {f} +
  • + ))} +
+ +
+
+ ))} +
+
+ ); +} diff --git a/src/components/badge.tsx b/src/components/badge.tsx new file mode 100755 index 0000000..a618955 --- /dev/null +++ b/src/components/badge.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "../lib/cn"; + +const badgeVariants = cva( + "inline-flex items-center gap-1 rounded-sm px-2 py-0.5 text-xs font-medium", + { + variants: { + variant: { + brand: "bg-brand/15 text-brand", + neutral: "bg-surface-2 text-muted", + success: "bg-success/15 text-success", + warning: "bg-warning/15 text-warning", + danger: "bg-danger/15 text-danger", + outline: "border border-border text-text", + }, + }, + defaultVariants: { variant: "neutral" }, + } +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +export const Badge = ({ className, variant, ...props }: BadgeProps) => ( + +); +export { badgeVariants }; diff --git a/src/components/button.tsx b/src/components/button.tsx new file mode 100755 index 0000000..2282acb --- /dev/null +++ b/src/components/button.tsx @@ -0,0 +1,45 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "../lib/cn"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded font-display font-semibold " + + "transition-[background-color,box-shadow,transform,color] duration-base ease-out " + + "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-2 focus-visible:ring-offset-bg " + + "active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + primary: "bg-brand text-bg hover:bg-brand-bright shadow-sm hover:shadow-glow", + secondary: "bg-surface-2 text-text hover:bg-surface-2/70 shadow-sm", + outline: "border border-border bg-transparent text-text hover:bg-surface-2/60", + ghost: "bg-transparent text-text hover:bg-surface-2/60", + danger: "bg-danger text-white hover:bg-danger/90 shadow-sm", + link: "text-brand underline-offset-4 hover:underline", + }, + size: { + sm: "h-8 px-3 text-sm", + md: "h-10 px-4 text-sm", + lg: "h-12 px-6 text-base", + icon: "size-10", + }, + }, + defaultVariants: { variant: "primary", size: "md" }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +export const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ; + } +); +Button.displayName = "Button"; +export { buttonVariants }; diff --git a/src/components/card.tsx b/src/components/card.tsx new file mode 100755 index 0000000..bf9bb80 --- /dev/null +++ b/src/components/card.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; +import { cn } from "../lib/cn"; + +export const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +); +Card.displayName = "Card"; + +export const CardHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +export const CardTitle = ({ className, ...props }: React.HTMLAttributes) => ( +

+); +export const CardDescription = ({ className, ...props }: React.HTMLAttributes) => ( +

+); +export const CardContent = ({ className, ...props }: React.HTMLAttributes) => ( +

+); +export const CardFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+); diff --git a/src/components/dialog.tsx b/src/components/dialog.tsx new file mode 100755 index 0000000..a82e77f --- /dev/null +++ b/src/components/dialog.tsx @@ -0,0 +1,68 @@ +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; +import { cn } from "../lib/cn"; + +export const Dialog = DialogPrimitive.Root; +export const DialogTrigger = DialogPrimitive.Trigger; +export const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = "DialogOverlay"; + +export const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = "DialogContent"; + +export const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +export const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = "DialogTitle"; +export const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = "DialogDescription"; diff --git a/src/components/dropdown-menu.tsx b/src/components/dropdown-menu.tsx new file mode 100755 index 0000000..4b2dc03 --- /dev/null +++ b/src/components/dropdown-menu.tsx @@ -0,0 +1,66 @@ +import * as React from "react"; +import * as Menu from "@radix-ui/react-dropdown-menu"; +import { Check } from "lucide-react"; +import { cn } from "../lib/cn"; + +export const DropdownMenu = Menu.Root; +export const DropdownMenuTrigger = Menu.Trigger; +export const DropdownMenuGroup = Menu.Group; +export const DropdownMenuSeparator = ({ className, ...props }: React.ComponentPropsWithoutRef) => ( + +); + +export const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 6, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = "DropdownMenuContent"; + +export const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = "DropdownMenuItem"; + +export const DropdownMenuLabel = ({ className, ...props }: React.ComponentPropsWithoutRef) => ( + +); +export const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem"; diff --git a/src/components/input.tsx b/src/components/input.tsx new file mode 100755 index 0000000..24d1b39 --- /dev/null +++ b/src/components/input.tsx @@ -0,0 +1,20 @@ +import * as React from "react"; +import { cn } from "../lib/cn"; + +export const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => ( + + ) +); +Input.displayName = "Input"; diff --git a/src/components/table.tsx b/src/components/table.tsx new file mode 100755 index 0000000..251f6cf --- /dev/null +++ b/src/components/table.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; +import { cn } from "../lib/cn"; + +export const Table = ({ className, ...props }: React.HTMLAttributes) => ( +
+ + +); +export const TableHeader = ({ className, ...props }: React.HTMLAttributes) => ( + +); +export const TableBody = ({ className, ...props }: React.HTMLAttributes) => ( + +); +export const TableRow = ({ className, ...props }: React.HTMLAttributes) => ( + +); +export const TableHead = ({ className, ...props }: React.ThHTMLAttributes) => ( +
+); +export const TableCell = ({ className, ...props }: React.TdHTMLAttributes) => ( + +); diff --git a/src/components/toast.tsx b/src/components/toast.tsx new file mode 100755 index 0000000..323d911 --- /dev/null +++ b/src/components/toast.tsx @@ -0,0 +1,20 @@ +import { Toaster as Sonner, toast } from "sonner"; + +/** Pre-themed sonner Toaster. Drop once near your app root. */ +export const Toaster = (props: React.ComponentProps) => ( + +); + +export { toast }; diff --git a/src/index.ts b/src/index.ts new file mode 100755 index 0000000..23bd701 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,34 @@ +// Utilities +export { cn } from "./lib/cn"; +export { tokens, type Tokens } from "./lib/tokens"; +export { setTheme, toggleTheme } from "./lib/theme"; + +// Primitives +export { Button, buttonVariants, type ButtonProps } from "./components/button"; +export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "./components/card"; +export { Input } from "./components/input"; +export { Badge, badgeVariants, type BadgeProps } from "./components/badge"; +export { + Dialog, DialogTrigger, DialogClose, DialogContent, + DialogHeader, DialogTitle, DialogDescription, +} from "./components/dialog"; +export { + DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, + DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuGroup, DropdownMenuCheckboxItem, +} from "./components/dropdown-menu"; +export { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "./components/table"; +export { Toaster, toast } from "./components/toast"; + +// Motion +export { FadeIn, Stagger, StaggerItem, staggerContainer, staggerItem, motion } from "./motion"; + +// Animated +export { AnimatedNumber } from "./animated/animated-number"; +export { SpotlightCard } from "./animated/spotlight-card"; + +// Blocks +export { AppShell, type NavItem } from "./blocks/app-shell"; +export { CommandPalette, type CommandAction } from "./blocks/command-palette"; +export { Hero } from "./blocks/hero"; +export { FeatureGrid, type Feature } from "./blocks/feature-grid"; +export { Pricing, type PricingTier } from "./blocks/pricing"; diff --git a/src/lib/cn.ts b/src/lib/cn.ts new file mode 100755 index 0000000..7908828 --- /dev/null +++ b/src/lib/cn.ts @@ -0,0 +1,7 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +/** Merge conditional class names, de-duplicating conflicting Tailwind utilities. */ +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/lib/theme.ts b/src/lib/theme.ts new file mode 100755 index 0000000..0bd0367 --- /dev/null +++ b/src/lib/theme.ts @@ -0,0 +1,9 @@ +/** Toggle the .light class on . Dark is the default (:root). */ +export function setTheme(mode: "dark" | "light") { + const el = document.documentElement; + el.classList.toggle("light", mode === "light"); +} +export function toggleTheme() { + const isLight = document.documentElement.classList.toggle("light"); + return isLight ? "light" : "dark"; +} diff --git a/src/lib/tokens.ts b/src/lib/tokens.ts new file mode 100755 index 0000000..1d74f89 --- /dev/null +++ b/src/lib/tokens.ts @@ -0,0 +1,46 @@ +/** + * Design tokens — the single source of truth for the brand. + * These mirror the CSS variables in styles/globals.css and feed the Tailwind preset. + * Change the brand once, here, and every app that consumes the preset updates. + */ +export const tokens = { + color: { + // Neutrals are tinted (warm), never pure black/gray — an Impeccable anti-slop rule. + bg: "#232022", + surface: "#2b282a", + surface2: "#333031", + border: "#3a3638", + text: "#F5F1EC", + muted: "#A99F97", + // Brand — warm gold on warm dark. + brand: "#DCBB4F", + brandBright: "#F5CD15", + brandDark: "#998643", + // Semantic + success: "#3F9E6A", + warning: "#DCBB4F", + danger: "#C0562F", + info: "#5B8DB8", + }, + radius: { sm: "6px", md: "10px", lg: "14px", xl: "20px" }, + font: { + display: '"Montserrat", system-ui, sans-serif', + sans: '"Open Sans", system-ui, sans-serif', + mono: '"JetBrains Mono", ui-monospace, monospace', + }, + // Layered, tinted shadows — depth instead of a flat plane. + shadow: { + sm: "0 1px 2px 0 rgb(20 16 18 / 0.20)", + md: "0 4px 12px -2px rgb(20 16 18 / 0.28), 0 2px 4px -2px rgb(20 16 18 / 0.20)", + lg: "0 12px 32px -8px rgb(20 16 18 / 0.40), 0 4px 8px -4px rgb(20 16 18 / 0.24)", + glow: "0 0 0 1px rgb(220 187 79 / 0.20), 0 8px 28px -6px rgb(220 187 79 / 0.28)", + }, + // Motion — smooth, never bouncy/elastic (Impeccable anti-slop rule). + ease: { + out: "cubic-bezier(0.22, 1, 0.36, 1)", + inOut: "cubic-bezier(0.65, 0, 0.35, 1)", + }, + duration: { fast: "120ms", base: "180ms", slow: "320ms" }, +} as const; + +export type Tokens = typeof tokens; diff --git a/src/motion/index.tsx b/src/motion/index.tsx new file mode 100755 index 0000000..4b898b2 --- /dev/null +++ b/src/motion/index.tsx @@ -0,0 +1,59 @@ +import * as React from "react"; +import { motion, useReducedMotion, type Variants } from "motion/react"; + +const easeOut = [0.22, 1, 0.36, 1] as const; + +/** Fade + rise on mount. Respects prefers-reduced-motion. */ +export function FadeIn({ + children, + delay = 0, + y = 8, + className, +}: { + children: React.ReactNode; + delay?: number; + y?: number; + className?: string; +}) { + const reduce = useReducedMotion(); + return ( + + {children} + + ); +} + +/** Stagger container — pair with . */ +export const staggerContainer: Variants = { + hidden: {}, + show: { transition: { staggerChildren: 0.06 } }, +}; +export const staggerItem: Variants = { + hidden: { opacity: 0, y: 10 }, + show: { opacity: 1, y: 0, transition: { duration: 0.4, ease: easeOut } }, +}; + +export function Stagger({ children, className }: { children: React.ReactNode; className?: string }) { + return ( + + {children} + + ); +} +export function StaggerItem({ children, className }: { children: React.ReactNode; className?: string }) { + return {children}; +} + +export { motion }; diff --git a/src/styles/globals.css b/src/styles/globals.css new file mode 100755 index 0000000..c8f6dc2 --- /dev/null +++ b/src/styles/globals.css @@ -0,0 +1,53 @@ +/* Import once in your app entry: import "@jason/ui-kit/styles.css"; */ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* + * Brand tokens as HSL channels so Tailwind's works. + * :root = dark (default brand). .light overrides for light surfaces. + */ +@layer base { + :root { + --bg: 330 4% 13%; + --surface: 330 4% 16%; + --surface-2: 330 3% 20%; + --border: 330 3% 22%; + --text: 33 33% 94%; + --muted: 28 9% 63%; + --brand: 46 68% 59%; + --brand-bright: 51 91% 52%; + --brand-dark: 46 40% 43%; + --success: 146 43% 43%; + --warning: 46 68% 59%; + --danger: 17 61% 47%; + --info: 205 37% 54%; + } + + .light { + --bg: 36 33% 97%; + --surface: 0 0% 100%; + --surface-2: 36 20% 95%; + --border: 33 12% 86%; + --text: 330 6% 15%; + --muted: 330 4% 40%; + } + + * { border-color: hsl(var(--border)); } + body { + background-color: hsl(var(--bg)); + color: hsl(var(--text)); + font-family: "Open Sans", system-ui, sans-serif; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + } + h1, h2, h3, h4, h5 { font-family: "Montserrat", system-ui, sans-serif; } + + /* Respect reduced-motion everywhere. */ + @media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } + } +} diff --git a/tailwind-preset.cjs b/tailwind-preset.cjs new file mode 100755 index 0000000..3c15334 --- /dev/null +++ b/tailwind-preset.cjs @@ -0,0 +1,70 @@ +/** + * Shared Tailwind preset. Consuming apps do: + * // tailwind.config.js + * module.exports = { + * presets: [require("@jason/ui-kit/preset")], + * content: [ + * "./src/**\/*.{ts,tsx}", + * "./node_modules/@jason/ui-kit/dist/**\/*.js" // so component classes aren't purged + * ], + * }; + * + * Colors are driven by CSS variables (see styles/globals.css) so light/dark and + * per-app rebranding work without recompiling the preset. + */ +const v = (name) => `hsl(var(${name}) / )`; + +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ["class"], + theme: { + extend: { + colors: { + bg: v("--bg"), + surface: v("--surface"), + "surface-2": v("--surface-2"), + border: v("--border"), + input: v("--border"), + ring: v("--brand"), + text: v("--text"), + muted: v("--muted"), + brand: { DEFAULT: v("--brand"), bright: v("--brand-bright"), dark: v("--brand-dark") }, + success: v("--success"), + warning: v("--warning"), + danger: v("--danger"), + info: v("--info"), + }, + fontFamily: { + display: ['"Montserrat"', "system-ui", "sans-serif"], + sans: ['"Open Sans"', "system-ui", "sans-serif"], + mono: ['"JetBrains Mono"', "ui-monospace", "monospace"], + }, + borderRadius: { sm: "6px", DEFAULT: "10px", md: "10px", lg: "14px", xl: "20px" }, + boxShadow: { + sm: "0 1px 2px 0 rgb(20 16 18 / 0.20)", + DEFAULT: "0 4px 12px -2px rgb(20 16 18 / 0.28), 0 2px 4px -2px rgb(20 16 18 / 0.20)", + md: "0 4px 12px -2px rgb(20 16 18 / 0.28), 0 2px 4px -2px rgb(20 16 18 / 0.20)", + lg: "0 12px 32px -8px rgb(20 16 18 / 0.40), 0 4px 8px -4px rgb(20 16 18 / 0.24)", + glow: "0 0 0 1px rgb(220 187 79 / 0.20), 0 8px 28px -6px rgb(220 187 79 / 0.28)", + }, + transitionTimingFunction: { + out: "cubic-bezier(0.22, 1, 0.36, 1)", + "in-out": "cubic-bezier(0.65, 0, 0.35, 1)", + }, + transitionDuration: { fast: "120ms", DEFAULT: "180ms", base: "180ms", slow: "320ms" }, + keyframes: { + "fade-in": { from: { opacity: "0" }, to: { opacity: "1" } }, + "fade-up": { from: { opacity: "0", transform: "translateY(8px)" }, to: { opacity: "1", transform: "translateY(0)" } }, + "scale-in": { from: { opacity: "0", transform: "scale(0.97)" }, to: { opacity: "1", transform: "scale(1)" } }, + shimmer: { from: { backgroundPosition: "200% 0" }, to: { backgroundPosition: "-200% 0" } }, + }, + animation: { + "fade-in": "fade-in 180ms cubic-bezier(0.22,1,0.36,1)", + "fade-up": "fade-up 320ms cubic-bezier(0.22,1,0.36,1)", + "scale-in": "scale-in 180ms cubic-bezier(0.22,1,0.36,1)", + shimmer: "shimmer 2.2s linear infinite", + }, + }, + }, + plugins: [], +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100755 index 0000000..6db9530 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "declaration": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { "@/*": ["./src/*"] } + }, + "include": ["src"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100755 index 0000000..97b2d9d --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + dts: true, + sourcemap: true, + clean: true, + treeshake: true, + external: ["react", "react-dom", "tailwindcss"], +});