add multi-page notepad and memos toolbox tool
All checks were successful
Deploy Sokko G / deploy (push) Successful in 16s

This commit is contained in:
Shinuwa 2026-08-25 12:03:11 +02:00
parent fcabbe8029
commit 6125336338
33 changed files with 3787 additions and 138 deletions

View file

@ -280,12 +280,15 @@ Regles UI :
- le mode simultane doit etre volontaire, via switch ou depot d'une touche dans une etape existante ; - 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. - 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 : 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 ; - toolbar compacte en haut de l'outil ;
- boutons courts pour gras, italique, souligne, barre, titre et listes ; - boutons courts pour gras, italique, souligne, barre, titre et listes ;
- le paragraphe est le style par defaut et n'a pas de bouton dedie ; - 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 ; - placeholder integre dans la surface d'edition quand la note est vide ;
- le HTML persiste doit rester nettoye via allowlist, jamais via HTML libre. - 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 ### Calque De Dessin
Les annotations dessinees utilisent un canvas transparent superpose au contenu annote. Les annotations dessinees utilisent un canvas transparent superpose au contenu annote.

View file

@ -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. - 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 la page toolbox complète.
- Vérifier l'affichage dans le panneau latéral. - 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 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 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. - Vérifier le quota de stockage si l'outil manipule des données lourdes.

View file

@ -83,12 +83,17 @@ Les associations entre pages jeux et toolboxes sont stockées dans `kv/links`.
} }
``` ```
## Outil Bloc Notes ## Outil Bloc Note
Type : `notepad` Type : `notepad`
```json ```json
{ {
"activePageId": "page1",
"pages": [
{
"id": "page1",
"title": "Page 1",
"html": "<p>Notes <strong>rapides</strong>...</p>", "html": "<p>Notes <strong>rapides</strong>...</p>",
"text": "Notes rapides...", "text": "Notes rapides...",
"updatedAt": "2026-07-29T12:00:00.000Z", "updatedAt": "2026-07-29T12:00:00.000Z",
@ -106,19 +111,47 @@ Type : `notepad`
} }
] ]
} }
}
]
} }
``` ```
Stockage compact : 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 ; - `html` est nettoyé avec une allowlist de balises et de couleurs ;
- `text` conserve une version texte pour les exports et les fallbacks ; - `text` conserve une version texte pour les exports et les fallbacks ;
- `updatedAt` est omis si vide ou invalide ; - `updatedAt` est omis si vide ou invalide ;
- `drawingMode` vaut `temporary` par défaut et est omis dans ce cas ; - `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 ; - `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 ; - les dessins temporaires du bloc-note sont conservés dans `sessionStorage` par toolbox, outil et page, pas dans IndexedDB ;
- si le texte, la date et les dessins permanents sont vides, l'entrée de module peut être supprimée. - 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 ## Outil Checklist
@ -878,7 +911,8 @@ Règles communes :
Formats : 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`. - `checklist` : sections `# Catégorie`, lignes `Item: quantité cible`.
- `links` : lignes `Titre: https://...` ou URL seule. - `links` : lignes `Titre: https://...` ou URL seule.
- `counters` : lignes `Libellé: valeur`, valeurs négatives acceptées. - `counters` : lignes `Libellé: valeur`, valeurs négatives acceptées.

View file

@ -130,6 +130,12 @@ export function validateSiteContent(site) {
"toolboxes.storageHelp.title", "toolboxes.storageHelp.title",
"toolboxes.storageHelp.text", "toolboxes.storageHelp.text",
"toolboxes.modules.notepad.placeholder", "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.toolbarLabel",
"toolboxes.modules.notepad.textFormatLabel", "toolboxes.modules.notepad.textFormatLabel",
"toolboxes.modules.notepad.boldTitle", "toolboxes.modules.notepad.boldTitle",
@ -167,6 +173,28 @@ export function validateSiteContent(site) {
"toolboxes.modules.notepad.exportSuccess", "toolboxes.modules.notepad.exportSuccess",
"toolboxes.modules.notepad.importError", "toolboxes.modules.notepad.importError",
"toolboxes.modules.notepad.exportInfo", "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.itemPlaceholder",
"toolboxes.modules.checklist.sectionPlaceholder", "toolboxes.modules.checklist.sectionPlaceholder",
"toolboxes.modules.checklist.quantityLabel", "toolboxes.modules.checklist.quantityLabel",
@ -621,7 +649,7 @@ export function validateSiteContent(site) {
}); });
const libraryToolDocs = valueAt(site, "library.toolDocs"); 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]; const docs = libraryToolDocs?.[toolType];
assert.equal(typeof docs, "object", `library.toolDocs.${toolType} must be an object`); 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`); 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"); const toolboxCreationTemplate = valueAt(site, "toolboxes.creationTemplate");
assert.equal(typeof toolboxCreationTemplate, "object", "toolboxes.creationTemplate must be an object"); assert.equal(typeof toolboxCreationTemplate, "object", "toolboxes.creationTemplate must be an object");
assertNonEmptyString({ toolboxCreationTemplate }, "toolboxCreationTemplate.notepad.title"); assertNonEmptyString({ toolboxCreationTemplate }, "toolboxCreationTemplate.memos.title");
assertNonEmptyString({ toolboxCreationTemplate }, "toolboxCreationTemplate.notepad.text"); assertNonEmptyString({ toolboxCreationTemplate }, "toolboxCreationTemplate.memos.text");
assertNonEmptyString({ toolboxCreationTemplate }, "toolboxCreationTemplate.notepad.html");
assertNonEmptyString({ toolboxCreationTemplate }, "toolboxCreationTemplate.links.moduleTitle"); assertNonEmptyString({ toolboxCreationTemplate }, "toolboxCreationTemplate.links.moduleTitle");
assertNonEmptyString({ toolboxCreationTemplate }, "toolboxCreationTemplate.links.title"); 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.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"); 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`); assert.ok(payload.toolbox.modules.some((module) => module.type === type), `library must include a ${type} example`);
}); });

View file

@ -49,7 +49,7 @@ test("toolbox storage, cards and pages are wired", async () => {
assert.match(toolboxActions, /importAllToolboxesPayload/); assert.match(toolboxActions, /importAllToolboxesPayload/);
assert.match(toolboxActions, /async function addImageFiles/); assert.match(toolboxActions, /async function addImageFiles/);
assert.match(toolboxActions, /creationTemplate/); assert.match(toolboxActions, /creationTemplate/);
assert.match(toolboxActions, /type: "notepad"/); assert.match(toolboxActions, /type: "memos"/);
assert.match(toolboxActions, /type: "links"/); assert.match(toolboxActions, /type: "links"/);
assert.match(toolboxActions, /#\/library/); assert.match(toolboxActions, /#\/library/);
assert.match(`${source}\n${toolboxActions}`, /features\/toolboxes\/storage\/toolboxStorage\.js/); assert.match(`${source}\n${toolboxActions}`, /features\/toolboxes\/storage\/toolboxStorage\.js/);

View file

@ -12,7 +12,7 @@ import { getMonthOccurrenceSegments, getWeekGridLayout, getWeekMinutePosition, l
import { appendChecklistItems, moveChecklistItem, moveChecklistItemToCategory, renameChecklistSection } from "../website/src/features/toolboxes/modules/checklistUtils.js"; 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 { formatUpdatedAt, isHtmlEmpty, queryBlockFormat, queryCommandState } from "../website/src/features/toolboxes/modules/notepadUtils.js";
import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.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"; import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js";
function createTextImportContext() { function createTextImportContext() {
@ -20,6 +20,7 @@ function createTextImportContext() {
return { return {
uid: (prefix) => `${prefix}-${index += 1}`, uid: (prefix) => `${prefix}-${index += 1}`,
normalizeNotepadData, normalizeNotepadData,
normalizeMemosData,
normalizeChecklistData, normalizeChecklistData,
normalizeLinksData, normalizeLinksData,
normalizeCountersData, 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); const countersImport = importModuleText("counters", exportModuleText("counters", counters), context);
assert.equal(countersImport.ok, true); assert.equal(countersImport.ok, true);
assert.deepEqual(countersImport.data.counters.map((counter) => [counter.label, counter.value]), [["Win", 12], ["Loss", -2]]); 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", () => { 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", drawingMode: "temporary",
drawings: { strokes: normalized.drawings.strokes } drawings: { strokes: normalized.drawings.strokes }
}); });
assert.equal(compactTemporary.drawings, undefined); assert.equal(compactTemporary.pages[0].drawings, undefined);
assert.equal(compactTemporary.drawingMode, undefined); assert.equal(compactTemporary.pages[0].drawingMode, undefined);
const compactPermanent = compactModuleDataForStorage("notepad", { const compactPermanent = compactModuleDataForStorage("notepad", {
html: "<p>Note</p>", html: "<p>Note</p>",
@ -962,9 +968,9 @@ test("notepad data migrates text, sanitizes html and compacts drawings", () => {
drawingMode: "permanent", drawingMode: "permanent",
drawings: normalized.drawings drawings: normalized.drawings
}); });
assert.equal(compactPermanent.updatedAt, undefined); assert.equal(compactPermanent.pages[0].updatedAt, undefined);
assert.equal(compactPermanent.drawingMode, "permanent"); assert.equal(compactPermanent.pages[0].drawingMode, "permanent");
assert.equal(compactPermanent.drawings.strokes.length, 1); assert.equal(compactPermanent.pages[0].drawings.strokes.length, 1);
}); });
test("image annotation storage keeps drawing strokes as percent coordinates", () => { test("image annotation storage keeps drawing strokes as percent coordinates", () => {

File diff suppressed because it is too large Load diff

View file

@ -60,8 +60,78 @@
"buildsCommands": "Les outils pour documenter des builds, organiser des équipements et conserver des séquences de commandes." "buildsCommands": "Les outils pour documenter des builds, organiser des équipements et conserver des séquences de commandes."
}, },
"toolDocs": { "toolDocs": {
"memos": {
"controls": [
{
"group": "Mémos",
"icon": "add",
"title": "Ajouter un mémo",
"preview": {
"kind": "iconButton",
"icon": "add"
}
},
{
"group": "Mémos",
"icon": "drag",
"title": "Déplacer",
"preview": {
"kind": "iconButton",
"icon": "drag"
}
},
{
"group": "Mémos",
"icon": "dropdown",
"title": "Couleur du mémo",
"preview": {
"kind": "color"
}
},
{
"group": "Bande",
"icon": "eye-open",
"title": "Afficher la bande déroulante",
"preview": {
"kind": "iconButton",
"icon": "eye-open"
}
}
],
"features": [
{
"title": "Infos courtes",
"text": "Post-it rapides pour garder une consigne, une route ou un rappel visible dans la toolbox.",
"details": [
"Chaque mémo reste volontairement court pour compléter le Bloc Note sans le remplacer.",
"L'édition se fait directement dans la carte du mémo."
]
},
{
"title": "Bande déroulante",
"text": "Tous les mémos apparaissent dans une bande compacte quand la bande est active.",
"details": [
"La bande peut être masquée sans retirer les mémos.",
"Les couleurs restent des accents localisés pour conserver une lecture calme."
]
}
],
"importFormat": {
"text": "Chaque ligne crée un mémo. Le suffixe color=#f6c453 conserve la couleur de la carte.",
"example": "- Sauvegarder avant le boss | color=#f6c453\n- Acheter des potions | color=#22d3ee"
}
},
"notepad": { "notepad": {
"controls": [ "controls": [
{
"group": "Pages",
"icon": "pages",
"title": "Affiche le menu des pages",
"preview": {
"kind": "iconButton",
"icon": "pages"
}
},
{ {
"group": "Édition de texte", "group": "Édition de texte",
"icon": "edit", "icon": "edit",
@ -191,8 +261,8 @@
} }
], ],
"importFormat": { "importFormat": {
"text": "Le bloc notes utilise un bloc HTML nettoyé pour conserver la mise en forme autorisée, puis une ligne dessins en JSON compact.", "text": "Le Bloc Note utilise des pages avec blocs HTML nettoyés, puis une ligne dessins en JSON compact si nécessaire.",
"example": "@notepad\n@html\n<h3>Obtenir Mew :</h3><div><ul><li>Obtenir la CS <span style=\"color: #f6c453\">Coupe </span>à l'intérieur du bateau le <span style=\"color: #d946ef\">S.S. Anne</span>.</li><li>Faire exprès de perdre un combat pour réapparaître au Centre Pokémon sans faire partir le navire.</li><li>Revenir sur les quais de <span style=\"color: #d946ef\">Carmin-sur-Mer</span> après avoir appris <span style=\"color: #f6c453\">Surf</span>.</li></ul></div>\n@endhtml\n@drawingMode: permanent\n@drawings: {\"strokes\":[...]}" "example": "@notepad\n@page: Page 1\n@html\n<h3>Obtenir Mew :</h3><div><ul><li>Obtenir la CS <span style=\"color: #f6c453\">Coupe </span>à l'intérieur du bateau le <span style=\"color: #d946ef\">S.S. Anne</span>.</li><li>Faire exprès de perdre un combat pour réapparaître au Centre Pokémon sans faire partir le navire.</li><li>Revenir sur les quais de <span style=\"color: #d946ef\">Carmin-sur-Mer</span> après avoir appris <span style=\"color: #f6c453\">Surf</span>.</li></ul></div>\n@endhtml\n@drawingMode: permanent\n@drawings: {\"strokes\":[...]}"
} }
}, },
"checklist": { "checklist": {
@ -1357,7 +1427,7 @@
}, },
{ {
"title": "Ajoutez vos outils", "title": "Ajoutez vos outils",
"text": "Choisissez seulement ce dont vous avez besoin : bloc notes, checklist, images, liens, timer ou compteur." "text": "Choisissez seulement ce dont vous avez besoin : Memos, Bloc Note, checklist, images, liens, timer ou compteur."
}, },
{ {
"title": "Importez une sauvegarde si vous en avez une", "title": "Importez une sauvegarde si vous en avez une",
@ -1591,10 +1661,9 @@
"toastPositionRight": "Afficher les alertes en bas à droite", "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.", "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": { "creationTemplate": {
"notepad": { "memos": {
"title": "Bienvenue", "title": "Memos",
"text": "Profite bien de ta nouvelle toolbox.", "text": "Profite bien de ta nouvelle toolbox."
"html": "<p>Profite bien de ta nouvelle toolbox.</p>"
}, },
"links": { "links": {
"moduleTitle": "Liens utiles", "moduleTitle": "Liens utiles",
@ -1631,7 +1700,7 @@
"target": "moduleTitle", "target": "moduleTitle",
"icon": "edit", "icon": "edit",
"title": "Renommer un outil", "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", "target": "deleteTool",
@ -1659,8 +1728,14 @@
}, },
"modules": { "modules": {
"notepad": { "notepad": {
"placeholder": "Notes rapides...", "placeholder": "Bloc Note...",
"toolbarLabel": "Mise en page du bloc notes", "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", "textFormatLabel": "Style du texte",
"boldTitle": "Gras", "boldTitle": "Gras",
"italicTitle": "Italique", "italicTitle": "Italique",
@ -1686,18 +1761,42 @@
"permanentStatus": "Dessins permanents", "permanentStatus": "Dessins permanents",
"updatedAtPrefix": "Modifié le", "updatedAtPrefix": "Modifié le",
"sessionStorageError": "Les annotations temporaires n'ont pas pu être sauvegardées.", "sessionStorageError": "Les annotations temporaires n'ont pas pu être sauvegardées.",
"importPlaceholder": "@notepad\n@html\n<h3>Route boss</h3>\n<p><strong style=\"color: #f6c453\">Préparer les consommables</strong></p>\n@endhtml\n@drawingMode: permanent\n@drawings: {\"strokes\":[]}", "importPlaceholder": "@notepad\n@page: Page 1\n@html\n<h3>Route boss</h3>\n<p><strong style=\"color: #f6c453\">Préparer les consommables</strong></p>\n@endhtml\n@drawingMode: permanent\n@drawings: {\"strokes\":[]}",
"importOpenButton": "Importer du texte", "importOpenButton": "Importer du texte",
"exportOpenButton": "Exporter en format texte", "exportOpenButton": "Exporter en format texte",
"importModalTitle": "Importer un bloc notes", "importModalTitle": "Importer un bloc note",
"exportModalTitle": "Exporter un bloc notes", "exportModalTitle": "Exporter un bloc note",
"importButton": "Importer le texte", "importButton": "Importer le texte",
"exportButton": "Copier le texte", "exportButton": "Copier le texte",
"importSuccess": "Import bloc notes terminé.", "importSuccess": "Import bloc note terminé.",
"exportSuccess": "Export bloc notes copié.", "exportSuccess": "Export bloc note copié.",
"importError": "Format de bloc notes invalide.", "importError": "Format de bloc note invalide.",
"exportInfo": "Le HTML est nettoyé à l'import. Les dessins permanents sont exportés en JSON compact." "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": { "checklist": {
"itemPlaceholder": "Nouvel item", "itemPlaceholder": "Nouvel item",
"sectionPlaceholder": "Catégorie", "sectionPlaceholder": "Catégorie",

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 224.045 224.045" xml:space="preserve">
<g>
<path d="M210.436,58.309l-7.416-2.041v-4.139c0-10.749-8.085-19.933-18.834-19.933H148.02V30.23c0-8.148-2.934-15.737-8.565-21.367
C133.74,3.147,126.258,0,118.236,0c-8.041,0-15.576,3.147-21.292,8.863c-4.069,4.068-6.804,9.098-8.055,14.578l-27.511-7.9
c-1.162-0.333-2.356-0.519-3.559-0.519c-5.777,0-10.92,3.643-12.51,9.186l-2.347,7.987h-2.239
c-10.749,0-19.703,9.184-19.703,19.933v55.247L4.822,164.638c-1.976,6.89,1.879,14.103,8.768,16.077l7.43,2.156v4.024
c0,10.749,8.954,19.3,19.703,19.3h62.003l60.593,17.374c1.162,0.333,2.361,0.476,3.565,0.476c5.777,0,10.922-3.982,12.511-9.525
l2.383-8.325h2.408c10.749,0,18.834-8.551,18.834-19.3v-56.47l16.205-56.04C221.201,67.496,217.326,60.283,210.436,58.309z
M102.646,27.164c0.605-2.99,2.147-5.713,4.5-8.067c2.95-2.949,6.903-4.519,11.131-4.52c4.194,0,8.128,1.652,11.078,4.603
c3.011,3.012,4.603,6.58,4.603,10.714l0.003,2.302h-13.021L102.646,27.164z M42.02,53.195h46.159l0.032,19.179l0.011,0.353
c0.418,4.989,2.401,9.749,5.957,13.303c4.019,4.02,8.841,6.259,14.841,6.259v0.013c6-0.001,11.223-2.21,15.245-6.233
c4.022-4.022,6.341-9.36,6.341-15.057c-0.046-5.942-0.111-7.59-0.196-8.185c-0.383-3.696-3.408-6.466-7.128-6.466
c-3.723,0-6.759,2.771-7.139,6.47c-0.086,0.609-0.169,2.248-0.072,8.207c0,1.77-0.689,3.436-1.946,4.693
c-1.258,1.258-2.924,1.95-4.694,1.951c-1.859,0-3.863-0.728-5.082-1.946c-1.375-1.376-2.33-3.081-2.33-4.678V53.195h80v132h-140
V53.195z"/>
<path d="M171.02,116.437c0-6.761-5.48-12.241-12.24-12.241H66.26c-6.76,0-12.24,5.481-12.24,12.241v0.518
c0,6.761,5.48,12.241,12.24,12.241h92.52c6.76,0,12.24-5.481,12.24-12.241V116.437z"/>
<path d="M171.02,156.437c0-6.761-5.48-12.241-12.24-12.241H66.26c-6.76,0-12.24,5.481-12.24,12.241v0.518
c0,6.761,5.48,12.241,12.24,12.241h92.52c6.76,0,12.24-5.481,12.24-12.241V156.437z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;stroke:#020202;stroke-miterlimit:10;stroke-width:1.91px;}</style></defs><line class="cls-1" x1="6.27" y1="13.91" x2="13.91" y2="13.91"/><line class="cls-1" x1="6.27" y1="10.09" x2="13.91" y2="10.09"/><line class="cls-1" x1="6.27" y1="17.73" x2="13.91" y2="17.73"/><polygon class="cls-1" points="16.77 8.18 16.77 22.5 3.41 22.5 3.41 5.32 16.77 5.32 16.77 8.18"/><polyline class="cls-1" points="16.77 18.68 20.59 18.68 20.59 4.36 20.59 1.5 7.23 1.5 7.23 5.32"/></svg>

After

Width:  |  Height:  |  Size: 736 B

View file

@ -23,6 +23,7 @@ import {
normalizeGoalTreeData, normalizeGoalTreeData,
normalizeImageAnnotationData, normalizeImageAnnotationData,
normalizeLinksData, normalizeLinksData,
normalizeMemosData,
normalizeModuleOrder, normalizeModuleOrder,
normalizeNotepadData, normalizeNotepadData,
normalizeTableData, normalizeTableData,
@ -191,6 +192,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
normalizeChecklistData, normalizeChecklistData,
normalizeCombosData, normalizeCombosData,
normalizeLinksData, normalizeLinksData,
normalizeMemosData,
normalizeCountersData, normalizeCountersData,
normalizeEquipmentPlannerData, normalizeEquipmentPlannerData,
normalizeGoalTreeData, normalizeGoalTreeData,

View file

@ -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 (
<div className="memos-module">
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="memos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
</div>
)}
{editing && (
<div className="module-add-panel">
<button className="primary memos-add-button" type="button" onClick={addMemo}>
<Icon name="add" />
<span>{textContent.addButton || "Ajouter"}</span>
</button>
</div>
)}
<div className="memos-toolbar">
<button
className={`memos-ticker-toggle ${data.tickerEnabled ? "active" : ""}`}
type="button"
onClick={() => save({ ...data, tickerEnabled: !data.tickerEnabled })}
aria-pressed={data.tickerEnabled}
title={data.tickerEnabled ? textContent.hideTickerTitle || "Masquer la bande" : textContent.showTickerTitle || "Afficher la bande"}
>
<Icon name={data.tickerEnabled ? "eye-open" : "eye-closed"} />
</button>
{tickerItems.length > 0 && (
<div className="memos-ticker" aria-label={textContent.tickerLabel || "Mémos"}>
<div className="memos-ticker-track">
{[...tickerItems, ...tickerItems].map((memo, index) => (
<span className="memos-ticker-item" key={`${memo.id}-${index}`} style={{ "--memo-color": memo.color }}>
{memo.text}
</span>
))}
</div>
</div>
)}
</div>
<div className="memos-list">
{data.items.length ? data.items.map((memo) => {
const className = [
"memo-card",
isItemDragging(memo.id) ? "is-dragging" : "",
isItemDropTarget(memo.id) ? "is-drop-target" : "",
getDropPlacement("item", memo.id) === "after" ? "drop-after" : ""
].filter(Boolean).join(" ");
return (
<article className={className} key={memo.id} style={{ "--memo-color": memo.color }} {...getItemProps({ itemId: memo.id, parentId: moduleId })}>
<button
className="memo-drag-handle"
type="button"
onPointerDown={(event) => itemReorder.startDrag(event, memo.id)}
aria-label={`${textContent.reorderTitle || "Déplacer"} ${memo.text}`}
title={textContent.reorderTitle || "Déplacer"}
>
<Icon name="drag" />
</button>
<textarea
value={drafts[memo.id] ?? memo.text}
rows={3}
placeholder={textContent.placeholder || "Mémo court..."}
onChange={(event) => setDrafts((current) => ({ ...current, [memo.id]: event.target.value }))}
onBlur={() => commitMemoText(memo.id)}
aria-label={textContent.editLabel || "Modifier le mémo"}
/>
<footer>
<CompactDropdown
className="memo-color-dropdown notepad-color-dropdown"
menuClassName="memo-color-menu notepad-color-menu"
label={textContent.colorLabel || "Couleur du mémo"}
renderTrigger={({ toggle }) => (
<button
className="notepad-color-toggle"
type="button"
style={{ "--swatch-color": memo.color }}
onClick={toggle}
title={textContent.colorLabel || "Couleur du mémo"}
aria-label={textContent.colorLabel || "Couleur du mémo"}
/>
)}
>
{({ close }) => (
<>
{TEXT_COLORS.map((color) => (
<button
key={color}
className={`notepad-swatch ${memo.color === color ? "active" : ""}`}
type="button"
style={{ "--swatch-color": color }}
onClick={() => {
updateMemo(memo.id, (item) => ({ ...item, color }));
close();
}}
aria-label={`${textContent.colorLabel || "Couleur"} ${color}`}
/>
))}
</>
)}
</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"}>
<Icon name="trash" />
</button>
</footer>
</article>
);
}) : (
<button className="memos-empty" type="button" onClick={addMemo}>
<Icon name="add" />
<span>{textContent.emptyLabel || "Ajouter un premier mémo"}</span>
</button>
)}
</div>
</div>
);
}

View file

@ -1,5 +1,7 @@
// Rôle : fournit l'outil bloc-notes riche avec annotations dessinées. // Rôle : fournit l'outil Bloc Note multi-pages avec annotations dessinées.
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { CompactDropdown } from "../../../components/CompactDropdown.jsx";
import { Icon } from "../../../components/Icon.jsx";
import { DrawingControls } from "./DrawingControls.jsx"; import { DrawingControls } from "./DrawingControls.jsx";
import { DrawingOverlay } from "./DrawingOverlay.jsx"; import { DrawingOverlay } from "./DrawingOverlay.jsx";
import { NotepadToolbar } from "./NotepadToolbar.jsx"; import { NotepadToolbar } from "./NotepadToolbar.jsx";
@ -21,21 +23,24 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
const storedData = context.getModuleData(toolboxId, moduleId, { text: "" }); const storedData = context.getModuleData(toolboxId, moduleId, { text: "" });
const normalizedData = useMemo(() => context.normalizeNotepadData(storedData), [context, storedData]); const normalizedData = useMemo(() => context.normalizeNotepadData(storedData), [context, storedData]);
const textContent = context.moduleText?.notepad || {}; const textContent = context.moduleText?.notepad || {};
const activePage = normalizedData.pages.find((page) => page.id === normalizedData.activePageId) || normalizedData.pages[0];
const editorRef = useRef(null); const editorRef = useRef(null);
const surfaceRef = useRef(null); const surfaceRef = useRef(null);
const selectionOffsetRef = useRef(null); const selectionOffsetRef = useRef(null);
const localSaveUpdatedAtRef = useRef(""); const localSaveUpdatedAtRef = useRef("");
const moduleKeyRef = useRef(`${toolboxId}:${moduleId}`); const pageKeyRef = useRef(`${toolboxId}:${moduleId}:${activePage.id}`);
const [html, setHtml] = useState(normalizedData.html); const [html, setHtml] = useState(activePage.html);
const [text, setText] = useState(normalizedData.text); const [text, setText] = useState(activePage.text);
const [updatedAt, setUpdatedAt] = useState(normalizedData.updatedAt); const [updatedAt, setUpdatedAt] = useState(activePage.updatedAt);
const [drawingMode, setDrawingMode] = useState(normalizedData.drawingMode); const [drawingMode, setDrawingMode] = useState(activePage.drawingMode);
const [drawingActive, setDrawingActive] = useState(false); const [drawingActive, setDrawingActive] = useState(false);
const [eraseActive, setEraseActive] = useState(false); const [eraseActive, setEraseActive] = useState(false);
const [drawingColor, setDrawingColor] = useState("#f6c453"); const [drawingColor, setDrawingColor] = useState("#f6c453");
const [drawingWidth, setDrawingWidth] = useState(4); const [drawingWidth, setDrawingWidth] = useState(4);
const [textColor, setTextColor] = useState(TEXT_COLORS[0]); const [textColor, setTextColor] = useState(TEXT_COLORS[0]);
const [highlightColor, setHighlightColor] = useState(HIGHLIGHT_COLORS[0]); const [highlightColor, setHighlightColor] = useState(HIGHLIGHT_COLORS[0]);
const [editingPageId, setEditingPageId] = useState("");
const [pageTitleDrafts, setPageTitleDrafts] = useState({});
const [activeFormats, setActiveFormats] = useState({ const [activeFormats, setActiveFormats] = useState({
bold: false, bold: false,
italic: false, italic: false,
@ -45,30 +50,34 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
bulletList: false, bulletList: false,
numberList: false numberList: false
}); });
const sessionKey = `sokkog:notepad-drawings:${toolboxId}:${moduleId}`; const sessionKey = `sokkog:notepad-drawings:${toolboxId}:${moduleId}:${activePage.id}`;
const [temporaryStrokes, setTemporaryStrokes] = useState([]); const [temporaryStrokes, setTemporaryStrokes] = useState([]);
useEffect(() => { useEffect(() => {
const moduleKey = `${toolboxId}:${moduleId}`; setPageTitleDrafts(Object.fromEntries(normalizedData.pages.map((page) => [page.id, page.title])));
if (moduleKeyRef.current !== moduleKey) { }, [normalizedData.pages]);
moduleKeyRef.current = moduleKey;
useEffect(() => {
const pageKey = `${toolboxId}:${moduleId}:${activePage.id}`;
if (pageKeyRef.current !== pageKey) {
pageKeyRef.current = pageKey;
localSaveUpdatedAtRef.current = ""; localSaveUpdatedAtRef.current = "";
} }
if (normalizedData.updatedAt && normalizedData.updatedAt === localSaveUpdatedAtRef.current) return; if (activePage.updatedAt && activePage.updatedAt === localSaveUpdatedAtRef.current) return;
const editor = editorRef.current; const editor = editorRef.current;
if (document.activeElement === editor && getPlainText(editor) === normalizedData.text) { if (document.activeElement === editor && getPlainText(editor) === activePage.text) {
setText(normalizedData.text); setText(activePage.text);
setUpdatedAt(normalizedData.updatedAt); setUpdatedAt(activePage.updatedAt);
setDrawingMode(normalizedData.drawingMode); setDrawingMode(activePage.drawingMode);
return; return;
} }
setHtml(normalizedData.html); setHtml(activePage.html);
setText(normalizedData.text); setText(activePage.text);
setUpdatedAt(normalizedData.updatedAt); setUpdatedAt(activePage.updatedAt);
setDrawingMode(normalizedData.drawingMode); setDrawingMode(activePage.drawingMode);
setDrawingActive(false); setDrawingActive(false);
setEraseActive(false); setEraseActive(false);
}, [toolboxId, moduleId, normalizedData.html, normalizedData.text, normalizedData.updatedAt, normalizedData.drawingMode]); }, [toolboxId, moduleId, activePage.id, activePage.html, activePage.text, activePage.updatedAt, activePage.drawingMode]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (!editorRef.current || editorRef.current.innerHTML === html) return; if (!editorRef.current || editorRef.current.innerHTML === html) return;
@ -121,48 +130,57 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
} }
}, [context, drawingMode, sessionKey, temporaryStrokes, textContent.sessionStorageError]); }, [context, drawingMode, sessionKey, temporaryStrokes, textContent.sessionStorageError]);
const activeStrokes = drawingMode === "permanent" ? normalizedData.drawings.strokes : temporaryStrokes; const activeStrokes = drawingMode === "permanent" ? activePage.drawings.strokes : temporaryStrokes;
const formattedUpdatedAt = formatUpdatedAt(updatedAt); const formattedUpdatedAt = formatUpdatedAt(updatedAt);
function saveNotepad(nextData) {
context.setModuleData(toolboxId, moduleId, nextData, "notepad");
}
function updateActivePage(updater, nextActivePageId = normalizedData.activePageId) {
const pages = normalizedData.pages.map((page) => page.id === activePage.id ? updater(page) : page);
saveNotepad({ ...normalizedData, activePageId: nextActivePageId, pages });
}
function saveContent(nextHtml, nextText) { function saveContent(nextHtml, nextText) {
const nextUpdatedAt = new Date().toISOString(); const nextUpdatedAt = new Date().toISOString();
localSaveUpdatedAtRef.current = nextUpdatedAt; localSaveUpdatedAtRef.current = nextUpdatedAt;
setHtml(nextHtml); setHtml(nextHtml);
setText(nextText); setText(nextText);
setUpdatedAt(nextUpdatedAt); setUpdatedAt(nextUpdatedAt);
context.setModuleData(toolboxId, moduleId, { updateActivePage((page) => ({
...normalizedData, ...page,
html: nextHtml, html: nextHtml,
text: nextText, text: nextText,
updatedAt: nextUpdatedAt, updatedAt: nextUpdatedAt,
drawingMode, drawingMode,
drawings: drawingMode === "permanent" ? { strokes: activeStrokes } : { strokes: [] } drawings: drawingMode === "permanent" ? { strokes: activeStrokes } : { strokes: [] }
}); }));
} }
function saveDrawingMode(nextMode, nextStrokes) { function saveDrawingMode(nextMode, nextStrokes) {
setDrawingMode(nextMode); setDrawingMode(nextMode);
if (nextMode === "temporary") setTemporaryStrokes(nextStrokes); if (nextMode === "temporary") setTemporaryStrokes(nextStrokes);
context.setModuleData(toolboxId, moduleId, { updateActivePage((page) => ({
...normalizedData, ...page,
html, html,
text, text,
updatedAt, updatedAt,
drawingMode: nextMode, drawingMode: nextMode,
drawings: nextMode === "permanent" ? { strokes: nextStrokes } : { strokes: [] } drawings: nextMode === "permanent" ? { strokes: nextStrokes } : { strokes: [] }
}); }));
} }
function saveStrokes(nextStrokes) { function saveStrokes(nextStrokes) {
if (drawingMode === "permanent") { if (drawingMode === "permanent") {
context.setModuleData(toolboxId, moduleId, { updateActivePage((page) => ({
...normalizedData, ...page,
html, html,
text, text,
updatedAt, updatedAt,
drawingMode, drawingMode,
drawings: { strokes: nextStrokes } drawings: { strokes: nextStrokes }
}); }));
} else { } else {
setTemporaryStrokes(nextStrokes); setTemporaryStrokes(nextStrokes);
} }
@ -211,9 +229,10 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
const getSurfaceSize = useCallback(() => { const getSurfaceSize = useCallback(() => {
const element = surfaceRef.current; const element = surfaceRef.current;
const editor = editorRef.current;
return { return {
width: element?.clientWidth || 0, width: element?.clientWidth || 0,
height: Math.max(element?.scrollHeight || 0, element?.clientHeight || 0) height: Math.max(editor?.scrollHeight || 0, element?.clientHeight || 0)
}; };
}, []); }, []);
@ -230,6 +249,39 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
saveStrokes([]); saveStrokes([]);
} }
function selectPage(pageId) {
if (pageId === normalizedData.activePageId) return;
saveNotepad({ ...normalizedData, activePageId: pageId });
}
function addPage(close) {
const nextPage = context.normalizeNotepadData({ pages: [{ id: context.uid("page"), title: `Page ${normalizedData.pages.length + 1}` }] }).pages[0];
saveNotepad({
...normalizedData,
activePageId: nextPage.id,
pages: [...normalizedData.pages, nextPage]
});
close();
}
function renamePage(pageId) {
const nextTitle = String(pageTitleDrafts[pageId] || "").trim();
if (!nextTitle) return;
saveNotepad({
...normalizedData,
pages: normalizedData.pages.map((page) => page.id === pageId ? { ...page, title: nextTitle } : page)
});
setEditingPageId("");
}
function deletePage(pageId, close) {
if (normalizedData.pages.length <= 1) return;
const pages = normalizedData.pages.filter((page) => page.id !== pageId);
const activePageId = pageId === normalizedData.activePageId ? pages[0].id : normalizedData.activePageId;
saveNotepad({ ...normalizedData, activePageId, pages });
if (pageId === normalizedData.activePageId) close();
}
return ( return (
<div className="notepad-module"> <div className="notepad-module">
{textExchangeOpen && ( {textExchangeOpen && (
@ -237,6 +289,88 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
<TextExchangeActions type="notepad" data={normalizedData} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "notepad")} /> <TextExchangeActions type="notepad" data={normalizedData} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "notepad")} />
</div> </div>
)} )}
<CompactDropdown
className="notepad-pages-dropdown"
menuClassName="notepad-pages-menu"
label={textContent.pagesMenuLabel || "Pages du bloc note"}
renderTrigger={({ open, toggle }) => (
<button
className={`notepad-pages-toggle ${open ? "active" : ""}`}
type="button"
onClick={toggle}
aria-label={textContent.pagesToggleLabel || "Afficher les pages du bloc note"}
aria-expanded={open}
title={textContent.pagesToggleLabel || "Afficher les pages du bloc note"}
>
<Icon name="pages" />
</button>
)}
>
{({ close }) => (
<>
<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">
{editingPageId === page.id ? (
<input
className="notepad-page-title-input"
value={pageTitleDrafts[page.id] ?? page.title}
aria-label={`${textContent.renamePageLabel || "Renommer"} ${page.title}`}
autoFocus
onChange={(event) => setPageTitleDrafts((current) => ({ ...current, [page.id]: event.target.value }))}
onBlur={() => renamePage(page.id)}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
renamePage(page.id);
}
if (event.key === "Escape") {
setPageTitleDrafts((current) => ({ ...current, [page.id]: page.title }));
setEditingPageId("");
}
}}
/>
) : (
<button type="button" className="notepad-page-select" onClick={() => { selectPage(page.id); close(); }}>
{page.title}
</button>
)}
</div>
<div className="notepad-page-actions">
<button
className="notepad-page-edit"
type="button"
onClick={() => {
setPageTitleDrafts((current) => ({ ...current, [page.id]: page.title }));
setEditingPageId(page.id);
}}
aria-label={`${textContent.renamePageLabel || "Renommer"} ${page.title}`}
title={textContent.renamePageLabel || "Renommer"}
>
<Icon name="edit" />
</button>
<button
className="notepad-page-delete danger"
type="button"
onClick={() => deletePage(page.id, close)}
disabled={normalizedData.pages.length <= 1}
aria-label={`${textContent.deletePageLabel || "Supprimer"} ${page.title}`}
title={textContent.deletePageLabel || "Supprimer la page"}
>
<Icon name="trash" />
</button>
</div>
</div>
))}
</div>
<button className="notepad-page-add" type="button" onClick={() => addPage(close)}>
<Icon name="add" />
<span>{textContent.addPageLabel || "Ajouter une page"}</span>
</button>
</>
)}
</CompactDropdown>
<NotepadToolbar <NotepadToolbar
textContent={textContent} textContent={textContent}
activeFormats={activeFormats} activeFormats={activeFormats}
@ -258,7 +392,7 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
suppressContentEditableWarning suppressContentEditableWarning
role="textbox" role="textbox"
aria-multiline="true" aria-multiline="true"
data-placeholder={textContent.placeholder || "Notes rapides..."} data-placeholder={textContent.placeholder || "Bloc Note..."}
onInput={handleEditorInput} onInput={handleEditorInput}
onPaste={handlePaste} onPaste={handlePaste}
/> />

View file

@ -15,7 +15,7 @@ export function NotepadToolbar({
onHighlightColorApply onHighlightColorApply
}) { }) {
return ( return (
<div className="notepad-toolbar" aria-label={textContent.toolbarLabel || "Mise en page du bloc notes"}> <div className="notepad-toolbar" aria-label={textContent.toolbarLabel || "Mise en page du bloc note"}>
<div className="notepad-format-controls"> <div className="notepad-format-controls">
<div className="notepad-toolbar-group" role="group" aria-label={textContent.textFormatLabel || "Style du texte"}> <div className="notepad-toolbar-group" role="group" aria-label={textContent.textFormatLabel || "Style du texte"}>
<ToolbarButton active={activeFormats.bold} title={textContent.boldTitle || "Gras"} onClick={() => onCommand("bold")}>B</ToolbarButton> <ToolbarButton active={activeFormats.bold} title={textContent.boldTitle || "Gras"} onClick={() => onCommand("bold")}>B</ToolbarButton>

View file

@ -199,7 +199,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
<span className="module-icon" aria-hidden="true"> <span className="module-icon" aria-hidden="true">
<span className={`module-icon-svg module-icon-${definition.icon || "notepad"}`} /> <span className={`module-icon-svg module-icon-${definition.icon || "notepad"}`} />
</span> </span>
<EditableModuleTitle value={module.title || label} fallback={label} tutorialTarget={module.type === "notepad" ? "moduleTitle" : ""} onSave={(title) => onRename(module.id, title)} /> <EditableModuleTitle value={module.title || label} fallback={label} tutorialTarget={module.type === "memos" ? "moduleTitle" : ""} onSave={(title) => onRename(module.id, title)} />
</div> </div>
<div> <div>
{definition.scrollable && ( {definition.scrollable && (

View file

@ -1,4 +1,4 @@
// Rôle : regroupe les constantes et helpers DOM du bloc-notes riche. // Rôle : regroupe les constantes et helpers DOM du Bloc Note riche.
export const TEXT_COLORS = ["#f5f7ff", "#b4bdd3", "#f6c453", "#22d3ee", "#8b5cf6", "#d946ef"]; export const TEXT_COLORS = ["#f5f7ff", "#b4bdd3", "#f6c453", "#22d3ee", "#8b5cf6", "#d946ef"];
export const HIGHLIGHT_COLORS = ["#f6c453", "#22d3ee", "#8b5cf6", "#d946ef", "#202745"]; export const HIGHLIGHT_COLORS = ["#f6c453", "#22d3ee", "#8b5cf6", "#d946ef", "#202745"];
export const DRAWING_WIDTHS = [2, 4, 8, 12]; export const DRAWING_WIDTHS = [2, 4, 8, 12];

View file

@ -4,12 +4,14 @@ import {
exportChecklistText, exportChecklistText,
exportCountersText, exportCountersText,
exportLinksText, exportLinksText,
exportMemosText,
exportNotepadText, exportNotepadText,
exportTableTsv, exportTableTsv,
importCalculatorText, importCalculatorText,
importChecklistText, importChecklistText,
importCountersText, importCountersText,
importLinksText, importLinksText,
importMemosText,
importNotepadText, importNotepadText,
importTableTsv importTableTsv
} from "./textImport/basic.js"; } from "./textImport/basic.js";
@ -24,10 +26,15 @@ export { parseColonImportLines };
const TEXT_EXCHANGE_HANDLERS = { const TEXT_EXCHANGE_HANDLERS = {
notepad: { notepad: {
hasContent: (data) => Boolean(data?.text || data?.html || data?.drawings?.strokes?.length), hasContent: (data) => Boolean(data?.pages?.some((page) => page.text || page.html || page.drawings?.strokes?.length) || data?.text || data?.html || data?.drawings?.strokes?.length),
exportText: exportNotepadText, exportText: exportNotepadText,
importText: importNotepadText importText: importNotepadText
}, },
memos: {
hasContent: (data) => Boolean(data?.items?.length),
exportText: exportMemosText,
importText: importMemosText
},
checklist: { checklist: {
hasContent: (data) => Boolean(data?.sections?.some((section) => section.items?.length)), hasContent: (data) => Boolean(data?.sections?.some((section) => section.items?.length)),
exportText: exportChecklistText, exportText: exportChecklistText,

View file

@ -3,10 +3,15 @@ import { cellAddress, columnIndexToName } from "../tableFormulaEngine.js";
import { cloneDrawingData, lineError, parseBoolean, parseColonImportLines, parseInteger, parseMetaLine, parseNumber, splitLines, textToNotepadExchangeHtml } from "./shared.js"; import { cloneDrawingData, lineError, parseBoolean, parseColonImportLines, parseInteger, parseMetaLine, parseNumber, splitLines, textToNotepadExchangeHtml } from "./shared.js";
export function exportNotepadText(data) { export function exportNotepadText(data) {
const html = String(data?.html || "").trim() || textToNotepadExchangeHtml(data?.text || ""); const pages = Array.isArray(data?.pages) && data.pages.length ? data.pages : [data];
const lines = ["@notepad", "@html", html, "@endhtml"]; const lines = ["@notepad"];
if (data?.drawingMode === "permanent") lines.push("@drawingMode: permanent"); pages.forEach((page, index) => {
if (data?.drawings?.strokes?.length) lines.push(`@drawings: ${JSON.stringify(cloneDrawingData(data.drawings))}`); const html = String(page?.html || "").trim() || textToNotepadExchangeHtml(page?.text || "");
lines.push(`@page: ${page?.title || `Page ${index + 1}`}`);
lines.push("@html", html, "@endhtml");
if (page?.drawingMode === "permanent") lines.push("@drawingMode: permanent");
if (page?.drawings?.strokes?.length) lines.push(`@drawings: ${JSON.stringify(cloneDrawingData(page.drawings))}`);
});
return lines.join("\n"); return lines.join("\n");
} }
@ -38,16 +43,34 @@ export function importNotepadText(text, context) {
let readingHtml = false; let readingHtml = false;
let htmlWasOpened = false; let htmlWasOpened = false;
let htmlWasClosed = false;
const htmlLines = []; const htmlLines = [];
let drawingMode = "temporary"; const pages = [];
let drawings = { strokes: [] }; let currentPage = {
title: "Page 1",
drawingMode: "temporary",
drawings: { strokes: [] }
};
function flushPage() {
if (!htmlWasOpened && !currentPage.drawings.strokes.length && currentPage.drawingMode !== "permanent") return;
pages.push({
...currentPage,
html: htmlLines.join("\n").trim()
});
htmlLines.length = 0;
htmlWasOpened = false;
currentPage = {
title: `Page ${pages.length + 1}`,
drawingMode: "temporary",
drawings: { strokes: [] }
};
}
lines.forEach((line, index) => { lines.forEach((line, index) => {
const cleanLine = line.trim(); const cleanLine = line.trim();
if (readingHtml) { if (readingHtml) {
if (cleanLine === "@endhtml") { if (cleanLine === "@endhtml") {
readingHtml = false; readingHtml = false;
htmlWasClosed = true;
return; return;
} }
htmlLines.push(line); htmlLines.push(line);
@ -61,24 +84,55 @@ export function importNotepadText(text, context) {
return; return;
} }
const meta = parseMetaLine(cleanLine); const meta = parseMetaLine(cleanLine);
if (meta?.key === "page") {
flushPage();
currentPage.title = meta.value || `Page ${pages.length + 1}`;
return;
}
if (meta?.key === "drawingMode") { if (meta?.key === "drawingMode") {
if (!["temporary", "permanent"].includes(meta.value)) throw lineError(index, "mode de dessin invalide."); if (!["temporary", "permanent"].includes(meta.value)) throw lineError(index, "mode de dessin invalide.");
drawingMode = meta.value; currentPage.drawingMode = meta.value;
return; return;
} }
if (meta?.key === "drawings") { if (meta?.key === "drawings") {
try { try {
drawings = cloneImportedNotepadDrawings(JSON.parse(meta.value), context); currentPage.drawings = cloneImportedNotepadDrawings(JSON.parse(meta.value), context);
} catch (error) { } catch (error) {
throw lineError(index, error?.message || "dessins invalides."); throw lineError(index, error?.message || "dessins invalides.");
} }
if (drawings.strokes.length) drawingMode = "permanent"; if (currentPage.drawings.strokes.length) currentPage.drawingMode = "permanent";
return; return;
} }
throw lineError(index, `ligne bloc notes invalide: ${cleanLine}`); throw lineError(index, `ligne bloc note invalide: ${cleanLine}`);
}); });
if (readingHtml || (htmlWasOpened && !htmlWasClosed)) throw new Error("Bloc HTML non terminé."); if (readingHtml) throw new Error("Bloc HTML non terminé.");
return context.normalizeNotepadData({ html: htmlLines.join("\n").trim(), drawingMode, drawings }); flushPage();
return context.normalizeNotepadData({ pages });
}
export function exportMemosText(data) {
return (data.items || []).map((memo) => `- ${memo.text} | color=${memo.color || "#f6c453"}`).join("\n");
}
export function importMemosText(text, context) {
const items = [];
splitLines(text).forEach((line) => {
const cleanLine = line.trim();
if (!cleanLine) return;
const memoLine = cleanLine.replace(/^[-*]\s*/, "").trim();
const colorMatch = memoLine.match(/\s+\|\s*color=(#[0-9a-f]{6})\s*$/i);
const label = (colorMatch ? memoLine.slice(0, colorMatch.index) : memoLine).trim();
if (!label) return;
items.push({
id: context.uid("memo"),
text: label,
color: colorMatch?.[1] || "#f6c453",
updatedAt: new Date().toISOString()
});
});
return context.normalizeMemosData({ items });
} }

View file

@ -8,6 +8,7 @@ import { CountersModule } from "./CountersModule.jsx";
import { ImageAnnotationModule } from "./ImageAnnotationModule.jsx"; import { ImageAnnotationModule } from "./ImageAnnotationModule.jsx";
import { ImagesModule } from "./ImagesModule.jsx"; import { ImagesModule } from "./ImagesModule.jsx";
import { LinksModule } from "./LinksModule.jsx"; import { LinksModule } from "./LinksModule.jsx";
import { MemosModule } from "./MemosModule.jsx";
import { NotepadModule } from "./NotepadModule.jsx"; import { NotepadModule } from "./NotepadModule.jsx";
import { TableModule } from "./TableModule.jsx"; import { TableModule } from "./TableModule.jsx";
import { TaskPlannerModule } from "./TaskPlannerModule.jsx"; import { TaskPlannerModule } from "./TaskPlannerModule.jsx";
@ -20,6 +21,7 @@ const EquipmentPlannerModule = lazy(() => import("./EquipmentPlannerModule.jsx")
export const MODULE_COMPONENTS = { export const MODULE_COMPONENTS = {
notepad: { ...TOOLBOX_MODULE_DEFINITIONS.notepad, Component: NotepadModule }, notepad: { ...TOOLBOX_MODULE_DEFINITIONS.notepad, Component: NotepadModule },
memos: { ...TOOLBOX_MODULE_DEFINITIONS.memos, Component: MemosModule },
checklist: { ...TOOLBOX_MODULE_DEFINITIONS.checklist, Component: ChecklistModule }, checklist: { ...TOOLBOX_MODULE_DEFINITIONS.checklist, Component: ChecklistModule },
images: { ...TOOLBOX_MODULE_DEFINITIONS.images, Component: ImagesModule }, images: { ...TOOLBOX_MODULE_DEFINITIONS.images, Component: ImagesModule },
links: { ...TOOLBOX_MODULE_DEFINITIONS.links, Component: LinksModule }, links: { ...TOOLBOX_MODULE_DEFINITIONS.links, Component: LinksModule },

View file

@ -8,6 +8,7 @@ import { compactEquipmentPlannerDataForStorage } from "./equipmentPlanner.js";
import { compactGoalTreeDataForStorage } from "./goalTree.js"; import { compactGoalTreeDataForStorage } from "./goalTree.js";
import { compactImageAnnotationDataForStorage, compactImagesDataForStorage } from "./imageAnnotation.js"; import { compactImageAnnotationDataForStorage, compactImagesDataForStorage } from "./imageAnnotation.js";
import { compactLinksDataForStorage } from "./links.js"; import { compactLinksDataForStorage } from "./links.js";
import { compactMemosDataForStorage } from "./memos.js";
import { compactNotepadDataForStorage } from "./notepad.js"; import { compactNotepadDataForStorage } from "./notepad.js";
import { compactTableDataForStorage } from "./table.js"; import { compactTableDataForStorage } from "./table.js";
import { compactTaskPlannerDataForStorage } from "./taskPlanner.js"; import { compactTaskPlannerDataForStorage } from "./taskPlanner.js";
@ -15,6 +16,7 @@ import { compactTimerDataForStorage } from "./timer.js";
export const MODULE_DATA_COMPACTORS = { export const MODULE_DATA_COMPACTORS = {
notepad: compactNotepadDataForStorage, notepad: compactNotepadDataForStorage,
memos: compactMemosDataForStorage,
checklist: compactChecklistDataForStorage, checklist: compactChecklistDataForStorage,
images: compactImagesDataForStorage, images: compactImagesDataForStorage,
imageAnnotation: compactImageAnnotationDataForStorage, imageAnnotation: compactImageAnnotationDataForStorage,

View file

@ -0,0 +1,43 @@
// Rôle : normalise et compacte les mémos courts des toolboxes.
import { NOTEPAD_ALLOWED_COLORS } from "./drawing.js";
import { normalizeIsoDate, uid } from "./shared.js";
const DEFAULT_MEMO_COLOR = "#f6c453";
function normalizeMemoItem(item) {
const text = String(item?.text || "").trim();
if (!text) return null;
const color = NOTEPAD_ALLOWED_COLORS.has(String(item?.color || "").toLowerCase()) ? String(item.color).toLowerCase() : DEFAULT_MEMO_COLOR;
return {
id: item?.id || uid("memo"),
text,
color,
updatedAt: normalizeIsoDate(item?.updatedAt)
};
}
export function normalizeMemosData(data) {
const source = data && typeof data === "object" ? data : {};
return {
tickerEnabled: source.tickerEnabled !== false,
items: (Array.isArray(source.items) ? source.items : []).map(normalizeMemoItem).filter(Boolean)
};
}
export function compactMemosDataForStorage(value) {
const normalized = normalizeMemosData(value);
const compact = {};
if (normalized.tickerEnabled === false) compact.tickerEnabled = false;
if (normalized.items.length) {
compact.items = normalized.items.map((item) => {
const compactItem = {
id: item.id,
text: item.text,
color: item.color
};
if (item.updatedAt) compactItem.updatedAt = item.updatedAt;
return compactItem;
});
}
return Object.keys(compact).length ? compact : null;
}

View file

@ -1,6 +1,6 @@
// Rôle : normalise, nettoie et compacte les données du bloc-notes riche. // Rôle : normalise, nettoie et compacte les pages du bloc-note riche.
import { NOTEPAD_ALLOWED_COLORS, normalizeDrawingData } from "./drawing.js"; import { NOTEPAD_ALLOWED_COLORS, normalizeDrawingData } from "./drawing.js";
import { normalizeIsoDate } from "./shared.js"; import { normalizeIsoDate, uid } from "./shared.js";
const NOTEPAD_ALLOWED_TAGS = new Set(["p", "div", "br", "strong", "b", "em", "i", "u", "s", "strike", "ul", "ol", "li", "h3", "h4", "span", "mark"]); const NOTEPAD_ALLOWED_TAGS = new Set(["p", "div", "br", "strong", "b", "em", "i", "u", "s", "strike", "ul", "ol", "li", "h3", "h4", "span", "mark"]);
const NOTEPAD_RGB_COLOR_MAP = { const NOTEPAD_RGB_COLOR_MAP = {
@ -106,24 +106,66 @@ function notepadHtmlToText(value) {
.trim(); .trim();
} }
export function normalizeNotepadData(data) { function normalizeNotepadPage(page, index = 0) {
const source = data && typeof data === "object" ? data : {}; const source = page && typeof page === "object" ? page : {};
const legacyText = String(source.text || ""); const legacyText = String(source.text || "");
const html = sanitizeNotepadHtml(source.html ? source.html : textToNotepadHtml(legacyText)); const html = sanitizeNotepadHtml(source.html ? source.html : textToNotepadHtml(legacyText));
const text = notepadHtmlToText(html) || legacyText.trim(); const text = notepadHtmlToText(html) || legacyText.trim();
const updatedAt = normalizeIsoDate(source.updatedAt); const updatedAt = normalizeIsoDate(source.updatedAt);
const drawingMode = source.drawingMode === "permanent" ? "permanent" : "temporary"; const drawingMode = source.drawingMode === "permanent" ? "permanent" : "temporary";
const drawings = normalizeDrawingData(source.drawings); const drawings = normalizeDrawingData(source.drawings);
return { html, text, updatedAt, drawingMode, drawings }; return {
id: source.id || uid("page"),
title: String(source.title || `Page ${index + 1}`).trim() || `Page ${index + 1}`,
html,
text,
updatedAt,
drawingMode,
drawings
};
}
function normalizeLegacyNotepadData(data) {
const source = data && typeof data === "object" ? data : {};
return normalizeNotepadPage({ ...source, id: source.id || uid("page"), title: source.title || "Page 1" }, 0);
}
export function normalizeNotepadData(data) {
const source = data && typeof data === "object" ? data : {};
const pages = (Array.isArray(source.pages) ? source.pages : [normalizeLegacyNotepadData(source)])
.map((page, index) => normalizeNotepadPage(page, index))
.filter(Boolean);
const normalizedPages = pages.length ? pages : [normalizeNotepadPage({ title: "Page 1" }, 0)];
const activePageId = normalizedPages.some((page) => page.id === source.activePageId) ? source.activePageId : normalizedPages[0].id;
const activePage = normalizedPages.find((page) => page.id === activePageId) || normalizedPages[0];
return {
activePageId,
pages: normalizedPages,
html: activePage.html,
text: activePage.text,
updatedAt: activePage.updatedAt,
drawingMode: activePage.drawingMode,
drawings: activePage.drawings
};
} }
export function compactNotepadDataForStorage(value) { export function compactNotepadDataForStorage(value) {
const normalized = normalizeNotepadData(value); const normalized = normalizeNotepadData(value);
const compact = {}; const hasMultiplePages = normalized.pages.length > 1;
if (normalized.html && normalized.text) compact.html = normalized.html; const pages = normalized.pages.map((page, index) => {
if (normalized.text) compact.text = normalized.text; const compact = {
if (normalized.updatedAt) compact.updatedAt = normalized.updatedAt; id: page.id,
if (normalized.drawingMode === "permanent") compact.drawingMode = "permanent"; title: page.title || `Page ${index + 1}`
if (normalized.drawingMode === "permanent" && normalized.drawings.strokes.length) compact.drawings = normalized.drawings; };
return Object.keys(compact).length ? compact : null; if (page.html && page.text) compact.html = page.html;
if (page.text) compact.text = page.text;
if (page.updatedAt) compact.updatedAt = page.updatedAt;
if (page.drawingMode === "permanent") compact.drawingMode = "permanent";
if (page.drawingMode === "permanent" && page.drawings.strokes.length) compact.drawings = page.drawings;
return compact;
}).filter((page) => hasMultiplePages || page.text || page.html || page.drawings?.strokes?.length || page.title !== "Page 1");
if (!pages.length) return null;
const compact = { pages };
if (pages.some((page) => page.id === normalized.activePageId) && normalized.activePageId !== pages[0].id) compact.activePageId = normalized.activePageId;
return compact;
} }

View file

@ -16,6 +16,8 @@ export const ID_PREFIXES = {
goal: "g", goal: "g",
relation: "e", relation: "e",
stroke: "d", stroke: "d",
page: "p",
memo: "w",
equipmentType: "y", equipmentType: "y",
equipment: "q", equipment: "q",
trait: "v", trait: "v",

View file

@ -13,6 +13,7 @@ export { normalizeEquipmentPlannerData, summarizeEquipmentPlannerData } from "./
export { normalizeGoalTreeData } from "./modules/goalTree.js"; export { normalizeGoalTreeData } from "./modules/goalTree.js";
export { labelFromFileName, normalizeImageAnnotationData } from "./modules/imageAnnotation.js"; export { labelFromFileName, normalizeImageAnnotationData } from "./modules/imageAnnotation.js";
export { hostnameFromUrl, normalizeLinksData, normalizeUrl } from "./modules/links.js"; export { hostnameFromUrl, normalizeLinksData, normalizeUrl } from "./modules/links.js";
export { normalizeMemosData } from "./modules/memos.js";
export { normalizeNotepadData, sanitizeNotepadHtml } from "./modules/notepad.js"; export { normalizeNotepadData, sanitizeNotepadHtml } from "./modules/notepad.js";
export { uid } from "./modules/shared.js"; export { uid } from "./modules/shared.js";
export { normalizeTableData } from "./modules/table.js"; export { normalizeTableData } from "./modules/table.js";
@ -215,6 +216,28 @@ function remapModuleDataForExport(type, data, nextId) {
return { counters: compact.counters.map((counter) => ({ ...counter, id: nextId("counter") })) }; return { counters: compact.counters.map((counter) => ({ ...counter, id: nextId("counter") })) };
} }
if (type === "memos") {
const remapped = {};
if (compact.tickerEnabled === false) remapped.tickerEnabled = false;
if (compact.items) remapped.items = compact.items.map((memo) => ({ ...memo, id: nextId("memo") }));
return remapped;
}
if (type === "notepad") {
const remapped = {
pages: compact.pages.map((page) => ({
...page,
id: nextId("page"),
drawings: page.drawings?.strokes?.length
? { strokes: page.drawings.strokes.map((stroke) => ({ ...stroke, id: nextId("stroke") })) }
: page.drawings
}))
};
const activeIndex = compact.pages.findIndex((page) => page.id === compact.activePageId);
if (activeIndex > 0) remapped.activePageId = remapped.pages[activeIndex].id;
return remapped;
}
if (type === "combos") { if (type === "combos") {
const remapped = {}; const remapped = {};
if (compact.device) remapped.device = compact.device; if (compact.device) remapped.device = compact.device;

View file

@ -1,14 +1,24 @@
// Rôle : centralise les métadonnées légères des outils toolbox sans importer leurs composants. // Rôle : centralise les métadonnées légères des outils toolbox sans importer leurs composants.
export const TOOLBOX_MODULE_DEFINITIONS = { export const TOOLBOX_MODULE_DEFINITIONS = {
notepad: { notepad: {
label: "Bloc notes", label: "Bloc Note",
defaultTitle: "Bloc notes", defaultTitle: "Bloc Note",
mode: "Texte riche", mode: "Texte riche",
icon: "notepad", icon: "notepad",
editable: true, editable: true,
scrollable: true,
addable: false, addable: false,
libraryCategory: "notesTracking" libraryCategory: "notesTracking"
}, },
memos: {
label: "Memos",
defaultTitle: "Memos",
mode: "Post-it courts",
icon: "memos",
editable: true,
scrollable: true,
libraryCategory: "notesTracking"
},
checklist: { checklist: {
label: "Checklist", label: "Checklist",
defaultTitle: "Checklist", defaultTitle: "Checklist",
@ -134,7 +144,7 @@ export const TOOLBOX_MODULES = Object.fromEntries(Object.entries(TOOLBOX_MODULE_
])); ]));
export const LIBRARY_CATEGORY_DEFINITIONS = [ export const LIBRARY_CATEGORY_DEFINITIONS = [
{ key: "notesTracking", types: ["notepad", "checklist", "counters"] }, { key: "notesTracking", types: ["memos", "notepad", "checklist", "counters"] },
{ key: "references", types: ["images", "imageAnnotation", "links"] }, { key: "references", types: ["images", "imageAnnotation", "links"] },
{ key: "calculationData", types: ["calculator", "table", "goalTree"] }, { key: "calculationData", types: ["calculator", "table", "goalTree"] },
{ key: "timeRoutines", types: ["timer", "calendar", "taskPlanner"] }, { key: "timeRoutines", types: ["timer", "calendar", "taskPlanner"] },

View file

@ -28,10 +28,9 @@ function getLibraryUrl() {
export function useToolboxActions({ store, notify, creationTemplate, setConfirmModal, setCreateModal, setLinkModalGameId, setDrawerGameId, setImage }) { export function useToolboxActions({ store, notify, creationTemplate, setConfirmModal, setCreateModal, setLinkModalGameId, setDrawerGameId, setImage }) {
function createToolbox(name, gameId = "", options = {}) { function createToolbox(name, gameId = "", options = {}) {
const now = new Date().toISOString(); const now = new Date().toISOString();
const notepadModuleId = uid("mod"); const memosModuleId = uid("mod");
const linksModuleId = uid("mod"); const linksModuleId = uid("mod");
const welcomeText = creationTemplate?.notepad?.text || "Profite bien de ta nouvelle toolbox."; const welcomeText = creationTemplate?.memos?.text || creationTemplate?.notepad?.text || "Profite bien de ta nouvelle toolbox.";
const welcomeHtml = creationTemplate?.notepad?.html || `<p>${welcomeText}</p>`;
const libraryLink = { const libraryLink = {
id: uid("link"), id: uid("link"),
title: creationTemplate?.links?.title || "Librairie des outils", title: creationTemplate?.links?.title || "Librairie des outils",
@ -42,13 +41,13 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
name: name.trim() || "Nouvelle toolbox", name: name.trim() || "Nouvelle toolbox",
updatedAt: now, updatedAt: now,
modules: [ 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" } { id: linksModuleId, type: "links", title: creationTemplate?.links?.moduleTitle || "Liens utiles" }
] ]
}; };
if (options.tutorial === true) toolbox.tutorial = true; if (options.tutorial === true) toolbox.tutorial = true;
if (!store.setToolboxes([toolbox, ...store.toolboxes])) return null; 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"); store.updateModuleData(toolbox.id, linksModuleId, { links: [libraryLink] }, "links");
if (gameId) store.setLinks({ ...store.links, [gameId]: toolbox.id }); if (gameId) store.setLinks({ ...store.links, [gameId]: toolbox.id });
return toolbox; return toolbox;

View file

@ -14,6 +14,7 @@ import {
normalizeGoalTreeData, normalizeGoalTreeData,
normalizeImageAnnotationData, normalizeImageAnnotationData,
normalizeLinksData, normalizeLinksData,
normalizeMemosData,
normalizeNotepadData, normalizeNotepadData,
normalizeTableData, normalizeTableData,
normalizeTaskPlannerData, normalizeTaskPlannerData,
@ -26,6 +27,7 @@ import {
const LIBRARY_TOOLBOX_ID = "library"; const LIBRARY_TOOLBOX_ID = "library";
const MODULE_COMPONENTS = { 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 }))), 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 }))), 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 }))), images: lazy(() => import("../features/toolboxes/modules/ImagesModule.jsx").then((module) => ({ default: module.ImagesModule }))),
@ -153,6 +155,7 @@ export function LibraryPage({ siteContent, actions }) {
normalizeChecklistData, normalizeChecklistData,
normalizeCombosData, normalizeCombosData,
normalizeLinksData, normalizeLinksData,
normalizeMemosData,
normalizeCountersData, normalizeCountersData,
normalizeEquipmentPlannerData, normalizeEquipmentPlannerData,
normalizeGoalTreeData, normalizeGoalTreeData,
@ -344,7 +347,7 @@ function LibraryToolExample({ module, sourceData, description, categoryTitle, do
</div> </div>
<div className={`library-example-layout ${Array.isArray(docs?.controls) && docs.controls.length ? "has-legend" : ""}`}> <div className={`library-example-layout ${Array.isArray(docs?.controls) && docs.controls.length ? "has-legend" : ""}`}>
<ToolControlLegend items={docs?.controls} heading={controlLegendHeading || "Repères"} /> <ToolControlLegend items={docs?.controls} heading={controlLegendHeading || "Repères"} />
<article className={`module library-tool-example ${scrollable ? "is-scrollable" : ""}`}> <article className={`module library-tool-example ${scrollable ? "is-scrollable" : ""}`} data-module-type={module.type}>
<header> <header>
<div> <div>
<h2>{definition.label}</h2> <h2>{definition.label}</h2>

View file

@ -55,6 +55,10 @@
padding: var(--space-4); padding: var(--space-4);
} }
.library-tool-example[data-module-type="notepad"] .module-content {
padding: 0;
}
.library-tool-example .image-annotation-stage, .library-tool-example .image-annotation-stage,
.library-tool-example .images-grid img { .library-tool-example .images-grid img {
max-height: 260px; max-height: 260px;

View file

@ -9,6 +9,16 @@
-webkit-mask-image: url("/static/icons/notepad.svg"); -webkit-mask-image: url("/static/icons/notepad.svg");
} }
.ui-icon-memos {
mask-image: url("/static/icons/memos.svg");
-webkit-mask-image: url("/static/icons/memos.svg");
}
.ui-icon-pages {
mask-image: url("/static/icons/pages.svg");
-webkit-mask-image: url("/static/icons/pages.svg");
}
.ui-icon-checklist { .ui-icon-checklist {
mask-image: url("/static/icons/checklist.svg"); mask-image: url("/static/icons/checklist.svg");
-webkit-mask-image: url("/static/icons/checklist.svg"); -webkit-mask-image: url("/static/icons/checklist.svg");

View file

@ -2,6 +2,7 @@
@use "layout"; @use "layout";
@use "module-shell"; @use "module-shell";
@use "notepad"; @use "notepad";
@use "memos";
@use "drawing"; @use "drawing";
@use "text-exchange"; @use "text-exchange";
@use "checklist"; @use "checklist";

View file

@ -0,0 +1,186 @@
// Rôle : stylise l'outil Memos, ses post-it et sa bande déroulante.
.memos-module {
display: grid;
gap: var(--space-3);
padding: var(--space-4);
}
.memos-toolbar {
display: flex;
align-items: center;
gap: var(--space-3);
min-width: 0;
}
.memos-add-button {
display: inline-flex;
align-items: center;
gap: 8px;
}
.memos-ticker-toggle {
width: 40px;
min-width: 40px;
padding: 0;
}
.memos-ticker-toggle.active,
.memo-card.is-drop-target {
border-color: rgba(246, 196, 83, 0.48);
background: rgba(246, 196, 83, 0.1);
color: var(--color-accent-gold);
}
.memos-ticker {
flex: 1;
min-width: 0;
overflow: hidden;
border: 1px solid rgba(246, 196, 83, 0.22);
border-radius: var(--radius-md);
background: rgba(5, 7, 17, 0.56);
}
.memos-ticker-track {
display: flex;
width: max-content;
min-width: 100%;
gap: calc(var(--space-6) * 3);
padding: 8px 10px;
animation: memos-ticker-scroll 24s linear infinite;
}
.memos-ticker-item {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
font-weight: 800;
white-space: nowrap;
}
.memos-ticker-item::before {
content: "";
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--memo-color);
box-shadow: 0 0 10px color-mix(in srgb, var(--memo-color) 42%, transparent);
}
.memos-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: var(--space-3);
}
.memo-card {
position: relative;
display: grid;
gap: var(--space-2);
min-height: 132px;
padding: var(--space-3) var(--space-3) var(--space-3) 52px;
border: 1px solid color-mix(in srgb, var(--memo-color) 42%, transparent);
border-radius: var(--radius-md);
background:
linear-gradient(135deg, color-mix(in srgb, var(--memo-color) 18%, transparent), transparent 54%),
rgba(5, 7, 17, 0.5);
transition:
border-color var(--duration-fast) var(--ease-standard),
box-shadow var(--duration-fast) var(--ease-standard),
opacity var(--duration-fast) var(--ease-standard);
}
.memo-card textarea {
width: 100%;
min-height: 76px;
resize: vertical;
border: 0;
background: transparent;
color: var(--color-text-primary);
font: inherit;
line-height: 1.45;
outline: none;
}
.memo-card.is-dragging {
opacity: 0.58;
}
.memo-drag-handle {
position: absolute;
top: var(--space-3);
left: var(--space-3);
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;
}
.memo-drag-handle:active {
cursor: grabbing;
}
.memo-card footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
}
.memo-color-menu {
top: auto;
bottom: calc(100% + 8px);
transform: translate(-50%, 4px);
}
.notepad-color-dropdown:hover .memo-color-menu,
.notepad-color-dropdown:focus-within .memo-color-menu,
.notepad-color-dropdown.is-open .memo-color-menu {
transform: translate(-50%, 0);
}
.memo-color-menu::after {
top: 100%;
bottom: auto;
}
.memo-color-menu::before {
top: 100%;
bottom: auto;
border-top: 7px solid rgba(10, 13, 28, 0.99);
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 {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}

View file

@ -207,6 +207,11 @@
-webkit-mask-image: url("/static/icons/notepad.svg"); -webkit-mask-image: url("/static/icons/notepad.svg");
} }
.module-icon-memos {
mask-image: url("/static/icons/memos.svg");
-webkit-mask-image: url("/static/icons/memos.svg");
}
.module-icon-checklist { .module-icon-checklist {
mask-image: url("/static/icons/checklist.svg"); mask-image: url("/static/icons/checklist.svg");
-webkit-mask-image: url("/static/icons/checklist.svg"); -webkit-mask-image: url("/static/icons/checklist.svg");

View file

@ -1,12 +1,193 @@
// Rôle : gère la surface de bloc-notes riche et sa barre de formatage. // Rôle : gère la surface de Bloc Note riche, ses pages et sa barre de formatage.
@use "../mixins"; @use "../mixins";
.notepad-module { .notepad-module {
position: relative;
display: grid; display: grid;
gap: var(--space-3); gap: var(--space-3);
padding: var(--space-4); padding: var(--space-4);
} }
.notepad-pages-dropdown {
position: absolute;
top: 50%;
bottom: auto;
left: 0;
z-index: 20;
width: 40px;
height: 40px;
pointer-events: auto;
transform: translateY(-50%);
}
.notepad-pages-dropdown.is-open {
top: 0;
bottom: 0;
width: min(360px, calc(100% - var(--space-4)));
height: auto;
z-index: 65;
transform: none;
}
.notepad-pages-toggle {
position: absolute;
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;
bottom: 0;
left: 0;
z-index: 64;
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
width: calc(100% - 40px);
gap: var(--space-2);
overflow: hidden;
padding: var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: rgba(10, 13, 28, 0.98);
box-shadow: var(--shadow-lg);
animation: notepad-pages-slide-in var(--duration-normal) var(--ease-standard);
pointer-events: auto;
}
.notepad-pages-list {
display: grid;
align-content: start;
gap: 6px;
min-height: 0;
overflow-y: auto;
}
.notepad-page-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 6px;
align-items: center;
}
.notepad-page-main {
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);
}
.notepad-page-row.active .notepad-page-main {
border-color: rgba(246, 196, 83, 0.42);
background: rgba(246, 196, 83, 0.08);
}
.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;
}
.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;
}
.notepad-page-actions {
display: flex;
gap: 4px;
}
.notepad-page-edit,
.notepad-page-delete {
width: 28px;
min-width: 28px;
min-height: 28px;
padding: 0;
}
.notepad-page-edit .ui-icon,
.notepad-page-delete .ui-icon {
width: 14px;
height: 14px;
}
@keyframes notepad-pages-slide-in {
from {
opacity: 0;
transform: translateX(-14px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.notepad-page-add {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 36px;
align-self: end;
}
.notepad-toolbar, .notepad-toolbar,
.notepad-toolbar-group, .notepad-toolbar-group,
.notepad-format-controls { .notepad-format-controls {
@ -361,6 +542,7 @@
.notepad-editor { .notepad-editor {
position: relative; position: relative;
z-index: 1; z-index: 1;
box-sizing: border-box;
min-height: 260px; min-height: 260px;
padding: var(--space-4); padding: var(--space-4);
color: var(--color-text-primary); color: var(--color-text-primary);