Share toolbox components and fix image and calendar updates
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s
Some checks failed
Deploy Sokko G / deploy (push) Failing after 2s
This commit is contained in:
parent
865a4ffd97
commit
34524d2961
13 changed files with 117 additions and 93 deletions
|
|
@ -20,3 +20,5 @@
|
|||
{"id":"int-da5413e82c18d19a8432288bec703dd7","kind":"field_change","created_at":"2026-08-28T07:50:07.033524644Z","actor":"Shinuwa","issue_id":"sokko-g-h7z","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Completed global NDS control migration and validation"}}
|
||||
{"id":"int-42f39cb889dfab3bc998d4ce566d2339","kind":"field_change","created_at":"2026-08-28T08:09:50.738078383Z","actor":"Shinuwa","issue_id":"sokko-g-9p1","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed filter radio group, sidebar storage buttons, memo controls, and textarea ghost variant"}}
|
||||
{"id":"int-03ed1c823da90551f51a5a0d5691e440","kind":"field_change","created_at":"2026-08-28T12:35:29.257332022Z","actor":"Shinuwa","issue_id":"sokko-g-39h","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Applied NDS event-surface to calendar week and month events, documented data-color/data-size, and validated checks."}}
|
||||
{"id":"int-88c39f90fdd5b81110acc4bdae55836c","kind":"field_change","created_at":"2026-09-10T11:49:36.768770213Z","actor":"Shinuwa","issue_id":"sokko-g-vfo","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Shared lazy React registry consumed by toolbox and library; obsolete static import assertions removed; all 17 library types exercised; npm run check and 13 E2E tests pass."}}
|
||||
{"id":"int-7ec422ecc24735e12910a965567f32fe","kind":"field_change","created_at":"2026-09-10T12:17:38.573656548Z","actor":"Shinuwa","issue_id":"sokko-g-531","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Concurrent image batches merge current data after compression; calendar selection handlers update drafts outside state updaters. Behavioral image test and reverse week/month selection E2E pass; npm run check passes."}}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ Checklist à suivre lors de l'ajout ou de la modification d'un outil, d'une page
|
|||
## Outil Toolbox
|
||||
|
||||
- Ajouter ou modifier le composant dans `website/src/features/toolboxes/modules/`.
|
||||
- Déclarer l'outil dans `website/src/features/toolboxes/modules/index.jsx`.
|
||||
- Déclarer ses métadonnées et son chargeur dans `website/src/features/toolboxes/toolboxModuleRegistry.js`. Le registre React `modules/toolboxModuleComponents.jsx` est partagé par la toolbox et la Librairie ; `modules/index.jsx` expose le shell.
|
||||
- Vérifier le chargement à la demande en toolbox et dans chaque catégorie de la Librairie.
|
||||
- Ajouter les textes nécessaires dans `website/public/data/site.json`.
|
||||
- Mettre à jour la validation de `site.json` dans `tests/helpers/data-validation.mjs`.
|
||||
- Mettre à jour la normalisation et le stockage compact dans `website/src/features/toolboxes/storage/modules/<outil>.js`, puis vérifier l'export public dans `website/src/features/toolboxes/storage/toolboxStorage.js`.
|
||||
|
|
|
|||
24
e2e/library.spec.js
Normal file
24
e2e/library.spec.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Rôle : vérifie le chargement des outils de chaque catégorie de la librairie.
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { LIBRARY_CATEGORY_DEFINITIONS } from "../website/src/features/toolboxes/toolboxModuleRegistry.js";
|
||||
|
||||
test("loads every library tool through the shared component registry", async ({ page }) => {
|
||||
const errors = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") errors.push(message.text());
|
||||
});
|
||||
await page.goto("/#/library");
|
||||
for (const category of LIBRARY_CATEGORY_DEFINITIONS) {
|
||||
await page.locator(`.library-doc-nav-group > a[href="#/library#library-category-${category.key}"]`).click();
|
||||
for (const type of category.types) {
|
||||
const tools = page.locator(`[data-module-type="${type}"] .module-content`);
|
||||
await expect(tools.first()).toBeVisible();
|
||||
for (const tool of await tools.all()) {
|
||||
await expect(tool.getByText("Chargement de l'outil...")).toHaveCount(0);
|
||||
await expect(tool.locator(":scope > *").first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
|
@ -90,6 +90,32 @@ test("creates, edits and deletes a weekly calendar event", async ({ page }) => {
|
|||
await expect(calendar.getByRole("button", { name: /Raid E2E modifié/ })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("updates weekly and monthly calendar drafts when extending a selection", async ({ page }) => {
|
||||
await createToolbox(page, "Calendar selection E2E");
|
||||
await addTool(page, "Calendrier");
|
||||
const weekly = moduleByType(page, "calendar").first();
|
||||
await weekly.getByRole("button", { name: "Hebdomadaire" }).click();
|
||||
await weekly.getByRole("button", { name: /Afficher l'ajout de Calendrier/ }).click();
|
||||
const slots = weekly.locator(".calendar-day-column").first().locator(".calendar-slot");
|
||||
await slots.nth(21).dispatchEvent("pointerdown", { buttons: 1 });
|
||||
await slots.nth(18).dispatchEvent("pointerover", { buttons: 1 });
|
||||
await slots.nth(18).dispatchEvent("pointerup", { buttons: 0 });
|
||||
await expect(weekly.getByRole("textbox", { name: "Début" })).toHaveValue("09:00");
|
||||
await expect(weekly.getByRole("textbox", { name: "Fin" })).toHaveValue("11:00");
|
||||
|
||||
await addTool(page, "Calendrier");
|
||||
const monthly = moduleByType(page, "calendar").last();
|
||||
await monthly.getByRole("button", { name: "Mensuel" }).click();
|
||||
await monthly.getByRole("button", { name: /Afficher l'ajout de Calendrier/ }).click();
|
||||
const days = monthly.locator(".calendar-month-cell:not(.is-outside)");
|
||||
await days.nth(4).dispatchEvent("pointerdown", { buttons: 1 });
|
||||
await days.nth(2).dispatchEvent("pointerover", { buttons: 1 });
|
||||
await expect(monthly.locator(".calendar-month-cell.is-selected")).toHaveCount(3);
|
||||
await days.nth(2).dispatchEvent("pointerup", { buttons: 0 });
|
||||
await expect(monthly.locator(".calendar-event-form")).toBeVisible();
|
||||
await expect(monthly.locator(".calendar-month-cell.is-form-selected")).toHaveCount(3);
|
||||
});
|
||||
|
||||
test("creates a goal tree checklist with more than three visible lines", async ({ page }) => {
|
||||
await createToolbox(page, "Goal Tree E2E Toolbox");
|
||||
await addTool(page, "Arbre d'objectifs");
|
||||
|
|
|
|||
|
|
@ -84,8 +84,7 @@ test("vite entrypoint and app shell are wired", async () => {
|
|||
assert.match(aboutPage, /about-tool-examples/);
|
||||
assert.match(libraryPage, /export function LibraryPage/);
|
||||
assert.match(libraryPage, /LIBRARY_TOOLBOX_ID/);
|
||||
assert.match(libraryPage, /TOOLBOX_MODULE_LOADERS/);
|
||||
assert.match(libraryPage, /Object\.entries\(TOOLBOX_MODULE_LOADERS\)/);
|
||||
assert.match(libraryPage, /toolboxModuleComponents\.jsx/);
|
||||
assert.match(moduleRegistry, /export const TOOLBOX_MODULE_LOADERS/);
|
||||
assert.match(moduleRegistry, /taskPlanner: \(\) => import\("\.\/modules\/TaskPlannerModule\.jsx"\)/);
|
||||
assert.match(moduleRegistry, /equipmentPlanner: \(\) => import\("\.\/modules\/EquipmentPlannerModule\.jsx"\)/);
|
||||
|
|
|
|||
|
|
@ -311,31 +311,9 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(moduleRegistry, /export \{ AddToolControls \}/);
|
||||
assert.match(moduleRegistry, /export \{ ToolboxModules \}/);
|
||||
assert.match(moduleComponents, /MODULE_COMPONENTS/);
|
||||
assert.match(moduleComponents, /lazy\(\(\) => import\("\.\/CombosModule\.jsx"\)/);
|
||||
assert.match(moduleComponents, /lazy\(\(\) => import\("\.\/CalendarModule\.jsx"\)/);
|
||||
assert.match(moduleComponents, /lazy\(\(\) => import\("\.\/GoalTreeModule\.jsx"\)/);
|
||||
assert.match(moduleComponents, /lazy\(\(\) => import\("\.\/EquipmentPlannerModule\.jsx"\)/);
|
||||
assert.match(toolboxModules, /Suspense fallback=\{<ModuleLoadingFallback label=\{label\} \/>\}/);
|
||||
assert.match(moduleComponents, /LAZY_MODULE_TYPES/);
|
||||
assert.match(moduleComponents, /notepad:/);
|
||||
assert.match(moduleComponents, /checklist:/);
|
||||
assert.match(moduleComponents, /images:/);
|
||||
assert.match(moduleComponents, /links:/);
|
||||
assert.match(moduleComponents, /counters:/);
|
||||
assert.match(moduleComponents, /combos:/);
|
||||
assert.match(moduleComponents, /CombosModule/);
|
||||
assert.match(moduleComponents, /calculator:/);
|
||||
assert.match(moduleComponents, /table:/);
|
||||
assert.match(moduleComponents, /table: \{ \.\.\.TOOLBOX_MODULE_DEFINITIONS\.table, Component: TableModule \}/);
|
||||
assert.match(moduleComponents, /timer:/);
|
||||
assert.match(moduleComponents, /calendar:/);
|
||||
assert.match(moduleComponents, /taskPlanner:/);
|
||||
assert.match(moduleComponents, /goalTree:/);
|
||||
assert.match(moduleComponents, /equipmentPlanner:/);
|
||||
assert.match(moduleMetadataRegistry, /Planificateur d'équipements/);
|
||||
assert.match(moduleComponents, /imageAnnotation:/);
|
||||
assert.match(moduleMetadataRegistry, /Annotation d'images/);
|
||||
assert.match(moduleComponents, /notepad: \{ \.\.\.TOOLBOX_MODULE_DEFINITIONS\.notepad, Component: NotepadModule \}/);
|
||||
assert.match(toolboxModules, /module-edit-button/);
|
||||
assert.match(toolboxModules, /itemReorder\.startDrag/);
|
||||
assert.match(moduleRegistryBundle, /onPointerDown/);
|
||||
|
|
|
|||
41
tests/toolbox-image-actions.test.mjs
Normal file
41
tests/toolbox-image-actions.test.mjs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Rôle : vérifie la fusion des ajouts d'images pendant une compression asynchrone.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { useToolboxActions } from "../website/src/features/toolboxes/useToolboxActions.js";
|
||||
|
||||
test("concurrent image batches preserve intervening edits and completed additions", async (t) => {
|
||||
const descriptors = Object.fromEntries(["document", "createImageBitmap"].map((key) => [
|
||||
key, Object.getOwnPropertyDescriptor(globalThis, key)
|
||||
]));
|
||||
t.after(() => {
|
||||
for (const [key, descriptor] of Object.entries(descriptors)) {
|
||||
if (descriptor) Object.defineProperty(globalThis, key, descriptor);
|
||||
else delete globalThis[key];
|
||||
}
|
||||
});
|
||||
const pending = new Map();
|
||||
globalThis.createImageBitmap = (file) => new Promise((resolve) => pending.set(file.name, resolve));
|
||||
globalThis.document = {
|
||||
createElement: () => ({
|
||||
getContext: () => ({ drawImage() {} }),
|
||||
toDataURL: () => "data:image/jpeg;base64,dGVzdA=="
|
||||
})
|
||||
};
|
||||
let current = { images: [{ id: "old", label: "Old" }] };
|
||||
const actions = useToolboxActions({
|
||||
store: {
|
||||
getModuleData: () => current,
|
||||
updateModuleData: (_toolboxId, _moduleId, value) => { current = value; return true; }
|
||||
}
|
||||
});
|
||||
const first = actions.addImageFiles("toolbox", "images", [{ name: "First.png", type: "image/png" }]);
|
||||
const second = actions.addImageFiles("toolbox", "images", [{ name: "Second.png", type: "image/png" }]);
|
||||
current = { images: [{ id: "old", label: "Renamed" }], annotation: "Keep me" };
|
||||
pending.get("Second.png")({ width: 10, height: 10 });
|
||||
assert.equal(await second, true);
|
||||
pending.get("First.png")({ width: 10, height: 10 });
|
||||
assert.equal(await first, true);
|
||||
assert.deepEqual(current.images.map((image) => image.label), ["First", "Second", "Renamed"]);
|
||||
assert.equal(current.annotation, "Keep me");
|
||||
assert.equal(await actions.addImageFiles("toolbox", "images", [{ type: "text/plain" }]), false);
|
||||
});
|
||||
|
|
@ -4,7 +4,7 @@ import { Icon } from "../../../components/Icon.jsx";
|
|||
import { useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
||||
import { supportsTextExchange } from "./textImport.js";
|
||||
import { LAZY_MODULE_TYPES, MODULE_COMPONENTS } from "./toolboxModuleComponents.jsx";
|
||||
import { MODULE_COMPONENTS } from "./toolboxModuleComponents.jsx";
|
||||
|
||||
const MODULE_COLUMN_IDS = ["left", "right"];
|
||||
|
||||
|
|
@ -263,13 +263,9 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
|||
</div>
|
||||
</header>
|
||||
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||
{LAZY_MODULE_TYPES.has(module.type) ? (
|
||||
<Suspense fallback={<ModuleLoadingFallback label={label} />}>
|
||||
<Component {...componentProps} />
|
||||
</Suspense>
|
||||
) : (
|
||||
<Component {...componentProps} />
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@
|
|||
export { TOOLBOX_MODULES } from "../toolboxModuleRegistry.js";
|
||||
export { AddToolControls } from "./AddToolControls.jsx";
|
||||
export { ToolboxModules } from "./ToolboxModules.jsx";
|
||||
export { LAZY_MODULE_TYPES, MODULE_COMPONENTS } from "./toolboxModuleComponents.jsx";
|
||||
export { MODULE_COMPONENTS } from "./toolboxModuleComponents.jsx";
|
||||
|
|
|
|||
|
|
@ -1,44 +1,8 @@
|
|||
// Rôle : associe les types d'outils toolbox à leurs composants React, synchrones ou lazy.
|
||||
/* eslint-disable react-refresh/only-export-components -- registre de composants toolbox consommé par le shell. */
|
||||
// Rôle : partage les composants lazy des outils entre toolbox et librairie.
|
||||
import { lazy } from "react";
|
||||
import { TOOLBOX_MODULE_DEFINITIONS } from "../toolboxModuleRegistry.js";
|
||||
import { CalculatorModule } from "./CalculatorModule.jsx";
|
||||
import { ChecklistModule } from "./ChecklistModule.jsx";
|
||||
import { CountersModule } from "./CountersModule.jsx";
|
||||
import { ImageAnnotationModule } from "./ImageAnnotationModule.jsx";
|
||||
import { ImagesModule } from "./ImagesModule.jsx";
|
||||
import { LinksModule } from "./LinksModule.jsx";
|
||||
import { MemosModule } from "./MemosModule.jsx";
|
||||
import { NotepadModule } from "./NotepadModule.jsx";
|
||||
import { RandomizerModule } from "./RandomizerModule.jsx";
|
||||
import { RngSimulatorModule } from "./RngSimulatorModule.jsx";
|
||||
import { TableModule } from "./TableModule.jsx";
|
||||
import { TaskPlannerModule } from "./TaskPlannerModule.jsx";
|
||||
import { TimerModule } from "./TimerModule.jsx";
|
||||
import { TOOLBOX_MODULE_DEFINITIONS, TOOLBOX_MODULE_LOADERS } from "../toolboxModuleRegistry.js";
|
||||
|
||||
const CombosModule = lazy(() => import("./CombosModule.jsx").then((module) => ({ default: module.CombosModule })));
|
||||
const CalendarModule = lazy(() => import("./CalendarModule.jsx").then((module) => ({ default: module.CalendarModule })));
|
||||
const GoalTreeModule = lazy(() => import("./GoalTreeModule.jsx").then((module) => ({ default: module.GoalTreeModule })));
|
||||
const EquipmentPlannerModule = lazy(() => import("./EquipmentPlannerModule.jsx").then((module) => ({ default: module.EquipmentPlannerModule })));
|
||||
|
||||
export const MODULE_COMPONENTS = {
|
||||
notepad: { ...TOOLBOX_MODULE_DEFINITIONS.notepad, Component: NotepadModule },
|
||||
memos: { ...TOOLBOX_MODULE_DEFINITIONS.memos, Component: MemosModule },
|
||||
checklist: { ...TOOLBOX_MODULE_DEFINITIONS.checklist, Component: ChecklistModule },
|
||||
images: { ...TOOLBOX_MODULE_DEFINITIONS.images, Component: ImagesModule },
|
||||
links: { ...TOOLBOX_MODULE_DEFINITIONS.links, Component: LinksModule },
|
||||
counters: { ...TOOLBOX_MODULE_DEFINITIONS.counters, Component: CountersModule },
|
||||
randomizer: { ...TOOLBOX_MODULE_DEFINITIONS.randomizer, Component: RandomizerModule },
|
||||
rngSimulator: { ...TOOLBOX_MODULE_DEFINITIONS.rngSimulator, Component: RngSimulatorModule },
|
||||
combos: { ...TOOLBOX_MODULE_DEFINITIONS.combos, Component: CombosModule },
|
||||
calculator: { ...TOOLBOX_MODULE_DEFINITIONS.calculator, Component: CalculatorModule },
|
||||
calendar: { ...TOOLBOX_MODULE_DEFINITIONS.calendar, Component: CalendarModule },
|
||||
table: { ...TOOLBOX_MODULE_DEFINITIONS.table, Component: TableModule },
|
||||
goalTree: { ...TOOLBOX_MODULE_DEFINITIONS.goalTree, Component: GoalTreeModule },
|
||||
timer: { ...TOOLBOX_MODULE_DEFINITIONS.timer, Component: TimerModule },
|
||||
taskPlanner: { ...TOOLBOX_MODULE_DEFINITIONS.taskPlanner, Component: TaskPlannerModule },
|
||||
equipmentPlanner: { ...TOOLBOX_MODULE_DEFINITIONS.equipmentPlanner, Component: EquipmentPlannerModule },
|
||||
imageAnnotation: { ...TOOLBOX_MODULE_DEFINITIONS.imageAnnotation, Component: ImageAnnotationModule }
|
||||
};
|
||||
|
||||
export const LAZY_MODULE_TYPES = new Set(["combos", "calendar", "goalTree", "equipmentPlanner"]);
|
||||
export const MODULE_COMPONENTS = Object.fromEntries(Object.entries(TOOLBOX_MODULE_LOADERS).map(([type, loader]) => [
|
||||
type,
|
||||
{ ...TOOLBOX_MODULE_DEFINITIONS[type], Component: lazy(loader) }
|
||||
]));
|
||||
|
|
|
|||
|
|
@ -142,12 +142,9 @@ export function useCalendarController({ data, context, toolboxId, moduleId, edit
|
|||
}
|
||||
|
||||
function handleSlotPointerEnter(event, dayIndex, slotIndex) {
|
||||
if (event.buttons !== 1) return;
|
||||
setSelection((current) => {
|
||||
if (!current || current.dayIndex !== dayIndex) return current;
|
||||
applySelection(dayIndex, current.startSlot, slotIndex);
|
||||
return { ...current, endSlot: slotIndex };
|
||||
});
|
||||
if (event.buttons !== 1 || !selection || selection.dayIndex !== dayIndex) return;
|
||||
applySelection(dayIndex, selection.startSlot, slotIndex);
|
||||
setSelection({ ...selection, endSlot: slotIndex });
|
||||
}
|
||||
|
||||
function handleMonthPointerDown(dateKey) {
|
||||
|
|
@ -158,12 +155,9 @@ export function useCalendarController({ data, context, toolboxId, moduleId, edit
|
|||
}
|
||||
|
||||
function handleMonthPointerEnter(event, dateKey) {
|
||||
if (event.buttons !== 1) return;
|
||||
setMonthSelection((current) => {
|
||||
if (!current) return current;
|
||||
applyMonthSelection(current.startDate, dateKey);
|
||||
return { ...current, endDate: dateKey };
|
||||
});
|
||||
if (event.buttons !== 1 || !monthSelection) return;
|
||||
applyMonthSelection(monthSelection.startDate, dateKey);
|
||||
setMonthSelection({ ...monthSelection, endDate: dateKey });
|
||||
}
|
||||
|
||||
function editEvent(event, occurrence = null) {
|
||||
|
|
|
|||
|
|
@ -86,7 +86,6 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
|
|||
async function addImageFiles(toolboxId, moduleId, files) {
|
||||
const imageFiles = [...files].filter((file) => file?.type?.startsWith("image/"));
|
||||
if (!imageFiles.length) return false;
|
||||
const data = store.getModuleData(toolboxId, moduleId, { images: [] });
|
||||
const images = [];
|
||||
for (const file of imageFiles) {
|
||||
const image = { id: uid("image"), dataUrl: await compressImage(file) };
|
||||
|
|
@ -94,6 +93,7 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
|
|||
if (label) image.label = label;
|
||||
images.unshift(image);
|
||||
}
|
||||
const data = store.getModuleData(toolboxId, moduleId, { images: [] });
|
||||
return store.updateModuleData(toolboxId, moduleId, {
|
||||
...data,
|
||||
images: [...images, ...(Array.isArray(data.images) ? data.images : [])]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Rôle : affiche la librairie des outils avec des exemples locaux non persistés.
|
||||
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Suspense, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Icon } from "../components/Icon.jsx";
|
||||
import { LIBRARY_CATEGORY_DEFINITIONS, getToolboxModuleDefinition, TOOLBOX_MODULE_LOADERS } from "../features/toolboxes/toolboxModuleRegistry.js";
|
||||
import { LIBRARY_CATEGORY_DEFINITIONS, getToolboxModuleDefinition } from "../features/toolboxes/toolboxModuleRegistry.js";
|
||||
import { MODULE_COMPONENTS } from "../features/toolboxes/modules/toolboxModuleComponents.jsx";
|
||||
import {
|
||||
clampQty,
|
||||
hostnameFromUrl,
|
||||
|
|
@ -29,8 +30,6 @@ import {
|
|||
const LIBRARY_TOOLBOX_ID = "library";
|
||||
const LIBRARY_ANCHOR_RETRY_DELAYS = [0, 80, 240, 600];
|
||||
|
||||
const MODULE_COMPONENTS = Object.fromEntries(Object.entries(TOOLBOX_MODULE_LOADERS).map(([type, loader]) => [type, lazy(loader)]));
|
||||
|
||||
function cloneData(value) {
|
||||
if (value === undefined) return {};
|
||||
if (typeof structuredClone === "function") return structuredClone(value);
|
||||
|
|
@ -326,7 +325,7 @@ function LibraryDocNav({ content, categories, activeCategoryKey, onSelectCategor
|
|||
|
||||
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 { 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";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue