@zevra/ui 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/DECISIONS.md +227 -0
  2. package/dist/actions.d.ts +13 -3
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +9 -2
  5. package/dist/actions.js.map +1 -1
  6. package/dist/catalogue.d.ts.map +1 -1
  7. package/dist/catalogue.js +160 -11
  8. package/dist/catalogue.js.map +1 -1
  9. package/dist/css-inline.d.ts.map +1 -1
  10. package/dist/css-inline.js +1 -1
  11. package/dist/css-inline.js.map +1 -1
  12. package/dist/data-table.d.ts +30 -0
  13. package/dist/data-table.d.ts.map +1 -0
  14. package/dist/data-table.js +35 -0
  15. package/dist/data-table.js.map +1 -0
  16. package/dist/data.d.ts +34 -1
  17. package/dist/data.d.ts.map +1 -1
  18. package/dist/data.js +27 -0
  19. package/dist/data.js.map +1 -1
  20. package/dist/feedback.d.ts +49 -0
  21. package/dist/feedback.d.ts.map +1 -1
  22. package/dist/feedback.js +24 -0
  23. package/dist/feedback.js.map +1 -1
  24. package/dist/forms.d.ts +36 -0
  25. package/dist/forms.d.ts.map +1 -1
  26. package/dist/forms.js +24 -0
  27. package/dist/forms.js.map +1 -1
  28. package/dist/icon-data.d.ts +1 -1
  29. package/dist/icon-data.d.ts.map +1 -1
  30. package/dist/icon-data.js +42 -0
  31. package/dist/icon-data.js.map +1 -1
  32. package/dist/icon.d.ts +1 -1
  33. package/dist/icon.js +1 -1
  34. package/dist/layout.d.ts +21 -0
  35. package/dist/layout.d.ts.map +1 -1
  36. package/dist/layout.js +18 -0
  37. package/dist/layout.js.map +1 -1
  38. package/dist/navigation.d.ts +45 -7
  39. package/dist/navigation.d.ts.map +1 -1
  40. package/dist/navigation.js +54 -7
  41. package/dist/navigation.js.map +1 -1
  42. package/dist/surfaces.d.ts +18 -0
  43. package/dist/surfaces.d.ts.map +1 -1
  44. package/dist/surfaces.js +6 -0
  45. package/dist/surfaces.js.map +1 -1
  46. package/icons/index.json +126 -0
  47. package/icons/sprite.svg +21 -0
  48. package/package.json +18 -2
  49. package/react/actions.tsx +28 -2
  50. package/react/catalogue.tsx +343 -17
  51. package/react/css-inline.ts +1 -1
  52. package/react/data-table.tsx +153 -0
  53. package/react/data.tsx +87 -1
  54. package/react/feedback.tsx +98 -0
  55. package/react/forms.tsx +90 -0
  56. package/react/icon-data.ts +63 -0
  57. package/react/icon.tsx +2 -2
  58. package/react/layout.tsx +39 -0
  59. package/react/navigation.tsx +108 -6
  60. package/react/surfaces.tsx +45 -0
  61. package/src/base.css +16 -0
  62. package/src/components.css +135 -3
  63. package/src/data.css +188 -1
  64. package/src/feedback.css +196 -37
  65. package/src/forms.css +137 -1
  66. package/src/layout.css +72 -0
  67. package/src/navigation.css +207 -12
  68. package/src/surfaces.css +76 -7
  69. package/src/tokens.css +6 -0
  70. package/tokens/index.js +2 -0
@@ -0,0 +1,153 @@
1
+ // Tableau piloté par TanStack Table.
2
+ //
3
+ // ⚠️ CE FICHIER N'EST PAS DANS LE BARREL, et c'est délibéré :
4
+ // `@tanstack/react-table` est une dépendance OPTIONNELLE. L'importer depuis
5
+ // `@zevra/ui/react` casserait toutes les apps qui ne l'ont pas installée —
6
+ // une landing n'a aucun tableau. D'où le sous-chemin dédié :
7
+ //
8
+ // import { DataTable } from '@zevra/ui/react/table'
9
+ //
10
+ // POURQUOI TANSTACK plutôt que notre propre tri/pagination : tri multi-colonnes,
11
+ // filtres, groupement, virtualisation, sélection de lignes — c'est un travail
12
+ // de plusieurs mois que personne ici n'a envie de refaire, et mal. TanStack est
13
+ // HEADLESS : il ne rend rien, il calcule des modèles de lignes. Ce fichier lui
14
+ // donne l'apparence du paquet, exactement comme shadcn le fait avec ses
15
+ // variables. Aucune classe nouvelle n'entre par cette porte : ce sont les
16
+ // mêmes .zv-table / .zv-row / .zv-cell que le tableau écrit à la main.
17
+
18
+ import type { ReactNode } from 'react'
19
+ import {
20
+ flexRender,
21
+ type RowData,
22
+ type Table as TableTanstack,
23
+ } from '@tanstack/react-table'
24
+ import { cx } from './cx.js'
25
+
26
+ // Le pont entre les deux mondes : une colonne déclare ici son registre
27
+ // d'affichage, et le composant le traduit en classes du paquet. C'est ce qui
28
+ // évite d'avoir à écrire une cellule sur mesure pour aligner un nombre.
29
+ declare module '@tanstack/react-table' {
30
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
31
+ interface ColumnMeta<TData extends RowData, TValue> {
32
+ /** `nom` : colonne d'identité, la seule en encre pleine.
33
+ * `num` : chiffres alignés à droite, en chasse tabulaire. */
34
+ registre?: 'nom' | 'num'
35
+ /** Intitulé qui REMPLACE l'en-tête sous le point de rupture, où les
36
+ * colonnes s'empilent et où l'en-tête disparaît. */
37
+ label?: string
38
+ /** Unité accolée au nombre, en plus petit. */
39
+ unite?: ReactNode
40
+ /** Largeur de la colonne dans la grille (syntaxe grid-template-columns).
41
+ * Par défaut : `minmax(0, 1fr)`. */
42
+ largeur?: string
43
+ }
44
+ }
45
+
46
+ export interface DataTableProps<T> {
47
+ table: TableTanstack<T>
48
+ /** Rendu à la place des lignes quand le modèle est vide. ⚠️ C'est la LIGNE
49
+ * d'absence qu'on attend ici (filtre trop étroit), pas l'écran d'absence :
50
+ * l'en-tête doit rester visible pour qu'on puisse élargir le filtre. */
51
+ vide?: ReactNode
52
+ /** Appelé au clic sur une ligne. Sans lui, la ligne n'est pas cliquable —
53
+ * et elle ne doit pas en avoir l'air. */
54
+ onRowClick?: (ligne: T) => void
55
+ className?: string
56
+ }
57
+
58
+ export function DataTable<T>({ table, vide, onRowClick, className }: DataTableProps<T>) {
59
+ const colonnes = table.getVisibleLeafColumns()
60
+ const gabarit = colonnes
61
+ .map((c) => c.columnDef.meta?.largeur ?? 'minmax(0, 1fr)')
62
+ .join(' ')
63
+ const lignes = table.getRowModel().rows
64
+
65
+ return (
66
+ <div
67
+ className={cx('zv-table', className)}
68
+ style={{ ['--zv-cols']: gabarit } as React.CSSProperties}
69
+ >
70
+ {table.getHeaderGroups().map((groupe) => (
71
+ <div className="zv-table__head" key={groupe.id}>
72
+ {groupe.headers.map((entete) => {
73
+ const triable = entete.column.getCanSort()
74
+ const sens = entete.column.getIsSorted()
75
+ const registre = entete.column.columnDef.meta?.registre
76
+ const contenu = entete.isPlaceholder
77
+ ? null
78
+ : flexRender(entete.column.columnDef.header, entete.getContext())
79
+ return (
80
+ <div
81
+ key={entete.id}
82
+ className={cx('zv-cell', registre === 'num' && 'zv-cell--num')}
83
+ // La colonne triée est annoncée : sans aria-sort, le tri est
84
+ // une information purement visuelle.
85
+ aria-sort={
86
+ sens === 'asc' ? 'ascending' : sens === 'desc' ? 'descending' : undefined
87
+ }
88
+ >
89
+ {triable ? (
90
+ <button
91
+ type="button"
92
+ className="zv-sort"
93
+ onClick={entete.column.getToggleSortingHandler()}
94
+ >
95
+ {contenu}
96
+ <span className="zv-sort__mark" aria-hidden="true">
97
+ {sens === 'asc' ? '↑' : sens === 'desc' ? '↓' : '↕'}
98
+ </span>
99
+ </button>
100
+ ) : (
101
+ contenu
102
+ )}
103
+ </div>
104
+ )
105
+ })}
106
+ </div>
107
+ ))}
108
+
109
+ {lignes.length === 0
110
+ ? vide
111
+ : lignes.map((ligne) => (
112
+ <div
113
+ key={ligne.id}
114
+ className="zv-row"
115
+ // Le clavier suit le clic : une ligne cliquable qui n'est pas
116
+ // atteignable au clavier est une action inaccessible.
117
+ role={onRowClick ? 'button' : undefined}
118
+ tabIndex={onRowClick ? 0 : undefined}
119
+ style={onRowClick ? undefined : { cursor: 'default' }}
120
+ onClick={onRowClick ? () => onRowClick(ligne.original) : undefined}
121
+ onKeyDown={
122
+ onRowClick
123
+ ? (e) => {
124
+ if (e.key === 'Enter' || e.key === ' ') {
125
+ e.preventDefault()
126
+ onRowClick(ligne.original)
127
+ }
128
+ }
129
+ : undefined
130
+ }
131
+ >
132
+ {ligne.getVisibleCells().map((cellule) => {
133
+ const meta = cellule.column.columnDef.meta
134
+ return (
135
+ <div
136
+ key={cellule.id}
137
+ className={cx(
138
+ 'zv-cell',
139
+ meta?.registre === 'nom' && 'zv-cell--name',
140
+ meta?.registre === 'num' && 'zv-cell--num',
141
+ )}
142
+ >
143
+ {meta?.label && <span className="zv-cell__label">{meta.label}</span>}
144
+ {flexRender(cellule.column.columnDef.cell, cellule.getContext())}
145
+ {meta?.unite && <span className="zv-cell__unit">{meta.unite}</span>}
146
+ </div>
147
+ )
148
+ })}
149
+ </div>
150
+ ))}
151
+ </div>
152
+ )
153
+ }
package/react/data.tsx CHANGED
@@ -5,7 +5,13 @@
5
5
  // et en carte empilée en dessous, sans dupliquer le balisage. `Unwrap` est la
6
6
  // primitive qui rend cela possible.
7
7
 
8
- import type { AnchorHTMLAttributes, CSSProperties, HTMLAttributes, ReactNode } from 'react'
8
+ import type {
9
+ AnchorHTMLAttributes,
10
+ CSSProperties,
11
+ DetailsHTMLAttributes,
12
+ HTMLAttributes,
13
+ ReactNode,
14
+ } from 'react'
9
15
  import { cx } from './cx.js'
10
16
 
11
17
  type Div = Omit<HTMLAttributes<HTMLDivElement>, 'className'>
@@ -298,3 +304,83 @@ export function FileType({ format, className, ...reste }: FileTypeProps) {
298
304
  </span>
299
305
  )
300
306
  }
