# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project

LISIO is an accessibility & ecology widget ("Solution d'accessibilité et d'écologie") that is injected into client websites (as an embedded script) or shipped as a browser extension. Written in TypeScript, bundled with Vite. Write code comments in English; user-facing UI text is in French.

## Commands

- `npm run dev` — watch + rebuild via nodemon (note: `nodemon.json` hardcodes `/var/www/html/solution`; on other machines run `npx vite build --mode site --watch` or adjust the path).
- `npm run build` — full site build: `verif-trans → copy-langs → tsc → vite build --mode site → minify-json → compress`. Output goes to `dist-site/`.
- `npm run build-debug` — same but unminified with sourcemaps.
- `npm run build-obfusc` — site build with `javascript-obfuscator` pass.
- `npm run build:ext` — extension build; runs Vite twice with `ENTRY=popin-factory` and `ENTRY=reading-mode`. Output to `dist-extension/`.
- `npm run lint` / `npm run lint-fix` — ESLint over `src`.
- `npm run format` — Prettier over `src`.
- `npm run doc` — TypeDoc.

There is **no test runner** configured. `verif-trans` (`scripts/verif-trans-files.js`) is the closest thing to a validation gate: it fails the build if the language JSON files in `src/assets/langs/` don't all have the same number of keys.

`tsc` runs with `noEmit` — it is a typecheck step only; Vite/Rollup does the actual bundling.

## Build targets and environment

Two independent axes control a build:

1. **`--mode` (Vite mode)** picks the target file `.env.site` or `.env.extension`, which only set `VITE_BUILD_TARGET` (`site`|`extension`) and `VITE_OUT_DIR`.
2. **`ENV_TYPE` in `.env`** (`dev`|`preprod`|`prod`) selects the *custom* env file `.env.<type>` (loaded in `vite.config.js`), which supplies the real values: `VITE_PHP_SERVER`, `VITE_LISIO_DOMAIN`, `VITE_SERVER_IFRAME`, etc.

`vite.config.js` injects these as compile-time `define`s:
- `__BUILD_TARGET__` — `'site' | 'extension'`. Branch on this for extension-vs-site behavior (e.g. transport layer, DOM injection).
- `__BUILD_SUB_TARGET__` — the env-type name (`dev`/`preprod`/`prod`); `dev` reads test token/URL from `VITE_ACCESSKEY`/`VITE_URL`.
- All `VITE_*` vars become `import.meta.env.VITE_*`.

`.env*` files are gitignored — copy `.env-template` to create them.

## Architecture

### Bootstrap
- `src/lisio-init.ts` — site entry point. Checks for the browser extension, does an IE fallback, then POSTs to `${VITE_PHP_SERVER}/verifToken.php` to authenticate the client's access key and fetch per-client `userSettings` (colors, position, hidden tabs, popin version, languages). On success it builds the widget via `LisioPopinFactory`. Re-hooks widget creation on Turbo/Turbolinks navigation.
- `src/reading-mode.ts` — the "reading mode" / linearized reader view, a separate Vite entry.
- `src/lisio-config.ts` — the `LisioConfig`/`userSettings` shape and its defaults.

### The `Lisio` class (`src/lisio.ts`)
Central orchestrator. Holds a `Map<LisioParameterNames, Adapter>` of every feature and wires together the walkers, controllers, mutation observer, broadcast channel, and message manager. Parameter name enums and defaults come from the external `@lisio/lisio-profils` package.

### Adapters (`src/adapters/`)
Each adapter implements **one accessibility feature** (contrast, daltonism, dyslexia font, zoom, cursor, reading mask, speech synthesis, etc.) by mutating the host page's DOM. All extend the abstract `LisioAdapter<T>` (`lisio-adapter.ts`) with `adapt(walker, value, ...)`. Font-related adapters live in `adapters/fontStyleAdapter/`, translation adapters in `adapters/translations/` (DeepL + Google), and the reader view in `adapters/reader/` (with per-element-type helpers under `reader/helpers/`). **To add a feature, create an adapter and register it in the `Lisio` adapter map.**

### Walkers (`src/walkers/`)
`LisioTextTreeWalker` / `LisioWalker` traverse the DOM (via `TreeWalker` + `NodeFilter`) so adapters can apply changes to text/element nodes, skipping `script`/`style`/`title`/etc. The `Lisio` class maintains separate walkers for `document.body` and for the widget's shadow root.

### Controllers (`src/controllers/`)
Cross-cutting concerns rather than page features: `lisio-widget-controller` (the widget iframe), `lisio-shadow-root-controller` (widget UI lives in a shadow DOM under `#lisio-shadow-env`), `lisio-mutation-observer-controller`, `lisio-params-controller`, `lisio-translation-controller`, `lisio-stats-controller`, `lisio-stylesheet-controller`, modal controllers under `controllers/modals/`, and the broadcast-channel controller (see below).

### Messaging & transport
The widget UI runs inside an **iframe** (site target) and communicates with the page context through message managers (`src/managers/`): `LisioMessageManager` (full) and `LisioLightMessageManager` (light), which dispatch to handlers in `managers/handlers/` keyed by `LisioMessageReceivedName`.

`LisioBroadcastChannelController` (`src/controllers/broadcastChannel/`) abstracts the transport: it uses the **`BroadcastChannel` API for the site target** and **`chrome.runtime` ports for the extension target** (branching on `__BUILD_TARGET__`). Handlers live in `broadcastChannel/handlers/`.

### UI factory (popins/popups)
`LisioPopinFactory` (`src/popins/lisio-popin-factory.ts`) builds the widget UI for a named popin version (e.g. `popin2025`, selectable per-client via `vPopin`). Shadow-DOM CSS is imported as raw strings (`import styles from "...css?raw"`) and injected into the shadow root.

### Enums & global state
`src/enums/` holds the string-keyed enums that glue the system together (parameter names, message names, broadcast channel + message names, icon names, popin names). `GlobalContext` (`src/global-context.ts`) is a singleton holding current popin name + isMobile.

## Cross-repo messaging contract

This repo (`lisio-solution`, the **client script**) does all the DOM adaptation. The **UI** it drives lives in the sibling repo `lisio-widget` (`../widget`, package `lisio-widget`) and runs inside an iframe (site) or an extension popup. The widget never touches the host page; it emits messages and this script applies them. This is a two-repo protocol — **any message change must be made in both repos, and the string enum values must match exactly on both sides.**

- Solution incoming enum: `src/enums/lisio-message-received-name.ts` (`LisioMessageReceivedName`) ↔ widget outgoing enum: `widget/src/modules/messages/sent-messages-names.ts` (`SentMessageNames`).
- Solution outgoing (string literals in `src/managers/lisio-message-manager.ts` + `lisio-light-message-manager.ts`) ↔ widget incoming enum: `widget/src/modules/messages/received-message-names.ts` (`ReceivedMessageNames`).

### Two managers, split by `target`
Inbound widget→solution messages carry a **`target: "light" | "full"`** field, and this repo runs *two* managers each with its own `window` listener that filters on it:
- **`LisioLightMessageManager`** (`target: "light"`) — bootstrap/config traffic. Sends `init_back`, `lisio_front_loaded`, `load_widget`, `popin_ready`, `shortcut_to`, `disable`, `enable`. Its `contentWindow` is set by `LisioPopinFactory` (`popins/lisio-popin-factory.ts`).
- **`LisioMessageManager`** (`target: "full"`) — all feature/adaptation traffic; handlers registered in `initMessages()` and dispatched via `_messageHandlers`. Its `contentWindow` is set in `lisio.ts`.

Both managers `postMessage` **without** a `target` field on outgoing messages (the widget dispatches purely on `name`); the `target` field only exists on the widget's replies to route them to the right manager here.

### Transport (branches on `__BUILD_TARGET__`)
- **site** — iframe boundary via `window.postMessage`. Every inbound listener validates `event.origin === VITE_SERVER_IFRAME` **and** `event.data.target`. Outbound goes to `this._contentWindow.postMessage(payload, VITE_SERVER_IFRAME)`.
- **extension** — named `chrome.runtime` ports relayed by a background service worker (not in either repo). This side opens `channel-front` (and `broadcast-channel-side-panel` in reading mode); the widget opens `channel-back`. Messages are wrapped/unwrapped through the background (`redirect_to_front`/`redirect_to_panel`).

### Envelope
Every message is `{ name: string, datas: string /* JSON-stringified payload */ }`; handlers `JSON.parse(datas)`.

