20b719893c
- 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>
42 lines
1.8 KiB
Markdown
42 lines
1.8 KiB
Markdown
---
|
|
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:
|
|
```tsx
|
|
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
|