Files
Nitro-V3/docs/superpowers/plans/2026-06-06-furni-editor-furnidata-client.md
T

241 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Furni editor — furnidata editing UI + typography refresh (Client/Renderer) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Steps use checkbox (`- [ ]`).
**Goal:** Expose the server-side furnidata name/description editing (Plan A, already on Arcturus `main`) in the React furni editor: make Classname/Public Name read-only, add an editable **Furnidata** section (Display Name + Description) with diff-confirm + revert, search by furnidata name, and refresh the editor's typography/colors to the theme tokens.
**Architecture:** Renderer (`Nitro_Render_V3`) gains 2 outgoing composers matching the server's incoming headers (update **10046**, revert **10048**); the success result reuses the existing `FurniEditorResult` (10044) and live propagation reuses the merged `FurnitureDataReload` (10047). Client (`Nitro-V3`) adds hook actions + UI. A small server tweak lets search match furnidata display names.
**Tech Stack:** React 19 + Vite + TailwindCSS 4 (theme tokens in `tailwind.config.js`), TS, Vitest (client); TS/PixiJS (renderer); Java/Maven (server tweak). Server feature already built (Plan A).
**Companion:** spec `Arcturus-Morningstar-Extended/docs/superpowers/specs/2026-06-06-furni-editor-furnidata-names-design.md`; server plan `…/plans/2026-06-06-furni-editor-furnidata-names-server.md`. Exploration of the client (exact file:line) is in this session's history — follow the cited patterns.
**Server header contract (already on Arcturus main):** incoming `FurniEditorUpdateFurnidataEvent = 10046` reads `int itemId` + `String` (JSON `{name,description}`); incoming `FurniEditorRevertFurnidataEvent = 10048` reads `int itemId`; both respond with `FurniEditorResultComposer` (10044) and broadcast `FurnitureDataReloadComposer` (10047).
---
## Task 1 (renderer): outgoing composers + headers
**Files (in `E:\Users\simol\Desktop\DEV\Nitro_Render_V3\packages\communication\src\messages`):**
- Modify: `outgoing/OutgoingHeader.ts` (after `FURNI_EDITOR_DELETE = 10045`, ~line 505)
- Create: `outgoing/furnieditor/FurniEditorUpdateFurnidataComposer.ts`
- Create: `outgoing/furnieditor/FurniEditorRevertFurnidataComposer.ts`
- Modify: the furnieditor `index.ts` barrel (same folder as the existing furni-editor composers)
- [ ] **Step 1: Add headers** in `OutgoingHeader.ts`:
```ts
public static readonly FURNI_EDITOR_UPDATE_FURNIDATA = 10046;
public static readonly FURNI_EDITOR_REVERT_FURNIDATA = 10048;
```
(Match the real declaration style in that file — `public static readonly NAME: number = id;` or the enum/const pattern actually used. Verify 10046/10048 are unused in OutgoingHeader.)
- [ ] **Step 2: Create `FurniEditorUpdateFurnidataComposer.ts`** (mirror the existing `FurniEditorUpdateComposer` in the same folder):
```ts
import { IMessageComposer } from '../../../../api';
import { OutgoingHeader } from '../OutgoingHeader';
export class FurniEditorUpdateFurnidataComposer implements IMessageComposer<ConstructorParameters<typeof FurniEditorUpdateFurnidataComposer>>
{
private _data: ConstructorParameters<typeof FurniEditorUpdateFurnidataComposer>;
constructor(itemId: number, jsonFields: string)
{
this._data = [ itemId, jsonFields ];
}
public getMessageArray() { return this._data; }
public dispose() { this._data = null; }
public getHeader() { return OutgoingHeader.FURNI_EDITOR_UPDATE_FURNIDATA; }
}
```
**Before writing, open the real `FurniEditorUpdateComposer.ts`** and copy its EXACT structure/imports (the `IMessageComposer` import path + the `getMessageArray/getHeader/dispose` shape may differ from the above; match it verbatim, only changing the header constant and that the payload is `[itemId, jsonFields]`).
- [ ] **Step 3: Create `FurniEditorRevertFurnidataComposer.ts`** — same pattern, constructor `(itemId: number)`, payload `[ itemId ]`, header `FURNI_EDITOR_REVERT_FURNIDATA`.
- [ ] **Step 4: Export both** from the furnieditor composers `index.ts` barrel (add the two `export * from './FurniEditor...Composer';` lines next to the existing furni-editor composer exports).
- [ ] **Step 5: Build**`cd E:\Users\simol\Desktop\DEV\Nitro_Render_V3 && yarn compile:fast` (or the real compile script in package.json). Expected: clean, no TS errors.
- [ ] **Step 6: Commit** (renderer repo):
```
git -C "E:/Users/simol/Desktop/DEV/Nitro_Render_V3" add packages/communication/src/messages/outgoing/OutgoingHeader.ts packages/communication/src/messages/outgoing/furnieditor/
git -C "E:/Users/simol/Desktop/DEV/Nitro_Render_V3" commit -m "feat(furnieditor): outgoing composers for furnidata update (10046) + revert (10048)"
```
NO `Co-Authored-By` trailer.
---
## Task 2 (client): hook actions
**Files:** Modify `E:\Users\simol\Desktop\DEV\Nitro-V3\src\hooks\furni-editor\useFurniEditor.ts`
- [ ] **Step 1: Parse furnidata name/desc into state.** Where the detail handler parses `furniDataJson` into `furniDataEntry` (lines ~140152), also derive convenience strings. The `furniDataEntry` is `Record<string,unknown>` with `name`/`description` keys. No new state needed — the EditView will read `furniDataEntry?.name`/`furniDataEntry?.description`. (No change required here if the EditView reads `furniDataEntry`; otherwise expose `furniDataName`/`furniDataDescription` strings. Choose the minimal path — prefer reading `furniDataEntry` directly in the view.)
- [ ] **Step 2: Add actions.** Mirror `updateItem` (lines ~233239). Add inside the hook body and to the return object (lines ~254259):
```ts
const updateFurnidata = useCallback((id: number, name: string, description: string) =>
{
pendingActionRef.current = { type: 'update', id };
setLoading(true);
SendMessageComposer(new FurniEditorUpdateFurnidataComposer(id, JSON.stringify({ name, description })));
}, []);
const revertFurnidata = useCallback((id: number) =>
{
pendingActionRef.current = { type: 'update', id };
setLoading(true);
SendMessageComposer(new FurniEditorRevertFurnidataComposer(id));
}, []);
```
Use the REAL send-composer helper this hook already uses (the exploration shows `updateItem` sends `new FurniEditorUpdateComposer(...)` — copy its exact send mechanism, whether `SendMessageComposer(...)` or a local `send`). Import the two new composers from `@nitrots/nitro-renderer`. Reusing `pendingActionRef.type='update'` makes the existing `FurniEditorResultEvent` success handler (lines ~162210) auto-reload the detail — which is what we want after a furnidata write.
- [ ] **Step 3: Export** `updateFurnidata`, `revertFurnidata` in the hook's return object.
- [ ] **Step 4: Typecheck**`cd E:\Users\simol\Desktop\DEV\Nitro-V3 && yarn typecheck`. Expected: no new errors (pre-existing renderer-SDK TS2307 in a sandbox without the renderer are acceptable, but here the renderer IS present so it should be clean for these files).
- [ ] **Step 5: Commit:**
```
git -C "E:/Users/simol/Desktop/DEV/Nitro-V3" add src/hooks/furni-editor/useFurniEditor.ts
git -C "E:/Users/simol/Desktop/DEV/Nitro-V3" commit -m "feat(furni-editor): updateFurnidata/revertFurnidata hook actions"
```
NO `Co-Authored-By`.
---
## Task 3 (client): EditView — read-only classname/public_name + editable Furnidata section + props
**Files:** Modify `src\components\furni-editor\views\FurniEditorEditView.tsx` and `src\components\furni-editor\FurniEditorView.tsx`.
- [ ] **Step 1: Thread props.** In `FurniEditorEditViewProps` add `onUpdateFurnidata: (id: number, name: string, description: string) => void;` and `onRevertFurnidata: (id: number) => void;`. In `FurniEditorView.tsx` (where `<FurniEditorEditView ... onUpdate=... onDelete=... />` is rendered, ~lines 149158), pass `onUpdateFurnidata={ updateFurnidata }` and `onRevertFurnidata={ revertFurnidata }` (destructure them from `useFurniEditor()`).
- [ ] **Step 2: Make Classname + Public Name read-only.** In the Basic Info section (lines ~232256): replace the **Item Name** `<input>` with a read-only display, relabel to **"Classname"**, and render the value in monospace on a muted background (see Task 4 classes). Same for **Public Name** (label it "Public Name (DB fallback)"). Use a shared `readonlyClass` (Task 4). Keep `form.itemName`/`form.publicName` in state (so `updateItem` still sends unchanged values harmlessly) but do NOT let them be edited. Example:
```tsx
<div>
<label className={ labelClass }>Classname</label>
<div className={ readonlyClass }>{ form.itemName }</div>
</div>
<div>
<label className={ labelClass }>Public Name (DB fallback)</label>
<div className={ readonlyClass }>{ form.publicName }</div>
</div>
```
- [ ] **Step 3: New editable Furnidata section.** Replace the read-only `FurniData.json` section (lines ~323334) with:
```tsx
<Section title="Furnidata (display name)" defaultOpen={ true }>
<Column gap={ 1 }>
<div>
<label className={ labelClass }>Display Name</label>
<input className={ inputClass() } value={ furniName } onChange={ e => setFurniName(e.target.value) } maxLength={ 256 } />
</div>
<div>
<label className={ labelClass }>Description</label>
<textarea className={ inputClass() } rows={ 3 } value={ furniDescription } onChange={ e => setFurniDescription(e.target.value) } maxLength={ 256 } />
</div>
{ (furniName !== (String(furniDataEntry?.name ?? '')) || furniDescription !== (String(furniDataEntry?.description ?? ''))) &&
<span className="text-[10px] text-orange-500 font-bold">Unsaved furnidata changes</span> }
<Flex gap={ 1 }>
<Button variant="success" disabled={ loading } onClick={ () => setConfirmFurnidata(true) }>Save name/desc</Button>
<Button variant="secondary" disabled={ loading } onClick={ () => onRevertFurnidata(item.id) }>Revert</Button>
</Flex>
</Column>
</Section>
```
Add local state near the other state (lines ~7191): `const [furniName, setFurniName] = useState('');` `const [furniDescription, setFurniDescription] = useState('');` `const [confirmFurnidata, setConfirmFurnidata] = useState(false);` and seed `furniName`/`furniDescription` from `furniDataEntry?.name`/`?.description` (falling back to `item.publicName`/`item.description`) in the same `useEffect` that syncs `form` (lines ~95122), re-running when `furniDataEntry` changes.
- [ ] **Step 4: Diff + confirm modal** (mirrors the existing Delete-confirm modal, lines ~353368). When `confirmFurnidata`, show a small modal listing old → new:
```tsx
{ confirmFurnidata &&
<div className="...overlay classes copied from the delete modal...">
<div className="...panel classes...">
<Text bold>Apply furnidata change to ALL clients?</Text>
<div className="text-xs"><b>Name:</b> { String(furniDataEntry?.name ?? '') } { furniName }</div>
<div className="text-xs"><b>Desc:</b> { String(furniDataEntry?.description ?? '') } { furniDescription }</div>
<Flex gap={ 1 }>
<Button variant="success" onClick={ () => { onUpdateFurnidata(item.id, furniName, furniDescription); setConfirmFurnidata(false); } }>Confirm</Button>
<Button variant="secondary" onClick={ () => setConfirmFurnidata(false) }>Cancel</Button>
</Flex>
</div>
</div> }
```
Copy the exact overlay/panel Tailwind classes from the existing delete-confirmation modal so it looks identical.
- [ ] **Step 5: Typecheck + manual render.** `cd Nitro-V3 && yarn typecheck` (clean). With `yarn start` running, open the editor on a furni: Classname/Public Name show read-only (monospace, muted), the Furnidata section shows the real display name from furnidata, editing + Save shows the confirm modal, Confirm sends the composer.
- [ ] **Step 6: Commit:**
```
git -C "E:/Users/simol/Desktop/DEV/Nitro-V3" add src/components/furni-editor/views/FurniEditorEditView.tsx src/components/furni-editor/FurniEditorView.tsx
git -C "E:/Users/simol/Desktop/DEV/Nitro-V3" commit -m "feat(furni-editor): editable furnidata name/desc section + read-only classname/public_name + diff-confirm + revert"
```
NO `Co-Authored-By`.
---
## Task 4 (client): typography / color refresh (theme tokens)
The chosen direction: replace scattered hardcoded hex with theme tokens, restyle labels for hierarchy, bump input font + focus ring, and render read-only/technical values in monospace on a muted bg.
**Files:** `FurniEditorEditView.tsx` (the in-file helper class strings).
- [ ] **Step 1: Update the helper class strings** near lines ~209211:
```ts
// inputs: bump xs→sm, add focus ring using the theme primary token
const inputClass = (field?: string) =>
`w-full px-2 py-1 text-sm leading-normal rounded-sm border border-[#bbb] focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/40 min-h-[calc(1.5em+0.5rem+2px)]${ field && errors[field] ? ' border-red-500 bg-red-50' : '' }`;
// labels: stronger hierarchy — uppercase, tracked, secondary token
const labelClass = 'text-[10px] font-bold text-secondary uppercase tracking-wider mb-0.5 flex items-center gap-0.5';
// read-only / technical values: monospace on muted bg, clearly "locked"
const readonlyClass = 'w-full px-2 py-1 text-sm font-mono rounded-sm border border-[#ddd] bg-[#f2f2eb] text-[#555] select-all';
```
(Match the real existing `inputClass` signature/`errors` variable name; only change the class string + add `readonlyClass`. `text-secondary`/`focus:ring-primary` resolve via `tailwind.config.js` tokens `secondary=#185D79`, `primary=#1E7295`.)
- [ ] **Step 2: Section titles** — they already use `<Text small bold variant="primary">` (theme `#1E7295`). Leave as-is (already token-aligned) OR, if a stronger separator is wanted, add `border-b border-[#e3e3da] pb-1` to the section header row. Keep minimal.
- [ ] **Step 3: Apply `font-mono` to technical inline values** already covered by `readonlyClass` (Classname/Public Name from Task 3). Also render the header `ID: {id} | Sprite: {spriteId}` (line ~223) in `font-mono text-[#555]` for consistency.
- [ ] **Step 4: Typecheck + visual check**`yarn typecheck` clean; with `yarn start`, confirm labels are now uppercase secondary-tinted, inputs larger with a focus ring, classname/public-name monospace on muted bg.
- [ ] **Step 5: Commit:**
```
git -C "E:/Users/simol/Desktop/DEV/Nitro-V3" add src/components/furni-editor/views/FurniEditorEditView.tsx
git -C "E:/Users/simol/Desktop/DEV/Nitro-V3" commit -m "style(furni-editor): theme-token typography refresh (labels, inputs focus ring, mono read-only)"
```
NO `Co-Authored-By`.
---
## Task 5 (server): search also matches furnidata display name
Lets the Search box find furni by their real (furnidata) name, not just `item_name`/`public_name`.
**Files (Arcturus):** Modify `Emulator/.../messages/incoming/furnieditor/FurniEditorSearchEvent.java`.
- [ ] **Step 1:** Read the existing `FurniEditorSearchEvent.handle()` (it queries `items_base` by `item_name`/`public_name` LIKE the query). After collecting the DB matches, also scan the in-memory furnidata index for display-name matches and union their item ids:
- Get the provider: `FurnitureTextProvider p = Emulator.getGameEnvironment().getFurnitureTextProvider();`
- The provider currently exposes `getName(classname)` but not a name→classnames search. Add a method to `FurnitureTextProvider`: `public java.util.List<String> findClassnamesByName(String q)` that lowercases `q` and returns classnames whose indexed name contains it (iterate the `index` map values; cap results e.g. 200). Then map those classnames → `items_base.id` via a `SELECT id FROM items_base WHERE item_name IN (...)` and merge with the existing result rows (dedupe by id, keep the existing result row shape).
- Keep it bounded (cap added rows) and behind the same `ACC_CATALOGFURNI` gate.
- [ ] **Step 2: Build** `cd Emulator && mvn -q compile` → SUCCESS.
- [ ] **Step 3: Commit** (Arcturus repo, `main`):
```
git -C "E:/Users/simol/Desktop/DEV/Arcturus-Morningstar-Extended" add Emulator/src/main/java/com/eu/habbo/messages/incoming/furnieditor/FurniEditorSearchEvent.java Emulator/src/main/java/com/eu/habbo/habbohotel/items/FurnitureTextProvider.java
git -C "E:/Users/simol/Desktop/DEV/Arcturus-Morningstar-Extended" commit -m "feat(furnieditor): search also matches furnidata display names"
```
NO `Co-Authored-By`. (This task is optional/last — if it balloons, ship Tasks 14 first.)
---
## Task 6: final build/verify
- [ ] Renderer: `cd Nitro_Render_V3 && yarn compile:fast` clean.
- [ ] Client: `cd Nitro-V3 && yarn typecheck && yarn test --run` green (pre-existing unrelated failures noted, not introduced).
- [ ] Server (if Task 5 done): `cd Emulator && mvn -q package -DskipTests=false` SUCCESS; deploy jar to `Latest_Compiled_Version` + restart for manual end-to-end.
- [ ] Manual acceptance: edit a furni's display name in the editor → confirm modal → live update in catalog/inventory/infostand without refresh; Revert restores; Classname/Public Name read-only; search by display name finds it; audit row written.
## Self-review
- Spec §5 coverage: editable furnidata name/desc (T3), read-only classname/public_name (T3), diff+confirm (T3), revert (T2/T3), live-preview/dirty (T3), search-by-name (T5), typography (T4), composers/headers matching server (T1). ✓
- Header consistency: client outgoing 10046/10048 == server incoming 10046/10048; result via 10044; live via 10047. ✓
- Types: `updateFurnidata(id,name,description)`, `revertFurnidata(id)`, `onUpdateFurnidata`/`onRevertFurnidata` props, `readonlyClass` — consistent across T2/T3/T4.
- Open: confirm the real renderer composer import path + send helper (T1/T2) and the real `inputClass`/`errors` names (T4) by reading the files first.