@hgdc/ui 0.74.0 → 0.74.2

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.
@@ -9,6 +9,11 @@ export type ShellNavLevel = "tabs" | "modules" | "edit";
9
9
  * ceux dont la session s'ouvre en arrière-plan (ADR 0026). Manager n'en fait pas partie.
10
10
  */
11
11
  export declare function sessionBases(entries: ReadonlyArray<ShellProductEntry>, shown: ReadonlyArray<ModuleWord>, current: FamilyProduct): Array<string>;
12
+ /**
13
+ * Base du module ouvert (`/mail`) si sa page est servie sous sa base, sinon `null` (rendu serveur,
14
+ * autre origine) : sa session décide de l'ouverture des autres (ADR 0026).
15
+ */
16
+ export declare function currentSessionBase(entries: ReadonlyArray<ShellProductEntry>): string | null;
12
17
  /**
13
18
  * Le milieu de la barre de Basalte : Manager, un trait, puis l'un des trois niveaux. Dans un module,
14
19
  * ses onglets (`nav`) sous « Retour aux modules » ; un clic y affiche la liste sans rien recharger ;
@@ -59,6 +59,14 @@ export function sessionBases(entries, shown, current) {
59
59
  return base ? [base] : [];
60
60
  });
61
61
  }
62
+ /**
63
+ * Base du module ouvert (`/mail`) si sa page est servie sous sa base, sinon `null` (rendu serveur,
64
+ * autre origine) : sa session décide de l'ouverture des autres (ADR 0026).
65
+ */
66
+ export function currentSessionBase(entries) {
67
+ const href = entries.find((entry) => entry.current)?.href.replace(/\/$/, "");
68
+ return href && href.startsWith("/") ? href : null;
69
+ }
62
70
  function ModuleList({ entries, order, onOpenCurrent, onCustomize, }) {
63
71
  const shown = order.flatMap((word) => {
64
72
  const entry = entries.find((item) => item.badge === word);
@@ -106,8 +114,8 @@ export function ShellNavigation({ product, nav, accountHref, level, onLevelChang
106
114
  const { products, account } = shellProductEntries({ current: product, accountHref, pageOrigin, organization });
107
115
  const shown = shownModules(order, product).filter((word) => !projectModules || word === product || projectModules.includes(word));
108
116
  // Sessions des autres modules affichés, ouvertes en arrière-plan : le premier clic n'attend pas
109
- // la connexion chez ID (ADR 0026).
110
- useSessionOpening(sessionBases(products, shown, product));
117
+ // la connexion chez ID (ADR 0026). Seulement si le module ouvert a sa session : pas dans la démo sans compte.
118
+ useSessionOpening(sessionBases(products, shown, product), currentSessionBase(products));
111
119
  return (_jsxs("div", { "data-slot": "shell-navigation", className: "flex min-h-0 flex-1 flex-col", children: [_jsxs("div", { "data-slot": "shell-manager", className: "shrink-0 px-3 pt-1", children: [_jsx("ul", { children: _jsx(ShellEntryLink, { entry: account, icon: UserGroupIcon }) }), _jsx("div", { "aria-hidden": true, className: "mx-2 mt-2 h-px bg-foreground/8" })] }), level === "modules" ? (_jsx(ModuleList, { entries: products, order: shown, onOpenCurrent: () => onLevelChange("tabs"), onCustomize: () => onLevelChange("edit") })) : level === "edit" ? (_jsx(ModuleBarEditor, { order: order, onSave: (next) => {
112
120
  saveModuleBar(next);
113
121
  void pushModuleBar(next);
@@ -15,6 +15,8 @@ export declare const BACKGROUND_REFRESH_INTERVAL_MS: number;
15
15
  export declare const ORG_SYNC_LAST_KEY_PREFIX = "hgdc-org-sync:";
16
16
  /** Cadre caché qui ouvre la session d'un autre produit, seulement si elle manque (ADR 0026). */
17
17
  export declare const SESSION_OPEN_PATH = "/auth/hgdc?frame=open";
18
+ /** Session du module ouvert, lue avant toute ouverture : la route better-auth sous sa base. */
19
+ export declare const SESSION_CHECK_PATH = "/api/auth/get-session";
18
20
  /** Une ouverture au plus par produit pendant ce délai : une session vit 7 jours (`@hgdc/auth`). */
19
21
  export declare const SESSION_OPEN_INTERVAL_MS: number;
20
22
  /** Attente après l'affichage de la page avant la première ouverture : la page passe d'abord. */
@@ -27,7 +29,11 @@ export declare const SESSION_OPEN_KEY_PREFIX = "hgdc-session-open:";
27
29
  * déconnexion (changer de compte, poste partagé) n'est jamais défaite sans la personne.
28
30
  */
29
31
  export declare const SESSION_OPEN_PAUSED_KEY = "hgdc-session-open:paused";
30
- /** À la déconnexion d'un module : plus d'ouverture en arrière-plan jusqu'à la prochaine connexion. */
32
+ /**
33
+ * À la déconnexion d'un module : plus d'ouverture en arrière-plan jusqu'à la prochaine connexion, et
34
+ * les cadres d'ouverture en cours sont retirés (un cadre en vol reposerait la session d'un module
35
+ * juste après la déconnexion).
36
+ */
31
37
  export declare function pauseSessionOpening(win?: Window | null): void;
32
38
  /** Marque d'un lien de la barre vers un autre produit servi sous la même origine : prérendu au survol. */
33
39
  export declare const PRODUCT_PRERENDER_ATTRIBUTE = "data-hgdc-prerender";
@@ -71,13 +77,19 @@ export declare function startBackgroundRefresh(doc?: Document): HTMLIFrameElemen
71
77
  export declare function productEntryBase(href: string): string | null;
72
78
  /**
73
79
  * Ouvre en arrière-plan la session des autres modules de la barre (ADR 0026) : un cadre caché à la
74
- * fois vers `<base>/auth/hgdc?frame=open`, au plus une fois par jour et par module. Session déjà
75
- * ouverte : réponse vide sans appel à ID. Sinon, la connexion HGDC se fait dans le cadre ; si ID
76
- * demande quelque chose (session expirée, consentement), il refuse d'être affiché dans un cadre et
77
- * rien ne se passe : le clic fera la connexion habituelle. Pas sur une page prérendue, ni quand
78
- * l'économie de données est demandée. Rend une fonction qui arrête la file.
80
+ * fois vers `<base>/auth/hgdc?frame=open`, au plus une fois par jour et par module, et seulement pour
81
+ * un module sans session : sa session est lue d'abord (`SESSION_CHECK_PATH`, même origine). Déjà
82
+ * ouverte : pas de cadre, la limite du jour est marquée. Lecture en échec : pas de cadre, la limite
83
+ * n'est pas consommée. Sinon, la connexion HGDC se fait dans le cadre ; si ID demande quelque chose
84
+ * (session expirée, consentement), il refuse d'être affiché dans un cadre et rien ne se passe : le
85
+ * clic fera la connexion habituelle. Pas sur une page prérendue, ni quand l'économie de données est
86
+ * demandée. Rend une fonction qui arrête la file.
87
+ *
88
+ * `current` : base du module ouvert. Donnée, rien ne part tant que ce module n'a pas de session : un
89
+ * visiteur de la démo, sans compte, n'a pas de session d'ID à reprendre, et chaque cadre partirait
90
+ * pour rien vers ID.
79
91
  */
80
- export declare function startSessionOpening(bases: ReadonlyArray<string>, doc?: Document): () => void;
92
+ export declare function startSessionOpening(bases: ReadonlyArray<string>, doc?: Document, current?: string): () => void;
81
93
  /**
82
94
  * Relecture en arrière-plan si l'entrée l'a demandé. Montée à la racine du document
83
95
  * (`FamilyLoaderRoot`), pour toutes les pages, y compris celles hors de la coque : la page « il te
@@ -95,6 +107,7 @@ export declare function useProductSwitch(enabled?: boolean): void;
95
107
  /**
96
108
  * Dans la barre de Basalte (`ShellNavigation`) : ouvre la session des modules affichés servis sous
97
109
  * la même origine, `SESSION_OPEN_DELAY_MS` après l'affichage (ADR 0026). `bases` : clé stable
98
- * (les bases jointes), pour ne pas relancer la file à chaque rendu.
110
+ * (les bases jointes), pour ne pas relancer la file à chaque rendu. `current` : base du module ouvert
111
+ * (`startSessionOpening`) ; `null` (base inconnue) : rien ne part.
99
112
  */
100
- export declare function useSessionOpening(bases: ReadonlyArray<string>): void;
113
+ export declare function useSessionOpening(bases: ReadonlyArray<string>, current?: string | null): void;
@@ -26,6 +26,8 @@ export const BACKGROUND_REFRESH_INTERVAL_MS = 10 * 60_000;
26
26
  export const ORG_SYNC_LAST_KEY_PREFIX = "hgdc-org-sync:";
27
27
  /** Cadre caché qui ouvre la session d'un autre produit, seulement si elle manque (ADR 0026). */
28
28
  export const SESSION_OPEN_PATH = "/auth/hgdc?frame=open";
29
+ /** Session du module ouvert, lue avant toute ouverture : la route better-auth sous sa base. */
30
+ export const SESSION_CHECK_PATH = "/api/auth/get-session";
29
31
  /** Une ouverture au plus par produit pendant ce délai : une session vit 7 jours (`@hgdc/auth`). */
30
32
  export const SESSION_OPEN_INTERVAL_MS = 24 * 60 * 60_000;
31
33
  /** Attente après l'affichage de la page avant la première ouverture : la page passe d'abord. */
@@ -38,7 +40,13 @@ export const SESSION_OPEN_KEY_PREFIX = "hgdc-session-open:";
38
40
  * déconnexion (changer de compte, poste partagé) n'est jamais défaite sans la personne.
39
41
  */
40
42
  export const SESSION_OPEN_PAUSED_KEY = "hgdc-session-open:paused";
41
- /** À la déconnexion d'un module : plus d'ouverture en arrière-plan jusqu'à la prochaine connexion. */
43
+ /** Marque des cadres d'ouverture (`startSessionOpening`), retirés par `pauseSessionOpening`. */
44
+ const SESSION_OPEN_FRAME_ATTRIBUTE = "data-hgdc-session-open";
45
+ /**
46
+ * À la déconnexion d'un module : plus d'ouverture en arrière-plan jusqu'à la prochaine connexion, et
47
+ * les cadres d'ouverture en cours sont retirés (un cadre en vol reposerait la session d'un module
48
+ * juste après la déconnexion).
49
+ */
42
50
  export function pauseSessionOpening(win = typeof window === "undefined" ? null : window) {
43
51
  try {
44
52
  win?.localStorage.setItem(SESSION_OPEN_PAUSED_KEY, String(Date.now()));
@@ -46,6 +54,8 @@ export function pauseSessionOpening(win = typeof window === "undefined" ? null :
46
54
  catch {
47
55
  // Stockage refusé : l'ouverture n'a pas lieu non plus sans lui (`startSessionOpening`).
48
56
  }
57
+ for (const frame of win?.document.querySelectorAll(`iframe[${SESSION_OPEN_FRAME_ATTRIBUTE}]`) ?? [])
58
+ frame.remove();
49
59
  }
50
60
  /** Marque d'un lien de la barre vers un autre produit servi sous la même origine : prérendu au survol. */
51
61
  export const PRODUCT_PRERENDER_ATTRIBUTE = "data-hgdc-prerender";
@@ -202,32 +212,84 @@ export function productEntryBase(href) {
202
212
  }
203
213
  /**
204
214
  * Ouvre en arrière-plan la session des autres modules de la barre (ADR 0026) : un cadre caché à la
205
- * fois vers `<base>/auth/hgdc?frame=open`, au plus une fois par jour et par module. Session déjà
206
- * ouverte : réponse vide sans appel à ID. Sinon, la connexion HGDC se fait dans le cadre ; si ID
207
- * demande quelque chose (session expirée, consentement), il refuse d'être affiché dans un cadre et
208
- * rien ne se passe : le clic fera la connexion habituelle. Pas sur une page prérendue, ni quand
209
- * l'économie de données est demandée. Rend une fonction qui arrête la file.
215
+ * fois vers `<base>/auth/hgdc?frame=open`, au plus une fois par jour et par module, et seulement pour
216
+ * un module sans session : sa session est lue d'abord (`SESSION_CHECK_PATH`, même origine). Déjà
217
+ * ouverte : pas de cadre, la limite du jour est marquée. Lecture en échec : pas de cadre, la limite
218
+ * n'est pas consommée. Sinon, la connexion HGDC se fait dans le cadre ; si ID demande quelque chose
219
+ * (session expirée, consentement), il refuse d'être affiché dans un cadre et rien ne se passe : le
220
+ * clic fera la connexion habituelle. Pas sur une page prérendue, ni quand l'économie de données est
221
+ * demandée. Rend une fonction qui arrête la file.
222
+ *
223
+ * `current` : base du module ouvert. Donnée, rien ne part tant que ce module n'a pas de session : un
224
+ * visiteur de la démo, sans compte, n'a pas de session d'ID à reprendre, et chaque cadre partirait
225
+ * pour rien vers ID.
210
226
  */
211
- export function startSessionOpening(bases, doc = document) {
227
+ export function startSessionOpening(bases, doc = document, current) {
212
228
  const win = doc.defaultView;
213
229
  const saveData = win?.navigator?.connection
214
230
  ?.saveData;
215
- if (saveData || doc.prerendering || openingPaused(win))
231
+ if (!win || saveData || doc.prerendering || openingPaused(win))
216
232
  return () => { };
217
- // Sans stockage, pas de limite par jour : rien ne part (sinon une connexion par module à chaque page).
218
- const queue = bases.filter((base) => refreshDue(base, win, Date.now(), SESSION_OPEN_KEY_PREFIX, SESSION_OPEN_INTERVAL_MS, false));
219
233
  let stopped = false;
220
- const next = () => {
221
- const base = queue.shift();
222
- if (stopped || base === undefined)
234
+ // Relu avant chaque étape et après chaque lecture : une déconnexion pendant la file l'arrête.
235
+ const halted = () => stopped || openingPaused(win);
236
+ void (async () => {
237
+ if (current !== undefined && ((await readSession(win, current)) !== true || halted()))
223
238
  return;
224
- openHiddenFrame(doc, `${base}${SESSION_OPEN_PATH}`, next);
225
- };
226
- next();
239
+ for (const base of bases) {
240
+ if (halted())
241
+ return;
242
+ // Sans stockage, pas de limite par jour : rien ne part (sinon une connexion par module à chaque page).
243
+ if (!sessionOpenDue(win, base))
244
+ continue;
245
+ const open = await readSession(win, base);
246
+ if (halted())
247
+ return;
248
+ // Lecture en échec : rien à décider, la limite reste libre pour la prochaine page.
249
+ if (open === null)
250
+ continue;
251
+ // Marque la limite ; faux si un autre onglet vient de le faire, ou si le stockage refuse.
252
+ if (!refreshDue(base, win, Date.now(), SESSION_OPEN_KEY_PREFIX, SESSION_OPEN_INTERVAL_MS, false))
253
+ continue;
254
+ if (open)
255
+ continue;
256
+ await new Promise((resolve) => {
257
+ openHiddenFrame(doc, `${base}${SESSION_OPEN_PATH}`, resolve).setAttribute(SESSION_OPEN_FRAME_ATTRIBUTE, "");
258
+ });
259
+ }
260
+ })();
227
261
  return () => {
228
262
  stopped = true;
229
263
  };
230
264
  }
265
+ /** Vrai si la limite du jour de ce module est libre, sans la marquer. Stockage refusé : faux. */
266
+ function sessionOpenDue(win, base) {
267
+ try {
268
+ const last = Number(win.localStorage.getItem(`${SESSION_OPEN_KEY_PREFIX}${base || "/"}`));
269
+ const age = Date.now() - last;
270
+ return !(Number.isFinite(last) && last > 0 && age >= 0 && age < SESSION_OPEN_INTERVAL_MS);
271
+ }
272
+ catch {
273
+ return false;
274
+ }
275
+ }
276
+ /** Session du module de `base` : ouverte, absente, ou `null` si la lecture échoue. */
277
+ async function readSession(win, base) {
278
+ try {
279
+ const response = await win.fetch(`${base}${SESSION_CHECK_PATH}`, { credentials: "include", cache: "no-store" });
280
+ if (!response.ok)
281
+ return null;
282
+ const body = await response.json();
283
+ if (body === null)
284
+ return false;
285
+ if (typeof body !== "object")
286
+ return null;
287
+ return "session" in body && Boolean(body.session);
288
+ }
289
+ catch {
290
+ return null;
291
+ }
292
+ }
231
293
  /**
232
294
  * Relecture en arrière-plan si l'entrée l'a demandé. Montée à la racine du document
233
295
  * (`FamilyLoaderRoot`), pour toutes les pages, y compris celles hors de la coque : la page « il te
@@ -266,20 +328,21 @@ export function useProductSwitch(enabled = true) {
266
328
  /**
267
329
  * Dans la barre de Basalte (`ShellNavigation`) : ouvre la session des modules affichés servis sous
268
330
  * la même origine, `SESSION_OPEN_DELAY_MS` après l'affichage (ADR 0026). `bases` : clé stable
269
- * (les bases jointes), pour ne pas relancer la file à chaque rendu.
331
+ * (les bases jointes), pour ne pas relancer la file à chaque rendu. `current` : base du module ouvert
332
+ * (`startSessionOpening`) ; `null` (base inconnue) : rien ne part.
270
333
  */
271
- export function useSessionOpening(bases) {
334
+ export function useSessionOpening(bases, current) {
272
335
  const key = bases.join(" ");
273
336
  useEffect(() => {
274
- if (!key)
337
+ if (!key || current === null)
275
338
  return;
276
339
  let stop = () => { };
277
340
  const timer = window.setTimeout(() => {
278
- stop = startSessionOpening(key.split(" "));
341
+ stop = startSessionOpening(key.split(" "), document, current);
279
342
  }, SESSION_OPEN_DELAY_MS);
280
343
  return () => {
281
344
  window.clearTimeout(timer);
282
345
  stop();
283
346
  };
284
- }, [key]);
347
+ }, [key, current]);
285
348
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hgdc/ui",
3
- "version": "0.74.0",
3
+ "version": "0.74.2",
4
4
  "description": "Interface de la H Collection : jetons de la DA, thèmes, polices et composants.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://github.com/hugogebel-boop/hgdc-kit/tree/main/packages/ui#readme",