@ktortu/aaa 0.1.0-beta.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +27 -9
  2. package/cdk/styles/tabs.css +100 -21
  3. package/fesm2022/ktortu-aaa-button.mjs +18 -11
  4. package/fesm2022/ktortu-aaa-button.mjs.map +1 -1
  5. package/fesm2022/ktortu-aaa-card.mjs +29 -4
  6. package/fesm2022/ktortu-aaa-card.mjs.map +1 -1
  7. package/fesm2022/ktortu-aaa-cdk.mjs +59 -15
  8. package/fesm2022/ktortu-aaa-cdk.mjs.map +1 -1
  9. package/fesm2022/ktortu-aaa-dialog.mjs +134 -24
  10. package/fesm2022/ktortu-aaa-dialog.mjs.map +1 -1
  11. package/fesm2022/ktortu-aaa-forms.mjs +748 -294
  12. package/fesm2022/ktortu-aaa-forms.mjs.map +1 -1
  13. package/fesm2022/ktortu-aaa-i18n.mjs +139 -0
  14. package/fesm2022/ktortu-aaa-i18n.mjs.map +1 -0
  15. package/fesm2022/ktortu-aaa-menu.mjs +38 -13
  16. package/fesm2022/ktortu-aaa-menu.mjs.map +1 -1
  17. package/fesm2022/ktortu-aaa-snackbar.mjs +465 -0
  18. package/fesm2022/ktortu-aaa-snackbar.mjs.map +1 -0
  19. package/fesm2022/ktortu-aaa-tabs.mjs +319 -42
  20. package/fesm2022/ktortu-aaa-tabs.mjs.map +1 -1
  21. package/fesm2022/ktortu-aaa-tooltip.mjs +3 -2
  22. package/fesm2022/ktortu-aaa-tooltip.mjs.map +1 -1
  23. package/fesm2022/ktortu-aaa.mjs +2 -0
  24. package/fesm2022/ktortu-aaa.mjs.map +1 -1
  25. package/forms/checkbox/checkbox-group.css +0 -8
  26. package/forms/chips/chip-list.css +5 -0
  27. package/forms/radio/radio-group.css +0 -8
  28. package/forms/styles/field-box.css +152 -2
  29. package/forms/styles/field-pending.css +46 -0
  30. package/forms/styles/select-panel.css +4 -0
  31. package/forms/styles/tokens.css +3 -0
  32. package/menu/menu.css +8 -4
  33. package/package.json +9 -1
  34. package/snackbar/snackbar-tokens.css +53 -0
  35. package/snackbar/snackbar.css +175 -0
  36. package/styles/forms.css +1 -0
  37. package/styles/snackbar.css +9 -0
  38. package/styles/styles.css +1 -0
  39. package/types/ktortu-aaa-button.d.ts +22 -8
  40. package/types/ktortu-aaa-card.d.ts +24 -4
  41. package/types/ktortu-aaa-cdk.d.ts +38 -0
  42. package/types/ktortu-aaa-dialog.d.ts +129 -31
  43. package/types/ktortu-aaa-forms.d.ts +503 -160
  44. package/types/ktortu-aaa-i18n.d.ts +77 -0
  45. package/types/ktortu-aaa-menu.d.ts +15 -8
  46. package/types/ktortu-aaa-snackbar.d.ts +275 -0
  47. package/types/ktortu-aaa-tabs.d.ts +130 -13
  48. package/types/ktortu-aaa-tooltip.d.ts +5 -0
  49. package/types/ktortu-aaa.d.ts +2 -0
@@ -1 +1 @@
1
- {"version":3,"file":"ktortu-aaa.mjs","sources":["../../../../projects/ktortu/aaa/src/public-api.ts","../../../../projects/ktortu/aaa/src/ktortu-aaa.ts"],"sourcesContent":["/*\n * Public API Surface of @ktortu/aaa\n */\nexport * from '@ktortu/aaa/button';\nexport * from '@ktortu/aaa/card';\nexport * from '@ktortu/aaa/cdk';\nexport * from '@ktortu/aaa/dialog';\nexport * from '@ktortu/aaa/forms';\nexport * from '@ktortu/aaa/menu';\nexport * from '@ktortu/aaa/tabs';\nexport * from '@ktortu/aaa/tooltip';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;;;;;;;;AAAA;;AAEG;;ACFH;;AAEG"}
1
+ {"version":3,"file":"ktortu-aaa.mjs","sources":["../../../../projects/ktortu/aaa/src/public-api.ts","../../../../projects/ktortu/aaa/src/ktortu-aaa.ts"],"sourcesContent":["/*\n * Public API Surface of @ktortu/aaa\n */\nexport * from '@ktortu/aaa/button';\nexport * from '@ktortu/aaa/card';\nexport * from '@ktortu/aaa/cdk';\nexport * from '@ktortu/aaa/dialog';\nexport * from '@ktortu/aaa/forms';\nexport * from '@ktortu/aaa/i18n';\nexport * from '@ktortu/aaa/menu';\nexport * from '@ktortu/aaa/snackbar';\nexport * from '@ktortu/aaa/tabs';\nexport * from '@ktortu/aaa/tooltip';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;;;;;;;;;;AAAA;;AAEG;;ACFH;;AAEG"}
@@ -21,14 +21,6 @@
21
21
  color: var(--field-required-color, var(--kt-danger, #8c1d18));
22
22
  }
23
23
 
24
- /* Disposition des options : pilotable par token (colonne par défaut, ligne possible). */
25
- .kt-checkbox-group__options {
26
- display: flex;
27
- flex-direction: var(--checkbox-group-direction, column);
28
- flex-wrap: wrap;
29
- gap: var(--checkbox-group-gap, 0.25rem);
30
- }
31
-
32
24
  .kt-checkbox-group__hint {
33
25
  margin: 0;
34
26
  font-size: var(--field-hint-font-size, 0.8125rem);
@@ -52,6 +52,11 @@
52
52
  /* Texte réservé aux lecteurs d'écran (live region des retraits). */
53
53
  .kt-chip-list__status {
54
54
  position: absolute;
55
+ /* Ancrée au coin de l'hôte : sans `inset`, un `position: absolute` sans ancêtre positionné
56
+ retombe sur sa position STATIQUE (bas de la liste). Dans un conteneur scrollable, ce 1px
57
+ atterrit loin dans le document et crée une 2ᵉ barre de défilement fantôme. */
58
+ inset-block-start: 0;
59
+ inset-inline-start: 0;
55
60
  inline-size: 1px;
56
61
  block-size: 1px;
57
62
  padding: 0;
@@ -21,14 +21,6 @@
21
21
  color: var(--field-required-color, var(--kt-danger, #8c1d18));
22
22
  }
23
23
 
24
- /* Disposition des options : pilotable par token (colonne par défaut, ligne possible). */
25
- .kt-radio-group__options {
26
- display: flex;
27
- flex-direction: var(--radio-group-direction, column);
28
- flex-wrap: wrap;
29
- gap: var(--radio-group-gap, 0.25rem);
30
- }
31
-
32
24
  .kt-radio-group__hint {
33
25
  margin: 0;
34
26
  font-size: var(--field-hint-font-size, 0.8125rem);
@@ -1,9 +1,22 @@
1
1
  @layer kt-aaa.components {
2
- /* Boîte de contrôle partagée par les champs simples (TextField, NumberField).
2
+ /* Boîte de contrôle partagée par les champs simples (TextField, NumberField) et la famille
3
+ Temporal (Date/Time/DateTime/YearMonth/Instant), qui réutilise la même `.kt-field-box`.
3
4
  Globale + classes namespacées : partagée entre plusieurs composants, donc non scopable. */
4
5
  kt-text-field,
5
- kt-number-field {
6
+ kt-text-area,
7
+ kt-number-field,
8
+ kt-date-field,
9
+ kt-time-field,
10
+ kt-date-time-field,
11
+ kt-year-month-field,
12
+ kt-instant-field,
13
+ kt-select,
14
+ kt-multi-select {
6
15
  display: block;
16
+ /* Largeur pilotable d'un coup via --field-width. Défaut `auto` : remplit le conteneur en
17
+ contexte bloc (formulaire), se dimensionne au contenu en flex. Surcharger sur :root ou un
18
+ conteneur pour fixer tous les champs (ex. --field-width: 320px). */
19
+ width: var(--field-width, auto);
7
20
  }
8
21
 
9
22
  .kt-field-box {
@@ -168,4 +181,141 @@
168
181
  field-sizing: content;
169
182
  resize: vertical;
170
183
  }
184
+
185
+ /* =========================================================================
186
+ Apparence OUTLINE (opt-in via [appearance]="'outline'" ou KT_FIELD_CONFIG).
187
+ Label flottant qui se loge dans la bordure haute de la boîte (façon Material).
188
+ Rendu GLOBAL (et non dans field.css scopé) car il référence .kt-field-box*,
189
+ contenu PROJETÉ depuis le composant de champ — inatteignable en CSS scopé.
190
+ L'apparence par défaut (fill) n'est jamais touchée.
191
+ ========================================================================= */
192
+ kt-field[data-appearance='outline'] {
193
+ position: relative;
194
+ margin-top: 0.5rem; /* espace pour le label une fois flotté au-dessus de la bordure */
195
+ }
196
+
197
+ /* Le header (label + aide) recouvre la boîte sans réserver de hauteur en flux. */
198
+ kt-field[data-appearance='outline'] .kt-field__header {
199
+ position: absolute;
200
+ inset: 0 0 auto 0;
201
+ height: var(--field-min-height, 44px);
202
+ margin: 0;
203
+ padding-inline: var(--field-padding-x, 0.75rem);
204
+ z-index: 1;
205
+ pointer-events: none; /* la boîte/l'input dessous restent cliquables */
206
+ }
207
+
208
+ /* Label au repos = position « placeholder » (centré verticalement dans la boîte). */
209
+ kt-field[data-appearance='outline'] .kt-field__label {
210
+ position: absolute;
211
+ inset-inline-start: var(--field-padding-x, 0.75rem);
212
+ top: 50%;
213
+ transform: translateY(-50%);
214
+ transform-origin: left center;
215
+ max-width: calc(100% - 2 * var(--field-padding-x, 0.75rem));
216
+ overflow: hidden;
217
+ white-space: nowrap;
218
+ text-overflow: ellipsis;
219
+ background: transparent;
220
+ padding-inline: 0.25rem;
221
+ font-size: var(--field-font-size, 1rem);
222
+ font-weight: 400;
223
+ color: var(--field-hint-color, #5f6368);
224
+ transition:
225
+ transform 0.15s ease,
226
+ color 0.15s ease;
227
+ }
228
+
229
+ /* RTL : `transform-origin` n'accepte pas de mot-clé logique → on bascule l'origine de la mise à
230
+ l'échelle vers le bord de départ (droite en RTL) pour rester aligné sur `inset-inline-start`. */
231
+ [dir='rtl'] kt-field[data-appearance='outline'] .kt-field__label {
232
+ transform-origin: right center;
233
+ }
234
+
235
+ /* Bouton d'aide : recliquable et calé à droite du header. */
236
+ kt-field[data-appearance='outline'] .kt-field__help {
237
+ margin-inline-start: auto;
238
+ pointer-events: auto;
239
+ }
240
+
241
+ /* Label flotté — DEUX règles séparées à dessein (dégradation gracieuse) : la 1re (focus / always)
242
+ reste valide même si le moteur ne supporte pas `:has()`. Une seule pseudo-classe inconnue dans une
243
+ liste de sélecteurs invaliderait TOUTE la liste → le label ne flotterait jamais, focus compris.
244
+ La 2nde ajoute les cas qui exigent `:has()` (input rempli, select `[data-filled]`). Les inputs
245
+ date/heure n'ayant pas de placeholder CSS, `:not(:placeholder-shown)` les garde flottés en permanence. */
246
+ kt-field[data-appearance='outline'][data-float-label='always'] .kt-field__label,
247
+ kt-field[data-appearance='outline']:focus-within .kt-field__label {
248
+ transform: translateY(-50%) translateY(calc(var(--field-min-height, 44px) / -2)) scale(0.75);
249
+ color: var(--field-label-color, inherit);
250
+ /* Fond du label flotté : masque la ligne d'outline sous le label. Défaut : fond PLEIN du champ.
251
+ Le consommateur peut poser un gradient (ex. `linear-gradient(transparent 50%, var(--field-bg) 50%)`)
252
+ via le token --field-label-float-bg. */
253
+ background: var(--field-label-float-bg, var(--field-bg, #fff));
254
+ }
255
+ kt-field[data-appearance='outline']:has(.kt-field-box[data-filled]) .kt-field__label,
256
+ kt-field[data-appearance='outline']:has(.kt-field-box__input:not(:placeholder-shown)) .kt-field__label {
257
+ transform: translateY(-50%) translateY(calc(var(--field-min-height, 44px) / -2)) scale(0.75);
258
+ color: var(--field-label-color, inherit);
259
+ background: var(--field-label-float-bg, var(--field-bg, #fff));
260
+ }
261
+
262
+ /* Bordure de l'outline : token dédié (état de repos). Surcharge possible indépendamment du
263
+ mode `fill`. Les états hover/focus/invalid gardent leurs propres tokens (réaffirmés ici
264
+ pour rester prioritaires sur la règle de repos). */
265
+ kt-field[data-appearance='outline'] .kt-field-box {
266
+ border-color: var(--field-outline-border-color, var(--field-border-color, #c4c7c5));
267
+ }
268
+ kt-field[data-appearance='outline'] .kt-field-box:hover {
269
+ border-color: var(--field-border-color-hover, #5f6368);
270
+ }
271
+ kt-field[data-appearance='outline'] .kt-field-box:focus-within {
272
+ border-color: var(--field-border-color-focus, #0b57d0);
273
+ }
274
+ kt-field[data-appearance='outline'] .kt-field-box[data-invalid] {
275
+ border-color: var(--field-error-color, #b3261e);
276
+ }
277
+
278
+ /* En outline AUTO, le label flottant tient lieu de placeholder au repos : on masque le placeholder
279
+ propre du select (sinon doublon avec le label en position « placeholder »). En ALWAYS, le label
280
+ est déjà en haut → on laisse le placeholder du select s'afficher. */
281
+ kt-field[data-appearance='outline']:not([data-float-label='always']) .kt-select__placeholder {
282
+ visibility: hidden;
283
+ }
284
+
285
+ /* En outline, le placeholder natif ne s'affiche QUE lorsque le label flotte : au repos le label
286
+ tient lieu de placeholder (sinon les deux se superposeraient). Masqué par défaut, révélé au
287
+ focus ou en floatLabel='always'. (Quand le champ est rempli, le navigateur ne le rend pas.) */
288
+ kt-field[data-appearance='outline'] .kt-field-box__input::placeholder {
289
+ color: transparent;
290
+ }
291
+ kt-field[data-appearance='outline']:focus-within .kt-field-box__input::placeholder,
292
+ kt-field[data-appearance='outline'][data-float-label='always'] .kt-field-box__input::placeholder {
293
+ color: var(--field-placeholder-color, var(--field-hint-color, #5f6368));
294
+ }
295
+
296
+ /* Accent au focus / état d'erreur. */
297
+ kt-field[data-appearance='outline']:focus-within .kt-field__label {
298
+ color: var(--field-border-color-focus, #0b57d0);
299
+ }
300
+ kt-field[data-appearance='outline']:has(.kt-field-box[data-invalid]) .kt-field__label {
301
+ color: var(--field-error-color, #b3261e);
302
+ }
303
+
304
+ @media (prefers-reduced-motion: reduce) {
305
+ kt-field[data-appearance='outline'] .kt-field__label {
306
+ transition: none;
307
+ }
308
+ }
309
+
310
+ /* Contraste forcé (Windows High Contrast) : le fond du label flotté masque la bordure ; un gradient
311
+ custom `--field-label-float-bg` serait ignoré → on force une couleur système opaque pour garantir
312
+ le « notch ». Au repos le label est transparent (non listé), donc rien à forcer. */
313
+ @media (forced-colors: active) {
314
+ kt-field[data-appearance='outline'][data-float-label='always'] .kt-field__label,
315
+ kt-field[data-appearance='outline']:focus-within .kt-field__label,
316
+ kt-field[data-appearance='outline']:has(.kt-field-box[data-filled]) .kt-field__label,
317
+ kt-field[data-appearance='outline']:has(.kt-field-box__input:not(:placeholder-shown)) .kt-field__label {
318
+ background: Canvas;
319
+ }
320
+ }
171
321
  }
@@ -0,0 +1,46 @@
1
+ @layer kt-aaa.components {
2
+ /* Spinner « validation asynchrone en cours » par défaut.
3
+
4
+ 100 % CSS (pseudo-élément, aucun markup) et piloté par l'attribut `[data-pending]` posé par les
5
+ contrôles quand leur input `pending` est vrai (l'accessibilité passe par `aria-busy`, indépendant).
6
+
7
+ Entièrement surchargeable / suppressible côté consommateur :
8
+ - retailler/recolorer via les tokens `--field-spinner-*` ;
9
+ - supprimer : kt-text-field .kt-field-box[data-pending]::after { content: none; }
10
+ - remplacer par sa propre affordance (le hook `[data-pending]` reste disponible). */
11
+ @keyframes kt-field-spin {
12
+ to {
13
+ transform: rotate(1turn);
14
+ }
15
+ }
16
+
17
+ .kt-field-box[data-pending]::after,
18
+ .kt-switch-field[data-pending] .kt-switch-row::after,
19
+ .kt-checkbox-field[data-pending] .kt-checkbox::after,
20
+ .kt-radio-group-field[data-pending] .kt-radio-group__legend::after,
21
+ .kt-checkbox-group-field[data-pending] .kt-checkbox-group__legend::after {
22
+ content: '';
23
+ display: inline-block;
24
+ flex: none;
25
+ vertical-align: middle;
26
+ inline-size: var(--field-spinner-size, 1em);
27
+ block-size: var(--field-spinner-size, 1em);
28
+ margin-inline-start: var(--field-spinner-gap, 0.5rem);
29
+ border: var(--field-spinner-width, 2px) solid var(--field-spinner-track, currentColor);
30
+ border-top-color: var(--field-spinner-color, var(--field-border-color-focus, #1a73e8));
31
+ border-radius: 50%;
32
+ opacity: var(--field-spinner-opacity, 0.7);
33
+ animation: kt-field-spin var(--field-spinner-duration, 0.7s) linear infinite;
34
+ }
35
+
36
+ /* Respect de prefers-reduced-motion : on fige la rotation (le cercle reste visible comme indicateur). */
37
+ @media (prefers-reduced-motion: reduce) {
38
+ .kt-field-box[data-pending]::after,
39
+ .kt-switch-field[data-pending] .kt-switch-row::after,
40
+ .kt-checkbox-field[data-pending] .kt-checkbox::after,
41
+ .kt-radio-group-field[data-pending] .kt-radio-group__legend::after,
42
+ .kt-checkbox-group-field[data-pending] .kt-checkbox-group__legend::after {
43
+ animation: none;
44
+ }
45
+ }
46
+ }
@@ -178,6 +178,10 @@
178
178
  /* Texte réservé aux lecteurs d'écran (live region du nombre de résultats). */
179
179
  .kt-select__sr-only {
180
180
  position: absolute;
181
+ /* Ancrée au coin : sinon ce 1px `absolute` retombe sur sa position statique et, dans un
182
+ conteneur scrollable, étire le document → barre de défilement fantôme. */
183
+ inset-block-start: 0;
184
+ inset-inline-start: 0;
181
185
  inline-size: 1px;
182
186
  block-size: 1px;
183
187
  padding: 0;
@@ -35,6 +35,9 @@
35
35
  --field-padding-y: 0.5rem;
36
36
  --field-gap: 0.375rem;
37
37
  --field-control-gap: 0.5rem;
38
+ /* Largeur du champ : un seul réglage pilote tous les champs. Défaut `auto` (remplit le
39
+ conteneur en bloc, se dimensionne au contenu en flex). Ex. global : --field-width: 320px. */
40
+ --field-width: auto;
38
41
 
39
42
  /* Typo */
40
43
  --field-font-size: var(--kt-control-font);
package/menu/menu.css CHANGED
@@ -42,8 +42,11 @@
42
42
  [ktMenu] {
43
43
  position: fixed;
44
44
  inset: auto;
45
- inset-block-start: anchor(block-end);
46
- inset-inline-start: anchor(inline-start);
45
+ /* Dropdown sous le déclencheur, aligné sur son bord inline-start. On utilise `position-area`
46
+ et NON la fonction `anchor()` : `anchor()` résout mal une ancre imbriquée dans un sous-arbre
47
+ déjà ancré → les sous-menus s'EMPILENT dès le 3ᵉ niveau. `position-area` cascade correctement
48
+ à n'importe quelle profondeur (validé Chrome 125+ / Firefox 147+ / Safari 26+). */
49
+ position-area: block-end span-inline-end;
47
50
  margin-block-start: var(--kt-menu-offset, 0.25rem);
48
51
  /* Bascule de côté / au-dessus si le bas/droite manque de place. */
49
52
  position-try-fallbacks:
@@ -61,8 +64,9 @@
61
64
  --_submenu-offset: calc(
62
65
  var(--kt-menu-padding, 0.25rem) + var(--kt-menu-border-width, 1px) + var(--kt-menu-submenu-gap, 0.375rem)
63
66
  );
64
- inset-block-start: anchor(block-start);
65
- inset-inline-start: anchor(inline-end);
67
+ /* À l'inline-end de l'item parent, aligné sur son haut. `position-area` (et non `anchor()`)
68
+ pour cascader correctement à toute profondeur — cf. note sur le menu racine ci-dessus. */
69
+ position-area: inline-end span-block-end;
66
70
  margin-block-start: 0;
67
71
  margin-inline-start: var(--_submenu-offset);
68
72
  position-try-fallbacks:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ktortu/aaa",
3
- "version": "0.1.0-beta.0",
3
+ "version": "0.9.1",
4
4
  "description": "Bibliothèque de composants Angular headless + thémés par tokens CSS.",
5
5
  "keywords": [
6
6
  "angular",
@@ -77,10 +77,18 @@
77
77
  "types": "./types/ktortu-aaa-forms.d.ts",
78
78
  "default": "./fesm2022/ktortu-aaa-forms.mjs"
79
79
  },
80
+ "./i18n": {
81
+ "types": "./types/ktortu-aaa-i18n.d.ts",
82
+ "default": "./fesm2022/ktortu-aaa-i18n.mjs"
83
+ },
80
84
  "./menu": {
81
85
  "types": "./types/ktortu-aaa-menu.d.ts",
82
86
  "default": "./fesm2022/ktortu-aaa-menu.mjs"
83
87
  },
88
+ "./snackbar": {
89
+ "types": "./types/ktortu-aaa-snackbar.d.ts",
90
+ "default": "./fesm2022/ktortu-aaa-snackbar.mjs"
91
+ },
84
92
  "./tabs": {
85
93
  "types": "./types/ktortu-aaa-tabs.d.ts",
86
94
  "default": "./fesm2022/ktortu-aaa-tabs.mjs"
@@ -0,0 +1,53 @@
1
+ /* Design tokens de la snackbar (service KtSnackbar) — contrat public de theming.
2
+
3
+ PRINCIPE (identique au reste de la lib) : tout DÉRIVE du socle --kt-*. Surcharger un --kt-*
4
+ rethématise la snackbar en même temps que les champs, boutons et dialog. Chaque valeur reste
5
+ surchargeable en redéclarant son token --snackbar-* (sur :root ou un conteneur de spécificité ≥).
6
+
7
+ La snackbar est posée sur une « surface inversée » (fond sombre, texte clair) façon Material —
8
+ d'où des on-* dérivés de --kt-on-surface / --kt-surface, qui suivent automatiquement un thème.
9
+ Les variantes ne recolorisent PAS la pastille (le texte reste à contraste AAA) : elles ne pilotent
10
+ que l'accent + l'icône. Un thème qui veut des pastilles pleinement colorées surcharge simplement
11
+ --snackbar-bg / --snackbar-fg sous [data-variant='…']. */
12
+ @layer kt-aaa.tokens {
13
+ :root {
14
+ /* Surface inversée : fond ~ on-surface, texte ~ surface (contraste ≈ 16:1 → AAA 1.4.6). */
15
+ --snackbar-bg: color-mix(in oklab, var(--kt-on-surface, #1f1f1f) 92%, var(--kt-surface, #ffffff));
16
+ --snackbar-fg: var(--kt-surface, #ffffff);
17
+
18
+ --snackbar-radius: var(--kt-control-radius, 8px);
19
+ --snackbar-shadow: 0 4px 12px rgb(0 0 0 / 24%);
20
+
21
+ /* Géométrie interne */
22
+ --snackbar-padding-block: 0.625rem;
23
+ --snackbar-padding-inline: 1rem;
24
+ --snackbar-gap: 0.75rem;
25
+ --snackbar-font: var(--kt-control-font, 1rem);
26
+
27
+ /* Placement : distance au bord du viewport, avec marge de sécurité mobile (encoches / barres). */
28
+ --snackbar-offset: 16px;
29
+ --snackbar-offset-bottom: calc(var(--snackbar-offset) + env(safe-area-inset-bottom, 0px));
30
+ --snackbar-offset-top: calc(var(--snackbar-offset) + env(safe-area-inset-top, 0px));
31
+ --snackbar-max-inline-size: 560px;
32
+
33
+ /* Bouton de fermeture : cible 44px (AAA 2.5.5). */
34
+ --snackbar-close-size: var(--kt-control-height, 44px);
35
+ --snackbar-close-icon-size: 1rem;
36
+
37
+ /* Variantes : accent (icône) clair pour contraster sur la pastille sombre (≥ 3:1), et glyphe
38
+ (ligature de la police d'icônes --kt-icon-font). Couleur + FORME : la forme distincte est
39
+ l'indice non coloré (WCAG 1.4.1). Surchargez accent/glyphe pour rebrander une variante. */
40
+ --snackbar-icon-size: 1.25rem;
41
+ --snackbar-accent-info: #8ab4f8;
42
+ --snackbar-accent-success: #81c995;
43
+ --snackbar-accent-warning: #fdd663;
44
+ --snackbar-accent-error: #f28b82;
45
+ --snackbar-glyph-info: 'info';
46
+ --snackbar-glyph-success: 'check_circle';
47
+ --snackbar-glyph-warning: 'warning';
48
+ --snackbar-glyph-error: 'error';
49
+
50
+ /* Animation d'entrée/sortie. Token de durée surchargeable ; neutralisée en prefers-reduced-motion. */
51
+ --snackbar-anim-duration: 150ms;
52
+ }
53
+ }
@@ -0,0 +1,175 @@
1
+ @layer kt-aaa.components {
2
+ /* Styles de la snackbar (conteneur monté dans un overlay CDK).
3
+ Global : l'élément est ajouté hors de toute vue (overlay top-layer).
4
+ Tokens (--snackbar-*) définis dans snackbar-tokens.css (chargé avant ce fichier).
5
+
6
+ Requiert côté hôte : '@angular/cdk/overlay-prebuilt.css' (positionnement de l'overlay) ET
7
+ '@angular/cdk/a11y-prebuilt.css' (masque l'élément du LiveAnnouncer qui porte l'annonce). */
8
+
9
+ /* Le pane d'overlay ne capte pas les clics hors de la snackbar (feedback NON bloquant). */
10
+ .kt-snackbar-pane {
11
+ pointer-events: none;
12
+ max-inline-size: 100vw;
13
+ }
14
+
15
+ .kt-snackbar {
16
+ pointer-events: auto; /* la snackbar elle-même reste survolable / cliquable */
17
+ display: flex;
18
+ align-items: center;
19
+ gap: var(--snackbar-gap);
20
+ box-sizing: border-box;
21
+
22
+ margin: var(--snackbar-offset);
23
+ inline-size: max-content;
24
+ max-inline-size: min(var(--snackbar-max-inline-size), 100vw - 2 * var(--snackbar-offset));
25
+
26
+ padding-block: var(--snackbar-padding-block);
27
+ padding-inline: var(--snackbar-padding-inline);
28
+
29
+ border-radius: var(--snackbar-radius);
30
+ background: var(--snackbar-bg);
31
+ color: var(--snackbar-fg);
32
+ box-shadow: var(--snackbar-shadow);
33
+
34
+ font-size: var(--snackbar-font);
35
+ line-height: 1.4;
36
+ }
37
+
38
+ /* Marge de sécurité mobile (encoches / barres système) selon le bord d'ancrage. */
39
+ .kt-snackbar-pane--bottom .kt-snackbar {
40
+ margin-block-end: var(--snackbar-offset-bottom);
41
+ }
42
+ .kt-snackbar-pane--top .kt-snackbar {
43
+ margin-block-start: var(--snackbar-offset-top);
44
+ }
45
+
46
+ .kt-snackbar__message {
47
+ flex: 1 1 auto;
48
+ min-inline-size: 0; /* autorise le retour à la ligne d'un message long */
49
+ }
50
+
51
+ /* ============ ICÔNE DE VARIANTE (décorative, aria-hidden) ============ */
52
+ /* Couleur = accent de la variante ; glyphe = ligature de la police d'icônes (comme [ktButton]).
53
+ Si la police d'icônes est absente, la ligature dégrade en texte (même comportement que le bouton). */
54
+ .kt-snackbar__icon {
55
+ flex: none;
56
+ display: inline-grid;
57
+ place-items: center;
58
+ color: var(--snackbar-accent, currentColor);
59
+ }
60
+ .kt-snackbar__icon::before {
61
+ content: var(--snackbar-glyph, '');
62
+ font-family: var(--kt-icon-font, 'Material Symbols Outlined');
63
+ font-weight: var(--kt-icon-font-weight, normal);
64
+ font-style: normal;
65
+ font-size: var(--snackbar-icon-size, 1.25rem);
66
+ line-height: 1;
67
+ font-feature-settings: 'liga';
68
+ -webkit-font-smoothing: antialiased;
69
+ }
70
+
71
+ /* Mapping variante → accent + glyphe (un thème surcharge accent/glyphe, ou bg/fg pour recolorer). */
72
+ .kt-snackbar[data-variant='info'] {
73
+ --snackbar-accent: var(--snackbar-accent-info);
74
+ --snackbar-glyph: var(--snackbar-glyph-info);
75
+ }
76
+ .kt-snackbar[data-variant='success'] {
77
+ --snackbar-accent: var(--snackbar-accent-success);
78
+ --snackbar-glyph: var(--snackbar-glyph-success);
79
+ }
80
+ .kt-snackbar[data-variant='warning'] {
81
+ --snackbar-accent: var(--snackbar-accent-warning);
82
+ --snackbar-glyph: var(--snackbar-glyph-warning);
83
+ }
84
+ .kt-snackbar[data-variant='error'] {
85
+ --snackbar-accent: var(--snackbar-accent-error);
86
+ --snackbar-glyph: var(--snackbar-glyph-error);
87
+ }
88
+
89
+ /* ============ BOUTON DE FERMETURE ============ */
90
+ /* Cible 44px, croix dessinée en CSS (zéro dépendance à une police d'icônes). */
91
+ .kt-snackbar__close {
92
+ position: relative;
93
+ flex: none;
94
+ inline-size: var(--snackbar-close-size);
95
+ block-size: var(--snackbar-close-size);
96
+ /* compense optiquement le padding inline du conteneur, sans réduire la cible de 44px */
97
+ margin-inline-end: calc(-0.5 * var(--snackbar-padding-inline));
98
+
99
+ display: inline-grid;
100
+ place-items: center;
101
+ padding: 0;
102
+ border: 0;
103
+ border-radius: 50%;
104
+ background: transparent;
105
+ color: inherit;
106
+ cursor: pointer;
107
+ }
108
+
109
+ .kt-snackbar__close::before,
110
+ .kt-snackbar__close::after {
111
+ content: '';
112
+ position: absolute;
113
+ inline-size: var(--snackbar-close-icon-size);
114
+ block-size: 2px;
115
+ border-radius: 2px;
116
+ background: currentColor;
117
+ translate: 0 0;
118
+ }
119
+ .kt-snackbar__close::before {
120
+ rotate: 45deg;
121
+ }
122
+ .kt-snackbar__close::after {
123
+ rotate: -45deg;
124
+ }
125
+
126
+ .kt-snackbar__close:hover {
127
+ background: color-mix(in srgb, currentColor 14%, transparent);
128
+ }
129
+ .kt-snackbar__close:focus-visible {
130
+ outline: var(--kt-focus-ring-width, 2px) solid currentColor;
131
+ outline-offset: 2px;
132
+ }
133
+
134
+ /* ============ ANIMATION D'ENTRÉE / SORTIE ============ */
135
+ /* Entrée par keyframe (glissement depuis le bord), sortie par transition (classe --leaving).
136
+ Toutes deux sont activées UNIQUEMENT hors prefers-reduced-motion → en reduced-motion la durée
137
+ de transition est nulle, donc playExit() démonte instantanément (WCAG 2.3.3). */
138
+ @media (prefers-reduced-motion: no-preference) {
139
+ .kt-snackbar {
140
+ transition:
141
+ opacity var(--snackbar-anim-duration) ease,
142
+ translate var(--snackbar-anim-duration) ease;
143
+ }
144
+ .kt-snackbar-pane--bottom .kt-snackbar {
145
+ animation: kt-snackbar-in-bottom var(--snackbar-anim-duration) ease-out;
146
+ }
147
+ .kt-snackbar-pane--top .kt-snackbar {
148
+ animation: kt-snackbar-in-top var(--snackbar-anim-duration) ease-out;
149
+ }
150
+
151
+ /* Sortie : on fond + on glisse vers le bord d'ancrage. */
152
+ .kt-snackbar--leaving {
153
+ opacity: 0;
154
+ }
155
+ .kt-snackbar-pane--bottom .kt-snackbar--leaving {
156
+ translate: 0 100%;
157
+ }
158
+ .kt-snackbar-pane--top .kt-snackbar--leaving {
159
+ translate: 0 -100%;
160
+ }
161
+ }
162
+
163
+ @keyframes kt-snackbar-in-bottom {
164
+ from {
165
+ opacity: 0;
166
+ translate: 0 100%;
167
+ }
168
+ }
169
+ @keyframes kt-snackbar-in-top {
170
+ from {
171
+ opacity: 0;
172
+ translate: 0 -100%;
173
+ }
174
+ }
175
+ }
package/styles/forms.css CHANGED
@@ -6,6 +6,7 @@
6
6
  @import '../forms/styles/tokens.css';
7
7
  @import '../forms/chips/tokens.css';
8
8
  @import '../forms/styles/field-box.css';
9
+ @import '../forms/styles/field-pending.css';
9
10
  @import '../forms/switch/switch.css';
10
11
  @import '../forms/checkbox/checkbox.css';
11
12
  @import '../forms/checkbox/checkbox-group.css';
@@ -0,0 +1,9 @@
1
+ @layer kt-aaa.tokens, kt-aaa.base, kt-aaa.components;
2
+
3
+ /* Snackbar (service KtSnackbar, overlay CDK). Nécessite le socle (@ktortu/aaa/foundation.css),
4
+ ET côté hôte l'overlay CDK + le masquage du LiveAnnouncer :
5
+ @import '@angular/cdk/overlay-prebuilt.css';
6
+ @import '@angular/cdk/a11y-prebuilt.css';
7
+ @import '@ktortu/aaa/snackbar.css'; */
8
+ @import '../snackbar/snackbar-tokens.css';
9
+ @import '../snackbar/snackbar.css';
package/styles/styles.css CHANGED
@@ -22,3 +22,4 @@
22
22
  @import './tooltip.css';
23
23
  @import './dialog.css';
24
24
  @import './menu.css';
25
+ @import './snackbar.css';