This commit is contained in:
2025-12-23 00:23:21 +00:00
commit 59f64a1291
91 changed files with 12991 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

24
README.md Normal file
View File

@@ -0,0 +1,24 @@
# idp.global
Open Source identity platform for everyone, everywhere.
## Tech Stack
- Vite
- TypeScript
- React
- shadcn/ui
- Tailwind CSS
## Development
```sh
# Install dependencies
pnpm install
# Start dev server
pnpm dev
# Build for production
pnpm build
```

40
changelog.md Normal file
View File

@@ -0,0 +1,40 @@
# Changelog
## 2025-12-22 - 0.2.0 - feat(docs)
add in-app documentation pages and Fair Usage Policy
- Add FairUsagePolicy page and route (/fair-usage) with full policy content and a sticky header/footer
- Introduce a docs section with a DocsLayout and nested routes: Introduction, Quick Start, Docker, Configuration, SDK, OIDC, Organizations, Users
- Update App.tsx to register docs routes and the Fair Usage route
- Update UI components (Navbar, Footer, Hero, HowItWorks) to use internal docs routes and add a Fair Usage link
- Refactor DocsLayout to provide independent scrolling for the sidebar and main content and tweak header/sidebar layout
- Include code examples and navigation between docs pages to improve developer onboarding
## 2025-12-22 - 0.1.0 - feat(site)
revamp landing pages and add documentation layout
- Add new DocsLayout component with sidebar navigation, header, sign-in CTA and documentation links
- Revise Hero, Features, HowItWorks, Navbar and Footer copy to emphasize organization management, OIDC provider, RBAC, 2FA, deploy/self-host options and TypeScript SDK
- Introduce new feature cards and icons; restructure feature list and feature preview UI
- Update CTAs and links to point to docs, repository, community and sign-in endpoints; wrap buttons with asChild anchors for proper navigation
- Minor UI tweak: reduce feature card animation delay from 150ms to 100ms
## 2025-04-03 - 0.0.0 - Site UI, theming, content and infra updates
Large update adding dark theme support, UI refactors, content/footer updates, and initial site creation.
- Add dark theme support across the app (complete dark theme implementation).
- Add a dark theme toggle (using next-themes) and enable dark mode support for UI components.
- Fix dark mode styling specifically for the HowItWorks section.
- Refactor UI for an improved modern look and feel; improve Hero preview to resemble a passport.
- Remove the CTA component and restructure the CTA section for improved layout.
- Add sample passport data (John Doe) and fix the hero passport link to the correct URL.
- Add identity URL mapping: map idp://user.global/identity to https://passport.idp.global/john.doe.
- Add legal and company links to the footer (note: one revert related to this change was recorded and then re-applied).
- Fix a syntax error in HowItWorks.tsx (missing comma).
- Create the about.idp.global website (initial implementation).
- Standardize project tech stack setup: use vite_react_shadcn_ts.
## 2025-11-30 - 0.0.0 - Miscellaneous housekeeping
Several small/undocumented updates and housekeeping commits were made on this date; no detailed changelog messages were provided.
- Multiple minor updates / maintenance changes (details not specified in commit messages).

20
components.json Normal file
View File

@@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

29
eslint.config.js Normal file
View File

@@ -0,0 +1,29 @@
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";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"@typescript-eslint/no-unused-vars": "off",
},
}
);

22
index.html Normal file
View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>idp.global - Free Open Source Identity Platform</title>
<meta name="description" content="idp.global is an OpenID like platform that provides an Open Source, free identity to everyone who wants one." />
<meta name="author" content="idp.global" />
<meta property="og:title" content="idp.global - Free Open Source Identity Platform" />
<meta property="og:description" content="idp.global is an OpenID like platform that provides an Open Source, free identity to everyone who wants one." />
<meta property="og:type" content="website" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@idp_global" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

83
package.json Normal file
View File

@@ -0,0 +1,83 @@
{
"name": "vite_react_shadcn_ts",
"private": true,
"version": "0.2.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"build:dev": "vite build --mode development",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-context-menu": "^2.2.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.1",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@tanstack/react-query": "^5.56.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.0",
"react-resizable-panels": "^2.1.3",
"react-router-dom": "^6.26.2",
"recharts": "^2.12.7",
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.3",
"zod": "^3.23.8"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@tailwindcss/typography": "^0.5.15",
"@types/node": "^22.5.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.11",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
"vite": "^5.4.1"
},
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34"
}

4481
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

14
public/robots.txt Normal file
View File

@@ -0,0 +1,14 @@
User-agent: Googlebot
Allow: /
User-agent: Bingbot
Allow: /
User-agent: Twitterbot
Allow: /
User-agent: facebookexternalhit
Allow: /
User-agent: *
Allow: /

57
src/App.tsx Normal file
View File

@@ -0,0 +1,57 @@
import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { ThemeProvider } from "@/components/ThemeProvider";
import Index from "./pages/Index";
import NotFound from "./pages/NotFound";
import FairUsagePolicy from "./pages/FairUsagePolicy";
// Docs pages
import DocsLayout from "./pages/docs/DocsLayout";
import Introduction from "./pages/docs/Introduction";
import QuickStart from "./pages/docs/QuickStart";
import Docker from "./pages/docs/Docker";
import Configuration from "./pages/docs/Configuration";
import SDK from "./pages/docs/SDK";
import OIDC from "./pages/docs/OIDC";
import Organizations from "./pages/docs/Organizations";
import Users from "./pages/docs/Users";
const queryClient = new QueryClient();
const App = () => (
<QueryClientProvider client={queryClient}>
<ThemeProvider defaultTheme="system">
<TooltipProvider>
<Toaster />
<Sonner />
<BrowserRouter>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/fair-usage" element={<FairUsagePolicy />} />
{/* Documentation routes */}
<Route path="/docs" element={<DocsLayout />}>
<Route index element={<Introduction />} />
<Route path="quick-start" element={<QuickStart />} />
<Route path="docker" element={<Docker />} />
<Route path="configuration" element={<Configuration />} />
<Route path="sdk" element={<SDK />} />
<Route path="oidc" element={<OIDC />} />
<Route path="organizations" element={<Organizations />} />
<Route path="users" element={<Users />} />
</Route>
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</TooltipProvider>
</ThemeProvider>
</QueryClientProvider>
);
export default App;

View File

@@ -0,0 +1,95 @@
import React from 'react';
import { Building2, Key, Users, Shield, Fingerprint, Container, Code2, Zap } from 'lucide-react';
import { cn } from '@/lib/utils';
const features = [
{
icon: Building2,
title: 'Organization Management',
description: 'Multi-tenant architecture with full organization lifecycle management. Invite members, assign roles, and transfer ownership.'
},
{
icon: Key,
title: 'OpenID Connect Provider',
description: 'Full OIDC compliance for third-party app integration. Authorization code flow with PKCE, token refresh, and revocation.'
},
{
icon: Users,
title: 'User Management',
description: 'Complete user lifecycle from registration to profile management. Support for email/password, magic links, and API tokens.'
},
{
icon: Shield,
title: 'Role-Based Access Control',
description: 'Fine-grained permissions with admin, member, and custom roles. Control access at organization and application level.'
},
{
icon: Fingerprint,
title: 'Two-Factor Authentication',
description: 'Enhanced security with 2FA support. JWT-based sessions with automatic refresh and device management.'
},
{
icon: Container,
title: 'Deploy Anywhere',
description: 'Self-host with our Docker image or use idp.global for free. Your digital identity, your choice of infrastructure.'
},
{
icon: Code2,
title: 'TypeScript SDK',
description: 'Type-safe client libraries for browser and Node.js. Real-time updates via WebSocket with typed API requests.'
},
{
icon: Zap,
title: 'Open Source',
description: 'MIT licensed and fully transparent. Community-driven development hosted on code.foss.global with no vendor lock-in.'
}
];
const Features = () => {
return (
<section id="features" className="section bg-background">
<div className="container">
{/* Section Header */}
<div className="text-center max-w-2xl mx-auto mb-16">
<div className="label-pill mb-4">Platform Capabilities</div>
<h2 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground mb-4">
Enterprise Identity, Open Source Freedom
</h2>
<p className="text-lg text-muted-foreground">
Everything you need to manage authentication, users, and organizations for your applications.
</p>
</div>
{/* Feature Cards */}
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
{features.map((feature, index) => {
const Icon = feature.icon;
return (
<div
key={index}
className={cn(
"group p-6 rounded-xl border border-border/50 bg-card",
"transition-all duration-300 ease-out",
"hover:border-border hover:shadow-lg hover:-translate-y-1",
"dark:hover:shadow-[0_0_30px_rgba(59,130,246,0.1)]",
"animate-fade-in-up"
)}
style={{ animationDelay: `${index * 100}ms` }}
>
{/* Icon Container */}
<div className="mb-4 w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors">
<Icon className="h-5 w-5 text-primary" />
</div>
<h3 className="text-lg font-semibold text-foreground mb-2">{feature.title}</h3>
<p className="text-sm text-muted-foreground leading-relaxed">{feature.description}</p>
</div>
);
})}
</div>
</div>
</section>
);
};
export default Features;

162
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,162 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Globe, Github, Twitter, Linkedin } from 'lucide-react';
const Footer = () => {
return (
<footer className="bg-background border-t border-border pt-16 pb-8">
<div className="container">
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-8 lg:gap-12 mb-12">
{/* Brand Column */}
<div className="col-span-2 md:col-span-4 lg:col-span-1">
<Link to="/" className="flex items-center gap-2 mb-4 group">
<Globe className="h-5 w-5 text-muted-foreground group-hover:text-foreground transition-colors" />
<span className="text-base font-semibold text-foreground">idp.global</span>
</Link>
<p className="text-sm text-muted-foreground mb-4 max-w-xs">
Open Source Identity Provider for SMEs and enterprises. MIT Licensed.
</p>
<div className="flex space-x-3">
<a
href="https://code.foss.global/idp.global/idp.global"
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="Source Code"
>
<Github className="h-4 w-4" />
</a>
<a
href="https://twitter.com/sixtyfour_tv"
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="Twitter"
>
<Twitter className="h-4 w-4" />
</a>
<a
href="https://www.linkedin.com/company/task-venture-capital-gmbh"
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="LinkedIn"
>
<Linkedin className="h-4 w-4" />
</a>
</div>
</div>
{/* Platform */}
<div>
<h3 className="text-xs font-medium uppercase tracking-widest text-muted-foreground mb-4">
Platform
</h3>
<ul className="space-y-3">
<li>
<Link to="#features" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
Features
</Link>
</li>
<li>
<Link to="#how-it-works" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
Getting Started
</Link>
</li>
<li>
<a href="https://idp.global" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
Sign In
</a>
</li>
</ul>
</div>
{/* Developers */}
<div>
<h3 className="text-xs font-medium uppercase tracking-widest text-muted-foreground mb-4">
Developers
</h3>
<ul className="space-y-3">
<li>
<Link to="/docs" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
Documentation
</Link>
</li>
<li>
<Link to="/docs/sdk" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
TypeScript SDK
</Link>
</li>
<li>
<a href="https://community.foss.global" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
Community
</a>
</li>
<li>
<a href="https://code.foss.global/idp.global/idp.global" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
Source Code
</a>
</li>
</ul>
</div>
{/* Company */}
<div>
<h3 className="text-xs font-medium uppercase tracking-widest text-muted-foreground mb-4">
Company
</h3>
<ul className="space-y-3">
<li>
<a href="https://task.vc/" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
About Task Venture Capital
</a>
</li>
<li>
<a href="https://hr.task.vc/jobs" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
Careers
</a>
</li>
<li>
<a href="mailto:hello@task.vc" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
Contact
</a>
</li>
</ul>
</div>
</div>
{/* Bottom Bar */}
<div className="border-t border-border/50 pt-6 mt-8">
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-xs text-muted-foreground">
© {new Date().getFullYear()} Task Venture Capital GmbH. All rights reserved.
</p>
<div className="flex flex-wrap justify-center gap-x-6 gap-y-2">
<a
href="https://legal.task.vc/privacy"
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
Privacy
</a>
<a
href="https://legal.task.vc/tos"
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
Terms
</a>
<a
href="https://legal.task.vc/cookie"
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
Cookies
</a>
<a
href="https://legal.task.vc/"
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
>
Impressum
</a>
</div>
</div>
</div>
</div>
</footer>
);
};
export default Footer;

124
src/components/Hero.tsx Normal file
View File

@@ -0,0 +1,124 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Building2, Users, Key, Shield, CheckCircle2 } from 'lucide-react';
const Hero = () => {
return (
<section className="relative pt-24 pb-20 md:pt-32 md:pb-28 bg-background overflow-hidden">
{/* Subtle background glow */}
<div className="absolute inset-0 -z-10">
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-primary/5 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-accent/5 rounded-full blur-3xl" />
</div>
<div className="container">
<div className="grid lg:grid-cols-2 gap-16 lg:gap-24 items-center">
{/* Content */}
<div className="space-y-8 animate-fade-in-up">
<div className="space-y-4">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 text-primary text-sm font-medium">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
</span>
Open Source Identity Provider
</div>
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold tracking-tight text-foreground text-balance">
Your Digital Identity, Forever Free
</h1>
<p className="text-lg md:text-xl text-muted-foreground max-w-lg leading-relaxed">
Get your free digital passport on idp.global. An Open Source Identity Provider with OIDC, organization management, and RBAC. Self-host or use our free SaaS.*
</p>
<p className="text-xs text-muted-foreground/70">
* <Link to="/fair-usage" className="hover:text-foreground underline underline-offset-2 transition-colors">Fair Usage Policy</Link> applies
</p>
</div>
<div className="flex flex-wrap gap-4 pt-2">
<Button
size="lg"
className="h-12 px-6 bg-foreground text-background hover:bg-foreground/90 font-medium rounded-lg transition-all duration-200 hover:-translate-y-0.5"
asChild
>
<a href="https://idp.global">Get Your Free Identity</a>
</Button>
<Button
variant="outline"
size="lg"
className="h-12 px-6 border-border text-foreground hover:bg-accent/10 font-medium rounded-lg transition-all duration-200"
asChild
>
<a href="https://code.foss.global/idp.global/idp.global">Self-Host</a>
</Button>
</div>
</div>
{/* Feature Preview Card */}
<div className="hidden lg:block relative animate-fade-in-up stagger-2">
{/* Glow effect behind card */}
<div className="absolute inset-0 -z-10 bg-primary/10 dark:bg-primary/5 rounded-3xl blur-2xl scale-95" />
<Card className="relative border border-border/50 bg-card shadow-2xl dark:shadow-glow-blue rounded-xl overflow-hidden">
<div className="p-6 space-y-5">
{/* Header */}
<div className="flex items-center justify-between">
<span className="text-[10px] font-medium uppercase tracking-widest text-muted-foreground">
Identity Provider
</span>
<span className="text-xs font-semibold text-primary">idp.global</span>
</div>
{/* Features Grid */}
<div className="grid grid-cols-2 gap-4">
<div className="p-3 rounded-lg bg-muted/50 border border-border/50">
<Building2 className="h-5 w-5 text-primary mb-2" />
<div className="text-xs font-medium text-foreground">Organizations</div>
<div className="text-[10px] text-muted-foreground">Multi-tenant support</div>
</div>
<div className="p-3 rounded-lg bg-muted/50 border border-border/50">
<Users className="h-5 w-5 text-primary mb-2" />
<div className="text-xs font-medium text-foreground">User Management</div>
<div className="text-[10px] text-muted-foreground">RBAC & invitations</div>
</div>
<div className="p-3 rounded-lg bg-muted/50 border border-border/50">
<Key className="h-5 w-5 text-primary mb-2" />
<div className="text-xs font-medium text-foreground">OIDC Provider</div>
<div className="text-[10px] text-muted-foreground">SSO for your apps</div>
</div>
<div className="p-3 rounded-lg bg-muted/50 border border-border/50">
<Shield className="h-5 w-5 text-primary mb-2" />
<div className="text-xs font-medium text-foreground">2FA & Security</div>
<div className="text-[10px] text-muted-foreground">JWT-based auth</div>
</div>
</div>
{/* Code snippet */}
<div className="pt-4 border-t border-border/50 space-y-2">
<div className="text-[10px] font-medium uppercase tracking-widest text-muted-foreground">Quick Integration</div>
<div className="bg-muted/50 rounded-md p-3">
<code className="text-xs font-mono text-muted-foreground">
<span className="text-primary">import</span> {'{ IdpClient }'} <span className="text-primary">from</span> <span className="text-accent">'@idp.global/idpclient'</span>
</code>
</div>
</div>
{/* Footer */}
<div className="flex justify-between items-center pt-2">
<div className="flex items-center gap-1.5">
<CheckCircle2 className="h-4 w-4 text-accent" />
<span className="text-xs font-medium text-muted-foreground">MIT Licensed</span>
</div>
<code className="text-xs font-mono text-muted-foreground">Self-host or Free SaaS</code>
</div>
</div>
</Card>
</div>
</div>
</div>
</section>
);
};
export default Hero;

View File

@@ -0,0 +1,111 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight, Container, Building2, Key, Rocket } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
const steps = [
{
number: '01',
title: 'Deploy Your Instance',
description: "Pull our Docker image and deploy in minutes. Configure with environment variables for MongoDB and your domain.",
icon: Container
},
{
number: '02',
title: 'Set Up Organizations',
description: "Create organizations for your teams or clients. Invite members and assign roles with granular permissions.",
icon: Building2
},
{
number: '03',
title: 'Register Your Apps',
description: 'Add your applications as OIDC clients. Get client IDs and secrets for secure OAuth 2.0 authentication.',
icon: Key
},
{
number: '04',
title: 'Integrate & Launch',
description: 'Use our TypeScript SDK or standard OIDC endpoints. Your users can now authenticate across all your services.',
icon: Rocket
}
];
const HowItWorks = () => {
return (
<section id="how-it-works" className="section bg-muted/30 dark:bg-muted/5">
<div className="container">
{/* Section Header */}
<div className="text-center max-w-2xl mx-auto mb-16">
<div className="label-pill mb-4">Getting Started</div>
<h2 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground mb-4">
From Zero to SSO in Minutes
</h2>
<p className="text-lg text-muted-foreground">
Deploy your own identity provider and integrate with your applications seamlessly
</p>
</div>
{/* Step Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{steps.map((step, index) => {
const Icon = step.icon;
return (
<Card
key={index}
className={cn(
"relative border border-transparent bg-card shadow-sm",
"transition-all duration-300 ease-out group overflow-hidden",
"hover:border-border hover:shadow-md"
)}
>
<CardContent className="p-6 flex flex-col h-full">
{/* Header with icon and step number */}
<div className="mb-6 flex items-center justify-between">
<div className="w-10 h-10 flex items-center justify-center rounded-md bg-primary/10 group-hover:bg-primary transition-colors duration-300">
<Icon className="w-5 h-5 text-primary group-hover:text-primary-foreground transition-colors duration-300" />
</div>
<span className="text-5xl font-bold tabular-nums text-muted/30 select-none">
{step.number}
</span>
</div>
{/* Content */}
<h3 className="text-lg font-semibold text-foreground mb-2 group-hover:text-primary transition-colors duration-300">
{step.title}
</h3>
<p className="text-sm text-muted-foreground leading-relaxed mb-4 flex-grow">
{step.description}
</p>
{/* Progress bar */}
<div className="mt-auto overflow-hidden">
<div className="h-0.5 w-8 bg-primary/30 group-hover:w-full group-hover:bg-primary transition-all duration-500 rounded-full" />
</div>
</CardContent>
</Card>
);
})}
</div>
{/* CTA */}
<div className="mt-12 text-center">
<Button
variant="outline"
className="group border-border text-foreground hover:bg-accent/10"
asChild
>
<Link to="/docs">
View Documentation
<ArrowRight className="ml-2 w-4 h-4 group-hover:translate-x-1 transition-transform duration-300" />
</Link>
</Button>
</div>
</div>
</section>
);
};
export default HowItWorks;

90
src/components/Navbar.tsx Normal file
View File

@@ -0,0 +1,90 @@
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { Globe, Menu } from 'lucide-react';
import { ThemeToggle } from '@/components/ThemeToggle';
interface NavbarProps extends React.HTMLAttributes<HTMLElement> {}
const Navbar = ({ className, ...props }: NavbarProps) => {
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 20);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<header
className={cn(
"sticky top-0 z-50 w-full transition-all duration-300",
scrolled
? "bg-background/80 backdrop-blur-xl border-b border-border/40 shadow-sm"
: "bg-transparent border-transparent",
className
)}
{...props}
>
<div className="container flex h-14 items-center justify-between">
{/* Logo */}
<Link to="/" className="flex items-center gap-2 group">
<Globe className="h-5 w-5 text-foreground/80 group-hover:text-primary transition-colors" />
<span className="text-base font-semibold tracking-tight text-foreground">idp.global</span>
</Link>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-1">
<Link
to="/"
className="px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors duration-200"
>
Home
</Link>
<Link
to="#features"
className="px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors duration-200"
>
Features
</Link>
<Link
to="#how-it-works"
className="px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors duration-200"
>
Getting Started
</Link>
<Link
to="/docs"
className="px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors duration-200"
>
Docs
</Link>
</nav>
{/* Desktop Actions */}
<div className="hidden md:flex items-center gap-3">
<ThemeToggle />
<Button
size="sm"
className="h-8 px-4 bg-foreground text-background hover:bg-foreground/90 dark:bg-foreground dark:text-background dark:hover:bg-foreground/90 font-medium"
asChild
>
<a href="https://idp.global">Sign In</a>
</Button>
</div>
{/* Mobile Actions */}
<div className="md:hidden flex items-center gap-2">
<ThemeToggle />
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-foreground">
<Menu className="h-5 w-5" />
</Button>
</div>
</div>
</header>
);
};
export default Navbar;

View File

@@ -0,0 +1,75 @@
import React from "react";
import { createContext, useContext, useEffect, useState } from "react";
type Theme = "dark" | "light" | "system";
type ThemeProviderProps = {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
};
type ThemeProviderState = {
theme: Theme;
setTheme: (theme: Theme) => void;
};
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
};
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
export function ThemeProvider({
children,
defaultTheme = "dark",
storageKey = "idp-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
);
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove("light", "dark");
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light";
root.classList.add(systemTheme);
return;
}
root.classList.add(theme);
}, [theme]);
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme);
setTheme(theme);
},
};
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
);
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext);
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
};

View File

@@ -0,0 +1,46 @@
import React from "react";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "@/components/ThemeProvider";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
return (
<Button
variant="ghost"
size="icon"
onClick={toggleTheme}
aria-label="Toggle theme"
className={cn(
"relative h-8 w-8 rounded-full overflow-hidden",
"text-muted-foreground hover:text-foreground",
"hover:bg-muted/50 dark:hover:bg-muted/30"
)}
>
<Sun
className={cn(
"h-4 w-4 transition-all duration-300",
theme === "dark"
? "rotate-90 scale-0 opacity-0"
: "rotate-0 scale-100 opacity-100"
)}
/>
<Moon
className={cn(
"absolute h-4 w-4 transition-all duration-300",
theme === "dark"
? "rotate-0 scale-100 opacity-100"
: "-rotate-90 scale-0 opacity-0"
)}
/>
<span className="sr-only">Toggle theme</span>
</Button>
);
}

View File

@@ -0,0 +1,56 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@@ -0,0 +1,139 @@
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"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@@ -0,0 +1,59 @@
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 p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@@ -0,0 +1,5 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root
export { AspectRatio }

View File

@@ -0,0 +1,48 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,115 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

View File

@@ -0,0 +1,56 @@
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 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,64 @@
import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker";
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ..._props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ..._props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar";
export { Calendar };

View File

@@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,260 @@
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
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
},
ref
) => {
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
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
)
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
})
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
})
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
)
})
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
)
})
CarouselNext.displayName = "CarouselNext"
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}

363
src/components/ui/chart.tsx Normal file
View File

@@ -0,0 +1,363 @@
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
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.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-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.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 [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
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
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
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
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.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(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
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="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
)
ChartLegendContent.displayName = "ChartLegend"
// 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,
}

View File

