Improve about page guidance and onboarding visuals

This commit is contained in:
Shinuwa 2026-08-13 23:56:30 +02:00
parent 48dcc8182c
commit 9248b5d075
15 changed files with 610 additions and 173 deletions

View file

@ -99,12 +99,18 @@ export function validateSiteContent(site) {
"about.eyebrow",
"about.title",
"about.description",
"about.sectionsEyebrow",
"about.sectionsTitle",
"about.toolsTitle",
"about.toolsIntro",
"about.toolsLibraryLink",
"about.limitsTitle",
"about.contribute.eyebrow",
"about.contribute.title",
"about.contribute.image",
"about.contribute.imageAlt",
"about.contribute.description",
"about.contribute.exampleIntro",
"about.contribute.example",
"about.contribute.promptTitle",
"about.contribute.promptIntro",
@ -521,17 +527,62 @@ export function validateSiteContent(site) {
assert.ok(Array.isArray(aboutSections), "about.sections must be an array");
assert.ok(aboutSections.length >= 2, "about.sections must contain at least 2 items");
aboutSections.forEach((section) => {
assertNonEmptyString({ section }, "section.icon");
assertNonEmptyString({ section }, "section.image");
assertNonEmptyString({ section }, "section.title");
assertNonEmptyString({ section }, "section.text");
if (section.afterText) {
assertNonEmptyString({ section }, "section.afterText");
}
if (section.warning) {
assertNonEmptyString({ section }, "section.warning");
}
if (section.bullets) {
assert.ok(Array.isArray(section.bullets), "section.bullets must be an array");
assert.ok(section.bullets.length >= 1, "section.bullets must not be empty");
if (section.bulletsColumns) {
assert.ok([1, 2].includes(section.bulletsColumns), "section.bulletsColumns must be 1 or 2");
}
section.bullets.forEach((bullet, bulletIndex) => {
assert.equal(typeof bullet, "string", `section.bullets[${bulletIndex}] must be a string`);
assert.ok(bullet.trim(), `section.bullets[${bulletIndex}] must not be empty`);
});
}
});
const aboutTools = valueAt(site, "about.tools");
assert.ok(Array.isArray(aboutTools), "about.tools must be an array");
assert.ok(aboutTools.length >= 1, "about.tools must contain at least 1 item");
aboutTools.forEach((tool) => {
assertNonEmptyString({ tool }, "tool.icon");
assertNonEmptyString({ tool }, "tool.name");
assertNonEmptyString({ tool }, "tool.description");
const aboutToolGroups = valueAt(site, "about.toolGroups");
assert.ok(Array.isArray(aboutToolGroups), "about.toolGroups must be an array");
assert.ok(aboutToolGroups.length >= 3, "about.toolGroups must contain at least 3 items");
aboutToolGroups.forEach((group, index) => {
assertNonEmptyString({ group }, "group.icon");
assertNonEmptyString({ group }, "group.title");
assertNonEmptyString({ group }, "group.text");
assert.ok(Array.isArray(group.examples), `about.toolGroups[${index}].examples must be an array`);
assert.ok(group.examples.length >= 1, `about.toolGroups[${index}].examples must not be empty`);
group.examples.forEach((example, exampleIndex) => {
assert.equal(typeof example, "string", `about.toolGroups[${index}].examples[${exampleIndex}] must be a string`);
assert.ok(example.trim(), `about.toolGroups[${index}].examples[${exampleIndex}] must not be empty`);
});
});
const contributeBlocks = valueAt(site, "about.contribute.blocks");
assert.ok(Array.isArray(contributeBlocks), "about.contribute.blocks must be an array");
assert.equal(contributeBlocks.length, 3, "about.contribute.blocks must contain 3 items");
contributeBlocks.forEach((block, index) => {
assertNonEmptyString({ block }, "block.icon");
assertNonEmptyString({ block }, "block.title");
assertNonEmptyString({ block }, "block.text");
if (block.note) {
assertNonEmptyString({ block }, "block.note");
}
if (block.bullets) {
assert.ok(Array.isArray(block.bullets), `about.contribute.blocks[${index}].bullets must be an array`);
assert.ok(block.bullets.length >= 1, `about.contribute.blocks[${index}].bullets must not be empty`);
block.bullets.forEach((bullet, bulletIndex) => {
assert.equal(typeof bullet, "string", `about.contribute.blocks[${index}].bullets[${bulletIndex}] must be a string`);
assert.ok(bullet.trim(), `about.contribute.blocks[${index}].bullets[${bulletIndex}] must not be empty`);
});
}
});
const onboardingCards = valueAt(site, "home.onboarding.cards");

View file

@ -72,7 +72,8 @@ test("vite entrypoint and app shell are wired", async () => {
assert.match(aboutPage, /export function AboutPage/);
assert.match(aboutPage, /HomeOnboardingSection/);
assert.match(aboutPage, /siteContent\.home\.onboarding/);
assert.match(aboutPage, /module-icon-\$\{tool\.icon \|\| "notepad"\}/);
assert.match(aboutPage, /module-icon-\$\{group\.icon \|\| "notepad"\}/);
assert.match(aboutPage, /about-tool-examples/);
assert.match(libraryPage, /export function LibraryPage/);
assert.match(libraryPage, /LIBRARY_TOOLBOX_ID/);
assert.match(libraryPage, /TaskPlannerModule/);
@ -133,9 +134,15 @@ test("legacy icons and primary images are available", async () => {
await readFile("website/public/static/icons/rubber.svg", "utf8");
await readFile("website/public/static/icons/trashcan.svg", "utf8");
await readFile("website/public/static/icons/zoom.svg", "utf8");
await readFile("website/public/static/icons/warning.svg", "utf8");
await readFile("website/public/favicon.ico");
await readFile("website/public/static/img/dragon-home.png");
await readFile("website/public/static/img/dragon-sidebar.png");
await readFile("website/public/static/img/dragon-dual-monitor.png");
await readFile("website/public/static/img/dragon-session-tools.png");
await readFile("website/public/static/img/dragon-game-guides.png");
await readFile("website/public/static/img/dragon-local-data.png");
await readFile("website/public/static/img/dragon-contribution-data.png");
await readFile("website/public/static/img/dragon-gameguide.png");
await readFile("website/public/static/img/dragon-toolbox.png");
await readFile("website/public/static/img/onboarding/toolbox-overview.png");

View file

@ -1299,98 +1299,100 @@
"about": {
"eyebrow": "À propos",
"title": "C'est quoi Sokko G ?",
"description": "Un espace second écran pour garder vos outils, repères et infos de jeu sous la main pendant une session.",
"description": "Sokko G est un espace local pour organiser vos notes, outils et repères de jeu. Il est pensé pour rester ouvert à côté de votre partie, sans compte et sans synchronisation obligatoire.",
"sectionsEyebrow": "Concept",
"sectionsTitle": "Faciliter vos sessions gaming",
"sections": [
{
"title": "Le but du site",
"text": "Sokko G sert à regrouper les outils utiles pendant une session : notes, suivis, images, liens et repères de jeu. L’objectif est de garder les informations importantes accessibles rapidement sur un second écran."
},
{
"title": "Fonctionnement",
"text": "Chaque toolbox est composée d’outils que vous ajoutez selon vos besoins. Elle peut rester indépendante ou être associée à une page jeu pour passer rapidement du guide à vos notes personnelles."
},
{
"title": "Stockage local",
"text": "Aucun compte, aucune synchronisation cloud et aucune donnée utilisateur envoyée au serveur. Les toolboxes sont enregistrées localement dans le navigateur que vous utilisez."
},
{
"title": "IndexedDB simplement",
"text": "IndexedDB est un espace de stockage intégré au navigateur. Il permet de conserver des données locales plus riches, comme des images ou des annotations. Ces données restent liées au navigateur et au profil utilisé : un nettoyage, un changement d’appareil ou une réinitialisation peut les supprimer."
}
],
"toolsTitle": "Outils disponibles",
"toolsLibraryLink": "Voir la librairie d'outils",
"contribute": {
"eyebrow": "Contribuer",
"title": "Proposer une liste de jeu",
"description": "Les listes servent à transformer des données de jeu en checklists prêtes à l’emploi : équipements à fabriquer, créatures à capturer, monstres à battre, objets à suivre, etc. Une bonne liste est structurée par catégories, avec des noms clairs, une courte description utile et une quantité uniquement quand elle a du sens.\n\nExemple :",
"example": "{\n \"titre\": \"Équipements\",\n \"armures\": [\n {\n \"nom\": \"Set d'armure sombre\",\n \"description\": \"Obtenir en quête\",\n \"quantite\": 1\n }\n ]\n}",
"promptTitle": "Prompt utile",
"promptIntro": "Ajoutez cette précision à votre demande pour obtenir un JSON directement exploitable :",
"promptText": "Génère uniquement un JSON valide.\nLe JSON doit contenir une propriété racine \"titre\", puis une ou plusieurs catégories.\nChaque catégorie doit être un tableau d’objets.\nChaque objet doit contenir la propriété \"nom\".\nLes propriétés \"description\" et \"quantite\" sont facultatives.\nRegroupe les éléments de manière logique.\nNe renvoie aucun texte, aucune explication et aucun Markdown."
},
"tools": [
{
"icon": "notepad",
"name": "Bloc notes",
"description": "Rédigez des notes riches avec titres, listes, mise en forme et repères rapides pour garder vos infos de session au même endroit."
},
{
"icon": "checklist",
"name": "Checklist",
"description": "Suivez des objectifs en catégories ou hors catégorie, avec cases, quantités et descriptions utiles pour crafts, ressources ou préparatifs."
"icon": "controller",
"image": "/static/img/dragon-session-tools.png",
"title": "Un support pour jouer plus efficacement",
"text": "Sokko G garde vos informations utiles à portée de main :",
"bullets": ["Objectifs", "Notes", "Images", "Timers", "Liens", "Calculs", "Suivis"],
"afterText": "L’idée est simple : limiter les allers-retours et retrouver rapidement ce dont vous avez besoin."
},
{
"icon": "picture",
"name": "Images",
"description": "Regroupez des captures, cartes ou références visuelles ajoutées par fichier ou collage, consultables sans quitter la toolbox."
"image": "/static/img/dragon-dual-monitor.png",
"title": "Pensé pour le second écran",
"text": "De plus en plus de joueurs utilisent deux écrans.\nSokko G profite de cet espace pour garder l’essentiel visible pendant que le jeu reste au premier plan : un guide, une checklist, une carte annotée ou un timer.\nVous consultez vos repères d’un coup d’œil, sans interrompre la partie."
},
{
"icon": "map",
"name": "Annotation d'images",
"description": "Ajoutez marqueurs et dessins sur une image unique pour préparer une carte, un plan de route ou une référence importante."
"icon": "toolbox",
"image": "/static/img/dragon-game-guides.png",
"title": "Guides de jeux et toolboxes",
"text": "Le site propose des pages de jeux qui servent de guides ou de références.\nVos toolboxes restent personnelles, mais elles peuvent être associées à un jeu.\nCela permet de garder les informations générales d’un côté, et vos notes, outils ou suivis de session juste à côté."
},
{
"icon": "link",
"name": "Liens",
"description": "Gardez vos raccourcis de session vers guides, builds, cartes interactives ou ressources externes, avec titres lisibles."
"icon": "export",
"image": "/static/img/dragon-local-data.png",
"title": "Stockage local : avantages et limites",
"text": "Vos toolboxes restent dans le navigateur utilisé :",
"bullets": ["Pas de compte", "Pas de configuration serveur", "Pas de synchronisation imposée"],
"bulletsColumns": 1,
"warning": "En échange, vous risquez de perdre vos données si vous videz le navigateur, changez de profil, changez de machine ou réinstallez le système. Pensez à exporter une sauvegarde avant ces actions."
}
],
"toolsTitle": "Outils disponibles",
"toolsIntro": "Les outils sont là pour gérer différents types d’informations. Vous choisissez seulement les groupes utiles à votre session, puis la librairie détaille chaque outil disponible.",
"toolsLibraryLink": "Voir la librairie d'outils",
"toolGroups": [
{
"icon": "notepad",
"title": "Notes et suivi",
"text": "Écrire des rappels, suivre des objectifs, organiser une routine ou préparer une liste de choses à faire.",
"examples": ["Notes", "Checklists", "Tâches"]
},
{
"icon": "abacus",
"name": "Compteurs",
"description": "Créez des compteurs ajustables pour suivre essais, ressources, scores, victoires ou stocks sans passer par un tableau."
},
{
"icon": "controller",
"name": "Combos",
"description": "Documentez des séquences de touches par catégorie, avec supports manette ou clavier/souris et étapes successives lisibles."
},
{
"icon": "calculator",
"name": "Calculateur",
"description": "Calculez des valeurs, enregistrez des résultats et construisez une arborescence pour comparer coûts, crafts ou conversions."
"icon": "picture",
"title": "Médias et repères visuels",
"text": "Conserver des captures, cartes, images de référence ou annotations visibles pendant la session.",
"examples": ["Images", "Annotations", "Cartes"]
},
{
"icon": "table",
"name": "Tableau",
"description": "Structurez des données en grille avec formules, lignes/colonnes ajustables et copie TSV pour comparer besoins ou valeurs."
"title": "Données et calculs",
"text": "Comparer des valeurs, compter des ressources, préparer des tableaux ou calculer des besoins de craft.",
"examples": ["Compteurs", "Tableaux", "Calculs"]
},
{
"icon": "clock",
"name": "Timer",
"description": "Préparez chronomètres, étapes, comptes à rebours, alertes et intervalles récurrents pour suivre les temps importants."
},
{
"icon": "tasklist",
"name": "Planificateur de tâches",
"description": "Organisez des tâches ponctuelles, quotidiennes ou hebdomadaires avec catégories, sous-tâches et dépendances parent/enfant."
},
{
"icon": "chest-armor",
"name": "Planificateur d'équipements",
"description": "Planifiez un build par types d'équipement, avec pièces actives, statistiques, matériaux de craft, bonus et sertissages."
"title": "Temps et actions",
"text": "Gérer des timers, routines, rotations, commandes ou séquences à reproduire sans perdre le fil.",
"examples": ["Timers", "Combos", "Routines"]
}
],
"contribute": {
"eyebrow": "Contribuer",
"title": "Proposer une liste de données",
"image": "/static/img/dragon-contribution-data.png",
"imageAlt": "Mascotte dragon préparant une liste de données structurée",
"description": "Les contributions attendues sont des listes de données, pas des guides complets. Une liste bien préparée permet de mettre à disposition une checklist complète, prête à être utilisée dans une toolbox.",
"blocks": [
{
"icon": "checklist",
"title": "Pourquoi contribuer ?",
"text": "Une bonne liste fait gagner du temps aux joueurs : ils peuvent importer une checklist déjà structurée au lieu de tout recopier à la main."
},
{
"icon": "tasklist",
"title": "Quels éléments proposer ?",
"text": "Tout ce qui se suit facilement en checklist : armures, matériaux, monstres, objets à récupérer, routes de farm ou objectifs de jeu.",
"bullets": ["Catégories", "Quantités à compléter", "Noms clairs"],
"note": "La propriété description peut servir de renseignement supplémentaire sur une page de liste, mais elle n’est pas affichée dans la checklist importée."
},
{
"icon": "export",
"title": "Comment contribuer ?",
"text": "Vous pouvez préparer la liste vous-même ou demander à une IA de générer le fichier. Le format final est un JSON : c’est ce qui permet à Sokko G de créer la checklist automatiquement."
}
],
"exampleIntro": "Exemple de liste exploitable :",
"example": "{\n \"titre\": \"Équipements\",\n \"armures\": [\n {\n \"nom\": \"Set d'armure sombre\",\n \"description\": \"Obtenir en quête\",\n \"quantite\": 1\n }\n ]\n}",
"promptTitle": "Se faire aider par une IA",
"promptIntro": "Si vous utilisez une IA pour préparer la liste, vous pouvez lui donner cette consigne afin d’obtenir un fichier JSON directement exploitable :",
"promptText": "Génère uniquement un JSON valide.\nLe JSON doit contenir une propriété racine \"titre\", puis une ou plusieurs catégories.\nChaque catégorie doit être un tableau d’objets.\nChaque objet doit contenir la propriété \"nom\".\nLes propriétés \"description\" et \"quantite\" sont facultatives.\nRegroupe les éléments de manière logique.\nNe renvoie aucun texte, aucune explication et aucun Markdown."
},
"limitsTitle": "À retenir",
"limits": [
{

View file

@ -0,0 +1,6 @@
<?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" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="17" r="1" fill="#000000"/>
<path d="M12 10L12 14" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.44722 18.1056L10.2111 4.57771C10.9482 3.10361 13.0518 3.10362 13.7889 4.57771L20.5528 18.1056C21.2177 19.4354 20.2507 21 18.7639 21H5.23607C3.7493 21 2.78231 19.4354 3.44722 18.1056Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 403 KiB

After

Width:  |  Height:  |  Size: 502 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 374 KiB

After

Width:  |  Height:  |  Size: 465 KiB

Before After
Before After

View file

@ -7,7 +7,8 @@ import { HomeOnboardingSection } from "./HomeOnboardingSection.jsx";
export function AboutPage({ siteContent }) {
const content = siteContent.about;
const limits = Array.isArray(content.limits) ? content.limits : [];
const storageSectionIndex = content.sections.findIndex((section) => section.title.toLowerCase().includes("stockage"));
const toolGroups = Array.isArray(content.toolGroups) ? content.toolGroups : [];
const contributeBlocks = Array.isArray(content.contribute?.blocks) ? content.contribute.blocks : [];
const normalizeReminder = (item, index) => typeof item === "string" ? { title: `Point ${index + 1}`, text: item, icon: "toolbox" } : item;
useEffect(() => {
if (!location.hash.includes("#contribuer")) return;
@ -25,41 +26,64 @@ export function AboutPage({ siteContent }) {
</section>
<section className="about-story-section nebula-panel" aria-labelledby="about-story-title">
<div className="about-section-heading">
<p className="eyebrow">Principes</p>
<h2 id="about-story-title">Comment ça fonctionne</h2>
<p className="eyebrow">{content.sectionsEyebrow}</p>
<h2 id="about-story-title">{content.sectionsTitle}</h2>
</div>
<div className="about-story-list">
{content.sections.map((section, index) => (
<article className="about-story-item" key={section.title}>
<span>{String(index + 1).padStart(2, "0")}</span>
<div>
<h3>{section.title}</h3>
<p><RichText text={section.text} /></p>
{index === storageSectionIndex && limits.length > 0 && (
<div className="about-storage-reminders">
<p className="eyebrow">{content.limitsTitle}</p>
<ul className="about-reminders-list">
{limits.map((item, itemIndex) => {
const reminder = normalizeReminder(item, itemIndex);
return (
<li className="about-reminder-item" key={reminder.title || reminder.text}>
<span className="module-icon" aria-hidden="true">
<span className={`module-icon-svg module-icon-${reminder.icon || "toolbox"}`} />
</span>
<div>
<h4>{reminder.title}</h4>
<p><RichText text={reminder.text} /></p>
</div>
</li>
);
})}
</ul>
</div>
<h3>
<span className="module-icon" aria-hidden="true">
<span className={`module-icon-svg module-icon-${section.icon || "toolbox"}`} />
</span>
{section.title}
</h3>
<div className="about-story-body">
{section.image && (
<span className="about-story-visual" aria-hidden="true">
<img className="about-story-illustration" src={section.image} alt="" />
</span>
)}
<div>
<p><RichText text={section.text} /></p>
{Array.isArray(section.bullets) && section.bullets.length > 0 && (
<ul className={`about-story-bullets ${section.bulletsColumns === 1 ? "single-column" : ""}`}>
{section.bullets.map((bullet) => <li key={bullet}>{bullet}</li>)}
</ul>
)}
{section.afterText && <p><RichText text={section.afterText} /></p>}
{section.warning && (
<p className="about-story-warning">
<Icon name="warning" />
<RichText text={section.warning} />
</p>
)}
</div>
</div>
</article>
))}
</div>
{limits.length > 0 && (
<div className="about-storage-reminders">
<p className="eyebrow">{content.limitsTitle}</p>
<ul className="about-reminders-list">
{limits.map((item, itemIndex) => {
const reminder = normalizeReminder(item, itemIndex);
return (
<li className="about-reminder-item" key={reminder.title || reminder.text}>
<div className="about-reminder-heading">
<span className="module-icon" aria-hidden="true">
<span className={`module-icon-svg module-icon-${reminder.icon || "toolbox"}`} />
</span>
<h4>{reminder.title}</h4>
</div>
<p><RichText text={reminder.text} /></p>
</li>
);
})}
</ul>
</div>
)}
</section>
<HomeOnboardingSection content={siteContent.home.onboarding} className="about-onboarding-section" />
<section className="about-tools-section nebula-panel">
@ -67,36 +91,71 @@ export function AboutPage({ siteContent }) {
<div>
<p className="eyebrow">Toolbox</p>
<h2>{content.toolsTitle}</h2>
{content.toolsIntro && <p>{content.toolsIntro}</p>}
</div>
<a className="button library-link-button" href="#/library"><Icon name="book" />{content.toolsLibraryLink}</a>
</div>
<div className="about-tools-list">
{content.tools.map((tool) => (
<article className="about-tool-item" key={tool.name}>
{toolGroups.map((group) => (
<article className="about-tool-item" key={group.title}>
<span className="module-icon" aria-hidden="true">
<span className={`module-icon-svg module-icon-${tool.icon || "notepad"}`} />
<span className={`module-icon-svg module-icon-${group.icon || "notepad"}`} />
</span>
<div>
<h3>{tool.name}</h3>
<p><RichText text={tool.description} /></p>
<h3>{group.title}</h3>
<p><RichText text={group.text} /></p>
{Array.isArray(group.examples) && group.examples.length > 0 && (
<ul className="about-tool-examples" aria-label={`Exemples pour ${group.title}`}>
{group.examples.map((example) => <li key={example}>{example}</li>)}
</ul>
)}
</div>
</article>
))}
</div>
<a className="button primary about-tools-library-button" href="#/library"><Icon name="book" />{content.toolsLibraryLink}</a>
</section>
{content.contribute && (
<section className="about-contribute-section nebula-panel" id="contribuer" aria-labelledby="about-contribute-title">
<div className="about-section-heading">
<p className="eyebrow">{content.contribute.eyebrow}</p>
<h2 id="about-contribute-title">{content.contribute.title}</h2>
{content.contribute.image && (
<img className="about-contribute-illustration" src={content.contribute.image} alt={content.contribute.imageAlt || ""} />
)}
</div>
<div className="about-contribute-content">
<p><RichText text={content.contribute.description} /></p>
<pre><code>{content.contribute.example}</code></pre>
<div className="about-prompt-box">
<h3>{content.contribute.promptTitle}</h3>
<p><RichText text={content.contribute.promptIntro} /></p>
<pre><code>{content.contribute.promptText}</code></pre>
{contributeBlocks.length > 0 && (
<div className="about-contribute-blocks">
{contributeBlocks.map((block) => (
<article className="about-contribute-block" key={block.title}>
<span className="module-icon" aria-hidden="true">
<span className={`module-icon-svg module-icon-${block.icon || "checklist"}`} />
</span>
<div>
<h3>{block.title}</h3>
<p><RichText text={block.text} /></p>
{Array.isArray(block.bullets) && block.bullets.length > 0 && (
<ul>
{block.bullets.map((bullet) => <li key={bullet}>{bullet}</li>)}
</ul>
)}
{block.note && <p className="about-contribute-note"><RichText text={block.note} /></p>}
</div>
</article>
))}
</div>
)}
<div className="about-contribute-formats">
<div className="about-code-example">
{content.contribute.exampleIntro && <p><RichText text={content.contribute.exampleIntro} /></p>}
<pre><code>{content.contribute.example}</code></pre>
</div>
<div className="about-prompt-box">
<h3>{content.contribute.promptTitle}</h3>
<p><RichText text={content.contribute.promptIntro} /></p>
<pre><code>{content.contribute.promptText}</code></pre>
</div>
</div>
</div>
</section>

View file

@ -1242,12 +1242,16 @@ span {
.about-story-section {
display: grid;
grid-template-columns: minmax(210px, 260px) minmax(0, 1fr);
gap: var(--space-6);
gap: var(--space-5);
align-items: start;
padding: var(--space-6);
}
.about-story-section .about-section-heading {
position: static;
max-width: 860px;
}
.about-section-heading {
position: sticky;
top: var(--space-5);
@ -1257,46 +1261,145 @@ span {
margin-bottom: 0;
}
.about-section-heading .about-contribute-illustration {
margin-top: var(--space-6);
}
.about-story-list {
display: grid;
gap: 0;
grid-template-columns: repeat(2, minmax(0, 1fr));
justify-content: center;
width: 100%;
gap: var(--space-4);
margin-inline: auto;
}
.about-story-item {
display: grid;
grid-template-columns: 42px minmax(0, 1fr);
grid-template-rows: auto 1fr;
gap: var(--space-3);
align-items: stretch;
min-width: 0;
overflow: hidden;
padding: var(--space-4);
border: 1px solid rgba(150, 165, 205, 0.12);
border-radius: var(--radius-md);
background:
linear-gradient(135deg, rgba(139, 92, 246, 0.1), transparent 42%),
rgba(16, 20, 38, 0.36);
}
.about-story-body {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: var(--space-4);
padding: var(--space-4) 0;
border-bottom: 1px solid rgba(165, 180, 252, 0.1);
align-items: center;
min-width: 0;
}
.about-story-item:first-child {
padding-top: 0;
}
.about-story-item:last-child {
border-bottom: 0;
}
.about-story-item > span {
color: var(--color-accent-gold);
font-size: var(--font-size-xs);
font-weight: 900;
letter-spacing: 0.08em;
line-height: 1.8;
.about-story-body > div {
display: grid;
align-content: center;
gap: 10px;
min-height: 210px;
padding-block: var(--space-4);
}
.about-story-item h3 {
margin: 0 0 6px;
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
margin: 0;
padding: 0 0 var(--space-3);
border-bottom: 1px solid rgba(246, 196, 83, 0.12);
color: var(--color-text-primary);
font-size: var(--font-size-lg);
font-size: var(--font-size-base);
}
.about-story-item > div > p {
max-width: 108ch;
.about-story-item h3 .module-icon {
width: 30px;
height: 30px;
flex: 0 0 30px;
border: 1px solid rgba(246, 196, 83, 0.24);
border-radius: var(--radius-sm);
background: rgba(246, 196, 83, 0.09);
}
.about-story-item h3 .module-icon-svg {
width: 16px;
height: 16px;
}
.about-story-body p {
margin: 0;
color: var(--color-text-secondary);
line-height: 1.62;
font-size: var(--font-size-sm);
line-height: 1.58;
}
.about-story-bullets {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 4px var(--space-4);
margin: 0;
padding-left: 1.15rem;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
line-height: 1.5;
}
.about-story-bullets li {
padding-left: 2px;
}
.about-story-bullets li::marker {
color: var(--color-accent-gold);
}
.about-story-bullets.single-column {
grid-template-columns: 1fr;
}
.about-story-warning {
display: flex;
align-items: flex-start;
gap: 7px;
color: var(--color-warning);
font-size: var(--font-size-xs);
font-weight: 500;
line-height: 1.5;
}
.about-story-warning .ui-icon {
width: 15px;
min-width: 15px;
height: 15px;
margin-top: 1px;
color: var(--color-warning);
opacity: 0.88;
}
.about-story-visual {
display: grid;
min-height: 230px;
place-items: center;
overflow: hidden;
border: 1px solid rgba(246, 196, 83, 0.12);
border-radius: var(--radius-md);
background:
radial-gradient(circle at center, rgba(139, 92, 246, 0.28), transparent 60%),
radial-gradient(circle at 24% 18%, rgba(246, 196, 83, 0.1), transparent 36%),
rgba(5, 7, 17, 0.18);
}
.about-story-illustration {
display: block;
width: min(96%, 340px);
max-height: 250px;
object-fit: contain;
opacity: 0.94;
filter: saturate(1.02) brightness(1.02) drop-shadow(0 18px 26px rgba(0, 0, 0, 0.32));
}
.about-tools-section {
@ -1305,14 +1408,28 @@ span {
padding: var(--space-6);
}
.about-tools-section .page-heading p:not(.eyebrow) {
max-width: 780px;
margin: 8px 0 0;
color: var(--color-text-secondary);
line-height: 1.56;
}
.about-contribute-section {
display: grid;
grid-template-columns: minmax(210px, 260px) minmax(0, 1fr);
gap: var(--space-6);
align-items: stretch;
scroll-margin-top: var(--space-6);
padding: var(--space-6);
}
.about-contribute-section .about-section-heading {
display: grid;
grid-template-rows: auto 1fr;
align-self: stretch;
}
.about-contribute-content {
display: grid;
gap: var(--space-4);
@ -1325,8 +1442,69 @@ span {
line-height: 1.62;
}
.about-contribute-blocks {
display: grid;
gap: var(--space-4);
}
.about-contribute-block {
display: grid;
grid-template-columns: 30px minmax(0, 1fr);
gap: 12px;
align-items: start;
min-width: 0;
}
.about-contribute-block .module-icon {
width: 28px;
height: 28px;
border: 1px solid rgba(246, 196, 83, 0.22);
border-radius: var(--radius-sm);
background: rgba(246, 196, 83, 0.09);
}
.about-contribute-block .module-icon-svg {
width: 15px;
height: 15px;
}
.about-contribute-block h3 {
margin: 0 0 6px;
color: var(--color-text-primary);
font-size: var(--font-size-sm);
}
.about-contribute-block p {
margin: 0;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
line-height: 1.5;
}
.about-contribute-block ul {
display: grid;
gap: 4px;
margin: 10px 0 0;
padding-left: 1.15rem;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
line-height: 1.45;
}
.about-contribute-block li::marker {
color: var(--color-accent-gold);
}
.about-contribute-block .about-contribute-note {
margin-top: var(--space-3);
color: var(--color-text-muted);
font-size: var(--font-size-xs);
line-height: 1.45;
}
.about-contribute-content pre,
.about-prompt-box {
.about-prompt-box,
.about-code-example {
margin: 0;
border: 1px solid rgba(165, 180, 252, 0.12);
border-radius: var(--radius-md);
@ -1340,6 +1518,58 @@ span {
overflow-wrap: anywhere;
}
.about-contribute-formats {
display: grid;
grid-template-columns: minmax(260px, 0.64fr) minmax(0, 1.36fr);
gap: var(--space-4);
align-items: stretch;
margin-top: var(--space-3);
}
.about-code-example {
display: grid;
grid-template-rows: auto 1fr;
overflow: hidden;
background:
linear-gradient(135deg, rgba(139, 92, 246, 0.08), transparent 45%),
rgba(5, 7, 17, 0.32);
}
.about-code-example > p {
margin: 0;
padding: 8px var(--space-3);
border-bottom: 1px solid rgba(246, 196, 83, 0.14);
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
font-weight: 800;
line-height: 1.4;
}
.about-code-example pre {
border: 0;
border-radius: 0;
background: transparent;
padding: var(--space-3);
}
.about-contribute-illustration {
display: block;
width: min(100%, 260px);
max-height: 420px;
object-fit: contain;
filter: saturate(1.05) brightness(1.04) drop-shadow(0 26px 38px rgba(0, 0, 0, 0.36));
}
.about-section-heading .about-contribute-illustration {
width: min(100%, 300px);
max-height: 520px;
align-self: center;
margin-top: var(--space-6);
object-fit: contain;
opacity: 0.94;
filter: saturate(1.02) brightness(0.99) drop-shadow(0 22px 30px rgba(0, 0, 0, 0.32));
}
.about-contribute-content code {
color: var(--color-text-primary);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
@ -1349,6 +1579,8 @@ span {
}
.about-prompt-box {
display: grid;
align-content: start;
padding: var(--space-4);
}
@ -1412,11 +1644,43 @@ span {
line-height: 1.5;
}
.about-tool-examples {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 10px 0 0;
padding: 0;
list-style: none;
}
.about-tool-examples li {
padding: 4px 8px;
border: 1px solid rgba(246, 196, 83, 0.18);
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;
}
.about-tools-library-button {
display: flex;
width: fit-content;
min-width: min(100%, 300px);
align-items: center;
justify-content: center;
justify-self: center;
margin-top: var(--space-2);
}
.about-storage-reminders {
display: grid;
gap: 12px;
margin-top: var(--space-4);
padding-left: 44px;
gap: var(--space-3);
margin-top: var(--space-2);
padding: var(--space-5);
border: 1px solid rgba(246, 196, 83, 0.16);
border-radius: var(--radius-md);
background: rgba(5, 7, 17, 0.24);
}
.about-storage-reminders .eyebrow {
@ -1426,52 +1690,53 @@ span {
.about-reminders-list {
display: grid;
gap: 0;
grid-template-columns: repeat(4, minmax(0, 260px));
justify-content: center;
gap: var(--space-4);
margin: 0;
padding: 2px 0 2px var(--space-5);
padding: 0;
list-style: none;
border-left: 1px solid rgba(246, 196, 83, 0.22);
}
.about-reminder-item {
display: grid;
grid-template-columns: 38px minmax(0, 1fr);
gap: 12px;
align-items: start;
padding: 12px 0;
border-bottom: 1px solid rgba(165, 180, 252, 0.1);
gap: 10px;
min-width: 0;
padding: var(--space-3);
border: 1px solid rgba(150, 165, 205, 0.1);
border-radius: var(--radius-sm);
background: rgba(16, 20, 38, 0.34);
}
.about-reminder-item:first-child {
padding-top: 0;
}
.about-reminder-item:last-child {
padding-bottom: 0;
border-bottom: 0;
.about-reminder-heading {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.about-reminder-item .module-icon {
width: 34px;
height: 34px;
flex: 0 0 34px;
width: 30px;
height: 30px;
flex: 0 0 30px;
border: 1px solid rgba(246, 196, 83, 0.26);
border-radius: var(--radius-md);
border-radius: var(--radius-sm);
background: rgba(246, 196, 83, 0.1);
}
.about-reminder-item h4 {
margin: 0 0 6px;
margin: 0;
color: var(--color-text-primary);
font-size: var(--font-size-base);
font-size: var(--font-size-xs);
line-height: 1.25;
}
.about-reminder-item p {
max-width: none;
margin: 0;
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
line-height: 1.48;
font-size: var(--font-size-xs);
line-height: 1.45;
}
.home-stats {

View file

@ -42,6 +42,11 @@
-webkit-mask-image: url("/static/icons/info.svg");
}
.ui-icon-warning {
mask-image: url("/static/icons/warning.svg");
-webkit-mask-image: url("/static/icons/warning.svg");
}
.ui-icon-export {
mask-image: url("/static/icons/export.svg");
-webkit-mask-image: url("/static/icons/export.svg");

View file

@ -41,6 +41,10 @@
.about-section-heading {
position: static;
}
.about-reminders-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-height: 760px) and (min-width: 1025px) {
@ -86,7 +90,11 @@
.home-onboarding-layout,
.home-onboarding-cards,
.about-story-section,
.about-story-list,
.about-contribute-blocks,
.about-contribute-section,
.about-contribute-formats,
.about-reminders-list,
.about-tools-list,
.section-grid,
.info-grid {
@ -101,8 +109,42 @@
border-bottom: 0;
}
.about-storage-reminders {
padding-left: 0;
.about-story-list {
width: 100%;
}
.about-story-body {
grid-template-columns: 1fr;
}
.about-story-visual {
min-height: 176px;
}
.about-story-illustration {
width: min(86%, 220px);
}
.about-story-bullets {
grid-template-columns: 1fr;
}
.about-contribute-illustration {
width: min(100%, 220px);
max-height: 260px;
justify-self: center;
}
.about-contribute-section .about-section-heading {
grid-template-rows: auto;
}
.about-section-heading .about-contribute-illustration {
margin-top: var(--space-5);
}
.about-code-example pre {
padding-left: var(--space-4);
}
.hero,