Commit Graph
4 Commits
Author SHA1 Message Date
TehRiehlDealandClaude Opus 5 c4f1fd9f84 style: audit phase 5 — real typography, design tokens, AA contrast
Typography:
- The codebase contained exactly one font-family declaration
  (LoadingIndicator), naming 'Cinzel Decorative' and 'Trajan Pro' — neither
  loaded, bundled, or present in static/. So every word on the site, in every
  component, rendered in the browser's default serif. For a site whose whole
  premise is atmosphere, that was the largest craft gap.
- Now a display/body pairing: self-hosted Cinzel Variable for the name,
  headings, welcome title and tooltip titles; a system stack for body and UI
  copy, which costs nothing to load and is far more legible than Times at the
  13-14px the modal and stats panel use.
- @fontsource-variable/cinzel ships two subsets with unicode-range, so only
  the 25.3KB latin file is fetched for this content (verified — latin-ext is
  emitted but never requested). font-display: swap, so text never blocks.

Contrast (WCAG 1.4.3):
- Measured every grey against the panel background (#1a1a2e) rather than
  eyeballing it. #666 is 2.97:1 and fails AA for normal text; it was used for
  the modal close glyph, the tooltip hint, the branch count and the skill
  years. Replaced with --text-subtle (#9a9aa8, 6.1:1).
- Correction to the audit: #888 was listed as failing. It measures 4.81:1 and
  passes. Tokenised for consistency, not because it was broken.
  --text-muted (#a8a8b8) is 7.3:1, not the ~9:1 I claimed.

Tokens:
- z-index was nine ad-hoc literals across seven files, with LoadingIndicator
  and the modal backdrop both on 1000. Now a named scale, and no raw z-index
  literal survives anywhere in src/ (verified against the live stylesheet).
- Added spacing, radius, motion and type tokens alongside the existing colour
  ones, so the system covers more than colour.

Note: package-lock.json in this commit also carries pre-existing churn from
before this work; adding the font dependency entangled the two.

Verified in the production build: Cinzel loads and applies to headings,
computed body font resolves to system-ui, one font file fetched, zero raw
z-index literals in the rendered CSS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 01:11:21 -07:00
TehRiehlDealandClaude Opus 5 75d0584952 a11y: audit phase 3 — keyboard access, reduced motion, focus management
The site claimed comprehensive accessibility. It had a Level A keyboard
failure, no reduced-motion support of any kind, and a dialog whose ARIA
actively lied about the page state.

Keyboard access to nodes (WCAG 2.1.1, Level A):
- Nodes are PixiJS Containers with no DOM representation, so there was no way
  to reach or activate one by keyboard. The only keyboard vocabulary was
  pan/zoom/reset. New TreeNavigator.svelte renders the same data.ts as a real
  WAI-ARIA tree: roving tabindex, Up/Down traversal, Right/Left to expand and
  collapse, Home/End, Enter/Space to open the same modal a click opens.
  Focusing a row pans the canvas to that node so the two stay in sync.
- The canvas advertised role="application", which tells screen readers to
  suppress their own navigation and forward every key — appropriate only for
  a widget implementing a full keyboard model, which it did not. Now
  role="img" with a description, still focusable for pan/zoom.
- .sr-only content is revealed on :focus-within. Previously ~20 links were in
  the tab order while clipped to 1x1px, so a sighted keyboard user tabbed into
  nothing (WCAG 2.4.7). Added a skip link as the first focusable element.

Focus management:
- The modal set aria-modal="true" — telling AT that everything outside does
  not exist — while focus stayed on the canvas behind it and Tab walked the
  hidden text version underneath. Focus now moves into the dialog on open, is
  trapped there, and is restored to the trigger on close.
- Removed two onkeydown Escape handlers (modal + stats backdrop) that could
  never fire: a role="presentation" div with no tabindex is never a keydown
  target. Their aria-labels were discarded by the role too.
- The 300ms mobile touch-bleed guard was applied to every close path, so
  Escape or the X immediately after opening silently did nothing. It now
  guards only outside-dismissal, which is what it was for.

Reduced motion (WCAG 2.3.3 / 2.2.2):
- Previously respected nowhere, on a site built from a 4.5s forced zoom, an
  ~8s staggered entrance, ~480 looping tweens and a continuous dust ticker.
  The entrance now jumps to the finished state, the dust ticker is not
  registered, and all six ambient loops route through one guard that simply
  never creates them. Plus the CSS media query the GSAP tweens can't reach.

Other:
- StatsPanel used aria-hidden while containing tabindex="0" children — an axe
  aria-hidden-focus violation. Now `inert`, which removes it from the a11y
  tree and the tab order together.
- Branch headers were div+role="button" handling Enter but not Space. Now
  real buttons with aria-controls.
- The global keydown listener had no scope guard: it preventDefault()ed arrow
  keys unconditionally, so the 380px scrollable stats panel could not be
  scrolled by keyboard, and Ctrl combos were not excluded. Ctrl+wheel (browser
  and trackpad zoom) is no longer swallowed either.
- Focus rings lived in SkillTree's *scoped* style block, so they never reached
  NodeModal or StatsPanel children. Moved to app.css.
- Second <h1> in StatsPanel demoted; the page heading is SeoContent's.
- getBeamIntensity used a truthiness check on yearsOfExperience, so 0 fell
  through to the default.

Tests 16 -> 27: reduced-motion helper (all four branches), hierarchy building
including cycle/orphan handling, and a data-integrity check that every
parentId resolves to a real node.

Verified in the production build: Tab reveals the text version with a visible
focus ring; arrow keys traverse the tree and expand/collapse (rows 5->6);
Enter opens the modal with focus landing on Close; Shift+Tab stays trapped;
Escape restores focus to the originating tree item.

Not verified end-to-end: the reduced-motion JS branch. This environment can't
emulate the media query — patching matchMedia after navigation always races
module init — so that branch is covered by unit tests and review, not
observation. Worth a manual pass with DevTools > Rendering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 01:03:36 -07:00
TehRiehlDealandClaude Opus 5 8874b35998 perf+fix: audit phase 0/1 — parallel textures, dead weight, two real bugs
Findings from a full frontend audit (static read + live instrumentation of
production). This covers the "bleeding" and performance phases.

Bug fixes:
- StatsPanel branch collapse/expand never re-rendered. Two Svelte 5 defects:
  $state(new Set()) is not proxied (proxy.js bails on non-object/array
  prototypes), so .add()/.delete() were untracked; and the Svelte 4
  `x = x` reactivity idiom is a no-op in runes mode, where sources use a
  strict `equals` comparator. Switched to SvelteSet and dropped the
  self-assignment. Verified: aria-expanded now flips and rows 28<->24.
- The closed stats panel's gold box-shadow (8px offset + 80px blur) bled
  ~88px past its right edge, painting a permanent olive stripe down the
  left of the viewport on every page view. Offset the closed transform
  past the shadow.
- data.ts shipped a placeholder repoUrl (github.com/yourusername/...), a
  live 404 on the node describing this very site. Also removed the
  "Tailwind CSS" techStack claim; it is not a dependency of this project.

Performance:
- nodes.ts awaited Assets.load() inside the node loop, serialising ~56 icon
  round-trips before the tree could render or accept input. Now one
  parallel batch. Measured: all 78 tree icon requests start at ~28ms and
  complete within ~618ms.
- isLoading was cleared right after app.init(), before a single texture
  existed, so the spinner resolved onto an empty screen. Moved to after
  the scene graph is built.
- Renderer left antialias on (costly, ~no benefit on soft glow circles)
  and resolution/autoDensity at defaults (blurry on HiDPI). Now
  antialias:false, resolution:min(dpr,2), autoDensity, high-performance.
- cacheAsTexture on the fully-static gradient (16 full-screen fills) and
  vignette (13); interactiveChildren:false on background containers.
- Dust ticker allocated a fresh closure per frame via forEach; replaced
  with a named function and an indexed loop.

Dead weight:
- static/{headshot,profile}.jpg.original (8.6MB, 83% of the static payload)
  were committed and deployed verbatim by adapter-static, publicly
  fetchable and referenced by nothing. Moved to a gitignored assets-src/
  so the full-res sources stay on disk without shipping.
- precompress:true so nginx gzip_static/brotli_static have siblings to
  serve (the origin currently sends everything uncompressed).

build/ 11.13MB -> 3.7MB. JS wire 727.6KB raw -> 232KB gzip / 202.7KB brotli.
svelte-check clean, 14/14 tests pass.

Note: the matching nginx change lives in the gitignored homelab/default.conf
and must be applied to the container separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 00:06:01 -07:00
TehRiehlDealandClaude Opus 4.8 f08138718a refactor: extract leaf UI components from SkillTree.svelte
Phase 4 (behavior-preserving): split the monolithic component's DOM overlays
into focused children, each owning its markup + scoped CSS:
- LoadingIndicator.svelte, WelcomeTitle.svelte, Tooltip.svelte
- NodeModal.svelte (modal + ~280 lines CSS; onClose callback, category-narrowed)
- StatsPanel.svelte (toggle + backdrop + panel + ~340 lines CSS; onToggle/
  onToggleBranch callbacks). Kept the .stats-panel-toggle class name stable so
  the entrance-animation GSAP querySelector still resolves.

SkillTree.svelte drops from 2453 -> ~600 lines. Modernized the moved event
handlers (on:click -> onclick), clearing the 9 Svelte deprecation warnings.

svelte-check: 0 errors, 0 warnings. lint/test/build clean. Verified the stats
panel renders identically in-browser (LVL/mastery/branches all correct).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:10:17 -07:00