@hgdc/ui 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +78 -21
  2. package/dist/account-menu.d.ts +32 -0
  3. package/dist/account-menu.js +39 -0
  4. package/dist/alert.d.ts +19 -0
  5. package/dist/alert.js +25 -0
  6. package/dist/app-shell.d.ts +56 -0
  7. package/dist/app-shell.js +107 -0
  8. package/dist/card.d.ts +24 -0
  9. package/dist/card.js +42 -0
  10. package/dist/chart-path.d.ts +9 -0
  11. package/dist/chart-path.js +46 -0
  12. package/dist/checkbox.d.ts +12 -0
  13. package/dist/checkbox.js +19 -0
  14. package/dist/command-menu.d.ts +56 -0
  15. package/dist/command-menu.js +79 -0
  16. package/dist/copy-button.d.ts +26 -0
  17. package/dist/copy-button.js +46 -0
  18. package/dist/data-table.d.ts +70 -0
  19. package/dist/data-table.js +46 -0
  20. package/dist/destructive-dialog.d.ts +28 -0
  21. package/dist/destructive-dialog.js +74 -0
  22. package/dist/dns-record-table.d.ts +30 -0
  23. package/dist/dns-record-table.js +33 -0
  24. package/dist/error-card.d.ts +23 -0
  25. package/dist/error-card.js +35 -0
  26. package/dist/filter-bar.d.ts +50 -0
  27. package/dist/filter-bar.js +88 -0
  28. package/dist/kbd.d.ts +9 -0
  29. package/dist/kbd.js +19 -0
  30. package/dist/log-viewer.d.ts +39 -0
  31. package/dist/log-viewer.js +114 -0
  32. package/dist/middle-truncate.d.ts +17 -0
  33. package/dist/middle-truncate.js +25 -0
  34. package/dist/org-switcher.d.ts +32 -0
  35. package/dist/org-switcher.js +32 -0
  36. package/dist/page-header.d.ts +27 -0
  37. package/dist/page-header.js +31 -0
  38. package/dist/popover.d.ts +13 -0
  39. package/dist/popover.js +22 -0
  40. package/dist/radio-group.d.ts +16 -0
  41. package/dist/radio-group.js +22 -0
  42. package/dist/relative-time.d.ts +16 -0
  43. package/dist/relative-time.js +54 -0
  44. package/dist/resource-list.d.ts +31 -0
  45. package/dist/resource-list.js +35 -0
  46. package/dist/secret-reveal.d.ts +13 -0
  47. package/dist/secret-reveal.js +26 -0
  48. package/dist/skeleton.d.ts +28 -0
  49. package/dist/skeleton.js +47 -0
  50. package/dist/sonner.d.ts +20 -1
  51. package/dist/sonner.js +43 -2
  52. package/dist/sparkline.d.ts +21 -0
  53. package/dist/sparkline.js +28 -0
  54. package/dist/stat.d.ts +30 -0
  55. package/dist/stat.js +26 -0
  56. package/dist/status.d.ts +22 -0
  57. package/dist/status.js +37 -0
  58. package/dist/table.d.ts +50 -0
  59. package/dist/table.js +68 -0
  60. package/dist/tabs.d.ts +16 -0
  61. package/dist/tabs.js +26 -0
  62. package/dist/time-chart.d.ts +40 -0
  63. package/dist/time-chart.js +90 -0
  64. package/dist/timeline.d.ts +21 -0
  65. package/dist/timeline.js +11 -0
  66. package/dist/tooltip.d.ts +15 -0
  67. package/dist/tooltip.js +25 -0
  68. package/dist/uptime-bar.d.ts +29 -0
  69. package/dist/uptime-bar.js +57 -0
  70. package/package.json +1 -1
  71. package/styles.css +45 -2
