Refactor tool catalog into documentation layout
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
d53d4160af
commit
a9b5ca423a
12 changed files with 1925 additions and 114 deletions
|
|
@ -49,6 +49,14 @@ const MODULE_COMPONENTS = {
|
|||
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);
|
||||
|
|
@ -75,6 +83,10 @@ 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]) : "";
|
||||
|
|
@ -88,16 +100,36 @@ function scrollToLibraryTool(anchorId, replace = false) {
|
|||
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;
|
||||
|
|
@ -118,11 +150,13 @@ export function LibraryPage({ siteContent, actions }) {
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!libraryPayload) return;
|
||||
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]);
|
||||
}, [libraryPayload, libraryCategories]);
|
||||
|
||||
const moduleContext = useMemo(() => ({
|
||||
getModuleData: (toolboxId, moduleId, fallback) => moduleData[`${toolboxId}:${moduleId}`] || fallback,
|
||||
|
|
@ -203,52 +237,94 @@ export function LibraryPage({ siteContent, actions }) {
|
|||
{!libraryError && !libraryPayload && <section className="empty"><p>Chargement des exemples...</p></section>}
|
||||
{libraryPayload && (
|
||||
<>
|
||||
<nav className="library-toc nebula-panel" aria-label={content.tocLabel || "Sommaire des outils"}>
|
||||
<div className="library-toc-heading">
|
||||
<h2>{content.tocTitle || "Sommaire"}</h2>
|
||||
<p>{content.tocDescription || "Accédez directement à un exemple d’outil dans la page."}</p>
|
||||
</div>
|
||||
<div className="library-toc-links">
|
||||
{libraryPayload.toolbox.modules.map((module) => {
|
||||
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
||||
const title = module.title || definition.label || "Outil";
|
||||
const anchorId = getLibraryAnchorId(module);
|
||||
return (
|
||||
<a
|
||||
key={module.id}
|
||||
href={`#/library#${anchorId}`}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
scrollToLibraryTool(anchorId, true);
|
||||
}}
|
||||
>
|
||||
<span className="module-icon" aria-hidden="true">
|
||||
<span className={`module-icon-svg module-icon-${definition.icon}`} />
|
||||
</span>
|
||||
<span>{title}</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
<section className="library-tools" aria-label={content.toolsLabel}>
|
||||
{libraryPayload.toolbox.modules.map((module) => (
|
||||
<LibraryToolExample
|
||||
key={module.id}
|
||||
module={module}
|
||||
sourceData={libraryPayload.modules?.[module.id] || {}}
|
||||
description={getToolDescription(siteContent, MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad)}
|
||||
context={moduleContext}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
<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 LibraryToolExample({ module, sourceData, description, context }) {
|
||||
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);
|
||||
|
|
@ -259,55 +335,220 @@ function LibraryToolExample({ module, sourceData, description, context }) {
|
|||
<section id={getLibraryAnchorId(module)} className="library-tool-section">
|
||||
<div className="library-tool-intro">
|
||||
<div>
|
||||
<h2>{title}</h2>
|
||||
{description && <p>{description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<article className={`module library-tool-example ${scrollable ? "is-scrollable" : ""}`}>
|
||||
<header>
|
||||
<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>
|
||||
<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" />
|
||||
<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>
|
||||
)}
|
||||
{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>
|
||||
</header>
|
||||
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||
<Component toolboxId={LIBRARY_TOOLBOX_ID} moduleId={module.id} context={context} editing={editing} />
|
||||
</div>
|
||||
</article>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue