@hgdc/ui 0.12.2 → 0.13.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 -18
- package/dist/account-menu.d.ts +2 -1
- package/dist/account-menu.js +5 -2
- package/dist/app-shell.d.ts +8 -1
- package/dist/app-shell.js +5 -3
- package/dist/family-loader.d.ts +6 -5
- package/dist/family-loader.js +8 -7
- package/dist/page-pending.d.ts +27 -0
- package/dist/page-pending.js +29 -0
- package/dist/shell-placement.d.ts +6 -0
- package/dist/shell-placement.js +8 -0
- package/package.json +1 -1
- package/styles.css +7 -10
package/README.md
CHANGED
|
@@ -80,24 +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
|
|
100
|
-
| `
|
|
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…" />` : le H de la famille qui se construit sur son fantôme (jamais de trou), après 120 ms ; `page` le centre sous l'en-tête en taille `lg`. Dans un bouton : `Spinner` |
|
|
100
|
+
| `page-pending` | `createRouter({ routeTree, ...routerPendingDefaults })` : `PagePending` (le H centré) dès qu'une navigation attend plus de 120 ms, au moins 400 ms ; avec `AppShell contentKey={location.pathname}`, une page qui charge dans ses composants (`useSuspenseQuery`) le montre aussi au lieu de garder l'ancienne page |
|
|
101
|
+
| `skeleton` | `<Pending pending={isLoading} fallback={<TableSkeleton columns={3} />}>…</Pending>` : après 200 ms, tenu 400 ms au moins |
|
|
101
102
|
|
|
102
103
|
## Listes, journaux et retours
|
|
103
104
|
|
package/dist/account-menu.d.ts
CHANGED
|
@@ -25,7 +25,8 @@ export type AccountUser = {
|
|
|
25
25
|
* l'avatar seul, pour une barre du haut. Le produit passe les actions ; le thème passe par
|
|
26
26
|
* `ThemeProvider`.
|
|
27
27
|
*
|
|
28
|
-
* « Produits »
|
|
28
|
+
* « Produits » (masquée au-delà de `lg` dans une coque qui affiche la barre des produits, où ils
|
|
29
|
+
* sont déjà ; gardée sur téléphone) liste par défaut Account et les produits ouverts du catalogue
|
|
29
30
|
* (`productMenuEntries`, `@hgdc/ui/products`), avec `product` coché. `products` remplace cette
|
|
30
31
|
* liste ; `products={[]}` la retire.
|
|
31
32
|
*/
|
package/dist/account-menu.js
CHANGED
|
@@ -16,6 +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
20
|
import { useTheme } from "./theme.js";
|
|
20
21
|
const THEMES = ["light", "dark", "system"];
|
|
21
22
|
/**
|
|
@@ -24,18 +25,20 @@ const THEMES = ["light", "dark", "system"];
|
|
|
24
25
|
* l'avatar seul, pour une barre du haut. Le produit passe les actions ; le thème passe par
|
|
25
26
|
* `ThemeProvider`.
|
|
26
27
|
*
|
|
27
|
-
* « Produits »
|
|
28
|
+
* « Produits » (masquée au-delà de `lg` dans une coque qui affiche la barre des produits, où ils
|
|
29
|
+
* sont déjà ; gardée sur téléphone) liste par défaut Account et les produits ouverts du catalogue
|
|
28
30
|
* (`productMenuEntries`, `@hgdc/ui/products`), avec `product` coché. `products` remplace cette
|
|
29
31
|
* liste ; `products={[]}` la retire.
|
|
30
32
|
*/
|
|
31
33
|
export function AccountMenu({ user, onProfile, onSecurity, onShortcuts, onSignOut, products: productsProp, product: currentProduct, accountHref, children, variant = "row", }) {
|
|
32
34
|
const { theme, setTheme } = useTheme();
|
|
33
35
|
const products = productsProp ?? productMenuEntries({ current: currentProduct, accountHref });
|
|
36
|
+
const railShown = useProductRailShown();
|
|
34
37
|
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) => {
|
|
35
38
|
const next = THEMES.find((t) => t === value);
|
|
36
39
|
if (next)
|
|
37
40
|
setTheme(next);
|
|
38
|
-
}, 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, {}), _jsxs(DropdownMenuGroup, { 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"] })] })] }));
|
|
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"] })] })] }));
|
|
39
42
|
}
|
|
40
43
|
function ProductMenuIcon({ product }) {
|
|
41
44
|
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, mainClassName, }: {
|
|
47
|
+
export declare function AppShell({ product, organization, productRail, accountHref, nav, sidebarFooter, header, onSearch, searchLabel, children, contentKey, 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. */
|
|
@@ -73,6 +73,13 @@ export declare function AppShell({ product, organization, productRail, accountHr
|
|
|
73
73
|
/** Texte du champ de recherche : « Rechercher » par défaut. */
|
|
74
74
|
searchLabel?: string;
|
|
75
75
|
children: ReactNode;
|
|
76
|
+
/**
|
|
77
|
+
* Clé de la page affichée (le chemin, `location.pathname`) : la zone de contenu devient une
|
|
78
|
+
* frontière `Suspense` neuve à chaque page, qui montre `PagePending` pendant qu'une page charge
|
|
79
|
+
* ses données dans ses composants (`useSuspenseQuery`), au lieu de garder l'ancienne page figée
|
|
80
|
+
* pendant la transition de la navigation. Sans clé, pas de frontière.
|
|
81
|
+
*/
|
|
82
|
+
contentKey?: string | undefined;
|
|
76
83
|
/**
|
|
77
84
|
* @deprecated La largeur du contenu est la même dans toute la famille (`max-w-6xl`, ADR 0012) :
|
|
78
85
|
* une page étroite passe par `Page` (`@hgdc/ui/page-header`), pas par la coque.
|
package/dist/app-shell.js
CHANGED
|
@@ -4,13 +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, useContext, useEffect, useState, useSyncExternalStore } from "react";
|
|
7
|
+
import { createContext, Suspense, useContext, useEffect, 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
12
|
import { ProductBadge } from "./product-badge.js";
|
|
12
13
|
import { ProductRail } from "./product-rail.js";
|
|
13
14
|
import { HGDC_ACCOUNT, HGDC_PRODUCTS } from "./products.js";
|
|
15
|
+
import { ProductRailShownContext } from "./shell-placement.js";
|
|
14
16
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
|
|
15
17
|
import { cn } from "./cn.js";
|
|
16
18
|
// ── Rail : la barre latérale réduite aux icônes, mémorisée par navigateur ──────────────
|
|
@@ -129,10 +131,10 @@ function SidebarHead({ product }) {
|
|
|
129
131
|
* Le kit ne connaît ni le routeur ni l'authentification : le produit passe ses éléments
|
|
130
132
|
* (`OrgSwitcher`, `SidebarNav`, `AccountMenu`) et ses liens.
|
|
131
133
|
*/
|
|
132
|
-
export function AppShell({ product, organization, productRail = true, accountHref, nav, sidebarFooter, header, onSearch, searchLabel, children, mainClassName, }) {
|
|
134
|
+
export function AppShell({ product, organization, productRail = true, accountHref, nav, sidebarFooter, header, onSearch, searchLabel, children, contentKey, mainClassName, }) {
|
|
133
135
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
134
136
|
// Barre des produits élargie : la barre latérale se plie le temps du survol (ADR 0011).
|
|
135
137
|
const [railPeek, setRailPeek] = useState(false);
|
|
136
138
|
useToggleShortcut();
|
|
137
|
-
return (_jsx(ShellContext.Provider, { value: { closeMobileNav: () => setMenuOpen(false) }, 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", { className: "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("main", { id: "contenu", tabIndex: -1, 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: children })] }) }), _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] }) })] }) }));
|
|
139
|
+
return (_jsx(ShellContext.Provider, { value: { closeMobileNav: () => setMenuOpen(false) }, children: _jsx(ProductRailShownContext, { value: productRail, 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", { className: "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("main", { id: "contenu", tabIndex: -1, 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: 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] }) })] }) }) }));
|
|
138
140
|
}
|
package/dist/family-loader.d.ts
CHANGED
|
@@ -5,19 +5,20 @@ declare const SIZES: {
|
|
|
5
5
|
};
|
|
6
6
|
/**
|
|
7
7
|
* Le chargement d'une page : le H tricolore de la H family se construit, tient, s'efface, en
|
|
8
|
-
* boucle (CSS seul, `styles.css`)
|
|
9
|
-
*
|
|
8
|
+
* boucle (CSS seul, `styles.css`), par-dessus son fantôme (le H à 15 %, toujours visible : la
|
|
9
|
+
* boucle n'a jamais de trou). La forme et les dégradés sont ceux de `FAMILY_MARK_SVG`.
|
|
10
|
+
* Il n'apparaît qu'après 120 ms (`delay`) : une page rapide ne le montre jamais, et sa place est
|
|
10
11
|
* réservée dès le départ, rien ne saute. Mouvement réduit : le H entier respire.
|
|
11
12
|
*
|
|
12
|
-
* Pour une page ou une route entière (`
|
|
13
|
-
* une ligne, `Spinner` reste la règle. `page` le centre dans la
|
|
13
|
+
* Pour une page ou une route entière (`PagePending`, `routerPendingDefaults`) ; dans un bouton ou
|
|
14
|
+
* une ligne, `Spinner` reste la règle. `page` le centre dans la zone de contenu, en taille `lg`.
|
|
14
15
|
*/
|
|
15
16
|
export declare function FamilyLoader({ size, label, delay, page, className, }: {
|
|
16
17
|
size?: keyof typeof SIZES;
|
|
17
18
|
label?: string;
|
|
18
19
|
/** Attente avant d'apparaître, en millisecondes. */
|
|
19
20
|
delay?: number;
|
|
20
|
-
/** Centré dans
|
|
21
|
+
/** Centré dans la zone de contenu (sous l'en-tête de la coque), en taille `lg` par défaut. */
|
|
21
22
|
page?: boolean;
|
|
22
23
|
className?: string;
|
|
23
24
|
}): React.JSX.Element;
|
package/dist/family-loader.js
CHANGED
|
@@ -4,18 +4,19 @@ import { cn } from "./cn.js";
|
|
|
4
4
|
const SIZES = { sm: "size-6", md: "size-10", lg: "size-16" };
|
|
5
5
|
/**
|
|
6
6
|
* Le chargement d'une page : le H tricolore de la H family se construit, tient, s'efface, en
|
|
7
|
-
* boucle (CSS seul, `styles.css`)
|
|
8
|
-
*
|
|
7
|
+
* boucle (CSS seul, `styles.css`), par-dessus son fantôme (le H à 15 %, toujours visible : la
|
|
8
|
+
* boucle n'a jamais de trou). La forme et les dégradés sont ceux de `FAMILY_MARK_SVG`.
|
|
9
|
+
* Il n'apparaît qu'après 120 ms (`delay`) : une page rapide ne le montre jamais, et sa place est
|
|
9
10
|
* réservée dès le départ, rien ne saute. Mouvement réduit : le H entier respire.
|
|
10
11
|
*
|
|
11
|
-
* Pour une page ou une route entière (`
|
|
12
|
-
* une ligne, `Spinner` reste la règle. `page` le centre dans la
|
|
12
|
+
* Pour une page ou une route entière (`PagePending`, `routerPendingDefaults`) ; dans un bouton ou
|
|
13
|
+
* une ligne, `Spinner` reste la règle. `page` le centre dans la zone de contenu, en taille `lg`.
|
|
13
14
|
*/
|
|
14
|
-
export function FamilyLoader({ size
|
|
15
|
+
export function FamilyLoader({ size, label = "Chargement…", delay = 120, page = false, className, }) {
|
|
15
16
|
// Ids des dégradés propres à chaque instance : deux loaders ne se volent pas leurs couleurs.
|
|
16
17
|
const id = useId().replace(/[^\w-]/g, "");
|
|
17
|
-
const loader = (_jsx("span", { role: "status", "aria-label": label, "data-slot": "family-loader", className: cn("family-loader inline-grid shrink-0 place-items-center", SIZES[size], !page && className), style: { ["--family-loader-delay"]: `${delay}ms` }, children: _jsxs("svg", { viewBox: "0 0 256 256", "aria-hidden": true, className: "size-full overflow-visible", children: [_jsxs("defs", { children: [_jsxs("linearGradient", { id: `${id}l`, x1: "0", y1: "24", x2: "0", y2: "232", gradientUnits: "userSpaceOnUse", children: [_jsx("stop", { offset: "0", stopColor: "#2c78fd" }), _jsx("stop", { offset: "1", stopColor: "#7343fe" })] }), _jsxs("linearGradient", { id: `${id}r`, x1: "0", y1: "24", x2: "0", y2: "232", gradientUnits: "userSpaceOnUse", children: [_jsx("stop", { offset: "0", stopColor: "#fe5292" }), _jsx("stop", { offset: "1", stopColor: "#ff9a46" })] }), _jsxs("linearGradient", { id: `${id}b`, x1: "64", y1: "0", x2: "192", y2: "0", gradientUnits: "userSpaceOnUse", children: [_jsx("stop", { offset: "0", stopColor: "#8145ff" }), _jsx("stop", { offset: "1", stopColor: "#fe5292" })] })] }), _jsx("rect", { "data-piece": "left", x: "43", y: "24", width: "57", height: "208", rx: "18", fill: `url(#${id}l)` }), _jsx("rect", { "data-piece": "right", x: "156", y: "24", width: "57", height: "208", rx: "18", fill: `url(#${id}r)` }), _jsx("rect", { "data-piece": "bar", x: "67", y: "107", width: "122", height: "43", rx: "14", fill: `url(#${id}b)` })] }) }));
|
|
18
|
+
const loader = (_jsx("span", { role: "status", "aria-label": label, "data-slot": "family-loader", className: cn("family-loader inline-grid shrink-0 place-items-center", SIZES[size ?? (page ? "lg" : "md")], !page && className), style: { ["--family-loader-delay"]: `${delay}ms` }, children: _jsxs("svg", { viewBox: "0 0 256 256", "aria-hidden": true, className: "size-full overflow-visible", children: [_jsxs("defs", { children: [_jsxs("linearGradient", { id: `${id}l`, x1: "0", y1: "24", x2: "0", y2: "232", gradientUnits: "userSpaceOnUse", children: [_jsx("stop", { offset: "0", stopColor: "#2c78fd" }), _jsx("stop", { offset: "1", stopColor: "#7343fe" })] }), _jsxs("linearGradient", { id: `${id}r`, x1: "0", y1: "24", x2: "0", y2: "232", gradientUnits: "userSpaceOnUse", children: [_jsx("stop", { offset: "0", stopColor: "#fe5292" }), _jsx("stop", { offset: "1", stopColor: "#ff9a46" })] }), _jsxs("linearGradient", { id: `${id}b`, x1: "64", y1: "0", x2: "192", y2: "0", gradientUnits: "userSpaceOnUse", children: [_jsx("stop", { offset: "0", stopColor: "#8145ff" }), _jsx("stop", { offset: "1", stopColor: "#fe5292" })] })] }), _jsxs("g", { "data-ghost": true, opacity: "0.15", children: [_jsx("rect", { x: "43", y: "24", width: "57", height: "208", rx: "18", fill: `url(#${id}l)` }), _jsx("rect", { x: "156", y: "24", width: "57", height: "208", rx: "18", fill: `url(#${id}r)` }), _jsx("rect", { x: "67", y: "107", width: "122", height: "43", rx: "14", fill: `url(#${id}b)` })] }), _jsx("rect", { "data-piece": "left", x: "43", y: "24", width: "57", height: "208", rx: "18", fill: `url(#${id}l)` }), _jsx("rect", { "data-piece": "right", x: "156", y: "24", width: "57", height: "208", rx: "18", fill: `url(#${id}r)` }), _jsx("rect", { "data-piece": "bar", x: "67", y: "107", width: "122", height: "43", rx: "14", fill: `url(#${id}b)` })] }) }));
|
|
18
19
|
if (!page)
|
|
19
20
|
return loader;
|
|
20
|
-
return (_jsx("div", { "data-slot": "family-loader-page", className: cn("grid min-h-[
|
|
21
|
+
return (_jsx("div", { "data-slot": "family-loader-page", className: cn("grid min-h-[calc(100dvh-10rem)] w-full place-items-center", className), children: loader }));
|
|
21
22
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* Le chargement d'une page dans la zone de contenu : le H de la famille, centré sous l'en-tête,
|
|
7
|
+
* la coque restant en place. Composant de `pendingComponent` (TanStack Router) et repli de la
|
|
8
|
+
* frontière `Suspense` de la coque (`AppShell`, `contentKey`). Sans délai propre : celui de la
|
|
9
|
+
* route (`PAGE_PENDING_MS`) suffit.
|
|
10
|
+
*/
|
|
11
|
+
export declare function PagePending({ label }: {
|
|
12
|
+
label?: string;
|
|
13
|
+
}): React.JSX.Element;
|
|
14
|
+
/**
|
|
15
|
+
* Réglages communs de `createRouter` (TanStack Router) : dès qu'une navigation attend ses données
|
|
16
|
+
* plus de 120 ms, la zone de contenu montre `PagePending` au lieu de garder l'ancienne page figée,
|
|
17
|
+
* au moins 400 ms. Le défaut de TanStack Router attend 1 s.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* createRouter({ routeTree, ...routerPendingDefaults })
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare const routerPendingDefaults: {
|
|
24
|
+
defaultPendingComponent: typeof PagePending;
|
|
25
|
+
defaultPendingMs: number;
|
|
26
|
+
defaultPendingMinMs: number;
|
|
27
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
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
|
+
/**
|
|
8
|
+
* Le chargement d'une page dans la zone de contenu : le H de la famille, centré sous l'en-tête,
|
|
9
|
+
* la coque restant en place. Composant de `pendingComponent` (TanStack Router) et repli de la
|
|
10
|
+
* frontière `Suspense` de la coque (`AppShell`, `contentKey`). Sans délai propre : celui de la
|
|
11
|
+
* route (`PAGE_PENDING_MS`) suffit.
|
|
12
|
+
*/
|
|
13
|
+
export function PagePending({ label = "Chargement…" }) {
|
|
14
|
+
return _jsx(FamilyLoader, { page: true, delay: 0, label: label });
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Réglages communs de `createRouter` (TanStack Router) : dès qu'une navigation attend ses données
|
|
18
|
+
* plus de 120 ms, la zone de contenu montre `PagePending` au lieu de garder l'ancienne page figée,
|
|
19
|
+
* au moins 400 ms. Le défaut de TanStack Router attend 1 s.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* createRouter({ routeTree, ...routerPendingDefaults })
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export const routerPendingDefaults = {
|
|
26
|
+
defaultPendingComponent: PagePending,
|
|
27
|
+
defaultPendingMs: PAGE_PENDING_MS,
|
|
28
|
+
defaultPendingMinMs: PAGE_PENDING_MIN_MS,
|
|
29
|
+
};
|
|
@@ -5,3 +5,9 @@
|
|
|
5
5
|
export type ShellPlacement = "sidebar" | "product-rail";
|
|
6
6
|
export declare const ShellPlacementContext: React.Context<ShellPlacement>;
|
|
7
7
|
export declare function useShellPlacement(): ShellPlacement;
|
|
8
|
+
/**
|
|
9
|
+
* Vrai quand la coque affiche la barre des produits (sur ordinateur) : les produits y sont déjà,
|
|
10
|
+
* le menu du compte ne les répète pas au-delà de `lg`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const ProductRailShownContext: React.Context<boolean>;
|
|
13
|
+
export declare function useProductRailShown(): boolean;
|
package/dist/shell-placement.js
CHANGED
|
@@ -3,3 +3,11 @@ export const ShellPlacementContext = createContext("sidebar");
|
|
|
3
3
|
export function useShellPlacement() {
|
|
4
4
|
return useContext(ShellPlacementContext);
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Vrai quand la coque affiche la barre des produits (sur ordinateur) : les produits y sont déjà,
|
|
8
|
+
* le menu du compte ne les répète pas au-delà de `lg`.
|
|
9
|
+
*/
|
|
10
|
+
export const ProductRailShownContext = createContext(false);
|
|
11
|
+
export function useProductRailShown() {
|
|
12
|
+
return useContext(ProductRailShownContext);
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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
|
@@ -483,7 +483,7 @@
|
|
|
483
483
|
léger chevauchement lie les trois gestes. Visible après 200 ms seulement : pas de flash. */
|
|
484
484
|
.family-loader {
|
|
485
485
|
opacity: 0;
|
|
486
|
-
animation: family-loader-in var(--duration-base) var(--ease-out) var(--family-loader-delay,
|
|
486
|
+
animation: family-loader-in var(--duration-base) var(--ease-out) var(--family-loader-delay, 120ms) forwards;
|
|
487
487
|
}
|
|
488
488
|
/* Le nom dans chaque raccourci : sans lui, lightningcss (minification de Vite) réduit
|
|
489
489
|
`animation: 1.8s infinite both` à `animation: none`. */
|
|
@@ -507,13 +507,12 @@
|
|
|
507
507
|
opacity: 1;
|
|
508
508
|
animation-timing-function: var(--ease-out);
|
|
509
509
|
}
|
|
510
|
-
|
|
511
|
-
|
|
510
|
+
22%,
|
|
511
|
+
76% {
|
|
512
512
|
clip-path: inset(0 0 0 0);
|
|
513
513
|
opacity: 1;
|
|
514
514
|
animation-timing-function: var(--ease-out);
|
|
515
515
|
}
|
|
516
|
-
90%,
|
|
517
516
|
100% {
|
|
518
517
|
clip-path: inset(0 0 0 0);
|
|
519
518
|
opacity: 0;
|
|
@@ -526,13 +525,12 @@
|
|
|
526
525
|
opacity: 1;
|
|
527
526
|
animation-timing-function: var(--ease-out);
|
|
528
527
|
}
|
|
529
|
-
|
|
530
|
-
|
|
528
|
+
38%,
|
|
529
|
+
80% {
|
|
531
530
|
clip-path: inset(0 0 0 0);
|
|
532
531
|
opacity: 1;
|
|
533
532
|
animation-timing-function: var(--ease-out);
|
|
534
533
|
}
|
|
535
|
-
93%,
|
|
536
534
|
100% {
|
|
537
535
|
clip-path: inset(0 0 0 0);
|
|
538
536
|
opacity: 0;
|
|
@@ -545,13 +543,12 @@
|
|
|
545
543
|
opacity: 1;
|
|
546
544
|
animation-timing-function: var(--ease-out);
|
|
547
545
|
}
|
|
548
|
-
|
|
549
|
-
|
|
546
|
+
54%,
|
|
547
|
+
84% {
|
|
550
548
|
clip-path: inset(0 0 0 0);
|
|
551
549
|
opacity: 1;
|
|
552
550
|
animation-timing-function: var(--ease-out);
|
|
553
551
|
}
|
|
554
|
-
96%,
|
|
555
552
|
100% {
|
|
556
553
|
clip-path: inset(0 0 0 0);
|
|
557
554
|
opacity: 0;
|