Claude/pixi react UI theme uj63lt - #31
Open
billsonnn wants to merge 149 commits into
Open
Conversation
Invert the render bootstrap so the room and the Pixi-rendered UI share a single Application/renderer/GPU context (no duplicate texture uploads): PixiApplicationRoot now owns the one renderer, adopted via a new SetRenderer() in nitro-renderer, and mounts the room's existing stage as its bottom layer. RoomCanvas and Nitro's bootstrap sequencing are updated accordingly. Add a theme-pixi package mirroring the DOM theme system's public shape (Box as the flex/positioning primitive via @pixi/layout, Border/Button with native Pixi tinting and nine-slice sprites, icon/text-style/ interaction-state hooks), reusing the existing variant-cascade system verbatim. Port PurseView to PurseViewPixi as a working proof of concept. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
@pixi/layout only positions/sizes an object if `.layout` is explicitly
set on it (confirmed by reading Layout.getRoot()/LayoutSystem.mjs in
node_modules): an object with no `.layout` is invisible to the yoga
tree and stays at Pixi's raw default position. NitroIcon,
NitroCurrencyIcon, and Button's inline text never set `layout` at all,
so icons and text ignored gap/justifyContent/sizing entirely. Both
components now default `layout` to `{}` so leaf content always joins
the tree (still overridable via a new `layout` prop).
Also, app.stage - the root of the yoga tree for all Pixi UI - never
had its own `.layout`, so a top-level `position: 'absolute'` child
(e.g. PurseViewPixi's outer Border) had no sized, positioned ancestor
to resolve `top`/`right`/etc against and landed at (0, 0) regardless.
PixiApplicationRoot now gives app.stage a `.layout` matching the
renderer's screen size (position: 'relative', so it's a valid
containing block), kept in sync on the renderer's 'resize' event.
Verified with a headless-browser pixel readback of the actual canvas
(toDataURL, since preserveDrawingBuffer is normally false): the Purse
widget now renders in the correct screen position with icons and text
correctly placed by the flex layout.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Frame + its supporting pieces (Header, ContentArea, Scaler, CloseButton) port theme/Frame.tsx and its siblings for variants '0' and '3' (the two actually used across the DOM views: FriendList/dialogs use '0', Catalog/ Inventory use '3'). Reuses the DOM package's variant-cascade system verbatim, same as Border/Button. Dragging and resizing are Pixi ports of hooks/ui/useFrameDrag.ts and useFrameResize.ts: - Pixi's FederatedPointerEvent has no setPointerCapture (confirmed absent from pixi.js's own types), so both hooks drive their gesture off window-level pointermove/pointerup listeners - the same technique the DOM drag hook already uses, just applied to resize too. - The drag offset is applied as the frame's own `.x`/`.y`: @pixi/layout ADDS the yoga-computed position on top of a container's existing `.position` (see updateLocalTransformWithLayout in node_modules/@pixi/layout), the same layering CSS `transform: translate()` gives the DOM version, so no fighting with the layout system for position during a drag. - Frame's width/height precedence now matches the DOM version: an active resize (stored/dragged size) overrides the caller's own layout, the same way the DOM Frame's resize-driven inline style out-specifies its className defaults - caught by testing, the first pass had this backwards. Also fixes a real gap found while building this: nine-slice frame/header art and the closebutton/scaler sprites needed slicing a sub-region out of a shared spritesheet texture (CSS background-position in the DOM version) - added utils/useSpriteFrameTexture.ts for that, and registered TilingSprite (the header's repeating background) in pixiElements.ts. Verified end-to-end with two live Frame instances (variants 0 and 3) in a headless-browser pixel readback: correct position, size, drag-ready z-order/header, close button, and content placement, with zero console errors. Not wired into any real view yet (out of scope for this pass, matching the PurseView-proof-of-concept precedent) - Frame is ready for whichever windowed view migrates next. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
…not just Box
Reported crash: "Text instances are not yet supported" when a Frame's raw
string children (e.g. `<Frame>some text</Frame>`) reached Pixi's reconciler,
since - unlike a DOM <div> - pixiContainer has no implicit text-node concept.
Box already special-cased its own JSX-authored children, but that only
covers `<Box>{string}</Box>` directly. ContentArea and Border both forward
an arbitrary caller-supplied `children` through VariantCascadeProvider (a
passthrough Fragment/Context.Provider with no host node of its own) before
it lands inside their own Box - so a raw string never appears as a
JSX-authored child of that Box; it only surfaces once React fully resolves
the passthrough wrapper at render time, past the point Box's own
Children.map can see it.
Extracted the wrapping helper to theme-pixi/utils/wrapTextChildren.tsx and
apply it directly in ContentArea and Border, at the point where each still
owns the caller's children, matching the exact Frame -> ContentArea ->
raw-string stack trace from the report.
…der parity
Batch 1+2a of the theme parity pass: cross-cutting rendering primitives that
every remaining component builds on, plus Border.tsx expanded from 2 to all
19 DOM variants as the reference implementation for the new patterns.
- utils/Layer.tsx: NineSliceLayer/SpriteLayer/TileLayer wrap the existing
fill-absolute + eventMode="none" background-layer idiom already used ad
hoc in Frame/Header; CompositeLayer renders the multi-piece DOM variants
(Border 101/102/103/106/107, and later Button/ContainerButton/Frame) as
positioned pixiSprites via the same absolute+inset stretch mechanism
@pixi/layout already uses everywhere else in this package; BlendOverlay
reproduces Border's `blend` white-wash tint as a live masked Graphics
overlay (same masked-Sprite technique as theme/utils/pixiTint.ts's
tintImage(), just without its canvas-bake round trip, which buys nothing
in Pixi).
- utils/textStyles.ts: expanded from 2 to all 18 real text-style-* keys in
theme/utilities.css (font family/size, plus the 3 utilities that bake in
their own fill color). The two Frame-referenced classnames that don't
actually exist in utilities.css (text-style-id-frame-title,
text-style-il-frame-modal-title - dead Tailwind utilities in the DOM
source) are intentionally not ported; those Frame variants get no special
text style, matching what DOM actually (fails to) render.
- Border.tsx: all 19 variants. Plain nine-slice variants (0-10, 100, 104,
105, 108, 200) use NineSliceLayer; the 5 composite variants (101, 102,
103, 106, 107) use CompositeLayer with piece geometry transcribed from
the DOM's background-position/background-size values, including two
cross-piece asset-reuse quirks (101 and 107 both reuse their center-left
texture for center-right) preserved as-is to match shipped art. 104/105
get their overlay via NineSliceLayer/CompositeLayer; `blend` support
added via BlendOverlay. Tint colors ('9', '108') match DOM exactly.
Variant '100' is the one DOM variant whose border-image-slice omits
`fill` (CSS leaves the center transparent); Pixi's NineSliceSprite has
no equivalent, so this port always fills the center - noted in a comment
rather than silently diverging.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
…ded components Batch 2 of the theme parity pass: every component previously ported with only the 1-2 variants the PurseView/Frame proof-of-concept used now covers its complete theme/ variant table, including tint colors, overlay/shine layers, and (for Frame/Button/ContainerButton-style composites) the multi-piece background composites, using the Layer.tsx/textStyles.ts primitives from the previous commit. - Border: 2 -> 19 variants (composite variants 101/102/103/106/107 via CompositeLayer, overlay variants 104/105, blend support, full tint table). - Button: 1 -> 13 variants (per-state nine-slice geometry since 100/101 change slice width between default and hover/active; 11-piece composite overlay for 100/101 with its pressed-state asset+spacer swap, 2-piece curve overlay for 102/103; per-variant text style/color; '1's pressed state correctly reuses `border-3-default-src`, a real DOM cross-asset quirk, not a bug). - CloseButton: 2 -> 7 variants, including '1'/'2's static (no real hover, active===default) art and '100's separate fixed-size texture. - Scaler: 2 -> 6 variants + the per-variant offset-positioning table (only '0'/'3' actually position bottom-right in DOM - '1'/'2'/'4' define no offset class at all, preserved as-is) + shine overlay for 0/1/2. - Header: 2 -> 8 variants (tile/stretch/nine-slice backgrounds, shine overlay for 0/1/2, the caption/close-button tint-color background chip, per-variant text style/color/font-aa drop shadow, and the real margin- vs-padding inconsistency between variants 0/2 and 1 preserved via Yoga margin vs. padding rather than flattened to one behavior). - Frame: 2 -> 9 variants, the universal drop-shadow (all DOM variants get `drop-shadow-[2.83px_2.83px_4px_rgba(0,0,0,0.349)]`) via a Pixi DropShadowFilter, shine overlays, and variant '100' reusing Border's own `border-101-*` composite asset set wholesale (a real DOM asset-reuse quirk). - ContentArea: fixed a real bug (not just an unported variant) - variant was accepted as a prop and cascade-resolved but had zero effect on the render; now applies '0's bottom padding and '3's `position: relative`. - NitroIcon/useIconTexture: 4 -> 264 icon keys (184 discrete PNGs from icons.css, 80 spritesheet frames from habbo-icons.css), full parity with both DOM icon registries. - NitroCurrencyIcon: added the DOM `Image.tsx`-equivalent loading/error fallback (a `loading.icon.url` texture shown whenever the real currency texture hasn't resolved, matching DOM's "stays visible forever on error" behavior since Pixi has no distinct loading-vs-errored texture state). - Added `pixi-filters` (already a transitive/hoisted dependency via nitro-renderer) as a direct nitro-react dependency for DropShadowFilter. Found and fixed two real bugs during verification (headless Playwright, multiple simultaneous Frame instances each containing Button+Border): 1. FRAME_3_SHINE's nine-slice args were passed in the wrong order (leftWidth/topHeight swapped vs. the base layer), corrupting variants 3/4/7's shine overlay geometry. 2. The new DropShadowFilter defaulted to resolution 1 while the app renders at `GetPixelRatio()` - the mismatch caused a stale-texture ghosting artifact (a fragment of one Frame's content bleeding into another's) whenever 2+ filtered Frames rendered in the same tick, only reproducible with multiple simultaneous Frames. Fixed by passing the app's own resolution to the filter explicitly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
…eckBox, RadioButton, Bubble family Batch 3 of the theme parity pass: the remaining "simple" theme/ primitives that weren't part of the original PurseView/Frame proof-of-concept, each following the variant-table + Layer.tsx recipe established in the previous two commits. - ButtonThick: 6 variants (0-2 stretch sprites, 3-6 nine-slice), per- variant text style/color, no overlay (DOM's overlay config exists but every entry is empty). - ContainerButton: 14 variants. Extensive real cross-component asset reuse confirmed directly from the DOM source (not guessed): 0/1/2 reuse Button's own textures, 3/6 reuse ButtonThick's, 100-103 reuse Button's 100-103 textures AND overlay composites verbatim - extracted the shared 11-piece/2-piece composite-overlay data Button and ContainerButton both need into utils/buttonOverlayPieces.ts (Button.tsx now imports from there too, replacing its former local copy). Preserves DOM's missing `not-aria-disabled:` cursor guard (ContainerButton always shows a pointer cursor, unlike Button/ButtonThick) and variant '1's `border-3-default-src` pressed-state quirk (same as Button's own). - ButtonGroupLeft/Center/Right: 4 variants each, sharing one `createButtonGroupComponent` factory (utils/buttonGroupFactory.tsx) since the three DOM components are otherwise byte-identical apart from asset-key prefix and slice geometry. `aria-selected`/`active:` collapse into one Pixi `selected` state since DOM swaps to the same art for both. - CheckBox/RadioButton: sprite-sheet toggle components, `selected` fully caller-controlled (matching DOM's `aria-selected`-driven, non- self-toggling design). Preserves CheckBox's real DOM quirk where pressing a checked box reverts to the unchecked frame while held down, and both components' variant '2' reusing variant '0's exact sprite positions. - Bubble/BubblePointer/FramePointerDown: chat-bubble family. Bubble's pointer-direction layout (up/down/left/right) ported via `flexDirection`/`flexDirection-reverse`; BubblePointer's 4 direction configs each get their own sprite-sheet frame + per-side Yoga margin matching DOM's small negative-margin nudge into the bubble body. FramePointerDown is DOM's simplest component (one variant, no states). Verified via headless Playwright: all new components render correctly inside Frames (selected states, nine-slice chrome, sprite frames, text) with no console/page errors and no ghosting artifacts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
…t/Tooltip Batches 4-6 of the theme parity pass. Tab family (TabContext, TabContent, TabButton, TabContainerButton): a shared utils/tabButtonChrome.tsx holds the border-image variant table used by both TabButton and TabContainerButton (identical chrome, TabButton adds padding/sizing/text on top of it - mirrors the buttonGroupFactory.tsx pattern from the previous commit). TabContent's variant '3' has an asymmetric nine-slice (only the top edge sliced) plus a negative top margin tucking it under the tab bar, reproduced via Yoga's negative-margin support. `aria-selected:`/`active:` collapse into one `selected` prop throughout, consistent with every other selection-driven component ported so far. VARIANT_CASCADE_CONFIG's existing `tabContext` -> `tabButton` cascade (reused verbatim from #base/theme) means a TabButton nested under a shiny TabContext auto-renders as the shiny variant with no explicit prop. Accordion family (Accordion, AccordionItem, AccordionContext, AccordionTrigger, AccordionContent): AccordionContext is pure React Context with zero DOM dependency, so it's re-exported verbatim from #base/theme rather than forked - and Accordion.tsx reuses that same Context *object* directly (not a re-implementation), so a Pixi Accordion.Provider and the shared useAccordion/useAccordionItem hooks stay correctly wired together. AccordionContent has zero open/close animation in DOM (hard conditional mount/unmount, no transition, no Motion usage anywhere in it or its call sites) - the instant show/hide here is exact parity, not a cut corner. AccordionTrigger's `alwaysOpen` case maps to `eventMode="none"` in Pixi, which is strictly more correct than DOM's still-technically-wired-but-inert click handler (it also lets pointer events reach whatever's beneath the trigger, which DOM's div can't do). Dropmenu/DropmenuItem/Droplist/DroplistItem/Tooltip: static-skinning parity only. Confirmed via full-codebase research earlier in this pass that DOM ships zero open/close/positioning/portal logic for any of these five - Dropmenu is only ever rendered as a bare closed box with no items, Droplist/DroplistItem/Tooltip/DropmenuItem have no call sites at all - so this port matches that reality (variant/tint/overlay-arrow art only) and does not add new floating-menu behavior DOM never had. Tooltip's DOM class string (`text-[#000000] text-style-u-tool-tip`) actually renders WHITE text in the browser, since text-style-u-tool-tip's own baked-in `text-white` is the later, winning declaration in Tailwind's cascade - reproduced here via the text style's own color rather than the literal (losing) black hex, matching real DOM output over the misleading literal. Verified via headless Playwright: tab switching, accordion open/close, dropdown arrow overlays, and selection states all render correctly inside Frames with no console/page errors and no ghosting artifacts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Ports theme/'s scroll system to theme-pixi/: useScrollController (Pixi
equivalent of useScrollbarController, since Pixi has no native scroll to
drive/be driven by), ScrollViewport (masked clipping window), useHoldToRepeat,
useRowVirtualizer (a small from-scratch row virtualizer replacing
@tanstack/react-virtual, which is inherently shaped around a DOM scroll
element Pixi doesn't have), the full ScrollbarVertical/Horizontal chrome
family, ScrollArea, and InfiniteGrid.
Also fixes a real, previously-shipped rendering bug found while building this:
a plain pixiSprite/pixiGraphics doesn't stretch to fill its parent via
`layout={{position:'absolute', top:0, left:0, right:0, bottom:0}}` alone in
this @pixi/layout version - it needs explicit width/height:'100%' too, since
NineSliceSprite/TilingSprite are the only leaf types that apply their computed
size directly. Fixed in Layer.tsx's shared FILL_LAYOUT (used by SpriteLayer/
BlendOverlay) and Header.tsx's TintChip.
This completes the theme-pixi package's full variant/color/overlay parity
with theme/.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
…t' into claude/pixi-react-ui-theme-uj63lt
Every component repeated the same three blocks of logic verbatim: - Variant resolution (useCascadedVariant + resolvedVariant + ownCascade lookup) - Interaction-state -> art resolution (resolveLayerState/resolveTabChromeLayer, identical in Button.tsx, ContainerButton.tsx, buttonGroupFactory.tsx and tabButtonChrome.tsx) - Background-layer kind switching (nineSlice/composite/tile/stretch), duplicated character-for-character between Border.tsx and Frame.tsx and reimplemented separately in Header.tsx Extracted three shared primitives and adopted them everywhere they apply: useResolvedVariant (utils/useResolvedVariant.ts), resolveByState + NineSliceLayerState/nineSliceLayerState (utils/useInteractionState.ts), and BackgroundLayer (utils/Layer.tsx). Applied to Border, Frame, Header, Button, ContainerButton, buttonGroupFactory (used by ButtonGroupLeft/Center/Right), tabButtonChrome (used by TabButton/TabContainerButton) - no behavior change, verified via a visual regression pass showing identical rendered output before/after. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Mechanical follow-up to the shared theme-pixi primitives consolidation: replaces the repeated cascadedVariant/resolvedVariant/ownCascade block with useResolvedVariant() in the 12 scrollbar-family components (ScrollbarVertical/ Horizontal, all slider bar/track/button sub-parts, ScrollableItemList/GridVertical). No behavior change - eslint/tsc verified clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
…t' into claude/pixi-react-ui-theme-uj63lt
Mechanical follow-up completing the shared-primitives consolidation: replaces the repeated cascadedVariant/resolvedVariant/ownCascade block with useResolvedVariant() in Bubble, BubblePointer, ButtonThick, CheckBox, CloseButton, ContentArea, Droplist, DroplistItem, Dropmenu, DropmenuItem, FramePointerDown, RadioButton, Scaler, TabContent, TabContext, Tooltip. Also opportunistically replaced three local per-state art resolvers (DroplistItem, ButtonThick, CloseButton) with the shared resolveByState utility where their state tables were an exact shape match. No behavior change - eslint/tsc verified clean, visual regression pass shows identical rendered output before/after across all touched components. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
First batch of the views/ -> views-pixi/ migration (see
/root/.claude/plans/pixi-views-migration.md for the full plan): ports
ActivityPointsView, AvatarEditorView and MessengerView, following the
Wrapper/Component/ViewPixi pattern already established by the Purse/Catalog
migrations. AvatarEditorView and MessengerView are themselves DOM stubs
("test"/"chats" placeholder content) - ported at the same stub fidelity.
Adds a shared ColorLayer primitive to theme-pixi/utils/Layer.tsx (a flat
solid-color fill, matching a DOM `bg-[#hex]` div) and switches Header.tsx's
local TintChip over to it, since views will need the same solid-fill pattern
constantly. Exposes utils/Layer.tsx from the theme-pixi/ barrel now that
views (not just theme-pixi components) need to consume it directly.
Splits MessengerComponent out of FriendListWrapper so it can move into
Nitro.tsx's Pixi tree independently - FriendListComponent still renders DOM
views and can't move until FriendList's own migration.
NotificationCenterView (theme/'s own source always returns null) and
HotelView (empty div, wrapped in DOM-only motion.js transition machinery
outside the view itself) have nothing to migrate - dropped rather than
ported as no-ops, noted in MainView.tsx.
Verified via eslint/tsc (105-error pre-existing baseline, zero new) and a
Playwright visual check of all three views rendering correctly with no
console errors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Adds useAvatarImageTexture (wraps AvatarRenderManager's already-decoded cropped avatar image in a Pixi Texture.from(), reusing the same engine call components/AvatarImage.tsx makes rather than round-tripping through a CSS background-image), a shared ToolbarMenu component behind ToolbarMeMenuPixi/ToolbarProgressionMenuPixi (structurally identical DOM sources), and ToolbarViewPixi itself (collapse toggles, me/progression popups, circular avatar icon masked via Pixi). Wires ToolbarViewPixi into Nitro.tsx's PixiApplicationRoot in place of the DOM ToolbarView. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
InventoryFurniViewPixi reuses theme-pixi's row-virtualizer/scroll- controller primitives (the same ones InfiniteGrid is built on) directly with a fixed 4-column layout, matching DOM's own hand-rolled react-virtual usage rather than InfiniteGrid's auto-column behavior. InventoryPetsView/BotsView/BadgesView (byte-identical DOM stubs) share one InventoryStubTabPixi. Wires InventoryComponent into Nitro.tsx's PixiApplicationRoot in place of the DOM InventoryView. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Adds two new shared theme-pixi primitives this migration needed for real (not stub) DOM behavior: TextInput (a faked editable text field - Pixi has no native editable text object - backing every real search/message box hit so far) and useOutsideClick (geometric hit-test port of hooks/ui/useOutsideClick.ts, used by the friend-row relationship dropdown). Extends AccordionTrigger with optional onPointerOver/onPointerOut passthrough for FriendListTab's hover-driven footer tooltip text. Ports the full FriendList tree: root view, the shared Tab wrapper (DOM's hard-edged two-color trigger gradient reproduced as two ColorLayer halves), Friends/Requests/Search tabs, group/item leaves, footers, and the two dialogs. Wires FriendListComponent to render the Pixi tree and moves FriendListWrapper's mount point into Nitro.tsx's PixiApplicationRoot. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Ports the Navigator window: root frame (collapsible left pane, dynamic frame width, top-level context tabs), quick links pane, search bar, category/room-entry results (rows and tiles modes, with the SWF's alternating-row-color and user-count-color math reused verbatim from NavigatorRoomEntryUtils.ts, which has no DOM dependency), and the create/random/promote room action buttons. Drops the DOM version's getBoundingClientRect-driven window-position telemetry sync (Frame doesn't forward a ref; not worth widening its contract for a background-only feature). Extends Dropmenu/DropmenuItem with an optional onPress - unlike every other confirmed-dead Dropmenu call site found so far, NavigatorSearchView uses it as a real interactive filter-type picker. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Ports the full catalog tree: root frame with tabs, header, search, navigation tree, item grid, product preview, purchase widgets, and the purchase confirmation dialog. Adds two new engine-integration hooks reusing existing DOM logic verbatim (both are pure engine/state logic with the DOM-specific I/O swapped out): - useFurnitureImageTexturePixi mirrors useAvatarImageTexture's own approach for components/FurnitureImage.tsx's GetRoomEngine() call. - useRoomPreviewerPixi mirrors hooks/room/useRoomPreviewer.tsx, but mounts the temp room's own `room.canvas.master` (confirmed to be a plain Pixi Container) directly into the UI's Pixi tree instead of DOM's extract-to-2D-canvas-every-tick round trip - a genuine simplification once room and UI share one renderer, not just a port. Adds a shared Image component generalizing the loading-placeholder pattern NitroCurrencyIcon.tsx already established for its own image src. Extends the AccordionTrigger circular-import port pattern to CatalogNavigationSetView/CatalogNavigationSetItemView's own recursion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Ports the object-infostand and object-menu view families to Pixi equivalents (InfostandBot/Pet/Furni/User, InfoBubbleName/Avatar/ OwnAvatar), plus RoomObjectMenuBubblePixi - a Pixi port of the imperative screen-position-tracking bubble container that mutates a Container ref's x/y/alpha/visible on every RoomRenderedEvent tick instead of DOM style properties - and RoomWidgetsPixi, which replaces the DOM createPortal root with a normal Pixi child carrying its own RoomEngineEvent-driven isReady state. The four logic-only wrapper components (RoomObjectInfostandWidget, InfostandFurni, RoomObjectMenuNameBubble, RoomObjectMenuWidget) are edited in place to point at the new Pixi views, and RoomWidgetsPixi is wired into Nitro.tsx in place of RoomContainer's DOM RoomWidgets portal. This completes the "convert all views to Pixi" migration. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Every themed background/overlay layer (NineSliceLayer, SpriteLayer, ColorLayer, CompositePieceSprite, BlendOverlay) sets eventMode="none" so its art doesn't shadow its own container's press handlers, and nothing ever gave the interactive container itself an explicit hitArea. Per Pixi's own EventBoundary (hitPruneFn/hitTestFn), a static/dynamic Container with no hitArea has no shape of its own to hit-test against, so its clickable region silently collapsed to whatever passive/interactive children happened to sit under the pointer - in practice just a text label, even though the button's rendered art fills the whole box. Box now attaches a self-tracking fallback hitArea on mount (skipped if the caller already set one) that re-reads the container's own yoga-computed size on every hit test, so it stays correct as content/ padding resize the box without an effect keeping a baked Rectangle in sync. Since every pressable primitive (Button, CloseButton, ContainerButton, Header, Frame, tabs, accordion triggers, dropmenu items, ...) is built on Box, this fixes the whole family in one place. Verified interactively: a corner click away from a button's centered text now fires onPress, same as clicking the text itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
Replaces every <pixiText style={getPixiTextStyle(key, overrides)}>
call site across theme-pixi and views-pixi with <Text textStyle={key}
textOptions={overrides}>, matching the pattern Button.tsx already
established. Left untouched: Text.tsx itself (the implementation),
utils/wrapTextChildren.tsx (the unstyled raw-string safety net for
Box's string children - Text's textStyle prop is required, and this
is deliberately unstyled), and the one fallback branch each in
CheckBox.tsx/RadioButton.tsx that renders unstyled pixiText when a
variant carries no textStyleKey at all (0/1/2, which have no label in
DOM either).
Every file's getPixiTextStyle import is removed once its only call
site becomes redundant (verified 1:1 per file first). Verified via
tsc --build --force (stable, no new errors) and a Playwright
screenshot comparison against the pre-conversion room-widgets/Button
render, pixel-identical.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
…m/nitrodevco/nitro-next into claude/pixi-react-ui-theme-uj63lt
Packs the ~250 individual border/button/scrollbar PNGs THEME_URLS points at into one combined atlas.png + Spritesheet-format manifest.json, built by a new build-theme-atlas.ts script (yarn bundle-theme-assets). Reduces the combined asset payload from 1.1MB across ~250 requests to a 24KB image + 40KB manifest across 2 requests. preloadThemeAssets() fetches and decodes the atlas once at boot, before the app renders: in Pixi it seeds AssetManager's texture cache via Pixi's own Spritesheet class (same technique AssetManager.processNitroBundle already uses for room/furniture .nitro bundles); in DOM it slices each asset's rect into a data: URL and overwrites THEME_URLS' values in place, so every existing DOM call site keeps working unchanged. Either path failing leaves assets on their existing lazy-load fallback. The bake script extrudes each packed sprite's edge pixels into its padding gutter to prevent bilinear-filtering bleed between adjacent atlas tiles, and the Pixi loader explicitly sets nearest-neighbor scaling on the atlas texture (the app's own renderer sets this as a global default, but this preload runs before that renderer exists). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
The atlas preload resolves themed textures synchronously at boot, so a scrollbar thumb's NineSliceSprite now mounts on the very first render instead of only once its texture finishes a lazy network fetch. That exposed a real Pixi bug: useScrollController's thumbSize briefly reads 0 on the first measure tick after content becomes scrollable (the track's own layout settles a tick after the viewport/content sizes that make it scrollable), and a NineSliceSprite that mounts at zero size never recovers once resized to a real one on the next tick - it used to render correctly by accident, because the lazy texture fetch usually didn't resolve until after that zero-size tick had already passed. Skip mounting the thumb while thumbSize is still 0 in both ScrollbarVertical and ScrollbarHorizontal, so it always mounts fresh at a real size instead of getting stuck invisible after a resize. Also drop themeAssetBundle.ts's Spritesheet-based shared atlas texture for Pixi in favor of a standalone per-asset Texture sliced from the decoded atlas image: TilingSprite (Header's tiled background/shine, any repeat-mode nine-slice's stretched middle) silently collapses a tile-kind texture cropped from a shared source into one flat sample instead of repeating the pattern, since its "can batch" fast path requires the texture's frame to equal its source's full size. A standalone texture per asset always satisfies that, exactly like the individually-loaded PNG it replaces. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
…m/nitrodevco/nitro-next into claude/pixi-react-ui-theme-uj63lt
Pixi's EventSystem listens for pointermove on document and pointerup on window rather than scoping them to its own canvas the way pointerdown is (by design, so a drag started on the canvas keeps tracking once the pointer leaves it). In DOM render mode the room's canvas sits underneath real DOM windows, so hovering or releasing the pointer over a window's own DOM elements still reached the room's hover/drag handling underneath it - confirmed directly with a raw EventSystem + overlaid DOM element reproduction. RoomCanvas's pointer handler now bails out whenever the real topmost element at the event's coordinates isn't the room's own canvas, in DOM mode only (Pixi UI mode has nothing to check - the UI lives on the same canvas already). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
…into useInteractionState useInteractionState now accepts pointer-handler props and composes them with its own hover/press state-transition handlers, so a caller's onPointerTap/onPointerDown etc. always fires alongside whatever internal state logic a themed component needs - no more hand-written merge wrappers per component. useThemeVariant threads the same handlers through to any component built on it. Box and ThemeImage now derive eventMode/pointer-events automatically from whichever pointer handlers are actually present, replacing ~99 hardcoded eventMode="static"/"none" occurrences across the theme package and views with the one consistent rule: a real handler implies interactivity, no handler stays passive. Auditing every occurrence during the sweep surfaced two latent bugs, fixed here: Scaler's resize-drag handle had no eventMode at all in Pixi mode (the window resize handler could never fire), and FriendListItemPixi's outer Box set eventMode="none" whenever onPress was absent, which silently blocked a nested child's own hover-tooltip handlers per Pixi's subtree-blocking semantics. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
…t' into claude/pixi-react-ui-theme-uj63lt
ThemeProps<T> (the public prop type most theme components extend) now carries the same
PointerHandlerProps every useThemeVariant/useInteractionState caller already accepted
internally - previously only the internal ThemeOptions type declared them, so a caller
literally could not pass onPointerOver/onPointerUp/etc. to <Button>, <Frame>, <Dropmenu> and
friends despite the hooks already knowing how to compose them. Every themed component now
destructures the full set and forwards it through, composing with whatever internal handler
it already owns (Frame's activate-on-click, CloseButton's drag-stop, Button's onPress) via the
newly-exported compose() helper, so a caller's handler always fires alongside the component's
own - never replacing it.
useInteractionState no longer fabricates hover/press-tracking closures or forces eventMode
when a component has neither an internal need nor a caller-supplied handler for any of the
six events - it returns {} and the component stays passive, matching Box's own auto-derivation
instead of overriding it unconditionally.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
BackgroundLayerConfig's 'sprite' kind now takes an optional frame, cropping a sub-region out of the texture the same way ThemeImage/BubblePointer already do - a themed component's variant table can now declare Stretch(textureKey, frame) directly and pass it through BackgroundLayer like any other layer kind, instead of reaching for a standalone <ThemeImage> on the side. A frame defaults the layer's own render size to its native crop dimensions rather than Stretch's usual "fill whatever box the caller assigns" (which only makes sense for a whole, uncropped texture) - a caller-supplied layout can still override positioning. Works on both backends: Pixi crops via a Rectangle-offset Texture (same technique ImagePixi already uses), DOM crops via background-position (same technique ImageDom already uses) - BackgroundLayer's own DOM dispatch, which calls BackgroundLayerDom directly rather than through SpriteLayer, shares the sizing default via the same spriteLayoutFromFrame helper so the two paths can't drift out of sync. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GvYpFc4uidm2Sa4hpY9FYa
- Memoize VariantCascadeProvider's context value so consumers only re-render when the cascaded variant map actually changes, instead of on every render of any ancestor provider. - Skip usePixiTexture's async retry effect entirely when the texture is already resolvable synchronously (at mount, or after a url change to an already-cached key) - avoids a redundant promise/microtask and the one-frame blank flash on a url swap to a cached texture. - Add a fast-path bailout to wrapTextChildren for the common case of a single non-text child, skipping Children.map's clone/key-assignment work when there's nothing that could need wrapping. - Stabilize useOutsideClick's callback via a ref so the global pointer listener isn't torn down and re-added on every render of the caller. - Wrap useFrameDrag/useFrameResize's stopDragging/stopResizing in useCallback so the cleanup effect's dependency is stable across renders, matching the pattern already used in useScrollController/ useDomScrollController. Verified via a temporary Playwright harness (deleted before commit): dragging/resizing survive a re-render mid-gesture in both DOM and Pixi modes, tsc holds at the pre-existing 89-error baseline, eslint clean.
event.stopImmediatePropagation() only exists on Pixi's FederatedPointerEvent - Box.tsx forwards this same handler as a React PointerEventHandler in DOM mode, where the event is a React SyntheticEvent with no stopImmediatePropagation method, so this would throw at runtime whenever stopsPropagation is used in DOM mode. Switched to stopPropagation(), which both event types implement and which is all that's actually needed here (block bubbling to an ancestor's pointerdown handler, e.g. Frame's drag-start) - matching the same method already used for this purpose in useScrollController/useDomScrollController.
…m/nitrodevco/nitro-next into claude/pixi-react-ui-theme-uj63lt
CloseButton's stopsPropagation was flipped on, but pressing and holding it still started a window drag. Root cause: @pixi/react maps the onPointerDown JSX prop straight onto Pixi's legacy on<type> property idiom, which EventBoundary.notifyTarget invokes for every ancestor along the hit-tested path unconditionally during the capturing sweep - before the actual target's own handler ever runs. So Header's onPointerDown (the drag-start handler) already fires before CloseButton's own handler gets a chance to call stopPropagation() - by then it's too late, the drag has already started. Fixed by having useFrameDrag's header handler check that it is itself the real hit target (event.target === event.currentTarget) before starting a drag, which correctly excludes a press that landed on a nested interactive descendant like CloseButton. Only applies in Pixi mode - DOM's real bubbling doesn't share this ordering quirk (stopPropagation already worked there), and DOM's target is the deepest DOM node regardless of interactivity, so the same check there would incorrectly reject legitimate header clicks. Also flipped CloseButton's stopsPropagation to true (it was still passing false, leaving the feature dormant) and removed a stray debug console.log left in Box.tsx's DOM path. Verified via a temporary Playwright harness (deleted before commit): normal header dragging still works, a quick tap on the close button still fires onClose with no drag, and pressing-and-holding the close button no longer drags the window - in both DOM and Pixi render modes. tsc holds at the pre-existing baseline, eslint clean.
`manualChunks` sent every `node_modules` module to a single `vendor` chunk, including modules that are only reached through `import()`. `truffle-text` top-level awaits `initFreeType()`, which `import()`s the FreeType Emscripten module, so both sides of that dynamic import landed in `vendor`: the chunk ended up dynamically importing itself and awaiting the result at top level, a promise that can only settle once the chunk has finished evaluating. The result was a deadlock with no error anywhere - `vendor` never finished evaluating, so the `theme` and entry chunks never evaluated either, `createRoot().render()` never ran, and the page stayed the black that `index.html` paints. Dev was unaffected because modules are served unbundled there, so the dynamic import resolves to a real separate module. Keep any module that is the target of an `import()` out of the manual chunks so the async boundary survives bundling. Same guard added to the nitro-renderer config, which has the identical rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bg582tPieGXQHdaahHoefm
The Flash WindowParam port centred an element with `left: '50%'` plus a negative pixel margin (and the same vertically). Replace that with flex mechanics, in the generator's centre branches and across all 266 generated layout views (583 horizontal + 262 vertical sites): - A centre-anchored child now carries no inset on that axis. Both Yoga and CSS resolve an absolutely-positioned child's inset-less axis to its static position - "as if it were the sole flex item" - so the parent's `justifyContent: 'center'` centres it horizontally (main axis of the shared default row direction), and the child's own `alignSelf: 'center'` centres it vertically (cross axis, needing nothing from the parent). - A non-zero design offset rides along as an equal-and-opposite margin pair (`marginLeft: d, marginRight: -d`), which shifts the centred box without changing its margin-box size. For fixed-size elements the result is pixel-identical to the old form at every container size (verified against both Yoga 3.2 and Chromium): old `W/2 + M` == new `(W - w)/2 + (M + w/2)`. - Auto-sized elements (translated labels, template rows) previously had their design-time left edge pinned near the centre, so a label wider or narrower than the Flash design drifted off-centre; they now stay truly centred at whatever width they render. Parent `justifyContent` is safe for every generated sibling - all other anchors always emit at least one inset per axis, and an inset always wins over alignment. Composite chrome pieces were the one exception: an axis with neither inset set relied on the static position being the top-left corner, which only holds under default alignment. Pin their start inset explicitly (CompositePieceSprite + CompositePieceDom) so themed boxes that now declare `justifyContent: 'center'` don't re-centre their own chrome. Known small delta: 13 ContainerButtons whose bare caption duplicates a centred label child - the caption (a flow text item) moves from the button's top-left to top-centre, aligning with the label it duplicates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bg582tPieGXQHdaahHoefm
One import - InventoryView's `import { InventoryLayout } from '../layouts'`
- went through the generated `export *` barrel over all ~800 layout views.
None of those modules are provably side-effect-free to the bundler, so
tree-shaking kept every one, and the entire lazily-registered catalogue
landed in the entry's static module graph: the built index.html carried
794 modulepreload links, so every page load fetched every layout chunk
up front, defeating layoutRegistry's per-entry dynamic `load()` entirely.
- InventoryView now imports InventoryLayout by its own path, like the
other two views that legitimately need a layout at boot.
- The barrel index.ts is deleted and no longer generated, so a future
convenience import fails loudly instead of silently re-eagerizing 800
chunks. Layouts are imported by path, or through the registry.
- LayoutBrowserView (the dev tool) is now behind React.lazy: it
statically holds layoutRegistry - ~6,400 lines of metadata plus the
dynamic-import map for every layout - which otherwise sits in the
boot-critical entry chunk for a window most sessions never open.
Built output, before -> after:
- index.html: 78,190 -> 2,258 bytes (modulepreload links 802 -> 11)
- entry chunk: 743 KB -> 217 KB
- dist/assets file count: 1,616 -> 826 (each dual static+dynamic layout
had split into a real chunk plus a re-export stub; only the three
intentionally-static layouts keep a facade now)
- boot fetches 11 JS files instead of 800+
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bg582tPieGXQHdaahHoefm
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.