@hgdc/ui 0.74.1 → 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. */
@@ -75,13 +77,19 @@ export declare function startBackgroundRefresh(doc?: Document): HTMLIFrameElemen
75
77
  export declare function productEntryBase(href: string): string | null;
76
78
  /**
77
79
  * Ouvre en arrière-plan la session des autres modules de la barre (ADR 0026) : un cadre caché à la
78
- * fois vers `<base>/auth/hgdc?frame=open`, au plus une fois par jour et par module. Session déjà
79
- * ouverte : réponse vide sans appel à ID. Sinon, la connexion HGDC se fait dans le cadre ; si ID
80
- * demande quelque chose (session expirée, consentement), il refuse d'être affiché dans un cadre et
81
- * rien ne se passe : le clic fera la connexion habituelle. Pas sur une page prérendue, ni quand
82
- * 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.
83
91
  */
84
- export declare function startSessionOpening(bases: ReadonlyArray<string>, doc?: Document): () => void;
92
+ export declare function startSessionOpening(bases: ReadonlyArray<string>, doc?: Document, current?: string): () => void;
85
93
  /**
86
94
  * Relecture en arrière-plan si l'entrée l'a demandé. Montée à la racine du document
87
95
  * (`FamilyLoaderRoot`), pour toutes les pages, y compris celles hors de la coque : la page « il te
@@ -99,6 +107,7 @@ export declare function useProductSwitch(enabled?: boolean): void;
99
107
  /**
100
108
  * Dans la barre de Basalte (`ShellNavigation`) : ouvre la session des modules affichés servis sous
101
109
  * la même origine, `SESSION_OPEN_DELAY_MS` après l'affichage (ADR 0026). `bases` : clé stable
102
- * (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.
103
112
  */
104
- 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. */
@@ -210,33 +212,84 @@ export function productEntryBase(href) {
210
212
  }
211
213
  /**
212
214
  * Ouvre en arrière-plan la session des autres modules de la barre (ADR 0026) : un cadre caché à la
213
- * fois vers `<base>/auth/hgdc?frame=open`, au plus une fois par jour et par module. Session déjà
214
- * ouverte : réponse vide sans appel à ID. Sinon, la connexion HGDC se fait dans le cadre ; si ID
215
- * demande quelque chose (session expirée, consentement), il refuse d'être affiché dans un cadre et
216
- * rien ne se passe : le clic fera la connexion habituelle. Pas sur une page prérendue, ni quand
217
- * 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.
218
226
  */
219
- export function startSessionOpening(bases, doc = document) {
227
+ export function startSessionOpening(bases, doc = document, current) {
220
228
  const win = doc.defaultView;
221
229
  const saveData = win?.navigator?.connection
222
230
  ?.saveData;
223
- if (saveData || doc.prerendering || openingPaused(win))
231
+ if (!win || saveData || doc.prerendering || openingPaused(win))
224
232
  return () => { };
225
- // Sans stockage, pas de limite par jour : rien ne part (sinon une connexion par module à chaque page).
226
- const queue = bases.filter((base) => refreshDue(base, win, Date.now(), SESSION_OPEN_KEY_PREFIX, SESSION_OPEN_INTERVAL_MS, false));
227
233
  let stopped = false;
228
- const next = () => {
229
- const base = queue.shift();
230
- // Relu avant chaque cadre : une déconnexion pendant la file l'arrête.
231
- if (stopped || base === undefined || openingPaused(win))
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()))
232
238
  return;
233
- openHiddenFrame(doc, `${base}${SESSION_OPEN_PATH}`, next).setAttribute(SESSION_OPEN_FRAME_ATTRIBUTE, "");
234
- };
235
- 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
+ })();
236
261
  return () => {
237
262
  stopped = true;
238
263
  };
239
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
+ }
240
293
  /**
241
294
  * Relecture en arrière-plan si l'entrée l'a demandé. Montée à la racine du document
242
295
  * (`FamilyLoaderRoot`), pour toutes les pages, y compris celles hors de la coque : la page « il te
@@ -275,20 +328,21 @@ export function useProductSwitch(enabled = true) {
275
328
  /**
276
329
  * Dans la barre de Basalte (`ShellNavigation`) : ouvre la session des modules affichés servis sous
277
330
  * la même origine, `SESSION_OPEN_DELAY_MS` après l'affichage (ADR 0026). `bases` : clé stable
278
- * (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.
279
333
  */
280
- export function useSessionOpening(bases) {
334
+ export function useSessionOpening(bases, current) {
281
335
  const key = bases.join(" ");
282
336
  useEffect(() => {
283
- if (!key)
337
+ if (!key || current === null)
284
338
  return;
285
339
  let stop = () => { };
286
340
  const timer = window.setTimeout(() => {
287
- stop = startSessionOpening(key.split(" "));
341
+ stop = startSessionOpening(key.split(" "), document, current);
288
342
  }, SESSION_OPEN_DELAY_MS);
289
343
  return () => {
290
344
  window.clearTimeout(timer);
291
345
  stop();
292
346
  };
293
- }, [key]);
347
+ }, [key, current]);
294
348
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hgdc/ui",
3
- "version": "0.74.1",
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",