Add CLAUDE.md + refresh docs/ARCHITECTURE.md to current state

Two doc changes so a fresh local Claude Code session can pick up the
branch without re-discovering the conventions and the work-in-progress.

CLAUDE.md (new, repo root)
- Onboarding file Claude Code reads automatically at session start.
- TL;DR with branch name + PR number, points at docs/ARCHITECTURE.md.
- Stack snapshot (React 19, TS 7 native, Vite 8 + Compiler, Zustand 5,
  TanStack Query 5, Vitest 3).
- Layout convention spelled out — `src/components/<area>/<feature>/`
  for views, `src/hooks/<area>/<feature?>/` flat for hooks. The
  rejected-feature-folders decision is the most stepped-on rake, so
  it lives here at the top.
- The canonical 3-file god-hook split shape with doorbell as the
  reference.
- Patterns to use with copy-pasteable signatures: useNitroEventState,
  useMessageEventState, useNitroQuery (with the accept() filter),
  Zustand stores via createNitroStore, WidgetErrorBoundary.
- "Wired up vs not yet" matrix: what each pattern is adopted on and
  what the next reasonable target is.
- Pointer to the two still-open logic bugs (MainView CREATED/ENDED
  race; LayoutFurniImageView async fetch race) with fix shapes.
- House rules: commit author override, no claude/... branch names,
  never merge a layout-violating branch, skip-motivated splits are
  fine if explained in the commit message.

docs/ARCHITECTURE.md (refresh)
- "What's already in place" rewritten to reflect the full state of
  the feat/react19-modernization branch:
    * stale references to the old claude/update-react-typescript-He2rs
      branch removed
    * the three additional god-hook splits done since the last edit
      (furni-chooser, user-chooser, friend-request) added
    * the 4 useNitroQuery migration sites listed (OfferView,
      CatalogLayoutRoomAdsView, ModToolsChatlogView, CfhChatlogView)
    * the three additional WiredCreatorToolsView tab extractions
      (Monitor, Inspection, Variables) with the 4493 -> 3544 line
      counter
    * dead-code removal of the legacy login dialogs documented
    * the Vitest count updated from 22 to 77 across 6 test files
    * usePollSubscriptions hoist to RoomWidgetsView noted
- "How to pick the next refactor PR" rewritten:
    * completed items removed (the previous list still had
      "hoist usePollSubscriptions" as todo even though it's done,
      and "per-tab WiredCreatorTools split" same)
    * remaining priorities re-ordered: useCatalog migration (1),
      useCatalog split (2), per-widget error boundaries (3),
      wired-tools shared-state Zustand slice (4), the two open
      logic bugs (5), wider Vitest coverage (6).
    * "skipped intentionally" subsection added for the god-hook
      splits that need design work first (pet-package, word-quiz,
      chat-input, chat-widget, avatar-info).

Verification
- yarn test: 77/77 still passing.
- grep claude/update-react-typescript-He2rs docs/ARCHITECTURE.md: 0
  (no stale branch refs).

