Improve toolbox reorder and checklist editing UX
All checks were successful
Deploy Sokko G / deploy (push) Successful in 8s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 8s
This commit is contained in:
parent
1e4ac4259b
commit
8eb622e05d
11 changed files with 695 additions and 140 deletions
|
|
@ -132,6 +132,12 @@ export function validateSiteContent(site) {
|
||||||
"toolboxes.modules.checklist.decrementLabel",
|
"toolboxes.modules.checklist.decrementLabel",
|
||||||
"toolboxes.modules.checklist.incrementLabel",
|
"toolboxes.modules.checklist.incrementLabel",
|
||||||
"toolboxes.modules.checklist.currentQuantityLabel",
|
"toolboxes.modules.checklist.currentQuantityLabel",
|
||||||
|
"toolboxes.modules.checklist.reorderTitle",
|
||||||
|
"toolboxes.modules.checklist.reorderSectionTitle",
|
||||||
|
"toolboxes.modules.checklist.sectionEditTitle",
|
||||||
|
"toolboxes.modules.checklist.itemEditLabel",
|
||||||
|
"toolboxes.modules.checklist.categoryEditLabel",
|
||||||
|
"toolboxes.modules.checklist.editTitle",
|
||||||
"toolboxes.modules.checklist.deleteTitle",
|
"toolboxes.modules.checklist.deleteTitle",
|
||||||
"toolboxes.modules.images.addImages",
|
"toolboxes.modules.images.addImages",
|
||||||
"toolboxes.modules.images.pastePlaceholder",
|
"toolboxes.modules.images.pastePlaceholder",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ test("toolbox storage, cards and pages are wired", async () => {
|
||||||
const reorderHook = await readFile("website/src/hooks/usePointerReorder.js", "utf8");
|
const reorderHook = await readFile("website/src/hooks/usePointerReorder.js", "utf8");
|
||||||
const groupedReorderHook = await readFile("website/src/hooks/useGroupedReorder.js", "utf8");
|
const groupedReorderHook = await readFile("website/src/hooks/useGroupedReorder.js", "utf8");
|
||||||
const inlineEditHook = await readFile("website/src/hooks/useInlineEdit.js", "utf8");
|
const inlineEditHook = await readFile("website/src/hooks/useInlineEdit.js", "utf8");
|
||||||
|
const draftFormHook = await readFile("website/src/hooks/useDraftForm.js", "utf8");
|
||||||
|
|
||||||
assert.match(`${indexedToolboxesHook}\n${toolboxPages}`, /indexedDbStorage\.js/);
|
assert.match(`${indexedToolboxesHook}\n${toolboxPages}`, /indexedDbStorage\.js/);
|
||||||
assert.match(source, /features\/toolboxes\/storage\/useIndexedToolboxes\.js/);
|
assert.match(source, /features\/toolboxes\/storage\/useIndexedToolboxes\.js/);
|
||||||
|
|
@ -90,6 +91,10 @@ test("toolbox storage, cards and pages are wired", async () => {
|
||||||
assert.match(inlineEditHook, /blurOnEscape/);
|
assert.match(inlineEditHook, /blurOnEscape/);
|
||||||
assert.match(inlineEditHook, /commitOnEnter/);
|
assert.match(inlineEditHook, /commitOnEnter/);
|
||||||
assert.match(inlineEditHook, /getContentEditableProps/);
|
assert.match(inlineEditHook, /getContentEditableProps/);
|
||||||
|
assert.match(draftFormHook, /export function useDraftForm/);
|
||||||
|
assert.match(draftFormHook, /getFieldProps/);
|
||||||
|
assert.match(draftFormHook, /handleSubmit/);
|
||||||
|
assert.match(draftFormHook, /preventDefault/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("toolbox module registry and modules expose expected behavior", async () => {
|
test("toolbox module registry and modules expose expected behavior", async () => {
|
||||||
|
|
@ -156,6 +161,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(combosModule, /useInlineEdit/);
|
assert.match(combosModule, /useInlineEdit/);
|
||||||
assert.match(combosModule, /useGroupedReorder/);
|
assert.match(combosModule, /useGroupedReorder/);
|
||||||
assert.match(combosModule, /reorderFeatures/);
|
assert.match(combosModule, /reorderFeatures/);
|
||||||
|
assert.match(combosModule, /getGroupBoundaryProps/);
|
||||||
|
assert.match(combosModule, /combos-category-boundary-drop-zone/);
|
||||||
assert.doesNotMatch(combosModule, /canMoveItem:/);
|
assert.doesNotMatch(combosModule, /canMoveItem:/);
|
||||||
assert.doesNotMatch(combosModule, /canMoveGroup:/);
|
assert.doesNotMatch(combosModule, /canMoveGroup:/);
|
||||||
assert.match(calculatorModule, /export function CalculatorModule/);
|
assert.match(calculatorModule, /export function CalculatorModule/);
|
||||||
|
|
@ -208,6 +215,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(timerModule, /stopwatch/);
|
assert.match(timerModule, /stopwatch/);
|
||||||
assert.match(timerModule, /countdown/);
|
assert.match(timerModule, /countdown/);
|
||||||
assert.match(timerModule, /useInlineEdit/);
|
assert.match(timerModule, /useInlineEdit/);
|
||||||
|
assert.match(timerModule, /useDraftForm/);
|
||||||
assert.match(timerModule, /normalizeTimerData/);
|
assert.match(timerModule, /normalizeTimerData/);
|
||||||
assert.match(taskPlannerModule, /export function TaskPlannerModule/);
|
assert.match(taskPlannerModule, /export function TaskPlannerModule/);
|
||||||
assert.match(taskPlannerModule, /normalizeTaskPlannerData/);
|
assert.match(taskPlannerModule, /normalizeTaskPlannerData/);
|
||||||
|
|
@ -219,6 +227,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(taskPlannerModule, /task-planner-warning/);
|
assert.match(taskPlannerModule, /task-planner-warning/);
|
||||||
assert.match(taskPlannerModule, /useGroupedReorder/);
|
assert.match(taskPlannerModule, /useGroupedReorder/);
|
||||||
assert.match(taskPlannerModule, /useInlineEdit/);
|
assert.match(taskPlannerModule, /useInlineEdit/);
|
||||||
|
assert.match(taskPlannerModule, /useDraftForm/);
|
||||||
assert.match(taskPlannerModule, /commitOnEnter: false/);
|
assert.match(taskPlannerModule, /commitOnEnter: false/);
|
||||||
assert.match(taskPlannerModule, /reorderFeatures/);
|
assert.match(taskPlannerModule, /reorderFeatures/);
|
||||||
assert.doesNotMatch(taskPlannerModule, /canMoveItem:/);
|
assert.doesNotMatch(taskPlannerModule, /canMoveItem:/);
|
||||||
|
|
@ -237,7 +246,15 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(checklistModule, /Number\.parseInt/);
|
assert.match(checklistModule, /Number\.parseInt/);
|
||||||
assert.match(checklistModule, /checklist-qty-current/);
|
assert.match(checklistModule, /checklist-qty-current/);
|
||||||
assert.match(checklistModule, /checklist-delete-button danger/);
|
assert.match(checklistModule, /checklist-delete-button danger/);
|
||||||
assert.doesNotMatch(checklistModule, /useGroupedReorder/);
|
assert.match(checklistModule, /useGroupedReorder/);
|
||||||
|
assert.match(checklistModule, /getItemProps/);
|
||||||
|
assert.match(checklistModule, /getGroupProps/);
|
||||||
|
assert.match(checklistModule, /getGroupBoundaryProps/);
|
||||||
|
assert.match(checklistModule, /checklist-category-boundary-drop-zone/);
|
||||||
|
assert.match(checklistModule, /itemReorder\.startDrag/);
|
||||||
|
assert.match(checklistModule, /groupReorder\.startDrag/);
|
||||||
|
assert.match(checklistModule, /useInlineEdit/);
|
||||||
|
assert.match(checklistModule, /moveChecklistItemToCategory/);
|
||||||
assert.match(textImportModal, /createPortal/);
|
assert.match(textImportModal, /createPortal/);
|
||||||
assert.match(textImportModal, /lockBodyScroll/);
|
assert.match(textImportModal, /lockBodyScroll/);
|
||||||
assert.match(imagesModule, /export function ImagesModule/);
|
assert.match(imagesModule, /export function ImagesModule/);
|
||||||
|
|
@ -264,11 +281,15 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(linksModule, /parseColonImportLines/);
|
assert.match(linksModule, /parseColonImportLines/);
|
||||||
assert.match(linksModule, /context\.normalizeUrl/);
|
assert.match(linksModule, /context\.normalizeUrl/);
|
||||||
assert.match(linksModule, /useGroupedReorder/);
|
assert.match(linksModule, /useGroupedReorder/);
|
||||||
|
assert.match(linksModule, /useDraftForm/);
|
||||||
|
assert.match(linksModule, /linkDraft\.getFieldProps/);
|
||||||
assert.match(linksModule, /link-drag-handle/);
|
assert.match(linksModule, /link-drag-handle/);
|
||||||
assert.match(countersModule, /export function CountersModule/);
|
assert.match(countersModule, /export function CountersModule/);
|
||||||
assert.match(countersModule, /editing &&/);
|
assert.match(countersModule, /editing &&/);
|
||||||
assert.match(countersModule, /counter-actions/);
|
assert.match(countersModule, /counter-actions/);
|
||||||
assert.match(countersModule, /useGroupedReorder/);
|
assert.match(countersModule, /useGroupedReorder/);
|
||||||
|
assert.match(countersModule, /useDraftForm/);
|
||||||
|
assert.match(countersModule, /counterDraft\.getFieldProps/);
|
||||||
assert.match(countersModule, /orientation: "horizontal"/);
|
assert.match(countersModule, /orientation: "horizontal"/);
|
||||||
assert.match(countersModule, /counter-drag-handle/);
|
assert.match(countersModule, /counter-drag-handle/);
|
||||||
assert.match(timerModule, /useGroupedReorder/);
|
assert.match(timerModule, /useGroupedReorder/);
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,12 @@
|
||||||
"decrementLabel": "Retirer une quantité",
|
"decrementLabel": "Retirer une quantité",
|
||||||
"incrementLabel": "Ajouter une quantité",
|
"incrementLabel": "Ajouter une quantité",
|
||||||
"currentQuantityLabel": "Quantité actuelle",
|
"currentQuantityLabel": "Quantité actuelle",
|
||||||
|
"reorderTitle": "Déplacer",
|
||||||
|
"reorderSectionTitle": "Déplacer la catégorie",
|
||||||
|
"sectionEditTitle": "Renommer la catégorie",
|
||||||
|
"itemEditLabel": "Libellé",
|
||||||
|
"categoryEditLabel": "Catégorie",
|
||||||
|
"editTitle": "Modifier",
|
||||||
"deleteTitle": "Supprimer"
|
"deleteTitle": "Supprimer"
|
||||||
},
|
},
|
||||||
"images": {
|
"images": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// Rôle : fournit l'outil checklist avec quantités, catégories et imports texte.
|
// Rôle : fournit l'outil checklist avec quantités, catégories et imports texte.
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
|
import { useGroupedReorder, moveItem } from "../../../hooks/useGroupedReorder.js";
|
||||||
|
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
||||||
import { TextImportModal } from "./TextImportModal.jsx";
|
import { TextImportModal } from "./TextImportModal.jsx";
|
||||||
import { parseColonImportLines } from "./textImport.js";
|
import { parseColonImportLines } from "./textImport.js";
|
||||||
|
|
||||||
|
|
@ -15,6 +17,49 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
||||||
const closeImportModal = useCallback(() => setImportOpen(false), []);
|
const closeImportModal = useCallback(() => setImportOpen(false), []);
|
||||||
const namedSections = data.sections.filter((section) => section.title);
|
const namedSections = data.sections.filter((section) => section.title);
|
||||||
const completedSectionsMode = data.hideCompletedSectionsFully ? "hidden" : data.hideCompletedSections ? "reduced" : "visible";
|
const completedSectionsMode = data.hideCompletedSectionsFully ? "hidden" : data.hideCompletedSections ? "reduced" : "visible";
|
||||||
|
const reorderItems = getChecklistReorderItems(data.sections);
|
||||||
|
const {
|
||||||
|
itemReorder,
|
||||||
|
groupReorder,
|
||||||
|
getItemProps,
|
||||||
|
getGroupProps,
|
||||||
|
getGroupBoundaryProps,
|
||||||
|
isItemDragging,
|
||||||
|
isItemDropTarget,
|
||||||
|
isGroupDragging,
|
||||||
|
isGroupDropTarget,
|
||||||
|
isBoundaryDropTarget,
|
||||||
|
getDropPlacement,
|
||||||
|
shouldShowGroupBoundaries
|
||||||
|
} = useGroupedReorder({
|
||||||
|
namespace: `checklist-${moduleId}`,
|
||||||
|
items: reorderItems,
|
||||||
|
getItemId: (item) => item.id,
|
||||||
|
getItemGroup: (item) => item.sectionId,
|
||||||
|
getEffectiveGroup: (item) => item.sectionId,
|
||||||
|
reorderFeatures: {
|
||||||
|
item: { groupChange: true, boundaryDrop: true },
|
||||||
|
group: { reorder: true, boundaryDrop: true }
|
||||||
|
},
|
||||||
|
onItemMove: (operation) => save(moveChecklistItem(data.sections, operation, context)),
|
||||||
|
onGroupMove: (operation) => save(moveItem(data.sections, operation.sourceId, operation.targetId, operation.placement))
|
||||||
|
});
|
||||||
|
const reorder = {
|
||||||
|
itemReorder,
|
||||||
|
groupReorder,
|
||||||
|
getItemProps,
|
||||||
|
getGroupProps,
|
||||||
|
getGroupBoundaryProps,
|
||||||
|
isItemDragging,
|
||||||
|
isItemDropTarget,
|
||||||
|
isGroupDragging,
|
||||||
|
isGroupDropTarget,
|
||||||
|
isBoundaryDropTarget,
|
||||||
|
getDropPlacement,
|
||||||
|
shouldShowGroupBoundaries
|
||||||
|
};
|
||||||
|
const showBoundaryDropZones = reorder.shouldShowGroupBoundaries();
|
||||||
|
const visibleSections = data.sections.filter((section) => !(section.title && data.hideCompletedSectionsFully && isSectionComplete(section, context)));
|
||||||
|
|
||||||
function save(sections, settings = {}) {
|
function save(sections, settings = {}) {
|
||||||
context.setModuleData(toolboxId, moduleId, { ...data, ...settings, sections });
|
context.setModuleData(toolboxId, moduleId, { ...data, ...settings, sections });
|
||||||
|
|
@ -57,6 +102,26 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateSection(sectionId, updater) {
|
||||||
|
save(data.sections.map((section) => section.id === sectionId ? updater(section) : section).filter(keepChecklistSection));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameSection(sectionId, title) {
|
||||||
|
save(renameChecklistSection(data.sections, sectionId, title));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateItem(itemId, updater) {
|
||||||
|
save(updateChecklistItem(data.sections, itemId, updater));
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveItemToCategory(itemId, title) {
|
||||||
|
save(moveChecklistItemToCategory(data.sections, itemId, title, context));
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteItem(itemId) {
|
||||||
|
save(data.sections.map((section) => ({ ...section, items: section.items.filter((item) => item.id !== itemId) })).filter(keepChecklistSection));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{editing && (
|
{editing && (
|
||||||
|
|
@ -109,9 +174,38 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ul className="checklist">
|
<ul className="checklist">
|
||||||
{data.sections.map((section) => (
|
{visibleSections.map((section, index) => {
|
||||||
<ChecklistSection key={section.id} section={section} sections={data.sections} data={data} context={context} save={save} />
|
const isLastSection = index === visibleSections.length - 1;
|
||||||
))}
|
return [
|
||||||
|
showBoundaryDropZones ? (
|
||||||
|
<li
|
||||||
|
className={`checklist-category-boundary-drop-zone ${reorder.isBoundaryDropTarget({ groupId: section.id, placement: "before" }) ? "is-drop-target" : ""}`}
|
||||||
|
key={`${section.id}:before-drop`}
|
||||||
|
{...reorder.getGroupBoundaryProps({ groupId: section.id, placement: "before" })}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
<ChecklistSection
|
||||||
|
key={section.id}
|
||||||
|
section={section}
|
||||||
|
data={data}
|
||||||
|
context={context}
|
||||||
|
reorder={reorder}
|
||||||
|
onRenameSection={renameSection}
|
||||||
|
onUpdateSection={updateSection}
|
||||||
|
onSaveSections={save}
|
||||||
|
onUpdateItem={updateItem}
|
||||||
|
onMoveItemToCategory={moveItemToCategory}
|
||||||
|
onDeleteItem={deleteItem}
|
||||||
|
/>,
|
||||||
|
showBoundaryDropZones && isLastSection ? (
|
||||||
|
<li
|
||||||
|
className={`checklist-category-boundary-drop-zone ${reorder.isBoundaryDropTarget({ groupId: section.id, placement: "after" }) ? "is-drop-target" : ""}`}
|
||||||
|
key={`${section.id}:after-drop`}
|
||||||
|
{...reorder.getGroupBoundaryProps({ groupId: section.id, placement: "after" })}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
];
|
||||||
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
@ -151,30 +245,142 @@ function getSectionHideWhenComplete(section, globalHide) {
|
||||||
return typeof section.hideWhenComplete === "boolean" ? section.hideWhenComplete : globalHide;
|
return typeof section.hideWhenComplete === "boolean" ? section.hideWhenComplete : globalHide;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CategoryTitle({ title }) {
|
function keepChecklistSection(section) {
|
||||||
return String(title || "").split(/([♂♀])/g).map((part, index) => (
|
return section.items.length || section.title;
|
||||||
part === "♂" || part === "♀"
|
|
||||||
? <span className="checklist-gender-symbol" key={index}>{part}</span>
|
|
||||||
: part
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChecklistSection({ section, context, sections, data, save }) {
|
function getChecklistReorderItems(sections) {
|
||||||
|
return sections.flatMap((section) => section.items.map((item) => ({ ...item, sectionId: section.id })));
|
||||||
|
}
|
||||||
|
|
||||||
|
function findChecklistSectionByItemId(sections, itemId) {
|
||||||
|
return sections.find((section) => section.items.some((item) => item.id === itemId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameChecklistSection(sections, sectionId, title) {
|
||||||
|
const cleanTitle = title.trim();
|
||||||
|
const section = sections.find((entry) => entry.id === sectionId);
|
||||||
|
if (!section) return sections;
|
||||||
|
const existingSection = sections.find((entry) => entry.id !== sectionId && entry.title.toLowerCase() === cleanTitle.toLowerCase());
|
||||||
|
if (!existingSection) {
|
||||||
|
return sections.map((entry) => entry.id === sectionId ? { ...entry, title: cleanTitle } : entry).filter(keepChecklistSection);
|
||||||
|
}
|
||||||
|
return sections.map((entry) => {
|
||||||
|
if (entry.id === existingSection.id) return { ...entry, items: [...entry.items, ...section.items] };
|
||||||
|
return entry;
|
||||||
|
}).filter((entry) => entry.id !== sectionId).filter(keepChecklistSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateChecklistItem(sections, itemId, updater) {
|
||||||
|
return sections.map((section) => ({
|
||||||
|
...section,
|
||||||
|
items: section.items.map((item) => item.id === itemId ? updater(item) : item).filter((item) => item.label)
|
||||||
|
})).filter(keepChecklistSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveChecklistItemToCategory(sections, itemId, title, context) {
|
||||||
|
const cleanTitle = title.trim();
|
||||||
|
const sourceSection = findChecklistSectionByItemId(sections, itemId);
|
||||||
|
const item = sourceSection?.items.find((entry) => entry.id === itemId);
|
||||||
|
if (!sourceSection || !item || sourceSection.title.toLowerCase() === cleanTitle.toLowerCase()) return sections;
|
||||||
|
const targetSection = sections.find((section) => section.title.toLowerCase() === cleanTitle.toLowerCase());
|
||||||
|
const sectionsWithoutItem = sections.map((section) => ({
|
||||||
|
...section,
|
||||||
|
items: section.items.filter((entry) => entry.id !== itemId)
|
||||||
|
})).filter(keepChecklistSection);
|
||||||
|
if (targetSection) {
|
||||||
|
return sectionsWithoutItem.map((section) => section.id === targetSection.id ? { ...section, items: [...section.items, item] } : section);
|
||||||
|
}
|
||||||
|
return [...sectionsWithoutItem, { id: context.uid("section"), title: cleanTitle, items: [item] }];
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveChecklistItem(sections, operation, context) {
|
||||||
|
const sourceSection = findChecklistSectionByItemId(sections, operation.sourceId);
|
||||||
|
const sourceItem = sourceSection?.items.find((item) => item.id === operation.sourceId);
|
||||||
|
if (!sourceSection || !sourceItem) return sections;
|
||||||
|
if (operation.targetType === "boundary") return moveChecklistItemToUngroupedBoundary(sections, sourceSection, sourceItem, operation, context);
|
||||||
|
const targetSection = operation.targetType === "item"
|
||||||
|
? findChecklistSectionByItemId(sections, operation.targetId)
|
||||||
|
: sections.find((section) => section.id === operation.targetGroup);
|
||||||
|
if (!targetSection) return sections;
|
||||||
|
const sectionsWithoutItem = sections.map((section) => ({
|
||||||
|
...section,
|
||||||
|
items: section.items.filter((item) => item.id !== operation.sourceId)
|
||||||
|
})).filter(keepChecklistSection);
|
||||||
|
return sectionsWithoutItem.map((section) => {
|
||||||
|
if (section.id !== targetSection.id) return section;
|
||||||
|
if (operation.targetType === "item") {
|
||||||
|
const targetIndex = section.items.findIndex((item) => item.id === operation.targetId);
|
||||||
|
if (targetIndex < 0) return { ...section, items: [...section.items, sourceItem] };
|
||||||
|
const nextItems = [...section.items];
|
||||||
|
nextItems.splice(operation.placement === "after" ? targetIndex + 1 : targetIndex, 0, sourceItem);
|
||||||
|
return { ...section, items: nextItems };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...section,
|
||||||
|
items: operation.placement === "after" ? [...section.items, sourceItem] : [sourceItem, ...section.items]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveChecklistItemToUngroupedBoundary(sections, sourceSection, sourceItem, operation, context) {
|
||||||
|
const targetIndex = sections.findIndex((section) => section.id === operation.targetGroup);
|
||||||
|
if (targetIndex < 0) return sections;
|
||||||
|
const preferredUngroupedSection = sourceSection.title ? sections.find((section) => !section.title && section.id !== sourceSection.id) : sourceSection;
|
||||||
|
const ungroupedSectionId = preferredUngroupedSection?.id || context.uid("section");
|
||||||
|
const baseSections = sections
|
||||||
|
.map((section) => ({
|
||||||
|
...section,
|
||||||
|
items: section.items.filter((item) => item.id !== sourceItem.id)
|
||||||
|
}))
|
||||||
|
.filter((section) => section.id !== ungroupedSectionId)
|
||||||
|
.filter(keepChecklistSection);
|
||||||
|
const targetSection = sections[targetIndex];
|
||||||
|
const nextTargetIndex = baseSections.findIndex((section) => section.id === targetSection.id);
|
||||||
|
const insertIndex = nextTargetIndex < 0
|
||||||
|
? Math.min(targetIndex, baseSections.length)
|
||||||
|
: nextTargetIndex + (operation.placement === "after" ? 1 : 0);
|
||||||
|
const ungroupedSection = {
|
||||||
|
id: ungroupedSectionId,
|
||||||
|
title: "",
|
||||||
|
items: [...(preferredUngroupedSection?.items || []).filter((item) => item.id !== sourceItem.id), sourceItem]
|
||||||
|
};
|
||||||
|
const nextSections = [...baseSections];
|
||||||
|
nextSections.splice(insertIndex, 0, ungroupedSection);
|
||||||
|
return nextSections;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChecklistSection({ section, context, data, reorder, onRenameSection, onUpdateSection, onSaveSections, onUpdateItem, onMoveItemToCategory, onDeleteItem }) {
|
||||||
const showTitle = Boolean(section.title);
|
const showTitle = Boolean(section.title);
|
||||||
const sectionComplete = showTitle && isSectionComplete(section, context);
|
const sectionComplete = showTitle && isSectionComplete(section, context);
|
||||||
const hideWhenComplete = getSectionHideWhenComplete(section, data.hideCompletedSections);
|
const hideWhenComplete = getSectionHideWhenComplete(section, data.hideCompletedSections);
|
||||||
const isAutoCollapsed = sectionComplete && hideWhenComplete;
|
const isAutoCollapsed = sectionComplete && hideWhenComplete;
|
||||||
const isCollapsed = Boolean(section.collapsed) || isAutoCollapsed;
|
const isCollapsed = Boolean(section.collapsed) || isAutoCollapsed;
|
||||||
|
const textContent = context.moduleText?.checklist || {};
|
||||||
|
const className = [
|
||||||
|
"checklist-section",
|
||||||
|
showTitle ? "is-grouped" : "",
|
||||||
|
sectionComplete ? "is-complete" : "",
|
||||||
|
isCollapsed ? "is-collapsed" : "",
|
||||||
|
reorder.isGroupDragging(section.id) ? "is-dragging" : "",
|
||||||
|
reorder.isGroupDropTarget(section.id) ? "is-drop-target" : "",
|
||||||
|
reorder.getDropPlacement("group", section.id) === "after" ? "drop-after" : ""
|
||||||
|
].filter(Boolean).join(" ");
|
||||||
|
const titleEdit = useInlineEdit({
|
||||||
|
value: section.title,
|
||||||
|
onCommit: (title) => onRenameSection(section.id, title)
|
||||||
|
});
|
||||||
|
const [editingItemId, setEditingItemId] = useState("");
|
||||||
|
|
||||||
if (sectionComplete && data.hideCompletedSectionsFully) return null;
|
if (sectionComplete && data.hideCompletedSectionsFully) return null;
|
||||||
|
|
||||||
function saveItems(items) {
|
function saveItems(items) {
|
||||||
save(sections.map((entry) => entry.id === section.id ? { ...entry, items } : entry).filter((entry) => entry.items.length || entry.title));
|
onUpdateSection(section.id, (entry) => ({ ...entry, items }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSectionCollapsed() {
|
function toggleSectionCollapsed() {
|
||||||
const nextCollapsed = !isCollapsed;
|
const nextCollapsed = !isCollapsed;
|
||||||
const nextSections = sections.map((entry) => {
|
const nextSections = data.sections.map((entry) => {
|
||||||
if (entry.id === section.id) {
|
if (entry.id === section.id) {
|
||||||
const nextSection = { ...entry };
|
const nextSection = { ...entry };
|
||||||
if (nextCollapsed) {
|
if (nextCollapsed) {
|
||||||
|
|
@ -195,23 +401,40 @@ function ChecklistSection({ section, context, sections, data, save }) {
|
||||||
});
|
});
|
||||||
const nextCompletedSections = nextSections.filter((entry) => entry.title && isSectionComplete(entry, context));
|
const nextCompletedSections = nextSections.filter((entry) => entry.title && isSectionComplete(entry, context));
|
||||||
const allHidden = nextCompletedSections.length > 0 && nextCompletedSections.every((entry) => getSectionHideWhenComplete(entry, false));
|
const allHidden = nextCompletedSections.length > 0 && nextCompletedSections.every((entry) => getSectionHideWhenComplete(entry, false));
|
||||||
save(nextSections, { hideCompletedSections: allHidden });
|
onSaveSections(nextSections, { hideCompletedSections: allHidden });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={`checklist-section ${showTitle ? "is-grouped" : ""} ${sectionComplete ? "is-complete" : ""} ${isCollapsed ? "is-collapsed" : ""}`}>
|
<li className={className} {...reorder.getGroupProps({ groupId: section.id })}>
|
||||||
{showTitle && (
|
{showTitle && (
|
||||||
<div className="checklist-section-header">
|
<div className="checklist-section-header">
|
||||||
<h3><CategoryTitle title={section.title || "Sans catégorie"} /></h3>
|
<button
|
||||||
<div>
|
className="checklist-drag-handle"
|
||||||
|
type="button"
|
||||||
|
onPointerDown={(event) => reorder.groupReorder.startDrag(event, { groupId: section.id })}
|
||||||
|
aria-label={`${textContent.reorderSectionTitle || "Déplacer la catégorie"} ${section.title}`}
|
||||||
|
title={textContent.reorderTitle || "Déplacer"}
|
||||||
|
>
|
||||||
|
<Icon name="drag" />
|
||||||
|
</button>
|
||||||
|
<h3>
|
||||||
|
<input
|
||||||
|
{...titleEdit.getInputProps({
|
||||||
|
className: "checklist-section-title-input",
|
||||||
|
title: textContent.sectionEditTitle || "Renommer la catégorie",
|
||||||
|
"aria-label": textContent.sectionEditTitle || "Renommer la catégorie"
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
<div className="checklist-section-actions">
|
||||||
<span>{section.items.filter((item) => context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget).length} / {section.items.length}</span>
|
<span>{section.items.filter((item) => context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget).length} / {section.items.length}</span>
|
||||||
<button
|
<button
|
||||||
className="checklist-section-collapse-button"
|
className="checklist-section-collapse-button"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={toggleSectionCollapsed}
|
onClick={toggleSectionCollapsed}
|
||||||
aria-expanded={!isCollapsed}
|
aria-expanded={!isCollapsed}
|
||||||
title={isCollapsed ? context.moduleText?.checklist?.showCompletedSectionTitle || "Afficher cette catégorie" : context.moduleText?.checklist?.hideCompletedSectionTitle || "Réduire cette catégorie"}
|
title={isCollapsed ? textContent.showCompletedSectionTitle || "Afficher cette catégorie" : textContent.hideCompletedSectionTitle || "Réduire cette catégorie"}
|
||||||
aria-label={isCollapsed ? context.moduleText?.checklist?.showCompletedSectionTitle || "Afficher cette catégorie" : context.moduleText?.checklist?.hideCompletedSectionTitle || "Réduire cette catégorie"}
|
aria-label={isCollapsed ? textContent.showCompletedSectionTitle || "Afficher cette catégorie" : textContent.hideCompletedSectionTitle || "Réduire cette catégorie"}
|
||||||
>
|
>
|
||||||
<Icon name={isCollapsed ? "chevron-down" : "chevron-up"} />
|
<Icon name={isCollapsed ? "chevron-down" : "chevron-up"} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -221,7 +444,20 @@ function ChecklistSection({ section, context, sections, data, save }) {
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<ul className="checklist-section-items">
|
<ul className="checklist-section-items">
|
||||||
{section.items.map((item) => (
|
{section.items.map((item) => (
|
||||||
<ChecklistItem key={item.id} item={item} context={context} items={section.items} save={saveItems} />
|
<ChecklistItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
section={section}
|
||||||
|
context={context}
|
||||||
|
reorder={reorder}
|
||||||
|
items={section.items}
|
||||||
|
save={saveItems}
|
||||||
|
onUpdateItem={onUpdateItem}
|
||||||
|
onMoveItemToCategory={onMoveItemToCategory}
|
||||||
|
onDeleteItem={onDeleteItem}
|
||||||
|
editing={editingItemId === item.id}
|
||||||
|
onToggleEditing={() => setEditingItemId(editingItemId === item.id ? "" : item.id)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
|
|
@ -229,16 +465,51 @@ function ChecklistSection({ section, context, sections, data, save }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChecklistItem({ item, context, items, save }) {
|
function ChecklistItem({ item, section, context, reorder, items, save, onUpdateItem, onMoveItemToCategory, onDeleteItem, editing, onToggleEditing }) {
|
||||||
const done = context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget;
|
const done = context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget;
|
||||||
|
const textContent = context.moduleText?.checklist || {};
|
||||||
|
const className = [
|
||||||
|
"checklist-item",
|
||||||
|
done ? "is-complete" : "",
|
||||||
|
reorder.isItemDragging(item.id) ? "is-dragging" : "",
|
||||||
|
reorder.isItemDropTarget(item.id) ? "is-drop-target" : "",
|
||||||
|
reorder.getDropPlacement("item", item.id) === "after" ? "drop-after" : ""
|
||||||
|
].filter(Boolean).join(" ");
|
||||||
|
const labelEdit = useInlineEdit({
|
||||||
|
value: item.label,
|
||||||
|
transform: (draft) => String(draft || "").trim() || item.label,
|
||||||
|
onCommit: (label) => onUpdateItem(item.id, (entry) => ({ ...entry, label })),
|
||||||
|
autoFocus: editing,
|
||||||
|
focusKey: item.id
|
||||||
|
});
|
||||||
|
const categoryEdit = useInlineEdit({
|
||||||
|
value: section.title,
|
||||||
|
onCommit: (title) => onMoveItemToCategory(item.id, title)
|
||||||
|
});
|
||||||
|
|
||||||
function updateItem(updater) {
|
function updateItem(updater) {
|
||||||
save(items.map((entry) => entry.id === item.id ? updater(entry) : entry));
|
save(items.map((entry) => entry.id === item.id ? updater(entry) : entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleChecked() {
|
||||||
|
updateItem((entry) => ({
|
||||||
|
...entry,
|
||||||
|
qtyCurrent: done ? 0 : entry.qtyTarget
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={`checklist-item ${done ? "is-complete" : ""}`}>
|
<li className={className} {...reorder.getItemProps({ itemId: item.id, groupId: section.id })}>
|
||||||
<div className="checklist-item-main">
|
<div className={`checklist-item-main ${editing ? "is-editing" : ""}`}>
|
||||||
|
<button
|
||||||
|
className="checklist-drag-handle"
|
||||||
|
type="button"
|
||||||
|
onPointerDown={(event) => reorder.itemReorder.startDrag(event, item.id)}
|
||||||
|
aria-label={`${textContent.reorderTitle || "Déplacer"} ${item.label}`}
|
||||||
|
title={textContent.reorderTitle || "Déplacer"}
|
||||||
|
>
|
||||||
|
<Icon name="drag" />
|
||||||
|
</button>
|
||||||
{item.qtyTarget === 1 ? (
|
{item.qtyTarget === 1 ? (
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|
@ -263,9 +534,32 @@ function ChecklistItem({ item, context, items, save }) {
|
||||||
<button type="button" onClick={() => updateItem((entry) => ({ ...entry, qtyCurrent: context.clampQty(entry.qtyCurrent + 1, entry.qtyTarget) }))} aria-label={context.moduleText?.checklist?.incrementLabel || "Ajouter une quantité"}>+</button>
|
<button type="button" onClick={() => updateItem((entry) => ({ ...entry, qtyCurrent: context.clampQty(entry.qtyCurrent + 1, entry.qtyTarget) }))} aria-label={context.moduleText?.checklist?.incrementLabel || "Ajouter une quantité"}>+</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span>{item.label}</span>
|
{editing ? (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
{...labelEdit.getInputProps({
|
||||||
|
className: "checklist-label-input",
|
||||||
|
"aria-label": textContent.itemEditLabel || "Libellé"
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
{...categoryEdit.getInputProps({
|
||||||
|
className: "checklist-category-input",
|
||||||
|
placeholder: textContent.sectionPlaceholder || "Catégorie",
|
||||||
|
"aria-label": textContent.categoryEditLabel || "Catégorie"
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<button className="checklist-item-summary" type="button" onClick={toggleChecked} title={item.label}>
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button className="checklist-delete-button danger" onClick={() => save(items.filter((entry) => entry.id !== item.id))} aria-label={`${context.moduleText?.checklist?.deleteTitle || "Supprimer"} ${item.label}`} title={context.moduleText?.checklist?.deleteTitle || "Supprimer"}>
|
<button type="button" className="checklist-edit-button" onClick={onToggleEditing} aria-label={`${textContent.editTitle || "Modifier"} ${item.label}`} title={textContent.editTitle || "Modifier"}>
|
||||||
|
<Icon name="edit" />
|
||||||
|
</button>
|
||||||
|
<button className="checklist-delete-button danger" onClick={() => onDeleteItem(item.id)} aria-label={`${context.moduleText?.checklist?.deleteTitle || "Supprimer"} ${item.label}`} title={context.moduleText?.checklist?.deleteTitle || "Supprimer"}>
|
||||||
<Icon name="trash" />
|
<Icon name="trash" />
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
|
|
@ -553,9 +553,10 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCategory(group) {
|
function renderCategory(group, index, groupCount) {
|
||||||
const category = group.category;
|
const category = group.category;
|
||||||
const isCollapsed = data.collapsedCategories.includes(category);
|
const isCollapsed = data.collapsedCategories.includes(category);
|
||||||
|
const isLastCategory = index === groupCount - 1;
|
||||||
const categoryClassName = [
|
const categoryClassName = [
|
||||||
"combos-category checklist-section is-grouped",
|
"combos-category checklist-section is-grouped",
|
||||||
isCollapsed ? "is-collapsed" : "",
|
isCollapsed ? "is-collapsed" : "",
|
||||||
|
|
@ -614,10 +615,19 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>,
|
</section>,
|
||||||
afterDropZone
|
showBoundaryDropZones && isLastCategory ? afterDropZone : null
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const categoryCount = entries.filter((entry) => entry.type === "group").length;
|
||||||
|
let renderedCategoryIndex = -1;
|
||||||
|
|
||||||
|
function renderEntry(entry) {
|
||||||
|
if (entry.type !== "group") return renderCombo(entry.item);
|
||||||
|
renderedCategoryIndex += 1;
|
||||||
|
return renderCategory(entry.group, renderedCategoryIndex, categoryCount);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="combos-module">
|
<div className="combos-module">
|
||||||
{(editing || editingComboId) && (
|
{(editing || editingComboId) && (
|
||||||
|
|
@ -738,7 +748,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="combos-category-list">
|
<div className="combos-category-list">
|
||||||
{entries.map((entry) => entry.type === "group" ? renderCategory(entry.group) : renderCombo(entry.item))}
|
{entries.map(renderEntry)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
// Rôle : fournit l'outil compteurs personnalisables.
|
// Rôle : fournit l'outil compteurs personnalisables.
|
||||||
import { useState } from "react";
|
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
|
import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
||||||
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||||
|
|
||||||
export function CountersModule({ toolboxId, moduleId, context, editing }) {
|
export function CountersModule({ toolboxId, moduleId, context, editing }) {
|
||||||
const data = context.normalizeCountersData(context.getModuleData(toolboxId, moduleId, { counters: [] }));
|
const data = context.normalizeCountersData(context.getModuleData(toolboxId, moduleId, { counters: [] }));
|
||||||
const textContent = context.moduleText?.counters || {};
|
const textContent = context.moduleText?.counters || {};
|
||||||
const [label, setLabel] = useState("");
|
const counterDraft = useDraftForm({ label: "" });
|
||||||
const {
|
const {
|
||||||
itemReorder,
|
itemReorder,
|
||||||
getItemProps,
|
getItemProps,
|
||||||
|
|
@ -28,12 +28,13 @@ export function CountersModule({ toolboxId, moduleId, context, editing }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCounter(event) {
|
function addCounter(event) {
|
||||||
event.preventDefault();
|
counterDraft.handleSubmit(event, (draft, { reset }) => {
|
||||||
const cleanLabel = label.trim();
|
const cleanLabel = draft.label.trim();
|
||||||
if (!cleanLabel) return;
|
if (!cleanLabel) return;
|
||||||
|
|
||||||
save([...data.counters, { id: context.uid("counter"), label: cleanLabel, value: 0 }]);
|
save([...data.counters, { id: context.uid("counter"), label: cleanLabel, value: 0 }]);
|
||||||
setLabel("");
|
reset();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCounter(counterId, updater) {
|
function updateCounter(counterId, updater) {
|
||||||
|
|
@ -44,7 +45,7 @@ export function CountersModule({ toolboxId, moduleId, context, editing }) {
|
||||||
<>
|
<>
|
||||||
{editing && (
|
{editing && (
|
||||||
<form className="inline-form counters-add-form module-add-panel" onSubmit={addCounter}>
|
<form className="inline-form counters-add-form module-add-panel" onSubmit={addCounter}>
|
||||||
<input name="label" placeholder={textContent.labelPlaceholder || "Nom du compteur"} value={label} onChange={(event) => setLabel(event.target.value)} />
|
<input {...counterDraft.getFieldProps("label", { placeholder: textContent.labelPlaceholder || "Nom du compteur" })} />
|
||||||
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Rôle : fournit l'outil liens avec ajout manuel et import texte.
|
// Rôle : fournit l'outil liens avec ajout manuel et import texte.
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
|
import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
||||||
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||||
import { TextImportModal } from "./TextImportModal.jsx";
|
import { TextImportModal } from "./TextImportModal.jsx";
|
||||||
import { parseColonImportLines } from "./textImport.js";
|
import { parseColonImportLines } from "./textImport.js";
|
||||||
|
|
@ -8,8 +9,7 @@ import { parseColonImportLines } from "./textImport.js";
|
||||||
export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
||||||
const data = context.normalizeLinksData(context.getModuleData(toolboxId, moduleId, { links: [] }));
|
const data = context.normalizeLinksData(context.getModuleData(toolboxId, moduleId, { links: [] }));
|
||||||
const textContent = context.moduleText?.links || {};
|
const textContent = context.moduleText?.links || {};
|
||||||
const [title, setTitle] = useState("");
|
const linkDraft = useDraftForm({ title: "", url: "" });
|
||||||
const [url, setUrl] = useState("");
|
|
||||||
const [textImport, setTextImport] = useState("");
|
const [textImport, setTextImport] = useState("");
|
||||||
const [importOpen, setImportOpen] = useState(false);
|
const [importOpen, setImportOpen] = useState(false);
|
||||||
const [copiedId, setCopiedId] = useState("");
|
const [copiedId, setCopiedId] = useState("");
|
||||||
|
|
@ -34,20 +34,20 @@ export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function addLink(event) {
|
function addLink(event) {
|
||||||
event.preventDefault();
|
linkDraft.handleSubmit(event, (draft, { reset }) => {
|
||||||
const cleanUrl = context.normalizeUrl(url);
|
const cleanUrl = context.normalizeUrl(draft.url);
|
||||||
if (!cleanUrl) return;
|
if (!cleanUrl) return;
|
||||||
|
|
||||||
save([
|
save([
|
||||||
...data.links,
|
...data.links,
|
||||||
{
|
{
|
||||||
id: context.uid("link"),
|
id: context.uid("link"),
|
||||||
title: title.trim(),
|
title: draft.title.trim(),
|
||||||
url: cleanUrl
|
url: cleanUrl
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
setTitle("");
|
reset();
|
||||||
setUrl("");
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function importLinks(event) {
|
function importLinks(event) {
|
||||||
|
|
@ -79,8 +79,8 @@ export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
||||||
{editing && (
|
{editing && (
|
||||||
<div className="module-add-panel">
|
<div className="module-add-panel">
|
||||||
<form className="inline-form links-add-form" onSubmit={addLink}>
|
<form className="inline-form links-add-form" onSubmit={addLink}>
|
||||||
<input name="title" placeholder={textContent.titlePlaceholder || "Nom du lien"} value={title} onChange={(event) => setTitle(event.target.value)} />
|
<input {...linkDraft.getFieldProps("title", { placeholder: textContent.titlePlaceholder || "Nom du lien" })} />
|
||||||
<input name="url" placeholder={textContent.urlPlaceholder || "https://..."} value={url} onChange={(event) => setUrl(event.target.value)} />
|
<input {...linkDraft.getFieldProps("url", { placeholder: textContent.urlPlaceholder || "https://..." })} />
|
||||||
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
||||||
</form>
|
</form>
|
||||||
<div className="text-import-actions">
|
<div className="text-import-actions">
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { InlineNotice } from "../../../components/AppOverlays.jsx";
|
import { InlineNotice } from "../../../components/AppOverlays.jsx";
|
||||||
|
import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
||||||
import { applyGroupedReorderOperation, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
import { applyGroupedReorderOperation, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||||
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
||||||
|
|
||||||
|
|
@ -222,8 +223,7 @@ function getRootTaskIdsForCategory(tasks, parentMap, category) {
|
||||||
export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
||||||
const data = context.normalizeTaskPlannerData(context.getModuleData(toolboxId, moduleId, { tasks: [] }));
|
const data = context.normalizeTaskPlannerData(context.getModuleData(toolboxId, moduleId, { tasks: [] }));
|
||||||
const textContent = context.moduleText?.taskPlanner || {};
|
const textContent = context.moduleText?.taskPlanner || {};
|
||||||
const [title, setTitle] = useState("");
|
const taskDraft = useDraftForm({ title: "", type: "daily" });
|
||||||
const [type, setType] = useState("daily");
|
|
||||||
const [nowMs, setNowMs] = useState(() => Date.now());
|
const [nowMs, setNowMs] = useState(() => Date.now());
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
const [openDescriptions, setOpenDescriptions] = useState(() => new Set());
|
const [openDescriptions, setOpenDescriptions] = useState(() => new Set());
|
||||||
|
|
@ -358,25 +358,25 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
||||||
});
|
});
|
||||||
|
|
||||||
function addTask(event) {
|
function addTask(event) {
|
||||||
event.preventDefault();
|
taskDraft.handleSubmit(event, (draft, { reset }) => {
|
||||||
const cleanTitle = title.trim();
|
const cleanTitle = draft.title.trim();
|
||||||
if (!cleanTitle) return;
|
if (!cleanTitle) return;
|
||||||
save({
|
save({
|
||||||
...data,
|
...data,
|
||||||
tasks: [
|
tasks: [
|
||||||
...data.tasks,
|
...data.tasks,
|
||||||
{
|
{
|
||||||
id: context.uid("task"),
|
id: context.uid("task"),
|
||||||
title: cleanTitle,
|
title: cleanTitle,
|
||||||
description: "",
|
description: "",
|
||||||
type,
|
type: draft.type,
|
||||||
checked: false,
|
checked: false,
|
||||||
checkedAt: 0
|
checkedAt: 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
});
|
||||||
|
reset();
|
||||||
});
|
});
|
||||||
setTitle("");
|
|
||||||
setType("daily");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTask(taskId, updater) {
|
function updateTask(taskId, updater) {
|
||||||
|
|
@ -509,8 +509,8 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing }) {
|
||||||
</div>
|
</div>
|
||||||
{editing && (
|
{editing && (
|
||||||
<form className="inline-form task-planner-add-form" onSubmit={addTask}>
|
<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)} />
|
<input {...taskDraft.getFieldProps("title", { placeholder: textContent.titlePlaceholder || "Nouvelle tâche" })} />
|
||||||
<select value={type} onChange={(event) => setType(event.target.value)} aria-label={textContent.typeLabel || "Type"}>
|
<select {...taskDraft.getFieldProps("type", { "aria-label": textContent.typeLabel || "Type" })}>
|
||||||
{TASK_TYPES.map((taskType) => <option key={taskType} value={taskType}>{getTaskTypeLabel(taskType, textContent)}</option>)}
|
{TASK_TYPES.map((taskType) => <option key={taskType} value={taskType}>{getTaskTypeLabel(taskType, textContent)}</option>)}
|
||||||
</select>
|
</select>
|
||||||
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
import { Tabs } from "../../../components/Tabs.jsx";
|
import { Tabs } from "../../../components/Tabs.jsx";
|
||||||
|
import { useDraftForm } from "../../../hooks/useDraftForm.js";
|
||||||
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
||||||
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||||
import {
|
import {
|
||||||
|
|
@ -24,6 +25,15 @@ const TIMER_TABS = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const COUNTDOWN_TYPES = ["duration", "daily_time", "time_pattern", "interval"];
|
const COUNTDOWN_TYPES = ["duration", "daily_time", "time_pattern", "interval"];
|
||||||
|
const EMPTY_TIME_PARTS = { hours: "", minutes: "", seconds: "" };
|
||||||
|
const DEFAULT_COUNTDOWN_FORM = {
|
||||||
|
label: "",
|
||||||
|
type: "duration",
|
||||||
|
duration: EMPTY_TIME_PARTS,
|
||||||
|
time: EMPTY_TIME_PARTS,
|
||||||
|
pattern: EMPTY_TIME_PARTS,
|
||||||
|
interval: EMPTY_TIME_PARTS
|
||||||
|
};
|
||||||
const ALERT_MODES = [
|
const ALERT_MODES = [
|
||||||
{ mode: "off", icon: "sound-mute" },
|
{ mode: "off", icon: "sound-mute" },
|
||||||
{ mode: "visible", icon: "sound-min" },
|
{ mode: "visible", icon: "sound-min" },
|
||||||
|
|
@ -73,14 +83,7 @@ export function TimerModule({ toolboxId, moduleId, context }) {
|
||||||
const textContent = context.moduleText?.timer || {};
|
const textContent = context.moduleText?.timer || {};
|
||||||
const activeTab = data.activeTab;
|
const activeTab = data.activeTab;
|
||||||
const [nowMs, setNowMs] = useState(getNowMs);
|
const [nowMs, setNowMs] = useState(getNowMs);
|
||||||
const [countdownForm, setCountdownForm] = useState({
|
const countdownDraft = useDraftForm(DEFAULT_COUNTDOWN_FORM);
|
||||||
label: "",
|
|
||||||
type: "duration",
|
|
||||||
duration: { hours: "", minutes: "", seconds: "" },
|
|
||||||
time: { hours: "", minutes: "", seconds: "" },
|
|
||||||
pattern: { hours: "", minutes: "", seconds: "" },
|
|
||||||
interval: { hours: "", minutes: "", seconds: "" }
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const intervalId = window.setInterval(() => setNowMs(getNowMs()), 50);
|
const intervalId = window.setInterval(() => setNowMs(getNowMs()), 50);
|
||||||
|
|
@ -172,38 +175,35 @@ export function TimerModule({ toolboxId, moduleId, context }) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCountdownForm(patch) {
|
|
||||||
setCountdownForm((current) => ({ ...current, ...patch }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function createCountdown(event) {
|
function createCountdown(event) {
|
||||||
event.preventDefault();
|
countdownDraft.handleSubmit(event, (draft, { updateValues }) => {
|
||||||
const type = COUNTDOWN_TYPES.includes(countdownForm.type) ? countdownForm.type : "duration";
|
const type = COUNTDOWN_TYPES.includes(draft.type) ? draft.type : "duration";
|
||||||
const label = countdownForm.label.trim() || textContent.defaultCountdownLabel || "Timer";
|
const label = draft.label.trim() || textContent.defaultCountdownLabel || "Timer";
|
||||||
const now = getNowMs();
|
const now = getNowMs();
|
||||||
let countdown = null;
|
let countdown = null;
|
||||||
|
|
||||||
if (type === "duration") {
|
if (type === "duration") {
|
||||||
const durationMs = timePartsToDurationMs(countdownForm.duration);
|
const durationMs = timePartsToDurationMs(draft.duration);
|
||||||
if (durationMs > 0) countdown = { id: context.uid("timer"), label, type, durationMs, targetAt: now + durationMs };
|
if (durationMs > 0) countdown = { id: context.uid("timer"), label, type, durationMs, targetAt: now + durationMs };
|
||||||
}
|
}
|
||||||
if (type === "daily_time") {
|
if (type === "daily_time") {
|
||||||
const time = timePartsToString(countdownForm.time);
|
const time = timePartsToString(draft.time);
|
||||||
const targetAt = getDailyTargetMs(time, now);
|
const targetAt = getDailyTargetMs(time, now);
|
||||||
if (time && targetAt) countdown = { id: context.uid("timer"), label, type, time, targetAt };
|
if (time && targetAt) countdown = { id: context.uid("timer"), label, type, time, targetAt };
|
||||||
}
|
}
|
||||||
if (type === "time_pattern") {
|
if (type === "time_pattern") {
|
||||||
const pattern = timePartsToString(countdownForm.pattern, { allowWildcard: true });
|
const pattern = timePartsToString(draft.pattern, { allowWildcard: true });
|
||||||
if (pattern && getTimePatternTargetMs(pattern, now)) countdown = { id: context.uid("timer"), label, type, pattern };
|
if (pattern && getTimePatternTargetMs(pattern, now)) countdown = { id: context.uid("timer"), label, type, pattern };
|
||||||
}
|
}
|
||||||
if (type === "interval") {
|
if (type === "interval") {
|
||||||
const intervalMs = timePartsToDurationMs(countdownForm.interval);
|
const intervalMs = timePartsToDurationMs(draft.interval);
|
||||||
if (intervalMs > 0) countdown = { id: context.uid("timer"), label, type, intervalMs, anchorAt: now };
|
if (intervalMs > 0) countdown = { id: context.uid("timer"), label, type, intervalMs, anchorAt: now };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!countdown) return;
|
if (!countdown) return;
|
||||||
save({ ...data, countdowns: [...data.countdowns, countdown] });
|
save({ ...data, countdowns: [...data.countdowns, countdown] });
|
||||||
setCountdownForm((current) => ({ ...current, label: "" }));
|
updateValues({ label: "" });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteCountdown(countdownId) {
|
function deleteCountdown(countdownId) {
|
||||||
|
|
@ -274,8 +274,8 @@ export function TimerModule({ toolboxId, moduleId, context }) {
|
||||||
) : (
|
) : (
|
||||||
<CountdownControls
|
<CountdownControls
|
||||||
textContent={textContent}
|
textContent={textContent}
|
||||||
form={countdownForm}
|
form={countdownDraft.values}
|
||||||
onChange={updateCountdownForm}
|
onChange={countdownDraft.updateValues}
|
||||||
onSubmit={createCountdown}
|
onSubmit={createCountdown}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
47
website/src/hooks/useDraftForm.js
Normal file
47
website/src/hooks/useDraftForm.js
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Rôle : centralise les petits formulaires contrôlés avec brouillon local.
|
||||||
|
// Fournit helpers de champs, submit preventDefault et reset, sans porter de validation métier.
|
||||||
|
// À utiliser quand plusieurs champs sont simplement saisis puis nettoyés au submit.
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function useDraftForm(initialValues) {
|
||||||
|
const [values, setValues] = useState(initialValues);
|
||||||
|
|
||||||
|
function setField(name, value) {
|
||||||
|
setValues((current) => ({ ...current, [name]: value }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateValues(patch) {
|
||||||
|
setValues((current) => ({ ...current, ...patch }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset(nextValues = initialValues) {
|
||||||
|
setValues(nextValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFieldProps(name, props = {}) {
|
||||||
|
return {
|
||||||
|
...props,
|
||||||
|
name: props.name || name,
|
||||||
|
value: values[name] ?? "",
|
||||||
|
onChange: (event) => {
|
||||||
|
setField(name, event.target.value);
|
||||||
|
props.onChange?.(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit(event, onSubmit) {
|
||||||
|
event.preventDefault();
|
||||||
|
onSubmit(values, { reset, setField, setValues, updateValues });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
values,
|
||||||
|
setField,
|
||||||
|
setValues,
|
||||||
|
updateValues,
|
||||||
|
reset,
|
||||||
|
getFieldProps,
|
||||||
|
handleSubmit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -470,13 +470,35 @@
|
||||||
|
|
||||||
[data-reorder-target="item"][data-reorder-orientation="vertical"].is-drop-target,
|
[data-reorder-target="item"][data-reorder-orientation="vertical"].is-drop-target,
|
||||||
[data-reorder-target="group"][data-reorder-orientation="vertical"].is-drop-target {
|
[data-reorder-target="group"][data-reorder-orientation="vertical"].is-drop-target {
|
||||||
|
position: relative;
|
||||||
border-color: rgba(246, 196, 83, 0.72);
|
border-color: rgba(246, 196, 83, 0.72);
|
||||||
box-shadow: var(--reorder-drop-shadow, none), inset 0 3px 0 rgba(246, 196, 83, 0.8);
|
box-shadow: var(--reorder-drop-shadow, none);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-reorder-target="item"][data-reorder-orientation="vertical"].is-drop-target::before,
|
||||||
|
[data-reorder-target="group"][data-reorder-orientation="vertical"].is-drop-target::before {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: rgba(246, 196, 83, 0.9);
|
||||||
|
box-shadow: 0 0 12px rgba(246, 196, 83, 0.24);
|
||||||
|
content: "";
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-reorder-target="item"][data-reorder-orientation="vertical"].is-drop-target.drop-after,
|
[data-reorder-target="item"][data-reorder-orientation="vertical"].is-drop-target.drop-after,
|
||||||
[data-reorder-target="group"][data-reorder-orientation="vertical"].is-drop-target.drop-after {
|
[data-reorder-target="group"][data-reorder-orientation="vertical"].is-drop-target.drop-after {
|
||||||
box-shadow: var(--reorder-drop-shadow, none), inset 0 -3px 0 rgba(246, 196, 83, 0.8);
|
box-shadow: var(--reorder-drop-shadow, none);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-reorder-target="item"][data-reorder-orientation="vertical"].is-drop-target.drop-after::before,
|
||||||
|
[data-reorder-target="group"][data-reorder-orientation="vertical"].is-drop-target.drop-after::before {
|
||||||
|
top: auto;
|
||||||
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-reorder-target="item"][data-reorder-orientation="horizontal"].is-drop-target,
|
[data-reorder-target="item"][data-reorder-orientation="horizontal"].is-drop-target,
|
||||||
|
|
@ -1441,7 +1463,12 @@ textarea:focus {
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checklist-section-header > div {
|
.checklist-section-header > h3 {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-section-actions {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -1454,18 +1481,22 @@ textarea:focus {
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checklist-gender-symbol {
|
.checklist-section-title-input {
|
||||||
display: inline-block;
|
width: 100%;
|
||||||
margin-left: 3px;
|
min-height: 30px;
|
||||||
color: currentColor;
|
padding: 5px 8px;
|
||||||
font-family: "Segoe UI Symbol", "Noto Sans Symbols 2", "Noto Sans Symbols", "Apple Symbols", sans-serif;
|
border: 1px solid transparent;
|
||||||
font-size: 1.18em;
|
border-radius: var(--radius-md);
|
||||||
font-weight: 900;
|
background: transparent;
|
||||||
line-height: 1;
|
color: var(--color-accent-gold);
|
||||||
text-shadow:
|
font: inherit;
|
||||||
0 0 0 currentColor,
|
outline: none;
|
||||||
0 0 4px rgba(246, 196, 83, 0.22);
|
}
|
||||||
transform: translateY(1px);
|
|
||||||
|
.checklist-section-title-input:hover,
|
||||||
|
.checklist-section-title-input:focus {
|
||||||
|
border-color: rgba(246, 196, 83, 0.34);
|
||||||
|
background: rgba(21, 26, 48, 0.34);
|
||||||
}
|
}
|
||||||
|
|
||||||
.checklist-section.is-complete .checklist-section-header h3 {
|
.checklist-section.is-complete .checklist-section-header h3 {
|
||||||
|
|
@ -1473,7 +1504,12 @@ textarea:focus {
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checklist-section-header > div > span {
|
.checklist-section.is-complete .checklist-section-title-input {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-section-actions > span {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
padding: 3px 9px;
|
padding: 3px 9px;
|
||||||
border: 1px solid rgba(246, 196, 83, 0.22);
|
border: 1px solid rgba(246, 196, 83, 0.22);
|
||||||
|
|
@ -1573,15 +1609,29 @@ textarea:focus {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checklist-category-boundary-drop-zone {
|
||||||
|
min-height: 10px;
|
||||||
|
border: 1px dashed rgba(165, 180, 252, 0.16);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: rgba(15, 23, 42, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-category-boundary-drop-zone.is-drop-target {
|
||||||
|
border-color: rgba(246, 196, 83, 0.72);
|
||||||
|
background: rgba(246, 196, 83, 0.08);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(246, 196, 83, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
.checklist-item {
|
.checklist-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 42px;
|
grid-template-columns: 1fr 42px 42px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checklist-item-main {
|
.checklist-item-main {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: 30px auto minmax(0, 1fr);
|
||||||
min-height: 42px;
|
min-height: 42px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
@ -1591,11 +1641,129 @@ textarea:focus {
|
||||||
background: rgba(5, 7, 17, 0.44);
|
background: rgba(5, 7, 17, 0.44);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checklist-item-main:hover {
|
||||||
|
border-color: rgba(196, 181, 253, 0.22);
|
||||||
|
background: rgba(10, 13, 30, 0.54);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-item-main.is-editing {
|
||||||
|
grid-template-columns: 30px auto minmax(96px, 1fr) minmax(84px, 0.45fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-drag-handle {
|
||||||
|
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);
|
||||||
|
cursor: grab;
|
||||||
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-drag-handle .ui-icon {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-drag-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-section.is-drop-target {
|
||||||
|
border-color: rgba(246, 196, 83, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-item.is-dragging .checklist-item-main {
|
||||||
|
opacity: 0.58;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-label-input,
|
||||||
|
.checklist-category-input {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-category-input {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-label-input:hover,
|
||||||
|
.checklist-label-input:focus,
|
||||||
|
.checklist-category-input:hover,
|
||||||
|
.checklist-category-input:focus {
|
||||||
|
border-color: rgba(246, 196, 83, 0.34);
|
||||||
|
background: rgba(21, 26, 48, 0.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-item-summary {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
text-align: left;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-item-summary:hover {
|
||||||
|
border-color: transparent;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-item-summary span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
font-weight: 700;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-edit-button {
|
||||||
|
display: inline-grid;
|
||||||
|
width: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-edit-button .ui-icon {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
.checklist input[type="checkbox"] {
|
.checklist input[type="checkbox"] {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checklist-item.is-complete .checklist-item-main > span {
|
.checklist-item.is-complete .checklist-label-input {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-item.is-complete .checklist-item-summary span {
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
|
|
@ -1946,11 +2114,10 @@ textarea:focus {
|
||||||
|
|
||||||
.task-planner-category-section.is-drop-target {
|
.task-planner-category-section.is-drop-target {
|
||||||
border-color: rgba(246, 196, 83, 0.72);
|
border-color: rgba(246, 196, 83, 0.72);
|
||||||
box-shadow: inset 0 3px 0 rgba(246, 196, 83, 0.8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-planner-category-section.is-drop-target.drop-after {
|
.task-planner-category-section.is-drop-target.drop-after {
|
||||||
box-shadow: inset 0 -3px 0 rgba(246, 196, 83, 0.8);
|
border-color: rgba(246, 196, 83, 0.72);
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-planner-item {
|
.task-planner-item {
|
||||||
|
|
@ -2459,15 +2626,16 @@ textarea:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.combos-category-boundary-drop-zone {
|
.combos-category-boundary-drop-zone {
|
||||||
min-height: 6px;
|
min-height: 10px;
|
||||||
border-radius: 999px;
|
border: 1px dashed rgba(165, 180, 252, 0.16);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: rgba(15, 23, 42, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.combos-category-boundary-drop-zone.is-drop-target {
|
.combos-category-boundary-drop-zone.is-drop-target {
|
||||||
min-height: 12px;
|
|
||||||
border: 1px dashed rgba(246, 196, 83, 0.72);
|
border: 1px dashed rgba(246, 196, 83, 0.72);
|
||||||
background: rgba(246, 196, 83, 0.12);
|
background: rgba(246, 196, 83, 0.08);
|
||||||
box-shadow: inset 0 2px 0 rgba(246, 196, 83, 0.74);
|
box-shadow: inset 0 0 0 1px rgba(246, 196, 83, 0.24);
|
||||||
}
|
}
|
||||||
|
|
||||||
.combos-list {
|
.combos-list {
|
||||||
|
|
@ -3736,6 +3904,8 @@ button.combo-input-token.combo-input-mouse {
|
||||||
.tool-split-root-button:hover,
|
.tool-split-root-button:hover,
|
||||||
.tool-split-action-button:hover:not(:disabled),
|
.tool-split-action-button:hover:not(:disabled),
|
||||||
.checklist-qty-controls button:hover,
|
.checklist-qty-controls button:hover,
|
||||||
|
.checklist-drag-handle:hover,
|
||||||
|
.checklist-edit-button:hover,
|
||||||
.tool-split-entry > button:not(.tool-split-entry-summary, .tool-split-entry-value, .danger):hover,
|
.tool-split-entry > button:not(.tool-split-entry-summary, .tool-split-entry-value, .danger):hover,
|
||||||
.counter-actions button:not(.danger):hover,
|
.counter-actions button:not(.danger):hover,
|
||||||
.link-item button:not(.danger):hover,
|
.link-item button:not(.danger):hover,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue