add task planner & alert improvments
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
This commit is contained in:
parent
2e6d8a2c0c
commit
e306bfc5d6
15 changed files with 1547 additions and 4 deletions
|
|
@ -322,6 +322,41 @@
|
|||
"renameTitle": "Renommer",
|
||||
"deleteTitle": "Supprimer"
|
||||
},
|
||||
"taskPlanner": {
|
||||
"settingsTitle": "Réglages du planificateur",
|
||||
"settingsButtonLabel": "Paramètres des réinitialisations",
|
||||
"sortModeLabel": "Tri automatique par type",
|
||||
"globalWeeklyResetDayLabel": "Reset hebdo",
|
||||
"resetTimeLabel": "Heure de reset",
|
||||
"titlePlaceholder": "Nouvelle tâche",
|
||||
"titleLabel": "Titre",
|
||||
"descriptionPlaceholder": "Description",
|
||||
"descriptionLabel": "Description",
|
||||
"typeLabel": "Type",
|
||||
"uniqueType": "Ponctuelle",
|
||||
"dailyType": "Quotidienne",
|
||||
"weeklyType": "Hebdomadaire",
|
||||
"addButton": "Ajouter",
|
||||
"reorderTitle": "Déplacer",
|
||||
"reorderDisabledTitle": "Tri manuel désactivé",
|
||||
"checkTitle": "Marquer effectué",
|
||||
"uncheckTitle": "Marquer non effectué",
|
||||
"deleteTitle": "Supprimer",
|
||||
"showDescriptionTitle": "Afficher la description",
|
||||
"hideDescriptionTitle": "Masquer la description",
|
||||
"taskSettingsTitle": "Réglages de la tâche",
|
||||
"missingDependencyTitle": "Pré requis non effectué",
|
||||
"missingDependencyBadge": "Pré requis",
|
||||
"missingDependencyNotice": "Pré requis parent non effectué",
|
||||
"taskWeeklyResetDayLabel": "Jour hebdo",
|
||||
"inheritWeeklyResetDay": "Global",
|
||||
"linksLabel": "Parent",
|
||||
"emptyLinks": "Aucun parent",
|
||||
"addLinkLabel": "Définir un parent",
|
||||
"dependencyCheckboxLabel": "Pré requis",
|
||||
"removeRelationTitle": "Retirer",
|
||||
"emptyTasks": "Aucune tâche planifiée."
|
||||
},
|
||||
"imageAnnotation": {
|
||||
"addImage": "Ajouter une image",
|
||||
"replaceImage": "Remplacer l'image",
|
||||
|
|
|
|||
5
website/public/static/icons/settings.svg
Normal file
5
website/public/static/icons/settings.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 9.50006C11.5147 9.50006 9.5 11.5148 9.5 14.0001C9.5 16.4853 11.5147 18.5001 14 18.5001C15.3488 18.5001 16.559 17.9066 17.3838 16.9666C18.0787 16.1746 18.5 15.1365 18.5 14.0001C18.5 13.5401 18.431 13.0963 18.3028 12.6784C17.7382 10.8381 16.0253 9.50006 14 9.50006ZM11 14.0001C11 12.3432 12.3431 11.0001 14 11.0001C15.6569 11.0001 17 12.3432 17 14.0001C17 15.6569 15.6569 17.0001 14 17.0001C12.3431 17.0001 11 15.6569 11 14.0001Z" fill="#212121"/>
|
||||
<path d="M21.7093 22.3948L19.9818 21.6364C19.4876 21.4197 18.9071 21.4515 18.44 21.7219C17.9729 21.9924 17.675 22.4693 17.6157 23.0066L17.408 24.8855C17.3651 25.273 17.084 25.5917 16.7055 25.682C14.9263 26.1061 13.0725 26.1061 11.2933 25.682C10.9148 25.5917 10.6336 25.273 10.5908 24.8855L10.3834 23.0093C10.3225 22.4731 10.0112 21.9976 9.54452 21.7281C9.07783 21.4586 8.51117 21.4269 8.01859 21.6424L6.29071 22.4009C5.93281 22.558 5.51493 22.4718 5.24806 22.1859C4.00474 20.8536 3.07924 19.2561 2.54122 17.5137C2.42533 17.1384 2.55922 16.7307 2.8749 16.4977L4.40219 15.3703C4.83721 15.0501 5.09414 14.5415 5.09414 14.0007C5.09414 13.4598 4.83721 12.9512 4.40162 12.6306L2.87529 11.5051C2.55914 11.272 2.42513 10.8638 2.54142 10.4882C3.08038 8.74734 4.00637 7.15163 5.24971 5.82114C5.51684 5.53528 5.93492 5.44941 6.29276 5.60691L8.01296 6.36404C8.50793 6.58168 9.07696 6.54881 9.54617 6.27415C10.0133 6.00264 10.3244 5.52527 10.3844 4.98794L10.5933 3.11017C10.637 2.71803 10.9245 2.39704 11.3089 2.31138C12.19 2.11504 13.0891 2.01071 14.0131 2.00006C14.9147 2.01047 15.8128 2.11485 16.6928 2.31149C17.077 2.39734 17.3643 2.71823 17.4079 3.11017L17.617 4.98937C17.7116 5.85221 18.4387 6.50572 19.3055 6.50663C19.5385 6.507 19.769 6.45838 19.9843 6.36294L21.7048 5.60568C22.0626 5.44818 22.4807 5.53405 22.7478 5.81991C23.9912 7.1504 24.9172 8.74611 25.4561 10.487C25.5723 10.8623 25.4386 11.2703 25.1228 11.5035L23.5978 12.6297C23.1628 12.95 22.9 13.4586 22.9 13.9994C22.9 14.5403 23.1628 15.0489 23.5988 15.3698L25.1251 16.4965C25.441 16.7296 25.5748 17.1376 25.4586 17.5131C24.9198 19.2536 23.9944 20.8492 22.7517 22.1799C22.4849 22.4657 22.0671 22.5518 21.7093 22.3948ZM16.263 22.1966C16.4982 21.4685 16.9889 20.8288 17.6884 20.4238C18.5702 19.9132 19.6536 19.8547 20.5841 20.2627L21.9281 20.8526C22.791 19.8538 23.4593 18.7013 23.8981 17.4552L22.7095 16.5778L22.7086 16.5771C21.898 15.98 21.4 15.0277 21.4 13.9994C21.4 12.9719 21.8974 12.0195 22.7073 11.4227L22.7085 11.4218L23.8957 10.545C23.4567 9.2988 22.7881 8.14636 21.9248 7.1477L20.5922 7.73425L20.5899 7.73527C20.1844 7.91463 19.7472 8.00722 19.3039 8.00663C17.6715 8.00453 16.3046 6.77431 16.1261 5.15465L16.1259 5.15291L15.9635 3.69304C15.3202 3.57328 14.6677 3.50872 14.013 3.50017C13.3389 3.50891 12.6821 3.57367 12.0377 3.69328L11.8751 5.15452C11.7625 6.16272 11.1793 7.05909 10.3019 7.56986C9.41937 8.0856 8.34453 8.14844 7.40869 7.73694L6.07273 7.14893C5.20949 8.14751 4.54092 9.29983 4.10196 10.5459L5.29181 11.4233C6.11115 12.0269 6.59414 12.9837 6.59414 14.0007C6.59414 15.0173 6.11142 15.9742 5.29237 16.5776L4.10161 17.4566C4.54002 18.7044 5.2085 19.8585 6.07205 20.8587L7.41742 20.2682C8.34745 19.8613 9.41573 19.9215 10.2947 20.4292C11.174 20.937 11.7593 21.832 11.8738 22.84L11.8744 22.8445L12.0362 24.3088C13.3326 24.5638 14.6662 24.5638 15.9626 24.3088L16.1247 22.8418C16.1491 22.6217 16.1955 22.4055 16.263 22.1966Z" fill="#212121"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
2
website/public/static/icons/tasklist.svg
Normal file
2
website/public/static/icons/tasklist.svg
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" ?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" id="Layer_1" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M16.71,9.29c-0.39-0.39-1.02-0.39-1.41,0L14,10.59l-0.29-0.29c-0.39-0.39-1.02-0.39-1.41,0s-0.39,1.02,0,1.41l1,1 C13.49,12.9,13.74,13,14,13s0.51-0.1,0.71-0.29l2-2C17.1,10.32,17.1,9.68,16.71,9.29z"/><path d="M7,5c0-0.55-0.45-1-1-1H5C4.45,4,4,4.45,4,5v16c0,0.55,0.45,1,1,1h14c0.55,0,1-0.45,1-1V5c0-0.55-0.45-1-1-1h-1 c-0.55,0-1,0.45-1,1s0.45,1,1,1v14H6V6C6.55,6,7,5.55,7,5z"/><path d="M15,2H9C8.45,2,8,2.45,8,3v4c0,0.55,0.45,1,1,1h6c0.55,0,1-0.45,1-1V3C16,2.45,15.55,2,15,2z M14,6h-4V4h4V6z"/><path d="M8,12c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1s-1,0.45-1,1V12z"/><path d="M8,17c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1s-1,0.45-1,1V17z"/><path d="M15.71,14.29c-0.39-0.39-1.02-0.39-1.41,0L14,14.59l-0.29-0.29c-0.39-0.39-1.02-0.39-1.41,0s-0.39,1.02,0,1.41L12.59,16 l-0.29,0.29c-0.39,0.39-0.39,1.02,0,1.41C12.49,17.9,12.74,18,13,18s0.51-0.1,0.71-0.29L14,17.41l0.29,0.29 C14.49,17.9,14.74,18,15,18s0.51-0.1,0.71-0.29c0.39-0.39,0.39-1.02,0-1.41L15.41,16l0.29-0.29C16.1,15.32,16.1,14.68,15.71,14.29z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -1,10 +1,22 @@
|
|||
// Rôle : regroupe les modales, notifications et viewers rendus au-dessus de l'application.
|
||||
import React from "react";
|
||||
import { Icon } from "./Icon.jsx";
|
||||
import { ImageViewer } from "./ImageViewer.jsx";
|
||||
import { ConfirmModal, CreateToolboxModal, LinkToolboxModal, NotificationToast } from "./ToolboxModals.jsx";
|
||||
import { navigate } from "../router/hashRouter.js";
|
||||
import { uid } from "../features/toolboxes/storage/toolboxStorage.js";
|
||||
|
||||
export function InlineNotice({ className = "", iconName = "link", message, tone = "warning" }) {
|
||||
if (!message) return null;
|
||||
const classes = ["inline-notice", `is-${tone}`, className].filter(Boolean).join(" ");
|
||||
return (
|
||||
<span className={classes} role="status">
|
||||
<Icon name={iconName} />
|
||||
<span>{message}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppOverlays({
|
||||
actions,
|
||||
confirmModal,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
normalizeCountersData,
|
||||
normalizeImageAnnotationData,
|
||||
normalizeLinksData,
|
||||
normalizeTaskPlannerData,
|
||||
normalizeTimerData,
|
||||
normalizeUrl,
|
||||
uid
|
||||
|
|
@ -146,6 +147,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
|||
normalizeCalculatorData,
|
||||
normalizeImageAnnotationData,
|
||||
normalizeTimerData,
|
||||
normalizeTaskPlannerData,
|
||||
normalizeUrl,
|
||||
hostnameFromUrl,
|
||||
copyText,
|
||||
|
|
|
|||
623
website/src/features/toolboxes/modules/TaskPlannerModule.jsx
Normal file
623
website/src/features/toolboxes/modules/TaskPlannerModule.jsx
Normal file
|
|
@ -0,0 +1,623 @@
|
|||
// Rôle : fournit l'outil task planner avec tâches récurrentes, parents et pré requis.
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { InlineNotice } from "../../../components/AppOverlays.jsx";
|
||||
import { usePointerReorder } from "../../../hooks/usePointerReorder.js";
|
||||
|
||||
const TASK_TYPES = ["daily", "weekly", "unique"];
|
||||
const WEEK_DAYS = [
|
||||
{ value: 1, label: "Lundi" },
|
||||
{ value: 2, label: "Mardi" },
|
||||
{ value: 3, label: "Mercredi" },
|
||||
{ value: 4, label: "Jeudi" },
|
||||
{ value: 5, label: "Vendredi" },
|
||||
{ value: 6, label: "Samedi" },
|
||||
{ value: 0, label: "Dimanche" }
|
||||
];
|
||||
|
||||
function getTaskTypeLabel(type, textContent) {
|
||||
if (type === "daily") return textContent.dailyType || "Quotidienne";
|
||||
if (type === "weekly") return textContent.weeklyType || "Hebdomadaire";
|
||||
return textContent.uniqueType || "Ponctuelle";
|
||||
}
|
||||
|
||||
function parseResetTime(value) {
|
||||
const [hours, minutes] = String(value || "00:00").split(":").map((part) => Number.parseInt(part, 10));
|
||||
return {
|
||||
hours: Number.isInteger(hours) ? Math.min(23, Math.max(0, hours)) : 0,
|
||||
minutes: Number.isInteger(minutes) ? Math.min(59, Math.max(0, minutes)) : 0
|
||||
};
|
||||
}
|
||||
|
||||
function getLatestDailyReset(now, resetTime) {
|
||||
const { hours, minutes } = parseResetTime(resetTime);
|
||||
const reset = new Date(now);
|
||||
reset.setHours(hours, minutes, 0, 0);
|
||||
if (reset.getTime() > now) reset.setDate(reset.getDate() - 1);
|
||||
return reset.getTime();
|
||||
}
|
||||
|
||||
function getLatestWeeklyReset(now, resetTime, weekDay) {
|
||||
const { hours, minutes } = parseResetTime(resetTime);
|
||||
const reset = new Date(now);
|
||||
reset.setHours(hours, minutes, 0, 0);
|
||||
const dayDiff = (reset.getDay() - weekDay + 7) % 7;
|
||||
reset.setDate(reset.getDate() - dayDiff);
|
||||
if (reset.getTime() > now) reset.setDate(reset.getDate() - 7);
|
||||
return reset.getTime();
|
||||
}
|
||||
|
||||
function applyDueResets(data, now = Date.now()) {
|
||||
let latestResetAt = data.lastResetAt;
|
||||
let changed = false;
|
||||
const tasks = data.tasks.map((task) => {
|
||||
if (task.type === "unique") return task;
|
||||
const resetAt = task.type === "daily"
|
||||
? getLatestDailyReset(now, data.resetTime)
|
||||
: getLatestWeeklyReset(now, data.resetTime, task.weeklyResetDay ?? data.weeklyResetDay);
|
||||
latestResetAt = Math.max(latestResetAt, resetAt);
|
||||
if (!task.checked || resetAt <= (task.checkedAt || data.lastResetAt)) return task;
|
||||
changed = true;
|
||||
return { ...task, checked: false, checkedAt: 0 };
|
||||
});
|
||||
|
||||
if (latestResetAt > data.lastResetAt) changed = true;
|
||||
return changed ? { ...data, lastResetAt: latestResetAt, tasks } : data;
|
||||
}
|
||||
|
||||
function moveTask(tasks, fromTaskId, toTaskId, placement = "before") {
|
||||
const nextTasks = [...tasks];
|
||||
const fromIndex = nextTasks.findIndex((task) => task.id === fromTaskId);
|
||||
const toIndex = nextTasks.findIndex((task) => task.id === toTaskId);
|
||||
if (fromIndex < 0 || toIndex < 0) return tasks;
|
||||
const [moved] = nextTasks.splice(fromIndex, 1);
|
||||
const targetIndex = nextTasks.findIndex((task) => task.id === toTaskId);
|
||||
nextTasks.splice(placement === "after" ? targetIndex + 1 : targetIndex, 0, moved);
|
||||
return nextTasks;
|
||||
}
|
||||
|
||||
function sortTasks(tasks, sortMode) {
|
||||
if (sortMode !== "type") return tasks;
|
||||
const order = new Map(TASK_TYPES.map((type, index) => [type, index]));
|
||||
return [...tasks].sort((a, b) => (order.get(a.type) ?? 99) - (order.get(b.type) ?? 99));
|
||||
}
|
||||
|
||||
function getTaskTitle(tasks, taskId) {
|
||||
return tasks.find((task) => task.id === taskId)?.title || "Tâche supprimée";
|
||||
}
|
||||
|
||||
function getTaskParentId(taskId, relations, taskIds) {
|
||||
return relations.find((relation) => relation.fromTaskId === taskId && taskIds.has(relation.toTaskId))?.toTaskId || "";
|
||||
}
|
||||
|
||||
function getTaskParentMap(tasks, relations) {
|
||||
const taskIds = new Set(tasks.map((task) => task.id));
|
||||
const parentMap = new Map(tasks.map((task) => [task.id, getTaskParentId(task.id, relations, taskIds)]));
|
||||
tasks.forEach((task) => {
|
||||
const visited = new Set([task.id]);
|
||||
let parentId = parentMap.get(task.id);
|
||||
while (parentId) {
|
||||
if (visited.has(parentId)) {
|
||||
parentMap.set(task.id, "");
|
||||
return;
|
||||
}
|
||||
visited.add(parentId);
|
||||
parentId = parentMap.get(parentId);
|
||||
}
|
||||
});
|
||||
return parentMap;
|
||||
}
|
||||
|
||||
function getTasksForParent(tasks, parentMap, parentId, sortMode) {
|
||||
return sortTasks(tasks.filter((task) => (parentMap.get(task.id) || "") === parentId), sortMode);
|
||||
}
|
||||
|
||||
export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
||||
const data = context.normalizeTaskPlannerData(context.getModuleData(toolboxId, moduleId, { tasks: [] }));
|
||||
const textContent = context.moduleText?.taskPlanner || {};
|
||||
const [title, setTitle] = useState("");
|
||||
const [type, setType] = useState("daily");
|
||||
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [openDescriptions, setOpenDescriptions] = useState(() => new Set());
|
||||
const [openTaskSettings, setOpenTaskSettings] = useState(() => new Set());
|
||||
const [dependencyNoticeTaskIds, setDependencyNoticeTaskIds] = useState(() => new Set());
|
||||
const dependencyNoticeTimeouts = useRef(new Map());
|
||||
const parentMap = useMemo(() => getTaskParentMap(data.tasks, data.relations), [data.tasks, data.relations]);
|
||||
const {
|
||||
draggingId,
|
||||
dropTarget,
|
||||
startDrag
|
||||
} = usePointerReorder({
|
||||
targetSelector: ".task-planner-item",
|
||||
getTargetId: (target) => target.dataset.taskId,
|
||||
canDropOn: (target, draggingTaskId) => data.sortMode === "manual" && target.dataset.parentId === (parentMap.get(draggingTaskId) || ""),
|
||||
onMove: (fromTaskId, toTaskId, placement) => save({ ...data, tasks: moveTask(data.tasks, fromTaskId, toTaskId, placement) })
|
||||
});
|
||||
|
||||
function save(nextData) {
|
||||
context.setModuleData(toolboxId, moduleId, nextData, "taskPlanner");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = window.setInterval(() => setNowMs(Date.now()), 60000);
|
||||
return () => window.clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => {
|
||||
dependencyNoticeTimeouts.current.forEach((timeoutId) => window.clearTimeout(timeoutId));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const resetData = applyDueResets(data, nowMs);
|
||||
if (resetData !== data) save(resetData);
|
||||
});
|
||||
|
||||
function addTask(event) {
|
||||
event.preventDefault();
|
||||
const cleanTitle = title.trim();
|
||||
if (!cleanTitle) return;
|
||||
save({
|
||||
...data,
|
||||
tasks: [
|
||||
...data.tasks,
|
||||
{
|
||||
id: context.uid("task"),
|
||||
title: cleanTitle,
|
||||
description: "",
|
||||
type,
|
||||
checked: false,
|
||||
checkedAt: 0
|
||||
}
|
||||
]
|
||||
});
|
||||
setTitle("");
|
||||
setType("daily");
|
||||
}
|
||||
|
||||
function updateTask(taskId, updater) {
|
||||
save({ ...data, tasks: data.tasks.map((task) => task.id === taskId ? updater(task) : task) });
|
||||
}
|
||||
|
||||
function deleteTask(taskId) {
|
||||
save({
|
||||
...data,
|
||||
tasks: data.tasks.filter((task) => task.id !== taskId),
|
||||
relations: data.relations.filter((relation) => relation.fromTaskId !== taskId && relation.toTaskId !== taskId)
|
||||
});
|
||||
}
|
||||
|
||||
function addLinkTarget(fromTaskId, toTaskId) {
|
||||
if (!toTaskId || fromTaskId === toTaskId || data.relations.some((relation) => relation.fromTaskId === fromTaskId && relation.toTaskId === toTaskId)) return;
|
||||
save({
|
||||
...data,
|
||||
relations: [
|
||||
...data.relations.filter((relation) => relation.fromTaskId !== fromTaskId),
|
||||
{ id: context.uid("relation"), fromTaskId, toTaskId, dependency: false }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
function removeLinkTarget(fromTaskId, toTaskId) {
|
||||
save({
|
||||
...data,
|
||||
relations: data.relations.filter((relation) => relation.fromTaskId !== fromTaskId || relation.toTaskId !== toTaskId)
|
||||
});
|
||||
}
|
||||
|
||||
function setDependency(fromTaskId, toTaskId, dependency) {
|
||||
const hasRelation = data.relations.some((relation) => relation.fromTaskId === fromTaskId && relation.toTaskId === toTaskId);
|
||||
save({
|
||||
...data,
|
||||
relations: hasRelation
|
||||
? data.relations.map((relation) => relation.fromTaskId === fromTaskId && relation.toTaskId === toTaskId ? { ...relation, dependency } : relation)
|
||||
: [...data.relations, { id: context.uid("relation"), fromTaskId, toTaskId, dependency }]
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSet(setter, taskId) {
|
||||
setter((current) => {
|
||||
const next = new Set(current);
|
||||
if (next.has(taskId)) next.delete(taskId);
|
||||
else next.add(taskId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
function showDependencyNotice(taskId) {
|
||||
const currentTimeoutId = dependencyNoticeTimeouts.current.get(taskId);
|
||||
if (currentTimeoutId) window.clearTimeout(currentTimeoutId);
|
||||
setDependencyNoticeTaskIds((current) => new Set(current).add(taskId));
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
dependencyNoticeTimeouts.current.delete(taskId);
|
||||
setDependencyNoticeTaskIds((current) => {
|
||||
const next = new Set(current);
|
||||
next.delete(taskId);
|
||||
return next;
|
||||
});
|
||||
}, 2600);
|
||||
dependencyNoticeTimeouts.current.set(taskId, timeoutId);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="task-planner-module">
|
||||
<div className="task-planner-topbar">
|
||||
<div className="task-planner-global-controls">
|
||||
<button
|
||||
className={`tool-split-scroll-toggle task-planner-sort-toggle ${data.sortMode === "type" ? "active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => save({ ...data, sortMode: data.sortMode === "type" ? "manual" : "type" })}
|
||||
aria-pressed={data.sortMode === "type"}
|
||||
aria-label={textContent.sortModeLabel || "Tri automatique"}
|
||||
title={textContent.sortModeLabel || "Tri automatique"}
|
||||
>
|
||||
<Icon name="sort-time" />
|
||||
<i aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
className={`task-planner-settings-toggle ${settingsOpen ? "active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => setSettingsOpen((value) => !value)}
|
||||
aria-expanded={settingsOpen}
|
||||
aria-label={textContent.settingsTitle || "Réglages du planificateur"}
|
||||
title={textContent.settingsTitle || "Réglages du planificateur"}
|
||||
>
|
||||
<Icon name="settings" />
|
||||
<span>{textContent.settingsButtonLabel || "Paramètres des réinitialisations"}</span>
|
||||
</button>
|
||||
</div>
|
||||
{editing && (
|
||||
<form className="inline-form task-planner-add-form" onSubmit={addTask}>
|
||||
<input name="title" placeholder={textContent.titlePlaceholder || "Nouvelle tâche"} value={title} onChange={(event) => setTitle(event.target.value)} />
|
||||
<select value={type} onChange={(event) => setType(event.target.value)} aria-label={textContent.typeLabel || "Type"}>
|
||||
{TASK_TYPES.map((taskType) => <option key={taskType} value={taskType}>{getTaskTypeLabel(taskType, textContent)}</option>)}
|
||||
</select>
|
||||
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{settingsOpen && (
|
||||
<section className="task-planner-settings" aria-label={textContent.settingsTitle || "Réglages du planificateur"}>
|
||||
<label>
|
||||
<span>{textContent.globalWeeklyResetDayLabel || "Reset hebdo"}</span>
|
||||
<select value={data.weeklyResetDay} onChange={(event) => save({ ...data, weeklyResetDay: Number(event.target.value) })}>
|
||||
{WEEK_DAYS.map((day) => <option key={day.value} value={day.value}>{day.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>{textContent.resetTimeLabel || "Heure de reset"}</span>
|
||||
<input type="time" value={data.resetTime} onChange={(event) => save({ ...data, resetTime: event.target.value || "00:00" })} />
|
||||
</label>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<ul className="task-planner-list">
|
||||
{data.tasks.length ? (
|
||||
<TaskPlannerBranch
|
||||
parentId=""
|
||||
parentMap={parentMap}
|
||||
tasks={data.tasks}
|
||||
data={data}
|
||||
draggingId={draggingId}
|
||||
dropTarget={dropTarget}
|
||||
textContent={textContent}
|
||||
openDescriptions={openDescriptions}
|
||||
openTaskSettings={openTaskSettings}
|
||||
dependencyNoticeTaskIds={dependencyNoticeTaskIds}
|
||||
onDragStart={startDrag}
|
||||
onToggleDescription={(taskId) => toggleSet(setOpenDescriptions, taskId)}
|
||||
onToggleSettings={(taskId) => toggleSet(setOpenTaskSettings, taskId)}
|
||||
onDependencyNotice={showDependencyNotice}
|
||||
onUpdateTask={updateTask}
|
||||
onDeleteTask={deleteTask}
|
||||
onAddLinkTarget={addLinkTarget}
|
||||
onRemoveLinkTarget={removeLinkTarget}
|
||||
onSetDependency={setDependency}
|
||||
/>
|
||||
) : (
|
||||
<li className="task-planner-empty">{textContent.emptyTasks || "Aucune tâche planifiée."}</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskPlannerBranch({
|
||||
parentId,
|
||||
parentMap,
|
||||
tasks,
|
||||
data,
|
||||
draggingId,
|
||||
dropTarget,
|
||||
textContent,
|
||||
openDescriptions,
|
||||
openTaskSettings,
|
||||
dependencyNoticeTaskIds,
|
||||
onDragStart,
|
||||
onToggleDescription,
|
||||
onToggleSettings,
|
||||
onDependencyNotice,
|
||||
onUpdateTask,
|
||||
onDeleteTask,
|
||||
onAddLinkTarget,
|
||||
onRemoveLinkTarget,
|
||||
onSetDependency,
|
||||
visited = new Set()
|
||||
}) {
|
||||
const children = getTasksForParent(tasks, parentMap, parentId, data.sortMode).filter((task) => !visited.has(task.id));
|
||||
if (!children.length) return null;
|
||||
const nextVisited = new Set([...visited, ...children.map((task) => task.id)]);
|
||||
|
||||
return children.map((task) => {
|
||||
const nestedChildren = getTasksForParent(tasks, parentMap, task.id, data.sortMode).filter((child) => !nextVisited.has(child.id));
|
||||
return (
|
||||
<TaskPlannerItem
|
||||
key={task.id}
|
||||
task={task}
|
||||
tasks={tasks}
|
||||
data={data}
|
||||
parentMap={parentMap}
|
||||
parentId={parentId}
|
||||
draggingId={draggingId}
|
||||
dropTarget={dropTarget}
|
||||
textContent={textContent}
|
||||
descriptionOpen={openDescriptions.has(task.id)}
|
||||
settingsOpen={openTaskSettings.has(task.id)}
|
||||
dependencyNoticeVisible={dependencyNoticeTaskIds.has(task.id)}
|
||||
onDragStart={onDragStart}
|
||||
onToggleDescription={() => onToggleDescription(task.id)}
|
||||
onToggleSettings={() => onToggleSettings(task.id)}
|
||||
onDependencyNotice={onDependencyNotice}
|
||||
onUpdateTask={onUpdateTask}
|
||||
onDeleteTask={onDeleteTask}
|
||||
onAddLinkTarget={onAddLinkTarget}
|
||||
onRemoveLinkTarget={onRemoveLinkTarget}
|
||||
onSetDependency={onSetDependency}
|
||||
>
|
||||
{nestedChildren.length ? (
|
||||
<TaskPlannerBranch
|
||||
parentId={task.id}
|
||||
parentMap={parentMap}
|
||||
tasks={tasks}
|
||||
data={data}
|
||||
draggingId={draggingId}
|
||||
dropTarget={dropTarget}
|
||||
textContent={textContent}
|
||||
openDescriptions={openDescriptions}
|
||||
openTaskSettings={openTaskSettings}
|
||||
dependencyNoticeTaskIds={dependencyNoticeTaskIds}
|
||||
onDragStart={onDragStart}
|
||||
onToggleDescription={onToggleDescription}
|
||||
onToggleSettings={onToggleSettings}
|
||||
onDependencyNotice={onDependencyNotice}
|
||||
onUpdateTask={onUpdateTask}
|
||||
onDeleteTask={onDeleteTask}
|
||||
onAddLinkTarget={onAddLinkTarget}
|
||||
onRemoveLinkTarget={onRemoveLinkTarget}
|
||||
onSetDependency={onSetDependency}
|
||||
visited={nextVisited}
|
||||
/>
|
||||
) : null}
|
||||
</TaskPlannerItem>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function TaskPlannerItem({
|
||||
task,
|
||||
tasks,
|
||||
data,
|
||||
parentMap,
|
||||
parentId,
|
||||
draggingId,
|
||||
dropTarget,
|
||||
textContent,
|
||||
descriptionOpen,
|
||||
settingsOpen,
|
||||
dependencyNoticeVisible,
|
||||
onDragStart,
|
||||
onToggleDescription,
|
||||
onToggleSettings,
|
||||
onDependencyNotice,
|
||||
onUpdateTask,
|
||||
onDeleteTask,
|
||||
onAddLinkTarget,
|
||||
onRemoveLinkTarget,
|
||||
onSetDependency,
|
||||
children
|
||||
}) {
|
||||
const relations = data.relations.filter((relation) => relation.fromTaskId === task.id);
|
||||
const missingDependencies = relations.filter((relation) => relation.dependency && !tasks.find((item) => item.id === relation.toTaskId)?.checked);
|
||||
const relationTargets = tasks.filter((item) => item.id !== task.id && !relations.some((relation) => relation.toTaskId === item.id));
|
||||
const noticeMessage = dependencyNoticeVisible && missingDependencies.length > 0 ? textContent.missingDependencyNotice || "Pré requis parent non effectué" : "";
|
||||
const className = [
|
||||
"task-planner-item",
|
||||
task.checked ? "is-complete" : "",
|
||||
missingDependencies.length ? "has-missing-dependency" : "",
|
||||
draggingId === task.id ? "is-dragging" : "",
|
||||
dropTarget.id === task.id ? "is-drop-target" : "",
|
||||
dropTarget.id === task.id && dropTarget.placement === "after" ? "drop-after" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
function updateChecked(event) {
|
||||
const checked = event.target.checked;
|
||||
if (checked && missingDependencies.length > 0) onDependencyNotice(task.id);
|
||||
onUpdateTask(task.id, (current) => ({
|
||||
...current,
|
||||
checked,
|
||||
checkedAt: checked ? Date.now() : 0
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={className} data-task-id={task.id} data-parent-id={parentId}>
|
||||
<div className="task-planner-line">
|
||||
<button
|
||||
className="task-planner-drag-handle"
|
||||
type="button"
|
||||
onPointerDown={(event) => onDragStart(event, task.id)}
|
||||
disabled={data.sortMode !== "manual"}
|
||||
aria-label={`${textContent.reorderTitle || "Déplacer"} ${task.title}`}
|
||||
title={data.sortMode === "manual" ? textContent.reorderTitle || "Déplacer" : textContent.reorderDisabledTitle || "Tri manuel désactivé"}
|
||||
>
|
||||
<Icon name="drag" />
|
||||
</button>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={task.checked}
|
||||
onChange={updateChecked}
|
||||
aria-label={`${task.checked ? textContent.uncheckTitle || "Marquer non effectué" : textContent.checkTitle || "Marquer effectué"} ${task.title}`}
|
||||
/>
|
||||
<input
|
||||
className="tool-split-entry-label task-planner-title-input"
|
||||
value={task.title}
|
||||
onChange={(event) => onUpdateTask(task.id, (current) => ({ ...current, title: event.target.value }))}
|
||||
onBlur={(event) => onUpdateTask(task.id, (current) => ({ ...current, title: event.target.value.trim() || current.title }))}
|
||||
aria-label={textContent.titleLabel || "Titre"}
|
||||
/>
|
||||
<select
|
||||
className="task-planner-type-select"
|
||||
value={task.type}
|
||||
onChange={(event) => onUpdateTask(task.id, (current) => {
|
||||
const nextType = event.target.value;
|
||||
const nextTask = { ...current, type: nextType };
|
||||
if (nextType !== "weekly") delete nextTask.weeklyResetDay;
|
||||
return nextTask;
|
||||
})}
|
||||
aria-label={textContent.typeLabel || "Type"}
|
||||
>
|
||||
{TASK_TYPES.map((taskType) => <option key={taskType} value={taskType}>{getTaskTypeLabel(taskType, textContent)}</option>)}
|
||||
</select>
|
||||
{missingDependencies.length > 0 && (
|
||||
<span className="task-planner-warning" title={textContent.missingDependencyTitle || "Pré requis non effectué"}>
|
||||
{textContent.missingDependencyBadge || "Pré requis"}
|
||||
</span>
|
||||
)}
|
||||
<div className="task-planner-actions">
|
||||
<button
|
||||
className={`task-planner-dropdown-button ${descriptionOpen ? "active" : ""}`}
|
||||
type="button"
|
||||
onClick={onToggleDescription}
|
||||
aria-expanded={descriptionOpen}
|
||||
aria-label={`${descriptionOpen ? textContent.hideDescriptionTitle || "Masquer la description" : textContent.showDescriptionTitle || "Afficher la description"} ${task.title}`}
|
||||
title={descriptionOpen ? textContent.hideDescriptionTitle || "Masquer la description" : textContent.showDescriptionTitle || "Afficher la description"}
|
||||
>
|
||||
<Icon name={descriptionOpen ? "chevron-up" : "chevron-down"} />
|
||||
</button>
|
||||
<button
|
||||
className={`task-planner-settings-button ${settingsOpen ? "active" : ""}`}
|
||||
type="button"
|
||||
onClick={onToggleSettings}
|
||||
aria-expanded={settingsOpen}
|
||||
aria-label={`${textContent.taskSettingsTitle || "Réglages de la tâche"} ${task.title}`}
|
||||
title={textContent.taskSettingsTitle || "Réglages de la tâche"}
|
||||
>
|
||||
<Icon name="settings" />
|
||||
</button>
|
||||
<button className="task-planner-delete-button danger" type="button" onClick={() => onDeleteTask(task.id)} aria-label={`${textContent.deleteTitle || "Supprimer"} ${task.title}`} title={textContent.deleteTitle || "Supprimer"}>
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<InlineNotice className="task-planner-notice" message={noticeMessage} />
|
||||
{descriptionOpen && (
|
||||
<textarea
|
||||
className="task-planner-description"
|
||||
value={task.description}
|
||||
onChange={(event) => onUpdateTask(task.id, (current) => ({ ...current, description: event.target.value }))}
|
||||
placeholder={textContent.descriptionPlaceholder || "Description"}
|
||||
aria-label={`${textContent.descriptionLabel || "Description"} ${task.title}`}
|
||||
/>
|
||||
)}
|
||||
{settingsOpen && (
|
||||
<div className="task-planner-task-settings">
|
||||
{task.type === "weekly" && (
|
||||
<label className="task-planner-weekly-override">
|
||||
<span>{textContent.taskWeeklyResetDayLabel || "Jour hebdo"}</span>
|
||||
<select
|
||||
value={task.weeklyResetDay ?? ""}
|
||||
onChange={(event) => onUpdateTask(task.id, (current) => {
|
||||
const nextTask = { ...current };
|
||||
if (event.target.value === "") {
|
||||
delete nextTask.weeklyResetDay;
|
||||
} else {
|
||||
nextTask.weeklyResetDay = Number(event.target.value);
|
||||
}
|
||||
return nextTask;
|
||||
})}
|
||||
>
|
||||
<option value="">{textContent.inheritWeeklyResetDay || "Global"}</option>
|
||||
{WEEK_DAYS.map((day) => <option key={day.value} value={day.value}>{day.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
<LinksEditor
|
||||
label={textContent.linksLabel || "Parent"}
|
||||
relations={relations}
|
||||
task={task}
|
||||
tasks={tasks}
|
||||
parentMap={parentMap}
|
||||
relationTargets={relationTargets}
|
||||
emptyLabel={textContent.emptyLinks || "Aucun parent"}
|
||||
addLabel={textContent.addLinkLabel || "Définir un parent"}
|
||||
removeLabel={textContent.removeRelationTitle || "Retirer"}
|
||||
dependencyLabel={textContent.dependencyCheckboxLabel || "Pré requis"}
|
||||
onAddLinkTarget={onAddLinkTarget}
|
||||
onRemoveLinkTarget={onRemoveLinkTarget}
|
||||
onSetDependency={onSetDependency}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{children && <ul className="task-planner-list task-planner-child-list">{children}</ul>}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function LinksEditor({ label, relations, task, tasks, parentMap, relationTargets, emptyLabel, addLabel, removeLabel, dependencyLabel, onAddLinkTarget, onRemoveLinkTarget, onSetDependency }) {
|
||||
const descendantIds = new Set();
|
||||
function collectDescendants(taskId) {
|
||||
tasks.filter((item) => parentMap.get(item.id) === taskId).forEach((child) => {
|
||||
descendantIds.add(child.id);
|
||||
collectDescendants(child.id);
|
||||
});
|
||||
}
|
||||
collectDescendants(task.id);
|
||||
const availableTargets = relationTargets.filter((target) => !descendantIds.has(target.id));
|
||||
|
||||
return (
|
||||
<div className="task-planner-relations">
|
||||
<span>{label}</span>
|
||||
<div>
|
||||
{relations.length ? relations.map((relation) => (
|
||||
<div className="task-planner-relation-chip" key={relation.id}>
|
||||
<span>{getTaskTitle(tasks, relation.toTaskId)}</span>
|
||||
<label title={dependencyLabel}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={relation.dependency}
|
||||
onChange={(event) => onSetDependency(task.id, relation.toTaskId, event.target.checked)}
|
||||
aria-label={`${dependencyLabel} ${getTaskTitle(tasks, relation.toTaskId)}`}
|
||||
/>
|
||||
<span>{dependencyLabel}</span>
|
||||
</label>
|
||||
<button type="button" onClick={() => onRemoveLinkTarget(task.id, relation.toTaskId)} title={removeLabel} aria-label={`${removeLabel} ${getTaskTitle(tasks, relation.toTaskId)}`}>
|
||||
<Icon name="close" />
|
||||
</button>
|
||||
</div>
|
||||
)) : <em>{emptyLabel}</em>}
|
||||
</div>
|
||||
<select
|
||||
value=""
|
||||
onChange={(event) => {
|
||||
onAddLinkTarget(task.id, event.target.value);
|
||||
event.target.value = "";
|
||||
}}
|
||||
aria-label={addLabel}
|
||||
>
|
||||
<option value="">{addLabel}</option>
|
||||
{availableTargets.map((target) => <option key={target.id} value={target.id}>{target.title}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import { ImageAnnotationModule } from "./ImageAnnotationModule.jsx";
|
|||
import { LinksModule } from "./LinksModule.jsx";
|
||||
import { NotepadModule } from "./NotepadModule.jsx";
|
||||
import { ImagesModule } from "./ImagesModule.jsx";
|
||||
import { TaskPlannerModule } from "./TaskPlannerModule.jsx";
|
||||
import { TimerModule } from "./TimerModule.jsx";
|
||||
|
||||
const MODULE_COMPONENTS = {
|
||||
|
|
@ -21,6 +22,7 @@ const MODULE_COMPONENTS = {
|
|||
counters: { label: "Compteurs", icon: "abacus", Component: CountersModule, editable: true, scrollable: true },
|
||||
calculator: { label: "Calculateur", icon: "calculator", Component: CalculatorModule, editable: false },
|
||||
timer: { label: "Timer", icon: "clock", Component: TimerModule, editable: false },
|
||||
taskPlanner: { label: "Planificateur de tâches", icon: "tasklist", Component: TaskPlannerModule, editable: true, scrollable: true },
|
||||
imageAnnotation: { label: "Annotation d'images", icon: "map", Component: ImageAnnotationModule, editable: true }
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
// Rôle : normalise, compacte et sérialise les données toolbox persistées.
|
||||
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", section: "g", image: "s", link: "l", counter: "c", calc: "r", marker: "k", timer: "z" };
|
||||
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", section: "g", image: "s", link: "l", counter: "c", calc: "r", marker: "k", timer: "z", task: "a", relation: "e" };
|
||||
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const TIMER_TABS = new Set(["stopwatch", "countdown"]);
|
||||
const COUNTDOWN_TYPES = new Set(["duration", "daily_time", "time_pattern", "interval"]);
|
||||
const TIMER_ALERT_MODES = new Set(["off", "visible", "site"]);
|
||||
const TASK_TYPES = new Set(["unique", "daily", "weekly"]);
|
||||
const TASK_SORT_MODES = new Set(["manual", "type"]);
|
||||
const DEFAULT_TASK_PLANNER_WEEKLY_RESET_DAY = 1;
|
||||
const DEFAULT_TASK_PLANNER_RESET_TIME = "00:00";
|
||||
const TOOLBOX_ICON_BASE = "/static/img/toolbox-icons/";
|
||||
const TOOLBOX_ICON_FILES = [
|
||||
"toolbox.png",
|
||||
|
|
@ -34,6 +38,7 @@ const DEFAULT_MODULE_TITLES = {
|
|||
counters: "Compteurs",
|
||||
calculator: "Calculateur",
|
||||
timer: "Timer",
|
||||
taskPlanner: "Planificateur de tâches",
|
||||
imageAnnotation: "Annotation d'images"
|
||||
};
|
||||
|
||||
|
|
@ -224,6 +229,22 @@ function isTimeString(value, allowWildcard = false) {
|
|||
});
|
||||
}
|
||||
|
||||
function isClockTimeString(value) {
|
||||
if (!/^\d{2}:\d{2}$/.test(String(value || ""))) return false;
|
||||
const [hours, minutes] = String(value).split(":").map(Number);
|
||||
return Number.isInteger(hours) && Number.isInteger(minutes) && hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59;
|
||||
}
|
||||
|
||||
function normalizeWeekDay(value, fallback = DEFAULT_TASK_PLANNER_WEEKLY_RESET_DAY) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 6 ? parsed : fallback;
|
||||
}
|
||||
|
||||
function normalizeTimestamp(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
||||
}
|
||||
|
||||
export function labelFromFileName(name) {
|
||||
if (!name || name === "image.png") return "";
|
||||
return name.replace(/\.[^.]+$/, "").trim();
|
||||
|
|
@ -300,6 +321,65 @@ export function normalizeTimerData(data) {
|
|||
};
|
||||
}
|
||||
|
||||
function normalizeTaskPlannerTask(task) {
|
||||
const title = String(task?.title || "").trim() || "Tâche";
|
||||
const type = TASK_TYPES.has(task?.type) ? task.type : "unique";
|
||||
const normalized = {
|
||||
id: task?.id || uid("task"),
|
||||
title,
|
||||
description: String(task?.description || "").trim(),
|
||||
type,
|
||||
checked: task?.checked === true,
|
||||
checkedAt: normalizeTimestamp(task?.checkedAt)
|
||||
};
|
||||
if (type === "weekly" && task?.weeklyResetDay !== undefined) normalized.weeklyResetDay = normalizeWeekDay(task.weeklyResetDay);
|
||||
if (!normalized.checked) normalized.checkedAt = 0;
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeTaskPlannerRelations(data, taskIds) {
|
||||
const seen = new Set();
|
||||
const seenChildren = new Set();
|
||||
const legacyDependencies = new Set((Array.isArray(data?.dependencies) ? data.dependencies : [])
|
||||
.map((relation) => `${relation?.fromTaskId || ""}:${relation?.toTaskId || ""}`));
|
||||
const sourceRelations = Array.isArray(data?.relations)
|
||||
? data.relations
|
||||
: [
|
||||
...(Array.isArray(data?.links) ? data.links : []),
|
||||
...(Array.isArray(data?.dependencies) ? data.dependencies : []).map((relation) => ({ ...relation, dependency: true }))
|
||||
];
|
||||
return sourceRelations
|
||||
.map((relation) => {
|
||||
const fromTaskId = String(relation?.fromTaskId || "");
|
||||
const toTaskId = String(relation?.toTaskId || "");
|
||||
if (!taskIds.has(fromTaskId) || !taskIds.has(toTaskId) || fromTaskId === toTaskId) return null;
|
||||
const key = `${fromTaskId}:${toTaskId}`;
|
||||
if (seen.has(key) || seenChildren.has(fromTaskId)) return null;
|
||||
seen.add(key);
|
||||
seenChildren.add(fromTaskId);
|
||||
return {
|
||||
id: relation?.id || uid("relation"),
|
||||
fromTaskId,
|
||||
toTaskId,
|
||||
dependency: relation?.dependency === true || legacyDependencies.has(key)
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function normalizeTaskPlannerData(data) {
|
||||
const tasks = (Array.isArray(data?.tasks) ? data.tasks : []).map(normalizeTaskPlannerTask).filter(Boolean);
|
||||
const taskIds = new Set(tasks.map((task) => task.id));
|
||||
return {
|
||||
sortMode: TASK_SORT_MODES.has(data?.sortMode) ? data.sortMode : "manual",
|
||||
weeklyResetDay: normalizeWeekDay(data?.weeklyResetDay),
|
||||
resetTime: isClockTimeString(data?.resetTime) ? data.resetTime : DEFAULT_TASK_PLANNER_RESET_TIME,
|
||||
lastResetAt: normalizeTimestamp(data?.lastResetAt),
|
||||
tasks,
|
||||
relations: normalizeTaskPlannerRelations(data, taskIds)
|
||||
};
|
||||
}
|
||||
|
||||
function clampPercent(value) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return 0;
|
||||
|
|
@ -433,6 +513,40 @@ export function compactModuleDataForStorage(type, value) {
|
|||
}
|
||||
return Object.keys(compact).length ? compact : null;
|
||||
}
|
||||
if (type === "taskPlanner") {
|
||||
const normalized = normalizeTaskPlannerData(value);
|
||||
const compact = {};
|
||||
if (normalized.sortMode !== "manual") compact.sortMode = normalized.sortMode;
|
||||
if (normalized.weeklyResetDay !== DEFAULT_TASK_PLANNER_WEEKLY_RESET_DAY) compact.weeklyResetDay = normalized.weeklyResetDay;
|
||||
if (normalized.resetTime !== DEFAULT_TASK_PLANNER_RESET_TIME) compact.resetTime = normalized.resetTime;
|
||||
if (normalized.lastResetAt) compact.lastResetAt = normalized.lastResetAt;
|
||||
if (normalized.tasks.length) {
|
||||
compact.tasks = normalized.tasks.map((task) => {
|
||||
const compactTask = {
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
type: task.type
|
||||
};
|
||||
if (task.description) compactTask.description = task.description;
|
||||
if (task.checked) compactTask.checked = true;
|
||||
if (task.checked && task.checkedAt) compactTask.checkedAt = task.checkedAt;
|
||||
if (task.type === "weekly" && task.weeklyResetDay !== undefined) compactTask.weeklyResetDay = task.weeklyResetDay;
|
||||
return compactTask;
|
||||
});
|
||||
}
|
||||
if (normalized.relations.length) {
|
||||
compact.relations = normalized.relations.map((relation) => {
|
||||
const compactRelation = {
|
||||
id: relation.id,
|
||||
fromTaskId: relation.fromTaskId,
|
||||
toTaskId: relation.toTaskId
|
||||
};
|
||||
if (relation.dependency) compactRelation.dependency = true;
|
||||
return compactRelation;
|
||||
});
|
||||
}
|
||||
return Object.keys(compact).length ? compact : null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -511,6 +625,24 @@ function remapModuleDataForExport(type, data, nextId) {
|
|||
return remapped;
|
||||
}
|
||||
|
||||
if (type === "taskPlanner") {
|
||||
const taskIdMap = new Map();
|
||||
compact.tasks?.forEach((task) => taskIdMap.set(task.id, nextId("task")));
|
||||
const remapped = { ...compact };
|
||||
if (compact.tasks) remapped.tasks = compact.tasks.map((task) => ({ ...task, id: taskIdMap.get(task.id) }));
|
||||
if (compact.relations) {
|
||||
remapped.relations = compact.relations
|
||||
.map((relation) => ({
|
||||
...relation,
|
||||
id: nextId("relation"),
|
||||
fromTaskId: taskIdMap.get(relation.fromTaskId),
|
||||
toTaskId: taskIdMap.get(relation.toTaskId)
|
||||
}))
|
||||
.filter((relation) => relation.fromTaskId && relation.toTaskId);
|
||||
}
|
||||
return remapped;
|
||||
}
|
||||
|
||||
return compact;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,16 @@
|
|||
-webkit-mask-image: url("/static/icons/link.svg");
|
||||
}
|
||||
|
||||
.ui-icon-settings {
|
||||
mask-image: url("/static/icons/settings.svg");
|
||||
-webkit-mask-image: url("/static/icons/settings.svg");
|
||||
}
|
||||
|
||||
.ui-icon-tasklist {
|
||||
mask-image: url("/static/icons/tasklist.svg");
|
||||
-webkit-mask-image: url("/static/icons/tasklist.svg");
|
||||
}
|
||||
|
||||
.ui-icon-edit {
|
||||
mask-image: url("/static/icons/edit.svg");
|
||||
-webkit-mask-image: url("/static/icons/edit.svg");
|
||||
|
|
|
|||
|
|
@ -205,6 +205,30 @@
|
|||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.task-planner-settings,
|
||||
.task-planner-task-settings {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.task-planner-line {
|
||||
grid-template-columns: 30px 24px minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.task-planner-type-select,
|
||||
.task-planner-warning {
|
||||
grid-column: 3 / -1;
|
||||
}
|
||||
|
||||
.task-planner-actions {
|
||||
grid-column: 4;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.task-planner-description,
|
||||
.task-planner-task-settings {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.calculator-form {
|
||||
grid-row: auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -686,6 +686,11 @@
|
|||
-webkit-mask-image: url("/static/icons/checklist.svg");
|
||||
}
|
||||
|
||||
.module-icon-tasklist {
|
||||
mask-image: url("/static/icons/tasklist.svg");
|
||||
-webkit-mask-image: url("/static/icons/tasklist.svg");
|
||||
}
|
||||
|
||||
.module-icon-picture {
|
||||
mask-image: url("/static/icons/picture.svg");
|
||||
-webkit-mask-image: url("/static/icons/picture.svg");
|
||||
|
|
@ -1164,10 +1169,455 @@ textarea:focus {
|
|||
|
||||
.module-add-panel + .links-list,
|
||||
.module-add-panel + .checklist,
|
||||
.module-add-panel + .task-planner-list,
|
||||
.module-add-panel + .counters-grid {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.task-planner-module {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.task-planner-topbar {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.task-planner-topbar .inline-form {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.task-planner-global-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.task-planner-settings-toggle,
|
||||
.task-planner-dropdown-button,
|
||||
.task-planner-settings-button {
|
||||
display: inline-grid;
|
||||
width: 34px;
|
||||
min-width: 34px;
|
||||
min-height: 34px;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(165, 180, 252, 0.1);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(21, 26, 48, 0.76);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.task-planner-settings-toggle {
|
||||
grid-template-columns: 16px minmax(0, auto);
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
padding: 0 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.task-planner-settings-toggle span {
|
||||
overflow: hidden;
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 900;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-planner-settings-toggle .ui-icon,
|
||||
.task-planner-dropdown-button .ui-icon,
|
||||
.task-planner-settings-button .ui-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.task-planner-settings {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(130px, 1fr) minmax(110px, 0.72fr);
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(139, 92, 246, 0.06), transparent 58%),
|
||||
rgba(7, 10, 24, 0.34);
|
||||
}
|
||||
|
||||
.task-planner-settings label,
|
||||
.task-planner-weekly-override {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-planner-settings span,
|
||||
.task-planner-weekly-override span,
|
||||
.task-planner-relations > span {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.task-planner-settings select,
|
||||
.task-planner-settings input,
|
||||
.task-planner-add-form select,
|
||||
.task-planner-type-select,
|
||||
.task-planner-weekly-override select,
|
||||
.task-planner-relations select {
|
||||
width: 100%;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: rgba(15, 23, 42, 0.64);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.task-planner-settings select option,
|
||||
.task-planner-add-form select option,
|
||||
.task-planner-type-select option,
|
||||
.task-planner-weekly-override select option,
|
||||
.task-planner-relations select option {
|
||||
background: #11182f;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.task-planner-add-form {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(120px, 0.36fr) auto;
|
||||
}
|
||||
|
||||
.task-planner-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.task-planner-child-list {
|
||||
position: relative;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.task-planner-child-list::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
bottom: 15px;
|
||||
left: 7px;
|
||||
width: 1px;
|
||||
background: linear-gradient(180deg, rgba(246, 196, 83, 0.46), rgba(139, 92, 246, 0.18));
|
||||
}
|
||||
|
||||
.task-planner-child-list > .task-planner-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.task-planner-child-list > .task-planner-item::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
left: -15px;
|
||||
width: 11px;
|
||||
height: 1px;
|
||||
background: rgba(246, 196, 83, 0.46);
|
||||
}
|
||||
|
||||
.task-planner-empty {
|
||||
padding: var(--space-4);
|
||||
border: 1px dashed rgba(165, 180, 252, 0.18);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.task-planner-item {
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(5, 7, 17, 0.36);
|
||||
}
|
||||
|
||||
.task-planner-item.is-complete > .task-planner-line {
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.task-planner-item.has-missing-dependency {
|
||||
border-color: rgba(246, 196, 83, 0.34);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.06), transparent 44%),
|
||||
rgba(5, 7, 17, 0.36);
|
||||
}
|
||||
|
||||
.task-planner-item.is-dragging {
|
||||
cursor: grabbing;
|
||||
opacity: 0.58;
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.task-planner-item.is-drop-target {
|
||||
border-color: rgba(246, 196, 83, 0.72);
|
||||
box-shadow: inset 0 3px 0 rgba(246, 196, 83, 0.8);
|
||||
}
|
||||
|
||||
.task-planner-item.is-drop-target.drop-after {
|
||||
box-shadow: inset 0 -3px 0 rgba(246, 196, 83, 0.8);
|
||||
}
|
||||
|
||||
.task-planner-line {
|
||||
display: grid;
|
||||
grid-template-columns: 30px 24px minmax(0, 1fr) 134px auto auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.task-planner-line > input[type="checkbox"] {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.task-planner-drag-handle,
|
||||
.task-planner-delete-button {
|
||||
display: inline-grid;
|
||||
width: 30px;
|
||||
min-width: 30px;
|
||||
min-height: 30px;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(165, 180, 252, 0.1);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(21, 26, 48, 0.76);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.task-planner-drag-handle:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.task-planner-drag-handle .ui-icon,
|
||||
.task-planner-delete-button .ui-icon {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.task-planner-actions {
|
||||
display: inline-flex;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.task-planner-title-input {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.task-planner-item.is-complete > .task-planner-line .task-planner-title-input {
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.task-planner-type-select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
overflow: hidden;
|
||||
padding: 0 30px 0 10px;
|
||||
border-color: rgba(165, 180, 252, 0.09);
|
||||
text-overflow: ellipsis;
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23c4b5fd' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
|
||||
linear-gradient(135deg, rgba(139, 92, 246, 0.04), rgba(31, 41, 78, 0.08)),
|
||||
linear-gradient(rgba(21, 26, 48, 0.38), rgba(21, 26, 48, 0.38));
|
||||
background-position:
|
||||
calc(100% - 10px) 50%,
|
||||
0 0,
|
||||
0 0;
|
||||
background-size:
|
||||
11px 11px,
|
||||
auto,
|
||||
auto;
|
||||
background-repeat: no-repeat;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.task-planner-type-select:hover,
|
||||
.task-planner-type-select:focus {
|
||||
border-color: rgba(196, 181, 253, 0.22);
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23f6c453' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.06), rgba(139, 92, 246, 0.08)),
|
||||
linear-gradient(rgba(21, 26, 48, 0.58), rgba(21, 26, 48, 0.58));
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.task-planner-warning {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid rgba(246, 196, 83, 0.28);
|
||||
border-radius: var(--radius-pill);
|
||||
background: rgba(246, 196, 83, 0.08);
|
||||
color: var(--color-accent-gold);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 900;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.inline-notice {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 5px 9px;
|
||||
border: 1px solid rgba(246, 196, 83, 0.28);
|
||||
border-radius: var(--radius-md);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.1), rgba(139, 92, 246, 0.08)),
|
||||
rgba(9, 12, 28, 0.9);
|
||||
box-shadow: 0 10px 24px rgba(2, 6, 23, 0.22);
|
||||
color: var(--color-accent-gold);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 900;
|
||||
line-height: 1.2;
|
||||
animation: inline-notice-in 160ms ease;
|
||||
}
|
||||
|
||||
.inline-notice .ui-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.task-planner-notice {
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
left: 72px;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes inline-notice-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.task-planner-description,
|
||||
.task-planner-task-settings {
|
||||
margin-left: 62px;
|
||||
}
|
||||
|
||||
.task-planner-description {
|
||||
min-height: 72px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.task-planner-task-settings {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(130px, 0.32fr) minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
padding: 10px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.1);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(7, 10, 24, 0.24);
|
||||
}
|
||||
|
||||
.task-planner-relations {
|
||||
display: grid;
|
||||
grid-column: span 1;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-planner-relations > div {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.task-planner-relations em {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.task-planner-relation-chip {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto 28px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 4px 5px 4px 9px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(21, 26, 48, 0.46);
|
||||
}
|
||||
|
||||
.task-planner-relation-chip > span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 800;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.task-planner-relation-chip label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.task-planner-relation-chip label input {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.task-planner-relation-chip button {
|
||||
display: inline-flex;
|
||||
width: 26px;
|
||||
min-width: 26px;
|
||||
min-height: 26px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(21, 26, 48, 0.58);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.task-planner-relation-chip button .ui-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.task-planner-relations select {
|
||||
min-height: 34px;
|
||||
padding-inline: 10px;
|
||||
}
|
||||
|
||||
.counters-add-form {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
|
@ -1336,6 +1786,52 @@ textarea:focus {
|
|||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.timer-countdown-form select,
|
||||
.task-planner-settings select,
|
||||
.task-planner-add-form select,
|
||||
.task-planner-type-select,
|
||||
.task-planner-weekly-override select,
|
||||
.task-planner-relations select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
overflow: hidden;
|
||||
padding-right: 34px;
|
||||
text-overflow: ellipsis;
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23c4b5fd' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
|
||||
linear-gradient(135deg, rgba(139, 92, 246, 0.04), rgba(31, 41, 78, 0.08)),
|
||||
linear-gradient(rgba(21, 26, 48, 0.38), rgba(21, 26, 48, 0.38));
|
||||
background-position:
|
||||
calc(100% - 10px) 50%,
|
||||
0 0,
|
||||
0 0;
|
||||
background-size:
|
||||
11px 11px,
|
||||
auto,
|
||||
auto;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.timer-countdown-form select:hover,
|
||||
.timer-countdown-form select:focus,
|
||||
.task-planner-settings select:hover,
|
||||
.task-planner-settings select:focus,
|
||||
.task-planner-add-form select:hover,
|
||||
.task-planner-add-form select:focus,
|
||||
.task-planner-type-select:hover,
|
||||
.task-planner-type-select:focus,
|
||||
.task-planner-weekly-override select:hover,
|
||||
.task-planner-weekly-override select:focus,
|
||||
.task-planner-relations select:hover,
|
||||
.task-planner-relations select:focus {
|
||||
border-color: rgba(196, 181, 253, 0.22);
|
||||
background-image:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23f6c453' viewBox='-6.5 0 32 32'%3E%3Cpath d='M18.813 11.406l-7.906 9.906c-.75.906-1.906.906-2.625 0L.376 11.406c-.75-.938-.375-1.656.781-1.656h16.875c1.188 0 1.531.719.781 1.656z'/%3E%3C/svg%3E"),
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.06), rgba(139, 92, 246, 0.08)),
|
||||
linear-gradient(rgba(21, 26, 48, 0.58), rgba(21, 26, 48, 0.58));
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.time-parts-input {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(34px, 1fr) 4px minmax(34px, 1fr) 4px minmax(34px, 1fr);
|
||||
|
|
@ -1735,6 +2231,14 @@ textarea:focus {
|
|||
.tool-split-entry > button:not(.tool-split-entry-summary, .tool-split-entry-value, .danger):hover,
|
||||
.counter-actions button:not(.danger):hover,
|
||||
.link-item button:not(.danger):hover,
|
||||
.task-planner-drag-handle:hover:not(:disabled),
|
||||
.task-planner-settings-toggle:hover,
|
||||
.task-planner-settings-toggle.active,
|
||||
.task-planner-dropdown-button:hover,
|
||||
.task-planner-dropdown-button.active,
|
||||
.task-planner-settings-button:hover,
|
||||
.task-planner-settings-button.active,
|
||||
.task-planner-relation-chip button:hover,
|
||||
.module-scroll-button:hover,
|
||||
.module-scroll-button.active,
|
||||
.tool-split-scroll-toggle:hover,
|
||||
|
|
@ -1820,7 +2324,8 @@ textarea:focus {
|
|||
.tool-split-action-button.danger,
|
||||
.tool-split-entry > button.danger,
|
||||
.counter-actions button.danger,
|
||||
.link-item button.danger {
|
||||
.link-item button.danger,
|
||||
.task-planner-delete-button.danger {
|
||||
border-color: rgba(251, 113, 133, 0.2);
|
||||
background: rgba(21, 26, 48, 0.88);
|
||||
color: var(--color-danger);
|
||||
|
|
@ -1829,7 +2334,8 @@ textarea:focus {
|
|||
.tool-split-action-button.danger:hover:not(:disabled),
|
||||
.tool-split-entry > button.danger:hover,
|
||||
.counter-actions button.danger:hover,
|
||||
.link-item button.danger:hover {
|
||||
.link-item button.danger:hover,
|
||||
.task-planner-delete-button.danger:hover {
|
||||
border-color: rgba(251, 113, 133, 0.56);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(80, 25, 42, 0.72), rgba(48, 18, 34, 0.82)) padding-box,
|
||||
|
|
@ -1843,7 +2349,8 @@ textarea:focus {
|
|||
.tool-split-action-button.danger:hover .ui-icon,
|
||||
.tool-split-entry > button.danger:hover .ui-icon,
|
||||
.counter-actions button.danger:hover .ui-icon,
|
||||
.link-item button.danger:hover .ui-icon {
|
||||
.link-item button.danger:hover .ui-icon,
|
||||
.task-planner-delete-button.danger:hover .ui-icon {
|
||||
background-color: #fff;
|
||||
filter: drop-shadow(0 0 6px rgba(251, 113, 133, 0.26));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue