Add planner-aware memos and unified tool configuration panels
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s

This commit is contained in:
Shinuwa 2026-10-03 15:58:33 +02:00
parent acd9b2fe11
commit 808610dcf2
53 changed files with 842 additions and 329 deletions

View file

@ -1,23 +1,26 @@
// Rôle : rend l'en-tête, les contrôles et le contenu repliable d'un outil toolbox.
import { Suspense, useEffect, useRef, useState } from "react";
import { Suspense, useEffect, useId, useRef, useState } from "react";
import { Icon } from "../../../components/Icon.jsx";
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
import { GuideChecklistControls, GuideChecklistToggle } from "../../games/GuideChecklistControls.jsx";
import { GuideChecklistControls } from "../../games/GuideChecklistControls.jsx";
import { useGuideChecklistPanel } from "../../games/useGuideChecklist.js";
import { ModuleDataGate, ModuleLoadingFallback } from "./ModuleDataGate.jsx";
import { supportsTextExchange } from "./textImport.js";
import { MODULE_COMPONENTS } from "./toolboxModuleComponents.jsx";
export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", spaces = [], activeSpaceId = "", moveText = "Déplacer vers", scrollIntoView = false, onScrolledIntoView, onRename, onUpdateModule, onDelete, onMoveToSpace }) {
export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", spaces = [], activeSpaceId = "", moveText = "Déplacer vers", configurationText = {}, scrollIntoView = false, onScrolledIntoView, onRename, onUpdateModule, onDelete, onMoveToSpace }) {
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
const Component = definition.Component;
const label = definition.label || module.type;
const [editing, setEditing] = useState(false);
const [textExchangeOpen, setTextExchangeOpen] = useState(false);
const [configurationOpen, setConfigurationOpen] = useState(false);
const [configurationTarget, setConfigurationTarget] = useState(null);
const configurationId = useId();
const moduleRef = useRef(null);
const guidePanel = useGuideChecklistPanel(context.guideChecklist, module);
const hasAddPanel = definition.editable && definition.addable !== false;
const hasTextExchange = supportsTextExchange(module.type);
const hasConfiguration = spaces.length > 1 || Boolean(guidePanel.list) || hasTextExchange;
const scrollable = definition.scrollable && module.scrollable === true;
const collapsed = module.collapsed === true;
const className = [
@ -35,10 +38,10 @@ export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "",
editing,
setEditing,
openEditor: () => {
setTextExchangeOpen(false);
setConfigurationOpen(false);
setEditing(true);
},
textExchangeOpen
configurationTarget
};
useEffect(() => {
@ -50,10 +53,19 @@ export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "",
return () => window.cancelAnimationFrame(frame);
}, [onScrolledIntoView, scrollIntoView]);
useEffect(() => {
if (!configurationOpen) return undefined;
const closeOnEscape = (event) => {
if (event.key === "Escape" && !document.querySelector('[aria-modal="true"]')) setConfigurationOpen(false);
};
window.addEventListener("keydown", closeOnEscape);
return () => window.removeEventListener("keydown", closeOnEscape);
}, [configurationOpen]);
function toggleCollapsed() {
if (!collapsed) {
setEditing(false);
setTextExchangeOpen(false);
setConfigurationOpen(false);
}
onUpdateModule(module.id, (currentModule) => {
const nextModule = { ...currentModule };
@ -74,8 +86,8 @@ export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "",
data-module-id={module.id}
{...reorder.getItemProps({ itemId: module.id, groupId: itemGroup, parentId: toolbox.id })}
>
<header>
<div>
<header className="module-header">
<div className="module-header-main">
<button
className="module-drag-handle nds-icon-button"
data-size="md"
@ -92,20 +104,7 @@ export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "",
</span>
<EditableModuleTitle value={module.title || label} fallback={label} tutorialTarget={module.type === "memos" ? "moduleTitle" : ""} onSave={(title) => onRename(module.id, title)} />
</div>
<div>
{spaces.length > 1 && (
<select
className="module-space-select nds-select"
data-size="sm"
value=""
aria-label={`${moveText} — ${module.title || label}`}
title={moveText}
onChange={(event) => onMoveToSpace(module.id, event.target.value)}
>
<option value="" disabled>{moveText}</option>
{spaces.filter((space) => space.id !== activeSpaceId).map((space) => <option key={space.id} value={space.id}>{space.name}</option>)}
</select>
)}
<div className="module-header-actions">
{definition.scrollable && (
<button
className={`module-scroll-button nds-switch ${scrollable ? "active" : ""}`}
@ -133,7 +132,7 @@ export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "",
data-size="md"
onClick={() => setEditing((value) => {
const nextEditing = !value;
if (nextEditing) setTextExchangeOpen(false);
if (nextEditing) setConfigurationOpen(false);
return nextEditing;
})}
aria-label={`${editing ? "Masquer" : "Afficher"} l'ajout de ${module.title || label}`}
@ -143,21 +142,23 @@ export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "",
<Icon name="add" />
</button>
)}
<GuideChecklistToggle checklist={context.guideChecklist} panel={guidePanel} label={module.title || label} />
{hasTextExchange && (
{hasConfiguration && (
<button
className={`module-exchange-button nds-icon-button ${textExchangeOpen ? "active" : ""}`}
className={`module-configuration-button nds-icon-button ${configurationOpen ? "active" : ""}`}
data-size="md"
onClick={() => setTextExchangeOpen((value) => {
data-guide-linked={guidePanel.list && context.guideChecklist?.selected?.id === guidePanel.list.id || undefined}
type="button"
onClick={() => setConfigurationOpen((value) => {
const nextOpen = !value;
if (nextOpen) setEditing(false);
return nextOpen;
})}
aria-label={`${textExchangeOpen ? "Masquer" : "Afficher"} l'import/export de ${module.title || label}`}
aria-pressed={textExchangeOpen}
title={textExchangeOpen ? "Masquer import/export" : "Import / export"}
aria-label={configurationText.buttonLabel || "Configuration"}
aria-expanded={configurationOpen}
aria-controls={configurationId}
title={configurationText.buttonLabel || "Configuration"}
>
<Icon name="import" />
<Icon name="settings" />
</button>
)}
<button className="module-delete-button nds-icon-button" data-size="md" data-variant="danger" onClick={() => onDelete(module.id)} aria-label={`Retirer ${module.title || label}`} data-toolbox-tutorial-target={module.type === "links" ? "deleteTool" : undefined} title="Retirer">
@ -176,14 +177,45 @@ export function ModuleShell({ toolbox, module, context, reorder, itemGroup = "",
</button>
</div>
</header>
{!collapsed && (
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
<GuideChecklistControls checklist={context.guideChecklist} {...guidePanel} />
<Suspense fallback={<ModuleLoadingFallback label={label} />}>
<ModuleDataGate key={`${toolbox.id}:${module.id}`} context={context} toolboxId={toolbox.id} moduleId={module.id}>
<Component {...componentProps} />
</ModuleDataGate>
</Suspense>
{configurationOpen && (
<section
id={configurationId}
className="module-configuration-panel"
aria-label={`${configurationText.panelLabel || "Configuration de"} ${module.title || label}`}
>
{spaces.length > 1 && (
<section className="module-configuration-section module-configuration-organization">
<h3>{configurationText.organizationTitle || "Espace"}</h3>
<select
className="module-space-select nds-select"
data-size="sm"
value=""
aria-label={configurationText.spaceLabel || moveText}
onChange={(event) => onMoveToSpace(module.id, event.target.value)}
>
<option value="" disabled>{moveText}</option>
{spaces.filter((space) => space.id !== activeSpaceId).map((space) => <option key={space.id} value={space.id}>{space.name}</option>)}
</select>
</section>
)}
{guidePanel.list && (
<section className="module-configuration-section module-configuration-specific">
<h3>{configurationText.guideTitle || "Guide"}</h3>
<GuideChecklistControls checklist={context.guideChecklist} {...guidePanel} open />
</section>
)}
<div className="module-configuration-target" ref={setConfigurationTarget} />
</section>
)}
{(!collapsed || configurationOpen) && (
<div className="module-component-host" hidden={collapsed}>
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
<Suspense fallback={<ModuleLoadingFallback label={label} />}>
<ModuleDataGate key={`${toolbox.id}:${module.id}`} context={context} toolboxId={toolbox.id} moduleId={module.id}>
<Component {...componentProps} />
</ModuleDataGate>
</Suspense>
</div>
</div>
)}
</article>