@hgdc/ui 0.11.1 → 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 +1 -0
- package/dist/app-shell.d.ts +2 -2
- package/dist/app-shell.js +5 -3
- package/dist/checkbox.js +1 -1
- package/dist/destructive-dialog.js +2 -3
- package/dist/family-loader.d.ts +24 -0
- package/dist/family-loader.js +21 -0
- package/dist/page-header.js +1 -1
- package/dist/product-rail.d.ts +8 -4
- package/dist/product-rail.js +13 -7
- package/dist/products.d.ts +9 -5
- package/dist/products.js +8 -8
- package/dist/radio-group.js +1 -1
- package/dist/switch.js +5 -1
- package/package.json +1 -1
- package/styles.css +105 -2
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
|
package/dist/app-shell.d.ts
CHANGED
|
@@ -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
|
|
39
|
-
*
|
|
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
|
|
124
|
-
*
|
|
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-
|
|
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-
|
|
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
|
}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import Copy01Icon from "@hugeicons/core-free-icons/Copy01Icon";
|
|
3
3
|
import Tick02Icon from "@hugeicons/core-free-icons/Tick02Icon";
|
|
4
4
|
import { HugeiconsIcon } from "@hugeicons/react";
|
|
5
|
-
import {
|
|
5
|
+
import { useRef, useState } from "react";
|
|
6
6
|
import { toast } from "sonner";
|
|
7
7
|
import { Button } from "./button.js";
|
|
8
8
|
import { useCopy } from "./copy-button.js";
|
|
@@ -38,7 +38,6 @@ export function DestructiveDialog({ open, onOpenChange, title, question, consequ
|
|
|
38
38
|
const view = shown.current;
|
|
39
39
|
const [typed, setTyped] = useState("");
|
|
40
40
|
const [pending, setPending] = useState(false);
|
|
41
|
-
const inputId = useId();
|
|
42
41
|
const ready = !pending && (view.name === undefined || matchesConfirmation(typed, view.name));
|
|
43
42
|
const [wasOpen, setWasOpen] = useState(open);
|
|
44
43
|
if (open !== wasOpen) {
|
|
@@ -70,5 +69,5 @@ export function DestructiveDialog({ open, onOpenChange, title, question, consequ
|
|
|
70
69
|
}, children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: view.title }), _jsxs(DialogDescription, { render: _jsx("div", {}), className: "grid gap-1 text-[15px]", children: [_jsx("p", { children: view.question }), view.consequence ? _jsx("p", { children: view.consequence }) : null, _jsx("p", { className: "font-semibold text-destructive", children: "Impossible de revenir en arri\u00E8re." })] })] }), view.name !== undefined ? (_jsxs("form", { className: "grid gap-3", onSubmit: (event) => {
|
|
71
70
|
event.preventDefault();
|
|
72
71
|
void confirm();
|
|
73
|
-
}, children: [_jsxs("
|
|
72
|
+
}, children: [_jsxs("p", { className: "text-[15px] leading-7 text-muted-foreground", children: ["Tape ", _jsx(NameChip, { name: view.name }), " pour confirmer."] }), _jsx(Input, { "aria-label": `Tape ${view.name} pour confirmer`, autoFocus: true, autoComplete: "off", autoCapitalize: "off", spellCheck: false, value: typed, onChange: (event) => setTyped(event.target.value), placeholder: view.name, "aria-invalid": typed.length > 0 && !view.name.startsWith(typed) ? true : undefined })] })) : null, _jsxs(DialogFooter, { className: "sm:justify-start", children: [_jsxs(Button, { variant: "destructive", isLoading: pending, disabled: !ready, onClick: () => void confirm(), children: [view.confirmLabel ?? view.title, _jsx(Kbd, { "aria-hidden": true, className: "ml-1 hidden bg-current/10 text-current lg:inline-flex", children: mod }), _jsx(Kbd, { "aria-hidden": true, className: "hidden bg-current/10 text-current lg:inline-flex", children: "\u21B5" })] }), _jsxs(Button, { variant: "outline", disabled: pending, onClick: () => onOpenChange(false), children: ["Annuler", _jsx(Kbd, { "aria-hidden": true, className: "ml-1 hidden lg:inline-flex", children: "\u00C9chap" })] })] })] }) }));
|
|
74
73
|
}
|
|
@@ -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
|
+
}
|
package/dist/page-header.js
CHANGED
|
@@ -27,7 +27,7 @@ export function Breadcrumbs({ items, className }) {
|
|
|
27
27
|
* actions passent sous le titre.
|
|
28
28
|
*/
|
|
29
29
|
export function PageHeader({ title, description, actions, breadcrumbs, className, }) {
|
|
30
|
-
return (_jsxs("header", { "data-slot": "page-header", className: cn("mb-8 grid gap-3", className), children: [breadcrumbs?.length ? _jsx(Breadcrumbs, { items: breadcrumbs }) : null, _jsxs("div", { className: "flex flex-wrap
|
|
30
|
+
return (_jsxs("header", { "data-slot": "page-header", className: cn("mb-8 grid gap-3", className), children: [breadcrumbs?.length ? _jsx(Breadcrumbs, { items: breadcrumbs }) : null, _jsxs("div", { "data-slot": "page-header-row", className: cn("flex flex-wrap justify-between gap-4", description ? "items-end" : "items-start"), children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "text-2xl font-semibold tracking-tight sm:text-[28px]", children: title }), description ? _jsx("p", { className: "mt-1.5 max-w-prose text-sm text-muted-foreground", children: description }) : null] }), actions ? _jsx("div", { className: "flex flex-wrap items-center gap-2", children: actions }) : null] })] }));
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* Une page de console, dans la coque (`AppShell`) : `PageHeader`, puis les sections empilées.
|
package/dist/product-rail.d.ts
CHANGED
|
@@ -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
|
|
8
|
-
* d'intention) ou au focus clavier, elle s'élargit
|
|
9
|
-
*
|
|
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;
|
package/dist/product-rail.js
CHANGED
|
@@ -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
|
|
15
|
-
* d'intention) ou au focus clavier, elle s'élargit
|
|
16
|
-
*
|
|
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",
|
|
73
|
-
|
|
74
|
-
|
|
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))
|
package/dist/products.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
81
|
-
*
|
|
82
|
-
* `/auth/hgdc?next=/` (ADR 0010), Account vers sa racine (`accountHref` en
|
|
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
|
|
77
|
-
*
|
|
78
|
-
* `/auth/hgdc?next=/` (ADR 0010), Account vers sa racine (`accountHref` en
|
|
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
|
}
|
package/dist/radio-group.js
CHANGED
|
@@ -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-
|
|
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",
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|