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

@ -96,6 +96,15 @@
"kind": "iconButton",
"icon": "eye-open"
}
},
{
"group": "Bande",
"icon": "settings",
"title": "Lier un ou tous les planificateurs de tâches",
"preview": {
"kind": "iconButton",
"icon": "settings"
}
}
],
"features": [
@ -112,6 +121,7 @@
"text": "Tous les mémos apparaissent dans une bande compacte quand la bande est active.",
"details": [
"La bande peut être masquée sans retirer les mémos.",
"Les paramètres permettent d'ajouter les tâches non effectuées qui approchent de leur reset depuis un planificateur précis ou tous ceux de la toolbox.",
"Les couleurs restent des accents localisés pour conserver une lecture calme."
]
}
@ -1832,6 +1842,16 @@
"emptyText": "Cet espace est vide. Ajoutez-y un outil pour commencer.",
"limitReached": "Limite de 5 espaces atteinte"
},
"moduleConfiguration": {
"buttonLabel": "Configuration",
"panelLabel": "Configuration de",
"organizationTitle": "Espace",
"spaceLabel": "Déplacer vers un autre espace",
"guideTitle": "Guide",
"dataTitle": "Données",
"importButtonLabel": "Importer",
"exportButtonLabel": "Exporter"
},
"eyebrow": "Données locales",
"title": "Toolboxes",
"newButton": "Nouvelle toolbox",
@ -2001,10 +2021,16 @@
"deleteTitle": "Supprimer le mémo",
"colorLabel": "Couleur du mémo",
"reorderTitle": "Déplacer",
"tickerLabel": "Mémos",
"hideTickerTitle": "Masquer la bande",
"showTickerTitle": "Afficher la bande",
"importPlaceholder": "- Pour la reproduction, combine Pierre Stase + Nœud Destin pour transmettre nature et IV. | color=#f6c453\n- Les IV obtenus par Hyper Training ne sont pas transmis aux oeufs. | color=#8b5cf6\n- Pour une équipe Distorsion, vise 0 IV en Vitesse sur les Pokémon lents. | color=#22d3ee\n- Utilise les objets Pouvoir pour cibler précisément les EV à entraîner. | color=#d946ef",
"tickerLabel": "Mémos",
"hideTickerTitle": "Masquer la bande",
"showTickerTitle": "Afficher la bande",
"settingsTitle": "Paramètres des mémos",
"plannerLinkLabel": "Planificateur lié",
"plannerNoneLabel": "Aucun",
"plannerAllLabel": "Tous les planificateurs",
"plannerDefaultLabel": "Planificateur de tâches",
"taskAlertTemplate": "Tâche : {category}{title} expire bientôt",
"importPlaceholder": "- Pour la reproduction, combine Pierre Stase + Nœud Destin pour transmettre nature et IV. | color=#f6c453\n- Les IV obtenus par Hyper Training ne sont pas transmis aux oeufs. | color=#8b5cf6\n- Pour une équipe Distorsion, vise 0 IV en Vitesse sur les Pokémon lents. | color=#22d3ee\n- Utilise les objets Pouvoir pour cibler précisément les EV à entraîner. | color=#d946ef",
"importOpenButton": "Importer du texte",
"exportOpenButton": "Exporter en format texte",
"importModalTitle": "Importer des mémos",
@ -2639,7 +2665,6 @@
"exportSuccess": "Export de tâches copié.",
"importError": "Format de tâches invalide.",
"settingsTitle": "Réglages du planificateur",
"settingsButtonLabel": "Paramètres des réinitialisations",
"hideCompletedTitle": "Masquer les tâches effectuées",
"globalWeeklyResetDayLabel": "Reset hebdo",
"resetTimeLabel": "Heure de reset",

View file

