Split toolbox text import and goal tree styles
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
7348969ac4
commit
bd6dd78e34
13 changed files with 2298 additions and 2203 deletions
|
|
@ -174,6 +174,9 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
const textExchangeHook = await readFile("website/src/features/toolboxes/modules/useTextExchange.js", "utf8");
|
const textExchangeHook = await readFile("website/src/features/toolboxes/modules/useTextExchange.js", "utf8");
|
||||||
const countersModule = await readFile("website/src/features/toolboxes/modules/CountersModule.jsx", "utf8");
|
const countersModule = await readFile("website/src/features/toolboxes/modules/CountersModule.jsx", "utf8");
|
||||||
const textImport = await readFile("website/src/features/toolboxes/modules/textImport.js", "utf8");
|
const textImport = await readFile("website/src/features/toolboxes/modules/textImport.js", "utf8");
|
||||||
|
const textImportShared = await readFile("website/src/features/toolboxes/modules/textImport/shared.js", "utf8");
|
||||||
|
const textImportPlanning = await readFile("website/src/features/toolboxes/modules/textImport/planning.js", "utf8");
|
||||||
|
const textImportTime = await readFile("website/src/features/toolboxes/modules/textImport/time.js", "utf8");
|
||||||
const imageViewer = await readFile("website/src/components/ImageViewer.jsx", "utf8");
|
const imageViewer = await readFile("website/src/components/ImageViewer.jsx", "utf8");
|
||||||
|
|
||||||
assert.match(moduleMetadataRegistry, /export const TOOLBOX_MODULES/);
|
assert.match(moduleMetadataRegistry, /export const TOOLBOX_MODULES/);
|
||||||
|
|
@ -483,16 +486,18 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(timerParts, /export function CountdownControls/);
|
assert.match(timerParts, /export function CountdownControls/);
|
||||||
assert.match(timerParts, /export function CountdownList/);
|
assert.match(timerParts, /export function CountdownList/);
|
||||||
assert.match(timerOptions, /export const ALERT_MIN_DELAY_MS/);
|
assert.match(timerOptions, /export const ALERT_MIN_DELAY_MS/);
|
||||||
assert.match(textImport, /export function parseColonImportLines/);
|
assert.match(textImport, /export \{ parseColonImportLines \}/);
|
||||||
assert.match(textImport, /export function exportModuleText/);
|
assert.match(textImport, /export function exportModuleText/);
|
||||||
assert.match(textImport, /export function importModuleText/);
|
assert.match(textImport, /export function importModuleText/);
|
||||||
assert.match(textImport, /export function hasTextExchangeContent/);
|
assert.match(textImport, /export function hasTextExchangeContent/);
|
||||||
assert.match(textImport, /line\.indexOf\(":"\)/);
|
assert.match(textImport, /TEXT_EXCHANGE_HANDLERS/);
|
||||||
assert.match(textImport, /exportCalendarText/);
|
assert.match(textImportShared, /export function parseColonImportLines/);
|
||||||
assert.match(textImport, /importCalendarText/);
|
assert.match(textImportShared, /line\.indexOf\(":"\)/);
|
||||||
assert.match(textImport, /exportGoalTreeText/);
|
assert.match(textImportTime, /export function exportCalendarText/);
|
||||||
assert.match(textImport, /importGoalTreeText/);
|
assert.match(textImportTime, /export function importCalendarText/);
|
||||||
assert.match(textImport, /CALENDAR_WEEKDAYS/);
|
assert.match(textImportPlanning, /export function exportGoalTreeText/);
|
||||||
|
assert.match(textImportPlanning, /export function importGoalTreeText/);
|
||||||
|
assert.match(textImportTime, /CALENDAR_WEEKDAYS/);
|
||||||
assert.match(imageViewer, /image-viewer-media/);
|
assert.match(imageViewer, /image-viewer-media/);
|
||||||
assert.match(imageViewer, /image-viewer-image-frame/);
|
assert.match(imageViewer, /image-viewer-image-frame/);
|
||||||
assert.match(imageViewer, /image-viewer-marker/);
|
assert.match(imageViewer, /image-viewer-marker/);
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
259
website/src/features/toolboxes/modules/textImport/basic.js
Normal file
259
website/src/features/toolboxes/modules/textImport/basic.js
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
// Rôle : gère l'import/export texte des outils simples et tabulaires.
|
||||||
|
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))}`);
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function cloneImportedNotepadDrawings(drawings, context) {
|
||||||
|
if (!drawings || typeof drawings !== "object" || !Array.isArray(drawings.strokes)) throw new Error("dessins invalides.");
|
||||||
|
return {
|
||||||
|
strokes: drawings.strokes.map((stroke) => {
|
||||||
|
if (!stroke || typeof stroke !== "object" || !Array.isArray(stroke.points) || stroke.points.length < 2) throw new Error("trait de dessin invalide.");
|
||||||
|
return {
|
||||||
|
...stroke,
|
||||||
|
id: context.uid("stroke"),
|
||||||
|
points: stroke.points.map((point) => {
|
||||||
|
const x = Number(point?.x);
|
||||||
|
const y = Number(point?.y);
|
||||||
|
if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error("point de dessin invalide.");
|
||||||
|
return { x, y };
|
||||||
|
})
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function importNotepadText(text, context) {
|
||||||
|
const lines = splitLines(text);
|
||||||
|
const hasStructuredMarker = lines.some((line) => ["@notepad", "@html", "@endhtml"].includes(line.trim()) || parseMetaLine(line));
|
||||||
|
if (!hasStructuredMarker) return context.normalizeNotepadData({ text });
|
||||||
|
|
||||||
|
let readingHtml = false;
|
||||||
|
let htmlWasOpened = false;
|
||||||
|
let htmlWasClosed = false;
|
||||||
|
const htmlLines = [];
|
||||||
|
let drawingMode = "temporary";
|
||||||
|
let drawings = { strokes: [] };
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
const cleanLine = line.trim();
|
||||||
|
if (readingHtml) {
|
||||||
|
if (cleanLine === "@endhtml") {
|
||||||
|
readingHtml = false;
|
||||||
|
htmlWasClosed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
htmlLines.push(line);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!cleanLine || cleanLine === "@notepad") return;
|
||||||
|
if (cleanLine === "@html") {
|
||||||
|
if (htmlWasOpened) throw lineError(index, "bloc html dupliqué.");
|
||||||
|
readingHtml = true;
|
||||||
|
htmlWasOpened = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const meta = parseMetaLine(cleanLine);
|
||||||
|
if (meta?.key === "drawingMode") {
|
||||||
|
if (!["temporary", "permanent"].includes(meta.value)) throw lineError(index, "mode de dessin invalide.");
|
||||||
|
drawingMode = meta.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (meta?.key === "drawings") {
|
||||||
|
try {
|
||||||
|
drawings = cloneImportedNotepadDrawings(JSON.parse(meta.value), context);
|
||||||
|
} catch (error) {
|
||||||
|
throw lineError(index, error?.message || "dessins invalides.");
|
||||||
|
}
|
||||||
|
if (drawings.strokes.length) drawingMode = "permanent";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw lineError(index, `ligne bloc notes invalide: ${cleanLine}`);
|
||||||
|
});
|
||||||
|
if (readingHtml || (htmlWasOpened && !htmlWasClosed)) throw new Error("Bloc HTML non terminé.");
|
||||||
|
return context.normalizeNotepadData({ html: htmlLines.join("\n").trim(), drawingMode, drawings });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function exportChecklistText(data) {
|
||||||
|
return (data.sections || []).flatMap((section) => [
|
||||||
|
section.title ? `# ${section.title}` : "",
|
||||||
|
...(section.items || []).map((item) => `${item.label}: ${Math.max(1, parseInteger(item.qtyTarget, 1))}`)
|
||||||
|
]).filter((line, index, lines) => line || lines[index + 1]).join("\n").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function importChecklistText(text, context) {
|
||||||
|
const sections = [];
|
||||||
|
let current = { id: context.uid("section"), title: "", items: [] };
|
||||||
|
splitLines(text).forEach((line) => {
|
||||||
|
const cleanLine = line.trim();
|
||||||
|
if (!cleanLine) return;
|
||||||
|
if (cleanLine.startsWith("#")) {
|
||||||
|
if (current.items.length) sections.push(current);
|
||||||
|
current = { id: context.uid("section"), title: cleanLine.replace(/^#+/, "").trim(), items: [] };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parseColonImportLines(cleanLine).forEach((entry) => {
|
||||||
|
current.items.push({
|
||||||
|
id: context.uid("item"),
|
||||||
|
label: entry.label,
|
||||||
|
qtyTarget: Math.max(1, parseInteger(String(entry.value).split("/").pop(), 1)),
|
||||||
|
qtyCurrent: 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (current.items.length) sections.push(current);
|
||||||
|
return context.normalizeChecklistData({ sections });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function exportLinksText(data) {
|
||||||
|
return (data.links || []).map((link) => link.title ? `${link.title}: ${link.url}` : link.url).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function importLinksText(text, context) {
|
||||||
|
const links = [];
|
||||||
|
splitLines(text).forEach((rawLine, index) => {
|
||||||
|
const line = rawLine.trim();
|
||||||
|
if (!line) return;
|
||||||
|
const separatorIndex = line.indexOf(":");
|
||||||
|
const maybeUrl = separatorIndex >= 0 ? line.slice(separatorIndex + 1).trim() : line;
|
||||||
|
const title = separatorIndex >= 0 && !/^[a-z][a-z0-9+.-]*:\/\//i.test(line) ? line.slice(0, separatorIndex).trim() : "";
|
||||||
|
const url = context.normalizeUrl(title ? maybeUrl : line);
|
||||||
|
if (!url) throw lineError(index, `URL invalide: ${line}`);
|
||||||
|
links.push({ id: context.uid("link"), title, url });
|
||||||
|
});
|
||||||
|
return context.normalizeLinksData({ links });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function exportCountersText(data) {
|
||||||
|
return (data.counters || []).map((counter) => `${counter.label}: ${counter.value}`).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function importCountersText(text, context) {
|
||||||
|
const counters = [];
|
||||||
|
splitLines(text).forEach((line, index) => {
|
||||||
|
const cleanLine = line.trim();
|
||||||
|
if (!cleanLine) return;
|
||||||
|
const separatorIndex = cleanLine.indexOf(":");
|
||||||
|
const entry = separatorIndex === -1
|
||||||
|
? { label: cleanLine, value: "" }
|
||||||
|
: { label: cleanLine.slice(0, separatorIndex).trim(), value: cleanLine.slice(separatorIndex + 1).trim() };
|
||||||
|
if (!entry.label) return;
|
||||||
|
if (!entry.value && entry.value !== "0") throw lineError(index, `valeur manquante pour ${entry.label}.`);
|
||||||
|
const value = Number.parseInt(entry.value, 10);
|
||||||
|
if (!Number.isFinite(value)) throw lineError(index, `valeur invalide pour ${entry.label}.`);
|
||||||
|
counters.push({ id: context.uid("counter"), label: entry.label, value });
|
||||||
|
});
|
||||||
|
return context.normalizeCountersData({ counters });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function exportCalculatorText(data) {
|
||||||
|
const lines = [];
|
||||||
|
if (data.scrollResults) lines.push("@scrollResults: true");
|
||||||
|
function visit(parentId = "", depth = 0) {
|
||||||
|
(data.entries || []).filter((entry) => (entry.parentId || "") === parentId).forEach((entry) => {
|
||||||
|
const formula = String(entry.formula || "").trim();
|
||||||
|
lines.push(`${" ".repeat(depth)}${entry.label || ""}: ${entry.value}${formula ? ` | formula=${formula}` : ""}`);
|
||||||
|
visit(entry.id, depth + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
visit();
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function importCalculatorText(text, context) {
|
||||||
|
const entries = [];
|
||||||
|
const parentStack = [];
|
||||||
|
let scrollResults = false;
|
||||||
|
splitLines(text).forEach((line, index) => {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
const meta = parseMetaLine(line);
|
||||||
|
if (meta?.key === "scrollResults") {
|
||||||
|
scrollResults = parseBoolean(meta.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const depth = Math.floor((line.match(/^ */)?.[0].length || 0) / 2);
|
||||||
|
const separatorIndex = line.indexOf(":");
|
||||||
|
if (separatorIndex < 0) throw lineError(index, `ligne calculateur invalide: ${line.trim()}`);
|
||||||
|
const label = line.slice(0, separatorIndex).trim();
|
||||||
|
const [rawValue, rawFormula = ""] = line.slice(separatorIndex + 1).split(/\s+\|\s+formula=/);
|
||||||
|
const value = parseNumber(rawValue, Number.NaN);
|
||||||
|
if (!Number.isFinite(value)) throw lineError(index, `valeur calculateur invalide: ${line.trim()}`);
|
||||||
|
const entry = { id: context.uid("calc"), parentId: parentStack[depth - 1] || "", label, value, formula: rawFormula.trim() };
|
||||||
|
entries.push(entry);
|
||||||
|
parentStack[depth] = entry.id;
|
||||||
|
parentStack.length = depth + 1;
|
||||||
|
});
|
||||||
|
return context.normalizeCalculatorData({ entries, scrollResults });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function exportTableTsv(data) {
|
||||||
|
const lines = [];
|
||||||
|
if (data.rows !== 10 || data.columns !== 6) lines.push(`@size: ${data.rows}x${data.columns}`);
|
||||||
|
if (Object.keys(data.columnLabels || {}).length) lines.push(`@columns: ${Array.from({ length: data.columns }, (_, index) => data.columnLabels?.[index] || columnIndexToName(index)).join("\t")}`);
|
||||||
|
if (Object.keys(data.rowLabels || {}).length) lines.push(`@rows: ${Array.from({ length: data.rows }, (_, index) => data.rowLabels?.[index] || index + 1).join("\t")}`);
|
||||||
|
if (lines.length) lines.push("@tsv");
|
||||||
|
for (let rowIndex = 0; rowIndex < data.rows; rowIndex += 1) {
|
||||||
|
lines.push(Array.from({ length: data.columns }, (_, columnIndex) => data.cells?.[cellAddress(rowIndex, columnIndex)] || "").join("\t"));
|
||||||
|
}
|
||||||
|
return lines.join("\n").replace(/\n+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function importTableTsv(text) {
|
||||||
|
const lines = splitLines(text);
|
||||||
|
let rows = 0;
|
||||||
|
let columns = 0;
|
||||||
|
let rowLabels = {};
|
||||||
|
let columnLabels = {};
|
||||||
|
let dataLines = lines;
|
||||||
|
const metaLines = [];
|
||||||
|
const tsvIndex = lines.findIndex((line) => line.trim() === "@tsv");
|
||||||
|
if (tsvIndex >= 0) {
|
||||||
|
metaLines.push(...lines.slice(0, tsvIndex));
|
||||||
|
dataLines = lines.slice(tsvIndex + 1);
|
||||||
|
}
|
||||||
|
metaLines.forEach((line, index) => {
|
||||||
|
const meta = parseMetaLine(line);
|
||||||
|
if (!meta) throw lineError(index, `meta tableau invalide: ${line}`);
|
||||||
|
if (meta.key === "size") {
|
||||||
|
const match = meta.value.match(/^(\d+)x(\d+)$/);
|
||||||
|
if (!match) throw lineError(index, "taille de tableau invalide.");
|
||||||
|
rows = parseInteger(match[1], 0);
|
||||||
|
columns = parseInteger(match[2], 0);
|
||||||
|
}
|
||||||
|
if (meta.key === "columns") {
|
||||||
|
columnLabels = Object.fromEntries(meta.value.split("\t").map((value, index) => [index, value]).filter(([index, value]) => value && value !== columnIndexToName(index)));
|
||||||
|
}
|
||||||
|
if (meta.key === "rows") {
|
||||||
|
rowLabels = Object.fromEntries(meta.value.split("\t").map((value, index) => [index, value]).filter(([index, value]) => value && value !== String(index + 1)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const tableRows = dataLines.filter((line) => line.trim() || line.includes("\t")).map((line) => line.split("\t"));
|
||||||
|
if (!tableRows.length) throw new Error("TSV vide.");
|
||||||
|
rows = rows || tableRows.length;
|
||||||
|
columns = columns || Math.max(...tableRows.map((row) => row.length));
|
||||||
|
const cells = {};
|
||||||
|
for (let rowIndex = 0; rowIndex < Math.min(rows, 50); rowIndex += 1) {
|
||||||
|
for (let columnIndex = 0; columnIndex < Math.min(columns, 20); columnIndex += 1) {
|
||||||
|
const value = tableRows[rowIndex]?.[columnIndex] || "";
|
||||||
|
if (value.trim()) cells[cellAddress(rowIndex, columnIndex)] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { rows, columns, cells, rowLabels, columnLabels };
|
||||||
|
}
|
||||||
84
website/src/features/toolboxes/modules/textImport/combos.js
Normal file
84
website/src/features/toolboxes/modules/textImport/combos.js
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
// Rôle : gère l'import/export texte des combos de commandes.
|
||||||
|
import { formatMeta, lineError, parseInteger, parseMetaLine, parsePipeParts, splitLines } from "./shared.js";
|
||||||
|
|
||||||
|
const COMBO_DEVICES = new Set(["playstation", "xbox", "switch", "n64", "keyboardMouse"]);
|
||||||
|
const COMBO_INPUT_KINDS = new Set(["button", "direction", "key", "mouse"]);
|
||||||
|
|
||||||
|
export function exportComboInput(input) {
|
||||||
|
let suffix = "";
|
||||||
|
if (input.holdMs) suffix = `[${Math.round(input.holdMs / 1000)}s]`;
|
||||||
|
else if (input.hold) suffix = "[hold]";
|
||||||
|
return `${input.kind}:${input.value}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function parseComboInput(text) {
|
||||||
|
const match = String(text || "").trim().match(/^([a-zA-Z]+):([^[]+?)(?:\[(hold|\d+s)\])?$/);
|
||||||
|
if (!match || !COMBO_INPUT_KINDS.has(match[1])) throw new Error(`Input combo invalide: ${text}`);
|
||||||
|
const input = { kind: match[1], value: match[2].trim() };
|
||||||
|
if (!input.value) throw new Error(`Input combo invalide: ${text}`);
|
||||||
|
if (match[3] === "hold") input.hold = true;
|
||||||
|
if (match[3]?.endsWith("s")) {
|
||||||
|
input.hold = true;
|
||||||
|
input.holdMs = Math.max(1000, Math.min(99000, parseInteger(match[3], 1) * 1000));
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function exportCombosText(data) {
|
||||||
|
const lines = [];
|
||||||
|
if (data.device && data.device !== "playstation") lines.push(`@device: ${data.device}`);
|
||||||
|
if (data.collapsedCategories?.length) lines.push(`@collapsed: ${data.collapsedCategories.join(", ")}`);
|
||||||
|
let lastCategory = null;
|
||||||
|
(data.combos || []).forEach((combo) => {
|
||||||
|
const category = combo.category || "";
|
||||||
|
if (category && category !== lastCategory) {
|
||||||
|
lines.push(`# ${category}`);
|
||||||
|
lastCategory = category;
|
||||||
|
}
|
||||||
|
const meta = formatMeta({ device: combo.device || "" });
|
||||||
|
const sequence = (combo.inputs || []).map((step) => step.map(exportComboInput).join("+")).join(" > ");
|
||||||
|
lines.push([combo.name, meta, sequence].filter(Boolean).join(" | "));
|
||||||
|
});
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function importCombosText(text, context) {
|
||||||
|
const combos = [];
|
||||||
|
let device = "playstation";
|
||||||
|
let collapsedCategories = [];
|
||||||
|
let category = "";
|
||||||
|
splitLines(text).forEach((line, index) => {
|
||||||
|
const cleanLine = line.trim();
|
||||||
|
if (!cleanLine) return;
|
||||||
|
const meta = parseMetaLine(cleanLine);
|
||||||
|
if (meta?.key === "device") {
|
||||||
|
if (!COMBO_DEVICES.has(meta.value)) throw lineError(index, "périphérique combo invalide.");
|
||||||
|
device = meta.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (meta?.key === "collapsed") {
|
||||||
|
collapsedCategories = meta.value.split(",").map((item) => item.trim()).filter(Boolean);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cleanLine.startsWith("#")) {
|
||||||
|
category = cleanLine.replace(/^#+/, "").trim();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { title, meta: lineMeta } = parsePipeParts(cleanLine);
|
||||||
|
const sequence = cleanLine.split("|").map((part) => part.trim()).filter((part, index) => index > 0 && !part.includes("=")).join(" | ");
|
||||||
|
const comboDevice = lineMeta.device || device;
|
||||||
|
if (!COMBO_DEVICES.has(comboDevice)) throw lineError(index, `périphérique combo invalide: ${comboDevice}`);
|
||||||
|
let inputs;
|
||||||
|
try {
|
||||||
|
inputs = sequence ? String(sequence).split(">").map((step) => step.split("+").map(parseComboInput)) : [];
|
||||||
|
} catch (error) {
|
||||||
|
throw lineError(index, error?.message || "input combo invalide.");
|
||||||
|
}
|
||||||
|
combos.push({ id: context.uid("combo"), name: title || "Combo", category, device: comboDevice, inputs });
|
||||||
|
});
|
||||||
|
return context.normalizeCombosData({ device, collapsedCategories, combos });
|
||||||
|
}
|
||||||
|
|
||||||
114
website/src/features/toolboxes/modules/textImport/equipment.js
Normal file
114
website/src/features/toolboxes/modules/textImport/equipment.js
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
// Rôle : gère l'import/export texte du planificateur d'équipements.
|
||||||
|
import { formatMeta, lineError, parseBoolean, parseColonImportLines, parseInteger, parseMetaLine, parsePipeParts, splitLines } from "./shared.js";
|
||||||
|
|
||||||
|
export function exportEquipmentPlannerText(data) {
|
||||||
|
const lines = [];
|
||||||
|
const typesById = new Map((data.types || []).map((type) => [type.id, type]));
|
||||||
|
(data.types || [{ id: "", title: "Équipements", icon: "shield", collapsed: false }]).forEach((type) => {
|
||||||
|
const equipments = (data.equipments || []).filter((equipment) => type.id ? equipment.typeId === type.id : !typesById.has(equipment.typeId));
|
||||||
|
if (!equipments.length) return;
|
||||||
|
lines.push(`# ${type.title} | ${formatMeta({ icon: type.icon, collapsed: type.collapsed })}`);
|
||||||
|
equipments.forEach((equipment) => {
|
||||||
|
lines.push(`## ${equipment.name} | ${formatMeta({ icon: equipment.icon, active: equipment.active !== false })}`);
|
||||||
|
if (equipment.characteristics?.length) {
|
||||||
|
lines.push("Stats");
|
||||||
|
equipment.characteristics.forEach((trait) => lines.push(`${trait.category || ""} | ${trait.icon || "sword"} | ${trait.name} | ${trait.value ?? ""}`));
|
||||||
|
}
|
||||||
|
if (equipment.socketItems?.length) {
|
||||||
|
lines.push("Sockets");
|
||||||
|
equipment.socketItems.forEach((socketItem) => {
|
||||||
|
lines.push(`${socketItem.name} | shape=${socketItem.shape} | color=${socketItem.color}`);
|
||||||
|
(socketItem.bonuses || []).forEach((bonus) => lines.push(` bonus | ${bonus.category || ""} | ${bonus.icon || "sword"} | ${bonus.name} | ${bonus.value ?? ""}`));
|
||||||
|
});
|
||||||
|
if (equipment.socketLinks?.length) {
|
||||||
|
const socketItemsById = new Map(equipment.socketItems.map((socketItem) => [socketItem.id, socketItem.name]));
|
||||||
|
equipment.socketLinks.forEach((link) => {
|
||||||
|
const fromName = socketItemsById.get(link.fromSocketItemId);
|
||||||
|
const toName = socketItemsById.get(link.toSocketItemId);
|
||||||
|
if (fromName && toName) lines.push(` link | ${fromName} | ${toName}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (equipment.obtain || equipment.materials?.length) {
|
||||||
|
lines.push("Craft");
|
||||||
|
if (equipment.obtain) lines.push(`@obtain: ${equipment.obtain.replace(/\n/g, "\\n")}`);
|
||||||
|
(equipment.materials || []).forEach((material) => lines.push(`${material.name}: ${material.qty}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function importEquipmentPlannerText(text, context) {
|
||||||
|
const types = [];
|
||||||
|
const equipments = [];
|
||||||
|
let currentType = null;
|
||||||
|
let currentEquipment = null;
|
||||||
|
let section = "";
|
||||||
|
let currentSocket = null;
|
||||||
|
const socketsByName = new Map();
|
||||||
|
splitLines(text).forEach((line, index) => {
|
||||||
|
const cleanLine = line.trim();
|
||||||
|
if (!cleanLine) return;
|
||||||
|
if (cleanLine.startsWith("##")) {
|
||||||
|
if (!currentType) throw lineError(index, "équipement sans type.");
|
||||||
|
const { title, meta } = parsePipeParts(cleanLine.replace(/^##+/, "").trim());
|
||||||
|
currentEquipment = { id: context.uid("equipment"), typeId: currentType.id, name: title, icon: meta.icon || "shield", active: meta.active !== "false", obtain: "", characteristics: [], socketItems: [], socketLinks: [], materials: [], categoryOrder: [], collapsedCategories: [] };
|
||||||
|
equipments.push(currentEquipment);
|
||||||
|
section = "";
|
||||||
|
currentSocket = null;
|
||||||
|
socketsByName.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cleanLine.startsWith("#")) {
|
||||||
|
const { title, meta } = parsePipeParts(cleanLine.replace(/^#+/, "").trim());
|
||||||
|
currentType = { id: context.uid("equipmentType"), title, icon: meta.icon || "shield", collapsed: parseBoolean(meta.collapsed) };
|
||||||
|
types.push(currentType);
|
||||||
|
currentEquipment = null;
|
||||||
|
section = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (["Stats", "Sockets", "Craft"].includes(cleanLine)) {
|
||||||
|
if (!currentEquipment) throw lineError(index, `${cleanLine} sans équipement.`);
|
||||||
|
section = cleanLine;
|
||||||
|
currentSocket = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!currentEquipment || !section) throw lineError(index, `ligne équipement invalide: ${cleanLine}`);
|
||||||
|
if (section === "Stats") {
|
||||||
|
const [category = "", icon = "sword", name = "", value = ""] = cleanLine.split("|").map((part) => part.trim());
|
||||||
|
currentEquipment.characteristics.push({ id: context.uid("trait"), category, icon, name, value });
|
||||||
|
}
|
||||||
|
if (section === "Sockets") {
|
||||||
|
if (cleanLine.startsWith("bonus")) {
|
||||||
|
if (!currentSocket) throw lineError(index, "bonus sans objet serti.");
|
||||||
|
const [, category = "", icon = "sword", name = "", value = ""] = cleanLine.split("|").map((part) => part.trim());
|
||||||
|
currentSocket.bonuses.push({ id: context.uid("socketBonus"), category, icon, name, value });
|
||||||
|
} else if (cleanLine.startsWith("link")) {
|
||||||
|
const [, fromName = "", toName = ""] = cleanLine.split("|").map((part) => part.trim());
|
||||||
|
const fromSocketItemId = socketsByName.get(fromName);
|
||||||
|
const toSocketItemId = socketsByName.get(toName);
|
||||||
|
if (!fromSocketItemId || !toSocketItemId) throw lineError(index, "lien de sertissage invalide.");
|
||||||
|
currentEquipment.socketLinks.push({ id: context.uid("socketLink"), fromSocketItemId, toSocketItemId });
|
||||||
|
} else {
|
||||||
|
const { title, meta } = parsePipeParts(cleanLine);
|
||||||
|
currentSocket = { id: context.uid("socketItem"), name: title, shape: meta.shape || "jewel", color: meta.color || "yellow", bonuses: [] };
|
||||||
|
currentEquipment.socketItems.push(currentSocket);
|
||||||
|
socketsByName.set(title, currentSocket.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (section === "Craft") {
|
||||||
|
const meta = parseMetaLine(cleanLine);
|
||||||
|
if (meta?.key === "obtain") {
|
||||||
|
currentEquipment.obtain = meta.value.replace(/\\n/g, "\n");
|
||||||
|
} else {
|
||||||
|
const [entry] = parseColonImportLines(cleanLine);
|
||||||
|
if (!entry) throw lineError(index, `matériau invalide: ${cleanLine}`);
|
||||||
|
currentEquipment.materials.push({ id: context.uid("material"), name: entry.label, qty: Math.max(1, parseInteger(entry.value, 1)) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return context.normalizeEquipmentPlannerData({ types, equipments, typeOrder: types.map((type) => type.id) });
|
||||||
|
}
|
||||||
|
|
||||||
67
website/src/features/toolboxes/modules/textImport/media.js
Normal file
67
website/src/features/toolboxes/modules/textImport/media.js
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Rôle : gère l'import/export texte des outils images et annotations.
|
||||||
|
import { cloneDrawingData, isValidImageDataUrl, lineError, parseMetaLine, parseNumber, splitLines } from "./shared.js";
|
||||||
|
|
||||||
|
export function exportImagesText(data) {
|
||||||
|
return (data.images || []).flatMap((image) => [
|
||||||
|
image.label ? `# ${image.label}` : "#",
|
||||||
|
image.dataUrl
|
||||||
|
]).join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function importImagesText(text, context) {
|
||||||
|
const images = [];
|
||||||
|
let label = "";
|
||||||
|
splitLines(text).forEach((line, index) => {
|
||||||
|
const cleanLine = line.trim();
|
||||||
|
if (!cleanLine) return;
|
||||||
|
if (cleanLine.startsWith("#")) {
|
||||||
|
label = cleanLine.replace(/^#+/, "").trim();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isValidImageDataUrl(cleanLine)) throw lineError(index, "image base64 invalide.");
|
||||||
|
images.push({ id: context.uid("image"), label, dataUrl: cleanLine });
|
||||||
|
label = "";
|
||||||
|
});
|
||||||
|
return { images };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function exportImageAnnotationText(data) {
|
||||||
|
const lines = [data.image];
|
||||||
|
(data.markers || []).forEach((marker) => lines.push(`@marker: ${marker.x},${marker.y},${marker.label || ""}`));
|
||||||
|
if (data.drawings?.strokes?.length) lines.push(`@drawings: ${JSON.stringify(cloneDrawingData(data.drawings))}`);
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function importImageAnnotationText(text, context) {
|
||||||
|
const markers = [];
|
||||||
|
let image = "";
|
||||||
|
let drawings = { strokes: [] };
|
||||||
|
splitLines(text).forEach((line, index) => {
|
||||||
|
const cleanLine = line.trim();
|
||||||
|
if (!cleanLine) return;
|
||||||
|
const meta = parseMetaLine(cleanLine);
|
||||||
|
if (meta?.key === "marker") {
|
||||||
|
const [x, y, ...labelParts] = meta.value.split(",");
|
||||||
|
const marker = { id: context.uid("marker"), x: parseNumber(x, Number.NaN), y: parseNumber(y, Number.NaN), label: labelParts.join(",").trim() };
|
||||||
|
if (!Number.isFinite(marker.x) || !Number.isFinite(marker.y) || marker.x < 0 || marker.x > 100 || marker.y < 0 || marker.y > 100) throw lineError(index, "marqueur invalide.");
|
||||||
|
markers.push(marker);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (meta?.key === "drawings") {
|
||||||
|
try {
|
||||||
|
drawings = JSON.parse(meta.value);
|
||||||
|
} catch {
|
||||||
|
throw lineError(index, "dessins invalides.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (image) throw lineError(index, "une seule image peut être importée.");
|
||||||
|
if (!isValidImageDataUrl(cleanLine)) throw lineError(index, "image base64 invalide.");
|
||||||
|
image = cleanLine;
|
||||||
|
});
|
||||||
|
return context.normalizeImageAnnotationData({ image, markers, drawings });
|
||||||
|
}
|
||||||
|
|
||||||
172
website/src/features/toolboxes/modules/textImport/planning.js
Normal file
172
website/src/features/toolboxes/modules/textImport/planning.js
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
// Rôle : gère l'import/export texte des plans de tâches et arbres d'objectifs.
|
||||||
|
import { formatMeta, lineError, parseBoolean, parseInteger, parseMetaLine, parseNumber, parsePipeParts, splitLines } from "./shared.js";
|
||||||
|
|
||||||
|
export function exportTaskPlannerText(data) {
|
||||||
|
const lines = [];
|
||||||
|
if (data.weeklyResetDay !== 1) lines.push(`@weeklyResetDay: ${data.weeklyResetDay}`);
|
||||||
|
if (data.resetTime && data.resetTime !== "00:00") lines.push(`@resetTime: ${data.resetTime}`);
|
||||||
|
if (data.hideCompleted) lines.push("@hideCompleted: true");
|
||||||
|
if (data.collapsedCategories?.length) lines.push(`@collapsed: ${data.collapsedCategories.join(", ")}`);
|
||||||
|
const parentByChild = new Map((data.relations || []).map((relation) => [relation.fromTaskId, relation]));
|
||||||
|
const childrenByParent = new Map();
|
||||||
|
(data.tasks || []).forEach((task) => {
|
||||||
|
const parentId = parentByChild.get(task.id)?.toTaskId || "";
|
||||||
|
childrenByParent.set(parentId, [...(childrenByParent.get(parentId) || []), task]);
|
||||||
|
});
|
||||||
|
function writeTask(task, depth = 0) {
|
||||||
|
const prefix = " ".repeat(depth);
|
||||||
|
lines.push(`${prefix}- ${task.type || "unique"} ${task.title}`);
|
||||||
|
if (task.description) lines.push(`${prefix} @description: ${task.description.replace(/\n/g, "\\n")}`);
|
||||||
|
if (task.dailyResetTime) lines.push(`${prefix} @dailyResetTime: ${task.dailyResetTime}`);
|
||||||
|
if (task.weeklyResetDay !== undefined) lines.push(`${prefix} @weeklyResetDay: ${task.weeklyResetDay}`);
|
||||||
|
if (parentByChild.get(task.id)?.prerequisite) lines.push(`${prefix} @prerequisite: true`);
|
||||||
|
(childrenByParent.get(task.id) || []).forEach((child) => writeTask(child, depth + 1));
|
||||||
|
}
|
||||||
|
let lastCategory = null;
|
||||||
|
(childrenByParent.get("") || []).forEach((task) => {
|
||||||
|
const category = task.category || "";
|
||||||
|
if (category && category !== lastCategory) {
|
||||||
|
lines.push(`# ${category}`);
|
||||||
|
lastCategory = category;
|
||||||
|
}
|
||||||
|
writeTask(task);
|
||||||
|
});
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function importTaskPlannerText(text, context) {
|
||||||
|
const tasks = [];
|
||||||
|
const relations = [];
|
||||||
|
const stack = [];
|
||||||
|
let category = "";
|
||||||
|
const data = { weeklyResetDay: 1, resetTime: "00:00", hideCompleted: false, collapsedCategories: [], tasks, relations };
|
||||||
|
splitLines(text).forEach((line, index) => {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
const cleanLine = line.trim();
|
||||||
|
const rootMeta = parseMetaLine(cleanLine);
|
||||||
|
if (rootMeta && !stack.length) {
|
||||||
|
if (rootMeta.key === "weeklyResetDay") data.weeklyResetDay = parseInteger(rootMeta.value, 1);
|
||||||
|
if (rootMeta.key === "resetTime") data.resetTime = rootMeta.value;
|
||||||
|
if (rootMeta.key === "hideCompleted") data.hideCompleted = parseBoolean(rootMeta.value);
|
||||||
|
if (rootMeta.key === "collapsed") data.collapsedCategories = rootMeta.value.split(",").map((item) => item.trim()).filter(Boolean);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cleanLine.startsWith("#")) {
|
||||||
|
category = cleanLine.replace(/^#+/, "").trim();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const depth = Math.floor((line.match(/^ */)?.[0].length || 0) / 2);
|
||||||
|
const meta = parseMetaLine(cleanLine);
|
||||||
|
if (meta) {
|
||||||
|
const task = stack[Math.max(0, depth - 1)];
|
||||||
|
if (!task) throw lineError(index, `meta de tâche sans tâche: ${cleanLine}`);
|
||||||
|
if (meta.key === "description") task.description = meta.value.replace(/\\n/g, "\n");
|
||||||
|
if (meta.key === "dailyResetTime") task.dailyResetTime = meta.value;
|
||||||
|
if (meta.key === "weeklyResetDay") task.weeklyResetDay = parseInteger(meta.value, 1);
|
||||||
|
if (meta.key === "prerequisite") {
|
||||||
|
const relation = relations.find((item) => item.fromTaskId === task.id);
|
||||||
|
if (relation) relation.prerequisite = parseBoolean(meta.value);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const match = cleanLine.match(/^-\s+(unique|daily|weekly)\s+(.+)$/);
|
||||||
|
if (!match) throw lineError(index, `ligne tâche invalide: ${cleanLine}`);
|
||||||
|
const task = { id: context.uid("task"), title: match[2].trim(), description: "", type: match[1], checked: false, checkedAt: 0 };
|
||||||
|
const parent = stack[depth - 1];
|
||||||
|
if (parent) relations.push({ id: context.uid("relation"), fromTaskId: task.id, toTaskId: parent.id, prerequisite: false });
|
||||||
|
else if (category) task.category = category;
|
||||||
|
tasks.push(task);
|
||||||
|
stack[depth] = task;
|
||||||
|
stack.length = depth + 1;
|
||||||
|
});
|
||||||
|
return context.normalizeTaskPlannerData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function exportGoalTreeText(data) {
|
||||||
|
const lines = [];
|
||||||
|
if (data.lastResetAt) lines.push(`@lastResetAt: ${data.lastResetAt}`);
|
||||||
|
function visit(parentId = "", depth = 0) {
|
||||||
|
(data.nodes || []).filter((node) => (node.parentId || "") === parentId).forEach((node) => {
|
||||||
|
const prefix = " ".repeat(depth);
|
||||||
|
const formula = String(node.formula || "").trim();
|
||||||
|
const qty = Number.isFinite(node.computedQtyTarget) ? node.computedQtyTarget : node.qtyTarget;
|
||||||
|
const suffix = node.kind === "step" ? (formula ? ` = ${formula}` : ` x${qty}`) : "";
|
||||||
|
const meta = formatMeta({
|
||||||
|
kind: node.kind !== "step" ? node.kind : "",
|
||||||
|
icon: node.kind === "step" && node.icon && node.icon !== "cube" ? node.icon : "",
|
||||||
|
color: node.kind === "step" && node.color && node.color !== "gold" ? node.color : "",
|
||||||
|
items: node.kind !== "step" && node.items?.length ? encodeURIComponent(JSON.stringify(node.items)) : "",
|
||||||
|
current: node.qtyCurrent || "",
|
||||||
|
reset: node.resetType !== "none" ? node.resetType : "",
|
||||||
|
resetTime: node.resetTime && node.resetTime !== "00:00" ? node.resetTime : "",
|
||||||
|
weeklyResetDay: node.weeklyResetDay !== 1 ? node.weeklyResetDay : "",
|
||||||
|
collapsed: node.collapsed
|
||||||
|
});
|
||||||
|
lines.push(`${prefix}- ${node.title}${suffix}${meta ? ` | ${meta}` : ""}`);
|
||||||
|
visit(node.id, depth + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
visit();
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function importGoalTreeText(text, context) {
|
||||||
|
const nodes = [];
|
||||||
|
const stack = [];
|
||||||
|
const data = { lastResetAt: 0, nodes };
|
||||||
|
splitLines(text).forEach((line, index) => {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
const cleanLine = line.trim();
|
||||||
|
const rootMeta = parseMetaLine(cleanLine);
|
||||||
|
if (rootMeta && !stack.length) {
|
||||||
|
if (rootMeta.key === "lastResetAt") data.lastResetAt = parseInteger(rootMeta.value, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const depth = Math.floor((line.match(/^ */)?.[0].length || 0) / 2);
|
||||||
|
const match = cleanLine.match(/^-\s+(.+)$/);
|
||||||
|
if (!match) throw lineError(index, `ligne arbre d'objectifs invalide: ${cleanLine}`);
|
||||||
|
const { title: rawTitle, meta } = parsePipeParts(match[1]);
|
||||||
|
const formulaIndex = rawTitle.lastIndexOf("=");
|
||||||
|
const quantityMatch = rawTitle.match(/\s+x(\d+(?:[,.]\d+)?)\s*$/i);
|
||||||
|
const hasFormula = formulaIndex >= 0;
|
||||||
|
const title = (hasFormula ? rawTitle.slice(0, formulaIndex) : rawTitle.replace(/\s+x(\d+(?:[,.]\d+)?)\s*$/i, "")).trim();
|
||||||
|
if (!title) throw lineError(index, "titre manquant.");
|
||||||
|
const parent = stack[depth - 1];
|
||||||
|
const node = {
|
||||||
|
id: context.uid("goal"),
|
||||||
|
parentId: parent?.id || "",
|
||||||
|
title,
|
||||||
|
kind: meta.kind || "step",
|
||||||
|
icon: meta.icon || "cube",
|
||||||
|
color: meta.color || "gold",
|
||||||
|
qtyTarget: hasFormula ? 1 : Math.max(0, parseNumber(quantityMatch?.[1], 1)) || 1,
|
||||||
|
qtyCurrent: 0,
|
||||||
|
formula: hasFormula ? rawTitle.slice(formulaIndex + 1).trim() : "",
|
||||||
|
items: parseGoalTreeItemsMeta(meta.items),
|
||||||
|
resetType: meta.reset || "none",
|
||||||
|
resetTime: meta.resetTime || "00:00",
|
||||||
|
weeklyResetDay: parseInteger(meta.weeklyResetDay, 1),
|
||||||
|
checkedAt: 0,
|
||||||
|
collapsed: parseBoolean(meta.collapsed)
|
||||||
|
};
|
||||||
|
nodes.push(node);
|
||||||
|
stack[depth] = node;
|
||||||
|
stack.length = depth + 1;
|
||||||
|
});
|
||||||
|
return context.normalizeGoalTreeData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function parseGoalTreeItemsMeta(value) {
|
||||||
|
if (!value) return [];
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(decodeURIComponent(value));
|
||||||
|
return Array.isArray(parsed) ? parsed : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
94
website/src/features/toolboxes/modules/textImport/shared.js
Normal file
94
website/src/features/toolboxes/modules/textImport/shared.js
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
// Rôle : fournit les helpers communs aux formats texte des outils toolbox.
|
||||||
|
const IMAGE_DATA_URL_PATTERN = /^data:image\/[a-z0-9.+-]+;base64,[a-z0-9+/=\s]+$/i;
|
||||||
|
|
||||||
|
export function parseColonImportLines(text) {
|
||||||
|
return String(text || "")
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((line) => {
|
||||||
|
const separatorIndex = line.indexOf(":");
|
||||||
|
if (separatorIndex === -1) return { label: line, value: "" };
|
||||||
|
return {
|
||||||
|
label: line.slice(0, separatorIndex).trim(),
|
||||||
|
value: line.slice(separatorIndex + 1).trim()
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((entry) => entry.label);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fail(error) {
|
||||||
|
return { ok: false, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitLines(text) {
|
||||||
|
return String(text || "").split(/\r?\n/);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseMetaLine(line) {
|
||||||
|
const match = String(line || "").trim().match(/^@([a-zA-Z][\w-]*)\s*:\s*(.*)$/);
|
||||||
|
return match ? { key: match[1], value: match[2] } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePipeParts(line) {
|
||||||
|
const [title = "", ...parts] = String(line || "").split("|").map((part) => part.trim());
|
||||||
|
const meta = {};
|
||||||
|
parts.forEach((part) => {
|
||||||
|
const separatorIndex = part.indexOf("=");
|
||||||
|
if (separatorIndex < 0) return;
|
||||||
|
meta[part.slice(0, separatorIndex).trim()] = part.slice(separatorIndex + 1).trim();
|
||||||
|
});
|
||||||
|
return { title, meta };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseBoolean(value) {
|
||||||
|
return String(value || "").trim().toLowerCase() === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lineError(index, message) {
|
||||||
|
return new Error(`Ligne ${index + 1}: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseNumber(value, fallback = 0) {
|
||||||
|
const parsed = Number(String(value || "").replace(",", "."));
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseInteger(value, fallback = 0) {
|
||||||
|
const parsed = Number.parseInt(value, 10);
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatMeta(meta) {
|
||||||
|
return Object.entries(meta)
|
||||||
|
.filter(([, value]) => value !== "" && value !== undefined && value !== false)
|
||||||
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
.join(" | ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidImageDataUrl(value) {
|
||||||
|
return IMAGE_DATA_URL_PATTERN.test(String(value || "").trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cloneDrawingData(drawings) {
|
||||||
|
return {
|
||||||
|
strokes: (drawings?.strokes || []).map((stroke) => ({
|
||||||
|
...stroke,
|
||||||
|
points: (stroke.points || []).map((point) => ({ ...point }))
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function textToNotepadExchangeHtml(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line ? `<p>${escapeHtml(line)}</p>` : "<p><br></p>")
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
197
website/src/features/toolboxes/modules/textImport/time.js
Normal file
197
website/src/features/toolboxes/modules/textImport/time.js
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
// Rôle : gère l'import/export texte des outils temporels timer et calendrier.
|
||||||
|
import { CALENDAR_ALERT_MODES, CALENDAR_ALERT_OFFSETS, CALENDAR_COLORS, CALENDAR_MONTHLY_REPEAT_BY, CALENDAR_MONTH_REPEAT_MODES, minutesToTimeValue, timeValueToMinutes } from "../calendarUtils.js";
|
||||||
|
import { getCountdownTargetMs, getDailyTargetMs, getIntervalAnchorMs } from "../timerUtils.js";
|
||||||
|
import { formatMeta, lineError, parseBoolean, parseInteger, parseMetaLine, parsePipeParts, splitLines } from "./shared.js";
|
||||||
|
|
||||||
|
const TIMER_TYPES = new Set(["duration", "daily_time", "time_pattern", "interval"]);
|
||||||
|
const ALERT_MODES = new Set(["off", "visible", "site"]);
|
||||||
|
const CALENDAR_ALERT_MODE_SET = new Set(CALENDAR_ALERT_MODES);
|
||||||
|
const CALENDAR_ALERT_OFFSET_SET = new Set(CALENDAR_ALERT_OFFSETS);
|
||||||
|
const CALENDAR_COLOR_SET = new Set(CALENDAR_COLORS);
|
||||||
|
const CALENDAR_MONTHLY_REPEAT_BY_SET = new Set(CALENDAR_MONTHLY_REPEAT_BY);
|
||||||
|
const CALENDAR_MONTH_REPEAT_MODE_SET = new Set(CALENDAR_MONTH_REPEAT_MODES);
|
||||||
|
const CALENDAR_WEEKDAYS = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"];
|
||||||
|
|
||||||
|
export function exportTimerText(data) {
|
||||||
|
const lines = [];
|
||||||
|
if (data.activeTab && data.activeTab !== "stopwatch") lines.push(`@activeTab: ${data.activeTab}`);
|
||||||
|
if (data.scrollResults) lines.push("@scrollResults: true");
|
||||||
|
if (data.sortResults) lines.push("@sortResults: true");
|
||||||
|
if (data.stopwatch?.elapsedMs) lines.push(`@stopwatchElapsedMs: ${data.stopwatch.elapsedMs}`);
|
||||||
|
(data.stopwatch?.laps || []).forEach((lap) => lines.push(`lap | label=${lap.label || ""} | elapsedMs=${lap.elapsedMs}`));
|
||||||
|
(data.countdowns || []).forEach((countdown) => {
|
||||||
|
const meta = { label: countdown.label, type: countdown.type, alertMode: countdown.alertMode || "", autoRefresh: countdown.autoRefresh === true };
|
||||||
|
if (countdown.type === "duration") meta.durationMs = countdown.durationMs;
|
||||||
|
if (countdown.type === "daily_time") meta.time = countdown.time;
|
||||||
|
if (countdown.type === "time_pattern") meta.pattern = countdown.pattern;
|
||||||
|
if (countdown.type === "interval") {
|
||||||
|
meta.intervalMs = countdown.intervalMs;
|
||||||
|
meta.startMode = countdown.startMode || "now";
|
||||||
|
meta.startTime = countdown.startTime || "";
|
||||||
|
}
|
||||||
|
lines.push(`countdown | ${formatMeta(meta)}`);
|
||||||
|
});
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function importTimerText(text, context) {
|
||||||
|
const now = Date.now();
|
||||||
|
const data = { activeTab: "stopwatch", scrollResults: false, sortResults: false, stopwatch: { elapsedMs: 0, startedAt: 0, laps: [] }, countdowns: [] };
|
||||||
|
splitLines(text).forEach((line, index) => {
|
||||||
|
const cleanLine = line.trim();
|
||||||
|
if (!cleanLine) return;
|
||||||
|
const meta = parseMetaLine(cleanLine);
|
||||||
|
if (meta) {
|
||||||
|
if (meta.key === "activeTab") data.activeTab = meta.value === "countdown" ? "countdown" : "stopwatch";
|
||||||
|
if (meta.key === "scrollResults") data.scrollResults = parseBoolean(meta.value);
|
||||||
|
if (meta.key === "sortResults") data.sortResults = parseBoolean(meta.value);
|
||||||
|
if (meta.key === "stopwatchElapsedMs") data.stopwatch.elapsedMs = Math.max(0, parseInteger(meta.value, 0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { title, meta: lineMeta } = parsePipeParts(cleanLine);
|
||||||
|
if (title === "lap") {
|
||||||
|
data.stopwatch.laps.push({ id: context.uid("timer"), label: lineMeta.label || "", elapsedMs: Math.max(0, parseInteger(lineMeta.elapsedMs, 0)) });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (title !== "countdown") throw lineError(index, `ligne timer invalide: ${cleanLine}`);
|
||||||
|
const type = lineMeta.type;
|
||||||
|
if (!TIMER_TYPES.has(type)) throw lineError(index, `type timer invalide: ${type}`);
|
||||||
|
const countdown = { id: context.uid("timer"), label: lineMeta.label || "Timer", type, alertMode: ALERT_MODES.has(lineMeta.alertMode) ? lineMeta.alertMode : "off" };
|
||||||
|
if (type === "duration") {
|
||||||
|
countdown.durationMs = Math.max(1, parseInteger(lineMeta.durationMs, 0));
|
||||||
|
countdown.targetAt = now + countdown.durationMs;
|
||||||
|
}
|
||||||
|
if (type === "daily_time") {
|
||||||
|
countdown.time = lineMeta.time;
|
||||||
|
countdown.targetAt = getDailyTargetMs(countdown.time, now);
|
||||||
|
countdown.autoRefresh = parseBoolean(lineMeta.autoRefresh);
|
||||||
|
}
|
||||||
|
if (type === "time_pattern") {
|
||||||
|
countdown.pattern = lineMeta.pattern;
|
||||||
|
countdown.autoRefresh = parseBoolean(lineMeta.autoRefresh);
|
||||||
|
}
|
||||||
|
if (type === "interval") {
|
||||||
|
countdown.intervalMs = Math.max(1, parseInteger(lineMeta.intervalMs, 0));
|
||||||
|
countdown.startMode = lineMeta.startMode === "time" && lineMeta.startTime ? "time" : "now";
|
||||||
|
if (countdown.startMode === "time") countdown.startTime = lineMeta.startTime;
|
||||||
|
countdown.anchorAt = getIntervalAnchorMs(countdown.startMode, countdown.startTime, now);
|
||||||
|
countdown.targetAt = getCountdownTargetMs({ ...countdown, autoRefresh: true }, now);
|
||||||
|
countdown.autoRefresh = parseBoolean(lineMeta.autoRefresh);
|
||||||
|
}
|
||||||
|
data.countdowns.push(countdown);
|
||||||
|
});
|
||||||
|
return context.normalizeTimerData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function parseCalendarDayIndex(value) {
|
||||||
|
const normalized = String(value || "").trim().toLowerCase();
|
||||||
|
const weekdayIndex = CALENDAR_WEEKDAYS.indexOf(normalized);
|
||||||
|
if (weekdayIndex >= 0) return weekdayIndex;
|
||||||
|
const parsed = parseInteger(normalized, -1);
|
||||||
|
return parsed >= 0 && parsed <= 6 ? parsed : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function parseCalendarAlertOffsets(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.split(",")
|
||||||
|
.map((item) => parseInteger(item.trim(), Number.NaN))
|
||||||
|
.filter((offset, index, offsets) => CALENDAR_ALERT_OFFSET_SET.has(offset) && offsets.indexOf(offset) === index)
|
||||||
|
.sort((a, b) => b - a);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function formatCalendarEventMeta(event, viewMode) {
|
||||||
|
const alertMode = CALENDAR_ALERT_MODE_SET.has(event.alertMode) ? event.alertMode : "off";
|
||||||
|
const meta = {
|
||||||
|
title: event.title,
|
||||||
|
color: event.color && event.color !== "violet" ? event.color : "",
|
||||||
|
repeatable: event.repeatable === false ? "false" : "",
|
||||||
|
alertMode: alertMode !== "off" ? alertMode : "",
|
||||||
|
alerts: alertMode !== "off" ? (event.alertOffsetMinutes || [10]).join(",") : ""
|
||||||
|
};
|
||||||
|
if (viewMode === "month") {
|
||||||
|
const monthlyRepeatBy = CALENDAR_MONTHLY_REPEAT_BY_SET.has(event.monthlyRepeatBy) ? event.monthlyRepeatBy : "monthDay";
|
||||||
|
if (monthlyRepeatBy === "weekday") {
|
||||||
|
return {
|
||||||
|
...meta,
|
||||||
|
repeatBy: "weekday",
|
||||||
|
repeat: event.repeat || "none",
|
||||||
|
startDate: event.startDate,
|
||||||
|
endDate: event.endDate && event.endDate !== event.startDate ? event.endDate : ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...meta,
|
||||||
|
repeatBy: "monthDay",
|
||||||
|
repeat: event.repeat || "none",
|
||||||
|
startDate: event.startDate,
|
||||||
|
endDate: event.endDate && event.endDate !== event.startDate ? event.endDate : ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...meta,
|
||||||
|
day: CALENDAR_WEEKDAYS[event.dayIndex] || "",
|
||||||
|
start: minutesToTimeValue(event.startMinutes),
|
||||||
|
end: event.endMinutes ? minutesToTimeValue(event.endMinutes) : ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function exportCalendarText(data) {
|
||||||
|
const viewMode = data?.viewMode === "month" ? "month" : "week";
|
||||||
|
const lines = [`@viewMode: ${viewMode}`];
|
||||||
|
if (data?.weekStartsOn === 0) lines.push("@weekStartsOn: 0");
|
||||||
|
(data?.events || []).forEach((event) => {
|
||||||
|
lines.push(`event | ${formatMeta(formatCalendarEventMeta(event, viewMode))}`);
|
||||||
|
});
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function importCalendarText(text, context) {
|
||||||
|
const trimmed = String(text || "").trim();
|
||||||
|
if (trimmed.startsWith("{")) {
|
||||||
|
return context.normalizeCalendarData(JSON.parse(trimmed));
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = { initialized: true, viewMode: "week", weekStartsOn: 1, events: [] };
|
||||||
|
splitLines(text).forEach((line, index) => {
|
||||||
|
const cleanLine = line.trim();
|
||||||
|
if (!cleanLine) return;
|
||||||
|
const meta = parseMetaLine(cleanLine);
|
||||||
|
if (meta) {
|
||||||
|
if (meta.key === "viewMode") data.viewMode = meta.value === "month" ? "month" : "week";
|
||||||
|
if (meta.key === "weekStartsOn") data.weekStartsOn = parseInteger(meta.value, 1) === 0 ? 0 : 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, meta: lineMeta } = parsePipeParts(cleanLine);
|
||||||
|
if (title !== "event") throw lineError(index, `ligne calendrier invalide: ${cleanLine}`);
|
||||||
|
const event = {
|
||||||
|
id: context.uid("calendar"),
|
||||||
|
title: lineMeta.title || "",
|
||||||
|
repeatable: lineMeta.repeatable !== "false",
|
||||||
|
alertMode: CALENDAR_ALERT_MODE_SET.has(lineMeta.alertMode) ? lineMeta.alertMode : "off",
|
||||||
|
alertOffsetMinutes: parseCalendarAlertOffsets(lineMeta.alerts),
|
||||||
|
color: CALENDAR_COLOR_SET.has(lineMeta.color) ? lineMeta.color : "violet"
|
||||||
|
};
|
||||||
|
if (data.viewMode === "month") {
|
||||||
|
const monthlyRepeatBy = CALENDAR_MONTHLY_REPEAT_BY_SET.has(lineMeta.repeatBy) ? lineMeta.repeatBy : "monthDay";
|
||||||
|
event.startMinutes = 0;
|
||||||
|
event.monthlyRepeatBy = monthlyRepeatBy;
|
||||||
|
event.repeat = CALENDAR_MONTH_REPEAT_MODE_SET.has(lineMeta.repeat) ? lineMeta.repeat : "none";
|
||||||
|
event.startDate = lineMeta.startDate || "";
|
||||||
|
event.endDate = lineMeta.endDate || event.startDate;
|
||||||
|
} else {
|
||||||
|
event.dayIndex = parseCalendarDayIndex(lineMeta.day);
|
||||||
|
event.startMinutes = timeValueToMinutes(lineMeta.start);
|
||||||
|
if (lineMeta.end) event.endMinutes = timeValueToMinutes(lineMeta.end);
|
||||||
|
}
|
||||||
|
data.events.push(event);
|
||||||
|
});
|
||||||
|
return context.normalizeCalendarData(data);
|
||||||
|
}
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load diff
401
website/src/styles/toolboxes/goal-tree/_editor.scss
Normal file
401
website/src/styles/toolboxes/goal-tree/_editor.scss
Normal file
|
|
@ -0,0 +1,401 @@
|
||||||
|
// Rôle : stylise les panneaux d'édition et contrôles de l'arbre d'objectifs.
|
||||||
|
.goal-tree-module {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(320px, 40fr) minmax(0, 60fr);
|
||||||
|
column-gap: var(--space-6);
|
||||||
|
row-gap: var(--space-4);
|
||||||
|
align-items: start;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module[data-module-type="goalTree"] {
|
||||||
|
contain: paint;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module[data-module-type="goalTree"] > .module-content {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module[data-module-type="goalTree"] > .module-content.is-scrollable {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-editor {
|
||||||
|
align-content: start;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-editor label,
|
||||||
|
.goal-tree-selected-form label {
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-editor input,
|
||||||
|
.goal-tree-editor select {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-field-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-selected-form > button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-selected-form {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding-top: var(--space-2);
|
||||||
|
border-top: 1px solid rgba(150, 165, 205, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-form-tabs {
|
||||||
|
width: 100%;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(72px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-form-tabs button {
|
||||||
|
min-height: 34px;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-form-tabs .module-icon-svg {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
flex: 0 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-form-tabs .module-icon-settings {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-basis: 16px;
|
||||||
|
transform: scale(1.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-form-tabs button > span:not(.module-icon-svg) {
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-form-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-form-section-title,
|
||||||
|
.goal-tree-color-field > span,
|
||||||
|
.goal-tree-icon-field > span {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-content-editor {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-content-editor-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 30px;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-content-editor-row.is-checklist {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 30px;
|
||||||
|
grid-template-areas:
|
||||||
|
"total delete"
|
||||||
|
"label delete";
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-content-label-input {
|
||||||
|
grid-area: label;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-content-formula-input,
|
||||||
|
.goal-tree-content-editor-row.is-checklist .mix-tags-input-wrap {
|
||||||
|
grid-area: total;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-content-editor-row.is-checklist button {
|
||||||
|
grid-area: delete;
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-content-editor-row > input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-content-editor-row button {
|
||||||
|
min-width: 30px;
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-content-qty-stepper {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 30px minmax(42px, auto) 30px;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 800;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-content-qty-stepper > span {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-content-hint {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-content-add-button {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 7px;
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-split-scroll-toggle.goal-tree-progress-mode-toggle {
|
||||||
|
grid-template-columns: 18px minmax(0, 1fr) 34px;
|
||||||
|
width: 100%;
|
||||||
|
justify-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-split-scroll-toggle.goal-tree-progress-mode-toggle span:not(.ui-icon) {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-progress-mode-hint {
|
||||||
|
margin: -2px 0 0;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.76rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-prerequisite-field,
|
||||||
|
.goal-tree-prerequisite-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-prerequisite-list {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-prerequisite-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: rgba(7, 10, 24, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-prerequisite-item input[type="checkbox"] {
|
||||||
|
display: grid;
|
||||||
|
width: 14px;
|
||||||
|
min-width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
min-height: 14px;
|
||||||
|
margin: 0;
|
||||||
|
place-items: center;
|
||||||
|
appearance: none;
|
||||||
|
border: 1px solid rgba(165, 180, 252, 0.28);
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(7, 10, 24, 0.56);
|
||||||
|
accent-color: var(--color-accent-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-prerequisite-item input[type="checkbox"]:checked {
|
||||||
|
border-color: rgba(246, 196, 83, 0.66);
|
||||||
|
background: rgba(246, 196, 83, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-prerequisite-item input[type="checkbox"]:checked::before {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
content: "✓";
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-prerequisite-item input[type="checkbox"]:disabled {
|
||||||
|
opacity: 0.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-prerequisite-item.is-primary {
|
||||||
|
border-color: rgba(246, 196, 83, 0.22);
|
||||||
|
background: rgba(246, 196, 83, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-prerequisite-item span {
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-color-field,
|
||||||
|
.goal-tree-icon-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-icon-picker {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-icon-picker-button {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: start;
|
||||||
|
min-height: 42px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-icon-picker-button > span:not(.goal-tree-icon-preview) {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-icon-preview {
|
||||||
|
display: grid;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid rgba(246, 196, 83, 0.24);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: rgba(246, 196, 83, 0.08);
|
||||||
|
color: var(--color-accent-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-icon-picker-menu {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 12;
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
left: 0;
|
||||||
|
display: grid;
|
||||||
|
width: min(260px, 72vw);
|
||||||
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid rgba(165, 180, 252, 0.14);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: rgba(7, 10, 24, 0.98);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-icon-picker-menu button {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 38px;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-icon-picker-menu button.active {
|
||||||
|
border-color: rgba(246, 196, 83, 0.62);
|
||||||
|
background: rgba(246, 196, 83, 0.1);
|
||||||
|
color: var(--color-accent-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-color-swatches {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-color-swatch {
|
||||||
|
width: 28px;
|
||||||
|
min-width: 28px;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
border-color: rgba(150, 165, 205, 0.18);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(255, 255, 255, 0.16), transparent 42%),
|
||||||
|
var(--goal-tree-swatch-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-color-swatch.active {
|
||||||
|
border-color: rgba(246, 196, 83, 0.76);
|
||||||
|
box-shadow: 0 0 0 2px rgba(246, 196, 83, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-form-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-form-actions button {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 36px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-form-actions button:not(.danger) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border-color: rgba(165, 180, 252, 0.14);
|
||||||
|
background: rgba(7, 10, 24, 0.28);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-form-actions button.danger {
|
||||||
|
flex: 1 1 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-form-actions .ui-icon {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
523
website/src/styles/toolboxes/goal-tree/_nodes.scss
Normal file
523
website/src/styles/toolboxes/goal-tree/_nodes.scss
Normal file
|
|
@ -0,0 +1,523 @@
|
||||||
|
// Rôle : stylise les cartes, contenus et états de drag de l'arbre d'objectifs.
|
||||||
|
.goal-tree-node {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
--goal-tree-card-color: var(--color-accent-gold);
|
||||||
|
min-width: 104px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-canvas.is-horizontal .goal-tree-node {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-text {
|
||||||
|
--goal-tree-card-color: var(--color-accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-checklist {
|
||||||
|
--goal-tree-card-color: #34d399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-card {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
width: 104px;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
grid-template-rows: minmax(0, 1fr);
|
||||||
|
grid-template-areas:
|
||||||
|
"main";
|
||||||
|
gap: 6px;
|
||||||
|
align-items: stretch;
|
||||||
|
align-content: stretch;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 104px;
|
||||||
|
padding: 7px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--goal-tree-card-color) 38%, rgba(150, 165, 205, 0.16));
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, color-mix(in srgb, var(--goal-tree-card-color) 10%, transparent), transparent 44%),
|
||||||
|
linear-gradient(160deg, rgba(139, 92, 246, 0.12), rgba(16, 20, 38, 0.72));
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 12px 22px rgba(0, 0, 0, 0.18),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||||
|
transition:
|
||||||
|
border-color var(--duration-fast) var(--ease-standard),
|
||||||
|
background var(--duration-fast) var(--ease-standard),
|
||||||
|
box-shadow var(--duration-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-card.has-actions {
|
||||||
|
grid-template-rows: minmax(0, 1fr) 22px;
|
||||||
|
grid-template-areas:
|
||||||
|
"main"
|
||||||
|
"actions";
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-hover-actions {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
right: 6px;
|
||||||
|
bottom: 7px;
|
||||||
|
left: 6px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 4px;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity var(--duration-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-card:hover .goal-tree-node-hover-actions,
|
||||||
|
.goal-tree-node-card:focus-within .goal-tree-node-hover-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-hover-actions button {
|
||||||
|
width: 24px;
|
||||||
|
min-width: 24px;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
border-color: rgba(165, 180, 252, 0.14);
|
||||||
|
background: rgba(7, 10, 24, 0.78);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-card:hover .goal-tree-node-hover-actions button,
|
||||||
|
.goal-tree-node-card:focus-within .goal-tree-node-hover-actions button {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-hover-actions .ui-icon {
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-text,
|
||||||
|
.goal-tree-node.is-checklist {
|
||||||
|
min-width: 184px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-text > .goal-tree-node-card,
|
||||||
|
.goal-tree-node.is-checklist > .goal-tree-node-card {
|
||||||
|
width: 184px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-card.is-viewing {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-selected > .goal-tree-node-card {
|
||||||
|
border-color: color-mix(in srgb, var(--goal-tree-card-color) 72%, rgba(246, 196, 83, 0.24));
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px color-mix(in srgb, var(--goal-tree-card-color) 22%, transparent),
|
||||||
|
0 12px 22px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-selected > .goal-tree-node-card.is-editing {
|
||||||
|
border-color: color-mix(in srgb, var(--goal-tree-card-color) 86%, var(--color-accent-gold));
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, color-mix(in srgb, var(--goal-tree-card-color) 20%, transparent), transparent 48%),
|
||||||
|
linear-gradient(160deg, rgba(139, 92, 246, 0.18), rgba(16, 20, 38, 0.82));
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 2px color-mix(in srgb, var(--goal-tree-card-color) 36%, transparent),
|
||||||
|
0 0 22px color-mix(in srgb, var(--goal-tree-card-color) 18%, transparent),
|
||||||
|
0 16px 28px rgba(0, 0, 0, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-complete > .goal-tree-node-card {
|
||||||
|
border-color: color-mix(in srgb, var(--goal-tree-card-color) 54%, rgba(34, 211, 238, 0.3));
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, color-mix(in srgb, var(--goal-tree-card-color) 12%, transparent), transparent 44%),
|
||||||
|
linear-gradient(160deg, rgba(139, 92, 246, 0.1), rgba(16, 20, 38, 0.72));
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-card:hover,
|
||||||
|
.goal-tree-node-card:focus-within {
|
||||||
|
border-color: color-mix(in srgb, var(--goal-tree-card-color) 68%, rgba(246, 196, 83, 0.24));
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, color-mix(in srgb, var(--goal-tree-card-color) 16%, transparent), transparent 48%),
|
||||||
|
linear-gradient(160deg, rgba(139, 92, 246, 0.14), rgba(16, 20, 38, 0.78));
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px color-mix(in srgb, var(--goal-tree-card-color) 16%, transparent),
|
||||||
|
0 14px 26px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-main {
|
||||||
|
grid-area: main;
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
justify-items: stretch;
|
||||||
|
gap: 5px;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 46px;
|
||||||
|
padding: 5px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-step > .goal-tree-node-card > .goal-tree-node-main {
|
||||||
|
justify-items: center;
|
||||||
|
min-height: 58px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-main:hover,
|
||||||
|
.goal-tree-node-main:focus {
|
||||||
|
border-color: transparent;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-heading {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-icon {
|
||||||
|
display: grid;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--goal-tree-node-color, var(--color-accent-gold)) 45%, transparent);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: color-mix(in srgb, var(--goal-tree-node-color, var(--color-accent-gold)) 14%, transparent);
|
||||||
|
color: var(--goal-tree-node-color, var(--color-accent-gold));
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-icon .ui-icon {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-step > .goal-tree-node-card > .goal-tree-node-main > .goal-tree-node-icon {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-step > .goal-tree-node-card > .goal-tree-node-main > .goal-tree-node-icon .ui-icon {
|
||||||
|
width: 27px;
|
||||||
|
height: 27px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-title {
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 900;
|
||||||
|
text-align: left;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-content-preview {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
gap: 3px;
|
||||||
|
justify-items: stretch;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-content-preview:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-content-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-checklist .goal-tree-node-content-row {
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-text .goal-tree-node-content-row {
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-text .goal-tree-node-content-row::before {
|
||||||
|
content: "";
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-content-row strong {
|
||||||
|
color: var(--color-accent-gold);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-content-label {
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-content-add-button {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 20px;
|
||||||
|
padding: 0;
|
||||||
|
place-items: center;
|
||||||
|
border-style: dashed;
|
||||||
|
border-color: color-mix(in srgb, var(--goal-tree-card-color, var(--color-accent-gold)) 56%, transparent);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: rgba(7, 10, 24, 0.2);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-content-add-button:hover,
|
||||||
|
.goal-tree-node-content-add-button:focus-visible {
|
||||||
|
border-color: color-mix(in srgb, var(--goal-tree-card-color, var(--color-accent-gold)) 78%, transparent);
|
||||||
|
background: color-mix(in srgb, var(--goal-tree-card-color, var(--color-accent-gold)) 14%, transparent);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-content-add-button .ui-icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-checklist .goal-tree-node-content-label {
|
||||||
|
padding-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-check {
|
||||||
|
display: grid;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
margin: 0;
|
||||||
|
accent-color: var(--color-accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-step-check {
|
||||||
|
justify-self: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin: 0;
|
||||||
|
accent-color: var(--color-accent-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-step-qty-stepper {
|
||||||
|
justify-self: center;
|
||||||
|
grid-template-columns: 18px minmax(34px, auto) 18px;
|
||||||
|
gap: 2px;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-step-qty-stepper button {
|
||||||
|
display: grid;
|
||||||
|
width: 18px;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
min-height: 18px;
|
||||||
|
padding: 0;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-step-qty-stepper .ui-icon {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-content-row .goal-tree-content-qty-stepper {
|
||||||
|
justify-self: start;
|
||||||
|
grid-template-columns: 16px minmax(30px, auto) 16px;
|
||||||
|
gap: 2px;
|
||||||
|
font-size: 0.64rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-content-row .goal-tree-content-qty-stepper button {
|
||||||
|
min-width: 16px;
|
||||||
|
min-height: 16px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-content-row .goal-tree-content-qty-stepper .ui-icon {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-progress-track {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
justify-self: stretch;
|
||||||
|
width: 100%;
|
||||||
|
height: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: rgba(150, 165, 205, 0.13);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-progress-track span {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: linear-gradient(90deg, var(--color-accent-gold), var(--color-accent-cyan));
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-actions {
|
||||||
|
grid-area: actions;
|
||||||
|
display: flex;
|
||||||
|
min-height: 22px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-actions button,
|
||||||
|
.goal-tree-drag-handle {
|
||||||
|
width: 22px;
|
||||||
|
min-width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
min-height: 22px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-actions .goal-tree-branch-toggle {
|
||||||
|
width: 22px;
|
||||||
|
min-width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
min-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-canvas.is-horizontal .goal-tree-branch-toggle .ui-icon-chevron-down,
|
||||||
|
.goal-tree-canvas.is-horizontal .goal-tree-branch-toggle .ui-icon-chevron-up {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-drag-handle {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
top: 6px;
|
||||||
|
left: 6px;
|
||||||
|
border-color: rgba(165, 180, 252, 0.34);
|
||||||
|
background: rgba(7, 10, 24, 0.82);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
cursor: grab;
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(7, 10, 24, 0.72),
|
||||||
|
0 6px 12px rgba(0, 0, 0, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-drag-handle:hover {
|
||||||
|
border-color: rgba(246, 196, 83, 0.42);
|
||||||
|
background: rgba(7, 10, 24, 0.92);
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(246, 196, 83, 0.12),
|
||||||
|
0 8px 14px rgba(0, 0, 0, 0.26);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-drag-handle .ui-icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-tooltip {
|
||||||
|
max-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-dragging {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-drop-target > .goal-tree-node-card {
|
||||||
|
border-color: rgba(246, 196, 83, 0.48);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-drop-target > .goal-tree-node-card::after {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: rgba(246, 196, 83, 0.9);
|
||||||
|
box-shadow: 0 0 12px rgba(246, 196, 83, 0.24);
|
||||||
|
content: "";
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node[data-reorder-orientation="horizontal"].is-drop-target > .goal-tree-node-card::after {
|
||||||
|
top: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
left: -8px;
|
||||||
|
width: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node[data-reorder-orientation="horizontal"].is-drop-target.drop-after > .goal-tree-node-card::after {
|
||||||
|
right: -8px;
|
||||||
|
left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node[data-reorder-orientation="vertical"].is-drop-target > .goal-tree-node-card::after {
|
||||||
|
top: -8px;
|
||||||
|
right: 8px;
|
||||||
|
left: 8px;
|
||||||
|
height: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node[data-reorder-orientation="vertical"].is-drop-target.drop-after > .goal-tree-node-card::after {
|
||||||
|
top: auto;
|
||||||
|
bottom: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-empty {
|
||||||
|
padding: var(--space-4);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-module .text-import-actions {
|
||||||
|
padding-top: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 860px) {
|
||||||
|
.goal-tree-module {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-view.tool-split-results {
|
||||||
|
min-height: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-list {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node {
|
||||||
|
min-width: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node-card {
|
||||||
|
width: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-text,
|
||||||
|
.goal-tree-node.is-checklist {
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-node.is-text > .goal-tree-node-card,
|
||||||
|
.goal-tree-node.is-checklist > .goal-tree-node-card {
|
||||||
|
width: 160px;
|
||||||
|
}
|
||||||
|
}
|
||||||
270
website/src/styles/toolboxes/goal-tree/_view.scss
Normal file
270
website/src/styles/toolboxes/goal-tree/_view.scss
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
// Rôle : stylise la zone de rendu, le canevas et les connecteurs de l'arbre d'objectifs.
|
||||||
|
.goal-tree-view.tool-split-results {
|
||||||
|
position: relative;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 260px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-scroll-area {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, max-content);
|
||||||
|
grid-template-rows: auto minmax(0, max-content);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: inherit;
|
||||||
|
align-content: start;
|
||||||
|
justify-content: start;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior-x: contain;
|
||||||
|
overscroll-behavior-y: auto;
|
||||||
|
padding: var(--space-4);
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(150, 165, 205, 0.035) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(150, 165, 205, 0.035) 1px, transparent 1px),
|
||||||
|
radial-gradient(circle at 50% 0%, rgba(139, 92, 246, 0.08), transparent 34%),
|
||||||
|
rgba(7, 10, 24, 0.2);
|
||||||
|
background-size:
|
||||||
|
28px 28px,
|
||||||
|
28px 28px,
|
||||||
|
auto,
|
||||||
|
auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-view.is-horizontal.is-editing .goal-tree-scroll-area {
|
||||||
|
grid-template-columns: 44px minmax(0, max-content);
|
||||||
|
grid-template-rows: minmax(260px, max-content);
|
||||||
|
column-gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-view.is-horizontal.is-editing .goal-tree-view-controls {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-view.is-horizontal.is-editing .goal-tree-canvas-frame,
|
||||||
|
.goal-tree-view.is-horizontal.is-editing .goal-tree-empty {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-view.is-vertical.is-editing .goal-tree-scroll-area {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-view.is-vertical.is-editing .goal-tree-view-controls {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding-right: 84px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-view.is-vertical.is-editing .goal-tree-canvas-frame,
|
||||||
|
.goal-tree-view.is-vertical.is-editing .goal-tree-empty {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-view:not(.is-editing) .goal-tree-canvas-frame {
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-view-controls {
|
||||||
|
position: relative;
|
||||||
|
z-index: 5;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--space-2);
|
||||||
|
min-width: 0;
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-view:not(.is-editing) .goal-tree-view-controls {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-view.is-horizontal .goal-tree-view-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
align-self: stretch;
|
||||||
|
min-height: 100%;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-view.is-vertical .goal-tree-view-controls {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-orientation-switch {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 7;
|
||||||
|
top: var(--space-4);
|
||||||
|
right: var(--space-4);
|
||||||
|
display: inline-grid;
|
||||||
|
grid-template-columns: 16px 30px;
|
||||||
|
width: fit-content;
|
||||||
|
min-width: 70px;
|
||||||
|
min-height: 40px;
|
||||||
|
align-items: center;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
gap: 7px;
|
||||||
|
padding: 0 7px 0 10px;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
box-shadow: 0 10px 18px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-orientation-switch .ui-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-module.is-view-only {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-canvas-frame {
|
||||||
|
position: relative;
|
||||||
|
width: var(--goal-tree-canvas-width, 100%);
|
||||||
|
min-width: min(100%, var(--goal-tree-canvas-width, 100%));
|
||||||
|
height: var(--goal-tree-canvas-height, 1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-canvas {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
width: max-content;
|
||||||
|
min-width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
align-items: start;
|
||||||
|
justify-items: center;
|
||||||
|
isolation: isolate;
|
||||||
|
transform: scale(var(--goal-tree-view-scale, 1));
|
||||||
|
transform-origin: top left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-canvas-frame > .goal-tree-canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-canvas.is-horizontal {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagram-connectors {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-connectors {
|
||||||
|
z-index: 1;
|
||||||
|
color: rgba(246, 196, 83, 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-connectors path {
|
||||||
|
fill: none;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
stroke-width: 1.35;
|
||||||
|
vector-effect: non-scaling-stroke;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-list {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: max-content;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
gap: 24px;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-list.is-root-list {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-list.is-child-list {
|
||||||
|
margin-top: 28px;
|
||||||
|
padding-top: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-canvas.is-horizontal .goal-tree-list {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-canvas.is-horizontal .goal-tree-list.is-root-list {
|
||||||
|
padding: 8px 48px 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-add-root-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 7px;
|
||||||
|
padding: 0;
|
||||||
|
border-style: dashed;
|
||||||
|
background: rgba(7, 10, 24, 0.72);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 0.84rem;
|
||||||
|
font-weight: 900;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: 0 10px 18px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-view.is-horizontal .goal-tree-add-root-button {
|
||||||
|
width: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 360px;
|
||||||
|
flex: 1 1 360px;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
padding: 16px 0;
|
||||||
|
text-align: center;
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-view.is-horizontal .goal-tree-add-root-button span {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-view.is-horizontal .goal-tree-add-root-button .ui-icon {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-view.is-vertical .goal-tree-add-root-button {
|
||||||
|
width: min(520px, 100%);
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: min(390px, 100%);
|
||||||
|
min-height: 40px;
|
||||||
|
flex: 0 1 520px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-add-root-button .ui-icon {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goal-tree-canvas.is-horizontal .goal-tree-list.is-child-list {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-left: 34px;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-left: 34px;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue