@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,193 @@
1
+ import {
2
+ createContext,
3
+ forwardRef,
4
+ useContext,
5
+ useEffect,
6
+ useRef,
7
+ useState,
8
+ type InputHTMLAttributes,
9
+ type ReactNode,
10
+ } from "react";
11
+ import { cn, toneClass } from "@sia-ui/utils";
12
+ import type { ComponentTone } from "@sia-ui/tokens";
13
+ import "./styles.css";
14
+
15
+ type CheckboxValue = string | number;
16
+ type CheckboxGroupContextValue = {
17
+ value: CheckboxValue[];
18
+ toggle: (value: CheckboxValue) => void;
19
+ disabled: boolean | undefined;
20
+ variant: "default" | "button";
21
+ size: "sm" | "md" | "lg";
22
+ tone: ComponentTone;
23
+ };
24
+ const CheckboxGroupContext = createContext<CheckboxGroupContextValue | null>(
25
+ null,
26
+ );
27
+
28
+ export interface CheckboxGroupProps {
29
+ value?: CheckboxValue[];
30
+ defaultValue?: CheckboxValue[];
31
+ onValueChange?: (value: CheckboxValue[]) => void;
32
+ disabled?: boolean;
33
+ variant?: "default" | "button";
34
+ orientation?: "horizontal" | "vertical";
35
+ size?: "sm" | "md" | "lg";
36
+ tone?: ComponentTone;
37
+ className?: string;
38
+ children: ReactNode;
39
+ }
40
+
41
+ export function CheckboxGroup({
42
+ value,
43
+ defaultValue = [],
44
+ onValueChange,
45
+ disabled,
46
+ variant = "default",
47
+ orientation = "vertical",
48
+ size = "md",
49
+ tone = "primary",
50
+ className,
51
+ children,
52
+ }: CheckboxGroupProps) {
53
+ const [internal, setInternal] = useState(defaultValue);
54
+ const current = value ?? internal;
55
+ const toggle = (item: CheckboxValue) => {
56
+ const next = current.includes(item)
57
+ ? current.filter((value) => value !== item)
58
+ : [...current, item];
59
+ if (value === undefined) setInternal(next);
60
+ onValueChange?.(next);
61
+ };
62
+ return (
63
+ <CheckboxGroupContext.Provider
64
+ value={{ value: current, toggle, disabled, variant, size, tone }}
65
+ >
66
+ <div
67
+ role="group"
68
+ className={cn(
69
+ "sia-checkbox-group",
70
+ `sia-checkbox-group--${orientation}`,
71
+ `sia-checkbox-group--${variant}`,
72
+ toneClass("sia-tone", tone),
73
+ className,
74
+ )}
75
+ >
76
+ {children}
77
+ </div>
78
+ </CheckboxGroupContext.Provider>
79
+ );
80
+ }
81
+
82
+ export interface CheckboxProps extends Omit<
83
+ InputHTMLAttributes<HTMLInputElement>,
84
+ "type" | "value" | "size"
85
+ > {
86
+ value?: CheckboxValue;
87
+ label?: ReactNode;
88
+ children?: ReactNode;
89
+ description?: ReactNode;
90
+ indeterminate?: boolean;
91
+ onValueChange?: (checked: boolean) => void;
92
+ size?: "sm" | "md" | "lg";
93
+ tone?: ComponentTone;
94
+ }
95
+
96
+ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
97
+ function Checkbox(
98
+ {
99
+ className,
100
+ value,
101
+ label,
102
+ children,
103
+ description,
104
+ indeterminate = false,
105
+ onValueChange,
106
+ size = "md",
107
+ tone = "primary",
108
+ disabled,
109
+ checked,
110
+ defaultChecked,
111
+ onChange,
112
+ ...props
113
+ },
114
+ forwardedRef,
115
+ ) {
116
+ const group = useContext(CheckboxGroupContext);
117
+ const localRef = useRef<HTMLInputElement>(null);
118
+ const grouped = Boolean(group && value !== undefined);
119
+
120
+ // Sans cet état, une case hors groupe et sans `checked` restait peinte
121
+ // « décochée » pour toujours : l'`<input>` réel basculait bien, mais il est
122
+ // masqué, et la case visible n'est dessinée que par la classe plus bas —
123
+ // qui, elle, ne bougeait pas.
124
+ const [internalChecked, setInternalChecked] = useState(
125
+ Boolean(defaultChecked),
126
+ );
127
+ const isControlled = checked !== undefined;
128
+ const resolvedChecked = grouped
129
+ ? group!.value.includes(value!)
130
+ : isControlled
131
+ ? checked
132
+ : internalChecked;
133
+ const resolvedDisabled = Boolean(disabled || group?.disabled);
134
+ const resolvedSize = group?.size ?? size;
135
+ const resolvedTone = group?.tone ?? tone;
136
+ const variant = group?.variant ?? "default";
137
+ useEffect(() => {
138
+ if (localRef.current) localRef.current.indeterminate = indeterminate;
139
+ }, [indeterminate]);
140
+ const setRef = (node: HTMLInputElement | null) => {
141
+ localRef.current = node;
142
+ if (typeof forwardedRef === "function") forwardedRef(node);
143
+ else if (forwardedRef) forwardedRef.current = node;
144
+ };
145
+ return (
146
+ <label
147
+ className={cn(
148
+ "sia-checkbox",
149
+ `sia-checkbox--${variant}`,
150
+ `sia-checkbox--${resolvedSize}`,
151
+ toneClass("sia-tone", resolvedTone),
152
+ resolvedChecked && "sia-checkbox--checked",
153
+ indeterminate && "sia-checkbox--indeterminate",
154
+ resolvedDisabled && "sia-checkbox--disabled",
155
+ className,
156
+ )}
157
+ >
158
+ <input
159
+ {...props}
160
+ ref={setRef}
161
+ // Le contrôle natif reste focusable et annoncé, mais sorti du rendu :
162
+ // en simple `opacity:0` il restait peint sous la case, et son propre
163
+ // dessin clignotait au clic.
164
+ className="sia-visually-hidden"
165
+ type="checkbox"
166
+ value={value}
167
+ checked={resolvedChecked}
168
+ disabled={resolvedDisabled}
169
+ onChange={(event) => {
170
+ if (grouped) group!.toggle(value!);
171
+ else if (!isControlled) setInternalChecked(event.target.checked);
172
+ onValueChange?.(event.target.checked);
173
+ onChange?.(event);
174
+ }}
175
+ />
176
+ <span className="sia-checkbox__box" aria-hidden="true">
177
+ <span />
178
+ </span>
179
+ {(label ?? children ?? description) && (
180
+ <span className="sia-checkbox__content">
181
+ {(label ?? children) && (
182
+ <span className="sia-checkbox__label">{label ?? children}</span>
183
+ )}
184
+ {description && (
185
+ <span className="sia-checkbox__description">{description}</span>
186
+ )}
187
+ </span>
188
+ )}
189
+ </label>
190
+ );
191
+ },
192
+ );
193
+ Checkbox.displayName = "Checkbox";
@@ -0,0 +1,39 @@
1
+ /* Legacy closure: shared checkbox, radio, input, tooltip and popover variants. */
2
+ .sia-checkbox-group { display:flex; gap:.65rem; }
3
+ .sia-checkbox-group--vertical { flex-direction:column; align-items:flex-start; }
4
+ .sia-checkbox-group--horizontal { flex-direction:row; flex-wrap:wrap; }
5
+ .sia-checkbox { --sia-accent:var(--sia-primary); display:inline-flex; align-items:flex-start; gap:.6rem; color:var(--sia-foreground); cursor:pointer; }
6
+ .sia-checkbox__box { display:grid; place-items:center; flex:none; width:1.1rem; height:1.1rem; margin-top:.12rem; border:1.5px solid var(--sia-border); border-radius:.3rem; background:var(--sia-background); transition:.15s; }
7
+ .sia-checkbox__box span { width:.48rem; height:.28rem; border-bottom:2px solid transparent; border-left:2px solid transparent; transform:rotate(-45deg) translateY(-.05rem); scale:0; }
8
+ .sia-checkbox--checked .sia-checkbox__box,.sia-checkbox--indeterminate .sia-checkbox__box { border-color:var(--sia-accent); background:var(--sia-accent); }
9
+ .sia-checkbox--checked .sia-checkbox__box span,.sia-checkbox--indeterminate .sia-checkbox__box span { scale:1; }
10
+ .sia-checkbox--checked .sia-checkbox__box span { border-color:var(--sia-accent-foreground); }
11
+ .sia-checkbox--indeterminate .sia-checkbox__box span { width:.55rem; height:2px; border:0; background:var(--sia-accent-foreground); transform:none; }
12
+ .sia-checkbox:focus-within .sia-checkbox__box { box-shadow:0 0 0 3px color-mix(in srgb,var(--sia-accent) 20%,transparent); }
13
+ .sia-checkbox__content { display:grid; gap:.12rem; }
14
+ .sia-checkbox__label { font-weight:600; }
15
+ .sia-checkbox__description { color:var(--sia-muted); font-size:.8rem; }
16
+ .sia-checkbox--sm { font-size:.85rem; }
17
+ .sia-checkbox--lg { font-size:1.05rem; }
18
+ .sia-checkbox--lg .sia-checkbox__box { width:1.25rem; height:1.25rem; }
19
+ .sia-checkbox--disabled { opacity:.5; cursor:not-allowed; }
20
+ .sia-checkbox--button { align-items:center; padding:.5rem .75rem; border:1px solid var(--sia-border); border-radius:var(--sia-radius); }
21
+ .sia-checkbox--button.sia-checkbox--checked { color:var(--sia-accent); border-color:var(--sia-accent); background:color-mix(in srgb,var(--sia-accent) 8%,transparent); }
22
+ .sia-checkbox--button .sia-checkbox__box { display:none; }
23
+
24
+ /* La coche se trace au lieu d'apparaître. */
25
+ .sia-checkbox__box {
26
+ transition:
27
+ background-color var(--sia-duration-fast) var(--sia-easing-standard),
28
+ border-color var(--sia-duration-fast) var(--sia-easing-standard),
29
+ box-shadow var(--sia-duration-fast) var(--sia-easing-standard);
30
+ }
31
+
32
+ .sia-checkbox__box span {
33
+ transition: scale var(--sia-duration-base) var(--sia-easing-spring);
34
+ }
35
+
36
+ .sia-checkbox:active:not(.sia-checkbox--disabled) .sia-checkbox__box {
37
+ transform: scale(0.92);
38
+ transition-duration: var(--sia-duration-instant);
39
+ }
@@ -0,0 +1,6 @@
1
+ import { forwardRef, useState, type InputHTMLAttributes } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import "./styles.css";
4
+ export interface ColorPickerProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "onChange"> { onValueChange?: (value: string) => void; showValue?: boolean; }
5
+ export const ColorPicker = forwardRef<HTMLInputElement, ColorPickerProps>(({ className, onValueChange, showValue = true, value, defaultValue = "#2563eb", ...props }, ref) => { const [internal, setInternal] = useState(String(defaultValue)); const current = value === undefined ? internal : String(value); return <span className={cn("sia-color-picker", className)}><input {...props} ref={ref} type="color" value={current} onChange={(event) => { if (value === undefined) setInternal(event.target.value); onValueChange?.(event.target.value); }} />{showValue && <code>{current}</code>}</span>; });
6
+ ColorPicker.displayName = "ColorPicker";
@@ -0,0 +1,2 @@
1
+ .sia-color-picker { display:flex; align-items:center; gap:.75rem; }
2
+ .sia-color-picker input { width:3rem; height:2.5rem; padding:.15rem; border:1px solid var(--sia-border); border-radius:var(--sia-radius); background:var(--sia-background); }
@@ -0,0 +1,7 @@
1
+ import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import "./styles.css";
4
+
5
+ export interface CommandItem { id: string; label: string; description?: string; keywords?: string[]; icon?: ReactNode; disabled?: boolean; onSelect: () => void; }
6
+ export interface CommandPaletteProps { open: boolean; onOpenChange: (open: boolean) => void; items: CommandItem[]; placeholder?: string; emptyLabel?: string; className?: string; }
7
+ export function CommandPalette({ open, onOpenChange, items, placeholder = "Rechercher une commande...", emptyLabel = "Aucune commande", className }: CommandPaletteProps) { const [query, setQuery] = useState(""); const [active, setActive] = useState(0); const inputRef = useRef<HTMLInputElement>(null); const filtered = useMemo(() => { const needle = query.trim().toLowerCase(); return items.filter((item) => !needle || [item.label, item.description, ...(item.keywords ?? [])].filter(Boolean).join(" ").toLowerCase().includes(needle)); }, [items, query]); useEffect(() => { if (!open) return; setQuery(""); setActive(0); requestAnimationFrame(() => inputRef.current?.focus()); }, [open]); if (!open) return null; const select = (item: CommandItem | undefined) => { if (!item || item.disabled) return; item.onSelect(); onOpenChange(false); }; return <div className="sia-command-palette__backdrop" onMouseDown={(event) => event.target === event.currentTarget && onOpenChange(false)}><section className={cn("sia-command-palette", className)} role="dialog" aria-modal="true" aria-label="Palette de commandes"><input ref={inputRef} value={query} placeholder={placeholder} aria-label={placeholder} onChange={(event) => { setQuery(event.target.value); setActive(0); }} onKeyDown={(event) => { if (event.key === "Escape") onOpenChange(false); if (event.key === "ArrowDown") { event.preventDefault(); setActive((value) => Math.min(value + 1, filtered.length - 1)); } if (event.key === "ArrowUp") { event.preventDefault(); setActive((value) => Math.max(value - 1, 0)); } if (event.key === "Enter") select(filtered[active]); }} /><div role="listbox">{filtered.length === 0 ? <p>{emptyLabel}</p> : filtered.map((item, index) => <button type="button" role="option" aria-selected={index === active} key={item.id} disabled={item.disabled} onMouseEnter={() => setActive(index)} onClick={() => select(item)}>{item.icon}<span><strong>{item.label}</strong>{item.description && <small>{item.description}</small>}</span></button>)}</div></section></div>; }
@@ -0,0 +1,8 @@
1
+ .sia-command-palette__backdrop { position:fixed; inset:0; z-index:1600; display:grid; align-items:start; justify-items:center; padding-top:12vh; background:rgb(2 6 23 / .55); }
2
+ .sia-command-palette { width:min(92vw,38rem); overflow:hidden; border:1px solid var(--sia-border); border-radius:calc(var(--sia-radius) * 1.5); background:var(--sia-background); box-shadow:0 24px 70px rgb(2 6 23 / .3); }
3
+ .sia-command-palette > input { width:100%; min-height:3.2rem; padding:0 1rem; border:0; border-bottom:1px solid var(--sia-border); background:transparent; font:inherit; }
4
+ .sia-command-palette [role="listbox"] { max-height:22rem; overflow:auto; padding:.5rem; }
5
+ .sia-command-palette [role="option"] { display:flex; gap:.75rem; width:100%; padding:.7rem; border:0; border-radius:.4rem; background:transparent; text-align:left; }
6
+ .sia-command-palette [role="option"][aria-selected="true"] { background:var(--sia-surface); }
7
+ .sia-command-palette [role="option"] span { display:grid; }
8
+ .sia-command-palette small { color:var(--sia-muted); }
@@ -0,0 +1,24 @@
1
+ import { type ReactNode } from "react";
2
+ import { Button } from "../Button";
3
+ import type { ComponentTone } from "@sia-ui/tokens";
4
+ import { Modal } from "../Modal";
5
+ import { useSiaLocale } from "@sia-ui/headless";
6
+
7
+ export interface ConfirmDialogProps {
8
+ open: boolean;
9
+ onOpenChange: (open: boolean) => void;
10
+ title: ReactNode;
11
+ description?: ReactNode;
12
+ confirmLabel?: string;
13
+ cancelLabel?: string;
14
+ tone?: Extract<ComponentTone, "primary" | "danger">;
15
+ loading?: boolean;
16
+ onConfirm: () => void | Promise<void>;
17
+ }
18
+
19
+ export function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel: confirmLabelProp, cancelLabel: cancelLabelProp, tone = "primary", loading = false, onConfirm }: ConfirmDialogProps) {
20
+ const locale = useSiaLocale();
21
+ const cancelLabel = cancelLabelProp ?? locale.cancel;
22
+ const confirmLabel = confirmLabelProp ?? locale.confirm;
23
+ return <Modal open={open} onOpenChange={onOpenChange} title={title} description={description} closeOnBackdrop={!loading} footer={<><Button variant="ghost" disabled={loading} onClick={() => onOpenChange(false)}>{cancelLabel}</Button><Button className={tone === "danger" ? "sia-button--danger" : undefined} loading={loading} onClick={() => void onConfirm()}>{confirmLabel}</Button></>}>{description ? null : <p>Cette action nécessite votre confirmation.</p>}</Modal>;
24
+ }
@@ -0,0 +1,11 @@
1
+ import { type HTMLAttributes } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import "./styles.css";
4
+
5
+ export function Container({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
6
+ return <div className={cn("sia-container", className)} {...props} />;
7
+ }
8
+
9
+ export function Stack({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
10
+ return <div className={cn("sia-stack", className)} {...props} />;
11
+ }
@@ -0,0 +1 @@
1
+ .sia-container { width:min(100% - 2rem,80rem); margin-inline:auto; }
@@ -0,0 +1,3 @@
1
+ import { Timer, type TimerProps } from "../Timer";
2
+ export type CountdownProps = Omit<TimerProps, "direction">;
3
+ export function Countdown(props: CountdownProps) { return <Timer {...props} direction="down" />; }
@@ -0,0 +1,9 @@
1
+ import { type ReactNode } from "react";
2
+ import { PageHeader } from "../PageHeader";
3
+ import { AsyncState } from "../AsyncState";
4
+ import { Pagination } from "../Pagination";
5
+ import { cn } from "@sia-ui/utils";
6
+ import "./styles.css";
7
+
8
+ export interface CrudPageProps { title: ReactNode; description?: ReactNode; actions?: ReactNode; toolbar?: ReactNode; children: ReactNode; loading?: boolean; error?: ReactNode; empty?: boolean; emptyTitle?: ReactNode; page?: number; totalPages?: number; onPageChange?: (page: number) => void; className?: string; }
9
+ export function CrudPage({ title, description, actions, toolbar, children, loading = false, error, empty = false, emptyTitle, page, totalPages, onPageChange, className }: CrudPageProps) { return <section className={cn("sia-crud-page", className)}><PageHeader title={title} description={description} actions={actions} />{toolbar}<AsyncState loading={loading} error={error} empty={empty} emptyTitle={emptyTitle}>{children}</AsyncState>{page !== undefined && totalPages !== undefined && onPageChange && <Pagination page={page} totalPages={totalPages} onPageChange={onPageChange} />}</section>; }
@@ -0,0 +1 @@
1
+ .sia-crud-page { display:grid; gap:1.25rem; }
@@ -0,0 +1,15 @@
1
+ import { forwardRef, useState, type InputHTMLAttributes } from "react";
2
+ import { cn, formatCurrency, parseCurrency } from "@sia-ui/utils";
3
+ import "./styles.css";
4
+
5
+ export interface CurrencyInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "defaultValue" | "onChange" | "inputMode"> { value?: number | null; defaultValue?: number | null; currency?: string; locale?: string; onValueChange?: (value: number | null) => void; invalid?: boolean; }
6
+
7
+ export const CurrencyInput = forwardRef<HTMLInputElement, CurrencyInputProps>(({ value, defaultValue = null, currency = "XAF", locale = "fr-FR", onValueChange, invalid, className, onBlur, onFocus, ...props }, ref) => {
8
+ const [focused, setFocused] = useState(false);
9
+ const [internal, setInternal] = useState<number | null>(defaultValue);
10
+ const current = value === undefined ? internal : value;
11
+ const [draft, setDraft] = useState(current === null ? "" : String(current));
12
+ const display = focused ? draft : current === null ? "" : formatCurrency(current, { locale, currency });
13
+ return <input {...props} ref={ref} className={cn("sia-input", "sia-currency-input", invalid && "sia-input--invalid", className)} inputMode="decimal" value={display} aria-invalid={invalid || undefined} onFocus={(event) => { setDraft(current === null ? "" : String(current)); setFocused(true); onFocus?.(event); }} onChange={(event) => { setDraft(event.target.value); const next = parseCurrency(event.target.value, { locale }); if (value === undefined) setInternal(next); onValueChange?.(next); }} onBlur={(event) => { setFocused(false); onBlur?.(event); }} />;
14
+ });
15
+ CurrencyInput.displayName = "CurrencyInput";
@@ -0,0 +1 @@
1
+ .sia-currency-input { text-align:right; font-variant-numeric:tabular-nums; }
@@ -0,0 +1,240 @@
1
+ import { useMemo, type Key, type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import {
4
+ useSiaLocale,
5
+ useTableQuery,
6
+ type TableQuery,
7
+ type TableQueryState,
8
+ type TableQueryStorage,
9
+ type TableSort,
10
+ } from "@sia-ui/headless";
11
+ import { ChevronDownIcon, ChevronUpIcon } from "../Icons";
12
+ import "./styles.css";
13
+
14
+ export interface DataTableColumn<T> {
15
+ key: string;
16
+ header: ReactNode;
17
+ cell: (row: T) => ReactNode;
18
+ align?: "left" | "center" | "right";
19
+ width?: string;
20
+ /** Rend l'entête cliquable. Le tri lui-même appartient au serveur. */
21
+ sortable?: boolean;
22
+ /** Le champ envoyé au serveur, si différent de `key`. */
23
+ sortKey?: string;
24
+ }
25
+
26
+ export interface DataTableProps<T> {
27
+ columns: Array<DataTableColumn<T>>;
28
+ data: T[];
29
+ getRowKey: (row: T) => Key;
30
+ caption?: string;
31
+ empty?: ReactNode;
32
+ loading?: boolean;
33
+ /** Le tri en vigueur — celui de `useTableQuery`, ou le vôtre. */
34
+ sort?: TableSort | null;
35
+ onSortChange?: (field: string) => void;
36
+ /** L'entête reste visible quand le tableau défile. */
37
+ stickyHeader?: boolean;
38
+ onRowClick?: (row: T) => void;
39
+ rowClassName?: (row: T) => string | undefined;
40
+ className?: string;
41
+ }
42
+
43
+ /**
44
+ * Un tableau de données.
45
+ *
46
+ * Il n'ordonne ni ne filtre rien lui-même : à partir de quelques centaines de
47
+ * lignes, trier côté client veut dire les avoir toutes téléchargées. Le tri
48
+ * est donc une intention envoyée au serveur — `onSortChange` — et le tableau
49
+ * ne fait qu'afficher ce qu'on lui rend.
50
+ */
51
+ export function DataTable<T>({
52
+ columns,
53
+ data,
54
+ getRowKey,
55
+ caption,
56
+ empty: emptyProp,
57
+ loading = false,
58
+ sort,
59
+ onSortChange,
60
+ stickyHeader = false,
61
+ onRowClick,
62
+ rowClassName,
63
+ className,
64
+ }: DataTableProps<T>) {
65
+ const locale = useSiaLocale();
66
+ const empty = emptyProp ?? locale.empty;
67
+
68
+ return (
69
+ <div
70
+ className={cn(
71
+ "sia-data-table",
72
+ stickyHeader && "sia-data-table--sticky",
73
+ onRowClick && "sia-data-table--clickable",
74
+ className,
75
+ )}
76
+ aria-busy={loading}
77
+ >
78
+ <table>
79
+ {caption && <caption>{caption}</caption>}
80
+ <thead>
81
+ <tr>
82
+ {columns.map((column) => {
83
+ const field = column.sortKey ?? column.key;
84
+ const active = sort?.field === field;
85
+ const sortable = Boolean(column.sortable && onSortChange);
86
+
87
+ return (
88
+ <th
89
+ key={column.key}
90
+ scope="col"
91
+ style={{ textAlign: column.align, width: column.width }}
92
+ {...(active
93
+ ? {
94
+ "aria-sort":
95
+ sort?.direction === "asc"
96
+ ? ("ascending" as const)
97
+ : ("descending" as const),
98
+ }
99
+ : {})}
100
+ >
101
+ {sortable ? (
102
+ <button
103
+ type="button"
104
+ className="sia-data-table__sort"
105
+ data-active={active || undefined}
106
+ onClick={() => onSortChange?.(field)}
107
+ >
108
+ <span>{column.header}</span>
109
+ <span className="sia-data-table__arrow" aria-hidden="true">
110
+ {active && sort?.direction === "desc" ? (
111
+ <ChevronDownIcon />
112
+ ) : (
113
+ <ChevronUpIcon />
114
+ )}
115
+ </span>
116
+ </button>
117
+ ) : (
118
+ column.header
119
+ )}
120
+ </th>
121
+ );
122
+ })}
123
+ </tr>
124
+ </thead>
125
+ <tbody>
126
+ {loading && data.length === 0 ? (
127
+ // Des lignes fantômes plutôt qu'un mot : la hauteur du tableau ne
128
+ // change pas quand les données arrivent, et rien ne saute.
129
+ Array.from({ length: 5 }, (_, index) => (
130
+ <tr key={`skeleton-${index}`} className="sia-data-table__ghost">
131
+ {columns.map((column) => (
132
+ <td key={column.key}>
133
+ <span className="sia-data-table__ghost-bar" />
134
+ </td>
135
+ ))}
136
+ </tr>
137
+ ))
138
+ ) : data.length === 0 ? (
139
+ <tr>
140
+ <td colSpan={columns.length} className="sia-data-table__state">
141
+ {empty}
142
+ </td>
143
+ </tr>
144
+ ) : (
145
+ data.map((row) => (
146
+ <tr
147
+ key={getRowKey(row)}
148
+ className={rowClassName?.(row)}
149
+ {...(onRowClick
150
+ ? {
151
+ onClick: () => onRowClick(row),
152
+ tabIndex: 0,
153
+ onKeyDown: (event) => {
154
+ if (event.key === "Enter") onRowClick(row);
155
+ },
156
+ }
157
+ : {})}
158
+ >
159
+ {columns.map((column) => (
160
+ <td key={column.key} style={{ textAlign: column.align }}>
161
+ {column.cell(row)}
162
+ </td>
163
+ ))}
164
+ </tr>
165
+ ))
166
+ )}
167
+ </tbody>
168
+ </table>
169
+
170
+ {/* Un rechargement en place — page suivante, tri changé — garde les
171
+ lignes visibles et les voile. Les faire disparaître donnerait
172
+ l'impression d'avoir perdu les données. */}
173
+ {loading && data.length > 0 && (
174
+ <div className="sia-data-table__veil" aria-hidden="true" />
175
+ )}
176
+ </div>
177
+ );
178
+ }
179
+
180
+ /**
181
+ * L'état du tableau dans la barre d'adresse.
182
+ *
183
+ * `history.replaceState` plutôt que `pushState` : trier une colonne trois
184
+ * fois de suite ne doit pas demander trois retours en arrière pour revenir
185
+ * d'où l'on vient.
186
+ */
187
+ export function createUrlTableStorage(
188
+ mode: "replace" | "push" = "replace",
189
+ ): TableQueryStorage {
190
+ return {
191
+ read: () =>
192
+ typeof window === "undefined" ? "" : window.location.search,
193
+ write: (value) => {
194
+ if (typeof window === "undefined") return;
195
+ const url = `${window.location.pathname}${value ? `?${value}` : ""}${window.location.hash}`;
196
+ if (mode === "push") window.history.pushState(null, "", url);
197
+ else window.history.replaceState(null, "", url);
198
+ },
199
+ subscribe: (listener) => {
200
+ if (typeof window === "undefined") return () => {};
201
+ window.addEventListener("popstate", listener);
202
+ return () => window.removeEventListener("popstate", listener);
203
+ },
204
+ };
205
+ }
206
+
207
+ export interface UseDataTableQueryOptions {
208
+ defaultState?: Partial<TableQueryState>;
209
+ /** `false` garde l'état en mémoire — utile dans une modale. */
210
+ syncUrl?: boolean;
211
+ /** Empile une entrée d'historique à chaque changement. */
212
+ history?: "replace" | "push";
213
+ /** Préfixe des clés, pour deux tableaux sur la même page. */
214
+ prefix?: string;
215
+ }
216
+
217
+ /**
218
+ * Tri, filtres et pagination d'un tableau, synchronisés avec l'URL.
219
+ *
220
+ * Le calcul vit dans `@sia-ui/headless`; il n'y a ici que le branchement sur
221
+ * la barre d'adresse, qui est la seule partie propre au navigateur.
222
+ */
223
+ export function useDataTableQuery(
224
+ options: UseDataTableQueryOptions = {},
225
+ ): TableQuery {
226
+ const { defaultState, syncUrl = true, history = "replace", prefix } = options;
227
+
228
+ // Créé une fois : un objet reconstruit à chaque rendu réabonnerait
229
+ // l'écouteur `popstate` en boucle.
230
+ const storage = useMemo(
231
+ () => (syncUrl ? createUrlTableStorage(history) : undefined),
232
+ [history, syncUrl],
233
+ );
234
+
235
+ return useTableQuery({
236
+ ...(defaultState ? { defaultState } : {}),
237
+ ...(storage ? { storage } : {}),
238
+ ...(prefix ? { prefix } : {}),
239
+ });
240
+ }