commit b75012525d6c218b99d0f679ce82dd7406524192 Author: Jason Date: Thu Jul 16 00:36:53 2026 -0500 feat: initial @jason/ui-kit — tokens, preset, components, motion, blocks, Impeccable 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"], +});