feat(biochem): skeleton loaders for async fetches
Replace plain "Загрузка..." placeholders with shimmer-animated skeletons matching the actual layout shape: - library: 12 placeholder cards (canvas + 2 lines) - reactions: 6 row skeletons (stripe + title + 2 text lines) - properties: 10 sidebar row shimmers (thumb + 2 lines) - biochem editor: 4-5 row skeletons for saved-molecules and challenges lists No existing skeleton classes in ls.css; added local .bc-sk-* helpers per page. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -209,6 +209,26 @@
|
|||||||
}
|
}
|
||||||
.detail-open-btn:hover { background: linear-gradient(135deg, rgba(155,93,229,.4), rgba(6,214,224,.25)); border-color: rgba(6,214,224,.5); color: #fff; }
|
.detail-open-btn:hover { background: linear-gradient(135deg, rgba(155,93,229,.4), rgba(6,214,224,.25)); border-color: rgba(6,214,224,.5); color: #fff; }
|
||||||
|
|
||||||
|
/* ── Shimmer skeleton ── */
|
||||||
|
@keyframes bc-shimmer {
|
||||||
|
0% { background-position: -200% 0; }
|
||||||
|
100% { background-position: 200% 0; }
|
||||||
|
}
|
||||||
|
.bc-sk {
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
rgba(255,255,255,0.04) 0%,
|
||||||
|
rgba(255,255,255,0.10) 50%,
|
||||||
|
rgba(255,255,255,0.04) 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: bc-shimmer 1.6s infinite;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.bc-sk-square { aspect-ratio: 1; }
|
||||||
|
.bc-sk-line { height: 12px; margin: 6px 0; }
|
||||||
|
.bc-sk-line.sm { width: 60%; }
|
||||||
|
.bc-sk-line.md { width: 80%; }
|
||||||
|
.bc-sk-card { padding: 12px; border: 1px solid rgba(255,255,255,.06); border-radius: 10px; }
|
||||||
|
|
||||||
/* Empty state */
|
/* Empty state */
|
||||||
.lib-empty {
|
.lib-empty {
|
||||||
grid-column: 1/-1;
|
grid-column: 1/-1;
|
||||||
@@ -294,14 +314,7 @@
|
|||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="lib-main">
|
<div class="lib-main">
|
||||||
<div class="lib-scroll" id="lib-scroll">
|
<div class="lib-scroll" id="lib-scroll">
|
||||||
<div class="lib-grid" id="lib-grid">
|
<div class="lib-grid" id="lib-grid"></div>
|
||||||
<div class="lib-empty">
|
|
||||||
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.3">
|
|
||||||
<circle cx="12" cy="12" r="3"/><path d="M2 12h3M19 12h3M12 2v3M12 19v3"/>
|
|
||||||
</svg>
|
|
||||||
<p>Загрузка молекул…</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Detail panel -->
|
<!-- Detail panel -->
|
||||||
@@ -470,7 +483,17 @@ let selectedId = null;
|
|||||||
const CAT_LABELS = { inorganic:'Неорганика', organic:'Органика', biomolecule:'Биомолекулы' };
|
const CAT_LABELS = { inorganic:'Неорганика', organic:'Органика', biomolecule:'Биомолекулы' };
|
||||||
const DIFF_STARS = ['', '<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>', '<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg><svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>', '<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg><svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg><svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>'];
|
const DIFF_STARS = ['', '<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>', '<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg><svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>', '<svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg><svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg><svg class="ic" viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>'];
|
||||||
|
|
||||||
|
function bcSkLibrary(n = 12) {
|
||||||
|
return Array.from({length: n}, () => `
|
||||||
|
<div class="bc-sk-card">
|
||||||
|
<div class="bc-sk bc-sk-square" style="margin-bottom:8px"></div>
|
||||||
|
<div class="bc-sk bc-sk-line md"></div>
|
||||||
|
<div class="bc-sk bc-sk-line sm"></div>
|
||||||
|
</div>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
document.getElementById('lib-grid').innerHTML = bcSkLibrary(12);
|
||||||
try {
|
try {
|
||||||
[allMols, allReactions] = await Promise.all([
|
[allMols, allReactions] = await Promise.all([
|
||||||
LS.biochemGetMolecules(),
|
LS.biochemGetMolecules(),
|
||||||
|
|||||||
@@ -155,6 +155,27 @@
|
|||||||
.add-card:hover { border-color: rgba(155,93,229,.5); color: #9B5DE5; background: rgba(155,93,229,.05); }
|
.add-card:hover { border-color: rgba(155,93,229,.5); color: #9B5DE5; background: rgba(155,93,229,.05); }
|
||||||
.add-card-icon { font-size: 2rem; }
|
.add-card-icon { font-size: 2rem; }
|
||||||
|
|
||||||
|
/* ── Shimmer skeleton ── */
|
||||||
|
@keyframes bc-shimmer {
|
||||||
|
0% { background-position: -200% 0; }
|
||||||
|
100% { background-position: 200% 0; }
|
||||||
|
}
|
||||||
|
.bc-sk {
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
rgba(255,255,255,0.04) 0%,
|
||||||
|
rgba(255,255,255,0.10) 50%,
|
||||||
|
rgba(255,255,255,0.04) 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: bc-shimmer 1.6s infinite;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.bc-sk-line { height: 10px; margin: 5px 0; }
|
||||||
|
.bc-sk-line.sm { width: 55%; }
|
||||||
|
.bc-sk-line.md { width: 80%; }
|
||||||
|
.bc-sk-molrow { display: flex; align-items: center; gap: 8px; padding: 7px 8px; margin-bottom: 2px; }
|
||||||
|
.bc-sk-molrow .bc-sk-thumb { width: 34px; height: 34px; border-radius: 8px; flex-shrink: 0; }
|
||||||
|
.bc-sk-molrow .bc-sk-info { flex: 1; }
|
||||||
|
|
||||||
/* ── Mobile ── */
|
/* ── Mobile ── */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
html, body { overflow: auto; }
|
html, body { overflow: auto; }
|
||||||
@@ -193,7 +214,7 @@
|
|||||||
<button class="ps-cat" data-cat="amino_acid" onclick="setCat(this,'amino_acid')">АК</button>
|
<button class="ps-cat" data-cat="amino_acid" onclick="setCat(this,'amino_acid')">АК</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ps-list" id="mol-list"><div style="padding:20px;color:#555;font-size:.8rem">Загрузка…</div></div>
|
<div class="ps-list" id="mol-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right: comparison -->
|
<!-- Right: comparison -->
|
||||||
@@ -294,7 +315,19 @@ let _catFilter = '';
|
|||||||
let _searchQ = '';
|
let _searchQ = '';
|
||||||
let _compare = []; // array of mol objects, max 4
|
let _compare = []; // array of mol objects, max 4
|
||||||
|
|
||||||
|
function bcSkMolList(n = 10) {
|
||||||
|
return Array.from({length: n}, () => `
|
||||||
|
<div class="bc-sk-molrow">
|
||||||
|
<div class="bc-sk bc-sk-thumb"></div>
|
||||||
|
<div class="bc-sk-info">
|
||||||
|
<div class="bc-sk bc-sk-line md"></div>
|
||||||
|
<div class="bc-sk bc-sk-line sm"></div>
|
||||||
|
</div>
|
||||||
|
</div>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
document.getElementById('mol-list').innerHTML = bcSkMolList(10);
|
||||||
try {
|
try {
|
||||||
_allMols = await LS.biochemGetMolecules();
|
_allMols = await LS.biochemGetMolecules();
|
||||||
applyFilter();
|
applyFilter();
|
||||||
|
|||||||
@@ -63,6 +63,28 @@
|
|||||||
.type-chip.active { background: rgba(155,93,229,.18); border-color: rgba(155,93,229,.6); color: #c084fc; }
|
.type-chip.active { background: rgba(155,93,229,.18); border-color: rgba(155,93,229,.6); color: #c084fc; }
|
||||||
.filter-count { font-size: 0.76rem; color: #444; margin-left: auto; white-space: nowrap; font-weight: 600; }
|
.filter-count { font-size: 0.76rem; color: #444; margin-left: auto; white-space: nowrap; font-weight: 600; }
|
||||||
|
|
||||||
|
/* ── Shimmer skeleton ── */
|
||||||
|
@keyframes bc-shimmer {
|
||||||
|
0% { background-position: -200% 0; }
|
||||||
|
100% { background-position: 200% 0; }
|
||||||
|
}
|
||||||
|
.bc-sk {
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
rgba(255,255,255,0.04) 0%,
|
||||||
|
rgba(255,255,255,0.10) 50%,
|
||||||
|
rgba(255,255,255,0.04) 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: bc-shimmer 1.6s infinite;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.bc-sk-line { height: 12px; margin: 5px 0; }
|
||||||
|
.bc-sk-line.sm { width: 60%; }
|
||||||
|
.bc-sk-line.md { width: 80%; }
|
||||||
|
.bc-sk-row { display: flex; gap: 12px; padding: 14px 16px; border-bottom: 1px solid rgba(255,255,255,.05);
|
||||||
|
border-radius: 12px; margin-bottom: 10px; border: 1px solid rgba(255,255,255,.06); }
|
||||||
|
.bc-sk-row .bc-sk-avatar { width: 4px; height: auto; border-radius: 4px; flex-shrink: 0; }
|
||||||
|
.bc-sk-row .bc-sk-text { flex: 1; }
|
||||||
|
|
||||||
/* ── Scroll area ── */
|
/* ── Scroll area ── */
|
||||||
.rxn-scroll { flex: 1; overflow-y: auto; padding: 18px 20px 40px; }
|
.rxn-scroll { flex: 1; overflow-y: auto; padding: 18px 20px 40px; }
|
||||||
.rxn-scroll::-webkit-scrollbar { width: 5px; }
|
.rxn-scroll::-webkit-scrollbar { width: 5px; }
|
||||||
@@ -330,14 +352,7 @@
|
|||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="rxn-scroll" id="rxn-scroll">
|
<div class="rxn-scroll" id="rxn-scroll">
|
||||||
<div id="rxn-list">
|
<div id="rxn-list"></div>
|
||||||
<div class="rxn-empty">
|
|
||||||
<svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.3">
|
|
||||||
<path d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2V9M9 21H5a2 2 0 01-2-2V9m0 0h18"/>
|
|
||||||
</svg>
|
|
||||||
<p>Загрузка реакций…</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -464,7 +479,20 @@ let molCache = {};
|
|||||||
let filterType = '';
|
let filterType = '';
|
||||||
let filterTopic = '';
|
let filterTopic = '';
|
||||||
|
|
||||||
|
function bcSkReactions(n = 6) {
|
||||||
|
return Array.from({length: n}, () => `
|
||||||
|
<div class="bc-sk-row">
|
||||||
|
<div class="bc-sk bc-sk-avatar" style="width:4px;border-radius:4px"></div>
|
||||||
|
<div class="bc-sk-text">
|
||||||
|
<div class="bc-sk bc-sk-line md"></div>
|
||||||
|
<div class="bc-sk bc-sk-line sm"></div>
|
||||||
|
<div class="bc-sk bc-sk-line" style="width:70%;height:10px;margin-top:8px;opacity:.6"></div>
|
||||||
|
</div>
|
||||||
|
</div>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
document.getElementById('rxn-list').innerHTML = bcSkReactions(6);
|
||||||
try {
|
try {
|
||||||
allRxns = await LS.biochemGetReactions();
|
allRxns = await LS.biochemGetReactions();
|
||||||
document.getElementById('subtitle').textContent = `${allRxns.length} реакций в базе`;
|
document.getElementById('subtitle').textContent = `${allRxns.length} реакций в базе`;
|
||||||
|
|||||||
+38
-4
@@ -16,6 +16,26 @@
|
|||||||
/* ── 3D mode button ── */
|
/* ── 3D mode button ── */
|
||||||
.tool-btn.mode-3d-active { background: rgba(6,214,224,.2) !important; border-color: #06D6E0 !important; color: #06D6E0 !important; }
|
.tool-btn.mode-3d-active { background: rgba(6,214,224,.2) !important; border-color: #06D6E0 !important; color: #06D6E0 !important; }
|
||||||
|
|
||||||
|
/* ── Shimmer skeleton ── */
|
||||||
|
@keyframes bc-shimmer {
|
||||||
|
0% { background-position: -200% 0; }
|
||||||
|
100% { background-position: 200% 0; }
|
||||||
|
}
|
||||||
|
.bc-sk {
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
rgba(255,255,255,0.04) 0%,
|
||||||
|
rgba(255,255,255,0.10) 50%,
|
||||||
|
rgba(255,255,255,0.04) 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: bc-shimmer 1.6s infinite;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.bc-sk-line { height: 11px; margin: 5px 0; }
|
||||||
|
.bc-sk-line.sm { width: 55%; }
|
||||||
|
.bc-sk-line.md { width: 80%; }
|
||||||
|
.bc-sk-saved { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-bottom: 1px solid rgba(255,255,255,.05); }
|
||||||
|
.bc-sk-saved .bc-sk-fi { flex: 1; }
|
||||||
|
|
||||||
/* ── Toolbar ── */
|
/* ── Toolbar ── */
|
||||||
.bio-toolbar {
|
.bio-toolbar {
|
||||||
display: flex; align-items: center; gap: 6px;
|
display: flex; align-items: center; gap: 6px;
|
||||||
@@ -452,13 +472,13 @@
|
|||||||
<button class="chal-type-chip" data-ctype="complete" onclick="setChalFilter(this,'complete')"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> Завершить</button>
|
<button class="chal-type-chip" data-ctype="complete" onclick="setChalFilter(this,'complete')"><svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> Завершить</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:0 12px 12px">
|
<div style="padding:0 12px 12px">
|
||||||
<div id="challenges-list" style="color:#666;font-size:0.82rem">Загрузка…</div>
|
<div id="challenges-list" style="color:#666;font-size:0.82rem"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Saved pane -->
|
<!-- Saved pane -->
|
||||||
<div class="panel-pane" id="pane-saved">
|
<div class="panel-pane" id="pane-saved">
|
||||||
<div id="saved-list" style="color:#666;font-size:0.82rem">Загрузка…</div>
|
<div id="saved-list" style="color:#666;font-size:0.82rem"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1367,11 +1387,13 @@ function setChalFilter(btn, type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadChallenges() {
|
async function loadChallenges() {
|
||||||
|
const cl = document.getElementById('challenges-list');
|
||||||
|
cl.innerHTML = bcSkSaved(5);
|
||||||
try {
|
try {
|
||||||
_challenges = await LS.biochemGetChallenges();
|
_challenges = await LS.biochemGetChallenges();
|
||||||
updateChalProgress();
|
updateChalProgress();
|
||||||
renderChalList();
|
renderChalList();
|
||||||
} catch { document.getElementById('challenges-list').innerHTML = '<div style="color:#666">Ошибка загрузки</div>'; }
|
} catch { cl.innerHTML = '<div style="color:#666">Ошибка загрузки</div>'; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateChalProgress() {
|
function updateChalProgress() {
|
||||||
@@ -1586,8 +1608,20 @@ async function submitChoiceAnswer(answer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Saved ──
|
// ── Saved ──
|
||||||
|
function bcSkSaved(n = 4) {
|
||||||
|
return Array.from({length: n}, () => `
|
||||||
|
<div class="bc-sk-saved">
|
||||||
|
<div class="bc-sk-fi">
|
||||||
|
<div class="bc-sk bc-sk-line md"></div>
|
||||||
|
<div class="bc-sk bc-sk-line sm"></div>
|
||||||
|
</div>
|
||||||
|
<div class="bc-sk" style="width:18px;height:18px;border-radius:4px"></div>
|
||||||
|
</div>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
async function loadSaved() {
|
async function loadSaved() {
|
||||||
const list = document.getElementById('saved-list');
|
const list = document.getElementById('saved-list');
|
||||||
|
list.innerHTML = bcSkSaved(4);
|
||||||
try {
|
try {
|
||||||
const saved = await LS.biochemGetSaved();
|
const saved = await LS.biochemGetSaved();
|
||||||
if (!saved.length) { list.innerHTML = '<div style="color:#555;font-size:0.8rem">Сохранённых молекул пока нет</div>'; return; }
|
if (!saved.length) { list.innerHTML = '<div style="color:#555;font-size:0.8rem">Сохранённых молекул пока нет</div>'; return; }
|
||||||
@@ -2007,7 +2041,7 @@ window.addEventListener('keydown', e => {
|
|||||||
if (hoveredAtomId) { pushHistory(); removeAtom(hoveredAtomId); hoveredAtomId=null; updateInfo(); render(); }
|
if (hoveredAtomId) { pushHistory(); removeAtom(hoveredAtomId); hoveredAtomId=null; updateInfo(); render(); }
|
||||||
else if (hoveredBondId) { pushHistory(); removeBond(hoveredBondId); hoveredBondId=null; updateInfo(); render(); }
|
else if (hoveredBondId) { pushHistory(); removeBond(hoveredBondId); hoveredBondId=null; updateInfo(); render(); }
|
||||||
}
|
}
|
||||||
if (e.key === 'Escape') { cancelChallenge(); bondFromId=null; closeRingMenu(); render(); }
|
if (e.key === 'Escape') { cancelChallenge(); bondFromId=null; closeRingMenu(); if (_is3D) toggle3D(); render(); }
|
||||||
if (e.ctrlKey && !e.shiftKey && e.key.toLowerCase()==='z') { e.preventDefault(); undo(); }
|
if (e.ctrlKey && !e.shiftKey && e.key.toLowerCase()==='z') { e.preventDefault(); undo(); }
|
||||||
if ((e.ctrlKey && e.shiftKey && e.key.toLowerCase()==='z') || (e.ctrlKey && e.key.toLowerCase()==='y')) { e.preventDefault(); redo(); }
|
if ((e.ctrlKey && e.shiftKey && e.key.toLowerCase()==='z') || (e.ctrlKey && e.key.toLowerCase()==='y')) { e.preventDefault(); redo(); }
|
||||||
// Element shortcuts
|
// Element shortcuts
|
||||||
|
|||||||
Reference in New Issue
Block a user