613 lines
24 KiB
JavaScript
613 lines
24 KiB
JavaScript
// 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 (
|
||
<div className="library-page">
|
||
<section className="page-hero nds-page-hero library-hero">
|
||
<div>
|
||
<p className="eyebrow">{content.eyebrow}</p>
|
||
<h1>{content.title}</h1>
|
||
<p>{content.description}</p>
|
||
</div>
|
||
</section>
|
||
<section className="library-summary nebula-panel nds-panel" aria-label={content.summaryLabel}>
|
||
{content.summaryItems.map((item) => (
|
||
<article key={item.title}>
|
||
<span className="library-summary-icon" aria-hidden="true">
|
||
<span className={`module-icon-svg module-icon-${item.icon || "toolbox"}`} />
|
||
</span>
|
||
<div>
|
||
<h2>{item.title}</h2>
|
||
<p>{item.text}</p>
|
||
</div>
|
||
</article>
|
||
))}
|
||
</section>
|
||
{libraryError && <section className="empty nds-empty-state"><h2>Librairie indisponible</h2><p>{libraryError}</p></section>}
|
||
{!libraryError && !libraryPayload && <section className="empty nds-empty-state"><p>Chargement des exemples...</p></section>}
|
||
{libraryPayload && (
|
||
<>
|
||
<div className="library-doc-layout">
|
||
<LibraryDocNav
|
||
content={content}
|
||
categories={libraryCategories}
|
||
activeCategoryKey={activeCategory?.key || ""}
|
||
onSelectCategory={(category, anchorId = getLibraryCategoryAnchorId(category)) => {
|
||
selectAnchor(anchorId);
|
||
}}
|
||
/>
|
||
<section className="library-tools" aria-label={content.toolsLabel}>
|
||
{activeCategory && (
|
||
<section className="library-tool-category" key={activeCategory.key} aria-labelledby={getLibraryCategoryAnchorId(activeCategory)}>
|
||
<div className="library-category-hero">
|
||
<div>
|
||
<p className="eyebrow">{content.categoryLabel || "Catégorie"}</p>
|
||
<h2 id={getLibraryCategoryAnchorId(activeCategory)}>{activeCategory.title}</h2>
|
||
{activeCategory.description && <p>{activeCategory.description}</p>}
|
||
</div>
|
||
</div>
|
||
{activeCategory.modules.map((module) => (
|
||
<LibraryToolExample
|
||
key={module.id}
|
||
module={module}
|
||
sourceData={libraryPayload.modules?.[module.id] || {}}
|
||
description={getToolDescription(siteContent, getToolboxModuleDefinition(module.type))}
|
||
categoryTitle={activeCategory.title}
|
||
docs={{
|
||
...(content.toolDocs?.[module.type] || {}),
|
||
...(module.controls ? { controls: module.controls } : {}),
|
||
...(module.importFormat ? { importFormat: module.importFormat } : {})
|
||
}}
|
||
featureHeading={content.featureHeading}
|
||
advancedHeading={content.advancedHeading}
|
||
advancedCopyLabel={content.copyExampleLabel}
|
||
advancedCopySuccess={content.copyExampleSuccess}
|
||
controlLegendHeading={content.controlLegendHeading}
|
||
context={moduleContext}
|
||
/>
|
||
))}
|
||
</section>
|
||
)}
|
||
</section>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function LibraryDocNav({ content, categories, activeCategoryKey, onSelectCategory }) {
|
||
return (
|
||
<nav className="library-doc-nav nebula-panel nds-panel" aria-label={content.tocLabel || "Sommaire des outils"}>
|
||
<div className="library-toc-heading">
|
||
<h2>{content.docTocTitle || content.tocTitle || "Sommaire"}</h2>
|
||
<p>{content.tocDescription || "Accédez directement à un exemple d’outil dans la page."}</p>
|
||
</div>
|
||
<div className="library-doc-nav-groups">
|
||
{categories.map((category) => (
|
||
<div className={`library-doc-nav-group ${category.key === activeCategoryKey ? "active" : ""}`} key={category.key}>
|
||
<a href={`#/library#${getLibraryCategoryAnchorId(category)}`} onClick={(event) => {
|
||
event.preventDefault();
|
||
onSelectCategory(category);
|
||
}}>{category.title}</a>
|
||
<ul hidden={category.key !== activeCategoryKey}>
|
||
{category.modules.map((module) => {
|
||
const definition = getToolboxModuleDefinition(module.type);
|
||
const title = module.title || definition.label || "Outil";
|
||
const anchorId = getLibraryAnchorId(module);
|
||
return (
|
||
<li key={module.id}>
|
||
<a
|
||
href={`#/library#${anchorId}`}
|
||
onClick={(event) => {
|
||
event.preventDefault();
|
||
onSelectCategory(category, anchorId);
|
||
}}
|
||
>
|
||
<span className="module-icon" aria-hidden="true">
|
||
<span className={`module-icon-svg module-icon-${definition.icon}`} />
|
||
</span>
|
||
<span>{title}</span>
|
||
</a>
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</nav>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<section id={getLibraryAnchorId(module)} className="library-tool-section nds-panel">
|
||
<div className="library-tool-intro">
|
||
<div>
|
||
<div className="library-tool-title-row">
|
||
<span className="module-icon" aria-hidden="true">
|
||
<span className={`module-icon-svg module-icon-${definition.icon}`} />
|
||
</span>
|
||
<h2>{title}</h2>
|
||
</div>
|
||
<span className="library-tool-badge nds-badge">{categoryTitle}</span>
|
||
{description && <p>{description}</p>}
|
||
</div>
|
||
</div>
|
||
<div className={`library-example-layout ${Array.isArray(docs?.controls) && docs.controls.length ? "has-legend" : ""}`}>
|
||
<ToolControlLegend items={docs?.controls} heading={controlLegendHeading || "Repères"} />
|
||
<article className={`module nds-tool-panel library-tool-example ${scrollable ? "is-scrollable" : ""}`} data-module-type={module.type}>
|
||
<header>
|
||
<div>
|
||
<h2>{definition.label}</h2>
|
||
<p>{definition.mode}</p>
|
||
</div>
|
||
<div className="library-example-actions">
|
||
{definition.scrollable && (
|
||
<button
|
||
className={`module-scroll-button nds-switch ${scrollable ? "active" : ""}`}
|
||
data-size="md"
|
||
type="button"
|
||
onClick={() => setScrollable((value) => !value)}
|
||
aria-label={`${scrollable ? "Désactiver" : "Activer"} le scroll de ${title}`}
|
||
aria-pressed={scrollable}
|
||
title={scrollable ? "Désactiver le scroll" : "Activer le scroll"}
|
||
>
|
||
<Icon name="scrollable" />
|
||
<i aria-hidden="true" />
|
||
</button>
|
||
)}
|
||
{definition.editable && definition.addable !== false && (
|
||
<button
|
||
className={`module-edit-button nds-icon-button ${editing ? "active" : ""}`}
|
||
data-size="sm"
|
||
type="button"
|
||
onClick={() => setEditing((value) => !value)}
|
||
aria-label={`${editing ? "Masquer" : "Afficher"} l'ajout de ${title}`}
|
||
aria-pressed={editing}
|
||
title={editing ? "Masquer l'ajout" : "Ajouter"}
|
||
>
|
||
<Icon name="add" />
|
||
</button>
|
||
)}
|
||
<button className="library-reset-button nds-icon-button" data-size="sm" type="button" onClick={() => context.setModuleData(LIBRARY_TOOLBOX_ID, module.id, cloneData(sourceData))} aria-label={`Réinitialiser ${title}`} title="Réinitialiser l'exemple">
|
||
<Icon name="refresh" />
|
||
</button>
|
||
</div>
|
||
</header>
|
||
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||
<Suspense fallback={<div className="empty nds-empty-state"><p>Chargement de l'outil...</p></div>}>
|
||
<Component
|
||
toolboxId={LIBRARY_TOOLBOX_ID}
|
||
moduleId={module.id}
|
||
context={context}
|
||
editing={editing}
|
||
setEditing={setEditing}
|
||
openEditor={() => setEditing(true)}
|
||
/>
|
||
</Suspense>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
<ToolFeatureList docs={docs} heading={featureHeading || "Fonctionnalités"} />
|
||
<ToolAdvancedFeatureList docs={docs} heading={advancedHeading || "Import par texte"} copyLabel={advancedCopyLabel} copySuccess={advancedCopySuccess} context={context} />
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function ToolControlLegend({ items, heading }) {
|
||
const legendItems = Array.isArray(items) ? items : [];
|
||
if (!legendItems.length) return null;
|
||
const groups = legendItems.reduce((accumulator, item) => {
|
||
const groupTitle = item.group || "Contrôles";
|
||
const existingGroup = accumulator.find((group) => group.title === groupTitle);
|
||
if (existingGroup) existingGroup.items.push(item);
|
||
else accumulator.push({ title: groupTitle, items: [item] });
|
||
return accumulator;
|
||
}, []);
|
||
|
||
return (
|
||
<aside className="library-control-legend" aria-label={heading}>
|
||
<h3>{heading}</h3>
|
||
{groups.map((group) => {
|
||
const genericSingleGroup = groups.length === 1 && group.title === "Contrôles";
|
||
return (
|
||
<section className={`library-control-legend-group ${genericSingleGroup ? "is-generic-single" : ""}`} key={group.title}>
|
||
{!genericSingleGroup && <h4>{group.title}</h4>}
|
||
<div className="library-control-legend-items">
|
||
{group.items.map((item) => (
|
||
<div className="library-control-legend-item" key={item.title}>
|
||
<ControlPreview item={item} />
|
||
<div>
|
||
<strong>{item.title}</strong>
|
||
{item.text && <p>{item.text}</p>}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
);
|
||
})}
|
||
</aside>
|
||
);
|
||
}
|
||
|
||
function ControlPreview({ item }) {
|
||
const preview = item.preview || {};
|
||
const kind = preview.kind || "iconButton";
|
||
|
||
if (kind === "textFormats") {
|
||
return (
|
||
<span className="library-control-preview notepad-toolbar-group" aria-hidden="true">
|
||
<span className="notepad-toolbar-button">B</span>
|
||
<span className="notepad-toolbar-button"><em>I</em></span>
|
||
<span className="notepad-toolbar-button"><u>U</u></span>
|
||
<span className="notepad-toolbar-button"><s>S</s></span>
|
||
</span>
|
||
);
|
||
}
|
||
|
||
if (kind === "textButton") {
|
||
return (
|
||
<span className="library-control-preview" aria-hidden="true">
|
||
<span className={`notepad-toolbar-button ${preview.active ? "active" : ""}`}>{preview.label || item.title.slice(0, 1)}</span>
|
||
</span>
|
||
);
|
||
}
|
||
|
||
if (kind === "listDropdown") {
|
||
return (
|
||
<span className="library-control-preview" aria-hidden="true">
|
||
<span className="notepad-toolbar-button active"><span className="notepad-list-icon" /></span>
|
||
<span className="notepad-toolbar-button">•</span>
|
||
<span className="notepad-toolbar-button">1.</span>
|
||
</span>
|
||
);
|
||
}
|
||
|
||
if (kind === "color") {
|
||
return (
|
||
<span className="library-control-preview" aria-hidden="true">
|
||
<span className="notepad-color-toggle is-rainbow active" />
|
||
</span>
|
||
);
|
||
}
|
||
|
||
if (kind === "drawingWidth") {
|
||
return (
|
||
<span className="library-control-preview" aria-hidden="true">
|
||
<span className="drawing-width-toggle active"><span style={{ "--line-width": `${preview.width || 4}px` }} /></span>
|
||
</span>
|
||
);
|
||
}
|
||
|
||
if (kind === "switch") {
|
||
return (
|
||
<span className="library-control-preview" aria-hidden="true">
|
||
<span className={`module-scroll-button drawing-storage-switch ${preview.active ? "active" : ""}`}>
|
||
<Icon name={preview.icon || item.icon || "settings"} />
|
||
<i />
|
||
</span>
|
||
</span>
|
||
);
|
||
}
|
||
|
||
if (kind === "buttonGroup") {
|
||
const buttons = Array.isArray(preview.buttons) ? preview.buttons : [];
|
||
return (
|
||
<span className="library-control-preview" aria-hidden="true">
|
||
{buttons.map((button, index) => (
|
||
<span
|
||
className={`notepad-toolbar-button nds-icon-button ${button.label && String(button.label).length > 2 ? "is-wide" : ""} ${button.active ? "active" : ""}`.trim()}
|
||
data-size="sm"
|
||
data-variant={button.danger ? "danger" : undefined}
|
||
key={`${button.label || button.icon || index}-${index}`}
|
||
>
|
||
{button.icon ? <Icon name={button.icon} /> : button.label}
|
||
</span>
|
||
))}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<span className="library-control-preview" aria-hidden="true">
|
||
<span className={`notepad-toolbar-button nds-icon-button ${preview.active ? "active" : ""}`.trim()} data-size="sm" data-variant={preview.danger ? "danger" : undefined}>
|
||
<Icon name={preview.icon || item.icon || "settings"} />
|
||
</span>
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function ToolFeatureList({ docs, heading }) {
|
||
const features = Array.isArray(docs?.features) ? docs.features : [];
|
||
if (!features.length) return null;
|
||
|
||
return (
|
||
<section className="library-tool-docs">
|
||
<details className="library-tool-feature-list">
|
||
<summary>
|
||
<h3>{heading}</h3>
|
||
<Icon name="chevron-down" />
|
||
</summary>
|
||
<div className="library-tool-feature-content">
|
||
<dl>
|
||
{features.map((feature) => (
|
||
<div key={feature.title}>
|
||
<dt>{feature.title}</dt>
|
||
<dd>
|
||
<p>{feature.text}</p>
|
||
{Array.isArray(feature.details) && feature.details.length > 0 && (
|
||
<ul>
|
||
{feature.details.map((detail) => <li key={detail}>{detail}</li>)}
|
||
</ul>
|
||
)}
|
||
</dd>
|
||
</div>
|
||
))}
|
||
</dl>
|
||
</div>
|
||
</details>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
function ToolAdvancedFeatureList({ docs, heading, copyLabel, copySuccess, context }) {
|
||
const [copied, setCopied] = useState(false);
|
||
const copiedTimeoutRef = useRef(0);
|
||
useEffect(() => () => window.clearTimeout(copiedTimeoutRef.current), []);
|
||
if (!docs?.importFormat) return null;
|
||
|
||
async function copyExample() {
|
||
if (!await context.copyText(docs.importFormat.example)) return;
|
||
setCopied(true);
|
||
context.notify?.(copySuccess || "Exemple copié.");
|
||
window.clearTimeout(copiedTimeoutRef.current);
|
||
copiedTimeoutRef.current = window.setTimeout(() => setCopied(false), 1400);
|
||
}
|
||
|
||
return (
|
||
<section className="library-tool-docs">
|
||
<details className="library-tool-feature-list library-tool-advanced-list">
|
||
<summary>
|
||
<h3>{heading}</h3>
|
||
<Icon name="chevron-down" />
|
||
</summary>
|
||
<div className="library-tool-feature-content">
|
||
<div className="library-tool-import-format">
|
||
<p>{docs.importFormat.text}</p>
|
||
<div className="library-tool-import-example">
|
||
<pre><code>{docs.importFormat.example}</code></pre>
|
||
<button className="nds-button" data-size="sm" type="button" onClick={copyExample} title={copyLabel || "Copier l'exemple"}>
|
||
<Icon name="copy" />
|
||
<span>{copied ? copySuccess || "Exemple copié." : copyLabel || "Copier l'exemple"}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
</section>
|
||
);
|
||
}
|