From a0d7e80a1a6d065cabeebea7323614de9495dcfd Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Tue, 25 Aug 2026 09:33:15 +0200 Subject: [PATCH] Split remaining toolbox modules by responsibility --- tests/static-toolboxes.test.mjs | 174 ++++--- .../toolboxes/modules/AddToolControls.jsx | 105 +++++ .../toolboxes/modules/CalendarModeChoice.jsx | 19 + .../toolboxes/modules/CalendarModule.jsx | 431 +---------------- .../toolboxes/modules/GoalTreeBranch.jsx | 160 +++++++ .../modules/GoalTreeEditorFields.jsx | 157 +++++++ .../toolboxes/modules/GoalTreeNodeContent.jsx | 71 +++ .../toolboxes/modules/GoalTreeParts.jsx | 392 +--------------- .../toolboxes/modules/GoalTreeView.jsx | 2 +- .../toolboxes/modules/TaskPlannerBranch.jsx | 199 ++++++++ .../toolboxes/modules/TaskPlannerItem.jsx | 152 ++++++ .../toolboxes/modules/TaskPlannerItems.jsx | 349 +------------- .../toolboxes/modules/TaskPlannerModule.jsx | 2 +- .../toolboxes/modules/TimerCountdown.jsx | 196 ++++++++ .../toolboxes/modules/TimerModule.jsx | 3 +- .../features/toolboxes/modules/TimerParts.jsx | 354 +------------- .../toolboxes/modules/TimerSharedControls.jsx | 96 ++++ .../toolboxes/modules/TimerStopwatch.jsx | 67 +++ .../toolboxes/modules/ToolboxModules.jsx | 295 ++++++++++++ .../toolboxes/modules/calendarEventActions.js | 110 +++++ .../src/features/toolboxes/modules/index.jsx | 437 +----------------- .../modules/toolboxModuleComponents.jsx | 38 ++ .../modules/useCalendarController.js | 282 +++++++++++ .../modules/useCalendarDraftControls.js | 38 ++ .../toolboxes/modules/useCalendarLifecycle.js | 62 +++ 25 files changed, 2199 insertions(+), 1992 deletions(-) create mode 100644 website/src/features/toolboxes/modules/AddToolControls.jsx create mode 100644 website/src/features/toolboxes/modules/CalendarModeChoice.jsx create mode 100644 website/src/features/toolboxes/modules/GoalTreeBranch.jsx create mode 100644 website/src/features/toolboxes/modules/GoalTreeEditorFields.jsx create mode 100644 website/src/features/toolboxes/modules/GoalTreeNodeContent.jsx create mode 100644 website/src/features/toolboxes/modules/TaskPlannerBranch.jsx create mode 100644 website/src/features/toolboxes/modules/TaskPlannerItem.jsx create mode 100644 website/src/features/toolboxes/modules/TimerCountdown.jsx create mode 100644 website/src/features/toolboxes/modules/TimerSharedControls.jsx create mode 100644 website/src/features/toolboxes/modules/TimerStopwatch.jsx create mode 100644 website/src/features/toolboxes/modules/ToolboxModules.jsx create mode 100644 website/src/features/toolboxes/modules/calendarEventActions.js create mode 100644 website/src/features/toolboxes/modules/toolboxModuleComponents.jsx create mode 100644 website/src/features/toolboxes/modules/useCalendarController.js create mode 100644 website/src/features/toolboxes/modules/useCalendarDraftControls.js create mode 100644 website/src/features/toolboxes/modules/useCalendarLifecycle.js diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index 4fd47be..5540764 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -11,6 +11,10 @@ import { getCalendarOccurrences, getCurrentCalendarOccurrence, getMonthEnd, getM import { exportModuleText, importModuleText } from "../website/src/features/toolboxes/modules/textImport.js"; import { compactCalendarDataForStorage, normalizeCalendarData } from "../website/src/features/toolboxes/storage/modules/calendar.js"; +function countLines(source) { + return source.trimEnd().split("\n").length; +} + test("toolbox storage, cards and pages are wired", async () => { const source = await readFile("website/src/main.jsx", "utf8"); const toolboxCard = await readFile("website/src/features/toolboxes/ToolboxCard.jsx", "utf8"); @@ -142,6 +146,10 @@ test("toolbox storage, cards and pages are wired", async () => { test("toolbox module registry and modules expose expected behavior", async () => { const source = await readFile("website/src/main.jsx", "utf8"); const moduleRegistry = await readFile("website/src/features/toolboxes/modules/index.jsx", "utf8"); + const moduleComponents = await readFile("website/src/features/toolboxes/modules/toolboxModuleComponents.jsx", "utf8"); + const addToolControls = await readFile("website/src/features/toolboxes/modules/AddToolControls.jsx", "utf8"); + const toolboxModules = await readFile("website/src/features/toolboxes/modules/ToolboxModules.jsx", "utf8"); + const moduleRegistryBundle = `${moduleRegistry}\n${moduleComponents}\n${addToolControls}\n${toolboxModules}`; const moduleMetadataRegistry = await readFile("website/src/features/toolboxes/toolboxModuleRegistry.js", "utf8"); const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8"); const notepadToolbar = await readFile("website/src/features/toolboxes/modules/NotepadToolbar.jsx", "utf8"); @@ -169,30 +177,44 @@ test("toolbox module registry and modules expose expected behavior", async () => const tableFormulaEngine = await readFile("website/src/features/toolboxes/modules/tableFormulaEngine.js", "utf8"); const timerModule = await readFile("website/src/features/toolboxes/modules/TimerModule.jsx", "utf8"); const timerParts = await readFile("website/src/features/toolboxes/modules/TimerParts.jsx", "utf8"); + const timerCountdown = await readFile("website/src/features/toolboxes/modules/TimerCountdown.jsx", "utf8"); + const timerSharedControls = await readFile("website/src/features/toolboxes/modules/TimerSharedControls.jsx", "utf8"); + const timerStopwatch = await readFile("website/src/features/toolboxes/modules/TimerStopwatch.jsx", "utf8"); + const timerBundle = `${timerModule}\n${timerParts}\n${timerCountdown}\n${timerSharedControls}\n${timerStopwatch}`; const timerOptions = await readFile("website/src/features/toolboxes/modules/timerOptions.js", "utf8"); const calendarModule = await readFile("website/src/features/toolboxes/modules/CalendarModule.jsx", "utf8"); + const calendarController = await readFile("website/src/features/toolboxes/modules/useCalendarController.js", "utf8"); + const calendarDraftControls = await readFile("website/src/features/toolboxes/modules/useCalendarDraftControls.js", "utf8"); + const calendarEventActions = await readFile("website/src/features/toolboxes/modules/calendarEventActions.js", "utf8"); + const calendarLifecycle = await readFile("website/src/features/toolboxes/modules/useCalendarLifecycle.js", "utf8"); + const calendarModeChoice = await readFile("website/src/features/toolboxes/modules/CalendarModeChoice.jsx", "utf8"); const calendarContent = await readFile("website/src/features/toolboxes/modules/CalendarContent.jsx", "utf8"); const calendarDraft = await readFile("website/src/features/toolboxes/modules/calendarDraft.js", "utf8"); const calendarEventForm = await readFile("website/src/features/toolboxes/modules/CalendarEventForm.jsx", "utf8"); const calendarEventControls = await readFile("website/src/features/toolboxes/modules/CalendarEventControls.jsx", "utf8"); const calendarMonthDatePicker = await readFile("website/src/features/toolboxes/modules/CalendarMonthDatePicker.jsx", "utf8"); const calendarViews = await readFile("website/src/features/toolboxes/modules/CalendarViews.jsx", "utf8"); - const calendarBundle = `${calendarModule}\n${calendarContent}\n${calendarDraft}\n${calendarEventForm}\n${calendarEventControls}\n${calendarMonthDatePicker}\n${calendarViews}`; + const calendarBundle = `${calendarModule}\n${calendarController}\n${calendarDraftControls}\n${calendarEventActions}\n${calendarLifecycle}\n${calendarModeChoice}\n${calendarContent}\n${calendarDraft}\n${calendarEventForm}\n${calendarEventControls}\n${calendarMonthDatePicker}\n${calendarViews}`; const calendarUtils = await readFile("website/src/features/toolboxes/modules/calendarUtils.js", "utf8"); const taskPlannerModule = await readFile("website/src/features/toolboxes/modules/TaskPlannerModule.jsx", "utf8"); const taskPlannerItems = await readFile("website/src/features/toolboxes/modules/TaskPlannerItems.jsx", "utf8"); + const taskPlannerBranch = await readFile("website/src/features/toolboxes/modules/TaskPlannerBranch.jsx", "utf8"); + const taskPlannerItem = await readFile("website/src/features/toolboxes/modules/TaskPlannerItem.jsx", "utf8"); const taskPlannerItemPanels = await readFile("website/src/features/toolboxes/modules/TaskPlannerItemPanels.jsx", "utf8"); const taskPlannerReorder = await readFile("website/src/features/toolboxes/modules/useTaskPlannerReorder.js", "utf8"); const taskPlannerTopbar = await readFile("website/src/features/toolboxes/modules/TaskPlannerTopbar.jsx", "utf8"); const taskPlannerSettings = await readFile("website/src/features/toolboxes/modules/TaskPlannerSettings.jsx", "utf8"); - const taskPlannerBundle = `${taskPlannerItems}\n${taskPlannerItemPanels}\n${taskPlannerReorder}\n${taskPlannerTopbar}\n${taskPlannerSettings}`; + const taskPlannerBundle = `${taskPlannerItems}\n${taskPlannerBranch}\n${taskPlannerItem}\n${taskPlannerItemPanels}\n${taskPlannerReorder}\n${taskPlannerTopbar}\n${taskPlannerSettings}`; const taskPlannerUtils = await readFile("website/src/features/toolboxes/modules/taskPlannerUtils.js", "utf8"); const goalTreeModule = await readFile("website/src/features/toolboxes/modules/GoalTreeModule.jsx", "utf8"); const goalTreeEditor = await readFile("website/src/features/toolboxes/modules/GoalTreeEditor.jsx", "utf8"); const goalTreeParts = await readFile("website/src/features/toolboxes/modules/GoalTreeParts.jsx", "utf8"); + const goalTreeBranch = await readFile("website/src/features/toolboxes/modules/GoalTreeBranch.jsx", "utf8"); + const goalTreeEditorFields = await readFile("website/src/features/toolboxes/modules/GoalTreeEditorFields.jsx", "utf8"); + const goalTreeNodeContent = await readFile("website/src/features/toolboxes/modules/GoalTreeNodeContent.jsx", "utf8"); const goalTreeController = await readFile("website/src/features/toolboxes/modules/useGoalTreeController.js", "utf8"); const goalTreeView = await readFile("website/src/features/toolboxes/modules/GoalTreeView.jsx", "utf8"); - const goalTreeBundle = `${goalTreeModule}\n${goalTreeEditor}\n${goalTreeParts}\n${goalTreeController}\n${goalTreeView}`; + const goalTreeBundle = `${goalTreeModule}\n${goalTreeEditor}\n${goalTreeParts}\n${goalTreeBranch}\n${goalTreeEditorFields}\n${goalTreeNodeContent}\n${goalTreeController}\n${goalTreeView}`; const goalTreeDisplay = await readFile("website/src/features/toolboxes/modules/goalTreeDisplay.js", "utf8"); const goalTreeUtils = await readFile("website/src/features/toolboxes/modules/goalTreeUtils.js", "utf8"); const equipmentPlannerModule = await readFile("website/src/features/toolboxes/modules/EquipmentPlannerModule.jsx", "utf8"); @@ -222,51 +244,81 @@ test("toolbox module registry and modules expose expected behavior", async () => const textImportTime = await readFile("website/src/features/toolboxes/modules/textImport/time.js", "utf8"); const imageViewer = await readFile("website/src/components/ImageViewer.jsx", "utf8"); + [ + ["CalendarModule.jsx", calendarModule], + ["modules/index.jsx", moduleRegistry], + ["GoalTreeParts.jsx", goalTreeParts], + ["TimerParts.jsx", timerParts], + ["TaskPlannerItems.jsx", taskPlannerItems] + ].forEach(([fileName, fileSource]) => { + assert.ok(countLines(fileSource) <= 220, `${fileName} must remain a short orchestrator/barrel`); + }); + + [ + ["AddToolControls.jsx", addToolControls], + ["ToolboxModules.jsx", toolboxModules], + ["useCalendarController.js", calendarController], + ["useCalendarDraftControls.js", calendarDraftControls], + ["calendarEventActions.js", calendarEventActions], + ["useCalendarLifecycle.js", calendarLifecycle], + ["CalendarModeChoice.jsx", calendarModeChoice], + ["GoalTreeBranch.jsx", goalTreeBranch], + ["GoalTreeEditorFields.jsx", goalTreeEditorFields], + ["GoalTreeNodeContent.jsx", goalTreeNodeContent], + ["TimerCountdown.jsx", timerCountdown], + ["TimerSharedControls.jsx", timerSharedControls], + ["TimerStopwatch.jsx", timerStopwatch], + ["TaskPlannerBranch.jsx", taskPlannerBranch], + ["TaskPlannerItem.jsx", taskPlannerItem] + ].forEach(([fileName, fileSource]) => { + assert.ok(countLines(fileSource) <= 300, `${fileName} must stay focused; split by responsibility before growing further`); + }); + assert.match(moduleMetadataRegistry, /export const TOOLBOX_MODULES/); assert.match(moduleMetadataRegistry, /export const TOOLBOX_MODULE_DEFINITIONS/); assert.match(moduleMetadataRegistry, /export function getDefaultModuleTitle/); - assert.match(moduleRegistry, /export function AddToolControls/); - assert.match(moduleRegistry, /export function ToolboxModules/); - assert.match(moduleRegistry, /MODULE_COMPONENTS/); - assert.match(moduleRegistry, /lazy\(\(\) => import\("\.\/CombosModule\.jsx"\)/); - assert.match(moduleRegistry, /lazy\(\(\) => import\("\.\/CalendarModule\.jsx"\)/); - assert.match(moduleRegistry, /lazy\(\(\) => import\("\.\/GoalTreeModule\.jsx"\)/); - assert.match(moduleRegistry, /lazy\(\(\) => import\("\.\/EquipmentPlannerModule\.jsx"\)/); - assert.match(moduleRegistry, /Suspense fallback=\{\}/); - assert.match(moduleRegistry, /LAZY_MODULE_TYPES/); - assert.match(moduleRegistry, /notepad:/); - assert.match(moduleRegistry, /checklist:/); - assert.match(moduleRegistry, /images:/); - assert.match(moduleRegistry, /links:/); - assert.match(moduleRegistry, /counters:/); - assert.match(moduleRegistry, /combos:/); - assert.match(moduleRegistry, /CombosModule/); - assert.match(moduleRegistry, /calculator:/); - assert.match(moduleRegistry, /table:/); - assert.match(moduleRegistry, /table: \{ \.\.\.TOOLBOX_MODULE_DEFINITIONS\.table, Component: TableModule \}/); - assert.match(moduleRegistry, /timer:/); - assert.match(moduleRegistry, /calendar:/); - assert.match(moduleRegistry, /taskPlanner:/); - assert.match(moduleRegistry, /goalTree:/); - assert.match(moduleRegistry, /equipmentPlanner:/); + assert.match(moduleRegistry, /export \{ AddToolControls \}/); + assert.match(moduleRegistry, /export \{ ToolboxModules \}/); + assert.match(moduleComponents, /MODULE_COMPONENTS/); + assert.match(moduleComponents, /lazy\(\(\) => import\("\.\/CombosModule\.jsx"\)/); + assert.match(moduleComponents, /lazy\(\(\) => import\("\.\/CalendarModule\.jsx"\)/); + assert.match(moduleComponents, /lazy\(\(\) => import\("\.\/GoalTreeModule\.jsx"\)/); + assert.match(moduleComponents, /lazy\(\(\) => import\("\.\/EquipmentPlannerModule\.jsx"\)/); + assert.match(toolboxModules, /Suspense fallback=\{\}/); + assert.match(moduleComponents, /LAZY_MODULE_TYPES/); + assert.match(moduleComponents, /notepad:/); + assert.match(moduleComponents, /checklist:/); + assert.match(moduleComponents, /images:/); + assert.match(moduleComponents, /links:/); + assert.match(moduleComponents, /counters:/); + assert.match(moduleComponents, /combos:/); + assert.match(moduleComponents, /CombosModule/); + assert.match(moduleComponents, /calculator:/); + assert.match(moduleComponents, /table:/); + assert.match(moduleComponents, /table: \{ \.\.\.TOOLBOX_MODULE_DEFINITIONS\.table, Component: TableModule \}/); + assert.match(moduleComponents, /timer:/); + assert.match(moduleComponents, /calendar:/); + assert.match(moduleComponents, /taskPlanner:/); + assert.match(moduleComponents, /goalTree:/); + assert.match(moduleComponents, /equipmentPlanner:/); assert.match(moduleMetadataRegistry, /Planificateur d'équipements/); - assert.match(moduleRegistry, /imageAnnotation:/); + assert.match(moduleComponents, /imageAnnotation:/); assert.match(moduleMetadataRegistry, /Annotation d'images/); - assert.match(moduleRegistry, /notepad: \{ \.\.\.TOOLBOX_MODULE_DEFINITIONS\.notepad, Component: NotepadModule \}/); - assert.match(moduleRegistry, /module-edit-button/); - assert.match(moduleRegistry, /itemReorder\.startDrag/); - assert.match(moduleRegistry, /onPointerDown/); - assert.match(moduleRegistry, /useGroupedReorder/); - assert.match(moduleRegistry, /useInlineEdit/); - assert.match(moduleRegistry, /getContentEditableProps/); - assert.match(moduleRegistry, /tool-add-card/); - assert.match(moduleRegistry, /tool-quick-add-button/); - assert.match(moduleRegistry, /tool-add-quick-toggle/); + assert.match(moduleComponents, /notepad: \{ \.\.\.TOOLBOX_MODULE_DEFINITIONS\.notepad, Component: NotepadModule \}/); + assert.match(toolboxModules, /module-edit-button/); + assert.match(toolboxModules, /itemReorder\.startDrag/); + assert.match(moduleRegistryBundle, /onPointerDown/); + assert.match(toolboxModules, /useGroupedReorder/); + assert.match(toolboxModules, /useInlineEdit/); + assert.match(toolboxModules, /getContentEditableProps/); + assert.match(addToolControls, /tool-add-card/); + assert.match(addToolControls, /tool-quick-add-button/); + assert.match(addToolControls, /tool-add-quick-toggle/); assert.match(moduleMetadataRegistry, /scrollable: true/); - assert.match(moduleRegistry, /module-content/); - assert.match(moduleRegistry, /module-scroll-button/); - assert.match(moduleRegistry, /Icon name="dropdown"/); - assert.match(moduleRegistry, /CompactDropdown/); + assert.match(toolboxModules, /module-content/); + assert.match(toolboxModules, /module-scroll-button/); + assert.match(addToolControls, /Icon name="dropdown"/); + assert.match(addToolControls, /CompactDropdown/); assert.doesNotMatch(source, / + {primaryParent.title} + + )} + {options.map((candidate) => ( + + ))} + + + ); +} + +export function GoalTreeTitleInput({ value, onChange }) { + const [draft, setDraft] = useState(value || ""); + return ( + { + const title = event.target.value; + setDraft(title); + onChange(title); + }} + /> + ); +} + +export function GoalTreeFormulaInput({ value, onChange, hasParent, textContent, placeholder, title, className = "", "aria-label": ariaLabel }) { + const normalizedValue = String(value ?? ""); + if (!hasParent) { + return onChange(event.target.value.replaceAll(",", "."))} placeholder={placeholder} title={title} value={normalizedValue} />; + } + + return ( + onChange(nextValue.replaceAll(",", "."))} + placeholder={placeholder} + title={title} + tokens={[{ + value: BASE_TOKEN, + label: textContent.baseTokenLabel || "Valeur du parent" + }]} + value={normalizedValue} + /> + ); +} + +export function GoalTreeContentEditor({ node, textContent, onAdd, onUpdate, onDelete }) { + return ( +
+
{textContent.contentSectionTitle || "Contenu"}
+ {(node.items || []).map((item) => ( +
+ onUpdate(item.id, (current) => ({ ...current, label: event.target.value }))} aria-label={textContent.itemLabelLabel || "Libellé"} /> + {node.kind === "checklist" && ( + onUpdate(item.id, (current) => ({ ...current, formula: value.replaceAll(",", ".") }))} + value={item.formula || "1"} + placeholder={textContent.itemTotalPlaceholder || "base"} + textContent={textContent} + title={textContent.itemTotalHelp || "Saisissez un nombre ou une formule avec base pour référencer la cible du parent."} + /> + )} + +
+ ))} + +
+ ); +} + +export function GoalTreeIconPicker({ label, value, textContent, onChange }) { + return ( +
+ {label} + ( + + )} + > + {({ close }) => ( + <> + {GOAL_TREE_ICON_NAMES.map((icon) => ( + + ))} + + )} + +
+ ); +} diff --git a/website/src/features/toolboxes/modules/GoalTreeNodeContent.jsx b/website/src/features/toolboxes/modules/GoalTreeNodeContent.jsx new file mode 100644 index 0000000..a502368 --- /dev/null +++ b/website/src/features/toolboxes/modules/GoalTreeNodeContent.jsx @@ -0,0 +1,71 @@ +// Rôle : rend les contrôles inline et aperçus de contenu des noeuds goal tree. +import { Icon } from "../../../components/Icon.jsx"; +import { Tooltip } from "../../../components/Tooltip.jsx"; +import { formatGoalTreeNumber } from "./goalTreeUtils.js"; + +function GoalTreeChecklistInlineControl({ item, textContent, onUpdate }) { + const target = Math.max(1, item.qtyTarget || 1); + const current = Math.min(Math.max(0, item.qtyCurrent || 0), target); + if (target <= 1) { + return = 1} onChange={(event) => onUpdate((entry) => ({ ...entry, qtyCurrent: event.target.checked ? 1 : 0 }))} aria-label={textContent.itemCheckedLabel || "État"} />; + } + return ( + + + {formatGoalTreeNumber(current)} / {formatGoalTreeNumber(target)} + + + ); +} + +export function GoalTreeStepLeafControl({ node, textContent, onChange }) { + const target = Math.max(1, node.computedQtyTarget || node.qtyTarget || 1); + const current = Math.min(Math.max(0, node.qtyCurrent || 0), target); + if (target <= 1) { + return = 1} onChange={(event) => onChange(event.target.checked ? 1 : 0)} aria-label={textContent.itemCheckedLabel || "État"} />; + } + return ( + + + {formatGoalTreeNumber(current)} / {formatGoalTreeNumber(target)} + + + ); +} + +export function GoalTreeNodeContentPreview({ node, editing, textContent, onAddContentItem, onUpdateItem }) { + const items = node.items || []; + return ( + + {items.map((item) => ( + + {node.kind === "checklist" && onUpdateItem(item.id, updater)} />} + + {item.label} + + + ))} + {editing && ( + + + + )} + + ); +} diff --git a/website/src/features/toolboxes/modules/GoalTreeParts.jsx b/website/src/features/toolboxes/modules/GoalTreeParts.jsx index 76bdbb3..9e66e6e 100644 --- a/website/src/features/toolboxes/modules/GoalTreeParts.jsx +++ b/website/src/features/toolboxes/modules/GoalTreeParts.jsx @@ -1,379 +1,13 @@ -// Rôle : rend les sous-composants d'édition et de prévisualisation de l'arbre d'objectifs. -import { useState } from "react"; -import { CompactDropdown } from "../../../components/CompactDropdown.jsx"; -import { Icon } from "../../../components/Icon.jsx"; -import { MixTagsInput } from "../../../components/MixTagsInput.jsx"; -import { Tooltip } from "../../../components/Tooltip.jsx"; -import { - GOAL_TREE_ICON_NAMES, - formatGoalTreeNumber, - getGoalTreeChildren, - getGoalTreeDescendantIds, - getGoalTreeParentIds -} from "./goalTreeUtils.js"; -import { - BASE_TOKEN, - getEffectiveGoalTreeProgressMode, - getGoalTreeNodeIcon, - getGoalTreeNodeIconColor, - getGoalTreeNodeTooltip, - isSingleValueGoalTreeNode -} from "./goalTreeDisplay.js"; - -export function GoalTreePrerequisitePicker({ nodes, node, textContent, onToggle }) { - const parentIds = new Set(getGoalTreeParentIds(node)); - const blockedIds = new Set([node.id, ...getGoalTreeDescendantIds(nodes, node.id)]); - const primaryParent = nodes.find((candidate) => candidate.id === node.parentId); - const options = nodes.filter((candidate) => !blockedIds.has(candidate.id) && candidate.id !== node.parentId); - if (!primaryParent && !options.length) return null; - return ( -
- {textContent.prerequisitesSectionTitle || "Prérequis supplémentaires"} -
- {primaryParent && ( - - )} - {options.map((candidate) => ( - - ))} -
-
- ); -} - -export function GoalTreeBranch({ nodes, parentId, selectedNodeId, editing, reorder, textContent, onSelect, onAdd, onAddContentItem, onSetStepLeafQuantity, onSetStepChildrenCompletion, onUpdateNodeItem, onEditNode, onToggleBranch, onDeleteNode }) { - const children = getGoalTreeChildren(nodes, parentId); - if (!children.length) return null; - return ( -
    - {children.map((node) => { - const nestedChildren = getGoalTreeChildren(nodes, node.id); - const linkedChildren = nodes.filter((candidate) => candidate.parentId !== node.id && getGoalTreeParentIds(candidate).includes(node.id)); - const branchCollapsed = node.collapsed; - const isSelected = selectedNodeId === node.id; - const isComplete = node.progress.complete; - const hasSingleValueChildren = nestedChildren.length > 0 && nestedChildren.every(isSingleValueGoalTreeNode); - const manualProgress = getEffectiveGoalTreeProgressMode(node, nestedChildren) === "manual"; - const hasBranchToggle = nestedChildren.length > 0 || linkedChildren.length > 0; - const hasNodeActions = editing || hasBranchToggle; - const iconColor = getGoalTreeNodeIconColor(node); - const selectFromCard = (event) => { - if (!editing) return; - if (event.target.closest("button:not(.goal-tree-node-main)")) return; - onSelect(node.id); - }; - const className = [ - "goal-tree-node", - `is-${node.kind}`, - isSelected ? "is-selected" : "", - isComplete ? "is-complete" : "", - reorder.isItemDragging(node.id) ? "is-dragging" : "", - reorder.isItemDropTarget(node.id) ? "is-drop-target" : "", - reorder.getDropPlacement("item", node.id) === "after" ? "drop-after" : "" - ].filter(Boolean).join(" "); - return ( -
  • -
    - {!editing && ( -
    - - -
    - )} - {editing && ( - - )} - - {node.kind === "step" ? ( - <> - - {(manualProgress || !nestedChildren.length) && ( - onSetStepLeafQuantity(node.id, quantity)} /> - )} - {!manualProgress && hasSingleValueChildren && ( - onSetStepChildrenCompletion(node.id, quantity > 0)} - /> - )} - - ) : ( - <> - - - {node.title} - - onAddContentItem(node.id)} - onUpdateItem={(itemId, updater) => onUpdateNodeItem(node.id, itemId, updater)} - /> - - )} - - {hasNodeActions && ( -
    - {editing && ( - - )} - {hasBranchToggle && ( - - )} - {editing && ( - - )} -
    - )} -
    - {!node.collapsed && nestedChildren.length > 0 && ( - - )} -
  • - ); - })} -
