@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.
- package/CHANGELOG.md +6 -0
- package/README.md +6 -0
- package/dist/registry/package/components/AccountMenu/index.tsx +107 -0
- package/dist/registry/package/components/AccountMenu/styles.css +30 -0
- package/dist/registry/package/components/ActivityLog/index.tsx +96 -25
- package/dist/registry/package/components/AmountDisplay/index.tsx +5 -2
- package/dist/registry/package/components/AppShell/index.tsx +44 -8
- package/dist/registry/package/components/AppShell/tab-bar.tsx +6 -3
- package/dist/registry/package/components/AsyncState/index.tsx +29 -7
- package/dist/registry/package/components/AuditMeta/index.tsx +12 -2
- package/dist/registry/package/components/Avatar/index.tsx +4 -9
- package/dist/registry/package/components/Badge/index.tsx +3 -2
- package/dist/registry/package/components/Badge/styles.css +4 -4
- package/dist/registry/package/components/BrandIcon/index.tsx +100 -0
- package/dist/registry/package/components/BrandIcon/styles.css +31 -0
- package/dist/registry/package/components/Breadcrumb/index.tsx +14 -2
- package/dist/registry/package/components/Button/index.tsx +3 -2
- package/dist/registry/package/components/Button/styles.css +4 -4
- package/dist/registry/package/components/Calendar/index.tsx +7 -3
- package/dist/registry/package/components/Card/index.tsx +14 -4
- package/dist/registry/package/components/ColorModeToggle/index.tsx +44 -0
- package/dist/registry/package/components/CommandPalette/index.tsx +3 -2
- package/dist/registry/package/components/ConfirmDialog/index.tsx +24 -4
- package/dist/registry/package/components/CrudPage/dialogs.tsx +53 -8
- package/dist/registry/package/components/CrudPage/index.tsx +99 -25
- package/dist/registry/package/components/CurrencyInput/index.tsx +5 -2
- package/dist/registry/package/components/DataTable/index.tsx +53 -18
- package/dist/registry/package/components/DataTable/row-actions.tsx +20 -8
- package/dist/registry/package/components/DataTable/types.ts +100 -0
- package/dist/registry/package/components/DatePicker/index.tsx +14 -6
- package/dist/registry/package/components/DateRangeFilter/index.tsx +12 -3
- package/dist/registry/package/components/DateRangePicker/index.tsx +13 -4
- package/dist/registry/package/components/DateTimePicker/index.tsx +3 -1
- package/dist/registry/package/components/Drawer/index.tsx +16 -6
- package/dist/registry/package/components/DropdownMenu/index.tsx +37 -4
- package/dist/registry/package/components/DropdownMenu/styles.css +10 -0
- package/dist/registry/package/components/DurationDisplay/index.tsx +20 -2
- package/dist/registry/package/components/EntityMeta/index.tsx +14 -9
- package/dist/registry/package/components/ErrorState/index.tsx +9 -3
- package/dist/registry/package/components/ErrorState/styles.css +1 -1
- package/dist/registry/package/components/EventCalendar/index.tsx +657 -9
- package/dist/registry/package/components/EventCalendar/styles.css +360 -8
- package/dist/registry/package/components/EventManager/index.tsx +456 -0
- package/dist/registry/package/components/EventManager/styles.css +12 -0
- package/dist/registry/package/components/Field/index.tsx +74 -27
- package/dist/registry/package/components/FileUpload/index.tsx +13 -3
- package/dist/registry/package/components/FiltersBar/index.tsx +8 -3
- package/dist/registry/package/components/FiltersBar/styles.css +7 -0
- package/dist/registry/package/components/Form/index.tsx +118 -14
- package/dist/registry/package/components/Form/styles.css +3 -0
- package/dist/registry/package/components/HoverCard/index.tsx +8 -4
- package/dist/registry/package/components/Icons/index.tsx +18 -0
- package/dist/registry/package/components/ImageUpload/index.tsx +17 -2
- package/dist/registry/package/components/Input/index.tsx +3 -2
- package/dist/registry/package/components/JobProgress/index.tsx +289 -0
- package/dist/registry/package/components/JobProgress/styles.css +58 -0
- package/dist/registry/package/components/JsonEditor/index.tsx +18 -3
- package/dist/registry/package/components/JsonView/index.tsx +282 -0
- package/dist/registry/package/components/JsonView/styles.css +99 -0
- package/dist/registry/package/components/LiveIndicator/index.tsx +43 -0
- package/dist/registry/package/components/LiveIndicator/styles.css +41 -0
- package/dist/registry/package/components/LogStream/index.tsx +545 -0
- package/dist/registry/package/components/LogStream/styles.css +152 -0
- package/dist/registry/package/components/MarkdownEditor/index.tsx +14 -3
- package/dist/registry/package/components/Modal/index.tsx +7 -1
- package/dist/registry/package/components/MultiSelect/index.tsx +14 -3
- package/dist/registry/package/components/OtpInput/index.tsx +2 -1
- package/dist/registry/package/components/Pagination/index.tsx +13 -2
- package/dist/registry/package/components/PasswordInput/index.tsx +23 -9
- package/dist/registry/package/components/PhoneInput/index.tsx +66 -30
- package/dist/registry/package/components/Popover/index.tsx +19 -4
- package/dist/registry/package/components/QrCode/index.tsx +3 -2
- package/dist/registry/package/components/RadioGroup/styles.css +3 -0
- package/dist/registry/package/components/Rating/index.tsx +6 -2
- package/dist/registry/package/components/ReferenceSelect/index.tsx +8 -1
- package/dist/registry/package/components/RelativeTime/index.tsx +5 -1
- package/dist/registry/package/components/RequireSession/index.tsx +10 -4
- package/dist/registry/package/components/Resource/index.tsx +70 -17
- package/dist/registry/package/components/RichTextEditor/index.tsx +2 -1
- package/dist/registry/package/components/SearchInput/index.tsx +4 -1
- package/dist/registry/package/components/SecretFields/index.tsx +70 -17
- package/dist/registry/package/components/Select/index.tsx +34 -12
- package/dist/registry/package/components/Sidebar/index.tsx +22 -6
- package/dist/registry/package/components/Slider/index.tsx +26 -5
- package/dist/registry/package/components/Spinner/index.tsx +3 -2
- package/dist/registry/package/components/StatCard/index.tsx +11 -3
- package/dist/registry/package/components/Statistic/index.tsx +5 -1
- package/dist/registry/package/components/Steps/styles.css +2 -2
- package/dist/registry/package/components/TagsInput/index.tsx +2 -1
- package/dist/registry/package/components/TimePicker/index.tsx +10 -4
- package/dist/registry/package/components/Toast/index.tsx +20 -5
- package/dist/registry/package/components/Tour/index.tsx +2 -1
- package/dist/registry/package/components/Transfer/index.tsx +2 -1
- package/dist/registry/package/components/Tree/index.tsx +3 -2
- package/dist/registry/package/components/TreeSelect/index.tsx +17 -4
- package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +34 -3
- package/dist/registry/registry.json +257 -75
- package/dist/registry/styles/sia-ui.css +23 -4
- package/package.json +5 -5
|
@@ -22,9 +22,14 @@ import {
|
|
|
22
22
|
required as regleRequise,
|
|
23
23
|
type Rule,
|
|
24
24
|
} from "@sia-ui/utils/validation";
|
|
25
|
-
import
|
|
25
|
+
import {
|
|
26
|
+
useSiaLocale,
|
|
27
|
+
type FormValidator,
|
|
28
|
+
type PermissionRule,
|
|
29
|
+
type ResourceMessages,
|
|
30
|
+
} from "@sia-ui/headless";
|
|
26
31
|
import type { ComponentTone } from "@sia-ui/tokens";
|
|
27
|
-
import type { FieldType, FieldValue } from "../Field";
|
|
32
|
+
import type { FieldControlOverrides, FieldType, FieldValue } from "../Field";
|
|
28
33
|
import type { SelectOption } from "../Select";
|
|
29
34
|
import type { FormEntry, FormFieldConfig, FormShape } from "../Form";
|
|
30
35
|
import type { ColumnCardRole, DataTableColumn } from "../DataTable/types";
|
|
@@ -118,6 +123,14 @@ export interface ResourceField<T = unknown> {
|
|
|
118
123
|
/** Le rendu en tableau et en détail. Écrase celui déduit du type. */
|
|
119
124
|
render?: (row: T, index: number) => ReactNode;
|
|
120
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Les props du contrôle de saisie, transmises telles quelles par le
|
|
128
|
+
* formulaire : `{ searchable: true }` pour un `select` long, `{ maxLength }`
|
|
129
|
+
* pour un texte. Sans elles, régler le contrôle obligeait à redéclarer tous
|
|
130
|
+
* les champs dans la page.
|
|
131
|
+
*/
|
|
132
|
+
controlProps?: FieldControlOverrides;
|
|
133
|
+
|
|
121
134
|
/**
|
|
122
135
|
* Une valeur calculée plutôt que lue sur la ligne.
|
|
123
136
|
*
|
|
@@ -228,9 +241,11 @@ const CHERCHABLES = new Set<FieldType>([
|
|
|
228
241
|
const NUMERIQUES = new Set<FieldType>(["number", "currency", "slider"]);
|
|
229
242
|
|
|
230
243
|
/** La règle que le type impose de lui-même. */
|
|
231
|
-
const REGLE_DU_TYPE: Partial<
|
|
232
|
-
|
|
233
|
-
|
|
244
|
+
const REGLE_DU_TYPE: Partial<
|
|
245
|
+
Record<FieldType, (messages?: ResourceMessages) => Rule<never>>
|
|
246
|
+
> = {
|
|
247
|
+
email: (messages) => regleEmail(messages?.invalidEmail) as Rule<never>,
|
|
248
|
+
phone: (messages) => reglePhone(messages?.invalidPhone) as Rule<never>,
|
|
234
249
|
};
|
|
235
250
|
|
|
236
251
|
/** « dateEcheance » → « Date echeance ». Un repli, pas une traduction. */
|
|
@@ -271,6 +286,45 @@ function entrees<T>(resource: ResourceDefinition<T>) {
|
|
|
271
286
|
return Object.entries(resource.fields) as Array<[string, ResourceField<T>]>;
|
|
272
287
|
}
|
|
273
288
|
|
|
289
|
+
/* ─────────────────────────────────────── les valeurs dans la langue */
|
|
290
|
+
|
|
291
|
+
/*
|
|
292
|
+
Les rendus par défaut sont des fonctions pures, appelées hors de tout
|
|
293
|
+
composant : la locale n'y est pas lisible. Ces trois petits composants la
|
|
294
|
+
lisent au moment où la cellule s'affiche — « Oui » ou « Yes », la date et
|
|
295
|
+
le nombre au format de la langue.
|
|
296
|
+
*/
|
|
297
|
+
|
|
298
|
+
function OuiNon({ value }: { value: unknown }) {
|
|
299
|
+
const locale = useSiaLocale();
|
|
300
|
+
return <>{value ? locale.yes : locale.no}</>;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function DateLocale({ value, time }: { value: string | Date; time: boolean }) {
|
|
304
|
+
const { language } = useSiaLocale();
|
|
305
|
+
return (
|
|
306
|
+
<>
|
|
307
|
+
{time
|
|
308
|
+
? formatDateTime(value, { locale: language })
|
|
309
|
+
: formatDate(value, { locale: language })}
|
|
310
|
+
</>
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function NombreLocale({ value, decimals }: { value: number; decimals?: number | undefined }) {
|
|
315
|
+
const { language } = useSiaLocale();
|
|
316
|
+
return (
|
|
317
|
+
<>
|
|
318
|
+
{formatNumber(value, {
|
|
319
|
+
locale: language,
|
|
320
|
+
...(decimals !== undefined
|
|
321
|
+
? { minimumFractionDigits: decimals, maximumFractionDigits: decimals }
|
|
322
|
+
: {}),
|
|
323
|
+
})}
|
|
324
|
+
</>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
274
328
|
/* ──────────────────────────────────────────────────── les dérivations */
|
|
275
329
|
|
|
276
330
|
/**
|
|
@@ -318,14 +372,14 @@ function renduParDefaut<T>(
|
|
|
318
372
|
}
|
|
319
373
|
|
|
320
374
|
if (type === "checkbox" || type === "switch") {
|
|
321
|
-
return (row) =>
|
|
375
|
+
return (row) => <OuiNon value={lire(row)} />;
|
|
322
376
|
}
|
|
323
377
|
|
|
324
378
|
if (type === "date" || type === "datetime") {
|
|
325
379
|
return (row) => {
|
|
326
380
|
const valeur = lire(row);
|
|
327
381
|
if (typeof valeur !== "string" && !(valeur instanceof Date)) return null;
|
|
328
|
-
return type === "
|
|
382
|
+
return <DateLocale value={valeur} time={type === "datetime"} />;
|
|
329
383
|
};
|
|
330
384
|
}
|
|
331
385
|
|
|
@@ -333,14 +387,7 @@ function renduParDefaut<T>(
|
|
|
333
387
|
return (row) => {
|
|
334
388
|
const valeur = lire(row);
|
|
335
389
|
if (typeof valeur !== "number") return null;
|
|
336
|
-
return
|
|
337
|
-
...(champ.decimals !== undefined
|
|
338
|
-
? {
|
|
339
|
-
minimumFractionDigits: champ.decimals,
|
|
340
|
-
maximumFractionDigits: champ.decimals,
|
|
341
|
-
}
|
|
342
|
-
: {}),
|
|
343
|
-
});
|
|
390
|
+
return <NombreLocale value={valeur} decimals={champ.decimals} />;
|
|
344
391
|
};
|
|
345
392
|
}
|
|
346
393
|
|
|
@@ -450,6 +497,7 @@ export function resourceFormFields<T>(
|
|
|
450
497
|
...(champ.description ? { description: champ.description } : {}),
|
|
451
498
|
...(champ.options ? { options: champ.options } : {}),
|
|
452
499
|
...(champ.colSpan ? { colSpan: champ.colSpan } : {}),
|
|
500
|
+
...(champ.controlProps ? { controlProps: champ.controlProps } : {}),
|
|
453
501
|
};
|
|
454
502
|
|
|
455
503
|
if (champ.group) {
|
|
@@ -472,10 +520,15 @@ export function resourceFormFields<T>(
|
|
|
472
520
|
*
|
|
473
521
|
* Un champ `email` valide une adresse sans qu'on le demande : le type le dit
|
|
474
522
|
* déjà. Les règles déclarées s'y ajoutent, elles ne les remplacent pas.
|
|
523
|
+
*
|
|
524
|
+
* `messages` — `locale.resource`, lu avec `useSiaLocale()` — donne leurs
|
|
525
|
+
* textes aux règles imposées ; `CrudPage` le passe. À défaut, les messages
|
|
526
|
+
* par défaut de `@sia-ui/utils/validation`.
|
|
475
527
|
*/
|
|
476
528
|
export function resourceValidator<T>(
|
|
477
529
|
resource: ResourceDefinition<T>,
|
|
478
530
|
mode: ResourceFormMode = "create",
|
|
531
|
+
messages?: ResourceMessages,
|
|
479
532
|
): FormValidator<FormShape> | undefined {
|
|
480
533
|
const schema: Record<string, Array<Rule<never>>> = {};
|
|
481
534
|
|
|
@@ -484,11 +537,11 @@ export function resourceValidator<T>(
|
|
|
484
537
|
|
|
485
538
|
const regles: Array<Rule<never>> = [];
|
|
486
539
|
if (estRequis(champ as ResourceField<never>, mode)) {
|
|
487
|
-
regles.push(regleRequise() as Rule<never>);
|
|
540
|
+
regles.push(regleRequise(messages?.required) as Rule<never>);
|
|
488
541
|
}
|
|
489
542
|
|
|
490
543
|
const duType = REGLE_DU_TYPE[champ.type ?? "text"];
|
|
491
|
-
if (duType) regles.push(duType());
|
|
544
|
+
if (duType) regles.push(duType(messages));
|
|
492
545
|
|
|
493
546
|
if (champ.rules) regles.push(...champ.rules);
|
|
494
547
|
if (regles.length > 0) schema[cle] = regles;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, type HTMLAttributes } from "react";
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
|
+
import { useSiaLocale } from "@sia-ui/headless";
|
|
3
4
|
import "./styles.css";
|
|
4
5
|
export interface RichTextEditorProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue"> { value?: string; defaultValue?: string; onValueChange?: (html: string) => void; placeholder?: string; disabled?: boolean; }
|
|
5
6
|
/**
|
|
@@ -9,4 +10,4 @@ export interface RichTextEditorProps extends Omit<HTMLAttributes<HTMLDivElement>
|
|
|
9
10
|
* renvoie et s'affiche partout ailleurs sans qu'un convertisseur soit
|
|
10
11
|
* nécessaire.
|
|
11
12
|
*/
|
|
12
|
-
export function RichTextEditor({ value, defaultValue = "", onValueChange, placeholder
|
|
13
|
+
export function RichTextEditor({ value, defaultValue = "", onValueChange, placeholder: placeholderProp, disabled, className, ...props }: RichTextEditorProps) { const messages = useSiaLocale().richTextEditor; const placeholder = placeholderProp ?? messages.placeholder; const editorRef = useRef<HTMLDivElement>(null); useEffect(() => { if (value !== undefined && editorRef.current && editorRef.current.innerHTML !== value) editorRef.current.innerHTML = value; }, [value]); const command = (name: string) => { editorRef.current?.focus(); document.execCommand(name); onValueChange?.(editorRef.current?.innerHTML ?? ""); }; return <div className={cn("sia-rich-text-editor", disabled && "sia-rich-text-editor--disabled", className)}><div className="sia-editor-toolbar"><button type="button" disabled={disabled} aria-label={messages.bold} onClick={() => command("bold")}><strong>B</strong></button><button type="button" disabled={disabled} aria-label={messages.italic} onClick={() => command("italic")}><em>I</em></button><button type="button" disabled={disabled} aria-label={messages.bulletList} onClick={() => command("insertUnorderedList")}>• {messages.bulletList}</button></div><div {...props} ref={editorRef} role="textbox" aria-multiline="true" contentEditable={!disabled} suppressContentEditableWarning data-placeholder={placeholder} dangerouslySetInnerHTML={value === undefined ? { __html: defaultValue } : undefined} onInput={(event) => onValueChange?.(event.currentTarget.innerHTML)} /></div>; }
|
|
@@ -26,7 +26,9 @@ export interface SearchInputProps
|
|
|
26
26
|
*/
|
|
27
27
|
searchDelay?: number;
|
|
28
28
|
clearable?: boolean;
|
|
29
|
+
/** Le nom du champ. Par défaut, `search` de la locale. */
|
|
29
30
|
searchLabel?: string;
|
|
31
|
+
/** Le bouton qui vide le champ. Par défaut, `searchInput.clear` de la locale. */
|
|
30
32
|
clearLabel?: string;
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -48,7 +50,7 @@ export const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
|
|
|
48
50
|
searchDelay = 0,
|
|
49
51
|
clearable = true,
|
|
50
52
|
searchLabel: searchLabelProp,
|
|
51
|
-
clearLabel
|
|
53
|
+
clearLabel: clearLabelProp,
|
|
52
54
|
className,
|
|
53
55
|
placeholder: placeholderProp,
|
|
54
56
|
...props
|
|
@@ -58,6 +60,7 @@ export const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
|
|
|
58
60
|
const locale = useSiaLocale();
|
|
59
61
|
const placeholder = placeholderProp ?? locale.searchPlaceholder;
|
|
60
62
|
const searchLabel = searchLabelProp ?? locale.search;
|
|
63
|
+
const clearLabel = clearLabelProp ?? locale.searchInput.clear;
|
|
61
64
|
|
|
62
65
|
const [internal, setInternal] = useState(defaultValue);
|
|
63
66
|
const current = value ?? internal;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useId, useMemo, useState, type ReactNode } from "react";
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { formatMessage, useSiaLocale } from "@sia-ui/headless";
|
|
4
|
+
import { Button, type ButtonProps } from "../Button";
|
|
5
|
+
import { IconButton, type IconButtonProps } from "../IconButton";
|
|
6
|
+
import { Input, type InputProps } from "../Input";
|
|
7
|
+
import { PasswordInput, type PasswordInputProps } from "../PasswordInput";
|
|
7
8
|
import { PlusIcon, TrashIcon } from "../Icons";
|
|
8
9
|
import "./styles.css";
|
|
9
10
|
|
|
@@ -29,12 +30,51 @@ export interface SecretFieldsProps {
|
|
|
29
30
|
templates?: string[];
|
|
30
31
|
/** La forme d'un nom valide. Par défaut, aucune contrainte. */
|
|
31
32
|
keyPattern?: RegExp;
|
|
33
|
+
/** Les libellés ci-dessous viennent par défaut du groupe `secretFields` de la locale. */
|
|
32
34
|
keyPatternMessage?: string;
|
|
33
35
|
keyLabel?: string;
|
|
34
36
|
valueLabel?: string;
|
|
35
37
|
addLabel?: ReactNode;
|
|
36
38
|
disabled?: boolean;
|
|
37
39
|
className?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Le bouton d'ajout : variante, taille, icône, classe. Son libellé passe
|
|
42
|
+
* par `addLabel`.
|
|
43
|
+
*/
|
|
44
|
+
addButtonProps?: Partial<Omit<ButtonProps, "onClick" | "children" | "disabled">>;
|
|
45
|
+
/**
|
|
46
|
+
* Le bouton qui retire une ligne : icône, variante, taille.
|
|
47
|
+
*
|
|
48
|
+
* Son nom reste calculé ligne par ligne — « Retirer API_KEY » — pour que
|
|
49
|
+
* chaque bouton dise lequel il retire.
|
|
50
|
+
*/
|
|
51
|
+
removeButtonProps?: Partial<
|
|
52
|
+
Omit<IconButtonProps, "onClick" | "label" | "disabled">
|
|
53
|
+
>;
|
|
54
|
+
/**
|
|
55
|
+
* Le champ du nom : placeholder, taille, classe.
|
|
56
|
+
*
|
|
57
|
+
* La valeur, la saisie et l'état d'erreur, avec son `aria-describedby`,
|
|
58
|
+
* restent calculés par ligne.
|
|
59
|
+
*/
|
|
60
|
+
keyInputProps?: Partial<
|
|
61
|
+
Omit<
|
|
62
|
+
InputProps,
|
|
63
|
+
"value" | "defaultValue" | "onChange" | "disabled" | "invalid" | "aria-describedby"
|
|
64
|
+
>
|
|
65
|
+
>;
|
|
66
|
+
/**
|
|
67
|
+
* Le champ de la valeur : placeholder, jauge, classe.
|
|
68
|
+
*
|
|
69
|
+
* Pas d'`autoComplete` : `new-password` est ce qui empêche le navigateur
|
|
70
|
+
* de proposer le mot de passe du compte à la place du secret.
|
|
71
|
+
*/
|
|
72
|
+
valueInputProps?: Partial<
|
|
73
|
+
Omit<
|
|
74
|
+
PasswordInputProps,
|
|
75
|
+
"value" | "defaultValue" | "onChange" | "disabled" | "autoComplete"
|
|
76
|
+
>
|
|
77
|
+
>;
|
|
38
78
|
}
|
|
39
79
|
|
|
40
80
|
/** Les valeurs effacées, les noms gardés — ce qu'on affiche après l'envoi. */
|
|
@@ -60,13 +100,22 @@ export function SecretFields({
|
|
|
60
100
|
storedKeys = [],
|
|
61
101
|
templates = [],
|
|
62
102
|
keyPattern,
|
|
63
|
-
keyPatternMessage
|
|
64
|
-
keyLabel
|
|
65
|
-
valueLabel
|
|
66
|
-
addLabel
|
|
103
|
+
keyPatternMessage: keyPatternMessageProp,
|
|
104
|
+
keyLabel: keyLabelProp,
|
|
105
|
+
valueLabel: valueLabelProp,
|
|
106
|
+
addLabel: addLabelProp,
|
|
67
107
|
disabled = false,
|
|
68
108
|
className,
|
|
109
|
+
addButtonProps,
|
|
110
|
+
removeButtonProps,
|
|
111
|
+
keyInputProps,
|
|
112
|
+
valueInputProps,
|
|
69
113
|
}: SecretFieldsProps) {
|
|
114
|
+
const m = useSiaLocale().secretFields;
|
|
115
|
+
const keyPatternMessage = keyPatternMessageProp ?? m.invalidKey;
|
|
116
|
+
const keyLabel = keyLabelProp ?? m.keyLabel;
|
|
117
|
+
const valueLabel = valueLabelProp ?? m.valueLabel;
|
|
118
|
+
const addLabel = addLabelProp ?? m.add;
|
|
70
119
|
const id = useId();
|
|
71
120
|
const [interne, setInterne] = useState<SecretEntry[]>(
|
|
72
121
|
() => defaultValue ?? storedKeys.map((key) => ({ key, value: "" })),
|
|
@@ -89,11 +138,11 @@ export function SecretFields({
|
|
|
89
138
|
return lignes.map((ligne) => {
|
|
90
139
|
const nom = ligne.key.trim();
|
|
91
140
|
if (!nom) return undefined;
|
|
92
|
-
if ((vus.get(nom) ?? 0) > 1) return
|
|
141
|
+
if ((vus.get(nom) ?? 0) > 1) return m.duplicateKey;
|
|
93
142
|
if (keyPattern && !keyPattern.test(nom)) return keyPatternMessage;
|
|
94
143
|
return undefined;
|
|
95
144
|
});
|
|
96
|
-
}, [keyPattern, keyPatternMessage, lignes]);
|
|
145
|
+
}, [keyPattern, keyPatternMessage, lignes, m]);
|
|
97
146
|
|
|
98
147
|
const presents = new Set(lignes.map((ligne) => ligne.key.trim()));
|
|
99
148
|
const proposes = templates.filter((nom) => !presents.has(nom));
|
|
@@ -119,11 +168,12 @@ export function SecretFields({
|
|
|
119
168
|
<div className="sia-secrets__row" key={index}>
|
|
120
169
|
<div className="sia-secrets__key">
|
|
121
170
|
<Input
|
|
122
|
-
value={ligne.key}
|
|
123
171
|
aria-label={`${keyLabel} ${index + 1}`}
|
|
124
|
-
placeholder=
|
|
172
|
+
placeholder={m.keyPlaceholder}
|
|
125
173
|
autoComplete="off"
|
|
126
174
|
spellCheck={false}
|
|
175
|
+
{...keyInputProps}
|
|
176
|
+
value={ligne.key}
|
|
127
177
|
disabled={disabled}
|
|
128
178
|
invalid={Boolean(erreur)}
|
|
129
179
|
{...(erreur ? { "aria-describedby": idErreur } : {})}
|
|
@@ -137,21 +187,23 @@ export function SecretFields({
|
|
|
137
187
|
</div>
|
|
138
188
|
|
|
139
189
|
<PasswordInput
|
|
140
|
-
value={ligne.value}
|
|
141
190
|
aria-label={`${valueLabel} ${ligne.key || index + 1}`}
|
|
142
|
-
placeholder={garde ?
|
|
191
|
+
placeholder={garde ? m.unchangedPlaceholder : valueLabel}
|
|
192
|
+
spellCheck={false}
|
|
193
|
+
{...valueInputProps}
|
|
194
|
+
value={ligne.value}
|
|
143
195
|
// `new-password` : le seul réglage que les navigateurs
|
|
144
196
|
// respectent pour ne pas proposer un mot de passe enregistré.
|
|
145
197
|
autoComplete="new-password"
|
|
146
|
-
spellCheck={false}
|
|
147
198
|
disabled={disabled}
|
|
148
199
|
onChange={(event) => modifier(index, { value: event.target.value })}
|
|
149
200
|
/>
|
|
150
201
|
|
|
151
202
|
<IconButton
|
|
152
|
-
label={`Retirer ${ligne.key || "la ligne"}`}
|
|
153
203
|
icon={<TrashIcon />}
|
|
154
204
|
variant="ghost"
|
|
205
|
+
{...removeButtonProps}
|
|
206
|
+
label={formatMessage(m.removeRow, { name: ligne.key || m.unnamedRow })}
|
|
155
207
|
disabled={disabled}
|
|
156
208
|
onClick={() => changer(lignes.filter((_, i) => i !== index))}
|
|
157
209
|
/>
|
|
@@ -164,8 +216,9 @@ export function SecretFields({
|
|
|
164
216
|
type="button"
|
|
165
217
|
variant="outline"
|
|
166
218
|
size="sm"
|
|
167
|
-
disabled={disabled}
|
|
168
219
|
leftIcon={<PlusIcon />}
|
|
220
|
+
{...addButtonProps}
|
|
221
|
+
disabled={disabled}
|
|
169
222
|
onClick={() => changer([...lignes, { key: "", value: "" }])}
|
|
170
223
|
>
|
|
171
224
|
{addLabel}
|
|
@@ -11,8 +11,9 @@ import {
|
|
|
11
11
|
type ReactNode,
|
|
12
12
|
} from "react";
|
|
13
13
|
import { cn, looseIncludes } from "@sia-ui/utils";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { useSiaLocale } from "@sia-ui/headless";
|
|
15
|
+
import { Overlay, type OverlayPlacement, type OverlayProps } from "../Overlay";
|
|
16
|
+
import { Spinner, type SpinnerProps } from "../Spinner";
|
|
16
17
|
import { ChevronDownIcon, SearchIcon, CheckIcon, XIcon } from "../Icons";
|
|
17
18
|
import "./styles.css";
|
|
18
19
|
|
|
@@ -33,19 +34,31 @@ export interface SelectProps {
|
|
|
33
34
|
value?: string;
|
|
34
35
|
defaultValue?: string;
|
|
35
36
|
onValueChange?: (value: string, option?: SelectOption) => void;
|
|
37
|
+
/** Le texte du champ vide. Par défaut, `select` de la locale. */
|
|
36
38
|
placeholder?: string;
|
|
37
39
|
searchable?: boolean;
|
|
40
|
+
/** Le texte indicatif de la recherche. Par défaut, `searchPlaceholder` de la locale. */
|
|
38
41
|
searchPlaceholder?: string;
|
|
39
42
|
fetcher?: (query: string, signal?: AbortSignal) => Promise<SelectOption[]>;
|
|
40
43
|
debounce?: number;
|
|
44
|
+
/** Pendant le chargement distant. Par défaut, `selectField.loadingOptions` de la locale. */
|
|
41
45
|
loadingText?: ReactNode;
|
|
46
|
+
/** Quand rien ne correspond. Par défaut, `noResults` de la locale. */
|
|
42
47
|
emptyText?: ReactNode;
|
|
48
|
+
/** Quand le chargement échoue. Par défaut, `selectField.loadError` de la locale. */
|
|
43
49
|
errorText?: ReactNode;
|
|
44
50
|
clearable?: boolean;
|
|
45
51
|
disabled?: boolean;
|
|
46
52
|
invalid?: boolean;
|
|
47
53
|
required?: boolean;
|
|
48
54
|
placement?: OverlayPlacement;
|
|
55
|
+
/**
|
|
56
|
+
* Réglages du panneau de la liste — décalage, marge au bord, classe.
|
|
57
|
+
* `matchAnchorWidth: false` le laisse prendre la largeur de ses options.
|
|
58
|
+
*/
|
|
59
|
+
overlayProps?: Partial<Omit<OverlayProps, "open" | "anchorRef" | "onOpenChange" | "children">>;
|
|
60
|
+
/** Réglages de l'indicateur de chargement des options distantes. */
|
|
61
|
+
spinnerProps?: Partial<SpinnerProps>;
|
|
49
62
|
/**
|
|
50
63
|
* Ce que montre le déclencheur. Par défaut, le libellé de l'option.
|
|
51
64
|
*
|
|
@@ -107,19 +120,21 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
107
120
|
value,
|
|
108
121
|
defaultValue = "",
|
|
109
122
|
onValueChange,
|
|
110
|
-
placeholder
|
|
123
|
+
placeholder: placeholderProp,
|
|
111
124
|
searchable = false,
|
|
112
|
-
searchPlaceholder
|
|
125
|
+
searchPlaceholder: searchPlaceholderProp,
|
|
113
126
|
fetcher,
|
|
114
127
|
debounce = 250,
|
|
115
|
-
loadingText
|
|
116
|
-
emptyText
|
|
117
|
-
errorText
|
|
128
|
+
loadingText: loadingTextProp,
|
|
129
|
+
emptyText: emptyTextProp,
|
|
130
|
+
errorText: errorTextProp,
|
|
118
131
|
clearable = false,
|
|
119
132
|
disabled = false,
|
|
120
133
|
invalid = false,
|
|
121
134
|
required = false,
|
|
122
135
|
placement = "bottom-start",
|
|
136
|
+
overlayProps,
|
|
137
|
+
spinnerProps,
|
|
123
138
|
renderValue,
|
|
124
139
|
variant = "default",
|
|
125
140
|
className,
|
|
@@ -129,6 +144,12 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
129
144
|
},
|
|
130
145
|
forwardedRef,
|
|
131
146
|
) {
|
|
147
|
+
const locale = useSiaLocale();
|
|
148
|
+
const placeholder = placeholderProp ?? locale.select;
|
|
149
|
+
const searchPlaceholder = searchPlaceholderProp ?? locale.searchPlaceholder;
|
|
150
|
+
const loadingText = loadingTextProp ?? locale.selectField.loadingOptions;
|
|
151
|
+
const emptyText = emptyTextProp ?? locale.noResults;
|
|
152
|
+
const errorText = errorTextProp ?? locale.selectField.loadError;
|
|
132
153
|
const generatedId = useId();
|
|
133
154
|
const controlId = id ?? `sia-select-${generatedId.replace(/:/g, "")}`;
|
|
134
155
|
const listboxId = `${controlId}-listbox`;
|
|
@@ -295,7 +316,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
295
316
|
<span
|
|
296
317
|
className="sia-select__clear"
|
|
297
318
|
aria-hidden="true"
|
|
298
|
-
title=
|
|
319
|
+
title={locale.selectField.clearSelection}
|
|
299
320
|
onClick={(event) => {
|
|
300
321
|
event.stopPropagation();
|
|
301
322
|
changeValue("");
|
|
@@ -308,12 +329,13 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
308
329
|
</span>
|
|
309
330
|
</button>
|
|
310
331
|
<Overlay
|
|
332
|
+
placement={placement}
|
|
333
|
+
matchAnchorWidth
|
|
334
|
+
{...overlayProps}
|
|
335
|
+
className={cn("sia-select__overlay", overlayProps?.className)}
|
|
311
336
|
open={open}
|
|
312
337
|
anchorRef={triggerRef}
|
|
313
338
|
onOpenChange={setOpen}
|
|
314
|
-
placement={placement}
|
|
315
|
-
matchAnchorWidth
|
|
316
|
-
className="sia-select__overlay"
|
|
317
339
|
>
|
|
318
340
|
<div className="sia-select__panel" onKeyDown={handleKeyDown}>
|
|
319
341
|
{showSearch && (
|
|
@@ -327,7 +349,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
327
349
|
aria-label={searchPlaceholder}
|
|
328
350
|
onChange={(event) => setQuery(event.target.value)}
|
|
329
351
|
/>
|
|
330
|
-
{loading && <Spinner size="xs" label=
|
|
352
|
+
{loading && <Spinner size="xs" label={locale.loading} {...spinnerProps} />}
|
|
331
353
|
</div>
|
|
332
354
|
)}
|
|
333
355
|
<div id={listboxId} role="listbox" aria-busy={loading || undefined} className="sia-select__options">
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type PermissionRule,
|
|
19
19
|
} from "@sia-ui/headless";
|
|
20
20
|
import { ChevronDownIcon } from "../Icons";
|
|
21
|
-
import { Overlay } from "../Overlay";
|
|
21
|
+
import { Overlay, type OverlayProps } from "../Overlay";
|
|
22
22
|
import "./styles.css";
|
|
23
23
|
|
|
24
24
|
export interface SidebarItem {
|
|
@@ -114,6 +114,15 @@ export interface SidebarProps {
|
|
|
114
114
|
/** De quel côté sortent les volets du rail. */
|
|
115
115
|
side?: "left" | "right";
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Réglages des volets du rail — placement, décalage, classe.
|
|
119
|
+
*
|
|
120
|
+
* L'objet descend jusqu'aux nœuds mémoïsés : le passer stable (constante,
|
|
121
|
+
* `useMemo`) plutôt qu'en littéral, sans quoi l'arbre se redessine à
|
|
122
|
+
* chaque rendu du parent.
|
|
123
|
+
*/
|
|
124
|
+
overlayProps?: Partial<Omit<OverlayProps, "open" | "anchorRef" | "onOpenChange" | "children">>;
|
|
125
|
+
|
|
117
126
|
variant?: "default" | "filled" | "floating";
|
|
118
127
|
size?: "sm" | "md" | "lg";
|
|
119
128
|
tone?: ComponentTone;
|
|
@@ -154,6 +163,7 @@ export function Sidebar({
|
|
|
154
163
|
activeKey,
|
|
155
164
|
collapsed = false,
|
|
156
165
|
side = "left",
|
|
166
|
+
overlayProps,
|
|
157
167
|
variant = "default",
|
|
158
168
|
size = "md",
|
|
159
169
|
tone = "primary",
|
|
@@ -232,6 +242,7 @@ export function Sidebar({
|
|
|
232
242
|
basculer,
|
|
233
243
|
renderLink,
|
|
234
244
|
onNavigate,
|
|
245
|
+
overlayProps,
|
|
235
246
|
};
|
|
236
247
|
|
|
237
248
|
return (
|
|
@@ -323,6 +334,7 @@ interface NoeudProps {
|
|
|
323
334
|
basculer: (cle: string, freres: string[]) => void;
|
|
324
335
|
renderLink: SidebarRenderLink | undefined;
|
|
325
336
|
onNavigate: ((item: SidebarItem) => void) | undefined;
|
|
337
|
+
overlayProps: SidebarProps["overlayProps"] | undefined;
|
|
326
338
|
}
|
|
327
339
|
|
|
328
340
|
/**
|
|
@@ -413,6 +425,7 @@ function NoeudEnLigne({
|
|
|
413
425
|
basculer,
|
|
414
426
|
renderLink,
|
|
415
427
|
onNavigate,
|
|
428
|
+
overlayProps,
|
|
416
429
|
}: NoeudProps) {
|
|
417
430
|
const enfants = item.items ?? [];
|
|
418
431
|
const aDesEnfants = enfants.length > 0;
|
|
@@ -523,6 +536,7 @@ function NoeudEnLigne({
|
|
|
523
536
|
basculer,
|
|
524
537
|
renderLink,
|
|
525
538
|
onNavigate,
|
|
539
|
+
overlayProps,
|
|
526
540
|
}}
|
|
527
541
|
/>
|
|
528
542
|
))}
|
|
@@ -542,7 +556,7 @@ function NoeudEnLigne({
|
|
|
542
556
|
* qu'aux rubriques sans enfant.
|
|
543
557
|
*/
|
|
544
558
|
function NoeudRail(props: NoeudProps) {
|
|
545
|
-
const { item, activeKey, side, renderLink, onNavigate } = props;
|
|
559
|
+
const { item, activeKey, side, renderLink, onNavigate, overlayProps } = props;
|
|
546
560
|
const enfants = item.items ?? [];
|
|
547
561
|
const ancre = useRef<HTMLDivElement>(null);
|
|
548
562
|
const { open, triggerProps, panelProps, setOpen } = useHoverIntent({
|
|
@@ -569,13 +583,14 @@ function NoeudRail(props: NoeudProps) {
|
|
|
569
583
|
</button>
|
|
570
584
|
|
|
571
585
|
<Overlay
|
|
572
|
-
open={open}
|
|
573
|
-
anchorRef={ancre}
|
|
574
|
-
onOpenChange={setOpen}
|
|
575
586
|
placement={side === "left" ? "right-start" : "left-start"}
|
|
576
587
|
offset={8}
|
|
577
|
-
className="sia-sidebar-flyout"
|
|
578
588
|
role="group"
|
|
589
|
+
{...overlayProps}
|
|
590
|
+
className={cn("sia-sidebar-flyout", overlayProps?.className)}
|
|
591
|
+
open={open}
|
|
592
|
+
anchorRef={ancre}
|
|
593
|
+
onOpenChange={setOpen}
|
|
579
594
|
>
|
|
580
595
|
<div {...panelProps}>
|
|
581
596
|
<div className="sia-sidebar-flyout__title">
|
|
@@ -610,6 +625,7 @@ function NoeudRail(props: NoeudProps) {
|
|
|
610
625
|
ouvertes: props.ouvertes,
|
|
611
626
|
basculer: props.basculer,
|
|
612
627
|
renderLink,
|
|
628
|
+
overlayProps,
|
|
613
629
|
}}
|
|
614
630
|
collapsed={false}
|
|
615
631
|
onNavigate={(cible) => {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
formatMessage,
|
|
5
|
+
useSiaLocale,
|
|
6
|
+
useSliderState,
|
|
7
|
+
type SliderValue,
|
|
8
|
+
} from "@sia-ui/headless";
|
|
4
9
|
|
|
5
10
|
export type { SliderValue };
|
|
6
11
|
export type SliderRangeValue = [number, number];
|
|
@@ -28,14 +33,22 @@ export interface SliderProps {
|
|
|
28
33
|
showValue?: boolean | undefined;
|
|
29
34
|
/** Rendu de la valeur affichée — devise, unité, arrondi. */
|
|
30
35
|
formatValue?: ((value: number) => string) | undefined;
|
|
31
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Libellé de la poignée basse, annoncé aux lecteurs d'écran. Par défaut,
|
|
38
|
+
* `slider.thumb` de la locale.
|
|
39
|
+
*/
|
|
32
40
|
startLabel?: string | undefined;
|
|
33
41
|
/** Libellé de la poignée haute, en mode plage. */
|
|
34
42
|
endLabel?: string | undefined;
|
|
35
43
|
disabled?: boolean | undefined;
|
|
36
44
|
className?: string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Réglages de la bulle qui affiche la valeur au-dessus de chaque poignée —
|
|
47
|
+
* placement, ton, délai. Son contenu reste la valeur formatée.
|
|
48
|
+
*/
|
|
49
|
+
tooltipProps?: Partial<Omit<TooltipProps, "content" | "children">> | undefined;
|
|
37
50
|
}
|
|
38
|
-
import { Tooltip } from "../Tooltip";
|
|
51
|
+
import { Tooltip, type TooltipProps } from "../Tooltip";
|
|
39
52
|
import "./styles.css";
|
|
40
53
|
|
|
41
54
|
/**
|
|
@@ -62,7 +75,9 @@ export function Slider({
|
|
|
62
75
|
onValueChange,
|
|
63
76
|
disabled,
|
|
64
77
|
className,
|
|
78
|
+
tooltipProps,
|
|
65
79
|
}: SliderProps) {
|
|
80
|
+
const messages = useSiaLocale().slider;
|
|
66
81
|
const trackRef = useRef<HTMLDivElement>(null);
|
|
67
82
|
const [activeThumb, setActiveThumb] = useState<number | null>(null);
|
|
68
83
|
|
|
@@ -150,8 +165,12 @@ export function Slider({
|
|
|
150
165
|
percent={slider.percents[index] ?? 0}
|
|
151
166
|
value={one}
|
|
152
167
|
label={formatValue ? formatValue(one) : String(one)}
|
|
153
|
-
name={
|
|
168
|
+
name={
|
|
169
|
+
(index === 0 ? startLabel : endLabel) ??
|
|
170
|
+
formatMessage(messages.thumb, { index: index + 1 })
|
|
171
|
+
}
|
|
154
172
|
disabled={disabled}
|
|
173
|
+
tooltipProps={tooltipProps}
|
|
155
174
|
onGrab={() => {
|
|
156
175
|
if (!disabled) setActiveThumb(index);
|
|
157
176
|
}}
|
|
@@ -167,6 +186,7 @@ function Thumb({
|
|
|
167
186
|
label,
|
|
168
187
|
name,
|
|
169
188
|
disabled,
|
|
189
|
+
tooltipProps,
|
|
170
190
|
onGrab,
|
|
171
191
|
}: {
|
|
172
192
|
percent: number;
|
|
@@ -174,10 +194,11 @@ function Thumb({
|
|
|
174
194
|
label: string;
|
|
175
195
|
name: string;
|
|
176
196
|
disabled?: boolean | undefined;
|
|
197
|
+
tooltipProps?: SliderProps["tooltipProps"];
|
|
177
198
|
onGrab: () => void;
|
|
178
199
|
}) {
|
|
179
200
|
return (
|
|
180
|
-
<Tooltip content={label}
|
|
201
|
+
<Tooltip placement="top" {...tooltipProps} content={label}>
|
|
181
202
|
<span
|
|
182
203
|
role="slider"
|
|
183
204
|
aria-valuenow={value}
|