Commit Graph
18 Commits
Author SHA1 Message Date
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 746d81b151 fix: audit phase 2 — teardown, failure paths, service worker, tooltip, resize
Correctness phase of the frontend audit. Nothing here is visible on a happy
path; it is what the app does when something goes wrong, when the window
changes, or when the component goes away.

Teardown (new src/lib/pixi/lifecycle.ts):
- Nothing was ever released: no app.destroy(), no ticker.remove(), no
  killTweensOf, no onDestroy. ~480 GSAP tweens (most repeat:-1), a ticker
  callback, and a dozen DOM listeners lived forever.
- 112 of those tweens are created inside an onComplete, so they don't exist
  when a call-site sweep would run. gsap.context() handles exactly this:
  GSAP records each animation's context on creation and restores it while
  invoking that animation's callbacks (see _callback in gsap-core), so
  late-created pulses land in the same context. One ctx.kill() gets all of
  them. kill(), not revert(), so GSAP doesn't write properties back onto
  objects about to be destroyed.
- All DOM listeners now go through life.listen(), which registers removal at
  the same time. Previously 6 canvas listeners were never removed, each
  closing over the whole ~713-object scene graph.
- The unmount race: cleanup used to be registered on the LAST line of an
  async onMount, after the asset waterfall, so unmounting mid-load cleaned up
  nothing. The lifecycle is now created before any await, with life.disposed
  guards after each one.
- Hover tweens re-enter the context per event and use overwrite:'auto' so
  rapid sweeps don't stack conflicting tweens.

Failure paths:
- onMount had no try/catch and no WebGL detection. Any init or asset failure
  left the spinner up forever, the stats toggle at opacity:0, and Escape dead
  (attachKeyboard never ran). Now caught, and it hands off to +page.svelte to
  reveal the text version.
- There was no <noscript> anywhere, and SeoContent is clipped to 1x1px with
  html/body overflow:hidden — so JS-disabled visitors got a pure black page.
  A <noscript><style> block now un-hides it with no JS involved.

Service worker (moved static/ -> src/, so SvelteKit builds it):
- It can now use $service-worker, so precache comes from the real build
  manifest instead of a hand-maintained 5-entry list that omitted all JS/CSS,
  and the cache name embeds `version` so old caches actually get pruned.
- cache.addAll was atomic: one 404 failed the whole install silently. Now
  per-asset with a warning on partial failure.
- The catch returned undefined for non-image assets, which makes
  respondWith throw a TypeError — a soft offline failure became a hard one.
  Every path now resolves to a Response.
- Unhashed assets (icons, headshot, resume PDF) were cache-first with no
  expiry, so replacing the resume at the same URL never reached a returning
  visitor. Those are network-first now; only hashed build output is
  cache-first.
- Navigation responses were cached without a status check, so a 404/500 body
  could become the offline fallback.
- Dropped skipWaiting() from install and added a real update prompt +
  controllerchange reload. The SKIP_WAITING handler was previously dead code
  and the "check for updates periodically" comment described an interval that
  did not exist.

Tooltip:
- Wrote Svelte state on every canvas mousemove even with nothing hovered.
  Now gated on hover and coalesced to one write per frame; raw coords are
  still tracked cheaply so the first hover isn't positioned from stale data.
- Animated left/top (layout + paint per frame on an element with two
  color-mix shadows); now translate3d.
- Listener moved to window, so coordinates no longer freeze when a drag
  leaves the canvas.
- Flip logic measured a hardcoded 280x120; now measures the real element
  (actual height is 113, so tall tooltips mis-flipped near the bottom edge).
- Dropped role="tooltip" + aria-live="polite", which announced the full
  label and description on every hover. Nothing referenced it via
  aria-describedby, so the role was inert. Now aria-hidden.

Resize:
- Star positions, gradient radius, vignette size and dust wrap bounds were
  all baked at first paint, so enlarging the window left an unlit band. The
  background is now rebuilt on a rAF-coalesced resize, under its own
  sub-lifecycle. The view is shifted by half the delta rather than re-centred,
  so the user's current view stays anchored.
- isTouchDevice used 'ontouchstart', true on touch-capable laptops and thus
  permanently killing mouse hover. Now matchMedia('(hover: hover)').

Known gap: node radii still don't change when the 768px breakpoint is
crossed mid-session; re-creating all 57 nodes would reset interaction state.

Verified in the production build: resize 1278x1220 -> 1734x821 rebuilds with
zero errors, tooltip positions via transform and stays on screen, fallback
renders as a readable resume page. svelte-check clean, lint clean, 14/14.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 00:27:30 -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 84112d8e42 refactor: extract PixiJS rendering into pixi/ modules
Phase 5 (behavior-preserving): decompose the ~1000-line onMount into focused
modules coordinated by a plain (non-reactive) controller object:
- pixi/types.ts        LineObject / NodeObject / BackgroundLayers
- pixi/controller.ts   shared mutable pan/zoom state + Pixi refs (NOT $state,
                       which would break PixiJS object identity)
- pixi/background.ts   gradient/nebula/stars/dust/vignette + ambient loops
- pixi/lines.ts        plasma-beam connections
- pixi/nodes.ts        origin + regular nodes; collapses the duplicated pointer
                       handlers into one attachNodeInteraction helper
- pixi/interaction.ts  pan/zoom/parallax + keyboard nav (return cleanups)
- pixi/animation.ts    sequenced entrance animation

onMount is now a thin orchestrator. SkillTree.svelte: 1177 -> 274 lines
(2453 at the start of the refactor). Listener passivity, the hasMoved click
guard, eventMode-until-animated ordering, and the R-reset parallax rebaseline
are all preserved.

svelte-check: 0 errors/0 warnings. lint/test/build clean. Verified onMount runs
with no runtime exceptions (background + stats panel render). NOTE: pan/zoom/
node-click/keyboard need a manual real-browser check (headless can't paint nodes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:23:54 -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
TehRiehlDealandClaude Opus 4.8 bf235680a4 refactor: model TreeNode as a discriminated union
Phase 3 (behavior-preserving):
- Replace the 45-optional-field TreeNode interface with a tagged union over
  category: OriginNode | ExperienceNode | ProjectNode | SkillNode | EducationNode,
  sharing a BaseNode. Category-specific fields now live only on their member.
- Modal template narrows by category (category === 'experience' && ...) instead
  of bare field-presence checks; equivalent at runtime since each field is
  exclusive to its category (proven by the union compiling against the data).
- stats utils narrow correctly: aggregateSkills filters to SkillNode[] via a type
  predicate; countByProficiency and getPlayerClass guard on category.

svelte-check: 0 errors (every data node conforms). lint/test/build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 09:59:25 -07:00
TehRiehlDealandClaude Opus 4.8 6091c62654 refactor: drive theme colors through CSS custom properties
Phase 2 (behavior-preserving):
- Add :root color tokens to app.css mirroring theme.ts cssVars
- Replace solid theme hex literals in SkillTree.svelte <style> with var(--..):
  #ffd700 -> --color-gold (x17), #a8a8b8 -> --text-muted (x8),
  #1a1a2e -> --panel-from, #0f0f1a -> --panel-to, #f9a825 -> --color-skills
- body background now var(--bg-space)

Values are identical to the literals; every referenced var has a :root
definition. rgba() shadows and one-off grays/badge colors left as-is.
check/lint/build all clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 09:54:29 -07:00
TehRiehlDealandClaude Opus 4.8 6c652e7138 refactor: centralize theme/config/utils + fix data and console bugs
Phase 1 of the cohesion refactor (behavior-preserving):

Single source of truth:
- New src/lib/theme.ts: category colors (moved from data.ts, re-exported
  for back-compat), derived hex strings, nebula palette, UI colors, CSS var map
- New src/lib/config.ts: breakpoint, zoom, parallax, node radii, particle
  counts, animation timings (previously scattered magic numbers)
- New src/lib/utils/tree.ts: getNodeById, getNodeRadius, getDistanceFromOrigin,
  getBeamIntensity (pure helpers lifted out of onMount)
- New src/lib/utils/stats.ts: branch ids/labels, proficiency order, and the
  stats-panel aggregation helpers (deduped from inline copies)
- SkillTree.svelte now imports all of the above; removed the duplicated
  getCategoryColorHex, stats helpers, branch id/label lists, and inline magic numbers
- Populated src/lib/index.ts barrel

Bug fixes:
- data.ts: tmdbAPI pypiUrl pointed at tvdbAPI -> corrected to tmdbAPI
- Removed/ gated stray console.log noise (skill-tree-loaded, SW logs)
- Removed dead vars (maxAlpha, glowMaxAlpha) and unused catch binding

Tooling: ESLint + Prettier now clean; added Vitest unit tests for the new
pure utils (14 tests). svelte-check: 0 errors. build: ok.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 09:40:27 -07:00
TehRiehlDealandClaude Opus 4.8 aa58425f1a style: apply Prettier formatting across codebase
Isolated reformat commit so subsequent logic changes have clean diffs.
No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 09:29:52 -07:00
TehRiehlDeal 5ffd531f3c Added Keyboard navigation, added parallax effect to the background, changed zoom to center on Origin node, increased accessibility, optimized lighthouse score 2026-01-15 22:38:37 -08:00
TehRiehlDeal eb961b752c Added node connection beam animations 2026-01-12 23:35:14 -08:00
TehRiehlDeal 13a57b39a0 Added Character Sheet style stats panel 2026-01-12 22:51:49 -08:00
TehRiehlDeal aa0464e12b Added welcome animation 2026-01-11 23:39:53 -08:00
TehRiehlDeal fb2df94db1 Added handling of disaplaying pypi link and maintain status of my projects 2026-01-11 18:17:53 -08:00
TehRiehlDeal b82aa75ff1 Added link to download my resume 2026-01-11 14:31:12 -08:00
TehRiehlDeal c279112778 Updated background to look a bit nicer, switched hero image, updated mobile responiveness 2026-01-11 12:07:44 -08:00
TehRiehlDeal 7f0f86d14e feat(skill-tree): add touch support and improve modal handling 2026-01-10 11:04:35 -08:00
TehRiehlDeal 3ad6b6edec Inital commit 2026-01-10 01:55:41 -08:00