Added accessibility support
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { type A11yState } from './state.js';
|
||||
type A11yContextValue = {
|
||||
reset: () => void;
|
||||
set: <K extends keyof A11yState>(key: K, value: A11yState[K]) => void;
|
||||
state: A11yState;
|
||||
};
|
||||
/**
|
||||
* Provides accessibility preferences, persists them in a cookie, and applies them
|
||||
* as data-attributes on <html> so the project's CSS can react. Like
|
||||
* ConsentProvider for cookies — wrap the app once; the widget/button consume it.
|
||||
*
|
||||
* The plugin ships NO styles: it only sets attributes (data-a11y-*). The project
|
||||
* writes CSS for those it supports (see docs/accessibility.md). This keeps the
|
||||
* design in the project's hands.
|
||||
*
|
||||
* // layout.tsx
|
||||
* import { AccessibilityProvider } from '@intecion/ipal-kit/client'
|
||||
* <AccessibilityProvider>{children}</AccessibilityProvider>
|
||||
*
|
||||
* To avoid a flash, the project can read the a11y-prefs cookie server-side and
|
||||
* set the attributes on <html> during SSR (see docs). This provider re-applies
|
||||
* on the client and keeps them in sync.
|
||||
*/
|
||||
export declare function AccessibilityProvider({ children }: {
|
||||
children: React.ReactNode;
|
||||
}): import("react/jsx-runtime").JSX.Element;
|
||||
/** Access accessibility preferences + setters. Use inside AccessibilityProvider. */
|
||||
export declare function useAccessibility(): A11yContextValue;
|
||||
export {};
|
||||
@@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
import { jsx as _jsx } from "react/jsx-runtime";
|
||||
import { createContext, use, useCallback, useEffect, useState } from 'react';
|
||||
import { A11Y_COOKIE, A11Y_COOKIE_MAX_AGE, A11Y_DEFAULT, a11yAttributes, parseA11y, serializeA11y } from './state.js';
|
||||
const A11yContext = /*#__PURE__*/ createContext(null);
|
||||
function readCookie(name) {
|
||||
if (typeof document === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
|
||||
return match ? decodeURIComponent(match[1]) : undefined;
|
||||
}
|
||||
/**
|
||||
* Provides accessibility preferences, persists them in a cookie, and applies them
|
||||
* as data-attributes on <html> so the project's CSS can react. Like
|
||||
* ConsentProvider for cookies — wrap the app once; the widget/button consume it.
|
||||
*
|
||||
* The plugin ships NO styles: it only sets attributes (data-a11y-*). The project
|
||||
* writes CSS for those it supports (see docs/accessibility.md). This keeps the
|
||||
* design in the project's hands.
|
||||
*
|
||||
* // layout.tsx
|
||||
* import { AccessibilityProvider } from '@intecion/ipal-kit/client'
|
||||
* <AccessibilityProvider>{children}</AccessibilityProvider>
|
||||
*
|
||||
* To avoid a flash, the project can read the a11y-prefs cookie server-side and
|
||||
* set the attributes on <html> during SSR (see docs). This provider re-applies
|
||||
* on the client and keeps them in sync.
|
||||
*/ export function AccessibilityProvider({ children }) {
|
||||
const [state, setState] = useState(A11Y_DEFAULT);
|
||||
// Hydrate from cookie on mount.
|
||||
useEffect(()=>{
|
||||
setState(parseA11y(readCookie(A11Y_COOKIE)));
|
||||
}, []);
|
||||
// Apply attributes to <html> whenever state changes.
|
||||
useEffect(()=>{
|
||||
const el = document.documentElement;
|
||||
const attrs = a11yAttributes(state);
|
||||
for (const [attr, value] of Object.entries(attrs)){
|
||||
if (value === null) {
|
||||
el.removeAttribute(attr);
|
||||
} else {
|
||||
el.setAttribute(attr, value);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
state
|
||||
]);
|
||||
const persist = useCallback((next)=>{
|
||||
document.cookie = `${A11Y_COOKIE}=${encodeURIComponent(serializeA11y(next))}; path=/; max-age=${A11Y_COOKIE_MAX_AGE}; samesite=lax`;
|
||||
}, []);
|
||||
const set = useCallback((key, value)=>{
|
||||
setState((prev)=>{
|
||||
const next = {
|
||||
...prev,
|
||||
[key]: value
|
||||
};
|
||||
persist(next);
|
||||
return next;
|
||||
});
|
||||
}, [
|
||||
persist
|
||||
]);
|
||||
const reset = useCallback(()=>{
|
||||
setState(A11Y_DEFAULT);
|
||||
persist(A11Y_DEFAULT);
|
||||
}, [
|
||||
persist
|
||||
]);
|
||||
return /*#__PURE__*/ _jsx(A11yContext, {
|
||||
value: {
|
||||
reset,
|
||||
set,
|
||||
state
|
||||
},
|
||||
children: children
|
||||
});
|
||||
}
|
||||
/** Access accessibility preferences + setters. Use inside AccessibilityProvider. */ export function useAccessibility() {
|
||||
const ctx = use(A11yContext);
|
||||
if (!ctx) {
|
||||
throw new Error('useAccessibility must be used within <AccessibilityProvider>');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=AccessibilityProvider.js.map
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,41 @@
|
||||
export type A11yTexts = {
|
||||
bigCursor?: string;
|
||||
close?: string;
|
||||
contrast?: string;
|
||||
contrastHigh?: string;
|
||||
contrastInverted?: string;
|
||||
grayscale?: string;
|
||||
lineHeight?: string;
|
||||
open?: string;
|
||||
readableFont?: string;
|
||||
reduceMotion?: string;
|
||||
reset?: string;
|
||||
textSize?: string;
|
||||
title?: string;
|
||||
underlineLinks?: string;
|
||||
};
|
||||
export type A11yClassNames = {
|
||||
active?: string;
|
||||
button?: string;
|
||||
closeButton?: string;
|
||||
control?: string;
|
||||
label?: string;
|
||||
panel?: string;
|
||||
resetButton?: string;
|
||||
row?: string;
|
||||
};
|
||||
/**
|
||||
* Accessibility toolbar: a floating button that opens a panel of options (text
|
||||
* size, line height, contrast, grayscale, underline links, readable font, reduce
|
||||
* motion, big cursor). Choices persist in a cookie and apply as data-attributes
|
||||
* on <html> (the project's CSS styles them).
|
||||
*
|
||||
* Unstyled by default — pass classNames to match the project's design (like
|
||||
* CookieBanner). Wrap the app in <AccessibilityProvider> first.
|
||||
*
|
||||
* <AccessibilityWidget classNames={{ button: 'a11y-btn', panel: 'a11y-panel' }} />
|
||||
*/
|
||||
export declare function AccessibilityWidget({ classNames, texts, }: {
|
||||
classNames?: A11yClassNames;
|
||||
texts?: A11yTexts;
|
||||
}): import("react/jsx-runtime").JSX.Element;
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
'use client';
|
||||
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
||||
import { useState } from 'react';
|
||||
import { useAccessibility } from './AccessibilityProvider.js';
|
||||
const DEFAULT_TEXTS = {
|
||||
bigCursor: 'Duży kursor',
|
||||
close: 'Zamknij',
|
||||
contrast: 'Kontrast',
|
||||
contrastHigh: 'Wysoki',
|
||||
contrastInverted: 'Odwrócony',
|
||||
grayscale: 'Skala szarości',
|
||||
lineHeight: 'Odstęp między liniami',
|
||||
open: 'Otwórz panel dostępności',
|
||||
readableFont: 'Czytelna czcionka',
|
||||
reduceMotion: 'Wyłącz animacje',
|
||||
reset: 'Resetuj',
|
||||
textSize: 'Rozmiar tekstu',
|
||||
title: 'Dostępność',
|
||||
underlineLinks: 'Podkreśl linki'
|
||||
};
|
||||
/**
|
||||
* Accessibility toolbar: a floating button that opens a panel of options (text
|
||||
* size, line height, contrast, grayscale, underline links, readable font, reduce
|
||||
* motion, big cursor). Choices persist in a cookie and apply as data-attributes
|
||||
* on <html> (the project's CSS styles them).
|
||||
*
|
||||
* Unstyled by default — pass classNames to match the project's design (like
|
||||
* CookieBanner). Wrap the app in <AccessibilityProvider> first.
|
||||
*
|
||||
* <AccessibilityWidget classNames={{ button: 'a11y-btn', panel: 'a11y-panel' }} />
|
||||
*/ export function AccessibilityWidget({ classNames, texts }) {
|
||||
const { reset, set, state } = useAccessibility();
|
||||
const [open, setOpen] = useState(false);
|
||||
const t = {
|
||||
...DEFAULT_TEXTS,
|
||||
...texts
|
||||
};
|
||||
const cn = classNames ?? {};
|
||||
const toggle = (key)=>set(key, !state[key]);
|
||||
const isActive = (on)=>on ? cn.active ?? '' : '';
|
||||
return /*#__PURE__*/ _jsxs(_Fragment, {
|
||||
children: [
|
||||
/*#__PURE__*/ _jsx("button", {
|
||||
"aria-expanded": open,
|
||||
"aria-label": t.open,
|
||||
className: cn.button,
|
||||
onClick: ()=>setOpen((o)=>!o),
|
||||
type: "button",
|
||||
children: /*#__PURE__*/ _jsx("span", {
|
||||
"aria-hidden": "true",
|
||||
children: "♿"
|
||||
})
|
||||
}),
|
||||
open && /*#__PURE__*/ _jsxs("div", {
|
||||
"aria-label": t.title,
|
||||
className: cn.panel,
|
||||
role: "dialog",
|
||||
children: [
|
||||
/*#__PURE__*/ _jsxs("div", {
|
||||
className: cn.row,
|
||||
children: [
|
||||
/*#__PURE__*/ _jsx("span", {
|
||||
className: cn.label,
|
||||
children: t.textSize
|
||||
}),
|
||||
/*#__PURE__*/ _jsx("div", {
|
||||
className: cn.control,
|
||||
children: [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3
|
||||
].map((n)=>/*#__PURE__*/ _jsxs("button", {
|
||||
className: isActive(state.textSize === n),
|
||||
onClick: ()=>set('textSize', n),
|
||||
type: "button",
|
||||
children: [
|
||||
"A",
|
||||
n > 0 ? '+'.repeat(n) : ''
|
||||
]
|
||||
}, n))
|
||||
})
|
||||
]
|
||||
}),
|
||||
/*#__PURE__*/ _jsxs("div", {
|
||||
className: cn.row,
|
||||
children: [
|
||||
/*#__PURE__*/ _jsx("span", {
|
||||
className: cn.label,
|
||||
children: t.lineHeight
|
||||
}),
|
||||
/*#__PURE__*/ _jsx("div", {
|
||||
className: cn.control,
|
||||
children: [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
].map((n)=>/*#__PURE__*/ _jsx("button", {
|
||||
className: isActive(state.lineHeight === n),
|
||||
onClick: ()=>set('lineHeight', n),
|
||||
type: "button",
|
||||
children: n === 0 ? '—' : '≡'.repeat(n)
|
||||
}, n))
|
||||
})
|
||||
]
|
||||
}),
|
||||
/*#__PURE__*/ _jsxs("div", {
|
||||
className: cn.row,
|
||||
children: [
|
||||
/*#__PURE__*/ _jsx("span", {
|
||||
className: cn.label,
|
||||
children: t.contrast
|
||||
}),
|
||||
/*#__PURE__*/ _jsxs("div", {
|
||||
className: cn.control,
|
||||
children: [
|
||||
/*#__PURE__*/ _jsx("button", {
|
||||
className: isActive(state.contrast === 'high'),
|
||||
onClick: ()=>set('contrast', state.contrast === 'high' ? 'default' : 'high'),
|
||||
type: "button",
|
||||
children: t.contrastHigh
|
||||
}),
|
||||
/*#__PURE__*/ _jsx("button", {
|
||||
className: isActive(state.contrast === 'inverted'),
|
||||
onClick: ()=>set('contrast', state.contrast === 'inverted' ? 'default' : 'inverted'),
|
||||
type: "button",
|
||||
children: t.contrastInverted
|
||||
})
|
||||
]
|
||||
})
|
||||
]
|
||||
}),
|
||||
[
|
||||
[
|
||||
'grayscale',
|
||||
t.grayscale
|
||||
],
|
||||
[
|
||||
'underlineLinks',
|
||||
t.underlineLinks
|
||||
],
|
||||
[
|
||||
'readableFont',
|
||||
t.readableFont
|
||||
],
|
||||
[
|
||||
'reduceMotion',
|
||||
t.reduceMotion
|
||||
],
|
||||
[
|
||||
'bigCursor',
|
||||
t.bigCursor
|
||||
]
|
||||
].map(([key, label])=>/*#__PURE__*/ _jsxs("div", {
|
||||
className: cn.row,
|
||||
children: [
|
||||
/*#__PURE__*/ _jsx("span", {
|
||||
className: cn.label,
|
||||
children: label
|
||||
}),
|
||||
/*#__PURE__*/ _jsx("button", {
|
||||
"aria-pressed": Boolean(state[key]),
|
||||
className: `${cn.control ?? ''} ${isActive(Boolean(state[key]))}`,
|
||||
onClick: ()=>toggle(key),
|
||||
type: "button",
|
||||
children: state[key] ? 'ON' : 'OFF'
|
||||
})
|
||||
]
|
||||
}, key)),
|
||||
/*#__PURE__*/ _jsx("button", {
|
||||
className: cn.resetButton,
|
||||
onClick: reset,
|
||||
type: "button",
|
||||
children: t.reset
|
||||
}),
|
||||
/*#__PURE__*/ _jsx("button", {
|
||||
className: cn.closeButton,
|
||||
onClick: ()=>setOpen(false),
|
||||
type: "button",
|
||||
children: t.close
|
||||
})
|
||||
]
|
||||
})
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
//# sourceMappingURL=AccessibilityWidget.js.map
|
||||
File diff suppressed because one or more lines are too long
+5
@@ -0,0 +1,5 @@
|
||||
export { AccessibilityProvider, useAccessibility } from './AccessibilityProvider.js';
|
||||
export { AccessibilityWidget } from './AccessibilityWidget.js';
|
||||
export type { A11yClassNames, A11yTexts } from './AccessibilityWidget.js';
|
||||
export type { A11yState } from './state.js';
|
||||
export { A11Y_COOKIE, a11yAttributes, parseA11y } from './state.js';
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
'use client';
|
||||
export { AccessibilityProvider, useAccessibility } from './AccessibilityProvider.js';
|
||||
export { AccessibilityWidget } from './AccessibilityWidget.js';
|
||||
export { A11Y_COOKIE, a11yAttributes, parseA11y } from './state.js';
|
||||
|
||||
//# sourceMappingURL=client.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/accessibility/client.ts"],"sourcesContent":["'use client'\nexport { AccessibilityProvider, useAccessibility } from './AccessibilityProvider.js'\nexport { AccessibilityWidget } from './AccessibilityWidget.js'\nexport type { A11yClassNames, A11yTexts } from './AccessibilityWidget.js'\nexport type { A11yState } from './state.js'\nexport { A11Y_COOKIE, a11yAttributes, parseA11y } from './state.js'\n"],"names":["AccessibilityProvider","useAccessibility","AccessibilityWidget","A11Y_COOKIE","a11yAttributes","parseA11y"],"mappings":"AAAA;AACA,SAASA,qBAAqB,EAAEC,gBAAgB,QAAQ,6BAA4B;AACpF,SAASC,mBAAmB,QAAQ,2BAA0B;AAG9D,SAASC,WAAW,EAAEC,cAAc,EAAEC,SAAS,QAAQ,aAAY"}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export { AccessibilityProvider, useAccessibility } from './AccessibilityProvider.js';
|
||||
export { AccessibilityWidget } from './AccessibilityWidget.js';
|
||||
export type { A11yClassNames, A11yTexts } from './AccessibilityWidget.js';
|
||||
export { A11Y_COOKIE, A11Y_COOKIE_MAX_AGE, A11Y_DEFAULT, a11yAttributes, parseA11y, serializeA11y, } from './state.js';
|
||||
export type { A11yState } from './state.js';
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
export { AccessibilityProvider, useAccessibility } from './AccessibilityProvider.js';
|
||||
export { AccessibilityWidget } from './AccessibilityWidget.js';
|
||||
export { A11Y_COOKIE, A11Y_COOKIE_MAX_AGE, A11Y_DEFAULT, a11yAttributes, parseA11y, serializeA11y } from './state.js';
|
||||
|
||||
//# sourceMappingURL=index.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/accessibility/index.ts"],"sourcesContent":["export { AccessibilityProvider, useAccessibility } from './AccessibilityProvider.js'\nexport { AccessibilityWidget } from './AccessibilityWidget.js'\nexport type { A11yClassNames, A11yTexts } from './AccessibilityWidget.js'\nexport {\n A11Y_COOKIE,\n A11Y_COOKIE_MAX_AGE,\n A11Y_DEFAULT,\n a11yAttributes,\n parseA11y,\n serializeA11y,\n} from './state.js'\nexport type { A11yState } from './state.js'\n"],"names":["AccessibilityProvider","useAccessibility","AccessibilityWidget","A11Y_COOKIE","A11Y_COOKIE_MAX_AGE","A11Y_DEFAULT","a11yAttributes","parseA11y","serializeA11y"],"mappings":"AAAA,SAASA,qBAAqB,EAAEC,gBAAgB,QAAQ,6BAA4B;AACpF,SAASC,mBAAmB,QAAQ,2BAA0B;AAE9D,SACEC,WAAW,EACXC,mBAAmB,EACnBC,YAAY,EACZC,cAAc,EACdC,SAAS,EACTC,aAAa,QACR,aAAY"}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Accessibility preferences state. Each option maps to a data-attribute on
|
||||
* <html> (e.g. data-a11y-contrast="high"); the PROJECT's CSS reacts to those
|
||||
* attributes. The plugin sets the attributes and persists the choice — it does
|
||||
* NOT ship styles, so it never fights the project's design.
|
||||
*/
|
||||
export type A11yState = {
|
||||
/** Larger cursor. */
|
||||
bigCursor: boolean;
|
||||
/** 'default' | 'high' (high contrast) | 'inverted' (dark-on-light flip). */
|
||||
contrast: 'default' | 'high' | 'inverted';
|
||||
/** Grayscale filter on the whole page. */
|
||||
grayscale: boolean;
|
||||
/** Line spacing: 0 = default, 1..2 = looser. */
|
||||
lineHeight: 0 | 1 | 2;
|
||||
/** Readable font (project maps this to a dyslexia-friendly / simple font). */
|
||||
readableFont: boolean;
|
||||
/** Stop animations / transitions (prefers-reduced-motion equivalent). */
|
||||
reduceMotion: boolean;
|
||||
/** Text size step: 0 = default, 1..3 = larger. */
|
||||
textSize: 0 | 1 | 2 | 3;
|
||||
/** Underline all links (WCAG: don't rely on color alone). */
|
||||
underlineLinks: boolean;
|
||||
};
|
||||
export declare const A11Y_DEFAULT: A11yState;
|
||||
export declare const A11Y_COOKIE = "a11y-prefs";
|
||||
export declare const A11Y_COOKIE_MAX_AGE: number;
|
||||
/** Serialize for the cookie (compact). */
|
||||
export declare function serializeA11y(state: A11yState): string;
|
||||
/** Parse from the cookie; falls back to defaults on any bad value. */
|
||||
export declare function parseA11y(raw: null | string | undefined): A11yState;
|
||||
/**
|
||||
* Maps state → data-attributes to set on <html>. Returns { attr: value|null };
|
||||
* null means remove the attribute (option is at default). The project's CSS
|
||||
* targets these, e.g. `[data-a11y-contrast="high"] { … }`.
|
||||
*/
|
||||
export declare function a11yAttributes(state: A11yState): Record<string, null | string>;
|
||||
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Accessibility preferences state. Each option maps to a data-attribute on
|
||||
* <html> (e.g. data-a11y-contrast="high"); the PROJECT's CSS reacts to those
|
||||
* attributes. The plugin sets the attributes and persists the choice — it does
|
||||
* NOT ship styles, so it never fights the project's design.
|
||||
*/ export const A11Y_DEFAULT = {
|
||||
bigCursor: false,
|
||||
contrast: 'default',
|
||||
grayscale: false,
|
||||
lineHeight: 0,
|
||||
readableFont: false,
|
||||
reduceMotion: false,
|
||||
textSize: 0,
|
||||
underlineLinks: false
|
||||
};
|
||||
export const A11Y_COOKIE = 'a11y-prefs';
|
||||
export const A11Y_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year
|
||||
;
|
||||
/** Serialize for the cookie (compact). */ export function serializeA11y(state) {
|
||||
return JSON.stringify(state);
|
||||
}
|
||||
/** Parse from the cookie; falls back to defaults on any bad value. */ export function parseA11y(raw) {
|
||||
if (!raw) {
|
||||
return A11Y_DEFAULT;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return {
|
||||
...A11Y_DEFAULT,
|
||||
...parsed
|
||||
};
|
||||
} catch {
|
||||
return A11Y_DEFAULT;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Maps state → data-attributes to set on <html>. Returns { attr: value|null };
|
||||
* null means remove the attribute (option is at default). The project's CSS
|
||||
* targets these, e.g. `[data-a11y-contrast="high"] { … }`.
|
||||
*/ export function a11yAttributes(state) {
|
||||
return {
|
||||
'data-a11y-contrast': state.contrast !== 'default' ? state.contrast : null,
|
||||
'data-a11y-cursor': state.bigCursor ? 'big' : null,
|
||||
'data-a11y-font': state.readableFont ? 'readable' : null,
|
||||
'data-a11y-grayscale': state.grayscale ? 'on' : null,
|
||||
'data-a11y-line': state.lineHeight > 0 ? String(state.lineHeight) : null,
|
||||
'data-a11y-motion': state.reduceMotion ? 'reduce' : null,
|
||||
'data-a11y-text': state.textSize > 0 ? String(state.textSize) : null,
|
||||
'data-a11y-underline': state.underlineLinks ? 'on' : null
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=state.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/accessibility/state.ts"],"sourcesContent":["/**\n * Accessibility preferences state. Each option maps to a data-attribute on\n * <html> (e.g. data-a11y-contrast=\"high\"); the PROJECT's CSS reacts to those\n * attributes. The plugin sets the attributes and persists the choice — it does\n * NOT ship styles, so it never fights the project's design.\n */\nexport type A11yState = {\n /** Larger cursor. */\n bigCursor: boolean\n /** 'default' | 'high' (high contrast) | 'inverted' (dark-on-light flip). */\n contrast: 'default' | 'high' | 'inverted'\n /** Grayscale filter on the whole page. */\n grayscale: boolean\n /** Line spacing: 0 = default, 1..2 = looser. */\n lineHeight: 0 | 1 | 2\n /** Readable font (project maps this to a dyslexia-friendly / simple font). */\n readableFont: boolean\n /** Stop animations / transitions (prefers-reduced-motion equivalent). */\n reduceMotion: boolean\n /** Text size step: 0 = default, 1..3 = larger. */\n textSize: 0 | 1 | 2 | 3\n /** Underline all links (WCAG: don't rely on color alone). */\n underlineLinks: boolean\n}\n\nexport const A11Y_DEFAULT: A11yState = {\n bigCursor: false,\n contrast: 'default',\n grayscale: false,\n lineHeight: 0,\n readableFont: false,\n reduceMotion: false,\n textSize: 0,\n underlineLinks: false,\n}\n\nexport const A11Y_COOKIE = 'a11y-prefs'\nexport const A11Y_COOKIE_MAX_AGE = 60 * 60 * 24 * 365 // 1 year\n\n/** Serialize for the cookie (compact). */\nexport function serializeA11y(state: A11yState): string {\n return JSON.stringify(state)\n}\n\n/** Parse from the cookie; falls back to defaults on any bad value. */\nexport function parseA11y(raw: null | string | undefined): A11yState {\n if (!raw) {return A11Y_DEFAULT}\n try {\n const parsed = JSON.parse(raw) as Partial<A11yState>\n return { ...A11Y_DEFAULT, ...parsed }\n } catch {\n return A11Y_DEFAULT\n }\n}\n\n/**\n * Maps state → data-attributes to set on <html>. Returns { attr: value|null };\n * null means remove the attribute (option is at default). The project's CSS\n * targets these, e.g. `[data-a11y-contrast=\"high\"] { … }`.\n */\nexport function a11yAttributes(state: A11yState): Record<string, null | string> {\n return {\n 'data-a11y-contrast': state.contrast !== 'default' ? state.contrast : null,\n 'data-a11y-cursor': state.bigCursor ? 'big' : null,\n 'data-a11y-font': state.readableFont ? 'readable' : null,\n 'data-a11y-grayscale': state.grayscale ? 'on' : null,\n 'data-a11y-line': state.lineHeight > 0 ? String(state.lineHeight) : null,\n 'data-a11y-motion': state.reduceMotion ? 'reduce' : null,\n 'data-a11y-text': state.textSize > 0 ? String(state.textSize) : null,\n 'data-a11y-underline': state.underlineLinks ? 'on' : null,\n }\n}\n"],"names":["A11Y_DEFAULT","bigCursor","contrast","grayscale","lineHeight","readableFont","reduceMotion","textSize","underlineLinks","A11Y_COOKIE","A11Y_COOKIE_MAX_AGE","serializeA11y","state","JSON","stringify","parseA11y","raw","parsed","parse","a11yAttributes","String"],"mappings":"AAAA;;;;;CAKC,GAoBD,OAAO,MAAMA,eAA0B;IACrCC,WAAW;IACXC,UAAU;IACVC,WAAW;IACXC,YAAY;IACZC,cAAc;IACdC,cAAc;IACdC,UAAU;IACVC,gBAAgB;AAClB,EAAC;AAED,OAAO,MAAMC,cAAc,aAAY;AACvC,OAAO,MAAMC,sBAAsB,KAAK,KAAK,KAAK,IAAI,SAAS;CAAV;AAErD,wCAAwC,GACxC,OAAO,SAASC,cAAcC,KAAgB;IAC5C,OAAOC,KAAKC,SAAS,CAACF;AACxB;AAEA,oEAAoE,GACpE,OAAO,SAASG,UAAUC,GAA8B;IACtD,IAAI,CAACA,KAAK;QAAC,OAAOhB;IAAY;IAC9B,IAAI;QACF,MAAMiB,SAASJ,KAAKK,KAAK,CAACF;QAC1B,OAAO;YAAE,GAAGhB,YAAY;YAAE,GAAGiB,MAAM;QAAC;IACtC,EAAE,OAAM;QACN,OAAOjB;IACT;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASmB,eAAeP,KAAgB;IAC7C,OAAO;QACL,sBAAsBA,MAAMV,QAAQ,KAAK,YAAYU,MAAMV,QAAQ,GAAG;QACtE,oBAAoBU,MAAMX,SAAS,GAAG,QAAQ;QAC9C,kBAAkBW,MAAMP,YAAY,GAAG,aAAa;QACpD,uBAAuBO,MAAMT,SAAS,GAAG,OAAO;QAChD,kBAAkBS,MAAMR,UAAU,GAAG,IAAIgB,OAAOR,MAAMR,UAAU,IAAI;QACpE,oBAAoBQ,MAAMN,YAAY,GAAG,WAAW;QACpD,kBAAkBM,MAAML,QAAQ,GAAG,IAAIa,OAAOR,MAAML,QAAQ,IAAI;QAChE,uBAAuBK,MAAMJ,cAAc,GAAG,OAAO;IACvD;AACF"}
|
||||
Reference in New Issue
Block a user