first commit
This commit is contained in:
45
apps/docs/components/api-endpoint-card.tsx
Normal file
45
apps/docs/components/api-endpoint-card.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { apiReference, type ApiEndpoint } from '@/lib/api-endpoints';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const methodColors: Record<ApiEndpoint['method'], string> = {
|
||||
GET: 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300',
|
||||
POST: 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300',
|
||||
PUT: 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300',
|
||||
PATCH: 'bg-violet-100 text-violet-800 dark:bg-violet-950 dark:text-violet-300',
|
||||
DELETE: 'bg-red-100 text-red-800 dark:bg-red-950 dark:text-red-300'
|
||||
};
|
||||
|
||||
export function ApiEndpointCard({ endpoint }: { endpoint: ApiEndpoint }) {
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="flex flex-col gap-2 p-4 sm:flex-row sm:items-start sm:gap-4">
|
||||
<Badge className={cn('w-fit shrink-0 font-mono', methodColors[endpoint.method])}>{endpoint.method}</Badge>
|
||||
<div className="min-w-0 flex-1">
|
||||
<code className="break-all text-sm font-medium">{endpoint.path}</code>
|
||||
<p className="mt-1 text-sm font-medium">{endpoint.summary}</p>
|
||||
{endpoint.description ? <p className="mt-1 text-sm text-zinc-500">{endpoint.description}</p> : null}
|
||||
{endpoint.auth ? <p className="mt-2 text-xs text-zinc-400">Требуется Authorization: Bearer JWT</p> : null}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function ApiReferenceSection() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{apiReference.map((group) => (
|
||||
<div key={group.tag}>
|
||||
<h3 className="mb-4 text-lg font-semibold">{group.tag}</h3>
|
||||
<div className="space-y-3">
|
||||
{group.endpoints.map((endpoint) => (
|
||||
<ApiEndpointCard key={`${endpoint.method}-${endpoint.path}`} endpoint={endpoint} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
8
apps/docs/components/auth-code-tabs.tsx
Normal file
8
apps/docs/components/auth-code-tabs.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { authLoginExamples } from '@/lib/auth-examples';
|
||||
import { CodeExampleTabs } from '@/components/code-example-tabs';
|
||||
|
||||
export function AuthLoginCodeTabs() {
|
||||
return <CodeExampleTabs examples={authLoginExamples} />;
|
||||
}
|
||||
8
apps/docs/components/auth-ldap-code-tabs.tsx
Normal file
8
apps/docs/components/auth-ldap-code-tabs.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { authLdapExamples } from '@/lib/auth-examples';
|
||||
import { CodeExampleTabs } from '@/components/code-example-tabs';
|
||||
|
||||
export function AuthLdapCodeTabs() {
|
||||
return <CodeExampleTabs examples={authLdapExamples} />;
|
||||
}
|
||||
43
apps/docs/components/code-block.tsx
Normal file
43
apps/docs/components/code-block.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function CodeBlock({ code, language, title }: { code: string; language?: string; title?: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
async function copy() {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group relative my-4 overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-800">
|
||||
{(title || language) && (
|
||||
<div className="flex items-center justify-between border-b border-zinc-200 bg-zinc-50 px-4 py-2 text-xs text-zinc-500 dark:border-zinc-800 dark:bg-zinc-900">
|
||||
<span>{title ?? language}</span>
|
||||
<Button variant="ghost" size="sm" className="h-7 px-2 opacity-0 group-hover:opacity-100" onClick={copy}>
|
||||
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
|
||||
{copied ? 'Скопировано' : 'Копировать'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<pre className={cn('overflow-x-auto bg-zinc-950 p-4 text-sm leading-relaxed text-zinc-50', !title && !language && 'rounded-xl')}>
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
{!title && !language && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-2 top-2 h-7 bg-zinc-900/80 px-2 text-zinc-300 opacity-0 group-hover:opacity-100"
|
||||
onClick={copy}
|
||||
>
|
||||
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
apps/docs/components/code-example-tabs.tsx
Normal file
26
apps/docs/components/code-example-tabs.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import type { CodeExample } from '@/lib/code-example';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { CodeBlock } from '@/components/code-block';
|
||||
|
||||
export function CodeExampleTabs({ examples }: { examples: CodeExample[] }) {
|
||||
if (examples.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Tabs defaultValue={examples[0].id} className="my-6">
|
||||
<TabsList className="mb-2 flex h-auto max-w-full flex-wrap gap-1">
|
||||
{examples.map((example) => (
|
||||
<TabsTrigger key={example.id} value={example.id} className="text-xs sm:text-sm">
|
||||
{example.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{examples.map((example) => (
|
||||
<TabsContent key={example.id} value={example.id}>
|
||||
<CodeBlock code={example.code} language={example.language} title={example.label} />
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
78
apps/docs/components/doc-block-renderer.tsx
Normal file
78
apps/docs/components/doc-block-renderer.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { DocBlock } from '@/lib/docs-pages';
|
||||
import { OAuthCodeTabs } from '@/components/oauth-code-tabs';
|
||||
import { AuthLoginCodeTabs } from '@/components/auth-code-tabs';
|
||||
import { AuthLdapCodeTabs } from '@/components/auth-ldap-code-tabs';
|
||||
import { ApiReferenceSection } from '@/components/api-endpoint-card';
|
||||
import { CodeBlock } from '@/components/code-block';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Callout({ variant, title, text }: { variant: 'info' | 'warning' | 'tip'; title: string; text: string }) {
|
||||
const styles = {
|
||||
info: 'border-blue-200 bg-blue-50 dark:border-blue-900 dark:bg-blue-950/40',
|
||||
warning: 'border-amber-200 bg-amber-50 dark:border-amber-900 dark:bg-amber-950/40',
|
||||
tip: 'border-emerald-200 bg-emerald-50 dark:border-emerald-900 dark:bg-emerald-950/40'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('my-4 rounded-xl border p-4', styles[variant])}>
|
||||
<p className="font-semibold">{title}</p>
|
||||
<p className="mt-1 text-sm leading-relaxed text-zinc-600 dark:text-zinc-300">{text}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DocBlockRenderer({ block }: { block: DocBlock }) {
|
||||
switch (block.type) {
|
||||
case 'paragraph':
|
||||
return <p className="leading-7 text-zinc-600 dark:text-zinc-300">{block.text}</p>;
|
||||
case 'list':
|
||||
return (
|
||||
<ul className="my-4 list-disc space-y-2 pl-6 leading-7 text-zinc-600 dark:text-zinc-300">
|
||||
{block.items.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
case 'code':
|
||||
return <CodeBlock code={block.code} language={block.language} title={block.title} />;
|
||||
case 'callout':
|
||||
return <Callout variant={block.variant} title={block.title} text={block.text} />;
|
||||
case 'table':
|
||||
return (
|
||||
<div className="my-4 overflow-x-auto rounded-xl border border-zinc-200 dark:border-zinc-800">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900">
|
||||
{block.headers.map((header) => (
|
||||
<th key={header} className="px-4 py-3 text-left font-semibold">
|
||||
{header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{block.rows.map((row, index) => (
|
||||
<tr key={index} className="border-b border-zinc-100 last:border-0 dark:border-zinc-800">
|
||||
{row.map((cell, cellIndex) => (
|
||||
<td key={cellIndex} className="px-4 py-3 align-top text-zinc-600 dark:text-zinc-300">
|
||||
{cell}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
case 'oauth-examples':
|
||||
return <OAuthCodeTabs />;
|
||||
case 'auth-login-examples':
|
||||
return <AuthLoginCodeTabs />;
|
||||
case 'auth-ldap-examples':
|
||||
return <AuthLdapCodeTabs />;
|
||||
case 'api-reference':
|
||||
return <ApiReferenceSection />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
103
apps/docs/components/docs-home-content.tsx
Normal file
103
apps/docs/components/docs-home-content.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { ArrowRight, BookOpen, Code2, Rocket, Shield } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { docNavigation, groupDocNavigation } from '@/lib/navigation';
|
||||
import { usePublicSettings } from '@/providers/public-settings-provider';
|
||||
|
||||
const highlights = [
|
||||
{
|
||||
icon: Rocket,
|
||||
title: 'Быстрый старт',
|
||||
description: 'Локальный запуск через Docker Compose за несколько минут.',
|
||||
href: '/docs/getting-started'
|
||||
},
|
||||
{
|
||||
icon: Code2,
|
||||
title: 'OAuth 2.0',
|
||||
description: 'Примеры интеграции на JavaScript, Python, PHP, Go и других языках.',
|
||||
href: '/docs/oauth'
|
||||
},
|
||||
{
|
||||
icon: Shield,
|
||||
title: 'Безопасность',
|
||||
description: 'PIN-код, сессии, LDAP/LDAPS и управление устройствами.',
|
||||
href: '/docs/sessions'
|
||||
},
|
||||
{
|
||||
icon: BookOpen,
|
||||
title: 'Справочник API',
|
||||
description: 'Полный список REST endpoints и ссылка на Swagger.',
|
||||
href: '/docs/api-reference'
|
||||
}
|
||||
];
|
||||
|
||||
export function DocsHomeContent() {
|
||||
const { projectName, projectTagline } = usePublicSettings();
|
||||
const groups = groupDocNavigation(docNavigation);
|
||||
|
||||
return (
|
||||
<main className="min-w-0 pb-16 xl:col-span-1">
|
||||
<section className="relative overflow-hidden rounded-2xl border border-zinc-200 bg-zinc-50 px-6 py-12 dark:border-zinc-800 dark:bg-zinc-900/50 sm:px-10 sm:py-16">
|
||||
<p className="text-sm font-medium text-zinc-500">Документация</p>
|
||||
<h1 className="mt-3 max-w-2xl text-4xl font-bold tracking-tight text-zinc-950 dark:text-zinc-50 sm:text-5xl">{projectName}</h1>
|
||||
<p className="mt-4 max-w-2xl text-lg leading-relaxed text-zinc-600 dark:text-zinc-400">{projectTagline}</p>
|
||||
<p className="mt-4 max-w-2xl text-zinc-600 dark:text-zinc-400">
|
||||
Enterprise Identity Provider: единый вход, OAuth 2.0, корпоративный LDAP, семейные группы, чат и админ-панель.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap gap-3">
|
||||
<Button asChild size="lg">
|
||||
<Link href="/docs/getting-started">
|
||||
Начать
|
||||
<ArrowRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="lg">
|
||||
<Link href="/docs/deployment">Развёртывание</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-10 grid gap-4 sm:grid-cols-2">
|
||||
{highlights.map((item) => (
|
||||
<Link key={item.href} href={item.href}>
|
||||
<Card className="h-full transition-colors hover:border-zinc-300 dark:hover:border-zinc-700">
|
||||
<CardHeader>
|
||||
<div className="mb-2 flex h-10 w-10 items-center justify-center rounded-lg bg-zinc-100 dark:bg-zinc-800">
|
||||
<item.icon className="h-5 w-5" />
|
||||
</div>
|
||||
<CardTitle>{item.title}</CardTitle>
|
||||
<CardDescription>{item.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<span className="text-sm font-medium text-zinc-500">Читать →</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="mt-12">
|
||||
<h2 className="text-xl font-semibold">Все разделы</h2>
|
||||
<div className="mt-4 grid gap-6 sm:grid-cols-2">
|
||||
{groups.map(([group, items]) => (
|
||||
<div key={group}>
|
||||
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-zinc-500">{group}</p>
|
||||
<ul className="space-y-2">
|
||||
{items.map((item) => (
|
||||
<li key={item.slug}>
|
||||
<Link href={`/docs/${item.slug}`} className="text-sm text-zinc-600 hover:text-zinc-950 dark:text-zinc-400 dark:hover:text-zinc-50">
|
||||
{item.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
109
apps/docs/components/docs-shell.tsx
Normal file
109
apps/docs/components/docs-shell.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { BookOpen, ExternalLink, Moon, Sun } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { usePublicSettings } from '@/providers/public-settings-provider';
|
||||
import { useTheme } from '@/providers/theme-provider';
|
||||
import { getApiBaseUrl } from '@/lib/api';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function DocsHeader() {
|
||||
const { projectName } = usePublicSettings();
|
||||
const { resolvedTheme, setTheme } = useTheme();
|
||||
const apiUrl = getApiBaseUrl();
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b border-zinc-200 bg-white/80 backdrop-blur dark:border-zinc-800 dark:bg-zinc-950/80">
|
||||
<div className="mx-auto flex h-14 max-w-screen-2xl items-center justify-between gap-4 px-4 lg:px-8">
|
||||
<Link href="/docs" className="flex items-center gap-2 font-semibold tracking-tight">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-zinc-900 text-white dark:bg-zinc-50 dark:text-zinc-900">
|
||||
<BookOpen className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="hidden sm:inline">{projectName}</span>
|
||||
<span className="text-zinc-400 font-normal hidden sm:inline">Docs</span>
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="ghost" size="sm" asChild className="hidden md:inline-flex">
|
||||
<a href={`${apiUrl}/api`} target="_blank" rel="noreferrer">
|
||||
Swagger
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
aria-label="Переключить тему"
|
||||
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
|
||||
>
|
||||
{resolvedTheme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export function DocsSidebar({ groups }: { groups: Array<[string, Array<{ slug: string; title: string }>]> }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<nav className="space-y-6">
|
||||
<Link
|
||||
href="/docs"
|
||||
className={cn(
|
||||
'block rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
||||
pathname === '/docs'
|
||||
? 'bg-zinc-100 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50'
|
||||
: 'text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-900 dark:hover:text-zinc-50'
|
||||
)}
|
||||
>
|
||||
Главная
|
||||
</Link>
|
||||
{groups.map(([group, items]) => (
|
||||
<div key={group}>
|
||||
<p className="mb-2 px-2 text-xs font-semibold uppercase tracking-wider text-zinc-500">{group}</p>
|
||||
<div className="space-y-1">
|
||||
{items.map((item) => {
|
||||
const href = `/docs/${item.slug}`;
|
||||
const active = pathname === href;
|
||||
return (
|
||||
<Link
|
||||
key={item.slug}
|
||||
href={href}
|
||||
className={cn(
|
||||
'block rounded-lg px-3 py-2 text-sm transition-colors',
|
||||
active
|
||||
? 'bg-zinc-100 font-medium text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50'
|
||||
: 'text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-900 dark:hover:text-zinc-50'
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
export function DocsToc({ sections }: { sections: Array<{ id: string; title: string }> }) {
|
||||
return (
|
||||
<nav className="space-y-2 text-sm">
|
||||
<p className="mb-3 font-semibold">На этой странице</p>
|
||||
{sections.map((section) => (
|
||||
<a
|
||||
key={section.id}
|
||||
href={`#${section.id}`}
|
||||
className="block text-zinc-500 transition-colors hover:text-zinc-900 dark:hover:text-zinc-50"
|
||||
>
|
||||
{section.title}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
8
apps/docs/components/oauth-code-tabs.tsx
Normal file
8
apps/docs/components/oauth-code-tabs.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { oauthExamples } from '@/lib/oauth-examples';
|
||||
import { CodeExampleTabs } from '@/components/code-example-tabs';
|
||||
|
||||
export function OAuthCodeTabs() {
|
||||
return <CodeExampleTabs examples={oauthExamples} />;
|
||||
}
|
||||
14
apps/docs/components/ui/badge.tsx
Normal file
14
apps/docs/components/ui/badge.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function Badge({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-md border border-zinc-200 px-2.5 py-0.5 text-xs font-semibold transition-colors dark:border-zinc-800',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
41
apps/docs/components/ui/button.tsx
Normal file
41
apps/docs/components/ui/button.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
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-xl text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'border border-transparent bg-zinc-900 text-zinc-50 shadow hover:bg-zinc-800 dark:border-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:shadow-none dark:hover:bg-zinc-200',
|
||||
secondary:
|
||||
'bg-zinc-100 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700',
|
||||
outline:
|
||||
'border border-zinc-300 bg-transparent text-zinc-900 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-transparent dark:text-zinc-100 dark:hover:bg-zinc-800',
|
||||
ghost: 'text-zinc-900 hover:bg-zinc-100 dark:text-zinc-100 dark:hover:bg-zinc-800',
|
||||
link: 'text-zinc-900 underline-offset-4 hover:underline dark:text-zinc-100'
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-lg px-3 text-xs',
|
||||
lg: 'h-11 rounded-xl px-8 text-base',
|
||||
icon: 'h-9 w-9'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
export function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return <Comp className={cn(buttonVariants({ variant, size, className }))} {...props} />;
|
||||
}
|
||||
22
apps/docs/components/ui/card.tsx
Normal file
22
apps/docs/components/ui/card.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn('rounded-xl border border-zinc-200 bg-white text-zinc-950 shadow-sm dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
|
||||
return <h3 className={cn('text-lg font-semibold leading-none tracking-tight', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
|
||||
return <p className={cn('text-sm text-zinc-500 dark:text-zinc-400', className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn('p-6 pt-0', className)} {...props} />;
|
||||
}
|
||||
31
apps/docs/components/ui/scroll-area.tsx
Normal file
31
apps/docs/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function ScrollArea({ className, children, ...props }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root className={cn('relative overflow-hidden', className)} {...props}>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ScrollBar({ className, orientation = 'vertical', ...props }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
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-zinc-200 dark:bg-zinc-800" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
);
|
||||
}
|
||||
31
apps/docs/components/ui/tabs.tsx
Normal file
31
apps/docs/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export const Tabs = TabsPrimitive.Root;
|
||||
|
||||
export function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
className={cn('inline-flex h-10 items-center justify-center rounded-lg bg-zinc-100 p-1 text-zinc-500 dark:bg-zinc-900', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-zinc-950 data-[state=active]:shadow dark:ring-offset-zinc-950 dark:data-[state=active]:bg-zinc-800 dark:data-[state=active]:text-zinc-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return <TabsPrimitive.Content className={cn('mt-4 focus-visible:outline-none', className)} {...props} />;
|
||||
}
|
||||
Reference in New Issue
Block a user