@sia-ui/cli 0.2.2 → 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.
- package/LICENSE +21 -21
- 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 +14 -14
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { memo, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "@sia-ui/utils";
|
|
3
|
+
import { navLeaves, navSome } from "@sia-ui/headless";
|
|
4
|
+
import type { SidebarItem, SidebarRenderLink } from "../Sidebar";
|
|
5
|
+
|
|
6
|
+
export interface BottomTabsProps {
|
|
7
|
+
/** Attendu déjà filtré : la coquille a fait le tri une seule fois. */
|
|
8
|
+
items: SidebarItem[];
|
|
9
|
+
activeKey?: string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Nombre d'onglets avant le bouton « Plus ».
|
|
13
|
+
*
|
|
14
|
+
* Quatre au maximum sur un téléphone : au-delà, les cibles tactiles
|
|
15
|
+
* descendent sous les quarante-quatre pixels recommandés, et on rate
|
|
16
|
+
* l'onglet voisin une fois sur trois.
|
|
17
|
+
*/
|
|
18
|
+
max?: number;
|
|
19
|
+
|
|
20
|
+
renderLink?: SidebarRenderLink;
|
|
21
|
+
onNavigate?: (item: SidebarItem) => void;
|
|
22
|
+
|
|
23
|
+
/** Le bouton qui ouvre l'arborescence complète. */
|
|
24
|
+
more?: ReactNode;
|
|
25
|
+
onMore?: () => void;
|
|
26
|
+
moreLabel?: string;
|
|
27
|
+
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* La navigation d'un téléphone.
|
|
33
|
+
*
|
|
34
|
+
* Un rail d'icônes muettes n'est pas utilisable au comptoir : sur petit écran
|
|
35
|
+
* la navigation descend en bas, à portée du pouce, avec des libellés.
|
|
36
|
+
*
|
|
37
|
+
* Les onglets ne montrent que des **feuilles**. Un onglet qui ouvrirait un
|
|
38
|
+
* sous-menu depuis une barre basse serait un piège tactile — on vise une
|
|
39
|
+
* destination, on obtient un menu. Le reste de l'arborescence passe par
|
|
40
|
+
* « Plus ».
|
|
41
|
+
*/
|
|
42
|
+
export const BottomTabs = memo(function BottomTabs({
|
|
43
|
+
items,
|
|
44
|
+
activeKey,
|
|
45
|
+
max = 4,
|
|
46
|
+
renderLink,
|
|
47
|
+
onNavigate,
|
|
48
|
+
more,
|
|
49
|
+
onMore,
|
|
50
|
+
moreLabel = "Plus",
|
|
51
|
+
className,
|
|
52
|
+
}: BottomTabsProps) {
|
|
53
|
+
const feuilles = navLeaves(items);
|
|
54
|
+
const onglets = feuilles.slice(0, max);
|
|
55
|
+
const reste = feuilles.length - onglets.length;
|
|
56
|
+
const afficherPlus = reste > 0 || Boolean(onMore);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<nav className={cn("sia-bottom-tabs", className)} aria-label="Navigation principale">
|
|
60
|
+
{onglets.map((item) => {
|
|
61
|
+
const actif = item.key === activeKey;
|
|
62
|
+
const contenu = (
|
|
63
|
+
<>
|
|
64
|
+
<span className="sia-bottom-tabs__pastille" aria-hidden="true">
|
|
65
|
+
{item.icon}
|
|
66
|
+
{navSome(item, (e) => e.badge !== undefined && e.badge !== null) && (
|
|
67
|
+
<span className="sia-bottom-tabs__point" />
|
|
68
|
+
)}
|
|
69
|
+
</span>
|
|
70
|
+
<span className="sia-bottom-tabs__label">{item.label}</span>
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const classe = cn("sia-bottom-tabs__item", actif && "sia-bottom-tabs__item--active");
|
|
75
|
+
const naviguer = () => {
|
|
76
|
+
item.onClick?.();
|
|
77
|
+
onNavigate?.(item);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (item.href && renderLink) {
|
|
81
|
+
return (
|
|
82
|
+
<span key={item.key} className="sia-bottom-tabs__cell">
|
|
83
|
+
{renderLink({
|
|
84
|
+
item,
|
|
85
|
+
children: contenu,
|
|
86
|
+
props: {
|
|
87
|
+
className: classe,
|
|
88
|
+
onClick: naviguer,
|
|
89
|
+
...(actif ? { "aria-current": "page" as const } : {}),
|
|
90
|
+
},
|
|
91
|
+
})}
|
|
92
|
+
</span>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return item.href ? (
|
|
97
|
+
<a
|
|
98
|
+
key={item.key}
|
|
99
|
+
href={item.href}
|
|
100
|
+
className={classe}
|
|
101
|
+
{...(actif ? { "aria-current": "page" as const } : {})}
|
|
102
|
+
onClick={naviguer}
|
|
103
|
+
>
|
|
104
|
+
{contenu}
|
|
105
|
+
</a>
|
|
106
|
+
) : (
|
|
107
|
+
<button
|
|
108
|
+
key={item.key}
|
|
109
|
+
type="button"
|
|
110
|
+
className={classe}
|
|
111
|
+
{...(actif ? { "aria-current": "page" as const } : {})}
|
|
112
|
+
onClick={naviguer}
|
|
113
|
+
>
|
|
114
|
+
{contenu}
|
|
115
|
+
</button>
|
|
116
|
+
);
|
|
117
|
+
})}
|
|
118
|
+
|
|
119
|
+
{afficherPlus && (
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
className="sia-bottom-tabs__item"
|
|
123
|
+
onClick={onMore}
|
|
124
|
+
aria-label={`${moreLabel}${reste > 0 ? ` (${reste})` : ""}`}
|
|
125
|
+
>
|
|
126
|
+
<span className="sia-bottom-tabs__pastille" aria-hidden="true">
|
|
127
|
+
{more ?? "···"}
|
|
128
|
+
</span>
|
|
129
|
+
<span className="sia-bottom-tabs__label">{moreLabel}</span>
|
|
130
|
+
</button>
|
|
131
|
+
)}
|
|
132
|
+
</nav>
|
|
133
|
+
);
|
|
134
|
+
});
|
|
@@ -14,6 +14,13 @@ export interface AsyncStateProps {
|
|
|
14
14
|
children: ReactNode;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Les trois états d'un chargement, au même endroit.
|
|
19
|
+
*
|
|
20
|
+
* En attente, en erreur, ou vide : ce sont les mêmes trois cas à chaque
|
|
21
|
+
* écran, et les écrire à la main en oublie toujours un — le plus souvent le
|
|
22
|
+
* vide, qui ressemble à un chargement qui n'en finit pas.
|
|
23
|
+
*/
|
|
17
24
|
export function AsyncState({ loading, error, empty, emptyTitle = "No data", emptyDescription, onRetry, children }: AsyncStateProps) {
|
|
18
25
|
if (loading) return <div className="sia-async-state" role="status"><span className="sia-spinner" /><span>Loading...</span></div>;
|
|
19
26
|
if (error) return <Alert tone="danger" title="Unable to load data" action={onRetry && <Button variant="outline" onClick={onRetry}>Retry</Button>}>{error}</Alert>;
|
|
@@ -17,6 +17,14 @@ export interface AuditMetaProps {
|
|
|
17
17
|
className?: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Qui a fait quoi, et quand.
|
|
22
|
+
*
|
|
23
|
+
* Une liste d'événements plutôt qu'un dernier état : savoir qu'une facture
|
|
24
|
+
* a été validée n'apprend rien si l'on ignore qu'elle avait été rejetée la
|
|
25
|
+
* veille. Les dates sont relatives — « il y a deux jours » se compare sans
|
|
26
|
+
* calcul mental.
|
|
27
|
+
*/
|
|
20
28
|
export function AuditMeta({ events, locale = "fr-FR", title = "Historique", emptyLabel = "Aucune activité", className }: AuditMetaProps) {
|
|
21
29
|
return (
|
|
22
30
|
<section className={cn("sia-audit-meta", className)}>
|
|
@@ -11,6 +11,13 @@ export interface AuthLayoutProps {
|
|
|
11
11
|
footer?: ReactNode;
|
|
12
12
|
className?: string;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* La page qui précède l'application : connexion, inscription, mot de passe
|
|
16
|
+
* oublié.
|
|
17
|
+
*
|
|
18
|
+
* Elle n'a ni navigation ni coquille, et c'est délibéré : tout ce qui mène
|
|
19
|
+
* ailleurs est hors de portée de quelqu'un qui n'est pas encore entré.
|
|
20
|
+
*/
|
|
14
21
|
export function AuthLayout({
|
|
15
22
|
title,
|
|
16
23
|
description,
|
|
@@ -3,6 +3,13 @@ import { cn } from "@sia-ui/utils";
|
|
|
3
3
|
import type { SelectOption } from "../Select";
|
|
4
4
|
import { useSiaLocale } from "@sia-ui/headless";
|
|
5
5
|
export interface AutocompleteProps { id?: string; options: SelectOption[]; value?: string; defaultValue?: string; onValueChange?: (value: string, option?: SelectOption) => void; placeholder?: string; allowCustomValue?: boolean; disabled?: boolean; className?: string; "aria-describedby"?: string; "aria-labelledby"?: string; "aria-invalid"?: boolean; "aria-required"?: boolean; }
|
|
6
|
+
/**
|
|
7
|
+
* Un champ qui propose, sans imposer.
|
|
8
|
+
*
|
|
9
|
+
* `allowCustomValue` est vrai par défaut : la liste aide à trouver, elle ne
|
|
10
|
+
* dit pas ce qui existe. Un nom de client absent du référentiel reste
|
|
11
|
+
* saisissable, quitte à être créé ensuite.
|
|
12
|
+
*/
|
|
6
13
|
export function Autocomplete({ id, options, value, defaultValue = "", onValueChange, placeholder: placeholderProp, allowCustomValue = true, disabled, className, ...ariaProps }: AutocompleteProps) {
|
|
7
14
|
const locale = useSiaLocale();
|
|
8
15
|
const placeholder = placeholderProp ?? locale.searchPlaceholder; const initialLabel = options.find((option) => option.value === defaultValue)?.label; const [internal, setInternal] = useState(defaultValue); const [query, setQuery] = useState(initialLabel ? String(initialLabel) : defaultValue); const [open, setOpen] = useState(false); const current = value ?? internal; const filtered = useMemo(() => { const needle = query.toLowerCase(); return options.filter((option) => [String(option.label), ...(option.keywords ?? [])].join(" ").toLowerCase().includes(needle)); }, [options, query]); const select = (option: SelectOption) => { if (value === undefined) setInternal(option.value); setQuery(String(option.label)); setOpen(false); onValueChange?.(option.value, option); }; return <div id={id} className={cn("sia-autocomplete", className)} {...ariaProps}><input value={query} placeholder={placeholder} disabled={disabled} role="combobox" aria-expanded={open} aria-controls={`${id ?? "sia-autocomplete"}-list`} onFocus={() => setOpen(true)} onChange={(event) => { const next = event.target.value; setQuery(next); setOpen(true); if (allowCustomValue) { if (value === undefined) setInternal(next); onValueChange?.(next); } }} onBlur={() => window.setTimeout(() => setOpen(false), 120)} />{open && <div id={`${id ?? "sia-autocomplete"}-list`} role="listbox">{filtered.map((option) => <button type="button" role="option" aria-selected={current === option.value} key={option.value} disabled={option.disabled} onMouseDown={(event) => event.preventDefault()} onClick={() => select(option)}>{option.label}</button>)}</div>}</div>; }
|