diff --git a/docs/FEATURE_CHECKLIST.md b/docs/FEATURE_CHECKLIST.md index 73a1d70..85ddd33 100644 --- a/docs/FEATURE_CHECKLIST.md +++ b/docs/FEATURE_CHECKLIST.md @@ -8,7 +8,7 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page - Déclarer l'outil dans `website/src/features/toolboxes/modules/index.jsx`. - Ajouter les textes nécessaires dans `website/public/data/site.json`. - Mettre à jour la validation de `site.json` dans `tests/helpers/data-validation.mjs`. -- Mettre à jour la normalisation et le stockage compact dans `website/src/main.jsx`. +- Mettre à jour la normalisation et le stockage compact dans `website/src/features/toolboxes/storage/toolboxStorage.js`. - Mettre à jour `docs/STORAGE_SCHEMA.md`. - Vérifier l'import et l'export si l'outil stocke des données. - Vérifier l'affichage dans la page toolbox complète. diff --git a/docs/STORAGE_SCHEMA.md b/docs/STORAGE_SCHEMA.md index 53d7f9e..bda25ce 100644 --- a/docs/STORAGE_SCHEMA.md +++ b/docs/STORAGE_SCHEMA.md @@ -19,7 +19,8 @@ Clés principales dans `kv` : "toolboxes": [], "links": {}, "setting:toolboxOrder": [], - "setting:storageBudgetBytes": 536870912 + "setting:storageBudgetBytes": 536870912, + "setting:toastPosition": "right" } ``` @@ -188,6 +189,74 @@ Stockage compact : - `label` est facultatif. - `label` est omis si vide. +## Outil Timer + +Type : `timer` + +Structure initiale : + +```json +{ + "activeTab": "stopwatch", + "scrollResults": true, + "sortResults": true, + "stopwatch": { + "elapsedMs": 12000, + "startedAt": 1760000000000, + "laps": [ + { + "id": "timer1", + "label": "Phase 1", + "elapsedMs": 9000 + } + ] + }, + "countdowns": [ + { + "id": "timer2", + "label": "Boss", + "type": "daily_time", + "alertMode": "visible", + "time": "12:03:00", + "targetAt": 1760000000000 + }, + { + "id": "timer3", + "label": "Event", + "type": "time_pattern", + "pattern": "X:45:00" + }, + { + "id": "timer4", + "label": "Event", + "type": "interval", + "intervalMs": 1800000, + "anchorAt": 1760000000000 + } + ] +} +``` + +Types de compte à rebours : + +- `duration` : durée ponctuelle, avec `durationMs` et `targetAt`. +- `daily_time` : occurrence ponctuelle à une heure précise, avec `time` au format `HH:MM:SS` et `targetAt`. +- `time_pattern` : prochaine occurrence automatique correspondant à un pattern `HH:MM:SS`, où chaque segment peut valoir `X`. +- `interval` : prochaine occurrence automatique toutes les X millisecondes, avec `intervalMs` et `anchorAt`. + +Stockage compact : + +- `activeTab` est omis si la valeur vaut `stopwatch`. +- `scrollResults` est omis si `false`. +- `sortResults` est omis si `false`. +- `stopwatch` est omis tant que le chrono est à zéro et qu'aucune étape n'est enregistrée. +- `elapsedMs` est omis si `0`. +- `startedAt` est omis si le chrono n'est pas lancé. +- `laps` est omis tant qu'aucune étape n'est enregistrée. +- `countdowns` est omis tant qu'aucun compte à rebours n'est configuré. +- `countdowns[].alertMode` vaut `off`, `visible` ou `site`, et est omis si `off`. +- L'activation d'une alerte est bloquée dans l'interface si la prochaine échéance est à moins de 5 minutes. + ## Outil Liens Type : `links` @@ -306,7 +375,7 @@ Exemple : Quand un outil change de structure : - mettre à jour ce document ; -- mettre à jour les normalisations dans `website/src/main.jsx` ; +- mettre à jour les normalisations dans `website/src/features/toolboxes/storage/toolboxStorage.js` ; - mettre à jour les textes dans `website/public/data/site.json` si nécessaire ; - mettre à jour la validation dans `tests/helpers/data-validation.mjs` si nécessaire ; - vérifier import/export ; diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs index 287bb14..f91a400 100644 --- a/tests/helpers/data-validation.mjs +++ b/tests/helpers/data-validation.mjs @@ -79,6 +79,10 @@ export function validateSiteContent(site) { "toolboxes.importAll", "toolboxes.exportAll", "toolboxes.importOne", + "toolboxes.preferencesLabel", + "toolboxes.toastPositionLabel", + "toolboxes.toastPositionLeft", + "toolboxes.toastPositionRight", "toolboxes.summary", "toolboxes.storageHelp.title", "toolboxes.storageHelp.text", @@ -146,6 +150,45 @@ export function validateSiteContent(site) { "toolboxes.modules.calculator.useEntryTitle", "toolboxes.modules.calculator.renameTitle", "toolboxes.modules.calculator.deleteTitle", + "toolboxes.modules.timer.stopwatchTab", + "toolboxes.modules.timer.countdownTab", + "toolboxes.modules.timer.stopwatchTitle", + "toolboxes.modules.timer.countdownTitle", + "toolboxes.modules.timer.startButton", + "toolboxes.modules.timer.pauseButton", + "toolboxes.modules.timer.lapButton", + "toolboxes.modules.timer.resetTitle", + "toolboxes.modules.timer.lapLabelAriaLabel", + "toolboxes.modules.timer.countdownLabel", + "toolboxes.modules.timer.countdownLabelPlaceholder", + "toolboxes.modules.timer.countdownTypeLabel", + "toolboxes.modules.timer.durationType", + "toolboxes.modules.timer.dailyTimeType", + "toolboxes.modules.timer.timePatternType", + "toolboxes.modules.timer.intervalType", + "toolboxes.modules.timer.durationLabel", + "toolboxes.modules.timer.dailyTimeLabel", + "toolboxes.modules.timer.timePatternLabel", + "toolboxes.modules.timer.intervalLabel", + "toolboxes.modules.timer.addCountdownButton", + "toolboxes.modules.timer.defaultCountdownLabel", + "toolboxes.modules.timer.finishedLabel", + "toolboxes.modules.timer.targetLabel", + "toolboxes.modules.timer.stepLabel", + "toolboxes.modules.timer.stepsTitle", + "toolboxes.modules.timer.countdownsTitle", + "toolboxes.modules.timer.scrollableTitle", + "toolboxes.modules.timer.sortTitle", + "toolboxes.modules.timer.alertModeTitle", + "toolboxes.modules.timer.alertOffTitle", + "toolboxes.modules.timer.alertVisibleTitle", + "toolboxes.modules.timer.alertSiteTitle", + "toolboxes.modules.timer.alertTooSoonTitle", + "toolboxes.modules.timer.alertMessage", + "toolboxes.modules.timer.emptySteps", + "toolboxes.modules.timer.emptyCountdowns", + "toolboxes.modules.timer.renameTitle", + "toolboxes.modules.timer.deleteTitle", "toolboxes.modules.imageAnnotation.addImage", "toolboxes.modules.imageAnnotation.replaceImage", "toolboxes.modules.imageAnnotation.pastePlaceholder", diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index a06db87..1604d0b 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -36,6 +36,7 @@ test("toolbox storage, cards and pages are wired", async () => { assert.match(toolboxStorage, /export function normalizeLinksData/); assert.match(toolboxStorage, /export function normalizeCountersData/); assert.match(toolboxStorage, /export function normalizeCalculatorData/); + assert.match(toolboxStorage, /export function normalizeTimerData/); assert.match(toolboxStorage, /export function normalizeImageAnnotationData/); assert.match(toolboxStorage, /export function compactModuleDataForStorage\(type, value\)/); assert.match(toolboxStorage, /scrollResults/); @@ -75,6 +76,7 @@ test("toolbox module registry and modules expose expected behavior", async () => const moduleRegistry = await readFile("website/src/features/toolboxes/modules/index.jsx", "utf8"); const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8"); const calculatorModule = await readFile("website/src/features/toolboxes/modules/CalculatorModule.jsx", "utf8"); + const timerModule = await readFile("website/src/features/toolboxes/modules/TimerModule.jsx", "utf8"); const checklistModule = await readFile("website/src/features/toolboxes/modules/ChecklistModule.jsx", "utf8"); const imagesModule = await readFile("website/src/features/toolboxes/modules/ImagesModule.jsx", "utf8"); const imageAnnotationModule = await readFile("website/src/features/toolboxes/modules/ImageAnnotationModule.jsx", "utf8"); @@ -94,6 +96,7 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(moduleRegistry, /links:/); assert.match(moduleRegistry, /counters:/); assert.match(moduleRegistry, /calculator:/); + assert.match(moduleRegistry, /timer:/); assert.match(moduleRegistry, /imageAnnotation:/); assert.match(moduleRegistry, /Annotation d'images/); assert.match(moduleRegistry, /editable: false/); @@ -116,9 +119,9 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(calculatorModule, /activeParentId/); assert.match(calculatorModule, /parentId/); assert.match(calculatorModule, /scrollResults/); - assert.match(calculatorModule, /calculator-scroll-toggle/); + assert.match(calculatorModule, /tool-split-scroll-toggle/); assert.match(calculatorModule, /EditableCalculatorLabel/); - assert.match(calculatorModule, /calculator-entry-label/); + assert.match(calculatorModule, /tool-split-entry-label/); assert.match(calculatorModule, /copyChecklistImport/); assert.match(calculatorModule, /resetCalculator/); assert.match(calculatorModule, /Quantité non modifiable/); @@ -126,6 +129,11 @@ test("toolbox module registry and modules expose expected behavior", async () => assert.match(calculatorModule, /ResizeObserver/); assert.match(calculatorModule, /--calculator-scroll-height/); assert.match(calculatorModule, /CalculatorEntries/); + assert.match(timerModule, /export function TimerModule/); + assert.match(timerModule, /Tabs/); + assert.match(timerModule, /stopwatch/); + assert.match(timerModule, /countdown/); + assert.match(timerModule, /normalizeTimerData/); assert.match(checklistModule, /export function ChecklistModule/); assert.match(checklistModule, /TextImportModal/); assert.match(checklistModule, /Icon name="import"/); diff --git a/website/public/data/site.json b/website/public/data/site.json index 0698883..2dee787 100644 --- a/website/public/data/site.json +++ b/website/public/data/site.json @@ -150,6 +150,11 @@ "icon": "calculator", "name": "Calculateur", "description": "Un calculateur avec résultats enregistrables et arborescence pour préparer des chaînes de craft ou de ressources." + }, + { + "icon": "clock", + "name": "Timer", + "description": "Un espace pour préparer chronomètres à étapes et comptes à rebours multiples pendant une session." } ], "limitsTitle": "À retenir", @@ -183,6 +188,10 @@ "importAll": "Importer tout", "exportAll": "Exporter tout", "importOne": "Importer une toolbox", + "preferencesLabel": "Préférences d'interface", + "toastPositionLabel": "Positionnement des alertes", + "toastPositionLeft": "Afficher les alertes en bas à gauche", + "toastPositionRight": "Afficher les alertes en bas à droite", "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.", "storageHelp": { "title": "Sauvegardes locales", @@ -272,6 +281,47 @@ "renameTitle": "Renommer", "deleteTitle": "Supprimer" }, + "timer": { + "stopwatchTab": "Chronomètre", + "countdownTab": "Compte à rebours", + "stopwatchTitle": "Chronomètre", + "countdownTitle": "Compte à rebours", + "startButton": "Lancer", + "pauseButton": "Pause", + "lapButton": "Étape", + "resetTitle": "Réinitialiser", + "lapLabelAriaLabel": "Libellé de l'étape", + "countdownLabel": "Libellé", + "countdownLabelPlaceholder": "Boss, event, craft...", + "countdownTypeLabel": "Type", + "durationType": "Durée", + "dailyTimeType": "Heure précise", + "timePatternType": "Horaire spécifique", + "intervalType": "Intervalle", + "durationLabel": "Durée", + "dailyTimeLabel": "Heure", + "timePatternLabel": "Heure / minute / seconde", + "intervalLabel": "Toutes les", + "addCountdownButton": "Ajouter", + "defaultCountdownLabel": "Timer", + "finishedLabel": "Terminé", + "targetLabel": "Prochaine occurrence", + "stepLabel": "Étape", + "stepsTitle": "Étapes", + "countdownsTitle": "Temps restants", + "scrollableTitle": "Liste scrollable", + "sortTitle": "Trier par temps restant", + "alertModeTitle": "Mode d'alerte", + "alertOffTitle": "Pas d'alerte", + "alertVisibleTitle": "Alerte quand la toolbox est affichée", + "alertSiteTitle": "Alerte sur tout le site", + "alertTooSoonTitle": "Alerte indisponible à moins de 5 minutes", + "alertMessage": "{label} est terminé dans {toolbox}.", + "emptySteps": "Aucune étape enregistrée.", + "emptyCountdowns": "Aucun compte à rebours configuré.", + "renameTitle": "Renommer", + "deleteTitle": "Supprimer" + }, "imageAnnotation": { "addImage": "Ajouter une image", "replaceImage": "Remplacer l'image", diff --git a/website/public/static/icons/hourglass.svg b/website/public/static/icons/hourglass.svg new file mode 100644 index 0000000..d835beb --- /dev/null +++ b/website/public/static/icons/hourglass.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/website/public/static/icons/pause-circle.svg b/website/public/static/icons/pause-circle.svg new file mode 100644 index 0000000..b09a1a9 --- /dev/null +++ b/website/public/static/icons/pause-circle.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/website/public/static/icons/play-circle.svg b/website/public/static/icons/play-circle.svg new file mode 100644 index 0000000..4d2004e --- /dev/null +++ b/website/public/static/icons/play-circle.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/website/public/static/icons/record-circle.svg b/website/public/static/icons/record-circle.svg new file mode 100644 index 0000000..1d881b6 --- /dev/null +++ b/website/public/static/icons/record-circle.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/website/public/static/icons/refresh.svg b/website/public/static/icons/refresh.svg new file mode 100644 index 0000000..5d8c47a --- /dev/null +++ b/website/public/static/icons/refresh.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/website/public/static/icons/sort-time.svg b/website/public/static/icons/sort-time.svg new file mode 100644 index 0000000..35d9130 --- /dev/null +++ b/website/public/static/icons/sort-time.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/website/public/static/icons/sound-max.svg b/website/public/static/icons/sound-max.svg new file mode 100644 index 0000000..4210e5e --- /dev/null +++ b/website/public/static/icons/sound-max.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/website/public/static/icons/sound-min.svg b/website/public/static/icons/sound-min.svg new file mode 100644 index 0000000..ddf3684 --- /dev/null +++ b/website/public/static/icons/sound-min.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/website/public/static/icons/sound-mute.svg b/website/public/static/icons/sound-mute.svg new file mode 100644 index 0000000..d70022a --- /dev/null +++ b/website/public/static/icons/sound-mute.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/website/public/static/icons/stop-circle.svg b/website/public/static/icons/stop-circle.svg new file mode 100644 index 0000000..864bcd4 --- /dev/null +++ b/website/public/static/icons/stop-circle.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/website/public/static/icons/stopwatch.svg b/website/public/static/icons/stopwatch.svg new file mode 100644 index 0000000..c599f3f --- /dev/null +++ b/website/public/static/icons/stopwatch.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/website/public/static/icons/toast-left.svg b/website/public/static/icons/toast-left.svg new file mode 100644 index 0000000..e777120 --- /dev/null +++ b/website/public/static/icons/toast-left.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/website/public/static/icons/toast-right.svg b/website/public/static/icons/toast-right.svg new file mode 100644 index 0000000..3606583 --- /dev/null +++ b/website/public/static/icons/toast-right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/website/src/components/AppOverlays.jsx b/website/src/components/AppOverlays.jsx index fbef409..d22d1a8 100644 --- a/website/src/components/AppOverlays.jsx +++ b/website/src/components/AppOverlays.jsx @@ -19,6 +19,8 @@ export function AppOverlays({ image, setImage, notification, + setNotification, + toastPosition, getGame, toolboxes, links @@ -47,7 +49,7 @@ export function AppOverlays({ }} />} {storageError && setStorageError("")} />} {image && setImage(null)} createMarkerId={() => uid("marker")} />} - {notification && } + {notification && setNotification(null)} />} ); } diff --git a/website/src/components/ImageViewer.jsx b/website/src/components/ImageViewer.jsx index 9ff60b7..54a23bd 100644 --- a/website/src/components/ImageViewer.jsx +++ b/website/src/components/ImageViewer.jsx @@ -147,7 +147,7 @@ export function ImageViewer({ image, onClose, createMarkerId = () => globalThis. onChange={(event) => updateViewerMarker(marker.id, (item) => ({ ...item, label: event.target.value }))} aria-label={`Libellé du marqueur ${index + 1}`} /> - diff --git a/website/src/components/Tabs.jsx b/website/src/components/Tabs.jsx new file mode 100644 index 0000000..61e3287 --- /dev/null +++ b/website/src/components/Tabs.jsx @@ -0,0 +1,25 @@ +// Rôle : fournit un contrôle d'onglets réutilisable avec rôles ARIA. +export function Tabs({ tabs, activeTab, onChange, className = "", iconOnly = false }) { + return ( +
+ {tabs.map((tab) => { + const active = tab.id === activeTab; + return ( + + ); + })} +
+ ); +} diff --git a/website/src/components/ToastPositionSwitch.jsx b/website/src/components/ToastPositionSwitch.jsx new file mode 100644 index 0000000..81104d9 --- /dev/null +++ b/website/src/components/ToastPositionSwitch.jsx @@ -0,0 +1,20 @@ +// Rôle : affiche le switch global de position des notifications internes. +import React from "react"; +import { Icon } from "./Icon.jsx"; + +export function ToastPositionSwitch({ position, labels, onChange }) { + const normalizedPosition = position === "left" ? "left" : "right"; + return ( + + ); +} diff --git a/website/src/components/ToolboxModals.jsx b/website/src/components/ToolboxModals.jsx index e699f72..e62a60e 100644 --- a/website/src/components/ToolboxModals.jsx +++ b/website/src/components/ToolboxModals.jsx @@ -1,5 +1,6 @@ // Rôle : fournit les modales globales liées aux toolboxes et confirmations. import { useEffect, useState } from "react"; +import { Icon } from "./Icon.jsx"; import { lockBodyScroll } from "../utils/bodyScrollLock.js"; function useModalScrollLock() { @@ -23,10 +24,13 @@ export function ConfirmModal({ title, message, confirmLabel = "Confirmer", cance ); } -export function NotificationToast({ message }) { +export function NotificationToast({ message, position = "right", onClose }) { return ( -
- {message} +
+ {message} +
); } diff --git a/website/src/features/toolboxes/ToolboxPages.jsx b/website/src/features/toolboxes/ToolboxPages.jsx index c42d0e5..5133967 100644 --- a/website/src/features/toolboxes/ToolboxPages.jsx +++ b/website/src/features/toolboxes/ToolboxPages.jsx @@ -3,6 +3,7 @@ import React, { useEffect, useRef, useState } from "react"; import { Icon } from "../../components/Icon.jsx"; import { ImportButton } from "../../components/ImportButton.jsx"; import { StorageQuota } from "../../components/StorageQuota.jsx"; +import { ToastPositionSwitch } from "../../components/ToastPositionSwitch.jsx"; import { usePointerReorder } from "../../hooks/usePointerReorder.js"; import { compressImage } from "../../utils/imageCompression.js"; import { getSetting as dbGetSetting, setSetting as dbSetSetting } from "../../utils/indexedDbStorage.js"; @@ -16,6 +17,7 @@ import { normalizeCountersData, normalizeImageAnnotationData, normalizeLinksData, + normalizeTimerData, normalizeUrl, uid } from "./storage/toolboxStorage.js"; @@ -31,7 +33,7 @@ async function copyText(value) { } } -export function ToolboxesPage({ siteContent, toolboxes, getToolboxGame, actions, storageUsage }) { +export function ToolboxesPage({ siteContent, toolboxes, getToolboxGame, actions, storageUsage, toastPosition, setToastPosition }) { const content = siteContent.toolboxes; const { draggingId: draggingToolboxId, @@ -71,6 +73,21 @@ export function ToolboxesPage({ siteContent, toolboxes, getToolboxGame, actions,
+
+
+

{content.preferencesLabel}

+ {content.toastPositionLabel} +
+ +
{toolboxes.length ? toolboxes.map((toolbox) => ( @@ -128,6 +145,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto normalizeCountersData, normalizeCalculatorData, normalizeImageAnnotationData, + normalizeTimerData, normalizeUrl, hostnameFromUrl, copyText, diff --git a/website/src/features/toolboxes/modules/CalculatorModule.jsx b/website/src/features/toolboxes/modules/CalculatorModule.jsx index 1e3cef9..d66b0d7 100644 --- a/website/src/features/toolboxes/modules/CalculatorModule.jsx +++ b/website/src/features/toolboxes/modules/CalculatorModule.jsx @@ -136,23 +136,23 @@ export function CalculatorModule({ toolboxId, moduleId, context }) {
-
+
{activeParent && ( - )} - -