@@ -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,90 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { areaPath, linePath } from "./chart-path.js";
4
+ import { cn } from "./cn.js";
5
+ /** Six teintes de la DA (`--chart-1` à `--chart-6`), dans l'ordre, jamais recyclées. */
6
+ export const CHART_COLORS = [
7
+ "var(--chart-1)",
8
+ "var(--chart-2)",
9
+ "var(--chart-3)",
10
+ "var(--chart-4)",
11
+ "var(--chart-5)",
12
+ "var(--chart-6)",
13
+ ];
14
+ const number = new Intl.NumberFormat("fr-CH");
15
+ const dayMonth = new Intl.DateTimeFormat("fr-CH", { day: "numeric", month: "short" });
16
+ /**
17
+ * Un maximum « rond » pour l'axe des valeurs, et ses graduations : 0, 25, 50, 75, 100 plutôt que
18
+ * 0, 23,7, 47,4. `ticks` est le nombre d'intervalles visé.
19
+ */
20
+ export function niceScale(max, ticks = 4) {
21
+ if (!(max > 0))
22
+ return { max: 1, step: 0.25 };
23
+ const raw = max / ticks;
24
+ const magnitude = 10 ** Math.floor(Math.log10(raw));
25
+ const residual = raw / magnitude;
26
+ const nice = residual <= 1 ? 1 : residual <= 2 ? 2 : residual <= 2.5 ? 2.5 : residual <= 5 ? 5 : 10;
27
+ const step = nice * magnitude;
28
+ return { max: Math.ceil(max / step) * step, step };
29
+ }
30
+ /**
31
+ * Série temporelle en SVG, sans bibliothèque : lignes (aire sous la courbe pour une série seule)
32
+ * ou barres empilées. Le SVG s'étire à la largeur de son conteneur ; les traits gardent 2 px
33
+ * (`non-scaling-stroke`) et les textes restent en HTML, nets à toute taille.
34
+ *
35
+ * Survol ou flèches du clavier : un repère vertical et une infobulle avec chaque valeur. Les
36
+ * lecteurs d'écran ont un tableau des données. Légende dès deux séries.
37
+ */
38
+ 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, }) {
39
+ const [active, setActive] = useState(null);
40
+ const n = timestamps.length;
41
+ const hasData = n > 0 && series.some((s) => s.values.some((v) => v !== null));
42
+ if (!hasData)
43
+ 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 }));
44
+ const totals = timestamps.map((_, i) => kind === "bar"
45
+ ? series.reduce((sum, s) => sum + (s.values[i] ?? 0), 0)
46
+ : Math.max(0, ...series.map((s) => s.values[i] ?? 0)));
47
+ const scale = niceScale(Math.max(0, ...totals));
48
+ const ticks = Array.from({ length: Math.round(scale.max / scale.step) + 1 }, (_, i) => i * scale.step);
49
+ const color = (index) => CHART_COLORS[index % CHART_COLORS.length] ?? "currentColor";
50
+ const indexFromPointer = (event) => {
51
+ const rect = event.currentTarget.getBoundingClientRect();
52
+ const ratio = (event.clientX - rect.left) / rect.width;
53
+ return Math.min(n - 1, Math.max(0, Math.floor(ratio * n)));
54
+ };
55
+ const onKeyDown = (event) => {
56
+ const moves = { ArrowLeft: -1, ArrowRight: 1, Home: -n, End: n };
57
+ if (event.key === "Escape")
58
+ return setActive(null);
59
+ const move = moves[event.key];
60
+ if (move === undefined)
61
+ return;
62
+ event.preventDefault();
63
+ setActive((current) => Math.min(n - 1, Math.max(0, (current ?? (move > 0 ? -1 : n)) + move)));
64
+ };
65
+ const activeTime = active !== null ? timestamps[active] : undefined;
66
+ const describe = (i) => {
67
+ const t = timestamps[i];
68
+ const values = series
69
+ .map((s) => `${s.label} ${s.values[i] == null ? "pas de mesure" : formatValue(s.values[i] ?? 0)}`)
70
+ .join(", ");
71
+ return t === undefined ? values : `${formatTime(t)} : ${values}`;
72
+ };
73
+ const xLabels = compact ? [] : [0, Math.floor((n - 1) / 2), n - 1].filter((v, i, a) => a.indexOf(v) === i);
74
+ 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", {
75
+ // Un curseur sur le temps : les flèches le déplacent d'un point, et chaque position se
76
+ // lit en entier (date et valeurs).
77
+ 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
78
+ ? 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)))
79
+ : null, kind === "bar"
80
+ ? timestamps.map((_, i) => {
81
+ let base = 100;
82
+ return series.map((s, si) => {
83
+ const v = s.values[i] ?? 0;
84
+ const h = (v / scale.max) * 100;
85
+ base -= h;
86
+ 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;
87
+ });
88
+ })
89
+ : 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))) })] })] }));
90
+ }
@@ -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;
@@ -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 };
@@ -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,29 @@
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
+ /** Le jour à atteindre pour une touche, ou `null` si la touche ne déplace pas. */
13
+ export declare function nextDay(key: string, current: number, count: number): number | null;
14
+ /**
15
+ * Une barre par jour, du plus ancien au plus récent, comme les pages de statut de référence.
16
+ * Chaque barre a sa couleur et, au survol, une infobulle en texte (jour, statut, disponibilité,
17
+ * note). Les lecteurs d'écran lisent la liste des jours, dont chacun dit son statut en mots.
18
+ *
19
+ * Au clavier, la barre est un seul arrêt de tabulation (le dernier jour visité, aujourd'hui au
20
+ * départ) ; les flèches, Début et Fin passent d'un jour à l'autre et ouvrent son infobulle.
21
+ */
22
+ export declare function UptimeBar({ days, label, summary, className, }: {
23
+ days: Array<UptimeDay>;
24
+ /** Nom de la liste pour les lecteurs d'écran. */
25
+ label?: string;
26
+ /** Au centre de la légende : « 99,95 % sur 90 jours ». */
27
+ summary?: string;
28
+ className?: string;
29
+ }): React.JSX.Element;
@@ -0,0 +1,57 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef, useState } from "react";
3
+ import { SERVICE_STATUS_LABEL } from "./status.js";
4
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
5
+ import { cn } from "./cn.js";
6
+ const BAR = {
7
+ operational: "bg-status-operational",
8
+ degraded: "bg-status-degraded",
9
+ outage: "bg-status-outage",
10
+ maintenance: "bg-status-maintenance",
11
+ unknown: "bg-foreground/10",
12
+ };
13
+ const day = new Intl.DateTimeFormat("fr-CH", { weekday: "short", day: "numeric", month: "short" });
14
+ function describe(entry) {
15
+ return [day.format(entry.date), SERVICE_STATUS_LABEL[entry.status], entry.uptime, entry.note]
16
+ .filter(Boolean)
17
+ .join(" · ");
18
+ }
19
+ /** Le jour à atteindre pour une touche, ou `null` si la touche ne déplace pas. */
20
+ export function nextDay(key, current, count) {
21
+ if (count === 0)
22
+ return null;
23
+ switch (key) {
24
+ case "ArrowLeft":
25
+ return Math.max(0, current - 1);
26
+ case "ArrowRight":
27
+ return Math.min(count - 1, current + 1);
28
+ case "Home":
29
+ return 0;
30
+ case "End":
31
+ return count - 1;
32
+ default:
33
+ return null;
34
+ }
35
+ }
36
+ /**
37
+ * Une barre par jour, du plus ancien au plus récent, comme les pages de statut de référence.
38
+ * Chaque barre a sa couleur et, au survol, une infobulle en texte (jour, statut, disponibilité,
39
+ * note). Les lecteurs d'écran lisent la liste des jours, dont chacun dit son statut en mots.
40
+ *
41
+ * Au clavier, la barre est un seul arrêt de tabulation (le dernier jour visité, aujourd'hui au
42
+ * départ) ; les flèches, Début et Fin passent d'un jour à l'autre et ouvrent son infobulle.
43
+ */
44
+ export function UptimeBar({ days, label = "Disponibilité", summary, className, }) {
45
+ const [focused, setFocused] = useState(null);
46
+ const list = useRef(null);
47
+ const current = Math.min(focused ?? days.length - 1, days.length - 1);
48
+ function onKeyDown(event) {
49
+ const target = nextDay(event.key, current, days.length);
50
+ if (target === null)
51
+ return;
52
+ event.preventDefault();
53
+ setFocused(target);
54
+ list.current?.querySelectorAll("[data-slot=uptime-day]")[target]?.focus();
55
+ }
56
+ return (_jsxs("div", { "data-slot": "uptime-bar", className: cn("grid gap-2", className), children: [_jsx("ol", { ref: list, "aria-label": label, className: "flex h-8 items-stretch gap-[2px]", children: days.map((entry, index) => (_jsx("li", { className: "flex min-w-0 flex-1", children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx("button", { type: "button" }), "data-slot": "uptime-day", tabIndex: index === current ? 0 : -1, onFocus: () => setFocused(index), onKeyDown: onKeyDown, className: cn("block w-full rounded-[3px] outline-none transition-opacity duration-(--duration-fast) ease-out hover:opacity-70 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background data-popup-open:opacity-70", BAR[entry.status]), children: _jsx("span", { className: "sr-only", children: describe(entry) }) }), _jsx(TooltipContent, { children: describe(entry) })] }) }, new Date(entry.date).toISOString()))) }), _jsxs("div", { "aria-hidden": true, className: "flex items-center justify-between gap-2 text-xs text-muted-foreground tabular-nums", children: [_jsx("span", { children: days.length > 0 ? `il y a ${days.length} jours` : null }), summary ? _jsx("span", { className: "text-foreground", children: summary }) : null, _jsx("span", { children: "Aujourd'hui" })] })] }));
57
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hgdc/ui",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Interface de la H family : jetons de la DA, thèmes, polices et composants.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://github.com/hugogebel-boop/hgdc-kit/tree/main/packages/ui#readme",
package/styles.css CHANGED
@@ -18,6 +18,9 @@
18
18
  @source "./dist";