### Bootstrap handshake (site target)
1. `accessedition.js` on the host injects this repo's built `lisioinit.js` with the access key.
2. `lisio-init.ts` POSTs to `${VITE_PHP_SERVER}/verifToken.php` to authenticate and fetch `userSettings`, then `LisioPopinFactory` builds the widget iframe.
3. Light manager sends **`load_widget`** (or **`popin_ready`**) into the iframe.
4. Widget lazy-loads and replies **`load_front`** (`target: "light"`).
5. Light manager sends **`init_back`** with the full config payload (`LisioInitBackMessageDatas` in `lisio-light-message-manager.ts`). Its shape must stay aligned with `InitMessageDatas` in `widget/src/modules/messages/message-types.ts` — **fields on both structs must match.**

### Messages solution → widget (this repo sends)
| name | manager | payload | purpose |
|---|---|---|---|
| `init_back` | light | `LisioInitBackMessageDatas` | initialize/build the widget |
| `lisio_front_loaded` | light | `{}` | client script finished loading |
| `shortcut_to` | light | `{ screenName, eventDetail }` | open a specific screen |
| `disable` / `enable` | light | `{}` | globally toggle the widget |
| `popup_response` | full | `{ value, parameter, focus? }` | confirm book-page/reading-mode popup |
| `process_end` | full | `{ value }` | adaptation processing finished |
| `disable_reading_mask` | full | `{}` | reading mask turned off page-side |
| `disable_parameter` | full | `{ value: parameter }` | turn one parameter off |
| `encode_profiles` / `all_encoded` | full | `{}` | request profile encoding |
| `close_reading_mode` / `close_widget` | full | `{}` | close reader / widget |
| `translator_api_translate` | full | `{ keyHandler, body: { originLangISO, translateLangISO, originalTexts, epochSent } }` | ask widget to run a translation |
| `dico_api` | full | `{ word }` | dictionary lookup |
| `load_widget`, `popin_ready` | light | `{}` | bootstrap (widget handles in `dynamic-load.ts`) |

The widget also declares handlers for `ask_profiles`, `ask_for_active`, and `hide_popin`, but **this repo has no sender for them** — reserved/legacy; add a sender here if you wire them up.

### Messages widget → solution (this repo receives; `LisioMessageReceivedName`)
| name | target | payload | purpose |
|---|---|---|---|
| `init_front` | full | `{ adaptation, isConfigured }` | client script may initialize |
| `feature` | full | `{ feature, value, defaultValue, dir?, isReallyActive, isConfigured }` | adapt one parameter |
| `bulk_features` | full | `{ bulk: [{ feature, value, defaultValue }], isReallyActive, isConfigured }` | apply a whole profile |
| `disable_all` | full | `{}` | clear all adaptations |
| `translation_language_change` | full | `{ flagIso, translation?, dir }` | translation language changed |
| `trans_banner_popup` | full | `{ confirmationModalDescription, confirmationModalParameter, confirmationModalWidgetTriggererFocus }` | show page-side confirmation banner |
| `translator_api_translate_response` | full | `{ keyHandler, texts }` | translation results |
| `dico_api_response` | full | `{ word }` | dictionary results |
| `profiles_response` | full | users map | stored users |
| `encode_profiles_response` | full | encoded query string | encoded users for the URL |
| `all_encoded_response` | full | encoded string | full encoded state |
| `reading_mode_response` | full | `{}` | start reading mode |
| `new_tab` | full | `{ url, ecoOrRural, same, urlQuery }` | navigate / open tab |
| `close_widget` | full | `{}` | close the widget |
| `active_response` | full | `{ value, defaultValue, isConfigured }` | active-state answer |
| `eco_mode` | full | `{ state }` | eco mode toggled |
| `load_front` | light | `{}` | widget module loaded (bootstrap) |

Note: `LisioMessageReceivedName` also declares `LOAD_FRONT = "load_front"`, but it is handled during bootstrap in `LisioPopinFactory`, not via `LisioMessageManager.initMessages`.

### Not to be confused with this contract
`LisioBroadcastChannelController` (`controllers/broadcastChannel/`, channel `reader`, `BCMS_*` sent / `BCMR_*` received) is a **separate internal channel** between the page context and the reading-mode view — it does not involve the widget. `open_side`, `end_init_html`, and `close_extension_popup` target the extension background service worker, not the widget.

## Internationalization
Translations are one JSON file per locale in `src/assets/langs/` (~50 locales). `fr.json` is the source and is **not** copied at build time; `copy-langs` copies the rest to `public/langs`. All files must have identical key counts or `verif-trans` fails the build. Global type augmentations (`window.lisioDomain`, `lisioServeurIframe`, `__BUILD_TARGET__`, etc.) are declared in `global.d.ts`.
