Commit Graph

599 Commits

Author SHA1 Message Date
simoleo89 12d24719cf feat(floorplan-editor): polish height slider + add hand tool for canvas pan
Two related polish improvements after the swatch-column → vertical-
slider swap.

Slider
- Wider track (18 px, was 14 px) for a more comfortable click area
  with the same on-screen footprint.
- Min / max chips above and below the rail (HEIGHT_BRUSH_MIN /
  _MAX) so users know which end is high and which is low without
  hovering to discover.
- Thumb now uses a warm amber radial gradient (#fff7c4 → #facc15
  → #ca8a04) on a dark brown border with a soft drop shadow + inset
  highlight, instead of the flat yellow disc. Hover adds a white
  ring; drag swaps it for a darker ring — clear gesture feedback.
- Track gains a hover/drag glow (inset white seam + amber outline
  via boxShadow) so you can tell the slider has focus before you
  even click.

Hand tool (canvas pan)
- New FloorplanToolbar button (FaHandPaper, sticky toggle, emerald
  fill when active) ties to a new  state lifted into
  FloorplanEditorView. When the hand is active, plain left-click
  + drag pans the canvas instead of brushing tiles. Cursor flips
  to grab / grabbing accordingly.
- FloorplanCanvasSVG's isPanGesture predicate becomes:
  middle-mouse  OR  Shift+left-click  OR  (panMode && left-click).
  Shift / middle still work whether or not the hand is on so power
  users keep their muscle memory.
- No change to the reducer (panMode is a canvas-level UI flag, not
  a brush action — keeps state/types tight).
2026-05-24 21:33:12 +02:00
simoleo89 abf43d86c3 feat(floorplan-editor): swap the height swatch column for a vertical slider
Replaces the SVG column of 27 colour swatches with a vertical
slider that fills the same role (pick a brush height 0-26) but
much faster to scrub:

- Track is a discrete-step linear gradient built from the real
  tile-fill colours, top = HEIGHT_BRUSH_MAX, bottom =
  HEIGHT_BRUSH_MIN. Each height occupies a clear band so the
  user still reads colour-to-height at a glance.
- Yellow circular thumb shows the current value as a number,
  centred at the picked height's band, with a darker border
  while dragging so the drag affordance is obvious.
- Click anywhere on the track to jump; the same gesture starts
  a drag (pointermove on window) so users can scrub up/down
  without releasing. Pointer-cancel + button-other-than-0 are
  handled.
- ARIA: slider role + valuemin / valuemax / valuenow, plus a
  touch-none style so mobile scrolling doesn't fight the drag.

Tests rewritten around the new contract (5 cases):
- thumb renders with the current value;
- click at top -> picks 26;
- click at bottom -> picks 0;
- click at middle -> picks 13;
- click at the band that's already selected -> no onSelect
  call (idempotent).
Track geometry is stubbed via getBoundingClientRect so the
pointer math is reproducible under jsdom. afterEach(cleanup)
keeps multiple renders from colliding on the data-testid lookup.
2026-05-24 21:27:22 +02:00
simoleo89 b540b163c6 feat(floorplan-editor): React rewrite + live in-room preview + UX polish
Complete modernization of the floor-plan editor. Three layered
changes shipped together since they share state shapes and the
test infrastructure stubs.

1) React rewrite (state + hooks + views + tests)

   Drops the FloorplanEditorContext singleton + legacy view
   components and replaces them with a pure-React reducer
   architecture:

   - state/ — typed FloorplanState + FloorplanAction union,
     pure reducer covering PAINT_TILE / ERASE_TILE /
     ADJUST_HEIGHT / SET_DOOR / SET_DOOR_DIR / SET_THICKNESS /
     SET_WALL_HEIGHT / BRUSH_SET / SELECT_RECT / SELECT_ALL /
     CLEAR_SELECTION / SQUARE_SELECT_TOGGLE / IMPORT_STRING /
     APPLY_REMOTE_DIFF / APPLY_REMOTE_SNAPSHOT. Source-tagged
     ('local' | 'remote') so the editor can distinguish user
     edits from server pushes. Co-located encoding helpers
     (parseTilemap / serializeTilemap) and area-counter
     selectors.
   - hooks/ — useFloorplanReducer (wraps useReducer with a
     history stack + loadFromServer + undo/redo), useTool
     (pointer events -> dispatch), usePointerToTile (screen
     -> tile projection that respects the viewBox origin so
     pan/zoom stays accurate).
   - views/ — FloorplanCanvasSVG, FloorplanHeightPicker,
     FloorplanToolbar, FloorplanOptionsPanel,
     FloorplanImportExport, FloorplanTile,
     FloorplanPreviewSVG (alternative iso preview kept as a
     fallback view, not wired into the main layout).
   - Co-located Vitest suites for every module above (encoding,
     reducer, selectors, hooks, views, integration). 100+ new
     test cases.

2) Live in-room preview (NEW capability)

   useFloorplanLiveSync drives client-side preview of the edit
   directly into the active room — every tile / door / wall
   height / thickness change is applied through
   GetRoomMessageHandler().applyFloorModelLocally (new public
   method on the renderer, see paired renderer PR) with
   zero server traffic during editing. The wire
   UpdateFloorPropertiesMessageComposer is only sent when the
   user explicitly clicks Save. Thickness slider additionally
   calls RoomEngine.updateRoomInstancePlaneThickness for
   zero-latency wall/floor-depth feedback while dragging.

   Toggle 'Live preview ON / OFF' in the bottom strip (default
   ON) lets the user opt out if they want to keep changes
   contained to the editor's own preview until Save.
   Revert button re-applies the original snapshot locally so
   the room snaps back to where it was when the editor opened.

3) UX polish

   - Undo / Redo (Ctrl+Z, Ctrl+Shift+Z / Ctrl+Y) backed by a
     100-step history stack inside useFloorplanReducer. Local
     mutating actions push history; brush/selection UI bumps
     and remote dispatches bypass it; loadFromServer wipes the
     stack.
   - Zoom 40-600 % with Ctrl+wheel, +/- buttons, % label.
     Shift+drag or middle-mouse drag pans the canvas.
   - Auto-fit on first paint: computes the screen-space
     bounding box of the painted (non-blocked) tiles, picks the
     zoom that just contains them with a 5 % margin, pans so
     the room sits in the viewport centre. Default view is now
     'room fills the canvas' instead of 'room is a dot at the
     top-centre of a huge empty canvas'. Clicking the % label
     re-runs the fit; crosshair button keeps zoom and recentres
     the pan only.
   - Door direction control: arrows + door icon triplet
     (8-way rotate by single click on prev/next, full cycle
     forward on the icon itself). Wall and floor thickness
     collapse from two 4-button rows into two compact
     segmented selectors (active state in emerald). Saves
     significant horizontal space.
   - Habbo floor pattern tile (~186 B PNG, vendored from
     habbofurni.com/images/furni_floor.png) tiled as the
     canvas background with image-rendering: pixelated so the
     texture stays crisp at every zoom level. Replaces the
     solid black background.

Test infrastructure

   nitro-renderer.mock grows constructors / proxies / functions
   for everything the new floor-editor tests transitively
   import (floor composers + events, RoomEngineEvent,
   ILinkEventTracker, convertNumbersForSaving /
   convertSettingToNumber, GetRoomMessageHandler,
   GetTicker, GetRenderer, NitroTicker, RoomPreviewer with a
   sufficiently real .updatePreviewModel / dispose surface,
   and a TextureUtils.createRenderTexture that returns an
   object with a no-op .destroy). test-setup adds a no-op
   ResizeObserver polyfill (jsdom doesn't ship one and the
   optional FloorplanRoomPreview observes its container) and
   a draggable-windows-container portal root for tests that
   mount NitroCardView.

Files: 44 changed (mostly new). yarn typecheck 0 errors,
yarn test 341/341 green.
2026-05-24 21:19:10 +02:00
DuckieTM 20ffd5cd7c Merge pull request #155 from simoleo89/patch-1
ci: dynamically resolve renderer pairing by client context
2026-05-24 10:45:02 +02:00
Life dc42cc4240 Enhance CI workflow with dynamic renderer inputs
Added workflow_dispatch inputs for renderer configuration and modified the renderer ref resolution logic.
2026-05-24 10:03:52 +02:00
duckietm 0996ed24d3 🆙 More toolbar update :) 2026-05-22 17:17:33 +02:00
duckietm b3ff46a771 🆙 Fix Toolbar & Pets layout 2026-05-22 16:00:59 +02:00
duckietm df4ec5201b 🆙 Fix text in the navigator 2026-05-22 15:25:05 +02:00
duckietm 20588533d3 🆙 Added translation to the Catalog Text 2026-05-22 11:47:26 +02:00
duckietm 5002b2fcdc 🆙 Fix Catalog Edit 2026-05-21 16:58:47 +02:00
duckietm 49917ed49b 🆕 Redesign of HC Club buy, now also give as gift 2026-05-21 14:00:03 +02:00
duckietm 690a196d42 🆙 Fix texts 2026-05-21 08:59:26 +02:00
DuckieTM e9591acc0e Merge pull request #146 from medievalshell/Dev
feat(loading): redesigned loader with progress bar, task labels, configurable assets + perf(build): granular code-split + preconnect hint for cold-load speed + docs: PERFORMANCE.md — client + server recipe for the 4s cold load
2026-05-21 07:39:32 +02:00
DuckieTM 1b2552dd11 Merge pull request #145 from simoleo89/feat/mod-tools-redesign
feat(mod-tools): complete redesign + i18n + correctness fixes
2026-05-21 07:39:10 +02:00
DuckieTM 6abc765680 Merge pull request #144 from simoleo89/fix/button-icon-alignment
fix(button): align icon + text via inline-flex, not inline-block
2026-05-21 07:38:55 +02:00
DuckieTM 207b06e77c Merge pull request #143 from simoleo89/fix/v35-followups
fix: V3.5.0 follow-ups (DraggableWindow useRef, catalog test contract)
2026-05-21 07:38:36 +02:00
medievalshell 35f16a6745 docs: PERFORMANCE.md — client + server recipe for the 4s cold load
Standalone performance guide for the Nitro V3 client, covering both
sides of the cold-load story so a deployer doesn't have to cross-
reference two repos to get from 60-90 s down to 4 s.

Sections:

1. Three Nitro-side changes that matter (code split, LoadingView with
   real progress, remember-token URL capture)
2. Vite manualChunks — why vendor-first ordering matters, why pixi
   stays inlined, expected chunk sizes after yarn build
3. LoadingView state model + the 12-stage progress table + the
   pre-React shell template in scripts/write-asset-loader.mjs
4. Remember-token capture from URL → SetRememberLogin, with DevTools
   verification of nitro.auth.remember localStorage entry
5. nginx gzip (the single biggest win at ~17x for JSON5) + 30-day
   cache headers on gamedata + try_files manifest fallback
6. Windows + IIS equivalent — URL Rewrite + ARR reverse proxy to
   Node, Dynamic Compression toggle, web.config snippets,
   trade-offs (CPU cost, JDBC quirks, shared hosting caveat)
7. End-to-end verification probes — chunks present, gzip on, dir
   fallback works, progress bar renders, remember-token persisted

Cross-references medievalshell/InertiaCMS:docs/PERFORMANCE.md for the
matching CMS-side application config (SSO TTL, /api/auth/remember
endpoint, migrations).
2026-05-21 02:49:51 +02:00
medievalshell 450b0fface security: don't dump the SSO ticket in the prepare() diagnostic log
The `[App] prepare() start` console.warn was including the full SSO
ticket from `window.location.search`. SSO tickets are one-shot bearer
credentials — any leak (copied logs in a bug report, screen share,
malicious browser extension reading console output) grants
single-use access to the user's session. Replace the actual ticket
with a boolean.
2026-05-21 02:23:56 +02:00
medievalshell 0c7814fe04 perf(build): granular code-split + preconnect hint for cold-load speed
The vendor chunk was a single ~1MB blob (react + tanstack-query +
framer-motion + jodit + emoji-mart + react-icons + howler + zustand +
json5 all merged), forcing every cold load to wait on the slowest of
those modules before the page could interactivate. Split it into
domain-specific chunks so HTTP/2 multiplexing can pull them in
parallel and CF can cache each independently:

- vendor-pixi  (pixi.js + pixi-filters — when rollup actually splits;
                currently inlined into the umbrella renderer chunk
                because nitro-renderer is its sole importer)
- vendor-audio (howler)
- vendor-emoji (@emoji-mart — heaviest at ~430KB, only used in chat
                so a longer-term win is making it lazy)
- vendor-editor (jodit + @react-page — admin-only news editor)
- vendor-react (react / react-dom / scheduler / error-boundary)
- vendor-motion / vendor-query / vendor-icons / vendor-state /
  vendor-json5
- nitro-renderer-{avatar,communication,room,assets} — heaviest
  renderer packages get their own chunks when imported directly
  (the umbrella @nitrots/nitro-renderer still hosts the rest)

Also add a `<link rel=preconnect>` for challenges.cloudflare.com so
the Turnstile JS handshake doesn't pay an extra TLS round-trip on
the first paint.

Net effect: roughly the same total bytes shipped on a cold load, but
they fetch in parallel instead of sequentially, and a warm second
visitor only re-downloads the chunks whose code actually changed.
2026-05-21 02:03:38 +02:00
medievalshell 9e38de6160 feat(auth): capture remember-token from URL and persist for reconnect
The CMS Inertia /client page now passes `&token=<uuid>&token_exp=<unix>`
on the iframe src so Nitro can persist the token to localStorage on first
boot. `App.tsx::prepare()` reads them from `window.location.search` and
calls `SetRememberLogin({ token, expiresAt })` when no remember-login is
already stored.

This wires up the existing reconnect flow: when the WS drops, the loop
in `tryRememberLogin()` (already in this file) POSTs the saved token to
`login.remember.endpoint` (defaults to `${api.url}/api/auth/remember`)
and uses the returned fresh SSO ticket to reconnect. Without this step
the localStorage stayed empty and the reconnect always fell through to
"Session expired" after a few retries because Arcturus clears
`auth_ticket` on first consume.

Server side: the CMS counterpart is in medievalshell/InertiaCMS
commit on djoohotel — adds the /api/auth/remember endpoint backed by
`users_remember_families` (UUID family + 30-day expiry + revoked flag).
2026-05-21 01:01:06 +02:00
medievalshell d762f00c44 test(catalog): add getNodesByOfferId to useCatalogActions contract
`useCatalogActions` filter (useCatalog.ts:1042-1043) destructures and
returns `getNodesByOfferId` from the store along with the other action
methods. The filter contract test was stale — it asserted 11 keys while
the actual filter returns 12.

Add `getNodesByOfferId` to the expected keys list and to the fakeStore
mock so the assertion matches the live hook output.
2026-05-21 00:35:03 +02:00
medievalshell c170eb0a5b fix(types): unblock CI typecheck on Dev
Two pre-existing tsgo failures surfaced by the loading-screen redesign PR:

