@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,104 @@
1
+ import { createPortal } from "react-dom";
2
+ import { useEffect, useId, useRef, type ReactNode } from "react";
3
+ import { cn } from "@sia-ui/utils";
4
+ import { useSiaLocale } from "@sia-ui/headless";
5
+ import { XIcon } from "../Icons";
6
+ import "./styles.css";
7
+
8
+ export interface ModalProps {
9
+ open: boolean;
10
+ onOpenChange: (open: boolean) => void;
11
+ title: ReactNode;
12
+ description?: ReactNode;
13
+ children?: ReactNode;
14
+ footer?: ReactNode;
15
+ closeOnBackdrop?: boolean;
16
+ closeLabel?: string;
17
+ className?: string;
18
+ }
19
+
20
+ export function Modal({
21
+ open,
22
+ onOpenChange,
23
+ title,
24
+ description,
25
+ children,
26
+ footer,
27
+ closeOnBackdrop = true,
28
+ closeLabel: closeLabelProp,
29
+ className
30
+ }: ModalProps) {
31
+ const locale = useSiaLocale();
32
+ const closeLabel = closeLabelProp ?? locale.close;
33
+ const titleId = useId();
34
+ const descriptionId = useId();
35
+ const dialogRef = useRef<HTMLElement>(null);
36
+
37
+ useEffect(() => {
38
+ if (!open) return;
39
+ const previousFocus = document.activeElement as HTMLElement | null;
40
+ const previousOverflow = document.body.style.overflow;
41
+ document.body.style.overflow = "hidden";
42
+
43
+ const focusableSelector = "button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex='-1'])";
44
+ const focusable = () => Array.from(dialogRef.current?.querySelectorAll<HTMLElement>(focusableSelector) ?? []);
45
+ requestAnimationFrame(() => focusable()[0]?.focus());
46
+
47
+ const onKeyDown = (event: KeyboardEvent) => {
48
+ if (event.key === "Escape") onOpenChange(false);
49
+ if (event.key === "Tab") {
50
+ const elements = focusable();
51
+ if (elements.length === 0) {
52
+ event.preventDefault();
53
+ return;
54
+ }
55
+ const first = elements[0];
56
+ const last = elements[elements.length - 1];
57
+ if (event.shiftKey && document.activeElement === first) {
58
+ event.preventDefault();
59
+ last?.focus();
60
+ } else if (!event.shiftKey && document.activeElement === last) {
61
+ event.preventDefault();
62
+ first?.focus();
63
+ }
64
+ }
65
+ };
66
+ document.addEventListener("keydown", onKeyDown);
67
+ return () => {
68
+ document.removeEventListener("keydown", onKeyDown);
69
+ document.body.style.overflow = previousOverflow;
70
+ previousFocus?.focus();
71
+ };
72
+ }, [open, onOpenChange]);
73
+
74
+ if (!open || typeof document === "undefined") return null;
75
+
76
+ return createPortal(
77
+ <div
78
+ className="sia-modal-backdrop"
79
+ onMouseDown={(event) => {
80
+ if (closeOnBackdrop && event.target === event.currentTarget) onOpenChange(false);
81
+ }}
82
+ >
83
+ <section
84
+ ref={dialogRef}
85
+ className={cn("sia-modal", className)}
86
+ role="dialog"
87
+ aria-modal="true"
88
+ aria-labelledby={titleId}
89
+ aria-describedby={description ? descriptionId : undefined}
90
+ >
91
+ <header className="sia-modal__header">
92
+ <div>
93
+ <h2 id={titleId} className="sia-modal__title">{title}</h2>
94
+ {description && <p id={descriptionId} className="sia-modal__description">{description}</p>}
95
+ </div>
96
+ <button type="button" className="sia-modal__close" aria-label={closeLabel} onClick={() => onOpenChange(false)}><XIcon /></button>
97
+ </header>
98
+ {children && <div className="sia-modal__body">{children}</div>}
99
+ {footer && <footer className="sia-modal__footer">{footer}</footer>}
100
+ </section>
101
+ </div>,
102
+ document.body
103
+ );
104
+ }
@@ -0,0 +1,20 @@
1
+ .sia-modal-backdrop { position:fixed; inset:0; z-index:1300; display:grid; place-items:center; padding:1rem; background:rgb(2 6 23 / .62); backdrop-filter:blur(4px); }
2
+ .sia-modal { width:min(100%,34rem); max-height:min(90vh,48rem); overflow:auto; border:1px solid var(--sia-border); border-radius:calc(var(--sia-radius) * 1.5); color:var(--sia-foreground); background:var(--sia-background); box-shadow:0 24px 70px rgb(2 6 23 / .35); }
3
+ .sia-modal__header { display:flex; align-items:flex-start; justify-content:space-between; gap:1rem; padding:1.25rem; border-bottom:1px solid var(--sia-border); }
4
+ .sia-modal__title { margin:0; font-size:1.2rem; }
5
+ .sia-modal__description { margin:.35rem 0 0; color:var(--sia-muted); font-size:.9rem; line-height:1.5; }
6
+ .sia-modal__close { flex:none; padding:.1rem .4rem; border:0; color:var(--sia-muted); background:transparent; font-size:1.4rem; cursor:pointer; }
7
+ .sia-modal__body { padding:1.25rem; }
8
+ .sia-modal__footer { display:flex; justify-content:flex-end; gap:.75rem; padding:1rem 1.25rem; border-top:1px solid var(--sia-border); }
9
+
10
+ /* Entrée de la modale : le fond se pose, la boîte monte légèrement. Sortie
11
+ non animée — React démonte l'élément, il n'y a rien à faire attendre. */
12
+ .sia-modal-backdrop,
13
+ .sia-modal__backdrop {
14
+ animation: sia-fade-in var(--sia-duration-base) var(--sia-easing-entrance);
15
+ }
16
+
17
+ .sia-modal {
18
+ animation: sia-scale-in var(--sia-duration-slow) var(--sia-easing-entrance);
19
+ box-shadow: var(--sia-elevation-modal);
20
+ }
@@ -0,0 +1,5 @@
1
+ import { forwardRef, type InputHTMLAttributes } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ export interface MonthPickerProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "onChange"> { onValueChange?: (value: string) => void; invalid?: boolean; }
4
+ export const MonthPicker = forwardRef<HTMLInputElement, MonthPickerProps>(({ onValueChange, invalid, className, ...props }, ref) => <input {...props} ref={ref} type="month" className={cn("sia-month-picker", invalid && "sia-picker--invalid", className)} aria-invalid={invalid || undefined} onChange={(event) => onValueChange?.(event.target.value)} />);
5
+ MonthPicker.displayName = "MonthPicker";
@@ -0,0 +1,133 @@
1
+ import { useMemo, useState } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import type { SelectOption } from "../Select";
4
+ import "./styles.css";
5
+
6
+ export type { SelectOption } from "../Select";
7
+ import { XIcon } from "../Icons";
8
+ export interface MultiSelectProps {
9
+ id?: string;
10
+ options: SelectOption[];
11
+ value?: string[];
12
+ defaultValue?: string[];
13
+ onValueChange?: (value: string[]) => void;
14
+ placeholder?: string;
15
+ searchable?: boolean;
16
+ disabled?: boolean;
17
+ className?: string;
18
+ "aria-describedby"?: string;
19
+ "aria-labelledby"?: string;
20
+ "aria-invalid"?: boolean;
21
+ "aria-required"?: boolean;
22
+ }
23
+ export function MultiSelect({
24
+ id,
25
+ options,
26
+ value,
27
+ defaultValue = [],
28
+ onValueChange,
29
+ placeholder = "Sélectionner",
30
+ searchable = true,
31
+ disabled,
32
+ className,
33
+ ...ariaProps
34
+ }: MultiSelectProps) {
35
+ const [internal, setInternal] = useState(defaultValue);
36
+ const [open, setOpen] = useState(false);
37
+ const [query, setQuery] = useState("");
38
+ const current = value ?? internal;
39
+ const selected = new Set(current);
40
+ const filtered = useMemo(() => {
41
+ const needle = query.trim().toLowerCase();
42
+ return options.filter(
43
+ (option) =>
44
+ !needle ||
45
+ [String(option.label), ...(option.keywords ?? [])]
46
+ .join(" ")
47
+ .toLowerCase()
48
+ .includes(needle),
49
+ );
50
+ }, [options, query]);
51
+ const update = (next: string[]) => {
52
+ if (value === undefined) setInternal(next);
53
+ onValueChange?.(next);
54
+ };
55
+ return (
56
+ <div
57
+ className={cn(
58
+ "sia-multi-select",
59
+ disabled && "sia-multi-select--disabled",
60
+ className,
61
+ )}
62
+ {...ariaProps}
63
+ >
64
+ <div
65
+ id={id}
66
+ role="combobox"
67
+ tabIndex={disabled ? -1 : 0}
68
+ className="sia-multi-select__trigger"
69
+ aria-disabled={disabled || undefined}
70
+ aria-expanded={open}
71
+ onClick={() => !disabled && setOpen(!open)}
72
+ onKeyDown={(event) => {
73
+ if (!disabled && (event.key === "Enter" || event.key === " ")) {
74
+ event.preventDefault();
75
+ setOpen(!open);
76
+ }
77
+ }}
78
+ >
79
+ {current.length ? (
80
+ <span className="sia-multi-select__values">
81
+ {options
82
+ .filter((option) => selected.has(option.value))
83
+ .map((option) => (
84
+ <span key={option.value}>
85
+ {option.label}
86
+ <button
87
+ type="button"
88
+ aria-label={`Retirer ${String(option.label)}`}
89
+ onClick={(event) => {
90
+ event.stopPropagation();
91
+ update(current.filter((item) => item !== option.value));
92
+ }}
93
+ >
94
+ <XIcon />
95
+ </button>
96
+ </span>
97
+ ))}
98
+ </span>
99
+ ) : (
100
+ placeholder
101
+ )}
102
+ </div>
103
+ {open && (
104
+ <div className="sia-multi-select__panel">
105
+ {searchable && (
106
+ <input
107
+ value={query}
108
+ placeholder="Rechercher..."
109
+ onChange={(event) => setQuery(event.target.value)}
110
+ />
111
+ )}
112
+ {filtered.map((option) => (
113
+ <label key={option.value}>
114
+ <input
115
+ type="checkbox"
116
+ checked={selected.has(option.value)}
117
+ disabled={option.disabled}
118
+ onChange={(event) =>
119
+ update(
120
+ event.target.checked
121
+ ? [...current, option.value]
122
+ : current.filter((item) => item !== option.value),
123
+ )
124
+ }
125
+ />
126
+ {option.label}
127
+ </label>
128
+ ))}
129
+ </div>
130
+ )}
131
+ </div>
132
+ );
133
+ }
@@ -0,0 +1,3 @@
1
+ .sia-multi-select__values { display:flex; gap:.3rem; flex-wrap:wrap; }
2
+ .sia-multi-select__panel > input { width:100%; min-height:2.25rem; margin-bottom:.4rem; padding:0 .6rem; border:1px solid var(--sia-border); border-radius:.35rem; }
3
+ .sia-multi-select--disabled { opacity:.55; }
@@ -0,0 +1,5 @@
1
+ import { useRef, useState, type ClipboardEvent, type KeyboardEvent } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import "./styles.css";
4
+ export interface OtpInputProps { id?: string; value?: string; defaultValue?: string; onValueChange?: (value: string) => void; length?: number; inputMode?: "numeric" | "text"; mask?: boolean; disabled?: boolean; className?: string; "aria-describedby"?: string; "aria-labelledby"?: string; "aria-invalid"?: boolean; "aria-required"?: boolean; }
5
+ export function OtpInput({ id, value, defaultValue = "", onValueChange, length = 6, inputMode = "numeric", mask, disabled, className, ...ariaProps }: OtpInputProps) { const [internal, setInternal] = useState(defaultValue); const refs = useRef<Array<HTMLInputElement | null>>([]); const current = (value ?? internal).padEnd(length, " ").slice(0, length).split(""); const update = (chars: string[]) => { const next = chars.join("").trimEnd(); if (value === undefined) setInternal(next); onValueChange?.(next); }; const accept = (text: string) => inputMode === "numeric" ? text.replace(/\D/g, "") : text; const paste = (event: ClipboardEvent<HTMLDivElement>) => { event.preventDefault(); const next = accept(event.clipboardData.getData("text")).slice(0, length).padEnd(length, " ").split(""); update(next); refs.current[Math.min(next.join("").trimEnd().length, length - 1)]?.focus(); }; return <div id={id} className={cn("sia-otp-input", className)} onPaste={paste} {...ariaProps}>{current.map((char, index) => <input key={index} ref={(node) => { refs.current[index] = node; }} value={char.trim()} type={mask ? "password" : "text"} inputMode={inputMode} maxLength={1} disabled={disabled} aria-label={`Caractère ${index + 1}`} onChange={(event) => { const next = [...current]; next[index] = accept(event.target.value).slice(-1) || " "; update(next); if (next[index]?.trim()) refs.current[index + 1]?.focus(); }} onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => { if (event.key === "Backspace" && !current[index]?.trim()) refs.current[index - 1]?.focus(); if (event.key === "ArrowLeft") refs.current[index - 1]?.focus(); if (event.key === "ArrowRight") refs.current[index + 1]?.focus(); }} />)}</div>; }
@@ -0,0 +1,2 @@
1
+ .sia-otp-input { display:flex; gap:.5rem; }
2
+ .sia-otp-input input { width:2.75rem; height:3rem; border:1px solid var(--sia-border); border-radius:var(--sia-radius); color:var(--sia-foreground); background:var(--sia-background); font:inherit; font-size:1.25rem; text-align:center; }
@@ -0,0 +1,140 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useLayoutEffect,
5
+ useRef,
6
+ useState,
7
+ type CSSProperties,
8
+ type ReactNode,
9
+ type RefObject,
10
+ } from "react";
11
+ import { createPortal } from "react-dom";
12
+ import { cn } from "@sia-ui/utils";
13
+ import "./styles.css";
14
+
15
+ export type OverlayPlacement =
16
+ | "bottom-start"
17
+ | "bottom-end"
18
+ | "top-start"
19
+ | "top-end";
20
+
21
+ export interface OverlayProps {
22
+ open: boolean;
23
+ anchorRef: RefObject<HTMLElement | null>;
24
+ onOpenChange: (open: boolean) => void;
25
+ children: ReactNode;
26
+ placement?: OverlayPlacement;
27
+ offset?: number;
28
+ matchAnchorWidth?: boolean;
29
+ viewportPadding?: number;
30
+ className?: string;
31
+ id?: string;
32
+ role?: string;
33
+ }
34
+
35
+ type Position = CSSProperties & { visibility: "hidden" | "visible" };
36
+
37
+ export function Overlay({
38
+ open,
39
+ anchorRef,
40
+ onOpenChange,
41
+ children,
42
+ placement = "bottom-start",
43
+ offset = 6,
44
+ matchAnchorWidth = false,
45
+ viewportPadding = 8,
46
+ className,
47
+ ...props
48
+ }: OverlayProps) {
49
+ const overlayRef = useRef<HTMLDivElement>(null);
50
+ const [style, setStyle] = useState<Position>({
51
+ position: "fixed",
52
+ top: 0,
53
+ left: 0,
54
+ visibility: "hidden",
55
+ });
56
+
57
+ const updatePosition = useCallback(() => {
58
+ const anchor = anchorRef.current;
59
+ const overlay = overlayRef.current;
60
+ if (!anchor || !overlay) return;
61
+
62
+ const anchorRect = anchor.getBoundingClientRect();
63
+ const overlayRect = overlay.getBoundingClientRect();
64
+ const width = matchAnchorWidth
65
+ ? Math.max(anchorRect.width, overlayRect.width)
66
+ : overlayRect.width;
67
+ const spaceBelow = window.innerHeight - anchorRect.bottom - viewportPadding;
68
+ const spaceAbove = anchorRect.top - viewportPadding;
69
+ const requestedTop = placement.startsWith("top");
70
+ const useTop = requestedTop
71
+ ? spaceAbove >= overlayRect.height || spaceAbove > spaceBelow
72
+ : spaceBelow < overlayRect.height && spaceAbove > spaceBelow;
73
+ const rawTop = useTop
74
+ ? anchorRect.top - overlayRect.height - offset
75
+ : anchorRect.bottom + offset;
76
+ const alignEnd = placement.endsWith("end");
77
+ const rawLeft = alignEnd
78
+ ? anchorRect.right - width
79
+ : anchorRect.left;
80
+ const maxLeft = Math.max(viewportPadding, window.innerWidth - width - viewportPadding);
81
+ const maxTop = Math.max(
82
+ viewportPadding,
83
+ window.innerHeight - overlayRect.height - viewportPadding,
84
+ );
85
+
86
+ setStyle({
87
+ position: "fixed",
88
+ top: Math.min(Math.max(rawTop, viewportPadding), maxTop),
89
+ left: Math.min(Math.max(rawLeft, viewportPadding), maxLeft),
90
+ minWidth: matchAnchorWidth ? anchorRect.width : undefined,
91
+ maxWidth: `calc(100vw - ${viewportPadding * 2}px)`,
92
+ visibility: "visible",
93
+ });
94
+ }, [anchorRef, matchAnchorWidth, offset, placement, viewportPadding]);
95
+
96
+ useLayoutEffect(() => {
97
+ if (open) updatePosition();
98
+ }, [children, open, updatePosition]);
99
+
100
+ useEffect(() => {
101
+ if (!open) return;
102
+ const closeOnOutsidePress = (event: PointerEvent) => {
103
+ const target = event.target as Node;
104
+ if (
105
+ !anchorRef.current?.contains(target) &&
106
+ !overlayRef.current?.contains(target)
107
+ ) {
108
+ onOpenChange(false);
109
+ }
110
+ };
111
+ const closeOnEscape = (event: KeyboardEvent) => {
112
+ if (event.key === "Escape") onOpenChange(false);
113
+ };
114
+ const reposition = () => updatePosition();
115
+ document.addEventListener("pointerdown", closeOnOutsidePress, true);
116
+ document.addEventListener("keydown", closeOnEscape);
117
+ window.addEventListener("resize", reposition);
118
+ window.addEventListener("scroll", reposition, true);
119
+ return () => {
120
+ document.removeEventListener("pointerdown", closeOnOutsidePress, true);
121
+ document.removeEventListener("keydown", closeOnEscape);
122
+ window.removeEventListener("resize", reposition);
123
+ window.removeEventListener("scroll", reposition, true);
124
+ };
125
+ }, [anchorRef, onOpenChange, open, updatePosition]);
126
+
127
+ if (!open || typeof document === "undefined") return null;
128
+
129
+ return createPortal(
130
+ <div
131
+ ref={overlayRef}
132
+ className={cn("sia-overlay", className)}
133
+ style={style}
134
+ {...props}
135
+ >
136
+ {children}
137
+ </div>,
138
+ document.body,
139
+ );
140
+ }
@@ -0,0 +1 @@
1
+ .sia-overlay { z-index:1450; }
@@ -0,0 +1,24 @@
1
+ import { type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import "./styles.css";
4
+
5
+ export interface PageHeaderProps {
6
+ title: ReactNode;
7
+ description?: ReactNode;
8
+ eyebrow?: ReactNode;
9
+ actions?: ReactNode;
10
+ className?: string;
11
+ }
12
+
13
+ export function PageHeader({ title, description, eyebrow, actions, className }: PageHeaderProps) {
14
+ return (
15
+ <header className={cn("sia-page-header", className)}>
16
+ <div className="sia-page-header__content">
17
+ {eyebrow && <div className="sia-page-header__eyebrow">{eyebrow}</div>}
18
+ <h1 className="sia-page-header__title">{title}</h1>
19
+ {description && <p className="sia-page-header__description">{description}</p>}
20
+ </div>
21
+ {actions && <div className="sia-page-header__actions">{actions}</div>}
22
+ </header>
23
+ );
24
+ }
@@ -0,0 +1,5 @@
1
+ .sia-page-header { display:flex; align-items:flex-start; justify-content:space-between; gap:1.5rem; margin-bottom:2rem; }
2
+ .sia-page-header__title { margin:0; font-size:clamp(1.5rem,3vw,2.25rem); }
3
+ .sia-page-header__description { margin:.5rem 0 0; color:var(--sia-muted); }
4
+ .sia-page-header__eyebrow { color:var(--sia-primary); font-size:.75rem; font-weight:700; text-transform:uppercase; letter-spacing:.08em; }
5
+ .sia-page-header__actions { display:flex; gap:.75rem; flex-wrap:wrap; }
@@ -0,0 +1,155 @@
1
+ import { useEffect, useId, useState, type FormEvent } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import { paginationItems, useSiaLocale } from "@sia-ui/headless";
4
+ import { ChevronLeftIcon, ChevronRightIcon } from "../Icons";
5
+ import "./styles.css";
6
+
7
+ export interface PaginationProps {
8
+ page: number;
9
+ totalPages: number;
10
+ onPageChange: (page: number) => void;
11
+ siblingCount?: number;
12
+ previousLabel?: string;
13
+ nextLabel?: string;
14
+ /**
15
+ * Ajoute un champ pour sauter directement à une page.
16
+ *
17
+ * Sur quarante pages, atteindre la vingt-septième demande sinon de cliquer
18
+ * dans les ellipses jusqu'à ce qu'elle apparaisse.
19
+ */
20
+ jumpTo?: boolean;
21
+ /** Affiche « Page 3 sur 40 » à côté des numéros. */
22
+ showTotal?: boolean;
23
+ /** Masque les libellés et ne garde que les flèches. */
24
+ compact?: boolean;
25
+ ariaLabel?: string;
26
+ className?: string;
27
+ }
28
+
29
+ /**
30
+ * La navigation entre les pages.
31
+ *
32
+ * Le calcul des numéros et des ellipses vient de `@sia-ui/headless` :
33
+ * `usePagination` et ce composant appliquent la même règle, écrite une fois.
34
+ */
35
+ export function Pagination({
36
+ page,
37
+ totalPages,
38
+ onPageChange,
39
+ siblingCount = 1,
40
+ previousLabel: previousLabelProp,
41
+ nextLabel: nextLabelProp,
42
+ jumpTo = false,
43
+ showTotal = false,
44
+ compact = false,
45
+ ariaLabel,
46
+ className,
47
+ }: PaginationProps) {
48
+ const locale = useSiaLocale();
49
+ const nextLabel = nextLabelProp ?? locale.next;
50
+ const previousLabel = previousLabelProp ?? locale.previous;
51
+
52
+ // Deux paginations sur la même page ne doivent pas partager l'identifiant
53
+ // de leur champ : le libellé pointerait alors vers l'autre.
54
+ const jumpId = useId();
55
+ const current = Math.min(Math.max(page, 1), Math.max(totalPages, 1));
56
+ const [draft, setDraft] = useState(String(current));
57
+
58
+ // Le champ suit la page quand elle change ailleurs — un clic sur un numéro,
59
+ // un retour arrière du navigateur.
60
+ useEffect(() => setDraft(String(current)), [current]);
61
+
62
+ if (totalPages <= 1) return null;
63
+
64
+ const items = paginationItems(current, totalPages, siblingCount);
65
+
66
+ const jump = (event: FormEvent) => {
67
+ event.preventDefault();
68
+ const asked = Number(draft);
69
+ if (!Number.isFinite(asked)) {
70
+ setDraft(String(current));
71
+ return;
72
+ }
73
+ // Une page hors bornes est ramenée dedans plutôt que refusée : taper 99
74
+ // sur 40 pages veut dire « la dernière ».
75
+ onPageChange(Math.min(Math.max(Math.round(asked), 1), totalPages));
76
+ };
77
+
78
+ return (
79
+ <nav
80
+ className={cn(
81
+ "sia-pagination",
82
+ compact && "sia-pagination--compact",
83
+ className,
84
+ )}
85
+ aria-label={ariaLabel ?? "Pagination"}
86
+ >
87
+ <button
88
+ type="button"
89
+ className="sia-pagination__step"
90
+ disabled={current === 1}
91
+ aria-label={previousLabel}
92
+ onClick={() => onPageChange(current - 1)}
93
+ >
94
+ <ChevronLeftIcon />
95
+ {!compact && <span>{previousLabel}</span>}
96
+ </button>
97
+
98
+ {items.map((item) =>
99
+ typeof item === "number" ? (
100
+ <button
101
+ type="button"
102
+ key={item}
103
+ className="sia-pagination__page"
104
+ aria-current={item === current ? "page" : undefined}
105
+ onClick={() => onPageChange(item)}
106
+ >
107
+ {item}
108
+ </button>
109
+ ) : (
110
+ <span key={item} className="sia-pagination__gap" aria-hidden="true">
111
+ …
112
+ </span>
113
+ ),
114
+ )}
115
+
116
+ <button
117
+ type="button"
118
+ className="sia-pagination__step"
119
+ disabled={current === totalPages}
120
+ aria-label={nextLabel}
121
+ onClick={() => onPageChange(current + 1)}
122
+ >
123
+ {!compact && <span>{nextLabel}</span>}
124
+ <ChevronRightIcon />
125
+ </button>
126
+
127
+ {showTotal && (
128
+ <span className="sia-pagination__total">
129
+ {locale.page} {current} / {totalPages}
130
+ </span>
131
+ )}
132
+
133
+ {jumpTo && (
134
+ <form className="sia-pagination__jump" onSubmit={jump}>
135
+ <label htmlFor={jumpId}>{locale.goToPage}</label>
136
+ <input
137
+ id={jumpId}
138
+ // `text` plutôt que `number` : le champ numérique natif accepte
139
+ // « 1e5 » et « -- », et vide sa valeur sans prévenir quand elle
140
+ // ne lui plaît pas.
141
+ type="text"
142
+ inputMode="numeric"
143
+ value={draft}
144
+ aria-label={locale.goToPage}
145
+ onChange={(event) => setDraft(event.target.value.replace(/\D/g, ""))}
146
+ onBlur={jump}
147
+ />
148
+ <span className="sia-pagination__jump-total" aria-hidden="true">
149
+ / {totalPages}
150
+ </span>
151
+ </form>
152
+ )}
153
+ </nav>
154
+ );
155
+ }