Compare commits

...
15 Commits
Author SHA1 Message Date
selimaj-devandclaude 2333ecf874 Pin Node 22 for Nixpacks builds
Next 16 needs Node >= 20.9; Nixpacks defaulted to 18.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-03 21:40:05 +02:00
selimaj-devandclaude e268f2a359 Use Mono.png as profile picture and favicon; sync proof statuses
- Dossier proof statuses now come from projects.ts
- Mono.png replaces the placeholder silhouette and the SVG favicon
- Header, hero and bust tweaks

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-03 21:20:37 +02:00
selimaj-devandclaude fd72e1082a Hero decode effect, static terrain, and selimaj.dev rebrand
- Brute-force scramble that decodes hero values left to right
- Full-screen hero on desktop with telemetry pinned to the bottom
- Terrain no longer drifts; home skips the page transition
- selimaj.dev is the root brand; Orus becomes one brand with its own
  projects, and independent projects branch from the root

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-03 21:04:01 +02:00
selimaj-devandclaude 85dbdb3db4 Rebuild portfolio in Next.js and Tailwind
Static-export Next.js 16 app with the tactical-map hero, Dajt terrain
background, dossier pages and the projects node index.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-03 20:40:10 +02:00
selimaj-dev 3ea4946164 back to fresh 2026-10-03 18:56:07 +02:00
selimaj-dev 277393578a Merge pull request #7 from selimaj-dev/improved-details
Improved details
2026-03-15 06:07:05 -07:00
selimaj-dev 9b04fe4034 Removed useless hooks 2026-03-15 14:02:33 +01:00
selimaj-dev 9c1d00fcd6 Removed tailwind warning on tooltip 2026-03-15 14:01:22 +01:00
selimaj-dev 0ba5c9f35c Removed unused ui components 2026-03-15 14:00:53 +01:00
selimaj-dev 32776684bf Removed tailwind warnings 2026-03-15 13:54:59 +01:00
selimaj-dev 12c0f8ad30 updated projects 2026-03-15 13:53:27 +01:00
selimaj-dev 447584c4d7 Email Me 2026-03-15 13:39:22 +01:00
selimaj-dev e2dda4f086 Updated title 2026-03-15 13:17:10 +01:00
selimaj-dev 4071d5c615 Merge pull request #6 from selimaj-dev/updated-portfolio
Updated portfolio UI (again)
2026-03-14 22:14:09 -07:00
selimaj-dev 0d415891a4 Hero flex wrap 2026-03-15 06:12:10 +01:00
118 changed files with 7641 additions and 16968 deletions
+38 -16
View File
@@ -1,24 +1,46 @@
# Logs # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
logs
*.log # dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log* .pnpm-debug.log*
lerna-debug.log*
node_modules # env files (can opt-in for committing if needed)
dist .env*
dist-ssr
*.local
# Editor directories and files # vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# editors
.vscode/* .vscode/*
!.vscode/extensions.json !.vscode/extensions.json
.idea .idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+9
View File
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
<!-- END:nextjs-agent-rules -->
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
-16
View File
@@ -1,16 +0,0 @@
# Stage 1: Build the application
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Stage 2: Serve the static files with Nginx
FROM nginx:alpine
# Copy the build files from the builder stage to the Nginx web directory
COPY --from=builder /app/dist /usr/share/nginx/html
# Expose port 80 where the Nginx server runs
EXPOSE 80
# Command to start Nginx server
CMD ["nginx", "-g", "daemon off;"]
+22 -67
View File
@@ -1,73 +1,28 @@
# React + TypeScript + Vite # selimaj.dev
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. Portfolio site. Next.js (App Router, static export) and Tailwind CSS v4.
Currently, two official plugins are available: ```bash
npm install
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) npm run dev # http://localhost:3000
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) npm run build # static site in out/
npm run lint
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
``` ```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: `npm run build` writes plain HTML/CSS/JS to `out/`, which any static host can serve.
```js ## Where things live
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([ - `src/content/`: all copy and data, including every placeholder (marked `PLACEHOLDER`).
globalIgnores(['dist']), - `profile.ts`: name, title, stance, dossier fields, links, map lock.
{ - `dossier.ts`: the five About files.
files: ['**/*.{ts,tsx}'], - `projects.ts`: projects, statuses, node-map positions, featured nodes.
extends: [ - `public/profile/Mono.png`: profile picture (the favicon is a copy at `src/app/icon.png`). Replace the file, or point `profile.pfp` at a new one.
// Other configs... - `public/terrain.svg`: hero terrain contours (Dajt elevation data).
// Enable lint rules for React - `src/app/globals.css`: colour tokens, bracket/grid utilities, and all motion.
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM ## Rules worth keeping
reactDom.configs.recommended,
], - Green (`accent`) means active or detected: hover, focus, current page, live telemetry, the lock. Nothing at rest is green.
languageOptions: { - The map shows Tirana city centre only. Never add anything finer than city scale.
parserOptions: { - All motion sits behind `prefers-reduced-motion: no-preference`. Text renders without JS.
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
-23
View File
@@ -1,23 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
-13
View File
@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>portfolio</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+10
View File
@@ -0,0 +1,10 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Static-first: `next build` emits plain HTML/CSS/JS into `out/`.
output: "export",
trailingSlash: true,
images: { unoptimized: true },
};
export default nextConfig;
+5233 -8452
View File
File diff suppressed because it is too large Load Diff
+21 -76
View File
@@ -1,84 +1,29 @@
{ {
"name": "selimaj-portfolio", "name": "selimaj-dev",
"version": "1.0.0", "version": "0.1.0",
"type": "module", "private": true,
"license": "MIT",
"scripts": { "scripts": {
"dev": "vite --host", "dev": "next dev",
"build": "vite build", "build": "next build",
"preview": "vite preview --host", "start": "next start",
"check": "tsc --noEmit", "lint": "eslint"
"format": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.2.2", "next": "16.3.8",
"@radix-ui/react-accordion": "^1.2.12", "react": "19.2.8",
"@radix-ui/react-alert-dialog": "^1.1.15", "react-dom": "19.2.8"
"@radix-ui/react-aspect-ratio": "^1.1.7",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"axios": "^1.12.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"embla-carousel-react": "^8.6.0",
"framer-motion": "^12.23.22",
"input-otp": "^1.4.2",
"lucide-react": "^0.453.0",
"nanoid": "^5.1.5",
"next-themes": "^0.4.6",
"react": "^19.2.1",
"react-day-picker": "^9.11.1",
"react-dom": "^19.2.1",
"react-hook-form": "^7.64.0",
"react-resizable-panels": "^3.0.6",
"recharts": "^2.15.2",
"sonner": "^2.0.7",
"streamdown": "^1.4.0",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"wouter": "^3.3.5",
"zod": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.15", "@tailwindcss/postcss": "^4",
"@tailwindcss/vite": "^4.2.1", "@types/node": "^20",
"@types/node": "^24.7.0", "@types/react": "^19",
"@types/react": "^19.2.1", "@types/react-dom": "^19",
"@types/react-dom": "^19.2.1", "eslint": "^9",
"@vitejs/plugin-react": "^5.0.4", "eslint-config-next": "16.3.8",
"autoprefixer": "^10.4.20", "tailwindcss": "^4",
"pnpm": "^10.15.1", "typescript": "^5"
"postcss": "^8.4.47",
"prettier": "^3.6.2",
"tailwindcss": "^4.2.1",
"tsx": "^4.19.1",
"tw-animate-css": "^1.4.0",
"typescript": "5.6.3",
"vite": "^7.1.7",
"vitest": "^2.1.4"
}, },
"packageManager": "[email protected]" "engines": {
"node": "22.x"
}
} }
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.8 KiB

-11
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.8 KiB

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 304 KiB

-32
View File
@@ -1,32 +0,0 @@
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import NotFound from "@/pages/NotFound";
import { Route, Switch } from "wouter";
import ErrorBoundary from "./components/ErrorBoundary";
import { ThemeProvider } from "./contexts/ThemeContext";
import Home from "./pages/Home";
function Router() {
return (
<Switch>
<Route path={"/"} component={Home} />
<Route path={"/404"} component={NotFound} />
<Route component={NotFound} />
</Switch>
);
}
function App() {
return (
<ErrorBoundary>
<ThemeProvider defaultTheme="dark">
<TooltipProvider>
<Toaster />
<Router />
</TooltipProvider>
</ThemeProvider>
</ErrorBoundary>
);
}
export default App;
+131
View File
@@ -0,0 +1,131 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { StatusTag } from "@/components/StatusTag";
import { dossier, fileBySlug, type Proof } from "@/content/dossier";
export const dynamicParams = false;
export function generateStaticParams() {
return dossier.map((f) => ({ slug: f.slug }));
}
export async function generateMetadata({ params }: PageProps<"/about/[slug]">): Promise<Metadata> {
const { slug } = await params;
const f = fileBySlug(slug);
return f ? { title: f.title, description: f.summary } : {};
}
function ProofRow({ p }: { p: Proof }) {
const inner = (
<>
<span>
<span className="font-display text-[17px] font-semibold tracking-[0.06em] uppercase">{p.label}</span>
<span className="mt-0.5 block text-[11px] text-mute">{p.note}</span>
</span>
{p.status ? <StatusTag status={p.status} /> : <span />}
<span aria-hidden="true" className="text-mute group-hover:text-accent group-focus-visible:text-accent">
{p.href ? (p.href.startsWith("http") ? "↗" : "→") : ""}
</span>
</>
);
const cls = "group bracket grid min-h-14 grid-cols-[minmax(0,1fr)_auto_16px] items-center gap-5 px-3 [--bc:transparent]";
if (!p.href) return <div className={cls}>{inner}</div>;
const interactive = `${cls} hover:[--bc:var(--color-accent)] focus-visible:outline-none focus-visible:[--bc:var(--color-accent)]`;
return p.href.startsWith("http") ? (
<a href={p.href} className={interactive}>
{inner}
</a>
) : (
<Link href={p.href} className={interactive}>
{inner}
</Link>
);
}
export default async function DossierFilePage({ params }: PageProps<"/about/[slug]">) {
const { slug } = await params;
const f = fileBySlug(slug);
if (!f) notFound();
const i = dossier.indexOf(f);
const prev = dossier[i - 1];
const next = dossier[i + 1];
return (
<main className="pt-[68px] md:pt-[85px]">
<div className="mx-auto flex max-w-[1240px] flex-col gap-8 px-4 pt-8 pb-20 md:px-16 md:pt-[72px] md:pb-24 lg:flex-row lg:items-start lg:gap-24">
<aside aria-label="Dossier files" className="lg:sticky lg:top-8 lg:w-[220px] lg:flex-none">
<Link href="/about/" className="inline-flex min-h-11 items-center gap-2 text-[10px] tracking-[0.22em] text-mute hover:text-accent">
<span aria-hidden="true">←</span> DOSSIER INDEX
</Link>
<ol className="mt-4 flex flex-wrap gap-x-4 text-[11px] tracking-[0.16em] lg:block">
{dossier.map((d) => {
const current = d.slug === f.slug;
return (
<li key={d.slug}>
<Link
href={`/about/${d.slug}/`}
aria-current={current ? "page" : undefined}
className={`flex min-h-10 items-center gap-3 uppercase ${current ? "text-ink" : "text-mute hover:text-ink"}`}
>
<span className={`w-6 ${current ? "text-accent" : ""}`}>{d.no}</span>
{d.title}
{current && <span aria-hidden="true" className="size-1.5 bg-accent" />}
</Link>
</li>
);
})}
</ol>
</aside>
<article className="min-w-0 max-w-[720px] flex-1">
<div className="flex flex-wrap gap-5 text-[10px] tracking-[0.24em] text-mute">
<span>
FILE {f.no} / {String(dossier.length).padStart(2, "0")}
</span>
<span>CLASS · OPEN</span>
</div>
<h1 className="mt-3.5 font-display text-[52px] leading-[0.95] font-semibold uppercase md:text-[80px]">{f.title}</h1>
<div aria-hidden="true" className="mt-8 h-px bg-white/10" />
<p className="mt-10 text-xl leading-[1.55] md:text-[22px]">{f.lead}</p>
{f.paragraphs.map((t) => (
<p key={t} className="mt-5 text-[15px] leading-[1.8] text-ink-2">
{t}
</p>
))}
<section aria-labelledby="proof-title" className="bracket mt-14 border border-white/6 bg-surface p-6 [--bc:rgb(242_244_245/0.45)]">
<h2 id="proof-title" className="text-[10px] font-normal tracking-[0.3em] text-mute">
PROOF
</h2>
<ul className="mt-4 flex flex-col gap-1">
{f.proof.map((p) => (
<li key={p.label}>
<ProofRow p={p} />
</li>
))}
</ul>
</section>
<nav aria-label="Adjacent files" className="mt-16 flex flex-col justify-between gap-4 border-t border-white/8 pt-6 sm:flex-row">
{prev ? (
<Link href={`/about/${prev.slug}/`} className="group flex min-h-11 flex-col gap-1.5">
<span className="text-[9px] tracking-[0.24em] text-mute">← FILE {prev.no}</span>
<span className="font-display text-lg font-semibold tracking-[0.06em] uppercase group-hover:text-accent">{prev.title}</span>
</Link>
) : (
<span />
)}
{next && (
<Link href={`/about/${next.slug}/`} className="group flex min-h-11 flex-col gap-1.5 sm:text-right">
<span className="text-[9px] tracking-[0.24em] text-mute">FILE {next.no} →</span>
<span className="font-display text-lg font-semibold tracking-[0.06em] uppercase group-hover:text-accent">{next.title}</span>
</Link>
)}
</nav>
</article>
</div>
</main>
);
}
+68
View File
@@ -0,0 +1,68 @@
import type { Metadata } from "next";
import Link from "next/link";
import { dossier } from "@/content/dossier";
import { profile } from "@/content/profile";
export const metadata: Metadata = {
title: "Dossier",
description: "Five short files: leadership, entrepreneurship, building, discipline, passions.",
};
const cols = "md:grid-cols-[96px_240px_minmax(0,1fr)_200px_32px] xl:grid-cols-[96px_260px_minmax(0,1fr)_240px_32px]";
export default function AboutPage() {
return (
<main className="grid-bg min-h-full pt-[68px] md:pt-[85px]">
<div className="mx-auto max-w-[1440px] px-4 pt-10 pb-20 md:px-16 md:pt-[72px] md:pb-24">
<div className="flex flex-col justify-between gap-10 md:flex-row md:items-end md:gap-16">
<div>
<div className="label tracking-[0.3em]">Case file · Subject 0001</div>
<h1 className="mt-3 font-display text-[52px] leading-[0.95] font-semibold uppercase md:text-7xl">Dossier</h1>
<p className="mt-5 max-w-[520px] text-sm leading-[1.7] text-ink-2">
Five files. Each one is short: two or three paragraphs and one piece of proof.
</p>
</div>
<dl className="bracket grid min-w-[280px] grid-cols-[110px_minmax(0,1fr)] gap-y-2 border border-white/6 bg-surface px-[22px] py-[18px] text-[11px] [--bc:rgb(242_244_245/0.4)]">
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">SUBJECT</dt>
<dd>{profile.displayName}</dd>
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">DOMAIN</dt>
<dd>selimaj.dev</dd>
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">FILES</dt>
<dd>{String(dossier.length).padStart(2, "0")}</dd>
<dt className="pt-0.5 text-[9px] tracking-[0.22em] text-mute">ACCESS</dt>
<dd>Open</dd>
</dl>
</div>
<div aria-hidden="true" className={`mt-16 hidden gap-6 border-b border-white/10 px-5 pb-3 text-[9px] tracking-[0.24em] text-mute md:grid ${cols}`}>
<span>ID</span>
<span>FILE</span>
<span>SUMMARY</span>
<span>PROOF</span>
<span />
</div>
<ol className="mt-8 border-t border-white/7 md:mt-0 md:border-t-0">
{dossier.map((f) => (
<li key={f.slug} className="border-b border-white/7">
<Link
href={`/about/${f.slug}/`}
className={`group bracket grid min-h-[88px] grid-cols-[72px_minmax(0,1fr)_24px] items-center gap-6 px-3 transition-colors [--bc:transparent] hover:bg-white/2 hover:[--bc:var(--color-accent)] focus-visible:outline-none focus-visible:[--bc:var(--color-accent)] md:px-5 ${cols}`}
>
<span className="text-[11px] tracking-[0.2em] text-mute group-hover:text-accent group-focus-visible:text-accent">FILE {f.no}</span>
<span className="font-display text-lg font-semibold tracking-[0.06em] uppercase md:text-[22px]">{f.title}</span>
<span className="hidden text-xs leading-[1.6] text-ink-2 md:block">{f.summary}</span>
<span className="hidden text-[10px] tracking-[0.18em] text-mute uppercase md:block">{f.proofSummary}</span>
<span aria-hidden="true" className="text-mute transition-transform group-hover:translate-x-1 group-hover:text-accent">
→
</span>
</Link>
</li>
))}
</ol>
<p aria-hidden="true" className="mt-5 text-[9px] tracking-[0.22em] text-mute/70">
{String(dossier.length).padStart(2, "0")} FILES · END OF INDEX
</p>
</div>
</main>
);
}
+342
View File
@@ -0,0 +1,342 @@
@import "tailwindcss";
/* `js:` applies only once the head script has marked <html class="js">. */
@custom-variant js (&:where(.js *));
@theme {
--color-night: #050607;
--color-surface: #0b0d0e;
--color-surface-2: #121517;
--color-ink: #f2f4f5;
--color-ink-2: #c4cacd;
--color-mute: #8b9296;
/* Green means active or detected. Never decoration. */
--color-accent: #4dff95;
--color-line: rgb(255 255 255 / 0.07);
--color-line-2: rgb(255 255 255 / 0.12);
}
@theme inline {
--font-display: var(--font-chakra), ui-sans-serif, system-ui, sans-serif;
--font-mono: var(--font-jetbrains), ui-monospace, SFMono-Regular, monospace;
}
@layer base {
html {
background: var(--color-night);
color: var(--color-ink);
color-scheme: dark;
}
body {
font-family: var(--font-mono);
-webkit-font-smoothing: antialiased;
}
:focus-visible {
outline: 1px solid var(--color-accent);
outline-offset: 3px;
}
::selection {
background: rgb(77 255 149 / 0.25);
}
}
/* Corner brackets. Set --bc for colour, --bl for arm length. Uses only the
background-image longhands so it composes with bg-* colour utilities. */
@utility bracket {
--bc: rgb(242 244 245 / 0.28);
--bl: 10px;
background-image:
linear-gradient(var(--bc), var(--bc)), linear-gradient(var(--bc), var(--bc)),
linear-gradient(var(--bc), var(--bc)), linear-gradient(var(--bc), var(--bc)),
linear-gradient(var(--bc), var(--bc)), linear-gradient(var(--bc), var(--bc)),
linear-gradient(var(--bc), var(--bc)), linear-gradient(var(--bc), var(--bc));
background-repeat: no-repeat;
background-position: 0 0, 0 0, 100% 0, 100% 0, 0 100%, 0 100%, 100% 100%, 100% 100%;
background-size:
var(--bl) 1px, 1px var(--bl), var(--bl) 1px, 1px var(--bl),
var(--bl) 1px, 1px var(--bl), var(--bl) 1px, 1px var(--bl);
}
@utility grid-bg {
background-image:
linear-gradient(rgb(255 255 255 / 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgb(255 255 255 / 0.03) 1px, transparent 1px);
background-size: 48px 48px;
}
@utility scan-bg {
background-image: repeating-linear-gradient(0deg, rgb(255 255 255 / 0.016) 0 1px, transparent 1px 3px);
}
@utility label {
font-size: 10px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--color-mute);
}
/* ---------- Boot overlay (only ever shown when the head script sets data-boot) ---------- */
.boot {
display: none;
}
html[data-boot] .boot {
display: flex;
}
html[data-boot] body {
overflow: hidden;
}
/* Hold the hero sequence until boot finishes; removing data-boot restarts it. */
html[data-boot] .seq {
animation: none !important;
}
/* ---------- Motion (all of it opt-in; reduced motion gets the final state) ---------- */
@media (prefers-reduced-motion: no-preference) {
.seq {
animation-delay: var(--d, 0s);
animation-fill-mode: both;
}
.spin {
animation: spin 16s linear infinite;
transform-origin: 60px 60px;
}
.blink {
animation: blink 1.1s steps(2) infinite;
}
/* Text resolves from outline to solid with a short glitch. */
.resolve {
animation-name: resolve;
animation-duration: 0.55s;
animation-timing-function: linear;
}
/* Panels build as a dashed wireframe, then fill. */
.panel {
animation-name: panel;
animation-duration: 0.6s;
animation-timing-function: linear;
}
/* Map strokes draw in (paths need pathLength="1"). */
.draw path,
.draw circle,
.draw rect {
stroke-dasharray: 1;
animation: draw 0.9s ease-out both;
animation-delay: var(--d, 0s);
}
html[data-boot] .draw path,
html[data-boot] .draw circle,
html[data-boot] .draw rect {
animation: none;
}
.reticle {
animation-name: reticle;
animation-duration: 0.7s;
animation-timing-function: cubic-bezier(0.6, 0, 0.2, 1);
}
.trace {
animation-name: trace;
animation-duration: 0.3s;
animation-timing-function: ease-out;
transform-origin: 0 50%;
}
.appear {
animation-name: appear;
animation-duration: 0.01s;
}
.bust-solid {
animation-name: bust-solid;
animation-duration: 0.8s;
animation-timing-function: linear;
}
.bust-scan {
animation-name: bust-scan;
animation-duration: 0.8s;
animation-timing-function: linear;
}
.page-resolve {
animation: page-resolve 0.45s linear both;
}
.type-line {
overflow: hidden;
white-space: nowrap;
animation: type 0.3s steps(24) both;
animation-delay: var(--d, 0s);
}
.boot-out {
animation: boot-out 0.25s linear both;
}
.seg-on {
animation: seg-grow 0.45s ease-out both;
}
}
@media (prefers-reduced-motion: reduce) {
.page-resolve {
animation: fade 0.2s ease-out both;
}
.bust-scan {
display: none;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes blink {
50% {
opacity: 0;
}
}
@keyframes fade {
from {
opacity: 0;
}
}
@keyframes appear {
from {
visibility: hidden;
}
}
@keyframes resolve {
0% {
color: transparent;
-webkit-text-stroke: 1px rgb(242 244 245 / 0.45);
clip-path: inset(0 100% 0 0);
}
40% {
color: transparent;
-webkit-text-stroke: 1px rgb(242 244 245 / 0.45);
clip-path: inset(0 0 0 0);
}
55% {
transform: translateX(2px);
text-shadow: -2px 0 rgb(255 70 90 / 0.45), 2px 0 rgb(70 150 255 / 0.45);
}
62% {
transform: translateX(-1px);
}
70% {
transform: none;
text-shadow: none;
}
}
@keyframes panel {
0% {
clip-path: inset(0 0 100% 0);
background-color: transparent;
outline: 1px dashed rgb(242 244 245 / 0.35);
}
45% {
clip-path: inset(0 0 0 0);
background-color: transparent;
outline: 1px dashed rgb(242 244 245 / 0.35);
}
58% {
transform: translateX(2px);
filter: drop-shadow(-1px 0 rgb(255 70 90 / 0.5)) drop-shadow(1px 0 rgb(70 150 255 / 0.5));
}
66% {
transform: none;
filter: none;
outline: 0;
}
}
@keyframes draw {
from {
stroke-dashoffset: 1;
}
to {
stroke-dashoffset: 0;
}
}
@keyframes reticle {
0% {
opacity: 0;
transform: translate(-90px, 50px) scale(1.4);
}
10% {
opacity: 1;
}
45% {
transform: translate(40px, -24px) scale(1.4);
}
80% {
transform: translate(0, 0) scale(1.4);
}
100% {
transform: translate(0, 0) scale(1);
}
}
@keyframes trace {
from {
transform: scaleX(0);
}
}
@keyframes bust-solid {
from {
clip-path: inset(0 100% 0 0);
}
to {
clip-path: inset(0 0 0 0);
}
}
@keyframes bust-scan {
0% {
left: 0;
opacity: 1;
}
95% {
left: 100%;
opacity: 1;
}
100% {
left: 100%;
opacity: 0;
}
}
@keyframes page-resolve {
0% {
clip-path: inset(0 0 100% 0);
filter: drop-shadow(-2px 0 rgb(255 70 90 / 0.5)) drop-shadow(2px 0 rgb(70 150 255 / 0.5));
}
60% {
clip-path: inset(0 0 0 0);
transform: translateX(2px);
}
75% {
transform: translateX(-1px);
}
100% {
transform: none;
filter: none;
}
}
@keyframes type {
from {
max-width: 0;
}
to {
max-width: 760px;
}
}
@keyframes boot-out {
40% {
transform: translateX(3px);
filter: drop-shadow(-2px 0 rgb(255 70 90 / 0.6)) drop-shadow(2px 0 rgb(70 150 255 / 0.6));
}
100% {
clip-path: inset(50% 0 50% 0);
}
}
@keyframes seg-grow {
from {
clip-path: inset(0 100% 0 0);
}
to {
clip-path: inset(0 0 0 0);
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+41
View File
@@ -0,0 +1,41 @@
import type { Metadata } from "next";
import { Chakra_Petch, JetBrains_Mono } from "next/font/google";
import { SiteHeader } from "@/components/SiteHeader";
import { SiteFooter } from "@/components/SiteFooter";
import "./globals.css";
const chakra = Chakra_Petch({
variable: "--font-chakra",
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
});
const jetbrains = JetBrains_Mono({
variable: "--font-jetbrains",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: { default: "Selimaj · Entrepreneur & Builder", template: "%s · Selimaj" },
description: "Selimaj, entrepreneur and builder. I build the layer underneath.",
metadataBase: new URL("https://selimaj.dev"),
};
// Runs before paint: marks JS as available, and arms the boot sequence once
// per session on the home page (never with reduced motion).
const headScript = `(function(){var d=document.documentElement;d.classList.add('js');try{if(location.pathname==='/'&&!sessionStorage.getItem('boot')&&!matchMedia('(prefers-reduced-motion: reduce)').matches){d.setAttribute('data-boot','')}}catch(e){}})();`;
export default function RootLayout({ children }: LayoutProps<"/">) {
return (
<html lang="en" suppressHydrationWarning className={`${chakra.variable} ${jetbrains.variable}`}>
<head>
<script dangerouslySetInnerHTML={{ __html: headScript }} />
</head>
<body className="flex min-h-dvh flex-col overflow-x-hidden bg-night text-ink">
<SiteHeader />
<div className="flex-1">{children}</div>
<SiteFooter />
</body>
</html>
);
}
+16
View File
@@ -0,0 +1,16 @@
import Link from "next/link";
export default function NotFound() {
return (
<main className="grid-bg flex min-h-[70dvh] items-center pt-[85px]">
<div className="mx-auto w-full max-w-[1440px] px-4 md:px-16">
<div className="label tracking-[0.3em]">404 · Signal lost</div>
<h1 className="mt-3 font-display text-[52px] leading-[0.95] font-semibold uppercase md:text-7xl">No trace</h1>
<p className="mt-5 text-sm text-ink-2">Nothing resolves at this address.</p>
<Link href="/" className="mt-8 inline-flex min-h-12 items-center gap-3 bg-ink px-[22px] text-xs tracking-[0.2em] text-night hover:bg-accent">
RETURN TO BASE <span aria-hidden="true">→</span>
</Link>
</div>
</main>
);
}
+29
View File
@@ -0,0 +1,29 @@
import Link from "next/link";
import { BootSequence } from "@/components/BootSequence";
import { FeaturedNodes } from "@/components/FeaturedNodes";
import { Hero } from "@/components/Hero";
export default function Home() {
return (
<>
<BootSequence />
<main>
<Hero />
<section aria-labelledby="nodes-title" className="mx-auto max-w-[1440px] px-4 pt-16 pb-16 md:px-16 md:pt-24 md:pb-20">
<div className="flex flex-wrap items-end justify-between gap-6">
<div>
<div className="label tracking-[0.3em]">02 / Nodes</div>
<h2 id="nodes-title" className="mt-2.5 font-display text-[28px] font-semibold tracking-[0.04em] uppercase md:text-[40px]">
Featured
</h2>
</div>
<Link href="/projects/" className="inline-flex min-h-11 items-center gap-2.5 text-[11px] tracking-[0.2em] text-mute hover:text-accent">
ALL PROJECTS <span aria-hidden="true">→</span>
</Link>
</div>
<FeaturedNodes />
</section>
</main>
</>
);
}
+23
View File
@@ -0,0 +1,23 @@
import type { Metadata } from "next";
import { ProjectIndex } from "@/components/ProjectIndex";
import { projects } from "@/content/projects";
export const metadata: Metadata = {
title: "Projects",
description: "Everything I’ve built and conceived, from Enclave and Osias to cyberdecks and RC planes.",
};
export default function ProjectsPage() {
return (
<main className="pt-[68px] md:pt-[85px]">
<div className="mx-auto max-w-[1440px] px-4 pt-10 pb-20 md:px-16 md:pt-16 md:pb-24">
<div className="label tracking-[0.3em]">Node index · Root selimaj.dev · {projects.length} nodes</div>
<h1 className="mt-3 font-display text-[52px] leading-[0.95] font-semibold uppercase md:text-7xl">Projects</h1>
<p className="mt-[18px] max-w-[560px] text-[13px] leading-[1.7] text-ink-2">
Most of what I&apos;ve built and conceived. Concepts are labelled as concepts.
</p>
<ProjectIndex />
</div>
</main>
);
}
+11
View File
@@ -0,0 +1,11 @@
"use client";
import { usePathname } from "next/navigation";
// Re-mounts on every navigation, so each page resolves in from wireframe.
// Home is skipped: the hero runs its own sequence and the terrain stays still.
export default function Template({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
if (pathname === "/") return <>{children}</>;
return <div className="page-resolve seq">{children}</div>;
}
-200
View File
@@ -1,200 +0,0 @@
/*
* ABOUT SECTION — selimaj.dev
* Design: Glassmorphic Terminal
* Asymmetric layout: text left, abstract visual right
*/
import { EarthIcon, LayersIcon, TargetIcon, ZapIcon } from "lucide-react";
import { useEffect, useRef } from "react";
const ABOUT_VISUAL =
"/logo.png";
function useReveal() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
el.classList.add("visible");
observer.disconnect();
}
},
{ threshold: 0.15 },
);
observer.observe(el);
return () => observer.disconnect();
}, []);
return ref;
}
const highlights = [
{ Icon: ZapIcon, label: "Currently", value: "Grinding on SaaS products" },
{ Icon: LayersIcon, label: "Fav Stack", value: "Rust · TypeScript · Linux" },
{ Icon: TargetIcon, label: "Focus", value: "Low-level systems & UI/UX" },
{ Icon: EarthIcon, label: "Based in", value: "Albania" },
];
export default function AboutSection() {
const ref1 = useReveal();
const ref2 = useReveal();
return (
<section
id="about"
className="py-32 relative"
style={{ background: "#080808" }}
>
{/* Subtle top border */}
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-6xl mx-auto px-6">
{/* Section label */}
<div className="terminal-label mb-4">&gt; about.me</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
{/* Left: Text */}
<div ref={ref1} className="reveal">
<h2 className="section-heading text-4xl sm:text-5xl text-white mb-8 leading-tight">
Building the future,
<br />
<span className="text-white/40">one commit at a time.</span>
</h2>
<div
className="space-y-4 text-white/60 leading-relaxed"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
<p>
I'm{" "}
<strong className="text-white font-semibold">
Klesti Selimaj
</strong>{" "}
— a 16-year-old software developer and entrepreneur from Albania
with 6 years of hands-on experience. I started coding at 10 and
never stopped.
</p>
<p>
My passion lies in{" "}
<strong className="text-white/80">
low-level systems programming
</strong>
, building developer tools, and crafting polished user
experiences. I work primarily in{" "}
<strong className="text-white/80">Rust</strong> and{" "}
<strong className="text-white/80">TypeScript</strong>, and I'm
deeply interested in hacking, Linux internals, and TUI
applications.
</p>
<p>
Beyond code, I'm building towards a successful SaaS product —
combining my technical depth with an entrepreneurial mindset to
create tools that developers actually want to use.
</p>
</div>
{/* Highlights grid */}
<div className="mt-10 grid grid-cols-2 gap-3">
{highlights.map((h) => (
<div key={h.label} className="glass rounded-xl p-4 card-hover">
<div className="flex items-center gap-3">
<div className="text-lg">{<h.Icon />}</div>
<div>
<div className="terminal-label text-xs mb-0.5">
{h.label}
</div>
<div
className="text-sm text-white/80 font-medium"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
{h.value}
</div>
</div>
</div>
</div>
))}
</div>
{/* Social links */}
<div className="mt-8 flex flex-wrap gap-3">
{[
{
label: "GitHub",
href: "https://github.com/selimaj-dev",
icon: "⌥",
},
{
label: "YouTube",
href: "https://youtube.com/@SelimajDev",
icon: "▶",
},
{
label: "Instagram",
href: "https://instagram.com/selimaj.dev",
icon: "◈",
},
].map((s) => (
<a
key={s.label}
href={s.href}
target="_blank"
rel="noopener noreferrer"
className="skill-chip hover:no-underline"
>
<span className="text-white/40">{s.icon}</span>
{s.label}
</a>
))}
</div>
</div>
{/* Right: Visual */}
<div ref={ref2} className="reveal flex justify-center lg:justify-end">
<div className="relative w-full max-w-md">
{/* Glow behind image */}
<div
className="absolute inset-0 rounded-2xl blur-3xl opacity-20"
style={{
background:
"radial-gradient(circle, rgba(255,255,255,0.15) 0%, transparent 70%)",
}}
/>
<div className="relative glass rounded-2xl overflow-hidden float-anim">
<img
src={ABOUT_VISUAL}
alt="Abstract tech visualization"
className="w-full h-auto object-cover"
style={{ aspectRatio: "1/1" }}
/>
{/* Overlay with terminal text */}
<div
className="absolute bottom-0 left-0 right-0 p-5"
style={{
background:
"linear-gradient(to top, rgba(8,8,8,0.9) 0%, transparent 100%)",
}}
>
<div className="terminal-label text-xs">
<span className="text-white/30">$</span>{" "}
<span className="text-white/60">whoami</span>
</div>
<div
className="text-white/80 text-sm mt-1"
style={{ fontFamily: "'JetBrains Mono', monospace" }}
>
Klesti Selimaj
</div>
<div className="terminal-label text-xs mt-0.5">
uid=1000(klesti) groups=dev,entrepreneur
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
+85
View File
@@ -0,0 +1,85 @@
"use client";
import { useEffect, useState } from "react";
import { useSession } from "./Telemetry";
const DURATION = 1900;
/**
* Once-per-session boot screen on the home page. It only shows when the head
* script has set <html data-boot>, so without JS (or with reduced motion) it
* never appears. Click, tap or any key skips it.
*/
export function BootSequence() {
const s = useSession();
const [leaving, setLeaving] = useState(false);
useEffect(() => {
const root = document.documentElement;
if (!root.hasAttribute("data-boot")) return;
let done = false;
let outTimer: number | undefined;
const finish = () => {
if (done) return;
done = true;
try {
sessionStorage.setItem("boot", "1");
} catch {}
setLeaving(true);
outTimer = window.setTimeout(() => {
root.removeAttribute("data-boot");
window.dispatchEvent(new Event("boot:done"));
}, 250);
};
const timer = window.setTimeout(finish, DURATION);
window.addEventListener("keydown", finish);
window.addEventListener("pointerdown", finish);
return () => {
clearTimeout(timer);
clearTimeout(outTimer);
window.removeEventListener("keydown", finish);
window.removeEventListener("pointerdown", finish);
};
}, []);
const stamp = s ? new Date(s.start).toISOString() : "";
const lines = [
{ text: "SESSION OPEN", tail: "[ OK ]" },
{ text: stamp },
{ text: s ? `HASH ${s.hash}` : "HASH" },
{ text: "SIGNAL ACQUIRED · TRACE ▸ TIRANA, AL" },
];
return (
<div
role="status"
aria-label="Loading"
className={`boot fixed inset-0 z-50 flex-col justify-end bg-night ${leaving ? "boot-out" : ""}`}
>
<div aria-hidden="true" className="grid-bg absolute inset-0" />
<div aria-hidden="true" className="scan-bg absolute inset-0" />
<div className="relative px-6 pb-[18vh] text-[13px] leading-[2.1] tracking-[0.08em] md:px-24 md:text-sm">
{lines.map((l, i) => (
<div key={i} className="type-line flex gap-[18px] text-ink-2 first:text-ink" style={{ "--d": `${0.1 + i * 0.3}s` } as React.CSSProperties}>
<span className="text-mute">&gt;</span>
{l.text}
{l.tail && <span className="text-mute">{l.tail}</span>}
</div>
))}
<div className="type-line flex gap-[18px] text-accent" style={{ "--d": "1.3s" } as React.CSSProperties}>
<span className="text-mute">&gt;</span>LOCKING<span className="blink">▌</span>
</div>
<div className="mt-10 w-full max-w-[420px]">
<div className="flex justify-between text-[9px] tracking-[0.22em] text-mute">
<span>TRACE</span>
</div>
<div className="mt-2 h-px bg-white/12">
<div className="h-px origin-left bg-accent shadow-[0_0_8px_var(--color-accent)] [animation:trace_1.8s_ease-out_both]" />
</div>
</div>
</div>
<p className="absolute right-6 bottom-8 text-[10px] tracking-[0.24em] text-mute md:right-16">CLICK OR PRESS ANY KEY TO SKIP</p>
</div>
);
}
+91
View File
@@ -0,0 +1,91 @@
"use client";
import Image from "next/image";
import { useRef } from "react";
/**
* 2.5D identity reveal. The picture first shows as a high-contrast scanline
* "wireframe", a scan sweeps across and resolves it to solid, and on desktop
* the layers tilt apart slightly with the pointer. No 3D library needed.
*/
export function Bust({
src,
delay = "0s",
className = "",
}: {
src: string;
delay?: string;
className?: string;
}) {
const tiltRef = useRef<HTMLDivElement>(null);
const meshRef = useRef<HTMLDivElement>(null);
function onMove(e: React.PointerEvent<HTMLDivElement>) {
if (
e.pointerType !== "mouse" ||
matchMedia("(prefers-reduced-motion: reduce)").matches
)
return;
const r = e.currentTarget.getBoundingClientRect();
const x = (e.clientX - r.left) / r.width - 0.5;
const y = (e.clientY - r.top) / r.height - 0.5;
if (tiltRef.current)
tiltRef.current.style.transform = `rotateX(${(-y * 8).toFixed(2)}deg) rotateY(${(x * 10).toFixed(2)}deg)`;
if (meshRef.current)
meshRef.current.style.transform = `translate(${(x * -6).toFixed(1)}px, ${(y * -6).toFixed(1)}px)`;
}
function onLeave() {
if (tiltRef.current) tiltRef.current.style.transform = "";
if (meshRef.current) meshRef.current.style.transform = "";
}
return (
<div
className={`relative aspect-square [perspective:600px] ${className}`}
onPointerMove={onMove}
onPointerLeave={onLeave}
>
<div
ref={tiltRef}
className="absolute inset-0 transition-transform duration-200 ease-out [transform-style:preserve-3d]"
>
{/* wireframe pass */}
<div className="absolute inset-0 [mask-image:repeating-linear-gradient(0deg,#000_0_1px,transparent_1px_5px)]">
<Image
src={src}
alt=""
fill
sizes="168px"
className="object-cover opacity-80 [filter:grayscale(1)_contrast(2.4)_brightness(1.6)_drop-shadow(1px_0_0_rgb(255_70_90/0.35))_drop-shadow(-1px_0_0_rgb(70_150_255/0.35))]"
/>
</div>
{/* solid pass */}
<div
className="bust-solid seq absolute inset-0"
style={{ "--d": delay } as React.CSSProperties}
>
<Image
src={src}
alt="Profile picture"
fill
sizes="168px"
className="object-cover"
/>
</div>
{/* residual mesh that floats a few px above the photo */}
{/*<div
ref={meshRef}
aria-hidden="true"
className="absolute inset-0 opacity-[0.12] transition-transform duration-200 ease-out [background-image:repeating-linear-gradient(0deg,#f2f4f5_0_1px,transparent_1px_8px)] [mask-image:linear-gradient(90deg,#000,transparent_70%)]"
/>*/}
{/* scan line */}
<div
aria-hidden="true"
className="bust-scan seq absolute top-0 bottom-0 left-full w-px bg-accent opacity-0 shadow-[0_0_8px_var(--color-accent)]"
style={{ "--d": delay } as React.CSSProperties}
/>
</div>
</div>
);
}
-175
View File
@@ -1,175 +0,0 @@
/*
* CONTACT SECTION — selimaj.dev
* Design: Glassmorphic Terminal
* Clean contact links with glass cards
*/
import { useEffect, useRef } from "react";
import { Github, Youtube, Instagram, ExternalLink, Mail } from "lucide-react";
function useReveal(threshold = 0.1) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
el.classList.add("visible");
observer.disconnect();
}
},
{ threshold }
);
observer.observe(el);
return () => observer.disconnect();
}, [threshold]);
return ref;
}
const contactLinks = [
{
icon: Github,
label: "GitHub",
handle: "@selimaj-dev",
href: "https://github.com/selimaj-dev",
description: "35+ repositories · 2,670 contributions/year",
},
{
icon: Youtube,
label: "YouTube",
handle: "@SelimajDev",
href: "https://youtube.com/@SelimajDev",
description: "Dev content, tutorials, and builds",
},
{
icon: Instagram,
label: "Instagram",
handle: "@selimaj.dev",
href: "https://instagram.com/selimaj.dev",
description: "Behind the scenes and updates",
},
{
icon: ExternalLink,
label: "Website",
handle: "selimaj.dev",
href: "https://selimaj.dev",
description: "You're already here!",
},
];
export default function ContactSection() {
const headerRef = useReveal();
const contentRef = useReveal(0.1);
return (
<section id="contact" className="py-32 relative" style={{ background: "#080808" }}>
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
{/* Subtle radial glow */}
<div
className="absolute inset-0 pointer-events-none"
style={{
background: "radial-gradient(ellipse 60% 40% at 50% 100%, rgba(255,255,255,0.03) 0%, transparent 70%)",
}}
/>
<div className="max-w-6xl mx-auto px-6">
{/* Header */}
<div ref={headerRef} className="reveal mb-16 text-center">
<div className="terminal-label mb-4 text-center">&gt; contact --open</div>
<h2 className="section-heading text-4xl sm:text-5xl text-white leading-tight mb-6">
Let's Build
<br />
<span className="text-white/40">Something Together</span>
</h2>
<p
className="text-white/50 max-w-xl mx-auto text-base leading-relaxed"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
I'm always open to interesting projects, collaborations, or just a good conversation
about tech. Reach out through any of the channels below.
</p>
</div>
{/* Contact cards */}
<div ref={contentRef} className="reveal">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-12">
{contactLinks.map((link) => {
const Icon = link.icon;
return (
<a
key={link.label}
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="glass rounded-2xl p-6 card-hover group no-underline flex flex-col gap-4"
>
<div className="flex items-center justify-between">
<div className="w-10 h-10 glass-strong rounded-xl flex items-center justify-center">
<Icon size={18} className="text-white/70 group-hover:text-white transition-colors" />
</div>
<ExternalLink size={12} className="text-white/20 group-hover:text-white/50 transition-colors" />
</div>
<div>
<div
className="text-white font-semibold text-sm mb-0.5"
style={{ fontFamily: "'Syne', sans-serif" }}
>
{link.label}
</div>
<div className="terminal-label text-xs mb-2">{link.handle}</div>
<div
className="text-white/40 text-xs leading-relaxed"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
{link.description}
</div>
</div>
</a>
);
})}
</div>
{/* Big CTA */}
<div className="glass rounded-2xl p-10 text-center">
<div className="terminal-label mb-4">&gt; open_to_work --status</div>
<h3
className="text-2xl sm:text-3xl font-bold text-white mb-4"
style={{ fontFamily: "'Syne', sans-serif" }}
>
Open to freelance &amp; collaboration
</h3>
<p
className="text-white/50 mb-8 max-w-lg mx-auto text-sm leading-relaxed"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
Looking for interesting projects, SaaS co-founders, or clients who need
a developer who ships fast and cares about quality.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<a
href="https://github.com/selimaj-dev"
target="_blank"
rel="noopener noreferrer"
className="px-8 py-3.5 bg-white text-black font-semibold rounded-xl hover:bg-white/90 transition-all duration-200 text-sm no-underline"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
View GitHub Profile
</a>
<a
href="https://instagram.com/selimaj.dev"
target="_blank"
rel="noopener noreferrer"
className="px-8 py-3.5 glass border border-white/10 text-white/70 font-medium rounded-xl hover:text-white hover:border-white/20 transition-all duration-200 text-sm no-underline"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
DM on Instagram
</a>
</div>
</div>
</div>
</div>
</section>
);
}
-62
View File
@@ -1,62 +0,0 @@
import { cn } from "@/lib/utils";
import { AlertTriangle, RotateCcw } from "lucide-react";
import { Component, ReactNode } from "react";
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return (
<div className="flex items-center justify-center min-h-screen p-8 bg-background">
<div className="flex flex-col items-center w-full max-w-2xl p-8">
<AlertTriangle
size={48}
className="text-destructive mb-6 flex-shrink-0"
/>
<h2 className="text-xl mb-4">An unexpected error occurred.</h2>
<div className="p-4 w-full rounded bg-muted overflow-auto mb-6">
<pre className="text-sm text-muted-foreground whitespace-break-spaces">
{this.state.error?.stack}
</pre>
</div>
<button
onClick={() => window.location.reload()}
className={cn(
"flex items-center gap-2 px-4 py-2 rounded-lg",
"bg-primary text-primary-foreground",
"hover:opacity-90 cursor-pointer"
)}
>
<RotateCcw size={16} />
Reload Page
</button>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
+105
View File
@@ -0,0 +1,105 @@
"use client";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { brandOf, featured, featuredRoot, projectById, projects, root } from "@/content/projects";
import { Segment } from "./Segment";
export function FeaturedNodes() {
const [active, setActive] = useState<string | null>(null);
const nodes = featured.map((f) => ({ ...f, project: projectById(f.id)! }));
const stageRef = useRef<HTMLDivElement>(null);
const [size, setSize] = useState<{ w: number; h: number } | null>(null);
// Lines are drawn in px so they stay 1px crisp at any stage size.
useEffect(() => {
const el = stageRef.current;
if (!el) return;
const ro = new ResizeObserver(([e]) => setSize({ w: e.contentRect.width, h: e.contentRect.height }));
ro.observe(el);
return () => ro.disconnect();
}, []);
return (
<div
ref={stageRef}
className="bracket grid-bg relative mt-8 flex flex-col gap-1 border border-white/6 bg-surface p-6 [--bc:rgb(242_244_245/0.35)] md:block md:h-[460px] md:p-0"
onMouseLeave={() => setActive(null)}
>
{size && (
<div aria-hidden="true" className="hidden md:block">
{nodes.map((n) => (
<Segment
key={`${n.id}-${active === n.id}`}
x1={(featuredRoot.x / 100) * size.w}
y1={(featuredRoot.y / 100) * size.h}
x2={(n.x / 100) * size.w}
y2={(n.y / 100) * size.h}
on={active === n.id}
dashed
strong
/>
))}
</div>
)}
{/* Root: the site itself, labelled to the left so no line crosses it. */}
<div
className="relative min-h-12 md:absolute md:top-(--y) md:left-(--x) md:min-h-0"
style={{ "--x": `${featuredRoot.x}%`, "--y": `${featuredRoot.y}%` } as React.CSSProperties}
>
<span
aria-hidden="true"
className="absolute top-2.5 left-0 box-border size-7 rounded-full border border-ink/70 bg-night md:-top-3.5 md:-left-3.5"
>
<span className="absolute top-[9px] left-[9px] size-2 rounded-full bg-ink" />
</span>
<span className="ml-10 block px-3 py-2.5 md:absolute md:-top-[22px] md:right-6 md:ml-0 md:w-max md:text-right">
<span className="flex items-baseline gap-3 md:flex-row-reverse">
<span className="font-display text-lg font-semibold tracking-[0.08em] uppercase">{root.name}</span>
<span className="text-[9px] tracking-[0.22em] text-mute uppercase">Root</span>
</span>
</span>
</div>
{nodes.map((n) => {
const on = active === n.id;
const brand = brandOf(n.project);
const meta = n.id === "orus" ? "BRAND" : brand ? `${n.project.status} · ${brand.toUpperCase()}` : n.project.status;
return (
<Link
key={n.id}
href={`/projects/#${n.id}`}
onMouseEnter={() => setActive(n.id)}
onFocus={() => setActive(n.id)}
onBlur={() => setActive(null)}
className="group relative block min-h-12 outline-none md:absolute md:top-(--y) md:left-(--x) md:min-h-0"
style={{ "--x": `${n.x}%`, "--y": `${n.y}%` } as React.CSSProperties}
>
<span
aria-hidden="true"
className={`absolute top-4 left-2 box-border size-3 border md:-top-1.5 md:-left-1.5 ${n.id === "orus" ? "rotate-45" : ""} ${
on ? "border-accent bg-accent shadow-[0_0_14px_var(--color-accent)]" : "border-ink/60 bg-night"
}`}
/>
<span
className={`bracket ml-10 block min-h-11 px-3 py-2.5 group-focus-visible:outline group-focus-visible:outline-accent md:absolute md:-top-[22px] md:left-6 md:ml-0 md:w-[300px] ${
on ? "[--bc:var(--color-accent)]" : "[--bc:transparent]"
}`}
>
<span className="flex items-baseline gap-3">
<span className="font-display text-lg font-semibold tracking-[0.08em] uppercase">{n.project.name}</span>
<span className="text-[9px] tracking-[0.22em] text-mute">{meta}</span>
</span>
<span className={`mt-1.5 block text-[11px] leading-[1.55] text-ink-2 ${on ? "" : "md:sr-only"}`}>{n.project.desc}</span>
</span>
</Link>
);
})}
<div aria-hidden="true" className="absolute right-4 bottom-3 hidden text-[9px] tracking-[0.2em] text-mute/70 uppercase md:block">
Root {root.sub} · {featured.length} of {projects.length} nodes
</div>
</div>
);
}
-59
View File
@@ -1,59 +0,0 @@
/*
* FOOTER — selimaj.dev
* Design: Glassmorphic Terminal
* Minimal footer with branding and links
*/
export default function Footer() {
const year = new Date().getFullYear();
return (
<footer
className="py-10 relative"
style={{
background: "#080808",
borderTop: "1px solid rgba(255,255,255,0.06)",
}}
>
<div className="max-w-6xl mx-auto px-6">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
{/* Brand */}
<div className="flex items-center gap-3">
<span
className="text-white font-bold text-base tracking-tight"
style={{ fontFamily: "'Syne', sans-serif" }}
>
selimaj<span className="text-white/30">.dev</span>
</span>
<span className="text-white/20 text-xs">·</span>
<span className="terminal-label text-xs">Klesti Selimaj</span>
</div>
{/* Links */}
<div className="flex items-center gap-6">
{[
{ label: "GitHub", href: "https://github.com/selimaj-dev" },
{ label: "YouTube", href: "https://youtube.com/@SelimajDev" },
{ label: "Instagram", href: "https://instagram.com/selimaj.dev" },
].map((link) => (
<a
key={link.label}
href={link.href}
target="_blank"
rel="noopener noreferrer"
className="terminal-label text-xs hover:text-white/70 transition-colors"
>
{link.label}
</a>
))}
</div>
{/* Copyright */}
<div className="terminal-label text-xs">
© {year} selimaj.dev
</div>
</div>
</div>
</footer>
);
}
+232
View File
@@ -0,0 +1,232 @@
import Link from "next/link";
import {
dossierFields,
lock,
profile,
type DossierField,
} from "@/content/profile";
import { Bust } from "./Bust";
import { Scramble } from "./Scramble";
import { LocalTime, SessionStamp } from "./Telemetry";
import { TerrainLayer } from "./TerrainLayer";
import { LockReticle, TiranaMap } from "./TiranaMap";
const d = (s: number) => ({ "--d": `${s}s` }) as React.CSSProperties;
/** Dossier values decode one after another, starting once the card has built. */
const DOSSIER_START = 1.35;
const DOSSIER_STAGGER = 0.14;
function DossierList({
fields,
className = "",
}: {
fields: DossierField[];
className?: string;
}) {
return (
<dl
className={`grid grid-cols-[124px_minmax(0,1fr)] gap-y-2.5 border-t border-white/8 pt-[18px] text-xs leading-[1.45] xl:grid-cols-[116px_minmax(0,1fr)] ${className}`}
>
{fields.map((f, i) => {
const delay = DOSSIER_START + i * DOSSIER_STAGGER;
return (
<div key={f.label} className="contents">
<dt className="pt-[3px] text-[9px] uppercase tracking-[0.22em] text-mute">
{f.label}
</dt>
<dd className="flex flex-wrap gap-x-3.5">
{f.links ? (
f.links.map((l, j) => (
<a
key={l.href}
href={l.href}
className="border-b border-white/25 hover:border-accent"
>
<Scramble
text={l.label}
delay={delay + j * 0.2}
step={30}
/>
</a>
))
) : (
<Scramble text={f.value} delay={delay} step={30} />
)}
</dd>
</div>
);
})}
</dl>
);
}
export function Hero() {
return (
<section
aria-labelledby="hero-title"
className="relative overflow-hidden border-b border-line xl:flex xl:min-h-dvh xl:items-center"
>
<TerrainLayer />
<div className="relative z-10 mx-auto w-full max-w-[1440px] pt-[68px] md:pt-[84px] xl:h-[864px]">
{/* Lock assembly: a strip on small screens, the full stage on desktop. */}
<div
aria-hidden="true"
className="relative h-[240px] overflow-hidden border-b border-line [--lx:50%] xl:pointer-events-none xl:[--lx:45%] 2xl:[--lx:50%] xl:absolute xl:inset-0 xl:h-auto xl:overflow-visible xl:border-0"
>
<TiranaMap className="absolute top-[calc(50%-320px)] left-[calc(var(--lx)-320px)] size-[640px] xl:top-[-86px]" />
<LockReticle
className="reticle seq absolute top-[calc(50%-60px)] left-[calc(var(--lx)-60px)] max-xl:scale-75 xl:top-[174px]"
style={d(0.25)}
/>
<div className="absolute bottom-3 left-4 text-[9px] leading-[1.7] tracking-[0.14em] text-mute xl:bottom-auto xl:left-[calc(var(--lx)-60px)] xl:top-[306px] xl:text-[10px]">
<div className="text-accent">
<Scramble text="LOCK ACQUIRED" delay={0.85} step={35} nowrap />
</div>
<div>
<Scramble
text={`${lock.lat} · ${lock.lon}`}
delay={0.95}
step={30}
nowrap
/>
</div>
<div className="uppercase">
<Scramble text={lock.place} delay={1.05} step={30} nowrap />
</div>
</div>
<div
className="trace seq absolute top-[233px] right-[510px] left-[calc(var(--lx)+66px)] hidden h-px bg-accent/55 xl:block"
style={d(0.9)}
>
<span className="absolute -top-4 left-0 text-[9px] whitespace-nowrap tracking-[0.2em] text-accent">
TRACE ▸ 0001
</span>
<span className="absolute -top-[3px] -right-[3px] size-[7px] bg-accent" />
</div>
</div>
<div className="flex flex-col gap-9 px-4 pt-6 pb-16 md:px-16 xl:block xl:p-0">
{/* Headline */}
<div className="xl:absolute xl:top-[364px] xl:left-16 xl:w-[620px]">
<p className="mb-3 text-[9px] tracking-[0.28em] text-mute xl:mb-5 xl:text-[10px] xl:tracking-[0.3em]">
<Scramble
text="TARGET RESOLVED · SUBJECT 0001"
delay={0.2}
step={15}
/>
</p>
<h1
id="hero-title"
className="font-display text-[46px] leading-[0.96] font-semibold uppercase sm:text-[64px] xl:text-[84px] xl:leading-[0.94] xl:tracking-[0.01em]"
>
<Scramble text="ENTREPRENEUR" delay={0.3} step={35} nowrap />
<br />
<Scramble text="& BUILDER" delay={0.5} step={35} nowrap />
</h1>
{/*<p className="mt-4 text-[15px] leading-normal text-ink-2 xl:mt-6 xl:text-[17px]">
<Scramble text={profile.stance} delay={0.7} step={15} />
</p>*/}
<div
className="resolve seq mt-7 flex flex-col gap-2.5 sm:flex-row sm:flex-wrap sm:gap-3 xl:mt-10"
style={d(1.4)}
>
<Link
href="/projects/"
className="inline-flex min-h-12 items-center justify-between gap-3 bg-ink px-[22px] text-xs font-medium tracking-[0.2em] text-night transition-colors hover:bg-accent hover:text-night"
>
VIEW PROJECTS <span aria-hidden="true">→</span>
</Link>
<Link
href="/about/"
className="bracket inline-flex min-h-12 items-center justify-between gap-3 border border-white/12 px-[22px] text-xs tracking-[0.2em] transition-colors [--bc:rgb(242_244_245/0.6)] hover:border-transparent hover:text-accent hover:[--bc:var(--color-accent)]"
>
ABOUT ME <span aria-hidden="true">↗</span>
</Link>
</div>
<DossierList fields={dossierFields} className="mt-9 xl:hidden" />
</div>
{/* Profile card */}
<article
aria-labelledby="profile-name"
className="bracket panel seq order-first border border-white/7 bg-surface/90 p-4 [--bc:rgb(242_244_245/0.55)] xl:absolute xl:top-[124px] xl:right-16 xl:order-none xl:w-[446px] xl:p-[22px]"
style={d(1.0)}
>
<div className="flex justify-between text-[9px] tracking-[0.2em] text-mute xl:text-[10px]">
<span>PROFILE / 0001</span>
<span className="text-accent">● MATCH CONFIRMED</span>
</div>
<div className="mt-3.5 flex items-end gap-4 xl:mt-[18px] xl:items-start xl:gap-[22px]">
<figure className="bracket w-[112px] flex-none [--bc:rgb(242_244_245/0.4)] xl:w-[168px]">
<Bust src={profile.pfp} delay="1.2s" />
{profile.pfpIsPlaceholder && (
<figcaption className="pt-2 pb-0.5 text-[9px] tracking-[0.2em] text-mute max-xl:hidden">
PFP · PLACEHOLDER
</figcaption>
)}
</figure>
<div className="min-w-0 xl:pt-1">
<div className="text-[9px] tracking-[0.24em] text-mute">
ALIAS
</div>
<h2
id="profile-name"
className="mt-1.5 font-display text-[28px] leading-none font-semibold tracking-[0.06em] uppercase xl:text-4xl"
>
<Scramble
text={profile.displayName.toUpperCase()}
delay={1.2}
step={90}
nowrap
/>
</h2>
<div className="mt-2.5 text-[9px] leading-[1.9] tracking-[0.16em] text-mute xl:mt-[22px]">
<div className="max-xl:hidden">MESH 2.5D · RESOLVED</div>
<div className="max-xl:hidden">CLEARANCE · PUBLIC</div>
{profile.pfpIsPlaceholder && (
<div className="xl:hidden">PFP · PLACEHOLDER</div>
)}
</div>
</div>
</div>
<DossierList
fields={dossierFields}
className="mt-[22px] hidden xl:grid"
/>
</article>
</div>
</div>
<div className="pointer-events-none absolute inset-x-0 bottom-0 z-10 hidden xl:block">
<div className="relative mx-auto h-20 max-w-[1440px]">
{/* Corner telemetry, pinned to the bottom of the screen-tall hero */}
<div
aria-hidden="true"
className="appear seq absolute bottom-[22px] left-16 hidden text-[9px] leading-[1.8] tracking-[0.16em] text-mute/70 xl:block"
style={d(0.2)}
>
<SessionStamp />
</div>
<div
aria-hidden="true"
className="absolute bottom-[22px] left-1/2 hidden -translate-x-1/2 text-[9px] tracking-[0.3em] text-mute xl:block"
>
SCROLL ▾ NODES
</div>
<div
aria-hidden="true"
className="appear seq absolute right-16 bottom-[22px] hidden text-right text-[9px] leading-[1.8] tracking-[0.16em] xl:block"
style={d(0.2)}
>
<div className="text-accent">
<LocalTime />
</div>
<div className="text-mute/70">VISITOR · UNRESOLVED</div>
</div>
</div>
</div>
</section>
);
}
-167
View File
@@ -1,167 +0,0 @@
/*
* HERO SECTION — selimaj.dev
* Design: Glassmorphic Terminal
* Full-viewport dark hero with animated background image, typewriter effect, and glass CTA
*/
import { useEffect, useState } from "react";
const HERO_BG = "https://d2xsxph8kpxj0f.cloudfront.net/310519663439135967/RkRL4wd7YzNoLyaN7suWbR/hero-bg-G3393XyTzqxu3D4dR63od8.webp";
const typewriterPhrases = [
"Software Developer.",
"Entrepreneur.",
"SaaS Builder.",
"Systems Programmer.",
"Open Source Contributor.",
];
function useTypewriter(phrases: string[], speed = 60, pause = 1800) {
const [display, setDisplay] = useState("");
const [phraseIdx, setPhraseIdx] = useState(0);
const [charIdx, setCharIdx] = useState(0);
const [deleting, setDeleting] = useState(false);
useEffect(() => {
const current = phrases[phraseIdx];
let timeout: ReturnType<typeof setTimeout>;
if (!deleting && charIdx < current.length) {
timeout = setTimeout(() => setCharIdx((c) => c + 1), speed);
} else if (!deleting && charIdx === current.length) {
timeout = setTimeout(() => setDeleting(true), pause);
} else if (deleting && charIdx > 0) {
timeout = setTimeout(() => setCharIdx((c) => c - 1), speed / 2);
} else if (deleting && charIdx === 0) {
setDeleting(false);
setPhraseIdx((i) => (i + 1) % phrases.length);
}
setDisplay(current.slice(0, charIdx));
return () => clearTimeout(timeout);
}, [charIdx, deleting, phraseIdx, phrases, speed, pause]);
return display;
}
export default function HeroSection() {
const typed = useTypewriter(typewriterPhrases);
return (
<section
id="hero"
className="relative min-h-screen flex items-center justify-center overflow-hidden"
style={{ background: "#080808" }}
>
{/* Background image */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url(${HERO_BG})`,
opacity: 0.55,
}}
/>
{/* Gradient overlays */}
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-[#080808]" />
<div className="absolute inset-0 bg-gradient-to-r from-[#080808]/60 via-transparent to-[#080808]/60" />
{/* Animated glow orb */}
<div
className="absolute top-1/3 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] rounded-full pointer-events-none"
style={{
background: "radial-gradient(circle, rgba(255,255,255,0.04) 0%, transparent 70%)",
animation: "float 6s ease-in-out infinite",
}}
/>
{/* Content */}
<div className="relative z-10 max-w-5xl mx-auto px-6 text-center">
{/* Terminal label */}
<div className="inline-flex items-center gap-2 mb-8 px-4 py-2 glass rounded-full">
<span className="w-2 h-2 rounded-full bg-white/60 inline-block" />
<span className="terminal-label text-xs">
&gt; klesti@selimaj.dev ~ $
</span>
</div>
{/* Main heading */}
<h1
className="text-5xl sm:text-6xl md:text-7xl lg:text-8xl font-bold text-white mb-6 leading-none tracking-tight fade-in-up"
style={{ fontFamily: "'Syne', sans-serif" }}
>
Klesti
<br />
<span className="text-white/30">Selimaj</span>
</h1>
{/* Typewriter subtitle */}
<div
className="text-xl sm:text-2xl md:text-3xl font-medium text-white/70 mb-4 h-10 fade-in-up delay-200"
style={{ fontFamily: "'Syne', sans-serif" }}
>
{typed}
<span className="cursor-blink text-white/50 ml-0.5">|</span>
</div>
{/* Description */}
<p
className="text-base sm:text-lg text-white/45 max-w-2xl mx-auto mb-12 leading-relaxed fade-in-up delay-400"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
16 years old · 6 years of experience · Albania
<br />
Passionate about low-level systems, hacking, UI/UX, and making cool tools.
</p>
{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 fade-in-up delay-500">
<button
onClick={() => {
document.querySelector("#projects")?.scrollIntoView({ behavior: "smooth" });
}}
className="px-8 py-3.5 bg-white text-black font-semibold rounded-xl hover:bg-white/90 transition-all duration-200 text-sm"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
View Projects
</button>
<button
onClick={() => {
document.querySelector("#contact")?.scrollIntoView({ behavior: "smooth" });
}}
className="px-8 py-3.5 glass text-white/80 font-medium rounded-xl hover:text-white hover:border-white/20 transition-all duration-200 text-sm border border-white/10"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
Get in Touch
</button>
</div>
{/* Stats row */}
<div className="mt-20 flex flex-col sm:flex-row items-center justify-center gap-6 sm:gap-12 fade-in-up delay-700">
{[
{ value: "6+", label: "Years Experience" },
{ value: "35+", label: "Repositories" },
{ value: "2,670+", label: "Contributions / Year" },
{ value: "#2", label: "National GitHub user" },
].map((stat) => (
<div key={stat.label} className="text-center">
<div
className="text-3xl font-bold text-white"
style={{ fontFamily: "'Syne', sans-serif" }}
>
{stat.value}
</div>
<div className="terminal-label mt-1">{stat.label}</div>
</div>
))}
</div>
</div>
{/* Scroll indicator */}
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2 opacity-0 fade-in-up delay-800" style={{animationFillMode:'forwards'}}>
<div className="w-px h-8 bg-gradient-to-b from-white/30 to-transparent" />
<span className="terminal-label text-xs">scroll</span>
</div>
</section>
);
}
-124
View File
@@ -1,124 +0,0 @@
/*
* NAVBAR — selimaj.dev
* Design: Glassmorphic Terminal
* Glass effect: backdrop-blur + rgba background
* Becomes more opaque on scroll
*/
import { useEffect, useState } from "react";
const navLinks = [
{ label: "About", href: "#about" },
{ label: "Projects", href: "#projects" },
{ label: "Skills", href: "#skills" },
{ label: "Contact", href: "#contact" },
];
export default function Navbar() {
const [scrolled, setScrolled] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 40);
};
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const handleNavClick = (href: string) => {
setMenuOpen(false);
const el = document.querySelector(href);
if (el) {
el.scrollIntoView({ behavior: "smooth" });
}
};
return (
<nav
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-500 ${
scrolled ? "navbar-glass-solid" : "navbar-glass"
}`}
>
<div className="max-w-6xl mx-auto px-6 flex items-center justify-between h-16">
{/* Logo */}
<a
href="#"
onClick={(e) => { e.preventDefault(); window.scrollTo({ top: 0, behavior: "smooth" }); }}
className="flex items-center gap-2 group"
>
<span
className="text-white font-bold text-lg tracking-tight"
style={{ fontFamily: "'Syne', sans-serif" }}
>
selimaj<span className="text-white/40">.dev</span>
</span>
</a>
{/* Desktop nav links */}
<div className="hidden md:flex items-center gap-1">
{navLinks.map((link) => (
<button
key={link.href}
onClick={() => handleNavClick(link.href)}
className="px-4 py-2 text-sm text-white/60 hover:text-white transition-colors duration-200 rounded-lg hover:bg-white/5"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
{link.label}
</button>
))}
<a
href="https://github.com/selimaj-dev"
target="_blank"
rel="noopener noreferrer"
className="ml-3 px-4 py-2 text-sm font-medium text-black bg-white rounded-lg hover:bg-white/90 transition-all duration-200"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
GitHub
</a>
</div>
{/* Mobile hamburger */}
<button
className="md:hidden flex flex-col gap-1.5 p-2"
onClick={() => setMenuOpen(!menuOpen)}
aria-label="Toggle menu"
>
<span className={`block w-5 h-0.5 bg-white/70 transition-all duration-300 ${menuOpen ? "rotate-45 translate-y-2" : ""}`} />
<span className={`block w-5 h-0.5 bg-white/70 transition-all duration-300 ${menuOpen ? "opacity-0" : ""}`} />
<span className={`block w-5 h-0.5 bg-white/70 transition-all duration-300 ${menuOpen ? "-rotate-45 -translate-y-2" : ""}`} />
</button>
</div>
{/* Mobile menu */}
<div
className={`md:hidden overflow-hidden transition-all duration-300 ${
menuOpen ? "max-h-64 opacity-100" : "max-h-0 opacity-0"
}`}
style={{ background: "rgba(8,8,8,0.95)", borderTop: "1px solid rgba(255,255,255,0.06)" }}
>
<div className="px-6 py-4 flex flex-col gap-1">
{navLinks.map((link) => (
<button
key={link.href}
onClick={() => handleNavClick(link.href)}
className="text-left px-3 py-3 text-sm text-white/60 hover:text-white transition-colors rounded-lg hover:bg-white/5"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
{link.label}
</button>
))}
<a
href="https://github.com/selimaj-dev"
target="_blank"
rel="noopener noreferrer"
className="mt-2 px-3 py-3 text-sm font-medium text-black bg-white rounded-lg text-center hover:bg-white/90 transition-all"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
GitHub
</a>
</div>
</div>
</nav>
);
}
+282
View File
@@ -0,0 +1,282 @@
"use client";
import { useEffect, useState } from "react";
import { brandOf, groups, projects, root, type GroupId, type Project } from "@/content/projects";
import { Segment } from "./Segment";
import { StatusTag } from "./StatusTag";
const W = 1200;
const H = 720;
const groupIds = Object.keys(groups) as GroupId[];
const isHub = (p: Project) => groups[p.group].node === p.id;
function Marker({ p, on }: { p: Project; on: boolean }) {
const hub = isHub(p);
const size = hub ? 22 : 14;
const rest =
p.status === "CONCEPT" ? "border border-dashed border-ink/60" : p.status === "ARCHIVED" ? "border border-ink/30" : "border border-ink/65";
return (
<span
aria-hidden="true"
className={`relative box-border flex-none ${hub ? "rotate-45" : ""} ${
on ? "bg-accent shadow-[0_0_12px_var(--color-accent)]" : `${rest} bg-surface`
}`}
style={{ width: size, height: size }}
/>
);
}
function ProjectCard({ p }: { p: Project }) {
const brand = brandOf(p);
const body = (
<>
<span className="flex items-center justify-between gap-3">
<span className="flex items-center gap-3">
<StatusTag status={p.status} />
{brand && <span className="text-[9px] tracking-[0.22em] text-mute uppercase">{brand}</span>}
</span>
<span aria-hidden="true" className="text-mute transition-colors group-hover:text-accent group-focus-visible:text-accent">
{p.href ? "↗" : "→"}
</span>
</span>
<span className="font-display text-[22px] font-semibold tracking-[0.04em] uppercase">{p.name}</span>
<span className="flex-1 text-xs leading-[1.65] text-ink-2">{p.desc}</span>
<span className="flex flex-wrap gap-1.5">
{p.tags.map((t) => (
<span key={t} className="bg-surface-2 px-[7px] py-[3px] text-[10px] text-mute">
{t}
</span>
))}
</span>
</>
);
const cls =
"group bracket flex min-h-[200px] scroll-mt-24 flex-col gap-3 border border-white/6 bg-surface p-[22px] [--bc:rgb(242_244_245/0.3)] hover:[--bc:var(--color-accent)] focus-visible:[--bc:var(--color-accent)]";
return p.href ? (
<a id={p.id} href={p.href} className={cls}>
{body}
</a>
) : (
<article id={p.id} className={cls}>
{body}
</article>
);
}
export function ProjectIndex() {
const [view, setView] = useState<"map" | "list">("map");
const [active, setActive] = useState<string | null>(null);
// Deep links (/projects/#enclave) select that node; small screens start on the list.
useEffect(() => {
const id = decodeURIComponent(location.hash.slice(1));
const small = matchMedia("(max-width: 767px)").matches;
if (projects.some((p) => p.id === id) || small) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- syncing with browser-only state once on mount
if (projects.some((p) => p.id === id)) setActive(id);
if (small) setView("list");
}
}, []);
const sel = projects.find((p) => p.id === active) ?? null;
const count = (s: Project["status"]) => String(projects.filter((p) => p.status === s).length).padStart(2, "0");
return (
<>
<div className="mt-8 flex flex-wrap items-end justify-between gap-6">
<ul aria-label="Status legend" className="flex flex-wrap gap-[18px] text-[9px] tracking-[0.2em] text-mute">
<li className="flex items-center gap-1.5">
<span aria-hidden="true" className="size-[5px] rounded-full bg-accent" />
LIVE {count("LIVE")}
</li>
<li>BUILDING {count("BUILDING")}</li>
<li>CONCEPT {count("CONCEPT")}</li>
<li>ARCHIVED {count("ARCHIVED")}</li>
</ul>
<div role="group" aria-label="View" className="hidden border border-white/12 js:flex">
{(["map", "list"] as const).map((v) => (
<button
key={v}
type="button"
aria-pressed={view === v}
onClick={() => setView(v)}
className={`min-h-11 min-w-[76px] border-b px-4 text-[11px] tracking-[0.22em] uppercase ${
view === v ? "border-accent bg-surface-2 text-ink" : "border-transparent text-mute hover:text-ink"
}`}
>
{v}
</button>
))}
</div>
</div>
{/* MAP: needs JS to be useful, so it is hidden without it. */}
<div className={`hidden ${view === "map" ? "js:block" : ""}`}>
<div className="mt-6 overflow-x-auto border border-white/6">
<div className="grid-bg relative mx-auto bg-surface [background-size:40px_40px]" style={{ width: W, height: H }}>
{groupIds.map((k) => (
<Segment key={k} x1={root.map.x} y1={root.map.y} x2={groups[k].x} y2={groups[k].y} on={!!sel && sel.group === k} strong />
))}
{projects
.filter((p) => !isHub(p))
.map((p) => (
<Segment
key={`${p.id}-${active === p.id}`}
x1={groups[p.group].x}
y1={groups[p.group].y}
x2={p.map.x}
y2={p.map.y}
on={active === p.id}
dashed={p.status === "CONCEPT"}
/>
))}
{/* Plain hubs (groups without a project of their own). */}
{groupIds
.filter((k) => !groups[k].node)
.map((k) => {
const g = groups[k];
return (
<div key={k} aria-hidden="true">
<div
className="absolute text-[9px] whitespace-nowrap tracking-[0.24em] text-mute uppercase"
style={{ top: g.y - 7, ...(g.side === "l" ? { right: W - g.x + 14 } : { left: g.x + 14 }) }}
>
{g.label}
</div>
<div className="absolute box-border size-1.5 rotate-45 border border-ink/40" style={{ left: g.x - 3, top: g.y - 3 }} />
</div>
);
})}
{/* Root: the site itself. */}
<div className="absolute flex items-center gap-3 whitespace-nowrap" style={{ left: root.map.x - 15, top: root.map.y - 15 }}>
<span aria-hidden="true" className="relative box-border size-[30px] flex-none rounded-full border border-ink/70 bg-surface">
<span className="absolute top-[10px] left-[10px] size-2 rounded-full bg-ink" />
</span>
<span className="flex flex-col gap-[3px]">
<span className="font-display text-xl font-semibold tracking-[0.08em] uppercase">{root.name}</span>
<span className="text-[8px] tracking-[0.22em] text-mute uppercase">Root · {root.sub}</span>
</span>
</div>
{projects.map((p) => {
const on = active === p.id;
const g = groups[p.group];
const labelLeft = (p.label ?? (p.map.x < g.x ? "l" : "r")) === "l";
const m = isHub(p) ? 22 : 14;
const pad = 8;
return (
<button
key={p.id}
type="button"
aria-pressed={on}
onClick={() => setActive(p.id)}
onMouseEnter={() => setActive(p.id)}
onFocus={() => setActive(p.id)}
className={`bracket absolute flex min-h-12 items-center gap-2.5 whitespace-nowrap focus-visible:outline-none ${
on ? "[--bc:var(--color-accent)]" : "[--bc:transparent] hover:[--bc:var(--color-accent)]"
} ${labelLeft ? "flex-row-reverse text-right" : ""}`}
style={{
top: p.map.y - 24,
padding: `0 ${pad}px`,
...(labelLeft ? { right: W - p.map.x - m / 2 - pad } : { left: p.map.x - m / 2 - pad }),
}}
>
<Marker p={p} on={on} />
{/* A hub's label sits a little high so its root line passes underneath. */}
<span className={`flex flex-col gap-[3px] ${isHub(p) ? "-translate-y-4" : ""}`}>
<span
className={`font-display font-semibold tracking-[0.08em] uppercase ${isHub(p) ? "text-base" : "text-[13px]"} ${
p.status === "ARCHIVED" && !on ? "text-ink/70" : "text-ink"
}`}
>
{p.name}
</span>
<span className="text-[8px] tracking-[0.22em] text-mute">{isHub(p) ? "BRAND" : p.status}</span>
</span>
</button>
);
})}
<div aria-hidden="true" className="absolute bottom-3 left-4 text-[9px] tracking-[0.2em] text-mute/70">
GRID 40 · ROOT {root.map.x},{root.map.y}
</div>
<div aria-hidden="true" className="absolute right-4 bottom-3 text-[9px] tracking-[0.2em] uppercase">
{sel ? <span className="text-accent">TRACKING ▸ {sel.name}</span> : <span className="text-mute/70">AWAITING TARGET</span>}
</div>
</div>
</div>
<section
aria-live="polite"
aria-label="Selected node"
className={`bracket mt-4 grid min-h-[176px] gap-6 border border-white/6 bg-surface px-7 py-6 md:grid-cols-[minmax(0,1fr)_auto] md:items-center md:gap-x-12 ${
sel ? "[--bc:var(--color-accent)]" : "[--bc:rgb(242_244_245/0.3)]"
}`}
>
{sel ? (
<>
<div>
<div className="flex flex-wrap items-center gap-3.5">
<StatusTag status={sel.status} />
<span className="text-[9px] tracking-[0.24em] text-mute uppercase">
{isHub(sel) ? "Brand" : (brandOf(sel) ?? groups[sel.group].label)}
</span>
</div>
<h2 className="mt-3 font-display text-3xl font-semibold tracking-[0.04em] uppercase">{sel.name}</h2>
<p className="mt-2 max-w-[720px] text-[13px] leading-[1.7] text-ink-2">{sel.desc}</p>
<div className="mt-3.5 flex flex-wrap gap-2">
{sel.tags.map((t) => (
<span key={t} className="bg-surface-2 px-2 py-1 text-[10px] tracking-[0.1em] text-mute">
{t}
</span>
))}
</div>
</div>
{sel.href ? (
<a href={sel.href} className="inline-flex min-h-12 items-center gap-3 justify-self-start border border-white/14 px-5 text-[11px] tracking-[0.2em] hover:border-accent">
VISIT <span aria-hidden="true">↗</span>
</a>
) : (
<button
type="button"
onClick={() => {
setView("list");
requestAnimationFrame(() => document.getElementById(sel.id)?.scrollIntoView({ block: "center" }));
}}
className="inline-flex min-h-12 items-center gap-3 justify-self-start border border-white/14 px-5 text-[11px] tracking-[0.2em] hover:border-accent hover:text-accent"
>
VIEW IN LIST <span aria-hidden="true">→</span>
</button>
)}
</>
) : (
<p className="text-[11px] tracking-[0.2em] text-mute">HOVER OR SELECT A NODE TO TRACK IT.</p>
)}
</section>
</div>
{/* LIST: always rendered; hidden only when JS is on and the map is chosen. */}
<div className={`mt-10 flex flex-col gap-14 ${view === "map" ? "js:hidden" : ""}`}>
{groupIds.map((k) => {
const items = projects.filter((p) => p.group === k);
return (
<section key={k} aria-labelledby={`g-${k}`}>
<div className="flex items-baseline gap-4 border-b border-white/8 pb-3.5">
<h2 id={`g-${k}`} className="font-display text-xl font-semibold tracking-[0.14em] uppercase">
{groups[k].label}
</h2>
<span className="text-[9px] tracking-[0.22em] text-mute">{String(items.length).padStart(2, "0")}</span>
</div>
<div className="mt-5 grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-4">
{items.map((p) => (
<ProjectCard key={p.id} p={p} />
))}
</div>
</section>
);
})}
</div>
</>
);
}
-282
View File
@@ -1,282 +0,0 @@
/*
* PROJECTS SECTION — selimaj.dev
* Design: Glassmorphic Terminal
* Masonry-style grid with glass cards, language badges, and star counts
*/
import { useEffect, useRef } from "react";
import { ExternalLink, Star, GitFork } from "lucide-react";
function useReveal(threshold = 0.1) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
el.classList.add("visible");
observer.disconnect();
}
},
{ threshold }
);
observer.observe(el);
return () => observer.disconnect();
}, [threshold]);
return ref;
}
const LANG_COLORS: Record<string, string> = {
TypeScript: "#3178c6",
Rust: "#ce422b",
Java: "#b07219",
JavaScript: "#f1e05a",
Python: "#3572A5",
Shell: "#89e051",
"C#": "#178600",
};
interface Project {
name: string;
org?: string;
description: string;
language: string;
stars: number;
forks?: number;
href: string;
featured?: boolean;
tag?: string;
}
const projects: Project[] = [
{
name: "osui",
org: "osui-rs",
description: "A TUI library written in Rust — build terminal UIs with a component-based approach.",
language: "Rust",
stars: 7,
href: "https://github.com/osui-rs/osui",
featured: true,
tag: "Library",
},
{
name: "saturnclient",
org: "saturnclientmc",
description: "Saturn Client — a Minecraft client full of customizability and free cosmetics.",
language: "Java",
stars: 7,
forks: 2,
href: "https://github.com/saturnclientmc/saturnclient",
featured: true,
tag: "Game Client",
},
{
name: "dev-run",
org: "orus-dev",
description: "DevRun — a community-driven competitive programming platform inspired by speedrunning.",
language: "TypeScript",
stars: 3,
href: "https://github.com/orus-dev/dev-run",
featured: true,
tag: "Platform",
},
{
name: "linear-linux",
description: "Linux support for linear.app — bringing the popular project management tool to Linux.",
language: "JavaScript",
stars: 27,
forks: 5,
href: "https://github.com/selimaj-dev/linear-linux",
featured: true,
tag: "Tool",
},
{
name: "dockship",
org: "orus-dev",
description: "A powerful self-hosted deployment platform for managing your applications.",
language: "TypeScript",
stars: 1,
href: "https://github.com/orus-dev/dockship",
tag: "DevOps",
},
{
name: "axiom",
org: "AxiomChat",
description: "A modern real-time chat application built with TypeScript.",
language: "TypeScript",
stars: 1,
href: "https://github.com/AxiomChat/axiom",
tag: "App",
},
{
name: "nexa-trade",
org: "nexa-sol",
description: "The Solana trading terminal supercharged by AI — trade smarter.",
language: "TypeScript",
stars: 1,
href: "https://github.com/nexa-sol/nexa-trade",
tag: "FinTech",
},
{
name: "osmium",
description: "A TUI component-based game engine built on top of OSUI.",
language: "Rust",
stars: 1,
href: "https://github.com/selimaj-dev/osmium",
tag: "Engine",
},
{
name: "spark",
description: "AI documentation generator that takes context from your repository's source code.",
language: "Python",
stars: 2,
href: "https://github.com/selimaj-dev/spark",
tag: "AI Tool",
},
{
name: "flux",
description: "A tool and library for using multiple languages in one codebase seamlessly.",
language: "Rust",
stars: 1,
href: "https://github.com/selimaj-dev/flux",
tag: "Tool",
},
{
name: "session-rs",
description: "A lightweight async WebSocket protocol library for Rust.",
language: "Rust",
stars: 1,
href: "https://github.com/selimaj-dev/session-rs",
tag: "Library",
},
{
name: "tailwind-motlib",
description: "A motion and stagger animation library for Tailwind CSS.",
language: "TypeScript",
stars: 0,
href: "https://github.com/selimaj-dev/tailwind-motlib",
tag: "Library",
},
];
function ProjectCard({ project, delay }: { project: Project; delay: number }) {
const langColor = LANG_COLORS[project.language] || "#888";
return (
<a
href={project.href}
target="_blank"
rel="noopener noreferrer"
className="block glass rounded-2xl p-6 card-hover group no-underline"
style={{ animationDelay: `${delay}ms` }}
>
{/* Header */}
<div className="flex items-start justify-between mb-3">
<div className="flex-1 min-w-0">
{project.tag && (
<span className="terminal-label text-xs inline-block mb-2 px-2 py-0.5 rounded-full border border-white/10 bg-white/5">
{project.tag}
</span>
)}
<h3
className="text-white font-semibold text-base leading-tight truncate"
style={{ fontFamily: "'Syne', sans-serif" }}
>
{project.org ? (
<span className="text-white/40">{project.org}/</span>
) : null}
{project.name}
</h3>
</div>
<ExternalLink
size={14}
className="text-white/20 group-hover:text-white/60 transition-colors ml-3 mt-1 shrink-0"
/>
</div>
{/* Description */}
<p
className="text-white/50 text-sm leading-relaxed mb-5 line-clamp-2"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
{project.description}
</p>
{/* Footer */}
<div className="flex items-center gap-4">
<span className="lang-badge">
<span
className="lang-dot"
style={{ backgroundColor: langColor }}
/>
{project.language}
</span>
<span className="lang-badge">
<Star size={11} className="text-white/40" />
{project.stars}
</span>
{project.forks !== undefined && (
<span className="lang-badge">
<GitFork size={11} className="text-white/40" />
{project.forks}
</span>
)}
</div>
</a>
);
}
export default function ProjectsSection() {
const headerRef = useReveal();
const gridRef = useReveal(0.05);
const featured = projects.filter((p) => p.featured);
const rest = projects.filter((p) => !p.featured);
return (
<section id="projects" className="py-32 relative" style={{ background: "#080808" }}>
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-6xl mx-auto px-6">
{/* Header */}
<div ref={headerRef} className="reveal mb-16">
<div className="terminal-label mb-4">&gt; ls projects/</div>
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4">
<h2 className="section-heading text-4xl sm:text-5xl text-white leading-tight">
Things I've
<br />
<span className="text-white/40">Built</span>
</h2>
<a
href="https://github.com/selimaj-dev?tab=repositories"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-white/40 hover:text-white transition-colors terminal-label"
>
View all 35+ repos →
</a>
</div>
</div>
{/* Featured projects (larger cards) */}
<div ref={gridRef} className="reveal">
<div className="terminal-label mb-4 text-xs">// featured</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
{featured.map((project, i) => (
<ProjectCard key={project.name} project={project} delay={i * 80} />
))}
</div>
{/* Rest of projects (smaller grid) */}
<div className="terminal-label mb-4 text-xs mt-8">// more projects</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
{rest.map((project, i) => (
<ProjectCard key={project.name} project={project} delay={i * 60} />
))}
</div>
</div>
</div>
</section>
);
}
+110
View File
@@ -0,0 +1,110 @@
"use client";
import { useEffect, useState } from "react";
const UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&@$*+=<>/";
const LOWER = "abcdefghijklmnopqrstuvwxyz0123456789#%&@$*+=<>/";
const DIGIT = "0123456789#%&@$*";
/** Characters that stay put while everything around them is brute-forced. */
const FIXED = /[\s.,·()\-/]/;
const FRAME_MS = 45;
function noise(ch: string) {
if (FIXED.test(ch)) return ch;
const pool = /[0-9]/.test(ch) ? DIGIT : ch === ch.toLowerCase() && ch !== ch.toUpperCase() ? LOWER : UPPER;
return pool[Math.floor(Math.random() * pool.length)];
}
type Frame = { done: string; head: string; rest: string };
/**
* Text that brute-forces itself: every character cycles through random
* glyphs, then the real value locks in from left to right.
*
* Server and no-JS output is the plain text. With reduced motion it never
* animates. While the boot screen is up it waits for `boot:done`.
*/
export function Scramble({
text,
delay = 0,
step = 45,
nowrap = false,
}: {
text: string;
/** Seconds before characters start locking in (noise runs from the start). */
delay?: number;
/** Milliseconds per character as the value locks in. */
step?: number;
nowrap?: boolean;
}) {
const [frame, setFrame] = useState<Frame | null>(null);
useEffect(() => {
if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const chars = Array.from(text);
let raf = 0;
let start = 0;
let last = -Infinity;
const build = (locked: number): Frame => ({
done: chars.slice(0, locked).join(""),
head: locked < chars.length ? chars[locked] : "",
rest: chars
.slice(locked + 1)
.map(noise)
.join(""),
});
const loop = (t: number) => {
if (!start) start = t;
const elapsed = t - start - delay * 1000;
const locked = elapsed < 0 ? 0 : Math.floor(elapsed / step);
if (locked >= chars.length) {
setFrame(null);
return;
}
if (t - last >= FRAME_MS) {
last = t;
// Before the reveal starts, the first character is noise too.
setFrame(elapsed < 0 ? { done: "", head: noise(chars[0]), rest: build(0).rest } : build(locked));
}
raf = requestAnimationFrame(loop);
};
const begin = () => {
raf = requestAnimationFrame(loop);
};
if (document.documentElement.hasAttribute("data-boot")) {
// Hide the real value under the boot screen, then start when it lifts.
raf = requestAnimationFrame(() => setFrame({ done: "", head: "", rest: chars.map(noise).join("") }));
window.addEventListener("boot:done", begin, { once: true });
} else {
begin();
}
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("boot:done", begin);
};
}, [text, delay, step]);
if (!frame) return <>{text}</>;
return (
<span className="relative inline-block max-w-full align-top">
<span className="sr-only">{text}</span>
{/* Invisible copy keeps the box exactly the size of the real text. */}
<span aria-hidden="true" className={`invisible ${nowrap ? "whitespace-nowrap" : ""}`}>
{text}
</span>
<span aria-hidden="true" className={`absolute inset-0 ${nowrap ? "whitespace-nowrap" : ""}`}>
{frame.done}
<span className="text-accent">{frame.head}</span>
<span className="opacity-50">{frame.rest}</span>
</span>
</span>
);
}
+35
View File
@@ -0,0 +1,35 @@
/** A 1px tracking line between two points, in px, inside a positioned parent. */
export function Segment({
x1,
y1,
x2,
y2,
on,
dashed,
strong,
}: {
x1: number;
y1: number;
x2: number;
y2: number;
on: boolean;
dashed?: boolean;
strong?: boolean;
}) {
const len = Math.hypot(x2 - x1, y2 - y1);
const ang = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI;
const c = on ? "var(--color-accent)" : strong ? "rgb(242 244 245 / 0.2)" : "rgb(242 244 245 / 0.11)";
return (
<div
aria-hidden="true"
className={`pointer-events-none absolute h-px origin-[0_50%] ${on ? "seg-on shadow-[0_0_6px_var(--color-accent)]" : ""}`}
style={{
left: x1,
top: y1,
width: len,
transform: `rotate(${ang}deg)`,
background: dashed && !on ? `repeating-linear-gradient(90deg, ${c} 0 4px, transparent 4px 9px)` : c,
}}
/>
);
}
+18
View File
@@ -0,0 +1,18 @@
import { profile } from "@/content/profile";
export function SiteFooter() {
return (
<footer className="border-t border-line">
<div className="mx-auto flex max-w-[1440px] flex-wrap justify-between gap-6 px-4 pt-7 pb-10 text-[10px] tracking-[0.18em] text-mute md:px-16">
<span>SELIMAJ.DEV · TIRANA, AL</span>
<span className="flex gap-6">
<a href={profile.github} className="hover:text-accent">GITHUB</a>
<a href={profile.git} className="hover:text-accent">GIT.SELIMAJ.DEV</a>
</span>
<span aria-hidden="true">
41.33 N · 19.82 E · <span className="blink text-accent">▌</span>
</span>
</div>
</footer>
);
}
+98
View File
@@ -0,0 +1,98 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useState } from "react";
import { SessionClock } from "./Telemetry";
const links = [
{ href: "/", label: "Home" },
{ href: "/about/", label: "About" },
{ href: "/projects/", label: "Projects" },
];
const isCurrent = (pathname: string, href: string) =>
href === "/"
? pathname === "/"
: pathname.startsWith(href.replace(/\/$/, ""));
export function SiteHeader() {
const pathname = usePathname();
const [open, setOpen] = useState(false);
const isHome = pathname === "/";
return (
<header
className={`absolute inset-x-0 top-0 z-30 ${isHome ? "" : "border-b border-line"}`}
>
<div className="mx-auto flex max-w-[1440px] items-center justify-between gap-6 px-4 py-3 md:px-16 md:py-[22px]">
<Link
href="/"
className="flex min-h-11 items-center gap-3"
aria-label="selimaj.dev home"
>
<span className="font-display text-sm font-semibold tracking-[0.24em]">
SELIMAJ<span className="text-mute">.DEV</span>
</span>
</Link>
<button
type="button"
className="flex min-h-11 items-center gap-2.5 px-1 text-[10px] tracking-[0.22em] md:hidden"
aria-expanded={open}
aria-controls="site-nav"
onClick={() => setOpen((o) => !o)}
>
{open ? "CLOSE" : "MENU"}
<svg
width="18"
height="12"
viewBox="0 0 18 12"
fill="none"
stroke="currentColor"
aria-hidden="true"
>
<path d="M0,1 H18 M6,6 H18 M0,11 H18" />
</svg>
</button>
<nav
id="site-nav"
aria-label="Primary"
className={`${open ? "block" : "hidden"} absolute inset-x-0 top-full border-y border-line bg-night/95 px-4 backdrop-blur md:static md:block md:border-0 md:bg-transparent md:p-0 md:backdrop-blur-none`}
>
<ul className="flex flex-col text-[11px] tracking-[0.2em] md:flex-row md:gap-9">
{links.map((l) => {
const current = isCurrent(pathname, l.href);
return (
<li key={l.href}>
<Link
href={l.href}
aria-current={current ? "page" : undefined}
onClick={() => setOpen(false)}
className={`flex min-h-11 items-center gap-2 uppercase md:py-3.5 ${current ? "text-ink" : "text-mute hover:text-accent"}`}
>
{current && (
<span
aria-hidden="true"
className="size-1.5 bg-accent shadow-[0_0_8px_var(--color-accent)]"
/>
)}
{l.label}
</Link>
</li>
);
})}
</ul>
</nav>
<div
aria-hidden="true"
className="hidden text-right text-[10px] leading-relaxed tracking-[0.14em] text-mute lg:block"
>
<SessionClock />
</div>
</div>
</header>
);
}
-248
View File
@@ -1,248 +0,0 @@
/*
* SKILLS SECTION — selimaj.dev
* Design: Glassmorphic Terminal
* Flowing chip layout with category grouping
*/
import {
CpuIcon,
PaletteIcon,
RocketIcon,
ShieldCheckIcon,
TerminalIcon,
ZapIcon,
} from "lucide-react";
import { useEffect, useRef } from "react";
function useReveal(threshold = 0.1) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
el.classList.add("visible");
observer.disconnect();
}
},
{ threshold },
);
observer.observe(el);
return () => observer.disconnect();
}, [threshold]);
return ref;
}
const CODE_VISUAL =
"https://d2xsxph8kpxj0f.cloudfront.net/310519663439135967/RkRL4wd7YzNoLyaN7suWbR/code-visual-96fdFEYLcFy2ct3UAAmiG6.webp";
interface SkillCategory {
label: string;
terminalLabel: string;
skills: { name: string; level?: "expert" | "proficient" | "learning" }[];
}
const skillCategories: SkillCategory[] = [
{
label: "Languages",
terminalLabel: "languages/",
skills: [
{ name: "Rust", level: "expert" },
{ name: "TypeScript", level: "expert" },
{ name: "JavaScript", level: "expert" },
{ name: "Java", level: "proficient" },
{ name: "Python", level: "proficient" },
{ name: "C#", level: "proficient" },
{ name: "Shell / Bash", level: "proficient" },
{ name: "Go", level: "learning" },
],
},
{
label: "Frontend",
terminalLabel: "frontend/",
skills: [
{ name: "React", level: "expert" },
{ name: "Next.js", level: "expert" },
{ name: "Tailwind CSS", level: "expert" },
{ name: "Framer Motion", level: "proficient" },
{ name: "WebSockets", level: "expert" },
{ name: "Vite", level: "expert" },
],
},
{
label: "Backend & Systems",
terminalLabel: "backend/",
skills: [
{ name: "Node.js", level: "expert" },
{ name: "Express", level: "expert" },
{ name: "Linux", level: "expert" },
{ name: "Docker", level: "proficient" },
{ name: "TUI / CLI", level: "expert" },
{ name: "Low-level Systems", level: "expert" },
{ name: "WebAssembly", level: "proficient" },
],
},
{
label: "Tools & Platforms",
terminalLabel: "tools/",
skills: [
{ name: "Git / GitHub", level: "expert" },
{ name: "Kali Linux", level: "proficient" },
{ name: "Neovim", level: "proficient" },
{ name: "PostgreSQL", level: "proficient" },
{ name: "Redis", level: "proficient" },
{ name: "Figma", level: "proficient" },
{ name: "Solana / Web3", level: "proficient" },
],
},
];
const levelColors: Record<string, string> = {
expert: "rgba(255,255,255,0.12)",
proficient: "rgba(255,255,255,0.06)",
learning: "rgba(255,255,255,0.03)",
};
const levelBorders: Record<string, string> = {
expert: "rgba(255,255,255,0.2)",
proficient: "rgba(255,255,255,0.1)",
learning: "rgba(255,255,255,0.06)",
};
export default function SkillsSection() {
const headerRef = useReveal();
const contentRef = useReveal(0.05);
return (
<section
id="skills"
className="py-32 relative"
style={{ background: "#080808" }}
>
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<div className="max-w-6xl mx-auto px-6">
{/* Header */}
<div ref={headerRef} className="reveal mb-16">
<div className="terminal-label mb-4">&gt; cat skills.json</div>
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-4">
<h2 className="section-heading text-4xl sm:text-5xl text-white leading-tight">
Tech I Work <span className="text-white/40">With</span>
</h2>
<div className="flex items-center gap-4 terminal-label text-xs">
<span className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full inline-block bg-white/50" />
Expert
</span>
<span className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full inline-block bg-white/25" />
Proficient
</span>
<span className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full inline-block bg-white/10" />
Learning
</span>
</div>
</div>
</div>
<div ref={contentRef} className="reveal">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: skill categories */}
<div className="lg:col-span-2 space-y-8">
{skillCategories.map((cat) => (
<div key={cat.label}>
<div className="flex items-center gap-3 mb-4">
<span className="terminal-label text-xs">
{cat.terminalLabel}
</span>
<div className="flex-1 h-px bg-white/5" />
</div>
<div className="flex flex-wrap gap-2">
{cat.skills.map((skill) => (
<span
key={skill.name}
className="skill-chip"
style={{
background: skill.level
? levelColors[skill.level]
: undefined,
borderColor: skill.level
? levelBorders[skill.level]
: undefined,
}}
>
{skill.name}
</span>
))}
</div>
</div>
))}
</div>
{/* Right: code visual + interests */}
<div className="space-y-4">
{/* Code visual */}
<div className="glass rounded-2xl overflow-hidden">
<img
src={CODE_VISUAL}
alt="Code visualization"
className="w-full h-48 object-cover opacity-70"
/>
<div className="p-4">
<div className="terminal-label text-xs mb-1">
$ git log --oneline
</div>
<div
className="text-white/50 text-xs leading-relaxed"
style={{ fontFamily: "'JetBrains Mono', monospace" }}
>
<div>214 commits this month</div>
<div>2,670 contributions/year</div>
<div>35+ repositories</div>
</div>
</div>
</div>
{/* Interests */}
<div className="glass rounded-2xl p-5">
<div className="terminal-label text-xs mb-4">
&gt; interests/
</div>
<div className="space-y-3">
{[
{
icon: <ShieldCheckIcon size={20} />,
label: "Hacking & Security",
},
{ icon: <CpuIcon size={20} />, label: "Low-level Systems" },
{ icon: <PaletteIcon size={20} />, label: "UI/UX Design" },
{
icon: <RocketIcon size={20} />,
label: "SaaS & Startups",
},
{ icon: <ZapIcon size={20} />, label: "Rust Ecosystem" },
{
icon: <TerminalIcon size={20} />,
label: "Linux & Open Source",
},
].map((item) => (
<div
key={item.label}
className="flex items-center gap-2 text-sm text-white/60"
style={{ fontFamily: "'DM Sans', sans-serif" }}
>
<span className="text-base">{item.icon}</span>
{item.label}
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
+17
View File
@@ -0,0 +1,17 @@
import type { Status } from "@/content/projects";
const styles: Record<Status, string> = {
LIVE: "text-ink-2 border border-white/30",
BUILDING: "text-ink-2 border border-white/30",
CONCEPT: "text-ink-2 border border-dashed border-white/40",
ARCHIVED: "text-mute border border-white/12",
};
export function StatusTag({ status }: { status: Status }) {
return (
<span className={`inline-flex items-center gap-1.5 px-2 py-1 text-[9px] tracking-[0.2em] ${styles[status]}`}>
{status === "LIVE" && <span aria-hidden="true" className="size-[5px] rounded-full bg-accent" />}
{status}
</span>
);
}
+90
View File
@@ -0,0 +1,90 @@
"use client";
import { useSyncExternalStore } from "react";
// Real data: when this session opened, a random per-session hash, and the
// visitor's local clock. All of it is client-only and never leaves the page.
type Session = { start: number; hash: string };
let session: Session | null = null;
function getSession(): Session {
if (session) return session;
try {
const raw = sessionStorage.getItem("session");
if (raw) return (session = JSON.parse(raw) as Session);
} catch {}
const bytes = crypto.getRandomValues(new Uint8Array(8));
const hash = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
session = { start: Date.now(), hash };
try {
sessionStorage.setItem("session", JSON.stringify(session));
} catch {}
return session;
}
const noop = () => () => {};
const tick = (cb: () => void) => {
const id = setInterval(cb, 1000);
return () => clearInterval(id);
};
const nowSeconds = () => Math.floor(Date.now() / 1000);
export function useSession() {
return useSyncExternalStore(noop, getSession, () => null);
}
export function useNow() {
const s = useSyncExternalStore(tick, nowSeconds, () => null);
return s === null ? null : new Date(s * 1000);
}
const p = (n: number) => String(n).padStart(2, "0");
export function formatTime(d: Date) {
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}
export function formatOffset(d: Date) {
const off = -d.getTimezoneOffset();
const a = Math.abs(off);
return `UTC${off >= 0 ? "+" : "-"}${p(Math.floor(a / 60))}:${p(a % 60)}`;
}
function formatElapsed(ms: number) {
const s = Math.max(0, Math.floor(ms / 1000));
return `${p(Math.floor(s / 60))}:${p(s % 60)}`;
}
export function SessionClock() {
const s = useSession();
const now = useNow();
const id = s ? `${s.hash.slice(0, 4)}·${s.hash.slice(4, 8)}`.toUpperCase() : "····";
return (
<>
<div>SESSION {id}</div>
<div>
<span className="text-accent">● LIVE</span> T+{s && now ? formatElapsed(now.getTime() - s.start) : "--:--"}
</div>
</>
);
}
export function SessionStamp() {
const s = useSession();
return (
<>
<div>SESSION OPEN · {s ? new Date(s.start).toISOString().slice(0, 19) + "Z" : "…"}</div>
<div>HASH {s ? `${s.hash.slice(0, 8)}·${s.hash.slice(8)}` : "…"} · RES CITY-SCALE</div>
</>
);
}
export function LocalTime({ prefix = "LOCAL" }: { prefix?: string }) {
const now = useNow();
return (
<>
{prefix} {now ? `${formatTime(now)} ${formatOffset(now)}` : "--:--:--"}
</>
);
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Hero background, bottom to top: terrain contours (real Dajt elevation data),
* grid, scanlines, vignette. Purely decorative and deliberately static.
*/
export function TerrainLayer({ className = "" }: { className?: string }) {
return (
<div aria-hidden="true" className={`pointer-events-none absolute inset-0 overflow-hidden ${className}`}>
{/* eslint-disable-next-line @next/next/no-img-element -- decorative SVG, no optimisation needed */}
<img
src="/terrain.svg"
alt=""
className="absolute -inset-10 h-[calc(100%+80px)] w-[calc(100%+80px)] max-w-none object-cover opacity-20 max-xl:object-[60%_50%]"
/>
<div className="grid-bg absolute inset-0" />
<div className="scan-bg absolute inset-0" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_55%_35%,transparent_35%,rgb(0_0_0/0.75)_100%)]" />
</div>
);
}
+59
View File
@@ -0,0 +1,59 @@
/**
* Stylised Tirana city centre, drawn by hand at city scale only.
* Centred on Skanderbeg Square. No street-level detail beyond that.
*/
export function TiranaMap({ className = "" }: { className?: string }) {
return (
<svg viewBox="0 0 640 640" aria-hidden="true" className={className}>
<defs>
<radialGradient id="map-fade">
<stop offset="0.45" stopColor="#fff" />
<stop offset="1" stopColor="#000" />
</radialGradient>
<mask id="map-mask">
<rect width="640" height="640" fill="url(#map-fade)" />
</mask>
</defs>
<g mask="url(#map-mask)" fill="none" stroke="#fff" strokeWidth="1" className="draw">
<circle pathLength={1} cx="320" cy="320" r="300" strokeOpacity="0.08" style={{ "--d": "0.1s" } as React.CSSProperties} />
<path pathLength={1} strokeOpacity="0.14" d="M150,250 C170,170 250,130 330,135 C420,140 495,190 505,280 C515,370 470,450 380,480 C290,505 190,480 155,400 C135,350 138,300 150,250Z" />
<path pathLength={1} strokeOpacity="0.18" d="M320,640 L320,354" />
<path pathLength={1} strokeOpacity="0.18" d="M320,286 L310,0" />
<path pathLength={1} strokeOpacity="0.13" d="M286,316 L0,262" />
<path pathLength={1} strokeOpacity="0.13" d="M354,322 L640,352" />
<path pathLength={1} strokeOpacity="0.08" d="M296,296 L120,120" style={{ "--d": "0.2s" } as React.CSSProperties} />
<path pathLength={1} strokeOpacity="0.08" d="M346,344 L560,560" style={{ "--d": "0.2s" } as React.CSSProperties} />
<path pathLength={1} strokeOpacity="0.08" d="M346,296 L540,110" style={{ "--d": "0.2s" } as React.CSSProperties} />
<path pathLength={1} strokeOpacity="0.08" d="M294,346 L90,560" style={{ "--d": "0.2s" } as React.CSSProperties} />
<path pathLength={1} strokeOpacity="0.14" d="M0,470 C110,452 200,492 320,482 C440,472 530,500 640,490" />
<path pathLength={1} strokeOpacity="0.14" d="M0,478 C110,460 200,500 320,490 C440,480 530,508 640,498" />
<g strokeOpacity="0.07" style={{ "--d": "0.35s" } as React.CSSProperties}>
{[
[360, 200, 60, 44], [432, 212, 46, 56], [360, 380, 70, 40], [208, 372, 64, 50], [200, 212, 56, 40],
[442, 380, 44, 60], [232, 150, 40, 34], [380, 150, 54, 30], [236, 268, 40, 30], [364, 262, 34, 34],
].map(([x, y, w, h]) => (
<rect key={`${x}-${y}`} pathLength={1} x={x} y={y} width={w} height={h} style={{ "--d": "0.35s" } as React.CSSProperties} />
))}
</g>
<circle pathLength={1} cx="320" cy="320" r="34" strokeOpacity="0.24" style={{ "--d": "0.4s" } as React.CSSProperties} />
<circle pathLength={1} cx="320" cy="320" r="16" strokeOpacity="0.12" style={{ "--d": "0.45s" } as React.CSSProperties} />
</g>
<text x="362" y="352" fill="#8b9296" fillOpacity="0.7" fontSize="9" letterSpacing="1.6" fontFamily="var(--font-mono)">
SKANDERBEG SQ.
</text>
</svg>
);
}
export function LockReticle({ className = "", style }: { className?: string; style?: React.CSSProperties }) {
return (
<div aria-hidden="true" className={`bracket size-[120px] [--bc:var(--color-accent)] [--bl:16px] ${className}`} style={style}>
<svg viewBox="0 0 120 120" width="120" height="120" fill="none" stroke="var(--color-accent)">
<circle className="spin" cx="60" cy="60" r="34" strokeDasharray="3 5" strokeOpacity="0.8" />
<circle cx="60" cy="60" r="20" strokeOpacity="0.35" />
<path d="M60,14 L60,30 M60,90 L60,106 M14,60 L30,60 M90,60 L106,60" />
<circle cx="60" cy="60" r="2.5" fill="var(--color-accent)" />
</svg>
</div>
);
}
-64
View File
@@ -1,64 +0,0 @@
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Accordion({
...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
}
function AccordionItem({
className,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b last:border-b-0", className)}
{...props}
/>
);
}
function AccordionTrigger({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
function AccordionContent({
className,
children,
...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
{...props}
>
<div className={cn("pt-0 pb-4", className)}>{children}</div>
</AccordionPrimitive.Content>
);
}
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
-155
View File
@@ -1,155 +0,0 @@
import * as React from "react";
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
);
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
);
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
);
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
);
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
);
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
);
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
);
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
);
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};
-66
View File
@@ -1,66 +0,0 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
},
},
defaultVariants: {
variant: "default",
},
}
);
function Alert({
className,
variant,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
return (
<div
data-slot="alert"
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
);
}
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-title"
className={cn(
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
className
)}
{...props}
/>
);
}
function AlertDescription({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...props}
/>
);
}
export { Alert, AlertTitle, AlertDescription };
-9
View File
@@ -1,9 +0,0 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
function AspectRatio({
...props
}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
}
export { AspectRatio };
-51
View File
@@ -1,51 +0,0 @@
import * as React from "react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@/lib/utils";
function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
);
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
);
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
);
}
export { Avatar, AvatarImage, AvatarFallback };
-46
View File
@@ -1,46 +0,0 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
);
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span";
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
);
}
export { Badge, badgeVariants };
-109
View File
@@ -1,109 +0,0 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import { cn } from "@/lib/utils";
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
className
)}
{...props}
/>
);
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
);
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "a";
return (
<Comp
data-slot="breadcrumb-link"
className={cn("hover:text-foreground transition-colors", className)}
{...props}
/>
);
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("text-foreground font-normal", className)}
{...props}
/>
);
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
);
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};
-83
View File
@@ -1,83 +0,0 @@
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
const buttonGroupVariants = cva(
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
{
variants: {
orientation: {
horizontal:
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
vertical:
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
},
},
defaultVariants: {
orientation: "horizontal",
},
}
);
function ButtonGroup({
className,
orientation,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
return (
<div
role="group"
data-slot="button-group"
data-orientation={orientation}
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
);
}
function ButtonGroupText({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "div";
return (
<Comp
className={cn(
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
);
}
function ButtonGroupSeparator({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="button-group-separator"
orientation={orientation}
className={cn(
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
className
)}
{...props}
/>
);
}
export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
buttonGroupVariants,
};
-60
View File
@@ -1,60 +0,0 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-transparent shadow-xs hover:bg-accent dark:bg-transparent dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };
-211
View File
@@ -1,211 +0,0 @@
import * as React from "react";
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react";
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
import { cn } from "@/lib/utils";
import { Button, buttonVariants } from "@/components/ui/button";
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"];
}) {
const defaultClassNames = getDefaultClassNames();
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: date =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months
),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
defaultClassNames.button_next
),
month_caption: cn(
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
defaultClassNames.month_caption
),
dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root
),
dropdown: cn(
"absolute bg-popover inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
defaultClassNames.weekday
),
week: cn("flex w-full mt-2", defaultClassNames.week),
week_number_header: cn(
"select-none w-(--cell-size)",
defaultClassNames.week_number_header
),
week_number: cn(
"text-[0.8rem] select-none text-muted-foreground",
defaultClassNames.week_number
),
day: cn(
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
defaultClassNames.day
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
);
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
);
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
);
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
);
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
);
},
...components,
}}
{...props}
/>
);
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames();
const ref = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus();
}, [modifiers.focused]);
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
);
}
export { Calendar, CalendarDayButton };
-92
View File
@@ -1,92 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};
-239
View File
@@ -1,239 +0,0 @@
import * as React from "react";
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
type CarouselApi = UseEmblaCarouselType[1];
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
type CarouselOptions = UseCarouselParameters[0];
type CarouselPlugin = UseCarouselParameters[1];
type CarouselProps = {
opts?: CarouselOptions;
plugins?: CarouselPlugin;
orientation?: "horizontal" | "vertical";
setApi?: (api: CarouselApi) => void;
};
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
api: ReturnType<typeof useEmblaCarousel>[1];
scrollPrev: () => void;
scrollNext: () => void;
canScrollPrev: boolean;
canScrollNext: boolean;
} & CarouselProps;
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
function useCarousel() {
const context = React.useContext(CarouselContext);
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />");
}
return context;
}
function Carousel({
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
}: React.ComponentProps<"div"> & CarouselProps) {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
);
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
const [canScrollNext, setCanScrollNext] = React.useState(false);
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) return;
setCanScrollPrev(api.canScrollPrev());
setCanScrollNext(api.canScrollNext());
}, []);
const scrollPrev = React.useCallback(() => {
api?.scrollPrev();
}, [api]);
const scrollNext = React.useCallback(() => {
api?.scrollNext();
}, [api]);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault();
scrollPrev();
} else if (event.key === "ArrowRight") {
event.preventDefault();
scrollNext();
}
},
[scrollPrev, scrollNext]
);
React.useEffect(() => {
if (!api || !setApi) return;
setApi(api);
}, [api, setApi]);
React.useEffect(() => {
if (!api) return;
onSelect(api);
api.on("reInit", onSelect);
api.on("select", onSelect);
return () => {
api?.off("select", onSelect);
};
}, [api, onSelect]);
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
data-slot="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
);
}
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
const { carouselRef, orientation } = useCarousel();
return (
<div
ref={carouselRef}
className="overflow-hidden"
data-slot="carousel-content"
>
<div
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
);
}
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
const { orientation } = useCarousel();
return (
<div
role="group"
aria-roledescription="slide"
data-slot="carousel-item"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
);
}
function CarouselPrevious({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
return (
<Button
data-slot="carousel-previous"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -left-12 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft />
<span className="sr-only">Previous slide</span>
</Button>
);
}
function CarouselNext({
className,
variant = "outline",
size = "icon",
...props
}: React.ComponentProps<typeof Button>) {
const { orientation, scrollNext, canScrollNext } = useCarousel();
return (
<Button
data-slot="carousel-next"
variant={variant}
size={size}
className={cn(
"absolute size-8 rounded-full",
orientation === "horizontal"
? "top-1/2 -right-12 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight />
<span className="sr-only">Next slide</span>
</Button>
);
}
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
};
-355
View File
@@ -1,355 +0,0 @@
import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils";
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const;
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
);
};
type ChartContextProps = {
config: ChartConfig;
};
const ChartContext = React.createContext<ChartContextProps | null>(null);
function useChart() {
const context = React.useContext(ChartContext);
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />");
}
return context;
}
function ChartContainer({
id,
className,
children,
config,
...props
}: React.ComponentProps<"div"> & {
config: ChartConfig;
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"];
}) {
const uniqueId = React.useId();
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
);
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color
);
if (!colorConfig.length) {
return null;
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color;
return color ? ` --color-${key}: ${color};` : null;
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
);
};
const ChartTooltip = RechartsPrimitive.Tooltip;
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}) {
const { config } = useChart();
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null;
}
const [item] = payload;
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label;
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
);
}
if (!value) {
return null;
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
]);
if (!active || !payload?.length) {
return null;
}
const nestLabel = payload.length === 1 && indicator !== "dot";
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload
.filter(item => item.type !== "none")
.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
}
const ChartLegend = RechartsPrimitive.Legend;
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
}) {
const { config } = useChart();
if (!payload?.length) {
return null;
}
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload
.filter(item => item.type !== "none")
.map(item => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
);
})}
</div>
);
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined;
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined;
let configLabelKey: string = key;
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string;
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string;
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config];
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
};
-30
View File
@@ -1,30 +0,0 @@
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };
-31
View File
@@ -1,31 +0,0 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
);
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
);
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
-184
View File
@@ -1,184 +0,0 @@
"use client";
import * as React from "react";
import { Command as CommandPrimitive } from "cmdk";
import { SearchIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...props}
/>
);
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string;
description?: string;
className?: string;
showCloseButton?: boolean;
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent
className={cn("overflow-hidden p-0", className)}
showCloseButton={showCloseButton}
>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
);
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props}
/>
);
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
);
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
);
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
);
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
);
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
);
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};
-250
View File
@@ -1,250 +0,0 @@
import * as React from "react";
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function ContextMenu({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
}
function ContextMenuTrigger({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
return (
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
);
}
function ContextMenuGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
return (
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
);
}
function ContextMenuPortal({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
return (
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
);
}
function ContextMenuSub({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
}
function ContextMenuRadioGroup({
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
return (
<ContextMenuPrimitive.RadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
);
}
function ContextMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto" />
</ContextMenuPrimitive.SubTrigger>
);
}
function ContextMenuSubContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
return (
<ContextMenuPrimitive.SubContent
data-slot="context-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
);
}
function ContextMenuContent({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
return (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
data-slot="context-menu-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
);
}
function ContextMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<ContextMenuPrimitive.Item
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
);
}
function ContextMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
return (
<ContextMenuPrimitive.CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
);
}
function ContextMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
return (
<ContextMenuPrimitive.RadioItem
data-slot="context-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
);
}
function ContextMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<ContextMenuPrimitive.Label
data-slot="context-menu-label"
data-inset={inset}
className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
);
}
function ContextMenuSeparator({
className,
...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
return (
<ContextMenuPrimitive.Separator
data-slot="context-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function ContextMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="context-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
);
}
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
};
-209
View File
@@ -1,209 +0,0 @@
import { cn } from "@/lib/utils";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import * as React from "react";
// Context to track composition state across dialog children
const DialogCompositionContext = React.createContext<{
isComposing: () => boolean;
setComposing: (composing: boolean) => void;
justEndedComposing: () => boolean;
markCompositionEnd: () => void;
}>({
isComposing: () => false,
setComposing: () => {},
justEndedComposing: () => false,
markCompositionEnd: () => {},
});
export const useDialogComposition = () =>
React.useContext(DialogCompositionContext);
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
const composingRef = React.useRef(false);
const justEndedRef = React.useRef(false);
const endTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
const contextValue = React.useMemo(
() => ({
isComposing: () => composingRef.current,
setComposing: (composing: boolean) => {
composingRef.current = composing;
},
justEndedComposing: () => justEndedRef.current,
markCompositionEnd: () => {
justEndedRef.current = true;
if (endTimerRef.current) {
clearTimeout(endTimerRef.current);
}
endTimerRef.current = setTimeout(() => {
justEndedRef.current = false;
}, 150);
},
}),
[]
);
return (
<DialogCompositionContext.Provider value={contextValue}>
<DialogPrimitive.Root data-slot="dialog" {...props} />
</DialogCompositionContext.Provider>
);
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
);
}
DialogOverlay.displayName = "DialogOverlay";
function DialogContent({
className,
children,
showCloseButton = true,
onEscapeKeyDown,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
}) {
const { isComposing } = useDialogComposition();
const handleEscapeKeyDown = React.useCallback(
(e: KeyboardEvent) => {
// Check both the native isComposing property and our context state
// This handles Safari's timing issues with composition events
const isCurrentlyComposing = (e as any).isComposing || isComposing();
// If IME is composing, prevent dialog from closing
if (isCurrentlyComposing) {
e.preventDefault();
return;
}
// Call user's onEscapeKeyDown if provided
onEscapeKeyDown?.(e);
},
[isComposing, onEscapeKeyDown]
);
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
onEscapeKeyDown={handleEscapeKeyDown}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
);
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
);
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
);
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger
};
-133
View File
@@ -1,133 +0,0 @@
import * as React from "react";
import { Drawer as DrawerPrimitive } from "vaul";
import { cn } from "@/lib/utils";
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
}
function DrawerTrigger({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
}
function DrawerPortal({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
}
function DrawerClose({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
}
function DrawerOverlay({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
return (
<DrawerPrimitive.Overlay
data-slot="drawer-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
);
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn(
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
className
)}
{...props}
>
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
);
}
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-header"
className={cn(
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
className
)}
{...props}
/>
);
}
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="drawer-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function DrawerTitle({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return (
<DrawerPrimitive.Title
data-slot="drawer-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
};
-255
View File
@@ -1,255 +0,0 @@
import * as React from "react";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
);
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
);
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
);
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
);
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
);
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
};
-104
View File
@@ -1,104 +0,0 @@
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
function Empty({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty"
className={cn(
"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12",
className
)}
{...props}
/>
);
}
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-header"
className={cn(
"flex max-w-sm flex-col items-center gap-2 text-center",
className
)}
{...props}
/>
);
}
const emptyMediaVariants = cva(
"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-transparent",
icon: "bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6",
},
},
defaultVariants: {
variant: "default",
},
}
);
function EmptyMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof emptyMediaVariants>) {
return (
<div
data-slot="empty-icon"
data-variant={variant}
className={cn(emptyMediaVariants({ variant, className }))}
{...props}
/>
);
}
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-title"
className={cn("text-lg font-medium tracking-tight", className)}
{...props}
/>
);
}
function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<div
data-slot="empty-description"
className={cn(
"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...props}
/>
);
}
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="empty-content"
className={cn(
"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance",
className
)}
{...props}
/>
);
}
export {
Empty,
EmptyHeader,
EmptyTitle,
EmptyDescription,
EmptyContent,
EmptyMedia,
};
-242
View File
@@ -1,242 +0,0 @@
import { useMemo } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
<fieldset
data-slot="field-set"
className={cn(
"flex flex-col gap-6",
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
className,
)}
{...props}
/>
);
}
function FieldLegend({
className,
variant = "legend",
...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
return (
<legend
data-slot="field-legend"
data-variant={variant}
className={cn(
"mb-3 font-medium",
"data-[variant=legend]:text-base",
"data-[variant=label]:text-sm",
className,
)}
{...props}
/>
);
}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-group"
className={cn(
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
className,
)}
{...props}
/>
);
}
const fieldVariants = cva(
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
{
variants: {
orientation: {
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
horizontal: [
"flex-row items-center",
"[&>[data-slot=field-label]]:flex-auto",
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
responsive: [
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
},
},
defaultVariants: {
orientation: "vertical",
},
},
);
function Field({
className,
orientation = "vertical",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
return (
<div
role="group"
data-slot="field"
data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
);
}
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-content"
className={cn(
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
className,
)}
{...props}
/>
);
}
function FieldLabel({
className,
...props
}: React.ComponentProps<typeof Label>) {
return (
<Label
data-slot="field-label"
className={cn(
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
className,
)}
{...props}
/>
);
}
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-label"
className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
className,
)}
{...props}
/>
);
}
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="field-description"
className={cn(
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className,
)}
{...props}
/>
);
}
function FieldSeparator({
children,
className,
...props
}: React.ComponentProps<"div"> & {
children?: React.ReactNode;
}) {
return (
<div
data-slot="field-separator"
data-content={!!children}
className={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
className,
)}
{...props}
>
<Separator className="absolute inset-0 top-1/2" />
{children && (
<span
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
data-slot="field-separator-content"
>
{children}
</span>
)}
</div>
);
}
function FieldError({
className,
children,
errors,
...props
}: React.ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined>;
}) {
const content = useMemo(() => {
if (children) {
return children;
}
if (!errors) {
return null;
}
if (errors?.length === 1 && errors[0]?.message) {
return errors[0].message;
}
return (
<ul className="ml-4 flex list-disc flex-col gap-1">
{errors.map(
(error, index) =>
error?.message && <li key={index}>{error.message}</li>,
)}
</ul>
);
}, [children, errors]);
if (!content) {
return null;
}
return (
<div
role="alert"
data-slot="field-error"
className={cn("text-destructive text-sm font-normal", className)}
{...props}
>
{content}
</div>
);
}
export {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSeparator,
FieldSet,
FieldContent,
FieldTitle,
};
-168
View File
@@ -1,168 +0,0 @@
"use client";
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form";
import { cn } from "@/lib/utils";
import { Label } from "@/components/ui/label";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
);
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
);
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField();
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField();
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message ?? "") : props.children;
if (!body) {
return null;
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
);
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
};
-42
View File
@@ -1,42 +0,0 @@
import * as React from "react";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import { cn } from "@/lib/utils";
function HoverCard({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
}
function HoverCardTrigger({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
);
}
function HoverCardContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
<HoverCardPrimitive.Content
data-slot="hover-card-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</HoverCardPrimitive.Portal>
);
}
export { HoverCard, HoverCardTrigger, HoverCardContent };
-168
View File
@@ -1,168 +0,0 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
"h-9 min-w-0 has-[>textarea]:h-auto",
// Variants based on alignment.
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
// Focus state.
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
// Error state.
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
className
)}
{...props}
/>
);
}
const inputGroupAddonVariants = cva(
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
{
variants: {
align: {
"inline-start":
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
"inline-end":
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
"block-start":
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
"block-end":
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
},
},
defaultVariants: {
align: "inline-start",
},
}
);
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={e => {
if ((e.target as HTMLElement).closest("button")) {
return;
}
e.currentTarget.parentElement?.querySelector("input")?.focus();
}}
{...props}
/>
);
}
const inputGroupButtonVariants = cva(
"text-sm shadow-none flex gap-2 items-center",
{
variants: {
size: {
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
}
);
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size"> &
VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
);
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
);
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
className
)}
{...props}
/>
);
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
className
)}
{...props}
/>
);
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
};
-75
View File
@@ -1,75 +0,0 @@
import * as React from "react";
import { OTPInput, OTPInputContext } from "input-otp";
import { MinusIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function InputOTP({
className,
containerClassName,
...props
}: React.ComponentProps<typeof OTPInput> & {
containerClassName?: string;
}) {
return (
<OTPInput
data-slot="input-otp"
containerClassName={cn(
"flex items-center gap-2 has-disabled:opacity-50",
containerClassName
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
);
}
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-otp-group"
className={cn("flex items-center", className)}
{...props}
/>
);
}
function InputOTPSlot({
index,
className,
...props
}: React.ComponentProps<"div"> & {
index: number;
}) {
const inputOTPContext = React.useContext(OTPInputContext);
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
return (
<div
data-slot="input-otp-slot"
data-active={isActive}
className={cn(
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
className
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
</div>
)}
</div>
);
}
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
return (
<div data-slot="input-otp-separator" role="separator" {...props}>
<MinusIcon />
</div>
);
}
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
-70
View File
@@ -1,70 +0,0 @@
import { useDialogComposition } from "@/components/ui/dialog";
import { useComposition } from "@/hooks/useComposition";
import { cn } from "@/lib/utils";
import * as React from "react";
function Input({
className,
type,
onKeyDown,
onCompositionStart,
onCompositionEnd,
...props
}: React.ComponentProps<"input">) {
// Get dialog composition context if available (will be no-op if not inside Dialog)
const dialogComposition = useDialogComposition();
// Add composition event handlers to support input method editor (IME) for CJK languages.
const {
onCompositionStart: handleCompositionStart,
onCompositionEnd: handleCompositionEnd,
onKeyDown: handleKeyDown,
} = useComposition<HTMLInputElement>({
onKeyDown: (e) => {
// Check if this is an Enter key that should be blocked
const isComposing = (e.nativeEvent as any).isComposing || dialogComposition.justEndedComposing();
// If Enter key is pressed while composing or just after composition ended,
// don't call the user's onKeyDown (this blocks the business logic)
if (e.key === "Enter" && isComposing) {
return;
}
// Otherwise, call the user's onKeyDown
onKeyDown?.(e);
},
onCompositionStart: e => {
dialogComposition.setComposing(true);
onCompositionStart?.(e);
},
onCompositionEnd: e => {
// Mark that composition just ended - this helps handle the Enter key that confirms input
dialogComposition.markCompositionEnd();
// Delay setting composing to false to handle Safari's event order
// In Safari, compositionEnd fires before the ESC keydown event
setTimeout(() => {
dialogComposition.setComposing(false);
}, 100);
onCompositionEnd?.(e);
},
});
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onKeyDown={handleKeyDown}
{...props}
/>
);
}
export { Input };
-193
View File
@@ -1,193 +0,0 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
role="list"
data-slot="item-group"
className={cn("group/item-group flex flex-col", className)}
{...props}
/>
);
}
function ItemSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="item-separator"
orientation="horizontal"
className={cn("my-0", className)}
{...props}
/>
);
}
const itemVariants = cva(
"group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border-border",
muted: "bg-muted/50",
},
size: {
default: "p-4 gap-4 ",
sm: "py-3 px-4 gap-2.5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
function Item({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"div"> &
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div";
return (
<Comp
data-slot="item"
data-variant={variant}
data-size={size}
className={cn(itemVariants({ variant, size, className }))}
{...props}
/>
);
}
const itemMediaVariants = cva(
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
{
variants: {
variant: {
default: "bg-transparent",
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
image:
"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
},
},
defaultVariants: {
variant: "default",
},
}
);
function ItemMedia({
className,
variant = "default",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
return (
<div
data-slot="item-media"
data-variant={variant}
className={cn(itemMediaVariants({ variant, className }))}
{...props}
/>
);
}
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-content"
className={cn(
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
className
)}
{...props}
/>
);
}
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-title"
className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium",
className
)}
{...props}
/>
);
}
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="item-description"
className={cn(
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...props}
/>
);
}
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-actions"
className={cn("flex items-center gap-2", className)}
{...props}
/>
);
}
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-header"
className={cn(
"flex basis-full items-center justify-between gap-2",
className
)}
{...props}
/>
);
}
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="item-footer"
className={cn(
"flex basis-full items-center justify-between gap-2",
className
)}
{...props}
/>
);
}
export {
Item,
ItemMedia,
ItemContent,
ItemActions,
ItemGroup,
ItemSeparator,
ItemTitle,
ItemDescription,
ItemHeader,
ItemFooter,
};
-28
View File
@@ -1,28 +0,0 @@
import { cn } from "@/lib/utils";
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
return (
<kbd
data-slot="kbd"
className={cn(
"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none",
"[&_svg:not([class*='size-'])]:size-3",
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
className
)}
{...props}
/>
);
}
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<kbd
data-slot="kbd-group"
className={cn("inline-flex items-center gap-1", className)}
{...props}
/>
);
}
export { Kbd, KbdGroup };
-22
View File
@@ -1,22 +0,0 @@
import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { cn } from "@/lib/utils";
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
);
}
export { Label };
-274
View File
@@ -1,274 +0,0 @@
import * as React from "react";
import * as MenubarPrimitive from "@radix-ui/react-menubar";
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Menubar({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Root>) {
return (
<MenubarPrimitive.Root
data-slot="menubar"
className={cn(
"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
className
)}
{...props}
/>
);
}
function MenubarMenu({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />;
}
function MenubarGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Group>) {
return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />;
}
function MenubarPortal({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />;
}
function MenubarRadioGroup({
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
return (
<MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
);
}
function MenubarTrigger({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
return (
<MenubarPrimitive.Trigger
data-slot="menubar-trigger"
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
className
)}
{...props}
/>
);
}
function MenubarContent({
className,
align = "start",
alignOffset = -4,
sideOffset = 8,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Content>) {
return (
<MenubarPortal>
<MenubarPrimitive.Content
data-slot="menubar-content"
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</MenubarPortal>
);
}
function MenubarItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof MenubarPrimitive.Item> & {
inset?: boolean;
variant?: "default" | "destructive";
}) {
return (
<MenubarPrimitive.Item
data-slot="menubar-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
);
}
function MenubarCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
return (
<MenubarPrimitive.CheckboxItem
data-slot="menubar-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
);
}
function MenubarRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
return (
<MenubarPrimitive.RadioItem
data-slot="menubar-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
);
}
function MenubarLabel({
className,
inset,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Label> & {
inset?: boolean;
}) {
return (
<MenubarPrimitive.Label
data-slot="menubar-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
);
}
function MenubarSeparator({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
return (
<MenubarPrimitive.Separator
data-slot="menubar-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function MenubarShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="menubar-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
);
}
function MenubarSub({
...props
}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
}
function MenubarSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean;
}) {
return (
<MenubarPrimitive.SubTrigger
data-slot="menubar-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
);
}
function MenubarSubContent({
className,
...props
}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
return (
<MenubarPrimitive.SubContent
data-slot="menubar-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
);
}
export {
Menubar,
MenubarPortal,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarGroup,
MenubarSeparator,
MenubarLabel,
MenubarItem,
MenubarShortcut,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarSub,
MenubarSubTrigger,
MenubarSubContent,
};
-168
View File
@@ -1,168 +0,0 @@
import * as React from "react";
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
import { cva } from "class-variance-authority";
import { ChevronDownIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function NavigationMenu({
className,
children,
viewport = true,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean;
}) {
return (
<NavigationMenuPrimitive.Root
data-slot="navigation-menu"
data-viewport={viewport}
className={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
);
}
function NavigationMenuList({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return (
<NavigationMenuPrimitive.List
data-slot="navigation-menu-list"
className={cn(
"group flex flex-1 list-none items-center justify-center gap-1",
className
)}
{...props}
/>
);
}
function NavigationMenuItem({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return (
<NavigationMenuPrimitive.Item
data-slot="navigation-menu-item"
className={cn("relative", className)}
{...props}
/>
);
}
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
);
function NavigationMenuTrigger({
className,
children,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return (
<NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger"
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDownIcon
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
);
}
function NavigationMenuContent({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return (
<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
className
)}
{...props}
/>
);
}
function NavigationMenuViewport({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return (
<div
className={cn(
"absolute top-full left-0 isolate z-50 flex justify-center"
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
{...props}
/>
</div>
);
}
function NavigationMenuLink({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return (
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
);
}
function NavigationMenuIndicator({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
return (
<NavigationMenuPrimitive.Indicator
data-slot="navigation-menu-indicator"
className={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className
)}
{...props}
>
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuPrimitive.Indicator>
);
}
export {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
};
-127
View File
@@ -1,127 +0,0 @@
import * as React from "react";
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { Button, buttonVariants } from "@/components/ui/button";
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
);
}
function PaginationContent({
className,
...props
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
);
}
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />;
}
type PaginationLinkProps = {
isActive?: boolean;
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">;
function PaginationLink({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
);
}
function PaginationPrevious({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
);
}
function PaginationNext({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) {
return (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
);
}
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />
<span className="sr-only">More pages</span>
</span>
);
}
export {
Pagination,
PaginationContent,
PaginationLink,
PaginationItem,
PaginationPrevious,
PaginationNext,
PaginationEllipsis,
};
-46
View File
@@ -1,46 +0,0 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
);
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
-29
View File
@@ -1,29 +0,0 @@
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils";
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
);
}
export { Progress };
-43
View File
@@ -1,43 +0,0 @@
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { CircleIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
);
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
}
export { RadioGroup, RadioGroupItem };
-54
View File
@@ -1,54 +0,0 @@
import * as React from "react";
import { GripVerticalIcon } from "lucide-react";
import * as ResizablePrimitive from "react-resizable-panels";
import { cn } from "@/lib/utils";
function ResizablePanelGroup({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
return (
<ResizablePrimitive.PanelGroup
data-slot="resizable-panel-group"
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
);
}
function ResizablePanel({
...props
}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
}
function ResizableHandle({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean;
}) {
return (
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
);
}
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
-56
View File
@@ -1,56 +0,0 @@
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils";
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
}
export { ScrollArea, ScrollBar };
-185
View File
@@ -1,185 +0,0 @@
import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default";
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = "popper",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
);
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
);
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};
-26
View File
@@ -1,26 +0,0 @@
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils";
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
);
}
export { Separator };
-139
View File
@@ -1,139 +0,0 @@
"use client";
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
function SheetTrigger({
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetClose({
...props
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetPortal({
...props
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
);
}
function SheetContent({
className,
children,
side = "right",
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left";
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
side === "top" &&
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
side === "bottom" &&
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
className
)}
{...props}
>
{children}
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<XIcon className="size-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1.5 p-4", className)}
{...props}
/>
);
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
);
}
function SheetTitle({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
function SheetDescription({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
export {
Sheet,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};
-734
View File
@@ -1,734 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useIsMobile } from "@/hooks/useMobile";
import { cn } from "@/lib/utils";
import { Slot } from "@radix-ui/react-slot";
import { cva, VariantProps } from "class-variance-authority";
import { PanelLeftIcon } from "lucide-react";
import * as React from "react";
const SIDEBAR_COOKIE_NAME = "sidebar_state";
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = "16rem";
const SIDEBAR_WIDTH_MOBILE = "18rem";
const SIDEBAR_WIDTH_ICON = "3rem";
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
type SidebarContextProps = {
state: "expanded" | "collapsed";
open: boolean;
setOpen: (open: boolean) => void;
openMobile: boolean;
setOpenMobile: (open: boolean) => void;
isMobile: boolean;
toggleSidebar: () => void;
};
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
function useSidebar() {
const context = React.useContext(SidebarContext);
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.");
}
return context;
}
function SidebarProvider({
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
}: React.ComponentProps<"div"> & {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}) {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen);
const open = openProp ?? _open;
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value;
if (setOpenProp) {
setOpenProp(openState);
} else {
_setOpen(openState);
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open]
);
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile ? setOpenMobile(open => !open) : setOpen(open => !open);
}, [isMobile, setOpen, setOpenMobile]);
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault();
toggleSidebar();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [toggleSidebar]);
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed";
const contextValue = React.useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
);
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
data-slot="sidebar-wrapper"
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
className
)}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
);
}
function Sidebar({
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
disableTransition = false,
className,
children,
...props
}: React.ComponentProps<"div"> & {
side?: "left" | "right";
variant?: "sidebar" | "floating" | "inset";
collapsible?: "offcanvas" | "icon" | "none";
disableTransition?: boolean;
}) {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
if (collapsible === "none") {
return (
<div
data-slot="sidebar"
className={cn(
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
className
)}
{...props}
>
{children}
</div>
);
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<SheetHeader className="sr-only">
<SheetTitle>Sidebar</SheetTitle>
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
</SheetHeader>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
);
}
return (
<div
className="group peer text-sidebar-foreground hidden md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
data-slot="sidebar"
>
{/* This is what handles the sidebar gap on desktop */}
<div
data-slot="sidebar-gap"
className={cn(
"relative w-(--sidebar-width) bg-transparent",
disableTransition
? "transition-none"
: "transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
)}
/>
<div
data-slot="sidebar-container"
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) md:flex",
disableTransition
? "transition-none"
: "transition-[left,right,width] duration-200 ease-linear",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...props}
>
<div
data-sidebar="sidebar"
data-slot="sidebar-inner"
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
>
{children}
</div>
</div>
</div>
);
}
function SidebarTrigger({
className,
onClick,
...props
}: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar();
return (
<Button
data-sidebar="trigger"
data-slot="sidebar-trigger"
variant="ghost"
size="icon"
className={cn("size-7", className)}
onClick={event => {
onClick?.(event);
toggleSidebar();
}}
{...props}
>
<PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span>
</Button>
);
}
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
const { toggleSidebar } = useSidebar();
return (
<button
data-sidebar="rail"
data-slot="sidebar-rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
);
}
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return (
<main
data-slot="sidebar-inset"
className={cn(
"bg-background relative flex w-full flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className
)}
{...props}
/>
);
}
function SidebarInput({
className,
...props
}: React.ComponentProps<typeof Input>) {
return (
<Input
data-slot="sidebar-input"
data-sidebar="input"
className={cn("bg-background h-8 w-full shadow-none", className)}
{...props}
/>
);
}
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-header"
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
}
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-footer"
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
);
}
function SidebarSeparator({
className,
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="sidebar-separator"
data-sidebar="separator"
className={cn("bg-sidebar-border mx-2 w-auto", className)}
{...props}
/>
);
}
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-content"
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props}
/>
);
}
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group"
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
);
}
function SidebarGroupLabel({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "div";
return (
<Comp
data-slot="sidebar-group-label"
data-sidebar="group-label"
className={cn(
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
{...props}
/>
);
}
function SidebarGroupAction({
className,
asChild = false,
...props
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="sidebar-group-action"
data-sidebar="group-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
);
}
function SidebarGroupContent({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-group-content"
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
);
}
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu"
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
);
}
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-item"
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
);
}
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
function SidebarMenuButton({
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean;
isActive?: boolean;
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
} & VariantProps<typeof sidebarMenuButtonVariants>) {
const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar();
const button = (
<Comp
data-slot="sidebar-menu-button"
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
);
if (!tooltip) {
return button;
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
};
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
);
}
function SidebarMenuAction({
className,
asChild = false,
showOnHover = false,
...props
}: React.ComponentProps<"button"> & {
asChild?: boolean;
showOnHover?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="sidebar-menu-action"
data-sidebar="menu-action"
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
className
)}
{...props}
/>
);
}
function SidebarMenuBadge({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="sidebar-menu-badge"
data-sidebar="menu-badge"
className={cn(
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
);
}
function SidebarMenuSkeleton({
className,
showIcon = false,
...props
}: React.ComponentProps<"div"> & {
showIcon?: boolean;
}) {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`;
}, []);
return (
<div
data-slot="sidebar-menu-skeleton"
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-(--skeleton-width) flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
);
}
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
return (
<ul
data-slot="sidebar-menu-sub"
data-sidebar="menu-sub"
className={cn(
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
);
}
function SidebarMenuSubItem({
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="sidebar-menu-sub-item"
data-sidebar="menu-sub-item"
className={cn("group/menu-sub-item relative", className)}
{...props}
/>
);
}
function SidebarMenuSubButton({
asChild = false,
size = "md",
isActive = false,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean;
size?: "sm" | "md";
isActive?: boolean;
}) {
const Comp = asChild ? Slot : "a";
return (
<Comp
data-slot="sidebar-menu-sub-button"
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
);
}
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar
};
-13
View File
@@ -1,13 +0,0 @@
import { cn } from "@/lib/utils";
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
);
}
export { Skeleton };
-61
View File
@@ -1,61 +0,0 @@
import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { cn } from "@/lib/utils";
function Slider({
className,
defaultValue,
value,
min = 0,
max = 100,
...props
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
const _values = React.useMemo(
() =>
Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max]
);
return (
<SliderPrimitive.Root
data-slot="slider"
defaultValue={defaultValue}
value={value}
min={min}
max={max}
className={cn(
"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
className
)}
{...props}
>
<SliderPrimitive.Track
data-slot="slider-track"
className={cn(
"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
)}
>
<SliderPrimitive.Range
data-slot="slider-range"
className={cn(
"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
)}
/>
</SliderPrimitive.Track>
{Array.from({ length: _values.length }, (_, index) => (
<SliderPrimitive.Thumb
data-slot="slider-thumb"
key={index}
className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
/>
))}
</SliderPrimitive.Root>
);
}
export { Slider };
-23
View File
@@ -1,23 +0,0 @@
import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
);
};
export { Toaster };
-16
View File
@@ -1,16 +0,0 @@
import { Loader2Icon } from "lucide-react";
import { cn } from "@/lib/utils";
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
return (
<Loader2Icon
role="status"
aria-label="Loading"
className={cn("size-4 animate-spin", className)}
{...props}
/>
);
}
export { Spinner };
-29
View File
@@ -1,29 +0,0 @@
import * as React from "react";
import * as SwitchPrimitive from "@radix-ui/react-switch";
import { cn } from "@/lib/utils";
function Switch({
className,
...props
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
return (
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitive.Root>
);
}
export { Switch };
-114
View File
@@ -1,114 +0,0 @@
import * as React from "react";
import { cn } from "@/lib/utils";
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
);
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
);
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
);
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
);
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
);
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
);
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
);
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
);
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};
-64
View File
@@ -1,64 +0,0 @@
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils";
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
);
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...props}
/>
);
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
);
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent };
-67
View File
@@ -1,67 +0,0 @@
import { useDialogComposition } from "@/components/ui/dialog";
import { useComposition } from "@/hooks/useComposition";
import { cn } from "@/lib/utils";
import * as React from "react";
function Textarea({
className,
onKeyDown,
onCompositionStart,
onCompositionEnd,
...props
}: React.ComponentProps<"textarea">) {
// Get dialog composition context if available (will be no-op if not inside Dialog)
const dialogComposition = useDialogComposition();
// Add composition event handlers to support input method editor (IME) for CJK languages.
const {
onCompositionStart: handleCompositionStart,
onCompositionEnd: handleCompositionEnd,
onKeyDown: handleKeyDown,
} = useComposition<HTMLTextAreaElement>({
onKeyDown: (e) => {
// Check if this is an Enter key that should be blocked
const isComposing = (e.nativeEvent as any).isComposing || dialogComposition.justEndedComposing();
// If Enter key is pressed while composing or just after composition ended,
// don't call the user's onKeyDown (this blocks the business logic)
// Note: For textarea, Shift+Enter should still work for newlines
if (e.key === "Enter" && !e.shiftKey && isComposing) {
return;
}
// Otherwise, call the user's onKeyDown
onKeyDown?.(e);
},
onCompositionStart: e => {
dialogComposition.setComposing(true);
onCompositionStart?.(e);
},
onCompositionEnd: e => {
// Mark that composition just ended - this helps handle the Enter key that confirms input
dialogComposition.markCompositionEnd();
// Delay setting composing to false to handle Safari's event order
// In Safari, compositionEnd fires before the ESC keydown event
setTimeout(() => {
dialogComposition.setComposing(false);
}, 100);
onCompositionEnd?.(e);
},
});
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onKeyDown={handleKeyDown}
{...props}
/>
);
}
export { Textarea };
-73
View File
@@ -1,73 +0,0 @@
"use client";
import * as React from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { toggleVariants } from "@/components/ui/toggle";
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
});
function ToggleGroup({
className,
variant,
size,
children,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<ToggleGroupPrimitive.Root
data-slot="toggle-group"
data-variant={variant}
data-size={size}
className={cn(
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
className
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
);
}
function ToggleGroupItem({
className,
children,
variant,
size,
...props
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
data-slot="toggle-group-item"
data-variant={context.variant || variant}
data-size={context.size || size}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
}
export { ToggleGroup, ToggleGroupItem };
-45
View File
@@ -1,45 +0,0 @@
import * as React from "react";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-9 px-2 min-w-9",
sm: "h-8 px-1.5 min-w-8",
lg: "h-10 px-2.5 min-w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
function Toggle({
className,
variant,
size,
...props
}: React.ComponentProps<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>) {
return (
<TogglePrimitive.Root
data-slot="toggle"
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Toggle, toggleVariants };
-59
View File
@@ -1,59 +0,0 @@
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
);
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

Some files were not shown because too many files have changed in this diff Show More