1. AvatarEffectsView: import loadGamedata via the umbrella
   `@nitrots/nitro-renderer` instead of `@nitrots/utils`. The deep
   sub-package alias only exists in vite.config.mjs; tsgo resolves
   against node_modules, where only the umbrella is symlinked. Same
   symbol — index.ts re-exports `* from '@nitrots/utils'`.

2. DraggableWindow: `useRef<HTMLDivElement>()` -> `useRef<HTMLDivElement>(null)`.
   React 19 typings now require an initial value. Fixed once in
   a39aa37, re-introduced by the merge in 03bebe4.
2026-05-21 00:31:14 +02:00
medievalshell c685c997a3 feat(loading): redesigned loader with progress bar, task labels, configurable assets
Loading screen overhaul:
- LoadingView: Nitro V3 logo flush top-left, loading.gif at viewport
  centre, large progress bar (max 900px / 90vw, h-8, gradient + glow)
  anchored bottom-centre with the percentage rendered inside the bar in
  Poppins, plus a friendly stage label underneath. Logo + background +
  progress bar colour overridable via renderer-config keys
  (loading.logo.url, loading.background, loading.progress.color).
- App.tsx: wired a real loadingProgress (0->100) + loadingTask driven by
  the boot pipeline: config init (10), renderer (20), per-warmup-task
  bumps for AssetManager/Localization/AvatarRender/SoundManager (25->70),
  session managers (78/85/92), Communication (98), ready (100). Each bump
  carries a task label looked up via a new taskLabel(key, fallback)
  helper so the Italian baseline ("Sto caricando il guardaroba",
  "Connessione al server", ...) can be translated by editing
  renderer-config; fallback keeps current strings if the key is missing.
