@sia-ui/cli 0.7.1 → 0.8.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 (99) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +6 -0
  3. package/dist/registry/package/components/AccountMenu/index.tsx +107 -0
  4. package/dist/registry/package/components/AccountMenu/styles.css +30 -0
  5. package/dist/registry/package/components/ActivityLog/index.tsx +96 -25
  6. package/dist/registry/package/components/AmountDisplay/index.tsx +5 -2
  7. package/dist/registry/package/components/AppShell/index.tsx +44 -8
  8. package/dist/registry/package/components/AppShell/tab-bar.tsx +6 -3
  9. package/dist/registry/package/components/AsyncState/index.tsx +29 -7
  10. package/dist/registry/package/components/AuditMeta/index.tsx +12 -2
  11. package/dist/registry/package/components/Avatar/index.tsx +4 -9
  12. package/dist/registry/package/components/Badge/index.tsx +3 -2
  13. package/dist/registry/package/components/Badge/styles.css +4 -4
  14. package/dist/registry/package/components/BrandIcon/index.tsx +100 -0
  15. package/dist/registry/package/components/BrandIcon/styles.css +31 -0
  16. package/dist/registry/package/components/Breadcrumb/index.tsx +14 -2
  17. package/dist/registry/package/components/Button/index.tsx +3 -2
  18. package/dist/registry/package/components/Button/styles.css +4 -4
  19. package/dist/registry/package/components/Calendar/index.tsx +7 -3
  20. package/dist/registry/package/components/Card/index.tsx +14 -4
  21. package/dist/registry/package/components/ColorModeToggle/index.tsx +44 -0
  22. package/dist/registry/package/components/CommandPalette/index.tsx +3 -2
  23. package/dist/registry/package/components/ConfirmDialog/index.tsx +24 -4
  24. package/dist/registry/package/components/CrudPage/dialogs.tsx +53 -8
  25. package/dist/registry/package/components/CrudPage/index.tsx +99 -25
  26. package/dist/registry/package/components/CurrencyInput/index.tsx +5 -2
  27. package/dist/registry/package/components/DataTable/index.tsx +53 -18
  28. package/dist/registry/package/components/DataTable/row-actions.tsx +20 -8
  29. package/dist/registry/package/components/DataTable/types.ts +100 -0
  30. package/dist/registry/package/components/DatePicker/index.tsx +14 -6
  31. package/dist/registry/package/components/DateRangeFilter/index.tsx +12 -3
  32. package/dist/registry/package/components/DateRangePicker/index.tsx +13 -4
  33. package/dist/registry/package/components/DateTimePicker/index.tsx +3 -1
  34. package/dist/registry/package/components/Drawer/index.tsx +16 -6
  35. package/dist/registry/package/components/DropdownMenu/index.tsx +37 -4
  36. package/dist/registry/package/components/DropdownMenu/styles.css +10 -0
  37. package/dist/registry/package/components/DurationDisplay/index.tsx +20 -2
  38. package/dist/registry/package/components/EntityMeta/index.tsx +14 -9
  39. package/dist/registry/package/components/ErrorState/index.tsx +9 -3
  40. package/dist/registry/package/components/ErrorState/styles.css +1 -1
  41. package/dist/registry/package/components/EventCalendar/index.tsx +657 -9
  42. package/dist/registry/package/components/EventCalendar/styles.css +360 -8
  43. package/dist/registry/package/components/EventManager/index.tsx +456 -0
  44. package/dist/registry/package/components/EventManager/styles.css +12 -0
  45. package/dist/registry/package/components/Field/index.tsx +74 -27
  46. package/dist/registry/package/components/FileUpload/index.tsx +13 -3
  47. package/dist/registry/package/components/FiltersBar/index.tsx +8 -3
  48. package/dist/registry/package/components/FiltersBar/styles.css +7 -0
  49. package/dist/registry/package/components/Form/index.tsx +118 -14
  50. package/dist/registry/package/components/Form/styles.css +3 -0
  51. package/dist/registry/package/components/HoverCard/index.tsx +8 -4
  52. package/dist/registry/package/components/Icons/index.tsx +18 -0
  53. package/dist/registry/package/components/ImageUpload/index.tsx +17 -2
  54. package/dist/registry/package/components/Input/index.tsx +3 -2
  55. package/dist/registry/package/components/JobProgress/index.tsx +289 -0
  56. package/dist/registry/package/components/JobProgress/styles.css +58 -0
  57. package/dist/registry/package/components/JsonEditor/index.tsx +18 -3
  58. package/dist/registry/package/components/JsonView/index.tsx +282 -0
  59. package/dist/registry/package/components/JsonView/styles.css +99 -0
  60. package/dist/registry/package/components/LiveIndicator/index.tsx +43 -0
  61. package/dist/registry/package/components/LiveIndicator/styles.css +41 -0
  62. package/dist/registry/package/components/LogStream/index.tsx +545 -0
  63. package/dist/registry/package/components/LogStream/styles.css +152 -0
  64. package/dist/registry/package/components/MarkdownEditor/index.tsx +14 -3
  65. package/dist/registry/package/components/Modal/index.tsx +7 -1
  66. package/dist/registry/package/components/MultiSelect/index.tsx +14 -3
  67. package/dist/registry/package/components/OtpInput/index.tsx +2 -1
  68. package/dist/registry/package/components/Pagination/index.tsx +13 -2
  69. package/dist/registry/package/components/PasswordInput/index.tsx +23 -9
  70. package/dist/registry/package/components/PhoneInput/index.tsx +66 -30
  71. package/dist/registry/package/components/Popover/index.tsx +19 -4
  72. package/dist/registry/package/components/QrCode/index.tsx +3 -2
  73. package/dist/registry/package/components/RadioGroup/styles.css +3 -0
  74. package/dist/registry/package/components/Rating/index.tsx +6 -2
  75. package/dist/registry/package/components/ReferenceSelect/index.tsx +8 -1
  76. package/dist/registry/package/components/RelativeTime/index.tsx +5 -1
  77. package/dist/registry/package/components/RequireSession/index.tsx +10 -4
  78. package/dist/registry/package/components/Resource/index.tsx +70 -17
  79. package/dist/registry/package/components/RichTextEditor/index.tsx +2 -1
  80. package/dist/registry/package/components/SearchInput/index.tsx +4 -1
  81. package/dist/registry/package/components/SecretFields/index.tsx +70 -17
  82. package/dist/registry/package/components/Select/index.tsx +34 -12
  83. package/dist/registry/package/components/Sidebar/index.tsx +22 -6
  84. package/dist/registry/package/components/Slider/index.tsx +26 -5
  85. package/dist/registry/package/components/Spinner/index.tsx +3 -2
  86. package/dist/registry/package/components/StatCard/index.tsx +11 -3
  87. package/dist/registry/package/components/Statistic/index.tsx +5 -1
  88. package/dist/registry/package/components/Steps/styles.css +2 -2
  89. package/dist/registry/package/components/TagsInput/index.tsx +2 -1
  90. package/dist/registry/package/components/TimePicker/index.tsx +10 -4
  91. package/dist/registry/package/components/Toast/index.tsx +20 -5
  92. package/dist/registry/package/components/Tour/index.tsx +2 -1
  93. package/dist/registry/package/components/Transfer/index.tsx +2 -1
  94. package/dist/registry/package/components/Tree/index.tsx +3 -2
  95. package/dist/registry/package/components/TreeSelect/index.tsx +17 -4
  96. package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +34 -3
  97. package/dist/registry/registry.json +257 -75
  98. package/dist/registry/styles/sia-ui.css +23 -4
  99. package/package.json +5 -5
