@hgdc/ui 0.13.0 → 0.14.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 +19 -19
- package/dist/account-menu.js +3 -2
- package/dist/app-shell.d.ts +8 -1
- package/dist/app-shell.js +32 -5
- package/dist/family-loader.d.ts +14 -14
- package/dist/family-loader.js +23 -15
- package/dist/page-pending.d.ts +40 -11
- package/dist/page-pending.js +66 -12
- package/dist/product-rail.js +4 -2
- package/dist/shell-placement.d.ts +8 -0
- package/dist/shell-placement.js +20 -0
- package/package.json +1 -1
- package/styles.css +92 -60
package/README.md
CHANGED
|
@@ -80,25 +80,25 @@ Sur ordinateur, la coque ouvre sur la barre des produits (ADR 0011) : organisati
|
|
|
80
80
|
|
|
81
81
|
## Contenu et données
|
|
82
82
|
|
|
83
|
-
| Import | Exemple
|
|
84
|
-
| --------------- |
|
|
85
|
-
| `card` | `<Card><CardHeader><CardTitle>Domaines</CardTitle><CardAction>…</CardAction></CardHeader><CardContent>…</CardContent><CardFooter>…</CardFooter></Card>`
|
|
86
|
-
| `alert` | `<Alert tone="warning" title="Quota presque atteint" action={<Button size="sm">Voir</Button>}>90 % utilisés.</Alert>`
|
|
87
|
-
| `table` | `<TableHead sort="descending" onSort={…}>Date</TableHead>`, `<TableRowLink render={<Link … />}>Production</TableRowLink>`, `<TableEmpty colSpan={4}>…</TableEmpty>`, `<TableSkeleton columns={4} />`
|
|
88
|
-
| `resource-list` | `<ResourceList><ResourceItem icon={…} title="Site vitrine" meta="acme.ch" badge={…} actions={…} href="/p/site" /></ResourceList>`
|
|
89
|
-
| `stat` | `<StatGrid><StatTile label="Erreurs" value="12" delta={{ value: "8 %", direction: "up", good: "down" }} /></StatGrid>`
|
|
90
|
-
| `time-chart` | `<TimeChart label="Envois par jour" timestamps={days} series={[{ key: "sent", label: "Envoyés", values }]} kind="bar" lastIncomplete />` (dernier point en pointillés : la journée n'est pas finie)
|
|
91
|
-
| `status` | `<StatusBadge status="degraded" />` (Opérationnel, Dégradé, Panne, Maintenance, Inconnu) ; hors services, `<StatusBadge tone="success" label="Remis" />` (`neutral`, `success`, `warning`, `danger`, `info`, `pending`)
|
|
92
|
-
| `uptime-bar` | `<UptimeBar days={[{ date, status: "operational", uptime: "100 %" }]} summary="99,95 % sur 90 jours" />`
|
|
93
|
-
| `timeline` | `<Timeline entries={[{ id, title: "Résolu", time, status: "operational" }]} />`
|
|
94
|
-
| `relative-time` | `<RelativeTime date={deployment._creationTime} />` (« il y a 3 min », date exacte au survol)
|
|
95
|
-
| `copy-button` | `<Snippet value="bun add @hgdc/ui" prompt />`, `<CopyButton value={url} />`, `<CopyButton value={all} label="Tout copier" showLabel />`
|
|
96
|
-
| `progress` | `<Progress label="Envoi de la campagne" value={120} max={400} />` (`value={null}` : durée inconnue), `<Meter label="E-mails ce mois" value={8600} max={10000} />` (orange puis rouge près de la limite)
|
|
97
|
-
| `rank-bar` | `<RankBar label="Pages les plus vues" items={[{ key, label: "/tarifs", value: 640, render: <Link … /> }]} limit={5} />`
|
|
98
|
-
| `secret-reveal` | `<SecretReveal label="Clé d'API" value={key} onDismiss={forget} />` : affichée une fois, puis retirée de la page
|
|
99
|
-
| `family-loader` | `<FamilyLoader size="lg" label="Ouverture du projet…" />` :
|
|
100
|
-
| `page-pending` | `createRouter({ routeTree, ...routerPendingDefaults })`
|
|
101
|
-
| `skeleton` | `<Pending pending={isLoading} fallback={<TableSkeleton columns={3} />}>…</Pending>` : après 200 ms, tenu 400 ms au moins
|
|
83
|
+
| Import | Exemple |
|
|
84
|
+
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
85
|
+
| `card` | `<Card><CardHeader><CardTitle>Domaines</CardTitle><CardAction>…</CardAction></CardHeader><CardContent>…</CardContent><CardFooter>…</CardFooter></Card>` |
|
|
86
|
+
| `alert` | `<Alert tone="warning" title="Quota presque atteint" action={<Button size="sm">Voir</Button>}>90 % utilisés.</Alert>` |
|
|
87
|
+
| `table` | `<TableHead sort="descending" onSort={…}>Date</TableHead>`, `<TableRowLink render={<Link … />}>Production</TableRowLink>`, `<TableEmpty colSpan={4}>…</TableEmpty>`, `<TableSkeleton columns={4} />` |
|
|
88
|
+
| `resource-list` | `<ResourceList><ResourceItem icon={…} title="Site vitrine" meta="acme.ch" badge={…} actions={…} href="/p/site" /></ResourceList>` |
|
|
89
|
+
| `stat` | `<StatGrid><StatTile label="Erreurs" value="12" delta={{ value: "8 %", direction: "up", good: "down" }} /></StatGrid>` |
|
|
90
|
+
| `time-chart` | `<TimeChart label="Envois par jour" timestamps={days} series={[{ key: "sent", label: "Envoyés", values }]} kind="bar" lastIncomplete />` (dernier point en pointillés : la journée n'est pas finie) |
|
|
91
|
+
| `status` | `<StatusBadge status="degraded" />` (Opérationnel, Dégradé, Panne, Maintenance, Inconnu) ; hors services, `<StatusBadge tone="success" label="Remis" />` (`neutral`, `success`, `warning`, `danger`, `info`, `pending`) |
|
|
92
|
+
| `uptime-bar` | `<UptimeBar days={[{ date, status: "operational", uptime: "100 %" }]} summary="99,95 % sur 90 jours" />` |
|
|
93
|
+
| `timeline` | `<Timeline entries={[{ id, title: "Résolu", time, status: "operational" }]} />` |
|
|
94
|
+
| `relative-time` | `<RelativeTime date={deployment._creationTime} />` (« il y a 3 min », date exacte au survol) |
|
|
95
|
+
| `copy-button` | `<Snippet value="bun add @hgdc/ui" prompt />`, `<CopyButton value={url} />`, `<CopyButton value={all} label="Tout copier" showLabel />` |
|
|
96
|
+
| `progress` | `<Progress label="Envoi de la campagne" value={120} max={400} />` (`value={null}` : durée inconnue), `<Meter label="E-mails ce mois" value={8600} max={10000} />` (orange puis rouge près de la limite) |
|
|
97
|
+
| `rank-bar` | `<RankBar label="Pages les plus vues" items={[{ key, label: "/tarifs", value: 640, render: <Link … /> }]} limit={5} />` |
|
|
98
|
+
| `secret-reveal` | `<SecretReveal label="Clé d'API" value={key} onDismiss={forget} />` : affichée une fois, puis retirée de la page |
|
|
99
|
+
| `family-loader` | `<FamilyLoader size="lg" label="Ouverture du projet…" />` : la silhouette grise du H, dont les pièces se remplissent de leurs dégradés en boucle (1,6 s, `transform` seul, fluide pendant un chargement). `page` le centre dans l'écran. Dans un bouton : `Spinner` |
|
|
100
|
+
| `page-pending` | `createRouter({ routeTree, ...routerPendingDefaults })` (préchargement au survol, chargement après 50 ms, au moins 300 ms) ; `AppShell navigating={…} contentKey={pathname}` : le H couvre la zone de contenu dès qu'une navigation attend, la coque reste ; `PagePending` : même H, même place, pour une page qui lit ses données |
|
|
101
|
+
| `skeleton` | `<Pending pending={isLoading} fallback={<TableSkeleton columns={3} />}>…</Pending>` : après 200 ms, tenu 400 ms au moins |
|
|
102
102
|
|
|
103
103
|
## Listes, journaux et retours
|
|
104
104
|
|
package/dist/account-menu.js
CHANGED
|
@@ -16,7 +16,7 @@ import { initials } from "./initials.js";
|
|
|
16
16
|
import { MENU_LINK_ITEM } from "./org-switcher.js";
|
|
17
17
|
import { ProductBadge } from "./product-badge.js";
|
|
18
18
|
import { productMenuEntries } from "./products.js";
|
|
19
|
-
import { useProductRailShown } from "./shell-placement.js";
|
|
19
|
+
import { useLeaveOnClick, useProductRailShown } from "./shell-placement.js";
|
|
20
20
|
import { useTheme } from "./theme.js";
|
|
21
21
|
const THEMES = ["light", "dark", "system"];
|
|
22
22
|
/**
|
|
@@ -34,11 +34,12 @@ export function AccountMenu({ user, onProfile, onSecurity, onShortcuts, onSignOu
|
|
|
34
34
|
const { theme, setTheme } = useTheme();
|
|
35
35
|
const products = productsProp ?? productMenuEntries({ current: currentProduct, accountHref });
|
|
36
36
|
const railShown = useProductRailShown();
|
|
37
|
+
const leaveOnClick = useLeaveOnClick();
|
|
37
38
|
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: variant === "row" ? (_jsx("button", { type: "button", "aria-label": `Compte de ${user.name}`, className: "flex w-full min-w-0 items-center gap-2.5 rounded-xl p-1.5 text-left outline-none transition-colors duration-(--duration-fast) ease-out hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/50 aria-expanded:bg-muted lg:hover:bg-shell-hover lg:aria-expanded:bg-shell-hover lg:rail:justify-center" })) : (_jsx("button", { type: "button", "aria-label": `Compte de ${user.name}`, className: "touch-target grid size-9 place-items-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring/50" })), children: [_jsxs(Avatar, { children: [user.image ? _jsx(AvatarImage, { src: user.image, alt: "" }) : null, _jsx(AvatarFallback, { className: "text-xs font-semibold uppercase", children: initials(user.name, user.email) })] }), variant === "row" ? (_jsxs("span", { className: "min-w-0 flex-1 lg:rail:hidden", children: [_jsx("span", { className: "block truncate text-sm font-medium", children: user.name }), _jsx("span", { className: "block truncate text-xs text-muted-foreground lg:text-shell-muted", children: user.email })] })) : null] }), _jsxs(DropdownMenuContent, { align: variant === "row" ? "start" : "end", side: variant === "row" ? "top" : "bottom", className: "w-64", children: [_jsx(DropdownMenuGroup, { children: _jsxs(DropdownMenuLabel, { children: [_jsx("span", { className: "block truncate text-foreground", children: user.name }), _jsx("span", { className: "block truncate", children: user.email })] }) }), _jsx(DropdownMenuSeparator, {}), onProfile ? (_jsxs(DropdownMenuItem, { onClick: onProfile, children: [_jsx(HugeiconsIcon, { icon: UserIcon, "aria-hidden": true }), "Profil"] })) : null, onSecurity ? (_jsxs(DropdownMenuItem, { onClick: onSecurity, children: [_jsx(HugeiconsIcon, { icon: SecurityLockIcon, "aria-hidden": true }), "S\u00E9curit\u00E9"] })) : null, children, _jsxs(DropdownMenuSub, { children: [_jsxs(DropdownMenuSubTrigger, { children: [_jsx(HugeiconsIcon, { icon: theme === "dark" ? Moon02Icon : theme === "light" ? Sun03Icon : ComputerIcon, "aria-hidden": true }), "Th\u00E8me"] }), _jsx(DropdownMenuSubContent, { children: _jsxs(DropdownMenuRadioGroup, { value: theme, onValueChange: (value) => {
|
|
38
39
|
const next = THEMES.find((t) => t === value);
|
|
39
40
|
if (next)
|
|
40
41
|
setTheme(next);
|
|
41
|
-
}, children: [_jsx(DropdownMenuRadioItem, { value: "light", children: "Clair" }), _jsx(DropdownMenuRadioItem, { value: "dark", children: "Sombre" }), _jsx(DropdownMenuRadioItem, { value: "system", children: "Syst\u00E8me" })] }) })] }), onShortcuts ? (_jsxs(DropdownMenuItem, { onClick: onShortcuts, children: [_jsx(HugeiconsIcon, { icon: KeyboardIcon, "aria-hidden": true }), "Raccourcis clavier", _jsx(DropdownMenuShortcut, { children: "?" })] })) : null, products.length ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, { className: railShown ? "lg:hidden" : undefined }), _jsxs(DropdownMenuGroup, { className: railShown ? "lg:hidden" : undefined, children: [_jsx(DropdownMenuLabel, { children: "Produits" }), products.map((product) => product.current ? (_jsxs(DropdownMenuItem, { disabled: true, className: "data-disabled:opacity-100", children: [_jsx(ProductMenuIcon, { product: product }), _jsx("span", { className: "flex-1", children: product.name }), _jsx(HugeiconsIcon, { icon: Tick02Icon, "aria-label": "Produit ouvert" })] }, product.name)) : (_jsxs(MenuPrimitive.LinkItem, { href: product.href, className: MENU_LINK_ITEM, children: [_jsx(ProductMenuIcon, { product: product }), _jsx("span", { className: "flex-1", children: product.name }), product.hint ? (_jsx("span", { className: "text-xs font-normal text-muted-foreground", children: product.hint })) : null] }, product.name)))] })] })) : null, _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: onSignOut, children: [_jsx(HugeiconsIcon, { icon: Logout01Icon, "aria-hidden": true }), "Se d\u00E9connecter"] })] })] }));
|
|
42
|
+
}, children: [_jsx(DropdownMenuRadioItem, { value: "light", children: "Clair" }), _jsx(DropdownMenuRadioItem, { value: "dark", children: "Sombre" }), _jsx(DropdownMenuRadioItem, { value: "system", children: "Syst\u00E8me" })] }) })] }), onShortcuts ? (_jsxs(DropdownMenuItem, { onClick: onShortcuts, children: [_jsx(HugeiconsIcon, { icon: KeyboardIcon, "aria-hidden": true }), "Raccourcis clavier", _jsx(DropdownMenuShortcut, { children: "?" })] })) : null, products.length ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, { className: railShown ? "lg:hidden" : undefined }), _jsxs(DropdownMenuGroup, { className: railShown ? "lg:hidden" : undefined, children: [_jsx(DropdownMenuLabel, { children: "Produits" }), products.map((product) => product.current ? (_jsxs(DropdownMenuItem, { disabled: true, className: "data-disabled:opacity-100", children: [_jsx(ProductMenuIcon, { product: product }), _jsx("span", { className: "flex-1", children: product.name }), _jsx(HugeiconsIcon, { icon: Tick02Icon, "aria-label": "Produit ouvert" })] }, product.name)) : (_jsxs(MenuPrimitive.LinkItem, { href: product.href, onClick: leaveOnClick, className: MENU_LINK_ITEM, children: [_jsx(ProductMenuIcon, { product: product }), _jsx("span", { className: "flex-1", children: product.name }), product.hint ? (_jsx("span", { className: "text-xs font-normal text-muted-foreground", children: product.hint })) : null] }, product.name)))] })] })) : null, _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: onSignOut, children: [_jsx(HugeiconsIcon, { icon: Logout01Icon, "aria-hidden": true }), "Se d\u00E9connecter"] })] })] }));
|
|
42
43
|
}
|
|
43
44
|
function ProductMenuIcon({ product }) {
|
|
44
45
|
if (product.badge)
|
package/dist/app-shell.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export declare function SidebarNavItem({ icon, label, active, href, render, }: {
|
|
|
44
44
|
* Le kit ne connaît ni le routeur ni l'authentification : le produit passe ses éléments
|
|
45
45
|
* (`OrgSwitcher`, `SidebarNav`, `AccountMenu`) et ses liens.
|
|
46
46
|
*/
|
|
47
|
-
export declare function AppShell({ product, organization, productRail, accountHref, nav, sidebarFooter, header, onSearch, searchLabel, children, contentKey, mainClassName, }: {
|
|
47
|
+
export declare function AppShell({ product, organization, productRail, accountHref, nav, sidebarFooter, header, onSearch, searchLabel, children, contentKey, navigating, mainClassName, }: {
|
|
48
48
|
/** Le produit affiché (`"host"`) : sa marque en haut de la barre, « vous êtes ici » dans la barre des produits. */
|
|
49
49
|
product: FamilyProduct;
|
|
50
50
|
/** `OrgSwitcher` : en haut de la barre des produits, en haut du tiroir sur téléphone. */
|
|
@@ -80,6 +80,13 @@ export declare function AppShell({ product, organization, productRail, accountHr
|
|
|
80
80
|
* pendant la transition de la navigation. Sans clé, pas de frontière.
|
|
81
81
|
*/
|
|
82
82
|
contentKey?: string | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Une navigation vers une autre page est en cours (TanStack Router :
|
|
85
|
+
* `useRouterState({ select: (s) => s.isLoading && s.location.pathname !== s.resolvedLocation?.pathname })`).
|
|
86
|
+
* Au-delà de 50 ms, la zone de contenu se couvre du H centré, la coque restant en place : jamais
|
|
87
|
+
* l'ancienne page figée, quel que soit ce qui attend (session, loaders, code de la page).
|
|
88
|
+
*/
|
|
89
|
+
navigating?: boolean;
|
|
83
90
|
/**
|
|
84
91
|
* @deprecated La largeur du contenu est la même dans toute la famille (`max-w-6xl`, ADR 0012) :
|
|
85
92
|
* une page étroite passe par `Page` (`@hgdc/ui/page-header`), pas par la coque.
|
package/dist/app-shell.js
CHANGED
|
@@ -4,15 +4,15 @@ import PanelLeftCloseIcon from "@hugeicons/core-free-icons/PanelLeftCloseIcon";
|
|
|
4
4
|
import PanelLeftOpenIcon from "@hugeicons/core-free-icons/PanelLeftOpenIcon";
|
|
5
5
|
import { HugeiconsIcon } from "@hugeicons/react";
|
|
6
6
|
import { useRender } from "@base-ui/react/use-render";
|
|
7
|
-
import { createContext, Suspense, useContext, useEffect, useState, useSyncExternalStore } from "react";
|
|
7
|
+
import { createContext, Suspense, useCallback, useContext, useEffect, useRef, useState, useSyncExternalStore, } from "react";
|
|
8
8
|
import { CommandMenuButton } from "./command-menu.js";
|
|
9
9
|
import { Drawer, DrawerContent, DrawerTitle } from "./drawer.js";
|
|
10
10
|
import { isApplePlatform, Kbd, KbdGroup } from "./kbd.js";
|
|
11
|
-
import { PagePending } from "./page-pending.js";
|
|
11
|
+
import { CONTENT_LOADING_LAYER, ContentLoadingContext, ContentLoadingStage, PAGE_PENDING_MIN_MS, PAGE_PENDING_MS, PagePending, useDelayedFlag, } from "./page-pending.js";
|
|
12
12
|
import { ProductBadge } from "./product-badge.js";
|
|
13
13
|
import { ProductRail } from "./product-rail.js";
|
|
14
14
|
import { HGDC_ACCOUNT, HGDC_PRODUCTS } from "./products.js";
|
|
15
|
-
import { ProductRailShownContext } from "./shell-placement.js";
|
|
15
|
+
import { ProductRailShownContext, ShellLeaveContext } from "./shell-placement.js";
|
|
16
16
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
|
|
17
17
|
import { cn } from "./cn.js";
|
|
18
18
|
// ── Rail : la barre latérale réduite aux icônes, mémorisée par navigateur ──────────────
|
|
@@ -131,10 +131,37 @@ function SidebarHead({ product }) {
|
|
|
131
131
|
* Le kit ne connaît ni le routeur ni l'authentification : le produit passe ses éléments
|
|
132
132
|
* (`OrgSwitcher`, `SidebarNav`, `AccountMenu`) et ses liens.
|
|
133
133
|
*/
|
|
134
|
-
export function AppShell({ product, organization, productRail = true, accountHref, nav, sidebarFooter, header, onSearch, searchLabel, children, contentKey, mainClassName, }) {
|
|
134
|
+
export function AppShell({ product, organization, productRail = true, accountHref, nav, sidebarFooter, header, onSearch, searchLabel, children, contentKey, navigating = false, mainClassName, }) {
|
|
135
135
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
136
136
|
// Barre des produits élargie : la barre latérale se plie le temps du survol (ADR 0011).
|
|
137
137
|
const [railPeek, setRailPeek] = useState(false);
|
|
138
138
|
useToggleShortcut();
|
|
139
|
-
|
|
139
|
+
// Chargement de la zone de contenu : navigation en cours (après 50 ms, au moins 300 ms), ou départ
|
|
140
|
+
// vers un autre produit (aussitôt). Une page qui affiche déjà `PagePending` montre le même H au
|
|
141
|
+
// même endroit : le calque de la coque s'efface alors derrière lui.
|
|
142
|
+
const navigatingShown = useDelayedFlag(navigating, PAGE_PENDING_MS, PAGE_PENDING_MIN_MS);
|
|
143
|
+
const [leaving, setLeaving] = useState(false);
|
|
144
|
+
const [pagePending, setPagePending] = useState(0);
|
|
145
|
+
const registerPagePending = useCallback(() => {
|
|
146
|
+
setPagePending((count) => count + 1);
|
|
147
|
+
return () => setPagePending((count) => count - 1);
|
|
148
|
+
}, []);
|
|
149
|
+
const leave = useCallback(() => setLeaving(true), []);
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
// Retour arrière depuis l'autre produit (cache de page) : la page revient telle quelle.
|
|
152
|
+
const onPageShow = (event) => {
|
|
153
|
+
if (event.persisted)
|
|
154
|
+
setLeaving(false);
|
|
155
|
+
};
|
|
156
|
+
window.addEventListener("pageshow", onPageShow);
|
|
157
|
+
return () => window.removeEventListener("pageshow", onPageShow);
|
|
158
|
+
}, []);
|
|
159
|
+
const loading = (navigatingShown || leaving) && pagePending === 0;
|
|
160
|
+
const contentRef = useRef(null);
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
// Sur ordinateur, la zone de contenu défile seule : le calque couvre sa partie visible, en haut.
|
|
163
|
+
if (loading && contentRef.current)
|
|
164
|
+
contentRef.current.scrollTop = 0;
|
|
165
|
+
}, [loading]);
|
|
166
|
+
return (_jsx(ShellContext.Provider, { value: { closeMobileNav: () => setMenuOpen(false) }, children: _jsx(ProductRailShownContext, { value: productRail, children: _jsx(ShellLeaveContext, { value: leave, children: _jsxs("div", { "data-slot": "app-shell", "data-rail-peek": railPeek ? "" : undefined, className: cn("min-h-dvh lg:grid lg:h-dvh lg:bg-shell", productRail ? "lg:grid-cols-[auto_auto_minmax(0,1fr)]" : "lg:grid-cols-[auto_minmax(0,1fr)]"), 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" }), productRail ? (_jsx(ProductRail, { current: product, organization: organization, accountHref: accountHref, onExpandedChange: setRailPeek })) : null, _jsxs("aside", { "aria-label": "Barre lat\u00E9rale", 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: [_jsx(SidebarHead, { product: product }), !productRail && organization ? _jsx("div", { className: "shrink-0 px-3 pb-1", children: organization }) : null, nav, sidebarFooter ? _jsx("div", { className: "grid gap-0.5 px-3 pt-1 pb-3", children: sidebarFooter }) : null] }), _jsx("div", { 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", children: [_jsxs("header", { className: "sticky top-[env(safe-area-inset-top,0px)] z-30 flex h-14 shrink-0 items-center gap-1 border-b border-foreground/5 bg-background px-2 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: productName(product) })] }), _jsx("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: header }), onSearch ? (_jsx(CommandMenuButton, { onClick: onSearch, compact: "mobile", className: "shrink-0 lg:w-96 lg:max-w-none", ...(searchLabel !== undefined ? { label: searchLabel } : {}) })) : null] }), _jsx("div", { "data-slot": "app-shell-loading", "data-visible": loading ? "" : undefined, "aria-hidden": !loading, className: CONTENT_LOADING_LAYER, children: _jsx(ContentLoadingStage, { label: "Chargement\u2026" }) }), _jsx("main", { id: "contenu", tabIndex: -1, "aria-busy": loading || pagePending > 0 ? 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(ContentLoadingContext, { value: registerPagePending, children: contentKey === undefined ? (children) : (_jsx(Suspense, { fallback: _jsx(PagePending, {}), children: children }, contentKey)) }) })] }) }), _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, nav, sidebarFooter ? (_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)]", children: sidebarFooter })) : null] }) })] }) }) }) }));
|
|
140
167
|
}
|
package/dist/family-loader.d.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
declare const SIZES: {
|
|
2
|
-
readonly sm:
|
|
3
|
-
readonly md:
|
|
4
|
-
readonly lg:
|
|
2
|
+
readonly sm: 24;
|
|
3
|
+
readonly md: 40;
|
|
4
|
+
readonly lg: 64;
|
|
5
5
|
};
|
|
6
6
|
/**
|
|
7
|
-
* Le chargement d'une page :
|
|
8
|
-
*
|
|
9
|
-
* boucle
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* Le chargement d'une page : la silhouette grise du H de la famille, toujours visible, dont les
|
|
8
|
+
* trois pièces se remplissent de leurs dégradés tricolores l'une après l'autre, puis se vident, en
|
|
9
|
+
* boucle (1,6 s). CSS seul (`styles.css`) et seulement `transform` : l'animation tourne sur le
|
|
10
|
+
* compositeur et reste fluide pendant qu'une page charge. Mouvement réduit : la silhouette reste,
|
|
11
|
+
* le remplissage pulse doucement.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Dans la coque (`AppShell`), un chargement de page passe par `PagePending`, qui affiche ce H à une
|
|
14
|
+
* position fixe au centre de la zone de contenu. Ailleurs, `page` le centre dans l'écran. Dans un
|
|
15
|
+
* bouton ou une ligne, `Spinner` reste la règle.
|
|
15
16
|
*/
|
|
16
|
-
export declare function FamilyLoader({ size, label,
|
|
17
|
+
export declare function FamilyLoader({ size, label, page, className, }: {
|
|
18
|
+
/** `sm` 24 px, `md` 40 px, `lg` 64 px. Par défaut `lg` en page, `md` sinon. */
|
|
17
19
|
size?: keyof typeof SIZES;
|
|
18
20
|
label?: string;
|
|
19
|
-
/**
|
|
20
|
-
delay?: number;
|
|
21
|
-
/** Centré dans la zone de contenu (sous l'en-tête de la coque), en taille `lg` par défaut. */
|
|
21
|
+
/** Centré dans l'écran, pour un chargement hors de la coque. */
|
|
22
22
|
page?: boolean;
|
|
23
23
|
className?: string;
|
|
24
24
|
}): React.JSX.Element;
|
package/dist/family-loader.js
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import {
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
3
3
|
import { cn } from "./cn.js";
|
|
4
|
-
const SIZES = { sm:
|
|
4
|
+
const SIZES = { sm: 24, md: 40, lg: 64 };
|
|
5
5
|
/**
|
|
6
|
-
* Le chargement d'une page :
|
|
7
|
-
*
|
|
8
|
-
* boucle
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Le chargement d'une page : la silhouette grise du H de la famille, toujours visible, dont les
|
|
7
|
+
* trois pièces se remplissent de leurs dégradés tricolores l'une après l'autre, puis se vident, en
|
|
8
|
+
* boucle (1,6 s). CSS seul (`styles.css`) et seulement `transform` : l'animation tourne sur le
|
|
9
|
+
* compositeur et reste fluide pendant qu'une page charge. Mouvement réduit : la silhouette reste,
|
|
10
|
+
* le remplissage pulse doucement.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Dans la coque (`AppShell`), un chargement de page passe par `PagePending`, qui affiche ce H à une
|
|
13
|
+
* position fixe au centre de la zone de contenu. Ailleurs, `page` le centre dans l'écran. Dans un
|
|
14
|
+
* bouton ou une ligne, `Spinner` reste la règle.
|
|
14
15
|
*/
|
|
15
|
-
export function FamilyLoader({ size, label = "Chargement…",
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
16
|
+
export function FamilyLoader({ size, label = "Chargement…", page = false, className, }) {
|
|
17
|
+
const px = SIZES[size ?? (page ? "lg" : "md")];
|
|
18
|
+
const ref = useRef(null);
|
|
19
|
+
// Tous les H de la page battent en phase, calés sur l'horloge du document : passer du calque de
|
|
20
|
+
// navigation de la coque à celui de la page, ou du rendu serveur à l'hydratation, ne saute pas.
|
|
21
|
+
useIsomorphicLayoutEffect(() => {
|
|
22
|
+
for (const animation of ref.current?.getAnimations?.({ subtree: true }) ?? [])
|
|
23
|
+
animation.startTime = 0;
|
|
24
|
+
}, []);
|
|
25
|
+
const loader = (_jsx("span", { ref: ref, role: "status", "aria-label": label, "data-slot": "family-loader", className: cn("family-loader", !page && className), style: { ["--family-loader-size"]: `${px}px` }, children: ["left", "right", "bar"].map((piece) => (_jsx("span", { "data-piece": piece, "aria-hidden": true, children: _jsx("span", { "data-fill": true }) }, piece))) }));
|
|
19
26
|
if (!page)
|
|
20
27
|
return loader;
|
|
21
|
-
return (_jsx("div", { "data-slot": "family-loader-page", className: cn("grid min-h-
|
|
28
|
+
return (_jsx("div", { "data-slot": "family-loader-page", className: cn("grid min-h-dvh w-full place-items-center", className), children: loader }));
|
|
22
29
|
}
|
|
30
|
+
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
package/dist/page-pending.d.ts
CHANGED
|
@@ -1,27 +1,56 @@
|
|
|
1
|
-
/** Attente avant d'afficher le chargement d'une page : une navigation rapide ne le montre jamais. */
|
|
2
|
-
export declare const PAGE_PENDING_MS = 120;
|
|
3
|
-
/** Durée minimale d'affichage une fois apparu : il ne clignote pas. */
|
|
4
|
-
export declare const PAGE_PENDING_MIN_MS = 400;
|
|
5
1
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
* Délai avant de couvrir l'ancienne page pendant une navigation : une navigation préchargée
|
|
3
|
+
* (survol, `defaultPreload: "intent"`) se termine avant et ne montre rien.
|
|
4
|
+
*/
|
|
5
|
+
export declare const PAGE_PENDING_MS = 50;
|
|
6
|
+
/** Durée minimale d'affichage une fois apparu : le H ne clignote pas. */
|
|
7
|
+
export declare const PAGE_PENDING_MIN_MS = 300;
|
|
8
|
+
/**
|
|
9
|
+
* Calque de chargement de la zone de contenu : sous l'en-tête de la coque (56 px), sur toute la
|
|
10
|
+
* largeur, opaque. Le H y est centré dans la hauteur visible et y reste (`sticky`) : même place
|
|
11
|
+
* pour le calque de navigation de la coque et pour `PagePending`, donc aucun saut de l'un à l'autre.
|
|
12
|
+
*/
|
|
13
|
+
export declare const CONTENT_LOADING_LAYER = "absolute inset-x-0 top-14 bottom-0 z-20 bg-background";
|
|
14
|
+
/** Le H centré dans la zone de contenu (calque de `CONTENT_LOADING_LAYER`). */
|
|
15
|
+
export declare function ContentLoadingStage({ label }: {
|
|
16
|
+
label: string;
|
|
17
|
+
}): React.JSX.Element;
|
|
18
|
+
/**
|
|
19
|
+
* Fourni par `AppShell` autour de la zone de contenu : `PagePending` s'y signale, pour que la coque
|
|
20
|
+
* n'affiche pas son propre calque de navigation par-dessus.
|
|
21
|
+
*/
|
|
22
|
+
export declare const ContentLoadingContext: React.Context<(() => () => void) | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Le chargement d'une page. Dans la coque (`AppShell`) : le H centré sur un calque opaque de la
|
|
25
|
+
* zone de contenu, la coque restant en place, dès le rendu serveur. Hors de la coque : le H centré
|
|
26
|
+
* dans l'écran. Composant de `pendingComponent` (TanStack Router, `routerPendingDefaults`), repli
|
|
27
|
+
* de la frontière `Suspense` de la coque, et état de chargement d'une page qui lit ses données.
|
|
10
28
|
*/
|
|
11
29
|
export declare function PagePending({ label }: {
|
|
12
30
|
label?: string;
|
|
13
31
|
}): React.JSX.Element;
|
|
14
32
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
|
|
33
|
+
* Vrai après `delay` ms de `active`, et le reste au moins `min` ms une fois vrai : un état bref ne
|
|
34
|
+
* montre rien, un état montré ne clignote pas.
|
|
35
|
+
*/
|
|
36
|
+
export declare function useDelayedFlag(active: boolean, delay: number, min: number): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Réglages communs de `createRouter` (TanStack Router) :
|
|
39
|
+
* - préchargement au survol ou au focus d'un lien (`intent`) : la plupart des pages s'ouvrent sans
|
|
40
|
+
* attente ; `defaultPreloadStaleTime: 0` laisse TanStack Query décider de la fraîcheur ;
|
|
41
|
+
* - une route qui attend encore ses données montre `PagePending` après 50 ms, au moins 300 ms.
|
|
18
42
|
*
|
|
19
43
|
* ```ts
|
|
20
44
|
* createRouter({ routeTree, ...routerPendingDefaults })
|
|
21
45
|
* ```
|
|
46
|
+
*
|
|
47
|
+
* La coque doit vivre dans une route de mise en page (`/o/$org`) et le `beforeLoad` de la racine
|
|
48
|
+
* ne doit pas appeler le serveur à chaque navigation : une attente là remplacerait toute l'app.
|
|
22
49
|
*/
|
|
23
50
|
export declare const routerPendingDefaults: {
|
|
24
51
|
defaultPendingComponent: typeof PagePending;
|
|
25
52
|
defaultPendingMs: number;
|
|
26
53
|
defaultPendingMinMs: number;
|
|
54
|
+
defaultPreload: "intent";
|
|
55
|
+
defaultPreloadStaleTime: number;
|
|
27
56
|
};
|
package/dist/page-pending.js
CHANGED
|
@@ -1,29 +1,83 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useEffect, useLayoutEffect, useState } from "react";
|
|
2
3
|
import { FamilyLoader } from "./family-loader.js";
|
|
3
|
-
/** Attente avant d'afficher le chargement d'une page : une navigation rapide ne le montre jamais. */
|
|
4
|
-
export const PAGE_PENDING_MS = 120;
|
|
5
|
-
/** Durée minimale d'affichage une fois apparu : il ne clignote pas. */
|
|
6
|
-
export const PAGE_PENDING_MIN_MS = 400;
|
|
7
4
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
* Délai avant de couvrir l'ancienne page pendant une navigation : une navigation préchargée
|
|
6
|
+
* (survol, `defaultPreload: "intent"`) se termine avant et ne montre rien.
|
|
7
|
+
*/
|
|
8
|
+
export const PAGE_PENDING_MS = 50;
|
|
9
|
+
/** Durée minimale d'affichage une fois apparu : le H ne clignote pas. */
|
|
10
|
+
export const PAGE_PENDING_MIN_MS = 300;
|
|
11
|
+
/**
|
|
12
|
+
* Calque de chargement de la zone de contenu : sous l'en-tête de la coque (56 px), sur toute la
|
|
13
|
+
* largeur, opaque. Le H y est centré dans la hauteur visible et y reste (`sticky`) : même place
|
|
14
|
+
* pour le calque de navigation de la coque et pour `PagePending`, donc aucun saut de l'un à l'autre.
|
|
15
|
+
*/
|
|
16
|
+
export const CONTENT_LOADING_LAYER = "absolute inset-x-0 top-14 bottom-0 z-20 bg-background";
|
|
17
|
+
const CONTENT_LOADING_STAGE = "sticky top-14 grid h-[calc(100dvh-3.5rem)] place-items-center lg:h-[calc(100dvh-1rem-3.5rem)]";
|
|
18
|
+
/** Le H centré dans la zone de contenu (calque de `CONTENT_LOADING_LAYER`). */
|
|
19
|
+
export function ContentLoadingStage({ label }) {
|
|
20
|
+
return (_jsx("div", { className: CONTENT_LOADING_STAGE, children: _jsx(FamilyLoader, { size: "lg", label: label }) }));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Fourni par `AppShell` autour de la zone de contenu : `PagePending` s'y signale, pour que la coque
|
|
24
|
+
* n'affiche pas son propre calque de navigation par-dessus.
|
|
25
|
+
*/
|
|
26
|
+
export const ContentLoadingContext = createContext(null);
|
|
27
|
+
/**
|
|
28
|
+
* Le chargement d'une page. Dans la coque (`AppShell`) : le H centré sur un calque opaque de la
|
|
29
|
+
* zone de contenu, la coque restant en place, dès le rendu serveur. Hors de la coque : le H centré
|
|
30
|
+
* dans l'écran. Composant de `pendingComponent` (TanStack Router, `routerPendingDefaults`), repli
|
|
31
|
+
* de la frontière `Suspense` de la coque, et état de chargement d'une page qui lit ses données.
|
|
12
32
|
*/
|
|
13
33
|
export function PagePending({ label = "Chargement…" }) {
|
|
14
|
-
|
|
34
|
+
const register = useContext(ContentLoadingContext);
|
|
35
|
+
useIsomorphicLayoutEffect(() => register?.(), [register]);
|
|
36
|
+
if (!register)
|
|
37
|
+
return _jsx(FamilyLoader, { page: true, label: label });
|
|
38
|
+
return (_jsx("div", { "data-slot": "page-pending", className: "min-h-[calc(100dvh-10rem)]", children: _jsx("div", { className: CONTENT_LOADING_LAYER, children: _jsx(ContentLoadingStage, { label: label }) }) }));
|
|
15
39
|
}
|
|
40
|
+
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
16
41
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
42
|
+
* Vrai après `delay` ms de `active`, et le reste au moins `min` ms une fois vrai : un état bref ne
|
|
43
|
+
* montre rien, un état montré ne clignote pas.
|
|
44
|
+
*/
|
|
45
|
+
export function useDelayedFlag(active, delay, min) {
|
|
46
|
+
const [visible, setVisible] = useState(false);
|
|
47
|
+
const [shownAt, setShownAt] = useState(0);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (active && !visible) {
|
|
50
|
+
const timer = window.setTimeout(() => {
|
|
51
|
+
setShownAt(Date.now());
|
|
52
|
+
setVisible(true);
|
|
53
|
+
}, delay);
|
|
54
|
+
return () => window.clearTimeout(timer);
|
|
55
|
+
}
|
|
56
|
+
if (!active && visible) {
|
|
57
|
+
const timer = window.setTimeout(() => setVisible(false), Math.max(0, shownAt + min - Date.now()));
|
|
58
|
+
return () => window.clearTimeout(timer);
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}, [active, visible, delay, min, shownAt]);
|
|
62
|
+
return visible;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Réglages communs de `createRouter` (TanStack Router) :
|
|
66
|
+
* - préchargement au survol ou au focus d'un lien (`intent`) : la plupart des pages s'ouvrent sans
|
|
67
|
+
* attente ; `defaultPreloadStaleTime: 0` laisse TanStack Query décider de la fraîcheur ;
|
|
68
|
+
* - une route qui attend encore ses données montre `PagePending` après 50 ms, au moins 300 ms.
|
|
20
69
|
*
|
|
21
70
|
* ```ts
|
|
22
71
|
* createRouter({ routeTree, ...routerPendingDefaults })
|
|
23
72
|
* ```
|
|
73
|
+
*
|
|
74
|
+
* La coque doit vivre dans une route de mise en page (`/o/$org`) et le `beforeLoad` de la racine
|
|
75
|
+
* ne doit pas appeler le serveur à chaque navigation : une attente là remplacerait toute l'app.
|
|
24
76
|
*/
|
|
25
77
|
export const routerPendingDefaults = {
|
|
26
78
|
defaultPendingComponent: PagePending,
|
|
27
79
|
defaultPendingMs: PAGE_PENDING_MS,
|
|
28
80
|
defaultPendingMinMs: PAGE_PENDING_MIN_MS,
|
|
81
|
+
defaultPreload: "intent",
|
|
82
|
+
defaultPreloadStaleTime: 0,
|
|
29
83
|
};
|
package/dist/product-rail.js
CHANGED
|
@@ -2,12 +2,14 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { ProductBadge } from "./product-badge.js";
|
|
4
4
|
import { productRailEntries } from "./products.js";
|
|
5
|
-
import { ShellPlacementContext } from "./shell-placement.js";
|
|
5
|
+
import { ShellPlacementContext, useLeaveOnClick } from "./shell-placement.js";
|
|
6
6
|
import { cn } from "./cn.js";
|
|
7
7
|
/** Délai d'intention avant d'élargir la barre au survol : un passage de souris ne l'ouvre pas. */
|
|
8
8
|
export const PRODUCT_RAIL_OPEN_DELAY = 150;
|
|
9
9
|
function ProductLink({ entry }) {
|
|
10
|
-
|
|
10
|
+
// Chaque produit s'ouvre en document complet : la coque montre le H dès le clic.
|
|
11
|
+
const leaveOnClick = useLeaveOnClick();
|
|
12
|
+
return (_jsx("li", { children: _jsxs("a", { href: entry.href, onClick: leaveOnClick, "aria-current": entry.current ? "page" : undefined, className: cn("flex h-10 items-center gap-3 rounded-xl px-2 text-shell-muted outline-none transition-colors duration-(--duration-fast) ease-out", "hover:bg-shell-hover hover:text-shell-foreground focus-visible:ring-2 focus-visible:ring-ring/50", "aria-[current=page]:bg-shell-active aria-[current=page]:text-shell-foreground"), children: [_jsx(ProductBadge, { product: entry.badge, className: "size-6" }), _jsxs("span", { className: "min-w-0 flex-1 truncate opacity-0 transition-opacity duration-(--duration-fast) ease-out group-data-expanded/rail:opacity-100 motion-reduce:transition-none", children: [_jsx("span", { className: "font-heading text-sm font-medium", children: entry.name }), entry.hint ? _jsx("span", { className: "sr-only", children: ", " }) : null, entry.hint ? _jsx("span", { className: "ml-1.5 text-xs text-shell-muted", children: entry.hint }) : null] })] }) }));
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* La barre des produits de la famille, tout à gauche de la coque sur ordinateur : organisation en
|
|
@@ -11,3 +11,11 @@ export declare function useShellPlacement(): ShellPlacement;
|
|
|
11
11
|
*/
|
|
12
12
|
export declare const ProductRailShownContext: React.Context<boolean>;
|
|
13
13
|
export declare function useProductRailShown(): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Prévient la coque qu'un lien vers un autre produit (ou une autre adresse servie en document
|
|
16
|
+
* complet) vient d'être suivi : elle couvre aussitôt la zone de contenu du H de chargement, le
|
|
17
|
+
* temps que le navigateur ouvre l'autre produit. Sans coque, ne fait rien.
|
|
18
|
+
*/
|
|
19
|
+
export declare const ShellLeaveContext: React.Context<() => void>;
|
|
20
|
+
/** `onClick` d'un lien vers un autre produit : clic simple seulement (pas Ctrl, Cmd, Maj, molette). */
|
|
21
|
+
export declare function useLeaveOnClick(): (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
package/dist/shell-placement.js
CHANGED
|
@@ -11,3 +11,23 @@ export const ProductRailShownContext = createContext(false);
|
|
|
11
11
|
export function useProductRailShown() {
|
|
12
12
|
return useContext(ProductRailShownContext);
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Prévient la coque qu'un lien vers un autre produit (ou une autre adresse servie en document
|
|
16
|
+
* complet) vient d'être suivi : elle couvre aussitôt la zone de contenu du H de chargement, le
|
|
17
|
+
* temps que le navigateur ouvre l'autre produit. Sans coque, ne fait rien.
|
|
18
|
+
*/
|
|
19
|
+
export const ShellLeaveContext = createContext(() => { });
|
|
20
|
+
/** `onClick` d'un lien vers un autre produit : clic simple seulement (pas Ctrl, Cmd, Maj, molette). */
|
|
21
|
+
export function useLeaveOnClick() {
|
|
22
|
+
const leave = useContext(ShellLeaveContext);
|
|
23
|
+
return (event) => {
|
|
24
|
+
if (event.defaultPrevented || event.button !== 0)
|
|
25
|
+
return;
|
|
26
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
|
|
27
|
+
return;
|
|
28
|
+
const target = event.currentTarget.getAttribute("target");
|
|
29
|
+
if (target && target !== "_self")
|
|
30
|
+
return;
|
|
31
|
+
leave();
|
|
32
|
+
};
|
|
33
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Interface de la H family : jetons de la DA, thèmes, polices et composants.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://github.com/hugogebel-boop/hgdc-kit/tree/main/packages/ui#readme",
|
package/styles.css
CHANGED
|
@@ -481,90 +481,122 @@
|
|
|
481
481
|
/* `FamilyLoader` : le H tricolore se construit (jambage gauche, barre, jambage droit), tient,
|
|
482
482
|
s'efface, recommence. Les pièces sont coupées par `clip-path` (la forme ne change pas), un
|
|
483
483
|
léger chevauchement lie les trois gestes. Visible après 200 ms seulement : pas de flash. */
|
|
484
|
+
/* Chargeur de la famille (`FamilyLoader`) : silhouette grise du H, pièces remplies de leurs
|
|
485
|
+
dégradés. Géométrie du H sur 256 (FAMILY_MARK_SVG). Seul `transform` s'anime : compositeur,
|
|
486
|
+
fluide pendant qu'une page charge. */
|
|
484
487
|
.family-loader {
|
|
485
|
-
|
|
486
|
-
|
|
488
|
+
--fl: var(--family-loader-size, 40px);
|
|
489
|
+
--fl-ghost: color-mix(in oklab, var(--foreground) var(--fl-ghost-mix, 11%), var(--background));
|
|
490
|
+
position: relative;
|
|
491
|
+
display: inline-block;
|
|
492
|
+
flex-shrink: 0;
|
|
493
|
+
width: var(--fl);
|
|
494
|
+
height: var(--fl);
|
|
495
|
+
}
|
|
496
|
+
/* Sur fond sombre, la silhouette a besoin d'un peu plus de lumière pour rester lisible. */
|
|
497
|
+
.dark .family-loader {
|
|
498
|
+
--fl-ghost-mix: 19%;
|
|
499
|
+
}
|
|
500
|
+
.family-loader [data-piece] {
|
|
501
|
+
position: absolute;
|
|
502
|
+
overflow: hidden;
|
|
503
|
+
background: var(--fl-ghost);
|
|
504
|
+
/* Le coin arrondi découpe aussi le remplissage, sur sa propre couche. */
|
|
505
|
+
isolation: isolate;
|
|
506
|
+
}
|
|
507
|
+
.family-loader [data-piece="left"],
|
|
508
|
+
.family-loader [data-piece="right"] {
|
|
509
|
+
top: calc(var(--fl) * 24 / 256);
|
|
510
|
+
width: calc(var(--fl) * 57 / 256);
|
|
511
|
+
height: calc(var(--fl) * 208 / 256);
|
|
512
|
+
border-radius: calc(var(--fl) * 18 / 256);
|
|
487
513
|
}
|
|
488
|
-
/* Le nom dans chaque raccourci : sans lui, lightningcss (minification de Vite) réduit
|
|
489
|
-
`animation: 1.8s infinite both` à `animation: none`. */
|
|
490
514
|
.family-loader [data-piece="left"] {
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
.family-loader [data-piece="bar"] {
|
|
494
|
-
animation: family-loader-bar 1.8s infinite both;
|
|
515
|
+
left: calc(var(--fl) * 43 / 256);
|
|
495
516
|
}
|
|
496
517
|
.family-loader [data-piece="right"] {
|
|
497
|
-
|
|
518
|
+
left: calc(var(--fl) * 156 / 256);
|
|
498
519
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
520
|
+
.family-loader [data-piece="bar"] {
|
|
521
|
+
left: calc(var(--fl) * 67 / 256);
|
|
522
|
+
top: calc(var(--fl) * 107 / 256);
|
|
523
|
+
width: calc(var(--fl) * 122 / 256);
|
|
524
|
+
height: calc(var(--fl) * 43 / 256);
|
|
525
|
+
border-radius: calc(var(--fl) * 14 / 256);
|
|
526
|
+
}
|
|
527
|
+
.family-loader [data-fill] {
|
|
528
|
+
position: absolute;
|
|
529
|
+
inset: 0;
|
|
530
|
+
will-change: transform;
|
|
531
|
+
}
|
|
532
|
+
.family-loader [data-piece="left"] [data-fill] {
|
|
533
|
+
background: linear-gradient(to bottom, #2c78fd, #7343fe);
|
|
534
|
+
animation: family-loader-rise 1.6s var(--ease-in-out) infinite both;
|
|
535
|
+
}
|
|
536
|
+
.family-loader [data-piece="right"] [data-fill] {
|
|
537
|
+
background: linear-gradient(to bottom, #fe5292, #ff9a46);
|
|
538
|
+
animation: family-loader-rise 1.6s var(--ease-in-out) 0.4s infinite both;
|
|
539
|
+
}
|
|
540
|
+
.family-loader [data-piece="bar"] [data-fill] {
|
|
541
|
+
background: linear-gradient(to right, #8145ff, #fe5292);
|
|
542
|
+
animation: family-loader-sweep 1.6s var(--ease-in-out) 0.2s infinite both;
|
|
543
|
+
}
|
|
544
|
+
/* Les jambes se remplissent par le bas, tiennent, puis la couleur continue de monter et laisse
|
|
545
|
+
la silhouette : le cycle suivant repart sans temps mort. */
|
|
546
|
+
@keyframes family-loader-rise {
|
|
505
547
|
0% {
|
|
506
|
-
|
|
507
|
-
opacity: 1;
|
|
508
|
-
animation-timing-function: var(--ease-out);
|
|
548
|
+
transform: translateY(101%);
|
|
509
549
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
opacity: 1;
|
|
514
|
-
animation-timing-function: var(--ease-out);
|
|
550
|
+
38%,
|
|
551
|
+
62% {
|
|
552
|
+
transform: translateY(0);
|
|
515
553
|
}
|
|
516
554
|
100% {
|
|
517
|
-
|
|
518
|
-
opacity: 0;
|
|
555
|
+
transform: translateY(-101%);
|
|
519
556
|
}
|
|
520
557
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
opacity: 1;
|
|
526
|
-
animation-timing-function: var(--ease-out);
|
|
558
|
+
/* La barre, de gauche à droite. */
|
|
559
|
+
@keyframes family-loader-sweep {
|
|
560
|
+
0% {
|
|
561
|
+
transform: translateX(-101%);
|
|
527
562
|
}
|
|
528
563
|
38%,
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
opacity: 1;
|
|
532
|
-
animation-timing-function: var(--ease-out);
|
|
564
|
+
62% {
|
|
565
|
+
transform: translateX(0);
|
|
533
566
|
}
|
|
534
567
|
100% {
|
|
535
|
-
|
|
536
|
-
opacity: 0;
|
|
568
|
+
transform: translateX(101%);
|
|
537
569
|
}
|
|
538
570
|
}
|
|
539
|
-
@keyframes family-loader-
|
|
571
|
+
@keyframes family-loader-glow {
|
|
540
572
|
0%,
|
|
541
|
-
32% {
|
|
542
|
-
clip-path: inset(100% 0 0 0);
|
|
543
|
-
opacity: 1;
|
|
544
|
-
animation-timing-function: var(--ease-out);
|
|
545
|
-
}
|
|
546
|
-
54%,
|
|
547
|
-
84% {
|
|
548
|
-
clip-path: inset(0 0 0 0);
|
|
549
|
-
opacity: 1;
|
|
550
|
-
animation-timing-function: var(--ease-out);
|
|
551
|
-
}
|
|
552
573
|
100% {
|
|
553
|
-
|
|
554
|
-
opacity: 0;
|
|
574
|
+
opacity: 0.25;
|
|
555
575
|
}
|
|
556
|
-
}
|
|
557
|
-
@keyframes family-loader-breathe {
|
|
558
576
|
50% {
|
|
559
|
-
opacity:
|
|
577
|
+
opacity: 1;
|
|
560
578
|
}
|
|
561
579
|
}
|
|
562
580
|
@media (prefers-reduced-motion: reduce) {
|
|
563
|
-
/*
|
|
564
|
-
.family-loader [data-piece] {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
.family-loader svg {
|
|
568
|
-
animation: family-loader-breathe 2.4s ease-in-out infinite;
|
|
581
|
+
/* La silhouette reste, le remplissage pulse en place, sans mouvement. */
|
|
582
|
+
.family-loader [data-piece] [data-fill] {
|
|
583
|
+
transform: none;
|
|
584
|
+
animation: family-loader-glow 1.6s ease-in-out infinite both;
|
|
569
585
|
}
|
|
570
586
|
}
|
|
587
|
+
/* Calque de chargement de la zone de contenu (`AppShell`) : opaque, au-dessus de l'ancienne page,
|
|
588
|
+
le H à une position fixe. Apparition courte, disparition plus courte encore. */
|
|
589
|
+
[data-slot="app-shell-loading"] {
|
|
590
|
+
opacity: 0;
|
|
591
|
+
visibility: hidden;
|
|
592
|
+
transition:
|
|
593
|
+
opacity 100ms var(--ease-out),
|
|
594
|
+
visibility 0s linear 100ms;
|
|
595
|
+
}
|
|
596
|
+
[data-slot="app-shell-loading"][data-visible] {
|
|
597
|
+
opacity: 1;
|
|
598
|
+
visibility: visible;
|
|
599
|
+
transition:
|
|
600
|
+
opacity 150ms var(--ease-out),
|
|
601
|
+
visibility 0s;
|
|
602
|
+
}
|