This commit is contained in:
parent
8b9171d4f9
commit
ef5f977f8a
15 changed files with 3571 additions and 272 deletions
|
|
@ -54,7 +54,9 @@ test("mhwilds data and assets are available", async () => {
|
||||||
items.forEach((item, index) => {
|
items.forEach((item, index) => {
|
||||||
assert.equal(typeof item.nom, "string", `${list.file}.${category}[${index}].nom must be a string`);
|
assert.equal(typeof item.nom, "string", `${list.file}.${category}[${index}].nom must be a string`);
|
||||||
assert.ok(item.nom.trim(), `${list.file}.${category}[${index}].nom must not be empty`);
|
assert.ok(item.nom.trim(), `${list.file}.${category}[${index}].nom must not be empty`);
|
||||||
|
if (item.description !== undefined) {
|
||||||
assert.equal(typeof item.description, "string", `${list.file}.${category}[${index}].description must be a string`);
|
assert.equal(typeof item.description, "string", `${list.file}.${category}[${index}].description must be a string`);
|
||||||
|
}
|
||||||
if (item.quantite !== undefined) {
|
if (item.quantite !== undefined) {
|
||||||
assert.ok(Number.isFinite(Number(item.quantite)), `${list.file}.${category}[${index}].quantite must be numeric`);
|
assert.ok(Number.isFinite(Number(item.quantite)), `${list.file}.${category}[${index}].quantite must be numeric`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,11 @@ export function validateSiteContent(site) {
|
||||||
"toolboxes.modules.checklist.importModalTitle",
|
"toolboxes.modules.checklist.importModalTitle",
|
||||||
"toolboxes.modules.checklist.importButton",
|
"toolboxes.modules.checklist.importButton",
|
||||||
"toolboxes.modules.checklist.hideCompletedSectionsTitle",
|
"toolboxes.modules.checklist.hideCompletedSectionsTitle",
|
||||||
|
"toolboxes.modules.checklist.hideCompletedSectionsFullyTitle",
|
||||||
|
"toolboxes.modules.checklist.completedSectionsModeTitle",
|
||||||
|
"toolboxes.modules.checklist.completedSectionsVisibleLabel",
|
||||||
|
"toolboxes.modules.checklist.completedSectionsReducedLabel",
|
||||||
|
"toolboxes.modules.checklist.completedSectionsHiddenLabel",
|
||||||
"toolboxes.modules.checklist.showCompletedSectionTitle",
|
"toolboxes.modules.checklist.showCompletedSectionTitle",
|
||||||
"toolboxes.modules.checklist.hideCompletedSectionTitle",
|
"toolboxes.modules.checklist.hideCompletedSectionTitle",
|
||||||
"toolboxes.modules.checklist.decrementLabel",
|
"toolboxes.modules.checklist.decrementLabel",
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,8 @@ test("react application defines the expected local toolbox primitives", async ()
|
||||||
assert.match(styleIcons, /ui-icon-calculator/);
|
assert.match(styleIcons, /ui-icon-calculator/);
|
||||||
assert.match(styleIcons, /ui-icon-scrollable/);
|
assert.match(styleIcons, /ui-icon-scrollable/);
|
||||||
assert.match(styleIcons, /ui-icon-hide/);
|
assert.match(styleIcons, /ui-icon-hide/);
|
||||||
|
assert.match(styleIcons, /ui-icon-eye-open/);
|
||||||
|
assert.match(styleIcons, /ui-icon-eye-closed/);
|
||||||
assert.match(styleIcons, /ui-icon-chevron-down/);
|
assert.match(styleIcons, /ui-icon-chevron-down/);
|
||||||
assert.match(styleIcons, /ui-icon-chevron-up/);
|
assert.match(styleIcons, /ui-icon-chevron-up/);
|
||||||
assert.match(styleIcons, /ui-icon-flip/);
|
assert.match(styleIcons, /ui-icon-flip/);
|
||||||
|
|
@ -223,8 +225,9 @@ test("react application defines the expected local toolbox primitives", async ()
|
||||||
assert.match(countersModule, /counter-actions/);
|
assert.match(countersModule, /counter-actions/);
|
||||||
assert.match(linksModule, /normalizeUrl/);
|
assert.match(linksModule, /normalizeUrl/);
|
||||||
assert.match(linksModule, /copyText/);
|
assert.match(linksModule, /copyText/);
|
||||||
assert.match(source, /DEFAULT_SITE_CONTENT/);
|
assert.doesNotMatch(source, /DEFAULT_SITE_CONTENT/);
|
||||||
assert.match(source, /\/data\/site\.json/);
|
assert.match(source, /\/data\/site\.json/);
|
||||||
|
assert.match(source, /setSiteContentError/);
|
||||||
assert.match(source, /useIndexedToolboxes/);
|
assert.match(source, /useIndexedToolboxes/);
|
||||||
assert.match(source, /getStorageEstimate/);
|
assert.match(source, /getStorageEstimate/);
|
||||||
assert.match(source, /DEFAULT_TOOLBOX_ICON/);
|
assert.match(source, /DEFAULT_TOOLBOX_ICON/);
|
||||||
|
|
@ -280,6 +283,7 @@ test("react application defines the expected local toolbox primitives", async ()
|
||||||
assert.match(source, /qtyTarget/);
|
assert.match(source, /qtyTarget/);
|
||||||
assert.match(source, /qtyCurrent/);
|
assert.match(source, /qtyCurrent/);
|
||||||
assert.match(source, /hideCompletedSections/);
|
assert.match(source, /hideCompletedSections/);
|
||||||
|
assert.match(source, /hideCompletedSectionsFully/);
|
||||||
assert.match(source, /hideWhenComplete/);
|
assert.match(source, /hideWhenComplete/);
|
||||||
assert.match(source, /collapsed/);
|
assert.match(source, /collapsed/);
|
||||||
assert.match(source, /module\.scrollable/);
|
assert.match(source, /module\.scrollable/);
|
||||||
|
|
|
||||||
3347
website/public/data/mhwilds/lists/armors.json
Normal file
3347
website/public/data/mhwilds/lists/armors.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,10 @@
|
||||||
{
|
{
|
||||||
"lists": [
|
"lists": [
|
||||||
|
{
|
||||||
|
"id": "armors",
|
||||||
|
"title": "Armures",
|
||||||
|
"file": "armors.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "events_items",
|
"id": "events_items",
|
||||||
"title": "Équipements d'événement",
|
"title": "Équipements d'événement",
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@
|
||||||
"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}",
|
"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",
|
"promptTitle": "Prompt utile",
|
||||||
"promptIntro": "Ajoutez cette précision à votre demande pour obtenir un JSON directement exploitable :",
|
"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 \"nom\", \"description\" et \"quantite\".\nLa propriété \"quantite\" est facultative.\nRegroupe les éléments de manière logique.\nNe renvoie aucun texte, aucune explication et aucun Markdown."
|
"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": [
|
"tools": [
|
||||||
{
|
{
|
||||||
|
|
@ -206,9 +206,14 @@
|
||||||
"importOpenButton": "Importer du texte",
|
"importOpenButton": "Importer du texte",
|
||||||
"importModalTitle": "Importer une checklist",
|
"importModalTitle": "Importer une checklist",
|
||||||
"importButton": "Importer le texte",
|
"importButton": "Importer le texte",
|
||||||
"hideCompletedSectionsTitle": "Masquer les catégories terminées",
|
"hideCompletedSectionsTitle": "Réduire les catégories terminées",
|
||||||
|
"hideCompletedSectionsFullyTitle": "Cacher les catégories terminées",
|
||||||
|
"completedSectionsModeTitle": "Affichage des catégories terminées",
|
||||||
|
"completedSectionsVisibleLabel": "Visible quand terminée",
|
||||||
|
"completedSectionsReducedLabel": "Réduite quand terminée",
|
||||||
|
"completedSectionsHiddenLabel": "Cachée quand terminée",
|
||||||
"showCompletedSectionTitle": "Afficher cette catégorie terminée",
|
"showCompletedSectionTitle": "Afficher cette catégorie terminée",
|
||||||
"hideCompletedSectionTitle": "Masquer cette catégorie terminée",
|
"hideCompletedSectionTitle": "Réduire cette catégorie terminée",
|
||||||
"decrementLabel": "Retirer une quantité",
|
"decrementLabel": "Retirer une quantité",
|
||||||
"incrementLabel": "Ajouter une quantité",
|
"incrementLabel": "Ajouter une quantité",
|
||||||
"currentQuantityLabel": "Quantité actuelle",
|
"currentQuantityLabel": "Quantité actuelle",
|
||||||
|
|
|
||||||
7
website/public/static/icons/eye-closed.svg
Normal file
7
website/public/static/icons/eye-closed.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?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">
|
||||||
|
<path d="M3.17163 5.12988L21.1716 19.1299" stroke="#323232" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M14.3653 13.8456C13.8162 14.5483 12.9609 15 12 15C10.3431 15 9 13.6569 9 12C9 11.3256 9.22253 10.7032 9.59817 10.2021" stroke="#323232" stroke-width="2"/>
|
||||||
|
<path d="M9 5.62667C11.5803 4.45322 14.7268 4.92775 16.8493 7.05025L19.8511 10.052C20.3477 10.5486 20.5959 10.7969 20.7362 11.0605C21.0487 11.6479 21.0487 12.3521 20.7362 12.9395C20.5959 13.2031 20.3477 13.4514 19.8511 13.948V13.948L19.799 14" stroke="#323232" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M7.01596 8.39827C7.40649 8.00774 7.40649 7.37458 7.01596 6.98406C6.62544 6.59353 5.99228 6.59353 5.60175 6.98406L7.01596 8.39827ZM7.65685 16.2427L5.53553 14.1213L4.12132 15.5356L6.24264 17.6569L7.65685 16.2427ZM16.1421 16.2427C13.799 18.5858 10 18.5858 7.65685 16.2427L6.24264 17.6569C9.36684 20.7811 14.4322 20.7811 17.5563 17.6569L16.1421 16.2427ZM5.53553 9.8787L7.01596 8.39827L5.60175 6.98406L4.12132 8.46449L5.53553 9.8787ZM16.7465 15.6383L16.1421 16.2427L17.5563 17.6569L18.1607 17.0526L16.7465 15.6383ZM5.53553 14.1213C4.84888 13.4347 4.40652 12.9893 4.12345 12.6183C3.85798 12.2704 3.82843 12.1077 3.82843 12L1.82843 12C1.82843 12.7208 2.1322 13.3056 2.53341 13.8315C2.917 14.3342 3.47464 14.8889 4.12132 15.5356L5.53553 14.1213ZM4.12132 8.46449C3.47464 9.11116 2.917 9.6658 2.53341 10.1686C2.1322 10.6944 1.82843 11.2792 1.82843 12H3.82843C3.82843 11.8924 3.85798 11.7297 4.12345 11.3817C4.40652 11.0107 4.84888 10.5654 5.53553 9.8787L4.12132 8.46449Z" fill="#323232"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
5
website/public/static/icons/eye-open.svg
Normal file
5
website/public/static/icons/eye-open.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?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">
|
||||||
|
<path d="M15 12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12C9 10.3431 10.3431 9 12 9C13.6569 9 15 10.3431 15 12Z" stroke="#323232" stroke-width="2"/>
|
||||||
|
<path d="M6.94975 7.05025C9.68342 4.31658 14.1156 4.31658 16.8492 7.05025L18.9706 9.17157C20.3039 10.5049 20.9706 11.1716 20.9706 12C20.9706 12.8284 20.3039 13.4951 18.9706 14.8284L16.8492 16.9497C14.1156 19.6834 9.68342 19.6834 6.94975 16.9497L4.82843 14.8284C3.49509 13.4951 2.82843 12.8284 2.82843 12C2.82843 11.1716 3.49509 10.5049 4.82843 9.17157L6.94975 7.05025Z" stroke="#323232" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 821 B |
|
|
@ -9,6 +9,14 @@ function getChecklistSections(list) {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SymbolText({ text }) {
|
||||||
|
return String(text || "").split(/([♂♀])/g).map((part, index) => (
|
||||||
|
part === "♂" || part === "♀"
|
||||||
|
? <span className="game-list-symbol" key={index}>{part}</span>
|
||||||
|
: part
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
export function GameListsPage({ game, lists, selectedListId, onSelectList, linkedToolboxId, onCreateChecklist }) {
|
export function GameListsPage({ game, lists, selectedListId, onSelectList, linkedToolboxId, onCreateChecklist }) {
|
||||||
const selectedList = lists.find((list) => list.id === selectedListId) || lists[0] || null;
|
const selectedList = lists.find((list) => list.id === selectedListId) || lists[0] || null;
|
||||||
const selectedSections = getChecklistSections(selectedList);
|
const selectedSections = getChecklistSections(selectedList);
|
||||||
|
|
@ -30,7 +38,7 @@ export function GameListsPage({ game, lists, selectedListId, onSelectList, linke
|
||||||
<section className="game-list-selector" aria-label="Listes disponibles">
|
<section className="game-list-selector" aria-label="Listes disponibles">
|
||||||
{lists.map((list) => (
|
{lists.map((list) => (
|
||||||
<article className={`game-list-selector-item ${selectedList?.id === list.id ? "active" : ""}`} key={list.id}>
|
<article className={`game-list-selector-item ${selectedList?.id === list.id ? "active" : ""}`} key={list.id}>
|
||||||
<button type="button" onClick={() => onSelectList(list.id)}>
|
<button className="game-list-select-button" type="button" onClick={() => onSelectList(list.id)}>
|
||||||
<strong>{list.title}</strong>
|
<strong>{list.title}</strong>
|
||||||
<small>{list.itemCount} éléments</small>
|
<small>{list.itemCount} éléments</small>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -62,7 +70,7 @@ export function GameListsPage({ game, lists, selectedListId, onSelectList, linke
|
||||||
{selectedList.categories.map((category) => (
|
{selectedList.categories.map((category) => (
|
||||||
<article className="game-list-category" key={category.id}>
|
<article className="game-list-category" key={category.id}>
|
||||||
<header>
|
<header>
|
||||||
<h3>{category.title}</h3>
|
<h3><SymbolText text={category.title} /></h3>
|
||||||
<div className="game-list-actions">
|
<div className="game-list-actions">
|
||||||
<span className="results-count">{category.items.length}</span>
|
<span className="results-count">{category.items.length}</span>
|
||||||
<CopyChecklistItemsButton sections={[getChecklistSections({ categories: [category] })[0]]} title={`Copier ${category.title.toLowerCase()} pour checklist`} />
|
<CopyChecklistItemsButton sections={[getChecklistSections({ categories: [category] })[0]]} title={`Copier ${category.title.toLowerCase()} pour checklist`} />
|
||||||
|
|
@ -72,7 +80,7 @@ export function GameListsPage({ game, lists, selectedListId, onSelectList, linke
|
||||||
{category.items.map((item) => (
|
{category.items.map((item) => (
|
||||||
<li key={`${selectedList.id}-${category.id}-${item.name}-${item.description}`}>
|
<li key={`${selectedList.id}-${category.id}-${item.name}-${item.description}`}>
|
||||||
<div className="game-list-item-title">
|
<div className="game-list-item-title">
|
||||||
<strong>{item.name}</strong>
|
<strong><SymbolText text={item.name} /></strong>
|
||||||
{item.hasQuantity && item.quantity !== 1 ? <em>x{item.quantity}</em> : null}
|
{item.hasQuantity && item.quantity !== 1 ? <em>x{item.quantity}</em> : null}
|
||||||
</div>
|
</div>
|
||||||
{item.description ? <span>{item.description}</span> : null}
|
{item.description ? <span>{item.description}</span> : null}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
||||||
const [textImport, setTextImport] = useState("");
|
const [textImport, setTextImport] = useState("");
|
||||||
const [importOpen, setImportOpen] = useState(false);
|
const [importOpen, setImportOpen] = useState(false);
|
||||||
const closeImportModal = useCallback(() => setImportOpen(false), []);
|
const closeImportModal = useCallback(() => setImportOpen(false), []);
|
||||||
const completedNamedSections = data.sections.filter((section) => section.title && isSectionComplete(section, context));
|
const namedSections = data.sections.filter((section) => section.title);
|
||||||
const allCompletedSectionsHidden = completedNamedSections.length > 0 && completedNamedSections.every((section) => getSectionHideWhenComplete(section, data.hideCompletedSections));
|
const completedSectionsMode = data.hideCompletedSectionsFully ? "hidden" : data.hideCompletedSections ? "reduced" : "visible";
|
||||||
|
|
||||||
function save(sections, settings = {}) {
|
function save(sections, settings = {}) {
|
||||||
context.setModuleData(toolboxId, moduleId, { ...data, ...settings, sections });
|
context.setModuleData(toolboxId, moduleId, { ...data, ...settings, sections });
|
||||||
|
|
@ -49,8 +49,11 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
||||||
context.notify?.("Import checklist terminé.");
|
context.notify?.("Import checklist terminé.");
|
||||||
}
|
}
|
||||||
|
|
||||||
function setHideCompletedSections(hideCompletedSections) {
|
function setCompletedSectionsMode(mode) {
|
||||||
save(data.sections.map((section) => ({ ...section, hideWhenComplete: undefined })), { hideCompletedSections });
|
save(data.sections.map((section) => ({ ...section, hideWhenComplete: undefined })), {
|
||||||
|
hideCompletedSections: mode === "reduced",
|
||||||
|
hideCompletedSectionsFully: mode === "hidden"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -82,19 +85,26 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
||||||
onClose={closeImportModal}
|
onClose={closeImportModal}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{completedNamedSections.length > 0 && (
|
{namedSections.length > 0 && (
|
||||||
<div className="checklist-toolbar">
|
<div className="checklist-toolbar" role="radiogroup" aria-label={textContent.completedSectionsModeTitle || "Affichage des catégories terminées"}>
|
||||||
|
{[
|
||||||
|
{ mode: "visible", label: textContent.completedSectionsVisibleLabel || "Visible", icon: "eye-open" },
|
||||||
|
{ mode: "reduced", label: textContent.completedSectionsReducedLabel || "Réduit", icon: "hide" },
|
||||||
|
{ mode: "hidden", label: textContent.completedSectionsHiddenLabel || "Caché", icon: "eye-closed" }
|
||||||
|
].map((option) => (
|
||||||
<button
|
<button
|
||||||
className={`checklist-hide-toggle ${allCompletedSectionsHidden ? "active" : ""}`}
|
className={`checklist-complete-mode-button ${completedSectionsMode === option.mode ? "active" : ""}`}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setHideCompletedSections(!allCompletedSectionsHidden)}
|
role="radio"
|
||||||
aria-pressed={allCompletedSectionsHidden}
|
aria-checked={completedSectionsMode === option.mode}
|
||||||
title={textContent.hideCompletedSectionsTitle || "Masquer les catégories terminées"}
|
aria-label={option.label}
|
||||||
aria-label={textContent.hideCompletedSectionsTitle || "Masquer les catégories terminées"}
|
key={option.mode}
|
||||||
|
onClick={() => setCompletedSectionsMode(option.mode)}
|
||||||
|
title={option.label}
|
||||||
>
|
>
|
||||||
<Icon name="hide" />
|
<Icon name={option.icon} />
|
||||||
<i aria-hidden="true" />
|
|
||||||
</button>
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ul className="checklist">
|
<ul className="checklist">
|
||||||
|
|
@ -140,6 +150,14 @@ function getSectionHideWhenComplete(section, globalHide) {
|
||||||
return typeof section.hideWhenComplete === "boolean" ? section.hideWhenComplete : globalHide;
|
return typeof section.hideWhenComplete === "boolean" ? section.hideWhenComplete : globalHide;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CategoryTitle({ title }) {
|
||||||
|
return String(title || "").split(/([♂♀])/g).map((part, index) => (
|
||||||
|
part === "♂" || part === "♀"
|
||||||
|
? <span className="checklist-gender-symbol" key={index}>{part}</span>
|
||||||
|
: part
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
function ChecklistSection({ section, context, sections, data, save }) {
|
function ChecklistSection({ section, context, sections, data, save }) {
|
||||||
const showTitle = Boolean(section.title);
|
const showTitle = Boolean(section.title);
|
||||||
const sectionComplete = showTitle && isSectionComplete(section, context);
|
const sectionComplete = showTitle && isSectionComplete(section, context);
|
||||||
|
|
@ -147,6 +165,8 @@ function ChecklistSection({ section, context, sections, data, save }) {
|
||||||
const isAutoCollapsed = sectionComplete && hideWhenComplete;
|
const isAutoCollapsed = sectionComplete && hideWhenComplete;
|
||||||
const isCollapsed = Boolean(section.collapsed) || isAutoCollapsed;
|
const isCollapsed = Boolean(section.collapsed) || isAutoCollapsed;
|
||||||
|
|
||||||
|
if (sectionComplete && data.hideCompletedSectionsFully) return null;
|
||||||
|
|
||||||
function saveItems(items) {
|
function saveItems(items) {
|
||||||
save(sections.map((entry) => entry.id === section.id ? { ...entry, items } : entry).filter((entry) => entry.items.length || entry.title));
|
save(sections.map((entry) => entry.id === section.id ? { ...entry, items } : entry).filter((entry) => entry.items.length || entry.title));
|
||||||
}
|
}
|
||||||
|
|
@ -178,10 +198,10 @@ function ChecklistSection({ section, context, sections, data, save }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={`checklist-section ${showTitle ? "is-grouped" : ""} ${isCollapsed ? "is-collapsed" : ""}`}>
|
<li className={`checklist-section ${showTitle ? "is-grouped" : ""} ${sectionComplete ? "is-complete" : ""} ${isCollapsed ? "is-collapsed" : ""}`}>
|
||||||
{showTitle && (
|
{showTitle && (
|
||||||
<div className="checklist-section-header">
|
<div className="checklist-section-header">
|
||||||
<h3>{section.title || "Sans catégorie"}</h3>
|
<h3><CategoryTitle title={section.title || "Sans catégorie"} /></h3>
|
||||||
<div>
|
<div>
|
||||||
<span>{section.items.filter((item) => context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget).length} / {section.items.length}</span>
|
<span>{section.items.filter((item) => context.clampQty(item.qtyCurrent, item.qtyTarget) >= item.qtyTarget).length} / {section.items.length}</span>
|
||||||
<button
|
<button
|
||||||
|
|
@ -189,8 +209,8 @@ function ChecklistSection({ section, context, sections, data, save }) {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={toggleSectionCollapsed}
|
onClick={toggleSectionCollapsed}
|
||||||
aria-expanded={!isCollapsed}
|
aria-expanded={!isCollapsed}
|
||||||
title={isCollapsed ? context.moduleText?.checklist?.showCompletedSectionTitle || "Afficher cette catégorie" : context.moduleText?.checklist?.hideCompletedSectionTitle || "Masquer cette catégorie"}
|
title={isCollapsed ? context.moduleText?.checklist?.showCompletedSectionTitle || "Afficher cette catégorie" : context.moduleText?.checklist?.hideCompletedSectionTitle || "Réduire cette catégorie"}
|
||||||
aria-label={isCollapsed ? context.moduleText?.checklist?.showCompletedSectionTitle || "Afficher cette catégorie" : context.moduleText?.checklist?.hideCompletedSectionTitle || "Masquer cette catégorie"}
|
aria-label={isCollapsed ? context.moduleText?.checklist?.showCompletedSectionTitle || "Afficher cette catégorie" : context.moduleText?.checklist?.hideCompletedSectionTitle || "Réduire cette catégorie"}
|
||||||
>
|
>
|
||||||
<Icon name={isCollapsed ? "chevron-down" : "chevron-up"} />
|
<Icon name={isCollapsed ? "chevron-down" : "chevron-up"} />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -52,179 +52,6 @@ const TOOLBOX_ICON_FILES = [
|
||||||
const TOOLBOX_ICONS = TOOLBOX_ICON_FILES.map((file) => `${TOOLBOX_ICON_BASE}${file}`);
|
const TOOLBOX_ICONS = TOOLBOX_ICON_FILES.map((file) => `${TOOLBOX_ICON_BASE}${file}`);
|
||||||
const DEFAULT_TOOLBOX_ICON = `${TOOLBOX_ICON_BASE}toolbox.png`;
|
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) {
|
function randomToken(length = 6) {
|
||||||
const bytes = new Uint8Array(length);
|
const bytes = new Uint8Array(length);
|
||||||
if (globalThis.crypto?.getRandomValues) {
|
if (globalThis.crypto?.getRandomValues) {
|
||||||
|
|
@ -345,13 +172,14 @@ function normalizeChecklistSection(section, fallbackTitle = "") {
|
||||||
|
|
||||||
function normalizeChecklistData(data) {
|
function normalizeChecklistData(data) {
|
||||||
const hideCompletedSections = Boolean(data?.hideCompletedSections);
|
const hideCompletedSections = Boolean(data?.hideCompletedSections);
|
||||||
|
const hideCompletedSectionsFully = Boolean(data?.hideCompletedSectionsFully);
|
||||||
const sections = Array.isArray(data?.sections)
|
const sections = Array.isArray(data?.sections)
|
||||||
? data.sections.map((section) => normalizeChecklistSection(section)).filter((section) => section.items.length || section.title)
|
? data.sections.map((section) => normalizeChecklistSection(section)).filter((section) => section.items.length || section.title)
|
||||||
: [];
|
: [];
|
||||||
const legacyItems = (data?.items || []).map(normalizeChecklistItem).filter((item) => item.label);
|
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);
|
const items = sections.flatMap((section) => section.items);
|
||||||
return { hideCompletedSections, sections, items };
|
return { hideCompletedSections, hideCompletedSectionsFully, sections, items };
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeUrl(value) {
|
function normalizeUrl(value) {
|
||||||
|
|
@ -475,7 +303,10 @@ function compactModuleDataForStorage(type, value) {
|
||||||
const normalized = normalizeChecklistData(value);
|
const normalized = normalizeChecklistData(value);
|
||||||
const sections = normalized.sections.map(compactChecklistSectionForStorage).filter((section) => section.items.length || section.title);
|
const sections = normalized.sections.map(compactChecklistSectionForStorage).filter((section) => section.items.length || section.title);
|
||||||
if (!sections.length) return null;
|
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 };
|
if (sections.length === 1 && !sections[0].title && typeof sections[0].hideWhenComplete !== "boolean") return { ...settings, items: sections[0].items };
|
||||||
return { ...settings, sections };
|
return { ...settings, sections };
|
||||||
}
|
}
|
||||||
|
|
@ -649,16 +480,6 @@ function formatDate(value) {
|
||||||
return new Intl.DateTimeFormat("fr-FR", { dateStyle: "medium", timeStyle: "short" }).format(new Date(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() {
|
function currentRoute() {
|
||||||
const hashRoute = location.hash.replace(/^#/, "");
|
const hashRoute = location.hash.replace(/^#/, "");
|
||||||
if (hashRoute) return hashRoute.replace(/#.+$/, "");
|
if (hashRoute) return hashRoute.replace(/#.+$/, "");
|
||||||
|
|
@ -845,7 +666,8 @@ function useIndexedToolboxes(onError) {
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const route = useHashRoute();
|
const route = useHashRoute();
|
||||||
const [siteContent, setSiteContent] = useState(DEFAULT_SITE_CONTENT);
|
const [siteContent, setSiteContent] = useState(null);
|
||||||
|
const [siteContentError, setSiteContentError] = useState("");
|
||||||
const [games, setGames] = useState([]);
|
const [games, setGames] = useState([]);
|
||||||
const [gamesError, setGamesError] = useState("");
|
const [gamesError, setGamesError] = useState("");
|
||||||
const [mhwilds, setMhwilds] = useState(INITIAL_MHWILDS_STATE);
|
const [mhwilds, setMhwilds] = useState(INITIAL_MHWILDS_STATE);
|
||||||
|
|
@ -885,13 +707,19 @@ function App() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([
|
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) => {
|
fetch("/data/games.json").then((response) => response.ok ? response.json() : { games: [] }).catch((error) => {
|
||||||
setGamesError(error.message);
|
setGamesError(error.message);
|
||||||
return { games: [] };
|
return { games: [] };
|
||||||
})
|
})
|
||||||
]).then(([content, gamesPayload]) => {
|
]).then(([content, gamesPayload]) => {
|
||||||
setSiteContent(mergeContent(DEFAULT_SITE_CONTENT, content));
|
if (content) setSiteContent(content);
|
||||||
setGames(Array.isArray(gamesPayload.games) ? gamesPayload.games : []);
|
setGames(Array.isArray(gamesPayload.games) ? gamesPayload.games : []);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -1129,6 +957,17 @@ function App() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!siteContent) {
|
||||||
|
return (
|
||||||
|
<main className="startup-error">
|
||||||
|
<section className="empty">
|
||||||
|
<h1>Contenu indisponible</h1>
|
||||||
|
<p>{siteContentError || "Chargement du contenu..."}</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell route={route} content={siteContent} games={games} toolboxes={store.toolboxes} links={store.links} actions={actions}>
|
<Shell route={route} content={siteContent} games={games} toolboxes={store.toolboxes} links={store.links} actions={actions}>
|
||||||
<RouteContent
|
<RouteContent
|
||||||
|
|
@ -1300,7 +1139,7 @@ function HomePage({ siteContent, toolboxes }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function AboutPage({ siteContent }) {
|
function AboutPage({ siteContent }) {
|
||||||
const content = siteContent.about || DEFAULT_SITE_CONTENT.about;
|
const content = siteContent.about;
|
||||||
const limits = Array.isArray(content.limits) ? content.limits : [];
|
const limits = Array.isArray(content.limits) ? content.limits : [];
|
||||||
const storageSectionIndex = content.sections.findIndex((section) => section.title.toLowerCase().includes("stockage"));
|
const storageSectionIndex = content.sections.findIndex((section) => section.title.toLowerCase().includes("stockage"));
|
||||||
const normalizeReminder = (item, index) => typeof item === "string" ? { title: `Point ${index + 1}`, text: item, icon: "toolbox" } : item;
|
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 }) {
|
function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, storageUsage, getModuleData, updateToolbox, updateModuleData, addScreenshotFiles }) {
|
||||||
const moduleColumns = Number(toolbox.moduleColumns) === 1 ? 1 : 2;
|
const moduleColumns = Number(toolbox.moduleColumns) === 1 ? 1 : 2;
|
||||||
const toolboxGameCover = getGameCardCover(toolboxGame);
|
const toolboxGameCover = getGameCardCover(toolboxGame);
|
||||||
const moduleText = siteContent?.toolboxes?.modules || DEFAULT_SITE_CONTENT.toolboxes.modules;
|
const moduleText = siteContent.toolboxes.modules;
|
||||||
const moduleContext = {
|
const moduleContext = {
|
||||||
getModuleData,
|
getModuleData,
|
||||||
moduleText,
|
moduleText,
|
||||||
|
|
|
||||||
|
|
@ -185,6 +185,12 @@ body.is-modal-open {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.startup-error {
|
||||||
|
min-height: 100vh;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.primary {
|
.primary {
|
||||||
border-color: rgba(246, 196, 83, 0.34);
|
border-color: rgba(246, 196, 83, 0.34);
|
||||||
background:
|
background:
|
||||||
|
|
|
||||||
|
|
@ -178,10 +178,10 @@
|
||||||
.game-list-selector-item {
|
.game-list-selector-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
min-height: 68px;
|
min-height: 100px;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 8px;
|
padding: var(--space-4);
|
||||||
border: 1px solid rgba(165, 180, 252, 0.14);
|
border: 1px solid rgba(165, 180, 252, 0.14);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
background: rgba(16, 20, 38, 0.58);
|
background: rgba(16, 20, 38, 0.58);
|
||||||
|
|
@ -197,32 +197,56 @@
|
||||||
0 0 14px rgba(246, 196, 83, 0.1);
|
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;
|
display: grid;
|
||||||
min-height: 48px;
|
grid-template-columns: minmax(0, 1fr);
|
||||||
align-content: center;
|
grid-template-rows: auto 1fr auto;
|
||||||
gap: 3px;
|
width: 100%;
|
||||||
padding: 0 8px;
|
min-height: 68px;
|
||||||
|
align-content: stretch;
|
||||||
|
justify-content: stretch;
|
||||||
|
justify-items: start;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-list-select-button:hover {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-list-selector-item strong {
|
.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);
|
font-size: var(--font-size-lg);
|
||||||
|
line-height: 1.25;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-list-selector-item small {
|
.game-list-selector-item small {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
align-self: end;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-list-selector-item .game-list-create-button {
|
.game-list-selector-item .game-list-create-button {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
|
align-self: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-list-panel {
|
.game-list-panel {
|
||||||
|
|
@ -271,6 +295,20 @@
|
||||||
font-size: var(--font-size-md);
|
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 {
|
.game-list-category ul {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,16 @@
|
||||||
-webkit-mask-image: url("/static/icons/hide.svg");
|
-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 {
|
.ui-icon-chevron-down {
|
||||||
mask-image: url("/static/icons/chevron-down.svg");
|
mask-image: url("/static/icons/chevron-down.svg");
|
||||||
-webkit-mask-image: url("/static/icons/chevron-down.svg");
|
-webkit-mask-image: url("/static/icons/chevron-down.svg");
|
||||||
|
|
|
||||||
|
|
@ -778,12 +778,26 @@ textarea:focus {
|
||||||
font-size: var(--font-size-sm);
|
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);
|
color: var(--color-text-muted);
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checklist-section-header span {
|
.checklist-section-header > div > span {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
padding: 3px 9px;
|
padding: 3px 9px;
|
||||||
border: 1px solid rgba(246, 196, 83, 0.22);
|
border: 1px solid rgba(246, 196, 83, 0.22);
|
||||||
|
|
@ -794,53 +808,38 @@ textarea:focus {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checklist-hide-toggle {
|
.checklist-complete-mode-button {
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
grid-template-columns: 18px 34px;
|
width: 36px;
|
||||||
width: fit-content;
|
min-width: 36px;
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
align-items: center;
|
place-items: center;
|
||||||
gap: 8px;
|
padding: 0;
|
||||||
padding: 0 6px 0 10px;
|
|
||||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||||
border-radius: var(--radius-pill);
|
border-radius: 0;
|
||||||
background: rgba(5, 7, 17, 0.24);
|
background: rgba(5, 7, 17, 0.24);
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-xs);
|
||||||
font-weight: 800;
|
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;
|
width: 16px;
|
||||||
height: 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 {
|
.checklist-section-collapse-button {
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
|
|
@ -1230,8 +1229,8 @@ textarea:focus {
|
||||||
.module-scroll-button.active,
|
.module-scroll-button.active,
|
||||||
.calculator-scroll-toggle:hover,
|
.calculator-scroll-toggle:hover,
|
||||||
.calculator-scroll-toggle.active,
|
.calculator-scroll-toggle.active,
|
||||||
.checklist-hide-toggle:hover,
|
.checklist-complete-mode-button:hover,
|
||||||
.checklist-hide-toggle.active {
|
.checklist-complete-mode-button.active {
|
||||||
border-color: rgba(196, 181, 253, 0.32);
|
border-color: rgba(196, 181, 253, 0.32);
|
||||||
background:
|
background:
|
||||||
linear-gradient(135deg, rgba(31, 37, 68, 0.94), rgba(18, 22, 44, 0.96)) padding-box,
|
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,
|
.module-scroll-button.active,
|
||||||
.calculator-scroll-toggle.active,
|
.calculator-scroll-toggle.active,
|
||||||
.checklist-hide-toggle.active {
|
.checklist-complete-mode-button.active {
|
||||||
border-color: rgba(246, 196, 83, 0.34);
|
border-color: rgba(246, 196, 83, 0.34);
|
||||||
background:
|
background:
|
||||||
linear-gradient(135deg, rgba(72, 43, 160, 0.82), rgba(45, 52, 146, 0.8)) padding-box,
|
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;
|
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,
|
.module-scroll-button.active i,
|
||||||
.calculator-scroll-toggle.active i,
|
.calculator-scroll-toggle.active i {
|
||||||
.checklist-hide-toggle.active i {
|
|
||||||
border-color: rgba(246, 196, 83, 0.36);
|
border-color: rgba(246, 196, 83, 0.36);
|
||||||
background: rgba(246, 196, 83, 0.12);
|
background: rgba(246, 196, 83, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-scroll-button.active i::before,
|
.module-scroll-button.active i::before,
|
||||||
.calculator-scroll-toggle.active i::before,
|
.calculator-scroll-toggle.active i::before {
|
||||||
.checklist-hide-toggle.active i::before {
|
|
||||||
left: 17px;
|
left: 17px;
|
||||||
background: var(--color-accent-gold);
|
background: var(--color-accent-gold);
|
||||||
box-shadow: 0 0 10px rgba(246, 196, 83, 0.28);
|
box-shadow: 0 0 10px rgba(246, 196, 83, 0.28);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue