import { useEffect, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { apiFetch } from '../lib/api'; import { WidgetSkeleton } from './Skeleton'; import type { WidgetConfig, Position } from '../lib/types'; const ALL_FEATURES = [ { id: 'font', label: 'Font Size' }, { id: 'contrast', label: 'High Contrast' }, { id: 'dyslexia', label: 'Dyslexia Font' }, { id: 'grayscale', label: 'Grayscale' }, { id: 'reading-guide', label: 'Reading Guide' }, { id: 'animations', label: 'Pause Animations' }, { id: 'line-height', label: 'Line Height' }, { id: 'cursor', label: 'Large Cursor' }, { id: 'links', label: 'Highlight Links' }, { id: 'screen-mask', label: 'Screen Mask' }, ]; const POSITIONS: { value: Position; label: string }[] = [ { value: 'bottom-right', label: 'Bottom Right' }, { value: 'bottom-left', label: 'Bottom Left' }, { value: 'top-right', label: 'Top Right' }, { value: 'top-left', label: 'Top Left' }, ]; const DEFAULT_CONFIG: WidgetConfig = { enabled: true, color: '#3858e9', position: 'bottom-right', features: ['font', 'contrast', 'dyslexia', 'reading-guide', 'animations', 'line-height'], hide_powered_by: false, }; // ── Exact replica of the widget CSS, scoped to .wpr ────────── const WIDGET_SCOPED_CSS = ` .wpr *,.wpr *::before,.wpr *::after{box-sizing:border-box;margin:0;padding:0} .wpr{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:14px;line-height:1.4;color:#1e1e2e} .wpr .fab{width:52px;height:52px;border-radius:50%;border:2px solid rgba(255,255,255,.25);cursor:default;display:flex;align-items:center;justify-content:center;font-size:24px;color:#fff;box-shadow:0 2px 14px rgba(0,0,0,.28)} .wpr .panel{width:292px;background:#fff;border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,.18);overflow:hidden} .wpr .ph{display:flex;align-items:center;justify-content:space-between;padding:12px 14px 11px;border-bottom:1px solid #e5e7eb} .wpr .pt{font-size:14px;font-weight:700} .wpr .xb{background:none;border:none;cursor:default;font-size:17px;color:#6b7280;padding:4px 6px;border-radius:6px;line-height:1} .wpr .fs{padding:6px 8px 8px;max-height:380px;overflow-y:auto} .wpr .fr{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 8px;border-radius:8px;transition:background .12s,box-shadow .12s} .wpr .fr:hover{background:#f0f4ff;box-shadow:inset 0 0 0 2px var(--c)} .wpr .fi{font-size:20px;width:28px;flex-shrink:0;display:flex;align-items:center;justify-content:center;line-height:1;opacity:.75} .wpr .fl{font-size:13px;font-weight:500;flex:1} .wpr .tb{width:44px;height:24px;border-radius:12px;border:none;cursor:default;position:relative;background:#d1d5db;flex-shrink:0} .wpr .tb::after{content:'';position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.2)} .wpr .sbs{display:flex;align-items:center;gap:4px} .wpr .sb{padding:2px 9px;border:1px solid #d1d5db;border-radius:6px;background:#fff;cursor:default;font-size:12px;font-weight:700;color:#374151;line-height:1.8} .wpr .sv{font-size:11px;color:#6b7280;min-width:26px;text-align:center} .wpr .pf{padding:8px 14px;border-top:1px solid #e5e7eb;text-align:center;font-size:10px;color:#9ca3af;letter-spacing:.02em} .wpr .pf a{color:#9ca3af;text-decoration:none;font-weight:600} `; const PREVIEW_ICONS: Record = { font: 'Aa', contrast: '◑', dyslexia: '\u{1D4BB}', grayscale: '◌', 'reading-guide': '☰', animations: '⏸', 'line-height': '↕', cursor: '↖', links: '⬡', 'screen-mask': '▤', }; function WidgetPreview({ config }: { config: WidgetConfig }) { const c = config.color || '#3858e9'; const isTop = config.position.startsWith('top'); const isLeft = config.position.endsWith('left'); const fabStyle: React.CSSProperties = { background: c, position: 'absolute', ...(isTop ? { top: 16 } : { bottom: 16 }), ...(isLeft ? { left: 16 } : { right: 16 }), }; const panelStyle: React.CSSProperties = { position: 'absolute', ...(isTop ? { top: 76 } : { bottom: 76 }), ...(isLeft ? { left: 16 } : { right: 16 }), }; return ( <> {/* Mock page background */}
{/* FAB */} {/* Panel (always open in preview) */}
Accessibility
{ALL_FEATURES.filter((f) => config.features.includes(f.id)).length === 0 && (

No features selected.

)} {ALL_FEATURES.filter((f) => config.features.includes(f.id)).map((f) => (
{f.label} {f.id === 'font' && (
)} {f.id === 'line-height' && ( )} {f.id !== 'font' && f.id !== 'line-height' && ( ))}
{!config.hide_powered_by && (
Powered by AccessMate
)}
); } export default function WidgetConfigurator() { const qc = useQueryClient(); const { data: saved, isLoading } = useQuery({ queryKey: ['widget-config'], queryFn: () => apiFetch('/widget-config'), retry: false, }); const [config, setConfig] = useState(DEFAULT_CONFIG); const [saveOk, setSaveOk] = useState(false); // Hydrate from server once loaded. useEffect(() => { if (saved) setConfig(saved); }, [saved]); const effective = config; const save = useMutation({ mutationFn: () => apiFetch<{ saved: boolean }>('/widget-config', { method: 'POST', body: JSON.stringify(effective), }), onSuccess: () => { setSaveOk(true); qc.invalidateQueries({ queryKey: ['widget-config'] }); setTimeout(() => setSaveOk(false), 3000); }, }); const toggleFeature = (featureId: string) => { const next = effective.features.includes(featureId) ? effective.features.filter((f) => f !== featureId) : [...effective.features, featureId]; setConfig({ ...effective, features: next }); }; if (isLoading) return ; return (

Widget Configurator

Configure the public-facing accessibility toolbar.

{/* Controls */}
Features {ALL_FEATURES.map((f) => ( ))}
{saveOk && Saved.} {save.isError && ( {(save.error as Error).message} )}
{/* Live preview */}

Live Preview

); }