update checklist component
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
This commit is contained in:
parent
772b9951de
commit
d9d22683dc
13 changed files with 709 additions and 65 deletions
|
|
@ -76,10 +76,16 @@ export function validateSiteContent(site) {
|
||||||
"toolboxes.storageHelp.text",
|
"toolboxes.storageHelp.text",
|
||||||
"toolboxes.modules.notepad.placeholder",
|
"toolboxes.modules.notepad.placeholder",
|
||||||
"toolboxes.modules.checklist.itemPlaceholder",
|
"toolboxes.modules.checklist.itemPlaceholder",
|
||||||
|
"toolboxes.modules.checklist.sectionPlaceholder",
|
||||||
"toolboxes.modules.checklist.quantityLabel",
|
"toolboxes.modules.checklist.quantityLabel",
|
||||||
"toolboxes.modules.checklist.addButton",
|
"toolboxes.modules.checklist.addButton",
|
||||||
"toolboxes.modules.checklist.importPlaceholder",
|
"toolboxes.modules.checklist.importPlaceholder",
|
||||||
|
"toolboxes.modules.checklist.importOpenButton",
|
||||||
|
"toolboxes.modules.checklist.importModalTitle",
|
||||||
"toolboxes.modules.checklist.importButton",
|
"toolboxes.modules.checklist.importButton",
|
||||||
|
"toolboxes.modules.checklist.hideCompletedSectionsTitle",
|
||||||
|
"toolboxes.modules.checklist.showCompletedSectionTitle",
|
||||||
|
"toolboxes.modules.checklist.hideCompletedSectionTitle",
|
||||||
"toolboxes.modules.checklist.decrementLabel",
|
"toolboxes.modules.checklist.decrementLabel",
|
||||||
"toolboxes.modules.checklist.incrementLabel",
|
"toolboxes.modules.checklist.incrementLabel",
|
||||||
"toolboxes.modules.checklist.currentQuantityLabel",
|
"toolboxes.modules.checklist.currentQuantityLabel",
|
||||||
|
|
@ -97,6 +103,8 @@ export function validateSiteContent(site) {
|
||||||
"toolboxes.modules.links.urlPlaceholder",
|
"toolboxes.modules.links.urlPlaceholder",
|
||||||
"toolboxes.modules.links.addButton",
|
"toolboxes.modules.links.addButton",
|
||||||
"toolboxes.modules.links.importPlaceholder",
|
"toolboxes.modules.links.importPlaceholder",
|
||||||
|
"toolboxes.modules.links.importOpenButton",
|
||||||
|
"toolboxes.modules.links.importModalTitle",
|
||||||
"toolboxes.modules.links.importButton",
|
"toolboxes.modules.links.importButton",
|
||||||
"toolboxes.modules.links.copyTitle",
|
"toolboxes.modules.links.copyTitle",
|
||||||
"toolboxes.modules.links.copiedTitle",
|
"toolboxes.modules.links.copiedTitle",
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ test("react application defines the expected local toolbox primitives", async ()
|
||||||
const screenshotsModule = await readFile("website/src/features/toolboxes/modules/ScreenshotsModule.jsx", "utf8");
|
const screenshotsModule = await readFile("website/src/features/toolboxes/modules/ScreenshotsModule.jsx", "utf8");
|
||||||
const imageAnnotationModule = await readFile("website/src/features/toolboxes/modules/ImageAnnotationModule.jsx", "utf8");
|
const imageAnnotationModule = await readFile("website/src/features/toolboxes/modules/ImageAnnotationModule.jsx", "utf8");
|
||||||
const linksModule = await readFile("website/src/features/toolboxes/modules/LinksModule.jsx", "utf8");
|
const linksModule = await readFile("website/src/features/toolboxes/modules/LinksModule.jsx", "utf8");
|
||||||
|
const textImportModal = await readFile("website/src/features/toolboxes/modules/TextImportModal.jsx", "utf8");
|
||||||
const countersModule = await readFile("website/src/features/toolboxes/modules/CountersModule.jsx", "utf8");
|
const countersModule = await readFile("website/src/features/toolboxes/modules/CountersModule.jsx", "utf8");
|
||||||
const textImport = await readFile("website/src/features/toolboxes/modules/textImport.js", "utf8");
|
const textImport = await readFile("website/src/features/toolboxes/modules/textImport.js", "utf8");
|
||||||
const reorderHook = await readFile("website/src/hooks/usePointerReorder.js", "utf8");
|
const reorderHook = await readFile("website/src/hooks/usePointerReorder.js", "utf8");
|
||||||
|
|
@ -90,12 +91,15 @@ test("react application defines the expected local toolbox primitives", async ()
|
||||||
assert.match(styleToolboxes, /\.toolbox-game-icon/);
|
assert.match(styleToolboxes, /\.toolbox-game-icon/);
|
||||||
assert.match(styleToolboxes, /\.toolbox-hero-link/);
|
assert.match(styleToolboxes, /\.toolbox-hero-link/);
|
||||||
assert.match(styleToolboxes, /\.toolbox-actions-row/);
|
assert.match(styleToolboxes, /\.toolbox-actions-row/);
|
||||||
assert.match(styleToolboxes, /\.text-import-form/);
|
assert.match(styleToolboxes, /\.text-import-actions/);
|
||||||
assert.match(styleIcons, /ui-icon-drag/);
|
assert.match(styleIcons, /ui-icon-drag/);
|
||||||
assert.match(styleIcons, /ui-icon-dropdown/);
|
assert.match(styleIcons, /ui-icon-dropdown/);
|
||||||
assert.match(styleIcons, /ui-icon-enter/);
|
assert.match(styleIcons, /ui-icon-enter/);
|
||||||
assert.match(styleIcons, /ui-icon-calculator/);
|
assert.match(styleIcons, /ui-icon-calculator/);
|
||||||
assert.match(styleIcons, /ui-icon-scrollable/);
|
assert.match(styleIcons, /ui-icon-scrollable/);
|
||||||
|
assert.match(styleIcons, /ui-icon-hide/);
|
||||||
|
assert.match(styleIcons, /ui-icon-chevron-down/);
|
||||||
|
assert.match(styleIcons, /ui-icon-chevron-up/);
|
||||||
assert.match(styleIcons, /ui-icon-flip/);
|
assert.match(styleIcons, /ui-icon-flip/);
|
||||||
assert.match(styleIcons, /ui-icon-add/);
|
assert.match(styleIcons, /ui-icon-add/);
|
||||||
assert.match(styleMhwilds, /\.mhwilds-card/);
|
assert.match(styleMhwilds, /\.mhwilds-card/);
|
||||||
|
|
@ -180,6 +184,13 @@ test("react application defines the expected local toolbox primitives", async ()
|
||||||
assert.match(calculatorModule, /--calculator-scroll-height/);
|
assert.match(calculatorModule, /--calculator-scroll-height/);
|
||||||
assert.match(calculatorModule, /CalculatorEntries/);
|
assert.match(calculatorModule, /CalculatorEntries/);
|
||||||
assert.match(checklistModule, /export function ChecklistModule/);
|
assert.match(checklistModule, /export function ChecklistModule/);
|
||||||
|
assert.match(checklistModule, /TextImportModal/);
|
||||||
|
assert.match(textImportModal, /createPortal/);
|
||||||
|
assert.match(textImportModal, /lockBodyScroll/);
|
||||||
|
assert.match(checklistModule, /Icon name="import"/);
|
||||||
|
assert.match(styleToolboxes, /\.text-import-modal-dialog/);
|
||||||
|
assert.match(linksModule, /TextImportModal/);
|
||||||
|
assert.match(linksModule, /Icon name="import"/);
|
||||||
assert.match(screenshotsModule, /export function ScreenshotsModule/);
|
assert.match(screenshotsModule, /export function ScreenshotsModule/);
|
||||||
assert.match(imageAnnotationModule, /export function ImageAnnotationModule/);
|
assert.match(imageAnnotationModule, /export function ImageAnnotationModule/);
|
||||||
assert.match(linksModule, /export function LinksModule/);
|
assert.match(linksModule, /export function LinksModule/);
|
||||||
|
|
@ -241,11 +252,18 @@ test("react application defines the expected local toolbox primitives", async ()
|
||||||
assert.match(moduleRegistry, /tool-add-card/);
|
assert.match(moduleRegistry, /tool-add-card/);
|
||||||
assert.match(moduleRegistry, /tool-quick-add-button/);
|
assert.match(moduleRegistry, /tool-quick-add-button/);
|
||||||
assert.match(moduleRegistry, /tool-add-quick-toggle/);
|
assert.match(moduleRegistry, /tool-add-quick-toggle/);
|
||||||
|
assert.match(moduleRegistry, /scrollable: true/);
|
||||||
|
assert.match(moduleRegistry, /module-content/);
|
||||||
|
assert.match(moduleRegistry, /module-scroll-button/);
|
||||||
assert.match(moduleRegistry, /Icon name="dropdown"/);
|
assert.match(moduleRegistry, /Icon name="dropdown"/);
|
||||||
assert.match(moduleRegistry, /aria-expanded=\{quickOpen\}/);
|
assert.match(moduleRegistry, /aria-expanded=\{quickOpen\}/);
|
||||||
assert.doesNotMatch(source, /<select value="" onChange=\{\(event\) => addModule/);
|
assert.doesNotMatch(source, /<select value="" onChange=\{\(event\) => addModule/);
|
||||||
assert.match(source, /qtyTarget/);
|
assert.match(source, /qtyTarget/);
|
||||||
assert.match(source, /qtyCurrent/);
|
assert.match(source, /qtyCurrent/);
|
||||||
|
assert.match(source, /hideCompletedSections/);
|
||||||
|
assert.match(source, /hideWhenComplete/);
|
||||||
|
assert.match(source, /collapsed/);
|
||||||
|
assert.match(source, /module\.scrollable/);
|
||||||
assert.match(source, /normalizeLinksData/);
|
assert.match(source, /normalizeLinksData/);
|
||||||
assert.match(source, /normalizeCountersData/);
|
assert.match(source, /normalizeCountersData/);
|
||||||
assert.match(source, /normalizeCalculatorData/);
|
assert.match(source, /normalizeCalculatorData/);
|
||||||
|
|
@ -253,7 +271,12 @@ test("react application defines the expected local toolbox primitives", async ()
|
||||||
assert.match(source, /compactModuleDataForStorage\(type, value\)/);
|
assert.match(source, /compactModuleDataForStorage\(type, value\)/);
|
||||||
assert.match(source, /scrollResults/);
|
assert.match(source, /scrollResults/);
|
||||||
assert.match(checklistModule, /ChecklistItem/);
|
assert.match(checklistModule, /ChecklistItem/);
|
||||||
|
assert.match(checklistModule, /ChecklistSection/);
|
||||||
|
assert.match(checklistModule, /parseChecklistImport/);
|
||||||
assert.match(checklistModule, /parseColonImportLines/);
|
assert.match(checklistModule, /parseColonImportLines/);
|
||||||
|
assert.match(checklistModule, /startsWith\("#"\)/);
|
||||||
|
assert.match(checklistModule, /chevron-down/);
|
||||||
|
assert.match(checklistModule, /chevron-up/);
|
||||||
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/);
|
||||||
|
|
@ -317,6 +340,9 @@ test("legacy svg icons are available for reuse", async () => {
|
||||||
await readFile("website/public/static/icons/map.svg", "utf8");
|
await readFile("website/public/static/icons/map.svg", "utf8");
|
||||||
await readFile("website/public/static/icons/enter.svg", "utf8");
|
await readFile("website/public/static/icons/enter.svg", "utf8");
|
||||||
await readFile("website/public/static/icons/export.svg", "utf8");
|
await readFile("website/public/static/icons/export.svg", "utf8");
|
||||||
|
await readFile("website/public/static/icons/hide.svg", "utf8");
|
||||||
|
await readFile("website/public/static/icons/chevron-down.svg", "utf8");
|
||||||
|
await readFile("website/public/static/icons/chevron-up.svg", "utf8");
|
||||||
await readFile("website/public/static/icons/scrollable.svg", "utf8");
|
await readFile("website/public/static/icons/scrollable.svg", "utf8");
|
||||||
await readFile("website/public/static/icons/notepad.svg", "utf8");
|
await readFile("website/public/static/icons/notepad.svg", "utf8");
|
||||||
await readFile("website/public/static/icons/picture.svg", "utf8");
|
await readFile("website/public/static/icons/picture.svg", "utf8");
|
||||||
|
|
|
||||||
|
|
@ -190,10 +190,16 @@
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"itemPlaceholder": "Nouvel item",
|
"itemPlaceholder": "Nouvel item",
|
||||||
|
"sectionPlaceholder": "Catégorie",
|
||||||
"quantityLabel": "Quantité cible",
|
"quantityLabel": "Quantité cible",
|
||||||
"addButton": "Ajouter",
|
"addButton": "Ajouter",
|
||||||
"importPlaceholder": "Importer plusieurs items...\nPotion:10\nMéga potion:5",
|
"importPlaceholder": "Importer plusieurs items...\n# Potions\nPotion:10\nMéga potion:5\n\n# Armures\nCasque Rey Dau:1",
|
||||||
|
"importOpenButton": "Importer du texte",
|
||||||
|
"importModalTitle": "Importer une checklist",
|
||||||
"importButton": "Importer le texte",
|
"importButton": "Importer le texte",
|
||||||
|
"hideCompletedSectionsTitle": "Masquer les catégories terminées",
|
||||||
|
"showCompletedSectionTitle": "Afficher cette catégorie terminée",
|
||||||
|
"hideCompletedSectionTitle": "Masquer cette catégorie terminée",
|
||||||
"decrementLabel": "Retirer une quantité",
|
"decrementLabel": "Retirer une quantité",
|
||||||
"incrementLabel": "Ajouter une quantité",
|
"incrementLabel": "Ajouter une quantité",
|
||||||
"currentQuantityLabel": "Quantité actuelle",
|
"currentQuantityLabel": "Quantité actuelle",
|
||||||
|
|
@ -215,6 +221,8 @@
|
||||||
"urlPlaceholder": "https://...",
|
"urlPlaceholder": "https://...",
|
||||||
"addButton": "Ajouter",
|
"addButton": "Ajouter",
|
||||||
"importPlaceholder": "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map",
|
"importPlaceholder": "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map",
|
||||||
|
"importOpenButton": "Importer du texte",
|
||||||
|
"importModalTitle": "Importer des liens",
|
||||||
"importButton": "Importer le texte",
|
"importButton": "Importer le texte",
|
||||||
"copyTitle": "Copier",
|
"copyTitle": "Copier",
|
||||||
"copiedTitle": "Copié",
|
"copiedTitle": "Copié",
|
||||||
|
|
|
||||||
3
website/public/static/icons/chevron-down.svg
Normal file
3
website/public/static/icons/chevron-down.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path fill="currentColor" d="M12 15.5a1 1 0 0 1-.7-.29l-5-5a1 1 0 1 1 1.4-1.42L12 13.08l4.3-4.29a1 1 0 1 1 1.4 1.42l-5 5a1 1 0 0 1-.7.29Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 230 B |
3
website/public/static/icons/chevron-up.svg
Normal file
3
website/public/static/icons/chevron-up.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path fill="currentColor" d="M17 15.5a1 1 0 0 1-.7-.29L12 10.92l-4.3 4.29a1 1 0 0 1-1.4-1.42l5-5a1 1 0 0 1 1.4 0l5 5a1 1 0 0 1-.7 1.71Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 228 B |
49
website/public/static/icons/hide.svg
Normal file
49
website/public/static/icons/hide.svg
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||||
|
<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
|
||||||
|
<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
|
||||||
|
<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
|
||||||
|
<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
|
||||||
|
<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
|
||||||
|
<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
|
||||||
|
<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
|
||||||
|
<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
|
||||||
|
]>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg fill="#000000" version="1.1" id="Layer_1" xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800px" height="800px"
|
||||||
|
viewBox="0 0 24 24" enable-background="new 0 0 24 24" xml:space="preserve">
|
||||||
|
<metadata>
|
||||||
|
<sfw xmlns="&ns_sfw;">
|
||||||
|
<slices>
|
||||||
|
</slices>
|
||||||
|
<sliceSourceBounds width="505" height="984" bottomLeftOrigin="true" x="0" y="-120">
|
||||||
|
</sliceSourceBounds>
|
||||||
|
</sfw>
|
||||||
|
</metadata>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M20,24H4c-2.2,0-4-1.8-4-4V4c0-2.2,1.8-4,4-4h16c2.2,0,4,1.8,4,4v16C24,22.2,22.2,24,20,24z M4,2C2.9,2,2,2.9,2,4v16
|
||||||
|
c0,1.1,0.9,2,2,2h16c1.1,0,2-0.9,2-2V4c0-1.1-0.9-2-2-2H4z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M23,9H1C0.4,9,0,8.6,0,8s0.4-1,1-1h22c0.6,0,1,0.4,1,1S23.6,9,23,9z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M15,18c-0.3,0-0.5-0.1-0.7-0.3l-3-3c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l3,3c0.4,0.4,0.4,1,0,1.4C15.5,17.9,15.3,18,15,18z
|
||||||
|
"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M9,18c-0.3,0-0.5-0.1-0.7-0.3c-0.4-0.4-0.4-1,0-1.4l3-3c0.4-0.4,1-0.4,1.4,0s0.4,1,0,1.4l-3,3C9.5,17.9,9.3,18,9,18z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -1,38 +1,55 @@
|
||||||
import { useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
|
import { TextImportModal } from "./TextImportModal.jsx";
|
||||||
import { parseColonImportLines } from "./textImport.js";
|
import { parseColonImportLines } from "./textImport.js";
|
||||||
|
|
||||||
export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
||||||
const data = context.normalizeChecklistData(context.getModuleData(toolboxId, moduleId, { items: [] }));
|
const data = context.normalizeChecklistData(context.getModuleData(toolboxId, moduleId, { items: [] }));
|
||||||
const textContent = context.moduleText?.checklist || {};
|
const textContent = context.moduleText?.checklist || {};
|
||||||
const [label, setLabel] = useState("");
|
const [label, setLabel] = useState("");
|
||||||
|
const [sectionTitle, setSectionTitle] = useState("");
|
||||||
const [qty, setQty] = useState(1);
|
const [qty, setQty] = useState(1);
|
||||||
const [textImport, setTextImport] = useState("");
|
const [textImport, setTextImport] = useState("");
|
||||||
|
const [importOpen, setImportOpen] = useState(false);
|
||||||
|
const closeImportModal = useCallback(() => setImportOpen(false), []);
|
||||||
|
const completedNamedSections = data.sections.filter((section) => section.title && isSectionComplete(section, context));
|
||||||
|
const allCompletedSectionsHidden = completedNamedSections.length > 0 && completedNamedSections.every((section) => getSectionHideWhenComplete(section, data.hideCompletedSections));
|
||||||
|
|
||||||
function save(items) {
|
function save(sections, settings = {}) {
|
||||||
context.setModuleData(toolboxId, moduleId, { items });
|
context.setModuleData(toolboxId, moduleId, { ...data, ...settings, sections });
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendItems(sections, title, items) {
|
||||||
|
const cleanTitle = title.trim();
|
||||||
|
const existingIndex = sections.findIndex((section) => section.title.toLowerCase() === cleanTitle.toLowerCase());
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
return sections.map((section, index) => index === existingIndex ? { ...section, items: [...section.items, ...items] } : section);
|
||||||
|
}
|
||||||
|
return [...sections, { id: context.uid("section"), title: cleanTitle, items }];
|
||||||
}
|
}
|
||||||
|
|
||||||
function addItem(event) {
|
function addItem(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const cleanLabel = label.trim();
|
const cleanLabel = label.trim();
|
||||||
if (!cleanLabel) return;
|
if (!cleanLabel) return;
|
||||||
save([...data.items, { id: context.uid("item"), label: cleanLabel, qtyTarget: Math.max(1, Number(qty) || 1), qtyCurrent: 0 }]);
|
const nextItem = { id: context.uid("item"), label: cleanLabel, qtyTarget: Math.max(1, Number(qty) || 1), qtyCurrent: 0 };
|
||||||
|
save(appendItems(data.sections, sectionTitle, [nextItem]));
|
||||||
setLabel("");
|
setLabel("");
|
||||||
setQty(1);
|
setQty(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function importItems(event) {
|
function importItems(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const imported = parseColonImportLines(textImport).map((entry) => ({
|
const importedSections = parseChecklistImport(textImport, context);
|
||||||
id: context.uid("item"),
|
if (!importedSections.length) return;
|
||||||
label: entry.label,
|
const nextSections = importedSections.reduce((sections, section) => appendItems(sections, section.title, section.items), data.sections);
|
||||||
qtyTarget: Math.max(1, Number.parseInt(entry.value, 10) || 1),
|
save(nextSections);
|
||||||
qtyCurrent: 0
|
|
||||||
}));
|
|
||||||
if (!imported.length) return;
|
|
||||||
save([...data.items, ...imported]);
|
|
||||||
setTextImport("");
|
setTextImport("");
|
||||||
|
setImportOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHideCompletedSections(hideCompletedSections) {
|
||||||
|
save(data.sections.map((section) => ({ ...section, hideWhenComplete: undefined })), { hideCompletedSections });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -40,30 +57,156 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
||||||
{editing && (
|
{editing && (
|
||||||
<div className="module-add-panel">
|
<div className="module-add-panel">
|
||||||
<form className="inline-form checklist-add-form" onSubmit={addItem}>
|
<form className="inline-form checklist-add-form" onSubmit={addItem}>
|
||||||
|
<input name="section" placeholder={textContent.sectionPlaceholder || "Catégorie"} value={sectionTitle} onChange={(event) => setSectionTitle(event.target.value)} />
|
||||||
<input name="label" placeholder={textContent.itemPlaceholder || "Nouvel item"} value={label} onChange={(event) => setLabel(event.target.value)} />
|
<input name="label" placeholder={textContent.itemPlaceholder || "Nouvel item"} value={label} onChange={(event) => setLabel(event.target.value)} />
|
||||||
<input className="checklist-qty-input" name="qty" type="number" min="1" value={qty} onChange={(event) => setQty(event.target.value)} aria-label={textContent.quantityLabel || "Quantité cible"} />
|
<input className="checklist-qty-input" name="qty" type="number" min="1" value={qty} onChange={(event) => setQty(event.target.value)} aria-label={textContent.quantityLabel || "Quantité cible"} />
|
||||||
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
||||||
</form>
|
</form>
|
||||||
<form className="text-import-form" onSubmit={importItems}>
|
<div className="text-import-actions">
|
||||||
<textarea
|
<button type="button" onClick={() => setImportOpen(true)}>
|
||||||
value={textImport}
|
<Icon name="import" />
|
||||||
onChange={(event) => setTextImport(event.target.value)}
|
<span>{textContent.importOpenButton || "Importer du texte"}</span>
|
||||||
placeholder={textContent.importPlaceholder || "Importer plusieurs items...\nPotion:10\nMéga potion:5"}
|
</button>
|
||||||
rows={3}
|
</div>
|
||||||
/>
|
</div>
|
||||||
<button type="submit">{textContent.importButton || "Importer le texte"}</button>
|
)}
|
||||||
</form>
|
{importOpen && (
|
||||||
|
<TextImportModal
|
||||||
|
title={textContent.importModalTitle || "Importer une checklist"}
|
||||||
|
value={textImport}
|
||||||
|
onChange={setTextImport}
|
||||||
|
placeholder={textContent.importPlaceholder || "Importer plusieurs items...\nPotion:10\nMéga potion:5"}
|
||||||
|
submitLabel={textContent.importButton || "Importer le texte"}
|
||||||
|
onSubmit={importItems}
|
||||||
|
onClose={closeImportModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{completedNamedSections.length > 0 && (
|
||||||
|
<div className="checklist-toolbar">
|
||||||
|
<button
|
||||||
|
className={`checklist-hide-toggle ${allCompletedSectionsHidden ? "active" : ""}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setHideCompletedSections(!allCompletedSectionsHidden)}
|
||||||
|
aria-pressed={allCompletedSectionsHidden}
|
||||||
|
title={textContent.hideCompletedSectionsTitle || "Masquer les catégories terminées"}
|
||||||
|
aria-label={textContent.hideCompletedSectionsTitle || "Masquer les catégories terminées"}
|
||||||
|
>
|
||||||
|
<Icon name="hide" />
|
||||||
|
<i aria-hidden="true" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ul className="checklist">
|
<ul className="checklist">
|
||||||
{data.items.map((item) => (
|
{data.sections.map((section) => (
|
||||||
<ChecklistItem key={item.id} item={item} toolboxId={toolboxId} moduleId={moduleId} context={context} items={data.items} save={save} />
|
<ChecklistSection key={section.id} section={section} sections={data.sections} data={data} context={context} save={save} />
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseChecklistImport(text, context) {
|
||||||
|
const sections = [];
|
||||||
|
let currentSection = { id: context.uid("section"), title: "", items: [] };
|
||||||
|
|
||||||
|
String(text || "").split(/\r?\n/).forEach((line) => {
|
||||||
|
const cleanLine = line.trim();
|
||||||
|
if (!cleanLine) return;
|
||||||
|
if (cleanLine.startsWith("#")) {
|
||||||
|
if (currentSection.items.length || currentSection.title) sections.push(currentSection);
|
||||||
|
currentSection = { id: context.uid("section"), title: cleanLine.replace(/^#+/, "").trim(), items: [] };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parseColonImportLines(cleanLine).forEach((entry) => {
|
||||||
|
currentSection.items.push({
|
||||||
|
id: context.uid("item"),
|
||||||
|
label: entry.label,
|
||||||
|
qtyTarget: Math.max(1, Number.parseInt(entry.value, 10) || 1),
|
||||||
|
qtyCurrent: 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentSection.items.length || currentSection.title) sections.push(currentSection);
|
||||||
|
return sections.filter((section) => section.items.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSectionComplete(section, context) {
|
||||||
|
return section.items.length > 0 && section.items.every((item) => context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSectionHideWhenComplete(section, globalHide) {
|
||||||
|
return typeof section.hideWhenComplete === "boolean" ? section.hideWhenComplete : globalHide;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChecklistSection({ section, context, sections, data, save }) {
|
||||||
|
const showTitle = Boolean(section.title);
|
||||||
|
const sectionComplete = showTitle && isSectionComplete(section, context);
|
||||||
|
const hideWhenComplete = getSectionHideWhenComplete(section, data.hideCompletedSections);
|
||||||
|
const isAutoCollapsed = sectionComplete && hideWhenComplete;
|
||||||
|
const isCollapsed = Boolean(section.collapsed) || isAutoCollapsed;
|
||||||
|
|
||||||
|
function saveItems(items) {
|
||||||
|
save(sections.map((entry) => entry.id === section.id ? { ...entry, items } : entry).filter((entry) => entry.items.length || entry.title));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSectionCollapsed() {
|
||||||
|
const nextCollapsed = !isCollapsed;
|
||||||
|
const nextSections = sections.map((entry) => {
|
||||||
|
if (entry.id === section.id) {
|
||||||
|
const nextSection = { ...entry };
|
||||||
|
if (nextCollapsed) {
|
||||||
|
if (sectionComplete) {
|
||||||
|
nextSection.hideWhenComplete = true;
|
||||||
|
delete nextSection.collapsed;
|
||||||
|
} else {
|
||||||
|
nextSection.collapsed = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete nextSection.collapsed;
|
||||||
|
if (sectionComplete) nextSection.hideWhenComplete = false;
|
||||||
|
}
|
||||||
|
return nextSection;
|
||||||
|
}
|
||||||
|
if (data.hideCompletedSections && !nextCollapsed && entry.title && isSectionComplete(entry, context)) return { ...entry, hideWhenComplete: true };
|
||||||
|
return entry;
|
||||||
|
});
|
||||||
|
const nextCompletedSections = nextSections.filter((entry) => entry.title && isSectionComplete(entry, context));
|
||||||
|
const allHidden = nextCompletedSections.length > 0 && nextCompletedSections.every((entry) => getSectionHideWhenComplete(entry, false));
|
||||||
|
save(nextSections, { hideCompletedSections: allHidden });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={`checklist-section ${showTitle ? "is-grouped" : ""} ${isCollapsed ? "is-collapsed" : ""}`}>
|
||||||
|
{showTitle && (
|
||||||
|
<div className="checklist-section-header">
|
||||||
|
<h3>{section.title || "Sans catégorie"}</h3>
|
||||||
|
<div>
|
||||||
|
<span>{section.items.filter((item) => context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget).length} / {section.items.length}</span>
|
||||||
|
<button
|
||||||
|
className="checklist-section-collapse-button"
|
||||||
|
type="button"
|
||||||
|
onClick={toggleSectionCollapsed}
|
||||||
|
aria-expanded={!isCollapsed}
|
||||||
|
title={isCollapsed ? context.moduleText?.checklist?.showCompletedSectionTitle || "Afficher cette catégorie" : context.moduleText?.checklist?.hideCompletedSectionTitle || "Masquer cette catégorie"}
|
||||||
|
aria-label={isCollapsed ? context.moduleText?.checklist?.showCompletedSectionTitle || "Afficher cette catégorie" : context.moduleText?.checklist?.hideCompletedSectionTitle || "Masquer cette catégorie"}
|
||||||
|
>
|
||||||
|
<Icon name={isCollapsed ? "chevron-down" : "chevron-up"} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isCollapsed && (
|
||||||
|
<ul className="checklist-section-items">
|
||||||
|
{section.items.map((item) => (
|
||||||
|
<ChecklistItem key={item.id} item={item} context={context} items={section.items} save={saveItems} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ChecklistItem({ item, context, items, save }) {
|
function ChecklistItem({ item, context, items, save }) {
|
||||||
const done = context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget;
|
const done = context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
|
import { TextImportModal } from "./TextImportModal.jsx";
|
||||||
import { parseColonImportLines } from "./textImport.js";
|
import { parseColonImportLines } from "./textImport.js";
|
||||||
|
|
||||||
export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
||||||
|
|
@ -8,7 +9,9 @@ export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [url, setUrl] = useState("");
|
const [url, setUrl] = useState("");
|
||||||
const [textImport, setTextImport] = useState("");
|
const [textImport, setTextImport] = useState("");
|
||||||
|
const [importOpen, setImportOpen] = useState(false);
|
||||||
const [copiedId, setCopiedId] = useState("");
|
const [copiedId, setCopiedId] = useState("");
|
||||||
|
const closeImportModal = useCallback(() => setImportOpen(false), []);
|
||||||
|
|
||||||
function save(links) {
|
function save(links) {
|
||||||
context.setModuleData(toolboxId, moduleId, { links });
|
context.setModuleData(toolboxId, moduleId, { links });
|
||||||
|
|
@ -43,6 +46,7 @@ export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
||||||
if (!imported.length) return;
|
if (!imported.length) return;
|
||||||
save([...data.links, ...imported]);
|
save([...data.links, ...imported]);
|
||||||
setTextImport("");
|
setTextImport("");
|
||||||
|
setImportOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyUrl(link) {
|
async function copyUrl(link) {
|
||||||
|
|
@ -61,17 +65,25 @@ export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
||||||
<input name="url" placeholder={textContent.urlPlaceholder || "https://..."} value={url} onChange={(event) => setUrl(event.target.value)} />
|
<input name="url" placeholder={textContent.urlPlaceholder || "https://..."} value={url} onChange={(event) => setUrl(event.target.value)} />
|
||||||
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
<button className="primary">{textContent.addButton || "Ajouter"}</button>
|
||||||
</form>
|
</form>
|
||||||
<form className="text-import-form" onSubmit={importLinks}>
|
<div className="text-import-actions">
|
||||||
<textarea
|
<button type="button" onClick={() => setImportOpen(true)}>
|
||||||
value={textImport}
|
<Icon name="import" />
|
||||||
onChange={(event) => setTextImport(event.target.value)}
|
<span>{textContent.importOpenButton || "Importer du texte"}</span>
|
||||||
placeholder={textContent.importPlaceholder || "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map"}
|
</button>
|
||||||
rows={3}
|
</div>
|
||||||
/>
|
|
||||||
<button type="submit">{textContent.importButton || "Importer le texte"}</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{importOpen && (
|
||||||
|
<TextImportModal
|
||||||
|
title={textContent.importModalTitle || "Importer des liens"}
|
||||||
|
value={textImport}
|
||||||
|
onChange={setTextImport}
|
||||||
|
placeholder={textContent.importPlaceholder || "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map"}
|
||||||
|
submitLabel={textContent.importButton || "Importer le texte"}
|
||||||
|
onSubmit={importLinks}
|
||||||
|
onClose={closeImportModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ul className="links-list">
|
<ul className="links-list">
|
||||||
{data.links.map((link) => (
|
{data.links.map((link) => (
|
||||||
<li className="link-item" key={link.id}>
|
<li className="link-item" key={link.id}>
|
||||||
|
|
|
||||||
47
website/src/features/toolboxes/modules/TextImportModal.jsx
Normal file
47
website/src/features/toolboxes/modules/TextImportModal.jsx
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
|
import { lockBodyScroll } from "../../../utils/bodyScrollLock.js";
|
||||||
|
|
||||||
|
export function TextImportModal({ title, value, placeholder, submitLabel, onChange, onSubmit, onClose }) {
|
||||||
|
useEffect(() => {
|
||||||
|
function handleKeyDown(event) {
|
||||||
|
if (event.key === "Escape") onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
const unlock = lockBodyScroll();
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
unlock();
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div className="confirm-modal-root text-import-modal-root">
|
||||||
|
<div className="confirm-backdrop" onClick={onClose} />
|
||||||
|
<section className="confirm-modal text-import-modal-dialog" role="dialog" aria-modal="true" aria-labelledby="text-import-title">
|
||||||
|
<header>
|
||||||
|
<h2 id="text-import-title">{title}</h2>
|
||||||
|
<button className="drawer-close-button" type="button" onClick={onClose} aria-label="Fermer" title="Fermer">
|
||||||
|
<Icon name="close" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<form className="text-import-modal-form" onSubmit={onSubmit}>
|
||||||
|
<textarea
|
||||||
|
autoFocus
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
rows={9}
|
||||||
|
/>
|
||||||
|
<footer>
|
||||||
|
<button type="button" onClick={onClose}>Annuler</button>
|
||||||
|
<button className="primary" type="submit">{submitLabel}</button>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -13,10 +13,10 @@ import { ScreenshotsModule } from "./ScreenshotsModule.jsx";
|
||||||
|
|
||||||
const MODULE_COMPONENTS = {
|
const MODULE_COMPONENTS = {
|
||||||
notepad: { label: "Bloc notes", icon: "notepad", Component: NotepadModule, editable: false },
|
notepad: { label: "Bloc notes", icon: "notepad", Component: NotepadModule, editable: false },
|
||||||
checklist: { label: "Checklist", icon: "checklist", Component: ChecklistModule, editable: true },
|
checklist: { label: "Checklist", icon: "checklist", Component: ChecklistModule, editable: true, scrollable: true },
|
||||||
screenshots: { label: "Images", icon: "picture", Component: ScreenshotsModule, editable: true },
|
screenshots: { label: "Images", icon: "picture", Component: ScreenshotsModule, editable: true, scrollable: true },
|
||||||
links: { label: "Liens", icon: "link", Component: LinksModule, editable: true },
|
links: { label: "Liens", icon: "link", Component: LinksModule, editable: true, scrollable: true },
|
||||||
counters: { label: "Compteurs", icon: "abacus", Component: CountersModule, editable: true },
|
counters: { label: "Compteurs", icon: "abacus", Component: CountersModule, editable: true, scrollable: true },
|
||||||
calculator: { label: "Calculateur", icon: "calculator", Component: CalculatorModule, editable: false },
|
calculator: { label: "Calculateur", icon: "calculator", Component: CalculatorModule, editable: false },
|
||||||
imageAnnotation: { label: "Annotation d'images", icon: "map", Component: ImageAnnotationModule, editable: true }
|
imageAnnotation: { label: "Annotation d'images", icon: "map", Component: ImageAnnotationModule, editable: true }
|
||||||
};
|
};
|
||||||
|
|
@ -184,7 +184,7 @@ export function AddToolControls({ onAdd }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onDelete, onMove }) {
|
export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onUpdateModule, onDelete, onMove }) {
|
||||||
const [measuredSplitIndex, setMeasuredSplitIndex] = useState(() => Math.ceil(toolbox.modules.length / 2));
|
const [measuredSplitIndex, setMeasuredSplitIndex] = useState(() => Math.ceil(toolbox.modules.length / 2));
|
||||||
const moduleElementsRef = useRef(new Map());
|
const moduleElementsRef = useRef(new Map());
|
||||||
const moduleIdSignature = useMemo(() => toolbox.modules.map((module) => module.id).join("|"), [toolbox.modules]);
|
const moduleIdSignature = useMemo(() => toolbox.modules.map((module) => module.id).join("|"), [toolbox.modules]);
|
||||||
|
|
@ -259,6 +259,7 @@ export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onDe
|
||||||
dropTarget={dropTarget}
|
dropTarget={dropTarget}
|
||||||
onDragStart={startModuleDrag}
|
onDragStart={startModuleDrag}
|
||||||
onRename={onRename}
|
onRename={onRename}
|
||||||
|
onUpdateModule={onUpdateModule}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onMove={onMove}
|
onMove={onMove}
|
||||||
registerModuleElement={registerModuleElement}
|
registerModuleElement={registerModuleElement}
|
||||||
|
|
@ -285,6 +286,7 @@ export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onDe
|
||||||
dropTarget={dropTarget}
|
dropTarget={dropTarget}
|
||||||
onDragStart={startModuleDrag}
|
onDragStart={startModuleDrag}
|
||||||
onRename={onRename}
|
onRename={onRename}
|
||||||
|
onUpdateModule={onUpdateModule}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onMove={onMove}
|
onMove={onMove}
|
||||||
registerModuleElement={registerModuleElement}
|
registerModuleElement={registerModuleElement}
|
||||||
|
|
@ -296,18 +298,20 @@ export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onDe
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModuleShell({ toolbox, module, context, draggingModuleId, dropTarget, onDragStart, onRename, onDelete, registerModuleElement }) {
|
function ModuleShell({ toolbox, module, context, draggingModuleId, dropTarget, onDragStart, onRename, onUpdateModule, onDelete, registerModuleElement }) {
|
||||||
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
||||||
const Component = definition.Component;
|
const Component = definition.Component;
|
||||||
const label = definition.label || module.type;
|
const label = definition.label || module.type;
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
|
const scrollable = definition.scrollable && module.scrollable === true;
|
||||||
const isDragging = draggingModuleId === module.id;
|
const isDragging = draggingModuleId === module.id;
|
||||||
const isDropTarget = dropTarget.id === module.id;
|
const isDropTarget = dropTarget.id === module.id;
|
||||||
const className = [
|
const className = [
|
||||||
"module",
|
"module",
|
||||||
isDragging ? "is-dragging" : "",
|
isDragging ? "is-dragging" : "",
|
||||||
isDropTarget ? "is-drop-target" : "",
|
isDropTarget ? "is-drop-target" : "",
|
||||||
isDropTarget && dropTarget.placement === "after" ? "drop-after" : ""
|
isDropTarget && dropTarget.placement === "after" ? "drop-after" : "",
|
||||||
|
scrollable ? "is-scrollable" : ""
|
||||||
].filter(Boolean).join(" ");
|
].filter(Boolean).join(" ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -334,6 +338,26 @@ function ModuleShell({ toolbox, module, context, draggingModuleId, dropTarget, o
|
||||||
<EditableModuleTitle value={module.title || label} fallback={label} onSave={(title) => onRename(module.id, title)} />
|
<EditableModuleTitle value={module.title || label} fallback={label} onSave={(title) => onRename(module.id, title)} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
{definition.scrollable && (
|
||||||
|
<button
|
||||||
|
className={`module-scroll-button ${scrollable ? "active" : ""}`}
|
||||||
|
onClick={() => onUpdateModule(module.id, (currentModule) => {
|
||||||
|
const nextModule = { ...currentModule };
|
||||||
|
if (scrollable) {
|
||||||
|
delete nextModule.scrollable;
|
||||||
|
} else {
|
||||||
|
nextModule.scrollable = true;
|
||||||
|
}
|
||||||
|
return nextModule;
|
||||||
|
})}
|
||||||
|
aria-label={`${scrollable ? "Désactiver" : "Activer"} le scroll de ${module.title || label}`}
|
||||||
|
aria-pressed={scrollable}
|
||||||
|
title={scrollable ? "Désactiver le scroll" : "Activer le scroll"}
|
||||||
|
>
|
||||||
|
<Icon name="scrollable" />
|
||||||
|
<i aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{definition.editable && (
|
{definition.editable && (
|
||||||
<button
|
<button
|
||||||
className={`module-edit-button ${editing ? "active" : ""}`}
|
className={`module-edit-button ${editing ? "active" : ""}`}
|
||||||
|
|
@ -350,7 +374,9 @@ function ModuleShell({ toolbox, module, context, draggingModuleId, dropTarget, o
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<Component toolboxId={toolbox.id} moduleId={module.id} context={context} editing={editing} />
|
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||||
|
<Component toolboxId={toolbox.id} moduleId={module.id} context={context} editing={editing} />
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import {
|
||||||
const APP_STORAGE_WARNING_RATIO = 0.85;
|
const APP_STORAGE_WARNING_RATIO = 0.85;
|
||||||
const APP_STORAGE_SOFT_LIMIT_BYTES = 250 * 1024 * 1024;
|
const APP_STORAGE_SOFT_LIMIT_BYTES = 250 * 1024 * 1024;
|
||||||
const DRAWER_WIDTH_SETTING = "drawerWidth";
|
const DRAWER_WIDTH_SETTING = "drawerWidth";
|
||||||
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", shot: "s", link: "l", counter: "c", calc: "r", marker: "k" };
|
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", section: "g", shot: "s", link: "l", counter: "c", calc: "r", marker: "k" };
|
||||||
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
const TOOLBOX_ICON_BASE = "/static/img/toolbox-icons/";
|
const TOOLBOX_ICON_BASE = "/static/img/toolbox-icons/";
|
||||||
const TOOLBOX_ICON_FILES = [
|
const TOOLBOX_ICON_FILES = [
|
||||||
|
|
@ -127,10 +127,16 @@ const DEFAULT_SITE_CONTENT = {
|
||||||
notepad: { placeholder: "Notes rapides..." },
|
notepad: { placeholder: "Notes rapides..." },
|
||||||
checklist: {
|
checklist: {
|
||||||
itemPlaceholder: "Nouvel item",
|
itemPlaceholder: "Nouvel item",
|
||||||
|
sectionPlaceholder: "Catégorie",
|
||||||
quantityLabel: "Quantité cible",
|
quantityLabel: "Quantité cible",
|
||||||
addButton: "Ajouter",
|
addButton: "Ajouter",
|
||||||
importPlaceholder: "Importer plusieurs items...\nPotion:10\nMéga potion:5",
|
importPlaceholder: "Importer plusieurs items...\n# Potions\nPotion:10\nMéga potion:5",
|
||||||
|
importOpenButton: "Importer du texte",
|
||||||
|
importModalTitle: "Importer une checklist",
|
||||||
importButton: "Importer le texte",
|
importButton: "Importer le texte",
|
||||||
|
hideCompletedSectionsTitle: "Masquer les catégories terminées",
|
||||||
|
showCompletedSectionTitle: "Afficher cette catégorie terminée",
|
||||||
|
hideCompletedSectionTitle: "Masquer cette catégorie terminée",
|
||||||
decrementLabel: "Retirer une quantité",
|
decrementLabel: "Retirer une quantité",
|
||||||
incrementLabel: "Ajouter une quantité",
|
incrementLabel: "Ajouter une quantité",
|
||||||
currentQuantityLabel: "Quantité actuelle",
|
currentQuantityLabel: "Quantité actuelle",
|
||||||
|
|
@ -152,6 +158,8 @@ const DEFAULT_SITE_CONTENT = {
|
||||||
urlPlaceholder: "https://...",
|
urlPlaceholder: "https://...",
|
||||||
addButton: "Ajouter",
|
addButton: "Ajouter",
|
||||||
importPlaceholder: "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map",
|
importPlaceholder: "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map",
|
||||||
|
importOpenButton: "Importer du texte",
|
||||||
|
importModalTitle: "Importer des liens",
|
||||||
importButton: "Importer le texte",
|
importButton: "Importer le texte",
|
||||||
copyTitle: "Copier",
|
copyTitle: "Copier",
|
||||||
copiedTitle: "Copié",
|
copiedTitle: "Copié",
|
||||||
|
|
@ -251,6 +259,7 @@ function normalizeToolboxModule(module) {
|
||||||
const title = String(module.title || "").trim();
|
const title = String(module.title || "").trim();
|
||||||
const normalized = { id: module.id || uid("mod"), type: module.type };
|
const normalized = { id: module.id || uid("mod"), type: module.type };
|
||||||
if (title && title !== getDefaultModuleTitle(module.type)) normalized.title = title;
|
if (title && title !== getDefaultModuleTitle(module.type)) normalized.title = title;
|
||||||
|
if (module.scrollable === true) normalized.scrollable = true;
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,10 +323,26 @@ function normalizeChecklistItem(item) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeChecklistData(data) {
|
function normalizeChecklistSection(section, fallbackTitle = "") {
|
||||||
return {
|
const normalized = {
|
||||||
items: (data?.items || []).map(normalizeChecklistItem).filter((item) => item.label)
|
id: section?.id || uid("section"),
|
||||||
|
title: String(section?.title ?? fallbackTitle).trim(),
|
||||||
|
items: (section?.items || []).map(normalizeChecklistItem).filter((item) => item.label)
|
||||||
};
|
};
|
||||||
|
if (typeof section?.hideWhenComplete === "boolean") normalized.hideWhenComplete = section.hideWhenComplete;
|
||||||
|
if (section?.collapsed === true) normalized.collapsed = true;
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeChecklistData(data) {
|
||||||
|
const hideCompletedSections = Boolean(data?.hideCompletedSections);
|
||||||
|
const sections = Array.isArray(data?.sections)
|
||||||
|
? data.sections.map((section) => normalizeChecklistSection(section)).filter((section) => section.items.length || section.title)
|
||||||
|
: [];
|
||||||
|
const legacyItems = (data?.items || []).map(normalizeChecklistItem).filter((item) => item.label);
|
||||||
|
if (!sections.length && legacyItems.length) return { hideCompletedSections, sections: [{ id: uid("section"), title: "", items: legacyItems }], items: legacyItems };
|
||||||
|
const items = sections.flatMap((section) => section.items);
|
||||||
|
return { hideCompletedSections, sections, items };
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeUrl(value) {
|
function normalizeUrl(value) {
|
||||||
|
|
@ -420,14 +445,30 @@ function compactChecklistItemForStorage(item) {
|
||||||
return compact;
|
return compact;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compactChecklistSectionForStorage(section) {
|
||||||
|
const normalized = normalizeChecklistSection(section);
|
||||||
|
const compact = {
|
||||||
|
id: normalized.id,
|
||||||
|
items: normalized.items.map(compactChecklistItemForStorage)
|
||||||
|
};
|
||||||
|
if (normalized.title) compact.title = normalized.title;
|
||||||
|
if (typeof normalized.hideWhenComplete === "boolean") compact.hideWhenComplete = normalized.hideWhenComplete;
|
||||||
|
if (normalized.collapsed === true) compact.collapsed = true;
|
||||||
|
return compact;
|
||||||
|
}
|
||||||
|
|
||||||
function compactModuleDataForStorage(type, value) {
|
function compactModuleDataForStorage(type, value) {
|
||||||
if (type === "notepad") {
|
if (type === "notepad") {
|
||||||
const text = String(value?.text || "");
|
const text = String(value?.text || "");
|
||||||
return text ? { text } : null;
|
return text ? { text } : null;
|
||||||
}
|
}
|
||||||
if (type === "checklist") {
|
if (type === "checklist") {
|
||||||
const items = normalizeChecklistData(value).items.map(compactChecklistItemForStorage);
|
const normalized = normalizeChecklistData(value);
|
||||||
return items.length ? { items } : null;
|
const sections = normalized.sections.map(compactChecklistSectionForStorage).filter((section) => section.items.length || section.title);
|
||||||
|
if (!sections.length) return null;
|
||||||
|
const settings = normalized.hideCompletedSections ? { hideCompletedSections: true } : {};
|
||||||
|
if (sections.length === 1 && !sections[0].title && typeof sections[0].hideWhenComplete !== "boolean") return { ...settings, items: sections[0].items };
|
||||||
|
return { ...settings, sections };
|
||||||
}
|
}
|
||||||
if (type === "screenshots") {
|
if (type === "screenshots") {
|
||||||
const shots = (Array.isArray(value?.shots) ? value.shots : [])
|
const shots = (Array.isArray(value?.shots) ? value.shots : [])
|
||||||
|
|
@ -500,6 +541,15 @@ function remapModuleDataForExport(type, data, nextId) {
|
||||||
const compact = compactModuleDataForStorage(type, data);
|
const compact = compactModuleDataForStorage(type, data);
|
||||||
if (!compact) return null;
|
if (!compact) return null;
|
||||||
if (type === "checklist") {
|
if (type === "checklist") {
|
||||||
|
if (compact.sections) {
|
||||||
|
return {
|
||||||
|
sections: compact.sections.map((section) => ({
|
||||||
|
...section,
|
||||||
|
id: nextId("section"),
|
||||||
|
items: section.items.map((item) => ({ ...item, id: nextId("item") }))
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
return { items: compact.items.map((item) => ({ ...item, id: nextId("item") })) };
|
return { items: compact.items.map((item) => ({ ...item, id: nextId("item") })) };
|
||||||
}
|
}
|
||||||
if (type === "screenshots") {
|
if (type === "screenshots") {
|
||||||
|
|
@ -1568,6 +1618,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
||||||
moduleColumns={moduleColumns}
|
moduleColumns={moduleColumns}
|
||||||
context={moduleContext}
|
context={moduleContext}
|
||||||
onRename={(moduleId, title) => updateToolbox({ ...toolbox, modules: toolbox.modules.map((module) => module.id === moduleId ? { ...module, title } : module) })}
|
onRename={(moduleId, title) => updateToolbox({ ...toolbox, modules: toolbox.modules.map((module) => module.id === moduleId ? { ...module, title } : module) })}
|
||||||
|
onUpdateModule={(moduleId, updater) => updateToolbox({ ...toolbox, modules: toolbox.modules.map((module) => module.id === moduleId ? updater(module) : module) })}
|
||||||
onDelete={(moduleId) => actions.setConfirmModal({
|
onDelete={(moduleId) => actions.setConfirmModal({
|
||||||
title: "Retirer l'outil",
|
title: "Retirer l'outil",
|
||||||
message: "Retirer cet outil de la toolbox ?",
|
message: "Retirer cet outil de la toolbox ?",
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,21 @@
|
||||||
-webkit-mask-image: url("/static/icons/scrollable.svg");
|
-webkit-mask-image: url("/static/icons/scrollable.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-icon-hide {
|
||||||
|
mask-image: url("/static/icons/hide.svg");
|
||||||
|
-webkit-mask-image: url("/static/icons/hide.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-icon-chevron-down {
|
||||||
|
mask-image: url("/static/icons/chevron-down.svg");
|
||||||
|
-webkit-mask-image: url("/static/icons/chevron-down.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-icon-chevron-up {
|
||||||
|
mask-image: url("/static/icons/chevron-up.svg");
|
||||||
|
-webkit-mask-image: url("/static/icons/chevron-up.svg");
|
||||||
|
}
|
||||||
|
|
||||||
.ui-icon-drag {
|
.ui-icon-drag {
|
||||||
mask-image: url("/static/icons/drag.svg");
|
mask-image: url("/static/icons/drag.svg");
|
||||||
-webkit-mask-image: url("/static/icons/drag.svg");
|
-webkit-mask-image: url("/static/icons/drag.svg");
|
||||||
|
|
|
||||||
|
|
@ -389,6 +389,16 @@
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-content {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-content.is-scrollable {
|
||||||
|
max-height: 450px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
.layout-switch {
|
.layout-switch {
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
grid-template-columns: repeat(2, 38px);
|
grid-template-columns: repeat(2, 38px);
|
||||||
|
|
@ -479,6 +489,52 @@
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-scroll-button {
|
||||||
|
display: inline-grid;
|
||||||
|
grid-template-columns: 16px 30px;
|
||||||
|
width: fit-content;
|
||||||
|
min-height: 40px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
padding: 0 7px 0 10px;
|
||||||
|
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: rgba(5, 7, 17, 0.24);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-scroll-button .ui-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-scroll-button i {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 30px;
|
||||||
|
height: 16px;
|
||||||
|
border: 1px solid rgba(165, 180, 252, 0.18);
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: rgba(7, 10, 24, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-scroll-button i::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 3px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-text-muted);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
transition:
|
||||||
|
left var(--duration-fast) var(--ease-standard),
|
||||||
|
background-color var(--duration-fast) var(--ease-standard),
|
||||||
|
box-shadow var(--duration-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
.module-edit-button.active {
|
.module-edit-button.active {
|
||||||
border-color: var(--color-primary-border);
|
border-color: var(--color-primary-border);
|
||||||
background: var(--color-primary-soft);
|
background: var(--color-primary-soft);
|
||||||
|
|
@ -577,7 +633,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.checklist-add-form {
|
.checklist-add-form {
|
||||||
grid-template-columns: minmax(0, 1fr) 76px auto;
|
grid-template-columns: minmax(120px, 0.45fr) minmax(0, 1fr) 76px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-add-panel {
|
.module-add-panel {
|
||||||
|
|
@ -590,20 +646,57 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-import-form {
|
.text-import-actions {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
justify-content: flex-end;
|
||||||
gap: 10px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-import-form textarea {
|
.text-import-actions button {
|
||||||
min-height: 86px;
|
display: inline-flex;
|
||||||
|
min-height: 42px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-import-actions .ui-icon {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-import-modal-dialog {
|
||||||
|
width: min(640px, calc(100vw - 32px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-import-modal-root {
|
||||||
|
z-index: 330;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-import-modal-dialog header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-4);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-import-modal-dialog h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-import-modal-form {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-import-modal-form textarea {
|
||||||
|
min-height: 260px;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-import-form button {
|
.text-import-modal-form footer {
|
||||||
align-self: end;
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checklist-qty-input {
|
.checklist-qty-input {
|
||||||
|
|
@ -638,12 +731,147 @@ textarea:focus {
|
||||||
|
|
||||||
.checklist {
|
.checklist {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: var(--space-3);
|
||||||
margin: var(--space-4) 0 0;
|
margin: var(--space-4) 0 0;
|
||||||
padding: 0 var(--space-4) var(--space-4);
|
padding: 0 var(--space-4) var(--space-4);
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checklist-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: var(--space-4) var(--space-4) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-section.is-grouped {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid rgba(246, 196, 83, 0.16);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(246, 196, 83, 0.04), transparent 42%),
|
||||||
|
rgba(7, 10, 24, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-section-header > div {
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-section-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-accent-gold);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-section.is-collapsed .checklist-section-header h3 {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-section-header span {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 3px 9px;
|
||||||
|
border: 1px solid rgba(246, 196, 83, 0.22);
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: rgba(246, 196, 83, 0.08);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-hide-toggle {
|
||||||
|
display: inline-grid;
|
||||||
|
grid-template-columns: 18px 34px;
|
||||||
|
width: fit-content;
|
||||||
|
min-height: 34px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 6px 0 10px;
|
||||||
|
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: rgba(5, 7, 17, 0.24);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-hide-toggle .ui-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-hide-toggle i {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 34px;
|
||||||
|
height: 18px;
|
||||||
|
border: 1px solid rgba(165, 180, 252, 0.18);
|
||||||
|
border-radius: var(--radius-pill);
|
||||||
|
background: rgba(7, 10, 24, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-hide-toggle i::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 3px;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-text-muted);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
transition:
|
||||||
|
left var(--duration-fast) var(--ease-standard),
|
||||||
|
background-color var(--duration-fast) var(--ease-standard),
|
||||||
|
box-shadow var(--duration-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-section-collapse-button {
|
||||||
|
display: inline-grid;
|
||||||
|
width: 30px;
|
||||||
|
min-width: 30px;
|
||||||
|
min-height: 30px;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid rgba(246, 196, 83, 0.28);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(246, 196, 83, 0.12), rgba(139, 92, 246, 0.08)) padding-box,
|
||||||
|
linear-gradient(135deg, rgba(246, 196, 83, 0.3), rgba(196, 181, 253, 0.18)) border-box;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-section-collapse-button .ui-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
opacity: 1;
|
||||||
|
filter: drop-shadow(0 0 7px rgba(255, 255, 255, 0.22));
|
||||||
|
}
|
||||||
|
|
||||||
|
.checklist-section-items {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
.checklist-item {
|
.checklist-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 42px;
|
grid-template-columns: 1fr 42px;
|
||||||
|
|
@ -998,8 +1226,12 @@ textarea:focus {
|
||||||
.calculator-entry > button:not(.calculator-entry-summary, .calculator-entry-value, .danger):hover,
|
.calculator-entry > button:not(.calculator-entry-summary, .calculator-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,
|
||||||
|
.module-scroll-button:hover,
|
||||||
|
.module-scroll-button.active,
|
||||||
.calculator-scroll-toggle:hover,
|
.calculator-scroll-toggle:hover,
|
||||||
.calculator-scroll-toggle.active {
|
.calculator-scroll-toggle.active,
|
||||||
|
.checklist-hide-toggle:hover,
|
||||||
|
.checklist-hide-toggle.active {
|
||||||
border-color: rgba(196, 181, 253, 0.32);
|
border-color: rgba(196, 181, 253, 0.32);
|
||||||
background:
|
background:
|
||||||
linear-gradient(135deg, rgba(31, 37, 68, 0.94), rgba(18, 22, 44, 0.96)) padding-box,
|
linear-gradient(135deg, rgba(31, 37, 68, 0.94), rgba(18, 22, 44, 0.96)) padding-box,
|
||||||
|
|
@ -1010,6 +1242,17 @@ textarea:focus {
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checklist-section-collapse-button:hover {
|
||||||
|
border-color: rgba(246, 196, 83, 0.58);
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(246, 196, 83, 0.18), rgba(139, 92, 246, 0.12)) padding-box,
|
||||||
|
linear-gradient(135deg, rgba(246, 196, 83, 0.62), rgba(196, 181, 253, 0.32)) border-box;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow:
|
||||||
|
0 0 10px rgba(246, 196, 83, 0.14),
|
||||||
|
0 0 16px rgba(139, 92, 246, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.calculator-action-button.danger,
|
.calculator-action-button.danger,
|
||||||
.calculator-entry > button.danger,
|
.calculator-entry > button.danger,
|
||||||
.counter-actions button.danger,
|
.counter-actions button.danger,
|
||||||
|
|
@ -1041,24 +1284,34 @@ textarea:focus {
|
||||||
filter: drop-shadow(0 0 6px rgba(251, 113, 133, 0.26));
|
filter: drop-shadow(0 0 6px rgba(251, 113, 133, 0.26));
|
||||||
}
|
}
|
||||||
|
|
||||||
.calculator-scroll-toggle.active {
|
.module-scroll-button.active,
|
||||||
|
.calculator-scroll-toggle.active,
|
||||||
|
.checklist-hide-toggle.active {
|
||||||
border-color: rgba(246, 196, 83, 0.34);
|
border-color: rgba(246, 196, 83, 0.34);
|
||||||
background:
|
background:
|
||||||
linear-gradient(135deg, rgba(72, 43, 160, 0.82), rgba(45, 52, 146, 0.8)) padding-box,
|
linear-gradient(135deg, rgba(72, 43, 160, 0.82), rgba(45, 52, 146, 0.8)) padding-box,
|
||||||
linear-gradient(135deg, rgba(246, 196, 83, 0.58), rgba(139, 92, 246, 0.32)) border-box;
|
linear-gradient(135deg, rgba(246, 196, 83, 0.58), rgba(139, 92, 246, 0.32)) border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calculator-scroll-toggle.active i {
|
.module-scroll-button.active i,
|
||||||
|
.calculator-scroll-toggle.active i,
|
||||||
|
.checklist-hide-toggle.active i {
|
||||||
border-color: rgba(246, 196, 83, 0.36);
|
border-color: rgba(246, 196, 83, 0.36);
|
||||||
background: rgba(246, 196, 83, 0.12);
|
background: rgba(246, 196, 83, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.calculator-scroll-toggle.active i::before {
|
.module-scroll-button.active i::before,
|
||||||
|
.calculator-scroll-toggle.active i::before,
|
||||||
|
.checklist-hide-toggle.active i::before {
|
||||||
left: 17px;
|
left: 17px;
|
||||||
background: var(--color-accent-gold);
|
background: var(--color-accent-gold);
|
||||||
box-shadow: 0 0 10px rgba(246, 196, 83, 0.28);
|
box-shadow: 0 0 10px rgba(246, 196, 83, 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-scroll-button.active i::before {
|
||||||
|
left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.calculator-tree {
|
.calculator-tree {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue