@sia-ui/cli 0.2.1 → 0.3.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 (100) hide show
  1. package/LICENSE +21 -21
  2. package/dist/index.js +2 -0
  3. package/dist/registry/package/components/Alert/index.tsx +7 -0
  4. package/dist/registry/package/components/AmountDisplay/index.tsx +8 -0
  5. package/dist/registry/package/components/AppShell/index.tsx +413 -3
  6. package/dist/registry/package/components/AppShell/styles.css +259 -7
  7. package/dist/registry/package/components/AppShell/tab-bar.tsx +134 -0
  8. package/dist/registry/package/components/AsyncState/index.tsx +7 -0
  9. package/dist/registry/package/components/AuditMeta/index.tsx +8 -0
  10. package/dist/registry/package/components/AuthLayout/index.tsx +7 -0
  11. package/dist/registry/package/components/Autocomplete/index.tsx +7 -0
  12. package/dist/registry/package/components/Avatar/index.tsx +333 -12
  13. package/dist/registry/package/components/Avatar/styles.css +236 -5
  14. package/dist/registry/package/components/Badge/index.tsx +7 -0
  15. package/dist/registry/package/components/Button/index.tsx +7 -0
  16. package/dist/registry/package/components/Calendar/index.tsx +11 -0
  17. package/dist/registry/package/components/Card/index.tsx +8 -0
  18. package/dist/registry/package/components/Checkbox/index.tsx +8 -0
  19. package/dist/registry/package/components/ColorPicker/index.tsx +7 -0
  20. package/dist/registry/package/components/CommandPalette/index.tsx +7 -0
  21. package/dist/registry/package/components/ConfirmDialog/index.tsx +7 -0
  22. package/dist/registry/package/components/Container/index.tsx +7 -0
  23. package/dist/registry/package/components/Countdown/index.tsx +7 -0
  24. package/dist/registry/package/components/CrudPage/dialogs.tsx +230 -0
  25. package/dist/registry/package/components/CrudPage/index.tsx +546 -5
  26. package/dist/registry/package/components/CrudPage/styles.css +25 -1
  27. package/dist/registry/package/components/CurrencyInput/index.tsx +7 -0
  28. package/dist/registry/package/components/DataTable/index.tsx +548 -200
  29. package/dist/registry/package/components/DataTable/query.ts +68 -0
  30. package/dist/registry/package/components/DataTable/row-actions.tsx +158 -0
  31. package/dist/registry/package/components/DataTable/styles.css +285 -73
  32. package/dist/registry/package/components/DataTable/types.ts +194 -0
  33. package/dist/registry/package/components/DataTable/values.ts +28 -0
  34. package/dist/registry/package/components/DatePicker/index.tsx +7 -0
  35. package/dist/registry/package/components/DateRangeFilter/index.tsx +7 -0
  36. package/dist/registry/package/components/DateRangePicker/index.tsx +7 -0
  37. package/dist/registry/package/components/DateTimePicker/index.tsx +7 -0
  38. package/dist/registry/package/components/Descriptions/index.tsx +7 -0
  39. package/dist/registry/package/components/Divider/index.tsx +7 -0
  40. package/dist/registry/package/components/Drawer/index.tsx +8 -0
  41. package/dist/registry/package/components/DurationDisplay/index.tsx +6 -0
  42. package/dist/registry/package/components/EmptyState/index.tsx +7 -0
  43. package/dist/registry/package/components/EntityMeta/index.tsx +7 -0
  44. package/dist/registry/package/components/ErrorState/index.tsx +8 -0
  45. package/dist/registry/package/components/EventCalendar/index.tsx +7 -0
  46. package/dist/registry/package/components/Field/index.tsx +12 -0
  47. package/dist/registry/package/components/FileUpload/index.tsx +7 -0
  48. package/dist/registry/package/components/FiltersBar/index.tsx +7 -0
  49. package/dist/registry/package/components/HoverCard/index.tsx +14 -37
  50. package/dist/registry/package/components/IconButton/index.tsx +8 -0
  51. package/dist/registry/package/components/Icons/index.tsx +15 -0
  52. package/dist/registry/package/components/ImageUpload/index.tsx +7 -0
  53. package/dist/registry/package/components/Input/index.tsx +7 -0
  54. package/dist/registry/package/components/JsonEditor/index.tsx +7 -0
  55. package/dist/registry/package/components/KpiGrid/index.tsx +7 -0
  56. package/dist/registry/package/components/Label/index.tsx +7 -0
  57. package/dist/registry/package/components/MarkdownEditor/index.tsx +7 -0
  58. package/dist/registry/package/components/MiniCalendar/index.tsx +7 -0
  59. package/dist/registry/package/components/Modal/index.tsx +8 -0
  60. package/dist/registry/package/components/MonthPicker/index.tsx +7 -0
  61. package/dist/registry/package/components/MultiSelect/index.tsx +7 -0
  62. package/dist/registry/package/components/OtpInput/index.tsx +7 -0
  63. package/dist/registry/package/components/Overlay/index.tsx +34 -24
  64. package/dist/registry/package/components/PageHeader/index.tsx +7 -0
  65. package/dist/registry/package/components/PermissionGate/index.tsx +6 -0
  66. package/dist/registry/package/components/Popover/index.tsx +7 -0
  67. package/dist/registry/package/components/QrCode/index.tsx +7 -0
  68. package/dist/registry/package/components/RadioGroup/index.tsx +7 -0
  69. package/dist/registry/package/components/Rating/index.tsx +6 -0
  70. package/dist/registry/package/components/ReferenceSelect/index.tsx +7 -0
  71. package/dist/registry/package/components/RelativeTime/index.tsx +6 -0
  72. package/dist/registry/package/components/ResizablePanel/index.tsx +7 -0
  73. package/dist/registry/package/components/Resource/index.tsx +514 -0
  74. package/dist/registry/package/components/RichTextEditor/index.tsx +7 -0
  75. package/dist/registry/package/components/SearchInput/index.tsx +8 -0
  76. package/dist/registry/package/components/Select/index.tsx +11 -0
  77. package/dist/registry/package/components/Sidebar/index.tsx +489 -113
  78. package/dist/registry/package/components/Sidebar/styles.css +96 -4
  79. package/dist/registry/package/components/Skeleton/index.tsx +7 -0
  80. package/dist/registry/package/components/Spinner/index.tsx +7 -0
  81. package/dist/registry/package/components/StatCard/index.tsx +7 -0
  82. package/dist/registry/package/components/Statistic/index.tsx +7 -0
  83. package/dist/registry/package/components/Switch/index.tsx +7 -0
  84. package/dist/registry/package/components/Tabs/index.tsx +7 -0
  85. package/dist/registry/package/components/TagsInput/index.tsx +7 -0
  86. package/dist/registry/package/components/Textarea/index.tsx +7 -0
  87. package/dist/registry/package/components/TimePicker/index.tsx +7 -0
  88. package/dist/registry/package/components/TimeRangePicker/index.tsx +6 -0
  89. package/dist/registry/package/components/Timeline/index.tsx +7 -0
  90. package/dist/registry/package/components/Timer/index.tsx +7 -0
  91. package/dist/registry/package/components/Tour/index.tsx +7 -0
  92. package/dist/registry/package/components/Transfer/index.tsx +7 -0
  93. package/dist/registry/package/components/Tree/index.tsx +7 -0
  94. package/dist/registry/package/components/TreeSelect/index.tsx +7 -0
  95. package/dist/registry/package/components/Typography/index.tsx +8 -0
  96. package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +8 -0
  97. package/dist/registry/package/components/WeekPicker/index.tsx +7 -0
  98. package/dist/registry/package/components/YearPicker/index.tsx +7 -0
  99. package/dist/registry/registry.json +69 -14
  100. package/package.json +14 -14
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Ivan Mbella
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ivan Mbella
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ #!/usr/bin/env node
2
+
1
3
  import { access, cp, mkdir, readFile, writeFile } from "node:fs/promises";
2
4
  import { dirname, join, resolve, sep } from "node:path";
3
5
  import { fileURLToPath } from "node:url";
@@ -15,6 +15,13 @@ export interface AlertProps {
15
15
  className?: string;
16
16
  }
17
17
 
18
+ /**
19
+ * Un message qui reste à l'écran.
20
+ *
21
+ * À la différence d'une annonce, il ne part pas tout seul : il décrit un
22
+ * état de la page — un solde insuffisant, une synchronisation en retard —
23
+ * et disparaît quand cet état change, pas au bout de cinq secondes.
24
+ */
18
25
  export function Alert({ tone = "info", title, children, action, dismissLabel: dismissLabelProp, onDismiss, className }: AlertProps) {
19
26
  const locale = useSiaLocale();
20
27
  const dismissLabel = dismissLabelProp ?? locale.close;
@@ -4,6 +4,14 @@ import type { ComponentTone } from "@sia-ui/tokens";
4
4
  import "./styles.css";
5
5
 
6
6
  export interface AmountDisplayProps { value: number; currency?: string; locale?: string; label?: ReactNode; secondary?: ReactNode; sign?: "auto" | "always"; tone?: ComponentTone; className?: string; }
7
+ /**
8
+ * Un montant, lisible d'un coup d'œil.
9
+ *
10
+ * Le signe est porté par la mise en forme et non par le nombre : un débit
11
+ * de 12 000 francs s'écrit « -12 000 F CFA », jamais « 12 000 F CFA » en
12
+ * rouge seul. La couleur peut manquer — un daltonien, une impression en
13
+ * noir et blanc — le signe, lui, reste.
14
+ */
7
15
  export function AmountDisplay({ value, currency = "XAF", locale = "fr-FR", label, secondary, sign = "auto", tone = "neutral", className }: AmountDisplayProps) {
8
16
  const formatted = formatCurrency(Math.abs(value), { locale, currency });
9
17
  const prefix = sign === "always" || value < 0 ? value < 0 ? "-" : "+" : "";
@@ -1,6 +1,416 @@
1
- import { useState, type ReactNode } from "react";
1
+ import {
2
+ useCallback,
3
+ useMemo,
4
+ useState,
5
+ type CSSProperties,
6
+ type ReactNode,
7
+ } from "react";
2
8
  import { cn } from "@sia-ui/utils";
9
+ import {
10
+ filterNavTree,
11
+ navKeyForPath,
12
+ type PermissionRule,
13
+ } from "@sia-ui/headless";
14
+ import { useMediaQuery } from "@sia-ui/react";
15
+ import { Drawer } from "../Drawer";
16
+ import {
17
+ Sidebar,
18
+ type SidebarEntry,
19
+ type SidebarItem,
20
+ type SidebarRenderLink,
21
+ } from "../Sidebar";
22
+ import { BottomTabs } from "./tab-bar";
3
23
  import "./styles.css";
4
24
 
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>; }
25
+ export { BottomTabs, type BottomTabsProps } from "./tab-bar";
26
+
27
+ /**
28
+ * L'état de la barre latérale sur grand écran.
29
+ *
30
+ * Trois valeurs et non un booléen : « réduite » et « masquée » sont deux
31
+ * intentions différentes, et une application qui veut l'une ne veut
32
+ * généralement pas l'autre. Sous le seuil mobile, l'état est ignoré — la
33
+ * barre cède la place à la navigation basse.
34
+ */
35
+ export type SidebarState = "expanded" | "collapsed" | "hidden";
36
+
37
+ /**
38
+ * Ce que fait le bouton de bascule.
39
+ *
40
+ * `icon` alterne déployé et rail, `offcanvas` alterne déployé et masqué,
41
+ * `false` retire le bouton.
42
+ */
43
+ export type AppShellCollapsible = "icon" | "offcanvas" | false;
44
+
45
+ /**
46
+ * La navigation sous le seuil mobile.
47
+ *
48
+ * `both` n'est pas un défaut par hasard : deux systèmes de navigation
49
+ * simultanés se disputent l'attention, et l'utilisateur ne sait plus lequel
50
+ * fait autorité.
51
+ */
52
+ export type AppShellMobileNav = "drawer" | "tabs" | "both";
53
+
54
+ export type AppShellContentWidth = "wide" | "narrow" | "full";
55
+
56
+ export interface AppShellProps {
57
+ nav: SidebarEntry[];
58
+ children: ReactNode;
59
+
60
+ /**
61
+ * La clé de l'entrée courante.
62
+ *
63
+ * Laissée vide, elle est déduite de `currentPath`. La fournir explicitement
64
+ * ne sert que si les clés ne correspondent à aucune adresse.
65
+ */
66
+ activeKey?: string;
67
+
68
+ /**
69
+ * L'adresse courante — **le point d'accroche du routeur**.
70
+ *
71
+ * Une seule chaîne, que tous savent produire :
72
+ *
73
+ * ```tsx
74
+ * const { pathname } = useLocation(); // React Router
75
+ * const pathname = usePathname(); // Next.js
76
+ * const [pathname] = useLocation(); // wouter
77
+ * ```
78
+ *
79
+ * De là, la coquille déduit l'entrée active par le préfixe le plus long, et
80
+ * referme le tiroir mobile à chaque navigation — sans jamais importer de
81
+ * routeur.
82
+ */
83
+ currentPath?: string;
84
+
85
+ /** Branche les liens sur le routeur. À stabiliser avec `useCallback`. */
86
+ renderLink?: SidebarRenderLink;
87
+ onNavigate?: (item: SidebarItem) => void;
88
+
89
+ /**
90
+ * Évalue les règles de permission.
91
+ *
92
+ * Le filtrage est fait **une seule fois** ici : ni la barre latérale ni les
93
+ * onglets ne doivent avoir leur propre idée de ce qui est visible.
94
+ */
95
+ can?: (rule: PermissionRule) => boolean;
96
+
97
+ /**
98
+ * En tête de barre latérale : logo, nom du produit, sélecteur d'entité.
99
+ *
100
+ * Reçoit `data-collapsed` en mode rail, comme `sidebarFooter` : sans quoi
101
+ * un nom de produit écrit en toutes lettres déborde des quatre rem du rail.
102
+ */
103
+ brand?: ReactNode;
104
+
105
+ /**
106
+ * Bas de la barre latérale, sous la navigation.
107
+ *
108
+ * C'est la place de la déconnexion et des réglages du compte — ce qui sort
109
+ * de l'application, par opposition à l'en-tête, réservé à ce qui agit sur
110
+ * l'écran courant. Reçoit `data-collapsed` en mode rail, de quoi masquer un
111
+ * libellé sans changer de composant.
112
+ */
113
+ sidebarFooter?: ReactNode;
114
+
115
+ /** La barre du haut : recherche, notifications, avatar de compte. */
116
+ header?: ReactNode;
117
+
118
+ /**
119
+ * Au-dessus du contenu, pleine largeur de la zone.
120
+ *
121
+ * L'endroit d'un `PageHeader`. La coquille ne prend pas de `title` : un
122
+ * `<h1>` rendu par la coquille force chaque page à passer par ses props, et
123
+ * une page qui veut deux titres ou une mise en page à elle doit se battre.
124
+ */
125
+ page?: ReactNode;
126
+
127
+ footer?: ReactNode;
128
+
129
+ /**
130
+ * La largeur de la zone de contenu.
131
+ *
132
+ * Décidée ici plutôt qu'écran par écran : des largeurs posées page après
133
+ * page finissent par diverger, et personne ne sait laquelle fait foi.
134
+ */
135
+ contentWidth?: AppShellContentWidth;
136
+
137
+ side?: "left" | "right";
138
+ variant?: "default" | "filled" | "floating";
139
+
140
+ /** L'en-tête suit-il le défilement. */
141
+ stickyHeader?: boolean;
142
+
143
+ sidebarState?: SidebarState;
144
+ defaultSidebarState?: SidebarState;
145
+ onSidebarStateChange?: (state: SidebarState) => void;
146
+ collapsible?: AppShellCollapsible;
147
+
148
+ /** Toute valeur CSS : `16rem`, `280px`, `min(20vw, 320px)`. */
149
+ sidebarWidth?: string;
150
+ sidebarCollapsedWidth?: string;
151
+
152
+ mobileNav?: AppShellMobileNav;
153
+ /** Sous cette largeur, la barre latérale cède la place. */
154
+ mobileBreakpoint?: number;
155
+
156
+ ariaLabel?: string;
157
+ className?: string;
158
+ }
159
+
160
+ /**
161
+ * La coquille d'une application.
162
+ *
163
+ * Elle n'invente rien : la navigation est le `Sidebar`, le tiroir est le
164
+ * `Drawer`, les droits viennent de `@sia-ui/headless`. Ce qu'elle apporte est
165
+ * l'assemblage — trois états de barre, une bascule mobile, et une seule
166
+ * source de vérité pour ce qui est visible et pour ce qui est actif.
167
+ */
168
+ export function AppShell({
169
+ nav,
170
+ children,
171
+ activeKey,
172
+ currentPath,
173
+ renderLink,
174
+ onNavigate,
175
+ can,
176
+ brand,
177
+ sidebarFooter,
178
+ header,
179
+ page,
180
+ footer,
181
+ contentWidth = "wide",
182
+ side = "left",
183
+ variant = "default",
184
+ stickyHeader = true,
185
+ sidebarState,
186
+ defaultSidebarState = "expanded",
187
+ onSidebarStateChange,
188
+ collapsible = "icon",
189
+ sidebarWidth,
190
+ sidebarCollapsedWidth,
191
+ mobileNav = "drawer",
192
+ mobileBreakpoint = 768,
193
+ ariaLabel,
194
+ className,
195
+ }: AppShellProps) {
196
+ const compact = useMediaQuery(`(max-width: ${mobileBreakpoint - 1}px)`);
197
+
198
+ // Filtré une fois, consommé par la barre et par les onglets.
199
+ const visibles = useMemo(
200
+ () => (can ? filterNavTree(nav as never, can) : nav),
201
+ [can, nav],
202
+ ) as SidebarEntry[];
203
+
204
+ const plats = useMemo(
205
+ () =>
206
+ visibles.flatMap((e) =>
207
+ "title" in e && Array.isArray(e.items) ? e.items : [e as SidebarItem],
208
+ ),
209
+ [visibles],
210
+ );
211
+
212
+ const deduite = useMemo(
213
+ () => navKeyForPath(plats, currentPath),
214
+ [currentPath, plats],
215
+ );
216
+ const active = activeKey ?? deduite;
217
+
218
+ const [interne, setInterne] = useState<SidebarState>(defaultSidebarState);
219
+ const pilote = sidebarState !== undefined;
220
+ const etat = pilote ? sidebarState : interne;
221
+
222
+ const basculer = useCallback(() => {
223
+ const reduit = collapsible === "offcanvas" ? "hidden" : "collapsed";
224
+ const suivant: SidebarState = etat === "expanded" ? reduit : "expanded";
225
+ if (!pilote) setInterne(suivant);
226
+ onSidebarStateChange?.(suivant);
227
+ }, [collapsible, etat, onSidebarStateChange, pilote]);
228
+
229
+ const [tiroirOuvert, setTiroirOuvert] = useState(false);
230
+
231
+ /**
232
+ * Le tiroir se referme à chaque navigation.
233
+ *
234
+ * C'est ce qu'on oublie toujours : on touche une entrée, la page change
235
+ * derrière, et le tiroir reste ouvert par-dessus. Le changement d'adresse
236
+ * suffit à le détecter — aucun abonnement au routeur n'est nécessaire.
237
+ */
238
+ const [dernierChemin, setDernierChemin] = useState(currentPath);
239
+ if (currentPath !== dernierChemin) {
240
+ setDernierChemin(currentPath);
241
+ if (tiroirOuvert) setTiroirOuvert(false);
242
+ }
243
+
244
+ const naviguer = useCallback(
245
+ (item: SidebarItem) => {
246
+ onNavigate?.(item);
247
+ setTiroirOuvert(false);
248
+ },
249
+ [onNavigate],
250
+ );
251
+
252
+ const reduite = etat === "collapsed";
253
+ const masquee = etat === "hidden";
254
+ const montrerTiroir = mobileNav === "drawer" || mobileNav === "both";
255
+ const montrerOnglets = mobileNav === "tabs" || mobileNav === "both";
256
+
257
+ const barre = (
258
+ <Sidebar
259
+ items={visibles}
260
+ collapsed={reduite && !compact}
261
+ side={side}
262
+ variant={variant}
263
+ {...(active !== undefined ? { activeKey: active } : {})}
264
+ {...(renderLink ? { renderLink } : {})}
265
+ onNavigate={naviguer}
266
+ {...(brand
267
+ ? {
268
+ header: (
269
+ <div data-collapsed={reduite && !compact ? "" : undefined}>
270
+ {brand}
271
+ </div>
272
+ ),
273
+ }
274
+ : {})}
275
+ {...(sidebarFooter
276
+ ? {
277
+ footer: (
278
+ <div data-collapsed={reduite && !compact ? "" : undefined}>
279
+ {sidebarFooter}
280
+ </div>
281
+ ),
282
+ }
283
+ : {})}
284
+ {...(ariaLabel ? { ariaLabel } : {})}
285
+ />
286
+ );
287
+
288
+ const style: CSSProperties & Record<string, string> = {} as never;
289
+ if (sidebarWidth) style["--sia-shell-sidebar"] = sidebarWidth;
290
+ if (sidebarCollapsedWidth) style["--sia-shell-rail"] = sidebarCollapsedWidth;
291
+
292
+ return (
293
+ <div
294
+ className={cn(
295
+ "sia-shell",
296
+ `sia-shell--${side}`,
297
+ `sia-shell--${variant}`,
298
+ reduite && "sia-shell--rail",
299
+ masquee && "sia-shell--hidden",
300
+ compact && "sia-shell--compact",
301
+ montrerOnglets && compact && "sia-shell--with-tabs",
302
+ className,
303
+ )}
304
+ style={style}
305
+ >
306
+ {/* Sur petit écran, la barre latérale n'a pas la place de rester
307
+ ouverte : elle devient un tiroir. Sans lui, l'arborescence complète
308
+ ne serait atteignable que par « Plus », ce qui ne se devine pas. */}
309
+ {!compact && !masquee && <aside className="sia-shell__aside">{barre}</aside>}
310
+
311
+ <div className="sia-shell__main">
312
+ {(header || collapsible !== false || (compact && montrerTiroir)) && (
313
+ <header
314
+ className={cn(
315
+ "sia-shell__header",
316
+ stickyHeader && "sia-shell__header--sticky",
317
+ )}
318
+ >
319
+ {compact && montrerTiroir ? (
320
+ <button
321
+ type="button"
322
+ className="sia-shell__toggle"
323
+ aria-label="Ouvrir la navigation"
324
+ aria-expanded={tiroirOuvert}
325
+ onClick={() => setTiroirOuvert(true)}
326
+ >
327
+ <BarresIcon />
328
+ </button>
329
+ ) : (
330
+ collapsible !== false && (
331
+ <button
332
+ type="button"
333
+ className="sia-shell__toggle"
334
+ aria-label={
335
+ etat === "expanded"
336
+ ? "Réduire la navigation"
337
+ : "Déployer la navigation"
338
+ }
339
+ aria-expanded={etat === "expanded"}
340
+ onClick={basculer}
341
+ >
342
+ <PanneauIcon />
343
+ </button>
344
+ )
345
+ )}
346
+
347
+ <div className="sia-shell__header-content">{header}</div>
348
+ </header>
349
+ )}
350
+
351
+ <main className={cn("sia-shell__content", `sia-shell__content--${contentWidth}`)}>
352
+ {page && <div className="sia-shell__page">{page}</div>}
353
+ {children}
354
+ </main>
355
+
356
+ {footer && <footer className="sia-shell__footer">{footer}</footer>}
357
+ </div>
358
+
359
+ {compact && montrerOnglets && (
360
+ <BottomTabs
361
+ items={plats}
362
+ {...(active !== undefined ? { activeKey: active } : {})}
363
+ {...(renderLink ? { renderLink } : {})}
364
+ onNavigate={naviguer}
365
+ {...(montrerTiroir ? { onMore: () => setTiroirOuvert(true) } : {})}
366
+ />
367
+ )}
368
+
369
+ {compact && montrerTiroir && (
370
+ <Drawer
371
+ open={tiroirOuvert}
372
+ onOpenChange={setTiroirOuvert}
373
+ position={side}
374
+ size="sm"
375
+ className="sia-shell__drawer"
376
+ >
377
+ {barre}
378
+ </Drawer>
379
+ )}
380
+ </div>
381
+ );
382
+ }
383
+
384
+ /** Trois barres. Dessinée ici pour ne pas tirer tout le jeu d'icônes. */
385
+ function BarresIcon() {
386
+ return (
387
+ <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
388
+ <path
389
+ d="M4 7h16M4 12h16M4 17h16"
390
+ fill="none"
391
+ stroke="currentColor"
392
+ strokeWidth="1.8"
393
+ strokeLinecap="round"
394
+ />
395
+ </svg>
396
+ );
397
+ }
398
+
399
+ /** Un panneau latéral, qui dit ce que le bouton replie. */
400
+ function PanneauIcon() {
401
+ return (
402
+ <svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
403
+ <rect
404
+ x="3"
405
+ y="4"
406
+ width="18"
407
+ height="16"
408
+ rx="2.5"
409
+ fill="none"
410
+ stroke="currentColor"
411
+ strokeWidth="1.8"
412
+ />
413
+ <path d="M9.5 4v16" stroke="currentColor" strokeWidth="1.8" />
414
+ </svg>
415
+ );
416
+ }