@sia-ui/cli 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.
Files changed (100) hide show
  1. package/LICENSE +21 -21
  2. package/dist/index.js +2 -0
  3. package/dist/registry/package/components/Alert/index.tsx +7 -0
  4. package/dist/registry/package/components/AmountDisplay/index.tsx +8 -0
  5. package/dist/registry/package/components/AppShell/index.tsx +413 -3
  6. package/dist/registry/package/components/AppShell/styles.css +259 -7
  7. package/dist/registry/package/components/AppShell/tab-bar.tsx +134 -0
  8. package/dist/registry/package/components/AsyncState/index.tsx +7 -0
  9. package/dist/registry/package/components/AuditMeta/index.tsx +8 -0
  10. package/dist/registry/package/components/AuthLayout/index.tsx +7 -0
  11. package/dist/registry/package/components/Autocomplete/index.tsx +7 -0
  12. package/dist/registry/package/components/Avatar/index.tsx +333 -12
  13. package/dist/registry/package/components/Avatar/styles.css +236 -5
  14. package/dist/registry/package/components/Badge/index.tsx +7 -0
  15. package/dist/registry/package/components/Button/index.tsx +7 -0
  16. package/dist/registry/package/components/Calendar/index.tsx +11 -0
  17. package/dist/registry/package/components/Card/index.tsx +8 -0
  18. package/dist/registry/package/components/Checkbox/index.tsx +8 -0
  19. package/dist/registry/package/components/ColorPicker/index.tsx +7 -0
  20. package/dist/registry/package/components/CommandPalette/index.tsx +7 -0
  21. package/dist/registry/package/components/ConfirmDialog/index.tsx +7 -0
  22. package/dist/registry/package/components/Container/index.tsx +7 -0
  23. package/dist/registry/package/components/Countdown/index.tsx +7 -0
  24. package/dist/registry/package/components/CrudPage/dialogs.tsx +230 -0
  25. package/dist/registry/package/components/CrudPage/index.tsx +546 -5
  26. package/dist/registry/package/components/CrudPage/styles.css +25 -1
  27. package/dist/registry/package/components/CurrencyInput/index.tsx +7 -0
  28. package/dist/registry/package/components/DataTable/index.tsx +548 -200
  29. package/dist/registry/package/components/DataTable/query.ts +68 -0
  30. package/dist/registry/package/components/DataTable/row-actions.tsx +158 -0
  31. package/dist/registry/package/components/DataTable/styles.css +285 -73
  32. package/dist/registry/package/components/DataTable/types.ts +194 -0
  33. package/dist/registry/package/components/DataTable/values.ts +28 -0
  34. package/dist/registry/package/components/DatePicker/index.tsx +7 -0
  35. package/dist/registry/package/components/DateRangeFilter/index.tsx +7 -0
  36. package/dist/registry/package/components/DateRangePicker/index.tsx +7 -0
  37. package/dist/registry/package/components/DateTimePicker/index.tsx +7 -0
  38. package/dist/registry/package/components/Descriptions/index.tsx +7 -0
  39. package/dist/registry/package/components/Divider/index.tsx +7 -0
  40. package/dist/registry/package/components/Drawer/index.tsx +8 -0
  41. package/dist/registry/package/components/DurationDisplay/index.tsx +6 -0
  42. package/dist/registry/package/components/EmptyState/index.tsx +7 -0
  43. package/dist/registry/package/components/EntityMeta/index.tsx +7 -0
  44. package/dist/registry/package/components/ErrorState/index.tsx +8 -0
  45. package/dist/registry/package/components/EventCalendar/index.tsx +7 -0
  46. package/dist/registry/package/components/Field/index.tsx +12 -0
  47. package/dist/registry/package/components/FileUpload/index.tsx +7 -0
  48. package/dist/registry/package/components/FiltersBar/index.tsx +7 -0
  49. package/dist/registry/package/components/HoverCard/index.tsx +14 -37
  50. package/dist/registry/package/components/IconButton/index.tsx +8 -0
  51. package/dist/registry/package/components/Icons/index.tsx +15 -0
  52. package/dist/registry/package/components/ImageUpload/index.tsx +7 -0
  53. package/dist/registry/package/components/Input/index.tsx +7 -0
  54. package/dist/registry/package/components/JsonEditor/index.tsx +7 -0
  55. package/dist/registry/package/components/KpiGrid/index.tsx +7 -0
  56. package/dist/registry/package/components/Label/index.tsx +7 -0
  57. package/dist/registry/package/components/MarkdownEditor/index.tsx +7 -0
  58. package/dist/registry/package/components/MiniCalendar/index.tsx +7 -0
  59. package/dist/registry/package/components/Modal/index.tsx +8 -0
  60. package/dist/registry/package/components/MonthPicker/index.tsx +7 -0
  61. package/dist/registry/package/components/MultiSelect/index.tsx +7 -0
  62. package/dist/registry/package/components/OtpInput/index.tsx +7 -0
  63. package/dist/registry/package/components/Overlay/index.tsx +34 -24
  64. package/dist/registry/package/components/PageHeader/index.tsx +7 -0
  65. package/dist/registry/package/components/PermissionGate/index.tsx +6 -0
  66. package/dist/registry/package/components/Popover/index.tsx +7 -0
  67. package/dist/registry/package/components/QrCode/index.tsx +7 -0
  68. package/dist/registry/package/components/RadioGroup/index.tsx +7 -0
  69. package/dist/registry/package/components/Rating/index.tsx +6 -0
  70. package/dist/registry/package/components/ReferenceSelect/index.tsx +7 -0
  71. package/dist/registry/package/components/RelativeTime/index.tsx +6 -0
  72. package/dist/registry/package/components/ResizablePanel/index.tsx +7 -0
  73. package/dist/registry/package/components/Resource/index.tsx +514 -0
  74. package/dist/registry/package/components/RichTextEditor/index.tsx +7 -0
  75. package/dist/registry/package/components/SearchInput/index.tsx +8 -0
  76. package/dist/registry/package/components/Select/index.tsx +11 -0
  77. package/dist/registry/package/components/Sidebar/index.tsx +489 -113
  78. package/dist/registry/package/components/Sidebar/styles.css +96 -4
  79. package/dist/registry/package/components/Skeleton/index.tsx +7 -0
  80. package/dist/registry/package/components/Spinner/index.tsx +7 -0
  81. package/dist/registry/package/components/StatCard/index.tsx +7 -0
  82. package/dist/registry/package/components/Statistic/index.tsx +7 -0
  83. package/dist/registry/package/components/Switch/index.tsx +7 -0
  84. package/dist/registry/package/components/Tabs/index.tsx +7 -0
  85. package/dist/registry/package/components/TagsInput/index.tsx +7 -0
  86. package/dist/registry/package/components/Textarea/index.tsx +7 -0
  87. package/dist/registry/package/components/TimePicker/index.tsx +7 -0
  88. package/dist/registry/package/components/TimeRangePicker/index.tsx +6 -0
  89. package/dist/registry/package/components/Timeline/index.tsx +7 -0
  90. package/dist/registry/package/components/Timer/index.tsx +7 -0
  91. package/dist/registry/package/components/Tour/index.tsx +7 -0
  92. package/dist/registry/package/components/Transfer/index.tsx +7 -0
  93. package/dist/registry/package/components/Tree/index.tsx +7 -0
  94. package/dist/registry/package/components/TreeSelect/index.tsx +7 -0
  95. package/dist/registry/package/components/Typography/index.tsx +8 -0
  96. package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +8 -0
  97. package/dist/registry/package/components/WeekPicker/index.tsx +7 -0
  98. package/dist/registry/package/components/YearPicker/index.tsx +7 -0
  99. package/dist/registry/registry.json +69 -14
  100. package/package.json +14 -14
@@ -0,0 +1,194 @@
1
+ import type { Key, ReactNode } from "react";
2
+ import type { PermissionRule } from "@sia-ui/headless";
3
+ import type { ComponentTone } from "@sia-ui/tokens";
4
+
5
+ /** Liste dense, ou grille de cartes. */
6
+ export type DataTableView = "table" | "cards";
7
+
8
+ /**
9
+ * Le rôle d'une colonne dans la carte.
10
+ *
11
+ * `title` sert d'en-tête — la première colonne le prend si rien n'est
12
+ * déclaré. `meta` s'affiche en paire libellé / valeur. `hidden` reste au
13
+ * tableau seul : une carte qui recopie douze colonnes n'est plus une carte.
14
+ */
15
+ export type ColumnCardRole = "title" | "meta" | "hidden";
16
+
17
+ export interface DataTableColumn<T> {
18
+ key: string;
19
+ header: ReactNode;
20
+
21
+ /**
22
+ * Où lire la valeur : une clé de l'objet, ou une fonction pour les valeurs
23
+ * calculées. Sert au rendu par défaut, à la carte et à la recherche locale.
24
+ */
25
+ accessor?: keyof T | ((row: T) => unknown);
26
+
27
+ /** Rendu personnalisé. Prioritaire sur `accessor`. */
28
+ cell?: (row: T, index: number) => ReactNode;
29
+
30
+ align?: "start" | "center" | "end";
31
+
32
+ /**
33
+ * Largeur suggérée — `120px`, `20%`.
34
+ *
35
+ * En `layout="auto"` (défaut) le navigateur n'y voit qu'une indication.
36
+ * Pour qu'elle soit tenue, passer en `layout="fixed"`.
37
+ */
38
+ width?: string;
39
+ minWidth?: string;
40
+
41
+ /** Coupe au lieu d'élargir. Indispensable en `layout="fixed"`. */
42
+ truncate?: boolean;
43
+
44
+ /** Retire la colonne du tableau sans toucher à la carte. */
45
+ hidden?: boolean;
46
+
47
+ /** Rend l'en-tête cliquable. Le tri lui-même reste au serveur. */
48
+ sortable?: boolean;
49
+ /** Le champ envoyé au serveur, si différent de `key`. */
50
+ sortKey?: string;
51
+
52
+ /** Place de la colonne dans la carte. */
53
+ card?: ColumnCardRole;
54
+
55
+ headerClassName?: string;
56
+ cellClassName?: string;
57
+ }
58
+
59
+ export interface RowAction<T> {
60
+ key: string;
61
+ label: ReactNode;
62
+ icon?: ReactNode;
63
+ tone?: ComponentTone;
64
+
65
+ /** Masque l'action pour cette ligne — un solde nul, un envoi déjà annulé. */
66
+ hidden?: (row: T, index: number) => boolean;
67
+ disabled?: (row: T, index: number) => boolean;
68
+
69
+ /** Même grammaire de droits que la navigation. */
70
+ permission?: PermissionRule;
71
+
72
+ /** Demande confirmation. Obligatoire pour tout ce qui détruit. */
73
+ confirm?: {
74
+ title: ReactNode;
75
+ description?: ReactNode;
76
+ confirmLabel?: string;
77
+ cancelLabel?: string;
78
+ destructive?: boolean;
79
+ };
80
+
81
+ onSelect: (row: T, index: number) => void | Promise<void>;
82
+ }
83
+
84
+ export interface DataTableProps<T> {
85
+ columns: Array<DataTableColumn<T>>;
86
+ data: T[];
87
+
88
+ /** La clé d'une ligne. Sans elle, l'index — à éviter si les lignes bougent. */
89
+ getRowKey?: (row: T, index: number) => Key;
90
+
91
+ /* ─────────────────────────────────────────────── en-tête et recherche */
92
+
93
+ title?: ReactNode;
94
+ description?: ReactNode;
95
+ /** Zone libre : filtres, sélecteur de période. */
96
+ toolbar?: ReactNode;
97
+ /** Actions globales, à droite. */
98
+ actions?: ReactNode;
99
+
100
+ /**
101
+ * Les clés sur lesquelles porte la recherche.
102
+ *
103
+ * Absente, aucun champ n'est affiché : un tableau sans champ déclaré n'a
104
+ * rien à chercher. Le filtrage est local, sur les lignes déjà chargées.
105
+ */
106
+ searchKeys?: string[];
107
+ searchPlaceholder?: string;
108
+ /** Recherche déléguée. Fournie, le tableau cesse de filtrer lui-même. */
109
+ onSearch?: (query: string) => void;
110
+
111
+ /* ─────────────────────────────────────────────────────── mise en page */
112
+
113
+ layout?: "auto" | "fixed";
114
+ /** Hauteur du corps. Sans elle, `stickyHeader` n'a rien où se coller. */
115
+ maxHeight?: string;
116
+ stickyHeader?: boolean;
117
+ density?: "compact" | "default" | "comfortable";
118
+ /** Un fond alterné, une ligne sur deux. */
119
+ striped?: boolean;
120
+ bordered?: boolean;
121
+
122
+ /* ────────────────────────────────────────────────── actions de ligne */
123
+
124
+ rowActions?: Array<RowAction<T>> | ((row: T, index: number) => Array<RowAction<T>>);
125
+ /** Au-delà, les actions passent dans un menu. */
126
+ inlineActionsLimit?: number;
127
+ actionsHeader?: ReactNode;
128
+ /** `icon` garde la colonne étroite sur un tableau déjà large. */
129
+ actionsDisplay?: "icon" | "label";
130
+
131
+ /** Évalue les règles de permission des actions. */
132
+ can?: (rule: PermissionRule) => boolean;
133
+
134
+ /* ────────────────────────────────────────────────── modes d'affichage */
135
+
136
+ view?: DataTableView;
137
+ defaultView?: DataTableView;
138
+ onViewChange?: (view: DataTableView) => void;
139
+ showViewToggle?: boolean;
140
+
141
+ /**
142
+ * Bascule en cartes sous le point de rupture. Actif par défaut : un
143
+ * tableau à sept colonnes est illisible sur un téléphone.
144
+ */
145
+ responsive?: boolean;
146
+ cardsBreakpoint?: number;
147
+
148
+ /** Rendu complet d'une carte. Sans lui, elle est déduite des colonnes. */
149
+ renderCard?: (row: T, index: number) => ReactNode;
150
+
151
+ /* ──────────────────────────────────────────────────────────── états */
152
+
153
+ loading?: boolean;
154
+ /** Lignes fantômes pendant le premier chargement. */
155
+ skeletonRows?: number;
156
+ error?: ReactNode;
157
+ onRetry?: () => void;
158
+ empty?: {
159
+ title?: ReactNode;
160
+ description?: ReactNode;
161
+ action?: ReactNode;
162
+ };
163
+
164
+ /* ───────────────────────────────────────────────────────── sélection */
165
+
166
+ /**
167
+ * Ajoute une colonne de cases à cocher.
168
+ *
169
+ * Sur une liste paginée côté serveur, la case d'en-tête ne porte que sur la
170
+ * page affichée : le composant ne connaît pas les lignes qu'il n'a pas
171
+ * reçues, et prétendre « tout sélectionner » serait un mensonge.
172
+ */
173
+ selectable?: boolean;
174
+ selectedKeys?: Key[];
175
+ defaultSelectedKeys?: Key[];
176
+ onSelectionChange?: (keys: Key[], rows: T[]) => void;
177
+ isRowSelectable?: (row: T, index: number) => boolean;
178
+ /** Barre affichée dès qu'une ligne est cochée. */
179
+ selectionActions?: (context: {
180
+ keys: Key[];
181
+ rows: T[];
182
+ clear: () => void;
183
+ }) => ReactNode;
184
+
185
+ /* ───────────────────────────────────────────────────────────── tri */
186
+
187
+ sort?: { field: string; direction: "asc" | "desc" } | null;
188
+ onSortChange?: (field: string) => void;
189
+
190
+ onRowClick?: (row: T, index: number) => void;
191
+ rowClassName?: (row: T, index: number) => string | undefined;
192
+ caption?: string;
193
+ className?: string;
194
+ }
@@ -0,0 +1,28 @@
1
+ import type { ReactNode } from "react";
2
+ import type { DataTableColumn } from "./types";
3
+
4
+ /**
5
+ * Lire une colonne.
6
+ *
7
+ * Tenu à part du tableau parce que trois endroits en ont besoin : le tableau
8
+ * lui-même, la carte, et la vue de détail de `CrudPage`. Recopié, ce petit
9
+ * morceau se serait mis à diverger — la vue de détail aurait ignoré les
10
+ * `accessor` calculés, et personne ne l'aurait remarqué avant de voir une
11
+ * ligne vide dans un modal.
12
+ */
13
+ export function valeurDe<T>(row: T, column: DataTableColumn<T>): unknown {
14
+ if (typeof column.accessor === "function") return column.accessor(row);
15
+ if (column.accessor !== undefined) return row[column.accessor];
16
+ return (row as Record<string, unknown>)[column.key];
17
+ }
18
+
19
+ /** Ce qu'une cellule affiche : son rendu propre, ou sa valeur en texte. */
20
+ export function renduCellule<T>(
21
+ row: T,
22
+ index: number,
23
+ column: DataTableColumn<T>,
24
+ ): ReactNode {
25
+ if (column.cell) return column.cell(row, index);
26
+ const valeur = valeurDe(row, column);
27
+ return valeur === null || valeur === undefined ? null : String(valeur);
28
+ }
@@ -40,6 +40,13 @@ function defaultFormat(value: string, locale: string) {
40
40
  return Number.isNaN(date.getTime()) ? value : new Intl.DateTimeFormat(locale, { day: "2-digit", month: "short", year: "numeric" }).format(date);
41
41
  }
42
42
 
43
+ /**
44
+ * Une date, choisie au calendrier ou saisie au clavier.
45
+ *
46
+ * La valeur est une chaîne `AAAA-MM-JJ` : un objet `Date` porte une heure
47
+ * et un fuseau dont une date d'échéance n'a que faire, et qui la décalent
48
+ * d'un jour selon l'endroit d'où on la lit.
49
+ */
43
50
  export const DatePicker = forwardRef<HTMLButtonElement, DatePickerProps>(function DatePicker({
44
51
  id,
45
52
  name,
@@ -5,4 +5,11 @@ import "./styles.css";
5
5
 
6
6
  export interface DateRangePreset { label: ReactNode; value: DateRangeValue; }
7
7
  export interface DateRangeFilterProps { value: DateRangeValue; onValueChange: (value: DateRangeValue) => void; presets?: DateRangePreset[]; onClear?: () => void; className?: string; }
8
+ /**
9
+ * Une période, avec ses raccourcis.
10
+ *
11
+ * Les périodes utiles se comptent sur une main — ce mois, le trimestre,
12
+ * l'exercice — et les proposer évite de saisir deux dates pour la question
13
+ * que tout le monde pose en premier.
14
+ */
8
15
  export function DateRangeFilter({ value, onValueChange, presets = [], onClear, className }: DateRangeFilterProps) { return <section className={cn("sia-date-range-filter", className)}><div className="sia-date-range-filter__presets">{presets.map((preset, index) => <button type="button" key={index} onClick={() => onValueChange(preset.value)}>{preset.label}</button>)}</div><DateRangePicker value={value} onValueChange={onValueChange} />{onClear && (value.start || value.end) && <button type="button" className="sia-date-range-filter__clear" onClick={onClear}>Effacer</button>}</section>; }
@@ -21,6 +21,13 @@ export interface DateRangePickerProps extends Pick<CalendarProps, "locale" | "fi
21
21
  disabled?: boolean;
22
22
  className?: string;
23
23
  }
24
+ /**
25
+ * Un début et une fin, choisis ensemble.
26
+ *
27
+ * Deux champs séparés laissent poser une fin antérieure au début, et il
28
+ * faut alors décider lequel des deux avait tort. Ici les deux bornes sont
29
+ * une seule valeur, et le calendrier les tient dans l'ordre.
30
+ */
24
31
  export function DateRangePicker({ value, defaultValue = { start: "", end: "" }, onValueChange, min, max, startLabel = "Date de début", endLabel = "Date de fin", allowClear = true, placement = "bottom-start", tone = "primary", invalid, disabled, locale = "fr-FR", firstDayOfWeek = 1, disabledDate, cellRender, showWeek, className }: DateRangePickerProps) {
25
32
  const anchorRef = useRef<HTMLButtonElement>(null);
26
33
  const [internal, setInternal] = useState(defaultValue);
@@ -23,6 +23,13 @@ export interface DateTimePickerProps {
23
23
  timeProps?: Omit<TimePickerProps, "value" | "defaultValue" | "onValueChange" | "tone" | "invalid" | "disabled">;
24
24
  className?: string;
25
25
  }
26
+ /**
27
+ * Une date et une heure, dans un seul champ.
28
+ *
29
+ * `needConfirm` existe parce que l'heure se choisit après la date : sans
30
+ * validation explicite, le panneau se referme sur la date et l'heure reste
31
+ * celle de la veille.
32
+ */
26
33
  export function DateTimePicker({ value, defaultValue = { date: "", time: "" }, onValueChange, minuteStep = 1, secondStep = 1, showSeconds = false, hourFormat = 24, needConfirm = false, minDate, maxDate, tone = "primary", invalid, disabled, dateProps, timeProps, className }: DateTimePickerProps) {
27
34
  const [internal, setInternal] = useState(defaultValue);
28
35
  const current = value ?? internal;
@@ -4,4 +4,11 @@ import "./styles.css";
4
4
 
5
5
  export interface DescriptionItem { key: string; label: ReactNode; value: ReactNode; span?: 1 | 2 | 3; }
6
6
  export interface DescriptionsProps { items: DescriptionItem[]; title?: ReactNode; columns?: 1 | 2 | 3; bordered?: boolean; className?: string; }
7
+ /**
8
+ * Des paires libellé / valeur, en colonnes.
9
+ *
10
+ * C'est la forme d'une fiche en lecture — ce qu'un formulaire montre quand
11
+ * il n'y a rien à saisir. `span` laisse une valeur longue occuper toute la
12
+ * ligne plutôt que d'être coupée dans une colonne étroite.
13
+ */
7
14
  export function Descriptions({ items, title, columns = 2, bordered = false, className }: DescriptionsProps) { return <section className={cn("sia-descriptions", bordered && "sia-descriptions--bordered", className)}>{title && <h3>{title}</h3>}<dl className={`sia-descriptions__grid sia-descriptions__grid--${columns}`}>{items.map((item) => <div key={item.key} style={{ gridColumn: `span ${item.span ?? 1}` }}><dt>{item.label}</dt><dd>{item.value}</dd></div>)}</dl></section>; }
@@ -7,6 +7,13 @@ export interface DividerProps extends HTMLAttributes<HTMLDivElement> {
7
7
  decorative?: boolean;
8
8
  }
9
9
 
10
+ /**
11
+ * Un trait entre deux choses.
12
+ *
13
+ * `decorative` décide s'il compte pour un lecteur d'écran. Un trait qui
14
+ * sépare deux groupes de sens est une information; un trait qui aère ne
15
+ * l'est pas, et l'annoncer ajoute du bruit à chaque parcours.
16
+ */
10
17
  export function Divider({ orientation = "horizontal", decorative = true, className, ...props }: DividerProps) {
11
18
  return <div className={cn("sia-divider", `sia-divider--${orientation}`, className)} role={decorative ? "presentation" : "separator"} aria-orientation={decorative ? undefined : orientation} {...props} />;
12
19
  }
@@ -46,6 +46,14 @@ export type DrawerComponent = ForwardRefExoticComponent<
46
46
  Close: typeof DrawerClose;
47
47
  };
48
48
 
49
+ /**
50
+ * Un panneau qui entre par un bord.
51
+ *
52
+ * Il sert là où une boîte modale serait trop : un filtre qu'on ajuste en
53
+ * regardant la liste derrière, un formulaire qu'on remplit sans perdre sa
54
+ * place. Le focus y est enfermé tant qu'il est ouvert, comme dans un
55
+ * modal — sans quoi la tabulation continue dans la page cachée.
56
+ */
49
57
  export const Drawer = forwardRef<DrawerRef, IDrawerProps>(
50
58
  (
51
59
  {
@@ -1,3 +1,9 @@
1
1
  import { cn } from "@sia-ui/utils";
2
2
  export interface DurationDisplayProps { milliseconds: number; locale?: string; style?: "short" | "long" | "clock"; className?: string; }
3
+ /**
4
+ * Une durée, en mots ou en pendule.
5
+ *
6
+ * Trois styles pour trois usages : `clock` pour un chronomètre qu'on lit en
7
+ * continu, `short` pour une colonne de tableau, `long` pour une phrase.
8
+ */
3
9
  export function DurationDisplay({ milliseconds, locale = "fr-FR", style = "short", className }: DurationDisplayProps) { const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000)); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; if (style === "clock") return <span className={cn("sia-duration-display", className)}>{[hours, minutes, seconds].map((value) => String(value).padStart(2, "0")).join(":")}</span>; const formatter = new Intl.NumberFormat(locale); const parts = [[hours, style === "long" ? "heure" : "h"], [minutes, style === "long" ? "minute" : "min"], [seconds, style === "long" ? "seconde" : "s"]] as const; return <span className={cn("sia-duration-display", className)}>{parts.filter(([value]) => value > 0).map(([value, unit]) => `${formatter.format(value)} ${unit}${style === "long" && value > 1 ? "s" : ""}`).join(" ") || `0 ${style === "long" ? "seconde" : "s"}`}</span>; }
@@ -11,6 +11,13 @@ export interface EmptyStateProps {
11
11
  className?: string;
12
12
  }
13
13
 
14
+ /**
15
+ * Ce qu'on montre quand il n'y a rien.
16
+ *
17
+ * Une liste vide sans explication ressemble à un chargement bloqué. Elle
18
+ * porte donc une action : le vide initial est le meilleur moment pour
19
+ * proposer de créer le premier élément.
20
+ */
14
21
  export function EmptyState({ icon, title, description, action, compact, className }: EmptyStateProps) {
15
22
  return (
16
23
  <section className={cn("sia-empty-state", compact && "sia-empty-state--compact", className)}>
@@ -32,6 +32,13 @@ function Detail({
32
32
  );
33
33
  }
34
34
 
35
+ /**
36
+ * Les traces d'un objet : créé le, modifié par, identifiant.
37
+ *
38
+ * L'identifiant est normalisé pour être lu à voix haute au téléphone —
39
+ * c'est la première chose qu'on demande à un client, et une chaîne de
40
+ * trente-six caractères ne se dicte pas.
41
+ */
35
42
  export function EntityMeta({
36
43
  createdAt,
37
44
  updatedAt,
@@ -21,6 +21,14 @@ function getErrorMessage(error: unknown): string | undefined {
21
21
  return undefined;
22
22
  }
23
23
 
24
+ /**
25
+ * Un échec, et de quoi en sortir.
26
+ *
27
+ * `error` accepte n'importe quoi : une `Error`, une réponse HTTP, une
28
+ * chaîne. Ce qui arrive d'un appel réseau n'a pas de forme garantie, et
29
+ * exiger un type ferait écrire un `try` autour de chaque affichage
30
+ * d'erreur.
31
+ */
24
32
  export function ErrorState({ title: titleProp, description, error, action, onRetry, retryLabel: retryLabelProp, compact, className }: ErrorStateProps) {
25
33
  const locale = useSiaLocale();
26
34
  const retryLabel = retryLabelProp ?? locale.retry;
@@ -4,5 +4,12 @@ import "./styles.css";
4
4
 
5
5
  export interface CalendarEvent { id: string; date: string; title: string; tone?: ComponentTone; }
6
6
  export interface EventCalendarProps extends Omit<CalendarProps, "renderDay"> { events: CalendarEvent[]; maxEventsPerDay?: number; onEventClick?: (event: CalendarEvent) => void; }
7
+ /**
8
+ * Un calendrier qui porte des événements.
9
+ *
10
+ * `maxEventsPerDay` plafonne l'affichage : une journée à douze rendez-vous
11
+ * déformerait toute la grille, et les douze ne se lisent de toute façon pas
12
+ * dans une case de calendrier.
13
+ */
7
14
  export function EventCalendar({ events, maxEventsPerDay = 2, onEventClick, ...props }: EventCalendarProps) { const renderDay = (day: CalendarDay) => { const matches = events.filter((event) => event.date === day.date).slice(0, maxEventsPerDay); return <span className="sia-event-calendar__events">{matches.map((event) => <button type="button" key={event.id} className={`sia-event-calendar__event sia-event-calendar__event--${event.tone ?? "primary"}`} onClick={() => onEventClick?.(event)}>{event.title}</button>)}</span>; }; return <Calendar {...props} className={cnEvent(props.className)} renderDay={renderDay} />; }
8
15
  function cnEvent(className?: string) { return ["sia-event-calendar", className].filter(Boolean).join(" "); }
@@ -559,6 +559,18 @@ function pick<K extends string, V>(key: K, value: V | undefined) {
559
559
  return (value === undefined ? {} : { [key]: value }) as { [P in K]?: V };
560
560
  }
561
561
 
562
+ /**
563
+ * Le champ, et tout ce qui l'entoure.
564
+ *
565
+ * Un libellé, un contrôle, un message d'erreur, et les attributs qui les
566
+ * relient — `id`, `aria-describedby`, `aria-invalid`. Ce câblage est
567
+ * toujours le même et toujours oublié quelque part : ici il est fait une
568
+ * fois.
569
+ *
570
+ * `type` choisit le contrôle parmi les vingt-sept que la bibliothèque
571
+ * fournit. Le champ ne les réimplémente pas : il les monte et leur passe le
572
+ * branchement.
573
+ */
562
574
  export function Field(rawProps: FieldProps) {
563
575
  const props = applyBinding(rawProps);
564
576
  const {
@@ -15,6 +15,13 @@ export interface FileUploadProps extends Omit<
15
15
  description?: string;
16
16
  onFilesChange?: (files: File[]) => void;
17
17
  }
18
+ /**
19
+ * Un dépôt de fichiers, par clic ou par glisser.
20
+ *
21
+ * Le glisser-déposer seul exclut le clavier et le tactile; le bouton seul
22
+ * ignore le geste que tout le monde essaie d'abord. Les deux mènent au même
23
+ * champ natif.
24
+ */
18
25
  export function FileUpload({
19
26
  label = "Ajouter des fichiers",
20
27
  description = "Glissez les fichiers ici ou parcourez votre appareil.",
@@ -13,6 +13,13 @@ export interface FiltersBarProps {
13
13
  className?: string;
14
14
  }
15
15
 
16
+ /**
17
+ * La barre au-dessus d'une liste : recherche, filtres, actions.
18
+ *
19
+ * `activeCount` affiche combien de filtres sont en vigueur. Sans ce compte,
20
+ * une liste filtrée ressemble à une liste vide, et l'on cherche la panne
21
+ * avant de penser au filtre posé la veille.
22
+ */
16
23
  export function FiltersBar({ search, filters, actions, activeCount = 0, onReset, resetLabel: resetLabelProp, className }: FiltersBarProps) {
17
24
  const locale = useSiaLocale();
18
25
  const resetLabel = resetLabelProp ?? locale.reset;
@@ -1,14 +1,12 @@
1
1
  import {
2
2
  cloneElement,
3
3
  isValidElement,
4
- useCallback,
5
- useEffect,
6
4
  useRef,
7
- useState,
8
5
  type ReactElement,
9
6
  type ReactNode,
10
7
  } from "react";
11
8
  import { cn } from "@sia-ui/utils";
9
+ import { useHoverIntent } from "@sia-ui/headless";
12
10
  import { Overlay, type OverlayPlacement } from "../Overlay";
13
11
  import "./styles.css";
14
12
 
@@ -48,49 +46,29 @@ export function HoverCard({
48
46
  className,
49
47
  }: HoverCardProps) {
50
48
  const anchorRef = useRef<HTMLElement>(null);
51
- const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
52
- const [internalOpen, setInternalOpen] = useState(false);
53
- const open = openProp ?? internalOpen;
54
49
 
55
- const setOpen = useCallback(
56
- (next: boolean) => {
57
- if (openProp === undefined) setInternalOpen(next);
58
- onOpenChange?.(next);
59
- },
60
- [onOpenChange, openProp],
61
- );
62
-
63
- const schedule = useCallback(
64
- (next: boolean, delay: number) => {
65
- if (timer.current) clearTimeout(timer.current);
66
- timer.current = setTimeout(() => setOpen(next), delay);
67
- },
68
- [setOpen],
69
- );
70
-
71
- useEffect(
72
- () => () => {
73
- if (timer.current) clearTimeout(timer.current);
74
- },
75
- [],
76
- );
50
+ // Les deux délais vivent dans `@sia-ui/headless` : le volet de navigation
51
+ // du `Sidebar` pose exactement la même question, et deux réglages qui
52
+ // divergent donnent deux sensations différentes dans la même application.
53
+ const { open, triggerProps, panelProps, setOpen } = useHoverIntent({
54
+ openDelay,
55
+ closeDelay,
56
+ ...(openProp !== undefined ? { open: openProp } : {}),
57
+ ...(onOpenChange ? { onOpenChange } : {}),
58
+ });
77
59
 
78
60
  const trigger = isValidElement(children)
79
61
  ? cloneElement(children, {
80
62
  ref: anchorRef,
81
- // Le focus ouvre aussi : au clavier il n'y a pas de survol, et une
82
- // fiche qu'on ne peut atteindre qu'à la souris n'existe pas pour
83
- // tout le monde.
63
+ ...triggerProps,
84
64
  onFocus: () => {
85
65
  children.props.onFocus?.();
86
- schedule(true, 0);
66
+ triggerProps.onFocus();
87
67
  },
88
- onBlur: () => schedule(false, closeDelay),
89
68
  onMouseEnter: () => {
90
69
  children.props.onMouseEnter?.();
91
- schedule(true, openDelay);
70
+ triggerProps.onMouseEnter();
92
71
  },
93
- onMouseLeave: () => schedule(false, closeDelay),
94
72
  } as never)
95
73
  : children;
96
74
 
@@ -110,8 +88,7 @@ export function HoverCard({
110
88
  className="sia-hover-card__body"
111
89
  // Survoler le panneau le maintient ouvert : sans cela il se ferme
112
90
  // dès que la souris quitte le déclencheur pour y aller.
113
- onMouseEnter={() => schedule(true, 0)}
114
- onMouseLeave={() => schedule(false, closeDelay)}
91
+ {...panelProps}
115
92
  >
116
93
  {content}
117
94
  </div>
@@ -13,6 +13,14 @@ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
13
13
  tooltip?: Omit<TooltipProps, "children">;
14
14
  }
15
15
 
16
+ /**
17
+ * Une action réduite à son icône.
18
+ *
19
+ * `label` est obligatoire : une icône seule n'a pas de nom, et un lecteur
20
+ * d'écran annoncerait « bouton » sans dire lequel. Le libellé sert aussi
21
+ * d'infobulle — ce que l'icône signifie doit rester atteignable à la
22
+ * souris comme au clavier.
23
+ */
16
24
  export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
17
25
  ({ label, icon, variant: variantProp, size: sizeProp, radius: radiusProp, tooltip, className, type = "button", ...props }, ref) => {
18
26
  const defaults = useComponentDefaults<IconButtonProps>("iconButton");
@@ -1,3 +1,18 @@
1
+ /**
2
+ * Le jeu d'icônes commun.
3
+ *
4
+ * Il n'a pas de composant principal : c'est une collection, et chaque icône y
5
+ * est une fonction. Toutes passent par le même socle — un trait de 1,8, des
6
+ * extrémités arrondies, une taille en `em` qui suit celle du texte, et un
7
+ * `aria-hidden` d'office. Une icône ne porte jamais de sens à elle seule;
8
+ * c'est le libellé à côté qui le porte.
9
+ *
10
+ * Ce jeu couvre ce dont la bibliothèque a besoin, et rien de plus. Un produit
11
+ * qui veut sa propre iconographie passe ses icônes en props plutôt que
12
+ * d'étendre celle-ci : embarquer trois cents dessins pour en servir douze
13
+ * pèserait sur tous les projets.
14
+ */
15
+
1
16
  import type { SVGProps } from "react";
2
17
 
3
18
  type IconProps = SVGProps<SVGSVGElement>;
@@ -3,4 +3,11 @@ import { FileUpload, type FileUploadProps } from "../FileUpload";
3
3
  import { cn } from "@sia-ui/utils";
4
4
  import "./styles.css";
5
5
  export interface ImageUploadProps extends Omit<FileUploadProps, "accept" | "onFilesChange"> { accept?: string; value?: string; onValueChange?: (file: File | null, previewUrl?: string) => void; previewAlt?: string; }
6
+ /**
7
+ * Un dépôt de fichier qui montre ce qu'on vient de choisir.
8
+ *
9
+ * L'aperçu est local, construit avant tout envoi : attendre le retour du
10
+ * serveur pour afficher l'image laisse un carré vide au moment précis où
11
+ * l'on veut vérifier qu'on n'a pas pris la mauvaise.
12
+ */
6
13
  export function ImageUpload({ accept = "image/*", value, onValueChange, previewAlt = "Aperçu de l'image", className, ...props }: ImageUploadProps) { const [preview, setPreview] = useState(value); useEffect(() => setPreview(value), [value]); useEffect(() => () => { if (preview?.startsWith("blob:")) URL.revokeObjectURL(preview); }, [preview]); const select = (files: File[]) => { const file = files[0]; if (!file) return; const next = URL.createObjectURL(file); setPreview(next); onValueChange?.(file, next); }; return <div className={cn("sia-image-upload", className)}>{preview && <div className="sia-image-upload__preview"><img src={preview} alt={previewAlt} /><button type="button" onClick={() => { setPreview(undefined); onValueChange?.(null); }}>Supprimer</button></div>}<FileUpload {...props} accept={accept} multiple={false} onFilesChange={select} /></div>; }
@@ -23,6 +23,13 @@ export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
23
23
  radius?: "default" | "none" | "sm" | "lg" | "full";
24
24
  }
25
25
 
26
+ /**
27
+ * La saisie d'une ligne de texte.
28
+ *
29
+ * `left` et `right` accueillent ce qui accompagne la valeur — une unité,
30
+ * une icône de recherche, un bouton de réinitialisation — à l'intérieur du
31
+ * cadre. Posés à côté, ils s'en désalignent dès que la taille change.
32
+ */
26
33
  export const Input = forwardRef<HTMLInputElement, InputProps>(
27
34
  (
28
35
  {
@@ -3,4 +3,11 @@ import { Textarea } from "../Textarea";
3
3
  import { cn } from "@sia-ui/utils";
4
4
  import "./styles.css";
5
5
  export interface JsonEditorProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "value" | "defaultValue" | "onChange"> { value?: string; defaultValue?: string; onValueChange?: (value: string, parsed: unknown | undefined) => void; formatOnBlur?: boolean; }
6
+ /**
7
+ * Une zone de saisie qui connaît la forme de ce qu'on y écrit.
8
+ *
9
+ * Elle signale une syntaxe invalide pendant la frappe et rend l'objet
10
+ * analysé à côté du texte : l'appelant reçoit donc les deux, et n'a pas à
11
+ * refaire un `JSON.parse` dans un `try`.
12
+ */
6
13
  export function JsonEditor({ value, defaultValue = "{}", onValueChange, formatOnBlur = true, className, ...props }: JsonEditorProps) { const [internal, setInternal] = useState(defaultValue); const current = value ?? internal; const result = useMemo(() => { try { return { valid: true, parsed: JSON.parse(current) as unknown }; } catch { return { valid: false, parsed: undefined }; } }, [current]); const update = (next: string) => { let parsed: unknown | undefined; try { parsed = JSON.parse(next) as unknown; } catch { parsed = undefined; } if (value === undefined) setInternal(next); onValueChange?.(next, parsed); }; return <div className={cn("sia-code-editor", className)}><Textarea {...props} value={current} invalid={!result.valid} resize="vertical" spellCheck={false} onChange={(event) => update(event.target.value)} onBlur={(event) => { if (formatOnBlur && result.valid) update(JSON.stringify(result.parsed, null, 2)); props.onBlur?.(event); }} /><small className={result.valid ? "is-valid" : "is-invalid"}>{result.valid ? "JSON valide" : "JSON invalide"}</small></div>; }
@@ -6,6 +6,13 @@ export interface KpiGridProps extends HTMLAttributes<HTMLDivElement> {
6
6
  columns?: 1 | 2 | 3 | 4;
7
7
  }
8
8
 
9
+ /**
10
+ * Une rangée d'indicateurs.
11
+ *
12
+ * Le nombre de colonnes est déclaré, pas déduit du nombre de cartes : trois
13
+ * indicateurs sur quatre colonnes laissent un trou, et ce trou dit qu'il
14
+ * manque une donnée.
15
+ */
9
16
  export function KpiGrid({ columns = 4, className, ...props }: KpiGridProps) {
10
17
  return <div className={cn("sia-kpi-grid", `sia-kpi-grid--${columns}`, className)} {...props} />;
11
18
  }
@@ -16,6 +16,13 @@ export interface LabelProps
16
16
  /** Alias historique, attendu par les composants venus du registre. */
17
17
  export type ILabelProps = LabelProps;
18
18
 
19
+ /**
20
+ * Le nom d'un champ.
21
+ *
22
+ * L'astérisque d'obligation est portée par une prop plutôt qu'écrite dans
23
+ * le texte : elle reçoit ainsi un `aria-hidden`, et le champ est annoncé
24
+ * « obligatoire » plutôt que « nom étoile ».
25
+ */
19
26
  export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
20
27
  ({ className, children, required, muted, tone, ...props }, ref) => {
21
28
  return (