554 lines
23 KiB
JavaScript
554 lines
23 KiB
JavaScript
// 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 (
|
||
<div className="library-page">
|
||
<section className="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" 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"><h2>Librairie indisponible</h2><p>{libraryError}</p></section>}
|
||
{!libraryError && !libraryPayload && <section className="empty"><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)) => {
|
||
setActiveCategoryKey(category.key);
|
||
requestAnimationFrame(() => scrollToLibraryTool(anchorId, true));
|
||
}}
|
||
/>
|
||
<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, MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad)}
|
||
categoryTitle={activeCategory.title}
|
||
docs={content.toolDocs?.[module.type]}
|
||
featureHeading={content.featureHeading}
|
||
controlLegendHeading={content.controlLegendHeading}
|
||
context={moduleContext}
|
||
/>
|
||
))}
|
||
</section>
|
||
)}
|
||
</section>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function LibraryDocNav({ content, categories, activeCategoryKey, onSelectCategory }) {
|
||
return (
|
||
<nav className="library-doc-nav nebula-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 = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
||
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, controlLegendHeading, context }) {
|
||
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
||
const Component = definition.Component;
|
||
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">
|
||
<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">{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 library-tool-example ${scrollable ? "is-scrollable" : ""}`}>
|
||
<header>
|
||
<div>
|
||
<h2>{definition.label}</h2>
|
||
<p>{definition.mode}</p>
|
||
</div>
|
||
<div className="library-example-actions">
|
||
{definition.scrollable && (
|
||
<button
|
||
className={`module-scroll-button ${scrollable ? "active" : ""}`}
|
||
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 && (
|
||
<button
|
||
className={`module-edit-button ${editing ? "active" : ""}`}
|
||
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" 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" : ""}`}>
|
||
<Component toolboxId={LIBRARY_TOOLBOX_ID} moduleId={module.id} context={context} editing={editing} />
|
||
</div>
|
||
</article>
|
||
</div>
|
||
<ToolFeatureList docs={docs} heading={featureHeading || "Fonctionnalités"} />
|
||
</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 ${button.label && String(button.label).length > 2 ? "is-wide" : ""} ${button.active ? "active" : ""} ${button.danger ? "danger" : ""}`} 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 ${preview.active ? "active" : ""} ${preview.danger ? "danger" : ""}`}>
|
||
<Icon name={preview.icon || item.icon || "settings"} />
|
||
</span>
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function ToolFeatureList({ docs, heading }) {
|
||
const features = Array.isArray(docs?.features) ? docs.features : [];
|
||
if (!features.length && !docs?.importFormat) 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>
|
||
{docs?.importFormat && (
|
||
<div className="library-tool-import-format">
|
||
<h3>{docs.importFormat.title}</h3>
|
||
<p>{docs.importFormat.text}</p>
|
||
<pre><code>{docs.importFormat.example}</code></pre>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</details>
|
||
</section>
|
||
);
|
||
}
|