Add checklist switching and contextual memo ticker
All checks were successful
Deploy Sokko G / deploy (push) Successful in 19s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 19s
This commit is contained in:
parent
273bc5d528
commit
bd4059c9c1
16 changed files with 231 additions and 28 deletions
|
|
@ -2771,6 +2771,8 @@
|
|||
},
|
||||
"guideChecklist": {
|
||||
"label": "Lien avec le guide",
|
||||
"selectLabel": "Checklist associée",
|
||||
"selectPlaceholder": "Choisir une checklist",
|
||||
"associate": "Associer à la page",
|
||||
"noMatches": "Cette checklist ne contient plus de correspondance avec le guide.",
|
||||
"matches": "correspondances",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
33
website/src/features/toolboxes/GlobalMemosTicker.jsx
Normal file
33
website/src/features/toolboxes/GlobalMemosTicker.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
15
website/src/features/toolboxes/modules/MemosTicker.jsx
Normal file
15
website/src/features/toolboxes/modules/MemosTicker.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue