Imported from yammmyu/engineering-portfolio (
AGENTS.md). Install upstream withnpx skills add yammmyu/engineering-portfolio. Copyright stays with the author.
AGENTS.md
Operating guide for any AI agent working in this repo. Read this before touching code.
- What this is: a personal engineering portfolio for Yanyu Chen, a Robotics Engineering student. React + Vite + Tailwind, static, deployed on Vercel.
- How it works: README.md — the showcase page a visitor lands on: stack, design system, and the IK solver writeup. It is reader-facing, so keep it that way.
- Adding or editing content: docs/CONTENT.md — projects, tags, images, repo layout.
- What it's for and what it must never become: docs/PRODUCT.md. Read it before any design, copy, or content change.
- Where the last agent left off: docs/JOURNAL.md. Read the top entry before starting; add one before you finish.
Before you finish, always
npm run check # conventions: translations, tags, tokens — must exit 0
npm run build # must succeed
npm run check (scripts/check.mjs) enforces the mechanical half of
this document. Warnings are acceptable (missing project images are expected — see below);
errors are not. If you add a convention that can be checked, add it to that script rather
than only writing it here.
There is no test suite and no linter config. npm run check and the build are the gate.
Do not add ESLint or Prettier configs without being asked — the style below is already
consistent across every file, and a formatter run would bury a real diff in noise.
The one rule that matters most
Comments explain why, and they record what was tried and rejected.
This is the defining characteristic of the codebase. Almost every non-obvious line has a comment giving the reasoning behind it, and often the failure that motivated it:
// The row highlighted on hover but only a link in the far corner was
// clickable. The primary link now owns the whole row via a stretched
// pseudo-element, so what lights up is what you can press.
{/* Everything about the project sits in one column at a readable
measure. Spreading it across the full 78rem sheet put the title and
its link 1200px apart and read as two unrelated things. */}
These comments are the project's memory. They are why a future agent doesn't re-introduce a bug that was already fixed once.
- When you make a non-obvious decision, write down why in the same commit.
- When you reject an approach for a concrete reason, say what it was and what went wrong.
- Never delete one of these comments to tidy up. If you change the code it describes, update the comment to match. If the comment is now wrong, that is a bug.
- Do not add comments that restate the code (
// set the count). The bar is: would a competent engineer reading this line wonder "why like that?" If yes, answer it.
Code style
Matched across every existing file; keep it that way.
- No semicolons. Single quotes. 2-space indent. ~100 column width.
- Trailing commas in multiline literals.
function Name() {}declarations for components; arrow functions for callbacks and small helpers.- Default export for a section component, named exports for primitives and hooks.
- Always include the file extension in imports (
'./Section.jsx','../lib/ik.js'). - Module-level constants are
SCREAMING_SNAKEand sit at the top of the file, above the components, with a comment if the values are not self-evident. import React from 'react'explicitly, even where the JSX transform doesn't need it.- JSDoc
/** */blocks on components and exported functions whose purpose isn't obvious from the name. Inline{/* */}in JSX for layout decisions.
Architecture
src/
components/ section components + Section.jsx primitives
context/ LanguageContext — EN/ZH state
hooks/ useReveal — IntersectionObserver scroll reveal
lib/ pure, dependency-free logic (ik.js)
models/ Project — id → translation key contract
index.css design tokens, base styles, component/utility layers
translations.json
src/lib/is pure and dependency-free. No React, no DOM.ik.jscan be exercised directly withnode. Math and algorithms go here; components draw and handle input.KinematicSketch.jsxis the drawing layer only — if you find yourself writing geometry in it, that belongs inlib/.- Reuse the primitives in
Section.jsx—Sheet(gutter + max width),SectionHeader(sheet number, title, rule),Reveal(scroll fade). Don't re-roll a section shell. Projectinmodels/Project.jsowns the id → key contract.titleKeyanddescKeyderive from the id. Never hardcode'proj_foo_title'at a call site.- The
PROJECTSarray inProjects.jsxis the registry. It is ordered by how much robotics is in each project, not by date. New rows go where they belong in that order.
Styling
- Every colour goes through a token. Tokens are CSS custom properties in
index.css(--c-paper,--c-ink,--c-rule,--c-accent, …), exposed to Tailwind as named colours intailwind.config.js.index.cssis the only file where a literal colour value may appear.KinematicSketch.jsxis the one exception — its canvas palette holds deliberate fallbacks for values it reads back viagetComputedStyle. - Never branch on theme in a component. The dark sheet redefines the same tokens under
@media (prefers-color-scheme: dark). There is no theme prop, nodark:variant. - Opacity modifiers do not work on token colours.
bg-paper/50resolves tovar(--c-paper)/50and silently produces nothing. Add a new token instead. accentis a marking colour — it never sets text. Useaccent-inkwhen the accent has to be legible as type.accentis for rules, marks, washes, and the end effector.- Square corners, hairline rules, no shadows. There is not one
rounded-*orshadow-*in the codebase. That is deliberate; see docs/PRODUCT.md. - Reach for
.labelfor any mono caption, column header, or data value.
Motion
- Animate
transformandopacityonly. Nothing that triggers layout. - Use the custom curves
var(--ease-out)/var(--ease-in-out), not Tailwind's defaults. - Budget: interaction ≤ 300ms, entrance ~450ms, the section rule draw 700ms.
- Gate hover nudges behind
@media (hover: hover) and (pointer: fine), so a tap on a touch device doesn't strand an element in its hovered state. - Everything must have a
prefers-reduced-motion: reducepath, including the canvas — it drops to a single static frame rather than animating. - The canvas loop only runs while the figure is on screen and the tab is visible. Keep it that way.
Content and i18n
- Every user-visible string goes through
t()and lives intranslations.jsonwith bothenandzh. No exceptions, and no English fallback left as a TODO.npm run checkfails on a missing language. - Keys are flat snake_case, grouped by section in file order.
translations.jsonis hand-aligned — never rewrite it with a JSON serializer. Short entries sit on one line with the{and the"zh"column aligned across their group; long ones expand to a block; blank lines separate sections.json.dump/JSON.stringifydestroys all of it and turns a two-key change into a 300-line diff. Edit the text directly, and match the shape of the neighbouring entries.- Project entries need matching
<id>_titleand<id>_desckeys. The full procedure for adding a project or a tag is in docs/CONTENT.md. - Chinese is not a machine translation of the English — it is written to read naturally in Chinese. If you are not confident writing it, say so rather than guessing.
- Never invent a project, a skill, a date, or a credential. This page represents a real person applying for real roles. Content facts come from the user, not from you.
- A project image is optional and a missing image is handled gracefully —
ProjectFigureremoves itself on the imageerrorevent, so a row can be added before its screenshot exists.npm run checkwarns about these rather than failing.
Accessibility
Non-negotiable, and cheap to keep.
- Decorative elements get
aria-hidden="true". Icon-only controls get anaria-label. - Body text holds ≥ 4.5:1 on both sheets.
--c-ink-3is tuned to exactly this — don't lighten it. - Don't remove the global
:focus-visibleoutline. - Honour
prefers-reduced-motion,prefers-reduced-transparency, andprefers-contrast. All three already have paths inindex.css; new work should not bypass them. - Interactive targets are sized for a fingertip, not for the glyph inside them.
Working agreements
- The user is the decision-maker on content, design direction, and scope. Fix defects freely; propose redesigns rather than performing them.
- Don't commit or push unless asked. The user drives git.
- Don't add a dependency without asking. The runtime dependency list is React and React DOM, and the page loads in ~60kB gzipped. That is a feature.
- Don't add analytics, tracking, cookies, or a backend. See docs/PRODUCT.md.
- Don't edit
dist/— it's build output and gitignored. - Append a
docs/JOURNAL.mdentry before you finish. Newest first. Say what changed, why, and what you left open.
