Add checklist switching and contextual memo ticker
All checks were successful
Deploy Sokko G / deploy (push) Successful in 19s

This commit is contained in:
Shinuwa 2026-09-28 09:40:29 +02:00
parent 273bc5d528
commit bd4059c9c1
16 changed files with 231 additions and 28 deletions

View file

@ -31,16 +31,27 @@ export function GuideChecklistControls({ checklist, list, id, open }) {
}
export function GuideChecklistPageControls({ checklist }) {
if (!checklist.selected) return null;
if (!checklist.lists.length) return null;
const { content, hideCompleted } = checklist;
return (
<aside className="guide-checklist-page-controls nds-tool-panel" aria-label={content.label}>
<button className={`nds-switch ${hideCompleted ? "active" : ""}`} data-size="sm" type="button" aria-pressed={hideCompleted} onClick={checklist.toggleHidden}>
<Icon name="hide" />
<span>{content.hideCompleted}</span>
<i aria-hidden="true" />
</button>
<button className="nds-button" data-size="xs" data-variant="secondary" type="button" onClick={() => checklist.associate("")}>{content.unlink}</button>
<label className="guide-checklist-page-select">
<span>{content.selectLabel}</span>
<select className="nds-select" data-size="sm" value={checklist.selected?.id || ""} onChange={(event) => checklist.associate(event.target.value)}>
{!checklist.selected && <option value="" disabled>{content.selectPlaceholder}</option>}
{checklist.lists.map((list) => <option key={list.id} value={list.id}>{list.title}</option>)}
</select>
</label>
{checklist.selected && (
<div className="guide-checklist-page-actions">
<button className={`nds-switch ${hideCompleted ? "active" : ""}`} data-size="sm" type="button" aria-pressed={hideCompleted} onClick={checklist.toggleHidden}>
<Icon name="hide" />
<span>{content.hideCompleted}</span>
<i aria-hidden="true" />
</button>
<button className="nds-button" data-size="xs" data-variant="secondary" type="button" onClick={() => checklist.associate("")}>{content.unlink}</button>
</div>
)}
</aside>
);
}

View file

