@hgdc/ui 0.2.0 → 0.3.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.
Files changed (57) hide show
  1. package/README.md +63 -18
  2. package/dist/account-menu.d.ts +32 -0
  3. package/dist/account-menu.js +39 -0
  4. package/dist/alert.d.ts +19 -0
  5. package/dist/alert.js +25 -0
  6. package/dist/app-shell.d.ts +56 -0
  7. package/dist/app-shell.js +107 -0
  8. package/dist/badge.js +3 -3
  9. package/dist/button.js +6 -5
  10. package/dist/card.d.ts +24 -0
  11. package/dist/card.js +42 -0
  12. package/dist/checkbox.d.ts +12 -0
  13. package/dist/checkbox.js +19 -0
  14. package/dist/command-menu.d.ts +56 -0
  15. package/dist/command-menu.js +79 -0
  16. package/dist/copy-button.d.ts +26 -0
  17. package/dist/copy-button.js +46 -0
  18. package/dist/kbd.d.ts +9 -0
  19. package/dist/kbd.js +19 -0
  20. package/dist/org-switcher.d.ts +32 -0
  21. package/dist/org-switcher.js +32 -0
  22. package/dist/page-header.d.ts +27 -0
  23. package/dist/page-header.js +31 -0
  24. package/dist/popover.d.ts +13 -0
  25. package/dist/popover.js +22 -0
  26. package/dist/radio-group.d.ts +16 -0
  27. package/dist/radio-group.js +22 -0
  28. package/dist/relative-time.d.ts +16 -0
  29. package/dist/relative-time.js +54 -0
  30. package/dist/resource-list.d.ts +31 -0
  31. package/dist/resource-list.js +35 -0
  32. package/dist/secret-reveal.d.ts +13 -0
  33. package/dist/secret-reveal.js +26 -0
  34. package/dist/segmented-control.js +1 -3
  35. package/dist/skeleton.d.ts +28 -0
  36. package/dist/skeleton.js +47 -0
  37. package/dist/sonner.js +2 -2
  38. package/dist/spinner.d.ts +15 -4
  39. package/dist/spinner.js +7 -6
  40. package/dist/stat.d.ts +30 -0
  41. package/dist/stat.js +26 -0
  42. package/dist/status.d.ts +22 -0
  43. package/dist/status.js +37 -0
  44. package/dist/table.d.ts +50 -0
  45. package/dist/table.js +68 -0
  46. package/dist/tabs.d.ts +16 -0
  47. package/dist/tabs.js +26 -0
  48. package/dist/time-chart.d.ts +40 -0
  49. package/dist/time-chart.js +127 -0
  50. package/dist/timeline.d.ts +21 -0
  51. package/dist/timeline.js +11 -0
  52. package/dist/tooltip.d.ts +15 -0
  53. package/dist/tooltip.js +25 -0
  54. package/dist/uptime-bar.d.ts +24 -0
  55. package/dist/uptime-bar.js +25 -0
  56. package/package.json +1 -1
  57. package/styles.css +74 -25
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @hgdc/ui
2
2
 
3
- Interface de la H family : jetons de la DA, thèmes clair et sombre, polices et composants de base, pour une application React 19 + Tailwind v4.
3
+ Interface de la H family : jetons de la DA, thèmes clair et sombre, polices, composants de base et coque des consoles, pour une application React 19 + Tailwind v4.
4
4
 
5
5
  ```sh
6
6
  bun add @hgdc/ui
@@ -19,30 +19,75 @@ bun add @hgdc/ui
19
19
 
20
20
  ```tsx
21
21
  import { Button } from "@hgdc/ui/button"
