Files
portfolio/.claude/rules/components.md
denshooter 486555dbf2 feat: plakat redesign for subpages (projects, books, legal, privacy)
- Shared SubpageShell (HeaderClient paper strip + ink footer) and SubpageIntro,
  btn/ghost/card helpers in app/_ui/subpage.tsx
- Projects list: poster cards with hard ink shadows, filter pills, ink search field;
  no fake 800ms skeleton and no framer-motion opacity:0 in SSR
- Project detail: poster hero, .plakat-prose markdown, links/stack aside,
  back link works without JS
- Books: server-side fetch (visible without JS), cover + rating pills
- Legal notice / privacy: moved to app/_ui/*Client.tsx, root routes removed
  (middleware always redirects to /[locale]), consent reset via useConsent
- ProjectThumbnail rebuilt on Plakat tokens
- Old Header.tsx + test removed; brown/cream/sand palettes and unused
  glass-panel/admin-glass(-light)/liquid-bg-blob/float CSS dropped
  (liquid palette, glass-card, admin-glass-card stay for admin + 404)
- .plakat-prose replaces the missing typography plugin

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 00:01:26 +02:00

1.8 KiB

paths
paths
app/components/**/*.tsx
app/_ui/**/*.tsx

Component Rules

SSR animation safety (critical)

Never use initial={{ opacity: 0 }} on server-rendered elements. This bakes style="opacity:0" into HTML — content is invisible if hydration fails.

Use ScrollFadeIn instead:

import ScrollFadeIn from "@/app/components/ScrollFadeIn"
<ScrollFadeIn><MyComponent /></ScrollFadeIn>

AnimatePresence is fine for modals and overlays that only appear after user interaction.

Design system (Plakat)

  • Colors: only the Plakat tokens signal, ink, paper, paper-2, ok — no hex, no raw Tailwind palette, no liquid-* (left over only in admin and 404)
  • Ink and paper swap roles in dark mode on their own; signal stays red
  • Fonts: font-display (Bricolage, [font-variation-settings:'wdth'_80] for posters), font-sans (Geist), font-mono (Geist Mono)
  • Cards: rounded-[18px] border-2 border-ink + hard offset shadow shadow-[6px_6px_0_rgb(var(--plakat-ink))]
  • Pills: rounded-full with shadow-[inset_0_0_0_1.5px_rgb(var(--plakat-ink))], filled variant bg-ink text-paper
  • Labels: font-mono text-[13px]; nothing below 12px
  • Subpages: wrap in app/_ui/SubpageShell.tsx, reuse SubpageIntro, btn, ghost, card from app/_ui/subpage.tsx; long text in .plakat-prose
  • Motion: ease-out / ease-drawer, press feedback active:scale-[0.97], never transition-all

Async components

Every component that fetches data must have a Skeleton loading state shown while data loads.

i18n

  • Client: useTranslations("namespace") from next-intl
  • Server: getTranslations("namespace") from next-intl/server
  • New client sections need a wrapper in ClientWrappers.tsx with scoped NextIntlClientProvider

TypeScript

  • No any — define interfaces in lib/directus.ts or types/
  • No emojis in code