@sia-ui/cli 0.7.0 → 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 +12 -0
- package/README.md +12 -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 +44 -12
- package/dist/registry/package/components/Select/styles.css +6 -3
- 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
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { useState, type ReactNode, type TextareaHTMLAttributes } from "react";
|
|
2
|
-
import { Textarea } from "../Textarea";
|
|
2
|
+
import { Textarea, type TextareaProps } from "../Textarea";
|
|
3
3
|
import { cn } from "@sia-ui/utils";
|
|
4
|
-
|
|
4
|
+
import { useSiaLocale } from "@sia-ui/headless";
|
|
5
|
+
export interface MarkdownEditorProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "value" | "defaultValue" | "onChange"> {
|
|
6
|
+
value?: string;
|
|
7
|
+
defaultValue?: string;
|
|
8
|
+
onValueChange?: (value: string) => void;
|
|
9
|
+
preview?: (value: string) => ReactNode;
|
|
10
|
+
/**
|
|
11
|
+
* La zone de saisie elle-même : `resize`, `invalid`, classe. La valeur et
|
|
12
|
+
* la saisie restent à l'éditeur, qui les partage avec l'aperçu.
|
|
13
|
+
*/
|
|
14
|
+
textareaProps?: Partial<Omit<TextareaProps, "value" | "defaultValue" | "onChange">>;
|
|
15
|
+
}
|
|
5
16
|
/**
|
|
6
17
|
* Une saisie Markdown, avec son aperçu.
|
|
7
18
|
*
|
|
@@ -9,4 +20,4 @@ export interface MarkdownEditorProps extends Omit<TextareaHTMLAttributes<HTMLTex
|
|
|
9
20
|
* veut. Embarquer un moteur Markdown imposerait le nôtre à un projet qui a
|
|
10
21
|
* déjà le sien — et ferait grossir le paquet pour tous les autres.
|
|
11
22
|
*/
|
|
12
|
-
export function MarkdownEditor({ value, defaultValue = "", onValueChange, preview, className, ...props }: MarkdownEditorProps) { const [internal, setInternal] = useState(defaultValue); const [mode, setMode] = useState<"write" | "preview">("write"); const current = value ?? internal; const update = (next: string) => { if (value === undefined) setInternal(next); onValueChange?.(next); }; return <div className={cn("sia-markdown-editor", className)}><div className="sia-editor-toolbar"><button type="button" aria-pressed={mode === "write"} onClick={() => setMode("write")}
|
|
23
|
+
export function MarkdownEditor({ value, defaultValue = "", onValueChange, preview, className, textareaProps, ...props }: MarkdownEditorProps) { const messages = useSiaLocale().markdownEditor; const [internal, setInternal] = useState(defaultValue); const [mode, setMode] = useState<"write" | "preview">("write"); const current = value ?? internal; const update = (next: string) => { if (value === undefined) setInternal(next); onValueChange?.(next); }; return <div className={cn("sia-markdown-editor", className)}><div className="sia-editor-toolbar"><button type="button" aria-pressed={mode === "write"} onClick={() => setMode("write")}>{messages.write}</button><button type="button" aria-pressed={mode === "preview"} onClick={() => setMode("preview")}>{messages.preview}</button></div>{mode === "write" ? <Textarea {...props} {...textareaProps} value={current} onChange={(event) => update(event.target.value)} /> : <div className="sia-editor-preview">{preview ? preview(current) : <pre>{current}</pre>}</div>}</div>; }
|
|
@@ -41,9 +41,13 @@ export function Modal({
|
|
|
41
41
|
const titleId = useId();
|
|
42
42
|
const descriptionId = useId();
|
|
43
43
|
const dialogRef = useRef<HTMLElement>(null);
|
|
44
|
+
// L'instant d'ouverture : un appui sur le voile qui le suit de près est le
|
|
45
|
+
// second clic d'un double clic, ou un rebond — pas une demande de fermeture.
|
|
46
|
+
const ouverteA = useRef(0);
|
|
44
47
|
|
|
45
48
|
useEffect(() => {
|
|
46
49
|
if (!open) return;
|
|
50
|
+
ouverteA.current = Date.now();
|
|
47
51
|
const previousFocus = document.activeElement as HTMLElement | null;
|
|
48
52
|
const previousOverflow = document.body.style.overflow;
|
|
49
53
|
document.body.style.overflow = "hidden";
|
|
@@ -85,7 +89,9 @@ export function Modal({
|
|
|
85
89
|
<div
|
|
86
90
|
className="sia-modal-backdrop"
|
|
87
91
|
onMouseDown={(event) => {
|
|
88
|
-
if (closeOnBackdrop
|
|
92
|
+
if (!closeOnBackdrop || event.target !== event.currentTarget) return;
|
|
93
|
+
if (Date.now() - ouverteA.current < 400) return;
|
|
94
|
+
onOpenChange(false);
|
|
89
95
|
}}
|
|
90
96
|
>
|
|
91
97
|
<section
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMemo, useState } from "react";
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
|
+
import { formatMessage, useSiaLocale } from "@sia-ui/headless";
|
|
3
4
|
import type { SelectOption } from "../Select";
|
|
4
5
|
import "./styles.css";
|
|
5
6
|
|
|
@@ -11,7 +12,10 @@ export interface MultiSelectProps {
|
|
|
11
12
|
value?: string[];
|
|
12
13
|
defaultValue?: string[];
|
|
13
14
|
onValueChange?: (value: string[]) => void;
|
|
15
|
+
/** Le texte du champ vide. Par défaut, `select` de la locale. */
|
|
14
16
|
placeholder?: string;
|
|
17
|
+
/** Le texte indicatif de la recherche. Par défaut, `searchPlaceholder` de la locale. */
|
|
18
|
+
searchPlaceholder?: string;
|
|
15
19
|
searchable?: boolean;
|
|
16
20
|
disabled?: boolean;
|
|
17
21
|
className?: string;
|
|
@@ -33,12 +37,16 @@ export function MultiSelect({
|
|
|
33
37
|
value,
|
|
34
38
|
defaultValue = [],
|
|
35
39
|
onValueChange,
|
|
36
|
-
placeholder
|
|
40
|
+
placeholder: placeholderProp,
|
|
41
|
+
searchPlaceholder: searchPlaceholderProp,
|
|
37
42
|
searchable = true,
|
|
38
43
|
disabled,
|
|
39
44
|
className,
|
|
40
45
|
...ariaProps
|
|
41
46
|
}: MultiSelectProps) {
|
|
47
|
+
const locale = useSiaLocale();
|
|
48
|
+
const placeholder = placeholderProp ?? locale.select;
|
|
49
|
+
const searchPlaceholder = searchPlaceholderProp ?? locale.searchPlaceholder;
|
|
42
50
|
const [internal, setInternal] = useState(defaultValue);
|
|
43
51
|
const [open, setOpen] = useState(false);
|
|
44
52
|
const [query, setQuery] = useState("");
|
|
@@ -92,7 +100,9 @@ export function MultiSelect({
|
|
|
92
100
|
{option.label}
|
|
93
101
|
<button
|
|
94
102
|
type="button"
|
|
95
|
-
aria-label={
|
|
103
|
+
aria-label={formatMessage(locale.multiSelect.removeItem, {
|
|
104
|
+
name: String(option.label),
|
|
105
|
+
})}
|
|
96
106
|
onClick={(event) => {
|
|
97
107
|
event.stopPropagation();
|
|
98
108
|
update(current.filter((item) => item !== option.value));
|
|
@@ -112,7 +122,8 @@ export function MultiSelect({
|
|
|
112
122
|
{searchable && (
|
|
113
123
|
<input
|
|
114
124
|
value={query}
|
|
115
|
-
placeholder=
|
|
125
|
+
placeholder={searchPlaceholder}
|
|
126
|
+
aria-label={searchPlaceholder}
|
|
116
127
|
onChange={(event) => setQuery(event.target.value)}
|
|
117
128
|
/>
|
|
118
129
|
)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useRef, useState, type ClipboardEvent, type KeyboardEvent } from "react";
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
|
+
import { formatMessage, useSiaLocale } from "@sia-ui/headless";
|
|
3
4
|
import "./styles.css";
|
|
4
5
|
export interface OtpInputProps { id?: string; value?: string; defaultValue?: string; onValueChange?: (value: string) => void; length?: number; inputMode?: "numeric" | "text"; mask?: boolean; disabled?: boolean; className?: string; "aria-describedby"?: string; "aria-labelledby"?: string; "aria-invalid"?: boolean; "aria-required"?: boolean; }
|
|
5
6
|
/**
|
|
@@ -9,4 +10,4 @@ export interface OtpInputProps { id?: string; value?: string; defaultValue?: str
|
|
|
9
10
|
* ainsi qu'on le récupère d'un SMS, et le forcer à être retapé serait une
|
|
10
11
|
* punition pour rien.
|
|
11
12
|
*/
|
|
12
|
-
export function OtpInput({ id, value, defaultValue = "", onValueChange, length = 6, inputMode = "numeric", mask, disabled, className, ...ariaProps }: OtpInputProps) { const [internal, setInternal] = useState(defaultValue); const refs = useRef<Array<HTMLInputElement | null>>([]); const current = (value ?? internal).padEnd(length, " ").slice(0, length).split(""); const update = (chars: string[]) => { const next = chars.join("").trimEnd(); if (value === undefined) setInternal(next); onValueChange?.(next); }; const accept = (text: string) => inputMode === "numeric" ? text.replace(/\D/g, "") : text; const paste = (event: ClipboardEvent<HTMLDivElement>) => { event.preventDefault(); const next = accept(event.clipboardData.getData("text")).slice(0, length).padEnd(length, " ").split(""); update(next); refs.current[Math.min(next.join("").trimEnd().length, length - 1)]?.focus(); }; return <div id={id} className={cn("sia-otp-input", className)} onPaste={paste} {...ariaProps}>{current.map((char, index) => <input key={index} ref={(node) => { refs.current[index] = node; }} value={char.trim()} type={mask ? "password" : "text"} inputMode={inputMode} maxLength={1} disabled={disabled} aria-label={
|
|
13
|
+
export function OtpInput({ id, value, defaultValue = "", onValueChange, length = 6, inputMode = "numeric", mask, disabled, className, ...ariaProps }: OtpInputProps) { const messages = useSiaLocale().otpInput; const [internal, setInternal] = useState(defaultValue); const refs = useRef<Array<HTMLInputElement | null>>([]); const current = (value ?? internal).padEnd(length, " ").slice(0, length).split(""); const update = (chars: string[]) => { const next = chars.join("").trimEnd(); if (value === undefined) setInternal(next); onValueChange?.(next); }; const accept = (text: string) => inputMode === "numeric" ? text.replace(/\D/g, "") : text; const paste = (event: ClipboardEvent<HTMLDivElement>) => { event.preventDefault(); const next = accept(event.clipboardData.getData("text")).slice(0, length).padEnd(length, " ").split(""); update(next); refs.current[Math.min(next.join("").trimEnd().length, length - 1)]?.focus(); }; return <div id={id} className={cn("sia-otp-input", className)} onPaste={paste} {...ariaProps}>{current.map((char, index) => <input key={index} ref={(node) => { refs.current[index] = node; }} value={char.trim()} type={mask ? "password" : "text"} inputMode={inputMode} maxLength={1} disabled={disabled} aria-label={formatMessage(messages.character, { index: index + 1 })} onChange={(event) => { const next = [...current]; next[index] = accept(event.target.value).slice(-1) || " "; update(next); if (next[index]?.trim()) refs.current[index + 1]?.focus(); }} onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => { if (event.key === "Backspace" && !current[index]?.trim()) refs.current[index - 1]?.focus(); if (event.key === "ArrowLeft") refs.current[index - 1]?.focus(); if (event.key === "ArrowRight") refs.current[index + 1]?.focus(); }} />)}</div>; }
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { cn } from "@sia-ui/utils";
|
|
10
10
|
import { paginationItems, useSiaLocale } from "@sia-ui/headless";
|
|
11
11
|
import { ChevronLeftIcon, ChevronRightIcon } from "../Icons";
|
|
12
|
-
import { Select } from "../Select";
|
|
12
|
+
import { Select, type SelectProps } from "../Select";
|
|
13
13
|
import "./styles.css";
|
|
14
14
|
|
|
15
15
|
export interface PaginationProps {
|
|
@@ -55,6 +55,15 @@ export interface PaginationProps {
|
|
|
55
55
|
* Le choix des tailles et leur tri sont déjà faits.
|
|
56
56
|
*/
|
|
57
57
|
renderPageSize?: (context: PageSizeContext) => ReactNode;
|
|
58
|
+
/**
|
|
59
|
+
* Le `Select` de taille de page : placement, variante, classe.
|
|
60
|
+
*
|
|
61
|
+
* La valeur, les options et le rappel restent calculés ici — on les règle
|
|
62
|
+
* par `pageSize`, `pageSizeOptions` et `onPageSizeChange`.
|
|
63
|
+
*/
|
|
64
|
+
pageSizeSelectProps?: Partial<
|
|
65
|
+
Omit<SelectProps, "value" | "defaultValue" | "options" | "children" | "onValueChange">
|
|
66
|
+
>;
|
|
58
67
|
|
|
59
68
|
ariaLabel?: string;
|
|
60
69
|
className?: string;
|
|
@@ -91,6 +100,7 @@ export function Pagination({
|
|
|
91
100
|
pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
|
|
92
101
|
pageSizeLabel,
|
|
93
102
|
renderPageSize,
|
|
103
|
+
pageSizeSelectProps,
|
|
94
104
|
ariaLabel,
|
|
95
105
|
className,
|
|
96
106
|
}: PaginationProps) {
|
|
@@ -147,7 +157,7 @@ export function Pagination({
|
|
|
147
157
|
compact && "sia-pagination--compact",
|
|
148
158
|
className,
|
|
149
159
|
)}
|
|
150
|
-
aria-label={ariaLabel ??
|
|
160
|
+
aria-label={ariaLabel ?? locale.pagination.label}
|
|
151
161
|
>
|
|
152
162
|
{/*
|
|
153
163
|
Deux groupes, qui passent à la ligne chacun d'un seul tenant : la
|
|
@@ -223,6 +233,7 @@ export function Pagination({
|
|
|
223
233
|
variant="embedded"
|
|
224
234
|
aria-label={tailleLibelle}
|
|
225
235
|
placement="top-end"
|
|
236
|
+
{...pageSizeSelectProps}
|
|
226
237
|
value={String(pageSize)}
|
|
227
238
|
options={tailles.map((taille) => ({
|
|
228
239
|
value: String(taille),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { forwardRef, useMemo, useState } from "react";
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
|
-
import { useSiaLocale } from "@sia-ui/headless";
|
|
3
|
+
import { useSiaLocale, type PasswordInputMessages } from "@sia-ui/headless";
|
|
4
4
|
import { Input, type InputProps } from "../Input";
|
|
5
5
|
import { EyeIcon, EyeOffIcon } from "../Icons";
|
|
6
6
|
import "./styles.css";
|
|
@@ -18,13 +18,12 @@ export interface PasswordInputProps extends Omit<InputProps, "type" | "right"> {
|
|
|
18
18
|
hideToggle?: boolean;
|
|
19
19
|
/**
|
|
20
20
|
* Remplace l'évaluation par défaut. Un projet qui utilise `zxcvbn` branche
|
|
21
|
-
* son résultat ici; le composant ne dépend de rien.
|
|
21
|
+
* son résultat ici; le composant ne dépend de rien. Les libellés de
|
|
22
|
+
* l'évaluation par défaut viennent de la locale (`passwordInput`).
|
|
22
23
|
*/
|
|
23
24
|
evaluate?: (value: string) => PasswordStrength;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
const LABELS = ["Très faible", "Faible", "Moyen", "Bon", "Excellent"];
|
|
27
|
-
|
|
28
27
|
/**
|
|
29
28
|
* L'évaluation par défaut : longueur et variété.
|
|
30
29
|
*
|
|
@@ -32,7 +31,10 @@ const LABELS = ["Très faible", "Faible", "Moyen", "Bon", "Excellent"];
|
|
|
32
31
|
* `zxcvbn`, qui pèse 800 ko. Elle décourage les mots de passe visiblement
|
|
33
32
|
* courts, ce qui est déjà l'essentiel du gain.
|
|
34
33
|
*/
|
|
35
|
-
function defaultEvaluate(
|
|
34
|
+
function defaultEvaluate(
|
|
35
|
+
value: string,
|
|
36
|
+
messages: PasswordInputMessages,
|
|
37
|
+
): PasswordStrength {
|
|
36
38
|
if (!value) return { score: 0, label: "" };
|
|
37
39
|
|
|
38
40
|
let score = 0;
|
|
@@ -41,7 +43,14 @@ function defaultEvaluate(value: string): PasswordStrength {
|
|
|
41
43
|
if (/[a-z]/.test(value) && /[A-Z]/.test(value)) score += 1;
|
|
42
44
|
if (/\d/.test(value) && /[^\w\s]/.test(value)) score += 1;
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
const labels = [
|
|
47
|
+
messages.strengthVeryWeak,
|
|
48
|
+
messages.strengthWeak,
|
|
49
|
+
messages.strengthFair,
|
|
50
|
+
messages.strengthGood,
|
|
51
|
+
messages.strengthExcellent,
|
|
52
|
+
];
|
|
53
|
+
return { score, label: labels[score] ?? "" };
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
/**
|
|
@@ -56,7 +65,7 @@ export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
|
|
|
56
65
|
{
|
|
57
66
|
showStrength = false,
|
|
58
67
|
hideToggle = false,
|
|
59
|
-
evaluate
|
|
68
|
+
evaluate,
|
|
60
69
|
className,
|
|
61
70
|
value,
|
|
62
71
|
onChange,
|
|
@@ -70,8 +79,13 @@ export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
|
|
|
70
79
|
const current = value !== undefined ? String(value) : internal;
|
|
71
80
|
|
|
72
81
|
const strength = useMemo(
|
|
73
|
-
() =>
|
|
74
|
-
|
|
82
|
+
() =>
|
|
83
|
+
showStrength
|
|
84
|
+
? evaluate
|
|
85
|
+
? evaluate(current)
|
|
86
|
+
: defaultEvaluate(current, locale.passwordInput)
|
|
87
|
+
: null,
|
|
88
|
+
[current, evaluate, locale.passwordInput, showStrength],
|
|
75
89
|
);
|
|
76
90
|
|
|
77
91
|
return (
|
|
@@ -2,7 +2,7 @@ import { forwardRef, useMemo, useState } from "react";
|
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
3
|
import { useSiaLocale } from "@sia-ui/headless";
|
|
4
4
|
import { Input, type InputProps } from "../Input";
|
|
5
|
-
import { Select, type SelectOption } from "../Select";
|
|
5
|
+
import { Select, type SelectOption, type SelectProps } from "../Select";
|
|
6
6
|
import "./styles.css";
|
|
7
7
|
|
|
8
8
|
export interface PhoneCountry {
|
|
@@ -10,7 +10,11 @@ export interface PhoneCountry {
|
|
|
10
10
|
code: string;
|
|
11
11
|
/** Indicatif, avec le `+`. */
|
|
12
12
|
dial: string;
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Le nom affiché. Par défaut, celui que donne `Intl.DisplayNames` dans la
|
|
15
|
+
* langue de la locale — « Cameroun », « Cameroon ».
|
|
16
|
+
*/
|
|
17
|
+
label?: string;
|
|
14
18
|
flag: string;
|
|
15
19
|
/** Nombre de chiffres attendus après l'indicatif. */
|
|
16
20
|
digits?: number;
|
|
@@ -26,20 +30,20 @@ export interface PhoneCountry {
|
|
|
26
30
|
* entièrement quand un projet en a besoin d'une autre.
|
|
27
31
|
*/
|
|
28
32
|
export const DEFAULT_COUNTRIES: PhoneCountry[] = [
|
|
29
|
-
{ code: "CM", dial: "+237",
|
|
30
|
-
{ code: "CI", dial: "+225",
|
|
31
|
-
{ code: "SN", dial: "+221",
|
|
32
|
-
{ code: "GA", dial: "+241",
|
|
33
|
-
{ code: "CG", dial: "+242",
|
|
34
|
-
{ code: "CD", dial: "+243",
|
|
35
|
-
{ code: "BF", dial: "+226",
|
|
36
|
-
{ code: "ML", dial: "+223",
|
|
37
|
-
{ code: "BJ", dial: "+229",
|
|
38
|
-
{ code: "TG", dial: "+228",
|
|
39
|
-
{ code: "TD", dial: "+235",
|
|
40
|
-
{ code: "FR", dial: "+33",
|
|
41
|
-
{ code: "BE", dial: "+32",
|
|
42
|
-
{ code: "CA", dial: "+1",
|
|
33
|
+
{ code: "CM", dial: "+237", flag: "🇨🇲", digits: 9, groups: [1, 2, 2, 2, 2] },
|
|
34
|
+
{ code: "CI", dial: "+225", flag: "🇨🇮", digits: 10, groups: [2, 2, 2, 2, 2] },
|
|
35
|
+
{ code: "SN", dial: "+221", flag: "🇸🇳", digits: 9, groups: [2, 3, 2, 2] },
|
|
36
|
+
{ code: "GA", dial: "+241", flag: "🇬🇦", digits: 8, groups: [2, 2, 2, 2] },
|
|
37
|
+
{ code: "CG", dial: "+242", flag: "🇨🇬", digits: 9, groups: [2, 3, 2, 2] },
|
|
38
|
+
{ code: "CD", dial: "+243", flag: "🇨🇩", digits: 9, groups: [3, 3, 3] },
|
|
39
|
+
{ code: "BF", dial: "+226", flag: "🇧🇫", digits: 8, groups: [2, 2, 2, 2] },
|
|
40
|
+
{ code: "ML", dial: "+223", flag: "🇲🇱", digits: 8, groups: [2, 2, 2, 2] },
|
|
41
|
+
{ code: "BJ", dial: "+229", flag: "🇧🇯", digits: 10, groups: [2, 2, 2, 2, 2] },
|
|
42
|
+
{ code: "TG", dial: "+228", flag: "🇹🇬", digits: 8, groups: [2, 2, 2, 2] },
|
|
43
|
+
{ code: "TD", dial: "+235", flag: "🇹🇩", digits: 8, groups: [2, 2, 2, 2] },
|
|
44
|
+
{ code: "FR", dial: "+33", flag: "🇫🇷", digits: 9, groups: [1, 2, 2, 2, 2] },
|
|
45
|
+
{ code: "BE", dial: "+32", flag: "🇧🇪", digits: 9, groups: [3, 2, 2, 2] },
|
|
46
|
+
{ code: "CA", dial: "+1", flag: "🇨🇦", digits: 10, groups: [3, 3, 4] },
|
|
43
47
|
];
|
|
44
48
|
|
|
45
49
|
export interface PhoneInputProps
|
|
@@ -54,6 +58,36 @@ export interface PhoneInputProps
|
|
|
54
58
|
defaultCountry?: string;
|
|
55
59
|
/** Verrouille le pays et masque le sélecteur. */
|
|
56
60
|
lockCountry?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Le sélecteur d'indicatif : placement, recherche, rendu, classe.
|
|
63
|
+
*
|
|
64
|
+
* La valeur, les options et le rappel viennent de `countries` et du
|
|
65
|
+
* numéro ; `disabled` suit celui du champ — pour figer le pays seul,
|
|
66
|
+
* `lockCountry`.
|
|
67
|
+
*/
|
|
68
|
+
countrySelectProps?: Partial<
|
|
69
|
+
Omit<
|
|
70
|
+
SelectProps,
|
|
71
|
+
"value" | "defaultValue" | "options" | "children" | "onValueChange" | "disabled"
|
|
72
|
+
>
|
|
73
|
+
>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Un `Intl.DisplayNames` par langue, pour toute la durée de la page.
|
|
77
|
+
const regionNamesCache = new Map<string, Intl.DisplayNames | undefined>();
|
|
78
|
+
|
|
79
|
+
/** Les noms de pays dans une langue — `undefined` si Intl ne les connaît pas. */
|
|
80
|
+
function regionNames(language: string) {
|
|
81
|
+
if (!regionNamesCache.has(language)) {
|
|
82
|
+
let names: Intl.DisplayNames | undefined;
|
|
83
|
+
try {
|
|
84
|
+
names = new Intl.DisplayNames([language], { type: "region" });
|
|
85
|
+
} catch {
|
|
86
|
+
names = undefined;
|
|
87
|
+
}
|
|
88
|
+
regionNamesCache.set(language, names);
|
|
89
|
+
}
|
|
90
|
+
return regionNamesCache.get(language);
|
|
57
91
|
}
|
|
58
92
|
|
|
59
93
|
function digitsOnly(value: string) {
|
|
@@ -101,6 +135,7 @@ export const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
|
101
135
|
countries = DEFAULT_COUNTRIES,
|
|
102
136
|
defaultCountry,
|
|
103
137
|
lockCountry = false,
|
|
138
|
+
countrySelectProps,
|
|
104
139
|
disabled,
|
|
105
140
|
className,
|
|
106
141
|
...props
|
|
@@ -130,16 +165,16 @@ export const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
|
130
165
|
: digitsOnly(value);
|
|
131
166
|
|
|
132
167
|
// La recherche porte sur le nom, l'indicatif et le code ISO : « cam »,
|
|
133
|
-
// « 237 » et « cm » trouvent tous le Cameroun.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
);
|
|
168
|
+
// « 237 » et « cm » trouvent tous le Cameroun. Le nom vient d'Intl dans
|
|
169
|
+
// la langue de la locale, sauf si le pays en porte un.
|
|
170
|
+
const options = useMemo<SelectOption[]>(() => {
|
|
171
|
+
const names = regionNames(locale.language);
|
|
172
|
+
return countries.map((entry) => ({
|
|
173
|
+
value: entry.code,
|
|
174
|
+
label: `${entry.flag} ${entry.label ?? names?.of(entry.code) ?? entry.code} (${entry.dial})`,
|
|
175
|
+
keywords: [entry.code],
|
|
176
|
+
}));
|
|
177
|
+
}, [countries, locale.language]);
|
|
143
178
|
|
|
144
179
|
const commit = (next: string) => {
|
|
145
180
|
if (!controlled) setInternal(next);
|
|
@@ -174,11 +209,7 @@ export const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
|
174
209
|
<Select
|
|
175
210
|
variant="embedded"
|
|
176
211
|
searchable
|
|
177
|
-
className="sia-phone__country"
|
|
178
212
|
aria-label={locale.countryCode}
|
|
179
|
-
options={options}
|
|
180
|
-
value={country.code}
|
|
181
|
-
disabled={disabled ?? false}
|
|
182
213
|
renderValue={() => (
|
|
183
214
|
<span className="sia-phone__value">
|
|
184
215
|
<span className="sia-phone__flag" aria-hidden="true">
|
|
@@ -187,6 +218,11 @@ export const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
|
187
218
|
<span className="sia-phone__dial">{country.dial}</span>
|
|
188
219
|
</span>
|
|
189
220
|
)}
|
|
221
|
+
{...countrySelectProps}
|
|
222
|
+
className={cn("sia-phone__country", countrySelectProps?.className)}
|
|
223
|
+
options={options}
|
|
224
|
+
value={country.code}
|
|
225
|
+
disabled={disabled ?? false}
|
|
190
226
|
onValueChange={(code) => {
|
|
191
227
|
const next = countries.find((entry) => entry.code === code);
|
|
192
228
|
if (!next) return;
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { cloneElement, isValidElement, useRef, useState, type ReactElement, type ReactNode } from "react";
|
|
2
2
|
import { cn, toneClass } from "@sia-ui/utils";
|
|
3
3
|
import type { ComponentTone } from "@sia-ui/tokens";
|
|
4
|
-
import { Overlay, type OverlayPlacement } from "../Overlay";
|
|
4
|
+
import { Overlay, type OverlayPlacement, type OverlayProps } from "../Overlay";
|
|
5
5
|
import "./styles.css";
|
|
6
6
|
|
|
7
|
-
export interface PopoverProps {
|
|
7
|
+
export interface PopoverProps {
|
|
8
|
+
content: ReactNode;
|
|
9
|
+
title?: ReactNode;
|
|
10
|
+
children: ReactElement<{ onClick?: () => void; "aria-expanded"?: boolean }>;
|
|
11
|
+
open?: boolean;
|
|
12
|
+
defaultOpen?: boolean;
|
|
13
|
+
onOpenChange?: (open: boolean) => void;
|
|
14
|
+
placement?: OverlayPlacement;
|
|
15
|
+
/** Réglages du panneau flottant — décalage, marge au bord, rôle, classe. */
|
|
16
|
+
overlayProps?: Partial<Omit<OverlayProps, "open" | "anchorRef" | "onOpenChange" | "children">>;
|
|
17
|
+
trigger?: "click" | "hover";
|
|
18
|
+
variant?: "solid" | "outline";
|
|
19
|
+
tone?: ComponentTone;
|
|
20
|
+
disabled?: boolean;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
8
23
|
/**
|
|
9
24
|
* Un panneau attaché à un élément, avec du contenu qu'on peut viser.
|
|
10
25
|
*
|
|
@@ -12,8 +27,8 @@ export interface PopoverProps { content: ReactNode; title?: ReactNode; children:
|
|
|
12
27
|
* bouton, un petit formulaire. Une infobulle qui contient un bouton est une
|
|
13
28
|
* infobulle cassée — elle disparaît dès qu'on va vers lui.
|
|
14
29
|
*/
|
|
15
|
-
export function Popover({ content, title, children, open: openProp, defaultOpen = false, onOpenChange, placement = "bottom-start", trigger = "click", variant = "outline", tone = "neutral", disabled = false, className }: PopoverProps) {
|
|
30
|
+
export function Popover({ content, title, children, open: openProp, defaultOpen = false, onOpenChange, placement = "bottom-start", overlayProps, trigger = "click", variant = "outline", tone = "neutral", disabled = false, className }: PopoverProps) {
|
|
16
31
|
const anchorRef = useRef<HTMLElement>(null); const [internalOpen, setInternalOpen] = useState(defaultOpen); const open = openProp ?? internalOpen; const setOpen = (next: boolean) => { if (disabled) return; if (openProp === undefined) setInternalOpen(next); onOpenChange?.(next); };
|
|
17
32
|
const child = isValidElement(children) ? cloneElement(children, { ref: anchorRef, "aria-expanded": open, ...(trigger === "click" ? { onClick: () => { children.props.onClick?.(); setOpen(!open); } } : {}) } as never) : children;
|
|
18
|
-
return <span className="sia-popover-anchor" onMouseEnter={trigger === "hover" ? () => setOpen(true) : undefined} onMouseLeave={trigger === "hover" ? () => setOpen(false) : undefined}>{child}<Overlay
|
|
33
|
+
return <span className="sia-popover-anchor" onMouseEnter={trigger === "hover" ? () => setOpen(true) : undefined} onMouseLeave={trigger === "hover" ? () => setOpen(false) : undefined}>{child}<Overlay placement={placement} role="dialog" {...overlayProps} className={cn("sia-popover", `sia-popover--${variant}`, toneClass("sia-tone", tone), className, overlayProps?.className)} open={open} anchorRef={anchorRef} onOpenChange={setOpen}>{title && <strong className="sia-popover__title">{title}</strong>}<div className="sia-popover__content">{content}</div></Overlay></span>;
|
|
19
34
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
|
+
import { useSiaLocale } from "@sia-ui/headless";
|
|
3
4
|
import "./styles.css";
|
|
4
5
|
|
|
5
|
-
export interface QRCodeProps { value: string; size?: number; label?: string; render?: (value: string, size: number) => ReactNode; className?: string; }
|
|
6
|
+
export interface QRCodeProps { value: string; size?: number; /** Par défaut, celui de la locale. */ label?: string; render?: (value: string, size: number) => ReactNode; className?: string; }
|
|
6
7
|
/**
|
|
7
8
|
* Un code à scanner.
|
|
8
9
|
*
|
|
@@ -10,4 +11,4 @@ export interface QRCodeProps { value: string; size?: number; label?: string; ren
|
|
|
10
11
|
* dépendance à tous ceux qui ne s'en servent pas, alors que les projets qui
|
|
11
12
|
* en ont besoin ont déjà le leur.
|
|
12
13
|
*/
|
|
13
|
-
export function QRCode({ value, size = 160, label
|
|
14
|
+
export function QRCode({ value, size = 160, label: labelProp, render, className }: QRCodeProps) { const m = useSiaLocale().qrCode; const label = labelProp ?? m.label; return <figure className={cn("sia-qr-code", className)} aria-label={label} style={{ width: size }}><div className="sia-qr-code__frame" style={{ width: size, height: size }}>{render ? render(value, size) : <div className="sia-qr-code__adapter"><strong>QR</strong><span>{m.encoderRequired}</span></div>}</div><figcaption>{value}</figcaption></figure>; }
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
.sia-radio-group { display:flex; gap:.75rem; padding:0; margin:0; border:0; }
|
|
2
2
|
.sia-radio-group--vertical { flex-direction:column; }
|
|
3
3
|
.sia-radio-group--horizontal { flex-direction:row; flex-wrap:wrap; }
|
|
4
|
+
/* Des `RadioButton` forment un seul contrôle segmenté : bordures communes,
|
|
5
|
+
coins arrondis aux seules extrémités. L'écart entre radios les séparait. */
|
|
6
|
+
.sia-radio-group:has(> .sia-radio-button) { gap:0; }
|
|
4
7
|
.sia-radio-group__legend { margin-bottom:.65rem; font-weight:600; }
|
|
5
8
|
.sia-radio { --sia-accent:var(--sia-primary); display:inline-flex; align-items:center; gap:.55rem; cursor:pointer; }
|
|
6
9
|
.sia-radio__dot { display:grid; place-items:center; width:1.1rem; height:1.1rem; border:1.5px solid var(--sia-border); border-radius:50%; }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
|
+
import { formatMessage, useSiaLocale } from "@sia-ui/headless";
|
|
3
4
|
import { StarIcon } from "../Icons";
|
|
4
5
|
import "./styles.css";
|
|
5
6
|
export interface RatingProps {
|
|
@@ -12,6 +13,7 @@ export interface RatingProps {
|
|
|
12
13
|
allowHalf?: boolean;
|
|
13
14
|
allowClear?: boolean;
|
|
14
15
|
disabled?: boolean;
|
|
16
|
+
/** Le nom du groupe d'étoiles. Par défaut, celui de la locale. */
|
|
15
17
|
label?: string;
|
|
16
18
|
className?: string;
|
|
17
19
|
"aria-describedby"?: string;
|
|
@@ -34,10 +36,12 @@ export function Rating({
|
|
|
34
36
|
allowHalf = false,
|
|
35
37
|
allowClear = true,
|
|
36
38
|
disabled,
|
|
37
|
-
label
|
|
39
|
+
label: labelProp,
|
|
38
40
|
className,
|
|
39
41
|
...ariaProps
|
|
40
42
|
}: RatingProps) {
|
|
43
|
+
const m = useSiaLocale().rating;
|
|
44
|
+
const label = labelProp ?? m.label;
|
|
41
45
|
const [internal, setInternal] = useState(defaultValue);
|
|
42
46
|
const current = value ?? internal;
|
|
43
47
|
const update = (next: number) => {
|
|
@@ -64,7 +68,7 @@ export function Rating({
|
|
|
64
68
|
type="button"
|
|
65
69
|
role="radio"
|
|
66
70
|
aria-checked={current === item}
|
|
67
|
-
aria-label={
|
|
71
|
+
aria-label={formatMessage(m.star, { value: item, max })}
|
|
68
72
|
disabled={disabled}
|
|
69
73
|
className={filled > 0 ? "is-active" : ""}
|
|
70
74
|
style={{ "--sia-rating-fill": `${filled * 100}%` } as React.CSSProperties}
|
|
@@ -3,7 +3,14 @@ import { Autocomplete, type AutocompleteProps } from "../Autocomplete";
|
|
|
3
3
|
import type { SelectOption } from "../Select";
|
|
4
4
|
import "./styles.css";
|
|
5
5
|
export interface ReferenceOption extends SelectOption { description?: ReactNode; }
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Les props d'`Autocomplete`, transmises telles quelles : le composant n'est
|
|
8
|
+
* qu'une mise en forme de ses options. Seules `options` changent de forme,
|
|
9
|
+
* pour porter une description.
|
|
10
|
+
*/
|
|
11
|
+
export interface ReferenceSelectProps extends Omit<AutocompleteProps, "options"> {
|
|
12
|
+
options: ReferenceOption[];
|
|
13
|
+
}
|
|
7
14
|
/**
|
|
8
15
|
* Un choix parmi des objets métier, pas parmi des mots.
|
|
9
16
|
*
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { useEffect, useState, type HTMLAttributes } 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 RelativeTimeProps extends Omit<HTMLAttributes<HTMLTimeElement>, "dateTime"> {
|
|
6
7
|
value: Date | string | number;
|
|
8
|
+
/** La langue de la phrase. Par défaut, celle de la locale SIA. */
|
|
7
9
|
locale?: string;
|
|
8
10
|
prefix?: string;
|
|
9
11
|
refreshInterval?: number;
|
|
@@ -15,7 +17,9 @@ export interface RelativeTimeProps extends Omit<HTMLAttributes<HTMLTimeElement>,
|
|
|
15
17
|
* Elle se rafraîchit d'elle-même : « il y a une minute » affiché une heure
|
|
16
18
|
* plus tard est faux, et rien ne le signale à qui lit l'écran resté ouvert.
|
|
17
19
|
*/
|
|
18
|
-
export function RelativeTime({ value, locale
|
|
20
|
+
export function RelativeTime({ value, locale: localeProp, prefix, refreshInterval = 60_000, className, title, ...props }: RelativeTimeProps) {
|
|
21
|
+
const messages = useSiaLocale();
|
|
22
|
+
const locale = localeProp ?? messages.language;
|
|
19
23
|
const [, refresh] = useState(0);
|
|
20
24
|
useEffect(() => {
|
|
21
25
|
if (refreshInterval <= 0) return;
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type SessionState,
|
|
7
7
|
type SessionStore,
|
|
8
8
|
} from "@sia-ui/headless";
|
|
9
|
-
import { Spinner } from "../Spinner";
|
|
9
|
+
import { Spinner, type SpinnerProps } from "../Spinner";
|
|
10
10
|
import "./styles.css";
|
|
11
11
|
|
|
12
12
|
export interface RequireSessionProps<TUser, TTenant, TRule> {
|
|
@@ -39,13 +39,18 @@ export interface RequireSessionProps<TUser, TTenant, TRule> {
|
|
|
39
39
|
onForbidden?: (state: SessionState<TUser, TTenant>) => void;
|
|
40
40
|
onIncomplete?: (state: SessionState<TUser, TTenant>) => void;
|
|
41
41
|
className?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Les props du `Spinner` d'attente par défaut — taille, variante, libellé
|
|
44
|
+
* lu par les lecteurs d'écran. Sans effet quand `pending` est fourni.
|
|
45
|
+
*/
|
|
46
|
+
spinnerProps?: Partial<SpinnerProps>;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
/**
|
|
45
50
|
* Ce qui se trouve derrière une session valide.
|
|
46
51
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
52
|
+
* Chaque application l'écrit, presque toujours de la même façon, à la cible
|
|
53
|
+
* de redirection près. Elle ne dépend d'aucun routeur : elle rend
|
|
49
54
|
* un état et prévient, l'application décide où aller.
|
|
50
55
|
*
|
|
51
56
|
* > Ceci protège l'interface, pas les données. Une route masquée reste une
|
|
@@ -63,6 +68,7 @@ export function RequireSession<TUser, TTenant, TRule = PermissionRule>({
|
|
|
63
68
|
onForbidden,
|
|
64
69
|
onIncomplete,
|
|
65
70
|
className,
|
|
71
|
+
spinnerProps,
|
|
66
72
|
}: RequireSessionProps<TUser, TTenant, TRule>) {
|
|
67
73
|
const state = useSession(session);
|
|
68
74
|
|
|
@@ -88,7 +94,7 @@ export function RequireSession<TUser, TTenant, TRule = PermissionRule>({
|
|
|
88
94
|
<div className={cn("sia-require-session", className)} aria-busy="true">
|
|
89
95
|
{pending ?? (
|
|
90
96
|
<div className="sia-require-session__pending">
|
|
91
|
-
<Spinner />
|
|
97
|
+
<Spinner {...spinnerProps} />
|
|
92
98
|
</div>
|
|
93
99
|
)}
|
|
94
100
|
</div>
|