@@ -1,13 +1,14 @@
1
1
  import { useMemo, useState, type Key, type ReactNode } from "react";
2
2
  import { cn } from "@sia-ui/utils";
3
- import type {
4
- FormSubmitResult,
5
- FormValidator,
6
- PermissionRule,
3
+ import {
4
+ useSiaLocale,
5
+ type FormSubmitResult,
6
+ type FormValidator,
7
+ type PermissionRule,
7
8
  } from "@sia-ui/headless";
8
- import { PageHeader } from "../PageHeader";
9
- import { Button } from "../Button";
10
- import { Pagination } from "../Pagination";
9
+ import { PageHeader, type PageHeaderProps } from "../PageHeader";
10
+ import { Button, type ButtonProps } from "../Button";
11
+ import { Pagination, type PaginationProps } from "../Pagination";
11
12
  import { DataTable } from "../DataTable";
12
13
  import { EyeIcon, PencilIcon, PlusIcon, TrashIcon } from "../Icons";
13
14
  import type {
@@ -29,15 +30,21 @@ import {
29
30
  } from "../Resource";
30
31
  import {
31
32
  CrudDialogs,
33
+ type CrudDescriptionsProps,
32
34
  type CrudDialogOptions,
33
35
  type CrudDialogState,
36
+ type CrudFormProps,
37
+ type CrudModalProps,
34
38
  } from "./dialogs";
35
39
  import "./styles.css";
36
40
 
37
41
  export {
38
42
  CrudDialogs,
43
+ type CrudDescriptionsProps,
39
44
  type CrudDialogOptions,
40
45
  type CrudDialogState,
46
+ type CrudFormProps,
47
+ type CrudModalProps,
41
48
  } from "./dialogs";
42
49
 
43
50
  /**
@@ -217,6 +224,40 @@ interface CrudPageBase<T> {
217
224
  * ni ressource ne montrerait rien, et le dirait à l'exécution.
218
225
  */
219
226
  export type CrudPageProps<T> = CrudPageBase<T> &
227
+ /*
228
+ Les props des composants que la page assemble, transmises telles quelles.
229
+ Chacune omet ce que la page calcule elle-même, et qu'une surcharge
230
+ casserait — une pagination qui ne suit plus la page, une boîte qui ne se
231
+ ferme plus.
232
+ */
233
+ {
234
+ /**
235
+ * Les props du `PageHeader`. Sans `actions`, composées de
236
+ * `headerActions` et du bouton de création. `className` s'ajoute.
237
+ */
238
+ pageHeaderProps?: Partial<Omit<PageHeaderProps, "actions">>;
239
+ /**
240
+ * Les props du bouton de création. Sans `onClick`, qui ouvre le
241
+ * formulaire, ni `children` : le libellé passe par
242
+ * `operations.create.label`.
243
+ */
244
+ createButtonProps?: Partial<Omit<ButtonProps, "onClick" | "children">>;
245
+ /** Les props du bouton de suppression groupée. Sans `onClick`. */
246
+ bulkDeleteButtonProps?: Partial<Omit<ButtonProps, "onClick">>;
247
+ /**
248
+ * Les props de la `Pagination` — `jumpTo`, `compact`, `siblingCount`.
249
+ * Sans `page`, `totalPages` ni `onPageChange`, qui ont leurs props ici.
250
+ */
251
+ paginationProps?: Partial<
252
+ Omit<PaginationProps, "page" | "totalPages" | "onPageChange">
253
+ >;
254
+ /** Les props des `Modal` de formulaire et de détail — voir `CrudModalProps`. */
255
+ modalProps?: CrudModalProps;
256
+ /** Les props du `Form` des boîtes — voir `CrudFormProps`. */
257
+ formProps?: CrudFormProps;
258
+ /** Les props de la vue de détail par défaut — voir `CrudDescriptionsProps`. */
259
+ descriptionsProps?: CrudDescriptionsProps;
260
+ } &
220
261
  (
221
262
  | {
222
263
  /**
@@ -235,6 +276,24 @@ export type CrudPageProps<T> = CrudPageBase<T> &
235
276
  }
236
277
  );
237
278
 
279
+ /**
280
+ * Glisse un nom — texte ou élément — à la place de `{name}` dans un message
281
+ * de la locale. `formatMessage` ne sait placer que du texte ; le nom d'une
282
+ * ressource peut être un élément.
283
+ */
284
+ function avecNom(modele: string, nom: ReactNode): ReactNode {
285
+ const [avant, ...apres] = modele.split("{name}");
286
+ return apres.length === 0 ? (
287
+ modele
288
+ ) : (
289
+ <>
290
+ {avant}
291
+ {nom}
292
+ {apres.join("{name}")}
293
+ </>
294
+ );
295
+ }
296
+
238
297
  /**
239
298
  * Une page de liste, avec ses quatre opérations.
240
299
  *
@@ -288,7 +347,15 @@ export function CrudPage<T>({
288
347
  table,
289
348
  children,
290
349
  className,
350
+ pageHeaderProps,
351
+ createButtonProps,
352
+ bulkDeleteButtonProps,
353
+ paginationProps,
354
+ modalProps,
355
+ formProps,
356
+ descriptionsProps,
291
357
  }: CrudPageProps<T>) {
358
+ const locale = useSiaLocale();
292
359
  const [boite, setBoite] = useState<CrudDialogState<T> | null>(null);
293
360
 
294
361
  /**
@@ -306,9 +373,9 @@ export function CrudPage<T>({
306
373
  columns: resourceColumns(resource),
307
374
  detailColumns: resourceDetailColumns(resource),
308
375
  fields: resourceFormFields(resource, "create"),
309
- validate: resourceValidator(resource, "create"),
376
+ validate: resourceValidator(resource, "create", locale.resource),
310
377
  editFields: resourceFormFields(resource, "edit"),
311
- editValidate: resourceValidator(resource, "edit"),
378
+ editValidate: resourceValidator(resource, "edit", locale.resource),
312
379
  searchKeys: resourceSearchKeys(resource),
313
380
  defaults: resourceDefaults(resource),
314
381
  getRowKey: resourceRowKey(resource),
@@ -317,7 +384,7 @@ export function CrudPage<T>({
317
384
  label: resource.label,
318
385
  }
319
386
  : undefined,
320
- [resource],
387
+ [resource, locale.resource],
321
388
  );
322
389
 
323
390
  const columns = colonnesFournies ?? derive?.columns ?? [];
@@ -406,24 +473,24 @@ export function CrudPage<T>({
406
473
  });
407
474
  };
408
475
 
409
- ajouter("view", consulter, { label: "Consulter", icon: <EyeIcon /> });
410
- ajouter("edit", modifier, { label: "Modifier", icon: <PencilIcon /> });
476
+ ajouter("view", consulter, { label: locale.crudPage.view, icon: <EyeIcon /> });
477
+ ajouter("edit", modifier, { label: locale.edit, icon: <PencilIcon /> });
411
478
  ajouter("delete", onDelete, {
412
- label: "Supprimer",
479
+ label: locale.delete,
413
480
  icon: <TrashIcon />,
414
481
  tone: "danger",
415
482
  // Détruire sans demander est une faute. On peut la retirer — encore
416
483
  // faut-il l'avoir écrit.
417
484
  confirm: {
418
- title: "Supprimer cet élément ?",
419
- description: "Cette action est définitive.",
420
- confirmLabel: "Supprimer",
485
+ title: locale.crudPage.deleteTitle,
486
+ description: locale.crudPage.deleteDescription,
487
+ confirmLabel: locale.delete,
421
488
  destructive: true,
422
489
  },
423
490
  });
424
491
 
425
492
  return liste;
426
- }, [operations, onDelete, modifier, consulter, derive]);
493
+ }, [operations, onDelete, modifier, consulter, derive, locale]);
427
494
 
428
495
  const toutesLesActions = useMemo(() => {
429
496
  if (!extraRowActions) {
@@ -459,11 +526,12 @@ export function CrudPage<T>({
459
526
  const boutonCreer =
460
527
  creer && creation && creationAutorisee ? (
461
528
  <Button
462
- onClick={creer}
463
529
  disabled={creation.disabled ?? false}
464
530
  leftIcon={creation.icon ?? <PlusIcon />}
531
+ {...createButtonProps}
532
+ onClick={creer}
465
533
  >
466
- {creation.label ?? derive?.label?.create ?? "Créer"}
534
+ {creation.label ?? derive?.label?.create ?? locale.create}
467
535
  </Button>
468
536
  ) : null;
469
537
  const enTeteActions =
@@ -483,6 +551,7 @@ export function CrudPage<T>({
483
551
  : {})}
484
552
  {...(eyebrow ? { eyebrow } : {})}
485
553
  {...(headingLevel ? { level: headingLevel } : {})}
554
+ {...pageHeaderProps}
486
555
  {...(enTeteActions ? { actions: enTeteActions } : {})}
487
556
  />
488
557
 
@@ -511,12 +580,13 @@ export function CrudPage<T>({
511
580
  variant="outline"
512
581
  tone="danger"
513
582
  leftIcon={<TrashIcon />}
583
+ {...bulkDeleteButtonProps}
514
584
  onClick={async () => {
515
585
  await onBulkDelete(keys, rows);
516
586
  clear();
517
587
  }}
518
588
  >
519
- Supprimer
589
+ {bulkDeleteButtonProps?.children ?? locale.delete}
520
590
  </Button>
521
591
  ),
522
592
  }
@@ -547,6 +617,9 @@ export function CrudPage<T>({
547
617
  toFormValues={toFormValues}
548
618
  renderDetail={typeof detail === "function" ? detail : undefined}
549
619
  getRowKey={getRowKey}
620
+ modalProps={modalProps}
621
+ formProps={formProps}
622
+ descriptionsProps={descriptionsProps}
550
623
  /*
551
624
  Les titres des boîtes reprennent le nom de la ressource, faute de
552
625
  mieux : « Modifier la facture » se lit mieux que « Modifier », et
@@ -556,8 +629,8 @@ export function CrudPage<T>({
556
629
  dialog={{
557
630
  ...(derive?.label?.singular
558
631
  ? {
559
- createTitle: derive.label.create ?? "Créer",
560
- editTitle: <>Modifier — {derive.label.singular}</>,
632
+ createTitle: derive.label.create ?? locale.create,
633
+ editTitle: avecNom(locale.crudPage.editTitle, derive.label.singular),
561
634
  viewTitle: derive.label.singular,
562
635
  }
563
636
  : {}),
@@ -568,13 +641,14 @@ export function CrudPage<T>({
568
641
 
569
642
  {page !== undefined && totalPages !== undefined && onPageChange && (
570
643
  <Pagination
571
- page={page}
572
- totalPages={totalPages}
573
- onPageChange={onPageChange}
574
644
  showTotal
575
645
  {...(pageSize !== undefined ? { pageSize } : {})}
576
646
  {...(onPageSizeChange ? { onPageSizeChange } : {})}
577
647
  {...(pageSizeOptions ? { pageSizeOptions } : {})}
648
+ {...paginationProps}
649
+ page={page}
650
+ totalPages={totalPages}
651
+ onPageChange={onPageChange}
578
652
  />
579
653
  )}
580
654
  </section>
@@ -1,8 +1,9 @@
1
1
  import { forwardRef, useState, type InputHTMLAttributes } from "react";
2
2
  import { cn, formatCurrency, parseCurrency } from "@sia-ui/utils";
3
+ import { useSiaLocale } from "@sia-ui/headless";
3
4
  import "./styles.css";
4
5
 
5
- export interface CurrencyInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "defaultValue" | "onChange" | "inputMode"> { value?: number | null; defaultValue?: number | null; currency?: string; locale?: string; onValueChange?: (value: number | null) => void; invalid?: boolean; }
6
+ export interface CurrencyInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "defaultValue" | "onChange" | "inputMode"> { value?: number | null; defaultValue?: number | null; currency?: string; /** La langue du format et de la saisie. Par défaut, celle de la locale SIA. */ locale?: string; onValueChange?: (value: number | null) => void; invalid?: boolean; }
6
7
 
7
8
  /**
8
9
  * La saisie d'un montant.
@@ -11,7 +12,9 @@ export interface CurrencyInputProps extends Omit<InputHTMLAttributes<HTMLInputEl
11
12
  * de milliers et le symbole appartiennent à l'affichage, et un serveur qui
12
13
  * reçoit « 1 250 000 F CFA » ne peut rien en faire.
13
14
  */
14
- export const CurrencyInput = forwardRef<HTMLInputElement, CurrencyInputProps>(({ value, defaultValue = null, currency = "XAF", locale = "fr-FR", onValueChange, invalid, className, onBlur, onFocus, ...props }, ref) => {
15
+ export const CurrencyInput = forwardRef<HTMLInputElement, CurrencyInputProps>(({ value, defaultValue = null, currency = "XAF", locale: localeProp, onValueChange, invalid, className, onBlur, onFocus, ...props }, ref) => {
16
+ const messages = useSiaLocale();
17
+ const locale = localeProp ?? messages.language;
15
18
  const [focused, setFocused] = useState(false);
16
19
  const [internal, setInternal] = useState<number | null>(defaultValue);
17
20
  const current = value === undefined ? internal : value;
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useMemo, useState, type Key, type ReactNode } from "react";
2
2
  import { cn } from "@sia-ui/utils";
3
- import { useSiaLocale } from "@sia-ui/headless";
3
+ import { plural, useSiaLocale } from "@sia-ui/headless";
4
4
  import { useMediaQuery } from "@sia-ui/react";
5
5
  import { Checkbox } from "../Checkbox";
6
6
  import { SearchInput } from "../SearchInput";
@@ -14,6 +14,7 @@ import type {
14
14
  DataTableProps,
15
15
  DataTableView,
16
16
  RowAction,
17
+ RowActionsSlots,
17
18
  } from "./types";
18
19
  import { renduCellule, valeurDe } from "./values";
19
20
  import "./styles.css";
@@ -57,6 +58,7 @@ export function DataTable<T>({
57
58
  searchPlaceholder,
58
59
  onSearch,
59
60
  searchDelay = 0,
61
+ searchInputProps,
60
62
  layout = "auto",
61
63
  maxHeight,
62
64
  stickyHeader = false,
@@ -67,6 +69,10 @@ export function DataTable<T>({
67
69
  inlineActionsLimit = 2,
68
70
  actionsHeader,
69
71
  actionsDisplay = "icon",
72
+ rowActionButtonProps,
73
+ rowActionIconButtonProps,
74
+ rowActionsMenuProps,
75
+ confirmDialogProps,
70
76
  can,
71
77
  view: viewProp,
72
78
  defaultView = "table",
@@ -77,14 +83,18 @@ export function DataTable<T>({
77
83
  renderCard,
78
84
  loading = false,
79
85
  skeletonRows = 5,
86
+ skeletonProps,
80
87
  error,
81
88
  onRetry,
89
+ errorStateProps,
82
90
  empty,
91
+ emptyStateProps,
83
92
  selectable = false,
84
93
  selectedKeys: selectedProp,
85
94
  defaultSelectedKeys,
86
95
  onSelectionChange,
87
96
  isRowSelectable,
97
+ checkboxProps,
88
98
  selectionActions,
89
99
  sort,
90
100
  onSortChange,
@@ -181,6 +191,12 @@ export function DataTable<T>({
181
191
  typeof rowActions === "function" ? rowActions(row, index) : (rowActions ?? []);
182
192
 
183
193
  const aDesActions = rowActions !== undefined;
194
+ const slotsActions: RowActionsSlots = {
195
+ button: rowActionButtonProps,
196
+ iconButton: rowActionIconButtonProps,
197
+ menu: rowActionsMenuProps,
198
+ confirmDialog: confirmDialogProps,
199
+ };
184
200
  const vide = !loading && !error && lignes.length === 0;
185
201
 
186
202
  const enTete =
@@ -211,8 +227,9 @@ export function DataTable<T>({
211
227
  <div className="sia-table__tools">
212
228
  {searchKeys && (
213
229
  <SearchInput
214
- value={recherche}
215
230
  placeholder={searchPlaceholder ?? locale.search}
231
+ {...searchInputProps}
232
+ value={recherche}
216
233
  // Avec un délai, c'est le champ qui temporise : il sait aussi
217
234
  // déclencher tout de suite sur Entrée et à l'effacement.
218
235
  {...(onSearch && searchDelay > 0 ? { onSearch, searchDelay } : {})}
@@ -225,7 +242,7 @@ export function DataTable<T>({
225
242
  {toolbar}
226
243
 
227
244
  {showViewToggle && !(responsive && etroit) && (
228
- <div className="sia-table__views" role="group" aria-label="Affichage">
245
+ <div className="sia-table__views" role="group" aria-label={locale.dataTable.viewToggle}>
229
246
  {(["table", "cards"] as const).map((mode) => (
230
247
  <button
231
248
  key={mode}
@@ -239,7 +256,7 @@ export function DataTable<T>({
239
256
  >
240
257
  {mode === "table" ? <IconeListe /> : <IconeGrille />}
241
258
  <span className="sia-visually-hidden">
242
- {mode === "table" ? "Liste" : "Cartes"}
259
+ {mode === "table" ? locale.dataTable.tableView : locale.dataTable.cardsView}
243
260
  </span>
244
261
  </button>
245
262
  ))}
@@ -256,7 +273,7 @@ export function DataTable<T>({
256
273
  {selectable && selection.length > 0 && selectionActions && (
257
274
  <div className="sia-table__selection">
258
275
  <span className="sia-table__selection-count">
259
- {selection.length} sélectionné{selection.length > 1 ? "s" : ""}
276
+ {plural(locale.dataTable, "selected", selection.length, locale.language)}
260
277
  </span>
261
278
  {selectionActions({
262
279
  keys: selection,
@@ -267,12 +284,20 @@ export function DataTable<T>({
267
284
  )}
268
285
 
269
286
  {error ? (
270
- <ErrorState description={error} {...(onRetry ? { onRetry } : {})} />
287
+ <ErrorState
288
+ {...errorStateProps}
289
+ description={error}
290
+ {...(onRetry ? { onRetry } : {})}
291
+ />
271
292
  ) : vide ? (
272
293
  <EmptyState
273
- title={empty?.title ?? locale.empty}
294
+ {...emptyStateProps}
295
+ // `empty` est la voie courte : là où il déclare une clé, il
296
+ // l'emporte sur le réglage complet.
297
+ title={empty?.title ?? emptyStateProps?.title ?? locale.empty}
274
298
  {...(empty?.description ? { description: empty.description } : {})}
275
299
  {...(empty?.action ? { action: empty.action } : {})}
300
+ {...(empty?.icon ? { icon: empty.icon } : {})}
276
301
  />
277
302
  ) : vue === "cards" ? (
278
303
  <CartesVue
@@ -280,6 +305,8 @@ export function DataTable<T>({
280
305
  colonnes={colonnes}
281
306
  loading={loading}
282
307
  skeletonRows={skeletonRows}
308
+ skeletonProps={skeletonProps}
309
+ slotsActions={slotsActions}
283
310
  cle={cle}
284
311
  {...(renderCard ? { renderCard } : {})}
285
312
  {...(onRowClick ? { onRowClick } : {})}
@@ -307,11 +334,12 @@ export function DataTable<T>({
307
334
  {selectable && (
308
335
  <th className="sia-table__cell--select" scope="col">
309
336
  <Checkbox
337
+ label=""
338
+ aria-label={locale.dataTable.selectAll}
339
+ {...checkboxProps}
310
340
  checked={toutesCochees}
311
341
  indeterminate={partiellement}
312
342
  onValueChange={basculerTout}
313
- label=""
314
- aria-label="Tout sélectionner"
315
343
  />
316
344
  </th>
317
345
  )}
@@ -361,7 +389,7 @@ export function DataTable<T>({
361
389
  {aDesActions && (
362
390
  <th className="sia-table__cell--actions" scope="col">
363
391
  {actionsHeader ?? (
364
- <span className="sia-visually-hidden">Actions</span>
392
+ <span className="sia-visually-hidden">{locale.dataTable.actions}</span>
365
393
  )}
366
394
  </th>
367
395
  )}
@@ -374,17 +402,17 @@ export function DataTable<T>({
374
402
  <tr key={`fantome-${i}`} className="sia-table__row--skeleton">
375
403
  {selectable && (
376
404
  <td>
377
- <Skeleton width="1.1rem" height="1.1rem" />
405
+ <Skeleton width="1.1rem" height="1.1rem" {...skeletonProps} />
378
406
  </td>
379
407
  )}
380
408
  {colonnes.map((c) => (
381
409
  <td key={c.key}>
382
- <Skeleton height=".8rem" />
410
+ <Skeleton height=".8rem" {...skeletonProps} />
383
411
  </td>
384
412
  ))}
385
413
  {aDesActions && (
386
414
  <td>
387
- <Skeleton width="3rem" height=".8rem" />
415
+ <Skeleton width="3rem" height=".8rem" {...skeletonProps} />
388
416
  </td>
389
417
  )}
390
418
  </tr>
@@ -411,11 +439,12 @@ export function DataTable<T>({
411
439
  {selectable && (
412
440
  <td className="sia-table__cell--select">
413
441
  <Checkbox
442
+ label=""
443
+ aria-label={locale.dataTable.selectRow}
444
+ {...checkboxProps}
414
445
  checked={cochee}
415
446
  disabled={!cochable}
416
447
  onValueChange={() => basculerLigne(k)}
417
- label=""
418
- aria-label="Sélectionner la ligne"
419
448
  onClick={(e) => e.stopPropagation()}
420
449
  />
421
450
  </td>
@@ -443,6 +472,7 @@ export function DataTable<T>({
443
472
  inlineLimit={inlineActionsLimit}
444
473
  display={actionsDisplay}
445
474
  can={can}
475
+ slots={slotsActions}
446
476
  />
447
477
  </td>
448
478
  )}
@@ -462,6 +492,8 @@ interface CartesVueProps<T> {
462
492
  colonnes: Array<DataTableColumn<T>>;
463
493
  loading: boolean;
464
494
  skeletonRows: number;
495
+ skeletonProps: DataTableProps<T>["skeletonProps"];
496
+ slotsActions: RowActionsSlots;
465
497
  cle: (row: T, index: number) => Key;
466
498
  renderCard?: (row: T, index: number) => ReactNode;
467
499
  onRowClick?: (row: T, index: number) => void;
@@ -484,6 +516,8 @@ function CartesVue<T>({
484
516
  colonnes,
485
517
  loading,
486
518
  skeletonRows,
519
+ skeletonProps,
520
+ slotsActions,
487
521
  cle,
488
522
  renderCard,
489
523
  onRowClick,
@@ -505,9 +539,9 @@ function CartesVue<T>({
505
539
  <div className="sia-table__cards">
506
540
  {Array.from({ length: skeletonRows }, (_, i) => (
507
541
  <div key={i} className="sia-table-card sia-table-card--skeleton">
508
- <Skeleton height="1rem" width="60%" />
509
- <Skeleton height=".75rem" />
510
- <Skeleton height=".75rem" width="80%" />
542
+ <Skeleton height="1rem" width="60%" {...skeletonProps} />
543
+ <Skeleton height=".75rem" {...skeletonProps} />
544
+ <Skeleton height=".75rem" width="80%" {...skeletonProps} />
511
545
  </div>
512
546
  ))}
513
547
  </div>
@@ -556,6 +590,7 @@ function CartesVue<T>({
556
590
  inlineLimit={inlineActionsLimit}
557
591
  display={actionsDisplay}
558
592
  can={can}
593
+ slots={slotsActions}
559
594
  />
560
595
  </footer>
561
596
  )}
@@ -1,12 +1,12 @@
1
1
  import { useState, type ReactNode } from "react";
2
2
  import { cn } from "@sia-ui/utils";
3
- import type { PermissionRule } from "@sia-ui/headless";
3
+ import { useSiaLocale, type PermissionRule } from "@sia-ui/headless";
4
4
  import { Button } from "../Button";
5
5
  import { IconButton } from "../IconButton";
6
6
  import { DropdownMenu } from "../DropdownMenu";
7
7
  import { ConfirmDialog } from "../ConfirmDialog";
8
8
  import { MoreHorizontalIcon } from "../Icons";
9
- import type { RowAction } from "./types";
9
+ import type { RowAction, RowActionsSlots } from "./types";
10
10
 
11
11
  interface RowActionsProps<T> {
12
12
  row: T;
@@ -15,6 +15,7 @@ interface RowActionsProps<T> {
15
15
  inlineLimit: number;
16
16
  display: "icon" | "label";
17
17
  can: ((rule: PermissionRule) => boolean) | undefined;
18
+ slots: RowActionsSlots;
18
19
  }
19
20
 
20
21
  /**
@@ -31,7 +32,9 @@ export function RowActions<T>({
31
32
  inlineLimit,
32
33
  display,
33
34
  can,
35
+ slots,
34
36
  }: RowActionsProps<T>) {
37
+ const locale = useSiaLocale();
35
38
  // Une confirmation en attente. Une seule à la fois : deux dialogues
36
39
  // superposés sur la même ligne n'ont aucun sens.
37
40
  const [enAttente, setEnAttente] = useState<RowAction<T> | null>(null);
@@ -68,12 +71,15 @@ export function RowActions<T>({
68
71
  return display === "icon" && action.icon ? (
69
72
  <IconButton
70
73
  key={action.key}
71
- label={nom}
72
- icon={action.icon}
73
74
  variant="ghost"
74
75
  size="sm"
76
+ {...slots.iconButton}
77
+ // L'infobulle réglée pour tout le tableau garde le libellé de
78
+ // l'action : sans lui, elle n'aurait plus rien à dire.
79
+ tooltip={{ ...slots.iconButton?.tooltip, content: action.label }}
80
+ label={nom}
81
+ icon={action.icon}
75
82
  disabled={desactivee}
76
- tooltip={{ content: action.label }}
77
83
  onClick={() => declencher(action)}
78
84
  />
79
85
  ) : (
@@ -81,9 +87,12 @@ export function RowActions<T>({
81
87
  key={action.key}
82
88
  variant="ghost"
83
89
  size="sm"
90
+ {...slots.button}
91
+ // Le ton et l'icône déclarés sur l'action sont plus précis que le
92
+ // réglage commun à tout le tableau.
84
93
  {...(action.tone ? { tone: action.tone } : {})}
85
- disabled={desactivee}
86
94
  {...(action.icon ? { leftIcon: action.icon } : {})}
95
+ disabled={desactivee}
87
96
  onClick={() => declencher(action)}
88
97
  >
89
98
  {action.label}
@@ -94,6 +103,7 @@ export function RowActions<T>({
94
103
  {dansLeMenu.length > 0 && (
95
104
  <DropdownMenu
96
105
  placement="bottom-end"
106
+ {...slots.menu}
97
107
  items={dansLeMenu.map((action) => ({
98
108
  key: action.key,
99
109
  label: action.label,
@@ -104,16 +114,18 @@ export function RowActions<T>({
104
114
  }))}
105
115
  >
106
116
  <IconButton
107
- label="Autres actions"
108
- icon={<MoreHorizontalIcon />}
109
117
  variant="ghost"
110
118
  size="sm"
119
+ {...slots.iconButton}
120
+ label={locale.dataTable.moreActions}
121
+ icon={<MoreHorizontalIcon />}
111
122
  />
112
123
  </DropdownMenu>
113
124
  )}
114
125
 
115
126
  {enAttente?.confirm && (
116
127
  <ConfirmDialog
128
+ {...slots.confirmDialog}
117
129
  open
118
130
  onOpenChange={(ouvert) => {
119
131
  if (!ouvert) setEnAttente(null);