satus
AGENTS.md1k starsChanged 15 days agoMITView on GitHub
This is the single source of truth for engineering standards in this repo. Claude Code, Codex, Cursor, and other AI tools read this file. .cursor/rules/ points here. Do not add a CLAUDE.md: while one exists, Claude Code skips loading this file. This file changes only by deliberate review. When next dev detects an AI coding agent it appends a managed block here (an HTML comment fencing nextjs-agent-rules); agentRules: false in next.config.ts stops that. Keep it off. If the block appears,…
- Reads credentials
Trending
Quiet this year.thin dataabout the whole repository
Who's talking about darkroomengineering/satus
- sweeneytr/tristansweeney.com“Links to add”GitHub · Apr 24, 2025
- vercel/next.js“(turbopack) doesn't support mixing units in sass calculations, while webpack does”GitHub · May 1, 2024
The biggest 2 of 2 mentions linking to the repository that our crawl found. Each line is their own headline.
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,700 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository3 other files it gives its agents.
- .cursor/rules/main.mdcCursor rule
- next-bundle-optimizerSkill
- react-doctorSkill
The file
Show its rules and text
We split this AGENTS.md into the rules it gives an agent
30 rules. About 0 are common, 1 is distinctive, and 29 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
29
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- could be automatic: a setting could enforce it
- Very specific: line 76
Use named function declarations for components, interface for object shapes, and type for unions/intersections.
- Very specific: line 78
Use kebab-case filenames (my-component.tsx, my-component.module.css) and camelCase CSS classes (.isPrimary, .isDisabled).
- Very specific: line 82
Use Tailwind for spacing, colors and typography.
- Very specific: line 83
Keep class lists inline; the lint rule covers stored lists.
- Very specific: line 85
Use h-svh for documents and heroes, h-dvh only for app shells tracking the browser toolbar, never h-screen.
- Very specific: line 92
Use useReveal (lib/hooks/use-reveal.ts) for CSS-driven entrances: it stays on the compositor, renders visible without JS and short-circuits …
- Very specific: line 93
Use anime.js for orchestration and scrubbing (createTimeline, animate with autoplay: onScroll({ sync: true }), splitText).
- Very specific: line 109
Use React state (useState, useReducer) for component state and Zustand stores (for example, lib/webgl/store.ts) for global state.
- Very specific: line 110
Use context for shared subtree state, shaped { state, actions, meta?
- Very specific: line 114
Add 'use client' only for hooks, event handlers or browser APIs.
- Very specific: line 132
Prefer E2E as the sole test mechanism for complex behavior.
- Distinctive: line 133
Never write unit tests after code: they tend to assert implementation rather than required behavior.
- Very specific: line 144
Use conventional commits: feat:, fix:, refactor:, docs:, chore:.
- Very specific: line 145
Do not force-push main or use --no-verify unless explicitly requested.
- Very specific: line 166
Use <Fragment ref={ref}> to focus or observe sibling DOM nodes without a wrapper.
- Very specific: line 167
Use use(browser()) in a Client Component beneath Suspense when meaningful server HTML is impossible.
- Very specific: line 169
Use <Activity /> for off-screen tabs, carousels and accordions.
- Very specific: line 172
Do not use forwardRef; React 19 refs are props and lint blocks the import.
- Very specific: line 178
Do not use 'use cache', cacheLife, cacheTag or export const instant.
- Very specific: line 181
Verify caching with bun run build && bun run start.
- Very specific: line 185
Read pages with sanityFetch (lib/integrations/sanity/fetch.ts).
- Very specific: line 187
Read only through sanityFetch (pages, generateMetadata) or sanityFetchPublished (sitemap, llms.txt, outside page requests).
- Very specific: line 188
Never prefix the token with NEXT_PUBLIC_.
- Very specific: line 189
After schema or query changes, run bun run sanity:extract and bun run sanity:typegen.
- Very specific: line 199
Do not put loading.tsx or <Suspense> above notFound(): streaming starts with 200 before the check, returning 200 with noindex instead of …
- Very specific: line 228
Use fetchWithTimeout for external API calls (default 10s) to bound stalled requests.
- Very specific: line 229
Read environment values through import { env } from '@/lib/env', not process.env, for typed, validated configuration.
- Very specific: line 230
Never commit plain-text secrets.
- Very specific: line 232
Keep action result shapes distinct: form actions return UI state, Shopify cart actions return CartActionResult = { ok: true } | { ok: …
- Very specific: line 262
When verifying an absent-env fallback, wipe .next before building: NEXT_PUBLIC_* values are inlined at build time, so a stale build can …
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
AGENTS.md - Satus Engineering Standards
This is the single source of truth for engineering standards in this repo. Claude Code, Codex, Cursor, and other AI tools read this file. .cursor/rules/ points here. Do not add a CLAUDE.md: while one exists, Claude Code skips loading this file.
This file changes only by deliberate review. When next dev detects an AI coding agent it appends a managed block here (an HTML comment fencing nextjs-agent-rules); agentRules: false in next.config.ts stops that. Keep it off. If the block appears, revert it and do not commit it. Do not include its opening marker in prose: Next searches for that exact string, and a match truncates everything below it on the next next dev run.
Stack
| Layer | Technology |
| Framework | Next.js 16 (App Router, proxy.ts) |
| UI | React 19.3 (React Compiler ON, no manual memoization) |
| Language | TypeScript 7, strict: true |
| Styling | Tailwind v4 (CSS-first) + CSS Modules |
| Runtime | Bun |
| Linter / Formatter | oxlint + oxfmt |
| Validation | Zod |
| Animation | Lenis, anime.js, Tempus |
| 3D (optional) | React Three Fiber, @react-three/drei |
Enforced Rules (CI fails without these)
bun run check and CI enforce these rules; the pre-commit hook runs oxlint and tsc. Type-aware rules run only in lint:types, check, and CI because next build does not typecheck (ignoreBuildErrors is on).
oxlint rules
| Rule | What it catches | Enforcer |
typescript/no-explicit-any | any types | oxlint typescript |
typescript/consistent-type-imports | Missing import type for type-only imports | oxlint typescript (.ts/.tsx) |
typescript/consistent-type-exports | Missing export type for type-only re-exports | oxlint typescript, type-aware |
nextjs/no-img-element | Raw <img> tags; use @/components/ui/image for optimization and WebGL integration | oxlint nextjs |
react/forbid-elements | Raw <a> tags; use @/components/ui/link for external detection, prefetching and consistent behavior | oxlint react |
eslint/no-restricted-imports | ../../ imports (use @/ aliases), forwardRef (React 19 ref-as-prop), useMemo/useCallback/memo (React Compiler), hamo useMediaQuery (use useDeviceDetection or desktop-only/mobile-only markup), next/image (use @/components/ui/image) | oxlint eslint |
hamo/no-raw-layout-read | getBoundingClientRect, offset*/client*/scroll* sizes, window.scrollY, getComputedStyle and other forced-layout reads; use hamo useRect, useWindowSize, useResizeObserver, or lenis.scroll because raw reads force layout on every scroll/frame | tools/oxlint/hamo |
hamo/no-raw-observer | new ResizeObserver/IntersectionObserver, matchMedia, window resize/scroll listeners; use hamo, useDeviceDetection, or Lenis | tools/oxlint/hamo |
styling/no-class-string-variable | Tailwind class lists stored in variables, cn() results, variant maps, or helper returns; write them in className, extract a component, or use the CSS module because stored lists travel across files and skip oxfmt sorting and IntelliSense | tools/oxlint/styling |
eslint/no-unused-vars | Unused imports, variables and parameters | oxlint eslint |
react/jsx-key | Missing key in list renders | oxlint react (.tsx/.jsx) |
jsx-a11y/alt-text | Images without alt text | oxlint jsx-a11y |
react/button-has-type | <button> without type | oxlint react |
react/no-danger-with-children | XSS risk | oxlint react |
import/first | Imports outside the top of a file | oxlint import |
react/rules-of-hooks | Hooks called conditionally or outside components | oxlint react |
typescript/no-floating-promises | Un-awaited promises | oxlint typescript, type-aware |
typescript/no-misused-promises | Async functions passed where void is expected | oxlint typescript, type-aware |
Fix lint errors in code; never disable a rule to get green. bun run check:disables (part of bun run check) counts every oxlint-disable/eslint-disable directive per rule and @ts-ignore, @ts-expect-error, and @ts-nocheck under their own names. Counts must match lint-disables.json, and each directive must include -- <reason> after rule names. An agent does not add a disable or change counts; report the error and the unresolved fix instead. The count makes suppressions visible for reviewer approval.
The hamo plugin (tools/oxlint/hamo/) routes layout reads and observers through a shared observer that measures once per resize; a raw read forces layout on every scroll or frame. Tests, e2e specs and lib/scripts/** are exempt.
The vendored tools/oxlint/anti-slop/ plugin (from dmmulroy/anti-slop) rejects Reflect.get/Reflect.apply, object-typed parameters, aliases that only rename unknown, widen-then-assert flows, and module mocking in tests. All fifteen rules are errors. eslint/prefer-named-capture-group and typescript/prefer-nullish-coalescing remain off with finding counts in oxlint.config.ts; enable them one at a time as findings are fixed. Run plugin tests with bun run test:oxlint-plugin under Node, not bun test: files use *.ruletest.ts because oxlint RuleTester refuses Bun.
oxfmt sorts Tailwind classes and imports, so sorting is fixed by bun run format or format-on-save and never fails bun lint. bun lint:types runs the two type-aware rules because syntax-only lint cannot detect them; it is in bun run check, not the pre-commit hook, to keep commits fast. Toolchain details: ARCHITECTURE.md § Linting and Formatting.
TypeScript strict flags (all active in tsconfig.json)
strict, noImplicitOverride, exactOptionalPropertyTypes, useUnknownInCatchVariables, noFallthroughCasesInSwitch, noImplicitReturns, noUnusedLocals, noUnusedParameters, noUncheckedIndexedAccess, noUncheckedSideEffectImports, verbatimModuleSyntax
Path aliases (required, enforced by eslint/no-restricted-imports)
Use @/components/ui/image and @/components/ui/link, not next/image or next/link; use named aliases @/hooks/*, @/styles/*, @/integrations/*, @/webgl/*, @/utils/*, @/config, @/dev, and @/dev/*. Everything else, including @/components/* and @/lib/*, resolves through @/*, mapped to the repository root in tsconfig.json (for example, @/components/ui/link resolves to components/ui/link). Aliases avoid deep relative imports and preserve stable paths.
House Style (Darkroom conventions)
These conventions are subjective starter choices and can change in a fork without breaking the build. Worked component patterns live in ARCHITECTURE.md § Code Patterns.
Component file shape
- Use named function declarations for components,
interfacefor object shapes, andtypefor unions/intersections. - Extend
ComponentProps<'element'>when wrapping HTML elements. - Use kebab-case filenames (
my-component.tsx,my-component.module.css) and camelCase CSS classes (.isPrimary,.isDisabled).
Styling split
- Use Tailwind for spacing, colors and typography; CSS Modules for complex animations, custom layouts and specificity. Combine with
cn()fromclsx. - Keep class lists inline; the lint rule covers stored lists. Repeated one-off classes become an
@utility, repeated markup a component, and repeated conditions stay inline incn(). - A component exposing
classNameputs its CSS module in@layer components: Tailwind utilities then override it.image.module.cssis the exception, layered intoutilitiesso Tailwindobject-*wins by specificity. Notailwind-mergeis needed because layers resolve module/utility precedence, not utility/utility conflicts. - Use
h-svhfor documents and heroes,h-dvhonly for app shells tracking the browser toolbar, neverh-screen. - Keep
<Wrapper safariEdges>strips (components/layout/safari-edges) outside transformed ancestors because transforms breakposition: fixed. They color Mobile Safari's status bar and toolbar via--edge-top/--edge-bottom(default--color-primary); each is one solid color. Drawing behind the notch also requiresviewportFit: 'cover'and safe-area padding, which satus does not enable. See the technique. - Animate only
transformandopacityfor compositor performance. - Gate below-fold CSS
background-imageURLs behind hamouseIntersectionObserver(rootMargin: '100% 0px') or use<img loading="lazy">: rendered elements download backgrounds at page load even when hidden, adding 302 KB and 5.4 seconds on Slow 3G in the measured case.
Animation
- Use
useReveal(lib/hooks/use-reveal.ts) for CSS-driven entrances: it stays on the compositor, renders visible without JS and short-circuits reduced motion. Stagger withdata-reveal-item; tune--reveal-transform,--reveal-stagger,--reveal-duration. The mechanism lives inglobal.css. - Use anime.js for orchestration and scrubbing (
createTimeline,animatewithautoplay: onScroll({ sync: true }),splitText). The Lenis bridge (components/layout/lenis/anime.tsx) schedules anime on Tempus after Lenis writes scroll, keeping one RAF loop and matching each scrub to that frame's scroll. Simple reveals stay in CSS to avoid main-thread work. - Create animations from elements in
useEffectbecause anime selectors are global, not ref-scoped. Returnrevert()to restore inline styles and tear down linkedonScroll; revert standaloneonScroll, remove shared timeline targets withtimeline.remove(target), and revertsplitTextmarkup and registeredaddEffectanimations (seecomponents/effects/progress-text) to prevent leaks across navigation. - Pin with CSS
position: stickyinside a taller parent; anime.js has no pin. Use CSS transitions for hover/toggle micro-interactions (at most 200ms). Use Lenis for smooth scroll and Tempus for RAF scheduling. - Honor reduced motion: movement and button
:activescaling go inside@media (--motion-ok)(prefers-reduced-motion: no-preference); opacity and color transitions stay unconditional. Gate JS/WebGL withusePreferredReducedMotion. - Off-screen content keeps animating. Start infinite CSS animations paused and run them only while visible using hamo
useIntersectionObserver; skip frame-loop writes while hidden (as incomponents/ui/marquee). Per-frame writes usetransformoropacity; quantize CSS custom-property updates and skip unchanged values because they restyle every reader. - Defer WebGL contexts, shader compilation and third-party runtimes with
useAfterLoad/<AfterLoad>(lib/hooks/use-after-load.ts) because hydration work blocks already-painted DOM. The root<Canvas>waits forwindow.load;forceopts out. - WebGL draws on demand. A scene with its own animation calls
useHoldAwake(active)fromlib/webgl/utils/awake.tswhile visible or the canvas freezes after about 2 seconds without changes.<Canvas root render="always">draws every frame for debugging. Seelib/webgl/README.md§ When the canvas draws.
Design tokens and custom utilities
- Tokens:
lib/styles/css/root.css; responsivedr-*utilities:lib/styles/README.md. - Custom viewport functions:
mobile-vw(),mobile-vh(),desktop-vw(),desktop-vh(); grid sizing:columns(n). - Desktop breakpoint: 800px, defined in
lib/styles/config.ts.
State management
- Use React state (
useState,useReducer) for component state and Zustand stores (for example,lib/webgl/store.ts) for global state. - Use context for shared subtree state, shaped
{ state, actions, meta? }and declared beside its provider (seecomponents/layout/theme/index.tsx).
Server Components by default
Add 'use client' only for hooks, event handlers or browser APIs. Keep data fetching in Server Components and pass serializable props; interactive components/ui/ primitives are client components, while not-configured, not-found-view and sanity-image remain server components.
No manual memoization
React Compiler handles optimization. Never use useMemo, useCallback or React.memo; lint rejects them. Use useRef for class/object instantiation to prevent infinite loops. Compiler-recognized refs come from useRef or names ending in Ref; destructure object-carried refs such as props.refs.panel into an xRef because otherwise the compiler reads a mutable value and can skip optimization. See ARCHITECTURE.md § Code Patterns.
WebGL cleanup
Dispose materials, textures, geometries and render targets on unmount; remove listeners; gate debug UI with process.env.NODE_ENV === 'development'.
Every WebGL surface uses useDeviceDetection's isWebGL gate and a DOM fallback. It is false for software renderers (SwiftShader, llvmpipe), narrow viewports and touch-only devices (isTouchOnly: coarse pointer, no hover), including landscape phones and tablets past 800px. Cloud Lighthouse has no GPU; ungated shaders can peg its main thread and stop the audit.
Fonts
next/font preloads every loaded family, competing with render-blocking CSS on slow links. Set preload: false for below-fold-only families. For licensed self-hosted fonts, subset to rendered characters: preload a base file (Basic Latin plus used punctuation), and declare an -Ext file (remaining Latin-1 and General Punctuation) with unicode-range so it downloads only when needed.
Testing
- Prefer E2E as the sole test mechanism for complex behavior. Use Playwright (
e2e/*.e2e.ts) and attach a screenshot/recording withtestInfo.attach()or produce a report reproducible by rerun. - Never write unit tests after code: they tend to assert implementation rather than required behavior.
- For isolated systems, list failure modes before coding and assert them. Existing unit tests cover CLI scripts (
setup:project,handoff), configured-integration paths (E2E has no credentials), and cross-module invariants that E2E cannot reach. - A test skips only for a dependency CI lacks; anything CI has fails when missing. Current skips:
e2e/render-on-demand.e2e.ts: software or missing WebGL2 (CI's SwiftShader);E2E_GPU=1runs it.e2e/route-sweep.e2e.ts: routes inOPT_OUT, reason in the test name.e2e/secrets-in-bundle.e2e.ts: locally, without theleakcheck-*values CI sets.lib/scripts/prerender-manifest.test.ts: without a prior build, except in ci.yml's build job (PRERENDER_MANIFEST_REQUIRED).lib/scripts/setup-project.full-run.test.tsandsetup-project.preflight-and-prune.test.ts: the chmod cases, as root.
Git
- Use conventional commits:
feat:,fix:,refactor:,docs:,chore:;lib/scripts/check-commit-msg.tsrejects other first lines and lists allowed types. - Do not force-push
mainor use--no-verifyunless explicitly requested.
Engineering rules
Comments calling a choice deliberate name its cost; otherwise they hide an unresolved trade-off.
Integration configuration has one answer across lib/env.ts, per-integration env.ts files and lib/utils/validation.ts. When modules cannot be shared, such as client/server dual-compiled lib/integrations/sanity/env.ts, tests assert they stay aligned. A shared module can still answer differently per side if fallbacks are not statically inlinable.
setup:project treats thrown and collected failures as blockers to self-prune. Required transforms with missing targets are collected, not skipped, so self-prune cannot delete the script needed to rerun after repair.
Ship every changed behavior claim with its fix in the changelog, README, JSDoc or audit ledger; otherwise inaccurate records hide incomplete fixes.
Worked patterns (compound components, context, server/client split, integration optionality, WebGL lifecycle, useRef instantiation) live in ARCHITECTURE.md § Code Patterns.
Stack-Specific Notes
React 19.3
- Use
<ViewTransition>for page transitions andstartTransitionfor local non-urgent changes. Shared names are unique among visible participants. Theglobal.csspage crossfade changes opacity only and works under reduced motion; put moving/resizing::view-transition-*rules inside@media (--motion-ok)because React does not disable them. Scroll entrances useuseReveal, orchestration uses anime.js. Seecomponents/layout/README.mdfor contract and opt-out. - Use
<Fragment ref={ref}>to focus or observe sibling DOM nodes without a wrapper. AFragmentInstanceis not an HTMLElement or anime.js target. The cart restores previous focus in Effect cleanup, including Strict Mode replay. - Use
use(browser())in a Client Component beneath Suspense when meaningful server HTML is impossible.lib/featurescombines it with lazy imports and independent boundaries; it does not replace code splitting oruseAfterLoadwindow-load scheduling for GPU boot. - A Server Component may render Context imported from a client module when its value is serializable. Remove only providers that forward data; Theme, Cart and GPU providers own client state/effects.
- Use
<Activity />for off-screen tabs, carousels and accordions. Hidden content retains DOM/state, renders at lower priority and cleans up Effects, but pre-rendering still costs work and memory.Tabs.Panel preserveStateactivates it after Base UI exit; retain WebGL capability and load gates. useEffectEventkeeps event logic out of effect dependencies.- Shopify passes
cacheSignal()so an in-flight request is cancelled when its server render ends or aborts (lib/integrations/shopify/client.ts). - Do not use
forwardRef; React 19 refs are props and lint blocks the import.
Caching
cacheComponents is off in next.config.ts; partialPrefetching and cachedNavigations require it. Pages without request-time reads prerender as static HTML; routes reading params without generateStaticParams, cookies or headers render per request. Cache per fetch, and use revalidateTag() in POST /api/revalidate to expire tagged static pages. Draft mode bypasses static pages and reads uncached. See ARCHITECTURE.md § Caching for the diagram.
- Do not use
'use cache',cacheLife,cacheTagorexport const instant; they requirecacheComponentsand some fail the build while it is off. Route segment config (dynamic,revalidate) works. - Set caching explicitly on cached fetches (
next: { revalidate, tags }orcache: 'force-cache'): Next does not cache by default, andnext: { tags }alone only caches duringnext build. - A
GETroute handler is dynamic unless it exportsdynamic = 'force-static'(seeapp/llms.txt/route.ts). - Verify caching with
bun run build && bun run start; dev renders every request and does not show cached behavior.
Sanity content and caching
Read pages with sanityFetch (lib/integrations/sanity/fetch.ts); published reads use sanityFetchPublished (lib/integrations/sanity/published.ts) with tag sanity and a 30-day revalidate backstop. The signed publish webhook calls revalidateTag('sanity', { expire: 0 }). Draft mode uses the server-only SANITY_API_READ_TOKEN, the sanity-preview-perspective cookie, stega markers and uncached reads; SanityVisualEditing refreshes through router.refresh().
- Read only through
sanityFetch(pages,generateMetadata) orsanityFetchPublished(sitemap,llms.txt, outside page requests); direct client fetches bypass the intended cache and token boundary. - Never prefix the token with
NEXT_PUBLIC_; Next would inline it into the browser bundle. - After schema or query changes, run
bun run sanity:extractandbun run sanity:typegen; TypeGen usessanity.cli.ts.
Next.js 16 Request Proxy
proxy.ts handles the SITE_PASSWORD HTTP Basic gate on deployments except next dev; revalidation and draft-mode webhooks stay open, while matcher-skipped paths such as client chunks and public/ files are never gated. It rate-limits /api/* through @/utils/rate-limit, except signed /api/revalidate, which uses its secret check. Security headers stay in next.config.ts, composed from registry cspSources by lib/integrations/csp.ts plus project origins in PROJECT_CSP_EXTRA_SOURCES; see SECURITY.md.
Error boundaries
Keep app/(site)/error.tsx and app/global-error.tsx as direct shared ErrorView exports: Next's route convention provides the boundary, and reset() clears client state only. For component isolation use catchError from next/error, not a custom boundary: it preserves notFound()/redirect() and retries failed Server Components in place. Its fallback must not depend on Wrapper, which may have caused the failure.
Do not put loading.tsx or <Suspense> above notFound(): streaming starts with 200 before the check, returning 200 with noindex instead of 404. In app/(site)/, loading.tsx would cover [...slug] and every unmatched URL. Run checks before opening <Suspense>; e2e/not-found.e2e.ts asserts the 404 status.
instrumentation.ts reports render, route handler, server action and proxy errors through onRequestError. It logs JSON and POSTs to ERROR_REPORT_URL when set, using ERROR_REPORT_TOKEN as bearer auth. Include message, stack, digest, query-stripped path and route; never include headers, cookies or bodies. Next awaits delivery for up to 3 seconds; failures are logged and dropped, not retried. Replace the body to use a provider SDK; satus ships none.
Tailwind v4
Configuration is CSS-first in @theme, not tailwind.config.js. Load @import "tailwindcss"; use bg-(--brand-color) for CSS variables, left-to-right stacked variants, @utility and @variant for custom utilities, built-in container queries, and slash opacity such as bg-black/50.
Colors: always oklch (oklab for interpolation)
Author colors as oklch() in lib/styles/colors.ts, CSS modules, inline style strings and SVG fills. Do not use hex, rgb() or hsl() literals. Write alpha as oklch(0 0 0 / 0.5), not rgba().
lib/styles/colors.tsis the palette source;bun run setup:stylesgenerates theme CSS. Never hand-editlib/styles/css/tailwind.cssorroot.css.- Mix colors with
color-mix(in oklab, ...); gradients blending hues take anoklabinterpolation hint (hard stops do not). JS mixing utilities return oklch strings. - CSS keywords (
transparent,currentColor, system colors) are allowed. - GLSL in
lib/webgl/is exempt because shader math uses linear RGB andlib/webgl/utils/blend.tsports sRGB/HSL compositing specs; changing these to OKLab changes standard blend output. Comment any other non-oklch exception with the parser or spec that requires it.
AI SEO (AEO)
Policy and mechanics live in lib/seo/README.md.
Integrations
Integrations are optional and self-contained in lib/integrations/{name}/; see lib/integrations/README.md.
- Use
fetchWithTimeoutfor external API calls (default 10s) to bound stalled requests. - Read environment values through
import { env } from '@/lib/env', notprocess.env, for typed, validated configuration. - Never commit plain-text secrets. Env files are encrypted with dotenvx (README § Environment variables),
lib/env-files.test.tsenforces this, and every variable belongs in.env.example. - Server actions return
{ status: number, message: string, fieldErrors?: Record<string, string> }; client form validation reuses Zod schemas throughzodToValidator(). - Keep action result shapes distinct: form actions return UI state, Shopify cart actions return
CartActionResult = { ok: true } | { ok: false, error }for optimistic UI, and API routes return{ data, error }(seeapp/api/README.md). Their consumers differ, so unifying them breaks the contract.
Commands
bun dev # Dev server (Turbopack)
bun run build # Production build (runs setup:styles first)
bun run check # oxlint, oxfmt --check, lint:types, typecheck, test:oxlint-plugin, manifest:check, check:assets and check:disables in parallel, then bun test (must pass before pushing)
bun run check:disables # lint-disable and @ts-ignore-style directives per rule must match lint-disables.json and carry a `-- <reason>`
bun lint # oxlint
bun lint:fix # oxlint with auto-fix
bun run lint:types # oxlint type-aware rules (no-floating-promises, no-misused-promises)
bun run format # oxfmt (writes in place; sorts imports + Tailwind classes)
bun run ensure:typegen # runs `next typegen` unless next-env.d.ts and both .next/types files it imports are already there
bun run typecheck # ensure:typegen + tsc --noEmit (TypeScript 7 native)
bun run typecheck:watch # ensure:typegen + tsc --noEmit --watch (native fast watcher; live feedback)
bun test # Unit tests (bun's built-in runner; ignores *.e2e.ts)
bun run test:e2e # Playwright E2E smoke test (boots dev server automatically)
bun run setup:project # Strip unused integrations (non-interactive: --preset/--keep, --yes, --clean-homepage, --skip-install, --dry-run)
bun run doctor # Diagnose setup issuesThe lefthook pre-commit hook runs oxfmt and oxlint --fix sequentially on staged files, alongside tsc typecheck. It omits type-aware linting to keep commits fast. The commit-msg hook enforces conventional subjects.
next-env.d.ts supplies ambient .svg/.css declarations from lib/utils/types.d.ts; it must be first in tsconfig.json's include. A fresh clone lacks it, so ensure:typegen runs next typegen before typecheck/check. It verifies next-env.d.ts, .next/types/routes.d.ts and .next/types/root-params.d.ts, because the post-checkout hook deletes .next/types; once present, bun run check works before or after bun run build.
e2e/route-sweep.e2e.ts discovers every static app/**/page.tsx at collection and runs five smoke assertions. Dynamic routes ([slug], [...slug]) need a bespoke *.e2e.ts with fixtures; write one for behavior beyond the smoke (see e2e/not-found.e2e.ts).
When verifying an absent-env fallback, wipe .next before building: NEXT_PUBLIC_* values are inlined at build time, so a stale build can silently test the configured variant.
Documentation Map
| Document | Purpose |
README.md | Project overview, setup, project structure |
PROD-README.md | Production deployment notes |
ARCHITECTURE.md | Architectural decisions, patterns, customization boundaries |
COMPONENTS.md | Generated component, hook and utility inventory |
CHANGELOG.md | Release history and versioning policy |
SECURITY.md | Security policy and vulnerability reporting |
THIRD-PARTY-NOTICES.md | Third-party license attributions |
app/README.md | App Router structure, page patterns, Wrapper props |
app/api/README.md | API route conventions and inventory |
components/README.md | Component inventory and conventions |
components/layout/README.md | Header, footer and page wrapper architecture |
components/effects/README.md | Animation component docs |
components/ui/image/README.md | Image component API and WebGL integration |
components/ui/real-viewport/README.md | Real viewport unit hook and CSS variables |
lib/README.md | Library structure overview |
lib/seo/README.md | AEO/SEO module, entity facts, JSON-LD, /llms.txt, /ai, markdown mirrors |
lib/integrations/README.md | Integration index, setup:project flags, adding an integration |
lib/integrations/sanity/README.md | Sanity CMS integration docs |
lib/integrations/shopify/README.md | Shopify integration docs |
lib/integrations/hubspot/README.md | HubSpot integration docs |
lib/integrations/mailchimp/README.md | Mailchimp integration docs |
lib/integrations/turnstile/README.md | Turnstile integration docs |
lib/styles/README.md | Design system and style generation |
lib/styles/scripts/README.md | Style generation scripts |
lib/utils/README.md | Shared utility inventory |
lib/webgl/README.md | WebGL/R3F architecture, tunnel system, device gating |
lib/hooks/README.md | Custom hook inventory |
lib/dev/README.md | Debug tools suite (Orchestra) |
lib/features/README.md | Optional feature loading for app layout |
Versioning
Semver is read from a fork's perspective; policy lives in CHANGELOG.md.
Built with Satūs by darkroom.engineering
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

