From 34524d2961f8c73dfde4240b448d5ff589a256a2 Mon Sep 17 00:00:00 2001 From: Shinuwa Date: Thu, 10 Sep 2026 14:19:55 +0200 Subject: [PATCH] Share toolbox components and fix image and calendar updates --- .beads/interactions.jsonl | 2 + docs/FEATURE_CHECKLIST.md | 3 +- e2e/library.spec.js | 24 ++++++++++ e2e/toolbox.spec.js | 26 ++++++++++ tests/static-app.test.mjs | 3 +- tests/static-toolboxes.test.mjs | 22 --------- tests/toolbox-image-actions.test.mjs | 41 ++++++++++++++++ .../toolboxes/modules/ToolboxModules.jsx | 10 ++-- .../src/features/toolboxes/modules/index.jsx | 2 +- .../modules/toolboxModuleComponents.jsx | 48 +++---------------- .../modules/useCalendarController.js | 18 +++---- .../features/toolboxes/useToolboxActions.js | 2 +- website/src/pages/LibraryPage.jsx | 9 ++-- 13 files changed, 117 insertions(+), 93 deletions(-) create mode 100644 e2e/library.spec.js create mode 100644 tests/toolbox-image-actions.test.mjs diff --git a/.beads/interactions.jsonl b/.beads/interactions.jsonl index 7180a5e..48dc9a7 100644 --- a/.beads/interactions.jsonl +++ b/.beads/interactions.jsonl @@ -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."}} diff --git a/docs/FEATURE_CHECKLIST.md b/docs/FEATURE_CHECKLIST.md index e0c7c6b..1f3166a 100644 --- a/docs/FEATURE_CHECKLIST.md +++ b/docs/FEATURE_CHECKLIST.md @@ -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/.js`, puis vérifier l'export public dans `website/src/features/toolboxes/storage/toolboxStorage.js`. diff --git a/e2e/library.spec.js b/e2e/library.spec.js new file mode 100644 index 0000000..c0f12b7 --- /dev/null +++ b/e2e/library.spec.js @@ -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([]); +}); diff --git a/e2e/toolbox.spec.js b/e2e/toolbox.spec.js index d35e0ea..7ab975e 100644 --- a/e2e/toolbox.spec.js +++ b/e2e/toolbox.spec.js @@ -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"); diff --git a/tests/static-app.test.mjs b/tests/static-app.test.mjs index ae557b7..346af9f 100644 --- a/tests/static-app.test.mjs +++ b/tests/static-app.test.mjs @@ -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"\)/); diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index 0f65cb1..7dd1be4 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -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=\{\}/); - 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/); diff --git a/tests/toolbox-image-actions.test.mjs b/tests/toolbox-image-actions.test.mjs new file mode 100644 index 0000000..b6a8f2c --- /dev/null +++ b/tests/toolbox-image-actions.test.mjs @@ -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); +}); diff --git a/website/src/features/toolboxes/modules/ToolboxModules.jsx b/website/src/features/toolboxes/modules/ToolboxModules.jsx index dc2349c..69182bd 100644 --- a/website/src/features/toolboxes/modules/ToolboxModules.jsx +++ b/website/src/features/toolboxes/modules/ToolboxModules.jsx @@ -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
- {LAZY_MODULE_TYPES.has(module.type) ? ( - }> - - - ) : ( + }> - )} +
); diff --git a/website/src/features/toolboxes/modules/index.jsx b/website/src/features/toolboxes/modules/index.jsx index 772abb2..aa3d637 100644 --- a/website/src/features/toolboxes/modules/index.jsx +++ b/website/src/features/toolboxes/modules/index.jsx @@ -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"; diff --git a/website/src/features/toolboxes/modules/toolboxModuleComponents.jsx b/website/src/features/toolboxes/modules/toolboxModuleComponents.jsx index 7e44c30..27f40b3 100644 --- a/website/src/features/toolboxes/modules/toolboxModuleComponents.jsx +++ b/website/src/features/toolboxes/modules/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) } +])); diff --git a/website/src/features/toolboxes/modules/useCalendarController.js b/website/src/features/toolboxes/modules/useCalendarController.js index 4653261..286a7a3 100644 --- a/website/src/features/toolboxes/modules/useCalendarController.js +++ b/website/src/features/toolboxes/modules/useCalendarController.js @@ -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) { diff --git a/website/src/features/toolboxes/useToolboxActions.js b/website/src/features/toolboxes/useToolboxActions.js index 9b1614d..adefd1f 100644 --- a/website/src/features/toolboxes/useToolboxActions.js +++ b/website/src/features/toolboxes/useToolboxActions.js @@ -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 : [])] diff --git a/website/src/pages/LibraryPage.jsx b/website/src/pages/LibraryPage.jsx index 0d8646b..9ce5215 100644 --- a/website/src/pages/LibraryPage.jsx +++ b/website/src/pages/LibraryPage.jsx @@ -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";