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

@ -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>
<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>
<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,52 +237,94 @@ 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-toc-heading">
<h2>{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) => {
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>
);
})}
</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 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 LibraryToolExample({ module, sourceData, description, context }) {
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.docTocTitle || content.tocTitle || "Sommaire"}</h2>
<p>{content.tocDescription || "Accédez directement à un exemple doutil dans la page."}</p>
</div>
<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
href={`#/library#${anchorId}`}
onClick={(event) => {
event.preventDefault();
onSelectCategory(category, anchorId);
}}
>
<span className="module-icon" aria-hidden="true">
<span className={`module-icon-svg module-icon-${definition.icon}`} />
</span>
<span>{title}</span>
</a>
</li>
);
})}
</ul>
</div>
))}
</div>
</nav>
);
}
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,55 +335,220 @@ 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>
<p>{definition.mode}</p>
</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" />
<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>
)}
{definition.editable && (
<button
className={`module-edit-button ${editing ? "active" : ""}`}
type="button"
onClick={() => setEditing((value) => !value)}
aria-label={`${editing ? "Masquer" : "Afficher"} l'ajout de ${title}`}
aria-pressed={editing}
title={editing ? "Masquer l'ajout" : "Ajouter"}
>
<Icon name="add" />
</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>
)}
{definition.editable && (
<button
className={`module-edit-button ${editing ? "active" : ""}`}
type="button"
onClick={() => setEditing((value) => !value)}
aria-label={`${editing ? "Masquer" : "Afficher"} l'ajout de ${title}`}
aria-pressed={editing}
title={editing ? "Masquer l'ajout" : "Ajouter"}
>
<Icon name="add" />
</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={editing} />
</div>
</header>
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
<Component toolboxId={LIBRARY_TOOLBOX_ID} moduleId={module.id} context={context} editing={editing} />
</div>
</article>
</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);