@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.
Files changed (200) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/diff.mjs +91 -0
  4. package/dist/index.js +325 -0
  5. package/dist/lifecycle.mjs +242 -0
  6. package/dist/lock.mjs +92 -0
  7. package/dist/registry/package/components/Accordion/index.tsx +172 -0
  8. package/dist/registry/package/components/Accordion/styles.css +97 -0
  9. package/dist/registry/package/components/Alert/index.tsx +31 -0
  10. package/dist/registry/package/components/Alert/styles.css +12 -0
  11. package/dist/registry/package/components/AmountDisplay/index.tsx +11 -0
  12. package/dist/registry/package/components/AmountDisplay/styles.css +9 -0
  13. package/dist/registry/package/components/AppShell/index.tsx +6 -0
  14. package/dist/registry/package/components/AppShell/styles.css +7 -0
  15. package/dist/registry/package/components/AspectRatio/index.tsx +29 -0
  16. package/dist/registry/package/components/AspectRatio/styles.css +15 -0
  17. package/dist/registry/package/components/AsyncState/index.tsx +22 -0
  18. package/dist/registry/package/components/AsyncState/styles.css +1 -0
  19. package/dist/registry/package/components/AuditMeta/index.tsx +44 -0
  20. package/dist/registry/package/components/AuditMeta/styles.css +12 -0
  21. package/dist/registry/package/components/AuthLayout/index.tsx +39 -0
  22. package/dist/registry/package/components/AuthLayout/styles.css +6 -0
  23. package/dist/registry/package/components/Autocomplete/index.tsx +8 -0
  24. package/dist/registry/package/components/Avatar/index.tsx +23 -0
  25. package/dist/registry/package/components/Avatar/styles.css +5 -0
  26. package/dist/registry/package/components/Badge/index.tsx +62 -0
  27. package/dist/registry/package/components/Badge/styles.css +26 -0
  28. package/dist/registry/package/components/Breadcrumb/index.tsx +131 -0
  29. package/dist/registry/package/components/Breadcrumb/styles.css +76 -0
  30. package/dist/registry/package/components/Button/index.tsx +91 -0
  31. package/dist/registry/package/components/Button/styles.css +111 -0
  32. package/dist/registry/package/components/Calendar/index.tsx +192 -0
  33. package/dist/registry/package/components/Card/index.tsx +8 -0
  34. package/dist/registry/package/components/Card/styles.css +4 -0
  35. package/dist/registry/package/components/Chart/index.tsx +437 -0
  36. package/dist/registry/package/components/Chart/styles.css +182 -0
  37. package/dist/registry/package/components/Checkbox/index.tsx +193 -0
  38. package/dist/registry/package/components/Checkbox/styles.css +39 -0
  39. package/dist/registry/package/components/ColorPicker/index.tsx +6 -0
  40. package/dist/registry/package/components/ColorPicker/styles.css +2 -0
  41. package/dist/registry/package/components/CommandPalette/index.tsx +7 -0
  42. package/dist/registry/package/components/CommandPalette/styles.css +8 -0
  43. package/dist/registry/package/components/ConfirmDialog/index.tsx +24 -0
  44. package/dist/registry/package/components/Container/index.tsx +11 -0
  45. package/dist/registry/package/components/Container/styles.css +1 -0
  46. package/dist/registry/package/components/Countdown/index.tsx +3 -0
  47. package/dist/registry/package/components/CrudPage/index.tsx +9 -0
  48. package/dist/registry/package/components/CrudPage/styles.css +1 -0
  49. package/dist/registry/package/components/CurrencyInput/index.tsx +15 -0
  50. package/dist/registry/package/components/CurrencyInput/styles.css +1 -0
  51. package/dist/registry/package/components/DataTable/index.tsx +240 -0
  52. package/dist/registry/package/components/DataTable/styles.css +98 -0
  53. package/dist/registry/package/components/DatePicker/index.tsx +102 -0
  54. package/dist/registry/package/components/DateRangeFilter/index.tsx +8 -0
  55. package/dist/registry/package/components/DateRangeFilter/styles.css +4 -0
  56. package/dist/registry/package/components/DateRangePicker/index.tsx +36 -0
  57. package/dist/registry/package/components/DateTimePicker/index.tsx +32 -0
  58. package/dist/registry/package/components/Descriptions/index.tsx +7 -0
  59. package/dist/registry/package/components/Descriptions/styles.css +8 -0
  60. package/dist/registry/package/components/Divider/index.tsx +12 -0
  61. package/dist/registry/package/components/Divider/styles.css +3 -0
  62. package/dist/registry/package/components/Drawer/context.ts +14 -0
  63. package/dist/registry/package/components/Drawer/index.tsx +209 -0
  64. package/dist/registry/package/components/Drawer/styles.css +151 -0
  65. package/dist/registry/package/components/DropdownMenu/index.tsx +350 -0
  66. package/dist/registry/package/components/DropdownMenu/styles.css +100 -0
  67. package/dist/registry/package/components/DurationDisplay/index.tsx +3 -0
  68. package/dist/registry/package/components/EmptyState/index.tsx +23 -0
  69. package/dist/registry/package/components/EmptyState/styles.css +6 -0
  70. package/dist/registry/package/components/EntityMeta/index.tsx +65 -0
  71. package/dist/registry/package/components/EntityMeta/styles.css +9 -0
  72. package/dist/registry/package/components/ErrorState/index.tsx +30 -0
  73. package/dist/registry/package/components/ErrorState/styles.css +6 -0
  74. package/dist/registry/package/components/EventCalendar/index.tsx +8 -0
  75. package/dist/registry/package/components/EventCalendar/styles.css +8 -0
  76. package/dist/registry/package/components/Field/index.tsx +664 -0
  77. package/dist/registry/package/components/Field/styles.css +14 -0
  78. package/dist/registry/package/components/FileUpload/index.tsx +68 -0
  79. package/dist/registry/package/components/FileUpload/styles.css +4 -0
  80. package/dist/registry/package/components/FiltersBar/index.tsx +20 -0
  81. package/dist/registry/package/components/FiltersBar/styles.css +6 -0
  82. package/dist/registry/package/components/Form/index.tsx +211 -0
  83. package/dist/registry/package/components/Form/styles.css +80 -0
  84. package/dist/registry/package/components/FormField/index.tsx +2 -0
  85. package/dist/registry/package/components/HoverCard/index.tsx +121 -0
  86. package/dist/registry/package/components/HoverCard/styles.css +26 -0
  87. package/dist/registry/package/components/IconButton/index.tsx +27 -0
  88. package/dist/registry/package/components/IconButton/styles.css +7 -0
  89. package/dist/registry/package/components/Icons/index.tsx +185 -0
  90. package/dist/registry/package/components/ImageUpload/index.tsx +6 -0
  91. package/dist/registry/package/components/ImageUpload/styles.css +4 -0
  92. package/dist/registry/package/components/Input/index.tsx +103 -0
  93. package/dist/registry/package/components/Input/styles.css +8 -0
  94. package/dist/registry/package/components/InputNumber/index.tsx +196 -0
  95. package/dist/registry/package/components/InputNumber/styles.css +54 -0
  96. package/dist/registry/package/components/JsonEditor/index.tsx +6 -0
  97. package/dist/registry/package/components/JsonEditor/styles.css +2 -0
  98. package/dist/registry/package/components/KpiGrid/index.tsx +11 -0
  99. package/dist/registry/package/components/KpiGrid/styles.css +5 -0
  100. package/dist/registry/package/components/Label/index.tsx +43 -0
  101. package/dist/registry/package/components/Label/styles.css +25 -0
  102. package/dist/registry/package/components/MarkdownEditor/index.tsx +5 -0
  103. package/dist/registry/package/components/MiniCalendar/index.tsx +3 -0
  104. package/dist/registry/package/components/Modal/index.tsx +104 -0
  105. package/dist/registry/package/components/Modal/styles.css +20 -0
  106. package/dist/registry/package/components/MonthPicker/index.tsx +5 -0
  107. package/dist/registry/package/components/MultiSelect/index.tsx +133 -0
  108. package/dist/registry/package/components/MultiSelect/styles.css +3 -0
  109. package/dist/registry/package/components/OtpInput/index.tsx +5 -0
  110. package/dist/registry/package/components/OtpInput/styles.css +2 -0
  111. package/dist/registry/package/components/Overlay/index.tsx +140 -0
  112. package/dist/registry/package/components/Overlay/styles.css +1 -0
  113. package/dist/registry/package/components/PageHeader/index.tsx +24 -0
  114. package/dist/registry/package/components/PageHeader/styles.css +5 -0
  115. package/dist/registry/package/components/Pagination/index.tsx +155 -0
  116. package/dist/registry/package/components/Pagination/styles.css +103 -0
  117. package/dist/registry/package/components/PasswordInput/index.tsx +125 -0
  118. package/dist/registry/package/components/PasswordInput/styles.css +58 -0
  119. package/dist/registry/package/components/PermissionGate/index.tsx +3 -0
  120. package/dist/registry/package/components/PhoneInput/index.tsx +189 -0
  121. package/dist/registry/package/components/PhoneInput/styles.css +35 -0
  122. package/dist/registry/package/components/Popover/index.tsx +12 -0
  123. package/dist/registry/package/components/Popover/styles.css +12 -0
  124. package/dist/registry/package/components/Progress/index.tsx +149 -0
  125. package/dist/registry/package/components/Progress/styles.css +101 -0
  126. package/dist/registry/package/components/QrCode/index.tsx +6 -0
  127. package/dist/registry/package/components/QrCode/styles.css +5 -0
  128. package/dist/registry/package/components/RadioGroup/index.tsx +29 -0
  129. package/dist/registry/package/components/RadioGroup/styles.css +20 -0
  130. package/dist/registry/package/components/Rating/index.tsx +82 -0
  131. package/dist/registry/package/components/Rating/styles.css +41 -0
  132. package/dist/registry/package/components/ReferenceSelect/index.tsx +7 -0
  133. package/dist/registry/package/components/ReferenceSelect/styles.css +2 -0
  134. package/dist/registry/package/components/RelativeTime/index.tsx +22 -0
  135. package/dist/registry/package/components/RelativeTime/styles.css +1 -0
  136. package/dist/registry/package/components/RequireSession/index.tsx +127 -0
  137. package/dist/registry/package/components/RequireSession/styles.css +13 -0
  138. package/dist/registry/package/components/ResizablePanel/index.tsx +6 -0
  139. package/dist/registry/package/components/ResizablePanel/styles.css +5 -0
  140. package/dist/registry/package/components/RichTextEditor/index.tsx +5 -0
  141. package/dist/registry/package/components/RichTextEditor/styles.css +2 -0
  142. package/dist/registry/package/components/ScrollArea/index.tsx +114 -0
  143. package/dist/registry/package/components/ScrollArea/styles.css +74 -0
  144. package/dist/registry/package/components/SearchInput/index.tsx +121 -0
  145. package/dist/registry/package/components/SearchInput/styles.css +6 -0
  146. package/dist/registry/package/components/Select/index.tsx +324 -0
  147. package/dist/registry/package/components/Select/styles.css +58 -0
  148. package/dist/registry/package/components/Sidebar/index.tsx +232 -0
  149. package/dist/registry/package/components/Sidebar/styles.css +140 -0
  150. package/dist/registry/package/components/Skeleton/index.tsx +21 -0
  151. package/dist/registry/package/components/Skeleton/styles.css +2 -0
  152. package/dist/registry/package/components/Slider/index.tsx +193 -0
  153. package/dist/registry/package/components/Slider/styles.css +53 -0
  154. package/dist/registry/package/components/Spinner/index.tsx +28 -0
  155. package/dist/registry/package/components/Spinner/styles.css +8 -0
  156. package/dist/registry/package/components/StatCard/index.tsx +15 -0
  157. package/dist/registry/package/components/StatCard/styles.css +3 -0
  158. package/dist/registry/package/components/Statistic/index.tsx +23 -0
  159. package/dist/registry/package/components/Statistic/styles.css +6 -0
  160. package/dist/registry/package/components/Steps/index.tsx +108 -0
  161. package/dist/registry/package/components/Steps/styles.css +138 -0
  162. package/dist/registry/package/components/Switch/index.tsx +46 -0
  163. package/dist/registry/package/components/Switch/styles.css +26 -0
  164. package/dist/registry/package/components/Tabs/index.tsx +44 -0
  165. package/dist/registry/package/components/Tabs/styles.css +41 -0
  166. package/dist/registry/package/components/TagsInput/index.tsx +6 -0
  167. package/dist/registry/package/components/TagsInput/styles.css +2 -0
  168. package/dist/registry/package/components/Textarea/index.tsx +21 -0
  169. package/dist/registry/package/components/Textarea/styles.css +7 -0
  170. package/dist/registry/package/components/TimePicker/index.tsx +76 -0
  171. package/dist/registry/package/components/TimePicker/styles.css +13 -0
  172. package/dist/registry/package/components/TimeRangePicker/index.tsx +32 -0
  173. package/dist/registry/package/components/TimeRangePicker/styles.css +1 -0
  174. package/dist/registry/package/components/Timeline/index.tsx +8 -0
  175. package/dist/registry/package/components/Timeline/styles.css +12 -0
  176. package/dist/registry/package/components/Timer/index.tsx +9 -0
  177. package/dist/registry/package/components/Toast/index.tsx +426 -0
  178. package/dist/registry/package/components/Toast/styles.css +175 -0
  179. package/dist/registry/package/components/Toggle/index.tsx +181 -0
  180. package/dist/registry/package/components/Toggle/styles.css +124 -0
  181. package/dist/registry/package/components/Tooltip/index.tsx +163 -0
  182. package/dist/registry/package/components/Tooltip/styles.css +57 -0
  183. package/dist/registry/package/components/Tour/index.tsx +8 -0
  184. package/dist/registry/package/components/Tour/styles.css +6 -0
  185. package/dist/registry/package/components/Transfer/index.tsx +7 -0
  186. package/dist/registry/package/components/Transfer/styles.css +6 -0
  187. package/dist/registry/package/components/Tree/index.tsx +8 -0
  188. package/dist/registry/package/components/Tree/styles.css +6 -0
  189. package/dist/registry/package/components/TreeSelect/index.tsx +10 -0
  190. package/dist/registry/package/components/TreeSelect/styles.css +3 -0
  191. package/dist/registry/package/components/Typography/index.tsx +27 -0
  192. package/dist/registry/package/components/Typography/styles.css +16 -0
  193. package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +4 -0
  194. package/dist/registry/package/components/WeekPicker/index.tsx +5 -0
  195. package/dist/registry/package/components/YearPicker/index.tsx +3 -0
  196. package/dist/registry/package/lib/form-adapter-rhf.tsx +141 -0
  197. package/dist/registry/registry.json +1780 -0
  198. package/dist/registry/styles/sia-ui.css +361 -0
  199. package/dist/source.mjs +93 -0
  200. 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,2 @@
1
+ export { Field as FormField } from "../Field";
2
+ export type { FieldProps as FormFieldProps } from "../Field";
@@ -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; }