19
19
 
20
20
  @custom-variant dark (&:is(.dark *));
21
+ /* Barre latérale réduite en rail : `data-sidebar="rail"` sur <html>, posé avant la première
22
+ peinture par `sidebarInitScript` (`@hgdc/ui/app-shell`). */
23
+ @custom-variant rail (&:where([data-sidebar="rail"] *));
21
24
 
22
25
  /* Courbes et durées. Pas d'ease-in : il démarre lentement à l'instant où l'œil regarde. */
23
26
  @theme {
@@ -70,6 +73,21 @@
70
73
  --color-info: var(--info);
71
74
  --color-info-ink: var(--info-ink);
72
75
  --color-chart-1: var(--chart-1);
76
+ --color-chart-2: var(--chart-2);
77
+ --color-chart-3: var(--chart-3);
78
+ --color-chart-4: var(--chart-4);
79
+ --color-chart-5: var(--chart-5);
80
+ --color-chart-6: var(--chart-6);
81
+ --color-status-operational: var(--status-operational);
82
+ --color-status-operational-ink: var(--status-operational-ink);
83
+ --color-status-degraded: var(--status-degraded);
84
+ --color-status-degraded-ink: var(--status-degraded-ink);
85
+ --color-status-outage: var(--status-outage);
86
+ --color-status-outage-ink: var(--status-outage-ink);
87
+ --color-status-maintenance: var(--status-maintenance);
88
+ --color-status-maintenance-ink: var(--status-maintenance-ink);
89
+ --color-status-unknown: var(--status-unknown);
90
+ --color-status-unknown-ink: var(--status-unknown-ink);
73
91
  --color-inverse: var(--inverse);
74
92
  --color-inverse-foreground: var(--inverse-foreground);
75
93
  --color-inverse-muted: var(--inverse-muted);
@@ -117,7 +135,27 @@
117
135
  --shell-muted-foreground: oklch(0.45 0 0);
118
136
  --shell-hover: oklch(0 0 0 / 5%);
119
137
  --shell-active: oklch(0 0 0 / 8%);
120
- --chart-1: oklch(0.62 0.19 256);
138
+ /* Séries de graphique, dans cet ordre, jamais recyclées : une 7e série se replie en « Autres ».
139
+ Validées (daltonisme, écart entre voisines) sur `surface` en clair et sur `card` en sombre.
140
+ En clair, 2 à 5 passent sous 3:1 : un graphique les accompagne d'une légende et de libellés. */
141
+ --chart-1: oklch(0.575 0.163 255.5);
142
+ --chart-2: oklch(0.671 0.175 40.6);
143
+ --chart-3: oklch(0.669 0.141 162.1);
144
+ --chart-4: oklch(0.764 0.161 75.1);
145
+ --chart-5: oklch(0.716 0.141 357.4);
146
+ --chart-6: oklch(0.527 0.197 283.4);
147
+ /* Disponibilité d'un service : toujours avec un libellé (`StatusBadge`), jamais la couleur seule.
148
+ Chaque jeton suit une couleur de la DA, donc les deux thèmes. */
149
+ --status-operational: var(--status-active);
150
+ --status-operational-ink: var(--status-active-ink);
151
+ --status-degraded: var(--status-paused);
152
+ --status-degraded-ink: var(--status-paused-ink);
153
+ --status-outage: var(--destructive);
154
+ --status-outage-ink: var(--destructive);
155
+ --status-maintenance: var(--info);
156
+ --status-maintenance-ink: var(--info-ink);
157
+ --status-unknown: var(--muted-foreground);
158
+ --status-unknown-ink: var(--muted-foreground);
121
159
  --status-active: oklch(0.55 0.15 156);
122
160
  --status-paused: oklch(0.62 0.14 75);
123
161
  --status-done: oklch(0.52 0.2 293);
@@ -160,7 +198,12 @@
160
198
  --shell-muted-foreground: oklch(0.708 0 0);
161
199
  --shell-hover: oklch(1 0 0 / 6%);
162
200
  --shell-active: oklch(1 0 0 / 12%);
163
- --chart-1: oklch(0.68 0.16 256);
201
+ --chart-1: oklch(0.622 0.161 255.1);
202
+ --chart-2: oklch(0.622 0.173 40.1);
203
+ --chart-3: oklch(0.621 0.128 163.1);
204
+ --chart-4: oklch(0.67 0.143 73.2);
205
+ --chart-5: oklch(0.622 0.171 0.8);
206
+ --chart-6: oklch(0.67 0.145 286.8);
164
207
  --status-active: oklch(0.74 0.14 156);
165
208
  --status-paused: oklch(0.82 0.15 86);
166
209
  --status-done: oklch(0.68 0.17 293);