@ktortu/aaa 0.1.0-beta.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.
- package/README.md +23 -7
- package/cdk/styles/tabs.css +100 -21
- package/fesm2022/ktortu-aaa-button.mjs +18 -11
- package/fesm2022/ktortu-aaa-button.mjs.map +1 -1
- package/fesm2022/ktortu-aaa-card.mjs +29 -4
- package/fesm2022/ktortu-aaa-card.mjs.map +1 -1
- package/fesm2022/ktortu-aaa-cdk.mjs +58 -15
- package/fesm2022/ktortu-aaa-cdk.mjs.map +1 -1
- package/fesm2022/ktortu-aaa-dialog.mjs +69 -12
- package/fesm2022/ktortu-aaa-dialog.mjs.map +1 -1
- package/fesm2022/ktortu-aaa-forms.mjs +455 -260
- package/fesm2022/ktortu-aaa-forms.mjs.map +1 -1
- package/fesm2022/ktortu-aaa-i18n.mjs +114 -0
- package/fesm2022/ktortu-aaa-i18n.mjs.map +1 -0
- package/fesm2022/ktortu-aaa-menu.mjs +38 -13
- package/fesm2022/ktortu-aaa-menu.mjs.map +1 -1
- package/fesm2022/ktortu-aaa-tabs.mjs +319 -42
- package/fesm2022/ktortu-aaa-tabs.mjs.map +1 -1
- package/fesm2022/ktortu-aaa-tooltip.mjs +3 -2
- package/fesm2022/ktortu-aaa-tooltip.mjs.map +1 -1
- package/fesm2022/ktortu-aaa.mjs +1 -0
- package/fesm2022/ktortu-aaa.mjs.map +1 -1
- package/forms/radio/radio-group.css +3 -3
- package/forms/styles/field-box.css +150 -2
- package/forms/styles/tokens.css +3 -0
- package/menu/menu.css +8 -4
- package/package.json +5 -1
- package/types/ktortu-aaa-button.d.ts +22 -8
- package/types/ktortu-aaa-card.d.ts +24 -4
- package/types/ktortu-aaa-cdk.d.ts +38 -0
- package/types/ktortu-aaa-dialog.d.ts +45 -9
- package/types/ktortu-aaa-forms.d.ts +336 -149
- package/types/ktortu-aaa-i18n.d.ts +74 -0
- package/types/ktortu-aaa-menu.d.ts +15 -8
- package/types/ktortu-aaa-tabs.d.ts +130 -13
- package/types/ktortu-aaa-tooltip.d.ts +5 -0
- package/types/ktortu-aaa.d.ts +1 -0
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Bibliothèque de composants Angular **headless + thémés par tokens CSS** (`--k
|
|
|
4
4
|
Les composants sont des directives/composants accessibles (appui sur `@angular/aria` / CDK) ;
|
|
5
5
|
leur apparence vit dans des feuilles CSS globales que vous importez à part.
|
|
6
6
|
|
|
7
|
+
> **Démo & documentation vivante** : [ktortu-aaa-demo.web.app](https://ktortu-aaa-demo.web.app/)
|
|
8
|
+
|
|
7
9
|
## Convention de nommage
|
|
8
10
|
|
|
9
11
|
- **Selectors** (templates) : préfixe `kt` — `[ktButton]`, `<kt-text-field>`, `[ktDialogTitle]`…
|
|
@@ -17,7 +19,7 @@ leur apparence vit dans des feuilles CSS globales que vous importez à part.
|
|
|
17
19
|
## Utilisation (TypeScript)
|
|
18
20
|
|
|
19
21
|
Chaque famille est importable depuis son point d'entrée `@ktortu/aaa/<feature>` ; les utilitaires
|
|
20
|
-
transverses (breakpoints, viewport, sheet-drag) depuis
|
|
22
|
+
transverses (breakpoints, viewport, sheet-drag, générateur d'id) depuis `@ktortu/aaa/cdk`.
|
|
21
23
|
|
|
22
24
|
```ts
|
|
23
25
|
import { Component } from '@angular/core';
|
|
@@ -37,11 +39,25 @@ export class Exemple {
|
|
|
37
39
|
}
|
|
38
40
|
```
|
|
39
41
|
|
|
40
|
-
Points d'entrée : `@ktortu/aaa/button`, `/card`, `/dialog`, `/menu`, `/tabs`,
|
|
41
|
-
`/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
Points d'entrée TypeScript : `@ktortu/aaa/button`, `/card`, `/dialog`, `/menu`, `/tabs`,
|
|
43
|
+
`/tooltip`, `/forms`, `/cdk` (breakpoints, viewport, sheet-drag, id-generator) et `/i18n`
|
|
44
|
+
(traductions). La racine `@ktortu/aaa` ré-exporte tout par commodité, mais importer depuis le
|
|
45
|
+
point d'entrée précis préserve le tree-shaking. Les thèmes sont des fichiers CSS (cf. §Styles),
|
|
46
|
+
pas un point d'entrée TypeScript.
|
|
47
|
+
Familles agrégées prêtes pour `imports:` : `KtCardImports`, `KtMenuImports`, `KtDialogImports`
|
|
48
|
+
(ex. `imports: [...KtCardImports]`).
|
|
49
|
+
|
|
50
|
+
### Formulaires & Signal Forms
|
|
51
|
+
|
|
52
|
+
Les contrôles dédiés (`kt-text-field`, `kt-number-field`, `kt-select`, `kt-checkbox`, champs
|
|
53
|
+
temporels…) implémentent le contrat **Signal Forms** (`FormValueControl`) : on les lie via
|
|
54
|
+
`[(value)]` ou via le directive `[formField]` d'`@angular/forms/signals`. Ils n'implémentent PAS
|
|
55
|
+
`ControlValueAccessor` ; pour les intégrer à des `ReactiveForms` existants, passez par le pont
|
|
56
|
+
officiel d'Angular (`compatForm` / `SignalFormControl`, `@angular/forms/signals`) et liez avec
|
|
57
|
+
`[formField]` plutôt que `formControlName`.
|
|
58
|
+
|
|
59
|
+
Pour un contrôle natif quelconque, le chrome de champ générique (`KtField` + `KtFieldControl`)
|
|
60
|
+
l'enveloppe : `<kt-field label="…"><input ktFieldControl [(value)]="…" /></kt-field>`.
|
|
45
61
|
|
|
46
62
|
### Configuration
|
|
47
63
|
|
|
@@ -50,7 +66,7 @@ type `Partial<…>`) ou via les helpers `provideKt*` :
|
|
|
50
66
|
|
|
51
67
|
```ts
|
|
52
68
|
import { KT_BUTTON_CONFIG } from '@ktortu/aaa/button';
|
|
53
|
-
import { provideKtBreakpoints } from '@ktortu/aaa';
|
|
69
|
+
import { provideKtBreakpoints } from '@ktortu/aaa/cdk';
|
|
54
70
|
import { provideKtDialogDefaults } from '@ktortu/aaa/dialog';
|
|
55
71
|
|
|
56
72
|
providers: [
|
package/cdk/styles/tabs.css
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
--tab-height: var(--kt-control-height, 44px);
|
|
10
10
|
--tab-text-color: var(--kt-muted);
|
|
11
11
|
--tab-text-hover: var(--kt-on-surface);
|
|
12
|
+
/* Couleur (et non opacité) de l'état désactivé : idiome de la lib (cf. button.css), évite de
|
|
13
|
+
faire baisser le contraste de tout le sous-arbre. Surchargeable par le thème. */
|
|
14
|
+
--tab-text-disabled: color-mix(in srgb, var(--tab-text-color) 55%, transparent);
|
|
12
15
|
|
|
13
16
|
/* L'état actif prend la couleur primaire du thème par défaut */
|
|
14
17
|
--tab-active-color: var(--kt-primary);
|
|
@@ -40,6 +43,12 @@
|
|
|
40
43
|
--tab-scroll-margin: 1rem;
|
|
41
44
|
--tab-scroll-snap: none; /* `x proximity` pour activer l'accroche */
|
|
42
45
|
--tab-scrollbar-width: thin;
|
|
46
|
+
|
|
47
|
+
/* ============ PAGINATION (directive ktTabScroller / composant kt-tab-scroller) ============ */
|
|
48
|
+
/* Quand la pagination réactive est active, la scrollbar native est masquée (les chevrons +
|
|
49
|
+
ombres prennent le relais). Réafficher la barre via `--tab-scroller-scrollbar: thin`. */
|
|
50
|
+
--tab-scroller-scrollbar: none;
|
|
51
|
+
--tab-scroller-gap: 0; /* espace boutons chevrons ↔ viewport */
|
|
43
52
|
/* Géométrie et fond (pour les thèmes extrêmes comme Win98) */
|
|
44
53
|
--tab-bg: transparent;
|
|
45
54
|
--tab-bg-hover: color-mix(in srgb, var(--tab-text-hover) 5%, transparent);
|
|
@@ -93,16 +102,36 @@
|
|
|
93
102
|
|
|
94
103
|
/* Ombres de défilement (Lea Verou) : couche "cache" (couleur surface, attachement local) +
|
|
95
104
|
couche "ombre" (attachement scroll). L'ombre n'apparaît que s'il reste du contenu de ce côté
|
|
96
|
-
et s'estompe à l'extrémité → pas de "ghosting" permanent du 1er/dernier onglet.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
linear-gradient(to
|
|
101
|
-
|
|
102
|
-
radial-gradient(farthest-side at 0 50%, var(--tab-scroll-shadow-color, rgb(0 0 0 / 16%)), transparent)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
et s'estompe à l'extrémité → pas de "ghosting" permanent du 1er/dernier onglet.
|
|
106
|
+
Longhands (et non shorthand `background`) pour NE PAS réinitialiser `background-color` : un
|
|
107
|
+
consommateur peut donc poser sa propre couleur de fond sans collision. */
|
|
108
|
+
background-image:
|
|
109
|
+
linear-gradient(to right, var(--tab-list-bg, var(--kt-surface, #fff)) 30%, transparent),
|
|
110
|
+
linear-gradient(to left, var(--tab-list-bg, var(--kt-surface, #fff)) 30%, transparent),
|
|
111
|
+
radial-gradient(farthest-side at 0 50%, var(--tab-scroll-shadow-color, rgb(0 0 0 / 16%)), transparent),
|
|
112
|
+
radial-gradient(farthest-side at 100% 50%, var(--tab-scroll-shadow-color, rgb(0 0 0 / 16%)), transparent);
|
|
113
|
+
background-position:
|
|
114
|
+
0 0,
|
|
115
|
+
100% 0,
|
|
116
|
+
0 0,
|
|
117
|
+
100% 0;
|
|
118
|
+
background-size:
|
|
119
|
+
var(--tab-scroll-shadow-size, 2rem) 100%,
|
|
120
|
+
var(--tab-scroll-shadow-size, 2rem) 100%,
|
|
121
|
+
var(--tab-scroll-shadow-spread, 0.85rem) 100%,
|
|
122
|
+
var(--tab-scroll-shadow-spread, 0.85rem) 100%;
|
|
123
|
+
background-repeat: no-repeat;
|
|
124
|
+
background-attachment: local, local, scroll, scroll;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* 2b. Pagination active (directive ktTabScroller) → scrollbar native masquée ; les chevrons et
|
|
128
|
+
les ombres Lea Verou prennent le relais. `--tab-scroller-scrollbar: thin` réaffiche la barre. */
|
|
129
|
+
[ngTabList][data-kt-tab-scroller] {
|
|
130
|
+
scrollbar-width: var(--tab-scroller-scrollbar, none);
|
|
131
|
+
}
|
|
132
|
+
[ngTabList][data-kt-tab-scroller]::-webkit-scrollbar {
|
|
133
|
+
width: 0;
|
|
134
|
+
height: 0;
|
|
106
135
|
}
|
|
107
136
|
|
|
108
137
|
/* 3. Un onglet individuel */
|
|
@@ -171,9 +200,8 @@
|
|
|
171
200
|
/* 6. État désactivé */
|
|
172
201
|
[ngTab][disabled],
|
|
173
202
|
[ngTab][aria-disabled='true'] {
|
|
174
|
-
opacity: 0.4;
|
|
175
203
|
cursor: not-allowed;
|
|
176
|
-
color: var(--tab-text-
|
|
204
|
+
color: var(--tab-text-disabled);
|
|
177
205
|
}
|
|
178
206
|
|
|
179
207
|
/* 7. Panneaux de contenu (Tab Panels) */
|
|
@@ -239,15 +267,24 @@
|
|
|
239
267
|
/* Débordement vertical (la hauteur est contrainte par le conteneur hôte) + ombres haut/bas. */
|
|
240
268
|
overflow-x: hidden;
|
|
241
269
|
overflow-y: var(--tab-list-overflow, auto);
|
|
242
|
-
background
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
linear-gradient(to top, var(--tab-list-bg, var(--kt-surface, #fff)) 30%, transparent)
|
|
246
|
-
|
|
247
|
-
radial-gradient(farthest-side at 50%
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
270
|
+
/* Longhands (cf. bloc horizontal) : préserve un éventuel `background-color` du consommateur. */
|
|
271
|
+
background-image:
|
|
272
|
+
linear-gradient(to bottom, var(--tab-list-bg, var(--kt-surface, #fff)) 30%, transparent),
|
|
273
|
+
linear-gradient(to top, var(--tab-list-bg, var(--kt-surface, #fff)) 30%, transparent),
|
|
274
|
+
radial-gradient(farthest-side at 50% 0, var(--tab-scroll-shadow-color, rgb(0 0 0 / 16%)), transparent),
|
|
275
|
+
radial-gradient(farthest-side at 50% 100%, var(--tab-scroll-shadow-color, rgb(0 0 0 / 16%)), transparent);
|
|
276
|
+
background-position:
|
|
277
|
+
0 0,
|
|
278
|
+
0 100%,
|
|
279
|
+
0 0,
|
|
280
|
+
0 100%;
|
|
281
|
+
background-size:
|
|
282
|
+
100% var(--tab-scroll-shadow-size, 2rem),
|
|
283
|
+
100% var(--tab-scroll-shadow-size, 2rem),
|
|
284
|
+
100% var(--tab-scroll-shadow-spread, 0.85rem),
|
|
285
|
+
100% var(--tab-scroll-shadow-spread, 0.85rem);
|
|
286
|
+
background-repeat: no-repeat;
|
|
287
|
+
background-attachment: local, local, scroll, scroll;
|
|
251
288
|
}
|
|
252
289
|
|
|
253
290
|
[ngTabList][aria-orientation='vertical'] [ngTab],
|
|
@@ -273,4 +310,46 @@
|
|
|
273
310
|
flex-grow: 1;
|
|
274
311
|
padding: 0 1.5rem;
|
|
275
312
|
}
|
|
313
|
+
|
|
314
|
+
/* ==========================================================================
|
|
315
|
+
PAGINATION CLÉ-EN-MAIN (composant <kt-tab-scroller>)
|
|
316
|
+
On ne stylise QUE le layout (flex : chevron / viewport / chevron) — l'apparence des chevrons
|
|
317
|
+
vient de [ktButton]. `min-inline-size: 0` est critique : sans lui, le `ul` (dont les onglets
|
|
318
|
+
sont `flex: none`) impose sa largeur intrinsèque et la liste ne déborde jamais.
|
|
319
|
+
========================================================================== */
|
|
320
|
+
kt-tab-scroller {
|
|
321
|
+
display: flex;
|
|
322
|
+
align-items: stretch;
|
|
323
|
+
gap: var(--tab-scroller-gap, 0);
|
|
324
|
+
min-inline-size: 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
kt-tab-scroller > .kt-tab-scroller__viewport {
|
|
328
|
+
flex: 1 1 auto;
|
|
329
|
+
min-inline-size: 0;
|
|
330
|
+
display: flex;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
kt-tab-scroller > .kt-tab-scroller__viewport > [ngTabList] {
|
|
334
|
+
flex: 1 1 auto;
|
|
335
|
+
min-inline-size: 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.kt-tab-scroller__chevron {
|
|
339
|
+
flex: none;
|
|
340
|
+
align-self: center;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* Pas de débordement → chevrons retirés (la liste occupe toute la barre). */
|
|
344
|
+
kt-tab-scroller:not([data-overflowing]) > .kt-tab-scroller__chevron {
|
|
345
|
+
display: none;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/* Orientation verticale : barre en colonne, chevrons haut/bas. */
|
|
349
|
+
kt-tab-scroller[data-orientation='vertical'] {
|
|
350
|
+
flex-direction: column;
|
|
351
|
+
}
|
|
352
|
+
kt-tab-scroller[data-orientation='vertical'] > .kt-tab-scroller__viewport {
|
|
353
|
+
min-block-size: 0;
|
|
354
|
+
}
|
|
276
355
|
}
|
|
@@ -3,6 +3,17 @@ import { InjectionToken, inject, ElementRef, PLATFORM_ID, signal, input, boolean
|
|
|
3
3
|
import { isPlatformBrowser } from '@angular/common';
|
|
4
4
|
|
|
5
5
|
const KT_BUTTON_CONFIG = new InjectionToken('KT_BUTTON_CONFIG');
|
|
6
|
+
/**
|
|
7
|
+
* Fournit des défauts de bouton (mode/couleur/taille…) pour un sous-arbre ou l'application entière.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* providers: [provideKtButton({ mode: 'tonal', size: 'lg' })]
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
function provideKtButton(config) {
|
|
15
|
+
return { provide: KT_BUTTON_CONFIG, useValue: config };
|
|
16
|
+
}
|
|
6
17
|
/**
|
|
7
18
|
* Bouton ou lien stylé par la directive `ktButton`. À poser sur un `<button>`
|
|
8
19
|
* (action) ou un `<a>` (navigation) — l'accessibilité s'adapte automatiquement
|
|
@@ -35,31 +46,27 @@ class KtButton {
|
|
|
35
46
|
size = input(this.config?.size ?? 'md', /* @ts-ignore */
|
|
36
47
|
...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
37
48
|
/** Étire le bouton sur toute la largeur disponible. @default false */
|
|
38
|
-
fullWidth = input(false, /*
|
|
39
|
-
...(ngDevMode ? [{ debugName: "fullWidth" }] : /* istanbul ignore next */ []));
|
|
49
|
+
fullWidth = input(false, { ...(ngDevMode ? { debugName: "fullWidth" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
40
50
|
/** Bouton carré sans texte ; impose un nom accessible via `ariaLabel`. @default false */
|
|
41
51
|
iconOnly = input(false, { ...(ngDevMode ? { debugName: "iconOnly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
42
|
-
/** Nom accessible. Obligatoire si `iconOnly
|
|
52
|
+
/** Nom accessible. **Obligatoire si `iconOnly`** ; à défaut, l'`aria-label` natif est préservé. @default undefined */
|
|
43
53
|
ariaLabel = input(/* @ts-ignore */
|
|
44
54
|
...(ngDevMode ? [undefined, { debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
45
55
|
/** Attribut HTML natif (comportement formulaire), distinct de `mode`. @default 'button' */
|
|
46
56
|
type = input('button', /* @ts-ignore */
|
|
47
57
|
...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
|
|
48
58
|
/** Affiche l'état de chargement et rend le bouton inerte (`aria-busy`). @default false */
|
|
49
|
-
loading = input(false, /*
|
|
50
|
-
|
|
51
|
-
/** Nom de l'icône (rendu via CSS `data-icon`). */
|
|
59
|
+
loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
60
|
+
/** Nom de l'icône (rendu via CSS `data-icon`). @default undefined */
|
|
52
61
|
icon = input(/* @ts-ignore */
|
|
53
62
|
...(ngDevMode ? [undefined, { debugName: "icon" }] : /* istanbul ignore next */ []));
|
|
54
63
|
/** Position de l'icône relative au texte. @default 'start' */
|
|
55
64
|
iconPosition = input('start', /* @ts-ignore */
|
|
56
65
|
...(ngDevMode ? [{ debugName: "iconPosition" }] : /* istanbul ignore next */ []));
|
|
57
66
|
/** Désactive le bouton. @default false */
|
|
58
|
-
disabled = input(false, /*
|
|
59
|
-
...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
67
|
+
disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
60
68
|
/** Garde le bouton focusable même désactivé (via `aria-disabled`). @default false (ou KT_BUTTON_CONFIG.disabledInteractive) */
|
|
61
|
-
disabledInteractive = input(this.config?.disabledInteractive ?? false, /*
|
|
62
|
-
...(ngDevMode ? [{ debugName: "disabledInteractive" }] : /* istanbul ignore next */ []));
|
|
69
|
+
disabledInteractive = input(this.config?.disabledInteractive ?? false, { ...(ngDevMode ? { debugName: "disabledInteractive" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
63
70
|
// loading implique un bouton inerte
|
|
64
71
|
isDisabled = computed(() => this.disabled() || this.loading(), /* @ts-ignore */
|
|
65
72
|
...(ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []));
|
|
@@ -124,5 +131,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
124
131
|
* Generated bundle index. Do not edit.
|
|
125
132
|
*/
|
|
126
133
|
|
|
127
|
-
export { KT_BUTTON_CONFIG, KtButton };
|
|
134
|
+
export { KT_BUTTON_CONFIG, KtButton, provideKtButton };
|
|
128
135
|
//# sourceMappingURL=ktortu-aaa-button.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ktortu-aaa-button.mjs","sources":["../../../../projects/ktortu/aaa/button/button.ts","../../../../projects/ktortu/aaa/button/ktortu-aaa-button.ts"],"sourcesContent":["import {\n Directive,\n ElementRef,\n InjectionToken,\n booleanAttribute,\n computed,\n inject,\n input,\n isDevMode,\n AfterViewInit,\n PLATFORM_ID,\n effect,\n signal,\n} from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\n\n/** Axe \"emphase\" (apparence). */\nexport type KtButtonMode = 'filled' | 'tonal' | 'outlined' | 'text';\n/** Axe \"intention\" (couleur sémantique). */\nexport type KtButtonColor = 'primary' | 'neutral' | 'danger';\n/** Taille (la cible md ~44-48px vise le confort tactile / AAA). */\nexport type KtButtonSize = 'sm' | 'md' | 'lg';\n\nexport interface KtButtonConfig {\n disabledInteractive: boolean;\n mode: KtButtonMode;\n color: KtButtonColor;\n size: KtButtonSize;\n}\n\nexport const KT_BUTTON_CONFIG = new InjectionToken<Partial<KtButtonConfig>>('KT_BUTTON_CONFIG');\n\n/**\n * Bouton ou lien stylé par la directive `ktButton`. À poser sur un `<button>`\n * (action) ou un `<a>` (navigation) — l'accessibilité s'adapte automatiquement\n * (un `<a>` désactivé bascule sur `aria-disabled` plutôt que `disabled`).\n *\n * @example\n * ```html\n * <button ktButton mode=\"filled\" color=\"primary\">Enregistrer</button>\n * <a ktButton mode=\"text\" href=\"/aide\">Aide</a>\n * <button ktButton iconOnly icon=\"close\" ariaLabel=\"Fermer\"></button>\n * ```\n */\n@Directive({\n selector: 'button[ktButton], a[ktButton]',\n host: {\n '[attr.data-mode]': 'mode()',\n '[attr.data-color]': 'color()',\n '[attr.data-size]': 'size()',\n '[attr.data-full-width]': 'fullWidth() ? \"\" : null',\n '[attr.data-icon-only]': 'iconOnly() ? \"\" : null',\n '[attr.type]': '!isLink ? type() : null',\n '[attr.aria-label]': 'resolvedAriaLabel()',\n '[class.loading]': 'loading()',\n '[class.disabled]': 'isDisabled()',\n '[attr.data-icon]': 'icon()',\n '[attr.data-icon-position]': 'iconPosition()',\n '[attr.disabled]': 'isDisabled() && !disabledInteractive() && !isLink ? \"\" : null',\n '[attr.aria-disabled]': 'isDisabled() && (disabledInteractive() || isLink) ? \"true\" : null',\n '[attr.tabindex]': 'isDisabled() && !disabledInteractive() && isLink ? \"-1\" : null',\n '[attr.aria-busy]': 'loading() ? \"true\" : null',\n '(click)': 'haltDisabledEvents($event)',\n },\n})\nexport class KtButton implements AfterViewInit {\n private readonly config = inject(KT_BUTTON_CONFIG, { optional: true });\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n private readonly platformId = inject(PLATFORM_ID);\n private readonly viewInitialized = signal(false);\n\n // Un <a> reste toujours focusable : on bascule sur aria-disabled au lieu du disabled natif.\n protected readonly isLink = this.host.tagName === 'A';\n // aria-label natif posé par le consommateur, capté à la construction (préservé si l'input n'est pas fourni).\n private readonly nativeAriaLabel = this.host.getAttribute('aria-label');\n\n /** Emphase visuelle du bouton (apparence, ≠ `type`). @default 'filled' (ou KT_BUTTON_CONFIG.mode) */\n readonly mode = input<KtButtonMode>(this.config?.mode ?? 'filled');\n\n /** Intention sémantique (couleur). @default 'primary' (ou KT_BUTTON_CONFIG.color) */\n readonly color = input<KtButtonColor>(this.config?.color ?? 'primary');\n\n /** Taille ; `md` vise une cible tactile ~44-48px. @default 'md' (ou KT_BUTTON_CONFIG.size) */\n readonly size = input<KtButtonSize>(this.config?.size ?? 'md');\n\n /** Étire le bouton sur toute la largeur disponible. @default false */\n readonly fullWidth = input<boolean>(false);\n\n /** Bouton carré sans texte ; impose un nom accessible via `ariaLabel`. @default false */\n readonly iconOnly = input<boolean, unknown>(false, { transform: booleanAttribute });\n\n /** Nom accessible. Obligatoire si `iconOnly` ; à défaut, l'`aria-label` natif est préservé. */\n readonly ariaLabel = input<string>();\n\n /** Attribut HTML natif (comportement formulaire), distinct de `mode`. @default 'button' */\n readonly type = input<'button' | 'submit' | 'reset'>('button');\n\n /** Affiche l'état de chargement et rend le bouton inerte (`aria-busy`). @default false */\n readonly loading = input<boolean>(false);\n\n /** Nom de l'icône (rendu via CSS `data-icon`). */\n readonly icon = input<string>();\n\n /** Position de l'icône relative au texte. @default 'start' */\n readonly iconPosition = input<'start' | 'end'>('start');\n\n /** Désactive le bouton. @default false */\n readonly disabled = input<boolean>(false);\n\n /** Garde le bouton focusable même désactivé (via `aria-disabled`). @default false (ou KT_BUTTON_CONFIG.disabledInteractive) */\n readonly disabledInteractive = input<boolean>(this.config?.disabledInteractive ?? false);\n\n // loading implique un bouton inerte\n protected readonly isDisabled = computed(() => this.disabled() || this.loading());\n\n // input prioritaire, sinon aria-label natif préservé (non destructif), sinon rien.\n protected readonly resolvedAriaLabel = computed(() => this.ariaLabel()?.trim() || this.nativeAriaLabel || null);\n\n constructor() {\n effect(() => {\n if (!isPlatformBrowser(this.platformId)) return;\n if (!this.viewInitialized()) return;\n if (!isDevMode() || !this.iconOnly()) return;\n\n if (!this.icon()) {\n console.warn('[ktButton] iconOnly attend une icône via [icon].');\n }\n if (!this.resolvedAriaLabel() && !this.host.hasAttribute('aria-labelledby')) {\n console.warn(\n '[ktButton] iconOnly sans nom accessible : ajoutez [ariaLabel] (WCAG 4.1.2) — ' +\n 'le bouton serait annoncé sans libellé.',\n );\n }\n });\n }\n\n ngAfterViewInit(): void {\n this.viewInitialized.set(true);\n }\n\n protected haltDisabledEvents(event: Event): void {\n if (!this.isDisabled()) return;\n\n event.preventDefault();\n event.stopImmediatePropagation();\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ktortu-aaa-button.mjs","sources":["../../../../projects/ktortu/aaa/button/button.ts","../../../../projects/ktortu/aaa/button/ktortu-aaa-button.ts"],"sourcesContent":["import {\n Directive,\n ElementRef,\n InjectionToken,\n Provider,\n booleanAttribute,\n computed,\n inject,\n input,\n isDevMode,\n AfterViewInit,\n PLATFORM_ID,\n effect,\n signal,\n} from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\n\n/** Axe \"emphase\" (apparence). */\nexport type KtButtonMode = 'filled' | 'tonal' | 'outlined' | 'text';\n/** Axe \"intention\" (couleur sémantique). */\nexport type KtButtonColor = 'primary' | 'neutral' | 'danger';\n/** Taille (la cible md ~44-48px vise le confort tactile / AAA). */\nexport type KtButtonSize = 'sm' | 'md' | 'lg';\n\n/** Défauts applicables à tous les `[ktButton]` (surchargeables par bouton via les inputs). */\nexport interface KtButtonConfig {\n /** Garde le bouton focusable même désactivé (via `aria-disabled`). */\n disabledInteractive: boolean;\n /** Emphase visuelle par défaut (apparence). */\n mode: KtButtonMode;\n /** Intention sémantique par défaut (couleur). */\n color: KtButtonColor;\n /** Taille par défaut. */\n size: KtButtonSize;\n}\n\nexport const KT_BUTTON_CONFIG = new InjectionToken<Partial<KtButtonConfig>>('KT_BUTTON_CONFIG');\n\n/**\n * Fournit des défauts de bouton (mode/couleur/taille…) pour un sous-arbre ou l'application entière.\n *\n * @example\n * ```ts\n * providers: [provideKtButton({ mode: 'tonal', size: 'lg' })]\n * ```\n */\nexport function provideKtButton(config: Partial<KtButtonConfig>): Provider {\n return { provide: KT_BUTTON_CONFIG, useValue: config };\n}\n\n/**\n * Bouton ou lien stylé par la directive `ktButton`. À poser sur un `<button>`\n * (action) ou un `<a>` (navigation) — l'accessibilité s'adapte automatiquement\n * (un `<a>` désactivé bascule sur `aria-disabled` plutôt que `disabled`).\n *\n * @example\n * ```html\n * <button ktButton mode=\"filled\" color=\"primary\">Enregistrer</button>\n * <a ktButton mode=\"text\" href=\"/aide\">Aide</a>\n * <button ktButton iconOnly icon=\"close\" ariaLabel=\"Fermer\"></button>\n * ```\n */\n@Directive({\n selector: 'button[ktButton], a[ktButton]',\n host: {\n '[attr.data-mode]': 'mode()',\n '[attr.data-color]': 'color()',\n '[attr.data-size]': 'size()',\n '[attr.data-full-width]': 'fullWidth() ? \"\" : null',\n '[attr.data-icon-only]': 'iconOnly() ? \"\" : null',\n '[attr.type]': '!isLink ? type() : null',\n '[attr.aria-label]': 'resolvedAriaLabel()',\n '[class.loading]': 'loading()',\n '[class.disabled]': 'isDisabled()',\n '[attr.data-icon]': 'icon()',\n '[attr.data-icon-position]': 'iconPosition()',\n '[attr.disabled]': 'isDisabled() && !disabledInteractive() && !isLink ? \"\" : null',\n '[attr.aria-disabled]': 'isDisabled() && (disabledInteractive() || isLink) ? \"true\" : null',\n '[attr.tabindex]': 'isDisabled() && !disabledInteractive() && isLink ? \"-1\" : null',\n '[attr.aria-busy]': 'loading() ? \"true\" : null',\n '(click)': 'haltDisabledEvents($event)',\n },\n})\nexport class KtButton implements AfterViewInit {\n private readonly config = inject(KT_BUTTON_CONFIG, { optional: true });\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n private readonly platformId = inject(PLATFORM_ID);\n private readonly viewInitialized = signal(false);\n\n // Un <a> reste toujours focusable : on bascule sur aria-disabled au lieu du disabled natif.\n protected readonly isLink = this.host.tagName === 'A';\n // aria-label natif posé par le consommateur, capté à la construction (préservé si l'input n'est pas fourni).\n private readonly nativeAriaLabel = this.host.getAttribute('aria-label');\n\n /** Emphase visuelle du bouton (apparence, ≠ `type`). @default 'filled' (ou KT_BUTTON_CONFIG.mode) */\n readonly mode = input<KtButtonMode>(this.config?.mode ?? 'filled');\n\n /** Intention sémantique (couleur). @default 'primary' (ou KT_BUTTON_CONFIG.color) */\n readonly color = input<KtButtonColor>(this.config?.color ?? 'primary');\n\n /** Taille ; `md` vise une cible tactile ~44-48px. @default 'md' (ou KT_BUTTON_CONFIG.size) */\n readonly size = input<KtButtonSize>(this.config?.size ?? 'md');\n\n /** Étire le bouton sur toute la largeur disponible. @default false */\n readonly fullWidth = input<boolean, unknown>(false, { transform: booleanAttribute });\n\n /** Bouton carré sans texte ; impose un nom accessible via `ariaLabel`. @default false */\n readonly iconOnly = input<boolean, unknown>(false, { transform: booleanAttribute });\n\n /** Nom accessible. **Obligatoire si `iconOnly`** ; à défaut, l'`aria-label` natif est préservé. @default undefined */\n readonly ariaLabel = input<string>();\n\n /** Attribut HTML natif (comportement formulaire), distinct de `mode`. @default 'button' */\n readonly type = input<'button' | 'submit' | 'reset'>('button');\n\n /** Affiche l'état de chargement et rend le bouton inerte (`aria-busy`). @default false */\n readonly loading = input<boolean, unknown>(false, { transform: booleanAttribute });\n\n /** Nom de l'icône (rendu via CSS `data-icon`). @default undefined */\n readonly icon = input<string>();\n\n /** Position de l'icône relative au texte. @default 'start' */\n readonly iconPosition = input<'start' | 'end'>('start');\n\n /** Désactive le bouton. @default false */\n readonly disabled = input<boolean, unknown>(false, { transform: booleanAttribute });\n\n /** Garde le bouton focusable même désactivé (via `aria-disabled`). @default false (ou KT_BUTTON_CONFIG.disabledInteractive) */\n readonly disabledInteractive = input<boolean, unknown>(this.config?.disabledInteractive ?? false, {\n transform: booleanAttribute,\n });\n\n // loading implique un bouton inerte\n protected readonly isDisabled = computed(() => this.disabled() || this.loading());\n\n // input prioritaire, sinon aria-label natif préservé (non destructif), sinon rien.\n protected readonly resolvedAriaLabel = computed(() => this.ariaLabel()?.trim() || this.nativeAriaLabel || null);\n\n constructor() {\n effect(() => {\n if (!isPlatformBrowser(this.platformId)) return;\n if (!this.viewInitialized()) return;\n if (!isDevMode() || !this.iconOnly()) return;\n\n if (!this.icon()) {\n console.warn('[ktButton] iconOnly attend une icône via [icon].');\n }\n if (!this.resolvedAriaLabel() && !this.host.hasAttribute('aria-labelledby')) {\n console.warn(\n '[ktButton] iconOnly sans nom accessible : ajoutez [ariaLabel] (WCAG 4.1.2) — ' +\n 'le bouton serait annoncé sans libellé.',\n );\n }\n });\n }\n\n ngAfterViewInit(): void {\n this.viewInitialized.set(true);\n }\n\n protected haltDisabledEvents(event: Event): void {\n if (!this.isDisabled()) return;\n\n event.preventDefault();\n event.stopImmediatePropagation();\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Directive, inject, ElementRef, afterNextRender, isDevMode, InjectionToken, contentChild, input, booleanAttribute, effect } from '@angular/core';
|
|
2
|
+
import { Directive, inject, ElementRef, afterNextRender, isDevMode, InjectionToken, PLATFORM_ID, contentChild, input, booleanAttribute, effect } from '@angular/core';
|
|
3
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* En-tête de la carte (rangée flex : média/avatar + titre + éventuelle action). Marqueur
|
|
@@ -83,6 +84,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
83
84
|
*
|
|
84
85
|
* Quand la carte ancêtre est `disabled`, le lien sort de l'ordre de tabulation (`tabindex="-1"`)
|
|
85
86
|
* et est annoncé `aria-disabled` : une carte inerte ne piège pas le focus clavier (WCAG 2.4.3).
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```html
|
|
90
|
+
* <article ktCard interactive>
|
|
91
|
+
* <div ktCardContent>
|
|
92
|
+
* <h3 id="t1">Titre</h3>
|
|
93
|
+
* <a ktCardLink routerLink="/detail" aria-labelledby="t1">Voir le détail</a>
|
|
94
|
+
* </div>
|
|
95
|
+
* </article>
|
|
96
|
+
* ```
|
|
86
97
|
*/
|
|
87
98
|
class KtCardLink {
|
|
88
99
|
host = inject(ElementRef).nativeElement;
|
|
@@ -128,6 +139,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
128
139
|
}], ctorParameters: () => [] });
|
|
129
140
|
|
|
130
141
|
const KT_CARD_CONFIG = new InjectionToken('KT_CARD_CONFIG');
|
|
142
|
+
/**
|
|
143
|
+
* Fournit des défauts de carte (apparence de surface) pour un sous-arbre ou l'application entière.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```ts
|
|
147
|
+
* providers: [provideKtCard({ variant: 'outlined' })]
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
function provideKtCard(config) {
|
|
151
|
+
return { provide: KT_CARD_CONFIG, useValue: config };
|
|
152
|
+
}
|
|
131
153
|
/**
|
|
132
154
|
* Carte : SURFACE de contenu. Directive (pas de composant) posée sur l'élément SÉMANTIQUE choisi
|
|
133
155
|
* par le consommateur (`<article>`, `<section>`, `<li>`, `<a>`…) — la lib n'impose jamais de
|
|
@@ -155,6 +177,7 @@ const KT_CARD_CONFIG = new InjectionToken('KT_CARD_CONFIG');
|
|
|
155
177
|
class KtCard {
|
|
156
178
|
config = inject(KT_CARD_CONFIG, { optional: true });
|
|
157
179
|
host = inject(ElementRef).nativeElement;
|
|
180
|
+
platformId = inject(PLATFORM_ID);
|
|
158
181
|
/** Référence réactive sur le lien primaire de la carte */
|
|
159
182
|
cardLink = contentChild(KtCardLink, { ...(ngDevMode ? { debugName: "cardLink" } : /* istanbul ignore next */ {}), descendants: true });
|
|
160
183
|
/** Apparence de la surface : `elevated` | `outlined` | `filled`. @default 'elevated' (ou `KT_CARD_CONFIG.variant`) */
|
|
@@ -171,6 +194,8 @@ class KtCard {
|
|
|
171
194
|
// Garde-fou a11y (dev seulement) : une carte interactive sans cible cliquable affiche une
|
|
172
195
|
// affordance trompeuse (hover/focus) qui ne mène à rien (WCAG 1.3.1).
|
|
173
196
|
effect(() => {
|
|
197
|
+
if (!isPlatformBrowser(this.platformId))
|
|
198
|
+
return;
|
|
174
199
|
if (!isDevMode() || !this.interactive())
|
|
175
200
|
return;
|
|
176
201
|
const hasTarget = this.host.matches('a, button') || !!this.cardLink();
|
|
@@ -197,13 +222,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
197
222
|
|
|
198
223
|
/**
|
|
199
224
|
* Import ergonomique de toute la famille card en une fois :
|
|
200
|
-
* `imports: [
|
|
225
|
+
* `imports: [KtCardImports]` au lieu d'énumérer chaque directive.
|
|
201
226
|
*/
|
|
202
|
-
const
|
|
227
|
+
const KtCardImports = [KtCard, KtCardHeader, KtCardMedia, KtCardContent, KtCardActions, KtCardLink];
|
|
203
228
|
|
|
204
229
|
/**
|
|
205
230
|
* Generated bundle index. Do not edit.
|
|
206
231
|
*/
|
|
207
232
|
|
|
208
|
-
export {
|
|
233
|
+
export { KT_CARD_CONFIG, KtCard, KtCardActions, KtCardContent, KtCardHeader, KtCardImports, KtCardLink, KtCardMedia, provideKtCard };
|
|
209
234
|
//# sourceMappingURL=ktortu-aaa-card.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ktortu-aaa-card.mjs","sources":["../../../../projects/ktortu/aaa/card/card-structure.ts","../../../../projects/ktortu/aaa/card/card.ts","../../../../projects/ktortu/aaa/card/public-api.ts","../../../../projects/ktortu/aaa/card/ktortu-aaa-card.ts"],"sourcesContent":["import { Directive, ElementRef, afterNextRender, inject, isDevMode } from '@angular/core';\n\nimport { KtCard } from './card';\n\n/**\n * En-tête de la carte (rangée flex : média/avatar + titre + éventuelle action). Marqueur\n * structurel sans logique — la mise en forme vit dans `card.css` via `[ktCardHeader]`.\n * L'ÉTIQUETTE accessible reste le titre fourni par le consommateur (`<h3 id>` + `aria-labelledby`\n * sur l'hôte), comme [ktDialogTitle] pour le dialog : on sépare layout et sémantique.\n *\n * @example\n * ```html\n * <header ktCardHeader><h3 id=\"t1\">Titre</h3></header>\n * ```\n */\n@Directive({ selector: '[ktCardHeader]' })\nexport class KtCardHeader {}\n\n/**\n * Média pleine largeur (image/vidéo). Marqueur structurel sans logique : `card.css` lui donne le\n * full-bleed (marge négative = padding de la carte) et `[ktCard]` clippe au rayon quand un média\n * est présent. À envelopper autour d'un `<img ngSrc>` (NgOptimizedImage).\n *\n * @example\n * ```html\n * <div ktCardMedia><img ngSrc=\"cover.jpg\" width=\"400\" height=\"200\" alt=\"\" /></div>\n * ```\n */\n@Directive({ selector: '[ktCardMedia]' })\nexport class KtCardMedia {}\n\n/**\n * Corps de la carte. Marqueur structurel sans logique : la mise en forme (rythme vertical) vit\n * dans `card.css` via `[ktCardContent]`.\n *\n * @example\n * ```html\n * <div ktCardContent>Texte de la carte.</div>\n * ```\n */\n@Directive({ selector: '[ktCardContent]' })\nexport class KtCardContent {}\n\n/**\n * Barre d'actions de la carte (rangée de boutons/liens). Marqueur structurel sans logique :\n * la mise en forme (flex, gap, épinglée en pied) vit dans `card.css` via `[ktCardActions]`.\n *\n * @example\n * ```html\n * <footer ktCardActions><button ktButton>Action</button></footer>\n * ```\n */\n@Directive({ selector: '[ktCardActions]' })\nexport class KtCardActions {}\n\n/**\n * Lien (ou bouton) PRIMAIRE d'une carte interactive : pattern « lien étiré » (Inclusive\n * Components). Un pseudo-élément `::after` couvre toute la carte (cf. `card.css`) → toute la\n * surface est cliquable, SANS imbriquer de contrôles interactifs (anti-pattern WCAG 4.1.2). Les\n * actions secondaires de la carte repassent au-dessus du lien (z-index dans `card.css`).\n *\n * UN SEUL [ktCardLink] par carte. Le focus clavier est porté sur ce lien ; l'anneau de focus est\n * relayé sur toute la carte (`[ktCard]:has([ktCardLink]:focus-visible)`).\n *\n * Quand la carte ancêtre est `disabled`, le lien sort de l'ordre de tabulation (`tabindex=\"-1\"`)\n * et est annoncé `aria-disabled` : une carte inerte ne piège pas le focus clavier (WCAG 2.4.3).\n */\n@Directive({\n selector: 'a[ktCardLink], button[ktCardLink]',\n host: {\n '[attr.aria-disabled]': 'card?.disabled() ? \"true\" : null',\n '[attr.tabindex]': 'card?.disabled() ? \"-1\" : null',\n '(click)': 'handleClick($event)',\n },\n})\nexport class KtCardLink {\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n /**\n * Carte ancêtre, injectée optionnellement via `inject(Card, { optional: true })` (DI par\n * hiérarchie d'éléments) : `null` si [ktCardLink] est utilisé hors d'une [ktCard]. Sert à relayer\n * l'état `disabled` de la carte (aria-disabled / tabindex) sans planter hors contexte.\n */\n protected readonly card = inject(KtCard, { optional: true });\n\n handleClick(event: Event): void {\n if (this.card?.disabled()) {\n event.preventDefault();\n event.stopPropagation();\n }\n }\n\n constructor() {\n // Garde-fou a11y (dev seulement) : un lien étiré sans nom accessible n'est pas annonçable.\n afterNextRender(() => {\n if (!isDevMode()) return;\n\n const hasName =\n !!this.host.textContent?.trim() ||\n !!this.host.getAttribute('aria-label')?.trim() ||\n this.host.hasAttribute('aria-labelledby');\n if (!hasName) {\n console.warn(\n '[ktCardLink] lien sans nom accessible : ajoutez du texte visible, [attr.aria-label] ' +\n 'ou aria-labelledby (WCAG 2.4.4 / 4.1.2).',\n );\n }\n });\n }\n}\n","import {\n Directive,\n ElementRef,\n InjectionToken,\n booleanAttribute,\n contentChild,\n effect,\n inject,\n input,\n isDevMode,\n} from '@angular/core';\nimport { KtCardLink } from './card-structure';\n\n/** Axe \"apparence\" de la surface. */\nexport type KtCardVariant = 'elevated' | 'outlined' | 'filled';\n\n/** Défauts applicables à toutes les `[ktCard]` (surchargeables par carte via les inputs). */\nexport interface KtCardConfig {\n /** Apparence par défaut de la surface. */\n variant: KtCardVariant;\n}\n\nexport const KT_CARD_CONFIG = new InjectionToken<Partial<KtCardConfig>>('KT_CARD_CONFIG');\n\n/**\n * Carte : SURFACE de contenu. Directive (pas de composant) posée sur l'élément SÉMANTIQUE choisi\n * par le consommateur (`<article>`, `<section>`, `<li>`, `<a>`…) — la lib n'impose jamais de\n * wrapper non sémantique ni de rôle (cf. précédent Dialog : la sémantique appartient à l'hôte).\n *\n * Trois axes pilotés en `data-*` (même contrat que ktButton) :\n * - variant : data-variant = elevated | outlined | filled (apparence)\n * - interactive : data-interactive (affordance hover/focus — PAS un rôle ; la cible cliquable\n * est un [ktCardLink], lien/bouton primaire au lien étiré)\n * - disabled : data-disabled (surface inerte)\n *\n * La mise en forme (layout des marqueurs, lien étiré, états) vit dans `card.css` ; les couleurs\n * et la géométrie dérivent du socle `--kt-*` via `card-tokens.css`.\n *\n * @example\n * ```html\n * <article ktCard variant=\"outlined\" interactive>\n * <div ktCardContent>\n * <h3 id=\"t1\">Titre</h3>\n * <a ktCardLink routerLink=\"/detail\" aria-labelledby=\"t1\">Voir le détail</a>\n * </div>\n * </article>\n * ```\n */\n@Directive({\n selector: '[ktCard]',\n host: {\n '[attr.data-variant]': 'variant()',\n '[attr.data-interactive]': 'interactive() ? \"\" : null',\n '[attr.data-disabled]': 'disabled() ? \"\" : null',\n },\n})\nexport class KtCard {\n private readonly config = inject(KT_CARD_CONFIG, { optional: true });\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n\n /** Référence réactive sur le lien primaire de la carte */\n readonly cardLink = contentChild(KtCardLink, { descendants: true });\n\n /** Apparence de la surface : `elevated` | `outlined` | `filled`. @default 'elevated' (ou `KT_CARD_CONFIG.variant`) */\n readonly variant = input<KtCardVariant>(this.config?.variant ?? 'elevated');\n\n /**\n * Affordance visuelle d'élément cliquable (hover/focus). N'AJOUTE pas de rôle : un lien/bouton\n * primaire ([ktCardLink]) porte l'interaction et le nom accessible. @default false\n */\n readonly interactive = input<boolean, unknown>(false, { transform: booleanAttribute });\n\n /** Rend la surface inerte (état `data-disabled`). @default false */\n readonly disabled = input<boolean, unknown>(false, { transform: booleanAttribute });\n\n constructor() {\n // Garde-fou a11y (dev seulement) : une carte interactive sans cible cliquable affiche une\n // affordance trompeuse (hover/focus) qui ne mène à rien (WCAG 1.3.1).\n effect(() => {\n if (!isDevMode() || !this.interactive()) return;\n\n const hasTarget = this.host.matches('a, button') || !!this.cardLink();\n if (!hasTarget) {\n console.warn(\n '[ktCard] interactive sans cible cliquable : ajoutez un [ktCardLink] (lien/bouton ' +\n 'primaire) ou posez [ktCard] sur un <a>/<button> — sinon l’affordance est trompeuse.',\n );\n }\n });\n }\n}\n","import { KtCard } from './card';\nimport { KtCardActions, KtCardContent, KtCardHeader, KtCardLink, KtCardMedia } from './card-structure';\n\nexport * from './card';\nexport * from './card-structure';\n\n/**\n * Import ergonomique de toute la famille card en une fois :\n * `imports: [KT_CARD]` au lieu d'énumérer chaque directive.\n */\nexport const KT_CARD = [KtCard, KtCardHeader, KtCardMedia, KtCardContent, KtCardActions, KtCardLink] as const;\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ktortu-aaa-card.mjs","sources":["../../../../projects/ktortu/aaa/card/card-structure.ts","../../../../projects/ktortu/aaa/card/card.ts","../../../../projects/ktortu/aaa/card/public-api.ts","../../../../projects/ktortu/aaa/card/ktortu-aaa-card.ts"],"sourcesContent":["import { Directive, ElementRef, afterNextRender, inject, isDevMode } from '@angular/core';\n\nimport { KtCard } from './card';\n\n/**\n * En-tête de la carte (rangée flex : média/avatar + titre + éventuelle action). Marqueur\n * structurel sans logique — la mise en forme vit dans `card.css` via `[ktCardHeader]`.\n * L'ÉTIQUETTE accessible reste le titre fourni par le consommateur (`<h3 id>` + `aria-labelledby`\n * sur l'hôte), comme [ktDialogTitle] pour le dialog : on sépare layout et sémantique.\n *\n * @example\n * ```html\n * <header ktCardHeader><h3 id=\"t1\">Titre</h3></header>\n * ```\n */\n@Directive({ selector: '[ktCardHeader]' })\nexport class KtCardHeader {}\n\n/**\n * Média pleine largeur (image/vidéo). Marqueur structurel sans logique : `card.css` lui donne le\n * full-bleed (marge négative = padding de la carte) et `[ktCard]` clippe au rayon quand un média\n * est présent. À envelopper autour d'un `<img ngSrc>` (NgOptimizedImage).\n *\n * @example\n * ```html\n * <div ktCardMedia><img ngSrc=\"cover.jpg\" width=\"400\" height=\"200\" alt=\"\" /></div>\n * ```\n */\n@Directive({ selector: '[ktCardMedia]' })\nexport class KtCardMedia {}\n\n/**\n * Corps de la carte. Marqueur structurel sans logique : la mise en forme (rythme vertical) vit\n * dans `card.css` via `[ktCardContent]`.\n *\n * @example\n * ```html\n * <div ktCardContent>Texte de la carte.</div>\n * ```\n */\n@Directive({ selector: '[ktCardContent]' })\nexport class KtCardContent {}\n\n/**\n * Barre d'actions de la carte (rangée de boutons/liens). Marqueur structurel sans logique :\n * la mise en forme (flex, gap, épinglée en pied) vit dans `card.css` via `[ktCardActions]`.\n *\n * @example\n * ```html\n * <footer ktCardActions><button ktButton>Action</button></footer>\n * ```\n */\n@Directive({ selector: '[ktCardActions]' })\nexport class KtCardActions {}\n\n/**\n * Lien (ou bouton) PRIMAIRE d'une carte interactive : pattern « lien étiré » (Inclusive\n * Components). Un pseudo-élément `::after` couvre toute la carte (cf. `card.css`) → toute la\n * surface est cliquable, SANS imbriquer de contrôles interactifs (anti-pattern WCAG 4.1.2). Les\n * actions secondaires de la carte repassent au-dessus du lien (z-index dans `card.css`).\n *\n * UN SEUL [ktCardLink] par carte. Le focus clavier est porté sur ce lien ; l'anneau de focus est\n * relayé sur toute la carte (`[ktCard]:has([ktCardLink]:focus-visible)`).\n *\n * Quand la carte ancêtre est `disabled`, le lien sort de l'ordre de tabulation (`tabindex=\"-1\"`)\n * et est annoncé `aria-disabled` : une carte inerte ne piège pas le focus clavier (WCAG 2.4.3).\n *\n * @example\n * ```html\n * <article ktCard interactive>\n * <div ktCardContent>\n * <h3 id=\"t1\">Titre</h3>\n * <a ktCardLink routerLink=\"/detail\" aria-labelledby=\"t1\">Voir le détail</a>\n * </div>\n * </article>\n * ```\n */\n@Directive({\n selector: 'a[ktCardLink], button[ktCardLink]',\n host: {\n '[attr.aria-disabled]': 'card?.disabled() ? \"true\" : null',\n '[attr.tabindex]': 'card?.disabled() ? \"-1\" : null',\n '(click)': 'handleClick($event)',\n },\n})\nexport class KtCardLink {\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n /**\n * Carte ancêtre, injectée optionnellement via `inject(Card, { optional: true })` (DI par\n * hiérarchie d'éléments) : `null` si [ktCardLink] est utilisé hors d'une [ktCard]. Sert à relayer\n * l'état `disabled` de la carte (aria-disabled / tabindex) sans planter hors contexte.\n */\n protected readonly card = inject(KtCard, { optional: true });\n\n handleClick(event: Event): void {\n if (this.card?.disabled()) {\n event.preventDefault();\n event.stopPropagation();\n }\n }\n\n constructor() {\n // Garde-fou a11y (dev seulement) : un lien étiré sans nom accessible n'est pas annonçable.\n afterNextRender(() => {\n if (!isDevMode()) return;\n\n const hasName =\n !!this.host.textContent?.trim() ||\n !!this.host.getAttribute('aria-label')?.trim() ||\n this.host.hasAttribute('aria-labelledby');\n if (!hasName) {\n console.warn(\n '[ktCardLink] lien sans nom accessible : ajoutez du texte visible, [attr.aria-label] ' +\n 'ou aria-labelledby (WCAG 2.4.4 / 4.1.2).',\n );\n }\n });\n }\n}\n","import {\n Directive,\n ElementRef,\n InjectionToken,\n PLATFORM_ID,\n Provider,\n booleanAttribute,\n contentChild,\n effect,\n inject,\n input,\n isDevMode,\n} from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\nimport { KtCardLink } from './card-structure';\n\n/** Axe \"apparence\" de la surface. */\nexport type KtCardVariant = 'elevated' | 'outlined' | 'filled';\n\n/** Défauts applicables à toutes les `[ktCard]` (surchargeables par carte via les inputs). */\nexport interface KtCardConfig {\n /** Apparence par défaut de la surface. */\n variant: KtCardVariant;\n}\n\nexport const KT_CARD_CONFIG = new InjectionToken<Partial<KtCardConfig>>('KT_CARD_CONFIG');\n\n/**\n * Fournit des défauts de carte (apparence de surface) pour un sous-arbre ou l'application entière.\n *\n * @example\n * ```ts\n * providers: [provideKtCard({ variant: 'outlined' })]\n * ```\n */\nexport function provideKtCard(config: Partial<KtCardConfig>): Provider {\n return { provide: KT_CARD_CONFIG, useValue: config };\n}\n\n/**\n * Carte : SURFACE de contenu. Directive (pas de composant) posée sur l'élément SÉMANTIQUE choisi\n * par le consommateur (`<article>`, `<section>`, `<li>`, `<a>`…) — la lib n'impose jamais de\n * wrapper non sémantique ni de rôle (cf. précédent Dialog : la sémantique appartient à l'hôte).\n *\n * Trois axes pilotés en `data-*` (même contrat que ktButton) :\n * - variant : data-variant = elevated | outlined | filled (apparence)\n * - interactive : data-interactive (affordance hover/focus — PAS un rôle ; la cible cliquable\n * est un [ktCardLink], lien/bouton primaire au lien étiré)\n * - disabled : data-disabled (surface inerte)\n *\n * La mise en forme (layout des marqueurs, lien étiré, états) vit dans `card.css` ; les couleurs\n * et la géométrie dérivent du socle `--kt-*` via `card-tokens.css`.\n *\n * @example\n * ```html\n * <article ktCard variant=\"outlined\" interactive>\n * <div ktCardContent>\n * <h3 id=\"t1\">Titre</h3>\n * <a ktCardLink routerLink=\"/detail\" aria-labelledby=\"t1\">Voir le détail</a>\n * </div>\n * </article>\n * ```\n */\n@Directive({\n selector: '[ktCard]',\n host: {\n '[attr.data-variant]': 'variant()',\n '[attr.data-interactive]': 'interactive() ? \"\" : null',\n '[attr.data-disabled]': 'disabled() ? \"\" : null',\n },\n})\nexport class KtCard {\n private readonly config = inject(KT_CARD_CONFIG, { optional: true });\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n private readonly platformId = inject(PLATFORM_ID);\n\n /** Référence réactive sur le lien primaire de la carte */\n readonly cardLink = contentChild(KtCardLink, { descendants: true });\n\n /** Apparence de la surface : `elevated` | `outlined` | `filled`. @default 'elevated' (ou `KT_CARD_CONFIG.variant`) */\n readonly variant = input<KtCardVariant>(this.config?.variant ?? 'elevated');\n\n /**\n * Affordance visuelle d'élément cliquable (hover/focus). N'AJOUTE pas de rôle : un lien/bouton\n * primaire ([ktCardLink]) porte l'interaction et le nom accessible. @default false\n */\n readonly interactive = input<boolean, unknown>(false, { transform: booleanAttribute });\n\n /** Rend la surface inerte (état `data-disabled`). @default false */\n readonly disabled = input<boolean, unknown>(false, { transform: booleanAttribute });\n\n constructor() {\n // Garde-fou a11y (dev seulement) : une carte interactive sans cible cliquable affiche une\n // affordance trompeuse (hover/focus) qui ne mène à rien (WCAG 1.3.1).\n effect(() => {\n if (!isPlatformBrowser(this.platformId)) return;\n if (!isDevMode() || !this.interactive()) return;\n\n const hasTarget = this.host.matches('a, button') || !!this.cardLink();\n if (!hasTarget) {\n console.warn(\n '[ktCard] interactive sans cible cliquable : ajoutez un [ktCardLink] (lien/bouton ' +\n 'primaire) ou posez [ktCard] sur un <a>/<button> — sinon l’affordance est trompeuse.',\n );\n }\n });\n }\n}\n","import { KtCard } from './card';\nimport { KtCardActions, KtCardContent, KtCardHeader, KtCardLink, KtCardMedia } from './card-structure';\n\nexport * from './card';\nexport * from './card-structure';\n\n/**\n * Import ergonomique de toute la famille card en une fois :\n * `imports: [KtCardImports]` au lieu d'énumérer chaque directive.\n */\nexport const KtCardImports = [KtCard, KtCardHeader, KtCardMedia, KtCardContent, KtCardActions, KtCardLink] as const;\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -12,19 +12,31 @@ import { map } from 'rxjs';
|
|
|
12
12
|
* Geste DOUBLÉ côté appelant par Échap + bouton Fermer + tap-extérieur (WCAG 2.5.1) : ce module ne
|
|
13
13
|
* gère que le glissement. Le gating « écran compact uniquement » reste à l'appelant (signal `isCompact`
|
|
14
14
|
* de `KtViewport`) — la primitive est volontairement agnostique.
|
|
15
|
+
*
|
|
16
|
+
* @param opts Élément à translater, callback de fermeture, classe de drag et seuil optionnel.
|
|
17
|
+
* @returns Un contrôleur {@link KtSheetDrag} (`start` à brancher sur `pointerdown`, `destroy` au teardown).
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const drag = createKtSheetDrag({
|
|
21
|
+
* pane: () => paneEl(),
|
|
22
|
+
* onDismiss: () => dialogRef.close(),
|
|
23
|
+
* draggingClass: 'kt-sheet--dragging',
|
|
24
|
+
* });
|
|
25
|
+
* // <div class="handle" (pointerdown)="drag.start($event)"></div>
|
|
26
|
+
* ```
|
|
15
27
|
*/
|
|
16
28
|
function createKtSheetDrag(opts) {
|
|
17
29
|
const threshold = opts.threshold ?? 0.25;
|
|
18
30
|
let startY = 0;
|
|
19
31
|
let pane = null;
|
|
20
32
|
let handle = null;
|
|
33
|
+
let ownerDoc = null;
|
|
21
34
|
const cleanupListeners = () => {
|
|
22
|
-
if (!
|
|
35
|
+
if (!ownerDoc)
|
|
23
36
|
return;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
handle.removeEventListener('lostpointercapture', onEnd);
|
|
37
|
+
ownerDoc.removeEventListener('pointermove', onMove);
|
|
38
|
+
ownerDoc.removeEventListener('pointerup', onEnd);
|
|
39
|
+
ownerDoc.removeEventListener('pointercancel', onEnd);
|
|
28
40
|
};
|
|
29
41
|
const onMove = (event) => {
|
|
30
42
|
if (!pane)
|
|
@@ -36,17 +48,25 @@ function createKtSheetDrag(opts) {
|
|
|
36
48
|
if (!pane || !handle)
|
|
37
49
|
return;
|
|
38
50
|
cleanupListeners();
|
|
39
|
-
pane.classList.remove(opts.draggingClass);
|
|
40
51
|
const dragged = event.clientY - startY;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
const dismiss = dragged > pane.getBoundingClientRect().height * threshold;
|
|
53
|
+
// WCAG 2.3.3 : sous prefers-reduced-motion, on applique l'état final SANS animation.
|
|
54
|
+
const reduceMotion = !!ownerDoc?.defaultView?.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
55
|
+
if (reduceMotion) {
|
|
56
|
+
// draggingClass (transition coupée) encore présente : la translation finale est instantanée,
|
|
57
|
+
// puis on retire la classe (aucun changement de valeur ⇒ aucune animation déclenchée).
|
|
58
|
+
pane.style.translate = dismiss ? '0 100%' : '';
|
|
59
|
+
pane.classList.remove(opts.draggingClass);
|
|
44
60
|
}
|
|
45
61
|
else {
|
|
46
|
-
pane.
|
|
62
|
+
pane.classList.remove(opts.draggingClass); // réactive la transition CSS (glissement / snap-back)
|
|
63
|
+
pane.style.translate = dismiss ? '0 100%' : ''; // dismiss : glisse jusqu'en bas ; sinon snap-back à 0
|
|
47
64
|
}
|
|
65
|
+
if (dismiss)
|
|
66
|
+
opts.onDismiss();
|
|
48
67
|
pane = null;
|
|
49
68
|
handle = null;
|
|
69
|
+
ownerDoc = null;
|
|
50
70
|
};
|
|
51
71
|
return {
|
|
52
72
|
start(event) {
|
|
@@ -56,21 +76,23 @@ function createKtSheetDrag(opts) {
|
|
|
56
76
|
if (!el)
|
|
57
77
|
return;
|
|
58
78
|
handle = event.currentTarget;
|
|
79
|
+
ownerDoc = handle.ownerDocument;
|
|
59
80
|
// La poignée ne doit PAS voler le focus (sinon un close-on-blur fermerait prématurément).
|
|
60
81
|
event.preventDefault();
|
|
61
|
-
handle.setPointerCapture(event.pointerId);
|
|
62
82
|
startY = event.clientY;
|
|
63
83
|
pane = el;
|
|
64
84
|
pane.classList.add(opts.draggingClass);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
85
|
+
// Écoute au niveau du document : un pointer qui sort de la poignée (petite cible) ou une
|
|
86
|
+
// capture perdue n'interrompt plus le suivi du glissement ni le snap-back.
|
|
87
|
+
ownerDoc.addEventListener('pointermove', onMove);
|
|
88
|
+
ownerDoc.addEventListener('pointerup', onEnd);
|
|
89
|
+
ownerDoc.addEventListener('pointercancel', onEnd);
|
|
69
90
|
},
|
|
70
91
|
destroy() {
|
|
71
92
|
cleanupListeners();
|
|
72
93
|
pane = null;
|
|
73
94
|
handle = null;
|
|
95
|
+
ownerDoc = null;
|
|
74
96
|
},
|
|
75
97
|
};
|
|
76
98
|
}
|
|
@@ -84,6 +106,8 @@ const KT_BREAKPOINTS = new InjectionToken('KT_BREAKPOINTS', {
|
|
|
84
106
|
});
|
|
85
107
|
/**
|
|
86
108
|
* À ajouter aux `providers` de `app.config.ts`. Fusionne avec les défauts → surcharge partielle OK.
|
|
109
|
+
* @param overrides Seuils à surcharger ; les clés absentes gardent leur défaut `KT_DEFAULT_BREAKPOINTS`.
|
|
110
|
+
* @returns Provider liant `KT_BREAKPOINTS` aux seuils fusionnés.
|
|
87
111
|
* @example provideKtBreakpoints({ tablet: 768, desktop: 1200 })
|
|
88
112
|
* @example provideKtBreakpoints({ tablet: '(min-width: 768px) and (orientation: landscape)' })
|
|
89
113
|
*/
|
|
@@ -106,6 +130,9 @@ function join(...parts) {
|
|
|
106
130
|
* Convertit les seuils (nombres et/ou chaînes) en 3 media queries. Un palier-chaîne est repris
|
|
107
131
|
* verbatim ; un palier-nombre devient la bande `[plancher courant .. plancher suivant[`. Si le
|
|
108
132
|
* plancher suivant est une chaîne (pas de borne numérique), la bande reste ouverte de ce côté.
|
|
133
|
+
* @param bp Seuils responsive à convertir (nombres et/ou chaînes).
|
|
134
|
+
* @returns Les trois media queries résolues (`mobile`, `tablet`, `desktop`).
|
|
135
|
+
* @example resolveKtBreakpointMedia(KT_DEFAULT_BREAKPOINTS)
|
|
109
136
|
*/
|
|
110
137
|
function resolveKtBreakpointMedia(bp) {
|
|
111
138
|
const { mobile, tablet, desktop } = bp;
|
|
@@ -154,9 +181,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImpor
|
|
|
154
181
|
args: [{ providedIn: 'root' }]
|
|
155
182
|
}] });
|
|
156
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Génère des identifiants numériques incrémentaux uniques au sein de l'application, soit globalement,
|
|
186
|
+
* soit par préfixe (compteur dédié). Sert à construire des `anchor-name` / `id` stables et distincts.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* const idGen = inject(KtIdGenerator);
|
|
191
|
+
* const anchor = `--kt-menu-anchor-${idGen.generateId('menu')}`;
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
157
194
|
class KtIdGenerator {
|
|
158
195
|
counters = new Map();
|
|
159
196
|
nextGlobalId = 0;
|
|
197
|
+
/**
|
|
198
|
+
* Renvoie le prochain identifiant. Sans `prefix`, utilise le compteur global ; avec `prefix`,
|
|
199
|
+
* utilise un compteur dédié à ce préfixe (séquences indépendantes).
|
|
200
|
+
* @param prefix Clé optionnelle de compteur ; chaque préfixe a sa propre séquence.
|
|
201
|
+
* @returns Le compteur courant (number) pour la portée demandée, avant incrément.
|
|
202
|
+
*/
|
|
160
203
|
generateId(prefix) {
|
|
161
204
|
if (!prefix) {
|
|
162
205
|
return this.nextGlobalId++;
|