Add planner-aware memos and unified tool configuration panels
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
This commit is contained in:
parent
acd9b2fe11
commit
808610dcf2
53 changed files with 842 additions and 329 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue