npm package: @pigmilcom/a11y
Description: WCAG 2.1 accessibility widget for React — text size, contrast, dyslexia font, motion reduction, and more.
Author: pigmilcom
Latest version: 1.2.0

# @pigmilcom/a11y

WCAG 2.1 accessibility widget. Drop in one `<script>` tag **or** install via npm — visitors get instant control over text size, contrast, colour filters, motion, and more. Preferences persist via `localStorage`.

---

## CDN — Plug & Play

No React, no npm, no build step. Add one tag to your `<head>`.

### jsDelivr (recommended)

Served from the npm registry via jsDelivr's global edge network — always the latest published version:

```html
<script
  src="https://cdn.jsdelivr.net/npm/@pigmilcom/a11y/dist/a11y.cdn.js"
  data-position="bottom-right"
></script>
```

Pin to a specific version:

```html
<script
  src="https://cdn.jsdelivr.net/npm/@pigmilcom/a11y@1.0.1/dist/a11y.cdn.js"
  data-position="bottom-right"
></script>
```

### Self-hosted (pigmil CDN)

Use your own CDN endpoint for full control over deployment:

```html
<script
  src="https://cdn.pigmil.com/a11y/dist/a11y.cdn.js"
  data-position="bottom-right"
></script>
```

The widget auto-mounts on `DOMContentLoaded`. React 18 and all CSS are bundled inside — nothing else needed.

### `data-position` values

| Value           | Placement          |
| --------------- | ------------------ |
| `bottom-right`  | Bottom-right corner (default) |
| `bottom-left`   | Bottom-left corner |
| `top-right`     | Top-right corner   |
| `top-left`      | Top-left corner    |

### Programmatic control

The CDN build exposes `window.PigmilA11y` for manual control:

```js
PigmilA11y.mount();    // mount the widget
PigmilA11y.unmount();  // remove the widget and clean up
```

---

## Plans & domain validation

Starting from v1.1.0, the widget validates the hosting domain against the **PIGMIL API** before rendering. This enables usage-based quotas and per-domain access control.

### Tiers

| Plan   | Requests / day | Requests / month | PIGMIL branding |
| ------ | -------------- | ---------------- | --------------- |
| Free   | 1,000          | 30,000           | Shown in footer |
| Pro    | Unlimited      | Unlimited        | Hidden          |

A "request" is one widget initialisation (page load / route change).

### How validation works

1. On widget mount the browser makes a single `GET` request to the PIGMIL API.
2. The API checks whether the domain is registered and within quota.
3. The response controls whether the widget renders:
   - **`valid`** — widget renders normally.
   - **`blocked`** — quota exceeded; widget does not render until the day / month resets or the plan is upgraded.
   - **`error`** / API unreachable — widget does not render (fail-closed on non-local domains).
4. The result is cached in `sessionStorage` for 30 minutes to avoid redundant calls.
5. Cached and returned result objects are `Object.freeze()`d — their `status` and `plan` properties cannot be mutated after resolution.

### Local / development environments

Validation is **automatically skipped** when the page runs on:
- `localhost`, `127.x.x.x`, `::1`, `0.0.0.0`
- Any non-standard port (e.g. `:3000`, `:5173`, `:8080`)

The widget always renders in these environments with a free-plan appearance. No API key or sign-up is required during development.

### Security layers

| Layer | What it does |
|---|---|
| **Server-side enforcement** (primary) | The PIGMIL API only validates registered domains, increments per-domain counters, and returns `blocked:true` when quotas are exceeded. This is the real gate — no client-side bypass can defeat it. |
| **Frozen result objects** | `Object.freeze()` on the validation result prevents callers from mutating `{ status, plan }` to escalate their own access level. |
| **CDN bundle obfuscation** | `dist/a11y.cdn.js` is post-processed by `javascript-obfuscator` with Base64 string-array encoding and self-defending code. The self-defending wrapper causes the bundle to break at runtime if it has been reformatted / prettified before execution, deterring the "prettify → grep → patch → re-minify" bypass workflow. The npm package source remains intentionally readable (open source). |
| **XOR-encoded endpoint** | The API URL is stored as a numeric XOR array in source and decoded at runtime, making it non-trivial to locate in the minified + obfuscated CDN bundle. |
| **Daily-rotating request signature** | Each API call includes an FNV-1a hash of `domain + YYYY-MM-DD` — a per-domain-per-day token the server can optionally verify to detect replayed or forged requests. |

### Current status (mock mode)

The API endpoint is being implemented in the PIGMIL backend. Until it is live, `src/license.js` uses `MOCK_MODE = true`, which always returns `{ ok: true, plan: 'free', blocked: false }`. To switch to the real API, set `MOCK_MODE = false` in that file — the fetch code is already written and ready.

---

## Features

- Text size — Normal / Large / X-Large
- High contrast
- Invert colours
- Grayscale
- Reduce motion
- Highlight links
- Text spacing (WCAG 1.4.12)
- ADHD-friendly mode
- Dyslexia-friendly font
- Persists in `localStorage`
- Full keyboard & screen-reader support (`role="dialog"`, `aria-checked`, `aria-expanded`)
- Zero runtime dependencies (React peer dep only)

---

## Dist files

Every build outputs these files to `dist/`:

