# CLAUDE.md

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

## Project overview

`lisio-widget` is the UI of the Lisio accessibility product. It is a TypeScript, component-based front end that renders an accessibility panel. It does **not** modify the client page directly — it sends messages to a separate client script (via `MessageManager`) which performs the actual adaptations. The widget is written to be as light as possible; it bundles to a single lazily-loaded module.

Two build targets share the same codebase, selected at build time via the `__BUILD_TARGET__` constant (see `vite.config.js`):
- **`site`** — an iframe embedded on a customer website; communicates with the parent window via `window.postMessage`.
- **`extension`** — a browser extension popup; communicates over `chrome.runtime` ports.

Nearly all target-specific behavior branches on `if (__BUILD_TARGET__ === "extension")`. When changing communication, storage, or bootstrap logic, check both branches.

Write code comments in English; user-facing UI text is in French.

## Commands

```bash
npm run dev            # nodemon → rebuilds site target on change (used inside Docker, watches /var/www/html/widget)
npm run build          # verif-trans + tsc + vite build (site) + minify-json + brotli/gzip compress → dist-site
npm run build:ext      # verif-trans + tsc + vite build (extension) → dist-extension
npm run build-debug    # site build, unminified, with sourcemaps
npm run build-obfusc   # site build + javascript-obfuscator pass
npm run lint           # eslint . --ext .ts
npm run lint-fix       # eslint with --fix
npm run format         # prettier ./src --write
npm run doc            # typedoc (reads MAIN.md as the docs homepage)
```

There is **no test suite**. `tsc` (run as part of every build) is the type-safety gate — `noEmit` is set, so Vite does the actual bundling and `tsc` only type-checks. `strict`, `noUnusedLocals`, and `noUnusedParameters` are all on.

`verif-trans` (`scripts/verif-trans-files.js`) runs before every build and **fails the build** if the language JSON files under `src/html/public/assets/langs/` do not all have the same number of keys. When adding a translation key, add it to *every* language file.

## Architecture

Entry is `src/dynamic-load.ts` (the initial script) which lazy-imports `src/main.ts` (`loadWidget()`) only once the host signals readiness (or immediately, in the extension). `main.ts` wires up the engine and all singletons.

The code depends on two published Lisio packages that supply the domain model and rendering engine — read their docs before touching render or parameter logic:
- **`@lisio/lisio-engine`** — `Engine`, `AssetsLoader`, and the `LisioComponent` base class all components extend. Handles rendering, DOM lifecycle, and asset lazy-loading. Docs: https://env-preprod-docs.lisio.fr/lisio-engine/
- **`@lisio/lisio-profils`** — the accessibility domain model: `LisioProfileNames`, `LisioCategoryNames`, and the `Lisio{Boolean,Numeric,String}ParameterNames` enums. Docs: https://env-preprod-docs.lisio.fr/lisio-profils/

### Domain concepts (from MAIN.md)
- **Parameter** — a single adaptation feature (boolean/numeric/string). Rendered via the matching input component.
- **Profile** — a named set of parameters with fixed values. Rendered as a checkbox.
- **Category** — a set of subcategories/profiles/parameters. Rendered as a screen.
- **Screen** — one navigable section; only one is displayed at a time.
- **RenderObject** — a plain data description of a UI element (ids, css classes, name, infoboxes). Components are built from RenderObjects so creation is automated and maintainable rather than hand-written HTML.

