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
+187
View File
@@ -0,0 +1,187 @@
import { notFound } from 'next/navigation'
import Link from 'next/link'
import Image from 'next/image'
import { DOSSIERS, getDossier } from '@/lib/dossiers'
import { CONTENT } from '@/lib/dossier-content'
import { SiteHeader, SiteFooter } from '@/components/site-chrome'
import {
Stamp,
OverlayStamp,
FieldRow,
SectionTitle,
Perforation,
} from '@/components/doc-elements'
export function generateStaticParams() {
return DOSSIERS.map((d) => ({ slug: d.slug }))
}
export default async function DossierPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const dossier = getDossier(slug)
const content = CONTENT[slug]
if (!dossier || !content) notFound()
return (
<div className="min-h-screen">
<SiteHeader />
<div className="banner-classified border-y border-foreground/30">
<div className="mx-auto flex max-w-4xl items-center justify-between gap-4 px-4 py-1.5 text-[0.65rem] font-700 uppercase tracking-[0.2em] sm:px-6">
<span>{dossier.classification}</span>
<span className="hidden sm:inline">Дело {dossier.code}</span>
<span>Экз. 1</span>
</div>
</div>
<main className="mx-auto max-w-4xl px-4 py-8 sm:px-6">
<Link
href="/"
className="font-heading text-xs font-600 uppercase tracking-wider text-muted-foreground hover:text-primary"
>
Вернуться в картотеку
</Link>
<article className="paper relative mt-4 -rotate-[0.3deg] p-6 sm:p-10">
<span className="tape -top-3 left-1/2 hidden -translate-x-1/2 rotate-1 sm:block" aria-hidden="true" />
{/* corner stamp */}
<div className="pointer-events-none absolute right-3 top-4 rotate-[9deg] sm:right-8 sm:top-10">
<OverlayStamp className="text-xl sm:text-3xl">
{dossier.classification}
</OverlayStamp>
</div>
<header className="pb-5">
<p className="font-mono text-[0.7rem] uppercase tracking-[0.25em] text-muted-foreground">
{dossier.category} · Дело {dossier.code} · Угроза{' '}
{dossier.threat}
</p>
<h1 className="mt-3 max-w-2xl text-balance font-heading text-3xl font-700 uppercase leading-[0.95] tracking-tight sm:text-5xl">
{dossier.name}
</h1>
<p className="mt-3 max-w-2xl text-sm italic leading-relaxed text-muted-foreground">
{dossier.fullName}
</p>
<hr className="rule rule-red mt-5" />
</header>
{/* Spec table */}
<section className="mt-2">
<dl>
{content.fields.map((f) => (
<FieldRow key={f.label} label={f.label}>
{f.value}
</FieldRow>
))}
</dl>
</section>
{/* Blueprint */}
{content.hasBlueprint && (
<figure className="paper paper-strong mt-8 p-3">
<Image
src="/blueprint-lopata.png"
alt={`Технический чертёж изделия ${dossier.name}`}
width={1200}
height={800}
className="h-auto w-full mix-blend-multiply"
/>
<figcaption className="mt-2 border-t border-dashed border-border pt-2 text-center font-mono text-[0.65rem] uppercase tracking-wider text-muted-foreground">
Рис. 1 Чертёж общего вида · ГОСТ-садовый 12-86
</figcaption>
</figure>
)}
{/* Photo */}
{content.hasPhoto && (
<figure className="paper paper-strong mt-8 max-w-sm rotate-1 p-2">
<Image
src="/classified-photo.png"
alt="Рассекреченный фотоматериал"
width={600}
height={600}
className="h-auto w-full grayscale"
/>
<figcaption className="mt-2 border-t border-dashed border-border pt-2 font-mono text-[0.6rem] uppercase tracking-wider text-muted-foreground">
Фотоматериал · участок 6 · 03:14
</figcaption>
</figure>
)}
{/* Sections */}
<div className="mt-10 space-y-10">
{content.sections.map((s) => (
<section key={s.index}>
<SectionTitle index={s.index}>{s.title}</SectionTitle>
<div className="text-sm leading-relaxed text-foreground/90">
{s.body}
</div>
</section>
))}
</div>
{/* Signature block */}
<footer className="mt-12 grid grid-cols-1 gap-6 border-t border-dashed border-border pt-6 sm:grid-cols-2">
<div>
<p className="text-[0.7rem] uppercase tracking-wider text-muted-foreground">
Утвердил
</p>
<p className="mt-3 font-heading text-base font-600">
Председатель Трибунала
</p>
<p className="mt-1 border-b border-foreground/50 pb-1 font-mono text-sm text-muted-foreground">
подпись: [ИЗЪЯТО]
</p>
</div>
<div className="flex items-start justify-end">
<Image
src="/emblem.png"
alt=""
width={90}
height={90}
className="h-20 w-20 rotate-6 opacity-80 mix-blend-multiply"
/>
</div>
</footer>
</article>
{/* Cross-references */}
<section className="mt-10">
<h2 className="mb-1 font-heading text-lg font-700 uppercase tracking-wide">
См. также
</h2>
<hr className="rule mb-5 w-40" />
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{DOSSIERS.filter((d) => d.slug !== slug)
.slice(0, 4)
.map((d) => (
<Link
key={d.slug}
href={`/dossier/${d.slug}`}
className="paper flex items-center justify-between px-3 py-2.5 text-sm transition-all hover:-translate-y-0.5 hover:text-primary"
>
<span className="font-heading uppercase tracking-wide">
{d.name}
</span>
<span className="font-mono text-[0.65rem] text-muted-foreground">
{d.code}
</span>
</Link>
))}
</div>
</section>
<div className="mt-12">
<Perforation />
</div>
</main>
<SiteFooter />
</div>
)
}
+230
View File
@@ -0,0 +1,230 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@import 'shadcn/tailwind.css';
@custom-variant dark (&:is(.dark *));
@theme inline {
--font-heading: var(--font-oswald);
--font-sans: var(--font-plex-mono), 'Geist Fallback';
--font-mono: var(--font-plex-mono), 'Geist Mono Fallback';
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--color-stamp: var(--stamp);
--color-ink: var(--ink);
--color-ink-soft: var(--ink-soft);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
}
:root {
/* Aged paper / Soviet document palette — warm, low-contrast, sepia ink */
--background: oklch(0.9 0.028 78);
--foreground: oklch(0.32 0.03 52);
--card: oklch(0.945 0.024 84);
--card-foreground: oklch(0.32 0.03 52);
--popover: oklch(0.945 0.024 84);
--popover-foreground: oklch(0.32 0.03 52);
--primary: oklch(0.47 0.16 30);
--primary-foreground: oklch(0.95 0.022 88);
--secondary: oklch(0.86 0.03 78);
--secondary-foreground: oklch(0.34 0.03 52);
--muted: oklch(0.87 0.026 80);
--muted-foreground: oklch(0.5 0.028 56);
--accent: oklch(0.47 0.16 30);
--accent-foreground: oklch(0.95 0.022 88);
--destructive: oklch(0.47 0.16 30);
--border: oklch(0.56 0.035 58 / 0.45);
--input: oklch(0.56 0.035 58 / 0.45);
--ring: oklch(0.47 0.16 30);
--stamp: oklch(0.46 0.17 30);
--ink: oklch(0.32 0.03 52);
--ink-soft: oklch(0.5 0.028 56);
--chart-1: oklch(0.47 0.16 30);
--chart-2: oklch(0.5 0.02 60);
--chart-3: oklch(0.4 0.02 55);
--chart-4: oklch(0.6 0.05 80);
--chart-5: oklch(0.3 0.02 55);
--radius: 0.125rem;
--sidebar: oklch(0.9 0.022 84);
--sidebar-foreground: oklch(0.32 0.03 52);
--sidebar-primary: oklch(0.47 0.16 30);
--sidebar-primary-foreground: oklch(0.95 0.022 88);
--sidebar-accent: oklch(0.86 0.03 78);
--sidebar-accent-foreground: oklch(0.34 0.03 52);
--sidebar-border: oklch(0.56 0.035 58 / 0.45);
--sidebar-ring: oklch(0.47 0.16 30);
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
/* layered paper: faint ledger rules + grain + warm vignette */
background-image:
/* vignette */
radial-gradient(
130% 120% at 50% 0%,
transparent 55%,
oklch(0.3 0.04 52 / 0.12) 100%
),
/* fine grain */
radial-gradient(oklch(0.3 0.03 52 / 0.05) 1px, transparent 1.4px),
radial-gradient(oklch(0.3 0.03 52 / 0.04) 1px, transparent 1.4px),
/* warm base wash */
linear-gradient(180deg, oklch(0.91 0.03 80), oklch(0.88 0.03 76));
background-size: 100% 100%, 16px 16px, 22px 22px, 100% 100%;
background-position: 0 0, 0 0, 8px 11px, 0 0;
background-attachment: fixed;
}
html {
@apply font-mono;
}
}
@layer utilities {
.font-heading {
font-family: var(--font-heading);
}
/* ---- Paper sheet: the core surface, replaces hard black rectangles ---- */
.paper {
position: relative;
background-color: var(--card);
background-image:
linear-gradient(
oklch(0.5 0.03 56 / 0.05) 1px,
transparent 1px
),
radial-gradient(oklch(0.3 0.03 52 / 0.04) 1px, transparent 1.3px);
background-size: 100% 30px, 18px 18px;
border: 1px solid oklch(0.5 0.04 56 / 0.35);
box-shadow:
0 1px 0 oklch(1 0 0 / 0.4) inset,
0 18px 40px -24px oklch(0.25 0.04 52 / 0.55),
0 2px 6px -3px oklch(0.25 0.04 52 / 0.3);
}
/* darker "cover/file" sheet */
.paper-strong {
background-color: oklch(0.5 0.04 56 / 0.06);
}
/* tape strip for pinning sheets */
.tape {
position: absolute;
height: 1.6rem;
width: 6.5rem;
background: oklch(0.78 0.06 92 / 0.55);
box-shadow: 0 1px 4px oklch(0.25 0.04 52 / 0.25);
backdrop-filter: blur(0.5px);
}
/* ---- Document divider rules (replace border-b-2 black) ---- */
.rule {
border: 0;
border-top: 1px solid oklch(0.45 0.04 56 / 0.5);
box-shadow: 0 3px 0 -2px oklch(0.45 0.04 56 / 0.5);
}
.rule-red {
border-top-color: var(--primary);
box-shadow: 0 3px 0 -2px var(--primary);
}
/* ---- Classification rubber stamp: rotated, distressed, translucent ---- */
.stamp {
--c: var(--stamp);
display: inline-flex;
align-items: center;
border: 2.5px solid var(--c);
color: var(--c);
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 700;
border-radius: 3px;
box-shadow: inset 0 0 0 1.5px var(--c);
opacity: 0.78;
mix-blend-mode: multiply;
/* worn ink texture */
-webkit-mask-image: radial-gradient(
oklch(0 0 0) 60%,
transparent 62%
),
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='40'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1.4 1.1'/%3E%3C/filter%3E%3Crect width='80' height='40' filter='url(%23n)'/%3E%3C/svg%3E");
-webkit-mask-composite: source-over;
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='40'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1.4 1.1'/%3E%3C/filter%3E%3Crect width='80' height='40' filter='url(%23n)'/%3E%3C/svg%3E");
mask-mode: luminance;
}
.stamp-skew {
transform: rotate(-7deg);
}
.stamp-ink {
--c: var(--ink-soft);
}
/* big translucent overlay stamp */
.stamp-overlay {
color: var(--stamp);
border: 4px solid var(--stamp);
opacity: 0.22;
mix-blend-mode: multiply;
text-transform: uppercase;
font-weight: 700;
letter-spacing: 0.12em;
border-radius: 6px;
}
/* redacted bar with toner-bleed edges */
.redacted {
background: var(--ink);
color: transparent;
user-select: none;
border-radius: 1px;
box-shadow: 0 0 1px 1px oklch(0.3 0.03 52 / 0.4);
}
/* ledger-style top banner */
.banner-classified {
background:
repeating-linear-gradient(
45deg,
oklch(0.42 0.16 30),
oklch(0.42 0.16 30) 14px,
oklch(0.38 0.15 30) 14px,
oklch(0.38 0.15 30) 28px
);
color: var(--primary-foreground);
}
}
+41
View File
@@ -0,0 +1,41 @@
import type { Metadata } from 'next'
import { Oswald, IBM_Plex_Mono } from 'next/font/google'
import { Analytics } from '@vercel/analytics/next'
import './globals.css'
const oswald = Oswald({
variable: '--font-oswald',
subsets: ['latin', 'cyrillic'],
weight: ['400', '500', '600', '700'],
})
const plexMono = IBM_Plex_Mono({
variable: '--font-plex-mono',
subsets: ['latin', 'cyrillic'],
weight: ['400', '500', '600', '700'],
})
export const metadata: Metadata = {
title: 'АРХИВ ДНТ-7 // Дачно-Наблюдательный Трибунал',
description:
'Рассекреченный архив Дачно-Наблюдательного Трибунала №7. Документы, инструкции и уставы по противодействию рептилоидным агентам планеты Нибиру.',
generator: 'v0.app',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html
lang="ru"
className={`${oswald.variable} ${plexMono.variable} bg-background`}
>
<body className="font-mono antialiased">
{children}
{process.env.NODE_ENV === 'production' && <Analytics />}
</body>
</html>
)
}
+119
View File
@@ -0,0 +1,119 @@
import Image from 'next/image'
import { DOSSIERS } from '@/lib/dossiers'
import { DossierCard } from '@/components/dossier-card'
import { SiteHeader, SiteFooter } from '@/components/site-chrome'
import { Stamp, OverlayStamp, Perforation } from '@/components/doc-elements'
const tilts = ['-0.6deg', '0.5deg', '-0.4deg', '0.7deg', '-0.5deg', '0.4deg']
export default function HomePage() {
return (
<div className="min-h-screen">
<SiteHeader />
{/* Classified banner */}
<div className="banner-classified border-y border-foreground/30">
<div className="mx-auto flex max-w-5xl items-center justify-between gap-4 px-4 py-1.5 text-[0.65rem] font-700 uppercase tracking-[0.2em] sm:px-6">
<span>Сов. Секретно</span>
<span className="hidden sm:inline">Экз. 1 из 3</span>
<span>Хранить вечно</span>
</div>
</div>
<main className="mx-auto max-w-5xl px-4 py-10 sm:px-6">
{/* Hero document header */}
<section className="paper relative mb-12 -rotate-[0.4deg] p-6 sm:p-10">
<span className="tape -top-3 left-10 rotate-3" aria-hidden="true" />
<span className="tape -top-3 right-12 -rotate-2" aria-hidden="true" />
<div className="pointer-events-none absolute right-4 top-4 sm:right-8 sm:top-8">
<Image
src="/emblem.png"
alt=""
width={120}
height={120}
className="h-20 w-20 opacity-90 mix-blend-multiply sm:h-28 sm:w-28"
/>
</div>
<p className="font-mono text-xs uppercase tracking-[0.3em] text-muted-foreground">
Союз Садоводческих Кооперативов · Отдел «Грядка»
</p>
<h1 className="mt-3 max-w-2xl text-balance font-heading text-3xl font-700 uppercase leading-[0.95] tracking-tight sm:text-5xl">
Архив Дачно-Наблюдательного Трибунала 7
</h1>
<p className="mt-4 max-w-xl text-pretty text-sm leading-relaxed text-foreground/90">
Рассекреченный свод документов, инструкций и уставов по
противодействию рептилоидным агентам планеты{' '}
<span className="font-600 text-primary">Нибиру</span> на территориях
садоводческих товариществ. Допуск согласно форме А-13.
</p>
<div className="mt-6 flex flex-wrap items-center gap-3">
<Stamp>Рассекречено · 06.07.2026</Stamp>
<Stamp variant="ink" skew={false}>
Подлежит изучению
</Stamp>
<span className="font-mono text-[0.65rem] uppercase tracking-wider text-muted-foreground">
Регистр. ДНТ-7/04-1986
</span>
</div>
<div className="pointer-events-none absolute -bottom-3 right-6 rotate-[8deg] sm:right-16">
<OverlayStamp>Архивъ · ДНТ-7</OverlayStamp>
</div>
</section>
{/* Stat strip */}
<section className="mb-12 grid grid-cols-2 gap-4 sm:grid-cols-4">
{[
{ k: 'Дел в картотеке', v: '147' },
{ k: 'Заземлено агентов', v: '38' },
{ k: 'Активных участков', v: '9' },
{ k: 'Уровень тревоги', v: 'III' },
].map((s, i) => (
<div
key={s.k}
className="paper px-4 py-5 text-center"
style={{ rotate: tilts[i] }}
>
<p className="font-heading text-3xl font-700 text-primary">
{s.v}
</p>
<p className="mt-1 text-[0.65rem] uppercase tracking-wider text-muted-foreground">
{s.k}
</p>
</div>
))}
</section>
{/* Catalog */}
<section>
<div className="mb-8 flex items-end justify-between">
<div>
<h2 className="font-heading text-2xl font-700 uppercase tracking-wide">
Картотека дел
</h2>
<hr className="rule rule-red mt-2 w-48" />
</div>
<p className="font-mono text-[0.65rem] uppercase tracking-wider text-muted-foreground">
Опись · лист 1
</p>
</div>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{DOSSIERS.map((d, i) => (
<DossierCard key={d.slug} d={d} rotate={tilts[i % tilts.length]} />
))}
</div>
</section>
<div className="mt-12">
<Perforation />
</div>
</main>
<SiteFooter />
</div>
)
}
+127
View File
@@ -0,0 +1,127 @@
import Image from 'next/image'
import { SiteHeader, SiteFooter } from '@/components/site-chrome'
import { Stamp, SectionTitle, Perforation } from '@/components/doc-elements'
const STEPS = [
{
n: '01',
t: 'Обнаружение',
d: 'Зафиксировать признаки маскировки: немигающий взгляд, любовь к солнцу, неприятие соли, идеальная рассада без труда.',
},
{
n: '02',
t: 'Доклад',
d: 'Сообщить старшему по кооперативу через условный сигнал (три удара по бочке с водой).',
},
{
n: '03',
t: 'Локализация',
d: 'Применить изделие Л.О.П.А.Т.А. (ИНВ-001) для заземления. Соблюдать меры безопасности.',
},
{
n: '04',
t: 'Экранирование',
d: 'Накрыть изделием В.Е.Д.Р.О. (ИНВ-022) для блокировки канала связи с орбитой.',
},
{
n: '05',
t: 'Регистрация',
d: 'Внести запись в журнал формы Б-2. Присвоить делу номер. Хранить вечно.',
},
]
export default function ProtocolPage() {
return (
<div className="min-h-screen">
<SiteHeader />
<div className="banner-classified border-y border-foreground/30">
<div className="mx-auto flex max-w-4xl items-center justify-between gap-4 px-4 py-1.5 text-[0.65rem] font-700 uppercase tracking-[0.2em] sm:px-6">
<span>Для служебного пользования</span>
<span className="hidden sm:inline">Форма Б-2</span>
<span>Изучить под роспись</span>
</div>
</div>
<main className="mx-auto max-w-4xl px-4 py-8 sm:px-6">
<article className="paper relative -rotate-[0.3deg] p-6 sm:p-10">
<span className="tape -top-3 left-1/2 hidden -translate-x-1/2 rotate-1 sm:block" aria-hidden="true" />
<div className="pointer-events-none absolute right-4 top-4 sm:right-8 sm:top-10">
<Image
src="/emblem.png"
alt=""
width={90}
height={90}
className="h-16 w-16 opacity-80 mix-blend-multiply sm:h-20 sm:w-20"
/>
</div>
<header className="pb-5">
<p className="font-mono text-[0.7rem] uppercase tracking-[0.25em] text-muted-foreground">
Единый полевой протокол · ДНТ-7
</p>
<h1 className="mt-3 max-w-xl text-balance font-heading text-3xl font-700 uppercase leading-[0.95] tracking-tight sm:text-5xl">
Протокол противодействия
</h1>
<div className="mt-4 flex flex-wrap gap-3">
<Stamp>Обязательно к исполнению</Stamp>
<Stamp variant="ink" skew={false}>
Ред. 1986
</Stamp>
</div>
<hr className="rule rule-red mt-5" />
</header>
<section className="mt-8">
<SectionTitle index="§">Порядок действий</SectionTitle>
<ol className="space-y-4">
{STEPS.map((s, i) => (
<li
key={s.n}
className="paper flex gap-4 p-4"
style={{ rotate: i % 2 === 0 ? '-0.4deg' : '0.4deg' }}
>
<span className="font-heading text-3xl font-700 leading-none text-primary">
{s.n}
</span>
<div>
<h3 className="font-heading text-base font-600 uppercase tracking-wide">
{s.t}
</h3>
<p className="mt-1 text-sm leading-relaxed text-foreground/90">
{s.d}
</p>
</div>
</li>
))}
</ol>
</section>
<section className="mt-10">
<SectionTitle index="!">Памятка наблюдателю</SectionTitle>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
{[
'Соль — табельное средство демаскировки.',
'Инвентарь держать заточенным и заземлённым.',
'О немигающих соседях докладывать немедленно.',
].map((p) => (
<p
key={p}
className="paper border-l-4 border-l-primary p-3 text-sm leading-relaxed"
>
{p}
</p>
))}
</div>
</section>
</article>
<div className="mt-12">
<Perforation />
</div>
</main>
<SiteFooter />
</div>
)
}