307
+
308
+ /* ─── Rangée dépliante ─────────────────────────────────────────────── */
309
+
310
+ export interface FoldProps
311
+ extends Omit<DetailsHTMLAttributes<HTMLDetailsElement>, 'className' | 'open'> {
312
+ titre: ReactNode
313
+ /** Ouverte au premier rendu. L'état vit ensuite dans le NAVIGATEUR
314
+ * (<details> natif) : pas de prop contrôlée, pas de JavaScript — le
315
+ * clavier et l'annonce viennent avec. `open` est retiré des attributs :
316
+ * deux canaux pour le même état, et c'est le silencieux qui gagnerait. */
317
+ ouvert?: boolean
318
+ className?: string
319
+ children?: ReactNode
320
+ }
321
+
322
+ /** Le pliage de l'applicatif : options avancées, détail d'une ligne de
323
+ * journal. La FAQ (contenu) reste le pliage de la VITRINE — question en
324
+ * 14px, respiration éditoriale ; celui-ci est un chrome dense. Le chevron
325
+ * fait partie du composant, comme la flèche de LinkArrow. */
326
+ export function Fold({ titre, ouvert, className, children, ...reste }: FoldProps) {
327
+ return (
328
+ <details className={cx('zv-fold', className)} open={ouvert} {...reste}>
329
+ <summary className="zv-fold__titre">
330
+ {titre}
331
+ <svg
332
+ className="zv-fold__chevron"
333
+ viewBox="0 0 24 24"
334
+ fill="none"
335
+ stroke="currentColor"
336
+ strokeWidth="2"
337
+ strokeLinecap="round"
338
+ strokeLinejoin="round"
339
+ aria-hidden="true"
340
+ >
341
+ <path d="M6.67 9.33 L12 14.67 L17.33 9.33" />
342
+ </svg>
343
+ </summary>
344
+ <div className="zv-fold__corps">{children}</div>
345
+ </details>
346
+ )
347
+ }
348
+
349
+ /* ─── Touche de clavier ────────────────────────────────────────────── */
350
+
351
+ /** Une touche, une seule : « ⌘K » est DEUX touches, donc deux `Kbd` — les
352
+ * fondre dans une seule dessine une touche qui n'existe sur aucun
353
+ * clavier. */
354
+ export function Kbd({
355
+ className,
356
+ ...reste
357
+ }: Omit<HTMLAttributes<HTMLElement>, 'className'> & { className?: string }) {
358
+ return <kbd className={cx('zv-kbd', className)} {...reste} />
359
+ }
360
+
361
+ /* ─── Code ─────────────────────────────────────────────────────────── */
362
+
363
+ /** Code EN LIGNE, dans une phrase : `pnpm build`. Taille relative au texte
364
+ * qui l'accueille. Pour un extrait de plusieurs lignes, `CodeBlock`. */
365
+ export function Code({
366
+ className,
367
+ ...reste
368
+ }: Omit<HTMLAttributes<HTMLElement>, 'className'> & { className?: string }) {
369
+ return <code className={cx('zv-code', className)} {...reste} />
370
+ }
371
+
372
+ /** Bloc de code. Chasse fixe RÉELLE (--font-code) : --font-mono est
373
+ * proportionnelle et n'aligne pas du code. Aucune coloration syntaxique —
374
+ * elle imposerait une palette entière que la charte n'a pas. Le bloc
375
+ * défile en son sein : une ligne longue ne pousse jamais la page. */
376
+ export function CodeBlock({
377
+ className,
378
+ children,
379
+ ...reste
380
+ }: Omit<HTMLAttributes<HTMLPreElement>, 'className'> & { className?: string }) {
381
+ return (
382
+ <pre className={cx('zv-codeblock', className)} {...reste}>
383
+ <code>{children}</code>
384
+ </pre>
385
+ )
386
+ }
@@ -227,3 +227,101 @@ export function Progress({ valeur, label, className, ...reste }: ProgressProps)
227
227
  </div>
228
228
  )
229
229
  }