22
- import { ThemeProvider, themeInitScript, useTheme } from "@hgdc/ui/theme"
22
+ import { ThemeProvider, themeInitScript } from "@hgdc/ui/theme"
23
+ import { sidebarInitScript } from "@hgdc/ui/app-shell"
23
24
  ```
24
25
 
25
- ## Contenu
26
+ Un sous-chemin par composant, sans fichier d'index. Les primitives viennent toutes de Base UI ; les graphiques sont en SVG, sans bibliothèque.
26
27
 
27
- | Import | Rôle |
28
- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
29
- | `@hgdc/ui/styles.css` | Jetons OKLCH, thèmes (`.dark` sur `<html>`), Inter et Geist, utilitaires `surface`, `pressable`, `soft-in`… |
30
- | `@hgdc/ui/theme` | `ThemeProvider`, `useTheme`, `themeInitScript` (à poser dans le `<head>` contre le flash de thème) |
31
- | `@hgdc/ui/cn` | `cn()` (clsx + tailwind-merge) |
32
- | `@hgdc/ui/use-media-query` | `useMediaQuery`, `useIsMobile` (sûrs au rendu serveur) |
33
- | `@hgdc/ui/floating` | Classes des surfaces flottantes (menus, sélecteurs) |
34
- | `avatar` `badge` `button` `dialog` `drawer` `dropdown-menu` `empty` `field` `input` `input-otp` | Primitives sur Base UI |
35
- | `label` `segmented-control` `select` `separator` `sonner` `spinner` `switch` `textarea` | Primitives sur Base UI, toasts sonner, icônes Hugeicons |
28
+ ## Fondations
36
29
 
37
- Un sous-chemin par composant, sans fichier d'index : `import { Badge } from "@hgdc/ui/badge"`.
30
+ | Import | Rôle |
31
+ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
32
+ | `@hgdc/ui/styles.css` | Jetons OKLCH, thèmes (`.dark` sur `<html>`), variante `rail:`, polices, utilitaires `surface`, `pressable`, `skeleton-pulse`… |
33
+ | `@hgdc/ui/theme` | `ThemeProvider`, `useTheme`, `themeInitScript` (dans le `<head>`, contre le flash de thème) |
34
+ | `@hgdc/ui/cn` | `cn()` (clsx + tailwind-merge) |
35
+ | `@hgdc/ui/use-media-query` | `useMediaQuery`, `useIsMobile` |
36
+ | `@hgdc/ui/floating` | Classes des surfaces flottantes |
37
+
38
+ Primitives déjà là : `avatar` `badge` `button` `dialog` `drawer` `dropdown-menu` `empty` `field` `input` `input-otp` `label` `segmented-control` `select` `separator` `sonner` `spinner` `switch` `textarea`.
39
+
40
+ ## Coque des consoles
41
+
42
+ | Import | Exemple |
43
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
44
+ | `app-shell` | `<AppShell sidebarHeader={<OrgSwitcher …/>} nav={<SidebarNav label="Projet">…</SidebarNav>} sidebarFooter={<AccountMenu …/>}>` |
45
+ | | `<SidebarNavItem icon={Rocket01Icon} label="Déploiements" render={<Link to="/deployments" />} />` |
46
+ | `org-switcher` | `<OrgSwitcher organizations={orgs} activeId={org.id} onSelect={go} createHref="/organisations/nouvelle" />` |
47
+ | `account-menu` | `<AccountMenu user={user} onProfile={…} onSecurity={…} onSignOut={signOut} products={HGDC_PRODUCTS} />` |
48
+ | `command-menu` | `<CommandMenu groups={[{ label: "Aller à", items }, { label: "Créer", items }, { label: "Changer", items }]} />` (`Ctrl+K`) |
49
+ | `kbd` | `<KbdGroup><Kbd>Ctrl</Kbd><Kbd>K</Kbd></KbdGroup>` |
50
+ | `page-header` | `<PageHeader title="Domaines" breadcrumbs={[{ label: "Acme", render: <Link to="/o/acme" /> }, { label: "Domaines" }]} actions={<Button>Ajouter</Button>} />` |
51
+
52
+ La barre se réduit en rail (`Ctrl+B`, mémorisé) : poser `sidebarInitScript` dans le `<head>`. Le kit ne connaît ni routeur ni authentification : liens par `render={<Link />}`, actions par callbacks. Rôles d'organisation : `owner`, `admin`, `member` (Propriétaire, Admin, Membre).
53
+
54
+ ## Contenu et données
55
+
56
+ | Import | Exemple |
57
+ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
58
+ | `card` | `<Card><CardHeader><CardTitle>Domaines</CardTitle><CardAction>…</CardAction></CardHeader><CardContent>…</CardContent><CardFooter>…</CardFooter></Card>` |
59
+ | `alert` | `<Alert tone="warning" title="Quota presque atteint" action={<Button size="sm">Voir</Button>}>90 % utilisés.</Alert>` |
60
+ | `table` | `<TableHead sort="descending" onSort={…}>Date</TableHead>`, `<TableRowLink render={<Link … />}>Production</TableRowLink>`, `<TableEmpty colSpan={4}>…</TableEmpty>`, `<TableSkeleton columns={4} />` |
61
+ | `resource-list` | `<ResourceList><ResourceItem icon={…} title="Site vitrine" meta="acme.ch" badge={…} actions={…} href="/p/site" /></ResourceList>` |
62
+ | `stat` | `<StatGrid><StatTile label="Erreurs" value="12" delta={{ value: "8 %", direction: "up", good: "down" }} /></StatGrid>` |
63
+ | `time-chart` | `<TimeChart label="Envois par jour" timestamps={days} series={[{ key: "sent", label: "Envoyés", values }]} kind="bar" />` |
64
+ | `status` | `<StatusBadge status="degraded" />` (Opérationnel, Dégradé, Panne, Maintenance, Inconnu) |
65
+ | `uptime-bar` | `<UptimeBar days={[{ date, status: "operational", uptime: "100 %" }]} summary="99,95 % sur 90 jours" />` |
66
+ | `timeline` | `<Timeline entries={[{ id, title: "Résolu", time, status: "operational" }]} />` |
67
+ | `relative-time` | `<RelativeTime date={deployment._creationTime} />` (« il y a 3 min », date exacte au survol) |
68
+ | `copy-button` | `<Snippet value="bun add @hgdc/ui" prompt />`, `<CopyButton value={url} />` |
69
+ | `secret-reveal` | `<SecretReveal label="Clé d'API" value={key} onDismiss={forget} />` : affichée une fois, puis retirée de la page |
70
+ | `skeleton` | `<Pending pending={isLoading} fallback={<TableSkeleton columns={3} />}>…</Pending>` : après 200 ms, tenu 400 ms au moins |
71
+
72
+ ## Contrôles et surfaces flottantes
73
+
74
+ | Import | Exemple |
75
+ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
76
+ | `checkbox` | `<label className="flex items-center gap-2.5"><Checkbox defaultChecked /> Résumé hebdomadaire</label>` |
77
+ | `radio-group` | `<RadioGroup aria-labelledby="role" defaultValue="member"><RadioGroupItem value="admin" />…</RadioGroup>` |
78
+ | `tabs` | `<Tabs defaultValue="apercu"><TabsList><TabsTab value="apercu">Aperçu</TabsTab></TabsList><TabsPanel value="apercu">…</TabsPanel></Tabs>` |
79
+ | `tooltip` | `<TooltipProvider>` autour de l'app, puis `<Tooltip><TooltipTrigger aria-label="Copier">…</TooltipTrigger><TooltipContent>Copier</TooltipContent></Tooltip>` |
80
+ | `popover` | `<Popover><PopoverTrigger>Aide</PopoverTrigger><PopoverContent><PopoverTitle>…</PopoverTitle></PopoverContent></Popover>` |
38
81
 
39
82
  ## Règles de la DA
40
83
 
41
- - Chrome monochrome, un seul accent violet : focus, lien, élément actif, un geste principal par écran.
42
- - Profondeur par la teinte : ni ombre ni bordure dans le flux, ombre réservée à ce qui flotte.
43
- - Boutons en pilule, champs et menus en `rounded-2xl`, cartes en `rounded-3xl`.
44
- - Un `Button` rendu en lien (`render={<Link />}`) reste un lien pour les lecteurs d'écran.
84
+ - Chrome monochrome, un seul accent violet : focus, lien, un geste principal par écran. « Vous êtes ici » (barre, onglets) reste en teinte.
85
+ - Profondeur par la teinte : cartes `surface` sans ombre ni contour, ombre réservée à ce qui flotte, aucun flou.
86
+ - Au survol, seule la couleur change. Le rebond est réservé à l'appui. La palette de commandes s'ouvre sans animation.
87
+ - Un statut s'écrit toujours (point et libellé), jamais par la couleur seule. Chiffres en `tabular-nums`.
88
+ - Graphiques : `--chart-1` à `--chart-6` dans cet ordre, validées pour le daltonisme dans les deux thèmes ; au-delà de six séries, regrouper en « Autres ».
89
+ - Référence de l'interface : Host. Couleur d'un client : `--brand-accent`.
45
90
 
46
- Mode d'emploi complet : [Adopter le kit](https://github.com/hugogebel-boop/hgdc-kit/blob/main/docs/guides/adopter-le-kit.md#7-interface). Décisions : [ADR 0005](https://github.com/hugogebel-boop/hgdc-kit/blob/main/docs/adr/0005-ui-css-brute-et-sous-chemins.md).
91
+ Mode d'emploi complet : [Adopter le kit](https://github.com/hugogebel-boop/hgdc-kit/blob/main/docs/guides/adopter-le-kit.md#7-interface). Décisions : [ADR 0005](https://github.com/hugogebel-boop/hgdc-kit/blob/main/docs/adr/0005-ui-css-brute-et-sous-chemins.md), [ADR 0006](https://github.com/hugogebel-boop/hgdc-kit/blob/main/docs/adr/0006-ui-composants-des-consoles.md).
47
92
 
48
93
  Tous droits réservés, Hugo Gebel Développement Conseil.
@@ -0,0 +1,32 @@
1
+ import type { ReactNode } from "react";
2
+ export type HgdcProduct = {
3
+ /** « HGDC Host ». */
4
+ name: string;
5
+ href: string;
6
+ /** Le produit ouvert : coché, sans lien. */
7
+ current?: boolean;
8
+ };
9
+ export type AccountUser = {
10
+ name: string;
11
+ email: string;
12
+ image?: string | null;
13
+ };
14
+ /**
15
+ * Le menu du compte, en bas de la barre latérale : profil, sécurité, thème, raccourcis, autres
16
+ * produits HGDC, déconnexion. `row` : avatar, nom et adresse (l'avatar seul en rail) ; `avatar` :
17
+ * l'avatar seul, pour une barre du haut. Le produit passe les actions ; le thème passe par
18
+ * `ThemeProvider`.
19
+ */
20
+ export declare function AccountMenu({ user, onProfile, onSecurity, onShortcuts, onSignOut, products, children, variant, }: {
21
+ user: AccountUser;
22
+ onProfile?: () => void;
23
+ onSecurity?: () => void;
24
+ /** Ouvre l'aide des raccourcis clavier. */
25
+ onShortcuts?: () => void;
26
+ onSignOut: () => void;
27
+ /** Les produits de la famille, pour passer de l'un à l'autre. */
28
+ products?: Array<HgdcProduct>;
29
+ /** Entrées propres au produit (« Administration »), après le profil. */
30
+ children?: ReactNode;
31
+ variant?: "row" | "avatar";
32
+ }): React.JSX.Element;
@@ -0,0 +1,39 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import ComputerIcon from "@hugeicons/core-free-icons/ComputerIcon";
3
+ import DashboardSquare01Icon from "@hugeicons/core-free-icons/DashboardSquare01Icon";
4
+ import KeyboardIcon from "@hugeicons/core-free-icons/KeyboardIcon";
5
+ import Logout01Icon from "@hugeicons/core-free-icons/Logout01Icon";
6
+ import Moon02Icon from "@hugeicons/core-free-icons/Moon02Icon";
7
+ import SecurityLockIcon from "@hugeicons/core-free-icons/SecurityLockIcon";
8
+ import Sun03Icon from "@hugeicons/core-free-icons/Sun03Icon";
9
+ import Tick02Icon from "@hugeicons/core-free-icons/Tick02Icon";
10
+ import UserIcon from "@hugeicons/core-free-icons/UserIcon";
11
+ import { HugeiconsIcon } from "@hugeicons/react";
12
+ import { Menu as MenuPrimitive } from "@base-ui/react/menu";
13
+ import { Avatar, AvatarFallback, AvatarImage } from "./avatar.js";
14
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "./dropdown-menu.js";
15
+ import { MENU_LINK_ITEM } from "./org-switcher.js";
16
+ import { useTheme } from "./theme.js";
17
+ const THEMES = ["light", "dark", "system"];
18
+ function initials(name) {
19
+ return name
20
+ .split(/\s+/)
21
+ .filter(Boolean)
22
+ .slice(0, 2)
23
+ .map((part) => part[0])
24
+ .join("");
25
+ }
26
+ /**
27
+ * Le menu du compte, en bas de la barre latérale : profil, sécurité, thème, raccourcis, autres
28
+ * produits HGDC, déconnexion. `row` : avatar, nom et adresse (l'avatar seul en rail) ; `avatar` :
29
+ * l'avatar seul, pour une barre du haut. Le produit passe les actions ; le thème passe par
30
+ * `ThemeProvider`.
31
+ */
32
+ export function AccountMenu({ user, onProfile, onSecurity, onShortcuts, onSignOut, products, children, variant = "row", }) {
33
+ const { theme, setTheme } = useTheme();
34
+ 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) })] }), 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
+ const next = THEMES.find((t) => t === value);
36
+ if (next)
37
+ 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 HGDC" }), products.map((product) => product.current ? (_jsxs(DropdownMenuItem, { disabled: true, className: "data-disabled:opacity-100", children: [_jsx(HugeiconsIcon, { icon: DashboardSquare01Icon, "aria-hidden": true }), _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(HugeiconsIcon, { icon: DashboardSquare01Icon, "aria-hidden": true }), product.name] }, product.name)))] })] })) : null, _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: onSignOut, children: [_jsx(HugeiconsIcon, { icon: Logout01Icon, "aria-hidden": true }), "Se d\u00E9connecter"] })] })] }));
39
+ }
@@ -0,0 +1,19 @@
1
+ import { type IconSvgElement } from "@hugeicons/react";
2
+ import type { ReactNode } from "react";
3
+ export type AlertTone = "info" | "success" | "warning" | "destructive";
4
+ /**
5
+ * Message posé dans la page, non bloquant : une information, un succès durable, un avertissement,
6
+ * une erreur. Lavis de la couleur, icône dans son encre, texte en couleur de texte : la couleur
7
+ * ne porte jamais seule le sens, l'icône et le titre le disent aussi.
8
+ *
9
+ * Une erreur s'annonce tout de suite (`role="alert"`), le reste poliment (`role="status"`).
10
+ * Pour un choix qui bloque, c'est un `Dialog`.
11
+ */
12
+ export declare function Alert({ tone, title, children, action, icon, className, ...props }: Omit<React.ComponentProps<"div">, "title"> & {
13
+ tone?: AlertTone;
14
+ title?: ReactNode;
15
+ /** Un bouton ou un lien à droite (« Réessayer », « Voir la doc »). */
16
+ action?: ReactNode;
17
+ /** Remplace l'icône du ton. */
18
+ icon?: IconSvgElement;
19
+ }): React.JSX.Element;
package/dist/alert.js ADDED
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Alert02Icon from "@hugeicons/core-free-icons/Alert02Icon";
3
+ import AlertCircleIcon from "@hugeicons/core-free-icons/AlertCircleIcon";
4
+ import CheckmarkCircle02Icon from "@hugeicons/core-free-icons/CheckmarkCircle02Icon";
5
+ import InformationCircleIcon from "@hugeicons/core-free-icons/InformationCircleIcon";
6
+ import { HugeiconsIcon } from "@hugeicons/react";
7
+ import { cn } from "./cn.js";
8
+ const TONES = {
9
+ info: { box: "bg-info/10", icon: "text-info-ink", glyph: InformationCircleIcon },
10
+ success: { box: "bg-status-active/12", icon: "text-status-active-ink", glyph: CheckmarkCircle02Icon },
11
+ warning: { box: "bg-status-paused/14", icon: "text-status-paused-ink", glyph: Alert02Icon },
12
+ destructive: { box: "bg-destructive/10", icon: "text-destructive", glyph: AlertCircleIcon },
13
+ };
14
+ /**
15
+ * Message posé dans la page, non bloquant : une information, un succès durable, un avertissement,
16
+ * une erreur. Lavis de la couleur, icône dans son encre, texte en couleur de texte : la couleur
17
+ * ne porte jamais seule le sens, l'icône et le titre le disent aussi.
18
+ *
19
+ * Une erreur s'annonce tout de suite (`role="alert"`), le reste poliment (`role="status"`).
20
+ * Pour un choix qui bloque, c'est un `Dialog`.
21
+ */
22
+ export function Alert({ tone = "info", title, children, action, icon, className, ...props }) {
23
+ const t = TONES[tone];
24
+ return (_jsxs("div", { role: tone === "destructive" ? "alert" : "status", "data-slot": "alert", "data-tone": tone, className: cn("flex flex-wrap items-start gap-x-3 gap-y-2 rounded-2xl px-4 py-3 text-sm sm:flex-nowrap", t.box, className), ...props, children: [_jsx(HugeiconsIcon, { icon: icon ?? t.glyph, strokeWidth: 2, "aria-hidden": true, className: cn("mt-0.5 size-4.5 shrink-0", t.icon) }), _jsxs("div", { className: "min-w-0 flex-1", children: [title ? _jsx("p", { className: "font-medium text-foreground", children: title }) : null, children ? _jsx("div", { className: cn("text-foreground/80", title ? "mt-0.5" : undefined), children: children }) : null] }), action ? _jsx("div", { className: "flex shrink-0 items-center gap-2 max-sm:w-full max-sm:pl-7.5", children: action }) : null] }));
25
+ }
@@ -0,0 +1,56 @@
1
+ import { type IconSvgElement } from "@hugeicons/react";
2
+ import { type ReactNode } from "react";
3
+ /**
4
+ * À poser dans le `<head>`, avec `themeInitScript` : il remet le rail avant la première
5
+ * peinture, et la variante CSS `rail:` le lit, donc rien ne saute au chargement.
6
+ */
7
+ export declare const sidebarInitScript: string;
8
+ /** La barre latérale est-elle réduite en rail ? L'état vit sur `<html data-sidebar="rail">`. */
9
+ export declare function useSidebarRail(): boolean;
10
+ export declare function toggleSidebar(): void;
11
+ /** Pour fermer le tiroir mobile depuis un élément de la barre (un sélecteur, un bouton). */
12
+ export declare function useAppShell(): {
13
+ closeMobileNav: () => void;
14
+ };
15
+ /** La navigation du produit : 4 à 7 entrées, à plat. */
16
+ export declare function SidebarNav({ label, className, children, }: {
17
+ /** Nom de la navigation pour les lecteurs d'écran : « Projet », « Organisation ». */
18
+ label: string;
19
+ className?: string;
20
+ children: ReactNode;
21
+ }): React.JSX.Element;
22
+ /**
23
+ * Une entrée de la barre. « Vous êtes ici » par `active` (ou `data-status="active"`, que pose le
24
+ * `Link` de TanStack Router) : teinte de la coque, jamais l'accent. En rail, l'icône seule, et le
25
+ * nom en infobulle.
26
+ */
27
+ export declare function SidebarNavItem({ icon, label, active, href, render, }: {
28
+ icon: IconSvgElement;
29
+ label: string;
30
+ active?: boolean;
31
+ href?: string;
32
+ /** Lien du routeur : `render={<Link to="…" />}`. */
33
+ render?: React.ReactElement;
34
+ }): React.JSX.Element;
35
+ /**
36
+ * La coque commune des consoles HGDC, celle de Host. Sur ordinateur : colonne teintée à gauche
37
+ * (organisation en haut, navigation, compte en bas), réductible en rail (`Ctrl+B`, mémorisé), et
38
+ * la page sur une carte à côté. Sur téléphone : une barre en haut, le menu dans un tiroir qui
39
+ * monte du bas et se referme quand on choisit une page.
40
+ *
41
+ * Le kit ne connaît ni le routeur ni l'authentification : le produit passe ses éléments
42
+ * (`OrgSwitcher`, `SidebarNav`, `AccountMenu`) et ses liens.
43
+ */
44
+ export declare function AppShell({ sidebarHeader, nav, sidebarFooter, header, children, mainClassName, }: {
45
+ /** Haut de la barre : `OrgSwitcher`. */
46
+ sidebarHeader?: ReactNode;
47
+ /** `SidebarNav` et ses `SidebarNavItem`. */
48
+ nav: ReactNode;
49
+ /** Bas de la barre : liens secondaires (Docs, Paramètres) et `AccountMenu`. */
50
+ sidebarFooter?: ReactNode;
51
+ /** Barre du haut de la page : sélecteur de projet, recherche (`CommandMenuButton`). */
52
+ header?: ReactNode;
53
+ children: ReactNode;
54
+ /** Largeur et marges du contenu : `max-w-6xl` par défaut. */
55
+ mainClassName?: string;
56
+ }): React.JSX.Element;
@@ -0,0 +1,107 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Menu01Icon from "@hugeicons/core-free-icons/Menu01Icon";
3
+ import PanelLeftCloseIcon from "@hugeicons/core-free-icons/PanelLeftCloseIcon";
4
+ import PanelLeftOpenIcon from "@hugeicons/core-free-icons/PanelLeftOpenIcon";
5
+ import { HugeiconsIcon } from "@hugeicons/react";
6
+ import { useRender } from "@base-ui/react/use-render";
7
+ import { createContext, useContext, useEffect, useState, useSyncExternalStore } from "react";
8
+ import { Drawer, DrawerContent, DrawerTitle } from "./drawer.js";
9
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
10
+ import { cn } from "./cn.js";
11
+ // ── Rail : la barre latérale réduite aux icônes, mémorisée par navigateur ──────────────
12
+ const RAIL_KEY = "sidebar";
13
+ const RAIL_EVENT = "sidebarchange";
14
+ /**
15
+ * À poser dans le `<head>`, avec `themeInitScript` : il remet le rail avant la première
16
+ * peinture, et la variante CSS `rail:` le lit, donc rien ne saute au chargement.
17
+ */
18
+ export const sidebarInitScript = `try{if(localStorage.getItem(${JSON.stringify(RAIL_KEY)})==="rail")document.documentElement.dataset.sidebar="rail"}catch(e){}`;
19
+ function subscribeRail(onChange) {
20
+ window.addEventListener(RAIL_EVENT, onChange);
21
+ return () => window.removeEventListener(RAIL_EVENT, onChange);
22
+ }
23
+ /** La barre latérale est-elle réduite en rail ? L'état vit sur `<html data-sidebar="rail">`. */
24
+ export function useSidebarRail() {
25
+ return useSyncExternalStore(subscribeRail, () => document.documentElement.dataset.sidebar === "rail", () => false);
26
+ }
27
+ export function toggleSidebar() {
28
+ const root = document.documentElement;
29
+ const rail = root.dataset.sidebar !== "rail";
30
+ if (rail)
31
+ root.dataset.sidebar = "rail";
32
+ else
33
+ delete root.dataset.sidebar;
34
+ try {
35
+ if (rail)
36
+ localStorage.setItem(RAIL_KEY, "rail");
37
+ else
38
+ localStorage.removeItem(RAIL_KEY);
39
+ }
40
+ catch {
41
+ // Stockage bloqué : le choix vaut pour cette page seulement.
42
+ }
43
+ window.dispatchEvent(new Event(RAIL_EVENT));
44
+ }
45
+ /** `Ctrl+B` (`⌘B`) réduit ou déplie la barre latérale, hors des champs de saisie. */
46
+ function useToggleShortcut() {
47
+ useEffect(() => {
48
+ const onKey = (event) => {
49
+ if (event.key.toLowerCase() !== "b" || !(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey)
50
+ return;
51
+ const target = event.target;
52
+ if (target instanceof Element && target.closest("input, textarea, select, [contenteditable=true]"))
53
+ return;
54
+ event.preventDefault();
55
+ toggleSidebar();
56
+ };
57
+ document.addEventListener("keydown", onKey);
58
+ return () => document.removeEventListener("keydown", onKey);
59
+ }, []);
60
+ }
61
+ // ── Navigation ──────────────────────────────────────────────────────────────────────
62
+ const ShellContext = createContext({ closeMobileNav: () => { } });
63
+ /** Pour fermer le tiroir mobile depuis un élément de la barre (un sélecteur, un bouton). */
64
+ export function useAppShell() {
65
+ return useContext(ShellContext);
66
+ }
67
+ /** La navigation du produit : 4 à 7 entrées, à plat. */
68
+ export function SidebarNav({ label, className, children, }) {
69
+ return (_jsx("nav", { "aria-label": label, className: cn("min-h-0 flex-1 overflow-y-auto px-3 py-2", className), children: _jsx("ul", { className: "grid gap-0.5", children: children }) }));
70
+ }
71
+ /**
72
+ * Une entrée de la barre. « Vous êtes ici » par `active` (ou `data-status="active"`, que pose le
73
+ * `Link` de TanStack Router) : teinte de la coque, jamais l'accent. En rail, l'icône seule, et le
74
+ * nom en infobulle.
75
+ */
76
+ export function SidebarNavItem({ icon, label, active, href, render, }) {
77
+ const rail = useSidebarRail();
78
+ const { closeMobileNav } = useAppShell();
79
+ const link = useRender({
80
+ defaultTagName: "a",
81
+ render,
82
+ props: {
83
+ href,
84
+ "aria-current": active ? "page" : undefined,
85
+ onClick: closeMobileNav,
86
+ className: cn("flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-medium whitespace-nowrap text-muted-foreground outline-none transition-colors duration-(--duration-fast) ease-out", "hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50", "aria-[current=page]:bg-muted aria-[current=page]:text-foreground data-[status=active]:bg-muted data-[status=active]:text-foreground", "lg:min-h-9 lg:gap-2.5 lg:py-2 lg:text-shell-muted lg:hover:bg-shell-hover lg:hover:text-shell-foreground", "lg:aria-[current=page]:bg-shell-active lg:aria-[current=page]:text-shell-foreground lg:data-[status=active]:bg-shell-active lg:data-[status=active]:text-shell-foreground", "lg:rail:justify-center lg:rail:px-0"),
87
+ children: (_jsxs(_Fragment, { children: [_jsx(HugeiconsIcon, { icon: icon, strokeWidth: 1.8, "aria-hidden": true, className: "size-5 shrink-0 lg:size-[1.125rem]" }), _jsx("span", { className: "truncate lg:rail:sr-only", children: label })] })),
88
+ },
89
+ });
90
+ return (_jsx("li", { children: _jsxs(Tooltip, { disabled: !rail, children: [_jsx(TooltipTrigger, { render: link }), _jsx(TooltipContent, { side: "right", children: label })] }) }));
91
+ }
92
+ // ── Coque ─────────────────────────────────────────────────────────────────────────────
93
+ /**
94
+ * La coque commune des consoles HGDC, celle de Host. Sur ordinateur : colonne teintée à gauche
95
+ * (organisation en haut, navigation, compte en bas), réductible en rail (`Ctrl+B`, mémorisé), et
96
+ * la page sur une carte à côté. Sur téléphone : une barre en haut, le menu dans un tiroir qui
97
+ * monte du bas et se referme quand on choisit une page.
98
+ *
99
+ * Le kit ne connaît ni le routeur ni l'authentification : le produit passe ses éléments
100
+ * (`OrgSwitcher`, `SidebarNav`, `AccountMenu`) et ses liens.
101
+ */
102
+ export function AppShell({ sidebarHeader, nav, sidebarFooter, header, children, mainClassName, }) {
103
+ const rail = useSidebarRail();
104
+ const [menuOpen, setMenuOpen] = useState(false);
105
+ useToggleShortcut();
106
+ return (_jsx(ShellContext.Provider, { value: { closeMobileNav: () => setMenuOpen(false) }, children: _jsxs("div", { "data-slot": "app-shell", className: "min-h-dvh lg:grid lg:h-dvh lg:grid-cols-[auto_minmax(0,1fr)] lg:bg-shell", 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" }), _jsxs("aside", { "aria-label": "Barre lat\u00E9rale", className: "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: [_jsxs("div", { className: "mt-2 flex min-h-14 shrink-0 items-center gap-1 px-3 rail:flex-col rail:justify-center rail:px-0 rail:py-1", children: [_jsx("div", { className: "min-w-0 flex-1 rail:flex-none", children: sidebarHeader }), _jsx("button", { type: "button", onClick: toggleSidebar, "aria-label": rail ? "Déplier la barre latérale" : "Réduire la barre latérale", "aria-keyshortcuts": "Control+B Meta+B", title: rail ? "Déplier (Ctrl+B)" : "Réduire (Ctrl+B)", className: "grid size-8 shrink-0 place-items-center rounded-lg text-shell-muted outline-none transition-colors duration-(--duration-fast) ease-out hover:bg-shell-hover hover:text-shell-foreground focus-visible:ring-2 focus-visible:ring-ring/50 active:scale-[0.96]", children: _jsx(HugeiconsIcon, { icon: rail ? PanelLeftCloseIcon : PanelLeftOpenIcon, strokeWidth: 1.8, "aria-hidden": true, className: "size-[1.125rem]" }) })] }), 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" }) }), _jsx("div", { className: "flex min-w-0 flex-1 items-center gap-2", children: header })] }), _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" }), sidebarHeader ? _jsx("div", { className: "px-3 pt-1", children: sidebarHeader }) : 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] }) })] }) }));
107
+ }
package/dist/badge.js CHANGED
@@ -5,9 +5,9 @@ const badgeVariants = cva("inline-flex w-fit items-center gap-1 rounded-full px-
5
5
  variants: {
6
6
  tone: {
7
7
  neutral: "bg-muted text-muted-foreground",
8
- success: "bg-status-active/15 text-status-active",
9
- warning: "bg-status-paused/15 text-status-paused",
10
- info: "bg-status-done/15 text-status-done",
8
+ success: "bg-status-active/15 text-status-active-ink",
9
+ warning: "bg-status-paused/15 text-status-paused-ink",
10
+ info: "bg-info/12 text-info-ink",
11
11
  destructive: "bg-destructive/10 text-destructive",
12
12
  },
13
13
  },
package/dist/button.js CHANGED
@@ -22,17 +22,18 @@ const buttonVariants = cva(
22
22
  // regarde à cet instant précis ; la relâche revient en 300 ms avec un petit
23
23
  // rebond (`--ease-bounce`, révision du 2026-09-24).
24
24
  //
25
- // Au survol d'un vrai pointeur (`hover:` ne s'applique qu'aux pointeurs
26
- // fins en Tailwind v4), le bouton s'avance d'un rien (1.02), comme sur le
27
- // site ; aucune ombre, jamais : ce qui répond au survol est la couleur et
28
- // l'échelle.
25
+ // Au survol, seule la couleur change : ni ombre, ni échelle. Le zoom de
26
+ // 1.02 faisait clignoter le libellé (le texte passe sur un calque GPU au
27
+ // lissage différent pendant l'animation, puis revient), et la couleur, qui
28
+ // suivait la courbe à rebond, dépassait sa cible : un éclair sur `ghost`.
29
+ // La couleur suit donc `--ease-out` en 150 ms ; le rebond reste à l'échelle.
29
30
  //
30
31
  // Ni bordure transparente ni `bg-clip-padding` (retirés le 2026-09-27) : le
31
32
  // fond s'arrêtait un pixel avant le bord, et dans une feuille du bas posée à
32
33
  // une coordonnée fractionnaire (hauteur en `svh`, glissement), iOS laissait
33
34
  // voir ce pixel comme un filet sombre en haut de la pilule, en thème sombre.
34
35
  // Le focus et l'erreur passent par l'anneau seul.
35
- "group/button touch-target inline-flex shrink-0 items-center justify-center rounded-full text-sm font-medium whitespace-nowrap [transition-property:color,background-color,opacity,scale,box-shadow] duration-(--duration-slow) ease-(--ease-bounce) outline-none select-none focus-visible:ring-3 focus-visible:ring-ring/50 active:scale-[0.96] active:duration-(--duration-instant) active:ease-out disabled:pointer-events-none disabled:opacity-50 aria-invalid:ring-3 aria-invalid:ring-destructive/40 data-[pressed]:scale-[0.96] data-[pressed]:duration-(--duration-instant) data-[pressed]:ease-out motion-safe:hover:scale-[1.02] dark:aria-invalid:ring-destructive/50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
36
+ "group/button touch-target inline-flex shrink-0 items-center justify-center rounded-full text-sm font-medium whitespace-nowrap [transition:color_var(--duration-fast)_var(--ease-out),background-color_var(--duration-fast)_var(--ease-out),box-shadow_var(--duration-fast)_var(--ease-out),opacity_var(--duration-fast)_var(--ease-out),scale_var(--duration-slow)_var(--ease-bounce)] outline-none select-none focus-visible:ring-3 focus-visible:ring-ring/50 active:scale-[0.96] active:duration-(--duration-instant) active:ease-out disabled:pointer-events-none disabled:opacity-50 aria-invalid:ring-3 aria-invalid:ring-destructive/40 data-[pressed]:scale-[0.96] data-[pressed]:duration-(--duration-instant) data-[pressed]:ease-out dark:aria-invalid:ring-destructive/50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
36
37
  variants: {
37
38
  variant: {
38
39
  default: "bg-primary text-primary-foreground hover:bg-primary/85",
package/dist/card.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { useRender } from "@base-ui/react/use-render";
2
+ type CardProps = useRender.ComponentProps<"section"> & {
3
+ /** `danger` : la zone des actions irréversibles (supprimer un projet), cerclée de rouge. */
4
+ tone?: "default" | "danger";
5
+ /**
6
+ * Toute la carte est cliquable (`render={<Link to="…" />}`) : le survol fonce la teinte,
7
+ * l'appui l'enfonce à peine. Jamais d'ombre ni de zoom.
8
+ */
9
+ interactive?: boolean;
10
+ };
11
+ /**
12
+ * Carte de la DA : une teinte (`surface`), ni contour ni ombre, `rounded-3xl`. Un seul niveau
13
+ * de carte par écran ; ce qui est dedans reste à plat.
14
+ */
15
+ declare function Card({ className, tone, interactive, render, ...props }: CardProps): React.JSX.Element;
16
+ declare function CardHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
17
+ declare function CardTitle({ className, children, ...props }: React.ComponentProps<"h2">): React.JSX.Element;
18
+ declare function CardDescription({ className, ...props }: React.ComponentProps<"p">): React.JSX.Element;
19
+ /** Action en haut à droite de l'en-tête (un bouton « … », un lien « Tout voir »). */
20
+ declare function CardAction({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
21
+ declare function CardContent({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
22
+ /** Pied d'action, comme celui des réglages de Host : un filet, puis les boutons alignés à droite. */
23
+ declare function CardFooter({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
24
+ export { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter, type CardProps };
package/dist/card.js ADDED
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { mergeProps } from "@base-ui/react/merge-props";
3
+ import { useRender } from "@base-ui/react/use-render";
4
+ import { cn } from "./cn.js";
5
+ /**
6
+ * Carte de la DA : une teinte (`surface`), ni contour ni ombre, `rounded-3xl`. Un seul niveau
7
+ * de carte par écran ; ce qui est dedans reste à plat.
8
+ */
9
+ function Card({ className, tone = "default", interactive = false, render, ...props }) {
10
+ return useRender({
11
+ defaultTagName: "section",
12
+ render,
13
+ props: mergeProps({
14
+ "data-slot": "card",
15
+ "data-tone": tone,
16
+ className: cn("flex flex-col overflow-hidden rounded-3xl text-card-foreground", interactive
17
+ ? "surface-link pressable-card outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
18
+ : "surface", tone === "danger" && "ring-1 ring-destructive/30 ring-inset", className),
19
+ }, props),
20
+ });
21
+ }
22
+ function CardHeader({ className, ...props }) {
23
+ return (_jsx("div", { "data-slot": "card-header", className: cn("grid auto-rows-min items-start gap-1 px-5 pt-5 sm:px-6 sm:pt-6 has-data-[slot=card-action]:grid-cols-[minmax(0,1fr)_auto]", className), ...props }));
24
+ }
25
+ function CardTitle({ className, children, ...props }) {
26
+ return (_jsx("h2", { "data-slot": "card-title", className: cn("text-base font-semibold", className), ...props, children: children }));
27
+ }
28
+ function CardDescription({ className, ...props }) {
29
+ return (_jsx("p", { "data-slot": "card-description", className: cn("max-w-prose text-sm text-muted-foreground", className), ...props }));
30
+ }
31
+ /** Action en haut à droite de l'en-tête (un bouton « … », un lien « Tout voir »). */
32
+ function CardAction({ className, ...props }) {
33
+ return (_jsx("div", { "data-slot": "card-action", className: cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className), ...props }));
34
+ }
35
+ function CardContent({ className, ...props }) {
36
+ return (_jsx("div", { "data-slot": "card-content", className: cn("grid gap-4 px-5 py-5 sm:px-6 sm:py-6 [[data-slot=card-header]+&]:pt-4", className), ...props }));
37
+ }
38
+ /** Pied d'action, comme celui des réglages de Host : un filet, puis les boutons alignés à droite. */
39
+ function CardFooter({ className, ...props }) {
40
+ return (_jsx("div", { "data-slot": "card-footer", className: cn("mt-auto flex flex-wrap items-center justify-end gap-3 border-t border-foreground/5 bg-foreground/[0.02] px-5 py-3 sm:px-6", className), ...props }));
41
+ }
42
+ export { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter };
@@ -0,0 +1,12 @@
1
+ import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
2
+ /**
3
+ * Case à cocher. Monochrome : cochée, elle prend l'encre du texte (`primary`), l'accent violet
4
+ * reste au focus. Le nom accessible vient d'un `<label>` qui l'entoure, ou d'un `Field`.
5
+ *
6
+ * ```tsx
7
+ * <label className="flex items-center gap-2.5 text-sm">
8
+ * <Checkbox defaultChecked /> Recevoir le résumé hebdomadaire
9
+ * </label>
10
+ * ```
11
+ */
12
+ export declare function Checkbox({ className, indeterminate, ...props }: CheckboxPrimitive.Root.Props): React.JSX.Element;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
3
+ import MinusSignIcon from "@hugeicons/core-free-icons/MinusSignIcon";
4
+ import Tick02Icon from "@hugeicons/core-free-icons/Tick02Icon";
5
+ import { HugeiconsIcon } from "@hugeicons/react";
6
+ import { cn } from "./cn.js";
7
+ /**
8
+ * Case à cocher. Monochrome : cochée, elle prend l'encre du texte (`primary`), l'accent violet
9
+ * reste au focus. Le nom accessible vient d'un `<label>` qui l'entoure, ou d'un `Field`.
10
+ *
11
+ * ```tsx
12
+ * <label className="flex items-center gap-2.5 text-sm">
13
+ * <Checkbox defaultChecked /> Recevoir le résumé hebdomadaire
14
+ * </label>
15
+ * ```
16
+ */
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", "dark:bg-white/[0.04]", 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" }) }) }));
19
+ }
@@ -0,0 +1,56 @@
1
+ import { type IconSvgElement } from "@hugeicons/react";
2
+ export type Command = {
3
+ /** Unique dans toute la palette. */
4
+ id: string;
5
+ label: string;
6
+ icon?: IconSvgElement;
7
+ /** Autres mots qui trouvent la commande : « dns » pour « Ajouter un domaine ». */
8
+ keywords?: Array<string>;
9
+ /** Texte discret à droite : l'organisation d'un projet, « Page ». */
10
+ hint?: string;
11
+ /** Touches affichées à droite, par exemple `["G", "P"]`. */
12
+ shortcut?: Array<string>;
13
+ onSelect: () => void;
14
+ };
15
+ /** Les groupes de la famille, dans cet ordre : « Aller à », « Créer », « Changer ». */
16
+ export type CommandGroup = {
17
+ label: string;
18
+ items: Array<Command>;
19
+ };
20
+ /**
21
+ * Une commande répond à la recherche si chaque mot tapé figure dans son libellé ou ses mots-clés,
22
+ * sans tenir compte des accents ni de la casse : « creer dom » trouve « Créer un domaine ».
23
+ */
24
+ export declare function matchCommand(command: Pick<Command, "label" | "keywords">, query: string): boolean;
25
+ /**
26
+ * `Ctrl+K` (`⌘K` sur Mac) partout, même dans un champ : c'est le geste qui ouvre tout. Le produit
27
+ * le pose une fois, autour de sa coque.
28
+ */
29
+ export declare function useCommandMenuShortcut(onToggle: () => void, key?: string): void;
30
+ /**
31
+ * Palette de commandes : chercher une page, créer une ressource, changer d'organisation ou de
32
+ * thème, sans la souris. Bâtie sur l'`Autocomplete` de Base UI rendu dans un `Dialog`, comme
33
+ * l'exemple « Command palette » de sa documentation : flèches pour choisir, Entrée pour lancer,
34
+ * Échap pour fermer, focus rendu à l'élément d'origine.
35
+ *
36
+ * Sans animation, ni à l'ouverture ni à la fermeture : elle s'ouvre des dizaines de fois par
37
+ * jour, au clavier, et doit être là à l'instant où la touche est pressée.
38
+ */
39
+ export declare function CommandMenu({ groups, open: openProp, onOpenChange, placeholder, emptyLabel, shortcut, }: {
40
+ groups: Array<CommandGroup>;
41
+ open?: boolean;
42
+ onOpenChange?: (open: boolean) => void;
43
+ placeholder?: string;
44
+ emptyLabel?: string;
45
+ /** Ouvrir et fermer par `Ctrl+K`. `false` si le produit gère le raccourci lui-même. */
46
+ shortcut?: boolean;
47
+ }): React.JSX.Element;
48
+ /**
49
+ * Le bouton qui ouvre la palette, pour la souris et le doigt : « Rechercher » et le raccourci.
50
+ * Dans la barre latérale ou l'en-tête.
51
+ */
52
+ export declare function CommandMenuButton({ onClick, label, className, }: {
53
+ onClick: () => void;
54
+ label?: string;
55
+ className?: string;
56
+ }): React.JSX.Element;