@hgdc/ui 0.16.1 → 0.17.1

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/app-shell.js CHANGED
@@ -12,6 +12,7 @@ import { isApplePlatform, Kbd, KbdGroup } from "./kbd.js";
12
12
  import { CONTENT_LOADING_LAYER, ContentLoadingContext, PAGE_PENDING_MIN_MS, PAGE_PENDING_MS, PagePending, } from "./page-pending.js";
13
13
  import { ProductBadge } from "./product-badge.js";
14
14
  import { ProductRail } from "./product-rail.js";
15
+ import { useProductSwitch } from "./product-switch.js";
15
16
  import { HGDC_ACCOUNT, HGDC_PRODUCTS } from "./products.js";
16
17
  import { ProductRailShownContext, ShellLeaveContext } from "./shell-placement.js";
17
18
  import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
@@ -177,6 +178,8 @@ export function AppShell({ product, organization, productRail = true, accountHre
177
178
  // Barre des produits élargie : la barre latérale se plie le temps du survol (ADR 0011).
178
179
  const [railPeek, setRailPeek] = useState(false);
179
180
  useToggleShortcut();
181
+ // Relecture des organisations après un passage depuis la barre, préchargement des autres produits (ADR 0016).
182
+ useProductSwitch(productRail);
180
183
  // Chargement de la zone de contenu (ADR 0015) : une navigation en cours (après 50 ms), une page
181
184
  // qui attend (`PagePending`, aussitôt : le routeur a déjà attendu), ou le départ vers un autre
182
185
  // produit (aussitôt). Une fois montré, au moins 300 ms. Le H est celui du document, en position
@@ -1,6 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createContext, useContext, useEffect, useLayoutEffect, useRef } from "react";
3
3
  import { cn } from "./cn.js";
4
+ import { useBackgroundOrganizationRefresh } from "./product-switch.js";
4
5
  const SIZES = { sm: 24, md: 40, lg: 64 };
5
6
  /** Durée de la boucle du H (`styles.css`, `.family-loader [data-fill]`). */
6
7
  export const FAMILY_LOADER_PERIOD_MS = 2000;
@@ -105,6 +106,8 @@ export function FamilyLoaderHost() {
105
106
  * que dans la coque et que dans le produit qu'on vient de quitter (ADR 0015).
106
107
  */
107
108
  export function FamilyLoaderRoot({ children }) {
109
+ // Une fois par document : relecture des organisations après un passage depuis la barre (ADR 0016).
110
+ useBackgroundOrganizationRefresh();
108
111
  return (_jsxs(FamilyLoaderRootContext, { value: true, children: [children, _jsx(FamilyLoaderHost, {})] }));
109
112
  }
110
113
  /**
@@ -0,0 +1,35 @@
1
+ /** Cookie posé par l'entrée : relire les organisations en arrière-plan. */
2
+ export declare const ORG_SYNC_COOKIE = "hgdc-org-sync";
3
+ /** Cadre caché de la relecture : l'entrée repasse par Account, retour sur une réponse vide. */
4
+ export declare const BACKGROUND_REFRESH_PATH = "/auth/hgdc?refresh=1&frame=1";
5
+ /** Abandon du cadre s'il ne s'est pas chargé : Account injoignable, réseau coupé. */
6
+ export declare const BACKGROUND_REFRESH_TIMEOUT_MS = 20000;
7
+ /**
8
+ * Liens préchargés au survol : l'entrée d'un autre produit en `refresh=background`. Elle ne fait
9
+ * que lire la session et rediriger ; sans session, elle répond 503 à un préchargement (rien n'est
10
+ * créé), et le clic repart normalement. Jamais de `prerender` : la page d'arrivée lit un cookie et
11
+ * ouvre un cadre, ce qu'un prérendu ferait pour un simple survol.
12
+ */
13
+ export declare const PRODUCT_PREFETCH_RULES: string;
14
+ /** Lit et efface le cookie de relecture. Vrai s'il était posé. */
15
+ export declare function takeOrgSyncCookie(doc?: Document): boolean;
16
+ /**
17
+ * Relit les organisations dans un cadre caché, une fois, si l'entrée l'a demandé. Le cadre n'a
18
+ * ni script ni formulaire (`sandbox`), il suit seulement les redirections d'Account ; la nouvelle
19
+ * session du produit recopie les organisations, et les requêtes Convex abonnées se mettent à jour.
20
+ * Si la session d'Account a expiré, Account refuse d'être affiché dans un cadre : rien ne se passe,
21
+ * la session du produit reste celle d'avant.
22
+ */
23
+ export declare function startBackgroundRefresh(doc?: Document): HTMLIFrameElement | null;
24
+ /**
25
+ * Relecture en arrière-plan si l'entrée l'a demandé. Montée à la racine du document
26
+ * (`FamilyLoaderRoot`), pour toutes les pages, y compris celles hors de la coque : la page « il te
27
+ * faut une organisation » d'une personne qui vient d'accepter une invitation en a le plus besoin.
28
+ * Le cadre vit hors de l'arbre React : un démontage ne l'interrompt pas, il se retire seul.
29
+ */
30
+ export declare function useBackgroundOrganizationRefresh(): void;
31
+ /**
32
+ * Dans la coque (`AppShell` le fait) : la relecture ci-dessus si la racine ne l'a pas déjà faite
33
+ * (le cookie ne sert qu'une fois), et le préchargement au survol des autres produits.
34
+ */
35
+ export declare function useProductSwitch(enabled?: boolean): void;
@@ -0,0 +1,102 @@
1
+ import { useEffect } from "react";
2
+ /*
3
+ * Passage d'un produit à l'autre (ADR 0016) : la barre des produits pointe vers
4
+ * `/auth/hgdc?refresh=background&next=/`. Avec une session du produit ouverte, l'entrée de
5
+ * `@hgdc/auth` va droit à la page et pose un cookie « relire les organisations » ; la coque le lit
6
+ * ici et relit les organisations dans un cadre caché, sans faire attendre la personne.
7
+ *
8
+ * Les trois noms ci-dessous sont ceux de `@hgdc/auth` (`HGDC_ORG_SYNC_COOKIE`,
9
+ * `HGDC_BACKGROUND_REFRESH_PATH`, `HGDC_ENTRY_REFRESH_BACKGROUND`) : `@hgdc/ui` n'en dépend pas,
10
+ * un test de `@hgdc/auth` vérifie qu'ils restent égaux.
11
+ */
12
+ /** Cookie posé par l'entrée : relire les organisations en arrière-plan. */
13
+ export const ORG_SYNC_COOKIE = "hgdc-org-sync";
14
+ /** Cadre caché de la relecture : l'entrée repasse par Account, retour sur une réponse vide. */
15
+ export const BACKGROUND_REFRESH_PATH = "/auth/hgdc?refresh=1&frame=1";
16
+ /** Abandon du cadre s'il ne s'est pas chargé : Account injoignable, réseau coupé. */
17
+ export const BACKGROUND_REFRESH_TIMEOUT_MS = 20_000;
18
+ /**
19
+ * Liens préchargés au survol : l'entrée d'un autre produit en `refresh=background`. Elle ne fait
20
+ * que lire la session et rediriger ; sans session, elle répond 503 à un préchargement (rien n'est
21
+ * créé), et le clic repart normalement. Jamais de `prerender` : la page d'arrivée lit un cookie et
22
+ * ouvre un cadre, ce qu'un prérendu ferait pour un simple survol.
23
+ */
24
+ export const PRODUCT_PREFETCH_RULES = JSON.stringify({
25
+ prefetch: [
26
+ {
27
+ source: "document",
28
+ where: { href_matches: "https://*.hgdc.ch/auth/hgdc?refresh=background*" },
29
+ eagerness: "moderate",
30
+ },
31
+ ],
32
+ });
33
+ /** Lit et efface le cookie de relecture. Vrai s'il était posé. */
34
+ export function takeOrgSyncCookie(doc = document) {
35
+ const present = doc.cookie.split(";").some((part) => part.trim() === `${ORG_SYNC_COOKIE}=1`);
36
+ if (present) {
37
+ const secure = doc.location.protocol === "https:" ? "; Secure" : "";
38
+ doc.cookie = `${ORG_SYNC_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax${secure}`;
39
+ }
40
+ return present;
41
+ }
42
+ /**
43
+ * Relit les organisations dans un cadre caché, une fois, si l'entrée l'a demandé. Le cadre n'a
44
+ * ni script ni formulaire (`sandbox`), il suit seulement les redirections d'Account ; la nouvelle
45
+ * session du produit recopie les organisations, et les requêtes Convex abonnées se mettent à jour.
46
+ * Si la session d'Account a expiré, Account refuse d'être affiché dans un cadre : rien ne se passe,
47
+ * la session du produit reste celle d'avant.
48
+ */
49
+ export function startBackgroundRefresh(doc = document) {
50
+ if (!takeOrgSyncCookie(doc))
51
+ return null;
52
+ const frame = doc.createElement("iframe");
53
+ frame.hidden = true;
54
+ frame.tabIndex = -1;
55
+ frame.title = "";
56
+ frame.setAttribute("aria-hidden", "true");
57
+ // Même origine pour les cookies du produit ; ni script, ni formulaire, ni navigation de la page.
58
+ frame.setAttribute("sandbox", "allow-same-origin");
59
+ frame.referrerPolicy = "no-referrer";
60
+ const done = () => {
61
+ window.clearTimeout(timer);
62
+ frame.remove();
63
+ };
64
+ const timer = window.setTimeout(done, BACKGROUND_REFRESH_TIMEOUT_MS);
65
+ frame.addEventListener("load", done, { once: true });
66
+ frame.src = BACKGROUND_REFRESH_PATH;
67
+ doc.body.append(frame);
68
+ return frame;
69
+ }
70
+ /**
71
+ * Relecture en arrière-plan si l'entrée l'a demandé. Montée à la racine du document
72
+ * (`FamilyLoaderRoot`), pour toutes les pages, y compris celles hors de la coque : la page « il te
73
+ * faut une organisation » d'une personne qui vient d'accepter une invitation en a le plus besoin.
74
+ * Le cadre vit hors de l'arbre React : un démontage ne l'interrompt pas, il se retire seul.
75
+ */
76
+ export function useBackgroundOrganizationRefresh() {
77
+ useEffect(() => {
78
+ startBackgroundRefresh();
79
+ }, []);
80
+ }
81
+ /**
82
+ * Dans la coque (`AppShell` le fait) : la relecture ci-dessus si la racine ne l'a pas déjà faite
83
+ * (le cookie ne sert qu'une fois), et le préchargement au survol des autres produits.
84
+ */
85
+ export function useProductSwitch(enabled = true) {
86
+ useEffect(() => {
87
+ if (enabled)
88
+ startBackgroundRefresh();
89
+ }, [enabled]);
90
+ useEffect(() => {
91
+ if (!enabled)
92
+ return;
93
+ if (typeof HTMLScriptElement === "undefined" || !HTMLScriptElement.supports?.("speculationrules"))
94
+ return;
95
+ // Créé par le DOM, pas par React : un script inséré par `innerHTML` n'est jamais lu.
96
+ const script = document.createElement("script");
97
+ script.type = "speculationrules";
98
+ script.textContent = PRODUCT_PREFETCH_RULES;
99
+ document.head.append(script);
100
+ return () => script.remove();
101
+ }, [enabled]);
102
+ }
@@ -16,9 +16,10 @@ export type HgdcCatalogProduct = {
16
16
  href: string;
17
17
  /**
18
18
  * Entrée dans l'app avec la session HGDC Account (`PRODUCT_ENTRY_PATH`), sans page d'accueil
19
- * ni bouton de connexion : `https://<mot>.hgdc.ch/auth/hgdc?refresh=1&next=/`. `refresh=1`
20
- * relit les organisations au passage (une organisation rejointe dans Account apparaît sans se
21
- * reconnecter, ADR 0013). Pour Account, sa racine.
19
+ * ni bouton de connexion : `https://<mot>.hgdc.ch/auth/hgdc?refresh=background&next=/`. Avec une
20
+ * session du produit ouverte, on arrive tout de suite et les organisations se relisent en
21
+ * arrière-plan (une organisation rejointe dans Account apparaît sans se reconnecter, ADR 0013,
22
+ * 0016). Pour Account, sa racine.
22
23
  */
23
24
  entryHref: string;
24
25
  /** Une phrase, au tutoiement. */
@@ -40,8 +41,11 @@ export type HgdcCatalogProduct = {
40
41
  * quand cette session existe.
41
42
  */
42
43
  export declare const PRODUCT_ENTRY_PATH = "/auth/hgdc";
43
- /** Requête de l'entrée depuis un autre produit : relire les organisations, arriver sur l'accueil. */
44
- export declare const PRODUCT_ENTRY_REFRESH_QUERY = "refresh=1&next=/";
44
+ /**
45
+ * Requête de l'entrée depuis un autre produit : arriver sur l'accueil, relire les organisations en
46
+ * arrière-plan (`refresh=background`, ADR 0016).
47
+ */
48
+ export declare const PRODUCT_ENTRY_REFRESH_QUERY = "refresh=background&next=/";
45
49
  export declare const HGDC_ACCOUNT: HgdcCatalogProduct;
46
50
  /** Les produits de la famille, dans leur ordre d'affichage. Account n'en fait pas partie (`HGDC_ACCOUNT`). */
47
51
  export declare const HGDC_PRODUCTS: ReadonlyArray<HgdcCatalogProduct>;
@@ -83,7 +87,7 @@ export type ProductRailEntry = {
83
87
  /**
84
88
  * Les entrées de la barre des produits : les produits ouverts du catalogue et Account, affiché en
85
89
  * tête (le compte commun, pas un produit). Chaque produit pointe vers son entrée
86
- * `/auth/hgdc?refresh=1&next=/` (ADR 0010, 0013), Account vers sa racine (`accountHref` en
90
+ * `/auth/hgdc?refresh=background&next=/` (ADR 0010, 0013, 0016), Account vers sa racine (`accountHref` en
87
91
  * développement). Le produit courant pointe vers `/`, sans détour par l'entrée.
88
92
  */
89
93
  export declare function productRailEntries(options?: {
package/dist/products.js CHANGED
@@ -18,8 +18,11 @@ export const PRODUCT_STAGE_LABEL = {
18
18
  * quand cette session existe.
19
19
  */
20
20
  export const PRODUCT_ENTRY_PATH = "/auth/hgdc";
21
- /** Requête de l'entrée depuis un autre produit : relire les organisations, arriver sur l'accueil. */
22
- export const PRODUCT_ENTRY_REFRESH_QUERY = "refresh=1&next=/";
21
+ /**
22
+ * Requête de l'entrée depuis un autre produit : arriver sur l'accueil, relire les organisations en
23
+ * arrière-plan (`refresh=background`, ADR 0016).
24
+ */
25
+ export const PRODUCT_ENTRY_REFRESH_QUERY = "refresh=background&next=/";
23
26
  function product(word, pitch, icon, stage) {
24
27
  const badge = word.toLowerCase();
25
28
  if (!isFamilyProduct(badge))
@@ -77,7 +80,7 @@ export function productMenuEntries(options = {}) {
77
80
  /**
78
81
  * Les entrées de la barre des produits : les produits ouverts du catalogue et Account, affiché en
79
82
  * tête (le compte commun, pas un produit). Chaque produit pointe vers son entrée
80
- * `/auth/hgdc?refresh=1&next=/` (ADR 0010, 0013), Account vers sa racine (`accountHref` en
83
+ * `/auth/hgdc?refresh=background&next=/` (ADR 0010, 0013, 0016), Account vers sa racine (`accountHref` en
81
84
  * développement). Le produit courant pointe vers `/`, sans détour par l'entrée.
82
85
  */
83
86
  export function productRailEntries(options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hgdc/ui",
3
- "version": "0.16.1",
3
+ "version": "0.17.1",
4
4
  "description": "Interface de la H family : 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",