230
+
231
+ /* ─── Roue ─────────────────────────────────────────────────────────── */
232
+
233
+ export interface SpinnerProps extends Span {
234
+ /** En tête d'un panneau d'attente : la roue seule, plus grande. */
235
+ grand?: boolean
236
+ /** Ce qu'on attend, annoncé au lecteur d'écran. Sans lui la roue est
237
+ * DÉCORATIVE et masquée — le bon défaut quand une phrase voisine dit
238
+ * déjà le travail (`Loading`, ou le libellé d'un bouton `occupe`). */
239
+ label?: string
240
+ className?: string
241
+ }
242
+
243
+ /** Attente COURTE ET LOCALE : un bouton, une cellule, un panneau sans la
244
+ * place d'une phrase. Pour l'attente d'un écran, préférer `Loading` — la
245
+ * phrase qui dit le travail réel reste le motif par défaut du système.
246
+ * Le libellé est un TEXTE masqué visuellement, pas un aria-label : sur un
247
+ * span vide, l'aria-label n'est pas annoncé par tous les lecteurs. */
248
+ export function Spinner({ grand, label, className, ...reste }: SpinnerProps) {
249
+ return (
250
+ <span
251
+ role={label ? 'status' : undefined}
252
+ aria-hidden={label ? undefined : true}
253
+ className={cx('zv-spinner', grand && 'zv-spinner--lg', className)}
254
+ {...reste}
255
+ >
256
+ {label && <span className="zv-sr-only">{label}</span>}
257
+ </span>
258
+ )
259
+ }
260
+
261
+ /* ─── Squelette ────────────────────────────────────────────────────── */
262
+
263
+ export interface SkeletonProps extends Div {
264
+ /** Une ligne de texte (hauteur d'un cadratin) plutôt qu'un bloc libre. */
265
+ texte?: boolean
266
+ /** La géométrie vient du CONSOMMATEUR : un squelette qui invente ses
267
+ * dimensions ne ressemble jamais au contenu qu'il annonce. */
268
+ largeur?: number | string
269
+ hauteur?: number | string
270
+ className?: string
271
+ }
272
+
273
+ /** Il ANNONCE une structure connue (la carte, la ligne qui va venir) — pour
274
+ * un contenu dont la forme est inconnue, c'est `Loading` qui dit ce cas.
275
+ * Le squelette est masqué aux lecteurs d'écran : c'est la zone qui charge
276
+ * qui porte aria-busy, pas chaque os. */
277
+ export function Skeleton({ texte, largeur, hauteur, className, style, ...reste }: SkeletonProps) {
278
+ return (
279
+ <div
280
+ aria-hidden="true"
281
+ className={cx('zv-skeleton', texte && 'zv-skeleton--text', className)}
282
+ style={{ width: largeur, height: hauteur, ...style }}
283
+ {...reste}
284
+ />
285
+ )
286
+ }
287
+
288
+ /* ─── Infobulle ────────────────────────────────────────────────────── */
289
+
290
+ export interface TooltipProps extends Span {
291
+ /** Le complément — un raccourci, une date exacte, une unité. JAMAIS le
292
+ * nom de la commande : lui vit sur la cible (libellé ou aria-label), une
293
+ * infobulle n'existe pas au doigt. */
294
+ contenu: ReactNode
295
+ /** Identifiant EXPLICITE, à répéter en `aria-describedby` sur la cible —
296
+ * le contrat du couple htmlFor/id de Field, pour la même raison : pas de
297
+ * useId, le composant reste rendu au serveur. */
298
+ id?: string
299
+ /** Sous la cible, pour les rares cas où le haut est occupé. */
300
+ bas?: boolean
301
+ className?: string
302
+ /** La cible. Elle doit être FOCALISABLE (un bouton, un lien) : c'est son
303
+ * focus qui montre l'infobulle au clavier. */
304
+ children?: ReactNode
305
+ }
306
+
307
+ /** Échap ferme la bulle jusqu'au blur (WCAG 1.4.13) : la classe est posée
308
+ * sur le nœud sans état React — le composant reste rendu au serveur, et
309
+ * sans script la bulle reste simplement au comportement survol/focus. */
310
+ export function Tooltip({ contenu, id, bas, className, children, ...reste }: TooltipProps) {
311
+ return (
312
+ <span
313
+ className={cx('zv-tooltip-hit', className)}
314
+ onKeyDown={(e) => {
315
+ if (e.key === 'Escape') e.currentTarget.classList.add('zv-tooltip-hit--off')
316
+ }}
317
+ onBlur={(e) => e.currentTarget.classList.remove('zv-tooltip-hit--off')}
318
+ onMouseLeave={(e) => e.currentTarget.classList.remove('zv-tooltip-hit--off')}
319
+ {...reste}
320
+ >
321
+ {children}
322
+ <span role="tooltip" id={id} className={cx('zv-tooltip', bas && 'zv-tooltip--bas')}>
323
+ {contenu}
324
+ </span>
325
+ </span>
326
+ )
327
+ }
package/react/forms.tsx CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  import type {
15
15
  ButtonHTMLAttributes,
16
+ CSSProperties,
16
17
  HTMLAttributes,
17
18
  InputHTMLAttributes,
18
19
  ReactNode,
@@ -381,3 +382,92 @@ export function DropInput({
381
382
  }) {
382
383
  return <input type="file" className={cx('zv-drop__input', className)} {...reste} />
383
384
  }
385
+
386
+ /* ─── Champ à affixe ───────────────────────────────────────────────── */
387
+
388
+ export interface InputAffixProps
389
+ extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
390
+ /** Préfixe, en mono label : « € », « HT ». Une unité est un libellé, pas
391
+ * une donnée — elle ne se saisit pas, elle qualifie ce qui est saisi. */
392
+ avant?: ReactNode
393
+ /** Suffixe : « JOURS », « / MOIS », « .zevra.tech ». */
394
+ apres?: ReactNode
395
+ /** Identifiants EXPLICITES des affixes, à citer dans `aria-describedby`
396
+ * sur le champ intérieur — le contrat htmlFor/id de `Field`, pour la
397
+ * même raison : pas de useId, le composant reste rendu au serveur.
398
+ * ⚠️ Sans ce câblage, un lecteur d'écran en mode formulaire entend
399
+ * « 1200 » sans jamais apprendre qu'il s'agit d'euros HT — exactement le
400
+ * défaut que le composant existe pour corriger. */
401
+ idAvant?: string
402
+ idApres?: string
403
+ className?: string
404
+ /** Le champ lui-même — un `<Input nu>` : c'est le CADRE qui fait le champ,
405
+ * un `<Input>` bordé dessinerait deux cadres imbriqués. */
406
+ children?: ReactNode
407
+ }
408
+
409
+ /** Cadre à affixe : « 30 », seul dans un champ, ne dit pas s'il s'agit de
410
+ * jours ou d'euros. Même grammaire que `Search` (cadre + champ nu) ; à ne
411
+ * pas employer quand l'unité est déjà dans l'intitulé du champ — deux fois
412
+ * la même unité se lisent comme deux informations. */
413
+ export function InputAffix({
414
+ avant,
415
+ apres,
416
+ idAvant,
417
+ idApres,
418
+ className,
419
+ children,
420
+ ...reste
421
+ }: InputAffixProps) {
422
+ return (
423
+ <div className={cx('zv-affix', className)} {...reste}>
424
+ {avant != null && (
425
+ <span className="zv-affix__add" id={idAvant}>
426
+ {avant}
427
+ </span>
428
+ )}
429
+ {children}
430
+ {apres != null && (
431
+ <span className="zv-affix__add" id={idApres}>
432
+ {apres}
433
+ </span>
434
+ )}
435
+ </div>
436
+ )
437
+ }
438
+
439
+ /* ─── Curseur ──────────────────────────────────────────────────────── */
440
+
441
+ export interface RangeProps
442
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, 'className' | 'type'> {
443
+ className?: string
444
+ }
445
+
446
+ /** Curseur natif habillé (`<input type="range">`) : clavier, annonce et
447
+ * bornes viennent du navigateur. Le remplissage de la piste est calculé
448
+ * ici depuis value/min/max — une géométrie, comme la largeur de
449
+ * `Progress`, pas une valeur de charte. En NON contrôlé, il reflète la
450
+ * valeur initiale et ne suit plus le doigt : contrôler le champ dès que le
451
+ * remplissage doit suivre.
452
+ * ⚠️ Toujours accompagné d'un intitulé (`Field`) ET d'une valeur affichée
453
+ * en clair : un curseur seul ne dit ni ce qu'il règle, ni sur combien. */
454
+ export function Range({ className, style, min, max, value, defaultValue, ...reste }: RangeProps) {
455
+ const borneMin = Number(min ?? 0)
456
+ const borneMax = Number(max ?? 100)
457
+ const courant = Number(value ?? defaultValue ?? NaN)
458
+ const part = Number.isNaN(courant)
459
+ ? 0.5
460
+ : Math.min(1, Math.max(0, (courant - borneMin) / (borneMax - borneMin || 1)))
461
+ return (
462
+ <input
463
+ type="range"
464
+ min={min}
465
+ max={max}
466
+ value={value}
467
+ defaultValue={defaultValue}
468
+ className={cx('zv-range', className)}
469
+ style={{ ...style, '--zv-range-pos': `${part * 100}%` } as CSSProperties}
470
+ {...reste}
471
+ />
472
+ )
473
+ }
@@ -4,28 +4,45 @@
4
4
  export type IconId =
