Add per-item inventory sparklines with expandable history charts.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -19,6 +19,7 @@ from db import (
|
|||||||
get_snapshot,
|
get_snapshot,
|
||||||
import_save,
|
import_save,
|
||||||
init_db,
|
init_db,
|
||||||
|
inventory_timeline,
|
||||||
list_snapshots,
|
list_snapshots,
|
||||||
get_connection,
|
get_connection,
|
||||||
timeline,
|
timeline,
|
||||||
@@ -113,6 +114,12 @@ def api_timeline(viewer_id: str):
|
|||||||
return jsonify(timeline(db_path=db_path))
|
return jsonify(timeline(db_path=db_path))
|
||||||
|
|
||||||
|
|
||||||
|
@viewer_bp.route("/api/inventory/timeline")
|
||||||
|
def api_inventory_timeline(viewer_id: str):
|
||||||
|
db_path = _resolve_viewer_db(viewer_id)
|
||||||
|
return jsonify(inventory_timeline(db_path=db_path))
|
||||||
|
|
||||||
|
|
||||||
@viewer_bp.route("/api/import", methods=["POST"])
|
@viewer_bp.route("/api/import", methods=["POST"])
|
||||||
@limiter.limit(IMPORT_LIMIT)
|
@limiter.limit(IMPORT_LIMIT)
|
||||||
def api_import(viewer_id: str):
|
def api_import(viewer_id: str):
|
||||||
|
|||||||
@@ -308,3 +308,37 @@ def timeline(db_path: Path | str = DEFAULT_DB) -> list[dict]:
|
|||||||
).fetchall()
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
def inventory_timeline(db_path: Path | str = DEFAULT_DB) -> dict[str, Any]:
|
||||||
|
"""Per-item quantity series aligned to snapshots (oldest → newest)."""
|
||||||
|
conn = get_connection(db_path)
|
||||||
|
init_db(conn)
|
||||||
|
snap_rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, exported_at FROM snapshots
|
||||||
|
ORDER BY exported_at ASC, id ASC
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
snapshots = [dict(r) for r in snap_rows]
|
||||||
|
if not snapshots:
|
||||||
|
conn.close()
|
||||||
|
return {"snapshots": [], "series": {}}
|
||||||
|
|
||||||
|
snap_index = {row["id"]: idx for idx, row in enumerate(snapshots)}
|
||||||
|
inv_rows = conn.execute(
|
||||||
|
"SELECT snapshot_id, item_key, qty FROM inventory_snapshots"
|
||||||
|
).fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
series: dict[str, list[int]] = {}
|
||||||
|
n = len(snapshots)
|
||||||
|
for row in inv_rows:
|
||||||
|
key = row["item_key"]
|
||||||
|
if key not in series:
|
||||||
|
series[key] = [0] * n
|
||||||
|
idx = snap_index.get(row["snapshot_id"])
|
||||||
|
if idx is not None:
|
||||||
|
series[key][idx] = row["qty"]
|
||||||
|
|
||||||
|
return {"snapshots": snapshots, "series": series}
|
||||||
|
|||||||
+141
-7
@@ -9,6 +9,7 @@ let state = {
|
|||||||
quests: { tab: "story", filter: "all" },
|
quests: { tab: "story", filter: "all" },
|
||||||
history: { olderId: null, newerId: null, diff: null },
|
history: { olderId: null, newerId: null, diff: null },
|
||||||
charts: {},
|
charts: {},
|
||||||
|
inventoryTimeline: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CATEGORY_ORDER = [
|
const CATEGORY_ORDER = [
|
||||||
@@ -238,6 +239,7 @@ async function loadData() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.data = await res.json();
|
state.data = await res.json();
|
||||||
|
state.inventoryTimeline = null;
|
||||||
renderAll();
|
renderAll();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showEmpty(t("empty.loadError", { message: err.message }));
|
showEmpty(t("empty.loadError", { message: err.message }));
|
||||||
@@ -436,8 +438,129 @@ function getFilteredInventoryItems(d, inv) {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureInventoryTimeline() {
|
||||||
|
if (state.inventoryTimeline) return state.inventoryTimeline;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${apiBase()}/inventory/timeline`);
|
||||||
|
state.inventoryTimeline = res.ok ? await res.json() : { snapshots: [], series: {} };
|
||||||
|
} catch {
|
||||||
|
state.inventoryTimeline = { snapshots: [], series: {} };
|
||||||
|
}
|
||||||
|
return state.inventoryTimeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inventoryTrendEnabled() {
|
||||||
|
return (state.inventoryTimeline?.snapshots?.length || 0) >= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemQtySeries(itemKey) {
|
||||||
|
const tl = state.inventoryTimeline;
|
||||||
|
if (!tl?.series) return null;
|
||||||
|
const values = tl.series[itemKey];
|
||||||
|
if (!values || values.length < 2) return null;
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sparklineSvg(values, width = 72, height = 24) {
|
||||||
|
const max = Math.max(...values);
|
||||||
|
const min = Math.min(...values);
|
||||||
|
const range = max - min || 1;
|
||||||
|
const points = values.map((v, i) => {
|
||||||
|
const x = (i / (values.length - 1)) * width;
|
||||||
|
const y = height - ((v - min) / range) * (height - 4) - 2;
|
||||||
|
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
||||||
|
}).join(" ");
|
||||||
|
return `<svg class="inv-spark-svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" aria-hidden="true"><polyline points="${points}" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderItemSparkCell(item) {
|
||||||
|
const values = itemQtySeries(item.key);
|
||||||
|
if (!values) {
|
||||||
|
return `<td class="col-trend"><span class="inv-spark-empty">—</span></td>`;
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<td class="col-trend">
|
||||||
|
<button type="button" class="inv-spark-btn" data-item-key="${esc(item.key)}" data-item-name="${esc(item.name)}" title="${esc(t("inventory.trendExpand"))}" aria-label="${esc(t("inventory.trendExpandFor", { name: item.name }))}">
|
||||||
|
${sparklineSvg(values)}
|
||||||
|
</button>
|
||||||
|
</td>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupInventoryChartModal() {
|
||||||
|
if (document.getElementById("inv-chart-modal")) return;
|
||||||
|
|
||||||
|
const modal = document.createElement("div");
|
||||||
|
modal.id = "inv-chart-modal";
|
||||||
|
modal.className = "inv-chart-modal";
|
||||||
|
modal.hidden = true;
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="inv-chart-modal-backdrop" data-close="1"></div>
|
||||||
|
<div class="inv-chart-modal-panel card" role="dialog" aria-modal="true" aria-labelledby="inv-chart-modal-title">
|
||||||
|
<button type="button" class="inv-chart-modal-close" data-close="1" aria-label="${esc(t("actions.dismiss"))}">×</button>
|
||||||
|
<h3 id="inv-chart-modal-title"></h3>
|
||||||
|
<div class="chart-wrap chart-wrap-modal"><canvas id="inv-chart-modal-canvas"></canvas></div>
|
||||||
|
</div>`;
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
modal.querySelectorAll("[data-close]").forEach((el) => {
|
||||||
|
el.addEventListener("click", closeInventoryChartModal);
|
||||||
|
});
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Escape" && !modal.hidden) closeInventoryChartModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openInventoryChartModal(itemKey, itemName) {
|
||||||
|
const values = itemQtySeries(itemKey);
|
||||||
|
const tl = state.inventoryTimeline;
|
||||||
|
if (!values || !tl) return;
|
||||||
|
|
||||||
|
setupInventoryChartModal();
|
||||||
|
const modal = document.getElementById("inv-chart-modal");
|
||||||
|
const title = document.getElementById("inv-chart-modal-title");
|
||||||
|
title.textContent = itemName;
|
||||||
|
modal.hidden = false;
|
||||||
|
document.body.classList.add("inv-chart-modal-open");
|
||||||
|
|
||||||
|
destroyChart("inventoryModal");
|
||||||
|
const labels = tl.snapshots.map((s) => formatTs(s.exported_at));
|
||||||
|
state.charts.inventoryModal = new Chart(document.getElementById("inv-chart-modal-canvas"), {
|
||||||
|
type: "line",
|
||||||
|
data: {
|
||||||
|
labels,
|
||||||
|
datasets: [{
|
||||||
|
label: t("inventory.qty"),
|
||||||
|
data: values,
|
||||||
|
borderColor: "#6c8cff",
|
||||||
|
backgroundColor: "rgba(108, 140, 255, 0.12)",
|
||||||
|
tension: 0.3,
|
||||||
|
fill: true,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
options: chartOpts(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeInventoryChartModal() {
|
||||||
|
const modal = document.getElementById("inv-chart-modal");
|
||||||
|
if (!modal || modal.hidden) return;
|
||||||
|
modal.hidden = true;
|
||||||
|
document.body.classList.remove("inv-chart-modal-open");
|
||||||
|
destroyChart("inventoryModal");
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindInventorySparklines(container) {
|
||||||
|
container.querySelectorAll(".inv-spark-btn").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
openInventoryChartModal(btn.dataset.itemKey, btn.dataset.itemName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderInventoryTable(d, inv) {
|
function renderInventoryTable(d, inv) {
|
||||||
const items = getFilteredInventoryItems(d, inv);
|
const items = getFilteredInventoryItems(d, inv);
|
||||||
|
const showTrend = inventoryTrendEnabled();
|
||||||
|
const colSpan = showTrend ? 4 : 3;
|
||||||
const grouped = {};
|
const grouped = {};
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (!grouped[item.category]) grouped[item.category] = [];
|
if (!grouped[item.category]) grouped[item.category] = [];
|
||||||
@@ -450,7 +573,7 @@ function renderInventoryTable(d, inv) {
|
|||||||
const catLabel = categoryLabel(cat);
|
const catLabel = categoryLabel(cat);
|
||||||
const header = `
|
const header = `
|
||||||
<tr class="inv-group-row" data-group="${esc(cat)}">
|
<tr class="inv-group-row" data-group="${esc(cat)}">
|
||||||
<td colspan="3">
|
<td colspan="${colSpan}">
|
||||||
<button type="button" class="inv-group-toggle" data-group="${esc(cat)}" aria-expanded="${expanded}">
|
<button type="button" class="inv-group-toggle" data-group="${esc(cat)}" aria-expanded="${expanded}">
|
||||||
<span class="inv-group-title">${esc(catLabel)}</span>
|
<span class="inv-group-title">${esc(catLabel)}</span>
|
||||||
<span class="inv-group-meta">${esc(t("inventory.groupMeta", { count: catItems.length, qty: fmt(totalQty) }))}</span>
|
<span class="inv-group-meta">${esc(t("inventory.groupMeta", { count: catItems.length, qty: fmt(totalQty) }))}</span>
|
||||||
@@ -460,6 +583,7 @@ function renderInventoryTable(d, inv) {
|
|||||||
const rows = catItems.map((i) => `
|
const rows = catItems.map((i) => `
|
||||||
<tr class="inv-item-row ${i.equipped && inv.highlightEquipped ? "item-equipped" : ""} ${expanded ? "" : "collapsed"}" data-group="${esc(cat)}">
|
<tr class="inv-item-row ${i.equipped && inv.highlightEquipped ? "item-equipped" : ""} ${expanded ? "" : "collapsed"}" data-group="${esc(cat)}">
|
||||||
<td class="col-name">${esc(i.name)}${i.equipped ? `<span class="equipped-mark" title="${esc(t("inventory.equipped"))}">⚡</span>` : ""}</td>
|
<td class="col-name">${esc(i.name)}${i.equipped ? `<span class="equipped-mark" title="${esc(t("inventory.equipped"))}">⚡</span>` : ""}</td>
|
||||||
|
${showTrend ? renderItemSparkCell(i) : ""}
|
||||||
<td class="col-qty">${fmt(i.qty)}</td>
|
<td class="col-qty">${fmt(i.qty)}</td>
|
||||||
<td class="col-key"><code>${esc(i.key)}</code></td>
|
<td class="col-key"><code>${esc(i.key)}</code></td>
|
||||||
</tr>`).join("");
|
</tr>`).join("");
|
||||||
@@ -472,17 +596,26 @@ function renderInventoryTable(d, inv) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trendCol = showTrend
|
||||||
|
? `<col class="col-trend">`
|
||||||
|
: "";
|
||||||
|
const trendHeader = showTrend
|
||||||
|
? `<th class="col-trend">${esc(t("inventory.trend"))}</th>`
|
||||||
|
: "";
|
||||||
|
|
||||||
results.innerHTML = `
|
results.innerHTML = `
|
||||||
<div class="inv-table-wrap">
|
<div class="inv-table-wrap">
|
||||||
<table class="inv-table">
|
<table class="inv-table ${showTrend ? "has-trend" : ""}">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col class="col-name">
|
<col class="col-name">
|
||||||
|
${trendCol}
|
||||||
<col class="col-qty">
|
<col class="col-qty">
|
||||||
<col class="col-key">
|
<col class="col-key">
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="col-name">${esc(t("inventory.item"))}</th>
|
<th class="col-name">${esc(t("inventory.item"))}</th>
|
||||||
|
${trendHeader}
|
||||||
<th class="col-qty">${esc(t("inventory.qty"))}</th>
|
<th class="col-qty">${esc(t("inventory.qty"))}</th>
|
||||||
<th class="col-key">${esc(t("inventory.id"))}</th>
|
<th class="col-key">${esc(t("inventory.id"))}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -503,6 +636,7 @@ function renderInventoryTable(d, inv) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
bindInventorySparklines(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderInventoryChips(d, inv) {
|
function renderInventoryChips(d, inv) {
|
||||||
@@ -518,7 +652,7 @@ function renderInventoryChips(d, inv) {
|
|||||||
if (inv.categories.has(cat)) inv.categories.delete(cat);
|
if (inv.categories.has(cat)) inv.categories.delete(cat);
|
||||||
else inv.categories.add(cat);
|
else inv.categories.add(cat);
|
||||||
renderInventoryChips(d, inv);
|
renderInventoryChips(d, inv);
|
||||||
renderInventoryTable(d, inv);
|
ensureInventoryTimeline().then(() => renderInventoryTable(d, inv));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -546,15 +680,15 @@ function renderInventory(d) {
|
|||||||
|
|
||||||
document.getElementById("inv-search").addEventListener("input", (e) => {
|
document.getElementById("inv-search").addEventListener("input", (e) => {
|
||||||
state.inventory.search = e.target.value;
|
state.inventory.search = e.target.value;
|
||||||
renderInventoryTable(state.data, state.inventory);
|
ensureInventoryTimeline().then(() => renderInventoryTable(state.data, state.inventory));
|
||||||
});
|
});
|
||||||
document.getElementById("inv-sort").addEventListener("change", (e) => {
|
document.getElementById("inv-sort").addEventListener("change", (e) => {
|
||||||
state.inventory.sort = e.target.value;
|
state.inventory.sort = e.target.value;
|
||||||
renderInventoryTable(state.data, state.inventory);
|
ensureInventoryTimeline().then(() => renderInventoryTable(state.data, state.inventory));
|
||||||
});
|
});
|
||||||
document.getElementById("inv-equipped").addEventListener("change", (e) => {
|
document.getElementById("inv-equipped").addEventListener("change", (e) => {
|
||||||
state.inventory.highlightEquipped = e.target.checked;
|
state.inventory.highlightEquipped = e.target.checked;
|
||||||
renderInventoryTable(state.data, state.inventory);
|
ensureInventoryTimeline().then(() => renderInventoryTable(state.data, state.inventory));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,7 +702,7 @@ function renderInventory(d) {
|
|||||||
document.getElementById("inv-sort").value = inv.sort;
|
document.getElementById("inv-sort").value = inv.sort;
|
||||||
document.getElementById("inv-equipped").checked = inv.highlightEquipped;
|
document.getElementById("inv-equipped").checked = inv.highlightEquipped;
|
||||||
renderInventoryChips(d, inv);
|
renderInventoryChips(d, inv);
|
||||||
renderInventoryTable(d, inv);
|
ensureInventoryTimeline().then(() => renderInventoryTable(d, inv));
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEquipment(d) {
|
function renderEquipment(d) {
|
||||||
|
|||||||
@@ -116,7 +116,10 @@
|
|||||||
"qty": "Menge",
|
"qty": "Menge",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
"equipped": "Ausgerüstet",
|
"equipped": "Ausgerüstet",
|
||||||
"groupMeta": "{count} Items · {qty} Stk."
|
"groupMeta": "{count} Items · {qty} Stk.",
|
||||||
|
"trend": "Verlauf",
|
||||||
|
"trendExpand": "Klicken für großes Diagramm",
|
||||||
|
"trendExpandFor": "Mengenverlauf für {name}"
|
||||||
},
|
},
|
||||||
"equipment": {
|
"equipment": {
|
||||||
"title": "Ausrüstung"
|
"title": "Ausrüstung"
|
||||||
|
|||||||
@@ -116,7 +116,10 @@
|
|||||||
"qty": "Qty",
|
"qty": "Qty",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
"equipped": "Equipped",
|
"equipped": "Equipped",
|
||||||
"groupMeta": "{count} items · {qty} pcs"
|
"groupMeta": "{count} items · {qty} pcs",
|
||||||
|
"trend": "Trend",
|
||||||
|
"trendExpand": "Click to enlarge chart",
|
||||||
|
"trendExpandFor": "Quantity history for {name}"
|
||||||
},
|
},
|
||||||
"equipment": {
|
"equipment": {
|
||||||
"title": "Equipment"
|
"title": "Equipment"
|
||||||
|
|||||||
@@ -383,6 +383,11 @@ tr:hover td { background: var(--bg-hover); }
|
|||||||
.inv-table col.col-qty { width: 6.25rem; }
|
.inv-table col.col-qty { width: 6.25rem; }
|
||||||
.inv-table col.col-key { width: 34%; }
|
.inv-table col.col-key { width: 34%; }
|
||||||
|
|
||||||
|
.inv-table.has-trend col.col-name { width: 38%; }
|
||||||
|
.inv-table.has-trend col.col-trend { width: 5.5rem; }
|
||||||
|
.inv-table.has-trend col.col-qty { width: 5.5rem; }
|
||||||
|
.inv-table.has-trend col.col-key { width: 30%; }
|
||||||
|
|
||||||
.inv-table thead th {
|
.inv-table thead th {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -451,6 +456,111 @@ tr:hover td { background: var(--bg-hover); }
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inv-table th.col-trend,
|
||||||
|
.inv-table td.col-trend {
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-right: 8px;
|
||||||
|
text-align: center;
|
||||||
|
width: 5.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inv-spark-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inv-spark-btn:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inv-spark-btn:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inv-spark-svg {
|
||||||
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inv-spark-empty {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.inv-chart-modal-open {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inv-chart-modal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inv-chart-modal[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inv-chart-modal-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(8, 10, 16, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inv-chart-modal-panel {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: min(720px, 100%);
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px 20px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inv-chart-modal-panel h3 {
|
||||||
|
margin: 0 32px 12px 0;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inv-chart-modal-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inv-chart-modal-close:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrap-modal {
|
||||||
|
height: 280px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.inv-group-row td {
|
.inv-group-row td {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user