- ); -} - -export function GoalTreeTitleInput({ value, onChange }) { - const [draft, setDraft] = useState(value || ""); - return ( - { - const title = event.target.value; - setDraft(title); - onChange(title); - }} - /> - ); -} - -export function GoalTreeFormulaInput({ value, onChange, hasParent, textContent, placeholder, title, className = "", "aria-label": ariaLabel }) { - const normalizedValue = String(value ?? ""); - if (!hasParent) { - return onChange(event.target.value.replaceAll(",", "."))} placeholder={placeholder} title={title} value={normalizedValue} />; - } - - return ( - onChange(nextValue.replaceAll(",", "."))} - placeholder={placeholder} - title={title} - tokens={[{ - value: BASE_TOKEN, - label: textContent.baseTokenLabel || "Valeur du parent" - }]} - value={normalizedValue} - /> - ); -} - -export function GoalTreeContentEditor({ node, textContent, onAdd, onUpdate, onDelete }) { - return ( -
-
{textContent.contentSectionTitle || "Contenu"}
- {(node.items || []).map((item) => ( -
- onUpdate(item.id, (current) => ({ ...current, label: event.target.value }))} aria-label={textContent.itemLabelLabel || "Libellé"} /> - {node.kind === "checklist" && ( - onUpdate(item.id, (current) => ({ ...current, formula: value.replaceAll(",", ".") }))} - value={item.formula || "1"} - placeholder={textContent.itemTotalPlaceholder || "base"} - textContent={textContent} - title={textContent.itemTotalHelp || "Saisissez un nombre ou une formule avec base pour référencer la cible du parent."} - /> - )} - -
- ))} - -
- ); -} - -function GoalTreeChecklistInlineControl({ item, textContent, onUpdate }) { - const target = Math.max(1, item.qtyTarget || 1); - const current = Math.min(Math.max(0, item.qtyCurrent || 0), target); - if (target <= 1) { - return = 1} onChange={(event) => onUpdate((entry) => ({ ...entry, qtyCurrent: event.target.checked ? 1 : 0 }))} aria-label={textContent.itemCheckedLabel || "État"} />; - } - return ( - - - {formatGoalTreeNumber(current)} / {formatGoalTreeNumber(target)} - - - ); -} - -function GoalTreeStepLeafControl({ node, textContent, onChange }) { - const target = Math.max(1, node.computedQtyTarget || node.qtyTarget || 1); - const current = Math.min(Math.max(0, node.qtyCurrent || 0), target); - if (target <= 1) { - return = 1} onChange={(event) => onChange(event.target.checked ? 1 : 0)} aria-label={textContent.itemCheckedLabel || "État"} />; - } - return ( - - - {formatGoalTreeNumber(current)} / {formatGoalTreeNumber(target)} - - - ); -} - -function GoalTreeNodeContentPreview({ node, editing, textContent, onAddContentItem, onUpdateItem }) { - const items = node.items || []; - return ( - - {items.map((item) => ( - - {node.kind === "checklist" && onUpdateItem(item.id, updater)} />} - - {item.label} - - - ))} - {editing && ( - - - - )} - - ); -} - -export function GoalTreeIconPicker({ label, value, textContent, onChange }) { - return ( -
- {label} - ( - - )} - > - {({ close }) => ( - <> - {GOAL_TREE_ICON_NAMES.map((icon) => ( - - ))} - - )} - -
- ); -} +// Rôle : conserve les exports publics historiques des sous-composants goal tree. +export { GoalTreeBranch } from "./GoalTreeBranch.jsx"; +export { + GoalTreeContentEditor, + GoalTreeFormulaInput, + GoalTreeIconPicker, + GoalTreePrerequisitePicker, + GoalTreeTitleInput +} from "./GoalTreeEditorFields.jsx"; +export { + GoalTreeNodeContentPreview, + GoalTreeStepLeafControl +} from "./GoalTreeNodeContent.jsx"; diff --git a/website/src/features/toolboxes/modules/GoalTreeView.jsx b/website/src/features/toolboxes/modules/GoalTreeView.jsx index 82919da..8e0e690 100644 --- a/website/src/features/toolboxes/modules/GoalTreeView.jsx +++ b/website/src/features/toolboxes/modules/GoalTreeView.jsx @@ -2,7 +2,7 @@ import { useLayoutEffect, useMemo, useRef, useState } from "react"; import { DiagramConnectors } from "../../../components/DiagramConnectors.jsx"; import { Icon } from "../../../components/Icon.jsx"; -import { GoalTreeBranch } from "./GoalTreeParts.jsx"; +import { GoalTreeBranch } from "./GoalTreeBranch.jsx"; import { getVisibleGoalTreeEdges } from "./goalTreeDisplay.js"; export function GoalTreeView({ diff --git a/website/src/features/toolboxes/modules/TaskPlannerBranch.jsx b/website/src/features/toolboxes/modules/TaskPlannerBranch.jsx new file mode 100644 index 0000000..4aa7927 --- /dev/null +++ b/website/src/features/toolboxes/modules/TaskPlannerBranch.jsx @@ -0,0 +1,199 @@ +// Rôle : rend les branches récursives, catégories et zones de drop du task planner. +import { Icon } from "../../../components/Icon.jsx"; +import { TaskPlannerItem } from "./TaskPlannerItem.jsx"; +import { + countCompletedTaskTree, + countTaskTree, + getTaskTypeLabel, + getTasksForParent, + getTaskCategoryGroups, + isTaskTreeComplete +} from "./taskPlannerUtils.js"; + +export function TaskPlannerBranch({ + parentId, + parentMap, + tasks, + data, + nowMs, + reorder, + textContent, + openDescriptions, + openTaskSettings, + prerequisiteNoticeTaskIds, + onToggleDescription, + onToggleSettings, + onPrerequisiteNotice, + onUpdateTask, + onDeleteTask, + onAddLinkTarget, + onRemoveLinkTarget, + onSetPrerequisite, + onSetTaskCategory, + onToggleCategoryCollapsed, + visited = new Set() +}) { + const groupedTasks = getTaskCategoryGroups(tasks, parentMap, parentId, data.categoryOrder); + const children = [...groupedTasks.uncategorized, ...groupedTasks.categories.flatMap((group) => group.tasks)].filter((task) => !visited.has(task.id)); + if (!children.length) return null; + const nextVisited = new Set([...visited, ...children.map((task) => task.id)]); + const showCategoryBoundaryDropZones = !parentId && reorder.shouldShowGroupBoundaries({ parentId }); + + function hasVisibleDescendants(taskId, seen = new Set()) { + if (seen.has(taskId)) return false; + seen.add(taskId); + return getTasksForParent(tasks, parentMap, taskId).some((child) => !child.checked || hasVisibleDescendants(child.id, seen)); + } + + function renderTask(task) { + const nestedChildren = getTasksForParent(tasks, parentMap, task.id).filter((child) => !nextVisited.has(child.id)); + const nestedBranch = nestedChildren.length ? ( + + ) : null; + if (data.hideCompleted && task.checked) { + if (!hasVisibleDescendants(task.id)) return null; + return ( +
  • +
    + onUpdateTask(task.id, (current) => ({ + ...current, + checked: event.target.checked, + checkedAt: event.target.checked ? Date.now() : 0 + }))} + aria-label={`${textContent.uncheckTitle || "Marquer non effectué"} ${task.title}`} + /> + {task.title} + {getTaskTypeLabel(task.type, textContent)} +
    +
      {nestedBranch}
    +
  • + ); + } + return ( + onToggleDescription(task.id)} + onToggleSettings={() => onToggleSettings(task.id)} + onPrerequisiteNotice={onPrerequisiteNotice} + onUpdateTask={onUpdateTask} + onDeleteTask={onDeleteTask} + onAddLinkTarget={onAddLinkTarget} + onRemoveLinkTarget={onRemoveLinkTarget} + onSetPrerequisite={onSetPrerequisite} + onSetTaskCategory={onSetTaskCategory} + > + {nestedBranch} + + ); + } + + function renderCategory(group) { + const groupTasks = group.tasks.filter((task) => !visited.has(task.id)); + if (!groupTasks.length) return null; + if (data.hideCompleted && isTaskTreeComplete(groupTasks.map((task) => task.id), tasks, parentMap)) return null; + const groupCount = countTaskTree(groupTasks.map((task) => task.id), tasks, parentMap); + const groupCompletedCount = countCompletedTaskTree(groupTasks.map((task) => task.id), tasks, parentMap); + const isCollapsed = data.collapsedCategories.includes(group.category); + const sectionClassName = [ + "task-planner-category-section", + groupCompletedCount === groupCount ? "is-complete" : "", + isCollapsed ? "is-collapsed" : "", + reorder.isGroupDragging(group.category, parentId) ? "is-dragging" : "", + reorder.isGroupDropTarget(group.category, parentId) ? "is-drop-target" : "", + reorder.getDropPlacement("group", group.category, parentId) === "after" ? "drop-after" : "" + ].filter(Boolean).join(" "); + const beforeDropZone = showCategoryBoundaryDropZones ? ( +
  • + ) : null; + const afterDropZone = showCategoryBoundaryDropZones ? ( +
  • + ) : null; + return [ + beforeDropZone, +
  • +
    +
    + +

    {group.category}

    +
    +
    + {groupCompletedCount} / {groupCount} + +
    +
    + {!isCollapsed && ( +
      + {groupTasks.map(renderTask).filter(Boolean)} +
    + )} +
  • , + afterDropZone + ]; + } + + return groupedTasks.entries.map((entry) => { + if (entry.type === "task") return visited.has(entry.task.id) ? null : renderTask(entry.task); + return renderCategory(entry.group); + }).filter(Boolean); +} diff --git a/website/src/features/toolboxes/modules/TaskPlannerItem.jsx b/website/src/features/toolboxes/modules/TaskPlannerItem.jsx new file mode 100644 index 0000000..313e647 --- /dev/null +++ b/website/src/features/toolboxes/modules/TaskPlannerItem.jsx @@ -0,0 +1,152 @@ +// Rôle : rend une ligne de tâche task planner et ses panneaux associés. +import { Icon } from "../../../components/Icon.jsx"; +import { useInlineEdit } from "../../../hooks/useInlineEdit.js"; +import { MissingPrerequisitesTooltip, TaskPlannerItemDetails } from "./TaskPlannerItemPanels.jsx"; +import { + TASK_TYPES, + getTaskCategoryDataset, + getTaskTypeLabel, + isTaskResetSoon +} from "./taskPlannerUtils.js"; + +export function TaskPlannerItem({ + task, + tasks, + data, + nowMs, + parentMap, + parentId, + reorder, + textContent, + descriptionOpen, + settingsOpen, + prerequisiteNoticeVisible, + onToggleDescription, + onToggleSettings, + onPrerequisiteNotice, + onUpdateTask, + onDeleteTask, + onAddLinkTarget, + onRemoveLinkTarget, + onSetPrerequisite, + onSetTaskCategory, + children +}) { + const relations = data.relations.filter((relation) => relation.fromTaskId === task.id); + const missingPrerequisites = relations.filter((relation) => relation.prerequisite && !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 = prerequisiteNoticeVisible && missingPrerequisites.length > 0 ? textContent.missingPrerequisiteNotice || "Pré requis parent non effectué" : ""; + const className = [ + "task-planner-item", + task.checked ? "is-complete" : "", + isTaskResetSoon(task, data, nowMs) ? "is-reset-soon" : "", + missingPrerequisites.length ? "has-missing-prerequisite" : "", + reorder.isItemDragging(task.id) ? "is-dragging" : "", + reorder.isItemDropTarget(task.id) ? "is-drop-target" : "", + reorder.getDropPlacement("item", task.id) === "after" ? "drop-after" : "" + ].filter(Boolean).join(" "); + const titleEdit = useInlineEdit({ + value: task.title, + transform: (draft) => String(draft || "").trim() || task.title, + onCommit: (title) => onUpdateTask(task.id, (current) => ({ ...current, title })) + }); + + function updateChecked(event) { + const checked = event.target.checked; + if (checked && missingPrerequisites.length > 0) onPrerequisiteNotice(task.id); + onUpdateTask(task.id, (current) => ({ + ...current, + checked, + checkedAt: checked ? Date.now() : 0 + })); + } + + return ( +
  • +
    + + + + + {missingPrerequisites.length > 0 && ( + + )} +
    + + + +
    +
    + + {children &&
      {children}
    } +
  • + ); +} diff --git a/website/src/features/toolboxes/modules/TaskPlannerItems.jsx b/website/src/features/toolboxes/modules/TaskPlannerItems.jsx index 791ef65..180d5c7 100644 --- a/website/src/features/toolboxes/modules/TaskPlannerItems.jsx +++ b/website/src/features/toolboxes/modules/TaskPlannerItems.jsx @@ -1,346 +1,3 @@ -// Rôle : rend les branches et lignes de tâches du task planner. -import { Icon } from "../../../components/Icon.jsx"; -import { useInlineEdit } from "../../../hooks/useInlineEdit.js"; -import { MissingPrerequisitesTooltip, TaskPlannerItemDetails } from "./TaskPlannerItemPanels.jsx"; -import { - TASK_TYPES, - countCompletedTaskTree, - countTaskTree, - getTaskCategoryDataset, - getTaskCategoryGroups, - getTaskTypeLabel, - getTasksForParent, - isTaskResetSoon, - isTaskTreeComplete -} from "./taskPlannerUtils.js"; - -export function TaskPlannerBranch({ - parentId, - parentMap, - tasks, - data, - nowMs, - reorder, - textContent, - openDescriptions, - openTaskSettings, - prerequisiteNoticeTaskIds, - onToggleDescription, - onToggleSettings, - onPrerequisiteNotice, - onUpdateTask, - onDeleteTask, - onAddLinkTarget, - onRemoveLinkTarget, - onSetPrerequisite, - onSetTaskCategory, - onToggleCategoryCollapsed, - visited = new Set() -}) { - const groupedTasks = getTaskCategoryGroups(tasks, parentMap, parentId, data.categoryOrder); - const children = [...groupedTasks.uncategorized, ...groupedTasks.categories.flatMap((group) => group.tasks)].filter((task) => !visited.has(task.id)); - if (!children.length) return null; - const nextVisited = new Set([...visited, ...children.map((task) => task.id)]); - const showCategoryBoundaryDropZones = !parentId && reorder.shouldShowGroupBoundaries({ parentId }); - - function hasVisibleDescendants(taskId, seen = new Set()) { - if (seen.has(taskId)) return false; - seen.add(taskId); - return getTasksForParent(tasks, parentMap, taskId).some((child) => !child.checked || hasVisibleDescendants(child.id, seen)); - } - - function renderTask(task) { - const nestedChildren = getTasksForParent(tasks, parentMap, task.id).filter((child) => !nextVisited.has(child.id)); - const nestedBranch = nestedChildren.length ? ( - - ) : null; - if (data.hideCompleted && task.checked) { - if (!hasVisibleDescendants(task.id)) return null; - return ( -
  • -
    - onUpdateTask(task.id, (current) => ({ - ...current, - checked: event.target.checked, - checkedAt: event.target.checked ? Date.now() : 0 - }))} - aria-label={`${textContent.uncheckTitle || "Marquer non effectué"} ${task.title}`} - /> - {task.title} - {getTaskTypeLabel(task.type, textContent)} -
    -
      {nestedBranch}
    -
  • - ); - } - return ( - onToggleDescription(task.id)} - onToggleSettings={() => onToggleSettings(task.id)} - onPrerequisiteNotice={onPrerequisiteNotice} - onUpdateTask={onUpdateTask} - onDeleteTask={onDeleteTask} - onAddLinkTarget={onAddLinkTarget} - onRemoveLinkTarget={onRemoveLinkTarget} - onSetPrerequisite={onSetPrerequisite} - onSetTaskCategory={onSetTaskCategory} - > - {nestedBranch} - - ); - } - - function renderCategory(group) { - const groupTasks = group.tasks.filter((task) => !visited.has(task.id)); - if (!groupTasks.length) return null; - if (data.hideCompleted && isTaskTreeComplete(groupTasks.map((task) => task.id), tasks, parentMap)) return null; - const groupCount = countTaskTree(groupTasks.map((task) => task.id), tasks, parentMap); - const groupCompletedCount = countCompletedTaskTree(groupTasks.map((task) => task.id), tasks, parentMap); - const isCollapsed = data.collapsedCategories.includes(group.category); - const sectionClassName = [ - "task-planner-category-section", - groupCompletedCount === groupCount ? "is-complete" : "", - isCollapsed ? "is-collapsed" : "", - reorder.isGroupDragging(group.category, parentId) ? "is-dragging" : "", - reorder.isGroupDropTarget(group.category, parentId) ? "is-drop-target" : "", - reorder.getDropPlacement("group", group.category, parentId) === "after" ? "drop-after" : "" - ].filter(Boolean).join(" "); - const beforeDropZone = showCategoryBoundaryDropZones ? ( -
  • - ) : null; - const afterDropZone = showCategoryBoundaryDropZones ? ( -
  • - ) : null; - return [ - beforeDropZone, -
  • -
    -
    - -

    {group.category}

    -
    -
    - {groupCompletedCount} / {groupCount} - -
    -
    - {!isCollapsed && ( -
      - {groupTasks.map(renderTask).filter(Boolean)} -
    - )} -
  • , - afterDropZone - ]; - } - - const entries = groupedTasks.entries.map((entry) => { - if (entry.type === "task") return visited.has(entry.task.id) ? null : renderTask(entry.task); - return renderCategory(entry.group); - }).filter(Boolean); - return entries; -} - -function TaskPlannerItem({ - task, - tasks, - data, - nowMs, - parentMap, - parentId, - reorder, - textContent, - descriptionOpen, - settingsOpen, - prerequisiteNoticeVisible, - onToggleDescription, - onToggleSettings, - onPrerequisiteNotice, - onUpdateTask, - onDeleteTask, - onAddLinkTarget, - onRemoveLinkTarget, - onSetPrerequisite, - onSetTaskCategory, - children -}) { - const relations = data.relations.filter((relation) => relation.fromTaskId === task.id); - const missingPrerequisites = relations.filter((relation) => relation.prerequisite && !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 = prerequisiteNoticeVisible && missingPrerequisites.length > 0 ? textContent.missingPrerequisiteNotice || "Pré requis parent non effectué" : ""; - const className = [ - "task-planner-item", - task.checked ? "is-complete" : "", - isTaskResetSoon(task, data, nowMs) ? "is-reset-soon" : "", - missingPrerequisites.length ? "has-missing-prerequisite" : "", - reorder.isItemDragging(task.id) ? "is-dragging" : "", - reorder.isItemDropTarget(task.id) ? "is-drop-target" : "", - reorder.getDropPlacement("item", task.id) === "after" ? "drop-after" : "" - ].filter(Boolean).join(" "); - const titleEdit = useInlineEdit({ - value: task.title, - transform: (draft) => String(draft || "").trim() || task.title, - onCommit: (title) => onUpdateTask(task.id, (current) => ({ ...current, title })) - }); - - function updateChecked(event) { - const checked = event.target.checked; - if (checked && missingPrerequisites.length > 0) onPrerequisiteNotice(task.id); - onUpdateTask(task.id, (current) => ({ - ...current, - checked, - checkedAt: checked ? Date.now() : 0 - })); - } - - return ( -
  • -
    - - - - - {missingPrerequisites.length > 0 && ( - - )} -
    - - - -
    -
    - - {children &&
      {children}
    } -
  • - ); -} +// Rôle : conserve les exports publics historiques des sous-composants task planner. +export { TaskPlannerBranch } from "./TaskPlannerBranch.jsx"; +export { TaskPlannerItem } from "./TaskPlannerItem.jsx"; diff --git a/website/src/features/toolboxes/modules/TaskPlannerModule.jsx b/website/src/features/toolboxes/modules/TaskPlannerModule.jsx index a9fb498..76a0467 100644 --- a/website/src/features/toolboxes/modules/TaskPlannerModule.jsx +++ b/website/src/features/toolboxes/modules/TaskPlannerModule.jsx @@ -1,7 +1,7 @@ // Rôle : fournit l'outil task planner avec tâches récurrentes, parents, pré requis et échange texte. import { useEffect, useMemo, useRef, useState } from "react"; import { useDraftForm } from "../../../hooks/useDraftForm.js"; -import { TaskPlannerBranch } from "./TaskPlannerItems.jsx"; +import { TaskPlannerBranch } from "./TaskPlannerBranch.jsx"; import { TaskPlannerSettings } from "./TaskPlannerSettings.jsx"; import { TaskPlannerTopbar } from "./TaskPlannerTopbar.jsx"; import { diff --git a/website/src/features/toolboxes/modules/TimerCountdown.jsx b/website/src/features/toolboxes/modules/TimerCountdown.jsx new file mode 100644 index 0000000..0c5bf23 --- /dev/null +++ b/website/src/features/toolboxes/modules/TimerCountdown.jsx @@ -0,0 +1,196 @@ +// Rôle : rend le formulaire et la liste des comptes à rebours timer. +import { useState } from "react"; +import { Icon } from "../../../components/Icon.jsx"; +import { formatTargetTime } from "./timerUtils.js"; +import { + AUTO_REFRESH_COUNTDOWN_TYPES, + TOGGLE_REFRESH_COUNTDOWN_TYPES, + canToggleAutoRefresh, + getAlertGuardMessage, + getCountdownMetaParts, + handleEditableKeyDown, + isAlertGuarded +} from "./timerOptions.js"; +import { EditableTimerLabel, TimerAlertModeSwitch, TimerValue, TimePartsInput } from "./TimerSharedControls.jsx"; + +export function CountdownControls({ textContent, form, error, onChange, onSubmit }) { + return ( +
    + + + {form.type === "duration" && ( + + )} + {form.type === "daily_time" && ( + + )} + {form.type === "time_pattern" && ( + + )} + {form.type === "interval" && ( +
    + + +
    + )} + {error && ( + + )} + +
    + ); +} + +export function CountdownList({ countdowns, nowMs, textContent, reorder, reorderEnabled, parentId, onRename, onAlertModeChange, onReset, onAutoRefreshToggle, onDelete }) { + const [editingId, setEditingId] = useState(""); + if (!countdowns.length) return

    {textContent.emptyCountdowns || "Aucun compte à rebours configuré."}

    ; + return ( +
      + {countdowns.map((countdown) => { + const remainingMs = countdown.targetMs - nowMs; + const expired = remainingMs <= 0; + const canReset = countdown.type === "duration"; + const canToggleRefresh = TOGGLE_REFRESH_COUNTDOWN_TYPES.has(countdown.type); + const refreshBlocked = canToggleRefresh && !canToggleAutoRefresh(countdown); + const refreshTitle = refreshBlocked + ? getAlertGuardMessage(textContent) + : textContent.autoRefreshTitle || "Répéter automatiquement"; + const className = [ + "timer-list-item", + expired ? "is-expired" : "", + editingId === countdown.id ? "is-editing" : "", + reorderEnabled && reorder.isItemDragging(countdown.id) ? "is-dragging" : "", + reorderEnabled && reorder.isItemDropTarget(countdown.id) ? "is-drop-target" : "", + reorderEnabled && reorder.getDropPlacement("item", countdown.id) === "after" ? "drop-after" : "" + ].filter(Boolean).join(" "); + const reorderProps = reorderEnabled ? reorder.getItemProps({ itemId: countdown.id, parentId }) : {}; + + return ( +
    • +
      + {reorderEnabled && ( + + )} + {editingId === countdown.id ? ( + onRename(countdown.id, label)} + onDone={() => setEditingId("")} + > + + + ) : ( + setEditingId(countdown.id)} + onKeyDown={(event) => handleEditableKeyDown(event, () => setEditingId(countdown.id))} + > + + + {countdown.label} + + {canReset && ( + + )} + {canToggleRefresh && ( +