Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,21 @@ button {
button:active:not(:disabled) {
transition: transform 0.08s var(--ease-out);
}

/* ── Reduced motion: respect user preference ──────────────────────
Users with vestibular disorders or motion sensitivity expect
apps to minimise movement. We disable all CSS animations and
transitions globally, and components that use JS-driven
animation frames (e.g. GraphView physics) should check this
media query via window.matchMedia('(prefers-reduced-motion: reduce)').
─────────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
39 changes: 38 additions & 1 deletion src/lib/components/DetailPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,56 @@

let { memoryId, onclose, onneighborclick, onedit, children }: Props = $props();

let panelEl: HTMLElement | null = null;
let previouslyFocused: HTMLElement | null = null;

function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
onclose();
return;
}
// Focus trap: cycle Tab/Shift+Tab within the dialog.
if (e.key !== 'Tab' || !panelEl) return;
const focusable = panelEl.querySelectorAll<HTMLElement>(
'button, [href], input, textarea, select, [tabindex]:not([tabindex="-1"])',
);
if (focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey) {
if (document.activeElement === first || !panelEl!.contains(document.activeElement)) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
}

onMount(() => {
previouslyFocused = document.activeElement as HTMLElement;
window.addEventListener('keydown', handleKeydown);
// Move focus into the panel on open so screen readers announce it.
if (panelEl) {
const first = panelEl.querySelector<HTMLElement>(
'button, [href], input, [tabindex]:not([tabindex="-1"])',
);
if (first) {
first.focus();
} else {
panelEl.focus();
}
}
});
onDestroy(() => {
window.removeEventListener('keydown', handleKeydown);
// Restore focus to the element that opened the panel.
previouslyFocused?.focus();
});
</script>

Expand All @@ -35,7 +72,7 @@
></div>

<!-- Slide-in panel -->
<aside class="detail-panel" role="dialog" aria-modal="true">
<aside class="detail-panel" bind:this={panelEl} role="dialog" aria-modal="true" tabindex="-1">
{@render children?.()}
</aside>

Expand Down
12 changes: 10 additions & 2 deletions src/lib/components/GraphView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@
let calmFrames = 0;
let needRedraw = true;

// Respect user's reduced-motion preference. When true, physics is
// skipped entirely — nodes render at their initial positions and
// the RAF loop only redraws on hover/expand, not continuously.
const prefersReducedMotion =
typeof window !== 'undefined' &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches;

const INITIAL_SEED = 30;
const EXPAND_LIMIT = 5;
// ─── Initial seed: fetch recent memories ───────────────────────────
Expand Down Expand Up @@ -250,7 +257,7 @@
console.error('[GraphView] loadSeed() failed', err);
}
loading = false;
physicsActive = true;
physicsActive = !prefersReducedMotion;
calmFrames = 0;
needRedraw = true;
updateCounts();
Expand Down Expand Up @@ -478,7 +485,8 @@
}

// Always redraw if there are un-expanded nodes (they have pulse animation)
const hasPulse = nodes.some(n => !n.expanded);
// Skip pulse animation entirely when reduced motion is preferred.
const hasPulse = !prefersReducedMotion && nodes.some(n => !n.expanded);
if (needRedraw || hasPulse) {
draw(ctx);
if (!physicsActive) needRedraw = false;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/MemoryDetail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@
}

// Click handler for doc slug links — navigation wiring comes later (#207 phase 2).
function handleDocClick(slug: string) {
console.log('[MemoryDetail] doc slug clicked (navigation not yet wired):', slug);
function handleDocClick(_slug: string) {
// Intentional no-op until document navigation is wired.
}

// Trust feedback handler (#207)
Expand Down