@hgdc/ui 0.3.0 → 0.5.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 +56 -24
- package/dist/account-menu.d.ts +15 -2
- package/dist/account-menu.js +8 -2
- package/dist/address-fields.d.ts +50 -0
- package/dist/address-fields.js +79 -0
- package/dist/chart-path.d.ts +9 -0
- package/dist/chart-path.js +46 -0
- package/dist/color-field.d.ts +32 -0
- package/dist/color-field.js +80 -0
- package/dist/command-menu.d.ts +4 -2
- package/dist/command-menu.js +5 -3
- package/dist/copy-button.d.ts +5 -2
- package/dist/copy-button.js +5 -2
- package/dist/data-table.d.ts +70 -0
- package/dist/data-table.js +46 -0
- package/dist/date-range-picker.d.ts +50 -0
- package/dist/date-range-picker.js +118 -0
- package/dist/date-time-field.d.ts +18 -0
- package/dist/date-time-field.js +31 -0
- package/dist/destructive-dialog.d.ts +28 -0
- package/dist/destructive-dialog.js +74 -0
- package/dist/dns-record-table.d.ts +30 -0
- package/dist/dns-record-table.js +33 -0
- package/dist/error-card.d.ts +23 -0
- package/dist/error-card.js +35 -0
- package/dist/filter-bar.d.ts +50 -0
- package/dist/filter-bar.js +88 -0
- package/dist/log-viewer.d.ts +39 -0
- package/dist/log-viewer.js +114 -0
- package/dist/middle-truncate.d.ts +17 -0
- package/dist/middle-truncate.js +25 -0
- package/dist/money-field.d.ts +31 -0
- package/dist/money-field.js +98 -0
- package/dist/products.d.ts +44 -0
- package/dist/products.js +47 -0
- package/dist/progress.d.ts +38 -0
- package/dist/progress.js +31 -0
- package/dist/rank-bar.d.ts +28 -0
- package/dist/rank-bar.js +31 -0
- package/dist/settings-section.d.ts +40 -0
- package/dist/settings-section.js +48 -0
- package/dist/sonner.d.ts +20 -1
- package/dist/sonner.js +43 -2
- package/dist/sparkline.d.ts +21 -0
- package/dist/sparkline.js +28 -0
- package/dist/status.d.ts +31 -8
- package/dist/status.js +19 -3
- package/dist/time-chart.d.ts +7 -1
- package/dist/time-chart.js +7 -42
- package/dist/uptime-bar.d.ts +5 -0
- package/dist/uptime-bar.js +33 -1
- package/package.json +1 -1
- package/styles.css +19 -0
package/dist/rank-bar.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
3
|
+
import { cn } from "./cn.js";
|
|
4
|
+
const number = new Intl.NumberFormat("fr-CH");
|
|
5
|
+
function RankRow({ item, max, formatValue, }) {
|
|
6
|
+
const width = max > 0 ? Math.max(0, Math.min(1, item.value / max)) : 0;
|
|
7
|
+
const content = (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, "data-slot": "rank-bar-fill", className: "absolute inset-y-0 left-0 rounded-lg bg-foreground/[0.06] transition-[width] duration-(--duration-base) ease-out", style: { width: `${width * 100}%` } }), _jsxs("span", { className: "relative flex min-w-0 flex-1 items-center gap-2", children: [item.icon ? (_jsx("span", { "aria-hidden": true, className: "grid size-4 shrink-0 place-items-center", children: item.icon })) : null, _jsx("span", { className: "min-w-0 truncate", children: item.label })] }), _jsx("span", { className: "relative shrink-0 font-medium tabular-nums", children: formatValue(item.value) })] }));
|
|
8
|
+
const row = "relative flex min-h-9 items-center gap-3 rounded-lg px-2.5 text-sm";
|
|
9
|
+
const link = useRender({
|
|
10
|
+
render: item.render,
|
|
11
|
+
enabled: item.render !== undefined,
|
|
12
|
+
props: {
|
|
13
|
+
className: cn(row, "outline-none transition-colors duration-(--duration-fast) ease-out hover:bg-foreground/[0.04] focus-visible:ring-2 focus-visible:ring-ring/50"),
|
|
14
|
+
children: content,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
return _jsx("li", { children: link ?? _jsx("div", { className: row, children: content }) });
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Classement d'un tableau de bord : pages les plus vues, domaines qui envoient le plus, pays.
|
|
21
|
+
* Une ligne par élément, triée du plus grand au plus petit, sa barre proportionnelle au premier.
|
|
22
|
+
* Une liste ordonnée : le rang se lit sans la barre, et chaque valeur s'écrit.
|
|
23
|
+
*/
|
|
24
|
+
export function RankBar({ label, items, limit, formatValue = (v) => number.format(v), valueLabel, emptyLabel = "Rien à classer sur cette période.", className, }) {
|
|
25
|
+
const sorted = [...items].sort((a, b) => b.value - a.value);
|
|
26
|
+
const shown = limit === undefined ? sorted : sorted.slice(0, limit);
|
|
27
|
+
const max = shown[0]?.value ?? 0;
|
|
28
|
+
if (shown.length === 0)
|
|
29
|
+
return (_jsx("p", { "data-slot": "rank-bar", "data-state": "empty", className: cn("py-6 text-center text-sm text-muted-foreground", className), children: emptyLabel }));
|
|
30
|
+
return (_jsxs("div", { "data-slot": "rank-bar", className: cn("grid gap-1", className), children: [valueLabel ? (_jsx("p", { "aria-hidden": true, className: "flex justify-end px-2.5 text-xs text-muted-foreground", children: valueLabel })) : null, _jsx("ol", { "aria-label": label, className: "grid gap-1", children: shown.map((item) => (_jsx(RankRow, { item: item, max: max, formatValue: formatValue }, item.key))) })] }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Un bloc de réglages : titre, description, champs, et un pied avec son propre bouton
|
|
4
|
+
* Enregistrer. Chaque bloc s'enregistre seul : on ne perd pas la saisie d'un bloc en validant
|
|
5
|
+
* un autre, et l'erreur dit lequel a échoué. Repris de la page de réglages de Host.
|
|
6
|
+
*
|
|
7
|
+
* Avec `onSave`, le bloc est un formulaire : Entrée dans un champ enregistre, le bouton reste
|
|
8
|
+
* grisé tant que `dirty` est faux et tourne pendant `saving`. Sans `onSave`, `footer` reçoit
|
|
9
|
+
* les actions telles quelles (« Supprimer le projet » d'un bloc `tone="danger"`).
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* <SettingsSection
|
|
13
|
+
* title="Nom de l'espace"
|
|
14
|
+
* description="Visible par les membres et dans les e-mails d'invitation."
|
|
15
|
+
* dirty={name !== initial}
|
|
16
|
+
* saving={saving}
|
|
17
|
+
* onSave={save}
|
|
18
|
+
* >
|
|
19
|
+
* <Field><FieldLabel>Nom</FieldLabel><Input value={name} onChange={…} /></Field>
|
|
20
|
+
* </SettingsSection>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function SettingsSection({ title, description, children, footer, footerNote, tone, onSave, dirty, saving, saveLabel, className, }: {
|
|
24
|
+
title: string;
|
|
25
|
+
description?: ReactNode;
|
|
26
|
+
children?: ReactNode;
|
|
27
|
+
/** Actions du pied, à la place du bouton Enregistrer (ou avant lui avec `onSave`). */
|
|
28
|
+
footer?: ReactNode;
|
|
29
|
+
/** Texte discret à gauche du pied : « Appliqué au prochain déploiement. » */
|
|
30
|
+
footerNote?: ReactNode;
|
|
31
|
+
/** `danger` : la zone des actions irréversibles, cerclée de rouge. */
|
|
32
|
+
tone?: "default" | "danger";
|
|
33
|
+
/** Enregistre le bloc et affiche lui-même son erreur (toast, `FieldError`) : rien ne l'attrape ici. */
|
|
34
|
+
onSave?: () => void | Promise<void>;
|
|
35
|
+
/** Faux : rien n'a changé, le bouton reste grisé. */
|
|
36
|
+
dirty?: boolean;
|
|
37
|
+
saving?: boolean;
|
|
38
|
+
saveLabel?: string;
|
|
39
|
+
className?: string;
|
|
40
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from "react";
|
|
3
|
+
import { Button } from "./button.js";
|
|
4
|
+
import { cn } from "./cn.js";
|
|
5
|
+
/**
|
|
6
|
+
* Un bloc de réglages : titre, description, champs, et un pied avec son propre bouton
|
|
7
|
+
* Enregistrer. Chaque bloc s'enregistre seul : on ne perd pas la saisie d'un bloc en validant
|
|
8
|
+
* un autre, et l'erreur dit lequel a échoué. Repris de la page de réglages de Host.
|
|
9
|
+
*
|
|
10
|
+
* Avec `onSave`, le bloc est un formulaire : Entrée dans un champ enregistre, le bouton reste
|
|
11
|
+
* grisé tant que `dirty` est faux et tourne pendant `saving`. Sans `onSave`, `footer` reçoit
|
|
12
|
+
* les actions telles quelles (« Supprimer le projet » d'un bloc `tone="danger"`).
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <SettingsSection
|
|
16
|
+
* title="Nom de l'espace"
|
|
17
|
+
* description="Visible par les membres et dans les e-mails d'invitation."
|
|
18
|
+
* dirty={name !== initial}
|
|
19
|
+
* saving={saving}
|
|
20
|
+
* onSave={save}
|
|
21
|
+
* >
|
|
22
|
+
* <Field><FieldLabel>Nom</FieldLabel><Input value={name} onChange={…} /></Field>
|
|
23
|
+
* </SettingsSection>
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function SettingsSection({ title, description, children, footer, footerNote, tone = "default", onSave, dirty = true, saving = false, saveLabel = "Enregistrer", className, }) {
|
|
27
|
+
const titleId = useId();
|
|
28
|
+
const descriptionId = useId();
|
|
29
|
+
const hasFooter = Boolean(footer || footerNote || onSave);
|
|
30
|
+
const body = (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid gap-4 p-5 sm:p-6", children: [_jsxs("div", { children: [_jsx("h2", { id: titleId, className: "text-base font-semibold text-balance", children: title }), description ? (_jsx("p", { id: descriptionId, className: "mt-1 max-w-prose text-sm text-pretty text-muted-foreground", children: description })) : null] }), children] }), hasFooter ? (_jsxs("div", { "data-slot": "settings-section-footer", className: "flex flex-wrap items-center justify-end gap-3 border-t border-border bg-muted/40 px-5 py-3 sm:px-6", children: [footerNote ? _jsx("p", { className: "mr-auto text-sm text-muted-foreground", children: footerNote }) : null, footer, onSave ? (_jsx(Button, { type: "submit", size: "sm", disabled: !dirty, isLoading: saving, children: saveLabel })) : null] })) : null] }));
|
|
31
|
+
const shared = {
|
|
32
|
+
"data-slot": "settings-section",
|
|
33
|
+
"data-tone": tone,
|
|
34
|
+
"aria-labelledby": titleId,
|
|
35
|
+
...(description ? { "aria-describedby": descriptionId } : {}),
|
|
36
|
+
className: cn("overflow-hidden rounded-3xl surface", tone === "danger" && "ring-1 ring-destructive/30", className),
|
|
37
|
+
};
|
|
38
|
+
if (!onSave)
|
|
39
|
+
return _jsx("section", { ...shared, children: body });
|
|
40
|
+
const submit = (event) => {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
if (!dirty || saving)
|
|
43
|
+
return;
|
|
44
|
+
void onSave();
|
|
45
|
+
};
|
|
46
|
+
// Un `<form>` nommé par son titre a le rôle « form » : il apparaît dans les repères du lecteur d'écran.
|
|
47
|
+
return (_jsx("form", { ...shared, onSubmit: submit, children: body }));
|
|
48
|
+
}
|
package/dist/sonner.d.ts
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
1
|
import { type ToasterProps } from "sonner";
|
|
2
2
|
declare function Toaster(props: ToasterProps): React.JSX.Element;
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Retour d'une action réversible, avec « Annuler » pendant quelques secondes : on agit tout de
|
|
5
|
+
* suite (mise à jour optimiste), sans boîte de confirmation, et on laisse revenir en arrière.
|
|
6
|
+
* Une action qui ne se rattrape pas passe par `DestructiveDialog`.
|
|
7
|
+
*
|
|
8
|
+
* `onUndo` défait l'action. `onCommit`, s'il est donné, part quand le toast se ferme sans
|
|
9
|
+
* annulation (délai écoulé, fermé à la main) : pour une action différée, envoyée seulement
|
|
10
|
+
* une fois le délai passé. Rend l'identifiant du toast (`toast.dismiss(id)`).
|
|
11
|
+
*/
|
|
12
|
+
declare function toastUndo(message: React.ReactNode, { onUndo, onCommit, description, duration, undoLabel, }: {
|
|
13
|
+
onUndo: () => unknown;
|
|
14
|
+
onCommit?: () => unknown;
|
|
15
|
+
description?: React.ReactNode;
|
|
16
|
+
/** En millisecondes ; Sonner met le délai en pause au survol et quand l'onglet est caché. */
|
|
17
|
+
duration?: number;
|
|
18
|
+
undoLabel?: string;
|
|
19
|
+
}): string | number;
|
|
20
|
+
/** Le message d'une erreur, lisible : celui de l'`Error`, sinon une phrase générique. */
|
|
21
|
+
declare function errorMessage(error: unknown, fallback?: string): string;
|
|
22
|
+
export { Toaster, toastUndo, errorMessage };
|
package/dist/sonner.js
CHANGED
|
@@ -4,7 +4,7 @@ import CheckmarkCircle02Icon from "@hugeicons/core-free-icons/CheckmarkCircle02I
|
|
|
4
4
|
import InformationCircleIcon from "@hugeicons/core-free-icons/InformationCircleIcon";
|
|
5
5
|
import MultiplicationSignCircleIcon from "@hugeicons/core-free-icons/MultiplicationSignCircleIcon";
|
|
6
6
|
import { HugeiconsIcon } from "@hugeicons/react";
|
|
7
|
-
import { Toaster as Sonner } from "sonner";
|
|
7
|
+
import { Toaster as Sonner, toast } from "sonner";
|
|
8
8
|
import { Spinner } from "./spinner.js";
|
|
9
9
|
import { useTheme } from "./theme.js";
|
|
10
10
|
function Toaster(props) {
|
|
@@ -30,4 +30,45 @@ function Toaster(props) {
|
|
|
30
30
|
},
|
|
31
31
|
}, ...props }));
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Retour d'une action réversible, avec « Annuler » pendant quelques secondes : on agit tout de
|
|
35
|
+
* suite (mise à jour optimiste), sans boîte de confirmation, et on laisse revenir en arrière.
|
|
36
|
+
* Une action qui ne se rattrape pas passe par `DestructiveDialog`.
|
|
37
|
+
*
|
|
38
|
+
* `onUndo` défait l'action. `onCommit`, s'il est donné, part quand le toast se ferme sans
|
|
39
|
+
* annulation (délai écoulé, fermé à la main) : pour une action différée, envoyée seulement
|
|
40
|
+
* une fois le délai passé. Rend l'identifiant du toast (`toast.dismiss(id)`).
|
|
41
|
+
*/
|
|
42
|
+
function toastUndo(message, { onUndo, onCommit, description, duration = 6000, undoLabel = "Annuler", }) {
|
|
43
|
+
let settled = false;
|
|
44
|
+
const commit = () => {
|
|
45
|
+
if (settled)
|
|
46
|
+
return;
|
|
47
|
+
settled = true;
|
|
48
|
+
void onCommit?.();
|
|
49
|
+
};
|
|
50
|
+
return toast(message, {
|
|
51
|
+
description,
|
|
52
|
+
duration,
|
|
53
|
+
action: {
|
|
54
|
+
label: undoLabel,
|
|
55
|
+
onClick: () => {
|
|
56
|
+
if (settled)
|
|
57
|
+
return;
|
|
58
|
+
settled = true;
|
|
59
|
+
void onUndo();
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
onAutoClose: commit,
|
|
63
|
+
onDismiss: commit,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** Le message d'une erreur, lisible : celui de l'`Error`, sinon une phrase générique. */
|
|
67
|
+
function errorMessage(error, fallback = "Quelque chose n'a pas marché. Réessaie.") {
|
|
68
|
+
if (error instanceof Error && error.message.trim() !== "")
|
|
69
|
+
return error.message;
|
|
70
|
+
if (typeof error === "string" && error.trim() !== "")
|
|
71
|
+
return error;
|
|
72
|
+
return fallback;
|
|
73
|
+
}
|
|
74
|
+
export { Toaster, toastUndo, errorMessage };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Ce que la courbe dit, en une phrase, pour qui ne la voit pas. */
|
|
2
|
+
export declare function describeSparkline(label: string, values: ReadonlyArray<number | null>, format?: (value: number) => string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Tendance en un trait, sans axe ni info-bulle, à côté d'un chiffre (ligne de tableau, `StatTile`).
|
|
5
|
+
* L'échelle va du minimum au maximum de la série : c'est la forme qui compte, pas le zéro. Pour lire
|
|
6
|
+
* des valeurs, c'est `TimeChart`. Les lecteurs d'écran entendent l'étendue et la dernière valeur.
|
|
7
|
+
*/
|
|
8
|
+
export declare function Sparkline({ values, label, color, area, height, formatValue, className, }: {
|
|
9
|
+
/** Du plus ancien au plus récent ; `null` : pas de mesure, le trait s'interrompt. */
|
|
10
|
+
values: ReadonlyArray<number | null>;
|
|
11
|
+
/** Ce que mesure la courbe : « Envois sur 7 jours ». */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Teinte `--chart-1` à `--chart-6`. */
|
|
14
|
+
color?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
15
|
+
/** Lavis sous la courbe. */
|
|
16
|
+
area?: boolean;
|
|
17
|
+
/** En pixels ; la largeur suit le conteneur. */
|
|
18
|
+
height?: number;
|
|
19
|
+
formatValue?: (value: number) => string;
|
|
20
|
+
className?: string;
|
|
21
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { areaPath, linePath } from "./chart-path.js";
|
|
3
|
+
import { cn } from "./cn.js";
|
|
4
|
+
import { CHART_COLORS } from "./time-chart.js";
|
|
5
|
+
const number = new Intl.NumberFormat("fr-CH");
|
|
6
|
+
/** Ce que la courbe dit, en une phrase, pour qui ne la voit pas. */
|
|
7
|
+
export function describeSparkline(label, values, format = (v) => number.format(v)) {
|
|
8
|
+
const known = values.filter((v) => v !== null);
|
|
9
|
+
const last = known.at(-1);
|
|
10
|
+
if (last === undefined)
|
|
11
|
+
return `${label} : aucune donnée`;
|
|
12
|
+
return `${label} : de ${format(Math.min(...known))} à ${format(Math.max(...known))}, dernière valeur ${format(last)}`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Tendance en un trait, sans axe ni info-bulle, à côté d'un chiffre (ligne de tableau, `StatTile`).
|
|
16
|
+
* L'échelle va du minimum au maximum de la série : c'est la forme qui compte, pas le zéro. Pour lire
|
|
17
|
+
* des valeurs, c'est `TimeChart`. Les lecteurs d'écran entendent l'étendue et la dernière valeur.
|
|
18
|
+
*/
|
|
19
|
+
export function Sparkline({ values, label, color = 1, area = true, height = 32, formatValue, className, }) {
|
|
20
|
+
const known = values.filter((v) => v !== null);
|
|
21
|
+
const min = Math.min(...known);
|
|
22
|
+
const max = Math.max(...known);
|
|
23
|
+
const stroke = CHART_COLORS[color - 1] ?? "currentColor";
|
|
24
|
+
const description = describeSparkline(label, values, formatValue);
|
|
25
|
+
// Une série plate se dessine au milieu ; une marge en haut et en bas garde le trait entier.
|
|
26
|
+
const [lo, hi] = max > min ? [min, max] : [min - 1, max + 1];
|
|
27
|
+
return (_jsx("span", { "data-slot": "sparkline", role: "img", "aria-label": description, style: { height }, className: cn("inline-block w-24 min-w-0 align-middle", className), children: known.length > 0 ? (_jsxs("svg", { "aria-hidden": true, viewBox: `0 -6 ${values.length} 112`, preserveAspectRatio: "none", className: "block size-full overflow-visible", children: [area ? _jsx("path", { d: areaPath(values, hi, lo), fill: stroke, opacity: 0.12 }) : null, _jsx("path", { d: linePath(values, hi, lo), fill: "none", stroke: stroke, strokeWidth: 1.5, strokeLinejoin: "round", strokeLinecap: "round", vectorEffect: "non-scaling-stroke" })] })) : (_jsx("span", { "aria-hidden": true, className: "flex size-full items-center", children: _jsx("span", { className: "h-px w-full bg-foreground/10" }) })) }));
|
|
28
|
+
}
|
package/dist/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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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(
|
|
36
|
-
|
|
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
|
}
|
package/dist/time-chart.d.ts
CHANGED
|
@@ -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;
|
package/dist/time-chart.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
+
import { areaPath, linePath } from "./chart-path.js";
|
|
3
4
|
import { cn } from "./cn.js";
|
|
4
5
|
/** Six teintes de la DA (`--chart-1` à `--chart-6`), dans l'ordre, jamais recyclées. */
|
|
5
6
|
export const CHART_COLORS = [
|
|
@@ -26,44 +27,6 @@ export function niceScale(max, ticks = 4) {
|
|
|
26
27
|
const step = nice * magnitude;
|
|
27
28
|
return { max: Math.ceil(max / step) * step, step };
|
|
28
29
|
}
|
|
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
30
|
/**
|
|
68
31
|
* Série temporelle en SVG, sans bibliothèque : lignes (aire sous la courbe pour une série seule)
|
|
69
32
|
* ou barres empilées. Le SVG s'étire à la largeur de son conteneur ; les traits gardent 2 px
|
|
@@ -72,7 +35,7 @@ function areaPath(values, max) {
|
|
|
72
35
|
* Survol ou flèches du clavier : un repère vertical et une infobulle avec chaque valeur. Les
|
|
73
36
|
* lecteurs d'écran ont un tableau des données. Légende dès deux séries.
|
|
74
37
|
*/
|
|
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, }) {
|
|
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, }) {
|
|
76
39
|
const [active, setActive] = useState(null);
|
|
77
40
|
const n = timestamps.length;
|
|
78
41
|
const hasData = n > 0 && series.some((s) => s.values.some((v) => v !== null));
|
|
@@ -99,13 +62,15 @@ export function TimeChart({ timestamps, series, kind = "line", height = 200, com
|
|
|
99
62
|
event.preventDefault();
|
|
100
63
|
setActive((current) => Math.min(n - 1, Math.max(0, (current ?? (move > 0 ? -1 : n)) + move)));
|
|
101
64
|
};
|
|
65
|
+
const incompleteIndex = lastIncomplete && n > 1 ? n - 1 : -1;
|
|
102
66
|
const activeTime = active !== null ? timestamps[active] : undefined;
|
|
103
67
|
const describe = (i) => {
|
|
104
68
|
const t = timestamps[i];
|
|
105
69
|
const values = series
|
|
106
70
|
.map((s) => `${s.label} ${s.values[i] == null ? "pas de mesure" : formatValue(s.values[i] ?? 0)}`)
|
|
107
71
|
.join(", ");
|
|
108
|
-
|
|
72
|
+
const time = t === undefined ? "" : `${formatTime(t)}${i === incompleteIndex ? " (en cours)" : ""} : `;
|
|
73
|
+
return `${time}${values}`;
|
|
109
74
|
};
|
|
110
75
|
const xLabels = compact ? [] : [0, Math.floor((n - 1) / 2), n - 1].filter((v, i, a) => a.indexOf(v) === i);
|
|
111
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", {
|
|
@@ -120,8 +85,8 @@ export function TimeChart({ timestamps, series, kind = "line", height = 200, com
|
|
|
120
85
|
const v = s.values[i] ?? 0;
|
|
121
86
|
const h = (v / scale.max) * 100;
|
|
122
87
|
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;
|
|
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;
|
|
124
89
|
});
|
|
125
90
|
})
|
|
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: [
|
|
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))) })] })] }));
|
|
127
92
|
}
|
package/dist/uptime-bar.d.ts
CHANGED
|
@@ -9,10 +9,15 @@ export type UptimeDay = {
|
|
|
9
9
|
/** Ce qui s'est passé : « Panne de 12 min, base de données ». */
|
|
10
10
|
note?: string;
|
|
11
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;
|
|
12
14
|
/**
|
|
13
15
|
* Une barre par jour, du plus ancien au plus récent, comme les pages de statut de référence.
|
|
14
16
|
* Chaque barre a sa couleur et, au survol, une infobulle en texte (jour, statut, disponibilité,
|
|
15
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.
|
|
16
21
|
*/
|
|
17
22
|
export declare function UptimeBar({ days, label, summary, className, }: {
|
|
18
23
|
days: Array<UptimeDay>;
|
package/dist/uptime-bar.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
2
3
|
import { SERVICE_STATUS_LABEL } from "./status.js";
|
|
3
4
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.js";
|
|
4
5
|
import { cn } from "./cn.js";
|
|
@@ -15,11 +16,42 @@ function describe(entry) {
|
|
|
15
16
|
.filter(Boolean)
|
|
16
17
|
.join(" · ");
|
|
17
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
|
+
}
|
|
18
36
|
/**
|
|
19
37
|
* Une barre par jour, du plus ancien au plus récent, comme les pages de statut de référence.
|
|
20
38
|
* Chaque barre a sa couleur et, au survol, une infobulle en texte (jour, statut, disponibilité,
|
|
21
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.
|
|
22
43
|
*/
|
|
23
44
|
export function UptimeBar({ days, label = "Disponibilité", summary, className, }) {
|
|
24
|
-
|
|
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" })] })] }));
|
|
25
57
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hgdc/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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
|
@@ -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
|
}
|