diff --git a/docs/01-standards/accessibility.md b/docs/01-standards/accessibility.md index ae8b5ad..eb2dce7 100644 --- a/docs/01-standards/accessibility.md +++ b/docs/01-standards/accessibility.md @@ -5,3 +5,5 @@ The floor, not a cleanup pass. 1. **WCAG 2.0 AA (AODA) is the baseline:** semantic landmarks (`header`/`main`/`footer`/`nav`/`section`), one `h1` per page, accordion and nav keyboard-operable with correct ARIA, focus trapped in the nav overlay while open, visible `ring` focus states, decorative art `aria-hidden`. 2. **Respect `prefers-reduced-motion`**: ambient and entrance animations pause or reduce; every motion moment has a static equivalent, and nothing is readable only through motion. 3. **Contrast is checked in every theme scope**: AA contrast for text tokens (`muted-foreground` on `background` included) in the base theme and inside any inverse-color section. +4. **Self-starting motion carries a control**: anything that begins moving on its own and runs past a few seconds (the marquee drift) pairs with a keyboard-operable pause. A hover pause is not one: it reaches neither keyboard nor touch, and the reduced-motion path only serves the readers who set the preference. +5. **Each landmark of a kind is named**: a page with more than one `nav` labels each (`Primary`, `Footer`, `Menu`), since an unnamed pair is indistinguishable to the reader listing them. The name is passed at the call site, never baked into the component. diff --git a/src/app/design/page.tsx b/src/app/design/page.tsx index ac784f7..299023b 100644 --- a/src/app/design/page.tsx +++ b/src/app/design/page.tsx @@ -515,11 +515,11 @@ const DesignPage = () => {
Nav
- +Menu
- +Emblem
diff --git a/src/components/atoms/marquee.tsx b/src/components/atoms/marquee.tsx index f59e5a9..cb7ddab 100644 --- a/src/components/atoms/marquee.tsx +++ b/src/components/atoms/marquee.tsx @@ -1,45 +1,87 @@ -import { forwardRef, type HTMLAttributes } from 'react' +'use client' + +import { forwardRef, useState, type HTMLAttributes } from 'react' +import { PauseIcon, PlayIcon } from 'lucide-react' +import { Button } from '@/components/atoms/button' +import { Icon } from '@/components/atoms/icon' import { cn, cva, type VariantProps } from '@/utils/theme' const styles = { - // the drift is the only thing bringing offscreen entries into view, so under reduced - // motion the row hands that job to the reader as a plain scroller - root: cva([ + root: cva('relative w-full'), + viewport: cva([ 'w-full overflow-hidden', 'motion-reduce:overflow-x-auto motion-reduce:overscroll-x-contain' ]), - track: cva(['animate-marquee flex w-max', 'hover:paused', 'motion-reduce:animate-none'], { - variants: { - direction: { - left: '', - right: 'direction-reverse' + track: cva( + [ + 'animate-marquee flex w-max', + 'hover:paused data-[paused=true]:paused', + 'motion-reduce:animate-none' + ], + { + variants: { + direction: { + left: '', + right: 'direction-reverse' + } + }, + defaultVariants: { + direction: 'left' } - }, - defaultVariants: { - direction: 'left' } - }), + ), group: cva('flex shrink-0 items-stretch gap-8 pr-8'), - // the duplicate exists only to hide the loop's seam; scrolling it would just repeat - duplicate: cva('motion-reduce:hidden') + duplicate: cva('motion-reduce:hidden'), + control: cva(['absolute top-4 right-4 z-10', 'text-muted-foreground', 'motion-reduce:hidden']) } type MarqueeRef = HTMLDivElement -type MarqueeProps = HTMLAttributes{'// Menu //'}
diff --git a/src/components/templates/footer.tsx b/src/components/templates/footer.tsx index 4486d70..7d18a10 100644 --- a/src/components/templates/footer.tsx +++ b/src/components/templates/footer.tsx @@ -48,7 +48,7 @@ const Footer: FC