| File                | Format | Description                                   |
| ------------------- | ------ | --------------------------------------------- |
| `index.js`          | CJS    | npm (unminified, for bundlers)                 |
| `index.mjs`         | ESM    | npm (unminified, for bundlers)                 |
| `index.min.js`      | CJS    | npm (minified)                                 |
| `index.min.mjs`     | ESM    | npm (minified)                                 |
| `index.css`         | CSS    | Stylesheet import (`@pigmilcom/a11y/styles`)    |
| `index.min.css`     | CSS    | Minified stylesheet                            |
| `a11y.cdn.js`       | IIFE   | CDN bundle — React bundled, CSS inlined, minified |

---

## npm Installation

```bash
npm install @pigmilcom/a11y
# yarn
yarn add @pigmilcom/a11y
# pnpm
pnpm add @pigmilcom/a11y
```

---

## Quick start (React)

### 1 — Import the stylesheet (once, globally)

```js
import '@pigmilcom/a11y/styles';
```

This single import contains both the panel UI styles and all the accessibility
class rules applied to `<html>`.

### 2 — Drop in the widget

```jsx
import a11y from '@pigmilcom/a11y';

// Assign to a capitalized variable to use in JSX
const A11y = a11y;

<A11y className="fixed bottom-4 right-4 rounded" />
```

The `className` prop is merged onto the trigger button — use Tailwind classes,
custom classes, or anything your bundler supports to position the widget.

---

## Customising the widget with CSS

The widget exposes stable **`a11y-widget-*`** class names on its key elements
as a public API. Target them from a `<style>` tag (CDN / self-hosted) or your
own stylesheet (React):

```html
<style>
  /* Trigger button */
  .a11y-widget-btn {
    border-radius: 8px !important;
    background: rgba(15, 15, 40, 0.9) !important;
  }

  /* Dialog panel */
  .a11y-widget-dialog {
    border-color: rgba(99, 102, 241, 0.4) !important;
  }

  /* Toggle option rows */
  .a11y-widget-toggle-btn:hover {
    background: rgba(99, 102, 241, 0.06) !important;
  }
</style>
```

| Public class                | Element              |
| --------------------------- | -------------------- |
| `a11y-widget-btn`           | Trigger button       |
| `a11y-widget-dialog`        | Dialog panel         |
| `a11y-widget-toggle-btn`    | Each toggle option   |

> Place your `<style>` tag **after** the widget `<script>` tag so it takes
> precedence in the cascade. Use `!important` where the widget's base styles
> have higher specificity.

---

## Tailwind CSS support

Tailwind utility classes work transparently via the `className` prop:

```jsx
import a11y from '@pigmilcom/a11y';
const A11y = a11y;

// Tailwind classes are passed straight through to the trigger button
<A11y className="fixed bottom-6 right-6 rounded-full shadow-xl" />
```

Because the classes come from *your* source files, Tailwind's JIT scanner
picks them up automatically. No purge exceptions or safelist entries needed.

---

## Framework examples

### Next.js App Router

```jsx
// app/layout.jsx
import '@pigmilcom/a11y/styles';
import a11y from '@pigmilcom/a11y';

const A11y = a11y;

export default function RootLayout({ children }) {
    return (
        <html lang="en">
            <body>
                {children}
                <A11y className="fixed bottom-4 right-4 rounded" />
            </body>
        </html>
    );
}
```

> The component is already marked `'use client'` — no extra wrapper needed.

### Vite + React

```jsx
// src/App.jsx
import a11y from '@pigmilcom/a11y';

const A11y = a11y;

export default function App() {
    return (
        <>
            {/* …your content… */}
            <A11y className="fixed bottom-4 right-4 rounded" />
        </>
    );
}
```

---

## Props

| Prop        | Type     | Default | Description                                 |
| ----------- | -------- | ------- | ------------------------------------------- |
| `className` | `string` | —       | Extra classes added to the trigger `<button>` |

---

## CSS classes applied to `<html>`

When a preference is enabled the widget adds a class to `document.documentElement`.
All rules live in `@pigmilcom/a11y/styles` and can be freely overridden in your own stylesheet.

| Class                   | Feature                                   |
| ----------------------- | ----------------------------------------- |
| `a11y-text-lg`          | Font size +18 %                           |
| `a11y-text-xl`          | Font size +45 %                           |
| `a11y-high-contrast`    | Contrast filter (1.6×)                    |
| `a11y-invert`           | Invert colours (re-inverts images/video)  |
| `a11y-grayscale`        | Grayscale filter                          |
| `a11y-reduce-motion`    | Strips all animations & transitions       |
| `a11y-highlight-links`  | Outlines every link and `[role="link"]`   |
| `a11y-text-spacing`     | Letter / word / line spacing (WCAG 1.4.12)|
| `a11y-adhd`             | Removes motion + adds strong focus ring   |
| `a11y-dyslexia`         | Switches to a high-readability system font|

---

## Storage

Preferences are saved under the `localStorage` key `pgm-a11y` as a JSON
object. The widget reads and re-applies them on first client render, so
preferences survive page reloads and navigation. On SSR the widget hydrates
without errors — all DOM access is guarded by a `mounted` flag.

---

## Requirements

### CDN

No dependencies. Works in any HTML page.

### npm package

| Peer dependency | Version |
| --------------- | ------- |
| `react`         | ≥ 17    |
| `react-dom`     | ≥ 17    |

---

## License

MIT © [pigmilcom](https://pigmilcom.com)