@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,149 @@
1
+ import { type ReactNode } from "react";
2
+ import { cn, toneClass } from "@sia-ui/utils";
3
+ import type { ComponentSize, ComponentTone } from "@sia-ui/tokens";
4
+ import { useComponentDefaults, useSiaLocale } from "@sia-ui/headless";
5
+ import "./styles.css";
6
+
7
+ export interface ProgressProps {
8
+ /** La valeur courante. Omise, la barre passe en indéterminé. */
9
+ value?: number | undefined;
10
+ max?: number;
11
+ shape?: "bar" | "circle";
12
+ tone?: ComponentTone;
13
+ size?: ComponentSize;
14
+ /** Épaisseur du trait, en pixels. Par défaut selon la taille. */
15
+ thickness?: number | undefined;
16
+ /** Affiche le pourcentage — au centre du cercle, en bout de barre. */
17
+ showValue?: boolean;
18
+ /** Remplace l'affichage par défaut. */
19
+ format?: ((percent: number, value: number, max: number) => ReactNode) | undefined;
20
+ label?: ReactNode;
21
+ className?: string;
22
+ }
23
+
24
+ const BAR_HEIGHT: Record<ComponentSize, number> = { sm: 4, md: 8, lg: 12 };
25
+ const CIRCLE_SIZE: Record<ComponentSize, number> = { sm: 36, md: 56, lg: 84 };
26
+ const CIRCLE_STROKE: Record<ComponentSize, number> = { sm: 4, md: 6, lg: 8 };
27
+
28
+ /**
29
+ * L'avancement d'une tâche.
30
+ *
31
+ * Sans `value`, le composant passe en indéterminé plutôt que d'afficher zéro :
32
+ * « je travaille » et « je n'ai rien fait » ne se ressemblent pas.
33
+ */
34
+ export function Progress(props: ProgressProps) {
35
+ const defaults = useComponentDefaults<ProgressProps>("progress");
36
+ const {
37
+ value,
38
+ max = 100,
39
+ shape = "bar",
40
+ tone = "primary",
41
+ size = "md",
42
+ thickness,
43
+ showValue = false,
44
+ format,
45
+ label,
46
+ className,
47
+ } = { ...defaults, ...props };
48
+
49
+ const locale = useSiaLocale();
50
+ const indeterminate = value === undefined || value === null;
51
+ const safeMax = max > 0 ? max : 100;
52
+ const clamped = indeterminate ? 0 : Math.min(Math.max(value, 0), safeMax);
53
+ const percent = Math.round((clamped / safeMax) * 100);
54
+ const text = format
55
+ ? format(percent, clamped, safeMax)
56
+ : `${percent}\u00A0%`;
57
+
58
+ const aria = {
59
+ role: "progressbar" as const,
60
+ "aria-valuemin": 0,
61
+ "aria-valuemax": safeMax,
62
+ ...(indeterminate ? {} : { "aria-valuenow": clamped }),
63
+ ...(indeterminate ? { "aria-label": locale.loading } : {}),
64
+ };
65
+
66
+ if (shape === "circle") {
67
+ const box = CIRCLE_SIZE[size];
68
+ const stroke = thickness ?? CIRCLE_STROKE[size];
69
+ const radius = (box - stroke) / 2;
70
+ const circumference = 2 * Math.PI * radius;
71
+
72
+ return (
73
+ <div
74
+ className={cn(
75
+ "sia-progress",
76
+ "sia-progress--circle",
77
+ toneClass("sia-progress", tone),
78
+ indeterminate && "sia-progress--indeterminate",
79
+ className,
80
+ )}
81
+ {...aria}
82
+ >
83
+ <svg width={box} height={box} viewBox={`0 0 ${box} ${box}`} aria-hidden="true">
84
+ <circle
85
+ className="sia-progress__track"
86
+ cx={box / 2}
87
+ cy={box / 2}
88
+ r={radius}
89
+ strokeWidth={stroke}
90
+ />
91
+ <circle
92
+ className="sia-progress__indicator"
93
+ cx={box / 2}
94
+ cy={box / 2}
95
+ r={radius}
96
+ strokeWidth={stroke}
97
+ strokeDasharray={circumference}
98
+ // Le trait se dessine par le décalage du pointillé : une seule
99
+ // propriété animée, sur le compositeur, sans reflow.
100
+ strokeDashoffset={
101
+ indeterminate
102
+ ? circumference * 0.75
103
+ : circumference - (clamped / safeMax) * circumference
104
+ }
105
+ />
106
+ </svg>
107
+ {(showValue || label) && (
108
+ <span className="sia-progress__center">
109
+ {showValue && !indeterminate && (
110
+ <strong className="sia-progress__value">{text}</strong>
111
+ )}
112
+ {label && <span className="sia-progress__label">{label}</span>}
113
+ </span>
114
+ )}
115
+ </div>
116
+ );
117
+ }
118
+
119
+ return (
120
+ <div
121
+ className={cn(
122
+ "sia-progress",
123
+ "sia-progress--bar",
124
+ toneClass("sia-progress", tone),
125
+ indeterminate && "sia-progress--indeterminate",
126
+ className,
127
+ )}
128
+ >
129
+ {(label || (showValue && !indeterminate)) && (
130
+ <div className="sia-progress__header">
131
+ {label && <span className="sia-progress__label">{label}</span>}
132
+ {showValue && !indeterminate && (
133
+ <span className="sia-progress__value">{text}</span>
134
+ )}
135
+ </div>
136
+ )}
137
+ <div
138
+ className="sia-progress__track"
139
+ style={{ height: thickness ?? BAR_HEIGHT[size] }}
140
+ {...aria}
141
+ >
142
+ <div
143
+ className="sia-progress__indicator"
144
+ style={indeterminate ? undefined : { width: `${percent}%` }}
145
+ />
146
+ </div>
147
+ </div>
148
+ );
149
+ }
@@ -0,0 +1,101 @@
1
+ .sia-progress { --sia-progress-color: var(--sia-primary); display: grid; gap: .375rem; }
2
+
3
+ .sia-progress__header { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; }
4
+ .sia-progress__label { color: var(--sia-muted); font-size: .8rem; }
5
+ .sia-progress__value { color: var(--sia-foreground); font-size: .8rem; font-variant-numeric: tabular-nums; font-weight: 600; }
6
+
7
+ /* ── Barre ─────────────────────────────────────────────────────────────── */
8
+
9
+ .sia-progress--bar .sia-progress__track {
10
+ position: relative;
11
+ overflow: hidden;
12
+ width: 100%;
13
+ border-radius: 999px;
14
+ background: color-mix(in srgb, var(--sia-progress-color) 14%, var(--sia-surface));
15
+ }
16
+
17
+ .sia-progress--bar .sia-progress__indicator {
18
+ height: 100%;
19
+ border-radius: inherit;
20
+ background: linear-gradient(
21
+ 90deg,
22
+ color-mix(in srgb, var(--sia-progress-color) 80%, white),
23
+ var(--sia-progress-color)
24
+ );
25
+ /* La largeur s'anime : un saut de 20 % à 80 % doit se voir monter, sinon on
26
+ ne sait pas si la valeur a changé ou si la page a été rechargée. */
27
+ transition: width var(--sia-duration-slow) var(--sia-easing-standard);
28
+ }
29
+
30
+ /* Le reflet : une bande claire qui traverse la barre, lentement. Elle ne dit
31
+ rien de plus que « ça avance », mais c'est précisément ce qu'on regarde. */
32
+ .sia-progress--bar .sia-progress__indicator::after {
33
+ content: "";
34
+ position: absolute;
35
+ inset: 0;
36
+ background: linear-gradient(
37
+ 90deg,
38
+ transparent,
39
+ rgb(255 255 255 / 0.35),
40
+ transparent
41
+ );
42
+ transform: translateX(-100%);
43
+ animation: sia-progress-sheen 2.2s var(--sia-easing-standard) infinite;
44
+ }
45
+
46
+ .sia-progress--bar.sia-progress--indeterminate .sia-progress__indicator {
47
+ width: 40%;
48
+ transition: none;
49
+ animation: sia-progress-slide 1.4s var(--sia-easing-standard) infinite;
50
+ }
51
+ .sia-progress--bar.sia-progress--indeterminate .sia-progress__indicator::after { animation: none; }
52
+
53
+ @keyframes sia-progress-sheen {
54
+ 0% { transform: translateX(-100%); }
55
+ 60%, 100% { transform: translateX(100%); }
56
+ }
57
+ @keyframes sia-progress-slide {
58
+ 0% { margin-left: -40%; }
59
+ 100% { margin-left: 100%; }
60
+ }
61
+
62
+ /* ── Cercle ────────────────────────────────────────────────────────────── */
63
+
64
+ .sia-progress--circle { position: relative; display: inline-grid; place-items: center; gap: 0; }
65
+ .sia-progress--circle svg { display: block; transform: rotate(-90deg); }
66
+
67
+ .sia-progress--circle .sia-progress__track {
68
+ fill: none;
69
+ stroke: color-mix(in srgb, var(--sia-progress-color) 14%, var(--sia-surface));
70
+ }
71
+ .sia-progress--circle .sia-progress__indicator {
72
+ fill: none;
73
+ stroke: var(--sia-progress-color);
74
+ stroke-linecap: round;
75
+ transition: stroke-dashoffset var(--sia-duration-slow) var(--sia-easing-standard);
76
+ }
77
+ .sia-progress--circle.sia-progress--indeterminate svg {
78
+ animation: sia-spin 1.1s linear infinite;
79
+ }
80
+ .sia-progress--circle.sia-progress--indeterminate .sia-progress__indicator {
81
+ transition: none;
82
+ }
83
+
84
+ .sia-progress__center {
85
+ position: absolute;
86
+ display: grid;
87
+ gap: 0;
88
+ justify-items: center;
89
+ line-height: 1.1;
90
+ text-align: center;
91
+ }
92
+ .sia-progress--circle .sia-progress__label { font-size: .7rem; }
93
+
94
+ /* ── Tons ──────────────────────────────────────────────────────────────── */
95
+
96
+ .sia-progress--neutral { --sia-progress-color: var(--sia-muted); }
97
+ .sia-progress--primary { --sia-progress-color: var(--sia-primary); }
98
+ .sia-progress--info { --sia-progress-color: var(--sia-info); }
99
+ .sia-progress--success { --sia-progress-color: var(--sia-success); }
100
+ .sia-progress--warning { --sia-progress-color: var(--sia-warning); }
101
+ .sia-progress--danger { --sia-progress-color: var(--sia-danger); }
@@ -0,0 +1,6 @@
1
+ import { type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import "./styles.css";
4
+
5
+ export interface QRCodeProps { value: string; size?: number; label?: string; render?: (value: string, size: number) => ReactNode; className?: string; }
6
+ export function QRCode({ value, size = 160, label = "Code QR", render, className }: QRCodeProps) { return <figure className={cn("sia-qr-code", className)} aria-label={label} style={{ width: size }}><div className="sia-qr-code__frame" style={{ width: size, height: size }}>{render ? render(value, size) : <div className="sia-qr-code__adapter"><strong>QR</strong><span>Encodeur requis</span></div>}</div><figcaption>{value}</figcaption></figure>; }
@@ -0,0 +1,5 @@
1
+ .sia-qr-code { display:grid; gap:.5rem; margin:0; }
2
+ .sia-qr-code__frame { display:grid; place-items:center; overflow:hidden; border:1px solid var(--sia-border); background:#fff; }
3
+ .sia-qr-code__adapter { display:grid; place-items:center; gap:.25rem; color:#0f172a; }
4
+ .sia-qr-code__adapter strong { font-size:2rem; }
5
+ .sia-qr-code__adapter span,.sia-qr-code figcaption { overflow:hidden; color:var(--sia-muted); font-size:.72rem; text-overflow:ellipsis; white-space:nowrap; }
@@ -0,0 +1,29 @@
1
+ import { createContext, useContext, useId, useState, type InputHTMLAttributes, type ReactNode } from "react";
2
+ import { cn, toneClass } from "@sia-ui/utils";
3
+ import type { ComponentTone } from "@sia-ui/tokens";
4
+ import "./styles.css";
5
+
6
+ type RadioValue = string | number;
7
+ type RadioContextValue = { name: string; value: RadioValue | undefined; setValue: (value: RadioValue) => void; disabled: boolean | undefined; size: "sm" | "md" | "lg"; tone: ComponentTone; };
8
+ const RadioGroupContext = createContext<RadioContextValue | null>(null);
9
+
10
+ export interface RadioGroupProps {
11
+ id?: string; name?: string; label?: ReactNode; value?: RadioValue; defaultValue?: RadioValue; onValueChange?: (value: RadioValue) => void; orientation?: "horizontal" | "vertical"; disabled?: boolean; size?: "sm" | "md" | "lg"; tone?: ComponentTone; className?: string; "aria-describedby"?: string; "aria-labelledby"?: string; "aria-invalid"?: boolean; "aria-required"?: boolean; children: ReactNode;
12
+ }
13
+ export function RadioGroup({ id, name, label, value, defaultValue, onValueChange, orientation = "vertical", disabled, size = "md", tone = "primary", className, children, ...ariaProps }: RadioGroupProps) {
14
+ const generatedName = useId(); const [internal, setInternal] = useState(defaultValue); const current = value ?? internal;
15
+ const setValue = (next: RadioValue) => { if (value === undefined) setInternal(next); onValueChange?.(next); };
16
+ return <fieldset id={id} className={cn("sia-radio-group", `sia-radio-group--${orientation}`, toneClass("sia-tone", tone), className)} disabled={disabled} {...ariaProps}>{label && <legend className="sia-radio-group__legend">{label}</legend>}<RadioGroupContext.Provider value={{ name: name ?? generatedName, value: current, setValue, disabled, size, tone }}>{children}</RadioGroupContext.Provider></fieldset>;
17
+ }
18
+
19
+ export interface RadioProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "name" | "value" | "size"> { value: RadioValue; label?: ReactNode; children?: ReactNode; }
20
+ export function Radio({ value, label, children, className, disabled, onChange, ...props }: RadioProps) {
21
+ const group = useContext(RadioGroupContext); if (!group) throw new Error("Radio must be used inside RadioGroup."); const checked = group.value === value; const resolvedDisabled = Boolean(disabled || group.disabled);
22
+ return <label className={cn("sia-radio", `sia-radio--${group.size}`, toneClass("sia-tone", group.tone), checked && "sia-radio--checked", resolvedDisabled && "sia-radio--disabled", className)}><input {...props} className="sia-visually-hidden" type="radio" name={group.name} value={value} checked={checked} disabled={resolvedDisabled} onChange={(event) => { group.setValue(value); onChange?.(event); }} /><span className="sia-radio__dot" aria-hidden="true" /><span className="sia-radio__label">{label ?? children}</span></label>;
23
+ }
24
+
25
+ export interface RadioButtonProps { value: RadioValue; disabled?: boolean; children: ReactNode; className?: string; }
26
+ export function RadioButton({ value, disabled, children, className }: RadioButtonProps) {
27
+ const group = useContext(RadioGroupContext); if (!group) throw new Error("RadioButton must be used inside RadioGroup."); const checked = group.value === value; const resolvedDisabled = Boolean(disabled || group.disabled);
28
+ return <button type="button" role="radio" aria-checked={checked} disabled={resolvedDisabled} className={cn("sia-radio-button", `sia-radio-button--${group.size}`, toneClass("sia-tone", group.tone), checked && "sia-radio-button--checked", className)} onClick={() => group.setValue(value)}>{children}</button>;
29
+ }
@@ -0,0 +1,20 @@
1
+ .sia-radio-group { display:flex; gap:.75rem; padding:0; margin:0; border:0; }
2
+ .sia-radio-group--vertical { flex-direction:column; }
3
+ .sia-radio-group--horizontal { flex-direction:row; flex-wrap:wrap; }
4
+ .sia-radio-group__legend { margin-bottom:.65rem; font-weight:600; }
5
+ .sia-radio { --sia-accent:var(--sia-primary); display:inline-flex; align-items:center; gap:.55rem; cursor:pointer; }
6
+ .sia-radio__dot { display:grid; place-items:center; width:1.1rem; height:1.1rem; border:1.5px solid var(--sia-border); border-radius:50%; }
7
+ .sia-radio__dot::after { content:""; width:.52rem; height:.52rem; border-radius:50%; background:transparent; }
8
+ .sia-radio--checked .sia-radio__dot { border-color:var(--sia-accent); }
9
+ .sia-radio--checked .sia-radio__dot::after { background:var(--sia-accent); }
10
+ .sia-radio:focus-within .sia-radio__dot { box-shadow:0 0 0 3px color-mix(in srgb,var(--sia-accent) 20%,transparent); }
11
+ .sia-radio--disabled { opacity:.5; cursor:not-allowed; }
12
+ .sia-radio--sm { font-size:.85rem; }
13
+ .sia-radio--lg { font-size:1.05rem; }
14
+ .sia-radio-button { --sia-accent:var(--sia-primary); min-height:2.35rem; padding:0 .8rem; border:1px solid var(--sia-border); color:var(--sia-foreground); background:var(--sia-background); font:inherit; cursor:pointer; }
15
+ .sia-radio-button:first-of-type { border-radius:var(--sia-radius) 0 0 var(--sia-radius); }
16
+ .sia-radio-button:last-of-type { border-radius:0 var(--sia-radius) var(--sia-radius) 0; }
17
+ .sia-radio-button + .sia-radio-button { margin-left:-1px; }
18
+ .sia-radio-button--checked { position:relative; z-index:1; color:var(--sia-accent); border-color:var(--sia-accent); background:color-mix(in srgb,var(--sia-accent) 9%,transparent); }
19
+ .sia-radio-button--sm { min-height:2rem; }
20
+ .sia-radio-button--lg { min-height:2.75rem; }
@@ -0,0 +1,82 @@
1
+ import { useState } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import { StarIcon } from "../Icons";
4
+ import "./styles.css";
5
+ export interface RatingProps {
6
+ id?: string;
7
+ value?: number;
8
+ defaultValue?: number;
9
+ onValueChange?: (value: number) => void;
10
+ max?: number;
11
+ /** Autorise les demi-valeurs — 3,5 sur 5. */
12
+ allowHalf?: boolean;
13
+ allowClear?: boolean;
14
+ disabled?: boolean;
15
+ label?: string;
16
+ className?: string;
17
+ "aria-describedby"?: string;
18
+ "aria-labelledby"?: string;
19
+ "aria-invalid"?: boolean;
20
+ "aria-required"?: boolean;
21
+ }
22
+ export function Rating({
23
+ id,
24
+ value,
25
+ defaultValue = 0,
26
+ onValueChange,
27
+ max = 5,
28
+ allowHalf = false,
29
+ allowClear = true,
30
+ disabled,
31
+ label = "Note",
32
+ className,
33
+ ...ariaProps
34
+ }: RatingProps) {
35
+ const [internal, setInternal] = useState(defaultValue);
36
+ const current = value ?? internal;
37
+ const update = (next: number) => {
38
+ const resolved = allowClear && next === current ? 0 : next;
39
+ if (value === undefined) setInternal(resolved);
40
+ onValueChange?.(resolved);
41
+ };
42
+ return (
43
+ <div
44
+ id={id}
45
+ role="radiogroup"
46
+ aria-label={label}
47
+ className={cn("sia-rating", className)}
48
+ {...ariaProps}
49
+ >
50
+ {Array.from({ length: max }, (_, index) => index + 1).map((item) => {
51
+ // Part remplie de CETTE étoile : pleine, vide, ou entre les deux.
52
+ // Impossible avec un caractère — d'où le passage au tracé.
53
+ const filled = Math.min(1, Math.max(0, current - item + 1));
54
+
55
+ return (
56
+ <button
57
+ key={item}
58
+ type="button"
59
+ role="radio"
60
+ aria-checked={current === item}
61
+ aria-label={`${item} sur ${max}`}
62
+ disabled={disabled}
63
+ className={filled > 0 ? "is-active" : ""}
64
+ style={{ "--sia-rating-fill": `${filled * 100}%` } as React.CSSProperties}
65
+ onClick={(event) => {
66
+ if (!allowHalf) return update(item);
67
+ // Moitié gauche du bouton = demi-étoile.
68
+ const box = event.currentTarget.getBoundingClientRect();
69
+ const half = event.clientX - box.left < box.width / 2;
70
+ update(half ? item - 0.5 : item);
71
+ }}
72
+ >
73
+ <span className="sia-rating__star" aria-hidden="true">
74
+ <StarIcon className="sia-rating__outline" />
75
+ <StarIcon className="sia-rating__fill" fill="currentColor" />
76
+ </span>
77
+ </button>
78
+ );
79
+ })}
80
+ </div>
81
+ );
82
+ }
@@ -0,0 +1,41 @@
1
+ .sia-rating { display:inline-flex; gap:.2rem; }
2
+ .sia-rating button { padding:.1rem; border:0; color:var(--sia-border); background:transparent; font-size:1.5rem; cursor:pointer; }
3
+ .sia-rating button.is-active { color:var(--sia-warning); }
4
+
5
+ /* La demi-étoile : deux tracés superposés, celui du dessus rogné à la part
6
+ remplie. Un caractère ne permettait pas ça — d'où son remplacement. */
7
+ .sia-rating__star {
8
+ position: relative;
9
+ display: inline-grid;
10
+ place-items: center;
11
+ }
12
+
13
+ .sia-rating__outline,
14
+ .sia-rating__fill {
15
+ grid-area: 1 / 1;
16
+ width: 1em;
17
+ height: 1em;
18
+ }
19
+
20
+ .sia-rating__outline {
21
+ color: var(--sia-border-strong);
22
+ }
23
+
24
+ .sia-rating__fill {
25
+ color: var(--sia-warning);
26
+ clip-path: inset(0 calc(100% - var(--sia-rating-fill, 0%)) 0 0);
27
+ transition: clip-path var(--sia-duration-base) var(--sia-easing-standard);
28
+ }
29
+
30
+ .sia-rating button {
31
+ transition: transform var(--sia-duration-fast) var(--sia-easing-spring);
32
+ }
33
+
34
+ .sia-rating button:hover:not(:disabled) {
35
+ transform: scale(1.15);
36
+ }
37
+
38
+ .sia-rating button:active:not(:disabled) {
39
+ transform: scale(0.95);
40
+ transition-duration: var(--sia-duration-instant);
41
+ }
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from "react";
2
+ import { Autocomplete, type AutocompleteProps } from "../Autocomplete";
3
+ import type { SelectOption } from "../Select";
4
+ import "./styles.css";
5
+ export interface ReferenceOption extends SelectOption { description?: ReactNode; }
6
+ export interface ReferenceSelectProps extends Omit<AutocompleteProps, "options"> { options: ReferenceOption[]; }
7
+ export function ReferenceSelect({ options, ...props }: ReferenceSelectProps) { return <Autocomplete {...props} options={options.map((option) => ({ ...option, label: <span className="sia-reference-select__option"><strong>{option.label}</strong>{option.description && <small>{option.description}</small>}</span> }))} />; }
@@ -0,0 +1,2 @@
1
+ .sia-reference-select__option { display:grid; }
2
+ .sia-reference-select__option small { color:var(--sia-muted); }
@@ -0,0 +1,22 @@
1
+ import { useEffect, useState, type HTMLAttributes } from "react";
2
+ import { cn, formatDate, formatTimeAgo } from "@sia-ui/utils";
3
+ import "./styles.css";
4
+
5
+ export interface RelativeTimeProps extends Omit<HTMLAttributes<HTMLTimeElement>, "dateTime"> {
6
+ value: Date | string | number;
7
+ locale?: string;
8
+ prefix?: string;
9
+ refreshInterval?: number;
10
+ }
11
+
12
+ export function RelativeTime({ value, locale = "fr-FR", prefix, refreshInterval = 60_000, className, title, ...props }: RelativeTimeProps) {
13
+ const [, refresh] = useState(0);
14
+ useEffect(() => {
15
+ if (refreshInterval <= 0) return;
16
+ const id = window.setInterval(() => refresh((current) => current + 1), refreshInterval);
17
+ return () => window.clearInterval(id);
18
+ }, [refreshInterval]);
19
+ const date = new Date(value);
20
+ if (Number.isNaN(date.getTime())) return null;
21
+ return <time className={cn("sia-relative-time", className)} dateTime={date.toISOString()} title={title ?? formatDate(date, { locale, dateStyle:"full", timeStyle:"short" })} {...props}>{formatTimeAgo(date, { locale, ...(prefix ? { prefix } : {}) })}</time>;
22
+ }
@@ -0,0 +1 @@
1
+ .sia-relative-time { color:var(--sia-muted); font-size:.875rem; }
@@ -0,0 +1,127 @@
1
+ import { useEffect, type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import {
4
+ useSession,
5
+ type PermissionRule,
6
+ type SessionState,
7
+ type SessionStore,
8
+ } from "@sia-ui/headless";
9
+ import { Spinner } from "../Spinner";
10
+ import "./styles.css";
11
+
12
+ export interface RequireSessionProps<TUser, TTenant, TRule> {
13
+ session: SessionStore<TUser, TTenant, TRule>;
14
+ children: ReactNode;
15
+ /** La règle de droits exigée, s'il y en a une. */
16
+ require?: TRule;
17
+ /**
18
+ * Affiché pendant la vérification.
19
+ *
20
+ * Ce rendu est ce qui distingue cette garde d'un `if (!user) redirect` :
21
+ * tant que le serveur n'a pas répondu, on attend. Rediriger pendant la
22
+ * vérification renvoie à la page de connexion quelqu'un qui est connecté.
23
+ */
24
+ pending?: ReactNode;
25
+ /** Rendu quand il n'y a pas de session. */
26
+ fallback?: ReactNode;
27
+ /** Rendu quand la session existe mais que les droits manquent. */
28
+ forbidden?: ReactNode;
29
+ /** Laisse passer une session `incomplete` — profil pas encore rempli. */
30
+ allowIncomplete?: boolean;
31
+ /**
32
+ * Appelé une fois quand la vérification conclut à l'absence de session.
33
+ *
34
+ * C'est ici que l'application navigue : `navigate("/login")`,
35
+ * `router.push(…)`, ou rien du tout si `fallback` suffit. La garde ne
36
+ * connaît aucun routeur.
37
+ */
38
+ onUnauthenticated?: (state: SessionState<TUser, TTenant>) => void;
39
+ onForbidden?: (state: SessionState<TUser, TTenant>) => void;
40
+ onIncomplete?: (state: SessionState<TUser, TTenant>) => void;
41
+ className?: string;
42
+ }
43
+
44
+ /**
45
+ * Ce qui se trouve derrière une session valide.
46
+ *
47
+ * Écrite cinq fois dans les projets du disque D, cinq fois de la même façon
48
+ * à la cible de redirection près. Elle ne dépend d'aucun routeur : elle rend
49
+ * un état et prévient, l'application décide où aller.
50
+ *
51
+ * > Ceci protège l'interface, pas les données. Une route masquée reste une
52
+ * > route ouverte tant que le serveur ne refuse pas l'appel.
53
+ */
54
+ export function RequireSession<TUser, TTenant, TRule = PermissionRule>({
55
+ session,
56
+ children,
57
+ require: rule,
58
+ pending,
59
+ fallback = null,
60
+ forbidden = null,
61
+ allowIncomplete = false,
62
+ onUnauthenticated,
63
+ onForbidden,
64
+ onIncomplete,
65
+ className,
66
+ }: RequireSessionProps<TUser, TTenant, TRule>) {
67
+ const state = useSession(session);
68
+
69
+ const denied =
70
+ state.status === "authenticated" &&
71
+ rule !== undefined &&
72
+ !session.access.can(rule);
73
+ const incomplete = state.status === "incomplete" && !allowIncomplete;
74
+
75
+ // Les rappels partent d'un effet, jamais du rendu : naviguer pendant un
76
+ // rendu démonte l'arbre que React est en train de construire.
77
+ useEffect(() => {
78
+ if (state.status === "anonymous") onUnauthenticated?.(state);
79
+ else if (incomplete) onIncomplete?.(state);
80
+ else if (denied) onForbidden?.(state);
81
+ // Seul le statut compte : réagir à chaque changement de l'objet d'état
82
+ // relancerait la navigation à chaque rendu.
83
+ // eslint-disable-next-line react-hooks/exhaustive-deps
84
+ }, [state.status, denied, incomplete]);
85
+
86
+ if (state.status === "checking") {
87
+ return (
88
+ <div className={cn("sia-require-session", className)} aria-busy="true">
89
+ {pending ?? (
90
+ <div className="sia-require-session__pending">
91
+ <Spinner />
92
+ </div>
93
+ )}
94
+ </div>
95
+ );
96
+ }
97
+
98
+ if (state.status === "anonymous") return <>{fallback}</>;
99
+ if (incomplete) return <>{fallback}</>;
100
+ if (denied) return <>{forbidden}</>;
101
+
102
+ return <>{children}</>;
103
+ }
104
+
105
+ export interface CanProps<TUser, TTenant, TRule> {
106
+ session: SessionStore<TUser, TTenant, TRule>;
107
+ require: TRule;
108
+ children: ReactNode;
109
+ fallback?: ReactNode;
110
+ }
111
+
112
+ /**
113
+ * Un morceau d'interface derrière un droit.
114
+ *
115
+ * `PermissionGate` fait la même chose à partir d'un tableau de permissions
116
+ * passé à la main; celui-ci lit la session et suit son évaluateur — jokers,
117
+ * expression textuelle ou égalité stricte selon le serveur.
118
+ */
119
+ export function Can<TUser, TTenant, TRule = PermissionRule>({
120
+ session,
121
+ require: rule,
122
+ children,
123
+ fallback = null,
124
+ }: CanProps<TUser, TTenant, TRule>) {
125
+ useSession(session);
126
+ return <>{session.access.can(rule) ? children : fallback}</>;
127
+ }
@@ -0,0 +1,13 @@
1
+ .sia-require-session { display: contents; }
2
+
3
+ /* Pendant la vérification, la place est tenue plutôt que laissée vide : une
4
+ page qui se remplit d'un coup après un écran blanc se lit comme un
5
+ rechargement, pas comme un chargement. */
6
+ .sia-require-session__pending {
7
+ display: grid;
8
+ place-items: center;
9
+ min-height: 12rem;
10
+ padding: 2rem;
11
+ color: var(--sia-muted);
12
+ animation: sia-fade-in var(--sia-duration-base) var(--sia-easing-standard);
13
+ }
@@ -0,0 +1,6 @@
1
+ import { useState, type PointerEvent, type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import "./styles.css";
4
+
5
+ export interface ResizablePanelProps { first: ReactNode; second: ReactNode; defaultSize?: number; minSize?: number; maxSize?: number; direction?: "horizontal" | "vertical"; className?: string; }
6
+ export function ResizablePanel({ first, second, defaultSize = 50, minSize = 20, maxSize = 80, direction = "horizontal", className }: ResizablePanelProps) { const [size, setSize] = useState(defaultSize); const start = (event: PointerEvent<HTMLDivElement>) => { const container = event.currentTarget.parentElement; if (!container) return; event.currentTarget.setPointerCapture(event.pointerId); const move = (moveEvent: globalThis.PointerEvent) => { const rect = container.getBoundingClientRect(); const next = direction === "horizontal" ? ((moveEvent.clientX - rect.left) / rect.width) * 100 : ((moveEvent.clientY - rect.top) / rect.height) * 100; setSize(Math.min(maxSize, Math.max(minSize, next))); }; const stop = () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", stop); }; window.addEventListener("pointermove", move); window.addEventListener("pointerup", stop); }; const firstStyle = { flexBasis: `${size}%` }; return <div className={cn("sia-resizable-panel", `sia-resizable-panel--${direction}`, className)}><section style={firstStyle}>{first}</section><div className="sia-resizable-panel__handle" role="separator" aria-orientation={direction} aria-valuenow={Math.round(size)} tabIndex={0} onPointerDown={start} onKeyDown={(event) => { const delta = event.key === "ArrowLeft" || event.key === "ArrowUp" ? -5 : event.key === "ArrowRight" || event.key === "ArrowDown" ? 5 : 0; if (delta) { event.preventDefault(); setSize((current) => Math.min(maxSize, Math.max(minSize, current + delta))); } }} /><section>{second}</section></div>; }
@@ -0,0 +1,5 @@
1
+ .sia-resizable-panel { display:flex; min-height:18rem; overflow:hidden; border:1px solid var(--sia-border); }
2
+ .sia-resizable-panel--vertical { flex-direction:column; }
3
+ .sia-resizable-panel > section { min-width:0; min-height:0; flex:1; overflow:auto; padding:1rem; }
4
+ .sia-resizable-panel__handle { flex:none; width:.4rem; background:var(--sia-border); cursor:col-resize; }
5
+ .sia-resizable-panel--vertical .sia-resizable-panel__handle { width:auto; height:.4rem; cursor:row-resize; }
@@ -0,0 +1,5 @@
1
+ import { useEffect, useRef, type HTMLAttributes } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import "./styles.css";
4
+ export interface RichTextEditorProps extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue"> { value?: string; defaultValue?: string; onValueChange?: (html: string) => void; placeholder?: string; disabled?: boolean; }
5
+ export function RichTextEditor({ value, defaultValue = "", onValueChange, placeholder = "Saisir du contenu...", disabled, className, ...props }: RichTextEditorProps) { const editorRef = useRef<HTMLDivElement>(null); useEffect(() => { if (value !== undefined && editorRef.current && editorRef.current.innerHTML !== value) editorRef.current.innerHTML = value; }, [value]); const command = (name: string) => { editorRef.current?.focus(); document.execCommand(name); onValueChange?.(editorRef.current?.innerHTML ?? ""); }; return <div className={cn("sia-rich-text-editor", disabled && "sia-rich-text-editor--disabled", className)}><div className="sia-editor-toolbar"><button type="button" disabled={disabled} aria-label="Gras" onClick={() => command("bold")}><strong>B</strong></button><button type="button" disabled={disabled} aria-label="Italique" onClick={() => command("italic")}><em>I</em></button><button type="button" disabled={disabled} aria-label="Liste" onClick={() => command("insertUnorderedList")}>• Liste</button></div><div {...props} ref={editorRef} role="textbox" aria-multiline="true" contentEditable={!disabled} suppressContentEditableWarning data-placeholder={placeholder} dangerouslySetInnerHTML={value === undefined ? { __html: defaultValue } : undefined} onInput={(event) => onValueChange?.(event.currentTarget.innerHTML)} /></div>; }
@@ -0,0 +1,2 @@
1
+ .sia-rich-text-editor [contenteditable]:empty::before { content:attr(data-placeholder); color:var(--sia-muted); }
2
+ .sia-rich-text-editor--disabled { opacity:.55; }