@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.
- package/README.md +78 -21
- package/dist/account-menu.d.ts +32 -0
- package/dist/account-menu.js +39 -0
- package/dist/alert.d.ts +19 -0
- package/dist/alert.js +25 -0
- package/dist/app-shell.d.ts +56 -0
- package/dist/app-shell.js +107 -0
- package/dist/card.d.ts +24 -0
- package/dist/card.js +42 -0
- package/dist/chart-path.d.ts +9 -0
- package/dist/chart-path.js +46 -0
- package/dist/checkbox.d.ts +12 -0
- package/dist/checkbox.js +19 -0
- package/dist/command-menu.d.ts +56 -0
- package/dist/command-menu.js +79 -0
- package/dist/copy-button.d.ts +26 -0
- package/dist/copy-button.js +46 -0
- package/dist/data-table.d.ts +70 -0
- package/dist/data-table.js +46 -0
- package/dist/destructive-dialog.d.ts +28 -0
- package/dist/destructive-dialog.js +74 -0
- package/dist/dns-record-table.d.ts +30 -0
- package/dist/dns-record-table.js +33 -0
- package/dist/error-card.d.ts +23 -0
- package/dist/error-card.js +35 -0
- package/dist/filter-bar.d.ts +50 -0
- package/dist/filter-bar.js +88 -0
- package/dist/kbd.d.ts +9 -0
- package/dist/kbd.js +19 -0
- package/dist/log-viewer.d.ts +39 -0
- package/dist/log-viewer.js +114 -0
- package/dist/middle-truncate.d.ts +17 -0
- package/dist/middle-truncate.js +25 -0
- package/dist/org-switcher.d.ts +32 -0
- package/dist/org-switcher.js +32 -0
- package/dist/page-header.d.ts +27 -0
- package/dist/page-header.js +31 -0
- package/dist/popover.d.ts +13 -0
- package/dist/popover.js +22 -0
- package/dist/radio-group.d.ts +16 -0
- package/dist/radio-group.js +22 -0
- package/dist/relative-time.d.ts +16 -0
- package/dist/relative-time.js +54 -0
- package/dist/resource-list.d.ts +31 -0
- package/dist/resource-list.js +35 -0
- package/dist/secret-reveal.d.ts +13 -0
- package/dist/secret-reveal.js +26 -0
- package/dist/skeleton.d.ts +28 -0
- package/dist/skeleton.js +47 -0
- package/dist/sonner.d.ts +20 -1
- package/dist/sonner.js +43 -2
- package/dist/sparkline.d.ts +21 -0
- package/dist/sparkline.js +28 -0
- package/dist/stat.d.ts +30 -0
- package/dist/stat.js +26 -0
- package/dist/status.d.ts +22 -0
- package/dist/status.js +37 -0
- package/dist/table.d.ts +50 -0
- package/dist/table.js +68 -0
- package/dist/tabs.d.ts +16 -0
- package/dist/tabs.js +26 -0
- package/dist/time-chart.d.ts +40 -0
- package/dist/time-chart.js +90 -0
- package/dist/timeline.d.ts +21 -0
- package/dist/timeline.js +11 -0
- package/dist/tooltip.d.ts +15 -0
- package/dist/tooltip.js +25 -0
- package/dist/uptime-bar.d.ts +29 -0
- package/dist/uptime-bar.js +57 -0
- package/package.json +1 -1
- package/styles.css +45 -2
|
@@ -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;
|
package/dist/skeleton.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { cn } from "./cn.js";
|
|
4
|
+
/**
|
|
5
|
+
* Un bloc de chargement : il prend la forme exacte de ce qu'il remplace (hauteur d'une ligne,
|
|
6
|
+
* largeur d'un titre), pour que rien ne saute à l'arrivée des données. `bg-foreground/6` plutôt
|
|
7
|
+
* que `bg-muted` : posé sur une carte `surface`, qui est déjà `muted` en clair, il restait invisible.
|
|
8
|
+
*/
|
|
9
|
+
export function Skeleton({ className, ...props }) {
|
|
10
|
+
return (_jsx("div", { "aria-hidden": true, "data-slot": "skeleton", className: cn("skeleton-pulse rounded-xl bg-foreground/6", className), ...props }));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Faut-il montrer le squelette ? Il n'apparaît qu'après `delay` (200 ms) et, une fois là, reste
|
|
14
|
+
* au moins `minDuration` (400 ms) : une donnée qui arrive en 250 ms ne fait pas clignoter
|
|
15
|
+
* l'écran deux fois.
|
|
16
|
+
*/
|
|
17
|
+
export function usePendingVisibility(pending, { delay = 200, minDuration = 400 } = {}) {
|
|
18
|
+
const [visible, setVisible] = useState(false);
|
|
19
|
+
const shownAt = useRef(0);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (pending === visible)
|
|
22
|
+
return;
|
|
23
|
+
const wait = pending ? delay : Math.max(0, shownAt.current + minDuration - Date.now());
|
|
24
|
+
const timer = setTimeout(() => {
|
|
25
|
+
if (pending)
|
|
26
|
+
shownAt.current = Date.now();
|
|
27
|
+
setVisible(pending);
|
|
28
|
+
}, wait);
|
|
29
|
+
return () => clearTimeout(timer);
|
|
30
|
+
}, [pending, visible, delay, minDuration]);
|
|
31
|
+
return visible;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Le contenu, ou son squelette quand il se fait attendre. Pendant le délai, rien n'est rendu :
|
|
35
|
+
* une page qui répond vite passe directement du vide au contenu.
|
|
36
|
+
*/
|
|
37
|
+
export function Pending({ pending, fallback, children, delay, minDuration, }) {
|
|
38
|
+
const showFallback = usePendingVisibility(pending, {
|
|
39
|
+
...(delay !== undefined ? { delay } : {}),
|
|
40
|
+
...(minDuration !== undefined ? { minDuration } : {}),
|
|
41
|
+
});
|
|
42
|
+
if (showFallback)
|
|
43
|
+
return (_jsxs("div", { "aria-busy": "true", "data-slot": "pending", children: [_jsx("span", { className: "sr-only", role: "status", children: "Chargement" }), fallback] }));
|
|
44
|
+
if (pending)
|
|
45
|
+
return null;
|
|
46
|
+
return _jsx(_Fragment, { children: children });
|
|
47
|
+
}
|
package/dist/sonner.d.ts
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
1
|
import { type ToasterProps } from "sonner";
|
|
2
2
|
declare function Toaster(props: ToasterProps): React.JSX.Element;
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Retour d'une action réversible, avec « Annuler » pendant quelques secondes : on agit tout de
|
|
5
|
+
* suite (mise à jour optimiste), sans boîte de confirmation, et on laisse revenir en arrière.
|
|
6
|
+
* Une action qui ne se rattrape pas passe par `DestructiveDialog`.
|
|
7
|
+
*
|
|
8
|
+
* `onUndo` défait l'action. `onCommit`, s'il est donné, part quand le toast se ferme sans
|
|
9
|
+
* annulation (délai écoulé, fermé à la main) : pour une action différée, envoyée seulement
|
|
10
|
+
* une fois le délai passé. Rend l'identifiant du toast (`toast.dismiss(id)`).
|
|
11
|
+
*/
|
|
12
|
+
declare function toastUndo(message: React.ReactNode, { onUndo, onCommit, description, duration, undoLabel, }: {
|
|
13
|
+
onUndo: () => unknown;
|
|
14
|
+
onCommit?: () => unknown;
|
|
15
|
+
description?: React.ReactNode;
|
|
16
|
+
/** En millisecondes ; Sonner met le délai en pause au survol et quand l'onglet est caché. */
|
|
17
|
+
duration?: number;
|
|
18
|
+
undoLabel?: string;
|
|
19
|
+
}): string | number;
|
|
20
|
+
/** Le message d'une erreur, lisible : celui de l'`Error`, sinon une phrase générique. */
|
|
21
|
+
declare function errorMessage(error: unknown, fallback?: string): string;
|
|
22
|
+
export { Toaster, toastUndo, errorMessage };
|
package/dist/sonner.js
CHANGED
|
@@ -4,7 +4,7 @@ import CheckmarkCircle02Icon from "@hugeicons/core-free-icons/CheckmarkCircle02I
|
|
|
4
4
|
import InformationCircleIcon from "@hugeicons/core-free-icons/InformationCircleIcon";
|
|
5
5
|
import MultiplicationSignCircleIcon from "@hugeicons/core-free-icons/MultiplicationSignCircleIcon";
|
|
6
6
|
import { HugeiconsIcon } from "@hugeicons/react";
|
|
7
|
-
import { Toaster as Sonner } from "sonner";
|
|
7
|
+
import { Toaster as Sonner, toast } from "sonner";
|
|
8
8
|
import { Spinner } from "./spinner.js";
|
|
9
9
|
import { useTheme } from "./theme.js";
|
|
10
10
|
function Toaster(props) {
|
|
@@ -30,4 +30,45 @@ function Toaster(props) {
|
|
|
30
30
|
},
|
|
31
31
|
}, ...props }));
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Retour d'une action réversible, avec « Annuler » pendant quelques secondes : on agit tout de
|
|
35
|
+
* suite (mise à jour optimiste), sans boîte de confirmation, et on laisse revenir en arrière.
|
|
36
|
+
* Une action qui ne se rattrape pas passe par `DestructiveDialog`.
|
|
37
|
+
*
|
|
38
|
+
* `onUndo` défait l'action. `onCommit`, s'il est donné, part quand le toast se ferme sans
|
|
39
|
+
* annulation (délai écoulé, fermé à la main) : pour une action différée, envoyée seulement
|
|
40
|
+
* une fois le délai passé. Rend l'identifiant du toast (`toast.dismiss(id)`).
|
|
41
|
+
*/
|
|
42
|
+
function toastUndo(message, { onUndo, onCommit, description, duration = 6000, undoLabel = "Annuler", }) {
|
|
43
|
+
let settled = false;
|
|
44
|
+
const commit = () => {
|
|
45
|
+
if (settled)
|
|
46
|
+
return;
|
|
47
|
+
settled = true;
|
|
48
|
+
void onCommit?.();
|
|
49
|
+
};
|
|
50
|
+
return toast(message, {
|
|
51
|
+
description,
|
|
52
|
+
duration,
|
|
53
|
+
action: {
|
|
54
|
+
label: undoLabel,
|
|
55
|
+
onClick: () => {
|
|
56
|
+
if (settled)
|
|
57
|
+
return;
|
|
58
|
+
settled = true;
|
|
59
|
+
void onUndo();
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
onAutoClose: commit,
|
|
63
|
+
onDismiss: commit,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** Le message d'une erreur, lisible : celui de l'`Error`, sinon une phrase générique. */
|
|
67
|
+
function errorMessage(error, fallback = "Quelque chose n'a pas marché. Réessaie.") {
|
|
68
|
+
if (error instanceof Error && error.message.trim() !== "")
|
|
69
|
+
return error.message;
|
|
70
|
+
if (typeof error === "string" && error.trim() !== "")
|
|
71
|
+
return error;
|
|
72
|
+
return fallback;
|
|
73
|
+
}
|
|
74
|
+
export { Toaster, toastUndo, errorMessage };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Ce que la courbe dit, en une phrase, pour qui ne la voit pas. */
|
|
2
|
+
export declare function describeSparkline(label: string, values: ReadonlyArray<number | null>, format?: (value: number) => string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Tendance en un trait, sans axe ni info-bulle, à côté d'un chiffre (ligne de tableau, `StatTile`).
|
|
5
|
+
* L'échelle va du minimum au maximum de la série : c'est la forme qui compte, pas le zéro. Pour lire
|
|
6
|
+
* des valeurs, c'est `TimeChart`. Les lecteurs d'écran entendent l'étendue et la dernière valeur.
|
|
7
|
+
*/
|
|
8
|
+
export declare function Sparkline({ values, label, color, area, height, formatValue, className, }: {
|
|
9
|
+
/** Du plus ancien au plus récent ; `null` : pas de mesure, le trait s'interrompt. */
|
|
10
|
+
values: ReadonlyArray<number | null>;
|
|
11
|
+
/** Ce que mesure la courbe : « Envois sur 7 jours ». */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Teinte `--chart-1` à `--chart-6`. */
|
|
14
|
+
color?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
15
|
+
/** Lavis sous la courbe. */
|
|
16
|
+
area?: boolean;
|
|
17
|
+
/** En pixels ; la largeur suit le conteneur. */
|
|
18
|
+
height?: number;
|
|
19
|
+
formatValue?: (value: number) => string;
|
|
20
|
+
className?: string;
|
|
21
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { areaPath, linePath } from "./chart-path.js";
|
|
3
|
+
import { cn } from "./cn.js";
|
|
4
|
+
import { CHART_COLORS } from "./time-chart.js";
|
|
5
|
+
const number = new Intl.NumberFormat("fr-CH");
|
|
6
|
+
/** Ce que la courbe dit, en une phrase, pour qui ne la voit pas. */
|
|
7
|
+
export function describeSparkline(label, values, format = (v) => number.format(v)) {
|
|
8
|
+
const known = values.filter((v) => v !== null);
|
|
9
|
+
const last = known.at(-1);
|
|
10
|
+
if (last === undefined)
|
|
11
|
+
return `${label} : aucune donnée`;
|
|
12
|
+
return `${label} : de ${format(Math.min(...known))} à ${format(Math.max(...known))}, dernière valeur ${format(last)}`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Tendance en un trait, sans axe ni info-bulle, à côté d'un chiffre (ligne de tableau, `StatTile`).
|
|
16
|
+
* L'échelle va du minimum au maximum de la série : c'est la forme qui compte, pas le zéro. Pour lire
|
|
17
|
+
* des valeurs, c'est `TimeChart`. Les lecteurs d'écran entendent l'étendue et la dernière valeur.
|
|
18
|
+
*/
|
|
19
|
+
export function Sparkline({ values, label, color = 1, area = true, height = 32, formatValue, className, }) {
|
|
20
|
+
const known = values.filter((v) => v !== null);
|
|
21
|
+
const min = Math.min(...known);
|
|
22
|
+
const max = Math.max(...known);
|
|
23
|
+
const stroke = CHART_COLORS[color - 1] ?? "currentColor";
|
|
24
|
+
const description = describeSparkline(label, values, formatValue);
|
|
25
|
+
// Une série plate se dessine au milieu ; une marge en haut et en bas garde le trait entier.
|
|
26
|
+
const [lo, hi] = max > min ? [min, max] : [min - 1, max + 1];
|
|
27
|
+
return (_jsx("span", { "data-slot": "sparkline", role: "img", "aria-label": description, style: { height }, className: cn("inline-block w-24 min-w-0 align-middle", className), children: known.length > 0 ? (_jsxs("svg", { "aria-hidden": true, viewBox: `0 -6 ${values.length} 112`, preserveAspectRatio: "none", className: "block size-full overflow-visible", children: [area ? _jsx("path", { d: areaPath(values, hi, lo), fill: stroke, opacity: 0.12 }) : null, _jsx("path", { d: linePath(values, hi, lo), fill: "none", stroke: stroke, strokeWidth: 1.5, strokeLinejoin: "round", strokeLinecap: "round", vectorEffect: "non-scaling-stroke" })] })) : (_jsx("span", { "aria-hidden": true, className: "flex size-full items-center", children: _jsx("span", { className: "h-px w-full bg-foreground/10" }) })) }));
|
|
28
|
+
}
|
package/dist/stat.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type StatDelta = {
|
|
3
|
+
/** Déjà formatée : « 12 % », « +340 ». */
|
|
4
|
+
value: string;
|
|
5
|
+
direction: "up" | "down" | "flat";
|
|
6
|
+
/**
|
|
7
|
+
* Ce qui est bon pour ce chiffre. Une hausse des erreurs est mauvaise : `good: "down"`.
|
|
8
|
+
* Par défaut, la hausse est bonne.
|
|
9
|
+
*/
|
|
10
|
+
good?: "up" | "down";
|
|
11
|
+
/** Période de comparaison : « vs 7 jours précédents ». */
|
|
12
|
+
label?: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Une métrique : libellé, valeur en chiffres de largeur fixe, variation. La variation dit son
|
|
16
|
+
* sens par la flèche et par un texte lu aux lecteurs d'écran, pas par la couleur seule.
|
|
17
|
+
*/
|
|
18
|
+
export declare function StatTile({ label, value, delta, hint, icon, className, children, }: {
|
|
19
|
+
label: ReactNode;
|
|
20
|
+
value: ReactNode;
|
|
21
|
+
delta?: StatDelta;
|
|
22
|
+
/** Une précision sous la valeur (« sur 30 jours »). */
|
|
23
|
+
hint?: ReactNode;
|
|
24
|
+
icon?: ReactNode;
|
|
25
|
+
className?: string;
|
|
26
|
+
/** Un petit graphique (`TimeChart` compact) sous la valeur. */
|
|
27
|
+
children?: ReactNode;
|
|
28
|
+
}): React.JSX.Element;
|
|
29
|
+
/** Rangée de métriques : une colonne sur téléphone, deux puis quatre en s'élargissant. */
|
|
30
|
+
export declare function StatGrid({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
package/dist/stat.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import ArrowDownRight01Icon from "@hugeicons/core-free-icons/ArrowDownRight01Icon";
|
|
3
|
+
import ArrowUpRight01Icon from "@hugeicons/core-free-icons/ArrowUpRight01Icon";
|
|
4
|
+
import { HugeiconsIcon } from "@hugeicons/react";
|
|
5
|
+
import { cn } from "./cn.js";
|
|
6
|
+
const DIRECTION_TEXT = { up: "en hausse de", down: "en baisse de" };
|
|
7
|
+
/**
|
|
8
|
+
* Une métrique : libellé, valeur en chiffres de largeur fixe, variation. La variation dit son
|
|
9
|
+
* sens par la flèche et par un texte lu aux lecteurs d'écran, pas par la couleur seule.
|
|
10
|
+
*/
|
|
11
|
+
export function StatTile({ label, value, delta, hint, icon, className, children, }) {
|
|
12
|
+
return (_jsxs("div", { "data-slot": "stat-tile", className: cn("surface flex min-w-0 flex-col gap-2 rounded-3xl p-5", className), children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("p", { className: "truncate text-sm text-muted-foreground", children: label }), icon ? (_jsx("span", { "aria-hidden": true, className: "text-muted-foreground [&_svg:not([class*='size-'])]:size-4", children: icon })) : null] }), _jsx("p", { className: "font-heading text-2xl font-semibold tracking-tight tabular-nums sm:text-[28px]", children: value }), delta || hint ? (_jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1 text-xs", children: [delta ? _jsx(StatDeltaText, { delta: delta }) : null, hint ? _jsx("span", { className: "text-muted-foreground", children: hint }) : null] })) : null, children] }));
|
|
13
|
+
}
|
|
14
|
+
function StatDeltaText({ delta }) {
|
|
15
|
+
const good = delta.good ?? "up";
|
|
16
|
+
const tone = delta.direction === "flat"
|
|
17
|
+
? "text-muted-foreground"
|
|
18
|
+
: delta.direction === good
|
|
19
|
+
? "text-status-active-ink"
|
|
20
|
+
: "text-destructive";
|
|
21
|
+
return (_jsxs("span", { "data-slot": "stat-delta", "data-direction": delta.direction, className: cn("inline-flex items-center gap-0.5 font-medium tabular-nums", tone), children: [delta.direction === "flat" ? null : (_jsx(HugeiconsIcon, { icon: delta.direction === "up" ? ArrowUpRight01Icon : ArrowDownRight01Icon, strokeWidth: 2, "aria-hidden": true, className: "size-3.5" })), delta.direction === "flat" ? ("Stable") : (_jsxs(_Fragment, { children: [_jsxs("span", { className: "sr-only", children: [DIRECTION_TEXT[delta.direction], " "] }), delta.value] })), delta.label ? _jsx("span", { className: "ml-1 font-normal text-muted-foreground", children: delta.label }) : null] }));
|
|
22
|
+
}
|
|
23
|
+
/** Rangée de métriques : une colonne sur téléphone, deux puis quatre en s'élargissant. */
|
|
24
|
+
export function StatGrid({ className, ...props }) {
|
|
25
|
+
return _jsx("div", { "data-slot": "stat-grid", className: cn("grid gap-3 sm:grid-cols-2 lg:grid-cols-4", className), ...props });
|
|
26
|
+
}
|
package/dist/status.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Disponibilité d'un service, vocabulaire fermé de la famille (Status, Host, Mail). */
|
|
2
|
+
export type ServiceStatus = "operational" | "degraded" | "outage" | "maintenance" | "unknown";
|
|
3
|
+
export declare const SERVICE_STATUS_LABEL: Record<ServiceStatus, string>;
|
|
4
|
+
/**
|
|
5
|
+
* Le point d'un statut, décoratif (`aria-hidden`) : il accompagne toujours un libellé.
|
|
6
|
+
* `live` ajoute l'anneau qui s'élargit, pour ce qui est en cours (incident, vérification DNS).
|
|
7
|
+
*/
|
|
8
|
+
export declare function StatusDot({ status, live, className, }: {
|
|
9
|
+
status: ServiceStatus;
|
|
10
|
+
live?: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
}): React.JSX.Element;
|
|
13
|
+
/**
|
|
14
|
+
* Un statut : point et libellé, jamais la couleur seule. Le libellé par défaut est celui de la
|
|
15
|
+
* famille ; un produit peut le préciser (« Panne partielle ») sans changer de couleur.
|
|
16
|
+
*/
|
|
17
|
+
export declare function StatusBadge({ status, label, live, className, }: {
|
|
18
|
+
status: ServiceStatus;
|
|
19
|
+
label?: string;
|
|
20
|
+
live?: boolean;
|
|
21
|
+
className?: string;
|
|
22
|
+
}): React.JSX.Element;
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "./cn.js";
|
|
3
|
+
export const SERVICE_STATUS_LABEL = {
|
|
4
|
+
operational: "Opérationnel",
|
|
5
|
+
degraded: "Dégradé",
|
|
6
|
+
outage: "Panne",
|
|
7
|
+
maintenance: "Maintenance",
|
|
8
|
+
unknown: "Inconnu",
|
|
9
|
+
};
|
|
10
|
+
const DOT = {
|
|
11
|
+
operational: "bg-status-operational",
|
|
12
|
+
degraded: "bg-status-degraded",
|
|
13
|
+
outage: "bg-status-outage",
|
|
14
|
+
maintenance: "bg-status-maintenance",
|
|
15
|
+
unknown: "bg-status-unknown",
|
|
16
|
+
};
|
|
17
|
+
const BADGE = {
|
|
18
|
+
operational: "bg-status-operational/12 text-status-operational-ink",
|
|
19
|
+
degraded: "bg-status-degraded/15 text-status-degraded-ink",
|
|
20
|
+
outage: "bg-status-outage/10 text-status-outage-ink",
|
|
21
|
+
maintenance: "bg-status-maintenance/12 text-status-maintenance-ink",
|
|
22
|
+
unknown: "bg-foreground/[0.06] text-status-unknown-ink",
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Le point d'un statut, décoratif (`aria-hidden`) : il accompagne toujours un libellé.
|
|
26
|
+
* `live` ajoute l'anneau qui s'élargit, pour ce qui est en cours (incident, vérification DNS).
|
|
27
|
+
*/
|
|
28
|
+
export function StatusDot({ status, live = false, className, }) {
|
|
29
|
+
return (_jsxs("span", { "aria-hidden": true, "data-slot": "status-dot", className: cn("relative inline-flex size-2 shrink-0", className), children: [live ? _jsx("span", { className: cn("absolute inset-0 live-ping rounded-full", DOT[status]) }) : null, _jsx("span", { className: cn("relative inline-flex size-full rounded-full", DOT[status]) })] }));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Un statut : point et libellé, jamais la couleur seule. Le libellé par défaut est celui de la
|
|
33
|
+
* famille ; un produit peut le préciser (« Panne partielle ») sans changer de couleur.
|
|
34
|
+
*/
|
|
35
|
+
export function StatusBadge({ status, label, live = false, className, }) {
|
|
36
|
+
return (_jsxs("span", { "data-slot": "status-badge", "data-status": status, className: cn("inline-flex w-fit items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium whitespace-nowrap", BADGE[status], className), children: [_jsx(StatusDot, { status: status, live: live, className: "size-1.5" }), label ?? SERVICE_STATUS_LABEL[status]] }));
|
|
37
|
+
}
|
package/dist/table.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Tableau de données. Posé sur une carte `surface` par défaut (`surface={false}` dans une carte
|
|
5
|
+
* qui en a déjà une), il défile horizontalement dans son cadre sur téléphone : la page, elle,
|
|
6
|
+
* ne défile jamais de côté.
|
|
7
|
+
*/
|
|
8
|
+
declare function Table({ className, surface, containerClassName, ...props }: React.ComponentProps<"table"> & {
|
|
9
|
+
surface?: boolean;
|
|
10
|
+
containerClassName?: string;
|
|
11
|
+
}): React.JSX.Element;
|
|
12
|
+
declare function TableHeader({ className, ...props }: React.ComponentProps<"thead">): React.JSX.Element;
|
|
13
|
+
declare function TableBody({ className, ...props }: React.ComponentProps<"tbody">): React.JSX.Element;
|
|
14
|
+
/**
|
|
15
|
+
* Une ligne. Avec un `TableRowLink` dedans, toute la ligne devient cliquable : survol teinté,
|
|
16
|
+
* lien étiré sur la ligne. Le lien reste un vrai lien (Tab, Entrée, clic milieu, « ouvrir dans
|
|
17
|
+
* un nouvel onglet »), là où un `onClick` sur `<tr>` n'en offrirait aucun.
|
|
18
|
+
*/
|
|
19
|
+
declare function TableRow({ className, ...props }: React.ComponentProps<"tr">): React.JSX.Element;
|
|
20
|
+
export type SortDirection = "ascending" | "descending" | "none";
|
|
21
|
+
/**
|
|
22
|
+
* En-tête de colonne. Triable avec `onSort` : un bouton, l'état dans `aria-sort` (lu par les
|
|
23
|
+
* lecteurs d'écran) et une flèche. Le tri lui-même reste au produit, idéalement dans l'URL.
|
|
24
|
+
*/
|
|
25
|
+
declare function TableHead({ className, children, sort, onSort, numeric, ...props }: React.ComponentProps<"th"> & {
|
|
26
|
+
sort?: SortDirection;
|
|
27
|
+
onSort?: () => void;
|
|
28
|
+
/** Colonne de chiffres : alignée à droite, chiffres de largeur fixe. */
|
|
29
|
+
numeric?: boolean;
|
|
30
|
+
}): React.JSX.Element;
|
|
31
|
+
declare function TableCell({ className, numeric, ...props }: React.ComponentProps<"td"> & {
|
|
32
|
+
numeric?: boolean;
|
|
33
|
+
}): React.JSX.Element;
|
|
34
|
+
/**
|
|
35
|
+
* Le lien d'une ligne, dans sa première cellule : son libellé est le nom de la ressource, et son
|
|
36
|
+
* pseudo-élément couvre toute la ligne. `render={<Link to="…" />}` pour le routeur du produit.
|
|
37
|
+
*/
|
|
38
|
+
declare function TableRowLink({ className, render, ...props }: useRender.ComponentProps<"a">): React.JSX.Element;
|
|
39
|
+
/** Aucune ligne : une phrase, et de quoi en créer une. Occupe toute la largeur du tableau. */
|
|
40
|
+
declare function TableEmpty({ colSpan, children, className, }: {
|
|
41
|
+
colSpan: number;
|
|
42
|
+
children: ReactNode;
|
|
43
|
+
className?: string;
|
|
44
|
+
}): React.JSX.Element;
|
|
45
|
+
/** Lignes de chargement à la forme des vraies : même hauteur, même nombre de colonnes. */
|
|
46
|
+
declare function TableSkeleton({ rows, columns }: {
|
|
47
|
+
rows?: number;
|
|
48
|
+
columns: number;
|
|
49
|
+
}): React.JSX.Element;
|
|
50
|
+
export { Table, TableHeader, TableBody, TableRow, TableHead, TableCell, TableRowLink, TableEmpty, TableSkeleton };
|
package/dist/table.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import ArrowDown01Icon from "@hugeicons/core-free-icons/ArrowDown01Icon";
|
|
3
|
+
import ArrowUp01Icon from "@hugeicons/core-free-icons/ArrowUp01Icon";
|
|
4
|
+
import ArrowUpDownIcon from "@hugeicons/core-free-icons/ArrowUpDownIcon";
|
|
5
|
+
import { HugeiconsIcon } from "@hugeicons/react";
|
|
6
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
7
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
8
|
+
import { cn } from "./cn.js";
|
|
9
|
+
import { Skeleton } from "./skeleton.js";
|
|
10
|
+
/**
|
|
11
|
+
* Tableau de données. Posé sur une carte `surface` par défaut (`surface={false}` dans une carte
|
|
12
|
+
* qui en a déjà une), il défile horizontalement dans son cadre sur téléphone : la page, elle,
|
|
13
|
+
* ne défile jamais de côté.
|
|
14
|
+
*/
|
|
15
|
+
function Table({ className, surface = true, containerClassName, ...props }) {
|
|
16
|
+
return (_jsx("div", { "data-slot": "table-container", className: cn("relative w-full overflow-x-auto", surface && "surface rounded-3xl", containerClassName), children: _jsx("table", { "data-slot": "table", className: cn("w-full caption-bottom border-collapse text-sm", className), ...props }) }));
|
|
17
|
+
}
|
|
18
|
+
function TableHeader({ className, ...props }) {
|
|
19
|
+
return _jsx("thead", { "data-slot": "table-header", className: cn(className), ...props });
|
|
20
|
+
}
|
|
21
|
+
function TableBody({ className, ...props }) {
|
|
22
|
+
return _jsx("tbody", { "data-slot": "table-body", className: cn(className), ...props });
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Une ligne. Avec un `TableRowLink` dedans, toute la ligne devient cliquable : survol teinté,
|
|
26
|
+
* lien étiré sur la ligne. Le lien reste un vrai lien (Tab, Entrée, clic milieu, « ouvrir dans
|
|
27
|
+
* un nouvel onglet »), là où un `onClick` sur `<tr>` n'en offrirait aucun.
|
|
28
|
+
*/
|
|
29
|
+
function TableRow({ className, ...props }) {
|
|
30
|
+
return (_jsx("tr", { "data-slot": "table-row", className: cn("relative border-t border-foreground/5 transition-colors duration-(--duration-fast) ease-out", "[[data-slot=table-header]_&]:border-t-0", "has-data-[slot=table-row-link]:hover:bg-foreground/[0.03] has-data-[slot=table-row-link]:has-focus-visible:bg-foreground/[0.03]", "data-[state=selected]:bg-foreground/[0.04]", className), ...props }));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* En-tête de colonne. Triable avec `onSort` : un bouton, l'état dans `aria-sort` (lu par les
|
|
34
|
+
* lecteurs d'écran) et une flèche. Le tri lui-même reste au produit, idéalement dans l'URL.
|
|
35
|
+
*/
|
|
36
|
+
function TableHead({ className, children, sort, onSort, numeric = false, ...props }) {
|
|
37
|
+
const sortable = onSort !== undefined;
|
|
38
|
+
const direction = sort ?? "none";
|
|
39
|
+
return (_jsx("th", { "data-slot": "table-head", scope: "col", "aria-sort": sortable ? direction : undefined, className: cn("h-11 px-4 text-left align-middle text-xs font-medium whitespace-nowrap text-muted-foreground first:pl-5 last:pr-5", numeric && "text-right", className), ...props, children: sortable ? (_jsxs("button", { type: "button", onClick: onSort, className: cn("-mx-1.5 inline-flex items-center gap-1 rounded-md px-1.5 py-1 outline-none transition-colors duration-(--duration-fast) ease-out hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50", direction !== "none" && "text-foreground", numeric && "flex-row-reverse"), children: [children, _jsx(HugeiconsIcon, { icon: direction === "ascending" ? ArrowUp01Icon : direction === "descending" ? ArrowDown01Icon : ArrowUpDownIcon, strokeWidth: 2, "aria-hidden": true, className: cn("size-3.5 shrink-0", direction === "none" && "opacity-50") })] })) : (children) }));
|
|
40
|
+
}
|
|
41
|
+
function TableCell({ className, numeric = false, ...props }) {
|
|
42
|
+
return (_jsx("td", { "data-slot": "table-cell", className: cn("px-4 py-3 align-middle first:pl-5 last:pr-5", numeric && "text-right tabular-nums",
|
|
43
|
+
// Un bouton ou un menu dans une ligne cliquable passe au-dessus du lien étiré.
|
|
44
|
+
"[&_a:not([data-slot=table-row-link])]:relative [&_a:not([data-slot=table-row-link])]:z-1 [&_button]:relative [&_button]:z-1", className), ...props }));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Le lien d'une ligne, dans sa première cellule : son libellé est le nom de la ressource, et son
|
|
48
|
+
* pseudo-élément couvre toute la ligne. `render={<Link to="…" />}` pour le routeur du produit.
|
|
49
|
+
*/
|
|
50
|
+
function TableRowLink({ className, render, ...props }) {
|
|
51
|
+
return useRender({
|
|
52
|
+
defaultTagName: "a",
|
|
53
|
+
render,
|
|
54
|
+
props: mergeProps({
|
|
55
|
+
"data-slot": "table-row-link",
|
|
56
|
+
className: cn("font-medium 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", className),
|
|
57
|
+
}, props),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** Aucune ligne : une phrase, et de quoi en créer une. Occupe toute la largeur du tableau. */
|
|
61
|
+
function TableEmpty({ colSpan, children, className, }) {
|
|
62
|
+
return (_jsx("tr", { "data-slot": "table-empty", children: _jsx("td", { colSpan: colSpan, className: cn("px-5 py-12 text-center text-sm text-muted-foreground", className), children: children }) }));
|
|
63
|
+
}
|
|
64
|
+
/** Lignes de chargement à la forme des vraies : même hauteur, même nombre de colonnes. */
|
|
65
|
+
function TableSkeleton({ rows = 5, columns }) {
|
|
66
|
+
return (_jsx(_Fragment, { children: Array.from({ length: rows }, (_, row) => (_jsx("tr", { "data-slot": "table-skeleton", "aria-hidden": true, className: "border-t border-foreground/5", children: Array.from({ length: columns }, (_, col) => (_jsx("td", { className: "px-4 py-3 first:pl-5 last:pr-5", children: _jsx(Skeleton, { className: cn("h-4 rounded-md", col === 0 ? "w-3/4" : "w-1/2") }) }, col))) }, row))) }));
|
|
67
|
+
}
|
|
68
|
+
export { Table, TableHeader, TableBody, TableRow, TableHead, TableCell, TableRowLink, TableEmpty, TableSkeleton };
|
package/dist/tabs.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
|
|
2
|
+
/**
|
|
3
|
+
* Onglets d'une même page (Aperçu, Journal, Réglages). Flèches pour passer d'un onglet à
|
|
4
|
+
* l'autre, Tab pour entrer dans le panneau. L'onglet ouvert a sa place dans l'URL quand on
|
|
5
|
+
* partage la page : `value` et `onValueChange` contrôlés par le routeur, ou des liens
|
|
6
|
+
* (`render={<Link to="…" />}` et `nativeButton={false}` sur `TabsTab`).
|
|
7
|
+
*/
|
|
8
|
+
declare function Tabs({ className, ...props }: TabsPrimitive.Root.Props): React.JSX.Element;
|
|
9
|
+
/**
|
|
10
|
+
* La rangée d'onglets. L'onglet ouvert est marqué d'une pastille teintée qui glisse de l'un à
|
|
11
|
+
* l'autre, monochrome comme la barre latérale : l'accent reste au focus.
|
|
12
|
+
*/
|
|
13
|
+
declare function TabsList({ className, children, ...props }: TabsPrimitive.List.Props): React.JSX.Element;
|
|
14
|
+
declare function TabsTab({ className, ...props }: TabsPrimitive.Tab.Props): React.JSX.Element;
|
|
15
|
+
declare function TabsPanel({ className, ...props }: TabsPrimitive.Panel.Props): React.JSX.Element;
|
|
16
|
+
export { Tabs, TabsList, TabsTab, TabsPanel };
|
package/dist/tabs.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
|
|
3
|
+
import { cn } from "./cn.js";
|
|
4
|
+
/**
|
|
5
|
+
* Onglets d'une même page (Aperçu, Journal, Réglages). Flèches pour passer d'un onglet à
|
|
6
|
+
* l'autre, Tab pour entrer dans le panneau. L'onglet ouvert a sa place dans l'URL quand on
|
|
7
|
+
* partage la page : `value` et `onValueChange` contrôlés par le routeur, ou des liens
|
|
8
|
+
* (`render={<Link to="…" />}` et `nativeButton={false}` sur `TabsTab`).
|
|
9
|
+
*/
|
|
10
|
+
function Tabs({ className, ...props }) {
|
|
11
|
+
return _jsx(TabsPrimitive.Root, { "data-slot": "tabs", className: cn("grid gap-6", className), ...props });
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* La rangée d'onglets. L'onglet ouvert est marqué d'une pastille teintée qui glisse de l'un à
|
|
15
|
+
* l'autre, monochrome comme la barre latérale : l'accent reste au focus.
|
|
16
|
+
*/
|
|
17
|
+
function TabsList({ className, children, ...props }) {
|
|
18
|
+
return (_jsxs(TabsPrimitive.List, { "data-slot": "tabs-list", className: cn("relative z-0 flex w-fit max-w-full items-center gap-1 overflow-x-auto [scrollbar-width:none]", className), ...props, children: [children, _jsx(TabsPrimitive.Indicator, { "data-slot": "tabs-indicator", className: "absolute top-(--active-tab-top) left-0 -z-1 h-(--active-tab-height) w-(--active-tab-width) translate-x-(--active-tab-left) rounded-full bg-foreground/[0.07] transition-[translate,width] duration-(--duration-base) ease-out motion-reduce:transition-none" })] }));
|
|
19
|
+
}
|
|
20
|
+
function TabsTab({ className, ...props }) {
|
|
21
|
+
return (_jsx(TabsPrimitive.Tab, { "data-slot": "tabs-tab", className: cn("touch-target flex h-9 shrink-0 items-center justify-center gap-1.5 rounded-full px-3.5 text-sm font-medium whitespace-nowrap text-muted-foreground", "transition-colors duration-(--duration-fast) ease-out outline-none select-none", "hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 data-active:text-foreground", "data-disabled:pointer-events-none data-disabled:opacity-50", "[&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props }));
|
|
22
|
+
}
|
|
23
|
+
function TabsPanel({ className, ...props }) {
|
|
24
|
+
return (_jsx(TabsPrimitive.Panel, { "data-slot": "tabs-panel", className: cn("rounded-2xl outline-none focus-visible:ring-2 focus-visible:ring-ring/50", className), ...props }));
|
|
25
|
+
}
|
|
26
|
+
export { Tabs, TabsList, TabsTab, TabsPanel };
|