Commit Graph

526 Commits

Author SHA1 Message Date
simoleo89 c35a2d4b4f fix(useSessionSnapshots): defensive guards against missing renderer methods
The snapshot hooks were chained against renderer Manager methods
(getUserDataSnapshot, getIgnoredUsersSnapshot, subscribe, …) under the
assumption that the resolved \`@nitrots/nitro-renderer\` bundle always
includes the v2.1.0+ snapshot API.

That assumption fails in two real scenarios:

1. A stale \`dist/index.js\` shadows the source umbrella at resolution
   time (the vite alias commit 790ad2b mitigates this in dev, but it
   only takes effect after a server restart).
2. A consumer bundles the client against an older renderer release
   (e.g. NitroV3-Housekeeping's embedded copy in \`public/nitro3\`).

In both cases the snapshot hook calls \`undefined()\` and React shows
the error-boundary fallback "(intermediate value)() is undefined".

Wrap every renderer-side call with a typeof guard:

  const manager = GetSessionDataManager();
  if(!manager || typeof manager.getUserDataSnapshot !== 'function')
      return DEFAULT_USER_DATA;
  return manager.getUserDataSnapshot();

Module-level frozen defaults (DEFAULT_USER_DATA, EMPTY_IGNORED_LIST,
EMPTY_GROUP_BADGES, EMPTY_USER_LIST, DEFAULT_VOLUMES, NOOP_UNSUBSCRIBE)
keep the snapshot reference stable across fallback calls, so
useSyncExternalStore's bailout still works and we don't trigger render
loops on the degraded path.

Once the renderer is upgraded (or the alias kicks in after restart),
the hooks transparently switch to the real getters — no code change
needed at any consumer.

Verification: yarn typecheck clean, yarn test 207/207, yarn build green.
The fix is defense-in-depth on top of 790ad2b (vite alias) — both can
coexist, neither alone is sufficient for every deployment surface.
2026-05-18 22:06:32 +02:00
simoleo89 790ad2b279 fix(vite): alias @nitrots/nitro-renderer umbrella to source index.ts
Without an explicit alias for the umbrella package
@nitrots/nitro-renderer, vite's resolver follows the node_modules
symlink (@nitrots/nitro-renderer -> Nitro_Render_V3) and the
\`"main": "./index"\` field, which can land on a stale built
\`dist/index.js\` when one exists in the renderer working tree.

When that happens the bundle ships pre-snapshot-pattern stubs of
SessionDataManager / IgnoredUsersManager / etc. — and the new
useSessionInfo / useUserDataSnapshot code calling getUserDataSnapshot()
explodes at runtime with the Firefox error

  TypeError: (intermediate value)() is undefined

(which is Firefox's way of reporting a chain like
\`GetSessionDataManager().getUserDataSnapshot()\` where the second
method is undefined). The reported call site is ToolbarView line 46
because that's the first consumer of useSessionInfo that mounts.

Two fixes together:

1. This commit: explicit alias \`@nitrots/nitro-renderer\` ->
   \`<renderer>/index.ts\`. Subsequent transitive imports
   (export * from '@nitrots/api', '@nitrots/events', ...) still go
   through the existing per-package aliases, so all renderer code is
   guaranteed-source even when a stale dist exists.

2. Rebuild the renderer's dist (yarn build in Nitro_Render_V3) so that
   any other consumer that bypasses vite's alias resolution (e.g. an
   ad-hoc Node script) also sees the current state. Done separately.

No source code change to any consumer. The client production build
\`yarn build\` now produces a bundle containing getUserDataSnapshot,
getIgnoredUsersSnapshot, SESSION_DATA_UPDATED, IGNORED_USERS_UPDATED
and the other new symbols — verified via grep on dist/assets/*.js.
2026-05-18 22:00:52 +02:00
simoleo89 364daf478c docs(CLAUDE.md): record targeted modernizations on the 5 skip-motivated hooks
The Not-yet table previously framed useChatWidget, useAvatarInfoWidget,
usePetPackageWidget, useWordQuizWidget, useChatCommandSelector as
purely skip-motivated. The data/actions split is still a bad fit for
all five (the original reason holds), but each got a real,
non-split modernization in the 2026-05-18 session:

- usePetPackageWidget: useReducer + getPetPackageNameError pure helper
  + 4 Vitest cases (c3a76b6)
- useWordQuizWidget: closure-captured stale-state bug fix + useRef
  for the question-clear timeout (5259c89)
- useChatCommandSelector: module-level `let` cache → Zustand store
  (19b4851)
- useChatWidget: reactive ownUserId via useUserDataSnapshot (05ff7df)
- useAvatarInfoWidget: typed __nitroAvatarClickControl accessor +
  module-scope DEBOUNCE const (05ff7df)

Updated the two Not-yet rows to reflect what landed and what
remains "skip-motivated" (data/actions split specifically). Vitest
count bumped 203 → 207.
2026-05-18 21:49:17 +02:00
simoleo89 05ff7df7d2 refactor(useChatWidget,useAvatarInfoWidget): reactive ownUserId + typed avatar-click-control
Two small modernization wins on the previously skip-motivated god-hooks.
Neither hook lends itself to the data/actions split, but both had
concrete imperative-style residue worth tidying:

== useChatWidget

Replace `const ownUserId = GetSessionDataManager()?.userId || -1;` with
`useUserDataSnapshot().userId`. The previous read happened at hook mount
and stayed pinned to whatever userId the manager held at that point —
a session change (re-login without page reload) would silently corrupt
the outgoing-translation owner check below. With the snapshot hook,
the value updates reactively via SESSION_DATA_UPDATED and the
useNitroEvent re-registration picks up the fresh ownUserId for every
incoming chat event.

== useAvatarInfoWidget

Two tidy points:

- CLICK_USER_DEBOUNCE_MS (the 120ms window during which a directional
  click suppresses the context menu) lifted from inside the hook body
  to a module-level const. It's never going to change at runtime and
  doesn't depend on hook state — keeping it inside meant it was
  redeclared on every render.

- The `(globalThis as any).__nitroAvatarClickControl` read replaced by
  a typed `getAvatarClickControl()` helper backed by a proper
  `NitroAvatarClickControl` interface. Same runtime behaviour; type
  channel no longer goes through `any`, and the symbol is documented
  in one place above the hook.

Public APIs of both hooks unchanged. Suite: 207/207.
2026-05-18 21:48:17 +02:00
simoleo89 19b48513d8 refactor(useChatCommandSelector): move module-level mutable cache into a Zustand store
Two module-level `let` declarations (cachedServerCommands +
globalListenerRegistered) were tracking the AvailableCommandsEvent
listener state outside React. The pattern was a React Compiler
violation flagged elsewhere in the codebase (the navigatorRoomCreator
fix was the canonical precedent — see commit fd1835c).

Move both into a per-hook Zustand store
(`useChatCommandStore`) following the same convention as
`useWiredCreatorToolsUiStore` and `useRoomCreatorStore`. The store
keeps the cached server-pushed CommandDefinitions plus a
single-shot isListenerRegistered flag that prevents the in-hook
useMessageEvent and the module-level pre-mount listener from
double-registering.

`CLIENT_COMMANDS` stays at module scope — it's a const array,
React Compiler is fine with constant data.

Behavioural change: zero. The pre-mount registration still tries
once at module load (covering the case where the server's
AvailableCommands lands before any React widget mounts). The in-hook
useMessageEvent still covers later mounts and rank-change refreshes.
Every push goes through `setServerCommands`, so all consumers see
the same data.

Side benefit: a future test can now `useChatCommandStore.setState({
  serverCommands: [...], isListenerRegistered: true })` to seed a
deterministic fixture without monkey-patching the module.

Public API of useChatCommandSelector unchanged; the one consumer
(ChatInputView) reads the same destructured fields. Verified via grep.

Suite: 207/207.
2026-05-18 21:44:59 +02:00
simoleo89 5259c8930f fix(useWordQuizWidget): closure-captured stale userAnswers + useRef for timeout handle
Two bugs and one tidy in the word-quiz widget hook.

== Bug 1: stale-closure read in setUserAnswers updater

  setUserAnswers(prevValue => {
      if(!prevValue.has(userData.roomIndex)) {
          const newValue = new Map(userAnswers);  // <- WRONG: reads
          //                              ^^^^^^^    the closed-over
          //                                         state, not prevValue
          newValue.set(userData.roomIndex, ...);
          return newValue;
      }
      return prevValue;
  });

The functional updater is supposed to read the *latest* state (its
`prevValue` argument), not the closed-over `userAnswers` from the
render that registered this listener. The old code mixed both:
`prevValue.has(...)` for the check but `new Map(userAnswers)` for the
copy. Under rapid successive ANSWERED events for different users
within the same tick, the second update would copy a stale map and
drop the first user's entry. Fixed: use prevValue throughout.

== Bug 2: questionClearTimeout stored in useState

The timeout handle is a side-channel value, not display state. Storing
it in useState meant every (re)schedule triggered a re-render even
though no widget reads it. It also let the cleanup effect close over
a stale handle if the unmount fired between the schedule and the
state commit. Moved to useRef + a small `scheduleQuestionClear(delay)`
helper that consolidates the clear-then-set pattern (was duplicated
across FINISHED and QUESTION handlers).

== Tidy

- The duration-zero branch of QUESTION now explicitly clears any
  pending timeout instead of falling through to a `setTimeout(..., null)`
  no-op path.
- Cleanup effect rewritten as a single arrow-return for brevity.

Public API of useWordQuizWidget unchanged. Suite: 207/207.
2026-05-18 21:43:09 +02:00
simoleo89 c3a76b643d refactor(hooks/rooms): collapse usePetPackageWidget 5 useStates into useReducer
The hook tracked five related useState fields driving the pet-package
naming dialog (isVisible / objectId / objectType / petName / errorResult).
They transitioned in lockstep on the two RoomSessionPetPackageEvent
types and the inline change handler — textbook state-machine territory.

Collapse into a single useReducer with four explicit transitions:
- 'open'      → REQUESTED event lands; flips visible, records target
- 'close'     → REQUESTED-result success OR user dismiss; resets to INITIAL
- 'set-name'  → input change; updates petName AND clears any error
                (the previous code had this side effect inlined in
                onChangePetName as `if(errorResult.length) setErrorResult('')`,
                now it's part of the reducer contract)
- 'set-error' → REQUESTED-result with validation failure; sets the label

Plus extract `getPetPackageNameError(code)` to a top-level exported
pure function (was an inline closure named getErrorResultForCode).
The mapping is server-protocol contract, not UI state — moving it out
of the hook means it's testable, reusable, and won't be recreated on
every render.

Public API of usePetPackageWidget is unchanged — the one consumer
(PetPackageWidgetView) reads the same destructured fields. Verified
via grep.

Tests: 4 new cases on getPetPackageNameError covering code 0 / 1-4 /
falsy / unknown-fallback. Suite: 207/207 (was 203/203).
2026-05-18 21:41:43 +02:00
simoleo89 2db6df71a9 docs(changelog): append Phase 12 — snapshot consumer wiring + first migrations
Extend SESSION_2026-05-18_changelog.md with the five-commit Phase 12
batch landed after the original changelog (e7e8bcc) was written:

- b2a86da: feat(hooks/session) — eight consumer hooks for the renderer
  snapshot pattern (useUserDataSnapshot, useActiveRoomSessionSnapshot,
  useIgnoredUsersSnapshot + useIsUserIgnored, useGroupBadgesSnapshot +
  useGroupBadge, useVolumesSnapshot, useRoomUserListSnapshot).
- 71a0eee: refactor(hooks/session) — migrate useSessionInfo's userFigure
  / respects mirrors to useUserDataSnapshot; drop 3 useState + 2
  useMessageEvent.
- 36addbe: fix(avatar-info) — reactive Ignore/Unignore menu entry; the
  previously-stale captured boolean now flips in real time via
  useIsUserIgnored.
- 02a396d: docs(CLAUDE.md) refresh — bump Vitest 193→203, drop obsolete
  rows, document the new snapshot-consumer pattern, mark the two old
  open logic bugs as closed.

Also bumps the headline commit count 109 → 114 and updates the
end-of-session HEAD references.
2026-05-18 21:37:11 +02:00
simoleo89 02a396db36 docs(CLAUDE.md): refresh stale sections — snapshot consumer hooks + closed bugs
- Adopted table: add new row for the useSessionSnapshots consumer hooks
  (pilots on useSessionInfo + AvatarInfoWidgetAvatarView); bump Vitest
  count from 193/193 to 203/203; expand the Zustand row to note that
  the WiredCreatorTools panel-lifecycle hoist roadmap is closed (every
  remaining useState in that component is genuinely transient).
- Not yet table: drop the obsolete "hoist Wired Creator Tools derived
  state" row (done in monitorSnapshot/selection/highlight/inline-editor
  hoists + today's three picker commits). Add a new row for migrating
  remaining session-data mirrors to the snapshot pattern.
- Patterns section: new "useSessionSnapshots" entry at the top
  documenting the 8 hook menu and which pilots already use them.
- Known open logic bugs: both previously-open races are closed
  (9d10e52 + 97c9717). Replace the section with a "no open bugs" entry
  pointing readers to docs/ARCHITECTURE.md "Recently fixed".

No code changes — pure doc refresh aligning CLAUDE.md with the
current state of the branch.
2026-05-18 21:34:56 +02:00
simoleo89 36addbe7d4 fix(avatar-info): reactive Ignore/Unignore menu entry via useIsUserIgnored
The Ignore <-> Unignore context-menu entry was driven by
avatarInfo.isIgnored — a boolean captured by AvatarInfoUtilities once,
at the time the avatar was clicked. If the user got ignored / unignored
*while the popup was already open* (e.g. via the friends panel, or
because a server push flipped the state), the menu kept showing the
stale option and clicking it would no-op (or worse, double-ignore).

Switch the menu items to read useIsUserIgnored(avatarInfo.name) — the
reactive hook backed by IgnoredUsersManager.getIgnoredUsersSnapshot()
+ NitroEventType.IGNORED_USERS_UPDATED. Now the menu flips automatically
the moment the ignore list changes, without re-opening.

avatarInfo.isIgnored stays on the data object (other code paths still
consume it) — only the user-facing menu toggle is now reactive.
2026-05-18 21:33:15 +02:00
simoleo89 71a0eee195 refactor(hooks/session): migrate useSessionInfo to useUserDataSnapshot
Replace the local useState mirror of userFigure / userRespectRemaining /
petRespectRemaining (driven by useMessageEvent<UserInfoEvent> +
useMessageEvent<FigureUpdateEvent> + manual setUser after giveRespect)
with a single useUserDataSnapshot() read.

Why this works: SessionDataManager already invalidates its snapshot
on every state change that mattered to the old hook — UserInfoEvent
handler (line 142), FigureUpdateEvent listener (line 117),
giveRespect / givePetRespect (lines 540/551). The snapshot's
respectsLeft / respectsPetLeft map directly to the parser fields
respectsRemaining / respectsPetRemaining the old code mirrored.

Net result: 3 useState declarations + 2 useMessageEvent subscriptions
removed; respectUser / respectPet become trivial pass-throughs (no
post-call setState because the manager's invalidate dispatches the
event for us). UserSettingsEvent stays on useMessageEvent —
chatStyleId is not in the snapshot.

Also drops the deprecated `userInfo: UserInfoDataParser` field from
the return shape — no in-tree consumer reads it (verified via grep
across src/), it was carried as legacy clutter.

Consumers unchanged: ToolbarView, HcCenterView, ChatInputView,
AvatarInfoPetTrainingPanelView, InfoStandWidgetPetView, AvatarInfoWidget
{Avatar,Pet,OwnPet}View. All destructure individual fields, not the
deprecated userInfo.

Verification: yarn typecheck clean, yarn test 203/203.
2026-05-18 21:31:36 +02:00
simoleo89 b2a86da912 feat(hooks/session): React-side consumer hooks for the renderer snapshot pattern
The renderer exposes six referentially-stable snapshot getters under the
v2.1.0 React-friendly pattern (SessionData / RoomSession / IgnoredUsers /
GroupBadges / RoomUserList / SoundVolumes), each invalidated by a
dedicated NitroEventType.*_UPDATED dispatch. Until now nothing on the
client consumed them — useExternalSnapshot existed as a useSyncExternalStore
wrapper but no widget was wired up to a snapshot.

Add thin consumer hooks under src/hooks/session/useSessionSnapshots.ts,
each a useExternalSnapshot wrapper around the matching subscribe+getter
pair:

- useUserDataSnapshot()        → Readonly<IUserDataSnapshot>
- useActiveRoomSessionSnapshot() → Readonly<IRoomSessionSnapshot> | null
- useIgnoredUsersSnapshot()    → ReadonlyArray<string>
- useIsUserIgnored(name)       → boolean (useMemo over the array)
- useGroupBadgesSnapshot()     → ReadonlyMap<number, string>
- useGroupBadge(groupId)       → string (useMemo over the map)
- useVolumesSnapshot()         → Readonly<ISoundVolumesSnapshot>
- useRoomUserListSnapshot()    → ReadonlyArray<IRoomUserData>

Two design details worth noting:

- useRoomUserListSnapshot subscribes to BOTH ROOM_USER_LIST_UPDATED (for
  join/leave/update inside a session) AND ROOM_SESSION_UPDATED (because
  the underlying userDataManager reference flips when the active room
  session changes). A single module-level frozen EMPTY_USER_LIST is the
  fallback when no session is active, keeping reference stability across
  reads in the no-room state.
- useIsUserIgnored / useGroupBadge memoize the scalar derivation so a
  re-render only happens when the underlying snapshot reference flips,
  not on unrelated useExternalSnapshot wake-ups.

These hooks unlock per-component snapshot consumption — widgets that
previously juggled addEventListener + useState pairs (or worse, read
GetSessionDataManager().userId directly and never re-rendered) can now
go through one of these and get reactivity for free. Migration of
existing consumers (useSessionInfo, AvatarInfoUtilities, etc.) is the
next pass.

Verification: yarn typecheck clean, yarn test 203/203, yarn build green.
2026-05-18 21:24:03 +02:00
simoleo89 e7e8bcc65f docs: full changelog for feat/react19-modernization + feat/react19-event-bus
End-to-end documentation of every modification since the two branches
were opened:
- 109 commits on feat/react19-modernization (baseline ae17619)
- 22 commits on feat/react19-event-bus (baseline 98b03aa)
- the 2026-05-18 Arcturus FF pull to v4.1.16

Organized into 11 phases on the client (React 19 baseline → infra
pillars → god-hook splits → WiredCreatorTools extraction + Zustand
hoists → typecheck cleanup → error boundaries → test infrastructure →
CI → PR #126 cherry-picks + asset middleware → toolbar spam-toggle fix
(PR #130 upstream) → full upstream sync + final picker hoists) and 9
phases on the renderer (v2.1.0 React-friendly API → TS 6/tsgo → API
interface alignments → ArrayBuffer drift → Pixi v8 → composer/parser
alignment with Arcturus → dead code → upstream sync → snapshot
extensions).

Includes the full commit index per branch, the public-API additions
table, the bugs-fixed table with severity, the Vitest test-count
evolution (0 -> 203 client, 0 -> 127 renderer), and the local
rollback-tag list.
2026-05-18 21:13:43 +02:00
simoleo89 1c2d8da08d wired-tools(store): hoist managed-holder give picker chain (selectedManagedVariableEntry, selectedManagedHolderVariableId, managedGiveVariableItemId, managedGiveValue)
Move the Variable-Manage panel's four-step picker cascade into the
Zustand store. Closes the WiredCreatorTools "fragmented picker state"
roadmap item — every remaining useState in the panel is now either a
store-backed UI flag or a transient component-only value (keepSelected,
globalClock, roomEnteredAt, monitor error/log details).

All writers were already direct assignments (no updater shape), so the
setters are plain typed setters. Sentinels remain `null` / `0` / `0` /
`'0'`; the cascade reset effects at WiredCreatorToolsView.tsx:2265-2307
keep the chain self-consistent. Panel close/reopen now preserves the
managed picker state, matching the lifecycle guarantee already provided
for selection, monitor snapshot, variable highlight, and inline editor.

Tests: 4 new cases (entry select/clear, chain write, post-action reset
to sentinels, panel-lifecycle persistence). Suite: 203/203.
2026-05-18 20:40:19 +02:00
simoleo89 8894fcc959 wired-tools(store): hoist inspection give pickers (inspectionGiveVariableItemId, inspectionGiveValue)
Move the Inspection-tab Give-variable popover picker pair into the
Zustand store. Both writers use direct assignments (no updater shape),
so the store setters are plain `(next: number) => void` /
`(next: string) => void`. Defaults `0` / `'0'` match the existing
"sentinel = not selected" convention used by the reset effects at
WiredCreatorToolsView.tsx:3026-3042.

Tests: 2 new cases (set+read pair, sentinel-reset). Suite: 199/199.
2026-05-18 20:38:02 +02:00
simoleo89 ba77806f52 wired-tools(store): hoist variable-key records (selectedInspectionVariableKeys, selectedVariableKeys)
Move the last two `Record<...Type, string>` useStates out of
WiredCreatorToolsView into useWiredCreatorToolsUiStore. Both writers
were already using the `prev => ({ ...prev, [key]: value })` updater
shape, so the new store setters expose `Updater<Record<...>>` to keep
existing call sites verbatim.

Initial values default to empty strings; the existing
`variableDefinitionsByType` sync effect at WiredCreatorToolsView.tsx
:1543-1574 already populates valid keys on first render and reconciles
whenever the server-side definitions change. Closing/reopening the panel
now preserves the active picker key instead of resetting it.

Tests: 4 new cases on the store (updater shape, single-key patch
preserving siblings, direct-record write path, panel-lifecycle
persistence). Suite: 197/197 (was 193/193).
2026-05-18 20:36:17 +02:00
simoleo89 3b35fa9175 docs(CLAUDE.md): refresh upstream-sync status after merging origin/Dev b2318b9
Replace the stale 'PR #126 cherry-picks' framing with the new state:
origin/Dev is fully merged through b2318b9 (merge commit 779a98c). Note
the recurring conflict surface (App.tsx / bootstrap.ts / LoginView.tsx
React 19 imports) for future upstream syncs.
2026-05-18 20:21:54 +02:00
simoleo89 779a98cae1 merge: sync upstream duckietm/Dev (b2318b9) into feat/react19-modernization
Absorbs 10 upstream commits (JSON5 config support, user-settings reset
password/email/username, wear-badge popup fix, login screen fix, About
update, offer selection logic, client path fix).

Conflicts resolved by keeping the modernized React 19 / Zustand / Form
Actions structure and porting upstream intent surgically:

- bootstrap.ts: kept GetConfiguration().init() pre-init + useEffectEvent,
  added JSON5 import (already wired into the parse fallback)
- LoginView.tsx: kept Form Actions (useActionState/useFormStatus); the
  upstream persistAccessTokenFromPayload(payload) fix was already
  integrated in the modernized SSO branch
- App.tsx: kept useEffectEvent import + StrictMode/ErrorBoundary umbrella
- vite.config.mjs: kept sirv plugin + react-compiler babel; absorbed
  upstream's base: process.env.VITE_BASE || './'
- package.json: kept superset (sirv, Vitest, Zustand, react-colorful,
  React Compiler) + added json5
- User-settings views: accepted upstream (duplicate of local cherry-pick
  2053c8e); notification badge bubble: accepted upstream fix

Verification: yarn typecheck clean, 193/193 Vitest, yarn build green.
2026-05-18 20:14:58 +02:00
duckietm b2318b9e7c 🆕 Added support for JSON5 2026-05-18 16:13:09 +02:00
DuckieTM e209146f47 🆙 Update About screen (needs a emu change as well) 2026-05-17 09:58:38 +02:00
simoleo89 4ab38d3f9a toolbar: always-mount nav rows + drive show/hide via framer variants
Replace the outer AnimatePresence wrapper around the four toolbar rows
(desktop backplate, left-nav, right-nav, mobile-nav) with always-mounted
motion.div elements driven by an isVisible-derived variant string
('visible' or 'hidden'). This eliminates the spam-toggle bug: rapid
clicks on the show/hide chevron previously left motion children in
inconsistent intermediate states (stuck opacity 0, phantom scale 0.8)
because AnimatePresence + Fragment + multiple keyed children breaks
when enter/exit cycles overlap. With variants, framer-motion's spring
solver picks up from the current animated value on each retarget, so
spam-clicking just settles smoothly toward whichever target is current.

Refactor details:

- containerVariants drops its 'exit' state (now lives in 'hidden').
- itemVariants drops 'exit' as well — animation target is the same as
  hidden, and exit doesn't apply without AnimatePresence.
- New shellVariants for the backplate.
- pointer-events is animated per-variant ('auto' visible / 'none'
  hidden) instead of pinned via a Tailwind class, so the hidden rows
  don't intercept clicks.
- Wrapper variants are computed inside the component because
  leftNavVariants.hidden depends on isInRoom (the nav slides in from
  the side in-room, from the bottom otherwise).
- Variant inheritance: outer wrapper drives 'visible'/'hidden';
  inner container (containerVariants) and items (itemVariants) inherit
  via framer's variant propagation, so stagger runs in both directions
  without needing AnimatePresence.
- Inner AnimatePresence around the Me popover stays — it has a single
  keyed child with a clean conditional and doesn't suffer from the
  Fragment-wrapping issue.

Cleanups while here:

- Dropped hasDesktopUnifiedShell: always equal to isToolbarOpen inside
  the isInRoom-gated block, so the ternary was always picking one
  branch. Inlined.
- Dropped showDesktopShell: same redundancy inside the (now removed)
  AnimatePresence. The 'else' branch of its ternary was dead code.
- Extracted spring transition constants (SHELL_TRANSITION,
  NAV_TRANSITION, ME_POPOVER_TRANSITION) so they're declared once.
- Removed pointer-events-auto from wrapper className strings where
  the variant now owns it (mobile-nav, left-nav, right-nav).

Behaviour: identical to before for a single click cycle (open → close
animates with the same spring). The previously broken spam-click path
now settles cleanly. Tests still 193/193, typecheck 0 errors, prod
build unchanged.
2026-05-16 12:52:05 +02:00
simoleo89 438b47d569 docs(claude): bump vitest count to 193/193 after editor-hoist cases 2026-05-16 12:37:44 +02:00
simoleo89 181ca096d0 wired-tools: hoist inline editor state (variables + managed holder) to the store
Move the four inline-editor useStates out of WiredCreatorToolsView and
into useWiredCreatorToolsUiStore:

- editingVariable / editingValue — Inspection-tab variables-table
  inline edit (current key being edited + in-flight input text).
- editingManagedHolderVariableId / editingManagedHolderValue — same
  pair for the Variable Manage panel's holder rows (id 0 = none).

WiredInspectionTabView drops three more props (editingVariable,
editingValue, onEditingValueChange) and consumes the store directly
for the read sides + the per-keystroke setEditingValue. The cancel /
keydown / begin handlers stay in the parent because they wrap
shouldPauseVariableSnapshotRefresh-aware logic plus selection
bookkeeping that doesn't belong to a pure tab body.

The shouldPauseVariableSnapshotRefresh derived flag still reads from
the same store now-backed values; no behaviour change on the polling
suppression path.

Tests: three new cases (set+read pair, null-clear, managed-holder
0-as-sentinel reset). 193/193 passing.
2026-05-16 12:37:29 +02:00
simoleo89 c1aafffd09 docs(claude): bump vitest count to 190/190 after highlight-hoist cases 2026-05-16 12:31:34 +02:00
simoleo89 0fc32a1e19 wired-tools: hoist variable-highlight toggle + overlays to the store
Move the highlight feature pair into useWiredCreatorToolsUiStore:
isVariableHighlightActive (toggle UI flag) and variableHighlightOverlays
(computed screen-space overlay positions). The two screen-coords effects
in WiredCreatorToolsView stay where they are (they need React's
lifecycle to install / tear down WiredSelectionVisualizer highlights on
the active room objects) but now write to setVariableHighlightOverlays.

WiredVariablesTabView drops the isVariableHighlightActive +
onToggleVariableHighlight props and consumes the store directly — same
shape as the previous tab-prop reductions on this branch. The toggle
button keeps the same UX (Highlight ↔ Undo) but no longer crosses the
prop boundary.

Direct benefit: closing and reopening the panel while a variable
highlight is active no longer flickers the overlays off and back on —
the active flag + the last-computed overlay set both persist in
zustand and the effect re-runs from the same starting point.

Tests: three new cases on the store (toggle via direct + updater,
overlay replace + clear, close/reopen persistence). 190/190 passing.

variableHighlightObjectsRef stays a useRef inside the component: it
tracks the live PIXI objects WiredSelectionVisualizer drew onto, used
only for the cleanup pass — refs don't trigger renders and don't need
to live in the store.
2026-05-16 12:31:19 +02:00
simoleo89 50fd908d5a docs(claude): bump vitest count to 187/187 after selection-hoist cases 2026-05-16 12:25:43 +02:00
simoleo89 8182e06be4 wired-tools: hoist inspection selection (+ live state + action version) to the store
Five more useStates leave WiredCreatorToolsView: selectedFurni,
selectedFurniLiveState, selectedUser, selectedUserLiveState, and the
monotonic selectedUserActionVersion counter. All five now live in
useWiredCreatorToolsUiStore; the room-event listeners
(useObjectSelectedEvent, the per-kind useMessageEvent + useNitroEvent
handlers, the per-action effects that bump the version counter) stay
in the component because they need React's subscription lifecycle —
they just call the store actions instead of setState.

Same persistence benefit as the previous monitorSnapshot pass: the
currently-inspected target survives a panel close/reopen instead of
being dropped to null on remount. Live-state setters and the action
version counter accept Updater<T> so the many `previousValue => ...`
call sites stayed verbatim.

Tests: six new cases (setSelectedFurni + null clear, functional
updater on FurniLiveState, paired setSelectedUser + LiveState,
monotonic ActionVersion via updater, close/reopen persistence). The
test fixtures use the real interface shapes — InspectionFurniSelection
includes a renderer-typed `info: AvatarInfoFurni` that is cast
through `as never` so the test doesn't have to construct the full
avatar info shape. 187/187 passing.
2026-05-16 12:25:31 +02:00
simoleo89 7758af710e docs(claude): bump vitest count to 181/181 after monitorSnapshot cases 2026-05-16 12:20:52 +02:00
simoleo89 82bccd4040 wired-tools: hoist monitorSnapshot + polling reset to the Zustand store
Move the monitor snapshot off WiredCreatorToolsView's useState into
useWiredCreatorToolsUiStore. The WiredMonitorDataEvent listener still
lives in the component (it can't move alongside without dragging
useMessageEvent into the store), but it now writes to setMonitorSnapshot
and the room-change reset calls resetMonitorSnapshot() instead of
re-instantiating the default in the component.

Direct benefit: the snapshot now survives closing and reopening the
panel between two server pushes. Before this commit, the parent
remounted on every visibility flip (parent renders null while
`!isVisible`) which dropped the snapshot back to the empty default;
the user would briefly see zeroed stats until the next `monitor:fetch`
roundtrip landed. Holding the snapshot in zustand decouples the data
from the component's mount lifecycle.

Tests: three new cases on the store cover setMonitorSnapshot,
resetMonitorSnapshot returning a fresh empty instance, and the
"close/reopen panel preserves snapshot" lifecycle. Total 181/181.
2026-05-16 12:20:35 +02:00
simoleo89 803de20dfe tests: flatten renderer mock to src/nitro-renderer.mock.ts (drop __mocks__/)
The Jest-style __mocks__/ folder added one indirection for a single
file. Move the stub to src/nitro-renderer.mock.ts at src/ root next to
test-setup.ts, drop the folder, repoint the vitest alias, and update
the lone test that imports the helpers directly (useDoorbellState).

Same behaviour, one fewer directory.
2026-05-16 11:37:33 +02:00
simoleo89 8b4308af16 tests: co-locate every Vitest suite next to its subject under src/
Eliminate the parallel `tests/` tree. Each `*.test.ts` / `*.test.tsx`
now sits in the same directory as the module it covers, mirroring its
filename (`Foo.ts` ↔ `Foo.test.ts`). The renderer-SDK mock used by
component / hook tests moves to `src/__mocks__/nitro-renderer.ts` and
the Vitest setup file becomes `src/test-setup.ts` — both still wired
through `vitest.config.mts` exactly as before, only the paths changed.

All 13 suites + 178/178 cases still pass. The production build is
unaffected: rollup only follows imports from `src/index.tsx` and never
crosses into `.test.ts` files, so test code is naturally tree-shaken
out of the bundle. `yarn build` output is byte-for-byte the same on
the user-facing chunks.

tsconfig drops the now-redundant `tests` include entry. CLAUDE.md
'Layout convention' replaces the old `tests/` row with three rows
documenting the new co-located convention, the `__mocks__/` directory
and the `test-setup.ts` entry; ARCHITECTURE.md picks up the same
update. The 'DO NOT CHANGE' qualifier on the layout is preserved —
this rewrite IS the change, decided deliberately to make tests a
first-class part of the source tree rather than a sibling project.
2026-05-16 11:35:03 +02:00
simoleo89 eb8d87969d docs(claude): record wiredCreatorToolsUiStore adoption + new test count
Zustand row in 'Adopted' now lists both store adoptions; the 'Not yet'
row reframes the Wired Creator Tools follow-up as 'hoist the *derived*
event-driven state' since the UI flags are now done. Vitest count
bumped to 178/178 and the second store suite is mentioned.
2026-05-16 11:22:50 +02:00
simoleo89 c16ac1d276 wired-tools: hoist UI-only state flags to Zustand store
Move 14 pure UI flags off useState in WiredCreatorToolsView and into a
new feature-local Zustand store (useWiredCreatorToolsUiStore): tab
navigation (isVisible, activeTab, inspectionType, variablesType), modal
open flags (monitor history/info, inspection give, variable manage,
managed give), and the variable-manage / monitor-history filter +
sort + page selectors. The setters accept either a value or a (prev =>
next) updater to preserve the toggle/pagination call sites.

WiredInspectionTabView and WiredVariablesTabView now consume the store
directly for inspectionType / variablesType / isInspectionGiveOpen,
dropping six props from their interfaces. Behaviour is unchanged: every
listener and memo in the parent still reads the same values through
selectors, and the new tests pin the defaults and setter semantics
across the 14 flags.

Derived selection state (selectedFurni, monitorSnapshot, variable
highlight overlays, etc.) intentionally stays in the parent for this
pass — moving those requires moving their listener effects too.
2026-05-16 11:21:10 +02:00
DuckieTM 54884835b1 Merge pull request #128 from RemcoEpicnabbo/dev
Simplify offer selection and activation logic
2026-05-15 14:33:17 +02:00
Remco Epicnabbo 35385ffdd0 Simplify offer selection and activation logic
Always call applySelectedOffer(offer) and consolidate activation into a single conditional. Removed the separate non-lazy branch and now only call offer.activate() when offer.isLazy && offer.offerId > -1, reducing duplicated logic and simplifying the flow.
2026-05-15 13:55:56 +02:00
DuckieTM 123c2fca7b Merge pull request #127 from duckietm/Dev
Dev
2026-05-15 13:16:04 +02:00
duckietm 0199437a82 🆙 Small fix login screen 2026-05-15 13:15:30 +02:00
duckietm 8b54a3ab92 🆙 Small fix when client is other paths like /client 2026-05-15 10:34:15 +02:00
simoleo89 ab93113ce7 widgets: wrap each room + furniture widget in its own WidgetErrorBoundary
The umbrella boundary on RoomWidgetsView caught any widget crash but
unmounted every sibling along with the failing widget — a single bad
parser in ChatWidget would dark out the avatar info, chat input,
doorbell and all furniture overlays until the next remount.

Wraps each of the 13 direct children of RoomWidgetsView (AvatarInfo,
Chat, ChatInput, Doorbell, RoomTools, RoomFilterWords, RoomThumbnail,
FurniChooser, PetPackage, UserChooser, WordQuiz, FriendRequest, plus
the FurnitureWidgets umbrella) and each of the 20 sub-widgets inside
FurnitureWidgetsView in its own named WidgetErrorBoundary. A crash
now silently logs through NitroLogger with the widget name and
renders null for that one widget; every sibling keeps rendering.

The outer umbrella stays as defense-in-depth for the wrapper div and
the listener setup in RoomWidgetsView itself.

Closes the "Per-widget WidgetErrorBoundary wrapping" roadmap item;
updates CLAUDE.md and docs/ARCHITECTURE.md accordingly.
2026-05-14 20:18:38 +02:00
simoleo89 97c9717253 fix(layout-image): guard async image fetch with a request-id ref
LayoutFurniImageView and LayoutAvatarImageView both fired async image
generation (TextureUtils.generateImage / SDK resetFigure callback) and
wrote the result back through setImageElement / setAvatarUrl with only
an isMounted / isDisposed component-level guard. If props changed
twice in rapid succession the older request could resolve last and
overwrite the newer image with a stale one, visible on slow
connections or fast scroll over grids of unique items.

Each effect now captures `const requestId = ++requestIdRef.current`
and threads it into every async callback (TextureUtils.generateImage,
the SDK's resetFigure listener, the cache write). When a callback
fires it bails if `requestIdRef.current !== requestId` — only the
latest effect's callbacks make it past the gate. A stale ENDED for
the previous figure now leaves the cache and the rendered url
unchanged.

Moves both bugs from "Open" to "Recently fixed" in
docs/ARCHITECTURE.md.
2026-05-14 20:10:56 +02:00
simoleo89 9d10e52a55 fix(MainView): collapse CREATED/ENDED listeners into a session-aware reducer
Two independent useNitroEvent listeners updated landingViewVisible from
RoomSessionEvent.CREATED and ENDED with no notion of which session was
active. Under flaky websocket reconnects the events can land out of
order: a stale ENDED for the previous room arrives after CREATED for
the new one, flips landingViewVisible back to true, and the user is
left at the hotel view inside a room (or vice versa) until the next
room change.

Folds both events into one useNitroEventReducer that carries the
tracked sessionId. CREATED sets the id and closes the landing view;
ENDED is applied only when its event.session.roomId matches the
tracked id (or no session is active) — otherwise it's a stale ENDED
for a previous session and is ignored. The reducer companion is the
existing useNitroEventReducer from src/hooks/events, so no new
infrastructure.

Moves the entry in docs/ARCHITECTURE.md from "Open" to "Recently
fixed".
2026-05-14 20:09:23 +02:00
simoleo89 0f9fa1203b catalog: migrate remaining 36 useCatalog() consumers to the three filters
Replaces every direct call to the deprecated useCatalog() shim with the
targeted filter(s) (useCatalogData / useCatalogUiState / useCatalogActions).
Each consumer now subscribes only to the slice it actually reads, which
restores React Compiler memoization and stops catalog-wide re-renders
whenever an unrelated key changes.

Removes the now-unused useCatalog shim from useCatalog.ts and the
shim-specific case in tests/useCatalog.filters.test.tsx. The "all four
hooks observe the same singleton" test becomes "all three filters", since
there is no shim left to compare against. useCatalogFavorites swaps its
internal useCatalog() call for useCatalogUiState() (currentType lives in
the UI slice).

Updates CLAUDE.md and docs/ARCHITECTURE.md to reflect that all 48
historical consumers are migrated and the shim is gone.

Vitest: 162/162 (was 163 — minus the deprecated-shim contract case).
2026-05-14 20:05:44 +02:00
simoleo89 cb7502f3b0 ci: opt the JavaScript actions into Node.js 24
Node 20 is being removed from GitHub-hosted runners in Sept 2026 and
the actions/checkout@v4 / setup-node@v4 steps were warning on every
run. Set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true at the workflow
level so they pick up the Node 24 runtime now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:06:04 +02:00
simoleo89 5d7a20ac39 ci: use absolute symlink target + check out feat/react19-event-bus on the renderer fork
Two follow-up fixes after the first CI run failed with TS2307 across
~250 files:

1. The relative symlink target `../../../Nitro_Render_V3` resolves
   from inside `Nitro-V3/node_modules/@nitrots/nitro-renderer` to
   `Nitro-V3/Nitro_Render_V3` (one too few `..`) — that path
   doesn't exist, so tsgo couldn't find the renderer SDK and bailed
   on every `@nitrots/nitro-renderer` import. Switch to an absolute
   target via ${{ github.workspace }}.

2. The client depends on renderer API additions (`allowUnderpass` on
   RoomSettingsData, `sendBackgroundMessage` on IRoomSession, the
   NitroConfig Window declaration alignment) that live on
   `feat/react19-event-bus` of `simoleo89/Nitro_Render_V3` and not
   on `duckietm/Nitro_Render_V3:main`. Point the checkout at the
   fork + that branch so tsgo sees what the local working tree sees.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:02:15 +02:00
simoleo89 53fc5f09fd ci: create renderer symlink after yarn install, not before
Yarn cleans up anything in node_modules that's not declared in
package.json, so the previous order (symlink → yarn install) wiped
the link and tsgo could not resolve @nitrots/nitro-renderer.
Move the symlink step after both installs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:57:35 +02:00
simoleo89 8844cc1328 ci: run typecheck + Vitest on every push to main/feat/** and on every PR
Until now the test suite was authoritative only when run locally;
nothing stopped a commit landing with `yarn test` red. Wire up a
GitHub Actions workflow that gates push + pull_request on both
`yarn typecheck` and `yarn test --run`.

The setup mirrors CLAUDE.md's "Setup walkthrough":
- Check the client into `<workspace>/Nitro-V3`.
- Check `duckietm/Nitro_Render_V3` as a sibling at
  `<workspace>/Nitro_Render_V3`, since the build / typecheck wire
  the renderer in via filesystem aliases that expect that layout.
- Symlink `Nitro-V3/node_modules/@nitrots/nitro-renderer` →
  `../../../Nitro_Render_V3` so `tsconfig.json`'s `include`
  entry pointing at `node_modules/@nitrots/nitro-renderer/src/**/*.ts`
  actually resolves.
- `yarn install --frozen-lockfile` in both repos, then run
  `yarn typecheck` and `yarn test --run` in the client.

Node 22 (matches the local toolchain). Yarn classic, with the
workflow's `setup-node` caching the `yarn.lock` of both repos so
subsequent runs don't reinstall from scratch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:55:31 +02:00
simoleo89 59d6c4cab3 catalog: three-way singleton-filter split + first 3 consumer migrations
Completes the useCatalog decomposition. After the previous commit
extracted the pure helpers, this one splits the singleton-via-useBetween
store into three slice-specific entry points and migrates a handful of
consumers as proof.

`src/hooks/catalog/useCatalog.ts`

- Internal `useCatalogState` → renamed to `useCatalogStore` and is no
  longer exported. The full return shape is unchanged so callers that
  still go through the shim see the exact same object.
- Three new exports built on top of the same `useBetween` instance:
    - `useCatalogData()` — server-driven read-only slice (rootNode,
      offersToNodes, currentPage, currentOffer, frontPageItems,
      searchResult, roomPreviewer, isBusy, catalog localization
      version, Builders Club counters + timers).
    - `useCatalogUiState()` — UI ephemeral state + writers
      (isVisible, pageId, previousPageId, currentType, activeNodes,
      navigationHidden, purchaseOptions, catalogPlaceMultipleObjects,
      plus every `set*` writer including the ones that mutate the
      data slice on user-driven selection).
    - `useCatalogActions()` — imperative operations only
      (openCatalogByType, toggleCatalogByType, activateNode,
      openPageBy{Id,Name,OfferId}, requestOfferToMover,
      selectCatalogOffer, getNodeBy{Id,Name},
      getBuilderFurniPlaceableStatus).
- `useCatalog` is kept as a deprecated shim that returns the full
  historical surface, so the 48 existing consumers compile and run
  unchanged.

Pilot consumer migrations (3 of 48):

- `CatalogBuildersClubStatusView` — Data (furni counters, seconds
  timers) + UiState (currentType).
- `CatalogBreadcrumbView` — UiState (activeNodes) + Actions
  (activateNode).
- `CatalogNavigationItemView` — UiState (currentType) + Actions
  (activateNode).

Tests: `tests/useCatalog.filters.test.tsx` (5 cases).

`useBetween` is mocked via `vi.hoisted` so the four hooks share one
deterministic fake store — rendering the real `useCatalogStore`
would mount ~30 useState calls + open a fresh RoomPreviewer +
subscribe to a dozen renderer events, which is more than these
contract tests need.

- `useCatalogData` exposes exactly its read-only keys.
- `useCatalogUiState` exposes exactly its UI keys + setters.
- `useCatalogActions` exposes exactly its imperative ops (and
  explicitly NOT data fields — proves no leak across slices).
- Singleton identity: callbacks read through the shim are `===` to
  the ones read through the slices.
- Shim surface: the historical key set is still present so
  un-migrated consumers don't silently break.

Suite: 163/163 (was 158/158). `yarn typecheck` green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:50:56 +02:00
simoleo89 fd3ef7875d catalog: extract pure helpers + 34 cases, consume them from useCatalog
First half of the proposed `useCatalog` decomposition. The 1036-line
god-hook still owns the singleton-via-useBetween, but the pure logic
it used to define inline now lives in a dependency-free module so it
can be tested in isolation and reused by future split-out hooks
(`useCatalogData` / `useCatalogUiState` / `useCatalogActions` when
those land).

New module: `src/hooks/catalog/useCatalog.helpers.ts` (222 LOC).

- `normalizeCatalogType(type?)` — coerce the optional catalog type to
  `NORMAL` / `BUILDER`. Was a 5-line `useCallback` with an empty
  dependency array.
- `getOfferProductKeys(offer)` — produces the canonical
  `productType:id:classId` and `productType:class:className` keys
  for the resolved-offer cache.
- `findNodeById` / `findNodeByName` — DFS over the catalog tree,
  root explicitly excluded so callers can't select the synthetic
  root by mistake.
- `getNodesByOfferIdFromMap(offerId, map, onlyVisible)` — extracted
  from the closed-over `getNodesByOfferId`. The `onlyVisible`
  fallback to the full bucket when nothing visible remains is
  preserved.
- `buildCatalogNodeTree(NodeData)` — pulled out of the
  `CatalogPagesListEvent` reducer. Builds the tree and the offerId
  index in one pass; the caller now does `const { rootNode,
  offersToNodes } = buildCatalogNodeTree(parser.root)` instead of
  carrying an inline recursive walker + a local map.
- `resolveBuilderFurniPlaceableStatus(input)` — the placement
  decision tree as a pure function. The hook keeps the
  `GetRoomEngine` / `GetSessionDataManager` reads that count
  non-self, non-moderator visitors (only when the subscription has
  expired) and forwards the resulting `visitorCount` into the
  helper, so the previous early-exit semantics are preserved.

`useCatalog.ts` now imports these and removes ~140 lines of inline
copies. Net hook size: 1036 → 961 LOC. Behavior unchanged.

Tests: `tests/useCatalog.helpers.test.ts` (34 cases).

- `normalizeCatalogType` (4) — BUILDER pass-through, NORMAL
  pass-through, undefined/empty fallback, unknown string fallback.
- `getOfferProductKeys` (5) — both keys, id-only when classId<0,
  class-only when className empty, no-product short-circuit,
  empty productType short-circuit.
- `findNodeById` (5) — null input, root exclusion, immediate child,
  grandchild, miss returns null.
- `findNodeByName` (2) — match by name + root exclusion, miss.
- `getNodesByOfferIdFromMap` (5) — empty map, raw bucket pass-through,
  visible-only filter, fallback when no visible remain, miss.
- `buildCatalogNodeTree` (3) — root depth=0 + empty offer map for a
  leaf-only root, DFS traversal tracks offer→nodes across branch
  and leaf, child.parent === root.
- `resolveBuilderFurniPlaceableStatus` (10) — missing offer,
  not-in-room, owner happy path, non-owner without fallback,
  guild admin with time, furni limit reached, shared-pool override
  ignoring the limit, expired+blocked-by-visitors flag,
  expired+visitor count > 0, expired+empty room is okay.

To support the placement-status test the renderer mock gains real
numeric values for `RoomControllerLevel` (NONE..MODERATOR) and
`RoomObjectCategory` (MINIMUM..MAXIMUM); the previous string-keyed
Proxy stubs made `controllerLevel >= GUILD_ADMIN` evaluate to NaN.

Suite: 158/158 (was 124/124). `yarn typecheck` green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:42:04 +02:00
simoleo89 c4018392f9 tests: add renderer-SDK mock layer + first 2 component-/hook-level pilots
Foundations for widening Vitest coverage past the pure-helper subset.

The real `@nitrots/nitro-renderer` eagerly loads Pixi v8 and the full
Habbo message parser/composer registry at module-import time, which
jsdom cannot host: any `tests/**` file that transitively pulled a
renderer symbol would throw before a single assertion ran. That's
why the existing 8 suites all stuck to pure modules imported by
concrete path and used `import type` for renderer-side names.

Add a stub at `tests/mocks/renderer-mock.ts`, aliased over the package
via `vitest.config.mts`. It exports:

- Explicit behavioral stubs for the symbols tests actually exercise:
  `NitroLogger`, `GetEventDispatcher`, the `mockEventDispatcher`
  helper with `addEventListener` / `removeEventListener` /
  `dispatchEvent` / `hasListeners`, and `RoomSessionDoorbellEvent`
  (signature matches the real `(type, session, userName)` to keep
  tsgo happy).
- String-keyed Proxy enums for `NitroEventType`, `RoomObjectCategory`,
  `AvatarFigurePartType`, etc. — each access returns a stable unique
  string so dispatch and listener agree.
- Lightweight `class StubClass {}` placeholders for the ~30 Pixi and
  gameplay classes the `src/api/*` barrel touches at import time
  (`NitroAlphaFilter`, `NitroContainer`, `EventDispatcher`, …).
  Keeps the cascade from throwing without simulating behavior tests
  don't care about.
- Singleton getters (`GetAssetManager`, `GetCommunication`,
  `GetSessionDataManager`, …) returning a chainable Proxy so deeply
  nested `GetX().y.z(…)` access evaluates to no-op proxies.

Pilots on top of that layer (each one designed to catch a different
class of regression):

- `tests/WidgetErrorBoundary.test.tsx` (4 cases) — happy path,
  default silent fallback + `NitroLogger.error` call, custom
  fallback node, default `unknown` widget name.
- `tests/useDoorbellState.test.tsx` (7 cases) — initial empty state,
  append on `RSDE_DOORBELL`, dedup duplicate names, remove on
  `RSDE_ACCEPTED` / `RSDE_REJECTED`, ignore stale events for
  never-pending users, full unsubscribe on unmount.

Suite count now 124/124 across 10 files (was 113/113 across 8).
`yarn typecheck` still green.

Docs: CLAUDE.md's Vitest row and "Where everything lives" pointer
updated; `docs/ARCHITECTURE.md` Tests section now lists the new
suites + a description of what the mock layer covers, and the
"Wider Vitest coverage" entry in the next-steps list is reframed
from "needs a renderer mock" to "pick the next adopter".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:31:08 +02:00
simoleo89 622d73c2f0 docs: reflect PR #126 cherry-pick + boot/asset infrastructure
CLAUDE.md
- TL;DR mentions the duckietm PR #126 cherry-pick (UserAccountSettings,
  wear-badge popup fix) and the sirv-based dev asset serving so a fresh
  session knows what's living on top of upstream main.
- New patterns section for the bootstrap.ts configuration pre-init
  and the nitroAssetsServer Vite plugin, with a pointer to the
  .gitignore note explaining why public/{nitro-assets,swf} symlinks
  are a trap.
- "What's wired up" table gets two rows: Form Actions, and the PR #126
  pickup.
- "Where everything lives" gets entries for UserAccountSettingsView,
  the persistAccessTokenFromPayload helper, the asset middleware, and
  the bootstrap pre-init call.

docs/ARCHITECTURE.md
- Recently fixed: adds the useAvatarEditor PETS/MISC paletteID
  null-pointer that surfaced when the editor was opened.
- New Bonus subsections describing the boot-time orchestration in
  bootstrap.ts, the dev asset serving via sirv (and why symlinking
  under public/ is the wrong move on Windows), and the upstream
  feature catch-up via PR #126.

No code changes in this commit — pure documentation refresh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:19:34 +02:00