Add planner-aware memos and unified tool configuration panels
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 18s
This commit is contained in:
parent
acd9b2fe11
commit
808610dcf2
53 changed files with 842 additions and 329 deletions
|
|
@ -1,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>;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" : ""}`}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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"}>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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")}>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 }))}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" && (
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
62
website/src/features/toolboxes/modules/memosPlannerUtils.js
Normal file
62
website/src/features/toolboxes/modules/memosPlannerUtils.js
Normal 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)
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue