@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.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/diff.mjs +91 -0
- package/dist/index.js +325 -0
- package/dist/lifecycle.mjs +242 -0
- package/dist/lock.mjs +92 -0
- package/dist/registry/package/components/Accordion/index.tsx +172 -0
- package/dist/registry/package/components/Accordion/styles.css +97 -0
- package/dist/registry/package/components/Alert/index.tsx +31 -0
- package/dist/registry/package/components/Alert/styles.css +12 -0
- package/dist/registry/package/components/AmountDisplay/index.tsx +11 -0
- package/dist/registry/package/components/AmountDisplay/styles.css +9 -0
- package/dist/registry/package/components/AppShell/index.tsx +6 -0
- package/dist/registry/package/components/AppShell/styles.css +7 -0
- package/dist/registry/package/components/AspectRatio/index.tsx +29 -0
- package/dist/registry/package/components/AspectRatio/styles.css +15 -0
- package/dist/registry/package/components/AsyncState/index.tsx +22 -0
- package/dist/registry/package/components/AsyncState/styles.css +1 -0
- package/dist/registry/package/components/AuditMeta/index.tsx +44 -0
- package/dist/registry/package/components/AuditMeta/styles.css +12 -0
- package/dist/registry/package/components/AuthLayout/index.tsx +39 -0
- package/dist/registry/package/components/AuthLayout/styles.css +6 -0
- package/dist/registry/package/components/Autocomplete/index.tsx +8 -0
- package/dist/registry/package/components/Avatar/index.tsx +23 -0
- package/dist/registry/package/components/Avatar/styles.css +5 -0
- package/dist/registry/package/components/Badge/index.tsx +62 -0
- package/dist/registry/package/components/Badge/styles.css +26 -0
- package/dist/registry/package/components/Breadcrumb/index.tsx +131 -0
- package/dist/registry/package/components/Breadcrumb/styles.css +76 -0
- package/dist/registry/package/components/Button/index.tsx +91 -0
- package/dist/registry/package/components/Button/styles.css +111 -0
- package/dist/registry/package/components/Calendar/index.tsx +192 -0
- package/dist/registry/package/components/Card/index.tsx +8 -0
- package/dist/registry/package/components/Card/styles.css +4 -0
- package/dist/registry/package/components/Chart/index.tsx +437 -0
- package/dist/registry/package/components/Chart/styles.css +182 -0
- package/dist/registry/package/components/Checkbox/index.tsx +193 -0
- package/dist/registry/package/components/Checkbox/styles.css +39 -0
- package/dist/registry/package/components/ColorPicker/index.tsx +6 -0
- package/dist/registry/package/components/ColorPicker/styles.css +2 -0
- package/dist/registry/package/components/CommandPalette/index.tsx +7 -0
- package/dist/registry/package/components/CommandPalette/styles.css +8 -0
- package/dist/registry/package/components/ConfirmDialog/index.tsx +24 -0
- package/dist/registry/package/components/Container/index.tsx +11 -0
- package/dist/registry/package/components/Container/styles.css +1 -0
- package/dist/registry/package/components/Countdown/index.tsx +3 -0
- package/dist/registry/package/components/CrudPage/index.tsx +9 -0
- package/dist/registry/package/components/CrudPage/styles.css +1 -0
- package/dist/registry/package/components/CurrencyInput/index.tsx +15 -0
- package/dist/registry/package/components/CurrencyInput/styles.css +1 -0
- package/dist/registry/package/components/DataTable/index.tsx +240 -0
- package/dist/registry/package/components/DataTable/styles.css +98 -0
- package/dist/registry/package/components/DatePicker/index.tsx +102 -0
- package/dist/registry/package/components/DateRangeFilter/index.tsx +8 -0
- package/dist/registry/package/components/DateRangeFilter/styles.css +4 -0
- package/dist/registry/package/components/DateRangePicker/index.tsx +36 -0
- package/dist/registry/package/components/DateTimePicker/index.tsx +32 -0
- package/dist/registry/package/components/Descriptions/index.tsx +7 -0
- package/dist/registry/package/components/Descriptions/styles.css +8 -0
- package/dist/registry/package/components/Divider/index.tsx +12 -0
- package/dist/registry/package/components/Divider/styles.css +3 -0
- package/dist/registry/package/components/Drawer/context.ts +14 -0
- package/dist/registry/package/components/Drawer/index.tsx +209 -0
- package/dist/registry/package/components/Drawer/styles.css +151 -0
- package/dist/registry/package/components/DropdownMenu/index.tsx +350 -0
- package/dist/registry/package/components/DropdownMenu/styles.css +100 -0
- package/dist/registry/package/components/DurationDisplay/index.tsx +3 -0
- package/dist/registry/package/components/EmptyState/index.tsx +23 -0
- package/dist/registry/package/components/EmptyState/styles.css +6 -0
- package/dist/registry/package/components/EntityMeta/index.tsx +65 -0
- package/dist/registry/package/components/EntityMeta/styles.css +9 -0
- package/dist/registry/package/components/ErrorState/index.tsx +30 -0
- package/dist/registry/package/components/ErrorState/styles.css +6 -0
- package/dist/registry/package/components/EventCalendar/index.tsx +8 -0
- package/dist/registry/package/components/EventCalendar/styles.css +8 -0
- package/dist/registry/package/components/Field/index.tsx +664 -0
- package/dist/registry/package/components/Field/styles.css +14 -0
- package/dist/registry/package/components/FileUpload/index.tsx +68 -0
- package/dist/registry/package/components/FileUpload/styles.css +4 -0
- package/dist/registry/package/components/FiltersBar/index.tsx +20 -0
- package/dist/registry/package/components/FiltersBar/styles.css +6 -0
- package/dist/registry/package/components/Form/index.tsx +211 -0
- package/dist/registry/package/components/Form/styles.css +80 -0
- package/dist/registry/package/components/FormField/index.tsx +2 -0
- package/dist/registry/package/components/HoverCard/index.tsx +121 -0
- package/dist/registry/package/components/HoverCard/styles.css +26 -0
- package/dist/registry/package/components/IconButton/index.tsx +27 -0
- package/dist/registry/package/components/IconButton/styles.css +7 -0
- package/dist/registry/package/components/Icons/index.tsx +185 -0
- package/dist/registry/package/components/ImageUpload/index.tsx +6 -0
- package/dist/registry/package/components/ImageUpload/styles.css +4 -0
- package/dist/registry/package/components/Input/index.tsx +103 -0
- package/dist/registry/package/components/Input/styles.css +8 -0
- package/dist/registry/package/components/InputNumber/index.tsx +196 -0
- package/dist/registry/package/components/InputNumber/styles.css +54 -0
- package/dist/registry/package/components/JsonEditor/index.tsx +6 -0
- package/dist/registry/package/components/JsonEditor/styles.css +2 -0
- package/dist/registry/package/components/KpiGrid/index.tsx +11 -0
- package/dist/registry/package/components/KpiGrid/styles.css +5 -0
- package/dist/registry/package/components/Label/index.tsx +43 -0
- package/dist/registry/package/components/Label/styles.css +25 -0
- package/dist/registry/package/components/MarkdownEditor/index.tsx +5 -0
- package/dist/registry/package/components/MiniCalendar/index.tsx +3 -0
- package/dist/registry/package/components/Modal/index.tsx +104 -0
- package/dist/registry/package/components/Modal/styles.css +20 -0
- package/dist/registry/package/components/MonthPicker/index.tsx +5 -0
- package/dist/registry/package/components/MultiSelect/index.tsx +133 -0
- package/dist/registry/package/components/MultiSelect/styles.css +3 -0
- package/dist/registry/package/components/OtpInput/index.tsx +5 -0
- package/dist/registry/package/components/OtpInput/styles.css +2 -0
- package/dist/registry/package/components/Overlay/index.tsx +140 -0
- package/dist/registry/package/components/Overlay/styles.css +1 -0
- package/dist/registry/package/components/PageHeader/index.tsx +24 -0
- package/dist/registry/package/components/PageHeader/styles.css +5 -0
- package/dist/registry/package/components/Pagination/index.tsx +155 -0
- package/dist/registry/package/components/Pagination/styles.css +103 -0
- package/dist/registry/package/components/PasswordInput/index.tsx +125 -0
- package/dist/registry/package/components/PasswordInput/styles.css +58 -0
- package/dist/registry/package/components/PermissionGate/index.tsx +3 -0
- package/dist/registry/package/components/PhoneInput/index.tsx +189 -0
- package/dist/registry/package/components/PhoneInput/styles.css +35 -0
- package/dist/registry/package/components/Popover/index.tsx +12 -0
- package/dist/registry/package/components/Popover/styles.css +12 -0
- package/dist/registry/package/components/Progress/index.tsx +149 -0
- package/dist/registry/package/components/Progress/styles.css +101 -0
- package/dist/registry/package/components/QrCode/index.tsx +6 -0
- package/dist/registry/package/components/QrCode/styles.css +5 -0
- package/dist/registry/package/components/RadioGroup/index.tsx +29 -0
- package/dist/registry/package/components/RadioGroup/styles.css +20 -0
- package/dist/registry/package/components/Rating/index.tsx +82 -0
- package/dist/registry/package/components/Rating/styles.css +41 -0
- package/dist/registry/package/components/ReferenceSelect/index.tsx +7 -0
- package/dist/registry/package/components/ReferenceSelect/styles.css +2 -0
- package/dist/registry/package/components/RelativeTime/index.tsx +22 -0
- package/dist/registry/package/components/RelativeTime/styles.css +1 -0
- package/dist/registry/package/components/RequireSession/index.tsx +127 -0
- package/dist/registry/package/components/RequireSession/styles.css +13 -0
- package/dist/registry/package/components/ResizablePanel/index.tsx +6 -0
- package/dist/registry/package/components/ResizablePanel/styles.css +5 -0
- package/dist/registry/package/components/RichTextEditor/index.tsx +5 -0
- package/dist/registry/package/components/RichTextEditor/styles.css +2 -0
- package/dist/registry/package/components/ScrollArea/index.tsx +114 -0
- package/dist/registry/package/components/ScrollArea/styles.css +74 -0
- package/dist/registry/package/components/SearchInput/index.tsx +121 -0
- package/dist/registry/package/components/SearchInput/styles.css +6 -0
- package/dist/registry/package/components/Select/index.tsx +324 -0
- package/dist/registry/package/components/Select/styles.css +58 -0
- package/dist/registry/package/components/Sidebar/index.tsx +232 -0
- package/dist/registry/package/components/Sidebar/styles.css +140 -0
- package/dist/registry/package/components/Skeleton/index.tsx +21 -0
- package/dist/registry/package/components/Skeleton/styles.css +2 -0
- package/dist/registry/package/components/Slider/index.tsx +193 -0
- package/dist/registry/package/components/Slider/styles.css +53 -0
- package/dist/registry/package/components/Spinner/index.tsx +28 -0
- package/dist/registry/package/components/Spinner/styles.css +8 -0
- package/dist/registry/package/components/StatCard/index.tsx +15 -0
- package/dist/registry/package/components/StatCard/styles.css +3 -0
- package/dist/registry/package/components/Statistic/index.tsx +23 -0
- package/dist/registry/package/components/Statistic/styles.css +6 -0
- package/dist/registry/package/components/Steps/index.tsx +108 -0
- package/dist/registry/package/components/Steps/styles.css +138 -0
- package/dist/registry/package/components/Switch/index.tsx +46 -0
- package/dist/registry/package/components/Switch/styles.css +26 -0
- package/dist/registry/package/components/Tabs/index.tsx +44 -0
- package/dist/registry/package/components/Tabs/styles.css +41 -0
- package/dist/registry/package/components/TagsInput/index.tsx +6 -0
- package/dist/registry/package/components/TagsInput/styles.css +2 -0
- package/dist/registry/package/components/Textarea/index.tsx +21 -0
- package/dist/registry/package/components/Textarea/styles.css +7 -0
- package/dist/registry/package/components/TimePicker/index.tsx +76 -0
- package/dist/registry/package/components/TimePicker/styles.css +13 -0
- package/dist/registry/package/components/TimeRangePicker/index.tsx +32 -0
- package/dist/registry/package/components/TimeRangePicker/styles.css +1 -0
- package/dist/registry/package/components/Timeline/index.tsx +8 -0
- package/dist/registry/package/components/Timeline/styles.css +12 -0
- package/dist/registry/package/components/Timer/index.tsx +9 -0
- package/dist/registry/package/components/Toast/index.tsx +426 -0
- package/dist/registry/package/components/Toast/styles.css +175 -0
- package/dist/registry/package/components/Toggle/index.tsx +181 -0
- package/dist/registry/package/components/Toggle/styles.css +124 -0
- package/dist/registry/package/components/Tooltip/index.tsx +163 -0
- package/dist/registry/package/components/Tooltip/styles.css +57 -0
- package/dist/registry/package/components/Tour/index.tsx +8 -0
- package/dist/registry/package/components/Tour/styles.css +6 -0
- package/dist/registry/package/components/Transfer/index.tsx +7 -0
- package/dist/registry/package/components/Transfer/styles.css +6 -0
- package/dist/registry/package/components/Tree/index.tsx +8 -0
- package/dist/registry/package/components/Tree/styles.css +6 -0
- package/dist/registry/package/components/TreeSelect/index.tsx +10 -0
- package/dist/registry/package/components/TreeSelect/styles.css +3 -0
- package/dist/registry/package/components/Typography/index.tsx +27 -0
- package/dist/registry/package/components/Typography/styles.css +16 -0
- package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +4 -0
- package/dist/registry/package/components/WeekPicker/index.tsx +5 -0
- package/dist/registry/package/components/YearPicker/index.tsx +3 -0
- package/dist/registry/package/lib/form-adapter-rhf.tsx +141 -0
- package/dist/registry/registry.json +1780 -0
- package/dist/registry/styles/sia-ui.css +361 -0
- package/dist/source.mjs +93 -0
- 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; }
|