@hgdc/ui 0.70.0 → 0.72.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 +3 -2
- package/dist/app-shell.d.ts +6 -5
- package/dist/app-shell.js +8 -16
- package/dist/edge-glow.d.ts +24 -0
- package/dist/edge-glow.js +148 -0
- package/dist/family-loader.d.ts +10 -9
- package/dist/family-loader.js +11 -17
- package/dist/module-bar.js +7 -1
- package/dist/page-pending.d.ts +34 -34
- package/dist/page-pending.js +38 -41
- package/dist/page-skeleton.d.ts +16 -0
- package/dist/page-skeleton.js +14 -0
- package/dist/product-switch.d.ts +16 -6
- package/dist/product-switch.js +37 -7
- package/dist/products.d.ts +10 -2
- package/dist/products.js +23 -5
- package/dist/spinner.d.ts +5 -4
- package/dist/spinner.js +8 -5
- package/dist/use-content-loading.d.ts +4 -7
- package/dist/use-content-loading.js +7 -17
- package/package.json +1 -1
- package/styles.css +127 -21
package/README.md
CHANGED
|
@@ -135,8 +135,9 @@ Une seule barre latérale (ADR 0024) : organisation et bouton Réduire en haut,
|
|
|
135
135
|
| `secret-reveal` | `<SecretReveal label="Clé d'API" value={key} onDismiss={forget} />` : affichée une fois, puis retirée de la page |
|
|
136
136
|
| `assistant-chat` | `<AssistantConversation><AssistantUserMessage>…</AssistantUserMessage><AssistantReply tools={chips} waiting={pending} actions={<AssistantActionCard title=… status="pending" onConfirm={…} onCancel={…} />}><Markdown>{text}</Markdown></AssistantReply></AssistantConversation>`, `<AssistantComposer value={v} onValueChange={setV} onSubmit={send} busy={busy} />`, `<AssistantExamples examples={…} onPick={setV} />` : le produit rend le Markdown |
|
|
137
137
|
| `assistant-view` | `<AssistantView status={status} threads={threads} threadId={id} onThreadChange={setId} thread={thread} renderText={(t) => <Markdown>{t}</Markdown>} onSend={send} onConfirm={confirm} onCancel={cancel} />` : le panneau complet pour `AppShell` (`assistant.panel`), aux formes de `@hgdc/assistant/convex` ; `subject` (page ouverte), `request` (demande venue de la page), `labels` (vouvoiement) |
|
|
138
|
-
| `family-loader` | `<FamilyLoaderRoot>{children}</FamilyLoaderRoot>` une fois dans le `body` de la route racine :
|
|
139
|
-
| `page-pending` | `createRouter({ routeTree, ...routerPendingDefaults })` (préchargement au survol,
|
|
138
|
+
| `family-loader` | `<FamilyLoaderRoot>{children}</FamilyLoaderRoot>` une fois dans le `body` de la route racine : la lueur du chargement de page (`EdgeGlow`, une bande tricolore qui ondule le long de la carte du contenu, WebGL, repli CSS rendu par le serveur), calée sur l'horloge (même phase d'une page et d'un produit à l'autre). `<FamilyLoader size="lg" />` : le H en ligne (une boucle de 2 s, `transform` et `opacity`). Dans un bouton : `Spinner` |
|
|
139
|
+
| `page-pending` | `createRouter({ routeTree, ...routerPendingDefaults })` (préchargement au survol, squelette après 1 s, au moins 500 ms, ADR 0025) ; `AppShell {...useRouterState({ select: shellNavigation, structuralSharing: true })}` : une navigation montre la barre de progression, l'ancienne page reste ; `PagePending variant="list" | "dashboard" | "form"` : dans la coque, le squelette de la page ; hors de la coque, la lueur du document |
|
|
140
|
+
| `page-skeleton` | `<PageSkeleton variant="dashboard" />` : la forme d'une page qui charge, calée sur `Page` (en-tête, colonne, espacement), pour que rien ne saute à l'arrivée |
|
|
140
141
|
| `skeleton` | `<Pending pending={isLoading} fallback={<TableSkeleton columns={3} />}>…</Pending>` : après 200 ms, tenu 400 ms au moins |
|
|
141
142
|
|
|
142
143
|
## Listes, journaux et retours
|
package/dist/app-shell.d.ts
CHANGED
|
@@ -79,16 +79,17 @@ export declare function AppShell({ product, organization, productRail: productRa
|
|
|
79
79
|
children: ReactNode;
|
|
80
80
|
/**
|
|
81
81
|
* Clé de la page affichée (le chemin, `location.pathname`) : la zone de contenu devient une
|
|
82
|
-
* frontière `Suspense` neuve à chaque page, qui montre
|
|
83
|
-
* ses données dans ses composants (`useSuspenseQuery`)
|
|
84
|
-
*
|
|
82
|
+
* frontière `Suspense` neuve à chaque page, qui montre le squelette de la page (`PagePending`)
|
|
83
|
+
* pendant qu'une page charge ses données dans ses composants (`useSuspenseQuery`). Sans clé, pas
|
|
84
|
+
* de frontière.
|
|
85
85
|
*/
|
|
86
86
|
contentKey?: string | undefined;
|
|
87
87
|
/**
|
|
88
88
|
* Une navigation vers une autre page est en cours (TanStack Router :
|
|
89
89
|
* `useRouterState({ select: (s) => s.isLoading && s.location.pathname !== s.resolvedLocation?.pathname })`).
|
|
90
|
-
* Au-delà de
|
|
91
|
-
*
|
|
90
|
+
* Au-delà de 150 ms, une fine barre de progression court sous la barre du haut ; l'ancienne page
|
|
91
|
+
* reste affichée jusqu'à ce que la nouvelle soit prête, ou que son squelette la remplace après 1 s
|
|
92
|
+
* (`routerPendingDefaults`, ADR 0025).
|
|
92
93
|
*/
|
|
93
94
|
navigating?: boolean;
|
|
94
95
|
/**
|
package/dist/app-shell.js
CHANGED
|
@@ -3,11 +3,11 @@ import Settings02Icon from "@hugeicons/core-free-icons/Settings02Icon";
|
|
|
3
3
|
import { SIDEBAR_ITEM } from "./sidebar-nav.js";
|
|
4
4
|
import Menu01Icon from "@hugeicons/core-free-icons/Menu01Icon";
|
|
5
5
|
import { HugeiconsIcon } from "@hugeicons/react";
|
|
6
|
-
import { Suspense,
|
|
6
|
+
import { Suspense, useContext, useEffect, useId, useRef, useState } from "react";
|
|
7
7
|
import { CommandMenuButton } from "./command-menu.js";
|
|
8
8
|
import { Drawer, DrawerContent, DrawerTitle } from "./drawer.js";
|
|
9
9
|
import { FamilyLoaderHost, FamilyLoaderRootContext } from "./family-loader.js";
|
|
10
|
-
import { CONTENT_LOADING_LAYER,
|
|
10
|
+
import { CONTENT_LOADING_LAYER, NAVIGATION_PROGRESS_MIN_MS, NAVIGATION_PROGRESS_MS, PagePending, ShellContentContext, useDelayedFlag, } from "./page-pending.js";
|
|
11
11
|
import { ProductBadge } from "./product-badge.js";
|
|
12
12
|
import { productBarShown, useProductBarPreference, useProductBarSync } from "./product-bar.js";
|
|
13
13
|
import { ShellNavigation } from "./module-bar.js";
|
|
@@ -22,7 +22,7 @@ import { rememberLastProduct, rememberShellNavHeight } from "./shell-memory.js";
|
|
|
22
22
|
import { ModuleBarShownContext, ShellLeaveContext, ShellMenuLinksContext, StatusPageContext, } from "./shell-placement.js";
|
|
23
23
|
import { ShellOrganizationHead, SidebarHead } from "./sidebar-head.js";
|
|
24
24
|
import { useToggleShortcut } from "./sidebar-rail.js";
|
|
25
|
-
import {
|
|
25
|
+
import { useHydrated, useIsomorphicLayoutEffect, useShellCover } from "./use-content-loading.js";
|
|
26
26
|
import { useProductLeave } from "./use-product-leave.js";
|
|
27
27
|
import { useMediaQuery } from "./use-media-query.js";
|
|
28
28
|
export { sidebarInitScript, toggleSidebar, useSidebarRail } from "./sidebar-rail.js";
|
|
@@ -50,20 +50,12 @@ export function AppShell({ product, organization, productRail: productRailOption
|
|
|
50
50
|
useToggleShortcut();
|
|
51
51
|
// Relecture des organisations après un passage depuis la barre, préchargement des autres produits (ADR 0016).
|
|
52
52
|
useProductSwitch(productRail);
|
|
53
|
-
// Chargement
|
|
54
|
-
//
|
|
55
|
-
// produit (aussitôt). Une fois montré, au moins 300 ms. Le H est celui du document, en position
|
|
56
|
-
// fixe : il ne bouge pas d'un état à l'autre et n'est jamais démonté.
|
|
57
|
-
const [pagePending, setPagePending] = useState(0);
|
|
58
|
-
const registerPagePending = useCallback(() => {
|
|
59
|
-
setPagePending((count) => count + 1);
|
|
60
|
-
return () => setPagePending((count) => count - 1);
|
|
61
|
-
}, []);
|
|
53
|
+
// Chargement (ADR 0025) : une navigation dans le module montre la barre de progression, l'ancienne
|
|
54
|
+
// page restant à l'écran ; seul le départ vers un autre module couvre la zone de contenu du H.
|
|
62
55
|
const { leaving, leave } = useProductLeave();
|
|
56
|
+
const progress = useDelayedFlag(navigating, NAVIGATION_PROGRESS_MS, NAVIGATION_PROGRESS_MIN_MS);
|
|
63
57
|
const statusPageUrl = statusPage === false ? null : (statusPage ?? statusPageUrlFor(product));
|
|
64
|
-
const loading =
|
|
65
|
-
// Avant l'hydratation, le CSS montre le calque et le H dès qu'une page attend (`:has`) ; ensuite
|
|
66
|
-
// `data-loading` prend le relais dans la même image.
|
|
58
|
+
const loading = useShellCover(leaving);
|
|
67
59
|
const hydrated = useHydrated();
|
|
68
60
|
const hasRoot = useContext(FamilyLoaderRootContext);
|
|
69
61
|
const contentRef = useRef(null);
|
|
@@ -111,5 +103,5 @@ export function AppShell({ product, organization, productRail: productRailOption
|
|
|
111
103
|
}
|
|
112
104
|
: {};
|
|
113
105
|
const navigation = productRail ? (_jsx(ShellNavigation, { product: product, nav: nav, accountHref: accountHref, level: navLevel, onLevelChange: setNavLevel, projectModules: project?.modules })) : (nav);
|
|
114
|
-
return (_jsx(ShellContext.Provider, { value: { closeMobileNav: () => setMenuOpen(false), product }, children: _jsx(ModuleBarShownContext, { value: productRail, children: _jsx(ShellMenuLinksContext, { value: menuLinks, children: _jsx(ShellLeaveContext, { value: leave, children: _jsx(StatusPageContext, { value: statusPageUrl, children: _jsxs("div", { "data-slot": "app-shell", "data-module-bar": productRail ? "" : undefined, "data-loading": loading ? "" : undefined, "data-hydrated": hydrated ? "" : undefined, "data-assistant": assistantOpen ? "" : undefined, className: "min-h-dvh lg:grid lg:h-dvh lg:grid-cols-[auto_minmax(0,1fr)_auto] lg:bg-shell", children: [_jsx("a", { href: "#contenu", className: "sr-only z-50 rounded-xl bg-background px-4 py-2 text-sm font-medium focus:not-sr-only focus:fixed focus:top-3 focus:left-3 focus:ring-2 focus:ring-ring", children: "Aller au contenu" }), _jsxs("aside", { ref: sidebarRef, "data-slot": "app-shell-sidebar", "aria-label": "Barre lat\u00E9rale", inert: pageBlocked || undefined, className: "group/sidebar hidden overflow-hidden text-shell-foreground transition-[width] duration-(--duration-base) ease-ios motion-reduce:transition-none lg:flex lg:w-64 lg:flex-col lg:rail:w-16", children: [productRail ? (_jsx(ShellOrganizationHead, { organization: organization })) : (_jsxs(_Fragment, { children: [_jsx(SidebarHead, { product: product }), organization ? _jsx("div", { className: "shrink-0 px-3 pb-1", children: organization }) : null] })), navigation, _jsx("div", { "data-slot": "app-shell-footer", className: "grid gap-0.5 px-3 pt-1 pb-3 empty:hidden", children: footer })] }), _jsx("div", { inert: pageBlocked || undefined, className: "flex min-h-dvh flex-col lg:min-h-0 lg:p-2 lg:pl-0", children: _jsxs("div", { ref: contentRef, className: "relative flex flex-1 flex-col lg:min-h-0 lg:overflow-y-auto lg:rounded-xl lg:bg-background lg:[scrollbar-gutter:stable]", children: [_jsxs("header", { "data-slot": "app-shell-header", className: "sticky top-[env(safe-area-inset-top,0px)] z-30 flex min-h-14 shrink-0 flex-wrap items-center gap-x-1 gap-y-1 border-b border-foreground/5 bg-background px-2 py-1.5 sm:px-4 lg:top-0", children: [_jsx("button", { type: "button", onClick: () => setMenuOpen(true), "aria-label": "Ouvrir le menu", className: "touch-target grid size-10 shrink-0 place-items-center rounded-xl text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 lg:hidden", children: _jsx(HugeiconsIcon, { icon: Menu01Icon, "aria-hidden": true, className: "size-5" }) }), _jsxs("div", { "data-slot": "app-shell-brand", className: "flex shrink-0 items-center gap-2 pr-1 lg:hidden", children: [_jsx(ProductBadge, { product: product, className: "size-6" }), _jsx("span", { className: "font-heading text-[0.9375rem] font-semibold", children: familyProductName(product) })] }), _jsx("div", { className: cn("flex min-w-0 items-center gap-2", header ? "flex-[1_1_7rem]" : "flex-1"), children: header }), onSearch ? (_jsx(CommandMenuButton, { onClick: onSearch, compact: "mobile", className: "shrink-0 lg:w-96 lg:max-w-none", ...(searchLabel !== undefined ? { label: searchLabel } : {}) })) : null, assistant ? (_jsx(AssistantButton, { assistant: assistant, panelId: assistantPanelId, buttonRef: assistantButtonRef })) : null] }), _jsx("div", { "data-slot": "app-shell-loading", "aria-hidden": !loading, className: CONTENT_LOADING_LAYER, children: loading ? (_jsx("span", { role: "status", className: "sr-only", children: "Chargement\u2026" })) : null }), _jsxs("main", { id: "contenu", tabIndex: -1, "aria-busy": loading ? true : undefined, inert: loading || undefined, className: cn("mx-auto w-full max-w-6xl flex-1 px-4 pt-6 pb-16 outline-none sm:px-6 lg:px-10 lg:pt-8", mainClassName), children: [_jsx(ServiceStatus, { pageUrl: statusPageUrl, className: "mb-6" }), _jsx(
|
|
106
|
+
return (_jsx(ShellContext.Provider, { value: { closeMobileNav: () => setMenuOpen(false), product }, children: _jsx(ModuleBarShownContext, { value: productRail, children: _jsx(ShellMenuLinksContext, { value: menuLinks, children: _jsx(ShellLeaveContext, { value: leave, children: _jsx(StatusPageContext, { value: statusPageUrl, children: _jsxs("div", { "data-slot": "app-shell", "data-module-bar": productRail ? "" : undefined, "data-loading": loading ? "" : undefined, "data-navigating": progress ? "" : undefined, "data-hydrated": hydrated ? "" : undefined, "data-assistant": assistantOpen ? "" : undefined, className: "min-h-dvh lg:grid lg:h-dvh lg:grid-cols-[auto_minmax(0,1fr)_auto] lg:bg-shell", children: [_jsx("a", { href: "#contenu", className: "sr-only z-50 rounded-xl bg-background px-4 py-2 text-sm font-medium focus:not-sr-only focus:fixed focus:top-3 focus:left-3 focus:ring-2 focus:ring-ring", children: "Aller au contenu" }), _jsxs("aside", { ref: sidebarRef, "data-slot": "app-shell-sidebar", "aria-label": "Barre lat\u00E9rale", inert: pageBlocked || undefined, className: "group/sidebar hidden overflow-hidden text-shell-foreground transition-[width] duration-(--duration-base) ease-ios motion-reduce:transition-none lg:flex lg:w-64 lg:flex-col lg:rail:w-16", children: [productRail ? (_jsx(ShellOrganizationHead, { organization: organization })) : (_jsxs(_Fragment, { children: [_jsx(SidebarHead, { product: product }), organization ? _jsx("div", { className: "shrink-0 px-3 pb-1", children: organization }) : null] })), navigation, _jsx("div", { "data-slot": "app-shell-footer", className: "grid gap-0.5 px-3 pt-1 pb-3 empty:hidden", children: footer })] }), _jsx("div", { inert: pageBlocked || undefined, className: "flex min-h-dvh flex-col lg:min-h-0 lg:p-2 lg:pl-0", children: _jsxs("div", { ref: contentRef, className: "relative flex flex-1 flex-col lg:min-h-0 lg:overflow-y-auto lg:rounded-xl lg:bg-background lg:[scrollbar-gutter:stable]", children: [_jsxs("header", { "data-slot": "app-shell-header", className: "sticky top-[env(safe-area-inset-top,0px)] z-30 flex min-h-14 shrink-0 flex-wrap items-center gap-x-1 gap-y-1 border-b border-foreground/5 bg-background px-2 py-1.5 sm:px-4 lg:top-0", children: [_jsx("div", { "data-slot": "app-shell-progress", "aria-hidden": true }), _jsx("button", { type: "button", onClick: () => setMenuOpen(true), "aria-label": "Ouvrir le menu", className: "touch-target grid size-10 shrink-0 place-items-center rounded-xl text-muted-foreground outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 lg:hidden", children: _jsx(HugeiconsIcon, { icon: Menu01Icon, "aria-hidden": true, className: "size-5" }) }), _jsxs("div", { "data-slot": "app-shell-brand", className: "flex shrink-0 items-center gap-2 pr-1 lg:hidden", children: [_jsx(ProductBadge, { product: product, className: "size-6" }), _jsx("span", { className: "font-heading text-[0.9375rem] font-semibold", children: familyProductName(product) })] }), _jsx("div", { className: cn("flex min-w-0 items-center gap-2", header ? "flex-[1_1_7rem]" : "flex-1"), children: header }), onSearch ? (_jsx(CommandMenuButton, { onClick: onSearch, compact: "mobile", className: "shrink-0 lg:w-96 lg:max-w-none", ...(searchLabel !== undefined ? { label: searchLabel } : {}) })) : null, assistant ? (_jsx(AssistantButton, { assistant: assistant, panelId: assistantPanelId, buttonRef: assistantButtonRef })) : null] }), progress ? (_jsx("span", { role: "status", className: "sr-only", children: "Chargement de la page\u2026" })) : null, _jsx("div", { "data-slot": "app-shell-loading", "aria-hidden": !loading, className: CONTENT_LOADING_LAYER, children: loading ? (_jsx("span", { role: "status", className: "sr-only", children: "Chargement\u2026" })) : null }), _jsxs("main", { id: "contenu", tabIndex: -1, "aria-busy": loading ? true : undefined, inert: loading || undefined, className: cn("mx-auto w-full max-w-6xl flex-1 px-4 pt-6 pb-16 outline-none sm:px-6 lg:px-10 lg:pt-8", mainClassName), children: [_jsx(ServiceStatus, { pageUrl: statusPageUrl, className: "mb-6" }), _jsx(ShellContentContext, { value: true, children: contentKey === undefined ? (children) : (_jsx(Suspense, { fallback: _jsx(PagePending, {}), children: children }, contentKey)) })] })] }) }), assistant && assistantMounted ? (_jsx(AssistantPanel, { assistant: assistant, panelId: assistantPanelId, wide: wide, openAtShellMount: assistantOpenAtMount, buttonRef: assistantButtonRef })) : null, hasRoot ? null : _jsx(FamilyLoaderHost, {}), _jsx(Drawer, { side: "bottom", open: menuOpen, onOpenChange: setMenuOpen, children: _jsxs(DrawerContent, { className: "lg:hidden", children: [_jsx(DrawerTitle, { className: "sr-only", children: "Menu" }), organization ? _jsx("div", { className: "px-3 pt-1", children: organization }) : null, navigation, _jsx("div", { className: "grid gap-0.5 border-t border-foreground/5 px-3 pt-2 pb-[max(env(safe-area-inset-bottom),0.75rem)] empty:hidden", children: footer })] }) })] }) }) }) }) }) }));
|
|
115
107
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Réglages de la lueur, choisis par Hugo le 2026-11-11 : épaisseur 47 px, houle 32 %, vitesse
|
|
3
|
+
* 68 %, pulsation 56 % sur 3 s.
|
|
4
|
+
*/
|
|
5
|
+
export declare const EDGE_GLOW: {
|
|
6
|
+
readonly width: 47;
|
|
7
|
+
readonly swell: 0.32;
|
|
8
|
+
readonly speed: 0.68;
|
|
9
|
+
readonly pulse: 0.56;
|
|
10
|
+
readonly pulsePeriodMs: 3000;
|
|
11
|
+
};
|
|
12
|
+
/** Temps des vagues (s) et phase de la pulsation (0 à 1) sur l'horloge : même image partout au même instant. */
|
|
13
|
+
export declare function edgeGlowClock(epochMs: number): {
|
|
14
|
+
time: number;
|
|
15
|
+
pulse: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* La lueur du chargement de page : une bande tricolore le long du bord de son parent (qui fixe la
|
|
19
|
+
* taille et l'arrondi, `overflow: hidden`). Elle pousse depuis le bord quand elle devient visible et
|
|
20
|
+
* s'y retire quand elle se cache ; l'animation ne tourne que pendant ce temps. Sans WebGL, avant
|
|
21
|
+
* l'hydratation, ou avec le mouvement réduit (une image fixe), le CSS dessine une lueur immobile
|
|
22
|
+
* (`[data-slot="edge-glow"]:not([data-live])`).
|
|
23
|
+
*/
|
|
24
|
+
export declare function EdgeGlow(): React.JSX.Element;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Réglages de la lueur, choisis par Hugo le 2026-11-11 : épaisseur 47 px, houle 32 %, vitesse
|
|
5
|
+
* 68 %, pulsation 56 % sur 3 s.
|
|
6
|
+
*/
|
|
7
|
+
export const EDGE_GLOW = { width: 47, swell: 0.32, speed: 0.68, pulse: 0.56, pulsePeriodMs: 3000 };
|
|
8
|
+
const VERTEX = "attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}";
|
|
9
|
+
/**
|
|
10
|
+
* Une seule bande le long du bord : opaque contre le bord, effacée vers l'intérieur. Son bord
|
|
11
|
+
* intérieur ondule (cinq vagues de vitesses différentes, qui ne se répètent pas), toute la bande
|
|
12
|
+
* respire (pulsation), les couleurs du H tournent. La profondeur est un minimum adouci des quatre
|
|
13
|
+
* distances aux bords : les coins intérieurs restent arrondis quelle que soit l'épaisseur.
|
|
14
|
+
*/
|
|
15
|
+
const FRAGMENT = `precision highp float;
|
|
16
|
+
uniform vec2 R;uniform float D,T,W,A,I,P,Q;
|
|
17
|
+
vec3 pal(float x){x=fract(x)*6.;vec3 b=vec3(.173,.471,.992),v=vec3(.451,.263,.996),k=vec3(.996,.322,.573),o=vec3(1.,.604,.275);
|
|
18
|
+
if(x<1.)return mix(b,v,x);if(x<2.)return mix(v,k,x-1.);if(x<3.)return mix(k,o,x-2.);if(x<4.)return mix(o,k,x-3.);if(x<5.)return mix(k,v,x-4.);return mix(v,b,x-5.);}
|
|
19
|
+
void main(){vec2 s=R/D;vec2 p=gl_FragCoord.xy/D;
|
|
20
|
+
float k=14.;float d=-k*log(exp(-p.x/k)+exp(-p.y/k)+exp(-(s.x-p.x)/k)+exp(-(s.y-p.y)/k));
|
|
21
|
+
vec2 c=p-s*.5;float a=atan(c.y/s.y,c.x/s.x);
|
|
22
|
+
float w=.55+A*(.22*sin(2.*a+1.1*T)+.16*sin(3.*a-1.7*T+1.3)+.12*sin(5.*a+2.3*T+.4)+.08*sin(8.*a-3.1*T+2.)+.05*sin(13.*a+4.2*T));
|
|
23
|
+
float br=.5-.5*cos(6.2832*Q);br=br*br*(3.-2.*br);
|
|
24
|
+
float th=max(W*w,W*.12)*I*(1.+P*(br*1.2-.4));
|
|
25
|
+
float f=pow(1.-smoothstep(0.,th,d),1.5);
|
|
26
|
+
float al=f*.92*min(I*1.5,1.)*(.88+.12*br);
|
|
27
|
+
gl_FragColor=vec4(pal(a/6.2832+T*.06)*al,al);}`;
|
|
28
|
+
/** Temps des vagues (s) et phase de la pulsation (0 à 1) sur l'horloge : même image partout au même instant. */
|
|
29
|
+
export function edgeGlowClock(epochMs) {
|
|
30
|
+
// Ramené sous 2π × 1000 : la précision des flottants du GPU reste fine, un saut toutes les 2,5 h.
|
|
31
|
+
const time = ((epochMs / 1000) * EDGE_GLOW.speed) % (Math.PI * 2000);
|
|
32
|
+
const pulse = (((epochMs % EDGE_GLOW.pulsePeriodMs) + EDGE_GLOW.pulsePeriodMs) % EDGE_GLOW.pulsePeriodMs) /
|
|
33
|
+
EDGE_GLOW.pulsePeriodMs;
|
|
34
|
+
return { time, pulse };
|
|
35
|
+
}
|
|
36
|
+
function createRenderer(canvas) {
|
|
37
|
+
const gl = canvas.getContext("webgl", { premultipliedAlpha: true, antialias: false, alpha: true });
|
|
38
|
+
if (!gl)
|
|
39
|
+
return null;
|
|
40
|
+
const program = gl.createProgram();
|
|
41
|
+
for (const [type, source] of [
|
|
42
|
+
[gl.VERTEX_SHADER, VERTEX],
|
|
43
|
+
[gl.FRAGMENT_SHADER, FRAGMENT],
|
|
44
|
+
]) {
|
|
45
|
+
const shader = gl.createShader(type);
|
|
46
|
+
if (!shader)
|
|
47
|
+
return null;
|
|
48
|
+
gl.shaderSource(shader, source);
|
|
49
|
+
gl.compileShader(shader);
|
|
50
|
+
gl.attachShader(program, shader);
|
|
51
|
+
}
|
|
52
|
+
gl.linkProgram(program);
|
|
53
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS))
|
|
54
|
+
return null;
|
|
55
|
+
gl.useProgram(program);
|
|
56
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
|
|
57
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
58
|
+
const position = gl.getAttribLocation(program, "p");
|
|
59
|
+
gl.enableVertexAttribArray(position);
|
|
60
|
+
gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);
|
|
61
|
+
const u = (name) => gl.getUniformLocation(program, name);
|
|
62
|
+
const [uR, uD, uT, uW, uA, uI, uP, uQ] = ["R", "D", "T", "W", "A", "I", "P", "Q"].map(u);
|
|
63
|
+
return {
|
|
64
|
+
draw(width, height, ratio, intensity, epochMs) {
|
|
65
|
+
const w = Math.round(width * ratio);
|
|
66
|
+
const h = Math.round(height * ratio);
|
|
67
|
+
if (canvas.width !== w || canvas.height !== h) {
|
|
68
|
+
canvas.width = w;
|
|
69
|
+
canvas.height = h;
|
|
70
|
+
gl.viewport(0, 0, w, h);
|
|
71
|
+
}
|
|
72
|
+
gl.clearColor(0, 0, 0, 0);
|
|
73
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
74
|
+
if (intensity < 0.003)
|
|
75
|
+
return;
|
|
76
|
+
const { time, pulse } = edgeGlowClock(epochMs);
|
|
77
|
+
gl.uniform2f(uR ?? null, w, h);
|
|
78
|
+
gl.uniform1f(uD ?? null, ratio);
|
|
79
|
+
gl.uniform1f(uT ?? null, time);
|
|
80
|
+
gl.uniform1f(uW ?? null, EDGE_GLOW.width);
|
|
81
|
+
gl.uniform1f(uA ?? null, EDGE_GLOW.swell);
|
|
82
|
+
gl.uniform1f(uI ?? null, intensity);
|
|
83
|
+
gl.uniform1f(uP ?? null, EDGE_GLOW.pulse);
|
|
84
|
+
gl.uniform1f(uQ ?? null, pulse);
|
|
85
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function isShown(element) {
|
|
90
|
+
return typeof element.checkVisibility === "function" ? element.checkVisibility({ visibilityProperty: true }) : true;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* La lueur du chargement de page : une bande tricolore le long du bord de son parent (qui fixe la
|
|
94
|
+
* taille et l'arrondi, `overflow: hidden`). Elle pousse depuis le bord quand elle devient visible et
|
|
95
|
+
* s'y retire quand elle se cache ; l'animation ne tourne que pendant ce temps. Sans WebGL, avant
|
|
96
|
+
* l'hydratation, ou avec le mouvement réduit (une image fixe), le CSS dessine une lueur immobile
|
|
97
|
+
* (`[data-slot="edge-glow"]:not([data-live])`).
|
|
98
|
+
*/
|
|
99
|
+
export function EdgeGlow() {
|
|
100
|
+
const ref = useRef(null);
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
const canvas = ref.current;
|
|
103
|
+
if (!canvas)
|
|
104
|
+
return;
|
|
105
|
+
const renderer = createRenderer(canvas);
|
|
106
|
+
if (!renderer)
|
|
107
|
+
return;
|
|
108
|
+
const still = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
109
|
+
canvas.dataset["live"] = "";
|
|
110
|
+
let frame = 0;
|
|
111
|
+
let intensity = 0;
|
|
112
|
+
let last = performance.now();
|
|
113
|
+
const tick = (now) => {
|
|
114
|
+
frame = 0;
|
|
115
|
+
const shown = isShown(canvas);
|
|
116
|
+
const dt = Math.min((now - last) / 1000, 0.05);
|
|
117
|
+
last = now;
|
|
118
|
+
const target = shown ? 1 : 0;
|
|
119
|
+
intensity = still.matches ? target : intensity + (target - intensity) * (1 - Math.exp(-dt * (shown ? 5 : 7)));
|
|
120
|
+
// Mouvement réduit : une image fixe, toujours la même.
|
|
121
|
+
const epoch = still.matches ? 0 : performance.timeOrigin + now;
|
|
122
|
+
const { width, height } = canvas.getBoundingClientRect();
|
|
123
|
+
renderer.draw(width, height, Math.min(window.devicePixelRatio || 1, 1.5), intensity, epoch);
|
|
124
|
+
if (still.matches || (!shown && intensity < 0.003))
|
|
125
|
+
return;
|
|
126
|
+
frame = requestAnimationFrame(tick);
|
|
127
|
+
};
|
|
128
|
+
const wake = (event) => {
|
|
129
|
+
if (event && !(event.target instanceof Element && event.target.contains(canvas)))
|
|
130
|
+
return;
|
|
131
|
+
if (frame)
|
|
132
|
+
return;
|
|
133
|
+
last = performance.now();
|
|
134
|
+
frame = requestAnimationFrame(tick);
|
|
135
|
+
};
|
|
136
|
+
wake();
|
|
137
|
+
// Le chargeur s'affiche par une transition CSS (`:has`) : elle réveille la boucle.
|
|
138
|
+
document.addEventListener("transitionrun", wake);
|
|
139
|
+
const onMotionChange = () => wake();
|
|
140
|
+
still.addEventListener("change", onMotionChange);
|
|
141
|
+
return () => {
|
|
142
|
+
cancelAnimationFrame(frame);
|
|
143
|
+
document.removeEventListener("transitionrun", wake);
|
|
144
|
+
still.removeEventListener("change", onMotionChange);
|
|
145
|
+
};
|
|
146
|
+
}, []);
|
|
147
|
+
return _jsx("canvas", { ref: ref, "data-slot": "edge-glow", "aria-hidden": true });
|
|
148
|
+
}
|
package/dist/family-loader.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare function familyLoaderPhase(epochMs: number, period?: number): num
|
|
|
16
16
|
/** Cale toutes les animations du H sous `root` sur l'horloge (voir `familyLoaderStartTime`). */
|
|
17
17
|
export declare function syncFamilyLoader(root: Element | null): void;
|
|
18
18
|
/**
|
|
19
|
-
* Vrai sous `FamilyLoaderRoot` : le document a son chargeur, `PagePending` et la coque s'y
|
|
19
|
+
* Vrai sous `FamilyLoaderRoot` : le document a son chargeur, `PagePending` (hors de la coque) et la coque s'y
|
|
20
20
|
* signalent au lieu de dessiner leur propre H.
|
|
21
21
|
*/
|
|
22
22
|
export declare const FamilyLoaderRootContext: React.Context<boolean>;
|
|
@@ -26,22 +26,23 @@ export declare const FamilyLoaderRootContext: React.Context<boolean>;
|
|
|
26
26
|
* H complet, fondu), calée sur l'horloge. CSS seul (`styles.css`), `transform` et `opacity`
|
|
27
27
|
* seulement. Mouvement réduit : le H plein, immobile.
|
|
28
28
|
*
|
|
29
|
-
* Un chargement de page ne
|
|
30
|
-
* document (`FamilyLoaderRoot`)
|
|
29
|
+
* Un chargement de page ne montre pas ce H : c'est `PagePending`, qui passe par la lueur du
|
|
30
|
+
* document (`FamilyLoaderRoot`, `EdgeGlow`). Ce composant sert au H en
|
|
31
31
|
* ligne (une carte, un panneau) ; dans un bouton ou une ligne, `Spinner` reste la règle.
|
|
32
32
|
*/
|
|
33
33
|
export declare function FamilyLoader({ size, label, page, className, }: {
|
|
34
34
|
/** `sm` 24 px, `md` 40 px, `lg` 64 px. Par défaut `lg` en page, `md` sinon. */
|
|
35
35
|
size?: keyof typeof SIZES;
|
|
36
36
|
label?: string;
|
|
37
|
-
/** Chargement de toute la page : passe par
|
|
37
|
+
/** Chargement de toute la page : passe par la lueur du document, comme `PagePending`. */
|
|
38
38
|
page?: boolean;
|
|
39
39
|
className?: string;
|
|
40
40
|
}): React.JSX.Element;
|
|
41
41
|
/**
|
|
42
|
-
* Le
|
|
43
|
-
* coque), toujours
|
|
44
|
-
* `FamilyLoaderRoot`, ou, à défaut, par la coque ou par
|
|
42
|
+
* Le chargeur du document : la lueur du bord (`EdgeGlow`), en position fixe sur la carte du contenu
|
|
43
|
+
* de la coque (tout l'écran sur téléphone), toujours montée, que le CSS montre quand une page attend
|
|
44
|
+
* ou que la coque charge. Monté par `FamilyLoaderRoot`, ou, à défaut, par la coque ou par
|
|
45
|
+
* `PagePending` eux-mêmes.
|
|
45
46
|
*/
|
|
46
47
|
export declare function FamilyLoaderHost(): React.JSX.Element;
|
|
47
48
|
/**
|
|
@@ -54,14 +55,14 @@ export declare function FamilyLoaderHost(): React.JSX.Element;
|
|
|
54
55
|
* </body>
|
|
55
56
|
* ```
|
|
56
57
|
*
|
|
57
|
-
* Il rend
|
|
58
|
+
* Il rend la lueur dans le HTML du serveur (repli CSS avant l'hydratation), à la même place et à la même phase
|
|
58
59
|
* que dans la coque et que dans le produit qu'on vient de quitter (ADR 0015).
|
|
59
60
|
*/
|
|
60
61
|
export declare function FamilyLoaderRoot({ children }: {
|
|
61
62
|
children: React.ReactNode;
|
|
62
63
|
}): React.JSX.Element;
|
|
63
64
|
/**
|
|
64
|
-
* La place d'une page qui attend, hors de la coque : sa seule présence montre
|
|
65
|
+
* La place d'une page qui attend, hors de la coque : sa seule présence montre la lueur du document
|
|
65
66
|
* (CSS `:has`), dès le rendu serveur. Sans `FamilyLoaderRoot`, elle monte son propre H.
|
|
66
67
|
*/
|
|
67
68
|
export declare function PageLoadingPlaceholder({ label, hasRoot, className, }: {
|
package/dist/family-loader.js
CHANGED
|
@@ -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 { EdgeGlow } from "./edge-glow.js";
|
|
4
5
|
import { useBackgroundOrganizationRefresh } from "./product-switch.js";
|
|
5
6
|
import { ShellFrame } from "./shell-frame.js";
|
|
6
7
|
const SIZES = { sm: 24, md: 40, lg: 64 };
|
|
@@ -35,12 +36,6 @@ export function syncFamilyLoader(root) {
|
|
|
35
36
|
animation.startTime = start;
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Même calage, en ligne dans le HTML rendu par le serveur, juste après l'élément : il s'exécute
|
|
40
|
-
* avant l'hydratation, une fois la feuille de style chargée (un script attend les feuilles de
|
|
41
|
-
* style qui le précèdent). Une animation recréée (mouvement réduit basculé) se recale aussi.
|
|
42
|
-
*/
|
|
43
|
-
const SYNC_SCRIPT = `(function(e){if(!e||!e.getAnimations)return;var p=${FAMILY_LOADER_PERIOD_MS};function s(){var t=-(((performance.timeOrigin%p)+p)%p);e.getAnimations({subtree:true}).forEach(function(a){if(String(a.animationName).indexOf("family-loader")===0&&a.startTime!==t)a.startTime=t})}s();e.addEventListener("animationstart",s)})(document.currentScript&&document.currentScript.previousElementSibling)`;
|
|
44
39
|
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
45
40
|
/** Cale le H sur l'horloge au montage et à chaque animation recréée. */
|
|
46
41
|
function useClockSync(ref) {
|
|
@@ -60,7 +55,7 @@ function Mark() {
|
|
|
60
55
|
return (_jsx(_Fragment, { children: PIECES.map((piece) => (_jsx("span", { "data-piece": piece, "aria-hidden": true, children: _jsx("span", { "data-fill": true }) }, piece))) }));
|
|
61
56
|
}
|
|
62
57
|
/**
|
|
63
|
-
* Vrai sous `FamilyLoaderRoot` : le document a son chargeur, `PagePending` et la coque s'y
|
|
58
|
+
* Vrai sous `FamilyLoaderRoot` : le document a son chargeur, `PagePending` (hors de la coque) et la coque s'y
|
|
64
59
|
* signalent au lieu de dessiner leur propre H.
|
|
65
60
|
*/
|
|
66
61
|
export const FamilyLoaderRootContext = createContext(false);
|
|
@@ -70,8 +65,8 @@ export const FamilyLoaderRootContext = createContext(false);
|
|
|
70
65
|
* H complet, fondu), calée sur l'horloge. CSS seul (`styles.css`), `transform` et `opacity`
|
|
71
66
|
* seulement. Mouvement réduit : le H plein, immobile.
|
|
72
67
|
*
|
|
73
|
-
* Un chargement de page ne
|
|
74
|
-
* document (`FamilyLoaderRoot`)
|
|
68
|
+
* Un chargement de page ne montre pas ce H : c'est `PagePending`, qui passe par la lueur du
|
|
69
|
+
* document (`FamilyLoaderRoot`, `EdgeGlow`). Ce composant sert au H en
|
|
75
70
|
* ligne (une carte, un panneau) ; dans un bouton ou une ligne, `Spinner` reste la règle.
|
|
76
71
|
*/
|
|
77
72
|
export function FamilyLoader({ size, label = "Chargement…", page = false, className, }) {
|
|
@@ -86,14 +81,13 @@ function FamilyMark({ px, label, className, }) {
|
|
|
86
81
|
return (_jsx("span", { ref: ref, ...(label ? { role: "status", "aria-label": label } : { "aria-hidden": true }), "data-slot": "family-loader", className: cn("family-loader", className), style: { ["--family-loader-size"]: `${px}px` }, children: _jsx(Mark, {}) }));
|
|
87
82
|
}
|
|
88
83
|
/**
|
|
89
|
-
* Le
|
|
90
|
-
* coque), toujours
|
|
91
|
-
* `FamilyLoaderRoot`, ou, à défaut, par la coque ou par
|
|
84
|
+
* Le chargeur du document : la lueur du bord (`EdgeGlow`), en position fixe sur la carte du contenu
|
|
85
|
+
* de la coque (tout l'écran sur téléphone), toujours montée, que le CSS montre quand une page attend
|
|
86
|
+
* ou que la coque charge. Monté par `FamilyLoaderRoot`, ou, à défaut, par la coque ou par
|
|
87
|
+
* `PagePending` eux-mêmes.
|
|
92
88
|
*/
|
|
93
89
|
export function FamilyLoaderHost() {
|
|
94
|
-
|
|
95
|
-
useClockSync(ref);
|
|
96
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: ref, "data-slot": "family-loader-root", "aria-hidden": true, children: [_jsx("div", { "data-frame": true, children: _jsx(ShellFrame, {}) }), _jsx("div", { "data-stage": true, children: _jsx("span", { "data-slot": "family-loader", className: "family-loader", style: { ["--family-loader-size"]: "64px" }, children: _jsx(Mark, {}) }) })] }), _jsx("script", { dangerouslySetInnerHTML: { __html: SYNC_SCRIPT } })] }));
|
|
90
|
+
return (_jsxs("div", { "data-slot": "family-loader-root", "aria-hidden": true, children: [_jsx("div", { "data-frame": true, children: _jsx(ShellFrame, {}) }), _jsx("div", { "data-glow": true, children: _jsx(EdgeGlow, {}) })] }));
|
|
97
91
|
}
|
|
98
92
|
/**
|
|
99
93
|
* Le chargeur du document, à poser une fois dans le `body` de la route racine, autour de l'app :
|
|
@@ -105,7 +99,7 @@ export function FamilyLoaderHost() {
|
|
|
105
99
|
* </body>
|
|
106
100
|
* ```
|
|
107
101
|
*
|
|
108
|
-
* Il rend
|
|
102
|
+
* Il rend la lueur dans le HTML du serveur (repli CSS avant l'hydratation), à la même place et à la même phase
|
|
109
103
|
* que dans la coque et que dans le produit qu'on vient de quitter (ADR 0015).
|
|
110
104
|
*/
|
|
111
105
|
export function FamilyLoaderRoot({ children }) {
|
|
@@ -114,7 +108,7 @@ export function FamilyLoaderRoot({ children }) {
|
|
|
114
108
|
return (_jsxs(FamilyLoaderRootContext, { value: true, children: [children, _jsx(FamilyLoaderHost, {})] }));
|
|
115
109
|
}
|
|
116
110
|
/**
|
|
117
|
-
* La place d'une page qui attend, hors de la coque : sa seule présence montre
|
|
111
|
+
* La place d'une page qui attend, hors de la coque : sa seule présence montre la lueur du document
|
|
118
112
|
* (CSS `:has`), dès le rendu serveur. Sans `FamilyLoaderRoot`, elle monte son propre H.
|
|
119
113
|
*/
|
|
120
114
|
export function PageLoadingPlaceholder({ label, hasRoot, className, }) {
|
package/dist/module-bar.js
CHANGED
|
@@ -17,6 +17,7 @@ import { ProductBadge } from "./product-badge.js";
|
|
|
17
17
|
import { PRODUCT_PRERENDER_ATTRIBUTE } from "./product-switch.js";
|
|
18
18
|
import { shellProductEntries } from "./products.js";
|
|
19
19
|
import { familyProductName } from "./product-name.js";
|
|
20
|
+
import { lastOrganization } from "./shell-memory.js";
|
|
20
21
|
import { useLeaveOnClick } from "./shell-placement.js";
|
|
21
22
|
import { SIDEBAR_ITEM, SidebarNavBack } from "./sidebar-nav.js";
|
|
22
23
|
import { useSidebarRail } from "./sidebar-rail.js";
|
|
@@ -26,6 +27,10 @@ const noSubscription = () => () => { };
|
|
|
26
27
|
function usePageOrigin() {
|
|
27
28
|
return useSyncExternalStore(noSubscription, () => window.location.origin, () => undefined);
|
|
28
29
|
}
|
|
30
|
+
/** Dernière organisation ouverte après l'hydratation, `null` au rendu serveur (lien vers l'accueil d'abord). */
|
|
31
|
+
function useLastOrganization() {
|
|
32
|
+
return useSyncExternalStore(noSubscription, lastOrganization, () => null);
|
|
33
|
+
}
|
|
29
34
|
const GROUP_LABEL = "px-3 pt-3 pb-1.5 text-xs font-medium text-muted-foreground lg:text-shell-muted lg:rail:sr-only";
|
|
30
35
|
function ShellEntryLink({ entry, icon, onOpenCurrent, }) {
|
|
31
36
|
const rail = useSidebarRail();
|
|
@@ -83,8 +88,9 @@ function ModuleBarEditor({ order, onSave, onCancel, }) {
|
|
|
83
88
|
*/
|
|
84
89
|
export function ShellNavigation({ product, nav, accountHref, level, onLevelChange, projectModules, }) {
|
|
85
90
|
const pageOrigin = usePageOrigin();
|
|
91
|
+
const organization = useLastOrganization();
|
|
86
92
|
const order = useModuleBar();
|
|
87
|
-
const { products, account } = shellProductEntries({ current: product, accountHref, pageOrigin });
|
|
93
|
+
const { products, account } = shellProductEntries({ current: product, accountHref, pageOrigin, organization });
|
|
88
94
|
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: shownModules(order, product).filter((word) => !projectModules || word === product || projectModules.includes(word)), onOpenCurrent: () => onLevelChange("tabs"), onCustomize: () => onLevelChange("edit") })) : level === "edit" ? (_jsx(ModuleBarEditor, { order: order, onSave: (next) => {
|
|
89
95
|
saveModuleBar(next);
|
|
90
96
|
void pushModuleBar(next);
|
package/dist/page-pending.d.ts
CHANGED
|
@@ -1,41 +1,38 @@
|
|
|
1
|
+
import { type PageSkeletonVariant } from "./page-skeleton.tsx";
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
3
|
+
* Attente avant de remplacer l'ancienne page par le squelette de la nouvelle (ADR 0025). En dessous,
|
|
4
|
+
* l'ancienne page reste à l'écran avec la barre de progression : une navigation courte ne fait rien
|
|
5
|
+
* clignoter. Valeur par défaut de TanStack Router, et seuil d'une seconde de Nielsen.
|
|
4
6
|
*/
|
|
5
|
-
export declare const PAGE_PENDING_MS =
|
|
6
|
-
/** Durée minimale d'affichage une fois apparu :
|
|
7
|
-
export declare const PAGE_PENDING_MIN_MS =
|
|
7
|
+
export declare const PAGE_PENDING_MS = 1000;
|
|
8
|
+
/** Durée minimale d'affichage du squelette une fois apparu : il ne clignote pas (défaut de TanStack Router). */
|
|
9
|
+
export declare const PAGE_PENDING_MIN_MS = 500;
|
|
10
|
+
/** Attente avant la barre de progression d'une navigation : une page préchargée s'ouvre sans elle. */
|
|
11
|
+
export declare const NAVIGATION_PROGRESS_MS = 150;
|
|
12
|
+
/** Durée minimale de la barre de progression une fois apparue. */
|
|
13
|
+
export declare const NAVIGATION_PROGRESS_MIN_MS = 300;
|
|
8
14
|
/**
|
|
9
|
-
* Calque de
|
|
10
|
-
*
|
|
11
|
-
* (`FamilyLoaderRoot`), en position fixe, au même pixel dans tous les états.
|
|
15
|
+
* Calque de la zone de contenu, sous l'en-tête de la coque (56 px), opaque : il cache la page
|
|
16
|
+
* pendant le départ vers un autre module, la lueur du document par-dessus (ADR 0015, 0020).
|
|
12
17
|
*/
|
|
13
18
|
export declare const CONTENT_LOADING_LAYER = "absolute inset-x-0 top-14 bottom-0 z-20 bg-background";
|
|
19
|
+
/** Vrai dans la zone de contenu d'`AppShell` : une page qui attend y montre son squelette, pas la lueur. */
|
|
20
|
+
export declare const ShellContentContext: React.Context<boolean>;
|
|
14
21
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Fourni par `AppShell` autour de la zone de contenu : `PagePending` s'y signale, la coque couvre
|
|
23
|
-
* alors la zone de contenu et montre le H du document.
|
|
24
|
-
*/
|
|
25
|
-
export declare const ContentLoadingContext: React.Context<(() => () => void) | null>;
|
|
26
|
-
/**
|
|
27
|
-
* Le chargement d'une page : le H de la famille, au centre de la zone de contenu de la coque, au
|
|
28
|
-
* même pixel et à la même phase dans tous les états (calque de navigation de la coque, attente
|
|
29
|
-
* d'une route, frontière `Suspense`, rendu serveur, page d'entrée, produit d'à côté). Ce composant
|
|
30
|
-
* ne dessine pas de H : il marque la place de la page (`data-slot="page-pending"`) et le H du
|
|
31
|
-
* document (`FamilyLoaderRoot`) s'affiche, dès le HTML du serveur. Dans la coque, la coque s'en
|
|
32
|
-
* charge ; hors de la coque, le cadre de la coque est dessiné autour du H.
|
|
22
|
+
* Le chargement d'une page.
|
|
23
|
+
*
|
|
24
|
+
* - **Dans la coque** (`AppShell`) : le squelette de la page (`PageSkeleton`), la coque restant en
|
|
25
|
+
* place. Jamais la lueur : elle est réservée au document qui n'a pas encore de coque (ADR 0025).
|
|
26
|
+
* - **Hors de la coque** (page d'entrée, rendu serveur sans coque) : la lueur du document
|
|
27
|
+
* (`FamilyLoaderRoot`), au même pixel que dans la coque (ADR 0015).
|
|
33
28
|
*
|
|
34
|
-
* Composant de `pendingComponent` (
|
|
35
|
-
* `Suspense` de la coque
|
|
29
|
+
* Composant par défaut de `pendingComponent` (`routerPendingDefaults`) et repli de la frontière
|
|
30
|
+
* `Suspense` de la coque. Une route dont la page n'est pas une liste passe sa forme :
|
|
31
|
+
* `pendingComponent: () => <PagePending variant="dashboard" />`.
|
|
36
32
|
*/
|
|
37
|
-
export declare function PagePending({ label }: {
|
|
33
|
+
export declare function PagePending({ label, variant, }: {
|
|
38
34
|
label?: string;
|
|
35
|
+
variant?: PageSkeletonVariant;
|
|
39
36
|
}): React.JSX.Element;
|
|
40
37
|
/**
|
|
41
38
|
* Vrai après `delay` ms de `active`, et le reste au moins `min` ms une fois vrai : un état bref ne
|
|
@@ -43,17 +40,20 @@ export declare function PagePending({ label }: {
|
|
|
43
40
|
*/
|
|
44
41
|
export declare function useDelayedFlag(active: boolean, delay: number, min: number): boolean;
|
|
45
42
|
/**
|
|
46
|
-
* Réglages communs de `createRouter` (TanStack Router) :
|
|
43
|
+
* Réglages communs de `createRouter` (TanStack Router), ADR 0025 :
|
|
47
44
|
* - préchargement au survol ou au focus d'un lien (`intent`) : la plupart des pages s'ouvrent sans
|
|
48
|
-
* attente ; `defaultPreloadStaleTime: 0` laisse TanStack Query décider de la fraîcheur
|
|
49
|
-
*
|
|
45
|
+
* attente ; `defaultPreloadStaleTime: 0` laisse TanStack Query décider de la fraîcheur (doc de
|
|
46
|
+
* TanStack Router, « External data loading ») ;
|
|
47
|
+
* - pendant une navigation, l'ancienne page reste avec la barre de progression de la coque ; si la
|
|
48
|
+
* nouvelle attend encore ses données après 1 s, son squelette (`PagePending`) la remplace, au
|
|
49
|
+
* moins 500 ms.
|
|
50
50
|
*
|
|
51
51
|
* ```ts
|
|
52
52
|
* createRouter({ routeTree, ...routerPendingDefaults })
|
|
53
53
|
* ```
|
|
54
54
|
*
|
|
55
|
-
* La coque doit vivre dans une route de mise en page (`/o/$org`)
|
|
56
|
-
* ne doit pas appeler le serveur à chaque navigation
|
|
55
|
+
* La coque doit vivre dans une route de mise en page (`/o/$org`), rendue avant les données des
|
|
56
|
+
* pages, et le `beforeLoad` de la racine ne doit pas appeler le serveur à chaque navigation.
|
|
57
57
|
*/
|
|
58
58
|
export declare const routerPendingDefaults: {
|
|
59
59
|
defaultPendingComponent: typeof PagePending;
|
package/dist/page-pending.js
CHANGED
|
@@ -1,51 +1,45 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useContext, useEffect,
|
|
2
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
3
3
|
import { FamilyLoaderRootContext, PageLoadingPlaceholder } from "./family-loader.js";
|
|
4
|
+
import { PageSkeleton } from "./page-skeleton.js";
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* Attente avant de remplacer l'ancienne page par le squelette de la nouvelle (ADR 0025). En dessous,
|
|
7
|
+
* l'ancienne page reste à l'écran avec la barre de progression : une navigation courte ne fait rien
|
|
8
|
+
* clignoter. Valeur par défaut de TanStack Router, et seuil d'une seconde de Nielsen.
|
|
7
9
|
*/
|
|
8
|
-
export const PAGE_PENDING_MS =
|
|
9
|
-
/** Durée minimale d'affichage une fois apparu :
|
|
10
|
-
export const PAGE_PENDING_MIN_MS =
|
|
10
|
+
export const PAGE_PENDING_MS = 1000;
|
|
11
|
+
/** Durée minimale d'affichage du squelette une fois apparu : il ne clignote pas (défaut de TanStack Router). */
|
|
12
|
+
export const PAGE_PENDING_MIN_MS = 500;
|
|
13
|
+
/** Attente avant la barre de progression d'une navigation : une page préchargée s'ouvre sans elle. */
|
|
14
|
+
export const NAVIGATION_PROGRESS_MS = 150;
|
|
15
|
+
/** Durée minimale de la barre de progression une fois apparue. */
|
|
16
|
+
export const NAVIGATION_PROGRESS_MIN_MS = 300;
|
|
11
17
|
/**
|
|
12
|
-
* Calque de
|
|
13
|
-
*
|
|
14
|
-
* (`FamilyLoaderRoot`), en position fixe, au même pixel dans tous les états.
|
|
18
|
+
* Calque de la zone de contenu, sous l'en-tête de la coque (56 px), opaque : il cache la page
|
|
19
|
+
* pendant le départ vers un autre module, la lueur du document par-dessus (ADR 0015, 0020).
|
|
15
20
|
*/
|
|
16
21
|
export const CONTENT_LOADING_LAYER = "absolute inset-x-0 top-14 bottom-0 z-20 bg-background";
|
|
22
|
+
/** Vrai dans la zone de contenu d'`AppShell` : une page qui attend y montre son squelette, pas la lueur. */
|
|
23
|
+
export const ShellContentContext = createContext(false);
|
|
17
24
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Fourni par `AppShell` autour de la zone de contenu : `PagePending` s'y signale, la coque couvre
|
|
26
|
-
* alors la zone de contenu et montre le H du document.
|
|
27
|
-
*/
|
|
28
|
-
export const ContentLoadingContext = createContext(null);
|
|
29
|
-
/**
|
|
30
|
-
* Le chargement d'une page : le H de la famille, au centre de la zone de contenu de la coque, au
|
|
31
|
-
* même pixel et à la même phase dans tous les états (calque de navigation de la coque, attente
|
|
32
|
-
* d'une route, frontière `Suspense`, rendu serveur, page d'entrée, produit d'à côté). Ce composant
|
|
33
|
-
* ne dessine pas de H : il marque la place de la page (`data-slot="page-pending"`) et le H du
|
|
34
|
-
* document (`FamilyLoaderRoot`) s'affiche, dès le HTML du serveur. Dans la coque, la coque s'en
|
|
35
|
-
* charge ; hors de la coque, le cadre de la coque est dessiné autour du H.
|
|
25
|
+
* Le chargement d'une page.
|
|
26
|
+
*
|
|
27
|
+
* - **Dans la coque** (`AppShell`) : le squelette de la page (`PageSkeleton`), la coque restant en
|
|
28
|
+
* place. Jamais la lueur : elle est réservée au document qui n'a pas encore de coque (ADR 0025).
|
|
29
|
+
* - **Hors de la coque** (page d'entrée, rendu serveur sans coque) : la lueur du document
|
|
30
|
+
* (`FamilyLoaderRoot`), au même pixel que dans la coque (ADR 0015).
|
|
36
31
|
*
|
|
37
|
-
* Composant de `pendingComponent` (
|
|
38
|
-
* `Suspense` de la coque
|
|
32
|
+
* Composant par défaut de `pendingComponent` (`routerPendingDefaults`) et repli de la frontière
|
|
33
|
+
* `Suspense` de la coque. Une route dont la page n'est pas une liste passe sa forme :
|
|
34
|
+
* `pendingComponent: () => <PagePending variant="dashboard" />`.
|
|
39
35
|
*/
|
|
40
|
-
export function PagePending({ label = "Chargement…" }) {
|
|
41
|
-
const
|
|
36
|
+
export function PagePending({ label = "Chargement…", variant, }) {
|
|
37
|
+
const inShell = useContext(ShellContentContext);
|
|
42
38
|
const hasRoot = useContext(FamilyLoaderRootContext);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return (_jsx("div", { "data-slot": "page-pending", role: "status", "aria-label": label, className: "min-h-[calc(100dvh-10rem)]" }));
|
|
39
|
+
if (inShell)
|
|
40
|
+
return _jsx(PageSkeleton, { label: label, ...(variant ? { variant } : {}) });
|
|
41
|
+
return _jsx(PageLoadingPlaceholder, { label: label, hasRoot: hasRoot });
|
|
47
42
|
}
|
|
48
|
-
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
49
43
|
/**
|
|
50
44
|
* Vrai après `delay` ms de `active`, et le reste au moins `min` ms une fois vrai : un état bref ne
|
|
51
45
|
* montre rien, un état montré ne clignote pas.
|
|
@@ -70,17 +64,20 @@ export function useDelayedFlag(active, delay, min) {
|
|
|
70
64
|
return visible;
|
|
71
65
|
}
|
|
72
66
|
/**
|
|
73
|
-
* Réglages communs de `createRouter` (TanStack Router) :
|
|
67
|
+
* Réglages communs de `createRouter` (TanStack Router), ADR 0025 :
|
|
74
68
|
* - préchargement au survol ou au focus d'un lien (`intent`) : la plupart des pages s'ouvrent sans
|
|
75
|
-
* attente ; `defaultPreloadStaleTime: 0` laisse TanStack Query décider de la fraîcheur
|
|
76
|
-
*
|
|
69
|
+
* attente ; `defaultPreloadStaleTime: 0` laisse TanStack Query décider de la fraîcheur (doc de
|
|
70
|
+
* TanStack Router, « External data loading ») ;
|
|
71
|
+
* - pendant une navigation, l'ancienne page reste avec la barre de progression de la coque ; si la
|
|
72
|
+
* nouvelle attend encore ses données après 1 s, son squelette (`PagePending`) la remplace, au
|
|
73
|
+
* moins 500 ms.
|
|
77
74
|
*
|
|
78
75
|
* ```ts
|
|
79
76
|
* createRouter({ routeTree, ...routerPendingDefaults })
|
|
80
77
|
* ```
|
|
81
78
|
*
|
|
82
|
-
* La coque doit vivre dans une route de mise en page (`/o/$org`)
|
|
83
|
-
* ne doit pas appeler le serveur à chaque navigation
|
|
79
|
+
* La coque doit vivre dans une route de mise en page (`/o/$org`), rendue avant les données des
|
|
80
|
+
* pages, et le `beforeLoad` de la racine ne doit pas appeler le serveur à chaque navigation.
|
|
84
81
|
*/
|
|
85
82
|
export const routerPendingDefaults = {
|
|
86
83
|
defaultPendingComponent: PagePending,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* La forme d'une page qui charge, dans la coque. Elle reprend la mise en page de `Page`
|
|
3
|
+
* (`@hgdc/ui/page-header`) : en-tête de la même hauteur, même colonne, même espacement, pour que
|
|
4
|
+
* rien ne saute quand la page arrive (ADR 0025).
|
|
5
|
+
*
|
|
6
|
+
* - `list` : un tableau ou une liste (par défaut) ;
|
|
7
|
+
* - `dashboard` : des chiffres en tête, un graphique, une liste ;
|
|
8
|
+
* - `form` : des réglages ou une fiche, dans la colonne de lecture.
|
|
9
|
+
*/
|
|
10
|
+
export type PageSkeletonVariant = "list" | "dashboard" | "form";
|
|
11
|
+
export declare function PageSkeleton({ variant, label, className, }: {
|
|
12
|
+
variant?: PageSkeletonVariant;
|
|
13
|
+
/** Annoncé aux lecteurs d'écran. */
|
|
14
|
+
label?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "./cn.js";
|
|
3
|
+
import { Skeleton } from "./skeleton.js";
|
|
4
|
+
export function PageSkeleton({ variant = "list", label = "Chargement…", className, }) {
|
|
5
|
+
return (_jsxs("div", { "data-slot": "page-skeleton", "data-variant": variant, role: "status", "aria-label": label, className: cn("w-full", variant === "form" && "max-w-3xl", className), children: [_jsx("div", { "data-slot": "page-header", className: "mb-8 grid gap-3", children: _jsxs("div", { className: "min-w-0", children: [_jsx(Skeleton, { className: "h-9 w-56 max-w-full rounded-lg" }), _jsx(Skeleton, { className: "mt-2 h-4 w-80 max-w-full rounded-md" })] }) }), _jsx("div", { className: "grid gap-6", children: BODIES[variant] })] }));
|
|
6
|
+
}
|
|
7
|
+
function Rows({ count }) {
|
|
8
|
+
return (_jsx("div", { className: "overflow-hidden rounded-2xl border border-foreground/5", children: Array.from({ length: count }, (_, index) => (_jsxs("div", { className: "flex items-center gap-4 border-b border-foreground/5 px-4 py-3 last:border-b-0", children: [_jsx(Skeleton, { className: "size-8 shrink-0 rounded-lg" }), _jsxs("div", { className: "grid min-w-0 flex-1 gap-1.5", children: [_jsx(Skeleton, { className: "h-4 w-2/5 rounded-md" }), _jsx(Skeleton, { className: "h-3 w-1/4 rounded-md" })] }), _jsx(Skeleton, { className: "hidden h-6 w-16 rounded-full sm:block" })] }, index))) }));
|
|
9
|
+
}
|
|
10
|
+
const BODIES = {
|
|
11
|
+
list: (_jsxs(_Fragment, { children: [_jsx(Skeleton, { className: "h-10 w-full max-w-md" }), _jsx(Rows, { count: 6 })] })),
|
|
12
|
+
dashboard: (_jsxs(_Fragment, { children: [_jsx("div", { className: "grid grid-cols-2 gap-4 lg:grid-cols-4", children: Array.from({ length: 4 }, (_, index) => (_jsx(Skeleton, { className: "h-24 rounded-2xl" }, index))) }), _jsx(Skeleton, { className: "h-64 rounded-2xl" }), _jsx(Rows, { count: 4 })] })),
|
|
13
|
+
form: (_jsx(_Fragment, { children: Array.from({ length: 3 }, (_, index) => (_jsxs("div", { className: "grid gap-4 rounded-2xl border border-foreground/5 p-6", children: [_jsx(Skeleton, { className: "h-5 w-40 rounded-md" }), _jsx(Skeleton, { className: "h-10 w-full" }), _jsx(Skeleton, { className: "h-10 w-2/3" })] }, index))) })),
|
|
14
|
+
};
|
package/dist/product-switch.d.ts
CHANGED
|
@@ -4,6 +4,15 @@ export declare const ORG_SYNC_COOKIE = "hgdc-org-sync";
|
|
|
4
4
|
export declare const BACKGROUND_REFRESH_PATH = "/auth/hgdc?refresh=1&frame=1";
|
|
5
5
|
/** Abandon du cadre s'il ne s'est pas chargé : Account injoignable, réseau coupé. */
|
|
6
6
|
export declare const BACKGROUND_REFRESH_TIMEOUT_MS = 20000;
|
|
7
|
+
/**
|
|
8
|
+
* Une relecture au plus par produit pendant ce délai. Elle refait une connexion complète chez ID
|
|
9
|
+
* (environ 3 s côté serveur, pendant le chargement de la page) : la refaire à chaque passage d'un
|
|
10
|
+
* produit à l'autre ralentissait la navigation (2026-11-11). Les changements d'appartenance arrivent
|
|
11
|
+
* déjà en direct par les événements d'Account ; la relecture n'est qu'un filet.
|
|
12
|
+
*/
|
|
13
|
+
export declare const BACKGROUND_REFRESH_INTERVAL_MS: number;
|
|
14
|
+
/** Heure de la dernière relecture d'un produit, en `localStorage` : `hgdc-org-sync:<base>` (`/` à la racine). */
|
|
15
|
+
export declare const ORG_SYNC_LAST_KEY_PREFIX = "hgdc-org-sync:";
|
|
7
16
|
/** Marque d'un lien de la barre vers un autre produit servi sous la même origine : prérendu au survol. */
|
|
8
17
|
export declare const PRODUCT_PRERENDER_ATTRIBUTE = "data-hgdc-prerender";
|
|
9
18
|
/**
|
|
@@ -30,12 +39,13 @@ export declare function takeOrgSync(doc?: Document): string | null;
|
|
|
30
39
|
/** Lit et efface le cookie de relecture. Vrai s'il était posé. */
|
|
31
40
|
export declare function takeOrgSyncCookie(doc?: Document): boolean;
|
|
32
41
|
/**
|
|
33
|
-
* Relit les organisations dans un cadre caché, une fois, si l'entrée l'a
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
42
|
+
* Relit les organisations dans un cadre caché, une fois, si l'entrée l'a demandé et que la dernière
|
|
43
|
+
* relecture de ce produit date de plus de `BACKGROUND_REFRESH_INTERVAL_MS`. Le cadre n'a ni script ni
|
|
44
|
+
* formulaire (`sandbox`), il suit seulement les redirections d'Account ; la nouvelle session du
|
|
45
|
+
* produit recopie les organisations, et les requêtes Convex abonnées se mettent à jour. Si la session
|
|
46
|
+
* d'Account a expiré, Account refuse d'être affiché dans un cadre : rien ne se passe, la session du
|
|
47
|
+
* produit reste celle d'avant. Une page prérendue au survol attend d'être affichée : un survol sans
|
|
48
|
+
* clic ne relit rien.
|
|
39
49
|
*/
|
|
40
50
|
export declare function startBackgroundRefresh(doc?: Document): HTMLIFrameElement | null;
|
|
41
51
|
/**
|
package/dist/product-switch.js
CHANGED
|
@@ -15,6 +15,15 @@ export const ORG_SYNC_COOKIE = "hgdc-org-sync";
|
|
|
15
15
|
export const BACKGROUND_REFRESH_PATH = "/auth/hgdc?refresh=1&frame=1";
|
|
16
16
|
/** Abandon du cadre s'il ne s'est pas chargé : Account injoignable, réseau coupé. */
|
|
17
17
|
export const BACKGROUND_REFRESH_TIMEOUT_MS = 20_000;
|
|
18
|
+
/**
|
|
19
|
+
* Une relecture au plus par produit pendant ce délai. Elle refait une connexion complète chez ID
|
|
20
|
+
* (environ 3 s côté serveur, pendant le chargement de la page) : la refaire à chaque passage d'un
|
|
21
|
+
* produit à l'autre ralentissait la navigation (2026-11-11). Les changements d'appartenance arrivent
|
|
22
|
+
* déjà en direct par les événements d'Account ; la relecture n'est qu'un filet.
|
|
23
|
+
*/
|
|
24
|
+
export const BACKGROUND_REFRESH_INTERVAL_MS = 10 * 60_000;
|
|
25
|
+
/** Heure de la dernière relecture d'un produit, en `localStorage` : `hgdc-org-sync:<base>` (`/` à la racine). */
|
|
26
|
+
export const ORG_SYNC_LAST_KEY_PREFIX = "hgdc-org-sync:";
|
|
18
27
|
/** Marque d'un lien de la barre vers un autre produit servi sous la même origine : prérendu au survol. */
|
|
19
28
|
export const PRODUCT_PRERENDER_ATTRIBUTE = "data-hgdc-prerender";
|
|
20
29
|
/**
|
|
@@ -83,12 +92,33 @@ export function takeOrgSyncCookie(doc = document) {
|
|
|
83
92
|
return takeOrgSync(doc) !== null;
|
|
84
93
|
}
|
|
85
94
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
* Vrai si la relecture de ce produit est due, et note alors l'heure : au plus une toutes les
|
|
96
|
+
* `BACKGROUND_REFRESH_INTERVAL_MS`. Sans `localStorage` (navigation privée stricte), toujours due.
|
|
97
|
+
*/
|
|
98
|
+
function refreshDue(base, win, now = Date.now()) {
|
|
99
|
+
const key = `${ORG_SYNC_LAST_KEY_PREFIX}${base || "/"}`;
|
|
100
|
+
try {
|
|
101
|
+
const storage = win?.localStorage;
|
|
102
|
+
if (!storage)
|
|
103
|
+
return true;
|
|
104
|
+
const last = Number(storage.getItem(key));
|
|
105
|
+
if (Number.isFinite(last) && last > 0 && now - last >= 0 && now - last < BACKGROUND_REFRESH_INTERVAL_MS)
|
|
106
|
+
return false;
|
|
107
|
+
storage.setItem(key, String(now));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Stockage refusé : la relecture part, comme avant.
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Relit les organisations dans un cadre caché, une fois, si l'entrée l'a demandé et que la dernière
|
|
116
|
+
* relecture de ce produit date de plus de `BACKGROUND_REFRESH_INTERVAL_MS`. Le cadre n'a ni script ni
|
|
117
|
+
* formulaire (`sandbox`), il suit seulement les redirections d'Account ; la nouvelle session du
|
|
118
|
+
* produit recopie les organisations, et les requêtes Convex abonnées se mettent à jour. Si la session
|
|
119
|
+
* d'Account a expiré, Account refuse d'être affiché dans un cadre : rien ne se passe, la session du
|
|
120
|
+
* produit reste celle d'avant. Une page prérendue au survol attend d'être affichée : un survol sans
|
|
121
|
+
* clic ne relit rien.
|
|
92
122
|
*/
|
|
93
123
|
export function startBackgroundRefresh(doc = document) {
|
|
94
124
|
if (doc.prerendering) {
|
|
@@ -96,7 +126,7 @@ export function startBackgroundRefresh(doc = document) {
|
|
|
96
126
|
return null;
|
|
97
127
|
}
|
|
98
128
|
const base = takeOrgSync(doc);
|
|
99
|
-
if (base === null)
|
|
129
|
+
if (base === null || !refreshDue(base, doc.defaultView))
|
|
100
130
|
return null;
|
|
101
131
|
const frame = doc.createElement("iframe");
|
|
102
132
|
frame.hidden = true;
|
package/dist/products.d.ts
CHANGED
|
@@ -65,13 +65,19 @@ export type ProductOrigin = {
|
|
|
65
65
|
export declare const PRODUCT_ORIGINS: Readonly<Record<string, ProductOrigin>>;
|
|
66
66
|
/** Adresse de l'app d'un produit : celle de `PRODUCT_ORIGINS`, sinon `https://<mot>.hgdc.ch`. */
|
|
67
67
|
export declare function productOrigin(word: string, origins?: Readonly<Record<string, ProductOrigin>>): ProductOrigin;
|
|
68
|
-
/**
|
|
69
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Entrée dans l'app d'un produit avec la session HGDC : `https://app.hgdc.ch/analytics/auth/hgdc?refresh=background&next=/`.
|
|
70
|
+
* Avec `organization` (slug de la dernière organisation ouverte), l'entrée mène droit à
|
|
71
|
+
* `/o/<organisation>` : sans les redirections de l'accueil (`/`, puis `/dashboard` ou `/o/…`).
|
|
72
|
+
*/
|
|
73
|
+
export declare function productEntryHref(word: string, origins?: Readonly<Record<string, ProductOrigin>>, organization?: string | null): string;
|
|
70
74
|
/**
|
|
71
75
|
* Requête de l'entrée depuis un autre produit : arriver sur l'accueil, relire les organisations en
|
|
72
76
|
* arrière-plan (`refresh=background`, ADR 0016).
|
|
73
77
|
*/
|
|
74
78
|
export declare const PRODUCT_ENTRY_REFRESH_QUERY = "refresh=background&next=/";
|
|
79
|
+
/** Requête de l'entrée : `refresh=background` toujours en tête (règles de prérendu), `next` vers l'organisation si connue. */
|
|
80
|
+
export declare function productEntryQuery(word: string, organization?: string | null): string;
|
|
75
81
|
export declare const HGDC_ACCOUNT: HgdcCatalogProduct;
|
|
76
82
|
/** Manager : les organisations, leurs membres et leurs rôles. En tête du menu et de la barre des produits. */
|
|
77
83
|
export declare const HGDC_MANAGER: HgdcCatalogProduct;
|
|
@@ -137,6 +143,8 @@ export declare function shellProductEntries(options?: {
|
|
|
137
143
|
accountHref?: string | undefined;
|
|
138
144
|
catalog?: ReadonlyArray<HgdcCatalogProduct> | undefined;
|
|
139
145
|
pageOrigin?: string | undefined;
|
|
146
|
+
/** Dernière organisation ouverte (`lastOrganization()`, absente au rendu serveur) : l'entrée y mène droit. */
|
|
147
|
+
organization?: string | null | undefined;
|
|
140
148
|
}): {
|
|
141
149
|
products: Array<ShellProductEntry>;
|
|
142
150
|
account: ShellProductEntry;
|
package/dist/products.js
CHANGED
|
@@ -37,16 +37,32 @@ export function productOrigin(word, origins = PRODUCT_ORIGINS) {
|
|
|
37
37
|
const key = word.toLowerCase();
|
|
38
38
|
return Object.hasOwn(origins, key) ? origins[key] : { origin: `https://${key}.hgdc.ch`, basePath: "" };
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Entrée dans l'app d'un produit avec la session HGDC : `https://app.hgdc.ch/analytics/auth/hgdc?refresh=background&next=/`.
|
|
42
|
+
* Avec `organization` (slug de la dernière organisation ouverte), l'entrée mène droit à
|
|
43
|
+
* `/o/<organisation>` : sans les redirections de l'accueil (`/`, puis `/dashboard` ou `/o/…`).
|
|
44
|
+
*/
|
|
45
|
+
export function productEntryHref(word, origins = PRODUCT_ORIGINS, organization) {
|
|
42
46
|
const app = productOrigin(word, origins);
|
|
43
|
-
return `${app.origin}${app.basePath}${PRODUCT_ENTRY_PATH}?${
|
|
47
|
+
return `${app.origin}${app.basePath}${PRODUCT_ENTRY_PATH}?${productEntryQuery(word, organization)}`;
|
|
44
48
|
}
|
|
45
49
|
/**
|
|
46
50
|
* Requête de l'entrée depuis un autre produit : arriver sur l'accueil, relire les organisations en
|
|
47
51
|
* arrière-plan (`refresh=background`, ADR 0016).
|
|
48
52
|
*/
|
|
49
53
|
export const PRODUCT_ENTRY_REFRESH_QUERY = "refresh=background&next=/";
|
|
54
|
+
/** Slug d'organisation tel que les produits l'écrivent dans `/o/<slug>` : rien d'autre ne part dans `next`. */
|
|
55
|
+
const ORGANIZATION_SLUG = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
56
|
+
/** Produits dont `/o/<slug>` est la page d'une organisation. Manager les désigne par leur identifiant. */
|
|
57
|
+
function opensOrganizationBySlug(word) {
|
|
58
|
+
return word.toLowerCase() !== "manager";
|
|
59
|
+
}
|
|
60
|
+
/** Requête de l'entrée : `refresh=background` toujours en tête (règles de prérendu), `next` vers l'organisation si connue. */
|
|
61
|
+
export function productEntryQuery(word, organization) {
|
|
62
|
+
if (!organization || !ORGANIZATION_SLUG.test(organization) || !opensOrganizationBySlug(word))
|
|
63
|
+
return PRODUCT_ENTRY_REFRESH_QUERY;
|
|
64
|
+
return `refresh=background&next=${encodeURIComponent(`/o/${organization}`)}`;
|
|
65
|
+
}
|
|
50
66
|
function product(word, pitch, icon, stage,
|
|
51
67
|
/** Nom affiché quand il diffère du mot de l'adresse : le nom du module (« Déploiements » pour `host`). */
|
|
52
68
|
name = word) {
|
|
@@ -147,7 +163,7 @@ export function sameOriginPath(href, pageOrigin) {
|
|
|
147
163
|
*/
|
|
148
164
|
export function shellProductEntries(options = {}) {
|
|
149
165
|
const current = options.current?.toLowerCase();
|
|
150
|
-
const { pageOrigin } = options;
|
|
166
|
+
const { pageOrigin, organization } = options;
|
|
151
167
|
const entry = (item, href, product) => {
|
|
152
168
|
const base = {
|
|
153
169
|
name: item.name,
|
|
@@ -165,7 +181,9 @@ export function shellProductEntries(options = {}) {
|
|
|
165
181
|
const head = manager ?? HGDC_MANAGER;
|
|
166
182
|
const local = localManagerHref(options.accountHref);
|
|
167
183
|
return {
|
|
168
|
-
products: others
|
|
184
|
+
products: others
|
|
185
|
+
.filter(isProductOpen)
|
|
186
|
+
.map((item) => entry(item, organization ? productEntryHref(item.badge, PRODUCT_ORIGINS, organization) : item.entryHref, true)),
|
|
169
187
|
account: entry(head, local ?? head.entryHref, local === undefined),
|
|
170
188
|
};
|
|
171
189
|
}
|
package/dist/spinner.d.ts
CHANGED
|
@@ -6,10 +6,11 @@ declare const SIZES: {
|
|
|
6
6
|
readonly 8: "size-8";
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
9
|
-
* L'anneau d'attente de la DA : un arc arrondi
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* L'anneau d'attente de la DA : un arc arrondi sur une piste à peine visible. La tête avance
|
|
10
|
+
* toujours, la queue la rattrape, puis l'arc repart d'un point ; l'anneau tourne lentement en même
|
|
11
|
+
* temps (`styles.css`, `.spinner-*`). Dans un bouton ou une ligne, il prend la couleur du texte
|
|
12
|
+
* (`tone="current"`) ; seul sur une page, les couleurs du H (`tone="accent"`).
|
|
13
|
+
* Mouvement réduit : un arc fixe qui respire.
|
|
13
14
|
*/
|
|
14
15
|
export declare function Spinner({ className, label, size, tone, }: {
|
|
15
16
|
className?: string;
|
package/dist/spinner.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from "react";
|
|
2
3
|
import { cn } from "./cn.js";
|
|
3
4
|
const SIZES = { 3: "size-3", 4: "size-4", 5: "size-5", 6: "size-6", 8: "size-8" };
|
|
4
5
|
/**
|
|
5
|
-
* L'anneau d'attente de la DA : un arc arrondi
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* L'anneau d'attente de la DA : un arc arrondi sur une piste à peine visible. La tête avance
|
|
7
|
+
* toujours, la queue la rattrape, puis l'arc repart d'un point ; l'anneau tourne lentement en même
|
|
8
|
+
* temps (`styles.css`, `.spinner-*`). Dans un bouton ou une ligne, il prend la couleur du texte
|
|
9
|
+
* (`tone="current"`) ; seul sur une page, les couleurs du H (`tone="accent"`).
|
|
10
|
+
* Mouvement réduit : un arc fixe qui respire.
|
|
9
11
|
*/
|
|
10
12
|
export function Spinner({ className, label, size = 4, tone = "current", }) {
|
|
11
|
-
|
|
13
|
+
const gradient = useId();
|
|
14
|
+
return (_jsxs("span", { role: "status", className: cn("inline-flex items-center gap-2", className), children: [_jsxs("svg", { viewBox: "0 0 32 32", fill: "none", "aria-hidden": true, className: cn("shrink-0", SIZES[size]), children: [tone === "accent" ? (_jsx("defs", { children: _jsxs("linearGradient", { id: gradient, x1: "0", y1: "0", x2: "1", y2: "1", children: [_jsx("stop", { offset: "0", stopColor: "#2c78fd" }), _jsx("stop", { offset: "0.45", stopColor: "#7343fe" }), _jsx("stop", { offset: "0.8", stopColor: "#fe5292" }), _jsx("stop", { offset: "1", stopColor: "#ff9a46" })] }) })) : null, _jsx("circle", { cx: "16", cy: "16", r: "12", strokeWidth: "4", className: "stroke-current opacity-15" }), _jsx("g", { className: "spinner-rotor", children: _jsx("circle", { cx: "16", cy: "16", r: "12", strokeWidth: "4", strokeLinecap: "round", pathLength: 100, transform: "rotate(-90 16 16)", className: cn("spinner-arc", tone === "accent" ? null : "stroke-current"), ...(tone === "accent" ? { stroke: `url(#${gradient})` } : {}) }) })] }), label ? _jsx("span", { children: label }) : _jsx("span", { className: "sr-only", children: "Chargement" })] }));
|
|
12
15
|
}
|
|
@@ -3,11 +3,8 @@ export declare const useIsomorphicLayoutEffect: typeof useLayoutEffect;
|
|
|
3
3
|
/** Faux au rendu serveur et à l'hydratation, vrai avant la première image qui suit. */
|
|
4
4
|
export declare function useHydrated(): boolean;
|
|
5
5
|
/**
|
|
6
|
-
* Le
|
|
7
|
-
* moins 300 ms.
|
|
8
|
-
*
|
|
6
|
+
* Le calque du H sur la zone de contenu, pendant le départ vers un autre module : aussitôt, puis
|
|
7
|
+
* au moins 300 ms. Effet de mise en page : la page est couverte avant d'être peinte, jamais une
|
|
8
|
+
* image vide entre les deux. Une navigation dans le module ne le montre plus (ADR 0025).
|
|
9
9
|
*/
|
|
10
|
-
export declare function
|
|
11
|
-
navigating: boolean;
|
|
12
|
-
pending: boolean;
|
|
13
|
-
}): boolean;
|
|
10
|
+
export declare function useShellCover(active: boolean): boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
-
|
|
2
|
+
/** Durée minimale du calque de départ une fois montré : le H ne clignote pas. */
|
|
3
|
+
const COVER_MIN_MS = 300;
|
|
3
4
|
export const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
4
5
|
/** Faux au rendu serveur et à l'hydratation, vrai avant la première image qui suit. */
|
|
5
6
|
export function useHydrated() {
|
|
@@ -8,22 +9,11 @@ export function useHydrated() {
|
|
|
8
9
|
return hydrated;
|
|
9
10
|
}
|
|
10
11
|
/**
|
|
11
|
-
* Le
|
|
12
|
-
* moins 300 ms.
|
|
13
|
-
*
|
|
12
|
+
* Le calque du H sur la zone de contenu, pendant le départ vers un autre module : aussitôt, puis
|
|
13
|
+
* au moins 300 ms. Effet de mise en page : la page est couverte avant d'être peinte, jamais une
|
|
14
|
+
* image vide entre les deux. Une navigation dans le module ne le montre plus (ADR 0025).
|
|
14
15
|
*/
|
|
15
|
-
export function
|
|
16
|
-
const [navigatingLong, setNavigatingLong] = useState(false);
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
if (!navigating)
|
|
19
|
-
return undefined;
|
|
20
|
-
const timer = window.setTimeout(() => setNavigatingLong(true), PAGE_PENDING_MS);
|
|
21
|
-
return () => {
|
|
22
|
-
window.clearTimeout(timer);
|
|
23
|
-
setNavigatingLong(false);
|
|
24
|
-
};
|
|
25
|
-
}, [navigating]);
|
|
26
|
-
const active = pending || (navigating && navigatingLong);
|
|
16
|
+
export function useShellCover(active) {
|
|
27
17
|
const [visible, setVisible] = useState(false);
|
|
28
18
|
const shownAt = useRef(0);
|
|
29
19
|
useIsomorphicLayoutEffect(() => {
|
|
@@ -33,7 +23,7 @@ export function useContentLoading({ navigating, pending }) {
|
|
|
33
23
|
return undefined;
|
|
34
24
|
}
|
|
35
25
|
if (!active && visible) {
|
|
36
|
-
const timer = window.setTimeout(() => setVisible(false), Math.max(0, shownAt.current +
|
|
26
|
+
const timer = window.setTimeout(() => setVisible(false), Math.max(0, shownAt.current + COVER_MIN_MS - performance.now()));
|
|
37
27
|
return () => window.clearTimeout(timer);
|
|
38
28
|
}
|
|
39
29
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.72.0",
|
|
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",
|
package/styles.css
CHANGED
|
@@ -558,6 +558,50 @@
|
|
|
558
558
|
animation: rise 420ms var(--ease-ios) both;
|
|
559
559
|
}
|
|
560
560
|
|
|
561
|
+
/* L'anneau d'attente (`Spinner`) : la tête de l'arc avance de 1 à 99 % du tour pendant que la
|
|
562
|
+
queue reste, puis la queue la rattrape (la tête ne recule jamais) ; l'anneau tourne lentement,
|
|
563
|
+
chaque départ tombe un peu plus loin. */
|
|
564
|
+
@keyframes spinner-arc {
|
|
565
|
+
0% {
|
|
566
|
+
stroke-dasharray: 1 150;
|
|
567
|
+
stroke-dashoffset: 0;
|
|
568
|
+
}
|
|
569
|
+
50% {
|
|
570
|
+
stroke-dasharray: 71 150;
|
|
571
|
+
stroke-dashoffset: -28;
|
|
572
|
+
}
|
|
573
|
+
100% {
|
|
574
|
+
stroke-dasharray: 71 150;
|
|
575
|
+
stroke-dashoffset: -99;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
@keyframes spinner-rotor {
|
|
579
|
+
to {
|
|
580
|
+
transform: rotate(360deg);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
@keyframes spinner-breathe {
|
|
584
|
+
50% {
|
|
585
|
+
opacity: 0.45;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
.spinner-rotor {
|
|
589
|
+
transform-origin: 50% 50%;
|
|
590
|
+
animation: spinner-rotor 1.6s linear infinite;
|
|
591
|
+
}
|
|
592
|
+
.spinner-arc {
|
|
593
|
+
animation: spinner-arc 1.2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
|
|
594
|
+
}
|
|
595
|
+
@media (prefers-reduced-motion: reduce) {
|
|
596
|
+
.spinner-rotor {
|
|
597
|
+
animation: none;
|
|
598
|
+
}
|
|
599
|
+
.spinner-arc {
|
|
600
|
+
stroke-dasharray: 30 150;
|
|
601
|
+
animation: spinner-breathe 1.6s ease-in-out infinite;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
561
605
|
@media (prefers-reduced-motion: reduce) {
|
|
562
606
|
.soft-in,
|
|
563
607
|
.pop-in,
|
|
@@ -792,11 +836,11 @@
|
|
|
792
836
|
}
|
|
793
837
|
}
|
|
794
838
|
|
|
795
|
-
/* Le chargeur du document (`FamilyLoaderRoot`) :
|
|
796
|
-
Visible quand une page attend (`PagePending`) hors de la coque, quand la
|
|
797
|
-
(`data-loading`)
|
|
798
|
-
|
|
799
|
-
pas clignoter
|
|
839
|
+
/* Le chargeur du document (`FamilyLoaderRoot`) : une seule lueur par page, en position fixe sur la
|
|
840
|
+
carte du contenu. Visible quand une page attend (`PagePending`) hors de la coque, ou quand la
|
|
841
|
+
coque part vers un autre module (`data-loading`). Dans la coque, une page qui attend montre son
|
|
842
|
+
squelette, jamais la lueur (ADR 0025). Masqué avec 80 ms de retenue : un `PagePending` qui laisse
|
|
843
|
+
la place à un autre ne fait pas clignoter la lueur. */
|
|
800
844
|
[data-slot="family-loader-root"] {
|
|
801
845
|
position: fixed;
|
|
802
846
|
inset: 0;
|
|
@@ -809,22 +853,40 @@
|
|
|
809
853
|
visibility 0s linear 200ms;
|
|
810
854
|
}
|
|
811
855
|
:root:has([data-slot="page-pending"]):not(:has([data-slot="app-shell"])) [data-slot="family-loader-root"],
|
|
812
|
-
:root:has([data-slot="app-shell"][data-loading]) [data-slot="family-loader-root"]
|
|
813
|
-
:root:has([data-slot="app-shell"]:not([data-hydrated]) [data-slot="page-pending"]) [data-slot="family-loader-root"] {
|
|
856
|
+
:root:has([data-slot="app-shell"][data-loading]) [data-slot="family-loader-root"] {
|
|
814
857
|
opacity: 1;
|
|
815
858
|
visibility: visible;
|
|
816
859
|
transition:
|
|
817
860
|
opacity 150ms var(--ease-out),
|
|
818
861
|
visibility 0s;
|
|
819
862
|
}
|
|
820
|
-
|
|
863
|
+
/* La lueur du chargement (`EdgeGlow`) : tout l'écran sur téléphone ; sur ordinateur, la carte du
|
|
864
|
+
contenu de la coque (marge de 8 px, `rounded-xl`), en-tête compris. */
|
|
865
|
+
[data-slot="family-loader-root"] > [data-glow] {
|
|
821
866
|
position: fixed;
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
867
|
+
inset: 0;
|
|
868
|
+
overflow: hidden;
|
|
869
|
+
}
|
|
870
|
+
@media (width >= 64rem) {
|
|
871
|
+
[data-slot="family-loader-root"] > [data-glow] {
|
|
872
|
+
inset: 0.5rem 0.5rem 0.5rem var(--hgdc-content-left);
|
|
873
|
+
border-radius: var(--radius-xl);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
[data-slot="edge-glow"] {
|
|
877
|
+
display: block;
|
|
878
|
+
width: 100%;
|
|
879
|
+
height: 100%;
|
|
880
|
+
}
|
|
881
|
+
/* Sans WebGL ou avant l'hydratation : une lueur immobile, mêmes couleurs, dessinée par le CSS. */
|
|
882
|
+
[data-slot="edge-glow"]:not([data-live]) {
|
|
883
|
+
padding: 1.5rem;
|
|
884
|
+
background: conic-gradient(#2c78fd, #7343fe, #fe5292, #ff9a46, #fe5292, #7343fe, #2c78fd);
|
|
885
|
+
mask:
|
|
886
|
+
linear-gradient(#000 0 0) content-box exclude,
|
|
887
|
+
linear-gradient(#000 0 0);
|
|
888
|
+
filter: blur(1.25rem);
|
|
889
|
+
opacity: 0.6;
|
|
828
890
|
}
|
|
829
891
|
/* Hors de la coque, le cadre de la coque : la page qui arrive a déjà sa forme, le H sa place. */
|
|
830
892
|
[data-slot="family-loader-root"] > [data-frame] {
|
|
@@ -877,8 +939,8 @@
|
|
|
877
939
|
content: var(--hgdc-org-initials, "");
|
|
878
940
|
}
|
|
879
941
|
|
|
880
|
-
/* Calque de la zone de contenu (`AppShell`)
|
|
881
|
-
|
|
942
|
+
/* Calque de la zone de contenu (`AppShell`) pendant le départ vers un autre module : opaque,
|
|
943
|
+
au-dessus de la page, sous la lueur du document. Même rythme que la lueur. */
|
|
882
944
|
[data-slot="app-shell-loading"] {
|
|
883
945
|
opacity: 0;
|
|
884
946
|
visibility: hidden;
|
|
@@ -886,8 +948,7 @@
|
|
|
886
948
|
opacity 120ms var(--ease-out) 80ms,
|
|
887
949
|
visibility 0s linear 200ms;
|
|
888
950
|
}
|
|
889
|
-
[data-slot="app-shell"][data-loading] [data-slot="app-shell-loading"]
|
|
890
|
-
[data-slot="app-shell"]:not([data-hydrated]):has([data-slot="page-pending"]) [data-slot="app-shell-loading"] {
|
|
951
|
+
[data-slot="app-shell"][data-loading] [data-slot="app-shell-loading"] {
|
|
891
952
|
opacity: 1;
|
|
892
953
|
visibility: visible;
|
|
893
954
|
transition:
|
|
@@ -895,6 +956,52 @@
|
|
|
895
956
|
visibility 0s;
|
|
896
957
|
}
|
|
897
958
|
|
|
959
|
+
/* Barre de progression d'une navigation dans le module (ADR 0025) : 2 px sous la barre du haut,
|
|
960
|
+
l'ancienne page reste lisible. Un segment qui court de gauche à droite, `transform` seul.
|
|
961
|
+
Mouvement réduit : la barre entière, immobile, en demi-teinte. */
|
|
962
|
+
[data-slot="app-shell-progress"] {
|
|
963
|
+
position: absolute;
|
|
964
|
+
inset-inline: 0;
|
|
965
|
+
bottom: -1px;
|
|
966
|
+
height: 2px;
|
|
967
|
+
overflow: hidden;
|
|
968
|
+
pointer-events: none;
|
|
969
|
+
opacity: 0;
|
|
970
|
+
transition: opacity 200ms var(--ease-out);
|
|
971
|
+
}
|
|
972
|
+
[data-slot="app-shell-progress"]::before {
|
|
973
|
+
content: "";
|
|
974
|
+
position: absolute;
|
|
975
|
+
inset-block: 0;
|
|
976
|
+
left: 0;
|
|
977
|
+
width: 40%;
|
|
978
|
+
background: var(--primary);
|
|
979
|
+
transform: translateX(-100%);
|
|
980
|
+
}
|
|
981
|
+
[data-slot="app-shell"][data-navigating] [data-slot="app-shell-progress"] {
|
|
982
|
+
opacity: 1;
|
|
983
|
+
transition-duration: 100ms;
|
|
984
|
+
}
|
|
985
|
+
[data-slot="app-shell"][data-navigating] [data-slot="app-shell-progress"]::before {
|
|
986
|
+
animation: hgdc-progress 1.1s var(--ease-in-out) infinite;
|
|
987
|
+
}
|
|
988
|
+
@keyframes hgdc-progress {
|
|
989
|
+
from {
|
|
990
|
+
transform: translateX(-100%);
|
|
991
|
+
}
|
|
992
|
+
to {
|
|
993
|
+
transform: translateX(250%);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
@media (prefers-reduced-motion: reduce) {
|
|
997
|
+
[data-slot="app-shell"][data-navigating] [data-slot="app-shell-progress"]::before {
|
|
998
|
+
animation: none;
|
|
999
|
+
width: 100%;
|
|
1000
|
+
transform: none;
|
|
1001
|
+
opacity: 0.5;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
898
1005
|
/* Passage d'un produit à l'autre sous la même origine (`app.hgdc.ch/<produit>`, ADR 0020) : une
|
|
899
1006
|
transition de vue entre documents. La barre latérale et la barre du haut
|
|
900
1007
|
gardent leur place (même nom des deux côtés : elles ne bougent pas, leur contenu passe en
|
|
@@ -925,9 +1032,8 @@
|
|
|
925
1032
|
}
|
|
926
1033
|
}
|
|
927
1034
|
:root:has([data-slot="page-pending"]):not(:has([data-slot="app-shell"])),
|
|
928
|
-
:root:has([data-slot="app-shell"][data-loading])
|
|
929
|
-
|
|
930
|
-
& [data-slot="family-loader-root"] > [data-stage] {
|
|
1035
|
+
:root:has([data-slot="app-shell"][data-loading]) {
|
|
1036
|
+
& [data-slot="family-loader-root"] > [data-glow] {
|
|
931
1037
|
view-transition-name: hgdc-loader;
|
|
932
1038
|
}
|
|
933
1039
|
}
|