From e7f8f7d7d33f5a81b21fc59b677451cd4c234098 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 17:17:31 +0000 Subject: [PATCH 1/2] Initial plan From e8654493b8d6e0cd0d82a2ba44000e1ea7a180c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 17:26:49 +0000 Subject: [PATCH 2/2] feat: add structured data and accessibility motion/focus improvements Agent-Logs-Url: https://github.com/SpicyDevs/posterium-frontend/sessions/0df3f116-50c7-4d28-a686-e73557388f5d Co-authored-by: ikrdikhit <155148519+ikrdikhit@users.noreply.github.com> --- src/components/a11y/LiveRegion.tsx | 68 +++++++++++ .../builder/components/CommandPalette.tsx | 8 ++ .../builder/components/ImportDialogue.tsx | 6 + .../builder/components/layout/Inspector.tsx | 29 +++-- .../builder/components/layout/MobileDock.tsx | 2 + src/components/builder/index.tsx | 41 ++++--- .../dashboard/FilmReelSection/DesktopReel.tsx | 9 +- src/components/dashboard/HeroSection.tsx | 26 +++-- src/components/dashboard/Nav.tsx | 14 ++- src/components/examples/ExamplesPage.tsx | 2 +- src/components/seo/JsonLd.astro | 17 +++ src/components/seo/PageSEO.astro | 24 ++-- src/components/shared/Accordion.tsx | 9 +- src/components/shared/ExportMenu.tsx | 6 + src/components/shared/MainNavbar.tsx | 7 ++ src/components/shared/ProgressiveImage.tsx | 23 ++-- src/hooks/useAnimation.ts | 27 +++++ src/hooks/useFocusTrap.ts | 58 ++++++++++ src/layouts/BaseLayout.astro | 30 +++++ src/lib/dashboard/hooks/useScrollReel.ts | 12 +- src/lib/seo/schema.ts | 109 ++++++++++++++++++ src/pages/build.astro | 6 +- src/pages/examples.astro | 14 ++- src/pages/faq.astro | 12 +- src/pages/installation.astro | 16 ++- src/pages/privacy.astro | 2 +- src/pages/terms.astro | 2 +- src/styles/global.css | 10 ++ 28 files changed, 519 insertions(+), 70 deletions(-) create mode 100644 src/components/a11y/LiveRegion.tsx create mode 100644 src/components/seo/JsonLd.astro create mode 100644 src/hooks/useAnimation.ts create mode 100644 src/hooks/useFocusTrap.ts create mode 100644 src/lib/seo/schema.ts diff --git a/src/components/a11y/LiveRegion.tsx b/src/components/a11y/LiveRegion.tsx new file mode 100644 index 00000000..64bf8ee9 --- /dev/null +++ b/src/components/a11y/LiveRegion.tsx @@ -0,0 +1,68 @@ +import React, { createContext, memo, useCallback, useContext, useState } from 'react'; + +interface LiveRegionContextValue { + announce: (message: string, politeness?: 'polite' | 'assertive') => void; +} + +const LiveRegionContext = createContext(null); + +export const LiveRegionProvider: React.FC<{ children: React.ReactNode }> = memo(({ children }) => { + const [politeMessage, setPoliteMessage] = useState(''); + const [assertiveMessage, setAssertiveMessage] = useState(''); + + const announce = useCallback((message: string, politeness: 'polite' | 'assertive' = 'polite') => { + if (politeness === 'assertive') { + setAssertiveMessage(''); + window.setTimeout(() => setAssertiveMessage(message), 30); + return; + } + + setPoliteMessage(''); + window.setTimeout(() => setPoliteMessage(message), 30); + }, []); + + return ( + + {children} +
+ {politeMessage} +
+
+ {assertiveMessage} +
+
+ ); +}); + +LiveRegionProvider.displayName = 'LiveRegionProvider'; + +export const useLiveRegion = (): LiveRegionContextValue['announce'] => { + const ctx = useContext(LiveRegionContext); + return ctx?.announce ?? (() => undefined); +}; diff --git a/src/components/builder/components/CommandPalette.tsx b/src/components/builder/components/CommandPalette.tsx index ed4d3576..71d8a846 100644 --- a/src/components/builder/components/CommandPalette.tsx +++ b/src/components/builder/components/CommandPalette.tsx @@ -27,6 +27,7 @@ import { Command, X, } from 'lucide-react'; +import { useFocusTrap } from '@/hooks/useFocusTrap'; export interface PaletteCommand { id: string; @@ -60,6 +61,8 @@ const CommandPalette: React.FC = memo(({ isOpen, onClose, commands }) => const [recentIds, setRecentIds] = useState([]); const inputRef = useRef(null); const listRef = useRef(null); + const panelRef = useRef(null); + useFocusTrap(isOpen, panelRef, onClose); // FIX: Push state to history to intercept mobile back button useEffect(() => { @@ -235,6 +238,11 @@ const CommandPalette: React.FC = memo(({ isOpen, onClose, commands }) => `}
(({ isOpen, onClose, onLoad, anchorRef }) => { top: number; left: number; } | null>(null); + useFocusTrap(isOpen, popoverRef, onClose); useEffect(() => { if (!isOpen) return; @@ -70,6 +72,10 @@ const ImportDialog = memo(({ isOpen, onClose, onLoad, anchorRef }) => { return (
= memo(({ config, setConfig }) => { = memo(({ config, setConfig }) => {
} > - +
+ +
); }); diff --git a/src/components/builder/components/layout/MobileDock.tsx b/src/components/builder/components/layout/MobileDock.tsx index 82019b3e..5f11de22 100644 --- a/src/components/builder/components/layout/MobileDock.tsx +++ b/src/components/builder/components/layout/MobileDock.tsx @@ -58,7 +58,9 @@ const MobileDock: React.FC<{
diff --git a/src/components/dashboard/HeroSection.tsx b/src/components/dashboard/HeroSection.tsx index bfc2c9c9..23bddbd6 100644 --- a/src/components/dashboard/HeroSection.tsx +++ b/src/components/dashboard/HeroSection.tsx @@ -1,6 +1,7 @@ import { memo, useState, useCallback, useEffect, useRef } from 'react'; import { ArrowRight, ChevronLeft, ChevronRight } from 'lucide-react'; import { API } from '@/lib/dashboard/constants'; +import { useAnimation } from '@/hooks/useAnimation'; interface HeroPoster { id: string; @@ -101,6 +102,7 @@ const CORNER_STYLE = (c: (typeof CORNERS)[number]): React.CSSProperties => ({ }); const CyclingPoster = memo(() => { + const animationsAllowed = useAnimation(); const [activeIdx, setActiveIdx] = useState(0); const [transitioning, setTransitioning] = useState(false); const [loaded, setLoaded] = useState>({}); @@ -159,6 +161,11 @@ const CyclingPoster = memo(() => { const doTransition = useCallback( (next: number) => { if (next === activeIdxRef.current || transitioningRef.current || !isPosterReady(next)) return; + if (!animationsAllowed) { + setActiveIdx(next); + finishTransition(); + return; + } clearTransitionTimers(); transitioningRef.current = true; setTransitioning(true); @@ -170,7 +177,7 @@ const CyclingPoster = memo(() => { }, 50); guardTimerRef.current = setTimeout(finishTransition, 1400); }, - [clearTransitionTimers, finishTransition, isPosterReady] + [animationsAllowed, clearTransitionTimers, finishTransition, isPosterReady] ); const goTo = useCallback( @@ -181,12 +188,13 @@ const CyclingPoster = memo(() => { ); const restartInterval = useCallback(() => { + if (!animationsAllowed) return; if (intervalRef.current) clearInterval(intervalRef.current); intervalRef.current = setInterval(() => { if (!isVisibleRef.current || transitioningRef.current) return; goTo((activeIdxRef.current + 1) % TOTAL); }, 4500); - }, [goTo]); + }, [animationsAllowed, goTo]); useEffect(() => { if (!loaded[0]) return; @@ -278,7 +286,7 @@ const CyclingPoster = memo(() => { display: 'block', opacity: i === activeIdx ? 1 : 0, willChange: transitioning ? 'opacity' : 'auto', - transition: 'opacity 0.35s ease', + transition: animationsAllowed ? 'opacity 0.35s ease' : 'none', pointerEvents: 'none', }} /> @@ -293,9 +301,9 @@ const CyclingPoster = memo(() => { zIndex: 1, background: 'linear-gradient(110deg,#151310 25%,#1e1b16 50%,#151310 75%)', backgroundSize: '200% 100%', - animation: 'shimmer 1.6s linear infinite', - }} - /> + animation: animationsAllowed ? 'shimmer 1.6s linear infinite' : 'none', + }} + /> )} {failed[activeIdx] && (
{ width: i === activeIdx ? 20 : 6, height: 6, borderRadius: 3, - transition: 'all 0.3s cubic-bezier(0.16,1,0.3,1)', - flexShrink: 0, - }} + transition: animationsAllowed ? 'all 0.3s cubic-bezier(0.16,1,0.3,1)' : 'none', + flexShrink: 0, + }} /> ))}
diff --git a/src/components/dashboard/Nav.tsx b/src/components/dashboard/Nav.tsx index 67c3b034..3760bdd7 100644 --- a/src/components/dashboard/Nav.tsx +++ b/src/components/dashboard/Nav.tsx @@ -1,5 +1,7 @@ import { memo, useState, useCallback, useEffect } from 'react'; import { Github, Menu, X } from 'lucide-react'; +import { useRef } from 'react'; +import { useFocusTrap } from '@/hooks/useFocusTrap'; const NAV_LINKS = [ { label: 'Reel', href: '#reel' }, @@ -33,7 +35,9 @@ const Nav = memo(() => { const [visible, setVisible] = useState(false); const [scrolled, setScrolled] = useState(false); const [menuOpen, setMenuOpen] = useState(false); + const mobileMenuRef = useRef(null); const closeMenu = useCallback(() => setMenuOpen(false), []); + useFocusTrap(menuOpen && visible, mobileMenuRef, closeMenu); useEffect(() => { const update = () => { @@ -142,9 +146,10 @@ const Nav = memo(() => {