@ -1,30 +1,14 @@
// Rôle : expose le suivi du guide dans chaque checklist compatible et marque les cartes terminées.
import { Icon } from "../../components/Icon.jsx";
export function GuideChecklistToggle({ checklist, panel, label }) {
if (!panel.list) return null;
return <button
className={`module-guide-button nds-icon-button ${panel.open ? "active" : ""}`}
data-size="md"
data-linked={checklist.selected?.id === panel.list.id || undefined}
type="button"
aria-label={`${checklist.content.label} — ${label}`}
title={checklist.content.label}
aria-expanded={panel.open}
aria-pressed={panel.open}
aria-controls={panel.id}
onClick={panel.toggle}
><Icon name="link" /></button>;
}
export function GuideChecklistControls({ checklist, list, id, open }) {
if (!list) return null;
const { content, selected, hideCompleted } = checklist;
const active = selected?.id === list.id;
return <div id={id} hidden={!open} className="guide-checklist-controls" data-linked={active || undefined}>
<div className="guide-checklist-actions">
<button type="button" className="guide-checklist-link nds-button" data-size="sm" data-variant={active ? "primary" : "secondary"} aria-pressed={active} onClick={() => checklist.associate(active ? "" : list.id)}>{active ? content.unlink : content.associate}</button>
<button type="button" className="nds-switch" disabled={!active} aria-pressed={active && hideCompleted} onClick={checklist.toggleHidden}><Icon name="hide" /><span>{content.hideCompleted}</span><i aria-hidden="true" /></button>
<button type="button" className="guide-checklist-link nds-button" data-size="xs" data-variant={active ? "primary" : "secondary"} aria-pressed={active} onClick={() => checklist.associate(active ? "" : list.id)}>{active ? content.unlink : content.associate}</button>
<button type="button" className="nds-switch" data-size="xs" disabled={!active} aria-pressed={active && hideCompleted} onClick={checklist.toggleHidden}><Icon name="hide" /><span>{content.hideCompleted}</span><i aria-hidden="true" /></button>
</div>
<p>{list.count ? `${list.count} ${content.matches}` : content.noMatches}</p>
</div>;

View file

@ -1,14 +1,13 @@
// Rôle : partage la progression réactive et les commandes de liaison entre les guides.
import { createContext, useContext, useId, useMemo, useState } from "react";
import { createContext, useContext, useId, useMemo } from "react";
import { getGuideChecklistEntries, matchGuideChecklist, toggleGuideChecklistRows } from "./guideChecklist.js";
export const GuideChecklistContext = createContext(null);
export function useGuideChecklistPanel(checklist, module) {
const id = useId();
const [open, setOpen] = useState(false);
const list = module.type === "checklist" ? checklist?.lists.find((item) => item.id === module.id) : undefined;
return { id, open, list, toggle: () => setOpen((value) => !value) };
return { id, list };
}
export function useGuideChecklist(requestedGuideId) {

View file

@ -1,7 +1,8 @@
// Rôle : affiche en bas de page les mémos actifs de la toolbox courante ou liée au jeu.
import { useMemo } from "react";
// Rôle : affiche en bas de page les mémos et alertes de tâches de la toolbox courante ou liée au jeu.
import { useEffect, useMemo, useState } from "react";
import { moduleStorageKey, normalizeMemosData } from "./storage/toolboxStorage.js";
import { MemosTicker } from "./modules/MemosTicker.jsx";
import { getPlannerTaskTickerItems, resolveMemosPlannerLink } from "./modules/memosPlannerUtils.js";
function getRouteToolboxId(route, links) {
const toolboxMatch = route.match(/^\/toolbox\/([^/]+)/);
@ -11,6 +12,13 @@ function getRouteToolboxId(route, links) {
}
export function GlobalMemosTicker({ route, toolboxes, links, moduleData, textContent }) {
const [nowMs, setNowMs] = useState(() => Date.now());
useEffect(() => {
const intervalId = window.setInterval(() => setNowMs(Date.now()), 60000);
return () => window.clearInterval(intervalId);
}, []);
const items = useMemo(() => {
const toolboxId = getRouteToolboxId(route, links);
const toolbox = toolboxes.find((item) => item.id === toolboxId);
@ -20,9 +28,20 @@ export function GlobalMemosTicker({ route, toolboxes, links, moduleData, textCon
.flatMap((module) => {
const data = normalizeMemosData(moduleData[moduleStorageKey(toolbox.id, module.id)]);
if (!data.tickerEnabled) return [];
return data.items.map((memo) => ({ ...memo, id: `${module.id}:${memo.id}` }));
const plannerLink = resolveMemosPlannerLink(data.plannerLink, toolbox.modules);
const taskItems = getPlannerTaskTickerItems({
plannerLink,
modules: toolbox.modules,
getPlannerData: (plannerId) => moduleData[moduleStorageKey(toolbox.id, plannerId)],
nowMs,
alertTemplate: textContent?.taskAlertTemplate
});
return [
...data.items.map((memo) => ({ ...memo, id: `${module.id}:${memo.id}` })),
...taskItems.map((item) => ({ ...item, id: `${module.id}:${item.id}` }))
];
});
}, [route, toolboxes, links, moduleData]);
}, [route, toolboxes, links, moduleData, nowMs, textContent?.taskAlertTemplate]);
if (!items.length) return null;
return (

View file

@ -11,6 +11,7 @@ import { useInlineEdit } from "../../hooks/useInlineEdit.js";
import { compressImage } from "../../utils/imageCompression.js";
import { getGameCardCover, ToolboxCard, ToolboxGameIcon, ToolboxIconPicker } from "./ToolboxCard.jsx";
import { AddToolControls, ToolboxModules } from "./modules/index.jsx";
import { getMemosDataAfterPlannerRemoval } from "./modules/memosPlannerUtils.js";
import { ToolboxSpaces } from "./ToolboxSpaces.jsx";
import {
addModuleToToolboxSpace,
@ -195,7 +196,9 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
isModuleDataReady: actions.isModuleDataReady,
storageStatus: siteContent.toolboxes.storageStatus,
storageReady,
toolboxModules: toolbox.modules,
moduleText,
moduleConfigurationText: siteContent.toolboxes.moduleConfiguration,
normalizeChecklistData,
normalizeCombosData,
normalizeLinksData,
@ -403,6 +406,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
context={moduleContext}
reorder={moduleReorder}
spacesText={spacesText}
configurationText={siteContent.toolboxes.moduleConfiguration}
scrollToModuleId={pendingModuleId}
onModuleScrolled={() => setPendingModuleId("")}
onRename={(moduleId, title) => updateToolbox({ ...toolbox, modules: toolbox.modules.map((module) => module.id === moduleId ? { ...module, title } : module) })}
@ -419,6 +423,15 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
danger: true,
onResolve: (confirmed) => {
if (!confirmed) return;
const removedModule = toolbox.modules.find((module) => module.id === moduleId);
const remainingModules = toolbox.modules.filter((module) => module.id !== moduleId);
if (removedModule?.type === "taskPlanner") {
toolbox.modules.filter((module) => module.type === "memos").forEach((memosModule) => {
const memosData = normalizeMemosData(getModuleData(toolbox.id, memosModule.id, { items: [] }));
const nextMemosData = getMemosDataAfterPlannerRemoval(memosData, moduleId, remainingModules);
if (nextMemosData !== memosData) updateModuleData(toolbox.id, memosModule.id, nextMemosData, "memos");
});
}
actions.removeModuleData(toolbox.id, moduleId);
updateToolbox(removeModuleFromToolboxSpaces(toolbox, moduleId));
}

View file

@ -4,7 +4,7 @@ import { Icon } from "../../../components/Icon.jsx";
import { MixTagsInput } from "../../../components/MixTagsInput.jsx";
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
import { CalculatorEntries, FormattedCalculatorNumber } from "./CalculatorEntries.jsx";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
import {
BASE_TOKEN,
calculateRemainingValue,
@ -36,7 +36,7 @@ function focusExpressionInputAtEnd(element) {
selection?.addRange(range);
}
export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpen }) {
export function CalculatorModule({ toolboxId, moduleId, context, configurationTarget }) {
const data = context.normalizeCalculatorData(context.getModuleData(toolboxId, moduleId, { entries: [] }));
const calculatedEntries = useMemo(() => evaluateCalculatorEntries(data.entries), [data.entries]);
const calculatedData = useMemo(() => ({ ...data, entries: calculatedEntries }), [data, calculatedEntries]);
@ -223,11 +223,7 @@ export function CalculatorModule({ toolboxId, moduleId, context, textExchangeOpe
return (
<div className="calculator-module">
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="calculator" data={calculatedData} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "calculator")} />
</div>
)}
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="calculator" data={calculatedData} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "calculator")} />
<form className="calculator-form" onSubmit={saveResult}>
<div className="calculator-card" ref={calculatorCardRef}>
<div className="calculator-field">

View file

@ -2,7 +2,7 @@
import { Icon } from "../../../components/Icon.jsx";
import { CalendarEventForm } from "./CalendarEventForm.jsx";
import { CalendarMonthView, CalendarWeekGrid } from "./CalendarViews.jsx";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
import {
addMonths,
formatLocalDateKey,
@ -31,7 +31,7 @@ export function CalendarContent({
nowDayIndex,
nowMinutes,
hideEmptyHours,
textExchangeOpen,
configurationTarget,
onKeyDown,
onSubmit,
onDraftChange,
@ -74,11 +74,7 @@ export function CalendarContent({
onDisplayedMonthChange={onDisplayedMonthChange}
/>
)}
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="calendar" data={data} context={context} textContent={textContent} onImport={onImport} />
</div>
)}
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="calendar" data={data} context={context} textContent={textContent} onImport={onImport} />
{data.viewMode === "week" && (
<CalendarWeekOptions textContent={textContent} hideEmptyHours={hideEmptyHours} onToggle={onHideEmptyHoursToggle} />

View file

@ -3,7 +3,7 @@ import { CalendarContent } from "./CalendarContent.jsx";
import { CalendarModeChoice } from "./CalendarModeChoice.jsx";
import { useCalendarController } from "./useCalendarController.js";
export function CalendarModule({ toolboxId, moduleId, context, editing = false, setEditing, textExchangeOpen }) {
export function CalendarModule({ toolboxId, moduleId, context, editing = false, setEditing, configurationTarget }) {
const data = context.normalizeCalendarData(context.getModuleData(toolboxId, moduleId, { initialized: false, viewMode: "week", events: [] }));
const textContent = context.moduleText?.calendar || {};
const controller = useCalendarController({ data, context, toolboxId, moduleId, editing, setEditing, textContent });
@ -18,5 +18,5 @@ export function CalendarModule({ toolboxId, moduleId, context, editing = false,
);
}
return <CalendarContent {...controller.contentProps} textExchangeOpen={textExchangeOpen} />;
return <CalendarContent {...controller.contentProps} configurationTarget={configurationTarget} />;
}

View file

@ -5,7 +5,7 @@ import { Icon } from "../../../components/Icon.jsx";
import { useGroupedReorder, moveItem } from "../../../hooks/useGroupedReorder.js";
import { ChecklistSection } from "./ChecklistParts.jsx";
import { ModuleEmptyMessage } from "./ModuleEmptyMessage.jsx";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
import {
appendChecklistItems,
getChecklistReorderItems,
@ -17,7 +17,7 @@ import {
updateChecklistItem
} from "./checklistUtils.js";
export function ChecklistModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
export function ChecklistModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
const data = context.normalizeChecklistData(context.getModuleData(toolboxId, moduleId, { items: [] }));
const textContent = context.moduleText?.checklist || {};
const [label, setLabel] = useState("");
@ -123,11 +123,7 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing, textExc
</form>
</div>
)}
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="checklist" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "checklist")} />
</div>
)}
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="checklist" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "checklist")} />
<div className="checklist-toolbar">
<button
className={`checklist-hide-completed-toggle nds-switch ${data.hideCompletedItems ? "active" : ""}`}

View file

@ -1,7 +1,7 @@
// Rôle : fournit l'outil Combos avec palettes d'inputs, rendu visuel et échange texte.
import { toggleListValue } from "../../../utils/collectionUtils.js";
import { getGroupedEntries } from "../../../hooks/useGroupedReorder.js";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
import { CombosEditor } from "./CombosEditor.jsx";
import { CombosList } from "./CombosList.jsx";
import {
@ -10,7 +10,7 @@ import {
import { useCombosEditorState } from "./useCombosEditorState.js";
import { useCombosReorder } from "./useCombosReorder.js";
export function CombosModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
export function CombosModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
const data = context.normalizeCombosData(context.getModuleData(toolboxId, moduleId, { combos: [] }));
const textContent = context.moduleText?.combos || {};
const categoryOptions = [
@ -86,11 +86,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing, textExchan
onComboEditCancel={editorState.cancelComboEdit}
/>
)}
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="combos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
</div>
)}
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="combos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
{!data.combos.length && (
<p className="empty-state">{textContent.emptyCombos || "Aucun combo enregistré."}</p>

View file

@ -2,10 +2,10 @@
import { Icon } from "../../../components/Icon.jsx";
import { useDraftForm } from "../../../hooks/useDraftForm.js";
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
import { ModuleEmptyMessage } from "./ModuleEmptyMessage.jsx";
export function CountersModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
export function CountersModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
const data = context.normalizeCountersData(context.getModuleData(toolboxId, moduleId, { counters: [] }));
const textContent = context.moduleText?.counters || {};
const counterDraft = useDraftForm({ label: "" });
@ -53,11 +53,7 @@ export function CountersModule({ toolboxId, moduleId, context, editing, textExch
</form>
</div>
)}
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="counters" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "counters")} />
</div>
)}
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="counters" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "counters")} />
{!data.counters.length && <ModuleEmptyMessage>{textContent.emptyCounters || "Aucun compteur."}</ModuleEmptyMessage>}
<div className="counters-grid">
{data.counters.map((counter) => {

View file

@ -1,13 +1,13 @@
// Rôle : fournit l'outil de planification d'équipements, sertissages, résumé et échange texte.
import { useMemo, useState } from "react";
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
import { EQUIPMENT_ICONS } from "./equipmentPlannerOptions.js";
import { EquipmentItem } from "./EquipmentPlannerItem.jsx";
import { IconChoiceDropdown } from "./EquipmentPlannerPickers.jsx";
import { EquipmentPlannerSummary } from "./EquipmentPlannerSummary.jsx";
export function EquipmentPlannerModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
export function EquipmentPlannerModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
const data = context.normalizeEquipmentPlannerData(context.getModuleData(toolboxId, moduleId, { types: [], equipments: [] }));
const summary = useMemo(() => context.summarizeEquipmentPlannerData(data), [context, data]);
const textContent = context.moduleText?.equipmentPlanner || {};
@ -114,11 +114,7 @@ export function EquipmentPlannerModule({ toolboxId, moduleId, context, editing,
</form>
</div>
)}
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="equipmentPlanner" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
</div>
)}
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="equipmentPlanner" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
<EquipmentPlannerSummary summary={summary} textContent={textContent} />

View file

@ -1,10 +1,10 @@
// Rôle : fournit l'outil arbre d'objectifs avec quantités, formules, resets et échange texte.
import { GoalTreeEditor } from "./GoalTreeEditor.jsx";
import { GoalTreeView } from "./GoalTreeView.jsx";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
import { useGoalTreeController } from "./useGoalTreeController.js";
export function GoalTreeModule({ toolboxId, moduleId, context, editing, setEditing = () => {}, openEditor = () => {}, textExchangeOpen }) {
export function GoalTreeModule({ toolboxId, moduleId, context, editing, setEditing = () => {}, openEditor = () => {}, configurationTarget }) {
const controller = useGoalTreeController({ toolboxId, moduleId, context, editing, setEditing, openEditor });
return (
@ -30,11 +30,7 @@ export function GoalTreeModule({ toolboxId, moduleId, context, editing, setEditi
onToggleLinkedParent={controller.toggleLinkedParent}
/>
)}
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="goalTree" data={controller.evaluatedData} context={context} textContent={controller.textContent} onImport={controller.importData} />
</div>
)}
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="goalTree" data={controller.evaluatedData} context={context} textContent={controller.textContent} onImport={controller.importData} />
<GoalTreeView
evaluatedNodes={controller.evaluatedNodes}
@ -46,7 +42,6 @@ export function GoalTreeModule({ toolboxId, moduleId, context, editing, setEditi
verticalView={controller.verticalView}
viewScale={controller.viewScale}
storageReady={controller.storageReady}
textExchangeOpen={textExchangeOpen}
onSelect={controller.setSelectedNodeId}
onAdd={controller.addNode}
onAddContentItem={controller.addNodeContentItem}

View file

@ -15,7 +15,6 @@ export function GoalTreeView({
verticalView,
viewScale,
storageReady,
textExchangeOpen,
onSelect,
onAdd,
onAddContentItem,
@ -57,7 +56,7 @@ export function GoalTreeView({
timeoutIds.forEach(clearTimeout);
window.removeEventListener("load", refreshConnectors);
};
}, [storageReady, connectorTopologyKey, evaluatedNodes.length, editing, textExchangeOpen]);
}, [storageReady, connectorTopologyKey, evaluatedNodes.length, editing]);
useLayoutEffect(() => {
const canvas = treeViewRef.current;
@ -82,7 +81,7 @@ export function GoalTreeView({
cancelAnimationFrame(frameId);
resizeObserver.disconnect();
};
}, [connectorTopologyKey, editing, textExchangeOpen, viewScale]);
}, [connectorTopologyKey, editing, viewScale]);
return (
<section className={`goal-tree-view tool-split-results ${verticalView ? "is-vertical" : "is-horizontal"} ${editing ? "is-editing" : ""}`} aria-label={textContent.treeTitle || "Arbre d'objectifs"}>

View file

@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { Icon } from "../../../components/Icon.jsx";
import { Tooltip } from "../../../components/Tooltip.jsx";
import { DrawingOverlay } from "./DrawingOverlay.jsx";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
import { useKeyedState } from "../../../hooks/useKeyedState.js";
function readTemporaryDrawings(sessionKey) {
@ -19,7 +19,7 @@ function markerLabel(index, marker, textContent) {
return marker.label || `${textContent.markerPrefix || "Marqueur"} ${index + 1}`;
}
export function ImageAnnotationModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
export function ImageAnnotationModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
const data = context.normalizeImageAnnotationData(context.getModuleData(toolboxId, moduleId, { image: "", markers: [] }));
const textContent = context.moduleText?.imageAnnotation || {};
const notify = context.notify;
@ -123,14 +123,10 @@ export function ImageAnnotationModule({ toolboxId, moduleId, context, editing, t
</div>
</div>
)}
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="imageAnnotation" data={data} context={context} textContent={textContent} onImport={(nextData) => {
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="imageAnnotation" data={data} context={context} textContent={textContent} onImport={(nextData) => {
save(nextData);
setTemporaryDrawings({ strokes: [] });
}} />
</div>
)}
{data.image ? (
<div className="annotation-workspace">

View file

@ -3,9 +3,9 @@ import { useState } from "react";
import { Icon } from "../../../components/Icon.jsx";
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
import { ModuleEmptyMessage } from "./ModuleEmptyMessage.jsx";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
export function ImagesModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
export function ImagesModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
const data = context.getModuleData(toolboxId, moduleId, { images: [] });
const textContent = context.moduleText?.images || {};
const [dragOver, setDragOver] = useState(false);
@ -99,11 +99,7 @@ export function ImagesModule({ toolboxId, moduleId, context, editing, textExchan
</div>
</div>
)}
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="images" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "images")} />
</div>
)}
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="images" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "images")} />
{data.images.length > 0 && (
<div className="images-toolbar">
<button className="nds-button" data-size="sm" type="button" onClick={() => openCollection("gallery")}>

View file

@ -5,7 +5,7 @@ import { CategoryAutocompleteInput } from "../../../components/CategoryAutocompl
import { Icon } from "../../../components/Icon.jsx";
import { useDraftForm } from "../../../hooks/useDraftForm.js";
import { applyGroupedReorderOperation, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
import { ModuleEmptyMessage } from "./ModuleEmptyMessage.jsx";
function getLinkCategory(link) {
@ -32,7 +32,7 @@ function getLinkEntries(data) {
return [...ungrouped, ...categories];
}
export function LinksModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
export function LinksModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
const data = context.normalizeLinksData(context.getModuleData(toolboxId, moduleId, { links: [] }));
const textContent = context.moduleText?.links || {};
const linkDraft = useDraftForm({ title: "", url: "", category: "" });
@ -169,11 +169,7 @@ export function LinksModule({ toolboxId, moduleId, context, editing, textExchang
</form>
</div>
)}
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="links" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "links")} />
</div>
)}
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="links" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "links")} />
{!data.links.length && <ModuleEmptyMessage>{textContent.emptyLinks || "Aucun lien."}</ModuleEmptyMessage>}
<div className="links-category-list">
{getLinkEntries(data).map((entry) => {

View file

@ -1,18 +1,35 @@
// Rôle : fournit l'outil de mémos courts avec post-it, bande déroulante et tri manuel.
// Rôle : fournit les mémos courts, leur tri et les alertes de planificateur dans la bande déroulante.
import { CompactDropdown } from "../../../components/CompactDropdown.jsx";
import { Icon } from "../../../components/Icon.jsx";
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
import { useMemo } from "react";
import { useEffect, useMemo, useState } from "react";
import {
ALL_TASK_PLANNERS_LINK,
getPlannerTaskTickerItems,
getTaskPlannerModules,
resolveMemosPlannerLink
} from "./memosPlannerUtils.js";
import { MemoTextEditor } from "./MemoTextEditor.jsx";
import { MemosTicker } from "./MemosTicker.jsx";
import { ModuleConfigurationPortal } from "./ModuleConfiguration.jsx";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { TEXT_COLORS } from "./notepadUtils.js";
export function MemosModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
export function MemosModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
const storedData = context.getModuleData(toolboxId, moduleId, { items: [] });
const data = useMemo(() => context.normalizeMemosData(storedData), [context, storedData]);
const textContent = context.moduleText?.memos || {};
const tickerItems = data.tickerEnabled ? data.items : [];
const [nowMs, setNowMs] = useState(() => Date.now());
const plannerModules = useMemo(() => getTaskPlannerModules(context.toolboxModules), [context.toolboxModules]);
const plannerLink = resolveMemosPlannerLink(data.plannerLink, plannerModules);
const plannerTickerItems = useMemo(() => getPlannerTaskTickerItems({
plannerLink,
modules: plannerModules,
getPlannerData: (plannerId) => context.getModuleData(toolboxId, plannerId, { tasks: [] }),
nowMs,
alertTemplate: textContent.taskAlertTemplate
}), [context, nowMs, plannerLink, plannerModules, textContent.taskAlertTemplate, toolboxId]);
const tickerItems = data.tickerEnabled ? [...data.items, ...plannerTickerItems] : [];
const {
itemReorder,
getItemProps,
@ -37,6 +54,15 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
save({ ...data, items });
}
useEffect(() => {
const intervalId = window.setInterval(() => setNowMs(Date.now()), 60000);
return () => window.clearInterval(intervalId);
}, []);
useEffect(() => {
if (data.plannerLink && !plannerLink) context.setModuleData(toolboxId, moduleId, { ...data, plannerLink: "" }, "memos");
}, [context, data, moduleId, plannerLink, toolboxId]);
function addMemo() {
const now = new Date().toISOString();
save({
@ -71,11 +97,33 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
return (
<div className="memos-module">
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="memos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
</div>
)}
<ModuleConfigurationPortal target={configurationTarget}>
<section className="module-configuration-section module-configuration-specific">
<h3>{textContent.settingsTitle || "Paramètres des mémos"}</h3>
<div className="memos-settings">
<label>
<span>{textContent.plannerLinkLabel || "Planificateur lié"}</span>
<select
className="nds-select"
data-size="xs"
value={plannerLink}
onChange={(event) => save({ ...data, plannerLink: event.target.value })}
aria-label={textContent.plannerLinkLabel || "Planificateur lié"}
>
<option value="">{textContent.plannerNoneLabel || "Aucun"}</option>
{plannerModules.length > 0 && <option value={ALL_TASK_PLANNERS_LINK}>{textContent.plannerAllLabel || "Tous les planificateurs"}</option>}
{plannerModules.map((planner) => (
<option key={planner.id} value={planner.id}>{planner.title || textContent.plannerDefaultLabel || "Planificateur de tâches"}</option>
))}
</select>
</label>
</div>
</section>
<section className="module-configuration-section module-configuration-data">
<h3>{context.moduleConfigurationText?.dataTitle || "Données"}</h3>
<TextExchangeActions type="memos" data={data} context={context} textContent={textContent} onImport={(nextData) => save({ ...data, items: nextData.items })} />
</section>
</ModuleConfigurationPortal>
{editing && (
<div className="module-add-panel">

View file

@ -1,11 +1,14 @@
// Rôle : affiche la bande déroulante partagée par l'outil Mémos et les pages associées.
// Rôle : affiche la bande déroulante partagée pour les mémos et alertes de tâches.
import { Icon } from "../../../components/Icon.jsx";
export function MemosTicker({ items, label = "Mémos", className = "" }) {
if (!items?.length) return null;
return (
<div className={["memos-ticker", className].filter(Boolean).join(" ")} aria-label={label}>
<div className="memos-ticker-track">
{[...items, ...items].map((memo, index) => (
<span className="memos-ticker-item" key={`${memo.id}-${index}`} style={{ "--memo-color": memo.color }}>
<span className={`memos-ticker-item ${memo.kind === "task-warning" ? "is-task-warning" : ""}`} key={`${memo.id}-${index}`} style={{ "--memo-color": memo.color }}>
{memo.kind === "task-warning" && <Icon name="warning" />}
{memo.text}
</span>
))}

View file

@ -0,0 +1,18 @@
// Rôle : déplace les réglages propres aux outils dans le panneau Configuration commun.
import { createPortal } from "react-dom";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
export function ModuleConfigurationPortal({ target, children }) {
return target ? createPortal(children, target) : null;
}
export function ModuleTextExchangeConfiguration({ target, title = "Données", ...props }) {
return (
<ModuleConfigurationPortal target={target}>
<section className="module-configuration-section module-configuration-data">
<h3>{title}</h3>
<TextExchangeActions {...props} />
</section>
</ModuleConfigurationPortal>
);
}

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>

View file

@ -5,7 +5,7 @@ import { Icon } from "../../../components/Icon.jsx";
import { DrawingControls } from "./DrawingControls.jsx";
import { DrawingOverlay } from "./DrawingOverlay.jsx";
import { NotepadToolbar } from "./NotepadToolbar.jsx";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
import { normalizeNotepadData } from "../storage/toolboxStorage.js";
import { useKeyedState } from "../../../hooks/useKeyedState.js";
import {
@ -31,7 +31,7 @@ function readTemporaryStrokes(sessionKey) {
}
}
export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }) {
export function NotepadModule({ toolboxId, moduleId, context, configurationTarget }) {
const storedData = context.getModuleData(toolboxId, moduleId, EMPTY_NOTEPAD_DATA);
const normalizedData = useMemo(() => normalizeNotepadData(storedData), [storedData]);
const notepadDataRef = useRef(normalizedData);
@ -295,11 +295,7 @@ export function NotepadModule({ toolboxId, moduleId, context, textExchangeOpen }
return (
<div className="notepad-module">
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="notepad" data={normalizedData} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "notepad")} />
</div>
)}
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="notepad" data={normalizedData} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "notepad")} />
<CompactDropdown
className="notepad-pages-dropdown"
menuClassName="notepad-pages-menu"

View file

@ -1,12 +1,12 @@
// Rôle : orchestre les onglets de l'outil Randomizer sans porter les sous-vues.
import { Tabs } from "../../../components/Tabs.jsx";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
import { RANDOMIZER_TABS } from "./randomizerUtils.js";
import { DiceRandomizer } from "./randomizer/RandomizerDice.jsx";
import { TeamsRandomizer } from "./randomizer/RandomizerTeams.jsx";
import { WheelRandomizer } from "./randomizer/RandomizerWheel.jsx";
export function RandomizerModule({ toolboxId, moduleId, context, textExchangeOpen }) {
export function RandomizerModule({ toolboxId, moduleId, context, configurationTarget }) {
const data = context.normalizeRandomizerData(context.getModuleData(toolboxId, moduleId, {}));
const textContent = context.moduleText?.randomizer || {};
@ -20,11 +20,7 @@ export function RandomizerModule({ toolboxId, moduleId, context, textExchangeOpe
return (
<>
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="randomizer" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "randomizer")} />
</div>
)}
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="randomizer" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "randomizer")} />
<div className="randomizer-tool">
<Tabs
tabs={RANDOMIZER_TABS.map((tab) => ({ ...tab, label: textContent[`${tab.id}Tab`] || tab.label }))}

View file

@ -4,7 +4,7 @@ import { Icon } from "../../../components/Icon.jsx";
import { Tabs } from "../../../components/Tabs.jsx";
import { useDraftForm } from "../../../hooks/useDraftForm.js";
import { useKeyedState } from "../../../hooks/useKeyedState.js";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
import {
RNG_SIMULATOR_DEFAULT_RATE,
RNG_SIMULATOR_HOURLY_DURATION_PRESETS,
@ -146,7 +146,7 @@ function getToneFromDelta(value) {
return value > 0 ? "positive" : "negative";
}
export function RngSimulatorModule({ toolboxId, moduleId, context, editing = false, setEditing = () => {}, textExchangeOpen }) {
export function RngSimulatorModule({ toolboxId, moduleId, context, editing = false, setEditing = () => {}, configurationTarget }) {
const rawData = context.getModuleData(toolboxId, moduleId, {});
const data = useMemo(() => context.normalizeRngSimulatorData(rawData), [context, rawData]);
const textContent = context.moduleText?.rngSimulator || {};
@ -405,11 +405,7 @@ export function RngSimulatorModule({ toolboxId, moduleId, context, editing = fal
return (
<>
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="rngSimulator" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "rngSimulator")} />
</div>
)}
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="rngSimulator" data={data} context={context} textContent={textContent} onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "rngSimulator")} />
<div className="rng-simulator-tool">
<section className="rng-simulator-panel rng-simulator-rates-panel">
<header>

View file

@ -1,6 +1,6 @@
// Rôle : fournit l'outil tableau avec cellules libres, formules simples et échange TSV.
import { useMemo } from "react";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
import { columnIndexToName, evaluateTable } from "./tableFormulaEngine.js";
import { TableGrid } from "./TableGrid.jsx";
import { TableToolbar } from "./TableToolbar.jsx";
@ -14,7 +14,7 @@ import {
} from "./tableUtils.js";
import { useTableInteractions } from "./useTableInteractions.js";
export function TableModule({ toolboxId, moduleId, context, textExchangeOpen }) {
export function TableModule({ toolboxId, moduleId, context, configurationTarget }) {
const rawData = context.getModuleData(toolboxId, moduleId, { rows: DEFAULT_ROWS, columns: DEFAULT_COLUMNS, cells: {} });
const data = useMemo(() => context.normalizeTableData(rawData), [context, rawData]);
const textContent = context.moduleText?.table || {};
@ -61,11 +61,7 @@ export function TableModule({ toolboxId, moduleId, context, textExchangeOpen })
return (
<div className="table-module">
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="table" data={data} context={context} textContent={textContent} exportLabel="Exporter TSV" onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "table")} />
</div>
)}
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="table" data={data} context={context} textContent={textContent} exportLabel="Exporter TSV" onImport={(nextData) => context.setModuleData(toolboxId, moduleId, nextData, "table")} />
<TableToolbar rows={data.rows} columns={data.columns} textContent={textContent} onResize={resize} onCopy={tableInteractions.copyTsv} />
<TableGrid
data={data}

View file

@ -3,15 +3,17 @@ import { toggleListValue } from "../../../utils/collectionUtils.js";
import { useEffect, useMemo, useRef, useState } from "react";
import { useDraftForm } from "../../../hooks/useDraftForm.js";
import { TaskPlannerBranch } from "./TaskPlannerBranch.jsx";
import { ModuleConfigurationPortal } from "./ModuleConfiguration.jsx";
import { TaskPlannerSettings } from "./TaskPlannerSettings.jsx";
import { TaskPlannerTopbar } from "./TaskPlannerTopbar.jsx";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import {
applyDueResets,
getTaskParentMap
} from "./taskPlannerUtils.js";
import { useTaskPlannerReorder } from "./useTaskPlannerReorder.js";
export function TaskPlannerModule({ toolboxId, moduleId, context, editing, textExchangeOpen }) {
export function TaskPlannerModule({ toolboxId, moduleId, context, editing, configurationTarget }) {
const data = context.normalizeTaskPlannerData(context.getModuleData(toolboxId, moduleId, { tasks: [] }));
const textContent = context.moduleText?.taskPlanner || {};
const taskDraft = useDraftForm({ title: "", type: "daily", category: "" });
@ -20,7 +22,6 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing, textE
...data.tasks.map((task) => task.category).filter(Boolean).filter((category) => !data.categoryOrder.includes(category))
], [data.categoryOrder, data.tasks]);
const [nowMs, setNowMs] = useState(() => Date.now());
const [settingsOpen, setSettingsOpen] = useState(false);
const [openDescriptions, setOpenDescriptions] = useState(() => new Set());
const [openTaskSettings, setOpenTaskSettings] = useState(() => new Set());
const [prerequisiteNoticeTaskIds, setPrerequisiteNoticeTaskIds] = useState(() => new Set());
@ -170,31 +171,32 @@ export function TaskPlannerModule({ toolboxId, moduleId, context, editing, textE
return (
<div className="task-planner-module">
<ModuleConfigurationPortal target={configurationTarget}>
<section className="module-configuration-section module-configuration-specific">
<h3>{textContent.settingsTitle || "Réglages du planificateur"}</h3>
<TaskPlannerSettings
data={data}
textContent={textContent}
onWeeklyResetDayChange={(weeklyResetDay) => save({ ...data, weeklyResetDay })}
onResetTimeChange={(resetTime) => save({ ...data, resetTime })}
onClearCompletedUniqueTasks={clearCompletedUniqueTasks}
/>
</section>
<section className="module-configuration-section module-configuration-data">
<h3>{context.moduleConfigurationText?.dataTitle || "Données"}</h3>
<TextExchangeActions type="taskPlanner" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
</section>
</ModuleConfigurationPortal>
<TaskPlannerTopbar
data={data}
context={context}
editing={editing}
settingsOpen={settingsOpen}
categoryOptions={categoryOptions}
taskDraft={taskDraft}
textContent={textContent}
textExchangeOpen={textExchangeOpen}
onAddTask={addTask}
onHideCompletedToggle={() => save({ ...data, hideCompleted: !data.hideCompleted })}
onSettingsToggle={() => setSettingsOpen((value) => !value)}
onImport={(nextData) => save(nextData)}
/>
{settingsOpen && (
<TaskPlannerSettings
data={data}
textContent={textContent}
onWeeklyResetDayChange={(weeklyResetDay) => save({ ...data, weeklyResetDay })}
onResetTimeChange={(resetTime) => save({ ...data, resetTime })}
onClearCompletedUniqueTasks={clearCompletedUniqueTasks}
/>
)}
<ul className="task-planner-list">
{data.tasks.length ? (
<TaskPlannerBranch

View file

@ -7,18 +7,18 @@ export function TaskPlannerSettings({ data, textContent, onWeeklyResetDayChange,
<section className="task-planner-settings" aria-label={textContent.settingsTitle || "Réglages du planificateur"}>
<label>
<span>{textContent.globalWeeklyResetDayLabel || "Reset hebdo"}</span>
<select className="nds-select" data-size="sm" value={data.weeklyResetDay} onChange={(event) => onWeeklyResetDayChange(Number(event.target.value))}>
<select className="nds-select" data-size="xs" value={data.weeklyResetDay} onChange={(event) => onWeeklyResetDayChange(Number(event.target.value))}>
{WEEK_DAYS.map((day) => <option key={day.value} value={day.value}>{day.label}</option>)}
</select>
</label>
<label>
<span>{textContent.resetTimeLabel || "Heure de reset"}</span>
<input className="nds-time" data-size="sm" type="time" value={data.resetTime} onChange={(event) => onResetTimeChange(event.target.value || "00:00")} />
<input className="nds-time" data-size="xs" type="time" value={data.resetTime} onChange={(event) => onResetTimeChange(event.target.value || "00:00")} />
</label>
<div className="task-planner-settings-actions">
<button
className="task-planner-clear-completed-button nds-button"
data-size="sm"
data-size="xs"
data-variant="danger"
type="button"
onClick={onClearCompletedUniqueTasks}

View file

@ -1,7 +1,6 @@
// Rôle : rend la barre haute du task planner, l'ajout rapide et l'échange texte.
// Rôle : rend les contrôles fréquents et l'ajout rapide du planificateur de tâches.
import { CategoryAutocompleteInput } from "../../../components/CategoryAutocompleteInput.jsx";
import { Icon } from "../../../components/Icon.jsx";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import {
TASK_TYPES,
getTaskTypeLabel
@ -9,17 +8,12 @@ import {
export function TaskPlannerTopbar({
data,
context,
editing,
settingsOpen,
categoryOptions,
taskDraft,
textContent,
textExchangeOpen,
onAddTask,
onHideCompletedToggle,
onSettingsToggle,
onImport
onHideCompletedToggle
}) {
return (
<div className="task-planner-topbar">
@ -36,18 +30,6 @@ export function TaskPlannerTopbar({
<Icon name={data.hideCompleted ? "eye-closed" : "eye-open"} />
<i aria-hidden="true" />
</button>
<button
className={`task-planner-settings-toggle nds-button ${settingsOpen ? "active" : ""}`}
data-size="sm"
type="button"
onClick={onSettingsToggle}
aria-expanded={settingsOpen}
aria-label={textContent.settingsTitle || "Réglages du planificateur"}
title={textContent.settingsTitle || "Réglages du planificateur"}
>
<Icon name="settings" />
<span>{textContent.settingsButtonLabel || "Paramètres des réinitialisations"}</span>
</button>
</div>
{editing && (
<div className="task-planner-add-stack">
@ -61,11 +43,6 @@ export function TaskPlannerTopbar({
</form>
</div>
)}
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="taskPlanner" data={data} context={context} textContent={textContent} onImport={onImport} />
</div>
)}
</div>
);
}

View file

@ -5,17 +5,20 @@ import { useTextExchange } from "./useTextExchange.js";
export function TextExchangeActions({ type, data, context, onImport, textContent = {}, exportLabel = "" }) {
const exchange = useTextExchange({ type, data, context, onImport, textContent });
const importAccessibleLabel = textContent.importOpenButton || "Importer du texte";
const exportAccessibleLabel = textContent.exportOpenButton || exportLabel || "Exporter en format texte";
const configurationText = context.moduleConfigurationText || {};
return (
<>
<div className="text-import-actions">
<button className="nds-button" data-size="sm" type="button" onClick={exchange.openImport}>
<button className="nds-button" data-size="sm" type="button" onClick={exchange.openImport} aria-label={importAccessibleLabel}>
<Icon name="import" />
<span>{textContent.importOpenButton || "Importer du texte"}</span>
<span>{configurationText.importButtonLabel || "Importer"}</span>
</button>
<button className="nds-button" data-size="sm" type="button" onClick={exchange.openExport} disabled={!exchange.canExport}>
<button className="nds-button" data-size="sm" type="button" onClick={exchange.openExport} disabled={!exchange.canExport} aria-label={exportAccessibleLabel}>
<Icon name="export" />
<span>{textContent.exportOpenButton || exportLabel || "Exporter en format texte"}</span>
<span>{configurationText.exportButtonLabel || "Exporter"}</span>
</button>
</div>
{exchange.mode === "import" && (

View file

@ -7,7 +7,10 @@ import { lockBodyScroll } from "../../../utils/bodyScrollLock.js";
export function TextImportModal({ title, value, placeholder, submitLabel, readOnly = false, error = "", info = "", onChange, onSubmit, onCopy, onClose }) {
useEffect(() => {
function handleKeyDown(event) {
if (event.key === "Escape") onClose();
if (event.key === "Escape") {
event.stopPropagation();
onClose();
}
}
const unlock = lockBodyScroll();

View file

@ -13,7 +13,7 @@ import {
timePartsToDurationMs,
timePartsToString
} from "./timerUtils.js";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { ModuleTextExchangeConfiguration } from "./ModuleConfiguration.jsx";
import { CountdownControls, CountdownList } from "./TimerCountdown.jsx";
import { StopwatchControls, StopwatchLapList } from "./TimerStopwatch.jsx";
import {
@ -30,7 +30,7 @@ import {
isAlertGuarded
} from "./timerOptions.js";
export function TimerModule({ toolboxId, moduleId, context, textExchangeOpen }) {
export function TimerModule({ toolboxId, moduleId, context, configurationTarget }) {
const rawData = context.getModuleData(toolboxId, moduleId, { activeTab: "stopwatch" });
const data = useMemo(() => context.normalizeTimerData(rawData), [context, rawData]);
const textContent = context.moduleText?.timer || {};
@ -252,11 +252,7 @@ export function TimerModule({ toolboxId, moduleId, context, textExchangeOpen })
return (
<div className="timer-module">
{textExchangeOpen && (
<div className="module-text-exchange-panel">
<TextExchangeActions type="timer" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
</div>
)}
<ModuleTextExchangeConfiguration target={configurationTarget} title={context.moduleConfigurationText?.dataTitle} type="timer" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
<section className="timer-control-card">
<Tabs
className="timer-tabs"

View file

@ -10,7 +10,7 @@ function getModulesByIds(modules, ids) {
return (Array.isArray(ids) ? ids : []).map((id) => moduleMap.get(id)).filter(Boolean);
}
export function ToolboxModules({ toolbox, space, moduleColumns, context, reorder, spacesText, scrollToModuleId = "", onModuleScrolled, onRename, onUpdateModule, onDelete, onMoveToSpace }) {
export function ToolboxModules({ toolbox, space, moduleColumns, context, reorder, spacesText, configurationText, scrollToModuleId = "", onModuleScrolled, onRename, onUpdateModule, onDelete, onMoveToSpace }) {
const twoColumnOrder = useMemo(() => space.moduleOrder?.two || [[], []], [space.moduleOrder?.two]);
const orderedModules = useMemo(() => getModulesByIds(toolbox.modules, space.moduleOrder?.one || []), [toolbox.modules, space.moduleOrder?.one]);
const columns = useMemo(() => [
@ -32,6 +32,7 @@ export function ToolboxModules({ toolbox, space, moduleColumns, context, reorder
spaces={toolbox.spaces}
activeSpaceId={space.id}
moveText={spacesText.moveLabel}
configurationText={configurationText}
onMoveToSpace={onMoveToSpace}
scrollIntoView={module.id === scrollToModuleId}
onScrolledIntoView={onModuleScrolled}
@ -65,6 +66,7 @@ export function ToolboxModules({ toolbox, space, moduleColumns, context, reorder
spaces={toolbox.spaces}
activeSpaceId={space.id}
moveText={spacesText.moveLabel}
configurationText={configurationText}
onMoveToSpace={onMoveToSpace}
scrollIntoView={module.id === scrollToModuleId}
onScrolledIntoView={onModuleScrolled}

View file

@ -0,0 +1,62 @@
// Rôle : résout les liaisons de Mémos vers les planificateurs et produit leurs alertes de reset.
import { normalizeTaskPlannerData } from "../storage/modules/taskPlanner.js";
import {
applyDueResets,
getEffectiveTaskCategory,
getTaskParentMap,
isTaskResetSoon
} from "./taskPlannerUtils.js";
export const ALL_TASK_PLANNERS_LINK = "all";
export function getTaskPlannerModules(modules) {
return (Array.isArray(modules) ? modules : []).filter((module) => module.type === "taskPlanner");
}
export function resolveMemosPlannerLink(plannerLink, modules) {
const value = String(plannerLink || "");
const planners = getTaskPlannerModules(modules);
if (value === ALL_TASK_PLANNERS_LINK) return planners.length ? value : "";
return planners.some((planner) => planner.id === value) ? value : "";
}
export function getLinkedTaskPlannerModules(plannerLink, modules) {
const planners = getTaskPlannerModules(modules);
if (plannerLink === ALL_TASK_PLANNERS_LINK) return planners;
return planners.filter((planner) => planner.id === plannerLink);
}
export function getMemosDataAfterPlannerRemoval(data, removedPlannerId, remainingModules) {
const remainingPlanners = getTaskPlannerModules(remainingModules);
const link = String(data?.plannerLink || "");
if (link === removedPlannerId || (link === ALL_TASK_PLANNERS_LINK && !remainingPlanners.length)) {
return { ...data, plannerLink: "" };
}
return data;
}
function formatTaskAlert(task, category, template) {
return String(template || "Tâche : {category}{title} expire bientôt")
.replace("{category}", category ? `${category} ` : "")
.replace("{title}", task.title);
}
export function getPlannerTaskTickerItems({
plannerLink,
modules,
getPlannerData,
nowMs = Date.now(),
alertTemplate
}) {
return getLinkedTaskPlannerModules(plannerLink, modules).flatMap((planner) => {
const data = applyDueResets(normalizeTaskPlannerData(getPlannerData(planner.id)), nowMs);
const parentMap = getTaskParentMap(data.tasks, data.relations);
return data.tasks
.filter((task) => isTaskResetSoon(task, data, nowMs))
.map((task) => ({
id: `planner:${planner.id}:${task.id}`,
kind: "task-warning",
text: formatTaskAlert(task, getEffectiveTaskCategory(task, data.tasks, parentMap), alertTemplate)
}));
});
}

View file

@ -1,4 +1,4 @@
// Rôle : normalise et compacte les mémos courts des toolboxes.
// Rôle : normalise et compacte les mémos courts et leur liaison éventuelle aux planificateurs.
import { NOTEPAD_ALLOWED_COLORS } from "./drawing.js";
import { normalizeIsoDate, uid } from "./shared.js";
@ -20,6 +20,7 @@ export function normalizeMemosData(data) {
const source = data && typeof data === "object" ? data : {};
return {
tickerEnabled: source.tickerEnabled !== false,
plannerLink: String(source.plannerLink || "").trim(),
items: (Array.isArray(source.items) ? source.items : []).map(normalizeMemoItem).filter(Boolean)
};
}
@ -28,6 +29,7 @@ export function compactMemosDataForStorage(value) {
const normalized = normalizeMemosData(value);
const compact = {};
if (normalized.tickerEnabled === false) compact.tickerEnabled = false;
if (normalized.plannerLink) compact.plannerLink = normalized.plannerLink;
if (normalized.items.length) {
compact.items = normalized.items.map((item) => {
const compactItem = {

View file

@ -242,6 +242,11 @@ export function compactModuleDataForStorage(type, value) {
return compactor ? compactor(value) : value;
}
export function remapMemosPlannerLink(data, resolveModuleId) {
if (!data || typeof data !== "object" || !data.plannerLink || data.plannerLink === "all") return data;
return { ...data, plannerLink: resolveModuleId(data.plannerLink) || "" };
}
export function prepareModuleDataForStorage(toolboxes, toolboxId, moduleId, value, moduleType = "") {
const module = toolboxes.find((toolbox) => toolbox.id === toolboxId)?.modules.find((item) => item.id === moduleId);
return compactModuleDataForStorage(moduleType || module?.type, value);
@ -255,7 +260,7 @@ function createExportIdFactory() {
};
}
function remapModuleDataForExport(type, data, nextId) {
function remapModuleDataForExport(type, data, nextId, moduleIdMap) {
const compact = compactModuleDataForStorage(type, data);
if (!compact) return null;
if (type === "checklist") {
@ -332,6 +337,8 @@ function remapModuleDataForExport(type, data, nextId) {
if (type === "memos") {
const remapped = {};
if (compact.tickerEnabled === false) remapped.tickerEnabled = false;
if (compact.plannerLink === "all") remapped.plannerLink = "all";
else if (compact.plannerLink && moduleIdMap.has(compact.plannerLink)) remapped.plannerLink = moduleIdMap.get(compact.plannerLink);
if (compact.items) remapped.items = compact.items.map((memo) => ({ ...memo, id: nextId("memo") }));
return remapped;
}
@ -490,7 +497,7 @@ export function createToolboxExportPayload(toolbox, moduleData) {
const modules = Object.fromEntries(source.modules
.map((module) => [
moduleIdMap.get(module.id),
remapModuleDataForExport(module.type, moduleData[moduleStorageKey(source.id, module.id)] || null, nextId)
remapModuleDataForExport(module.type, moduleData[moduleStorageKey(source.id, module.id)] || null, nextId, moduleIdMap)
])
.filter(([, data]) => data != null));
return { version: 2, toolbox: exportedToolbox, modules };

View file

@ -7,6 +7,7 @@ import {
labelFromFileName,
MAX_TOOLBOX_SPACES,
normalizeToolbox,
remapMemosPlannerLink,
remapModuleOrderIds,
uid
} from "./storage/toolboxStorage.js";
@ -207,7 +208,8 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
const nextId = moduleIdMap.get(oldId);
const module = normalizedImport.modules.find((item) => item.id === nextId);
if (nextId) {
const compact = compactModuleDataForStorage(module?.type, data);
const remappedData = module?.type === "memos" ? remapMemosPlannerLink(data, (plannerId) => moduleIdMap.get(plannerId)) : data;
const compact = compactModuleDataForStorage(module?.type, remappedData);
if (compact) nextModuleData[`${normalizedImport.id}:${nextId}`] = compact;
}
});
@ -270,7 +272,10 @@ export function useToolboxActions({ store, notify, creationTemplate, setConfirmM
const nextModuleId = moduleIdMap.get(key);
const toolbox = importedToolboxes.find((item) => item.id === nextToolboxId);
const module = toolbox?.modules.find((item) => item.id === nextModuleId);
const compact = compactModuleDataForStorage(module?.type, data);
const remappedData = module?.type === "memos"
? remapMemosPlannerLink(data, (plannerId) => moduleIdMap.get(globalModuleKey(oldToolboxId, plannerId)))
: data;
const compact = compactModuleDataForStorage(module?.type, remappedData);
if (nextToolboxId && nextModuleId && compact) nextModuleData[`${nextToolboxId}:${nextModuleId}`] = compact;
});
const saved = await store.persistToolboxSnapshot((current) => ({

View file

@ -74,11 +74,41 @@
.topbar,
.page-heading,
.toolbox-head,
.module header {
.module .module-header {
align-items: stretch;
flex-direction: column;
}
.module .module-header-actions {
justify-content: flex-end;
}
.module-configuration-panel {
grid-template-columns: 1fr;
}
.module-configuration-organization,
.module-configuration-data,
.module-configuration-panel:not(:has(.module-configuration-organization)) .module-configuration-data,
.module-configuration-specific {
grid-column: 1;
grid-row: auto;
}
.module-configuration-organization {
order: 0;
padding-right: 6px;
border-right: 0;
}
.module-configuration-data {
order: 1;
}
.module-configuration-specific {
order: 2;
}
.topbar {
padding: var(--space-4);
}

View file

@ -9,7 +9,7 @@
.nds-switch { max-width: 100%; }
.guide-checklist-actions { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: var(--space-2); padding-top: var(--space-2); }
}
.module-guide-button[data-linked] .ui-icon { color: var(--color-accent-gold); }
.module-configuration-button[data-guide-linked] .ui-icon { color: var(--color-accent-gold); }
.game-heading-actions {
display: flex;
align-self: stretch;

View file

@ -22,6 +22,28 @@
flex: 0 0 auto;
}
.memos-settings {
display: grid;
}
.memos-settings label {
display: grid;
grid-template-columns: minmax(120px, auto) minmax(180px, 1fr);
gap: var(--space-3);
align-items: center;
}
.memos-settings label > span {
color: var(--color-text-secondary);
font-size: var(--font-size-xs);
font-weight: 900;
text-transform: uppercase;
}
.memos-settings select {
width: 100%;
}
.memo-card.is-drop-target {
border-color: rgba(246, 196, 83, 0.48);
background: rgba(246, 196, 83, 0.1);
@ -74,6 +96,10 @@
bottom: 84px;
left: 12px;
}
.memos-settings label {
grid-template-columns: 1fr;
}
}
.memos-ticker-track {
@ -104,6 +130,19 @@
box-shadow: 0 0 10px color-mix(in srgb, var(--memo-color) 42%, transparent);
}
.memos-ticker-item.is-task-warning {
color: var(--color-warning, #f6c453);
}
.memos-ticker-item.is-task-warning::before {
content: none;
}
.memos-ticker-item.is-task-warning .ui-icon {
width: 16px;
height: 16px;
}
.memos-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));

View file

@ -2,7 +2,7 @@
@use "../design-system/controls";
@use "../mixins";
.module header {
.module .module-header {
display: flex;
align-items: center;
justify-content: space-between;
@ -11,26 +11,43 @@
border-bottom: 1px solid var(--color-border);
}
.module header > div {
.module .module-header-main,
.module .module-header-actions {
display: flex;
align-items: center;
gap: 10px;
}
.module header h2 {
.module .module-header-main {
min-width: 0;
flex: 1 1 auto;
}
.module .module-header-actions {
flex: 0 0 auto;
}
.module .module-header h2 {
margin: 0;
font-size: 17px;
}
.module-space-select {
width: auto;
max-width: 150px;
width: 100%;
}
.module-content {
min-width: 0;
}
.module-component-host {
display: contents;
}
.module-component-host[hidden] {
display: none;
}
.module.is-collapsed header {
border-bottom-color: transparent;
}
@ -78,11 +95,14 @@
}
.module-title {
min-width: 80px;
min-width: 0;
max-width: 100%;
overflow: hidden;
padding: 4px 6px;
border-radius: var(--radius-sm);
cursor: text;
text-overflow: ellipsis;
white-space: nowrap;
}
.module-title:focus {
@ -92,14 +112,14 @@
}
.module-edit-button,
.module-exchange-button,
.module-configuration-button,
.module-delete-button,
.module-collapse-button {
flex: 0 0 40px;
}
.module-edit-button,
.module-exchange-button,
.module-configuration-button,
.module-collapse-button {
color: var(--color-text-secondary);
}
@ -122,10 +142,88 @@
}
.module-edit-button.active,
.module-exchange-button.active {
.module-configuration-button.active {
@include mixins.temporary-toggle-active;
}
.module-configuration-panel {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: var(--space-4);
row-gap: var(--space-2);
margin: var(--space-1) var(--space-2) var(--space-2);
padding: 10px 12px 12px;
border: 1px solid rgba(165, 180, 252, 0.1);
border-radius: var(--radius-md);
background:
linear-gradient(135deg, rgba(139, 92, 246, 0.035), transparent 56%),
rgba(7, 10, 24, 0.2);
}
.module-configuration-target {
display: contents;
}
.module-configuration-section {
display: grid;
min-width: 0;
align-content: start;
gap: var(--space-2);
padding: 4px 6px;
}
.module-configuration-organization {
grid-column: 1;
grid-row: 1;
padding-right: var(--space-4);
border-right: 1px solid rgba(165, 180, 252, 0.1);
}
.module-configuration-data {
grid-column: 2;
grid-row: 1;
}
.module-configuration-panel:not(:has(.module-configuration-organization)) .module-configuration-data {
grid-column: 1 / -1;
}
.module-configuration-specific {
grid-column: 1 / -1;
padding-top: var(--space-3);
border-top: 1px solid rgba(165, 180, 252, 0.1);
}
.module-configuration-section > h3 {
margin: 0;
color: var(--color-text-secondary);
font-size: 0.6875rem;
font-weight: 900;
letter-spacing: 0.07em;
text-transform: uppercase;
}
.module-configuration-section .module-text-exchange-panel,
.module-configuration-section .guide-checklist-controls {
margin: 0;
padding: 0;
border: 0;
}
.module-configuration-panel .text-import-actions {
gap: 6px;
}
.module-configuration-panel .text-import-actions button {
min-height: 34px;
gap: 6px;
}
.module-configuration-panel .text-import-actions .ui-icon {
width: 15px;
height: 15px;
}
.module-icon {
display: grid;
width: 34px;

View file

@ -29,29 +29,14 @@
gap: 10px;
}
.task-planner-settings-toggle {
grid-template-columns: 16px minmax(0, auto);
width: auto;
max-width: 100%;
padding: 0 12px;
gap: 8px;
}
.task-planner-settings-toggle span {
overflow: hidden;
font-size: var(--font-size-xs);
font-weight: 900;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-planner-clear-completed-button {
display: inline-flex;
min-height: 38px;
min-height: 30px;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 12px;
gap: 6px;
padding: 0 10px;
font-size: var(--font-size-xs);
white-space: nowrap;
}
@ -73,6 +58,12 @@
rgba(7, 10, 24, 0.34);
}
.module-configuration-specific .task-planner-settings {
padding: 0;
border: 0;
background: none;
}
.task-planner-settings-actions {
display: flex;
grid-column: 1 / -1;
@ -108,12 +99,18 @@
width: 100%;
}
.task-planner-settings input,
.task-planner-settings input {
@include controls.field-control(xs);
}
.task-planner-reset-override input {
@include controls.field-control(sm);
}
.task-planner-settings input[type="time"],
.task-planner-settings input[type="time"] {
@include controls.time-control(xs);
}
.task-planner-reset-override input[type="time"] {
@include controls.time-control(sm);
}