Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,20 @@ 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
```

- Import a feature only through its `index.ts` (`@/features/posts`), shared UI through `@/components`, and third-party libraries through `@/lib/<library>`. 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

Expand Down Expand Up @@ -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.
Expand Down
7 changes: 4 additions & 3 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
25 changes: 16 additions & 9 deletions src/app/resume/resume.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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;
}

Expand Down Expand Up @@ -99,17 +106,17 @@
.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;
}

.rs-button--ghost {
color: var(--rs-ink);
background: #fff;
background: var(--rs-paper);
border-color: var(--rs-line);
text-decoration: none;
}
Expand All @@ -135,7 +142,7 @@

.rs-root {
padding: 0;
background: #fff;
background: var(--rs-paper);
}

.rs-sheet {
Expand Down
2 changes: 1 addition & 1 deletion src/components/page-transition.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
@keyframes page-enter {
from {
opacity: 0;
transform: translateY(8px);
transform: translateY(0.5rem);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/features/command-palette/command-palette.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Loading