@hgdc/ui 0.69.1 → 0.71.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 +2 -1
- package/dist/app-shell.d.ts +6 -5
- package/dist/app-shell.js +8 -16
- package/dist/basalte-mark.d.ts +1 -0
- package/dist/basalte-mark.js +4 -0
- package/dist/email.js +1 -1
- package/dist/family-loader.d.ts +1 -1
- package/dist/family-loader.js +1 -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-badge.d.ts +3 -1
- package/dist/product-badge.js +6 -1
- package/dist/use-content-loading.d.ts +4 -7
- package/dist/use-content-loading.js +7 -17
- package/family/basalte.png +0 -0
- package/package.json +1 -1
- package/styles.css +54 -11
package/README.md
CHANGED
|
@@ -136,7 +136,8 @@ Une seule barre latérale (ADR 0024) : organisation et bouton Réduire en haut,
|
|
|
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
138
|
| `family-loader` | `<FamilyLoaderRoot>{children}</FamilyLoaderRoot>` une fois dans le `body` de la route racine : le seul H d'un chargement de page, fixe sur la zone de contenu de la coque, rendu par le serveur, calé sur l'horloge (même pixel et même phase d'une page et d'un produit à l'autre, ADR 0015). `<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,
|
|
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, le H 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 @@
|
|
|
1
|
+
export declare const BASALTE_MARK_URI = "data:image/webp;base64,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";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Pierre de Basalte, 64 px en WebP (assez pour 20 px sur un écran 3x), réduite depuis
|
|
2
|
+
// `family/basalte.png` (256 px, pierre du site hgdc.ch détourée). Pas de SVG : le logo est une image.
|
|
3
|
+
// Régénérer : réduire `family/basalte.png` à 64 px (sharp, lanczos3, WebP qualité 92) et recoller ici.
|
|
4
|
+
export const BASALTE_MARK_URI = "data:image/webp;base64,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";
|
package/dist/email.js
CHANGED
|
@@ -11,7 +11,7 @@ import { HGDC_ACCOUNT, HGDC_PRODUCTS } from "./products.js";
|
|
|
11
11
|
*/
|
|
12
12
|
export { EMAIL_ICON_BASE, emailIconUrl };
|
|
13
13
|
const MAIL_HREF = "https://mail.hgdc.ch";
|
|
14
|
-
const LEGAL = "Hugo Gebel Développement Conseil,
|
|
14
|
+
const LEGAL = "Hugo Gebel Développement Conseil, Denens (VD)";
|
|
15
15
|
// Pas de police web : celle du système, Inter ou Geist quand elles sont installées.
|
|
16
16
|
const FONT = 'Geist, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
17
17
|
const MONO = 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace';
|
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>;
|
package/dist/family-loader.js
CHANGED
|
@@ -60,7 +60,7 @@ function Mark() {
|
|
|
60
60
|
return (_jsx(_Fragment, { children: PIECES.map((piece) => (_jsx("span", { "data-piece": piece, "aria-hidden": true, children: _jsx("span", { "data-fill": true }) }, piece))) }));
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
|
-
* Vrai sous `FamilyLoaderRoot` : le document a son chargeur, `PagePending` et la coque s'y
|
|
63
|
+
* Vrai sous `FamilyLoaderRoot` : le document a son chargeur, `PagePending` (hors de la coque) et la coque s'y
|
|
64
64
|
* signalent au lieu de dessiner leur propre H.
|
|
65
65
|
*/
|
|
66
66
|
export const FamilyLoaderRootContext = createContext(false);
|
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, le H 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 le H. */
|
|
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 le H : il est réservé au document qui n'a pas encore de coque (ADR 0025).
|
|
26
|
+
* - **Hors de la coque** (page d'entrée, rendu serveur sans coque) : le H 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, le H 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 le H. */
|
|
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 le H : il est réservé au document qui n'a pas encore de coque (ADR 0025).
|
|
29
|
+
* - **Hors de la coque** (page d'entrée, rendu serveur sans coque) : le H 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-badge.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ type MarkProps = {
|
|
|
20
20
|
export declare function ProductBadge({ product, className, label }: MarkProps & {
|
|
21
21
|
product: FamilyProduct;
|
|
22
22
|
}): React.JSX.Element;
|
|
23
|
-
/**
|
|
23
|
+
/** La pierre de Basalte (« Propulsé par … | Basalte »). Même image dans les deux thèmes. `size-4` par défaut. */
|
|
24
|
+
export declare function BasalteMark({ className, label }: MarkProps): React.JSX.Element;
|
|
25
|
+
/** Le H « Double », marque de la H Collection (hub). `size-4` par défaut. */
|
|
24
26
|
export declare function FamilyMark({ className, label }: MarkProps): React.JSX.Element;
|
|
25
27
|
export {};
|
package/dist/product-badge.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { BASALTE_MARK_URI } from "./basalte-mark.js";
|
|
2
3
|
import { cn } from "./cn.js";
|
|
3
4
|
import { FAMILY_MARK_SVG, PRODUCT_BADGE_SVG } from "./family-svg.js";
|
|
4
5
|
/**
|
|
@@ -37,7 +38,11 @@ function ThemedMark({ svg, slot, className, label }) {
|
|
|
37
38
|
export function ProductBadge({ product, className, label }) {
|
|
38
39
|
return (_jsx("img", { src: dataUri(PRODUCT_BADGE_SVG[product]), alt: label ?? "", "aria-hidden": label ? undefined : true, width: 16, height: 16, draggable: false, "data-slot": "product-badge", className: cn("size-4 shrink-0 select-none", className) }));
|
|
39
40
|
}
|
|
40
|
-
/**
|
|
41
|
+
/** La pierre de Basalte (« Propulsé par … | Basalte »). Même image dans les deux thèmes. `size-4` par défaut. */
|
|
42
|
+
export function BasalteMark({ className, label }) {
|
|
43
|
+
return (_jsx("img", { src: BASALTE_MARK_URI, alt: label ?? "", "aria-hidden": label ? undefined : true, width: 16, height: 16, draggable: false, "data-slot": "basalte-mark", className: cn("size-4 shrink-0 select-none", className) }));
|
|
44
|
+
}
|
|
45
|
+
/** Le H « Double », marque de la H Collection (hub). `size-4` par défaut. */
|
|
41
46
|
export function FamilyMark({ className, label }) {
|
|
42
47
|
return _jsx(ThemedMark, { svg: FAMILY_MARK_SVG, slot: "family-mark", className: className, label: label });
|
|
43
48
|
}
|
|
@@ -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;
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.71.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
|
@@ -793,9 +793,9 @@
|
|
|
793
793
|
}
|
|
794
794
|
|
|
795
795
|
/* Le chargeur du document (`FamilyLoaderRoot`) : un seul H par page, en position fixe sur la scène.
|
|
796
|
-
Visible quand une page attend (`PagePending`) hors de la coque, quand la coque
|
|
797
|
-
(`data-loading`)
|
|
798
|
-
|
|
796
|
+
Visible quand une page attend (`PagePending`) hors de la coque, ou quand la coque part vers un
|
|
797
|
+
autre module (`data-loading`). Dans la coque, une page qui attend montre son squelette, jamais
|
|
798
|
+
le H (ADR 0025). Masqué avec 80 ms de retenue : un `PagePending` qui laisse la place à un autre ne fait
|
|
799
799
|
pas clignoter le H. */
|
|
800
800
|
[data-slot="family-loader-root"] {
|
|
801
801
|
position: fixed;
|
|
@@ -809,8 +809,7 @@
|
|
|
809
809
|
visibility 0s linear 200ms;
|
|
810
810
|
}
|
|
811
811
|
: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"] {
|
|
812
|
+
:root:has([data-slot="app-shell"][data-loading]) [data-slot="family-loader-root"] {
|
|
814
813
|
opacity: 1;
|
|
815
814
|
visibility: visible;
|
|
816
815
|
transition:
|
|
@@ -877,8 +876,8 @@
|
|
|
877
876
|
content: var(--hgdc-org-initials, "");
|
|
878
877
|
}
|
|
879
878
|
|
|
880
|
-
/* Calque de la zone de contenu (`AppShell`)
|
|
881
|
-
|
|
879
|
+
/* Calque de la zone de contenu (`AppShell`) pendant le départ vers un autre module : opaque,
|
|
880
|
+
au-dessus de la page, sous le H du document. Même rythme que le H. */
|
|
882
881
|
[data-slot="app-shell-loading"] {
|
|
883
882
|
opacity: 0;
|
|
884
883
|
visibility: hidden;
|
|
@@ -886,8 +885,7 @@
|
|
|
886
885
|
opacity 120ms var(--ease-out) 80ms,
|
|
887
886
|
visibility 0s linear 200ms;
|
|
888
887
|
}
|
|
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"] {
|
|
888
|
+
[data-slot="app-shell"][data-loading] [data-slot="app-shell-loading"] {
|
|
891
889
|
opacity: 1;
|
|
892
890
|
visibility: visible;
|
|
893
891
|
transition:
|
|
@@ -895,6 +893,52 @@
|
|
|
895
893
|
visibility 0s;
|
|
896
894
|
}
|
|
897
895
|
|
|
896
|
+
/* Barre de progression d'une navigation dans le module (ADR 0025) : 2 px sous la barre du haut,
|
|
897
|
+
l'ancienne page reste lisible. Un segment qui court de gauche à droite, `transform` seul.
|
|
898
|
+
Mouvement réduit : la barre entière, immobile, en demi-teinte. */
|
|
899
|
+
[data-slot="app-shell-progress"] {
|
|
900
|
+
position: absolute;
|
|
901
|
+
inset-inline: 0;
|
|
902
|
+
bottom: -1px;
|
|
903
|
+
height: 2px;
|
|
904
|
+
overflow: hidden;
|
|
905
|
+
pointer-events: none;
|
|
906
|
+
opacity: 0;
|
|
907
|
+
transition: opacity 200ms var(--ease-out);
|
|
908
|
+
}
|
|
909
|
+
[data-slot="app-shell-progress"]::before {
|
|
910
|
+
content: "";
|
|
911
|
+
position: absolute;
|
|
912
|
+
inset-block: 0;
|
|
913
|
+
left: 0;
|
|
914
|
+
width: 40%;
|
|
915
|
+
background: var(--primary);
|
|
916
|
+
transform: translateX(-100%);
|
|
917
|
+
}
|
|
918
|
+
[data-slot="app-shell"][data-navigating] [data-slot="app-shell-progress"] {
|
|
919
|
+
opacity: 1;
|
|
920
|
+
transition-duration: 100ms;
|
|
921
|
+
}
|
|
922
|
+
[data-slot="app-shell"][data-navigating] [data-slot="app-shell-progress"]::before {
|
|
923
|
+
animation: hgdc-progress 1.1s var(--ease-in-out) infinite;
|
|
924
|
+
}
|
|
925
|
+
@keyframes hgdc-progress {
|
|
926
|
+
from {
|
|
927
|
+
transform: translateX(-100%);
|
|
928
|
+
}
|
|
929
|
+
to {
|
|
930
|
+
transform: translateX(250%);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
@media (prefers-reduced-motion: reduce) {
|
|
934
|
+
[data-slot="app-shell"][data-navigating] [data-slot="app-shell-progress"]::before {
|
|
935
|
+
animation: none;
|
|
936
|
+
width: 100%;
|
|
937
|
+
transform: none;
|
|
938
|
+
opacity: 0.5;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
898
942
|
/* Passage d'un produit à l'autre sous la même origine (`app.hgdc.ch/<produit>`, ADR 0020) : une
|
|
899
943
|
transition de vue entre documents. La barre latérale et la barre du haut
|
|
900
944
|
gardent leur place (même nom des deux côtés : elles ne bougent pas, leur contenu passe en
|
|
@@ -925,8 +969,7 @@
|
|
|
925
969
|
}
|
|
926
970
|
}
|
|
927
971
|
:root:has([data-slot="page-pending"]):not(:has([data-slot="app-shell"])),
|
|
928
|
-
:root:has([data-slot="app-shell"][data-loading])
|
|
929
|
-
:root:has([data-slot="app-shell"]:not([data-hydrated]) [data-slot="page-pending"]) {
|
|
972
|
+
:root:has([data-slot="app-shell"][data-loading]) {
|
|
930
973
|
& [data-slot="family-loader-root"] > [data-stage] {
|
|
931
974
|
view-transition-name: hgdc-loader;
|
|
932
975
|
}
|