@hgdc/ui 0.4.0 → 0.5.1

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/dist/status.d.ts CHANGED
@@ -1,22 +1,45 @@
1
1
  /** Disponibilité d'un service, vocabulaire fermé de la famille (Status, Host, Mail). */
2
2
  export type ServiceStatus = "operational" | "degraded" | "outage" | "maintenance" | "unknown";
3
3
  export declare const SERVICE_STATUS_LABEL: Record<ServiceStatus, string>;
4
+ /**
5
+ * Ton d'un statut quelconque (déploiement, e-mail, domaine), quand le vocabulaire des services ne
6
+ * convient pas : `pending` pour ce qui attend ou tourne encore (« En file », « Vérification »).
7
+ */
8
+ export type StatusTone = "neutral" | "success" | "warning" | "danger" | "info" | "pending";
9
+ /** Couleur de chaque ton, prise dans les jetons de statut : mêmes teintes que les services. */
10
+ export declare const STATUS_TONE_COLOR: Readonly<Record<StatusTone, ServiceStatus>>;
4
11
  /**
5
12
  * Le point d'un statut, décoratif (`aria-hidden`) : il accompagne toujours un libellé.
6
13
  * `live` ajoute l'anneau qui s'élargit, pour ce qui est en cours (incident, vérification DNS).
7
14
  */
8
- export declare function StatusDot({ status, live, className, }: {
9
- status: ServiceStatus;
15
+ export declare function StatusDot({ status: statusProp, tone, live, className, }: {
16
+ /** Statut de service ; sinon `tone`. */
17
+ status?: ServiceStatus;
18
+ tone?: StatusTone;
10
19
  live?: boolean;
11
20
  className?: string;
12
21
  }): 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, }: {
22
+ export type StatusBadgeProps = {
23
+ /** Disponibilité d'un service : le libellé de la famille par défaut. */
18
24
  status: ServiceStatus;
25
+ tone?: never;
19
26
  label?: string;
20
27
  live?: boolean;
21
28
  className?: string;
22
- }): React.JSX.Element;
29
+ } | {
30
+ status?: never;
31
+ /** Statut d'autre chose (déploiement, e-mail, domaine) : le libellé est alors obligatoire. */
32
+ tone: StatusTone;
33
+ label: string;
34
+ /** Par défaut vrai pour `pending` : l'anneau dit que ça tourne encore. */
35
+ live?: boolean;
36
+ className?: string;
37
+ };
38
+ /**
39
+ * Un statut : point et libellé, jamais la couleur seule. Le libellé par défaut est celui de la
40
+ * famille ; un produit peut le préciser (« Panne partielle ») sans changer de couleur.
41
+ *
42
+ * Hors des services, `tone` et `label` : `<StatusBadge tone="success" label="Remis" />`,
43
+ * `<StatusBadge tone="pending" label="En construction" />`.
44
+ */
45
+ export declare function StatusBadge(props: StatusBadgeProps): React.JSX.Element;
package/dist/status.js CHANGED
@@ -21,17 +21,33 @@ const BADGE = {
21
21
  maintenance: "bg-status-maintenance/12 text-status-maintenance-ink",
22
22
  unknown: "bg-foreground/[0.06] text-status-unknown-ink",
23
23
  };
24
+ /** Couleur de chaque ton, prise dans les jetons de statut : mêmes teintes que les services. */
25
+ export const STATUS_TONE_COLOR = {
26
+ neutral: "unknown",
27
+ success: "operational",
28
+ warning: "degraded",
29
+ danger: "outage",
30
+ info: "maintenance",
31
+ pending: "degraded",
32
+ };
24
33
  /**
25
34
  * Le point d'un statut, décoratif (`aria-hidden`) : il accompagne toujours un libellé.
26
35
  * `live` ajoute l'anneau qui s'élargit, pour ce qui est en cours (incident, vérification DNS).
27
36
  */
