@hgdc/ui 0.12.1 → 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 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 page />` en `pendingComponent` d'une route, `<FamilyLoader size="lg" label="Ouverture du projet…" />` : le H de la famille qui se construit, après 200 ms. Dans un bouton : `Spinner` |
100
- | `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…" />` : 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
 
@@ -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 » liste par défaut Account et les produits ouverts du catalogue
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
  */
@@ -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 » liste par défaut Account et les produits ouverts du catalogue
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)
@@ -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
  }
@@ -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`). La forme et les dégradés sont ceux de `FAMILY_MARK_SVG`.
9
- * Il n'apparaît qu'après 200 ms (`delay`) : une page rapide ne le montre jamais, et sa place est
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 (`pendingComponent`, `Pending` de page) ; dans un bouton ou
13
- * une ligne, `Spinner` reste la règle. `page` le centre dans la hauteur visible.
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 un bloc de la hauteur de l'écran, pour un chargement de page. */
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;
@@ -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`). La forme et les dégradés sont ceux de `FAMILY_MARK_SVG`.
8
- * Il n'apparaît qu'après 200 ms (`delay`) : une page rapide ne le montre jamais, et sa place est
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 (`pendingComponent`, `Pending` de page) ; dans un bouton ou
12
- * une ligne, `Spinner` reste la règle. `page` le centre dans la hauteur visible.
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 = "md", label = "Chargement…", delay = 200, page = false, className, }) {
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-[60dvh] w-full place-items-center", className), children: loader }));
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
+ };
@@ -8,7 +8,8 @@ export declare const PRODUCT_RAIL_OPEN_DELAY = 150;
8
8
  * d'intention) ou au focus clavier, elle s'élargit et écrit le nom de chaque produit. Elle ne
9
9
  * recouvre jamais la page : `AppShell` plie la barre latérale du produit le temps du survol, la
10
10
  * barre prend exactement la place libérée (248 px) ; si la barre latérale est déjà pliée, elle
