@sia-ui/cli 0.2.2 → 0.3.1
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/CHANGELOG.md +168 -0
- package/LICENSE +21 -21
- package/README.md +183 -3
- package/dist/index.js +0 -0
- package/dist/registry/package/components/Alert/index.tsx +7 -0
- package/dist/registry/package/components/AmountDisplay/index.tsx +8 -0
- package/dist/registry/package/components/AppShell/index.tsx +413 -3
- package/dist/registry/package/components/AppShell/styles.css +259 -7
- package/dist/registry/package/components/AppShell/tab-bar.tsx +134 -0
- package/dist/registry/package/components/AsyncState/index.tsx +7 -0
- package/dist/registry/package/components/AuditMeta/index.tsx +8 -0
- package/dist/registry/package/components/AuthLayout/index.tsx +7 -0
- package/dist/registry/package/components/Autocomplete/index.tsx +7 -0
- package/dist/registry/package/components/Avatar/index.tsx +333 -12
- package/dist/registry/package/components/Avatar/styles.css +236 -5
- package/dist/registry/package/components/Badge/index.tsx +7 -0
- package/dist/registry/package/components/Button/index.tsx +7 -0
- package/dist/registry/package/components/Calendar/index.tsx +11 -0
- package/dist/registry/package/components/Card/index.tsx +8 -0
- package/dist/registry/package/components/Checkbox/index.tsx +8 -0
- package/dist/registry/package/components/ColorPicker/index.tsx +7 -0
- package/dist/registry/package/components/CommandPalette/index.tsx +7 -0
- package/dist/registry/package/components/ConfirmDialog/index.tsx +7 -0
- package/dist/registry/package/components/Container/index.tsx +7 -0
- package/dist/registry/package/components/Countdown/index.tsx +7 -0
- package/dist/registry/package/components/CrudPage/dialogs.tsx +230 -0
- package/dist/registry/package/components/CrudPage/index.tsx +546 -5
- package/dist/registry/package/components/CrudPage/styles.css +25 -1
- package/dist/registry/package/components/CurrencyInput/index.tsx +7 -0
- package/dist/registry/package/components/DataTable/index.tsx +548 -200
- package/dist/registry/package/components/DataTable/query.ts +68 -0
- package/dist/registry/package/components/DataTable/row-actions.tsx +158 -0
- package/dist/registry/package/components/DataTable/styles.css +285 -73
- package/dist/registry/package/components/DataTable/types.ts +194 -0
- package/dist/registry/package/components/DataTable/values.ts +28 -0
- package/dist/registry/package/components/DatePicker/index.tsx +7 -0
- package/dist/registry/package/components/DateRangeFilter/index.tsx +7 -0
- package/dist/registry/package/components/DateRangePicker/index.tsx +7 -0
- package/dist/registry/package/components/DateTimePicker/index.tsx +7 -0
- package/dist/registry/package/components/Descriptions/index.tsx +7 -0
- package/dist/registry/package/components/Divider/index.tsx +7 -0
- package/dist/registry/package/components/Drawer/index.tsx +8 -0
- package/dist/registry/package/components/DurationDisplay/index.tsx +6 -0
- package/dist/registry/package/components/EmptyState/index.tsx +7 -0
- package/dist/registry/package/components/EntityMeta/index.tsx +7 -0
- package/dist/registry/package/components/ErrorState/index.tsx +8 -0
- package/dist/registry/package/components/EventCalendar/index.tsx +7 -0
- package/dist/registry/package/components/Field/index.tsx +12 -0
- package/dist/registry/package/components/FileUpload/index.tsx +7 -0
- package/dist/registry/package/components/FiltersBar/index.tsx +7 -0
- package/dist/registry/package/components/HoverCard/index.tsx +14 -37
- package/dist/registry/package/components/IconButton/index.tsx +8 -0
- package/dist/registry/package/components/Icons/index.tsx +15 -0
- package/dist/registry/package/components/ImageUpload/index.tsx +7 -0
- package/dist/registry/package/components/Input/index.tsx +7 -0
- package/dist/registry/package/components/JsonEditor/index.tsx +7 -0
- package/dist/registry/package/components/KpiGrid/index.tsx +7 -0
- package/dist/registry/package/components/Label/index.tsx +7 -0
- package/dist/registry/package/components/MarkdownEditor/index.tsx +7 -0
- package/dist/registry/package/components/MiniCalendar/index.tsx +7 -0
- package/dist/registry/package/components/Modal/index.tsx +8 -0
- package/dist/registry/package/components/MonthPicker/index.tsx +7 -0
- package/dist/registry/package/components/MultiSelect/index.tsx +7 -0
- package/dist/registry/package/components/OtpInput/index.tsx +7 -0
- package/dist/registry/package/components/Overlay/index.tsx +34 -24
- package/dist/registry/package/components/PageHeader/index.tsx +7 -0
- package/dist/registry/package/components/PermissionGate/index.tsx +6 -0
- package/dist/registry/package/components/Popover/index.tsx +7 -0
- package/dist/registry/package/components/QrCode/index.tsx +7 -0
- package/dist/registry/package/components/RadioGroup/index.tsx +7 -0
- package/dist/registry/package/components/Rating/index.tsx +6 -0
- package/dist/registry/package/components/ReferenceSelect/index.tsx +7 -0
- package/dist/registry/package/components/RelativeTime/index.tsx +6 -0
- package/dist/registry/package/components/ResizablePanel/index.tsx +7 -0
- package/dist/registry/package/components/Resource/index.tsx +514 -0
- package/dist/registry/package/components/RichTextEditor/index.tsx +7 -0
- package/dist/registry/package/components/SearchInput/index.tsx +8 -0
- package/dist/registry/package/components/Select/index.tsx +11 -0
- package/dist/registry/package/components/Sidebar/index.tsx +489 -113
- package/dist/registry/package/components/Sidebar/styles.css +96 -4
- package/dist/registry/package/components/Skeleton/index.tsx +7 -0
- package/dist/registry/package/components/Spinner/index.tsx +7 -0
- package/dist/registry/package/components/StatCard/index.tsx +7 -0
- package/dist/registry/package/components/Statistic/index.tsx +7 -0
- package/dist/registry/package/components/Switch/index.tsx +7 -0
- package/dist/registry/package/components/Tabs/index.tsx +7 -0
- package/dist/registry/package/components/TagsInput/index.tsx +7 -0
- package/dist/registry/package/components/Textarea/index.tsx +7 -0
- package/dist/registry/package/components/TimePicker/index.tsx +7 -0
- package/dist/registry/package/components/TimeRangePicker/index.tsx +6 -0
- package/dist/registry/package/components/Timeline/index.tsx +7 -0
- package/dist/registry/package/components/Timer/index.tsx +7 -0
- package/dist/registry/package/components/Tour/index.tsx +7 -0
- package/dist/registry/package/components/Transfer/index.tsx +7 -0
- package/dist/registry/package/components/Tree/index.tsx +7 -0
- package/dist/registry/package/components/TreeSelect/index.tsx +7 -0
- package/dist/registry/package/components/Typography/index.tsx +8 -0
- package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +8 -0
- package/dist/registry/package/components/WeekPicker/index.tsx +7 -0
- package/dist/registry/package/components/YearPicker/index.tsx +7 -0
- package/dist/registry/registry.json +69 -14
- package/package.json +18 -18
|
@@ -1,6 +1,416 @@
|
|
|
1
|
-
import {
|
|
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
|
|
6
|
-
|
|
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
|
+
}
|
|
@@ -1,7 +1,259 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
/**
|
|
2
|
+
* La coquille.
|
|
3
|
+
*
|
|
4
|
+
* Deux variables portent les largeurs — `--sia-shell-sidebar` et
|
|
5
|
+
* `--sia-shell-rail` — et l'appelant peut les redéfinir en ligne. La grille
|
|
6
|
+
* les lit sans jamais les coder en dur : changer la largeur ne demande donc
|
|
7
|
+
* pas de toucher au composant.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
.sia-shell {
|
|
11
|
+
--sia-shell-sidebar: 16rem;
|
|
12
|
+
--sia-shell-rail: 4rem;
|
|
13
|
+
--sia-shell-header: 3.5rem;
|
|
14
|
+
--sia-shell-tabs: 4rem;
|
|
15
|
+
|
|
16
|
+
display: grid;
|
|
17
|
+
grid-template-columns: var(--sia-shell-sidebar) minmax(0, 1fr);
|
|
18
|
+
min-height: 100%;
|
|
19
|
+
background: var(--sia-background);
|
|
20
|
+
color: var(--sia-foreground);
|
|
21
|
+
transition: grid-template-columns var(--sia-duration-base) var(--sia-easing-standard);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.sia-shell--rail { grid-template-columns: var(--sia-shell-rail) minmax(0, 1fr); }
|
|
25
|
+
.sia-shell--hidden,
|
|
26
|
+
.sia-shell--compact { grid-template-columns: minmax(0, 1fr); }
|
|
27
|
+
|
|
28
|
+
/* Posée à droite, la barre passe en seconde colonne : l'ordre du DOM ne
|
|
29
|
+
change pas, donc l'ordre de lecture au clavier non plus. */
|
|
30
|
+
.sia-shell--right { grid-template-columns: minmax(0, 1fr) var(--sia-shell-sidebar); }
|
|
31
|
+
.sia-shell--right.sia-shell--rail { grid-template-columns: minmax(0, 1fr) var(--sia-shell-rail); }
|
|
32
|
+
.sia-shell--right .sia-shell__aside { order: 2; }
|
|
33
|
+
|
|
34
|
+
.sia-shell__aside {
|
|
35
|
+
min-width: 0;
|
|
36
|
+
height: 100vh;
|
|
37
|
+
/* Collée : le contenu défile, la navigation reste. Sans cela, revenir en
|
|
38
|
+
haut d'une longue liste devient un préalable à tout déplacement. */
|
|
39
|
+
position: sticky;
|
|
40
|
+
top: 0;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
border-right: 1px solid var(--sia-border);
|
|
43
|
+
background: var(--sia-surface);
|
|
44
|
+
}
|
|
45
|
+
.sia-shell--right .sia-shell__aside {
|
|
46
|
+
border-right: 0;
|
|
47
|
+
border-left: 1px solid var(--sia-border);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.sia-shell--filled .sia-shell__aside { background: var(--sia-surface-raised, var(--sia-surface)); }
|
|
51
|
+
|
|
52
|
+
/* La barre flottante se détache de son bord : la coquille lui rend la marge
|
|
53
|
+
que le composant seul ne peut pas prendre. */
|
|
54
|
+
.sia-shell--floating .sia-shell__aside {
|
|
55
|
+
border: 0;
|
|
56
|
+
background: transparent;
|
|
57
|
+
padding: .5rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.sia-shell__main {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
min-width: 0;
|
|
64
|
+
min-height: 100vh;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ── L'en-tête ──────────────────────────────────────────────────────────── */
|
|
68
|
+
|
|
69
|
+
.sia-shell__header {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: .75rem;
|
|
73
|
+
flex: none;
|
|
74
|
+
min-height: var(--sia-shell-header);
|
|
75
|
+
padding: .5rem 1rem;
|
|
76
|
+
border-bottom: 1px solid var(--sia-border);
|
|
77
|
+
background: color-mix(in srgb, var(--sia-background) 88%, transparent);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Le titre d'écran et l'identité servent de repère pendant le défilement.
|
|
81
|
+
`backdrop-filter` n'est posé qu'ici, où il couvre une bande étroite : sur
|
|
82
|
+
une grande surface il coûte une couche de composition à chaque image. */
|
|
83
|
+
.sia-shell__header--sticky {
|
|
84
|
+
position: sticky;
|
|
85
|
+
top: 0;
|
|
86
|
+
z-index: 20;
|
|
87
|
+
backdrop-filter: blur(8px);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.sia-shell__header-content {
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: .75rem;
|
|
94
|
+
flex: 1;
|
|
95
|
+
min-width: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.sia-shell__toggle {
|
|
99
|
+
display: grid;
|
|
100
|
+
place-items: center;
|
|
101
|
+
flex: none;
|
|
102
|
+
width: 2.25rem;
|
|
103
|
+
height: 2.25rem;
|
|
104
|
+
border: 0;
|
|
105
|
+
border-radius: var(--sia-radius);
|
|
106
|
+
background: transparent;
|
|
107
|
+
color: var(--sia-muted);
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
transition:
|
|
110
|
+
background-color var(--sia-duration-fast) var(--sia-easing-standard),
|
|
111
|
+
color var(--sia-duration-fast) var(--sia-easing-standard);
|
|
112
|
+
}
|
|
113
|
+
.sia-shell__toggle:hover { background: var(--sia-surface); color: var(--sia-foreground); }
|
|
114
|
+
.sia-shell__toggle:focus-visible {
|
|
115
|
+
outline: 2px solid var(--sia-primary);
|
|
116
|
+
outline-offset: 2px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* ── Le contenu ─────────────────────────────────────────────────────────── */
|
|
120
|
+
|
|
121
|
+
.sia-shell__content {
|
|
122
|
+
flex: 1;
|
|
123
|
+
min-width: 0;
|
|
124
|
+
width: 100%;
|
|
125
|
+
margin-inline: auto;
|
|
126
|
+
padding: 1.25rem 1rem 2rem;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Assez large pour deux colonnes de cartes, assez borné pour qu'une ligne de
|
|
130
|
+
texte reste lisible. */
|
|
131
|
+
.sia-shell__content--wide { max-width: 87.5rem; }
|
|
132
|
+
/* Pour un écran qui n'est qu'un formulaire. */
|
|
133
|
+
.sia-shell__content--narrow { max-width: 48rem; }
|
|
134
|
+
/* Pour un tableau qui a besoin de toute la place. */
|
|
135
|
+
.sia-shell__content--full { max-width: none; }
|
|
136
|
+
|
|
137
|
+
.sia-shell__page { margin-bottom: 1.25rem; }
|
|
138
|
+
|
|
139
|
+
.sia-shell__footer {
|
|
140
|
+
flex: none;
|
|
141
|
+
padding: 1rem;
|
|
142
|
+
border-top: 1px solid var(--sia-border);
|
|
143
|
+
color: var(--sia-muted);
|
|
144
|
+
font-size: .85rem;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ── Petit écran ────────────────────────────────────────────────────────── */
|
|
148
|
+
|
|
149
|
+
.sia-shell--compact .sia-shell__content { padding-inline: .875rem; }
|
|
150
|
+
|
|
151
|
+
/* La barre d'onglets couvre le bas : sans cette réserve, la dernière ligne
|
|
152
|
+
d'un tableau reste inatteignable. */
|
|
153
|
+
.sia-shell--with-tabs .sia-shell__main {
|
|
154
|
+
padding-bottom: calc(var(--sia-shell-tabs) + env(safe-area-inset-bottom, 0px));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.sia-shell__drawer .sia-sidebar { height: 100%; }
|
|
158
|
+
|
|
159
|
+
/* ── La barre d'onglets ─────────────────────────────────────────────────── */
|
|
160
|
+
|
|
161
|
+
/* `sia-bottom-tabs` et non `sia-tabs` : ce dernier appartient au composant
|
|
162
|
+
`Tabs`, dont la première règle est `display: grid`. La collision jouait
|
|
163
|
+
dans les deux sens — mes onglets héritaient de sa grille, et mes règles
|
|
164
|
+
fixaient ses onglets en bas de l'écran. */
|
|
165
|
+
|
|
166
|
+
.sia-bottom-tabs {
|
|
167
|
+
position: fixed;
|
|
168
|
+
right: 0;
|
|
169
|
+
bottom: 0;
|
|
170
|
+
left: 0;
|
|
171
|
+
z-index: 30;
|
|
172
|
+
display: flex;
|
|
173
|
+
align-items: stretch;
|
|
174
|
+
height: var(--sia-shell-tabs);
|
|
175
|
+
/* La barre d'accueil des téléphones sans bouton mange le bas de l'écran. */
|
|
176
|
+
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
177
|
+
border-top: 1px solid var(--sia-border);
|
|
178
|
+
background: color-mix(in srgb, var(--sia-background) 92%, transparent);
|
|
179
|
+
backdrop-filter: blur(12px);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.sia-bottom-tabs__cell { display: contents; }
|
|
183
|
+
|
|
184
|
+
.sia-bottom-tabs__item {
|
|
185
|
+
display: flex;
|
|
186
|
+
flex: 1;
|
|
187
|
+
flex-direction: column;
|
|
188
|
+
align-items: center;
|
|
189
|
+
justify-content: center;
|
|
190
|
+
gap: .15rem;
|
|
191
|
+
min-width: 0;
|
|
192
|
+
padding: .25rem .25rem .3rem;
|
|
193
|
+
border: 0;
|
|
194
|
+
background: transparent;
|
|
195
|
+
color: var(--sia-muted);
|
|
196
|
+
font: inherit;
|
|
197
|
+
font-size: .68rem;
|
|
198
|
+
text-decoration: none;
|
|
199
|
+
cursor: pointer;
|
|
200
|
+
transition: color var(--sia-duration-fast) var(--sia-easing-standard);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* L'onglet actif reçoit une pastille qui grandit sous l'icône.
|
|
205
|
+
*
|
|
206
|
+
* Une simple bascule de couleur passe inaperçue sur un téléphone tenu à bout
|
|
207
|
+
* de bras ; un changement de forme, non.
|
|
208
|
+
*/
|
|
209
|
+
.sia-bottom-tabs__pastille {
|
|
210
|
+
position: relative;
|
|
211
|
+
display: grid;
|
|
212
|
+
place-items: center;
|
|
213
|
+
width: 3rem;
|
|
214
|
+
height: 1.75rem;
|
|
215
|
+
border-radius: 999px;
|
|
216
|
+
background: transparent;
|
|
217
|
+
font-size: 1.1rem;
|
|
218
|
+
transform: scale(.9);
|
|
219
|
+
transition:
|
|
220
|
+
background-color var(--sia-duration-base) var(--sia-easing-spring),
|
|
221
|
+
transform var(--sia-duration-base) var(--sia-easing-spring);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.sia-bottom-tabs__item--active { color: var(--sia-primary); }
|
|
225
|
+
.sia-bottom-tabs__item--active .sia-bottom-tabs__pastille {
|
|
226
|
+
background: color-mix(in srgb, var(--sia-primary) 14%, transparent);
|
|
227
|
+
transform: scale(1);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.sia-bottom-tabs__label {
|
|
231
|
+
max-width: 100%;
|
|
232
|
+
overflow: hidden;
|
|
233
|
+
text-overflow: ellipsis;
|
|
234
|
+
white-space: nowrap;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Le compte exact ne tient pas sous une icône d'onglet : il devient un point,
|
|
238
|
+
comme sur le rail. */
|
|
239
|
+
.sia-bottom-tabs__point {
|
|
240
|
+
position: absolute;
|
|
241
|
+
top: .05rem;
|
|
242
|
+
right: .65rem;
|
|
243
|
+
width: .4rem;
|
|
244
|
+
height: .4rem;
|
|
245
|
+
border-radius: 50%;
|
|
246
|
+
background: var(--sia-danger);
|
|
247
|
+
box-shadow: 0 0 0 2px var(--sia-background);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.sia-bottom-tabs__item:focus-visible {
|
|
251
|
+
outline: 2px solid var(--sia-primary);
|
|
252
|
+
outline-offset: -3px;
|
|
253
|
+
border-radius: var(--sia-radius);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@media (prefers-reduced-motion: reduce) {
|
|
257
|
+
.sia-shell,
|
|
258
|
+
.sia-bottom-tabs__pastille { transition: none; }
|
|
259
|
+
}
|