Imported from oBusk/authority (
AGENTS.md). Install upstream withnpx skills add oBusk/authority. Copyright stays with the author.
This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in node_modules/next/dist/docs/ (resolved from this file's directory; in monorepos the next package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by next dev — verify at node_modules/next/dist/server/lib/generate-agent-files.js. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
Authority
An authority (life) counter for the Star Realms card game, deployed to
authority.nulldozzer.io. A phone lies on the table beside the game showing
two big numbers.
Product invariants
These are deliberate decisions, not oversights. Do not "improve" them without being asked:
- Exactly two players. No player-count setting.
- Games start at 50 authority, fixed. Authority floors at 0, no ceiling.
- Neither counter is rotated 180°. Both read the same way up.
- Dark is the default regardless of the system preference
(
enableSystem={false}). The light theme exists for playing in sunlight. - Dark mode is pure black (
--background: 0 0% 0%) so an OLED screen costs almost no battery over a whole game. Do not soften it to near-black. - The wake lock has no toggle. It is on wherever the browser allows it.
- Each player has their own colour —
--player-1(sky) and--player-2(amber). Sky and amber stay distinguishable under the common forms of colour blindness; a red/green pairing would not. Both sit deliberately below--foregroundin luminance: the totals are the largest lit area on an OLED screen, so softening them is both easier on the eye and cheaper on battery. Do not "fix" them back to white. /carries the branding, and only the branding. The header holds the wordmark and the one-line tagline — the only crawlable copy on the counter screen, and itsh1. Anything longer belongs on/about.
Conventions
- Import alias is
^/, not@/— e.g.import { cx } from "^/lib/cva". - Take
cva/cxfrom^/lib/cva; those wraptwMerge. Never import them fromclass-variance-authoritydirectly. - Tailwind v4 is configured in CSS.
src/app/globals.cssis the config; there is notailwind.config.*. Use v4 spellings (size-5,focus:outline-hidden,bg-linear-to-b). - Theming is
@wrksz/themes, notnext-themes. - 4-space indent, double quotes, LF, 80 columns (see
.editorconfig). TS/TSX formatting comes fromeslint --fix; Prettier only handles md/yml/yaml/json. - Plain
.js/.mjsfiles need typed JSDoc on every function — the lint config enforces it. TS files do not.
Architecture landmarks
src/app/page.tsxis a static server shell. All interactivity lives in theAuthorityBoardclient subtree, so/prerenders to complete HTML.AuthorityBoardis a flex column:Header(branding + controls) above a two-columnmainofPlayerCounters. The controls deliberately live in the header rather than in a strip between the counters.- The no-flash score restore has two halves that must stay in sync: the
inline script from
restoreScriptSource()insrc/lib/storage.tspatches the DOM before first paint, andAuthorityBoard's lazyuseStateinitializer reads the same key so React agrees. Change one without the other and either the 50/50 flash comes back or hydration breaks. The numbers carrysuppressHydrationWarningfor exactly this reason. public/sw.jsis hand-written. BumpVERSIONwhenever you edit it —activatedeletes every cache that does not match, which is also the escape hatch for a poisoned cache. It must never intercept RSC or router-prefetch traffic;isRouterTraffic()guards that.src/instrumentation-client.tsregisters the worker and hands it the asset URLs the page already loaded. Without that handoff a cold offline load only works from the second visit.<meta name="theme-color">is owned byThemeProvidervia itsthemeColorprop, so it can follow the toggle. Never addviewport.themeColor— the theme script mutates the first matching tag, so a second one produces garbage.- Icons are committed PNGs under
public/icons/, regenerated bynode scripts/generate-icons.mjsfrompublic/icons/icon.svg. They cannot use theapp/icon.*file convention because the manifest and the service worker need stable, unhashed URLs. The same script also writessrc/app/favicon.ico(16/32/48, PNG-compressed entries) — that one uses theapp/favicon.icoconvention, which stays unhashed at/favicon.ico. Do not hand-edit any of these; changeicon.svgand re-run the script.
Cache Components hazards
cacheComponents: true is enabled. Synchronous IO during prerender —
new Date(), Date.now(), Math.random() — is a build error, including in
metadata routes and in the render body of client components (they are
server-rendered during prerender). src/app/sitemap.ts uses a literal date
string for this reason. Do not add dynamic or revalidate segment configs.
Testing
@wrksz/themesis ESM-only and Jest's CJS runtime cannot require it. Any test that mounts a component reachingThemeTogglemust mock@wrksz/themes/clientwith{ virtual: true }— seesrc/components/AuthorityBoard.test.tsx.- jsdom implements neither
HTMLDialogElement.showModalnor the Wake Lock API. The former needs stubbing; the latter is feature-detected and no-ops. - Cache Components keeps the previous route mounted but hidden on a soft
navigation, so after clicking through to
/aboutthe counter screen'sheader—h1and all — is still in the DOM. Scope browser-test selectors (main h1, noth1) rather than assuming one match. A direct fetch of/aboutis unaffected, so this never reaches a crawler. next/jestdoes not map the^/alias —jest.config.tsdoes it explicitly.
Commands
pnpm run dev # dev server
pnpm run lint # eslint + prettier check (CI)
pnpm run test-ci # jest (CI)
pnpm run build # production build (CI)
pnpm run typecheck # next typegen + tsc --noEmit
Offline and service-worker behaviour cannot be verified with next dev; use
pnpm run build && pnpm run start.