diff --git a/AGENTS.md b/AGENTS.md index cd3054c..f560a59 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,8 @@ src/ ├── features/ about, command-palette, markdown, posts, resume, theme ├── components/ shared UI, imported from '@/components' ├── hooks/ hooks shared across features -├── types/ domain types (post, heading, resume) +├── types/ domain types and DTOs (post, heading, resume) +├── utils/ helpers shared by two or more features (none yet) ├── lib/ one wrapper per third-party library └── config/ site config and shared metadata ``` @@ -29,7 +30,11 @@ src/ - Import a feature only through its `index.ts` (`@/features/posts`), shared UI through `@/components`, and third-party libraries through `@/lib/`. Next.js and React are imported directly. - Kebab-case file names, named exports. Default exports only where Next.js requires them (`page.tsx`, `layout.tsx`, route files). - Every implementation file has a sibling test; barrels and type-only files don't. -- Split a file past ~300 lines or with more than one concern, keeping its import path. +- External data gets a DTO type in `src/types` and a mapper to the domain type in its feature (`PostFrontmatterDto` → `toPost` in `features/posts/post-mapper.ts`). The rest of the app sees domain types only. +- A helper used by one feature lives in that feature (`features/posts/format-post-date.ts`). Move it to `src/utils/` when a second feature needs it. +- Functions take at most two parameters; pass an options object beyond that. +- ESLint enforces braces on every `if`, two parameters and 200 lines per file (`curly`, `max-params`, `max-lines`). Split a file by concern before it gets there, keeping its import path. +- Next.js conventions win where they collide with these rules: default exports and fixed names for route files. ## Testing @@ -73,7 +78,8 @@ Vitest, React Testing Library and jsdom, with Istanbul coverage. Tests sit next - Use the design tokens in `src/app/globals.css`, with light values in `:root` and dark values in `.dark`. - Colors are always CSS variables. Never write a color literal in a component or CSS module: add or reuse a token, deriving new ones from existing tokens with `color-mix()` so both themes follow. -- Use `rem` for font sizes, spacing and other lengths (`0.0625rem` for hairlines), so the layout follows the reader's text size. +- Use `rem` for font sizes, spacing and other lengths (`0.0625rem` for hairlines), so the layout follows the reader's text size. The printed resume keeps `pt` and `mm` for its A4 page. +- Plain CSS: CSS Modules per component, global tokens in `globals.css`. No CSS-in-JS or utility frameworks. - Prefer CSS over JS. Add client JS only when CSS can't do it. - Respect `prefers-reduced-motion`. - Fonts are the system stack (`--font-sans`). Don't add web fonts. diff --git a/src/app/globals.css b/src/app/globals.css index f141b80..1bdc47f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -67,6 +67,7 @@ --radius: 0.375rem; --radius-lg: 0.5rem; --radius-xl: 0.75rem; + --shadow-dialog: 0 1.5rem 3rem rgba(0, 0, 0, 0.35); /* Typography scale */ --heading-h1-page: clamp(1.75rem, 4vw, 2.25rem); @@ -279,10 +280,10 @@ pre[class*='language-'] { .sr-only { position: absolute; - width: 1px; - height: 1px; + width: 0.0625rem; + height: 0.0625rem; padding: 0; - margin: -1px; + margin: -0.0625rem; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; diff --git a/src/app/resume/resume.css b/src/app/resume/resume.css index d96a6b1..2ccaa5e 100644 --- a/src/app/resume/resume.css +++ b/src/app/resume/resume.css @@ -9,6 +9,13 @@ --rs-muted: #4a4a4a; --rs-line: #a6a6a6; --rs-line-soft: #d4d4d4; + --rs-paper: #ffffff; + --rs-canvas: #e6e6e6; + --rs-shadow-tint: rgb(17 21 28 / 12%); + --rs-shadow-tint-soft: rgb(17 21 28 / 10%); + + /* Print-safe font stack, explained at `.rs-root *` below */ + --rs-font: 'Helvetica Neue', Helvetica, Arial, sans-serif; /* Type scale in pt (print-native unit for font sizes) */ --rs-text-2xs: 8.5pt; @@ -30,7 +37,7 @@ color-scheme: light; color: var(--rs-ink); - background: #e6e6e6; + background: var(--rs-canvas); font-size: var(--rs-text-base); line-height: var(--rs-leading); padding: 2rem 1rem 4rem; @@ -43,10 +50,10 @@ max-width: 100%; margin: 0 auto; padding: 12mm; - background: #fff; + background: var(--rs-paper); box-shadow: - 0 1px 3px rgb(17 21 28 / 12%), - 0 12px 32px rgb(17 21 28 / 10%); + 0 0.0625rem 0.1875rem var(--rs-shadow-tint), + 0 0.75rem 2rem var(--rs-shadow-tint-soft); } /* Print-safe font stack, not the site's Inter: Chrome emits subsetted variable @@ -56,7 +63,7 @@ ("Define", "effort") and break keyword matching in weaker resume parsers. */ .rs-root, .rs-root * { - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-family: var(--rs-font); font-variant-ligatures: none; } @@ -99,9 +106,9 @@ .rs-button { font: inherit; font-weight: 600; - color: #fff; + color: var(--rs-paper); background: var(--rs-accent); - border: 1px solid var(--rs-accent); + border: 0.0625rem solid var(--rs-accent); border-radius: 0.375rem; padding: 0.375rem 0.875rem; cursor: pointer; @@ -109,7 +116,7 @@ .rs-button--ghost { color: var(--rs-ink); - background: #fff; + background: var(--rs-paper); border-color: var(--rs-line); text-decoration: none; } @@ -135,7 +142,7 @@ .rs-root { padding: 0; - background: #fff; + background: var(--rs-paper); } .rs-sheet { diff --git a/src/components/page-transition.module.css b/src/components/page-transition.module.css index ff6b46e..9728dc2 100644 --- a/src/components/page-transition.module.css +++ b/src/components/page-transition.module.css @@ -7,7 +7,7 @@ @keyframes page-enter { from { opacity: 0; - transform: translateY(8px); + transform: translateY(0.5rem); } } diff --git a/src/features/command-palette/command-palette.module.css b/src/features/command-palette/command-palette.module.css index fb82544..caca53c 100644 --- a/src/features/command-palette/command-palette.module.css +++ b/src/features/command-palette/command-palette.module.css @@ -53,10 +53,11 @@ color: var(--foreground); padding: 0; margin: 10vh auto auto; - box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.35); + box-shadow: var(--shadow-dialog); } .dialog::backdrop { + /* Literal: older browsers don't pass custom properties to ::backdrop */ background: rgba(0, 0, 0, 0.5); }