### Modules (`src/modules/`)
- **`render/components/`** — UI components, each extending `LisioComponent`. Inputs (`inputs/{choice,range,slider,switch,text}`) each have an `input/` subfolder for the raw input plus a wrapper component.
- **`render/screens/`** — one folder per screen (accessibility, comfort, daltonism, dyslexia, eco, rural, synth, translation, settings, home, more, management, reset, users). Screens extend `LisioScreen` (`render/screens/lisio-screen.ts`). `screen-types.ts` holds `ScreenNames`, category names, and `CategoryRenderObject`.
- **`versions/`** — a widget "version" (`WidgetVersion` subclass) declares which screens/categories/profiles/parameters are included. Versions: `complete`, `essential`, `first`, `welcome`, mapped from an incoming number in `numberToVersion`. `LisioWidgetVersionFactory` builds them; **to add a version**, add a builder method and register it in the factory's `_builders` map.
- **`managers/`** — process-wide singletons: `ScreenManager` (navigation, breadcrumb, screen rendering), `InputManager` (input state, "really active"/"configured" checks), `MessageManager`.
- **`controllers/`** — `UserController` (users/profiles, storage version), `CompressorController` (encode/decode profiles for the URL), `StatisticsController`, `TranslationController`, and pluggable storage backends under `storages/` (`cookie`, `local-storage`, `extension-storage`).
- **`messages/`** — the client-script protocol. `received-message-names.ts` / `sent-messages-names.ts` are the enums; `handlers/` holds one handler per received message. `MessageManager` maps `ReceivedMessageNames → handler` in `_messageHandlers` and exposes typed `send*` methods for outgoing messages. **To add an incoming message:** add the name to the enum, write a handler in `handlers/`, and register it in the `_messageHandlers` map (handlers are `.bind(this)`-ed to the manager).

### Singleton pattern
Managers, controllers, factories, and the engine are singletons: private static `_current`, public static `current` getter, and a constructor that throws `CoreErrorCodes.SINGLETON_NOT_UNIQUE` if re-instantiated. Access them anywhere via `SomeManager.current`. `main.ts` constructs them in dependency order.

## Conventions

- **Do not use raw DOM APIs** (`querySelector`, `appendChild`, `dispatchEvent`, etc.). Use managers, controllers, or `LisioComponent` methods. The engine's dynamic loading + input wiring makes ad-hoc DOM manipulation cause subtle bugs. (per MAIN.md)
- ESLint (`.eslintrc.json`) enforces: single quotes, semicolons, `prefer-const`/`no-var`, no inline comments (`no-inline-comments`), `object-curly-spacing: always`, 1tbs brace style. Run `npm run lint` before finishing.
- Source uses TSDoc heavily with typedoc tags (`@module`, `@source`, `@mergeTarget`, `{@link}`). Follow the existing doc style when adding public members.
- Every added translation key must exist in all `langs/*.json` files or the build fails.

## Cross-repo messaging contract

The widget (`lisio-widget`, this repo) is only the **UI**. The **client script** it talks to lives in the sibling repo `lisio-solution` (`../solution`, package `lisio-solution`). The widget never touches the host page; it emits messages and the client script performs the actual DOM adaptations. 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.**

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

### Transport (branches on `__BUILD_TARGET__`)
- **site** — the widget runs in an **iframe**; both sides use `window.postMessage`. Widget sends via `window.parent.postMessage(..., originOfMessages)`. It learns `originOfMessages` from the `origin` field of the `init_back` payload (`init-message-handler.ts`); before init it falls back to `"*"`. The solution validates `event.origin === VITE_SERVER_IFRAME` on every inbound message; **the widget does not validate `event.origin`** (it dispatches purely on `event.data.name`).
- **extension** — no window boundary; both sides open named `chrome.runtime` ports relayed by a background service worker (not in either repo). Widget opens `channel-back` (and `sidepanel-channel` in reading mode); solution opens `channel-front` (and `broadcast-channel-side-panel`). The widget wraps outgoing messages as `{ name: "redirect_to_front" | "redirect_to_panel", datas: JSON.stringify({ name, datas }) }`; `close_widget` becomes a local `window.close()` instead of a message.

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

