Skip to main content

UI Improvements - March 2026

Last Updated: March 26, 2026
Related PRs: #1093, #1092, #1089, #1088

Overview

March 2026 saw a comprehensive UI polish pass focusing on consistency, reusability, and user experience. Key improvements include unified skilling panels, redesigned NPC dialogue system, combat panel enhancements, and critical bug fixes.

Skilling Panel Unification (PR #1093)

Problem

Each skilling panel (Fletching, Cooking, Smelting, Smithing, Crafting, Tanning) had its own duplicated styling code:
  • ~500 lines of duplicated CSS-in-JS across 5 panels
  • Inconsistent visual treatment (colors, borders, shadows)
  • Duplicated quantity selector logic
  • Maintenance burden (changes required updating 5+ files)

Solution

Extracted shared components and style helpers into SkillingPanelShared.tsx:

SkillingPanelBody

Wrapper component for panel content with intro text and empty state support.
Usage:

SkillingSection

Themed section card for grouping recipes.
Styling:
  • Background: theme.colors.background.panelSecondary
  • Border: theme.colors.border.default
  • Inset highlight: rgba(255, 255, 255, 0.03)
  • Rounded corners: rounded-xl
Usage:

SkillingQuantitySelector

Reusable quantity selector with preset buttons and custom input mode.
Features:
  • Preset buttons: 1, 5, 10, All, X (custom)
  • Custom input mode with Enter/Escape keyboard shortcuts
  • Remembers last custom quantity
  • Responsive layout (2 columns mobile, 5 columns desktop)
Usage:

Style Helpers

Consistent visual treatment for selectable items and badges.
getSkillingSelectableStyle:
  • Selected: Accent-tinted background with glow border
  • Unselected: Dark semi-transparent background
  • Disabled: 48% opacity
getSkillingBadgeStyle:
  • Dark background with subtle border
  • Secondary text color
  • Consistent across all panels

Impact

  • Code Reduction: ~500 lines of duplicated styling eliminated
  • Consistency: All skilling panels now have identical visual language
  • Maintainability: Single source of truth for skilling UI patterns
  • Reusability: New skilling panels can use shared components immediately
  • Mobile: Responsive layouts with proper touch targets

Migration

Before (duplicated in each panel):
After (shared helper):

NPC Dialogue Redesign (PR #1093)

Problem

Old dialogue system had several issues:
  • Generic modal shell (not dialogue-specific)
  • No character portraits (less immersive)
  • Service handoffs (bank, store, tanner) left orphaned dialogue panels
  • Inconsistent focus management

Solution

DialoguePopupShell

Dedicated modal shell for NPC dialogue with proper focus management.
Features:
  • Auto-focus on open
  • Escape key to close
  • Click outside to close
  • Prevents event bubbling to game world
  • Gold accent bar at top (dialogue-specific styling)
  • Responsive sizing (mobile-friendly)
Default Dimensions:
  • Width: 700px
  • Max width: min(86vw, 700px) (responsive)
  • Max height: min(40vh, 400px) (prevents overflow)

DialogueCharacterPortrait

Live 3D VRM portrait rendering in dialogue panels.
Features:
  • Renders NPC’s VRM model in real-time
  • Isolated Three.js scene (doesn’t affect main game)
  • Automatic camera positioning
  • Memoized for performance
  • Fallback to placeholder if VRM not loaded
Implementation:
  • Creates dedicated WebGPURenderer instance
  • Clones NPC’s VRM model (shares textures, independent materials)
  • Renders to canvas element in dialogue panel
  • Cleanup on unmount (disposes renderer, scene, materials)

Service Handoff Fix

Opening bank/store/tanner now properly closes dialogue: Before:
After:
Client-side (packages/client/src/hooks/useModalPanels.ts):

Impact

  • Immersion: Live NPC portraits make dialogue feel more engaging
  • Consistency: Dedicated dialogue shell with dialogue-specific styling
  • UX: Service handoffs no longer leave orphaned panels
  • Accessibility: Proper focus management and keyboard navigation

Combat Panel Enhancements (PR #1088)

Combat Style Banners

Features:
  • Drag-to-action-bar support for combat styles
  • Fixed click handling on drag overlays
  • Stabilized banner width (4-column calc width instead of flex:1)
  • Banners stay same size whether 3 or 4 styles are shown
Implementation:
Drag Overlay Fix:

Auto-Retaliate Toggle

Fix: Auto-retaliate toggle was overridden by stale entity read on useEffect re-run. Problem:
Solution:
Impact: User’s auto-retaliate toggle persists correctly during combat.

Equipment Panel Cross-Player Leak Fix (PR #1089)

Problem

Equipment panel showed stale data from previously inspected players:
  1. User inspects Player A’s equipment
  2. Panel opens with Player A’s data
  3. User closes panel
  4. User inspects Player B’s equipment
  5. Panel opens with Player A’s data (stale!)
Root Cause: Panel data was captured in closure at render time. When renderPanel function was created, it closed over the initial data value. Subsequent data changes didn’t recreate the function.

Solution

Include panel data in useMemo dependencies to recreate renderPanel when data changes: Before:
After:
Alternative Approach (also valid):

Impact

  • Equipment panel always shows current player’s data
  • No cross-contamination between inspected players
  • Fixes confusing UX where wrong player’s stats appeared

Arrow Key Capture Fix (PR #1092)

Problem

When a combined panel tab retained focus, pressing an arrow key would switch tabs instead of moving the camera. This broke camera controls during gameplay. Root Cause: Tab component’s onKeyDown handler consumed arrow key events without checking if they should be reserved for game controls.

Solution

Added reserveArrowKeys prop to Tab component:
Usage:

Impact

  • Arrow keys control camera movement even when panel tabs have focus
  • Enter/Space still activate tabs for keyboard accessibility
  • Better separation between game controls and UI navigation

Missing Packet Handlers (PR #1091)

Problem

Server was sending 8 packet types that client had no handlers for, causing console errors:

Solution

Added 8 missing handler methods in ClientNetwork.ts:
Pattern: Each handler forwards packet data to client world event bus so UI systems can react.

Impact

  • Eliminates “No handler for packet” console errors
  • UI systems can now react to skill completion events
  • Combat end notifications work correctly
  • Quest start notifications work correctly

Prayer Login Sync Fix (PR #1090)

Problem

Prayer state (points, active prayers) wasn’t syncing correctly on player login:
  • Prayer points reset to max on login
  • Active prayers cleared on login
  • Inconsistent state between sessions

Solution

Fixed prayer state synchronization in login flow:
Also Fixed:
  • Prayer drain persistence
  • Prayer point restoration on level-up
  • Active prayer sync on reconnect

Impact

  • Prayer points persist correctly between sessions
  • Active prayers remain active after reconnect
  • Consistent prayer state across client and server

Component API Reference

SkillingPanelBody

Props:
  • theme: Theme - Current theme object
  • children?: ReactNode - Panel content (recipe grid, etc.)
  • emptyMessage?: string - Message shown when no content available
  • intro?: string - Introductory text at top of panel
Styling:
  • Intro text: text-xs, secondary color, relaxed line height
  • Empty message: Centered, rounded border, secondary background
Example:

SkillingSection

Props:
  • theme: Theme - Current theme object
  • children: ReactNode - Section content
  • className?: string - Additional CSS classes
  • style?: CSSProperties - Additional inline styles
Styling:
  • Background: panelSecondary
  • Border: border.default
  • Padding: p-3 (12px)
  • Border radius: rounded-xl
  • Inset highlight: rgba(255, 255, 255, 0.03)
Example:

SkillingQuantitySelector

Props:
  • theme: Theme - Current theme object
  • showCustomInput: boolean - Whether custom input mode is active
  • customQuantity: string - Current custom quantity value
  • lastCustomQuantity: number - Last submitted custom quantity (for placeholder)
  • onCustomQuantityChange: (value: string) => void - Custom input change handler
  • onCustomSubmit: () => void - Custom input submit handler
  • onCancelCustomInput: () => void - Custom input cancel handler
  • onPresetQuantity: (quantity: number) => void - Preset button click handler
  • allQuantity: number - Maximum quantity for “All” button
  • onShowCustomInput: () => void - Show custom input mode handler
Modes: Preset Mode (default):
  • Buttons: 1, 5, 10, All, X
  • Grid layout: 2 columns (mobile), 5 columns (desktop)
  • Click preset → immediately process quantity
Custom Input Mode:
  • Number input with placeholder showing last custom quantity
  • OK/Cancel buttons
  • Enter key → submit
  • Escape key → cancel
  • Auto-focus on input
Example:

DialoguePopupShell

Props:
  • visible: boolean - Whether dialogue is visible
  • title: string - Dialogue title (NPC name)
  • children: ReactNode - Dialogue content (text, responses, portrait)
  • onClose: () => void - Close handler
  • width?: number | string - Panel width (default: 700)
  • maxWidth?: number | string - Max width (default: min(86vw, 700px))
  • maxHeight?: number | string - Max height (default: min(40vh, 400px))
  • contentStyle?: CSSProperties - Additional content area styles
Features:
  • Auto-focus on open
  • Escape key to close
  • Click outside to close
  • Prevents event bubbling to game world
  • Gold accent bar at top
  • Responsive sizing
Example:

DialogueCharacterPortrait

Props:
  • world: World - Game world instance
  • npcEntityId: string - NPC entity ID
  • npcName: string - NPC name (for fallback)
  • className?: string - Additional CSS classes
Rendering:
  • Creates isolated Three.js scene
  • Clones NPC’s VRM model
  • Renders to 200×200 canvas
  • Auto-cleanup on unmount
Example:

Style Helpers

getSkillingSelectableStyle

Generate consistent style for selectable items (recipe cards, material options).
Returns:
Usage:

getSkillingBadgeStyle

Generate consistent style for badges (level requirements, quantities).
Returns:
Usage:

Migration Guide

Updating Existing Skilling Panels

Step 1: Import shared components
Step 2: Replace panel body wrapper
Step 3: Replace section wrappers
Step 4: Replace quantity selector
Step 5: Replace style helpers

Creating New Skilling Panels

Use shared components from the start:

Best Practices

Skilling Panels

  1. Always use shared components - Don’t duplicate styling
  2. Consistent intro text - Explain what materials are needed
  3. Empty states - Show helpful message when no recipes available
  4. Responsive grids - Use grid-cols-2 (mobile) and sm:grid-cols-3 (desktop)
  5. Proper touch targets - Minimum 44×44px for mobile

Dialogue Panels

  1. Use DialoguePopupShell - Don’t use generic modal
  2. Include character portrait - More immersive than text-only
  3. Close on service handoff - Bank/store/tanner should close dialogue
  4. Keyboard navigation - Escape to close, Enter to select response
  5. Prevent event bubbling - Mark events as isCoreUI to prevent game interaction

Combat Panels

  1. Fixed banner width - Use calc width, not flex:1
  2. Drag overlay - Handle both click and drag events
  3. Optimistic updates - Update UI immediately, sync with server
  4. Auto-retaliate - Don’t override with stale entity reads

Performance Considerations

Skilling Panels

  • Shared components: Reduce bundle size (single implementation)
  • Memoization: Use React.memo for expensive recipe cards
  • Virtual scrolling: For panels with 100+ recipes (future)

Dialogue Panels

  • Portrait rendering: Isolated Three.js scene (doesn’t affect main game)
  • Memoization: DialogueCharacterPortrait is memoized
  • Cleanup: Dispose renderer/scene/materials on unmount
  • Texture sharing: Cloned VRM shares textures (memory efficient)

Combat Panels

  • Optimistic updates: Reduce perceived latency
  • Debounced sync: Batch style changes to server
  • Fixed layouts: Prevent reflow on style count change

Accessibility

Keyboard Navigation

  • Tab: Focus next interactive element
  • Enter/Space: Activate focused element
  • Escape: Close modal/cancel input
  • Arrow keys: Reserved for game controls (camera movement)

Screen Readers

  • ARIA labels: All buttons have aria-label
  • Role attributes: Modals use role="dialog" and aria-modal="true"
  • Focus management: Auto-focus on modal open, restore on close
  • Semantic HTML: Use <button>, <input>, <h2> instead of <div>

Touch Targets

  • Minimum size: 44×44px for mobile
  • Spacing: 8px gap between interactive elements
  • Visual feedback: Hover/active states for all buttons

Future Improvements

Skilling Panels

  • Virtual scrolling for large recipe lists
  • Recipe search/filter
  • Favorite recipes
  • Batch crafting queue
  • Material requirement tooltips

Dialogue Panels

  • Dialogue history (scroll back)
  • Voice acting integration
  • Animated portraits (lip sync, expressions)
  • Dialogue choices with icons
  • Multi-NPC dialogues (group conversations)

Combat Panels

  • Combat style presets (save/load)
  • Damage calculator
  • DPS meter
  • Combat log
  • Auto-retaliate delay slider

References

  • PR #1093: Dialogue and skilling panel polish
  • PR #1092: Arrow key capture fix
  • PR #1091: Missing packet handlers
  • PR #1090: Prayer login sync
  • PR #1089: Equipment panel cross-player leak
  • PR #1088: Comprehensive UI panel upgrade
  • SkillingPanelShared.tsx: Shared skilling components
  • DialoguePopupShell.tsx: Dialogue modal shell
  • DialogueCharacterPortrait.tsx: Live NPC portrait rendering