@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.
- package/README.md +62 -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/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/kbd.d.ts +9 -0
- package/dist/kbd.js +19 -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/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 +127 -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 +24 -0
- package/dist/uptime-bar.js +25 -0
- package/package.json +1 -1
- package/styles.css +45 -2
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/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 };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type TimeSeries = {
|
|
2
|
+
/** Identifiant stable : la couleur suit la série, jamais son rang dans un filtre. */
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** Une valeur par horodatage de `timestamps` ; `null` : pas de mesure (la ligne s'interrompt). */
|
|
6
|
+
values: Array<number | null>;
|
|
7
|
+
};
|
|
8
|
+
/** Six teintes de la DA (`--chart-1` à `--chart-6`), dans l'ordre, jamais recyclées. */
|
|
9
|
+
export declare const CHART_COLORS: ReadonlyArray<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Un maximum « rond » pour l'axe des valeurs, et ses graduations : 0, 25, 50, 75, 100 plutôt que
|
|
12
|
+
* 0, 23,7, 47,4. `ticks` est le nombre d'intervalles visé.
|
|
13
|
+
*/
|
|
14
|
+
export declare function niceScale(max: number, ticks?: number): {
|
|
15
|
+
max: number;
|
|
16
|
+
step: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Série temporelle en SVG, sans bibliothèque : lignes (aire sous la courbe pour une série seule)
|
|
20
|
+
* ou barres empilées. Le SVG s'étire à la largeur de son conteneur ; les traits gardent 2 px
|
|
21
|
+
* (`non-scaling-stroke`) et les textes restent en HTML, nets à toute taille.
|
|
22
|
+
*
|
|
23
|
+
* Survol ou flèches du clavier : un repère vertical et une infobulle avec chaque valeur. Les
|
|
24
|
+
* lecteurs d'écran ont un tableau des données. Légende dès deux séries.
|
|
25
|
+
*/
|
|
26
|
+
export declare function TimeChart({ timestamps, series, kind, height, compact, label, formatValue, formatTime, emptyLabel, className, }: {
|
|
27
|
+
timestamps: Array<number>;
|
|
28
|
+
series: Array<TimeSeries>;
|
|
29
|
+
kind?: "line" | "bar";
|
|
30
|
+
/** Hauteur de la zone de tracé, en pixels. */
|
|
31
|
+
height?: number;
|
|
32
|
+
/** Sans axes, grille ni légende : la courbe seule, sous un `StatTile`. */
|
|
33
|
+
compact?: boolean;
|
|
34
|
+
/** Ce que montre le graphique, pour les lecteurs d'écran : « Envois par jour ». */
|
|
35
|
+
label: string;
|
|
36
|
+
formatValue?: (value: number) => string;
|
|
37
|
+
formatTime?: (time: number) => string;
|
|
38
|
+
emptyLabel?: string;
|
|
39
|
+
className?: string;
|
|
40
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { cn } from "./cn.js";
|
|
4
|
+
/** Six teintes de la DA (`--chart-1` à `--chart-6`), dans l'ordre, jamais recyclées. */
|
|
5
|
+
export const CHART_COLORS = [
|
|
6
|
+
"var(--chart-1)",
|
|
7
|
+
"var(--chart-2)",
|
|
8
|
+
"var(--chart-3)",
|
|
9
|
+
"var(--chart-4)",
|
|
10
|
+
"var(--chart-5)",
|
|
11
|
+
"var(--chart-6)",
|
|
12
|
+
];
|
|
13
|
+
const number = new Intl.NumberFormat("fr-CH");
|
|
14
|
+
const dayMonth = new Intl.DateTimeFormat("fr-CH", { day: "numeric", month: "short" });
|
|
15
|
+
/**
|
|
16
|
+
* Un maximum « rond » pour l'axe des valeurs, et ses graduations : 0, 25, 50, 75, 100 plutôt que
|
|
17
|
+
* 0, 23,7, 47,4. `ticks` est le nombre d'intervalles visé.
|
|
18
|
+
*/
|
|
19
|
+
export function niceScale(max, ticks = 4) {
|
|
20
|
+
if (!(max > 0))
|
|
21
|
+
return { max: 1, step: 0.25 };
|
|
22
|
+
const raw = max / ticks;
|
|
23
|
+
const magnitude = 10 ** Math.floor(Math.log10(raw));
|
|
24
|
+
const residual = raw / magnitude;
|
|
25
|
+
const nice = residual <= 1 ? 1 : residual <= 2 ? 2 : residual <= 2.5 ? 2.5 : residual <= 5 ? 5 : 10;
|
|
26
|
+
const step = nice * magnitude;
|
|
27
|
+
return { max: Math.ceil(max / step) * step, step };
|
|
28
|
+
}
|
|
29
|
+
function linePath(values, max) {
|
|
30
|
+
let d = "";
|
|
31
|
+
let open = false;
|
|
32
|
+
values.forEach((v, i) => {
|
|
33
|
+
if (v === null) {
|
|
34
|
+
open = false;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const x = i + 0.5;
|
|
38
|
+
const y = 100 - (v / max) * 100;
|
|
39
|
+
d += `${open ? "L" : "M"}${x} ${y}`;
|
|
40
|
+
open = true;
|
|
41
|
+
});
|
|
42
|
+
return d;
|
|
43
|
+
}
|
|
44
|
+
function areaPath(values, max) {
|
|
45
|
+
// Une aire par segment continu, fermée sur l'axe.
|
|
46
|
+
const parts = [];
|
|
47
|
+
let start = -1;
|
|
48
|
+
const close = (end) => {
|
|
49
|
+
if (start < 0)
|
|
50
|
+
return;
|
|
51
|
+
const segment = values.slice(start, end);
|
|
52
|
+
const top = segment
|
|
53
|
+
.map((v, j) => `${j === 0 ? "M" : "L"}${start + j + 0.5} ${100 - ((v ?? 0) / max) * 100}`)
|
|
54
|
+
.join("");
|
|
55
|
+
parts.push(`${top}L${end - 1 + 0.5} 100L${start + 0.5} 100Z`);
|
|
56
|
+
start = -1;
|
|
57
|
+
};
|
|
58
|
+
values.forEach((v, i) => {
|
|
59
|
+
if (v === null)
|
|
60
|
+
close(i);
|
|
61
|
+
else if (start < 0)
|
|
62
|
+
start = i;
|
|
63
|
+
});
|
|
64
|
+
close(values.length);
|
|
65
|
+
return parts.join("");
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Série temporelle en SVG, sans bibliothèque : lignes (aire sous la courbe pour une série seule)
|
|
69
|
+
* ou barres empilées. Le SVG s'étire à la largeur de son conteneur ; les traits gardent 2 px
|
|
70
|
+
* (`non-scaling-stroke`) et les textes restent en HTML, nets à toute taille.
|
|
71
|
+
*
|
|
72
|
+
* Survol ou flèches du clavier : un repère vertical et une infobulle avec chaque valeur. Les
|
|
73
|
+
* lecteurs d'écran ont un tableau des données. Légende dès deux séries.
|
|
74
|
+
*/
|
|
75
|
+
export function TimeChart({ timestamps, series, kind = "line", height = 200, compact = false, label, formatValue = (v) => number.format(v), formatTime = (t) => dayMonth.format(t), emptyLabel = "Aucune donnée sur cette période.", className, }) {
|
|
76
|
+
const [active, setActive] = useState(null);
|
|
77
|
+
const n = timestamps.length;
|
|
78
|
+
const hasData = n > 0 && series.some((s) => s.values.some((v) => v !== null));
|
|
79
|
+
if (!hasData)
|
|
80
|
+
return (_jsx("div", { "data-slot": "time-chart", "data-state": "empty", style: { height }, className: cn("grid place-items-center rounded-2xl bg-foreground/[0.03] text-sm text-muted-foreground", className), children: emptyLabel }));
|
|
81
|
+
const totals = timestamps.map((_, i) => kind === "bar"
|
|
82
|
+
? series.reduce((sum, s) => sum + (s.values[i] ?? 0), 0)
|
|
83
|
+
: Math.max(0, ...series.map((s) => s.values[i] ?? 0)));
|
|
84
|
+
const scale = niceScale(Math.max(0, ...totals));
|
|
85
|
+
const ticks = Array.from({ length: Math.round(scale.max / scale.step) + 1 }, (_, i) => i * scale.step);
|
|
86
|
+
const color = (index) => CHART_COLORS[index % CHART_COLORS.length] ?? "currentColor";
|
|
87
|
+
const indexFromPointer = (event) => {
|
|
88
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
89
|
+
const ratio = (event.clientX - rect.left) / rect.width;
|
|
90
|
+
return Math.min(n - 1, Math.max(0, Math.floor(ratio * n)));
|
|
91
|
+
};
|
|
92
|
+
const onKeyDown = (event) => {
|
|
93
|
+
const moves = { ArrowLeft: -1, ArrowRight: 1, Home: -n, End: n };
|
|
94
|
+
if (event.key === "Escape")
|
|
95
|
+
return setActive(null);
|
|
96
|
+
const move = moves[event.key];
|
|
97
|
+
if (move === undefined)
|
|
98
|
+
return;
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
setActive((current) => Math.min(n - 1, Math.max(0, (current ?? (move > 0 ? -1 : n)) + move)));
|
|
101
|
+
};
|
|
102
|
+
const activeTime = active !== null ? timestamps[active] : undefined;
|
|
103
|
+
const describe = (i) => {
|
|
104
|
+
const t = timestamps[i];
|
|
105
|
+
const values = series
|
|
106
|
+
.map((s) => `${s.label} ${s.values[i] == null ? "pas de mesure" : formatValue(s.values[i] ?? 0)}`)
|
|
107
|
+
.join(", ");
|
|
108
|
+
return t === undefined ? values : `${formatTime(t)} : ${values}`;
|
|
109
|
+
};
|
|
110
|
+
const xLabels = compact ? [] : [0, Math.floor((n - 1) / 2), n - 1].filter((v, i, a) => a.indexOf(v) === i);
|
|
111
|
+
return (_jsxs("figure", { "data-slot": "time-chart", className: cn("grid gap-3", className), children: [!compact && series.length > 1 ? (_jsx("figcaption", { className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground", children: series.map((s, i) => (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx("span", { "aria-hidden": true, className: "size-2.5 rounded-[3px]", style: { background: color(i) } }), s.label] }, s.key))) })) : null, _jsxs("div", { className: cn("grid", !compact && "grid-cols-[auto_minmax(0,1fr)] gap-x-2"), children: [!compact ? (_jsx("div", { "aria-hidden": true, className: "relative w-10 text-right text-[11px] text-muted-foreground tabular-nums", style: { height }, children: ticks.map((t) => (_jsx("span", { className: "absolute right-0 -translate-y-1/2", style: { top: `${100 - (t / scale.max) * 100}%` }, children: formatValue(t) }, t))) })) : null, _jsxs("div", { className: "grid gap-1.5", children: [_jsxs("div", {
|
|
112
|
+
// Un curseur sur le temps : les flèches le déplacent d'un point, et chaque position se
|
|
113
|
+
// lit en entier (date et valeurs).
|
|
114
|
+
role: "slider", tabIndex: 0, "aria-label": label, "aria-orientation": "horizontal", "aria-valuemin": 0, "aria-valuemax": n - 1, "aria-valuenow": active ?? n - 1, "aria-valuetext": describe(active ?? n - 1), onPointerMove: (event) => setActive(indexFromPointer(event)), onPointerLeave: () => setActive(null), onKeyDown: onKeyDown, onBlur: () => setActive(null), className: "relative rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring/50", style: { height }, children: [_jsxs("svg", { viewBox: `0 0 ${n} 100`, preserveAspectRatio: "none", "aria-hidden": true, className: "absolute inset-0 size-full overflow-visible", children: [!compact
|
|
115
|
+
? ticks.map((t) => (_jsx("line", { x1: 0, x2: n, y1: 100 - (t / scale.max) * 100, y2: 100 - (t / scale.max) * 100, vectorEffect: "non-scaling-stroke", className: t === 0 ? "stroke-foreground/15" : "stroke-foreground/[0.06]", strokeWidth: 1 }, t)))
|
|
116
|
+
: null, kind === "bar"
|
|
117
|
+
? timestamps.map((_, i) => {
|
|
118
|
+
let base = 100;
|
|
119
|
+
return series.map((s, si) => {
|
|
120
|
+
const v = s.values[i] ?? 0;
|
|
121
|
+
const h = (v / scale.max) * 100;
|
|
122
|
+
base -= h;
|
|
123
|
+
return v > 0 ? (_jsx("rect", { x: i + 0.15, y: base, width: 0.7, height: h, fill: color(si), opacity: active === null || active === i ? 1 : 0.55 }, `${s.key}-${i}`)) : null;
|
|
124
|
+
});
|
|
125
|
+
})
|
|
126
|
+
: series.map((s, si) => (_jsxs("g", { children: [series.length === 1 ? (_jsx("path", { d: areaPath(s.values, scale.max), fill: color(si), opacity: 0.1 })) : null, _jsx("path", { d: linePath(s.values, scale.max), fill: "none", stroke: color(si), strokeWidth: 2, strokeLinejoin: "round", strokeLinecap: "round", vectorEffect: "non-scaling-stroke" })] }, s.key))), active !== null ? (_jsx("line", { x1: active + 0.5, x2: active + 0.5, y1: 0, y2: 100, vectorEffect: "non-scaling-stroke", className: "stroke-foreground/25", strokeWidth: 1 })) : null] }), active !== null && activeTime !== undefined ? (_jsxs("div", { "data-slot": "time-chart-tooltip", className: cn("pointer-events-none absolute top-0 z-10 grid min-w-36 gap-1 rounded-xl bg-popover px-3 py-2 text-xs shadow-lg shadow-black/10 [--popover:var(--floating)]", (active + 0.5) / n > 0.6 ? "-translate-x-[calc(100%+0.75rem)]" : "translate-x-3"), style: { left: `${((active + 0.5) / n) * 100}%` }, children: [_jsx("p", { className: "font-medium", children: formatTime(activeTime) }), series.map((s, si) => (_jsxs("p", { className: "flex items-center gap-2", children: [_jsx("span", { "aria-hidden": true, className: "size-2 shrink-0 rounded-[2px]", style: { background: color(si) } }), _jsx("span", { className: "flex-1 text-muted-foreground", children: s.label }), _jsx("span", { className: "font-medium tabular-nums", children: s.values[active] === null ? "-" : formatValue(s.values[active] ?? 0) })] }, s.key)))] })) : null] }), xLabels.length > 0 ? (_jsx("div", { "aria-hidden": true, className: "relative h-4 text-[11px] text-muted-foreground tabular-nums", children: xLabels.map((i, k) => (_jsx("span", { className: cn("absolute", k === 0 ? "left-0" : k === xLabels.length - 1 ? "right-0" : "-translate-x-1/2"), style: k === 0 || k === xLabels.length - 1 ? undefined : { left: `${((i + 0.5) / n) * 100}%` }, children: timestamps[i] !== undefined ? formatTime(timestamps[i]) : null }, i))) })) : null] })] }), _jsxs("table", { className: "sr-only", children: [_jsx("caption", { children: label }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Date" }), series.map((s) => (_jsx("th", { scope: "col", children: s.label }, s.key)))] }) }), _jsx("tbody", { children: timestamps.map((t, i) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: formatTime(t) }), series.map((s) => (_jsx("td", { children: s.values[i] === null ? "-" : formatValue(s.values[i] ?? 0) }, s.key)))] }, t))) })] })] }));
|
|
127
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type ServiceStatus } from "./status.tsx";
|
|
3
|
+
export type TimelineEntry = {
|
|
4
|
+
id: string;
|
|
5
|
+
/** « Identifié », « Correctif déployé », « Résolu ». */
|
|
6
|
+
title: ReactNode;
|
|
7
|
+
description?: ReactNode;
|
|
8
|
+
/** Instant de l'étape, affiché en relatif avec la date exacte au survol. */
|
|
9
|
+
time: number | Date;
|
|
10
|
+
/** Couleur du point ; sans statut, un point neutre. */
|
|
11
|
+
status?: ServiceStatus;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Historique d'un incident, d'un envoi, d'un déploiement : une étape par ligne, reliées par un
|
|
15
|
+
* fil. Le produit les passe dans l'ordre voulu (le plus récent d'abord, d'habitude).
|
|
16
|
+
*/
|
|
17
|
+
export declare function Timeline({ entries, now, className, }: {
|
|
18
|
+
entries: Array<TimelineEntry>;
|
|
19
|
+
now?: number;
|
|
20
|
+
className?: string;
|
|
21
|
+
}): React.JSX.Element;
|
package/dist/timeline.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { RelativeTime } from "./relative-time.js";
|
|
3
|
+
import { StatusDot } from "./status.js";
|
|
4
|
+
import { cn } from "./cn.js";
|
|
5
|
+
/**
|
|
6
|
+
* Historique d'un incident, d'un envoi, d'un déploiement : une étape par ligne, reliées par un
|
|
7
|
+
* fil. Le produit les passe dans l'ordre voulu (le plus récent d'abord, d'habitude).
|
|
8
|
+
*/
|
|
9
|
+
export function Timeline({ entries, now, className, }) {
|
|
10
|
+
return (_jsx("ol", { "data-slot": "timeline", className: cn("grid", className), children: entries.map((entry, index) => (_jsxs("li", { className: "relative grid grid-cols-[1rem_minmax(0,1fr)] gap-x-3 pb-6 last:pb-0", children: [index < entries.length - 1 ? (_jsx("span", { "aria-hidden": true, className: "absolute top-5 bottom-0 left-[calc(0.5rem-0.5px)] w-px bg-foreground/10" })) : null, _jsx("span", { className: "flex h-5 items-center justify-center", children: entry.status ? (_jsx(StatusDot, { status: entry.status, className: "size-2.5" })) : (_jsx("span", { "aria-hidden": true, className: "size-2.5 rounded-full bg-foreground/20" })) }), _jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2", children: [_jsx("p", { className: "text-sm font-medium", children: entry.title }), _jsx("span", { className: "text-xs text-muted-foreground", children: _jsx(RelativeTime, { date: entry.time, ...(now !== undefined ? { now } : {}) }) })] }), entry.description ? _jsx("div", { className: "mt-1 text-sm text-muted-foreground", children: entry.description }) : null] })] }, entry.id))) }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
|
|
2
|
+
/**
|
|
3
|
+
* Infobulle : le nom d'un bouton-icône ou d'un raccourci, pour la souris et le clavier. Jamais
|
|
4
|
+
* une information indispensable : elle n'existe pas au doigt (Base UI la coupe sur écran
|
|
5
|
+
* tactile). Le déclencheur porte toujours un `aria-label` qui dit la même chose. Pour une
|
|
6
|
+
* explication à lire, c'est un `Popover`.
|
|
7
|
+
*
|
|
8
|
+
* Un `TooltipProvider` autour de l'application : passée la première, les infobulles voisines
|
|
9
|
+
* s'ouvrent aussitôt, sans animation.
|
|
10
|
+
*/
|
|
11
|
+
declare function TooltipProvider({ delay, ...props }: TooltipPrimitive.Provider.Props): React.JSX.Element;
|
|
12
|
+
declare const Tooltip: typeof TooltipPrimitive.Root;
|
|
13
|
+
declare const TooltipTrigger: typeof TooltipPrimitive.Trigger;
|
|
14
|
+
declare function TooltipContent({ className, side, sideOffset, align, children, ...props }: TooltipPrimitive.Popup.Props & Pick<TooltipPrimitive.Positioner.Props, "side" | "sideOffset" | "align">): React.JSX.Element;
|
|
15
|
+
export { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent };
|
package/dist/tooltip.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
|
|
3
|
+
import { FLOATING_COLLISION_AVOIDANCE, FLOATING_COLLISION_PADDING, FLOATING_MAX_WIDTH } from "./floating.js";
|
|
4
|
+
import { cn } from "./cn.js";
|
|
5
|
+
/**
|
|
6
|
+
* Infobulle : le nom d'un bouton-icône ou d'un raccourci, pour la souris et le clavier. Jamais
|
|
7
|
+
* une information indispensable : elle n'existe pas au doigt (Base UI la coupe sur écran
|
|
8
|
+
* tactile). Le déclencheur porte toujours un `aria-label` qui dit la même chose. Pour une
|
|
9
|
+
* explication à lire, c'est un `Popover`.
|
|
10
|
+
*
|
|
11
|
+
* Un `TooltipProvider` autour de l'application : passée la première, les infobulles voisines
|
|
12
|
+
* s'ouvrent aussitôt, sans animation.
|
|
13
|
+
*/
|
|
14
|
+
function TooltipProvider({ delay = 500, ...props }) {
|
|
15
|
+
return _jsx(TooltipPrimitive.Provider, { delay: delay, ...props });
|
|
16
|
+
}
|
|
17
|
+
const Tooltip = TooltipPrimitive.Root;
|
|
18
|
+
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
19
|
+
function TooltipContent({ className, side = "top", sideOffset = 6, align = "center", children, ...props }) {
|
|
20
|
+
return (_jsx(TooltipPrimitive.Portal, { children: _jsx(TooltipPrimitive.Positioner, { className: "isolate z-50", side: side, sideOffset: sideOffset, align: align, collisionPadding: FLOATING_COLLISION_PADDING, collisionAvoidance: FLOATING_COLLISION_AVOIDANCE, children: _jsx(TooltipPrimitive.Popup, { "data-slot": "tooltip-content", className: cn(FLOATING_MAX_WIDTH,
|
|
21
|
+
// Encre inversée : une étiquette courte, lisible sur n'importe quel fond. Elle flotte,
|
|
22
|
+
// elle a donc une ombre, douce.
|
|
23
|
+
"w-fit max-w-xs origin-(--transform-origin) rounded-xl bg-foreground px-2.5 py-1.5 text-xs font-medium text-balance text-background shadow-lg shadow-black/10", "transition-[scale,opacity] duration-(--duration-fast) ease-out data-ending-style:scale-[0.97] data-ending-style:opacity-0 data-ending-style:duration-(--duration-instant) data-instant:transition-none data-starting-style:scale-[0.97] data-starting-style:opacity-0", "[&_kbd]:bg-background/15 [&_kbd]:text-background", className), ...props, children: children }) }) }));
|
|
24
|
+
}
|
|
25
|
+
export { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ServiceStatus } from "./status.tsx";
|
|
2
|
+
export type UptimeDay = {
|
|
3
|
+
/** Le jour, en millisecondes ou en `Date`. */
|
|
4
|
+
date: number | Date;
|
|
5
|
+
/** Le pire statut du jour. `unknown` : pas de mesure (service plus récent que la période). */
|
|
6
|
+
status: ServiceStatus;
|
|
7
|
+
/** Disponibilité du jour, déjà formatée : « 99,98 % ». */
|
|
8
|
+
uptime?: string;
|
|
9
|
+
/** Ce qui s'est passé : « Panne de 12 min, base de données ». */
|
|
10
|
+
note?: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Une barre par jour, du plus ancien au plus récent, comme les pages de statut de référence.
|
|
14
|
+
* Chaque barre a sa couleur et, au survol, une infobulle en texte (jour, statut, disponibilité,
|
|
15
|
+
* note). Les lecteurs d'écran lisent la liste des jours, dont chacun dit son statut en mots.
|
|
16
|
+
*/
|
|
17
|
+
export declare function UptimeBar({ days, label, summary, className, }: {
|
|
18
|
+
days: Array<UptimeDay>;
|
|
19
|
+
/** Nom de la liste pour les lecteurs d'écran. */
|
|
20
|
+
label?: string;
|
|
21
|
+
/** Au centre de la légende : « 99,95 % sur 90 jours ». */
|
|
22
|
+
summary?: string;
|
|
23
|
+
className?: string;
|
|
24
|
+
}): React.JSX.Element;
|