feat: initial @jason/ui-kit — tokens, preset, components, motion, blocks, Impeccable
ci / build-and-design (push) Failing after 15s
ci / build-and-design (push) Failing after 15s
This commit is contained in:
Executable
+21
@@ -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
|
||||||
Executable
+14
@@ -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
|
||||||
Executable
+11
@@ -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 }
|
||||||
|
}
|
||||||
Executable
+4
@@ -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}
|
||||||
Executable
+17
@@ -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 <patch|minor|major>` 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.
|
||||||
@@ -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 `<html>` 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.
|
||||||
@@ -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.
|
||||||
Executable
+52
@@ -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 `<button>`/modal/input/table/toast with kit
|
||||||
|
components. Delete the local copies. Standardize on `cn()` and `toast()`.
|
||||||
|
5. **Add depth + motion.** Wrap page sections in `FadeIn`/`Stagger`; use
|
||||||
|
`SpotlightCard` for feature/stat cards; `AnimatedNumber` on dashboard metrics.
|
||||||
|
6. **Add the shell.** Move dashboards onto `AppShell` + `CommandPalette` (⌘K).
|
||||||
|
7. **Guard it.** `npx impeccable install` in the app, run `/impeccable init` once,
|
||||||
|
then `/impeccable audit` and `/impeccable polish` per screen. Add the detector
|
||||||
|
to the app's CI (copy `.gitea/workflows/ci.yml`).
|
||||||
|
|
||||||
|
## Rules of thumb
|
||||||
|
|
||||||
|
- Don't fork kit components in-app. If one needs changing, change it here and bump the version — every app benefits.
|
||||||
|
- If a pattern shows up in 2+ apps, promote it into `src/blocks/`.
|
||||||
|
- New apps: start from the kit on day one. No hand-rolled buttons ever again.
|
||||||
|
|
||||||
|
## Path forward for NEW apps
|
||||||
|
|
||||||
|
Baseline stack going forward: **React + Vite + TypeScript + Tailwind (this preset)
|
||||||
|
+ @jason/ui-kit + Impeccable**, backed by your usual Express/Prisma or
|
||||||
|
Next + better-sqlite3, Dockerized for Unraid. `sonner` and `motion` are defaults,
|
||||||
|
not per-app decisions.
|
||||||
Executable
+19
@@ -0,0 +1,19 @@
|
|||||||
|
# Product Context
|
||||||
|
|
||||||
|
> Read by Impeccable commands so design work stays on-brand. One file, shared by every app that adopts this kit.
|
||||||
|
|
||||||
|
## Audience
|
||||||
|
Internal operators and small-business/SaaS end-users of self-hosted work tools (dashboards, trackers, portals, generators). Competent but time-poor — they value clarity and speed over decoration.
|
||||||
|
|
||||||
|
## Product lane
|
||||||
|
**Product** (app UI, dashboards, tools) first; **brand** (landing/marketing) second. Both share the same tokens.
|
||||||
|
|
||||||
|
## Voice
|
||||||
|
Direct, confident, no fluff. Short labels. Verbs over nouns. Never cutesy.
|
||||||
|
|
||||||
|
## Anti-references (what we must NOT look like)
|
||||||
|
- Generic AI-generated SaaS templates: Inter everywhere, purple→blue gradients, cards nested in cards, gray text on colored fills, a rounded-square icon tile above every heading.
|
||||||
|
- Flat, shadowless "utility-class soup" with no motion.
|
||||||
|
|
||||||
|
## Design north star
|
||||||
|
Warm, confident, slightly editorial. Gold accent used sparingly for emphasis and action. Depth via layered tinted shadows, not borders-on-everything. Motion is purposeful and quiet.
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# @jason/ui-kit
|
||||||
|
|
||||||
|
Shared design foundation for all my apps. One brand system, reused everywhere — so
|
||||||
|
new apps start polished and existing apps have a clear upgrade path.
|
||||||
|
|
||||||
|
Built on the stack I already use (**React + Vite + Tailwind + TypeScript**) and
|
||||||
|
guarded by [Impeccable](https://impeccable.style) so nothing ships looking like a
|
||||||
|
generic AI-generated template.
|
||||||
|
|
||||||
|
## What's inside
|
||||||
|
|
||||||
|
- **Design tokens + Tailwind preset** — warm gold/dark brand, radius/shadow/motion scales, CSS-variable theming (dark default, `.light` opt-in), Montserrat / Open Sans / JetBrains Mono.
|
||||||
|
- **`cn()`** helper (clsx + tailwind-merge) — the standard everywhere.
|
||||||
|
- **Primitives** — Button, Card, Input, Badge, Dialog, DropdownMenu, Table, Toaster (sonner). Radix-backed, accessible, pre-themed.
|
||||||
|
- **Motion** — `FadeIn`, `Stagger` (framer/`motion`, reduced-motion aware).
|
||||||
|
- **Animated** — `AnimatedNumber` (count-up stats), `SpotlightCard` (cursor glow).
|
||||||
|
- **Blocks** — `AppShell` (sidebar + topbar), `CommandPalette` (⌘K), `Hero`, `FeatureGrid`, `Pricing`.
|
||||||
|
- **Impeccable** wired in — `DESIGN.md` / `PRODUCT.md` design context, config, and a CI anti-slop detector.
|
||||||
|
|
||||||
|
## Install (in a consuming app)
|
||||||
|
|
||||||
|
This package publishes to the Gitea npm registry. Add an `.npmrc` (see `.npmrc.example`):
|
||||||
|
|
||||||
|
```
|
||||||
|
@jason:registry=https://git.alwisp.com/api/packages/jason/npm/
|
||||||
|
//git.alwisp.com/api/packages/jason/npm/:_authToken=${GITEA_NPM_TOKEN}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @jason/ui-kit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wire up Tailwind
|
||||||
|
|
||||||
|
```js
|
||||||
|
// tailwind.config.js
|
||||||
|
module.exports = {
|
||||||
|
presets: [require("@jason/ui-kit/preset")],
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{ts,tsx}",
|
||||||
|
"./node_modules/@jason/ui-kit/dist/**/*.js", // so kit classes aren't purged
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app entry (main.tsx)
|
||||||
|
import "@jason/ui-kit/styles.css";
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use it
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Button, Card, CardHeader, CardTitle, Hero, AnimatedNumber, Toaster, toast } from "@jason/ui-kit";
|
||||||
|
|
||||||
|
<Hero
|
||||||
|
eyebrow="New"
|
||||||
|
title="Ship tools that look designed"
|
||||||
|
subtitle="One brand system across every app."
|
||||||
|
primaryCta={{ label: "Get started", href: "/signup" }}
|
||||||
|
secondaryCta={{ label: "Docs", href: "/docs" }}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Develop
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev # tsup watch build
|
||||||
|
npm run typecheck
|
||||||
|
npm run design:detect # run the Impeccable anti-slop detector on src/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publish a new version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm version patch # or minor / major
|
||||||
|
npm publish # builds via prepublishOnly, pushes to Gitea registry
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extending with 21st.dev
|
||||||
|
|
||||||
|
This kit is the base layer. For richer one-off blocks, pull
|
||||||
|
[21st.dev](https://21st.dev) components (shadcn-registry compatible) into an app,
|
||||||
|
then re-theme them with these tokens and, if broadly useful, promote them into
|
||||||
|
`src/blocks/` here. Run `/impeccable polish` on anything new before it ships.
|
||||||
|
|
||||||
|
See **DESIGN.md** for the full system spec and **MIGRATION.md** for adopting the
|
||||||
|
kit in apps that already exist.
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "@jason/ui-kit",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Shared design foundation — branded tokens, Tailwind preset, shadcn-style components, motion, app shell + marketing blocks. Guarded by Impeccable.",
|
||||||
|
"type": "module",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "Jason <jason@messagepoint.tv>",
|
||||||
|
"repository": { "type": "git", "url": "https://git.alwisp.com/jason/ui-kit.git" },
|
||||||
|
"publishConfig": { "registry": "https://git.alwisp.com/api/packages/jason/npm/" },
|
||||||
|
"sideEffects": ["**/*.css"],
|
||||||
|
"files": ["dist", "tailwind-preset.cjs", "src/styles/globals.css"],
|
||||||
|
"exports": {
|
||||||
|
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
|
||||||
|
"./preset": "./tailwind-preset.cjs",
|
||||||
|
"./styles.css": "./src/styles/globals.css"
|
||||||
|
},
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"dev": "tsup --watch",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"design:detect": "impeccable detect src/",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-dialog": "^1.1.4",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||||
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.0.4",
|
||||||
|
"lucide-react": "^0.469.0",
|
||||||
|
"motion": "^11.15.0",
|
||||||
|
"sonner": "^1.7.1",
|
||||||
|
"tailwind-merge": "^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": { "react": ">=18", "react-dom": ">=18", "tailwindcss": ">=3.4" },
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.18",
|
||||||
|
"@types/react-dom": "^18.3.5",
|
||||||
|
"impeccable": "^3.2.1",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"tsup": "^8.3.5",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } };
|
||||||
Executable
+29
@@ -0,0 +1,29 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { useInView, useMotionValue, useSpring, useReducedMotion } from "motion/react";
|
||||||
|
|
||||||
|
/** Count-up number for dashboards/stat cards. Springs to `value` when scrolled into view. */
|
||||||
|
export function AnimatedNumber({
|
||||||
|
value,
|
||||||
|
format = (n) => Math.round(n).toLocaleString(),
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
value: number;
|
||||||
|
format?: (n: number) => string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const ref = React.useRef<HTMLSpanElement>(null);
|
||||||
|
const inView = useInView(ref, { once: true, margin: "-20% 0px" });
|
||||||
|
const reduce = useReducedMotion();
|
||||||
|
const mv = useMotionValue(0);
|
||||||
|
const spring = useSpring(mv, { stiffness: 90, damping: 20 });
|
||||||
|
const [display, setDisplay] = React.useState(format(0));
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (reduce) { setDisplay(format(value)); return; }
|
||||||
|
if (inView) mv.set(value);
|
||||||
|
}, [inView, value, reduce, mv, format]);
|
||||||
|
|
||||||
|
React.useEffect(() => spring.on("change", (v) => setDisplay(format(v))), [spring, format]);
|
||||||
|
|
||||||
|
return <span ref={ref} className={className}>{display}</span>;
|
||||||
|
}
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "../lib/cn";
|
||||||
|
|
||||||
|
/** Card with a soft brand glow that tracks the cursor. Depth without gimmick. */
|
||||||
|
export function SpotlightCard({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
const [pos, setPos] = React.useState({ x: 50, y: 50, active: false });
|
||||||
|
|
||||||
|
const onMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
const r = ref.current?.getBoundingClientRect();
|
||||||
|
if (!r) return;
|
||||||
|
setPos({ x: ((e.clientX - r.left) / r.width) * 100, y: ((e.clientY - r.top) / r.height) * 100, active: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
onMouseMove={onMove}
|
||||||
|
onMouseLeave={() => setPos((p) => ({ ...p, active: false }))}
|
||||||
|
className={cn("group relative overflow-hidden rounded-lg border border-border bg-surface p-6 shadow-sm transition-shadow duration-slow hover:shadow-md", className)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-slow group-hover:opacity-100"
|
||||||
|
style={{
|
||||||
|
background: `radial-gradient(400px circle at ${pos.x}% ${pos.y}%, hsl(var(--brand) / 0.10), transparent 60%)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="relative">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "../lib/cn";
|
||||||
|
|
||||||
|
export interface NavItem {
|
||||||
|
label: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
href?: string;
|
||||||
|
active?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dashboard chrome: fixed sidebar + sticky topbar + scrolling content. */
|
||||||
|
export function AppShell({
|
||||||
|
brand,
|
||||||
|
nav,
|
||||||
|
topbar,
|
||||||
|
children,
|
||||||
|
footer,
|
||||||
|
}: {
|
||||||
|
brand: React.ReactNode;
|
||||||
|
nav: NavItem[];
|
||||||
|
topbar?: React.ReactNode;
|
||||||
|
children: React.ReactNode;
|
||||||
|
footer?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen bg-bg text-text">
|
||||||
|
<aside className="hidden w-60 shrink-0 flex-col border-r border-border bg-surface md:flex">
|
||||||
|
<div className="flex h-14 items-center px-5 font-display text-lg font-bold">{brand}</div>
|
||||||
|
<nav className="flex-1 space-y-0.5 px-3 py-3">
|
||||||
|
{nav.map((item) => {
|
||||||
|
const Comp = item.href ? "a" : "button";
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
key={item.label}
|
||||||
|
href={item.href}
|
||||||
|
onClick={item.onClick}
|
||||||
|
className={cn(
|
||||||
|
"flex w-full items-center gap-3 rounded px-3 py-2 text-sm font-medium transition-colors duration-fast [&_svg]:size-4",
|
||||||
|
item.active ? "bg-brand/15 text-brand" : "text-muted hover:bg-surface-2/60 hover:text-text"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
{item.label}
|
||||||
|
</Comp>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
{footer && <div className="border-t border-border p-3 text-sm text-muted">{footer}</div>}
|
||||||
|
</aside>
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
|
<header className="sticky top-0 z-30 flex h-14 items-center justify-between gap-4 border-b border-border bg-bg/80 px-5 backdrop-blur">
|
||||||
|
{topbar}
|
||||||
|
</header>
|
||||||
|
<main className="flex-1 p-6">{children}</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Executable
+76
@@ -0,0 +1,76 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Command } from "cmdk";
|
||||||
|
import { Search } from "lucide-react";
|
||||||
|
import { cn } from "../lib/cn";
|
||||||
|
|
||||||
|
export interface CommandAction {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
group?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
onSelect: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ⌘K palette. Opens on Cmd/Ctrl+K by default; big perceived-quality boost on any tool. */
|
||||||
|
export function CommandPalette({
|
||||||
|
actions,
|
||||||
|
placeholder = "Type a command or search…",
|
||||||
|
}: {
|
||||||
|
actions: CommandAction[];
|
||||||
|
placeholder?: string;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
React.useEffect(() => {
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen((o) => !o);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", onKey);
|
||||||
|
return () => document.removeEventListener("keydown", onKey);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const groups = React.useMemo(() => {
|
||||||
|
const map = new Map<string, CommandAction[]>();
|
||||||
|
for (const a of actions) {
|
||||||
|
const g = a.group ?? "Actions";
|
||||||
|
map.set(g, [...(map.get(g) ?? []), a]);
|
||||||
|
}
|
||||||
|
return [...map.entries()];
|
||||||
|
}, [actions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Command.Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
label="Command palette"
|
||||||
|
className={cn(
|
||||||
|
"fixed left-1/2 top-1/4 z-50 w-full max-w-xl -translate-x-1/2 overflow-hidden",
|
||||||
|
"rounded-lg border border-border bg-surface shadow-lg data-[state=open]:animate-scale-in"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 border-b border-border px-4">
|
||||||
|
<Search className="size-4 text-muted" />
|
||||||
|
<Command.Input placeholder={placeholder} className="h-12 flex-1 bg-transparent text-sm text-text outline-none placeholder:text-muted" />
|
||||||
|
</div>
|
||||||
|
<Command.List className="max-h-80 overflow-auto p-2">
|
||||||
|
<Command.Empty className="py-6 text-center text-sm text-muted">No results.</Command.Empty>
|
||||||
|
{groups.map(([group, items]) => (
|
||||||
|
<Command.Group key={group} heading={group} className="px-1 py-1 text-xs font-semibold uppercase tracking-wide text-muted [&_[cmdk-group-items]]:mt-1">
|
||||||
|
{items.map((a) => (
|
||||||
|
<Command.Item
|
||||||
|
key={a.id}
|
||||||
|
onSelect={() => { a.onSelect(); setOpen(false); }}
|
||||||
|
className="flex cursor-pointer items-center gap-2 rounded px-2 py-2 text-sm text-text aria-selected:bg-surface-2 [&_svg]:size-4"
|
||||||
|
>
|
||||||
|
{a.icon}
|
||||||
|
{a.label}
|
||||||
|
</Command.Item>
|
||||||
|
))}
|
||||||
|
</Command.Group>
|
||||||
|
))}
|
||||||
|
</Command.List>
|
||||||
|
</Command.Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
Executable
+44
@@ -0,0 +1,44 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Stagger, StaggerItem } from "../motion";
|
||||||
|
import { SpotlightCard } from "../animated/spotlight-card";
|
||||||
|
import { cn } from "../lib/cn";
|
||||||
|
|
||||||
|
export interface Feature {
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FeatureGrid({
|
||||||
|
heading,
|
||||||
|
features,
|
||||||
|
columns = 3,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
heading?: React.ReactNode;
|
||||||
|
features: Feature[];
|
||||||
|
columns?: 2 | 3 | 4;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const cols = { 2: "sm:grid-cols-2", 3: "sm:grid-cols-2 lg:grid-cols-3", 4: "sm:grid-cols-2 lg:grid-cols-4" }[columns];
|
||||||
|
return (
|
||||||
|
<section className={cn("mx-auto max-w-6xl px-6 py-20", className)}>
|
||||||
|
{heading && <h2 className="mb-10 text-center font-display text-3xl font-bold tracking-tight">{heading}</h2>}
|
||||||
|
<Stagger className={cn("grid grid-cols-1 gap-4", cols)}>
|
||||||
|
{features.map((f) => (
|
||||||
|
<StaggerItem key={f.title}>
|
||||||
|
<SpotlightCard className="h-full">
|
||||||
|
{f.icon && (
|
||||||
|
<div className="mb-4 inline-flex size-10 items-center justify-center rounded bg-brand/15 text-brand [&_svg]:size-5">
|
||||||
|
{f.icon}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h3 className="font-display text-lg font-semibold">{f.title}</h3>
|
||||||
|
<p className="mt-2 text-sm leading-relaxed text-muted">{f.description}</p>
|
||||||
|
</SpotlightCard>
|
||||||
|
</StaggerItem>
|
||||||
|
))}
|
||||||
|
</Stagger>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
Executable
+64
@@ -0,0 +1,64 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { FadeIn } from "../motion";
|
||||||
|
import { Button } from "../components/button";
|
||||||
|
import { cn } from "../lib/cn";
|
||||||
|
|
||||||
|
export function Hero({
|
||||||
|
eyebrow,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
primaryCta,
|
||||||
|
secondaryCta,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
eyebrow?: string;
|
||||||
|
title: React.ReactNode;
|
||||||
|
subtitle?: React.ReactNode;
|
||||||
|
primaryCta?: { label: string; href?: string; onClick?: () => void };
|
||||||
|
secondaryCta?: { label: string; href?: string; onClick?: () => void };
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<section className={cn("relative overflow-hidden bg-bg px-6 py-24 text-center", className)}>
|
||||||
|
{/* Subtle brand aura — depth, not a purple gradient. */}
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute inset-x-0 top-0 h-[420px] opacity-60"
|
||||||
|
style={{ background: "radial-gradient(600px 300px at 50% 0%, hsl(var(--brand) / 0.14), transparent 70%)" }}
|
||||||
|
/>
|
||||||
|
<div className="relative mx-auto max-w-3xl">
|
||||||
|
{eyebrow && (
|
||||||
|
<FadeIn>
|
||||||
|
<span className="mb-4 inline-block rounded-sm bg-brand/15 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-brand">
|
||||||
|
{eyebrow}
|
||||||
|
</span>
|
||||||
|
</FadeIn>
|
||||||
|
)}
|
||||||
|
<FadeIn delay={0.05}>
|
||||||
|
<h1 className="font-display text-4xl font-bold leading-[1.1] tracking-tight sm:text-6xl">{title}</h1>
|
||||||
|
</FadeIn>
|
||||||
|
{subtitle && (
|
||||||
|
<FadeIn delay={0.1}>
|
||||||
|
<p className="mx-auto mt-6 max-w-xl text-lg text-muted">{subtitle}</p>
|
||||||
|
</FadeIn>
|
||||||
|
)}
|
||||||
|
{(primaryCta || secondaryCta) && (
|
||||||
|
<FadeIn delay={0.15}>
|
||||||
|
<div className="mt-10 flex items-center justify-center gap-3">
|
||||||
|
{primaryCta && (
|
||||||
|
<Button size="lg" asChild={!!primaryCta.href} onClick={primaryCta.onClick}>
|
||||||
|
{primaryCta.href ? <a href={primaryCta.href}>{primaryCta.label}</a> : primaryCta.label}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{secondaryCta && (
|
||||||
|
<Button size="lg" variant="outline" asChild={!!secondaryCta.href} onClick={secondaryCta.onClick}>
|
||||||
|
{secondaryCta.href ? <a href={secondaryCta.href}>{secondaryCta.label}</a> : secondaryCta.label}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FadeIn>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
Executable
+70
@@ -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 (
|
||||||
|
<section className={cn("mx-auto max-w-5xl px-6 py-20", className)}>
|
||||||
|
{heading && <h2 className="mb-10 text-center font-display text-3xl font-bold tracking-tight">{heading}</h2>}
|
||||||
|
<Stagger className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
|
{tiers.map((t) => (
|
||||||
|
<StaggerItem key={t.name}>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-full flex-col rounded-lg border bg-surface p-6 transition-shadow duration-slow",
|
||||||
|
t.featured ? "border-brand/50 shadow-glow" : "border-border shadow-sm hover:shadow-md"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-display text-lg font-semibold">{t.name}</h3>
|
||||||
|
{t.featured && <Badge variant="brand">Popular</Badge>}
|
||||||
|
</div>
|
||||||
|
{t.description && <p className="mt-1 text-sm text-muted">{t.description}</p>}
|
||||||
|
<div className="mt-5 flex items-baseline gap-1">
|
||||||
|
<span className="font-display text-4xl font-bold">{t.price}</span>
|
||||||
|
{t.period && <span className="text-sm text-muted">/{t.period}</span>}
|
||||||
|
</div>
|
||||||
|
<ul className="mt-6 flex-1 space-y-2.5">
|
||||||
|
{t.features.map((f) => (
|
||||||
|
<li key={f} className="flex items-start gap-2 text-sm text-text">
|
||||||
|
<Check className="mt-0.5 size-4 shrink-0 text-brand" />
|
||||||
|
{f}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<Button
|
||||||
|
className="mt-6 w-full"
|
||||||
|
variant={t.featured ? "primary" : "outline"}
|
||||||
|
asChild={!!t.cta.href}
|
||||||
|
onClick={t.cta.onClick}
|
||||||
|
>
|
||||||
|
{t.cta.href ? <a href={t.cta.href}>{t.cta.label}</a> : t.cta.label}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</StaggerItem>
|
||||||
|
))}
|
||||||
|
</Stagger>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
Executable
+29
@@ -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<HTMLSpanElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
export const Badge = ({ className, variant, ...props }: BadgeProps) => (
|
||||||
|
<span className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
);
|
||||||
|
export { badgeVariants };
|
||||||
Executable
+45
@@ -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<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
return <Comp ref={ref} className={cn(buttonVariants({ variant, size }), className)} {...props} />;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Button.displayName = "Button";
|
||||||
|
export { buttonVariants };
|
||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "../lib/cn";
|
||||||
|
|
||||||
|
export const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("rounded-lg border border-border bg-surface shadow-sm", className)} {...props} />
|
||||||
|
)
|
||||||
|
);
|
||||||
|
Card.displayName = "Card";
|
||||||
|
|
||||||
|
export const CardHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={cn("flex flex-col gap-1 p-5", className)} {...props} />
|
||||||
|
);
|
||||||
|
export const CardTitle = ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||||
|
<h3 className={cn("font-display text-lg font-semibold leading-tight", className)} {...props} />
|
||||||
|
);
|
||||||
|
export const CardDescription = ({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) => (
|
||||||
|
<p className={cn("text-sm text-muted", className)} {...props} />
|
||||||
|
);
|
||||||
|
export const CardContent = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={cn("p-5 pt-0", className)} {...props} />
|
||||||
|
);
|
||||||
|
export const CardFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={cn("flex items-center gap-2 p-5 pt-0", className)} {...props} />
|
||||||
|
);
|
||||||
Executable
+68
@@ -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<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/60 backdrop-blur-sm",
|
||||||
|
"data-[state=open]:animate-fade-in data-[state=closed]:opacity-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogOverlay.displayName = "DialogOverlay";
|
||||||
|
|
||||||
|
export const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Portal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2",
|
||||||
|
"rounded-lg border border-border bg-surface p-6 shadow-lg",
|
||||||
|
"data-[state=open]:animate-scale-in",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm text-muted transition-colors hover:text-text focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60">
|
||||||
|
<X className="size-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPrimitive.Portal>
|
||||||
|
));
|
||||||
|
DialogContent.displayName = "DialogContent";
|
||||||
|
|
||||||
|
export const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={cn("flex flex-col gap-1.5 pb-4", className)} {...props} />
|
||||||
|
);
|
||||||
|
export const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title ref={ref} className={cn("font-display text-lg font-semibold", className)} {...props} />
|
||||||
|
));
|
||||||
|
DialogTitle.displayName = "DialogTitle";
|
||||||
|
export const DialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted", className)} {...props} />
|
||||||
|
));
|
||||||
|
DialogDescription.displayName = "DialogDescription";
|
||||||
Executable
+66
@@ -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<typeof Menu.Separator>) => (
|
||||||
|
<Menu.Separator className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DropdownMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof Menu.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof Menu.Content>
|
||||||
|
>(({ className, sideOffset = 6, ...props }, ref) => (
|
||||||
|
<Menu.Portal>
|
||||||
|
<Menu.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[10rem] overflow-hidden rounded-md border border-border bg-surface p-1 shadow-lg",
|
||||||
|
"data-[state=open]:animate-scale-in",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Menu.Portal>
|
||||||
|
));
|
||||||
|
DropdownMenuContent.displayName = "DropdownMenuContent";
|
||||||
|
|
||||||
|
export const DropdownMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof Menu.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof Menu.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<Menu.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-pointer select-none items-center gap-2 rounded px-2 py-1.5 text-sm text-text outline-none",
|
||||||
|
"transition-colors focus:bg-surface-2 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DropdownMenuItem.displayName = "DropdownMenuItem";
|
||||||
|
|
||||||
|
export const DropdownMenuLabel = ({ className, ...props }: React.ComponentPropsWithoutRef<typeof Menu.Label>) => (
|
||||||
|
<Menu.Label className={cn("px-2 py-1.5 text-xs font-semibold uppercase tracking-wide text-muted", className)} {...props} />
|
||||||
|
);
|
||||||
|
export const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof Menu.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof Menu.CheckboxItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<Menu.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn("relative flex cursor-pointer select-none items-center rounded py-1.5 pl-7 pr-2 text-sm outline-none focus:bg-surface-2", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<Menu.ItemIndicator><Check className="size-3.5" /></Menu.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</Menu.CheckboxItem>
|
||||||
|
));
|
||||||
|
DropdownMenuCheckboxItem.displayName = "DropdownMenuCheckboxItem";
|
||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "../lib/cn";
|
||||||
|
|
||||||
|
export const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
|
||||||
|
({ className, type, ...props }, ref) => (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full rounded border border-border bg-bg/40 px-3 py-2 text-sm text-text",
|
||||||
|
"placeholder:text-muted transition-shadow duration-base ease-out",
|
||||||
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:border-brand/60",
|
||||||
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
Input.displayName = "Input";
|
||||||
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "../lib/cn";
|
||||||
|
|
||||||
|
export const Table = ({ className, ...props }: React.HTMLAttributes<HTMLTableElement>) => (
|
||||||
|
<div className="relative w-full overflow-auto rounded-lg border border-border">
|
||||||
|
<table className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
export const TableHeader = ({ className, ...props }: React.HTMLAttributes<HTMLTableSectionElement>) => (
|
||||||
|
<thead className={cn("[&_tr]:border-b [&_tr]:border-border", className)} {...props} />
|
||||||
|
);
|
||||||
|
export const TableBody = ({ className, ...props }: React.HTMLAttributes<HTMLTableSectionElement>) => (
|
||||||
|
<tbody className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
||||||
|
);
|
||||||
|
export const TableRow = ({ className, ...props }: React.HTMLAttributes<HTMLTableRowElement>) => (
|
||||||
|
<tr className={cn("border-b border-border transition-colors hover:bg-surface-2/50 data-[state=selected]:bg-surface-2", className)} {...props} />
|
||||||
|
);
|
||||||
|
export const TableHead = ({ className, ...props }: React.ThHTMLAttributes<HTMLTableCellElement>) => (
|
||||||
|
<th className={cn("sticky top-0 z-10 bg-surface px-4 py-3 text-left font-display text-xs font-semibold uppercase tracking-wide text-muted", className)} {...props} />
|
||||||
|
);
|
||||||
|
export const TableCell = ({ className, ...props }: React.TdHTMLAttributes<HTMLTableCellElement>) => (
|
||||||
|
<td className={cn("px-4 py-3 align-middle", className)} {...props} />
|
||||||
|
);
|
||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
import { Toaster as Sonner, toast } from "sonner";
|
||||||
|
|
||||||
|
/** Pre-themed sonner Toaster. Drop <Toaster /> once near your app root. */
|
||||||
|
export const Toaster = (props: React.ComponentProps<typeof Sonner>) => (
|
||||||
|
<Sonner
|
||||||
|
theme="dark"
|
||||||
|
position="bottom-right"
|
||||||
|
toastOptions={{
|
||||||
|
classNames: {
|
||||||
|
toast: "!bg-surface !border-border !text-text !rounded-lg !shadow-lg",
|
||||||
|
description: "!text-muted",
|
||||||
|
actionButton: "!bg-brand !text-bg",
|
||||||
|
cancelButton: "!bg-surface-2 !text-muted",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { toast };
|
||||||
Executable
+34
@@ -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";
|
||||||
Executable
+7
@@ -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));
|
||||||
|
}
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
/** Toggle the .light class on <html>. 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";
|
||||||
|
}
|
||||||
Executable
+46
@@ -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;
|
||||||
Executable
+59
@@ -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 (
|
||||||
|
<motion.div
|
||||||
|
className={className}
|
||||||
|
initial={reduce ? false : { opacity: 0, y }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true, margin: "-10% 0px" }}
|
||||||
|
transition={{ duration: 0.4, ease: easeOut, delay }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stagger container — pair with <StaggerItem>. */
|
||||||
|
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 (
|
||||||
|
<motion.div
|
||||||
|
className={className}
|
||||||
|
variants={staggerContainer}
|
||||||
|
initial="hidden"
|
||||||
|
whileInView="show"
|
||||||
|
viewport={{ once: true, margin: "-10% 0px" }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function StaggerItem({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||||
|
return <motion.div className={className} variants={staggerItem}>{children}</motion.div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { motion };
|
||||||
Executable
+53
@@ -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 <alpha-value> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+70
@@ -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}) / <alpha-value>)`;
|
||||||
|
|
||||||
|
/** @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: [],
|
||||||
|
};
|
||||||
Executable
+17
@@ -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"]
|
||||||
|
}
|
||||||
Executable
+11
@@ -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"],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user