Migrate site controls to Nebula design system
All checks were successful
Deploy Sokko G / deploy (push) Successful in 17s

This commit is contained in:
Shinuwa 2026-08-29 09:16:58 +02:00
parent e3d0925eb1
commit 817e21567b
127 changed files with 5380 additions and 2363 deletions

View file

@ -0,0 +1,22 @@
{"id":"int-8d5f8f5714677d91360a20177b1f5a9e","kind":"field_change","created_at":"2026-08-27T12:58:35.614943857Z","actor":"Shinuwa","issue_id":"sokko-g-lki","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed"}}
{"id":"int-206ccb97f9d09712e2b4136d115a0e43","kind":"field_change","created_at":"2026-08-27T13:09:45.211867088Z","actor":"Shinuwa","issue_id":"sokko-g-e7s","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed remaining toolbox control migration to NDS mixins and updated design-system catalogue examples."}}
{"id":"int-5131e4130f57a4e8a55f176a46240fad","kind":"field_change","created_at":"2026-08-27T16:19:44.570963534Z","actor":"Shinuwa","issue_id":"sokko-g-npt","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed NDS button variant cleanup and catalogue simplification."}}
{"id":"int-3db2dfcbabd4cbb63eaeedb1bea8ab47","kind":"field_change","created_at":"2026-08-27T16:43:20.612129607Z","actor":"Shinuwa","issue_id":"sokko-g-0o4","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed NDS data attribute API migration and validation."}}
{"id":"int-578a8a6aff782082d539f0e14bb0aa79","kind":"field_change","created_at":"2026-08-27T16:58:07.410812508Z","actor":"Shinuwa","issue_id":"sokko-g-alp","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Expanded /design-system into exhaustive NDS family attribute matrices."}}
{"id":"int-cc32f7330c12cf2d37b0d22650ccfb5e","kind":"field_change","created_at":"2026-08-27T17:13:59.871782552Z","actor":"Shinuwa","issue_id":"sokko-g-pp5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Reworked /design-system into component cards with configurable attribute matrices."}}
{"id":"int-17da0005e87891c66734a766d8456459","kind":"field_change","created_at":"2026-08-27T17:23:07.045839669Z","actor":"Shinuwa","issue_id":"sokko-g-73i","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed"}}
{"id":"int-d0531629593c076b63281346c729ac3e","kind":"field_change","created_at":"2026-08-27T17:28:11.740302589Z","actor":"Shinuwa","issue_id":"sokko-g-wnh","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed"}}
{"id":"int-fb41489d0517f51fcda643c4230461fe","kind":"field_change","created_at":"2026-08-27T17:35:40.407737936Z","actor":"Shinuwa","issue_id":"sokko-g-s0a","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed"}}
{"id":"int-b7bafbc214af080356e07d2d97f1ffba","kind":"field_change","created_at":"2026-08-27T21:32:26.611249333Z","actor":"Shinuwa","issue_id":"sokko-g-2u9","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed"}}
{"id":"int-ea50d03c7c84b32ab2fce86f15bbe05f","kind":"field_change","created_at":"2026-08-27T21:43:00.775108889Z","actor":"Shinuwa","issue_id":"sokko-g-bh4","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Completed"}}
{"id":"int-4f7a574335bb2d616e9caae09e22e55b","kind":"field_change","created_at":"2026-08-27T21:45:18.547229481Z","actor":"Shinuwa","issue_id":"sokko-g-7jm","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed"}}
{"id":"int-ae5fb0bbe41a7958929560a1bbf65052","kind":"field_change","created_at":"2026-08-27T21:48:45.268794089Z","actor":"Shinuwa","issue_id":"sokko-g-z2t","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed"}}
{"id":"int-8e3b38de72d948a6c241b0d8b2f8077b","kind":"field_change","created_at":"2026-08-27T21:54:30.02159376Z","actor":"Shinuwa","issue_id":"sokko-g-coh","extra":{"field":"status","new_value":"closed","old_value":"open","reason":"Completed"}}
{"id":"int-0d86b94b9a04df5b68e2362e0824a37b","kind":"field_change","created_at":"2026-08-27T21:57:24.401323676Z","actor":"Shinuwa","issue_id":"sokko-g-hif","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed"}}
{"id":"int-3fcb6bc2b605db6210c0da18da07e902","kind":"field_change","created_at":"2026-08-27T22:01:35.180268661Z","actor":"Shinuwa","issue_id":"sokko-g-6fs","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed NDS textarea min heights by size"}}
{"id":"int-611cb19bdd31f0e79cc8c2f2bb7a3dd7","kind":"field_change","created_at":"2026-08-28T07:13:54.812999765Z","actor":"Shinuwa","issue_id":"sokko-g-tvo","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed NDS field hover cascade and replaced public date trigger with select trigger"}}
{"id":"int-0e1196655dabd3096c10beef1ba8aff2","kind":"field_change","created_at":"2026-08-28T07:19:28.131918475Z","actor":"Shinuwa","issue_id":"sokko-g-z4x","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed select trigger hover chevron and focus-visible outline"}}
{"id":"int-9c27343c043121862608b8ed9013c306","kind":"field_change","created_at":"2026-08-28T07:25:53.259127006Z","actor":"Shinuwa","issue_id":"sokko-g-eln","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed select trigger md hover/focus and disabled checkbox styling"}}
{"id":"int-da5413e82c18d19a8432288bec703dd7","kind":"field_change","created_at":"2026-08-28T07:50:07.033524644Z","actor":"Shinuwa","issue_id":"sokko-g-h7z","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed global NDS control migration and validation"}}
{"id":"int-42f39cb889dfab3bc998d4ce566d2339","kind":"field_change","created_at":"2026-08-28T08:09:50.738078383Z","actor":"Shinuwa","issue_id":"sokko-g-9p1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed filter radio group, sidebar storage buttons, memo controls, and textarea ghost variant"}}
{"id":"int-03ed1c823da90551f51a5a0d5691e440","kind":"field_change","created_at":"2026-08-28T12:35:29.257332022Z","actor":"Shinuwa","issue_id":"sokko-g-39h","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Applied NDS event-surface to calendar week and month events, documented data-color/data-size, and validated checks."}}

1
.gitignore vendored
View file

@ -4,6 +4,7 @@ playwright-report/
test-results/
.env
.DS_Store
.local/
# Beads / Dolt files (added by bd init)
.dolt/

View file

@ -1,6 +1,6 @@
# Design System - Sokko G
# Nebula Design System - Sokko G
Ce document est la reference visuelle du projet Sokko G. Toute nouvelle interface doit rester coherente avec le theme **Nebula Command** actuel.
Ce document est la reference visuelle du projet Sokko G. Toute nouvelle interface doit rester coherente avec le theme **Nebula Command** actuel et les controles du **Nebula Design System**.
## 1. Vision
@ -63,7 +63,36 @@ Animations :
- `--duration-normal: 180ms`
- `--ease-standard: cubic-bezier(0.2, 0.8, 0.2, 1)`
## 3. Fond Nebula
## 3. API Des Contrôles NDS
Le Nebula Design System expose ses contrôles publics avec une classe de famille et des attributs `data-*`.
Règles :
- la classe décrit uniquement la famille : `nds-button`, `nds-icon-button`, `nds-input`, `nds-select`, `nds-select-trigger`, `nds-time`, `nds-textarea`, `nds-inline-input`, `nds-switch`, `nds-radio-button-group`, `nds-checkbox`, `nds-quantity-stepper`, `nds-list-row`, `nds-large-icon-picker-trigger`, `nds-event-surface`, `nds-scrollbox` ;
- les variations passent par attributs : `data-size`, `data-variant`, `data-edge`, `data-border`, `data-axis` ;
- la page `/design-system` affiche aussi `data-state` comme axe de comparaison visuelle des états ;
- `data-size` accepte seulement les tailles prévues par la famille (`xs`, `sm`, `md`, `lg`, `xxs`, `micro` selon le contrôle) ; les checkboxes utilisent `md` 20 px, `sm` 18 px et `xs` 14 px ;
- `data-variant` couvre les intentions visuelles partagées : `primary`, `secondary`, `ghost`, `dashed`, `danger` et les variantes spécialisées documentées dans `/design-system`, dont `panel-toggle` pour les boutons icones accolés à un panneau et `icon-picker` pour les triggers de sélection compacts à icône ;
- `data-edge="left|middle|right|top|center|bottom|top-left|top-right|bottom-right|bottom-left"` décrit la position d'un bouton dans un groupe ou contre un bord, et ajuste uniquement les coins ;
- `data-border="joined"` masque les bordures de jonction déduites depuis `data-edge` ;
- `data-border="none"` masque toutes les bordures sans changer la taille, le fond, le hover ou l'état actif ;
- `data-axis="horizontal|vertical"` reste réservé aux composants dont l'axe est métier, comme `nds-scrollbox` ;
- les états fonctionnels restent portés par le HTML et l'application : `disabled`, `aria-pressed="true"`, `.active`, `.is-active`.
Exemples :
```html
<button class="nds-button" data-size="xs" data-variant="primary" data-edge="left" data-border="joined">Ajouter</button>
<button class="nds-icon-button" data-size="sm" data-variant="danger" aria-label="Supprimer"></button>
<button class="nds-icon-button" data-variant="panel-toggle" data-edge="right" data-border="joined" aria-pressed="true" aria-label="Pages"></button>
<select class="nds-select" data-size="sm"></select>
<button class="nds-select-trigger" data-size="sm" aria-pressed="false"></button>
```
Les anciennes classes de variation `nds-*--*` ne doivent plus etre ajoutees. Les anciennes classes globales `.button`, `.primary`, `.danger`, `.ghost` et `.import-button` ne sont plus des APIs visuelles publiques : utiliser `nds-button` / `nds-icon-button` avec `data-variant`. Dans les outils, les classes métier peuvent rester pour le layout, la grille, les largeurs, le positionnement et les états fonctionnels. Le style visuel d'un contrôle générique doit venir de `website/src/styles/design-system/_controls.scss` ou de l'API publique `website/src/styles/design-system/_attributes.scss`.
## 4. Fond Nebula
Le fond global est defini dans `website/src/styles/_nebula.scss`.
@ -76,7 +105,7 @@ Regles :
Ne pas ajouter d'orbes decoratifs supplementaires. Si une section a besoin de profondeur, utiliser les fonds semi-transparents et les bordures existantes.
## 4. Typographie
## 5. Typographie
Police principale :
@ -93,7 +122,7 @@ Regles :
- paragraphs limites visuellement, pas de longs blocs sans respiration ;
- les `<i>` doivent etre en italique, pas utilises comme gris decoratif.
## 5. Heroes
## 6. Heroes
Les heroes de la page d'accueil et des pages jeux utilisent le meme style via `.hero` et `.game-hero`.
@ -108,7 +137,7 @@ Caracteristiques :
Les pages jeux doivent heriter du hero commun. Si un jeu a une image, elle doit etre utilisee dans les cards ou les covers, pas comme remplacement complet du hero commun sauf decision explicite.
## 6. Cards
## 7. Cards
Les cards representent les elements repetes : toolboxes, jeux, categories, monstres, faune.
@ -124,7 +153,7 @@ La bordure haute/basse interne d'une cover doit rester doree quand elle separe u
Eviter les cards dans des cards. Les sections de page doivent rester des layouts ou panneaux, pas des empilements de cartes decoratives.
## 7. Modules Toolbox
## 8. Modules Toolbox
Les modules sont des panneaux de travail, pas des cards classiques.
@ -389,7 +418,7 @@ Regles :
### Selects Des Outils
Tous les `<select>` rendus dans le contenu d'un outil doivent utiliser le style factorise de `.module-content select`, defini dans `website/src/styles/toolboxes/_shared-controls.scss`.
Tous les `<select>` rendus dans le contenu d'un outil doivent utiliser le style factorise par `website/src/styles/design-system/_controls.scss`.
Regles :
@ -571,37 +600,180 @@ Notifications internes :
- duree d'affichage cible : 2 minutes, fermeture manuelle possible ;
- style plus visible que les confirmations simples : bordure conique animee, pulse leger, arrivee depuis le cote configure.
## 17. Scrollbars
## 17. Controles Interactifs
Les scrollbars reprennent l'ancien rendu :
Les champs, selects, boutons icones, switchs et choix compacts utilisent une base commune issue de `website/src/styles/design-system/_controls.scss`. Les fichiers d'outils peuvent choisir une taille ou poser une contrainte de layout, mais ne doivent pas redefinir localement les fonds, bordures, hovers ou etats `primary`/`danger`.
```css
::-webkit-scrollbar {
width: 6px;
height: 6px;
background-color: transparent;
}
Inputs et textareas :
::-webkit-scrollbar-track {
background-color: transparent;
box-shadow: none;
}
- utiliser `field-control` pour les champs standards ;
- la base globale cible uniquement les types texte explicites : `text`, `search`, `email`, `url`, `password` et `textarea` ;
- ne pas utiliser de selecteur global negatif comme `input:not([type="checkbox"])`, afin de ne pas capter les champs `number`, `time`, `file` ou les cellules specialisees ;
- tout nouvel `<input>` texte doit declarer son `type` explicitement ;
- utiliser `time-control` pour les champs `input[type="time"]`, avec variante `sm` ou `xs` selon le contexte ;
- taille normale : `44px` minimum ;
- taille compacte `sm` : `36px` minimum ;
- taille dense `xs` : `32px` minimum ;
- utiliser `field-textarea` pour les textareas redimensionnables avec scrollbar stable : `md` `108px`, `sm` `88px`, `xs` `72px` minimum ;
- utiliser `data-variant="ghost"` sur `nds-textarea` pour les zones d'edition intégrées à une surface métier, comme les mémos ;
- placeholders via `field-placeholder` ;
- hover via `field-hover`, focus via `field-focus` ;
- disabled via `field-disabled` ;
- les variantes complexes comme les cellules de table et le champ timer `hh:mm:ss` utilisent des mixins dedies du meme fichier.
::-webkit-scrollbar-thumb {
background-image: -webkit-gradient(
linear,
left bottom,
left top,
color-stop(0.5, rgb(79, 26, 114)),
color-stop(1, rgb(44, 1, 135))
);
border-radius: 10px;
}
```
Edition inline :
Ne pas ajouter de rail noir visible ni de double bordure pres des panneaux.
- utiliser `inline-field` pour les inputs transparents au repos ;
- hover/focus via `inline-field-hover` ;
- utiliser `inline-textarea` pour les zones texte transparentes intégrées à une carte ;
- les outils peuvent changer la couleur texte selon le contexte, par exemple titre de section ou état terminé.
## 18. Images Et Assets
Selects :
- taille normale : `44px` minimum ;
- taille compacte toolbox : `36px` minimum ;
- taille dense `xs` : `32px` minimum ;
- fleche custom unique, jamais de chevrons natifs ou empiles ;
- fond sombre Nebula force via `background-color`, hover/focus violet avec chevron dore ;
- focus visible global dore conserve.
Select triggers :
- utiliser `select-trigger` pour les boutons qui ouvrent une liste, un picker ou un calendrier avec le langage visuel d'un select ;
- tailles disponibles : `md` `44px`, `sm` `36px`, `xs` `32px` ;
- le chevron est le meme asset CSS que `select-control`, pas une icone locale ;
- hover via `select-trigger-hover`, focus via `field-focus` et focus visible dore ;
- `date-picker-trigger` reste un alias de compatibilite pour les outils deja migrés.
Menus et palettes :
- utiliser `floating-control-menu` pour les menus flottants compacts ;
- utiliser `compact-menu-button` pour les options d'icones ou d'actions compactes ;
- utiliser `color-swatch`, `color-swatch-rainbow` et `color-swatch-active` pour les palettes couleur ;
- utiliser `large-icon-picker-trigger` pour les gros triggers de choix d'icone de toolbox ;
- les outils peuvent garder uniquement la position du menu, le nombre de colonnes et la variable de couleur métier.
Champs horaires :
- les champs `input[type="time"]` restent natifs ;
- utiliser `time-control` pour aligner le champ avec le theme Nebula ;
- la popup native du navigateur/OS n'est pas consideree comme stylable de facon fiable en CSS.
Boutons texte :
- utiliser `text-button` depuis `website/src/styles/design-system/_controls.scss` ;
- taille normale : hauteur minimale `42px` ;
- taille compacte `sm` : hauteur minimale `34px` ;
- taille dense `xs` : hauteur minimale `30px` ;
- le bouton standard sert de variant secondaire par defaut ;
- variants disponibles : `primary`, `secondary`, `ghost`, `dashed`, `danger` ;
- les boutons texte peuvent utiliser `data-edge` pour ajuster leurs coins et `data-border` pour gérer les bordures jointes ou masquées ;
- etat toggler via `.active` ou `.is-active`, avec le mixin `temporary-toggle-active` ;
- etat desactive via `control-disabled` ;
- les outils peuvent uniquement ajuster la largeur, l'alignement ou l'espacement environnant.
Boutons icones :
- taille normale : `40px`, icone `21px` ;
- taille compacte : `34px`, icone `19px` ;
- taille dense `xs` : `30px`, icone `17px` ;
- taille inline `xxs` : `26px`, icone `15px` ;
- tailles micro reservees aux noeuds denses : `24px`/`13px` et `22px`/`12px` ;
- fond compact sombre, rayon `--radius-md`, hover violet contenu ;
- variant `primary` dore/violet pour les actions principales ;
- variant `ghost` transparent pour les handles discrets ;
- variant `danger` rouge contenu pour les suppressions ;
- variant de forme `button-edge(left|middle|right|top|center|bottom|top-left|top-right|bottom-right|bottom-left)` pour ajuster uniquement les coins contre une bordure ou dans un groupe ;
- variant de bordure `button-joined-border(...)` pour masquer les bordures de jonction déduites depuis `data-edge` ;
- variant de bordure `button-borderless` pour masquer toutes les bordures ;
- variant visuel `panel-toggle` pour les boutons icones accolés à un panneau, comme le bouton pages ;
- etat toggler via `.active` ou `.is-active`, avec le mixin `temporary-toggle-active`.
- les barres d'outils compactes reutilisent la famille bouton icone ; `toolbar-button` reste un alias pratique pour les boutons courts du bloc-notes (`B`, `I`, `U`) et les boutons de dessin.
Boutons composes et toggles de bord :
- utiliser `button-edge` + `button-joined-border` pour les boutons groupés type ajout + chevron ;
- utiliser `button-edge` pour la forme des toggles accoles au bord d'un module, et `icon-button-panel-toggle` pour leur rendu visuel quand ils reprennent le pattern du bouton `pages` ;
- utiliser `list-row-button` pour les lignes selectionnables type pages du bloc-notes ;
- utiliser `text-button-dashed` pour les actions d'ajout contextuelles encadrees en pointilles ;
- `split-button-start`, `split-button-end`, `edge-toggle` et `dashed-action-button` restent des alias de compatibilite internes, pas des familles visuelles separees ;
- les outils peuvent garder uniquement la position absolue, la largeur contextuelle et les icones ;
- l'etat actif/hover doit rester dans le DS, sans fond/bordure locale.
Steppers de quantite :
- utiliser `quantity-stepper` depuis `website/src/styles/design-system/_controls.scss` ;
- variante normale : checklist, boutons `28px` autour de la valeur editable ;
- variante compacte : arbre d'objectifs, boutons `18px` ou `16px` selon la densite du noeud ;
- hover des boutons via `quantity-stepper-button-hover`, pas via `icon-button-hover` ;
- les outils peuvent adapter la grille environnante, mais pas redefinir les boutons `- / +`, le hover ou le champ de valeur.
Switchs binaires :
- utiliser `switch-control` depuis `website/src/styles/design-system/_controls.scss` ;
- taille `lg` : switchs de module, hauteur `40px`, rail `30 x 16px` ;
- taille normale : tool-split, hauteur `34px`, rail `34 x 18px` ;
- taille compacte `sm` : contrôles denses, hauteur `28px`, rail `24 x 14px` ;
- taille inline `xs` : timer inline, hauteur `26px`, rail `18 x 12px` ;
- les variantes natives avec `<label><input type="checkbox" />...` doivent réutiliser `native-switch-label`, `native-switch-input`, `switch-track` et `switch-thumb` ;
- les outils peuvent uniquement ajuster l'alignement ou la largeur environnante.
Radio button groups :
- la primitive commune est `radio-button-group` / `radio-button-group-option` depuis `website/src/styles/design-system/_controls.scss` ;
- tailles disponibles : `md`, `sm`, `xs` ;
- cette base couvre les tabs d'outils, switches de layout, choix d'alertes et répétitions calendrier ;
- le nombre d'options, le fait d'utiliser une icone, un texte ou les deux ne constitue pas un variant visuel ;
- les toggles compacts non natifs peuvent reutiliser ces mixins meme avec des `span`, tant que le comportement reste celui d'un choix exclusif ou d'un choix compact ;
- l'etat actif passe par `.active` ou `.is-active` avec le style toggler temporaire ;
- les outils peuvent definir la grille ou la largeur, mais pas redefinir localement les fonds, bordures, hovers ou actifs.
Controles composes :
- utiliser `compound-toggle-field` pour les cas qui combinent un choix compact et un champ, comme `Hold + number` ;
- le bouton interne utilise `compound-toggle-field-option`, base sur `radio-button-group-option(xs)` ;
- le champ interne utilise `compound-toggle-field-input`, base sur `field-control(xs)` et `number-spinner-reset` ;
- ces controles ne sont pas des variants du groupe de choix, car leur structure mélange action et saisie.
Checkboxes :
- utiliser `checkbox-control` depuis `website/src/styles/design-system/_controls.scss` ;
- taille normale globale : `20px`, format carre strict `1:1` ;
- taille compacte `xs` : `14px` pour les listes denses comme les prérequis de l'arbre d'objectifs ;
- etat coche avec gradient dore/violet et coche blanche ;
- les outils peuvent ajuster uniquement l'alignement ou l'espacement environnant.
Date picker et menus compacts :
- les tabs internes d'outils utilisent les primitives `radio-button-group-surface`, `radio-button-group-option`, `radio-button-group-option-hover` et `radio-button-group-option-active` ;
- les boutons du mini calendrier mensuel utilisent `date-picker-button` et `date-picker-button-hover` ;
- les boutons de menu/picker compacts utilisent `compact-menu-button` et `compact-menu-button-hover` ;
- les boutons transparents integres aux evenements calendrier utilisent `event-surface-button`, `event-surface-button-hover` et les contrastes `event-surface-contrast-default` / `event-surface-contrast-dark` ;
- `nds-event-surface[data-variant="default"]` cible un texte blanc/gris blanc sur fond sombre ;
- `nds-event-surface[data-variant="dark"]` cible un texte noir/gris noir sur fond clair ;
- `nds-event-surface[data-size="md"]` vise les surfaces à partir de `44px` et affiche le titre et la metadata secondaire, par exemple l'heure ;
- `nds-event-surface[data-size="sm"]` vise les surfaces autour de `22px` et masque la metadata secondaire ;
- `nds-event-surface[data-size="xs"]` vise les surfaces ultra-fines autour de `10px` et affiche uniquement le titre en taille réduite ;
- `nds-event-surface[data-color="#8b5cf6"]` expose la couleur métier de l'event ; la variable inline `--nds-event-color` applique cette couleur au fond et a la bordure ;
- le hover utilise une superposition sombre legere pour rester lisible sur une couleur dynamique inconnue du Sass ;
- les couleurs de fond des events restent métier et ne sont pas des variants NDS ;
- les fichiers d'outils peuvent conserver les dimensions de grille, le placement du menu et les états métier `disabled`.
## 18. Scrollbars
Les scrollbars utilisent `scrollbar-control` depuis `website/src/styles/design-system/_controls.scss`.
Regles :
- largeur/hauteur WebKit : `6px` ;
- `scrollbar-width: thin` pour Firefox ;
- track transparent ;
- thumb violet/indigo avec hover dore contenu ;
- coin transparent pour les zones scrollables dans les deux axes ;
- les textareas redimensionnables ajoutent `scrollbar-resizer`, sans fond noir ;
- ne pas ajouter de rail noir visible ni de double bordure pres des panneaux.
## 19. Images Et Assets
Regles :
@ -617,7 +789,7 @@ Regles :
- les categories de jeu utilisent des images representatives, entieres et bien cadrees ;
- eviter les images trop grandes dans les cards.
## 19. Accessibilite
## 20. Accessibilite
Minimum attendu :
@ -629,7 +801,7 @@ Minimum attendu :
- modales avec `role="dialog"` et `aria-modal="true"` ;
- ne pas transmettre une information uniquement par couleur.
## 20. A Eviter
## 21. A Eviter
- accents dores partout ;
- hover trop lumineux sur tous les elements ;

View file

@ -138,9 +138,9 @@ test("style entrypoint and theme layers are wired", async () => {
assert.match(styles, /@use "diablo4"/);
assert.match(styleTokens, /--gradient-brand/);
assert.match(styleBase, /:is\(/);
assert.match(styleBase, /\.card-icon-button/);
assert.match(styleBase, /\.drawer-action-button/);
assert.match(styleBase, /\.import-icon-button/);
assert.doesNotMatch(styleBase, /\.card-icon-button/);
assert.doesNotMatch(styleBase, /\.drawer-action-button/);
assert.doesNotMatch(styleBase, /\.import-icon-button/);
assert.match(styleBase, /themed-scrollbar/);
assert.match(styleBase, /body\.is-modal-open/);
assert.match(styleNebula, /@function star-field/);
@ -172,7 +172,7 @@ test("style entrypoint and theme layers are wired", async () => {
assert.match(styleToolboxCalendar, /\.calendar-event-form/);
assert.match(styleToolboxCalendar, /\.calendar-month-date-picker/);
assert.match(styleToolboxCalendar, /\.calendar-alert-mode-switch/);
assert.match(styleToolboxSharedControls, /\.notepad-toolbar-button\.danger/);
assert.match(styles, /@use "design-system\/attributes"/);
assert.match(styleToolboxLayout, /\.toolbox-icon-picker/);
assert.match(styleToolboxLayout, /\.toolbox-game-icon/);
assert.match(styleToolboxLayout, /\.toolbox-hero-link/);

View file

@ -437,7 +437,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(calendarBundle, /calendar-repeat-switch/);
assert.match(calendarBundle, /layout-switch calendar-month-repeat-switch/);
assert.match(calendarBundle, /WEEK_TIME_INPUT_STEP_MINUTES \* 60/);
assert.match(calendarBundle, /durationMinutes < 120/);
assert.match(calendarBundle, /naturalHeight/);
assert.match(calendarBundle, /data-size=\{eventSurfaceSize\}/);
assert.match(calendarBundle, /is-compact/);
assert.match(calendarController, /handleKeyDown/);
assert.match(calendarContent, /TextExchangeActions/);
@ -534,7 +535,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
assert.match(checklistParts, /chevron-down/);
assert.match(checklistParts, /chevron-up/);
assert.match(checklistParts, /checklist-qty-current/);
assert.match(checklistParts, /checklist-delete-button danger/);
assert.match(checklistParts, /checklist-delete-button nds-icon-button/);
assert.match(checklistModule, /useGroupedReorder/);
assert.match(checklistBundle, /getItemProps/);
assert.match(checklistParts, /getGroupProps/);

View file

@ -89,7 +89,7 @@ export function GuidedTooltipTour({ steps, labels = {}, targetAttribute = "data-
return createPortal(
<section ref={tooltipRef} className="toolbox-tutorial-tooltip" aria-live="polite" aria-labelledby="toolbox-tutorial-title" data-placement={placement} role="dialog" style={tooltipStyle}>
<button className="toolbox-tutorial-close" type="button" onClick={onDismiss} aria-label={labels.dismiss || "Fermer le tutoriel"} title={labels.dismiss || "Fermer"}>
<button className="toolbox-tutorial-close nds-icon-button" data-size="xs" type="button" onClick={onDismiss} aria-label={labels.dismiss || "Fermer le tutoriel"} title={labels.dismiss || "Fermer"}>
<Icon name="close" />
</button>
<span className="toolbox-tutorial-count">{stepIndex + 1}/{steps.length}</span>
@ -99,7 +99,7 @@ export function GuidedTooltipTour({ steps, labels = {}, targetAttribute = "data-
</div>
<p>{currentStep.text}</p>
<footer>
<button className="primary" type="button" onClick={isLastStep ? onDismiss : () => setStepIndex((index) => Math.min(index + 1, steps.length - 1))}>
<button className="nds-button" data-size="sm" data-variant="primary" type="button" onClick={isLastStep ? onDismiss : () => setStepIndex((index) => Math.min(index + 1, steps.length - 1))}>
{isLastStep ? labels.done || "Terminer" : labels.next || "Suivant"}
</button>
</footer>

View file

@ -142,9 +142,9 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis.
{image.label && <strong className="image-viewer-title">{image.label}</strong>}
<div className="image-viewer-actions">
{!canAnnotate && !hasMarkers && (
<button className="image-viewer-button" onClick={() => openImageInNewTab(image.dataUrl)} aria-label="Ouvrir l'image en taille réelle" title="Taille réelle"><Icon name="zoom" /></button>
<button className="image-viewer-button nds-icon-button" data-size="sm" onClick={() => openImageInNewTab(image.dataUrl)} aria-label="Ouvrir l'image en taille réelle" title="Taille réelle"><Icon name="zoom" /></button>
)}
<button className="image-viewer-button" onClick={onClose} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
<button className="image-viewer-button nds-icon-button" data-size="sm" onClick={onClose} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
</div>
</header>
<div className={`image-viewer-body ${canAnnotate ? "has-annotation-side" : ""}`}>
@ -211,12 +211,15 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis.
<li key={marker.id}>
<span>{index + 1}</span>
<input
className="nds-input"
data-size="sm"
type="text"
value={marker.label}
placeholder={image.markerPlaceholder || "Libellé du marqueur"}
onChange={(event) => updateViewerMarker(marker.id, (item) => ({ ...item, label: event.target.value }))}
aria-label={`Libellé du marqueur ${index + 1}`}
/>
<button className="tool-split-action-button danger" type="button" onClick={() => removeViewerMarker(marker.id)} aria-label={`${image.deleteMarkerTitle || "Supprimer"} ${marker.label || `${image.markerPrefix || "Marqueur"} ${index + 1}`}`} title={image.deleteMarkerTitle || "Supprimer"}>
<button className="tool-split-action-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={() => removeViewerMarker(marker.id)} aria-label={`${image.deleteMarkerTitle || "Supprimer"} ${marker.label || `${image.markerPrefix || "Marqueur"} ${index + 1}`}`} title={image.deleteMarkerTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</li>

View file

@ -1,9 +1,9 @@
// Rôle : encapsule l'input fichier JSON derrière un bouton d'import réutilisable.
import { Icon } from "./Icon.jsx";
export function ImportButton({ className, label, title, ariaLabel, icon, onFile }) {
export function ImportButton({ className, label, title, ariaLabel, icon, onFile, ...props }) {
return (
<label className={className} aria-label={ariaLabel} title={title}>
<label className={className} aria-label={ariaLabel} title={title} {...props}>
{icon && <Icon name={icon} />}
{label}
<input type="file" accept="application/json" hidden onChange={async (event) => {

View file

@ -6,7 +6,7 @@ export function Shell({ route, content, games, toolboxes, links, actions, childr
const gameId = route.split("/")[1] === "games" ? route.split("/")[2] || "" : "";
const game = games.find((item) => item.id === gameId);
const linkedToolbox = game ? toolboxes.find((item) => item.id === links[game.id]) : null;
const topbarLabel = route.startsWith("/toolbox") ? content.topbar.toolbox : route.startsWith("/games") ? content.topbar.games : route === "/library" ? content.topbar.library : content.topbar.dashboard;
const topbarLabel = route === "/design-system" && import.meta.env.DEV ? "Design system" : route.startsWith("/toolbox") ? content.topbar.toolbox : route.startsWith("/games") ? content.topbar.games : route === "/library" ? content.topbar.library : content.topbar.dashboard;
const toolboxLabel = linkedToolbox ? `Ouvrir la toolbox ${linkedToolbox.name}` : "Associer une toolbox";
return (
@ -27,13 +27,16 @@ export function Shell({ route, content, games, toolboxes, links, actions, childr
<div className="sidebar-secondary-links">
<a className={`sidebar-about-link ${route === "/about" ? "active" : ""}`} href="#/about"><span className="sidebar-about-icon" aria-hidden="true">?</span><strong>{content.navigation.about}</strong></a>
<a className={`sidebar-about-link ${route === "/library" ? "active" : ""}`} href="#/library"><span className="sidebar-about-icon" aria-hidden="true"><Icon name="book" /></span><strong>{content.library.eyebrow}</strong></a>
{import.meta.env.DEV && (
<a className={`sidebar-about-link ${route === "/design-system" ? "active" : ""}`} href="#/design-system"><span className="sidebar-about-icon" aria-hidden="true"><Icon name="settings" /></span><strong>Design system</strong></a>
)}
</div>
<div className="sidebar-note">
<span className="badge">{content.sidebar.badge}</span>
<p>{content.sidebar.note}</p>
<div className="sidebar-storage-actions" aria-label="Actions globales de stockage">
<button className="sidebar-action-button primary" onClick={actions.exportAllToolboxes} aria-label="Exporter toutes les toolboxes" data-toolbox-tutorial-target="exportAll" title="Exporter tout"><Icon name="export" /></button>
<ImportButton className="sidebar-action-button import-icon-button" title="Importer tout" ariaLabel="Importer toutes les toolboxes" onFile={actions.importAllToolboxes} icon="import" />
<button className="sidebar-action-button nds-icon-button" data-size="sm" data-variant="primary" onClick={actions.exportAllToolboxes} aria-label="Exporter toutes les toolboxes" data-toolbox-tutorial-target="exportAll" title="Exporter tout"><Icon name="export" /></button>
<ImportButton className="sidebar-action-button nds-icon-button import-icon-button" data-size="sm" title="Importer tout" ariaLabel="Importer toutes les toolboxes" onFile={actions.importAllToolboxes} icon="import" />
</div>
</div>
</aside>
@ -42,7 +45,7 @@ export function Shell({ route, content, games, toolboxes, links, actions, childr
<div><strong>{topbarLabel}</strong></div>
{game && (
<div className="topbar-actions">
<button className="drawer-button toolbox-icon-button primary" onClick={() => actions.setDrawerGameId(game.id)} aria-label={toolboxLabel} title={toolboxLabel}>
<button className="drawer-button toolbox-icon-button nds-icon-button" data-size="sm" data-variant="primary" onClick={() => actions.setDrawerGameId(game.id)} aria-label={toolboxLabel} title={toolboxLabel}>
<img src="/static/icons/toolbox.svg" alt="" aria-hidden="true" />
</button>
</div>
@ -53,7 +56,7 @@ export function Shell({ route, content, games, toolboxes, links, actions, childr
<nav className="mobile-nav" aria-label="Navigation mobile">
<a className={route === "/" ? "active" : ""} href="#/">{content.navigation.home}</a>
<a className={route.startsWith("/toolboxes") || route.startsWith("/toolbox") ? "active" : ""} href="#/toolboxes">{content.navigation.toolboxes}</a>
<button className="primary" onClick={() => actions.setCreateModal({})}>+</button>
<button className="nds-button" data-size="sm" data-variant="primary" onClick={() => actions.setCreateModal({})}>+</button>
<a className={route === "/library" ? "active" : ""} href="#/library">{content.navigation.mobileLibrary}</a>
<a className={route.startsWith("/games") ? "active" : ""} href="#/games">{content.navigation.mobileGames}</a>
</nav>

View file

@ -1,7 +1,7 @@
// Rôle : fournit un contrôle d'onglets réutilisable avec rôles ARIA.
export function Tabs({ tabs, activeTab, onChange, className = "", iconOnly = false }) {
export function Tabs({ tabs, activeTab, onChange, className = "", iconOnly = false, size = "sm" }) {
return (
<div className={`tabs ${className}`.trim()} role="tablist">
<div className={`tabs nds-radio-button-group ${className}`.trim()} data-size={size} role="tablist">
{tabs.map((tab) => {
const active = tab.id === activeTab;
return (

View file

@ -4,16 +4,34 @@ import { Icon } from "./Icon.jsx";
export function ToastPositionSwitch({ position, labels, onChange }) {
const normalizedPosition = position === "left" ? "left" : "right";
return (
<button
className="toast-position-switch"
type="button"
<div
className="toast-position-switch nds-radio-button-group"
data-size="xs"
role="radiogroup"
data-position={normalizedPosition}
onClick={() => onChange(normalizedPosition === "left" ? "right" : "left")}
aria-label={normalizedPosition === "left" ? labels.left : labels.right}
title={labels.title}
>
<span><Icon name="toast-left" /></span>
<span><Icon name="toast-right" /></span>
</button>
<button
className={normalizedPosition === "left" ? "active" : ""}
type="button"
role="radio"
aria-checked={normalizedPosition === "left"}
aria-label={labels.left}
onClick={() => onChange("left")}
>
<Icon name="toast-left" />
</button>
<button
className={normalizedPosition === "right" ? "active" : ""}
type="button"
role="radio"
aria-checked={normalizedPosition === "right"}
aria-label={labels.right}
onClick={() => onChange("right")}
>
<Icon name="toast-right" />
</button>
</div>
);
}

View file

@ -16,8 +16,8 @@ export function ConfirmModal({ title, message, confirmLabel = "Confirmer", cance
<header><h2 id="confirm-title">{title}</h2></header>
<p id="confirm-message">{message}</p>
<footer>
<button onClick={() => onClose(false)}>{cancelLabel}</button>
<button className={danger ? "danger confirm-danger" : "primary"} onClick={() => onClose(true)}>{confirmLabel}</button>
<button className="nds-button" data-size="sm" onClick={() => onClose(false)}>{cancelLabel}</button>
<button className="confirm-danger nds-button" data-size="sm" data-variant={danger ? "danger" : "primary"} onClick={() => onClose(true)}>{confirmLabel}</button>
</footer>
</section>
</div>
@ -33,7 +33,7 @@ export function NotificationToast({ id, message, durationMs = 120000, onClose })
return (
<div className="notification-toast" role="status">
<span>{message}</span>
<button type="button" onClick={() => onClose(id)} aria-label="Fermer l'alerte" title="Fermer">
<button className="nds-icon-button" data-size="xs" type="button" onClick={() => onClose(id)} aria-label="Fermer l'alerte" title="Fermer">
<Icon name="close" />
</button>
</div>
@ -52,8 +52,8 @@ export function CreateToolboxModal({ gameId, initialName = "", onClose }) {
event.preventDefault();
if (name.trim()) onClose(name.trim());
}}>
<label className="field"><span>Nom</span><input name="name" autoComplete="off" placeholder="Nom de la toolbox" required value={name} onChange={(event) => setName(event.target.value)} autoFocus /></label>
<footer><button type="button" onClick={() => onClose("")}>Annuler</button><button className="primary" type="submit">Créer</button></footer>
<label className="field"><span>Nom</span><input className="nds-input" data-size="sm" name="name" type="text" autoComplete="off" placeholder="Nom de la toolbox" required value={name} onChange={(event) => setName(event.target.value)} autoFocus /></label>
<footer><button className="nds-button" data-size="sm" type="button" onClick={() => onClose("")}>Annuler</button><button className="nds-button" data-size="sm" data-variant="primary" type="submit">Créer</button></footer>
</form>
</section>
</div>
@ -72,8 +72,8 @@ export function LinkToolboxModal({ toolboxes, selectedId, onClose }) {
event.preventDefault();
onClose(toolboxId);
}}>
<label className="field"><span>Toolbox associée</span><select value={toolboxId} onChange={(event) => setToolboxId(event.target.value)} autoFocus><option value="">Aucune</option>{toolboxes.map((toolbox) => <option key={toolbox.id} value={toolbox.id}>{toolbox.name}</option>)}</select></label>
<footer><button type="button" onClick={() => onClose(null)}>Annuler</button><button className="primary" type="submit">Valider</button></footer>
<label className="field"><span>Toolbox associée</span><select className="nds-select" data-size="sm" value={toolboxId} onChange={(event) => setToolboxId(event.target.value)} autoFocus><option value="">Aucune</option>{toolboxes.map((toolbox) => <option key={toolbox.id} value={toolbox.id}>{toolbox.name}</option>)}</select></label>
<footer><button className="nds-button" data-size="sm" type="button" onClick={() => onClose(null)}>Annuler</button><button className="nds-button" data-size="sm" data-variant="primary" type="submit">Valider</button></footer>
</form>
</section>
</div>

View file

@ -57,7 +57,8 @@ export function CopyChecklistItemsButton({ labels, sections, title = "Copier pou
return (
<button
className="filter-reset-button results-copy-button"
className="filter-reset-button results-copy-button nds-icon-button"
data-size="xs"
type="button"
onClick={copyItems}
disabled={!text}

View file

@ -17,8 +17,6 @@ export function GameFiltersPanel({
onChange
}) {
const selected = new Set(filters[selectedKey] || []);
const logicLabel = filters.logic === "or" ? logicLabels.or : logicLabels.and;
function update(updater) {
onChange(updater(filters));
}
@ -31,7 +29,8 @@ export function GameFiltersPanel({
<h2>{title}</h2>
</div>
<button
className="filter-reset-button"
className="filter-reset-button nds-icon-button"
data-size="xs"
onClick={() => onChange({ name: "", [selectedKey]: [], logic: "and" })}
aria-label={resetLabel}
title={resetLabel}
@ -41,11 +40,24 @@ export function GameFiltersPanel({
</div>
<label className="field compact">
<span>{nameLabel}</span>
<input value={filters.name} placeholder="Rechercher..." onChange={(event) => update((state) => ({ ...state, name: event.target.value }))} />
<input className="nds-input" data-size="sm" type="search" value={filters.name} placeholder="Rechercher..." onChange={(event) => update((state) => ({ ...state, name: event.target.value }))} />
</label>
<div className="filter-logic">
<span>{matchLabel}</span>
<button onClick={() => update((state) => ({ ...state, logic: state.logic === "and" ? "or" : "and" }))}>{logicLabel}</button>
<div className="filter-logic-group nds-radio-button-group" data-size="xs" role="radiogroup" aria-label={matchLabel}>
{["and", "or"].map((logic) => (
<button
key={logic}
className={filters.logic === logic ? "active" : ""}
type="button"
role="radio"
aria-checked={filters.logic === logic}
onClick={() => update((state) => ({ ...state, logic }))}
>
{logicLabels[logic]}
</button>
))}
</div>
</div>
<div className="filter-options legacy-scrollbar">
{options.map((option) => {
@ -53,6 +65,8 @@ export function GameFiltersPanel({
return (
<label className={`filter-chip ${getOptionClassName(option)} ${selected.has(optionId) ? "active" : ""}`} key={optionId}>
<input
className="nds-checkbox"
data-size="sm"
type="checkbox"
checked={selected.has(optionId)}
onChange={(event) => update((state) => {

View file

@ -30,7 +30,7 @@ export function GameListsPage({ game, lists, selectedListId, onSelectList, linke
<h1>Listes</h1>
</div>
<p>Sélectionnez une liste, puis copiez-la ou créez une checklist dans la toolbox associée.</p>
<a className="game-list-help-link button" href="#/about#contribuer">Proposer une nouvelle liste</a>
<a className="game-list-help-link nds-button" data-size="sm" href="#/about#contribuer">Proposer une nouvelle liste</a>
</div>
</section>
{lists.length ? (
@ -43,7 +43,9 @@ export function GameListsPage({ game, lists, selectedListId, onSelectList, linke
<small>{list.itemCount} éléments</small>
</button>
<button
className="game-list-create-button primary"
className="game-list-create-button nds-icon-button"
data-size="sm"
data-variant="primary"
type="button"
disabled={!linkedToolboxId}
onClick={() => onCreateChecklist?.(list)}

View file

@ -32,7 +32,7 @@ export function GamesPage({ siteContent, games, gamesError }) {
<h2>{game.title}</h2>
<p>{game.summary}</p>
<div className="card-actions">
<a className="button primary" href={`#/games/${game.id}`}>Ouvrir</a>
<a className="nds-button" data-size="sm" data-variant="primary" href={`#/games/${game.id}`}>Ouvrir</a>
</div>
</div>
</article>

View file

@ -30,8 +30,8 @@ export function MhwildsListing({ category, game, mhwilds, filters, setFilters, t
<p>{isMonsters ? "Filtrez les monstres par nom et faiblesses, puis consultez leurs dégâts détaillés." : "Filtrez la faune par nom et zones d'apparition."}</p>
</div>
<div className="actions">
<a className={`button ${isMonsters ? "primary" : ""}`} href="#/games/mhwilds/monsters">Monstres</a>
<a className={`button ${!isMonsters ? "primary" : ""}`} href="#/games/mhwilds/endemic">Faune</a>
<a className="nds-button" data-size="sm" data-variant={isMonsters ? "primary" : "secondary"} href="#/games/mhwilds/monsters">Monstres</a>
<a className="nds-button" data-size="sm" data-variant={!isMonsters ? "primary" : "secondary"} href="#/games/mhwilds/endemic">Faune</a>
</div>
</section>
<section className="game-layout" data-mhwilds-category={category}>

View file

@ -34,7 +34,9 @@ export function ToolboxCard({ toolbox, game, actions, reorder }) {
return (
<article className={className} {...reorder.getItemProps({ itemId: toolbox.id })}>
<button
className="toolbox-card-drag-handle"
className="toolbox-card-drag-handle nds-icon-button"
data-size="md"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, toolbox.id)}
aria-label={`Déplacer ${toolbox.name}`}
@ -56,9 +58,9 @@ export function ToolboxCard({ toolbox, game, actions, reorder }) {
<h2>{toolbox.name}</h2>
<small>Modifiée le {formatDate(toolbox.updatedAt)}</small>
<div className="card-actions">
<a className="button card-icon-button" href={`#/toolbox/${toolbox.id}`} aria-label={`Ouvrir ${toolbox.name}`} title="Ouvrir"><Icon name="open" /></a>
<button className="card-icon-button" onClick={() => actions.exportToolbox(toolbox.id)} aria-label={`Exporter ${toolbox.name}`} title="Exporter"><Icon name="export" /></button>
<button className="card-icon-button danger" onClick={() => actions.setConfirmModal({
<a className="card-icon-button nds-icon-button" data-size="md" href={`#/toolbox/${toolbox.id}`} aria-label={`Ouvrir ${toolbox.name}`} title="Ouvrir"><Icon name="open" /></a>
<button className="card-icon-button nds-icon-button" data-size="md" onClick={() => actions.exportToolbox(toolbox.id)} aria-label={`Exporter ${toolbox.name}`} title="Exporter"><Icon name="export" /></button>
<button className="card-icon-button nds-icon-button" data-size="md" data-variant="danger" onClick={() => actions.setConfirmModal({
title: "Supprimer la toolbox",
message: `Supprimer "${toolbox.name}" et ses données locales ?`,
confirmLabel: "Supprimer",
@ -81,11 +83,12 @@ export function ToolboxIconPicker({ toolbox, onChange, tutorialTarget = "" }) {
label="Icônes de toolbox"
renderTrigger={({ open, toggle }) => (
<button
className="toolbox-icon-picker-button"
className={`toolbox-icon-picker-button nds-large-icon-picker-trigger ${open ? "active" : ""}`}
type="button"
onClick={toggle}
aria-label="Changer l'icône de la toolbox"
aria-expanded={open}
aria-pressed={open}
data-toolbox-tutorial-target={tutorialTarget || undefined}
title="Changer l'icône"
>
@ -98,7 +101,8 @@ export function ToolboxIconPicker({ toolbox, onChange, tutorialTarget = "" }) {
{TOOLBOX_ICONS.map((path) => (
<button
key={path}
className={path === icon ? "active" : ""}
className={`nds-icon-button ${path === icon ? "active" : ""}`}
data-size="md"
type="button"
onClick={() => {
onChange(path);

View file

@ -102,17 +102,17 @@ export function ToolboxesPage({ siteContent, toolboxes, getToolboxGame, actions,
<p className="eyebrow">{content.eyebrow}</p>
<h1>{content.title}</h1>
<p>{content.summary}</p>
<a className="button hero-library-button library-link-button" href="#/library"><Icon name="book" />{content.libraryLink}</a>
<a className="hero-library-button library-link-button nds-button" data-size="sm" href="#/library"><Icon name="book" />{content.libraryLink}</a>
</div>
</section>
<section className="toolbar toolbox-actions-row">
<div>
<button className="primary" onClick={() => actions.setCreateModal({})}>{content.newButton}</button>
<ImportButton className="import-button" label={content.importOne} onFile={actions.importToolbox} />
<button className="nds-button" data-size="sm" data-variant="primary" onClick={() => actions.setCreateModal({})}>{content.newButton}</button>
<ImportButton className="nds-button" data-size="sm" label={content.importOne} onFile={actions.importToolbox} />
</div>
<div>
<button className="primary" onClick={actions.exportAllToolboxes}>{content.exportAll}</button>
<ImportButton className="import-button" label={content.importAll} onFile={actions.importAllToolboxes} />
<button className="nds-button" data-size="sm" data-variant="primary" onClick={actions.exportAllToolboxes}>{content.exportAll}</button>
<ImportButton className="nds-button" data-size="sm" label={content.importAll} onFile={actions.importAllToolboxes} />
</div>
</section>
<section className="toolbox-preferences-row" aria-label={content.preferencesLabel}>
@ -169,7 +169,7 @@ function StorageHelpCard({ help }) {
export function ToolboxPage(props) {
const { toolboxId, toolboxes, getToolboxGame } = props;
const toolbox = toolboxes.find((item) => item.id === toolboxId);
if (!toolbox) return <div className="empty"><h1>Toolbox introuvable</h1><a className="button" href="#/toolboxes">Retour</a></div>;
if (!toolbox) return <div className="empty"><h1>Toolbox introuvable</h1><a className="nds-button" data-size="sm" href="#/toolboxes">Retour</a></div>;
return <ToolboxView {...props} toolbox={toolbox} toolboxGame={getToolboxGame(toolbox)} embedded={false} />;
}
@ -265,7 +265,8 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
</div>
{toolboxGame && (
<a
className="button toolbox-hero-link"
className="toolbox-hero-link nds-button"
data-size="sm"
href={embedded ? `#/toolbox/${toolbox.id}` : `#/games/${toolboxGame.id}`}
onClick={embedded ? () => actions.setDrawerGameId("") : undefined}
>
@ -299,7 +300,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
)}
{!embedded && (
<section className="modules-toolbar" aria-label="Options d'affichage des outils">
<div className="layout-switch" role="group" aria-label="Mode d'affichage des outils">
<div className="layout-switch nds-radio-button-group" data-size="sm" role="group" aria-label="Mode d'affichage des outils">
<button className={moduleColumns === 1 ? "active" : ""} onClick={() => updateToolbox({ ...toolbox, moduleColumns: 1 })} aria-pressed={moduleColumns === 1} aria-label="Afficher en lignes" title="Afficher en lignes"><Icon name="rows" /></button>
<button className={moduleColumns === 2 ? "active" : ""} onClick={() => updateToolbox({ ...toolbox, moduleColumns: 2 })} aria-pressed={moduleColumns === 2} aria-label="Afficher en colonnes" title="Afficher en colonnes"><Icon name="columns" /></button>
</div>
@ -387,15 +388,15 @@ export function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, act
<div>
<p className="eyebrow">Toolbox liée</p>
<div className="drawer-toolbox-actions" aria-label="Actions toolbox liée">
<button className="drawer-action-button" onClick={() => actions.setLinkModalGameId(gameId)} aria-label="Lier une toolbox" title="Lier"><Icon name="link" /></button>
<ImportButton className="drawer-action-button import-icon-button" ariaLabel="Importer une toolbox" title="Importer" icon="import" onFile={(file) => actions.importToolbox(file, gameId)} />
<button className="drawer-action-button" onClick={() => actions.exportToolbox(selectedId)} disabled={!selectedId} aria-label="Exporter la toolbox liée" title="Exporter"><Icon name="export" /></button>
<button className="drawer-action-button nds-icon-button" data-size="sm" onClick={() => actions.setLinkModalGameId(gameId)} aria-label="Lier une toolbox" title="Lier"><Icon name="link" /></button>
<ImportButton className="drawer-action-button nds-icon-button import-icon-button" data-size="sm" ariaLabel="Importer une toolbox" title="Importer" icon="import" onFile={(file) => actions.importToolbox(file, gameId)} />
<button className="drawer-action-button nds-icon-button" data-size="sm" onClick={() => actions.exportToolbox(selectedId)} disabled={!selectedId} aria-label="Exporter la toolbox liée" title="Exporter"><Icon name="export" /></button>
</div>
</div>
<button className="drawer-close-button" onClick={() => actions.setDrawerGameId("")} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
<button className="drawer-close-button nds-icon-button" data-size="sm" onClick={() => actions.setDrawerGameId("")} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
</header>
{toolbox ? <ToolboxView siteContent={siteContent} toolbox={toolbox} toolboxGame={game} embedded actions={actions} storageUsage={storageUsage} storageReady={storageReady} getModuleData={getModuleData} updateToolbox={updateToolbox} updateModuleData={updateModuleData} addImageFiles={addImageFiles} /> : (
<div className="empty"><p>Aucune toolbox associée à cette page jeu.</p><button className="primary" onClick={() => actions.setCreateModal({ gameId })}>Créer et associer</button></div>
<div className="empty"><p>Aucune toolbox associée à cette page jeu.</p><button className="nds-button" data-size="sm" data-variant="primary" onClick={() => actions.setCreateModal({ gameId })}>Créer et associer</button></div>
)}
<StorageQuota usage={storageUsage} />
</div>

View file

@ -27,7 +27,11 @@ export function AddToolControls({ onAdd }) {
aria-label="Ajouter un outil"
>
<button
className="primary tool-add-modal-button"
className="tool-add-modal-button nds-button"
data-size="sm"
data-variant="primary"
data-edge="left"
data-border="joined"
type="button"
onClick={() => setOpen(true)}
data-toolbox-tutorial-target="addTool"
@ -38,11 +42,13 @@ export function AddToolControls({ onAdd }) {
className="tool-add-quick-dropdown"
menuClassName="tool-quick-add"
label="Ajout rapide"
openOnHover
closeOnMouseLeave
renderTrigger={({ open: quickOpen, toggle }) => (
<button
className={`primary tool-add-quick-toggle ${quickOpen ? "active" : ""}`}
className={`tool-add-quick-toggle nds-icon-button ${quickOpen ? "active" : ""}`}
data-size="sm"
data-variant="primary"
data-edge="right"
data-border="joined"
type="button"
onClick={toggle}
aria-label="Afficher l'ajout rapide"
@ -58,7 +64,8 @@ export function AddToolControls({ onAdd }) {
{modules.map(([type, module]) => (
<button
key={type}
className="tool-quick-add-button"
className="tool-quick-add-button nds-icon-button"
data-size="sm"
type="button"
onClick={() => {
onAdd(type);
@ -82,7 +89,7 @@ export function AddToolControls({ onAdd }) {
<section className="confirm-modal tool-add-modal">
<header>
<h2 id="tool-add-modal-title">Ajouter un outil</h2>
<button className="drawer-close-button" type="button" onClick={() => setOpen(false)} aria-label="Fermer" title="Fermer">
<button className="drawer-close-button nds-icon-button" data-size="sm" type="button" onClick={() => setOpen(false)} aria-label="Fermer" title="Fermer">
<Icon name="close" />
</button>
</header>

View file

@ -41,7 +41,9 @@ export function CalculatorEntries({ entries, parentId, getScopedParentId, active
<li className={className} key={entry.id} {...reorder.getItemProps({ itemId: entry.id, parentId: getScopedParentId(parentId) })}>
<div className="tool-split-entry calculator-entry has-drag-handle">
<button
className="calculator-drag-handle"
className="calculator-drag-handle nds-icon-button"
data-size="xs"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, entry.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${entry.label || formatResult(entry.value)}`}
@ -58,6 +60,8 @@ export function CalculatorEntries({ entries, parentId, getScopedParentId, active
</span>
</button>
<button
className="nds-icon-button"
data-size="xs"
type="button"
onClick={() => onAddChild(entry)}
aria-label={`${textContent.addChildTitle || "Ajouter un calcul enfant"} ${entry.label || formatResult(entry.value)}`}
@ -65,7 +69,7 @@ export function CalculatorEntries({ entries, parentId, getScopedParentId, active
>
<Icon name="add" />
</button>
<button type="button" className="danger" onClick={() => onDelete(entry.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${entry.label}`} title={textContent.deleteTitle || "Supprimer"}>
<button type="button" className="nds-icon-button" data-size="xs" data-variant="danger" onClick={() => onDelete(entry.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${entry.label}`} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>

View file

@ -228,8 +228,10 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
/>
) : (
<input
type="text"
aria-label={textContent.expressionLabel || "Calcul"}
className="calculator-expression-input"
className="calculator-expression-input nds-input"
data-size="sm"
ref={expressionInputRef}
onChange={(event) => setExpression(event.target.value)}
onKeyDown={(event) => {
@ -251,9 +253,9 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
</div>
<label>
<span>{textContent.labelLabel || "Libellé"}</span>
<input value={label} onChange={(event) => setLabel(event.target.value)} placeholder={textContent.labelPlaceholder || "Lingots de fer"} />
<input className="nds-input" data-size="sm" type="text" value={label} onChange={(event) => setLabel(event.target.value)} placeholder={textContent.labelPlaceholder || "Lingots de fer"} />
</label>
<button className="primary" disabled={result == null}>{formMode === "edit" ? textContent.updateButton || "Mettre à jour" : textContent.saveButton || "Enregistrer"}</button>
<button className="nds-button" data-size="sm" data-variant="primary" disabled={result == null}>{formMode === "edit" ? textContent.updateButton || "Mettre à jour" : textContent.saveButton || "Enregistrer"}</button>
</div>
</form>
<div
@ -262,18 +264,19 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
>
<div className="tool-split-actions">
{(formulaParent || editingEntry) && (
<button className="tool-split-root-button" type="button" onClick={returnToRoot}>
<button className="tool-split-root-button nds-button" data-size="xs" type="button" onClick={returnToRoot}>
{editingEntry ? textContent.newButton || "Nouveau calcul" : textContent.rootButton || "Revenir à la racine"}
</button>
)}
<button className="tool-split-action-button danger" type="button" onClick={resetCalculator} aria-label={textContent.resetTitle || "Réinitialiser"} title={textContent.resetTitle || "Réinitialiser"}>
<button className="tool-split-action-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={resetCalculator} aria-label={textContent.resetTitle || "Réinitialiser"} title={textContent.resetTitle || "Réinitialiser"}>
<Icon name="rubber" />
</button>
<button className="tool-split-action-button" type="button" onClick={copyChecklistImport} disabled={!data.entries.length} aria-label={textContent.copyTitle || "Copier pour checklist"} title={copied ? textContent.copiedTitle || "Copié" : textContent.copyTitle || "Copier pour checklist"}>
<button className="tool-split-action-button nds-icon-button" data-size="sm" type="button" onClick={copyChecklistImport} disabled={!data.entries.length} aria-label={textContent.copyTitle || "Copier pour checklist"} title={copied ? textContent.copiedTitle || "Copié" : textContent.copyTitle || "Copier pour checklist"}>
<Icon name="copy" />
</button>
<button
className={`tool-split-scroll-toggle ${data.scrollResults ? "active" : ""}`}
className={`tool-split-scroll-toggle nds-switch ${data.scrollResults ? "active" : ""}`}
data-size="md"
type="button"
onClick={() => setScrollResults(!data.scrollResults)}
aria-pressed={data.scrollResults}

View file

@ -130,7 +130,8 @@ function CalendarWeekOptions({ textContent, hideEmptyHours, onToggle }) {
<div className="calendar-week-options">
<span>{textContent.hideEmptyHoursLabel || "Masquer les heures vides"}</span>
<button
className={`tool-split-scroll-toggle calendar-hide-hours-switch ${hideEmptyHours ? "active" : ""}`}
className={`tool-split-scroll-toggle calendar-hide-hours-switch nds-switch ${hideEmptyHours ? "active" : ""}`}
data-size="md"
type="button"
onClick={onToggle}
aria-pressed={hideEmptyHours}

View file

@ -2,19 +2,11 @@
import { Icon } from "../../../components/Icon.jsx";
import {
CALENDAR_ALERT_OFFSETS,
CALENDAR_COLORS
CALENDAR_COLORS,
CALENDAR_COLOR_VALUES
} from "./calendarUtils.js";
import { ALERT_MODES } from "./timerOptions.js";
const CALENDAR_COLOR_VALUES = {
violet: "#8b5cf6",
gold: "#f6c453",
cyan: "#22d3ee",
pink: "#d946ef",
green: "#22c55e",
gray: "#7d879f"
};
export function MonthlyRepeatSwitch({ value, textContent, onChange }) {
const options = [
{ value: "monthDay", icon: "day-number", label: textContent.monthlyRepeatMonthDay || "Numéro du jour" },
@ -22,7 +14,7 @@ export function MonthlyRepeatSwitch({ value, textContent, onChange }) {
];
return (
<div className="layout-switch calendar-month-repeat-switch" role="group" aria-label={textContent.monthlyRepeatLabel || "Règle mensuelle"}>
<div className="layout-switch calendar-month-repeat-switch nds-radio-button-group" data-size="sm" role="group" aria-label={textContent.monthlyRepeatLabel || "Règle mensuelle"}>
{options.map((option) => (
<button
key={option.value}
@ -49,7 +41,7 @@ export function MonthEventRepeatSwitch({ value, monthlyRepeatBy, textContent, on
const safeValue = monthlyRepeatBy === "monthDay" && value === "weekly" ? "none" : value;
return (
<div className="calendar-month-event-repeat-switch" role="group" aria-label={textContent.repeatLabel || "Répétition"}>
<div className="calendar-month-event-repeat-switch nds-radio-button-group" data-size="sm" role="group" aria-label={textContent.repeatLabel || "Répétition"}>
{options.map((option) => (
<button
key={option.value}
@ -80,7 +72,7 @@ export function CalendarAlertOffsets({ selectedOffsets, textContent, onToggle })
<legend>{textContent.alertOffsetLabel || "Alerte"}</legend>
{CALENDAR_ALERT_OFFSETS.map((offset) => (
<label key={offset}>
<input type="checkbox" checked={selectedOffsets.includes(offset)} onChange={() => onToggle(offset)} />
<input className="nds-checkbox" data-size="sm" type="checkbox" checked={selectedOffsets.includes(offset)} onChange={() => onToggle(offset)} />
<span>{labels[offset]}</span>
</label>
))}
@ -94,7 +86,7 @@ export function CalendarColorPicker({ value, textContent, onChange }) {
<div className="calendar-color-picker notepad-color-dropdown">
<span>{label}</span>
<button
className="calendar-color-toggle notepad-color-toggle"
className="calendar-color-toggle notepad-color-toggle nds-swatch"
type="button"
title={label}
aria-label={label}
@ -104,7 +96,7 @@ export function CalendarColorPicker({ value, textContent, onChange }) {
{CALENDAR_COLORS.map((color) => (
<button
key={color}
className={`notepad-swatch ${value === color ? "active" : ""}`}
className={`notepad-swatch nds-swatch ${value === color ? "active" : ""}`}
type="button"
title={textContent[`color${color.charAt(0).toUpperCase()}${color.slice(1)}`] || color}
aria-label={textContent[`color${color.charAt(0).toUpperCase()}${color.slice(1)}`] || color}
@ -126,7 +118,7 @@ export function CalendarAlertModeSwitch({ mode, textContent, onChange }) {
};
return (
<div className="timer-alert-mode-switch calendar-alert-mode-switch" role="group" aria-label={textContent.alertModeTitle || "Mode d'alerte"}>
<div className="timer-alert-mode-switch calendar-alert-mode-switch nds-radio-button-group" data-size="xs" role="group" aria-label={textContent.alertModeTitle || "Mode d'alerte"}>
{ALERT_MODES.map((option) => (
<button
key={option.mode}

View file

@ -29,14 +29,14 @@ export function CalendarEventForm({
<div className="calendar-form-row calendar-form-main-row">
<label className="calendar-title-field">
<span>{textContent.titleLabel || "Titre"}</span>
<input value={draft.title} onChange={(event) => onDraftChange({ title: event.target.value })} placeholder={textContent.titlePlaceholder || "Raid, reset, event..."} />
<input className="nds-input" data-size="sm" type="text" value={draft.title} onChange={(event) => onDraftChange({ title: event.target.value })} placeholder={textContent.titlePlaceholder || "Raid, reset, event..."} />
</label>
<CalendarColorPicker value={draft.color} textContent={textContent} onChange={(color) => onDraftChange({ color })} />
{draft.id && <div className="calendar-delete-actions">
{data.viewMode === "month" && draft.repeat !== "none" && activeOccurrenceDate && (
<button className="danger calendar-form-delete-button" type="button" onClick={() => onDeleteOccurrence(draft.id)} aria-label={textContent.deleteOccurrenceTitle || "Supprimer cette occurrence"} title={textContent.deleteOccurrenceTitle || "Supprimer cette occurrence"}><Icon name="trashcan-one" /></button>
<button className="calendar-form-delete-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={() => onDeleteOccurrence(draft.id)} aria-label={textContent.deleteOccurrenceTitle || "Supprimer cette occurrence"} title={textContent.deleteOccurrenceTitle || "Supprimer cette occurrence"}><Icon name="trashcan-one" /></button>
)}
<button className="danger calendar-form-delete-button" type="button" onClick={() => onDeleteEvent(draft.id)} aria-label={data.viewMode === "month" && draft.repeat !== "none" ? textContent.deleteSeriesTitle || "Supprimer toutes les répétitions" : textContent.deleteTitle || "Supprimer"} title={data.viewMode === "month" && draft.repeat !== "none" ? textContent.deleteSeriesTitle || "Supprimer toutes les répétitions" : textContent.deleteTitle || "Supprimer"}><Icon name="trash" /></button>
<button className="calendar-form-delete-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={() => onDeleteEvent(draft.id)} aria-label={data.viewMode === "month" && draft.repeat !== "none" ? textContent.deleteSeriesTitle || "Supprimer toutes les répétitions" : textContent.deleteTitle || "Supprimer"} title={data.viewMode === "month" && draft.repeat !== "none" ? textContent.deleteSeriesTitle || "Supprimer toutes les répétitions" : textContent.deleteTitle || "Supprimer"}><Icon name="trash" /></button>
</div>}
</div>
<div className={`calendar-form-row ${data.viewMode === "month" ? "calendar-form-month-row" : "calendar-form-time-row"}`}>
@ -67,6 +67,8 @@ export function CalendarEventForm({
/>
) : (
<select
className="nds-select"
data-size="sm"
value={draft.dayIndex}
onChange={(event) => {
const value = Number(event.target.value);
@ -97,11 +99,11 @@ export function CalendarEventForm({
<>
<label className="calendar-time-field">
<span>{textContent.startLabel || "Début"}</span>
<input type="time" step={WEEK_TIME_INPUT_STEP_MINUTES * 60} value={draft.startTime} onChange={(event) => onDraftChange({ startTime: event.target.value })} />
<input className="nds-time" data-size="xs" type="time" step={WEEK_TIME_INPUT_STEP_MINUTES * 60} value={draft.startTime} onChange={(event) => onDraftChange({ startTime: event.target.value })} />
</label>
<label className="calendar-time-field">
<span>{textContent.endLabel || "Fin"}</span>
<input type="time" step={WEEK_TIME_INPUT_STEP_MINUTES * 60} value={draft.endTime} onChange={(event) => onDraftChange({ endTime: event.target.value })} />
<input className="nds-time" data-size="xs" type="time" step={WEEK_TIME_INPUT_STEP_MINUTES * 60} value={draft.endTime} onChange={(event) => onDraftChange({ endTime: event.target.value })} />
</label>
</>
)}
@ -113,7 +115,8 @@ export function CalendarEventForm({
) : <div className="calendar-repeat-field">
<span>{textContent.repeatableTitle || "Répétable"}</span>
<button
className={`tool-split-scroll-toggle calendar-repeat-switch ${draft.repeatable !== false ? "active" : ""}`}
className={`tool-split-scroll-toggle calendar-repeat-switch nds-switch ${draft.repeatable !== false ? "active" : ""}`}
data-size="lg"
type="button"
onClick={() => onDraftChange({ repeatable: draft.repeatable === false })}
aria-pressed={draft.repeatable !== false}
@ -131,8 +134,8 @@ export function CalendarEventForm({
<CalendarAlertModeSwitch mode={draft.alertMode} textContent={textContent} onChange={onAlertModeChange} />
</div>
<div className="calendar-form-actions">
<button className="primary" type="submit">{draft.id ? textContent.saveButton || "Enregistrer" : textContent.addButton || "Ajouter"}</button>
<button type="button" onClick={onClose}>{textContent.cancelButton || "Annuler"}</button>
<button className="nds-button" data-size="sm" data-variant="primary" type="submit">{draft.id ? textContent.saveButton || "Enregistrer" : textContent.addButton || "Ajouter"}</button>
<button className="nds-button" data-size="sm" type="button" onClick={onClose}>{textContent.cancelButton || "Annuler"}</button>
</div>
</div>
{error && <p className="timer-form-error calendar-form-error" role="alert"><Icon name="warning" /><span>{error}</span></p>}

View file

@ -48,7 +48,7 @@ export function MonthDateDropdown({ value, displayedMonth, minDate = "", mode, t
const safeValue = normalizeDraftDateKey(value) || formatLocalDateKey(displayedMonth);
return (
<div className={`calendar-month-date-dropdown ${align === "end" ? "is-aligned-end" : ""}`}>
<button className="calendar-month-date-trigger" type="button" aria-label={label}>
<button className="calendar-month-date-trigger nds-date-trigger" data-size="sm" type="button" aria-label={label}>
<Icon name={mode === "weekday" ? "day-name" : "day-number"} />
<span>{formatMonthDateLabel(safeValue, mode)}</span>
<Icon name="chevron-down" />

View file

@ -4,6 +4,8 @@ import {
WEEK_SLOT_MINUTES,
addDays,
formatLocalDateKey,
getCalendarColorValue,
getCalendarEventSurfaceVariant,
minutesToTimeValue,
pad2
} from "./calendarUtils.js";
@ -137,19 +139,26 @@ function CalendarOccurrence({ occurrence, layout, active, onEdit }) {
const durationMinutes = Math.max(WEEK_SLOT_MINUTES, (occurrence.endMs - occurrence.startMs) / 60000 || WEEK_SLOT_MINUTES);
const startMinutes = occurrence.event.startMinutes;
const timeLabel = `${minutesToTimeValue(occurrence.event.startMinutes)}${occurrence.event.endMinutes ? ` - ${minutesToTimeValue(occurrence.event.endMinutes)}` : ""}`;
const compact = durationMinutes < 120;
const top = getWeekMinutePosition(layout, startMinutes);
const bottom = getWeekMinutePosition(layout, Math.min(24 * 60, startMinutes + durationMinutes));
const height = compact ? WEEK_COMPACT_EVENT_HEIGHT : Math.max(WEEK_COMPACT_EVENT_HEIGHT, bottom - top);
const naturalHeight = bottom - top;
const eventSurfaceSize = naturalHeight < WEEK_COMPACT_EVENT_HEIGHT ? "xs" : naturalHeight < 44 ? "sm" : "md";
const height = eventSurfaceSize === "xs" ? 10 : eventSurfaceSize === "sm" ? WEEK_COMPACT_EVENT_HEIGHT : Math.max(44, naturalHeight);
const color = occurrence.event.color || "violet";
return (
<article
className={`calendar-event calendar-event-${occurrence.event.color || "violet"} ${active ? "is-active" : ""} ${compact ? "is-compact" : ""}`}
className={`calendar-event nds-event-surface ${active ? "is-active" : ""} ${eventSurfaceSize !== "md" ? "is-compact" : ""}`}
data-color={getCalendarColorValue(color)}
data-size={eventSurfaceSize}
data-variant={getCalendarEventSurfaceVariant(color)}
style={{
top: `${top}px`,
width: "calc((100% / var(--calendar-event-lane-count, 1)) - 2px)",
height: `${height}px`,
"--calendar-event-lane": occurrence.lane || 0,
"--calendar-event-lane-count": occurrence.laneCount || 1
"--calendar-event-lane-count": occurrence.laneCount || 1,
"--nds-event-color": getCalendarColorValue(color)
}}
>
<Tooltip
@ -161,7 +170,7 @@ function CalendarOccurrence({ occurrence, layout, active, onEdit }) {
onClick={onEdit}
>
<strong>{occurrence.event.title}</strong>
{!compact && <span>{timeLabel}</span>}
<span>{timeLabel}</span>
</Tooltip>
</article>
);
@ -188,10 +197,10 @@ export function CalendarMonthView({
return (
<>
<div className="calendar-month-toolbar">
<button type="button" onClick={onPreviousMonth} title={textContent.previousMonthTitle || "Mois précédent"} aria-label={textContent.previousMonthTitle || "Mois précédent"}>{textContent.previousShortLabel || "Prec."}</button>
<button className="nds-button" data-size="xs" type="button" onClick={onPreviousMonth} title={textContent.previousMonthTitle || "Mois précédent"} aria-label={textContent.previousMonthTitle || "Mois précédent"}>{textContent.previousShortLabel || "Prec."}</button>
<strong>{formatMonthTitle(monthStart)}</strong>
<button type="button" onClick={onCurrentMonth}>{textContent.currentMonthTitle || "Mois courant"}</button>
<button type="button" onClick={onNextMonth} title={textContent.nextMonthTitle || "Mois suivant"} aria-label={textContent.nextMonthTitle || "Mois suivant"}>{textContent.nextShortLabel || "Suiv."}</button>
<button className="nds-button" data-size="xs" type="button" onClick={onCurrentMonth}>{textContent.currentMonthTitle || "Mois courant"}</button>
<button className="nds-button" data-size="xs" type="button" onClick={onNextMonth} title={textContent.nextMonthTitle || "Mois suivant"} aria-label={textContent.nextMonthTitle || "Mois suivant"}>{textContent.nextShortLabel || "Suiv."}</button>
</div>
<CalendarMonthGrid
days={monthGridDays}
@ -268,22 +277,29 @@ function CalendarMonthBar({ segment, active, onEdit }) {
const startColumn = segment.startIndex % WEEKDAY_LABELS.length + 1;
const endColumn = segment.endIndex % WEEKDAY_LABELS.length + 2;
const laneCount = Math.max(1, Math.min(3, segment.laneCount || 1));
const barHeight = laneCount === 1 ? 28 : laneCount === 2 ? 23 : 18;
const barGap = laneCount === 3 ? 3 : 4;
const barHeight = laneCount <= 2 ? 22 : 10;
const barGap = laneCount === 3 ? 2 : 4;
const eventSurfaceSize = barHeight <= 10 ? "xs" : barHeight <= 22 ? "sm" : "md";
const startLabel = new Date(segment.occurrence.startMs).toLocaleDateString(undefined, { day: "2-digit", month: "short", year: "numeric" });
const endDate = addDays(new Date(segment.occurrence.endMs), -1);
const endLabel = endDate.toLocaleDateString(undefined, { day: "2-digit", month: "short", year: "numeric" });
const dateLabel = startLabel === endLabel ? startLabel : `${startLabel} - ${endLabel}`;
const color = segment.occurrence.event.color || "violet";
return (
<article
className={`calendar-month-bar calendar-event-${segment.occurrence.event.color || "violet"} ${active ? "is-active" : ""}`}
className={`calendar-month-bar nds-event-surface ${active ? "is-active" : ""}`}
data-color={getCalendarColorValue(color)}
data-size={eventSurfaceSize}
data-variant={getCalendarEventSurfaceVariant(color)}
style={{
gridColumn: `${startColumn} / ${endColumn}`,
gridRow,
width: "auto",
"--calendar-event-lane": segment.lane || 0,
"--calendar-month-bar-height": `${barHeight}px`,
"--calendar-month-bar-gap": `${barGap}px`
"--calendar-month-bar-gap": `${barGap}px`,
"--nds-event-color": getCalendarColorValue(color)
}}
>
<Tooltip

View file

@ -113,10 +113,10 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing, textExc
{editing && (
<div className="module-add-panel">
<form className="inline-form checklist-add-form" onSubmit={addItem}>
<input name="section" placeholder={textContent.sectionPlaceholder || "Catégorie"} value={sectionTitle} onChange={(event) => setSectionTitle(event.target.value)} />
<input name="label" placeholder={textContent.itemPlaceholder || "Nouvel item"} value={label} onChange={(event) => setLabel(event.target.value)} />
<input className="checklist-qty-input" name="qty" type="number" min="1" value={qty} onChange={(event) => setQty(event.target.value)} aria-label={textContent.quantityLabel || "Quantité cible"} />
<button className="primary">{textContent.addButton || "Ajouter"}</button>
<input className="nds-input" data-size="sm" name="section" type="text" placeholder={textContent.sectionPlaceholder || "Catégorie"} value={sectionTitle} onChange={(event) => setSectionTitle(event.target.value)} />
<input className="nds-input" data-size="sm" name="label" type="text" placeholder={textContent.itemPlaceholder || "Nouvel item"} value={label} onChange={(event) => setLabel(event.target.value)} />
<input className="checklist-qty-input nds-input" data-size="sm" name="qty" type="number" min="1" value={qty} onChange={(event) => setQty(event.target.value)} aria-label={textContent.quantityLabel || "Quantité cible"} />
<button className="nds-button" data-size="sm" data-variant="primary">{textContent.addButton || "Ajouter"}</button>
</form>
</div>
)}
@ -126,25 +126,27 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing, textExc
</div>
)}
{namedSections.length > 0 && (
<div className="checklist-toolbar" role="radiogroup" aria-label={textContent.completedSectionsModeTitle || "Affichage des catégories terminées"}>
{[
{ mode: "visible", label: textContent.completedSectionsVisibleLabel || "Visible", icon: "eye-open" },
{ mode: "reduced", label: textContent.completedSectionsReducedLabel || "Réduit", icon: "hide" },
{ mode: "hidden", label: textContent.completedSectionsHiddenLabel || "Caché", icon: "eye-closed" }
].map((option) => (
<button
className={`checklist-complete-mode-button ${completedSectionsMode === option.mode ? "active" : ""}`}
type="button"
role="radio"
aria-checked={completedSectionsMode === option.mode}
aria-label={option.label}
key={option.mode}
onClick={() => setCompletedSectionsMode(option.mode)}
title={option.label}
>
<Icon name={option.icon} />
</button>
))}
<div className="checklist-toolbar">
<div className="checklist-complete-mode-group nds-radio-button-group" data-size="xs" role="radiogroup" aria-label={textContent.completedSectionsModeTitle || "Affichage des catégories terminées"}>
{[
{ mode: "visible", label: textContent.completedSectionsVisibleLabel || "Visible", icon: "eye-open" },
{ mode: "reduced", label: textContent.completedSectionsReducedLabel || "Réduit", icon: "hide" },
{ mode: "hidden", label: textContent.completedSectionsHiddenLabel || "Caché", icon: "eye-closed" }
].map((option) => (
<button
className={`checklist-complete-mode-button ${completedSectionsMode === option.mode ? "active" : ""}`}
type="button"
role="radio"
aria-checked={completedSectionsMode === option.mode}
aria-label={option.label}
key={option.mode}
onClick={() => setCompletedSectionsMode(option.mode)}
title={option.label}
>
<Icon name={option.icon} />
</button>
))}
</div>
</div>
)}
<ul className="checklist">

View file

@ -67,7 +67,9 @@ export function ChecklistSection({ section, context, data, reorder, onRenameSect
{showTitle && (
<div className="checklist-section-header">
<button
className="checklist-drag-handle"
className="checklist-drag-handle nds-icon-button"
data-size="xs"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.groupReorder.startDrag(event, { groupId: section.id })}
aria-label={`${textContent.reorderSectionTitle || "Déplacer la catégorie"} ${section.title}`}
@ -79,6 +81,7 @@ export function ChecklistSection({ section, context, data, reorder, onRenameSect
<input
{...titleEdit.getInputProps({
className: "checklist-section-title-input",
type: "text",
title: textContent.sectionEditTitle || "Renommer la catégorie",
"aria-label": textContent.sectionEditTitle || "Renommer la catégorie"
})}
@ -87,7 +90,8 @@ export function ChecklistSection({ section, context, data, reorder, onRenameSect
<div className="checklist-section-actions">
<span>{section.items.filter((item) => context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget).length} / {section.items.length}</span>
<button
className="checklist-section-collapse-button"
className="checklist-section-collapse-button nds-icon-button"
data-size="xs"
type="button"
onClick={toggleSectionCollapsed}
aria-expanded={!isCollapsed}
@ -160,7 +164,9 @@ function ChecklistItem({ item, section, context, reorder, items, save, onUpdateI
<li className={className} {...reorder.getItemProps({ itemId: item.id, groupId: section.id })}>
<div className={`checklist-item-main ${editing ? "is-editing" : ""}`}>
<button
className="checklist-drag-handle"
className="checklist-drag-handle nds-icon-button"
data-size="xs"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, item.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${item.label}`}
@ -170,17 +176,20 @@ function ChecklistItem({ item, section, context, reorder, items, save, onUpdateI
</button>
{item.qtyTarget === 1 ? (
<input
className="nds-checkbox"
data-size="sm"
type="checkbox"
checked={done}
onChange={(event) => updateItem((entry) => ({ ...entry, qtyTarget: 1, qtyCurrent: event.target.checked ? 1 : 0 }))}
aria-label={`Terminer ${item.label}`}
/>
) : (
<div className="checklist-qty-controls" aria-label={`Quantité ${item.label}`}>
<div className="checklist-qty-controls nds-quantity-stepper" data-size="md" aria-label={`Quantité ${item.label}`}>
<button type="button" onClick={() => updateItem((entry) => ({ ...entry, qtyCurrent: context.clampQty(entry.qtyCurrent - 1, entry.qtyTarget) }))} aria-label={textContent.decrementLabel || "Retirer une quantité"}>-</button>
<label className="checklist-qty-current">
<input
type="number"
className="nds-quantity-stepper-input"
max={item.qtyTarget}
value={context.clampQty(item.qtyCurrent, item.qtyTarget)}
style={{ "--qty-current-digits": String(context.clampQty(item.qtyCurrent, item.qtyTarget)).length }}
@ -196,13 +205,15 @@ function ChecklistItem({ item, section, context, reorder, items, save, onUpdateI
<>
<input
{...labelEdit.getInputProps({
className: "checklist-label-input",
className: "checklist-label-input nds-inline-input",
type: "text",
"aria-label": textContent.itemEditLabel || "Libellé"
})}
/>
<input
{...categoryEdit.getInputProps({
className: "checklist-category-input",
className: "checklist-category-input nds-inline-input",
type: "text",
placeholder: textContent.sectionPlaceholder || "Catégorie",
"aria-label": textContent.categoryEditLabel || "Catégorie"
})}
@ -214,10 +225,10 @@ function ChecklistItem({ item, section, context, reorder, items, save, onUpdateI
</button>
)}
</div>
<button type="button" className="checklist-edit-button" onClick={onToggleEditing} aria-label={`${textContent.editTitle || "Modifier"} ${item.label}`} title={textContent.editTitle || "Modifier"}>
<button type="button" className="checklist-edit-button nds-icon-button" data-size="sm" onClick={onToggleEditing} aria-label={`${textContent.editTitle || "Modifier"} ${item.label}`} title={textContent.editTitle || "Modifier"}>
<Icon name="edit" />
</button>
<button className="checklist-delete-button danger" onClick={() => onDeleteItem(item.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${item.label}`} title={textContent.deleteTitle || "Supprimer"}>
<button className="checklist-delete-button nds-icon-button" data-size="sm" data-variant="danger" onClick={() => onDeleteItem(item.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${item.label}`} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</li>

View file

@ -48,14 +48,14 @@ export function CombosEditor({
<div className="combos-device-row">
<label>
<span>{textContent.deviceLabel || "Périphérique"}</span>
<select value={data.device} onChange={(event) => onDeviceChange(event.target.value)}>
<select className="nds-select" data-size="sm" value={data.device} onChange={(event) => onDeviceChange(event.target.value)}>
{DEVICE_OPTIONS.map((device) => (
<option key={device.value} value={device.value}>{device.label}</option>
))}
</select>
</label>
{data.device === "keyboardMouse" && (
<div className="combos-keyboard-layout-switch" role="group" aria-label={textContent.keyboardLayoutLabel || "Disposition clavier"}>
<div className="combos-keyboard-layout-switch nds-radio-button-group" data-size="sm" role="group" aria-label={textContent.keyboardLayoutLabel || "Disposition clavier"}>
{[
{ value: "azerty", label: "AZERTY" },
{ value: "qwerty", label: "QWERTY" }
@ -78,11 +78,11 @@ export function CombosEditor({
<div className="combos-form-grid">
<label>
<span>{textContent.targetCategoryLabel || "Catégorie"}</span>
<input value={comboCategory} onChange={(event) => onComboCategoryChange(event.target.value)} placeholder={textContent.noCategoryLabel || "Sans catégorie"} />
<input className="nds-input" data-size="sm" type="text" value={comboCategory} onChange={(event) => onComboCategoryChange(event.target.value)} placeholder={textContent.noCategoryLabel || "Sans catégorie"} />
</label>
<label>
<span>{textContent.comboNameLabel || "Nom"}</span>
<input value={comboName} onChange={(event) => onComboNameChange(event.target.value)} placeholder={textContent.comboNamePlaceholder || "Nom du combo"} />
<input className="nds-input" data-size="sm" type="text" value={comboName} onChange={(event) => onComboNameChange(event.target.value)} placeholder={textContent.comboNamePlaceholder || "Nom du combo"} />
</label>
</div>
<div className="combos-palette" aria-label={textContent.paletteLabel || "Palette de touches"}>
@ -115,8 +115,8 @@ export function CombosEditor({
{data.device === "keyboardMouse" && (
<div className="inline-form combos-custom-key-form">
<input value={customKey} onChange={(event) => onCustomKeyChange(event.target.value)} placeholder={textContent.customKeyPlaceholder || "Touche personnalisée"} />
<button type="button" onClick={onCustomKeyAdd}>{textContent.addCustomKeyButton || "Ajouter touche"}</button>
<input className="nds-input" data-size="sm" type="text" value={customKey} onChange={(event) => onCustomKeyChange(event.target.value)} placeholder={textContent.customKeyPlaceholder || "Touche personnalisée"} />
<button className="nds-button" data-size="sm" type="button" onClick={onCustomKeyAdd}>{textContent.addCustomKeyButton || "Ajouter touche"}</button>
</div>
)}
@ -135,7 +135,7 @@ export function CombosEditor({
/>
<div className="combo-draft-actions">
<label className="combo-hold-field">
<span className="combo-hold-controls">
<span className="combo-hold-controls nds-compound-toggle-field">
<button
type="button"
className={selectedDraftInput?.hold || selectedDraftInput?.holdMs ? "active" : ""}
@ -146,6 +146,7 @@ export function CombosEditor({
{textContent.holdToggleLabel || "Hold"}
</button>
<input
className="nds-compound-toggle-field-input"
type="number"
min="0"
max="99"
@ -161,7 +162,8 @@ export function CombosEditor({
</label>
<button
type="button"
className={`combos-simultaneous-toggle ${simultaneousMode ? "active" : ""}`}
className={`combos-simultaneous-toggle nds-radio-button-group ${simultaneousMode ? "active" : ""}`}
data-size="xs"
onClick={onSimultaneousModeToggle}
aria-pressed={simultaneousMode}
title={textContent.simultaneousModeTitle || "Ajout simultané"}
@ -170,19 +172,19 @@ export function CombosEditor({
<span className={!simultaneousMode ? "active" : ""}>{textContent.successiveModeLabel || "A › B"}</span>
<span className={simultaneousMode ? "active" : ""}>{textContent.simultaneousModeLabel || "A + B"}</span>
</button>
<button type="button" className="notepad-toolbar-button" onClick={onLastInputRemove} disabled={!cleanDraftInputs.length} title={textContent.removeLastInputTitle || "Retirer la dernière touche"} aria-label={textContent.removeLastInputTitle || "Retirer la dernière touche"}>
<button type="button" className="notepad-toolbar-button nds-icon-button" data-size="sm" onClick={onLastInputRemove} disabled={!cleanDraftInputs.length} title={textContent.removeLastInputTitle || "Retirer la dernière touche"} aria-label={textContent.removeLastInputTitle || "Retirer la dernière touche"}>
<Icon name="back" />
</button>
<button type="button" className="notepad-toolbar-button danger" onClick={onActiveStepRemove} disabled={draftInputs.length <= 1} title={textContent.removeStepTitle || "Supprimer l'étape"} aria-label={textContent.removeStepTitle || "Supprimer l'étape"}>
<button type="button" className="notepad-toolbar-button nds-icon-button" data-size="sm" data-variant="danger" onClick={onActiveStepRemove} disabled={draftInputs.length <= 1} title={textContent.removeStepTitle || "Supprimer l'étape"} aria-label={textContent.removeStepTitle || "Supprimer l'étape"}>
<Icon name="trash" />
</button>
<div className="combo-save-actions">
{editingComboId && (
<button type="button" className="combo-cancel-button" onClick={onComboEditCancel}>
<button type="button" className="combo-cancel-button nds-button" data-size="sm" onClick={onComboEditCancel}>
{textContent.cancelEditButton || "Annuler"}
</button>
)}
<button className="primary" disabled={!comboName.trim() && !cleanDraftInputs.length}>
<button className="nds-button" data-size="sm" data-variant="primary" disabled={!comboName.trim() && !cleanDraftInputs.length}>
{editingComboId ? textContent.saveComboButton || "Enregistrer" : textContent.addComboButton || "Ajouter le combo"}
</button>
</div>

View file

@ -27,7 +27,9 @@ export function CombosList({
return (
<article className={comboClassName} key={combo.id} {...reorder.getItemProps({ itemId: combo.id, groupId: category })}>
<button
className="task-planner-drag-handle combo-drag-handle"
className="task-planner-drag-handle combo-drag-handle nds-icon-button"
data-size="xs"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, combo.id)}
aria-label={`${textContent.reorderComboTitle || "Déplacer le combo"} ${combo.name}`}
@ -45,10 +47,10 @@ export function CombosList({
<ComboSequence inputs={combo.inputs} device={combo.device || data.device} emptyLabel={textContent.emptySequence || "Combo sans touches"} />
</div>
<div className="combo-card-actions">
<button type="button" className="checklist-delete-button" onClick={() => onComboEdit(combo)} aria-label={`${textContent.editComboTitle || "Modifier le combo"} ${combo.name}`} title={textContent.editComboTitle || "Modifier le combo"}>
<button type="button" className="checklist-delete-button nds-icon-button" data-size="sm" onClick={() => onComboEdit(combo)} aria-label={`${textContent.editComboTitle || "Modifier le combo"} ${combo.name}`} title={textContent.editComboTitle || "Modifier le combo"}>
<Icon name="edit" />
</button>
<button type="button" className="checklist-delete-button danger" onClick={() => onComboDelete(combo.id)} aria-label={`${textContent.deleteComboTitle || "Supprimer le combo"} ${combo.name}`} title={textContent.deleteComboTitle || "Supprimer le combo"}>
<button type="button" className="checklist-delete-button nds-icon-button" data-size="sm" data-variant="danger" onClick={() => onComboDelete(combo.id)} aria-label={`${textContent.deleteComboTitle || "Supprimer le combo"} ${combo.name}`} title={textContent.deleteComboTitle || "Supprimer le combo"}>
<Icon name="trash" />
</button>
</div>
@ -88,7 +90,9 @@ export function CombosList({
<div className="checklist-section-header combos-category-header">
<div className="combos-category-title">
<button
className="task-planner-category-drag-handle"
className="task-planner-category-drag-handle nds-icon-button"
data-size="xs"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.groupReorder.startDrag(event, { groupId: category })}
aria-label={`${textContent.reorderCategoryTitle || "Déplacer la catégorie"} ${category}`}
@ -101,7 +105,8 @@ export function CombosList({
<div className="checklist-section-actions">
<span>{group.combos.length}</span>
<button
className="checklist-section-collapse-button"
className="checklist-section-collapse-button nds-icon-button"
data-size="xs"
type="button"
onClick={() => onCategoryToggle(category)}
aria-expanded={!isCollapsed}

View file

@ -83,7 +83,8 @@ export function InlineTextInput({ value, onCommit, className, ariaLabel, placeho
return (
<input
{...inlineEdit.getInputProps({
className,
className: `${className} nds-inline-input`.trim(),
type: "text",
placeholder,
"aria-label": ariaLabel
})}

View file

@ -47,8 +47,8 @@ export function CountersModule({ toolboxId, moduleId, context, editing, textExch
{editing && (
<div className="module-add-panel">
<form className="inline-form counters-add-form" onSubmit={addCounter}>
<input {...counterDraft.getFieldProps("label", { placeholder: textContent.labelPlaceholder || "Nom du compteur" })} />
<button className="primary">{textContent.addButton || "Ajouter"}</button>
<input {...counterDraft.getFieldProps("label", { className: "nds-input", "data-size": "sm", type: "text", placeholder: textContent.labelPlaceholder || "Nom du compteur" })} />
<button className="nds-button" data-size="sm" data-variant="primary">{textContent.addButton || "Ajouter"}</button>
</form>
</div>
)}
@ -69,7 +69,9 @@ export function CountersModule({ toolboxId, moduleId, context, editing, textExch
return (
<article className={className} key={counter.id} {...getItemProps({ itemId: counter.id, parentId: moduleId })}>
<button
className="counter-drag-handle"
className="counter-drag-handle nds-icon-button"
data-size="sm"
data-variant="ghost"
type="button"
onPointerDown={(event) => itemReorder.startDrag(event, counter.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${counter.label}`}
@ -82,12 +84,12 @@ export function CountersModule({ toolboxId, moduleId, context, editing, textExch
<span>{counter.label}</span>
</div>
<div className="counter-actions">
<button type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: item.value - 1 }))} aria-label={`${textContent.decrementLabel || "Décrémenter"} ${counter.label}`}>-</button>
<button type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: item.value + 1 }))} aria-label={`${textContent.incrementLabel || "Incrémenter"} ${counter.label}`}>+</button>
<button type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: 0 }))} aria-label={`${textContent.resetTitle || "Réinitialiser"} ${counter.label}`} title={textContent.resetTitle || "Réinitialiser"}>
<button className="nds-icon-button" data-size="sm" type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: item.value - 1 }))} aria-label={`${textContent.decrementLabel || "Décrémenter"} ${counter.label}`}>-</button>
<button className="nds-icon-button" data-size="sm" type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: item.value + 1 }))} aria-label={`${textContent.incrementLabel || "Incrémenter"} ${counter.label}`}>+</button>
<button className="nds-icon-button" data-size="sm" type="button" onClick={() => updateCounter(counter.id, (item) => ({ ...item, value: 0 }))} aria-label={`${textContent.resetTitle || "Réinitialiser"} ${counter.label}`} title={textContent.resetTitle || "Réinitialiser"}>
<Icon name="rubber" />
</button>
<button type="button" className="danger" onClick={() => save(data.counters.filter((item) => item.id !== counter.id))} aria-label={`${textContent.deleteTitle || "Supprimer"} ${counter.label}`} title={textContent.deleteTitle || "Supprimer"}>
<button type="button" className="nds-icon-button" data-size="sm" data-variant="danger" onClick={() => save(data.counters.filter((item) => item.id !== counter.id))} aria-label={`${textContent.deleteTitle || "Supprimer"} ${counter.label}`} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>

View file

@ -7,13 +7,14 @@ const DEFAULT_WIDTHS = [2, 4, 8, 12];
export function DrawingControls({ textContent = {}, active, onToggleActive, eraseActive, onToggleErase, color, colors, onColorChange, width, widths = DEFAULT_WIDTHS, onWidthChange, onUndo, onClear, storageMode, onToggleStorageMode }) {
return (
<div className={`drawing-controls-panel ${active ? "is-open" : ""}`} aria-label={textContent.drawingLabel || "Dessin"}>
<button className={`drawing-control-button ${active ? "active" : ""}`} type="button" aria-pressed={active} title={textContent.drawTitle || "Dessiner"} onClick={onToggleActive}>
<button className={`drawing-control-button nds-icon-button ${active ? "active" : ""}`} data-size="sm" type="button" aria-pressed={active} title={textContent.drawTitle || "Dessiner"} onClick={onToggleActive}>
<Icon name="edit" />
</button>
{active && (
<>
<button
className={`drawing-control-button ${eraseActive ? "active" : ""}`}
className={`drawing-control-button nds-icon-button ${eraseActive ? "active" : ""}`}
data-size="sm"
type="button"
aria-pressed={eraseActive}
title={textContent.eraseTitle || "Gommer"}
@ -33,15 +34,16 @@ export function DrawingControls({ textContent = {}, active, onToggleActive, eras
value={width}
onSelect={onWidthChange}
/>
<button className="drawing-control-button" type="button" title={textContent.undoDrawingTitle || "Annuler le dernier trait"} onClick={onUndo}>
<button className="drawing-control-button nds-icon-button" data-size="sm" type="button" title={textContent.undoDrawingTitle || "Annuler le dernier trait"} onClick={onUndo}>
<Icon name="back" />
</button>
<button className="drawing-control-button danger" type="button" title={textContent.clearDrawingTitle || "Effacer les dessins"} onClick={onClear}>
<button className="drawing-control-button nds-icon-button" data-size="sm" data-variant="danger" type="button" title={textContent.clearDrawingTitle || "Effacer les dessins"} onClick={onClear}>
<Icon name="trash" />
</button>
{onToggleStorageMode && (
<button
className={`module-scroll-button drawing-storage-switch ${storageMode === "permanent" ? "active" : ""}`}
className={`module-scroll-button nds-switch drawing-storage-switch ${storageMode === "permanent" ? "active" : ""}`}
data-size="lg"
type="button"
aria-pressed={storageMode === "permanent"}
aria-label={storageMode === "permanent" ? textContent.permanentTitle || "Annotations permanentes" : textContent.temporaryTitle || "Annotations temporaires"}
@ -67,7 +69,7 @@ export function FormattingColorDropdown({ label, sections }) {
closeOnMouseLeave
preventMouseDown
renderTrigger={({ toggle }) => (
<button className="notepad-color-toggle is-rainbow" type="button" title={label} aria-label={label} onClick={toggle} />
<button className="notepad-color-toggle nds-swatch is-rainbow" data-variant="rainbow" type="button" title={label} aria-label={label} onClick={toggle} />
)}
>
{({ close }) => (
@ -79,7 +81,7 @@ export function FormattingColorDropdown({ label, sections }) {
{section.colors.map((color) => (
<button
key={`${section.label}-${color}`}
className={`notepad-swatch ${section.value === color ? "active" : ""}`}
className={`notepad-swatch nds-swatch ${section.value === color ? "active" : ""}`}
type="button"
title={`${section.label} ${color}`}
aria-label={`${section.label} ${color}`}
@ -110,7 +112,8 @@ export function DrawingColorDropdown({ label, colors, value, onSelect }) {
preventMouseDown
renderTrigger={({ toggle }) => (
<button
className="notepad-color-toggle is-rainbow"
className="notepad-color-toggle nds-swatch is-rainbow"
data-variant="rainbow"
type="button"
title={label}
aria-label={label}
@ -124,7 +127,7 @@ export function DrawingColorDropdown({ label, colors, value, onSelect }) {
{colors.map((color) => (
<button
key={color}
className={`notepad-swatch ${value === color ? "active" : ""}`}
className={`notepad-swatch nds-swatch ${value === color ? "active" : ""}`}
type="button"
title={`${label} ${color}`}
aria-label={`${label} ${color}`}
@ -151,7 +154,7 @@ export function DrawingWidthDropdown({ label, widths, value, onSelect }) {
closeOnMouseLeave
preventMouseDown
renderTrigger={({ toggle }) => (
<button className="drawing-width-toggle" type="button" title={label} aria-label={label} onClick={toggle}>
<button className="drawing-width-toggle nds-icon-button" data-size="sm" type="button" title={label} aria-label={label} onClick={toggle}>
<span style={{ "--line-width": `${value}px` }} />
</button>
)}
@ -161,7 +164,8 @@ export function DrawingWidthDropdown({ label, widths, value, onSelect }) {
{widths.map((lineWidth) => (
<button
key={lineWidth}
className={`drawing-width-button ${value === lineWidth ? "active" : ""}`}
className={`drawing-width-button nds-icon-button ${value === lineWidth ? "active" : ""}`}
data-size="sm"
type="button"
aria-pressed={value === lineWidth}
title={`${label} ${lineWidth}`}

View file

@ -81,10 +81,10 @@ export function CharacteristicsEditor({ equipment, textContent, editing, onUpdat
options={TRAIT_ICONS.map((icon) => ({ icon, label: icon }))}
onChange={(icon) => setDraft((current) => ({ ...current, icon }))}
/>
<input value={draft.category} onChange={(event) => setDraft((current) => ({ ...current, category: event.target.value }))} placeholder={textContent.categoryPlaceholder || "Catégorie"} />
<input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.traitNamePlaceholder || "Nom"} />
<input value={draft.value} onChange={(event) => setDraft((current) => ({ ...current, value: event.target.value }))} placeholder={textContent.traitValuePlaceholder || "Valeur"} />
<button>{textContent.addTraitButton || "Ajouter"}</button>
<input className="nds-input" data-size="sm" type="text" value={draft.category} onChange={(event) => setDraft((current) => ({ ...current, category: event.target.value }))} placeholder={textContent.categoryPlaceholder || "Catégorie"} />
<input className="nds-input" data-size="sm" type="text" value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.traitNamePlaceholder || "Nom"} />
<input className="nds-input" data-size="sm" type="text" value={draft.value} onChange={(event) => setDraft((current) => ({ ...current, value: event.target.value }))} placeholder={textContent.traitValuePlaceholder || "Valeur"} />
<button className="nds-button" data-size="sm" data-variant="primary">{textContent.addTraitButton || "Ajouter"}</button>
</form>
)}
<TraitCategoryList
@ -145,12 +145,12 @@ function TraitCategoryList({ traits, categories, collapsedCategories, reorder, t
return (
<section className={className} key={category} {...reorder.getGroupProps({ groupId: category })}>
<div className="equipment-planner-trait-category-header">
<button type="button" className="task-planner-category-drag-handle" onPointerDown={(event) => reorder.groupReorder.startDrag(event, { groupId: category })} title={textContent.reorderCategoryTitle || "Déplacer la catégorie"}>
<button type="button" className="task-planner-category-drag-handle nds-icon-button" data-size="xs" data-variant="ghost" onPointerDown={(event) => reorder.groupReorder.startDrag(event, { groupId: category })} title={textContent.reorderCategoryTitle || "Déplacer la catégorie"}>
<Icon name="drag" />
</button>
<strong>{category}</strong>
<span>{categoryTraits.length}</span>
<button type="button" className="checklist-section-collapse-button" onClick={() => onToggleCategory(category)} aria-expanded={!collapsed}>
<button type="button" className="checklist-section-collapse-button nds-icon-button" data-size="xs" onClick={() => onToggleCategory(category)} aria-expanded={!collapsed}>
<Icon name={collapsed ? "chevron-down" : "chevron-up"} />
</button>
</div>
@ -182,7 +182,7 @@ function EquipmentTraitRow({ trait, className, itemProps, editing, textContent,
<li className={className} {...itemProps}>
{editing ? (
<>
<button type="button" className="task-planner-drag-handle" onPointerDown={onStartDrag} title={textContent.reorderTraitTitle || "Déplacer"}>
<button type="button" className="task-planner-drag-handle nds-icon-button" data-size="xs" data-variant="ghost" onPointerDown={onStartDrag} title={textContent.reorderTraitTitle || "Déplacer"}>
<Icon name="drag" />
</button>
<IconChoiceDropdown
@ -191,10 +191,10 @@ function EquipmentTraitRow({ trait, className, itemProps, editing, textContent,
options={TRAIT_ICONS.map((icon) => ({ icon, label: icon }))}
onChange={(icon) => onUpdateTrait(trait.id, (current) => ({ ...current, icon }))}
/>
<input {...categoryEdit.getInputProps({ placeholder: textContent.categoryPlaceholder || "Catégorie" })} />
<input {...nameEdit.getInputProps({ placeholder: textContent.traitNamePlaceholder || "Nom" })} />
<input {...valueEdit.getInputProps({ placeholder: textContent.traitValuePlaceholder || "Valeur" })} />
<button type="button" className="task-planner-delete-button danger" onClick={() => onUpdateTrait(trait.id, () => ({ ...trait, name: "" }))} title={textContent.deleteTraitTitle || "Supprimer"}>
<input {...categoryEdit.getInputProps({ className: "nds-inline-input", type: "text", placeholder: textContent.categoryPlaceholder || "Catégorie" })} />
<input {...nameEdit.getInputProps({ className: "nds-inline-input", type: "text", placeholder: textContent.traitNamePlaceholder || "Nom" })} />
<input {...valueEdit.getInputProps({ className: "nds-inline-input", type: "text", placeholder: textContent.traitValuePlaceholder || "Valeur" })} />
<button type="button" className="task-planner-delete-button nds-icon-button" data-size="xs" data-variant="danger" onClick={() => onUpdateTrait(trait.id, () => ({ ...trait, name: "" }))} title={textContent.deleteTraitTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</>

View file

@ -30,7 +30,9 @@ export function EquipmentItem({ equipment, reorder, textContent, onUpdate, onDel
<li className={className} {...reorder.getItemProps({ itemId: equipment.id })}>
<div className="equipment-planner-line">
<button
className="task-planner-drag-handle"
className="task-planner-drag-handle nds-icon-button"
data-size="sm"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, equipment.id)}
aria-label={`${textContent.reorderEquipmentTitle || "Déplacer"} ${equipment.name}`}
@ -42,9 +44,9 @@ export function EquipmentItem({ equipment, reorder, textContent, onUpdate, onDel
<span className="equipment-planner-type-icon" aria-hidden="true">
<Icon name={equipment.icon || "shield"} />
</span>
<strong {...nameEdit.getContentEditableProps({ className: "equipment-planner-name", title: textContent.renameEquipmentTitle || "Renommer" })}>{equipment.name}</strong>
<strong {...nameEdit.getContentEditableProps({ className: "equipment-planner-name nds-inline-input", title: textContent.renameEquipmentTitle || "Renommer" })}>{equipment.name}</strong>
</div>
<label className="equipment-planner-include-switch">
<label className="equipment-planner-include-switch nds-native-switch">
<input
type="checkbox"
checked={equipment.active}
@ -55,21 +57,22 @@ export function EquipmentItem({ equipment, reorder, textContent, onUpdate, onDel
</label>
<button
type="button"
className={`task-planner-settings-button equipment-planner-row-edit-button ${detailEditing ? "active" : ""}`}
className={`task-planner-settings-button equipment-planner-row-edit-button nds-icon-button ${detailEditing ? "active" : ""}`}
data-size="sm"
onClick={() => setDetailEditing((value) => !value)}
aria-pressed={detailEditing}
title={detailEditing ? textContent.viewModeTitle || "Mode affichage" : textContent.editModeTitle || "Mode édition"}
>
<Icon name="edit" />
</button>
<button type="button" className="task-planner-delete-button danger" onClick={onDelete} aria-label={`${textContent.deleteEquipmentTitle || "Supprimer"} ${equipment.name}`} title={textContent.deleteEquipmentTitle || "Supprimer"}>
<button type="button" className="task-planner-delete-button nds-icon-button" data-size="sm" data-variant="danger" onClick={onDelete} aria-label={`${textContent.deleteEquipmentTitle || "Supprimer"} ${equipment.name}`} title={textContent.deleteEquipmentTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>
<div className="equipment-planner-details">
{detailEditing ? (
<>
<div className="equipment-planner-tabs" role="tablist" aria-label={textContent.tabsLabel || "Ajout de caractéristiques"}>
<div className="equipment-planner-tabs nds-radio-button-group" data-size="sm" role="tablist" aria-label={textContent.tabsLabel || "Ajout de caractéristiques"}>
{DETAIL_TABS.map((tab) => (
<button
key={tab}
@ -91,7 +94,7 @@ export function EquipmentItem({ equipment, reorder, textContent, onUpdate, onDel
<section className="equipment-planner-craft-tab">
<label className="equipment-planner-field">
<span>{textContent.obtainLabel || "Obtention"}</span>
<textarea {...obtainEdit.getInputProps({ placeholder: textContent.obtainPlaceholder || "Comment obtenir ou créer cet équipement" })} />
<textarea {...obtainEdit.getInputProps({ className: "nds-textarea", "data-size": "sm", placeholder: textContent.obtainPlaceholder || "Comment obtenir ou créer cet équipement" })} />
</label>
<MaterialsEditor equipment={equipment} textContent={textContent} editing onUpdate={onUpdate} />
</section>

View file

@ -23,9 +23,9 @@ export function MaterialsEditor({ equipment, textContent, editing, onUpdate }) {
<h4>{textContent.materialsTitle || "Matériaux"}</h4>
{editing && (
<form className="inline-form equipment-planner-material-form" onSubmit={addMaterial}>
<input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.materialNamePlaceholder || "Matériau"} />
<input type="number" min="1" value={draft.qty} onChange={(event) => setDraft((current) => ({ ...current, qty: event.target.value }))} aria-label={textContent.materialQtyLabel || "Quantité"} />
<button>{textContent.addMaterialButton || "Ajouter"}</button>
<input className="nds-input" data-size="sm" type="text" value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.materialNamePlaceholder || "Matériau"} />
<input className="nds-input" data-size="sm" type="number" min="1" value={draft.qty} onChange={(event) => setDraft((current) => ({ ...current, qty: event.target.value }))} aria-label={textContent.materialQtyLabel || "Quantité"} />
<button className="nds-button" data-size="sm" data-variant="primary">{textContent.addMaterialButton || "Ajouter"}</button>
</form>
)}
<ul className="equipment-planner-materials">
@ -54,9 +54,9 @@ function MaterialRow({ material, editing, textContent, onUpdate }) {
<li>
{editing ? (
<>
<input {...nameEdit.getInputProps()} />
<input type="number" min="1" value={material.qty} onChange={(event) => onUpdate((current) => ({ ...current, materials: current.materials.map((item) => item.id === material.id ? { ...item, qty: Math.max(1, Number.parseInt(event.target.value, 10) || 1) } : item) }))} />
<button type="button" className="task-planner-delete-button danger" onClick={() => onUpdate((current) => ({ ...current, materials: current.materials.filter((item) => item.id !== material.id) }))} title={textContent.deleteMaterialTitle || "Supprimer"}>
<input {...nameEdit.getInputProps({ className: "nds-inline-input", type: "text" })} />
<input className="nds-input" data-size="xs" type="number" min="1" value={material.qty} onChange={(event) => onUpdate((current) => ({ ...current, materials: current.materials.map((item) => item.id === material.id ? { ...item, qty: Math.max(1, Number.parseInt(event.target.value, 10) || 1) } : item) }))} />
<button type="button" className="task-planner-delete-button nds-icon-button" data-size="xs" data-variant="danger" onClick={() => onUpdate((current) => ({ ...current, materials: current.materials.filter((item) => item.id !== material.id) }))} title={textContent.deleteMaterialTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</>

View file

@ -109,8 +109,8 @@ export function EquipmentPlannerModule({ toolboxId, moduleId, context, editing,
options={EQUIPMENT_ICONS}
onChange={setEquipmentIcon}
/>
<input value={equipmentName} onChange={(event) => setEquipmentName(event.target.value)} placeholder={textContent.equipmentPlaceholder || "Nouvel équipement"} />
<button className="primary">{textContent.addEquipmentButton || "Ajouter"}</button>
<input className="nds-input" data-size="sm" type="text" value={equipmentName} onChange={(event) => setEquipmentName(event.target.value)} placeholder={textContent.equipmentPlaceholder || "Nouvel équipement"} />
<button className="nds-button" data-size="sm" data-variant="primary">{textContent.addEquipmentButton || "Ajouter"}</button>
</form>
</div>
)}

View file

@ -21,7 +21,9 @@ export function IconChoiceDropdown({ className = "", label, value, options, onCh
columns={Math.min(options.length, 5)}
renderTrigger={({ open, toggle }) => (
<button
className={`equipment-planner-picker-trigger ${open ? "active" : ""}`}
className={`equipment-planner-picker-trigger nds-select-trigger ${open ? "active" : ""}`}
data-size="sm"
data-variant="icon-picker"
type="button"
onClick={toggle}
aria-label={label}
@ -29,7 +31,6 @@ export function IconChoiceDropdown({ className = "", label, value, options, onCh
title={selected.label}
>
<Icon name={selected.icon} />
<Icon name="dropdown" />
</button>
)}
>
@ -63,9 +64,8 @@ export function ShapeChoiceDropdown({ label, value, onChange }) {
label={label}
columns={SOCKET_SHAPES.length}
renderTrigger={({ open, toggle }) => (
<button className={`equipment-planner-picker-trigger ${open ? "active" : ""}`} type="button" onClick={toggle} aria-label={label} aria-expanded={open} title={label}>
<button className={`equipment-planner-picker-trigger nds-select-trigger ${open ? "active" : ""}`} data-size="sm" data-variant="icon-picker" type="button" onClick={toggle} aria-label={label} aria-expanded={open} title={label}>
<SocketShapeIcon shape={value} />
<Icon name="dropdown" />
</button>
)}
>
@ -98,9 +98,8 @@ export function ColorChoiceDropdown({ label, value, onChange }) {
label={label}
columns={6}
renderTrigger={({ open, toggle }) => (
<button className={`equipment-planner-picker-trigger ${open ? "active" : ""}`} type="button" onClick={toggle} aria-label={label} aria-expanded={open} title={value}>
<button className={`equipment-planner-picker-trigger nds-select-trigger ${open ? "active" : ""}`} data-size="sm" data-variant="icon-picker" type="button" onClick={toggle} aria-label={label} aria-expanded={open} title={value}>
<span className={`equipment-planner-color-swatch color-${value}`} aria-hidden="true" />
<Icon name="dropdown" />
</button>
)}
>

View file

@ -53,10 +53,10 @@ export function SocketItemsEditor({ equipment, textContent, editing, onUpdate })
<h4>{textContent.socketItemsTitle || "Objets sertis"}</h4>
{editing && (
<form className="inline-form equipment-planner-socket-form" onSubmit={addSocketItem}>
<input value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.socketNamePlaceholder || "Joyau, materia..."} />
<input className="nds-input" data-size="sm" type="text" value={draft.name} onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.socketNamePlaceholder || "Joyau, materia..."} />
<ShapeChoiceDropdown label={textContent.socketShapeLabel || "Forme"} value={draft.shape} onChange={(shape) => setDraft((current) => ({ ...current, shape }))} />
<ColorChoiceDropdown label={textContent.socketColorLabel || "Couleur"} value={draft.color} onChange={(color) => setDraft((current) => ({ ...current, color }))} />
<button>{textContent.addSocketButton || "Ajouter"}</button>
<button className="nds-button" data-size="sm" data-variant="primary">{textContent.addSocketButton || "Ajouter"}</button>
</form>
)}
<div className="equipment-planner-socket-grid">
@ -104,8 +104,8 @@ function SocketItemEditor({ equipment, socketItem, textContent, editing, onUpdat
<SocketShapeIcon shape={socketItem.shape} color={socketItem.color} />
{editing ? (
<>
<input {...nameEdit.getInputProps()} />
<button type="button" className="task-planner-delete-button danger" onClick={onDelete} title={textContent.deleteSocketTitle || "Supprimer"}>
<input {...nameEdit.getInputProps({ className: "nds-inline-input", type: "text" })} />
<button type="button" className="task-planner-delete-button nds-icon-button" data-size="xs" data-variant="danger" onClick={onDelete} title={textContent.deleteSocketTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</>
@ -119,16 +119,16 @@ function SocketItemEditor({ equipment, socketItem, textContent, editing, onUpdat
<ShapeChoiceDropdown label={textContent.socketShapeLabel || "Forme"} value={socketItem.shape} onChange={(shape) => onUpdate((current) => ({ ...current, shape }))} />
<ColorChoiceDropdown label={textContent.socketColorLabel || "Couleur"} value={socketItem.color} onChange={(color) => onUpdate((current) => ({ ...current, color }))} />
{equipment.socketItems.length > 1 && (
<select defaultValue="" onChange={(event) => { onAddLink(event.target.value); event.target.value = ""; }} aria-label={textContent.addSocketLinkLabel || "Lier à"}>
<select className="nds-select" data-size="xs" defaultValue="" onChange={(event) => { onAddLink(event.target.value); event.target.value = ""; }} aria-label={textContent.addSocketLinkLabel || "Lier à"}>
<option value="">{textContent.addSocketLinkLabel || "Lier à"}</option>
{equipment.socketItems.filter((item) => item.id !== socketItem.id).map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
)}
</div>
<form className="inline-form equipment-planner-bonus-form" onSubmit={addBonus}>
<input value={bonusDraft.name} onChange={(event) => setBonusDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.bonusNamePlaceholder || "Bonus"} />
<input value={bonusDraft.value} onChange={(event) => setBonusDraft((current) => ({ ...current, value: event.target.value }))} placeholder={textContent.traitValuePlaceholder || "Valeur"} />
<button aria-label={textContent.addBonusButton || "Ajouter"} title={textContent.addBonusButton || "Ajouter"}>+</button>
<input className="nds-input" data-size="sm" type="text" value={bonusDraft.name} onChange={(event) => setBonusDraft((current) => ({ ...current, name: event.target.value }))} placeholder={textContent.bonusNamePlaceholder || "Bonus"} />
<input className="nds-input" data-size="sm" type="text" value={bonusDraft.value} onChange={(event) => setBonusDraft((current) => ({ ...current, value: event.target.value }))} placeholder={textContent.traitValuePlaceholder || "Valeur"} />
<button className="nds-button" data-size="sm" data-variant="primary" aria-label={textContent.addBonusButton || "Ajouter"} title={textContent.addBonusButton || "Ajouter"}>+</button>
</form>
</>
)}
@ -138,7 +138,7 @@ function SocketItemEditor({ equipment, socketItem, textContent, editing, onUpdat
<span>{bonus.category ? `${bonus.category} · ` : ""}{bonus.name}</span>
<b>{formatValue(bonus.value)}</b>
{editing && (
<button type="button" className="task-planner-delete-button danger" onClick={() => onUpdate((current) => ({ ...current, bonuses: current.bonuses.filter((item) => item.id !== bonus.id) }))} title={textContent.deleteBonusTitle || "Supprimer"}>
<button type="button" className="task-planner-delete-button nds-icon-button" data-size="xs" data-variant="danger" onClick={() => onUpdate((current) => ({ ...current, bonuses: current.bonuses.filter((item) => item.id !== bonus.id) }))} title={textContent.deleteBonusTitle || "Supprimer"}>
<Icon name="trash" />
</button>
)}

View file

@ -49,17 +49,19 @@ export function GoalTreeBranch({ nodes, parentId, selectedNodeId, editing, reord
<article className={`goal-tree-node-card ${editing ? "is-editing" : "is-viewing"} ${hasNodeActions ? "has-actions" : ""}`} data-diagram-node={node.id} onClick={selectFromCard}>
{!editing && (
<div className="goal-tree-node-hover-actions" aria-label={textContent.cardActionsLabel || "Actions de l'objectif"}>
<button type="button" onClick={() => onEditNode(node.id)} aria-label={`${textContent.editTitle || "Modifier"} ${node.title}`} title={textContent.editTitle || "Modifier"}>
<button className="nds-icon-button" data-size="tiny" type="button" onClick={() => onEditNode(node.id)} aria-label={`${textContent.editTitle || "Modifier"} ${node.title}`} title={textContent.editTitle || "Modifier"}>
<Icon name="edit" />
</button>
<button className="danger" type="button" onClick={() => onDeleteNode(node.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${node.title}`} title={textContent.deleteTitle || "Supprimer"}>
<button className="nds-icon-button" data-size="tiny" data-variant="danger" type="button" onClick={() => onDeleteNode(node.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${node.title}`} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>
)}
{editing && (
<button
className="goal-tree-drag-handle"
className="goal-tree-drag-handle nds-icon-button"
data-size="micro"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, node.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${node.title}`}
@ -109,13 +111,14 @@ export function GoalTreeBranch({ nodes, parentId, selectedNodeId, editing, reord
{hasNodeActions && (
<div className="goal-tree-node-actions">
{editing && (
<button type="button" onClick={() => onAdd(node.id)} aria-label={`${textContent.addChildTitle || "Ajouter une sous-étape"} ${node.title}`} title={textContent.addChildTitle || "Ajouter une sous-étape"}>
<button className="nds-icon-button" data-size="micro" type="button" onClick={() => onAdd(node.id)} aria-label={`${textContent.addChildTitle || "Ajouter une sous-étape"} ${node.title}`} title={textContent.addChildTitle || "Ajouter une sous-étape"}>
<Icon name="add" />
</button>
)}
{hasBranchToggle && (
<button
className="goal-tree-branch-toggle"
className="goal-tree-branch-toggle nds-icon-button"
data-size="micro"
type="button"
onClick={() => onToggleBranch(node.id)}
aria-expanded={!branchCollapsed}
@ -126,7 +129,7 @@ export function GoalTreeBranch({ nodes, parentId, selectedNodeId, editing, reord
</button>
)}
{editing && (
<button className="danger" type="button" onClick={() => onDeleteNode(node.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${node.title}`} title={textContent.deleteTitle || "Supprimer"}>
<button className="nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={() => onDeleteNode(node.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${node.title}`} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
)}

View file

@ -94,11 +94,11 @@ export function GoalTreeEditor({
)}
<div className="goal-tree-form-actions">
<button type="button" onClick={onClose}>
<button className="nds-button" data-size="sm" type="button" onClick={onClose}>
<Icon name="close" />
<span>{textContent.closeButton || "Fermer"}</span>
</button>
<button className="danger" type="button" onClick={() => onDeleteNode(selectedNode.id)}>
<button className="nds-button" data-size="sm" data-variant="danger" type="button" onClick={() => onDeleteNode(selectedNode.id)}>
<Icon name="trash" />
<span>{textContent.deleteButton || "Supprimer la branche"}</span>
</button>
@ -113,7 +113,7 @@ function GoalTreeBasePanel({ selectedNode, selectedNodeIsStep, textContent, onSe
<div className="goal-tree-form-panel">
<label>
<span>{textContent.kindLabel || "Type de l'objectif"}</span>
<select value={selectedNode.kind} onChange={(event) => onSetKind(selectedNode.id, event.target.value)}>
<select className="nds-select" data-size="sm" value={selectedNode.kind} onChange={(event) => onSetKind(selectedNode.id, event.target.value)}>
{GOAL_TREE_KINDS.map((kind) => <option key={kind} value={kind}>{textContent[`${kind}Kind`] || KIND_LABELS[kind]}</option>)}
</select>
</label>
@ -139,7 +139,7 @@ function GoalTreeBasePanel({ selectedNode, selectedNodeIsStep, textContent, onSe
{GOAL_TREE_COLORS.map((color) => (
<button
key={color}
className={`goal-tree-color-swatch ${selectedNode.color === color ? "active" : ""}`}
className={`goal-tree-color-swatch nds-swatch ${selectedNode.color === color ? "active" : ""}`}
type="button"
onClick={() => onUpdateNode(selectedNode.id, (node) => ({ ...node, color }))}
aria-pressed={selectedNode.color === color}
@ -164,20 +164,20 @@ function GoalTreeAdvancedPanel({ selectedNode, selectedNodeIsStep, selectedProgr
<div className="goal-tree-form-panel">
<label>
<span>{textContent.resetTypeLabel || "Type de reset"}</span>
<select value={selectedNode.resetType} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, resetType: event.target.value }))}>
<select className="nds-select" data-size="sm" value={selectedNode.resetType} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, resetType: event.target.value }))}>
{GOAL_TREE_RESET_TYPES.map((resetType) => <option key={resetType} value={resetType}>{textContent[`${resetType}Reset`] || RESET_LABELS[resetType]}</option>)}
</select>
</label>
{selectedNode.resetType !== "none" && (
<label>
<span>{textContent.resetTimeLabel || "Heure de reset"}</span>
<input type="time" value={selectedNode.resetTime || "00:00"} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, resetTime: event.target.value || "00:00" }))} />
<input className="nds-time" data-size="sm" type="time" value={selectedNode.resetTime || "00:00"} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, resetTime: event.target.value || "00:00" }))} />
</label>
)}
{selectedNode.resetType === "weekly" && (
<label>
<span>{textContent.weeklyResetDayLabel || "Jour de reset"}</span>
<select value={selectedNode.weeklyResetDay ?? 1} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, weeklyResetDay: Number(event.target.value) }))}>
<select className="nds-select" data-size="sm" value={selectedNode.weeklyResetDay ?? 1} onChange={(event) => onUpdateNode(selectedNode.id, (node) => ({ ...node, weeklyResetDay: Number(event.target.value) }))}>
{GOAL_TREE_WEEK_DAYS.map((day) => <option key={day.value} value={day.value}>{day.label}</option>)}
</select>
</label>
@ -199,7 +199,8 @@ function GoalTreeAdvancedPanel({ selectedNode, selectedNodeIsStep, selectedProgr
</label>
<div className="goal-tree-form-section-title">{textContent.progressSectionTitle || "Progression"}</div>
<button
className={`tool-split-scroll-toggle goal-tree-progress-mode-toggle ${selectedProgressMode === "auto" ? "active" : ""}`}
className={`tool-split-scroll-toggle goal-tree-progress-mode-toggle nds-switch ${selectedProgressMode === "auto" ? "active" : ""}`}
data-size="lg"
type="button"
onClick={() => onUpdateNode(selectedNode.id, (node) => ({ ...node, progressMode: selectedProgressMode === "auto" ? "manual" : "auto" }))}
aria-pressed={selectedProgressMode === "auto"}

View file

@ -22,13 +22,13 @@ export function GoalTreePrerequisitePicker({ nodes, node, textContent, onToggle
<div className="goal-tree-prerequisite-list themed-scrollbar">
{primaryParent && (
<label className="goal-tree-prerequisite-item is-primary">
<input type="checkbox" checked disabled readOnly />
<input className="nds-checkbox" data-size="xs" type="checkbox" checked disabled readOnly />
<span>{primaryParent.title}</span>
</label>
)}
{options.map((candidate) => (
<label className="goal-tree-prerequisite-item" key={candidate.id}>
<input type="checkbox" checked={parentIds.has(candidate.id)} onChange={() => onToggle(candidate.id)} />
<input className="nds-checkbox" data-size="xs" type="checkbox" checked={parentIds.has(candidate.id)} onChange={() => onToggle(candidate.id)} />
<span>{candidate.title}</span>
</label>
))}
@ -41,6 +41,7 @@ export function GoalTreeTitleInput({ value, onChange }) {
const [draft, setDraft] = useState(value || "");
return (
<input
type="text"
value={draft}
onChange={(event) => {
const title = event.target.value;
@ -54,7 +55,7 @@ export function GoalTreeTitleInput({ value, onChange }) {
export function GoalTreeFormulaInput({ value, onChange, hasParent, textContent, placeholder, title, className = "", "aria-label": ariaLabel }) {
const normalizedValue = String(value ?? "");
if (!hasParent) {
return <input aria-label={ariaLabel} className={className} inputMode="decimal" onChange={(event) => onChange(event.target.value.replaceAll(",", "."))} placeholder={placeholder} title={title} value={normalizedValue} />;
return <input type="text" aria-label={ariaLabel} className={`${className} nds-inline-input`.trim()} inputMode="decimal" onChange={(event) => onChange(event.target.value.replaceAll(",", "."))} placeholder={placeholder} title={title} value={normalizedValue} />;
}
return (
@ -87,7 +88,7 @@ export function GoalTreeContentEditor({ node, textContent, onAdd, onUpdate, onDe
<div className="goal-tree-form-section-title">{textContent.contentSectionTitle || "Contenu"}</div>
{(node.items || []).map((item) => (
<div className={`goal-tree-content-editor-row is-${node.kind}`} key={item.id}>
<input className="goal-tree-content-label-input" value={item.label} placeholder={textContent.itemLabelPlaceholder || "Élément"} onChange={(event) => onUpdate(item.id, (current) => ({ ...current, label: event.target.value }))} aria-label={textContent.itemLabelLabel || "Libellé"} />
<input className="goal-tree-content-label-input nds-input" data-size="sm" type="text" value={item.label} placeholder={textContent.itemLabelPlaceholder || "Élément"} onChange={(event) => onUpdate(item.id, (current) => ({ ...current, label: event.target.value }))} aria-label={textContent.itemLabelLabel || "Libellé"} />
{node.kind === "checklist" && (
<GoalTreeFormulaInput
aria-label={textContent.itemTotalLabel || "Total"}
@ -100,12 +101,12 @@ export function GoalTreeContentEditor({ node, textContent, onAdd, onUpdate, onDe
title={textContent.itemTotalHelp || "Saisissez un nombre ou une formule avec base pour référencer la cible du parent."}
/>
)}
<button className="danger" type="button" onClick={() => onDelete(item.id)} aria-label={textContent.deleteTitle || "Supprimer"} title={textContent.deleteTitle || "Supprimer"}>
<button className="nds-icon-button" data-size="xs" data-variant="danger" type="button" onClick={() => onDelete(item.id)} aria-label={textContent.deleteTitle || "Supprimer"} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>
))}
<button className="goal-tree-content-add-button" type="button" onClick={onAdd}>
<button className="goal-tree-content-add-button nds-button" data-size="xs" data-variant="dashed" type="button" onClick={onAdd}>
<Icon name="add" />
<span>{textContent.addContentItemButton || "Ajouter une ligne"}</span>
</button>
@ -122,12 +123,9 @@ export function GoalTreeIconPicker({ label, value, textContent, onChange }) {
menuClassName="goal-tree-icon-picker-menu"
label={label}
renderTrigger={({ open, toggle }) => (
<button className="goal-tree-icon-picker-button" type="button" onClick={toggle} aria-label={label} aria-expanded={open} title={label}>
<span className="goal-tree-icon-preview" aria-hidden="true">
<Icon name={value} />
</span>
<button className="goal-tree-icon-picker-button nds-select-trigger" data-size="sm" data-variant="icon-picker" type="button" onClick={toggle} aria-label={label} aria-expanded={open} title={label}>
<Icon name={value} />
<span>{textContent[`icon${value}`] || value}</span>
<Icon name="dropdown" />
</button>
)}
>
@ -136,7 +134,8 @@ export function GoalTreeIconPicker({ label, value, textContent, onChange }) {
{GOAL_TREE_ICON_NAMES.map((icon) => (
<button
key={icon}
className={icon === value ? "active" : ""}
className={`nds-icon-button ${icon === value ? "active" : ""}`.trim()}
data-size="sm"
type="button"
onClick={() => {
onChange(icon);

View file

@ -7,10 +7,10 @@ function GoalTreeChecklistInlineControl({ item, textContent, onUpdate }) {
const target = Math.max(1, item.qtyTarget || 1);
const current = Math.min(Math.max(0, item.qtyCurrent || 0), target);
if (target <= 1) {
return <input className="goal-tree-node-check" type="checkbox" checked={current >= 1} onChange={(event) => onUpdate((entry) => ({ ...entry, qtyCurrent: event.target.checked ? 1 : 0 }))} aria-label={textContent.itemCheckedLabel || "État"} />;
return <input className="goal-tree-node-check nds-checkbox" data-size="xs" type="checkbox" checked={current >= 1} onChange={(event) => onUpdate((entry) => ({ ...entry, qtyCurrent: event.target.checked ? 1 : 0 }))} aria-label={textContent.itemCheckedLabel || "État"} />;
}
return (
<span className="goal-tree-content-qty-stepper">
<span className="goal-tree-content-qty-stepper nds-quantity-stepper" data-size="sm">
<button type="button" onClick={() => onUpdate((entry) => ({ ...entry, qtyCurrent: Math.max(0, (entry.qtyCurrent || 0) - 1) }))} aria-label={textContent.decrementItemTitle || "Retirer"} title={textContent.decrementItemTitle || "Retirer"}>
<span aria-hidden="true">-</span>
</button>
@ -26,10 +26,10 @@ export function GoalTreeStepLeafControl({ node, textContent, onChange }) {
const target = Math.max(1, node.computedQtyTarget || node.qtyTarget || 1);
const current = Math.min(Math.max(0, node.qtyCurrent || 0), target);
if (target <= 1) {
return <input className="goal-tree-step-check" type="checkbox" checked={current >= 1} onChange={(event) => onChange(event.target.checked ? 1 : 0)} aria-label={textContent.itemCheckedLabel || "État"} />;
return <input className="goal-tree-step-check nds-checkbox" data-size="xs" type="checkbox" checked={current >= 1} onChange={(event) => onChange(event.target.checked ? 1 : 0)} aria-label={textContent.itemCheckedLabel || "État"} />;
}
return (
<span className="goal-tree-step-qty-stepper goal-tree-content-qty-stepper">
<span className="goal-tree-step-qty-stepper goal-tree-content-qty-stepper nds-quantity-stepper" data-size="xs">
<button type="button" onClick={() => onChange(Math.max(0, current - 1))} aria-label={textContent.decrementItemTitle || "Retirer"} title={textContent.decrementItemTitle || "Retirer"}>
<span aria-hidden="true">-</span>
</button>

View file

@ -87,7 +87,8 @@ export function GoalTreeView({
return (
<section className={`goal-tree-view tool-split-results ${verticalView ? "is-vertical" : "is-horizontal"} ${editing ? "is-editing" : ""}`} aria-label={textContent.treeTitle || "Arbre d'objectifs"}>
<button
className={`module-scroll-button goal-tree-orientation-switch ${verticalView ? "active" : ""}`}
className={`module-scroll-button goal-tree-orientation-switch nds-switch ${verticalView ? "active" : ""}`}
data-size="lg"
type="button"
onClick={onToggleOrientation}
aria-label={verticalView ? textContent.horizontalViewTitle || "Afficher horizontalement" : textContent.verticalViewTitle || "Afficher verticalement"}
@ -100,7 +101,7 @@ export function GoalTreeView({
<div className="goal-tree-scroll-area themed-scrollbar">
<div className="goal-tree-view-controls">
{editing && (
<button className="goal-tree-add-root-button" type="button" onClick={() => onAdd("")}>
<button className="goal-tree-add-root-button nds-button" data-size="sm" data-variant="dashed" type="button" onClick={() => onAdd("")}>
<Icon name="add" />
<span>{textContent.addRootSchemaButton || "Ajouter un objectif a la racine"}</span>
</button>

View file

@ -123,7 +123,8 @@ export function ImageAnnotationModule({ toolboxId, moduleId, context, editing, t
<img src={data.image} alt={textContent.imageAlt || "Image annotée"} draggable="false" />
<div className="annotation-preview-actions">
<button
className="tool-split-action-button"
className="tool-split-action-button nds-icon-button"
data-size="sm"
type="button"
onClick={() => context.setImage({
dataUrl: data.image,
@ -164,7 +165,7 @@ export function ImageAnnotationModule({ toolboxId, moduleId, context, editing, t
<Icon name="zoom" />
</button>
{editing && (
<button className="tool-split-action-button danger" type="button" onClick={removeImage} aria-label={textContent.deleteImageTitle || "Supprimer l'image"} title={textContent.deleteImageTitle || "Supprimer l'image"}>
<button className="tool-split-action-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={removeImage} aria-label={textContent.deleteImageTitle || "Supprimer l'image"} title={textContent.deleteImageTitle || "Supprimer l'image"}>
<Icon name="trash" />
</button>
)}

View file

@ -104,7 +104,9 @@ export function ImagesModule({ toolboxId, moduleId, context, editing, textExchan
return (
<figure key={image.id} className={className} {...getItemProps({ itemId: image.id, parentId: moduleId })}>
<button
className="image-drag-handle"
className="image-drag-handle nds-icon-button"
data-size="sm"
data-variant="ghost"
type="button"
onPointerDown={(event) => itemReorder.startDrag(event, image.id)}
aria-label={textContent.reorderAriaLabel || "Déplacer l'image"}
@ -117,7 +119,8 @@ export function ImagesModule({ toolboxId, moduleId, context, editing, textExchan
</button>
{editing ? (
<input
className="image-label-input"
className="image-label-input nds-inline-input"
type="text"
value={image.label || ""}
placeholder={textContent.labelPlaceholder || "Libellé de l'image"}
onChange={(event) => updateImage(image.id, { label: event.target.value })}
@ -128,7 +131,8 @@ export function ImagesModule({ toolboxId, moduleId, context, editing, textExchan
) : null}
<div className="image-actions">
<button
className="image-annotate-button"
className="image-annotate-button nds-icon-button"
data-size="sm"
type="button"
onClick={() => context.createImageAnnotationModule?.(image.dataUrl)}
aria-label={textContent.annotateAriaLabel || "Annoter l'image"}
@ -136,7 +140,7 @@ export function ImagesModule({ toolboxId, moduleId, context, editing, textExchan
>
<Icon name="map" />
</button>
<button className="image-delete-button danger" type="button" onClick={() => {
<button className="image-delete-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={() => {
context.setModuleData(toolboxId, moduleId, { images: data.images.filter((item) => item.id !== image.id) });
}} aria-label={textContent.deleteAriaLabel || "Supprimer l'image"} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />

View file

@ -59,9 +59,9 @@ export function LinksModule({ toolboxId, moduleId, context, editing, textExchang
{editing && (
<div className="module-add-panel">
<form className="inline-form links-add-form" onSubmit={addLink}>
<input {...linkDraft.getFieldProps("title", { placeholder: textContent.titlePlaceholder || "Nom du lien" })} />
<input {...linkDraft.getFieldProps("url", { placeholder: textContent.urlPlaceholder || "https://..." })} />
<button className="primary">{textContent.addButton || "Ajouter"}</button>
<input {...linkDraft.getFieldProps("title", { className: "nds-input", "data-size": "sm", type: "text", placeholder: textContent.titlePlaceholder || "Nom du lien" })} />
<input {...linkDraft.getFieldProps("url", { className: "nds-input", "data-size": "sm", type: "url", placeholder: textContent.urlPlaceholder || "https://..." })} />
<button className="nds-button" data-size="sm" data-variant="primary">{textContent.addButton || "Ajouter"}</button>
</form>
</div>
)}
@ -82,7 +82,9 @@ export function LinksModule({ toolboxId, moduleId, context, editing, textExchang
return (
<li className={className} key={link.id} {...getItemProps({ itemId: link.id, parentId: moduleId })}>
<button
className="link-drag-handle"
className="link-drag-handle nds-icon-button"
data-size="sm"
data-variant="ghost"
type="button"
onPointerDown={(event) => itemReorder.startDrag(event, link.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${link.title || link.url}`}
@ -95,10 +97,10 @@ export function LinksModule({ toolboxId, moduleId, context, editing, textExchang
<span>{link.url}</span>
</a>
<div>
<button type="button" onClick={() => copyUrl(link)} aria-label={`${textContent.copyTitle || "Copier"} ${link.title || link.url}`} title={copiedId === link.id ? textContent.copiedTitle || "Copié" : textContent.copyTitle || "Copier"}>
<button type="button" className="nds-icon-button" data-size="sm" onClick={() => copyUrl(link)} aria-label={`${textContent.copyTitle || "Copier"} ${link.title || link.url}`} title={copiedId === link.id ? textContent.copiedTitle || "Copié" : textContent.copyTitle || "Copier"}>
<Icon name="copy" />
</button>
<button type="button" className="danger" onClick={() => save(data.links.filter((item) => item.id !== link.id))} aria-label={`${textContent.deleteTitle || "Supprimer"} ${link.title || link.url}`} title={textContent.deleteTitle || "Supprimer"}>
<button type="button" className="nds-icon-button" data-size="sm" data-variant="danger" onClick={() => save(data.links.filter((item) => item.id !== link.id))} aria-label={`${textContent.deleteTitle || "Supprimer"} ${link.title || link.url}`} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>

View file

@ -40,7 +40,7 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
function resizeMemoTextarea(element) {
if (!element) return;
element.style.height = "auto";
element.style.height = `${element.scrollHeight}px`;
element.style.height = `${element.scrollHeight + 2}px`;
}
function save(nextData) {
@ -93,7 +93,7 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
{editing && (
<div className="module-add-panel">
<button className="primary memos-add-button" type="button" onClick={addMemo}>
<button className="memos-add-button nds-button" data-size="sm" data-variant="primary" type="button" onClick={addMemo}>
<Icon name="add" />
<span>{textContent.addButton || "Ajouter"}</span>
</button>
@ -102,7 +102,8 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
<div className="memos-toolbar">
<button
className={`memos-ticker-toggle ${data.tickerEnabled ? "active" : ""}`}
className={`memos-ticker-toggle nds-icon-button ${data.tickerEnabled ? "active" : ""}`}
data-size="sm"
type="button"
onClick={() => save({ ...data, tickerEnabled: !data.tickerEnabled })}
aria-pressed={data.tickerEnabled}
@ -135,7 +136,9 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
return (
<article className={className} key={memo.id} style={{ "--memo-color": memo.color }} {...getItemProps({ itemId: memo.id, parentId: moduleId })}>
<button
className="memo-drag-handle"
className="memo-drag-handle nds-icon-button"
data-size="sm"
data-variant="ghost"
type="button"
onPointerDown={(event) => itemReorder.startDrag(event, memo.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${memo.text}`}
@ -144,6 +147,9 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
<Icon name="drag" />
</button>
<textarea
className="nds-textarea"
data-size="sm"
data-variant="ghost"
ref={(element) => {
if (element) {
textareaRefs.current.set(memo.id, element);
@ -169,7 +175,7 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
label={textContent.colorLabel || "Couleur du mémo"}
renderTrigger={({ toggle }) => (
<button
className="notepad-color-toggle"
className="notepad-color-toggle nds-swatch"
type="button"
style={{ "--swatch-color": memo.color }}
onClick={toggle}
@ -183,7 +189,7 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
{TEXT_COLORS.map((color) => (
<button
key={color}
className={`notepad-swatch ${memo.color === color ? "active" : ""}`}
className={`notepad-swatch nds-swatch ${memo.color === color ? "active" : ""}`}
type="button"
style={{ "--swatch-color": color }}
onClick={() => {
@ -196,14 +202,14 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
</>
)}
</CompactDropdown>
<button className="memo-delete-button danger" type="button" onClick={() => deleteMemo(memo.id)} aria-label={textContent.deleteTitle || "Supprimer le mémo"} title={textContent.deleteTitle || "Supprimer"}>
<button className="memo-delete-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={() => deleteMemo(memo.id)} aria-label={textContent.deleteTitle || "Supprimer le mémo"} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</footer>
</article>
);
}) : (
<button className="memos-empty" type="button" onClick={addMemo}>
<button className="memos-empty nds-button" data-size="sm" data-variant="dashed" type="button" onClick={addMemo}>
<Icon name="add" />
<span>{textContent.emptyLabel || "Ajouter un premier mémo"}</span>
</button>

View file

@ -295,7 +295,11 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
label={textContent.pagesMenuLabel || "Pages du bloc note"}
renderTrigger={({ open, toggle }) => (
<button
className={`notepad-pages-toggle ${open ? "active" : ""}`}
className={`notepad-pages-toggle nds-icon-button ${open ? "active" : ""}`}
data-size="sm"
data-variant="panel-toggle"
data-edge="right"
data-border="joined"
type="button"
onClick={toggle}
aria-label={textContent.pagesToggleLabel || "Afficher les pages du bloc note"}
@ -311,10 +315,12 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
<div className="notepad-pages-list">
{normalizedData.pages.map((page) => (
<div className={`notepad-page-row ${page.id === normalizedData.activePageId ? "active" : ""}`} key={page.id}>
<div className="notepad-page-main">
<div className="notepad-page-main nds-list-row">
{editingPageId === page.id ? (
<input
className="notepad-page-title-input"
className="notepad-page-title-input nds-input"
data-size="xs"
type="text"
value={pageTitleDrafts[page.id] ?? page.title}
aria-label={`${textContent.renamePageLabel || "Renommer"} ${page.title}`}
autoFocus
@ -339,7 +345,8 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
</div>
<div className="notepad-page-actions">
<button
className="notepad-page-edit"
className="notepad-page-edit nds-icon-button"
data-size="xxs"
type="button"
onClick={() => {
setPageTitleDrafts((current) => ({ ...current, [page.id]: page.title }));
@ -351,7 +358,9 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
<Icon name="edit" />
</button>
<button
className="notepad-page-delete danger"
className="notepad-page-delete nds-icon-button"
data-size="xxs"
data-variant="danger"
type="button"
onClick={() => deletePage(page.id, close)}
disabled={normalizedData.pages.length <= 1}
@ -364,7 +373,7 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
</div>
))}
</div>
<button className="notepad-page-add" type="button" onClick={() => addPage(close)}>
<button className="notepad-page-add nds-button" data-size="xs" data-variant="dashed" type="button" onClick={() => addPage(close)}>
<Icon name="add" />
<span>{textContent.addPageLabel || "Ajouter une page"}</span>
</button>

View file

@ -52,7 +52,8 @@ export function NotepadToolbar({
function ToolbarButton({ active, title, children, onClick }) {
return (
<button
className={`notepad-toolbar-button ${active ? "active" : ""}`}
className={`notepad-toolbar-button nds-icon-button ${active ? "active" : ""}`}
data-size="sm"
type="button"
aria-pressed={active}
title={title}
@ -74,7 +75,8 @@ function ListTypeDropdown({ textContent, activeFormats, onCommand }) {
preventMouseDown
renderTrigger={({ toggle }) => (
<button
className={`notepad-toolbar-button ${activeFormats.bulletList || activeFormats.numberList ? "active" : ""}`}
className={`notepad-toolbar-button nds-icon-button ${activeFormats.bulletList || activeFormats.numberList ? "active" : ""}`}
data-size="sm"
type="button"
aria-pressed={activeFormats.bulletList || activeFormats.numberList}
title={textContent.listGroupLabel || "Listes"}
@ -87,7 +89,8 @@ function ListTypeDropdown({ textContent, activeFormats, onCommand }) {
{({ close }) => (
<>
<button
className={`notepad-list-option ${activeFormats.bulletList ? "active" : ""}`}
className={`notepad-list-option nds-icon-button ${activeFormats.bulletList ? "active" : ""}`}
data-size="sm"
type="button"
aria-pressed={activeFormats.bulletList}
onClick={() => {
@ -98,7 +101,8 @@ function ListTypeDropdown({ textContent, activeFormats, onCommand }) {
•
</button>
<button
className={`notepad-list-option ${activeFormats.numberList ? "active" : ""}`}
className={`notepad-list-option nds-icon-button ${activeFormats.numberList ? "active" : ""}`}
data-size="sm"
type="button"
aria-pressed={activeFormats.numberList}
onClick={() => {

View file

@ -33,6 +33,7 @@ export function TableRow({ rowIndex, columns, cells, evaluatedCells, editingCell
return (
<input
type="text"
key={address}
ref={(element) => registerInput(address, element)}
className={className}
@ -82,6 +83,7 @@ export function HeaderLabelInput({ className, value, fallback, onSave, ...props
<input
{...inlineEdit.getInputProps({
...props,
type: "text",
className,
onFocus: (event) => {
if (value === fallback) window.requestAnimationFrame(() => event.target.select());

View file

@ -8,7 +8,8 @@ export function TableToolbar({ rows, columns, textContent, onResize, onCopy }) {
<div className="notepad-format-controls table-controls">
<div className="notepad-toolbar-group" role="group" aria-label={textContent.rowActionsLabel || "Lignes"}>
<button
className="notepad-toolbar-button"
className="notepad-toolbar-button nds-icon-button"
data-size="sm"
type="button"
onClick={() => onResize(Math.min(MAX_ROWS, rows + 1), columns)}
disabled={rows >= MAX_ROWS}
@ -18,7 +19,9 @@ export function TableToolbar({ rows, columns, textContent, onResize, onCopy }) {
<Icon name="add-row" />
</button>
<button
className="notepad-toolbar-button danger"
className="notepad-toolbar-button nds-icon-button"
data-size="sm"
data-variant="danger"
type="button"
onClick={() => onResize(Math.max(1, rows - 1), columns)}
disabled={rows <= 1}
@ -30,7 +33,8 @@ export function TableToolbar({ rows, columns, textContent, onResize, onCopy }) {
</div>
<div className="notepad-toolbar-group" role="group" aria-label={textContent.columnActionsLabel || "Colonnes"}>
<button
className="notepad-toolbar-button"
className="notepad-toolbar-button nds-icon-button"
data-size="sm"
type="button"
onClick={() => onResize(rows, Math.min(MAX_COLUMNS, columns + 1))}
disabled={columns >= MAX_COLUMNS}
@ -40,7 +44,9 @@ export function TableToolbar({ rows, columns, textContent, onResize, onCopy }) {
<Icon name="add-column" />
</button>
<button
className="notepad-toolbar-button danger"
className="notepad-toolbar-button nds-icon-button"
data-size="sm"
data-variant="danger"
type="button"
onClick={() => onResize(rows, Math.max(1, columns - 1))}
disabled={columns <= 1}
@ -56,7 +62,8 @@ export function TableToolbar({ rows, columns, textContent, onResize, onCopy }) {
</div>
<div className="notepad-toolbar-group table-copy-group">
<button
className="notepad-toolbar-button table-copy-button"
className="notepad-toolbar-button table-copy-button nds-button"
data-size="sm"
type="button"
onClick={onCopy}
aria-label={textContent.copyTitle || "Copier les valeurs"}

View file

@ -78,6 +78,8 @@ export function TaskPlannerBranch({
<li className="task-planner-hidden-parent" key={`${task.id}:visible-children`}>
<div className="task-planner-hidden-parent-line">
<input
className="nds-checkbox"
data-size="sm"
type="checkbox"
checked={task.checked}
onChange={(event) => onUpdateTask(task.id, (current) => ({
@ -158,7 +160,9 @@ export function TaskPlannerBranch({
<div className="checklist-section-header task-planner-category-header">
<div className="task-planner-category-title">
<button
className="task-planner-category-drag-handle"
className="task-planner-category-drag-handle nds-icon-button"
data-size="xs"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.groupReorder.startDrag(event, { groupId: group.category, parentId })}
aria-label={`${textContent.categoryReorderTitle || "Déplacer la catégorie"} ${group.category}`}
@ -171,7 +175,8 @@ export function TaskPlannerBranch({
<div className="task-planner-category-actions">
<span>{groupCompletedCount} / {groupCount}</span>
<button
className="checklist-section-collapse-button"
className="checklist-section-collapse-button nds-icon-button"
data-size="xs"
type="button"
onClick={() => onToggleCategoryCollapsed(group.category)}
aria-expanded={!isCollapsed}

View file

@ -65,7 +65,9 @@ export function TaskPlannerItem({
<li className={className} {...reorder.getItemProps({ itemId: task.id, groupId: getTaskCategoryDataset(task, tasks, parentMap), parentId })}>
<div className="task-planner-line">
<button
className="task-planner-drag-handle"
className="task-planner-drag-handle nds-icon-button"
data-size="xs"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, task.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${task.title}`}
@ -74,6 +76,8 @@ export function TaskPlannerItem({
<Icon name="drag" />
</button>
<input
className="nds-checkbox"
data-size="sm"
type="checkbox"
checked={task.checked}
onChange={updateChecked}
@ -81,12 +85,14 @@ export function TaskPlannerItem({
/>
<input
{...titleEdit.getInputProps({
className: "tool-split-entry-label task-planner-title-input",
className: "tool-split-entry-label task-planner-title-input nds-inline-input",
type: "text",
"aria-label": textContent.titleLabel || "Titre"
})}
/>
<select
className="task-planner-type-select"
className="task-planner-type-select nds-select"
data-size="xs"
value={task.type}
onChange={(event) => onUpdateTask(task.id, (current) => {
const nextType = event.target.value;
@ -104,7 +110,8 @@ export function TaskPlannerItem({
)}
<div className="task-planner-actions">
<button
className={`task-planner-dropdown-button ${descriptionOpen ? "active" : ""}`}
className={`task-planner-dropdown-button nds-icon-button ${descriptionOpen ? "active" : ""}`}
data-size="xs"
type="button"
onClick={onToggleDescription}
aria-expanded={descriptionOpen}
@ -114,7 +121,8 @@ export function TaskPlannerItem({
<Icon name={descriptionOpen ? "chevron-up" : "chevron-down"} />
</button>
<button
className={`task-planner-settings-button ${settingsOpen ? "active" : ""}`}
className={`task-planner-settings-button nds-icon-button ${settingsOpen ? "active" : ""}`}
data-size="xs"
type="button"
onClick={onToggleSettings}
aria-expanded={settingsOpen}
@ -123,7 +131,7 @@ export function TaskPlannerItem({
>
<Icon name="settings" />
</button>
<button className="task-planner-delete-button danger" type="button" onClick={() => onDeleteTask(task.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${task.title}`} title={textContent.deleteTitle || "Supprimer"}>
<button className="task-planner-delete-button nds-icon-button" data-size="xs" data-variant="danger" type="button" onClick={() => onDeleteTask(task.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${task.title}`} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>

View file

@ -110,7 +110,9 @@ function TaskDescriptionPanel({ task, tasks, parentMap, parentId, textContent, o
<span>{textContent.categoryLabel || "Catégorie"}</span>
<input
{...categoryEdit.getInputProps({
className: "nds-inline-input",
disabled: !categoryEditable,
type: "text",
placeholder: textContent.categoryPlaceholder || "Nom de catégorie",
"aria-label": `${textContent.categoryLabel || "Catégorie"} ${task.title}`
})}
@ -120,7 +122,8 @@ function TaskDescriptionPanel({ task, tasks, parentMap, parentId, textContent, o
<span>{textContent.descriptionLabel || "Description"}</span>
<textarea
{...descriptionEdit.getInputProps({
className: "task-planner-description",
className: "task-planner-description nds-textarea",
"data-size": "sm",
placeholder: textContent.descriptionPlaceholder || "Description",
"aria-label": `${textContent.descriptionLabel || "Description"} ${task.title}`
})}
@ -138,6 +141,8 @@ function TaskResetSettings({ task, data, textContent, onUpdateTask }) {
<span>{textContent.taskDailyResetTimeLabel || "Heure quotidienne"}</span>
<div className="task-planner-reset-control">
<input
className="nds-time"
data-size="xs"
type="time"
value={task.dailyResetTime || data.resetTime}
onChange={(event) => onUpdateTask(task.id, (current) => {
@ -149,6 +154,8 @@ function TaskResetSettings({ task, data, textContent, onUpdateTask }) {
aria-label={`${textContent.taskDailyResetTimeLabel || "Heure quotidienne"} ${task.title}`}
/>
<button
className="task-planner-reset-button nds-icon-button"
data-size="xs"
type="button"
onClick={() => onUpdateTask(task.id, (current) => {
const nextTask = { ...current };
@ -171,6 +178,8 @@ function TaskResetSettings({ task, data, textContent, onUpdateTask }) {
<label className="task-planner-reset-override">
<span>{textContent.taskWeeklyResetDayLabel || "Jour hebdo"}</span>
<select
className="nds-select"
data-size="xs"
value={task.weeklyResetDay ?? ""}
onChange={(event) => onUpdateTask(task.id, (current) => {
const nextTask = { ...current };
@ -212,6 +221,8 @@ function LinksEditor({ label, relations, task, tasks, parentMap, relationTargets
<div className="task-planner-relations">
<span>{label}</span>
<select
className="nds-select"
data-size="sm"
value=""
onChange={(event) => {
onAddLinkTarget(task.id, event.target.value);
@ -228,6 +239,8 @@ function LinksEditor({ label, relations, task, tasks, parentMap, relationTargets
<span>{getTaskTitle(tasks, relation.toTaskId)}</span>
<label title={prerequisiteLabel}>
<input
className="nds-checkbox"
data-size="sm"
type="checkbox"
checked={relation.prerequisite}
onChange={(event) => onSetPrerequisite(task.id, relation.toTaskId, event.target.checked)}
@ -235,7 +248,7 @@ function LinksEditor({ label, relations, task, tasks, parentMap, relationTargets
/>
<span>{prerequisiteLabel}</span>
</label>
<button type="button" onClick={() => onRemoveLinkTarget(task.id, relation.toTaskId)} title={removeLabel} aria-label={`${removeLabel} ${getTaskTitle(tasks, relation.toTaskId)}`}>
<button className="nds-icon-button" data-size="xs" data-variant="danger" type="button" onClick={() => onRemoveLinkTarget(task.id, relation.toTaskId)} title={removeLabel} aria-label={`${removeLabel} ${getTaskTitle(tasks, relation.toTaskId)}`}>
<Icon name="close" />
</button>
</div>

View file

@ -7,17 +7,19 @@ export function TaskPlannerSettings({ data, textContent, onWeeklyResetDayChange,
<section className="task-planner-settings" aria-label={textContent.settingsTitle || "Réglages du planificateur"}>
<label>
<span>{textContent.globalWeeklyResetDayLabel || "Reset hebdo"}</span>
<select value={data.weeklyResetDay} onChange={(event) => onWeeklyResetDayChange(Number(event.target.value))}>
<select className="nds-select" data-size="sm" value={data.weeklyResetDay} onChange={(event) => onWeeklyResetDayChange(Number(event.target.value))}>
{WEEK_DAYS.map((day) => <option key={day.value} value={day.value}>{day.label}</option>)}
</select>
</label>
<label>
<span>{textContent.resetTimeLabel || "Heure de reset"}</span>
<input type="time" value={data.resetTime} onChange={(event) => onResetTimeChange(event.target.value || "00:00")} />
<input className="nds-time" data-size="sm" type="time" value={data.resetTime} onChange={(event) => onResetTimeChange(event.target.value || "00:00")} />
</label>
<div className="task-planner-settings-actions">
<button
className="task-planner-clear-completed-button danger"
className="task-planner-clear-completed-button nds-button"
data-size="sm"
data-variant="danger"
type="button"
onClick={onClearCompletedUniqueTasks}
disabled={!data.tasks.some((task) => task.type === "unique" && task.checked)}

View file

@ -23,7 +23,8 @@ export function TaskPlannerTopbar({
<div className="task-planner-topbar">
<div className="task-planner-global-controls">
<button
className={`tool-split-scroll-toggle task-planner-hide-completed-toggle ${data.hideCompleted ? "active" : ""}`}
className={`tool-split-scroll-toggle task-planner-hide-completed-toggle nds-switch ${data.hideCompleted ? "active" : ""}`}
data-size="lg"
type="button"
onClick={onHideCompletedToggle}
aria-pressed={data.hideCompleted}
@ -34,7 +35,8 @@ export function TaskPlannerTopbar({
<i aria-hidden="true" />
</button>
<button
className={`task-planner-settings-toggle ${settingsOpen ? "active" : ""}`}
className={`task-planner-settings-toggle nds-button ${settingsOpen ? "active" : ""}`}
data-size="sm"
type="button"
onClick={onSettingsToggle}
aria-expanded={settingsOpen}
@ -48,11 +50,11 @@ export function TaskPlannerTopbar({
{editing && (
<div className="task-planner-add-stack">
<form className="inline-form task-planner-add-form" onSubmit={onAddTask}>
<input {...taskDraft.getFieldProps("title", { placeholder: textContent.titlePlaceholder || "Nouvelle tâche" })} />
<select {...taskDraft.getFieldProps("type", { "aria-label": textContent.typeLabel || "Type" })}>
<input {...taskDraft.getFieldProps("title", { className: "nds-input", "data-size": "sm", type: "text", placeholder: textContent.titlePlaceholder || "Nouvelle tâche" })} />
<select {...taskDraft.getFieldProps("type", { className: "nds-select", "data-size": "sm", "aria-label": textContent.typeLabel || "Type" })}>
{TASK_TYPES.map((taskType) => <option key={taskType} value={taskType}>{getTaskTypeLabel(taskType, textContent)}</option>)}
</select>
<button className="primary">{textContent.addButton || "Ajouter"}</button>
<button className="nds-button" data-size="sm" data-variant="primary">{textContent.addButton || "Ajouter"}</button>
</form>
</div>
)}

View file

@ -9,11 +9,11 @@ export function TextExchangeActions({ type, data, context, onImport, textContent
return (
<>
<div className="text-import-actions">
<button type="button" onClick={exchange.openImport}>
<button className="nds-button" data-size="sm" type="button" onClick={exchange.openImport}>
<Icon name="import" />
<span>{textContent.importOpenButton || "Importer du texte"}</span>
</button>
<button type="button" onClick={exchange.openExport} disabled={!exchange.canExport}>
<button className="nds-button" data-size="sm" type="button" onClick={exchange.openExport} disabled={!exchange.canExport}>
<Icon name="export" />
<span>{textContent.exportOpenButton || exportLabel || "Exporter en format texte"}</span>
</button>

View file

@ -24,14 +24,15 @@ export function TextImportModal({ title, value, placeholder, submitLabel, readOn
<section className="confirm-modal text-import-modal-dialog" role="dialog" aria-modal="true" aria-labelledby="text-import-title">
<header>
<h2 id="text-import-title">{title}</h2>
<button className="drawer-close-button" type="button" onClick={onClose} aria-label="Fermer" title="Fermer">
<button className="drawer-close-button nds-icon-button" data-size="sm" type="button" onClick={onClose} aria-label="Fermer" title="Fermer">
<Icon name="close" />
</button>
</header>
<form className="text-import-modal-form" onSubmit={onSubmit}>
<textarea
autoFocus
className={error ? "has-error" : ""}
className={`nds-textarea ${error ? "has-error" : ""}`.trim()}
data-size="sm"
readOnly={readOnly}
value={value}
onChange={(event) => onChange?.(event.target.value)}
@ -53,11 +54,11 @@ export function TextImportModal({ title, value, placeholder, submitLabel, readOn
</p>
)}
<footer>
<button type="button" onClick={onClose}>Annuler</button>
<button className="nds-button" data-size="sm" type="button" onClick={onClose}>Annuler</button>
{readOnly ? (
<button className="primary" type="button" onClick={onCopy}>{submitLabel}</button>
<button className="nds-button" data-size="sm" data-variant="primary" type="button" onClick={onCopy}>{submitLabel}</button>
) : (
<button className="primary" type="submit">{submitLabel}</button>
<button className="nds-button" data-size="sm" data-variant="primary" type="submit">{submitLabel}</button>
)}
</footer>
</form>

View file

@ -18,11 +18,11 @@ export function CountdownControls({ textContent, form, error, onChange, onSubmit
<form className="timer-control-panel timer-countdown-form" role="tabpanel" onSubmit={onSubmit} aria-describedby={error ? "timer-countdown-error" : undefined}>
<label>
<span>{textContent.countdownLabel || "Libellé"}</span>
<input value={form.label} onChange={(event) => onChange({ label: event.target.value })} placeholder={textContent.countdownLabelPlaceholder || "Boss, event, craft..."} />
<input className="nds-input" data-size="sm" type="text" value={form.label} onChange={(event) => onChange({ label: event.target.value })} placeholder={textContent.countdownLabelPlaceholder || "Boss, event, craft..."} />
</label>
<label>
<span>{textContent.countdownTypeLabel || "Type"}</span>
<select value={form.type} onChange={(event) => onChange({ type: event.target.value })}>
<select className="nds-select" data-size="sm" value={form.type} onChange={(event) => onChange({ type: event.target.value })}>
<option value="duration">{textContent.durationType || "Durée"}</option>
<option value="daily_time">{textContent.dailyTimeType || "Heure précise"}</option>
<option value="time_pattern">{textContent.timePatternType || "Pattern horaire"}</option>
@ -68,7 +68,7 @@ export function CountdownControls({ textContent, form, error, onChange, onSubmit
<span>{error}</span>
</p>
)}
<button className="primary" type="submit">{textContent.addCountdownButton || "Ajouter"}</button>
<button className="nds-button" data-size="sm" data-variant="primary" type="submit">{textContent.addCountdownButton || "Ajouter"}</button>
</form>
);
}
@ -102,7 +102,8 @@ export function CountdownList({ countdowns, nowMs, textContent, reorder, reorder
<div className={`tool-split-entry timer-entry has-single-action ${reorderEnabled ? "has-drag-handle" : ""} ${editingId !== countdown.id ? "has-inline-controls" : ""} ${(canReset || canToggleRefresh) && editingId !== countdown.id ? "has-inline-reset" : ""}`}>
{reorderEnabled && (
<button
className="timer-drag-handle"
className="timer-drag-handle nds-icon-button"
data-size="xs"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, countdown.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${countdown.label}`}
@ -135,7 +136,8 @@ export function CountdownList({ countdowns, nowMs, textContent, reorder, reorder
<span className="timer-inline-controls">
{canReset && (
<button
className="timer-inline-reset"
className="timer-inline-reset nds-icon-button"
data-size="xxs"
type="button"
onClick={(event) => {
event.stopPropagation();
@ -149,7 +151,8 @@ export function CountdownList({ countdowns, nowMs, textContent, reorder, reorder
)}
{canToggleRefresh && (
<button
className={`timer-inline-refresh-toggle ${countdown.autoRefresh ? "active" : ""} ${refreshBlocked ? "is-blocked" : ""}`}
className={`timer-inline-refresh-toggle nds-switch ${countdown.autoRefresh ? "active" : ""} ${refreshBlocked ? "is-blocked" : ""}`}
data-size="xs"
type="button"
onClick={(event) => {
event.stopPropagation();
@ -174,7 +177,7 @@ export function CountdownList({ countdowns, nowMs, textContent, reorder, reorder
</span>
</span>
)}
<button className="danger" type="button" onClick={() => onDelete(countdown.id)} aria-label={textContent.deleteTitle || "Supprimer"} title={textContent.deleteTitle || "Supprimer"}>
<button className="nds-icon-button" data-size="xs" data-variant="danger" type="button" onClick={() => onDelete(countdown.id)} aria-label={textContent.deleteTitle || "Supprimer"} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>

View file

@ -286,12 +286,13 @@ export function TimerModule({ toolboxId, moduleId, context, textExchangeOpen })
</section>
<section className={`tool-split-results timer-results-card ${data.scrollResults ? "is-scrollable" : ""}`}>
<div className="tool-split-actions">
<button className="tool-split-action-button danger" type="button" onClick={resetActiveResults} aria-label={textContent.resetTitle || "Réinitialiser"} title={textContent.resetTitle || "Réinitialiser"}>
<button className="tool-split-action-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={resetActiveResults} aria-label={textContent.resetTitle || "Réinitialiser"} title={textContent.resetTitle || "Réinitialiser"}>
<Icon name="rubber" />
</button>
{activeTab === "countdown" && (
<button
className={`tool-split-scroll-toggle timer-sort-toggle ${data.sortResults ? "active" : ""}`}
className={`tool-split-scroll-toggle timer-sort-toggle nds-switch ${data.sortResults ? "active" : ""}`}
data-size="md"
type="button"
onClick={() => setSortResults(!data.sortResults)}
aria-pressed={data.sortResults}
@ -303,7 +304,8 @@ export function TimerModule({ toolboxId, moduleId, context, textExchangeOpen })
</button>
)}
<button
className={`tool-split-scroll-toggle timer-scroll-toggle ${data.scrollResults ? "active" : ""}`}
className={`tool-split-scroll-toggle timer-scroll-toggle nds-switch ${data.scrollResults ? "active" : ""}`}
data-size="md"
type="button"
onClick={() => setScrollResults(!data.scrollResults)}
aria-pressed={data.scrollResults}

View file

@ -26,7 +26,8 @@ export function TimerAlertModeSwitch({ mode, blocked, textContent, onChange }) {
return (
<div
className={`timer-alert-mode-switch ${blocked ? "is-blocked" : ""}`}
className={`timer-alert-mode-switch nds-radio-button-group ${blocked ? "is-blocked" : ""}`}
data-size="xs"
role="group"
aria-label={textContent.alertModeTitle || "Mode d'alerte"}
title={blocked ? blockedTitle : undefined}
@ -63,11 +64,11 @@ export function TimePartsInput({ value, onChange, allowWildcard = false }) {
return (
<div className="time-parts-input" role="group" aria-label="Format heure minute seconde">
<input value={value.hours} onChange={(event) => updatePart("hours", event.target.value)} placeholder="hh" inputMode={allowWildcard ? "text" : "numeric"} />
<input type="text" value={value.hours} onChange={(event) => updatePart("hours", event.target.value)} placeholder="hh" inputMode={allowWildcard ? "text" : "numeric"} />
<span>:</span>
<input value={value.minutes} onChange={(event) => updatePart("minutes", event.target.value)} placeholder="mm" inputMode={allowWildcard ? "text" : "numeric"} />
<input type="text" value={value.minutes} onChange={(event) => updatePart("minutes", event.target.value)} placeholder="mm" inputMode={allowWildcard ? "text" : "numeric"} />
<span>:</span>
<input value={value.seconds} onChange={(event) => updatePart("seconds", event.target.value)} placeholder="ss" inputMode={allowWildcard ? "text" : "numeric"} />
<input type="text" value={value.seconds} onChange={(event) => updatePart("seconds", event.target.value)} placeholder="ss" inputMode={allowWildcard ? "text" : "numeric"} />
</div>
);
}
@ -87,6 +88,7 @@ export function EditableTimerLabel({ value, placeholder, ariaLabel, children, on
<input
{...inlineEdit.getInputProps({
className: "tool-split-entry-label timer-lap-label",
type: "text",
placeholder,
"aria-label": ariaLabel
})}

View file

@ -10,13 +10,13 @@ export function StopwatchControls({ textContent, elapsedMs, running, onStart, on
<span>{textContent.stopwatchTitle || "Chronomètre à étapes"}</span>
<strong><TimerValue ms={elapsedMs} compact /></strong>
<div className="timer-actions">
<button className="primary timer-action-button" type="button" onClick={running ? onPause : onStart} aria-label={running ? textContent.pauseButton || "Pause" : textContent.startButton || "Lancer"} title={running ? textContent.pauseButton || "Pause" : textContent.startButton || "Lancer"}>
<button className="timer-action-button nds-icon-button" data-size="sm" data-variant="primary" type="button" onClick={running ? onPause : onStart} aria-label={running ? textContent.pauseButton || "Pause" : textContent.startButton || "Lancer"} title={running ? textContent.pauseButton || "Pause" : textContent.startButton || "Lancer"}>
<Icon name={running ? "pause-circle" : "play-circle"} />
</button>
<button className="timer-action-button" type="button" onClick={onLap} disabled={!elapsedMs} aria-label={textContent.lapButton || "Étape"} title={textContent.lapButton || "Étape"}>
<button className="timer-action-button nds-icon-button" data-size="sm" type="button" onClick={onLap} disabled={!elapsedMs} aria-label={textContent.lapButton || "Étape"} title={textContent.lapButton || "Étape"}>
<Icon name="record-circle" />
</button>
<button className="danger timer-action-button" type="button" onClick={onReset} aria-label={textContent.resetTitle || "Réinitialiser"} title={textContent.resetTitle || "Réinitialiser"}>
<button className="timer-action-button nds-icon-button" data-size="sm" data-variant="danger" type="button" onClick={onReset} aria-label={textContent.resetTitle || "Réinitialiser"} title={textContent.resetTitle || "Réinitialiser"}>
<Icon name="stop-circle" />
</button>
</div>
@ -56,7 +56,7 @@ export function StopwatchLapList({ laps, textContent, onRename, onDelete }) {
</span>
</span>
)}
<button className="danger" type="button" onClick={() => onDelete(lap.id)} aria-label={textContent.deleteTitle || "Supprimer"} title={textContent.deleteTitle || "Supprimer"}>
<button className="nds-icon-button" data-size="xs" data-variant="danger" type="button" onClick={() => onDelete(lap.id)} aria-label={textContent.deleteTitle || "Supprimer"} title={textContent.deleteTitle || "Supprimer"}>
<Icon name="trash" />
</button>
</div>

View file

@ -188,7 +188,9 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
<header>
<div>
<button
className="module-drag-handle"
className="module-drag-handle nds-icon-button"
data-size="md"
data-variant="ghost"
type="button"
onPointerDown={(event) => reorder.itemReorder.startDrag(event, module.id)}
aria-label={`Déplacer ${module.title || label}`}
@ -204,7 +206,8 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
<div>
{definition.scrollable && (
<button
className={`module-scroll-button ${scrollable ? "active" : ""}`}
className={`module-scroll-button nds-switch ${scrollable ? "active" : ""}`}
data-size="lg"
onClick={() => onUpdateModule(module.id, (currentModule) => {
const nextModule = { ...currentModule };
if (scrollable) {
@ -224,7 +227,8 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
)}
{hasAddPanel && (
<button
className={`module-edit-button ${editing ? "active" : ""}`}
className={`module-edit-button nds-icon-button ${editing ? "active" : ""}`}
data-size="md"
onClick={() => setEditing((value) => {
const nextEditing = !value;
if (nextEditing) setTextExchangeOpen(false);
@ -239,7 +243,8 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
)}
{hasTextExchange && (
<button
className={`module-exchange-button ${textExchangeOpen ? "active" : ""}`}
className={`module-exchange-button nds-icon-button ${textExchangeOpen ? "active" : ""}`}
data-size="md"
onClick={() => setTextExchangeOpen((value) => {
const nextOpen = !value;
if (nextOpen) setEditing(false);
@ -252,7 +257,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
<Icon name="import" />
</button>
)}
<button className="module-delete-button danger" onClick={() => onDelete(module.id)} aria-label={`Retirer ${module.title || label}`} data-toolbox-tutorial-target={module.type === "links" ? "deleteTool" : undefined} title="Retirer">
<button className="module-delete-button nds-icon-button" data-size="md" data-variant="danger" onClick={() => onDelete(module.id)} aria-label={`Retirer ${module.title || label}`} data-toolbox-tutorial-target={module.type === "links" ? "deleteTool" : undefined} title="Retirer">
<span className="ui-icon ui-icon-trash" aria-hidden="true" />
</button>
</div>

View file

@ -6,7 +6,7 @@ export const HOUR_HALF_SLOT_COUNT = 2;
export const WEEK_SLOT_COUNT = DAY_HOUR_COUNT * HOUR_HALF_SLOT_COUNT;
export const WEEK_HOUR_ROW_HEIGHT = 27;
export const WEEK_HIDDEN_ROW_HEIGHT = 20;
export const WEEK_COMPACT_EVENT_HEIGHT = WEEK_HOUR_ROW_HEIGHT / HOUR_HALF_SLOT_COUNT;
export const WEEK_COMPACT_EVENT_HEIGHT = 22;
export const WEEKDAY_LABELS = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"];
function occurrencesOverlap(first, second) {

View file

@ -6,6 +6,15 @@ export const CALENDAR_MONTH_REPEAT_MODES = ["none", "weekly", "monthly"];
export const CALENDAR_ALERT_MODES = ["off", "visible", "site"];
export const CALENDAR_ALERT_OFFSETS = [10, 5, 0];
export const CALENDAR_COLORS = ["violet", "gold", "cyan", "pink", "green", "gray"];
export const CALENDAR_COLOR_VALUES = {
violet: "#8b5cf6",
gold: "#f6c453",
cyan: "#22d3ee",
pink: "#d946ef",
green: "#22c55e",
gray: "#7d879f"
};
export const CALENDAR_DARK_TEXT_COLORS = new Set(["gold", "cyan", "green"]);
export const WEEK_SLOT_MINUTES = 30;
export const WEEK_TIME_INPUT_STEP_MINUTES = 5;
export const WEEK_SLOT_COUNT = 48;
@ -13,6 +22,14 @@ export const DAY_MS = 24 * 60 * 60 * 1000;
export const MINUTE_MS = 60 * 1000;
export const WEEK_DAY_COUNT = 7;
export function getCalendarColorValue(color) {
return CALENDAR_COLOR_VALUES[color] || CALENDAR_COLOR_VALUES.violet;
}
export function getCalendarEventSurfaceVariant(color) {
return CALENDAR_DARK_TEXT_COLORS.has(color) ? "dark" : "default";
}
export function pad2(value) {
return String(value).padStart(2, "0");
}

View file

@ -112,7 +112,7 @@ export function AboutPage({ siteContent }) {
</article>
))}
</div>
<a className="button primary about-tools-library-button" href="#/library"><Icon name="book" />{content.toolsLibraryLink}</a>
<a className="about-tools-library-button nds-button" data-size="md" data-variant="primary" href="#/library"><Icon name="book" />{content.toolsLibraryLink}</a>
</section>
{content.contribute && (
<section className="about-contribute-section nebula-panel" id="contribuer" aria-labelledby="about-contribute-title">

View file

@ -0,0 +1,641 @@
// Rôle : expose en développement un catalogue vivant et interactif des contrôles NDS.
import { useState } from "react";
import { Icon } from "../components/Icon.jsx";
import { MixTagsInput } from "../components/MixTagsInput.jsx";
import { ToastPositionSwitch } from "../components/ToastPositionSwitch.jsx";
const SIZE_VALUES = {
field: ["xs", "sm", "md"],
button: ["xs", "sm", "md"],
iconButton: ["micro", "xxs", "xs", "sm", "md"],
group: ["xs", "sm", "md"],
switch: ["xs", "sm", "md", "lg"],
none: ["default"]
};
const VARIANT_VALUES = {
button: ["secondary", "primary", "ghost", "dashed", "danger"],
iconButton: ["secondary", "primary", "ghost", "panel-toggle", "danger"],
selectTrigger: ["default", "icon-picker"],
textarea: ["default", "ghost"],
swatch: ["color", "rainbow"],
event: ["default", "dark"],
none: ["default"]
};
const EDGE_VALUES = ["none", "left", "middle", "right", "top", "center", "bottom", "top-left", "top-right", "bottom-right", "bottom-left"];
const BORDER_VALUES = ["default", "joined", "none"];
const SCROLL_AXIS_VALUES = ["horizontal", "vertical"];
const EVENT_COLOR_VALUES = ["#8b5cf6", "#f6c453", "#22d3ee", "#d946ef", "#22c55e", "#7d879f"];
const STATE_VALUES = ["normal", "active", "disabled"];
const FIELD_STATE_VALUES = ["normal", "disabled"];
const DEFAULT_STATE_VALUES = ["normal"];
const CONTENT_VALUES = ["text", "icon", "mixed"];
const MIX_TAG_TOKENS = [
{ value: "base", label: "Base", title: "Valeur base" },
{ value: "bonus", label: "Bonus", title: "Valeur bonus" }
];
const COLOR_TOKENS = [
["Page", "var(--color-bg-page)"],
["Surface", "var(--color-bg-surface)"],
["Primary", "var(--color-primary)"],
["Gold", "var(--color-accent-gold)"],
["Cyan", "var(--color-accent-cyan)"],
["Danger", "var(--color-danger)"]
];
const RADIO_OPTIONS = {
text: [
{ value: "a", label: "A" },
{ value: "b", label: "B" },
{ value: "c", label: "C" }
],
icon: [
{ value: "rows", label: "Rows", icon: "rows" },
{ value: "columns", label: "Columns", icon: "columns" }
],
mixed: [
{ value: "one", label: "One", icon: "eye-open" },
{ value: "two", label: "Two", icon: "home" },
{ value: "three", label: "Three", icon: "close" }
]
};
const COMPONENTS = [
{
id: "nds-input",
name: "Input",
className: "nds-input",
sizes: SIZE_VALUES.field,
variants: VARIANT_VALUES.none,
attributes: { "data-size": SIZE_VALUES.field, "data-state": FIELD_STATE_VALUES },
render: ({ attributes, state, label, size }) => <input className="nds-input" {...attributes} type="text" defaultValue={state === "disabled" ? "Disabled" : size.toUpperCase()} disabled={state === "disabled"} aria-label={label} />
},
{
id: "nds-select",
name: "Select",
className: "nds-select",
sizes: SIZE_VALUES.field,
variants: VARIANT_VALUES.none,
attributes: { "data-size": SIZE_VALUES.field, "data-state": FIELD_STATE_VALUES },
render: ({ attributes, state, label }) => (
<select className="nds-select" {...attributes} defaultValue="b" disabled={state === "disabled"} aria-label={label}>
<option value="a">Option A</option>
<option value="b">{state === "disabled" ? "Disabled" : "Option B"}</option>
<option value="c">Option C</option>
</select>
)
},
{
id: "nds-select-trigger",
name: "Select Trigger",
className: "nds-select-trigger",
sizes: SIZE_VALUES.field,
variants: VARIANT_VALUES.selectTrigger,
attributes: { "data-size": SIZE_VALUES.field, "data-variant": VARIANT_VALUES.selectTrigger, "data-state": STATE_VALUES },
render: ({ attributes, state, variant }) => (
<button className="nds-select-trigger" {...attributes} type="button" disabled={state === "disabled"} aria-pressed={state === "active"}>
<Icon name={variant === "icon-picker" ? "shield" : "calendar"} />
{variant !== "icon-picker" && <span>{state === "disabled" ? "Disabled" : "27 août"}</span>}
</button>
)
},
{
id: "nds-time",
name: "Time Input",
className: "nds-time",
sizes: SIZE_VALUES.field,
variants: VARIANT_VALUES.none,
attributes: { "data-size": SIZE_VALUES.field, "data-state": FIELD_STATE_VALUES },
render: ({ attributes, state, label }) => <input className="nds-time" {...attributes} type="time" defaultValue="21:30" disabled={state === "disabled"} aria-label={label} />
},
{
id: "nds-textarea",
name: "Textarea",
className: "nds-textarea",
sizes: SIZE_VALUES.field,
variants: VARIANT_VALUES.textarea,
attributes: { "data-size": SIZE_VALUES.field, "data-variant": VARIANT_VALUES.textarea, "data-state": FIELD_STATE_VALUES },
render: ({ attributes, state, variant, label }) => <textarea className="nds-textarea" {...attributes} defaultValue={state === "disabled" ? "Disabled" : `${variant || "default"}\nscrollbar commune\nresize`} disabled={state === "disabled"} aria-label={label} />
},
{
id: "nds-inline-input",
name: "Inline Input",
className: "nds-inline-input",
sizes: SIZE_VALUES.none,
variants: VARIANT_VALUES.none,
attributes: { "data-state": FIELD_STATE_VALUES },
render: ({ state, label }) => <input className="nds-inline-input" type="text" defaultValue={state === "disabled" ? "Disabled" : "Inline edit"} disabled={state === "disabled"} aria-label={label} />
},
{
id: "nds-button",
name: "Button",
className: "nds-button",
sizes: SIZE_VALUES.button,
variants: VARIANT_VALUES.button,
configurable: { "data-edge": EDGE_VALUES, "data-border": BORDER_VALUES },
attributes: { "data-size": SIZE_VALUES.button, "data-variant": VARIANT_VALUES.button, "data-edge": EDGE_VALUES, "data-border": BORDER_VALUES, "data-state": STATE_VALUES },
render: ({ attributes, state, variant, label }) => <button className="nds-button" {...attributes} type="button" disabled={state === "disabled"} aria-pressed={state === "active"}>{variant === "secondary" ? "secondary" : variant || label}</button>
},
{
id: "nds-icon-button",
name: "Icon Button",
className: "nds-icon-button",
sizes: SIZE_VALUES.iconButton,
variants: VARIANT_VALUES.iconButton,
configurable: { "data-edge": EDGE_VALUES, "data-border": BORDER_VALUES },
attributes: { "data-size": SIZE_VALUES.iconButton, "data-variant": VARIANT_VALUES.iconButton, "data-edge": EDGE_VALUES, "data-border": BORDER_VALUES, "data-state": STATE_VALUES },
render: ({ attributes, state, variant, label }) => {
const icon = variant === "danger" ? "trash" : variant === "primary" ? "export" : "settings";
return <button className="nds-icon-button" {...attributes} type="button" disabled={state === "disabled"} aria-pressed={state === "active"} aria-label={label}><Icon name={icon} /></button>;
}
},
{
id: "nds-radio-button-group",
name: "Radio Button Group",
className: "nds-radio-button-group",
sizes: SIZE_VALUES.group,
variants: VARIANT_VALUES.none,
configurable: { content: CONTENT_VALUES },
attributes: { "data-size": SIZE_VALUES.group, content: CONTENT_VALUES, "data-state": ["normal", "disabled"] },
render: ({ attributes, config, state }) => state === "disabled"
? <div className="nds-radio-button-group" {...attributes} aria-disabled="true"><span>A</span><span>B</span></div>
: <InteractiveRadioButtonGroup attributes={attributes} options={RADIO_OPTIONS[config.content || "text"]} iconOnly={config.content === "icon"} />
},
{
id: "nds-checkbox",
name: "Checkbox",
className: "nds-checkbox",
sizes: ["md", "sm", "xs"],
variants: VARIANT_VALUES.none,
attributes: { "data-size": ["md", "sm", "xs"], "data-state": STATE_VALUES },
render: ({ attributes, state, label }) => <input className="nds-checkbox" {...attributes} type="checkbox" defaultChecked={state === "active"} disabled={state === "disabled"} aria-label={label} />
},
{
id: "nds-quantity-stepper",
name: "Quantity Stepper",
className: "nds-quantity-stepper",
sizes: SIZE_VALUES.group,
variants: VARIANT_VALUES.none,
configurable: { content: ["value", "input"] },
attributes: { "data-size": SIZE_VALUES.group, content: ["value", "input"] },
render: ({ attributes, config }) => <InteractiveStepper attributes={attributes} input={config.content === "input"} />
},
{
id: "nds-switch",
name: "Switch",
className: "nds-switch",
sizes: SIZE_VALUES.switch,
variants: VARIANT_VALUES.none,
attributes: { "data-size": SIZE_VALUES.switch, "data-state": STATE_VALUES },
render: ({ attributes, state }) => state === "disabled"
? <button className="nds-switch" {...attributes} type="button" disabled aria-pressed="true"><Icon name="scrollable" /><i aria-hidden="true" /></button>
: <InteractiveSwitch attributes={attributes} initialActive={state === "active"} />
},
{
id: "nds-native-switch",
name: "Native Switch",
className: "nds-native-switch",
sizes: SIZE_VALUES.none,
variants: VARIANT_VALUES.none,
attributes: { "data-state": STATE_VALUES },
render: ({ state }) => <NativeSwitchPreview disabled={state === "disabled"} initialChecked={state === "active"} />
},
{
id: "nds-swatch",
name: "Swatch",
className: "nds-swatch",
sizes: SIZE_VALUES.none,
variants: VARIANT_VALUES.swatch,
attributes: { "data-variant": VARIANT_VALUES.swatch, "data-state": STATE_VALUES },
render: ({ attributes, state, variant, label }) => <button className="nds-swatch" {...attributes} type="button" style={variant === "rainbow" ? undefined : { "--swatch-color": "#22d3ee" }} disabled={state === "disabled"} aria-pressed={state === "active"} aria-label={label} />
},
{
id: "nds-compound-toggle-field",
name: "Compound Toggle Field",
className: "nds-compound-toggle-field",
sizes: SIZE_VALUES.none,
variants: VARIANT_VALUES.none,
attributes: { "data-state": STATE_VALUES },
render: ({ state }) => <div className="nds-compound-toggle-field" aria-disabled={state === "disabled"}><button type="button" disabled={state === "disabled"} aria-pressed={state === "active"}>Hold</button><input className="nds-compound-toggle-field-input" type="number" defaultValue="2" disabled={state === "disabled"} aria-label="Hold seconds" /></div>
},
{
id: "nds-list-row",
name: "List Row",
className: "nds-list-row",
sizes: SIZE_VALUES.none,
variants: VARIANT_VALUES.none,
attributes: { "data-state": ["normal", "active", "disabled"] },
render: ({ state }) => <div className={`nds-list-row ${state === "active" ? "is-active" : ""} ${state === "disabled" ? "is-disabled" : ""}`.trim()}><button type="button" disabled={state === "disabled"} aria-pressed={state === "active"}>{state === "active" ? "Page active" : "Page"}</button></div>
},
{
id: "nds-large-icon-picker-trigger",
name: "Large Icon Picker Trigger",
className: "nds-large-icon-picker-trigger",
sizes: SIZE_VALUES.none,
variants: VARIANT_VALUES.none,
attributes: { "data-state": STATE_VALUES },
render: ({ state, label }) => <button className="nds-large-icon-picker-trigger" type="button" disabled={state === "disabled"} aria-pressed={state === "active"} aria-label={label}><Icon name="toolbox" /></button>
},
{
id: "nds-event-surface",
name: "Event Surface",
className: "nds-event-surface",
sizes: ["xs", "sm", "md"],
variants: VARIANT_VALUES.event,
configurable: { "data-color": EVENT_COLOR_VALUES },
attributes: { "data-size": ["xs", "sm", "md"], "data-variant": VARIANT_VALUES.event, "data-color": EVENT_COLOR_VALUES },
render: ({ attributes, config, variant, size }) => (
<div className="nds-event-surface" {...attributes} style={{ "--nds-event-color": config["data-color"], height: size === "xs" ? "10px" : size === "sm" ? "22px" : "44px" }}>
<button type="button"><strong>{variant === "dark" ? "Market" : "Raid"}</strong><span>21:30 - 22:45</span></button>
</div>
)
},
{
id: "nds-scrollbox",
name: "Scrollbox",
className: "nds-scrollbox",
sizes: SIZE_VALUES.none,
variants: VARIANT_VALUES.none,
configurable: { "data-axis": SCROLL_AXIS_VALUES },
attributes: { "data-axis": SCROLL_AXIS_VALUES },
render: ({ attributes, config }) => config["data-axis"] === "horizontal"
? <div className="nds-scrollbox" {...attributes}><div className="nds-scrollbox-content-wide"><span>Alpha sector</span><span>Beta sector</span><span>Gamma sector</span><span>Delta sector</span><span>Epsilon sector</span><span>Zeta sector</span><span>Eta sector</span></div></div>
: <div className="nds-scrollbox" {...attributes}><p>Zone scrollable</p><p>Track transparent</p><p>Thumb commun</p><p>Contenu supplémentaire</p><p>Encore une ligne</p></div>
}
];
const NAV_ITEMS = [
{ id: "foundations", label: "Foundations" },
...COMPONENTS.map(({ id, name }) => ({ id, label: name })),
{ id: "date-picker-buttons", label: "Date Picker Buttons" },
{ id: "specialized-components", label: "Specialized Components" },
{ id: "audit", label: "Audit" }
];
function dataProps(attributes = {}) {
return Object.fromEntries(Object.entries(attributes).filter(([, value]) => value !== undefined && value !== null && value !== ""));
}
function attributeLabel(attributes = {}) {
const entries = Object.entries(attributes).filter(([, value]) => value && value !== "default" && value !== "secondary" && value !== "md" && value !== "vertical" && value !== "none" && value !== "normal");
return entries.length ? entries.map(([key, value]) => `${key}="${value}"`).join(" ") : "default";
}
function DesignSystemNav() {
const scrollToSection = (id) => {
document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" });
};
return (
<aside className="design-system-nav" aria-label="Navigation design system">
<div>
<p className="eyebrow">Sokko G</p>
<h1>Nebula Design System</h1>
<p>Catalogue vivant des classes publiques, attributs, tailles, variants et états des contrôles interactifs Nebula.</p>
</div>
<div className="design-system-token-stack">
<SpecPill label="Theme" value="Nebula Command" />
<SpecPill label="Focus" value="gold outline" />
<SpecPill label="API" value="class + data-*" />
</div>
<nav>
{NAV_ITEMS.map((item) => <button key={item.id} type="button" onClick={() => scrollToSection(item.id)}>{item.label}</button>)}
</nav>
</aside>
);
}
function SpecPill({ label, value }) {
return <span className="design-system-spec-pill"><b>{label}</b><span>{value}</span></span>;
}
function ComponentSection({ id, title, meta, children }) {
return (
<section id={id} className="design-system-section">
<header>
<div>
<p className="eyebrow">{meta}</p>
<h2>{title}</h2>
</div>
</header>
{children}
</section>
);
}
function AttributeSummary({ component }) {
return (
<article className="design-system-component-intro">
<div className="design-system-doc-summary">
<span>Element</span>
<strong>{component.name}</strong>
<span>Classe</span>
<code>{component.className}</code>
</div>
<div className="design-system-attributes-doc">
<span>Attributes</span>
<dl>
{Object.entries(component.attributes).map(([name, values]) => (
<div key={name}>
<dt><code>{name}</code></dt>
<dd>{values.map((value) => <code key={value}>{value}</code>)}</dd>
</div>
))}
</dl>
</div>
</article>
);
}
function VariantMatrix({ columns, rows, controls, children }) {
return (
<article className="design-system-matrix">
{controls && <header>{controls}</header>}
<div className="design-system-matrix-grid" style={{ "--nds-matrix-columns": columns.length }}>
<div className="design-system-matrix-corner" aria-hidden="true" />
{columns.map((column) => <div key={column} className="design-system-matrix-heading">{column}</div>)}
{children || rows.map((row) => <StatePreview key={row.label} label={row.label} cells={row.cells} />)}
</div>
</article>
);
}
function StatePreview({ label, cells }) {
const [attribute, value] = label.includes("=") ? label.split("=") : [null, label];
return (
<>
<div className="design-system-matrix-row-label">
{attribute ? (
<>
<span>{attribute}</span>
<strong>{value}</strong>
</>
) : <strong>{value}</strong>}
</div>
{cells.map((cell, index) => <div key={`${label}-${index}`} className="design-system-matrix-cell">{cell}</div>)}
</>
);
}
function ConfigSelect({ name, values, value, onChange }) {
return (
<label className="design-system-config-field">
<span>{name}</span>
<select className="nds-select" data-size="xs" value={value} onChange={(event) => onChange(name, event.target.value)}>
{values.map((option) => <option key={option} value={option}>{option}</option>)}
</select>
</label>
);
}
function ControlSample({ children }) {
return (
<div className="design-system-control-sample">
<div className="design-system-sample-preview">{children}</div>
</div>
);
}
function ComponentCatalogSection({ component }) {
const [config, setConfig] = useState(() => Object.fromEntries(Object.entries(component.configurable || {}).map(([name, values]) => [name, values[0]])));
const sizes = component.sizes;
const variants = component.variants;
const stateValues = component.attributes["data-state"] || DEFAULT_STATE_VALUES;
const stateRows = stateValues.filter((state) => state !== "normal");
const hasConfigurableAttributes = Boolean(component.configurable && Object.keys(component.configurable).length);
const defaultStateVariant = variants.includes("secondary") ? "secondary" : variants[0];
const setConfigValue = (name, value) => setConfig((current) => ({ ...current, [name]: value }));
const renderCell = ({ size, variant, state = "normal" }) => {
const attrs = {
...Object.fromEntries(Object.entries(config).filter(([name]) => name.startsWith("data-"))),
"data-size": size,
"data-variant": variant,
"data-edge": config["data-edge"],
"data-border": config["data-border"],
"data-axis": config["data-axis"],
"data-state": state
};
const attributes = dataProps(attrs);
const label = `${component.className} ${attributeLabel(attributes)}`;
return <ControlSample key={`${variant}-${size}-${state}`} label={label}>{component.render({ attributes, config, state, variant, label, size })}</ControlSample>;
};
return (
<ComponentSection id={component.id} title={component.name} meta="NDS element">
<AttributeSummary component={component} />
<VariantMatrix
title="data-size x data-variant"
note="Les colonnes pilotent data-size. Les lignes affichent d'abord data-variant, puis data-state. Le formulaire applique les autres attributs à toute la matrice."
columns={sizes.map((size) => size === "default" ? "default" : `data-size=${size}`)}
controls={hasConfigurableAttributes && (
<div className="design-system-config-panel">
{Object.entries(component.configurable).map(([name, values]) => <ConfigSelect key={name} name={name} values={values} value={config[name]} onChange={setConfigValue} />)}
</div>
)}
>
{variants.map((variant) => (
<StatePreview
key={variant}
label={variant === "default" ? "default" : `data-variant=${variant}`}
cells={sizes.map((size) => renderCell({ size, variant }))}
/>
))}
{stateRows.length > 0 && <div className="design-system-matrix-separator">data-state</div>}
{stateRows.map((state) => (
<StatePreview
key={state}
label={`data-state=${state}`}
cells={sizes.map((size) => renderCell({ size, variant: defaultStateVariant, state }))}
/>
))}
</VariantMatrix>
</ComponentSection>
);
}
function ToggleButton({ children, className = "nds-button", attributes, ariaLabel }) {
const [pressed, setPressed] = useState(false);
return (
<button className={className} {...dataProps(attributes)} type="button" aria-pressed={pressed} aria-label={ariaLabel} onClick={() => setPressed((value) => !value)}>
{children}
</button>
);
}
function InteractiveRadioButtonGroup({ attributes, options, iconOnly = false }) {
const [value, setValue] = useState(options[0]?.value);
return (
<div className="nds-radio-button-group" {...dataProps(attributes)} role="radiogroup" aria-label="Groupe interactif">
{options.map((option) => {
const active = option.value === value;
return (
<button key={option.value} className={active ? "is-active active" : ""} type="button" role="radio" aria-checked={active} aria-label={iconOnly ? option.label : undefined} onClick={() => setValue(option.value)}>
{option.icon && <Icon name={option.icon} />}
{!iconOnly && <span>{option.label}</span>}
</button>
);
})}
</div>
);
}
function InteractiveStepper({ attributes, input = false }) {
const [value, setValue] = useState(2);
const increment = (step) => setValue((current) => Math.min(20, Math.max(0, current + step)));
return (
<span className="nds-quantity-stepper" {...dataProps(attributes)} aria-label="Stepper interactif">
<button type="button" aria-label="Retirer" onClick={() => increment(-1)}><span aria-hidden="true">-</span></button>
<span className="nds-quantity-stepper-value">
{input ? <input className="nds-quantity-stepper-input" type="number" min="0" max="20" value={value} onChange={(event) => setValue(Math.min(20, Math.max(0, Number.parseInt(event.target.value, 10) || 0)))} aria-label="Quantité actuelle" /> : value}
<span>/ 20</span>
</span>
<button type="button" aria-label="Ajouter" onClick={() => increment(1)}><Icon name="add" /></button>
</span>
);
}
function InteractiveSwitch({ attributes, initialActive = false }) {
const [active, setActive] = useState(initialActive);
return (
<button className="nds-switch" {...dataProps(attributes)} type="button" aria-pressed={active} onClick={() => setActive((value) => !value)}>
<Icon name="scrollable" />
<i aria-hidden="true" />
</button>
);
}
function NativeSwitchPreview({ disabled = false, initialChecked = false }) {
const [checked, setChecked] = useState(initialChecked);
return (
<label className="nds-native-switch">
<input type="checkbox" checked={checked} disabled={disabled} onChange={(event) => setChecked(event.target.checked)} aria-label="Inclure" />
<span aria-hidden="true" />
<b>{disabled ? "Disabled" : "Inclus"}</b>
</label>
);
}
function DatePickerPreview() {
const [activeDay, setActiveDay] = useState(2);
const days = Array.from({ length: 35 }, (_, index) => {
const day = index - 2;
return { id: index, label: day > 0 && day <= 31 ? day : "", outside: day <= 0 || day > 31 };
});
return (
<div className="nds-date-picker-preview" role="group" aria-label="Mini calendrier interactif">
{["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"].map((day) => <span key={day} className="nds-date-picker-preview-day">{day}</span>)}
{days.map((day) => (
<button key={day.id} className={`${day.label === activeDay ? "active" : ""} ${day.outside ? "is-outside" : ""}`.trim()} type="button" aria-pressed={day.label === activeDay} disabled={day.outside} onClick={() => setActiveDay(day.label)}>
{day.label}
</button>
))}
</div>
);
}
function FoundationsSection() {
return (
<ComponentSection id="foundations" title="Foundations" meta="tokens">
<div className="design-system-foundations">
<article className="design-system-type-card"><p>Typography</p><strong>Aa</strong><span>Inter, system-ui</span></article>
<article className="design-system-color-card">
<p>Colors</p>
<div className="design-system-swatches">{COLOR_TOKENS.map(([label, color]) => <span key={label} style={{ "--nds-swatch-color": color }} title={label} />)}</div>
</article>
<article className="design-system-radius-card">
<p>Radius</p>
<div><span style={{ borderRadius: "var(--radius-sm)" }}>sm</span><span style={{ borderRadius: "var(--radius-md)" }}>md</span><span style={{ borderRadius: "var(--radius-lg)" }}>lg</span></div>
</article>
<article className="design-system-focus-card"><p>Focus</p><button className="nds-button" data-variant="primary" type="button">Focus visible</button></article>
</div>
</ComponentSection>
);
}
function DatePickerButtonsSection() {
return (
<ComponentSection id="date-picker-buttons" title="Date Picker Buttons" meta="internal NDS pattern">
<VariantMatrix title="month grid" note="Boutons internes du mini calendrier, affichés avec un mois complet." columns={["interactive"]} rows={[{ label: "buttons", cells: [<ControlSample label="date-picker-button mixin"><DatePickerPreview /></ControlSample>] }]} />
</ComponentSection>
);
}
function SpecializedComponentsSection() {
const [mixValue, setMixValue] = useState("base + bonus");
const [smallMixValue, setSmallMixValue] = useState("base * 2");
const [toastPosition, setToastPosition] = useState("right");
return (
<ComponentSection id="specialized-components" title="Specialized Components" meta="not NDS public classes">
<VariantMatrix
title="Composants spécialisés conservés"
note="Ces éléments restent spécialisés : ils ne créent pas de classe de famille NDS publique."
columns={["MixTagsInput", "MixTagsInput small", "Toast switch"]}
rows={[{
label: "interactive",
cells: [
<ControlSample label="MixTagsInput"><MixTagsInput value={mixValue} onChange={setMixValue} tokens={MIX_TAG_TOKENS} insertToken={{ value: "base", icon: "add", title: "Insérer base" }} aria-label="MixTagsInput standard" /></ControlSample>,
<ControlSample label="MixTagsInput size=small"><MixTagsInput size="small" value={smallMixValue} onChange={setSmallMixValue} tokens={MIX_TAG_TOKENS} insertToken={{ value: "bonus", icon: "add", title: "Insérer bonus" }} aria-label="MixTagsInput compact" /></ControlSample>,
<ControlSample label="ToastPositionSwitch"><ToastPositionSwitch position={toastPosition} labels={{ left: "Notifications à gauche", right: "Notifications à droite", title: "Position des notifications" }} onChange={setToastPosition} /></ControlSample>
]
}]}
/>
</ComponentSection>
);
}
function AuditSection() {
return (
<ComponentSection id="audit" title="Audit" meta="api conventions">
<VariantMatrix
title="NDS Attribute API"
note="Une classe publique correspond à une famille. Les attributs data-* configurent les variations. Les états restent natifs, ARIA ou métier."
columns={["family class", "configuration", "state"]}
rows={[
{
label: "rule",
cells: [
<code>className=&quot;nds-button&quot;</code>,
<code>data-size data-variant data-edge data-border data-axis</code>,
<code>disabled aria-pressed .active .is-active</code>
]
},
{
label: "global",
cells: [
<code>Home / About / Games CTAs</code>,
<code>Shell / drawer / modales / filters</code>,
<code>Image viewer / library actions</code>
]
},
{
label: "sample",
cells: [
<button className="nds-icon-button" data-size="sm" data-variant="primary" type="button" aria-label="Exemple"><Icon name="export" /></button>,
<button className="nds-button" data-size="xs" data-variant="dashed" data-edge="left" data-border="joined" type="button"><Icon name="add" />XS</button>,
<ToggleButton attributes={{ "data-variant": "primary" }}>Actif</ToggleButton>
]
}
]}
/>
</ComponentSection>
);
}
export function DesignSystemPage() {
return (
<div className="design-system-page">
<DesignSystemNav />
<main className="design-system-main">
<FoundationsSection />
{COMPONENTS.map((component) => <ComponentCatalogSection key={component.id} component={component} />)}
<DatePickerButtonsSection />
<SpecializedComponentsSection />
<AuditSection />
</main>
</div>
);
}

View file

@ -47,7 +47,7 @@ export function HomeOnboardingSection({ content, className = "", onCreateToolbox
</ol>
{onCreateToolbox && content.primaryAction && (
<div className="home-onboarding-steps-action">
<button className="primary home-onboarding-create-button" type="button" onClick={onCreateToolbox}>
<button className="home-onboarding-create-button nds-button" data-size="md" data-variant="primary" type="button" onClick={onCreateToolbox}>
{content.primaryAction}
</button>
</div>

View file

@ -15,8 +15,8 @@ export function HomePage({ siteContent, toolboxes, actions }) {
<h1>{content.hero.title}</h1>
<p>{content.hero.description}</p>
<div className="actions">
<a className="button primary" href="#/toolboxes">{content.hero.primaryAction}</a>
<a className="button" href="#/games">{content.hero.secondaryAction}</a>
<a className="nds-button" data-size="md" data-variant="primary" href="#/toolboxes">{content.hero.primaryAction}</a>
<a className="nds-button" data-size="md" href="#/games">{content.hero.secondaryAction}</a>
</div>
</div>
<div className="home-stats hero-stats" aria-label="Statistiques locales du site">

View file

@ -356,7 +356,8 @@ function LibraryToolExample({ module, sourceData, description, categoryTitle, do
<div className="library-example-actions">
{definition.scrollable && (
<button
className={`module-scroll-button ${scrollable ? "active" : ""}`}
className={`module-scroll-button nds-switch ${scrollable ? "active" : ""}`}
data-size="md"
type="button"
onClick={() => setScrollable((value) => !value)}
aria-label={`${scrollable ? "Désactiver" : "Activer"} le scroll de ${title}`}
@ -369,7 +370,8 @@ function LibraryToolExample({ module, sourceData, description, categoryTitle, do
)}
{definition.editable && definition.addable !== false && (
<button
className={`module-edit-button ${editing ? "active" : ""}`}
className={`module-edit-button nds-icon-button ${editing ? "active" : ""}`}
data-size="sm"
type="button"
onClick={() => setEditing((value) => !value)}
aria-label={`${editing ? "Masquer" : "Afficher"} l'ajout de ${title}`}
@ -379,7 +381,7 @@ function LibraryToolExample({ module, sourceData, description, categoryTitle, do
<Icon name="add" />
</button>
)}
<button className="library-reset-button" type="button" onClick={() => context.setModuleData(LIBRARY_TOOLBOX_ID, module.id, cloneData(sourceData))} aria-label={`Réinitialiser ${title}`} title="Réinitialiser l'exemple">
<button className="library-reset-button nds-icon-button" data-size="sm" type="button" onClick={() => context.setModuleData(LIBRARY_TOOLBOX_ID, module.id, cloneData(sourceData))} aria-label={`Réinitialiser ${title}`} title="Réinitialiser l'exemple">
<Icon name="refresh" />
</button>
</div>
@ -506,7 +508,12 @@ function ControlPreview({ item }) {
return (
<span className="library-control-preview" aria-hidden="true">
{buttons.map((button, index) => (
<span className={`notepad-toolbar-button ${button.label && String(button.label).length > 2 ? "is-wide" : ""} ${button.active ? "active" : ""} ${button.danger ? "danger" : ""}`} key={`${button.label || button.icon || index}-${index}`}>
<span
className={`notepad-toolbar-button nds-icon-button ${button.label && String(button.label).length > 2 ? "is-wide" : ""} ${button.active ? "active" : ""}`.trim()}
data-size="sm"
data-variant={button.danger ? "danger" : undefined}
key={`${button.label || button.icon || index}-${index}`}
>
{button.icon ? <Icon name={button.icon} /> : button.label}
</span>
))}
@ -516,7 +523,7 @@ function ControlPreview({ item }) {
return (
<span className="library-control-preview" aria-hidden="true">
<span className={`notepad-toolbar-button ${preview.active ? "active" : ""} ${preview.danger ? "danger" : ""}`}>
<span className={`notepad-toolbar-button nds-icon-button ${preview.active ? "active" : ""}`.trim()} data-size="sm" data-variant={preview.danger ? "danger" : undefined}>
<Icon name={preview.icon || item.icon || "settings"} />
</span>
</span>
@ -582,7 +589,7 @@ function ToolAdvancedFeatureList({ docs, heading, copyLabel, copySuccess, contex
<p>{docs.importFormat.text}</p>
<div className="library-tool-import-example">
<pre><code>{docs.importFormat.example}</code></pre>
<button type="button" onClick={copyExample} title={copyLabel || "Copier l'exemple"}>
<button className="nds-button" data-size="sm" type="button" onClick={copyExample} title={copyLabel || "Copier l'exemple"}>
<Icon name="copy" />
<span>{copied ? copySuccess || "Exemple copié." : copyLabel || "Copier l'exemple"}</span>
</button>

View file

@ -9,6 +9,9 @@ const GamesPage = lazy(() => import("../features/games/GamesPage.jsx").then((mod
const LibraryPage = lazy(() => import("../pages/LibraryPage.jsx").then((module) => ({ default: module.LibraryPage })));
const ToolboxPage = lazy(() => import("../features/toolboxes/ToolboxPages.jsx").then((module) => ({ default: module.ToolboxPage })));
const ToolboxesPage = lazy(() => import("../features/toolboxes/ToolboxPages.jsx").then((module) => ({ default: module.ToolboxesPage })));
const DesignSystemPage = import.meta.env.DEV
? lazy(() => import("../pages/DesignSystemPage.jsx").then((module) => ({ default: module.DesignSystemPage })))
: null;
function RouteLoader() {
return <section className="empty"><p>Chargement...</p></section>;
@ -19,6 +22,7 @@ export function RouteContent(props) {
if (route === "/") return <HomePage {...props} />;
if (route === "/about") return <AboutPage siteContent={props.siteContent} />;
if (route === "/library") return <Suspense fallback={<RouteLoader />}><LibraryPage {...props} /></Suspense>;
if (route === "/design-system" && DesignSystemPage) return <Suspense fallback={<RouteLoader />}><DesignSystemPage /></Suspense>;
if (route === "/toolboxes") return <Suspense fallback={<RouteLoader />}><ToolboxesPage {...props} /></Suspense>;
if (route.startsWith("/toolbox/")) return <Suspense fallback={<RouteLoader />}><ToolboxPage {...props} toolboxId={route.split("/")[2]} /></Suspense>;
if (route === "/games") return <Suspense fallback={<RouteLoader />}><GamesPage {...props} /></Suspense>;

View file

@ -1,4 +1,6 @@
// Rôle : définit les bases globales HTML, body, typographie et contrôles natifs.
@use "design-system/controls";
* {
box-sizing: border-box;
}
@ -36,52 +38,73 @@ a {
text-decoration: none;
}
button,
.button,
select,
input,
textarea,
.import-button {
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: rgba(21, 26, 48, 0.92);
color: var(--color-text-primary);
font: inherit;
transition:
transform var(--duration-fast) var(--ease-standard),
border-color var(--duration-fast) var(--ease-standard),
background-color var(--duration-fast) var(--ease-standard),
color var(--duration-fast) var(--ease-standard),
box-shadow var(--duration-fast) var(--ease-standard);
button {
@include controls.text-button;
}
button,
.button,
.import-button {
display: inline-flex;
min-height: 42px;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 18px;
cursor: pointer;
select {
@include controls.select-control;
}
button:hover,
.button:hover,
.import-button:hover {
border-color: rgba(196, 181, 253, 0.34);
background:
linear-gradient(135deg, rgba(31, 37, 68, 0.96), rgba(22, 26, 51, 0.96)) padding-box,
linear-gradient(135deg, rgba(139, 92, 246, 0.32), rgba(196, 181, 253, 0.2)) border-box;
box-shadow:
0 0 14px rgba(139, 92, 246, 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
select:hover,
select:focus {
@include controls.select-control-hover;
}
button:active,
.button:active,
.import-button:active {
select:disabled {
@include controls.control-disabled;
}
select option {
@include controls.select-option;
}
:where(input[type="text"], input[type="search"], input[type="email"], input[type="url"], input[type="password"], textarea) {
@include controls.field-control;
}
input[type="time"] {
@include controls.time-control;
}
:where(textarea) {
@include controls.field-textarea;
@include controls.scrollbar-control;
@include controls.scrollbar-resizer;
}
:where(input, textarea)::placeholder {
@include controls.field-placeholder;
}
:where(input[type="text"], input[type="search"], input[type="email"], input[type="url"], input[type="password"], textarea):hover,
input[type="time"]:hover {
@include controls.field-hover;
}
:where(input[type="text"], input[type="search"], input[type="email"], input[type="url"], input[type="password"], textarea):focus,
input[type="time"]:focus {
@include controls.field-focus;
outline: none;
}
:where(input[type="text"], input[type="search"], input[type="email"], input[type="url"], input[type="password"], textarea):focus-visible,
input[type="time"]:focus-visible {
outline: 2px solid var(--color-accent-gold);
outline-offset: 3px;
}
button:disabled,
:where(input[type="text"], input[type="search"], input[type="email"], input[type="url"], input[type="password"], textarea):disabled,
input[type="time"]:disabled {
@include controls.control-disabled;
}
button:hover:not(:disabled, .active, .is-active, [aria-pressed="true"], .nds-button, .nds-icon-button) {
@include controls.text-button-hover;
}
button:active {
transform: translateY(0);
}
@ -91,66 +114,31 @@ button:active,
}
input[type="checkbox"] {
appearance: none;
-webkit-appearance: none;
display: inline-grid;
width: 20px;
min-width: 20px;
max-width: 20px;
inline-size: 20px;
min-inline-size: 20px;
max-inline-size: 20px;
aspect-ratio: 1;
height: 20px;
min-height: 20px;
max-height: 20px;
block-size: 20px;
min-block-size: 20px;
max-block-size: 20px;
place-content: center;
margin: 0;
padding: 0;
border: 1px solid transparent;
border-radius: 3px;
line-height: 0;
background:
linear-gradient(135deg, rgba(27, 34, 66, 0.92), rgba(16, 20, 44, 0.94)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.5), rgba(139, 92, 246, 0.44)) border-box;
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.06),
0 0 0 1px rgba(5, 7, 17, 0.42);
cursor: pointer;
@include controls.checkbox-control;
}
input[type="checkbox"]::before {
content: "";
width: 11px;
height: 11px;
background: #fff;
transform: scale(0);
transition: transform var(--duration-fast) var(--ease-standard);
clip-path: polygon(14% 52%, 0 66%, 38% 100%, 100% 18%, 84% 5%, 34% 68%);
@include controls.checkbox-check;
}
input[type="checkbox"]:hover {
border-color: rgba(246, 196, 83, 0.6);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.04),
0 0 14px rgba(246, 196, 83, 0.14);
@include controls.checkbox-hover;
}
input[type="checkbox"]:checked {
border-color: rgba(246, 196, 83, 0.88);
background:
linear-gradient(135deg, rgba(246, 196, 83, 0.96), rgba(139, 92, 246, 0.86)) padding-box,
linear-gradient(135deg, var(--color-accent-gold), rgba(196, 181, 253, 0.88)) border-box;
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
0 0 16px rgba(246, 196, 83, 0.2);
@include controls.checkbox-checked;
}
input[type="checkbox"]:checked::before {
transform: scale(1);
@include controls.checkbox-checked-check;
}
input[type="checkbox"]:disabled {
@include controls.checkbox-disabled;
}
input[type="checkbox"]:disabled::before {
opacity: 0.54;
}
input[type="checkbox"]:focus-visible {
@ -160,38 +148,24 @@ input[type="checkbox"]:focus-visible {
.themed-scrollbar,
.legacy-scrollbar {
scrollbar-color: rgb(79, 26, 114) transparent;
scrollbar-width: thin;
@include controls.scrollbar-control;
}
.filter-options::-webkit-scrollbar,
.drawer-panel::-webkit-scrollbar,
.damage-table-wrap::-webkit-scrollbar,
.themed-scrollbar::-webkit-scrollbar,
.legacy-scrollbar::-webkit-scrollbar {
background-color: transparent;
width: 6px;
height: 6px;
border-radius: 0;
}
.filter-options::-webkit-scrollbar-track,
.drawer-panel::-webkit-scrollbar-track,
.damage-table-wrap::-webkit-scrollbar-track,
.themed-scrollbar::-webkit-scrollbar-track,
.legacy-scrollbar::-webkit-scrollbar-track {
background-color: transparent;
border-radius: 0;
box-shadow: none;
}
.filter-options::-webkit-scrollbar-thumb,
.drawer-panel::-webkit-scrollbar-thumb,
.damage-table-wrap::-webkit-scrollbar-thumb,
.themed-scrollbar::-webkit-scrollbar-thumb,
.legacy-scrollbar::-webkit-scrollbar-thumb {
background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.5, rgb(79, 26, 114)), color-stop(1, rgb(44, 1, 135)));
border-radius: 10px;
:is(
.annotation-marker-list,
.damage-table-wrap,
.drawer-panel,
.filter-options,
.goal-tree-editor-panel,
.goal-tree-prerequisite-list,
.goal-tree-view-wrap,
.module-content,
.notepad-pages-list,
.notepad-surface,
.table-grid-wrap,
.tool-split-tree
) {
@include controls.scrollbar-control;
}
body.is-modal-open {
@ -203,238 +177,3 @@ body.is-modal-open {
align-items: center;
justify-content: center;
}
.primary {
border-color: rgba(246, 196, 83, 0.34);
background:
linear-gradient(135deg, rgba(72, 43, 160, 0.96), rgba(45, 52, 146, 0.94)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.66), rgba(139, 92, 246, 0.36)) border-box;
box-shadow:
0 8px 22px rgba(5, 7, 17, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
color: #fff;
font-weight: 700;
}
.primary:hover {
border-color: rgba(246, 196, 83, 0.72);
background:
linear-gradient(135deg, rgba(109, 40, 217, 0.98), rgba(67, 56, 202, 0.96)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.96), rgba(196, 181, 253, 0.68)) border-box;
box-shadow:
0 0 0 1px rgba(246, 196, 83, 0.16),
0 0 18px rgba(139, 92, 246, 0.3),
0 0 30px rgba(246, 196, 83, 0.16),
inset 0 1px 0 rgba(255, 255, 255, 0.18);
}
.ghost {
border-color: transparent;
background: transparent;
color: var(--color-text-secondary);
}
.ghost:hover {
border-color: rgba(196, 181, 253, 0.18);
background: rgba(139, 92, 246, 0.08);
color: var(--color-text-primary);
box-shadow: none;
}
.danger {
color: var(--color-danger);
}
.danger:hover {
border-color: rgba(251, 113, 133, 0.48);
background:
linear-gradient(135deg, rgba(80, 25, 42, 0.72), rgba(48, 18, 34, 0.82)) padding-box,
linear-gradient(135deg, rgba(251, 113, 133, 0.56), rgba(196, 181, 253, 0.16)) border-box;
color: #fff;
box-shadow:
0 0 14px rgba(251, 113, 133, 0.14),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
:is(
.sidebar-action-button,
.import-icon-button,
.toolbox-icon-button,
.card-icon-button,
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
.image-viewer-button,
.module-edit-button,
.module-exchange-button,
.module-delete-button,
.image-annotate-button,
.image-delete-button,
.checklist-delete-button,
.tool-quick-add-button
) {
display: inline-flex;
width: 40px;
min-width: 40px;
min-height: 40px;
align-items: center;
justify-content: center;
padding: 0;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: rgba(21, 26, 48, 0.88);
color: var(--color-text-primary);
font: inherit;
cursor: pointer;
transition:
transform var(--duration-fast) var(--ease-standard),
border-color var(--duration-fast) var(--ease-standard),
background-color var(--duration-fast) var(--ease-standard),
color var(--duration-fast) var(--ease-standard),
box-shadow var(--duration-fast) var(--ease-standard);
}
:is(
.sidebar-action-button,
.import-icon-button,
.toolbox-icon-button,
.card-icon-button,
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
.image-viewer-button,
.module-edit-button,
.module-exchange-button,
.module-delete-button,
.image-annotate-button,
.image-delete-button,
.checklist-delete-button,
.tool-quick-add-button
) .ui-icon {
transition:
background-color var(--duration-fast) var(--ease-standard),
filter var(--duration-fast) var(--ease-standard);
}
:is(
.sidebar-action-button,
.import-icon-button,
.toolbox-icon-button,
.card-icon-button,
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
.image-viewer-button,
.module-edit-button,
.module-exchange-button,
.module-delete-button,
.image-annotate-button,
.image-delete-button,
.checklist-delete-button,
.tool-quick-add-button
):hover {
border-color: rgba(196, 181, 253, 0.32);
background:
linear-gradient(135deg, rgba(31, 37, 68, 0.94), rgba(18, 22, 44, 0.96)) padding-box,
linear-gradient(135deg, rgba(139, 92, 246, 0.3), rgba(196, 181, 253, 0.2)) border-box;
color: #ddd6fe;
box-shadow:
0 0 12px rgba(139, 92, 246, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
:is(
.sidebar-action-button,
.import-icon-button,
.toolbox-icon-button,
.card-icon-button,
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
.image-viewer-button,
.module-edit-button,
.module-exchange-button,
.module-delete-button,
.image-annotate-button,
.image-delete-button,
.tool-quick-add-button
).primary {
border-color: rgba(246, 196, 83, 0.34);
background:
linear-gradient(135deg, rgba(72, 43, 160, 0.96), rgba(45, 52, 146, 0.94)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.66), rgba(139, 92, 246, 0.36)) border-box;
color: #fff;
box-shadow:
0 8px 22px rgba(5, 7, 17, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
:is(
.sidebar-action-button,
.import-icon-button,
.toolbox-icon-button,
.card-icon-button,
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
.image-viewer-button,
.module-edit-button,
.module-exchange-button,
.module-delete-button,
.image-annotate-button,
.image-delete-button,
.tool-quick-add-button
).primary:hover {
border-color: rgba(246, 196, 83, 0.72);
background:
linear-gradient(135deg, rgba(109, 40, 217, 0.98), rgba(67, 56, 202, 0.96)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.96), rgba(196, 181, 253, 0.68)) border-box;
color: #fff;
box-shadow:
0 0 0 1px rgba(246, 196, 83, 0.16),
0 0 18px rgba(139, 92, 246, 0.3),
0 0 30px rgba(246, 196, 83, 0.16),
inset 0 1px 0 rgba(255, 255, 255, 0.18);
}
:is(
.sidebar-action-button,
.import-icon-button,
.toolbox-icon-button,
.card-icon-button,
.drawer-action-button,
.drawer-close-button,
.filter-reset-button,
.image-viewer-button,
.module-edit-button,
.module-exchange-button,
.module-delete-button,
.image-annotate-button,
.image-delete-button,
.checklist-delete-button,
.tool-quick-add-button
):hover .ui-icon {
background-color: #c4b5fd;
filter: drop-shadow(0 0 6px rgba(196, 181, 253, 0.28));
}
:is(.card-icon-button, .module-delete-button, .image-delete-button, .checklist-delete-button).danger {
border-color: rgba(251, 113, 133, 0.2);
color: var(--color-danger);
}
:is(.card-icon-button, .module-delete-button, .image-delete-button, .checklist-delete-button).danger:hover {
border-color: rgba(251, 113, 133, 0.56);
background:
linear-gradient(135deg, rgba(80, 25, 42, 0.72), rgba(48, 18, 34, 0.82)) padding-box,
linear-gradient(135deg, rgba(251, 113, 133, 0.56), rgba(196, 181, 253, 0.16)) border-box;
color: #fff;
box-shadow:
0 0 14px rgba(251, 113, 133, 0.14),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
:is(.card-icon-button, .module-delete-button, .image-delete-button, .checklist-delete-button).danger:hover .ui-icon {
background-color: #fff;
filter: drop-shadow(0 0 6px rgba(251, 113, 133, 0.26));
}

View file

@ -452,5 +452,5 @@
}
.card-icon-button {
flex: 0 0 40px;
flex: 0 0 auto;
}

View file

@ -0,0 +1,514 @@
// Rôle : met en page le catalogue de contrôles visible uniquement en développement.
@use "design-system/controls";
@use "design-system/interactive-controls";
.design-system-page {
display: grid;
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
gap: var(--space-6);
width: min(1480px, calc(100% - 32px));
margin: 0 auto;
padding: var(--space-6) 0;
}
.design-system-nav {
position: sticky;
top: var(--space-4);
display: grid;
max-height: calc(100vh - var(--space-6));
align-content: start;
gap: var(--space-4);
padding: var(--space-5);
overflow: auto;
border: 1px solid rgba(150, 165, 205, 0.14);
border-radius: var(--radius-lg);
background:
radial-gradient(circle at 100% 0%, rgba(246, 196, 83, 0.08), transparent 34%),
rgba(7, 10, 24, 0.66);
box-shadow: var(--shadow-md);
}
.design-system-nav h1,
.design-system-section h2,
.design-system-matrix h3,
.design-system-nav p {
margin: 0;
}
.design-system-nav h1 {
font-size: clamp(2rem, 4vw, 3.2rem);
line-height: 0.95;
}
.design-system-nav p:not(.eyebrow) {
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
}
.design-system-nav nav,
.design-system-token-stack {
display: grid;
gap: 8px;
}
.design-system-nav nav {
padding-top: var(--space-2);
border-top: 1px solid rgba(150, 165, 205, 0.12);
}
.design-system-nav button {
display: flex;
min-height: 34px;
align-items: center;
padding: 0 10px;
border: 1px solid transparent;
border-radius: var(--radius-md);
background: transparent;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
font-weight: 800;
text-align: left;
cursor: pointer;
}
.design-system-nav button:hover,
.design-system-nav button:focus-visible {
border-color: rgba(246, 196, 83, 0.28);
background: rgba(246, 196, 83, 0.08);
color: var(--color-text-primary);
}
.design-system-main {
display: grid;
min-width: 0;
gap: var(--space-6);
}
.design-system-section {
display: grid;
min-width: 0;
scroll-margin-top: var(--space-5);
gap: var(--space-4);
}
.design-system-section > header {
display: flex;
min-width: 0;
align-items: end;
justify-content: space-between;
gap: var(--space-4);
padding: 0;
border: 0;
}
.design-system-section h2 {
font-size: clamp(1.6rem, 3vw, 2.35rem);
line-height: 1;
}
.design-system-component-intro {
display: grid;
grid-template-columns: minmax(180px, 0.8fr) minmax(0, 2.4fr);
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid rgba(150, 165, 205, 0.14);
border-radius: var(--radius-lg);
background: rgba(7, 10, 24, 0.34);
}
.design-system-doc-summary,
.design-system-attributes-doc {
display: grid;
align-content: start;
gap: 8px;
min-width: 0;
}
.design-system-component-intro span:first-child {
color: var(--color-accent-gold);
font-size: var(--font-size-xs);
font-weight: 900;
text-transform: uppercase;
}
.design-system-component-intro strong {
color: var(--color-text-primary);
font-size: var(--font-size-lg);
}
.design-system-component-intro code,
.design-system-component-intro li code {
color: var(--color-text-primary);
font-size: var(--font-size-sm);
}
.design-system-attributes-doc dl {
display: grid;
overflow: hidden;
margin: 0;
padding: 0;
border: 1px solid rgba(165, 180, 252, 0.12);
border-radius: var(--radius-md);
background: rgba(5, 7, 17, 0.22);
}
.design-system-attributes-doc dl > div {
display: grid;
grid-template-columns: minmax(118px, 0.42fr) minmax(0, 1fr);
min-width: 0;
border-top: 1px solid rgba(165, 180, 252, 0.1);
}
.design-system-attributes-doc dl > div:first-child {
border-top: 0;
}
.design-system-attributes-doc dt,
.design-system-attributes-doc dd {
min-width: 0;
margin: 0;
padding: 8px 10px;
}
.design-system-attributes-doc dt {
border-right: 1px solid rgba(165, 180, 252, 0.1);
background: rgba(139, 92, 246, 0.06);
}
.design-system-attributes-doc dd {
display: flex;
flex-wrap: wrap;
gap: 6px;
color: var(--color-text-secondary);
font-size: var(--font-size-xs);
font-weight: 800;
}
.design-system-attributes-doc dd code {
padding: 2px 6px;
border: 1px solid rgba(246, 196, 83, 0.16);
border-radius: var(--radius-sm);
background: rgba(246, 196, 83, 0.06);
}
.design-system-matrix {
display: grid;
min-width: 0;
overflow: hidden;
border: 1px solid rgba(150, 165, 205, 0.14);
border-radius: var(--radius-lg);
background: rgba(7, 10, 24, 0.42);
box-shadow: var(--shadow-sm);
}
.design-system-matrix > header {
display: flex;
min-width: 0;
align-items: start;
justify-content: flex-end;
gap: var(--space-3);
padding: var(--space-3);
border-bottom: 1px solid rgba(150, 165, 205, 0.12);
}
.design-system-matrix h3 {
font-size: var(--font-size-lg);
}
.design-system-matrix p {
margin: 4px 0 0;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
}
.design-system-config-panel {
display: flex;
width: 100%;
min-width: 0;
flex-wrap: wrap;
justify-content: flex-end;
gap: 8px;
}
.design-system-config-field {
display: grid;
min-width: 118px;
gap: 4px;
}
.design-system-config-field span {
color: var(--color-text-muted);
font-size: 0.68rem;
font-weight: 900;
text-transform: uppercase;
}
.design-system-matrix-grid {
@include controls.scrollbar-control;
display: grid;
grid-template-columns: 136px repeat(var(--nds-matrix-columns), minmax(150px, 1fr));
min-width: 0;
overflow-x: auto;
overscroll-behavior-x: contain;
}
.design-system-matrix-grid > * {
min-width: 0;
border-top: 1px dashed rgba(139, 92, 246, 0.42);
border-left: 1px dashed rgba(139, 92, 246, 0.42);
}
.design-system-matrix-corner,
.design-system-matrix-heading,
.design-system-matrix-row-label {
display: flex;
min-height: 48px;
align-items: center;
padding: 10px 12px;
color: var(--color-text-secondary);
font-size: var(--font-size-xs);
font-weight: 900;
text-transform: uppercase;
}
.design-system-matrix-heading {
color: var(--color-accent-gold);
}
.design-system-matrix-row-label {
display: grid;
align-content: center;
gap: 3px;
background: rgba(5, 7, 17, 0.22);
line-height: 1.25;
overflow-wrap: anywhere;
}
.design-system-matrix-row-label span {
color: var(--color-text-muted);
font-size: 0.66rem;
}
.design-system-matrix-row-label strong {
color: var(--color-text-secondary);
font-size: 0.78rem;
}
.design-system-matrix-cell {
display: flex;
min-height: 104px;
align-items: center;
gap: 10px;
padding: 12px;
}
.design-system-matrix-separator {
grid-column: 1 / -1;
display: flex;
min-height: 38px;
align-items: center;
padding: 8px 12px;
border-top: 2px solid rgba(246, 196, 83, 0.34);
border-left: 0;
background: rgba(246, 196, 83, 0.055);
color: var(--color-accent-gold);
font-size: var(--font-size-xs);
font-weight: 900;
text-transform: uppercase;
}
.design-system-control-sample {
display: grid;
width: 100%;
min-width: 0;
min-height: 48px;
align-items: center;
justify-items: center;
}
.design-system-sample-preview {
display: flex;
width: 100%;
min-width: 0;
align-items: center;
justify-content: center;
gap: 10px;
}
.design-system-matrix-cell > input,
.design-system-matrix-cell > select,
.design-system-matrix-cell > textarea,
.design-system-matrix-cell > .nds-field,
.design-system-matrix-cell > .mix-tags-input,
.design-system-control-sample input,
.design-system-control-sample select,
.design-system-control-sample textarea,
.design-system-control-sample .mix-tags-input {
width: 100%;
}
.design-system-foundations {
display: grid;
grid-template-columns: minmax(210px, 1.2fr) repeat(3, minmax(180px, 1fr));
gap: var(--space-3);
}
.design-system-foundations article {
display: grid;
min-height: 180px;
align-content: space-between;
gap: var(--space-3);
padding: var(--space-4);
border: 1px solid rgba(150, 165, 205, 0.14);
border-radius: var(--radius-lg);
background: rgba(7, 10, 24, 0.42);
box-shadow: var(--shadow-sm);
}
.design-system-foundations p {
margin: 0;
color: var(--color-text-primary);
font-size: var(--font-size-lg);
font-weight: 900;
}
.design-system-type-card strong {
display: block;
color: var(--color-text-primary);
font-size: clamp(4rem, 10vw, 7rem);
line-height: 0.9;
}
.design-system-type-card span {
color: var(--color-text-secondary);
}
.design-system-swatches {
display: grid;
grid-template-columns: repeat(6, minmax(24px, 1fr));
overflow: hidden;
border-radius: var(--radius-md);
}
.design-system-swatches span {
min-height: 96px;
background: var(--nds-swatch-color);
}
.design-system-radius-card > div,
.design-system-focus-card {
align-items: end;
}
.design-system-radius-card > div {
display: flex;
gap: 8px;
}
.design-system-radius-card span {
display: grid;
width: 52px;
height: 52px;
place-items: center;
border: 1px solid rgba(246, 196, 83, 0.32);
background: rgba(21, 26, 48, 0.72);
color: var(--color-text-secondary);
font-size: var(--font-size-xs);
font-weight: 900;
}
.design-system-spec-pill,
.design-system-size-spec {
display: inline-flex;
min-width: 0;
align-items: center;
}
.design-system-size-spec {
flex-wrap: wrap;
gap: 6px;
}
.design-system-spec-pill {
gap: 5px;
width: fit-content;
max-width: 100%;
padding: 3px 7px;
border: 1px solid rgba(165, 180, 252, 0.12);
border-radius: var(--radius-pill);
background: rgba(5, 7, 17, 0.28);
color: var(--color-text-muted);
font-size: 0.68rem;
font-weight: 800;
}
.design-system-spec-pill b {
color: var(--color-accent-gold);
}
.design-system-inline-controls {
display: flex;
align-items: center;
gap: 6px;
}
.design-system-page .calendar-month-event-repeat-switch {
width: min(230px, 100%);
}
@media (max-width: 980px) {
.design-system-page {
grid-template-columns: 1fr;
}
.design-system-nav {
position: static;
max-height: none;
}
.design-system-nav nav {
grid-template-columns: repeat(auto-fit, minmax(138px, 1fr));
}
.design-system-foundations {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.design-system-component-intro {
grid-template-columns: 1fr;
}
.design-system-matrix > header {
display: grid;
}
.design-system-config-panel {
max-width: none;
justify-content: start;
}
}
@media (max-width: 640px) {
.design-system-page {
width: min(100% - 20px, 1480px);
padding-block: var(--space-4) calc(var(--space-4) + 88px);
}
.design-system-nav,
.design-system-foundations article,
.design-system-matrix > header {
padding: var(--space-3);
}
.design-system-foundations {
grid-template-columns: 1fr;
}
.design-system-matrix-grid {
grid-template-columns: 116px repeat(var(--nds-matrix-columns), minmax(140px, 1fr));
}
.design-system-matrix-cell {
padding: 12px;
}
}

View file

@ -62,4 +62,19 @@
box-shadow:
inset 0 -2px 0 rgba(246, 196, 83, 0.34),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
&:hover:not(:disabled) {
border-color: rgba(246, 196, 83, 0.86);
background:
radial-gradient(circle at 100% 100%, rgba(246, 196, 83, 0.18), transparent 48%) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.2), rgba(139, 92, 246, 0.12) 52%, rgba(21, 26, 48, 0.08)) padding-box,
linear-gradient(135deg, rgba(16, 20, 38, 0.94), rgba(21, 26, 48, 0.86)) padding-box,
linear-gradient(120deg, rgba(246, 196, 83, 1), rgba(255, 244, 196, 0.92), rgba(246, 196, 83, 0.84)) border-box;
color: var(--color-text-primary);
box-shadow:
0 0 0 1px rgba(246, 196, 83, 0.18),
0 0 16px rgba(246, 196, 83, 0.18),
inset 0 -2px 0 rgba(246, 196, 83, 0.48),
inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
}

View file

@ -290,7 +290,6 @@
.sidebar-action-button {
display: inline-flex;
background: rgba(7, 10, 24, 0.36);
}
.content-shell {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,116 @@
// Rôle : complète le catalogue dev avec les helpers de présentation non publics.
@use "controls";
.design-system-page {
.nds-control-row,
.nds-field {
display: grid;
gap: 6px;
min-width: 0;
}
.nds-field {
width: 100%;
}
.nds-label,
.nds-checkbox-label {
color: var(--color-text-secondary);
font-size: var(--font-size-xs);
font-weight: 900;
text-transform: uppercase;
}
.design-system-spec-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 0;
}
.design-system-spec-list div {
display: inline-flex;
min-width: 0;
align-items: center;
gap: 4px;
padding: 3px 6px;
border: 1px solid rgba(165, 180, 252, 0.1);
border-radius: var(--radius-sm);
background: rgba(5, 7, 17, 0.28);
color: var(--color-text-muted);
font-size: 0.68rem;
font-weight: 800;
}
.design-system-spec-list dt,
.design-system-spec-list dd {
margin: 0;
}
.design-system-spec-list dt {
color: var(--color-accent-gold);
}
.nds-checkbox-label {
display: inline-flex;
align-items: center;
gap: 8px;
text-transform: none;
}
.nds-date-picker-preview {
display: grid;
grid-template-columns: repeat(7, minmax(0, 1fr));
gap: 3px;
width: min(300px, 100%);
padding: 4px;
border: 1px solid rgba(150, 165, 205, 0.16);
border-radius: var(--radius-sm);
background: rgba(8, 11, 24, 0.98);
}
.nds-date-picker-preview button {
@include controls.date-picker-button;
}
.nds-date-picker-preview button:hover,
.nds-date-picker-preview button.active {
@include controls.date-picker-button-hover;
}
.nds-date-picker-preview button.is-outside {
opacity: 0.32;
}
.nds-date-picker-preview-day {
display: grid;
min-height: 22px;
place-items: center;
color: var(--color-text-muted);
font-size: 0.66rem;
font-weight: 900;
text-transform: uppercase;
}
.nds-scrollbox-content-wide {
display: inline-flex;
width: max-content;
min-width: 1280px;
gap: 10px;
}
.nds-scrollbox-content-wide span {
display: inline-flex;
min-width: 170px;
min-height: 42px;
align-items: center;
justify-content: center;
padding: 0 12px;
border: 1px solid rgba(165, 180, 252, 0.12);
border-radius: var(--radius-md);
background: rgba(21, 26, 48, 0.58);
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
font-weight: 800;
}
}

View file

@ -76,15 +76,6 @@
font-size: var(--font-size-lg);
}
.filter-reset-button {
color: var(--color-text-secondary);
box-shadow: var(--shadow-sm);
}
.filter-reset-button:hover {
background: rgba(21, 26, 48, 0.96);
}
.field.compact {
padding: var(--space-4) 0;
}
@ -98,27 +89,14 @@
background: rgba(5, 7, 17, 0.36);
}
.filter-logic button {
min-height: 30px;
padding: 0 12px;
border-color: rgba(246, 196, 83, 0.24);
background:
linear-gradient(135deg, rgba(72, 43, 160, 0.72), rgba(45, 52, 146, 0.68)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.46), rgba(139, 92, 246, 0.28)) border-box;
color: var(--color-text-primary);
font-size: var(--font-size-sm);
font-weight: 800;
text-transform: uppercase;
.filter-logic-group {
flex: 0 0 auto;
}
.filter-logic button:hover {
border-color: rgba(246, 196, 83, 0.52);
background:
linear-gradient(135deg, rgba(109, 40, 217, 0.86), rgba(67, 56, 202, 0.82)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.72), rgba(196, 181, 253, 0.42)) border-box;
box-shadow:
0 0 14px rgba(139, 92, 246, 0.14),
inset 0 1px 0 rgba(255, 255, 255, 0.1);
.filter-logic-group button {
min-height: 30px;
padding: 0 12px;
text-transform: uppercase;
}
.filter-options {
@ -151,22 +129,6 @@
color: var(--color-text-primary);
}
.filter-chip input {
flex: 0 0 18px;
width: 18px;
min-width: 18px;
max-width: 18px;
inline-size: 18px;
min-inline-size: 18px;
max-inline-size: 18px;
height: 18px;
min-height: 18px;
max-height: 18px;
block-size: 18px;
min-block-size: 18px;
max-block-size: 18px;
}
.filter-chip img {
width: 22px;
height: 22px;

View file

@ -20,26 +20,8 @@
}
.game-list-help-link {
display: inline-flex;
width: fit-content;
min-height: 36px;
align-items: center;
justify-content: center;
margin-top: var(--space-3);
border-color: rgba(165, 180, 252, 0.18);
background: rgba(5, 7, 17, 0.24);
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
font-weight: 800;
padding: 7px 12px;
}
.game-list-help-link:hover {
border-color: rgba(246, 196, 83, 0.44);
background:
linear-gradient(135deg, rgba(246, 196, 83, 0.1), rgba(139, 92, 246, 0.08)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.42), rgba(196, 181, 253, 0.22)) border-box;
color: var(--color-text-primary);
}
.game-list-selector {
@ -244,13 +226,6 @@
width: 40px;
min-width: 40px;
min-height: 40px;
padding: 0;
border-radius: var(--radius-md);
}
.game-list-create-button:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.game-list-create-button .module-icon {

View file

@ -138,17 +138,6 @@ span {
gap: 10px;
}
.toolbar-stack {
align-items: flex-start;
flex-direction: column;
}
.toolbar-stack > div {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.hero-library-button {
display: inline-flex;
width: fit-content;
@ -198,4 +187,3 @@ span {
.home-section-heading p:not(.eyebrow) {
max-width: 980px;
}

View file

@ -97,17 +97,6 @@
border-right: 0;
}
.library-control-preview-stack {
display: inline-grid;
gap: 6px;
justify-items: start;
}
.library-control-preview-row {
display: inline-flex;
gap: 6px;
}
.library-control-preview .notepad-toolbar-button,
.library-control-preview .drawing-control-button,
.library-control-preview .notepad-color-toggle,

View file

@ -37,18 +37,10 @@
}
.library-reset-button {
display: inline-grid;
width: 40px;
min-width: 40px;
min-height: 40px;
flex: 0 0 40px;
place-items: center;
padding: 0;
}
.library-reset-button .ui-icon {
width: 16px;
height: 16px;
}
.library-tool-example .module-content {

View file

@ -6,10 +6,12 @@
@use "home";
@use "cards";
@use "components";
@use "design-system";
@use "icons";
@use "games";
@use "toolboxes";
@use "overlays";
@use "mhwilds";
@use "diablo4";
@use "design-system/attributes";
@use "responsive";

View file

@ -1,6 +1,4 @@
// Rôle : gère les résultats et entrées sauvegardées du calculateur.
@use "../mixins";
.calculator-card {
gap: 10px;
align-self: start;
@ -28,11 +26,6 @@
}
.calculator-drag-handle {
width: 30px;
min-width: 30px;
min-height: 36px;
padding: 0;
color: var(--color-text-muted);
cursor: grab;
touch-action: none;
user-select: none;

View file

@ -1,6 +1,4 @@
// Rôle : gère la structure commune du calculateur et ses panneaux de saisie.
@use "../mixins";
.calculator-module,
.timer-module {
display: grid;

View file

@ -1,5 +1,5 @@
// Rôle : gère les checklists et les espacements de listes adjacentes.
@use "../mixins";
@use "../design-system/controls";
.checklist {
display: grid;
@ -15,6 +15,12 @@
padding: var(--space-4) var(--space-4) 0;
}
.checklist-complete-mode-group {
@include controls.radio-button-group(xs);
width: fit-content;
align-items: center;
}
.checklist-section {
display: grid;
gap: 8px;
@ -56,21 +62,13 @@
}
.checklist-section-title-input {
width: 100%;
min-height: 30px;
padding: 5px 8px;
border: 1px solid transparent;
border-radius: var(--radius-md);
background: transparent;
@include controls.inline-field;
color: var(--color-accent-gold);
font: inherit;
outline: none;
}
.checklist-section-title-input:hover,
.checklist-section-title-input:focus {
border-color: rgba(246, 196, 83, 0.34);
background: rgba(21, 26, 48, 0.34);
@include controls.inline-field-hover;
}
.checklist-section.is-complete .checklist-section-header h3 {
@ -95,7 +93,7 @@
}
.timer-alert-mode-switch {
display: inline-flex;
@include controls.radio-button-group(xs);
width: fit-content;
align-items: center;
opacity: 0.78;
@ -103,76 +101,22 @@
.checklist-complete-mode-button,
.timer-alert-mode-button {
@include controls.radio-button-group-option(xs);
display: inline-grid;
width: 36px;
min-width: 36px;
min-height: 34px;
place-items: center;
padding: 0;
border: 1px solid rgba(165, 180, 252, 0.12);
border-radius: 0;
background: rgba(5, 7, 17, 0.24);
color: var(--color-text-secondary);
font-size: var(--font-size-xs);
font-weight: 800;
}
.checklist-complete-mode-button:first-child,
.timer-alert-mode-button:first-child {
border-radius: var(--radius-pill) 0 0 var(--radius-pill);
}
.checklist-complete-mode-button:last-child,
.timer-alert-mode-button:last-child {
border-radius: 0 var(--radius-pill) var(--radius-pill) 0;
}
.checklist-complete-mode-button + .checklist-complete-mode-button,
.timer-alert-mode-button + .timer-alert-mode-button {
margin-left: -1px;
}
.checklist-complete-mode-button .ui-icon,
.timer-alert-mode-button .ui-icon {
width: 16px;
height: 16px;
@include controls.radio-button-group-option-icon(xs);
}
.timer-alert-mode-button {
width: 24px;
min-width: 24px;
min-height: 26px;
border-color: rgba(165, 180, 252, 0.08);
background: rgba(5, 7, 17, 0.26);
color: var(--color-text-muted);
}
.timer-alert-mode-button .ui-icon {
width: 13px;
height: 13px;
}
.checklist-section-collapse-button {
display: inline-grid;
width: 30px;
min-width: 30px;
min-height: 30px;
place-items: center;
padding: 0;
border: 1px solid rgba(246, 196, 83, 0.28);
border-radius: var(--radius-md);
background:
linear-gradient(135deg, rgba(246, 196, 83, 0.12), rgba(139, 92, 246, 0.08)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.3), rgba(196, 181, 253, 0.18)) border-box;
color: #fff;
}
.checklist-section-collapse-button .ui-icon {
width: 16px;
height: 16px;
background-color: #fff;
opacity: 1;
filter: drop-shadow(0 0 7px rgba(255, 255, 255, 0.22));
}
.checklist-section-items {
@ -198,7 +142,7 @@
.checklist-item {
display: grid;
grid-template-columns: 1fr 42px 42px;
grid-template-columns: 1fr 34px 34px;
gap: 8px;
align-items: center;
}
@ -225,26 +169,11 @@
}
.checklist-drag-handle {
display: inline-grid;
width: 30px;
min-width: 30px;
min-height: 30px;
place-items: center;
padding: 0;
border: 1px solid rgba(165, 180, 252, 0.1);
border-radius: var(--radius-md);
background: rgba(21, 26, 48, 0.76);
color: var(--color-text-secondary);
cursor: grab;
touch-action: none;
user-select: none;
}
.checklist-drag-handle .ui-icon {
width: 15px;
height: 15px;
}
.checklist-drag-handle:active {
cursor: grabbing;
}
@ -259,14 +188,7 @@
.checklist-label-input,
.checklist-category-input {
min-width: 0;
width: 100%;
min-height: 30px;
padding: 5px 8px;
border: 1px solid transparent;
border-radius: var(--radius-md);
background: transparent;
color: var(--color-text-primary);
@include controls.inline-field;
}
.checklist-category-input {
@ -278,8 +200,7 @@
.checklist-label-input:focus,
.checklist-category-input:hover,
.checklist-category-input:focus {
border-color: rgba(246, 196, 83, 0.34);
background: rgba(21, 26, 48, 0.42);
@include controls.inline-field-hover;
}
.checklist-item-summary {
@ -312,20 +233,7 @@
.checklist-edit-button {
display: inline-grid;
width: 40px;
min-width: 40px;
min-height: 40px;
place-items: center;
padding: 0;
border: 1px solid rgba(165, 180, 252, 0.1);
border-radius: var(--radius-md);
background: rgba(21, 26, 48, 0.76);
color: var(--color-text-secondary);
}
.checklist-edit-button .ui-icon {
width: 17px;
height: 17px;
}
.checklist input[type="checkbox"] {
@ -343,53 +251,29 @@
}
.checklist-qty-controls {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
gap: 6px;
@include controls.quantity-stepper;
}
.checklist-qty-controls button {
width: 28px;
min-width: 28px;
min-height: 28px;
padding: 0;
border: 1px solid rgba(165, 180, 252, 0.12);
border-radius: var(--radius-md);
background: rgba(21, 26, 48, 0.88);
color: var(--color-text-secondary);
line-height: 1;
@include controls.quantity-stepper-button;
}
.checklist-qty-controls button:hover {
@include controls.quantity-stepper-button-hover;
}
.checklist-qty-current {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 3px;
color: var(--color-text-secondary);
@include controls.quantity-stepper-value;
}
.checklist-qty-current input {
width: max(18px, calc((var(--qty-current-digits, 1) * 0.72rem) + 8px));
min-height: 28px;
padding: 0 4px;
border: 1px solid transparent;
background: transparent;
color: var(--color-text-primary);
text-align: right;
-moz-appearance: textfield;
}
.checklist-qty-current input::-webkit-outer-spin-button,
.checklist-qty-current input::-webkit-inner-spin-button {
margin: 0;
-webkit-appearance: none;
@include controls.quantity-stepper-input;
@include controls.number-spinner-reset;
}
.checklist-qty-current input:hover,
.checklist-qty-current input:focus {
border-color: rgba(246, 196, 83, 0.34);
background: rgba(21, 26, 48, 0.42);
@include controls.quantity-stepper-input-hover;
}
.checklist-qty-current span {

View file

@ -1,6 +1,4 @@
// Rôle : gère l’outil compteurs et ses actions compactes.
@use "../mixins";
.counters-add-form {
grid-template-columns: minmax(0, 1fr) auto;
}
@ -31,13 +29,6 @@
position: absolute;
top: 10px;
right: 10px;
display: inline-grid;
width: 34px;
min-width: 34px;
min-height: 34px;
place-items: center;
padding: 0;
color: var(--color-text-muted);
cursor: grab;
touch-action: none;
user-select: none;
@ -71,11 +62,3 @@
justify-content: start;
gap: 8px;
}
.counter-actions button {
width: 34px;
min-width: 34px;
min-height: 34px;
padding: 0;
font-weight: 900;
}

View file

@ -1,6 +1,4 @@
// Rôle : gère le calque de dessin, son panneau et ses contrôles superposés.
@use "../mixins";
.drawing-overlay {
position: absolute;
top: 0;

View file

@ -1,5 +1,5 @@
// Rôle : gère les imports image, annotations, marqueurs et galeries.
@use "../mixins";
@use "../design-system/controls";
.dropzone {
display: grid;
@ -186,10 +186,7 @@
}
.annotation-marker-list input {
width: 100%;
min-width: 0;
min-height: 34px;
padding: 0 10px;
@include controls.field-control(xs);
}
.annotation-empty {
@ -220,27 +217,20 @@
}
.image-drag-handle {
@include controls.icon-button(sm);
position: absolute;
z-index: 4;
top: 12px;
left: 12px;
display: grid;
width: 32px;
height: 32px;
place-items: center;
padding: 0;
border: 1px solid rgba(138, 150, 197, 0.22);
border-radius: var(--radius-md);
background: rgba(9, 13, 29, 0.76);
color: rgba(229, 235, 255, 0.78);
cursor: grab;
opacity: 0;
transition: border-color var(--transition-fast), background var(--transition-fast), color var(--transition-fast), opacity var(--transition-fast), box-shadow var(--transition-fast);
}
.image-drag-handle .ui-icon {
width: 16px;
height: 16px;
@include controls.icon-button-icon(sm);
}
.images figure:hover .image-drag-handle,
@ -249,10 +239,7 @@
}
.image-drag-handle:hover {
border-color: rgba(246, 196, 83, 0.56);
background: rgba(20, 28, 55, 0.86);
color: var(--color-accent-gold);
box-shadow: 0 0 14px rgba(246, 196, 83, 0.12);
@include controls.icon-button-hover;
}
.image-drag-handle:active {
@ -289,23 +276,15 @@
}
.image-label-input {
width: 100%;
min-height: 36px;
padding: 0 10px;
border: 1px solid rgba(138, 150, 197, 0.2);
border-radius: var(--radius-md);
background: rgba(8, 13, 30, 0.62);
color: var(--color-text);
@include controls.field-control(sm);
}
.image-label-input::placeholder {
color: rgba(185, 194, 215, 0.48);
@include controls.field-placeholder;
}
.image-label-input:focus {
border-color: rgba(246, 196, 83, 0.72);
outline: none;
box-shadow: 0 0 0 3px rgba(246, 196, 83, 0.14);
@include controls.field-focus;
}
.images img {

View file

@ -1,6 +1,4 @@
// Rôle : gère l’outil liens et ses actions de liste.
@use "../mixins";
.link-item {
display: grid;
grid-template-columns: 34px minmax(0, 1fr) auto;
@ -13,11 +11,6 @@
}
.link-drag-handle {
width: 34px;
min-width: 34px;
min-height: 34px;
padding: 0;
color: var(--color-text-muted);
cursor: grab;
touch-action: none;
user-select: none;
@ -50,15 +43,3 @@
display: flex;
gap: 8px;
}
.link-item button {
width: 40px;
min-width: 40px;
min-height: 40px;
}
.link-item .link-drag-handle {
width: 34px;
min-width: 34px;
min-height: 34px;
}

View file

@ -19,12 +19,9 @@
}
.memos-ticker-toggle {
width: 40px;
min-width: 40px;
padding: 0;
flex: 0 0 auto;
}
.memos-ticker-toggle.active,
.memo-card.is-drop-target {
border-color: rgba(246, 196, 83, 0.48);
background: rgba(246, 196, 83, 0.1);
@ -92,16 +89,11 @@
}
.memo-card textarea {
width: 100%;
min-height: 76px;
overflow: hidden;
resize: none;
border: 0;
background: transparent;
color: var(--color-text-primary);
font: inherit;
line-height: 1.45;
outline: none;
scrollbar-gutter: auto;
}
.memo-card.is-dragging {
@ -112,13 +104,6 @@
position: absolute;
top: 8px;
right: 8px;
display: inline-grid;
width: 34px;
min-width: 34px;
min-height: 34px;
place-items: center;
padding: 0;
color: var(--color-text-muted);
cursor: grab;
opacity: 0.72;
touch-action: none;
@ -166,21 +151,12 @@
border-bottom: 0;
}
.memo-delete-button {
width: 34px;
min-width: 34px;
min-height: 34px;
padding: 0;
}
.memos-empty {
display: inline-flex;
min-height: 118px;
align-items: center;
justify-content: center;
gap: 8px;
border-style: dashed;
color: var(--color-text-secondary);
}
@keyframes memos-ticker-scroll {

View file

@ -1,4 +1,5 @@
// Rôle : gère le shell des modules toolbox, titres, icônes, onglets et boutons de module.
@use "../design-system/controls";
@use "../mixins";
.module header {
@ -41,58 +42,21 @@
}
.layout-switch {
display: inline-grid;
grid-template-columns: repeat(2, 38px);
gap: 4px;
padding: 4px;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: rgba(5, 7, 17, 0.48);
}
.layout-switch button {
min-height: 34px;
align-items: center;
justify-content: center;
padding: 0;
border-color: transparent;
background: transparent;
color: var(--color-text-muted);
font-weight: 900;
}
.layout-switch button:hover {
border-color: rgba(196, 181, 253, 0.22);
background: rgba(21, 26, 48, 0.5);
color: var(--color-text-primary);
box-shadow: none;
}
.layout-switch button.active {
@include mixins.temporary-toggle-active;
}
.module-drag-handle {
display: inline-grid;
width: 34px;
height: 34px;
flex: 0 0 34px;
place-items: center;
padding: 0;
border-color: transparent;
background: transparent;
color: var(--color-text-muted);
flex: 0 0 40px;
cursor: grab;
opacity: 0.62;
touch-action: none;
user-select: none;
}
.module-drag-handle .ui-icon {
width: 18px;
height: 18px;
}
.module:hover .module-drag-handle,
.module:focus-within .module-drag-handle {
opacity: 1;
@ -128,49 +92,20 @@
}
.module-scroll-button {
display: inline-grid;
grid-template-columns: 16px 30px;
width: fit-content;
min-height: 40px;
@include controls.switch-control(lg);
flex: 0 0 auto;
align-items: center;
gap: 7px;
padding: 0 7px 0 10px;
border: 1px solid rgba(165, 180, 252, 0.12);
border-radius: var(--radius-pill);
background: rgba(5, 7, 17, 0.24);
color: var(--color-text-secondary);
}
.module-scroll-button .ui-icon {
width: 16px;
height: 16px;
@include controls.switch-icon;
}
.module-scroll-button i {
position: relative;
display: block;
width: 30px;
height: 16px;
border: 1px solid rgba(165, 180, 252, 0.18);
border-radius: var(--radius-pill);
background: rgba(7, 10, 24, 0.72);
@include controls.switch-track;
}
.module-scroll-button i::before {
content: "";
position: absolute;
top: 50%;
left: 3px;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--color-text-muted);
transform: translateY(-50%);
transition:
left var(--duration-fast) var(--ease-standard),
background-color var(--duration-fast) var(--ease-standard),
box-shadow var(--duration-fast) var(--ease-standard);
@include controls.switch-thumb;
}
.module-edit-button.active,
@ -318,39 +253,22 @@
}
.tabs {
@include controls.radio-button-group-surface;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
gap: 6px;
padding: 5px;
border: 1px solid rgba(165, 180, 252, 0.12);
border-radius: var(--radius-md);
background: rgba(5, 7, 17, 0.34);
}
.tabs button {
display: inline-flex;
min-width: 0;
min-height: 36px;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 10px;
border: 1px solid transparent;
border-radius: calc(var(--radius-md) - 2px);
background: transparent;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
font-weight: 900;
@include controls.radio-button-group-option;
}
.tabs button:hover {
border-color: rgba(246, 196, 83, 0.28);
background: rgba(246, 196, 83, 0.08);
color: var(--color-text-primary);
@include controls.radio-button-group-option-hover;
}
.tabs button.active {
@include mixins.temporary-toggle-active;
@include controls.radio-button-group-option-active;
}
.tabs .module-icon-svg {

View file

@ -1,5 +1,5 @@
// Rôle : gère la surface de Bloc Note riche, ses pages et sa barre de formatage.
@use "../mixins";
@use "../design-system/controls";
.notepad-module {
position: relative;
@ -34,39 +34,13 @@
top: 50%;
right: 0;
z-index: 66;
display: inline-grid;
width: 40px;
min-width: 40px;
min-height: 40px;
place-items: center;
padding: 0;
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
background: rgba(39, 18, 69, 0.86);
color: var(--color-text-primary);
transform: translateY(-50%);
transition:
border-color var(--duration-fast) var(--ease-standard),
background-color var(--duration-fast) var(--ease-standard),
color var(--duration-fast) var(--ease-standard),
box-shadow var(--duration-fast) var(--ease-standard);
}
.notepad-pages-toggle .ui-icon {
width: 20px;
height: 20px;
}
.notepad-pages-toggle:active {
transform: translateY(-50%);
}
.notepad-pages-toggle:hover,
.notepad-pages-toggle.active {
border-color: rgba(246, 196, 83, 0.44);
background: rgba(56, 28, 92, 0.94);
color: var(--color-accent-gold);
}
.notepad-pages-menu {
position: absolute;
top: 0;
@ -103,49 +77,25 @@
}
.notepad-page-main {
@include controls.list-row-button;
min-width: 0;
overflow: hidden;
border: 1px solid rgba(150, 165, 205, 0.12);
border-radius: var(--radius-sm);
background: rgba(5, 7, 17, 0.36);
}
.notepad-page-row:hover .notepad-page-main,
.notepad-page-row:focus-within .notepad-page-main {
border-color: rgba(150, 165, 205, 0.24);
background: rgba(5, 7, 17, 0.5);
@include controls.list-row-button-hover;
}
.notepad-page-row.active .notepad-page-main {
border-color: rgba(246, 196, 83, 0.42);
background: rgba(246, 196, 83, 0.08);
@include controls.list-row-button-active;
}
.notepad-page-select {
width: 100%;
min-height: 32px;
justify-content: flex-start;
overflow: hidden;
padding: 0 8px;
border-color: transparent;
background: transparent;
color: var(--color-text-primary);
font-size: var(--font-size-sm);
font-weight: 900;
text-overflow: ellipsis;
white-space: nowrap;
@include controls.list-row-button-content;
}
.notepad-page-title-input {
width: 100%;
min-width: 0;
min-height: 32px;
padding: 0 8px;
border: 1px solid rgba(150, 165, 205, 0.14);
border-radius: var(--radius-sm);
background: rgba(7, 10, 24, 0.72);
color: var(--color-text-primary);
font: inherit;
@include controls.field-control(xs);
}
.notepad-page-actions {
@ -155,16 +105,21 @@
.notepad-page-edit,
.notepad-page-delete {
@include controls.icon-button(xxs);
width: 28px;
min-width: 28px;
height: 28px;
min-height: 28px;
padding: 0;
}
.notepad-page-edit .ui-icon,
.notepad-page-delete .ui-icon {
width: 14px;
height: 14px;
@include controls.icon-button-icon(xxs);
}
.notepad-page-edit:hover:not(:disabled),
.notepad-page-delete:hover:not(:disabled) {
@include controls.icon-button-hover;
}
@keyframes notepad-pages-slide-in {
@ -220,45 +175,48 @@
}
.notepad-toolbar-button,
.drawing-control-button,
.drawing-control-button {
@include controls.toolbar-button;
}
.notepad-swatch,
.notepad-color-toggle,
.drawing-width-toggle,
.drawing-width-button {
display: inline-grid;
place-items: center;
width: 34px;
min-width: 34px;
min-height: 34px;
padding: 0;
border-color: rgba(150, 165, 205, 0.18);
border-radius: var(--radius-sm);
background: rgba(16, 20, 38, 0.74);
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
font-weight: 900;
box-shadow: none;
@include controls.toolbar-button;
}
.notepad-swatch,
.notepad-color-toggle {
@include controls.color-swatch;
}
.notepad-color-toggle.is-rainbow {
@include controls.color-swatch-rainbow;
}
.notepad-toolbar-button .ui-icon,
.drawing-control-button .ui-icon {
width: 17px;
height: 17px;
@include controls.toolbar-button-icon;
}
.notepad-toolbar-button:hover,
.drawing-control-button:hover,
.notepad-toolbar-button:hover:not(.active, .is-active),
.drawing-control-button:hover:not(.active, .is-active),
.notepad-swatch:hover,
.notepad-color-toggle:hover,
.drawing-width-toggle:hover,
.drawing-width-button:hover,
.drawing-width-button:hover {
@include controls.toolbar-button-hover;
}
.notepad-toolbar-button.active,
.drawing-control-button.active,
.notepad-swatch.active,
.drawing-width-button.active {
border-color: rgba(246, 196, 83, 0.48);
background: rgba(246, 196, 83, 0.1);
color: var(--color-accent-gold);
@include controls.toolbar-button-active;
}
.notepad-swatch.active {
@include controls.color-swatch-active;
}
.notepad-color-dropdown {
@ -298,48 +256,12 @@
top: 6px;
}
.notepad-color-toggle::before,
.notepad-swatch::before {
content: "";
width: 18px;
height: 18px;
border: 1px solid rgba(245, 247, 255, 0.22);
border-radius: 50%;
background: var(--swatch-color);
box-shadow: inset 0 0 0 2px rgba(5, 7, 17, 0.36);
}
.notepad-color-toggle.is-rainbow::before {
width: 20px;
height: 20px;
border-color: rgba(245, 247, 255, 0.28);
background: conic-gradient(
from 20deg,
#f6c453,
#22d3ee,
#8b5cf6,
#d946ef,
#f5f7ff,
#f6c453
);
box-shadow:
inset 0 0 0 3px rgba(5, 7, 17, 0.34),
0 0 10px rgba(139, 92, 246, 0.16);
}
.notepad-color-menu {
position: absolute;
top: calc(100% + 8px);
left: 50%;
z-index: 50;
display: grid;
grid-template-columns: repeat(3, 34px);
gap: 6px;
padding: 8px;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: rgba(10, 13, 28, 0.99);
box-shadow: var(--shadow-lg);
@include controls.floating-control-menu(3);
opacity: 0;
pointer-events: none;
transform: translate(-50%, -4px);
@ -353,14 +275,7 @@
top: calc(100% + 8px);
left: 50%;
z-index: 50;
display: grid;
grid-template-columns: repeat(2, 34px);
gap: 6px;
padding: 8px;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: rgba(10, 13, 28, 0.99);
box-shadow: var(--shadow-lg);
@include controls.floating-control-menu(2);
transform: translateX(-50%);
}
@ -384,24 +299,17 @@
}
.notepad-list-option {
justify-content: center;
width: 34px;
min-width: 34px;
min-height: 34px;
padding: 0;
border-color: rgba(150, 165, 205, 0.18);
background: rgba(16, 20, 38, 0.74);
color: var(--color-text-secondary);
@include controls.compact-menu-button(sm);
font-size: var(--font-size-sm);
font-weight: 800;
box-shadow: none;
}
.notepad-list-option:hover,
.notepad-list-option:hover {
@include controls.compact-menu-button-hover;
}
.notepad-list-option.active {
border-color: rgba(246, 196, 83, 0.48);
background: rgba(246, 196, 83, 0.1);
color: var(--color-accent-gold);
@include controls.icon-button-active;
}
.notepad-color-menu.is-sectioned {

View file

@ -1,5 +1,5 @@
// Rôle : gère l’outil table, sa grille, ses cellules et états de sélection.
@use "../mixins";
@use "../design-system/controls";
.table-module {
display: grid;
@ -88,17 +88,11 @@
}
.table-header-input {
width: 100%;
padding: 0 8px;
border-top: 0;
border-left: 0;
border-radius: 0;
text-align: center;
@include controls.table-header-field;
}
.table-header-input::placeholder {
color: var(--color-text-muted);
opacity: 1;
@include controls.field-placeholder;
}
.table-header-input:hover {
@ -106,12 +100,7 @@
}
.table-header-input:focus {
z-index: 5;
border-color: rgba(246, 196, 83, 0.38);
background: rgba(21, 26, 48, 0.8);
color: var(--color-text-primary);
box-shadow: inset 0 0 0 1px rgba(246, 196, 83, 0.55), 0 0 0 2px rgba(139, 92, 246, 0.18);
outline: 0;
@include controls.table-header-field-focus;
}
.table-header-cell {
@ -134,19 +123,7 @@
}
.table-cell-input {
width: 100%;
min-width: 0;
min-height: 34px;
padding: 6px 8px;
border: 0;
border-right: 1px solid rgba(165, 180, 252, 0.08);
border-bottom: 1px solid rgba(165, 180, 252, 0.08);
border-radius: 0;
background: rgba(16, 20, 38, 0.34);
color: var(--color-text-primary);
caret-color: transparent;
font-size: var(--font-size-sm);
font-variant-numeric: tabular-nums;
@include controls.table-cell-field;
}
.table-cell-input:hover {
@ -154,21 +131,11 @@
}
.table-cell-input:focus {
position: relative;
z-index: 4;
border-color: rgba(34, 211, 238, 0.42);
background:
linear-gradient(135deg, rgba(34, 211, 238, 0.08), rgba(139, 92, 246, 0.06)),
rgba(21, 26, 48, 0.58);
box-shadow: inset 0 0 0 1px rgba(34, 211, 238, 0.58);
outline: 0;
@include controls.table-cell-field-focus;
}
.table-cell-input.is-editing:focus {
border-color: rgba(246, 196, 83, 0.38);
background: rgba(21, 26, 48, 0.72);
caret-color: auto;
box-shadow: inset 0 0 0 1px rgba(246, 196, 83, 0.55), 0 0 0 2px rgba(139, 92, 246, 0.18);
@include controls.table-cell-field-editing-focus;
}
.table-cell-input.is-formula {

View file

@ -1,5 +1,5 @@
// Rôle : gère les formulaires communs et les panneaux d’import/export texte.
@use "../mixins";
@use "../design-system/controls";
.notepad-footer {
display: flex;
@ -133,27 +133,23 @@
text-align: center;
}
input,
select,
textarea {
.text-import-modal-form input,
.text-import-modal-form select,
.text-import-modal-form textarea {
min-height: 44px;
padding: 0 14px;
outline: none;
}
textarea {
padding-block: 14px;
.text-import-modal-form textarea {
@include controls.field-textarea;
}
input::placeholder,
textarea::placeholder {
color: var(--color-text-muted);
opacity: 0.58;
.text-import-modal-form input::placeholder,
.text-import-modal-form textarea::placeholder {
@include controls.field-placeholder;
}
input:focus,
select:focus,
textarea:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.16);
.text-import-modal-form input:focus,
.text-import-modal-form select:focus,
.text-import-modal-form textarea:focus {
@include controls.field-focus;
}

View file

@ -1,5 +1,5 @@
// Rôle : gère l’outil timer, ses formulaires, listes et états d’alerte.
@use "../mixins";
@use "../design-system/controls";
.timer-tabs {
width: fit-content;
@ -7,6 +7,7 @@
}
.timer-tabs button {
@include controls.radio-button-group-option;
width: 40px;
min-width: 40px;
min-height: 36px;
@ -48,31 +49,15 @@
}
.time-parts-input {
display: grid;
grid-template-columns: minmax(34px, 1fr) 4px minmax(34px, 1fr) 4px minmax(34px, 1fr);
align-items: center;
min-height: 44px;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: rgba(15, 23, 42, 0.64);
overflow: hidden;
@include controls.time-parts-control;
}
.time-parts-input:focus-within {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.16);
@include controls.time-parts-focus;
}
.time-parts-input input {
width: auto;
min-width: 0;
min-height: 42px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
text-align: center;
box-shadow: none;
@include controls.number-spinner-reset;
}
.time-parts-input input:focus {
@ -104,8 +89,7 @@
}
.timer-interval-fields .time-parts-input {
grid-template-columns: minmax(22px, 1fr) 4px minmax(22px, 1fr) 4px minmax(22px, 1fr);
min-height: 38px;
@include controls.time-parts-control(sm);
}
.timer-interval-fields .time-parts-input input {
@ -207,8 +191,7 @@
gap: var(--space-2);
}
.timer-action-button,
.timer-actions .danger {
.timer-action-button {
display: inline-flex;
width: 40px;
min-width: 40px;
@ -282,72 +265,39 @@
}
.timer-drag-handle {
width: 30px;
min-width: 30px;
min-height: 36px;
padding: 0;
color: var(--color-text-muted);
@include controls.icon-button(xs);
cursor: grab;
touch-action: none;
user-select: none;
}
.timer-drag-handle .ui-icon {
@include controls.icon-button-icon(xs);
}
.timer-inline-reset {
width: 26px;
min-width: 26px;
min-height: 26px;
padding: 0;
border-color: rgba(165, 180, 252, 0.08);
border-radius: var(--radius-sm);
background: rgba(5, 7, 17, 0.26);
color: var(--color-text-muted);
@include controls.icon-button(xxs);
@include controls.icon-button-ghost;
}
.timer-inline-refresh-toggle {
display: inline-grid;
grid-template-columns: 13px 18px;
width: 42px;
min-width: 42px;
min-height: 26px;
align-items: center;
gap: 3px;
padding: 0 4px;
border-color: rgba(165, 180, 252, 0.08);
border-radius: var(--radius-sm);
background: rgba(5, 7, 17, 0.26);
color: var(--color-text-muted);
@include controls.switch-control(xs);
}
.timer-inline-refresh-toggle i {
position: relative;
display: block;
width: 18px;
height: 12px;
border: 1px solid rgba(165, 180, 252, 0.18);
border-radius: var(--radius-pill);
background: rgba(7, 10, 24, 0.72);
@include controls.switch-track;
}
.timer-inline-refresh-toggle i::before {
content: "";
position: absolute;
top: 50%;
left: 2px;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-text-muted);
transform: translateY(-50%);
transition:
left var(--duration-fast) var(--ease-standard),
background-color var(--duration-fast) var(--ease-standard),
box-shadow var(--duration-fast) var(--ease-standard);
@include controls.switch-thumb;
}
.timer-inline-reset .ui-icon {
@include controls.icon-button-icon(xxs);
}
.timer-inline-reset .ui-icon,
.timer-inline-refresh-toggle .ui-icon {
width: 14px;
height: 14px;
@include controls.switch-icon;
}
.timer-inline-controls {

Some files were not shown because too many files have changed in this diff Show more