style / tools optis & new tools calculator
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s
This commit is contained in:
parent
6b84607b25
commit
0db35a0d2c
23 changed files with 1106 additions and 24 deletions
43
website/src/features/games/CopyChecklistItemsButton.jsx
Normal file
43
website/src/features/games/CopyChecklistItemsButton.jsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { useState } from "react";
|
||||
import { Icon } from "../../components/Icon.jsx";
|
||||
|
||||
async function copyText(value) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function formatChecklistImportItems(labels) {
|
||||
return labels
|
||||
.map((label) => String(label || "").trim())
|
||||
.filter(Boolean)
|
||||
.map((label) => `${label}:1`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function CopyChecklistItemsButton({ labels, title = "Copier pour checklist" }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const text = formatChecklistImportItems(labels);
|
||||
|
||||
async function copyItems() {
|
||||
if (!text || !await copyText(text)) return;
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 1400);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className="filter-reset-button results-copy-button"
|
||||
type="button"
|
||||
onClick={copyItems}
|
||||
disabled={!text}
|
||||
aria-label={title}
|
||||
title={copied ? "Copié" : title}
|
||||
>
|
||||
<Icon name="copy" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { useMemo } from "react";
|
||||
import { CopyChecklistItemsButton } from "../CopyChecklistItemsButton.jsx";
|
||||
import { Diablo4AffixCard } from "./Diablo4AffixCard.jsx";
|
||||
import { Diablo4Filters } from "./Diablo4Filters.jsx";
|
||||
import { getCategoryLabel, getFilteredDiablo4Affixes } from "./utils.js";
|
||||
|
|
@ -19,6 +20,7 @@ export function Diablo4Listing({ diablo4, filters, setFilters }) {
|
|||
<div className="game-title-row">
|
||||
<h1>Affixes</h1>
|
||||
<span className="results-count">{visible.length} / {diablo4.affixes.length}</span>
|
||||
<CopyChecklistItemsButton labels={visible.map((affix) => affix.label)} title="Copier les affixes visibles pour checklist" />
|
||||
</div>
|
||||
<p>Filtrez les affixes par nom et catégories pour retrouver rapidement les statistiques utiles à votre build.</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useMemo } from "react";
|
||||
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";
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ export function MhwildsListing({ category, mhwilds, filters, setFilters, t }) {
|
|||
<div className="game-title-row">
|
||||
<h1>{label}</h1>
|
||||
<span className="results-count">{visible.length} / {items.length}</span>
|
||||
<CopyChecklistItemsButton labels={visible.map((item) => t(item.name, { capitalize: true }))} title={`Copier les ${label.toLowerCase()} visibles pour checklist`} />
|
||||
</div>
|
||||
<p>{isMonsters ? "Filtrez les monstres par nom et faiblesses, puis consultez leurs dégâts détaillés." : "Filtrez la faune par nom et zones d'apparition."}</p>
|
||||
</div>
|
||||
|
|
|
|||
243
website/src/features/toolboxes/modules/CalculatorModule.jsx
Normal file
243
website/src/features/toolboxes/modules/CalculatorModule.jsx
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
|
||||
function calculateExpression(expression) {
|
||||
const normalized = String(expression || "").replaceAll(",", ".").trim();
|
||||
if (!normalized || !/^[\d\s+\-*/().]+$/.test(normalized)) return null;
|
||||
|
||||
try {
|
||||
const value = Function(`"use strict"; return (${normalized})`)();
|
||||
return Number.isFinite(value) ? value : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatResult(value) {
|
||||
if (!Number.isFinite(value)) return "";
|
||||
return Number.parseFloat(value.toFixed(6)).toString();
|
||||
}
|
||||
|
||||
function getChildren(entries, parentId) {
|
||||
return entries.filter((entry) => (entry.parentId || "") === parentId);
|
||||
}
|
||||
|
||||
function getDescendantIds(entries, parentId) {
|
||||
const children = getChildren(entries, parentId);
|
||||
return children.flatMap((child) => [child.id, ...getDescendantIds(entries, child.id)]);
|
||||
}
|
||||
|
||||
function getEntriesInTreeOrder(entries, parentId = "") {
|
||||
return getChildren(entries, parentId).flatMap((entry) => [entry, ...getEntriesInTreeOrder(entries, entry.id)]);
|
||||
}
|
||||
|
||||
export function CalculatorModule({ toolboxId, moduleId, context }) {
|
||||
const data = context.normalizeCalculatorData(context.getModuleData(toolboxId, moduleId, { entries: [] }));
|
||||
const textContent = context.moduleText?.calculator || {};
|
||||
const [expression, setExpression] = useState("");
|
||||
const [label, setLabel] = useState("");
|
||||
const [activeParentId, setActiveParentId] = useState("");
|
||||
const [calculatorHeight, setCalculatorHeight] = useState(0);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const calculatorCardRef = useRef(null);
|
||||
const result = useMemo(() => calculateExpression(expression), [expression]);
|
||||
const activeParent = data.entries.find((entry) => entry.id === activeParentId);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const element = calculatorCardRef.current;
|
||||
if (!element) return undefined;
|
||||
|
||||
function updateHeight() {
|
||||
setCalculatorHeight(Math.ceil(element.getBoundingClientRect().height));
|
||||
}
|
||||
|
||||
updateHeight();
|
||||
const observer = new ResizeObserver(updateHeight);
|
||||
observer.observe(element);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
function save(entries) {
|
||||
context.setModuleData(toolboxId, moduleId, { ...data, entries });
|
||||
}
|
||||
|
||||
function setScrollResults(scrollResults) {
|
||||
context.setModuleData(toolboxId, moduleId, { ...data, scrollResults });
|
||||
}
|
||||
|
||||
function saveResult(event) {
|
||||
event.preventDefault();
|
||||
if (result == null) return;
|
||||
const cleanLabel = label.trim();
|
||||
|
||||
save([
|
||||
...data.entries,
|
||||
{
|
||||
id: context.uid("calc"),
|
||||
parentId: activeParentId,
|
||||
label: cleanLabel,
|
||||
value: result
|
||||
}
|
||||
]);
|
||||
setExpression("");
|
||||
setLabel("");
|
||||
}
|
||||
|
||||
function useEntry(entry) {
|
||||
setExpression(formatResult(entry.value));
|
||||
setActiveParentId(entry.id);
|
||||
}
|
||||
|
||||
function deleteEntry(entryId) {
|
||||
const deletedIds = new Set([entryId, ...getDescendantIds(data.entries, entryId)]);
|
||||
if (deletedIds.has(activeParentId)) setActiveParentId("");
|
||||
save(data.entries.filter((entry) => !deletedIds.has(entry.id)));
|
||||
}
|
||||
|
||||
function renameEntry(entryId, label) {
|
||||
const cleanLabel = label.trim();
|
||||
save(data.entries.map((entry) => entry.id === entryId ? { ...entry, label: cleanLabel } : entry));
|
||||
}
|
||||
|
||||
function resetCalculator() {
|
||||
setExpression("");
|
||||
setLabel("");
|
||||
setActiveParentId("");
|
||||
}
|
||||
|
||||
async function copyChecklistImport() {
|
||||
const text = getEntriesInTreeOrder(data.entries)
|
||||
.map((entry) => `${entry.label || formatResult(entry.value)}:${formatResult(entry.value)}`)
|
||||
.join("\n");
|
||||
if (!text || !await context.copyText(text)) return;
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 1400);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="calculator-module">
|
||||
<form className="calculator-form" onSubmit={saveResult}>
|
||||
<div className="calculator-card" ref={calculatorCardRef}>
|
||||
<label>
|
||||
<span>Calcul</span>
|
||||
<input value={expression} onChange={(event) => setExpression(event.target.value)} placeholder={textContent.expressionPlaceholder || "10*10"} inputMode="decimal" />
|
||||
</label>
|
||||
<div className="calculator-result" aria-live="polite">
|
||||
<span>Résultat</span>
|
||||
<strong>{result == null ? "-" : formatResult(result)}</strong>
|
||||
</div>
|
||||
<label>
|
||||
<span>Libellé</span>
|
||||
<input value={label} onChange={(event) => setLabel(event.target.value)} placeholder={textContent.labelPlaceholder || "Lingots de fer"} />
|
||||
</label>
|
||||
<button className="primary" disabled={result == null}>Enregistrer</button>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
className={`calculator-saved ${data.scrollResults ? "is-scrollable" : ""}`}
|
||||
style={data.scrollResults && calculatorHeight ? { "--calculator-scroll-height": `${calculatorHeight}px` } : undefined}
|
||||
>
|
||||
<div className="calculator-result-actions">
|
||||
{activeParent && (
|
||||
<button className="calculator-root-button" type="button" onClick={() => setActiveParentId("")}>
|
||||
Revenir à la racine
|
||||
</button>
|
||||
)}
|
||||
<button className="calculator-action-button danger" type="button" onClick={resetCalculator} aria-label="Réinitialiser le calculateur" title="Réinitialiser">
|
||||
<Icon name="rubber" />
|
||||
</button>
|
||||
<button className="calculator-action-button" type="button" onClick={copyChecklistImport} disabled={!data.entries.length} aria-label="Copier pour checklist" title={copied ? "Copié" : "Copier pour checklist"}>
|
||||
<Icon name="copy" />
|
||||
</button>
|
||||
<button
|
||||
className={`calculator-scroll-toggle ${data.scrollResults ? "active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => setScrollResults(!data.scrollResults)}
|
||||
aria-pressed={data.scrollResults}
|
||||
title={textContent.scrollableTitle || "Liste scrollable"}
|
||||
aria-label={textContent.scrollableTitle || "Liste scrollable"}
|
||||
>
|
||||
<Icon name="scrollable" />
|
||||
<i aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className={`calculator-tree ${data.scrollResults ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||
{data.entries.length ? (
|
||||
<CalculatorEntries entries={data.entries} parentId="" activeParentId={activeParentId} onUse={useEntry} onRename={renameEntry} onDelete={deleteEntry} />
|
||||
) : (
|
||||
<p className="muted">Aucun résultat enregistré.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CalculatorEntries({ entries, parentId, activeParentId, onUse, onRename, onDelete }) {
|
||||
const children = getChildren(entries, parentId);
|
||||
const [editingId, setEditingId] = useState("");
|
||||
if (!children.length) return null;
|
||||
|
||||
return (
|
||||
<ul className="calculator-entry-list">
|
||||
{children.map((entry) => (
|
||||
<li className={`${entry.id === activeParentId ? "active" : ""} ${entry.id === editingId ? "is-editing" : ""}`} key={entry.id}>
|
||||
<div className="calculator-entry">
|
||||
{entry.id === editingId ? (
|
||||
<>
|
||||
<span className="calculator-entry-value is-readonly" title="Quantité non modifiable">
|
||||
<strong>{formatResult(entry.value)}</strong>
|
||||
</span>
|
||||
<EditableCalculatorLabel entry={entry} onRename={onRename} onDone={() => setEditingId("")} />
|
||||
</>
|
||||
) : (
|
||||
<button className="calculator-entry-summary" type="button" onClick={() => onUse(entry)} title="Utiliser comme base">
|
||||
<span><strong>{formatResult(entry.value)}</strong> {entry.label}</span>
|
||||
</button>
|
||||
)}
|
||||
<button type="button" onClick={() => setEditingId(entry.id === editingId ? "" : entry.id)} aria-label={`Renommer ${entry.label}`} title="Renommer">
|
||||
<Icon name="edit" />
|
||||
</button>
|
||||
<button type="button" className="danger" onClick={() => onDelete(entry.id)} aria-label={`Supprimer ${entry.label}`} title="Supprimer">
|
||||
<Icon name="trash" />
|
||||
</button>
|
||||
</div>
|
||||
<CalculatorEntries entries={entries} parentId={entry.id} activeParentId={activeParentId} onUse={onUse} onRename={onRename} onDelete={onDelete} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function EditableCalculatorLabel({ entry, onRename, onDone }) {
|
||||
const [label, setLabel] = useState(entry.label);
|
||||
const inputRef = useRef(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
}, [entry.id]);
|
||||
|
||||
function saveLabel() {
|
||||
const cleanLabel = label.trim();
|
||||
if (cleanLabel !== entry.label) onRename(entry.id, cleanLabel);
|
||||
onDone();
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
className="calculator-entry-label"
|
||||
value={label}
|
||||
onChange={(event) => setLabel(event.target.value)}
|
||||
onBlur={saveLabel}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") event.currentTarget.blur();
|
||||
if (event.key === "Escape") {
|
||||
setLabel(entry.label);
|
||||
onDone();
|
||||
}
|
||||
}}
|
||||
aria-label={`Renommer ${entry.label}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
import { useState } from "react";
|
||||
import { parseColonImportLines } from "./textImport.js";
|
||||
|
||||
export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
||||
const data = context.normalizeChecklistData(context.getModuleData(toolboxId, moduleId, { items: [] }));
|
||||
const textContent = context.moduleText?.checklist || {};
|
||||
const [label, setLabel] = useState("");
|
||||
const [qty, setQty] = useState(1);
|
||||
const [textImport, setTextImport] = useState("");
|
||||
|
||||
function save(items) {
|
||||
context.setModuleData(toolboxId, moduleId, { items });
|
||||
|
|
@ -18,14 +21,38 @@ export function ChecklistModule({ toolboxId, moduleId, context, editing }) {
|
|||
setQty(1);
|
||||
}
|
||||
|
||||
function importItems(event) {
|
||||
event.preventDefault();
|
||||
const imported = parseColonImportLines(textImport).map((entry) => ({
|
||||
id: context.uid("item"),
|
||||
label: entry.label,
|
||||
qtyTarget: Math.max(1, Number.parseInt(entry.value, 10) || 1),
|
||||
qtyCurrent: 0
|
||||
}));
|
||||
if (!imported.length) return;
|
||||
save([...data.items, ...imported]);
|
||||
setTextImport("");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{editing && (
|
||||
<form className="inline-form checklist-add-form module-add-panel" onSubmit={addItem}>
|
||||
<input name="label" placeholder="Nouvel item" value={label} onChange={(event) => setLabel(event.target.value)} />
|
||||
<input className="checklist-qty-input" name="qty" type="number" min="1" value={qty} onChange={(event) => setQty(event.target.value)} aria-label="Quantité cible" />
|
||||
<button className="primary">Ajouter</button>
|
||||
</form>
|
||||
<div className="module-add-panel">
|
||||
<form className="inline-form checklist-add-form" onSubmit={addItem}>
|
||||
<input name="label" placeholder={textContent.itemPlaceholder || "Nouvel item"} value={label} onChange={(event) => setLabel(event.target.value)} />
|
||||
<input className="checklist-qty-input" name="qty" type="number" min="1" value={qty} onChange={(event) => setQty(event.target.value)} aria-label="Quantité cible" />
|
||||
<button className="primary">Ajouter</button>
|
||||
</form>
|
||||
<form className="text-import-form" onSubmit={importItems}>
|
||||
<textarea
|
||||
value={textImport}
|
||||
onChange={(event) => setTextImport(event.target.value)}
|
||||
placeholder={textContent.importPlaceholder || "Importer plusieurs items...\nPotion:10\nMéga potion:5"}
|
||||
rows={3}
|
||||
/>
|
||||
<button type="submit">Importer le texte</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
<ul className="checklist">
|
||||
{data.items.map((item) => (
|
||||
|
|
@ -56,7 +83,17 @@ function ChecklistItem({ item, context, items, save }) {
|
|||
) : (
|
||||
<div className="checklist-qty-controls" aria-label={`Quantité ${item.label}`}>
|
||||
<button type="button" onClick={() => updateItem((entry) => ({ ...entry, qtyCurrent: context.clampQty(entry.qtyCurrent - 1, entry.qtyTarget) }))} aria-label="Retirer une quantité">-</button>
|
||||
<small>{context.clampQty(item.qtyCurrent, item.qtyTarget)}/{item.qtyTarget}</small>
|
||||
<label className="checklist-qty-current">
|
||||
<input
|
||||
type="number"
|
||||
max={item.qtyTarget}
|
||||
value={context.clampQty(item.qtyCurrent, item.qtyTarget)}
|
||||
style={{ "--qty-current-digits": String(context.clampQty(item.qtyCurrent, item.qtyTarget)).length }}
|
||||
onChange={(event) => updateItem((entry) => ({ ...entry, qtyCurrent: context.clampQty(event.target.value, entry.qtyTarget) }))}
|
||||
aria-label={`Quantité actuelle pour ${item.label}`}
|
||||
/>
|
||||
<span>/ {item.qtyTarget}</span>
|
||||
</label>
|
||||
<button type="button" onClick={() => updateItem((entry) => ({ ...entry, qtyCurrent: context.clampQty(entry.qtyCurrent + 1, entry.qtyTarget) }))} aria-label="Ajouter une quantité">+</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Icon } from "../../../components/Icon.jsx";
|
|||
|
||||
export function CountersModule({ toolboxId, moduleId, context, editing }) {
|
||||
const data = context.normalizeCountersData(context.getModuleData(toolboxId, moduleId, { counters: [] }));
|
||||
const textContent = context.moduleText?.counters || {};
|
||||
const [label, setLabel] = useState("");
|
||||
|
||||
function save(counters) {
|
||||
|
|
@ -26,7 +27,7 @@ export function CountersModule({ toolboxId, moduleId, context, editing }) {
|
|||
<>
|
||||
{editing && (
|
||||
<form className="inline-form counters-add-form module-add-panel" onSubmit={addCounter}>
|
||||
<input name="label" placeholder="Nom du compteur" value={label} onChange={(event) => setLabel(event.target.value)} />
|
||||
<input name="label" placeholder={textContent.labelPlaceholder || "Nom du compteur"} value={label} onChange={(event) => setLabel(event.target.value)} />
|
||||
<button className="primary">Ajouter</button>
|
||||
</form>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
import { useState } from "react";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { parseColonImportLines } from "./textImport.js";
|
||||
|
||||
export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
||||
const data = context.normalizeLinksData(context.getModuleData(toolboxId, moduleId, { links: [] }));
|
||||
const textContent = context.moduleText?.links || {};
|
||||
const [title, setTitle] = useState("");
|
||||
const [url, setUrl] = useState("");
|
||||
const [textImport, setTextImport] = useState("");
|
||||
const [copiedId, setCopiedId] = useState("");
|
||||
|
||||
function save(links) {
|
||||
|
|
@ -28,6 +31,20 @@ export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
|||
setUrl("");
|
||||
}
|
||||
|
||||
function importLinks(event) {
|
||||
event.preventDefault();
|
||||
const imported = parseColonImportLines(textImport)
|
||||
.map((entry) => ({
|
||||
id: context.uid("link"),
|
||||
title: entry.label,
|
||||
url: context.normalizeUrl(entry.value)
|
||||
}))
|
||||
.filter((link) => link.url);
|
||||
if (!imported.length) return;
|
||||
save([...data.links, ...imported]);
|
||||
setTextImport("");
|
||||
}
|
||||
|
||||
async function copyUrl(link) {
|
||||
const copied = await context.copyText(link.url);
|
||||
if (!copied) return;
|
||||
|
|
@ -38,11 +55,22 @@ export function LinksModule({ toolboxId, moduleId, context, editing }) {
|
|||
return (
|
||||
<>
|
||||
{editing && (
|
||||
<form className="inline-form links-add-form module-add-panel" onSubmit={addLink}>
|
||||
<input name="title" placeholder="Nom du lien" value={title} onChange={(event) => setTitle(event.target.value)} />
|
||||
<input name="url" placeholder="https://..." value={url} onChange={(event) => setUrl(event.target.value)} />
|
||||
<button className="primary">Ajouter</button>
|
||||
</form>
|
||||
<div className="module-add-panel">
|
||||
<form className="inline-form links-add-form" onSubmit={addLink}>
|
||||
<input name="title" placeholder={textContent.titlePlaceholder || "Nom du lien"} value={title} onChange={(event) => setTitle(event.target.value)} />
|
||||
<input name="url" placeholder={textContent.urlPlaceholder || "https://..."} value={url} onChange={(event) => setUrl(event.target.value)} />
|
||||
<button className="primary">Ajouter</button>
|
||||
</form>
|
||||
<form className="text-import-form" onSubmit={importLinks}>
|
||||
<textarea
|
||||
value={textImport}
|
||||
onChange={(event) => setTextImport(event.target.value)}
|
||||
placeholder={textContent.importPlaceholder || "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map"}
|
||||
rows={3}
|
||||
/>
|
||||
<button type="submit">Importer le texte</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
<ul className="links-list">
|
||||
{data.links.map((link) => (
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useState } from "react";
|
|||
export function NotepadModule({ toolboxId, moduleId, context }) {
|
||||
const data = context.getModuleData(toolboxId, moduleId, { text: "" });
|
||||
const [text, setText] = useState(data.text || "");
|
||||
const textContent = context.moduleText?.notepad || {};
|
||||
|
||||
return (
|
||||
<textarea
|
||||
|
|
@ -12,7 +13,7 @@ export function NotepadModule({ toolboxId, moduleId, context }) {
|
|||
setText(event.target.value);
|
||||
context.setModuleData(toolboxId, moduleId, { text: event.target.value });
|
||||
}}
|
||||
placeholder="Notes rapides..."
|
||||
placeholder={textContent.placeholder || "Notes rapides..."}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|||
import { createPortal } from "react-dom";
|
||||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { usePointerReorder } from "../../../hooks/usePointerReorder.js";
|
||||
import { CalculatorModule } from "./CalculatorModule.jsx";
|
||||
import { ChecklistModule } from "./ChecklistModule.jsx";
|
||||
import { CountersModule } from "./CountersModule.jsx";
|
||||
import { LinksModule } from "./LinksModule.jsx";
|
||||
|
|
@ -13,7 +14,8 @@ const MODULE_COMPONENTS = {
|
|||
checklist: { label: "Checklist", icon: "checklist", Component: ChecklistModule, editable: true },
|
||||
screenshots: { label: "Screenshots", icon: "picture", Component: ScreenshotsModule, editable: true },
|
||||
links: { label: "Liens", icon: "link", Component: LinksModule, editable: true },
|
||||
counters: { label: "Compteurs", icon: "abacus", Component: CountersModule, editable: true }
|
||||
counters: { label: "Compteurs", icon: "abacus", Component: CountersModule, editable: true },
|
||||
calculator: { label: "Calculateur", icon: "calculator", Component: CalculatorModule, editable: false }
|
||||
};
|
||||
|
||||
const MODULE_COLUMN_GAP_PX = 16;
|
||||
|
|
|
|||
15
website/src/features/toolboxes/modules/textImport.js
Normal file
15
website/src/features/toolboxes/modules/textImport.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export function parseColonImportLines(text) {
|
||||
return String(text || "")
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const separatorIndex = line.indexOf(":");
|
||||
if (separatorIndex === -1) return { label: line, value: "" };
|
||||
return {
|
||||
label: line.slice(0, separatorIndex).trim(),
|
||||
value: line.slice(separatorIndex + 1).trim()
|
||||
};
|
||||
})
|
||||
.filter((entry) => entry.label);
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ const STORAGE_KEYS = {
|
|||
const APP_STORAGE_PREFIX = "sokkog:";
|
||||
const APP_STORAGE_LIMIT_BYTES = 5 * 1024 * 1024;
|
||||
const APP_STORAGE_WARNING_RATIO = 0.85;
|
||||
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", shot: "s", link: "l", counter: "c" };
|
||||
const ID_PREFIXES = { tbx: "t", mod: "m", item: "i", shot: "s", link: "l", counter: "c", calc: "r" };
|
||||
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const TOOLBOX_ICON_BASE = "/static/img/toolbox-icons/";
|
||||
const TOOLBOX_ICON_FILES = [
|
||||
|
|
@ -95,6 +95,24 @@ const DEFAULT_SITE_CONTENT = {
|
|||
text: "",
|
||||
items: []
|
||||
},
|
||||
modules: {
|
||||
notepad: { placeholder: "Notes rapides..." },
|
||||
checklist: {
|
||||
itemPlaceholder: "Nouvel item",
|
||||
importPlaceholder: "Importer plusieurs items...\nPotion:10\nMéga potion:5"
|
||||
},
|
||||
links: {
|
||||
titlePlaceholder: "Nom du lien",
|
||||
urlPlaceholder: "https://...",
|
||||
importPlaceholder: "Importer plusieurs liens...\nBuild:https://example.com/build?class=rogue\nMap:https://example.com/map"
|
||||
},
|
||||
counters: { labelPlaceholder: "Nom du compteur" },
|
||||
calculator: {
|
||||
expressionPlaceholder: "10*10",
|
||||
labelPlaceholder: "Lingots de fer",
|
||||
scrollableTitle: "Liste scrollable"
|
||||
}
|
||||
},
|
||||
emptyTitle: "Aucune toolbox",
|
||||
emptyText: ""
|
||||
}
|
||||
|
|
@ -253,7 +271,7 @@ function parsePositiveInt(value, fallback = 1) {
|
|||
function clampQty(value, target) {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
const safeValue = Number.isFinite(parsed) ? parsed : 0;
|
||||
return Math.min(Math.max(safeValue, 0), Math.max(1, target));
|
||||
return Math.min(safeValue, Math.max(1, target));
|
||||
}
|
||||
|
||||
function normalizeChecklistItem(item) {
|
||||
|
|
@ -327,6 +345,26 @@ function normalizeCountersData(data) {
|
|||
};
|
||||
}
|
||||
|
||||
function normalizeCalculatorValue(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : 0;
|
||||
}
|
||||
|
||||
function normalizeCalculatorData(data) {
|
||||
const entries = (data?.entries || [])
|
||||
.map((entry) => ({
|
||||
id: entry?.id || uid("calc"),
|
||||
parentId: String(entry?.parentId || ""),
|
||||
label: String(entry?.label || "").trim(),
|
||||
value: normalizeCalculatorValue(entry?.value)
|
||||
}));
|
||||
const entryIds = new Set(entries.map((entry) => entry.id));
|
||||
return {
|
||||
scrollResults: data?.scrollResults === true,
|
||||
entries: entries.map((entry) => entryIds.has(entry.parentId) ? entry : { ...entry, parentId: "" })
|
||||
};
|
||||
}
|
||||
|
||||
function compactChecklistItemForStorage(item) {
|
||||
const normalized = normalizeChecklistItem(item);
|
||||
const compact = { id: normalized.id, label: normalized.label };
|
||||
|
|
@ -369,6 +407,20 @@ function compactModuleDataForStorage(type, value) {
|
|||
}));
|
||||
return counters.length ? { counters } : null;
|
||||
}
|
||||
if (type === "calculator") {
|
||||
const normalized = normalizeCalculatorData(value);
|
||||
const entries = normalized.entries.map((entry) => {
|
||||
const compact = {
|
||||
id: entry.id,
|
||||
label: entry.label,
|
||||
value: entry.value
|
||||
};
|
||||
if (entry.parentId) compact.parentId = entry.parentId;
|
||||
return compact;
|
||||
});
|
||||
if (!entries.length && !normalized.scrollResults) return null;
|
||||
return normalized.scrollResults ? { entries, scrollResults: true } : { entries };
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
|
@ -409,6 +461,20 @@ function remapModuleDataForExport(type, data, nextId) {
|
|||
return { counters: compact.counters.map((counter) => ({ ...counter, id: nextId("counter") })) };
|
||||
}
|
||||
|
||||
if (type === "calculator") {
|
||||
const entryIdMap = new Map();
|
||||
compact.entries.forEach((entry) => entryIdMap.set(entry.id, nextId("calc")));
|
||||
const remapped = {
|
||||
entries: compact.entries.map((entry) => {
|
||||
const remapped = { ...entry, id: entryIdMap.get(entry.id) };
|
||||
if (entry.parentId) remapped.parentId = entryIdMap.get(entry.parentId) || "";
|
||||
return remapped;
|
||||
})
|
||||
};
|
||||
if (compact.scrollResults) remapped.scrollResults = true;
|
||||
return remapped;
|
||||
}
|
||||
|
||||
return compact;
|
||||
}
|
||||
|
||||
|
|
@ -842,6 +908,8 @@ function App() {
|
|||
{drawerGameId && (
|
||||
<ToolboxDrawer
|
||||
gameId={drawerGameId}
|
||||
game={getGame(drawerGameId)}
|
||||
siteContent={siteContent}
|
||||
toolboxes={store.toolboxes}
|
||||
links={store.links}
|
||||
actions={actions}
|
||||
|
|
@ -1207,14 +1275,17 @@ function ToolboxGameIcon({ game }) {
|
|||
);
|
||||
}
|
||||
|
||||
function ToolboxView({ toolbox, toolboxGame, embedded, actions, updateToolbox, updateModuleData, addScreenshotFiles }) {
|
||||
function ToolboxView({ siteContent, toolbox, toolboxGame, embedded, actions, updateToolbox, updateModuleData, addScreenshotFiles }) {
|
||||
const moduleColumns = Number(toolbox.moduleColumns) === 1 ? 1 : 2;
|
||||
const toolboxGameCover = getGameCardCover(toolboxGame);
|
||||
const moduleText = siteContent?.toolboxes?.modules || DEFAULT_SITE_CONTENT.toolboxes.modules;
|
||||
const moduleContext = {
|
||||
getModuleData,
|
||||
moduleText,
|
||||
normalizeChecklistData,
|
||||
normalizeLinksData,
|
||||
normalizeCountersData,
|
||||
normalizeCalculatorData,
|
||||
normalizeUrl,
|
||||
hostnameFromUrl,
|
||||
copyText,
|
||||
|
|
@ -1255,6 +1326,16 @@ function ToolboxView({ toolbox, toolboxGame, embedded, actions, updateToolbox, u
|
|||
<EditableTitle className="toolbox-title" value={toolbox.name} fallback="Nouvelle toolbox" onSave={(name) => name !== toolbox.name && updateToolbox({ ...toolbox, name })} />
|
||||
)}
|
||||
</div>
|
||||
{toolboxGame && (
|
||||
<a
|
||||
className="button toolbox-hero-link"
|
||||
href={embedded ? `#/toolbox/${toolbox.id}` : `#/games/${toolboxGame.id}`}
|
||||
onClick={embedded ? () => actions.setDrawerGameId("") : undefined}
|
||||
>
|
||||
<Icon name="enter" />
|
||||
<span>{embedded ? "Vers la toolbox complète" : "Vers la page de jeu"}</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="actions">
|
||||
<AddToolControls onAdd={addModule} />
|
||||
|
|
@ -1291,7 +1372,7 @@ function ToolboxView({ toolbox, toolboxGame, embedded, actions, updateToolbox, u
|
|||
);
|
||||
}
|
||||
|
||||
function ToolboxDrawer({ gameId, toolboxes, links, actions, updateToolbox, updateModuleData, addScreenshotFiles }) {
|
||||
function ToolboxDrawer({ gameId, game, siteContent, toolboxes, links, actions, updateToolbox, updateModuleData, addScreenshotFiles }) {
|
||||
const selectedId = links[gameId] || "";
|
||||
const toolbox = toolboxes.find((item) => item.id === selectedId);
|
||||
const [width, setWidth] = useState(() => {
|
||||
|
|
@ -1343,7 +1424,7 @@ function ToolboxDrawer({ gameId, toolboxes, links, actions, updateToolbox, updat
|
|||
</div>
|
||||
<button className="drawer-close-button" onClick={() => actions.setDrawerGameId("")} aria-label="Fermer" title="Fermer"><Icon name="close" /></button>
|
||||
</header>
|
||||
{toolbox ? <ToolboxView toolbox={toolbox} embedded actions={actions} updateToolbox={updateToolbox} updateModuleData={updateModuleData} addScreenshotFiles={addScreenshotFiles} /> : (
|
||||
{toolbox ? <ToolboxView siteContent={siteContent} toolbox={toolbox} toolboxGame={game} embedded actions={actions} updateToolbox={updateToolbox} updateModuleData={updateModuleData} addScreenshotFiles={addScreenshotFiles} /> : (
|
||||
<div className="empty"><p>Aucune toolbox associée à cette page jeu.</p><button className="primary" onClick={() => actions.setCreateModal({ gameId })}>Créer et associer</button></div>
|
||||
)}
|
||||
<StorageQuota />
|
||||
|
|
|
|||
|
|
@ -89,6 +89,12 @@
|
|||
font-weight: 800;
|
||||
}
|
||||
|
||||
.results-copy-button {
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.game-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@
|
|||
-webkit-mask-image: url("/static/icons/open.svg");
|
||||
}
|
||||
|
||||
.ui-icon-enter {
|
||||
mask-image: url("/static/icons/enter.svg");
|
||||
-webkit-mask-image: url("/static/icons/enter.svg");
|
||||
}
|
||||
|
||||
.ui-icon-zoom {
|
||||
mask-image: url("/static/icons/zoom.svg");
|
||||
-webkit-mask-image: url("/static/icons/zoom.svg");
|
||||
|
|
@ -56,6 +61,16 @@
|
|||
-webkit-mask-image: url("/static/icons/copy.svg");
|
||||
}
|
||||
|
||||
.ui-icon-calculator {
|
||||
mask-image: url("/static/icons/calculator.svg");
|
||||
-webkit-mask-image: url("/static/icons/calculator.svg");
|
||||
}
|
||||
|
||||
.ui-icon-scrollable {
|
||||
mask-image: url("/static/icons/scrollable.svg");
|
||||
-webkit-mask-image: url("/static/icons/scrollable.svg");
|
||||
}
|
||||
|
||||
.ui-icon-drag {
|
||||
mask-image: url("/static/icons/drag.svg");
|
||||
-webkit-mask-image: url("/static/icons/drag.svg");
|
||||
|
|
|
|||
|
|
@ -160,6 +160,18 @@
|
|||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.calculator-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.calculator-module {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.calculator-form {
|
||||
grid-row: auto;
|
||||
}
|
||||
|
||||
.toolbox-actions-row {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -70,6 +70,19 @@
|
|||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.toolbox-hero-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
width: fit-content;
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.toolbox-hero-link .ui-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.toolbox-icon-picker {
|
||||
position: relative;
|
||||
z-index: 70;
|
||||
|
|
@ -526,6 +539,11 @@
|
|||
-webkit-mask-image: url("/static/icons/abacus.svg");
|
||||
}
|
||||
|
||||
.module-icon-calculator {
|
||||
mask-image: url("/static/icons/calculator.svg");
|
||||
-webkit-mask-image: url("/static/icons/calculator.svg");
|
||||
}
|
||||
|
||||
.notepad {
|
||||
display: block;
|
||||
width: calc(100% - 32px);
|
||||
|
|
@ -552,6 +570,32 @@
|
|||
grid-template-columns: minmax(0, 1fr) 76px auto;
|
||||
}
|
||||
|
||||
.module-add-panel {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.module-add-panel .inline-form {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.text-import-form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.text-import-form textarea {
|
||||
min-height: 86px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.text-import-form button {
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
.checklist-qty-input {
|
||||
padding-inline: 10px;
|
||||
text-align: center;
|
||||
|
|
@ -572,6 +616,7 @@ textarea {
|
|||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
|
|
@ -631,10 +676,40 @@ textarea:focus {
|
|||
line-height: 1;
|
||||
}
|
||||
|
||||
.checklist-qty-controls small {
|
||||
min-width: 42px;
|
||||
.checklist-qty-current {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.checklist-qty-current input {
|
||||
width: max(18px, calc((var(--qty-current-digits, 1) * 0.72rem) + 8px));
|
||||
min-height: 28px;
|
||||
padding: 0 4px;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--color-text-primary);
|
||||
text-align: right;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.checklist-qty-current input::-webkit-outer-spin-button,
|
||||
.checklist-qty-current input::-webkit-inner-spin-button {
|
||||
margin: 0;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.checklist-qty-current input:hover,
|
||||
.checklist-qty-current input:focus {
|
||||
border-color: rgba(246, 196, 83, 0.34);
|
||||
background: rgba(21, 26, 48, 0.42);
|
||||
}
|
||||
|
||||
.checklist-qty-current span {
|
||||
color: var(--color-text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.links-add-form {
|
||||
|
|
@ -712,6 +787,398 @@ textarea:focus {
|
|||
font-weight: 900;
|
||||
}
|
||||
|
||||
.calculator-module {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 35fr) minmax(0, 65fr);
|
||||
column-gap: var(--space-6);
|
||||
row-gap: var(--space-4);
|
||||
align-items: start;
|
||||
overflow: hidden;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.calculator-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
align-self: start;
|
||||
align-content: start;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.calculator-card,
|
||||
.calculator-saved {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(139, 92, 246, 0.08), transparent 62%),
|
||||
rgba(7, 10, 24, 0.3);
|
||||
}
|
||||
|
||||
.calculator-card {
|
||||
gap: 10px;
|
||||
align-self: start;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.calculator-form label,
|
||||
.calculator-result {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.calculator-card > *,
|
||||
.calculator-form input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.calculator-form label span,
|
||||
.calculator-result span,
|
||||
.calculator-root-button {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.calculator-result {
|
||||
min-height: 72px;
|
||||
justify-content: stretch;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
background:
|
||||
radial-gradient(circle at 100% 0%, rgba(246, 196, 83, 0.08), transparent 36%),
|
||||
rgba(5, 7, 17, 0.56);
|
||||
}
|
||||
|
||||
.calculator-result strong {
|
||||
color: var(--color-text-primary);
|
||||
font-size: clamp(1.55rem, 3vw, 2.15rem);
|
||||
font-weight: 900;
|
||||
line-height: 0.95;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.calculator-saved {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: var(--space-3);
|
||||
overflow: visible;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(139, 92, 246, 0.045), transparent 62%),
|
||||
rgba(7, 10, 24, 0.22);
|
||||
}
|
||||
|
||||
.calculator-saved.is-scrollable {
|
||||
height: var(--calculator-scroll-height, auto);
|
||||
max-height: var(--calculator-scroll-height, none);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calculator-result-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.calculator-root-button {
|
||||
width: fit-content;
|
||||
min-height: 34px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(5, 7, 17, 0.24);
|
||||
}
|
||||
|
||||
.calculator-action-button {
|
||||
display: inline-flex;
|
||||
width: 34px;
|
||||
min-width: 34px;
|
||||
min-height: 34px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(5, 7, 17, 0.24);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.calculator-action-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.48;
|
||||
}
|
||||
|
||||
.calculator-action-button.danger {
|
||||
border-color: rgba(251, 113, 133, 0.2);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(251, 113, 133, 0.1), rgba(7, 10, 24, 0.32));
|
||||
color: #fecdd3;
|
||||
}
|
||||
|
||||
.calculator-action-button.danger:hover {
|
||||
border-color: rgba(251, 113, 133, 0.54);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(251, 113, 133, 0.2), rgba(7, 10, 24, 0.44));
|
||||
color: #fff1f2;
|
||||
box-shadow: 0 0 14px rgba(251, 113, 133, 0.16);
|
||||
}
|
||||
|
||||
.calculator-action-button .ui-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.calculator-scroll-toggle {
|
||||
display: inline-grid;
|
||||
grid-template-columns: 18px 34px;
|
||||
width: fit-content;
|
||||
min-height: 34px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 6px 0 10px;
|
||||
border: 1px solid rgba(165, 180, 252, 0.12);
|
||||
border-radius: var(--radius-pill);
|
||||
background: rgba(5, 7, 17, 0.24);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.calculator-scroll-toggle .ui-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.calculator-scroll-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);
|
||||
}
|
||||
|
||||
.calculator-scroll-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);
|
||||
}
|
||||
|
||||
.calculator-root-button {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.calculator-root-button:hover,
|
||||
.calculator-action-button:hover:not(:disabled),
|
||||
.calculator-scroll-toggle:hover,
|
||||
.calculator-scroll-toggle.active {
|
||||
border-color: rgba(196, 181, 253, 0.32);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.calculator-scroll-toggle.active {
|
||||
background: rgba(139, 92, 246, 0.18);
|
||||
}
|
||||
|
||||
.calculator-scroll-toggle.active i {
|
||||
border-color: rgba(246, 196, 83, 0.36);
|
||||
background: rgba(246, 196, 83, 0.12);
|
||||
}
|
||||
|
||||
.calculator-scroll-toggle.active i::before {
|
||||
left: 17px;
|
||||
background: var(--color-accent-gold);
|
||||
box-shadow: 0 0 10px rgba(246, 196, 83, 0.28);
|
||||
}
|
||||
|
||||
.calculator-tree {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.calculator-tree.is-scrollable {
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.calculator-entry-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.calculator-entry-list .calculator-entry-list {
|
||||
position: relative;
|
||||
margin-top: 6px;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.calculator-entry-list .calculator-entry-list::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
bottom: 14px;
|
||||
left: 7px;
|
||||
width: 1px;
|
||||
background: linear-gradient(180deg, rgba(246, 196, 83, 0.52), rgba(139, 92, 246, 0.2));
|
||||
}
|
||||
|
||||
.calculator-entry-list .calculator-entry-list > li {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.calculator-entry-list .calculator-entry-list > li::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: -15px;
|
||||
width: 11px;
|
||||
height: 1px;
|
||||
background: rgba(246, 196, 83, 0.52);
|
||||
}
|
||||
|
||||
.calculator-entry {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 34px 34px;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.calculator-entry-list li.is-editing > .calculator-entry {
|
||||
grid-template-columns: auto minmax(0, 1fr) 34px 34px;
|
||||
}
|
||||
|
||||
.calculator-entry > button {
|
||||
min-width: 0;
|
||||
min-height: 36px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.calculator-entry-summary,
|
||||
.calculator-entry-value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
border: 1px solid rgba(165, 180, 252, 0.09);
|
||||
border-radius: var(--radius-md);
|
||||
border-color: rgba(165, 180, 252, 0.09);
|
||||
background: rgba(21, 26, 48, 0.58);
|
||||
color: var(--color-text-secondary);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.calculator-entry-summary {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.calculator-entry-value {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.calculator-entry-value.is-readonly {
|
||||
min-height: 36px;
|
||||
padding: 0 10px;
|
||||
background: rgba(21, 26, 48, 0.38);
|
||||
cursor: default;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.calculator-entry-summary:hover,
|
||||
.calculator-entry-value:not(.is-readonly):hover {
|
||||
border-color: rgba(196, 181, 253, 0.2);
|
||||
background: rgba(21, 26, 48, 0.72);
|
||||
}
|
||||
|
||||
.calculator-entry-summary span {
|
||||
overflow: hidden;
|
||||
color: var(--color-text-secondary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.calculator-entry-summary strong,
|
||||
.calculator-entry-value strong {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.calculator-entry-label {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 36px;
|
||||
padding: 0 10px;
|
||||
overflow: hidden;
|
||||
border-color: rgba(165, 180, 252, 0.09);
|
||||
background: rgba(21, 26, 48, 0.38);
|
||||
color: var(--color-text-secondary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.calculator-entry-label:hover,
|
||||
.calculator-entry-label:focus {
|
||||
border-color: rgba(196, 181, 253, 0.22);
|
||||
background: rgba(21, 26, 48, 0.58);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.calculator-entry .ui-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.calculator-entry > button.danger {
|
||||
min-width: 34px;
|
||||
min-height: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.calculator-entry > button:not(.calculator-entry-summary, .calculator-entry-value, .danger) {
|
||||
min-width: 34px;
|
||||
min-height: 36px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.calculator-entry-list li.active > .calculator-entry > .calculator-entry-summary,
|
||||
.calculator-entry-list li.active > .calculator-entry > .calculator-entry-value,
|
||||
.calculator-entry-list li.active > .calculator-entry > .calculator-entry-label {
|
||||
border-color: var(--color-accent-gold);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(246, 196, 83, 0.09), rgba(139, 92, 246, 0.1)),
|
||||
rgba(7, 10, 24, 0.48);
|
||||
color: var(--color-text-primary);
|
||||
box-shadow: 0 0 0 1px rgba(246, 196, 83, 0.06);
|
||||
}
|
||||
|
||||
.link-item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue