@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.
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/button/button-tokens.css +152 -0
- package/button/button.css +319 -0
- package/card/card-tokens.css +49 -0
- package/card/card.css +200 -0
- package/cdk/styles/foundation.css +83 -0
- package/cdk/styles/tabs.css +276 -0
- package/dialog/dialog.css +350 -0
- package/fesm2022/ktortu-aaa-button.mjs +128 -0
- package/fesm2022/ktortu-aaa-button.mjs.map +1 -0
- package/fesm2022/ktortu-aaa-card.mjs +209 -0
- package/fesm2022/ktortu-aaa-card.mjs.map +1 -0
- package/fesm2022/ktortu-aaa-cdk.mjs +183 -0
- package/fesm2022/ktortu-aaa-cdk.mjs.map +1 -0
- package/fesm2022/ktortu-aaa-dialog.mjs +512 -0
- package/fesm2022/ktortu-aaa-dialog.mjs.map +1 -0
- package/fesm2022/ktortu-aaa-forms.mjs +3215 -0
- package/fesm2022/ktortu-aaa-forms.mjs.map +1 -0
- package/fesm2022/ktortu-aaa-menu.mjs +315 -0
- package/fesm2022/ktortu-aaa-menu.mjs.map +1 -0
- package/fesm2022/ktortu-aaa-tabs.mjs +79 -0
- package/fesm2022/ktortu-aaa-tabs.mjs.map +1 -0
- package/fesm2022/ktortu-aaa-tooltip.mjs +356 -0
- package/fesm2022/ktortu-aaa-tooltip.mjs.map +1 -0
- package/fesm2022/ktortu-aaa.mjs +17 -0
- package/fesm2022/ktortu-aaa.mjs.map +1 -0
- package/forms/checkbox/checkbox-group.css +55 -0
- package/forms/checkbox/checkbox.css +216 -0
- package/forms/chips/chip-list.css +70 -0
- package/forms/chips/chip.css +92 -0
- package/forms/chips/tokens.css +102 -0
- package/forms/field/field.css +87 -0
- package/forms/multi-select/multi-select.css +136 -0
- package/forms/radio/radio-group.css +55 -0
- package/forms/radio/radio.css +165 -0
- package/forms/styles/field-box.css +171 -0
- package/forms/styles/select-panel.css +464 -0
- package/forms/styles/tokens.css +67 -0
- package/forms/switch/switch.css +188 -0
- package/menu/menu-tokens.css +58 -0
- package/menu/menu.css +224 -0
- package/package.json +96 -0
- package/styles/button.css +6 -0
- package/styles/card.css +6 -0
- package/styles/dialog.css +6 -0
- package/styles/forms.css +13 -0
- package/styles/foundation.css +7 -0
- package/styles/menu.css +6 -0
- package/styles/styles.css +24 -0
- package/styles/tabs.css +5 -0
- package/styles/tooltip.css +5 -0
- package/themes/theme-ant.css +44 -0
- package/themes/theme-architecte.css +83 -0
- package/themes/theme-aurora.css +97 -0
- package/themes/theme-bootstrap.css +46 -0
- package/themes/theme-carbon.css +49 -0
- package/themes/theme-catppuccin.css +66 -0
- package/themes/theme-cyberpunk.css +211 -0
- package/themes/theme-fluent.css +45 -0
- package/themes/theme-material-you.css +74 -0
- package/themes/theme-material.css +48 -0
- package/themes/theme-primer.css +46 -0
- package/themes/theme-vegetal.css +78 -0
- package/tooltip/tooltip.css +129 -0
- package/types/ktortu-aaa-button.d.ts +70 -0
- package/types/ktortu-aaa-card.d.ts +143 -0
- package/types/ktortu-aaa-cdk.d.ts +110 -0
- package/types/ktortu-aaa-dialog.d.ts +286 -0
- package/types/ktortu-aaa-forms.d.ts +1574 -0
- package/types/ktortu-aaa-menu.d.ts +171 -0
- package/types/ktortu-aaa-tabs.d.ts +27 -0
- package/types/ktortu-aaa-tooltip.d.ts +90 -0
- package/types/ktortu-aaa.d.ts +8 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ktortu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the software, and to permit persons to whom the software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# @ktortu/aaa
|
|
2
|
+
|
|
3
|
+
Bibliothèque de composants Angular **headless + thémés par tokens CSS** (`--kt-*`).
|
|
4
|
+
Les composants sont des directives/composants accessibles (appui sur `@angular/aria` / CDK) ;
|
|
5
|
+
leur apparence vit dans des feuilles CSS globales que vous importez à part.
|
|
6
|
+
|
|
7
|
+
## Convention de nommage
|
|
8
|
+
|
|
9
|
+
- **Selectors** (templates) : préfixe `kt` — `[ktButton]`, `<kt-text-field>`, `[ktDialogTitle]`…
|
|
10
|
+
- **Symboles TypeScript** : préfixe `Kt` — classes/directives/pipes `KtX` (`KtButton`,
|
|
11
|
+
`KtSelect`…), tokens `KT_X` (`KT_BUTTON_CONFIG`, `KT_BREAKPOINTS`…). Ce préfixe évite les
|
|
12
|
+
collisions d'imports chez les consommateurs.
|
|
13
|
+
- **Exception assumée** : le namespace natif `Temporal` et ses alias de types
|
|
14
|
+
(`Timestamp`, `CalendarDate`, `WallTime`, `LocalDateTime`, `ZonedTimestamp`) ne sont pas
|
|
15
|
+
préfixés, pour rester ergonomiques côté dates.
|
|
16
|
+
|
|
17
|
+
## Utilisation (TypeScript)
|
|
18
|
+
|
|
19
|
+
Chaque famille est importable depuis son point d'entrée `@ktortu/aaa/<feature>` ; les utilitaires
|
|
20
|
+
transverses (breakpoints, viewport, sheet-drag) depuis la racine `@ktortu/aaa`.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { Component } from '@angular/core';
|
|
24
|
+
import { KtButton } from '@ktortu/aaa/button';
|
|
25
|
+
import { KtTextField } from '@ktortu/aaa/forms';
|
|
26
|
+
|
|
27
|
+
@Component({
|
|
28
|
+
selector: 'app-exemple',
|
|
29
|
+
imports: [KtButton, KtTextField],
|
|
30
|
+
template: `
|
|
31
|
+
<kt-text-field label="E-mail" type="email" [(value)]="email" />
|
|
32
|
+
<button ktButton mode="filled" color="primary">Enregistrer</button>
|
|
33
|
+
`,
|
|
34
|
+
})
|
|
35
|
+
export class Exemple {
|
|
36
|
+
email = '';
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Points d'entrée : `@ktortu/aaa/button`, `/card`, `/dialog`, `/menu`, `/tabs`, `/tooltip`,
|
|
41
|
+
`/forms`, `/themes`, et la racine `@ktortu/aaa` (breakpoints, viewport, sheet-drag).
|
|
42
|
+
Familles agrégées prêtes pour `imports:` : `KT_CARD`, `KT_MENU` (ex. `imports: [...KT_CARD]`).
|
|
43
|
+
Le chrome de champ générique (`KtField` + `KtFieldControl`) enveloppe un contrôle natif :
|
|
44
|
+
`<kt-field label="…"><input ktFieldControl [(ngModel)]="…" /></kt-field>`.
|
|
45
|
+
|
|
46
|
+
### Configuration
|
|
47
|
+
|
|
48
|
+
Les valeurs par défaut sont surchargeables par token d'injection (`KT_*_CONFIG`,
|
|
49
|
+
type `Partial<…>`) ou via les helpers `provideKt*` :
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { KT_BUTTON_CONFIG } from '@ktortu/aaa/button';
|
|
53
|
+
import { provideKtBreakpoints } from '@ktortu/aaa';
|
|
54
|
+
import { provideKtDialogDefaults } from '@ktortu/aaa/dialog';
|
|
55
|
+
|
|
56
|
+
providers: [
|
|
57
|
+
{ provide: KT_BUTTON_CONFIG, useValue: { size: 'lg' } },
|
|
58
|
+
provideKtBreakpoints({ tablet: 768, desktop: 1200 }),
|
|
59
|
+
provideKtDialogDefaults({ maxWidth: '40rem' }),
|
|
60
|
+
];
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Styles (CSS)
|
|
64
|
+
|
|
65
|
+
L'apparence vit dans des **feuilles CSS globales** thémées par des tokens `--kt-*` : importez le
|
|
66
|
+
CSS de la lib en plus des composants.
|
|
67
|
+
|
|
68
|
+
### Mise en place minimale
|
|
69
|
+
|
|
70
|
+
Importez la feuille **agrégée** (socle + styles de base de tous les composants, ordre de cascade
|
|
71
|
+
garanti) :
|
|
72
|
+
|
|
73
|
+
```css
|
|
74
|
+
/* dans votre styles.css global */
|
|
75
|
+
@import '@ktortu/aaa/styles.css';
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
…ou via `angular.json` :
|
|
79
|
+
|
|
80
|
+
```jsonc
|
|
81
|
+
"styles": [
|
|
82
|
+
"node_modules/@ktortu/aaa/styles.css",
|
|
83
|
+
"src/styles.css"
|
|
84
|
+
]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
> Le **Dialog** s'appuie sur l'overlay du CDK : ajoutez aussi
|
|
88
|
+
> `@import '@angular/cdk/overlay-prebuilt.css';`.
|
|
89
|
+
|
|
90
|
+
### Thèmes (optionnels, à la carte)
|
|
91
|
+
|
|
92
|
+
Les thèmes ne sont **pas** inclus dans `styles.css`. Importez ceux que vous voulez, **après** :
|
|
93
|
+
|
|
94
|
+
```css
|
|
95
|
+
@import '@ktortu/aaa/styles.css';
|
|
96
|
+
@import '@ktortu/aaa/themes/theme-material.css';
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Thèmes disponibles (`@ktortu/aaa/themes/theme-<id>.css`) :
|
|
100
|
+
`material`, `material-you`, `primer`, `carbon`, `fluent`, `ant`, `bootstrap`,
|
|
101
|
+
`catppuccin`, `architecte`, `vegetal`, `cyberpunk`, `aurora`.
|
|
102
|
+
|
|
103
|
+
Un thème = des redéclarations de tokens `--kt-*`. Surcharger un seul `--kt-*` (ex. `--kt-primary`)
|
|
104
|
+
rebrande toute la lib.
|
|
105
|
+
|
|
106
|
+
### Import à la carte (socle + composants choisis)
|
|
107
|
+
|
|
108
|
+
Pour n'embarquer que ce qui vous intéresse, importez le **socle** (requis) puis **un fichier par
|
|
109
|
+
composant**. Chaque bundle inclut déjà ses propres tokens — pas besoin d'importer les `*-tokens`
|
|
110
|
+
séparément.
|
|
111
|
+
|
|
112
|
+
```css
|
|
113
|
+
@import '@ktortu/aaa/foundation.css'; /* REQUIS — socle de tokens --kt-*, à mettre en premier */
|
|
114
|
+
@import '@ktortu/aaa/menu.css'; /* puis uniquement les composants utilisés */
|
|
115
|
+
@import '@ktortu/aaa/button.css';
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Bundles disponibles :
|
|
119
|
+
|
|
120
|
+
| Import | Contenu |
|
|
121
|
+
| ---------------------------- | ------------------------------------------------------------ |
|
|
122
|
+
| `@ktortu/aaa/foundation.css` | **socle de tokens `--kt-*`** (requis, à importer en premier) |
|
|
123
|
+
| `@ktortu/aaa/button.css` | bouton (`ktButton`) |
|
|
124
|
+
| `@ktortu/aaa/card.css` | carte (`ktCard` + marqueurs) |
|
|
125
|
+
| `@ktortu/aaa/menu.css` | menu (`ktMenu`, `ktMenuItem`, …) |
|
|
126
|
+
| `@ktortu/aaa/tooltip.css` | tooltip (`ktTooltip`) |
|
|
127
|
+
| `@ktortu/aaa/dialog.css` | dialog (requiert aussi l'overlay CDK) |
|
|
128
|
+
| `@ktortu/aaa/forms.css` | base des formulaires (champs, chips, switch) |
|
|
129
|
+
| `@ktortu/aaa/tabs.css` | onglets |
|
|
130
|
+
|
|
131
|
+
> `foundation.css` doit **toujours** être importé en premier (les bundles composant en dérivent).
|
|
132
|
+
> Si vous utilisez plusieurs composants, l'agrégat `@ktortu/aaa/styles.css` fait tout cela dans le
|
|
133
|
+
> bon ordre.
|
|
134
|
+
>
|
|
135
|
+
> Les formulaires « riches » (Select, MultiSelect, Field, Chips) embarquent leur CSS via les
|
|
136
|
+
> composants eux-mêmes (`styleUrl`) : rien à importer en plus pour ceux-ci.
|
|
137
|
+
|
|
138
|
+
## Développement
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
ng build @ktortu/aaa # build de la lib -> dist/ktortu/aaa
|
|
142
|
+
ng test # tests unitaires (Vitest)
|
|
143
|
+
ng serve demo # app de démo / documentation vivante (port 4210)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Publication
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
cd dist/ktortu/aaa
|
|
150
|
+
npm publish
|
|
151
|
+
```
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/* Design tokens du bouton (directive ktButton) — contrat public de theming.
|
|
2
|
+
|
|
3
|
+
PRINCIPE : ne saisir que les 3 fills de base (--btn-primary / --btn-neutral / --btn-danger).
|
|
4
|
+
Tout le reste — containers tonals, fg outlined/text, états — en DÉRIVE automatiquement
|
|
5
|
+
via color-mix(). Chaque valeur dérivée reste surchargeable : il suffit de REDÉCLARER son
|
|
6
|
+
token (sur :root plus bas, ou sur un conteneur de spécificité ≥). C'est la cascade qui
|
|
7
|
+
tranche, pas un fallback var() — donc pas de token « -override » à part.
|
|
8
|
+
|
|
9
|
+
3 paliers :
|
|
10
|
+
1. Bases saisies (4 tokens).
|
|
11
|
+
2. Rôles : labels statiques (garantie AA, voir plus bas) + container/fg dérivés.
|
|
12
|
+
3. Réglages globaux : leviers de dérivation, state-layer, focus, géométrie.
|
|
13
|
+
|
|
14
|
+
NB : ne PAS enregistrer ces tokens en @property avec initial-value (ça neutraliserait la
|
|
15
|
+
surcharge par redéclaration). */
|
|
16
|
+
@layer kt-aaa.tokens {
|
|
17
|
+
:root {
|
|
18
|
+
/* ============ PALIER 1 — BASES (dérivées du socle --kt-*) ============ */
|
|
19
|
+
/* Saisir les couleurs au niveau du socle (src/app/styles/foundation.css) rebrande
|
|
20
|
+
champs ET boutons d'un coup ; surcharger ici reste possible pour le bouton seul. */
|
|
21
|
+
--btn-primary: var(--kt-primary);
|
|
22
|
+
--btn-neutral: var(--kt-neutral);
|
|
23
|
+
--btn-danger: var(--kt-danger);
|
|
24
|
+
|
|
25
|
+
/* Surface de référence : base des fills tonals + désaturation disabled.
|
|
26
|
+
Re-pointer --kt-surface suffit pour un futur dark mode. */
|
|
27
|
+
--btn-surface: var(--kt-surface, #ffffff);
|
|
28
|
+
|
|
29
|
+
/* ============ PALIER 2 — RÔLES ============ */
|
|
30
|
+
|
|
31
|
+
/* Labels = STATIQUES et vérifiés AA : c'est la garantie axe, appliquée dans TOUS les
|
|
32
|
+
navigateurs. (contrast-color() est proposé en bonus opt-in tout en bas — il ne garantit
|
|
33
|
+
pas le 4.5:1 sur les tons moyens.) Pour une marque custom, redéclarer ces on-* aussi. */
|
|
34
|
+
--btn-on-primary: #ffffff; /* sur fill rempli (~6.4:1) */
|
|
35
|
+
--btn-on-neutral: #ffffff; /* ~13:1 */
|
|
36
|
+
--btn-on-danger: #ffffff; /* ~6.5:1 */
|
|
37
|
+
--btn-on-primary-container: #0842a0; /* sur fill tonal (~7:1) */
|
|
38
|
+
--btn-on-neutral-container: #1b1c1d; /* ~15:1 */
|
|
39
|
+
--btn-on-danger-container: #8c1d18; /* ~7:1 (le "piège" tonal+danger) */
|
|
40
|
+
|
|
41
|
+
/* Fills tonals = DÉRIVÉS : base mélangée vers la surface (auto-désature → reste pâle,
|
|
42
|
+
pas de clipping de gamut). Poids réglable via --btn-tonal-weight. */
|
|
43
|
+
--btn-primary-container: color-mix(in oklab, var(--btn-primary) var(--btn-tonal-weight, 16%), var(--btn-surface));
|
|
44
|
+
--btn-neutral-container: color-mix(in oklab, var(--btn-neutral) var(--btn-tonal-weight, 16%), var(--btn-surface));
|
|
45
|
+
--btn-danger-container: color-mix(in oklab, var(--btn-danger) var(--btn-tonal-weight, 16%), var(--btn-surface));
|
|
46
|
+
|
|
47
|
+
/* fg outlined/text = DÉRIVÉ : base légèrement assombrie (garde la teinte de marque, gagne
|
|
48
|
+
de la marge de contraste sur la page). Poids réglable via --btn-fg-weight. */
|
|
49
|
+
--btn-primary-fg: color-mix(in oklab, var(--btn-primary) var(--btn-fg-weight, 90%), black);
|
|
50
|
+
--btn-neutral-fg: color-mix(in oklab, var(--btn-neutral) var(--btn-fg-weight, 90%), black);
|
|
51
|
+
--btn-danger-fg: color-mix(in oklab, var(--btn-danger) var(--btn-fg-weight, 90%), black);
|
|
52
|
+
|
|
53
|
+
/* ============ PALIER 3 — RÉGLAGES GLOBAUX ============ */
|
|
54
|
+
|
|
55
|
+
/* Leviers de dérivation */
|
|
56
|
+
--btn-tonal-weight: 16%; /* poids de la base dans le fill tonal (plus haut = plus saturé) */
|
|
57
|
+
--btn-fg-weight: 90%; /* poids de la base dans le fg (plus bas = plus sombre) */
|
|
58
|
+
|
|
59
|
+
/* Icône : padding RÉDUIT côté icône (asymétrie leading/trailing, règle M3 « −8dp côté icône »)
|
|
60
|
+
et taille d'icône absolue par taille. */
|
|
61
|
+
--btn-icon-pad-x-sm: 6px;
|
|
62
|
+
--btn-icon-pad-x-md: 10px;
|
|
63
|
+
--btn-icon-pad-x-lg: 12px;
|
|
64
|
+
--btn-icon-size-sm: 18px;
|
|
65
|
+
--btn-icon-size-md: 20px;
|
|
66
|
+
--btn-icon-size-lg: 22px;
|
|
67
|
+
|
|
68
|
+
/* Forme : token de BASCULE --btn-radius (NON déclaré ici exprès).
|
|
69
|
+
Non défini => chaque taille garde son radius (--btn-radius-sm/md/lg).
|
|
70
|
+
Défini (ex. `--btn-radius: 999px` sur :root ou un conteneur) => pilule sur tous les boutons
|
|
71
|
+
ET cercle sur les icon-only (carrés). Les champs (--field-radius) ne sont pas affectés. */
|
|
72
|
+
|
|
73
|
+
/* Ombres par état : tokens de BASCULE (NON déclarés ici exprès, même principe que --btn-radius).
|
|
74
|
+
Non définis => box-shadow: none. Un thème déclare --btn-shadow (repos), --btn-shadow-hover,
|
|
75
|
+
--btn-shadow-focus pour un glow/elevation (hover/focus retombent sur --btn-shadow si absents) ;
|
|
76
|
+
disabled force toujours `none`. Pour référencer --btn-fill/--btn-fg (couleur du mode résolu),
|
|
77
|
+
les déclarer sur un sélecteur d'ÉLÉMENT bouton, jamais sur :root (substitution var() au
|
|
78
|
+
point de déclaration). */
|
|
79
|
+
|
|
80
|
+
/* Bordure : --btn-border-width et --btn-border-style, tokens de BASCULE (NON déclarés)
|
|
81
|
+
=> 1px solid. La bordure étant réservée sur tous les modes, l'épaissir ne provoque aucun
|
|
82
|
+
reflow entre modes. Longhands côté consommation : width/style acceptent 1 à 4 valeurs
|
|
83
|
+
(par côté, ex. `solid dashed` = haut/bas pleins + côtés tirets) ; --btn-border (couleur)
|
|
84
|
+
doit rester UNE couleur (elle passe dans un color-mix au disabled). */
|
|
85
|
+
|
|
86
|
+
/* Mouvement : tokens de BASCULE (NON déclarés ici exprès) => bouton inerte par défaut.
|
|
87
|
+
--btn-transition (ex. `transform 160ms ease`), --btn-transform (repos),
|
|
88
|
+
--btn-transform-hover / --btn-transform-active (retombent sur --btn-transform si absents) ;
|
|
89
|
+
disabled force `transform: none`. Ne PAS mettre box-shadow dans --btn-transition si un thème
|
|
90
|
+
anime déjà l'ombre en continu (la transition lutterait contre l'animation). */
|
|
91
|
+
|
|
92
|
+
/* Autres BASCULES non déclarées (défauts entre parenthèses) :
|
|
93
|
+
- Fill en image : --btn-fill-image (none) — dégradé/motif empilé SOUS le voile d'état ;
|
|
94
|
+
disabled force background-image: none. Pour dériver du mode résolu (var(--btn-fill)),
|
|
95
|
+
le poser sur un sélecteur d'ÉLÉMENT (substitution au point de déclaration).
|
|
96
|
+
- Focus ring : --btn-focus-ring-style (solid).
|
|
97
|
+
- Typo d'emphase : --btn-text-transform (none), --btn-letter-spacing (normal),
|
|
98
|
+
--btn-font-weight (inherit). */
|
|
99
|
+
|
|
100
|
+
/* State layer (voile d'état, opacités Material 3) */
|
|
101
|
+
--btn-state-hover-opacity: 0.08;
|
|
102
|
+
--btn-state-focus-opacity: 0.12;
|
|
103
|
+
--btn-state-pressed-opacity: 0.16;
|
|
104
|
+
|
|
105
|
+
/* Anneau de focus (opaque, distinct du voile) — couleur/épaisseur depuis le socle ;
|
|
106
|
+
offset propre au bouton (2px) vs field (1px). */
|
|
107
|
+
--btn-focus-ring-width: var(--kt-focus-ring-width);
|
|
108
|
+
--btn-focus-ring-offset: 2px;
|
|
109
|
+
--btn-focus-ring-color: var(
|
|
110
|
+
--kt-focus-ring-color
|
|
111
|
+
); /* opaque + offset => contraste calculable, indépendant du mode */
|
|
112
|
+
|
|
113
|
+
/* Géométrie (par taille). md dérive du socle (= field) ; sm/lg propres au bouton. */
|
|
114
|
+
--btn-radius-sm: 6px;
|
|
115
|
+
--btn-radius-md: var(--kt-control-radius);
|
|
116
|
+
--btn-radius-lg: 10px;
|
|
117
|
+
|
|
118
|
+
--btn-height-sm: 32px; /* >= 24px (cible AA) via padding ; voir min-width sm */
|
|
119
|
+
--btn-height-md: var(--kt-control-height); /* taille confort (touch / AAA), alignée field */
|
|
120
|
+
--btn-height-lg: 52px;
|
|
121
|
+
|
|
122
|
+
--btn-pad-x-sm: 0.625rem;
|
|
123
|
+
--btn-pad-x-md: 1rem;
|
|
124
|
+
--btn-pad-x-lg: 1.25rem;
|
|
125
|
+
|
|
126
|
+
--btn-font-sm: 0.875rem;
|
|
127
|
+
--btn-font-md: var(--kt-control-font);
|
|
128
|
+
--btn-font-lg: 1.0625rem;
|
|
129
|
+
|
|
130
|
+
--btn-gap: 0.5rem;
|
|
131
|
+
--btn-corner-shape: round;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* ============ BONUS (opt-in) — labels auto-contrastés pour palette 100% custom ============
|
|
135
|
+
Décommentez pour laisser le navigateur choisir noir/blanc sur les on-* quand vous changez
|
|
136
|
+
les bases sans redéfinir les labels. ATTENTION : contrast-color() ne garantit PAS le 4.5:1
|
|
137
|
+
sur les tons moyens — revalidez axe. Désactivé par défaut car il remplacerait les labels de
|
|
138
|
+
marque (ex. #0842a0) par du noir pur. Sans support navigateur (~26%), le @supports ne
|
|
139
|
+
s'active pas et les on-* statiques ci-dessus restent appliqués (aucune régression). */
|
|
140
|
+
/*
|
|
141
|
+
@supports (color: contrast-color(red)) {
|
|
142
|
+
:root {
|
|
143
|
+
--btn-on-primary: contrast-color(var(--btn-primary));
|
|
144
|
+
--btn-on-neutral: contrast-color(var(--btn-neutral));
|
|
145
|
+
--btn-on-danger: contrast-color(var(--btn-danger));
|
|
146
|
+
--btn-on-primary-container: contrast-color(var(--btn-primary-container));
|
|
147
|
+
--btn-on-neutral-container: contrast-color(var(--btn-neutral-container));
|
|
148
|
+
--btn-on-danger-container: contrast-color(var(--btn-danger-container));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
*/
|
|
152
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
@layer kt-aaa.components {
|
|
2
|
+
/* Styles de la directive Button (button[ktButton], a[ktButton]).
|
|
3
|
+
Global : enregistré dans angular.json > styles, applicable partout où la directive est posée.
|
|
4
|
+
Tokens (--btn-*) définis dans button-tokens.css (chargé avant ce fichier).
|
|
5
|
+
|
|
6
|
+
Deux axes orthogonaux :
|
|
7
|
+
- mode (apparence) : data-mode = filled | tonal | outlined | text
|
|
8
|
+
- color (intention) : data-color = primary | neutral | danger
|
|
9
|
+
La règle de base ne consomme que 4 variables résolues (--btn-fill / --btn-fg /
|
|
10
|
+
--btn-border / --btn-state-layer) ; chaque bloc [data-mode][data-color] les pose. */
|
|
11
|
+
|
|
12
|
+
[ktButton] {
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
gap: var(--btn-gap, 0.5rem);
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
|
|
19
|
+
min-height: var(--btn-height-md, 44px);
|
|
20
|
+
padding-block: 0;
|
|
21
|
+
/* Variables résolues par taille (padding label / padding côté icône / taille d'icône). */
|
|
22
|
+
--btn-pad-x: var(--btn-pad-x-md, 1rem);
|
|
23
|
+
--btn-icon-pad-x: var(--btn-icon-pad-x-md, 10px);
|
|
24
|
+
--btn-icon-size: var(--btn-icon-size-md, 20px);
|
|
25
|
+
padding-inline: var(--btn-pad-x);
|
|
26
|
+
font-size: var(--btn-font-md, 1rem);
|
|
27
|
+
|
|
28
|
+
/* Bordure réservée (outlined ne reflow pas) ; largeur surchargeable par token (uniforme sur
|
|
29
|
+
tous les modes => pas de reflow entre modes non plus). Longhands : width et style acceptent
|
|
30
|
+
1 à 4 valeurs (par côté) ; --btn-border reste UNE couleur (color-mix au disabled). */
|
|
31
|
+
border-width: var(--btn-border-width, 1px);
|
|
32
|
+
border-style: var(--btn-border-style, solid);
|
|
33
|
+
border-color: var(--btn-border, transparent);
|
|
34
|
+
/* --btn-radius non défini => radius par taille ; le définir (ex. 999px) => pilule / cercle. */
|
|
35
|
+
border-radius: var(--btn-radius, var(--btn-radius-md, 8px));
|
|
36
|
+
corner-shape: var(--btn-corner-shape, round);
|
|
37
|
+
text-decoration: none; /* reset du lien quand la directive est sur un <a> */
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
|
|
40
|
+
/* Défauts résolus = filled + primary. Surchargés par les blocs [data-mode][data-color]. */
|
|
41
|
+
--btn-fill: var(--btn-primary);
|
|
42
|
+
--btn-fg: var(--btn-on-primary);
|
|
43
|
+
--btn-border: transparent;
|
|
44
|
+
--btn-state-layer: var(--btn-on-primary);
|
|
45
|
+
|
|
46
|
+
/* State layer SANS pseudo-élément : un aplat (--btn-overlay) empilé par-dessus le fill.
|
|
47
|
+
::before (icône) et ::after (spinner) sont déjà pris ; on passe donc par background-image.
|
|
48
|
+
--btn-fill-image (bascule, défaut none) : fill en dégradé/motif, empilé SOUS le voile
|
|
49
|
+
d'état pour que hover/focus/pressed restent visibles par-dessus. */
|
|
50
|
+
background-color: var(--btn-fill);
|
|
51
|
+
background-image:
|
|
52
|
+
linear-gradient(var(--btn-overlay, transparent), var(--btn-overlay, transparent)), var(--btn-fill-image, none);
|
|
53
|
+
color: var(--btn-fg);
|
|
54
|
+
|
|
55
|
+
/* Typo d'emphase : bascules => neutres par défaut (boutons UPPERCASE façon Material 2,
|
|
56
|
+
tracking éditorial…). */
|
|
57
|
+
text-transform: var(--btn-text-transform, none);
|
|
58
|
+
letter-spacing: var(--btn-letter-spacing, normal);
|
|
59
|
+
font-weight: var(--btn-font-weight, inherit);
|
|
60
|
+
|
|
61
|
+
/* Ombres par état : tokens de bascule NON déclarés à :root (cf. button-tokens.css)
|
|
62
|
+
=> `none` par défaut. Un thème peut les poser sur l'élément pour référencer --btn-fill/--btn-fg
|
|
63
|
+
(substitution var() au point de déclaration : impossible depuis :root). */
|
|
64
|
+
box-shadow: var(--btn-shadow, none);
|
|
65
|
+
|
|
66
|
+
/* Translucidité (verre) : bascule => aucun effet par défaut (invisible derrière un fill
|
|
67
|
+
opaque ; agit avec un fill semi-transparent, cf. theme-liquid-glass). */
|
|
68
|
+
backdrop-filter: var(--btn-backdrop-filter, none);
|
|
69
|
+
|
|
70
|
+
/* Mouvement : mêmes tokens de bascule (cf. button-tokens.css) => inertes par défaut. */
|
|
71
|
+
transition: var(--btn-transition, none);
|
|
72
|
+
transform: var(--btn-transform, none);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ============ TAILLES ============ */
|
|
76
|
+
[ktButton][data-size='sm'] {
|
|
77
|
+
position: relative; /* requis pour le positionnement de ::after */
|
|
78
|
+
min-height: var(--btn-height-sm, 32px);
|
|
79
|
+
min-width: var(--btn-height-sm, 32px); /* garde une cible >= 24px même pour un label court */
|
|
80
|
+
--btn-pad-x: var(--btn-pad-x-sm, 0.625rem);
|
|
81
|
+
--btn-icon-pad-x: var(--btn-icon-pad-x-sm, 6px);
|
|
82
|
+
--btn-icon-size: var(--btn-icon-size-sm, 18px);
|
|
83
|
+
font-size: var(--btn-font-sm, 0.875rem);
|
|
84
|
+
border-radius: var(--btn-radius, var(--btn-radius-sm, 6px));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
[ktButton][data-size='lg'] {
|
|
88
|
+
min-height: var(--btn-height-lg, 52px);
|
|
89
|
+
--btn-pad-x: var(--btn-pad-x-lg, 1.25rem);
|
|
90
|
+
--btn-icon-pad-x: var(--btn-icon-pad-x-lg, 12px);
|
|
91
|
+
--btn-icon-size: var(--btn-icon-size-lg, 22px);
|
|
92
|
+
font-size: var(--btn-font-lg, 1.0625rem);
|
|
93
|
+
border-radius: var(--btn-radius, var(--btn-radius-lg, 10px));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ============ PLEINE LARGEUR ============ */
|
|
97
|
+
[ktButton][data-full-width] {
|
|
98
|
+
width: 100%;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* ============ RÉSOLUTION mode × couleur (4 × 3) ============ */
|
|
102
|
+
|
|
103
|
+
/* --- FILLED : fill plein, label on-color --- */
|
|
104
|
+
[ktButton][data-mode='filled'][data-color='primary'] {
|
|
105
|
+
--btn-fill: var(--btn-primary);
|
|
106
|
+
--btn-fg: var(--btn-on-primary);
|
|
107
|
+
--btn-border: transparent;
|
|
108
|
+
--btn-state-layer: var(--btn-on-primary);
|
|
109
|
+
}
|
|
110
|
+
[ktButton][data-mode='filled'][data-color='neutral'] {
|
|
111
|
+
--btn-fill: var(--btn-neutral);
|
|
112
|
+
--btn-fg: var(--btn-on-neutral);
|
|
113
|
+
--btn-border: transparent;
|
|
114
|
+
--btn-state-layer: var(--btn-on-neutral);
|
|
115
|
+
}
|
|
116
|
+
[ktButton][data-mode='filled'][data-color='danger'] {
|
|
117
|
+
--btn-fill: var(--btn-danger);
|
|
118
|
+
--btn-fg: var(--btn-on-danger);
|
|
119
|
+
--btn-border: transparent;
|
|
120
|
+
--btn-state-layer: var(--btn-on-danger);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* --- TONAL : fill container (faible chroma), label on-container --- */
|
|
124
|
+
[ktButton][data-mode='tonal'][data-color='primary'] {
|
|
125
|
+
--btn-fill: var(--btn-primary-container);
|
|
126
|
+
--btn-fg: var(--btn-on-primary-container);
|
|
127
|
+
--btn-border: transparent;
|
|
128
|
+
--btn-state-layer: var(--btn-on-primary-container);
|
|
129
|
+
}
|
|
130
|
+
[ktButton][data-mode='tonal'][data-color='neutral'] {
|
|
131
|
+
--btn-fill: var(--btn-neutral-container);
|
|
132
|
+
--btn-fg: var(--btn-on-neutral-container);
|
|
133
|
+
--btn-border: transparent;
|
|
134
|
+
--btn-state-layer: var(--btn-on-neutral-container);
|
|
135
|
+
}
|
|
136
|
+
[ktButton][data-mode='tonal'][data-color='danger'] {
|
|
137
|
+
--btn-fill: var(--btn-danger-container);
|
|
138
|
+
--btn-fg: var(--btn-on-danger-container);
|
|
139
|
+
--btn-border: transparent;
|
|
140
|
+
--btn-state-layer: var(--btn-on-danger-container);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* --- OUTLINED : fill transparent, label + bordure colorés (fg) --- */
|
|
144
|
+
[ktButton][data-mode='outlined'][data-color='primary'] {
|
|
145
|
+
--btn-fill: transparent;
|
|
146
|
+
--btn-fg: var(--btn-primary-fg);
|
|
147
|
+
--btn-border: var(--btn-primary-fg);
|
|
148
|
+
--btn-state-layer: var(--btn-primary-fg);
|
|
149
|
+
}
|
|
150
|
+
[ktButton][data-mode='outlined'][data-color='neutral'] {
|
|
151
|
+
--btn-fill: transparent;
|
|
152
|
+
--btn-fg: var(--btn-neutral-fg);
|
|
153
|
+
--btn-border: var(--btn-neutral-fg);
|
|
154
|
+
--btn-state-layer: var(--btn-neutral-fg);
|
|
155
|
+
}
|
|
156
|
+
[ktButton][data-mode='outlined'][data-color='danger'] {
|
|
157
|
+
--btn-fill: transparent;
|
|
158
|
+
--btn-fg: var(--btn-danger-fg);
|
|
159
|
+
--btn-border: var(--btn-danger-fg);
|
|
160
|
+
--btn-state-layer: var(--btn-danger-fg);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* --- TEXT : fill + bordure transparents, label coloré (fg) --- */
|
|
164
|
+
[ktButton][data-mode='text'][data-color='primary'] {
|
|
165
|
+
--btn-fill: transparent;
|
|
166
|
+
--btn-fg: var(--btn-primary-fg);
|
|
167
|
+
--btn-border: transparent;
|
|
168
|
+
--btn-state-layer: var(--btn-primary-fg);
|
|
169
|
+
}
|
|
170
|
+
[ktButton][data-mode='text'][data-color='neutral'] {
|
|
171
|
+
--btn-fill: transparent;
|
|
172
|
+
--btn-fg: var(--btn-neutral-fg);
|
|
173
|
+
--btn-border: transparent;
|
|
174
|
+
--btn-state-layer: var(--btn-neutral-fg);
|
|
175
|
+
}
|
|
176
|
+
[ktButton][data-mode='text'][data-color='danger'] {
|
|
177
|
+
--btn-fill: transparent;
|
|
178
|
+
--btn-fg: var(--btn-danger-fg);
|
|
179
|
+
--btn-border: transparent;
|
|
180
|
+
--btn-state-layer: var(--btn-danger-fg);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* ============ ICÔNE (pur CSS via une ligature de police d'icône) ============ */
|
|
184
|
+
/* Police configurable : --btn-icon-font (par bouton) > --kt-icon-font (global) > Material Symbols.
|
|
185
|
+
Permet d'utiliser une AUTRE police d'icônes à ligatures sans changer l'API `icon`. Pour un set
|
|
186
|
+
NON-ligature (ex. Font Awesome), projeter le markup de l'icône dans le bouton. */
|
|
187
|
+
[ktButton][data-icon]::before {
|
|
188
|
+
content: attr(data-icon);
|
|
189
|
+
font-family: var(--btn-icon-font, var(--kt-icon-font, 'Material Symbols Outlined'));
|
|
190
|
+
font-weight: var(--btn-icon-font-weight, var(--kt-icon-font-weight, normal));
|
|
191
|
+
font-style: normal;
|
|
192
|
+
font-size: var(--btn-icon-size, 20px); /* px par taille */
|
|
193
|
+
line-height: 1;
|
|
194
|
+
font-feature-settings: 'liga';
|
|
195
|
+
-webkit-font-smoothing: antialiased;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Icône à droite : le pseudo ::before est un flex item, on le déplace via order */
|
|
199
|
+
[ktButton][data-icon-position='end']::before {
|
|
200
|
+
order: 1;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* Asymétrie : on resserre le padding DU CÔTÉ de l'icône (silhouette recentrée optiquement).
|
|
204
|
+
Le longhand bat le shorthand padding-inline ; var(--btn-icon-pad-x) s'adapte à la taille.
|
|
205
|
+
:not([data-icon-only]) protège le mode icône seule (padding symétrique). */
|
|
206
|
+
[ktButton][data-icon][data-icon-position='start']:not([data-icon-only]) {
|
|
207
|
+
padding-inline-start: var(--btn-icon-pad-x);
|
|
208
|
+
}
|
|
209
|
+
[ktButton][data-icon][data-icon-position='end']:not([data-icon-only]) {
|
|
210
|
+
padding-inline-end: var(--btn-icon-pad-x);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* ============ ICON-ONLY (carré = hauteur, icône centrée ; forme suit --btn-radius) ============ */
|
|
214
|
+
[ktButton][data-icon-only] {
|
|
215
|
+
--btn-pad-x: 0;
|
|
216
|
+
gap: 0;
|
|
217
|
+
inline-size: var(--btn-height-md, 44px);
|
|
218
|
+
min-inline-size: var(--btn-height-md, 44px);
|
|
219
|
+
position: relative;
|
|
220
|
+
}
|
|
221
|
+
[ktButton][data-size='sm'][data-icon-only] {
|
|
222
|
+
inline-size: var(--btn-height-sm, 32px);
|
|
223
|
+
min-inline-size: var(--btn-height-sm, 32px);
|
|
224
|
+
}
|
|
225
|
+
[ktButton][data-size='lg'][data-icon-only] {
|
|
226
|
+
inline-size: var(--btn-height-lg, 52px);
|
|
227
|
+
min-inline-size: var(--btn-height-lg, 52px);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* sm boutons : zone de clic étendue à au moins 44x44px (AAA 2.5.5) via ::after transparent.
|
|
231
|
+
Hors loading uniquement : le spinner occupe déjà ::after (états mutuellement exclusifs). */
|
|
232
|
+
[ktButton][data-size='sm']:not(.loading)::after {
|
|
233
|
+
content: '';
|
|
234
|
+
position: absolute;
|
|
235
|
+
inset: -6px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/* ============ ÉTATS (voile teinté avec la couleur de contenu --btn-state-layer) ============ */
|
|
239
|
+
[ktButton]:hover:not(:disabled):not([aria-disabled='true']) {
|
|
240
|
+
--btn-overlay: color-mix(
|
|
241
|
+
in srgb,
|
|
242
|
+
var(--btn-state-layer) calc(var(--btn-state-hover-opacity, 0.08) * 100%),
|
|
243
|
+
transparent
|
|
244
|
+
);
|
|
245
|
+
box-shadow: var(--btn-shadow-hover, var(--btn-shadow, none));
|
|
246
|
+
transform: var(--btn-transform-hover, var(--btn-transform, none));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
[ktButton]:focus-visible:not(:disabled):not([aria-disabled='true']) {
|
|
250
|
+
--btn-overlay: color-mix(
|
|
251
|
+
in srgb,
|
|
252
|
+
var(--btn-state-layer) calc(var(--btn-state-focus-opacity, 0.12) * 100%),
|
|
253
|
+
transparent
|
|
254
|
+
);
|
|
255
|
+
box-shadow: var(--btn-shadow-focus, var(--btn-shadow, none));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* pressed après hover/focus dans la cascade => l'emporte au clic maintenu */
|
|
259
|
+
[ktButton]:active:not(:disabled):not([aria-disabled='true']) {
|
|
260
|
+
--btn-overlay: color-mix(
|
|
261
|
+
in srgb,
|
|
262
|
+
var(--btn-state-layer) calc(var(--btn-state-pressed-opacity, 0.16) * 100%),
|
|
263
|
+
transparent
|
|
264
|
+
);
|
|
265
|
+
transform: var(--btn-transform-active, var(--btn-transform, none));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* Anneau de focus : outline séparé du voile, posé sur le fond de page via l'offset.
|
|
269
|
+
Reste visible aussi sur un <a>/bouton en aria-disabled (toujours focusable). */
|
|
270
|
+
[ktButton]:focus-visible {
|
|
271
|
+
outline: var(--btn-focus-ring-width, 2px) var(--btn-focus-ring-style, solid) var(--btn-focus-ring-color, #0b57d0);
|
|
272
|
+
outline-offset: var(--btn-focus-ring-offset, 2px);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* ============ ÉTAT INERTE (disabled natif ou aria-disabled) ============ */
|
|
276
|
+
/* Après les états => l'emporte sur hover/active. */
|
|
277
|
+
[ktButton]:disabled,
|
|
278
|
+
[ktButton][aria-disabled='true'] {
|
|
279
|
+
--btn-overlay: transparent;
|
|
280
|
+
cursor: not-allowed;
|
|
281
|
+
color: color-mix(in srgb, var(--btn-fg) 40%, transparent);
|
|
282
|
+
background-color: color-mix(in srgb, var(--btn-fill) 40%, var(--btn-surface, #ffffff));
|
|
283
|
+
border-color: color-mix(in srgb, var(--btn-border) 40%, transparent);
|
|
284
|
+
box-shadow: none; /* un bouton inerte ne « rayonne » pas, quel que soit le thème */
|
|
285
|
+
transform: none; /* ni ne bouge */
|
|
286
|
+
background-image: none; /* ni ne garde son dégradé (--btn-fill-image) */
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* ============ LOADING ============ */
|
|
290
|
+
[ktButton].loading {
|
|
291
|
+
position: relative;
|
|
292
|
+
color: transparent !important;
|
|
293
|
+
|
|
294
|
+
&::before {
|
|
295
|
+
opacity: 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* Spinner centré en currentColor (suit le label, inversé en dark mode) */
|
|
299
|
+
&::after {
|
|
300
|
+
content: '';
|
|
301
|
+
position: absolute;
|
|
302
|
+
top: 50%;
|
|
303
|
+
left: 50%;
|
|
304
|
+
width: 1em;
|
|
305
|
+
height: 1em;
|
|
306
|
+
translate: -50% -50%;
|
|
307
|
+
border: 2px solid var(--btn-fg);
|
|
308
|
+
border-right-color: transparent;
|
|
309
|
+
border-radius: 50%;
|
|
310
|
+
animation: button-spinner 0.6s linear infinite;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
@keyframes button-spinner {
|
|
315
|
+
to {
|
|
316
|
+
transform: rotate(360deg);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|