28
- export function StatusDot({ status, live = false, className, }) {
37
+ export function StatusDot({ status: statusProp, tone, live = false, className, }) {
38
+ const status = statusProp ?? STATUS_TONE_COLOR[tone ?? "neutral"];
29
39
  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
40
  }
31
41
  /**
32
42
  * Un statut : point et libellé, jamais la couleur seule. Le libellé par défaut est celui de la
33
43
  * famille ; un produit peut le préciser (« Panne partielle ») sans changer de couleur.
44
+ *
45
+ * Hors des services, `tone` et `label` : `<StatusBadge tone="success" label="Remis" />`,
46
+ * `<StatusBadge tone="pending" label="En construction" />`.
34
47
  */
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]] }));
48
+ export function StatusBadge(props) {
49
+ const { label, className } = props;
50
+ const status = props.status !== undefined ? props.status : STATUS_TONE_COLOR[props.tone];
51
+ const live = props.live ?? props.tone === "pending";
52
+ return (_jsxs("span", { "data-slot": "status-badge", "data-status": status, ...(props.tone ? { "data-tone": props.tone } : {}), 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
53
  }
@@ -23,7 +23,7 @@ export declare function niceScale(max: number, ticks?: number): {
23
23
  * Survol ou flèches du clavier : un repère vertical et une infobulle avec chaque valeur. Les
24
24
  * lecteurs d'écran ont un tableau des données. Légende dès deux séries.
25
25
  */
26
- export declare function TimeChart({ timestamps, series, kind, height, compact, label, formatValue, formatTime, emptyLabel, className, }: {
26
+ export declare function TimeChart({ timestamps, series, kind, height, compact, label, formatValue, formatTime, emptyLabel, lastIncomplete, className, }: {
27
27
  timestamps: Array<number>;
28
28
  series: Array<TimeSeries>;
29
29
  kind?: "line" | "bar";
@@ -36,5 +36,11 @@ export declare function TimeChart({ timestamps, series, kind, height, compact, l
36
36
  formatValue?: (value: number) => string;
37
37
  formatTime?: (time: number) => string;
38
38
  emptyLabel?: string;
39
+ /**
40
+ * Le dernier point couvre une période pas encore finie (aujourd'hui, ce mois-ci) : sa ligne est
41
+ * en pointillés, sa barre pâlie, et l'infobulle le dit. Sans cela, une chute du dernier jour
42
+ * se lit comme une baisse alors que la journée n'est pas terminée.
43
+ */
44
+ lastIncomplete?: boolean;
39
45
  className?: string;
40
46
  }): React.JSX.Element;
@@ -35,7 +35,7 @@ export function niceScale(max, ticks = 4) {
35
35
  * Survol ou flèches du clavier : un repère vertical et une infobulle avec chaque valeur. Les
36
36
  * lecteurs d'écran ont un tableau des données. Légende dès deux séries.
37
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, }) {
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.", lastIncomplete = false, className, }) {
39
39
  const [active, setActive] = useState(null);
40
40
  const n = timestamps.length;
41
41
  const hasData = n > 0 && series.some((s) => s.values.some((v) => v !== null));
@@ -62,13 +62,15 @@ export function TimeChart({ timestamps, series, kind = "line", height = 200, com
62
62
  event.preventDefault();
63
63
  setActive((current) => Math.min(n - 1, Math.max(0, (current ?? (move > 0 ? -1 : n)) + move)));
64
64
  };
65
+ const incompleteIndex = lastIncomplete && n > 1 ? n - 1 : -1;
65
66
  const activeTime = active !== null ? timestamps[active] : undefined;
66
67
  const describe = (i) => {
67
68
  const t = timestamps[i];
68
69
  const values = series
69
70
  .map((s) => `${s.label} ${s.values[i] == null ? "pas de mesure" : formatValue(s.values[i] ?? 0)}`)
70
71
  .join(", ");
71
- return t === undefined ? values : `${formatTime(t)} : ${values}`;
72
+ const time = t === undefined ? "" : `${formatTime(t)}${i === incompleteIndex ? " (en cours)" : ""} : `;
73
+ return `${time}${values}`;
72
74
  };
73
75
  const xLabels = compact ? [] : [0, Math.floor((n - 1) / 2), n - 1].filter((v, i, a) => a.indexOf(v) === i);
74
76
  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", {
@@ -83,8 +85,8 @@ export function TimeChart({ timestamps, series, kind = "line", height = 200, com
83
85
  const v = s.values[i] ?? 0;
84
86
  const h = (v / scale.max) * 100;
85
87
  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;
88
+ 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) * (i === incompleteIndex ? 0.5 : 1) }, `${s.key}-${i}`)) : null;
87
89
  });
88
90
  })
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))) })] })] }));
91
+ : 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(incompleteIndex < 0 ? s.values : s.values.map((v, i) => (i === incompleteIndex ? null : v)), scale.max), fill: "none", stroke: color(si), strokeWidth: 2, strokeLinejoin: "round", strokeLinecap: "round", vectorEffect: "non-scaling-stroke" }), incompleteIndex >= 0 ? (_jsx("path", { "data-slot": "time-chart-incomplete", d: linePath(s.values.map((v, i) => (i >= incompleteIndex - 1 ? v : null)), scale.max), fill: "none", stroke: color(si), strokeWidth: 2, strokeDasharray: "3 4", strokeLinecap: "round", vectorEffect: "non-scaling-stroke" })) : null] }, 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: [_jsxs("p", { className: "font-medium", children: [formatTime(activeTime), active === incompleteIndex ? (_jsx("span", { className: "ml-1.5 font-normal text-muted-foreground", children: "En cours" })) : null] }), 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: [_jsxs("th", { scope: "row", children: [formatTime(t), i === incompleteIndex ? " (en cours)" : ""] }), series.map((s) => (_jsx("td", { children: s.values[i] === null ? "-" : formatValue(s.values[i] ?? 0) }, s.key)))] }, t))) })] })] }));
90
92
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hgdc/ui",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
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
@@ -394,6 +394,19 @@
394
394
  animation: live-ping 1.6s var(--ease-out) infinite;
395
395
  }
396
396
 
397
+ /* Barre d'avancement de durée inconnue : un tiers de piste qui glisse, à vitesse constante. */
398
+ @keyframes progress-slide {
399
+ from {
400
+ translate: -100% 0;
401
+ }
402
+ to {
403
+ translate: 300% 0;
404
+ }
405
+ }
406
+ @utility progress-slide {
407
+ animation: progress-slide 1.4s linear infinite;
408
+ }
409
+
397
410
  /* Des étapes qui montent l'une après l'autre au premier affichage. */
398
411
  @keyframes rise {
399
412
  from {
@@ -420,4 +433,10 @@
420
433
  .card-in {
421
434
  transition: none;
422
435
  }
436
+ /* Sans mouvement, la durée inconnue se lit sur une piste pleine et pâle. */
437
+ .progress-slide {
438
+ animation: none;
439
+ width: 100%;
440
+ opacity: 0.35;
441
+ }
423
442
  }