- MUST: Use TypeScript interfaces over types.
- MUST: Keep all types in the global scope.
- MUST: Use arrow functions over function declarations.
- MUST: Default to NO comments. Only add a comment when the user explicitly asks, or when the "why" is truly non-obvious - browser quirks, platform bugs, performance tradeoffs, fragile internal patching, or counter-intuitive design decisions. Never add comments that restate what the code does or what a well-named function/variable already conveys. When in doubt, leave the comment out.
- If a hack is required (like a
setTimeoutthat hides a race), prefix with// HACK: reason for hack. - Do not delete descriptive comments >3 lines without confirming with the user.
- If a hack is required (like a
- MUST: Use kebab-case for files.
- MUST: Use descriptive names for variables (avoid shorthands, or 1-2 character names).
- Example: for
.map(), useinnerNodeinstead ofn. - Example: instead of
movedusedidPositionChange.
- Example: for
- MUST: Frequently re-evaluate and refactor variable names to be more accurate and descriptive.
- MUST: Do not type cast (
as) unless absolutely necessary. - MUST: Remove unused code and don't repeat yourself.
- MUST: Always search the codebase, think of many solutions, then implement the most elegant solution.
- MUST: Put all magic numbers in
constants.tsusingSCREAMING_SNAKE_CASEwith unit suffixes (_MS,_PX). - MUST: Put small, focused utility functions in
utils/with one utility per file. - MUST: Use
Boolean(x)over!!x.
React Scan's overlay UI runs as a Preact app mounted inside a Shadow DOM (see packages/scan/src/core/index.ts). Reactivity is driven by @preact/signals, not React state.
- MUST: Read signals via
.valueinside JSX/effects:signalWidget.valuenotsignalWidget(). - MUST: Write signals atomically:
signalWidget.value = { ...signalWidget.value, dimensions: ... }. One signal per logical slice. - MUST: Use
useSignal(initial)for component-local reactive state,useComputed(() => ...)for derived values,useSignalEffect(() => ...)for subscriptions/DOM imperative work. - MUST: Wrap module-level signals exported from a tree-shaken bundle with
/* @__PURE__ */ signal(...)so production builds can drop unused state. - SHOULD: Persist user-facing state via
readLocalStorage/saveLocalStoragefrom~web/utils/helpersand seed the signal with the persisted value. - NEVER: Mirror one signal into another inside
useSignalEffect- find the single source of truth (compute on read).
Before reaching for useEffect/useSignalEffect, classify the work:
- MUST: Use
useComputedwhen the result is pure derived state from other signals. If no external system is touched, it is not an effect. - MUST: Use event handlers and direct action calls when work happens because the user clicked, dragged, or navigated. Do not watch a flag in an effect to trigger imperative logic.
- MUST: Use
useEffect(..., [])(oruseSignalEffectwith no deps) for one-time mount/cleanup of subscriptions, timers, listeners, andMutationObservers. Always return the cleanup. - MUST: Keep each effect single-purpose - one effect, one external bridge. Split mixed-responsibility effects.
- NEVER: Use an effect just to copy one signal into another.
- NEVER: Use an effect as an event bus (watching a trigger signal to run a command). Call the action directly from the event source.
- MUST: Access props via
props.title, not destructuring, when the prop is read inside an event handler that fires later or inside an effect. - SHOULD: Destructure props at the top of the body for purely-rendered values (read once during render).
- NEVER: Destructure a prop that is itself a signal-driven slice if you intend to re-read it after async work.
- MUST: Use
className(we render via Preact's React-compat JSX intsconfig.jsonjsxImportSource: preact). - MUST: Combine static
className="btn"with reactiveclassName={cn("btn", isActive.value && "active")}viacn. - MUST: Read refs in
useEffector via the callback ref pattern - DOM refs are populated after render. - MUST: Mount overlay UI under the existing shadow root from
initRootContainer()incore/index.ts. Do not append directly todocument.body. - SHOULD: Use
style={{ "--css-var": value }}for dynamic CSS variables; class toggles for boolean states. - SHOULD: Type refs as
let element: HTMLElement | null = nullwith a guard.
This is a pnpm 10 monorepo with packages/* (libraries, extension, website) and a top-level kitchen-sink/ (Playwright target). The toolchain is Vite+ (vp lint, vp fmt, vp check) and turbo for pipeline orchestration.
The root package.json declares pnpm.onlyBuiltDependencies for @parcel/watcher, esbuild, sharp, spawn-sync, unrs-resolver. Without this list, pnpm install skips their native build steps and downstream packages fail.
pnpm build must complete before pnpm test, pnpm test:e2e, or pnpm lint. After modifying source files, always rebuild before running tests. Turbo enforces this via dependsOn: ["^build"] in turbo.json.
pnpm test:e2e runs Playwright against the kitchen-sink Vite dev server on port 5173 (auto-started by playwright.config.ts). Chromium must be installed: npx playwright install chromium --with-deps.
| Task | Command |
|---|---|
| Install | pnpm install |
| Build | pnpm build |
| Dev watch | pnpm dev (watches react-scan + kitchen-sink) |
| Unit tests | pnpm test |
| E2E tests | pnpm test:e2e |
| Lint | pnpm lint (oxlint via vite-plus) |
| Lint + fix | pnpm lint:fix |
| Format | pnpm format (oxfmt via vite-plus) |
| Format check | pnpm format:check |
| Typecheck | pnpm typecheck |
| Combined | pnpm check (lint + fmt check + typecheck) |
Run checks always before committing with:
pnpm build
pnpm lint
pnpm format
pnpm typecheck
pnpm test:e2e