{section.items.filter((item) => context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget).length} / {section.items.length}
diff --git a/website/src/main.jsx b/website/src/main.jsx
index cb3c541..9644178 100644
--- a/website/src/main.jsx
+++ b/website/src/main.jsx
@@ -52,179 +52,6 @@ const TOOLBOX_ICON_FILES = [
const TOOLBOX_ICONS = TOOLBOX_ICON_FILES.map((file) => `${TOOLBOX_ICON_BASE}${file}`);
const DEFAULT_TOOLBOX_ICON = `${TOOLBOX_ICON_BASE}toolbox.png`;
-const DEFAULT_SITE_CONTENT = {
- brand: { name: "Sokko G", homeAriaLabel: "Accueil Sokko G" },
- navigation: { home: "Accueil", toolboxes: "Toolboxes", games: "Jeux", about: "C'est quoi Sokko G ?", mobileGames: "Infos" },
- sidebar: { badge: "", note: "" },
- topbar: { dashboard: "", toolbox: "", games: "" },
- gamesPage: {
- eyebrow: "",
- title: "Jeux",
- description: "",
- emptyTitle: "Aucun jeu disponible",
- emptyText: ""
- },
- home: {
- hero: {
- eyebrow: "",
- title: "Sokko G",
- description: "",
- primaryAction: "Toolboxes",
- secondaryAction: "Jeux"
- },
- stats: {
- toolboxSingular: "toolbox",
- toolboxPlural: "toolboxes",
- toolSingular: "outil",
- toolPlural: "outils"
- },
- origin: {
- eyebrow: "",
- title: "",
- visualAlt: "",
- dialogueAriaLabel: "",
- caption: "",
- lines: []
- },
- aboutLink: "Comprendre Sokko G",
- legal: {
- copyright: "",
- disclaimer: ""
- }
- },
- about: {
- eyebrow: "À propos",
- title: "C'est quoi Sokko G ?",
- description: "Sokko G est une webapp locale pensée pour garder les bons outils et les bonnes informations à portée de main pendant une session gaming.",
- sections: [],
- toolsTitle: "Outils disponibles",
- 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 \"equipements\": [\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 format 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 \"nom\" et \"description\".\nLa propriété \"quantite\" est facultative ; si elle est absente, elle sera interprétée comme 1.\nRegroupe les éléments de manière logique.\nNe renvoie aucun texte, aucune explication et aucun Markdown."
- },
- tools: [
- { icon: "notepad", name: "Bloc notes", description: "Notes rapides pendant une session." },
- { icon: "checklist", name: "Checklist", description: "Objectifs et quantités à suivre." },
- { icon: "picture", name: "Images", description: "Images ajoutées par fichier ou collage." },
- { icon: "map", name: "Annotation d'images", description: "Marqueurs sur une carte ou un plan." },
- { icon: "link", name: "Liens", description: "Raccourcis vers guides, builds ou cartes." },
- { icon: "abacus", name: "Compteurs", description: "Scores, ressources, essais ou résultats." },
- { icon: "calculator", name: "Calculateur", description: "Calculs enregistrés et chaînes de craft." }
- ],
- limitsTitle: "À retenir",
- limits: []
- },
- toolboxes: {
- eyebrow: "",
- title: "Toolboxes",
- newButton: "Nouvelle toolbox",
- importAll: "Importer",
- exportAll: "Exporter",
- importOne: "Importer",
- summary: "",
- storageHelp: {
- title: "",
- text: "",
- items: []
- },
- modules: {
- notepad: { placeholder: "Notes rapides..." },
- checklist: {
- itemPlaceholder: "Nouvel item",
- sectionPlaceholder: "Catégorie",
- quantityLabel: "Quantité cible",
- addButton: "Ajouter",
- importPlaceholder: "Importer plusieurs items...\n# Potions\nPotion:10\nMéga potion:5",
- importOpenButton: "Importer du texte",
- importModalTitle: "Importer une checklist",
- importButton: "Importer le texte",
- hideCompletedSectionsTitle: "Masquer les catégories terminées",
- showCompletedSectionTitle: "Afficher cette catégorie terminée",
- hideCompletedSectionTitle: "Masquer cette catégorie terminée",
- decrementLabel: "Retirer une quantité",
- incrementLabel: "Ajouter une quantité",
- currentQuantityLabel: "Quantité actuelle",
- deleteTitle: "Supprimer"
- },
- screenshots: {
- addImages: "Ajouter des images",
- pastePlaceholder: "Coller une image ici",
- pasteAriaLabel: "Coller une image depuis le presse-papiers",
- imageAlt: "Image",
- previewAriaLabel: "Agrandir l'image",
- annotateAriaLabel: "Annoter l'image",
- annotateTitle: "Annoter",
- deleteAriaLabel: "Supprimer l'image",
- deleteTitle: "Supprimer"
- },
- links: {
- titlePlaceholder: "Nom du lien",
- urlPlaceholder: "https://...",
- addButton: "Ajouter",
- importPlaceholder: "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map",
- importOpenButton: "Importer du texte",
- importModalTitle: "Importer des liens",
- importButton: "Importer le texte",
- copyTitle: "Copier",
- copiedTitle: "Copié",
- deleteTitle: "Supprimer"
- },
- counters: {
- labelPlaceholder: "Nom du compteur",
- addButton: "Ajouter",
- decrementLabel: "Décrémenter",
- incrementLabel: "Incrémenter",
- resetTitle: "Réinitialiser",
- deleteTitle: "Supprimer"
- },
- calculator: {
- expressionLabel: "Calcul",
- expressionPlaceholder: "10*10",
- resultLabel: "Résultat",
- labelLabel: "Libellé",
- labelPlaceholder: "Lingots de fer",
- saveButton: "Enregistrer",
- rootButton: "Revenir à la racine",
- resetTitle: "Réinitialiser",
- copyTitle: "Copier pour checklist",
- copiedTitle: "Copié",
- scrollableTitle: "Liste scrollable",
- emptyResults: "Aucun résultat enregistré.",
- readonlyValueTitle: "Quantité non modifiable",
- useEntryTitle: "Utiliser comme base",
- renameTitle: "Renommer",
- deleteTitle: "Supprimer"
- },
- imageAnnotation: {
- addImage: "Ajouter une image",
- replaceImage: "Remplacer l'image",
- pastePlaceholder: "Coller une image ici",
- pasteAriaLabel: "Coller une image à annoter depuis le presse-papiers",
- stageAriaLabel: "Ajouter un marqueur sur l'image",
- addMarkerAriaLabel: "Ajouter un marqueur sur l'image",
- imageAlt: "Image annotée",
- previewTitle: "Agrandir",
- previewAriaLabel: "Agrandir l'image annotée",
- markerPrefix: "Marqueur",
- markersSingular: "marqueur",
- markersPlural: "marqueurs",
- markerPlaceholder: "Libellé du marqueur",
- emptyImage: "Ajoutez une image pour commencer l'annotation.",
- emptyMarkers: "Agrandissez l'image pour ajouter un marqueur.",
- deleteImageTitle: "Supprimer l'image",
- deleteMarkerTitle: "Supprimer"
- }
- },
- emptyTitle: "Aucune toolbox",
- emptyText: ""
- }
-};
-
function randomToken(length = 6) {
const bytes = new Uint8Array(length);
if (globalThis.crypto?.getRandomValues) {
@@ -345,13 +172,14 @@ function normalizeChecklistSection(section, fallbackTitle = "") {
function normalizeChecklistData(data) {
const hideCompletedSections = Boolean(data?.hideCompletedSections);
+ const hideCompletedSectionsFully = Boolean(data?.hideCompletedSectionsFully);
const sections = Array.isArray(data?.sections)
? data.sections.map((section) => normalizeChecklistSection(section)).filter((section) => section.items.length || section.title)
: [];
const legacyItems = (data?.items || []).map(normalizeChecklistItem).filter((item) => item.label);
- if (!sections.length && legacyItems.length) return { hideCompletedSections, sections: [{ id: uid("section"), title: "", items: legacyItems }], items: legacyItems };
+ if (!sections.length && legacyItems.length) return { hideCompletedSections, hideCompletedSectionsFully, sections: [{ id: uid("section"), title: "", items: legacyItems }], items: legacyItems };
const items = sections.flatMap((section) => section.items);
- return { hideCompletedSections, sections, items };
+ return { hideCompletedSections, hideCompletedSectionsFully, sections, items };
}
function normalizeUrl(value) {
@@ -475,7 +303,10 @@ function compactModuleDataForStorage(type, value) {
const normalized = normalizeChecklistData(value);
const sections = normalized.sections.map(compactChecklistSectionForStorage).filter((section) => section.items.length || section.title);
if (!sections.length) return null;
- const settings = normalized.hideCompletedSections ? { hideCompletedSections: true } : {};
+ const settings = {
+ ...(normalized.hideCompletedSections ? { hideCompletedSections: true } : {}),
+ ...(normalized.hideCompletedSectionsFully ? { hideCompletedSectionsFully: true } : {})
+ };
if (sections.length === 1 && !sections[0].title && typeof sections[0].hideWhenComplete !== "boolean") return { ...settings, items: sections[0].items };
return { ...settings, sections };
}
@@ -649,16 +480,6 @@ function formatDate(value) {
return new Intl.DateTimeFormat("fr-FR", { dateStyle: "medium", timeStyle: "short" }).format(new Date(value));
}
-function mergeContent(defaults, overrides) {
- if (!overrides || typeof overrides !== "object" || Array.isArray(overrides)) return defaults;
- return Object.fromEntries(Object.entries(defaults).map(([key, value]) => {
- const override = overrides[key];
- if (Array.isArray(value)) return [key, Array.isArray(override) ? override : value];
- if (value && typeof value === "object") return [key, mergeContent(value, override)];
- return [key, override ?? value];
- }));
-}
-
function currentRoute() {
const hashRoute = location.hash.replace(/^#/, "");
if (hashRoute) return hashRoute.replace(/#.+$/, "");
@@ -845,7 +666,8 @@ function useIndexedToolboxes(onError) {
function App() {
const route = useHashRoute();
- const [siteContent, setSiteContent] = useState(DEFAULT_SITE_CONTENT);
+ const [siteContent, setSiteContent] = useState(null);
+ const [siteContentError, setSiteContentError] = useState("");
const [games, setGames] = useState([]);
const [gamesError, setGamesError] = useState("");
const [mhwilds, setMhwilds] = useState(INITIAL_MHWILDS_STATE);
@@ -885,13 +707,19 @@ function App() {
useEffect(() => {
Promise.all([
- fetch("/data/site.json").then((response) => response.ok ? response.json() : DEFAULT_SITE_CONTENT).catch(() => DEFAULT_SITE_CONTENT),
+ fetch("/data/site.json").then((response) => {
+ if (!response.ok) throw new Error("Impossible de charger le contenu du site.");
+ return response.json();
+ }).catch((error) => {
+ setSiteContentError(error.message);
+ return null;
+ }),
fetch("/data/games.json").then((response) => response.ok ? response.json() : { games: [] }).catch((error) => {
setGamesError(error.message);
return { games: [] };
})
]).then(([content, gamesPayload]) => {
- setSiteContent(mergeContent(DEFAULT_SITE_CONTENT, content));
+ if (content) setSiteContent(content);
setGames(Array.isArray(gamesPayload.games) ? gamesPayload.games : []);
});
}, []);
@@ -1129,6 +957,17 @@ function App() {
}
};
+ if (!siteContent) {
+ return (
+
+
+ Contenu indisponible
+ {siteContentError || "Chargement du contenu..."}
+
+
+ );
+ }
+
return (
section.title.toLowerCase().includes("stockage"));
const normalizeReminder = (item, index) => typeof item === "string" ? { title: `Point ${index + 1}`, text: item, icon: "toolbox" } : item;
@@ -1628,7 +1467,7 @@ function ToolboxGameIcon({ game }) {
function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, storageUsage, getModuleData, updateToolbox, updateModuleData, addScreenshotFiles }) {
const moduleColumns = Number(toolbox.moduleColumns) === 1 ? 1 : 2;
const toolboxGameCover = getGameCardCover(toolboxGame);
- const moduleText = siteContent?.toolboxes?.modules || DEFAULT_SITE_CONTENT.toolboxes.modules;
+ const moduleText = siteContent.toolboxes.modules;
const moduleContext = {
getModuleData,
moduleText,
diff --git a/website/src/styles/_base.scss b/website/src/styles/_base.scss
index a4a72b2..c40bd15 100644
--- a/website/src/styles/_base.scss
+++ b/website/src/styles/_base.scss
@@ -185,6 +185,12 @@ body.is-modal-open {
overflow: hidden;
}
+.startup-error {
+ min-height: 100vh;
+ align-items: center;
+ justify-content: center;
+}
+
.primary {
border-color: rgba(246, 196, 83, 0.34);
background:
diff --git a/website/src/styles/_games.scss b/website/src/styles/_games.scss
index 1c9cdb6..ded251d 100644
--- a/website/src/styles/_games.scss
+++ b/website/src/styles/_games.scss
@@ -178,10 +178,10 @@
.game-list-selector-item {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
- min-height: 68px;
- align-items: center;
+ min-height: 100px;
+ align-items: stretch;
gap: 10px;
- padding: 8px;
+ padding: var(--space-4);
border: 1px solid rgba(165, 180, 252, 0.14);
border-radius: var(--radius-md);
background: rgba(16, 20, 38, 0.58);
@@ -197,32 +197,56 @@
0 0 14px rgba(246, 196, 83, 0.1);
}
-.game-list-selector-item > button:not(.game-list-create-button) {
+.game-list-select-button {
display: grid;
- min-height: 48px;
- align-content: center;
- gap: 3px;
- padding: 0 8px;
+ grid-template-columns: minmax(0, 1fr);
+ grid-template-rows: auto 1fr auto;
+ width: 100%;
+ min-height: 68px;
+ align-content: stretch;
+ justify-content: stretch;
+ justify-items: start;
+ gap: 4px;
+ padding: 0;
border: 0;
+ border-radius: 0;
background: transparent;
box-shadow: none;
text-align: left;
+ transition: none;
+}
+
+.game-list-select-button:hover {
+ border: 0;
+ background: transparent;
+ box-shadow: none;
}
.game-list-selector-item strong {
+ display: block;
+ width: 100%;
+ min-height: calc(var(--font-size-lg) * 2.5);
+ align-self: start;
font-size: var(--font-size-lg);
+ line-height: 1.25;
+ text-align: left;
}
.game-list-selector-item small {
+ display: block;
+ width: 100%;
+ align-self: end;
color: var(--color-text-muted);
font-size: var(--font-size-sm);
font-weight: 800;
+ text-align: left;
}
.game-list-selector-item .game-list-create-button {
width: 40px;
min-width: 40px;
min-height: 40px;
+ align-self: start;
}
.game-list-panel {
@@ -271,6 +295,20 @@
font-size: var(--font-size-md);
}
+.game-list-symbol {
+ display: inline-block;
+ margin-left: 3px;
+ color: currentColor;
+ font-family: "Segoe UI Symbol", "Noto Sans Symbols 2", "Noto Sans Symbols", "Apple Symbols", sans-serif;
+ font-size: 1.18em;
+ font-weight: 900;
+ line-height: 1;
+ text-shadow:
+ 0 0 0 currentColor,
+ 0 0 4px rgba(246, 196, 83, 0.18);
+ transform: translateY(1px);
+}
+
.game-list-category ul {
display: grid;
grid-template-columns: 1fr;
diff --git a/website/src/styles/_icons.scss b/website/src/styles/_icons.scss
index 9b3517b..e4ed102 100644
--- a/website/src/styles/_icons.scss
+++ b/website/src/styles/_icons.scss
@@ -86,6 +86,16 @@
-webkit-mask-image: url("/static/icons/hide.svg");
}
+.ui-icon-eye-closed {
+ mask-image: url("/static/icons/eye-closed.svg");
+ -webkit-mask-image: url("/static/icons/eye-closed.svg");
+}
+
+.ui-icon-eye-open {
+ mask-image: url("/static/icons/eye-open.svg");
+ -webkit-mask-image: url("/static/icons/eye-open.svg");
+}
+
.ui-icon-chevron-down {
mask-image: url("/static/icons/chevron-down.svg");
-webkit-mask-image: url("/static/icons/chevron-down.svg");
diff --git a/website/src/styles/_toolboxes.scss b/website/src/styles/_toolboxes.scss
index 15e5bc9..510016f 100644
--- a/website/src/styles/_toolboxes.scss
+++ b/website/src/styles/_toolboxes.scss
@@ -778,12 +778,26 @@ textarea:focus {
font-size: var(--font-size-sm);
}
-.checklist-section.is-collapsed .checklist-section-header h3 {
+.checklist-gender-symbol {
+ display: inline-block;
+ margin-left: 3px;
+ color: currentColor;
+ font-family: "Segoe UI Symbol", "Noto Sans Symbols 2", "Noto Sans Symbols", "Apple Symbols", sans-serif;
+ font-size: 1.18em;
+ font-weight: 900;
+ line-height: 1;
+ text-shadow:
+ 0 0 0 currentColor,
+ 0 0 4px rgba(246, 196, 83, 0.22);
+ transform: translateY(1px);
+}
+
+.checklist-section.is-complete .checklist-section-header h3 {
color: var(--color-text-muted);
text-decoration: line-through;
}
-.checklist-section-header span {
+.checklist-section-header > div > span {
flex: 0 0 auto;
padding: 3px 9px;
border: 1px solid rgba(246, 196, 83, 0.22);
@@ -794,53 +808,38 @@ textarea:focus {
font-weight: 800;
}
-.checklist-hide-toggle {
+.checklist-complete-mode-button {
display: inline-grid;
- grid-template-columns: 18px 34px;
- width: fit-content;
+ width: 36px;
+ min-width: 36px;
min-height: 34px;
- align-items: center;
- gap: 8px;
- padding: 0 6px 0 10px;
+ place-items: center;
+ padding: 0;
border: 1px solid rgba(165, 180, 252, 0.12);
- border-radius: var(--radius-pill);
+ border-radius: 0;
background: rgba(5, 7, 17, 0.24);
color: var(--color-text-secondary);
- font-size: var(--font-size-sm);
+ font-size: var(--font-size-xs);
font-weight: 800;
}
-.checklist-hide-toggle .ui-icon {
+.checklist-complete-mode-button:first-child {
+ border-radius: var(--radius-pill) 0 0 var(--radius-pill);
+}
+
+.checklist-complete-mode-button:last-child {
+ border-radius: 0 var(--radius-pill) var(--radius-pill) 0;
+}
+
+.checklist-complete-mode-button + .checklist-complete-mode-button {
+ margin-left: -1px;
+}
+
+.checklist-complete-mode-button .ui-icon {
width: 16px;
height: 16px;
}
-.checklist-hide-toggle i {
- position: relative;
- display: block;
- width: 34px;
- height: 18px;
- border: 1px solid rgba(165, 180, 252, 0.18);
- border-radius: var(--radius-pill);
- background: rgba(7, 10, 24, 0.72);
-}
-
-.checklist-hide-toggle i::before {
- content: "";
- position: absolute;
- top: 50%;
- left: 3px;
- width: 12px;
- height: 12px;
- border-radius: 50%;
- background: var(--color-text-muted);
- transform: translateY(-50%);
- transition:
- left var(--duration-fast) var(--ease-standard),
- background-color var(--duration-fast) var(--ease-standard),
- box-shadow var(--duration-fast) var(--ease-standard);
-}
-
.checklist-section-collapse-button {
display: inline-grid;
width: 30px;
@@ -1230,8 +1229,8 @@ textarea:focus {
.module-scroll-button.active,
.calculator-scroll-toggle:hover,
.calculator-scroll-toggle.active,
-.checklist-hide-toggle:hover,
-.checklist-hide-toggle.active {
+.checklist-complete-mode-button:hover,
+.checklist-complete-mode-button.active {
border-color: rgba(196, 181, 253, 0.32);
background:
linear-gradient(135deg, rgba(31, 37, 68, 0.94), rgba(18, 22, 44, 0.96)) padding-box,
@@ -1286,23 +1285,22 @@ textarea:focus {
.module-scroll-button.active,
.calculator-scroll-toggle.active,
-.checklist-hide-toggle.active {
+.checklist-complete-mode-button.active {
border-color: rgba(246, 196, 83, 0.34);
background:
linear-gradient(135deg, rgba(72, 43, 160, 0.82), rgba(45, 52, 146, 0.8)) padding-box,
linear-gradient(135deg, rgba(246, 196, 83, 0.58), rgba(139, 92, 246, 0.32)) border-box;
+ color: var(--color-text-primary);
}
.module-scroll-button.active i,
-.calculator-scroll-toggle.active i,
-.checklist-hide-toggle.active i {
+.calculator-scroll-toggle.active i {
border-color: rgba(246, 196, 83, 0.36);
background: rgba(246, 196, 83, 0.12);
}
.module-scroll-button.active i::before,
-.calculator-scroll-toggle.active i::before,
-.checklist-hide-toggle.active i::before {
+.calculator-scroll-toggle.active i::before {
left: 17px;
background: var(--color-accent-gold);
box-shadow: 0 0 10px rgba(246, 196, 83, 0.28);