GappHub Expert Review

Project-wide code review cross-referenced against the UI spec, roadmap, drift report, interview logs, and task history. All findings verified with a second read of the source code.

2 Critical findings
2 High findings
111 Vitest tests passing
3 Validation gates clean

Critical

Critical Reduced motion CSS selectors target stale element type

globals.css:109-124 — The prefers-reduced-motion CSS rules target [role="gridcell"] a and [role="toolbar"] a, but since Phase 7, all app icons are <button> elements (see AppIcon.tsx:62). These selectors match nothing — reduced motion users still get hover scale/shadow and press-in transforms. Additionally, Accessibility.test.tsx:114 asserts the stale [role="gridcell"] a selector exists in the CSS, so the test passes while the actual behavior is broken.

Fix: Change a to button in all four CSS rule blocks and update the test assertions to match.

Critical Sort order omits badge "N" (New)

products.ts:52-58sortProducts calls byBadge("L"), byBadge("B"), byBadge("C"), and byBadge(null) but never byBadge("N"). The Product type at product.ts:7 explicitly allows "N" as a valid badge. Any product added with badge "N" will silently disappear from the grid. No products currently use this badge, so the bug is latent.

Fix: Add ...byBadge("N") between byBadge("B") and byBadge("C").

High

High Barrel import of entire lucide-react icon library

AppIcon.tsx:6 and AppStoreDrawer.tsx:7 both use import * as icons from "lucide-react" to dynamically resolve icons by name. This pulls ~1,000+ icons into the client bundle. Tree-shaking cannot optimize dynamic key lookups like icons[pascalName].

Fix: Extract a map of only the icons referenced in products.json, switch all products to custom PNGs, or verify the bundle stays under the 150KB gzip performance budget.

High Duplicated icon rendering logic

getIcon(), CUSTOM_ICON_IDS, iconAlignmentClassMap, and badgeColorMap are copy-pasted between AppIcon.tsx and AppStoreDrawer.tsx. If a product or color changes, both files must be updated in lockstep.

Fix: Extract shared constants and helpers into src/lib/icon-utils.ts.

Medium

Medium Unused useIsMobile hook

src/hooks/useIsMobile.ts is not imported by any production component. The mock at Accessibility.test.tsx:5 is also stale — none of the components imported by that test use the hook.

Medium Slide and Assemble animation variants are dead code

page.tsx:9 hardcodes variant="boot". The SlidePhoneContent and AssemblePhoneContent components, their state, and their timer effects in PageContent.tsx are unreachable. These were kept after Phase 5 comparison — may be intentional.

Medium Duplicated reduced-motion wrapper

PageContent.tsx:196-219 — Two conditional branches produce identical motion.div wrappers with the same props. Can be merged into a single condition.

Medium StatusBar hydration mismatch

StatusBar.tsx:6-8useState(() => new Date().toLocaleTimeString(...)) bakes the build-time clock into the statically generated HTML. On client hydration, a different time is computed, causing a React hydration warning. Fix: initialize with empty string and set real time in useEffect, or add suppressHydrationWarning.

Low

Spec Conformance

Documented Decisions (no action needed)

Positive Observations

Validation