add list page and multiples optis
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
This commit is contained in:
parent
d9d22683dc
commit
959fa42454
30 changed files with 1015 additions and 45 deletions
|
|
@ -10,21 +10,46 @@ async function copyText(value) {
|
|||
}
|
||||
}
|
||||
|
||||
function notify(message) {
|
||||
window.dispatchEvent(new CustomEvent("sokkog:notify", { detail: { message } }));
|
||||
}
|
||||
|
||||
export function formatChecklistImportItems(labels) {
|
||||
return labels
|
||||
.map((label) => String(label || "").trim())
|
||||
.map((entry) => {
|
||||
if (typeof entry === "object" && entry !== null) {
|
||||
const label = String(entry.label || entry.name || "").trim();
|
||||
const quantity = Number.parseInt(entry.quantity ?? entry.quantite ?? 1, 10);
|
||||
return label ? { label, quantity: Number.isFinite(quantity) ? quantity : 1 } : null;
|
||||
}
|
||||
const label = String(entry || "").trim();
|
||||
return label ? { label, quantity: 1 } : null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.map((label) => `${label}:1`)
|
||||
.map((entry) => `${entry.label}:${entry.quantity}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function CopyChecklistItemsButton({ labels, title = "Copier pour checklist" }) {
|
||||
export function formatChecklistImportSections(sections = []) {
|
||||
return sections
|
||||
.map((section) => {
|
||||
const title = String(section?.title || "").trim();
|
||||
const items = formatChecklistImportItems(section?.items || []);
|
||||
if (!items) return "";
|
||||
return title ? `# ${title}\n${items}` : items;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
export function CopyChecklistItemsButton({ labels, sections, title = "Copier pour checklist" }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const text = formatChecklistImportItems(labels);
|
||||
const text = sections ? formatChecklistImportSections(sections) : formatChecklistImportItems(labels);
|
||||
|
||||
async function copyItems() {
|
||||
if (!text || !await copyText(text)) return;
|
||||
setCopied(true);
|
||||
notify("Copié dans le presse-papiers.");
|
||||
window.setTimeout(() => setCopied(false), 1400);
|
||||
}
|
||||
|
||||
|
|
|
|||
15
website/src/features/games/GameBreadcrumb.jsx
Normal file
15
website/src/features/games/GameBreadcrumb.jsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { Icon } from "../../components/Icon.jsx";
|
||||
|
||||
export function GameBreadcrumb({ game }) {
|
||||
if (!game) return null;
|
||||
|
||||
return (
|
||||
<nav className="game-breadcrumb" aria-label="Fil d'Ariane">
|
||||
<a href="#/" aria-label="Accueil">
|
||||
<Icon name="home" />
|
||||
</a>
|
||||
<span aria-hidden="true">/</span>
|
||||
<a href={`#/games/${game.id}`}>{game.title}</a>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
95
website/src/features/games/GameListsPage.jsx
Normal file
95
website/src/features/games/GameListsPage.jsx
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { Icon } from "../../components/Icon.jsx";
|
||||
import { CopyChecklistItemsButton } from "./CopyChecklistItemsButton.jsx";
|
||||
import { GameBreadcrumb } from "./GameBreadcrumb.jsx";
|
||||
|
||||
function getChecklistSections(list) {
|
||||
return (list?.categories || []).map((category) => ({
|
||||
title: category.title,
|
||||
items: category.items.map((item) => ({ label: item.name, quantity: item.quantity }))
|
||||
}));
|
||||
}
|
||||
|
||||
export function GameListsPage({ game, lists, selectedListId, onSelectList, linkedToolboxId, onCreateChecklist }) {
|
||||
const selectedList = lists.find((list) => list.id === selectedListId) || lists[0] || null;
|
||||
const selectedSections = getChecklistSections(selectedList);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="page-heading game-heading">
|
||||
<div>
|
||||
<GameBreadcrumb game={game} />
|
||||
<div className="game-title-row">
|
||||
<h1>Listes</h1>
|
||||
</div>
|
||||
<p>Sélectionnez une liste, puis copiez-la ou créez une checklist dans la toolbox associée.</p>
|
||||
<a className="game-list-help-link button" href="#/about#contribuer">Proposer une nouvelle liste</a>
|
||||
</div>
|
||||
</section>
|
||||
{lists.length ? (
|
||||
<>
|
||||
<section className="game-list-selector" aria-label="Listes disponibles">
|
||||
{lists.map((list) => (
|
||||
<article className={`game-list-selector-item ${selectedList?.id === list.id ? "active" : ""}`} key={list.id}>
|
||||
<button type="button" onClick={() => onSelectList(list.id)}>
|
||||
<strong>{list.title}</strong>
|
||||
<small>{list.itemCount} éléments</small>
|
||||
</button>
|
||||
<button
|
||||
className="game-list-create-button primary"
|
||||
type="button"
|
||||
disabled={!linkedToolboxId}
|
||||
onClick={() => onCreateChecklist?.(list)}
|
||||
title={linkedToolboxId ? "Créer une checklist dans la toolbox associée" : "Associez une toolbox pour créer la checklist"}
|
||||
aria-label={linkedToolboxId ? `Créer une checklist ${list.title}` : "Associez une toolbox pour créer la checklist"}
|
||||
>
|
||||
<span className="module-icon" aria-hidden="true">
|
||||
<span className="module-icon-svg module-icon-checklist" />
|
||||
</span>
|
||||
</button>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
<section className="game-list-panel nebula-panel">
|
||||
<header>
|
||||
<div>
|
||||
<h2>{selectedList.title}</h2>
|
||||
</div>
|
||||
<div className="game-list-actions">
|
||||
<CopyChecklistItemsButton sections={selectedSections} title={`Copier ${selectedList.title.toLowerCase()} pour checklist`} />
|
||||
</div>
|
||||
</header>
|
||||
<div className="game-list-categories">
|
||||
{selectedList.categories.map((category) => (
|
||||
<article className="game-list-category" key={category.id}>
|
||||
<header>
|
||||
<h3>{category.title}</h3>
|
||||
<div className="game-list-actions">
|
||||
<span className="results-count">{category.items.length}</span>
|
||||
<CopyChecklistItemsButton sections={[getChecklistSections({ categories: [category] })[0]]} title={`Copier ${category.title.toLowerCase()} pour checklist`} />
|
||||
</div>
|
||||
</header>
|
||||
<ul>
|
||||
{category.items.map((item) => (
|
||||
<li key={`${selectedList.id}-${category.id}-${item.name}-${item.description}`}>
|
||||
<div className="game-list-item-title">
|
||||
<strong>{item.name}</strong>
|
||||
{item.hasQuantity && item.quantity !== 1 ? <em>x{item.quantity}</em> : null}
|
||||
</div>
|
||||
{item.description ? <span>{item.description}</span> : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<section className="empty">
|
||||
<h2>Aucune liste</h2>
|
||||
<p>Aucune liste n’est encore disponible pour ce jeu.</p>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
import { useMemo } from "react";
|
||||
import { CopyChecklistItemsButton } from "../CopyChecklistItemsButton.jsx";
|
||||
import { GameBreadcrumb } from "../GameBreadcrumb.jsx";
|
||||
import { Diablo4AffixCard } from "./Diablo4AffixCard.jsx";
|
||||
import { Diablo4Filters } from "./Diablo4Filters.jsx";
|
||||
import { getCategoryLabel, getFilteredDiablo4Affixes } from "./utils.js";
|
||||
|
||||
export function Diablo4Listing({ diablo4, filters, setFilters }) {
|
||||
export function Diablo4Listing({ game, diablo4, filters, setFilters }) {
|
||||
const activeFilters = filters.diablo4;
|
||||
const options = [...(diablo4.filterOptions.affixes || [])].sort((a, b) => getCategoryLabel(a).localeCompare(getCategoryLabel(b), "fr"));
|
||||
const visible = useMemo(
|
||||
|
|
@ -16,7 +17,7 @@ export function Diablo4Listing({ diablo4, filters, setFilters }) {
|
|||
<>
|
||||
<section className="page-heading game-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Diablo IV</p>
|
||||
<GameBreadcrumb game={game} />
|
||||
<div className="game-title-row">
|
||||
<h1>Affixes</h1>
|
||||
<span className="results-count">{visible.length} / {diablo4.affixes.length}</span>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export function Diablo4Overview({ game }) {
|
|||
</section>
|
||||
<section className="game-home-grid diablo4-home-grid">
|
||||
<a className="feature game-home-card diablo4-home-card" href="#/games/diablo4/affixes">
|
||||
<span className="diablo4-home-mark" aria-hidden="true" />
|
||||
<span className="game-home-card-icon diablo4-home-mark" aria-hidden="true" />
|
||||
<strong>Affixes</strong>
|
||||
<span>Recherche et filtres par catégorie pour retrouver les statistiques utiles.</span>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -34,5 +34,5 @@ export function Diablo4Page({ category, game, diablo4, filters, setFilters }) {
|
|||
|
||||
if (!activeCategory) return <Diablo4Overview game={game} />;
|
||||
|
||||
return <Diablo4Listing diablo4={diablo4} filters={filters} setFilters={setFilters} />;
|
||||
return <Diablo4Listing game={game} diablo4={diablo4} filters={filters} setFilters={setFilters} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export const INITIAL_MHWILDS_STATE = {
|
|||
translations: {},
|
||||
monsters: [],
|
||||
endemic: [],
|
||||
lists: [],
|
||||
filterOptions: { monsters: [], endemic: [] },
|
||||
filterOptionKeys: { monsters: "", endemic: "" }
|
||||
};
|
||||
|
|
@ -20,21 +21,24 @@ export const INITIAL_DIABLO4_STATE = {
|
|||
};
|
||||
|
||||
export async function loadMhwildsData() {
|
||||
const [monstersResponse, endemicResponse, translationsResponse] = await Promise.all([
|
||||
const [monstersResponse, endemicResponse, listIndexResponse, translationsResponse] = await Promise.all([
|
||||
fetch("/data/mhwilds/monsters.json"),
|
||||
fetch("/data/mhwilds/endemic_life.json"),
|
||||
fetch("/data/mhwilds/lists/index.json"),
|
||||
fetch("/data/mhwilds/i18n/fr.json")
|
||||
]);
|
||||
|
||||
if (!monstersResponse.ok || !endemicResponse.ok || !translationsResponse.ok) {
|
||||
if (!monstersResponse.ok || !endemicResponse.ok || !listIndexResponse.ok || !translationsResponse.ok) {
|
||||
throw new Error("Impossible de charger les données Monster Hunter Wilds.");
|
||||
}
|
||||
|
||||
const [monstersJson, endemicJson, translations] = await Promise.all([
|
||||
const [monstersJson, endemicJson, listIndexJson, translations] = await Promise.all([
|
||||
monstersResponse.json(),
|
||||
endemicResponse.json(),
|
||||
listIndexResponse.json(),
|
||||
translationsResponse.json()
|
||||
]);
|
||||
const lists = await loadGameLists("mhwilds", listIndexJson);
|
||||
const monsterFilterKey = Object.keys(monstersJson).find((key) => key !== "monsters") || "";
|
||||
const endemicFilterKey = Object.keys(endemicJson).find((key) => key !== "endemicLife" && key !== "aquaticLife") || "";
|
||||
|
||||
|
|
@ -45,6 +49,7 @@ export async function loadMhwildsData() {
|
|||
translations,
|
||||
monsters: monstersJson.monsters || [],
|
||||
endemic: [...(endemicJson.endemicLife || []), ...(endemicJson.aquaticLife || [])],
|
||||
lists,
|
||||
filterOptions: {
|
||||
monsters: monstersJson[monsterFilterKey] || [],
|
||||
endemic: endemicJson[endemicFilterKey] || []
|
||||
|
|
@ -56,6 +61,51 @@ export async function loadMhwildsData() {
|
|||
};
|
||||
}
|
||||
|
||||
async function loadGameLists(gameId, indexPayload) {
|
||||
const entries = Array.isArray(indexPayload?.lists) ? indexPayload.lists : [];
|
||||
const results = await Promise.all(entries.map(async (entry) => {
|
||||
const response = await fetch(`/data/${gameId}/lists/${entry.file}`);
|
||||
if (!response.ok) throw new Error(`Impossible de charger la liste ${entry.file}.`);
|
||||
const payload = await response.json();
|
||||
return normalizeGameListFile(entry, payload);
|
||||
}));
|
||||
return results;
|
||||
}
|
||||
|
||||
function normalizeGameListFile(entry, payload) {
|
||||
const id = String(entry?.id || entry?.file || "").replace(/\.json$/i, "");
|
||||
const categories = Object.entries(payload || {}).filter(([, items]) => Array.isArray(items)).map(([categoryId, items]) => ({
|
||||
id: categoryId,
|
||||
title: formatListTitle(categoryId),
|
||||
items: Array.isArray(items) ? items.map(normalizeGameListItem).filter((item) => item.name) : []
|
||||
})).filter((category) => category.items.length);
|
||||
|
||||
return {
|
||||
id,
|
||||
title: payload?.titre || entry?.title || formatListTitle(id),
|
||||
file: entry?.file || `${id}.json`,
|
||||
categories,
|
||||
itemCount: categories.reduce((count, category) => count + category.items.length, 0)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeGameListItem(item) {
|
||||
const hasQuantity = item?.quantite !== undefined || item?.quantity !== undefined;
|
||||
const quantity = Number.parseInt(item?.quantite ?? item?.quantity ?? 1, 10);
|
||||
return {
|
||||
name: String(item?.nom || item?.name || item?.label || "").trim(),
|
||||
description: String(item?.description || item?.quete || item?.quest || item?.source || "").trim(),
|
||||
quantity: Number.isFinite(quantity) ? quantity : 1,
|
||||
hasQuantity
|
||||
};
|
||||
}
|
||||
|
||||
function formatListTitle(value) {
|
||||
return String(value || "")
|
||||
.replace(/_/g, " ")
|
||||
.replace(/\b\p{L}/gu, (letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
export async function loadDiablo4Data() {
|
||||
const response = await fetch("/data/diablo4/affixes_types.json");
|
||||
if (!response.ok) throw new Error("Impossible de charger les données Diablo IV.");
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { useMemo } from "react";
|
||||
import { GameBreadcrumb } from "../GameBreadcrumb.jsx";
|
||||
import { EndemicCard } from "./cards/EndemicCard.jsx";
|
||||
import { MonsterCard } from "./cards/MonsterCard.jsx";
|
||||
import { CopyChecklistItemsButton } from "../CopyChecklistItemsButton.jsx";
|
||||
import { MhwildsFilters } from "./MhwildsFilters.jsx";
|
||||
import { getFilteredMhwildsItems, getMhwildsFilterKey, getUniqueConditionValues } from "./utils.js";
|
||||
|
||||
export function MhwildsListing({ category, mhwilds, filters, setFilters, t }) {
|
||||
export function MhwildsListing({ category, game, mhwilds, filters, setFilters, t }) {
|
||||
const isMonsters = category === "monsters";
|
||||
const filterKey = getMhwildsFilterKey(category, mhwilds);
|
||||
const items = isMonsters ? mhwilds.monsters : mhwilds.endemic;
|
||||
|
|
@ -19,7 +20,7 @@ export function MhwildsListing({ category, mhwilds, filters, setFilters, t }) {
|
|||
<>
|
||||
<section className="page-heading game-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Monster Hunter Wilds</p>
|
||||
<GameBreadcrumb game={game} />
|
||||
<div className="game-title-row">
|
||||
<h1>{label}</h1>
|
||||
<span className="results-count">{visible.length} / {items.length}</span>
|
||||
|
|
|
|||
22
website/src/features/games/mhwilds/MhwildsLists.jsx
Normal file
22
website/src/features/games/mhwilds/MhwildsLists.jsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { useMemo, useState } from "react";
|
||||
import { GameListsPage } from "../GameListsPage.jsx";
|
||||
|
||||
export function MhwildsLists({ game, mhwilds, linkedToolboxId, actions }) {
|
||||
const lists = mhwilds.lists || [];
|
||||
const [selectedListId, setSelectedListId] = useState("");
|
||||
const selectedList = useMemo(
|
||||
() => lists.find((list) => list.id === selectedListId) || lists[0] || null,
|
||||
[lists, selectedListId]
|
||||
);
|
||||
|
||||
return (
|
||||
<GameListsPage
|
||||
game={game}
|
||||
lists={lists}
|
||||
selectedListId={selectedList?.id || ""}
|
||||
onSelectList={setSelectedListId}
|
||||
linkedToolboxId={linkedToolboxId}
|
||||
onCreateChecklist={(list) => actions.createChecklistFromList(game.id, list)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -23,6 +23,13 @@ export function MhwildsOverview({ game }) {
|
|||
<strong>Faune endémique</strong>
|
||||
<span>Faune endémique et aquatique filtrable par localisation.</span>
|
||||
</a>
|
||||
<a className="feature game-home-card" href="#/games/mhwilds/lists">
|
||||
<span className="game-home-card-icon" aria-hidden="true">
|
||||
<span className="module-icon-svg module-icon-checklist" />
|
||||
</span>
|
||||
<strong>Listes</strong>
|
||||
<span>Données copiables au format checklist pour préparer vos suivis.</span>
|
||||
</a>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { MhwildsListing } from "./MhwildsListing.jsx";
|
||||
import { MhwildsLists } from "./MhwildsLists.jsx";
|
||||
import { MhwildsOverview } from "./MhwildsOverview.jsx";
|
||||
|
||||
export function MhwildsPage({ category, game, mhwilds, filters, setFilters, t }) {
|
||||
const activeCategory = category === "monsters" || category === "endemic" ? category : "";
|
||||
export function MhwildsPage({ category, game, mhwilds, filters, setFilters, t, links, actions }) {
|
||||
const activeCategory = category === "monsters" || category === "endemic" || category === "lists" ? category : "";
|
||||
const heroStyle = game?.images?.heroBg ? { "--game-hero-image": `url("${game.images.heroBg}")` } : undefined;
|
||||
|
||||
if (!mhwilds.loaded) {
|
||||
|
|
@ -33,10 +34,12 @@ export function MhwildsPage({ category, game, mhwilds, filters, setFilters, t })
|
|||
}
|
||||
|
||||
if (!activeCategory) return <MhwildsOverview game={game} />;
|
||||
if (activeCategory === "lists") return <MhwildsLists game={game} mhwilds={mhwilds} linkedToolboxId={links?.[game.id] || ""} actions={actions} />;
|
||||
|
||||
return (
|
||||
<MhwildsListing
|
||||
category={activeCategory}
|
||||
game={game}
|
||||
mhwilds={mhwilds}
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ export function CalculatorModule({ toolboxId, moduleId, context }) {
|
|||
.join("\n");
|
||||
if (!text || !await context.copyText(text)) return;
|
||||
setCopied(true);
|
||||
context.notify?.(textContent.copiedTitle || "Copié");
|
||||
window.setTimeout(() => setCopied(false), 1400);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
|||
save(nextSections);
|
||||
setTextImport("");
|
||||
setImportOpen(false);
|
||||
context.notify?.("Import checklist terminé.");
|
||||
}
|
||||
|
||||
function setHideCompletedSections(hideCompletedSections) {
|
||||
|
|
|
|||
|
|
@ -47,12 +47,14 @@ export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
|||
save([...data.links, ...imported]);
|
||||
setTextImport("");
|
||||
setImportOpen(false);
|
||||
context.notify?.("Import de liens terminé.");
|
||||
}
|
||||
|
||||
async function copyUrl(link) {
|
||||
const copied = await context.copyText(link.url);
|
||||
if (!copied) return;
|
||||
setCopiedId(link.id);
|
||||
context.notify?.(textContent.copiedTitle || "Copié");
|
||||
window.setTimeout(() => setCopiedId(""), 1400);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,6 +98,15 @@ const DEFAULT_SITE_CONTENT = {
|
|||
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." },
|
||||
|
|
@ -652,11 +661,12 @@ function mergeContent(defaults, overrides) {
|
|||
|
||||
function currentRoute() {
|
||||
const hashRoute = location.hash.replace(/^#/, "");
|
||||
if (hashRoute) return hashRoute;
|
||||
if (hashRoute) return hashRoute.replace(/#.+$/, "");
|
||||
const path = location.pathname.replace(/\/+$/, "") || "/";
|
||||
if (path === "/mhwilds") return "/games/mhwilds";
|
||||
if (path === "/mhwilds/monsters") return "/games/mhwilds/monsters";
|
||||
if (path === "/mhwilds/endemic") return "/games/mhwilds/endemic";
|
||||
if (path === "/mhwilds/lists") return "/games/mhwilds/lists";
|
||||
return path;
|
||||
}
|
||||
|
||||
|
|
@ -850,9 +860,29 @@ function App() {
|
|||
const [linkModalGameId, setLinkModalGameId] = useState("");
|
||||
const [drawerGameId, setDrawerGameId] = useState("");
|
||||
const [screenshot, setScreenshot] = useState(null);
|
||||
const [notification, setNotification] = useState(null);
|
||||
const [storageError, setStorageError] = useState("");
|
||||
const store = useIndexedToolboxes((message) => setStorageError(message));
|
||||
|
||||
function notify(message) {
|
||||
setNotification({ id: Date.now(), message });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function handleNotification(event) {
|
||||
if (event.detail?.message) notify(event.detail.message);
|
||||
}
|
||||
|
||||
window.addEventListener("sokkog:notify", handleNotification);
|
||||
return () => window.removeEventListener("sokkog:notify", handleNotification);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!notification) return undefined;
|
||||
const timeoutId = window.setTimeout(() => setNotification(null), 1800);
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [notification]);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
fetch("/data/site.json").then((response) => response.ok ? response.json() : DEFAULT_SITE_CONTENT).catch(() => DEFAULT_SITE_CONTENT),
|
||||
|
|
@ -945,6 +975,41 @@ function App() {
|
|||
return store.updateModuleData(toolboxId, moduleId, data);
|
||||
}
|
||||
|
||||
function createChecklistFromList(gameId, list) {
|
||||
const toolboxId = store.links[gameId] || "";
|
||||
const toolbox = store.toolboxes.find((item) => item.id === toolboxId);
|
||||
if (!toolbox || !list) return false;
|
||||
|
||||
const moduleId = uid("mod");
|
||||
const sections = (list.categories || []).map((category) => ({
|
||||
id: uid("section"),
|
||||
title: category.title || "",
|
||||
items: (category.items || []).map((item) => ({
|
||||
id: uid("item"),
|
||||
label: item.name,
|
||||
qtyTarget: Math.max(1, Number.parseInt(item.quantity, 10) || 1),
|
||||
qtyCurrent: 0
|
||||
})).filter((item) => item.label)
|
||||
})).filter((section) => section.items.length);
|
||||
|
||||
if (!sections.length) return false;
|
||||
|
||||
setConfirmModal({
|
||||
title: "Créer une checklist",
|
||||
message: `Créer la checklist "${list.title || "Checklist"}" dans la toolbox "${toolbox.name}" ?`,
|
||||
confirmLabel: "Créer",
|
||||
onResolve: (confirmed) => {
|
||||
if (!confirmed) return;
|
||||
store.setToolboxes(store.toolboxes.map((item) => item.id === toolboxId
|
||||
? { ...item, modules: [...item.modules, { id: moduleId, type: "checklist", title: list.title || "Checklist" }], updatedAt: new Date().toISOString() }
|
||||
: item));
|
||||
store.updateModuleData(toolboxId, moduleId, { sections }, "checklist");
|
||||
notify("Checklist créée dans la toolbox associée.");
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
async function importToolbox(file, gameId = "") {
|
||||
const payload = JSON.parse(await file.text());
|
||||
if (!payload.toolbox?.modules) throw new Error("Format d'outils invalide");
|
||||
|
|
@ -1011,7 +1076,9 @@ function App() {
|
|||
setLinkModalGameId,
|
||||
setDrawerGameId,
|
||||
setScreenshot,
|
||||
createChecklistFromList,
|
||||
addScreenshotFiles,
|
||||
notify,
|
||||
removeModuleData: store.removeModuleData,
|
||||
updateModuleData: store.updateModuleData,
|
||||
updateToolboxOrder: (orderedIds) => {
|
||||
|
|
@ -1020,7 +1087,9 @@ function App() {
|
|||
},
|
||||
importToolbox: async (file, gameId = "") => {
|
||||
try {
|
||||
return await importToolbox(file, gameId);
|
||||
const toolbox = await importToolbox(file, gameId);
|
||||
if (toolbox) notify("Toolbox importée.");
|
||||
return toolbox;
|
||||
} catch (error) {
|
||||
setConfirmModal({
|
||||
title: "Import impossible",
|
||||
|
|
@ -1035,6 +1104,7 @@ function App() {
|
|||
importAllToolboxes: async (file) => {
|
||||
try {
|
||||
await importAllToolboxes(file);
|
||||
notify("Import global terminé.");
|
||||
return true;
|
||||
} catch (error) {
|
||||
setConfirmModal({
|
||||
|
|
@ -1051,8 +1121,12 @@ function App() {
|
|||
const toolbox = normalizeToolbox(store.toolboxes.find((item) => item.id === id));
|
||||
if (!toolbox) return;
|
||||
downloadJson(createToolboxExportPayload(toolbox, store.moduleData), `${toolbox.name.toLowerCase().replace(/[^a-z0-9]+/gi, "-")}.sokkog.json`);
|
||||
notify("Export de la toolbox lancé.");
|
||||
},
|
||||
exportAllToolboxes: () => downloadJson(createGlobalExportPayload(store.toolboxes, store.links, store.moduleData), `sokkog-global-${new Date().toISOString().slice(0, 10)}.json`)
|
||||
exportAllToolboxes: () => {
|
||||
downloadJson(createGlobalExportPayload(store.toolboxes, store.links, store.moduleData), `sokkog-global-${new Date().toISOString().slice(0, 10)}.json`);
|
||||
notify("Export global lancé.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -1115,6 +1189,7 @@ function App() {
|
|||
}} />}
|
||||
{storageError && <ConfirmModal title="Quota local atteint" message={storageError} confirmLabel="Compris" cancelLabel="Fermer" danger onClose={() => setStorageError("")} />}
|
||||
{screenshot && <ScreenshotViewer shot={screenshot} onClose={() => setScreenshot(null)} />}
|
||||
{notification && <NotificationToast key={notification.id} message={notification.message} />}
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
|
@ -1229,13 +1304,17 @@ function AboutPage({ siteContent }) {
|
|||
const limits = Array.isArray(content.limits) ? content.limits : [];
|
||||
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;
|
||||
useEffect(() => {
|
||||
if (!location.hash.includes("#contribuer")) return;
|
||||
requestAnimationFrame(() => document.querySelector("#contribuer")?.scrollIntoView({ behavior: "smooth", block: "start" }));
|
||||
}, []);
|
||||
return (
|
||||
<div className="about-page">
|
||||
<section className="page-hero">
|
||||
<div>
|
||||
<p className="eyebrow">{content.eyebrow}</p>
|
||||
<h1>{content.title}</h1>
|
||||
<p>{content.description}</p>
|
||||
<p><RichText text={content.description} /></p>
|
||||
</div>
|
||||
</section>
|
||||
<section className="about-story-section nebula-panel" aria-labelledby="about-story-title">
|
||||
|
|
@ -1249,7 +1328,7 @@ function AboutPage({ siteContent }) {
|
|||
<span>{String(index + 1).padStart(2, "0")}</span>
|
||||
<div>
|
||||
<h3>{section.title}</h3>
|
||||
<p>{section.text}</p>
|
||||
<p><RichText text={section.text} /></p>
|
||||
{index === storageSectionIndex && limits.length > 0 && (
|
||||
<div className="about-storage-reminders">
|
||||
<p className="eyebrow">{content.limitsTitle}</p>
|
||||
|
|
@ -1263,7 +1342,7 @@ function AboutPage({ siteContent }) {
|
|||
</span>
|
||||
<div>
|
||||
<h4>{reminder.title}</h4>
|
||||
<p>{reminder.text}</p>
|
||||
<p><RichText text={reminder.text} /></p>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
|
|
@ -1291,12 +1370,29 @@ function AboutPage({ siteContent }) {
|
|||
</span>
|
||||
<div>
|
||||
<h3>{tool.name}</h3>
|
||||
<p>{tool.description}</p>
|
||||
<p><RichText text={tool.description} /></p>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1544,6 +1640,7 @@ function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, sto
|
|||
normalizeUrl,
|
||||
hostnameFromUrl,
|
||||
copyText,
|
||||
notify: actions.notify,
|
||||
compressImageFile: compressImage,
|
||||
clampQty,
|
||||
uid,
|
||||
|
|
@ -1754,6 +1851,14 @@ function ConfirmModal({ title, message, confirmLabel = "Confirmer", cancelLabel
|
|||
);
|
||||
}
|
||||
|
||||
function NotificationToast({ message }) {
|
||||
return (
|
||||
<div className="notification-toast" role="status" aria-live="polite">
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateToolboxModal({ gameId, initialName = "", onClose }) {
|
||||
useModalScrollLock();
|
||||
const [name, setName] = useState(initialName);
|
||||
|
|
|
|||
|
|
@ -15,25 +15,14 @@
|
|||
}
|
||||
|
||||
.diablo4-home-mark {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
margin: 0;
|
||||
place-items: center;
|
||||
border-bottom: 1px solid rgba(246, 196, 83, 0.42);
|
||||
background: rgba(5, 7, 17, 0.28);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.game-home-card .diablo4-home-mark {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.diablo4-home-mark::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
width: 82px;
|
||||
height: 82px;
|
||||
background: currentColor;
|
||||
filter: drop-shadow(0 16px 24px rgba(0, 0, 0, 0.44));
|
||||
mask: url("/static/img/diablo4/cube.svg") center / contain no-repeat;
|
||||
|
|
|
|||
|
|
@ -48,16 +48,32 @@
|
|||
background: rgba(5, 7, 17, 0.28);
|
||||
}
|
||||
|
||||
.game-home-card strong,
|
||||
.game-home-card span {
|
||||
.game-home-card-icon {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
place-items: center;
|
||||
border-bottom: 1px solid rgba(246, 196, 83, 0.42);
|
||||
background: rgba(5, 7, 17, 0.28);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.game-home-card-icon .module-icon-svg {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
filter: drop-shadow(0 12px 22px rgba(0, 0, 0, 0.36));
|
||||
}
|
||||
|
||||
.game-home-card > strong,
|
||||
.game-home-card > span:not(.game-home-card-icon) {
|
||||
margin-inline: var(--space-5);
|
||||
}
|
||||
|
||||
.game-home-card strong {
|
||||
.game-home-card > strong {
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.game-home-card span {
|
||||
.game-home-card > span:not(.game-home-card-icon) {
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
|
|
@ -76,6 +92,39 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.game-breadcrumb {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--color-accent-gold);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.game-breadcrumb a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: inherit;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.game-breadcrumb .ui-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: var(--color-accent-gold);
|
||||
filter:
|
||||
drop-shadow(0 0 0 currentColor)
|
||||
drop-shadow(0 0 4px rgba(246, 196, 83, 0.22));
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.game-breadcrumb a:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.results-count {
|
||||
display: inline-flex;
|
||||
min-height: 28px;
|
||||
|
|
@ -95,6 +144,215 @@
|
|||
min-height: 32px;
|
||||
}
|
||||
|
||||
.game-list-help-link {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
min-height: 36px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: var(--space-3);
|
||||
border-color: rgba(165, 180, 252, 0.18);
|
||||
background: rgba(5, 7, 17, 0.24);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 800;
|
||||
padding: 7px 12px;
|
||||
}
|
||||
|
||||
.game-list-help-link:hover {
|
||||
border-color: rgba(246, 196, 83, 0.44);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.1), rgba(139, 92, 246, 0.08)) padding-box,
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.42), rgba(196, 181, 253, 0.22)) border-box;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.game-list-selector {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 300px));
|
||||
justify-content: center;
|
||||
gap: var(--space-4);
|
||||
margin-top: var(--space-6);
|
||||
}
|
||||
|
||||
.game-list-selector-item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
min-height: 68px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.14);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(16, 20, 38, 0.58);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.game-list-selector-item:hover,
|
||||
.game-list-selector-item.active {
|
||||
border-color: rgba(246, 196, 83, 0.42);
|
||||
background: rgba(31, 37, 68, 0.72);
|
||||
box-shadow:
|
||||
var(--shadow-sm),
|
||||
0 0 14px rgba(246, 196, 83, 0.1);
|
||||
}
|
||||
|
||||
.game-list-selector-item > button:not(.game-list-create-button) {
|
||||
display: grid;
|
||||
min-height: 48px;
|
||||
align-content: center;
|
||||
gap: 3px;
|
||||
padding: 0 8px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.game-list-selector-item strong {
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.game-list-selector-item small {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.game-list-selector-item .game-list-create-button {
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.game-list-panel {
|
||||
margin-top: var(--space-5);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.game-list-panel > header,
|
||||
.game-list-category header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
border-bottom: 1px solid rgba(246, 196, 83, 0.32);
|
||||
}
|
||||
|
||||
.game-list-category header {
|
||||
padding: 0 0 var(--space-3);
|
||||
}
|
||||
|
||||
.game-list-panel h2 {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
.game-list-categories {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.game-list-category {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.game-list-category:not(:last-child) {
|
||||
padding-bottom: var(--space-4);
|
||||
border-bottom: 1px solid rgba(165, 180, 252, 0.1);
|
||||
}
|
||||
|
||||
.game-list-category h3 {
|
||||
margin: 0;
|
||||
color: var(--color-accent-gold);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.game-list-category ul {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.game-list-category li {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
min-height: 58px;
|
||||
align-content: start;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.07);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(5, 7, 17, 0.14);
|
||||
}
|
||||
|
||||
.game-list-item-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.game-list-category li strong {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.game-list-category li span {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.game-list-category li em {
|
||||
display: inline-flex;
|
||||
min-height: 20px;
|
||||
align-items: center;
|
||||
padding: 2px 7px;
|
||||
border: 1px solid rgba(246, 196, 83, 0.28);
|
||||
border-radius: var(--radius-pill);
|
||||
background: rgba(246, 196, 83, 0.08);
|
||||
color: var(--color-accent-gold);
|
||||
font-size: var(--font-size-xs);
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.game-list-actions {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.game-list-create-button {
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
padding: 0;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.game-list-create-button:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.game-list-create-button .module-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.game-list-create-button .module-icon-svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.game-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
|
||||
|
|
|
|||
|
|
@ -319,7 +319,7 @@ span {
|
|||
}
|
||||
|
||||
.about-story-item > div > p {
|
||||
max-width: 78ch;
|
||||
max-width: 108ch;
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.62;
|
||||
|
|
@ -331,6 +331,70 @@ span {
|
|||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.about-contribute-section {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(210px, 260px) minmax(0, 1fr);
|
||||
gap: var(--space-6);
|
||||
scroll-margin-top: var(--space-6);
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.about-contribute-content {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.about-contribute-content > p {
|
||||
max-width: 112ch;
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.62;
|
||||
}
|
||||
|
||||
.about-contribute-content pre,
|
||||
.about-prompt-box {
|
||||
margin: 0;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(5, 7, 17, 0.28);
|
||||
}
|
||||
|
||||
.about-contribute-content pre {
|
||||
overflow: auto;
|
||||
padding: var(--space-4);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.about-contribute-content code {
|
||||
color: var(--color-text-primary);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.55;
|
||||
white-space: inherit;
|
||||
}
|
||||
|
||||
.about-prompt-box {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.about-prompt-box h3 {
|
||||
margin: 0 0 8px;
|
||||
color: var(--color-accent-gold);
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.about-prompt-box p {
|
||||
margin: 0 0 var(--space-3);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.58;
|
||||
}
|
||||
|
||||
.about-prompt-box pre {
|
||||
border-color: rgba(246, 196, 83, 0.16);
|
||||
background: rgba(5, 7, 17, 0.34);
|
||||
}
|
||||
|
||||
.about-tools-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@
|
|||
-webkit-mask-image: url("/static/icons/open.svg");
|
||||
}
|
||||
|
||||
.ui-icon-home {
|
||||
mask-image: url("/static/icons/home.svg");
|
||||
-webkit-mask-image: url("/static/icons/home.svg");
|
||||
}
|
||||
|
||||
.ui-icon-enter {
|
||||
mask-image: url("/static/icons/enter.svg");
|
||||
-webkit-mask-image: url("/static/icons/enter.svg");
|
||||
|
|
|
|||
|
|
@ -306,6 +306,27 @@ body.is-resizing-drawer * {
|
|||
gap: 10px;
|
||||
}
|
||||
|
||||
.notification-toast {
|
||||
position: fixed;
|
||||
right: var(--space-5);
|
||||
bottom: var(--space-5);
|
||||
z-index: 360;
|
||||
max-width: min(360px, calc(100vw - 32px));
|
||||
padding: 11px 14px;
|
||||
border: 1px solid rgba(246, 196, 83, 0.34);
|
||||
border-radius: var(--radius-md);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(16, 20, 38, 0.94), rgba(24, 20, 42, 0.94)) padding-box,
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.48), rgba(139, 92, 246, 0.28)) border-box;
|
||||
color: var(--color-text-primary);
|
||||
box-shadow:
|
||||
var(--shadow-sm),
|
||||
0 0 18px rgba(139, 92, 246, 0.16);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 800;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tool-add-modal {
|
||||
width: min(560px, calc(100vw - 32px));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.about-story-section {
|
||||
.about-story-section,
|
||||
.about-contribute-section {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +75,7 @@
|
|||
.page-hero,
|
||||
.origin-section,
|
||||
.about-story-section,
|
||||
.about-contribute-section,
|
||||
.about-tools-list,
|
||||
.section-grid,
|
||||
.info-grid {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue