@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
|
-
import { useSiaLocale } from "@sia-ui/headless";
|
|
3
|
+
import { plural, useSiaLocale } from "@sia-ui/headless";
|
|
4
4
|
import "./styles.css";
|
|
5
5
|
|
|
6
6
|
export interface FiltersBarProps {
|
|
@@ -10,6 +10,11 @@ export interface FiltersBarProps {
|
|
|
10
10
|
activeCount?: number;
|
|
11
11
|
onReset?: () => void;
|
|
12
12
|
resetLabel?: string;
|
|
13
|
+
/**
|
|
14
|
+
* La largeur de chaque filtre — `12rem` par défaut. Les filtres se placent
|
|
15
|
+
* côte à côte à cette largeur, et passent à la ligne quand elle manque.
|
|
16
|
+
*/
|
|
17
|
+
filterWidth?: string;
|
|
13
18
|
className?: string;
|
|
14
19
|
}
|
|
15
20
|
|
|
@@ -20,8 +25,8 @@ export interface FiltersBarProps {
|
|
|
20
25
|
* une liste filtrée ressemble à une liste vide, et l'on cherche la panne
|
|
21
26
|
* avant de penser au filtre posé la veille.
|
|
22
27
|
*/
|
|
23
|
-
export function FiltersBar({ search, filters, actions, activeCount = 0, onReset, resetLabel: resetLabelProp, className }: FiltersBarProps) {
|
|
28
|
+
export function FiltersBar({ search, filters, actions, activeCount = 0, onReset, resetLabel: resetLabelProp, filterWidth, className }: FiltersBarProps) {
|
|
24
29
|
const locale = useSiaLocale();
|
|
25
30
|
const resetLabel = resetLabelProp ?? locale.reset;
|
|
26
|
-
return <section className={cn("sia-filters-bar", className)} aria-label="
|
|
31
|
+
return <section className={cn("sia-filters-bar", className)} aria-label={locale.filtersBar.label} {...(filterWidth ? { style: { ["--sia-filter-width" as string]: filterWidth } } : {})}><div className="sia-filters-bar__main">{search && <div className="sia-filters-bar__search">{search}</div>}{filters && <div className="sia-filters-bar__filters">{filters}</div>}</div><div className="sia-filters-bar__actions">{activeCount > 0 && <span className="sia-filters-bar__count">{plural(locale.filtersBar, "active", activeCount, locale.language)}</span>}{onReset && activeCount > 0 && <button type="button" className="sia-filters-bar__reset" onClick={onReset}>{resetLabel}</button>}{actions}</div></section>;
|
|
27
32
|
}
|
|
@@ -2,5 +2,12 @@
|
|
|
2
2
|
.sia-filters-bar__main,.sia-filters-bar__filters,.sia-filters-bar__actions { display:flex; align-items:center; gap:.75rem; flex-wrap:wrap; }
|
|
3
3
|
.sia-filters-bar__main { flex:1; min-width:min(100%,18rem); }
|
|
4
4
|
.sia-filters-bar__search { flex:1; min-width:min(100%,16rem); }
|
|
5
|
+
/* Les filtres occupent la place restante et s'y rangent en ligne : sans
|
|
6
|
+
cela le conteneur gardait la largeur de son contenu, et deux `Select` en
|
|
7
|
+
largeur pleine s'empilaient au lieu de s'aligner. Chaque filtre garde sa
|
|
8
|
+
largeur de base — une liste de 60rem pour trois options serait absurde —
|
|
9
|
+
et passe à la ligne quand la place manque. */
|
|
10
|
+
.sia-filters-bar__filters { flex:1 1 auto; min-width:0; }
|
|
11
|
+
.sia-filters-bar__filters > * { flex:0 1 var(--sia-filter-width,12rem); min-width:0; max-width:100%; }
|
|
5
12
|
.sia-filters-bar__count { color:var(--sia-muted); font-size:.8rem; font-weight:650; }
|
|
6
13
|
.sia-filters-bar__reset { border:0; color:var(--sia-primary); background:transparent; font:inherit; font-weight:650; cursor:pointer; }
|
|
@@ -2,16 +2,19 @@ import { useState, type CSSProperties, type ReactNode } from "react";
|
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
3
|
import {
|
|
4
4
|
readSubmitError,
|
|
5
|
+
useAsyncValidation,
|
|
5
6
|
useLocalForm,
|
|
6
7
|
useSiaLocale,
|
|
8
|
+
type AsyncFieldValidator,
|
|
7
9
|
type FormAdapter,
|
|
8
10
|
type FormErrors,
|
|
9
11
|
type FormSubmitResult,
|
|
10
12
|
type FormValidator,
|
|
11
13
|
} from "@sia-ui/headless";
|
|
12
|
-
import { Alert } from "../Alert";
|
|
14
|
+
import { Alert, type AlertProps } from "../Alert";
|
|
13
15
|
import { Button, type ButtonProps } from "../Button";
|
|
14
16
|
import { Field, type FieldProps, type FieldValue } from "../Field";
|
|
17
|
+
import { Spinner, type SpinnerProps } from "../Spinner";
|
|
15
18
|
import "./styles.css";
|
|
16
19
|
|
|
17
20
|
/** Un champ du formulaire : les props de `Field`, plus sa place. */
|
|
@@ -21,6 +24,17 @@ export interface FormFieldConfig extends Omit<FieldProps, "field" | "name"> {
|
|
|
21
24
|
/** Nombre de colonnes occupées. Ignoré sur une seule colonne. */
|
|
22
25
|
colSpan?: number;
|
|
23
26
|
rowSpan?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Une vérification qui demande le serveur — un identifiant libre, un nom
|
|
29
|
+
* de domaine disponible. Rend le message d'erreur, ou rien.
|
|
30
|
+
*
|
|
31
|
+
* Relancée après la frappe (`asyncValidationDelay`), annulée à la frappe
|
|
32
|
+
* suivante par son `signal`. Pendant la vérification le champ l'annonce,
|
|
33
|
+
* et l'envoi attend ; une valeur refusée bloque l'envoi.
|
|
34
|
+
*/
|
|
35
|
+
validateAsync?: AsyncFieldValidator<FieldValue>;
|
|
36
|
+
/** Affiché sous le champ quand `validateAsync` accepte la valeur. */
|
|
37
|
+
validText?: ReactNode;
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
/** Un groupe de champs, avec son titre et sa propre grille. */
|
|
@@ -37,14 +51,14 @@ function isGroup(entry: FormEntry): entry is FormFieldGroup {
|
|
|
37
51
|
return "group" in entry;
|
|
38
52
|
}
|
|
39
53
|
|
|
54
|
+
/** Les champs, groupes aplatis. */
|
|
55
|
+
function champsAplatis(fields: FormEntry[]): FormFieldConfig[] {
|
|
56
|
+
return fields.flatMap((entree) => (isGroup(entree) ? entree.fields : [entree]));
|
|
57
|
+
}
|
|
58
|
+
|
|
40
59
|
/** Les noms de champs, groupes aplatis. */
|
|
41
60
|
export function formFieldNames(fields: FormEntry[]): string[] {
|
|
42
|
-
|
|
43
|
-
for (const entree of fields) {
|
|
44
|
-
if (isGroup(entree)) noms.push(...entree.fields.map((f) => f.name));
|
|
45
|
-
else noms.push(entree.name);
|
|
46
|
-
}
|
|
47
|
-
return noms;
|
|
61
|
+
return champsAplatis(fields).map((champ) => champ.name);
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
/**
|
|
@@ -80,10 +94,18 @@ export interface FormProps<TValues extends FormShape = FormShape> {
|
|
|
80
94
|
values: TValues,
|
|
81
95
|
) => FormSubmitResult<TValues> | Promise<FormSubmitResult<TValues>>;
|
|
82
96
|
columns?: number;
|
|
97
|
+
/** Le texte du bouton d'envoi. Par défaut, `save` de la locale. */
|
|
83
98
|
submitText?: ReactNode;
|
|
99
|
+
/** Le texte du bouton de remise à zéro. Par défaut, `reset` de la locale. */
|
|
84
100
|
clearText?: ReactNode;
|
|
85
101
|
showClear?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Les props du bouton d'envoi. `disabled` s'ajoute aux conditions du
|
|
104
|
+
* formulaire au lieu d'être remplacé par elles.
|
|
105
|
+
*/
|
|
86
106
|
submitProps?: ButtonProps;
|
|
107
|
+
/** Millisecondes sans frappe avant une vérification `validateAsync`. 400 par défaut. */
|
|
108
|
+
asyncValidationDelay?: number;
|
|
87
109
|
/** Remplace entièrement la barre de boutons. */
|
|
88
110
|
actions?: ReactNode;
|
|
89
111
|
loading?: boolean;
|
|
@@ -92,6 +114,39 @@ export interface FormProps<TValues extends FormShape = FormShape> {
|
|
|
92
114
|
/** Désactive l'envoi tant qu'une erreur est en vigueur. */
|
|
93
115
|
requireValid?: boolean;
|
|
94
116
|
className?: string;
|
|
117
|
+
/**
|
|
118
|
+
* Les props communes à tous les champs — orientation, libellé
|
|
119
|
+
* « facultatif », `controlProps`. Chaque champ déclaré dans `fields`
|
|
120
|
+
* l'emporte sur elles.
|
|
121
|
+
*
|
|
122
|
+
* Sans ce que le formulaire branche lui-même : le nom, la valeur, le
|
|
123
|
+
* changement et la sortie de champ (`field`), ni les messages d'erreur et
|
|
124
|
+
* de validation, qui sont propres à chaque champ.
|
|
125
|
+
*/
|
|
126
|
+
fieldProps?: Partial<
|
|
127
|
+
Omit<
|
|
128
|
+
FieldProps,
|
|
129
|
+
| "field"
|
|
130
|
+
| "name"
|
|
131
|
+
| "value"
|
|
132
|
+
| "defaultValue"
|
|
133
|
+
| "onValueChange"
|
|
134
|
+
| "onBlur"
|
|
135
|
+
| "error"
|
|
136
|
+
| "success"
|
|
137
|
+
| "message"
|
|
138
|
+
>
|
|
139
|
+
>;
|
|
140
|
+
/**
|
|
141
|
+
* Les props de l'`Alert` qui annonce un échec d'envoi. Sans `children`, le
|
|
142
|
+
* message d'échec, ni `onDismiss`, qui l'efface.
|
|
143
|
+
*/
|
|
144
|
+
alertProps?: Partial<Omit<AlertProps, "children" | "onDismiss">>;
|
|
145
|
+
/**
|
|
146
|
+
* Les props du `Spinner` d'une vérification `validateAsync` en cours.
|
|
147
|
+
* Sans `label`, repris de la locale comme le texte qui l'accompagne.
|
|
148
|
+
*/
|
|
149
|
+
spinnerProps?: Partial<Omit<SpinnerProps, "label">>;
|
|
95
150
|
}
|
|
96
151
|
|
|
97
152
|
const VIDE = {} as FormShape;
|
|
@@ -123,15 +178,19 @@ export function Form<TValues extends FormShape = FormShape>({
|
|
|
123
178
|
validate,
|
|
124
179
|
onSubmit,
|
|
125
180
|
columns = 1,
|
|
126
|
-
submitText
|
|
127
|
-
clearText
|
|
181
|
+
submitText,
|
|
182
|
+
clearText,
|
|
128
183
|
showClear = false,
|
|
129
184
|
submitProps,
|
|
130
185
|
actions,
|
|
131
186
|
loading = false,
|
|
132
187
|
requireDirty = true,
|
|
133
188
|
requireValid = true,
|
|
189
|
+
asyncValidationDelay = 400,
|
|
134
190
|
className,
|
|
191
|
+
fieldProps,
|
|
192
|
+
alertProps,
|
|
193
|
+
spinnerProps,
|
|
135
194
|
}: FormProps<TValues>) {
|
|
136
195
|
// Le hook est appelé sans condition, comme il se doit; son résultat n'est
|
|
137
196
|
// utilisé que si aucun adaptateur n'a été fourni. Un `useLocalForm` inerte
|
|
@@ -152,7 +211,21 @@ export function Form<TValues extends FormShape = FormShape>({
|
|
|
152
211
|
*/
|
|
153
212
|
const [echec, setEchec] = useState<string | null>(null);
|
|
154
213
|
|
|
214
|
+
// Les vérifications serveur déclarées sur les champs.
|
|
215
|
+
const verifications: Record<string, AsyncFieldValidator> = {};
|
|
216
|
+
for (const champ of champsAplatis(fields)) {
|
|
217
|
+
if (champ.validateAsync) {
|
|
218
|
+
verifications[champ.name] = champ.validateAsync as AsyncFieldValidator;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const asynchrone = useAsyncValidation(verifications, form.state.values, {
|
|
222
|
+
delay: asyncValidationDelay,
|
|
223
|
+
});
|
|
224
|
+
|
|
155
225
|
const envoyer = () => {
|
|
226
|
+
// Entrée dans un champ envoie le formulaire même bouton désactivé : la
|
|
227
|
+
// garde est ici, pas seulement sur le bouton.
|
|
228
|
+
if (asynchrone.pending || asynchrone.invalid || submitProps?.disabled) return;
|
|
156
229
|
setEchec(null);
|
|
157
230
|
form.submit().catch((error: unknown) => {
|
|
158
231
|
const { fields: erreurs, message } = readSubmitError(error);
|
|
@@ -178,6 +251,9 @@ export function Form<TValues extends FormShape = FormShape>({
|
|
|
178
251
|
const busy = loading || form.state.isSubmitting;
|
|
179
252
|
const submitDisabled =
|
|
180
253
|
busy ||
|
|
254
|
+
Boolean(submitProps?.disabled) ||
|
|
255
|
+
asynchrone.pending ||
|
|
256
|
+
asynchrone.invalid ||
|
|
181
257
|
(requireDirty && !form.state.isDirty) ||
|
|
182
258
|
(requireValid && !form.state.isValid);
|
|
183
259
|
|
|
@@ -186,7 +262,29 @@ export function Form<TValues extends FormShape = FormShape>({
|
|
|
186
262
|
});
|
|
187
263
|
|
|
188
264
|
const rendre = (config: FormFieldConfig) => {
|
|
189
|
-
const { name, colSpan, rowSpan, ...props } = config;
|
|
265
|
+
const { name, colSpan, rowSpan, validateAsync, validText, ...props } = config;
|
|
266
|
+
const binding = form.bind(name as keyof TValues & string);
|
|
267
|
+
const verification = validateAsync ? asynchrone.states[name] : undefined;
|
|
268
|
+
|
|
269
|
+
// L'erreur locale passe devant : une valeur mal formée n'a pas à être
|
|
270
|
+
// vérifiée auprès du serveur pour être refusée.
|
|
271
|
+
const etat =
|
|
272
|
+
binding.error || !verification
|
|
273
|
+
? {}
|
|
274
|
+
: verification.status === "checking"
|
|
275
|
+
? {
|
|
276
|
+
message: (
|
|
277
|
+
<span className="sia-form-checking">
|
|
278
|
+
<Spinner size="xs" {...spinnerProps} label={locale.checking} />
|
|
279
|
+
{locale.checking}
|
|
280
|
+
</span>
|
|
281
|
+
),
|
|
282
|
+
}
|
|
283
|
+
: verification.status === "invalid"
|
|
284
|
+
? { error: verification.error }
|
|
285
|
+
: verification.status === "valid" && validText
|
|
286
|
+
? { success: validText }
|
|
287
|
+
: {};
|
|
190
288
|
|
|
191
289
|
return (
|
|
192
290
|
<div
|
|
@@ -198,8 +296,13 @@ export function Form<TValues extends FormShape = FormShape>({
|
|
|
198
296
|
}}
|
|
199
297
|
>
|
|
200
298
|
<Field
|
|
299
|
+
{...fieldProps}
|
|
201
300
|
{...props}
|
|
202
|
-
|
|
301
|
+
{...(fieldProps?.className || props.className
|
|
302
|
+
? { className: cn(fieldProps?.className, props.className) }
|
|
303
|
+
: {})}
|
|
304
|
+
{...etat}
|
|
305
|
+
field={binding}
|
|
203
306
|
{...(busy ? { disabled: true } : {})}
|
|
204
307
|
/>
|
|
205
308
|
</div>
|
|
@@ -243,7 +346,8 @@ export function Form<TValues extends FormShape = FormShape>({
|
|
|
243
346
|
{echec && (
|
|
244
347
|
<Alert
|
|
245
348
|
tone="danger"
|
|
246
|
-
|
|
349
|
+
{...alertProps}
|
|
350
|
+
className={cn("sia-form-error", alertProps?.className)}
|
|
247
351
|
onDismiss={() => setEchec(null)}
|
|
248
352
|
>
|
|
249
353
|
{echec}
|
|
@@ -259,7 +363,7 @@ export function Form<TValues extends FormShape = FormShape>({
|
|
|
259
363
|
disabled={busy}
|
|
260
364
|
onClick={() => form.reset()}
|
|
261
365
|
>
|
|
262
|
-
{clearText}
|
|
366
|
+
{clearText ?? locale.reset}
|
|
263
367
|
</Button>
|
|
264
368
|
)}
|
|
265
369
|
<Button
|
|
@@ -268,7 +372,7 @@ export function Form<TValues extends FormShape = FormShape>({
|
|
|
268
372
|
loading={busy}
|
|
269
373
|
disabled={submitDisabled}
|
|
270
374
|
>
|
|
271
|
-
{submitText}
|
|
375
|
+
{submitText ?? locale.save}
|
|
272
376
|
</Button>
|
|
273
377
|
</div>
|
|
274
378
|
)}
|
|
@@ -78,3 +78,6 @@
|
|
|
78
78
|
.sia-form-actions { flex-direction: column-reverse; }
|
|
79
79
|
.sia-form-actions > * { width: 100%; }
|
|
80
80
|
}
|
|
81
|
+
|
|
82
|
+
/* « Vérification… » sous un champ : le témoin aligné sur le texte. */
|
|
83
|
+
.sia-form-checking { display: inline-flex; align-items: center; gap: .4rem; color: var(--sia-muted); }
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "react";
|
|
8
8
|
import { cn } from "@sia-ui/utils";
|
|
9
9
|
import { useHoverIntent } from "@sia-ui/headless";
|
|
10
|
-
import { Overlay, type OverlayPlacement } from "../Overlay";
|
|
10
|
+
import { Overlay, type OverlayPlacement, type OverlayProps } from "../Overlay";
|
|
11
11
|
import "./styles.css";
|
|
12
12
|
|
|
13
13
|
export interface HoverCardProps {
|
|
@@ -21,6 +21,8 @@ export interface HoverCardProps {
|
|
|
21
21
|
/** Délai avant fermeture, le temps d'atteindre le panneau à la souris. */
|
|
22
22
|
closeDelay?: number;
|
|
23
23
|
placement?: OverlayPlacement;
|
|
24
|
+
/** Réglages du panneau flottant — décalage, marge au bord, rôle, classe. */
|
|
25
|
+
overlayProps?: Partial<Omit<OverlayProps, "open" | "anchorRef" | "onOpenChange" | "children">>;
|
|
24
26
|
open?: boolean;
|
|
25
27
|
onOpenChange?: (open: boolean) => void;
|
|
26
28
|
width?: number | string;
|
|
@@ -40,6 +42,7 @@ export function HoverCard({
|
|
|
40
42
|
openDelay = 300,
|
|
41
43
|
closeDelay = 150,
|
|
42
44
|
placement = "bottom-start",
|
|
45
|
+
overlayProps,
|
|
43
46
|
open: openProp,
|
|
44
47
|
onOpenChange,
|
|
45
48
|
width = 20 * 16,
|
|
@@ -76,12 +79,13 @@ export function HoverCard({
|
|
|
76
79
|
<span className="sia-hover-card-anchor">
|
|
77
80
|
{trigger}
|
|
78
81
|
<Overlay
|
|
82
|
+
placement={placement}
|
|
83
|
+
role="dialog"
|
|
84
|
+
{...overlayProps}
|
|
85
|
+
className={cn("sia-hover-card", className, overlayProps?.className)}
|
|
79
86
|
open={open}
|
|
80
87
|
anchorRef={anchorRef}
|
|
81
88
|
onOpenChange={setOpen}
|
|
82
|
-
placement={placement}
|
|
83
|
-
className={cn("sia-hover-card", className)}
|
|
84
|
-
role="dialog"
|
|
85
89
|
>
|
|
86
90
|
<div
|
|
87
91
|
style={{ width }}
|
|
@@ -291,3 +291,21 @@ export function PencilIcon(props: IconProps) {
|
|
|
291
291
|
</IconBase>
|
|
292
292
|
);
|
|
293
293
|
}
|
|
294
|
+
|
|
295
|
+
/* ── Mode de couleur ───────────────────────────────────────────────────── */
|
|
296
|
+
|
|
297
|
+
export function SunIcon(props: IconProps) {
|
|
298
|
+
return (
|
|
299
|
+
<IconBase {...props}>
|
|
300
|
+
<circle cx="12" cy="12" r="4" />
|
|
301
|
+
<path d="M12 2.5v2M12 19.5v2M4.6 4.6 6 6M18 18l1.4 1.4M2.5 12h2M19.5 12h2M4.6 19.4 6 18M18 6l1.4-1.4" />
|
|
302
|
+
</IconBase>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
export function MoonIcon(props: IconProps) {
|
|
306
|
+
return (
|
|
307
|
+
<IconBase {...props}>
|
|
308
|
+
<path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z" />
|
|
309
|
+
</IconBase>
|
|
310
|
+
);
|
|
311
|
+
}
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import { FileUpload, type FileUploadProps } from "../FileUpload";
|
|
3
3
|
import { cn } from "@sia-ui/utils";
|
|
4
|
+
import { useSiaLocale } from "@sia-ui/headless";
|
|
4
5
|
import "./styles.css";
|
|
5
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Les props de `FileUpload`, transmises telles quelles au dépôt : libellé,
|
|
8
|
+
* description, désactivation. `onFilesChange` cède la place à
|
|
9
|
+
* `onValueChange`, qui porte l'aperçu, et `multiple` est forcé à `false` —
|
|
10
|
+
* un aperçu ne montre qu'une image.
|
|
11
|
+
*/
|
|
12
|
+
export interface ImageUploadProps extends Omit<FileUploadProps, "accept" | "onFilesChange"> {
|
|
13
|
+
accept?: string;
|
|
14
|
+
value?: string;
|
|
15
|
+
onValueChange?: (file: File | null, previewUrl?: string) => void;
|
|
16
|
+
/** Le texte alternatif de l'aperçu. Par défaut, celui de la locale (`imageUpload.previewAlt`). */
|
|
17
|
+
previewAlt?: string;
|
|
18
|
+
/** Le bouton qui retire l'image. Par défaut, `delete` de la locale. */
|
|
19
|
+
removeLabel?: string;
|
|
20
|
+
}
|
|
6
21
|
/**
|
|
7
22
|
* Un dépôt de fichier qui montre ce qu'on vient de choisir.
|
|
8
23
|
*
|
|
@@ -10,4 +25,4 @@ export interface ImageUploadProps extends Omit<FileUploadProps, "accept" | "onFi
|
|
|
10
25
|
* serveur pour afficher l'image laisse un carré vide au moment précis où
|
|
11
26
|
* l'on veut vérifier qu'on n'a pas pris la mauvaise.
|
|
12
27
|
*/
|
|
13
|
-
export function ImageUpload({ accept = "image/*", value, onValueChange, previewAlt
|
|
28
|
+
export function ImageUpload({ accept = "image/*", value, onValueChange, previewAlt: previewAltProp, removeLabel: removeLabelProp, className, ...props }: ImageUploadProps) { const locale = useSiaLocale(); const previewAlt = previewAltProp ?? locale.imageUpload.previewAlt; const removeLabel = removeLabelProp ?? locale.delete; const [preview, setPreview] = useState(value); useEffect(() => setPreview(value), [value]); useEffect(() => () => { if (preview?.startsWith("blob:")) URL.revokeObjectURL(preview); }, [preview]); const select = (files: File[]) => { const file = files[0]; if (!file) return; const next = URL.createObjectURL(file); setPreview(next); onValueChange?.(file, next); }; return <div className={cn("sia-image-upload", className)}>{preview && <div className="sia-image-upload__preview"><img src={preview} alt={previewAlt} /><button type="button" onClick={() => { setPreview(undefined); onValueChange?.(null); }}>{removeLabel}</button></div>}<FileUpload {...props} accept={accept} multiple={false} onFilesChange={select} /></div>; }
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import { cn, toneClass } from "@sia-ui/utils";
|
|
8
8
|
import { Spinner, type SpinnerProps } from "../Spinner";
|
|
9
9
|
import type { ComponentTone } from "@sia-ui/tokens";
|
|
10
|
-
import { useComponentDefaults } from "@sia-ui/headless";
|
|
10
|
+
import { useComponentDefaults, useSiaLocale } from "@sia-ui/headless";
|
|
11
11
|
import "./styles.css";
|
|
12
12
|
|
|
13
13
|
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
@@ -50,7 +50,8 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
50
50
|
ref,
|
|
51
51
|
) => {
|
|
52
52
|
const defaults = useComponentDefaults<InputProps>("input");
|
|
53
|
-
const
|
|
53
|
+
const locale = useSiaLocale();
|
|
54
|
+
const loadingLabel = loadingLabelProp ?? defaults.loadingLabel ?? locale.loading;
|
|
54
55
|
const generatedId = useId();
|
|
55
56
|
const id = providedId ?? generatedId;
|
|
56
57
|
const decorated = Boolean(left || right || loading);
|