// Rôle : affiche la librairie des outils avec des exemples locaux non persistés. import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Icon } from "../components/Icon.jsx"; import { LIBRARY_CATEGORY_DEFINITIONS, getToolboxModuleDefinition } from "../features/toolboxes/toolboxModuleRegistry.js"; import { MODULE_COMPONENTS } from "../features/toolboxes/modules/toolboxModuleComponents.jsx"; import { clampQty, hostnameFromUrl, normalizeCalculatorData, normalizeCalendarData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeImageAnnotationData, normalizeLinksData, normalizeMemosData, normalizeNotepadData, normalizeRandomizerData, normalizeRngSimulatorData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, summarizeEquipmentPlannerData, normalizeUrl, uid } from "../features/toolboxes/storage/toolboxStorage.js"; const LIBRARY_TOOLBOX_ID = "library"; const LIBRARY_ANCHOR_RETRY_DELAYS = [0, 80, 240, 600]; 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#(.+)$/); try { return match ? decodeURIComponent(match[1]) : ""; } catch { return ""; } } function getLibraryAnchorOffset() { const topbar = document.querySelector(".topbar"); const topbarHeight = topbar?.getBoundingClientRect?.().height || 0; return topbarHeight + 16; } function scrollToLibraryTool(anchorId, behavior) { let timers = []; const frame = requestAnimationFrame(() => { timers = LIBRARY_ANCHOR_RETRY_DELAYS.map((delay, index) => window.setTimeout(() => { const element = document.getElementById(anchorId); if (!element) return; const top = element.getBoundingClientRect().top + window.scrollY - getLibraryAnchorOffset(); window.scrollTo({ top: Math.max(0, top), behavior: index === 0 ? behavior : "auto" }); }, delay)); }); return () => { cancelAnimationFrame(frame); timers.forEach((timer) => window.clearTimeout(timer)); }; } 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 || []).forEach((module) => { modulesByType.set(module.type, [...(modulesByType.get(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.flatMap((type) => modulesByType.get(type) || []) })).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 [navigation, setNavigation] = useState(() => ({ anchorId: getCurrentLibraryAnchor(), behavior: "auto" })); const libraryCategories = useMemo(() => getLibraryCategories(libraryPayload?.toolbox?.modules || [], content), [content, libraryPayload]); const activeCategory = findCategoryForAnchor(libraryCategories, navigation.anchorId) || libraryCategories[0] || null; const selectAnchor = useCallback((anchorId) => { history.replaceState(null, "", `#/library#${encodeURIComponent(anchorId)}`); setNavigation({ anchorId, behavior: "smooth" }); }, []); 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(() => { const onHashChange = () => setNavigation({ anchorId: getCurrentLibraryAnchor(), behavior: "auto" }); window.addEventListener("hashchange", onHashChange); return () => window.removeEventListener("hashchange", onHashChange); }, []); useEffect(() => { if (!activeCategory?.key || !navigation.anchorId) return undefined; return scrollToLibraryTool(navigation.anchorId, navigation.behavior); }, [activeCategory?.key, navigation]); 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, normalizeMemosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeGoalTreeData, normalizeCalculatorData, normalizeCalendarData, normalizeImageAnnotationData, normalizeNotepadData, normalizeRandomizerData, normalizeRngSimulatorData, 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: [] } } })); selectAnchor(getLibraryAnchorId(annotationModule)); } }), [actions, libraryPayload, moduleData, selectAnchor, siteContent.toolboxes.modules]); return (

{content.eyebrow}

{content.title}

{content.description}

{content.summaryItems.map((item) => (
))}
{libraryError &&

Librairie indisponible

{libraryError}

} {!libraryError && !libraryPayload &&

Chargement des exemples...

} {libraryPayload && ( <>
{ selectAnchor(anchorId); }} />
{activeCategory && (

{content.categoryLabel || "Catégorie"}

{activeCategory.title}

{activeCategory.description &&

{activeCategory.description}

}
{activeCategory.modules.map((module) => ( ))}
)}
)}
); } function LibraryDocNav({ content, categories, activeCategoryKey, onSelectCategory }) { return ( ); } function LibraryToolExample({ module, sourceData, description, categoryTitle, docs, featureHeading, advancedHeading, advancedCopyLabel, advancedCopySuccess, controlLegendHeading, context }) { const definition = getToolboxModuleDefinition(module.type); const { Component } = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad; const [scrollable, setScrollable] = useState(false); const [editing, setEditing] = useState(false); const title = module.title || definition.label || "Outil"; return (
{categoryTitle} {description &&

{description}

}

{definition.label}

{definition.mode}

{definition.scrollable && (