@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.
- package/LICENSE +21 -21
- package/dist/index.js +2 -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,23 +1,344 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useMemo,
|
|
5
|
+
useState,
|
|
6
|
+
type HTMLAttributes,
|
|
7
|
+
type ImgHTMLAttributes,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
2
10
|
import { cn } from "@sia-ui/utils";
|
|
3
11
|
import "./styles.css";
|
|
4
12
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
export type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl";
|
|
14
|
+
export type AvatarShape = "circle" | "rounded" | "square";
|
|
15
|
+
export type AvatarVariant = "soft" | "solid" | "outline";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* La présence d'une personne.
|
|
19
|
+
*
|
|
20
|
+
* Quatre états et pas davantage : au-delà, la pastille devient un code que
|
|
21
|
+
* personne ne retient, et il faut une légende pour la lire.
|
|
22
|
+
*/
|
|
23
|
+
export type AvatarStatus = "online" | "offline" | "busy" | "away";
|
|
24
|
+
|
|
25
|
+
const LIBELLES_STATUT: Record<AvatarStatus, string> = {
|
|
26
|
+
online: "en ligne",
|
|
27
|
+
offline: "hors ligne",
|
|
28
|
+
busy: "occupé",
|
|
29
|
+
away: "absent",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Les teintes dérivées du nom.
|
|
34
|
+
*
|
|
35
|
+
* Deux personnes aux mêmes initiales doivent se distinguer : dans une liste
|
|
36
|
+
* de participants, six pastilles identiques ne valent pas mieux qu'aucune.
|
|
37
|
+
* La couleur est donc déterministe — la même personne garde la sienne d'un
|
|
38
|
+
* écran à l'autre, d'une session à l'autre.
|
|
39
|
+
*/
|
|
40
|
+
const TEINTES = [
|
|
41
|
+
"primary",
|
|
42
|
+
"info",
|
|
43
|
+
"success",
|
|
44
|
+
"warning",
|
|
45
|
+
"danger",
|
|
46
|
+
"secondary",
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
export type AvatarTint = (typeof TEINTES)[number];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Une empreinte stable, calculée sur le nom.
|
|
53
|
+
*
|
|
54
|
+
* Volontairement minuscule : cette fonction tourne pour chaque avatar d'une
|
|
55
|
+
* liste qui peut en compter des centaines. Un hachage cryptographique y
|
|
56
|
+
* coûterait mille fois plus pour un résultat visuellement identique.
|
|
57
|
+
*/
|
|
58
|
+
function empreinte(texte: string): number {
|
|
59
|
+
let h = 0;
|
|
60
|
+
for (let i = 0; i < texte.length; i += 1) {
|
|
61
|
+
h = (h * 31 + texte.charCodeAt(i)) | 0;
|
|
62
|
+
}
|
|
63
|
+
return Math.abs(h);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function tintForName(name: string): AvatarTint {
|
|
67
|
+
return TEINTES[empreinte(name) % TEINTES.length]!;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Les initiales d'un nom.
|
|
72
|
+
*
|
|
73
|
+
* Première et dernière partie plutôt que les deux premières : « Jean-Baptiste
|
|
74
|
+
* Kouassi Mbella » se lit « JM », pas « JK ». Les particules sont écartées,
|
|
75
|
+
* sinon « Marie de la Tour » donnerait « MD ».
|
|
76
|
+
*/
|
|
77
|
+
const PARTICULES = new Set([
|
|
78
|
+
"de", "du", "des", "la", "le", "les", "van", "von", "da", "di", "el", "al",
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
export function initialsOf(name: string, count = 2): string {
|
|
82
|
+
const parts = name
|
|
83
|
+
.trim()
|
|
84
|
+
.split(/[\s'-]+/)
|
|
85
|
+
.filter((p) => p.length > 0 && !PARTICULES.has(p.toLowerCase()));
|
|
86
|
+
|
|
87
|
+
if (parts.length === 0) return "";
|
|
88
|
+
if (parts.length === 1 || count === 1) {
|
|
89
|
+
return (parts[0]!.slice(0, count) || "").toUpperCase();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
(parts[0]![0] ?? "") + (parts[parts.length - 1]![0] ?? "")
|
|
94
|
+
).toUpperCase();
|
|
9
95
|
}
|
|
10
96
|
|
|
11
|
-
|
|
12
|
-
|
|
97
|
+
interface AvatarGroupContextValue {
|
|
98
|
+
size: AvatarSize;
|
|
99
|
+
shape: AvatarShape;
|
|
100
|
+
variant: AvatarVariant;
|
|
13
101
|
}
|
|
14
102
|
|
|
15
|
-
|
|
16
|
-
|
|
103
|
+
const AvatarGroupContext = createContext<AvatarGroupContextValue | null>(null);
|
|
104
|
+
|
|
105
|
+
export interface AvatarProps
|
|
106
|
+
extends Omit<ImgHTMLAttributes<HTMLImageElement>, "children" | "width" | "height"> {
|
|
107
|
+
/** Le nom de la personne : il sert au repli, à la couleur et à l'annonce. */
|
|
108
|
+
name: string;
|
|
109
|
+
|
|
110
|
+
size?: AvatarSize;
|
|
111
|
+
shape?: AvatarShape;
|
|
112
|
+
variant?: AvatarVariant;
|
|
113
|
+
|
|
114
|
+
/** Force la teinte au lieu de la dériver du nom. */
|
|
115
|
+
tint?: AvatarTint;
|
|
116
|
+
|
|
117
|
+
/** Remplace les initiales — une icône, un emoji, deux lettres à soi. */
|
|
118
|
+
fallback?: ReactNode;
|
|
119
|
+
|
|
120
|
+
/** Nombre d'initiales quand le repli est automatique. */
|
|
121
|
+
initials?: 1 | 2;
|
|
122
|
+
|
|
123
|
+
status?: AvatarStatus;
|
|
124
|
+
/** Le libellé du statut, pour les lecteurs d'écran. */
|
|
125
|
+
statusLabel?: string;
|
|
126
|
+
|
|
127
|
+
/** Un liseré qui détache l'avatar de son fond. */
|
|
128
|
+
ring?: boolean;
|
|
129
|
+
|
|
130
|
+
/** Rend l'avatar actionnable — un menu de compte, par exemple. */
|
|
131
|
+
onPress?: () => void;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Le portrait d'une personne, ou ce qui le remplace.
|
|
136
|
+
*
|
|
137
|
+
* Une image met du temps à arriver, échoue parfois, et manque souvent. Les
|
|
138
|
+
* trois cas sont traités sans que la mise en page bouge : l'espace est réservé
|
|
139
|
+
* d'emblée, le repli est peint dessous, et l'image se fond par-dessus quand
|
|
140
|
+
* elle arrive.
|
|
141
|
+
*/
|
|
142
|
+
export function Avatar({
|
|
143
|
+
name,
|
|
144
|
+
size,
|
|
145
|
+
shape,
|
|
146
|
+
variant,
|
|
147
|
+
tint,
|
|
148
|
+
fallback,
|
|
149
|
+
initials = 2,
|
|
150
|
+
status,
|
|
151
|
+
statusLabel,
|
|
152
|
+
ring = false,
|
|
153
|
+
onPress,
|
|
154
|
+
className,
|
|
155
|
+
src,
|
|
156
|
+
alt,
|
|
157
|
+
onError,
|
|
158
|
+
onLoad,
|
|
159
|
+
style,
|
|
160
|
+
...props
|
|
161
|
+
}: AvatarProps) {
|
|
162
|
+
const groupe = useContext(AvatarGroupContext);
|
|
163
|
+
const [etatImage, setEtatImage] = useState<"attente" | "prete" | "echouee">(
|
|
164
|
+
src ? "attente" : "echouee",
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const resolvedSize = size ?? groupe?.size ?? "md";
|
|
168
|
+
const resolvedShape = shape ?? groupe?.shape ?? "circle";
|
|
169
|
+
const resolvedVariant = variant ?? groupe?.variant ?? "soft";
|
|
170
|
+
|
|
171
|
+
// Le nom ne change pas à chaque rendu, le hachage non plus : dans une liste
|
|
172
|
+
// de cent participants, c'est cent calculs évités par frappe au clavier.
|
|
173
|
+
const teinte = useMemo(() => tint ?? tintForName(name), [name, tint]);
|
|
174
|
+
const lettres = useMemo(
|
|
175
|
+
() => (fallback === undefined ? initialsOf(name, initials) : null),
|
|
176
|
+
[fallback, initials, name],
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const classe = cn(
|
|
180
|
+
"sia-avatar",
|
|
181
|
+
`sia-avatar--${resolvedSize}`,
|
|
182
|
+
`sia-avatar--${resolvedShape}`,
|
|
183
|
+
`sia-avatar--${resolvedVariant}`,
|
|
184
|
+
`sia-avatar--tint-${teinte}`,
|
|
185
|
+
ring && "sia-avatar--ring",
|
|
186
|
+
etatImage === "attente" && "sia-avatar--loading",
|
|
187
|
+
onPress && "sia-avatar--pressable",
|
|
188
|
+
className,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const contenu = (
|
|
192
|
+
<>
|
|
193
|
+
{/* Le repli est toujours peint, sous l'image. C'est lui qui tient la
|
|
194
|
+
place pendant le chargement, et qui reste si l'image échoue —
|
|
195
|
+
aucune bascule, donc aucun saut. */}
|
|
196
|
+
<span className="sia-avatar__fallback" aria-hidden="true">
|
|
197
|
+
{fallback ?? lettres}
|
|
198
|
+
</span>
|
|
199
|
+
|
|
200
|
+
{src && etatImage !== "echouee" && (
|
|
201
|
+
<img
|
|
202
|
+
{...props}
|
|
203
|
+
className="sia-avatar__image"
|
|
204
|
+
src={src}
|
|
205
|
+
alt={alt ?? ""}
|
|
206
|
+
loading="lazy"
|
|
207
|
+
decoding="async"
|
|
208
|
+
onLoad={(event) => {
|
|
209
|
+
setEtatImage("prete");
|
|
210
|
+
onLoad?.(event);
|
|
211
|
+
}}
|
|
212
|
+
onError={(event) => {
|
|
213
|
+
setEtatImage("echouee");
|
|
214
|
+
onError?.(event);
|
|
215
|
+
}}
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
{status && (
|
|
220
|
+
<span
|
|
221
|
+
className={cn("sia-avatar__status", `sia-avatar__status--${status}`)}
|
|
222
|
+
aria-hidden="true"
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
{/* Le nom n'est jamais dans l'image : un `alt` vide et un texte masqué
|
|
227
|
+
annoncent la personne une seule fois, statut compris. */}
|
|
228
|
+
<span className="sia-visually-hidden">
|
|
229
|
+
{name}
|
|
230
|
+
{status ? ` — ${statusLabel ?? LIBELLES_STATUT[status]}` : ""}
|
|
231
|
+
</span>
|
|
232
|
+
</>
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
if (onPress) {
|
|
236
|
+
return (
|
|
237
|
+
<button
|
|
238
|
+
type="button"
|
|
239
|
+
className={classe}
|
|
240
|
+
title={name}
|
|
241
|
+
onClick={onPress}
|
|
242
|
+
{...(style ? { style } : {})}
|
|
243
|
+
>
|
|
244
|
+
{contenu}
|
|
245
|
+
</button>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
17
249
|
return (
|
|
18
|
-
<span className={
|
|
19
|
-
{
|
|
20
|
-
{!src || failed ? <span className="sia-visually-hidden">{name}</span> : null}
|
|
250
|
+
<span className={classe} title={name} {...(style ? { style } : {})}>
|
|
251
|
+
{contenu}
|
|
21
252
|
</span>
|
|
22
253
|
);
|
|
23
254
|
}
|
|
255
|
+
|
|
256
|
+
export interface AvatarGroupProps extends HTMLAttributes<HTMLDivElement> {
|
|
257
|
+
children: ReactNode;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Au-delà, les suivants sont résumés par un compteur.
|
|
261
|
+
*
|
|
262
|
+
* Sans limite, une réunion de quarante personnes déborde de sa ligne et
|
|
263
|
+
* n'apprend plus rien : au-delà de cinq visages, on ne lit plus des gens,
|
|
264
|
+
* on lit une foule.
|
|
265
|
+
*/
|
|
266
|
+
max?: number;
|
|
267
|
+
|
|
268
|
+
size?: AvatarSize;
|
|
269
|
+
shape?: AvatarShape;
|
|
270
|
+
variant?: AvatarVariant;
|
|
271
|
+
|
|
272
|
+
/** Le chevauchement, de 0 (côte à côte) à 1 (fortement empilés). */
|
|
273
|
+
overlap?: number;
|
|
274
|
+
|
|
275
|
+
/** Écarte les avatars au survol, pour les distinguer. */
|
|
276
|
+
spreadOnHover?: boolean;
|
|
277
|
+
|
|
278
|
+
/** Ce qu'affiche le compteur. Par défaut `+N`. */
|
|
279
|
+
renderOverflow?: (count: number) => ReactNode;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Plusieurs personnes, sur une ligne.
|
|
284
|
+
*
|
|
285
|
+
* Les avatars se chevauchent — c'est ce qui dit « un groupe » plutôt qu'« une
|
|
286
|
+
* liste ». Chacun porte un liseré de la couleur du fond, sans quoi la pile
|
|
287
|
+
* devient une bouillie dès que deux teintes se ressemblent.
|
|
288
|
+
*/
|
|
289
|
+
export function AvatarGroup({
|
|
290
|
+
children,
|
|
291
|
+
max,
|
|
292
|
+
size = "md",
|
|
293
|
+
shape = "circle",
|
|
294
|
+
variant = "soft",
|
|
295
|
+
overlap = 0.25,
|
|
296
|
+
spreadOnHover = false,
|
|
297
|
+
renderOverflow,
|
|
298
|
+
className,
|
|
299
|
+
style,
|
|
300
|
+
...props
|
|
301
|
+
}: AvatarGroupProps) {
|
|
302
|
+
const tous = Array.isArray(children) ? children : [children];
|
|
303
|
+
const visibles = max === undefined ? tous : tous.slice(0, max);
|
|
304
|
+
const reste = tous.length - visibles.length;
|
|
305
|
+
|
|
306
|
+
const contexte = useMemo(
|
|
307
|
+
() => ({ size, shape, variant }),
|
|
308
|
+
[shape, size, variant],
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<AvatarGroupContext.Provider value={contexte}>
|
|
313
|
+
<div
|
|
314
|
+
className={cn(
|
|
315
|
+
"sia-avatar-group",
|
|
316
|
+
`sia-avatar-group--${size}`,
|
|
317
|
+
spreadOnHover && "sia-avatar-group--spread",
|
|
318
|
+
className,
|
|
319
|
+
)}
|
|
320
|
+
style={{
|
|
321
|
+
...style,
|
|
322
|
+
"--sia-avatar-overlap": String(Math.min(Math.max(overlap, 0), 1)),
|
|
323
|
+
} as Record<string, string>}
|
|
324
|
+
{...props}
|
|
325
|
+
>
|
|
326
|
+
{visibles}
|
|
327
|
+
{reste > 0 && (
|
|
328
|
+
<span
|
|
329
|
+
className={cn(
|
|
330
|
+
"sia-avatar",
|
|
331
|
+
`sia-avatar--${size}`,
|
|
332
|
+
`sia-avatar--${shape}`,
|
|
333
|
+
"sia-avatar--overflow",
|
|
334
|
+
)}
|
|
335
|
+
>
|
|
336
|
+
<span className="sia-avatar__fallback">
|
|
337
|
+
{renderOverflow ? renderOverflow(reste) : `+${reste}`}
|
|
338
|
+
</span>
|
|
339
|
+
</span>
|
|
340
|
+
)}
|
|
341
|
+
</div>
|
|
342
|
+
</AvatarGroupContext.Provider>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
@@ -1,5 +1,236 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* L'avatar.
|
|
3
|
+
*
|
|
4
|
+
* Tout repose sur une teinte, `--sia-avatar-tint`, posée par une classe et
|
|
5
|
+
* consommée par les trois variantes. Changer de variante ne redéfinit donc
|
|
6
|
+
* jamais la couleur : c'est ce qui permet à une teinte dérivée du nom de
|
|
7
|
+
* survivre à `soft`, `solid` et `outline` sans triplement des règles.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
.sia-avatar {
|
|
11
|
+
--sia-avatar-tint: var(--sia-primary);
|
|
12
|
+
--sia-avatar-size: 2.5rem;
|
|
13
|
+
--sia-avatar-radius: 50%;
|
|
14
|
+
|
|
15
|
+
position: relative;
|
|
16
|
+
display: inline-grid;
|
|
17
|
+
place-items: center;
|
|
18
|
+
flex: none;
|
|
19
|
+
width: var(--sia-avatar-size);
|
|
20
|
+
height: var(--sia-avatar-size);
|
|
21
|
+
border: 0;
|
|
22
|
+
border-radius: var(--sia-avatar-radius);
|
|
23
|
+
padding: 0;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
font-family: inherit;
|
|
26
|
+
font-weight: 600;
|
|
27
|
+
/* Les initiales ne doivent jamais être sélectionnées par un double-clic
|
|
28
|
+
destiné à ouvrir une fiche. */
|
|
29
|
+
user-select: none;
|
|
30
|
+
line-height: 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* ── Le repli ───────────────────────────────────────────────────────────── */
|
|
34
|
+
|
|
35
|
+
.sia-avatar__fallback {
|
|
36
|
+
display: grid;
|
|
37
|
+
place-items: center;
|
|
38
|
+
width: 100%;
|
|
39
|
+
height: 100%;
|
|
40
|
+
/* La même courbure que le parent. Sans elle, l'ombre interne de la
|
|
41
|
+
variante `outline` dessine un cadre carré que le masque circulaire
|
|
42
|
+
rogne : il n'en restait que deux fragments verticaux. */
|
|
43
|
+
border-radius: inherit;
|
|
44
|
+
/* Une lettre ne se centre pas d'elle-même : la casse haute laisse un vide
|
|
45
|
+
sous la ligne de base, et l'initiale paraît trop haute. */
|
|
46
|
+
padding-top: .04em;
|
|
47
|
+
font-size: calc(var(--sia-avatar-size) * .38);
|
|
48
|
+
letter-spacing: .01em;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* ── L'image ────────────────────────────────────────────────────────────── */
|
|
52
|
+
|
|
53
|
+
.sia-avatar__image {
|
|
54
|
+
position: absolute;
|
|
55
|
+
inset: 0;
|
|
56
|
+
width: 100%;
|
|
57
|
+
height: 100%;
|
|
58
|
+
object-fit: cover;
|
|
59
|
+
/* Elle arrive par-dessus le repli, qui tenait la place : rien ne saute,
|
|
60
|
+
rien ne clignote. */
|
|
61
|
+
opacity: 0;
|
|
62
|
+
animation: sia-avatar-image var(--sia-duration-base) var(--sia-easing-standard) forwards;
|
|
63
|
+
}
|
|
64
|
+
@keyframes sia-avatar-image {
|
|
65
|
+
from { opacity: 0; transform: scale(1.04); }
|
|
66
|
+
to { opacity: 1; transform: none; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Pendant l'attente, un souffle très léger sur le repli — assez pour dire
|
|
70
|
+
« ça arrive », pas assez pour attirer l'œil dans une liste de cinquante. */
|
|
71
|
+
.sia-avatar--loading .sia-avatar__fallback {
|
|
72
|
+
animation: sia-avatar-attente 1.6s ease-in-out infinite;
|
|
73
|
+
}
|
|
74
|
+
@keyframes sia-avatar-attente {
|
|
75
|
+
50% { opacity: .55; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* ── Les variantes ──────────────────────────────────────────────────────── */
|
|
79
|
+
|
|
80
|
+
/* `soft` : la teinte à faible saturation, posée en dégradé très court. C'est
|
|
81
|
+
ce qui donne la matière sans crier — un aplat pur paraît plat, un dégradé
|
|
82
|
+
marqué paraît daté. */
|
|
83
|
+
.sia-avatar--soft .sia-avatar__fallback {
|
|
84
|
+
background: linear-gradient(
|
|
85
|
+
145deg,
|
|
86
|
+
color-mix(in srgb, var(--sia-avatar-tint) 20%, var(--sia-background)),
|
|
87
|
+
color-mix(in srgb, var(--sia-avatar-tint) 11%, var(--sia-background))
|
|
88
|
+
);
|
|
89
|
+
color: color-mix(in srgb, var(--sia-avatar-tint) 82%, var(--sia-foreground));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.sia-avatar--solid .sia-avatar__fallback {
|
|
93
|
+
background: linear-gradient(
|
|
94
|
+
145deg,
|
|
95
|
+
color-mix(in srgb, var(--sia-avatar-tint) 88%, white),
|
|
96
|
+
var(--sia-avatar-tint)
|
|
97
|
+
);
|
|
98
|
+
color: var(--sia-primary-foreground, #fff);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.sia-avatar--outline .sia-avatar__fallback {
|
|
102
|
+
box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--sia-avatar-tint) 45%, transparent);
|
|
103
|
+
background: var(--sia-background);
|
|
104
|
+
color: color-mix(in srgb, var(--sia-avatar-tint) 85%, var(--sia-foreground));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Le liseré qui détache du fond — indispensable dans une pile. */
|
|
108
|
+
.sia-avatar--ring {
|
|
109
|
+
box-shadow:
|
|
110
|
+
0 0 0 2px var(--sia-background),
|
|
111
|
+
0 0 0 3px color-mix(in srgb, var(--sia-avatar-tint) 30%, transparent);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* ── Les teintes ────────────────────────────────────────────────────────── */
|
|
115
|
+
|
|
116
|
+
.sia-avatar--tint-primary { --sia-avatar-tint: var(--sia-primary); }
|
|
117
|
+
.sia-avatar--tint-secondary { --sia-avatar-tint: var(--sia-secondary); }
|
|
118
|
+
.sia-avatar--tint-info { --sia-avatar-tint: var(--sia-info); }
|
|
119
|
+
.sia-avatar--tint-success { --sia-avatar-tint: var(--sia-success); }
|
|
120
|
+
.sia-avatar--tint-warning { --sia-avatar-tint: var(--sia-warning); }
|
|
121
|
+
.sia-avatar--tint-danger { --sia-avatar-tint: var(--sia-danger); }
|
|
122
|
+
|
|
123
|
+
/* ── Les formes ─────────────────────────────────────────────────────────── */
|
|
124
|
+
|
|
125
|
+
.sia-avatar--circle { --sia-avatar-radius: 50%; }
|
|
126
|
+
/* Un rayon proportionnel à la taille : un carré arrondi de 2 rem et un de
|
|
127
|
+
5 rem n'ont pas la même courbure apparente à rayon fixe. */
|
|
128
|
+
.sia-avatar--rounded { --sia-avatar-radius: calc(var(--sia-avatar-size) * .28); }
|
|
129
|
+
.sia-avatar--square { --sia-avatar-radius: calc(var(--sia-avatar-size) * .08); }
|
|
130
|
+
|
|
131
|
+
/* ── Les tailles ────────────────────────────────────────────────────────── */
|
|
132
|
+
|
|
133
|
+
.sia-avatar--xs { --sia-avatar-size: 1.5rem; }
|
|
134
|
+
.sia-avatar--sm { --sia-avatar-size: 2rem; }
|
|
135
|
+
.sia-avatar--md { --sia-avatar-size: 2.5rem; }
|
|
136
|
+
.sia-avatar--lg { --sia-avatar-size: 3.25rem; }
|
|
137
|
+
.sia-avatar--xl { --sia-avatar-size: 4.5rem; }
|
|
138
|
+
|
|
139
|
+
/* ── Le statut ──────────────────────────────────────────────────────────── */
|
|
140
|
+
|
|
141
|
+
.sia-avatar__status {
|
|
142
|
+
position: absolute;
|
|
143
|
+
right: 0;
|
|
144
|
+
bottom: 0;
|
|
145
|
+
width: calc(var(--sia-avatar-size) * .26);
|
|
146
|
+
height: calc(var(--sia-avatar-size) * .26);
|
|
147
|
+
/* Un minimum absolu : sous six pixels, la pastille n'est plus lisible,
|
|
148
|
+
quelle que soit la taille de l'avatar. */
|
|
149
|
+
min-width: .4rem;
|
|
150
|
+
min-height: .4rem;
|
|
151
|
+
border-radius: 50%;
|
|
152
|
+
box-shadow: 0 0 0 2px var(--sia-background);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Décalée vers l'intérieur sur un cercle : posée au coin d'une boîte carrée,
|
|
156
|
+
elle sortirait du disque. */
|
|
157
|
+
.sia-avatar--circle .sia-avatar__status {
|
|
158
|
+
right: calc(var(--sia-avatar-size) * .03);
|
|
159
|
+
bottom: calc(var(--sia-avatar-size) * .03);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.sia-avatar__status--online { background: var(--sia-success); }
|
|
163
|
+
.sia-avatar__status--busy { background: var(--sia-danger); }
|
|
164
|
+
.sia-avatar__status--away { background: var(--sia-warning); }
|
|
165
|
+
.sia-avatar__status--offline {
|
|
166
|
+
background: var(--sia-background);
|
|
167
|
+
box-shadow: 0 0 0 2px var(--sia-background), inset 0 0 0 2px var(--sia-muted);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* ── Actionnable ────────────────────────────────────────────────────────── */
|
|
171
|
+
|
|
172
|
+
.sia-avatar--pressable {
|
|
173
|
+
cursor: pointer;
|
|
174
|
+
transition:
|
|
175
|
+
transform var(--sia-duration-fast) var(--sia-easing-standard),
|
|
176
|
+
box-shadow var(--sia-duration-fast) var(--sia-easing-standard);
|
|
177
|
+
}
|
|
178
|
+
.sia-avatar--pressable:hover { transform: translateY(-1px); }
|
|
179
|
+
.sia-avatar--pressable:active { transform: translateY(0) scale(.96); }
|
|
180
|
+
.sia-avatar--pressable:focus-visible {
|
|
181
|
+
outline: 2px solid var(--sia-primary);
|
|
182
|
+
outline-offset: 2px;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* ── Le groupe ──────────────────────────────────────────────────────────── */
|
|
186
|
+
|
|
187
|
+
.sia-avatar-group {
|
|
188
|
+
--sia-avatar-overlap: .25;
|
|
189
|
+
display: inline-flex;
|
|
190
|
+
align-items: center;
|
|
191
|
+
/* Une marge négative dont l'amplitude suit la taille : un chevauchement
|
|
192
|
+
fixe disparaît sur les grands avatars et les écrase sur les petits. */
|
|
193
|
+
padding-left: calc(var(--sia-avatar-size, 2.5rem) * var(--sia-avatar-overlap));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.sia-avatar-group > .sia-avatar {
|
|
197
|
+
margin-left: calc(var(--sia-avatar-size) * var(--sia-avatar-overlap) * -1);
|
|
198
|
+
/* Le liseré de la couleur du fond : sans lui, deux teintes voisines se
|
|
199
|
+
fondent l'une dans l'autre et la pile devient une tache. Son épaisseur
|
|
200
|
+
suit la taille — deux pixels disparaissent sur un avatar de 4,5 rem. */
|
|
201
|
+
box-shadow: 0 0 0 calc(var(--sia-avatar-size) * .055) var(--sia-background);
|
|
202
|
+
transition: transform var(--sia-duration-base) var(--sia-easing-spring);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* Chaque avatar recouvre le précédent — l'ordre de peinture naturel, sans
|
|
206
|
+
`z-index`.
|
|
207
|
+
|
|
208
|
+
L'inverse semblait plus logique, et c'est un piège : en mettant le premier
|
|
209
|
+
devant, chaque suivant est masqué **à gauche**, c'est-à-dire précisément là
|
|
210
|
+
où commence la lecture. « AK » devenait « K ». Recouvert à droite, chacun
|
|
211
|
+
garde au moins sa première initiale, et le compteur final reste entier. */
|
|
212
|
+
|
|
213
|
+
/* Au survol, la pile s'ouvre pour qu'on puisse viser quelqu'un. */
|
|
214
|
+
.sia-avatar-group--spread:hover > .sia-avatar {
|
|
215
|
+
margin-left: calc(var(--sia-avatar-size) * var(--sia-avatar-overlap) * -.25);
|
|
216
|
+
}
|
|
217
|
+
.sia-avatar-group--spread > .sia-avatar {
|
|
218
|
+
transition:
|
|
219
|
+
margin-left var(--sia-duration-base) var(--sia-easing-spring),
|
|
220
|
+
transform var(--sia-duration-base) var(--sia-easing-spring);
|
|
221
|
+
}
|
|
222
|
+
.sia-avatar-group > .sia-avatar:hover { transform: translateY(-2px); z-index: 10; }
|
|
223
|
+
|
|
224
|
+
.sia-avatar--overflow .sia-avatar__fallback {
|
|
225
|
+
background: var(--sia-surface);
|
|
226
|
+
color: var(--sia-muted);
|
|
227
|
+
font-size: calc(var(--sia-avatar-size) * .32);
|
|
228
|
+
font-variant-numeric: tabular-nums;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@media (prefers-reduced-motion: reduce) {
|
|
232
|
+
.sia-avatar__image,
|
|
233
|
+
.sia-avatar--loading .sia-avatar__fallback { animation: none; opacity: 1; }
|
|
234
|
+
.sia-avatar--pressable:hover,
|
|
235
|
+
.sia-avatar-group > .sia-avatar:hover { transform: none; }
|
|
236
|
+
}
|
|
@@ -15,6 +15,13 @@ export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
|
|
15
15
|
spinnerProps?: Omit<SpinnerProps, "label">;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Une étiquette d'état, courte et dense.
|
|
20
|
+
*
|
|
21
|
+
* Elle porte un ton, jamais une action : un badge cliquable se confond avec
|
|
22
|
+
* un bouton petit, et l'on découvre qu'il ne l'était pas en cliquant. Ce
|
|
23
|
+
* qui agit est un bouton.
|
|
24
|
+
*/
|
|
18
25
|
export function Badge({
|
|
19
26
|
tone: toneProp,
|
|
20
27
|
variant: variantProp,
|
|
@@ -26,6 +26,13 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
|
26
26
|
rightIcon?: ReactNode;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* L'action, sous toutes ses formes.
|
|
31
|
+
*
|
|
32
|
+
* Le chargement y est un état et non un remplacement : le bouton garde sa
|
|
33
|
+
* taille et son libellé pendant l'appel. Un bouton qui devient un rond
|
|
34
|
+
* tournant fait bouger la mise en page, et le clic suivant tombe à côté.
|
|
35
|
+
*/
|
|
29
36
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
30
37
|
(
|
|
31
38
|
{
|
|
@@ -59,6 +59,17 @@ function weekNumber(value: string) {
|
|
|
59
59
|
return Math.ceil((((target.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Un mois de dates, sélectionnables une à une ou par plage.
|
|
64
|
+
*
|
|
65
|
+
* Trois panneaux — jours, mois, années — plutôt qu'un défilement infini :
|
|
66
|
+
* choisir une date de naissance demande de remonter trente ans, et trente
|
|
67
|
+
* ans de défilement ne sont pas une navigation.
|
|
68
|
+
*
|
|
69
|
+
* Les dates circulent en chaînes `AAAA-MM-JJ`, jamais en objets `Date` : un
|
|
70
|
+
* `Date` porte une heure et un fuseau dont une date de facture n'a que
|
|
71
|
+
* faire, et qui la décalent d'un jour une fois sur deux.
|
|
72
|
+
*/
|
|
62
73
|
export function Calendar({
|
|
63
74
|
value,
|
|
64
75
|
defaultValue,
|
|
@@ -2,6 +2,14 @@ import { type HTMLAttributes } from "react";
|
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
3
|
import "./styles.css";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Une surface qui regroupe ce qui va ensemble.
|
|
7
|
+
*
|
|
8
|
+
* Quatre parties séparées — `Card`, `CardHeader`, `CardBody`, `CardFooter` —
|
|
9
|
+
* plutôt que des props `title` et `footer` : dès qu'un en-tête doit porter
|
|
10
|
+
* deux boutons et une pastille, une prop ne suffit plus, et l'on se retrouve
|
|
11
|
+
* à passer du JSX à travers une chaîne.
|
|
12
|
+
*/
|
|
5
13
|
export function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>) { return <div className={cn("sia-card", className)} {...props} />; }
|
|
6
14
|
export function CardHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) { return <div className={cn("sia-card__header", className)} {...props} />; }
|
|
7
15
|
export function CardBody({ className, ...props }: HTMLAttributes<HTMLDivElement>) { return <div className={cn("sia-card__body", className)} {...props} />; }
|
|
@@ -38,6 +38,14 @@ export interface CheckboxGroupProps {
|
|
|
38
38
|
children: ReactNode;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Une case, ou un groupe de cases.
|
|
43
|
+
*
|
|
44
|
+
* `CheckboxGroup` gère la liste des valeurs cochées; la case seule gère la
|
|
45
|
+
* sienne. L'état indéterminé est une troisième valeur, pas un entre-deux
|
|
46
|
+
* visuel : il dit « certains enfants sont cochés », ce qu'aucun booléen ne
|
|
47
|
+
* sait exprimer.
|
|
48
|
+
*/
|
|
41
49
|
export function CheckboxGroup({
|
|
42
50
|
value,
|
|
43
51
|
defaultValue = [],
|
|
@@ -2,5 +2,12 @@ import { forwardRef, useState, type InputHTMLAttributes } from "react";
|
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
3
|
import "./styles.css";
|
|
4
4
|
export interface ColorPickerProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "onChange"> { onValueChange?: (value: string) => void; showValue?: boolean; }
|
|
5
|
+
/**
|
|
6
|
+
* Le choix d'une couleur.
|
|
7
|
+
*
|
|
8
|
+
* La valeur hexadécimale est affichée à côté du nuancier : deux bleus
|
|
9
|
+
* voisins ne se distinguent pas à l'œil sur un écran mal calibré, alors que
|
|
10
|
+
* leurs codes, si.
|
|
11
|
+
*/
|
|
5
12
|
export const ColorPicker = forwardRef<HTMLInputElement, ColorPickerProps>(({ className, onValueChange, showValue = true, value, defaultValue = "#2563eb", ...props }, ref) => { const [internal, setInternal] = useState(String(defaultValue)); const current = value === undefined ? internal : String(value); return <span className={cn("sia-color-picker", className)}><input {...props} ref={ref} type="color" value={current} onChange={(event) => { if (value === undefined) setInternal(event.target.value); onValueChange?.(event.target.value); }} />{showValue && <code>{current}</code>}</span>; });
|
|
6
13
|
ColorPicker.displayName = "ColorPicker";
|