@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,242 @@
1
+ import { readFile, rm } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { lignesDiff } from "./diff.mjs";
4
+ import { LOCK_FILE, ecrireLock, empreinte, lireLock, oublier } from "./lock.mjs";
5
+
6
+ /**
7
+ * Ce qu'on peut faire d'un composant **après** l'avoir copié.
8
+ *
9
+ * Sans ces quatre commandes, le registre est une voie sans retour : on
10
+ * installe, et plus rien ne permet de savoir ce qu'on a, ce qu'on a retouché,
11
+ * ni ce qui a bougé en amont. Corriger ou déprécier une entrée n'atteindrait
12
+ * jamais personne.
13
+ *
14
+ * Toutes travaillent sur `sia-ui.lock.json` : c'est lui qui sait ce qui a été
15
+ * installé, dans quelle version, et sous quelle forme exacte.
16
+ */
17
+
18
+ /** Un état, calculé une fois et lu par les quatre commandes. */
19
+ async function etatDe(contexte, nom, entreeLock) {
20
+ const { cwd, registry } = contexte;
21
+ const item = registry.items.find((candidate) => candidate.name === nom);
22
+
23
+ const fichiers = [];
24
+ for (const [relatif, empreinteInstallee] of Object.entries(
25
+ entreeLock.files ?? {},
26
+ )) {
27
+ let actuel;
28
+ try {
29
+ actuel = await readFile(join(cwd, relatif));
30
+ } catch {
31
+ fichiers.push({ relatif, etat: "absent" });
32
+ continue;
33
+ }
34
+
35
+ const modifie = empreinte(actuel) !== empreinteInstallee;
36
+ fichiers.push({ relatif, etat: modifie ? "modifié" : "intact", actuel });
37
+ }
38
+
39
+ return {
40
+ nom,
41
+ item,
42
+ verrou: entreeLock,
43
+ fichiers,
44
+ inconnu: !item,
45
+ retire: item?.status === "removed",
46
+ deprecie: item?.status === "deprecated",
47
+ // Une version différente en amont, dans un sens comme dans l'autre : une
48
+ // version locale plus récente que le registre signale un verrou bricolé.
49
+ desynchronise: Boolean(item) && item.version !== entreeLock.version,
50
+ };
51
+ }
52
+
53
+ async function etats(contexte, noms) {
54
+ const lock = await lireLock(contexte.cwd);
55
+ const choisis =
56
+ noms.length > 0 ? noms : Object.keys(lock.items).sort();
57
+
58
+ const inconnusDuVerrou = choisis.filter((nom) => !lock.items[nom]);
59
+ if (inconnusDuVerrou.length > 0) {
60
+ throw new Error(
61
+ `Non installé${inconnusDuVerrou.length > 1 ? "s" : ""} d'après ${LOCK_FILE} : ${inconnusDuVerrou.join(", ")}`,
62
+ );
63
+ }
64
+
65
+ const resultat = [];
66
+ for (const nom of choisis) {
67
+ resultat.push(await etatDe(contexte, nom, lock.items[nom]));
68
+ }
69
+ return { lock, etats: resultat };
70
+ }
71
+
72
+ /**
73
+ * Le contenu que le registre sert aujourd'hui pour ce fichier installé.
74
+ *
75
+ * La lecture passe par la source choisie — copie embarquée ou registre HTTP —
76
+ * et non par un chemin de disque : `diff` doit pouvoir comparer avec ce que
77
+ * sert un registre distant.
78
+ */
79
+ async function sourceAmont(contexte, item, relatif) {
80
+ const { config, cibleDe, lireSource } = contexte;
81
+ for (const fichier of item.files) {
82
+ if (cibleDe(fichier, config) === relatif) {
83
+ return (await lireSource(fichier)).toString("utf8");
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
89
+ export async function diff(contexte, noms) {
90
+ const { etats: liste } = await etats(contexte, noms);
91
+ let differences = 0;
92
+
93
+ for (const etat of liste) {
94
+ if (etat.inconnu) {
95
+ console.log(`\n${etat.nom} — absent du registre, rien à comparer.`);
96
+ continue;
97
+ }
98
+
99
+ for (const fichier of etat.fichiers) {
100
+ if (fichier.etat === "absent") {
101
+ console.log(`\n${fichier.relatif}\n supprimé du projet`);
102
+ differences += 1;
103
+ continue;
104
+ }
105
+
106
+ const amont = await sourceAmont(contexte, etat.item, fichier.relatif);
107
+ if (amont === null) continue;
108
+
109
+ const local = fichier.actuel.toString("utf8");
110
+ if (local === amont) continue;
111
+
112
+ differences += 1;
113
+ console.log(`\n--- ${fichier.relatif} (registre ${etat.item.version})`);
114
+ console.log(`+++ ${fichier.relatif} (projet)`);
115
+ for (const { signe, ligne } of lignesDiff(amont, local)) {
116
+ console.log(`${signe} ${ligne}`);
117
+ }
118
+ }
119
+ }
120
+
121
+ if (differences === 0) {
122
+ console.log("Aucune différence avec le registre.");
123
+ }
124
+ }
125
+
126
+ export async function remove(contexte, noms, flags) {
127
+ if (noms.length === 0) {
128
+ throw new Error("Indiquer au moins un nom de composant à retirer.");
129
+ }
130
+
131
+ const { lock, etats: liste } = await etats(contexte, noms);
132
+ const force = flags.has("--force");
133
+ const aRetirer = new Set(noms);
134
+
135
+ // Retirer un composant dont un autre dépend casse silencieusement ce
136
+ // dernier : ses imports pointeraient sur un dossier vide.
137
+ const dependants = [];
138
+ for (const [nom, entree] of Object.entries(lock.items)) {
139
+ if (aRetirer.has(nom)) continue;
140
+ const item = contexte.registry.items.find((c) => c.name === nom);
141
+ for (const besoin of item?.registryDependencies ?? []) {
142
+ if (aRetirer.has(besoin)) dependants.push(`${nom} → ${besoin}`);
143
+ }
144
+ void entree;
145
+ }
146
+
147
+ if (dependants.length > 0 && !force) {
148
+ throw new Error(
149
+ `Encore utilisé par :\n ${dependants.join("\n ")}\n\n--force pour retirer quand même.`,
150
+ );
151
+ }
152
+
153
+ for (const etat of liste) {
154
+ for (const fichier of etat.fichiers) {
155
+ if (fichier.etat === "modifié" && !force) {
156
+ console.log(
157
+ `keep ${fichier.relatif} (modifié depuis l'installation — --force pour supprimer)`,
158
+ );
159
+ continue;
160
+ }
161
+ if (fichier.etat !== "absent") {
162
+ await rm(join(contexte.cwd, fichier.relatif), { force: true });
163
+ console.log(`rm ${fichier.relatif}`);
164
+ }
165
+ }
166
+ oublier(lock, etat.nom);
167
+ }
168
+
169
+ await ecrireLock(contexte.cwd, lock);
170
+ console.log(`\nlock ${LOCK_FILE}`);
171
+ }
172
+
173
+ export async function doctor(contexte) {
174
+ const { etats: liste } = await etats(contexte, []);
175
+
176
+ if (liste.length === 0) {
177
+ console.log(`Rien d'installé d'après ${LOCK_FILE}.`);
178
+ return;
179
+ }
180
+
181
+ const problemes = [];
182
+
183
+ for (const etat of liste) {
184
+ if (etat.inconnu) {
185
+ problemes.push(`${etat.nom} — absent du registre de cette CLI`);
186
+ } else if (etat.retire) {
187
+ problemes.push(
188
+ `${etat.nom} — retiré du registre${etat.item.replacedBy ? `, remplacé par ${etat.item.replacedBy}` : ""}`,
189
+ );
190
+ } else if (etat.deprecie) {
191
+ problemes.push(
192
+ `${etat.nom} — déprécié${etat.item.replacedBy ? `, préférer ${etat.item.replacedBy}` : ""}`,
193
+ );
194
+ }
195
+
196
+ if (etat.desynchronise) {
197
+ problemes.push(
198
+ `${etat.nom} — installé en ${etat.verrou.version}, le registre est en ${etat.item.version}`,
199
+ );
200
+ }
201
+
202
+ for (const fichier of etat.fichiers) {
203
+ if (fichier.etat === "absent") {
204
+ problemes.push(`${etat.nom} — ${fichier.relatif} a disparu`);
205
+ } else if (fichier.etat === "modifié") {
206
+ // Ce n'est pas une erreur : posséder le code, c'est le droit de le
207
+ // modifier. Mais `add --overwrite` écraserait la modification.
208
+ problemes.push(
209
+ `${etat.nom} — ${fichier.relatif} modifié depuis l'installation`,
210
+ );
211
+ }
212
+ }
213
+ }
214
+
215
+ console.log(`${liste.length} entrée(s) installée(s).`);
216
+
217
+ if (problemes.length === 0) {
218
+ console.log("Rien à signaler.");
219
+ return;
220
+ }
221
+
222
+ console.log("");
223
+ for (const probleme of problemes) console.log(` ${probleme}`);
224
+ }
225
+
226
+ export async function outdated(contexte) {
227
+ const { etats: liste } = await etats(contexte, []);
228
+ const enRetard = liste.filter((etat) => etat.desynchronise && !etat.inconnu);
229
+
230
+ if (enRetard.length === 0) {
231
+ console.log("Tout est à jour.");
232
+ return;
233
+ }
234
+
235
+ const largeur = Math.max(...enRetard.map((etat) => etat.nom.length));
236
+ for (const etat of enRetard) {
237
+ console.log(
238
+ `${etat.nom.padEnd(largeur)} ${etat.verrou.version} → ${etat.item.version}`,
239
+ );
240
+ }
241
+ console.log(`\nMettre à jour :\n sia-ui add ${enRetard.map((e) => e.nom).join(" ")} --overwrite`);
242
+ }
package/dist/lock.mjs ADDED
@@ -0,0 +1,92 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+
5
+ /**
6
+ * Ce que le projet a installé, et dans quel état.
7
+ *
8
+ * Un composant copié cesse d'être suivi : il vit dans le dépôt de
9
+ * l'utilisateur, qui le modifie comme il veut. Sans trace écrite au moment de
10
+ * la copie, plus rien ne permet de répondre à « qu'est-ce que j'ai installé »,
11
+ * « l'ai-je retouché » ni « une version plus récente existe-t-elle ». On ne
12
+ * peut donc ni corriger ni déprécier quoi que ce soit après coup.
13
+ *
14
+ * L'empreinte est celle du fichier **tel qu'il a été copié**. Comparée au
15
+ * fichier sur le disque, elle dit si l'utilisateur l'a modifié; comparée à la
16
+ * source du registre, elle dit si l'entrée a bougé depuis.
17
+ */
18
+
19
+ export const LOCK_FILE = "sia-ui.lock.json";
20
+
21
+ /** La forme du fichier. Un entier, parce qu'il évoluera. */
22
+ const FORMAT = 1;
23
+
24
+ const VIDE = { format: FORMAT, items: {} };
25
+
26
+ export function empreinte(contenu) {
27
+ return "sha256:" + createHash("sha256").update(contenu).digest("hex");
28
+ }
29
+
30
+ export async function empreinteFichier(chemin) {
31
+ return empreinte(await readFile(chemin));
32
+ }
33
+
34
+ export async function lireLock(cwd) {
35
+ try {
36
+ const brut = await readFile(join(cwd, LOCK_FILE), "utf8");
37
+ const lock = JSON.parse(brut);
38
+
39
+ // Un format inconnu vient d'une CLI plus récente : l'écraser ferait perdre
40
+ // ce qu'elle y avait noté.
41
+ if (lock.format !== FORMAT) {
42
+ throw new Error(
43
+ `${LOCK_FILE} est au format ${lock.format}, et cette CLI lit le format ${FORMAT}. Mettre la CLI à jour.`,
44
+ );
45
+ }
46
+
47
+ return { format: FORMAT, items: lock.items ?? {} };
48
+ } catch (error) {
49
+ if (error.code === "ENOENT") return structuredClone(VIDE);
50
+ if (error instanceof SyntaxError) {
51
+ throw new Error(`${LOCK_FILE} est illisible : ${error.message}`, {
52
+ cause: error,
53
+ });
54
+ }
55
+ throw error;
56
+ }
57
+ }
58
+
59
+ export async function ecrireLock(cwd, lock) {
60
+ // Les noms triés : le fichier est versionné chez l'utilisateur, et deux
61
+ // installations dans un ordre différent ne doivent pas produire un diff.
62
+ const items = Object.fromEntries(
63
+ Object.keys(lock.items)
64
+ .sort()
65
+ .map((nom) => [nom, lock.items[nom]]),
66
+ );
67
+
68
+ await writeFile(
69
+ join(cwd, LOCK_FILE),
70
+ `${JSON.stringify({ format: FORMAT, items }, null, 2)}\n`,
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Note une entrée installée.
76
+ *
77
+ * `fichiers` associe le chemin relatif au projet à l'empreinte de ce qui y a
78
+ * été écrit.
79
+ */
80
+ export function noter(lock, item, fichiers) {
81
+ lock.items[item.name] = {
82
+ version: item.version,
83
+ installedAt: new Date().toISOString(),
84
+ files: Object.fromEntries(Object.keys(fichiers).sort().map((f) => [f, fichiers[f]])),
85
+ };
86
+ return lock;
87
+ }
88
+
89
+ export function oublier(lock, nom) {
90
+ delete lock.items[nom];
91
+ return lock;
92
+ }
@@ -0,0 +1,172 @@
1
+ import { useId, type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import { useAccordionState } from "@sia-ui/headless";
4
+ import { ChevronDownIcon } from "../Icons";
5
+ import "./styles.css";
6
+
7
+ export interface AccordionItem {
8
+ key: string;
9
+ title: ReactNode;
10
+ content: ReactNode;
11
+ /** Une ligne sous le titre, visible même replié. */
12
+ description?: ReactNode;
13
+ icon?: ReactNode;
14
+ /** À droite du titre : un compteur, un badge, un statut. */
15
+ meta?: ReactNode;
16
+ disabled?: boolean;
17
+ }
18
+
19
+ export interface AccordionProps {
20
+ items: AccordionItem[];
21
+ /** `single` referme la section précédente, `multiple` les cumule. */
22
+ type?: "single" | "multiple";
23
+ value?: string[];
24
+ defaultValue?: string[];
25
+ onValueChange?: (value: string[]) => void;
26
+ /** En `single`, autorise à tout refermer. */
27
+ collapsible?: boolean;
28
+ variant?: "bordered" | "separated" | "plain";
29
+ className?: string;
30
+ }
31
+
32
+ /**
33
+ * Des sections dépliables.
34
+ *
35
+ * La hauteur s'anime sans être mesurée : le panneau est une grille dont la
36
+ * ligne passe de `0fr` à `1fr`. Pas de `scrollHeight`, pas de recalcul au
37
+ * redimensionnement, et un contenu qui change de taille suit tout seul.
38
+ */
39
+ export function Accordion({
40
+ items,
41
+ type = "single",
42
+ value,
43
+ defaultValue,
44
+ onValueChange,
45
+ collapsible = true,
46
+ variant = "bordered",
47
+ className,
48
+ }: AccordionProps) {
49
+ const baseId = useId();
50
+ const state = useAccordionState({
51
+ type,
52
+ value,
53
+ defaultValue,
54
+ onValueChange,
55
+ collapsible,
56
+ });
57
+
58
+ return (
59
+ <div className={cn("sia-accordion", `sia-accordion--${variant}`, className)}>
60
+ {items.map((item) => {
61
+ const open = state.isOpen(item.key);
62
+ const triggerId = `${baseId}-${item.key}-trigger`;
63
+ const panelId = `${baseId}-${item.key}-panel`;
64
+
65
+ return (
66
+ <div
67
+ key={item.key}
68
+ className="sia-accordion__item"
69
+ data-open={open || undefined}
70
+ >
71
+ <button
72
+ type="button"
73
+ id={triggerId}
74
+ className="sia-accordion__trigger"
75
+ aria-expanded={open}
76
+ aria-controls={panelId}
77
+ disabled={item.disabled}
78
+ onClick={() => state.toggle(item.key)}
79
+ >
80
+ {item.icon && (
81
+ <span className="sia-accordion__icon" aria-hidden="true">
82
+ {item.icon}
83
+ </span>
84
+ )}
85
+ <span className="sia-accordion__heading">
86
+ <span className="sia-accordion__title">{item.title}</span>
87
+ {item.description && (
88
+ <span className="sia-accordion__description">
89
+ {item.description}
90
+ </span>
91
+ )}
92
+ </span>
93
+ {item.meta && <span className="sia-accordion__meta">{item.meta}</span>}
94
+ <span className="sia-accordion__chevron" aria-hidden="true">
95
+ <ChevronDownIcon />
96
+ </span>
97
+ </button>
98
+
99
+ <div
100
+ id={panelId}
101
+ role="region"
102
+ aria-labelledby={triggerId}
103
+ className="sia-accordion__panel"
104
+ >
105
+ {/* Le contenu reste monté : le démonter à chaque repli ferait
106
+ perdre la position de défilement, le texte déjà saisi et
107
+ l'état des sous-composants. `inert` le retire du parcours au
108
+ clavier et de l'arbre d'accessibilité tant qu'il est replié —
109
+ c'est la seule chose qui doit disparaître. */}
110
+ <div className="sia-accordion__content" inert={open ? undefined : true}>
111
+ <div className="sia-accordion__inner">{item.content}</div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ );
116
+ })}
117
+ </div>
118
+ );
119
+ }
120
+
121
+ export interface CollapseProps {
122
+ title: ReactNode;
123
+ children: ReactNode;
124
+ description?: ReactNode;
125
+ meta?: ReactNode;
126
+ icon?: ReactNode;
127
+ open?: boolean;
128
+ defaultOpen?: boolean;
129
+ onOpenChange?: (open: boolean) => void;
130
+ variant?: AccordionProps["variant"];
131
+ disabled?: boolean;
132
+ className?: string;
133
+ }
134
+
135
+ /** Une section seule. Le même rendu, pour le cas le plus courant. */
136
+ export function Collapse({
137
+ title,
138
+ children,
139
+ description,
140
+ meta,
141
+ icon,
142
+ open,
143
+ defaultOpen = false,
144
+ onOpenChange,
145
+ variant = "bordered",
146
+ disabled = false,
147
+ className,
148
+ }: CollapseProps) {
149
+ return (
150
+ <Accordion
151
+ {...(className !== undefined ? { className } : {})}
152
+ variant={variant}
153
+ type="multiple"
154
+ {...(open !== undefined ? { value: open ? ["only"] : [] } : {})}
155
+ {...(open === undefined ? { defaultValue: defaultOpen ? ["only"] : [] } : {})}
156
+ {...(onOpenChange
157
+ ? { onValueChange: (next: string[]) => onOpenChange(next.length > 0) }
158
+ : {})}
159
+ items={[
160
+ {
161
+ key: "only",
162
+ title,
163
+ content: children,
164
+ disabled,
165
+ ...(description !== undefined ? { description } : {}),
166
+ ...(meta !== undefined ? { meta } : {}),
167
+ ...(icon !== undefined ? { icon } : {}),
168
+ },
169
+ ]}
170
+ />
171
+ );
172
+ }
@@ -0,0 +1,97 @@
1
+ .sia-accordion { display: grid; }
2
+
3
+ .sia-accordion--bordered {
4
+ border: 1px solid var(--sia-border);
5
+ border-radius: calc(var(--sia-radius) + 2px);
6
+ background: var(--sia-surface-raised);
7
+ overflow: hidden;
8
+ }
9
+ .sia-accordion--bordered .sia-accordion__item + .sia-accordion__item {
10
+ border-top: 1px solid var(--sia-border);
11
+ }
12
+
13
+ .sia-accordion--separated { gap: .5rem; }
14
+ .sia-accordion--separated .sia-accordion__item {
15
+ border: 1px solid var(--sia-border);
16
+ border-radius: calc(var(--sia-radius) + 2px);
17
+ background: var(--sia-surface-raised);
18
+ overflow: hidden;
19
+ transition: box-shadow var(--sia-duration-base) var(--sia-easing-standard);
20
+ }
21
+ .sia-accordion--separated .sia-accordion__item[data-open] {
22
+ box-shadow: var(--sia-elevation-rest);
23
+ }
24
+
25
+ .sia-accordion--plain .sia-accordion__item + .sia-accordion__item {
26
+ border-top: 1px solid var(--sia-border);
27
+ }
28
+
29
+ .sia-accordion__trigger {
30
+ display: flex;
31
+ align-items: center;
32
+ gap: .75rem;
33
+ width: 100%;
34
+ padding: .875rem 1rem;
35
+ border: 0;
36
+ background: transparent;
37
+ color: var(--sia-foreground);
38
+ font-size: .925rem;
39
+ text-align: left;
40
+ cursor: pointer;
41
+ transition: background-color var(--sia-duration-fast) var(--sia-easing-standard);
42
+ }
43
+ .sia-accordion__trigger:hover:not(:disabled) { background: var(--sia-surface); }
44
+ .sia-accordion__trigger:disabled { opacity: .5; cursor: not-allowed; }
45
+
46
+ .sia-accordion__icon { display: grid; place-items: center; flex: none; color: var(--sia-muted); }
47
+ .sia-accordion__heading { flex: 1; display: grid; gap: .1rem; min-width: 0; }
48
+ .sia-accordion__title { font-weight: 600; }
49
+ .sia-accordion__description { color: var(--sia-muted); font-size: .8rem; line-height: 1.4; }
50
+ .sia-accordion__meta { flex: none; color: var(--sia-muted); font-size: .8rem; }
51
+
52
+ .sia-accordion__chevron {
53
+ display: grid;
54
+ place-items: center;
55
+ flex: none;
56
+ color: var(--sia-muted);
57
+ transition: transform var(--sia-duration-base) var(--sia-easing-standard);
58
+ }
59
+ .sia-accordion__item[data-open] .sia-accordion__chevron { transform: rotate(180deg); }
60
+
61
+ /* La hauteur automatique, animée.
62
+
63
+ Une grille dont l'unique ligne passe de `0fr` à `1fr` interpole la hauteur
64
+ réelle du contenu — ce que `height: auto` ne sait pas faire. Aucun
65
+ `scrollHeight` n'est lu, donc rien à recalculer quand le contenu change ou
66
+ quand la fenêtre est redimensionnée. */
67
+ .sia-accordion__panel {
68
+ display: grid;
69
+ grid-template-rows: 0fr;
70
+ transition: grid-template-rows var(--sia-duration-base) var(--sia-easing-standard);
71
+ }
72
+ .sia-accordion__item[data-open] .sia-accordion__panel { grid-template-rows: 1fr; }
73
+
74
+ .sia-accordion__content {
75
+ overflow: hidden;
76
+ min-height: 0;
77
+ color: var(--sia-muted);
78
+ font-size: .875rem;
79
+ line-height: 1.6;
80
+ /* Le texte monte légèrement derrière l'ouverture de la hauteur : les deux
81
+ mouvements se recouvrent, l'ensemble paraît d'un seul tenant. */
82
+ opacity: 0;
83
+ transform: translateY(-4px);
84
+ transition:
85
+ opacity var(--sia-duration-base) var(--sia-easing-standard),
86
+ transform var(--sia-duration-base) var(--sia-easing-standard);
87
+ }
88
+ .sia-accordion__item[data-open] .sia-accordion__content {
89
+ opacity: 1;
90
+ transform: none;
91
+ transition-delay: 40ms;
92
+ }
93
+
94
+ /* Le rembourrage vit sur un enfant, jamais sur `__content` : une marge
95
+ intérieure posée là garderait une hauteur résiduelle une fois replié — le
96
+ défaut classique de cette technique. */
97
+ .sia-accordion__inner { padding: 0 1rem 1rem; }
@@ -0,0 +1,31 @@
1
+ import { type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import type { ComponentTone } from "@sia-ui/tokens";
4
+ import { useSiaLocale } from "@sia-ui/headless";
5
+ import { XIcon } from "../Icons";
6
+ import "./styles.css";
7
+
8
+ export interface AlertProps {
9
+ tone?: ComponentTone;
10
+ title?: ReactNode;
11
+ children?: ReactNode;
12
+ action?: ReactNode;
13
+ dismissLabel?: string;
14
+ onDismiss?: () => void;
15
+ className?: string;
16
+ }
17
+
18
+ export function Alert({ tone = "info", title, children, action, dismissLabel: dismissLabelProp, onDismiss, className }: AlertProps) {
19
+ const locale = useSiaLocale();
20
+ const dismissLabel = dismissLabelProp ?? locale.close;
21
+ return (
22
+ <div className={cn("sia-alert", `sia-alert--${tone}`, className)} role={tone === "danger" ? "alert" : "status"}>
23
+ <div className="sia-alert__content">
24
+ {title && <strong className="sia-alert__title">{title}</strong>}
25
+ {children && <div className="sia-alert__description">{children}</div>}
26
+ </div>
27
+ {action && <div className="sia-alert__action">{action}</div>}
28
+ {onDismiss && <button type="button" className="sia-alert__dismiss" aria-label={dismissLabel} onClick={onDismiss}><XIcon /></button>}
29
+ </div>
30
+ );
31
+ }
@@ -0,0 +1,12 @@
1
+ .sia-alert { display:flex; align-items:flex-start; gap:1rem; padding:1rem; border:1px solid var(--sia-border); border-radius:var(--sia-radius); background:var(--sia-surface); }
2
+ .sia-alert__content { flex:1; display:grid; gap:.25rem; }
3
+ .sia-alert__title { color:var(--sia-foreground); }
4
+ .sia-alert__description { color:var(--sia-muted); font-size:.9rem; line-height:1.5; }
5
+ .sia-alert__action { flex:none; }
6
+ .sia-alert__dismiss { padding:.1rem .35rem; border:0; color:currentColor; background:transparent; font-size:1.25rem; cursor:pointer; }
7
+ .sia-alert--neutral { border-color:var(--sia-border); }
8
+ .sia-alert--primary { border-color:color-mix(in srgb,var(--sia-primary) 35%,var(--sia-border)); }
9
+ .sia-alert--info { border-color:color-mix(in srgb,var(--sia-info) 35%,var(--sia-border)); }
10
+ .sia-alert--success { border-color:color-mix(in srgb,var(--sia-success) 40%,var(--sia-border)); }
11
+ .sia-alert--warning { border-color:color-mix(in srgb,var(--sia-warning) 45%,var(--sia-border)); }
12
+ .sia-alert--danger { border-color:color-mix(in srgb,var(--sia-danger) 40%,var(--sia-border)); }
@@ -0,0 +1,11 @@
1
+ import { type ReactNode } from "react";
2
+ import { cn, formatCurrency } from "@sia-ui/utils";
3
+ import type { ComponentTone } from "@sia-ui/tokens";
4
+ import "./styles.css";
5
+
6
+ export interface AmountDisplayProps { value: number; currency?: string; locale?: string; label?: ReactNode; secondary?: ReactNode; sign?: "auto" | "always"; tone?: ComponentTone; className?: string; }
7
+ export function AmountDisplay({ value, currency = "XAF", locale = "fr-FR", label, secondary, sign = "auto", tone = "neutral", className }: AmountDisplayProps) {
8
+ const formatted = formatCurrency(Math.abs(value), { locale, currency });
9
+ const prefix = sign === "always" || value < 0 ? value < 0 ? "-" : "+" : "";
10
+ return <span className={cn("sia-amount-display", `sia-amount-display--${tone}`, className)}>{label && <span className="sia-amount-display__label">{label}</span>}<strong>{prefix}{formatted}</strong>{secondary && <span className="sia-amount-display__secondary">{secondary}</span>}</span>;
11
+ }
@@ -0,0 +1,9 @@
1
+ .sia-amount-display { display:inline-grid; gap:.15rem; }
2
+ .sia-amount-display strong { font-variant-numeric:tabular-nums; }
3
+ .sia-amount-display__label,.sia-amount-display__secondary { color:var(--sia-muted); font-size:.8rem; }
4
+ .sia-amount-display--neutral strong { color:var(--sia-foreground); }
5
+ .sia-amount-display--primary strong { color:var(--sia-primary); }
6
+ .sia-amount-display--info strong { color:var(--sia-info); }
7
+ .sia-amount-display--success strong { color:var(--sia-success); }
8
+ .sia-amount-display--warning strong { color:var(--sia-warning); }
9
+ .sia-amount-display--danger strong { color:var(--sia-danger); }
@@ -0,0 +1,6 @@
1
+ import { useState, type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import "./styles.css";
4
+
5
+ export interface AppShellProps { logo?: ReactNode; navigation: ReactNode; header?: ReactNode; children: ReactNode; footer?: ReactNode; defaultCollapsed?: boolean; className?: string; }
6
+ export function AppShell({ logo, navigation, header, children, footer, defaultCollapsed = false, className }: AppShellProps) { const [collapsed, setCollapsed] = useState(defaultCollapsed); return <div className={cn("sia-app-shell", collapsed && "sia-app-shell--collapsed", className)}><aside><div className="sia-app-shell__brand">{logo}<button type="button" aria-label={collapsed ? "Développer le menu" : "Réduire le menu"} onClick={() => setCollapsed(!collapsed)}>☰</button></div><nav>{navigation}</nav></aside><div className="sia-app-shell__main">{header && <header>{header}</header>}<main>{children}</main>{footer && <footer>{footer}</footer>}</div></div>; }
@@ -0,0 +1,7 @@
1
+ .sia-app-shell { min-height:32rem; display:grid; grid-template-columns:16rem 1fr; border:1px solid var(--sia-border); }
2
+ .sia-app-shell--collapsed { grid-template-columns:4rem 1fr; }
3
+ .sia-app-shell > aside { overflow:hidden; border-right:1px solid var(--sia-border); background:var(--sia-surface); }
4
+ .sia-app-shell__brand { display:flex; align-items:center; justify-content:space-between; min-height:4rem; padding:.75rem; }
5
+ .sia-app-shell__main { min-width:0; display:flex; flex-direction:column; }
6
+ .sia-app-shell__main > header,.sia-app-shell__main > footer { padding:1rem; border-bottom:1px solid var(--sia-border); }
7
+ .sia-app-shell__main > main { flex:1; padding:1rem; }