diff --git a/DESIGN_SYSTEM.md b/DESIGN_SYSTEM.md index 8c0aa16..011b0a7 100644 --- a/DESIGN_SYSTEM.md +++ b/DESIGN_SYSTEM.md @@ -280,12 +280,15 @@ Regles UI : - le mode simultane doit etre volontaire, via switch ou depot d'une touche dans une etape existante ; - les categories sont regroupees automatiquement depuis le champ categorie du combo. -### Bloc Notes Riche +### Bloc Note Riche -Le bloc notes 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 : +- pages multiples accessibles par un bouton icone `pages` pose en absolute sur la bordure gauche de l'outil ; +- le bouton ouvre toujours un menu deroulant de pages, y compris en page toolbox complete, avec deroulement de gauche vers la droite ; +- le renommage d'une page passe par un bouton edition qui active l'edition inline du titre ; - toolbar compacte en haut de l'outil ; - boutons courts pour gras, italique, souligne, barre, titre et listes ; - le paragraphe est le style par defaut et n'a pas de bouton dedie ; @@ -296,6 +299,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 + +L'outil Memos sert aux informations courtes type post-it. + +Regles UI : + +- cartes compactes, non imbriquees, avec accent couleur localise ; +- edition directe du texte court ; +- 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 ; +- 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. + ### Calque De Dessin Les annotations dessinees utilisent un canvas transparent superpose au contenu annote. diff --git a/docs/FEATURE_CHECKLIST.md b/docs/FEATURE_CHECKLIST.md index dded614..c6212c2 100644 --- a/docs/FEATURE_CHECKLIST.md +++ b/docs/FEATURE_CHECKLIST.md @@ -15,6 +15,8 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page - Si l'outil porte un état de progression, vérifier que l'import texte repart de l'état initial attendu. - 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. - 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 5ec71bf..22261d6 100644 --- a/docs/STORAGE_SCHEMA.md +++ b/docs/STORAGE_SCHEMA.md @@ -83,42 +83,75 @@ Les associations entre pages jeux et toolboxes sont stockées dans `kv/links`. } ``` -## Outil Bloc Notes +## Outil Bloc Note Type : `notepad` ```json { - "html": "
Notes rapides...
", - "text": "Notes rapides...", - "updatedAt": "2026-07-29T12:00:00.000Z", - "drawingMode": "permanent", - "drawings": { - "strokes": [ - { - "id": "stroke1", - "color": "#f6c453", - "width": 4, - "points": [ - { "x": 12.5, "y": 48 }, - { "x": 24, "y": 52.5 } + "activePageId": "page1", + "pages": [ + { + "id": "page1", + "title": "Page 1", + "html": "Notes rapides...
", + "text": "Notes rapides...", + "updatedAt": "2026-07-29T12:00:00.000Z", + "drawingMode": "permanent", + "drawings": { + "strokes": [ + { + "id": "stroke1", + "color": "#f6c453", + "width": 4, + "points": [ + { "x": 12.5, "y": 48 }, + { "x": 24, "y": 52.5 } + ] + } ] } - ] - } + } + ] } ``` Stockage compact : -- l'ancien format `{ "text": "..." }` reste accepté et est converti en HTML riche au prochain enregistrement ; +- l'ancien format mono-page `{ "html": "...", "text": "..." }` reste accepté et est converti en `pages[0]` au prochain enregistrement ; +- `activePageId` est omis si la première page stockée est active ; - `html` est nettoyé avec une allowlist de balises et de couleurs ; - `text` conserve une version texte pour les exports et les fallbacks ; - `updatedAt` est omis si vide ou invalide ; - `drawingMode` vaut `temporary` par défaut et est omis dans ce cas ; - `drawings` est stocké uniquement si `drawingMode` vaut `permanent` et qu'au moins un trait valide existe ; -- les dessins temporaires du bloc-notes sont conservés dans `sessionStorage`, pas dans IndexedDB ; -- si le texte, la date et les dessins permanents sont vides, l'entrée de module peut être supprimée. +- 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 + +Type : `memos` + +```json +{ + "tickerEnabled": true, + "items": [ + { + "id": "memo1", + "text": "Penser à sauvegarder avant le boss", + "color": "#f6c453", + "updatedAt": "2026-08-21T12:00:00.000Z" + } + ] +} +``` + +Stockage compact : + +- `tickerEnabled` vaut `true` par défaut et est omis dans ce cas ; +- `updatedAt` est omis si vide ou invalide ; +- les mémos sans texte sont ignorés ; +- si aucun mémo n'est valide et que la bande est active par défaut, l'entrée de module peut être supprimée. ## Outil Checklist @@ -878,7 +911,8 @@ Règles communes : Formats : -- `notepad` : format structuré `@notepad`, bloc `@html` / `@endhtml` pour le HTML riche nettoyé, meta `@drawingMode: permanent` et `@drawings: {...}` pour les dessins permanents. Un collage texte simple reste accepté et devient une note sans mise en forme. +- `notepad` : format structuré `@notepad`, blocs `@page: Titre`, `@html` / `@endhtml` pour le HTML riche nettoyé, meta `@drawingMode: permanent` et `@drawings: {...}` pour les dessins permanents. Un collage texte simple reste accepté et devient une page sans mise en forme. +- `memos` : une ligne par mémo, avec préfixe `-` optionnel et suffixe couleur `| color=#f6c453` ; les anciens préfixes `*` sont acceptés puis normalisés. - `checklist` : sections `# Catégorie`, lignes `Item: quantité cible`. - `links` : lignes `Titre: https://...` ou URL seule. - `counters` : lignes `Libellé: valeur`, valeurs négatives acceptées. diff --git a/tests/helpers/data-validation.mjs b/tests/helpers/data-validation.mjs index 3ca3a73..46aeaf6 100644 --- a/tests/helpers/data-validation.mjs +++ b/tests/helpers/data-validation.mjs @@ -130,6 +130,12 @@ export function validateSiteContent(site) { "toolboxes.storageHelp.title", "toolboxes.storageHelp.text", "toolboxes.modules.notepad.placeholder", + "toolboxes.modules.notepad.pagesMenuLabel", + "toolboxes.modules.notepad.pagesToggleLabel", + "toolboxes.modules.notepad.pagesToggleText", + "toolboxes.modules.notepad.addPageLabel", + "toolboxes.modules.notepad.renamePageLabel", + "toolboxes.modules.notepad.deletePageLabel", "toolboxes.modules.notepad.toolbarLabel", "toolboxes.modules.notepad.textFormatLabel", "toolboxes.modules.notepad.boldTitle", @@ -167,6 +173,28 @@ export function validateSiteContent(site) { "toolboxes.modules.notepad.exportSuccess", "toolboxes.modules.notepad.importError", "toolboxes.modules.notepad.exportInfo", + "toolboxes.modules.memos.defaultText", + "toolboxes.modules.memos.placeholder", + "toolboxes.modules.memos.addButton", + "toolboxes.modules.memos.emptyLabel", + "toolboxes.modules.memos.editLabel", + "toolboxes.modules.memos.deleteTitle", + "toolboxes.modules.memos.colorLabel", + "toolboxes.modules.memos.reorderTitle", + "toolboxes.modules.memos.tickerLabel", + "toolboxes.modules.memos.hideTickerTitle", + "toolboxes.modules.memos.showTickerTitle", + "toolboxes.modules.memos.importPlaceholder", + "toolboxes.modules.memos.importOpenButton", + "toolboxes.modules.memos.exportOpenButton", + "toolboxes.modules.memos.importModalTitle", + "toolboxes.modules.memos.exportModalTitle", + "toolboxes.modules.memos.importButton", + "toolboxes.modules.memos.exportButton", + "toolboxes.modules.memos.importSuccess", + "toolboxes.modules.memos.exportSuccess", + "toolboxes.modules.memos.importError", + "toolboxes.modules.memos.exportInfo", "toolboxes.modules.checklist.itemPlaceholder", "toolboxes.modules.checklist.sectionPlaceholder", "toolboxes.modules.checklist.quantityLabel", @@ -621,7 +649,7 @@ export function validateSiteContent(site) { }); const libraryToolDocs = valueAt(site, "library.toolDocs"); - ["notepad", "checklist", "counters", "images", "imageAnnotation", "links", "calculator", "table", "goalTree", "timer", "calendar", "taskPlanner", "combos", "equipmentPlanner"].forEach((toolType) => { + ["memos", "notepad", "checklist", "counters", "images", "imageAnnotation", "links", "calculator", "table", "goalTree", "timer", "calendar", "taskPlanner", "combos", "equipmentPlanner"].forEach((toolType) => { const docs = libraryToolDocs?.[toolType]; assert.equal(typeof docs, "object", `library.toolDocs.${toolType} must be an object`); assert.ok(Array.isArray(docs.controls), `library.toolDocs.${toolType}.controls must be an array`); @@ -772,9 +800,8 @@ export function validateSiteContent(site) { const toolboxCreationTemplate = valueAt(site, "toolboxes.creationTemplate"); assert.equal(typeof toolboxCreationTemplate, "object", "toolboxes.creationTemplate must be an object"); - assertNonEmptyString({ toolboxCreationTemplate }, "toolboxCreationTemplate.notepad.title"); - assertNonEmptyString({ toolboxCreationTemplate }, "toolboxCreationTemplate.notepad.text"); - assertNonEmptyString({ toolboxCreationTemplate }, "toolboxCreationTemplate.notepad.html"); + assertNonEmptyString({ toolboxCreationTemplate }, "toolboxCreationTemplate.memos.title"); + assertNonEmptyString({ toolboxCreationTemplate }, "toolboxCreationTemplate.memos.text"); assertNonEmptyString({ toolboxCreationTemplate }, "toolboxCreationTemplate.links.moduleTitle"); assertNonEmptyString({ toolboxCreationTemplate }, "toolboxCreationTemplate.links.title"); @@ -834,7 +861,7 @@ export function validateLibraryData(payload) { assert.deepEqual(new Set(twoColumnIds), moduleIds, "library.toolbox.moduleOrder.two must contain every module exactly once"); assert.equal(twoColumnIds.length, moduleIds.size, "library.toolbox.moduleOrder.two must not contain duplicate modules"); - ["notepad", "checklist", "images", "links", "counters", "combos", "calculator", "table", "goalTree", "timer", "calendar", "taskPlanner", "equipmentPlanner", "imageAnnotation"].forEach((type) => { + ["memos", "notepad", "checklist", "images", "links", "counters", "combos", "calculator", "table", "goalTree", "timer", "calendar", "taskPlanner", "equipmentPlanner", "imageAnnotation"].forEach((type) => { assert.ok(payload.toolbox.modules.some((module) => module.type === type), `library must include a ${type} example`); }); diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index 5540764..f7d4a5e 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -49,7 +49,7 @@ test("toolbox storage, cards and pages are wired", async () => { assert.match(toolboxActions, /importAllToolboxesPayload/); assert.match(toolboxActions, /async function addImageFiles/); assert.match(toolboxActions, /creationTemplate/); - assert.match(toolboxActions, /type: "notepad"/); + assert.match(toolboxActions, /type: "memos"/); assert.match(toolboxActions, /type: "links"/); assert.match(toolboxActions, /#\/library/); assert.match(`${source}\n${toolboxActions}`, /features\/toolboxes\/storage\/toolboxStorage\.js/); diff --git a/tests/toolbox-modules.test.mjs b/tests/toolbox-modules.test.mjs index d9d1bc1..f16a63f 100644 --- a/tests/toolbox-modules.test.mjs +++ b/tests/toolbox-modules.test.mjs @@ -12,7 +12,7 @@ import { getMonthOccurrenceSegments, getWeekGridLayout, getWeekMinutePosition, l import { appendChecklistItems, moveChecklistItem, moveChecklistItemToCategory, renameChecklistSection } from "../website/src/features/toolboxes/modules/checklistUtils.js"; import { formatUpdatedAt, isHtmlEmpty, queryBlockFormat, queryCommandState } from "../website/src/features/toolboxes/modules/notepadUtils.js"; import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js"; -import { compactModuleDataForStorage, createToolboxExportPayload, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, normalizeModuleOrder, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js"; +import { compactModuleDataForStorage, createToolboxExportPayload, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, normalizeMemosData, normalizeModuleOrder, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeToolbox, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js"; import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js"; function createTextImportContext() { @@ -20,6 +20,7 @@ function createTextImportContext() { return { uid: (prefix) => `${prefix}-${index += 1}`, normalizeNotepadData, + normalizeMemosData, normalizeChecklistData, normalizeLinksData, normalizeCountersData, @@ -124,6 +125,11 @@ test("text exchange round trips simple toolbox modules and resets checklist prog const countersImport = importModuleText("counters", exportModuleText("counters", counters), context); assert.equal(countersImport.ok, true); assert.deepEqual(countersImport.data.counters.map((counter) => [counter.label, counter.value]), [["Win", 12], ["Loss", -2]]); + + const memos = normalizeMemosData({ items: [{ id: "m1", text: "Route", color: "#f6c453" }, { id: "m2", text: "Potion", color: "#22d3ee" }] }); + const memosImport = importModuleText("memos", exportModuleText("memos", memos), context); + assert.equal(memosImport.ok, true); + assert.deepEqual(memosImport.data.items.map((memo) => [memo.text, memo.color]), [["Route", "#f6c453"], ["Potion", "#22d3ee"]]); }); test("notepad helpers keep empty html and invalid command states stable", () => { @@ -952,8 +958,8 @@ test("notepad data migrates text, sanitizes html and compacts drawings", () => { drawingMode: "temporary", drawings: { strokes: normalized.drawings.strokes } }); - assert.equal(compactTemporary.drawings, undefined); - assert.equal(compactTemporary.drawingMode, undefined); + assert.equal(compactTemporary.pages[0].drawings, undefined); + assert.equal(compactTemporary.pages[0].drawingMode, undefined); const compactPermanent = compactModuleDataForStorage("notepad", { html: "Note
", @@ -962,9 +968,9 @@ test("notepad data migrates text, sanitizes html and compacts drawings", () => { drawingMode: "permanent", drawings: normalized.drawings }); - assert.equal(compactPermanent.updatedAt, undefined); - assert.equal(compactPermanent.drawingMode, "permanent"); - assert.equal(compactPermanent.drawings.strokes.length, 1); + assert.equal(compactPermanent.pages[0].updatedAt, undefined); + assert.equal(compactPermanent.pages[0].drawingMode, "permanent"); + assert.equal(compactPermanent.pages[0].drawings.strokes.length, 1); }); test("image annotation storage keeps drawing strokes as percent coordinates", () => { diff --git a/website/public/data/library.json b/website/public/data/library.json index 12a54dc..b8bebf3 100644 --- a/website/public/data/library.json +++ b/website/public/data/library.json @@ -3,10 +3,15 @@ "id": "t1", "name": "Librairie", "modules": [ + { + "id": "m0", + "type": "memos", + "title": "Memos" + }, { "id": "m1", "type": "notepad", - "title": "Notes rapides" + "title": "Bloc Note" }, { "id": "m2", @@ -217,22 +222,2553 @@ } ], "moduleOrder": { - "one": ["m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8", "mf", "m9", "md", "me", "ma", "mb", "mc"], - "two": [["m1", "m2", "m3", "m4", "m5", "m6", "ma"], ["m7", "m8", "mf", "m9", "md", "me", "mb", "mc"]] + "one": [ + "m0", + "m1", + "m2", + "m3", + "m4", + "m5", + "m6", + "m7", + "m8", + "mf", + "m9", + "md", + "me", + "ma", + "mb", + "mc" + ], + "two": [ + [ + "m0", + "m1", + "m2", + "m3", + "m4", + "m5", + "m6", + "ma" + ], + [ + "m7", + "m8", + "mf", + "m9", + "md", + "me", + "mb", + "mc" + ] + ] }, "updatedAt": "2026-08-09T07:34:31.425Z", "moduleColumns": 1 }, "modules": { + "m0": { + "items": [ + { + "id": "w1", + "text": "Pour la reproduction, combine Pierre Stase + Nœud Destin pour transmettre nature et IV.", + "color": "#f6c453", + "updatedAt": "2026-08-09T07:34:31.425Z" + }, + { + "id": "w2", + "text": "Les IV obtenus par Hyper Training ne sont pas transmis aux oeufs.", + "color": "#8b5cf6" + }, + { + "id": "w3", + "text": "Pour une équipe Distorsion, vise 0 IV en Vitesse sur les Pokémon lents.", + "color": "#22d3ee" + } + , + { + "id": "w4", + "text": "Utilise les objets Pouvoir pour cibler précisément les EV à entraîner.", + "color": "#d946ef" + } + ] + }, "m1": { - "html": "Profite bien de ta nouvelle toolbox.
" + "memos": { + "title": "Memos", + "text": "Profite bien de ta nouvelle toolbox." }, "links": { "moduleTitle": "Liens utiles", @@ -1631,7 +1700,7 @@ "target": "moduleTitle", "icon": "edit", "title": "Renommer un outil", - "text": "Cliquez sur le titre d’un outil, comme Bloc notes, pour l’adapter à votre usage." + "text": "Cliquez sur le titre d’un outil, comme Memos, pour l’adapter à votre usage." }, { "target": "deleteTool", @@ -1659,8 +1728,14 @@ }, "modules": { "notepad": { - "placeholder": "Notes rapides...", - "toolbarLabel": "Mise en page du bloc notes", + "placeholder": "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", "textFormatLabel": "Style du texte", "boldTitle": "Gras", "italicTitle": "Italique", @@ -1686,18 +1761,42 @@ "permanentStatus": "Dessins permanents", "updatedAtPrefix": "Modifié le", "sessionStorageError": "Les annotations temporaires n'ont pas pu être sauvegardées.", - "importPlaceholder": "@notepad\n@html\nPréparer les consommables
\n@endhtml\n@drawingMode: permanent\n@drawings: {\"strokes\":[]}", + "importPlaceholder": "@notepad\n@page: Page 1\n@html\nPréparer les consommables
\n@endhtml\n@drawingMode: permanent\n@drawings: {\"strokes\":[]}", "importOpenButton": "Importer du texte", "exportOpenButton": "Exporter en format texte", - "importModalTitle": "Importer un bloc notes", - "exportModalTitle": "Exporter un bloc notes", + "importModalTitle": "Importer un bloc note", + "exportModalTitle": "Exporter un bloc note", "importButton": "Importer le texte", "exportButton": "Copier le texte", - "importSuccess": "Import bloc notes terminé.", - "exportSuccess": "Export bloc notes copié.", - "importError": "Format de bloc notes 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": { + "defaultText": "Nouveau mémo", + "placeholder": "Mémo court...", + "addButton": "Ajouter", + "emptyLabel": "Ajouter un premier mémo", + "editLabel": "Modifier le mémo", + "deleteTitle": "Supprimer le mémo", + "colorLabel": "Couleur du mémo", + "reorderTitle": "Déplacer", + "tickerLabel": "Mémos", + "hideTickerTitle": "Masquer la bande", + "showTickerTitle": "Afficher la bande", + "importPlaceholder": "- Mémo prioritaire | color=#f6c453\n- Mémo simple | color=#22d3ee", + "importOpenButton": "Importer du texte", + "exportOpenButton": "Exporter en format texte", + "importModalTitle": "Importer des mémos", + "exportModalTitle": "Exporter des mémos", + "importButton": "Importer le texte", + "exportButton": "Copier le texte", + "importSuccess": "Import mémos terminé.", + "exportSuccess": "Export mémos copié.", + "importError": "Format de mémos invalide.", + "exportInfo": "Chaque ligne est exportée avec la couleur du mémo au format color=#hex." + }, "checklist": { "itemPlaceholder": "Nouvel item", "sectionPlaceholder": "Catégorie", diff --git a/website/public/static/icons/memos.svg b/website/public/static/icons/memos.svg new file mode 100644 index 0000000..238e628 --- /dev/null +++ b/website/public/static/icons/memos.svg @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/website/public/static/icons/pages.svg b/website/public/static/icons/pages.svg new file mode 100644 index 0000000..d9cdcb8 --- /dev/null +++ b/website/public/static/icons/pages.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/website/src/features/toolboxes/ToolboxPages.jsx b/website/src/features/toolboxes/ToolboxPages.jsx index 2d280a0..82c9eae 100644 --- a/website/src/features/toolboxes/ToolboxPages.jsx +++ b/website/src/features/toolboxes/ToolboxPages.jsx @@ -23,6 +23,7 @@ import { normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, + normalizeMemosData, normalizeModuleOrder, normalizeNotepadData, normalizeTableData, @@ -191,6 +192,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto normalizeChecklistData, normalizeCombosData, normalizeLinksData, + normalizeMemosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, diff --git a/website/src/features/toolboxes/modules/MemosModule.jsx b/website/src/features/toolboxes/modules/MemosModule.jsx new file mode 100644 index 0000000..3b7a1f7 --- /dev/null +++ b/website/src/features/toolboxes/modules/MemosModule.jsx @@ -0,0 +1,192 @@ +// Rôle : fournit l'outil de mémos courts avec post-it, bande déroulante et tri manuel. +import { CompactDropdown } from "../../../components/CompactDropdown.jsx"; +import { Icon } from "../../../components/Icon.jsx"; +import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js"; +import { useEffect, useMemo, useState } from "react"; +import { TextExchangeActions } from "./TextExchangeActions.jsx"; +import { TEXT_COLORS } from "./notepadUtils.js"; + +export function MemosModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) { + const storedData = context.getModuleData(toolboxId, moduleId, { items: [] }); + const data = useMemo(() => context.normalizeMemosData(storedData), [context, storedData]); + const textContent = context.moduleText?.memos || {}; + const tickerItems = data.tickerEnabled ? data.items : []; + const [drafts, setDrafts] = useState({}); + const { + itemReorder, + getItemProps, + isItemDragging, + isItemDropTarget, + getDropPlacement + } = useGroupedReorder({ + namespace: `memos-${moduleId}`, + items: data.items, + getItemId: (memo) => memo.id, + getParentId: () => moduleId, + orientation: "horizontal", + onItemMove: (operation) => saveItems(moveItem(data.items, operation.sourceId, operation.targetId, operation.placement)), + hierarchy: { enabled: true, stickyParents: true } + }); + + useEffect(() => { + setDrafts(Object.fromEntries(data.items.map((item) => [item.id, item.text]))); + }, [data.items]); + + function save(nextData) { + context.setModuleData(toolboxId, moduleId, nextData, "memos"); + } + + function saveItems(items) { + save({ ...data, items }); + } + + function addMemo() { + const now = new Date().toISOString(); + save({ + ...data, + items: [ + { id: context.uid("memo"), text: textContent.defaultText || "Nouveau mémo", color: "#f6c453", updatedAt: now }, + ...data.items + ] + }); + } + + function updateMemo(memoId, updater) { + const now = new Date().toISOString(); + save({ + ...data, + items: data.items.map((item) => item.id === memoId ? { ...updater(item), updatedAt: now } : item) + }); + } + + function deleteMemo(memoId) { + save({ ...data, items: data.items.filter((item) => item.id !== memoId) }); + } + + function commitMemoText(memoId) { + const nextText = String(drafts[memoId] || "").trim(); + if (!nextText) { + deleteMemo(memoId); + return; + } + updateMemo(memoId, (item) => ({ ...item, text: nextText })); + } + + return ( +${welcomeText}
`; + const welcomeText = creationTemplate?.memos?.text || creationTemplate?.notepad?.text || "Profite bien de ta nouvelle toolbox."; const libraryLink = { id: uid("link"), title: creationTemplate?.links?.title || "Librairie des outils", @@ -42,13 +41,13 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM name: name.trim() || "Nouvelle toolbox", updatedAt: now, modules: [ - { id: notepadModuleId, type: "notepad", title: creationTemplate?.notepad?.title || "Bienvenue" }, + { id: memosModuleId, type: "memos", title: creationTemplate?.memos?.title || "Memos" }, { id: linksModuleId, type: "links", title: creationTemplate?.links?.moduleTitle || "Liens utiles" } ] }; if (options.tutorial === true) toolbox.tutorial = true; if (!store.setToolboxes([toolbox, ...store.toolboxes])) return null; - store.updateModuleData(toolbox.id, notepadModuleId, { html: welcomeHtml, text: welcomeText, updatedAt: now }, "notepad"); + store.updateModuleData(toolbox.id, memosModuleId, { items: [{ id: uid("memo"), text: welcomeText, color: "#f6c453", updatedAt: now }] }, "memos"); store.updateModuleData(toolbox.id, linksModuleId, { links: [libraryLink] }, "links"); if (gameId) store.setLinks({ ...store.links, [gameId]: toolbox.id }); return toolbox; diff --git a/website/src/pages/LibraryPage.jsx b/website/src/pages/LibraryPage.jsx index 299ad77..dbcf2ca 100644 --- a/website/src/pages/LibraryPage.jsx +++ b/website/src/pages/LibraryPage.jsx @@ -14,6 +14,7 @@ import { normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, + normalizeMemosData, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, @@ -26,6 +27,7 @@ import { const LIBRARY_TOOLBOX_ID = "library"; const MODULE_COMPONENTS = { + memos: lazy(() => import("../features/toolboxes/modules/MemosModule.jsx").then((module) => ({ default: module.MemosModule }))), notepad: lazy(() => import("../features/toolboxes/modules/NotepadModule.jsx").then((module) => ({ default: module.NotepadModule }))), checklist: lazy(() => import("../features/toolboxes/modules/ChecklistModule.jsx").then((module) => ({ default: module.ChecklistModule }))), images: lazy(() => import("../features/toolboxes/modules/ImagesModule.jsx").then((module) => ({ default: module.ImagesModule }))), @@ -153,6 +155,7 @@ export function LibraryPage({ siteContent, actions }) { normalizeChecklistData, normalizeCombosData, normalizeLinksData, + normalizeMemosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, @@ -344,7 +347,7 @@ function LibraryToolExample({ module, sourceData, description, categoryTitle, do