From 772b6dd6320746bbf751ce70818c7de059f9d049 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Thu, 28 May 2026 17:54:20 +0200 Subject: [PATCH 01/13] docs: refresh CLAUDE.md after navigator modernization merge - TL;DR: working base is now main (not the old feat/react19-modernization long-running branch); document the navigator hook split landing - Add useNitroQuery fragility note: the one-shot listener pattern is unreliable for primary visible data (bit ModTools chatlogs + navigator search); reserve it for config/secondary fetches, use useMessageEvent + useState for primary content - Add navigator modernization row to the "What's wired up" table - Add navigator hook locations to "Where everything lives" --- CLAUDE.md | 52 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e9c7383..cd950d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,19 +6,27 @@ 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`. +This client carries a long-running React 19.2 modernization: React 19 +idioms + supporting infrastructure (TanStack Query, Zustand, Vitest, +React Compiler, error boundaries), god-hook splits, and logic-bug audits. -Upstream `duckietm/Nitro-V3` (`origin/Dev`) is merged in through -`b2318b9` as of 2026-05-18 (merge commit `779a98c`). That brings in -JSON5 config support, user-settings (reset password / email / change -username), wear-badge popup fix, login screen fix, About update, and -the offer-selection refactor. When syncing the next batch of upstream -commits, expect conflicts in `App.tsx` / `bootstrap.ts` / `LoginView.tsx` -on React 19 imports — always keep the modernized local version. +**Working base is now `main`** (tracking `duckietm/Nitro-V3`). The earlier +`feat/react19-modernization` long-running branch was superseded — feature +work now ships as small focused PRs against `duckietm:Dev`, staged through +Dev then merged to main. (`feat/react19-modernization` still exists on the +fork as backup; do not force-push it.) + +**Navigator modernization landed** (merged to main 2026-05-28, PRs +#168/#169/#170): the 492-line `useNavigator` god-hook was split into +`useNavigatorStore` + `useNavigatorData`/`useNavigatorUiState`/ +`useNavigatorSearch` filters (wired-tools layout), door lifecycle extracted +to `src/hooks/rooms/widgets/useDoorState.ts`, 9 UI flags moved to a Zustand +`navigatorUiStore`, search migrated to a query hook, and 5 sub-views wrapped +in `WidgetErrorBoundary`. **Caveat**: duckietm patched `useNavigatorSearch` +post-merge (`05d71dd1`) — see the `useNitroQuery` fragility note below. + +When syncing upstream, expect conflicts in `App.tsx` / `bootstrap.ts` / +`LoginView.tsx` on React 19 imports — always keep the modernized version. Local-dev game assets are served by a small Vite plugin (`sirv` middleware mounted on `/nitro-assets` and `/swf`, reading from @@ -236,6 +244,20 @@ and invalidates the query slot on every push, so server-driven refresh paths work the same as the initial request/response (e.g. ClubGiftInfoEvent firing again after the user claims a gift). +**⚠️ Fragility — do NOT use `useNitroQuery` for primary visible data.** +The one-shot listener inside `awaitNitroResponse` (register listener → +await one matching response → remove itself) is fragile against +renderer-bundle quirks: for some parsers the event fires but the listener +never matches, so the promise never resolves and `query.data` stays +`undefined` forever — the UI shows the server's response arriving in logs +but renders blank. This bit **ModTools Room/CFH chatlog** (reverted to +`useMessageEvent + useEffect`) and then **Navigator search** (P2 shipped +with `useNitroQuery`, duckietm reverted it in `05d71dd1` to the god-hook +pattern). **Rule: reserve `useNitroQuery` for config / secondary fetches +where a brief blank is tolerable. For anything that is the primary visible +content of a panel, use `useMessageEvent + useState/useEffect`** — that's +what the rest of the codebase does and it's robust. + ### Singleton-filter split for `useBetween`-based hooks When a hook backs many consumers but most only need either state OR @@ -339,6 +361,7 @@ into `configurePreviewServer` so `yarn preview` keeps working. | Zustand | `NavigatorRoomCreatorView` (`useRoomCreatorStore`), `WiredCreatorToolsView` (`useWiredCreatorToolsUiStore` — every panel-lifecycle-relevant flag, snapshot, selection, highlight, inline editor, picker chain hoisted; what's left in the component as `useState` is genuinely transient: keepSelected, globalClock, roomEnteredAt, selectedMonitorErrorType, selectedMonitorLogDetails) | | God-hook split (state + actions + shim) | `doorbell`, `poll`, `furni-chooser`, `user-chooser`, `friend-request`, `chat-input` | | God-hook split (`useBetween` singleton + state filter + actions filter + shim) | `wired-tools`, `translation`, `notification`, `friends`, `catalog` (three-way: `useCatalogData` / `useCatalogUiState` / `useCatalogActions` — all 48 consumers migrated, deprecated `useCatalog` shim removed) | +| Navigator modernization (merged to main 2026-05-28, PRs #168/#169/#170) | 492-line `useNavigator` god-hook split into `useNavigatorStore` (internal `useBetween` closure) + flat filters `useNavigatorData` / `useNavigatorUiState` / `useNavigatorSearch`; door bell/password lifecycle extracted to `src/hooks/rooms/widgets/useDoorState.ts` (dual-subscribes `GetGuestRoomResultEvent` + `GenericErrorEvent` alongside the nav store, each filtering by branch/errorCode); 9 UI flags + `currentTabCode`/`currentFilter` in Zustand `navigatorUiStore` (`src/hooks/navigator/navigatorUiStore.ts`); all 5 Navigator sub-views wrapped in `WidgetErrorBoundary`; old shim deleted. **`useNavigatorSearch` was reverted by duckietm (`05d71dd1`) from `useNitroQuery` to `useMessageEvent + useEffect`** — see the useNitroQuery fragility note. Specs/plans under `docs/superpowers/`. | | `WidgetErrorBoundary` | `RoomWidgetsView` umbrella + per-widget wrap on all 13 room widgets and all 20 furniture widgets (so a crash in one widget no longer takes down its siblings) | | Vitest | 207/207 cases — pure helpers (incl. 4 new on `getPetPackageNameError`) + 2 Zustand store suites (`navigatorRoomCreatorStore`, `wiredCreatorToolsUiStore` with 45 cases including the picker-chain hoists) + 2 component-/hook-level pilots (WidgetErrorBoundary, useDoorbellState) on top of the renderer-SDK mock at `src/nitro-renderer.mock.ts`, 34 cases on the catalog pure helpers, 4 contract cases on the catalog filters. **Tests are co-located** under `src/`, alongside their subject. | | Form Actions | Login / Register / Forgot (LoginView.tsx) | @@ -412,6 +435,11 @@ See `docs/ARCHITECTURE.md` "Recently fixed" for fix shapes. `useCatalogUiState` / `useCatalogActions` in `src/hooks/catalog/useCatalog.ts` (all 48 consumers migrated; deprecated `useCatalog` shim removed) +- Navigator hooks: `src/hooks/navigator/` — `useNavigatorStore.ts` + (internal closure), `useNavigatorData.ts` / `useNavigatorUiState.ts` / + `useNavigatorSearch.ts` (filters), `navigatorUiStore.ts` (Zustand UI + flags + `setTab`/`setFilter`). Door lifecycle: `src/hooks/rooms/widgets/useDoorState.ts`. + Specs/plans: `docs/superpowers/specs/2026-05-2*-navigator-*.md` - Renderer-SDK mock for Vitest: `src/nitro-renderer.mock.ts` (aliased over `@nitrots/nitro-renderer` via `vitest.config.mts`). Hosts the explicit `NitroLogger` mock, the `mockEventDispatcher` / From 3bce0c019134c9ce814a6155cbdd005986531b76 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Thu, 28 May 2026 18:02:48 +0200 Subject: [PATCH 02/13] feat(navigator): empty-state + skeleton views, fix double search fetch (P4 wave 1a) Visual polish, first wave: - NavigatorEmptyStateView: replaces the bare "No rooms found" text with a centered icon + message + a Create-room CTA. Reuses existing i18n keys (navigator.search.returned.no.results / .roomsettings.moderation.none / .createroom.create) so no new localization entries are needed. - NavigatorSearchSkeletonView: animate-pulse placeholder rows shown while a search is in flight and no result is cached yet (matches the HK dashboard skeleton pattern). Replaces the NitroCard.Content spinner overlay for the result list. Bug fix bundled in: NavigatorSearchView called useNavigatorSearch() a second time purely to read searchResult for its input-sync effect. Since the hook is not a useBetween singleton, that registered a duplicate NavigatorSearchEvent listener AND fired a duplicate NavigatorSearchComposer on every search. NavigatorView now owns the single useNavigatorSearch() call and passes searchResult to NavigatorSearchView via prop. Test maintenance: useNavigatorSearch.test.tsx was written for the original useNitroQuery implementation, which upstream reverted (05d71dd1) to useMessageEvent + useState. Removed the dead QueryClient scaffolding, fixed case 1 (assert no fetch starts with empty tab), dropped case 7 (the query invalidator no longer exists). 6 cases, all green. Full suite 471/471. Typecheck: only the environmental renderer-mismatch errors (soundboard / rare-values / floorplan APIs absent from the linked renderer), none in navigator files. --- src/components/navigator/NavigatorView.tsx | 12 +- .../views/search/NavigatorEmptyStateView.tsx | 33 +++++ .../search/NavigatorSearchSkeletonView.tsx | 25 ++++ .../views/search/NavigatorSearchView.tsx | 12 +- .../navigator/useNavigatorSearch.test.tsx | 127 +++--------------- 5 files changed, 93 insertions(+), 116 deletions(-) create mode 100644 src/components/navigator/views/search/NavigatorEmptyStateView.tsx create mode 100644 src/components/navigator/views/search/NavigatorSearchSkeletonView.tsx diff --git a/src/components/navigator/NavigatorView.tsx b/src/components/navigator/NavigatorView.tsx index b93b8d5..727d06b 100644 --- a/src/components/navigator/NavigatorView.tsx +++ b/src/components/navigator/NavigatorView.tsx @@ -14,8 +14,10 @@ import { NavigatorRoomCreatorView } from './views/NavigatorRoomCreatorView'; import { NavigatorRoomInfoView } from './views/NavigatorRoomInfoView'; import { NavigatorRoomLinkView } from './views/NavigatorRoomLinkView'; import { NavigatorRoomSettingsView } from './views/room-settings/NavigatorRoomSettingsView'; +import { NavigatorEmptyStateView } from './views/search/NavigatorEmptyStateView'; import { NavigatorSearchResultView } from './views/search/NavigatorSearchResultView'; import { NavigatorSearchSavesResultView } from './views/search/NavigatorSearchSavesResultView'; +import { NavigatorSearchSkeletonView } from './views/search/NavigatorSearchSkeletonView'; import { NavigatorSearchView } from './views/search/NavigatorSearchView'; export const NavigatorView: FC<{}> = props => @@ -132,7 +134,7 @@ export const NavigatorView: FC<{}> = props => - + { !isCreatorOpen &&
{ isOpenSavesSearches && @@ -140,13 +142,13 @@ export const NavigatorView: FC<{}> = props =>
}
- +
+ { (isFetching && !searchResult) && + } { searchResult && searchResult.results.map((result, index) => ) } { searchResult && (!searchResult.results || searchResult.results.length === 0) && -
- { LocalizeText(searchResult.code === 'myworld_view' ? 'navigator.roomsettings.moderation.none' : 'navigator.search.returned.no.results') } -
} + useNavigatorUiStore.getState().openCreator() } /> }
void; +} + +export const NavigatorEmptyStateView: FC = props => +{ + const { code, onCreateRoom } = props; + + const isMyWorld = (code === 'myworld_view'); + const messageKey = isMyWorld ? 'navigator.roomsettings.moderation.none' : 'navigator.search.returned.no.results'; + + return ( +
+
+ +
+
+ { LocalizeText(messageKey) } +
+ +
+ ); +}; diff --git a/src/components/navigator/views/search/NavigatorSearchSkeletonView.tsx b/src/components/navigator/views/search/NavigatorSearchSkeletonView.tsx new file mode 100644 index 0000000..95666d8 --- /dev/null +++ b/src/components/navigator/views/search/NavigatorSearchSkeletonView.tsx @@ -0,0 +1,25 @@ +import { FC } from 'react'; + +interface NavigatorSearchSkeletonViewProps +{ + rows?: number; +} + +export const NavigatorSearchSkeletonView: FC = props => +{ + const { rows = 5 } = props; + + return ( +