- AvatarEffectsView: replace raw fetch(url).json() with
  loadGamedata(url) so the effectmap root manifest (JSON5 with
  // comments) parses correctly and supports the core/custom/seasonal
  tier merge.
- fallbackToLogin: respect login.screen.enabled=false. When login is
  disabled (SSO-only deployments), init failures now route to
  showSessionExpired() (home + diagnostic) instead of rendering an empty
  LoginView placeholder.
- scripts/write-asset-loader.mjs: the pre-React shell rendered into
  #root before the JS bundle takes over was a light-blue login skeleton
  (linear gradient + two grey rectangles) producing a visible flash
  before the real loader appeared. Replaced with the same
  radial-gradient the LoadingView paints — the handoff is now invisible.
- renderer-config.example: document the 13 loader keys so operators can
  copy & translate.
2026-05-21 00:22:17 +02:00
simoleo89 18effe33eb feat(toolbar): show open-ticket count badge on ModTools button
When a new CFH ticket arrives the moderator currently only finds out
by opening the ModTools launcher and looking at the Report Tool
counter. If the launcher is closed they have no signal — same
treatment friend requests already get on the People button next door.

Match the existing pattern: read `tickets` from `useModTools()`
(useBetween-shared, no extra subscription cost), filter to
state===1 (OPEN), and render a <LayoutItemCountView> over the
ToolbarItemView in absolute-positioned relative wrapper. Same
positioning as the friend-requests badge (-right-1 -top-1 z-10
pointer-events-none).

Gated on `isMod` so non-mods don't compute the filter or render
the wrapper — and since useModTools is a useBetween singleton its
event listeners only register once across the whole app regardless
of consumer count.

Applied to both toolbar layouts (desktop and mobile, lines ~272 and
~382) so the badge follows the user across breakpoints.
2026-05-20 22:10:43 +02:00
simoleo89 c2d581225b fix(button): align icon + text by forcing inline-flex display
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.
2026-05-20 22:07:36 +02:00
simoleo89 0ad284fa9c refactor(mod-tools): drop the launcher Context strip
The Context strip at the top of the launcher showed which room the
mod is currently observing — green pill + door icon when in a room,
zinc strip when not. In practice it's noise: the Room Tool / Chatlog
Tool buttons right under it already gate on the same in-room state
(disabled when not in a room) and carry their own tooltip explaining
that. The strip duplicated the signal without adding actionable info.

Remove the section, the now-unused FaDoorOpen / FaDoorClosed imports,
and the matching `modtools.window.section.context` /
`modtools.window.context.room` locale keys (from both the runtime
UITexts.json and the versioned UITexts.example template).
2026-05-20 22:01:58 +02:00
simoleo89 46daa96100 fix(mod-tools): empty-value placeholder no longer renders as music note
User reported empty fields (Email, Last Purchase, Lock Expires, Banned
Accs, Abusive CFHs) showing what looks like a music-note glyph next to
the label. They aren't censored — they're genuinely empty (a rank-7
Administrator account has none of that data populated). The em-dash
"—" (U+2014) used as the placeholder doesn't have a glyph in the
Habbo pixel font (Volter / Volter-Goldfish), so the engine falls
through to a placeholder glyph that on some font stacks looks like a
music note.

Two-part fix in ModToolsUserView (Field), ModToolsIssueInfoView
(Field) and ModToolsRoomView (owner fallback):

1. Replace the U+2014 em-dash with a plain ASCII `-`. Hyphen-minus is
   safely in Volter, so the placeholder renders correctly across the
   whole client.

2. The `value || placeholder` guard is now `(value || value === 0)`.
   Stat fields whose value is the literal number 0 — a clean account
   with cfhCount=0, banCount=0, cautionCount=0 — were rendering the
   placeholder because 0 is falsy. Treat 0 as a real value.

Also dropped the `italic` class on the placeholder span — the
hyphen does the job on its own and italic on a single-character
glyph in a pixel font was making it look like a tilted line.
2026-05-20 21:41:52 +02:00
simoleo89 91938985a2 refactor(mod-tools): launcher box gets context strip + section grouping
The launcher panel was a flat stack of four buttons (Room Tool, Chatlog
Tool, selected-user + presence dot inline, Report Tool) with no visual
hierarchy. The selected-user row was particularly cramped — name, the
2px dot and the 4×4 close-X all crammed into a single button row, easy
to misclick.

Reorganize into four logical groups, each with a small uppercase
section label:

  Context  — gradient strip (emerald when in a room, zinc when not)
             showing "Room #<id>" or "Enter a room first" with a
             matching door icon. Source of truth for "what is the
             mod observing right now"; both Room Tool and Chatlog
             Tool feed from the same currentRoomId.

  Room     — Room Tool + Chatlog Tool stacked. Both still gate on
             isInRoom; the disabled state now reads from a single
             flag instead of repeating `currentRoomId <= 0`.

  User     — When a user is selected: a card with the presence dot
             (emerald = still in room, zinc = left), the username at
             a real legible size, a bigger close button, plus a
             dedicated "Open Info" button to toggle ModToolsUserView.
             Splitting the click target from the close action removes
             the misclick footgun.
             When no user is selected: a dashed-border empty state
             with a FaUserSlash icon and the "Select a user" hint —
             reads as a clear "no selection" instead of an active
             button you can't press.

  Reports  — Report Tool with the open-ticket badge. Badge gets a 2px
             rose halo box-shadow so a new ticket pulses into view
             instead of competing with the button background.

Locale keys added under modtools.window.section.* and
modtools.window.context.room / modtools.window.user.open_info, in both
the runtime UITexts.json and the versioned UITexts.example template.

The "Open Info" button label is a fix in flight — the old layout
overloaded the username row to also open user info, with no separate
label. The new explicit button gets its own key so the action is
unambiguous (the previous version mislabelled the button as "Mod
Action", which is actually a different sub-panel).

typecheck + vitest 214/214 + JSON validation all clean.
2026-05-20 21:41:52 +02:00
simoleo89 a9515cb1a0 fix(mod-tools): chatlog wrappers back to useMessageEvent + useEffect
ModToolsChatlogView and CfhChatlogView were on the useNitroQuery
pattern. Symptom: the card opens, the spinner spins, but the data
never arrives — even when the server is correctly answering with
ModToolRoomChatlogComposer (header 3434) and GetCfhChatlogComposer
(607). Both header IDs match the renderer's Incoming map, both server
handlers gate only on ACC_SUPPORTTOOL and reply unconditionally when
the room/issue lookup succeeds. So the request DOES go out and the
response DOES come back — but useNitroQuery's listener (registered
via `new (ParserCtor)(callback)` + `registerMessageEvent`) isn't
delivering the event to the React side here.

ModToolsUserChatlogView already uses the plain `useMessageEvent` +
`useEffect(sendComposer)` pattern and works on this same setup, so
align the two broken views with it. Keep the loading-spinner empty
state introduced yesterday so the user still gets visible feedback
while the response is in flight.

This sidesteps useNitroQuery for these two cases rather than fixing
it in place — the underlying createNitroQuery + listener registration
plumbing still works for OfferView, useUserGroups, useClubOffers,
useSellablePetPalette, useMarketplaceConfiguration, useClubGifts,
CatalogLayoutRoomAdsView, so the regression is specific to these two
parsers and worth investigating separately. Filed as a follow-up.
2026-05-20 21:41:52 +02:00
simoleo89 65af9a564d fix(mod-tools): Room Chatlog button now renders a loading state
ModToolsChatlogView returned null whenever roomChatlog was undefined
— including the entire window between click and server response (up
to a 15-second NitroQuery timeout). Result: clicking the Chatlog
button in the launcher or in Room Info appeared to do nothing at all
on any session where the server reply was slow or the accept-filter
correlation didn't match.

The other two chatlog wrappers (ModToolsUserChatlogView,
CfhChatlogView) already render a spinner while data is loading after
yesterday's redesign — this view was the one I missed.

Apply the same fix: always render the NitroCardView, and show the
FaSpinner loading state inside until useNitroQuery resolves.
2026-05-20 21:41:52 +02:00
simoleo89 75815fa022 i18n(mod-tools): route every label/title/placeholder through LocalizeText
The ModTools template refresh introduced ~80 hardcoded English strings
(labels, placeholders, tooltips, empty-state copy, button text). Move
every one of them onto the modtools.* namespace and read via
LocalizeText so the panels translate alongside the rest of the client.

UITexts.example (versioned template) extended with the full set:

  modtools.window.*            Launcher box (toolbar item, tools,
                               selected-user state, ticket count)
  modtools.userinfo.*          User info card — already had the
                               legacy modtools.userinfo.{userName,
                               cfhCount, …} keys from before; added
                               refresh tooltip, presence pill labels
                               (in_room / online / offline with
                               matching .title tooltips), section
                               headings, action button labels, stat
                               card labels
  modtools.roominfo.*          Room info card — title, refresh, loading,
                               owner pill (here/away + tooltips), stat
                               labels, action buttons, moderate panel
                               heading + checkboxes + textarea
                               placeholder + caution/alert CTAs
  modtools.user.message.*      Send-message dialog (recipient label,
                               body label, placeholder, char counter,
                               empty state, send button)
  modtools.user.modaction.*    Mod Action form — header, sanctioning
                               label, 3-step section titles, select
                               placeholders, message label + optional
                               note, message placeholder, preview
                               heading, default/apply buttons, every
                               sendAlert error message
  modtools.user.visits.*       Room visits — title, header strip
                               heading, entry count (singular/plural),
                               empty state, column headers, visit
                               button + tooltip
  modtools.user.chatlog.*      User chatlog — title (with username
                               variant), loading state
  modtools.room.chatlog.*      Room chatlog title
  modtools.chatlog.*           Shared ChatlogView — column headers,
                               empty state, room-separator Visit/Tools
                               buttons
  modtools.tickets.*           Tickets window — title, tab labels
                               (open/mine/picked), column headers,
                               empty states, action buttons (pick/
                               handle/release), issue resolution
                               window (title, label, details heading,
                               field labels, chatlog toggle, resolve-as
                               heading, resolution buttons, release
                               back to queue), CFH chatlog title

The same 130 entries land in Nitro-Files/.../UITexts.json (runtime).
Both files validate as JSON. The runtime additions take effect on
next client reload; the template additions ship the strings to any
fresh deploy.

Notes:
  - The MOD_ACTION_DEFINITIONS sanction names ("Alert", "Mute 1h",
    "Ban 18h" …) stay hardcoded for now since they're keyed off
    server-side action IDs that don't have an existing locale key
    convention. Worth a follow-up if needed.
  - help.cfh.topic.* keys (CFH topic display names) are already in
    ExternalTexts.json and were already read via LocalizeText, so
    they didn't need changes.

typecheck + vitest 214/214 + lint:hooks all clean.
2026-05-20 21:41:52 +02:00
simoleo89 d3552a0948 refactor(mod-tools): redesign all related windows with shared visual language
Applies the visual language introduced in ModToolsUserView yesterday
to every other ModTools window. The design tokens used consistently:

  emerald — present in current room / positive state
  sky     — online / informational / current selection
  zinc    — neutral / disabled
  amber   — warn-level (CFH, alerts, cautions)
  rose    — danger (bans, releases, abusive)

Files redesigned:

ModToolsRoomView
  Identity header with FaDoorOpen, room name + ID, owner-present pill
  (emerald/zinc), manual refresh button. Stat strip: user count (sky)
  + clickable owner name (zinc) opening user info. Quick actions
  (Visit / Chatlog) in a 2-col grid. Moderate panel collapsed into an
  amber-tinted card with the 3 toggles + textarea + two CTAs (Send
  Caution=danger, Send Alert=warning). CTAs disabled until a message
  is typed AND the room info has loaded.

ModToolsUserModActionView
  Numbered 3-step form (CFH topic → sanction → optional message).
  Live preview row showing the chosen topic + sanction as tone-coded
  pills (amber/sky/rose/orange/fuchsia/zinc by action type). Primary
  CTA = Default Sanction, success CTA = Apply Sanction, both
  disabled until the required selections are made.

ModToolsUserSendMessageView
  Recipient header with FaEnvelope and the username, autofocused
  textarea, char counter, single full-width Send button gated on
  non-empty message.

ModToolsUserRoomVisitsView
  Header strip with entry count badge, three-column grid (time / room
  name / visit button), monospace timestamps, hover row highlight,
  empty state with FaDoorOpen icon.

ModToolsUserChatlogView / ModToolsChatlogView / CfhChatlogView
  Loading state with spinner instead of returning null. Cards grow to
  min-w-[460px] max-w-[520px] max-h-[500px] for usable chatlog area.

ChatlogView
  Replace Bootstrap-ish striped table with a CSS grid (60px / 120px /
  1fr). Room-info separator rendered as a sky card with Visit/Tools
  pill buttons. Per-row hover + even-row tint; highlighted rows
  (hasHighlighting) get an amber wash. Username is a button opening
  user info via existing link event. Empty state with FaCommentDots.

ModToolsTicketsView
  Tabs get icons (FaListUl / FaUserCheck / FaCheckSquare) and inline
  count badges (amber/sky/zinc) so the moderator sees the queue size
  at a glance. ticket bucket filtering memoized off the tickets array.

ModToolsOpenIssuesTabView / MyIssuesTabView / PickedIssuesTabView
  Same CSS grid table style. Category renders as a tone-coded pill
  (Open=amber, Mine=sky, All picked=zinc). Action buttons get icons
  (FaHandPointer Pick, FaTools Handle, FaSignOutAlt Release). Empty
  state with FaInbox.

ModToolsIssueInfoView
  Card header with category + topic pills. Details rendered as a dl
  grid instead of a striped table. Caller / Reported names as inline
  link buttons with external-link icon. Chatlog toggle is full-width
  secondary. Resolution buttons in a 3-col grid with intent colours
  (success=Resolved, dark=Useless, danger=Abusive) + a separate
  Release-to-queue button on its own row so it isn't confused with
  the resolutions.

No behaviour changes — all composers, message events, parent state
hookups, and sanction validation paths are unchanged. This is purely
a presentation pass. typecheck + vitest 214/214 + lint:hooks all
clean.
2026-05-20 21:41:52 +02:00
simoleo89 7ade398610 refactor(mod-tools): redesign ModToolsUserView template
Replace the flat striped table with a structured layout that surfaces
the moderation signal at a glance:

Identity header
  Username + ID + classification, presence pill (In room / Online /
  Offline) with colour coding (emerald / sky / zinc) and a matching
  dot, plus a manual refresh button. The pill source-of-truth is
  useRoomUserListSnapshot for the "in room" case (reactive) falling
  back to userInfo.online — tooltip discloses which path produced
  the value.

Stat strip
  Four counter cards in a single row — CFH, Cautions, Bans, Trade
  locks — tinted warn (amber) or danger (rose) when value > 0, neutral
  (zinc) when zero. Big tabular-nums numbers so the moderator sees a
  problem account immediately without parsing rows.

Sectioned body
  Account / Activity / Sanctions / Trading as labelled dl groups
  (grid-cols-[auto_1fr]) replacing the 14-row striped table. Missing
  values render as a dim em-dash instead of an empty cell.

Action bar
  2×2 button grid with react-icons/fa glyphs (FaCommentDots,
  FaEnvelope, FaDoorOpen, FaGavel). Mod Action keeps variant="danger"
  so the destructive action stands out from the three info actions
  (variant="secondary").

No behaviour changes — the same composer / event listeners /
sub-views are wired up; this is a presentation rewrite. Card grows
to min-w-[420px] max-w-[480px] to fit the new layout without
horizontal scroll on mod laptops.
2026-05-20 21:41:52 +02:00
simoleo89 ef313adcfa feat(mod-tools): reactive ModToolsUserView (online dot + refresh on sanction)
ModToolsUserView used a one-shot ModeratorUserInfoData snapshot taken at
panel-open time. Two consequences:

- The online/offline icon (rendered next to userName) was frozen on the
  value at open. If the target user joined/left while the panel stayed
  open, the icon kept lying.
- After the moderator applied a sanction via ModToolsUserModActionView
  the user info window stayed open with stale cfhCount / banCount /
  cautionCount / lastSanctionTime; you had to close and reopen to see
  the bump.

Fix shape mirrors the ModToolsView selected-user dot from yesterday:

- Read useRoomUserListSnapshot in the component (outside any useBetween
  scope — useSyncExternalStore constraint). If the target user is in
  the current room they're online; fall back to userInfo.online
  otherwise. Tooltip surfaces which path produced the value.
- Subscribe to ModeratorActionResultMessageEvent (parser carries
  userId + success). On a successful action targeting THIS userId,
  re-send GetModeratorUserInfoMessageComposer so the table re-fetches.
2026-05-20 21:41:51 +02:00
simoleo89 49dfb43c2a test(catalog): include getNodesByOfferId in useCatalogActions contract
Upstream Buy/Search fix added getNodesByOfferId to the
useCatalogActions filter but didn't refresh the actions-shape contract
test in useCatalog.filters.test.tsx. Add the key so the test reflects
the current public surface.
2026-05-20 21:41:21 +02:00
simoleo89 f4d17ece16 fix(draggable-window): pass null to useRef<HTMLDivElement> for TS6
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.
2026-05-20 21:41:21 +02:00
duckietm 3880e3441f 🆙 Put back Room Templates in UI Login 2026-05-20 20:54:07 +02:00
duckietm a08c002c53 🆙 Fix backgrounds after the update 2026-05-20 14:56:35 +02:00
duckietm 0d935a15c1 🆙 Update for the render-config 2026-05-20 13:50:27 +02:00
duckietm 9c7f630d16 🆙 Nitro V3.5.0 2026-05-20 12:57:10 +02:00
duckietm 03795f975d 🆙 Fix Buy when search 2026-05-20 12:00:41 +02:00
duckietm 6d5cf65947 🆙 Small fix for Turnstile 2026-05-20 11:36:00 +02:00
DuckieTM 0b010eac7e Merge pull request #137 from simoleo89/feat/react19-modernization
feat(client): React 19 modernization + permission-driven UI gating
2026-05-20 10:46:48 +02:00
DuckieTM 03bebe4ece Merge branch 'Dev' into feat/react19-modernization 2026-05-20 10:42:34 +02:00
DuckieTM 2ae00ff969 Merge pull request #136 from Lorenzune/merge-duckie-main-2026-05-06
Add badge leaderboard UI and rarity styling
2026-05-20 07:59:15 +02:00
simoleo89 442db09b97 docs(claude): record AvatarInfo rights + ModTools snapshot adoption
Extends the "Adopted" snapshot pilot-sites table with ModToolsView
(useRoomUserListSnapshot driving the selected-user presence dot) and
adds a new row documenting the companion pattern: event-driven local
state for cases where there's no manager-snapshot to read from yet —
the AvatarInfoWidgetAvatarView Give/Remove Rights flow uses local
useState + useMessageEvent(FlatControllerAdded/Removed) + optimistic
bump. Same reactive shape as useIsUserIgnored but sourced from the
renderer event bus instead of a snapshot getter, so the next contributor
knows when to reach for which pattern.
2026-05-19 22:17:46 +02:00
simoleo89 5c3589c29e feat(mod-tools): reactive box + bug fixes in useModTools
ModToolsView
- Subscribe to useRoomUserListSnapshot so the selected user's
  "still in room" state is reactive — green dot when the user is
  in the current room, gray dot when they've left. Previously the
  selection was a static capture at click time.
- Add an inline X to clear the selected-user slot without having
  to click a different avatar.
- Report Tool button shows a count badge for OPEN tickets
  (IssueMessageData.STATE_OPEN) so a new ticket arriving while
  the panel is open is visible immediately. Caps display at 99+.
- Tooltip on the room-bound buttons explains why they're disabled
  ("Enter a room first") instead of showing a silent disabled state.
- Buttons grow their labels with `flex-grow text-start` so the
  trailing dot / badge / clear-X sits flush right.

useModTools
- Fix splice(index) → splice(index, 1) in close{Room,RoomChatlog,
  UserInfo,UserChatlog} — the omitted second argument was
  silently deleting EVERY subsequent open panel, not just the one
  being closed. Visible whenever a moderator had two or more panels
  of the same kind open.
- Fix toggleUserChatlog reading from openRoomChatlogs instead of
  openUserChatlogs — copy-paste typo made the toggle inconsistent
  with the underlying state.
2026-05-19 22:12:19 +02:00
simoleo89 888a6a3255 feat(avatar-info): make Give/Remove Rights instantly reactive
Replaced the cached `avatarInfo.targetRoomControllerLevel` derivation
with a local `controllerLevel` state that:
- starts from the popup-open snapshot
- listens to FlatControllerAddedEvent / FlatControllerRemovedEvent
  filtered by avatarInfo.webID
- is optimistically bumped on `give_rights` / `remove_rights` clicks
  so the moderate submenu flips immediately without waiting for the
  server roundtrip

Same shape as the recent useIsUserIgnored migration: the popup now
auto-flips the button without forcing the user to close+reopen it.
2026-05-19 21:40:11 +02:00
simoleo89 73b9f9319d Merge remote-tracking branch 'origin/Dev' into feat/react19-modernization
# Conflicts:
#	src/components/backgrounds/BackgroundsView.tsx
#	src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx
2026-05-19 20:40:07 +02:00
simoleo89 989b132c6a fix(hooks): useHasPermission must distinguish ALLOWED from ROOM_OWNER
The permission map shipped over the wire carries both
PermissionSetting.ALLOWED (value 1) and PermissionSetting.ROOM_OWNER
(value 2). Server-side, `Habbo.hasPermission(key)` calls
`Rank.hasPermission(key, isRoomOwner=false)`, whose implementation
at Rank.java:120 is:

  setting == ALLOWED || (setting == ROOM_OWNER && isRoomOwner)

So a permission whose rank value is ROOM_OWNER is only granted when
the caller is the active room owner — Habbo.hasPermission(key) with
the default `false` therefore returns false for ROOM_OWNER entries.

The previous useHasPermission implementation (`> 0`) treated
ROOM_OWNER as unconditionally true, which would let a UI gate light
up even when the server would refuse the action. Real example from
the default seed: `acc_closedice_room` is ROOM_OWNER for rank_1..6
and ALLOWED only for rank_7 — under `> 0` the predicate was true for
every rank, diverging from the server behaviour.

Tighten useHasPermission to `=== 1` (ALLOWED only). For the genuine
"this is a ROOM_OWNER permission, combine with room session"
scenarios, code reaches for usePermissionValue(key) and checks
`=== 2 && roomSession.isRoomOwner` explicitly.

None of the 11 migrated consumers are affected by the tightening:
the keys they use (acc_supporttool / acc_anyroomowner /
acc_catalogfurni / acc_calendar_force / acc_staff_pick /
acc_ambassador) are all ALLOWED-only in the default seed.

Test refresh:
- useHasPermission('acc_supporttool') (value 1) stays true.
- useHasPermission('acc_anyroomowner') with value 2 in the mock
  flips from true to false — the new contract.
- Other cases unchanged.

Verification: yarn typecheck clean, yarn lint:hooks clean, yarn test
214/214.
2026-05-19 19:45:19 +02:00