|
| 1 | +# cartes.gouv.fr |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +`react-dsfr-tiptap` is a React rich-text / markdown editor library implementing the French |
| 6 | +government Design System (DSFR, `@codegouvfr/react-dsfr`) on top of **Tiptap v3**. It is an |
| 7 | +npm-workspaces + Lerna monorepo: |
| 8 | + |
| 9 | +- `packages/react-dsfr-tiptap/` — the published library |
| 10 | +- `examples/` — a Vite demo app (deployed to GitHub Pages) |
| 11 | + |
| 12 | +UI strings, button titles, and docs are in **French**. Icons are remixicon (`ri-*`) via DSFR. |
| 13 | + |
| 14 | +## Commands |
| 15 | + |
| 16 | +Run from the repo root (workspace-aware): |
| 17 | + |
| 18 | +| Task | Command | |
| 19 | +| ------------------------------ | ----------------------------------- | |
| 20 | +| Build the library | `npm run build` (tsup) | |
| 21 | +| Run tests | `npm run test` | |
| 22 | +| Run examples dev server | `npm run examples` (vite) | |
| 23 | +| Build examples (GH Pages base) | `npm run build:examples` | |
| 24 | +| Lint | `npm run lint` / `npm run lint:fix` | |
| 25 | +| Type-check (all workspaces) | `npm run check-types` | |
| 26 | + |
| 27 | +**Single test:** `cd packages/react-dsfr-tiptap && npx jest src/components/ColorInput.test.tsx` |
| 28 | +(or filter by name: `npx jest -t "color"`). |
| 29 | + |
| 30 | +**Release** (maintainers): `npm run release:patch|minor|major` — regenerates CHANGELOG via |
| 31 | +`generate-changelog`, commits, then `lerna version --no-private`. |
| 32 | + |
| 33 | +CI (`.github/workflows/test.yml`, on every push) runs, in order: lint → test → build → |
| 34 | +build:examples. Keep all four green. |
| 35 | + |
| 36 | +## Build & package layout |
| 37 | + |
| 38 | +`tsup` produces dual CJS/ESM + `.d.ts` from three entry points, exposed as subpath exports: |
| 39 | + |
| 40 | +- `react-dsfr-tiptap` → `src/index.ts` — `RichTextEditor`, all controls, `createControl*` |
| 41 | + utilities, editor context, constants |
| 42 | +- `react-dsfr-tiptap/markdown` → `src/markdown.ts` — `MarkdownEditor` + markdown constants |
| 43 | +- `react-dsfr-tiptap/dialog` → `src/dialog.ts` — dialog controls (Link/Unlink/Image/Youtube), |
| 44 | + `Dialog` primitives, dialog context |
| 45 | +- `react-dsfr-tiptap/index.css` — the `.fr-tiptap` stylesheet for rendering generated HTML |
| 46 | + |
| 47 | +When adding a new exported symbol, wire it through the correct entry file **and** the |
| 48 | +`exports`/`files` maps in `packages/react-dsfr-tiptap/package.json`. Run `npm run check-exports` |
| 49 | +(`attw`) to validate the published type/export shape. |
| 50 | + |
| 51 | +## Architecture |
| 52 | + |
| 53 | +Composition is layered: |
| 54 | +`RichTextEditor` / `MarkdownEditor` → `Loader` → `Provider` → Tiptap `useEditor` + `editorContext`, |
| 55 | +then `Menu` (groups of control buttons) + `Content`. |
| 56 | + |
| 57 | +1. **Compound component** (`components/RichTextEditor.tsx`). `RichTextEditor` is a function with |
| 58 | + statics attached: `.Provider`, `.Menu`, `.Group`, `.Content`, plus every named control |
| 59 | + (`.Bold`, `.Italic`, …) attached from the `richTextEditorControls` map. This supports both the |
| 60 | + all-in-one API and the low-level composable API shown in the README. |
| 61 | + |
| 62 | +2. **Control system** (`controls/`). Controls are built declaratively by three factories in |
| 63 | + `controls/createControls.tsx`: |
| 64 | + - `createControl({ buttonProps, isActive, operation })` — runs a Tiptap command, auto-derives |
| 65 | + `disabled` from `editor.can()…` |
| 66 | + - `createDialogControl({ buttonProps, DialogContent, onClick })` — opens a DSFR Modal |
| 67 | + - `createCustomControl({ Control, DialogContent, isActive, isDisabled })` — fully custom render |
| 68 | + |
| 69 | + Concrete controls live in `Controls.tsx` (basic), `CustomControls.tsx` (Color), |
| 70 | + `DialogControls.tsx` (Link/Image/Youtube). They are aggregated into `richTextEditorControls` |
| 71 | + and `markdownControls` in `utils/controls.ts`. **Add a new built-in control here**, and add its |
| 72 | + name to `types/controls.ts` and the `extensionMapping` in `Loader.tsx`. |
| 73 | + The `controls` prop is `(Control | ControlComponent)[][]`: outer array = visual groups; string |
| 74 | + entries resolve via `controlMap` (caller override) first, then the built-in map. |
| 75 | + |
| 76 | +3. **Extension lazy-loading** (`components/Loader.tsx`) — the subtlest piece. `extensionMapping` |
| 77 | + maps each control → the Tiptap extension it needs (most → `starterKit`; e.g. `Color` → `color`, |
| 78 | + `AlignLeft` → `textAlign`). If the caller passes `extensionLoader`, Loader computes which |
| 79 | + extensions the active `controls` require, dynamically imports only those, applies |
| 80 | + `extensionDefaultConfiguration` (image `inline`, link `openOnClick:false`, textAlign types, |
| 81 | + youtube `nocookie`), and renders `null` until they resolve. A needed extension that is neither |
| 82 | + loaded nor in `extensionLoader` triggers a `console.warn` with a copy-paste fix — do not turn |
| 83 | + this into a throw. |
| 84 | + |
| 85 | +4. **Editor context** (`contexts/editor.ts`). `useEditor()` reads `editorContext` and throws if |
| 86 | + used outside a `Provider`. Custom controls combine `useEditor()` with Tiptap's `useEditorState` |
| 87 | + to derive `disabled`/`isActive`. `Provider.tsx` wraps Tiptap's `useEditor`, syncs the `content` |
| 88 | + prop into the editor (HTML, or markdown when `contentType === "markdown"`), and applies the |
| 89 | + DSFR border via `tss-react`. |
| 90 | + |
| 91 | +5. **Dialogs** (`dialogs/`). `Dialog.tsx` wraps `@codegouvfr/react-dsfr` `createModal` behind a |
| 92 | + `forwardRef` `open/close` handle exposed through `dialogContext`. `LinkDialog`/`ImageDialog`/ |
| 93 | + `YoutubeDialog` use `react-hook-form` + `yup` + `validator` for form validation. |
| 94 | + |
| 95 | +## Conventions |
| 96 | + |
| 97 | +- **Optional peer deps.** Tiptap extensions, `react-hook-form`, `yup`, `validator` are optional |
| 98 | + peers — only required when the corresponding control is used. Code must degrade gracefully |
| 99 | + (warn, not crash) when an extension is absent. |
| 100 | +- **Explicit DSFR import suffixes.** Import from `@codegouvfr/react-dsfr` with explicit `.js` / |
| 101 | + `/index.js` (e.g. `@codegouvfr/react-dsfr/Button.js`) for ESM resolution. |
| 102 | +- **Tiptap v3 StarterKit** includes `Link` and `Underline`. When providing your own versions, |
| 103 | + disable them: `StarterKit.configure({ link: false, underline: false })`. |
| 104 | +- **Prettier:** tabWidth 4, printWidth 160, trailingComma `es5`. **Commits:** Conventional Commits |
| 105 | + (enforced by commitlint via the husky `commit-msg` hook). pre-commit runs lint-staged |
| 106 | + (prettier on all files, eslint on `*.{js,ts,jsx,tsx}`). |
| 107 | + |
| 108 | +## Testing |
| 109 | + |
| 110 | +Jest + ts-jest + jsdom + Testing Library. `jest.config.js` sets `transformIgnorePatterns` to |
| 111 | +transform the ESM deps `@codegouvfr`, `@tiptap/markdown`, `marked` — extend this list if a new ESM |
| 112 | +dependency fails to parse. CSS imports are mocked via `__mocks__/styleMock.ts`. Use |
| 113 | +`src/test-utils.ts` (`suppressConsoleError`, `renderHookWithError`) when testing hooks that throw |
| 114 | +(e.g. context guards). |
0 commit comments