@hgdc/ui 0.2.1 → 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 (51) hide show
  1. package/README.md +62 -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/checkbox.d.ts +12 -0
  11. package/dist/checkbox.js +19 -0
  12. package/dist/command-menu.d.ts +56 -0
  13. package/dist/command-menu.js +79 -0
  14. package/dist/copy-button.d.ts +26 -0
  15. package/dist/copy-button.js +46 -0
  16. package/dist/kbd.d.ts +9 -0
  17. package/dist/kbd.js +19 -0
  18. package/dist/org-switcher.d.ts +32 -0
  19. package/dist/org-switcher.js +32 -0
  20. package/dist/page-header.d.ts +27 -0
  21. package/dist/page-header.js +31 -0
  22. package/dist/popover.d.ts +13 -0
  23. package/dist/popover.js +22 -0
  24. package/dist/radio-group.d.ts +16 -0
  25. package/dist/radio-group.js +22 -0
  26. package/dist/relative-time.d.ts +16 -0
  27. package/dist/relative-time.js +54 -0
  28. package/dist/resource-list.d.ts +31 -0
  29. package/dist/resource-list.js +35 -0
  30. package/dist/secret-reveal.d.ts +13 -0
  31. package/dist/secret-reveal.js +26 -0
  32. package/dist/skeleton.d.ts +28 -0
  33. package/dist/skeleton.js +47 -0
  34. package/dist/stat.d.ts +30 -0
  35. package/dist/stat.js +26 -0
  36. package/dist/status.d.ts +22 -0
  37. package/dist/status.js +37 -0
  38. package/dist/table.d.ts +50 -0
  39. package/dist/table.js +68 -0
  40. package/dist/tabs.d.ts +16 -0
  41. package/dist/tabs.js +26 -0
  42. package/dist/time-chart.d.ts +40 -0
  43. package/dist/time-chart.js +127 -0
  44. package/dist/timeline.d.ts +21 -0
  45. package/dist/timeline.js +11 -0
  46. package/dist/tooltip.d.ts +15 -0
  47. package/dist/tooltip.js +25 -0
  48. package/dist/uptime-bar.d.ts +24 -0
  49. package/dist/uptime-bar.js +25 -0
  50. package/package.json +1 -1
  51. package/styles.css +45 -2
@@ -0,0 +1,79 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Autocomplete } from "@base-ui/react/autocomplete";
3
+ import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
4
+ import Search01Icon from "@hugeicons/core-free-icons/Search01Icon";
5
+ import { HugeiconsIcon } from "@hugeicons/react";
6
+ import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
7
+ import { FLOATING_SURFACE } from "./floating.js";
8
+ import { Kbd, KbdGroup, isApplePlatform } from "./kbd.js";
9
+ import { cn } from "./cn.js";
10
+ function normalize(text) {
11
+ return text
12
+ .normalize("NFD")
13
+ .replace(/\p{Diacritic}/gu, "")
14
+ .toLowerCase();
15
+ }
16
+ /**
17
+ * Une commande répond à la recherche si chaque mot tapé figure dans son libellé ou ses mots-clés,
18
+ * sans tenir compte des accents ni de la casse : « creer dom » trouve « Créer un domaine ».
19
+ */
20
+ export function matchCommand(command, query) {
21
+ const words = normalize(query).split(/\s+/).filter(Boolean);
22
+ if (words.length === 0)
23
+ return true;
24
+ const haystack = normalize([command.label, ...(command.keywords ?? [])].join(" "));
25
+ return words.every((word) => haystack.includes(word));
26
+ }
27
+ /**
28
+ * `Ctrl+K` (`⌘K` sur Mac) partout, même dans un champ : c'est le geste qui ouvre tout. Le produit
29
+ * le pose une fois, autour de sa coque.
30
+ */
31
+ export function useCommandMenuShortcut(onToggle, key = "k") {
32
+ useEffect(() => {
33
+ const onKey = (event) => {
34
+ if (event.key.toLowerCase() !== key || !(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey)
35
+ return;
36
+ event.preventDefault();
37
+ onToggle();
38
+ };
39
+ document.addEventListener("keydown", onKey);
40
+ return () => document.removeEventListener("keydown", onKey);
41
+ }, [onToggle, key]);
42
+ }
43
+ /**
44
+ * Palette de commandes : chercher une page, créer une ressource, changer d'organisation ou de
45
+ * thème, sans la souris. Bâtie sur l'`Autocomplete` de Base UI rendu dans un `Dialog`, comme
46
+ * l'exemple « Command palette » de sa documentation : flèches pour choisir, Entrée pour lancer,
47
+ * Échap pour fermer, focus rendu à l'élément d'origine.
48
+ *
49
+ * Sans animation, ni à l'ouverture ni à la fermeture : elle s'ouvre des dizaines de fois par
50
+ * jour, au clavier, et doit être là à l'instant où la touche est pressée.
51
+ */
52
+ export function CommandMenu({ groups, open: openProp, onOpenChange, placeholder = "Rechercher une page, une action…", emptyLabel = "Aucune commande ne correspond.", shortcut = true, }) {
53
+ const [openState, setOpenState] = useState(false);
54
+ const open = openProp ?? openState;
55
+ const setOpen = useCallback((next) => {
56
+ if (openProp === undefined)
57
+ setOpenState(next);
58
+ onOpenChange?.(next);
59
+ }, [openProp, onOpenChange]);
60
+ const toggle = useCallback(() => setOpen(!open), [open, setOpen]);
61
+ useCommandMenuShortcut(shortcut ? toggle : noop);
62
+ const visibleGroups = groups.filter((group) => group.items.length > 0);
63
+ return (_jsx(DialogPrimitive.Root, { open: open, onOpenChange: setOpen, children: _jsxs(DialogPrimitive.Portal, { children: [_jsx(DialogPrimitive.Backdrop, { className: "fixed inset-0 z-50 bg-foreground/30 dark:bg-black/60" }), _jsx(DialogPrimitive.Viewport, { className: "fixed inset-0 z-50 flex items-start justify-center px-2 pt-[min(12dvh,6rem)] pb-2", children: _jsx(DialogPrimitive.Popup, { "data-slot": "command-menu", "aria-label": "Palette de commandes", className: cn(FLOATING_SURFACE, "flex max-h-[min(36rem,calc(100dvh-2rem))] w-full max-w-xl flex-col overflow-hidden rounded-3xl shadow-2xl shadow-black/10 outline-none"), children: _jsxs(Autocomplete.Root, { open: true, inline: true, items: visibleGroups, itemToStringValue: (command) => command.label, filter: (command, query) => matchCommand(command, query), autoHighlight: "always", keepHighlight: true, children: [_jsxs(Autocomplete.InputGroup, { className: "flex items-center gap-3 border-b border-foreground/5 px-4", children: [_jsx(HugeiconsIcon, { icon: Search01Icon, strokeWidth: 2, "aria-hidden": true, className: "size-4.5 shrink-0 text-muted-foreground" }), _jsx(Autocomplete.Input, { "aria-label": "Rechercher une commande", placeholder: placeholder, className: "h-14 w-full min-w-0 bg-transparent text-base outline-none placeholder:text-muted-foreground" }), _jsx(DialogPrimitive.Close, { className: "sr-only", children: "Fermer la palette" })] }), _jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto overscroll-contain p-1.5 [scroll-padding-block:0.375rem]", children: [_jsx(Autocomplete.Empty, { className: "px-3 py-10 text-center text-sm text-muted-foreground empty:hidden", children: emptyLabel }), _jsx(Autocomplete.List, { children: (group) => (_jsxs(Autocomplete.Group, { items: group.items, className: "not-last:mb-1.5", children: [_jsx(Autocomplete.GroupLabel, { className: "px-3 pt-2 pb-1.5 text-xs font-medium text-muted-foreground", children: group.label }), _jsx(Autocomplete.Collection, { children: (command) => (_jsxs(Autocomplete.Item, { value: command, onClick: () => {
64
+ setOpen(false);
65
+ command.onSelect();
66
+ }, className: "flex min-h-11 cursor-default items-center gap-3 rounded-xl px-3 py-2 text-sm font-medium outline-none select-none data-highlighted:bg-muted lg:min-h-10", children: [command.icon ? (_jsx(HugeiconsIcon, { icon: command.icon, strokeWidth: 1.8, "aria-hidden": true, className: "size-4.5 shrink-0 text-muted-foreground" })) : null, _jsx("span", { className: "min-w-0 flex-1 truncate", children: command.label }), command.hint ? (_jsx("span", { className: "shrink-0 truncate text-xs font-normal text-muted-foreground", children: command.hint })) : null, command.shortcut ? (_jsx(KbdGroup, { className: "hidden shrink-0 sm:inline-flex", children: command.shortcut.map((key) => (_jsx(Kbd, { children: key }, key))) })) : null] }, command.id)) })] }, group.label)) })] }), _jsxs("div", { className: "hidden items-center gap-4 border-t border-foreground/5 px-4 py-2.5 text-xs text-muted-foreground sm:flex", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsxs(KbdGroup, { children: [_jsx(Kbd, { children: "\u2191" }), _jsx(Kbd, { children: "\u2193" })] }), "Choisir"] }), _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Kbd, { children: "Entr\u00E9e" }), "Ouvrir"] }), _jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx(Kbd, { children: "\u00C9chap" }), "Fermer"] })] })] }) }) })] }) }));
67
+ }
68
+ function noop() { }
69
+ function subscribeNever() {
70
+ return noop;
71
+ }
72
+ /**
73
+ * Le bouton qui ouvre la palette, pour la souris et le doigt : « Rechercher » et le raccourci.
74
+ * Dans la barre latérale ou l'en-tête.
75
+ */
76
+ export function CommandMenuButton({ onClick, label = "Rechercher", className, }) {
77
+ const apple = useSyncExternalStore(subscribeNever, isApplePlatform, () => false);
78
+ return (_jsxs("button", { type: "button", onClick: onClick, "aria-keyshortcuts": "Control+K Meta+K", className: cn("touch-target flex h-9 w-full max-w-sm min-w-0 items-center gap-2 rounded-xl bg-foreground/[0.05] px-3 text-sm text-muted-foreground outline-none transition-colors duration-(--duration-fast) ease-out hover:bg-foreground/[0.08] hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50", className), children: [_jsx(HugeiconsIcon, { icon: Search01Icon, strokeWidth: 2, "aria-hidden": true, className: "size-4 shrink-0" }), _jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: label }), _jsxs(KbdGroup, { "aria-hidden": true, className: "max-lg:hidden", children: [_jsx(Kbd, { children: apple ? "⌘" : "Ctrl" }), _jsx(Kbd, { children: "K" })] })] }));
79
+ }
@@ -0,0 +1,26 @@
1
+ /** Copie dans le presse-papiers, et dit si c'est fait pendant 1,5 s. */
2
+ export declare function useCopy(): {
3
+ copied: boolean;
4
+ copy: (value: string) => Promise<boolean>;
5
+ };
6
+ /**
7
+ * Bouton-icône qui copie une valeur ; l'icône devient une coche, et « Copié » est annoncé aux
8
+ * lecteurs d'écran.
9
+ */
10
+ export declare function CopyButton({ value, label, className, }: {
11
+ value: string;
12
+ label?: string;
13
+ className?: string;
14
+ }): React.JSX.Element;
15
+ /**
16
+ * Une commande, une valeur DNS, une URL : en police à chasse fixe, sur une ligne qui défile si
17
+ * elle est longue, avec son bouton de copie. `prompt` affiche « $ » devant une commande, sans
18
+ * qu'il soit copié.
19
+ */
20
+ export declare function Snippet({ value, prompt, label, className, }: {
21
+ value: string;
22
+ prompt?: boolean;
23
+ /** Nom accessible du bouton : « Copier la commande ». */
24
+ label?: string;
25
+ className?: string;
26
+ }): React.JSX.Element;
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Copy01Icon from "@hugeicons/core-free-icons/Copy01Icon";
3
+ import Tick02Icon from "@hugeicons/core-free-icons/Tick02Icon";
4
+ import { HugeiconsIcon } from "@hugeicons/react";
5
+ import { useEffect, useState } from "react";
6
+ import { cn } from "./cn.js";
7
+ /** Copie dans le presse-papiers, et dit si c'est fait pendant 1,5 s. */
8
+ export function useCopy() {
9
+ const [copied, setCopied] = useState(false);
10
+ useEffect(() => {
11
+ if (!copied)
12
+ return;
13
+ const timer = setTimeout(() => setCopied(false), 1500);
14
+ return () => clearTimeout(timer);
15
+ }, [copied]);
16
+ return {
17
+ copied,
18
+ copy: async (value) => {
19
+ try {
20
+ await navigator.clipboard.writeText(value);
21
+ setCopied(true);
22
+ return true;
23
+ }
24
+ catch {
25
+ // Presse-papiers refusé (page non sécurisée, permission) : rien n'est annoncé comme copié.
26
+ return false;
27
+ }
28
+ },
29
+ };
30
+ }
31
+ /**
32
+ * Bouton-icône qui copie une valeur ; l'icône devient une coche, et « Copié » est annoncé aux
33
+ * lecteurs d'écran.
34
+ */
35
+ export function CopyButton({ value, label = "Copier", className, }) {
36
+ const { copied, copy } = useCopy();
37
+ return (_jsxs("button", { type: "button", "aria-label": copied ? "Copié" : label, title: copied ? "Copié" : label, onClick: () => void copy(value), className: cn("touch-target grid size-8 shrink-0 pressable place-items-center rounded-lg text-muted-foreground outline-none hover:bg-foreground/[0.06] hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50", copied && "text-status-active-ink hover:text-status-active-ink", className), children: [_jsx(HugeiconsIcon, { icon: copied ? Tick02Icon : Copy01Icon, "aria-hidden": true, className: "size-4 pop-in" }, copied ? "ok" : "copy"), _jsx("span", { className: "sr-only", "aria-live": "polite", children: copied ? "Copié" : "" })] }));
38
+ }
39
+ /**
40
+ * Une commande, une valeur DNS, une URL : en police à chasse fixe, sur une ligne qui défile si
41
+ * elle est longue, avec son bouton de copie. `prompt` affiche « $ » devant une commande, sans
42
+ * qu'il soit copié.
43
+ */
44
+ export function Snippet({ value, prompt = false, label, className, }) {
45
+ return (_jsxs("div", { "data-slot": "snippet", className: cn("flex min-w-0 items-center gap-2 rounded-2xl bg-foreground/[0.05] py-1.5 pr-1.5 pl-4", className), children: [_jsxs("pre", { className: "min-w-0 flex-1 overflow-x-auto py-1 font-mono text-[13px] [scrollbar-width:thin]", children: [prompt ? (_jsx("span", { "aria-hidden": true, className: "mr-2 text-muted-foreground select-none", children: "$" })) : null, _jsx("code", { children: value })] }), _jsx(CopyButton, { value: value, ...(label !== undefined ? { label } : {}) })] }));
46
+ }
package/dist/kbd.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Une touche de clavier : `<Kbd>Ctrl</Kbd><Kbd>K</Kbd>`. Teinte discrète, chiffres et lettres en
3
+ * largeur fixe. Dans une infobulle, elle prend l'encre inversée.
4
+ */
5
+ export declare function Kbd({ className, ...props }: React.ComponentProps<"kbd">): React.JSX.Element;
6
+ /** Plusieurs touches d'un même raccourci, espacées et lues comme un groupe. */
7
+ export declare function KbdGroup({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element;
8
+ /** « ⌘ » sur Mac, « Ctrl » ailleurs. `false` au rendu serveur, puis corrigé au montage. */
9
+ export declare function isApplePlatform(): boolean;
package/dist/kbd.js ADDED
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from "./cn.js";
3
+ /**
4
+ * Une touche de clavier : `<Kbd>Ctrl</Kbd><Kbd>K</Kbd>`. Teinte discrète, chiffres et lettres en
5
+ * largeur fixe. Dans une infobulle, elle prend l'encre inversée.
6
+ */
7
+ export function Kbd({ className, ...props }) {
8
+ return (_jsx("kbd", { "data-slot": "kbd", className: cn("inline-flex h-5 min-w-5 items-center justify-center rounded-md bg-foreground/[0.07] px-1 font-sans text-[11px] font-medium text-muted-foreground tabular-nums select-none", className), ...props }));
9
+ }
10
+ /** Plusieurs touches d'un même raccourci, espacées et lues comme un groupe. */
11
+ export function KbdGroup({ className, ...props }) {
12
+ return _jsx("span", { "data-slot": "kbd-group", className: cn("inline-flex items-center gap-1", className), ...props });
13
+ }
14
+ /** « ⌘ » sur Mac, « Ctrl » ailleurs. `false` au rendu serveur, puis corrigé au montage. */
15
+ export function isApplePlatform() {
16
+ if (typeof navigator === "undefined")
17
+ return false;
18
+ return /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);
19
+ }
@@ -0,0 +1,32 @@
1
+ /** Les trois rôles d'une organisation de la famille. */
2
+ export type OrgRole = "owner" | "admin" | "member";
3
+ export declare const ORG_ROLE_LABEL: Record<OrgRole, string>;
4
+ export type Organization = {
5
+ id: string;
6
+ name: string;
7
+ role?: OrgRole;
8
+ /** URL d'un logo carré ; sans logo, les deux premières lettres du nom. */
9
+ logo?: string;
10
+ };
11
+ /** Un lien dans un menu, à l'allure d'un `DropdownMenuItem`. */
12
+ export declare const MENU_LINK_ITEM: string;
13
+ /** Marque d'une organisation : son logo, ou ses deux premières lettres sur un carré arrondi. */
14
+ export declare function OrgMark({ org, className, }: {
15
+ org: Pick<Organization, "name" | "logo">;
16
+ className?: string;
17
+ }): React.JSX.Element;
18
+ /**
19
+ * Haut de la barre latérale : l'organisation active, et le menu pour en changer ou en créer une.
20
+ * Présentationnel : le produit passe la liste, l'active et ce qu'il faut faire au choix (changer
21
+ * l'URL `/o/<slug>/…`), le kit n'appelle rien.
22
+ */
23
+ export declare function OrgSwitcher({ organizations, activeId, onSelect, createHref, onCreate, className, }: {
24
+ organizations: Array<Organization>;
25
+ activeId: string;
26
+ onSelect: (id: string) => void;
27
+ /** Lien « Créer une organisation ». */
28
+ createHref?: string;
29
+ /** Ou une action, si la création s'ouvre dans une boîte de dialogue. */
30
+ onCreate?: () => void;
31
+ className?: string;
32
+ }): React.JSX.Element | null;
@@ -0,0 +1,32 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import PlusSignIcon from "@hugeicons/core-free-icons/PlusSignIcon";
3
+ import Tick02Icon from "@hugeicons/core-free-icons/Tick02Icon";
4
+ import UnfoldMoreIcon from "@hugeicons/core-free-icons/UnfoldMoreIcon";
5
+ import { HugeiconsIcon } from "@hugeicons/react";
6
+ import { Menu as MenuPrimitive } from "@base-ui/react/menu";
7
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "./dropdown-menu.js";
8
+ import { cn } from "./cn.js";
9
+ export const ORG_ROLE_LABEL = {
10
+ owner: "Propriétaire",
11
+ admin: "Admin",
12
+ member: "Membre",
13
+ };
14
+ /** Un lien dans un menu, à l'allure d'un `DropdownMenuItem`. */
15
+ export const MENU_LINK_ITEM = "relative flex cursor-default items-center gap-2.5 rounded-xl px-3 py-2 text-sm font-medium outline-hidden select-none focus:bg-muted focus:text-foreground data-highlighted:bg-muted max-lg:min-h-11 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4";
16
+ /** Marque d'une organisation : son logo, ou ses deux premières lettres sur un carré arrondi. */
17
+ export function OrgMark({ org, className, }) {
18
+ if (org.logo)
19
+ return _jsx("img", { src: org.logo, alt: "", className: cn("size-8 shrink-0 rounded-lg object-cover", className) });
20
+ return (_jsx("span", { "aria-hidden": true, className: cn("grid size-8 shrink-0 place-items-center rounded-lg bg-foreground text-xs font-semibold text-background uppercase", className), children: org.name.slice(0, 2) }));
21
+ }
22
+ /**
23
+ * Haut de la barre latérale : l'organisation active, et le menu pour en changer ou en créer une.
24
+ * Présentationnel : le produit passe la liste, l'active et ce qu'il faut faire au choix (changer
25
+ * l'URL `/o/<slug>/…`), le kit n'appelle rien.
26
+ */
27
+ export function OrgSwitcher({ organizations, activeId, onSelect, createHref, onCreate, className, }) {
28
+ const active = organizations.find((org) => org.id === activeId);
29
+ if (!active)
30
+ return null;
31
+ return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: _jsx("button", { type: "button", "aria-label": `Organisation ${active.name}, changer d'organisation`, className: cn("flex h-10 w-full min-w-0 items-center gap-2.5 rounded-xl px-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:w-auto lg:rail:justify-center lg:rail:px-1", className) }), children: [_jsx(OrgMark, { org: active }), _jsxs("span", { className: "min-w-0 flex-1 lg:rail:hidden", children: [_jsx("span", { className: "block truncate text-sm font-medium", children: active.name }), active.role ? (_jsx("span", { className: "block truncate text-xs text-muted-foreground lg:text-shell-muted", children: ORG_ROLE_LABEL[active.role] })) : null] }), _jsx(HugeiconsIcon, { icon: UnfoldMoreIcon, "aria-hidden": true, className: "size-4 shrink-0 text-muted-foreground lg:text-shell-muted lg:rail:hidden" })] }), _jsxs(DropdownMenuContent, { align: "start", className: "w-64", children: [_jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Organisations" }), organizations.map((org) => (_jsxs(DropdownMenuItem, { onClick: () => onSelect(org.id), children: [_jsx(OrgMark, { org: org, className: "size-6 rounded-md bg-muted text-[10px] text-foreground" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: org.name }), org.id === activeId ? (_jsx(HugeiconsIcon, { icon: Tick02Icon, "aria-label": "Organisation active", className: "size-4" })) : org.role ? (_jsx("span", { className: "text-xs font-normal text-muted-foreground", children: ORG_ROLE_LABEL[org.role] })) : null] }, org.id)))] }), createHref !== undefined ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(MenuPrimitive.LinkItem, { href: createHref, className: MENU_LINK_ITEM, children: [_jsx(HugeiconsIcon, { icon: PlusSignIcon, "aria-hidden": true }), "Cr\u00E9er une organisation"] })] })) : onCreate !== undefined ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: onCreate, children: [_jsx(HugeiconsIcon, { icon: PlusSignIcon, "aria-hidden": true }), "Cr\u00E9er une organisation"] })] })) : null] })] }));
32
+ }
@@ -0,0 +1,27 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ export type Crumb = {
3
+ label: string;
4
+ /** Lien simple. Pour le routeur du produit, passer `render={<Link to="…" />}`. */
5
+ href?: string;
6
+ render?: ReactElement;
7
+ };
8
+ /**
9
+ * Fil d'Ariane court : organisation, projet, ressource. Le dernier élément est la page ouverte,
10
+ * en texte simple et `aria-current="page"`.
11
+ */
12
+ export declare function Breadcrumbs({ items, className }: {
13
+ items: Array<Crumb>;
14
+ className?: string;
15
+ }): React.JSX.Element;
16
+ /**
17
+ * En-tête d'une page : fil d'Ariane, titre, description, actions à droite. Une seule action
18
+ * primaire (`Button` par défaut), les autres dans un menu « … » (`DropdownMenu`). Sous `sm`, les
19
+ * actions passent sous le titre.
20
+ */
21
+ export declare function PageHeader({ title, description, actions, breadcrumbs, className, }: {
22
+ title: ReactNode;
23
+ description?: ReactNode;
24
+ actions?: ReactNode;
25
+ breadcrumbs?: Array<Crumb>;
26
+ className?: string;
27
+ }): React.JSX.Element;
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import ArrowRight01Icon from "@hugeicons/core-free-icons/ArrowRight01Icon";
3
+ import { HugeiconsIcon } from "@hugeicons/react";
4
+ import { useRender } from "@base-ui/react/use-render";
5
+ import { cn } from "./cn.js";
6
+ const crumbLink = "rounded-md text-muted-foreground transition-colors duration-(--duration-fast) ease-out outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50";
7
+ function CrumbLink({ crumb }) {
8
+ return useRender({
9
+ defaultTagName: "a",
10
+ render: crumb.render,
11
+ props: { href: crumb.href, className: crumbLink, children: crumb.label },
12
+ });
13
+ }
14
+ /**
15
+ * Fil d'Ariane court : organisation, projet, ressource. Le dernier élément est la page ouverte,
16
+ * en texte simple et `aria-current="page"`.
17
+ */
18
+ export function Breadcrumbs({ items, className }) {
19
+ return (_jsx("nav", { "aria-label": "Fil d'Ariane", className: cn("min-w-0", className), children: _jsx("ol", { className: "flex min-w-0 flex-wrap items-center gap-1 text-sm", children: items.map((crumb, index) => {
20
+ const last = index === items.length - 1;
21
+ return (_jsxs("li", { className: "flex min-w-0 items-center gap-1", children: [last || (crumb.href === undefined && crumb.render === undefined) ? (_jsx("span", { "aria-current": last ? "page" : undefined, className: cn("truncate", last ? "text-foreground" : "text-muted-foreground"), children: crumb.label })) : (_jsx(CrumbLink, { crumb: crumb })), last ? null : (_jsx(HugeiconsIcon, { icon: ArrowRight01Icon, strokeWidth: 2, "aria-hidden": true, className: "size-3.5 shrink-0 text-muted-foreground/60" }))] }, `${index}-${crumb.label}`));
22
+ }) }) }));
23
+ }
24
+ /**
25
+ * En-tête d'une page : fil d'Ariane, titre, description, actions à droite. Une seule action
26
+ * primaire (`Button` par défaut), les autres dans un menu « … » (`DropdownMenu`). Sous `sm`, les
27
+ * actions passent sous le titre.
28
+ */
29
+ export function PageHeader({ title, description, actions, breadcrumbs, className, }) {
30
+ return (_jsxs("header", { "data-slot": "page-header", className: cn("mb-8 grid gap-3", className), children: [breadcrumbs?.length ? _jsx(Breadcrumbs, { items: breadcrumbs }) : null, _jsxs("div", { className: "flex flex-wrap items-end justify-between gap-4", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h1", { className: "text-2xl font-semibold tracking-tight sm:text-[28px]", children: title }), description ? _jsx("p", { className: "mt-1.5 max-w-prose text-sm text-muted-foreground", children: description }) : null] }), actions ? _jsx("div", { className: "flex flex-wrap items-center gap-2", children: actions }) : null] })] }));
31
+ }
@@ -0,0 +1,13 @@
1
+ import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
2
+ /**
3
+ * Bulle ouverte par un clic : une explication (« Qu'est-ce qu'un domaine vérifié ? »), un petit
4
+ * formulaire, un filtre. Elle garde le focus, se ferme par Échap ou un clic à côté, et rend le
5
+ * focus à son déclencheur. Même surface que les menus (`floating.ts`).
6
+ */
7
+ declare const Popover: typeof PopoverPrimitive.Root;
8
+ declare const PopoverTrigger: typeof PopoverPrimitive.Trigger;
9
+ declare const PopoverClose: typeof PopoverPrimitive.Close;
10
+ declare function PopoverContent({ className, side, sideOffset, align, ...props }: PopoverPrimitive.Popup.Props & Pick<PopoverPrimitive.Positioner.Props, "side" | "sideOffset" | "align">): React.JSX.Element;
11
+ declare function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props): React.JSX.Element;
12
+ declare function PopoverDescription({ className, ...props }: PopoverPrimitive.Description.Props): React.JSX.Element;
13
+ export { Popover, PopoverTrigger, PopoverClose, PopoverContent, PopoverTitle, PopoverDescription };
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
3
+ import { FLOATING_COLLISION_AVOIDANCE, FLOATING_COLLISION_PADDING, FLOATING_MAX_WIDTH, FLOATING_SURFACE, } from "./floating.js";
4
+ import { cn } from "./cn.js";
5
+ /**
6
+ * Bulle ouverte par un clic : une explication (« Qu'est-ce qu'un domaine vérifié ? »), un petit
7
+ * formulaire, un filtre. Elle garde le focus, se ferme par Échap ou un clic à côté, et rend le
8
+ * focus à son déclencheur. Même surface que les menus (`floating.ts`).
9
+ */
10
+ const Popover = PopoverPrimitive.Root;
11
+ const PopoverTrigger = PopoverPrimitive.Trigger;
12
+ const PopoverClose = PopoverPrimitive.Close;
13
+ function PopoverContent({ className, side = "bottom", sideOffset = 6, align = "center", ...props }) {
14
+ return (_jsx(PopoverPrimitive.Portal, { children: _jsx(PopoverPrimitive.Positioner, { className: "isolate z-50 outline-none", side: side, sideOffset: sideOffset, align: align, collisionPadding: FLOATING_COLLISION_PADDING, collisionAvoidance: FLOATING_COLLISION_AVOIDANCE, children: _jsx(PopoverPrimitive.Popup, { "data-slot": "popover-content", className: cn(FLOATING_MAX_WIDTH, FLOATING_SURFACE, "grid w-72 origin-(--transform-origin) gap-2 rounded-2xl p-4 text-sm outline-none", "transition-[scale,opacity] duration-(--duration-fast) ease-ios data-ending-style:scale-[0.98] data-ending-style:opacity-0 data-ending-style:duration-(--duration-instant) data-starting-style:scale-[0.98] data-starting-style:opacity-0", className), ...props }) }) }));
15
+ }
16
+ function PopoverTitle({ className, ...props }) {
17
+ return _jsx(PopoverPrimitive.Title, { "data-slot": "popover-title", className: cn("font-medium", className), ...props });
18
+ }
19
+ function PopoverDescription({ className, ...props }) {
20
+ return (_jsx(PopoverPrimitive.Description, { "data-slot": "popover-description", className: cn("text-muted-foreground", className), ...props }));
21
+ }
22
+ export { Popover, PopoverTrigger, PopoverClose, PopoverContent, PopoverTitle, PopoverDescription };
@@ -0,0 +1,16 @@
1
+ import { Radio as RadioPrimitive } from "@base-ui/react/radio";
2
+ import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
3
+ /**
4
+ * Groupe de boutons radio : un seul choix parmi peu d'options, toutes visibles. Les flèches
5
+ * passent d'une option à l'autre, Tab sort du groupe. Le groupe se nomme par
6
+ * `aria-labelledby` (un titre) ou un `Fieldset`.
7
+ *
8
+ * ```tsx
9
+ * <RadioGroup aria-labelledby="role" defaultValue="member">
10
+ * <label className="flex items-center gap-2.5 text-sm"><RadioGroupItem value="admin" /> Admin</label>
11
+ * <label className="flex items-center gap-2.5 text-sm"><RadioGroupItem value="member" /> Membre</label>
12
+ * </RadioGroup>
13
+ * ```
14
+ */
15
+ export declare function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props): React.JSX.Element;
16
+ export declare function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props): React.JSX.Element;
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Radio as RadioPrimitive } from "@base-ui/react/radio";
3
+ import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
4
+ import { cn } from "./cn.js";
5
+ /**
6
+ * Groupe de boutons radio : un seul choix parmi peu d'options, toutes visibles. Les flèches
7
+ * passent d'une option à l'autre, Tab sort du groupe. Le groupe se nomme par
8
+ * `aria-labelledby` (un titre) ou un `Fieldset`.
9
+ *
10
+ * ```tsx
11
+ * <RadioGroup aria-labelledby="role" defaultValue="member">
12
+ * <label className="flex items-center gap-2.5 text-sm"><RadioGroupItem value="admin" /> Admin</label>
13
+ * <label className="flex items-center gap-2.5 text-sm"><RadioGroupItem value="member" /> Membre</label>
14
+ * </RadioGroup>
15
+ * ```
16
+ */
17
+ export function RadioGroup({ className, ...props }) {
18
+ return _jsx(RadioGroupPrimitive, { "data-slot": "radio-group", className: cn("grid gap-3", className), ...props });
19
+ }
20
+ export function RadioGroupItem({ className, ...props }) {
21
+ return (_jsx(RadioPrimitive.Root, { "data-slot": "radio-group-item", className: cn("touch-target grid size-5 shrink-0 place-items-center rounded-full border border-foreground/25 bg-background", "transition-[border-color,box-shadow] duration-(--duration-fast) ease-out outline-none", "hover:border-foreground/40 focus-visible:ring-3 focus-visible:ring-ring/50", "data-checked:border-primary data-checked:bg-primary", "data-disabled:cursor-not-allowed data-disabled:opacity-50", "aria-invalid:border-destructive data-invalid:border-destructive", "dark:bg-white/[0.04]", className), ...props, children: _jsx(RadioPrimitive.Indicator, { className: "size-2 rounded-full bg-primary-foreground data-unchecked:hidden" }) }));
22
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * « à l'instant », « il y a 3 min », « il y a 2 h », « hier », « il y a 4 j », puis la date
3
+ * (« 3 oct. », avec l'année si ce n'est pas celle de `now`). Le futur se dit « dans 5 min ».
4
+ */
5
+ export declare function formatRelative(date: number | Date, now?: number): string;
6
+ /** Date complète, pour l'infobulle : « jeudi 15 octobre 2026 à 14:32:05 ». */
7
+ export declare function formatFullDate(date: number | Date): string;
8
+ /**
9
+ * Une date relative qui se met à jour seule (toutes les 30 s), la date exacte au survol.
10
+ * `now` fixe l'instant de référence (tests, listes qui partagent une horloge).
11
+ */
12
+ export declare function RelativeTime({ date, now, className, }: {
13
+ date: number | Date;
14
+ now?: number;
15
+ className?: string;
16
+ }): React.JSX.Element;
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
4
+ import { cn } from "./cn.js";
5
+ const LOCALE = "fr-CH";
6
+ const relative = new Intl.RelativeTimeFormat(LOCALE, { numeric: "auto", style: "short" });
7
+ const dayMonth = new Intl.DateTimeFormat(LOCALE, { day: "numeric", month: "short" });
8
+ const dayMonthYear = new Intl.DateTimeFormat(LOCALE, { day: "numeric", month: "short", year: "numeric" });
9
+ const full = new Intl.DateTimeFormat(LOCALE, { dateStyle: "full", timeStyle: "medium" });
10
+ const MINUTE = 60_000;
11
+ const HOUR = 60 * MINUTE;
12
+ const DAY = 24 * HOUR;
13
+ /**
14
+ * « à l'instant », « il y a 3 min », « il y a 2 h », « hier », « il y a 4 j », puis la date
15
+ * (« 3 oct. », avec l'année si ce n'est pas celle de `now`). Le futur se dit « dans 5 min ».
16
+ */
17
+ export function formatRelative(date, now = Date.now()) {
18
+ const ms = typeof date === "number" ? date : date.getTime();
19
+ const diff = ms - now;
20
+ const abs = Math.abs(diff);
21
+ if (abs < 45_000)
22
+ return "à l'instant";
23
+ if (abs < HOUR)
24
+ return relative.format(Math.round(diff / MINUTE), "minute");
25
+ if (abs < DAY)
26
+ return relative.format(Math.round(diff / HOUR), "hour");
27
+ if (abs < 7 * DAY)
28
+ return relative.format(Math.round(diff / DAY), "day");
29
+ const d = new Date(ms);
30
+ return new Date(now).getFullYear() === d.getFullYear() ? dayMonth.format(d) : dayMonthYear.format(d);
31
+ }
32
+ /** Date complète, pour l'infobulle : « jeudi 15 octobre 2026 à 14:32:05 ». */
33
+ export function formatFullDate(date) {
34
+ return full.format(typeof date === "number" ? new Date(date) : date);
35
+ }
36
+ function useNow(enabled) {
37
+ const [now, setNow] = useState(() => Date.now());
38
+ useEffect(() => {
39
+ if (!enabled)
40
+ return;
41
+ const timer = setInterval(() => setNow(Date.now()), 30_000);
42
+ return () => clearInterval(timer);
43
+ }, [enabled]);
44
+ return now;
45
+ }
46
+ /**
47
+ * Une date relative qui se met à jour seule (toutes les 30 s), la date exacte au survol.
48
+ * `now` fixe l'instant de référence (tests, listes qui partagent une horloge).
49
+ */
50
+ export function RelativeTime({ date, now, className, }) {
51
+ const ticking = useNow(now === undefined);
52
+ const ms = typeof date === "number" ? date : date.getTime();
53
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx("time", { dateTime: new Date(ms).toISOString(), suppressHydrationWarning: true, className: cn("tabular-nums", className) }), children: formatRelative(ms, now ?? ticking) }), _jsx(TooltipContent, { children: formatFullDate(ms) })] }));
54
+ }
@@ -0,0 +1,31 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ /**
3
+ * Liste de ressources (projets, domaines, clés, moniteurs) : une carte `surface`, une ligne par
4
+ * ressource, séparées d'un filet. La forme des listes de Host, plus lisible qu'un tableau quand
5
+ * chaque ressource tient en un titre et une ligne de méta.
6
+ */
7
+ export declare function ResourceList({ className, surface, ...props }: React.ComponentProps<"ul"> & {
8
+ surface?: boolean;
9
+ }): React.JSX.Element;
10
+ /**
11
+ * Une ressource : icône, titre, méta, badge, actions. Avec `href` ou `render`, le titre devient
12
+ * le lien de la ligne, étiré sur toute sa surface ; les actions restent cliquables au-dessus.
13
+ */
14
+ export declare function ResourceItem({ icon, title, meta, badge, actions, href, render, className, }: {
15
+ /** Icône, favicon ou marque, dans une tuile de 40 px. */
16
+ icon?: ReactNode;
17
+ title: ReactNode;
18
+ /** Une ligne de contexte : adresse, date, auteur. Tronquée si trop longue. */
19
+ meta?: ReactNode;
20
+ badge?: ReactNode;
21
+ /** Boutons-icônes ou menu « … ». */
22
+ actions?: ReactNode;
23
+ href?: string;
24
+ /** Lien du routeur du produit : `render={<Link to="…" />}`. */
25
+ render?: ReactElement;
26
+ className?: string;
27
+ }): React.JSX.Element;
28
+ /** Lignes de chargement à la forme d'un `ResourceItem` avec icône. */
29
+ export declare function ResourceListSkeleton({ rows }: {
30
+ rows?: number;
31
+ }): React.JSX.Element;
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } 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
+ import { Skeleton } from "./skeleton.js";
6
+ /**
7
+ * Liste de ressources (projets, domaines, clés, moniteurs) : une carte `surface`, une ligne par
8
+ * ressource, séparées d'un filet. La forme des listes de Host, plus lisible qu'un tableau quand
9
+ * chaque ressource tient en un titre et une ligne de méta.
10
+ */
11
+ export function ResourceList({ className, surface = true, ...props }) {
12
+ return (_jsx("ul", { "data-slot": "resource-list", className: cn("overflow-hidden", surface && "surface rounded-3xl", className), ...props }));
13
+ }
14
+ /**
15
+ * Une ressource : icône, titre, méta, badge, actions. Avec `href` ou `render`, le titre devient
16
+ * le lien de la ligne, étiré sur toute sa surface ; les actions restent cliquables au-dessus.
17
+ */
18
+ export function ResourceItem({ icon, title, meta, badge, actions, href, render, className, }) {
19
+ const linked = href !== undefined || render !== undefined;
20
+ return (_jsxs("li", { "data-slot": "resource-item", className: cn("relative flex min-h-16 items-center gap-3 px-4 py-3 transition-colors duration-(--duration-fast) ease-out not-first:border-t not-first:border-foreground/5 sm:gap-4 sm:px-5", linked && "hover:bg-foreground/[0.03] has-focus-visible:bg-foreground/[0.03]", className), children: [icon ? (_jsx("span", { "aria-hidden": true, className: "grid size-10 shrink-0 place-items-center overflow-hidden rounded-xl bg-background text-muted-foreground dark:bg-muted [&_svg:not([class*='size-'])]:size-5", children: icon })) : null, _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "truncate text-sm font-medium", children: linked ? (_jsx(ResourceLink, { href: href, render: render, children: title })) : (title) }), meta ? _jsx("p", { className: "mt-0.5 truncate text-xs text-muted-foreground", children: meta }) : null] }), badge ? _jsx("div", { className: "hidden shrink-0 sm:block", children: badge }) : null, actions ? _jsx("div", { className: "relative z-1 flex shrink-0 items-center gap-1", children: actions }) : null] }));
21
+ }
22
+ function ResourceLink({ href, render, children, }) {
23
+ return useRender({
24
+ defaultTagName: "a",
25
+ render,
26
+ props: mergeProps({
27
+ "data-slot": "resource-link",
28
+ className: "outline-none after:absolute after:inset-0 after:content-[''] focus-visible:after:ring-2 focus-visible:after:ring-ring/50 focus-visible:after:ring-inset",
29
+ }, href !== undefined ? { href, children } : { children }),
30
+ });
31
+ }
32
+ /** Lignes de chargement à la forme d'un `ResourceItem` avec icône. */
33
+ export function ResourceListSkeleton({ rows = 4 }) {
34
+ return (_jsx("ul", { "aria-hidden": true, "data-slot": "resource-list-skeleton", className: "surface overflow-hidden rounded-3xl", children: Array.from({ length: rows }, (_, i) => (_jsxs("li", { className: "flex min-h-16 items-center gap-4 px-5 py-3 not-first:border-t not-first:border-foreground/5", children: [_jsx(Skeleton, { className: "size-10 shrink-0" }), _jsxs("div", { className: "grid flex-1 gap-2", children: [_jsx(Skeleton, { className: "h-4 w-1/3 rounded-md" }), _jsx(Skeleton, { className: "h-3 w-1/2 rounded-md" })] })] }, i))) }));
35
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Un secret qui ne s'affiche qu'une fois (clé d'API, jeton) : masqué à l'écran tant qu'on ne
3
+ * demande pas à le voir, copiable sans l'afficher, puis retiré de la page quand la personne
4
+ * confirme l'avoir mis de côté. Le produit, lui, n'en garde que l'empreinte.
5
+ */
6
+ export declare function SecretReveal({ value, label, onDismiss, className, }: {
7
+ value: string;
8
+ /** Ce qu'est le secret : « Clé d'API », « Jeton de déploiement ». */
9
+ label?: string;
10
+ /** Appelé quand la personne confirme l'avoir copié : le produit oublie la valeur en clair. */
11
+ onDismiss?: () => void;
12
+ className?: string;
13
+ }): React.JSX.Element;
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import ViewIcon from "@hugeicons/core-free-icons/ViewIcon";
3
+ import ViewOffIcon from "@hugeicons/core-free-icons/ViewOffIcon";
4
+ import { HugeiconsIcon } from "@hugeicons/react";
5
+ import { useId, useState } from "react";
6
+ import { Alert } from "./alert.js";
7
+ import { Button } from "./button.js";
8
+ import { CopyButton } from "./copy-button.js";
9
+ import { cn } from "./cn.js";
10
+ const MASK = "•".repeat(24);
11
+ /**
12
+ * Un secret qui ne s'affiche qu'une fois (clé d'API, jeton) : masqué à l'écran tant qu'on ne
13
+ * demande pas à le voir, copiable sans l'afficher, puis retiré de la page quand la personne
14
+ * confirme l'avoir mis de côté. Le produit, lui, n'en garde que l'empreinte.
15
+ */
16
+ export function SecretReveal({ value, label = "Clé secrète", onDismiss, className, }) {
17
+ const [visible, setVisible] = useState(false);
18
+ const [dismissed, setDismissed] = useState(false);
19
+ const id = useId();
20
+ if (dismissed)
21
+ return (_jsxs("div", { "data-slot": "secret-reveal", "data-state": "dismissed", className: cn("grid gap-2", className), children: [_jsx("p", { className: "text-sm font-medium", children: label }), _jsxs("p", { className: "rounded-2xl bg-foreground/[0.05] px-4 py-3 font-mono text-[13px] text-muted-foreground", children: [_jsx("span", { "aria-hidden": true, children: MASK }), _jsx("span", { className: "sr-only", children: "Valeur masqu\u00E9e" })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Cette valeur n'est plus affich\u00E9e. Pour en obtenir une nouvelle, cr\u00E9e une autre cl\u00E9." })] }));
22
+ return (_jsxs("div", { "data-slot": "secret-reveal", "data-state": "shown", className: cn("grid gap-3", className), children: [_jsx(Alert, { tone: "warning", title: "Copie-la maintenant", children: "Tu ne pourras plus la revoir une fois cette fen\u00EAtre ferm\u00E9e." }), _jsxs("div", { className: "grid gap-2", children: [_jsx("p", { id: id, className: "text-sm font-medium", children: label }), _jsxs("div", { className: "flex min-w-0 items-center gap-1 rounded-2xl bg-foreground/[0.05] py-1.5 pr-1.5 pl-4", children: [_jsx("code", { "aria-labelledby": id, className: "min-w-0 flex-1 truncate py-1 font-mono text-[13px]", children: visible ? value : MASK }), _jsx("button", { type: "button", onClick: () => setVisible((v) => !v), "aria-label": visible ? "Masquer" : "Afficher", "aria-pressed": visible, title: visible ? "Masquer" : "Afficher", className: "touch-target grid size-8 shrink-0 place-items-center rounded-lg text-muted-foreground outline-none hover:bg-foreground/[0.06] hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50", children: _jsx(HugeiconsIcon, { icon: visible ? ViewOffIcon : ViewIcon, "aria-hidden": true, className: "size-4" }) }), _jsx(CopyButton, { value: value, label: `Copier : ${label}` })] })] }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { variant: "outline", onClick: () => {
23
+ setDismissed(true);
24
+ onDismiss?.();
25
+ }, children: "C'est copi\u00E9" }) })] }));
26
+ }
@@ -0,0 +1,28 @@
1
+ import { type ReactNode } from "react";
2
+ /**
3
+ * Un bloc de chargement : il prend la forme exacte de ce qu'il remplace (hauteur d'une ligne,
4
+ * largeur d'un titre), pour que rien ne saute à l'arrivée des données. `bg-foreground/6` plutôt
5
+ * que `bg-muted` : posé sur une carte `surface`, qui est déjà `muted` en clair, il restait invisible.
6
+ */
7
+ export declare function Skeleton({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
8
+ export type PendingTiming = {
9
+ /** Attente avant d'afficher le squelette : une réponse rapide n'en montre aucun (ms). */
10
+ delay?: number;
11
+ /** Durée minimale d'affichage d'un squelette apparu, pour qu'il ne clignote pas (ms). */
12
+ minDuration?: number;
13
+ };
14
+ /**
15
+ * Faut-il montrer le squelette ? Il n'apparaît qu'après `delay` (200 ms) et, une fois là, reste
16
+ * au moins `minDuration` (400 ms) : une donnée qui arrive en 250 ms ne fait pas clignoter
17
+ * l'écran deux fois.
18
+ */
19
+ export declare function usePendingVisibility(pending: boolean, { delay, minDuration }?: PendingTiming): boolean;
20
+ /**
21
+ * Le contenu, ou son squelette quand il se fait attendre. Pendant le délai, rien n'est rendu :
22
+ * une page qui répond vite passe directement du vide au contenu.
23
+ */
24
+ export declare function Pending({ pending, fallback, children, delay, minDuration, }: PendingTiming & {
25
+ pending: boolean;
26
+ fallback: ReactNode;
27
+ children?: ReactNode;
28
+ }): React.JSX.Element | null;