@hgdc/ui 0.2.1 → 0.4.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 (71) hide show
  1. package/README.md +78 -21
  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/card.d.ts +24 -0
  9. package/dist/card.js +42 -0
  10. package/dist/chart-path.d.ts +9 -0
  11. package/dist/chart-path.js +46 -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/data-table.d.ts +70 -0
  19. package/dist/data-table.js +46 -0
  20. package/dist/destructive-dialog.d.ts +28 -0
  21. package/dist/destructive-dialog.js +74 -0
  22. package/dist/dns-record-table.d.ts +30 -0
  23. package/dist/dns-record-table.js +33 -0
  24. package/dist/error-card.d.ts +23 -0
  25. package/dist/error-card.js +35 -0
  26. package/dist/filter-bar.d.ts +50 -0
  27. package/dist/filter-bar.js +88 -0
  28. package/dist/kbd.d.ts +9 -0
  29. package/dist/kbd.js +19 -0
  30. package/dist/log-viewer.d.ts +39 -0
  31. package/dist/log-viewer.js +114 -0
  32. package/dist/middle-truncate.d.ts +17 -0
  33. package/dist/middle-truncate.js +25 -0
  34. package/dist/org-switcher.d.ts +32 -0
  35. package/dist/org-switcher.js +32 -0
  36. package/dist/page-header.d.ts +27 -0
  37. package/dist/page-header.js +31 -0
  38. package/dist/popover.d.ts +13 -0
  39. package/dist/popover.js +22 -0
  40. package/dist/radio-group.d.ts +16 -0
  41. package/dist/radio-group.js +22 -0
  42. package/dist/relative-time.d.ts +16 -0
  43. package/dist/relative-time.js +54 -0
  44. package/dist/resource-list.d.ts +31 -0
  45. package/dist/resource-list.js +35 -0
  46. package/dist/secret-reveal.d.ts +13 -0
  47. package/dist/secret-reveal.js +26 -0
  48. package/dist/skeleton.d.ts +28 -0
  49. package/dist/skeleton.js +47 -0
  50. package/dist/sonner.d.ts +20 -1
  51. package/dist/sonner.js +43 -2
  52. package/dist/sparkline.d.ts +21 -0
  53. package/dist/sparkline.js +28 -0
  54. package/dist/stat.d.ts +30 -0
  55. package/dist/stat.js +26 -0
  56. package/dist/status.d.ts +22 -0
  57. package/dist/status.js +37 -0
  58. package/dist/table.d.ts +50 -0
  59. package/dist/table.js +68 -0
  60. package/dist/tabs.d.ts +16 -0
  61. package/dist/tabs.js +26 -0
  62. package/dist/time-chart.d.ts +40 -0
  63. package/dist/time-chart.js +90 -0
  64. package/dist/timeline.d.ts +21 -0
  65. package/dist/timeline.js +11 -0
  66. package/dist/tooltip.d.ts +15 -0
  67. package/dist/tooltip.js +25 -0
  68. package/dist/uptime-bar.d.ts +29 -0
  69. package/dist/uptime-bar.js +57 -0
  70. package/package.json +1 -1
  71. package/styles.css +45 -2
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,34 +19,91 @@ 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
+ ## Listes, journaux et retours
73
+
74
+ | Import | Exemple |
75
+ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
76
+ | `filter-bar` | `<FilterBar search={{ value: q, onChange }} filters={[{ key: "status", label: "Statut", options }]} values={{ status }} onFilterChange={…} onReset={…} />` |
77
+ | `data-table` | `<DataTable label="Déploiements" rows={rows} columns={columns} rowKey={(d) => d.id} sort={sort} onSortChange={…} filters={<FilterBar …/>} loadMore={…} />` |
78
+ | `dns-record-table` | `<DnsRecordTable records={[{ type: "MX", name: "send", value, priority: 10, status: "pending" }]} />` (Vérifié, En attente, Échec, À ajouter) |
79
+ | `log-viewer` | `<LogViewer lines={[{ id, time, level: "error", message }]} live />` : niveaux, recherche, « Suivre », copie ; seules les lignes visibles sont rendues |
80
+ | `sparkline` | `<Sparkline label="Envois sur 7 jours" values={[3, 8, 5]} color={2} />` |
81
+ | `middle-truncate` | `<MiddleTruncate text={deployment.url} />` : « …/routes/index.tsx », texte entier dans la page |
82
+ | `error-card` | `<ErrorCard title="Impossible de charger les journaux" detail={message} reference={requestId} onRetry={refetch} />` |
83
+ | `destructive-dialog` | `<DestructiveDialog open={open} onOpenChange={setOpen} title="Supprimer le domaine" question="…" name="acme.ch" onConfirm={remove} />` |
84
+ | `sonner` | `<Toaster />` une fois, puis `toastUndo("Domaine retiré", { onUndo: restore })` ; `errorMessage(error)` pour un toast d'erreur |
85
+
86
+ Filtres, tri et page vivent dans l'URL du produit : les composants sont contrôlés et ne connaissent pas le routeur.
87
+
88
+ ## Contrôles et surfaces flottantes
89
+
90
+ | Import | Exemple |
91
+ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
92
+ | `checkbox` | `<label className="flex items-center gap-2.5"><Checkbox defaultChecked /> Résumé hebdomadaire</label>` |
93
+ | `radio-group` | `<RadioGroup aria-labelledby="role" defaultValue="member"><RadioGroupItem value="admin" />…</RadioGroup>` |
94
+ | `tabs` | `<Tabs defaultValue="apercu"><TabsList><TabsTab value="apercu">Aperçu</TabsTab></TabsList><TabsPanel value="apercu">…</TabsPanel></Tabs>` |
95
+ | `tooltip` | `<TooltipProvider>` autour de l'app, puis `<Tooltip><TooltipTrigger aria-label="Copier">…</TooltipTrigger><TooltipContent>Copier</TooltipContent></Tooltip>` |
96
+ | `popover` | `<Popover><PopoverTrigger>Aide</PopoverTrigger><PopoverContent><PopoverTitle>…</PopoverTitle></PopoverContent></Popover>` |
38
97
 
39
98
  ## Règles de la DA
40
99
 
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
- - Au survol, seule la couleur change : ni ombre, ni zoom. Le rebond est réservé à l'appui.
45
- - Couleurs de statut : la couleur vive pour les pastilles, son encre (`text-status-active-ink`, `text-info-ink`…) pour le texte. L'information est bleue, le violet reste l'accent.
46
- - Chargement : l'anneau de la DA, `<Spinner />` dans un bouton, `<Spinner size={8} tone="accent" />` seul sur une page.
47
- - Un `Button` rendu en lien (`render={<Link />}`) reste un lien pour les lecteurs d'écran.
100
+ - Chrome monochrome, un seul accent violet : focus, lien, un geste principal par écran. « Vous êtes ici » (barre, onglets) reste en teinte.
101
+ - Profondeur par la teinte : cartes `surface` sans ombre ni contour, ombre réservée à ce qui flotte, aucun flou.
102
+ - Au survol, seule la couleur change. Le rebond est réservé à l'appui. La palette de commandes s'ouvre sans animation.
103
+ - Un statut s'écrit toujours (point et libellé), jamais par la couleur seule. Chiffres en `tabular-nums`.
104
+ - 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 ».
48
105
  - Référence de l'interface : Host. Couleur d'un client : `--brand-accent`.
49
106
 
50
- 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).
107
+ 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).
51
108
 
52
109
  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/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,9 @@
1
+ /**
2
+ * Tracés SVG partagés par `TimeChart` et `Sparkline`. Repère de `values.length` unités de large
3
+ * sur 100 de haut : le point `i` est au centre de sa colonne (`i + 0.5`), `min` en bas, `max` en
4
+ * haut. Le SVG s'étire ensuite (`preserveAspectRatio="none"`).
5
+ */
6
+ /** Une ligne, interrompue là où une valeur manque (`null`). */
7
+ export declare function linePath(values: ReadonlyArray<number | null>, max: number, min?: number): string;
8
+ /** L'aire sous la ligne : une forme par segment continu, fermée sur le bas du repère. */
9
+ export declare function areaPath(values: ReadonlyArray<number | null>, max: number, min?: number): string;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Tracés SVG partagés par `TimeChart` et `Sparkline`. Repère de `values.length` unités de large
3
+ * sur 100 de haut : le point `i` est au centre de sa colonne (`i + 0.5`), `min` en bas, `max` en
4
+ * haut. Le SVG s'étire ensuite (`preserveAspectRatio="none"`).
5
+ */
6
+ function y(value, min, max) {
7
+ const range = max - min;
8
+ return range > 0 ? 100 - ((value - min) / range) * 100 : 50;
9
+ }
10
+ /** Une ligne, interrompue là où une valeur manque (`null`). */
11
+ export function linePath(values, max, min = 0) {
12
+ let d = "";
13
+ let open = false;
14
+ values.forEach((v, i) => {
15
+ if (v === null) {
16
+ open = false;
17
+ return;
18
+ }
19
+ d += `${open ? "L" : "M"}${i + 0.5} ${y(v, min, max)}`;
20
+ open = true;
21
+ });
22
+ return d;
23
+ }
24
+ /** L'aire sous la ligne : une forme par segment continu, fermée sur le bas du repère. */
25
+ export function areaPath(values, max, min = 0) {
26
+ const parts = [];
27
+ let start = -1;
28
+ const close = (end) => {
29
+ if (start < 0)
30
+ return;
31
+ const top = values
32
+ .slice(start, end)
33
+ .map((v, j) => `${j === 0 ? "M" : "L"}${start + j + 0.5} ${y(v ?? min, min, max)}`)
34
+ .join("");
35
+ parts.push(`${top}L${end - 1 + 0.5} 100L${start + 0.5} 100Z`);
36
+ start = -1;
37
+ };
38
+ values.forEach((v, i) => {
39
+ if (v === null)
40
+ close(i);
41
+ else if (start < 0)
42
+ start = i;
43
+ });
44
+ close(values.length);
45
+ return parts.join("");
46
+ }
@@ -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
+ }