From 827cd9bcbca19d356e56a6810b72b028431ebe0d Mon Sep 17 00:00:00 2001 From: rasm-its Date: Sat, 26 Sep 2026 22:29:53 +0200 Subject: [PATCH] Added accessibility support --- dist/exports/client.d.ts | 2 + dist/exports/client.js | 1 + dist/exports/client.js.map | 2 +- dist/index.d.ts | 2 + dist/index.js | 1 + dist/index.js.map | 2 +- .../accessibility/AccessibilityProvider.d.ts | 29 +++ .../accessibility/AccessibilityProvider.js | 87 ++++++++ .../AccessibilityProvider.js.map | 1 + .../accessibility/AccessibilityWidget.d.ts | 41 ++++ .../accessibility/AccessibilityWidget.js | 188 ++++++++++++++++++ .../accessibility/AccessibilityWidget.js.map | 1 + dist/modules/accessibility/client.d.ts | 5 + dist/modules/accessibility/client.js | 6 + dist/modules/accessibility/client.js.map | 1 + dist/modules/accessibility/index.d.ts | 5 + dist/modules/accessibility/index.js | 5 + dist/modules/accessibility/index.js.map | 1 + dist/modules/accessibility/state.d.ts | 37 ++++ dist/modules/accessibility/state.js | 53 +++++ dist/modules/accessibility/state.js.map | 1 + docs/accessibility.md | 171 ++++++++++++++++ src/exports/client.ts | 8 +- src/index.ts | 8 + .../accessibility/AccessibilityProvider.tsx | 94 +++++++++ .../accessibility/AccessibilityWidget.tsx | 182 +++++++++++++++++ src/modules/accessibility/client.ts | 6 + src/modules/accessibility/index.ts | 12 ++ src/modules/accessibility/state.ts | 72 +++++++ 29 files changed, 1021 insertions(+), 3 deletions(-) create mode 100644 dist/modules/accessibility/AccessibilityProvider.d.ts create mode 100644 dist/modules/accessibility/AccessibilityProvider.js create mode 100644 dist/modules/accessibility/AccessibilityProvider.js.map create mode 100644 dist/modules/accessibility/AccessibilityWidget.d.ts create mode 100644 dist/modules/accessibility/AccessibilityWidget.js create mode 100644 dist/modules/accessibility/AccessibilityWidget.js.map create mode 100644 dist/modules/accessibility/client.d.ts create mode 100644 dist/modules/accessibility/client.js create mode 100644 dist/modules/accessibility/client.js.map create mode 100644 dist/modules/accessibility/index.d.ts create mode 100644 dist/modules/accessibility/index.js create mode 100644 dist/modules/accessibility/index.js.map create mode 100644 dist/modules/accessibility/state.d.ts create mode 100644 dist/modules/accessibility/state.js create mode 100644 dist/modules/accessibility/state.js.map create mode 100644 docs/accessibility.md create mode 100644 src/modules/accessibility/AccessibilityProvider.tsx create mode 100644 src/modules/accessibility/AccessibilityWidget.tsx create mode 100644 src/modules/accessibility/client.ts create mode 100644 src/modules/accessibility/index.ts create mode 100644 src/modules/accessibility/state.ts diff --git a/dist/exports/client.d.ts b/dist/exports/client.d.ts index d80f00a..a9acbac 100644 --- a/dist/exports/client.d.ts +++ b/dist/exports/client.d.ts @@ -1,5 +1,7 @@ export { MaskedField } from '../globals/SiteIntegrations/components/MaskedField.js'; export { TestEmailButton } from '../globals/SiteIntegrations/components/TestEmailButton.js'; +export { AccessibilityProvider, AccessibilityWidget, useAccessibility, } from '../modules/accessibility/client.js'; +export type { A11yClassNames, A11yState, A11yTexts } from '../modules/accessibility/client.js'; export { Analytics } from '../modules/analytics/client.js'; /** * Entry point: ipal-kit/client diff --git a/dist/exports/client.js b/dist/exports/client.js index 215c75d..b8cdf4c 100644 --- a/dist/exports/client.js +++ b/dist/exports/client.js @@ -1,6 +1,7 @@ 'use client'; export { MaskedField } from '../globals/SiteIntegrations/components/MaskedField.js'; export { TestEmailButton } from '../globals/SiteIntegrations/components/TestEmailButton.js'; +export { AccessibilityProvider, AccessibilityWidget, useAccessibility } from '../modules/accessibility/client.js'; export { Analytics } from '../modules/analytics/client.js'; /** * Entry point: ipal-kit/client diff --git a/dist/exports/client.js.map b/dist/exports/client.js.map index 67eff2d..359461c 100644 --- a/dist/exports/client.js.map +++ b/dist/exports/client.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["'use client'\nexport { MaskedField } from '../globals/SiteIntegrations/components/MaskedField.js'\nexport { TestEmailButton } from '../globals/SiteIntegrations/components/TestEmailButton.js'\nexport { Analytics } from '../modules/analytics/client.js'\n/**\n * Entry point: ipal-kit/client\n *\n * Client-side ('use client') exports — React hooks, providers, and UI\n * components. Kept separate from the main entry so server bundles don't pull in\n * client-only code.\n */\nexport {\n ConsentProvider,\n CookieBanner,\n CookieButton,\n useConsent,\n useConsentContext,\n} from '../modules/consent/client.js'\n\nexport type { CookieBannerClassNames } from '../modules/consent/client.js'\n// Pure i18n path helpers — no server/RSC deps, safe to import in client\n// components (e.g. a LanguageSwitcher that computes locale URLs on the client).\nexport {\n buildLocalizedPath,\n getLocaleCodes,\n getLocalizedSlugs,\n switchLocalePath,\n} from '../modules/i18n/index.js'\nexport type { I18nConfig, LocalizedSlugs } from '../modules/i18n/index.js'\nexport { Turnstile } from '../modules/turnstile/client.js'\nexport { TurnstileProvider, useTurnstile } from '../modules/turnstile/client.js'\nexport type { TurnstileProps } from '../modules/turnstile/client.js'\n"],"names":["MaskedField","TestEmailButton","Analytics","ConsentProvider","CookieBanner","CookieButton","useConsent","useConsentContext","buildLocalizedPath","getLocaleCodes","getLocalizedSlugs","switchLocalePath","Turnstile","TurnstileProvider","useTurnstile"],"mappings":"AAAA;AACA,SAASA,WAAW,QAAQ,wDAAuD;AACnF,SAASC,eAAe,QAAQ,4DAA2D;AAC3F,SAASC,SAAS,QAAQ,iCAAgC;AAC1D;;;;;;CAMC,GACD,SACEC,eAAe,EACfC,YAAY,EACZC,YAAY,EACZC,UAAU,EACVC,iBAAiB,QACZ,+BAA8B;AAGrC,wEAAwE;AACxE,gFAAgF;AAChF,SACEC,kBAAkB,EAClBC,cAAc,EACdC,iBAAiB,EACjBC,gBAAgB,QACX,2BAA0B;AAEjC,SAASC,SAAS,QAAQ,iCAAgC;AAC1D,SAASC,iBAAiB,EAAEC,YAAY,QAAQ,iCAAgC"} \ No newline at end of file +{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["'use client'\nexport { MaskedField } from '../globals/SiteIntegrations/components/MaskedField.js'\nexport { TestEmailButton } from '../globals/SiteIntegrations/components/TestEmailButton.js'\nexport {\n AccessibilityProvider,\n AccessibilityWidget,\n useAccessibility,\n} from '../modules/accessibility/client.js'\nexport type { A11yClassNames, A11yState, A11yTexts } from '../modules/accessibility/client.js'\n\nexport { Analytics } from '../modules/analytics/client.js'\n/**\n * Entry point: ipal-kit/client\n *\n * Client-side ('use client') exports — React hooks, providers, and UI\n * components. Kept separate from the main entry so server bundles don't pull in\n * client-only code.\n */\nexport {\n ConsentProvider,\n CookieBanner,\n CookieButton,\n useConsent,\n useConsentContext,\n} from '../modules/consent/client.js'\nexport type { CookieBannerClassNames } from '../modules/consent/client.js'\n// Pure i18n path helpers — no server/RSC deps, safe to import in client\n// components (e.g. a LanguageSwitcher that computes locale URLs on the client).\nexport {\n buildLocalizedPath,\n getLocaleCodes,\n getLocalizedSlugs,\n switchLocalePath,\n} from '../modules/i18n/index.js'\nexport type { I18nConfig, LocalizedSlugs } from '../modules/i18n/index.js'\nexport { Turnstile } from '../modules/turnstile/client.js'\nexport { TurnstileProvider, useTurnstile } from '../modules/turnstile/client.js'\nexport type { TurnstileProps } from '../modules/turnstile/client.js'\n"],"names":["MaskedField","TestEmailButton","AccessibilityProvider","AccessibilityWidget","useAccessibility","Analytics","ConsentProvider","CookieBanner","CookieButton","useConsent","useConsentContext","buildLocalizedPath","getLocaleCodes","getLocalizedSlugs","switchLocalePath","Turnstile","TurnstileProvider","useTurnstile"],"mappings":"AAAA;AACA,SAASA,WAAW,QAAQ,wDAAuD;AACnF,SAASC,eAAe,QAAQ,4DAA2D;AAC3F,SACEC,qBAAqB,EACrBC,mBAAmB,EACnBC,gBAAgB,QACX,qCAAoC;AAG3C,SAASC,SAAS,QAAQ,iCAAgC;AAC1D;;;;;;CAMC,GACD,SACEC,eAAe,EACfC,YAAY,EACZC,YAAY,EACZC,UAAU,EACVC,iBAAiB,QACZ,+BAA8B;AAErC,wEAAwE;AACxE,gFAAgF;AAChF,SACEC,kBAAkB,EAClBC,cAAc,EACdC,iBAAiB,EACjBC,gBAAgB,QACX,2BAA0B;AAEjC,SAASC,SAAS,QAAQ,iCAAgC;AAC1D,SAASC,iBAAiB,EAAEC,YAAY,QAAQ,iCAAgC"} \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts index 1aa0b9c..548be3c 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,5 +1,7 @@ export type { AccessOption, Role } from './modules/access/index.js'; export { adminOnly, adminOnlyField, adminOrEditor, adminOrEditorField, adminOrSelf, authenticated, hasMinimumRole, isAdmin, isEditor, requireRole, requireRoleField, ROLE_HIERARCHY, } from './modules/access/index.js'; +export { A11Y_COOKIE, A11Y_DEFAULT, a11yAttributes, parseA11y, serializeA11y, } from './modules/accessibility/index.js'; +export type { A11yState } from './modules/accessibility/index.js'; export type { AnalyticsConfig } from './modules/analytics/index.js'; export { getAnalyticsConfig } from './modules/analytics/index.js'; export { ACCEPT_ALL_CONSENT, CONSENT_CATEGORIES, CONSENT_COOKIE, CONSENT_MAX_AGE, CONSENT_VERSION, DEFAULT_CONSENT, getConsentTexts, parseConsent, REJECT_ALL_CONSENT, serializeConsent, setDefaultConsent, updateConsent, } from './modules/consent/index.js'; diff --git a/dist/index.js b/dist/index.js index ffbd78d..40231c9 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,4 +1,5 @@ export { adminOnly, adminOnlyField, adminOrEditor, adminOrEditorField, adminOrSelf, authenticated, hasMinimumRole, isAdmin, isEditor, requireRole, requireRoleField, ROLE_HIERARCHY } from './modules/access/index.js'; +export { A11Y_COOKIE, A11Y_DEFAULT, a11yAttributes, parseA11y, serializeA11y } from './modules/accessibility/index.js'; export { getAnalyticsConfig } from './modules/analytics/index.js'; export { ACCEPT_ALL_CONSENT, CONSENT_CATEGORIES, CONSENT_COOKIE, CONSENT_MAX_AGE, CONSENT_VERSION, DEFAULT_CONSENT, getConsentTexts, parseConsent, REJECT_ALL_CONSENT, serializeConsent, setDefaultConsent, updateConsent } from './modules/consent/index.js'; export { archiveFieldName, buildArchivePath, buildEntryPath, getArchiveEntries, parsePageParam, resolveRoute } from './modules/content/index.js'; diff --git a/dist/index.js.map b/dist/index.js.map index e355143..6c7c033 100644 --- a/dist/index.js.map +++ b/dist/index.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export type { AccessOption, Role } from './modules/access/index.js'\nexport {\n adminOnly,\n adminOnlyField,\n adminOrEditor,\n adminOrEditorField,\n adminOrSelf,\n authenticated,\n hasMinimumRole,\n isAdmin,\n isEditor,\n requireRole,\n requireRoleField,\n ROLE_HIERARCHY,\n} from './modules/access/index.js'\nexport type { AnalyticsConfig } from './modules/analytics/index.js'\nexport { getAnalyticsConfig } from './modules/analytics/index.js'\nexport {\n ACCEPT_ALL_CONSENT,\n CONSENT_CATEGORIES,\n CONSENT_COOKIE,\n CONSENT_MAX_AGE,\n CONSENT_VERSION,\n DEFAULT_CONSENT,\n getConsentTexts,\n parseConsent,\n REJECT_ALL_CONSENT,\n serializeConsent,\n setDefaultConsent,\n updateConsent,\n} from './modules/consent/index.js'\nexport type { ConsentCategory, ConsentState, ConsentTexts } from './modules/consent/index.js'\nexport type {\n ContentCollectionOption,\n ContentOption,\n ResolvedRoute,\n} from './modules/content/index.js'\nexport {\n archiveFieldName,\n buildArchivePath,\n buildEntryPath,\n getArchiveEntries,\n parsePageParam,\n resolveRoute,\n} from './modules/content/index.js'\nexport type { ArchiveEntries } from './modules/content/index.js'\nexport { graphAdapter } from './modules/email/graphAdapter.js'\nexport type { GraphAdapterArgs } from './modules/email/graphAdapter.js'\nexport { mailAdapter } from './modules/email/mailAdapter.js'\nexport type { MailAdapterArgs } from './modules/email/mailAdapter.js'\n// Imported straight from the file, NOT from ./modules/email/index.js — that\n// barrel re-exports sendEmail, which imports 'server-only' and would crash when\n// Payload loads the config (or runs generate:importmap) as a plain Node script.\nexport { panelSmtpAdapter } from './modules/email/panelSmtpAdapter.js'\nexport type { PanelSmtpAdapterArgs } from './modules/email/panelSmtpAdapter.js'\nexport { buildFormsPlugin } from './modules/forms/formsPluginConfig.js'\nexport type {\n FormsCollectionOverrides,\n FormsFieldsOverride,\n FormsOption,\n} from './modules/forms/types.js'\nexport { createContentHelpers } from './modules/frontend/index.js'\nexport {\n buildPreventDeleteSystemPage,\n buildRevalidateHook,\n buildValidateUniqueRole,\n setPublishedAtHook,\n trackSlugHistoryHook,\n} from './modules/hooks/index.js'\nexport type { I18nConfig, LocaleDefinition, LocalizedSlugs } from './modules/i18n/index.js'\nexport {\n buildLocalizedPath,\n getDefaultLocale,\n getLocaleCodes,\n getLocaleDefinition,\n getLocalizedSlugs,\n isValidLocale,\n LOCALE_COOKIE_NAME,\n matchAcceptLanguage,\n negotiateLocale,\n switchLocalePath,\n} from './modules/i18n/index.js'\nexport type { LocaleMiddlewareResult } from './modules/i18n/index.js'\nexport { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from './modules/i18n/index.js'\nexport { normalizeFilename, normalizeFilenameHook } from './modules/media/index.js'\nexport {\n getNotificationTexts,\n NOTIFICATION_FALLBACK,\n resolveFormMessage,\n} from './modules/notifications/index.js'\nexport type { FormNotificationTexts, NotificationTexts } from './modules/notifications/index.js'\nexport type { PagesOption, SystemPageRole } from './modules/pages/index.js'\nexport { ALL_SYSTEM_PAGE_ROLES, getSystemPagePath } from './modules/pages/index.js'\nexport type { GlobalQueryOptions } from './modules/payload/index.js'\nexport {\n getGlobal,\n getSiteIntegrations,\n getSiteSettings,\n SITE_INTEGRATIONS_SLUG,\n SITE_SETTINGS_SLUG,\n} from './modules/payload/index.js'\nexport { buildSecurityHeaders } from './modules/security/index.js'\nexport { buildCsp } from './modules/security/index.js'\nexport type { BuildCspArgs } from './modules/security/index.js'\nexport type { BuildSecurityHeadersArgs, SecurityHeader } from './modules/security/index.js'\n\nexport type { PageMetadata, SeoMeta, SeoOption } from './modules/seo/index.js'\nexport { buildHreflangAlternates, buildMetadata, composeTitle } from './modules/seo/index.js'\nexport type { AutoFillMapping, RobotsRules, SitemapEntry } from './modules/seo/index.js'\nexport {\n buildAutoFillMetaHook,\n buildRobots,\n buildSitemapEntries,\n createMetadataGenerator,\n createPageMetadata,\n injectAutoFillMeta,\n} from './modules/seo/index.js'\nexport {\n buildIconsMetadata,\n buildOrganizationJsonLd,\n validateFaviconField,\n} from './modules/seo/index.js'\nexport {\n buildBreadcrumbJsonLd,\n buildSiteNavigationJsonLd,\n buildWebSiteJsonLd,\n} from './modules/seo/index.js'\n// Local SEO structured data — LocalBusiness (map pack), Service (offering), FAQPage.\nexport {\n buildFaqJsonLd,\n buildLocalBusinessJsonLd,\n buildServiceJsonLd,\n} from './modules/seo/index.js'\nexport { buildArticleJsonLd } from './modules/seo/index.js'\nexport { buildSitemapXml } from './modules/seo/index.js'\nexport { buildLlmsTxt } from './modules/seo/index.js'\nexport { buildSlugField, toSlug } from './modules/slug/index.js'\nexport { buildR2Storage } from './modules/storage/index.js'\nexport { ipalKit } from './plugin.js'\nexport type { IpalOptions } from './types.js'\n"],"names":["adminOnly","adminOnlyField","adminOrEditor","adminOrEditorField","adminOrSelf","authenticated","hasMinimumRole","isAdmin","isEditor","requireRole","requireRoleField","ROLE_HIERARCHY","getAnalyticsConfig","ACCEPT_ALL_CONSENT","CONSENT_CATEGORIES","CONSENT_COOKIE","CONSENT_MAX_AGE","CONSENT_VERSION","DEFAULT_CONSENT","getConsentTexts","parseConsent","REJECT_ALL_CONSENT","serializeConsent","setDefaultConsent","updateConsent","archiveFieldName","buildArchivePath","buildEntryPath","getArchiveEntries","parsePageParam","resolveRoute","graphAdapter","mailAdapter","panelSmtpAdapter","buildFormsPlugin","createContentHelpers","buildPreventDeleteSystemPage","buildRevalidateHook","buildValidateUniqueRole","setPublishedAtHook","trackSlugHistoryHook","buildLocalizedPath","getDefaultLocale","getLocaleCodes","getLocaleDefinition","getLocalizedSlugs","isValidLocale","LOCALE_COOKIE_NAME","matchAcceptLanguage","negotiateLocale","switchLocalePath","createLocaleMiddleware","DEFAULT_MIDDLEWARE_MATCHER","normalizeFilename","normalizeFilenameHook","getNotificationTexts","NOTIFICATION_FALLBACK","resolveFormMessage","ALL_SYSTEM_PAGE_ROLES","getSystemPagePath","getGlobal","getSiteIntegrations","getSiteSettings","SITE_INTEGRATIONS_SLUG","SITE_SETTINGS_SLUG","buildSecurityHeaders","buildCsp","buildHreflangAlternates","buildMetadata","composeTitle","buildAutoFillMetaHook","buildRobots","buildSitemapEntries","createMetadataGenerator","createPageMetadata","injectAutoFillMeta","buildIconsMetadata","buildOrganizationJsonLd","validateFaviconField","buildBreadcrumbJsonLd","buildSiteNavigationJsonLd","buildWebSiteJsonLd","buildFaqJsonLd","buildLocalBusinessJsonLd","buildServiceJsonLd","buildArticleJsonLd","buildSitemapXml","buildLlmsTxt","buildSlugField","toSlug","buildR2Storage","ipalKit"],"mappings":"AACA,SACEA,SAAS,EACTC,cAAc,EACdC,aAAa,EACbC,kBAAkB,EAClBC,WAAW,EACXC,aAAa,EACbC,cAAc,EACdC,OAAO,EACPC,QAAQ,EACRC,WAAW,EACXC,gBAAgB,EAChBC,cAAc,QACT,4BAA2B;AAElC,SAASC,kBAAkB,QAAQ,+BAA8B;AACjE,SACEC,kBAAkB,EAClBC,kBAAkB,EAClBC,cAAc,EACdC,eAAe,EACfC,eAAe,EACfC,eAAe,EACfC,eAAe,EACfC,YAAY,EACZC,kBAAkB,EAClBC,gBAAgB,EAChBC,iBAAiB,EACjBC,aAAa,QACR,6BAA4B;AAOnC,SACEC,gBAAgB,EAChBC,gBAAgB,EAChBC,cAAc,EACdC,iBAAiB,EACjBC,cAAc,EACdC,YAAY,QACP,6BAA4B;AAEnC,SAASC,YAAY,QAAQ,kCAAiC;AAE9D,SAASC,WAAW,QAAQ,iCAAgC;AAE5D,4EAA4E;AAC5E,gFAAgF;AAChF,gFAAgF;AAChF,SAASC,gBAAgB,QAAQ,sCAAqC;AAEtE,SAASC,gBAAgB,QAAQ,uCAAsC;AAMvE,SAASC,oBAAoB,QAAQ,8BAA6B;AAClE,SACEC,4BAA4B,EAC5BC,mBAAmB,EACnBC,uBAAuB,EACvBC,kBAAkB,EAClBC,oBAAoB,QACf,2BAA0B;AAEjC,SACEC,kBAAkB,EAClBC,gBAAgB,EAChBC,cAAc,EACdC,mBAAmB,EACnBC,iBAAiB,EACjBC,aAAa,EACbC,kBAAkB,EAClBC,mBAAmB,EACnBC,eAAe,EACfC,gBAAgB,QACX,0BAAyB;AAEhC,SAASC,sBAAsB,EAAEC,0BAA0B,QAAQ,0BAAyB;AAC5F,SAASC,iBAAiB,EAAEC,qBAAqB,QAAQ,2BAA0B;AACnF,SACEC,oBAAoB,EACpBC,qBAAqB,EACrBC,kBAAkB,QACb,mCAAkC;AAGzC,SAASC,qBAAqB,EAAEC,iBAAiB,QAAQ,2BAA0B;AAEnF,SACEC,SAAS,EACTC,mBAAmB,EACnBC,eAAe,EACfC,sBAAsB,EACtBC,kBAAkB,QACb,6BAA4B;AACnC,SAASC,oBAAoB,QAAQ,8BAA6B;AAClE,SAASC,QAAQ,QAAQ,8BAA6B;AAKtD,SAASC,uBAAuB,EAAEC,aAAa,EAAEC,YAAY,QAAQ,yBAAwB;AAE7F,SACEC,qBAAqB,EACrBC,WAAW,EACXC,mBAAmB,EACnBC,uBAAuB,EACvBC,kBAAkB,EAClBC,kBAAkB,QACb,yBAAwB;AAC/B,SACEC,kBAAkB,EAClBC,uBAAuB,EACvBC,oBAAoB,QACf,yBAAwB;AAC/B,SACEC,qBAAqB,EACrBC,yBAAyB,EACzBC,kBAAkB,QACb,yBAAwB;AAC/B,qFAAqF;AACrF,SACEC,cAAc,EACdC,wBAAwB,EACxBC,kBAAkB,QACb,yBAAwB;AAC/B,SAASC,kBAAkB,QAAQ,yBAAwB;AAC3D,SAASC,eAAe,QAAQ,yBAAwB;AACxD,SAASC,YAAY,QAAQ,yBAAwB;AACrD,SAASC,cAAc,EAAEC,MAAM,QAAQ,0BAAyB;AAChE,SAASC,cAAc,QAAQ,6BAA4B;AAC3D,SAASC,OAAO,QAAQ,cAAa"} \ No newline at end of file +{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export type { AccessOption, Role } from './modules/access/index.js'\nexport {\n adminOnly,\n adminOnlyField,\n adminOrEditor,\n adminOrEditorField,\n adminOrSelf,\n authenticated,\n hasMinimumRole,\n isAdmin,\n isEditor,\n requireRole,\n requireRoleField,\n ROLE_HIERARCHY,\n} from './modules/access/index.js'\nexport {\n A11Y_COOKIE,\n A11Y_DEFAULT,\n a11yAttributes,\n parseA11y,\n serializeA11y,\n} from './modules/accessibility/index.js'\nexport type { A11yState } from './modules/accessibility/index.js'\nexport type { AnalyticsConfig } from './modules/analytics/index.js'\nexport { getAnalyticsConfig } from './modules/analytics/index.js'\nexport {\n ACCEPT_ALL_CONSENT,\n CONSENT_CATEGORIES,\n CONSENT_COOKIE,\n CONSENT_MAX_AGE,\n CONSENT_VERSION,\n DEFAULT_CONSENT,\n getConsentTexts,\n parseConsent,\n REJECT_ALL_CONSENT,\n serializeConsent,\n setDefaultConsent,\n updateConsent,\n} from './modules/consent/index.js'\nexport type { ConsentCategory, ConsentState, ConsentTexts } from './modules/consent/index.js'\nexport type {\n ContentCollectionOption,\n ContentOption,\n ResolvedRoute,\n} from './modules/content/index.js'\nexport {\n archiveFieldName,\n buildArchivePath,\n buildEntryPath,\n getArchiveEntries,\n parsePageParam,\n resolveRoute,\n} from './modules/content/index.js'\nexport type { ArchiveEntries } from './modules/content/index.js'\nexport { graphAdapter } from './modules/email/graphAdapter.js'\nexport type { GraphAdapterArgs } from './modules/email/graphAdapter.js'\nexport { mailAdapter } from './modules/email/mailAdapter.js'\nexport type { MailAdapterArgs } from './modules/email/mailAdapter.js'\n// Imported straight from the file, NOT from ./modules/email/index.js — that\n// barrel re-exports sendEmail, which imports 'server-only' and would crash when\n// Payload loads the config (or runs generate:importmap) as a plain Node script.\nexport { panelSmtpAdapter } from './modules/email/panelSmtpAdapter.js'\nexport type { PanelSmtpAdapterArgs } from './modules/email/panelSmtpAdapter.js'\nexport { buildFormsPlugin } from './modules/forms/formsPluginConfig.js'\nexport type {\n FormsCollectionOverrides,\n FormsFieldsOverride,\n FormsOption,\n} from './modules/forms/types.js'\nexport { createContentHelpers } from './modules/frontend/index.js'\nexport {\n buildPreventDeleteSystemPage,\n buildRevalidateHook,\n buildValidateUniqueRole,\n setPublishedAtHook,\n trackSlugHistoryHook,\n} from './modules/hooks/index.js'\nexport type { I18nConfig, LocaleDefinition, LocalizedSlugs } from './modules/i18n/index.js'\nexport {\n buildLocalizedPath,\n getDefaultLocale,\n getLocaleCodes,\n getLocaleDefinition,\n getLocalizedSlugs,\n isValidLocale,\n LOCALE_COOKIE_NAME,\n matchAcceptLanguage,\n negotiateLocale,\n switchLocalePath,\n} from './modules/i18n/index.js'\nexport type { LocaleMiddlewareResult } from './modules/i18n/index.js'\nexport { createLocaleMiddleware, DEFAULT_MIDDLEWARE_MATCHER } from './modules/i18n/index.js'\nexport { normalizeFilename, normalizeFilenameHook } from './modules/media/index.js'\nexport {\n getNotificationTexts,\n NOTIFICATION_FALLBACK,\n resolveFormMessage,\n} from './modules/notifications/index.js'\nexport type { FormNotificationTexts, NotificationTexts } from './modules/notifications/index.js'\nexport type { PagesOption, SystemPageRole } from './modules/pages/index.js'\nexport { ALL_SYSTEM_PAGE_ROLES, getSystemPagePath } from './modules/pages/index.js'\nexport type { GlobalQueryOptions } from './modules/payload/index.js'\nexport {\n getGlobal,\n getSiteIntegrations,\n getSiteSettings,\n SITE_INTEGRATIONS_SLUG,\n SITE_SETTINGS_SLUG,\n} from './modules/payload/index.js'\nexport { buildSecurityHeaders } from './modules/security/index.js'\nexport { buildCsp } from './modules/security/index.js'\nexport type { BuildCspArgs } from './modules/security/index.js'\nexport type { BuildSecurityHeadersArgs, SecurityHeader } from './modules/security/index.js'\n\nexport type { PageMetadata, SeoMeta, SeoOption } from './modules/seo/index.js'\nexport { buildHreflangAlternates, buildMetadata, composeTitle } from './modules/seo/index.js'\nexport type { AutoFillMapping, RobotsRules, SitemapEntry } from './modules/seo/index.js'\nexport {\n buildAutoFillMetaHook,\n buildRobots,\n buildSitemapEntries,\n createMetadataGenerator,\n createPageMetadata,\n injectAutoFillMeta,\n} from './modules/seo/index.js'\nexport {\n buildIconsMetadata,\n buildOrganizationJsonLd,\n validateFaviconField,\n} from './modules/seo/index.js'\nexport {\n buildBreadcrumbJsonLd,\n buildSiteNavigationJsonLd,\n buildWebSiteJsonLd,\n} from './modules/seo/index.js'\n// Local SEO structured data — LocalBusiness (map pack), Service (offering), FAQPage.\nexport {\n buildFaqJsonLd,\n buildLocalBusinessJsonLd,\n buildServiceJsonLd,\n} from './modules/seo/index.js'\nexport { buildArticleJsonLd } from './modules/seo/index.js'\nexport { buildSitemapXml } from './modules/seo/index.js'\nexport { buildLlmsTxt } from './modules/seo/index.js'\nexport { buildSlugField, toSlug } from './modules/slug/index.js'\nexport { buildR2Storage } from './modules/storage/index.js'\nexport { ipalKit } from './plugin.js'\nexport type { IpalOptions } from './types.js'\n"],"names":["adminOnly","adminOnlyField","adminOrEditor","adminOrEditorField","adminOrSelf","authenticated","hasMinimumRole","isAdmin","isEditor","requireRole","requireRoleField","ROLE_HIERARCHY","A11Y_COOKIE","A11Y_DEFAULT","a11yAttributes","parseA11y","serializeA11y","getAnalyticsConfig","ACCEPT_ALL_CONSENT","CONSENT_CATEGORIES","CONSENT_COOKIE","CONSENT_MAX_AGE","CONSENT_VERSION","DEFAULT_CONSENT","getConsentTexts","parseConsent","REJECT_ALL_CONSENT","serializeConsent","setDefaultConsent","updateConsent","archiveFieldName","buildArchivePath","buildEntryPath","getArchiveEntries","parsePageParam","resolveRoute","graphAdapter","mailAdapter","panelSmtpAdapter","buildFormsPlugin","createContentHelpers","buildPreventDeleteSystemPage","buildRevalidateHook","buildValidateUniqueRole","setPublishedAtHook","trackSlugHistoryHook","buildLocalizedPath","getDefaultLocale","getLocaleCodes","getLocaleDefinition","getLocalizedSlugs","isValidLocale","LOCALE_COOKIE_NAME","matchAcceptLanguage","negotiateLocale","switchLocalePath","createLocaleMiddleware","DEFAULT_MIDDLEWARE_MATCHER","normalizeFilename","normalizeFilenameHook","getNotificationTexts","NOTIFICATION_FALLBACK","resolveFormMessage","ALL_SYSTEM_PAGE_ROLES","getSystemPagePath","getGlobal","getSiteIntegrations","getSiteSettings","SITE_INTEGRATIONS_SLUG","SITE_SETTINGS_SLUG","buildSecurityHeaders","buildCsp","buildHreflangAlternates","buildMetadata","composeTitle","buildAutoFillMetaHook","buildRobots","buildSitemapEntries","createMetadataGenerator","createPageMetadata","injectAutoFillMeta","buildIconsMetadata","buildOrganizationJsonLd","validateFaviconField","buildBreadcrumbJsonLd","buildSiteNavigationJsonLd","buildWebSiteJsonLd","buildFaqJsonLd","buildLocalBusinessJsonLd","buildServiceJsonLd","buildArticleJsonLd","buildSitemapXml","buildLlmsTxt","buildSlugField","toSlug","buildR2Storage","ipalKit"],"mappings":"AACA,SACEA,SAAS,EACTC,cAAc,EACdC,aAAa,EACbC,kBAAkB,EAClBC,WAAW,EACXC,aAAa,EACbC,cAAc,EACdC,OAAO,EACPC,QAAQ,EACRC,WAAW,EACXC,gBAAgB,EAChBC,cAAc,QACT,4BAA2B;AAClC,SACEC,WAAW,EACXC,YAAY,EACZC,cAAc,EACdC,SAAS,EACTC,aAAa,QACR,mCAAkC;AAGzC,SAASC,kBAAkB,QAAQ,+BAA8B;AACjE,SACEC,kBAAkB,EAClBC,kBAAkB,EAClBC,cAAc,EACdC,eAAe,EACfC,eAAe,EACfC,eAAe,EACfC,eAAe,EACfC,YAAY,EACZC,kBAAkB,EAClBC,gBAAgB,EAChBC,iBAAiB,EACjBC,aAAa,QACR,6BAA4B;AAOnC,SACEC,gBAAgB,EAChBC,gBAAgB,EAChBC,cAAc,EACdC,iBAAiB,EACjBC,cAAc,EACdC,YAAY,QACP,6BAA4B;AAEnC,SAASC,YAAY,QAAQ,kCAAiC;AAE9D,SAASC,WAAW,QAAQ,iCAAgC;AAE5D,4EAA4E;AAC5E,gFAAgF;AAChF,gFAAgF;AAChF,SAASC,gBAAgB,QAAQ,sCAAqC;AAEtE,SAASC,gBAAgB,QAAQ,uCAAsC;AAMvE,SAASC,oBAAoB,QAAQ,8BAA6B;AAClE,SACEC,4BAA4B,EAC5BC,mBAAmB,EACnBC,uBAAuB,EACvBC,kBAAkB,EAClBC,oBAAoB,QACf,2BAA0B;AAEjC,SACEC,kBAAkB,EAClBC,gBAAgB,EAChBC,cAAc,EACdC,mBAAmB,EACnBC,iBAAiB,EACjBC,aAAa,EACbC,kBAAkB,EAClBC,mBAAmB,EACnBC,eAAe,EACfC,gBAAgB,QACX,0BAAyB;AAEhC,SAASC,sBAAsB,EAAEC,0BAA0B,QAAQ,0BAAyB;AAC5F,SAASC,iBAAiB,EAAEC,qBAAqB,QAAQ,2BAA0B;AACnF,SACEC,oBAAoB,EACpBC,qBAAqB,EACrBC,kBAAkB,QACb,mCAAkC;AAGzC,SAASC,qBAAqB,EAAEC,iBAAiB,QAAQ,2BAA0B;AAEnF,SACEC,SAAS,EACTC,mBAAmB,EACnBC,eAAe,EACfC,sBAAsB,EACtBC,kBAAkB,QACb,6BAA4B;AACnC,SAASC,oBAAoB,QAAQ,8BAA6B;AAClE,SAASC,QAAQ,QAAQ,8BAA6B;AAKtD,SAASC,uBAAuB,EAAEC,aAAa,EAAEC,YAAY,QAAQ,yBAAwB;AAE7F,SACEC,qBAAqB,EACrBC,WAAW,EACXC,mBAAmB,EACnBC,uBAAuB,EACvBC,kBAAkB,EAClBC,kBAAkB,QACb,yBAAwB;AAC/B,SACEC,kBAAkB,EAClBC,uBAAuB,EACvBC,oBAAoB,QACf,yBAAwB;AAC/B,SACEC,qBAAqB,EACrBC,yBAAyB,EACzBC,kBAAkB,QACb,yBAAwB;AAC/B,qFAAqF;AACrF,SACEC,cAAc,EACdC,wBAAwB,EACxBC,kBAAkB,QACb,yBAAwB;AAC/B,SAASC,kBAAkB,QAAQ,yBAAwB;AAC3D,SAASC,eAAe,QAAQ,yBAAwB;AACxD,SAASC,YAAY,QAAQ,yBAAwB;AACrD,SAASC,cAAc,EAAEC,MAAM,QAAQ,0BAAyB;AAChE,SAASC,cAAc,QAAQ,6BAA4B;AAC3D,SAASC,OAAO,QAAQ,cAAa"} \ No newline at end of file diff --git a/dist/modules/accessibility/AccessibilityProvider.d.ts b/dist/modules/accessibility/AccessibilityProvider.d.ts new file mode 100644 index 0000000..f235b69 --- /dev/null +++ b/dist/modules/accessibility/AccessibilityProvider.d.ts @@ -0,0 +1,29 @@ +import { type A11yState } from './state.js'; +type A11yContextValue = { + reset: () => void; + set: (key: K, value: A11yState[K]) => void; + state: A11yState; +}; +/** + * Provides accessibility preferences, persists them in a cookie, and applies them + * as data-attributes on 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' + * {children} + * + * To avoid a flash, the project can read the a11y-prefs cookie server-side and + * set the attributes on 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 {}; diff --git a/dist/modules/accessibility/AccessibilityProvider.js b/dist/modules/accessibility/AccessibilityProvider.js new file mode 100644 index 0000000..81b0861 --- /dev/null +++ b/dist/modules/accessibility/AccessibilityProvider.js @@ -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 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' + * {children} + * + * To avoid a flash, the project can read the a11y-prefs cookie server-side and + * set the attributes on 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 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 '); + } + return ctx; +} + +//# sourceMappingURL=AccessibilityProvider.js.map \ No newline at end of file diff --git a/dist/modules/accessibility/AccessibilityProvider.js.map b/dist/modules/accessibility/AccessibilityProvider.js.map new file mode 100644 index 0000000..d9e8e41 --- /dev/null +++ b/dist/modules/accessibility/AccessibilityProvider.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../../src/modules/accessibility/AccessibilityProvider.tsx"],"sourcesContent":["'use client'\n\nimport { createContext, use, useCallback, useEffect, useState } from 'react'\n\nimport {\n A11Y_COOKIE,\n A11Y_COOKIE_MAX_AGE,\n A11Y_DEFAULT,\n a11yAttributes,\n type A11yState,\n parseA11y,\n serializeA11y,\n} from './state.js'\n\ntype A11yContextValue = {\n reset: () => void\n set: (key: K, value: A11yState[K]) => void\n state: A11yState\n}\n\nconst A11yContext = createContext(null)\n\nfunction readCookie(name: string): string | undefined {\n if (typeof document === 'undefined') {return undefined}\n const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`))\n return match ? decodeURIComponent(match[1]) : undefined\n}\n\n/**\n * Provides accessibility preferences, persists them in a cookie, and applies them\n * as data-attributes on so the project's CSS can react. Like\n * ConsentProvider for cookies — wrap the app once; the widget/button consume it.\n *\n * The plugin ships NO styles: it only sets attributes (data-a11y-*). The project\n * writes CSS for those it supports (see docs/accessibility.md). This keeps the\n * design in the project's hands.\n *\n * // layout.tsx\n * import { AccessibilityProvider } from '@intecion/ipal-kit/client'\n * {children}\n *\n * To avoid a flash, the project can read the a11y-prefs cookie server-side and\n * set the attributes on during SSR (see docs). This provider re-applies\n * on the client and keeps them in sync.\n */\nexport function AccessibilityProvider({ children }: { children: React.ReactNode }) {\n const [state, setState] = useState(A11Y_DEFAULT)\n\n // Hydrate from cookie on mount.\n useEffect(() => {\n setState(parseA11y(readCookie(A11Y_COOKIE)))\n }, [])\n\n // Apply attributes to whenever state changes.\n useEffect(() => {\n const el = document.documentElement\n const attrs = a11yAttributes(state)\n for (const [attr, value] of Object.entries(attrs)) {\n if (value === null) {el.removeAttribute(attr)}\n else {el.setAttribute(attr, value)}\n }\n }, [state])\n\n const persist = useCallback((next: A11yState) => {\n document.cookie = `${A11Y_COOKIE}=${encodeURIComponent(\n serializeA11y(next),\n )}; path=/; max-age=${A11Y_COOKIE_MAX_AGE}; samesite=lax`\n }, [])\n\n const set = useCallback(\n (key: K, value: A11yState[K]) => {\n setState((prev) => {\n const next = { ...prev, [key]: value }\n persist(next)\n return next\n })\n },\n [persist],\n )\n\n const reset = useCallback(() => {\n setState(A11Y_DEFAULT)\n persist(A11Y_DEFAULT)\n }, [persist])\n\n return {children}\n}\n\n/** Access accessibility preferences + setters. Use inside AccessibilityProvider. */\nexport function useAccessibility(): A11yContextValue {\n const ctx = use(A11yContext)\n if (!ctx) {throw new Error('useAccessibility must be used within ')}\n return ctx\n}\n"],"names":["createContext","use","useCallback","useEffect","useState","A11Y_COOKIE","A11Y_COOKIE_MAX_AGE","A11Y_DEFAULT","a11yAttributes","parseA11y","serializeA11y","A11yContext","readCookie","name","document","undefined","match","cookie","RegExp","decodeURIComponent","AccessibilityProvider","children","state","setState","el","documentElement","attrs","attr","value","Object","entries","removeAttribute","setAttribute","persist","next","encodeURIComponent","set","key","prev","reset","useAccessibility","ctx","Error"],"mappings":"AAAA;;AAEA,SAASA,aAAa,EAAEC,GAAG,EAAEC,WAAW,EAAEC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AAE5E,SACEC,WAAW,EACXC,mBAAmB,EACnBC,YAAY,EACZC,cAAc,EAEdC,SAAS,EACTC,aAAa,QACR,aAAY;AAQnB,MAAMC,4BAAcX,cAAuC;AAE3D,SAASY,WAAWC,IAAY;IAC9B,IAAI,OAAOC,aAAa,aAAa;QAAC,OAAOC;IAAS;IACtD,MAAMC,QAAQF,SAASG,MAAM,CAACD,KAAK,CAAC,IAAIE,OAAO,CAAC,QAAQ,EAAEL,KAAK,QAAQ,CAAC;IACxE,OAAOG,QAAQG,mBAAmBH,KAAK,CAAC,EAAE,IAAID;AAChD;AAEA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASK,sBAAsB,EAAEC,QAAQ,EAAiC;IAC/E,MAAM,CAACC,OAAOC,SAAS,GAAGnB,SAAoBG;IAE9C,gCAAgC;IAChCJ,UAAU;QACRoB,SAASd,UAAUG,WAAWP;IAChC,GAAG,EAAE;IAEL,qDAAqD;IACrDF,UAAU;QACR,MAAMqB,KAAKV,SAASW,eAAe;QACnC,MAAMC,QAAQlB,eAAec;QAC7B,KAAK,MAAM,CAACK,MAAMC,MAAM,IAAIC,OAAOC,OAAO,CAACJ,OAAQ;YACjD,IAAIE,UAAU,MAAM;gBAACJ,GAAGO,eAAe,CAACJ;YAAK,OACxC;gBAACH,GAAGQ,YAAY,CAACL,MAAMC;YAAM;QACpC;IACF,GAAG;QAACN;KAAM;IAEV,MAAMW,UAAU/B,YAAY,CAACgC;QAC3BpB,SAASG,MAAM,GAAG,GAAGZ,YAAY,CAAC,EAAE8B,mBAClCzB,cAAcwB,OACd,kBAAkB,EAAE5B,oBAAoB,cAAc,CAAC;IAC3D,GAAG,EAAE;IAEL,MAAM8B,MAAMlC,YACV,CAA4BmC,KAAQT;QAClCL,SAAS,CAACe;YACR,MAAMJ,OAAO;gBAAE,GAAGI,IAAI;gBAAE,CAACD,IAAI,EAAET;YAAM;YACrCK,QAAQC;YACR,OAAOA;QACT;IACF,GACA;QAACD;KAAQ;IAGX,MAAMM,QAAQrC,YAAY;QACxBqB,SAAShB;QACT0B,QAAQ1B;IACV,GAAG;QAAC0B;KAAQ;IAEZ,qBAAO,KAACtB;QAAYiB,OAAO;YAAEW;YAAOH;YAAKd;QAAM;kBAAID;;AACrD;AAEA,kFAAkF,GAClF,OAAO,SAASmB;IACd,MAAMC,MAAMxC,IAAIU;IAChB,IAAI,CAAC8B,KAAK;QAAC,MAAM,IAAIC,MAAM;IAA+D;IAC1F,OAAOD;AACT"} \ No newline at end of file diff --git a/dist/modules/accessibility/AccessibilityWidget.d.ts b/dist/modules/accessibility/AccessibilityWidget.d.ts new file mode 100644 index 0000000..3ab6816 --- /dev/null +++ b/dist/modules/accessibility/AccessibilityWidget.d.ts @@ -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 (the project's CSS styles them). + * + * Unstyled by default — pass classNames to match the project's design (like + * CookieBanner). Wrap the app in first. + * + * + */ +export declare function AccessibilityWidget({ classNames, texts, }: { + classNames?: A11yClassNames; + texts?: A11yTexts; +}): import("react/jsx-runtime").JSX.Element; diff --git a/dist/modules/accessibility/AccessibilityWidget.js b/dist/modules/accessibility/AccessibilityWidget.js new file mode 100644 index 0000000..cd5ce1b --- /dev/null +++ b/dist/modules/accessibility/AccessibilityWidget.js @@ -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 (the project's CSS styles them). + * + * Unstyled by default — pass classNames to match the project's design (like + * CookieBanner). Wrap the app in first. + * + * + */ 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 \ No newline at end of file diff --git a/dist/modules/accessibility/AccessibilityWidget.js.map b/dist/modules/accessibility/AccessibilityWidget.js.map new file mode 100644 index 0000000..ea03d56 --- /dev/null +++ b/dist/modules/accessibility/AccessibilityWidget.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../../src/modules/accessibility/AccessibilityWidget.tsx"],"sourcesContent":["'use client'\n\nimport { useState } from 'react'\n\nimport type { A11yState } from './state.js'\n\nimport { useAccessibility } from './AccessibilityProvider.js'\n\nexport type A11yTexts = {\n bigCursor?: string\n close?: string\n contrast?: string\n contrastHigh?: string\n contrastInverted?: string\n grayscale?: string\n lineHeight?: string\n open?: string\n readableFont?: string\n reduceMotion?: string\n reset?: string\n textSize?: string\n title?: string\n underlineLinks?: string\n}\n\nconst DEFAULT_TEXTS: Required = {\n bigCursor: 'Duży kursor',\n close: 'Zamknij',\n contrast: 'Kontrast',\n contrastHigh: 'Wysoki',\n contrastInverted: 'Odwrócony',\n grayscale: 'Skala szarości',\n lineHeight: 'Odstęp między liniami',\n open: 'Otwórz panel dostępności',\n readableFont: 'Czytelna czcionka',\n reduceMotion: 'Wyłącz animacje',\n reset: 'Resetuj',\n textSize: 'Rozmiar tekstu',\n title: 'Dostępność',\n underlineLinks: 'Podkreśl linki',\n}\n\nexport type A11yClassNames = {\n active?: string\n button?: string\n closeButton?: string\n control?: string\n label?: string\n panel?: string\n resetButton?: string\n row?: string\n}\n\n/**\n * Accessibility toolbar: a floating button that opens a panel of options (text\n * size, line height, contrast, grayscale, underline links, readable font, reduce\n * motion, big cursor). Choices persist in a cookie and apply as data-attributes\n * on (the project's CSS styles them).\n *\n * Unstyled by default — pass classNames to match the project's design (like\n * CookieBanner). Wrap the app in first.\n *\n * \n */\nexport function AccessibilityWidget({\n classNames,\n texts,\n}: {\n classNames?: A11yClassNames\n texts?: A11yTexts\n}) {\n const { reset, set, state } = useAccessibility()\n const [open, setOpen] = useState(false)\n const t = { ...DEFAULT_TEXTS, ...texts }\n const cn = classNames ?? {}\n\n const toggle = (key: K) => set(key, !state[key] as A11yState[K])\n\n const isActive = (on: boolean) => (on ? (cn.active ?? '') : '')\n\n return (\n <>\n setOpen((o) => !o)}\n type=\"button\"\n >\n {/* Project can style/replace via CSS; simple glyph fallback. */}\n ♿\n \n\n {open && (\n
\n
\n {t.textSize}\n
\n {[0, 1, 2, 3].map((n) => (\n set('textSize', n as A11yState['textSize'])}\n type=\"button\"\n >\n A{n > 0 ? '+'.repeat(n) : ''}\n \n ))}\n
\n
\n\n
\n {t.lineHeight}\n
\n {[0, 1, 2].map((n) => (\n set('lineHeight', n as A11yState['lineHeight'])}\n type=\"button\"\n >\n {n === 0 ? '—' : '≡'.repeat(n)}\n \n ))}\n
\n
\n\n
\n {t.contrast}\n
\n set('contrast', state.contrast === 'high' ? 'default' : 'high')}\n type=\"button\"\n >\n {t.contrastHigh}\n \n \n set('contrast', state.contrast === 'inverted' ? 'default' : 'inverted')\n }\n type=\"button\"\n >\n {t.contrastInverted}\n \n
\n
\n\n {(\n [\n ['grayscale', t.grayscale],\n ['underlineLinks', t.underlineLinks],\n ['readableFont', t.readableFont],\n ['reduceMotion', t.reduceMotion],\n ['bigCursor', t.bigCursor],\n ] as Array<[keyof A11yState, string]>\n ).map(([key, label]) => (\n
\n {label}\n toggle(key)}\n type=\"button\"\n >\n {state[key] ? 'ON' : 'OFF'}\n \n
\n ))}\n\n \n \n
\n )}\n \n )\n}\n"],"names":["useState","useAccessibility","DEFAULT_TEXTS","bigCursor","close","contrast","contrastHigh","contrastInverted","grayscale","lineHeight","open","readableFont","reduceMotion","reset","textSize","title","underlineLinks","AccessibilityWidget","classNames","texts","set","state","setOpen","t","cn","toggle","key","isActive","on","active","button","aria-expanded","aria-label","className","onClick","o","type","span","aria-hidden","div","panel","role","row","label","control","map","n","repeat","aria-pressed","Boolean","resetButton","closeButton"],"mappings":"AAAA;;AAEA,SAASA,QAAQ,QAAQ,QAAO;AAIhC,SAASC,gBAAgB,QAAQ,6BAA4B;AAmB7D,MAAMC,gBAAqC;IACzCC,WAAW;IACXC,OAAO;IACPC,UAAU;IACVC,cAAc;IACdC,kBAAkB;IAClBC,WAAW;IACXC,YAAY;IACZC,MAAM;IACNC,cAAc;IACdC,cAAc;IACdC,OAAO;IACPC,UAAU;IACVC,OAAO;IACPC,gBAAgB;AAClB;AAaA;;;;;;;;;;CAUC,GACD,OAAO,SAASC,oBAAoB,EAClCC,UAAU,EACVC,KAAK,EAIN;IACC,MAAM,EAAEN,KAAK,EAAEO,GAAG,EAAEC,KAAK,EAAE,GAAGpB;IAC9B,MAAM,CAACS,MAAMY,QAAQ,GAAGtB,SAAS;IACjC,MAAMuB,IAAI;QAAE,GAAGrB,aAAa;QAAE,GAAGiB,KAAK;IAAC;IACvC,MAAMK,KAAKN,cAAc,CAAC;IAE1B,MAAMO,SAAS,CAA4BC,MAAWN,IAAIM,KAAK,CAACL,KAAK,CAACK,IAAI;IAE1E,MAAMC,WAAW,CAACC,KAAiBA,KAAMJ,GAAGK,MAAM,IAAI,KAAM;IAE5D,qBACE;;0BACE,KAACC;gBACCC,iBAAerB;gBACfsB,cAAYT,EAAEb,IAAI;gBAClBuB,WAAWT,GAAGM,MAAM;gBACpBI,SAAS,IAAMZ,QAAQ,CAACa,IAAM,CAACA;gBAC/BC,MAAK;0BAGL,cAAA,KAACC;oBAAKC,eAAY;8BAAO;;;YAG1B5B,sBACC,MAAC6B;gBAAIP,cAAYT,EAAER,KAAK;gBAAEkB,WAAWT,GAAGgB,KAAK;gBAAEC,MAAK;;kCAClD,MAACF;wBAAIN,WAAWT,GAAGkB,GAAG;;0CACpB,KAACL;gCAAKJ,WAAWT,GAAGmB,KAAK;0CAAGpB,EAAET,QAAQ;;0CACtC,KAACyB;gCAAIN,WAAWT,GAAGoB,OAAO;0CACvB;oCAAC;oCAAG;oCAAG;oCAAG;iCAAE,CAACC,GAAG,CAAC,CAACC,kBACjB,MAAChB;wCACCG,WAAWN,SAASN,MAAMP,QAAQ,KAAKgC;wCAEvCZ,SAAS,IAAMd,IAAI,YAAY0B;wCAC/BV,MAAK;;4CACN;4CACGU,IAAI,IAAI,IAAIC,MAAM,CAACD,KAAK;;uCAJrBA;;;;kCAUb,MAACP;wBAAIN,WAAWT,GAAGkB,GAAG;;0CACpB,KAACL;gCAAKJ,WAAWT,GAAGmB,KAAK;0CAAGpB,EAAEd,UAAU;;0CACxC,KAAC8B;gCAAIN,WAAWT,GAAGoB,OAAO;0CACvB;oCAAC;oCAAG;oCAAG;iCAAE,CAACC,GAAG,CAAC,CAACC,kBACd,KAAChB;wCACCG,WAAWN,SAASN,MAAMZ,UAAU,KAAKqC;wCAEzCZ,SAAS,IAAMd,IAAI,cAAc0B;wCACjCV,MAAK;kDAEJU,MAAM,IAAI,MAAM,IAAIC,MAAM,CAACD;uCAJvBA;;;;kCAUb,MAACP;wBAAIN,WAAWT,GAAGkB,GAAG;;0CACpB,KAACL;gCAAKJ,WAAWT,GAAGmB,KAAK;0CAAGpB,EAAElB,QAAQ;;0CACtC,MAACkC;gCAAIN,WAAWT,GAAGoB,OAAO;;kDACxB,KAACd;wCACCG,WAAWN,SAASN,MAAMhB,QAAQ,KAAK;wCACvC6B,SAAS,IAAMd,IAAI,YAAYC,MAAMhB,QAAQ,KAAK,SAAS,YAAY;wCACvE+B,MAAK;kDAEJb,EAAEjB,YAAY;;kDAEjB,KAACwB;wCACCG,WAAWN,SAASN,MAAMhB,QAAQ,KAAK;wCACvC6B,SAAS,IACPd,IAAI,YAAYC,MAAMhB,QAAQ,KAAK,aAAa,YAAY;wCAE9D+B,MAAK;kDAEJb,EAAEhB,gBAAgB;;;;;;oBAMvB;wBACE;4BAAC;4BAAagB,EAAEf,SAAS;yBAAC;wBAC1B;4BAAC;4BAAkBe,EAAEP,cAAc;yBAAC;wBACpC;4BAAC;4BAAgBO,EAAEZ,YAAY;yBAAC;wBAChC;4BAAC;4BAAgBY,EAAEX,YAAY;yBAAC;wBAChC;4BAAC;4BAAaW,EAAEpB,SAAS;yBAAC;qBAC3B,CACD0C,GAAG,CAAC,CAAC,CAACnB,KAAKiB,MAAM,iBACjB,MAACJ;4BAAIN,WAAWT,GAAGkB,GAAG;;8CACpB,KAACL;oCAAKJ,WAAWT,GAAGmB,KAAK;8CAAGA;;8CAC5B,KAACb;oCACCkB,gBAAcC,QAAQ5B,KAAK,CAACK,IAAI;oCAChCO,WAAW,GAAGT,GAAGoB,OAAO,IAAI,GAAG,CAAC,EAAEjB,SAASsB,QAAQ5B,KAAK,CAACK,IAAI,IAAI;oCACjEQ,SAAS,IAAMT,OAAOC;oCACtBU,MAAK;8CAEJf,KAAK,CAACK,IAAI,GAAG,OAAO;;;2BARIA;kCAa/B,KAACI;wBAAOG,WAAWT,GAAG0B,WAAW;wBAAEhB,SAASrB;wBAAOuB,MAAK;kCACrDb,EAAEV,KAAK;;kCAEV,KAACiB;wBAAOG,WAAWT,GAAG2B,WAAW;wBAAEjB,SAAS,IAAMZ,QAAQ;wBAAQc,MAAK;kCACpEb,EAAEnB,KAAK;;;;;;AAMpB"} \ No newline at end of file diff --git a/dist/modules/accessibility/client.d.ts b/dist/modules/accessibility/client.d.ts new file mode 100644 index 0000000..a71f0fd --- /dev/null +++ b/dist/modules/accessibility/client.d.ts @@ -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'; diff --git a/dist/modules/accessibility/client.js b/dist/modules/accessibility/client.js new file mode 100644 index 0000000..351adb7 --- /dev/null +++ b/dist/modules/accessibility/client.js @@ -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 \ No newline at end of file diff --git a/dist/modules/accessibility/client.js.map b/dist/modules/accessibility/client.js.map new file mode 100644 index 0000000..4ff5110 --- /dev/null +++ b/dist/modules/accessibility/client.js.map @@ -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"} \ No newline at end of file diff --git a/dist/modules/accessibility/index.d.ts b/dist/modules/accessibility/index.d.ts new file mode 100644 index 0000000..f6fe13d --- /dev/null +++ b/dist/modules/accessibility/index.d.ts @@ -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'; diff --git a/dist/modules/accessibility/index.js b/dist/modules/accessibility/index.js new file mode 100644 index 0000000..11c71ef --- /dev/null +++ b/dist/modules/accessibility/index.js @@ -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 \ No newline at end of file diff --git a/dist/modules/accessibility/index.js.map b/dist/modules/accessibility/index.js.map new file mode 100644 index 0000000..33aca75 --- /dev/null +++ b/dist/modules/accessibility/index.js.map @@ -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"} \ No newline at end of file diff --git a/dist/modules/accessibility/state.d.ts b/dist/modules/accessibility/state.d.ts new file mode 100644 index 0000000..479300d --- /dev/null +++ b/dist/modules/accessibility/state.d.ts @@ -0,0 +1,37 @@ +/** + * Accessibility preferences state. Each option maps to a data-attribute on + * (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 . 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; diff --git a/dist/modules/accessibility/state.js b/dist/modules/accessibility/state.js new file mode 100644 index 0000000..5bc45f4 --- /dev/null +++ b/dist/modules/accessibility/state.js @@ -0,0 +1,53 @@ +/** + * Accessibility preferences state. Each option maps to a data-attribute on + * (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 . 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 \ No newline at end of file diff --git a/dist/modules/accessibility/state.js.map b/dist/modules/accessibility/state.js.map new file mode 100644 index 0000000..a8c7ea5 --- /dev/null +++ b/dist/modules/accessibility/state.js.map @@ -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 * (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\n return { ...A11Y_DEFAULT, ...parsed }\n } catch {\n return A11Y_DEFAULT\n }\n}\n\n/**\n * Maps state → data-attributes to set on . 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 {\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"} \ No newline at end of file diff --git a/docs/accessibility.md b/docs/accessibility.md new file mode 100644 index 0000000..da7ed3c --- /dev/null +++ b/docs/accessibility.md @@ -0,0 +1,171 @@ +# Dostępność — widget a11y (WCAG) + +Widget dostępności: pływający przycisk otwierający panel z opcjami dla osób z +niepełnosprawnościami (rozmiar tekstu, kontrast, skala szarości, podkreślone +linki, czytelna czcionka, wyłączenie animacji, duży kursor). + +> **OPCJONALNY — nie dodawaj domyślnie.** Widget a11y jest wymagany prawnie +> TYLKO dla niektórych stron (podmioty publiczne, część e-commerce/usług objętych +> European Accessibility Act). Dla większości stron komercyjnych to OPCJA, nie +> obowiązek. Dodawaj GDY klient/projekt tego wymaga — nie na każdej stronie z +> automatu. W razie wątpliwości: zapytaj, czy strona podlega wymogom dostępności. + +Wzorzec jak CookieBanner: provider + widget, wpinasz raz. Preferencje w cookie +(bez flash), stosowane jako atrybuty `data-a11y-*` na ``. + +> **Granica plugin/projekt:** plugin dostarcza MECHANIZM (widget, stan, cookie, +> atrybuty na html). Projekt dostarcza CSS reagujący na atrybuty — bo style +> zależą od designu projektu (kolory, czcionki, Tailwind). Plugin NIE narzuca +> stylów, żeby nie kolidować. Gotowy CSS do skopiowania niżej. + +--- + +## 1. Wpięcie — Provider + Widget + +```tsx +// app/(frontend)/[locale]/layout.tsx +import { AccessibilityProvider, AccessibilityWidget } from '@intecion/ipal-kit/client' + + + + {children} + + + +``` + +Widget jest bez stylów (jak CookieBanner) — classNames dopasowujesz do designu. + +--- + +## 2. CSS reagujący na atrybuty (OBOWIĄZKOWE — projekt) + +Widget ustawia atrybuty na ``. Bez tego CSS nic się nie dzieje. Skopiuj do +globalnego CSS projektu (dostosuj do designu): + +```css +/* Rozmiar tekstu */ +html[data-a11y-text="1"] { font-size: 112.5%; } +html[data-a11y-text="2"] { font-size: 125%; } +html[data-a11y-text="3"] { font-size: 150%; } + +/* Odstęp między liniami */ +html[data-a11y-line="1"] * { line-height: 1.8 !important; } +html[data-a11y-line="2"] * { line-height: 2.2 !important; } + +/* Kontrast wysoki */ +html[data-a11y-contrast="high"] { + filter: contrast(1.4); +} +/* Kontrast odwrócony */ +html[data-a11y-contrast="inverted"] { + filter: invert(1) hue-rotate(180deg); +} +html[data-a11y-contrast="inverted"] img, +html[data-a11y-contrast="inverted"] video { + filter: invert(1) hue-rotate(180deg); /* przywróć media */ +} + +/* Skala szarości */ +html[data-a11y-grayscale="on"] { filter: grayscale(1); } +/* Uwaga: filter na html nie kumuluje się — jeśli łączysz kontrast+szarość, + zastosuj na body albo połącz w jednej regule. */ + +/* Podkreślone linki */ +html[data-a11y-underline="on"] a { text-decoration: underline !important; } + +/* Czytelna czcionka (podmień na swoją dyslexia-friendly / prostą) */ +html[data-a11y-font="readable"] * { + font-family: Verdana, Tahoma, sans-serif !important; + letter-spacing: 0.02em; +} + +/* Wyłączenie animacji */ +html[data-a11y-motion="reduce"] *, +html[data-a11y-motion="reduce"] *::before, +html[data-a11y-motion="reduce"] *::after { + animation-duration: 0.001ms !important; + transition-duration: 0.001ms !important; + scroll-behavior: auto !important; +} + +/* Duży kursor */ +html[data-a11y-cursor="big"] * { + cursor: url('/cursors/big.svg') 4 4, auto !important; +} +``` + +Dostosuj wartości do projektu (kolory kontrastu, czcionka, kursor). To Twój CSS — +plugin tylko ustawia atrybuty. + +> **Kontrast + filter:** wiele opcji używa `filter` na ``. CSS `filter` na +> jednym elemencie NIE kumuluje wielu wartości z różnych reguł — ostatnia wygrywa. +> Jeśli chcesz łączyć (np. szarość + kontrast), zastosuj filtry na `body` z +> pełną wartością, albo zbuduj reguły kombinowane. Dla pojedynczych opcji działa +> bez problemu. + +--- + +## 3. Bez flash (SSR) — opcjonalne + +Domyślnie widget stosuje atrybuty po hydratacji (krótki flash przy ładowaniu, +jeśli użytkownik miał ustawienia). Żeby tego uniknąć, odczytaj cookie server-side +i ustaw atrybuty na `` w SSR: + +```tsx +// layout.tsx (server) — odczytaj cookie i ustaw atrybuty od razu +import { cookies } from 'next/headers' +import { A11Y_COOKIE, parseA11y, a11yAttributes } from '@intecion/ipal-kit' + +const raw = (await cookies()).get(A11Y_COOKIE)?.value +const attrs = a11yAttributes(parseA11y(raw)) +const htmlAttrs = Object.fromEntries( + Object.entries(attrs).filter(([, v]) => v !== null), +) + +return ... +``` + +Provider i tak re-aplikuje na kliencie i synchronizuje. To tylko eliminuje flash. + +--- + +## 4. Osobny przycisk otwierający (opcjonalnie) + +Widget ma wbudowany pływający przycisk. Jeśli chcesz otwierać panel z innego +miejsca (np. stopka „Dostępność"), użyj hooka: + +```tsx +'use client' +import { useAccessibility } from '@intecion/ipal-kit/client' +// stan otwarcia trzymaj sam, albo rozbuduj widget — hook daje state/set/reset +``` + +--- + +## 5. Compliance — kiedy dostępność jest wymagana + +Dostępność (WCAG) jest wymagana prawnie TYLKO dla części stron: +- **Podmioty publiczne** (urzędy, szkoły, instytucje) — ustawa o dostępności cyfrowej +- **Duże e-commerce / usługi** objęte European Accessibility Act (2019/882, od 2025) +- Strony, gdzie klient sam tego wymaga (polityka firmy, przetarg) + +Dla **większości stron komercyjnych** (wizytówka, mała firma, katalog) widget a11y +to **opcja, nie obowiązek** — dodawaj gdy klient wymaga, nie z automatu. + +Gdy dodajesz: widget sam w sobie NIE czyni strony w pełni dostępną — to pomoc dla +użytkownika. Pełna dostępność to też semantyczny HTML, alt teksty, nawigacja +klawiaturą, kontrast bazowy. Widget uzupełnia, nie zastępuje. Nie sprzedawaj +klientowi „mamy widget = jesteśmy zgodni z WCAG". Patrz wymagania-prawne.md. \ No newline at end of file diff --git a/src/exports/client.ts b/src/exports/client.ts index c5208c2..01f40a9 100644 --- a/src/exports/client.ts +++ b/src/exports/client.ts @@ -1,6 +1,13 @@ 'use client' export { MaskedField } from '../globals/SiteIntegrations/components/MaskedField.js' export { TestEmailButton } from '../globals/SiteIntegrations/components/TestEmailButton.js' +export { + AccessibilityProvider, + AccessibilityWidget, + useAccessibility, +} from '../modules/accessibility/client.js' +export type { A11yClassNames, A11yState, A11yTexts } from '../modules/accessibility/client.js' + export { Analytics } from '../modules/analytics/client.js' /** * Entry point: ipal-kit/client @@ -16,7 +23,6 @@ export { useConsent, useConsentContext, } from '../modules/consent/client.js' - export type { CookieBannerClassNames } from '../modules/consent/client.js' // Pure i18n path helpers — no server/RSC deps, safe to import in client // components (e.g. a LanguageSwitcher that computes locale URLs on the client). diff --git a/src/index.ts b/src/index.ts index f586ccc..1825477 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,14 @@ export { requireRoleField, ROLE_HIERARCHY, } from './modules/access/index.js' +export { + A11Y_COOKIE, + A11Y_DEFAULT, + a11yAttributes, + parseA11y, + serializeA11y, +} from './modules/accessibility/index.js' +export type { A11yState } from './modules/accessibility/index.js' export type { AnalyticsConfig } from './modules/analytics/index.js' export { getAnalyticsConfig } from './modules/analytics/index.js' export { diff --git a/src/modules/accessibility/AccessibilityProvider.tsx b/src/modules/accessibility/AccessibilityProvider.tsx new file mode 100644 index 0000000..2aaf514 --- /dev/null +++ b/src/modules/accessibility/AccessibilityProvider.tsx @@ -0,0 +1,94 @@ +'use client' + +import { createContext, use, useCallback, useEffect, useState } from 'react' + +import { + A11Y_COOKIE, + A11Y_COOKIE_MAX_AGE, + A11Y_DEFAULT, + a11yAttributes, + type A11yState, + parseA11y, + serializeA11y, +} from './state.js' + +type A11yContextValue = { + reset: () => void + set: (key: K, value: A11yState[K]) => void + state: A11yState +} + +const A11yContext = createContext(null) + +function readCookie(name: string): string | undefined { + 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 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' + * {children} + * + * To avoid a flash, the project can read the a11y-prefs cookie server-side and + * set the attributes on during SSR (see docs). This provider re-applies + * on the client and keeps them in sync. + */ +export function AccessibilityProvider({ children }: { children: React.ReactNode }) { + const [state, setState] = useState(A11Y_DEFAULT) + + // Hydrate from cookie on mount. + useEffect(() => { + setState(parseA11y(readCookie(A11Y_COOKIE))) + }, []) + + // Apply attributes to 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: A11yState) => { + document.cookie = `${A11Y_COOKIE}=${encodeURIComponent( + serializeA11y(next), + )}; path=/; max-age=${A11Y_COOKIE_MAX_AGE}; samesite=lax` + }, []) + + const set = useCallback( + (key: K, value: A11yState[K]) => { + setState((prev) => { + const next = { ...prev, [key]: value } + persist(next) + return next + }) + }, + [persist], + ) + + const reset = useCallback(() => { + setState(A11Y_DEFAULT) + persist(A11Y_DEFAULT) + }, [persist]) + + return {children} +} + +/** Access accessibility preferences + setters. Use inside AccessibilityProvider. */ +export function useAccessibility(): A11yContextValue { + const ctx = use(A11yContext) + if (!ctx) {throw new Error('useAccessibility must be used within ')} + return ctx +} diff --git a/src/modules/accessibility/AccessibilityWidget.tsx b/src/modules/accessibility/AccessibilityWidget.tsx new file mode 100644 index 0000000..ca546e1 --- /dev/null +++ b/src/modules/accessibility/AccessibilityWidget.tsx @@ -0,0 +1,182 @@ +'use client' + +import { useState } from 'react' + +import type { A11yState } from './state.js' + +import { useAccessibility } from './AccessibilityProvider.js' + +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 +} + +const DEFAULT_TEXTS: Required = { + 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', +} + +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 (the project's CSS styles them). + * + * Unstyled by default — pass classNames to match the project's design (like + * CookieBanner). Wrap the app in first. + * + * + */ +export function AccessibilityWidget({ + classNames, + texts, +}: { + classNames?: A11yClassNames + texts?: A11yTexts +}) { + const { reset, set, state } = useAccessibility() + const [open, setOpen] = useState(false) + const t = { ...DEFAULT_TEXTS, ...texts } + const cn = classNames ?? {} + + const toggle = (key: K) => set(key, !state[key] as A11yState[K]) + + const isActive = (on: boolean) => (on ? (cn.active ?? '') : '') + + return ( + <> + + + {open && ( +
+
+ {t.textSize} +
+ {[0, 1, 2, 3].map((n) => ( + + ))} +
+
+ +
+ {t.lineHeight} +
+ {[0, 1, 2].map((n) => ( + + ))} +
+
+ +
+ {t.contrast} +
+ + +
+
+ + {( + [ + ['grayscale', t.grayscale], + ['underlineLinks', t.underlineLinks], + ['readableFont', t.readableFont], + ['reduceMotion', t.reduceMotion], + ['bigCursor', t.bigCursor], + ] as Array<[keyof A11yState, string]> + ).map(([key, label]) => ( +
+ {label} + +
+ ))} + + + +
+ )} + + ) +} diff --git a/src/modules/accessibility/client.ts b/src/modules/accessibility/client.ts new file mode 100644 index 0000000..ac7afec --- /dev/null +++ b/src/modules/accessibility/client.ts @@ -0,0 +1,6 @@ +'use client' +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' diff --git a/src/modules/accessibility/index.ts b/src/modules/accessibility/index.ts new file mode 100644 index 0000000..d4d5d7b --- /dev/null +++ b/src/modules/accessibility/index.ts @@ -0,0 +1,12 @@ +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' diff --git a/src/modules/accessibility/state.ts b/src/modules/accessibility/state.ts new file mode 100644 index 0000000..c728bf7 --- /dev/null +++ b/src/modules/accessibility/state.ts @@ -0,0 +1,72 @@ +/** + * Accessibility preferences state. Each option maps to a data-attribute on + * (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 const A11Y_DEFAULT: A11yState = { + 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: A11yState): string { + return JSON.stringify(state) +} + +/** Parse from the cookie; falls back to defaults on any bad value. */ +export function parseA11y(raw: null | string | undefined): A11yState { + if (!raw) {return A11Y_DEFAULT} + try { + const parsed = JSON.parse(raw) as Partial + return { ...A11Y_DEFAULT, ...parsed } + } catch { + return A11Y_DEFAULT + } +} + +/** + * Maps state → data-attributes to set on . 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: A11yState): Record { + 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, + } +}