@lystech/sections 0.1.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 ADDED
@@ -0,0 +1,55 @@
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.
@@ -0,0 +1,108 @@
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 { }
package/dist/index.js ADDED
@@ -0,0 +1,107 @@
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
10
+ }) {
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"));
21
+ }).catch((s) => {
22
+ u.signal.aborted || s?.name === "AbortError" || f("erreur");
23
+ }), () => u.abort();
24
+ }, [e, r, o, A]);
25
+ const p = m(
26
+ () => ({
27
+ site: e,
28
+ langue: t,
29
+ sections: c,
30
+ etat: d,
31
+ recharger: () => g((u) => u + 1)
32
+ }),
33
+ [e, t, c, d]
34
+ );
35
+ return /* @__PURE__ */ j(b.Provider, { value: p, children: n });
36
+ }
37
+ function y() {
38
+ return E(b);
39
+ }
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));
42
+ }
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;
56
+ }
57
+ function U(e, r) {
58
+ if (!r || typeof r != "object" || Array.isArray(r))
59
+ return e;
60
+ const t = { ...e };
61
+ for (const [o, n] of Object.entries(r))
62
+ n != null && (t[o] = n);
63
+ return t;
64
+ }
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;
71
+ return {
72
+ contenu: U(r.defaut, c),
73
+ publie: !!n,
74
+ UID: o?.UID ?? null,
75
+ chargement: t?.etat === "chargement"
76
+ };
77
+ }, [t?.sections, t?.langue, t?.etat, e, r.defaut]);
78
+ }
79
+ function P(e, r) {
80
+ const t = y();
81
+ return m(() => {
82
+ const o = (t?.sections ?? []).filter(
83
+ (n) => n.blockType === e
84
+ );
85
+ return o.length === 0 ? {
86
+ items: (r?.defaut ?? []).map((n, c) => ({
87
+ ...n,
88
+ __uid: `defaut-${c}`
89
+ })),
90
+ publie: !1,
91
+ chargement: t?.etat === "chargement"
92
+ } : {
93
+ items: o.map((n) => ({
94
+ ...i(n.data ?? {}, t?.langue ?? "fr"),
95
+ __uid: n.UID
96
+ })),
97
+ publie: !0,
98
+ chargement: t?.etat === "chargement"
99
+ };
100
+ }, [t?.sections, t?.langue, t?.etat, e, r?.defaut]);
101
+ }
102
+ export {
103
+ D as SectionsProvider,
104
+ O as useSection,
105
+ P as useSectionList,
106
+ y as useSectionsContext
107
+ };
@@ -0,0 +1 @@
1
+ (function(s,a){typeof exports=="object"&&typeof module<"u"?a(exports,require("react/jsx-runtime"),require("react")):typeof define=="function"&&define.amd?define(["exports","react/jsx-runtime","react"],a):(s=typeof globalThis<"u"?globalThis:s||self,a(s.LystechSections={},s.jsxRuntime,s.React))})(this,(function(s,a,c){"use strict";const y=c.createContext(null),j="https://api.lystech.ca/api";function A({site:t,page:n,langue:e="fr",apiBaseUrl:o=j,children:r}){const[i,l]=c.useState([]),[S,h]=c.useState("chargement"),[E,P]=c.useState(0),b=c.useRef(null);c.useEffect(()=>{if(!t||!n)return;b.current?.abort();const f=new AbortController;return b.current=f,h("chargement"),fetch(`${o}/site-sections/public/${encodeURIComponent(t)}/${encodeURIComponent(n)}`,{signal:f.signal}).then(u=>u.ok?u.json():Promise.reject(new Error(String(u.status)))).then(u=>{f.signal.aborted||(l(Array.isArray(u)?u:[]),h("pret"))}).catch(u=>{f.signal.aborted||u?.name==="AbortError"||h("erreur")}),()=>f.abort()},[t,n,o,E]);const R=c.useMemo(()=>({site:t,langue:e,sections:i,etat:S,recharger:()=>P(f=>f+1)}),[t,e,i,S]);return a.jsx(y.Provider,{value:R,children:r})}function m(){return c.useContext(y)}function g(t){return!!t&&typeof t=="object"&&!Array.isArray(t)&&Object.keys(t).every(n=>/^[a-z]{2}(-[A-Za-z]{2})?$/.test(n))}function d(t,n){if(Array.isArray(t))return t.map(e=>d(e,n));if(g(t)){const e=t,o=n.slice(0,2);return e[n]??e[o]??Object.values(e).find(i=>i!=null)??""}if(t&&typeof t=="object"){const e={};for(const[o,r]of Object.entries(t))e[o]=d(r,n);return e}return t}function p(t,n){if(!n||typeof n!="object"||Array.isArray(n))return t;const e={...t};for(const[o,r]of Object.entries(n))r!=null&&(e[o]=r);return e}function x(t,n){const e=m();return c.useMemo(()=>{const o=e?.sections.find(l=>l.blockType===t),r=o?.data??null,i=r?d(r,e?.langue??"fr"):null;return{contenu:p(n.defaut,i),publie:!!r,UID:o?.UID??null,chargement:e?.etat==="chargement"}},[e?.sections,e?.langue,e?.etat,t,n.defaut])}function C(t,n){const e=m();return c.useMemo(()=>{const o=(e?.sections??[]).filter(r=>r.blockType===t);return o.length===0?{items:(n?.defaut??[]).map((r,i)=>({...r,__uid:`defaut-${i}`})),publie:!1,chargement:e?.etat==="chargement"}:{items:o.map(r=>({...d(r.data??{},e?.langue??"fr"),__uid:r.UID})),publie:!0,chargement:e?.etat==="chargement"}},[e?.sections,e?.langue,e?.etat,t,n?.defaut])}s.SectionsProvider=A,s.useSection=x,s.useSectionList=C,s.useSectionsContext=m,Object.defineProperty(s,Symbol.toStringTag,{value:"Module"})}));
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@lystech/sections",
3
+ "version": "0.1.0",
4
+ "description": "Contenu de site editable par le client — lecture legere, edition chargee a la demande.",
5
+ "type": "module",
6
+ "main": "./dist/index.umd.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.umd.cjs"
14
+ }
15
+ },
16
+ "files": ["dist"],
17
+ "scripts": {
18
+ "build": "tsc -b && vite build",
19
+ "dev": "vite build --watch"
20
+ },
21
+ "peerDependencies": {
22
+ "react": ">=18",
23
+ "react-dom": ">=18"
24
+ },
25
+ "devDependencies": {
26
+ "@types/react": "^19.0.0",
27
+ "@types/react-dom": "^19.0.0",
28
+ "react": "^19.0.0",
29
+ "react-dom": "^19.0.0",
30
+ "typescript": "^5.7.2",
31
+ "vite": "^7.0.0",
32
+ "vite-plugin-dts": "^4.5.4"
33
+ },
34
+ "publishConfig": { "access": "public" }
35
+ }