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
The Turnstile render effect had a stale-closure hazard: it captured
onToken/onExpire/onError props but didn't list them in the dependency
array (deps: scriptReady, siteKey, theme, size). On parent re-renders
the captured callbacks could go stale.
Wrap the three callback props with useEffectEvent so they always read
the latest props without invalidating the render effect. The render
effect still only re-runs when the script readiness or widget config
truly change.
useEffectEvent shipped in React 19.2 (already on the project) and
@types/react 19.2.x exports it.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
InfiniteGridRoot called useVirtualizer + 2 useEffect after an early
return for the squareItems branch, which violates the rules of hooks
(react-hooks v7 now flags this as an error and react-compiler skips
the component entirely).
Split the component into three:
- useColumnMeasure: shared custom hook that owns parentRef +
ResizeObserver-based column measurement (used by both branches).
- InfiniteGridSquare: the non-virtualized grid for squareItems mode.
Doesn't call useVirtualizer.
- InfiniteGridVirtualized: the virtualized branch with TanStack
Virtual + scroll/padding effects.
InfiniteGridRoot becomes a thin selector that routes by props.squareItems.
All hooks in each sub-component are now unconditional.
The remaining lint findings on this file (set-state-in-effect inside
InfiniteGridItem, react-hooks/incompatible-library on useVirtualizer)
are pre-existing/informational and out of scope.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
- @radix-ui/react-popover: ^1.1.6 → ^1.1.15
- @radix-ui/react-slider: ^1.2.4 → ^1.3.6
- react-icons: ^5.5.0 → ^5.6.0
- dompurify: ^3.4.1 → ^3.4.2
- @tanstack/react-virtual: pin → ^3.13.24 (loosen to caret; no version change)
framer-motion (12.38.0), @tanstack/react-virtual (3.13.24), emoji-mart
(5.6.0), and @emoji-mart/* are already at latest. react-player was
intentionally not bumped (3.x is a major release).
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
App.tsx's prepare() useEffect ran four .init() calls
(SessionDataManager, RoomSessionManager, RoomEngine, Communication)
without any guard, plus an immediate heartbeat ping and a legacy
authentication track. Under StrictMode dev double-invoke, those
fire twice — risking duplicate session/communication state.
- Gate the four .init() chain behind gameInitPromiseRef: both the
first and the simulated second invocation await the same promise.
- Gate the legacy track + immediate heartbeat behind bootstrapDoneRef.
- Heartbeat and remember-rotate intervals were already idempotent
(clearInterval before setInterval); ticker registration was already
guarded by tickersStartedRef; renderer/warmup were already gated by
rendererPromiseRef/warmupPromiseRef. No change needed there.
Wrap <App /> in <StrictMode> in src/index.tsx now that the renderer
init path is double-invoke safe.
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
Migrate all three inline forms in LoginView.tsx to React 19 Actions:
- Login form: handleLoginSubmit → loginAction(prevState, FormData) wrapped in
useActionState. Submit button extracted as <LoginSubmitButton/> reading
pending via useFormStatus, dropping the local `submitting` flag for the
login flow. Reads username/password/remember from FormData; rememberMe
checkbox now carries name="remember".
- Forgot form (inline): forgotAction wrapped in useActionState; awaits
parent's onSubmit so pending stays true through the parent fetch.
ForgotSubmitButton uses useFormStatus.
- Register credentials step: credentialsAction with useActionState; the
step transition (setStep('avatar')) happens inside the action after
pingServer + onCheckEmail.
- Register avatar step: avatarAction validates username, pings server,
checks availability, then awaits onSubmit. The button label uses
isAvatarPending to show "Creating…" without prop drilling submitting.
- DialogSharedProps onSubmit signatures updated to return Promise<void>
so dialog actions can await the parent's fetch.
- lockState memo replaced with a direct readLock() call in render: the
previous useMemo depended on `submitting` to refresh after a failed
attempt; now any re-render (triggered by the action's pending toggle)
recomputes it.
- Remove unused FormEvent import; remove unused checking state in
RegisterDialog (replaced by isCredentialsPending / isAvatarPending).
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
The example data has been provided in /Content-Gamedata so you could place it in /gamadata or anything you like.
Do not forget the render-config.json to update :
"login.health.method": "GET",
"login.news.url": "${asset.url}/news/news.json",
Adds a "Cards" tab to the Profile Background picker (BackgroundsView)
that selects a pattern applied to the entire user info card and the
extended profile container, in addition to the existing avatar-pad
background/stand/overlay layers.
- AvatarInfoUser/Utilities: propagate cardBackgroundId from RoomUserData.
- InfoStandWidgetUserView: stateful cardBackgroundId, applied as
.profile-card-background.card-background-{id} on the outer Column
with bg-color suppressed when active.
- UserContainerView: same class on the wrapper of the extended profile.
- BackgroundsView: 4th tab "cards" backed by cards.data config
(falls back to backgrounds.data); sends 4-id message via the
extended sendBackgroundMessage signature.
- ui-config.example: cards.data dataset (15 entries).
- BackgroundsView.css: 188 .card-background-{N} rules cloned from
background-{N} (repeat-tiled) plus 15 CSS-pattern overrides for the
provisional dataset (gradients, stripes, dots, grid, checker).
The face avatar (headOnly LayoutAvatarImageView) sits in a 63px-tall
box (44px on mobile) while sibling toolbar icons are smaller, so its
head sprite rendered visually higher than the other icons. Bumped
marginTop from 2px → 12px (desktop) and 4px → 9px (mobile) so the
head sits on the same horizontal axis as the rest of the toolbar.
Removed `absolute bottom-[60px] left-[33px]` from the inner Flex of
ToolbarMeView. The outer wrapper in ToolbarView already anchors the
popup above the face button (bottom-[calc(100%+8px)] left-1/2 -translate-x-1/2),
so the inner pixel-perfect override was detaching it and making it float
mid-screen.