diff --git a/.gitignore b/.gitignore index e7bee96..25c5f52 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,4 @@ Thumbs.db # the dev server takes minutes to start with 100k+ files under public/. /public/nitro-assets /public/swf +.superpowers/ diff --git a/CLAUDE.md b/CLAUDE.md index e9c7383..cd950d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,19 +6,27 @@ the ground running. ## TL;DR -This branch — **`feat/react19-modernization`** — is a long-running modernization -of the Nitro V3 client: bump to React 19.2 idioms, add the supporting -infrastructure (TanStack Query, Zustand, Vitest, React Compiler, error -boundaries), split a few god-hooks, and audit logic bugs along the way. -PR is **#2** on `simoleo89/Nitro-V3`. +This client carries a long-running React 19.2 modernization: React 19 +idioms + supporting infrastructure (TanStack Query, Zustand, Vitest, +React Compiler, error boundaries), god-hook splits, and logic-bug audits. -Upstream `duckietm/Nitro-V3` (`origin/Dev`) is merged in through -`b2318b9` as of 2026-05-18 (merge commit `779a98c`). That brings in -JSON5 config support, user-settings (reset password / email / change -username), wear-badge popup fix, login screen fix, About update, and -the offer-selection refactor. When syncing the next batch of upstream -commits, expect conflicts in `App.tsx` / `bootstrap.ts` / `LoginView.tsx` -on React 19 imports — always keep the modernized local version. +**Working base is now `main`** (tracking `duckietm/Nitro-V3`). The earlier +`feat/react19-modernization` long-running branch was superseded — feature +work now ships as small focused PRs against `duckietm:Dev`, staged through +Dev then merged to main. (`feat/react19-modernization` still exists on the +fork as backup; do not force-push it.) + +**Navigator modernization landed** (merged to main 2026-05-28, PRs +#168/#169/#170): the 492-line `useNavigator` god-hook was split into +`useNavigatorStore` + `useNavigatorData`/`useNavigatorUiState`/ +`useNavigatorSearch` filters (wired-tools layout), door lifecycle extracted +to `src/hooks/rooms/widgets/useDoorState.ts`, 9 UI flags moved to a Zustand +`navigatorUiStore`, search migrated to a query hook, and 5 sub-views wrapped +in `WidgetErrorBoundary`. **Caveat**: duckietm patched `useNavigatorSearch` +post-merge (`05d71dd1`) — see the `useNitroQuery` fragility note below. + +When syncing upstream, expect conflicts in `App.tsx` / `bootstrap.ts` / +`LoginView.tsx` on React 19 imports — always keep the modernized version. Local-dev game assets are served by a small Vite plugin (`sirv` middleware mounted on `/nitro-assets` and `/swf`, reading from @@ -236,6 +244,20 @@ and invalidates the query slot on every push, so server-driven refresh paths work the same as the initial request/response (e.g. ClubGiftInfoEvent firing again after the user claims a gift). +**⚠️ Fragility — do NOT use `useNitroQuery` for primary visible data.** +The one-shot listener inside `awaitNitroResponse` (register listener → +await one matching response → remove itself) is fragile against +renderer-bundle quirks: for some parsers the event fires but the listener +never matches, so the promise never resolves and `query.data` stays +`undefined` forever — the UI shows the server's response arriving in logs +but renders blank. This bit **ModTools Room/CFH chatlog** (reverted to +`useMessageEvent + useEffect`) and then **Navigator search** (P2 shipped +with `useNitroQuery`, duckietm reverted it in `05d71dd1` to the god-hook +pattern). **Rule: reserve `useNitroQuery` for config / secondary fetches +where a brief blank is tolerable. For anything that is the primary visible +content of a panel, use `useMessageEvent + useState/useEffect`** — that's +what the rest of the codebase does and it's robust. + ### Singleton-filter split for `useBetween`-based hooks When a hook backs many consumers but most only need either state OR @@ -339,6 +361,7 @@ into `configurePreviewServer` so `yarn preview` keeps working. | Zustand | `NavigatorRoomCreatorView` (`useRoomCreatorStore`), `WiredCreatorToolsView` (`useWiredCreatorToolsUiStore` — every panel-lifecycle-relevant flag, snapshot, selection, highlight, inline editor, picker chain hoisted; what's left in the component as `useState` is genuinely transient: keepSelected, globalClock, roomEnteredAt, selectedMonitorErrorType, selectedMonitorLogDetails) | | God-hook split (state + actions + shim) | `doorbell`, `poll`, `furni-chooser`, `user-chooser`, `friend-request`, `chat-input` | | God-hook split (`useBetween` singleton + state filter + actions filter + shim) | `wired-tools`, `translation`, `notification`, `friends`, `catalog` (three-way: `useCatalogData` / `useCatalogUiState` / `useCatalogActions` — all 48 consumers migrated, deprecated `useCatalog` shim removed) | +| Navigator modernization (merged to main 2026-05-28, PRs #168/#169/#170) | 492-line `useNavigator` god-hook split into `useNavigatorStore` (internal `useBetween` closure) + flat filters `useNavigatorData` / `useNavigatorUiState` / `useNavigatorSearch`; door bell/password lifecycle extracted to `src/hooks/rooms/widgets/useDoorState.ts` (dual-subscribes `GetGuestRoomResultEvent` + `GenericErrorEvent` alongside the nav store, each filtering by branch/errorCode); 9 UI flags + `currentTabCode`/`currentFilter` in Zustand `navigatorUiStore` (`src/hooks/navigator/navigatorUiStore.ts`); all 5 Navigator sub-views wrapped in `WidgetErrorBoundary`; old shim deleted. **`useNavigatorSearch` was reverted by duckietm (`05d71dd1`) from `useNitroQuery` to `useMessageEvent + useEffect`** — see the useNitroQuery fragility note. Specs/plans under `docs/superpowers/`. | | `WidgetErrorBoundary` | `RoomWidgetsView` umbrella + per-widget wrap on all 13 room widgets and all 20 furniture widgets (so a crash in one widget no longer takes down its siblings) | | Vitest | 207/207 cases — pure helpers (incl. 4 new on `getPetPackageNameError`) + 2 Zustand store suites (`navigatorRoomCreatorStore`, `wiredCreatorToolsUiStore` with 45 cases including the picker-chain hoists) + 2 component-/hook-level pilots (WidgetErrorBoundary, useDoorbellState) on top of the renderer-SDK mock at `src/nitro-renderer.mock.ts`, 34 cases on the catalog pure helpers, 4 contract cases on the catalog filters. **Tests are co-located** under `src/`, alongside their subject. | | Form Actions | Login / Register / Forgot (LoginView.tsx) | @@ -412,6 +435,11 @@ See `docs/ARCHITECTURE.md` "Recently fixed" for fix shapes. `useCatalogUiState` / `useCatalogActions` in `src/hooks/catalog/useCatalog.ts` (all 48 consumers migrated; deprecated `useCatalog` shim removed) +- Navigator hooks: `src/hooks/navigator/` — `useNavigatorStore.ts` + (internal closure), `useNavigatorData.ts` / `useNavigatorUiState.ts` / + `useNavigatorSearch.ts` (filters), `navigatorUiStore.ts` (Zustand UI + flags + `setTab`/`setFilter`). Door lifecycle: `src/hooks/rooms/widgets/useDoorState.ts`. + Specs/plans: `docs/superpowers/specs/2026-05-2*-navigator-*.md` - Renderer-SDK mock for Vitest: `src/nitro-renderer.mock.ts` (aliased over `@nitrots/nitro-renderer` via `vitest.config.mts`). Hosts the explicit `NitroLogger` mock, the `mockEventDispatcher` / diff --git a/custom-themes/README.md b/custom-themes/README.md new file mode 100644 index 0000000..e9f7436 --- /dev/null +++ b/custom-themes/README.md @@ -0,0 +1,40 @@ +# Custom themes (graphics-only) + +Ecosistema temi caricati a **runtime** (niente rebuild del client). Un tema = +una cartella con un manifest + "pezzi" CSS. Ogni pezzo è attivabile/disattivabile +dall'utente da **Impostazioni → Temi** (checkbox). Se un pezzo è rotto/404 → +fallback automatico al default (solo quel pezzo). + +## Dove vivono +- **Questa cartella (`custom-themes/`) è solo il TEMPLATE di riferimento**, versionata su git. +- I temi **veri** stanno sul server in `public/nitro/custom-themes/` (serviti via + l'url configurato in ui-config `theme.base.url`, es. `/client/nitro/custom-themes`). + NON vanno su git → vedi `.gitignore` (`public/custom-themes/`). + +## Struttura +``` +custom-themes/ + index.json # { "themes": [ { "id", "name", "author?" } ] } + / + theme.json # { "name", "pieces": [ { "id", "name", "file" } ] } + cards.css chat.css ... # un file per "pezzo" + assets/... # immagini referenziate dai CSS (url assoluti) +``` + +## Creare un tema +1. Copia `neon-viola/` in una nuova cartella `/`. +2. Modifica `theme.json` (nome + elenco pezzi). +3. Scrivi i CSS dei pezzi (override con `!important`, caricati dopo il base). +4. Aggiungi `{ "id": "", "name": "..." }` a `index.json`. +5. Carica la cartella in `public/nitro/custom-themes/` sul server. **Nessun rebuild.** + +## Default hotel-wide (admin) +In `ui-config.json`: +- `theme.base.url` → dove sono serviti i temi +- `theme.default` → id del tema attivo di default (vuoto = nessuno) +- `theme.default.pieces` → array di id pezzi attivi di default + +Ogni utente può comunque sovrascrivere da Impostazioni → Temi (salvato in localStorage). + +> Nota: i temi ri-skinnano solo la **grafica** (CSS). Non cambiano la struttura +> dei componenti né il comportamento. diff --git a/custom-themes/index.example.json b/custom-themes/index.example.json new file mode 100644 index 0000000..46fd0ea --- /dev/null +++ b/custom-themes/index.example.json @@ -0,0 +1,5 @@ +{ + "themes": [ + { "id": "neon-viola", "name": "Neon Viola", "author": "infinityhotel" } + ] +} diff --git a/custom-themes/neon-viola/cards.css b/custom-themes/neon-viola/cards.css new file mode 100644 index 0000000..5608fb7 --- /dev/null +++ b/custom-themes/neon-viola/cards.css @@ -0,0 +1,24 @@ +/* Tema Neon Viola — pezzo "cards" (finestre / NitroCard). + Ricolora header + cornice delle finestre. Caricato DOPO il CSS base, quindi + usa !important per vincere. Tocca solo la cornice/header (non lo sfondo del + contenuto) per non rovinare la leggibilita' del testo. */ + +.nitro-card-shell:not(.nitro-wired) { + border-color: #7c3aed !important; + box-shadow: 0 0 14px rgba(124, 58, 237, .55), 0 8px 22px rgba(0, 0, 0, .4) !important; +} + +.nitro-card-shell:not(.nitro-wired) .nitro-card-header-shell { + background: linear-gradient(180deg, #9333ea 0%, #6d28d9 100%) !important; + border-color: #a855f7 !important; + border-bottom-color: #2a0a4a !important; +} + +.nitro-card-shell:not(.nitro-wired) .nitro-card-title { + color: #fff !important; + text-shadow: 0 0 6px #c084fc, 0 1px 0 #3b0764 !important; +} + +.nitro-card-shell:not(.nitro-wired) .nitro-card-tabs-shell .nitro-card-tab-item-active { + box-shadow: inset 0 -2px 0 #a855f7 !important; +} diff --git a/custom-themes/neon-viola/catalog.css b/custom-themes/neon-viola/catalog.css new file mode 100644 index 0000000..1f8973d --- /dev/null +++ b/custom-themes/neon-viola/catalog.css @@ -0,0 +1,10 @@ +/* Tema Neon Viola — pezzo "catalog" (catalogo Hippiehotel, .nitro-catalog). */ + +.nitro-catalog .nitro-card-header-shell { + background: linear-gradient(180deg, #9333ea 0%, #6d28d9 100%) !important; +} + +.nitro-catalog .group\/rail { + background: #1a1030 !important; + border-right-color: #7c3aed !important; +} diff --git a/custom-themes/neon-viola/chat.css b/custom-themes/neon-viola/chat.css new file mode 100644 index 0000000..c0f767e --- /dev/null +++ b/custom-themes/neon-viola/chat.css @@ -0,0 +1,12 @@ +/* Tema Neon Viola — pezzo "chat". + Accento viola sulla bubble di default (bubble-0) e sull'input chat. + (Le bubble custom hanno la loro grafica; qui tocchiamo solo l'accento base.) */ + +.chat-bubble.bubble-0 { + filter: drop-shadow(0 0 5px rgba(168, 85, 247, .8)); +} + +.nitro-chat-input-container, +.chat-input-container { + box-shadow: inset 0 0 0 1px #7c3aed !important; +} diff --git a/custom-themes/neon-viola/theme.json b/custom-themes/neon-viola/theme.json new file mode 100644 index 0000000..199fd09 --- /dev/null +++ b/custom-themes/neon-viola/theme.json @@ -0,0 +1,10 @@ +{ + "name": "Neon Viola", + "author": "infinityhotel", + "pieces": [ + { "id": "cards", "name": "Finestre / Card", "file": "cards.css" }, + { "id": "chat", "name": "Chat", "file": "chat.css" }, + { "id": "toolbar", "name": "Toolbar", "file": "toolbar.css" }, + { "id": "catalog", "name": "Catalogo", "file": "catalog.css" } + ] +} diff --git a/custom-themes/neon-viola/toolbar.css b/custom-themes/neon-viola/toolbar.css new file mode 100644 index 0000000..ed74f7f --- /dev/null +++ b/custom-themes/neon-viola/toolbar.css @@ -0,0 +1,9 @@ +/* Tema Neon Viola — pezzo "toolbar". + Best-effort: ricolora la barra strumenti in basso. Se i selettori non + matchano nella tua build, il pezzo non ha effetto (fallback sicuro). */ + +.nitro-toolbar, +[class*="toolbar-container"] { + background: linear-gradient(180deg, #2a0a4a 0%, #1a0730 100%) !important; + box-shadow: 0 -2px 10px rgba(124, 58, 237, .4) !important; +} diff --git a/docs/superpowers/specs/2026-05-31-navigator-base-tab-stacked-labels-design.md b/docs/superpowers/specs/2026-05-31-navigator-base-tab-stacked-labels-design.md new file mode 100644 index 0000000..fc858bd --- /dev/null +++ b/docs/superpowers/specs/2026-05-31-navigator-base-tab-stacked-labels-design.md @@ -0,0 +1,71 @@ +# Navigator — Room Settings "Base" tab: stacked-label layout + +**Date:** 2026-05-31 +**Component:** Nitro-V3 client +**File:** `src/components/navigator/views/room-settings/NavigatorRoomSettingsBasicTabView.tsx` +**Type:** Layout-only refactor (no logic / data-flow change) + +## Problem + +The Base tab uses a horizontal two-column row layout: a fixed-width label on the +left, the control on the right. In the narrow room-settings panel the label column +is too tight, so multi-word Italian labels ("Visitatori massimi", "Impostazioni +scambio") wrap onto two lines and look broken. An earlier fix replaced dead +Bootstrap `col-3` classes with `w-1/4 shrink-0`, which stopped the crushing but +still leaves the labels cramped and occasionally wrapping. + +The other five room-settings tabs (Access, Rights, VIP/Chat, Mod, Misc) already use +idiomatic vertical/grouped layouts. Base is the outlier. + +## Decision + +Adopt the **stacked-label** pattern (chosen from three mockup options — A stacked, +B sectioned cards, C wider label column). Each field becomes a vertical block: bold +label on top, full-width control below, validation message underneath. This mirrors +the sibling **Access** tab's existing `` + `` shape, so +the two tabs become visually consistent and labels can never wrap. + +## Layout + +Every field → its own `` block: + +```tsx + + { LocalizeText('navigator.roomname') } + + { (roomName.length < ROOM_NAME_MIN_LENGTH) && + { LocalizeText('navigator.roomsettings.roomnameismandatory') } } + +``` + +Field-by-field: + +- **Nome stanza** — stacked block, mandatory-name validation preserved. +- **Descrizione** — stacked block, `