diff --git a/AGENTS.md b/AGENTS.md index 556de15..99360c6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,6 +21,7 @@ Lire le fichier d'instructions le plus proche du code modifié avant d'interveni - Chaque nouveau fichier source ou test commence par un commentaire court `Rôle : ...` dans le style du langage. - Ne pas lancer automatiquement un serveur sur `5174` si `5173` répond déjà. - Avant de terminer une modification significative, lancer `npm run check`. +- Quand l'utilisateur indique qu'il va push, traiter le message comme une demande implicite de pré-push check : relire le diff, lancer les validations pertinentes et fournir un message de commit. Ne pas exécuter `git commit`, `git push` ou `git sync` sauf demande explicite. ## Beads diff --git a/DESIGN_SYSTEM.md b/DESIGN_SYSTEM.md index cb0f8e2..9908551 100644 --- a/DESIGN_SYSTEM.md +++ b/DESIGN_SYSTEM.md @@ -134,9 +134,9 @@ Caracteristiques : - bordure gradient subtile dore/violet clair ; - `border-radius: var(--radius-xl)` ; - `box-shadow: var(--shadow-md)` ; -- pas de background inline sur les heroes de jeux. +- les pages jeux peuvent fournir une image de hero via `--game-hero-image`. -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. +Les pages jeux doivent heriter du hero commun. Une image de jeu peut enrichir le hero quand elle reste lisible avec l'overlay commun ; les cards et covers restent les emplacements prioritaires pour les images inspectables. ## 7. Cards @@ -152,6 +152,8 @@ Style courant : La bordure haute/basse interne d'une cover doit rester doree quand elle separe une image du contenu. +Les surfaces de cards reutilisent les mixins `card-surface`, `card-surface-hover` et `card-cover-divider` depuis `website/src/styles/design-system/_surfaces.scss`. + Eviter les cards dans des cards. Les sections de page doivent rester des layouts ou panneaux, pas des empilements de cartes decoratives. ## 8. Modules Toolbox @@ -242,7 +244,7 @@ Regles UI : - la copie et le collage d'entree utilisent les raccourcis clavier `Ctrl+C` et `Ctrl+V` ; - le mini-switch repetable est actif par defaut ; une entree non repetable est supprimee quand son echeance de la semaine courante est terminee ; - les alertes reutilisent le switch a 3 etats du timer : aucune, toolbox visible, globale site ; -- le choix de couleur reprend le pattern de palette compacte du bloc-notes ; +- le choix de couleur reprend le pattern de palette compacte du Bloc Note ; - le choix initial s'affiche dans l'outil lui-meme ; la vue mensuelle affiche le nom du mois et des boutons mois precedent / courant / suivant ; - en vue mensuelle, la règle mensuelle peut cibler le numero du jour ou le rang du jour de semaine via un switch 2 états compact, indépendamment du choix de répétition. - en vue mensuelle, les dates de debut et de fin utilisent un champ compact qui ouvre un mini-calendrier avec navigation ; @@ -312,7 +314,7 @@ Regles UI : ### Bloc Note Riche -Le bloc note utilise une edition directe en `contentEditable`, sans mode preview separe. +Le Bloc Note utilise une edition directe en `contentEditable`, sans mode preview separe. Regles UI : @@ -329,19 +331,19 @@ Regles UI : - placeholder integre dans la surface d'edition quand la note est vide ; - le HTML persiste doit rester nettoye via allowlist, jamais via HTML libre. -### Memos +### Mémos -L'outil Memos sert aux informations courtes type post-it. +L'outil Mémos sert aux informations courtes type post-it. Regles UI : - cartes compactes, non imbriquees, avec accent couleur localise ; - edition directe du texte court ; - zone de texte en hauteur automatique, sans resize manuel ni scrollbar interne ; -- palette de couleurs compacte reprenant les swatches du bloc note ; -- la creation de memo est masquee par defaut et s'ouvre via le bouton `+` du header d'outil ; +- palette de couleurs compacte reprenant les swatches du Bloc Note ; +- la creation de mémo est masquee par defaut et s'ouvre via le bouton `+` du header d'outil ; - réorganisation des cartes par poignée de drag & drop compacte ; -- bande deroulante visible uniquement quand elle est active et qu'au moins un memo existe ; elle affiche tous les memos. +- bande deroulante visible uniquement quand elle est active et qu'au moins un mémo existe ; elle affiche tous les mémos. ### Calque De Dessin @@ -679,7 +681,7 @@ Boutons icones : - 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` ; +- taille micro reservee aux noeuds denses : `22px`, icone `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 ; @@ -689,13 +691,13 @@ Boutons icones : - 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. +- les barres d'outils compactes reutilisent la famille bouton icone ; `toolbar-button` reste un alias pratique pour les boutons courts du Bloc Note (`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 `list-row-button` pour les lignes selectionnables type pages du Bloc Note ; - 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 ; diff --git a/docs/FEATURE_CHECKLIST.md b/docs/FEATURE_CHECKLIST.md index c6212c2..e0c7c6b 100644 --- a/docs/FEATURE_CHECKLIST.md +++ b/docs/FEATURE_CHECKLIST.md @@ -16,7 +16,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page - Vérifier l'affichage dans la page toolbox complète. - Vérifier l'affichage dans le panneau latéral. - Pour le Bloc Note, vérifier la sélection, l'ajout, le renommage et la suppression de pages en page complète, panneau latéral et mobile. -- Pour Memos, vérifier l'édition courte, les couleurs, la réorganisation et la bande déroulante. +- Pour Mémos, vérifier l'édition courte, les couleurs, la réorganisation et la bande déroulante. - Vérifier le mode une colonne et deux colonnes. - Vérifier que l'ajout, la suppression et la réorganisation respectent `moduleOrder.one` et `moduleOrder.two`. - Vérifier le quota de stockage si l'outil manipule des données lourdes. diff --git a/docs/STORAGE_SCHEMA.md b/docs/STORAGE_SCHEMA.md index f2cf6b3..959ad30 100644 --- a/docs/STORAGE_SCHEMA.md +++ b/docs/STORAGE_SCHEMA.md @@ -128,7 +128,7 @@ Stockage compact : - les dessins temporaires du bloc-note sont conservés dans `sessionStorage` par toolbox, outil et page, pas dans IndexedDB ; - si toutes les pages sont vides et que seule la page par défaut reste présente, l'entrée de module peut être supprimée. -## Outil Memos +## Outil Mémos Type : `memos` diff --git a/e2e/home.spec.js b/e2e/home.spec.js index fa82216..4546f8d 100644 --- a/e2e/home.spec.js +++ b/e2e/home.spec.js @@ -1,13 +1,15 @@ -// Rôle : vérifie en navigateur que le shell principal se charge pour les diagnostics visuels. +// Rôle : vérifie en navigateur que la home charge les zones principales. import { expect, test } from "@playwright/test"; test("home page renders the application shell", async ({ page }) => { await page.goto("/"); await expect(page.locator(".app-shell")).toBeVisible(); - await expect(page.locator("main")).toBeVisible(); - await expect(page).toHaveScreenshot("home.png", { - fullPage: true, - maxDiffPixelRatio: 0.03 - }); + const main = page.getByRole("main"); + await expect(main).toBeVisible(); + await expect(main.getByRole("heading", { name: "Sokko G", exact: true })).toBeVisible(); + await expect(main.getByRole("link", { name: "Voir les toolboxes", exact: true })).toBeVisible(); + await expect(main.getByRole("link", { name: "Voir les jeux", exact: true })).toBeVisible(); + await expect(page.locator(".home-stats")).toBeVisible(); + await expect(page.locator(".origin-section")).toBeVisible(); }); diff --git a/e2e/home.spec.js-snapshots/home-chromium-linux.png b/e2e/home.spec.js-snapshots/home-chromium-linux.png deleted file mode 100644 index 3391ec2..0000000 Binary files a/e2e/home.spec.js-snapshots/home-chromium-linux.png and /dev/null differ diff --git a/e2e/toolbox.spec.js b/e2e/toolbox.spec.js index e1e7f97..6f28432 100644 --- a/e2e/toolbox.spec.js +++ b/e2e/toolbox.spec.js @@ -41,7 +41,7 @@ test("creates a toolbox and lazy-loads an added tool", async ({ page }) => { await createToolbox(page, "E2E Toolbox"); await expect(page.getByRole("heading", { name: "E2E Toolbox" })).toBeVisible(); - await expect(page.locator("[data-module-type='notepad']")).toBeVisible(); + await expect(page.locator("[data-module-type='memos']")).toBeVisible(); await expect(page.locator("[data-module-type='links']")).toBeVisible(); await addTool(page, "Arbre d'objectifs"); @@ -102,6 +102,26 @@ test("creates a goal tree checklist with more than three visible lines", async ( await expect(previewRows.filter({ hasText: "Objectif ligne 5" })).toBeVisible(); }); +test("loads randomizer tabs after module split", async ({ page }) => { + await createToolbox(page, "Randomizer E2E Toolbox"); + await addTool(page, "Randomizer"); + + const randomizer = moduleByType(page, "randomizer"); + await expect(randomizer).toBeVisible(); + await expect(randomizer.getByRole("tab", { name: "Dé" })).toBeVisible(); + await expect(randomizer.getByRole("button", { name: "Lancer" })).toBeVisible(); + + await randomizer.getByRole("tab", { name: "Roue" }).click(); + await expect(randomizer.getByLabel("Liste de valeurs")).toBeVisible(); + await randomizer.getByLabel("Liste de valeurs").fill("Rouge\nBleu"); + await randomizer.getByRole("button", { name: "Ajouter" }).click(); + await expect(randomizer.locator(".randomizer-wheel-value-row .nds-input").first()).toHaveValue("Rouge"); + + await randomizer.getByRole("tab", { name: "Équipes" }).click(); + await expect(randomizer.getByLabel("Liste de noms")).toBeVisible(); + await expect(randomizer.getByRole("button", { name: "Créer les équipes" })).toBeVisible(); +}); + test("creates a game-linked toolbox from the drawer", async ({ page }) => { await page.goto("/#/games/mhwilds"); await expect(page.getByRole("heading", { name: /Monster Hunter.*Wilds/ })).toBeVisible(); @@ -115,7 +135,8 @@ test("creates a game-linked toolbox from the drawer", async ({ page }) => { await dialog.getByRole("button", { name: "Créer" }).click(); await expect(drawer.getByRole("heading", { name: /Monster Hunter.*Wilds/ })).toBeVisible(); - await expect(drawer.locator("[data-module-type='notepad']")).toBeVisible(); + await expect(drawer.locator("[data-module-type='memos']")).toBeVisible(); + await expect(drawer.locator("[data-module-type='links']")).toBeVisible(); await expect(page.getByRole("button", { name: /Ouvrir la toolbox Monster Hunter.*Wilds/ })).toBeVisible(); }); @@ -217,6 +238,7 @@ test("creates edits summarizes and deletes equipment", async ({ page }) => { test("edits rich notepad content and persists it", async ({ page }) => { await createToolbox(page, "Notepad E2E Toolbox"); + await addTool(page, "Bloc Note"); const notepad = moduleByType(page, "notepad"); await expect(notepad).toBeVisible(); diff --git a/tests/data-validation.test.mjs b/tests/data-validation.test.mjs index bd945f7..4c0675d 100644 --- a/tests/data-validation.test.mjs +++ b/tests/data-validation.test.mjs @@ -19,6 +19,10 @@ test("site content json is complete", async () => { test("library examples are available", async () => { const library = JSON.parse(await readFile("website/public/data/library.json", "utf8")); validateLibraryData(library); + const memosModule = library.toolbox.modules.find((module) => module.type === "memos"); + const notepadModule = library.toolbox.modules.find((module) => module.type === "notepad"); + assert.equal(memosModule?.title, "Mémos"); + assert.equal(notepadModule?.title, "Bloc Note"); }); test("mhwilds data and assets are available", async () => { diff --git a/tests/static-styles.test.mjs b/tests/static-styles.test.mjs index 0058cbb..6a6dbe1 100644 --- a/tests/static-styles.test.mjs +++ b/tests/static-styles.test.mjs @@ -19,6 +19,10 @@ test("style entrypoint and theme layers are wired", async () => { const styleMixins = await readFile("website/src/styles/_mixins.scss", "utf8"); const styleShell = await readFile("website/src/styles/_shell.scss", "utf8"); const styleCards = await readFile("website/src/styles/_cards.scss", "utf8"); + const styleSurfaces = await readFile("website/src/styles/design-system/_surfaces.scss", "utf8"); + const styleControlsButtons = await readFile("website/src/styles/design-system/controls/_buttons.scss", "utf8"); + const styleAttributesButtons = await readFile("website/src/styles/design-system/attributes/_buttons.scss", "utf8"); + const goalTreeBranch = await readFile("website/src/features/toolboxes/modules/GoalTreeBranch.jsx", "utf8"); const styleHome = await readJoinedStyles([ "website/src/styles/_home.scss", "website/src/styles/home/_shared.scss", @@ -216,10 +220,30 @@ test("style entrypoint and theme layers are wired", async () => { assert.match(styleGames, /\.game-layout/); assert.match(styleGames, /\.game-grid/); assert.match(styleGames, /\.filter-panel/); + assert.match(styleSurfaces, /@mixin card-surface/); + assert.match(styleSurfaces, /@mixin card-surface-hover/); + assert.match(styleSurfaces, /@mixin card-cover-divider/); + assert.match(styleCards, /@use "design-system\/surfaces"/); + assert.match(styleCards, /surfaces\.card-surface/); + assert.match(styleGames, /@use "\.\.\/design-system\/surfaces"/); + assert.match(styleGames, /surfaces\.card-surface/); + assert.match(styleGames, /surfaces\.card-cover-divider/); assert.match(styleMhwilds, /\.mhwilds-card/); assert.match(styleMhwilds, /\.mhwilds-flip-indicator/); + assert.match(styleMhwilds, /@use "design-system\/surfaces"/); + assert.match(styleMhwilds, /surfaces\.card-surface/); + assert.match(styleMhwilds, /surfaces\.card-cover-divider/); assert.doesNotMatch(styleMhwilds, /\.game-(home-card|layout|grid)/); assert.match(styleCards, /compact-data-card/); + assert.doesNotMatch( + [ + goalTreeBranch, + styleToolboxGoalTree, + styleControlsButtons, + styleAttributesButtons + ].join("\n"), + /(?:data-size="tiny"|icon-button(?:-icon)?\(tiny\)|\$size == tiny)/ + ); assert.match(styleOverlays, /overscroll-behavior: contain/); assert.match(styleResponsive, /@media \(max-width: 760px\)/); }); diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index eb780d2..98222db 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -243,13 +243,21 @@ test("toolbox module registry and modules expose expected behavior", async () => const textImportPlanning = await readFile("website/src/features/toolboxes/modules/textImport/planning.js", "utf8"); const textImportTime = await readFile("website/src/features/toolboxes/modules/textImport/time.js", "utf8"); const imageViewer = await readFile("website/src/components/ImageViewer.jsx", "utf8"); + const randomizerModule = await readFile("website/src/features/toolboxes/modules/RandomizerModule.jsx", "utf8"); + const randomizerDice = await readFile("website/src/features/toolboxes/modules/randomizer/RandomizerDice.jsx", "utf8"); + const randomizerWheel = await readFile("website/src/features/toolboxes/modules/randomizer/RandomizerWheel.jsx", "utf8"); + const randomizerTeams = await readFile("website/src/features/toolboxes/modules/randomizer/RandomizerTeams.jsx", "utf8"); + const randomizerShared = await readFile("website/src/features/toolboxes/modules/randomizer/RandomizerShared.jsx", "utf8"); + const randomizerUiUtils = await readFile("website/src/features/toolboxes/modules/randomizer/randomizerUiUtils.js", "utf8"); + const randomizerBundle = `${randomizerModule}\n${randomizerDice}\n${randomizerWheel}\n${randomizerTeams}\n${randomizerShared}\n${randomizerUiUtils}`; [ ["CalendarModule.jsx", calendarModule], ["modules/index.jsx", moduleRegistry], ["GoalTreeParts.jsx", goalTreeParts], ["TimerParts.jsx", timerParts], - ["TaskPlannerItems.jsx", taskPlannerItems] + ["TaskPlannerItems.jsx", taskPlannerItems], + ["RandomizerModule.jsx", randomizerModule] ].forEach(([fileName, fileSource]) => { assert.ok(countLines(fileSource) <= 220, `${fileName} must remain a short orchestrator/barrel`); }); @@ -358,6 +366,19 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(calculatorModule, /parentId/); assert.match(calculatorModule, /MixTagsInput/); assert.match(calculatorModule, /useGroupedReorder/); + assert.match(moduleMetadataRegistry, /label: "Mémos"/); + assert.match(moduleMetadataRegistry, /defaultTitle: "Mémos"/); + assert.match(moduleMetadataRegistry, /label: "Bloc Note"/); + assert.match(moduleMetadataRegistry, /defaultTitle: "Bloc Note"/); + assert.match(randomizerModule, /RandomizerDice\.jsx/); + assert.match(randomizerModule, /RandomizerWheel\.jsx/); + assert.match(randomizerModule, /RandomizerTeams\.jsx/); + assert.match(randomizerDice, /export function DiceRandomizer/); + assert.match(randomizerWheel, /export function WheelRandomizer/); + assert.match(randomizerTeams, /export function TeamsRandomizer/); + assert.match(randomizerShared, /export function RandomizerHistory/); + assert.match(randomizerUiUtils, /export function hasReducedMotion/); + assert.match(randomizerBundle, /type="randomizer"/); assert.match(calculatorEntries, /calculator-drag-handle/); assert.match(calculatorModule, /scrollResults/); assert.match(calculatorModule, /tool-split-scroll-toggle/); diff --git a/website/AGENTS.md b/website/AGENTS.md index 81cbfd3..fc60585 100644 --- a/website/AGENTS.md +++ b/website/AGENTS.md @@ -51,7 +51,7 @@ Le check couvre la génération des index de listes, les tests et le build de pr ## Pré-push check -Quand l'utilisateur indique qu'il va push, faire une passe globale avant de conclure : +Quand l'utilisateur indique qu'il va push, par exemple "je vais push", "je push", "go push" ou "prêt à push", traiter le message comme une demande implicite de pré-push check. Ne pas attendre de précision supplémentaire et ne pas exécuter `git commit`, `git push` ou `git sync` sauf demande explicite. Faire une passe globale avant de conclure : - code mort, imports inutilisés, fonctions inutilisées, styles obsolètes ; - duplication de code ou de style pouvant être factorisée sans complexifier ; diff --git a/website/public/data/library.json b/website/public/data/library.json index eba7b6c..d3a446b 100644 --- a/website/public/data/library.json +++ b/website/public/data/library.json @@ -6,7 +6,7 @@ { "id": "m0", "type": "memos", - "title": "Memos" + "title": "Mémos" }, { "id": "m1", diff --git a/website/public/data/site.json b/website/public/data/site.json index 96d31a2..f1f40f3 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -1557,7 +1557,7 @@ }, { "title": "Ajoutez vos outils", - "text": "Choisissez seulement ce dont vous avez besoin : Memos, Bloc Note, checklist, images, liens, timer ou compteur." + "text": "Choisissez seulement ce dont vous avez besoin : Mémos, Bloc Note, checklist, images, liens, timer ou compteur." }, { "title": "Importez une sauvegarde si vous en avez une", @@ -1700,8 +1700,8 @@ { "icon": "memos", "title": "Notes et suivi", - "text": "Écrire des rappels courts, garder un bloc note multi-pages, suivre des objectifs simples ou compter des ressources.", - "examples": ["Memos", "Bloc Note", "Checklist", "Compteurs"] + "text": "Écrire des rappels courts, garder un Bloc Note multi-pages, suivre des objectifs simples ou compter des ressources.", + "examples": ["Mémos", "Bloc Note", "Checklist", "Compteurs"] }, { "icon": "picture", @@ -1798,7 +1798,7 @@ "summary": "Créez des espaces modulaires pour regrouper vos outils de session : notes, suivis, références, captures et futurs outils restent accessibles rapidement, en local, par jeu ou par usage.", "creationTemplate": { "memos": { - "title": "Memos", + "title": "Mémos", "text": "Profite bien de ta nouvelle toolbox." }, "links": { @@ -1836,7 +1836,7 @@ "target": "moduleTitle", "icon": "edit", "title": "Renommer un outil", - "text": "Cliquez sur le titre d’un outil, comme Memos, pour l’adapter à votre usage." + "text": "Cliquez sur le titre d’un outil, comme Mémos, pour l’adapter à votre usage." }, { "target": "deleteTool", @@ -1865,13 +1865,13 @@ "modules": { "notepad": { "placeholder": "Bloc Note...", - "pagesMenuLabel": "Pages du bloc note", - "pagesToggleLabel": "Afficher les pages du bloc note", - "pagesToggleText": "Afficher les pages du bloc note", + "pagesMenuLabel": "Pages du Bloc Note", + "pagesToggleLabel": "Afficher les pages du Bloc Note", + "pagesToggleText": "Afficher les pages du Bloc Note", "addPageLabel": "Ajouter une page", "renamePageLabel": "Renommer", "deletePageLabel": "Supprimer la page", - "toolbarLabel": "Mise en page du bloc note", + "toolbarLabel": "Mise en page du Bloc Note", "textFormatLabel": "Style du texte", "boldTitle": "Gras", "italicTitle": "Italique", @@ -1900,13 +1900,13 @@ "importPlaceholder": "@notepad\n@page: Page 1\n@html\n

Route boss

\n

Préparer les consommables

\n@endhtml\n@drawingMode: permanent\n@drawings: {\"strokes\":[]}", "importOpenButton": "Importer du texte", "exportOpenButton": "Exporter en format texte", - "importModalTitle": "Importer un bloc note", - "exportModalTitle": "Exporter un bloc note", + "importModalTitle": "Importer un Bloc Note", + "exportModalTitle": "Exporter un Bloc Note", "importButton": "Importer le texte", "exportButton": "Copier le texte", - "importSuccess": "Import bloc note terminé.", - "exportSuccess": "Export bloc note copié.", - "importError": "Format de bloc note invalide.", + "importSuccess": "Import Bloc Note terminé.", + "exportSuccess": "Export Bloc Note copié.", + "importError": "Format de Bloc Note invalide.", "exportInfo": "Le HTML est nettoyé à l'import. Les dessins permanents sont exportés en JSON compact." }, "memos": { diff --git a/website/src/features/toolboxes/modules/GoalTreeBranch.jsx b/website/src/features/toolboxes/modules/GoalTreeBranch.jsx index ee0fa7a..679f5b2 100644 --- a/website/src/features/toolboxes/modules/GoalTreeBranch.jsx +++ b/website/src/features/toolboxes/modules/GoalTreeBranch.jsx @@ -49,10 +49,10 @@ export function GoalTreeBranch({ nodes, parentId, selectedNodeId, editing, reord
{!editing && (
- -
diff --git a/website/src/features/toolboxes/modules/NotepadModule.jsx b/website/src/features/toolboxes/modules/NotepadModule.jsx index 96be374..f61cf15 100644 --- a/website/src/features/toolboxes/modules/NotepadModule.jsx +++ b/website/src/features/toolboxes/modules/NotepadModule.jsx @@ -292,7 +292,7 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen } ( diff --git a/website/src/features/toolboxes/modules/NotepadToolbar.jsx b/website/src/features/toolboxes/modules/NotepadToolbar.jsx index f18b865..3780285 100644 --- a/website/src/features/toolboxes/modules/NotepadToolbar.jsx +++ b/website/src/features/toolboxes/modules/NotepadToolbar.jsx @@ -1,4 +1,4 @@ -// Rôle : rend la barre d'outils de mise en forme du bloc-notes. +// Rôle : rend la barre d'outils de mise en forme du Bloc Note. import { CompactDropdown } from "../../../components/CompactDropdown.jsx"; import { FormattingColorDropdown } from "./DrawingControls.jsx"; @@ -15,7 +15,7 @@ export function NotepadToolbar({ onHighlightColorApply }) { return ( -
+
onCommand("bold")}>B diff --git a/website/src/features/toolboxes/modules/RandomizerModule.jsx b/website/src/features/toolboxes/modules/RandomizerModule.jsx index 0ff93e5..59ae099 100644 --- a/website/src/features/toolboxes/modules/RandomizerModule.jsx +++ b/website/src/features/toolboxes/modules/RandomizerModule.jsx @@ -1,71 +1,10 @@ -// Rôle : fournit l'outil Randomizer avec dé, roue pondérée et équipes équilibrées. -import { useEffect, useMemo, useRef, useState } from "react"; -import { CompactDropdown } from "../../../components/CompactDropdown.jsx"; -import { Icon } from "../../../components/Icon.jsx"; +// Rôle : orchestre les onglets de l'outil Randomizer sans porter les sous-vues. import { Tabs } from "../../../components/Tabs.jsx"; -import { useDraftForm } from "../../../hooks/useDraftForm.js"; import { TextExchangeActions } from "./TextExchangeActions.jsx"; -import { - RANDOMIZER_LIMITS, - RANDOMIZER_TABS, - addRandomizerHistory, - buildWheelSegments, - clampNumber, - createBalancedTeams, - drawWeightedEntries, - filterExclusionsForPlayers, - getDiceShape, - rollDice -} from "./randomizerUtils.js"; - -const WHEEL_COLORS = ["#8b5cf6", "#f6c453", "#22d3ee", "#d946ef", "#6366f1", "#22c55e"]; -const DRAW_PAUSE_MS = 500; -const DICE_ROLL_DELAYS = [62, 70, 82, 98, 120, 148, 182, 224, 276, 338, 410]; -const TEAM_REEL_DELAYS = [58, 66, 78, 94, 116, 144, 180, 226, 286, 360]; -const WHEEL_SPIN_MS = 1120; -const DEFAULT_EXCLUSION_DRAFT = { playerAId: "", playerBIds: [], mode: "group" }; - -function hasReducedMotion() { - return window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches; -} - -function polarToCartesian(radius, angle) { - const radians = (angle * Math.PI) / 180; - return { - x: 50 + radius * Math.cos(radians), - y: 50 + radius * Math.sin(radians) - }; -} - -function describeWheelArc(startAngle, endAngle) { - const start = polarToCartesian(48, startAngle); - const end = polarToCartesian(48, endAngle); - const largeArc = endAngle - startAngle > 180 ? 1 : 0; - return `M 50 50 L ${start.x} ${start.y} A 48 48 0 ${largeArc} 1 ${end.x} ${end.y} Z`; -} - -function formatDateTime(value) { - const date = new Date(value); - return Number.isFinite(date.getTime()) ? date.toLocaleString("fr-FR", { dateStyle: "short", timeStyle: "short" }) : ""; -} - -function getWheelTargetRotation(currentRotation, targetAngle) { - const pointerAngle = -90; - const targetRotation = pointerAngle - targetAngle; - const currentModulo = ((currentRotation % 360) + 360) % 360; - const targetModulo = ((targetRotation % 360) + 360) % 360; - const delta = (targetModulo - currentModulo + 360) % 360; - return currentRotation + 1080 + delta; -} - -function pickRandomItems(items, count) { - const nextItems = [...items]; - for (let index = nextItems.length - 1; index > 0; index -= 1) { - const targetIndex = Math.floor(Math.random() * (index + 1)); - [nextItems[index], nextItems[targetIndex]] = [nextItems[targetIndex], nextItems[index]]; - } - return nextItems.slice(0, count); -} +import { RANDOMIZER_TABS } from "./randomizerUtils.js"; +import { DiceRandomizer } from "./randomizer/RandomizerDice.jsx"; +import { TeamsRandomizer } from "./randomizer/RandomizerTeams.jsx"; +import { WheelRandomizer } from "./randomizer/RandomizerWheel.jsx"; export function RandomizerModule({ toolboxId, moduleId, context, textExchangeOpen }) { const data = context.normalizeRandomizerData(context.getModuleData(toolboxId, moduleId, {})); @@ -101,773 +40,3 @@ export function RandomizerModule({ toolboxId, moduleId, context, textExchangeOpe ); } - -function DiceRandomizer({ data, textContent, onChange, context }) { - const [displayValue, setDisplayValue] = useState(data.results[0] || 1); - const [displayResults, setDisplayResults] = useState(data.results); - const [rolling, setRolling] = useState(false); - const [rollingIndex, setRollingIndex] = useState(-1); - const [rollDuration, setRollDuration] = useState(DICE_ROLL_DELAYS[0]); - const timersRef = useRef([]); - const skipDiceSyncRef = useRef(false); - const shape = getDiceShape(data.faces); - - useEffect(() => { - if (rolling) return; - if (skipDiceSyncRef.current) { - skipDiceSyncRef.current = false; - return; - } - setDisplayValue(data.results[0] || 1); - setDisplayResults(data.results); - }, [data.results, rolling]); - - useEffect(() => () => { - timersRef.current.forEach((timerId) => window.clearTimeout(timerId)); - timersRef.current = []; - }, []); - - function queueTimer(callback, delay) { - const timerId = window.setTimeout(callback, delay); - timersRef.current.push(timerId); - return timerId; - } - - function clearDiceTimers() { - timersRef.current.forEach((timerId) => window.clearTimeout(timerId)); - timersRef.current = []; - } - - function stopDice() { - clearDiceTimers(); - skipDiceSyncRef.current = true; - setRolling(false); - setRollingIndex(-1); - } - - function updateDiceField(field, value) { - const limitMin = field === "faces" ? RANDOMIZER_LIMITS.diceFacesMin : RANDOMIZER_LIMITS.diceRollsMin; - const limitMax = field === "faces" ? RANDOMIZER_LIMITS.diceFacesMax : RANDOMIZER_LIMITS.diceRollsMax; - onChange({ ...data, [field]: clampNumber(value, limitMin, limitMax, data[field]) }); - } - - function launchDice() { - clearDiceTimers(); - skipDiceSyncRef.current = false; - const results = rollDice(data.faces, data.rolls); - const historyEntry = { id: context.uid("randomizer"), at: new Date().toISOString(), results }; - if (hasReducedMotion()) { - onChange({ ...data, results, history: addRandomizerHistory(data.history, historyEntry) }); - return; - } - setRolling(true); - setRollingIndex(0); - setDisplayResults([]); - - function animateRoll(index) { - let ticks = 0; - const tick = () => { - ticks += 1; - const isFinalTick = ticks >= DICE_ROLL_DELAYS.length; - const tickDelay = DICE_ROLL_DELAYS[Math.min(ticks, DICE_ROLL_DELAYS.length - 1)]; - setDisplayValue(isFinalTick ? results[index] : rollDice(data.faces, 1)[0]); - setRollDuration(Math.max(54, Math.round(tickDelay * 0.86))); - if (!isFinalTick) { - queueTimer(tick, tickDelay); - return; - } - setDisplayResults((current) => [...current, results[index]]); - if (index < results.length - 1) { - setRollingIndex(index + 1); - queueTimer(() => animateRoll(index + 1), DRAW_PAUSE_MS); - return; - } - setRolling(false); - setRollingIndex(-1); - onChange({ ...data, results, history: addRandomizerHistory(data.history, historyEntry) }); - }; - tick(); - } - - animateRoll(0); - } - - function clearDiceHistory() { - onChange({ ...data, history: [] }); - } - - function removeDiceHistoryEntry(entryId) { - onChange({ ...data, history: data.history.filter((entry) => entry.id !== entryId) }); - } - - return ( -
-
- -
- -
-
- -
-
-
- - - -
-
- `${entry.results.join(" - ")} · ${textContent.totalLabel || "Total"} ${entry.results.reduce((sum, result) => sum + result, 0)}`} - onClear={clearDiceHistory} - onRemove={removeDiceHistoryEntry} - disabled={rolling} - textContent={textContent} - showEmpty - /> -
-
- ); -} - -function RandomizerDie({ shape, rolling, value, duration, faces }) { - const cleanFaces = clampNumber(faces, RANDOMIZER_LIMITS.diceFacesMin, RANDOMIZER_LIMITS.diceFacesMax, 6); - const valueIndex = clampNumber(value, 1, cleanFaces, 1) - 1; - const values = useMemo(() => Array.from({ length: cleanFaces }, (_, index) => index + 1), [cleanFaces]); - - return ( -
- - - - {value} - -
- ); -} - -function WheelRandomizer({ data, textContent, onChange, context }) { - const [rotation, setRotation] = useState(0); - const [spinning, setSpinning] = useState(false); - const [drawing, setDrawing] = useState(false); - const [displayResults, setDisplayResults] = useState(data.results); - const [entriesText, setEntriesText] = useState(""); - const timersRef = useRef([]); - const skipWheelSyncRef = useRef(false); - const segments = useMemo(() => buildWheelSegments(data.entries), [data.entries]); - - useEffect(() => { - if (drawing) return; - if (skipWheelSyncRef.current) { - skipWheelSyncRef.current = false; - return; - } - setDisplayResults(data.results); - }, [data.results, drawing]); - - useEffect(() => () => { - timersRef.current.forEach((timerId) => window.clearTimeout(timerId)); - timersRef.current = []; - }, []); - - function queueTimer(callback, delay) { - const timerId = window.setTimeout(callback, delay); - timersRef.current.push(timerId); - return timerId; - } - - function clearWheelTimers() { - timersRef.current.forEach((timerId) => window.clearTimeout(timerId)); - timersRef.current = []; - } - - function stopWheel() { - clearWheelTimers(); - skipWheelSyncRef.current = true; - setSpinning(false); - setDrawing(false); - } - - function addEntries(event) { - event.preventDefault(); - const availableSlots = RANDOMIZER_LIMITS.wheelEntriesMax - data.entries.length; - const labels = entriesText - .split(/\r?\n/) - .map((label) => label.trim()) - .filter(Boolean) - .slice(0, availableSlots); - if (!labels.length) return; - onChange({ - ...data, - entries: [ - ...data.entries, - ...labels.map((label) => ({ id: context.uid("randomizer"), label, weight: 1 })) - ] - }); - setEntriesText(""); - } - - function updateEntry(entryId, patch) { - onChange({ ...data, entries: data.entries.map((entry) => entry.id === entryId ? { ...entry, ...patch } : entry) }); - } - - function removeEntry(entryId) { - onChange({ ...data, entries: data.entries.filter((entry) => entry.id !== entryId) }); - } - - function clearEntries() { - onChange({ ...data, entries: [], results: [] }); - } - - function clearWheelHistory() { - onChange({ ...data, history: [] }); - } - - function removeWheelHistoryEntry(entryId) { - onChange({ ...data, history: data.history.filter((entry) => entry.id !== entryId) }); - } - - function launchWheel() { - clearWheelTimers(); - skipWheelSyncRef.current = false; - const drawnEntries = drawWeightedEntries(data.entries, data.draws); - if (!drawnEntries.length) { - context.notify?.(textContent.wheelInvalid || "Ajoutez au moins deux valeurs."); - return; - } - const results = drawnEntries.map((entry) => entry.label); - const historyEntry = { id: context.uid("randomizer"), at: new Date().toISOString(), results }; - if (hasReducedMotion()) { - setDisplayResults(results); - onChange({ ...data, results, history: addRandomizerHistory(data.history, historyEntry) }); - return; - } - setDrawing(true); - setDisplayResults([]); - let rotationCursor = rotation; - - function animateDraw(index) { - const drawnEntry = drawnEntries[index]; - const winnerSegment = segments.find((segment) => segment.entry.id === drawnEntry.id) || segments[0]; - rotationCursor = getWheelTargetRotation(rotationCursor, winnerSegment.midAngle); - setSpinning(true); - setRotation(rotationCursor); - queueTimer(() => { - setDisplayResults((current) => [...current, drawnEntry.label]); - setSpinning(false); - if (index < drawnEntries.length - 1) { - queueTimer(() => animateDraw(index + 1), DRAW_PAUSE_MS); - return; - } - setDrawing(false); - onChange({ ...data, results, history: addRandomizerHistory(data.history, historyEntry) }); - }, WHEEL_SPIN_MS); - } - - animateDraw(0); - } - - return ( -
-
-
-
- -
- -
-
-
-