Brand guidelines (docs/brand/magnotia-brand-guidelines.md §4) say "Never go below 12px for any text" and "tertiary text must be ≥18px bold or ≥24px regular". Audit found 246 sub-12px className sites and 166 cases of text-text-tertiary paired with body sizes. Bumped text-[9/10/11px] → text-[12px] across 26 .svelte files in src/lib and src/routes. Promoted text-text-tertiary → text-text-secondary at body sizes (12-14px) where context allowed; left placeholder pseudo-states, ternary-conditional inactive states, and line-through "done" states alone (8 residual pairings, all decorative). Affects neurodivergent users on 1366×768 budget hardware most directly. svelte-check: 0 errors, 0 warnings.
87 lines
3.0 KiB
Svelte
87 lines
3.0 KiB
Svelte
<script lang="ts">
|
|
// Phase 9. Progressive-disclosure wrapper around the native <details>
|
|
// element. The chevron animates on open via a CSS transform; height
|
|
// animation uses interpolate-size where the engine supports it
|
|
// (modern Chromium / WebKit), and falls back to instant open/close
|
|
// elsewhere. prefers-reduced-motion disables both.
|
|
import { ChevronRight } from "lucide-svelte";
|
|
import type { Component } from "svelte";
|
|
|
|
interface Props {
|
|
title: string;
|
|
description?: string;
|
|
open?: boolean;
|
|
onopen?: () => void;
|
|
icon?: Component<{ size?: number; class?: string; "aria-hidden"?: boolean | "true" | "false" }>;
|
|
children?: import("svelte").Snippet;
|
|
}
|
|
|
|
let { title, description = "", open = false, onopen, icon: Icon, children }: Props = $props();
|
|
|
|
// Fires once on the first transition from closed → open. Used by
|
|
// sections that lazy-load expensive data (e.g. TTS voice enumeration).
|
|
// Plain let (not $state) — the flag is mutated only by the toggle
|
|
// handler and never has to be reactive in the template.
|
|
let hasOpened = false;
|
|
let detailsEl: HTMLDetailsElement | null = $state(null);
|
|
|
|
function handleToggle(event: Event) {
|
|
const el = event.currentTarget as HTMLDetailsElement | null;
|
|
if (el?.open && !hasOpened) {
|
|
hasOpened = true;
|
|
onopen?.();
|
|
}
|
|
}
|
|
|
|
// Push prop changes to the DOM. The native <details> open attribute
|
|
// is mutated by user interaction outside Svelte's reactive graph;
|
|
// when the parent later changes the open prop (for example because a
|
|
// search filter expanded a different group), $effect force-syncs.
|
|
// User-driven toggles where the prop value did not change keep their
|
|
// local state because $effect only re-runs on prop diff.
|
|
$effect(() => {
|
|
if (detailsEl && detailsEl.open !== open) detailsEl.open = open;
|
|
});
|
|
</script>
|
|
|
|
<details bind:this={detailsEl} {open} ontoggle={handleToggle} class="settings-group border-t border-border-subtle">
|
|
<summary
|
|
class="flex items-start gap-2 cursor-pointer list-none py-3 px-1 rounded hover:bg-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent"
|
|
>
|
|
<ChevronRight
|
|
class="chevron size-4 mt-1 shrink-0 text-text-tertiary"
|
|
aria-hidden="true"
|
|
/>
|
|
{#if Icon}
|
|
<Icon size={16} class="mt-1 shrink-0 text-text-tertiary" aria-hidden="true" />
|
|
{/if}
|
|
<div class="flex-1">
|
|
<p class="text-sm text-text font-semibold">{title}</p>
|
|
{#if description}
|
|
<p class="text-[12px] text-text-secondary tracking-wide mt-1">{description}</p>
|
|
{/if}
|
|
</div>
|
|
</summary>
|
|
<div class="pl-6 pr-2 pb-3">
|
|
{@render children?.()}
|
|
</div>
|
|
</details>
|
|
|
|
<style>
|
|
.settings-group :global(summary)::-webkit-details-marker {
|
|
display: none;
|
|
}
|
|
:global(.chevron) {
|
|
transition: transform 180ms ease;
|
|
}
|
|
details[open] > summary :global(.chevron) {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
:global(.chevron) {
|
|
transition: none;
|
|
}
|
|
}
|
|
</style>
|