@hgdc/ui 0.11.2 → 0.12.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
@@ -96,6 +96,7 @@ Sur ordinateur, la coque ouvre sur la barre des produits (ADR 0011) : organisati
96
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
97
  | `rank-bar` | `<RankBar label="Pages les plus vues" items={[{ key, label: "/tarifs", value: 640, render: <Link … /> }]} limit={5} />` |
98
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` |
99
100
  | `skeleton` | `<Pending pending={isLoading} fallback={<TableSkeleton columns={3} />}>…</Pending>` : après 200 ms, tenu 400 ms au moins |
100
101
 
101
102
  ## Listes, journaux et retours
@@ -35,8 +35,8 @@ export declare function SidebarNavItem({ icon, label, active, href, render, }: {
35
35
  }): React.JSX.Element;
36
36
  /**
37
37
  * La coque commune des consoles HGDC, celle de Host. Sur ordinateur, de gauche à droite : la barre
38
- * des produits de la famille (organisation en haut, pastilles, Account en bas, élargie au survol
39
- * par-dessus la page), la barre latérale du produit (sa marque, sa navigation, le compte en bas),
38
+ * des produits de la famille (organisation, Account, pastilles ; élargie au survol à la place
39
+ * de la barre latérale, qui se plie le temps du survol), la barre latérale du produit (sa marque, sa navigation, le compte en bas),
40
40
  * réductible en rail (`Ctrl+B`, mémorisé), et la page sur une carte. Sur téléphone : une barre en
41
41
  * haut, le menu dans un tiroir qui monte du bas et se referme quand on choisit une page ; les
42
42
  * produits restent dans le menu de l'avatar.
package/dist/app-shell.js CHANGED
@@ -120,8 +120,8 @@ function SidebarHead({ product }) {
120
120
  }
121
121
  /**
122
122
  * La coque commune des consoles HGDC, celle de Host. Sur ordinateur, de gauche à droite : la barre
123
- * des produits de la famille (organisation en haut, pastilles, Account en bas, élargie au survol
124
- * par-dessus la page), la barre latérale du produit (sa marque, sa navigation, le compte en bas),
123
+ * des produits de la famille (organisation, Account, pastilles ; élargie au survol à la place
124
+ * de la barre latérale, qui se plie le temps du survol), la barre latérale du produit (sa marque, sa navigation, le compte en bas),
125
125
  * réductible en rail (`Ctrl+B`, mémorisé), et la page sur une carte. Sur téléphone : une barre en
126
126
  * haut, le menu dans un tiroir qui monte du bas et se referme quand on choisit une page ; les
127
127
  * produits restent dans le menu de l'avatar.
@@ -131,6 +131,8 @@ function SidebarHead({ product }) {
131
131
  */
132
132
  export function AppShell({ product, organization, productRail = true, accountHref, nav, sidebarFooter, header, onSearch, searchLabel, children, mainClassName, }) {
133
133
  const [menuOpen, setMenuOpen] = useState(false);
134
+ // Barre des produits élargie : la barre latérale se plie le temps du survol (ADR 0011).
135
+ const [railPeek, setRailPeek] = useState(false);
134
136
  useToggleShortcut();
135
- return (_jsx(ShellContext.Provider, { value: { closeMobileNav: () => setMenuOpen(false) }, children: _jsxs("div", { "data-slot": "app-shell", 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 }) : null, _jsxs("aside", { "aria-label": "Barre lat\u00E9rale", className: "group/sidebar hidden overflow-hidden text-shell-foreground transition-[width] duration-(--duration-drawer) 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] }) })] }) }));
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] }) })] }) }));
136
138
  }
package/dist/checkbox.js CHANGED
@@ -15,7 +15,7 @@ import { cn } from "./cn.js";
15
15
  * ```
16
16
  */
17
17
  export function Checkbox({ className, indeterminate, ...props }) {
18
- return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", indeterminate: indeterminate, className: cn("touch-target peer grid size-5 shrink-0 place-items-center rounded-[0.4rem] border border-foreground/25 bg-background text-primary-foreground", "transition-[background-color,border-color,box-shadow] duration-(--duration-fast) ease-out outline-none", "hover:border-foreground/40 focus-visible:ring-3 focus-visible:ring-ring/50", "data-checked:border-primary data-checked:bg-primary data-indeterminate:border-primary data-indeterminate:bg-primary", "data-disabled:cursor-not-allowed data-disabled:opacity-50", "aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-invalid:border-destructive",
18
+ return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", indeterminate: indeterminate, className: cn("touch-target peer grid size-5 shrink-0 place-items-center rounded-[0.4rem] border border-control-off bg-background text-primary-foreground", "transition-[background-color,border-color,box-shadow] duration-(--duration-fast) ease-out outline-none", "hover:border-foreground/70 focus-visible:ring-3 focus-visible:ring-ring/50", "data-checked:border-primary data-checked:bg-primary data-indeterminate:border-primary data-indeterminate:bg-primary", "data-disabled:cursor-not-allowed data-disabled:opacity-50", "aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-invalid:border-destructive",
19
19
  // Le fond sombre de la case vide ne doit jamais recouvrir le fond coché (même spécificité).
20
20
  "dark:bg-white/[0.04] dark:data-checked:bg-primary dark:data-indeterminate:bg-primary", className), ...props, children: _jsx(CheckboxPrimitive.Indicator, { className: "grid place-items-center data-unchecked:hidden", children: _jsx(HugeiconsIcon, { icon: indeterminate ? MinusSignIcon : Tick02Icon, strokeWidth: 2.5, className: "size-3.5" }) }) }));
21
21
  }
@@ -0,0 +1,24 @@
1
+ declare const SIZES: {
2
+ readonly sm: "size-6";
3
+ readonly md: "size-10";
4
+ readonly lg: "size-16";
5
+ };
6
+ /**
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
10
+ * réservée dès le départ, rien ne saute. Mouvement réduit : le H entier respire.
11
+ *
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.
14
+ */
15
+ export declare function FamilyLoader({ size, label, delay, page, className, }: {
16
+ size?: keyof typeof SIZES;
17
+ label?: string;
18
+ /** Attente avant d'apparaître, en millisecondes. */
19
+ delay?: number;
20
+ /** Centré dans un bloc de la hauteur de l'écran, pour un chargement de page. */
21
+ page?: boolean;
22
+ className?: string;
23
+ }): React.JSX.Element;
24
+ export {};
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from "react";
3
+ import { cn } from "./cn.js";
4
+ const SIZES = { sm: "size-6", md: "size-10", lg: "size-16" };
5
+ /**
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
9
+ * réservée dès le départ, rien ne saute. Mouvement réduit : le H entier respire.
10
+ *
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.
13
+ */
14
+ export function FamilyLoader({ size = "md", label = "Chargement…", delay = 200, page = false, className, }) {
15
+ // Ids des dégradés propres à chaque instance : deux loaders ne se volent pas leurs couleurs.
16
+ 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
+ if (!page)
19
+ 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
+ }
@@ -4,11 +4,13 @@ import { type HgdcCatalogProduct } from "./products.ts";
4
4
  export declare const PRODUCT_RAIL_OPEN_DELAY = 150;
5
5
  /**
6
6
  * La barre des produits de la famille, tout à gauche de la coque sur ordinateur : organisation en
7
- * haut, pastilles des produits, Account en bas. 56 px de large ; au survol (après un court délai
8
- * d'intention) ou au focus clavier, elle s'élargit par-dessus la page et écrit le nom de chaque
9
- * produit, sans rien décaler. Elle se referme dès que la souris sort.
7
+ * haut, Account, puis les pastilles des produits. 56 px de large ; au survol (après un court délai
8
+ * d'intention) ou au focus clavier, elle s'élargit et écrit le nom de chaque produit. Elle ne
9
+ * recouvre jamais la page : `AppShell` plie la barre latérale du produit le temps du survol, la
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.
10
12
  */
11
- export declare function ProductRail({ current, organization, accountHref, catalog, }: {
13
+ export declare function ProductRail({ current, organization, accountHref, catalog, onExpandedChange, }: {
12
14
  /** Le mot du produit affiché (« host ») : marqué en teinte. */
13
15
  current?: string | undefined;
14
16
  /** `OrgSwitcher`, rendu compact. */
@@ -16,4 +18,6 @@ export declare function ProductRail({ current, organization, accountHref, catalo
16
18
  /** Adresse d'Account (un Account local en développement). */
17
19
  accountHref?: string | undefined;
18
20
  catalog?: ReadonlyArray<HgdcCatalogProduct> | undefined;
21
+ /** Prévenu à chaque ouverture et fermeture : `AppShell` y plie sa barre latérale. */
22
+ onExpandedChange?: ((expanded: boolean) => void) | undefined;
19
23
  }): React.JSX.Element;
@@ -11,12 +11,17 @@ function ProductLink({ entry }) {
11
11
  }
12
12
  /**
13
13
  * La barre des produits de la famille, tout à gauche de la coque sur ordinateur : organisation en
14
- * haut, pastilles des produits, Account en bas. 56 px de large ; au survol (après un court délai
15
- * d'intention) ou au focus clavier, elle s'élargit par-dessus la page et écrit le nom de chaque
16
- * produit, sans rien décaler. Elle se referme dès que la souris sort.
14
+ * haut, Account, puis les pastilles des produits. 56 px de large ; au survol (après un court délai
15
+ * d'intention) ou au focus clavier, elle s'élargit et écrit le nom de chaque produit. Elle ne
16
+ * recouvre jamais la page : `AppShell` plie la barre latérale du produit le temps du survol, la
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.
17
19
  */
18
- export function ProductRail({ current, organization, accountHref, catalog, }) {
20
+ export function ProductRail({ current, organization, accountHref, catalog, onExpandedChange, }) {
19
21
  const [expanded, setExpanded] = useState(false);
22
+ useEffect(() => {
23
+ onExpandedChange?.(expanded);
24
+ }, [expanded, onExpandedChange]);
20
25
  const ref = useRef(null);
21
26
  const { products, account } = productRailEntries({ current, accountHref, catalog });
22
27
  // Écouteurs natifs : la barre est un repère `nav`, pas un contrôle ; c'est son survol et le
@@ -69,9 +74,10 @@ export function ProductRail({ current, organization, accountHref, catalog, }) {
69
74
  rail.removeEventListener("keydown", onKey);
70
75
  };
71
76
  }, []);
72
- return (_jsx("div", { "data-slot": "product-rail", className: "relative hidden w-14 shrink-0 lg:block", children: _jsxs("nav", { ref: ref, "aria-label": "Produits", "data-expanded": expanded ? "" : undefined, className: cn("group/rail absolute inset-y-0 left-0 z-40 flex w-14 flex-col gap-2 overflow-hidden bg-shell-rail px-2 py-3 text-shell-foreground", "transition-[width,box-shadow] duration-(--duration-base) ease-ios motion-reduce:transition-none",
73
- // L'ombre seulement quand elle flotte ; en sombre, où l'ombre ne se voit pas, un filet.
74
- "data-expanded:w-55 data-expanded:shadow-xl data-expanded:shadow-black/10 dark:data-expanded:shadow-black/50 dark:data-expanded:ring-1 dark:data-expanded:ring-white/8"), children: [organization ? (_jsxs(_Fragment, { children: [_jsx(ShellPlacementContext, { value: "product-rail", children: organization }), _jsx("div", { "aria-hidden": true, className: "mx-2 h-px shrink-0 bg-foreground/8" })] })) : null, _jsx("ul", { className: "grid min-h-0 gap-1 overflow-x-hidden overflow-y-auto", children: products.map((entry) => (_jsx(ProductLink, { entry: entry }, entry.badge))) }), _jsx("ul", { className: "mt-auto grid", children: _jsx(ProductLink, { entry: account }) })] }) }));
77
+ return (_jsx("div", { "data-slot": "product-rail", "data-expanded": expanded ? "" : undefined, className: cn("hidden w-14 shrink-0 overflow-hidden lg:block",
78
+ // Même durée et même courbe que la barre latérale qui se plie : la somme des deux largeurs
79
+ // reste constante (56 + 256 = 248 + 64), la page ne bouge pas.
80
+ "transition-[width] duration-(--duration-base) ease-ios motion-reduce:transition-none", "data-expanded:w-62 folded:data-expanded:w-55"), children: _jsxs("nav", { ref: ref, "aria-label": "Produits", "data-expanded": expanded ? "" : undefined, className: "group/rail flex h-full w-full flex-col gap-2 overflow-hidden bg-shell-rail px-2 py-3 text-shell-foreground", children: [organization ? (_jsxs(_Fragment, { children: [_jsx(ShellPlacementContext, { value: "product-rail", children: organization }), _jsx("div", { "aria-hidden": true, className: "mx-2 h-px shrink-0 bg-foreground/8" })] })) : null, _jsxs("ul", { className: "grid min-h-0 gap-1 overflow-x-hidden overflow-y-auto", children: [_jsx(ProductLink, { entry: account }), products.map((entry) => (_jsx(ProductLink, { entry: entry }, entry.badge)))] })] }) }));
75
81
  }
76
82
  function focusVisible(target) {
77
83
  if (!(target instanceof Element))
@@ -16,7 +16,9 @@ export type HgdcCatalogProduct = {
16
16
  href: string;
17
17
  /**
18
18
  * Entrée dans l'app avec la session HGDC Account (`PRODUCT_ENTRY_PATH`), sans page d'accueil
19
- * ni bouton de connexion. Pour Account, sa racine.
19
+ * ni bouton de connexion : `https://<mot>.hgdc.ch/auth/hgdc?refresh=1&next=/`. `refresh=1`
20
+ * relit les organisations au passage (une organisation rejointe dans Account apparaît sans se
21
+ * reconnecter, ADR 0013). Pour Account, sa racine.
20
22
  */
21
23
  entryHref: string;
22
24
  /** Une phrase, au tutoiement. */
@@ -38,6 +40,8 @@ export type HgdcCatalogProduct = {
38
40
  * quand cette session existe.
39
41
  */
40
42
  export declare const PRODUCT_ENTRY_PATH = "/auth/hgdc";
43
+ /** Requête de l'entrée depuis un autre produit : relire les organisations, arriver sur l'accueil. */
44
+ export declare const PRODUCT_ENTRY_REFRESH_QUERY = "refresh=1&next=/";
41
45
  export declare const HGDC_ACCOUNT: HgdcCatalogProduct;
42
46
  /** Les produits de la famille, dans leur ordre d'affichage. Account n'en fait pas partie (`HGDC_ACCOUNT`). */
43
47
  export declare const HGDC_PRODUCTS: ReadonlyArray<HgdcCatalogProduct>;
@@ -77,10 +81,10 @@ export type ProductRailEntry = {
77
81
  hint?: string;
78
82
  };
79
83
  /**
80
- * Les entrées de la barre des produits : les produits ouverts du catalogue, puis Account, à part
81
- * en bas (le compte commun, pas un produit). Chaque produit pointe vers son entrée
82
- * `/auth/hgdc?next=/` (ADR 0010), Account vers sa racine (`accountHref` en développement). Le
83
- * produit courant pointe vers `/`, sans détour par l'entrée.
84
+ * Les entrées de la barre des produits : les produits ouverts du catalogue et Account, affiché en
85
+ * tête (le compte commun, pas un produit). Chaque produit pointe vers son entrée
86
+ * `/auth/hgdc?refresh=1&next=/` (ADR 0010, 0013), Account vers sa racine (`accountHref` en
87
+ * développement). Le produit courant pointe vers `/`, sans détour par l'entrée.
84
88
  */
85
89
  export declare function productRailEntries(options?: {
86
90
  current?: string | undefined;
package/dist/products.js CHANGED
@@ -18,6 +18,8 @@ export const PRODUCT_STAGE_LABEL = {
18
18
  * quand cette session existe.
19
19
  */
20
20
  export const PRODUCT_ENTRY_PATH = "/auth/hgdc";
21
+ /** Requête de l'entrée depuis un autre produit : relire les organisations, arriver sur l'accueil. */
22
+ export const PRODUCT_ENTRY_REFRESH_QUERY = "refresh=1&next=/";
21
23
  function product(word, pitch, icon, stage) {
22
24
  const badge = word.toLowerCase();
23
25
  if (!isFamilyProduct(badge))
@@ -29,7 +31,7 @@ function product(word, pitch, icon, stage) {
29
31
  name: word,
30
32
  subdomain,
31
33
  href,
32
- entryHref: `${href}${PRODUCT_ENTRY_PATH}`,
34
+ entryHref: `${href}${PRODUCT_ENTRY_PATH}?${PRODUCT_ENTRY_REFRESH_QUERY}`,
33
35
  pitch,
34
36
  badge,
35
37
  icon,
@@ -73,10 +75,10 @@ export function productMenuEntries(options = {}) {
73
75
  }));
74
76
  }
75
77
  /**
76
- * Les entrées de la barre des produits : les produits ouverts du catalogue, puis Account, à part
77
- * en bas (le compte commun, pas un produit). Chaque produit pointe vers son entrée
78
- * `/auth/hgdc?next=/` (ADR 0010), Account vers sa racine (`accountHref` en développement). Le
79
- * produit courant pointe vers `/`, sans détour par l'entrée.
78
+ * Les entrées de la barre des produits : les produits ouverts du catalogue et Account, affiché en
79
+ * tête (le compte commun, pas un produit). Chaque produit pointe vers son entrée
80
+ * `/auth/hgdc?refresh=1&next=/` (ADR 0010, 0013), Account vers sa racine (`accountHref` en
81
+ * développement). Le produit courant pointe vers `/`, sans détour par l'entrée.
80
82
  */
81
83
  export function productRailEntries(options = {}) {
82
84
  const current = options.current?.toLowerCase();
@@ -87,9 +89,7 @@ export function productRailEntries(options = {}) {
87
89
  ...(item.stage === "preview" ? { hint: PRODUCT_STAGE_LABEL.preview } : {}),
88
90
  });
89
91
  return {
90
- products: (options.catalog ?? HGDC_PRODUCTS)
91
- .filter(isProductOpen)
92
- .map((item) => entry(item, `${item.entryHref}?next=/`)),
92
+ products: (options.catalog ?? HGDC_PRODUCTS).filter(isProductOpen).map((item) => entry(item, item.entryHref)),
93
93
  account: entry(HGDC_ACCOUNT, options.accountHref ?? HGDC_ACCOUNT.entryHref),
94
94
  };
95
95
  }
@@ -18,5 +18,5 @@ export function RadioGroup({ className, ...props }) {
18
18
  return _jsx(RadioGroupPrimitive, { "data-slot": "radio-group", className: cn("grid gap-3", className), ...props });
19
19
  }
20
20
  export function RadioGroupItem({ className, ...props }) {
21
- return (_jsx(RadioPrimitive.Root, { "data-slot": "radio-group-item", className: cn("touch-target grid size-5 shrink-0 place-items-center rounded-full border border-foreground/25 bg-background", "transition-[border-color,box-shadow] duration-(--duration-fast) ease-out outline-none", "hover:border-foreground/40 focus-visible:ring-3 focus-visible:ring-ring/50", "data-checked:border-primary data-checked:bg-primary", "data-disabled:cursor-not-allowed data-disabled:opacity-50", "aria-invalid:border-destructive data-invalid:border-destructive", "dark:bg-white/[0.04] dark:data-checked:bg-primary", className), ...props, children: _jsx(RadioPrimitive.Indicator, { className: "size-2 rounded-full bg-primary-foreground data-unchecked:hidden" }) }));
21
+ return (_jsx(RadioPrimitive.Root, { "data-slot": "radio-group-item", className: cn("touch-target grid size-5 shrink-0 place-items-center rounded-full border border-control-off bg-background", "transition-[border-color,box-shadow] duration-(--duration-fast) ease-out outline-none", "hover:border-foreground/70 focus-visible:ring-3 focus-visible:ring-ring/50", "data-checked:border-primary data-checked:bg-primary", "data-disabled:cursor-not-allowed data-disabled:opacity-50", "aria-invalid:border-destructive data-invalid:border-destructive", "dark:bg-white/[0.04] dark:data-checked:bg-primary", className), ...props, children: _jsx(RadioPrimitive.Indicator, { className: "size-2 rounded-full bg-primary-foreground data-unchecked:hidden" }) }));
22
22
  }
package/dist/switch.js CHANGED
@@ -17,7 +17,11 @@ import { cn } from "./cn.js";
17
17
  * pouce le trouve sans viser.
18
18
  */
19
19
  export function Switch({ checked, onCheckedChange, disabled, className, ...props }) {
20
- return (_jsx(SwitchPrimitive.Root, { checked: checked, onCheckedChange: onCheckedChange, disabled: disabled, className: cn("relative inline-flex h-8 w-[3.25rem] shrink-0 items-center rounded-full p-0.5", "transition-colors duration-200 ease-ios", "outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2", "focus-visible:ring-offset-background", "disabled:cursor-not-allowed disabled:opacity-50", "bg-muted data-checked:bg-status-active", className), ...props, children: _jsx(SwitchPrimitive.Thumb, { className: cn("size-7 rounded-full bg-background shadow-sm ring-1 ring-foreground/5",
20
+ return (_jsx(SwitchPrimitive.Root, { checked: checked, onCheckedChange: onCheckedChange, disabled: disabled, className: cn("relative inline-flex h-8 w-[3.25rem] shrink-0 items-center rounded-full p-0.5", "transition-colors duration-200 ease-ios", "outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-offset-2", "focus-visible:ring-offset-background",
21
+ // Désactivé : tout l'interrupteur s'estompe, la piste ne disparaît pas.
22
+ "disabled:cursor-not-allowed disabled:opacity-50 data-disabled:cursor-not-allowed data-disabled:opacity-50",
23
+ // Éteint : piste grise à 3:1 du fond, de la carte et de `--muted` ; allumé : l'accent d'état.
24
+ "bg-control-off data-checked:bg-status-active", className), ...props, children: _jsx(SwitchPrimitive.Thumb, { className: cn("size-7 rounded-full bg-background shadow-sm ring-1 ring-foreground/5",
21
25
  // `translate` et non `left` : la position part sur le compositeur,
22
26
  // le pouce ne voit donc pas la bascule sautiller quand la page est
23
27
  // occupée ailleurs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hgdc/ui",
3
- "version": "0.11.2",
3
+ "version": "0.12.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
@@ -19,8 +19,11 @@
19
19
 
20
20
  @custom-variant dark (&:is(.dark *));
21
21
  /* Barre latérale réduite en rail : `data-sidebar="rail"` sur <html>, posé avant la première
22
- peinture par `sidebarInitScript` (`@hgdc/ui/app-shell`). */
23
- @custom-variant rail (&:where([data-sidebar="rail"] *));
22
+ peinture par `sidebarInitScript` (`@hgdc/ui/app-shell`), ou `data-rail-peek` sur la coque le
23
+ temps que la barre des produits est élargie (ADR 0011). */
24
+ @custom-variant rail (&:where([data-sidebar="rail"] *, [data-rail-peek] *));
25
+ /* Pliée par la personne seulement (pas le temps du survol de la barre des produits). */
26
+ @custom-variant folded (&:where([data-sidebar="rail"] *));
24
27
 
25
28
  /* Courbes et durées. Pas d'ease-in : il démarre lentement à l'instant où l'œil regarde. */
26
29
  @theme {
@@ -57,6 +60,7 @@
57
60
  --color-destructive: var(--destructive);
58
61
  --color-border: var(--border);
59
62
  --color-input: var(--input);
63
+ --color-control-off: var(--control-off);
60
64
  --color-ring: var(--ring);
61
65
  --color-shell: var(--shell);
62
66
  --color-shell-foreground: var(--shell-foreground);
@@ -129,6 +133,9 @@
129
133
  --destructive: oklch(0.577 0.245 27.325);
130
134
  --border: oklch(0.922 0 0);
131
135
  --input: oklch(0.922 0 0);
136
+ /* Contrôle éteint (piste d'interrupteur, bord de case et de bouton radio) : 3:1 au moins contre
137
+ le fond, une carte et `--muted` (WCAG 1.4.11, vérifié par contrast.test.ts). */
138
+ --control-off: oklch(0.62 0 0);
132
139
  --ring: var(--accent);
133
140
  /* Coque de l'application : barre latérale et en-tête. */
134
141
  --shell: oklch(0.955 0 0);
@@ -196,6 +203,7 @@
196
203
  --destructive: oklch(0.704 0.191 22.216);
197
204
  --border: oklch(1 0 0 / 10%);
198
205
  --input: oklch(1 0 0 / 15%);
206
+ --control-off: oklch(0.57 0 0);
199
207
  --shell: oklch(0.08 0 0);
200
208
  --shell-foreground: oklch(0.985 0 0);
201
209
  --shell-muted-foreground: oklch(0.708 0 0);
@@ -469,3 +477,98 @@
469
477
  opacity: 0.35;
470
478
  }
471
479
  }
480
+
481
+ /* `FamilyLoader` : le H tricolore se construit (jambage gauche, barre, jambage droit), tient,
482
+ s'efface, recommence. Les pièces sont coupées par `clip-path` (la forme ne change pas), un
483
+ léger chevauchement lie les trois gestes. Visible après 200 ms seulement : pas de flash. */
484
+ .family-loader {
485
+ opacity: 0;
486
+ animation: family-loader-in var(--duration-base) var(--ease-out) var(--family-loader-delay, 200ms) forwards;
487
+ }
488
+ .family-loader [data-piece] {
489
+ animation: 1.8s infinite both;
490
+ }
491
+ .family-loader [data-piece="left"] {
492
+ animation-name: family-loader-left;
493
+ }
494
+ .family-loader [data-piece="bar"] {
495
+ animation-name: family-loader-bar;
496
+ }
497
+ .family-loader [data-piece="right"] {
498
+ animation-name: family-loader-right;
499
+ }
500
+ @keyframes family-loader-in {
501
+ to {
502
+ opacity: 1;
503
+ }
504
+ }
505
+ @keyframes family-loader-left {
506
+ 0% {
507
+ clip-path: inset(100% 0 0 0);
508
+ opacity: 1;
509
+ animation-timing-function: var(--ease-out);
510
+ }
511
+ 24%,
512
+ 74% {
513
+ clip-path: inset(0 0 0 0);
514
+ opacity: 1;
515
+ animation-timing-function: var(--ease-out);
516
+ }
517
+ 90%,
518
+ 100% {
519
+ clip-path: inset(0 0 0 0);
520
+ opacity: 0;
521
+ }
522
+ }
523
+ @keyframes family-loader-bar {
524
+ 0%,
525
+ 16% {
526
+ clip-path: inset(0 100% 0 0);
527
+ opacity: 1;
528
+ animation-timing-function: var(--ease-out);
529
+ }
530
+ 40%,
531
+ 77% {
532
+ clip-path: inset(0 0 0 0);
533
+ opacity: 1;
534
+ animation-timing-function: var(--ease-out);
535
+ }
536
+ 93%,
537
+ 100% {
538
+ clip-path: inset(0 0 0 0);
539
+ opacity: 0;
540
+ }
541
+ }
542
+ @keyframes family-loader-right {
543
+ 0%,
544
+ 32% {
545
+ clip-path: inset(100% 0 0 0);
546
+ opacity: 1;
547
+ animation-timing-function: var(--ease-out);
548
+ }
549
+ 56%,
550
+ 80% {
551
+ clip-path: inset(0 0 0 0);
552
+ opacity: 1;
553
+ animation-timing-function: var(--ease-out);
554
+ }
555
+ 96%,
556
+ 100% {
557
+ clip-path: inset(0 0 0 0);
558
+ opacity: 0;
559
+ }
560
+ }
561
+ @keyframes family-loader-breathe {
562
+ 50% {
563
+ opacity: 0.45;
564
+ }
565
+ }
566
+ @media (prefers-reduced-motion: reduce) {
567
+ /* Le H entier, qui respire lentement. */
568
+ .family-loader [data-piece] {
569
+ animation: none;
570
+ }
571
+ .family-loader svg {
572
+ animation: family-loader-breathe 2.4s ease-in-out infinite;
573
+ }
574
+ }