Merge pull request #5 from simoleo89/feat/command-autocomplete-refactor

Feat/command autocomplete refactor
This commit is contained in:
Life
2026-06-02 20:55:40 +02:00
committed by GitHub
160 changed files with 3024 additions and 795 deletions
+1
View File
@@ -44,3 +44,4 @@ Thumbs.db
# the dev server takes minutes to start with 100k+ files under public/.
/public/nitro-assets
/public/swf
.superpowers/
+40 -12
View File
@@ -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` /
+40
View File
@@ -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?" } ] }
<id>/
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 `<id>/`.
2. Modifica `theme.json` (nome + elenco pezzi).
3. Scrivi i CSS dei pezzi (override con `!important`, caricati dopo il base).
4. Aggiungi `{ "id": "<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.
+5
View File
@@ -0,0 +1,5 @@
{
"themes": [
{ "id": "neon-viola", "name": "Neon Viola", "author": "infinityhotel" }
]
}
+24
View File
@@ -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;
}
+10
View File
@@ -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;
}
+12
View File
@@ -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;
}
+10
View File
@@ -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" }
]
}
+9
View File
@@ -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;
}
@@ -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 `<Column gap={1}>` + `<Text bold>` shape, so
the two tabs become visually consistent and labels can never wrap.
## Layout
Every field → its own `<Column gap={1}>` block:
```tsx
<Column gap={ 1 }>
<Text bold>{ LocalizeText('navigator.roomname') }</Text>
<input className="form-control form-control-sm" value={ roomName } onBlur={ saveRoomName } />
{ (roomName.length < ROOM_NAME_MIN_LENGTH) &&
<Text bold small variant="danger">{ LocalizeText('navigator.roomsettings.roomnameismandatory') }</Text> }
</Column>
```
Field-by-field:
- **Nome stanza** — stacked block, mandatory-name validation preserved.
- **Descrizione** — stacked block, `<textarea>` full width.
- **Categoria** — stacked block, `<select>` from `categories`.
- **Visitatori massimi** — stacked block, `<select>` from `GetMaxVisitorsList`.
- **Impostazioni scambio** — stacked block, 3-option `<select>`.
- **Tag** — one "Tag" label, then the two tag inputs side-by-side in a
`<Flex gap={1}>`, each `fullWidth`, each keeping its own length/type validation.
- **allow_walkthrough / allow_underpass** — remain inline `checkbox + label` rows;
remove the empty `<Base className="w-1/4 shrink-0" />` spacers that only existed
to align with the old label column.
- **Delete link** — unchanged at the bottom.
## Explicit non-goals
- No change to `handleChange` field names or values.
- No change to validation thresholds (`ROOM_NAME_MIN_LENGTH=3`,
`ROOM_NAME_MAX_LENGTH=60`, `DESC_MAX_LENGTH=255`, `TAGS_MAX_LENGTH=15`).
- No change to save-on-blur handlers (`saveRoomName`, `saveRoomDescription`,
`saveTags`), the `RoomSettingsSaveErrorEvent` subscription, or `deleteRoom`.
- No change to field order or any localization key.
- No change to the other five tabs.
- The `w-1/4 shrink-0` utility classes added in the prior fix are removed (labels
are full-width now).
## Risk
Single-file, JSX-only diff. No test covers this view, so no test impact. Manual
check: open Room Settings → Base, confirm no label wraps, all controls full width,
validation still appears, save-on-blur still fires.
@@ -642,6 +642,9 @@
'wheel.buy': 'Buy spin',
'wheel.winners': 'Latest winners',
'wheel.winners.empty': 'No winners yet',
'wheel.win.title': 'You won!',
'wheel.win.jackpot': '★ Jackpot ★',
'wheel.win.nothing': 'Better luck next time!',
// ------------------------------------------------------------------------
// Soundboard
@@ -674,6 +677,8 @@
'rarevalues.editor.weight': 'Chance',
'rarevalues.editor.label': 'Label',
'rarevalues.editor.save': 'Save',
'rarevalues.editor.add': '+ Add prize',
'rarevalues.editor.remove': 'Remove prize',
'rarevalues.editor.cat.item': 'Furni (ID)',
'rarevalues.editor.cat.spin': 'Extra spins',
'rarevalues.editor.cat.nothing': 'Nothing',
@@ -700,4 +705,71 @@
'chatcmd.client.ejectall': 'Eject all furni',
'chatcmd.client.settings': 'Room settings',
'chatcmd.client.info': 'Client info',
// ------------------------------------------------------------------------
// Me-menu settings + User account settings window
// ------------------------------------------------------------------------
'usersettings.tab.general': "General",
'usersettings.tab.themes': "Themes",
'memenu.settings.other.place.multiple.objects': "Place multiple objects",
'memenu.settings.other.skip.purchase.confirmation': "Skip purchase confirmation",
'memenu.settings.other.enable.chat.window': "Enable chat window",
'memenu.settings.other.catalog.classic.style': "Catalog: classic style",
'usersettings.open.title': "User settings",
'usersettings.open.subtitle': "Password & account",
'usersettings.themes.custom': "Custom theme",
'usersettings.themes.default_option': "Default (no theme)",
'usersettings.themes.active_pieces': "Active pieces",
'usersettings.themes.invalid': "Theme invalid or unreachable — using the default.",
'usersettings.themes.none': "No themes available. Add a folder in custom-themes/ on the server.",
'usersettings.title': "User Settings",
'usersettings.account.label': "My account",
'usersettings.guest': "Guest",
'usersettings.subtitle': "Manage your account and security",
'usersettings.menu.section': "Account",
'usersettings.menu.password.title': "Reset password",
'usersettings.menu.password.desc': "Change the password used to log in.",
'usersettings.menu.email.title': "Change email",
'usersettings.menu.email.desc': "Update the email address on your account.",
'usersettings.menu.username.title': "Change username",
'usersettings.menu.username.desc': "Pick a new name. You'll need to log in again.",
'usersettings.menu.soon.title': "More coming soon",
'usersettings.menu.soon.desc': "Two-factor authentication and more.",
'usersettings.password.hint': "Use at least %count% characters. Mix upper & lowercase, numbers and symbols for a stronger password.",
'usersettings.email.hint': "For security we ask you to confirm your current password before changing the email on your account.",
'usersettings.username.hint': "Renaming will log you out and you can only rename again after 30 days. Make sure your friends know your new name!",
'usersettings.field.current_password': "Current password",
'usersettings.field.new_password': "New password",
'usersettings.field.retype_password': "Retype new password",
'usersettings.field.new_email': "New email address",
'usersettings.field.new_username': "New username",
'usersettings.username.rules': "%min%-%max% characters. Letters, numbers, dot, underscore and dash only.",
'usersettings.strength.weak': "Weak",
'usersettings.strength.fair': "Fair",
'usersettings.strength.good': "Good",
'usersettings.strength.strong': "Strong",
'usersettings.aria.show_password': "Show password",
'usersettings.aria.hide_password': "Hide password",
'usersettings.btn.cancel': "Cancel",
'usersettings.btn.saving': "Saving…",
'usersettings.btn.save_password': "Save password",
'usersettings.btn.save_email': "Save email",
'usersettings.btn.renaming': "Renaming…",
'usersettings.btn.rename': "Rename me",
'usersettings.error.fields_required': "All fields are required.",
'usersettings.error.password_min': "Password must be at least %count% characters.",
'usersettings.error.password_long': "Password is too long.",
'usersettings.error.password_mismatch': "New passwords do not match.",
'usersettings.error.password_same': "New password must be different from the current password.",
'usersettings.error.not_authenticated': "You are not authenticated. Please log in again.",
'usersettings.error.network': "Could not reach the server. Please try again.",
'usersettings.error.request_failed': "Request failed (%status%).",
'usersettings.error.email_long': "Email address is too long.",
'usersettings.error.email_invalid': "Please enter a valid email address.",
'usersettings.error.username_length': "Username must be between %min% and %max% characters.",
'usersettings.error.username_invalid': "Username may only contain letters, numbers, dot, underscore and dash.",
'usersettings.error.username_same': "New username must be different from the current one.",
'usersettings.success.password': "Password updated successfully.",
'usersettings.success.email': "Email updated successfully.",
'usersettings.success.username': "Username updated. Please log in again with your new name.",
}
@@ -642,6 +642,9 @@
'wheel.buy': 'Acquista giro',
'wheel.winners': 'Ultimi vincitori',
'wheel.winners.empty': 'Ancora nessun vincitore',
'wheel.win.title': 'Hai vinto!',
'wheel.win.jackpot': '★ Jackpot ★',
'wheel.win.nothing': 'Sarà per la prossima!',
// ------------------------------------------------------------------------
// Soundboard
@@ -674,6 +677,8 @@
'rarevalues.editor.weight': 'Probabilità',
'rarevalues.editor.label': 'Etichetta',
'rarevalues.editor.save': 'Salva',
'rarevalues.editor.add': '+ Aggiungi premio',
'rarevalues.editor.remove': 'Rimuovi premio',
'rarevalues.editor.cat.item': 'Arredo (ID)',
'rarevalues.editor.cat.spin': 'Giri extra',
'rarevalues.editor.cat.nothing': 'Niente',
@@ -700,4 +705,71 @@
'chatcmd.client.ejectall': 'Rimuovi tutti gli arredi',
'chatcmd.client.settings': 'Impostazioni stanza',
'chatcmd.client.info': 'Info client',
// ------------------------------------------------------------------------
// Me-menu settings + User account settings window
// ------------------------------------------------------------------------
'usersettings.tab.general': "Generale",
'usersettings.tab.themes': "Temi",
'memenu.settings.other.place.multiple.objects': "Posiziona più oggetti",
'memenu.settings.other.skip.purchase.confirmation': "Salta la conferma d'acquisto",
'memenu.settings.other.enable.chat.window': "Abilita finestra chat",
'memenu.settings.other.catalog.classic.style': "Catalogo: stile classico",
'usersettings.open.title': "Impostazioni utente",
'usersettings.open.subtitle': "Password e account",
'usersettings.themes.custom': "Tema personalizzato",
'usersettings.themes.default_option': "Predefinito (nessun tema)",
'usersettings.themes.active_pieces': "Elementi attivi",
'usersettings.themes.invalid': "Tema non valido o non raggiungibile — uso il predefinito.",
'usersettings.themes.none': "Nessun tema disponibile. Aggiungi una cartella in custom-themes/ sul server.",
'usersettings.title': "Impostazioni utente",
'usersettings.account.label': "Il mio account",
'usersettings.guest': "Ospite",
'usersettings.subtitle': "Gestisci il tuo account e la sicurezza",
'usersettings.menu.section': "Account",
'usersettings.menu.password.title': "Reimposta password",
'usersettings.menu.password.desc': "Cambia la password che usi per accedere.",
'usersettings.menu.email.title': "Cambia email",
'usersettings.menu.email.desc': "Aggiorna l'indirizzo email del tuo account.",
'usersettings.menu.username.title': "Cambia nome utente",
'usersettings.menu.username.desc': "Scegli un nuovo nome. Dovrai accedere di nuovo.",
'usersettings.menu.soon.title': "Altro in arrivo",
'usersettings.menu.soon.desc': "Autenticazione a due fattori e altro.",
'usersettings.password.hint': "Usa almeno %count% caratteri. Combina maiuscole e minuscole, numeri e simboli per una password più sicura.",
'usersettings.email.hint': "Per sicurezza ti chiediamo di confermare la password attuale prima di cambiare l'email del tuo account.",
'usersettings.username.hint': "Cambiando nome verrai disconnesso e potrai rinominarti di nuovo solo dopo 30 giorni. Assicurati che i tuoi amici conoscano il tuo nuovo nome!",
'usersettings.field.current_password': "Password attuale",
'usersettings.field.new_password': "Nuova password",
'usersettings.field.retype_password': "Ripeti la nuova password",
'usersettings.field.new_email': "Nuovo indirizzo email",
'usersettings.field.new_username': "Nuovo nome utente",
'usersettings.username.rules': "%min%-%max% caratteri. Solo lettere, numeri, punto, trattino basso e trattino.",
'usersettings.strength.weak': "Debole",
'usersettings.strength.fair': "Discreta",
'usersettings.strength.good': "Buona",
'usersettings.strength.strong': "Forte",
'usersettings.aria.show_password': "Mostra password",
'usersettings.aria.hide_password': "Nascondi password",
'usersettings.btn.cancel': "Annulla",
'usersettings.btn.saving': "Salvataggio…",
'usersettings.btn.save_password': "Salva password",
'usersettings.btn.save_email': "Salva email",
'usersettings.btn.renaming': "Rinomina…",
'usersettings.btn.rename': "Rinominami",
'usersettings.error.fields_required': "Tutti i campi sono obbligatori.",
'usersettings.error.password_min': "La password deve contenere almeno %count% caratteri.",
'usersettings.error.password_long': "La password è troppo lunga.",
'usersettings.error.password_mismatch': "Le nuove password non corrispondono.",
'usersettings.error.password_same': "La nuova password deve essere diversa da quella attuale.",
'usersettings.error.not_authenticated': "Non sei autenticato. Effettua di nuovo l'accesso.",
'usersettings.error.network': "Impossibile raggiungere il server. Riprova.",
'usersettings.error.request_failed': "Richiesta non riuscita (%status%).",
'usersettings.error.email_long': "L'indirizzo email è troppo lungo.",
'usersettings.error.email_invalid': "Inserisci un indirizzo email valido.",
'usersettings.error.username_length': "Il nome utente deve contenere tra %min% e %max% caratteri.",
'usersettings.error.username_invalid': "Il nome utente può contenere solo lettere, numeri, punto, trattino basso e trattino.",
'usersettings.error.username_same': "Il nuovo nome utente deve essere diverso da quello attuale.",
'usersettings.success.password': "Password aggiornata con successo.",
'usersettings.success.email': "Email aggiornata con successo.",
'usersettings.success.username': "Nome utente aggiornato. Accedi di nuovo con il tuo nuovo nome.",
}
+82 -10
View File
@@ -372,14 +372,14 @@
// ------------------------------------------------------------------------
// Login
// ------------------------------------------------------------------------
'login.username': 'Wat is jou Camwijs naam',
'login.username': 'Wat is jou habbo naam',
'login.forgot_password': 'Wachtwoord vergeten?',
// First-time visitors card
'nitro.login.firsttime.title': 'Voor het eerst hier?',
'nitro.login.firsttime.text': 'Heb je nog geen Camwijs account?',
'nitro.login.firsttime.text': 'Heb je nog geen habbo account?',
'nitro.login.firsttime.link': 'Je kunt er hier een aanmaken',
'nitro.login.card.title': 'Aanmelden bij Camwijs',
'nitro.login.card.title': 'Aanmelden bij habbo',
// Server status checks
'nitro.login.server.offline.short': 'De gameserver draait momenteel niet. Probeer het zo meteen opnieuw.',
@@ -388,12 +388,12 @@
'nitro.login.server.retry': 'Opnieuw proberen',
// Registration flow
'nitro.login.register.title': 'Camwijs-gegevens',
'nitro.login.register.title': 'habbo-gegevens',
'nitro.login.register.next': 'Volgende',
'nitro.login.register.finish': 'Voltooien',
'nitro.login.register.creating': 'Bezig met aanmaken…',
'nitro.login.register.intro.credentials': 'Laten we je account aanmaken. Voer je e-mailadres in en kies een wachtwoord — we controleren of dit e-mailadres nog niet in gebruik is.',
'nitro.login.register.intro.avatar': 'Nu is het tijd om je eigen Camwijs-personage te maken! Begin met het kiezen van je Camwijs-naam.',
'nitro.login.register.intro.avatar': 'Nu is het tijd om je eigen habbo-personage te maken! Begin met het kiezen van je habbo-naam.',
'nitro.login.register.intro.room': 'Laatste stap — kies een startkamer, of sla dit over en maak later je eigen kamer.',
'nitro.login.register.confirm.label': 'Bevestig wachtwoord',
'nitro.login.register.username.placeholder': 'HabboNaam',
@@ -412,8 +412,8 @@
'nitro.login.forgot.success': 'E-mail verzonden! Als er een account bij dit adres hoort, vind je binnenkort een resetlink in je inbox (controleer je spam als je binnen een minuut niets ziet).',
// Login errors (validation + transport)
'nitro.login.error.missing_credentials': 'Voer zowel je Camwijs-naam als wachtwoord in.',
'nitro.login.error.invalid_credentials': 'Ongeldige Camwijs-naam of wachtwoord.',
'nitro.login.error.missing_credentials': 'Voer zowel je habbo-naam als wachtwoord in.',
'nitro.login.error.invalid_credentials': 'Ongeldige habbo-naam of wachtwoord.',
'nitro.login.error.too_many_attempts': 'Te veel pogingen. Probeer het opnieuw over %seconds%s.',
'nitro.login.error.turnstile': 'Voltooi de beveiligingscontrole.',
'nitro.login.error.server_offline': 'De gameserver draait niet. Probeer het later opnieuw.',
@@ -427,9 +427,9 @@
'nitro.login.error.password_too_short': 'Je wachtwoord moet minimaal 8 tekens lang zijn.',
'nitro.login.error.password_mismatch': 'Wachtwoorden komen niet overeen.',
'nitro.login.error.email_taken': 'Dit e-mailadres is al in gebruik.',
'nitro.login.error.missing_username': 'Kies een Camwijs-naam.',
'nitro.login.error.username_length': 'De Camwijs-naam moet 316 tekens bevatten.',
'nitro.login.error.username_taken': 'Deze Camwijs-naam is al in gebruik.',
'nitro.login.error.missing_username': 'Kies een habbo-naam.',
'nitro.login.error.username_length': 'De habbo-naam moet 316 tekens bevatten.',
'nitro.login.error.username_taken': 'Deze habbo-naam is al in gebruik.',
'nitro.login.error.missing_email': 'Voer je e-mailadres in.',
// ------------------------------------------------------------------------
@@ -644,6 +644,9 @@
'wheel.buy': 'Draaibeurt kopen',
'wheel.winners': 'Laatste winnaars',
'wheel.winners.empty': 'Nog geen winnaars',
'wheel.win.title': 'Gewonnen!',
'wheel.win.jackpot': '★ Jackpot ★',
'wheel.win.nothing': 'Volgende keer beter!',
// ------------------------------------------------------------------------
// Soundboard
@@ -676,6 +679,8 @@
'rarevalues.editor.weight': 'Kans',
'rarevalues.editor.label': 'Label',
'rarevalues.editor.save': 'Opslaan',
'rarevalues.editor.add': '+ Prijs toevoegen',
'rarevalues.editor.remove': 'Prijs verwijderen',
'rarevalues.editor.cat.item': 'Meubel (ID)',
'rarevalues.editor.cat.spin': 'Extra draaien',
'rarevalues.editor.cat.nothing': 'Niets',
@@ -702,4 +707,71 @@
'chatcmd.client.ejectall': 'Verwijder alle meubels',
'chatcmd.client.settings': 'Kamerinstellingen',
'chatcmd.client.info': 'Client info',
// ------------------------------------------------------------------------
// Me-menu settings + User account settings window
// ------------------------------------------------------------------------
'usersettings.tab.general': "Algemeen",
'usersettings.tab.themes': "Thema's",
'memenu.settings.other.place.multiple.objects': "Meerdere objecten plaatsen",
'memenu.settings.other.skip.purchase.confirmation': "Aankoopbevestiging overslaan",
'memenu.settings.other.enable.chat.window': "Chatvenster inschakelen",
'memenu.settings.other.catalog.classic.style': "Catalogus: klassieke stijl",
'usersettings.open.title': "Gebruikersinstellingen",
'usersettings.open.subtitle': "Wachtwoord & account",
'usersettings.themes.custom': "Aangepast thema",
'usersettings.themes.default_option': "Standaard (geen thema)",
'usersettings.themes.active_pieces': "Actieve onderdelen",
'usersettings.themes.invalid': "Thema ongeldig of onbereikbaar — standaard wordt gebruikt.",
'usersettings.themes.none': "Geen thema's beschikbaar. Voeg een map toe in custom-themes/ op de server.",
'usersettings.title': "Gebruikersinstellingen",
'usersettings.account.label': "Mijn account",
'usersettings.guest': "Gast",
'usersettings.subtitle': "Beheer je account en beveiliging",
'usersettings.menu.section': "Account",
'usersettings.menu.password.title': "Wachtwoord wijzigen",
'usersettings.menu.password.desc': "Wijzig het wachtwoord waarmee je inlogt.",
'usersettings.menu.email.title': "E-mail wijzigen",
'usersettings.menu.email.desc': "Werk het e-mailadres van je account bij.",
'usersettings.menu.username.title': "Gebruikersnaam wijzigen",
'usersettings.menu.username.desc': "Kies een nieuwe naam. Je moet daarna opnieuw inloggen.",
'usersettings.menu.soon.title': "Meer komt binnenkort",
'usersettings.menu.soon.desc': "Tweestapsverificatie en meer.",
'usersettings.password.hint': "Gebruik minimaal %count% tekens. Combineer hoofd- en kleine letters, cijfers en symbolen voor een sterker wachtwoord.",
'usersettings.email.hint': "Voor de veiligheid vragen we je je huidige wachtwoord te bevestigen voordat je het e-mailadres van je account wijzigt.",
'usersettings.username.hint': "Door je naam te wijzigen word je uitgelogd en je kunt pas na 30 dagen opnieuw wijzigen. Zorg dat je vrienden je nieuwe naam kennen!",
'usersettings.field.current_password': "Huidig wachtwoord",
'usersettings.field.new_password': "Nieuw wachtwoord",
'usersettings.field.retype_password': "Herhaal nieuw wachtwoord",
'usersettings.field.new_email': "Nieuw e-mailadres",
'usersettings.field.new_username': "Nieuwe gebruikersnaam",
'usersettings.username.rules': "%min%-%max% tekens. Alleen letters, cijfers, punt, underscore en streepje.",
'usersettings.strength.weak': "Zwak",
'usersettings.strength.fair': "Redelijk",
'usersettings.strength.good': "Goed",
'usersettings.strength.strong': "Sterk",
'usersettings.aria.show_password': "Wachtwoord tonen",
'usersettings.aria.hide_password': "Wachtwoord verbergen",
'usersettings.btn.cancel': "Annuleren",
'usersettings.btn.saving': "Opslaan…",
'usersettings.btn.save_password': "Wachtwoord opslaan",
'usersettings.btn.save_email': "E-mail opslaan",
'usersettings.btn.renaming': "Bezig met hernoemen…",
'usersettings.btn.rename': "Hernoem mij",
'usersettings.error.fields_required': "Alle velden zijn verplicht.",
'usersettings.error.password_min': "Het wachtwoord moet minimaal %count% tekens bevatten.",
'usersettings.error.password_long': "Het wachtwoord is te lang.",
'usersettings.error.password_mismatch': "De nieuwe wachtwoorden komen niet overeen.",
'usersettings.error.password_same': "Het nieuwe wachtwoord moet anders zijn dan het huidige wachtwoord.",
'usersettings.error.not_authenticated': "Je bent niet ingelogd. Log opnieuw in.",
'usersettings.error.network': "Kan de server niet bereiken. Probeer het opnieuw.",
'usersettings.error.request_failed': "Verzoek mislukt (%status%).",
'usersettings.error.email_long': "Het e-mailadres is te lang.",
'usersettings.error.email_invalid': "Voer een geldig e-mailadres in.",
'usersettings.error.username_length': "De gebruikersnaam moet tussen %min% en %max% tekens bevatten.",
'usersettings.error.username_invalid': "De gebruikersnaam mag alleen letters, cijfers, punt, underscore en streepje bevatten.",
'usersettings.error.username_same': "De nieuwe gebruikersnaam moet anders zijn dan de huidige.",
'usersettings.success.password': "Wachtwoord succesvol bijgewerkt.",
'usersettings.success.email': "E-mail succesvol bijgewerkt.",
'usersettings.success.username': "Gebruikersnaam bijgewerkt. Log opnieuw in met je nieuwe naam.",
}
@@ -32,7 +32,6 @@
"badge.asset.url": "${image.library.url}album1584/%badgename%.gif",
"radio.url": "${gamedata.url}/radio-stations.json5?t=%timestamp%",
"soundboard.url": "${gamedata.url}/soundboard-sounds.json5?t=%timestamp%",
"radio_ui": false,
"furni.rotation.bounce.steps": 20,
"furni.rotation.bounce.height": 0.0625,
"enable.avatar.arrow": false,
+1
View File
@@ -24,6 +24,7 @@
"wired.action.kick.from.room.max.length": 100,
"wired.action.mute.user.max.length": 100,
"game.center.enabled": false,
"radio_ui.enabled": false,
"guides.enabled": true,
"housekeeping.enabled": true,
"toolbar.hide.quests": true,
+3
View File
@@ -7,6 +7,7 @@ import { LoginView } from './components/login/LoginView';
import { MainView } from './components/MainView';
import { ReconnectView } from './components/reconnect/ReconnectView';
import { useMessageEvent, useNitroEvent } from './hooks';
import { ensureChatCommandListener } from './hooks/rooms/widgets/useChatCommandSelector';
NitroVersion.UI_VERSION = GetUIVersion();
@@ -562,7 +563,9 @@ export const App: FC<{}> = props =>
bumpProgress(85, taskLabel('loading.task.rooms', 'Loading rooms...'));
await GetRoomEngine().init();
bumpProgress(92, taskLabel('loading.task.engine', 'Loading graphics engine...'));
ensureChatCommandListener();
await GetCommunication().init();
ensureChatCommandListener();
bumpProgress(98, taskLabel('generic.reconnecting', 'Connecting to server...'));
})();
}
+1
View File
@@ -28,6 +28,7 @@ export * from './room';
export * from './room/events';
export * from './room/widgets';
export * from './soundboard';
export * from './theme';
export * from './ui-settings';
export * from './user';
export * from './utils';
@@ -5,6 +5,7 @@ export class RoomWidgetUpdateChatInputContentEvent extends RoomWidgetUpdateEvent
public static CHAT_INPUT_CONTENT: string = 'RWUCICE_CHAT_INPUT_CONTENT';
public static WHISPER: string = 'whisper';
public static SHOUT: string = 'shout';
public static TEXT: string = 'text';
private _chatMode: string = '';
private _userName: string = '';
+122
View File
@@ -0,0 +1,122 @@
import { NitroLogger } from '@nitrots/nitro-renderer';
import { GetConfigurationValue } from '../nitro';
// ---------------------------------------------------------------------------
// Custom theme ecosystem (graphics-only, runtime-loaded).
//
// A "theme" is a folder on the server (NOT bundled in the build) made of:
// <base>/index.json -> { "themes": [ { id, name, author? } ] }
// <base>/<id>/theme.json -> { name, pieces: [ { id, name, file } ] }
// <base>/<id>/<file>.css -> one CSS "piece" (cards, chat, catalog, ...)
//
// Each enabled piece is injected as a <link> in <head>. If a piece fails to
// load (404 / network) the link removes itself, so the UI falls back to the
// default look for that piece (per-piece fallback, never breaks the client).
//
// The base url is configurable via ui-config ("theme.base.url") so themes can
// live anywhere (and never need a client rebuild to add/change them).
// ---------------------------------------------------------------------------
export interface ThemeInfo
{
id: string;
name: string;
author?: string;
}
export interface ThemePiece
{
id: string;
name: string;
file: string;
}
export interface ThemeManifest
{
name: string;
pieces: ThemePiece[];
}
const LINK_ATTR = 'data-nitro-theme';
export const GetThemeBaseUrl = (): string =>
GetConfigurationValue<string>('theme.base.url', 'custom-themes').replace(/\/+$/, '');
export const FetchThemeIndex = async (): Promise<ThemeInfo[]> =>
{
try
{
const response = await fetch(`${ GetThemeBaseUrl() }/index.json`, { cache: 'no-cache' });
if(!response.ok) return [];
const data = await response.json();
return Array.isArray(data?.themes) ? data.themes.filter((t: any) => t && t.id) : [];
}
catch(error)
{
NitroLogger.warn('[ThemeManager] index.json non caricabile, nessun tema custom', error);
return [];
}
};
export const FetchThemeManifest = async (themeId: string): Promise<ThemeManifest> =>
{
if(!themeId) return null;
try
{
const response = await fetch(`${ GetThemeBaseUrl() }/${ themeId }/theme.json`, { cache: 'no-cache' });
if(!response.ok) return null;
const data = await response.json();
if(!data || !Array.isArray(data.pieces)) return null;
return {
name: data.name ?? themeId,
pieces: data.pieces.filter((p: any) => p && p.id && p.file)
};
}
catch(error)
{
NitroLogger.warn(`[ThemeManager] manifest non valido per tema "${ themeId }" -> fallback default`, error);
return null;
}
};
export const ClearTheme = (): void =>
{
document.head.querySelectorAll(`link[${ LINK_ATTR }]`).forEach(node => node.remove());
};
export const ApplyThemePieces = (themeId: string, pieces: ThemePiece[]): void =>
{
ClearTheme();
if(!themeId || !pieces || !pieces.length) return;
const base = GetThemeBaseUrl();
for(const piece of pieces)
{
const link = document.createElement('link');
link.rel = 'stylesheet';
link.setAttribute(LINK_ATTR, piece.id);
link.href = `${ base }/${ themeId }/${ piece.file }`;
// Per-piece fallback: a broken piece removes itself, leaving the default.
link.onerror = () =>
{
NitroLogger.warn(`[ThemeManager] pezzo tema rotto "${ themeId }/${ piece.file }" -> fallback default`);
link.remove();
};
document.head.appendChild(link);
}
};
+1
View File
@@ -0,0 +1 @@
export * from './ThemeManager';
+2
View File
@@ -5,4 +5,6 @@ export class LocalStorageKeys
public static CHAT_WINDOW_ENABLED: string = 'chatWindowEnabled';
public static CHAT_TRANSLATION_SETTINGS: string = 'chatTranslationSettings';
public static CATALOG_CLASSIC_STYLE: string = 'catalogClassicStyle';
public static THEME_ACTIVE: string = 'nitroThemeActive';
public static THEME_PIECES: string = 'nitroThemePieces';
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 661 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 831 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 B

+2
View File
@@ -28,6 +28,7 @@ import { HousekeepingView } from './housekeeping/HousekeepingView';
import { RareValuesView } from './rare-values/RareValuesView';
import { FortuneWheelView } from './fortune-wheel/FortuneWheelView';
import { SoundboardView } from './soundboard/SoundboardView';
import { ThemeApplier } from './theme/ThemeApplier';
import { RadioView } from './radio/RadioView';
import { InventoryView } from './inventory/InventoryView';
import { ModToolsView } from './mod-tools/ModToolsView';
@@ -134,6 +135,7 @@ export const MainView: FC<{}> = props =>
return (
<>
<ThemeApplier />
<div className="hidden" data-localization-version={ localizationVersion } />
<AnimatePresence>
{ landingViewVisible &&

Some files were not shown because too many files have changed in this diff Show More