Allow toolbox tools to collapse into their headers
All checks were successful
Deploy Sokko G / deploy (push) Successful in 17s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 17s
This commit is contained in:
parent
1df7b32dbb
commit
b5901b31e3
8 changed files with 228 additions and 166 deletions
|
|
@ -188,6 +188,7 @@ Style courant :
|
||||||
- hover violet/violet clair moins intense que les cards ;
|
- hover violet/violet clair moins intense que les cards ;
|
||||||
- pas de mouvement au hover ;
|
- pas de mouvement au hover ;
|
||||||
- contenu interne au-dessus des pseudo-bordures via `z-index`.
|
- contenu interne au-dessus des pseudo-bordures via `z-index`.
|
||||||
|
- chevron de repli tout à droite de l'en-tête ; l'état replié conserve uniquement l'en-tête visible et retire son séparateur bas.
|
||||||
|
|
||||||
Le mot visible cote utilisateur est **outil**, pas **module**. Le terme module reste acceptable uniquement dans le code.
|
Le mot visible cote utilisateur est **outil**, pas **module**. Le terme module reste acceptable uniquement dans le code.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,8 @@ Les toolboxes sont stockées dans `kv/toolboxes`.
|
||||||
{
|
{
|
||||||
"id": "mod1",
|
"id": "mod1",
|
||||||
"type": "checklist",
|
"type": "checklist",
|
||||||
"title": "Armures"
|
"title": "Armures",
|
||||||
|
"collapsed": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"moduleColumns": 2,
|
"moduleColumns": 2,
|
||||||
|
|
@ -137,6 +138,7 @@ Notes :
|
||||||
- Chaque identifiant d'outil présent dans `modules` doit apparaître exactement une fois dans `moduleOrder.one` et exactement une fois dans l'une des deux colonnes de `moduleOrder.two`.
|
- Chaque identifiant d'outil présent dans `modules` doit apparaître exactement une fois dans `moduleOrder.one` et exactement une fois dans l'une des deux colonnes de `moduleOrder.two`.
|
||||||
- Si une ancienne toolbox n'a pas encore `moduleOrder.two`, les outils sont répartis automatiquement depuis l'ordre une colonne avec une moitié en colonne gauche et une moitié en colonne droite.
|
- Si une ancienne toolbox n'a pas encore `moduleOrder.two`, les outils sont répartis automatiquement depuis l'ordre une colonne avec une moitié en colonne gauche et une moitié en colonne droite.
|
||||||
- Les données lourdes des outils ne sont pas stockées dans la toolbox, mais dans `modules`.
|
- Les données lourdes des outils ne sont pas stockées dans la toolbox, mais dans `modules`.
|
||||||
|
- `modules[].collapsed` vaut `true` lorsque seul l'en-tête de l'outil est affiché ; il est omis lorsque l'outil est déplié.
|
||||||
- `tutorial` est un indicateur temporaire réservé aux toolboxes créées depuis la prise en main. Il déclenche les premiers repères à l'ouverture, puis il est retiré automatiquement du stockage.
|
- `tutorial` est un indicateur temporaire réservé aux toolboxes créées depuis la prise en main. Il déclenche les premiers repères à l'ouverture, puis il est retiré automatiquement du stockage.
|
||||||
|
|
||||||
## Liens Jeu / Toolbox
|
## Liens Jeu / Toolbox
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
const moduleComponents = await readFile("website/src/features/toolboxes/modules/toolboxModuleComponents.jsx", "utf8");
|
const moduleComponents = await readFile("website/src/features/toolboxes/modules/toolboxModuleComponents.jsx", "utf8");
|
||||||
const addToolControls = await readFile("website/src/features/toolboxes/modules/AddToolControls.jsx", "utf8");
|
const addToolControls = await readFile("website/src/features/toolboxes/modules/AddToolControls.jsx", "utf8");
|
||||||
const toolboxModules = await readFile("website/src/features/toolboxes/modules/ToolboxModules.jsx", "utf8");
|
const toolboxModules = await readFile("website/src/features/toolboxes/modules/ToolboxModules.jsx", "utf8");
|
||||||
const moduleRegistryBundle = `${moduleRegistry}\n${moduleComponents}\n${addToolControls}\n${toolboxModules}`;
|
const moduleShell = await readFile("website/src/features/toolboxes/modules/ModuleShell.jsx", "utf8");
|
||||||
|
const moduleRegistryBundle = `${moduleRegistry}\n${moduleComponents}\n${addToolControls}\n${toolboxModules}\n${moduleShell}`;
|
||||||
const moduleMetadataRegistry = await readFile("website/src/features/toolboxes/toolboxModuleRegistry.js", "utf8");
|
const moduleMetadataRegistry = await readFile("website/src/features/toolboxes/toolboxModuleRegistry.js", "utf8");
|
||||||
const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8");
|
const notepadModule = await readFile("website/src/features/toolboxes/modules/NotepadModule.jsx", "utf8");
|
||||||
const notepadToolbar = await readFile("website/src/features/toolboxes/modules/NotepadToolbar.jsx", "utf8");
|
const notepadToolbar = await readFile("website/src/features/toolboxes/modules/NotepadToolbar.jsx", "utf8");
|
||||||
|
|
@ -189,6 +190,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
[
|
[
|
||||||
["AddToolControls.jsx", addToolControls],
|
["AddToolControls.jsx", addToolControls],
|
||||||
["ToolboxModules.jsx", toolboxModules],
|
["ToolboxModules.jsx", toolboxModules],
|
||||||
|
["ModuleShell.jsx", moduleShell],
|
||||||
["useCalendarController.js", calendarController],
|
["useCalendarController.js", calendarController],
|
||||||
["useCalendarDraftControls.js", calendarDraftControls],
|
["useCalendarDraftControls.js", calendarDraftControls],
|
||||||
["calendarEventActions.js", calendarEventActions],
|
["calendarEventActions.js", calendarEventActions],
|
||||||
|
|
@ -212,21 +214,24 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
||||||
assert.match(moduleRegistry, /export \{ AddToolControls \}/);
|
assert.match(moduleRegistry, /export \{ AddToolControls \}/);
|
||||||
assert.match(moduleRegistry, /export \{ ToolboxModules \}/);
|
assert.match(moduleRegistry, /export \{ ToolboxModules \}/);
|
||||||
assert.match(moduleComponents, /MODULE_COMPONENTS/);
|
assert.match(moduleComponents, /MODULE_COMPONENTS/);
|
||||||
assert.match(toolboxModules, /Suspense fallback=\{<ModuleLoadingFallback label=\{label\} \/>\}/);
|
assert.match(moduleShell, /Suspense fallback=\{<ModuleLoadingFallback label=\{label\} \/>\}/);
|
||||||
assert.match(moduleMetadataRegistry, /Planificateur d'équipements/);
|
assert.match(moduleMetadataRegistry, /Planificateur d'équipements/);
|
||||||
assert.match(moduleMetadataRegistry, /Annotation d'images/);
|
assert.match(moduleMetadataRegistry, /Annotation d'images/);
|
||||||
assert.match(toolboxModules, /module-edit-button/);
|
assert.match(moduleShell, /module-edit-button/);
|
||||||
assert.match(toolboxModules, /itemReorder\.startDrag/);
|
assert.match(moduleShell, /itemReorder\.startDrag/);
|
||||||
assert.match(moduleRegistryBundle, /onPointerDown/);
|
assert.match(moduleRegistryBundle, /onPointerDown/);
|
||||||
assert.match(toolboxModules, /useGroupedReorder/);
|
assert.match(toolboxModules, /useGroupedReorder/);
|
||||||
assert.match(toolboxModules, /useInlineEdit/);
|
assert.match(moduleShell, /useInlineEdit/);
|
||||||
assert.match(toolboxModules, /getContentEditableProps/);
|
assert.match(moduleShell, /getContentEditableProps/);
|
||||||
assert.match(addToolControls, /tool-add-card/);
|
assert.match(addToolControls, /tool-add-card/);
|
||||||
assert.match(addToolControls, /tool-quick-add-button/);
|
assert.match(addToolControls, /tool-quick-add-button/);
|
||||||
assert.match(addToolControls, /tool-add-quick-toggle/);
|
assert.match(addToolControls, /tool-add-quick-toggle/);
|
||||||
assert.match(moduleMetadataRegistry, /scrollable: true/);
|
assert.match(moduleMetadataRegistry, /scrollable: true/);
|
||||||
assert.match(toolboxModules, /module-content/);
|
assert.match(moduleShell, /module-content/);
|
||||||
assert.match(toolboxModules, /module-scroll-button/);
|
assert.match(moduleShell, /module-scroll-button/);
|
||||||
|
assert.match(moduleShell, /module-collapse-button/);
|
||||||
|
assert.match(moduleShell, /aria-expanded=\{!collapsed\}/);
|
||||||
|
assert.match(moduleShell, /!collapsed &&/);
|
||||||
assert.match(addToolControls, /Icon name="dropdown"/);
|
assert.match(addToolControls, /Icon name="dropdown"/);
|
||||||
assert.match(addToolControls, /CompactDropdown/);
|
assert.match(addToolControls, /CompactDropdown/);
|
||||||
assert.doesNotMatch(source, /<select value="" onChange=\{\(event\) => addModule/);
|
assert.doesNotMatch(source, /<select value="" onChange=\{\(event\) => addModule/);
|
||||||
|
|
|
||||||
|
|
@ -968,6 +968,19 @@ test("toolbox module order normalizes one and two column layouts", () => {
|
||||||
}, modules).two, [["a", "b", "c", "d"], []]);
|
}, modules).two, [["a", "b", "c", "d"], []]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("toolbox modules preserve only active shell display options", () => {
|
||||||
|
const toolbox = normalizeToolbox({
|
||||||
|
modules: [
|
||||||
|
{ id: "collapsed", type: "notepad", collapsed: true, scrollable: true },
|
||||||
|
{ id: "expanded", type: "links", collapsed: false }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(toolbox.modules[0].collapsed, true);
|
||||||
|
assert.equal(toolbox.modules[0].scrollable, true);
|
||||||
|
assert.equal("collapsed" in toolbox.modules[1], false);
|
||||||
|
});
|
||||||
|
|
||||||
test("toolbox export remaps module order ids", () => {
|
test("toolbox export remaps module order ids", () => {
|
||||||
const payload = createToolboxExportPayload(
|
const payload = createToolboxExportPayload(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
186
website/src/features/toolboxes/modules/ModuleShell.jsx
Normal file
186
website/src/features/toolboxes/modules/ModuleShell.jsx
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
// Rôle : rend l'en-tête, les contrôles et le contenu repliable d'un outil toolbox.
|
||||||
|
import { Suspense, useState } from "react";
|
||||||
|
import { Icon } from "../../../components/Icon.jsx";
|
||||||
|
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
||||||
|
import { GuideChecklistControls, GuideChecklistToggle } 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 = "", onRename, onUpdateModule, onDelete }) {
|
||||||
|
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 guidePanel = useGuideChecklistPanel(context.guideChecklist, module);
|
||||||
|
const hasAddPanel = definition.editable && definition.addable !== false;
|
||||||
|
const hasTextExchange = supportsTextExchange(module.type);
|
||||||
|
const scrollable = definition.scrollable && module.scrollable === true;
|
||||||
|
const collapsed = module.collapsed === true;
|
||||||
|
const className = [
|
||||||
|
"module",
|
||||||
|
reorder.isItemDragging(module.id) ? "is-dragging" : "",
|
||||||
|
reorder.isItemDropTarget(module.id) ? "is-drop-target" : "",
|
||||||
|
reorder.getDropPlacement("item", module.id) === "after" ? "drop-after" : "",
|
||||||
|
scrollable ? "is-scrollable" : "",
|
||||||
|
collapsed ? "is-collapsed" : ""
|
||||||
|
].filter(Boolean).join(" ");
|
||||||
|
const componentProps = {
|
||||||
|
toolboxId: toolbox.id,
|
||||||
|
moduleId: module.id,
|
||||||
|
context,
|
||||||
|
editing,
|
||||||
|
setEditing,
|
||||||
|
openEditor: () => {
|
||||||
|
setTextExchangeOpen(false);
|
||||||
|
setEditing(true);
|
||||||
|
},
|
||||||
|
textExchangeOpen
|
||||||
|
};
|
||||||
|
|
||||||
|
function toggleCollapsed() {
|
||||||
|
if (!collapsed) {
|
||||||
|
setEditing(false);
|
||||||
|
setTextExchangeOpen(false);
|
||||||
|
}
|
||||||
|
onUpdateModule(module.id, (currentModule) => {
|
||||||
|
const nextModule = { ...currentModule };
|
||||||
|
if (collapsed) {
|
||||||
|
delete nextModule.collapsed;
|
||||||
|
} else {
|
||||||
|
nextModule.collapsed = true;
|
||||||
|
}
|
||||||
|
return nextModule;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className={className}
|
||||||
|
data-module-type={module.type}
|
||||||
|
{...reorder.getItemProps({ itemId: module.id, groupId: itemGroup, parentId: toolbox.id })}
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="module-drag-handle nds-icon-button"
|
||||||
|
data-size="md"
|
||||||
|
data-variant="secondary"
|
||||||
|
type="button"
|
||||||
|
onPointerDown={(event) => reorder.itemReorder.startDrag(event, module.id)}
|
||||||
|
aria-label={`Déplacer ${module.title || label}`}
|
||||||
|
title="Déplacer"
|
||||||
|
>
|
||||||
|
<Icon name="drag" />
|
||||||
|
</button>
|
||||||
|
<span className="module-icon" aria-hidden="true">
|
||||||
|
<span className={`module-icon-svg module-icon-${definition.icon || "notepad"}`} />
|
||||||
|
</span>
|
||||||
|
<EditableModuleTitle value={module.title || label} fallback={label} tutorialTarget={module.type === "memos" ? "moduleTitle" : ""} onSave={(title) => onRename(module.id, title)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{definition.scrollable && (
|
||||||
|
<button
|
||||||
|
className={`module-scroll-button nds-switch ${scrollable ? "active" : ""}`}
|
||||||
|
data-size="lg"
|
||||||
|
onClick={() => onUpdateModule(module.id, (currentModule) => {
|
||||||
|
const nextModule = { ...currentModule };
|
||||||
|
if (scrollable) {
|
||||||
|
delete nextModule.scrollable;
|
||||||
|
} else {
|
||||||
|
nextModule.scrollable = true;
|
||||||
|
}
|
||||||
|
return nextModule;
|
||||||
|
})}
|
||||||
|
aria-label={`${scrollable ? "Désactiver" : "Activer"} le scroll de ${module.title || label}`}
|
||||||
|
aria-pressed={scrollable}
|
||||||
|
title={scrollable ? "Désactiver le scroll" : "Activer le scroll"}
|
||||||
|
>
|
||||||
|
<Icon name="scrollable" />
|
||||||
|
<i aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{hasAddPanel && (
|
||||||
|
<button
|
||||||
|
className={`module-edit-button nds-icon-button ${editing ? "active" : ""}`}
|
||||||
|
data-size="md"
|
||||||
|
onClick={() => setEditing((value) => {
|
||||||
|
const nextEditing = !value;
|
||||||
|
if (nextEditing) setTextExchangeOpen(false);
|
||||||
|
return nextEditing;
|
||||||
|
})}
|
||||||
|
aria-label={`${editing ? "Masquer" : "Afficher"} l'ajout de ${module.title || label}`}
|
||||||
|
aria-pressed={editing}
|
||||||
|
title={editing ? "Masquer l'ajout" : "Ajouter"}
|
||||||
|
>
|
||||||
|
<Icon name="add" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<GuideChecklistToggle checklist={context.guideChecklist} panel={guidePanel} label={module.title || label} />
|
||||||
|
{hasTextExchange && (
|
||||||
|
<button
|
||||||
|
className={`module-exchange-button nds-icon-button ${textExchangeOpen ? "active" : ""}`}
|
||||||
|
data-size="md"
|
||||||
|
onClick={() => setTextExchangeOpen((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"}
|
||||||
|
>
|
||||||
|
<Icon name="import" />
|
||||||
|
</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">
|
||||||
|
<span className="ui-icon ui-icon-trash" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="module-collapse-button nds-icon-button"
|
||||||
|
data-size="md"
|
||||||
|
type="button"
|
||||||
|
onClick={toggleCollapsed}
|
||||||
|
aria-expanded={!collapsed}
|
||||||
|
aria-label={`${collapsed ? "Afficher" : "Masquer"} ${module.title || label}`}
|
||||||
|
title={collapsed ? "Afficher l’outil" : "Masquer l’outil"}
|
||||||
|
>
|
||||||
|
<Icon name={collapsed ? "chevron-down" : "chevron-up"} />
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditableModuleTitle({ value, fallback, onSave, tutorialTarget = "" }) {
|
||||||
|
const inlineEdit = useInlineEdit({
|
||||||
|
value,
|
||||||
|
onCommit: (title) => onSave(title || fallback),
|
||||||
|
blurOnEscape: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<h2
|
||||||
|
{...inlineEdit.getContentEditableProps({
|
||||||
|
className: "module-title",
|
||||||
|
"data-toolbox-tutorial-target": tutorialTarget || undefined,
|
||||||
|
title: "Cliquer pour renommer"
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</h2>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,7 @@
|
||||||
// Rôle : rend les modules d'une toolbox, leur shell commun et leur réorganisation.
|
// Rôle : répartit et réorganise les modules d'une toolbox dans leurs colonnes.
|
||||||
import { Suspense, useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import { Icon } from "../../../components/Icon.jsx";
|
|
||||||
import { GuideChecklistControls, GuideChecklistToggle } from "../../games/GuideChecklistControls.jsx";
|
|
||||||
import { useGuideChecklistPanel } from "../../games/useGuideChecklist.js";
|
|
||||||
import { useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
import { useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
|
||||||
import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
import { ModuleShell } from "./ModuleShell.jsx";
|
||||||
import { supportsTextExchange } from "./textImport.js";
|
|
||||||
import { MODULE_COMPONENTS } from "./toolboxModuleComponents.jsx";
|
|
||||||
import { ModuleDataGate, ModuleLoadingFallback } from "./ModuleDataGate.jsx";
|
|
||||||
|
|
||||||
const MODULE_COLUMN_IDS = ["left", "right"];
|
const MODULE_COLUMN_IDS = ["left", "right"];
|
||||||
|
|
||||||
|
|
@ -148,149 +142,3 @@ export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onUp
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRename, onUpdateModule, onDelete }) {
|
|
||||||
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 guidePanel = useGuideChecklistPanel(context.guideChecklist, module);
|
|
||||||
const hasAddPanel = definition.editable && definition.addable !== false;
|
|
||||||
const hasTextExchange = supportsTextExchange(module.type);
|
|
||||||
const scrollable = definition.scrollable && module.scrollable === true;
|
|
||||||
const className = [
|
|
||||||
"module",
|
|
||||||
reorder.isItemDragging(module.id) ? "is-dragging" : "",
|
|
||||||
reorder.isItemDropTarget(module.id) ? "is-drop-target" : "",
|
|
||||||
reorder.getDropPlacement("item", module.id) === "after" ? "drop-after" : "",
|
|
||||||
scrollable ? "is-scrollable" : ""
|
|
||||||
].filter(Boolean).join(" ");
|
|
||||||
const componentProps = {
|
|
||||||
toolboxId: toolbox.id,
|
|
||||||
moduleId: module.id,
|
|
||||||
context,
|
|
||||||
editing,
|
|
||||||
setEditing,
|
|
||||||
openEditor: () => {
|
|
||||||
setTextExchangeOpen(false);
|
|
||||||
setEditing(true);
|
|
||||||
},
|
|
||||||
textExchangeOpen
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<article
|
|
||||||
className={className}
|
|
||||||
data-module-type={module.type}
|
|
||||||
{...reorder.getItemProps({ itemId: module.id, groupId: itemGroup, parentId: toolbox.id })}
|
|
||||||
>
|
|
||||||
<header>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
className="module-drag-handle nds-icon-button"
|
|
||||||
data-size="md"
|
|
||||||
data-variant="secondary"
|
|
||||||
type="button"
|
|
||||||
onPointerDown={(event) => reorder.itemReorder.startDrag(event, module.id)}
|
|
||||||
aria-label={`Déplacer ${module.title || label}`}
|
|
||||||
title="Déplacer"
|
|
||||||
>
|
|
||||||
<Icon name="drag" />
|
|
||||||
</button>
|
|
||||||
<span className="module-icon" aria-hidden="true">
|
|
||||||
<span className={`module-icon-svg module-icon-${definition.icon || "notepad"}`} />
|
|
||||||
</span>
|
|
||||||
<EditableModuleTitle value={module.title || label} fallback={label} tutorialTarget={module.type === "memos" ? "moduleTitle" : ""} onSave={(title) => onRename(module.id, title)} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{definition.scrollable && (
|
|
||||||
<button
|
|
||||||
className={`module-scroll-button nds-switch ${scrollable ? "active" : ""}`}
|
|
||||||
data-size="lg"
|
|
||||||
onClick={() => onUpdateModule(module.id, (currentModule) => {
|
|
||||||
const nextModule = { ...currentModule };
|
|
||||||
if (scrollable) {
|
|
||||||
delete nextModule.scrollable;
|
|
||||||
} else {
|
|
||||||
nextModule.scrollable = true;
|
|
||||||
}
|
|
||||||
return nextModule;
|
|
||||||
})}
|
|
||||||
aria-label={`${scrollable ? "Désactiver" : "Activer"} le scroll de ${module.title || label}`}
|
|
||||||
aria-pressed={scrollable}
|
|
||||||
title={scrollable ? "Désactiver le scroll" : "Activer le scroll"}
|
|
||||||
>
|
|
||||||
<Icon name="scrollable" />
|
|
||||||
<i aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{hasAddPanel && (
|
|
||||||
<button
|
|
||||||
className={`module-edit-button nds-icon-button ${editing ? "active" : ""}`}
|
|
||||||
data-size="md"
|
|
||||||
onClick={() => setEditing((value) => {
|
|
||||||
const nextEditing = !value;
|
|
||||||
if (nextEditing) setTextExchangeOpen(false);
|
|
||||||
return nextEditing;
|
|
||||||
})}
|
|
||||||
aria-label={`${editing ? "Masquer" : "Afficher"} l'ajout de ${module.title || label}`}
|
|
||||||
aria-pressed={editing}
|
|
||||||
title={editing ? "Masquer l'ajout" : "Ajouter"}
|
|
||||||
>
|
|
||||||
<Icon name="add" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<GuideChecklistToggle checklist={context.guideChecklist} panel={guidePanel} label={module.title || label} />
|
|
||||||
{hasTextExchange && (
|
|
||||||
<button
|
|
||||||
className={`module-exchange-button nds-icon-button ${textExchangeOpen ? "active" : ""}`}
|
|
||||||
data-size="md"
|
|
||||||
onClick={() => setTextExchangeOpen((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"}
|
|
||||||
>
|
|
||||||
<Icon name="import" />
|
|
||||||
</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">
|
|
||||||
<span className="ui-icon ui-icon-trash" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EditableModuleTitle({ value, fallback, onSave, tutorialTarget = "" }) {
|
|
||||||
const inlineEdit = useInlineEdit({
|
|
||||||
value,
|
|
||||||
onCommit: (title) => onSave(title || fallback),
|
|
||||||
blurOnEscape: true
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<h2
|
|
||||||
{...inlineEdit.getContentEditableProps({
|
|
||||||
className: "module-title",
|
|
||||||
"data-toolbox-tutorial-target": tutorialTarget || undefined,
|
|
||||||
title: "Cliquer pour renommer"
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</h2>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ function normalizeToolboxModule(module) {
|
||||||
const normalized = { id: module.id || uid("mod"), type: module.type };
|
const normalized = { id: module.id || uid("mod"), type: module.type };
|
||||||
if (title && title !== getDefaultModuleTitle(module.type)) normalized.title = title;
|
if (title && title !== getDefaultModuleTitle(module.type)) normalized.title = title;
|
||||||
if (module.scrollable === true) normalized.scrollable = true;
|
if (module.scrollable === true) normalized.scrollable = true;
|
||||||
|
if (module.collapsed === true) normalized.collapsed = true;
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module.is-collapsed header {
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.module-loading-fallback {
|
.module-loading-fallback {
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
|
|
@ -84,12 +88,14 @@
|
||||||
|
|
||||||
.module-edit-button,
|
.module-edit-button,
|
||||||
.module-exchange-button,
|
.module-exchange-button,
|
||||||
.module-delete-button {
|
.module-delete-button,
|
||||||
|
.module-collapse-button {
|
||||||
flex: 0 0 40px;
|
flex: 0 0 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-edit-button,
|
.module-edit-button,
|
||||||
.module-exchange-button {
|
.module-exchange-button,
|
||||||
|
.module-collapse-button {
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue