Compare commits
11 Commits
feat/propo
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 266b364195 | |||
| cb4cd01409 | |||
| a09dbca800 | |||
| 874c630a2a | |||
| 5aec056e0d | |||
| fd9612d333 | |||
| 927d8b6f85 | |||
| 5149bdec89 | |||
| 5f34a1d393 | |||
| 84c295d4ce | |||
| 04d6db10af |
35
.gitea/workflows/ci.yml
Normal file
35
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["**"]
|
||||
pull_request:
|
||||
branches: ["master"]
|
||||
|
||||
jobs:
|
||||
build-and-anchors:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:20-bookworm-slim
|
||||
|
||||
steps:
|
||||
- name: Install git (needed by checkout)
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends git ca-certificates
|
||||
git --version
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install deps
|
||||
run: npm ci
|
||||
|
||||
- name: Inline scripts syntax check
|
||||
run: node scripts/check-inline-js.mjs
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Anchors contract
|
||||
run: npm run test:anchors
|
||||
58
README.md
58
README.md
@@ -1,43 +1,25 @@
|
||||
# Astro Starter Kit: Minimal
|
||||
# Archicratie — Web Edition (Atelier éditorial)
|
||||
|
||||
```sh
|
||||
npm create astro@latest -- --template minimal
|
||||
```
|
||||
Ce repo contient le site Astro “Archicratie – Web Edition” et sa machine éditoriale :
|
||||
- Proposer (tickets Gitea pré-remplis depuis les paragraphes)
|
||||
- Apply-ticket (application sûre des corrections)
|
||||
- Contrat d’ancres (citabilité) + churn test
|
||||
- CI Gitea Actions + runner DS220+
|
||||
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
## Démarrage rapide
|
||||
- `npm install`
|
||||
- `npm run dev`
|
||||
|
||||
## 🚀 Project Structure
|
||||
## Documentation (référence)
|
||||
- **Quickstart (10 min)** : `docs/QUICKSTART.md`
|
||||
- **Manuel de référence** : `docs/MANUEL_REFERENCE.md`
|
||||
- **Contrat de tickets** : `docs/CONTRAT_TICKETS.md`
|
||||
- **Topo logique métier** : `docs/TOPO_LOGIQUE_METIER.md`
|
||||
|
||||
Inside of your Astro project, you'll see the following folders and files:
|
||||
## Commandes clés
|
||||
- Tests complets : `npm test`
|
||||
- Test ancres : `npm run test:anchors`
|
||||
- Appliquer un ticket :
|
||||
- `node scripts/apply-ticket.mjs <N> --dry-run`
|
||||
- `node scripts/apply-ticket.mjs <N>`
|
||||
|
||||
```text
|
||||
/
|
||||
├── public/
|
||||
├── src/
|
||||
│ └── pages/
|
||||
│ └── index.astro
|
||||
└── package.json
|
||||
```
|
||||
|
||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||
|
||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||
|
||||
Any static assets, like images, can be placed in the `public/` directory.
|
||||
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `npm install` | Installs dependencies |
|
||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||
| `npm run build` | Build your production site to `./dist/` |
|
||||
| `npm run preview` | Preview your build locally, before deploying |
|
||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||
|
||||
29
docs/CONTRAT_TICKETS.md
Normal file
29
docs/CONTRAT_TICKETS.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Contrat de ticket — Proposer / Apply-ticket / Auto-label
|
||||
|
||||
Ce document fixe le format minimal et les invariants des tickets d’édition.
|
||||
Objectif : parsing fiable par scripts + workflows.
|
||||
|
||||
## Invariants (non négociables)
|
||||
Doivent toujours exister dans le body du ticket :
|
||||
|
||||
- `Chemin: /.../`
|
||||
- `URL locale: ...#...` (utile pour audit humain)
|
||||
- `Ancre: #p-...`
|
||||
- `Type: type/...`
|
||||
- `State: state/...`
|
||||
- `Proposition (remplacer par):`
|
||||
|
||||
## Texte actuel : best effort
|
||||
Priorité :
|
||||
1) `Texte actuel (copie exacte du paragraphe):`
|
||||
2) sinon `Texte actuel (extrait):` + note de troncature
|
||||
|
||||
> Même si le texte actuel est un extrait, l’ancre + chemin rendent le ticket opposable.
|
||||
|
||||
## Catégorie (optionnelle)
|
||||
- `Category: cat/...` (ou vide)
|
||||
|
||||
## Pourquoi ce contrat ?
|
||||
- `apply-ticket.mjs` dépend de repères textuels stables
|
||||
- `auto-label-issues` dépend de `Type/State/Category`
|
||||
- on veut éviter des tickets “illisibles machine” qui cassent l’industrialisation
|
||||
206
docs/MANUEL_REFERENCE.md
Normal file
206
docs/MANUEL_REFERENCE.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# Manuel de référence — Archicratie Web Edition (Site + Gitea + Runner)
|
||||
|
||||
Ce manuel explique comment utiliser et maintenir l’outil d’édition :
|
||||
- Site Astro “Archicratie – Web Edition”
|
||||
- Proposer (tickets Gitea pré-remplis)
|
||||
- Apply-ticket (application semi-automatique dans le contenu)
|
||||
- Contrat d’ancres (citabilité) + tests
|
||||
- CI Gitea Actions + ds220-runner (DS220+)
|
||||
|
||||
> Objectif : une boucle éditoriale reproductible, opposable, sûre, et testée.
|
||||
|
||||
---
|
||||
|
||||
## 0) Glossaire minimal (anti-jargon)
|
||||
- **PR** (Pull Request) : demande de fusion d’une branche vers `master`.
|
||||
- **PAT** (Personal Access Token) : jeton d’accès Gitea utilisé comme “mot de passe” pour API/Git.
|
||||
- **Anchor / Ancre** : identifiant stable d’un paragraphe (ex: `p-8-0e65838d`) utilisable dans une URL `#...`.
|
||||
- **Churn** : variation des ancres d’une page entre deux builds (mesure de stabilité).
|
||||
- **dist/** : sortie buildée (artefact), jamais la “source de vérité”.
|
||||
- **CI** : automatisation (build + tests) à chaque push/PR.
|
||||
|
||||
---
|
||||
|
||||
## 1) Pré-requis (poste local)
|
||||
### Outils
|
||||
- Node.js + npm
|
||||
- Git
|
||||
- (Optionnel) Python3 pour scripts de debug ponctuels
|
||||
|
||||
### Accès
|
||||
- Compte Gitea avec droits sur le repo
|
||||
- Un **PAT** Gitea
|
||||
- Accès au site local (dev) et/ou build (dist)
|
||||
|
||||
---
|
||||
|
||||
## 2) Vue d’ensemble : la boucle éditoriale (rituel)
|
||||
### Boucle standard (la “cadence courte”)
|
||||
1) Sur le site, cliquer **Proposer** sur un paragraphe.
|
||||
2) Choisir Type (Correction / Fact-check), puis Category.
|
||||
3) Gitea ouvre un ticket pré-rempli (Chemin/URL/Ancre + texte actuel).
|
||||
4) Rédiger la **Proposition (remplacer par)** + Justification.
|
||||
5) En local :
|
||||
- `node scripts/apply-ticket.mjs <num>` (d’abord `--dry-run`)
|
||||
- `npm run test:anchors`
|
||||
- `npm run build`
|
||||
6) Commit + push + PR si nécessaire.
|
||||
|
||||
---
|
||||
|
||||
## 3) Le site “Proposer / Citer / ¶” (para-tools)
|
||||
### Où ça vit ?
|
||||
- `src/layouts/EditionLayout.astro` injecte un script qui :
|
||||
- ajoute “¶” (lien d’ancre), “Citer”, “Proposer”
|
||||
- construit l’URL de création d’issue Gitea
|
||||
- embarque si possible le **paragraphe exact** (sinon un extrait)
|
||||
|
||||
- `src/components/ProposeModal.astro` gère la modal 2 étapes :
|
||||
- Type (correction/fact-check)
|
||||
- Category (cat/style, cat/lexique, etc.)
|
||||
- ouvre ensuite l’issue en **nouvel onglet** sans quitter le site
|
||||
|
||||
### Invariants (non négociables)
|
||||
- **Ancre + URL + Chemin** doivent toujours être présents.
|
||||
- Le mode “texte exact” est *best effort* :
|
||||
- si trop long → fallback extrait + note
|
||||
- priorité absolue : rester robuste et cliquable.
|
||||
|
||||
### Limites / règles de taille
|
||||
- **FULL_TEXT_SOFT_LIMIT** : taille max pour tenter d’embarquer “copie exacte”
|
||||
- **URL_HARD_LIMIT** : si l’URL devient trop longue → repasse en extrait
|
||||
|
||||
> But : éviter des issues amputées + éviter des URLs trop longues / fragiles.
|
||||
|
||||
---
|
||||
|
||||
## 4) Format des tickets (machine-readable)
|
||||
Le parsing des tickets est fait par `scripts/apply-ticket.mjs` + workflow auto-label.
|
||||
Donc le contenu doit rester “parsable”.
|
||||
|
||||
Sections attendues (au minimum) :
|
||||
- `Chemin: /.../`
|
||||
- `Ancre: #p-...`
|
||||
- `Texte actuel (...)` (idéalement “copie exacte”, sinon “extrait”)
|
||||
- `Proposition (remplacer par): ...`
|
||||
- `Justification: ...` (peut être vide, mais le champ doit exister si possible)
|
||||
|
||||
> Voir aussi : docs/CONTRAT_TICKETS.md
|
||||
|
||||
---
|
||||
|
||||
## 5) Appliquer un ticket en local : `apply-ticket`
|
||||
### Commandes
|
||||
- Dry run (recommandé) :
|
||||
- `node scripts/apply-ticket.mjs <N> --dry-run`
|
||||
|
||||
- Application réelle :
|
||||
- `node scripts/apply-ticket.mjs <N>`
|
||||
|
||||
### Comportements de sûreté (guardrails)
|
||||
- `--dry-run` :
|
||||
- **aucun fichier écrit**
|
||||
- **aucun backup créé**
|
||||
- affiche BEFORE/AFTER (extrait)
|
||||
|
||||
- Mode écriture :
|
||||
- crée un backup `.bak.issue-<N>` uniquement si écriture
|
||||
- match “best effort” mais refuse si score insuffisant
|
||||
|
||||
### Après application
|
||||
- `git diff -- <fichier>`
|
||||
- `git add <fichier>`
|
||||
- `git commit -m "edit: apply ticket #N (...)"`
|
||||
|
||||
---
|
||||
|
||||
## 6) Contrat de citabilité : ancres + churn test
|
||||
### Pourquoi ?
|
||||
Les ancres sont le “socle de citabilité”. On veut prévenir les liens morts.
|
||||
|
||||
### Scripts
|
||||
- `npm run test:anchors`
|
||||
- compare les ancres de `dist/` avec une baseline
|
||||
- calcule le churn et signale les pages modifiées
|
||||
|
||||
- `npm run test:anchors:update`
|
||||
- met à jour la baseline (à faire seulement quand c’est volontaire)
|
||||
|
||||
> Important : `dist/` est un artefact ; la baseline sert à mesurer, pas à “éditer dist”.
|
||||
|
||||
---
|
||||
|
||||
## 7) Garde-fou JS inline : `check-inline-js`
|
||||
Pourquoi : éviter qu’un JS inline cassé supprime les boutons et fasse disparaître “Proposer/Citer”.
|
||||
|
||||
- `node scripts/check-inline-js.mjs`
|
||||
- intégré dans `npm test`
|
||||
|
||||
---
|
||||
|
||||
## 8) CI (Gitea Actions)
|
||||
### Objectif
|
||||
Sur chaque push/PR : vérifier automatiquement que :
|
||||
- build OK
|
||||
- ancres OK
|
||||
- JS inline OK
|
||||
|
||||
### Workflow
|
||||
- `.gitea/workflows/ci.yml`
|
||||
|
||||
### Commande “contrat”
|
||||
- `npm test`
|
||||
- exécute : build + anchors + inline-js
|
||||
|
||||
---
|
||||
|
||||
## 9) ds220-runner (DS220+ / Gitea act-runner)
|
||||
### Comportement normal
|
||||
Le runner lance des jobs dans des conteneurs éphémères qui démarrent/stop.
|
||||
DSM peut notifier “conteneur arrêté de manière inattendue” : c’est souvent un faux positif “bruyant”.
|
||||
|
||||
### À surveiller vraiment
|
||||
- Jobs en échec côté Gitea Actions
|
||||
- logs mentionnant :
|
||||
- auth API (401)
|
||||
- labels manquants
|
||||
- npm ci/build en échec
|
||||
|
||||
---
|
||||
|
||||
## 10) Dépannage (symptômes → causes fréquentes)
|
||||
### “Proposer” ouvre deux onglets / remplace l’onglet courant
|
||||
Cause : double handler click / manque de `stopPropagation` / mauvais fallback.
|
||||
Fix : le flux doit ouvrir **nouvel onglet uniquement**, et sinon prompt.
|
||||
|
||||
### “Proposer/Citer/¶” disparaissent
|
||||
Cause : JS inline cassé → exception ou erreur syntaxe.
|
||||
Fix : `node scripts/check-inline-js.mjs` + vérifier `dist/.../index.html` console.
|
||||
|
||||
### apply-ticket : “Proposition introuvable”
|
||||
Cause : ticket sans section “Proposition (remplacer par):”.
|
||||
Fix : respecter le contrat de ticket.
|
||||
|
||||
### apply-ticket : “Match trop faible”
|
||||
Cause : texte actuel absent / trop tronqué / mismatch.
|
||||
Fix :
|
||||
- privilégier “Texte actuel (copie exacte du paragraphe)”
|
||||
- sinon le script récupère via `dist` (si possible).
|
||||
|
||||
---
|
||||
|
||||
## 11) Règles de contribution (discipline de repo)
|
||||
- Toute modif éditoriale passe par ticket → apply-ticket → tests.
|
||||
- Toute modif structurelle (layouts, rendu, MDX) doit être suivie de :
|
||||
- `npm test`
|
||||
- éventuellement `npm run test:anchors:update` si changement volontaire d’ancres.
|
||||
- Ne jamais éditer `dist/` à la main.
|
||||
|
||||
---
|
||||
|
||||
## 12) Commandes utiles (raccourcis)
|
||||
- Dev : `npm run dev`
|
||||
- Build : `npm run build`
|
||||
- Tests : `npm test`
|
||||
- Anchors : `npm run test:anchors`
|
||||
- Apply ticket : `node scripts/apply-ticket.mjs <N> --dry-run`
|
||||
80
docs/QUICKSTART.md
Normal file
80
docs/QUICKSTART.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Quickstart — 10 minutes (Proposer → Ticket → Apply → Tests)
|
||||
|
||||
## 1) Pré-requis
|
||||
- Node.js + npm
|
||||
- Accès Gitea (compte) + PAT si usage API (apply-ticket)
|
||||
|
||||
## 2) Lancer le site
|
||||
en bash :
|
||||
npm install
|
||||
npm run dev
|
||||
|
||||
Dans le navigateur, url : http://localhost:4321
|
||||
|
||||
## 3) Proposer une correction
|
||||
|
||||
Ouvre une page (ex: Prologue).
|
||||
|
||||
Sur un paragraphe : clique Proposer.
|
||||
|
||||
Choisis :
|
||||
|
||||
Type : Correction / Fact-check
|
||||
|
||||
Category (optionnel)
|
||||
|
||||
Un nouvel onglet Gitea s’ouvre sur une issue pré-remplie.
|
||||
|
||||
Rédige :
|
||||
|
||||
Proposition (remplacer par):
|
||||
|
||||
Justification:
|
||||
|
||||
## 4) Appliquer le ticket en local (<NUMERO> = numéro du ticket non pas id-paragraphe)
|
||||
|
||||
En bash :
|
||||
|
||||
# dry-run (recommandé)
|
||||
node scripts/apply-ticket.mjs <NUMERO> --dry-run
|
||||
|
||||
# appliquer
|
||||
node scripts/apply-ticket.mjs <NUMERO>
|
||||
git diff
|
||||
git add <fichier>
|
||||
git commit -m "edit: apply ticket #<NUMERO> (...)"
|
||||
|
||||
## 5) Vérifier avant push
|
||||
|
||||
npm test
|
||||
|
||||
## 6) Règle d’or
|
||||
|
||||
Ne jamais éditer dist/ à la main.
|
||||
|
||||
Toujours garder Chemin + Ancre + Proposition dans le ticket.
|
||||
|
||||
Déplacer/ajouter les deux docs que je t’ai donnés
|
||||
- `docs/MANUEL_REFERENCE.md`
|
||||
- `docs/CONTRAT_TICKETS.md`
|
||||
|
||||
*(Tu peux reprendre mes versions telles quelles.)*
|
||||
|
||||
---
|
||||
|
||||
## Commandes terminal P0 (copier-coller)
|
||||
En bash
|
||||
mkdir -p docs
|
||||
|
||||
# crée/édite les fichiers avec ton éditeur habituel
|
||||
# README.md
|
||||
# docs/QUICKSTART.md
|
||||
# docs/MANUEL_REFERENCE.md
|
||||
# docs/CONTRAT_TICKETS.md
|
||||
|
||||
git status -sb
|
||||
npm test
|
||||
git add docs/QUICKSTART.md docs/MANUEL_REFERENCE.md docs/CONTRAT_TICKETS.md
|
||||
git commit -m "docs: add quickstart + reference manual + ticket contract"
|
||||
git push
|
||||
|
||||
250
docs/TOPO_LOGIQUE_METIER.md
Normal file
250
docs/TOPO_LOGIQUE_METIER.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# Topo – Logique métier de l’outil éditorial (Web Edition ↔ Gitea ↔ apply-ticket ↔ CI)
|
||||
|
||||
Ce document explique **ce que fait réellement l’outil**, pourquoi il existe, et comment le maintenir **sans casser** la promesse centrale : **citabilité durable + édition traçable + intégration sûre**.
|
||||
|
||||
> Public visé : “commun des mortels” (nouveau contributeur, mainteneur occasionnel, moi dans 6 mois).
|
||||
|
||||
---
|
||||
|
||||
## 0) La promesse (le “pourquoi”)
|
||||
|
||||
On veut un site (Astro) où chaque paragraphe est :
|
||||
- **citable** (URL + ancre stable),
|
||||
- **corrigeable** (ouvrir un ticket pré-rempli depuis le paragraphe),
|
||||
- **réparable de manière sûre** (appliquer la proposition dans les sources sans magie),
|
||||
- **vérifié automatiquement** (tests + build + garde-fous en CI).
|
||||
|
||||
En bref :
|
||||
**Lecture → proposition → ticket → application → tests → build → publication**
|
||||
…sans perdre la traçabilité, ni casser les citations historiques.
|
||||
|
||||
---
|
||||
|
||||
## 1) Glossaire (pas de jargon sans définition)
|
||||
|
||||
- **Ancre** : partie `#...` d’une URL (ex : `#p-8-0e65838d`). Le navigateur saute à l’élément HTML portant cet `id`.
|
||||
- **Paragraphe citable** : un `<p id="p-...">...</p>` dans l’article.
|
||||
- **Ticket / Issue** : demande de correction sur Gitea (ex : #14).
|
||||
- **PR (Pull Request)** : “demande d’ajout / fusion” : une proposition de merge d’une branche vers `master`.
|
||||
- **CI** : tests automatiques exécutés sur le serveur à chaque push/PR.
|
||||
- **Runner** : machine/conteneur qui exécute la CI (chez nous : DS220+ / ds220-runner).
|
||||
- **DEV** : `npm run dev` (serveur Astro à la volée, pas de `dist/`).
|
||||
- **BUILD / PROD-like** : `npm run build` puis servir `dist/` (ce qui ressemble à la prod).
|
||||
|
||||
---
|
||||
|
||||
## 2) Vue d’ensemble : le pipeline (la “machine éditoriale”)
|
||||
|
||||
### 2.1 Le flux humain (ce que fait l’éditeur)
|
||||
1) Lire une page sur “Archicratie – Web Edition”.
|
||||
2) Sur un paragraphe : cliquer **Proposer**.
|
||||
3) Choisir **Type** (Correction / Fact-check) + **Category** (lexique, style, etc.).
|
||||
4) Gitea s’ouvre en **nouvel onglet** avec un ticket pré-rempli.
|
||||
5) L’éditeur écrit la proposition, justifie, et valide le ticket.
|
||||
|
||||
### 2.2 Le flux technique (ce que fait la machine)
|
||||
6) `apply-ticket` récupère le ticket via l’API Gitea.
|
||||
7) Il retrouve le bon fichier source (MDX) + le bon paragraphe (matching sûr).
|
||||
8) Il applique la modification (ou refuse si trop incertain).
|
||||
9) On lance `npm test` → build + test ancres + check JS inline.
|
||||
10) On commit / push.
|
||||
11) La CI revalide automatiquement sur le serveur.
|
||||
|
||||
---
|
||||
|
||||
## 3) Composant A — Le site (Astro) et les outils de paragraphe
|
||||
|
||||
Dans le rendu final, chaque paragraphe important reçoit un `id` de forme :
|
||||
|
||||
- `p-<index>-<hash8>`
|
||||
ex : `p-8-0e65838d`
|
||||
|
||||
Ensuite, un script ajoute des **outils de paragraphe** (“para-tools”) :
|
||||
- **¶** : lien direct sur le paragraphe (ancre)
|
||||
- **Citer** : copie une citation (titre + version + URL sans query-string)
|
||||
- **Proposer** : ouvre la modale (2 étapes), puis ouvre le ticket Gitea
|
||||
|
||||
### Point crucial : progressive enhancement
|
||||
- Si JS marche : UX optimale.
|
||||
- Si JS casse : le site reste lisible, et le lien de proposition reste utilisable au minimum via `href`.
|
||||
|
||||
---
|
||||
|
||||
## 4) Composant B — Contrat des tickets (machine-readable)
|
||||
|
||||
Pour que `apply-ticket` puisse travailler, le ticket doit rester **structuré**.
|
||||
|
||||
Le ticket contient des lignes “clé: valeur” (une par ligne), par ex :
|
||||
- `Chemin: /archicratie/prologue/`
|
||||
- `URL locale: http://localhost:4321/archicratie/prologue/#p-...`
|
||||
- `Ancre: #p-8-...`
|
||||
- `Version: ...`
|
||||
- `Type: type/correction` (ou type/fact-check)
|
||||
- `State: state/recevable` (ou state/a-sourcer)
|
||||
- `Category: cat/lexique` (optionnel)
|
||||
|
||||
**Règle d’or :** ces clés doivent rester simples, stables, sur une ligne chacune.
|
||||
Sinon, scripts + CI + auto-labeling deviennent fragiles.
|
||||
|
||||
---
|
||||
|
||||
## 5) Composant C — apply-ticket (appliquer sans magie)
|
||||
|
||||
`apply-ticket` fait 4 choses :
|
||||
|
||||
1) **Fetch** : récupère le ticket via API (FORGE_TOKEN requis).
|
||||
2) **Parse** : extrait Chemin/Ancre/Proposition/(Texte actuel si présent).
|
||||
3) **Match** : retrouve le paragraphe dans le fichier source (MDX) :
|
||||
- idéal : “Texte actuel (copie exacte…)” → match fort
|
||||
- sinon : match par score → si score trop faible, **refus** (sécurité)
|
||||
4) **Apply** : modifie le fichier source, sans casser le reste.
|
||||
|
||||
### Mode sûr
|
||||
- `--dry-run` : montre BEFORE/AFTER, **n’écrit rien**
|
||||
- mode normal : écrit + propose ensuite `git diff`, `git add`, `git commit`
|
||||
|
||||
---
|
||||
|
||||
## 6) Composant D — Citabilité P0 : ancres, churn test, aliases
|
||||
|
||||
### 6.1 Le problème
|
||||
Si l’ID dépend du contenu, modifier un paragraphe peut changer l’ID.
|
||||
Donc une citation historique `...#ancien` casse.
|
||||
|
||||
### 6.2 La solution robuste (“web native”)
|
||||
On ne “résout” pas l’ancre : **on la fait exister**.
|
||||
|
||||
On maintient un fichier :
|
||||
- `src/anchors/anchor-aliases.json`
|
||||
|
||||
Exemple (par page/chemin) :
|
||||
en json
|
||||
{
|
||||
"/archicratie/prologue/": {
|
||||
"p-8-e7075fe3": "p-8-0e65838d"
|
||||
}
|
||||
}
|
||||
|
||||
Au build, un script injecte dans dist/.../index.html :
|
||||
<span class="para-alias" id="p-8-e7075fe3"></span>
|
||||
<p id="p-8-0e65838d">...</p>
|
||||
|
||||
Résultat :
|
||||
|
||||
le navigateur résout #p-8-e7075fe3 sans JS.
|
||||
|
||||
### 6.3 Pourquoi un fallback JS existe encore ?
|
||||
|
||||
En mode npm run dev, on ne passe pas par dist/, donc pas d’injection build-time.
|
||||
Le fallback JS est un filet :
|
||||
|
||||
utile en DEV,
|
||||
|
||||
utile si un alias manque,
|
||||
|
||||
mais pas la solution principale.
|
||||
|
||||
## 7) Tests & garde-fous (qualité automatique)
|
||||
### 7.1 npm test doit rester “simple et vrai”
|
||||
|
||||
Il exécute :
|
||||
|
||||
npm run build (génère dist)
|
||||
|
||||
npm run test:anchors (stabilité des ancres)
|
||||
|
||||
node scripts/check-inline-js.mjs (évite les scripts inline invalides)
|
||||
|
||||
### 7.2 test:anchors (baseline + churn)
|
||||
|
||||
tests/anchors-baseline.json = snapshot de référence.
|
||||
|
||||
check-anchors compare dist à la baseline.
|
||||
|
||||
Si trop de churn → échec (donc on voit la casse).
|
||||
|
||||
On met à jour la baseline uniquement quand on accepte consciemment la nouvelle réalité :
|
||||
npm run test:anchors:update
|
||||
|
||||
## 8) DEV vs BUILD : comprendre sans se tromper
|
||||
|
||||
npm run dev → serveur Astro “live”, pas de dist/
|
||||
⇒ les alias build-time n’existent pas ⇒ fallback JS peut s’activer.
|
||||
|
||||
npm run build && npx serve dist → rendu final “prod-like”
|
||||
⇒ alias injectés ⇒ citations historiques fonctionnent sans JS.
|
||||
|
||||
Repère immédiat :
|
||||
|
||||
port 4321 = DEV
|
||||
|
||||
port 3000 (serve dist) = PROD-like
|
||||
|
||||
## 9) Rituels opérationnels (les 3 recettes)
|
||||
### Recette 1 — Créer un ticket propre
|
||||
|
||||
Cliquer “Proposer”
|
||||
|
||||
Choisir Type + Category
|
||||
|
||||
Dans Gitea : compléter “Proposition (remplacer par)” + “Justification”
|
||||
|
||||
Ne pas détruire les lignes Chemin / Ancre / Type / State / Category
|
||||
|
||||
### Recette 2 — Appliquer un ticket
|
||||
en bash :
|
||||
node scripts/apply-ticket.mjs <ID> --dry-run
|
||||
node scripts/apply-ticket.mjs <ID>
|
||||
git diff
|
||||
npm test
|
||||
git add ...
|
||||
git commit -m "edit: apply ticket #<ID> (<chemin>#<ancre>)"
|
||||
git push
|
||||
|
||||
### Recette 3 — Quand un ID change (citabilité)
|
||||
|
||||
trouver ancien id + nouveau id
|
||||
|
||||
ajouter alias dans src/anchors/anchor-aliases.json
|
||||
|
||||
npm run build
|
||||
|
||||
vérifier ...#ancien → scroll sur le bon paragraphe
|
||||
|
||||
npm test
|
||||
|
||||
## 10) Règles d’or (invariants non négociables)
|
||||
|
||||
Ne jamais éditer dist/ à la main (artefact).
|
||||
|
||||
Ne jamais sacrifier l’ancre : Chemin + Ancre doivent exister.
|
||||
|
||||
Tickets toujours “parsables” (clés stables ligne-par-ligne).
|
||||
|
||||
apply-ticket doit pouvoir refuser quand c’est ambigu.
|
||||
|
||||
npm test doit rester “le bouton rouge” fiable.
|
||||
|
||||
## 11) Dépannage rapide
|
||||
|
||||
401 invalid token : FORGE_TOKEN absent ou mal exporté.
|
||||
|
||||
Header invalid value : token collé avec des caractères parasites (espaces, retours, texte autour).
|
||||
|
||||
Proposer ouvre 2 onglets / remplace la page : bug d’interception click → vérifier preventDefault + stopPropagation et l’“openInNewTab”.
|
||||
|
||||
Ancien #id ne marche plus :
|
||||
|
||||
en PROD-like : vérifier alias injecté + JSON
|
||||
|
||||
en DEV : normal que fallback JS soit requis
|
||||
|
||||
## 12) Où lire quoi (docs)
|
||||
|
||||
docs/QUICKSTART.md : démarrer vite
|
||||
|
||||
docs/MANUEL_REFERENCE.md : usage complet + procédures
|
||||
|
||||
docs/CONTRAT_TICKETS.md : format strict des issues
|
||||
|
||||
(ce doc) docs/TOPO_LOGIQUE_METIER.md : la logique qui relie tout
|
||||
@@ -7,9 +7,10 @@
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"postbuild": "npx pagefind --site dist",
|
||||
"postbuild": "node scripts/inject-anchor-aliases.mjs && npx pagefind --site dist",
|
||||
"import": "node scripts/import-docx.mjs",
|
||||
"apply:ticket": "node scripts/apply-ticket.mjs",
|
||||
"test": "npm run build && npm run test:anchors && node scripts/check-inline-js.mjs",
|
||||
"test:anchors": "node scripts/check-anchors.mjs",
|
||||
"test:anchors:update": "node scripts/check-anchors.mjs --update"
|
||||
},
|
||||
|
||||
@@ -29,14 +29,29 @@ async function walk(dir) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Contrat : .reading p[id^="p-"]
|
||||
// Contrat :
|
||||
// - paragraphes citables : .reading p[id^="p-"]
|
||||
// - alias web-natifs : .reading span.para-alias[id^="p-"]
|
||||
function extractIds(html) {
|
||||
if (!html.includes('class="reading"')) return [];
|
||||
const ids = [];
|
||||
const re = /<p\b[^>]*\sid="(p-[^"]+)"/g;
|
||||
let m;
|
||||
while ((m = re.exec(html))) ids.push(m[1]);
|
||||
|
||||
const ids = [];
|
||||
let m;
|
||||
|
||||
// 1) IDs principaux (paragraphes)
|
||||
const reP = /<p\b[^>]*\sid="(p-[^"]+)"/g;
|
||||
while ((m = reP.exec(html))) ids.push(m[1]);
|
||||
|
||||
// 2) IDs alias (spans injectés)
|
||||
// cas A : id="..." avant class="...para-alias..."
|
||||
const reA1 = /<span\b[^>]*\bid="(p-[^"]+)"[^>]*\bclass="[^"]*\bpara-alias\b[^"]*"/g;
|
||||
while ((m = reA1.exec(html))) ids.push(m[1]);
|
||||
|
||||
// cas B : class="...para-alias..." avant id="..."
|
||||
const reA2 = /<span\b[^>]*\bclass="[^"]*\bpara-alias\b[^"]*"[^>]*\bid="(p-[^"]+)"/g;
|
||||
while ((m = reA2.exec(html))) ids.push(m[1]);
|
||||
|
||||
// Dé-doublonnage (on garde un ordre stable)
|
||||
const seen = new Set();
|
||||
const uniq = [];
|
||||
for (const id of ids) {
|
||||
|
||||
70
scripts/check-inline-js.mjs
Normal file
70
scripts/check-inline-js.mjs
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const SRC = path.join(ROOT, "src");
|
||||
|
||||
async function* walk(dir) {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const e of entries) {
|
||||
const full = path.join(dir, e.name);
|
||||
if (e.isDirectory()) yield* walk(full);
|
||||
else yield full;
|
||||
}
|
||||
}
|
||||
|
||||
function extractInlineScripts(astroText) {
|
||||
// capture <script ... is:inline ...> ... </script>
|
||||
const re = /<script\b[^>]*\bis:inline\b[^>]*>([\s\S]*?)<\/script>/gi;
|
||||
const out = [];
|
||||
let m;
|
||||
while ((m = re.exec(astroText))) out.push(m[1] ?? "");
|
||||
return out;
|
||||
}
|
||||
|
||||
function checkSyntax(js, label) {
|
||||
const tmp = path.join(os.tmpdir(), `inline-js-check-${Date.now()}-${Math.random().toString(16).slice(2)}.mjs`);
|
||||
const payload = `// ${label}\n${js}\n`;
|
||||
return fs.writeFile(tmp, payload, "utf-8").then(() => {
|
||||
const r = spawnSync(process.execPath, ["--check", tmp], { encoding: "utf-8" });
|
||||
fs.unlink(tmp).catch(() => {});
|
||||
if (r.status !== 0) {
|
||||
const msg = (r.stderr || r.stdout || "").trim();
|
||||
throw new Error(`${label}\n${msg}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const targets = [];
|
||||
for await (const f of walk(SRC)) {
|
||||
if (f.endsWith(".astro")) targets.push(f);
|
||||
}
|
||||
|
||||
let checked = 0;
|
||||
|
||||
for (const file of targets) {
|
||||
const txt = await fs.readFile(file, "utf-8");
|
||||
const scripts = extractInlineScripts(txt);
|
||||
if (!scripts.length) continue;
|
||||
|
||||
for (let i = 0; i < scripts.length; i++) {
|
||||
const js = (scripts[i] || "").trim();
|
||||
if (!js) continue;
|
||||
const label = `${path.relative(ROOT, file)} :: <script is:inline> #${i + 1}`;
|
||||
await checkSyntax(js, label);
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`OK inline-js: scripts checked=${checked}`);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("❌ inline-js syntax check failed");
|
||||
console.error(e?.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
143
scripts/inject-anchor-aliases.mjs
Normal file
143
scripts/inject-anchor-aliases.mjs
Normal file
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
const CWD = process.cwd();
|
||||
const DIST_ROOT = path.join(CWD, "dist");
|
||||
const ALIASES_PATH = path.join(CWD, "src", "anchors", "anchor-aliases.json");
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const DRY_RUN = argv.includes("--dry-run");
|
||||
const STRICT = argv.includes("--strict");
|
||||
|
||||
function escRe(s) {
|
||||
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function normalizeRoute(route) {
|
||||
let r = String(route || "").trim();
|
||||
if (!r.startsWith("/")) r = "/" + r;
|
||||
if (!r.endsWith("/")) r = r + "/";
|
||||
r = r.replace(/\/{2,}/g, "/");
|
||||
return r;
|
||||
}
|
||||
|
||||
async function exists(p) {
|
||||
try {
|
||||
await fs.access(p);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function hasId(html, id) {
|
||||
const re = new RegExp(`\\bid=(["'])${escRe(id)}\\1`, "i");
|
||||
return re.test(html);
|
||||
}
|
||||
|
||||
function injectBeforeId(html, newId, injectHtml) {
|
||||
// insère juste avant la balise qui porte id="newId"
|
||||
const re = new RegExp(
|
||||
`(<[^>]+\\bid=(["'])${escRe(newId)}\\2[^>]*>)`,
|
||||
"i"
|
||||
);
|
||||
const m = html.match(re);
|
||||
if (!m || m.index == null) return { html, injected: false };
|
||||
const i = m.index;
|
||||
const out = html.slice(0, i) + injectHtml + "\n" + html.slice(i);
|
||||
return { html: out, injected: true };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!(await exists(ALIASES_PATH))) {
|
||||
console.log("ℹ️ Aucun fichier d'aliases (src/anchors/anchor-aliases.json). Skip.");
|
||||
return;
|
||||
}
|
||||
|
||||
const raw = await fs.readFile(ALIASES_PATH, "utf-8");
|
||||
/** @type {Record<string, Record<string,string>>} */
|
||||
const aliases = JSON.parse(raw);
|
||||
|
||||
const routes = Object.keys(aliases || {});
|
||||
if (routes.length === 0) {
|
||||
console.log("ℹ️ Aliases vides. Rien à injecter.");
|
||||
return;
|
||||
}
|
||||
|
||||
let changedFiles = 0;
|
||||
let injectedCount = 0;
|
||||
let warnCount = 0;
|
||||
|
||||
for (const routeKey of routes) {
|
||||
const route = normalizeRoute(routeKey);
|
||||
const map = aliases[routeKey] || {};
|
||||
const entries = Object.entries(map);
|
||||
|
||||
if (entries.length === 0) continue;
|
||||
|
||||
const rel = route.replace(/^\/+|\/+$/g, ""); // sans slash
|
||||
const htmlPath = path.join(DIST_ROOT, rel, "index.html");
|
||||
|
||||
if (!(await exists(htmlPath))) {
|
||||
const msg = `⚠️ dist introuvable pour route=${route} (${htmlPath})`;
|
||||
if (STRICT) throw new Error(msg);
|
||||
console.log(msg);
|
||||
warnCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
let html = await fs.readFile(htmlPath, "utf-8");
|
||||
let fileChanged = false;
|
||||
|
||||
for (const [oldId, newId] of entries) {
|
||||
if (!oldId || !newId) continue;
|
||||
|
||||
if (hasId(html, oldId)) {
|
||||
// alias déjà présent → idempotent
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!hasId(html, newId)) {
|
||||
const msg = `⚠️ newId introuvable: ${route} old=${oldId} -> new=${newId}`;
|
||||
if (STRICT) throw new Error(msg);
|
||||
console.log(msg);
|
||||
warnCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const aliasSpan = `<span id="${oldId}" class="para-alias" aria-hidden="true"></span>`;
|
||||
const r = injectBeforeId(html, newId, aliasSpan);
|
||||
|
||||
if (!r.injected) {
|
||||
const msg = `⚠️ injection impossible (pattern non trouvé) : ${route} new=${newId}`;
|
||||
if (STRICT) throw new Error(msg);
|
||||
console.log(msg);
|
||||
warnCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
html = r.html;
|
||||
fileChanged = true;
|
||||
injectedCount++;
|
||||
}
|
||||
|
||||
if (fileChanged) {
|
||||
changedFiles++;
|
||||
if (!DRY_RUN) await fs.writeFile(htmlPath, html, "utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ inject-anchor-aliases: files_changed=${changedFiles} aliases_injected=${injectedCount} warnings=${warnCount}` +
|
||||
(DRY_RUN ? " (dry-run)" : "")
|
||||
);
|
||||
|
||||
if (STRICT && warnCount > 0) process.exit(2);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("💥 inject-anchor-aliases:", e?.message || e);
|
||||
process.exit(1);
|
||||
});
|
||||
6
src/anchors/anchor-aliases.json
Normal file
6
src/anchors/anchor-aliases.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"/archicratie/prologue/": {
|
||||
"p-8-e7075fe3": "p-8-0e65838d"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
const upsertLine = (text, key, value) => {
|
||||
const re = new RegExp(`^\\s*${esc(key)}\\s*:\\s*.*$`, "mi");
|
||||
|
||||
// value vide => supprimer la ligne si elle existe
|
||||
if (!value) {
|
||||
if (!re.test(text)) return text;
|
||||
return (
|
||||
@@ -113,8 +114,10 @@
|
||||
);
|
||||
}
|
||||
|
||||
// remplace si existe
|
||||
if (re.test(text)) return text.replace(re, `${key}: ${value}`);
|
||||
|
||||
// sinon append
|
||||
const sep = text && !text.endsWith("\n") ? "\n" : "";
|
||||
return text + sep + `${key}: ${value}\n`;
|
||||
};
|
||||
@@ -148,23 +151,47 @@
|
||||
|
||||
if (next === body) return;
|
||||
|
||||
const prev = u.toString();
|
||||
u.searchParams.set("body", next);
|
||||
|
||||
// garde-fou URL
|
||||
if (u.toString().length > URL_HARD_LIMIT) {
|
||||
// revert
|
||||
u.searchParams.set("body", body);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// ✅ Ouvre EN NOUVEL ONGLET sans jamais remplacer l’onglet courant
|
||||
const openInNewTab = (url) => {
|
||||
// 1) tente window.open
|
||||
try {
|
||||
const w = window.open(url, "_blank", "noopener,noreferrer");
|
||||
if (w) return true;
|
||||
} catch {}
|
||||
|
||||
// 2) fallback "anchor click" (souvent mieux toléré)
|
||||
try {
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener noreferrer";
|
||||
a.style.display = "none";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
return true;
|
||||
} catch {}
|
||||
|
||||
// 3) dernier recours: on ne quitte PAS la page
|
||||
window.prompt("Popup bloquée. Copiez ce lien pour ouvrir le ticket :", url);
|
||||
return false;
|
||||
};
|
||||
|
||||
const openWith = (url) => {
|
||||
pending = url;
|
||||
kind = "";
|
||||
dlg.dataset.step = "1";
|
||||
if (typeof dlg.showModal === "function") dlg.showModal();
|
||||
else window.open(url.toString(), "_blank", "noopener,noreferrer");
|
||||
else openInNewTab(url.toString());
|
||||
};
|
||||
|
||||
// Intercepte UNIQUEMENT les liens marqués data-propose
|
||||
@@ -173,6 +200,7 @@
|
||||
if (!a) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const rawUrl = a.dataset.url || a.getAttribute("href") || "";
|
||||
if (!rawUrl || rawUrl === "#") return;
|
||||
@@ -191,7 +219,7 @@
|
||||
|
||||
openWith(u);
|
||||
} catch {
|
||||
window.open(rawUrl, "_blank", "noopener,noreferrer");
|
||||
openInNewTab(rawUrl);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -232,14 +260,14 @@
|
||||
const cat = btn.getAttribute("data-category") || "";
|
||||
let body = pending.searchParams.get("body") || "";
|
||||
body = upsertLine(body, "Category", cat);
|
||||
|
||||
pending.searchParams.set("body", body);
|
||||
|
||||
const u = pending.toString();
|
||||
dlg.close();
|
||||
|
||||
const w = window.open(u, "_blank", "noopener,noreferrer");
|
||||
if (!w) window.location.href = u;
|
||||
// ✅ ouvre en nouvel onglet, sans jamais remplacer l’onglet courant
|
||||
openInNewTab(u);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -76,6 +76,27 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
||||
history.replaceState(null, "", window.location.pathname + window.location.hash);
|
||||
}
|
||||
|
||||
// ✅ Hotfix compat citabilité :
|
||||
// si l'URL pointe vers un ancien hash (#p-8-xxxxxxxx) qui n'existe plus,
|
||||
// on retombe sur le paragraphe actuel du même index (#p-8-YYYYYYYY).
|
||||
(function resolveLegacyParagraphHash() {
|
||||
const h = window.location.hash || "";
|
||||
const m = h.match(/^#p-(\d+)-[0-9a-f]{8}$/i);
|
||||
if (!m) return;
|
||||
|
||||
const oldId = h.slice(1);
|
||||
if (document.getElementById(oldId)) return; // l'ancre existe, rien à faire
|
||||
|
||||
const idx = m[1];
|
||||
const prefix = `p-${idx}-`;
|
||||
const replacement = document.querySelector(`[id^="${prefix}"]`);
|
||||
if (!replacement) return;
|
||||
|
||||
console.warn("[anchors] legacy hash fallback used:", `#${oldId}`, "→", `#${replacement.id}`);
|
||||
// On ne réécrit PAS le hash : on garde la citabilité historique visible.
|
||||
replacement.scrollIntoView({ block: "start" });
|
||||
})();
|
||||
|
||||
const docTitle = document.body.dataset.docTitle || document.title;
|
||||
const docVersion = document.body.dataset.docVersion || "";
|
||||
|
||||
@@ -90,7 +111,7 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
||||
const quoteBlock = (s) =>
|
||||
String(s || "")
|
||||
.split(/\r?\n/)
|
||||
.map((l) => `> ${l}`.trimEnd())
|
||||
.map((l) => (`> ${l}`).trimEnd())
|
||||
.join("\n");
|
||||
|
||||
function buildIssueURL(anchorId, fullText, excerpt) {
|
||||
@@ -128,7 +149,7 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
||||
`Texte actuel (extrait):`,
|
||||
quoteBlock(excerpt || ""),
|
||||
``,
|
||||
`Note: paragraphe long → extrait (texte complet copié au clic si possible).`,
|
||||
`Note: paragraphe long → extrait (pour éviter une URL trop longue).`,
|
||||
];
|
||||
|
||||
const footer = [
|
||||
@@ -214,9 +235,6 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
||||
// la modal lit data-url en priorité (garde aussi href).
|
||||
propose.dataset.url = issueUrl;
|
||||
|
||||
// Option B : texte complet disponible au clic (presse-papier + upgrade)
|
||||
propose.dataset.full = raw;
|
||||
|
||||
tools.appendChild(propose);
|
||||
}
|
||||
|
||||
|
||||
@@ -121,3 +121,10 @@ body[data-reading-level="2"] .level-3 { display: none; }
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.para-alias {
|
||||
display: block;
|
||||
height: 0;
|
||||
/* ajuste si header sticky : */
|
||||
scroll-margin-top: var(--scroll-margin-top, 96px);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"p-5-85126fa5",
|
||||
"p-6-3515039d",
|
||||
"p-7-64a0ca9c",
|
||||
"p-8-e7075fe3",
|
||||
"p-8-0e65838d",
|
||||
"p-9-5ff70fb7",
|
||||
"p-10-e250e810",
|
||||
"p-11-594bf307",
|
||||
@@ -141,7 +141,8 @@
|
||||
"p-134-358f5875",
|
||||
"p-135-c19330ce",
|
||||
"p-136-17f1cf51",
|
||||
"p-137-d8f1539e"
|
||||
"p-137-d8f1539e",
|
||||
"p-8-e7075fe3"
|
||||
],
|
||||
"atlas/00-demarrage/index.html": [
|
||||
"p-0-97681330"
|
||||
|
||||
Reference in New Issue
Block a user