diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..638df31 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,215 @@ +# Claude Code — project memory for Nitro-V3 + +This file is read automatically by Claude Code at session start. It captures +the conventions and current state of this branch so a new session can hit +the ground running. + +## TL;DR + +This branch — **`feat/react19-modernization`** — is a long-running modernization +of the Nitro V3 client: bump to React 19.2 idioms, add the supporting +infrastructure (TanStack Query, Zustand, Vitest, React Compiler, error +boundaries), split a few god-hooks, and audit logic bugs along the way. +PR is **#2** on `simoleo89/Nitro-V3`. + +Detailed status, decisions, and next steps live in **`docs/ARCHITECTURE.md`** — +read that before starting anything non-trivial. + +## Commands + +| Goal | Command | +|---|---| +| Dev server | `yarn start` | +| Production build | `yarn build` | +| Lint | `yarn eslint` | +| Type-check (TS 7 native, fast) | `yarn typecheck` | +| Test (Vitest, once) | `yarn test` | +| Test (watch) | `yarn test:watch` | + +The renderer SDK (`@nitrots/nitro-renderer`) is consumed via a filesystem +link to a sibling working tree — `../Nitro_Render_V3` (preferred) or +`../renderer` (legacy). Without it, `yarn typecheck` reports TS2307 across +the codebase — that's expected on a sandbox without the renderer, not a +regression. + +## Stack snapshot + +- React 19.2.5, `react-dom` 19.2.5, `@types/react` 19.2.x. +- TypeScript: TS 6 for build, **TS 7 native preview** (`@typescript/native-preview`, + invoked via `tsgo`) for the `typecheck` script. +- Vite 8 + `@vitejs/plugin-react` 6 + `babel-plugin-react-compiler` 1.0. +- ESLint 10 + `typescript-eslint` 8 + `eslint-plugin-react-hooks@7` + + `eslint-plugin-react-compiler`. +- TanStack Query 5 (`@tanstack/react-query` + devtools). +- Zustand 5. +- Vitest 3 + jsdom + `@testing-library/react` + `@testing-library/jest-dom`. +- `react-error-boundary` 6. + +## Layout convention (DO NOT CHANGE) + +Established by the team and recorded in `docs/ARCHITECTURE.md` proposal #3 +(rejected the `src/features/` alternative). Stay on this layout — every PR +that violates it will need to be reworked. + +``` +src/components/// → views (.tsx only) + e.g. src/components/room/widgets/doorbell/DoorbellWidgetView.tsx + +src/hooks/// → hooks, FLAT files, no per-feature subfolder + e.g. src/hooks/rooms/widgets/useDoorbellState.ts + src/hooks/rooms/widgets/useDoorbellActions.ts + src/hooks/rooms/widgets/useDoorbellWidget.ts (deprecated shim) + +src/api/ → cross-cutting helpers (LocalizeText, composers, formatters) +src/common/ → reusable UI primitives + error boundary +src/state/ → Zustand stores (cross-feature only) +tests/ → Vitest suites (mirror filename of subject) +``` + +When splitting a god-hook the convention is **3 files, all flat in the +hooks barrel directory**: + +- `useState.ts` — state + event subscriptions + derived values +- `useActions.ts` — pure imperative actions (no state writes) +- `useWidget.ts` — deprecated wrapper that composes the two and + preserves the old return shape so existing consumers don't break + +See `useDoorbellState`/`useDoorbellActions`/`useDoorbellWidget` as the +canonical pattern. + +## Patterns to use + +### `useNitroEventState` / `useMessageEventState` + +For "derived state from a single event" replace the two-step +`useState + useNitroEvent(e => setState(...))` with a single call: + +```ts +const foo = useNitroEventState(SomeEvent, e => e.payload, initial); +const data = useMessageEventState(SomeParser, e => e.getParser()?.field ?? null, null); +``` + +The selector is held in a `useLayoutEffect`-refreshed ref so the listener +stays registered across renders. Both hooks are exported from +`src/hooks/events`. + +### `useNitroQuery` + +For composer/parser request-response pairs: + +```ts +const { data } = useNitroQuery({ + key: ['nitro', 'domain', 'request', ...args], + request: () => new SomeComposer(args), + parser: SomeParser, + select: e => e.getParser()?.data, + accept: e => e.getParser()?.correlationKey === args, // optional, for shared event bus + staleTime: 60_000, +}); +``` + +Already wired up; `QueryClientProvider` is mounted in `src/index.tsx`. +Adopted on `OfferView`, `CatalogLayoutRoomAdsView`, `ModToolsChatlogView`, +`CfhChatlogView`. + +### Zustand stores + +For cross-feature UI state (avoid module-level `let`): + +```ts +import { createNitroStore } from '@/state/createNitroStore'; + +export const useFooStore = createNitroStore()((set) => ({ + ... +})); +``` + +Components subscribe to slices, not the whole store: + +```ts +const value = useFooStore(s => s.value); +``` + +First adoption: `src/components/navigator/views/navigatorRoomCreatorStore.ts`. + +### `WidgetErrorBoundary` + +Wrap any in-room widget tree so a crash degrades gracefully (logs to +NitroLogger, falls back to `null`). Already applied at `RoomWidgetsView` +as an umbrella; per-widget wrapping is a follow-up. + +```tsx + + + +``` + +### Form Actions + +Login / Register / Forgot in `src/components/login/LoginView.tsx` use +`useActionState` + `useFormStatus`. The legacy non-Action versions in +`src/components/login/components/{Register,Forgot}Dialog.tsx` and +`shared.ts` have been **removed** (dead code). + +## What's wired up and what isn't + +| Adopted | Pilot sites | +|---|---| +| `useNitroEventState` | `OfferView` | +| `useNitroQuery` | `OfferView`, `CatalogLayoutRoomAdsView`, `ModToolsChatlogView`, `CfhChatlogView` | +| Zustand | `NavigatorRoomCreatorView` (`useRoomCreatorStore`) | +| God-hook split | `doorbell`, `poll`, `furni-chooser`, `user-chooser`, `friend-request` | +| `WidgetErrorBoundary` | `RoomWidgetsView` umbrella | +| Vitest | 77/77 cases on pure helpers + the Zustand store | + +| Not yet | Notes | +|---|---| +| Split `useCatalog` (~1100 LOC) | Migrate read-only fetches to `useNitroQuery` first, then split into `useCatalogData` / `useCatalogUiState` / `useCatalogActions`. | +| Split `useChatInputWidget` / `useChatWidget` / `useAvatarInfoWidget` | Large state machines; needs careful per-file design before mechanical split. | +| Split `usePetPackageWidget` / `useWordQuizWidget` | Their "actions" mutate internal state; need to either pass args or hoist state to a store first. Documented in commit messages, skipped intentionally. | +| Hoist Wired Creator Tools shared state to a Zustand slice | Would remove ~25 props passed to the 3 tab sub-components. | +| Wider Vitest coverage (React components) | `@testing-library/*` is installed; needs a small renderer-SDK mock layer first. | + +## Known open logic bugs + +Read `docs/ARCHITECTURE.md` "Known logic bugs" section. The two still-open +ones: + +- `MainView.tsx:47-48` — race between `RoomSessionEvent.CREATED` and `ENDED` + (no session token guard). +- `LayoutFurniImageView` / `LayoutAvatarImageView` — async fetch race when + props change twice in quick succession. + +Fix shapes documented; both are reasonable PRs on their own. + +## House rules + +- **Commit author**: `simoleo89 `. + When committing, pass these via per-command overrides + (`git -c user.name=simoleo89 -c user.email=...`) — do NOT modify the + global git config. +- **No `claude/...` branch names** — auto-generated names should be + renamed before pushing. Prefer `feat/`. +- **Never merge a branch that violates the layout convention** above. + The `feat/react19-hooks-adapter` branch (deleted) put hooks under + `src/components/...`; that's wrong and a recurring temptation. +- **Skip-motivated god-hook splits are fine** — when a hook's actions + mutate internal state, document the reason in the commit message and + move on rather than forcing a bad split. +- **`yarn test` must stay green** on every commit. Currently 77/77. +- **Lint baseline**: don't regress. Some pre-existing errors (`FC<{}>`, + `IMessageEvent | undefined` redundant union in the local sandbox where + the renderer SDK isn't installed) are out of scope here. + +## Where everything lives + +- Architecture doc: `docs/ARCHITECTURE.md` +- Test runner config: `vitest.config.mts` (separate from `vite.config.mjs`) +- Test setup: `tests/setup.ts` +- React Query adapter: `src/api/nitro-query/createNitroQuery.ts` +- Zustand factory: `src/state/createNitroStore.ts` +- Error boundary: `src/common/error-boundary/WidgetErrorBoundary.tsx` +- Event hooks (`useNitroEvent`, `useMessageEvent`, `useNitroEventState`, + `useMessageEventState`): `src/hooks/events/` +- Wired-tools split (types/constants/helpers + 3 tab views): + `src/components/wired-tools/` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index bde6d49..950901b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -296,70 +296,175 @@ failures. ## What's already in place -The current branch (`claude/update-react-typescript-He2rs`) has applied: +The current branch (**`feat/react19-modernization`**, PR #2) has applied: -- **React 19.2 / TypeScript 7 (Native preview) / ESLint 10 / React Hooks v7 / React Compiler 1.0** — toolchain bump, all warnings audited. -- **Form Actions** — `
` + `useActionState` adopted in - `LoginView.tsx` (login, register, forgot dialogs). -- **`useEffectEvent`** — adopted in `App.tsx`, `FurniEditorSearchView`, - `NotificationBadgeReceivedBubbleView`, `NavigatorRoomSettingsRightsTabView`, - `UiSettingsContext` to clear all `react-hooks/exhaustive-deps` warnings. -- **Targeted `set-state-in-effect` cleanup** — `CatalogHeaderView` (pure - derive), `NavigatorRoomCreatorView` (lazy state init), `LoginView` +### Toolchain +- React 19.2 / `react-dom` 19.2 / `@types/react` 19.2. +- TS 6 for build + **TS 7 native preview** (`tsgo`) for `yarn typecheck`. +- ESLint 10 + `typescript-eslint` 8 + `eslint-plugin-react-hooks@7` + + `eslint-plugin-react-compiler`. +- Vite 8 + React Compiler 1.0 (`babel-plugin-react-compiler`). +- `` mounted; `App.tsx` made idempotent for the double-mount. + +### React 19 idioms +- **`forwardRef` → `ref` prop** on 7 layout/component files (11 call sites). +- **`` → ``** on 6 contexts. +- **Native `