@ktortu/aaa 0.1.0-beta.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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +151 -0
  3. package/button/button-tokens.css +152 -0
  4. package/button/button.css +319 -0
  5. package/card/card-tokens.css +49 -0
  6. package/card/card.css +200 -0
  7. package/cdk/styles/foundation.css +83 -0
  8. package/cdk/styles/tabs.css +276 -0
  9. package/dialog/dialog.css +350 -0
  10. package/fesm2022/ktortu-aaa-button.mjs +128 -0
  11. package/fesm2022/ktortu-aaa-button.mjs.map +1 -0
  12. package/fesm2022/ktortu-aaa-card.mjs +209 -0
  13. package/fesm2022/ktortu-aaa-card.mjs.map +1 -0
  14. package/fesm2022/ktortu-aaa-cdk.mjs +183 -0
  15. package/fesm2022/ktortu-aaa-cdk.mjs.map +1 -0
  16. package/fesm2022/ktortu-aaa-dialog.mjs +512 -0
  17. package/fesm2022/ktortu-aaa-dialog.mjs.map +1 -0
  18. package/fesm2022/ktortu-aaa-forms.mjs +3215 -0
  19. package/fesm2022/ktortu-aaa-forms.mjs.map +1 -0
  20. package/fesm2022/ktortu-aaa-menu.mjs +315 -0
  21. package/fesm2022/ktortu-aaa-menu.mjs.map +1 -0
  22. package/fesm2022/ktortu-aaa-tabs.mjs +79 -0
  23. package/fesm2022/ktortu-aaa-tabs.mjs.map +1 -0
  24. package/fesm2022/ktortu-aaa-tooltip.mjs +356 -0
  25. package/fesm2022/ktortu-aaa-tooltip.mjs.map +1 -0
  26. package/fesm2022/ktortu-aaa.mjs +17 -0
  27. package/fesm2022/ktortu-aaa.mjs.map +1 -0
  28. package/forms/checkbox/checkbox-group.css +55 -0
  29. package/forms/checkbox/checkbox.css +216 -0
  30. package/forms/chips/chip-list.css +70 -0
  31. package/forms/chips/chip.css +92 -0
  32. package/forms/chips/tokens.css +102 -0
  33. package/forms/field/field.css +87 -0
  34. package/forms/multi-select/multi-select.css +136 -0
  35. package/forms/radio/radio-group.css +55 -0
  36. package/forms/radio/radio.css +165 -0
  37. package/forms/styles/field-box.css +171 -0
  38. package/forms/styles/select-panel.css +464 -0
  39. package/forms/styles/tokens.css +67 -0
  40. package/forms/switch/switch.css +188 -0
  41. package/menu/menu-tokens.css +58 -0
  42. package/menu/menu.css +224 -0
  43. package/package.json +96 -0
  44. package/styles/button.css +6 -0
  45. package/styles/card.css +6 -0
  46. package/styles/dialog.css +6 -0
  47. package/styles/forms.css +13 -0
  48. package/styles/foundation.css +7 -0
  49. package/styles/menu.css +6 -0
  50. package/styles/styles.css +24 -0
  51. package/styles/tabs.css +5 -0
  52. package/styles/tooltip.css +5 -0
  53. package/themes/theme-ant.css +44 -0
  54. package/themes/theme-architecte.css +83 -0
  55. package/themes/theme-aurora.css +97 -0
  56. package/themes/theme-bootstrap.css +46 -0
  57. package/themes/theme-carbon.css +49 -0
  58. package/themes/theme-catppuccin.css +66 -0
  59. package/themes/theme-cyberpunk.css +211 -0
  60. package/themes/theme-fluent.css +45 -0
  61. package/themes/theme-material-you.css +74 -0
  62. package/themes/theme-material.css +48 -0
  63. package/themes/theme-primer.css +46 -0
  64. package/themes/theme-vegetal.css +78 -0
  65. package/tooltip/tooltip.css +129 -0
  66. package/types/ktortu-aaa-button.d.ts +70 -0
  67. package/types/ktortu-aaa-card.d.ts +143 -0
  68. package/types/ktortu-aaa-cdk.d.ts +110 -0
  69. package/types/ktortu-aaa-dialog.d.ts +286 -0
  70. package/types/ktortu-aaa-forms.d.ts +1574 -0
  71. package/types/ktortu-aaa-menu.d.ts +171 -0
  72. package/types/ktortu-aaa-tabs.d.ts +27 -0
  73. package/types/ktortu-aaa-tooltip.d.ts +90 -0
  74. package/types/ktortu-aaa.d.ts +8 -0