11
- * pousse la page (220 px). Tout revient dès que la souris sort.
11
+ * pousse la page (220 px). Un menu ouvert depuis la barre (sélecteur d'organisation) la garde
12
+ * élargie ; tout revient quand la souris est sortie et que le menu est fermé.
12
13
  */
13
14
  export declare function ProductRail({ current, organization, accountHref, catalog, onExpandedChange, }: {
14
15
  /** Le mot du produit affiché (« host ») : marqué en teinte. */
@@ -15,17 +15,24 @@ function ProductLink({ entry }) {
15
15
  * d'intention) ou au focus clavier, elle s'élargit et écrit le nom de chaque produit. Elle ne
16
16
  * recouvre jamais la page : `AppShell` plie la barre latérale du produit le temps du survol, la
17
17
  * barre prend exactement la place libérée (248 px) ; si la barre latérale est déjà pliée, elle
18
- * pousse la page (220 px). Tout revient dès que la souris sort.
18
+ * pousse la page (220 px). Un menu ouvert depuis la barre (sélecteur d'organisation) la garde
19
+ * élargie ; tout revient quand la souris est sortie et que le menu est fermé.
19
20
  */
20
21
  export function ProductRail({ current, organization, accountHref, catalog, onExpandedChange, }) {
21
- const [expanded, setExpanded] = useState(false);
22
+ // Trois raisons de rester élargie : le survol (après le délai d'intention), le focus clavier, et
23
+ // un menu ouvert depuis la barre (sélecteur d'organisation ou tout autre) : la souris qui quitte
24
+ // la barre pour aller dans le menu ne doit pas la replier sous elle.
25
+ const [hovered, setHovered] = useState(false);
26
+ const [focused, setFocused] = useState(false);
27
+ const [menuOpen, setMenuOpen] = useState(false);
28
+ const expanded = hovered || focused || menuOpen;
22
29
  useEffect(() => {
23
30
  onExpandedChange?.(expanded);
24
31
  }, [expanded, onExpandedChange]);
25
32
  const ref = useRef(null);
26
33
  const { products, account } = productRailEntries({ current, accountHref, catalog });
27
- // Écouteurs natifs : la barre est un repère `nav`, pas un contrôle ; c'est son survol et le
28
- // focus de ses liens qui l'élargissent.
34
+ // Écouteurs natifs : la barre est un repère `nav`, pas un contrôle ; c'est son survol, le focus
35
+ // de ses liens et les menus qu'elle ouvre qui l'élargissent.
29
36
  useEffect(() => {
30
37
  const rail = ref.current;
31
38
  if (!rail)
@@ -39,27 +46,44 @@ export function ProductRail({ current, organization, accountHref, catalog, onExp
39
46
  if (event.pointerType !== "mouse")
40
47
  return;
41
48
  cancel();
42
- timer = window.setTimeout(() => setExpanded(true), PRODUCT_RAIL_OPEN_DELAY);
49
+ timer = window.setTimeout(() => setHovered(true), PRODUCT_RAIL_OPEN_DELAY);
43
50
  };
44
51
  const onLeave = () => {
45
52
  cancel();
46
- setExpanded(false);
53
+ setHovered(false);
54
+ };
55
+ // Au clavier seulement : un clic de souris ne laisse pas la barre ouverte, ni le focus rendu au
56
+ // déclencheur quand un menu ouvert à la souris se ferme. La dernière interaction fait foi.
57
+ let keyboard = false;
58
+ const onDocKey = () => {
59
+ keyboard = true;
60
+ };
61
+ const onDocPointer = () => {
62
+ keyboard = false;
47
63
  };
48
- // Au clavier seulement : un clic de souris ne laisse pas la barre ouverte.
49
64
  const onFocusIn = (event) => {
50
- if (event.target && focusVisible(event.target))
51
- setExpanded(true);
65
+ if (keyboard && event.target && focusVisible(event.target))
66
+ setFocused(true);
52
67
  };
53
68
  const onFocusOut = (event) => {
54
69
  if (!(event.relatedTarget instanceof Node) || !rail.contains(event.relatedTarget))
55
- setExpanded(false);
70
+ setFocused(false);
56
71
  };
57
72
  const onKey = (event) => {
58
73
  if (event.key !== "Escape")
59
74
  return;
60
75
  cancel();
61
- setExpanded(false);
76
+ setHovered(false);
77
+ setFocused(false);
62
78
  };
79
+ // Un menu ouvert depuis la barre : son déclencheur porte `aria-expanded="true"` (Base UI, Radix,
80
+ // natif). Le menu lui-même vit dans un portail, hors de la barre : on suit donc le déclencheur.
81
+ const syncMenu = () => setMenuOpen(rail.querySelector('[aria-expanded="true"]') !== null);
82
+ const observer = new MutationObserver(syncMenu);
83
+ observer.observe(rail, { subtree: true, attributes: true, attributeFilter: ["aria-expanded"], childList: true });
84
+ syncMenu();
85
+ document.addEventListener("keydown", onDocKey, true);
86
+ document.addEventListener("pointerdown", onDocPointer, true);
63
87
  rail.addEventListener("pointerenter", onEnter);
64
88
  rail.addEventListener("pointerleave", onLeave);
65
89
  rail.addEventListener("focusin", onFocusIn);
@@ -67,6 +91,9 @@ export function ProductRail({ current, organization, accountHref, catalog, onExp
67
91
  rail.addEventListener("keydown", onKey);
68
92
  return () => {
69
93
  cancel();
94
+ observer.disconnect();
95
+ document.removeEventListener("keydown", onDocKey, true);
96
+ document.removeEventListener("pointerdown", onDocPointer, true);
70
97
  rail.removeEventListener("pointerenter", onEnter);
71
98
  rail.removeEventListener("pointerleave", onLeave);
72
99
  rail.removeEventListener("focusin", onFocusIn);
@@ -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;
@@ -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.12.1",
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, 200ms) forwards;
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
- 24%,
511
- 74% {
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
- 40%,
530
- 77% {
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
- 56%,
549
- 80% {
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;