The Button base class string declared `inline-block`, even though the
component renders through <Flex center> which passes display="flex".
Both `inline-block` (from the Button base class) and `flex` (from
Flex) ended up as classes on the same element. Tailwind v4's emitted
stylesheet orders display utilities in source order — the
unfortunate result on this build was that the icon kept rendering at
the baseline (top-left of the line box) while the text settled
centered via text-center, i.e. inline-block layout was winning.
Resolve the ambiguity by passing display="inline-flex" to Flex
explicitly. Now there's only ONE display utility on the element
(inline-flex), and Flex's center=true still adds items-center +
justify-center. Strip the now-conflicting `inline-block` /
`align-middle` from the Button base class string — flex's
items-center already handles vertical alignment.
text-center is kept so multi-line label buttons that relied on it
still render centered (it's a no-op for flex-row layout otherwise).
No call-site changes needed — pure CSS-equivalence fix on a single
common component.
Upstream V3.5.0 introduced a useRef<HTMLDivElement>() call with no
initial value. TS6 (and the tsgo preview compiler) now require an
explicit initial value for useRef typed against a DOM element. Pass
null to match the React 19 RefObject<HTMLDivElement | null> shape.
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.
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.
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.
IGetImageListener.imageReady(result: IImageResult) takes a single
IImageResult object (with .id, .data, .image), but three call sites in
the client still used the old 3-arg destructure '(id, texture, image)
=> ...'. The renderer's RoomEngine.ts already passes
'new ImageResult(...)' to the listener, so the runtime payload matches
the new contract; the old call-site shape just type-errored.
Migrated:
- LayoutPetImageView (pet thumbnail loader)
- LayoutRoomObjectImageView (furniture thumbnail loader)
- useFurniturePresentWidget (gift box image generator)
Also tightened imageFailed handlers from 'imageFailed: null' to a
proper no-op arrow — the interface requires a callback.
React 19 dropped the no-arg useRef overload — the type-only useRef<T>()
form (no initial value) is gone, every call must pass an initial value.
The codebase had 15 occurrences of useRef<HTMLDivElement>() (DOM ref
pattern) all flagged by tsgo as 'Expected 1 arguments, but got 0'.
Mechanical sweep to useRef<HTMLDivElement>(null) — no behavior change,
React still hands out a ref object with .current set to null at mount.
Net tsgo error count: 57 -> 42.
- ColorVariantType missed the 5 outline-* bootstrap variants
GroupForumThreadView and GroupForumThreadListView already use; adding
them clears 4 errors.
- React 19 moved the JSX namespace out of the global scope into the
react module; WiredNeighborhoodSelectorView referenced JSX.Element
without importing it.
- showConfirm() takes 7 args; the chat link confirm in useOnClickChat
passed an 8th 'link' icon arg left over from an older signature.
- LocalizeText placeholder array is string[]; UserContainerView passed
userProfile.friendsCount (number) — call .toString().
Net tsgo error count: 97 -> 90.
This is the structural plan promised in the previous session, with concrete
pilots for all five proposals + the bonus error-boundary work.
== docs/ARCHITECTURE.md (new, ~370 lines)
Living document describing:
- where the project stands today (event-bus pattern friction with React 19,
god-hooks, oversized files);
- the five proposed structural improvements with the why/how/status of each;
- what's already in place across this branch;
- recommended order for the next refactor PRs.
This is the deliverable the rest of this commit references.
== Proposal #3 + #4 pilots: src/features/doorbell/ (new)
Concrete feature-folder migration on the doorbell widget (chosen because
it's small enough to migrate end-to-end in one commit).
src/features/doorbell/
index.ts public API
views/DoorbellWidgetView.tsx
hooks/useDoorbellState.ts reduces 3 events into a users array (data only)
hooks/useDoorbellActions.ts answer(name, flag) (imperative actions only)
The split (data vs actions) is the pattern proposal #4 wants applied to
useCatalog/useChat/useWiredTools later. The original useDoorbellWidget had
both concerns + a buggy `useEffect(() => setIsVisible(!!users.length), [users])`
derive-state-in-effect. The new view computes visibility in render.
Compat shims kept so existing imports keep working:
- src/components/room/widgets/doorbell/DoorbellWidgetView.tsx -> 1-line re-export
- src/hooks/rooms/widgets/useDoorbellWidget.ts -> deprecated wrapper around
the two new hooks, returning the same { users, answer } shape.
== Proposal #2 prototype: src/api/nitro-query/ (new)
Adapter outline for wrapping composer/parser request-response pairs in
TanStack Query. Not yet enabled because @tanstack/react-query is not in
package.json. The file documents the activation steps:
yarn add @tanstack/react-query @tanstack/react-query-devtools
+ mount QueryClientProvider in src/index.tsx
awaitNitroResponse() throws with a helpful pointer to the doc section if
called before activation, so accidental adoption fails loudly.
== Proposal #5 skeleton: src/state/createNitroStore.ts (new)
Same pattern: skeleton + activation instructions. Not yet enabled because
zustand is not in package.json.
yarn add zustand
+ replace the throw with `import { create } from 'zustand'; export const createNitroStore = create;`
The doc inside the file shows the recommended slice shape and points to
the suggested first migration target (the let isCreatingRoom singleton in
NavigatorRoomCreatorView).
== Bonus: WidgetErrorBoundary
src/common/error-boundary/WidgetErrorBoundary.tsx wraps react-error-boundary
with a sensible default (silent fallback, NitroLogger.error). Re-exported
from src/common/index.ts.
Applied as the umbrella around RoomWidgetsView's children — a widget
crash in a room (e.g. malformed pet data) now degrades gracefully
instead of unmounting the whole UI.
== Verification
- yarn eslint on all new + modified files: 0 errors / 0 warnings introduced.
RoomWidgetsView still has its 1 pre-existing FC<{}> error (1 before, 1 after).
- yarn tsc on all new files: clean (only project-wide pre-existing
TS2307 about @nitrots/nitro-renderer not installed locally remains).
- No regressions: existing imports of DoorbellWidgetView and
useDoorbellWidget keep resolving via the compat shims.
== What's NOT in this commit (intentionally)
- Mass adoption of the new patterns elsewhere — left as follow-up PRs in
the order documented in ARCHITECTURE.md "How to pick the next refactor PR".
- Installation of @tanstack/react-query / zustand — explicit team decision,
not the LLM's to make.
- Test infrastructure (Vitest setup) — listed as the #1 missing piece in
the doc, but a separate PR.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
TypeScript 7 is the Go-native rewrite of tsc, ~10x faster but only
distributed as @typescript/native-preview daily builds at the time
of writing (npm typescript@latest is still 6.0.3). Add it as a
non-disruptive type-check tool: yarn typecheck → tsgo --noEmit.
Vite still uses esbuild for transpilation, ESLint still uses TS 6
through @typescript-eslint v8, IDEs continue using their bundled TS.
This commit only adds a type-check tool — nothing replaces.
Required tsconfig.json adjustments for TS 7 compatibility (still
valid for TS 6):
- Drop baseUrl: "./src" (removed in TS 7). The codebase has no
bare/non-relative imports that depended on it; all imports are
relative or aliased.
- Drop downlevelIteration: true (removed in TS 7; target es2022
doesn't need it).
- moduleResolution: "node" → "bundler" (TS 7 dropped node10; bundler
is the right mode for Vite anyway).
- paths "@layout/*" entries now use leading "./" (TS 7 disallows
non-relative path mappings). Add "@/*" → "./src/*" to match the
Vite alias used in some components.
Other TS 7 adjustments:
- src/react-app-env.d.ts: add module declarations for *.css/.scss/.sass
side-effect imports (TS 7 with bundler resolution requires them) +
Window.NitroConfig / Window.NitroSecureApiUrl globals which were
used in App.tsx without a declaration.
- src/common/Popover.tsx: explicit `import { JSX } from 'react'`
because TS 7 dropped the implicit global JSX namespace.
Verification:
- yarn eslint still passes (TS 6 / @typescript-eslint v8 happy with
the migrated config).
- yarn typecheck (tsgo) runs and reports only cascading errors
rooted in the missing @nitrots/nitro-renderer sibling repo
(environmental, not introduced here).
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
Run eslint --fix across src/ to clear ~1900 mechanical lint errors
surfaced by the @typescript-eslint v8 + react-hooks v7 + react-compiler
upgrade in the React 19 modernization PR.
Issues fixed automatically:
- brace-style (Allman): try/catch one-liners reformatted to multi-line
- indent: tab-vs-space and depth corrections
- semi: missing trailing semicolons
- no-trailing-spaces
No semantic changes. Remaining 701 errors are real-code issues
(set-state-in-effect, rules-of-hooks, no-unsafe-* type checks) that
need manual per-file review.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
Adopt React 19 idioms across the codebase. The runtime was already on
react@19.2.5 but no React 19 APIs were in use.
- forwardRef -> ref-as-prop in 7 layout/component files
(NitroInput/Button/ItemCountBadge/Card×5/InfiniteGridItem,
ToolbarItemView, AvatarEditorIcon)
- <Ctx.Provider> -> <Ctx> in 6 contexts (CatalogAdmin, FloorplanEditor,
UiSettings, GridContext, NitroCardContext, NitroCardAccordionContext)
- Native <script> hoisting for Turnstile, ExternalPluginLoader, GoogleAdsView
(React 19 dedupes by src; removes manual document.head.appendChild +
module-level promise caches)
- React Compiler enabled at build time via babel-plugin-react-compiler
in vite.config.mjs (target: '19'), plus eslint-plugin-react-compiler
in lint mode
- Global <ErrorBoundary> + <Suspense> in src/index.tsx using
react-error-boundary, with LoadingView as fallback
- BackgroundsView migrated to use(promise) as a demonstrator pattern
for Suspense-driven config loading
- ESLint react setting bumped 18.3.1 -> 19.2; legacy
@typescript-eslint/ban-types replaced with no-restricted-types
(the old rule was removed in @typescript-eslint v8)
- Refresh public/configuration/{asset-loader,bootstrap}.js to match
current write-asset-loader.mjs output
Phase 3 (login forms -> useActionState/useFormStatus) deferred:
LoginView is 1623 lines with lockout + Turnstile + heartbeat
interleaving; safer as its own PR.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q