@ -21,6 +21,7 @@ export function useGuideChecklist(requestedGuideId) {
const association = guideChecklistStorage.associations[guideId];
const lists = useMemo(() => (toolbox?.modules || []).filter((module) => module.type === "checklist").map((module) => ({
id: module.id,
title: module.title || "Checklist",
...matchGuideChecklist(entries || [], moduleData[`${toolbox.id}:${module.id}`])
})), [entries, toolbox, moduleData]);
const selected = lists.find((list) => list.id === association?.moduleId);

View file

@ -0,0 +1,33 @@
// Rôle : affiche en bas de page les mémos actifs de la toolbox courante ou liée au jeu.
import { useMemo } from "react";
import { moduleStorageKey, normalizeMemosData } from "./storage/toolboxStorage.js";
import { MemosTicker } from "./modules/MemosTicker.jsx";
function getRouteToolboxId(route, links) {
const toolboxMatch = route.match(/^\/toolbox\/([^/]+)/);
if (toolboxMatch) return decodeURIComponent(toolboxMatch[1]);
const gameMatch = route.match(/^\/games\/([^/]+)/);
return gameMatch ? links[gameMatch[1]] || "" : "";
}
export function GlobalMemosTicker({ route, toolboxes, links, moduleData, textContent }) {
const items = useMemo(() => {
const toolboxId = getRouteToolboxId(route, links);
const toolbox = toolboxes.find((item) => item.id === toolboxId);
if (!toolbox) return [];
return toolbox.modules
.filter((module) => module.type === "memos")
.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}` }));
});
}, [route, toolboxes, links, moduleData]);
if (!items.length) return null;
return (
<aside className="global-memos-ticker" aria-label={textContent?.tickerLabel || "Mémos"}>
<MemosTicker items={items} label={textContent?.tickerLabel || "Mémos"} />
</aside>
);
}

View file

@ -4,6 +4,7 @@ import { Icon } from "../../../components/Icon.jsx";
import { moveItem, useGroupedReorder } from "../../../hooks/useGroupedReorder.js";
import { useMemo } from "react";
import { MemoTextEditor } from "./MemoTextEditor.jsx";
import { MemosTicker } from "./MemosTicker.jsx";
import { TextExchangeActions } from "./TextExchangeActions.jsx";
import { TEXT_COLORS } from "./notepadUtils.js";
@ -92,21 +93,12 @@ export function MemosModule({ toolboxId, moduleId, context, editing, textExchang
type="button"
onClick={() => save({ ...data, tickerEnabled: !data.tickerEnabled })}
aria-pressed={data.tickerEnabled}
aria-label={data.tickerEnabled ? textContent.hideTickerTitle || "Masquer la bande" : textContent.showTickerTitle || "Afficher la bande"}
title={data.tickerEnabled ? textContent.hideTickerTitle || "Masquer la bande" : textContent.showTickerTitle || "Afficher la bande"}
>
<Icon name={data.tickerEnabled ? "eye-open" : "eye-closed"} />
</button>
{tickerItems.length > 0 && (
<div className="memos-ticker" aria-label={textContent.tickerLabel || "Mémos"}>
<div className="memos-ticker-track">
{[...tickerItems, ...tickerItems].map((memo, index) => (
<span className="memos-ticker-item" key={`${memo.id}-${index}`} style={{ "--memo-color": memo.color }}>
{memo.text}
</span>
))}
</div>
</div>
)}
<MemosTicker items={tickerItems} label={textContent.tickerLabel || "Mémos"} />
</div>
<div className="memos-list">

View file

@ -0,0 +1,15 @@
// Rôle : affiche la bande déroulante partagée par l'outil Mémos et les pages associées.
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 }}>
{memo.text}
</span>
))}
</div>
</div>
);
}

View file

@ -18,6 +18,7 @@ import { RouteContent } from "./router/RouteContent.jsx";
import { useHashRoute } from "./router/hashRouter.js";
import { normalizeCalendarData, normalizeTimerData } from "./features/toolboxes/storage/toolboxStorage.js";
import { useAutoBackup } from "./features/toolboxes/storage/useAutoBackup.js";
import { GlobalMemosTicker } from "./features/toolboxes/GlobalMemosTicker.jsx";
const ToolboxDrawer = lazy(() => import("./features/toolboxes/ToolboxPages.jsx").then((module) => ({ default: module.ToolboxDrawer })));
const DEFAULT_NOTIFICATION_DURATION_MS = 120000;
@ -211,6 +212,13 @@ function App() {
/>
</Suspense>
)}
<GlobalMemosTicker
route={route}
toolboxes={store.toolboxes}
links={store.links}
moduleData={store.moduleData}
textContent={siteContent.toolboxes.modules.memos}
/>
<AppOverlays
storageErrorTitle={siteContent.toolboxes.storageStatus.writeErrorTitle}
actions={actions}

View file

@ -101,6 +101,11 @@
justify-content: space-between;
}
.guide-checklist-page-actions {
flex-wrap: wrap;
justify-content: space-between;
}
.home-onboarding-steps-action {
padding-left: 0;
}

View file

@ -18,13 +18,31 @@
justify-content: space-between;
}
.guide-checklist-page-controls {
display: flex;
display: grid;
align-self: flex-end;
gap: var(--space-2);
width: min(100%, 360px);
padding: 8px;
}
.guide-checklist-page-select {
display: flex;
align-items: center;
gap: var(--space-2);
color: var(--color-text-muted);
font-size: var(--font-size-xs);
font-weight: 700;
}
.guide-checklist-page-select > span { white-space: nowrap; }
.guide-checklist-page-select .nds-select {
width: auto;
min-width: 0;
flex: 1;
}
.guide-checklist-page-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: var(--space-2);
width: fit-content;
padding: 8px;
}
.guide-checklist-page-controls .nds-switch { max-width: 100%; }
.game-heading-navigation {

View file

@ -37,6 +37,45 @@
background: rgba(5, 7, 17, 0.56);
}
.global-memos-ticker {
position: fixed;
right: 12px;
bottom: 12px;
left: 260px;
z-index: 45;
padding: 6px;
border: 1px solid rgba(246, 196, 83, 0.28);
border-radius: var(--radius-lg);
background: rgba(7, 9, 19, 0.9);
box-shadow: var(--shadow-md);
backdrop-filter: blur(10px);
}
.global-memos-ticker .memos-ticker {
width: 100%;
background: rgba(5, 7, 17, 0.72);
}
.app-shell.sidebar-compact .global-memos-ticker {
left: 84px;
}
.app-shell:has(.global-memos-ticker) main {
padding-bottom: calc(var(--space-12) + 60px);
}
@media (max-width: 1024px) {
.global-memos-ticker { left: 84px; }
}
@media (max-width: 760px) {
.global-memos-ticker {
right: 12px;
bottom: 84px;
left: 12px;
}
}
.memos-ticker-track {
display: flex;
width: max-content;