{item.title}
{item.text}
// Rôle : affiche la librairie des outils avec des exemples locaux non persistés. import React, { useEffect, useMemo, useState } from "react"; import { Icon } from "../components/Icon.jsx"; import { CalculatorModule } from "../features/toolboxes/modules/CalculatorModule.jsx"; import { ChecklistModule } from "../features/toolboxes/modules/ChecklistModule.jsx"; import { CombosModule } from "../features/toolboxes/modules/CombosModule.jsx"; import { CountersModule } from "../features/toolboxes/modules/CountersModule.jsx"; import { EquipmentPlannerModule } from "../features/toolboxes/modules/EquipmentPlannerModule.jsx"; import { ImageAnnotationModule } from "../features/toolboxes/modules/ImageAnnotationModule.jsx"; import { ImagesModule } from "../features/toolboxes/modules/ImagesModule.jsx"; import { LinksModule } from "../features/toolboxes/modules/LinksModule.jsx"; import { NotepadModule } from "../features/toolboxes/modules/NotepadModule.jsx"; import { TableModule } from "../features/toolboxes/modules/TableModule.jsx"; import { TaskPlannerModule } from "../features/toolboxes/modules/TaskPlannerModule.jsx"; import { TimerModule } from "../features/toolboxes/modules/TimerModule.jsx"; import { clampQty, hostnameFromUrl, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeImageAnnotationData, normalizeLinksData, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, summarizeEquipmentPlannerData, normalizeUrl, uid } from "../features/toolboxes/storage/toolboxStorage.js"; const LIBRARY_TOOLBOX_ID = "library"; const MODULE_COMPONENTS = { notepad: { label: "Bloc notes", mode: "Texte riche", icon: "notepad", Component: NotepadModule }, checklist: { label: "Checklist", mode: "Catégories + sans catégorie", icon: "checklist", Component: ChecklistModule, editable: true, scrollable: true }, images: { label: "Images", mode: "Média", icon: "picture", Component: ImagesModule, editable: true, scrollable: true }, links: { label: "Liens", mode: "Liste simple", icon: "link", Component: LinksModule, editable: true, scrollable: true }, counters: { label: "Compteurs", mode: "Valeurs", icon: "abacus", Component: CountersModule, editable: true, scrollable: true }, combos: { label: "Combos", mode: "Catégories", icon: "controller", Component: CombosModule, editable: true, scrollable: true }, calculator: { label: "Calculateur", mode: "Arborescence", icon: "calculator", Component: CalculatorModule }, table: { label: "Tableau", mode: "Grille + formules", icon: "table", Component: TableModule, scrollable: true }, timer: { label: "Timer", mode: "Chrono + comptes à rebours", icon: "clock", Component: TimerModule }, taskPlanner: { label: "Planificateur de tâches", mode: "Catégories + parent/enfant", icon: "tasklist", Component: TaskPlannerModule, editable: true, scrollable: true }, equipmentPlanner: { label: "Planificateur d'équipements", mode: "Build + sertissages", icon: "chest-armor", Component: EquipmentPlannerModule, editable: true, scrollable: true }, imageAnnotation: { label: "Annotation d'images", mode: "Image + marqueurs", icon: "map", Component: ImageAnnotationModule, editable: true } }; const LIBRARY_CATEGORY_DEFINITIONS = [ { key: "notesTracking", types: ["notepad", "checklist", "counters"] }, { key: "references", types: ["images", "imageAnnotation", "links"] }, { key: "calculationData", types: ["calculator", "table"] }, { key: "timeRoutines", types: ["timer", "taskPlanner"] }, { key: "buildsCommands", types: ["combos", "equipmentPlanner"] } ]; function cloneData(value) { if (value === undefined) return {}; if (typeof structuredClone === "function") return structuredClone(value); return JSON.parse(JSON.stringify(value)); } function fileToDataUrl(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.addEventListener("load", () => resolve(String(reader.result || ""))); reader.addEventListener("error", () => reject(reader.error || new Error("Image illisible."))); reader.readAsDataURL(file); }); } function createModuleDataFromPayload(payload) { return Object.fromEntries((payload?.toolbox?.modules || []).map((module) => [ `${LIBRARY_TOOLBOX_ID}:${module.id}`, cloneData(payload?.modules?.[module.id] || {}) ])); } function getLibraryAnchorId(module) { return `library-tool-${String(module?.id || module?.type || "outil").replace(/[^a-z0-9_-]+/gi, "-").toLowerCase()}`; } function getLibraryCategoryAnchorId(category) { return `library-category-${category.key}`; } function getCurrentLibraryAnchor() { const match = location.hash.match(/^#\/library#(.+)$/); return match ? decodeURIComponent(match[1]) : ""; } function scrollToLibraryTool(anchorId, replace = false) { const element = document.getElementById(anchorId); if (!element) return; if (replace) history.replaceState(null, "", `#/library#${encodeURIComponent(anchorId)}`); const top = element.getBoundingClientRect().top + window.scrollY - 24; window.scrollTo({ top: Math.max(0, top), behavior: "smooth" }); } function findCategoryForAnchor(categories, anchorId) { return categories.find((category) => ( getLibraryCategoryAnchorId(category) === anchorId || category.modules.some((module) => getLibraryAnchorId(module) === anchorId) )) || null; } function getToolDescription(siteContent, definition) { const tools = siteContent.about?.tools || []; return tools.find((tool) => tool.name === definition.label)?.description || ""; } function getLibraryCategories(modules, content) { const modulesByType = new Map((modules || []).map((module) => [module.type, module])); return LIBRARY_CATEGORY_DEFINITIONS.map((category) => ({ ...category, title: content.categories?.[category.key] || category.key, description: content.categoryDescriptions?.[category.key] || "", modules: category.types.map((type) => modulesByType.get(type)).filter(Boolean) })).filter((category) => category.modules.length); } export function LibraryPage({ siteContent, actions }) { const content = siteContent.library; const [libraryPayload, setLibraryPayload] = useState(null); const [libraryError, setLibraryError] = useState(""); const [moduleData, setModuleData] = useState({}); const [activeCategoryKey, setActiveCategoryKey] = useState(""); const libraryCategories = useMemo(() => getLibraryCategories(libraryPayload?.toolbox?.modules || [], content), [content, libraryPayload]); const activeCategory = libraryCategories.find((category) => category.key === activeCategoryKey) || libraryCategories[0] || null; useEffect(() => { let cancelled = false; fetch("/data/library.json") .then((response) => { if (!response.ok) throw new Error("Impossible de charger les exemples de librairie."); return response.json(); }) .then((payload) => { if (cancelled) return; setLibraryPayload(payload); setModuleData(createModuleDataFromPayload(payload)); }) .catch((error) => { if (!cancelled) setLibraryError(error.message); }); return () => { cancelled = true; }; }, []); useEffect(() => { if (!libraryPayload || !libraryCategories.length) return; const anchorId = getCurrentLibraryAnchor(); const category = findCategoryForAnchor(libraryCategories, anchorId) || libraryCategories[0]; setActiveCategoryKey(category.key); if (!anchorId) return; requestAnimationFrame(() => scrollToLibraryTool(anchorId)); }, [libraryPayload, libraryCategories]); const moduleContext = useMemo(() => ({ getModuleData: (toolboxId, moduleId, fallback) => moduleData[`${toolboxId}:${moduleId}`] || fallback, setModuleData: (toolboxId, moduleId, data) => setModuleData((current) => ({ ...current, [`${toolboxId}:${moduleId}`]: data })), moduleText: siteContent.toolboxes.modules, normalizeChecklistData, normalizeCombosData, normalizeLinksData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeCalculatorData, normalizeImageAnnotationData, normalizeNotepadData, normalizeTableData, normalizeTimerData, normalizeTaskPlannerData, summarizeEquipmentPlannerData, normalizeUrl, hostnameFromUrl, clampQty, uid, copyText: async (value) => { await navigator.clipboard.writeText(value); return true; }, notify: actions.notify, compressImageFile: fileToDataUrl, addImageFiles: async (toolboxId, moduleId, files) => { const imageFiles = [...files].filter((file) => file?.type?.startsWith("image/")); if (!imageFiles.length) return false; const images = await Promise.all(imageFiles.map(async (file) => ({ id: uid("image"), label: file.name?.replace(/\.[^.]+$/, "").trim() || "", dataUrl: await fileToDataUrl(file) }))); setModuleData((current) => { const key = `${toolboxId}:${moduleId}`; const data = current[key] || { images: [] }; return { ...current, [key]: { ...data, images: [...(data.images || []), ...images] } }; }); return true; }, setImage: actions.setImage, createImageAnnotationModule: (dataUrl) => { const annotationModule = libraryPayload?.toolbox?.modules?.find((module) => module.type === "imageAnnotation"); if (!annotationModule || !dataUrl) return; setModuleData((current) => ({ ...current, [`${LIBRARY_TOOLBOX_ID}:${annotationModule.id}`]: { image: dataUrl, markers: [], drawings: { strokes: [] } } })); requestAnimationFrame(() => scrollToLibraryTool(getLibraryAnchorId(annotationModule), true)); } }), [actions, libraryPayload, moduleData, siteContent.toolboxes.modules]); return (
{content.eyebrow}
{content.description}
{item.text}
{libraryError}
Chargement des exemples...
{content.categoryLabel || "Catégorie"}
{activeCategory.description}
}{description}
}{definition.mode}
{feature.text}
{Array.isArray(feature.details) && feature.details.length > 0 && ({docs.importFormat.text}
{docs.importFormat.example}