Refactor tool catalog into documentation layout
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s

This commit is contained in:
Shinuwa 2026-08-09 13:34:08 +02:00
parent d53d4160af
commit a9b5ca423a
12 changed files with 1925 additions and 114 deletions

View file

@ -267,7 +267,26 @@ Style courant :
- pas de hover sur les panneaux de lecture ;
- implementation via le mixin `gold-panel-frame` dans `website/src/styles/_mixins.scss`.
## 10. Boutons
## 10. Catalogue Documentation
La page catalogue des outils utilise un layout de documentation.
Regles :
- hero court, puis contenu principal en deux colonnes sur desktop ;
- sommaire a gauche en panneau sticky, avec categories puis liens d'outils de la categorie active ;
- une seule categorie d'usage est affichee a droite a la fois ;
- la categorie active commence par un mini-hero descriptif, sans compteur decoratif ;
- chaque fiche commence par le titre, l'icone, le badge de categorie et une description directe de l'outil ;
- l'exemple interactif reste visible en permanence et centre horizontalement ;
- les reperes de boutons propres a l'outil sont places a gauche de l'exemple, puis empiles au-dessus sur mobile ;
- les reperes sont groupes par categorie de controles de l'outil, avec apercu visuel du bouton et label court ;
- les descriptions de reperes sont reservees aux comportements non evidents, par exemple un bouton qui agit sur toute la ligne ou une action de drag & drop ;
- les fonctionnalites detaillees et formats d'import texte sont presentes sous l'exemple dans un panneau repliable pour ne pas alourdir la lecture avant manipulation ;
- les controles propres au catalogue, comme la reinitialisation d'une demo, ne sont pas documentes comme fonctionnalites de l'outil ;
- sur mobile, le sommaire devient un bloc normal place avant les fiches, sans position sticky.
## 11. Boutons
### Primary

View file

@ -53,8 +53,22 @@ export function validateSiteContent(site) {
"library.summaryLabel",
"library.tocLabel",
"library.tocTitle",
"library.docTocTitle",
"library.tocDescription",
"library.toolsLabel",
"library.categoryLabel",
"library.featureHeading",
"library.controlLegendHeading",
"library.categories.notesTracking",
"library.categories.references",
"library.categories.calculationData",
"library.categories.timeRoutines",
"library.categories.buildsCommands",
"library.categoryDescriptions.notesTracking",
"library.categoryDescriptions.references",
"library.categoryDescriptions.calculationData",
"library.categoryDescriptions.timeRoutines",
"library.categoryDescriptions.buildsCommands",
"home.hero.eyebrow",
"home.hero.title",
"home.hero.description",
@ -442,6 +456,45 @@ export function validateSiteContent(site) {
assert.equal(typeof item, "object", `library.summaryItems[${index}] must be an object`);
});
const libraryToolDocs = valueAt(site, "library.toolDocs");
["notepad", "checklist", "counters", "images", "imageAnnotation", "links", "calculator", "table", "timer", "taskPlanner", "combos", "equipmentPlanner"].forEach((toolType) => {
const docs = libraryToolDocs?.[toolType];
assert.equal(typeof docs, "object", `library.toolDocs.${toolType} must be an object`);
assert.ok(Array.isArray(docs.controls), `library.toolDocs.${toolType}.controls must be an array`);
assert.ok(docs.controls.length >= 1, `library.toolDocs.${toolType}.controls must not be empty`);
docs.controls.forEach((control, index) => {
assert.equal(typeof control, "object", `library.toolDocs.${toolType}.controls[${index}] must be an object`);
assertNonEmptyString({ control }, "control.group");
assertNonEmptyString({ control }, "control.icon");
assertNonEmptyString({ control }, "control.title");
if (control.text) assertNonEmptyString({ control }, "control.text");
if (control.preview) {
assert.equal(typeof control.preview, "object", `library.toolDocs.${toolType}.controls[${index}].preview must be an object`);
assertNonEmptyString({ preview: control.preview }, "preview.kind");
}
});
assert.ok(Array.isArray(docs.features), `library.toolDocs.${toolType}.features must be an array`);
assert.ok(docs.features.length >= 1, `library.toolDocs.${toolType}.features must not be empty`);
docs.features.forEach((feature, index) => {
assertNonEmptyString({ feature }, "feature.title");
assertNonEmptyString({ feature }, "feature.text");
assert.equal(typeof feature, "object", `library.toolDocs.${toolType}.features[${index}] must be an object`);
if (feature.details) {
assert.ok(Array.isArray(feature.details), `library.toolDocs.${toolType}.features[${index}].details must be an array`);
assert.ok(feature.details.length >= 1, `library.toolDocs.${toolType}.features[${index}].details must not be empty`);
feature.details.forEach((detail, detailIndex) => {
assert.equal(typeof detail, "string", `library.toolDocs.${toolType}.features[${index}].details[${detailIndex}] must be a string`);
assert.ok(detail.trim(), `library.toolDocs.${toolType}.features[${index}].details[${detailIndex}] must not be empty`);
});
}
});
if (docs.importFormat) {
assertNonEmptyString({ importFormat: docs.importFormat }, "importFormat.title");
assertNonEmptyString({ importFormat: docs.importFormat }, "importFormat.text");
assertNonEmptyString({ importFormat: docs.importFormat }, "importFormat.example");
}
});
const aboutLimits = valueAt(site, "about.limits");
assert.ok(Array.isArray(aboutLimits), "about.limits must be an array");
assert.ok(aboutLimits.length >= 3, "about.limits must contain at least 3 items");

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;stroke:#020202;stroke-miterlimit:10;stroke-width:1.91px;}</style></defs><rect class="cls-1" x="7.23" y="5.32" width="9.55" height="3.82"/><path class="cls-1" d="M20.59,22.5H5.32A1.91,1.91,0,0,1,4,19.24a1.89,1.89,0,0,1,1.35-.56H20.59"/><line class="cls-1" x1="19.64" y1="18.68" x2="19.64" y2="22.5"/><path class="cls-1" d="M20.59,1.5V18.68H5.32a1.91,1.91,0,0,0-1.91,1.91V3.41A1.92,1.92,0,0,1,5.32,1.5Z"/></svg>

After

Width:  |  Height:  |  Size: 680 B

View file

@ -22,7 +22,10 @@ export function Shell({ route, content, games, toolboxes, links, actions, childr
<a className={`nav-item ${route.startsWith("/games") ? "active" : ""}`} href="#/games"><span className="nav-icon nav-icon-controller" aria-hidden="true" /><strong>{content.navigation.games}</strong></a>
<a className={`nav-item ${route.startsWith("/toolboxes") || route.startsWith("/toolbox") ? "active" : ""}`} href="#/toolboxes"><span className="nav-icon nav-icon-toolbox" aria-hidden="true" /><strong>{content.navigation.toolboxes}</strong></a>
</nav>
<div className="sidebar-secondary-links">
<a className={`sidebar-about-link ${route === "/about" ? "active" : ""}`} href="#/about"><span className="sidebar-about-icon" aria-hidden="true">?</span><strong>{content.navigation.about}</strong></a>
<a className={`sidebar-about-link ${route === "/library" ? "active" : ""}`} href="#/library"><span className="sidebar-about-icon" aria-hidden="true"><Icon name="book" /></span><strong>{content.library.eyebrow}</strong></a>
</div>
<div className="sidebar-note">
<span className="badge">{content.sidebar.badge}</span>
<p>{content.sidebar.note}</p>

View file

@ -72,7 +72,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>
<a className="button hero-library-button library-link-button" href="#/library"><Icon name="book" />{content.libraryLink}</a>
</div>
</section>
<section className="toolbar toolbox-actions-row">

View file

@ -1,5 +1,6 @@
// Rôle : présente le projet, son fonctionnement local et les repères de contribution.
import React, { useEffect } from "react";
import { Icon } from "../components/Icon.jsx";
import { RichText } from "../components/RichText.jsx";
export function AboutPage({ siteContent }) {
@ -65,7 +66,7 @@ export function AboutPage({ siteContent }) {
<p className="eyebrow">Toolbox</p>
<h2>{content.toolsTitle}</h2>
</div>
<a className="button" href="#/library">{content.toolsLibraryLink}</a>
<a className="button library-link-button" href="#/library"><Icon name="book" />{content.toolsLibraryLink}</a>
</div>
<div className="about-tools-list">
{content.tools.map((tool) => (

View file

@ -49,6 +49,14 @@ const MODULE_COMPONENTS = {
imageAnnotation: { label: "Annotation d'images", mode: "Image + marqueurs", icon: "map", Component: ImageAnnotationModule, editable: true }
};
const LIBRARY_CATEGORY_DEFINITIONS = [
{ key: "notesTracking", types: ["notepad", "checklist", "counters"] },
{ key: "references", types: ["images", "imageAnnotation", "links"] },
{ key: "calculationData", types: ["calculator", "table"] },
{ key: "timeRoutines", types: ["timer", "taskPlanner"] },
{ key: "buildsCommands", types: ["combos", "equipmentPlanner"] }
];
function cloneData(value) {
if (value === undefined) return {};
if (typeof structuredClone === "function") return structuredClone(value);
@ -75,6 +83,10 @@ function getLibraryAnchorId(module) {
return `library-tool-${String(module?.id || module?.type || "outil").replace(/[^a-z0-9_-]+/gi, "-").toLowerCase()}`;
}
function getLibraryCategoryAnchorId(category) {
return `library-category-${category.key}`;
}
function getCurrentLibraryAnchor() {
const match = location.hash.match(/^#\/library#(.+)$/);
return match ? decodeURIComponent(match[1]) : "";
@ -88,16 +100,36 @@ function scrollToLibraryTool(anchorId, replace = false) {
window.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
}
function findCategoryForAnchor(categories, anchorId) {
return categories.find((category) => (
getLibraryCategoryAnchorId(category) === anchorId ||
category.modules.some((module) => getLibraryAnchorId(module) === anchorId)
)) || null;
}
function getToolDescription(siteContent, definition) {
const tools = siteContent.about?.tools || [];
return tools.find((tool) => tool.name === definition.label)?.description || "";
}
function getLibraryCategories(modules, content) {
const modulesByType = new Map((modules || []).map((module) => [module.type, module]));
return LIBRARY_CATEGORY_DEFINITIONS.map((category) => ({
...category,
title: content.categories?.[category.key] || category.key,
description: content.categoryDescriptions?.[category.key] || "",
modules: category.types.map((type) => modulesByType.get(type)).filter(Boolean)
})).filter((category) => category.modules.length);
}
export function LibraryPage({ siteContent, actions }) {
const content = siteContent.library;
const [libraryPayload, setLibraryPayload] = useState(null);
const [libraryError, setLibraryError] = useState("");
const [moduleData, setModuleData] = useState({});
const [activeCategoryKey, setActiveCategoryKey] = useState("");
const libraryCategories = useMemo(() => getLibraryCategories(libraryPayload?.toolbox?.modules || [], content), [content, libraryPayload]);
const activeCategory = libraryCategories.find((category) => category.key === activeCategoryKey) || libraryCategories[0] || null;
useEffect(() => {
let cancelled = false;
@ -118,11 +150,13 @@ export function LibraryPage({ siteContent, actions }) {
}, []);
useEffect(() => {
if (!libraryPayload) return;
if (!libraryPayload || !libraryCategories.length) return;
const anchorId = getCurrentLibraryAnchor();
const category = findCategoryForAnchor(libraryCategories, anchorId) || libraryCategories[0];
setActiveCategoryKey(category.key);
if (!anchorId) return;
requestAnimationFrame(() => scrollToLibraryTool(anchorId));
}, [libraryPayload]);
}, [libraryPayload, libraryCategories]);
const moduleContext = useMemo(() => ({
getModuleData: (toolboxId, moduleId, fallback) => moduleData[`${toolboxId}:${moduleId}`] || fallback,
@ -203,23 +237,75 @@ export function LibraryPage({ siteContent, actions }) {
{!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"}>
<div className="library-doc-layout">
<LibraryDocNav
content={content}
categories={libraryCategories}
activeCategoryKey={activeCategory?.key || ""}
onSelectCategory={(category, anchorId = getLibraryCategoryAnchorId(category)) => {
setActiveCategoryKey(category.key);
requestAnimationFrame(() => scrollToLibraryTool(anchorId, true));
}}
/>
<section className="library-tools" aria-label={content.toolsLabel}>
{activeCategory && (
<section className="library-tool-category" key={activeCategory.key} aria-labelledby={getLibraryCategoryAnchorId(activeCategory)}>
<div className="library-category-hero">
<div>
<p className="eyebrow">{content.categoryLabel || "Catégorie"}</p>
<h2 id={getLibraryCategoryAnchorId(activeCategory)}>{activeCategory.title}</h2>
{activeCategory.description && <p>{activeCategory.description}</p>}
</div>
</div>
{activeCategory.modules.map((module) => (
<LibraryToolExample
key={module.id}
module={module}
sourceData={libraryPayload.modules?.[module.id] || {}}
description={getToolDescription(siteContent, MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad)}
categoryTitle={activeCategory.title}
docs={content.toolDocs?.[module.type]}
featureHeading={content.featureHeading}
controlLegendHeading={content.controlLegendHeading}
context={moduleContext}
/>
))}
</section>
)}
</section>
</div>
</>
)}
</div>
);
}
function LibraryDocNav({ content, categories, activeCategoryKey, onSelectCategory }) {
return (
<nav className="library-doc-nav nebula-panel" aria-label={content.tocLabel || "Sommaire des outils"}>
<div className="library-toc-heading">
<h2>{content.tocTitle || "Sommaire"}</h2>
<h2>{content.docTocTitle || content.tocTitle || "Sommaire"}</h2>
<p>{content.tocDescription || "Accédez directement à un exemple doutil dans la page."}</p>
</div>
<div className="library-toc-links">
{libraryPayload.toolbox.modules.map((module) => {
<div className="library-doc-nav-groups">
{categories.map((category) => (
<div className={`library-doc-nav-group ${category.key === activeCategoryKey ? "active" : ""}`} key={category.key}>
<a href={`#/library#${getLibraryCategoryAnchorId(category)}`} onClick={(event) => {
event.preventDefault();
onSelectCategory(category);
}}>{category.title}</a>
<ul hidden={category.key !== activeCategoryKey}>
{category.modules.map((module) => {
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
const title = module.title || definition.label || "Outil";
const anchorId = getLibraryAnchorId(module);
return (
<li key={module.id}>
<a
key={module.id}
href={`#/library#${anchorId}`}
onClick={(event) => {
event.preventDefault();
scrollToLibraryTool(anchorId, true);
onSelectCategory(category, anchorId);
}}
>
<span className="module-icon" aria-hidden="true">
@ -227,28 +313,18 @@ export function LibraryPage({ siteContent, actions }) {
</span>
<span>{title}</span>
</a>
</li>
);
})}
</ul>
</div>
))}
</div>
</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 }) {
function LibraryToolExample({ module, sourceData, description, categoryTitle, docs, featureHeading, controlLegendHeading, context }) {
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
const Component = definition.Component;
const [scrollable, setScrollable] = useState(false);
@ -259,19 +335,23 @@ function LibraryToolExample({ module, sourceData, description, context }) {
<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>
<div className="library-tool-title-row">
<span className="module-icon" aria-hidden="true">
<span className={`module-icon-svg module-icon-${definition.icon}`} />
</span>
<h2>{title}</h2>
</div>
<span className="library-tool-badge">{categoryTitle}</span>
{description && <p>{description}</p>}
</div>
</div>
<div className={`library-example-layout ${Array.isArray(docs?.controls) && docs.controls.length ? "has-legend" : ""}`}>
<ToolControlLegend items={docs?.controls} heading={controlLegendHeading || "Repères"} />
<article className={`module library-tool-example ${scrollable ? "is-scrollable" : ""}`}>
<header>
<div>
<h2>{definition.label}</h2>
</div>
<p>{definition.mode}</p>
</div>
<div className="library-example-actions">
{definition.scrollable && (
@ -308,6 +388,167 @@ function LibraryToolExample({ module, sourceData, description, context }) {
<Component toolboxId={LIBRARY_TOOLBOX_ID} moduleId={module.id} context={context} editing={editing} />
</div>
</article>
</div>
<ToolFeatureList docs={docs} heading={featureHeading || "Fonctionnalités"} />
</section>
);
}
function ToolControlLegend({ items, heading }) {
const legendItems = Array.isArray(items) ? items : [];
if (!legendItems.length) return null;
const groups = legendItems.reduce((accumulator, item) => {
const groupTitle = item.group || "Contrôles";
const existingGroup = accumulator.find((group) => group.title === groupTitle);
if (existingGroup) existingGroup.items.push(item);
else accumulator.push({ title: groupTitle, items: [item] });
return accumulator;
}, []);
return (
<aside className="library-control-legend" aria-label={heading}>
<h3>{heading}</h3>
{groups.map((group) => {
const genericSingleGroup = groups.length === 1 && group.title === "Contrôles";
return (
<section className={`library-control-legend-group ${genericSingleGroup ? "is-generic-single" : ""}`} key={group.title}>
{!genericSingleGroup && <h4>{group.title}</h4>}
<div className="library-control-legend-items">
{group.items.map((item) => (
<div className="library-control-legend-item" key={item.title}>
<ControlPreview item={item} />
<div>
<strong>{item.title}</strong>
{item.text && <p>{item.text}</p>}
</div>
</div>
))}
</div>
</section>
);
})}
</aside>
);
}
function ControlPreview({ item }) {
const preview = item.preview || {};
const kind = preview.kind || "iconButton";
if (kind === "textFormats") {
return (
<span className="library-control-preview notepad-toolbar-group" aria-hidden="true">
<span className="notepad-toolbar-button">B</span>
<span className="notepad-toolbar-button"><em>I</em></span>
<span className="notepad-toolbar-button"><u>U</u></span>
<span className="notepad-toolbar-button"><s>S</s></span>
</span>
);
}
if (kind === "textButton") {
return (
<span className="library-control-preview" aria-hidden="true">
<span className={`notepad-toolbar-button ${preview.active ? "active" : ""}`}>{preview.label || item.title.slice(0, 1)}</span>
</span>
);
}
if (kind === "listDropdown") {
return (
<span className="library-control-preview" aria-hidden="true">
<span className="notepad-toolbar-button active"><span className="notepad-list-icon" /></span>
<span className="notepad-toolbar-button"></span>
<span className="notepad-toolbar-button">1.</span>
</span>
);
}
if (kind === "color") {
return (
<span className="library-control-preview" aria-hidden="true">
<span className="notepad-color-toggle is-rainbow active" />
</span>
);
}
if (kind === "drawingWidth") {
return (
<span className="library-control-preview" aria-hidden="true">
<span className="drawing-width-toggle active"><span style={{ "--line-width": `${preview.width || 4}px` }} /></span>
</span>
);
}
if (kind === "switch") {
return (
<span className="library-control-preview" aria-hidden="true">
<span className={`module-scroll-button drawing-storage-switch ${preview.active ? "active" : ""}`}>
<Icon name={preview.icon || item.icon || "settings"} />
<i />
</span>
</span>
);
}
if (kind === "buttonGroup") {
const buttons = Array.isArray(preview.buttons) ? preview.buttons : [];
return (
<span className="library-control-preview" aria-hidden="true">
{buttons.map((button, index) => (
<span className={`notepad-toolbar-button ${button.label && String(button.label).length > 2 ? "is-wide" : ""} ${button.active ? "active" : ""} ${button.danger ? "danger" : ""}`} key={`${button.label || button.icon || index}-${index}`}>
{button.icon ? <Icon name={button.icon} /> : button.label}
</span>
))}
</span>
);
}
return (
<span className="library-control-preview" aria-hidden="true">
<span className={`notepad-toolbar-button ${preview.active ? "active" : ""} ${preview.danger ? "danger" : ""}`}>
<Icon name={preview.icon || item.icon || "settings"} />
</span>
</span>
);
}
function ToolFeatureList({ docs, heading }) {
const features = Array.isArray(docs?.features) ? docs.features : [];
if (!features.length && !docs?.importFormat) return null;
return (
<section className="library-tool-docs">
<details className="library-tool-feature-list">
<summary>
<h3>{heading}</h3>
<Icon name="chevron-down" />
</summary>
<div className="library-tool-feature-content">
<dl>
{features.map((feature) => (
<div key={feature.title}>
<dt>{feature.title}</dt>
<dd>
<p>{feature.text}</p>
{Array.isArray(feature.details) && feature.details.length > 0 && (
<ul>
{feature.details.map((detail) => <li key={detail}>{detail}</li>)}
</ul>
)}
</dd>
</div>
))}
</dl>
{docs?.importFormat && (
<div className="library-tool-import-format">
<h3>{docs.importFormat.title}</h3>
<p>{docs.importFormat.text}</p>
<pre><code>{docs.importFormat.example}</code></pre>
</div>
)}
</div>
</details>
</section>
);
}

View file

@ -155,6 +155,17 @@ span {
margin-top: var(--space-4);
}
.library-link-button {
align-items: center;
gap: 8px;
}
.library-link-button .ui-icon {
width: 16px;
height: 16px;
color: var(--color-accent-gold);
}
.hero-panel {
padding: var(--space-5);
border: 1px solid rgba(165, 180, 252, 0.14);
@ -249,9 +260,20 @@ span {
background: currentColor;
}
.library-toc {
.library-doc-layout {
display: grid;
gap: var(--space-3);
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
align-items: start;
gap: var(--space-5);
}
.library-doc-nav {
position: sticky;
top: 0;
display: grid;
max-height: calc(100vh - var(--space-10));
gap: var(--space-4);
overflow: auto;
padding: var(--space-4);
scroll-margin-top: var(--space-5);
}
@ -278,53 +300,91 @@ span {
font-size: var(--font-size-sm);
}
.library-toc-links {
display: flex;
flex-wrap: wrap;
gap: 8px;
min-width: 0;
.library-doc-nav-groups {
display: grid;
gap: var(--space-4);
}
.library-toc-links a {
display: inline-flex;
min-width: 0;
min-height: 40px;
align-items: center;
.library-doc-nav-group {
display: grid;
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);
}
.library-doc-nav-group > a {
color: var(--color-text-primary);
font-size: var(--font-size-sm);
font-weight: 800;
text-decoration: none;
}
.library-toc-links a:hover,
.library-toc-links a:focus-visible {
border-color: rgba(246, 196, 83, 0.42);
.library-doc-nav-group > a:hover,
.library-doc-nav-group > a:focus-visible {
color: var(--color-accent-gold);
}
.library-doc-nav-group.active > a {
color: var(--color-accent-gold);
}
.library-doc-nav-group ul {
display: grid;
gap: 6px;
margin: 0;
padding: 0;
list-style: none;
}
.library-doc-nav-group li {
min-width: 0;
}
.library-doc-nav-group li a {
display: grid;
grid-template-columns: 28px minmax(0, 1fr);
min-height: 36px;
align-items: center;
gap: 8px;
padding: 4px 8px 4px 4px;
border: 1px solid rgba(150, 165, 205, 0.1);
border-radius: var(--radius-md);
background: rgba(5, 7, 17, 0.22);
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
font-weight: 700;
text-decoration: none;
}
.library-doc-nav-group li a:hover,
.library-doc-nav-group li a:focus-visible {
border-color: rgba(246, 196, 83, 0.36);
background: rgba(246, 196, 83, 0.08);
color: var(--color-text-primary);
}
.library-toc-links .module-icon {
.library-doc-nav-group .module-icon {
width: 28px;
height: 28px;
border-radius: var(--radius-sm);
}
.library-toc-links .module-icon-svg {
.library-doc-nav-group .module-icon-svg {
width: 16px;
height: 16px;
}
.library-tools {
display: grid;
gap: var(--space-5);
gap: var(--space-6);
justify-items: stretch;
}
.library-tool-category {
display: grid;
gap: var(--space-5);
min-width: 0;
scroll-margin-top: var(--space-8);
}
.library-tool-section {
position: relative;
display: grid;
@ -347,17 +407,75 @@ span {
z-index: 1;
}
.library-category-hero {
position: relative;
isolation: isolate;
display: block;
overflow: hidden;
padding: var(--space-6);
border: 1px solid transparent;
border-radius: var(--radius-lg);
background:
radial-gradient(circle at 92% 12%, rgba(246, 196, 83, 0.12), transparent 30%),
var(--gradient-nebula),
linear-gradient(135deg, rgba(16, 20, 38, 0.9), rgba(5, 7, 17, 0.88));
box-shadow: var(--shadow-sm);
@include mixins.gold-panel-frame;
}
.library-category-hero > * {
position: relative;
z-index: 1;
}
.library-category-hero h2,
.library-category-hero p {
margin: 0;
}
.library-category-hero h2 {
margin-bottom: var(--space-3);
color: var(--color-text-primary);
font-size: clamp(1.7rem, 3vw, var(--font-size-3xl));
line-height: 1.05;
}
.library-category-hero p:not(.eyebrow) {
max-width: 760px;
}
.library-tool-intro {
min-width: 0;
}
.library-tool-intro h2,
.library-tool-title-row {
display: flex;
min-width: 0;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-2);
}
.library-tool-title-row .module-icon {
width: 36px;
height: 36px;
}
.library-tool-title-row .module-icon-svg {
width: 20px;
height: 20px;
}
.library-tool-intro h2 {
margin: 0;
}
.library-tool-intro p {
margin: 0;
}
.library-tool-intro h2 {
margin-bottom: 6px;
font-size: var(--font-size-xl);
}
@ -365,6 +483,277 @@ span {
max-width: 860px;
}
.library-example-layout {
display: grid;
grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
align-items: start;
justify-content: stretch;
gap: var(--space-4);
}
.library-example-layout:not(.has-legend) {
grid-template-columns: minmax(0, 860px);
justify-content: center;
}
.library-tool-docs {
display: block;
}
.library-control-legend,
.library-tool-feature-list {
min-width: 0;
border: 1px solid rgba(150, 165, 205, 0.12);
border-radius: var(--radius-md);
background: rgba(5, 7, 17, 0.28);
}
.library-control-legend {
display: grid;
align-content: start;
gap: var(--space-3);
padding: var(--space-3);
border-color: rgba(246, 196, 83, 0.22);
background:
radial-gradient(circle at 100% 0%, rgba(246, 196, 83, 0.08), transparent 34%),
rgba(5, 7, 17, 0.46);
}
.library-control-legend h3,
.library-tool-feature-list h3 {
margin: 0;
color: var(--color-text-primary);
font-size: var(--font-size-md);
}
.library-control-legend-group {
display: grid;
gap: var(--space-2);
min-width: 0;
padding: var(--space-2);
border: 1px solid rgba(150, 165, 205, 0.1);
border-radius: var(--radius-md);
background: rgba(5, 7, 17, 0.28);
}
.library-control-legend-group.is-generic-single {
padding: 0;
border: 0;
background: transparent;
}
.library-control-legend-group h4 {
margin: 0;
color: var(--color-accent-gold);
font-size: var(--font-size-xs);
font-weight: 900;
text-transform: uppercase;
}
.library-control-legend-items {
display: grid;
gap: 6px;
min-width: 0;
}
.library-control-legend-item {
display: grid;
grid-template-columns: minmax(34px, max-content) minmax(0, 1fr);
align-items: center;
gap: var(--space-2);
min-width: 0;
}
.library-control-preview {
display: inline-flex;
width: fit-content;
max-width: 100%;
min-height: 34px;
align-items: center;
flex-wrap: wrap;
gap: 6px;
pointer-events: none;
}
.library-control-preview.notepad-toolbar-group {
flex-wrap: nowrap;
padding-right: 0;
border-right: 0;
}
.library-control-preview-stack {
display: inline-grid;
gap: 6px;
justify-items: start;
}
.library-control-preview-row {
display: inline-flex;
gap: 6px;
}
.library-control-preview .notepad-toolbar-button,
.library-control-preview .drawing-control-button,
.library-control-preview .notepad-color-toggle,
.library-control-preview .drawing-width-toggle,
.library-control-preview .module-scroll-button {
cursor: default;
}
.library-control-preview .notepad-toolbar-button.is-wide {
width: auto;
min-width: 46px;
padding: 0 9px;
}
.library-control-preview .notepad-color-toggle.active,
.library-control-preview .drawing-width-toggle.active {
border-color: rgba(246, 196, 83, 0.48);
background: rgba(246, 196, 83, 0.1);
color: var(--color-accent-gold);
}
.library-control-preview .module-scroll-button {
min-height: 34px;
}
.library-control-legend-item strong {
display: block;
color: var(--color-text-primary);
font-size: var(--font-size-sm);
line-height: 1.2;
}
.library-control-legend-item p {
margin: 3px 0 0;
font-size: var(--font-size-xs);
line-height: 1.35;
}
.library-tool-feature-list {
display: grid;
overflow: hidden;
padding: 0;
border-color: rgba(246, 196, 83, 0.18);
border-radius: var(--radius-md);
background:
linear-gradient(135deg, rgba(16, 20, 38, 0.52), rgba(5, 7, 17, 0.36)),
rgba(5, 7, 17, 0.3);
}
.library-tool-feature-list summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
padding: var(--space-4);
cursor: pointer;
list-style: none;
}
.library-tool-feature-list summary::-webkit-details-marker {
display: none;
}
.library-tool-feature-list summary .ui-icon {
width: 18px;
height: 18px;
flex: 0 0 auto;
color: var(--color-text-secondary);
transition: transform var(--duration-fast) var(--ease-standard);
}
.library-tool-feature-list[open] summary .ui-icon {
transform: rotate(180deg);
}
.library-tool-feature-content {
display: grid;
gap: var(--space-4);
padding: 0 var(--space-5) var(--space-5);
}
.library-tool-feature-list dl {
display: grid;
gap: var(--space-3);
margin: 0;
}
.library-tool-feature-list dl > div {
display: grid;
grid-template-columns: minmax(180px, 0.28fr) minmax(0, 1fr);
gap: var(--space-3);
padding: var(--space-3);
border: 1px solid rgba(150, 165, 205, 0.12);
border-radius: var(--radius-md);
background: rgba(5, 7, 17, 0.28);
}
.library-tool-feature-list dt {
color: var(--color-text-primary);
font-size: var(--font-size-sm);
font-weight: 800;
}
.library-tool-feature-list dd {
margin: 0;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
}
.library-tool-feature-list dd p {
margin: 0;
}
.library-tool-feature-list dd ul {
display: grid;
gap: 6px;
margin: var(--space-2) 0 0;
padding-left: 1.1rem;
}
.library-tool-feature-list dd li {
color: var(--color-text-secondary);
}
.library-tool-import-format {
display: grid;
gap: var(--space-2);
padding-top: var(--space-3);
border-top: 1px solid rgba(246, 196, 83, 0.16);
}
.library-tool-import-format h3,
.library-tool-import-format p {
margin: 0;
}
.library-tool-import-format pre {
max-width: 100%;
margin: 0;
padding: var(--space-3);
overflow: auto;
border: 1px solid rgba(150, 165, 205, 0.12);
border-radius: var(--radius-sm);
background: rgba(2, 3, 9, 0.42);
color: var(--color-text-primary);
font-size: var(--font-size-sm);
white-space: pre-wrap;
}
.library-tool-badge {
display: inline-flex;
width: fit-content;
margin-bottom: var(--space-3);
padding: 4px 9px;
border: 1px solid rgba(246, 196, 83, 0.24);
border-radius: var(--radius-pill);
background: rgba(246, 196, 83, 0.08);
color: var(--color-accent-gold);
font-size: var(--font-size-xs);
font-weight: 800;
}
.library-tool-example {
width: min(100%, 860px);
min-width: 0;

View file

@ -72,6 +72,11 @@
-webkit-mask-image: url("/static/icons/add.svg");
}
.ui-icon-book {
mask-image: url("/static/icons/book.svg");
-webkit-mask-image: url("/static/icons/book.svg");
}
.ui-icon-copy {
mask-image: url("/static/icons/copy.svg");
-webkit-mask-image: url("/static/icons/copy.svg");

View file

@ -180,7 +180,8 @@
}
.game-home-grid,
.game-layout {
.game-layout,
.library-doc-layout {
grid-template-columns: 1fr;
}
@ -188,6 +189,16 @@
position: static;
}
.library-doc-nav {
position: static;
max-height: none;
}
.library-example-layout,
.library-tool-feature-list dl > div {
grid-template-columns: 1fr;
}
.filter-options {
max-height: none;
}

View file

@ -145,12 +145,17 @@
color: var(--color-text-primary);
}
.sidebar-secondary-links {
display: grid;
gap: 4px;
margin-top: auto;
}
.sidebar-about-link {
display: flex;
min-height: 34px;
align-items: center;
gap: 10px;
margin-top: auto;
padding: 7px 12px;
border: 0;
border-radius: var(--radius-md);
@ -181,6 +186,12 @@
filter var(--duration-fast) var(--ease-standard);
}
.sidebar-about-icon .ui-icon {
width: 14px;
height: 14px;
color: inherit;
}
.sidebar-about-link:hover {
background: var(--color-bg-hover);
color: var(--color-text-secondary);