@lystech/sections 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,55 +1,109 @@
1
- # @lystech/sections
2
-
3
- Le contenu d'un site, modifiable par son propriétaire — sans reconstruction ni
4
- déploiement.
5
-
6
- ## Pourquoi un paquet à part de `@lystech/core`
7
-
8
- `core` exige React 18 et embarque antd, Mantine et React Query : 2,4 Mo de
9
- JavaScript et 1,2 Mo de CSS. Une clinique qui veut changer trois textes n'a
10
- aucune raison de charger un magasin, un panier et PayPal — et beaucoup de sites
11
- clients sont en React 19.
12
-
13
- Ce paquet pèse **2,9 Ko** et ne dépend que de React. `core` le réexporte, pour
14
- qu'une boutique qui l'a déjà n'ait rien de plus à installer.
15
-
16
- ## La plateforme fixe l'alphabet, pas les mots
17
-
18
- Le serveur ne connaît aucun type de bloc. « plat-du-jour », « praticien »,
19
- « tarifs » appartiennent au site, qui les déclare. Ce que la plateforme
20
- connaît, ce sont les types de champs — `text`, `richText`, `image`, `number`,
21
- `boolean`, `url`, `date`, `choice`, `list` — vrais dans tous les métiers, et
22
- suffisants pour fabriquer un formulaire pour un bloc jamais vu.
23
-
24
- ## Usage
25
-
26
- ```tsx
27
- import { SectionsProvider, useSection } from "@lystech/sections";
28
-
29
- <SectionsProvider site="armoise" page="massage" langue={i18n.language}>
30
- <MaPage />
31
- </SectionsProvider>;
32
-
33
- // Dans un composant déjà écrit :
34
- const { contenu: tarifs } = useSection("tarifs-massotherapie", {
35
- defaut: SITE.tarifs.massotherapie, // ce que le site affichait déjà
36
- });
37
- ```
38
-
39
- ### Le repli n'est pas une option
40
-
41
- `defaut` porte les valeurs actuelles du site. Tant que rien n'est publié — ou
42
- si l'API ne répond pas — ce sont elles qui s'affichent. Brancher un site
43
- existant ne peut donc rien casser : au pire il continue d'afficher ce qu'il
44
- affichait hier.
45
-
46
- ### Publié ≠ brouillon
47
-
48
- Le site public ne lit QUE le contenu publié. Un brouillon enregistré ne change
49
- rien pour les visiteurs tant que personne n'a cliqué « Publier ».
50
-
51
- ## Champs traduits
52
-
53
- Un champ déclaré `localized: true` stocke `{ fr: …, en: … }` ; le paquet rend
54
- la langue courante, avec repli sur une autre langue plutôt que sur du vide —
55
- une version anglaise oubliée vaut mieux affichée en français qu'absente.
1
+ # @lystech/sections
2
+
3
+ Le contenu d'un site, modifiable par son propriétaire — sans reconstruction ni
4
+ déploiement.
5
+
6
+ ## Pourquoi un paquet à part de `@lystech/core`
7
+
8
+ `core` exige React 18 et embarque antd, Mantine et React Query : 2,4 Mo de
9
+ JavaScript et 1,2 Mo de CSS. Une clinique qui veut changer trois textes n'a
10
+ aucune raison de charger un magasin, un panier et PayPal — et beaucoup de sites
11
+ clients sont en React 19.
12
+
13
+ Ce paquet pèse **4,7 Ko** en lecture (1,9 Ko compressé) et ne dépend que de
14
+ React. `core` le réexporte, pour qu'une boutique qui l'a déjà n'ait rien de
15
+ plus à installer.
16
+
17
+ ## La plateforme fixe l'alphabet, pas les mots
18
+
19
+ Le serveur ne connaît aucun type de bloc. « plat-du-jour », « praticien »,
20
+ « tarifs » appartiennent au site, qui les déclare. Ce que la plateforme
21
+ connaît, ce sont les types de champs — `text`, `richText`, `image`, `number`,
22
+ `boolean`, `url`, `date`, `choice`, `list` — vrais dans tous les métiers, et
23
+ suffisants pour fabriquer un formulaire pour un bloc jamais vu.
24
+
25
+ ## Usage
26
+
27
+ ```tsx
28
+ import { SectionsProvider, useSection } from "@lystech/sections";
29
+
30
+ <SectionsProvider site="armoise" page="massage" langue={i18n.language}>
31
+ <MaPage />
32
+ </SectionsProvider>;
33
+
34
+ // Dans un composant déjà écrit :
35
+ const { contenu: tarifs } = useSection("tarifs-massotherapie", {
36
+ defaut: SITE.tarifs.massotherapie, // ce que le site affichait déjà
37
+ });
38
+ ```
39
+
40
+ ### Le repli n'est pas une option
41
+
42
+ `defaut` porte les valeurs actuelles du site. Tant que rien n'est publié — ou
43
+ si l'API ne répond pas — ce sont elles qui s'affichent. Brancher un site
44
+ existant ne peut donc rien casser : au pire il continue d'afficher ce qu'il
45
+ affichait hier.
46
+
47
+ ### Publié ≠ brouillon
48
+
49
+ Le site public ne lit QUE le contenu publié. Un brouillon enregistré ne change
50
+ rien pour les visiteurs tant que personne n'a cliqué « Publier ».
51
+
52
+ ## Champs traduits
53
+
54
+ Un champ déclaré `localized: true` stocke `{ fr: …, en: … }` ; le paquet rend
55
+ la langue courante, avec repli sur une autre langue plutôt que sur du vide —
56
+ une version anglaise oubliée vaut mieux affichée en français qu'absente.
57
+
58
+ ## L'éditeur en place
59
+
60
+ La personne qui édite ouvre SON site avec un jeton dans l'adresse. Le paquet le
61
+ détecte, le retire aussitôt de la barre d'adresse, et charge l'interface
62
+ d'édition — 22 Ko qu'aucun visiteur ne télécharge jamais, parce que le
63
+ `import()` qui y mène n'est atteint que s'il y a un jeton.
64
+
65
+ Elle voit alors son site, ses polices, sa mise en page, avec chaque bloc
66
+ modifiable encadré. Ce qu'elle tape apparaît dedans à mesure qu'elle le tape.
67
+
68
+ ### Marquer les blocs dans la page
69
+
70
+ L'éditeur doit savoir quel rectangle de la page correspond à quel bloc. Aucun
71
+ moyen fiable de le deviner : le site le dit.
72
+
73
+ ```tsx
74
+ const { contenu, ancrage } = useSection("tarifs", { defaut });
75
+ return <section {...ancrage}>…</section>;
76
+ ```
77
+
78
+ Hors édition, c'est un attribut `data-` inerte : aucun poids, aucun effet.
79
+
80
+ ### Trois états, jamais confondus
81
+
82
+ | | Qui le voit |
83
+ |---|---|
84
+ | **Aperçu** | la personne qui édite, dans son navigateur |
85
+ | **Brouillon** | elle seule, enregistré sur le serveur |
86
+ | **Publié** | les visiteurs |
87
+
88
+ ### Le jeton
89
+
90
+ Délivré par `POST /site-sections/edit-token` depuis la console Lystech. Il vaut
91
+ quelques heures, pour UNE organisation, et il est **plafonné à l'éditeur** même
92
+ si la personne est administratrice : un lien qui fuite ne peut pas ouvrir la
93
+ console. Le droit est relu dans les rôles à chaque requête, donc retirer un
94
+ accès ferme les jetons déjà émis sur-le-champ.
95
+
96
+ ## Sites prérendus
97
+
98
+ Un site généré au build affiche les replis du code dans son HTML, puis bascule
99
+ sur le contenu publié une fois le JavaScript chargé — l'ancien prix clignote
100
+ vers le nouveau, et les moteurs de recherche n'indexent que l'ancien.
101
+
102
+ Passez le contenu au premier rendu pour supprimer les deux problèmes :
103
+
104
+ ```tsx
105
+ <SectionsProvider site="armoise" page="massage" sectionsInitiales={dejaLu}>
106
+ ```
107
+
108
+ où `dejaLu` vient d'un appel, dans le script de prérendu, à
109
+ `GET /site-sections/public/<site>/<page>`.
@@ -0,0 +1,78 @@
1
+ import { ReactNode } from 'react';
2
+ import { SourceTexte } from './sourceTexte';
3
+ import { SectionPubliee } from './types';
4
+ type Etat = "chargement" | "pret" | "erreur";
5
+ type Contexte = {
6
+ /** Sous-domaine de l'organisation dans Lystech. */
7
+ site: string;
8
+ /** La page courante — l'éditeur en a besoin pour créer une section. */
9
+ page: string;
10
+ /** Langue courante — décide quelle version d'un champ traduit est rendue. */
11
+ langue: string;
12
+ apiBaseUrl: string;
13
+ sections: SectionPubliee[];
14
+ etat: Etat;
15
+ recharger: () => void;
16
+ /**
17
+ * L'APERÇU : ce que l'éditeur est en train d'écrire, avant publication.
18
+ *
19
+ * Il prime sur le contenu publié, par type de bloc. C'est ce qui rend
20
+ * l'édition « en place » : la cliente voit ses mots dans SA mise en page,
21
+ * avec SES polices et SES couleurs, au lieu d'un formulaire posé à côté
22
+ * d'un aperçu qui ne lui ressemble jamais tout à fait.
23
+ */
24
+ apercu: Record<string, Record<string, unknown> | null>;
25
+ poserApercu: (blockType: string, data: Record<string, unknown> | null) => void;
26
+ /** Vrai quand un jeton d'édition est actif. Faux pour tout visiteur. */
27
+ modeEdition: boolean;
28
+ };
29
+ export declare function SectionsProvider({ site, page, langue, apiBaseUrl, sectionsInitiales, sourceTexte, pagesPartagees, children, }: {
30
+ /** Sous-domaine de l'organisation (ex. « armoise »). */
31
+ site: string;
32
+ /** La page dont on veut les sections (ex. « accueil »). */
33
+ page: string;
34
+ langue?: string | undefined;
35
+ apiBaseUrl?: string | undefined;
36
+ /**
37
+ * LE CONTENU DÉJÀ CONNU AU PREMIER RENDU — pour un site prérendu.
38
+ *
39
+ * Sans lui, une page générée au build affiche les REPLIS du code, puis
40
+ * bascule sur le contenu publié une fois le JavaScript chargé. Le visiteur
41
+ * voit l'ancien prix clignoter vers le nouveau, et les moteurs de recherche
42
+ * n'indexent jamais que l'ancien. Un script de prérendu qui lit l'API et
43
+ * passe le résultat ici supprime les deux problèmes.
44
+ */
45
+ sectionsInitiales?: SectionPubliee[] | undefined;
46
+ /**
47
+ * D'OU VIENNENT LES MOTS DU SITE.
48
+ *
49
+ * Fournie, le contenu publie est verse dedans : chaque texte du site —
50
+ * y compris le titre de l'onglet et la description des resultats de
51
+ * recherche — devient modifiable, sans marquer une seule section a la
52
+ * main. `adaptateurI18next(i18n)` la construit en une ligne.
53
+ */
54
+ sourceTexte?: SourceTexte | undefined;
55
+ /**
56
+ * Des pages LUES EN PLUS de la page courante.
57
+ *
58
+ * Tout site a des textes partages — pied de page, navigation,
59
+ * coordonnees. Les ranger dans une page « commun » lue partout evite de
60
+ * les recopier sur chaque page, ou de forcer quelqu'un a revenir a
61
+ * l'accueil pour corriger une adresse affichee en bas de toutes.
62
+ *
63
+ * Elles ne changent PAS `page` : une nouvelle section se cree toujours
64
+ * sur la page courante, jamais dans le commun par accident.
65
+ */
66
+ pagesPartagees?: string[] | undefined;
67
+ children: ReactNode;
68
+ }): import("react").JSX.Element;
69
+ /**
70
+ * Le contexte, ou `null` HORS d'un fournisseur.
71
+ *
72
+ * On ne lève pas : un composant branché sur les sections doit rester
73
+ * utilisable dans une page qui n'en a pas (une maquette, un test, une page
74
+ * d'erreur). Il affichera son repli, ce qui est exactement le bon
75
+ * comportement.
76
+ */
77
+ export declare function useSectionsContext(): Contexte | null;
78
+ export {};
@@ -0,0 +1,9 @@
1
+ export declare function SignatureLystech({ site, apiBaseUrl, className, mention, connexion, }: {
2
+ /** Sous-domaine de l'organisation. Pris du fournisseur si omis. */
3
+ site?: string | undefined;
4
+ apiBaseUrl?: string | undefined;
5
+ className?: string | undefined;
6
+ /** Les mots de la mention, si le site en préfère d'autres. */
7
+ mention?: string | undefined;
8
+ connexion?: string | undefined;
9
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,6 @@
1
+ export declare function Connexion({ site, apiBaseUrl, surJeton, surFermer, }: {
2
+ site: string;
3
+ apiBaseUrl: string;
4
+ surJeton: (jeton: string) => void;
5
+ surFermer: () => void;
6
+ }): import("react").JSX.Element;
@@ -0,0 +1,6 @@
1
+ import { Api } from './api';
2
+ import { PontEdition } from './pont';
3
+ export declare function Editeur({ api, pont }: {
4
+ api: Api;
5
+ pont: PontEdition;
6
+ }): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { SourceTexte } from '../sourceTexte';
2
+ export declare function TexteEnPlace({ source, actif, surModification, }: {
3
+ source: SourceTexte;
4
+ /** Faux pendant qu'un panneau est ouvert : on ne veut pas deux modes. */
5
+ actif: boolean;
6
+ surModification: (ns: string, chemin: string, langue: string, valeur: string) => void | Promise<void>;
7
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,39 @@
1
+ import { BlocDeclare } from '../types';
2
+ /** Une section telle que l'éditeur la voit — brouillon compris. */
3
+ export type SectionEditeur = {
4
+ UID: string;
5
+ page: string;
6
+ blockType: string;
7
+ position: number;
8
+ enabled: boolean;
9
+ data: Record<string, unknown> | null;
10
+ draft: Record<string, unknown> | null;
11
+ publishedOn: string | null;
12
+ };
13
+ export type Api = ReturnType<typeof creerApi>;
14
+ export declare function creerApi({ jeton, site, apiBaseUrl, surJetonRefuse, }: {
15
+ jeton: string;
16
+ site: string;
17
+ apiBaseUrl: string;
18
+ /**
19
+ * Appele quand le serveur refuse le jeton.
20
+ *
21
+ * UN JETON EXPIRE NE DOIT PAS LAISSER UNE BARRE D'EDITION A L'ECRAN. Sans
22
+ * ce rappel, la personne verrait « Mode edition », ouvrirait un panneau, y
23
+ * ecrirait, cliquerait « Publier » — et n'obtiendrait qu'une erreur, sans
24
+ * comprendre qu'il suffisait de se reconnecter.
25
+ */
26
+ surJetonRefuse: () => void;
27
+ }): {
28
+ /** Les blocs que le site a déclaré savoir afficher. Route publique. */
29
+ schemas: () => Promise<BlocDeclare[]>;
30
+ /** Ce qui existe sur une page, brouillons compris. */
31
+ pourPage: (page: string) => Promise<SectionEditeur[]>;
32
+ creer: (page: string, blockType: string, draft: Record<string, unknown>) => Promise<{
33
+ UID: string;
34
+ }>;
35
+ enregistrerBrouillon: (UID: string, draft: Record<string, unknown>) => Promise<any>;
36
+ publier: (UID: string) => Promise<any>;
37
+ /** Ramène le brouillon à ce qui est en ligne. */
38
+ annuler: (UID: string) => Promise<any>;
39
+ };
@@ -0,0 +1,9 @@
1
+ import { ChampDeclare } from '../types';
2
+ type Props = {
3
+ champ: ChampDeclare;
4
+ valeur: unknown;
5
+ changer: (v: unknown) => void;
6
+ langue: string;
7
+ };
8
+ export declare function Champ({ champ, valeur, changer, langue }: Props): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,20 @@
1
+ import { PontEdition } from './pont';
2
+ export type { PontEdition } from './pont';
3
+ /**
4
+ * LE PANNEAU DE CONNEXION, sur le site lui-meme.
5
+ *
6
+ * Charge au clic, jamais avant : le lien du pied de page est un simple bouton
7
+ * tant que personne ne le presse. Un visiteur ne telecharge donc rien de tout
8
+ * ceci.
9
+ */
10
+ export declare function monterConnexion({ site, apiBaseUrl, surJeton, }: {
11
+ site: string;
12
+ apiBaseUrl: string;
13
+ surJeton: (jeton: string) => void;
14
+ }): () => void;
15
+ export declare function monterEditeur({ jeton, site, apiBaseUrl, pont, }: {
16
+ jeton: string;
17
+ site: string;
18
+ apiBaseUrl: string;
19
+ pont: PontEdition;
20
+ }): () => void;
@@ -0,0 +1,28 @@
1
+ import { SourceTexte } from '../sourceTexte';
2
+ export type PontEdition = {
3
+ /**
4
+ * D'où viennent les mots du site — et comment les remplacer.
5
+ *
6
+ * C'est elle qui permet a l'editeur de reconnaitre un texte affiche dans
7
+ * la page et de remonter a son identite. Sans elle, il ne reste que les
8
+ * blocs marques a la main.
9
+ */
10
+ sourceTexte?: SourceTexte | undefined;
11
+ /** Sous-domaine de l'organisation — affiché, jamais envoyé. */
12
+ site: string;
13
+ /** La page courante. Mise à jour par le fournisseur. */
14
+ page: string;
15
+ /** La langue affichée par le site en ce moment. */
16
+ langue: string;
17
+ /** Afficher un contenu non publié dans la page, ou `null` pour l'enlever. */
18
+ poserApercu: (blockType: string, data: Record<string, unknown> | null) => void;
19
+ /** Relire le contenu publié depuis le serveur. */
20
+ recharger: () => void;
21
+ /**
22
+ * Posé par l'ÉDITEUR, appelé par le FOURNISSEUR quand la page change.
23
+ *
24
+ * Sans cet avis, un site à page unique qui change d'adresse sans rien
25
+ * recharger laisserait l'éditeur sur les sections de la page précédente.
26
+ */
27
+ surChangementDePage?: (page: string) => void;
28
+ };
@@ -0,0 +1,2 @@
1
+ export declare const CSS = "\n:host {\n all: initial;\n --fond: #16181d;\n --fond-clair: #21242b;\n --bord: #333844;\n --texte: #f2f3f5;\n --texte-doux: #a3a9b6;\n --accent: #3ba58d;\n --accent-sombre: #2d8570;\n --danger: #d9776a;\n font-family: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto,\n \"Helvetica Neue\", Arial, sans-serif;\n font-size: 14px;\n line-height: 1.45;\n color: var(--texte);\n}\n\n* { box-sizing: border-box; }\n\n/* LE CALQUE LAISSE PASSER LES CLICS. Un conteneur fixe en plein \u00E9cran qui\n intercepte tout rendrait le site inutilisable derri\u00E8re lui : on ne pourrait\n plus naviguer, ni m\u00EAme faire d\u00E9filer correctement. Seuls ses ENFANTS\n reprennent les clics. */\n.calque {\n position: fixed;\n inset: 0;\n pointer-events: none;\n z-index: 2147483000;\n}\n.calque > * { pointer-events: auto; }\n\n/* ---------------------------------------------------------- CADRES ------ */\n\n.cadre {\n position: fixed;\n border: 2px dashed rgba(59, 165, 141, 0.55);\n border-radius: 6px;\n pointer-events: none;\n transition: border-color 120ms ease;\n}\n.cadre--actif { border-color: var(--accent); border-style: solid; }\n\n.pastille {\n position: fixed;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 5px 10px;\n border: 0;\n border-radius: 999px;\n background: var(--accent);\n color: #06120e;\n font-size: 12px;\n font-weight: 650;\n letter-spacing: 0.01em;\n cursor: pointer;\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);\n}\n.pastille:hover { background: #45bda2; }\n.pastille:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }\n\n/* ------------------------------------------------------------ BARRE ----- */\n\n.barre {\n position: fixed;\n left: 50%;\n bottom: 18px;\n transform: translateX(-50%);\n display: flex;\n align-items: center;\n gap: 14px;\n padding: 9px 14px;\n border: 1px solid var(--bord);\n border-radius: 999px;\n background: var(--fond);\n box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);\n max-width: calc(100vw - 24px);\n}\n.barre__point {\n width: 8px; height: 8px; border-radius: 50%;\n background: var(--accent);\n flex: none;\n}\n.barre__texte { font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n.barre__texte b { font-weight: 650; }\n.barre__compte { color: var(--texte-doux); font-size: 12px; white-space: nowrap; }\n\n/* ----------------------------------------------------------- PANNEAU ---- */\n\n.panneau {\n position: fixed;\n top: 0; right: 0; bottom: 0;\n width: min(420px, 100vw);\n display: flex;\n flex-direction: column;\n background: var(--fond);\n border-left: 1px solid var(--bord);\n box-shadow: -12px 0 40px rgba(0, 0, 0, 0.4);\n}\n@media (max-width: 520px) {\n .panneau { top: auto; height: 78vh; border-left: 0; border-top: 1px solid var(--bord); }\n}\n\n.panneau__tete {\n display: flex; align-items: flex-start; gap: 10px;\n padding: 16px 16px 12px;\n border-bottom: 1px solid var(--bord);\n}\n.panneau__titre { font-size: 15px; font-weight: 650; margin: 0; }\n.panneau__sous { margin: 3px 0 0; font-size: 12px; color: var(--texte-doux); }\n.panneau__corps { flex: 1; overflow-y: auto; padding: 14px 16px 18px; }\n.panneau__pied {\n display: flex; gap: 8px; align-items: center;\n padding: 12px 16px;\n border-top: 1px solid var(--bord);\n background: var(--fond-clair);\n}\n.panneau__pied .espace { flex: 1; }\n\n/* --------------------------------------------------- CONNEXION ---------- */\n\n/* Le voile PREND les clics, lui : une boite de dialogue qui demande un mot de\n passe ne doit pas laisser cliquer la page derriere elle. C'est l'exception\n a la regle du calque, et elle est volontaire. */\n.voile {\n position: fixed;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n background: rgba(8, 10, 14, 0.62);\n backdrop-filter: blur(2px);\n}\n\n.fenetre {\n width: min(420px, 100%);\n max-height: 90vh;\n overflow-y: auto;\n border: 1px solid var(--bord);\n border-radius: 12px;\n background: var(--fond);\n box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);\n}\n\n/* ------------------------------------------------------------ CHAMPS ---- */\n\n.champ { margin-bottom: 16px; }\n.champ__etiquette {\n display: block; margin-bottom: 5px;\n font-size: 12px; font-weight: 600; color: var(--texte);\n}\n.champ__aide { margin: 4px 0 0; font-size: 11.5px; color: var(--texte-doux); }\n.champ__langue {\n display: inline-block; margin: 0 0 4px;\n font-size: 10.5px; font-weight: 700; letter-spacing: 0.06em;\n text-transform: uppercase; color: var(--texte-doux);\n}\n\ninput[type=\"text\"], input[type=\"url\"], input[type=\"number\"], input[type=\"date\"],\nselect, textarea {\n width: 100%;\n padding: 8px 10px;\n border: 1px solid var(--bord);\n border-radius: 6px;\n background: var(--fond-clair);\n color: var(--texte);\n font: inherit;\n font-size: 13px;\n}\ntextarea { min-height: 92px; resize: vertical; line-height: 1.5; }\ninput:focus, select:focus, textarea:focus {\n outline: none; border-color: var(--accent);\n box-shadow: 0 0 0 3px rgba(59, 165, 141, 0.18);\n}\n\n.bascule { display: flex; align-items: center; gap: 8px; }\n.bascule input { width: 16px; height: 16px; accent-color: var(--accent); }\n\n.vignette {\n margin-top: 8px; max-height: 110px; border-radius: 6px;\n border: 1px solid var(--bord); display: block;\n}\n\n/* --------------------------------------------------------- LISTES ------- */\n\n.liste__item {\n border: 1px solid var(--bord); border-radius: 8px;\n padding: 12px; margin-bottom: 10px; background: var(--fond-clair);\n}\n.liste__tete {\n display: flex; align-items: center; justify-content: space-between;\n margin-bottom: 8px;\n}\n.liste__rang { font-size: 11px; font-weight: 700; color: var(--texte-doux); }\n\n/* --------------------------------------------------------- BOUTONS ------ */\n\n.bouton {\n display: inline-flex; align-items: center; justify-content: center; gap: 6px;\n padding: 8px 14px; border-radius: 7px; border: 1px solid transparent;\n font: inherit; font-size: 13px; font-weight: 600; cursor: pointer;\n background: var(--fond-clair); color: var(--texte); border-color: var(--bord);\n}\n.bouton:hover { background: #2a2e37; }\n.bouton:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }\n.bouton--principal { background: var(--accent); border-color: var(--accent); color: #06120e; }\n.bouton--principal:hover { background: #45bda2; }\n.bouton--discret { background: transparent; border-color: transparent; color: var(--texte-doux); }\n.bouton--discret:hover { background: var(--fond-clair); color: var(--texte); }\n.bouton[disabled] { opacity: 0.5; cursor: default; }\n.bouton--icone { padding: 6px 9px; }\n\n.message {\n margin: 0 0 12px; padding: 9px 11px; border-radius: 7px;\n font-size: 12.5px; border: 1px solid;\n}\n.message--erreur { background: rgba(217, 119, 106, 0.12); border-color: rgba(217, 119, 106, 0.4); color: #f0b3a9; }\n.message--ok { background: rgba(59, 165, 141, 0.12); border-color: rgba(59, 165, 141, 0.4); color: #8fd9c6; }\n.message--info { background: var(--fond-clair); border-color: var(--bord); color: var(--texte-doux); }\n\n@media (prefers-reduced-motion: reduce) {\n * { transition: none !important; animation: none !important; }\n}\n";
2
+ export declare const CSS_EN_PLACE = "\n/* --------------------------------------------------------- CHOIX -------- */\n\n/* Quand un meme texte vient de plusieurs cles, on demande laquelle plutot que\n de deviner \u2014 et de modifier la mauvaise sans que personne le voie. */\n.choix {\n position: fixed;\n width: 260px;\n padding: 12px;\n border: 1px solid var(--bord);\n border-radius: 10px;\n background: var(--fond);\n box-shadow: 0 14px 40px rgba(0, 0, 0, 0.45);\n}\n.choix__titre { margin: 0 0 2px; font-size: 12.5px; font-weight: 650; }\n.choix__aide { margin: 0 0 10px; font-size: 11.5px; color: var(--texte-doux); }\n.choix__option {\n display: flex; flex-direction: column; align-items: flex-start; gap: 2px;\n width: 100%; margin-bottom: 6px; padding: 7px 9px;\n border: 1px solid var(--bord); border-radius: 7px;\n background: var(--fond-clair); color: var(--texte);\n font: inherit; text-align: left; cursor: pointer;\n}\n.choix__option:hover { background: #2a2e37; border-color: var(--accent); }\n.choix__chemin { font-size: 12px; font-weight: 600; }\n.choix__ns { font-size: 10.5px; color: var(--texte-doux); }\n\n/* ------------------------------------------------------- RANGEES -------- */\n\n.rangee {\n display: flex; align-items: center; justify-content: space-between; gap: 10px;\n width: 100%; margin-bottom: 6px; padding: 10px 12px;\n border: 1px solid var(--bord); border-radius: 8px;\n background: var(--fond-clair); color: var(--texte);\n font: inherit; text-align: left; cursor: pointer;\n}\n.rangee:hover { background: #2a2e37; border-color: var(--accent); }\n.rangee__nom { font-size: 13px; font-weight: 600; }\n.rangee__info { font-size: 11.5px; color: var(--texte-doux); white-space: nowrap; }\n\n/* ------------------------------------------------------ FLOTTANT -------- */\n\n/* Un message au-dessus de la barre, pour confirmer sans ouvrir de panneau. */\n.flottant {\n position: fixed;\n left: 50%; bottom: 76px;\n transform: translateX(-50%);\n max-width: min(460px, calc(100vw - 24px));\n}\n.flottant .message { margin: 0; box-shadow: 0 8px 26px rgba(0,0,0,.4); }\n";