feat(types): normalized data model — primitives, entities, store, migrations, player view#254
feat(types): normalized data model — primitives, entities, store, migrations, player view#254
Conversation
Complete codebase audit (24 findings) and 30 modularization recommendations covering visual framework separation, logic modularization, testing infrastructure, performance and accessibility hardening, and repo cleanup. 13-session execution plan with per-task specs, acceptance criteria, and dependency ordering. This serves as the persistent roadmap for incremental refactoring across multiple sessions. https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
…timize icon - Delete 5 dead Vite boilerplate files (App.css, react.svg, 3 public SVGs) - Gate FogOfWarLayer diagnostic logging behind DEBUG_VISION flag (was ~40 lines of console.log firing on every render in dev mode) - Optimize icon.png from 927KB to 72KB (512x512, 128-color quantized) - Add coverage/ to .gitignore (generated reports shouldn't be tracked) - Document test coverage baseline in CLAUDE.md session notes: 33% statements, 27% branches, 40% functions across 792 passing tests https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
…action - Consolidate root docs: delete duplicate ARCHITECTURE.md, move 8 docs to docs/ subdirs (guides/, features/, architecture/, planning/) - Move diagnose-dungeon.ts to tests/helpers/, update tsconfig.json - Update all internal cross-references across moved docs - Create 4 component subdirectories: ErrorBoundaries/, Dialogs/, Managers/, Mobile/ - Move 32 files (17 error boundaries, 6 dialogs, 6 managers, 3 mobile) - Update ~35 import paths in consumer files and moved files - Fix vi.mock() paths in test files, fix import ordering via eslint --fix - Extract 14 domain types + 2 constants from gameStore.ts to src/types/domain.ts - gameStore.ts re-exports all types for backward compatibility - Update IStorageService.ts to import from types/domain per architecture contract All 792 tests pass. Zero TypeScript errors. Zero lint errors. Build succeeds. https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
… contrast - Add ~120 CSS custom properties to theme.css covering canvas, doors, stairs, minimap, measurement, touch, tokens, walls, toolbar, monitor, home, dialogs, error boundaries, logo, and library categories with dark-mode variants - Sweep 20+ component files replacing hardcoded hex/rgba values with theme tokens; use *_COLORS constant objects for Konva canvas components (can't use CSS vars) - Remove 7 redundant Radix dark-mode CSS imports (values already declared in [data-theme='dark'] block) - Scope global * transition rule to specific UI classes + .themed-transition opt-in - Add @media (prefers-contrast: more) block for WCAG 2.2 AA high-contrast support - Create brand.css configuration layer for single-file rebranding (accent colors, fonts, logo paths) All 792 tests pass. TypeScript 0 errors. Build succeeds. https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
Create the design system primitive layer (src/components/primitives/): - Button: 5 variants (primary, secondary, ghost, destructive, tool), 3 sizes (sm, md, lg), isActive/isLoading/leftIcon/rightIcon support, focus-visible ring, aria-disabled/aria-busy attributes - Input: label, error state with role="alert", helper text, aria-invalid, auto-generated IDs via useId() - Card: 3 variants (surface, elevated, outlined), configurable padding - ToggleSwitch: moved to primitives/ with re-export at old path Add primitives.css with all component styles using theme tokens. Migrate 7 buttons across 3 files (ConfirmDialog, DoorControls, MapSettingsSheet) as proof-of-concept. Update DesignSystemPlayground to showcase all new primitives. All 792 tests pass. TypeScript 0 errors. Build succeeds. https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
- Create Dialog primitive with full a11y: focus trap (Tab/Shift+Tab), Escape/overlay-click to close, auto-focus on open, return-focus on close, scroll lock, aria-modal/labelledby/describedby, 4 size variants, prefers-reduced-motion support - Migrate ConfirmDialog and PreferencesDialog to use Dialog primitive, eliminating duplicate overlay/keyboard/focus handling - Extract 1,032 lines of inline <style> from HomeScreen.tsx to src/styles/home-screen.css — HomeScreen drops from 1,776 to 745 lines - JS bundle drops 26KB (912KB→886KB) since CSS string no longer in JS All 792 tests pass. TypeScript 0 errors. Build succeeds. https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
…t boundaries Install eslint-plugin-jsx-a11y and fix 74 accessibility violations across 15 files. Upgrade 5 TypeScript rules from warn to error (no-explicit-any, no-misused-promises, no-unsafe-member-access, no-unsafe-call, no-unsafe-assignment) after fixing ~115 violations. Add import/no-restricted-paths enforcing Design System Contract boundaries. Add ExposedIpcRenderer type to window.d.ts for typed IPC access. https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
Created src/store/uiStore.ts with all UI ephemeral state (toast, confirmDialog, showResourceMonitor, dungeonDialog, isGamePaused, isMobileSidebarOpen, isCommandPaletteOpen) extracted from gameStore. gameStore is now domain-pure (607 lines, down from 836). Migrated 23 source files and 9 test files. SyncManager subscription no longer fires on UI state changes. https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
…ic hooks Extracted 4 pure functions (calculateVisibilityPolygon, castRay, lineSegmentIntersection, getWallSegments) from FogOfWarLayer.tsx to src/utils/vision.ts — zero React/Konva dependencies, 100% test coverage (26 tests). Created useRecentCampaigns and usePlatformDetection hooks to remove direct localStorage/navigator access from HomeScreen. https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
- useToolState: tool selection, colors, door orientation, measurement mode, keyboard shortcuts - useMenuCommands: Electron IPC menu handler registration (save/load/new/about) - useLibraryLoader: token library index loading on startup - campaignService: save/load/new campaign orchestration (zero React imports) - Toolbar component: extracted desktop toolbar JSX from App.tsx - App.tsx reduced from 770 to 283 lines (63% reduction) https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
Extract keyboard, drop, selection, and drawing concerns from the 1,892-line CanvasManager into focused, testable hooks. Extract door context menu into a standalone component. CanvasManager reduced to 1,450 lines (23% reduction). New files: - useCanvasKeyboard.ts: keyboard shortcuts + modifier key state - useCanvasDrop.ts: file/token drop handling + image crop flow - useCanvasSelection.ts: selection rect, transformer, hover tracking - useCanvasDrawing.ts: drawing refs/state (isDrawing, tempLine, etc.) - DoorContextMenu.tsx: right-click door actions (open/close/lock/delete) Also: gate diagnostic logging behind DEBUG_CANVAS flag, fix production console.log leak, remove stray DoorLayer render log. https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
Lazy-load 5 infrequent components with React.lazy, reducing main bundle from 891KB to 810KB (gzip 259KB → 238KB, 9% reduction). Add Konva performance budget: pixelRatio capped at 2 (1 on low-end devices detected via deviceMemory/hardwareConcurrency), explored fog regions cached at Konva level to avoid re-executing sceneFunc on unchanged regions. Lazy chunks: DesignSystemPlayground (46KB), DungeonGeneratorDialog (12KB), UpdateManager (11KB), CommandPalette (8KB), ResourceMonitor (6KB). https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
… focus indicators Add CanvasAccessibility component with screen reader announcements for token, door, and tool state changes. Enable keyboard token navigation (Tab to cycle, Enter to activate, Arrow keys to move by grid cell). Add skip-to-content link, landmark roles (nav/main), visible :focus-visible indicators on all interactive elements, and keyboard-accessible sidebar token items with tabIndex/role/aria-label. Fix MapNavigator and Sidebar edit buttons to show on focus-within (not hover-only). https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
All extracted modules now have comprehensive unit test coverage: - uiStore (100%), campaignService (100%), vision (100% — existing) - useToolState (94%), useMenuCommands (96%), useRecentCampaigns (100%) - Button (100%), Dialog (92%), Input (100%) Total: 969 tests passing across 48 test files. https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
- useToolState: wrap DOM element tests in try/finally to prevent leak on failure - Button: add icons.length guard, use toHaveClass instead of toContain - Dialog: remove unused queryDialog, document aria-hidden pattern - campaignService: suppress expected console.error in error-path tests https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
- useToolState: Hoist vi.fn() refs to module scope so gameStore selectors return stable identity (prevents spurious useEffect re-fires). Add test verifying setActiveMeasurement(null) on mode change. - useRecentCampaigns: Replace mockReturnValueOnce with mockReturnValue for mount-phase mocks (StrictMode double-invokes useState initializers). - Dialog: Add auto-focus tests with rAF mocking (first focusable, closed dialog no-op) and focus-restoration tests (return focus to trigger on close). 975 tests passing, 48 test files. https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
- Remove all backward-compat type re-exports from gameStore.ts (ADR-005 cleanup). Migrate 25+ files to import domain types from types/domain directly instead of gameStore. - Delete ToggleSwitch.tsx re-export shim (0 consumers remained). - Move MapSettingsSheet to import ToggleSwitch from primitives/. - Move 6 loose docs from docs/ root to proper subdirectories (features/, guides/, planning/). Fix cross-reference in HYBRID_TESTING_WORKFLOW.md. - Update CHANGELOG.md with comprehensive modular architecture refactor summary covering all 13 sessions. - Clean unused type imports from gameStore.ts (TokenMetadata, ToastMessage, ConfirmDialog). 975 tests passing, TypeScript 0 errors, ESLint 0 errors. https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
Fix gridColor not persisting across map switch/create/delete/IPC sync, keyboard trap in canvas accessibility widget (WCAG 2.1.2), Dialog focus trap edge case, Object URL memory leak, stale Konva cache, dead code, CSS layout mismatch, missing ARIA labels, and wire keyboard token placement. https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
- README.md: Coverage badge 100% → 36% to match actual coverage - CONTRIBUTING.md: Update project structure with new directories (Dialogs/, ErrorBoundaries/, Managers/, Mobile/, primitives/, hooks/, services/, styles/, types/) and document store split (gameStore + uiStore) and campaignService - CLAUDE.md: Update Quick Reference line counts to match actual file sizes after Sessions 12-14 https://claude.ai/code/session_012fJP8VEtRL2fj1Fdgo6aEV
…lity, and add tests (#252) * Initial plan * Fix PR review issues: import paths, memory leaks, asset pipeline, accessibility - Fix DoorBlocking.test.ts import paths (../types → ../../types) - Revoke existing object URL before creating new one in useCanvasDrop - Use asset pipeline (processImage) instead of base64 for cropped images - Improve CommandPalette keyboard accessibility (tabIndex={0} + aria-activedescendant) - Gate arrow key movement in World View in CanvasAccessibility Addresses feedback from PR #251 review thread. Co-authored-by: kocheck <7952000+kocheck@users.noreply.github.com> * Add comprehensive unit tests for useCanvasDrop hook - Test drag over behavior (World View blocking) - Test library token drop - Test generic token drop - Test file drop and object URL creation - Test object URL revocation on multiple drops (memory leak prevention) - Test crop confirm with asset pipeline integration - Test crop confirm error handling - Test crop cancel cleanup 12 tests covering all main branches and edge cases. Addresses Issue #6 from PR review thread. Co-authored-by: kocheck <7952000+kocheck@users.noreply.github.com> * Fix async handler linting error in CanvasManager Wrap handleCropConfirm with void operator to satisfy TypeScript linter when passing async function to prop expecting void return. Co-authored-by: kocheck <7952000+kocheck@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: kocheck <7952000+kocheck@users.noreply.github.com>
- Rewrote CLAUDE.md (1,904 → 133 lines): commands, architecture tree, design system contract table, key files, gotchas & patterns, doc links - Created docs/planning/REFACTOR_SESSIONS.md (868 lines): full historical archive of 14-session modular refactor (task specs, checklists, session notes, ADR-001 through ADR-005) - Appended 5 refactor ADRs to docs/architecture/DECISIONS.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… 100+ files - Run npm install to restore missing eslint-plugin-jsx-a11y dependency - Fix all 822 ESLint warnings across entire codebase (8 sessions of accumulated debt) - Fixes by category: explicit-function-return-type (220), no-non-null-assertion (99), no-console eslint-disable (78), prefer-nullish-coalescing (75), no-explicit-any (27), nested-ternary (16), prefer-optional-chain (14), import/no-unused-modules, and misc - Fix SyncManager throttle generic constraint (any[] → proper generic) - Fix all error boundary render() return types (JSX.Element → ReactNode) - npm run lint, type-check, test:run, build:web all pass Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove leftover debug console.log from syncUtils.ts detectChanges() — was suppressed with eslint-disable rather than deleted - Fix SyncManager.tsx:353-354 || [] → ?? [] for consistency with rest of file Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…llowing
Replace empty .catch(() => {}) with console.warn so theme storage errors
are observable in development and production diagnostics. The storage
service already returns a safe 'system' fallback internally, so this
catch only fires on truly unexpected errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SyncManager:
- Replace removeAllListeners() with named listener refs + ipcRenderer.off()
so cleanup only removes this effect's listeners, not all channel listeners
- Guard BroadcastChannel messages against unknown action types using
VALID_SYNC_ACTIONS set before casting to SyncAction
WebStorageService:
- Add reject() to loadCampaign() Promise constructor — previously throw
inside async onchange callback produced an unhandled rejection and left
the outer Promise permanently pending on parse errors
AutoSaveManager:
- Show error toast when auto-save returns false so users know their
progress may not be saved, instead of silently logging to console only
DungeonGenerator:
- Replace default: break in 7 Direction switch statements with
throw new Error() so unexpected direction values surface immediately
instead of leaving !-asserted variables uninitialized
pathOptimization:
- Replace ?? 0 / ?? {x:0,y:0} silent fallbacks with explicit guards
and console.warn so out-of-bounds array access is visible in diagnostics
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ty issues syncUtils.ts: - Export SYNC_ACTION_TYPES as Set<SyncAction['type']> so it stays type-checked against the union (no manual string maintenance) - Export isSyncActionType() type guard for narrowing unknown strings SyncManager.tsx: - Use isSyncActionType() instead of local VALID_SYNC_ACTIONS Set - Add listenerSetupRef guard to Architect branch to match World View branch — prevents duplicate IPC listeners on React StrictMode re-runs WebStorageService.ts: - Add input 'cancel' event handler so loadCampaign() Promise resolves null when user dismisses the file picker (was hanging forever) AutoSaveManager.tsx: - Show toast in catch block too — thrown exceptions now surface to the user, not just WebStorageService false return pathOptimization.ts: - Replace unreachable undefined guards in rdpRecursive with ! assertions and eslint-disable comments (length >= 3 is guaranteed by caller guard) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Captures approved color system (Radix Sand), typography scale, Remix icon mapping, responsive breakpoints, and rules for Torchlit (amber) and Arcane Codex (violet) variations.
Covers HexColor/PixelSize branded types, createGridGeometry graceful fallback, and campaign-load validation boundary.
…PixiJS Rewrites both overlay components from react-konva to the PixiJS v8 imperative pattern (worldContainer + containerRef + useEffect), matching the pattern established by DoorLayer, StairsLayer, and other Phase 5 layers. - MeasurementOverlay: ruler (line+label), blast (circle+dot+label), and cone (poly+dot+label) shapes drawn with PixiJS Graphics + Text; zIndex 150; parseRgba helper converts CSS rgba/hex strings to PixiJS numbers - MovementRangeOverlay: BFS flood-fill logic unchanged; each reachable cell drawn as a closed polygon via Graphics.poly(); zIndex 140 - Both test files rewritten using vi.hoisted + vi.mock pattern (no WebGL needed) - CanvasManager: wires MeasurementOverlay; MovementRangeOverlay kept as TODO import until token-selection context is available in a future phase
…to MovementRangeOverlay - Create src/utils/pixiColor.ts with parseRgba (fixes /./g → /[0-9a-fA-F]/g bug in 3-digit hex expansion) - Add src/utils/pixiColor.test.ts with 5 cases covering hex, 3-digit hex, rgba, rgb, and invalid fallback - Update MeasurementOverlay and MovementRangeOverlay to import from utils and remove local copies - Add named export `export function MovementRangeOverlay` alongside existing default export
… packages
Delete 5 dead-code files (DoorShape, StairsShape, URLImage, CanvasUtils,
useCanvasInteraction) — all replaced by PixiJS equivalents with no live imports.
Remove react-konva import from TokenErrorBoundary; replace canvas indicator
with auto-shown DOM Portal overlay in dev mode, removing showDebugOverlay
toggle state and TOKEN_ERROR_COLORS constant.
Remove vi.mock('react-konva') block from test setup — no longer needed.
Remove Konva migration ESLint override from .eslintrc.cjs — permanent
no-restricted-imports guardrail now enforced across all files.
Uninstall konva, react-konva, use-image packages.
Zero konva/react-konva imports remain in src/. 1005 tests pass.
…nager - Consolidate drawRuler/drawBlast/drawCone params into DrawStyle object (max-params) - Replace non-null assertions in strokeGeometry.ts and PressureSensitiveLine.tsx with safe guards (no-non-null-assertion) - Add eslint-disable-next-line import/no-unused-modules to all public-API exports, test helpers, and not-yet-wired layer components - Add eslint-disable-next-line react-refresh/only-export-components to buildGridGeometry in GridOverlay.tsx - Add eslint-disable comments to glsl.d.ts ambient module declarations - Remove stale TODO comments referencing deleted files useCanvasInteraction, URLImage, and TokenErrorBoundary import from CanvasManager.tsx
…, hexToRgbFloats; pre-allocate uniforms; remove dead props
- Extract usePixiContainer hook (hooks/usePixiContainer.ts) and replace
duplicated mount/unmount useEffect in DoorLayer, StairsLayer,
MeasurementOverlay, and MovementRangeOverlay
- Extract clearContainer to src/utils/pixiUtils.ts; replace all
container.removeChildren().forEach(c => c.destroy({children:true})) calls
- Add hexToRgbFloats to pixiColor.ts; remove private hexToRgb copy from
PressureSensitiveLine
- Pre-allocate _lightsA/_lightsB Float32Arrays in FogOfWarFilter as instance
fields; reuse with fill(0) on every updateLights call
- Remove dead isWorldView prop from StairsLayerProps and CanvasManager callsite
- GridOverlay: replace parseInt hex parse with parseRgba(gridColor).color
- MovementRangeOverlay: return geometry from useMemo to eliminate double
createGridGeometry call; add geometry to useEffect deps
- TextureCache: remove resolved promises from inFlight map via .finally() so
it tracks only truly in-flight loads
Replace clearContainer + full rebuild with a Map<string, Graphics> ref. Stairs are static elements — only add/remove logic needed. Exports stairsKey() pure function and clears the map on worldContainer change to avoid stale refs.
…irsKey export Add worldContainer to the stairs effect dependency array so the diff loop re-runs when usePixiContainer swaps the container, preventing the new container from remaining empty until stairs itself changes. Remove the stairsKey export (a one-liner alias for stair.id) along with its two eslint-disable comments and the test file that existed solely to cover it. Remove the unused default export as well.
…hader recreate on geometry change
… prevent GPU resource leak
Update package-lock.json to reflect upgraded dev dependencies: flatted 3.4.2 and tar 7.5.11. Resolved URLs and integrity hashes were updated in the lockfile.
… screen
The package uses dynamic require('url') which crashes Vite's ESM renderer
in dev mode, preventing React from mounting (white screen). The Transformer
was a Phase 2 feature targeting PixiJS v6 with as-any casts — not functional
with v8. Will reimplement with a v8-native solution later.
Fix remaining strict-mode issues in plan code blocks: - Use Record<string, unknown> instead of `as any` for import.meta.env mutation in DEV-warn test - Use typed narrowing chain instead of `as any` for campaign.version access in migrateCampaign
…cated Stairs/Campaign
Implements the player view sync layer: strips exploredRegions, filters hidden entities, resolves attached light positions from tokens, and suppresses lights whose parent token is hidden. 14 tests, all passing.
Adds tests for migrateDrawings (pressures/x/y/scale defaults) and migrateDoors (mapId/hidden/thickness/swingDirection defaults), plus a guard asserting MIGRATIONS table has exactly one entry at CURRENT_VERSION=1.
📚 Documentation Check (Rule-Based)This PR was analyzed for potential documentation impacts. 🔌 IPC Changes DetectedFiles in Please review:
Check if you need to:
⚛️ Component Changes DetectedFiles in Please review:
Check if you need to:
🗄️ State Management Changes DetectedFiles in Please review:
Check if you need to:
🛠️ Utility Changes DetectedFiles in Please review:
Check if you need to: 📦 Dependencies Changed
Please review:
Check if you need to:
📖 Documentation StandardsAll changes should follow Hyle's documentation standards:
Complete documentation guide: DOCUMENTATION.md Rule-based check • For AI-powered analysis, see documentation-check.yml |
📚 Documentation CheckGitHub Copilot has analyzed this PR for documentation impact. null 📝 Documentation Files ReferenceRoot docs: View all See DOCUMENTATION.md for complete documentation inventory. Powered by GitHub Copilot • Documentation Guide |
❌ Accessibility Audit FailedThis pull request introduces WCAG AA contrast violations. Please fix the following issues before merging: See uploaded report for details. ResourcesReminder: Always use semantic CSS variables (e.g., |
Summary
Record<BrandedId, Entity>tables for O(1) access;MapDataholds only ID arrays, no nested objectsprimitives.ts,entities.ts,store.ts,player-view.ts,features.ts,migrations.ts— replaces the monolithicdomain.ts(kept as backward-compat re-export shim with deprecated legacy types)migrateCampaignupgrades v0 (nested arrays) → v1 (flat tables); pure functions, typed narrowing, noas anyon main pathderivePlayerViewfilters hidden entities, resolves attached light positions, stripsexploredRegions— memoization-ready pure functionFeatureFlagsinterface +DEFAULT_FEATURE_FLAGSinuiStore(runtime-only, not persisted)Stairs,Campaign,LegacyCampaignMap,TokenMetadatare-exported with@deprecatedJSDoc for gradual migrationTest Plan
npm run test:run— 1050 tests, 0 failuresnpm run type-check— no type errorsnpm run lint— no lint errorsdomain.tsconsumers still compile (shim re-exports all prior public types)migrateCampaignshould upgrade it to v1 without errors