Widget → solution messages additionally carry a **`target: "light" | "full"`** field. This is the routing key: the solution runs *two* independent managers each with its own `window` listener filtering on `target`. `LisioLightMessageManager` handles bootstrap/config-level traffic (`"light"`); `LisioMessageManager` handles all feature/adaptation traffic (`"full"`). Currently the widget sends `"light"` only for `load_front` (from `dynamic-load.ts`); everything routed through `MessageManager.sendMessage` uses `"full"`. Solution → widget messages omit `target` (the widget doesn't filter on it).

### Bootstrap handshake (site target)
1. `accessedition.js` on the host page injects `lisioinit.js` (the solution's built entry) with the access key.
2. Solution authenticates the key (`verifToken.php`), builds the widget iframe via `LisioPopinFactory`, and its light manager sends **`load_widget`** (or **`popin_ready`**) to the iframe.
3. Widget `dynamic-load.ts` receives it: on `load_widget` it lazy-imports `main.ts` (`loadWidget()`); on `popin_ready` it only loads if a stored user exists. After load it posts **`load_front`** (`target: "light"`) back to the parent.
4. Solution light manager sends **`init_back`** with the full `InitMessageDatas` config (version, colors, languages, hidden tabs, statistics URL, shortcut, origin, …). `initMessageHandler` builds the widget version from it. This payload's shape must stay aligned with `message-types.ts → InitMessageDatas` here and `LisioInitBackMessageDatas` in the solution's light manager.

### Messages solution → widget (widget receives; `ReceivedMessageNames`)
| name | manager | payload | purpose |
|---|---|---|---|
| `init_back` | light | `InitMessageDatas` (see above) | 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 } }` | proxy a translation request |
| `dico_api` | full | `{ word }` | dictionary lookup |
| `load_widget`, `popin_ready` | (bootstrap) | — | handled in `dynamic-load.ts`, not registered in `MessageManager` |

`ASK_PROFILS` (`ask_profiles`), `ASK_FOR_ACTIVE` (`ask_for_active`), and `HIDE_POPIN` (`hide_popin`) have handlers here but **no sender currently exists in the solution repo** — treat them as reserved/legacy.

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

### Not part of this contract
The solution's `LisioBroadcastChannelController` (channel `reader`, `BCMS_*`/`BCMR_*` messages) is a **separate intra-solution channel** between the page context and the reading-mode view — it does not involve the widget. Solution-internal-only messages (`open_side`, `end_init_html`, `close_extension_popup`) target the extension background, not the widget.

## Internationalization

This is the **UI locale** of the widget itself (the panel's own labels/infoboxes) — distinct from the *translation feature* the widget offers to the host page (DeepL/Google, driven by the `translation` screen and the `translator_api_translate*` messages).

- **Files** — one JSON file per locale in `src/html/public/assets/langs/` (~51 files, keyed by ISO code, e.g. `en.json`, `pt_br.json`). Each is a flat `{ textId: string }` map. `fr.json` is the reference. **To add a language, drop in a `<iso>.json` file** named after the `TranslationLanguages` enum value; **to add a key, add it to every file.**
- **Enum** — supported locales are the `TranslationLanguages` enum in `src/modules/controllers/translation/translation-languages-enum-and-types.ts`. RTL locales are listed there; `getDirection(lang)` returns `"rtl"`/`"ltr"` (used to set `dir` on the shadow root and forwarded to the client script in `feature`/`translation_language_change` messages).
- **Loading** — `TranslationController` (singleton) is the only way to read UI text. `getTranslation(textId)` returns the string (throws `TRANSLATION_NOT_FOUND` / `TRANSLATIONS_NOT_LOADED`); `loadTranslationFile(lang)` lazy-loads `./langs/<lang>.json` via the engine's `AssetsLoader` (files are fetched on demand, not bundled). Only one locale is held in memory at a time.
- **Init** — the active locale comes from the `init_back` message's `widgetLang` and is applied in `init-message-handler.ts` (sets `TranslationController.defaultLang`, loads the file, and sets `MessageManager.langIso`/`flagIso` plus the shadow root's `dir`/`lang` attributes).
- **Build gate** — `verif-trans` (`scripts/verif-trans-files.js`) fails the build if the `langs/*.json` files don't all have the same key count. `minify-json` (`scripts/minify-json.js`) minifies `dist-site/assets/langs` (and `assets/icons`) after the site build.

Global type augmentations live in `src/types/window.d.ts` (`Window.lisioIsExtension`) and `src/vite-env.d.ts` (`__BUILD_TARGET__`, the `VITE_*` env vars).

## Deployment note

The `Dockerfile` builds against a private npm registry (`nexus.lisio.fr`, needs `NPM_TOKEN`) and runs `npm run dev` under nodemon; `sync-modules.sh` copies cached `node_modules` into the mounted volume on start.
