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

@ -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>
);
}