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

@ -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,

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 { 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}
/>

View file

@ -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>

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-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 && (

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 HIGHLIGHT_COLORS = ["#f6c453", "#22d3ee", "#8b5cf6", "#d946ef", "#202745"];
export const DRAWING_WIDTHS = [2, 4, 8, 12];

View file

@ -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,

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

View file

@ -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 },

View file

@ -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,

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 { 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;
}

View file

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

View file

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

View file

@ -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"] },

View file

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

View file

@ -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>

View file

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

View file

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

View file

@ -2,6 +2,7 @@
@use "layout";
@use "module-shell";
@use "notepad";
@use "memos";
@use "drawing";
@use "text-exchange";
@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");
}
.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");

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";
.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);