Commit Graph

539 Commits

Author SHA1 Message Date
simoleo89 532cb28ca7 feat(hooks): useIsModerator() + migrate 6 component reads
Adds a reactive `useIsModerator()` derived from
`useUserDataSnapshot().securityLevel >= SecurityLevel.MODERATOR`
(mirrors the renderer-side getter at SessionDataManager.ts:684), and
migrates the six React component-body reads of
`GetSessionDataManager().isModerator`:

- ToolbarView (mod-only chat-input button)
- CatalogClassicView, CatalogModernView (admin toggles in catalog
  header)
- ChooserWidgetView (room-object id column visibility)
- YouTubePlayerView (room-control affordance — hook moved above the
  `if (!isOpen) return null` early return so the hook order stays
  stable when the player opens/closes)
- CalendarView (mod-only "open all" affordance)

UX impact: any future promote/demote that flips
SESSION_DATA_UPDATED now re-renders the mod-only UI live, instead of
requiring an F5. Imperative call sites
(AvatarInfoUtilities.populate*, CanManipulateFurniture,
RoomChatHandler) still read the manager directly — they run at click
time, not in a React render, so reactivity has no upside there.

Five of the six call sites are top-level component-body reads (no
early-return interaction). YouTubePlayerView has an
`if (!isOpen) return null` below the hook list, so the hook had to
move ABOVE it; same shape as the recent CatalogPurchaseWidgetView and
CatalogItemGridWidgetView fixes.

Verification: yarn typecheck clean, yarn lint:hooks clean, yarn test
209/209.
2026-05-19 18:07:17 +02:00
simoleo89 3459400ed7 docs(claude,architecture): refresh snapshot adoption status after 2026-05-19 fix
The earlier "BLOCKED" / "rolled back" framing in CLAUDE.md +
ARCHITECTURE.md is stale: the three pilot snapshot-consumer migrations
shipped in d28819d on 2026-05-19 once the root cause was pinpointed
(`use-between` 1.x ships a dispatcher proxy that doesn't implement
`useSyncExternalStore`, so any snapshot hook called inside
useBetween(stateFn) crashes the first render).

Updated:

- CLAUDE.md → "Patterns to use → useSessionSnapshots": rewrote the
  adoption-status paragraph to record the three live consumers, the
  hard structural constraint (snapshot reads MUST be outside
  useBetween scope, with the precise dispatcher line numbers + the
  exact error fingerprint), and the fix template applied to
  useSessionInfo (outer wrapper reads the snapshot, inner state
  function keeps only use-between-safe hooks).

- CLAUDE.md → "What's wired up and what isn't" tables:
  - Adopted row for "Renderer snapshot consumer hooks" lists the
    three live consumers instead of the old "No in-tree consumers"
    note.
  - "Not yet" row renamed from "Blocked" to "Unblocked — migrate more
    consumers", with concrete next candidates
    (GetSessionDataManager().userId / userName / clubLevel /
    securityLevel, GetRoomSessionManager().getActiveSession(),
    GetSoundManager().<volume>) and a reminder of the constraint
    + the CI gate that enforces it.
  - useChatWidget.ownUserId row notes the reactive migration via
    useUserDataSnapshot landed (direct hook call — useChatWidget
    isn't wrapped in useBetween, so the constraint doesn't apply).

- ARCHITECTURE.md → "useExternalSnapshot" subsection: replaced the
  2026-05-18 rollback note with the structural constraint + the
  2026-05-19 fix landing, including pointers to the regression test
  and the new CI gate (eslint.hooks.config.mjs + yarn lint:hooks).

No code change in this commit — yarn typecheck clean, yarn
lint:hooks clean.
2026-05-19 18:01:04 +02:00
simoleo89 a029ee63cb fix(catalog,ci): catch hook-order violations + add CI gate
Two follow-ups to the CatalogPurchaseWidgetView fix (6bf3366):

1. CatalogItemGridWidgetView had the same shape — four useCallback
   declarations (handleDragStart / handleDragOver / handleDrop /
   handleDragEnd) sat below an `if(!currentPage) return null` early
   return. When currentPage flipped from null to a real page the hook
   count jumped by 4 and React would have thrown "Rendered more hooks
   than during the previous render" the moment any consumer rendered
   the grid in admin mode. Moved the four useCallback declarations
   above the early-return; their bodies are safe pre-load (only
   currentPage?.offers is accessed inside handleDrop, optional-chained
   already).

2. CI gate — the existing GitHub Actions workflow runs `yarn
   typecheck` and `yarn test`, but NOT `yarn eslint`. That's why this
   pattern slipped through twice in a row: ESLint flags it locally
   but no PR check enforces it. Full `yarn eslint` emits ~900
   pre-existing baseline errors (brace-style, indentation,
   recommended TS rules — out of scope for this branch), so a blanket
   step would always fail. Instead added a focused
   `eslint.hooks.config.mjs` + `yarn lint:hooks` script that runs
   ESLint with ONLY `react-hooks/rules-of-hooks: error`. Wired into
   ci.yml between `typecheck` and `test`. The local repo now has
   zero violations of the rule.

3. useSessionSnapshots.test.tsx — added eslint-disable-next-line
   comments on the three lines that intentionally violate the rule
   (they're the assertions that the broken pattern crashes). Without
   the comments the new CI gate would fail on the regression-guard
   suite.

Verification: yarn lint:hooks green, yarn typecheck clean, yarn test
209/209.
2026-05-19 17:57:28 +02:00
simoleo89 6bf3366af7 fix(catalog): stabilise hook order in CatalogPurchaseWidgetView
React reported "Rendered more hooks than during the previous render"
when CatalogPurchaseWidgetView transitioned from currentOffer=null to
a real offer: hook count jumped from 22 to 23 because the
useMemo/useEffect block for the builders-club placement state sat
*below* the `if(!currentOffer) return null` early-return on line 140.
On the first render it never ran; on the next render (offer loaded)
it did, and React's hook-call tracker flagged the divergence and
unmounted the component via the error boundary.

Fix: move the three builders-club hooks (useMemo builderPlaceableStatus,
useMemo buildersClubPlaceOneButtonStyle, useEffect interval) above the
early return. They already short-circuit cleanly when
isBuildersClubPlaceable is false — added a defensive `!currentOffer`
guard on the first useMemo and an explicit `!!currentOffer` clause on
the derived isBuildersClubPlaceable so the .product access stays safe
when offer is null. Behavior unchanged for the loaded-offer path; the
early-render path now runs the hooks but their bodies no-op.

Verification: yarn typecheck clean, yarn test 209/209.
2026-05-19 17:43:20 +02:00
simoleo89 d28819db89 fix(snapshots): re-apply the 3 snapshot-consumer migrations with the use-between/useSyncExternalStore incompatibility resolved
Root cause of last session's "(intermediate value)() is undefined" at
ToolbarView.tsx:46:

  use-between 1.x ships its own React-dispatcher proxy (ownDispatcher
  in node_modules/use-between/release/index.esm.js:54-169) that
  re-implements only useState, useReducer, useEffect, useLayoutEffect,
  useCallback, useMemo, useRef and useImperativeHandle. It does NOT
  implement useSyncExternalStore. When the inner state function of
  useBetween(stateFn) calls useSyncExternalStore (directly or via
  useExternalSnapshot / useUserDataSnapshot), React resolves the
  dispatcher to use-between's proxy, finds .useSyncExternalStore
  missing, and calls undefined() — that's the exact production crash
  in Firefox. Chrome reports the same as
  "dispatcher.useSyncExternalStore is not a function".

Neither the vite alias (790ad2b) nor the defensive renderer-method
guards (c35a2d4) could fix it — both addressed downstream symptoms
(stale dist / missing manager methods) but the dispatcher is upstream
of both. That's why every retry kept reproducing the same error.

Fix is structural: snapshot hooks (useUserDataSnapshot,
useIsUserIgnored, etc.) MUST run outside any useBetween scope. Three
re-applied migrations:

- useSessionInfo: snapshot read moved into the outer wrapper. The
  inner useSessionInfoState (useBetween-shared) now contains only
  use-between-safe hooks: useState, useMessageEvent, plain actions.
  userFigure / userRespectRemaining / petRespectRemaining come from
  useUserDataSnapshot() OUTSIDE useBetween, so useSyncExternalStore
  installs against the real React dispatcher.

- useChatWidget.ownUserId: direct snapshot read. useChatWidget is
  exported as `useChatWidget = useChatWidgetState` (NOT wrapped in
  useBetween), so this hook never sat inside the broken scope — the
  precautionary rollback was unnecessary in retrospect. Gains
  session-change reactivity (e.g. reconnect under a different user id).

- AvatarInfoWidgetAvatarView Ignore/Unignore: useIsUserIgnored(name)
  read directly in the component body. Same reasoning as
  useChatWidget — never inside useBetween. The menu auto-flips
  Ignore <-> Unignore while the popup is open.

Added regression guard at src/hooks/session/useSessionSnapshots.test.tsx
with two cases: (1) useSyncExternalStore inside useBetween throws,
(2) useSyncExternalStore outside useBetween in the same render works.
Pins the constraint so future migrations cannot reintroduce the bad
shape silently.

Verification: yarn typecheck clean, yarn test 209/209 (207 baseline
+ 2 new regression cases), no consumer surface changes — every
destructured field (userFigure, userRespectRemaining, respectUser,
petRespectRemaining, respectPet, chatStyleId, updateChatStyleId) is
still returned with the same name and shape.
2026-05-19 17:30:03 +02:00
simoleo89 06f9b66073 merge: integrate duckietm/Dev (JSON mode selector, split-gamedata script, installer, IT→ENG)
# Conflicts:
#	vite.config.mjs
2026-05-19 17:04:58 +02:00
duckietm 53b208e7b0 🆙 IT ==> ENG and Remove the base path (this should a user do manual) 2026-05-19 10:28:09 +02:00
DuckieTM 76fb571efe Merge pull request #134 from medievalshell/Dev
feat: interactive JSON / JSON5 mode selector at build time
2026-05-19 09:55:46 +02:00
simoleo89 f4ada81321 docs(CLAUDE.md,ARCHITECTURE.md): record snapshot-consumer rollback (e142efd)
CLAUDE.md updates:
- Patterns to use: "useSessionSnapshots" section retitled "(OPT-IN)";
  the documented pilot adopters (useSessionInfo,
  AvatarInfoWidgetAvatarView) are removed. Adds explicit warning about
  the suspected useBetween + useSyncExternalStore + React Compiler
  interaction and the rollback in e142efd.
- Adopted table: snapshot-consumer row changed to "No in-tree
  consumers" with note about defensive fallbacks remaining.
- Not yet table: the useChatWidget reactive-ownUserId line corrected
  to reflect the rollback; the "migrate session-data mirrors" row
  marked BLOCKED with a retry hint (try a non-useBetween consumer
  first to isolate the cause).

ARCHITECTURE.md update:
- useExternalSnapshot bullet in the "Solution" section gains a note
  pointing at the 8 pre-built consumers in useSessionSnapshots.ts and
  the 2026-05-18 rollback caveat with the suspected interaction and
  retry guidance.

Pure documentation refresh; no code change. The useSessionSnapshots.ts
file and the vite alias remain in place — they're not what got rolled
back, only the consumer-side migrations were.
2026-05-18 22:27:09 +02:00
simoleo89 e142efd793 revert(hooks): roll back the three snapshot-consumer migrations to pre-71a0eee state
The migrations of useSessionInfo, useChatWidget.ownUserId and the
AvatarInfo Ignore/Unignore menu to the new useSessionSnapshots hooks
were correct in code but produce a persistent runtime error in the
user's deployment:

  TypeError: (intermediate value)() is undefined
      ToolbarView ToolbarView.tsx:46

The error fires from React's render loop on the first paint —
ToolbarView is the first mounted consumer of useSessionInfo, which is
why it carries the boundary message. Two attempted fixes did not
resolve it on the user's side:
- 790ad2b: vite alias forcing @nitrots/nitro-renderer to source index.ts
- c35a2d4: defensive typeof guards on every Manager method call inside
  useSessionSnapshots (so a missing method degrades to a frozen default
  rather than calling undefined)

Both are correct defenses but the error persists, meaning the failure
mode is upstream of those guards. Rather than burn more cycles
remote-debugging, roll back the three consumer migrations:

- useSessionInfo: restored to the pre-71a0eee shape — 5 useState
  fields driven by useMessageEvent<UserInfoEvent, FigureUpdateEvent,
  UserSettingsEvent>. The five consumers (ToolbarView, HcCenterView,
  ChatInputView, AvatarInfoPetTrainingPanelView, InfoStandWidgetPetView,
  AvatarInfo{Avatar,Pet,OwnPet}View) get the same destructured shape
  they had before this session.
- useChatWidget.ownUserId: restored to `GetSessionDataManager()?.userId`
  (synchronous, captured at mount). Loses the session-change reactivity
  but matches the previous, working behaviour.
- AvatarInfoWidgetAvatarView Ignore/Unignore: restored to
  `avatarInfo.isIgnored` (captured by AvatarInfoUtilities at click
  time, not reactive). Loses the live-toggle if the user is
  ignored/unignored while the popup is open — known small regression,
  worth it for stability.

Kept intact:
- The useSessionSnapshots.ts hook file itself, with defensive guards,
  so the API stays available for any future opt-in consumer.
- 790ad2b vite alias for the umbrella, still useful as defence in
  depth for future migrations.
- All the other non-snapshot modernizations from this session
  (usePetPackageWidget reducer, useWordQuizWidget bug fix,
  useChatCommandSelector Zustand store, useAvatarInfoWidget typed
  globalThis accessor).

Verification: yarn typecheck clean, yarn test 207/207, yarn build green.
The toolbar should boot without the error now — the call chain on the
first paint no longer touches the new useExternalSnapshot / snapshot
getter path.
2026-05-18 22:16:48 +02:00
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
medievalshell 8200132b1f feat(scripts): add split-gamedata.mjs CLI splitter
Companion tool for the split-aware gamedata loader added in
@nitrots/utils. Takes a legacy single-file gamedata JSON/JSON5 and
produces the directory layout the loader expects:

  <out>/
    manifest.json5          (root manifest, tier order)
    core/
      manifest.json5        (file list in load order)
      <part>.json5 ...

The tool auto-detects the gamedata type from top-level keys and applies
the right split strategy:

- EffectMap            -> one file per effect type (dance, fx, ...)
- FigureData           -> palettes + one file per setType
- FigureMap            -> chunked libraries (default 500/file)
- FurnitureData        -> floor/wall, chunks of furnitype (default 300)
- HabboAvatarActions   -> grouped by state
- ProductData          -> chunked products (default 500)
- ExternalTexts/UITexts-> grouped by key prefix

Only the core/ tier is generated; custom/ and seasonal/ are operator-
owned and the loader auto-discovers them when their manifest.json5
exists.

Flags: --input, --output, --type, --chunk-size, --json (legacy emit),
--force, --help.

README extended with a 'Splitting gamedata' section covering the layout,
the strategy table, CLI usage and the renderer-config migration step.
2026-05-18 21:20:13 +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
medievalshell 0028b03b6a feat: cross-platform installer with JSON-mode integration
Adds install.bat / install.sh / install.mjs at the project root for a
one-shot setup: prereqs check, renderer clone & link, dependency install,
config copy, JSON parser mode selection, URL prompt with validation, and
the production build.

- install.bat / install.sh: thin OS-specific wrappers around install.mjs
- install.mjs: 9-step installer with --help, --non-interactive,
  --skip-build/clone/link and per-URL override flags
- new step 6 'Choose JSON parsing mode': prompts the operator (json5
  recommended) or accepts --json-mode=json5|legacy|auto in CI; writes
  .nitro-build.json so the final 'yarn build' picks it up directly
- summary now reports the selected JSON mode and its source
- .gitattributes: force LF on install.sh / install.mjs so the shebang
  stays valid on Linux/macOS after a Windows checkout
- install.sh marked executable in the index (100755)
- README: new 'Quick install' section with interactive and CI usage,
  plus a complete --non-interactive example
2026-05-18 20:54:29 +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
medievalshell 2fded7bc79 feat: interactive JSON / JSON5 mode selector at build time
Lets the operator pick between strict JSON (legacy) and JSON5 for every
configuration file consumed by Nitro and the renderer.

- scripts/configure-json.mjs: interactive prompt (JSON5 recommended),
  with --if-missing and --non-interactive flags for CI use
- package.json: yarn configure / prestart / prebuild hooks
- vite.config.mjs: reads .nitro-build.json (or NITRO_JSON_MODE env) and
  injects the compile-time constant __NITRO_JSON_MODE__ via define
- src/bootstrap.ts: routes client-mode.json parsing through the
  selected mode
- .gitignore: ignore the per-deployment .nitro-build.json
- README: full usage and override section
- public/configuration assets regenerated by the updated prebuild flow

The renderer side (@nitrots/utils JsonParser) is updated in the
companion Nitro_Render_V3 commit on the dev branch.
2026-05-18 20:38:26 +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