Now a fresh `claude` session in this repo can read CLAUDE.md, follow
the link to ARCHITECTURE.md, and start contributing without re-asking
the conventions.
This commit is contained in:
simoleo89
2026-05-11 18:05:12 +00:00
parent dbafc97e89
commit f75762a2db
2 changed files with 372 additions and 52 deletions
+215
View File
@@ -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/<area>/<feature>/ → views (.tsx only)
e.g. src/components/room/widgets/doorbell/DoorbellWidgetView.tsx
src/hooks/<area>/<feature?>/ → 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**:
- `use<Feature>State.ts` — state + event subscriptions + derived values
- `use<Feature>Actions.ts` — pure imperative actions (no state writes)
- `use<Feature>Widget.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<SomeParser, SomeData>({
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<FooState>()((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
<WidgetErrorBoundary name="ChatWidget">
<ChatWidgetView />
</WidgetErrorBoundary>
```
### 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 <simoleo89@users.noreply.github.com>`.
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/<description>`.
- **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/`
+157 -52
View File
@@ -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** — `<form action={...}>` + `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`).
- `<StrictMode>` mounted; `App.tsx` made idempotent for the double-mount.
### React 19 idioms
- **`forwardRef``ref` prop** on 7 layout/component files (11 call sites).
- **`<Ctx.Provider>``<Ctx>`** on 6 contexts.
- **Native `<script>`** in `TurnstileWidget`, `ExternalPluginLoader`,
`GoogleAdsView`.
- **Form Actions** (`useActionState` + `useFormStatus`) for the inline
Login/Register/Forgot dialogs in `LoginView.tsx`. Legacy non-Action
versions in `components/login/components/` removed as dead code.
- **`useEffectEvent`** in `App.tsx`, `FurniEditorSearchView`,
`NotificationBadgeReceivedBubbleView`,
`NavigatorRoomSettingsRightsTabView`, `UiSettingsContext`,
`TurnstileWidget` — clears all remaining `exhaustive-deps` warnings.
- Targeted `set-state-in-effect` fixes: `CatalogHeaderView` (pure derive),
`NavigatorRoomCreatorView` (lazy state init), `LoginView`
(track-previous-prop reset), `ChooserWidgetView` (callback in
`useEffectEvent`).
- **`WiredCreatorToolsView` split** — types/constants/helpers extracted to
sibling files; main view 4493 → 3901 lines.
- **Pattern #1 (`useNitroEventState`)** — implemented + 1 pilot.
- **Pattern #3 (feature folder)** — **rejected**; the existing
`src/components/<area>/` + `src/hooks/<area>/` layout is kept.
- **Pattern #4 (split god-hook)** — applied to:
- doorbell: `useDoorbellState` (data) + `useDoorbellActions`;
- poll: `usePollSubscriptions` (3 listeners) + `usePollActions`
(3 imperative actions). `useWordQuizWidget` migrated to import
`usePollActions` directly (it doesn't need the subscriptions).
- **Pattern #2 (`useNitroQuery`)** — **enabled**: `@tanstack/react-query`
installed, `QueryClientProvider` mounted, real adapter in
`src/api/nitro-query/`, first migration on `OfferView`.
- **Pattern #5 (Zustand store)** — **enabled**: `zustand` installed,
`createNitroStore` is now a real re-export, first migration converts
the `let isCreatingRoom` / `createRoomTimeout` singleton in
`NavigatorRoomCreatorView` to `useRoomCreatorStore`.
- **Test infrastructure** — Vitest 3 + jsdom + @testing-library set up.
22 smoke tests passing on the pure helpers
(`WiredCreatorTools.helpers.ts`) and the new Zustand store.
- **Bonus (error boundaries)** — `WidgetErrorBoundary` applied at
`RoomWidgetsView`.
### Patterns + adoption (proposals #1, #2, #4, #5)
- **`useNitroEventState` / `useMessageEventState`** (proposal #1) — adapter
in `src/hooks/events/`. Pilot: `OfferView`. Selector held in a
`useLayoutEffect`-refreshed ref (Dan Abramov's use-event-callback
pattern) so the listener stays mounted across renders.
- **`useNitroQuery`** (proposal #2) — **enabled**. `@tanstack/react-query` +
devtools installed; `QueryClientProvider` mounted in `src/index.tsx`.
Adapter at `src/api/nitro-query/createNitroQuery.ts` with `select`,
`accept` (correlation-key filter), `timeoutMs`, `staleTime`, plus a
lower-level `awaitNitroResponse()` for imperative use. Pilots:
`OfferView`, `CatalogLayoutRoomAdsView`, `ModToolsChatlogView`,
`CfhChatlogView`.
- **Layout / feature folders** (proposal #3) — **rejected**. The existing
`src/components/<area>/<feature>/` (views) +
`src/hooks/<area>/<feature?>/` (flat hook files) is the layout that
stays. See section 3 above for the full rule.
- **God-hook split** (proposal #4) — applied to:
- **doorbell**: `useDoorbellState` + `useDoorbellActions` + shim.
- **poll**: `usePollSubscriptions` (mounted once in `RoomWidgetsView`)
+ `usePollActions` + shim. `useWordQuizWidget` was migrated to
import `usePollActions` directly so it doesn't pull subscriptions.
- **furni chooser**: `useFurniChooserState` + `useFurniChooserActions`
+ shim. Helper `buildWallItem`/`buildFloorItem` dedupes ~50 lines
of inline `RoomObjectItem` construction.
- **user chooser**: `useUserChooserState` + `useUserChooserActions`
+ shim. Helper `buildUserItem`. Adds `?.` guards on
`roomSession?.userDataManager?` to avoid the room-transition NPE
pattern.
- **friend request**: `useFriendRequestState` (3 useState + 2 event
bridges + 1 derive effect) + `useFriendRequestActions` (thin
adapter on the friends store) + shim. Exports `ActiveFriendRequest`
type.
- **Zustand** (proposal #5) — **enabled**. `zustand` installed; factory at
`src/state/createNitroStore.ts`. First adoption: the `let isCreatingRoom`
/ `createRoomTimeout` module-level pair in `NavigatorRoomCreatorView`
replaced by `useRoomCreatorStore` (timer lives in the store closure,
survives StrictMode double-mount).
### `WiredCreatorToolsView` decomposition
- Top-level constants/types/helpers extracted to sibling files
(`WiredCreatorTools.{types,constants,helpers}.ts`).
- All four tab JSX bodies extracted into sibling components:
- `WiredMonitorTabView`
- `WiredInspectionTabView`
- `WiredVariablesTabView`
- `WiredToolsSettingsTabView` (already separate from before this PR)
- The three Monitor-tab overlay popups guarded by `{ false && ... }`
were dead duplicates of the live overlays mounted at the root level —
dropped.
- Main view: **4493 → 3544 lines** (21%).
### Tests
- Vitest 3 + jsdom + `@testing-library/react` + `@testing-library/jest-dom`
configured. Separate `vitest.config.mts` so the runner doesn't drag in
the renderer SDK aliases from `vite.config.mjs`.
- **77 cases passing** across 6 test files:
- `WiredCreatorTools.helpers.test.ts` (18) — formatters + snapshot
factory.
- `navigatorRoomCreatorStore.test.ts` (4) — Zustand store invariants
with fake timers.
- `api-utils.test.ts` (27) — `ConvertSeconds`, `LocalizeShortNumber`,
`CloneObject`, `GetWiredTimeLocale`, `WiredDateToString`,
`PrefixUtils`.
- `api-utils-extra.test.ts` (16) — `ColorUtils`, `FixedSizeStack`,
`LocalizeFormattedNumber`.
- `friendly-time.test.ts` (12) — `FriendlyTime` with a deterministic
`LocalizeText` mock (cuts the transitive renderer-SDK import).
- `yarn test` + `yarn test:watch` scripts added.
### Logic bug fixes
- Doorbell close button didn't close while users were pending
(`useEffect(() => setIsVisible(!!users.length))` overrode the close).
- Doorbell `answer()` removed users locally before the server confirmed
via `RSDE_ACCEPTED`/`RSDE_REJECTED`, desyncing on network drop.
- `RoomToolsWidgetView` wiped `nitro.room.history` from localStorage on
every `beforeunload` (every tab close).
- `AvatarInfoPetTrainingPanelView` crashed if `roomSession` was null at
parser time.
### Dead code removed
- `src/components/login/components/RegisterDialog.tsx`.
- `src/components/login/components/ForgotDialog.tsx`.
- `src/components/login/components/shared.ts` (consumed only by the two
legacy dialogs).
### Bonus
- **`WidgetErrorBoundary`** (`src/common/error-boundary/`) — wraps the
`RoomWidgetsView` umbrella. A widget crash now degrades gracefully
(logged to `NitroLogger.error`) instead of unmounting the room.
- **`CLAUDE.md`** at the repo root — onboarding file Claude Code reads at
session start. Captures the layout convention, the patterns to use,
what's wired up, what isn't, and the open logic bugs.
---
## How to pick the next refactor PR
Foundations #1#3 (React Query, Zustand, Vitest) are **done**. Order of
value/risk for the next contributor:
Foundations are **done**: React Query enabled with 4 pilot migrations,
Zustand enabled with 1 store, Vitest with 77 cases, error boundary on
the room widgets umbrella, `usePollSubscriptions` already hoisted to
`RoomWidgetsView`, `WiredCreatorToolsView` fully split per tab.
Remaining order of value/risk for the next contributor:
1. **Migrate `useCatalog`'s read-only fetches to `useNitroQuery`.**
Biggest expected payoff (cache + dedup + loading state for free).
Move the page-tree fetch first; the imperative purchase/gift flows
stay where they are. Adds tests against the new hooks as you go.
2. **Mount `usePollSubscriptions` once at room-session level** instead
of inside `useWordQuizWidget`. The shim in `usePollWidget` works for
now but is wrong design: subscriptions don't belong inside an actions
hook. Right place is probably `RoomWidgetsView` or wherever poll
state should be observable.
3. **Split `useCatalog` along the doorbell/poll lines**
The hook is ~1100 lines; start with the page-tree fetch and the
handful of fire-and-forget request/response pairs (gift wrapping
config, builders-club furni count, sellable pet palettes). The
imperative purchase / gift flows stay where they are. Add a
Vitest case per migration.
2. **Split `useCatalog` along the doorbell/poll lines**
(`useCatalogData` / `useCatalogUiState` / `useCatalogActions`,
siblings under `src/hooks/catalog/`). Only after #1 — React Query
removes ~60% of the file's responsibility, Zustand absorbs the UI
state slice.
4. **Wider Vitest coverage**: add cases for `useDoorbellState` (event
reducer), `useNitroQuery` (timeout + cleanup), the smaller pure
formatters in `src/api/`. ~20 cases gets us to a meaningful smoke
baseline.
5. **Per-tab split of `WiredCreatorToolsView`** (Monitor / Inspection /
Variables / Settings panels). Needs a tiny Zustand slice for the
shared state, then each tab moves to its own file. Unblocks the
React Compiler memoization on the parent module.
siblings under `src/hooks/catalog/`). Only after step 1 — React
Query removes ~60% of the file's responsibility, Zustand can absorb
the UI state slice.
3. **Per-widget `WidgetErrorBoundary` wrapping** inside `RoomWidgetsView`.
The umbrella is in place; granular wrapping means a crash in one
widget (e.g. `ChatWidgetView`) doesn't take down the rest of the
room overlay. Mechanical and safe.
4. **Hoist `WiredCreatorToolsView`'s shared state to a Zustand slice.**
The 4-tab split is done but the parent still passes ~25 props to
each tab. A slice at `src/components/wired-tools/wiredToolsStore.ts`
would make each tab subscribe to the keys it needs.
5. **Address the two open logic bugs** (see the "Known logic bugs"
section above): the `MainView` CREATED/ENDED race needs a session
token; the `LayoutFurniImageView` / `LayoutAvatarImageView` async
fetch race needs a request-id ref (or is solved by migrating the
image fetch to `useNitroQuery` keyed on props).
6. **Wider Vitest coverage** — next worthwhile targets: the
`useNitroQuery` adapter (timeout + cleanup + accept-filter
behavior, needs a stub for `@nitrots/nitro-renderer`),
`useDoorbellState`/`useUserChooserState` event-reducer logic
(needs the same renderer stub).
Skipped intentionally and documented in commit messages:
- `usePetPackageWidget` and `useWordQuizWidget` god-hook splits — their
"actions" mutate internal state, so a clean data/actions split would
need either action arguments or a shared store first.
- `useChatInputWidget` / `useChatWidget` / `useAvatarInfoWidget`
large state machines, need per-file design before a mechanical split.
Anything else (the `LoginView` dialog split, the
`react-compiler/react-compiler` warnings on the remaining big files,