@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @sia-ui/cli
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Version alignée sur les autres paquets SIA UI : tous sortent désormais ensemble, au même numéro.
8
+
3
9
  ## 0.7.1
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -68,6 +68,12 @@ Le contenu ci-dessous reprend intégralement `CHANGELOG.md` pour rester visible
68
68
  <!-- sia:changelog:start -->
69
69
  # @sia-ui/cli
70
70
 
71
+ ## 0.8.0
72
+
73
+ ### Minor Changes
74
+
75
+ - Version alignée sur les autres paquets SIA UI : tous sortent désormais ensemble, au même numéro.
76
+
71
77
  ## 0.7.1
72
78
 
73
79
  ### Patch Changes
@@ -0,0 +1,107 @@
1
+ import { type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import { formatMessage, useSiaLocale } from "@sia-ui/headless";
4
+ import { Avatar, type AvatarProps } from "../Avatar";
5
+ import { Badge, type BadgeProps } from "../Badge";
6
+ import {
7
+ DropdownMenu,
8
+ type DropdownMenuProps,
9
+ type MenuEntry,
10
+ type MenuItem,
11
+ } from "../DropdownMenu";
12
+ import "./styles.css";
13
+
14
+ export interface AccountMenuProps {
15
+ /** Le nom de la personne connectée : avatar, en-tête et libellé du bouton. */
16
+ name: string;
17
+ email?: ReactNode;
18
+ /** Le rôle, en pastille sous le nom : « Administrateur ». */
19
+ role?: ReactNode;
20
+ /** La photo. À défaut, les initiales de `name`. */
21
+ avatarSrc?: string;
22
+ /** Les entrées sous l'en-tête : profil, préférences, déconnexion. */
23
+ items: MenuEntry[];
24
+ onSelect?: (item: MenuItem) => void;
25
+ /** Le nom accessible du déclencheur. Par défaut, celui de la locale, `accountMenu.trigger` (« Compte de {name} »). */
26
+ triggerLabel?: string;
27
+ /** L'avatar du déclencheur — taille, forme, variante. */
28
+ avatarProps?: Partial<Omit<AvatarProps, "name">>;
29
+ /** L'avatar de l'en-tête du menu. */
30
+ headerAvatarProps?: Partial<Omit<AvatarProps, "name">>;
31
+ /** La pastille du rôle — ton, variante. */
32
+ roleBadgeProps?: Partial<Omit<BadgeProps, "children">>;
33
+ /** Le menu lui-même, hors entrées et déclencheur qu'il reçoit d'ici. */
34
+ dropdownMenuProps?: Partial<Omit<DropdownMenuProps, "items" | "children" | "onSelect">>;
35
+ className?: string;
36
+ }
37
+
38
+ /**
39
+ * Le menu du compte connecté : un avatar qui ouvre un menu à en-tête de profil.
40
+ *
41
+ * Chaque application le recomposait — avatar dans un bouton qui lui
42
+ * ajoutait un cadre, en-tête forcé dans un titre de section en capitales.
43
+ * Ici, le déclencheur est l'avatar seul, rond et sans fond, et l'en-tête une
44
+ * entrée `header` à contenu libre du `DropdownMenu`.
45
+ */
46
+ export function AccountMenu({
47
+ name,
48
+ email,
49
+ role,
50
+ avatarSrc,
51
+ items,
52
+ onSelect,
53
+ triggerLabel,
54
+ avatarProps,
55
+ headerAvatarProps,
56
+ roleBadgeProps,
57
+ dropdownMenuProps,
58
+ className,
59
+ }: AccountMenuProps) {
60
+ const locale = useSiaLocale();
61
+ const entete: MenuEntry = {
62
+ key: "__sia-account-header",
63
+ type: "header",
64
+ content: (
65
+ <div className="sia-account__who">
66
+ <Avatar
67
+ name={name}
68
+ size="md"
69
+ {...(avatarSrc ? { src: avatarSrc } : {})}
70
+ {...headerAvatarProps}
71
+ />
72
+ <div className="sia-account__identity">
73
+ <strong className="sia-account__name">{name}</strong>
74
+ {email && <span className="sia-account__email">{email}</span>}
75
+ {role && (
76
+ <Badge tone="primary" {...roleBadgeProps} className={cn("sia-account__role", roleBadgeProps?.className)}>
77
+ {role}
78
+ </Badge>
79
+ )}
80
+ </div>
81
+ </div>
82
+ ),
83
+ };
84
+
85
+ return (
86
+ <DropdownMenu
87
+ placement="bottom-end"
88
+ {...dropdownMenuProps}
89
+ items={[entete, ...items]}
90
+ {...(onSelect ? { onSelect } : {})}
91
+ className={cn("sia-account__menu", dropdownMenuProps?.className)}
92
+ >
93
+ <button
94
+ type="button"
95
+ className={cn("sia-account__trigger", className)}
96
+ aria-label={triggerLabel ?? formatMessage(locale.accountMenu.trigger, { name })}
97
+ >
98
+ <Avatar
99
+ name={name}
100
+ size="sm"
101
+ {...(avatarSrc ? { src: avatarSrc } : {})}
102
+ {...avatarProps}
103
+ />
104
+ </button>
105
+ </DropdownMenu>
106
+ );
107
+ }
@@ -0,0 +1,30 @@
1
+ /* Le déclencheur est l'avatar, rien d'autre : pas de cadre ni de fond de
2
+ bouton autour d'un cercle. L'anneau de focus suit sa forme. */
3
+ .sia-account__trigger {
4
+ display: inline-grid;
5
+ place-items: center;
6
+ padding: 0;
7
+ border: 0;
8
+ border-radius: 9999px;
9
+ background: transparent;
10
+ cursor: pointer;
11
+ transition: box-shadow var(--sia-duration-fast) var(--sia-easing-standard);
12
+ }
13
+ .sia-account__trigger:hover { box-shadow: 0 0 0 3px color-mix(in srgb, var(--sia-foreground) 8%, transparent); }
14
+ .sia-account__trigger:focus-visible { outline: 3px solid color-mix(in srgb, var(--sia-primary) 35%, transparent); outline-offset: 2px; }
15
+ .sia-account__trigger[aria-expanded="true"] { box-shadow: 0 0 0 3px color-mix(in srgb, var(--sia-primary) 25%, transparent); }
16
+
17
+ .sia-account__menu { min-width: 15rem; }
18
+
19
+ .sia-account__who { display: flex; align-items: center; gap: .75rem; min-width: 0; }
20
+ .sia-account__identity { display: grid; justify-items: start; gap: .15rem; min-width: 0; }
21
+ .sia-account__name { font-weight: 600; line-height: 1.3; }
22
+ .sia-account__email {
23
+ max-width: 14rem;
24
+ overflow: hidden;
25
+ color: var(--sia-muted);
26
+ font-size: .8rem;
27
+ text-overflow: ellipsis;
28
+ white-space: nowrap;
29
+ }
30
+ .sia-account__role { margin-top: .2rem; }
@@ -1,15 +1,15 @@
1
1
  import { useMemo, type Key, type ReactNode } from "react";
2
2
  import { cn, formatDateTime } from "@sia-ui/utils";
3
- import type { TableQuery } from "@sia-ui/headless";
3
+ import { FRENCH, useSiaLocale, type ActivityLogMessages, type TableQuery } from "@sia-ui/headless";
4
4
  import type { ComponentTone } from "@sia-ui/tokens";
5
5
  import { DataTable } from "../DataTable";
6
6
  import type { DataTableColumn, DataTableProps } from "../DataTable/types";
7
- import { FiltersBar } from "../FiltersBar";
8
- import { PageHeader } from "../PageHeader";
9
- import { Pagination } from "../Pagination";
10
- import { SearchInput } from "../SearchInput";
11
- import { Select, type SelectOption } from "../Select";
12
- import { StatusBadge } from "../Badge";
7
+ import { FiltersBar, type FiltersBarProps } from "../FiltersBar";
8
+ import { PageHeader, type PageHeaderProps } from "../PageHeader";
9
+ import { Pagination, type PaginationProps } from "../Pagination";
10
+ import { SearchInput, type SearchInputProps } from "../SearchInput";
11
+ import { Select, type SelectOption, type SelectProps } from "../Select";
12
+ import { StatusBadge, type StatusBadgeProps } from "../Badge";
13
13
  import "./styles.css";
14
14
 
15
15
  /** Une entrée de journal, dans sa forme la plus courante. */
@@ -71,6 +71,51 @@ export interface ActivityLogProps<T = ActivityEntry> {
71
71
  onRetry?: () => void;
72
72
  empty?: DataTableProps<T>["empty"];
73
73
  className?: string;
74
+
75
+ /** Les props du `PageHeader`, rendu quand `title` est fourni. */
76
+ pageHeaderProps?: Partial<PageHeaderProps>;
77
+ /**
78
+ * Les props de la `FiltersBar` — des actions, un autre libellé de
79
+ * réinitialisation. Sans `search`, `filters`, `activeCount` ni `onReset`,
80
+ * que le journal compose à partir de la requête.
81
+ */
82
+ filtersBarProps?: Partial<
83
+ Omit<FiltersBarProps, "search" | "filters" | "activeCount" | "onReset">
84
+ >;
85
+ /**
86
+ * Les props du champ de recherche. Sans `value`, `defaultValue`,
87
+ * `onValueChange` ni `onSearch` : la recherche en vigueur vit dans la
88
+ * requête.
89
+ */
90
+ searchInputProps?: Partial<
91
+ Omit<SearchInputProps, "value" | "defaultValue" | "onValueChange" | "onSearch">
92
+ >;
93
+ /**
94
+ * Les props communes aux `Select` des filtres. Sans `value`,
95
+ * `onValueChange` ni `options`, propres à chaque filtre et branchés sur la
96
+ * requête.
97
+ */
98
+ filterSelectProps?: Partial<Omit<SelectProps, "value" | "onValueChange" | "options">>;
99
+ /**
100
+ * Les props du `StatusBadge` de la colonne « Action » par défaut. Sans
101
+ * `value`, lue sur l'entrée. Sans effet quand `columns` est fourni.
102
+ */
103
+ actionBadgeProps?: Partial<Omit<StatusBadgeProps, "value">>;
104
+ /**
105
+ * Les props du `DataTable` — densité, mode cartes, sélection. Sans
106
+ * `columns` ni `data`, qui ont leurs props ici.
107
+ */
108
+ tableProps?: Partial<Omit<DataTableProps<T>, "columns" | "data">>;
109
+ /**
110
+ * Les props de la `Pagination` — `jumpTo`, `pageSizeOptions`. Sans la page
111
+ * et la taille de page ni leurs changements, branchés sur la requête.
112
+ */
113
+ paginationProps?: Partial<
114
+ Omit<
115
+ PaginationProps,
116
+ "page" | "totalPages" | "onPageChange" | "pageSize" | "onPageSizeChange"
117
+ >
118
+ >;
74
119
  }
75
120
 
76
121
  /** Les colonnes d'une `ActivityEntry` : quand, qui, quoi, sur quoi. */
@@ -78,36 +123,45 @@ export function activityColumns(
78
123
  options: {
79
124
  actionTones?: Record<string, ComponentTone> | undefined;
80
125
  actionLabels?: Record<string, ReactNode> | undefined;
126
+ /** Les props du `StatusBadge` de la colonne « Action », sans `value`. */
127
+ actionBadgeProps?: Partial<Omit<StatusBadgeProps, "value">> | undefined;
128
+ /** Les en-têtes : le groupe `activityLog` de la locale. Français par défaut. */
129
+ messages?: ActivityLogMessages | undefined;
130
+ /** La langue des dates. Par défaut, `fr-FR`. */
131
+ language?: string | undefined;
81
132
  } = {},
82
133
  ): Array<DataTableColumn<ActivityEntry>> {
134
+ const textes = options.messages ?? FRENCH.activityLog;
135
+ const language = options.language ?? FRENCH.language;
83
136
  return [
84
137
  {
85
138
  key: "at",
86
- header: "Date",
139
+ header: textes.date,
87
140
  width: "11rem",
88
141
  cell: (entry) => (
89
142
  <time className="sia-activity__date" dateTime={new Date(entry.at).toISOString()}>
90
- {formatDateTime(entry.at)}
143
+ {formatDateTime(entry.at, { locale: language })}
91
144
  </time>
92
145
  ),
93
146
  card: "meta",
94
147
  },
95
- { key: "actor", header: "Acteur", card: "title" },
148
+ { key: "actor", header: textes.actor, card: "title" },
96
149
  {
97
150
  key: "action",
98
- header: "Action",
151
+ header: textes.action,
99
152
  cell: (entry) => (
100
153
  <StatusBadge
101
- value={entry.action}
102
154
  {...(options.actionTones ? { tones: options.actionTones } : {})}
103
155
  {...(options.actionLabels ? { labels: options.actionLabels } : {})}
156
+ {...options.actionBadgeProps}
157
+ value={entry.action}
104
158
  />
105
159
  ),
106
160
  card: "meta",
107
161
  },
108
162
  {
109
163
  key: "target",
110
- header: "Cible",
164
+ header: textes.target,
111
165
  cell: (entry) =>
112
166
  entry.target || entry.summary ? (
113
167
  <span className="sia-activity__target">
@@ -146,14 +200,27 @@ export function ActivityLog<T = ActivityEntry>({
146
200
  onRetry,
147
201
  empty,
148
202
  className,
203
+ pageHeaderProps,
204
+ filtersBarProps,
205
+ searchInputProps,
206
+ filterSelectProps,
207
+ actionBadgeProps,
208
+ tableProps,
209
+ paginationProps,
149
210
  }: ActivityLogProps<T>) {
211
+ const locale = useSiaLocale();
212
+ const textes = locale.activityLog;
150
213
  const colonnes = useMemo(
151
214
  () =>
152
215
  columns ??
153
- (activityColumns({ actionTones, actionLabels }) as unknown as Array<
154
- DataTableColumn<T>
155
- >),
156
- [actionLabels, actionTones, columns],
216
+ (activityColumns({
217
+ actionTones,
218
+ actionLabels,
219
+ actionBadgeProps,
220
+ messages: textes,
221
+ language: locale.language,
222
+ }) as unknown as Array<DataTableColumn<T>>),
223
+ [actionBadgeProps, actionLabels, actionTones, columns, textes, locale.language],
157
224
  );
158
225
 
159
226
  const actifs =
@@ -171,11 +238,13 @@ export function ActivityLog<T = ActivityEntry>({
171
238
  title={title}
172
239
  {...(description ? { description } : {})}
173
240
  {...(headingLevel ? { level: headingLevel } : {})}
241
+ {...pageHeaderProps}
174
242
  />
175
243
  )}
176
244
 
177
245
  {(filters.length > 0 || searchPlaceholder) && (
178
246
  <FiltersBar
247
+ {...filtersBarProps}
179
248
  {...(searchPlaceholder
180
249
  ? {
181
250
  search: (
@@ -183,9 +252,10 @@ export function ActivityLog<T = ActivityEntry>({
183
252
  // Remonté à la réinitialisation : le champ reprend la
184
253
  // recherche en vigueur plutôt que sa saisie d'avant.
185
254
  key={query.state.search === "" ? "vide" : "saisie"}
186
- defaultValue={query.state.search}
187
255
  placeholder={searchPlaceholder}
188
256
  searchDelay={300}
257
+ {...searchInputProps}
258
+ defaultValue={query.state.search}
189
259
  onSearch={query.setSearch}
190
260
  />
191
261
  ),
@@ -196,10 +266,11 @@ export function ActivityLog<T = ActivityEntry>({
196
266
  <Select
197
267
  aria-label={filtre.label}
198
268
  placeholder={filtre.label}
199
- options={filtre.options}
200
- value={query.state.filters[filtre.key] ?? ""}
201
269
  clearable
202
270
  searchable={filtre.searchable ?? filtre.options.length > 10}
271
+ {...filterSelectProps}
272
+ options={filtre.options}
273
+ value={query.state.filters[filtre.key] ?? ""}
203
274
  onValueChange={(valeur) => query.setFilter(filtre.key, valeur)}
204
275
  />
205
276
  </div>
@@ -218,22 +289,22 @@ export function ActivityLog<T = ActivityEntry>({
218
289
  {...(onRetry ? { onRetry } : {})}
219
290
  empty={
220
291
  empty ?? {
221
- title: actifs > 0 ? "Aucune activité pour ces filtres" : "Aucune activité",
222
- ...(actifs > 0
223
- ? { description: "Élargissez la période ou retirez un filtre." }
224
- : {}),
292
+ title: actifs > 0 ? textes.emptyFiltered : textes.empty,
293
+ ...(actifs > 0 ? { description: textes.emptyFilteredHint } : {}),
225
294
  }
226
295
  }
296
+ {...tableProps}
227
297
  />
228
298
 
229
299
  {totalPages !== undefined && (
230
300
  <Pagination
301
+ showTotal
302
+ {...paginationProps}
231
303
  page={query.state.page}
232
304
  totalPages={totalPages}
233
305
  onPageChange={query.setPage}
234
306
  pageSize={query.state.perPage}
235
307
  onPageSizeChange={query.setPerPage}
236
- showTotal
237
308
  />
238
309
  )}
239
310
  </section>
@@ -1,9 +1,10 @@
1
1
  import { type ReactNode } from "react";
2
2
  import { cn, formatCurrency } from "@sia-ui/utils";
3
3
  import type { ComponentTone } from "@sia-ui/tokens";
4
+ import { useSiaLocale } from "@sia-ui/headless";
4
5
  import "./styles.css";
5
6
 
6
- export interface AmountDisplayProps { value: number; currency?: string; locale?: string; label?: ReactNode; secondary?: ReactNode; sign?: "auto" | "always"; tone?: ComponentTone; className?: string; }
7
+ export interface AmountDisplayProps { value: number; currency?: string; /** La langue du format. Par défaut, celle de la locale SIA. */ locale?: string; label?: ReactNode; secondary?: ReactNode; sign?: "auto" | "always"; tone?: ComponentTone; className?: string; }
7
8
  /**
8
9
  * Un montant, lisible d'un coup d'œil.
9
10
  *
@@ -12,7 +13,9 @@ export interface AmountDisplayProps { value: number; currency?: string; locale?:
12
13
  * rouge seul. La couleur peut manquer — un daltonien, une impression en
13
14
  * noir et blanc — le signe, lui, reste.
14
15
  */
15
- export function AmountDisplay({ value, currency = "XAF", locale = "fr-FR", label, secondary, sign = "auto", tone = "neutral", className }: AmountDisplayProps) {
16
+ export function AmountDisplay({ value, currency = "XAF", locale: localeProp, label, secondary, sign = "auto", tone = "neutral", className }: AmountDisplayProps) {
17
+ const messages = useSiaLocale();
18
+ const locale = localeProp ?? messages.language;
16
19
  const formatted = formatCurrency(Math.abs(value), { locale, currency });
17
20
  const prefix = sign === "always" || value < 0 ? value < 0 ? "-" : "+" : "";
18
21
  return <span className={cn("sia-amount-display", `sia-amount-display--${tone}`, className)}>{label && <span className="sia-amount-display__label">{label}</span>}<strong>{prefix}{formatted}</strong>{secondary && <span className="sia-amount-display__secondary">{secondary}</span>}</span>;
@@ -11,14 +11,16 @@ import { cn } from "@sia-ui/utils";
11
11
  import {
12
12
  filterNavTree,
13
13
  navKeyForPath,
14
+ useSiaLocale,
14
15
  type PermissionRule,
15
16
  } from "@sia-ui/headless";
16
17
  import { useMediaQuery } from "@sia-ui/react";
17
- import { Drawer } from "../Drawer";
18
+ import { Drawer, type DrawerProps } from "../Drawer";
18
19
  import {
19
20
  Sidebar,
20
21
  type SidebarEntry,
21
22
  type SidebarItem,
23
+ type SidebarProps,
22
24
  type SidebarRenderLink,
23
25
  } from "../Sidebar";
24
26
  import { BottomTabs } from "./tab-bar";
@@ -236,6 +238,35 @@ export interface AppShellProps {
236
238
  /** Sous cette largeur, la barre latérale cède la place. */
237
239
  mobileBreakpoint?: number;
238
240
 
241
+ /**
242
+ * Réglages de la barre latérale — taille, ton, accordéon, branches
243
+ * ouvertes, volets du rail.
244
+ *
245
+ * Ce que la coquille décide seule en est retiré : les entrées filtrées,
246
+ * l'entrée active, l'état réduit, les liens, l'en-tête et le pied, le côté
247
+ * et la variante — tous déjà réglables ici, et qui doivent rester
248
+ * d'accord avec la mise en page de la coquille.
249
+ */
250
+ sidebarProps?: Partial<
251
+ Omit<
252
+ SidebarProps,
253
+ | "items"
254
+ | "can"
255
+ | "activeKey"
256
+ | "collapsed"
257
+ | "side"
258
+ | "variant"
259
+ | "renderLink"
260
+ | "onNavigate"
261
+ | "header"
262
+ | "footer"
263
+ | "ariaLabel"
264
+ >
265
+ >;
266
+
267
+ /** Réglages du tiroir mobile — taille, fermeture au clic extérieur, classe. */
268
+ drawerProps?: Partial<Omit<DrawerProps, "open" | "defaultOpen" | "onOpenChange" | "children">>;
269
+
239
270
  ariaLabel?: string;
240
271
  className?: string;
241
272
  }
@@ -273,9 +304,12 @@ export function AppShell({
273
304
  sidebarCollapsedWidth,
274
305
  mobileNav = "drawer",
275
306
  mobileBreakpoint = 768,
307
+ sidebarProps,
308
+ drawerProps,
276
309
  ariaLabel,
277
310
  className,
278
311
  }: AppShellProps) {
312
+ const m = useSiaLocale().appShell;
279
313
  const compact = useMediaQuery(`(max-width: ${mobileBreakpoint - 1}px)`);
280
314
 
281
315
  // Filtré une fois, consommé par la barre et par les onglets.
@@ -339,6 +373,7 @@ export function AppShell({
339
373
 
340
374
  const barre = (
341
375
  <Sidebar
376
+ {...sidebarProps}
342
377
  items={visibles}
343
378
  collapsed={reduite && !compact}
344
379
  side={side}
@@ -405,7 +440,7 @@ export function AppShell({
405
440
  <button
406
441
  type="button"
407
442
  className="sia-shell__toggle"
408
- aria-label="Ouvrir la navigation"
443
+ aria-label={m.openNavigation}
409
444
  aria-expanded={tiroirOuvert}
410
445
  onClick={() => setTiroirOuvert(true)}
411
446
  >
@@ -418,8 +453,8 @@ export function AppShell({
418
453
  className="sia-shell__toggle"
419
454
  aria-label={
420
455
  etat === "expanded"
421
- ? "Réduire la navigation"
422
- : "Déployer la navigation"
456
+ ? m.collapseNavigation
457
+ : m.expandNavigation
423
458
  }
424
459
  aria-expanded={etat === "expanded"}
425
460
  onClick={basculer}
@@ -458,14 +493,15 @@ export function AppShell({
458
493
 
459
494
  {compact && montrerTiroir && (
460
495
  <Drawer
461
- open={tiroirOuvert}
462
- onOpenChange={setTiroirOuvert}
463
496
  position={side}
464
497
  size="sm"
465
- className="sia-shell__drawer"
498
+ {...drawerProps}
499
+ className={cn("sia-shell__drawer", drawerProps?.className)}
500
+ open={tiroirOuvert}
501
+ onOpenChange={setTiroirOuvert}
466
502
  >
467
503
  <Drawer.Header>
468
- <Drawer.Title>Navigation</Drawer.Title>
504
+ <Drawer.Title>{m.navigationTitle}</Drawer.Title>
469
505
  <Drawer.Close />
470
506
  </Drawer.Header>
471
507
  <Drawer.Body>{barre}</Drawer.Body>
@@ -1,6 +1,6 @@
1
1
  import { memo, type ReactNode } from "react";
2
2
  import { cn } from "@sia-ui/utils";
3
- import { navLeaves, navSome } from "@sia-ui/headless";
3
+ import { navLeaves, navSome, useSiaLocale } from "@sia-ui/headless";
4
4
  import type { SidebarItem, SidebarRenderLink } from "../Sidebar";
5
5
 
6
6
  export interface BottomTabsProps {
@@ -23,6 +23,7 @@ export interface BottomTabsProps {
23
23
  /** Le bouton qui ouvre l'arborescence complète. */
24
24
  more?: ReactNode;
25
25
  onMore?: () => void;
26
+ /** Le libellé de « Plus ». Par défaut, celui de la locale. */
26
27
  moreLabel?: string;
27
28
 
28
29
  className?: string;
@@ -47,16 +48,18 @@ export const BottomTabs = memo(function BottomTabs({
47
48
  onNavigate,
48
49
  more,
49
50
  onMore,
50
- moreLabel = "Plus",
51
+ moreLabel: moreLabelProp,
51
52
  className,
52
53
  }: BottomTabsProps) {
54
+ const m = useSiaLocale().appShell;
55
+ const moreLabel = moreLabelProp ?? m.more;
53
56
  const feuilles = navLeaves(items);
54
57
  const onglets = feuilles.slice(0, max);
55
58
  const reste = feuilles.length - onglets.length;
56
59
  const afficherPlus = reste > 0 || Boolean(onMore);
57
60
 
58
61
  return (
59
- <nav className={cn("sia-bottom-tabs", className)} aria-label="Navigation principale">
62
+ <nav className={cn("sia-bottom-tabs", className)} aria-label={m.bottomTabsLabel}>
60
63
  {onglets.map((item) => {
61
64
  const actif = item.key === activeKey;
62
65
  const contenu = (
@@ -1,7 +1,9 @@
1
1
  import { type ReactNode } from "react";
2
- import { Alert } from "../Alert";
3
- import { Button } from "../Button";
4
- import { EmptyState } from "../EmptyState";
2
+ import { useSiaLocale } from "@sia-ui/headless";
3
+ import { Alert, type AlertProps } from "../Alert";
4
+ import { Button, type ButtonProps } from "../Button";
5
+ import { EmptyState, type EmptyStateProps } from "../EmptyState";
6
+ import { Spinner, type SpinnerProps } from "../Spinner";
5
7
  import "./styles.css";
6
8
 
7
9
  export interface AsyncStateProps {
@@ -12,6 +14,21 @@ export interface AsyncStateProps {
12
14
  emptyDescription?: ReactNode;
13
15
  onRetry?: () => void;
14
16
  children: ReactNode;
17
+ /**
18
+ * Les props de l'`Alert` d'erreur — un autre titre, un autre ton. Sans
19
+ * `children` : le message est `error`.
20
+ */
21
+ alertProps?: Partial<Omit<AlertProps, "children">>;
22
+ /**
23
+ * Les props du bouton « Réessayer ». Sans `onClick`, branché sur
24
+ * `onRetry` : un bouton de reprise qui ne reprend rien serait pire que
25
+ * pas de bouton.
26
+ */
27
+ retryButtonProps?: Partial<Omit<ButtonProps, "onClick">>;
28
+ /** Les props de l'`EmptyState` — une icône, une action, la taille. */
29
+ emptyStateProps?: Partial<EmptyStateProps>;
30
+ /** Le témoin de chargement. Son libellé vient de la locale. */
31
+ spinnerProps?: Partial<Omit<SpinnerProps, "label">>;
15
32
  }
16
33
 
17
34
  /**
@@ -21,9 +38,14 @@ export interface AsyncStateProps {
21
38
  * écran, et les écrire à la main en oublie toujours un — le plus souvent le
22
39
  * vide, qui ressemble à un chargement qui n'en finit pas.
23
40
  */
24
- export function AsyncState({ loading, error, empty, emptyTitle = "No data", emptyDescription, onRetry, children }: AsyncStateProps) {
25
- if (loading) return <div className="sia-async-state" role="status"><span className="sia-spinner" /><span>Loading...</span></div>;
26
- if (error) return <Alert tone="danger" title="Unable to load data" action={onRetry && <Button variant="outline" onClick={onRetry}>Retry</Button>}>{error}</Alert>;
27
- if (empty) return <EmptyState compact title={emptyTitle} description={emptyDescription} />;
41
+ export function AsyncState({ loading, error, empty, emptyTitle, emptyDescription, onRetry, children, alertProps, retryButtonProps, emptyStateProps, spinnerProps }: AsyncStateProps) {
42
+ // Les libellés viennent de la locale, comme partout ailleurs : ils étaient
43
+ // restés en anglais ici, seuls de toute la bibliothèque.
44
+ const locale = useSiaLocale();
45
+ // Le vrai `Spinner` : un `<span class="sia-spinner">` brut ignorait ses
46
+ // réglages par défaut (`SiaProvider defaults.spinner`).
47
+ if (loading) return <div className="sia-async-state" role="status"><Spinner size="sm" {...spinnerProps} label={locale.loading} /><span aria-hidden="true">{locale.loading}</span></div>;
48
+ if (error) return <Alert tone="danger" title={locale.error} action={onRetry && <Button variant="outline" {...retryButtonProps} onClick={onRetry}>{retryButtonProps?.children ?? locale.retry}</Button>} {...alertProps}>{error}</Alert>;
49
+ if (empty) return <EmptyState compact title={emptyTitle ?? locale.empty} description={emptyDescription} {...emptyStateProps} />;
28
50
  return <>{children}</>;
29
51
  }
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode } from "react";
2
2
  import { cn, formatDate, formatTimeAgo } from "@sia-ui/utils";
3
+ import { useSiaLocale } from "@sia-ui/headless";
3
4
  import "./styles.css";
4
5
 
5
6
  export interface AuditEvent {
@@ -11,8 +12,11 @@ export interface AuditEvent {
11
12
 
12
13
  export interface AuditMetaProps {
13
14
  events: AuditEvent[];
15
+ /** La langue des dates. Par défaut, celle de la locale SIA. */
14
16
  locale?: string;
17
+ /** Par défaut, « Historique » de la locale SIA. */
15
18
  title?: ReactNode;
19
+ /** Par défaut, « Aucune activité » de la locale SIA. */
16
20
  emptyLabel?: ReactNode;
17
21
  className?: string;
18
22
  }
@@ -25,7 +29,13 @@ export interface AuditMetaProps {
25
29
  * veille. Les dates sont relatives — « il y a deux jours » se compare sans
26
30
  * calcul mental.
27
31
  */
28
- export function AuditMeta({ events, locale = "fr-FR", title = "Historique", emptyLabel = "Aucune activité", className }: AuditMetaProps) {
32
+ export function AuditMeta({ events, locale: localeProp, title: titleProp, emptyLabel: emptyLabelProp, className }: AuditMetaProps) {
33
+ const messages = useSiaLocale();
34
+ const locale = localeProp ?? messages.language;
35
+ const title = titleProp ?? messages.auditMeta.title;
36
+ const emptyLabel = emptyLabelProp ?? messages.auditMeta.empty;
37
+ // « par {name} » : l'auteur est un nœud React, le texte l'entoure.
38
+ const [avantAuteur = "", apresAuteur = ""] = messages.auditMeta.by.split("{name}");
29
39
  return (
30
40
  <section className={cn("sia-audit-meta", className)}>
31
41
  <h2 className="sia-audit-meta__title">{title}</h2>
@@ -37,7 +47,7 @@ export function AuditMeta({ events, locale = "fr-FR", title = "Historique", empt
37
47
  <li className="sia-audit-meta__event" key={`${new Date(event.at).getTime()}-${index}`}>
38
48
  <span className="sia-audit-meta__marker" aria-hidden="true" />
39
49
  <div className="sia-audit-meta__content">
40
- <div className="sia-audit-meta__heading"><strong>{event.label}</strong>{event.by && <span>par {event.by}</span>}</div>
50
+ <div className="sia-audit-meta__heading"><strong>{event.label}</strong>{event.by && <span>{avantAuteur}{event.by}{apresAuteur}</span>}</div>
41
51
  {event.detail && <div className="sia-audit-meta__detail">{event.detail}</div>}
42
52
  <time className="sia-audit-meta__time" dateTime={new Date(event.at).toISOString()} title={formatDate(event.at, { locale, dateStyle: "full", timeStyle: "short" })}>
43
53
  {formatTimeAgo(event.at, { locale })}