@@ -0,0 +1,315 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Directive, inject, ElementRef, PLATFORM_ID, effect, model, InjectionToken, input, computed, isDevMode } from '@angular/core';
3
+ import { isPlatformBrowser } from '@angular/common';
4
+ import { MenuItem, MenuTrigger } from '@angular/aria/menu';
5
+ import { KtIdGenerator } from '@ktortu/aaa/cdk';
6
+
7
+ /**
8
+ * `[ktMenu]` — marqueur de THÈME posé sur l'hôte `[ngMenu]` d'`@angular/aria`. Style UNIQUEMENT la
9
+ * SURFACE du menu (fond, bord, ombre, rayon, padding, positionnement en dropdown ancré). N'ajoute
10
+ * AUCUN rôle ni comportement : la sémantique (`role="menu"`), le clavier, le focus roving, le
11
+ * retour de focus au trigger, les sous-menus et le typeahead viennent intégralement d'aria
12
+ * (`[ngMenu]`). Même philosophie que `[ktCard]` : la lib thématise, aria gère l'accessibilité.
13
+ *
14
+ * La directive n'a pas de logique propre : tout vit dans `menu.css` via le sélecteur `[ktMenu]`,
15
+ * piloté par l'attribut `data-visible` qu'aria pose sur l'hôte (ouvert/fermé). Le positionnement
16
+ * (CSS Anchor Positioning) est câblé par `[ktMenuTrigger]` (menu racine) et par `[ktMenuItem]`
17
+ * (sous-menus) qui posent les `anchor-name` / `position-anchor`.
18
+ *
19
+ * @example
20
+ * ```html
21
+ * <button ngMenuTrigger ktMenuTrigger [menu]="m">Options</button>
22
+ * <div ngMenu ktMenu #m="ngMenu" (itemSelected)="onSelect($event)" aria-label="Options">
23
+ * <button ngMenuItem ktMenuItem value="new">Nouveau</button>
24
+ * </div>
25
+ * ```
26
+ */
27
+ class KtMenu {
28
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMenu, deps: [], target: i0.ɵɵFactoryTarget.Directive });
29
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: KtMenu, isStandalone: true, selector: "[ktMenu]", ngImport: i0 });
30
+ }
31
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMenu, decorators: [{
32
+ type: Directive,
33
+ args: [{ selector: '[ktMenu]' }]
34
+ }] });
35
+ /**
36
+ * `[ktMenuItem]` — marqueur de thème posé sur l'hôte `[ngMenuItem]`. Style la RANGÉE (hauteur de
37
+ * cible tactile ≥44px, focus, survol, désactivé, item de sous-menu avec chevron). Comme `[ktMenu]`,
38
+ * sans logique de sélection : l'état stylé est lu sur les attributs ARIA déjà posés par aria
39
+ * (`:focus-visible` pour l'item actif via focus roving, `[aria-disabled]`, `[aria-expanded]`,
40
+ * `[aria-haspopup]`, `[aria-checked]`).
41
+ *
42
+ * SEULE responsabilité technique : si l'item porte un sous-menu (`[submenu]` côté aria), on câble
43
+ * l'ancrage CSS du sous-menu sur cet item (anchor-name sur l'item, position-anchor sur la surface
44
+ * du sous-menu, marqueur `data-kt-submenu` pour l'ouvrir en latéral plutôt qu'en dropdown).
45
+ *
46
+ * EXIGENCE DE STRUCTURE (sous-menus) : le `[ngMenu]` d'un sous-menu DOIT être un DESCENDANT DOM du
47
+ * menu parent (et non un frère). aria décide de maintenir un menu ouvert via `element.contains()`
48
+ * sur la cible du focus/survol ; un sous-menu placé hors du sous-arbre du parent ferait croire à
49
+ * aria que le focus a quitté le menu et provoquerait sa fermeture dès qu'on survole le sous-menu.
50
+ * Le sous-menu reste rendu en place (top-layer non requis : `position: fixed` + ancrage CSS).
51
+ *
52
+ * @example
53
+ * ```html
54
+ * <button ngMenuItem ktMenuItem value="open">Ouvrir</button>
55
+ * ```
56
+ */
57
+ class KtMenuItem {
58
+ host = inject(ElementRef).nativeElement;
59
+ // Optionnel : `[ktMenuItem]` reste utilisable seul (simple thème) même hors d'un `[ngMenuItem]`.
60
+ ariaItem = inject(MenuItem, { self: true, optional: true });
61
+ platformId = inject(PLATFORM_ID);
62
+ // Alloué à la PREMIÈRE détection d'un sous-menu (et non pour chaque item) : un item simple ne
63
+ // consomme pas d'anchor-name.
64
+ anchorName;
65
+ idGen = inject(KtIdGenerator);
66
+ constructor() {
67
+ if (!this.ariaItem)
68
+ return;
69
+ effect(() => {
70
+ const submenu = this.ariaItem.submenu()?.element;
71
+ if (!submenu) {
72
+ this.anchorName = undefined;
73
+ return;
74
+ }
75
+ this.anchorName ??= `--kt-submenu-anchor-${this.idGen.generateId()}`;
76
+ if (isPlatformBrowser(this.platformId)) {
77
+ submenu.style.setProperty('position-anchor', this.anchorName);
78
+ // Marque la surface comme un SOUS-menu : menu.css l'ouvre en latéral (inline-end) et non
79
+ // sous l'item, avec ses propres fallbacks de débordement.
80
+ submenu.setAttribute('data-kt-submenu', '');
81
+ }
82
+ });
83
+ }
84
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
85
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: KtMenuItem, isStandalone: true, selector: "[ktMenuItem]", host: { properties: { "style.anchor-name": "anchorName" } }, ngImport: i0 });
86
+ }
87
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMenuItem, decorators: [{
88
+ type: Directive,
89
+ args: [{
90
+ selector: '[ktMenuItem]',
91
+ host: {
92
+ '[style.anchor-name]': 'anchorName',
93
+ },
94
+ }]
95
+ }], ctorParameters: () => [] });
96
+ /**
97
+ * `[ktMenuSeparator]` — filet de séparation entre groupes d'items. Pose `role="separator"`
98
+ * (sémantique standard d'un séparateur de menu) ; la ligne elle-même vit dans `menu.css`.
99
+ *
100
+ * @example
101
+ * ```html
102
+ * <hr ktMenuSeparator />
103
+ * ```
104
+ */
105
+ class KtMenuSeparator {
106
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMenuSeparator, deps: [], target: i0.ɵɵFactoryTarget.Directive });
107
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: KtMenuSeparator, isStandalone: true, selector: "[ktMenuSeparator]", host: { attributes: { "role": "separator" } }, ngImport: i0 });
108
+ }
109
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMenuSeparator, decorators: [{
110
+ type: Directive,
111
+ args: [{
112
+ selector: '[ktMenuSeparator]',
113
+ host: { role: 'separator' },
114
+ }]
115
+ }] });
116
+
117
+ /**
118
+ * `[ktMenuTrigger]` — sucre de POSITIONNEMENT pour le menu racine. À poser SUR le même élément
119
+ * qu'`[ngMenuTrigger]` (le bouton qui ouvre le menu). Il ne réimplémente RIEN du comportement :
120
+ * l'ouverture/fermeture, `aria-expanded`, Échap, le clic-dehors et le retour de focus sont gérés
121
+ * par aria (`[ngMenuTrigger]`). Sa seule tâche : poser un `anchor-name` sur le trigger et le
122
+ * `position-anchor` correspondant sur la surface du menu, pour que `menu.css` l'ancre en dropdown
123
+ * (CSS Anchor Positioning), comme le popup du Select — sans CDK Overlay (styles scopés conservés,
124
+ * et pas de coordination fragile avec le focus qu'aria pose à l'ouverture).
125
+ *
126
+ * Requiert `[ngMenuTrigger]` sur le même hôte (injecté en `self`).
127
+ *
128
+ * @example
129
+ * ```html
130
+ * <button ngMenuTrigger ktMenuTrigger [menu]="m">Fichier</button>
131
+ * ```
132
+ */
133
+ class KtMenuTrigger {
134
+ host = inject(ElementRef).nativeElement;
135
+ ariaTrigger = inject(MenuTrigger, { self: true });
136
+ platformId = inject(PLATFORM_ID);
137
+ idGen = inject(KtIdGenerator);
138
+ anchorName = `--kt-menu-anchor-${this.idGen.generateId()}`;
139
+ constructor() {
140
+ effect(() => {
141
+ const menuEl = this.ariaTrigger.menu()?.element;
142
+ if (!menuEl)
143
+ return;
144
+ if (isPlatformBrowser(this.platformId)) {
145
+ menuEl.style.setProperty('position-anchor', this.anchorName);
146
+ }
147
+ });
148
+ }
149
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMenuTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
150
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: KtMenuTrigger, isStandalone: true, selector: "[ktMenuTrigger]", host: { properties: { "style.anchor-name": "anchorName" } }, ngImport: i0 });
151
+ }
152
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMenuTrigger, decorators: [{
153
+ type: Directive,
154
+ args: [{
155
+ selector: '[ktMenuTrigger]',
156
+ host: {
157
+ '[style.anchor-name]': 'anchorName',
158
+ },
159
+ }]
160
+ }], ctorParameters: () => [] });
161
+
162
+ /**
163
+ * Items de menu À ÉTAT (checkbox / radio).
164
+ *
165
+ * Pourquoi ces directives existent : `@angular/aria` accepte bien `role="menuitemcheckbox"` /
166
+ * `"menuitemradio"` sur `[ngMenuItem]`, MAIS NE POSE JAMAIS `aria-checked` — son pattern se
167
+ * contente d'émettre `itemSelected(value)`. Sans `aria-checked`, l'item coché est MUET pour les
168
+ * lecteurs d'écran (échec WCAG 4.1.2). Ces directives comblent ce trou : elles possèdent l'état
169
+ * coché, le bindent en `aria-checked`, et le basculent à l'activation (clic / Entrée / Espace).
170
+ *
171
+ * Elles ne touchent pas au `role` (laissé à l'input `role` d'aria, posé par le consommateur) pour
172
+ * éviter tout conflit de binding sur `[attr.role]` ; la démo montre l'usage `role="menuitemradio"`.
173
+ */
174
+ /** Activation commune : un item de menu se déclenche au clic ET au clavier (Entrée / Espace). */
175
+ const ACTIVATION_HOST = {
176
+ '(click)': 'activate($event)',
177
+ '(keydown.enter)': 'activate($event)',
178
+ '(keydown.space)': 'activate($event)',
179
+ };
180
+ /**
181
+ * `[ktMenuItemCheckbox]` — case à cocher de menu (bascule indépendante). À poser sur un
182
+ * `[ngMenuItem] role="menuitemcheckbox"`. Bind `aria-checked` et bascule `checked` à l'activation.
183
+ *
184
+ * @example
185
+ * ```html
186
+ * <button ngMenuItem ktMenuItem ktMenuItemCheckbox role="menuitemcheckbox" [(checked)]="wrap">
187
+ * Retour à la ligne
188
+ * </button>
189
+ * ```
190
+ */
191
+ class KtMenuItemCheckbox {
192
+ ariaItem = inject(MenuItem, { self: true, optional: true });
193
+ /** État coché, bidirectionnel : `[(checked)]`. */
194
+ checked = model(false, /* @ts-ignore */
195
+ ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
196
+ activate(event) {
197
+ if (this.ariaItem?.disabled())
198
+ return;
199
+ if (event.type === 'keydown') {
200
+ event.preventDefault();
201
+ }
202
+ this.checked.update((v) => !v);
203
+ }
204
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMenuItemCheckbox, deps: [], target: i0.ɵɵFactoryTarget.Directive });
205
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: KtMenuItemCheckbox, isStandalone: true, selector: "[ktMenuItemCheckbox]", inputs: { checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { checked: "checkedChange" }, host: { listeners: { "click": "activate($event)", "keydown.enter": "activate($event)", "keydown.space": "activate($event)" }, properties: { "attr.aria-checked": "checked()" } }, ngImport: i0 });
206
+ }
207
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMenuItemCheckbox, decorators: [{
208
+ type: Directive,
209
+ args: [{
210
+ selector: '[ktMenuItemCheckbox]',
211
+ host: {
212
+ ...ACTIVATION_HOST,
213
+ '[attr.aria-checked]': 'checked()',
214
+ },
215
+ }]
216
+ }], propDecorators: { checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }, { type: i0.Output, args: ["checkedChange"] }] } });
217
+ /** Source de la valeur sélectionnée d'un groupe radio, exposée aux `[ktMenuItemRadio]` enfants. */
218
+ const KT_MENU_RADIO_GROUP = new InjectionToken('KT_MENU_RADIO_GROUP');
219
+ /**
220
+ * `[ktMenuRadioGroup]` — coordinateur d'un groupe d'items radio mutuellement exclusifs. À poser sur
221
+ * un conteneur englobant les `[ktMenuItemRadio]` (typiquement un `role="group"`). Détient la valeur
222
+ * sélectionnée ; chaque radio s'y compare pour son `aria-checked` et la met à jour à l'activation.
223
+ *
224
+ * @example
225
+ * ```html
226
+ * <div role="group" ktMenuRadioGroup [(value)]="sortBy" aria-label="Trier par">
227
+ * <button ngMenuItem ktMenuItem ktMenuItemRadio role="menuitemradio" [value]="'name'">Nom</button>
228
+ * <button ngMenuItem ktMenuItem ktMenuItemRadio role="menuitemradio" [value]="'date'">Date</button>
229
+ * </div>
230
+ * ```
231
+ */
232
+ class KtMenuRadioGroup {
233
+ /** Valeur sélectionnée du groupe, bidirectionnelle : `[(value)]`. */
234
+ value = model(null, /* @ts-ignore */
235
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
236
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMenuRadioGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
237
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: KtMenuRadioGroup, isStandalone: true, selector: "[ktMenuRadioGroup]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, providers: [{ provide: KT_MENU_RADIO_GROUP, useExisting: KtMenuRadioGroup }], exportAs: ["ktMenuRadioGroup"], ngImport: i0 });
238
+ }
239
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMenuRadioGroup, decorators: [{
240
+ type: Directive,
241
+ args: [{
242
+ selector: '[ktMenuRadioGroup]',
243
+ exportAs: 'ktMenuRadioGroup',
244
+ providers: [{ provide: KT_MENU_RADIO_GROUP, useExisting: KtMenuRadioGroup }],
245
+ }]
246
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }] } });
247
+ /**
248
+ * `[ktMenuItemRadio]` — bouton radio de menu. À poser sur un `[ngMenuItem] role="menuitemradio"`
249
+ * dans un `[ktMenuRadioGroup]`. `aria-checked` reflète l'égalité avec la valeur du groupe ;
250
+ * l'activation sélectionne cette valeur.
251
+ *
252
+ * @example
253
+ * ```html
254
+ * <button ngMenuItem ktMenuItem ktMenuItemRadio role="menuitemradio" [value]="'name'">Trier par nom</button>
255
+ * ```
256
+ */
257
+ class KtMenuItemRadio {
258
+ ariaItem = inject(MenuItem, { self: true, optional: true });
259
+ group = inject(KT_MENU_RADIO_GROUP, { optional: true });
260
+ /** Valeur portée par ce radio (comparée à celle du groupe). */
261
+ value = input.required(/* @ts-ignore */
262
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
263
+ checked = computed(() => this.group?.value() === this.value(), /* @ts-ignore */
264
+ ...(ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []));
265
+ constructor() {
266
+ // Garde-fou dev : sans [ktMenuRadioGroup] parent, aria-checked reste figé (item muet pour les SR).
267
+ if (isDevMode() && !this.group) {
268
+ console.warn('[ktMenuItemRadio] doit être placé dans un [ktMenuRadioGroup] : ' +
269
+ 'sans groupe, la sélection et `aria-checked` restent inopérants.');
270
+ }
271
+ }
272
+ activate(event) {
273
+ if (this.ariaItem?.disabled())
274
+ return;
275
+ if (event.type === 'keydown') {
276
+ event.preventDefault();
277
+ }
278
+ this.group?.value.set(this.value());
279
+ }
280
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMenuItemRadio, deps: [], target: i0.ɵɵFactoryTarget.Directive });
281
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.1", type: KtMenuItemRadio, isStandalone: true, selector: "[ktMenuItemRadio]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null } }, host: { listeners: { "click": "activate($event)", "keydown.enter": "activate($event)", "keydown.space": "activate($event)" }, properties: { "attr.aria-checked": "checked()" } }, ngImport: i0 });
282
+ }
283
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtMenuItemRadio, decorators: [{
284
+ type: Directive,
285
+ args: [{
286
+ selector: '[ktMenuItemRadio]',
287
+ host: {
288
+ ...ACTIVATION_HOST,
289
+ '[attr.aria-checked]': 'checked()',
290
+ },
291
+ }]
292
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }] } });
293
+
294
+ /**
295
+ * Import ergonomique de toute la famille menu (couche de THÈME ktortu) en une fois :
296
+ * `imports: [KT_MENU]` au lieu d'énumérer chaque directive. À composer avec les directives
297
+ * d'`@angular/aria/menu` (`Menu`, `MenuItem`, `MenuTrigger`, `MenuContent`), qui apportent le
298
+ * comportement accessible.
299
+ */
300
+ const KT_MENU = [
301
+ KtMenu,
302
+ KtMenuItem,
303
+ KtMenuSeparator,
304
+ KtMenuTrigger,
305
+ KtMenuItemCheckbox,
306
+ KtMenuItemRadio,
307
+ KtMenuRadioGroup,
308
+ ];
309
+
310
+ /**
311
+ * Generated bundle index. Do not edit.
312
+ */
313
+
314
+ export { KT_MENU, KT_MENU_RADIO_GROUP, KtMenu, KtMenuItem, KtMenuItemCheckbox, KtMenuItemRadio, KtMenuRadioGroup, KtMenuSeparator, KtMenuTrigger };
315
+ //# sourceMappingURL=ktortu-aaa-menu.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ktortu-aaa-menu.mjs","sources":["../../../../projects/ktortu/aaa/menu/menu.ts","../../../../projects/ktortu/aaa/menu/menu-trigger.ts","../../../../projects/ktortu/aaa/menu/menu-toggle.ts","../../../../projects/ktortu/aaa/menu/public-api.ts","../../../../projects/ktortu/aaa/menu/ktortu-aaa-menu.ts"],"sourcesContent":["import { Directive, ElementRef, effect, inject, PLATFORM_ID } from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\nimport { MenuItem as AriaMenuItem } from '@angular/aria/menu';\nimport { KtIdGenerator } from '@ktortu/aaa/cdk';\n\n/**\n * `[ktMenu]` — marqueur de THÈME posé sur l'hôte `[ngMenu]` d'`@angular/aria`. Style UNIQUEMENT la\n * SURFACE du menu (fond, bord, ombre, rayon, padding, positionnement en dropdown ancré). N'ajoute\n * AUCUN rôle ni comportement : la sémantique (`role=\"menu\"`), le clavier, le focus roving, le\n * retour de focus au trigger, les sous-menus et le typeahead viennent intégralement d'aria\n * (`[ngMenu]`). Même philosophie que `[ktCard]` : la lib thématise, aria gère l'accessibilité.\n *\n * La directive n'a pas de logique propre : tout vit dans `menu.css` via le sélecteur `[ktMenu]`,\n * piloté par l'attribut `data-visible` qu'aria pose sur l'hôte (ouvert/fermé). Le positionnement\n * (CSS Anchor Positioning) est câblé par `[ktMenuTrigger]` (menu racine) et par `[ktMenuItem]`\n * (sous-menus) qui posent les `anchor-name` / `position-anchor`.\n *\n * @example\n * ```html\n * <button ngMenuTrigger ktMenuTrigger [menu]=\"m\">Options</button>\n * <div ngMenu ktMenu #m=\"ngMenu\" (itemSelected)=\"onSelect($event)\" aria-label=\"Options\">\n * <button ngMenuItem ktMenuItem value=\"new\">Nouveau</button>\n * </div>\n * ```\n */\n@Directive({ selector: '[ktMenu]' })\nexport class KtMenu {}\n\n/**\n * `[ktMenuItem]` — marqueur de thème posé sur l'hôte `[ngMenuItem]`. Style la RANGÉE (hauteur de\n * cible tactile ≥44px, focus, survol, désactivé, item de sous-menu avec chevron). Comme `[ktMenu]`,\n * sans logique de sélection : l'état stylé est lu sur les attributs ARIA déjà posés par aria\n * (`:focus-visible` pour l'item actif via focus roving, `[aria-disabled]`, `[aria-expanded]`,\n * `[aria-haspopup]`, `[aria-checked]`).\n *\n * SEULE responsabilité technique : si l'item porte un sous-menu (`[submenu]` côté aria), on câble\n * l'ancrage CSS du sous-menu sur cet item (anchor-name sur l'item, position-anchor sur la surface\n * du sous-menu, marqueur `data-kt-submenu` pour l'ouvrir en latéral plutôt qu'en dropdown).\n *\n * EXIGENCE DE STRUCTURE (sous-menus) : le `[ngMenu]` d'un sous-menu DOIT être un DESCENDANT DOM du\n * menu parent (et non un frère). aria décide de maintenir un menu ouvert via `element.contains()`\n * sur la cible du focus/survol ; un sous-menu placé hors du sous-arbre du parent ferait croire à\n * aria que le focus a quitté le menu et provoquerait sa fermeture dès qu'on survole le sous-menu.\n * Le sous-menu reste rendu en place (top-layer non requis : `position: fixed` + ancrage CSS).\n *\n * @example\n * ```html\n * <button ngMenuItem ktMenuItem value=\"open\">Ouvrir</button>\n * ```\n */\n@Directive({\n selector: '[ktMenuItem]',\n host: {\n '[style.anchor-name]': 'anchorName',\n },\n})\nexport class KtMenuItem {\n private readonly host = inject(ElementRef).nativeElement;\n // Optionnel : `[ktMenuItem]` reste utilisable seul (simple thème) même hors d'un `[ngMenuItem]`.\n private readonly ariaItem = inject(AriaMenuItem, { self: true, optional: true });\n private readonly platformId = inject(PLATFORM_ID);\n\n // Alloué à la PREMIÈRE détection d'un sous-menu (et non pour chaque item) : un item simple ne\n // consomme pas d'anchor-name.\n protected anchorName: string | undefined;\n private readonly idGen = inject(KtIdGenerator);\n\n constructor() {\n if (!this.ariaItem) return;\n\n effect(() => {\n const submenu = this.ariaItem!.submenu()?.element;\n if (!submenu) {\n this.anchorName = undefined;\n return;\n }\n this.anchorName ??= `--kt-submenu-anchor-${this.idGen.generateId()}`;\n if (isPlatformBrowser(this.platformId)) {\n submenu.style.setProperty('position-anchor', this.anchorName);\n // Marque la surface comme un SOUS-menu : menu.css l'ouvre en latéral (inline-end) et non\n // sous l'item, avec ses propres fallbacks de débordement.\n submenu.setAttribute('data-kt-submenu', '');\n }\n });\n }\n}\n\n/**\n * `[ktMenuSeparator]` — filet de séparation entre groupes d'items. Pose `role=\"separator\"`\n * (sémantique standard d'un séparateur de menu) ; la ligne elle-même vit dans `menu.css`.\n *\n * @example\n * ```html\n * <hr ktMenuSeparator />\n * ```\n */\n@Directive({\n selector: '[ktMenuSeparator]',\n host: { role: 'separator' },\n})\nexport class KtMenuSeparator {}\n","import { Directive, ElementRef, effect, inject, PLATFORM_ID } from '@angular/core';\nimport { isPlatformBrowser } from '@angular/common';\nimport { MenuTrigger as AriaMenuTrigger } from '@angular/aria/menu';\nimport { KtIdGenerator } from '@ktortu/aaa/cdk';\n\n/**\n * `[ktMenuTrigger]` — sucre de POSITIONNEMENT pour le menu racine. À poser SUR le même élément\n * qu'`[ngMenuTrigger]` (le bouton qui ouvre le menu). Il ne réimplémente RIEN du comportement :\n * l'ouverture/fermeture, `aria-expanded`, Échap, le clic-dehors et le retour de focus sont gérés\n * par aria (`[ngMenuTrigger]`). Sa seule tâche : poser un `anchor-name` sur le trigger et le\n * `position-anchor` correspondant sur la surface du menu, pour que `menu.css` l'ancre en dropdown\n * (CSS Anchor Positioning), comme le popup du Select — sans CDK Overlay (styles scopés conservés,\n * et pas de coordination fragile avec le focus qu'aria pose à l'ouverture).\n *\n * Requiert `[ngMenuTrigger]` sur le même hôte (injecté en `self`).\n *\n * @example\n * ```html\n * <button ngMenuTrigger ktMenuTrigger [menu]=\"m\">Fichier</button>\n * ```\n */\n@Directive({\n selector: '[ktMenuTrigger]',\n host: {\n '[style.anchor-name]': 'anchorName',\n },\n})\nexport class KtMenuTrigger {\n private readonly host = inject(ElementRef).nativeElement;\n private readonly ariaTrigger = inject(AriaMenuTrigger, { self: true });\n private readonly platformId = inject(PLATFORM_ID);\n\n private readonly idGen = inject(KtIdGenerator);\n protected readonly anchorName = `--kt-menu-anchor-${this.idGen.generateId()}`;\n\n constructor() {\n effect(() => {\n const menuEl = this.ariaTrigger.menu()?.element;\n if (!menuEl) return;\n if (isPlatformBrowser(this.platformId)) {\n menuEl.style.setProperty('position-anchor', this.anchorName);\n }\n });\n }\n}\n","import { Directive, InjectionToken, computed, inject, input, isDevMode, model } from '@angular/core';\nimport { MenuItem as AriaMenuItem } from '@angular/aria/menu';\n\n/**\n * Items de menu À ÉTAT (checkbox / radio).\n *\n * Pourquoi ces directives existent : `@angular/aria` accepte bien `role=\"menuitemcheckbox\"` /\n * `\"menuitemradio\"` sur `[ngMenuItem]`, MAIS NE POSE JAMAIS `aria-checked` — son pattern se\n * contente d'émettre `itemSelected(value)`. Sans `aria-checked`, l'item coché est MUET pour les\n * lecteurs d'écran (échec WCAG 4.1.2). Ces directives comblent ce trou : elles possèdent l'état\n * coché, le bindent en `aria-checked`, et le basculent à l'activation (clic / Entrée / Espace).\n *\n * Elles ne touchent pas au `role` (laissé à l'input `role` d'aria, posé par le consommateur) pour\n * éviter tout conflit de binding sur `[attr.role]` ; la démo montre l'usage `role=\"menuitemradio\"`.\n */\n\n/** Activation commune : un item de menu se déclenche au clic ET au clavier (Entrée / Espace). */\nconst ACTIVATION_HOST = {\n '(click)': 'activate($event)',\n '(keydown.enter)': 'activate($event)',\n '(keydown.space)': 'activate($event)',\n} as const;\n\n/**\n * `[ktMenuItemCheckbox]` — case à cocher de menu (bascule indépendante). À poser sur un\n * `[ngMenuItem] role=\"menuitemcheckbox\"`. Bind `aria-checked` et bascule `checked` à l'activation.\n *\n * @example\n * ```html\n * <button ngMenuItem ktMenuItem ktMenuItemCheckbox role=\"menuitemcheckbox\" [(checked)]=\"wrap\">\n * Retour à la ligne\n * </button>\n * ```\n */\n@Directive({\n selector: '[ktMenuItemCheckbox]',\n host: {\n ...ACTIVATION_HOST,\n '[attr.aria-checked]': 'checked()',\n },\n})\nexport class KtMenuItemCheckbox {\n private readonly ariaItem = inject(AriaMenuItem, { self: true, optional: true });\n\n /** État coché, bidirectionnel : `[(checked)]`. */\n readonly checked = model<boolean>(false);\n\n protected activate(event: Event): void {\n if (this.ariaItem?.disabled()) return;\n if (event.type === 'keydown') {\n event.preventDefault();\n }\n this.checked.update((v) => !v);\n }\n}\n\n/** Source de la valeur sélectionnée d'un groupe radio, exposée aux `[ktMenuItemRadio]` enfants. */\nexport const KT_MENU_RADIO_GROUP = new InjectionToken<KtMenuRadioGroup<unknown>>('KT_MENU_RADIO_GROUP');\n\n/**\n * `[ktMenuRadioGroup]` — coordinateur d'un groupe d'items radio mutuellement exclusifs. À poser sur\n * un conteneur englobant les `[ktMenuItemRadio]` (typiquement un `role=\"group\"`). Détient la valeur\n * sélectionnée ; chaque radio s'y compare pour son `aria-checked` et la met à jour à l'activation.\n *\n * @example\n * ```html\n * <div role=\"group\" ktMenuRadioGroup [(value)]=\"sortBy\" aria-label=\"Trier par\">\n * <button ngMenuItem ktMenuItem ktMenuItemRadio role=\"menuitemradio\" [value]=\"'name'\">Nom</button>\n * <button ngMenuItem ktMenuItem ktMenuItemRadio role=\"menuitemradio\" [value]=\"'date'\">Date</button>\n * </div>\n * ```\n */\n@Directive({\n selector: '[ktMenuRadioGroup]',\n exportAs: 'ktMenuRadioGroup',\n providers: [{ provide: KT_MENU_RADIO_GROUP, useExisting: KtMenuRadioGroup }],\n})\nexport class KtMenuRadioGroup<V> {\n /** Valeur sélectionnée du groupe, bidirectionnelle : `[(value)]`. */\n readonly value = model<V | null>(null);\n}\n\n/**\n * `[ktMenuItemRadio]` — bouton radio de menu. À poser sur un `[ngMenuItem] role=\"menuitemradio\"`\n * dans un `[ktMenuRadioGroup]`. `aria-checked` reflète l'égalité avec la valeur du groupe ;\n * l'activation sélectionne cette valeur.\n *\n * @example\n * ```html\n * <button ngMenuItem ktMenuItem ktMenuItemRadio role=\"menuitemradio\" [value]=\"'name'\">Trier par nom</button>\n * ```\n */\n@Directive({\n selector: '[ktMenuItemRadio]',\n host: {\n ...ACTIVATION_HOST,\n '[attr.aria-checked]': 'checked()',\n },\n})\nexport class KtMenuItemRadio<V> {\n private readonly ariaItem = inject(AriaMenuItem, { self: true, optional: true });\n private readonly group = inject<KtMenuRadioGroup<V>>(KT_MENU_RADIO_GROUP, { optional: true });\n\n /** Valeur portée par ce radio (comparée à celle du groupe). */\n readonly value = input.required<V>();\n\n protected readonly checked = computed(() => this.group?.value() === this.value());\n\n constructor() {\n // Garde-fou dev : sans [ktMenuRadioGroup] parent, aria-checked reste figé (item muet pour les SR).\n if (isDevMode() && !this.group) {\n console.warn(\n '[ktMenuItemRadio] doit être placé dans un [ktMenuRadioGroup] : ' +\n 'sans groupe, la sélection et `aria-checked` restent inopérants.',\n );\n }\n }\n\n protected activate(event: Event): void {\n if (this.ariaItem?.disabled()) return;\n if (event.type === 'keydown') {\n event.preventDefault();\n }\n this.group?.value.set(this.value());\n }\n}\n","import { KtMenu, KtMenuItem, KtMenuSeparator } from './menu';\nimport { KtMenuTrigger } from './menu-trigger';\nimport { KtMenuItemCheckbox, KtMenuItemRadio, KtMenuRadioGroup } from './menu-toggle';\n\nexport * from './menu';\nexport * from './menu-trigger';\nexport * from './menu-toggle';\n\n/**\n * Import ergonomique de toute la famille menu (couche de THÈME ktortu) en une fois :\n * `imports: [KT_MENU]` au lieu d'énumérer chaque directive. À composer avec les directives\n * d'`@angular/aria/menu` (`Menu`, `MenuItem`, `MenuTrigger`, `MenuContent`), qui apportent le\n * comportement accessible.\n */\nexport const KT_MENU = [\n KtMenu,\n KtMenuItem,\n KtMenuSeparator,\n KtMenuTrigger,\n KtMenuItemCheckbox,\n KtMenuItemRadio,\n KtMenuRadioGroup,\n] as const;\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":["AriaMenuItem","AriaMenuTrigger"],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ import * as i0 from '@angular/core';
2
+ import { inject, ElementRef, afterRenderEffect, Directive } from '@angular/core';
3
+ import { TabList } from '@angular/aria/tabs';
4
+
5
+ /**
6
+ * Amène l'onglet sélectionné dans la zone visible quand la liste d'onglets déborde et défile
7
+ * (cf. tabs.css). Le focus clavier d'`@angular/aria` scrolle déjà l'onglet focalisé ; cette
8
+ * directive couvre ce que le CSS seul ne peut pas faire de façon fiable : l'onglet actif
9
+ * **hors-champ au montage** et lors d'un **changement de sélection programmatique**.
10
+ *
11
+ * Opt-in, à poser sur `[ngTabList]` :
12
+ *
13
+ * @example
14
+ * ```html
15
+ * <ul ngTabList ktTabScroll [(selectedTab)]="tab"> … </ul>
16
+ * ```
17
+ *
18
+ * Intégrée réactivement avec `@angular/aria/tabs` via son `ModelSignal` `selectedTab`.
19
+ */
20
+ class KtTabScroll {
21
+ list = inject(ElementRef).nativeElement;
22
+ tabList = inject(TabList);
23
+ constructor() {
24
+ // afterRenderEffect est la primitive réactive d'Angular v22 conçue spécifiquement
25
+ // pour exécuter des effets dépendant de signaux APRES la mise à jour complète du DOM et du layout.
26
+ afterRenderEffect(() => {
27
+ // Enregistrer la dépendance réactive sur le signal de sélection
28
+ this.tabList.selectedTab();
29
+ // Exécuter le scroll (le DOM est garanti stable et à jour)
30
+ this.scrollSelectedIntoView();
31
+ });
32
+ }
33
+ scrollSelectedIntoView() {
34
+ const selected = this.list.querySelector('[ngTab][aria-selected="true"]');
35
+ if (!selected)
36
+ return;
37
+ const container = this.list;
38
+ const isVertical = container.getAttribute('aria-orientation') === 'vertical' || container.getAttribute('orientation') === 'vertical';
39
+ if (isVertical) {
40
+ const selectedTop = selected.offsetTop;
41
+ const selectedBottom = selectedTop + selected.offsetHeight;
42
+ const containerTop = container.scrollTop;
43
+ const containerBottom = containerTop + container.clientHeight;
44
+ if (selectedTop < containerTop) {
45
+ container.scrollTo({ top: selectedTop, behavior: 'smooth' });
46
+ }
47
+ else if (selectedBottom > containerBottom) {
48
+ container.scrollTo({ top: selectedBottom - container.clientHeight, behavior: 'smooth' });
49
+ }
50
+ }
51
+ else {
52
+ const selectedLeft = selected.offsetLeft;
53
+ const selectedRight = selectedLeft + selected.offsetWidth;
54
+ const containerLeft = container.scrollLeft;
55
+ const containerRight = containerLeft + container.clientWidth;
56
+ if (selectedLeft < containerLeft) {
57
+ container.scrollTo({ left: selectedLeft, behavior: 'smooth' });
58
+ }
59
+ else if (selectedRight > containerRight) {
60
+ container.scrollTo({ left: selectedRight - container.clientWidth, behavior: 'smooth' });
61
+ }
62
+ }
63
+ }
64
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtTabScroll, deps: [], target: i0.ɵɵFactoryTarget.Directive });
65
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.1", type: KtTabScroll, isStandalone: true, selector: "[ngTabList][ktTabScroll]", ngImport: i0 });
66
+ }
67
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: KtTabScroll, decorators: [{
68
+ type: Directive,
69
+ args: [{
70
+ selector: '[ngTabList][ktTabScroll]',
71
+ }]
72
+ }], ctorParameters: () => [] });
73
+
74
+ /**
75
+ * Generated bundle index. Do not edit.
76
+ */
77
+
78
+ export { KtTabScroll };
79
+ //# sourceMappingURL=ktortu-aaa-tabs.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ktortu-aaa-tabs.mjs","sources":["../../../../projects/ktortu/aaa/tabs/tab-scroll.ts","../../../../projects/ktortu/aaa/tabs/ktortu-aaa-tabs.ts"],"sourcesContent":["import { Directive, ElementRef, afterRenderEffect, inject } from '@angular/core';\nimport { TabList } from '@angular/aria/tabs';\n\n/**\n * Amène l'onglet sélectionné dans la zone visible quand la liste d'onglets déborde et défile\n * (cf. tabs.css). Le focus clavier d'`@angular/aria` scrolle déjà l'onglet focalisé ; cette\n * directive couvre ce que le CSS seul ne peut pas faire de façon fiable : l'onglet actif\n * **hors-champ au montage** et lors d'un **changement de sélection programmatique**.\n *\n * Opt-in, à poser sur `[ngTabList]` :\n *\n * @example\n * ```html\n * <ul ngTabList ktTabScroll [(selectedTab)]=\"tab\"> … </ul>\n * ```\n *\n * Intégrée réactivement avec `@angular/aria/tabs` via son `ModelSignal` `selectedTab`.\n */\n@Directive({\n selector: '[ngTabList][ktTabScroll]',\n})\nexport class KtTabScroll {\n private readonly list = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n private readonly tabList = inject(TabList);\n\n constructor() {\n // afterRenderEffect est la primitive réactive d'Angular v22 conçue spécifiquement\n // pour exécuter des effets dépendant de signaux APRES la mise à jour complète du DOM et du layout.\n afterRenderEffect(() => {\n // Enregistrer la dépendance réactive sur le signal de sélection\n this.tabList.selectedTab();\n\n // Exécuter le scroll (le DOM est garanti stable et à jour)\n this.scrollSelectedIntoView();\n });\n }\n\n private scrollSelectedIntoView(): void {\n const selected = this.list.querySelector<HTMLElement>('[ngTab][aria-selected=\"true\"]');\n if (!selected) return;\n\n const container = this.list;\n const isVertical =\n container.getAttribute('aria-orientation') === 'vertical' || container.getAttribute('orientation') === 'vertical';\n\n if (isVertical) {\n const selectedTop = selected.offsetTop;\n const selectedBottom = selectedTop + selected.offsetHeight;\n const containerTop = container.scrollTop;\n const containerBottom = containerTop + container.clientHeight;\n\n if (selectedTop < containerTop) {\n container.scrollTo({ top: selectedTop, behavior: 'smooth' });\n } else if (selectedBottom > containerBottom) {\n container.scrollTo({ top: selectedBottom - container.clientHeight, behavior: 'smooth' });\n }\n } else {\n const selectedLeft = selected.offsetLeft;\n const selectedRight = selectedLeft + selected.offsetWidth;\n const containerLeft = container.scrollLeft;\n const containerRight = containerLeft + container.clientWidth;\n\n if (selectedLeft < containerLeft) {\n container.scrollTo({ left: selectedLeft, behavior: 'smooth' });\n } else if (selectedRight > containerRight) {\n container.scrollTo({ left: selectedRight - container.clientWidth, behavior: 'smooth' });\n }\n }\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;;;AAGA;;;;;;;;;;;;;;AAcG;MAIU,WAAW,CAAA;AACL,IAAA,IAAI,GAAG,MAAM,CAA0B,UAAU,CAAC,CAAC,aAAa;AAChE,IAAA,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;AAE1C,IAAA,WAAA,GAAA;;;QAGE,iBAAiB,CAAC,MAAK;;AAErB,YAAA,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE;;YAG1B,IAAI,CAAC,sBAAsB,EAAE;AAC/B,QAAA,CAAC,CAAC;IACJ;IAEQ,sBAAsB,GAAA;QAC5B,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,CAAc,+BAA+B,CAAC;AACtF,QAAA,IAAI,CAAC,QAAQ;YAAE;AAEf,QAAA,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI;AAC3B,QAAA,MAAM,UAAU,GACd,SAAS,CAAC,YAAY,CAAC,kBAAkB,CAAC,KAAK,UAAU,IAAI,SAAS,CAAC,YAAY,CAAC,aAAa,CAAC,KAAK,UAAU;QAEnH,IAAI,UAAU,EAAE;AACd,YAAA,MAAM,WAAW,GAAG,QAAQ,CAAC,SAAS;AACtC,YAAA,MAAM,cAAc,GAAG,WAAW,GAAG,QAAQ,CAAC,YAAY;AAC1D,YAAA,MAAM,YAAY,GAAG,SAAS,CAAC,SAAS;AACxC,YAAA,MAAM,eAAe,GAAG,YAAY,GAAG,SAAS,CAAC,YAAY;AAE7D,YAAA,IAAI,WAAW,GAAG,YAAY,EAAE;AAC9B,gBAAA,SAAS,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;YAC9D;AAAO,iBAAA,IAAI,cAAc,GAAG,eAAe,EAAE;AAC3C,gBAAA,SAAS,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,cAAc,GAAG,SAAS,CAAC,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;YAC1F;QACF;aAAO;AACL,YAAA,MAAM,YAAY,GAAG,QAAQ,CAAC,UAAU;AACxC,YAAA,MAAM,aAAa,GAAG,YAAY,GAAG,QAAQ,CAAC,WAAW;AACzD,YAAA,MAAM,aAAa,GAAG,SAAS,CAAC,UAAU;AAC1C,YAAA,MAAM,cAAc,GAAG,aAAa,GAAG,SAAS,CAAC,WAAW;AAE5D,YAAA,IAAI,YAAY,GAAG,aAAa,EAAE;AAChC,gBAAA,SAAS,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;YAChE;AAAO,iBAAA,IAAI,aAAa,GAAG,cAAc,EAAE;AACzC,gBAAA,SAAS,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,aAAa,GAAG,SAAS,CAAC,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;YACzF;QACF;IACF;uGA/CW,WAAW,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;2FAAX,WAAW,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,0BAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA;;2FAAX,WAAW,EAAA,UAAA,EAAA,CAAA;kBAHvB,SAAS;AAAC,YAAA,IAAA,EAAA,CAAA;AACT,oBAAA,QAAQ,EAAE,0BAA0B;AACrC,iBAAA;;;ACpBD;;AAEG;;;;"}