@@ -0,0 +1,28 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@@ -0,0 +1,9 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@@ -0,0 +1,153 @@
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[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>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@@ -0,0 +1,198 @@
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const ContextMenu = ContextMenuPrimitive.Root
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
const ContextMenuGroup = ContextMenuPrimitive.Group
const ContextMenuPortal = ContextMenuPrimitive.Portal
const ContextMenuSub = ContextMenuPrimitive.Sub
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
))
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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",
className
)}
{...props}
/>
))
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 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",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
))
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
))
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
))
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
))
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
ContextMenuShortcut.displayName = "ContextMenuShortcut"
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

View File

@@ -0,0 +1,120 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@@ -0,0 +1,116 @@
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

View File

@@ -0,0 +1,198 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

176
src/components/ui/form.tsx Normal file
View File

@@ -0,0 +1,176 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} 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, formState } = useFormContext()
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
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@@ -0,0 +1,27 @@
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/lib/utils"
const HoverCard = HoverCardPrimitive.Root
const HoverCardTrigger = HoverCardPrimitive.Trigger
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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",
className
)}
{...props}
/>
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
export { HoverCard, HoverCardTrigger, HoverCardContent }

View File

@@ -0,0 +1,69 @@
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { Dot } from "lucide-react"
import { cn } from "@/lib/utils"
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50",
containerClassName
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
))
InputOTP.displayName = "InputOTP"
const InputOTPGroup = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center", className)} {...props} />
))
InputOTPGroup.displayName = "InputOTPGroup"
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext)
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
return (
<div
ref={ref}
className={cn(
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background",
className
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
</div>
)}
</div>
)
})
InputOTPSlot.displayName = "InputOTPSlot"
const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
))
InputOTPSeparator.displayName = "InputOTPSeparator"
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }

View File

@@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,24 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@@ -0,0 +1,234 @@
import * as React from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const MenubarMenu = MenubarPrimitive.Menu
const MenubarGroup = MenubarPrimitive.Group
const MenubarPortal = MenubarPrimitive.Portal
const MenubarSub = MenubarPrimitive.Sub
const MenubarRadioGroup = MenubarPrimitive.RadioGroup
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn(
"flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
className
)}
{...props}
/>
))
Menubar.displayName = MenubarPrimitive.Root.displayName
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className
)}
{...props}
/>
))
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
))
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 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",
className
)}
{...props}
/>
))
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(
(
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
ref
) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md 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",
className
)}
{...props}
/>
</MenubarPrimitive.Portal>
)
)
MenubarContent.displayName = MenubarPrimitive.Content.displayName
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarItem.displayName = MenubarPrimitive.Item.displayName
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
))
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
))
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
const MenubarShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
MenubarShortcut.displayname = "MenubarShortcut"
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
}

View File

@@ -0,0 +1,128 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className
)}
{...props}
/>
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50"
)
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full 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 md:absolute md:w-auto ",
className
)}
{...props}
/>
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
ref={ref}
{...props}
/>
</div>
))
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}

View File

@@ -0,0 +1,117 @@
import * as React from "react"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
import { ButtonProps, buttonVariants } from "@/components/ui/button"
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
Pagination.displayName = "Pagination"
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
))
PaginationContent.displayName = "PaginationContent"
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
))
PaginationItem.displayName = "PaginationItem"
type PaginationLinkProps = {
isActive?: boolean
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">
const PaginationLink = ({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
)
PaginationLink.displayName = "PaginationLink"
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
)
PaginationPrevious.displayName = "PaginationPrevious"
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
)
PaginationNext.displayName = "PaginationNext"
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
)
PaginationEllipsis.displayName = "PaginationEllipsis"
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
}

View File

@@ -0,0 +1,29 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View File

@@ -0,0 +1,26 @@
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

View File

@@ -0,0 +1,42 @@
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

View File

@@ -0,0 +1,43 @@
import { GripVertical } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
const ResizablePanel = ResizablePrimitive.Panel
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 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-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

View File

@@ -0,0 +1,46 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@@ -0,0 +1,158 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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",
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}
{...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)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@@ -0,0 +1,29 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

131
src/components/ui/sheet.tsx Normal file
View File

@@ -0,0 +1,131 @@
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import * as React from "react"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> { }
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet, SheetClose,
SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger
}

View File

@@ -0,0 +1,761 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { VariantProps, cva } from "class-variance-authority"
import { PanelLeft } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { Sheet, SheetContent } from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
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 SidebarContext = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext<SidebarContext | null>(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}
>(
(
{
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
},
ref
) => {
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<SidebarContext>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
className
)}
ref={ref}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
)
}
)
SidebarProvider.displayName = "SidebarProvider"
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}
>(
(
{
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
},
ref
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
return (
<div
className={cn(
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
className
)}
ref={ref}
{...props}
>
{children}
</div>
)
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
}
return (
<div
ref={ref}
className="group peer hidden md:block text-sidebar-foreground"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"duration-200 relative h-svh w-[--sidebar-width] bg-transparent transition-[width] 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)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
)}
/>
<div
className={cn(
"duration-200 fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex",
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)_+_theme(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"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
{children}
</div>
</div>
</div>
)
}
)
Sidebar.displayName = "Sidebar"
const SidebarTrigger = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentProps<typeof Button>
>(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn("h-7 w-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
})
SidebarTrigger.displayName = "SidebarTrigger"
const SidebarRail = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button">
>(({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
)
})
SidebarRail.displayName = "SidebarRail"
const SidebarInset = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"main">
>(({ className, ...props }, ref) => {
return (
<main
ref={ref}
className={cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className
)}
{...props}
/>
)
})
SidebarInset.displayName = "SidebarInset"
const SidebarInput = React.forwardRef<
React.ElementRef<typeof Input>,
React.ComponentProps<typeof Input>
>(({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
className
)}
{...props}
/>
)
})
SidebarInput.displayName = "SidebarInput"
const SidebarHeader = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
})
SidebarHeader.displayName = "SidebarHeader"
const SidebarFooter = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
})
SidebarFooter.displayName = "SidebarFooter"
const SidebarSeparator = React.forwardRef<
React.ElementRef<typeof Separator>,
React.ComponentProps<typeof Separator>
>(({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
data-sidebar="separator"
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
)
})
SidebarSeparator.displayName = "SidebarSeparator"
const SidebarContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
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}
/>
)
})
SidebarContent.displayName = "SidebarContent"
const SidebarGroup = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
})
SidebarGroup.displayName = "SidebarGroup"
const SidebarGroupLabel = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "div"
return (
<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
"duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] 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}
/>
)
})
SidebarGroupLabel.displayName = "SidebarGroupLabel"
const SidebarGroupAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
})
SidebarGroupAction.displayName = "SidebarGroupAction"
const SidebarGroupContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
))
SidebarGroupContent.displayName = "SidebarGroupContent"
const SidebarMenu = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
))
SidebarMenu.displayName = "SidebarMenu"
const SidebarMenuItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
))
SidebarMenuItem.displayName = "SidebarMenuItem"
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none 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",
},
}
)
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>
>(
(
{
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
},
ref
) => {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
<Comp
ref={ref}
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>
)
}
)
SidebarMenuButton.displayName = "SidebarMenuButton"
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md: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 &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className
)}
{...props}
/>
)
})
SidebarMenuAction.displayName = "SidebarMenuAction"
const SidebarMenuBadge = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
"absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground select-none pointer-events-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}
/>
))
SidebarMenuBadge.displayName = "SidebarMenuBadge"
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
showIcon?: boolean
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
return (
<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn("rounded-md h-8 flex gap-2 px-2 items-center", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 flex-1 max-w-[--skeleton-width]"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
)
})
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
const SidebarMenuSub = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
))
SidebarMenuSub.displayName = "SidebarMenuSub"
const SidebarMenuSubItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ ...props }, ref) => <li ref={ref} {...props} />)
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
}
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring 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 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"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}
/>
)
})
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
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,
}

View File

@@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@@ -0,0 +1,26 @@
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }

View File

@@ -0,0 +1,29 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }

View File

@@ -0,0 +1,27 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

117
src/components/ui/table.tsx Normal file
View File

@@ -0,0 +1,117 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -0,0 +1,53 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

127
src/components/ui/toast.tsx Normal file
View File

@@ -0,0 +1,127 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View File

@@ -0,0 +1,33 @@
import { useToast } from "@/hooks/use-toast"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

View File

@@ -0,0 +1,59 @@
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",
})
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn("flex items-center justify-center gap-1", className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
))
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
})
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
export { ToggleGroup, ToggleGroupItem }

View File

@@ -0,0 +1,43 @@
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 rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
))
Toggle.displayName = TogglePrimitive.Root.displayName
export { Toggle, toggleVariants }

View File

@@ -0,0 +1,28 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md 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",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@@ -0,0 +1,3 @@
import { useToast, toast } from "@/hooks/use-toast";
export { useToast, toast };

19
src/hooks/use-mobile.tsx Normal file
View File

@@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}

191
src/hooks/use-toast.ts Normal file
View File

@@ -0,0 +1,191 @@
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

205
src/index.css Normal file
View File

@@ -0,0 +1,205 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
/* Light Mode - Secondary experience */
:root {
--background: 0 0% 100%;
--foreground: 240 10% 4%;
--card: 0 0% 100%;
--card-foreground: 240 10% 4%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 4%;
--primary: 217 91% 55%;
--primary-foreground: 0 0% 100%;
--secondary: 240 5% 96%;
--secondary-foreground: 240 6% 10%;
--muted: 240 5% 96%;
--muted-foreground: 240 4% 46%;
--accent: 188 94% 43%;
--accent-foreground: 240 6% 10%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 100%;
--border: 240 6% 90%;
--input: 240 6% 90%;
--ring: 217 91% 55%;
--radius: 0.5rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5% 26%;
--sidebar-primary: 240 6% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 5% 96%;
--sidebar-accent-foreground: 240 6% 10%;
--sidebar-border: 240 6% 90%;
--sidebar-ring: 217 91% 60%;
}
/* Dark Mode - Primary experience (Bloomberg/Vercel style) */
.dark {
--background: 240 10% 4%;
--foreground: 0 0% 98%;
--card: 240 7% 8%;
--card-foreground: 0 0% 98%;
--popover: 240 7% 8%;
--popover-foreground: 0 0% 98%;
--primary: 217 91% 60%;
--primary-foreground: 0 0% 100%;
--secondary: 240 5% 13%;
--secondary-foreground: 0 0% 98%;
--muted: 240 5% 13%;
--muted-foreground: 240 5% 65%;
--accent: 188 94% 43%;
--accent-foreground: 0 0% 100%;
--destructive: 0 63% 31%;
--destructive-foreground: 0 0% 98%;
--border: 240 6% 16%;
--input: 240 6% 16%;
--ring: 217 91% 60%;
--sidebar-background: 240 7% 8%;
--sidebar-foreground: 0 0% 98%;
--sidebar-primary: 217 91% 60%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 5% 13%;
--sidebar-accent-foreground: 0 0% 98%;
--sidebar-border: 240 6% 16%;
--sidebar-ring: 217 91% 60%;
}
/* Smooth theme transitions */
html {
transition: background-color 0.3s ease, color 0.3s ease;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground font-sans antialiased;
}
h1, h2, h3, h4, h5, h6 {
@apply font-semibold tracking-tight;
}
.container {
@apply px-4 sm:px-6 lg:px-8 mx-auto;
max-width: 1280px;
}
.section {
@apply py-20 md:py-28 lg:py-32;
}
/* Focus states for accessibility */
*:focus {
outline: none;
}
*:focus-visible {
@apply ring-2 ring-primary/50 ring-offset-2 ring-offset-background;
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
scroll-padding-top: 80px;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
}
@layer components {
/* Professional card styling */
.feature-card {
@apply bg-card p-6 rounded-xl border border-border/50;
@apply transition-all duration-300 ease-out;
}
.feature-card:hover {
@apply border-border shadow-lg -translate-y-1;
}
.dark .feature-card:hover {
box-shadow: 0 0 30px rgba(59, 130, 246, 0.1);
}
/* Label pill (IBM/Vercel style) */
.label-pill {
@apply inline-flex items-center rounded-full border border-border/50 px-3 py-1 text-xs font-medium text-muted-foreground;
}
/* Subtle glow for dark mode accents */
.dark .glow-blue {
box-shadow: 0 0 30px rgba(59, 130, 246, 0.15);
}
.dark .glow-teal {
box-shadow: 0 0 30px rgba(6, 182, 212, 0.15);
}
}
@layer utilities {
/* Monospace for technical elements */
.font-mono-tech {
@apply font-mono text-sm tracking-tight;
}
/* Text balance for headlines */
.text-balance {
text-wrap: balance;
}
/* Stagger animation delays */
.stagger-1 { animation-delay: 0.15s; }
.stagger-2 { animation-delay: 0.3s; }
.stagger-3 { animation-delay: 0.45s; }
.stagger-4 { animation-delay: 0.6s; }
}
/* Hide Lovable badge */
#lovable-badge,
[data-lovable],
a[href*="lovable.dev"],
a[href*="lovable.app"],
div[class*="lovable"],
img[alt*="Lovable"] {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

5
src/main.tsx Normal file
View File

@@ -0,0 +1,5 @@
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'
createRoot(document.getElementById("root")!).render(<App />);

View File

@@ -0,0 +1,231 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Globe, ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ThemeToggle } from '@/components/ThemeToggle';
const FairUsagePolicy = () => {
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="sticky top-0 z-50 w-full border-b border-border bg-background/95 backdrop-blur">
<div className="container flex h-14 items-center">
<Link to="/" className="flex items-center gap-2 mr-6 group">
<Globe className="h-5 w-5 text-foreground/80 group-hover:text-primary transition-colors" />
<span className="text-base font-semibold tracking-tight text-foreground">idp.global</span>
</Link>
<div className="ml-auto flex items-center gap-3">
<ThemeToggle />
<Button size="sm" className="h-8" asChild>
<a href="https://idp.global">Sign In</a>
</Button>
</div>
</div>
</header>
{/* Content */}
<main className="container max-w-3xl py-12 md:py-16">
<Button variant="ghost" size="sm" className="mb-8" asChild>
<Link to="/">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Home
</Link>
</Button>
<article className="prose prose-neutral dark:prose-invert max-w-none">
<h1 className="text-4xl font-bold tracking-tight text-foreground mb-4">
Fair Usage Policy
</h1>
<p className="text-lg text-muted-foreground mb-8">
Last updated: December 2024
</p>
<section className="space-y-6">
<div>
<h2 className="text-2xl font-semibold text-foreground mt-8 mb-4">Overview</h2>
<p className="text-muted-foreground leading-relaxed">
idp.global provides a free, Open Source Identity Provider platform for individuals,
developers, and organizations. To ensure the service remains available, performant,
and secure for all users, we have established this Fair Usage Policy. By using
idp.global, you agree to comply with these guidelines.
</p>
</div>
<div>
<h2 className="text-2xl font-semibold text-foreground mt-8 mb-4">Acceptable Use</h2>
<p className="text-muted-foreground leading-relaxed mb-4">
You may use idp.global for:
</p>
<ul className="list-disc list-inside text-muted-foreground space-y-2 ml-4">
<li>Personal digital identity management</li>
<li>Authentication for your applications and services</li>
<li>Organization and team management</li>
<li>Development, testing, and production workloads</li>
<li>Integration via our OIDC/OAuth 2.0 endpoints</li>
</ul>
</div>
<div>
<h2 className="text-2xl font-semibold text-foreground mt-8 mb-4">Prohibited Activities</h2>
<p className="text-muted-foreground leading-relaxed mb-4">
The following activities are strictly prohibited:
</p>
<h3 className="text-lg font-medium text-foreground mt-6 mb-3">Illegal Activities</h3>
<ul className="list-disc list-inside text-muted-foreground space-y-2 ml-4">
<li>Using the service for any unlawful purpose or in violation of any applicable laws</li>
<li>Identity theft, fraud, or impersonation of others</li>
<li>Distribution of malware, phishing, or other malicious content</li>
<li>Money laundering or financing of illegal activities</li>
<li>Violation of intellectual property rights</li>
</ul>
<h3 className="text-lg font-medium text-foreground mt-6 mb-3">Service Abuse</h3>
<ul className="list-disc list-inside text-muted-foreground space-y-2 ml-4">
<li>Denial of Service (DoS) or Distributed Denial of Service (DDoS) attacks</li>
<li>Excessive API requests designed to overload or disrupt the service</li>
<li>Automated account creation (bot registrations), other than explicitly designed for by the platform</li>
<li>Circumventing rate limits or security measures</li>
<li>Scraping, crawling, or harvesting user data</li>
<li>Attempting to gain unauthorized access to other users' accounts or data</li>
</ul>
<h3 className="text-lg font-medium text-foreground mt-6 mb-3">Harmful Content & Behavior</h3>
<ul className="list-disc list-inside text-muted-foreground space-y-2 ml-4">
<li>Harassment, abuse, or threats against other users</li>
<li>Spam or unsolicited bulk communications</li>
<li>Distribution of harmful, offensive, or illegal content</li>
<li>Creating accounts for the purpose of abusing invitations or referrals</li>
</ul>
</div>
<div>
<h2 className="text-2xl font-semibold text-foreground mt-8 mb-4">Rate Limits</h2>
<p className="text-muted-foreground leading-relaxed mb-4">
To ensure fair access for all users, the following rate limits apply:
</p>
<ul className="list-disc list-inside text-muted-foreground space-y-2 ml-4">
<li>API requests are subject to reasonable rate limiting</li>
<li>Authentication attempts are limited to prevent brute-force attacks</li>
<li>Bulk operations (invitations, exports) may be throttled</li>
</ul>
<p className="text-muted-foreground leading-relaxed mt-4">
If you require higher limits for legitimate use cases, please contact us to discuss
enterprise options or self-hosting.
</p>
</div>
<div>
<h2 className="text-2xl font-semibold text-foreground mt-8 mb-4">Resource Limits</h2>
<p className="text-muted-foreground leading-relaxed mb-4">
Free accounts on the hosted platform are subject to reasonable resource limits:
</p>
<ul className="list-disc list-inside text-muted-foreground space-y-2 ml-4">
<li>Organizations per user: Fair use, no hard limit for legitimate purposes</li>
<li>Members per organization: Fair use, contact us for large teams</li>
<li>API tokens per user: Reasonable limits apply</li>
<li>Storage: Profile data and authentication records only</li>
</ul>
</div>
<div>
<h2 className="text-2xl font-semibold text-foreground mt-8 mb-4">Our Commitment</h2>
<p className="text-muted-foreground leading-relaxed mb-4">
We believe that digital identity is a fundamental right. We will always try, to the best
of our conscience, to not lock anyone out of the platform. We strive to tolerate different
viewpoints and will not discriminate based on personal beliefs, political opinions, or
other lawful expressions.
</p>
<p className="text-muted-foreground leading-relaxed mb-4">
That said, in the end, all decisions regarding this platform come down to our own judgment.
We are human, and while we aim to be fair and consistent, we cannot guarantee that our
decisions will align with everyone's expectations.
</p>
<p className="text-muted-foreground leading-relaxed">
If you have any concerns about relying on our judgment, or if you simply prefer complete
autonomy over your identity infrastructure, we strongly encourage you to{' '}
<Link to="/docs/docker" className="text-primary hover:underline">
self-host your own instance
</Link>.
The software is fully Open Source under the MIT License, and self-hosted instances
operate entirely under your own control with no dependency on our decisions.
</p>
</div>
<div>
<h2 className="text-2xl font-semibold text-foreground mt-8 mb-4">Enforcement</h2>
<p className="text-muted-foreground leading-relaxed mb-4">
Violations of this Fair Usage Policy may result in:
</p>
<ul className="list-disc list-inside text-muted-foreground space-y-2 ml-4">
<li>Warning notifications</li>
<li>Temporary rate limiting or service restrictions</li>
<li>Suspension of account access</li>
<li>Permanent termination of accounts</li>
<li>Reporting to appropriate authorities for illegal activities</li>
</ul>
<p className="text-muted-foreground leading-relaxed mt-4">
We reserve the right to take action at our discretion to protect the service and its users.
</p>
</div>
<div>
<h2 className="text-2xl font-semibold text-foreground mt-8 mb-4">Self-Hosting Alternative</h2>
<p className="text-muted-foreground leading-relaxed">
idp.global is Open Source (MIT License). If your use case exceeds the fair usage
guidelines of our hosted platform, you are welcome to{' '}
<Link to="/docs/docker" className="text-primary hover:underline">
self-host your own instance
</Link>{' '}
without any restrictions. Self-hosted instances are not subject to this policy.
</p>
</div>
<div>
<h2 className="text-2xl font-semibold text-foreground mt-8 mb-4">Reporting Violations</h2>
<p className="text-muted-foreground leading-relaxed">
If you become aware of any violations of this policy, please report them to{' '}
<a href="mailto:abuse@idp.global" className="text-primary hover:underline">
abuse@idp.global
</a>{' '}
or through our{' '}
<a href="https://community.foss.global" className="text-primary hover:underline">
community forum
</a>.
</p>
</div>
<div>
<h2 className="text-2xl font-semibold text-foreground mt-8 mb-4">Changes to This Policy</h2>
<p className="text-muted-foreground leading-relaxed">
We may update this Fair Usage Policy from time to time. Continued use of the service
after changes constitutes acceptance of the updated policy. Significant changes will
be announced through our usual communication channels.
</p>
</div>
<div>
<h2 className="text-2xl font-semibold text-foreground mt-8 mb-4">Contact</h2>
<p className="text-muted-foreground leading-relaxed">
For questions about this policy or to discuss your specific use case, please contact
us at{' '}
<a href="mailto:hello@task.vc" className="text-primary hover:underline">
hello@task.vc
</a>.
</p>
</div>
</section>
</article>
{/* Footer */}
<div className="mt-16 pt-8 border-t border-border">
<p className="text-sm text-muted-foreground">
© {new Date().getFullYear()} Task Venture Capital GmbH. All rights reserved.
</p>
</div>
</main>
</div>
);
};
export default FairUsagePolicy;

23
src/pages/Index.tsx Normal file
View File

@@ -0,0 +1,23 @@
import React from 'react';
import Navbar from '@/components/Navbar';
import Hero from '@/components/Hero';
import Features from '@/components/Features';
import HowItWorks from '@/components/HowItWorks';
import Footer from '@/components/Footer';
const Index = () => {
return (
<div className="min-h-screen bg-background">
<Navbar />
<main>
<Hero />
<Features />
<HowItWorks />
</main>
<Footer />
</div>
);
};
export default Index;

27
src/pages/NotFound.tsx Normal file
View File

@@ -0,0 +1,27 @@
import { useLocation } from "react-router-dom";
import { useEffect } from "react";
const NotFound = () => {
const location = useLocation();
useEffect(() => {
console.error(
"404 Error: User attempted to access non-existent route:",
location.pathname
);
}, [location.pathname]);
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">404</h1>
<p className="text-xl text-gray-600 mb-4">Oops! Page not found</p>
<a href="/" className="text-blue-500 hover:text-blue-700 underline">
Return to Home
</a>
</div>
</div>
);
};
export default NotFound;

View File

@@ -0,0 +1,176 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { ArrowRight, ArrowLeft } from 'lucide-react';
const CodeBlock = ({ children, title }: { children: string; title?: string }) => (
<div className="rounded-lg border border-border overflow-hidden">
{title && (
<div className="px-4 py-2 bg-muted/50 border-b border-border text-xs font-medium text-muted-foreground">
{title}
</div>
)}
<pre className="p-4 overflow-x-auto bg-muted/30">
<code className="text-sm font-mono text-foreground">{children}</code>
</pre>
</div>
);
const Configuration = () => {
return (
<div className="space-y-8">
{/* Header */}
<div className="space-y-4">
<h1 className="text-4xl font-bold tracking-tight text-foreground">
Configuration
</h1>
<p className="text-xl text-muted-foreground leading-relaxed">
Configure your idp.global instance for production use.
</p>
</div>
{/* Core Configuration */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Core Configuration</h2>
<p className="text-muted-foreground">
idp.global is configured via environment variables. These are the essential settings:
</p>
<h3 className="text-lg font-medium text-foreground">Database</h3>
<CodeBlock title="Environment">
{`# MongoDB connection string
# Supports replica sets and authentication
MONGODB_URL=mongodb://user:pass@host:27017/idp?authSource=admin
# For MongoDB Atlas
MONGODB_URL=mongodb+srv://user:pass@cluster.mongodb.net/idp`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">Server</h3>
<CodeBlock title="Environment">
{`# Public URL of your instance (required for OAuth redirects)
IDP_BASEURL=https://idp.yourdomain.com
# Instance name shown in UI
INSTANCE_NAME=My Company IDP`}
</CodeBlock>
</div>
{/* JWT Configuration */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">JWT & Security</h2>
<p className="text-muted-foreground">
idp.global uses RS256 (RSA) for JWT signing. Keys are automatically generated and rotated.
</p>
<div className="p-4 rounded-lg border border-border bg-muted/30">
<h4 className="font-medium text-foreground mb-2">Key Management</h4>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
<li>RSA key pairs are auto-generated on first startup</li>
<li>Public keys are available at <code className="text-primary">/.well-known/jwks.json</code></li>
<li>Keys are stored in MongoDB for persistence across restarts</li>
<li>Automatic key rotation can be configured</li>
</ul>
</div>
</div>
{/* OIDC Configuration */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">OIDC Endpoints</h2>
<p className="text-muted-foreground">
The following OIDC endpoints are automatically configured based on your <code className="text-primary">IDP_BASEURL</code>:
</p>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-2 px-3 font-medium text-foreground">Endpoint</th>
<th className="text-left py-2 px-3 font-medium text-foreground">Path</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
<tr>
<td className="py-2 px-3 text-muted-foreground">Discovery</td>
<td className="py-2 px-3"><code className="text-primary">/.well-known/openid-configuration</code></td>
</tr>
<tr>
<td className="py-2 px-3 text-muted-foreground">JWKS</td>
<td className="py-2 px-3"><code className="text-primary">/.well-known/jwks.json</code></td>
</tr>
<tr>
<td className="py-2 px-3 text-muted-foreground">Authorization</td>
<td className="py-2 px-3"><code className="text-primary">/oauth/authorize</code></td>
</tr>
<tr>
<td className="py-2 px-3 text-muted-foreground">Token</td>
<td className="py-2 px-3"><code className="text-primary">/oauth/token</code></td>
</tr>
<tr>
<td className="py-2 px-3 text-muted-foreground">UserInfo</td>
<td className="py-2 px-3"><code className="text-primary">/oauth/userinfo</code></td>
</tr>
<tr>
<td className="py-2 px-3 text-muted-foreground">Revocation</td>
<td className="py-2 px-3"><code className="text-primary">/oauth/revoke</code></td>
</tr>
</tbody>
</table>
</div>
</div>
{/* Supported Scopes */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Supported OAuth Scopes</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-2 px-3 font-medium text-foreground">Scope</th>
<th className="text-left py-2 px-3 font-medium text-foreground">Description</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
<tr>
<td className="py-2 px-3"><code className="text-primary">openid</code></td>
<td className="py-2 px-3 text-muted-foreground">Required for OIDC. Returns sub claim.</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-primary">profile</code></td>
<td className="py-2 px-3 text-muted-foreground">User's name and profile information</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-primary">email</code></td>
<td className="py-2 px-3 text-muted-foreground">User's email address and verification status</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-primary">organizations</code></td>
<td className="py-2 px-3 text-muted-foreground">User's organization memberships</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-primary">roles</code></td>
<td className="py-2 px-3 text-muted-foreground">User's roles within organizations</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* Navigation */}
<div className="flex justify-between pt-8 border-t border-border">
<Button variant="ghost" asChild>
<Link to="/docs/docker">
<ArrowLeft className="mr-2 h-4 w-4" />
Docker Deployment
</Link>
</Button>
<Button variant="ghost" asChild>
<Link to="/docs/sdk">
TypeScript SDK
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</div>
);
};
export default Configuration;

227
src/pages/docs/Docker.tsx Normal file
View File

@@ -0,0 +1,227 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { ArrowRight, ArrowLeft } from 'lucide-react';
const CodeBlock = ({ children, title }: { children: string; title?: string }) => (
<div className="rounded-lg border border-border overflow-hidden">
{title && (
<div className="px-4 py-2 bg-muted/50 border-b border-border text-xs font-medium text-muted-foreground">
{title}
</div>
)}
<pre className="p-4 overflow-x-auto bg-muted/30">
<code className="text-sm font-mono text-foreground">{children}</code>
</pre>
</div>
);
const Docker = () => {
return (
<div className="space-y-8">
{/* Header */}
<div className="space-y-4">
<h1 className="text-4xl font-bold tracking-tight text-foreground">
Docker Deployment
</h1>
<p className="text-xl text-muted-foreground leading-relaxed">
Deploy idp.global on your own infrastructure using Docker. Full control over your
data with the same features as the hosted platform.
</p>
</div>
{/* Prerequisites */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Prerequisites</h2>
<ul className="list-disc list-inside text-muted-foreground space-y-2 ml-2">
<li>Docker 20.10 or later</li>
<li>MongoDB 5.0 or later (local or cloud-hosted like MongoDB Atlas)</li>
<li>A domain name with SSL certificate (for production)</li>
<li>Minimum 1GB RAM, 10GB storage</li>
</ul>
</div>
{/* Quick Start */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Quick Start</h2>
<p className="text-muted-foreground">
Pull and run the official Docker image:
</p>
<CodeBlock title="Terminal">
{`# Pull the latest image
docker pull code.foss.global/idp.global/idp.global
# Run with required environment variables
docker run -d \\
--name idp-global \\
-p 2999:2999 \\
-e MONGODB_URL=mongodb://localhost:27017/idp \\
-e IDP_BASEURL=https://idp.yourdomain.com \\
-e INSTANCE_NAME=my-idp \\
code.foss.global/idp.global/idp.global`}
</CodeBlock>
</div>
{/* Docker Compose */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Docker Compose</h2>
<p className="text-muted-foreground">
For a complete setup including MongoDB, use Docker Compose:
</p>
<CodeBlock title="docker-compose.yml">
{`version: '3.8'
services:
idp:
image: code.foss.global/idp.global/idp.global
container_name: idp-global
restart: unless-stopped
ports:
- "2999:2999"
environment:
MONGODB_URL: mongodb://mongo:27017/idp
IDP_BASEURL: https://idp.yourdomain.com
INSTANCE_NAME: my-idp
depends_on:
- mongo
networks:
- idp-network
mongo:
image: mongo:7
container_name: idp-mongo
restart: unless-stopped
volumes:
- mongo-data:/data/db
networks:
- idp-network
networks:
idp-network:
driver: bridge
volumes:
mongo-data:`}
</CodeBlock>
<CodeBlock title="Terminal">
{`# Start all services
docker-compose up -d
# View logs
docker-compose logs -f idp
# Stop services
docker-compose down`}
</CodeBlock>
</div>
{/* Environment Variables */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Environment Variables</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-2 px-3 font-medium text-foreground">Variable</th>
<th className="text-left py-2 px-3 font-medium text-foreground">Required</th>
<th className="text-left py-2 px-3 font-medium text-foreground">Description</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
<tr>
<td className="py-2 px-3">
<code className="text-primary">MONGODB_URL</code>
</td>
<td className="py-2 px-3 text-green-600 dark:text-green-400">Yes</td>
<td className="py-2 px-3 text-muted-foreground">MongoDB connection string</td>
</tr>
<tr>
<td className="py-2 px-3">
<code className="text-primary">IDP_BASEURL</code>
</td>
<td className="py-2 px-3 text-green-600 dark:text-green-400">Yes</td>
<td className="py-2 px-3 text-muted-foreground">Public URL of your instance (with https://)</td>
</tr>
<tr>
<td className="py-2 px-3">
<code className="text-primary">INSTANCE_NAME</code>
</td>
<td className="py-2 px-3 text-muted-foreground">No</td>
<td className="py-2 px-3 text-muted-foreground">Display name for this instance (default: idp.global)</td>
</tr>
<tr>
<td className="py-2 px-3">
<code className="text-primary">SERVEZONE_PLATFORM_AUTHORIZATION</code>
</td>
<td className="py-2 px-3 text-muted-foreground">No</td>
<td className="py-2 px-3 text-muted-foreground">ServeZone platform auth token (optional)</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* Reverse Proxy */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Reverse Proxy Setup</h2>
<p className="text-muted-foreground">
For production, configure a reverse proxy with SSL. Here's an example nginx configuration:
</p>
<CodeBlock title="nginx.conf">
{`server {
listen 443 ssl http2;
server_name idp.yourdomain.com;
ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;
location / {
proxy_pass http://localhost:2999;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}`}
</CodeBlock>
</div>
{/* Health Check */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Health Check</h2>
<p className="text-muted-foreground">
Verify your instance is running correctly:
</p>
<CodeBlock title="Terminal">
{`# Check if the server is responding
curl https://idp.yourdomain.com
# Check OIDC discovery endpoint
curl https://idp.yourdomain.com/.well-known/openid-configuration`}
</CodeBlock>
</div>
{/* Navigation */}
<div className="flex justify-between pt-8 border-t border-border">
<Button variant="ghost" asChild>
<Link to="/docs/quick-start">
<ArrowLeft className="mr-2 h-4 w-4" />
Quick Start
</Link>
</Button>
<Button variant="ghost" asChild>
<Link to="/docs/configuration">
Configuration
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</div>
);
};
export default Docker;

View File

@@ -0,0 +1,142 @@
import React from 'react';
import { Link, Outlet, useLocation } from 'react-router-dom';
import { cn } from '@/lib/utils';
import { Globe, BookOpen, Rocket, Container, Code2, Key, Building2, Terminal, Users, ExternalLink } from 'lucide-react';
import { ThemeToggle } from '@/components/ThemeToggle';
import { Button } from '@/components/ui/button';
const navigation = [
{
title: 'Getting Started',
items: [
{ title: 'Introduction', href: '/docs', icon: BookOpen },
{ title: 'Quick Start', href: '/docs/quick-start', icon: Rocket },
]
},
{
title: 'Deployment',
items: [
{ title: 'Docker', href: '/docs/docker', icon: Container },
{ title: 'Configuration', href: '/docs/configuration', icon: Terminal },
]
},
{
title: 'Integration',
items: [
{ title: 'TypeScript SDK', href: '/docs/sdk', icon: Code2 },
{ title: 'OIDC / OAuth 2.0', href: '/docs/oidc', icon: Key },
]
},
{
title: 'Features',
items: [
{ title: 'Organizations', href: '/docs/organizations', icon: Building2 },
{ title: 'User Management', href: '/docs/users', icon: Users },
]
},
];
const DocsLayout = () => {
const location = useLocation();
return (
<div className="h-screen flex flex-col bg-background overflow-hidden">
{/* Header */}
<header className="shrink-0 z-50 w-full border-b border-border bg-background">
<div className="flex h-14 items-center px-4 md:px-6">
<Link to="/" className="flex items-center gap-2 mr-6 group">
<Globe className="h-5 w-5 text-foreground/80 group-hover:text-primary transition-colors" />
<span className="text-base font-semibold tracking-tight text-foreground">idp.global</span>
</Link>
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<span>/</span>
<span className="font-medium text-foreground">Documentation</span>
</div>
<div className="ml-auto flex items-center gap-3">
<a
href="https://community.foss.global"
className="text-sm text-muted-foreground hover:text-foreground transition-colors hidden md:flex items-center gap-1"
>
Community
<ExternalLink className="h-3 w-3" />
</a>
<a
href="https://code.foss.global/idp.global/idp.global"
className="text-sm text-muted-foreground hover:text-foreground transition-colors hidden md:flex items-center gap-1"
>
Source
<ExternalLink className="h-3 w-3" />
</a>
<ThemeToggle />
<Button size="sm" className="h-8" asChild>
<a href="https://idp.global">Sign In</a>
</Button>
</div>
</div>
</header>
{/* Content area with independent scrolling */}
<div className="flex flex-1 overflow-hidden">
{/* Sidebar - independent scroll */}
<aside className="hidden md:flex flex-col w-64 border-r border-border shrink-0 overflow-hidden">
<div className="flex-1 overflow-y-auto py-6 px-4">
<nav className="space-y-6">
{navigation.map((section) => (
<div key={section.title}>
<h4 className="text-xs font-semibold uppercase tracking-widest text-muted-foreground mb-2 px-2">
{section.title}
</h4>
<ul className="space-y-1">
{section.items.map((item) => {
const Icon = item.icon;
const isActive = location.pathname === item.href;
return (
<li key={item.href}>
<Link
to={item.href}
className={cn(
"flex items-center gap-2 px-2 py-1.5 text-sm rounded-md transition-colors",
isActive
? "bg-primary/10 text-primary font-medium"
: "text-muted-foreground hover:text-foreground hover:bg-muted"
)}
>
<Icon className="h-4 w-4" />
{item.title}
</Link>
</li>
);
})}
</ul>
</div>
))}
</nav>
{/* Community CTA */}
<div className="mt-8 p-4 rounded-lg border border-border bg-muted/30">
<h4 className="text-sm font-semibold text-foreground mb-1">Need Help?</h4>
<p className="text-xs text-muted-foreground mb-3">
Join our community for support and discussions.
</p>
<Button variant="outline" size="sm" className="w-full" asChild>
<a href="https://community.foss.global">
Visit Community
<ExternalLink className="ml-2 h-3 w-3" />
</a>
</Button>
</div>
</div>
</aside>
{/* Main content - independent scroll */}
<main className="flex-1 overflow-y-auto">
<div className="max-w-4xl mx-auto px-4 md:px-8 py-8 md:py-12">
<Outlet />
</div>
</main>
</div>
</div>
);
};
export default DocsLayout;

View File

@@ -0,0 +1,162 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { ArrowRight, Building2, Key, Users, Shield, Container, Code2 } from 'lucide-react';
const Introduction = () => {
return (
<div className="space-y-8">
{/* Header */}
<div className="space-y-4">
<h1 className="text-4xl font-bold tracking-tight text-foreground">
idp.global Documentation
</h1>
<p className="text-xl text-muted-foreground leading-relaxed">
A modern, Open Source Identity Provider (IdP) for managing user authentication,
organizations, and role-based access control. Built with TypeScript for SMEs and enterprises.
</p>
</div>
{/* Quick links */}
<div className="grid sm:grid-cols-2 gap-4">
<Card className="p-5 hover:border-primary/50 transition-colors">
<Link to="/docs/quick-start" className="block space-y-2">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-foreground">Quick Start</h3>
<ArrowRight className="h-4 w-4 text-muted-foreground" />
</div>
<p className="text-sm text-muted-foreground">
Get up and running with idp.global in under 5 minutes.
</p>
</Link>
</Card>
<Card className="p-5 hover:border-primary/50 transition-colors">
<Link to="/docs/docker" className="block space-y-2">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-foreground">Docker Deployment</h3>
<ArrowRight className="h-4 w-4 text-muted-foreground" />
</div>
<p className="text-sm text-muted-foreground">
Deploy your own instance with Docker in minutes.
</p>
</Link>
</Card>
</div>
{/* Features overview */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Core Features</h2>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{[
{
icon: Users,
title: 'User Management',
description: 'Email/password, magic links, API tokens, password reset, and device management.'
},
{
icon: Building2,
title: 'Organizations',
description: 'Multi-tenant architecture with member invitations and ownership transfer.'
},
{
icon: Shield,
title: 'Role-Based Access',
description: 'Fine-grained RBAC with admin, member, and custom roles.'
},
{
icon: Key,
title: 'OIDC Provider',
description: 'Full OpenID Connect compliance for third-party app SSO.'
},
{
icon: Container,
title: 'Self-Hostable',
description: 'Deploy anywhere with Docker or use our free hosted platform.'
},
{
icon: Code2,
title: 'TypeScript SDK',
description: 'Type-safe client libraries for browser and Node.js.'
},
].map((feature) => {
const Icon = feature.icon;
return (
<div key={feature.title} className="p-4 rounded-lg border border-border bg-card">
<Icon className="h-5 w-5 text-primary mb-2" />
<h3 className="font-medium text-foreground mb-1">{feature.title}</h3>
<p className="text-sm text-muted-foreground">{feature.description}</p>
</div>
);
})}
</div>
</div>
{/* Architecture */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Architecture</h2>
<p className="text-muted-foreground">
idp.global is built as a modular TypeScript monorepo with the following structure:
</p>
<div className="bg-muted/50 rounded-lg p-4 font-mono text-sm">
<pre className="text-muted-foreground overflow-x-auto">{`├── ts/ # Server-side code (Node.js)
│ └── reception/ # Core identity management logic
├── ts_interfaces/ # Shared TypeScript interfaces
├── ts_idpclient/ # Browser/Node client library
├── ts_idpcli/ # Command-line interface tool
└── ts_web/ # Web frontend components`}</pre>
</div>
</div>
{/* Published packages */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">NPM Packages</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-2 px-3 font-medium text-foreground">Package</th>
<th className="text-left py-2 px-3 font-medium text-foreground">Description</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
<tr>
<td className="py-2 px-3">
<code className="text-primary">@idp.global/interfaces</code>
</td>
<td className="py-2 px-3 text-muted-foreground">TypeScript interfaces for API contracts</td>
</tr>
<tr>
<td className="py-2 px-3">
<code className="text-primary">@idp.global/idpclient</code>
</td>
<td className="py-2 px-3 text-muted-foreground">Client library for browser and Node.js</td>
</tr>
<tr>
<td className="py-2 px-3">
<code className="text-primary">@idp.global/web</code>
</td>
<td className="py-2 px-3 text-muted-foreground">Web UI components</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* CTA */}
<div className="flex flex-wrap gap-4 pt-4">
<Button asChild>
<Link to="/docs/quick-start">
Get Started
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
<Button variant="outline" asChild>
<a href="https://idp.global">Try Free Platform</a>
</Button>
</div>
</div>
);
};
export default Introduction;

224
src/pages/docs/OIDC.tsx Normal file
View File

@@ -0,0 +1,224 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { ArrowRight, ArrowLeft } from 'lucide-react';
const CodeBlock = ({ children, title }: { children: string; title?: string }) => (
<div className="rounded-lg border border-border overflow-hidden">
{title && (
<div className="px-4 py-2 bg-muted/50 border-b border-border text-xs font-medium text-muted-foreground">
{title}
</div>
)}
<pre className="p-4 overflow-x-auto bg-muted/30">
<code className="text-sm font-mono text-foreground">{children}</code>
</pre>
</div>
);
const OIDC = () => {
return (
<div className="space-y-8">
{/* Header */}
<div className="space-y-4">
<h1 className="text-4xl font-bold tracking-tight text-foreground">
OIDC / OAuth 2.0
</h1>
<p className="text-xl text-muted-foreground leading-relaxed">
idp.global implements a full OpenID Connect provider. Use it for SSO across
your applications with standard OAuth 2.0 flows.
</p>
</div>
{/* Discovery */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Discovery Document</h2>
<p className="text-muted-foreground">
The OIDC discovery endpoint provides all configuration information:
</p>
<CodeBlock title="Request">
{`GET https://idp.global/.well-known/openid-configuration`}
</CodeBlock>
<CodeBlock title="Response">
{`{
"issuer": "https://idp.global",
"authorization_endpoint": "https://idp.global/oauth/authorize",
"token_endpoint": "https://idp.global/oauth/token",
"userinfo_endpoint": "https://idp.global/oauth/userinfo",
"jwks_uri": "https://idp.global/.well-known/jwks.json",
"revocation_endpoint": "https://idp.global/oauth/revoke",
"scopes_supported": ["openid", "profile", "email", "organizations", "roles"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"]
}`}
</CodeBlock>
</div>
{/* Register Your App */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Register Your Application</h2>
<p className="text-muted-foreground">
Before integrating, register your application in the idp.global dashboard to get:
</p>
<ul className="list-disc list-inside text-muted-foreground space-y-1 ml-2">
<li><strong>Client ID</strong> - Your application's public identifier</li>
<li><strong>Client Secret</strong> - Keep this secure, used for token exchange</li>
<li><strong>Redirect URIs</strong> - Allowed callback URLs after authentication</li>
</ul>
</div>
{/* Authorization Code Flow */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Authorization Code Flow (with PKCE)</h2>
<p className="text-muted-foreground">
The recommended flow for web and mobile applications.
</p>
<h3 className="text-lg font-medium text-foreground">Step 1: Generate PKCE Challenge</h3>
<CodeBlock title="JavaScript">
{`// Generate code verifier (random string)
function generateCodeVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return btoa(String.fromCharCode(...array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// Generate code challenge from verifier
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode(...new Uint8Array(hash)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Store codeVerifier for later use`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">Step 2: Redirect to Authorization</h3>
<CodeBlock title="URL">
{`https://idp.global/oauth/authorize?
client_id=your-client-id&
redirect_uri=https://yourapp.com/callback&
response_type=code&
scope=openid profile email organizations&
state=random-state-value&
code_challenge=YOUR_CODE_CHALLENGE&
code_challenge_method=S256`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">Step 3: Handle Callback</h3>
<p className="text-muted-foreground">
After user authenticates, they're redirected to your callback URL with an authorization code:
</p>
<CodeBlock title="Callback URL">
{`https://yourapp.com/callback?code=AUTHORIZATION_CODE&state=random-state-value`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">Step 4: Exchange Code for Tokens</h3>
<CodeBlock title="Request">
{`POST https://idp.global/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=https://yourapp.com/callback&
client_id=your-client-id&
client_secret=your-client-secret&
code_verifier=YOUR_CODE_VERIFIER`}
</CodeBlock>
<CodeBlock title="Response">
{`{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}`}
</CodeBlock>
</div>
{/* UserInfo */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">UserInfo Endpoint</h2>
<p className="text-muted-foreground">
Retrieve user information using the access token:
</p>
<CodeBlock title="Request">
{`GET https://idp.global/oauth/userinfo
Authorization: Bearer ACCESS_TOKEN`}
</CodeBlock>
<CodeBlock title="Response">
{`{
"sub": "user-uuid-here",
"name": "John Doe",
"email": "john@example.com",
"email_verified": true,
"organizations": [
{
"id": "org-uuid",
"name": "Acme Corp",
"slug": "acme",
"roles": ["admin", "member"]
}
],
"roles": ["user"]
}`}
</CodeBlock>
</div>
{/* Token Refresh */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Refresh Tokens</h2>
<CodeBlock title="Request">
{`POST https://idp.global/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token=YOUR_REFRESH_TOKEN&
client_id=your-client-id&
client_secret=your-client-secret`}
</CodeBlock>
</div>
{/* Token Revocation */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Token Revocation</h2>
<CodeBlock title="Request">
{`POST https://idp.global/oauth/revoke
Content-Type: application/x-www-form-urlencoded
token=TOKEN_TO_REVOKE&
client_id=your-client-id&
client_secret=your-client-secret`}
</CodeBlock>
</div>
{/* Navigation */}
<div className="flex justify-between pt-8 border-t border-border">
<Button variant="ghost" asChild>
<Link to="/docs/sdk">
<ArrowLeft className="mr-2 h-4 w-4" />
TypeScript SDK
</Link>
</Button>
<Button variant="ghost" asChild>
<Link to="/docs/organizations">
Organizations
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</div>
);
};
export default OIDC;

View File

@@ -0,0 +1,233 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { ArrowRight, ArrowLeft } from 'lucide-react';
const CodeBlock = ({ children, title }: { children: string; title?: string }) => (
<div className="rounded-lg border border-border overflow-hidden">
{title && (
<div className="px-4 py-2 bg-muted/50 border-b border-border text-xs font-medium text-muted-foreground">
{title}
</div>
)}
<pre className="p-4 overflow-x-auto bg-muted/30">
<code className="text-sm font-mono text-foreground">{children}</code>
</pre>
</div>
);
const Organizations = () => {
return (
<div className="space-y-8">
{/* Header */}
<div className="space-y-4">
<h1 className="text-4xl font-bold tracking-tight text-foreground">
Organizations
</h1>
<p className="text-xl text-muted-foreground leading-relaxed">
idp.global supports multi-tenant architecture with organizations. Users can belong to
multiple organizations with different roles in each.
</p>
</div>
{/* Concepts */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Key Concepts</h2>
<div className="grid sm:grid-cols-2 gap-4">
<div className="p-4 rounded-lg border border-border bg-card">
<h3 className="font-medium text-foreground mb-1">Organization</h3>
<p className="text-sm text-muted-foreground">
A container for users, roles, and applications. Typically represents a company, team, or project.
</p>
</div>
<div className="p-4 rounded-lg border border-border bg-card">
<h3 className="font-medium text-foreground mb-1">Member</h3>
<p className="text-sm text-muted-foreground">
A user who belongs to an organization. Members can have one or more roles.
</p>
</div>
<div className="p-4 rounded-lg border border-border bg-card">
<h3 className="font-medium text-foreground mb-1">Role</h3>
<p className="text-sm text-muted-foreground">
Defines permissions within an organization. Built-in roles include admin and member.
</p>
</div>
<div className="p-4 rounded-lg border border-border bg-card">
<h3 className="font-medium text-foreground mb-1">Owner</h3>
<p className="text-sm text-muted-foreground">
The user who created the organization. Has full control and can transfer ownership.
</p>
</div>
</div>
</div>
{/* Creating Organizations */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Creating Organizations</h2>
<CodeBlock title="TypeScript">
{`import { IdpClient } from '@idp.global/idpclient';
const idpClient = new IdpClient('https://idp.global');
// Create a new organization
const result = await idpClient.createOrganization(
'Acme Corporation', // Display name
'acme-corp', // URL-friendly slug (must be unique)
'manifest' // Organization type
);
if (result.resultingOrganization) {
console.log('Created:', result.resultingOrganization.id);
console.log('Slug:', result.resultingOrganization.slug);
}`}
</CodeBlock>
</div>
{/* Member Management */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Member Management</h2>
<h3 className="text-lg font-medium text-foreground">Invite Members</h3>
<CodeBlock title="TypeScript">
{`// Send invitation email to new member
await idpClient.requests.createInvitation.fire({
jwt: await idpClient.getJwt(),
organizationId: 'org-uuid',
email: 'newmember@example.com',
roles: ['member'] // Roles to assign
});
// Invite as admin
await idpClient.requests.createInvitation.fire({
jwt: await idpClient.getJwt(),
organizationId: 'org-uuid',
email: 'admin@example.com',
roles: ['admin', 'member']
});`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">List Members</h3>
<CodeBlock title="TypeScript">
{`const members = await idpClient.requests.getOrgMembers.fire({
jwt: await idpClient.getJwt(),
organizationId: 'org-uuid'
});
for (const member of members.members) {
console.log(member.user.email, member.roles);
}`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">Update Member Roles</h3>
<CodeBlock title="TypeScript">
{`await idpClient.requests.updateMemberRoles.fire({
jwt: await idpClient.getJwt(),
organizationId: 'org-uuid',
userId: 'user-uuid',
roles: ['admin', 'member']
});`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">Remove Members</h3>
<CodeBlock title="TypeScript">
{`await idpClient.requests.removeMember.fire({
jwt: await idpClient.getJwt(),
organizationId: 'org-uuid',
userId: 'user-uuid'
});`}
</CodeBlock>
</div>
{/* Role-Based Access */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Built-in Roles</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-2 px-3 font-medium text-foreground">Role</th>
<th className="text-left py-2 px-3 font-medium text-foreground">Permissions</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
<tr>
<td className="py-2 px-3"><code className="text-primary">admin</code></td>
<td className="py-2 px-3 text-muted-foreground">
Full organization management: invite/remove members, manage roles, update settings, register apps
</td>
</tr>
<tr>
<td className="py-2 px-3"><code className="text-primary">member</code></td>
<td className="py-2 px-3 text-muted-foreground">
Basic access: view organization, access approved applications
</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* Ownership Transfer */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Ownership Transfer</h2>
<p className="text-muted-foreground">
Organization owners can transfer ownership to another admin member:
</p>
<CodeBlock title="TypeScript">
{`await idpClient.requests.transferOwnership.fire({
jwt: await idpClient.getJwt(),
organizationId: 'org-uuid',
newOwnerId: 'new-owner-user-uuid'
});`}
</CodeBlock>
</div>
{/* Organizations in JWT */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Organizations in JWT Claims</h2>
<p className="text-muted-foreground">
When requesting the <code className="text-primary">organizations</code> scope, the user's organizations
are included in the ID token and userinfo response:
</p>
<CodeBlock title="JWT Payload">
{`{
"sub": "user-uuid",
"email": "user@example.com",
"organizations": [
{
"id": "org-uuid-1",
"name": "Acme Corp",
"slug": "acme",
"roles": ["admin", "member"]
},
{
"id": "org-uuid-2",
"name": "Other Org",
"slug": "other",
"roles": ["member"]
}
]
}`}
</CodeBlock>
</div>
{/* Navigation */}
<div className="flex justify-between pt-8 border-t border-border">
<Button variant="ghost" asChild>
<Link to="/docs/oidc">
<ArrowLeft className="mr-2 h-4 w-4" />
OIDC / OAuth 2.0
</Link>
</Button>
<Button variant="ghost" asChild>
<Link to="/docs/users">
User Management
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</div>
);
};
export default Organizations;

View File

@@ -0,0 +1,182 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { ArrowRight, ArrowLeft, ExternalLink } from 'lucide-react';
const CodeBlock = ({ children, title }: { children: string; title?: string }) => (
<div className="rounded-lg border border-border overflow-hidden">
{title && (
<div className="px-4 py-2 bg-muted/50 border-b border-border text-xs font-medium text-muted-foreground">
{title}
</div>
)}
<pre className="p-4 overflow-x-auto bg-muted/30">
<code className="text-sm font-mono text-foreground">{children}</code>
</pre>
</div>
);
const QuickStart = () => {
return (
<div className="space-y-8">
{/* Header */}
<div className="space-y-4">
<h1 className="text-4xl font-bold tracking-tight text-foreground">
Quick Start
</h1>
<p className="text-xl text-muted-foreground leading-relaxed">
Get started with idp.global in under 5 minutes. Choose between using our
free hosted platform or self-hosting with Docker.
</p>
</div>
{/* Option 1: Hosted Platform */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Option 1: Use Free Hosted Platform</h2>
<p className="text-muted-foreground">
The fastest way to get started. Create your free account on idp.global and start using
it immediately. No installation required.
</p>
<div className="space-y-3">
<div className="flex items-start gap-3">
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-xs font-bold shrink-0">1</span>
<div>
<p className="font-medium text-foreground">Create your account</p>
<p className="text-sm text-muted-foreground">
Visit <a href="https://idp.global" className="text-primary hover:underline">idp.global</a> and sign up for a free account.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-xs font-bold shrink-0">2</span>
<div>
<p className="font-medium text-foreground">Create an organization</p>
<p className="text-sm text-muted-foreground">
Set up your first organization and invite team members.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<span className="flex items-center justify-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-xs font-bold shrink-0">3</span>
<div>
<p className="font-medium text-foreground">Register your application</p>
<p className="text-sm text-muted-foreground">
Create an OAuth/OIDC client for your app and get your credentials.
</p>
</div>
</div>
</div>
<Button asChild>
<a href="https://idp.global">
Get Started Free
<ExternalLink className="ml-2 h-4 w-4" />
</a>
</Button>
</div>
{/* Option 2: Self-Host */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Option 2: Self-Host with Docker</h2>
<p className="text-muted-foreground">
Deploy your own instance for complete control over your data and infrastructure.
</p>
<h3 className="text-lg font-medium text-foreground">Prerequisites</h3>
<ul className="list-disc list-inside text-muted-foreground space-y-1 ml-2">
<li>Docker installed on your server</li>
<li>MongoDB instance (local or cloud)</li>
<li>A domain name pointing to your server</li>
</ul>
<h3 className="text-lg font-medium text-foreground">1. Pull the Docker image</h3>
<CodeBlock title="Terminal">
{`docker pull code.foss.global/idp.global/idp.global`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">2. Run the container</h3>
<CodeBlock title="Terminal">
{`docker run -d \\
-p 2999:2999 \\
-e MONGODB_URL=mongodb://your-mongo:27017/idp \\
-e IDP_BASEURL=https://your-domain.com \\
-e INSTANCE_NAME=my-idp \\
code.foss.global/idp.global/idp.global`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">3. Access your instance</h3>
<p className="text-muted-foreground">
Your idp.global instance is now running on port 2999. Configure your reverse proxy
(nginx, traefik, etc.) to handle HTTPS and route traffic to the container.
</p>
<Button variant="outline" asChild>
<Link to="/docs/docker">
Full Docker Guide
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
{/* Integrate with your app */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Integrate with Your Application</h2>
<p className="text-muted-foreground">
Once you have access to an idp.global instance, install the TypeScript client:
</p>
<CodeBlock title="Terminal">
{`npm install @idp.global/idpclient`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">Basic Usage</h3>
<CodeBlock title="app.ts">
{`import { IdpClient } from '@idp.global/idpclient';
// Initialize the client
const idpClient = new IdpClient('https://idp.global');
// Enable WebSocket connection for real-time updates
await idpClient.enableTypedSocket();
// Check if user is logged in
const isLoggedIn = await idpClient.determineLoginStatus();
if (isLoggedIn) {
// Get current user info
const userInfo = await idpClient.whoIs();
console.log('User:', userInfo.user);
// Get user's organizations
const orgs = await idpClient.getRolesAndOrganizations();
console.log('Organizations:', orgs.organizations);
}`}
</CodeBlock>
<Button variant="outline" asChild>
<Link to="/docs/sdk">
Full SDK Documentation
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
{/* Navigation */}
<div className="flex justify-between pt-8 border-t border-border">
<Button variant="ghost" asChild>
<Link to="/docs">
<ArrowLeft className="mr-2 h-4 w-4" />
Introduction
</Link>
</Button>
<Button variant="ghost" asChild>
<Link to="/docs/docker">
Docker Deployment
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</div>
);
};
export default QuickStart;

219
src/pages/docs/SDK.tsx Normal file
View File

@@ -0,0 +1,219 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { ArrowRight, ArrowLeft } from 'lucide-react';
const CodeBlock = ({ children, title }: { children: string; title?: string }) => (
<div className="rounded-lg border border-border overflow-hidden">
{title && (
<div className="px-4 py-2 bg-muted/50 border-b border-border text-xs font-medium text-muted-foreground">
{title}
</div>
)}
<pre className="p-4 overflow-x-auto bg-muted/30">
<code className="text-sm font-mono text-foreground">{children}</code>
</pre>
</div>
);
const SDK = () => {
return (
<div className="space-y-8">
{/* Header */}
<div className="space-y-4">
<h1 className="text-4xl font-bold tracking-tight text-foreground">
TypeScript SDK
</h1>
<p className="text-xl text-muted-foreground leading-relaxed">
The official TypeScript client library for idp.global. Works in both browser and Node.js
environments with full type safety.
</p>
</div>
{/* Installation */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Installation</h2>
<CodeBlock title="Terminal">
{`npm install @idp.global/idpclient
# or with pnpm
pnpm add @idp.global/idpclient
# or with yarn
yarn add @idp.global/idpclient`}
</CodeBlock>
</div>
{/* Initialization */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Initialization</h2>
<CodeBlock title="client.ts">
{`import { IdpClient } from '@idp.global/idpclient';
// Connect to the hosted platform
const idpClient = new IdpClient('https://idp.global');
// Or connect to your self-hosted instance
const idpClient = new IdpClient('https://idp.yourdomain.com');
// Enable WebSocket for real-time updates
await idpClient.enableTypedSocket();`}
</CodeBlock>
</div>
{/* Authentication */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Authentication</h2>
<h3 className="text-lg font-medium text-foreground">Check Login Status</h3>
<CodeBlock title="auth.ts">
{`// Check if user is currently logged in
const isLoggedIn = await idpClient.determineLoginStatus();
if (isLoggedIn) {
console.log('User is authenticated');
}`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">Login with Email & Password</h3>
<CodeBlock title="auth.ts">
{`const response = await idpClient.requests.loginWithUserNameAndPassword.fire({
username: 'user@example.com',
password: 'securepassword'
});
if (response.refreshToken) {
// Store the refresh token securely
await idpClient.refreshJwt(response.refreshToken);
console.log('Login successful!');
} else if (response.error) {
console.error('Login failed:', response.error);
}`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">Login with Magic Link</h3>
<CodeBlock title="auth.ts">
{`// Request magic link email
await idpClient.requests.loginWithEmail.fire({
email: 'user@example.com'
});
// User clicks link in email, which contains a token
// Your callback page handles the token:
await idpClient.requests.verifyMagicLink.fire({
token: tokenFromUrl
});`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">Get Current User</h3>
<CodeBlock title="user.ts">
{`// Get current user information
const userInfo = await idpClient.whoIs();
console.log('User ID:', userInfo.user.id);
console.log('Email:', userInfo.user.email);
console.log('Name:', userInfo.user.name);`}
</CodeBlock>
</div>
{/* Organizations */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Organizations</h2>
<h3 className="text-lg font-medium text-foreground">Get User's Organizations</h3>
<CodeBlock title="organizations.ts">
{`const result = await idpClient.getRolesAndOrganizations();
for (const org of result.organizations) {
console.log('Organization:', org.name);
console.log('Slug:', org.slug);
console.log('Your roles:', org.roles);
}`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">Create Organization</h3>
<CodeBlock title="organizations.ts">
{`const result = await idpClient.createOrganization(
'My Company', // Display name
'my-company', // URL slug
'manifest' // Type
);
console.log('Created:', result.resultingOrganization);`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">Invite Members</h3>
<CodeBlock title="organizations.ts">
{`await idpClient.requests.createInvitation.fire({
jwt: await idpClient.getJwt(),
organizationId: 'org-id',
email: 'newmember@example.com',
roles: ['member']
});`}
</CodeBlock>
</div>
{/* JWT Handling */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">JWT Handling</h2>
<CodeBlock title="jwt.ts">
{`// Get current JWT for API calls
const jwt = await idpClient.getJwt();
// Use in Authorization header
fetch('/api/protected', {
headers: {
'Authorization': \`Bearer \${jwt}\`
}
});
// Refresh JWT with refresh token
await idpClient.refreshJwt(refreshToken);
// Logout (clears tokens)
await idpClient.logout();`}
</CodeBlock>
</div>
{/* TypeScript Interfaces */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">TypeScript Interfaces</h2>
<p className="text-muted-foreground">
All API types are available from <code className="text-primary">@idp.global/interfaces</code>:
</p>
<CodeBlock title="types.ts">
{`import type {
IUser,
IOrganization,
IRole,
IJwt,
IApp
} from '@idp.global/interfaces';
// Use in your application
function handleUser(user: IUser) {
console.log(user.email);
}`}
</CodeBlock>
</div>
{/* Navigation */}
<div className="flex justify-between pt-8 border-t border-border">
<Button variant="ghost" asChild>
<Link to="/docs/configuration">
<ArrowLeft className="mr-2 h-4 w-4" />
Configuration
</Link>
</Button>
<Button variant="ghost" asChild>
<Link to="/docs/oidc">
OIDC / OAuth 2.0
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</div>
);
};
export default SDK;

253
src/pages/docs/Users.tsx Normal file
View File

@@ -0,0 +1,253 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { ArrowLeft, ExternalLink } from 'lucide-react';
const CodeBlock = ({ children, title }: { children: string; title?: string }) => (
<div className="rounded-lg border border-border overflow-hidden">
{title && (
<div className="px-4 py-2 bg-muted/50 border-b border-border text-xs font-medium text-muted-foreground">
{title}
</div>
)}
<pre className="p-4 overflow-x-auto bg-muted/30">
<code className="text-sm font-mono text-foreground">{children}</code>
</pre>
</div>
);
const Users = () => {
return (
<div className="space-y-8">
{/* Header */}
<div className="space-y-4">
<h1 className="text-4xl font-bold tracking-tight text-foreground">
User Management
</h1>
<p className="text-xl text-muted-foreground leading-relaxed">
Complete user lifecycle management from registration to profile updates,
with multiple authentication methods and security features.
</p>
</div>
{/* Authentication Methods */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Authentication Methods</h2>
<div className="grid sm:grid-cols-2 gap-4">
<div className="p-4 rounded-lg border border-border bg-card">
<h3 className="font-medium text-foreground mb-1">Email & Password</h3>
<p className="text-sm text-muted-foreground">
Traditional authentication with secure password hashing and optional 2FA.
</p>
</div>
<div className="p-4 rounded-lg border border-border bg-card">
<h3 className="font-medium text-foreground mb-1">Magic Links</h3>
<p className="text-sm text-muted-foreground">
Passwordless authentication via email. Secure, time-limited tokens.
</p>
</div>
<div className="p-4 rounded-lg border border-border bg-card">
<h3 className="font-medium text-foreground mb-1">API Tokens</h3>
<p className="text-sm text-muted-foreground">
Long-lived tokens for programmatic access and CI/CD pipelines.
</p>
</div>
<div className="p-4 rounded-lg border border-border bg-card">
<h3 className="font-medium text-foreground mb-1">OAuth / OIDC</h3>
<p className="text-sm text-muted-foreground">
SSO via third-party applications using standard protocols.
</p>
</div>
</div>
</div>
{/* Registration */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">User Registration</h2>
<CodeBlock title="TypeScript">
{`import { IdpClient } from '@idp.global/idpclient';
const idpClient = new IdpClient('https://idp.global');
// Start registration - sends verification email
const result = await idpClient.requests.firstRegistration.fire({
email: 'newuser@example.com'
});
if (result.success) {
console.log('Verification email sent');
}
// User clicks verification link, then completes registration
await idpClient.requests.finishRegistration.fire({
token: verificationTokenFromEmail,
password: 'securePassword123',
name: 'John Doe'
});`}
</CodeBlock>
</div>
{/* Profile Management */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Profile Management</h2>
<h3 className="text-lg font-medium text-foreground">Get User Data</h3>
<CodeBlock title="TypeScript">
{`const userData = await idpClient.requests.getUserData.fire({
jwt: await idpClient.getJwt()
});
console.log('Name:', userData.user.name);
console.log('Email:', userData.user.email);
console.log('Created:', userData.user.createdAt);`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">Update Profile</h3>
<CodeBlock title="TypeScript">
{`await idpClient.requests.setUserData.fire({
jwt: await idpClient.getJwt(),
name: 'John Updated',
// Other profile fields as needed
});`}
</CodeBlock>
</div>
{/* Password Management */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Password Management</h2>
<h3 className="text-lg font-medium text-foreground">Change Password</h3>
<CodeBlock title="TypeScript">
{`await idpClient.requests.changePassword.fire({
jwt: await idpClient.getJwt(),
currentPassword: 'oldPassword',
newPassword: 'newSecurePassword'
});`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">Password Reset Flow</h3>
<CodeBlock title="TypeScript">
{`// Request password reset email
await idpClient.requests.requestPasswordReset.fire({
email: 'user@example.com'
});
// User clicks reset link, then sets new password
await idpClient.requests.resetPassword.fire({
token: resetTokenFromEmail,
newPassword: 'newSecurePassword'
});`}
</CodeBlock>
</div>
{/* Session Management */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Session Management</h2>
<h3 className="text-lg font-medium text-foreground">List Active Sessions</h3>
<CodeBlock title="TypeScript">
{`const sessions = await idpClient.requests.getUserSessions.fire({
jwt: await idpClient.getJwt()
});
for (const session of sessions.sessions) {
console.log('Device:', session.userAgent);
console.log('Last active:', session.lastActiveAt);
console.log('IP:', session.ipAddress);
}`}
</CodeBlock>
<h3 className="text-lg font-medium text-foreground">Revoke Session</h3>
<CodeBlock title="TypeScript">
{`// Revoke a specific session
await idpClient.requests.revokeSession.fire({
jwt: await idpClient.getJwt(),
sessionId: 'session-uuid'
});
// Revoke all sessions except current
await idpClient.requests.revokeAllSessions.fire({
jwt: await idpClient.getJwt()
});`}
</CodeBlock>
</div>
{/* Two-Factor Authentication */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">Two-Factor Authentication</h2>
<p className="text-muted-foreground">
Users can enable 2FA for enhanced account security:
</p>
<CodeBlock title="TypeScript">
{`// Enable 2FA - returns QR code data for authenticator app
const setup = await idpClient.requests.enable2FA.fire({
jwt: await idpClient.getJwt()
});
console.log('Scan QR code:', setup.qrCodeUrl);
console.log('Or enter manually:', setup.secret);
// Verify and activate 2FA
await idpClient.requests.verify2FA.fire({
jwt: await idpClient.getJwt(),
code: '123456' // Code from authenticator app
});
// Disable 2FA
await idpClient.requests.disable2FA.fire({
jwt: await idpClient.getJwt(),
code: '123456' // Requires valid 2FA code
});`}
</CodeBlock>
</div>
{/* User Data Model */}
<div className="space-y-4">
<h2 className="text-2xl font-semibold text-foreground">User Data Model</h2>
<CodeBlock title="TypeScript Interface">
{`interface IUser {
id: string;
email: string;
emailVerified: boolean;
name: string;
createdAt: string;
updatedAt: string;
twoFactorEnabled: boolean;
// Additional fields based on configuration
}`}
</CodeBlock>
</div>
{/* Help */}
<div className="p-4 rounded-lg border border-border bg-muted/30">
<h3 className="font-medium text-foreground mb-2">Need Help?</h3>
<p className="text-sm text-muted-foreground mb-3">
Join our community for support, discussions, and feature requests.
</p>
<Button variant="outline" size="sm" asChild>
<a href="https://community.foss.global">
Visit Community
<ExternalLink className="ml-2 h-3 w-3" />
</a>
</Button>
</div>
{/* Navigation */}
<div className="flex justify-between pt-8 border-t border-border">
<Button variant="ghost" asChild>
<Link to="/docs/organizations">
<ArrowLeft className="mr-2 h-4 w-4" />
Organizations
</Link>
</Button>
<Button variant="ghost" asChild>
<Link to="/docs">
Back to Overview
</Link>
</Button>
</div>
</div>
);
};
export default Users;

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

126
tailwind.config.ts Normal file
View File

@@ -0,0 +1,126 @@
import type { Config } from "tailwindcss";
export default {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
prefix: "",
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px'
}
},
extend: {
colors: {
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))'
},
idp: {
blue: '#0056b3',
teal: '#33C3F0',
darkBlue: '#003366',
lightBlue: '#e6f7ff',
gray: '#f5f5f5',
darkGray: '#333333'
}
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' }
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' }
},
'fade-in': {
'0%': { opacity: '0' },
'100%': { opacity: '1' }
},
'fade-in-up': {
'0%': { opacity: '0', transform: 'translateY(16px)' },
'100%': { opacity: '1', transform: 'translateY(0)' }
},
'scale-in': {
'0%': { opacity: '0', transform: 'scale(0.96)' },
'100%': { opacity: '1', transform: 'scale(1)' }
},
'glow-pulse': {
'0%, 100%': { boxShadow: '0 0 20px rgba(59, 130, 246, 0.15)' },
'50%': { boxShadow: '0 0 40px rgba(59, 130, 246, 0.25)' }
}
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
'fade-in': 'fade-in 0.5s cubic-bezier(0.16, 1, 0.3, 1)',
'fade-in-up': 'fade-in-up 0.6s cubic-bezier(0.16, 1, 0.3, 1) both',
'scale-in': 'scale-in 0.4s cubic-bezier(0.16, 1, 0.3, 1)',
'glow-pulse': 'glow-pulse 3s ease-in-out infinite'
},
fontFamily: {
sans: ['Inter', 'sans-serif'],
mono: ['JetBrains Mono', 'SF Mono', 'Fira Code', 'monospace']
},
boxShadow: {
'glow-blue': '0 0 30px rgba(59, 130, 246, 0.15)',
'glow-teal': '0 0 30px rgba(6, 182, 212, 0.15)',
'glow-strong': '0 0 40px rgba(59, 130, 246, 0.25)'
}
}
},
plugins: [require("tailwindcss-animate")],
} satisfies Config;

30
tsconfig.app.json Normal file
View File

@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitAny": false,
"noFallthroughCasesInSwitch": false,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}

19
tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"noImplicitAny": false,
"noUnusedParameters": false,
"skipLibCheck": true,
"allowJs": true,
"noUnusedLocals": false,
"strictNullChecks": false
}
}

22
tsconfig.node.json Normal file
View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

17
vite.config.ts Normal file
View File

@@ -0,0 +1,17 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import path from "path";
// https://vitejs.dev/config/
export default defineConfig({
server: {
host: "::",
port: 8080,
},
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});