add multi-page notepad and memos toolbox tool
All checks were successful
Deploy Sokko G / deploy (push) Successful in 16s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 16s
This commit is contained in:
parent
fcabbe8029
commit
6125336338
33 changed files with 3787 additions and 138 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -60,8 +60,78 @@
|
|||
"buildsCommands": "Les outils pour documenter des builds, organiser des équipements et conserver des séquences de commandes."
|
||||
},
|
||||
"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": {
|
||||
"controls": [
|
||||
{
|
||||
"group": "Pages",
|
||||
"icon": "pages",
|
||||
"title": "Affiche le menu des pages",
|
||||
"preview": {
|
||||
"kind": "iconButton",
|
||||
"icon": "pages"
|
||||
}
|
||||
},
|
||||
{
|
||||
"group": "Édition de texte",
|
||||
"icon": "edit",
|
||||
|
|
@ -191,8 +261,8 @@
|
|||
}
|
||||
],
|
||||
"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.",
|
||||
"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\":[...]}"
|
||||
"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@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": {
|
||||
|
|
@ -1357,7 +1427,7 @@
|
|||
},
|
||||
{
|
||||
"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",
|
||||
|
|
@ -1591,10 +1661,9 @@
|
|||
"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.",
|
||||
"creationTemplate": {
|
||||
"notepad": {
|
||||
"title": "Bienvenue",
|
||||
"text": "Profite bien de ta nouvelle toolbox.",
|
||||
"html": "<p>Profite bien de ta nouvelle toolbox.</p>"
|
||||
"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\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",
|
||||
"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",
|
||||
|
|
|
|||
24
website/public/static/icons/memos.svg
Normal file
24
website/public/static/icons/memos.svg
Normal 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 |
2
website/public/static/icons/pages.svg
Normal file
2
website/public/static/icons/pages.svg
Normal 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 |
|
|
@ -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,
|
||||
|
|
|
|||
192
website/src/features/toolboxes/modules/MemosModule.jsx
Normal file
192
website/src/features/toolboxes/modules/MemosModule.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 { CompactDropdown } from "../../../components/CompactDropdown.jsx";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { DrawingControls } from "./DrawingControls.jsx";
|
||||
import { DrawingOverlay } from "./DrawingOverlay.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 normalizedData = useMemo(() => context.normalizeNotepadData(storedData), [context, storedData]);
|
||||
const textContent = context.moduleText?.notepad || {};
|
||||
const activePage = normalizedData.pages.find((page) => page.id === normalizedData.activePageId) || normalizedData.pages[0];
|
||||
const editorRef = useRef(null);
|
||||
const surfaceRef = useRef(null);
|
||||
const selectionOffsetRef = useRef(null);
|
||||
const localSaveUpdatedAtRef = useRef("");
|
||||
const moduleKeyRef = useRef(`${toolboxId}:${moduleId}`);
|
||||
const [html, setHtml] = useState(normalizedData.html);
|
||||
const [text, setText] = useState(normalizedData.text);
|
||||
const [updatedAt, setUpdatedAt] = useState(normalizedData.updatedAt);
|
||||
const [drawingMode, setDrawingMode] = useState(normalizedData.drawingMode);
|
||||
const pageKeyRef = useRef(`${toolboxId}:${moduleId}:${activePage.id}`);
|
||||
const [html, setHtml] = useState(activePage.html);
|
||||
const [text, setText] = useState(activePage.text);
|
||||
const [updatedAt, setUpdatedAt] = useState(activePage.updatedAt);
|
||||
const [drawingMode, setDrawingMode] = useState(activePage.drawingMode);
|
||||
const [drawingActive, setDrawingActive] = useState(false);
|
||||
const [eraseActive, setEraseActive] = useState(false);
|
||||
const [drawingColor, setDrawingColor] = useState("#f6c453");
|
||||
const [drawingWidth, setDrawingWidth] = useState(4);
|
||||
const [textColor, setTextColor] = useState(TEXT_COLORS[0]);
|
||||
const [highlightColor, setHighlightColor] = useState(HIGHLIGHT_COLORS[0]);
|
||||
const [editingPageId, setEditingPageId] = useState("");
|
||||
const [pageTitleDrafts, setPageTitleDrafts] = useState({});
|
||||
const [activeFormats, setActiveFormats] = useState({
|
||||
bold: false,
|
||||
italic: false,
|
||||
|
|
@ -45,30 +50,34 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
|
|||
bulletList: false,
|
||||
numberList: false
|
||||
});
|
||||
const sessionKey = `sokkog:notepad-drawings:${toolboxId}:${moduleId}`;
|
||||
const sessionKey = `sokkog:notepad-drawings:${toolboxId}:${moduleId}:${activePage.id}`;
|
||||
const [temporaryStrokes, setTemporaryStrokes] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const moduleKey = `${toolboxId}:${moduleId}`;
|
||||
if (moduleKeyRef.current !== moduleKey) {
|
||||
moduleKeyRef.current = moduleKey;
|
||||
setPageTitleDrafts(Object.fromEntries(normalizedData.pages.map((page) => [page.id, page.title])));
|
||||
}, [normalizedData.pages]);
|
||||
|
||||
useEffect(() => {
|
||||
const pageKey = `${toolboxId}:${moduleId}:${activePage.id}`;
|
||||
if (pageKeyRef.current !== pageKey) {
|
||||
pageKeyRef.current = pageKey;
|
||||
localSaveUpdatedAtRef.current = "";
|
||||
}
|
||||
if (normalizedData.updatedAt && normalizedData.updatedAt === localSaveUpdatedAtRef.current) return;
|
||||
if (activePage.updatedAt && activePage.updatedAt === localSaveUpdatedAtRef.current) return;
|
||||
const editor = editorRef.current;
|
||||
if (document.activeElement === editor && getPlainText(editor) === normalizedData.text) {
|
||||
setText(normalizedData.text);
|
||||
setUpdatedAt(normalizedData.updatedAt);
|
||||
setDrawingMode(normalizedData.drawingMode);
|
||||
if (document.activeElement === editor && getPlainText(editor) === activePage.text) {
|
||||
setText(activePage.text);
|
||||
setUpdatedAt(activePage.updatedAt);
|
||||
setDrawingMode(activePage.drawingMode);
|
||||
return;
|
||||
}
|
||||
setHtml(normalizedData.html);
|
||||
setText(normalizedData.text);
|
||||
setUpdatedAt(normalizedData.updatedAt);
|
||||
setDrawingMode(normalizedData.drawingMode);
|
||||
setHtml(activePage.html);
|
||||
setText(activePage.text);
|
||||
setUpdatedAt(activePage.updatedAt);
|
||||
setDrawingMode(activePage.drawingMode);
|
||||
setDrawingActive(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(() => {
|
||||
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]);
|
||||
|
||||
const activeStrokes = drawingMode === "permanent" ? normalizedData.drawings.strokes : temporaryStrokes;
|
||||
const activeStrokes = drawingMode === "permanent" ? activePage.drawings.strokes : temporaryStrokes;
|
||||
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) {
|
||||
const nextUpdatedAt = new Date().toISOString();
|
||||
localSaveUpdatedAtRef.current = nextUpdatedAt;
|
||||
setHtml(nextHtml);
|
||||
setText(nextText);
|
||||
setUpdatedAt(nextUpdatedAt);
|
||||
context.setModuleData(toolboxId, moduleId, {
|
||||
...normalizedData,
|
||||
updateActivePage((page) => ({
|
||||
...page,
|
||||
html: nextHtml,
|
||||
text: nextText,
|
||||
updatedAt: nextUpdatedAt,
|
||||
drawingMode,
|
||||
drawings: drawingMode === "permanent" ? { strokes: activeStrokes } : { strokes: [] }
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
function saveDrawingMode(nextMode, nextStrokes) {
|
||||
setDrawingMode(nextMode);
|
||||
if (nextMode === "temporary") setTemporaryStrokes(nextStrokes);
|
||||
context.setModuleData(toolboxId, moduleId, {
|
||||
...normalizedData,
|
||||
updateActivePage((page) => ({
|
||||
...page,
|
||||
html,
|
||||
text,
|
||||
updatedAt,
|
||||
drawingMode: nextMode,
|
||||
drawings: nextMode === "permanent" ? { strokes: nextStrokes } : { strokes: [] }
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
function saveStrokes(nextStrokes) {
|
||||
if (drawingMode === "permanent") {
|
||||
context.setModuleData(toolboxId, moduleId, {
|
||||
...normalizedData,
|
||||
updateActivePage((page) => ({
|
||||
...page,
|
||||
html,
|
||||
text,
|
||||
updatedAt,
|
||||
drawingMode,
|
||||
drawings: { strokes: nextStrokes }
|
||||
});
|
||||
}));
|
||||
} else {
|
||||
setTemporaryStrokes(nextStrokes);
|
||||
}
|
||||
|
|
@ -211,9 +229,10 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
|
|||
|
||||
const getSurfaceSize = useCallback(() => {
|
||||
const element = surfaceRef.current;
|
||||
const editor = editorRef.current;
|
||||
return {
|
||||
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([]);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="notepad-module">
|
||||
{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")} />
|
||||
</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
|
||||
textContent={textContent}
|
||||
activeFormats={activeFormats}
|
||||
|
|
@ -258,7 +392,7 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
|
|||
suppressContentEditableWarning
|
||||
role="textbox"
|
||||
aria-multiline="true"
|
||||
data-placeholder={textContent.placeholder || "Notes rapides..."}
|
||||
data-placeholder={textContent.placeholder || "Bloc Note..."}
|
||||
onInput={handleEditorInput}
|
||||
onPaste={handlePaste}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function NotepadToolbar({
|
|||
onHighlightColorApply
|
||||
}) {
|
||||
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-toolbar-group" role="group" aria-label={textContent.textFormatLabel || "Style du texte"}>
|
||||
<ToolbarButton active={activeFormats.bold} title={textContent.boldTitle || "Gras"} onClick={() => onCommand("bold")}>B</ToolbarButton>
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
|||
<span className="module-icon" aria-hidden="true">
|
||||
<span className={`module-icon-svg module-icon-${definition.icon || "notepad"}`} />
|
||||
</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>
|
||||
{definition.scrollable && (
|
||||
|
|
|
|||
|
|
@ -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 HIGHLIGHT_COLORS = ["#f6c453", "#22d3ee", "#8b5cf6", "#d946ef", "#202745"];
|
||||
export const DRAWING_WIDTHS = [2, 4, 8, 12];
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import {
|
|||
exportChecklistText,
|
||||
exportCountersText,
|
||||
exportLinksText,
|
||||
exportMemosText,
|
||||
exportNotepadText,
|
||||
exportTableTsv,
|
||||
importCalculatorText,
|
||||
importChecklistText,
|
||||
importCountersText,
|
||||
importLinksText,
|
||||
importMemosText,
|
||||
importNotepadText,
|
||||
importTableTsv
|
||||
} from "./textImport/basic.js";
|
||||
|
|
@ -24,10 +26,15 @@ export { parseColonImportLines };
|
|||
|
||||
const TEXT_EXCHANGE_HANDLERS = {
|
||||
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,
|
||||
importText: importNotepadText
|
||||
},
|
||||
memos: {
|
||||
hasContent: (data) => Boolean(data?.items?.length),
|
||||
exportText: exportMemosText,
|
||||
importText: importMemosText
|
||||
},
|
||||
checklist: {
|
||||
hasContent: (data) => Boolean(data?.sections?.some((section) => section.items?.length)),
|
||||
exportText: exportChecklistText,
|
||||
|
|
|
|||
|
|
@ -3,10 +3,15 @@ import { cellAddress, columnIndexToName } from "../tableFormulaEngine.js";
|
|||
import { cloneDrawingData, lineError, parseBoolean, parseColonImportLines, parseInteger, parseMetaLine, parseNumber, splitLines, textToNotepadExchangeHtml } from "./shared.js";
|
||||
|
||||
export function exportNotepadText(data) {
|
||||
const html = String(data?.html || "").trim() || textToNotepadExchangeHtml(data?.text || "");
|
||||
const lines = ["@notepad", "@html", html, "@endhtml"];
|
||||
if (data?.drawingMode === "permanent") lines.push("@drawingMode: permanent");
|
||||
if (data?.drawings?.strokes?.length) lines.push(`@drawings: ${JSON.stringify(cloneDrawingData(data.drawings))}`);
|
||||
const pages = Array.isArray(data?.pages) && data.pages.length ? data.pages : [data];
|
||||
const lines = ["@notepad"];
|
||||
pages.forEach((page, index) => {
|
||||
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");
|
||||
}
|
||||
|
||||
|
|
@ -38,16 +43,34 @@ export function importNotepadText(text, context) {
|
|||
|
||||
let readingHtml = false;
|
||||
let htmlWasOpened = false;
|
||||
let htmlWasClosed = false;
|
||||
const htmlLines = [];
|
||||
let drawingMode = "temporary";
|
||||
let drawings = { strokes: [] };
|
||||
const pages = [];
|
||||
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) => {
|
||||
const cleanLine = line.trim();
|
||||
if (readingHtml) {
|
||||
if (cleanLine === "@endhtml") {
|
||||
readingHtml = false;
|
||||
htmlWasClosed = true;
|
||||
return;
|
||||
}
|
||||
htmlLines.push(line);
|
||||
|
|
@ -61,24 +84,55 @@ export function importNotepadText(text, context) {
|
|||
return;
|
||||
}
|
||||
const meta = parseMetaLine(cleanLine);
|
||||
if (meta?.key === "page") {
|
||||
flushPage();
|
||||
currentPage.title = meta.value || `Page ${pages.length + 1}`;
|
||||
return;
|
||||
}
|
||||
if (meta?.key === "drawingMode") {
|
||||
if (!["temporary", "permanent"].includes(meta.value)) throw lineError(index, "mode de dessin invalide.");
|
||||
drawingMode = meta.value;
|
||||
currentPage.drawingMode = meta.value;
|
||||
return;
|
||||
}
|
||||
if (meta?.key === "drawings") {
|
||||
try {
|
||||
drawings = cloneImportedNotepadDrawings(JSON.parse(meta.value), context);
|
||||
currentPage.drawings = cloneImportedNotepadDrawings(JSON.parse(meta.value), context);
|
||||
} catch (error) {
|
||||
throw lineError(index, error?.message || "dessins invalides.");
|
||||
}
|
||||
if (drawings.strokes.length) drawingMode = "permanent";
|
||||
if (currentPage.drawings.strokes.length) currentPage.drawingMode = "permanent";
|
||||
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é.");
|
||||
return context.normalizeNotepadData({ html: htmlLines.join("\n").trim(), drawingMode, drawings });
|
||||
if (readingHtml) throw new Error("Bloc HTML non terminé.");
|
||||
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 });
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { CountersModule } from "./CountersModule.jsx";
|
|||
import { ImageAnnotationModule } from "./ImageAnnotationModule.jsx";
|
||||
import { ImagesModule } from "./ImagesModule.jsx";
|
||||
import { LinksModule } from "./LinksModule.jsx";
|
||||
import { MemosModule } from "./MemosModule.jsx";
|
||||
import { NotepadModule } from "./NotepadModule.jsx";
|
||||
import { TableModule } from "./TableModule.jsx";
|
||||
import { TaskPlannerModule } from "./TaskPlannerModule.jsx";
|
||||
|
|
@ -20,6 +21,7 @@ const EquipmentPlannerModule = lazy(() => import("./EquipmentPlannerModule.jsx")
|
|||
|
||||
export const MODULE_COMPONENTS = {
|
||||
notepad: { ...TOOLBOX_MODULE_DEFINITIONS.notepad, Component: NotepadModule },
|
||||
memos: { ...TOOLBOX_MODULE_DEFINITIONS.memos, Component: MemosModule },
|
||||
checklist: { ...TOOLBOX_MODULE_DEFINITIONS.checklist, Component: ChecklistModule },
|
||||
images: { ...TOOLBOX_MODULE_DEFINITIONS.images, Component: ImagesModule },
|
||||
links: { ...TOOLBOX_MODULE_DEFINITIONS.links, Component: LinksModule },
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { compactEquipmentPlannerDataForStorage } from "./equipmentPlanner.js";
|
|||
import { compactGoalTreeDataForStorage } from "./goalTree.js";
|
||||
import { compactImageAnnotationDataForStorage, compactImagesDataForStorage } from "./imageAnnotation.js";
|
||||
import { compactLinksDataForStorage } from "./links.js";
|
||||
import { compactMemosDataForStorage } from "./memos.js";
|
||||
import { compactNotepadDataForStorage } from "./notepad.js";
|
||||
import { compactTableDataForStorage } from "./table.js";
|
||||
import { compactTaskPlannerDataForStorage } from "./taskPlanner.js";
|
||||
|
|
@ -15,6 +16,7 @@ import { compactTimerDataForStorage } from "./timer.js";
|
|||
|
||||
export const MODULE_DATA_COMPACTORS = {
|
||||
notepad: compactNotepadDataForStorage,
|
||||
memos: compactMemosDataForStorage,
|
||||
checklist: compactChecklistDataForStorage,
|
||||
images: compactImagesDataForStorage,
|
||||
imageAnnotation: compactImageAnnotationDataForStorage,
|
||||
|
|
|
|||
43
website/src/features/toolboxes/storage/modules/memos.js
Normal file
43
website/src/features/toolboxes/storage/modules/memos.js
Normal 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;
|
||||
}
|
||||
|
|
@ -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 { 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_RGB_COLOR_MAP = {
|
||||
|
|
@ -106,24 +106,66 @@ function notepadHtmlToText(value) {
|
|||
.trim();
|
||||
}
|
||||
|
||||
export function normalizeNotepadData(data) {
|
||||
const source = data && typeof data === "object" ? data : {};
|
||||
function normalizeNotepadPage(page, index = 0) {
|
||||
const source = page && typeof page === "object" ? page : {};
|
||||
const legacyText = String(source.text || "");
|
||||
const html = sanitizeNotepadHtml(source.html ? source.html : textToNotepadHtml(legacyText));
|
||||
const text = notepadHtmlToText(html) || legacyText.trim();
|
||||
const updatedAt = normalizeIsoDate(source.updatedAt);
|
||||
const drawingMode = source.drawingMode === "permanent" ? "permanent" : "temporary";
|
||||
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) {
|
||||
const normalized = normalizeNotepadData(value);
|
||||
const compact = {};
|
||||
if (normalized.html && normalized.text) compact.html = normalized.html;
|
||||
if (normalized.text) compact.text = normalized.text;
|
||||
if (normalized.updatedAt) compact.updatedAt = normalized.updatedAt;
|
||||
if (normalized.drawingMode === "permanent") compact.drawingMode = "permanent";
|
||||
if (normalized.drawingMode === "permanent" && normalized.drawings.strokes.length) compact.drawings = normalized.drawings;
|
||||
return Object.keys(compact).length ? compact : null;
|
||||
const hasMultiplePages = normalized.pages.length > 1;
|
||||
const pages = normalized.pages.map((page, index) => {
|
||||
const compact = {
|
||||
id: page.id,
|
||||
title: page.title || `Page ${index + 1}`
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ export const ID_PREFIXES = {
|
|||
goal: "g",
|
||||
relation: "e",
|
||||
stroke: "d",
|
||||
page: "p",
|
||||
memo: "w",
|
||||
equipmentType: "y",
|
||||
equipment: "q",
|
||||
trait: "v",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export { normalizeEquipmentPlannerData, summarizeEquipmentPlannerData } from "./
|
|||
export { normalizeGoalTreeData } from "./modules/goalTree.js";
|
||||
export { labelFromFileName, normalizeImageAnnotationData } from "./modules/imageAnnotation.js";
|
||||
export { hostnameFromUrl, normalizeLinksData, normalizeUrl } from "./modules/links.js";
|
||||
export { normalizeMemosData } from "./modules/memos.js";
|
||||
export { normalizeNotepadData, sanitizeNotepadHtml } from "./modules/notepad.js";
|
||||
export { uid } from "./modules/shared.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") })) };
|
||||
}
|
||||
|
||||
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") {
|
||||
const remapped = {};
|
||||
if (compact.device) remapped.device = compact.device;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,24 @@
|
|||
// Rôle : centralise les métadonnées légères des outils toolbox sans importer leurs composants.
|
||||
export const TOOLBOX_MODULE_DEFINITIONS = {
|
||||
notepad: {
|
||||
label: "Bloc notes",
|
||||
defaultTitle: "Bloc notes",
|
||||
label: "Bloc Note",
|
||||
defaultTitle: "Bloc Note",
|
||||
mode: "Texte riche",
|
||||
icon: "notepad",
|
||||
editable: true,
|
||||
scrollable: true,
|
||||
addable: false,
|
||||
libraryCategory: "notesTracking"
|
||||
},
|
||||
memos: {
|
||||
label: "Memos",
|
||||
defaultTitle: "Memos",
|
||||
mode: "Post-it courts",
|
||||
icon: "memos",
|
||||
editable: true,
|
||||
scrollable: true,
|
||||
libraryCategory: "notesTracking"
|
||||
},
|
||||
checklist: {
|
||||
label: "Checklist",
|
||||
defaultTitle: "Checklist",
|
||||
|
|
@ -134,7 +144,7 @@ export const TOOLBOX_MODULES = Object.fromEntries(Object.entries(TOOLBOX_MODULE_
|
|||
]));
|
||||
|
||||
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: "calculationData", types: ["calculator", "table", "goalTree"] },
|
||||
{ key: "timeRoutines", types: ["timer", "calendar", "taskPlanner"] },
|
||||
|
|
|
|||
|
|
@ -28,10 +28,9 @@ function getLibraryUrl() {
|
|||
export function useToolboxActions({ store, notify, creationTemplate, setConfirmModal, setCreateModal, setLinkModalGameId, setDrawerGameId, setImage }) {
|
||||
function createToolbox(name, gameId = "", options = {}) {
|
||||
const now = new Date().toISOString();
|
||||
const notepadModuleId = uid("mod");
|
||||
const memosModuleId = uid("mod");
|
||||
const linksModuleId = uid("mod");
|
||||
const welcomeText = creationTemplate?.notepad?.text || "Profite bien de ta nouvelle toolbox.";
|
||||
const welcomeHtml = creationTemplate?.notepad?.html || `<p>${welcomeText}</p>`;
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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
|
|||
</div>
|
||||
<div className={`library-example-layout ${Array.isArray(docs?.controls) && docs.controls.length ? "has-legend" : ""}`}>
|
||||
<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>
|
||||
<div>
|
||||
<h2>{definition.label}</h2>
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@
|
|||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.library-tool-example[data-module-type="notepad"] .module-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.library-tool-example .image-annotation-stage,
|
||||
.library-tool-example .images-grid img {
|
||||
max-height: 260px;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,16 @@
|
|||
-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 {
|
||||
mask-image: url("/static/icons/checklist.svg");
|
||||
-webkit-mask-image: url("/static/icons/checklist.svg");
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
@use "layout";
|
||||
@use "module-shell";
|
||||
@use "notepad";
|
||||
@use "memos";
|
||||
@use "drawing";
|
||||
@use "text-exchange";
|
||||
@use "checklist";
|
||||
|
|
|
|||
186
website/src/styles/toolboxes/_memos.scss
Normal file
186
website/src/styles/toolboxes/_memos.scss
Normal 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%);
|
||||
}
|
||||
}
|
||||
|
|
@ -207,6 +207,11 @@
|
|||
-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 {
|
||||
mask-image: url("/static/icons/checklist.svg");
|
||||
-webkit-mask-image: url("/static/icons/checklist.svg");
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
.notepad-module {
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
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-group,
|
||||
.notepad-format-controls {
|
||||
|
|
@ -361,6 +542,7 @@
|
|||
.notepad-editor {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-sizing: border-box;
|
||||
min-height: 260px;
|
||||
padding: var(--space-4);
|
||||
color: var(--color-text-primary);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue