@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/dist/index.d.ts CHANGED
@@ -1,108 +1,18 @@
1
- import { JSX } from 'react';
2
- import { ReactNode } from 'react';
3
-
4
- export declare type BlocDeclare = {
5
- blockType: string;
6
- label: string;
7
- description?: string;
8
- fields: ChampDeclare[];
9
- pages?: string[] | null;
10
- maxPerPage?: number | null;
11
- };
12
-
13
- export declare type ChampDeclare = {
14
- key: string;
15
- label: string;
16
- type: TypeDeChamp;
17
- required?: boolean;
18
- help?: string;
19
- options?: string[];
20
- /** Sous-champs d'une `list`. */
21
- of?: ChampDeclare[];
22
- /**
23
- * Champ TRADUIT. La valeur stockée devient { fr: …, en: … } et le paquet
24
- * rend celle de la langue courante.
25
- *
26
- * Le serveur ne connaît pas cette clé — il stocke la déclaration telle
27
- * quelle. C'est voulu : ajouter une langue ne doit pas demander une
28
- * migration côté plateforme.
29
- */
30
- localized?: boolean;
31
- };
32
-
33
- declare type Contexte = {
34
- /** Sous-domaine de l'organisation dans Lystech. */
35
- site: string;
36
- /** Langue courante — décide quelle version d'un champ traduit est rendue. */
37
- langue: string;
38
- sections: SectionPubliee[];
39
- etat: Etat;
40
- recharger: () => void;
41
- };
42
-
43
- declare type Etat = "chargement" | "pret" | "erreur";
44
-
45
- export declare type ResultatSection<T> = {
46
- /** Le contenu à afficher : publié si présent, défaut sinon. */
47
- contenu: T;
48
- /** Vrai quand le contenu vient du serveur, faux quand c'est le défaut. */
49
- publie: boolean;
50
- /** L'identifiant de la section — nécessaire à l'édition. */
51
- UID: string | null;
52
- chargement: boolean;
53
- };
54
-
55
- /** Une section telle qu'elle revient du serveur. */
56
- export declare type SectionPubliee = {
57
- UID: string;
58
- page: string;
59
- blockType: string;
60
- position: number;
61
- data: Record<string, unknown> | null;
62
- };
63
-
64
- export declare function SectionsProvider({ site, page, langue, apiBaseUrl, children, }: {
65
- /** Sous-domaine de l'organisation (ex. « armoise »). */
66
- site: string;
67
- /** La page dont on veut les sections (ex. « accueil »). */
68
- page: string;
69
- langue?: string;
70
- apiBaseUrl?: string;
71
- children: ReactNode;
72
- }): JSX.Element;
73
-
74
- export declare type TypeDeChamp = "text" | "richText" | "image" | "number" | "boolean" | "url" | "date" | "choice" | "list";
75
-
76
- /**
77
- * Le contenu d'un type de bloc sur la page courante.
78
- *
79
- * Le cas visé est celui d'un site DÉJÀ DESSINÉ : un bloc par type et par page,
80
- * dont on veut rendre les mots et les images modifiables — pas un constructeur
81
- * de pages. Pour des blocs répétables, voir `useSectionList`.
82
- */
83
- export declare function useSection<T extends object>(blockType: string, options: {
84
- defaut: T;
85
- }): ResultatSection<T>;
86
-
87
- /** Plusieurs blocs du même type, dans l'ordre choisi par le client. */
88
- export declare function useSectionList<T extends object>(blockType: string, options?: {
89
- defaut?: T[];
90
- }): {
91
- items: Array<T & {
92
- __uid: string;
93
- }>;
94
- publie: boolean;
95
- chargement: boolean;
96
- };
97
-
98
- /**
99
- * Le contexte, ou `null` HORS d'un fournisseur.
100
- *
101
- * On ne lève pas : un composant branché sur les sections doit rester
102
- * utilisable dans une page qui n'en a pas (une maquette, un test, une page
103
- * d'erreur). Il affichera son repli, ce qui est exactement le bon
104
- * comportement.
105
- */
106
- export declare function useSectionsContext(): Contexte | null;
107
-
108
- export { }
1
+ /**
2
+ * @lystech/sections — le contenu d'un site, modifiable par son propriétaire.
3
+ *
4
+ * CE QUI EST EXPORTÉ ICI EST CE QU'UN VISITEUR TÉLÉCHARGE : la lecture, et
5
+ * rien d'autre. L'interface d'édition vivra dans un point d'entrée séparé,
6
+ * chargé à la demande. Un visiteur ne doit pas payer le poids d'un éditeur
7
+ * qu'il ne verra jamais.
8
+ *
9
+ * Le paquet ne dépend PAS de @lystech/core : une clinique qui veut modifier
10
+ * trois textes n'a aucune raison de charger un magasin, un panier et PayPal.
11
+ */
12
+ export { SectionsProvider, useSectionsContext } from './SectionsProvider';
13
+ export { SignatureLystech } from './SignatureLystech';
14
+ export { adaptateurI18next, appliquerAuxTextes } from './sourceTexte';
15
+ export type { Dictionnaire, SourceTexte } from './sourceTexte';
16
+ export { useSection, useSectionList } from './useSection';
17
+ export type { BlocDeclare, ChampDeclare, SectionPubliee, TypeDeChamp, } from './types';
18
+ export type { AncrageEdition, ResultatSection } from './useSection';
package/dist/index.js CHANGED
@@ -1,107 +1,284 @@
1
- import { jsx as j } from "react/jsx-runtime";
2
- import { createContext as x, useState as l, useRef as C, useEffect as S, useMemo as m, useContext as E } from "react";
3
- const b = x(null), _ = "https://api.lystech.ca/api";
4
- function D({
5
- site: e,
6
- page: r,
7
- langue: t = "fr",
8
- apiBaseUrl: o = _,
9
- children: n
1
+ import { jsx as S, jsxs as L, Fragment as F } from "react/jsx-runtime";
2
+ import { useState as h, useRef as P, useEffect as x, useCallback as O, useMemo as _, useContext as M, createContext as N } from "react";
3
+ import { appliquerAuxTextes as T } from "./morceaux/sourceTexte-BMPMpNg9.js";
4
+ import { adaptateurI18next as rt } from "./morceaux/sourceTexte-BMPMpNg9.js";
5
+ const d = "lystech-edit", k = N(null), q = "https://api.lystech.ca/api";
6
+ function H() {
7
+ if (typeof window > "u") return null;
8
+ const t = (n) => {
9
+ try {
10
+ window.sessionStorage.setItem(d, n);
11
+ } catch {
12
+ }
13
+ return n;
14
+ };
15
+ try {
16
+ const n = window.location.hash.replace(/^#/, "");
17
+ if (n.includes(d + "=")) {
18
+ const c = new URLSearchParams(n), o = c.get(d);
19
+ if (o) {
20
+ c.delete(d);
21
+ const i = c.toString();
22
+ return window.history.replaceState(
23
+ {},
24
+ "",
25
+ window.location.pathname + window.location.search + (i ? "#" + i : "")
26
+ ), t(o);
27
+ }
28
+ }
29
+ const e = new URL(window.location.href), r = e.searchParams.get(d);
30
+ return r ? (e.searchParams.delete(d), window.history.replaceState({}, "", e.toString()), t(r)) : window.sessionStorage.getItem(d);
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+ function X({
36
+ site: t,
37
+ page: n,
38
+ langue: e = "fr",
39
+ apiBaseUrl: r = q,
40
+ sectionsInitiales: c,
41
+ sourceTexte: o,
42
+ pagesPartagees: i,
43
+ children: m
10
44
  }) {
11
- const [c, a] = l([]), [d, f] = l("chargement"), [A, g] = l(0), h = C(null);
12
- S(() => {
13
- if (!e || !r) return;
14
- h.current?.abort();
15
- const u = new AbortController();
16
- return h.current = u, f("chargement"), fetch(
17
- `${o}/site-sections/public/${encodeURIComponent(e)}/${encodeURIComponent(r)}`,
18
- { signal: u.signal }
19
- ).then((s) => s.ok ? s.json() : Promise.reject(new Error(String(s.status)))).then((s) => {
20
- u.signal.aborted || (a(Array.isArray(s) ? s : []), f("pret"));
45
+ const [u, g] = h(
46
+ c ?? []
47
+ ), [w, p] = h(
48
+ c?.length ? "pret" : "chargement"
49
+ ), [y, A] = h(0), [R, J] = h({}), [U, $] = h(!1), D = P(null);
50
+ x(() => {
51
+ if (!t || !n) return;
52
+ D.current?.abort();
53
+ const a = new AbortController();
54
+ D.current = a, p("chargement");
55
+ const f = r + "/site-sections/public/" + encodeURIComponent(t) + "/" + encodeURIComponent([n, ...i ?? []].join(","));
56
+ return fetch(f, { signal: a.signal }).then(
57
+ (s) => s.ok ? s.json() : Promise.reject(new Error(String(s.status)))
58
+ ).then((s) => {
59
+ a.signal.aborted || (g(Array.isArray(s) ? s : []), p("pret"));
21
60
  }).catch((s) => {
22
- u.signal.aborted || s?.name === "AbortError" || f("erreur");
23
- }), () => u.abort();
24
- }, [e, r, o, A]);
25
- const p = m(
61
+ a.signal.aborted || s?.name === "AbortError" || p("erreur");
62
+ }), () => a.abort();
63
+ }, [t, n, r, y, (i ?? []).join(",")]);
64
+ const E = O(() => A((a) => a + 1), []);
65
+ x(() => {
66
+ !o || !u.length || T(u, o);
67
+ }, [u, o]);
68
+ const j = O(
69
+ (a, f) => J((s) => ({ ...s, [a]: f })),
70
+ []
71
+ ), l = P({
72
+ site: t,
73
+ page: n,
74
+ langue: e,
75
+ poserApercu: () => {
76
+ },
77
+ recharger: () => {
78
+ }
79
+ });
80
+ l.current.site = t, l.current.page = n, l.current.langue = e, l.current.poserApercu = j, l.current.sourceTexte = o, l.current.recharger = E, x(() => {
81
+ l.current.surChangementDePage?.(n);
82
+ }, [n]), x(() => {
83
+ const a = H();
84
+ if (!a) return;
85
+ let f = !0, s;
86
+ return import("@lystech/sections/editeur").then((b) => {
87
+ f && ($(!0), s = b.monterEditeur({
88
+ jeton: a,
89
+ site: t,
90
+ apiBaseUrl: r,
91
+ pont: l.current
92
+ }));
93
+ }).catch((b) => {
94
+ console.error("[lystech] éditeur indisponible:", b?.message ?? b);
95
+ }), () => {
96
+ f = !1, s?.();
97
+ };
98
+ }, [t, r]);
99
+ const z = _(
26
100
  () => ({
27
- site: e,
28
- langue: t,
29
- sections: c,
30
- etat: d,
31
- recharger: () => g((u) => u + 1)
101
+ site: t,
102
+ page: n,
103
+ langue: e,
104
+ apiBaseUrl: r,
105
+ sections: u,
106
+ etat: w,
107
+ recharger: E,
108
+ apercu: R,
109
+ poserApercu: j,
110
+ modeEdition: U
32
111
  }),
33
- [e, t, c, d]
112
+ [
113
+ t,
114
+ n,
115
+ e,
116
+ r,
117
+ u,
118
+ w,
119
+ E,
120
+ R,
121
+ j,
122
+ U
123
+ ]
34
124
  );
35
- return /* @__PURE__ */ j(b.Provider, { value: p, children: n });
125
+ return /* @__PURE__ */ S(k.Provider, { value: z, children: m });
36
126
  }
37
- function y() {
38
- return E(b);
127
+ function I() {
128
+ return M(k);
39
129
  }
40
- function I(e) {
41
- return !!e && typeof e == "object" && !Array.isArray(e) && Object.keys(e).every((r) => /^[a-z]{2}(-[A-Za-z]{2})?$/.test(r));
130
+ const W = "https://api.lystech.ca/api", v = "lystech-edit";
131
+ function Y({
132
+ site: t,
133
+ apiBaseUrl: n,
134
+ className: e,
135
+ mention: r = "Réalisé avec Lystech",
136
+ connexion: c = "Connexion"
137
+ }) {
138
+ const o = I(), i = t ?? o?.site ?? "", m = n ?? o?.apiBaseUrl ?? W, [u, g] = h(!1);
139
+ if (!i) return null;
140
+ const w = async () => {
141
+ if (!u) {
142
+ g(!0);
143
+ try {
144
+ (await import("@lystech/sections/editeur")).monterConnexion({
145
+ site: i,
146
+ apiBaseUrl: m,
147
+ surJeton: (A) => {
148
+ try {
149
+ window.sessionStorage.setItem(v, A);
150
+ } catch {
151
+ window.location.hash = v + "=" + encodeURIComponent(A);
152
+ }
153
+ window.location.reload();
154
+ }
155
+ });
156
+ } catch (y) {
157
+ console.error("[lystech] connexion indisponible:", y?.message ?? y);
158
+ } finally {
159
+ g(!1);
160
+ }
161
+ }
162
+ }, p = {
163
+ color: "inherit",
164
+ font: "inherit",
165
+ background: "none",
166
+ border: 0,
167
+ padding: 0,
168
+ cursor: "pointer",
169
+ textDecoration: "underline",
170
+ textUnderlineOffset: "0.2em"
171
+ };
172
+ return /* @__PURE__ */ L(
173
+ "div",
174
+ {
175
+ className: e,
176
+ style: {
177
+ display: "flex",
178
+ flexWrap: "wrap",
179
+ alignItems: "center",
180
+ gap: "0.5em",
181
+ fontSize: "0.85em",
182
+ opacity: 0.7,
183
+ lineHeight: 1.5
184
+ },
185
+ children: [
186
+ /* @__PURE__ */ S(
187
+ "a",
188
+ {
189
+ href: "https://lystech.ca",
190
+ target: "_blank",
191
+ rel: "noreferrer",
192
+ style: { color: "inherit" },
193
+ children: r
194
+ }
195
+ ),
196
+ !o?.modeEdition && /* @__PURE__ */ L(F, { children: [
197
+ /* @__PURE__ */ S("span", { "aria-hidden": "true", children: "·" }),
198
+ /* @__PURE__ */ S("button", { type: "button", style: p, onClick: w, children: u ? "…" : c })
199
+ ] })
200
+ ]
201
+ }
202
+ );
42
203
  }
43
- function i(e, r) {
44
- if (Array.isArray(e)) return e.map((t) => i(t, r));
45
- if (I(e)) {
46
- const t = e, o = r.slice(0, 2);
47
- return t[r] ?? t[o] ?? Object.values(t).find((c) => c != null) ?? "";
48
- }
49
- if (e && typeof e == "object") {
50
- const t = {};
51
- for (const [o, n] of Object.entries(e))
52
- t[o] = i(n, r);
53
- return t;
54
- }
55
- return e;
204
+ function Z(t) {
205
+ return !!t && typeof t == "object" && !Array.isArray(t) && Object.keys(t).every((n) => /^[a-z]{2}(-[A-Za-z]{2})?$/.test(n));
56
206
  }
57
- function U(e, r) {
58
- if (!r || typeof r != "object" || Array.isArray(r))
207
+ function C(t, n) {
208
+ if (Array.isArray(t)) return t.map((e) => C(e, n));
209
+ if (Z(t)) {
210
+ const e = t, r = n.slice(0, 2);
211
+ return e[n] ?? e[r] ?? Object.values(e).find((o) => o != null) ?? "";
212
+ }
213
+ if (t && typeof t == "object") {
214
+ const e = {};
215
+ for (const [r, c] of Object.entries(t))
216
+ e[r] = C(c, n);
59
217
  return e;
60
- const t = { ...e };
61
- for (const [o, n] of Object.entries(r))
62
- n != null && (t[o] = n);
218
+ }
63
219
  return t;
64
220
  }
65
- function O(e, r) {
66
- const t = y();
67
- return m(() => {
68
- const o = t?.sections.find(
69
- (a) => a.blockType === e
70
- ), n = o?.data ?? null, c = n ? i(n, t?.langue ?? "fr") : null;
221
+ function G(t, n) {
222
+ if (!n || typeof n != "object" || Array.isArray(n))
223
+ return t;
224
+ const e = { ...t };
225
+ for (const [r, c] of Object.entries(n))
226
+ c != null && (e[r] = c);
227
+ return e;
228
+ }
229
+ function B(t, n) {
230
+ const e = I(), r = e?.apercu?.[t];
231
+ return _(() => {
232
+ const c = e?.sections.find(
233
+ (m) => m.blockType === t
234
+ ), o = r ?? c?.data ?? null, i = o ? C(o, e?.langue ?? "fr") : null;
71
235
  return {
72
- contenu: U(r.defaut, c),
73
- publie: !!n,
74
- UID: o?.UID ?? null,
75
- chargement: t?.etat === "chargement"
236
+ contenu: G(n.defaut, i),
237
+ publie: !!c?.data,
238
+ UID: c?.UID ?? null,
239
+ chargement: e?.etat === "chargement",
240
+ ancrage: { "data-lystech-section": t }
76
241
  };
77
- }, [t?.sections, t?.langue, t?.etat, e, r.defaut]);
242
+ }, [
243
+ e?.sections,
244
+ e?.langue,
245
+ e?.etat,
246
+ r,
247
+ t,
248
+ n.defaut
249
+ ]);
78
250
  }
79
- function P(e, r) {
80
- const t = y();
81
- return m(() => {
82
- const o = (t?.sections ?? []).filter(
83
- (n) => n.blockType === e
251
+ function tt(t, n) {
252
+ const e = I();
253
+ return _(() => {
254
+ const r = { "data-lystech-section": t }, c = (e?.sections ?? []).filter(
255
+ (o) => o.blockType === t
84
256
  );
85
- return o.length === 0 ? {
86
- items: (r?.defaut ?? []).map((n, c) => ({
87
- ...n,
88
- __uid: `defaut-${c}`
257
+ return c.length === 0 ? {
258
+ items: (n?.defaut ?? []).map((o, i) => ({
259
+ ...o,
260
+ __uid: `defaut-${i}`
89
261
  })),
90
262
  publie: !1,
91
- chargement: t?.etat === "chargement"
263
+ chargement: e?.etat === "chargement",
264
+ ancrage: r
92
265
  } : {
93
- items: o.map((n) => ({
94
- ...i(n.data ?? {}, t?.langue ?? "fr"),
95
- __uid: n.UID
266
+ items: c.map((o) => ({
267
+ ...C(o.data ?? {}, e?.langue ?? "fr"),
268
+ __uid: o.UID
96
269
  })),
97
270
  publie: !0,
98
- chargement: t?.etat === "chargement"
271
+ chargement: e?.etat === "chargement",
272
+ ancrage: r
99
273
  };
100
- }, [t?.sections, t?.langue, t?.etat, e, r?.defaut]);
274
+ }, [e?.sections, e?.langue, e?.etat, t, n?.defaut]);
101
275
  }
102
276
  export {
103
- D as SectionsProvider,
104
- O as useSection,
105
- P as useSectionList,
106
- y as useSectionsContext
277
+ X as SectionsProvider,
278
+ Y as SignatureLystech,
279
+ rt as adaptateurI18next,
280
+ T as appliquerAuxTextes,
281
+ B as useSection,
282
+ tt as useSectionList,
283
+ I as useSectionsContext
107
284
  };
@@ -0,0 +1,71 @@
1
+ function y(t, o) {
2
+ const e = t.split("."), r = {};
3
+ let n = r;
4
+ for (let s = 0; s < e.length - 1; s++) {
5
+ const c = e[s];
6
+ n[c] = {}, n = n[c];
7
+ }
8
+ return n[e[e.length - 1]] = o, r;
9
+ }
10
+ function l(t, o = "", e = {}) {
11
+ if (!t || typeof t != "object") return e;
12
+ for (const [r, n] of Object.entries(t)) {
13
+ const s = o ? o + "." + r : r;
14
+ typeof n == "string" ? e[s] = n : n && typeof n == "object" && !Array.isArray(n) && l(n, s, e);
15
+ }
16
+ return e;
17
+ }
18
+ function h(t) {
19
+ return {
20
+ langues: () => {
21
+ const o = t?.options?.supportedLngs, e = Array.isArray(o) ? o.filter((r) => r !== "cimode") : [];
22
+ return e.length ? e : [t?.language ?? "fr"];
23
+ },
24
+ langueCourante: () => String(t?.language ?? "fr").slice(0, 2),
25
+ dictionnaire: (o) => {
26
+ const e = Array.isArray(t?.options?.ns) ? t.options.ns : [t?.options?.defaultNS ?? "translation"], r = {};
27
+ for (const n of e) {
28
+ const s = t?.getResourceBundle?.(o, n);
29
+ s && (r[n] = l(s));
30
+ }
31
+ return r;
32
+ },
33
+ poser: (o, e, r, n) => {
34
+ t?.addResourceBundle?.(
35
+ o,
36
+ e,
37
+ y(r, n),
38
+ !0,
39
+ !0
40
+ );
41
+ }
42
+ };
43
+ }
44
+ const d = /^i18n:([^:]+):(.*)$/;
45
+ function A(t, o) {
46
+ if (typeof t == "string") return t;
47
+ if (!t || typeof t != "object" || Array.isArray(t)) return null;
48
+ const e = t, r = o.slice(0, 2), n = e[o] ?? e[r];
49
+ return typeof n == "string" ? n : null;
50
+ }
51
+ function j(t, o) {
52
+ const e = o.langues();
53
+ let r = 0;
54
+ for (const n of t ?? []) {
55
+ const s = d.exec(n.blockType ?? "");
56
+ if (!s || !n.data) continue;
57
+ const c = s[1], u = s[2];
58
+ for (const [a, p] of Object.entries(n.data)) {
59
+ const g = u ? u + "." + a : a;
60
+ for (const f of e) {
61
+ const i = A(p, f);
62
+ i === null || i === "" || (o.poser(f, c, g, i), r++);
63
+ }
64
+ }
65
+ }
66
+ return r;
67
+ }
68
+ export {
69
+ h as adaptateurI18next,
70
+ j as appliquerAuxTextes
71
+ };
@@ -0,0 +1,34 @@
1
+ /** Un dictionnaire aplati : espace de noms → chemin pointé → texte. */
2
+ export type Dictionnaire = Record<string, Record<string, string>>;
3
+ export type SourceTexte = {
4
+ /** Les langues que le site sait rendre. */
5
+ langues: () => string[];
6
+ /** Celle qu'il affiche en ce moment. */
7
+ langueCourante: () => string;
8
+ /** Tout ce que le site sait dire, dans une langue. */
9
+ dictionnaire: (langue: string) => Dictionnaire;
10
+ /** Remplacer un texte. Le site doit se redessiner ensuite. */
11
+ poser: (langue: string, espace: string, chemin: string, valeur: string) => void;
12
+ };
13
+ /**
14
+ * Branche une instance i18next.
15
+ *
16
+ * `bindI18nStore: "added"` doit être actif dans la configuration du site,
17
+ * sinon React ne se redessine pas quand on remplace un texte — la
18
+ * modification serait enregistrée mais invisible, ce qui est la pire des
19
+ * deux issues : la personne réécrit, republie, et finit par croire que rien
20
+ * ne fonctionne.
21
+ */
22
+ export declare function adaptateurI18next(i18n: any): SourceTexte;
23
+ /**
24
+ * Applique des sections publiées à la source de textes du site.
25
+ *
26
+ * Utilisée à deux moments, et c'est ce qui rend le référencement juste :
27
+ * pendant le PRÉ-RENDU, pour que le HTML livré porte le bon texte et les
28
+ * bonnes balises ; puis dans le NAVIGATEUR, pour rattraper ce qui aurait été
29
+ * publié depuis la dernière reconstruction.
30
+ */
31
+ export declare function appliquerAuxTextes(sections: Array<{
32
+ blockType: string;
33
+ data: Record<string, unknown> | null;
34
+ }>, source: SourceTexte): number;
@@ -0,0 +1,41 @@
1
+ export type TypeDeChamp = "text" | "richText" | "image" | "number" | "boolean" | "url" | "date" | "choice" | "list";
2
+ export type ChampDeclare = {
3
+ key: string;
4
+ label: string;
5
+ type: TypeDeChamp;
6
+ required?: boolean;
7
+ help?: string;
8
+ options?: string[];
9
+ /** Sous-champs d'une `list`. */
10
+ of?: ChampDeclare[];
11
+ /**
12
+ * Champ TRADUIT. La valeur stockée devient { fr: …, en: … } et le paquet
13
+ * rend celle de la langue courante.
14
+ *
15
+ * Le serveur ne connaît pas cette clé — il stocke la déclaration telle
16
+ * quelle. C'est voulu : ajouter une langue ne doit pas demander une
17
+ * migration côté plateforme.
18
+ */
19
+ localized?: boolean;
20
+ };
21
+ export type BlocDeclare = {
22
+ blockType: string;
23
+ label: string;
24
+ description?: string;
25
+ fields: ChampDeclare[];
26
+ pages?: string[] | null;
27
+ maxPerPage?: number | null;
28
+ };
29
+ /** Une section telle qu'elle revient du serveur. */
30
+ export type SectionPubliee = {
31
+ UID: string;
32
+ page: string;
33
+ blockType: string;
34
+ position: number;
35
+ data: Record<string, unknown> | null;
36
+ };
37
+ export type ValeurTraduite = {
38
+ [langue: string]: unknown;
39
+ };
40
+ /** Un champ traduit est un objet de langues ; on le reconnaît sans schéma. */
41
+ export declare function estTraduite(v: unknown): v is ValeurTraduite;