5
5
  | 'accueil'
6
6
  | 'activite'
7
+ | 'aide'
7
8
  | 'app-couches'
8
9
  | 'archiver'
10
+ | 'attention'
11
+ | 'balance'
9
12
  | 'base-de-donnees'
10
13
  | 'bouclier-verifie'
11
14
  | 'cadenas'
12
15
  | 'cadrer'
16
+ | 'calendrier'
13
17
  | 'chevron-bas'
14
18
  | 'chevron-droit'
15
19
  | 'chevron-gauche'
20
+ | 'chevron-haut'
16
21
  | 'coche'
17
22
  | 'copier'
18
23
  | 'corbeille'
24
+ | 'courriel'
19
25
  | 'cube'
20
26
  | 'deconnexion'
21
27
  | 'depot-fichier'
28
+ | 'diplome'
22
29
  | 'dossier'
23
30
  | 'eclair'
31
+ | 'envoyer'
32
+ | 'epingle'
24
33
  | 'equipe'
25
34
  | 'etincelles'
35
+ | 'etoile'
26
36
  | 'fermer'
37
+ | 'fichier'
38
+ | 'filtre'
39
+ | 'globe'
40
+ | 'graphique'
27
41
  | 'horloge'
42
+ | 'imprimante'
43
+ | 'info'
28
44
  | 'lien-externe'
45
+ | 'livre-ouvert'
29
46
  | 'menu'
30
47
  | 'moins'
31
48
  | 'oeil'
@@ -35,33 +52,54 @@ export type IconId =
35
52
  | 'reglages'
36
53
  | 'reseau'
37
54
  | 'taches'
55
+ | 'telecharger'
56
+ | 'telephone'
57
+ | 'tri'
38
58
  | 'trois-points'
59
+ | 'utilisateur'
39
60
 
40
61
  export const CORPS: Record<IconId, string> = {
41
62
  'accueil': "<path d=\"M4 10.67 L12 4 L20 10.67 V20 H4 Z\" /><path d=\"M9.33 20 V13.33 H14.67 V20\" />",
42
63
  'activite': "<path d=\"M3.33 12 H8 L10.67 5.33 L14 18.67 L16 12 H20.67\" />",
64
+ 'aide': "<circle cx=\"12\" cy=\"12\" r=\"8.67\" /><path d=\"M9.33 9.33 A2.67 2.67 0 0 1 14.53 10.13 C14.53 11.87 12 12.27 12 14\" /><circle cx=\"12\" cy=\"17.07\" r=\"0.8\" fill=\"currentColor\" stroke=\"none\" />",
43
65
  'app-couches': "<path d=\"M12 2.67 L20 6.67 L12 10.67 L4 6.67 Z\" /><path d=\"M4 12 L12 16 L20 12\" /><path d=\"M4 17.33 L12 21.33 L20 17.33\" />",
44
66
  'archiver': "<rect x=\"3.33\" y=\"4\" width=\"17.33\" height=\"4.67\" rx=\"1.07\" /><path d=\"M5.33 8.67 V18.93 A1.07 1.07 0 0 0 6.4 20 H17.6 A1.07 1.07 0 0 0 18.67 18.93 V8.67\" /><path d=\"M9.33 12.67 H14.67\" />",
67
+ 'attention': "<path d=\"M10.67 3.87 L2.53 17.87 A1.55 1.55 0 0 0 3.87 20.19 H20.13 A1.55 1.55 0 0 0 21.47 17.87 L13.33 3.87 A1.54 1.54 0 0 0 10.67 3.87 Z\" /><path d=\"M12 9.33 V13.33\" /><circle cx=\"12\" cy=\"16.53\" r=\"0.8\" fill=\"currentColor\" stroke=\"none\" />",
68
+ 'balance': "<path d=\"M12 3.33 V20.67\" /><path d=\"M8.67 20.67 H15.33\" /><path d=\"M5.33 6 H18.67\" /><path d=\"M6.67 6 L3.33 13.33\" /><path d=\"M6.67 6 L10 13.33\" /><path d=\"M3.33 13.33 A3.33 3.33 0 0 0 10 13.33\" /><path d=\"M17.33 6 L14 13.33\" /><path d=\"M17.33 6 L20.67 13.33\" /><path d=\"M14 13.33 A3.33 3.33 0 0 0 20.67 13.33\" />",
45
69
  'base-de-donnees': "<ellipse cx=\"12\" cy=\"6\" rx=\"7.33\" ry=\"2.67\" /><path d=\"M4.67 6 V12 C4.67 13.47 7.87 14.67 12 14.67 C16.13 14.67 19.33 13.47 19.33 12 V6\" /><path d=\"M4.67 12 V18 C4.67 19.47 7.87 20.67 12 20.67 C16.13 20.67 19.33 19.47 19.33 18 V12\" />",
46
70
  'bouclier-verifie': "<path d=\"M12 2.67 L20 6 V12 C20 16.67 16.67 19.73 12 21.33 C7.33 19.73 4 16.67 4 12 V6 Z\" /><path d=\"M8.67 12 L11.07 14.4 L15.33 10\" />",
47
71
  'cadenas': "<rect x=\"4.67\" y=\"10.67\" width=\"14.67\" height=\"9.33\" rx=\"1.6\" /><path d=\"M8 10.67 V7.33 A4 4 0 0 1 16 7.33 V10.67\" /><circle cx=\"12\" cy=\"15.33\" r=\"1.33\" />",
48
72
  'cadrer': "<path d=\"M4 8 V4 H8\" /><path d=\"M20 8 V4 H16\" /><path d=\"M4 16 V20 H8\" /><path d=\"M20 16 V20 H16\" />",
73
+ 'calendrier': "<rect x=\"3.33\" y=\"4.67\" width=\"17.33\" height=\"16\" rx=\"1.33\" /><path d=\"M3.33 9.33 H20.67\" /><path d=\"M8 2.67 V6.67\" /><path d=\"M16 2.67 V6.67\" />",
49
74
  'chevron-bas': "<path d=\"M6.67 9.33 L12 14.67 L17.33 9.33\" />",
50
75
  'chevron-droit': "<path d=\"M9.33 4 L17.33 12 L9.33 20\" />",
51
76
  'chevron-gauche': "<path d=\"M14.67 4 L6.67 12 L14.67 20\" />",
77
+ 'chevron-haut': "<path d=\"M6.67 14.67 L12 9.33 L17.33 14.67\" />",
52
78
  'coche': "<path d=\"M5 12.4 L9.4 16.8 L19 6.8\" />",
53
79
  'copier': "<rect x=\"4\" y=\"4\" width=\"12\" height=\"12\" rx=\"1.6\" /><path d=\"M8 16 V18.93 A1.07 1.07 0 0 0 9.07 20 H18.93 A1.07 1.07 0 0 0 20 18.93 V9.07 A1.07 1.07 0 0 0 18.93 8 H16\" />",
54
80
  'corbeille': "<path d=\"M4 6.67 H20\" /><path d=\"M9.33 6.67 V4.67 A1.33 1.33 0 0 1 10.67 3.33 H13.33 A1.33 1.33 0 0 1 14.67 4.67 V6.67\" /><path d=\"M6 6.67 L6.93 18.93 A1.33 1.33 0 0 0 8.27 20 H15.73 A1.33 1.33 0 0 0 17.07 18.93 L18 6.67\" /><path d=\"M10 10.67 V16.67\" /><path d=\"M14 10.67 V16.67\" />",
81
+ 'courriel': "<rect x=\"2.67\" y=\"5.33\" width=\"18.67\" height=\"13.33\" rx=\"1.33\" /><path d=\"M2.67 7.33 L12 13.33 L21.33 7.33\" />",
55
82
  'cube': "<path d=\"M3.33 8 L12 4 L20.67 8 V16 L12 20 L3.33 16 Z\" /><path d=\"M3.33 8 L12 12 L20.67 8\" /><path d=\"M12 12 V20\" />",
56
83
  'deconnexion': "<path d=\"M14.67 4 H5.33 V20 H14.67\" /><path d=\"M10.67 12 H20\" /><path d=\"M16.67 8 L20.67 12 L16.67 16\" />",
57
84
  'depot-fichier': "<path d=\"M12 3.2 V14.4\" /><polyline points=\"7.6,10 12,14.4 16.4,10\" /><path d=\"M4 16.8 v2.4 a1.6 1.6 0 0 0 1.6 1.6 h12.8 a1.6 1.6 0 0 0 1.6-1.6 v-2.4\" />",
85
+ 'diplome': "<path d=\"M2.67 9.33 L12 4.67 L21.33 9.33 L12 14 Z\" /><path d=\"M6.67 11.6 V16 C6.67 17.47 9.07 18.67 12 18.67 C14.93 18.67 17.33 17.47 17.33 16 V11.6\" /><path d=\"M21.33 9.33 V14.67\" />",
58
86
  'dossier': "<path d=\"M1.41 6.64a1.69 1.69 0 0 1 1.69-1.69h4.8l2.12 2.4h10.87a1.69 1.69 0 0 1 1.69 1.69v9.88a1.69 1.69 0 0 1-1.69 1.69H3.11A1.69 1.69 0 0 1 1.41 20.33V6.64Z\" />",
59
87
  'eclair': "<path d=\"M13.33 2.67 L5.33 13.33 H12 L10.67 21.33 L18.67 10.67 H12 Z\" />",
88
+ 'envoyer': "<path d=\"M21.33 2.67 L10.67 13.33\" /><path d=\"M21.33 2.67 L14.67 21.33 L10.67 13.33 L2.67 9.33 Z\" />",
89
+ 'epingle': "<path d=\"M12 21.33 C7.56 16.89 5.33 13.42 5.33 10.67 A6.67 6.67 0 0 1 18.67 10.67 C18.67 13.42 16.44 16.89 12 21.33 Z\" /><circle cx=\"12\" cy=\"10.67\" r=\"2.4\" />",
60
90
  'equipe': "<circle cx=\"9.33\" cy=\"8\" r=\"3.33\" /><path d=\"M3.33 20 C3.33 15.33 6 13.33 9.33 13.33 C12.67 13.33 15.33 15.33 15.33 20\" /><path d=\"M16 5.33 A2.93 2.93 0 0 1 16 11.07\" /><path d=\"M17.33 13.6 C19.73 14.27 20.67 16.4 20.67 20\" />",
61
91
  'etincelles': "<path d=\"M12 2.67 L13.87 8.8 L20 10.67 L13.87 12.53 L12 18.67 L10.13 12.53 L4 10.67 L10.13 8.8 Z\" /><path d=\"M19.33 16.67 L20 18.67 L22 19.33 L20 20 L19.33 22 L18.67 20 L16.67 19.33 L18.67 18.67 Z\" />",
92
+ 'etoile': "<path d=\"M12 3.33 L14.67 8.53 L20.4 9.37 L16.2 13.4 L17.2 19.07 L12 16.4 L6.8 19.07 L7.8 13.4 L3.6 9.37 L9.33 8.53 Z\" />",
62
93
  'fermer': "<path d=\"M5.33 5.33 L18.67 18.67\" /><path d=\"M18.67 5.33 L5.33 18.67\" />",
94
+ 'fichier': "<path d=\"M13.33 2.67 H6.67 A1.33 1.33 0 0 0 5.33 4 V20 A1.33 1.33 0 0 0 6.67 21.33 H17.33 A1.33 1.33 0 0 0 18.67 20 V8 Z\" /><path d=\"M13.33 2.67 V8 H18.67\" />",
95
+ 'filtre': "<path d=\"M2.67 4 H21.33 L14.67 12.27 V18.67 L9.33 21.33 V12.27 Z\" />",
96
+ 'globe': "<circle cx=\"12\" cy=\"12\" r=\"8.67\" /><path d=\"M3.33 12 H20.67\" /><path d=\"M12 3.33 C14.22 5.64 15.47 8.71 15.47 12 C15.47 15.29 14.22 18.36 12 20.67 C9.78 18.36 8.53 15.29 8.53 12 C8.53 8.71 9.78 5.64 12 3.33 Z\" />",
97
+ 'graphique': "<path d=\"M4 4 V20 H20\" /><path d=\"M8.67 20 V12.67\" /><path d=\"M12.67 20 V8\" /><path d=\"M16.67 20 V15.33\" />",
63
98
  'horloge': "<circle cx=\"12\" cy=\"12\" r=\"8\" /><path d=\"M12 6.67 V12 L15.33 14\" />",
99
+ 'imprimante': "<path d=\"M6.67 8 V3.33 H17.33 V8\" /><path d=\"M6.67 17.33 H4.67 A1.33 1.33 0 0 1 3.33 16 V10.67 A1.33 1.33 0 0 1 4.67 9.33 H19.33 A1.33 1.33 0 0 1 20.67 10.67 V16 A1.33 1.33 0 0 1 19.33 17.33 H17.33\" /><rect x=\"6.67\" y=\"13.33\" width=\"10.67\" height=\"7.33\" rx=\"0.8\" />",
100
+ 'info': "<circle cx=\"12\" cy=\"12\" r=\"8.67\" /><path d=\"M12 11.33 V16\" /><circle cx=\"12\" cy=\"8\" r=\"0.8\" fill=\"currentColor\" stroke=\"none\" />",
64
101
  'lien-externe': "<path d=\"M10.67 4 H4 V20 H20 V13.33\" /><path d=\"M14.67 4 H20 V9.33\" /><path d=\"M20 4 L12 12\" />",
102
+ 'livre-ouvert': "<path d=\"M2.67 5.33 C5.33 4.67 7.78 4.89 12 6.67 C16.22 4.89 18.67 4.67 21.33 5.33 V18 C18.67 17.33 16.22 17.56 12 19.33 C7.78 17.56 5.33 17.33 2.67 18 Z\" /><path d=\"M12 6.67 V19.33\" />",
65
103
  'menu': "<path d=\"M0 5.33 H24\" /><path d=\"M0 12 H24\" /><path d=\"M0 18.67 H24\" />",
66
104
  'moins': "<path d=\"M4 12 H20\" />",
67
105
  'oeil': "<path d=\"M2 12 C5.33 6.67 8.67 4.67 12 4.67 C15.33 4.67 18.67 6.67 22 12 C18.67 17.33 15.33 19.33 12 19.33 C8.67 19.33 5.33 17.33 2 12 Z\" /><circle cx=\"12\" cy=\"12\" r=\"2.67\" />",
@@ -71,7 +109,11 @@ export const CORPS: Record<IconId, string> = {
71
109
  'reglages': "<circle cx=\"12\" cy=\"12\" r=\"2.93\" /><path d=\"M12 2 V4.67 M12 19.33 V22 M2 12 H4.67 M19.33 12 H22 M4.8 4.8 L6.67 6.67 M17.33 17.33 L19.2 19.2 M19.2 4.8 L17.33 6.67 M6.67 17.33 L4.8 19.2\" />",
72
110
  'reseau': "<circle cx=\"12\" cy=\"12\" r=\"8\" /><path d=\"M4 12 H20\" /><path d=\"M12 4 C14.67 8 14.67 16 12 20\" /><path d=\"M12 4 C9.33 8 9.33 16 12 20\" />",
73
111
  'taches': "<path d=\"M4.67 6.67 L6.67 8.67 L10 5.33\" /><path d=\"M4.67 16 L6.67 18 L10 14.67\" /><path d=\"M13.33 7.33 H20\" /><path d=\"M13.33 16.67 H20\" />",
112
+ 'telecharger': "<path d=\"M12 3.33 V15.33\" /><polyline points=\"6.67,10 12,15.33 17.33,10\" /><path d=\"M4.67 20.67 H19.33\" />",
113
+ 'telephone': "<path d=\"M20.67 16.93 V19.33 A1.6 1.6 0 0 1 18.93 20.93 C16.13 20.67 13.47 19.67 11.2 18.13 C9.09 16.71 7.29 14.91 5.87 12.8 C4.33 10.52 3.33 7.85 3.07 5.05 A1.6 1.6 0 0 1 4.67 3.33 H7.07 A1.6 1.6 0 0 1 8.67 4.71 C8.79 5.83 9.04 6.93 9.4 7.97 A1.6 1.6 0 0 1 9.04 9.65 L8 10.69 C9.32 13.01 11.03 14.71 13.33 16 L14.37 14.96 A1.6 1.6 0 0 1 16.05 14.6 C17.09 14.96 18.19 15.21 19.31 15.33 A1.6 1.6 0 0 1 20.67 16.93 Z\" />",
114
+ 'tri': "<path d=\"M8 4 V20\" /><path d=\"M4.67 7.33 L8 4 L11.33 7.33\" /><path d=\"M16 20 V4\" /><path d=\"M12.67 16.67 L16 20 L19.33 16.67\" />",
74
115
  'trois-points': "<circle cx=\"5.33\" cy=\"12\" r=\"1.33\" fill=\"currentColor\" stroke=\"none\" /><circle cx=\"12\" cy=\"12\" r=\"1.33\" fill=\"currentColor\" stroke=\"none\" /><circle cx=\"18.67\" cy=\"12\" r=\"1.33\" fill=\"currentColor\" stroke=\"none\" />",
116
+ 'utilisateur': "<circle cx=\"12\" cy=\"8\" r=\"3.33\" /><path d=\"M5.33 20 C5.33 15.33 8.33 13.33 12 13.33 C15.67 13.33 18.67 15.33 18.67 20\" />",
75
117
  }
76
118
 
77
119
  /** Nom lisible de chaque icône — sert de titre accessible par défaut à
@@ -79,28 +121,45 @@ export const CORPS: Record<IconId, string> = {
79
121
  export const NOMS: Record<IconId, string> = {
80
122
  'accueil': "Accueil",
81
123
  'activite': "Activité (pouls)",
124
+ 'aide': "Aide",
82
125
  'app-couches': "Application (couches)",
83
126
  'archiver': "Archiver",
127
+ 'attention': "Attention (avertissement)",
128
+ 'balance': "Balance (justice)",
84
129
  'base-de-donnees': "Base de données",
85
130
  'bouclier-verifie': "Bouclier vérifié (authentification)",
86
131
  'cadenas': "Cadenas (secret)",
87
132
  'cadrer': "Cadrer (ajuster à la vue)",
133
+ 'calendrier': "Calendrier",
88
134
  'chevron-bas': "Chevron bas",
89
135
  'chevron-droit': "Chevron droit",
90
136
  'chevron-gauche': "Chevron gauche",
137
+ 'chevron-haut': "Chevron haut",
91
138
  'coche': "Coche",
92
139
  'copier': "Copier",
93
140
  'corbeille': "Corbeille (supprimer)",
141
+ 'courriel': "Courriel",
94
142
  'cube': "Cube (stockage objet)",
95
143
  'deconnexion': "Déconnexion",
96
144
  'depot-fichier': "Dépôt de fichier (zone de glisser-déposer)",
145
+ 'diplome': "Diplôme (Campus)",
97
146
  'dossier': "Dossier",
98
147
  'eclair': "Éclair (action rapide)",
148
+ 'envoyer': "Envoyer",
149
+ 'epingle': "Épingle (lieu)",
99
150
  'equipe': "Équipe (membres)",
100
151
  'etincelles': "Étincelles (IA)",
152
+ 'etoile': "Étoile (favori)",
101
153
  'fermer': "Fermer",
154
+ 'fichier': "Fichier (document)",
155
+ 'filtre': "Filtre",
156
+ 'globe': "Globe (international)",
157
+ 'graphique': "Graphique (barres)",
102
158
  'horloge': "Horloge (intégration continue)",
159
+ 'imprimante': "Imprimante",
160
+ 'info': "Information",
103
161
  'lien-externe': "Lien externe",
162
+ 'livre-ouvert': "Livre ouvert (formation)",
104
163
  'menu': "Menu (hamburger)",
105
164
  'moins': "Moins",
106
165
  'oeil': "Œil (afficher)",
@@ -110,5 +169,9 @@ export const NOMS: Record<IconId, string> = {
110
169
  'reglages': "Réglages",
111
170
  'reseau': "Réseau (entrée / globe)",
112
171
  'taches': "Tâches (liste à cocher)",
172
+ 'telecharger': "Télécharger",
173
+ 'telephone': "Téléphone",
174
+ 'tri': "Tri",
113
175
  'trois-points': "Trois points (menu contextuel)",
176
+ 'utilisateur': "Utilisateur",
114
177
  }
package/react/icon.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // Icônes — jeu de 34, tracées, sur grille de 24.
1
+ // Icônes — jeu de 55, tracées, sur grille de 24.
2
2
  //
3
3
  // Le corps du tracé est INLINÉ (voir scripts/build-icon-react.mjs) : pas de
4
4
  // <use href="…sprite.svg">, donc rien à résoudre côté bundler et rien à
@@ -12,7 +12,7 @@ import { CORPS, NOMS, type IconId } from './icon-data.js'
12
12
  export type { IconId }
13
13
  export { NOMS as NOMS_ICONES }
14
14
 
15
- /** Les 34 identifiants, triés — pour construire une planche ou un sélecteur. */
15
+ /** Les 55 identifiants, triés — pour construire une planche ou un sélecteur. */
16
16
  export const ICONES = Object.keys(CORPS) as IconId[]
17
17
 
18
18
  export interface IconProps