@sia-ui/cli 0.1.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/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/diff.mjs +91 -0
- package/dist/index.js +325 -0
- package/dist/lifecycle.mjs +242 -0
- package/dist/lock.mjs +92 -0
- package/dist/registry/package/components/Accordion/index.tsx +172 -0
- package/dist/registry/package/components/Accordion/styles.css +97 -0
- package/dist/registry/package/components/Alert/index.tsx +31 -0
- package/dist/registry/package/components/Alert/styles.css +12 -0
- package/dist/registry/package/components/AmountDisplay/index.tsx +11 -0
- package/dist/registry/package/components/AmountDisplay/styles.css +9 -0
- package/dist/registry/package/components/AppShell/index.tsx +6 -0
- package/dist/registry/package/components/AppShell/styles.css +7 -0
- package/dist/registry/package/components/AspectRatio/index.tsx +29 -0
- package/dist/registry/package/components/AspectRatio/styles.css +15 -0
- package/dist/registry/package/components/AsyncState/index.tsx +22 -0
- package/dist/registry/package/components/AsyncState/styles.css +1 -0
- package/dist/registry/package/components/AuditMeta/index.tsx +44 -0
- package/dist/registry/package/components/AuditMeta/styles.css +12 -0
- package/dist/registry/package/components/AuthLayout/index.tsx +39 -0
- package/dist/registry/package/components/AuthLayout/styles.css +6 -0
- package/dist/registry/package/components/Autocomplete/index.tsx +8 -0
- package/dist/registry/package/components/Avatar/index.tsx +23 -0
- package/dist/registry/package/components/Avatar/styles.css +5 -0
- package/dist/registry/package/components/Badge/index.tsx +62 -0
- package/dist/registry/package/components/Badge/styles.css +26 -0
- package/dist/registry/package/components/Breadcrumb/index.tsx +131 -0
- package/dist/registry/package/components/Breadcrumb/styles.css +76 -0
- package/dist/registry/package/components/Button/index.tsx +91 -0
- package/dist/registry/package/components/Button/styles.css +111 -0
- package/dist/registry/package/components/Calendar/index.tsx +192 -0
- package/dist/registry/package/components/Card/index.tsx +8 -0
- package/dist/registry/package/components/Card/styles.css +4 -0
- package/dist/registry/package/components/Chart/index.tsx +437 -0
- package/dist/registry/package/components/Chart/styles.css +182 -0
- package/dist/registry/package/components/Checkbox/index.tsx +193 -0
- package/dist/registry/package/components/Checkbox/styles.css +39 -0
- package/dist/registry/package/components/ColorPicker/index.tsx +6 -0
- package/dist/registry/package/components/ColorPicker/styles.css +2 -0
- package/dist/registry/package/components/CommandPalette/index.tsx +7 -0
- package/dist/registry/package/components/CommandPalette/styles.css +8 -0
- package/dist/registry/package/components/ConfirmDialog/index.tsx +24 -0
- package/dist/registry/package/components/Container/index.tsx +11 -0
- package/dist/registry/package/components/Container/styles.css +1 -0
- package/dist/registry/package/components/Countdown/index.tsx +3 -0
- package/dist/registry/package/components/CrudPage/index.tsx +9 -0
- package/dist/registry/package/components/CrudPage/styles.css +1 -0
- package/dist/registry/package/components/CurrencyInput/index.tsx +15 -0
- package/dist/registry/package/components/CurrencyInput/styles.css +1 -0
- package/dist/registry/package/components/DataTable/index.tsx +240 -0
- package/dist/registry/package/components/DataTable/styles.css +98 -0
- package/dist/registry/package/components/DatePicker/index.tsx +102 -0
- package/dist/registry/package/components/DateRangeFilter/index.tsx +8 -0
- package/dist/registry/package/components/DateRangeFilter/styles.css +4 -0
- package/dist/registry/package/components/DateRangePicker/index.tsx +36 -0
- package/dist/registry/package/components/DateTimePicker/index.tsx +32 -0
- package/dist/registry/package/components/Descriptions/index.tsx +7 -0
- package/dist/registry/package/components/Descriptions/styles.css +8 -0
- package/dist/registry/package/components/Divider/index.tsx +12 -0
- package/dist/registry/package/components/Divider/styles.css +3 -0
- package/dist/registry/package/components/Drawer/context.ts +14 -0
- package/dist/registry/package/components/Drawer/index.tsx +209 -0
- package/dist/registry/package/components/Drawer/styles.css +151 -0
- package/dist/registry/package/components/DropdownMenu/index.tsx +350 -0
- package/dist/registry/package/components/DropdownMenu/styles.css +100 -0
- package/dist/registry/package/components/DurationDisplay/index.tsx +3 -0
- package/dist/registry/package/components/EmptyState/index.tsx +23 -0
- package/dist/registry/package/components/EmptyState/styles.css +6 -0
- package/dist/registry/package/components/EntityMeta/index.tsx +65 -0
- package/dist/registry/package/components/EntityMeta/styles.css +9 -0
- package/dist/registry/package/components/ErrorState/index.tsx +30 -0
- package/dist/registry/package/components/ErrorState/styles.css +6 -0
- package/dist/registry/package/components/EventCalendar/index.tsx +8 -0
- package/dist/registry/package/components/EventCalendar/styles.css +8 -0
- package/dist/registry/package/components/Field/index.tsx +664 -0
- package/dist/registry/package/components/Field/styles.css +14 -0
- package/dist/registry/package/components/FileUpload/index.tsx +68 -0
- package/dist/registry/package/components/FileUpload/styles.css +4 -0
- package/dist/registry/package/components/FiltersBar/index.tsx +20 -0
- package/dist/registry/package/components/FiltersBar/styles.css +6 -0
- package/dist/registry/package/components/Form/index.tsx +211 -0
- package/dist/registry/package/components/Form/styles.css +80 -0
- package/dist/registry/package/components/FormField/index.tsx +2 -0
- package/dist/registry/package/components/HoverCard/index.tsx +121 -0
- package/dist/registry/package/components/HoverCard/styles.css +26 -0
- package/dist/registry/package/components/IconButton/index.tsx +27 -0
- package/dist/registry/package/components/IconButton/styles.css +7 -0
- package/dist/registry/package/components/Icons/index.tsx +185 -0
- package/dist/registry/package/components/ImageUpload/index.tsx +6 -0
- package/dist/registry/package/components/ImageUpload/styles.css +4 -0
- package/dist/registry/package/components/Input/index.tsx +103 -0
- package/dist/registry/package/components/Input/styles.css +8 -0
- package/dist/registry/package/components/InputNumber/index.tsx +196 -0
- package/dist/registry/package/components/InputNumber/styles.css +54 -0
- package/dist/registry/package/components/JsonEditor/index.tsx +6 -0
- package/dist/registry/package/components/JsonEditor/styles.css +2 -0
- package/dist/registry/package/components/KpiGrid/index.tsx +11 -0
- package/dist/registry/package/components/KpiGrid/styles.css +5 -0
- package/dist/registry/package/components/Label/index.tsx +43 -0
- package/dist/registry/package/components/Label/styles.css +25 -0
- package/dist/registry/package/components/MarkdownEditor/index.tsx +5 -0
- package/dist/registry/package/components/MiniCalendar/index.tsx +3 -0
- package/dist/registry/package/components/Modal/index.tsx +104 -0
- package/dist/registry/package/components/Modal/styles.css +20 -0
- package/dist/registry/package/components/MonthPicker/index.tsx +5 -0
- package/dist/registry/package/components/MultiSelect/index.tsx +133 -0
- package/dist/registry/package/components/MultiSelect/styles.css +3 -0
- package/dist/registry/package/components/OtpInput/index.tsx +5 -0
- package/dist/registry/package/components/OtpInput/styles.css +2 -0
- package/dist/registry/package/components/Overlay/index.tsx +140 -0
- package/dist/registry/package/components/Overlay/styles.css +1 -0
- package/dist/registry/package/components/PageHeader/index.tsx +24 -0
- package/dist/registry/package/components/PageHeader/styles.css +5 -0
- package/dist/registry/package/components/Pagination/index.tsx +155 -0
- package/dist/registry/package/components/Pagination/styles.css +103 -0
- package/dist/registry/package/components/PasswordInput/index.tsx +125 -0
- package/dist/registry/package/components/PasswordInput/styles.css +58 -0
- package/dist/registry/package/components/PermissionGate/index.tsx +3 -0
- package/dist/registry/package/components/PhoneInput/index.tsx +189 -0
- package/dist/registry/package/components/PhoneInput/styles.css +35 -0
- package/dist/registry/package/components/Popover/index.tsx +12 -0
- package/dist/registry/package/components/Popover/styles.css +12 -0
- package/dist/registry/package/components/Progress/index.tsx +149 -0
- package/dist/registry/package/components/Progress/styles.css +101 -0
- package/dist/registry/package/components/QrCode/index.tsx +6 -0
- package/dist/registry/package/components/QrCode/styles.css +5 -0
- package/dist/registry/package/components/RadioGroup/index.tsx +29 -0
- package/dist/registry/package/components/RadioGroup/styles.css +20 -0
- package/dist/registry/package/components/Rating/index.tsx +82 -0
- package/dist/registry/package/components/Rating/styles.css +41 -0
- package/dist/registry/package/components/ReferenceSelect/index.tsx +7 -0
- package/dist/registry/package/components/ReferenceSelect/styles.css +2 -0
- package/dist/registry/package/components/RelativeTime/index.tsx +22 -0
- package/dist/registry/package/components/RelativeTime/styles.css +1 -0
- package/dist/registry/package/components/RequireSession/index.tsx +127 -0
- package/dist/registry/package/components/RequireSession/styles.css +13 -0
- package/dist/registry/package/components/ResizablePanel/index.tsx +6 -0
- package/dist/registry/package/components/ResizablePanel/styles.css +5 -0
- package/dist/registry/package/components/RichTextEditor/index.tsx +5 -0
- package/dist/registry/package/components/RichTextEditor/styles.css +2 -0
- package/dist/registry/package/components/ScrollArea/index.tsx +114 -0
- package/dist/registry/package/components/ScrollArea/styles.css +74 -0
- package/dist/registry/package/components/SearchInput/index.tsx +121 -0
- package/dist/registry/package/components/SearchInput/styles.css +6 -0
- package/dist/registry/package/components/Select/index.tsx +324 -0
- package/dist/registry/package/components/Select/styles.css +58 -0
- package/dist/registry/package/components/Sidebar/index.tsx +232 -0
- package/dist/registry/package/components/Sidebar/styles.css +140 -0
- package/dist/registry/package/components/Skeleton/index.tsx +21 -0
- package/dist/registry/package/components/Skeleton/styles.css +2 -0
- package/dist/registry/package/components/Slider/index.tsx +193 -0
- package/dist/registry/package/components/Slider/styles.css +53 -0
- package/dist/registry/package/components/Spinner/index.tsx +28 -0
- package/dist/registry/package/components/Spinner/styles.css +8 -0
- package/dist/registry/package/components/StatCard/index.tsx +15 -0
- package/dist/registry/package/components/StatCard/styles.css +3 -0
- package/dist/registry/package/components/Statistic/index.tsx +23 -0
- package/dist/registry/package/components/Statistic/styles.css +6 -0
- package/dist/registry/package/components/Steps/index.tsx +108 -0
- package/dist/registry/package/components/Steps/styles.css +138 -0
- package/dist/registry/package/components/Switch/index.tsx +46 -0
- package/dist/registry/package/components/Switch/styles.css +26 -0
- package/dist/registry/package/components/Tabs/index.tsx +44 -0
- package/dist/registry/package/components/Tabs/styles.css +41 -0
- package/dist/registry/package/components/TagsInput/index.tsx +6 -0
- package/dist/registry/package/components/TagsInput/styles.css +2 -0
- package/dist/registry/package/components/Textarea/index.tsx +21 -0
- package/dist/registry/package/components/Textarea/styles.css +7 -0
- package/dist/registry/package/components/TimePicker/index.tsx +76 -0
- package/dist/registry/package/components/TimePicker/styles.css +13 -0
- package/dist/registry/package/components/TimeRangePicker/index.tsx +32 -0
- package/dist/registry/package/components/TimeRangePicker/styles.css +1 -0
- package/dist/registry/package/components/Timeline/index.tsx +8 -0
- package/dist/registry/package/components/Timeline/styles.css +12 -0
- package/dist/registry/package/components/Timer/index.tsx +9 -0
- package/dist/registry/package/components/Toast/index.tsx +426 -0
- package/dist/registry/package/components/Toast/styles.css +175 -0
- package/dist/registry/package/components/Toggle/index.tsx +181 -0
- package/dist/registry/package/components/Toggle/styles.css +124 -0
- package/dist/registry/package/components/Tooltip/index.tsx +163 -0
- package/dist/registry/package/components/Tooltip/styles.css +57 -0
- package/dist/registry/package/components/Tour/index.tsx +8 -0
- package/dist/registry/package/components/Tour/styles.css +6 -0
- package/dist/registry/package/components/Transfer/index.tsx +7 -0
- package/dist/registry/package/components/Transfer/styles.css +6 -0
- package/dist/registry/package/components/Tree/index.tsx +8 -0
- package/dist/registry/package/components/Tree/styles.css +6 -0
- package/dist/registry/package/components/TreeSelect/index.tsx +10 -0
- package/dist/registry/package/components/TreeSelect/styles.css +3 -0
- package/dist/registry/package/components/Typography/index.tsx +27 -0
- package/dist/registry/package/components/Typography/styles.css +16 -0
- package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +4 -0
- package/dist/registry/package/components/WeekPicker/index.tsx +5 -0
- package/dist/registry/package/components/YearPicker/index.tsx +3 -0
- package/dist/registry/package/lib/form-adapter-rhf.tsx +141 -0
- package/dist/registry/registry.json +1780 -0
- package/dist/registry/styles/sia-ui.css +361 -0
- package/dist/source.mjs +93 -0
- package/package.json +55 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useRef,
|
|
3
|
+
type ChangeEvent,
|
|
4
|
+
type DragEvent,
|
|
5
|
+
type InputHTMLAttributes,
|
|
6
|
+
} from "react";
|
|
7
|
+
import { cn } from "@sia-ui/utils";
|
|
8
|
+
import "./styles.css";
|
|
9
|
+
|
|
10
|
+
export interface FileUploadProps extends Omit<
|
|
11
|
+
InputHTMLAttributes<HTMLInputElement>,
|
|
12
|
+
"type" | "onChange"
|
|
13
|
+
> {
|
|
14
|
+
label?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
onFilesChange?: (files: File[]) => void;
|
|
17
|
+
}
|
|
18
|
+
export function FileUpload({
|
|
19
|
+
label = "Ajouter des fichiers",
|
|
20
|
+
description = "Glissez les fichiers ici ou parcourez votre appareil.",
|
|
21
|
+
onFilesChange,
|
|
22
|
+
className,
|
|
23
|
+
multiple = true,
|
|
24
|
+
accept,
|
|
25
|
+
disabled,
|
|
26
|
+
...props
|
|
27
|
+
}: FileUploadProps) {
|
|
28
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
29
|
+
const emit = (files: FileList | null) =>
|
|
30
|
+
onFilesChange?.(Array.from(files ?? []));
|
|
31
|
+
const drop = (event: DragEvent<HTMLDivElement>) => {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
if (!disabled) emit(event.dataTransfer.files);
|
|
34
|
+
};
|
|
35
|
+
const change = (event: ChangeEvent<HTMLInputElement>) =>
|
|
36
|
+
emit(event.target.files);
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
className={cn(
|
|
40
|
+
"sia-file-upload",
|
|
41
|
+
disabled && "sia-file-upload--disabled",
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
onDragOver={(event) => event.preventDefault()}
|
|
45
|
+
onDrop={drop}
|
|
46
|
+
>
|
|
47
|
+
<input
|
|
48
|
+
{...props}
|
|
49
|
+
ref={inputRef}
|
|
50
|
+
hidden
|
|
51
|
+
type="file"
|
|
52
|
+
accept={accept}
|
|
53
|
+
multiple={multiple}
|
|
54
|
+
disabled={disabled}
|
|
55
|
+
onChange={change}
|
|
56
|
+
/>
|
|
57
|
+
<strong>{label}</strong>
|
|
58
|
+
<span>{description}</span>
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
disabled={disabled}
|
|
62
|
+
onClick={() => inputRef.current?.click()}
|
|
63
|
+
>
|
|
64
|
+
Parcourir
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
.sia-file-upload { display:grid; place-items:center; gap:.55rem; min-height:12rem; padding:1.5rem; text-align:center; border:1px dashed var(--sia-border); border-radius:var(--sia-radius); background:var(--sia-surface); }
|
|
2
|
+
.sia-file-upload span { color:var(--sia-muted); }
|
|
3
|
+
.sia-file-upload button { min-height:2.25rem; padding:0 .85rem; border:1px solid var(--sia-border); border-radius:var(--sia-radius); background:var(--sia-background); cursor:pointer; }
|
|
4
|
+
.sia-file-upload--disabled { opacity:.55; }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { cn } from "@sia-ui/utils";
|
|
3
|
+
import { useSiaLocale } from "@sia-ui/headless";
|
|
4
|
+
import "./styles.css";
|
|
5
|
+
|
|
6
|
+
export interface FiltersBarProps {
|
|
7
|
+
search?: ReactNode;
|
|
8
|
+
filters?: ReactNode;
|
|
9
|
+
actions?: ReactNode;
|
|
10
|
+
activeCount?: number;
|
|
11
|
+
onReset?: () => void;
|
|
12
|
+
resetLabel?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function FiltersBar({ search, filters, actions, activeCount = 0, onReset, resetLabel: resetLabelProp, className }: FiltersBarProps) {
|
|
17
|
+
const locale = useSiaLocale();
|
|
18
|
+
const resetLabel = resetLabelProp ?? locale.reset;
|
|
19
|
+
return <section className={cn("sia-filters-bar", className)} aria-label="Filtres"><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">{activeCount} actif{activeCount > 1 ? "s" : ""}</span>}{onReset && activeCount > 0 && <button type="button" className="sia-filters-bar__reset" onClick={onReset}>{resetLabel}</button>}{actions}</div></section>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
.sia-filters-bar { display:flex; align-items:center; justify-content:space-between; gap:1rem; flex-wrap:wrap; padding:1rem; border:1px solid var(--sia-border); border-radius:calc(var(--sia-radius) * 1.25); background:var(--sia-surface); }
|
|
2
|
+
.sia-filters-bar__main,.sia-filters-bar__filters,.sia-filters-bar__actions { display:flex; align-items:center; gap:.75rem; flex-wrap:wrap; }
|
|
3
|
+
.sia-filters-bar__main { flex:1; min-width:min(100%,18rem); }
|
|
4
|
+
.sia-filters-bar__search { flex:1; min-width:min(100%,16rem); }
|
|
5
|
+
.sia-filters-bar__count { color:var(--sia-muted); font-size:.8rem; font-weight:650; }
|
|
6
|
+
.sia-filters-bar__reset { border:0; color:var(--sia-primary); background:transparent; font:inherit; font-weight:650; cursor:pointer; }
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "@sia-ui/utils";
|
|
3
|
+
import {
|
|
4
|
+
useLocalForm,
|
|
5
|
+
type FormAdapter,
|
|
6
|
+
type FormValidator,
|
|
7
|
+
} from "@sia-ui/headless";
|
|
8
|
+
import { Button, type ButtonProps } from "../Button";
|
|
9
|
+
import { Field, type FieldProps, type FieldValue } from "../Field";
|
|
10
|
+
import "./styles.css";
|
|
11
|
+
|
|
12
|
+
/** Un champ du formulaire : les props de `Field`, plus sa place. */
|
|
13
|
+
export interface FormFieldConfig extends Omit<FieldProps, "field" | "name"> {
|
|
14
|
+
/** La clé dans les valeurs. C'est elle qui branche le champ. */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Nombre de colonnes occupées. Ignoré sur une seule colonne. */
|
|
17
|
+
colSpan?: number;
|
|
18
|
+
rowSpan?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Un groupe de champs, avec son titre et sa propre grille. */
|
|
22
|
+
export interface FormFieldGroup {
|
|
23
|
+
group: string;
|
|
24
|
+
description?: ReactNode;
|
|
25
|
+
columns?: number;
|
|
26
|
+
fields: FormFieldConfig[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type FormEntry = FormFieldConfig | FormFieldGroup;
|
|
30
|
+
|
|
31
|
+
function isGroup(entry: FormEntry): entry is FormFieldGroup {
|
|
32
|
+
return "group" in entry;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Les valeurs qu'un formulaire déclaratif peut porter.
|
|
37
|
+
*
|
|
38
|
+
* Plus étroit que `FormValues` volontairement : ce formulaire rend ses
|
|
39
|
+
* champs avec `Field`, qui ne sait afficher que ces types-là. Un objet
|
|
40
|
+
* imbriqué n'aurait pas de contrôle pour le montrer.
|
|
41
|
+
*/
|
|
42
|
+
export type FormShape = Record<string, FieldValue>;
|
|
43
|
+
|
|
44
|
+
export interface FormProps<TValues extends FormShape = FormShape> {
|
|
45
|
+
fields: FormEntry[];
|
|
46
|
+
/**
|
|
47
|
+
* L'adaptateur de formulaire.
|
|
48
|
+
*
|
|
49
|
+
* Sans lui, un `useLocalForm` est monté en interne à partir de
|
|
50
|
+
* `defaultValues`, `validate` et `onSubmit`. Avec lui — react-hook-form,
|
|
51
|
+
* Formik, votre propre magasin — le formulaire ne fait plus que rendre.
|
|
52
|
+
*/
|
|
53
|
+
form?: FormAdapter<TValues>;
|
|
54
|
+
defaultValues?: TValues;
|
|
55
|
+
/** La validation. Un schéma Zod s'y branche en cinq lignes. */
|
|
56
|
+
validate?: FormValidator<TValues>;
|
|
57
|
+
onSubmit?: (values: TValues) => void | Promise<void>;
|
|
58
|
+
columns?: number;
|
|
59
|
+
submitText?: ReactNode;
|
|
60
|
+
clearText?: ReactNode;
|
|
61
|
+
showClear?: boolean;
|
|
62
|
+
submitProps?: ButtonProps;
|
|
63
|
+
/** Remplace entièrement la barre de boutons. */
|
|
64
|
+
actions?: ReactNode;
|
|
65
|
+
loading?: boolean;
|
|
66
|
+
/** Désactive l'envoi tant que rien n'a changé. */
|
|
67
|
+
requireDirty?: boolean;
|
|
68
|
+
/** Désactive l'envoi tant qu'une erreur est en vigueur. */
|
|
69
|
+
requireValid?: boolean;
|
|
70
|
+
className?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const VIDE = {} as FormShape;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Un formulaire décrit par ses champs.
|
|
77
|
+
*
|
|
78
|
+
* Il ne connaît aucune bibliothèque de formulaires : il reçoit un
|
|
79
|
+
* `FormAdapter`, ou en monte un lui-même. C'est ce qui lui permet de servir un
|
|
80
|
+
* projet sous react-hook-form et un projet sans rien, avec le même code.
|
|
81
|
+
*
|
|
82
|
+
* ```tsx
|
|
83
|
+
* <Form
|
|
84
|
+
* columns={2}
|
|
85
|
+
* defaultValues={{ nom: "", email: "" }}
|
|
86
|
+
* validate={createValidator({ nom: [required()], email: [required(), email()] })}
|
|
87
|
+
* onSubmit={(values) => api.post("/clients", { body: values })}
|
|
88
|
+
* fields={[
|
|
89
|
+
* { name: "nom", label: "Nom du client", required: true },
|
|
90
|
+
* { name: "email", label: "Adresse e-mail", type: "email", required: true },
|
|
91
|
+
* ]}
|
|
92
|
+
* />
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function Form<TValues extends FormShape = FormShape>({
|
|
96
|
+
fields,
|
|
97
|
+
form: adapter,
|
|
98
|
+
defaultValues,
|
|
99
|
+
validate,
|
|
100
|
+
onSubmit,
|
|
101
|
+
columns = 1,
|
|
102
|
+
submitText = "Enregistrer",
|
|
103
|
+
clearText = "Réinitialiser",
|
|
104
|
+
showClear = false,
|
|
105
|
+
submitProps,
|
|
106
|
+
actions,
|
|
107
|
+
loading = false,
|
|
108
|
+
requireDirty = true,
|
|
109
|
+
requireValid = true,
|
|
110
|
+
className,
|
|
111
|
+
}: FormProps<TValues>) {
|
|
112
|
+
// Le hook est appelé sans condition, comme il se doit; son résultat n'est
|
|
113
|
+
// utilisé que si aucun adaptateur n'a été fourni. Un `useLocalForm` inerte
|
|
114
|
+
// ne coûte que trois `useState`.
|
|
115
|
+
const interne = useLocalForm<TValues>({
|
|
116
|
+
defaultValues: (defaultValues ?? VIDE) as TValues,
|
|
117
|
+
...(validate ? { validate } : {}),
|
|
118
|
+
...(onSubmit ? { onSubmit } : {}),
|
|
119
|
+
});
|
|
120
|
+
const form = adapter ?? interne;
|
|
121
|
+
|
|
122
|
+
const busy = loading || form.state.isSubmitting;
|
|
123
|
+
const submitDisabled =
|
|
124
|
+
busy ||
|
|
125
|
+
(requireDirty && !form.state.isDirty) ||
|
|
126
|
+
(requireValid && !form.state.isValid);
|
|
127
|
+
|
|
128
|
+
const grille = (nombre: number): CSSProperties => ({
|
|
129
|
+
gridTemplateColumns: `repeat(${nombre}, minmax(0, 1fr))`,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const rendre = (config: FormFieldConfig) => {
|
|
133
|
+
const { name, colSpan, rowSpan, ...props } = config;
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div
|
|
137
|
+
key={name}
|
|
138
|
+
className="sia-form-cell"
|
|
139
|
+
style={{
|
|
140
|
+
...(columns > 1 && colSpan ? { gridColumn: `span ${colSpan}` } : {}),
|
|
141
|
+
...(columns > 1 && rowSpan ? { gridRow: `span ${rowSpan}` } : {}),
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<Field
|
|
145
|
+
{...props}
|
|
146
|
+
field={form.bind(name as keyof TValues & string)}
|
|
147
|
+
{...(busy ? { disabled: true } : {})}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<form
|
|
155
|
+
className={cn("sia-form", className)}
|
|
156
|
+
data-columns={columns}
|
|
157
|
+
noValidate
|
|
158
|
+
onSubmit={(event) => {
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
void form.submit();
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
<div className="sia-form-grid" style={grille(columns)}>
|
|
164
|
+
{fields.map((entry) =>
|
|
165
|
+
isGroup(entry) ? (
|
|
166
|
+
<fieldset key={entry.group} className="sia-form-group">
|
|
167
|
+
<legend className="sia-form-group-header">
|
|
168
|
+
<span className="sia-form-group-title">{entry.group}</span>
|
|
169
|
+
{entry.description && (
|
|
170
|
+
<span className="sia-form-group-desc">{entry.description}</span>
|
|
171
|
+
)}
|
|
172
|
+
</legend>
|
|
173
|
+
|
|
174
|
+
<div
|
|
175
|
+
className="sia-form-group-grid"
|
|
176
|
+
style={grille(columns === 1 ? 1 : (entry.columns ?? columns))}
|
|
177
|
+
>
|
|
178
|
+
{entry.fields.map(rendre)}
|
|
179
|
+
</div>
|
|
180
|
+
</fieldset>
|
|
181
|
+
) : (
|
|
182
|
+
rendre(entry)
|
|
183
|
+
),
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{actions ?? (
|
|
188
|
+
<div className="sia-form-actions">
|
|
189
|
+
{showClear && (
|
|
190
|
+
<Button
|
|
191
|
+
type="button"
|
|
192
|
+
variant="ghost"
|
|
193
|
+
disabled={busy}
|
|
194
|
+
onClick={() => form.reset()}
|
|
195
|
+
>
|
|
196
|
+
{clearText}
|
|
197
|
+
</Button>
|
|
198
|
+
)}
|
|
199
|
+
<Button
|
|
200
|
+
type="submit"
|
|
201
|
+
{...submitProps}
|
|
202
|
+
loading={busy}
|
|
203
|
+
disabled={submitDisabled}
|
|
204
|
+
>
|
|
205
|
+
{submitText}
|
|
206
|
+
</Button>
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
</form>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
.sia-form {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 1.5rem;
|
|
5
|
+
width: 100%;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.sia-form-grid {
|
|
9
|
+
display: grid;
|
|
10
|
+
gap: 1rem;
|
|
11
|
+
width: 100%;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Indispensable : sans lui, un champ au contenu large — une longue option de
|
|
15
|
+
`Select` — fait déborder toute la colonne au lieu de se tronquer. */
|
|
16
|
+
.sia-form-cell { min-width: 0; }
|
|
17
|
+
|
|
18
|
+
/* Sur une seule colonne, les portées n'ont plus de sens. */
|
|
19
|
+
.sia-form[data-columns="1"] .sia-form-cell {
|
|
20
|
+
grid-column: auto;
|
|
21
|
+
grid-row: auto;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* ── Groupes ───────────────────────────────────────────────────────────── */
|
|
25
|
+
|
|
26
|
+
.sia-form-group {
|
|
27
|
+
grid-column: 1 / -1;
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
gap: 1rem;
|
|
31
|
+
margin: 0;
|
|
32
|
+
padding: 0;
|
|
33
|
+
border: 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* Un `fieldset` plutôt qu'un `div` : le lecteur d'écran annonce alors le
|
|
37
|
+
titre du groupe avant chaque champ qu'il contient. */
|
|
38
|
+
.sia-form-group-header {
|
|
39
|
+
display: block;
|
|
40
|
+
width: 100%;
|
|
41
|
+
padding: 0 0 .5rem;
|
|
42
|
+
border-bottom: 1px solid var(--sia-border);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.sia-form-group-title {
|
|
46
|
+
display: block;
|
|
47
|
+
color: var(--sia-foreground);
|
|
48
|
+
font-size: 1rem;
|
|
49
|
+
font-weight: 600;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.sia-form-group-desc {
|
|
53
|
+
display: block;
|
|
54
|
+
margin-top: .25rem;
|
|
55
|
+
color: var(--sia-muted);
|
|
56
|
+
font-size: .875rem;
|
|
57
|
+
line-height: 1.45;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.sia-form-group-grid { display: grid; gap: 1rem; }
|
|
61
|
+
|
|
62
|
+
/* ── Actions ───────────────────────────────────────────────────────────── */
|
|
63
|
+
|
|
64
|
+
.sia-form-actions {
|
|
65
|
+
display: flex;
|
|
66
|
+
justify-content: flex-end;
|
|
67
|
+
gap: .75rem;
|
|
68
|
+
padding-top: 1rem;
|
|
69
|
+
border-top: 1px solid var(--sia-border);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* Sous 640 px, deux colonnes de champs deviennent illisibles. */
|
|
73
|
+
@media (max-width: 640px) {
|
|
74
|
+
.sia-form-grid,
|
|
75
|
+
.sia-form-group-grid {
|
|
76
|
+
grid-template-columns: 1fr !important;
|
|
77
|
+
}
|
|
78
|
+
.sia-form-actions { flex-direction: column-reverse; }
|
|
79
|
+
.sia-form-actions > * { width: 100%; }
|
|
80
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneElement,
|
|
3
|
+
isValidElement,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
type ReactElement,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { cn } from "@sia-ui/utils";
|
|
12
|
+
import { Overlay, type OverlayPlacement } from "../Overlay";
|
|
13
|
+
import "./styles.css";
|
|
14
|
+
|
|
15
|
+
export interface HoverCardProps {
|
|
16
|
+
content: ReactNode;
|
|
17
|
+
children: ReactElement<{ onMouseEnter?: () => void; onFocus?: () => void }>;
|
|
18
|
+
/**
|
|
19
|
+
* Délai avant ouverture. Il n'est pas cosmétique : sans lui, traverser la
|
|
20
|
+
* page avec la souris ouvre et ferme une dizaine de panneaux au passage.
|
|
21
|
+
*/
|
|
22
|
+
openDelay?: number;
|
|
23
|
+
/** Délai avant fermeture, le temps d'atteindre le panneau à la souris. */
|
|
24
|
+
closeDelay?: number;
|
|
25
|
+
placement?: OverlayPlacement;
|
|
26
|
+
open?: boolean;
|
|
27
|
+
onOpenChange?: (open: boolean) => void;
|
|
28
|
+
width?: number | string;
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Un aperçu au survol.
|
|
34
|
+
*
|
|
35
|
+
* Ce n'est pas un `Tooltip` : celui-ci porte une phrase et se lit d'un coup
|
|
36
|
+
* d'œil, celui-là porte du contenu — une fiche, un résumé, une image — que
|
|
37
|
+
* l'on peut vouloir survoler à son tour, voire sélectionner.
|
|
38
|
+
*/
|
|
39
|
+
export function HoverCard({
|
|
40
|
+
content,
|
|
41
|
+
children,
|
|
42
|
+
openDelay = 300,
|
|
43
|
+
closeDelay = 150,
|
|
44
|
+
placement = "bottom-start",
|
|
45
|
+
open: openProp,
|
|
46
|
+
onOpenChange,
|
|
47
|
+
width = 20 * 16,
|
|
48
|
+
className,
|
|
49
|
+
}: HoverCardProps) {
|
|
50
|
+
const anchorRef = useRef<HTMLElement>(null);
|
|
51
|
+
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
52
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
53
|
+
const open = openProp ?? internalOpen;
|
|
54
|
+
|
|
55
|
+
const setOpen = useCallback(
|
|
56
|
+
(next: boolean) => {
|
|
57
|
+
if (openProp === undefined) setInternalOpen(next);
|
|
58
|
+
onOpenChange?.(next);
|
|
59
|
+
},
|
|
60
|
+
[onOpenChange, openProp],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const schedule = useCallback(
|
|
64
|
+
(next: boolean, delay: number) => {
|
|
65
|
+
if (timer.current) clearTimeout(timer.current);
|
|
66
|
+
timer.current = setTimeout(() => setOpen(next), delay);
|
|
67
|
+
},
|
|
68
|
+
[setOpen],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
useEffect(
|
|
72
|
+
() => () => {
|
|
73
|
+
if (timer.current) clearTimeout(timer.current);
|
|
74
|
+
},
|
|
75
|
+
[],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const trigger = isValidElement(children)
|
|
79
|
+
? cloneElement(children, {
|
|
80
|
+
ref: anchorRef,
|
|
81
|
+
// Le focus ouvre aussi : au clavier il n'y a pas de survol, et une
|
|
82
|
+
// fiche qu'on ne peut atteindre qu'à la souris n'existe pas pour
|
|
83
|
+
// tout le monde.
|
|
84
|
+
onFocus: () => {
|
|
85
|
+
children.props.onFocus?.();
|
|
86
|
+
schedule(true, 0);
|
|
87
|
+
},
|
|
88
|
+
onBlur: () => schedule(false, closeDelay),
|
|
89
|
+
onMouseEnter: () => {
|
|
90
|
+
children.props.onMouseEnter?.();
|
|
91
|
+
schedule(true, openDelay);
|
|
92
|
+
},
|
|
93
|
+
onMouseLeave: () => schedule(false, closeDelay),
|
|
94
|
+
} as never)
|
|
95
|
+
: children;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<span className="sia-hover-card-anchor">
|
|
99
|
+
{trigger}
|
|
100
|
+
<Overlay
|
|
101
|
+
open={open}
|
|
102
|
+
anchorRef={anchorRef}
|
|
103
|
+
onOpenChange={setOpen}
|
|
104
|
+
placement={placement}
|
|
105
|
+
className={cn("sia-hover-card", className)}
|
|
106
|
+
role="dialog"
|
|
107
|
+
>
|
|
108
|
+
<div
|
|
109
|
+
style={{ width }}
|
|
110
|
+
className="sia-hover-card__body"
|
|
111
|
+
// Survoler le panneau le maintient ouvert : sans cela il se ferme
|
|
112
|
+
// dès que la souris quitte le déclencheur pour y aller.
|
|
113
|
+
onMouseEnter={() => schedule(true, 0)}
|
|
114
|
+
onMouseLeave={() => schedule(false, closeDelay)}
|
|
115
|
+
>
|
|
116
|
+
{content}
|
|
117
|
+
</div>
|
|
118
|
+
</Overlay>
|
|
119
|
+
</span>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.sia-hover-card-anchor { display: inline-flex; }
|
|
2
|
+
|
|
3
|
+
.sia-hover-card {
|
|
4
|
+
padding: 0;
|
|
5
|
+
border: 0;
|
|
6
|
+
background: transparent;
|
|
7
|
+
box-shadow: none;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.sia-hover-card__body {
|
|
11
|
+
max-width: calc(100vw - 2rem);
|
|
12
|
+
padding: .875rem 1rem;
|
|
13
|
+
border: 1px solid var(--sia-border);
|
|
14
|
+
border-radius: calc(var(--sia-radius) + 2px);
|
|
15
|
+
background: var(--sia-surface-raised);
|
|
16
|
+
box-shadow: var(--sia-elevation-overlay);
|
|
17
|
+
color: var(--sia-foreground);
|
|
18
|
+
font-size: .875rem;
|
|
19
|
+
line-height: 1.5;
|
|
20
|
+
animation: sia-hover-card-in var(--sia-duration-base) var(--sia-easing-entrance);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@keyframes sia-hover-card-in {
|
|
24
|
+
from { opacity: 0; transform: translateY(6px) scale(.98); }
|
|
25
|
+
to { opacity: 1; transform: none; }
|
|
26
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "@sia-ui/utils";
|
|
3
|
+
import { Tooltip, type TooltipProps } from "../Tooltip";
|
|
4
|
+
import { useComponentDefaults } from "@sia-ui/headless";
|
|
5
|
+
import "./styles.css";
|
|
6
|
+
|
|
7
|
+
export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
8
|
+
label: string;
|
|
9
|
+
icon: ReactNode;
|
|
10
|
+
variant?: "solid" | "outline" | "ghost";
|
|
11
|
+
size?: "sm" | "md" | "lg";
|
|
12
|
+
radius?: "default" | "none" | "sm" | "lg" | "full";
|
|
13
|
+
tooltip?: Omit<TooltipProps, "children">;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
17
|
+
({ label, icon, variant: variantProp, size: sizeProp, radius: radiusProp, tooltip, className, type = "button", ...props }, ref) => {
|
|
18
|
+
const defaults = useComponentDefaults<IconButtonProps>("iconButton");
|
|
19
|
+
const variant = variantProp ?? defaults.variant ?? "ghost";
|
|
20
|
+
const size = sizeProp ?? defaults.size ?? "md";
|
|
21
|
+
const radius = radiusProp ?? defaults.radius ?? "default";
|
|
22
|
+
const button = <button ref={ref} type={type} aria-label={label} {...(!tooltip ? { title: label } : {})} className={cn("sia-icon-button", `sia-icon-button--${variant}`, `sia-icon-button--${size}`, `sia-radius--${radius}`, className)} {...props}>{icon}</button>;
|
|
23
|
+
return tooltip ? <Tooltip {...tooltip}>{button}</Tooltip> : button;
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
IconButton.displayName = "IconButton";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
.sia-icon-button { display:inline-grid; place-items:center; width:2.5rem; height:2.5rem; padding:0; border:1px solid transparent; border-radius:var(--sia-radius); color:var(--sia-foreground); background:transparent; font:inherit; cursor:pointer; }
|
|
2
|
+
.sia-icon-button--sm { width:2rem; height:2rem; }
|
|
3
|
+
.sia-icon-button--lg { width:3rem; height:3rem; }
|
|
4
|
+
.sia-icon-button--solid { color:var(--sia-primary-foreground); background:var(--sia-primary); }
|
|
5
|
+
.sia-icon-button--outline { border-color:var(--sia-border); background:var(--sia-background); }
|
|
6
|
+
.sia-icon-button:hover { background:color-mix(in srgb,var(--sia-primary) 8%,var(--sia-background)); }
|
|
7
|
+
.sia-icon-button:focus-visible { outline:3px solid color-mix(in srgb,var(--sia-primary) 30%,transparent); outline-offset:2px; }
|