diff --git a/.claude/agents/design-system-expert.md b/.claude/agents/design-system-expert.md index a5b8e746..b62da51c 100644 --- a/.claude/agents/design-system-expert.md +++ b/.claude/agents/design-system-expert.md @@ -17,7 +17,7 @@ You are an Angular 22 headless design-system expert for this starter's component | | `storybook/docs/**` (global foundations/specifications docs) | | | `scripts/**`, `.github/**` | -**Out-of-scope requests**: refuse and orient — domain components (project prefix, composition of `ui-*`) and core services belong to the main thread per `AGENTS.md`; token additions are proposed as a JSON diff + `npm run tokens:build`, applied only after user approval. +**Out-of-scope requests**: refuse and orient — domain components (project prefix, composition of `ui-*`) and core services belong to the main thread per `AGENTS.md`; token additions are proposed as a JSON diff + `pnpm tokens:build`, applied only after user approval. ## Sources of truth (consult, never duplicate) @@ -76,7 +76,7 @@ If the verb matches none of these, or the phrasing is ambiguous (“check that 4. Create/modify the component files, **then** the co-located `.stories.ts` and `.mdx` — both mandatory. 5. Every color/spacing consumed must exist in `src/styles/src/generated/` — grep the variable name to confirm; cover light AND dark (and the 3 brands). 6. Update `components-index.md` (check off / add the component). -7. Verify: `npm run lint` + `npm run build-storybook` (the real AoT typecheck). For visual verification, suggest the `/verify` skill. +7. Verify: `pnpm lint` + `pnpm build-storybook` (the real AoT typecheck). For visual verification, suggest the `/verify` skill. 8. Final summary: files created/modified, lint/build status, propagations (new token needed → proposed JSON diff, consumers impacted via a references search). ### AUDIT mode — gap report, **zero modification** @@ -136,14 +136,14 @@ For each component in scope, check: 2. Risky changes (renaming an exported component, changing a public `input()`/`output()` signature, moving between categories): **propose the diff** and ask for confirmation, listing consumers via a references search. 3. Trivial gaps (`*ngIf` → `@if`, `[ngClass]` → `[class]`, decorator → signal, hardcoded value → token, missing `track`): apply directly. 4. **Always** update `.stories.ts` + `.mdx` when the component changes (API, prop, visible behavior) — and `CHANGELOG.md` `[Unreleased]` for user-visible changes. -5. Final verification: `npm run lint` + `npm run build-storybook`. +5. Final verification: `pnpm lint` + `pnpm build-storybook`. ## Guardrails - **Out of scope**: refuse and orient (domain components, core services, token JSON, Figma). -- **Missing token**: never invent a variable — propose the addition in `src/design-tokens/*.json` + `npm run tokens:build`, ask first. +- **Missing token**: never invent a variable — propose the addition in `src/design-tokens/*.json` + `pnpm tokens:build`, ask first. - **Doubt about a convention**: cite the source (AGENTS.md section, rule, reference pattern). Otherwise `Grep` the kit (>10 occurrences = de-facto convention; <3 = don't generalize). -- **Verification**: at minimum `npm run lint` + `npm run build-storybook` after any change; report status. Visual doubts → suggest `/verify`. +- **Verification**: at minimum `pnpm lint` + `pnpm build-storybook` after any change; report status. Visual doubts → suggest `/verify`. - **Never commit** — report the touched files and let the user commit (via `/git-commit`). ## Output format diff --git a/.claude/rules/git-conventions.md b/.claude/rules/git-conventions.md index 67342cf7..03cb7059 100644 --- a/.claude/rules/git-conventions.md +++ b/.claude/rules/git-conventions.md @@ -1,6 +1,6 @@ # Git conventions -Full release workflow (SemVer bumps, `npm version`, tags): see `docs/VERSIONING.md`. +Full release workflow (SemVer bumps, `pnpm version`, tags): see `docs/VERSIONING.md`. ## Commit format — Conventional Commits diff --git a/.claude/settings.json b/.claude/settings.json index b9b3fe11..b57363dd 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -4,9 +4,9 @@ "Read", "Glob", "Grep", - "Bash(npm run lint)", - "Bash(npm run tokens:build)", - "Bash(npm run build-storybook)", + "Bash(pnpm lint)", + "Bash(pnpm tokens:build)", + "Bash(pnpm build-storybook)", "Bash(git status)", "Bash(git diff:*)", "Bash(git log:*)", diff --git a/.claude/skills/generate-ui-component/SKILL.md b/.claude/skills/generate-ui-component/SKILL.md index 4aa3063f..6e95fa4b 100644 --- a/.claude/skills/generate-ui-component/SKILL.md +++ b/.claude/skills/generate-ui-component/SKILL.md @@ -113,7 +113,7 @@ Rules: ### `///` comments = the theming doc (mandatory) -The doc's "Theming" table is **generated** from the `.scss` by `npm run docs:config` +The doc's "Theming" table is **generated** from the `.scss` by `pnpm docs:config` (`scripts/docs.config.mjs` → `storybook/generated/ui-config.json`). So the SCSS comments are what write the doc: nothing to copy elsewhere. @@ -137,7 +137,7 @@ what write the doc: nothing to copy elsewhere. project rebinds the variable. - Describe the **role**, not the binding: the "Default (starter)" column and the chain to `ui-config` are inferred automatically. -- **Every** config variable must have its `///` — `npm run docs:config` counts the missing ones +- **Every** config variable must have its `///` — `pnpm docs:config` counts the missing ones (they don't appear in the table). - **Exposed custom properties** (override points) are documented with a `///` where the hook lives: on its declaration, or on the line reading it with its fallback @@ -224,9 +224,9 @@ import { ConfigTable } from '<…>/storybook/blocks/config-table'; ## Verification -1. `npx tsc --noEmit -p tsconfig.json` → must pass. +1. `pnpm exec tsc --noEmit -p tsconfig.json` → must pass. 2. Compile the SCSS: `node_modules/.bin/sass --load-path=src/styles --no-source-map --quiet ` and check the generated selectors/values. - 2bis. `npm run docs:config` → the component must appear, with **0 variables missing a `///` + 2bis. `pnpm docs:config` → the component must appear, with **0 variables missing a `///` comment** (the script prints the count), and every line resolved to a token, a map, a list, or an accepted literal value. 3. **Live Storybook** (already running on `:6006`, HMR): via the browser tools, open `iframe.html?id=components-ui-{cat}-ui-{name}--&viewMode=story`, measure (getBoundingClientRect, getComputedStyle), test the interaction (click/keyboard), and take a screenshot. Wait for transitions to settle before measuring. diff --git a/.claude/skills/qa-component-methodology/SKILL.md b/.claude/skills/qa-component-methodology/SKILL.md index d869d61b..920d9dd3 100644 --- a/.claude/skills/qa-component-methodology/SKILL.md +++ b/.claude/skills/qa-component-methodology/SKILL.md @@ -47,8 +47,8 @@ juste à l'œil : ## 3. Vérifications statiques ```bash -npx eslint --max-warnings 0 -npx tsc --noEmit -p tsconfig.app.json | grep +pnpm exec eslint --max-warnings 0 +pnpm exec tsc --noEmit -p tsconfig.app.json | grep ``` ## 4. Vérification visuelle/interactive dans Storybook diff --git a/.claude/skills/verify/SKILL.md b/.claude/skills/verify/SKILL.md index 9be382be..addf3834 100644 --- a/.claude/skills/verify/SKILL.md +++ b/.claude/skills/verify/SKILL.md @@ -10,18 +10,24 @@ Project surface: **Storybook** (no meaningful app page; `ng serve` shows almost ## Recipe that works ```bash -npm run build-storybook # AOT of all stories → storybook-static/ +pnpm build-storybook # AOT of all stories → storybook-static/ cd storybook-static && python3 -m http.server 6007 & ``` Drive with Playwright + system Chrome (no browser download): ```js -// npm i playwright in a temporary directory (e.g. /tmp/sb-verify) — not in the repo +// `playwright` is now a repo devDependency, +// so there is no temporary install to do any more: run the script through +// `pnpm exec node ` and import it directly. import { chromium } from 'playwright'; const browser = await chromium.launch({ channel: 'chrome', headless: true }); ``` +`channel: 'chrome'` uses the system Chrome, so no browser download is needed. If it +is missing, `pnpm exec playwright install chromium` fetches the bundled one — at the +version the repo pins, which is also the one `@storybook/test-runner` uses. + - URL of a single story: `http://localhost:6007/iframe.html?id=` - Story IDs: `curl -s localhost:6007/index.json` (`entries`), e.g. `components-ui-forms-ui-input--signal-forms` (title `Components/ui/forms/ui-input` → kebab). diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b3344fdf..03a1fc81 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,18 @@ version: 2 updates: - # npm : cadence hebdomadaire + regroupement par famille technique. Sans ça, + # L'écosystème s'appelle toujours `npm` chez Dependabot : c'est le nom du + # REGISTRE, pas du client. Il lit `pnpm-lock.yaml` (v9) sous cette clé — il n'y + # a pas d'écosystème `pnpm` à basculer + # + # ⚠️ Interaction avec `minimumReleaseAge: 1440` (pnpm-workspace.yaml) : une PR + # qui propose une version publiée depuis moins de 24 h échoue à l'install + # (« no version satisfies minimumReleaseAge »), puis passe d'elle-même à la + # relance suivante. La cadence hebdomadaire ci-dessous rend le cas peu + # fréquent ; pour un correctif de sécurité à prendre tout de suite, passer par + # `minimumReleaseAgeExclude`. + # + # Cadence hebdomadaire + regroupement par famille technique. Sans ça, # Angular (22.0.x → 22.1.x → ...), Storybook et l'outillage ESLint génèrent # chacun leur propre PR à chaque bump — sur cette stack (Angular 22, # Storybook 10, ng-packagr, Style Dictionary), le bruit ferait abandonner @@ -22,6 +33,8 @@ updates: - '@storybook/*' - '@storybook-community/*' - 'storybook' + - 'playwright' + - 'axe-playwright' eslint: patterns: - 'eslint*' @@ -37,6 +50,8 @@ updates: - '@storybook/*' - '@storybook-community/*' - 'storybook' + - 'playwright' + - 'axe-playwright' - 'eslint*' - '@eslint/*' - 'typescript-eslint' diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index f50e2096..4c190346 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -29,7 +29,7 @@ jobs: name: Build Storybook and demo app runs-on: ubuntu-latest steps: - # fetch-depth 0 : `npm run build-storybook` calls `build-info.mjs`, which + # fetch-depth 0 : `pnpm build-storybook` calls `build-info.mjs`, which # runs `git describe --tags` to find the last published version. A # shallow checkout (the default) fetches no tags, so the "ahead of # X.Y.Z" part of the label would silently disappear. @@ -42,20 +42,26 @@ jobs: id: nvmrc run: echo "NODE_VERSION=$(cat .nvmrc)" >> $GITHUB_OUTPUT + # Avant `setup-node` : c'est la présence de `pnpm` sur le PATH qui permet à + # setup-node de localiser le store à mettre en cache. La version installée + # vient du champ `packageManager` du package.json — un seul endroit à bumper. + - name: Install pnpm + uses: pnpm/action-setup@v6 + - name: Set up Node.js uses: actions/setup-node@v7 with: node-version: ${{ steps.nvmrc.outputs.NODE_VERSION }} - cache: 'npm' + cache: 'pnpm' - name: Install dependencies - run: npm ci + run: pnpm install --frozen-lockfile - name: Setup Pages uses: actions/configure-pages@v6 - name: Build Storybook - run: npm run build-storybook + run: pnpm build-storybook - name: Fix base href for GitHub Pages run: | @@ -63,7 +69,7 @@ jobs: sed -i -e 's|||' storybook-static/iframe.html - name: Build demo application - run: npm run build -- --configuration production --base-href "$DEMO_BASE" + run: pnpm build --configuration production --base-href "$DEMO_BASE" - name: Assemble the Pages artifact run: | diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index fafa726e..8acf596f 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -31,25 +31,31 @@ jobs: id: nvmrc run: echo "NODE_VERSION=$(cat .nvmrc)" >> $GITHUB_OUTPUT + # Avant `setup-node` : c'est la présence de `pnpm` sur le PATH qui permet à + # setup-node de localiser le store à mettre en cache. La version installée + # vient du champ `packageManager` du package.json — un seul endroit à bumper. + - name: Install pnpm + uses: pnpm/action-setup@v6 + - name: Set up Node.js uses: actions/setup-node@v7 with: node-version: ${{ steps.nvmrc.outputs.NODE_VERSION }} - cache: 'npm' + cache: 'pnpm' # `postinstall` chains tokens:build → ui-kit:build → docs:config, so this # step already proves the 53 entry points compile. A failing package build # fails the install, before any check below runs. - name: Install dependencies (builds tokens + package) - run: npm ci + run: pnpm install --frozen-lockfile - # `npm run lint` carries --fix: it repairs instead of reporting, so in CI + # `pnpm lint` carries --fix: it repairs instead of reporting, so in CI # it would never fail. lint:check is the same target without it. - name: Lint - run: npm run lint:check + run: pnpm lint:check - name: Typecheck the demo app - run: npx tsc -p tsconfig.app.json --noEmit + run: pnpm exec tsc -p tsconfig.app.json --noEmit # FSHSP-93: unit tests on the kit (vitest, via the `test` architect # target). Covers both the pure-logic specs (mask engine, option @@ -57,7 +63,7 @@ jobs: # specs (select, autocomplete, input-tags, segment-control, tabs, modal, # accordion, base CVA). - name: Unit tests - run: npm run test + run: pnpm test # `postinstall` has just regenerated everything `docs:config` produces, so # this step looks redundant. It is not — it enforces two things nothing @@ -72,7 +78,7 @@ jobs: # - The hand-written component lists, via the `components:check` this # chains — the six places that claim to describe `projects/ui-kit/`. - name: Check the docs (lists, conventions, quoted counts) - run: npm run docs:config:check + run: pnpm docs:config:check # The opposite case, which the step above CANNOT catch: two of those # generated files are committed — `component-vars.scss` ships in the @@ -100,17 +106,23 @@ jobs: id: nvmrc run: echo "NODE_VERSION=$(cat .nvmrc)" >> $GITHUB_OUTPUT + # Avant `setup-node` : c'est la présence de `pnpm` sur le PATH qui permet à + # setup-node de localiser le store à mettre en cache. La version installée + # vient du champ `packageManager` du package.json — un seul endroit à bumper. + - name: Install pnpm + uses: pnpm/action-setup@v6 + - name: Set up Node.js uses: actions/setup-node@v7 with: node-version: ${{ steps.nvmrc.outputs.NODE_VERSION }} - cache: 'npm' + cache: 'pnpm' - name: Install dependencies - run: npm ci + run: pnpm install --frozen-lockfile - name: Build Storybook - run: npm run build-storybook + run: pnpm build-storybook # Consumed by the `a11y` job below — avoids rebuilding Storybook twice. - name: Upload Storybook build @@ -139,14 +151,20 @@ jobs: id: nvmrc run: echo "NODE_VERSION=$(cat .nvmrc)" >> $GITHUB_OUTPUT + # Avant `setup-node` : c'est la présence de `pnpm` sur le PATH qui permet à + # setup-node de localiser le store à mettre en cache. La version installée + # vient du champ `packageManager` du package.json — un seul endroit à bumper. + - name: Install pnpm + uses: pnpm/action-setup@v6 + - name: Set up Node.js uses: actions/setup-node@v7 with: node-version: ${{ steps.nvmrc.outputs.NODE_VERSION }} - cache: 'npm' + cache: 'pnpm' - name: Install dependencies - run: npm ci + run: pnpm install --frozen-lockfile - name: Download Storybook build uses: actions/download-artifact@v8 @@ -154,8 +172,20 @@ jobs: name: storybook-static path: storybook-static + # `playwright` est une devDependency DÉCLARÉE, et doit le rester (FSHSP-177). + # Sous npm cette étape était un `npx playwright install` : npx va chercher le + # paquet sur le registre s'il manque, et npm aplatissait de toute façon le + # binaire transitif dans node_modules/.bin. pnpm ne fait ni l'un ni l'autre — + # `pnpm exec` ne lance QUE ce qui est déclaré. + # + # Ne pas « simplifier » en `pnpm dlx playwright` : dlx tire `latest`, donc les + # navigateurs installés ne correspondraient plus au `playwright-core` que + # `@storybook/test-runner` et `axe-playwright` résolvent, et les tests + # échoueraient sur « Executable doesn't exist » — sans bruit, ce job étant + # `continue-on-error`. Pour la même raison, `playwright` est groupé avec + # `storybook` dans dependabot.yml : les trois doivent bouger ensemble. - name: Install Playwright's Chromium - run: npx playwright install --with-deps chromium + run: pnpm exec playwright install --with-deps chromium - name: Run accessibility checks (axe-core via test-runner) - run: npm run test-storybook:ci + run: pnpm test-storybook:ci diff --git a/.github/workflows/publish-ui-kit.yml b/.github/workflows/publish-ui-kit.yml index 6e17132d..a75cf4b6 100644 --- a/.github/workflows/publish-ui-kit.yml +++ b/.github/workflows/publish-ui-kit.yml @@ -26,6 +26,26 @@ name: Publish @4sh/ui-kit to npm # declared on the package settings page, then issues a short-lived publish right # valid for this job only. The environment gate stays essential: OIDC proves # *where* the publish comes from, never *who* decided it. +# +# ⚠️ DEPENDENCIES ARE INSTALLED WITH pnpm, BUT THE PUBLISH STEPS USE THE npm CLI. +# That is deliberate, not a migration left half-done : do not +# "finish" it: +# +# - The two concerns are orthogonal. pnpm is how this repo resolves its +# dependency graph; `npm publish` is how a tarball is handed to the npm +# registry. Nothing about the tarball depends on which client built it. +# - Trusted Publishing is where they stop being interchangeable. `pnpm publish` +# supported OIDC on pnpm 10, then regressed on 11.0.8 (the registry answers +# 404 because the OIDC exchange never happens) : see pnpm/pnpm#11513, fixed +# by pnpm/pnpm#11526. Publishing is IRREVERSIBLE: a name+version can never be +# reused. Carrying that risk buys nothing here. +# - The npm CLI is also what the guardrail step below inspects (`npm --version` +# >= 11.5.1, and the `_authToken` check). Swapping the client would silently +# invalidate those checks along with the publish itself. +# +# If this ever has to move, the acceptance test is a real dry run against the +# registry from this workflow : not a green local `pnpm publish --dry-run`, which +# never performs the OIDC exchange at all. on: workflow_dispatch: inputs: @@ -49,11 +69,17 @@ jobs: id: nvmrc run: echo "NODE_VERSION=$(cat .nvmrc)" >> $GITHUB_OUTPUT + # Avant `setup-node` : c'est la présence de `pnpm` sur le PATH qui permet à + # setup-node de localiser le store à mettre en cache. La version installée + # vient du champ `packageManager` du package.json. + - name: Install pnpm + uses: pnpm/action-setup@v6 + - name: Set up Node.js uses: actions/setup-node@v7 with: node-version: ${{ steps.nvmrc.outputs.NODE_VERSION }} - cache: 'npm' + cache: 'pnpm' # Before anything expensive, and above all before the irreversible publish: # the release notes are the CHANGELOG section of this version, so a missing @@ -66,13 +92,13 @@ jobs: node scripts/changelog.section.mjs "$version" - name: Install dependencies - run: npm ci + run: pnpm install --frozen-lockfile - name: Build the package - run: npm run ui-kit:build + run: pnpm ui-kit:build # FSHSP-141: `ui-kit:build` alone never embeds the MCP server — that step - # only lives in `ui-kit:pack` (`ui-kit:build && mcp-embed && npm pack`), + # only lives in `ui-kit:pack` (`ui-kit:build && mcp-embed && pnpm pack`), # which this workflow never called. The 0.4.0 tarball shipped without # `mcp/`, silently: `ui-kit:build` succeeds either way, and the dry-run # pack below doesn't diff against what the README promises. Embedding @@ -83,7 +109,7 @@ jobs: run: node scripts/ui-kit-mcp-embed.build.mjs - name: Build the schematics package - run: npm run schematics:build + run: pnpm schematics:build # Les deux packages partent ensemble et portent le MÊME numéro : c'est lui # qui identifie de quel kit vient un fichier copié (en-tête de traçabilité @@ -139,11 +165,17 @@ jobs: # OIDC exchange entirely, and the registry rejects the anonymous PUT with a # misleading `E404 Not Found` (it masks a 403 on scoped packages). # registry.npmjs.org is the default registry anyway, so nothing is lost. + # Avant `setup-node` : c'est la présence de `pnpm` sur le PATH qui permet à + # setup-node de localiser le store à mettre en cache. La version installée + # vient du champ `packageManager` du package.json. + - name: Install pnpm + uses: pnpm/action-setup@v6 + - name: Set up Node.js uses: actions/setup-node@v7 with: node-version: ${{ steps.nvmrc.outputs.NODE_VERSION }} - cache: 'npm' + cache: 'pnpm' # Trusted Publishing needs npm >= 11.5.1. Node 24.15.0 (.nvmrc) ships # 11.12.1, so nothing to install — this only makes the version visible in @@ -192,16 +224,16 @@ jobs: ENVIRONMENT_NAME: npm-publish - name: Install dependencies - run: npm ci + run: pnpm install --frozen-lockfile # Rebuilt here rather than carried over as an artifact, so the published # tarball is unambiguously produced by this job — which is also what the # provenance attestation ends up certifying. - name: Build the package - run: npm run ui-kit:build + run: pnpm ui-kit:build # FSHSP-141: `ui-kit:build` alone never embeds the MCP server — that step - # only lives in `ui-kit:pack` (`ui-kit:build && mcp-embed && npm pack`), + # only lives in `ui-kit:pack` (`ui-kit:build && mcp-embed && pnpm pack`), # which this workflow never called. The 0.4.0 tarball shipped without # `mcp/`, silently: `ui-kit:build` succeeds either way, and the dry-run # pack below doesn't diff against what the README promises. Embedding @@ -212,7 +244,7 @@ jobs: run: node scripts/ui-kit-mcp-embed.build.mjs - name: Build the schematics package - run: npm run schematics:build + run: pnpm schematics:build # ORDER MATTERS — the companion goes FIRST. # diff --git a/.gitignore b/.gitignore index 2231e261..0ed29f57 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ node_modules .temp-tokens .tokenflow -# Generated Tokens (npm run tokens:build) +# Generated Tokens (pnpm tokens:build) projects/ui-kit/styles/generated # Copie servie par Storybook pour le téléchargement (regénérée par docs:config) @@ -22,13 +22,16 @@ storybook/public/component-vars.scss # Index de recherche plein texte de la doc (regénéré par docs:search) storybook/public/text-search-docs.json +# Copie temporaire d'axe-core déposée par le run a11y (test-storybook:ci) +storybook/public/_axe-tmp.js + # Compodoc (generated for Storybook) documentation.json -# Storybook doc config manifest (npm run docs:config) +# Storybook doc config manifest (pnpm docs:config) storybook/generated -# Manifeste doc embarqué dans @4sh/ui-kit-mcp (regénéré par npm run mcp:assets) +# Manifeste doc embarqué dans @4sh/ui-kit-mcp (regénéré par pnpm mcp:assets) projects/ui-kit-mcp/data # Storybook build output @@ -44,6 +47,7 @@ coverage # Logs *.log npm-debug.log* +pnpm-debug.log* yarn-debug.log* yarn-error.log* @@ -69,5 +73,5 @@ yarn-error.log* Thumbs.db ehthumbs.db -# npm pack output (local package testing) +# pnpm pack output (local package testing) *.tgz diff --git a/.idea/runConfigurations/npm_start.xml b/.idea/runConfigurations/pnpm_start.xml similarity index 79% rename from .idea/runConfigurations/npm_start.xml rename to .idea/runConfigurations/pnpm_start.xml index 3b341b39..040ecc56 100644 --- a/.idea/runConfigurations/npm_start.xml +++ b/.idea/runConfigurations/pnpm_start.xml @@ -1,5 +1,5 @@ - + diff --git a/AGENTS.md b/AGENTS.md index 6df75563..f4aaa99b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,6 +3,9 @@ > Single entry point for any AI agent. Read this file first, then consult the indicated sources of truth. For the Figma side (component generation/audit), see `CLAUDE.md`. > > Release/versioning: `docs/VERSIONING.md` + `CHANGELOG.md`. Publishing the npm package: `docs/PUBLISHING.md`. +> +> Security rules (Angular bypass APIs are **banned by default**) and the register of +> authorised exceptions: `docs/SECURITY-PRACTICES.md`. --- @@ -99,13 +102,13 @@ Rules when working in `projects/ui-kit/`: for `ui-image`) — both wired in `src/app/app.config.ts` and `storybook/preview.ts`. - The kit's SCSS foundation lives in `projects/ui-kit/styles/` (NOT `src/styles/`, which is app-only): a published package cannot depend on the host app. Tokens are - generated there (`tokens.config.json`), and `npm run ui-kit:styles` compiles + generated there (`tokens.config.json`), and `pnpm ui-kit:styles` compiles `index.scss` → `dist/ui-kit/styles.css`, shipped alongside the SCSS sources. - **Never `@forward` anything that emits CSS from `utils.scss`.** Each component `.scss` is its own Sass compilation unit, so emitted rules get duplicated into all 54 components. Global utility classes belong in `index.scss` (and, app-side, in `src/styles/main.scss`). -- `npm run ui-kit:build` runs before the app/Storybook (chained into `serve` / +- `pnpm ui-kit:build` runs before the app/Storybook (chained into `serve` / `build` / `storybook` / `build-storybook` / `postinstall`), since `@4sh/ui-kit/*` resolves to `dist/ui-kit/`. @@ -174,6 +177,30 @@ holds what is specific to the demo app (aggregator, fonts, grid settings, layout ## Non-negotiable code rules +### Never bypass Angular's protections + +`DomSanitizer.bypassSecurityTrust…()`, a direct `innerHTML` / `outerHTML` write, +`insertAdjacentHTML()`, `eval()`, `new Function()` — **banned by default**, and +enforced by `no-restricted-syntax` in `eslint.config.js` (TypeScript **and** HTML +templates), which is blocking in CI. + +```typescript +// ✅ Angular sanitises it +this.sanitizer.sanitize(SecurityContext.HTML, value); + +// ❌ marks the value safe WITHOUT checking it +this.sanitizer.bypassSecurityTrustHtml(value); +``` + +Angular only sanitises what goes through a **template binding**. `el.innerHTML = x` +is not sanitised by anything. + +An exception is raised in three steps, never fewer: **scrub the value in code you +can test**, justify it on the spot (`/* eslint-disable-next-line no-restricted-syntax +-- EXCEPTION JUSTIFIÉE: … */`), and add it to the register in +`docs/SECURITY-PRACTICES.md`. The kit holds **one** bypass in total (`ui-image`, for +inline SVG) — read §3 of that doc before adding a second one. + ### Angular Signals — always ```typescript @@ -268,7 +295,7 @@ pitfall, extension point). No paraphrasing of what the next line does. #### A `///` on a config variable = the published doc In a **component** `.scss`, a `///` on a declaration (`$var` or custom property) is the -**public contract**: `npm run docs:config` reads it and the doc's "Theming" section displays it. +**public contract**: `pnpm docs:config` reads it and the doc's "Theming" section displays it. `//` remains the internal note, invisible in the doc. (The `///` on mixins/functions in `projects/ui-kit/styles/utils/` is never scanned: the generator only reads components.) @@ -288,7 +315,7 @@ $card-radius: var(--radius-md); /// Rayon des coins. - A **custom property** is only public if it carries a `///`, placed where the hook lives (declaration, or a fallback read `var(--ui-x, )`) — never on an internal dark-mode override, otherwise the doc shows the wrong default value. -- Every config variable must have its `///`: `npm run docs:config` counts the missing ones. +- Every config variable must have its `///`: `pnpm docs:config` counts the missing ones. #### Every structural value is read through a `--ui-*` hook @@ -314,7 +341,7 @@ $stroke-width: var( - **Naming**: `--ui-{family}[-{part}]-{property}[-{modifier}]`, modifier **last** (same rule as the tokens: `actions-high-surface-hover`). `{family}` is the entry-point folder minus `ui-`, so a sub-component uses its family (`ui-tab-list.scss` → `--ui-tabs-*`). The property vocabulary - and the modifier list live in `scripts/component-vars.build.mjs`; `npm run docs:config` **fails** + and the modifier list live in `scripts/component-vars.build.mjs`; `pnpm docs:config` **fails** on a hook that doesn't parse, so extend the vocabulary there rather than inventing a name. - **Inside a map**, every value carries its own hook (`height: var(--ui-button-height-small, …)`). - **What gets one**: dimensions, spacings, gaps, radii, stroke/focus-ring widths, font sizes, @@ -335,7 +362,7 @@ $stroke-width: var( The `///` stays on the mirror: that is what publishes the public hook's role. - Never a value hardcoded inline in a rule when it is structural: promote it to a config variable with its hook and its `///`. -- `npm run docs:config` regenerates `projects/ui-kit/styles/component-vars.scss` (the consumer's copy-me +- `pnpm docs:config` regenerates `projects/ui-kit/styles/component-vars.scss` (the consumer's copy-me theme) and `figma/component-vars.json` from these hooks. Both are committed; `docs:config:check` fails when they are stale, when a hook doesn't parse, or when a hook has no `///`. Values in both files are read from the **compiled CSS**, not from the SCSS text — so @@ -412,7 +439,7 @@ $focus-ring-width: utils.$form-focus-ring-width; // ← replace the value here t - **Tables**: always in HTML tags (``, ``, `
`) rather than native Markdown in `.mdx` files, to guarantee rendering and CSS control. - **"Theming" section**: never written by hand. `` - (`storybook/blocks/config-table.js`), fed by `npm run docs:config` from the `.scss`'s `///` + (`storybook/blocks/config-table.js`), fed by `pnpm docs:config` from the `.scss`'s `///` comments. It is **always the last section** of the page. Optional prose before the table for whatever it doesn't say (architecture, pointer to a shell like `ui-field`); no paragraph explaining how to read the table — that's in the block's tooltip. Several @@ -427,7 +454,7 @@ $focus-ring-width: utils.$form-focus-ring-width; // ← replace the value here t 1. Read `projects/ui-kit//ui-/ui-.stories.ts` → identify `argTypes` 2. Read `projects/ui-kit//ui-/src/lib/` → check types and structure 3. Modify `.ts`, `.html`, `.scss` (tokens only) — any added config variable carries its `///` -4. Update the story + the `.mdx` if the API changes; `npm run docs:config` if the SCSS config moved +4. Update the story + the `.mdx` if the API changes; `pnpm docs:config` if the SCSS config moved 5. Verify light + dark + the 3 brands (Storybook → `Foundations / Colors` for the tokens) ### Create a ui-* component (generic) @@ -437,7 +464,7 @@ $focus-ring-width: utils.$form-focus-ring-width; // ← replace the value here t 3. Create the story + the `.mdx` **co-located** in `projects/ui-kit//ui-/` (outside `src/`; global doc only → `storybook/docs/`) — pick `` from `storybook/docs/Overview.mdx`'s sections, or ask if the component doesn't fit an existing one 4. Check off the component in `components-index.md`, add its card to `Overview.mdx`, add it to the family table of **both** package READMEs (EN + FR) -5. `npm run docs:config:check` — it is what tells you which of those you forgot +5. `pnpm docs:config:check` — it is what tells you which of those you forgot > File **placement** does not drive the Storybook sidebar tree — the `title` does > (``, or the story's `title:`). Placement only decides what ships: a @@ -453,7 +480,7 @@ Like a `ui-*`, but: project prefix, `domain/` folder, and **composition of `ui-* ### Add / modify a token 1. Edit `src/design-tokens/*.json` (semantics reference primitives; never a primitive directly in a component) -2. `npm run tokens:build` (adding a collection/mode → edit `tokens.config.json`, not the script) +2. `pnpm tokens:build` (adding a collection/mode → edit `tokens.config.json`, not the script) 3. Verify in Storybook `Foundations / Colors` --- @@ -469,14 +496,14 @@ Like a `ui-*`, but: project prefix, `domain/` folder, and **composition of `ui-* ## Commands ```bash -npm start # Launch Storybook (source of truth) — alias of npm run storybook -npm run serve # Launch the Angular app (minimal demo) -npm run tokens:build # Regenerate the CSS variables from the JSON -npm run docs:search # Rebuild the doc full-text search index -npm run build-storybook # Static Storybook build -npm run lint # ESLint --fix -npm test # Unit tests on the kit -npm run docs:config:check # Guardrail on the hand-written doc — see below +pnpm start # Launch Storybook (source of truth) — alias of pnpm storybook +pnpm serve # Launch the Angular app (minimal demo) +pnpm tokens:build # Regenerate the CSS variables from the JSON +pnpm docs:search # Rebuild the doc full-text search index +pnpm build-storybook # Static Storybook build +pnpm lint # ESLint --fix +pnpm test # Unit tests on the kit +pnpm docs:config:check # Guardrail on the hand-written doc — see below ``` `docs:config:check` is what catches a doc that has drifted from the code, and **you are diff --git a/CHANGELOG.md b/CHANGELOG.md index 886e97df..4961b1a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -154,6 +154,9 @@ Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le pr qu'un bouton qui n'effectue pas toujours ce qu'il annonce, il est retiré de `DEFAULT_EDITOR_TOOLS` en attendant, sans rien retirer du kit — le tool, l'icône et `clearFormatMarkers()` restent en place pour un projet qui l'ajoute explicitement. +- **`ng add @4sh/ui-kit-schematics` écrit les commandes du gestionnaire de paquets du projet, plus `npm run` en dur** (FSHSP-177). Les scripts posés dans le `package.json` du consommateur (`storybook`, `build-storybook`, le hook `postinstall`) et la commande de démarrage affichée en fin d'installation nommaient `npm run` quel que soit le projet. Dans un projet pnpm ou yarn, ces scripts fonctionnaient — npm est toujours présent — mais contournaient le lockfile et faisaient apparaître un `package-lock.json` parasite au premier lancement. Le gestionnaire est désormais déduit du champ `packageManager` puis, à défaut, du lockfile présent, dans le même ordre de précédence que le CLI Angular (pnpm, yarn, bun, npm). L'**installation** des dépendances, elle, était déjà correcte : le CLI Angular transmet le gestionnaire qu'il détecte à `NodePackageInstallTask`. +- **Les scripts de la chaîne de doc copiés chez le consommateur nomment son gestionnaire dans leurs messages d'erreur** (FSHSP-177). `docs.config.mjs` et `docs.search.mjs` conseillaient « lance `npm run docs:config` » ; ils lisent maintenant `npm_config_user_agent` (renseigné par npm, pnpm, yarn et bun) et affichent une commande copiable telle quelle. Les messages rendus **dans le navigateur** (table `ConfigTable`, addons de recherche), qui n'ont pas accès à cette information, nomment désormais le script seul plutôt qu'un client. +- **`projects/ui-kit/README.md` (EN/FR) mène l'installation avec `pnpm add`** (FSHSP-177), en précisant que npm et yarn installent exactement la même chose — le package reste un tarball npm standard. C'est le fichier affiché sur npmjs.com. - **`ui-datepicker` : la plage sélectionnée (`range`) se lit comme une seule zone continue** (FSHSP-166). Elle était peinte sur la cellule elle-même — fond en dégradé mi-cellule sur les deux extrémités, filets haut et bas en bordure — ce qui la faisait _encadrer_ la sélection plutôt que la porter, avec deux débordements visibles : les filets couraient sur toute la largeur des cellules de début et de fin, donc au-delà de la moitié réellement remplie, et la pastille des jours de début/fin, dont la bordure transparente s'ajoutait à sa taille (`content-box`), dépassait la hauteur de la bande. La bande est désormais une couche à part, exactement de la hauteur d'une pastille et arrêtée au centre des deux extrémités : plus rien ne peut la dépasser, ni en hauteur ni sur les côtés. Les filets sont retirés, et seuls les deux bouts d'une ligne de semaine sont arrondis — sur le retrait qu'une pastille laisse dans sa colonne, pas sur le bord de la cellule, pour qu'une plage finissant sur la dernière colonne s'aligne avec la rangée du dessus au lieu de s'arrêter une demi-gouttière trop tôt. Et une date de début dont la fin n'est pas encore choisie n'affiche plus de demi-bande partant vers une extrémité qui n'existe pas : la pastille reste seule jusqu'au second clic. Deux nouveaux réglages, `--ui-datepicker-range-background` et `--ui-datepicker-range-color`, permettent de re-skinner la bande et le chiffre qu'elle porte (voir la table « Theming » de la doc). @@ -173,6 +176,9 @@ Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le pr - **Corriger un segment déjà tapé décalait tout ce qui suit** (FSHSP-179). Remplacer le mois « 07 » par un seul chiffre re-découpait le flux de chiffres complet : « 08/07/2026 » devenait « 08/12/026 », le « 2 » de l'année promu dans le mois et l'année réduite à trois chiffres. Le masque ne sait re-dériver le champ qu'en frappe séquentielle vers l'avant (ou en suppression depuis la fin) ; dès qu'une frappe n'a pas lieu en fin de texte, il **cède la place pour le reste de la saisie** — texte libre, parsé au blur/Entrée, exactement ce qui s'appliquait déjà quand une valeur existe. Il se réarme au commit suivant, ou dès que le champ se lit vide. - **Le panneau de `ui-datepicker` débordait de l'écran sur un viewport étroit**. Trois causes cumulées, corrigées ensemble : le panneau était plafonné à `100vw - units-lg` mais la colonne de mois gardait une largeur fixe de 360px sans pouvoir rétrécir, donc la dernière colonne de jours et le chevron « suivant » passaient sous la bordure ; l'overlay CDK n'avait ni marge de viewport ni repoussage (`push`), donc un champ ancré n'importe où ailleurs qu'à gauche sortait le panneau hors de l'écran ; et en `numberOfMonths > 1` la rangée de mois faisait deux fois 360px, dont le second mois entièrement invisible. La colonne de mois et les grilles mois / année rétrécissent maintenant avec le panneau, la pastille d'un jour suit la largeur de sa colonne quand celle-ci passe sous `size/components/default` (la bande d'une plage la suit aussi), les mois s'empilent verticalement sous 600px avec un filet horizontal en séparateur, et l'overlay garde 12px de gouttière avec les bords du viewport. Le panneau défile désormais au-delà de `100dvh - units-3xl` plutôt que de déborder en hauteur (paysage sur téléphone), et un panneau `inline` ne dépasse plus la largeur de son conteneur. +- **`ui-image` inlinait les SVG locaux sans les assainir** (FSHSP-177). Le composant est le seul du kit à lever un `bypassSecurityTrustHtml()` — inévitable, puisque l'assainisseur d'Angular supprime `` en entier et que l'inline est ce qui permet à l'asset d'hériter du CSS (`currentColor`, tokens de thème). Mais la réponse HTTP y était passée **telle quelle** : un SVG servi depuis `assets/img/` exécutait son propre JavaScript dans l'origine de l'application. Vérifié dans un navigateur réel : un `` glissé dans un `` s'exécutait **sans aucune interaction**, et les attributs `onload`/`onclick`/`onerror` survivaient dans le DOM. Le markup passe désormais par un scrub explicite (`sanitizeInlineSvg`) avant le bypass : `script`, `foreignObject`, `iframe`, `object`, `embed` et les éléments SMIL (`animate`, `animateTransform`, `animateMotion`, `set`, `handler`) sont retirés **avec leur contenu** ; tout attribut `on…` est supprimé ; `href`/`xlink:href`/`src` sont réduits à une référence intra-document (`#id`) ou à une URL `http(s)`, ce qui écarte `javascript:` et `data:`. Le parsing a lieu dans un `