Add toolbox library page with editable examples
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
This commit is contained in:
parent
8eb622e05d
commit
9b72f4934c
20 changed files with 4774 additions and 31 deletions
4123
website/public/data/library.json
Normal file
4123
website/public/data/library.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -7,7 +7,9 @@
|
|||
"home": "Accueil",
|
||||
"toolboxes": "Toolboxes",
|
||||
"games": "Jeux",
|
||||
"library": "Librairie",
|
||||
"about": "C'est quoi Sokko G ?",
|
||||
"mobileLibrary": "Lib.",
|
||||
"mobileGames": "Infos"
|
||||
},
|
||||
"sidebar": {
|
||||
|
|
@ -17,6 +19,7 @@
|
|||
"topbar": {
|
||||
"dashboard": "Dashboard",
|
||||
"toolbox": "Toolboxes",
|
||||
"library": "Librairie",
|
||||
"games": "Informations jeu"
|
||||
},
|
||||
"gamesPage": {
|
||||
|
|
@ -26,6 +29,31 @@
|
|||
"emptyTitle": "Aucun jeu disponible",
|
||||
"emptyText": "Ajoutez des entrées dans /data/games.json."
|
||||
},
|
||||
"library": {
|
||||
"eyebrow": "Catalogue des outils",
|
||||
"title": "Librairie",
|
||||
"description": "Parcourez tous les outils disponibles avec des exemples basiques couvrant les listes simples, les catégories, les contenus sans catégorie, les médias, les tableaux et les suivis de session.",
|
||||
"summaryLabel": "Repères de la librairie",
|
||||
"tocLabel": "Sommaire des outils",
|
||||
"toolsLabel": "Exemples d'outils disponibles",
|
||||
"summaryItems": [
|
||||
{
|
||||
"icon": "toolbox",
|
||||
"title": "Tous les outils",
|
||||
"text": "Chaque exemple utilise le vrai composant de toolbox pour refléter le comportement réel."
|
||||
},
|
||||
{
|
||||
"icon": "checklist",
|
||||
"title": "Formats variés",
|
||||
"text": "Les exemples couvrent les catégories, les entrées libres, les relations parent/enfant et les vues en grille."
|
||||
},
|
||||
{
|
||||
"icon": "notepad",
|
||||
"title": "Démo locale",
|
||||
"text": "Les modifications restent limitées à cette page et ne changent pas vos toolboxes enregistrées."
|
||||
}
|
||||
]
|
||||
},
|
||||
"home": {
|
||||
"hero": {
|
||||
"eyebrow": "Session gaming efficace",
|
||||
|
|
@ -106,6 +134,7 @@
|
|||
}
|
||||
],
|
||||
"toolsTitle": "Outils disponibles",
|
||||
"toolsLibraryLink": "Voir la librairie d'outils",
|
||||
"contribute": {
|
||||
"eyebrow": "Contribuer",
|
||||
"title": "Proposer une liste de jeu",
|
||||
|
|
@ -151,10 +180,20 @@
|
|||
"name": "Calculateur",
|
||||
"description": "Un calculateur avec résultats enregistrables et arborescence pour préparer des chaînes de craft ou de ressources."
|
||||
},
|
||||
{
|
||||
"icon": "table",
|
||||
"name": "Tableau",
|
||||
"description": "Une grille compacte avec formules et copie TSV pour comparer ressources, stocks, besoins ou valeurs."
|
||||
},
|
||||
{
|
||||
"icon": "clock",
|
||||
"name": "Timer",
|
||||
"description": "Un espace pour préparer chronomètres à étapes et comptes à rebours multiples pendant une session."
|
||||
},
|
||||
{
|
||||
"icon": "tasklist",
|
||||
"name": "Planificateur de tâches",
|
||||
"description": "Un suivi de tâches ponctuelles, quotidiennes ou hebdomadaires avec catégories et dépendances."
|
||||
}
|
||||
],
|
||||
"limitsTitle": "À retenir",
|
||||
|
|
@ -185,6 +224,7 @@
|
|||
"eyebrow": "Données locales",
|
||||
"title": "Toolboxes",
|
||||
"newButton": "Nouvelle toolbox",
|
||||
"libraryLink": "Voir la librairie d'outils",
|
||||
"importAll": "Importer tout",
|
||||
"exportAll": "Exporter tout",
|
||||
"importOne": "Importer une toolbox",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export function Shell({ route, content, games, toolboxes, links, actions, childr
|
|||
const gameId = route.split("/")[1] === "games" ? route.split("/")[2] || "" : "";
|
||||
const game = games.find((item) => item.id === gameId);
|
||||
const linkedToolbox = game ? toolboxes.find((item) => item.id === links[game.id]) : null;
|
||||
const topbarLabel = route.startsWith("/toolbox") ? content.topbar.toolbox : route.startsWith("/games") ? content.topbar.games : content.topbar.dashboard;
|
||||
const topbarLabel = route.startsWith("/toolbox") ? content.topbar.toolbox : route.startsWith("/games") ? content.topbar.games : route === "/library" ? content.topbar.library : content.topbar.dashboard;
|
||||
const toolboxLabel = linkedToolbox ? `Ouvrir la toolbox ${linkedToolbox.name}` : "Associer une toolbox";
|
||||
|
||||
return (
|
||||
|
|
@ -49,6 +49,7 @@ export function Shell({ route, content, games, toolboxes, links, actions, childr
|
|||
<a className={route === "/" ? "active" : ""} href="#/">{content.navigation.home}</a>
|
||||
<a className={route.startsWith("/toolboxes") || route.startsWith("/toolbox") ? "active" : ""} href="#/toolboxes">{content.navigation.toolboxes}</a>
|
||||
<button className="primary" onClick={() => actions.setCreateModal({})}>+</button>
|
||||
<a className={route === "/library" ? "active" : ""} href="#/library">{content.navigation.mobileLibrary}</a>
|
||||
<a className={route.startsWith("/games") ? "active" : ""} href="#/games">{content.navigation.mobileGames}</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export function ToolboxesPage({ siteContent, toolboxes, getToolboxGame, actions,
|
|||
<p className="eyebrow">{content.eyebrow}</p>
|
||||
<h1>{content.title}</h1>
|
||||
<p>{content.summary}</p>
|
||||
<a className="button hero-library-button" href="#/library">{content.libraryLink}</a>
|
||||
</div>
|
||||
</section>
|
||||
<section className="toolbar toolbox-actions-row">
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ function getSectionHideWhenComplete(section, globalHide) {
|
|||
}
|
||||
|
||||
function keepChecklistSection(section) {
|
||||
return section.items.length || section.title;
|
||||
return section.items.length;
|
||||
}
|
||||
|
||||
function getChecklistReorderItems(sections) {
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ function getComboCategory(combo) {
|
|||
return String(combo?.category || "").trim();
|
||||
}
|
||||
|
||||
function setComboCategory(combo, category) {
|
||||
function setComboItemCategory(combo, category) {
|
||||
const nextCombo = { ...combo };
|
||||
if (category) nextCombo.category = category;
|
||||
else delete nextCombo.category;
|
||||
|
|
@ -292,7 +292,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
getDropPlacement,
|
||||
shouldShowGroupBoundaries
|
||||
} = useGroupedReorder({
|
||||
namespace: "combos",
|
||||
namespace: `combos-${moduleId}`,
|
||||
items: data.combos,
|
||||
getItemId: (combo) => combo.id,
|
||||
getItemGroup: getComboCategory,
|
||||
|
|
@ -334,7 +334,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
groupOrderKey: "categoryOrder",
|
||||
collapsedGroupsKey: "collapsedCategories",
|
||||
getItemGroup: getComboCategory,
|
||||
setItemGroup: setComboCategory
|
||||
setItemGroup: setComboItemCategory
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -595,7 +595,7 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
</button>
|
||||
<h3>{category}</h3>
|
||||
</div>
|
||||
<div>
|
||||
<div className="checklist-section-actions">
|
||||
<span>{group.combos.length}</span>
|
||||
<button
|
||||
className="checklist-section-collapse-button"
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export function CountersModule({ toolboxId, moduleId, context, editing }) {
|
|||
>
|
||||
<Icon name="drag" />
|
||||
</button>
|
||||
<div>
|
||||
<div className="counter-value">
|
||||
<strong>{counter.value}</strong>
|
||||
<span>{counter.label}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ export function normalizeChecklistData(data) {
|
|||
const hideCompletedSections = Boolean(data?.hideCompletedSections);
|
||||
const hideCompletedSectionsFully = Boolean(data?.hideCompletedSectionsFully);
|
||||
const sections = Array.isArray(data?.sections)
|
||||
? data.sections.map((section) => normalizeChecklistSection(section)).filter((section) => section.items.length || section.title)
|
||||
? data.sections.map((section) => normalizeChecklistSection(section)).filter((section) => section.items.length)
|
||||
: [];
|
||||
const simpleItems = (data?.items || []).map(normalizeChecklistItem).filter((item) => item.label);
|
||||
if (!sections.length && simpleItems.length) return { hideCompletedSections, hideCompletedSectionsFully, sections: [{ id: uid("section"), title: "", items: simpleItems }], items: simpleItems };
|
||||
|
|
@ -505,8 +505,8 @@ export function normalizeTableData(data) {
|
|||
for (let rowIndex = 0; rowIndex < rows; rowIndex += 1) {
|
||||
for (let columnIndex = 0; columnIndex < columns; columnIndex += 1) {
|
||||
const address = tableCellAddress(rowIndex, columnIndex);
|
||||
const value = String(sourceCells[address] || "").trim();
|
||||
if (value) cells[address] = value.slice(0, 500);
|
||||
const value = String(sourceCells[address] || "");
|
||||
if (value.trim()) cells[address] = value.slice(0, 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -704,7 +704,7 @@ export function compactModuleDataForStorage(type, value) {
|
|||
}
|
||||
if (type === "checklist") {
|
||||
const normalized = normalizeChecklistData(value);
|
||||
const sections = normalized.sections.map(compactChecklistSectionForStorage).filter((section) => section.items.length || section.title);
|
||||
const sections = normalized.sections.map(compactChecklistSectionForStorage).filter((section) => section.items.length);
|
||||
if (!sections.length) return null;
|
||||
const settings = {
|
||||
...(normalized.hideCompletedSections ? { hideCompletedSections: true } : {}),
|
||||
|
|
|
|||
|
|
@ -28,19 +28,18 @@ export function usePointerReorder({
|
|||
|
||||
function getDropTarget(event) {
|
||||
const { getTargetId, canDropOn, getPlacement } = optionsRef.current;
|
||||
const element = document.elementFromPoint(event.clientX, event.clientY);
|
||||
const target = element?.closest?.(targetSelector);
|
||||
if (!target || !canDropOn(target, draggingId)) {
|
||||
return { id: "", placement: "before" };
|
||||
const elements = document.elementsFromPoint?.(event.clientX, event.clientY) || [document.elementFromPoint(event.clientX, event.clientY)].filter(Boolean);
|
||||
for (const element of elements) {
|
||||
const target = element?.closest?.(targetSelector);
|
||||
if (!target || !canDropOn(target, draggingId)) continue;
|
||||
const targetId = getTargetId(target);
|
||||
if (targetId === draggingId) continue;
|
||||
return {
|
||||
id: targetId,
|
||||
placement: getPlacement(event, target)
|
||||
};
|
||||
}
|
||||
const targetId = getTargetId(target);
|
||||
if (targetId === draggingId) {
|
||||
return { id: "", placement: "before" };
|
||||
}
|
||||
return {
|
||||
id: targetId,
|
||||
placement: getPlacement(event, target)
|
||||
};
|
||||
return { id: "", placement: "before" };
|
||||
}
|
||||
|
||||
function handlePointerMove(event) {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ export function AboutPage({ siteContent }) {
|
|||
<p className="eyebrow">Toolbox</p>
|
||||
<h2>{content.toolsTitle}</h2>
|
||||
</div>
|
||||
<a className="button" href="#/library">{content.toolsLibraryLink}</a>
|
||||
</div>
|
||||
<div className="about-tools-list">
|
||||
{content.tools.map((tool) => (
|
||||
|
|
|
|||
257
website/src/pages/LibraryPage.jsx
Normal file
257
website/src/pages/LibraryPage.jsx
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
// Rôle : affiche la librairie des outils avec des exemples locaux non persistés.
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Icon } from "../components/Icon.jsx";
|
||||
import { CalculatorModule } from "../features/toolboxes/modules/CalculatorModule.jsx";
|
||||
import { ChecklistModule } from "../features/toolboxes/modules/ChecklistModule.jsx";
|
||||
import { CombosModule } from "../features/toolboxes/modules/CombosModule.jsx";
|
||||
import { CountersModule } from "../features/toolboxes/modules/CountersModule.jsx";
|
||||
import { ImageAnnotationModule } from "../features/toolboxes/modules/ImageAnnotationModule.jsx";
|
||||
import { ImagesModule } from "../features/toolboxes/modules/ImagesModule.jsx";
|
||||
import { LinksModule } from "../features/toolboxes/modules/LinksModule.jsx";
|
||||
import { NotepadModule } from "../features/toolboxes/modules/NotepadModule.jsx";
|
||||
import { TableModule } from "../features/toolboxes/modules/TableModule.jsx";
|
||||
import { TaskPlannerModule } from "../features/toolboxes/modules/TaskPlannerModule.jsx";
|
||||
import { TimerModule } from "../features/toolboxes/modules/TimerModule.jsx";
|
||||
import {
|
||||
clampQty,
|
||||
hostnameFromUrl,
|
||||
normalizeCalculatorData,
|
||||
normalizeChecklistData,
|
||||
normalizeCombosData,
|
||||
normalizeCountersData,
|
||||
normalizeImageAnnotationData,
|
||||
normalizeLinksData,
|
||||
normalizeNotepadData,
|
||||
normalizeTableData,
|
||||
normalizeTaskPlannerData,
|
||||
normalizeTimerData,
|
||||
normalizeUrl,
|
||||
uid
|
||||
} from "../features/toolboxes/storage/toolboxStorage.js";
|
||||
|
||||
const LIBRARY_TOOLBOX_ID = "library";
|
||||
|
||||
const MODULE_COMPONENTS = {
|
||||
notepad: { label: "Bloc notes", mode: "Texte riche", icon: "notepad", Component: NotepadModule },
|
||||
checklist: { label: "Checklist", mode: "Catégories + sans catégorie", icon: "checklist", Component: ChecklistModule, scrollable: true },
|
||||
images: { label: "Images", mode: "Média", icon: "picture", Component: ImagesModule, scrollable: true },
|
||||
links: { label: "Liens", mode: "Liste simple", icon: "link", Component: LinksModule, scrollable: true },
|
||||
counters: { label: "Compteurs", mode: "Valeurs", icon: "abacus", Component: CountersModule, scrollable: true },
|
||||
combos: { label: "Combos", mode: "Catégories", icon: "controller", Component: CombosModule, scrollable: true },
|
||||
calculator: { label: "Calculateur", mode: "Arborescence", icon: "calculator", Component: CalculatorModule },
|
||||
table: { label: "Tableau", mode: "Grille + formules", icon: "table", Component: TableModule, scrollable: true },
|
||||
timer: { label: "Timer", mode: "Chrono + comptes à rebours", icon: "clock", Component: TimerModule },
|
||||
taskPlanner: { label: "Planificateur de tâches", mode: "Catégories + parent/enfant", icon: "tasklist", Component: TaskPlannerModule, scrollable: true },
|
||||
imageAnnotation: { label: "Annotation d'images", mode: "Image + marqueurs", icon: "map", Component: ImageAnnotationModule }
|
||||
};
|
||||
|
||||
function cloneData(value) {
|
||||
if (value === undefined) return {};
|
||||
if (typeof structuredClone === "function") return structuredClone(value);
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function createModuleDataFromPayload(payload) {
|
||||
return Object.fromEntries((payload?.toolbox?.modules || []).map((module) => [
|
||||
`${LIBRARY_TOOLBOX_ID}:${module.id}`,
|
||||
cloneData(payload?.modules?.[module.id] || {})
|
||||
]));
|
||||
}
|
||||
|
||||
function getLibraryAnchorId(module) {
|
||||
return `library-tool-${String(module?.id || module?.type || "outil").replace(/[^a-z0-9_-]+/gi, "-").toLowerCase()}`;
|
||||
}
|
||||
|
||||
function getCurrentLibraryAnchor() {
|
||||
const match = location.hash.match(/^#\/library#(.+)$/);
|
||||
return match ? decodeURIComponent(match[1]) : "";
|
||||
}
|
||||
|
||||
function scrollToLibraryTool(anchorId, replace = false) {
|
||||
const element = document.getElementById(anchorId);
|
||||
if (!element) return;
|
||||
if (replace) history.replaceState(null, "", `#/library#${encodeURIComponent(anchorId)}`);
|
||||
const top = element.getBoundingClientRect().top + window.scrollY - 24;
|
||||
window.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
|
||||
}
|
||||
|
||||
function getToolDescription(siteContent, definition) {
|
||||
const tools = siteContent.about?.tools || [];
|
||||
return tools.find((tool) => tool.name === definition.label)?.description || "";
|
||||
}
|
||||
|
||||
export function LibraryPage({ siteContent, actions }) {
|
||||
const content = siteContent.library;
|
||||
const [libraryPayload, setLibraryPayload] = useState(null);
|
||||
const [libraryError, setLibraryError] = useState("");
|
||||
const [moduleData, setModuleData] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetch("/data/library.json")
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error("Impossible de charger les exemples de librairie.");
|
||||
return response.json();
|
||||
})
|
||||
.then((payload) => {
|
||||
if (cancelled) return;
|
||||
setLibraryPayload(payload);
|
||||
setModuleData(createModuleDataFromPayload(payload));
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!cancelled) setLibraryError(error.message);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!libraryPayload) return;
|
||||
const anchorId = getCurrentLibraryAnchor();
|
||||
if (!anchorId) return;
|
||||
requestAnimationFrame(() => scrollToLibraryTool(anchorId));
|
||||
}, [libraryPayload]);
|
||||
|
||||
const moduleContext = useMemo(() => ({
|
||||
getModuleData: (toolboxId, moduleId, fallback) => moduleData[`${toolboxId}:${moduleId}`] || fallback,
|
||||
setModuleData: (toolboxId, moduleId, data) => setModuleData((current) => ({ ...current, [`${toolboxId}:${moduleId}`]: data })),
|
||||
moduleText: siteContent.toolboxes.modules,
|
||||
normalizeChecklistData,
|
||||
normalizeCombosData,
|
||||
normalizeLinksData,
|
||||
normalizeCountersData,
|
||||
normalizeCalculatorData,
|
||||
normalizeImageAnnotationData,
|
||||
normalizeNotepadData,
|
||||
normalizeTableData,
|
||||
normalizeTimerData,
|
||||
normalizeTaskPlannerData,
|
||||
normalizeUrl,
|
||||
hostnameFromUrl,
|
||||
clampQty,
|
||||
uid,
|
||||
copyText: async (value) => {
|
||||
await navigator.clipboard.writeText(value);
|
||||
return true;
|
||||
},
|
||||
notify: actions.notify,
|
||||
compressImageFile: async () => "",
|
||||
addImageFiles: async () => false,
|
||||
setImage: actions.setImage,
|
||||
createImageAnnotationModule: () => {}
|
||||
}), [actions, moduleData, siteContent.toolboxes.modules]);
|
||||
|
||||
return (
|
||||
<div className="library-page">
|
||||
<section className="page-hero library-hero">
|
||||
<div>
|
||||
<p className="eyebrow">{content.eyebrow}</p>
|
||||
<h1>{content.title}</h1>
|
||||
<p>{content.description}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section className="library-summary nebula-panel" aria-label={content.summaryLabel}>
|
||||
{content.summaryItems.map((item) => (
|
||||
<article key={item.title}>
|
||||
<span className="library-summary-icon" aria-hidden="true">
|
||||
<span className={`module-icon-svg module-icon-${item.icon || "toolbox"}`} />
|
||||
</span>
|
||||
<div>
|
||||
<h2>{item.title}</h2>
|
||||
<p>{item.text}</p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
{libraryError && <section className="empty"><h2>Librairie indisponible</h2><p>{libraryError}</p></section>}
|
||||
{!libraryError && !libraryPayload && <section className="empty"><p>Chargement des exemples...</p></section>}
|
||||
{libraryPayload && (
|
||||
<>
|
||||
<nav className="library-toc nebula-panel" aria-label={content.tocLabel || "Sommaire des outils"}>
|
||||
{libraryPayload.toolbox.modules.map((module) => {
|
||||
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
||||
const title = module.title || definition.label || "Outil";
|
||||
const anchorId = getLibraryAnchorId(module);
|
||||
return (
|
||||
<a
|
||||
key={module.id}
|
||||
href={`#/library#${anchorId}`}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
scrollToLibraryTool(anchorId, true);
|
||||
}}
|
||||
>
|
||||
<span className="module-icon" aria-hidden="true">
|
||||
<span className={`module-icon-svg module-icon-${definition.icon}`} />
|
||||
</span>
|
||||
<span>{title}</span>
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<section className="library-tools" aria-label={content.toolsLabel}>
|
||||
{libraryPayload.toolbox.modules.map((module) => (
|
||||
<LibraryToolExample
|
||||
key={module.id}
|
||||
module={module}
|
||||
sourceData={libraryPayload.modules?.[module.id] || {}}
|
||||
description={getToolDescription(siteContent, MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad)}
|
||||
context={moduleContext}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LibraryToolExample({ module, sourceData, description, context }) {
|
||||
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
|
||||
const Component = definition.Component;
|
||||
const [scrollable, setScrollable] = useState(false);
|
||||
const title = module.title || definition.label || "Outil";
|
||||
|
||||
return (
|
||||
<section id={getLibraryAnchorId(module)} className="library-tool-section">
|
||||
<div className="library-tool-intro">
|
||||
<div>
|
||||
<h2>{title}</h2>
|
||||
{description && <p>{description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<article className={`module library-tool-example ${scrollable ? "is-scrollable" : ""}`}>
|
||||
<header>
|
||||
<div>
|
||||
<span className="module-icon" aria-hidden="true">
|
||||
<span className={`module-icon-svg module-icon-${definition.icon}`} />
|
||||
</span>
|
||||
<div>
|
||||
<h2>{definition.label}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="library-example-actions">
|
||||
{definition.scrollable && (
|
||||
<button
|
||||
className={`module-scroll-button ${scrollable ? "active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => setScrollable((value) => !value)}
|
||||
aria-label={`${scrollable ? "Désactiver" : "Activer"} le scroll de ${title}`}
|
||||
aria-pressed={scrollable}
|
||||
title={scrollable ? "Désactiver le scroll" : "Activer le scroll"}
|
||||
>
|
||||
<Icon name="scrollable" />
|
||||
<i aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
<button className="library-reset-button" type="button" onClick={() => context.setModuleData(LIBRARY_TOOLBOX_ID, module.id, cloneData(sourceData))} aria-label={`Réinitialiser ${title}`} title="Réinitialiser l'exemple">
|
||||
<Icon name="refresh" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||
<Component toolboxId={LIBRARY_TOOLBOX_ID} moduleId={module.id} context={context} editing={false} />
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,11 +5,13 @@ import { GamesPage } from "../features/games/GamesPage.jsx";
|
|||
import { ToolboxesPage, ToolboxPage } from "../features/toolboxes/ToolboxPages.jsx";
|
||||
import { AboutPage } from "../pages/AboutPage.jsx";
|
||||
import { HomePage } from "../pages/HomePage.jsx";
|
||||
import { LibraryPage } from "../pages/LibraryPage.jsx";
|
||||
import { navigate } from "./hashRouter.js";
|
||||
|
||||
export function RouteContent(props) {
|
||||
const { route } = props;
|
||||
if (route === "/") return <HomePage {...props} />;
|
||||
if (route === "/library") return <LibraryPage {...props} />;
|
||||
if (route === "/about") return <AboutPage siteContent={props.siteContent} />;
|
||||
if (route === "/toolboxes") return <ToolboxesPage {...props} />;
|
||||
if (route.startsWith("/toolbox/")) return <ToolboxPage {...props} toolboxId={route.split("/")[2]} />;
|
||||
|
|
|
|||
|
|
@ -149,6 +149,12 @@ span {
|
|||
gap: 10px;
|
||||
}
|
||||
|
||||
.hero-library-button {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.hero-panel {
|
||||
padding: var(--space-5);
|
||||
border: 1px solid rgba(165, 180, 252, 0.14);
|
||||
|
|
@ -187,6 +193,202 @@ span {
|
|||
filter: drop-shadow(0 22px 36px rgba(0, 0, 0, 0.34));
|
||||
}
|
||||
|
||||
.library-page {
|
||||
display: grid;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
|
||||
.library-hero {
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.library-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.library-summary article {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
border: 1px solid rgba(150, 165, 205, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(5, 7, 17, 0.28);
|
||||
}
|
||||
|
||||
.library-summary h2,
|
||||
.library-summary p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.library-summary h2 {
|
||||
margin-bottom: 5px;
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.library-summary-icon {
|
||||
display: grid;
|
||||
width: 34px;
|
||||
min-width: 34px;
|
||||
height: 34px;
|
||||
place-items: center;
|
||||
border: 1px solid rgba(196, 181, 253, 0.24);
|
||||
border-radius: var(--radius-md);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(139, 92, 246, 0.92), rgba(99, 102, 241, 0.84));
|
||||
box-shadow: var(--shadow-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.library-summary-icon .module-icon-svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.library-toc {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: var(--space-4);
|
||||
scroll-margin-top: var(--space-5);
|
||||
}
|
||||
|
||||
.library-toc a {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
min-height: 40px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 10px 5px 5px;
|
||||
border: 1px solid rgba(150, 165, 205, 0.14);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(5, 7, 17, 0.28);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.library-toc a:hover,
|
||||
.library-toc a:focus-visible {
|
||||
border-color: rgba(246, 196, 83, 0.42);
|
||||
background: rgba(246, 196, 83, 0.08);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.library-toc .module-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.library-toc .module-icon-svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.library-tools {
|
||||
display: grid;
|
||||
gap: var(--space-5);
|
||||
justify-items: stretch;
|
||||
}
|
||||
|
||||
.library-tool-section {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
gap: var(--space-4);
|
||||
overflow: hidden;
|
||||
padding: var(--space-5);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(16, 20, 38, 0.42);
|
||||
box-shadow: var(--shadow-sm);
|
||||
scroll-margin-top: var(--space-8);
|
||||
|
||||
@include mixins.gold-panel-frame;
|
||||
}
|
||||
|
||||
.library-tool-section > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.library-tool-intro {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.library-tool-intro h2,
|
||||
.library-tool-intro p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.library-tool-intro h2 {
|
||||
margin-bottom: 6px;
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
.library-tool-intro p {
|
||||
max-width: 860px;
|
||||
}
|
||||
|
||||
.library-tool-example {
|
||||
width: min(100%, 860px);
|
||||
min-width: 0;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.library-tool-example header > div {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.library-tool-example header p {
|
||||
margin: 3px 0 0;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.library-example-actions {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.library-reset-button {
|
||||
display: inline-grid;
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
flex: 0 0 40px;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.library-reset-button .ui-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.library-tool-example .module-content {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.library-tool-example .module-add-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.library-tool-example .image-annotation-stage,
|
||||
.library-tool-example .images-grid img {
|
||||
max-height: 260px;
|
||||
}
|
||||
|
||||
.nebula-panel {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
|
|
|||
|
|
@ -1002,6 +1002,7 @@
|
|||
border-radius: var(--radius-md);
|
||||
background: rgba(10, 13, 28, 0.99);
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.notepad-list-menu::before {
|
||||
|
|
@ -1014,6 +1015,15 @@
|
|||
border-left: 7px solid transparent;
|
||||
}
|
||||
|
||||
.notepad-list-menu::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.notepad-list-option {
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
|
|
@ -2621,7 +2631,7 @@ textarea:focus {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.combos-category-header > div {
|
||||
.combos-category-header .combos-category-title {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
|
|
@ -2661,6 +2671,8 @@ textarea:focus {
|
|||
|
||||
.combo-drag-handle {
|
||||
align-self: start;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.combo-card-main {
|
||||
|
|
@ -3060,7 +3072,8 @@ button.combo-input-token.combo-input-mouse {
|
|||
|
||||
.counters-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 180px), 220px));
|
||||
justify-content: start;
|
||||
gap: 10px;
|
||||
margin: var(--space-4);
|
||||
}
|
||||
|
|
@ -3068,6 +3081,8 @@ button.combo-input-token.combo-input-mouse {
|
|||
.counter-item {
|
||||
position: relative;
|
||||
display: grid;
|
||||
min-height: 106px;
|
||||
align-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 52px 12px 12px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
|
|
@ -3093,7 +3108,7 @@ button.combo-input-token.combo-input-mouse {
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
.counter-item > div:first-child {
|
||||
.counter-value {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
|
|
@ -3117,11 +3132,16 @@ button.combo-input-token.combo-input-mouse {
|
|||
|
||||
.counter-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(4, 34px);
|
||||
justify-content: start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.counter-actions button {
|
||||
width: 34px;
|
||||
min-width: 34px;
|
||||
min-height: 34px;
|
||||
padding: 0;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue