docs: add reference design

This commit is contained in:
2026-06-11 11:59:15 +05:00
parent 05f0a80c80
commit a5be6d7552
32 changed files with 5395 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
import { cn } from '@/lib/utils'
import type { ReactNode } from 'react'
export function Stamp({
children,
className,
skew = true,
variant = 'red',
}: {
children: ReactNode
className?: string
skew?: boolean
variant?: 'red' | 'ink'
}) {
return (
<span
className={cn(
'stamp px-2 py-1 text-[0.65rem] sm:text-xs',
variant === 'ink' && 'stamp-ink',
skew && 'stamp-skew',
className,
)}
>
{children}
</span>
)
}
export function OverlayStamp({
children,
className,
}: {
children: ReactNode
className?: string
}) {
return (
<span
aria-hidden="true"
className={cn(
'stamp-overlay pointer-events-none select-none px-4 py-2 font-heading text-2xl sm:text-4xl',
className,
)}
>
{children}
</span>
)
}
export function Redacted({ children }: { children: ReactNode }) {
return <span className="redacted px-1">{children}</span>
}
export function FieldRow({
label,
children,
}: {
label: string
children: ReactNode
}) {
return (
<div className="grid grid-cols-1 gap-1 border-b border-dashed border-border py-3 sm:grid-cols-[200px_1fr] sm:gap-4">
<dt className="font-heading text-xs font-600 uppercase tracking-wider text-muted-foreground">
{label}
</dt>
<dd className="text-sm leading-relaxed text-foreground">{children}</dd>
</div>
)
}
export function SectionTitle({
index,
children,
}: {
index: string
children: ReactNode
}) {
return (
<h2 className="mb-4 flex items-baseline gap-3 font-heading text-lg font-700 uppercase tracking-wide">
<span className="flex h-7 min-w-7 items-center justify-center bg-primary px-1.5 text-sm text-primary-foreground">
{index}
</span>
<span className="flex-1 border-b border-foreground/30 pb-1">
{children}
</span>
</h2>
)
}
export function Perforation({ className }: { className?: string }) {
return (
<div
aria-hidden="true"
className={cn(
'flex h-4 w-full items-center justify-between overflow-hidden px-2 opacity-60',
className,
)}
>
{Array.from({ length: 60 }).map((_, i) => (
<span
key={i}
className="h-1.5 w-1.5 shrink-0 rounded-full bg-background shadow-[inset_0_1px_2px_rgba(0,0,0,0.35)]"
/>
))}
</div>
)
}
+58
View File
@@ -0,0 +1,58 @@
import Link from 'next/link'
import type { Dossier } from '@/lib/dossiers'
import { Stamp } from '@/components/doc-elements'
const classColor: Record<string, string> = {
'СОВ. СЕКРЕТНО': 'text-primary',
'СЕКРЕТНО': 'stamp-ink',
'ДСП': 'stamp-ink',
'РАССЕКРЕЧЕНО': 'stamp-ink opacity-60',
}
export function DossierCard({ d, rotate }: { d: Dossier; rotate?: string }) {
return (
<Link
href={`/dossier/${d.slug}`}
style={{ rotate }}
className="paper group relative flex flex-col gap-3 p-5 transition-all duration-200 hover:-translate-y-1 hover:rotate-0 hover:shadow-[0_26px_50px_-22px_rgba(40,25,15,0.7)]"
>
{/* tape pin */}
<span className="tape -top-3 left-1/2 -translate-x-1/2 -rotate-2" aria-hidden="true" />
<div className="flex items-start justify-between gap-2">
<div>
<p className="font-mono text-[0.65rem] uppercase tracking-widest text-muted-foreground">
Дело {d.code}
</p>
<p className="mt-0.5 font-heading text-[0.7rem] uppercase tracking-wider text-primary">
{d.category}
</p>
</div>
<Stamp
className={`px-1.5 py-0.5 text-[0.55rem] ${classColor[d.classification] ?? ''}`}
>
{d.classification}
</Stamp>
</div>
<h3 className="font-heading text-xl font-700 uppercase leading-none tracking-tight transition-colors group-hover:text-primary">
{d.name}
</h3>
<p className="text-xs italic leading-snug text-muted-foreground">
{d.fullName}
</p>
<p className="text-sm leading-relaxed text-foreground/90">{d.summary}</p>
<div className="mt-auto flex items-center justify-between border-t border-dashed border-border pt-3">
<span className="font-mono text-[0.65rem] uppercase tracking-wider text-muted-foreground">
Угроза: <span className="text-foreground">{d.threat}</span>
</span>
<span className="font-heading text-[0.7rem] font-600 uppercase tracking-wider text-primary opacity-0 transition-opacity group-hover:opacity-100">
Открыть дело
</span>
</div>
</Link>
)
}
export { Stamp }
+68
View File
@@ -0,0 +1,68 @@
import Link from 'next/link'
import Image from 'next/image'
export function SiteHeader() {
return (
<header className="sticky top-0 z-30 border-b border-foreground/25 bg-card/85 backdrop-blur-sm shadow-[0_2px_12px_-8px_rgba(40,25,15,0.6)]">
<div className="mx-auto flex max-w-5xl flex-col gap-4 px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-6">
<Link href="/" className="flex items-center gap-3">
<Image
src="/emblem.png"
alt="Эмблема Трибунала"
width={52}
height={52}
className="h-12 w-12 mix-blend-multiply"
/>
<div className="leading-tight">
<p className="font-heading text-base font-700 uppercase tracking-wide sm:text-lg">
Архив ДНТ-7
</p>
<p className="text-[0.65rem] uppercase tracking-widest text-muted-foreground">
Дачно-Наблюдательный Трибунал
</p>
</div>
</Link>
<nav className="flex flex-wrap items-center gap-x-5 gap-y-1 font-heading text-xs font-600 uppercase tracking-wider">
{[
{ href: '/', label: 'Картотека' },
{ href: '/dossier/directive-kompot', label: 'Уставы' },
{ href: '/dossier/lopata-bs-3000', label: 'Инвентарь' },
{ href: '/protocol', label: 'Протокол' },
].map((l) => (
<Link
key={l.label}
href={l.href}
className="relative text-foreground/80 transition-colors hover:text-primary"
>
{l.label}
</Link>
))}
</nav>
</div>
</header>
)
}
export function SiteFooter() {
return (
<footer className="mt-16 border-t border-foreground/25 bg-card/60">
<div className="mx-auto max-w-5xl px-4 py-8 sm:px-6">
<div className="flex flex-col justify-between gap-4 text-[0.7rem] uppercase tracking-wider text-muted-foreground sm:flex-row">
<p>
©{' '}
<span className="text-foreground">
Дачно-Наблюдательный Трибунал 7
</span>{' '}
· Кооператив «Заря-9»
</p>
<p>Форма А-13 · Хранить вечно · Не выносить за периметр</p>
</div>
<p className="mt-4 max-w-2xl text-[0.7rem] leading-relaxed text-muted-foreground">
Все материалы являются художественным вымыслом. Любое совпадение с
реальными грядками, соседями и небесными телами случайно. Разглашение
преследуется лишением компоста.
</p>
</div>
</footer>
)
}
+58
View File
@@ -0,0 +1,58 @@
import { Button as ButtonPrimitive } from '@base-ui/react/button'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
outline:
'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
ghost:
'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
destructive:
'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default:
'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
icon: 'size-8',
'icon-xs':
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
'icon-sm':
'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
'icon-lg': 'size-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
function Button({
className,
variant = 'default',
size = 'default',
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }