358b761eb2
Проблема: динамическая вставка через JS вызывала мигание (nav появлялся через ~100ms после первого пейнта). Решение: nav — статичный HTML в каждой странице, CSS — в <head>. Активная вкладка проставлена в HTML (class bsn-active) — нет JS, нет мигания, работает с первого байта. Редизайн .biochem-subnav: - frosted glass (backdrop-filter blur 14px, rgba 0.92) - активная вкладка: фиолетовый фон-пилюля + нижняя линия 2.5px - hover: мягкий фиолетовый фон - mobile <560px: только иконки (bsn-label display:none) - overflow-x auto + scrollbar-width:none — горизонтальная прокрутка без полосы - biochem-nav.js сведён к no-op комментарию Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
658 lines
31 KiB
HTML
658 lines
31 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Библиотека молекул — LearnSpace</title>
|
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
|
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
<link rel="stylesheet" href="/css/ls.css" />
|
|
<style>
|
|
html, body { height: 100%; overflow: hidden; }
|
|
.app-layout { height: 100vh; overflow: hidden; }
|
|
.sb-content { padding: 0 !important; overflow: hidden; display: flex; flex-direction: column; background: #07070f; }
|
|
.sb-sub-link { padding-left: 28px !important; font-size: 0.76rem !important; opacity: .75; }
|
|
.sb-sub-link:hover { opacity: 1; }
|
|
|
|
/* ── Page header ── */
|
|
.page-header {
|
|
padding: 22px 28px 18px;
|
|
background: linear-gradient(135deg, rgba(155,93,229,.12) 0%, rgba(6,214,224,.06) 100%);
|
|
border-bottom: 1px solid rgba(155,93,229,.15);
|
|
flex-shrink: 0;
|
|
display: flex; align-items: center; gap: 16px;
|
|
}
|
|
.page-header-icon {
|
|
width: 46px; height: 46px; border-radius: 14px;
|
|
background: rgba(6,214,224,.12); border: 1.5px solid rgba(6,214,224,.2);
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 1.4rem; flex-shrink: 0;
|
|
}
|
|
.page-title {
|
|
font-family: 'Unbounded', sans-serif; font-size: 1.05rem; font-weight: 800;
|
|
color: #f0f0ff; margin-bottom: 3px;
|
|
background: linear-gradient(135deg,#c084fc,#06D6E0); -webkit-background-clip:text; -webkit-text-fill-color:transparent;
|
|
}
|
|
.page-subtitle { font-size: 0.8rem; color: #666; }
|
|
|
|
/* ── Filters ── */
|
|
.filters-row {
|
|
display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
|
|
padding: 12px 20px;
|
|
background: rgba(7,7,20,0.9);
|
|
border-bottom: 1px solid rgba(255,255,255,.06);
|
|
flex-shrink: 0;
|
|
}
|
|
.filter-input {
|
|
flex: 1; min-width: 200px; max-width: 280px;
|
|
padding: 8px 14px; border-radius: 10px;
|
|
background: rgba(255,255,255,.05); border: 1.5px solid rgba(255,255,255,.09);
|
|
color: #ddd; font-family: 'Manrope', sans-serif; font-size: 0.83rem;
|
|
outline: none; transition: border-color .18s;
|
|
}
|
|
.filter-input:focus { border-color: rgba(155,93,229,.5); background: rgba(155,93,229,.05); }
|
|
.filter-input::placeholder { color: #444; }
|
|
.filter-select {
|
|
padding: 7px 11px; border-radius: 10px;
|
|
background: rgba(255,255,255,.05); border: 1.5px solid rgba(255,255,255,.09);
|
|
color: #aaa; font-family: 'Manrope', sans-serif; font-size: 0.81rem;
|
|
cursor: pointer; outline: none; transition: border-color .18s;
|
|
}
|
|
.filter-select:focus { border-color: rgba(155,93,229,.5); }
|
|
.filter-select option { background: #10101e; }
|
|
.filter-chip {
|
|
padding: 5px 13px; border-radius: 999px;
|
|
border: 1.5px solid rgba(255,255,255,.08);
|
|
background: rgba(255,255,255,.03); color: #666;
|
|
font-family: 'Manrope', sans-serif; font-size: 0.76rem; font-weight: 700;
|
|
cursor: pointer; transition: all .16s; white-space: nowrap;
|
|
}
|
|
.filter-chip:hover { border-color: rgba(155,93,229,.35); color: #ccc; background: rgba(155,93,229,.06); }
|
|
.filter-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; }
|
|
|
|
/* ── Main area ── */
|
|
.lib-main { flex: 1; display: flex; overflow: hidden; min-height: 0; }
|
|
.lib-scroll { flex: 1; overflow-y: auto; padding: 20px; }
|
|
.lib-scroll::-webkit-scrollbar { width: 5px; }
|
|
.lib-scroll::-webkit-scrollbar-thumb { background: rgba(155,93,229,.3); border-radius: 4px; }
|
|
|
|
/* ── Grid ── */
|
|
.lib-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
|
gap: 14px;
|
|
}
|
|
|
|
/* ── Molecule card ── */
|
|
.mol-card {
|
|
border-radius: 16px;
|
|
border: 1.5px solid rgba(255,255,255,.07);
|
|
background: rgba(255,255,255,.03);
|
|
overflow: hidden; cursor: pointer;
|
|
transition: transform .2s, border-color .2s, box-shadow .2s;
|
|
display: flex; flex-direction: column;
|
|
animation: cardIn .4s ease both;
|
|
animation-delay: calc(var(--i, 0) * 40ms);
|
|
}
|
|
@keyframes cardIn { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:none; } }
|
|
.mol-card:hover {
|
|
transform: translateY(-3px);
|
|
border-color: rgba(155,93,229,.4);
|
|
box-shadow: 0 8px 28px rgba(0,0,0,.4), 0 0 0 1px rgba(155,93,229,.15);
|
|
}
|
|
.mol-card.selected {
|
|
border-color: rgba(6,214,224,.5);
|
|
box-shadow: 0 0 0 1px rgba(6,214,224,.2), 0 8px 28px rgba(6,214,224,.08);
|
|
}
|
|
|
|
/* canvas preview */
|
|
.mol-thumb-wrap {
|
|
width: 100%; aspect-ratio: 4/3;
|
|
background: radial-gradient(ellipse at 40% 35%, rgba(155,93,229,.07) 0%, rgba(5,5,15,0.95) 70%);
|
|
position: relative; overflow: hidden;
|
|
}
|
|
.mol-thumb-wrap canvas {
|
|
position: absolute; inset: 0; width: 100%; height: 100%;
|
|
}
|
|
.mol-thumb-placeholder {
|
|
position: absolute; inset: 0;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 2.2rem; opacity: .18;
|
|
}
|
|
/* Category ribbon in top-right corner */
|
|
.mol-cat-ribbon {
|
|
position: absolute; top: 8px; right: 8px;
|
|
font-size: 0.62rem; font-weight: 700; padding: 2px 8px; border-radius: 999px;
|
|
letter-spacing: .04em; text-transform: uppercase;
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
.mol-cat-ribbon.inorganic { background: rgba(96,165,250,.2); color: #93c5fd; border: 1px solid rgba(96,165,250,.25); }
|
|
.mol-cat-ribbon.organic { background: rgba(52,211,153,.18); color: #6ee7b7; border: 1px solid rgba(52,211,153,.25); }
|
|
.mol-cat-ribbon.biomolecule{ background: rgba(251,191,36,.18); color: #fde68a; border: 1px solid rgba(251,191,36,.25); }
|
|
|
|
/* card body */
|
|
.mol-card-body { padding: 11px 13px 13px; display: flex; flex-direction: column; gap: 2px; flex: 1; }
|
|
.mol-formula {
|
|
font-family: 'Unbounded', monospace; font-size: 0.95rem; font-weight: 800;
|
|
background: linear-gradient(135deg,#a78bfa,#06D6E0);
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
|
letter-spacing: .01em; margin-bottom: 1px;
|
|
}
|
|
.mol-name { font-size: 0.8rem; color: #bbb; font-weight: 600; line-height: 1.3; }
|
|
.mol-name-lat { font-size: 0.7rem; color: #555; font-style: italic; }
|
|
.mol-card-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; }
|
|
.mol-diff {
|
|
font-size: 0.7rem; letter-spacing: .06em;
|
|
color: #facc15; text-shadow: 0 0 6px rgba(250,204,21,.4);
|
|
}
|
|
.mol-open-btn {
|
|
width: 100%; padding: 7px; margin-top: 7px;
|
|
background: rgba(155,93,229,.1); border: 1px solid rgba(155,93,229,.25);
|
|
border-radius: 9px; color: #a78bfa;
|
|
font-family: 'Manrope', sans-serif; font-size: 0.74rem; font-weight: 700;
|
|
cursor: pointer; transition: all .16s; text-align: center;
|
|
}
|
|
.mol-open-btn:hover { background: rgba(155,93,229,.22); border-color: rgba(155,93,229,.5); color: #c084fc; }
|
|
|
|
/* ── Detail panel ── */
|
|
.mol-detail {
|
|
width: 280px; flex-shrink: 0;
|
|
background: rgba(6,6,18,0.96);
|
|
border-left: 1px solid rgba(155,93,229,.12);
|
|
display: none; flex-direction: column;
|
|
overflow-y: auto;
|
|
}
|
|
.mol-detail.visible { display: flex; }
|
|
.mol-detail::-webkit-scrollbar { width: 4px; }
|
|
.mol-detail::-webkit-scrollbar-thumb { background: rgba(155,93,229,.25); border-radius: 4px; }
|
|
|
|
.detail-header {
|
|
padding: 18px 18px 14px;
|
|
background: linear-gradient(160deg, rgba(155,93,229,.1), rgba(6,214,224,.05));
|
|
border-bottom: 1px solid rgba(255,255,255,.06);
|
|
}
|
|
.detail-formula {
|
|
font-family: 'Unbounded', monospace; font-size: 1.3rem; font-weight: 800; margin-bottom: 4px;
|
|
background: linear-gradient(135deg,#c084fc,#06D6E0);
|
|
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
|
}
|
|
.detail-name { font-size: 0.88rem; color: #ddd; font-weight: 700; }
|
|
.detail-lat { font-size: 0.74rem; color: #555; font-style: italic; margin-top: 2px; }
|
|
|
|
.detail-canvas-wrap {
|
|
margin: 14px;
|
|
border-radius: 14px; overflow: hidden;
|
|
background: radial-gradient(ellipse at 40% 35%, rgba(155,93,229,.08) 0%, rgba(5,5,15,.98) 70%);
|
|
border: 1px solid rgba(255,255,255,.07);
|
|
aspect-ratio: 1; position: relative;
|
|
}
|
|
.detail-canvas-wrap canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
|
|
|
|
.detail-section { padding: 10px 18px; border-bottom: 1px solid rgba(255,255,255,.04); }
|
|
.detail-label { font-size: 0.63rem; color: #555; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; margin-bottom: 4px; }
|
|
.detail-value { font-size: 0.81rem; color: #bbb; line-height: 1.55; }
|
|
.detail-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 4px; }
|
|
.detail-tag {
|
|
font-size: 0.65rem; padding: 3px 9px; border-radius: 999px;
|
|
background: rgba(167,139,250,.1); color: #a78bfa; border: 1px solid rgba(167,139,250,.2);
|
|
font-weight: 600;
|
|
}
|
|
.detail-open-btn {
|
|
margin: 14px; padding: 11px;
|
|
background: linear-gradient(135deg, rgba(155,93,229,.25), rgba(6,214,224,.15));
|
|
border: 1.5px solid rgba(155,93,229,.4);
|
|
border-radius: 12px; color: #c084fc;
|
|
font-family: 'Manrope', sans-serif; font-size: 0.83rem; font-weight: 700;
|
|
cursor: pointer; transition: all .18s; text-align: center;
|
|
display: block; width: calc(100% - 28px); text-decoration: none;
|
|
}
|
|
.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 */
|
|
.lib-empty {
|
|
grid-column: 1/-1;
|
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
padding: 70px 20px; gap: 12px; color: #444;
|
|
}
|
|
.lib-empty svg { opacity: .15; }
|
|
.lib-empty p { font-size: 0.86rem; font-weight: 600; }
|
|
|
|
/* Reaction items in detail panel */
|
|
.det-rxn-item {
|
|
padding: 7px 10px;
|
|
border-radius: 9px;
|
|
background: rgba(255,255,255,.03);
|
|
border: 1px solid rgba(255,255,255,.06);
|
|
margin-bottom: 5px;
|
|
cursor: pointer;
|
|
transition: border-color .15s, background .15s;
|
|
}
|
|
.det-rxn-item:last-child { margin-bottom: 0; }
|
|
.det-rxn-item:hover { border-color: rgba(6,214,224,.3); background: rgba(6,214,224,.04); }
|
|
.det-rxn-name { font-size: 0.76rem; color: #bbb; font-weight: 600; margin-bottom: 2px; }
|
|
.det-rxn-eq { font-size: 0.68rem; color: #555; font-family: 'Unbounded', monospace; line-height: 1.4; word-break: break-all; }
|
|
.det-rxn-type { display: inline-block; font-size: 0.6rem; padding: 1px 6px; border-radius: 999px;
|
|
background: rgba(155,93,229,.12); color: #a78bfa; border: 1px solid rgba(155,93,229,.2);
|
|
font-weight: 700; margin-top: 3px; }
|
|
|
|
/* Mobile */
|
|
@media (max-width: 1100px) { .mol-detail { display: none !important; } }
|
|
@media (max-width: 768px) {
|
|
html, body { height: auto; overflow: auto; }
|
|
.app-layout { height: auto; overflow: visible; }
|
|
.sb-content { overflow: visible !important; height: auto !important; }
|
|
.lib-main { flex-direction: column; overflow: visible; }
|
|
.lib-scroll { padding: 12px; overflow: visible; }
|
|
.lib-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
|
|
.filters-row { padding: 10px 12px; gap: 6px; flex-wrap: wrap; }
|
|
.filter-input { min-width: 0; max-width: none; flex: 1 1 100%; }
|
|
.page-header { padding: 14px 12px; gap: 10px; }
|
|
.page-header-icon { width: 36px; height: 36px; font-size: 1.1rem; border-radius: 10px; }
|
|
.page-title { font-size: 0.88rem; }
|
|
.mol-card { border-radius: 12px; }
|
|
.mol-name { font-size: 0.78rem; }
|
|
}
|
|
@media (max-width: 480px) {
|
|
.lib-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; }
|
|
.page-header { padding: 10px; }
|
|
.filters-row { padding: 8px 10px; }
|
|
}
|
|
|
|
/* ── Biochem subnav ─────────────────────────────────────────────── */
|
|
.biochem-subnav {
|
|
display: flex; align-items: center; gap: 2px;
|
|
padding: 0 16px;
|
|
background: rgba(255,255,255,0.92);
|
|
backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px);
|
|
border-bottom: 1.5px solid rgba(15,23,42,0.07);
|
|
flex-shrink: 0; overflow-x: auto;
|
|
scrollbar-width: none; position: relative;
|
|
}
|
|
.biochem-subnav::-webkit-scrollbar { display: none; }
|
|
.biochem-subnav::after {
|
|
content: ''; position: absolute; bottom: -1px; left: 0; right: 0;
|
|
height: 1.5px; background: rgba(15,23,42,0.07);
|
|
}
|
|
.bsn-tab {
|
|
display: inline-flex; align-items: center; gap: 7px;
|
|
padding: 11px 14px; border-radius: 9px; margin: 5px 1px;
|
|
font-family: 'Manrope', sans-serif; font-size: 0.82rem; font-weight: 600;
|
|
color: var(--text-3, #56687A); text-decoration: none; white-space: nowrap;
|
|
transition: background .15s, color .15s; position: relative;
|
|
}
|
|
.bsn-tab svg { stroke: currentColor; width: 15px; height: 15px; flex-shrink: 0; fill: none; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
|
|
.bsn-tab:hover { background: rgba(155,93,229,0.08); color: #9B5DE5; }
|
|
.bsn-active {
|
|
background: rgba(155,93,229,0.10); color: #7c3aed; font-weight: 700;
|
|
}
|
|
.bsn-active::after {
|
|
content: ''; position: absolute; bottom: -5px; left: 14px; right: 14px;
|
|
height: 2.5px; border-radius: 99px; background: #9B5DE5;
|
|
}
|
|
@media (max-width: 560px) {
|
|
.biochem-subnav { padding: 0 6px; }
|
|
.bsn-tab { padding: 10px 10px; gap: 0; }
|
|
.bsn-tab .bsn-label { display: none; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app-layout" id="app">
|
|
<aside class="sidebar" id="app-sidebar"></aside>
|
|
<div class="notif-drop" id="notif-drop"></div>
|
|
|
|
<div class="sb-content">
|
|
<nav class="biochem-subnav" aria-label="Разделы биохимии">
|
|
<a class="bsn-tab" href="/biochem"><svg viewBox="0 0 24 24"><path d="M9 3h6M10 3v6l-5.4 9.3A1.5 1.5 0 0 0 5.9 21h12.2a1.5 1.5 0 0 0 1.3-2.3L14 9V3M7.5 15h9"/></svg><span class="bsn-label">Редактор</span></a><a class="bsn-tab bsn-active" href="/biochem-library" aria-current="page"><svg viewBox="0 0 24 24"><path d="M4 5a2 2 0 0 1 2-2h6v17H6a2 2 0 0 0-2 2z M20 5a2 2 0 0 0-2-2h-6v17h6a2 2 0 0 1 2 2z"/></svg><span class="bsn-label">Библиотека</span></a><a class="bsn-tab" href="/biochem-reactions"><svg viewBox="0 0 24 24"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg><span class="bsn-label">Реакции</span></a><a class="bsn-tab" href="/biochem-properties"><svg viewBox="0 0 24 24"><path d="M9 17H7A5 5 0 0 1 7 7h2m6 10h2a5 5 0 0 0 0-10h-2m-6 5h6"/></svg><span class="bsn-label">Свойства</span></a><a class="bsn-tab" href="/biochem-pathways"><svg viewBox="0 0 24 24"><path d="M6 18h8M3 22h18M8 22V12l4-10 4 10v10M10 9h4"/></svg><span class="bsn-label">Пути</span></a>
|
|
</nav>
|
|
<!-- Header -->
|
|
<div class="page-header">
|
|
<div class="page-header-icon"><svg class="ic" viewBox="0 0 24 24"><path d="M9 3h6m-4.5 0v5.5l-4 7.5a1 1 0 0 0 .9 1.5h8.2a1 1 0 0 0 .9-1.5l-4-7.5V3"/></svg></div>
|
|
<div>
|
|
<div class="page-title">Библиотека молекул</div>
|
|
<div class="page-subtitle" id="subtitle">Загрузка…</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="filters-row">
|
|
<input type="search" class="filter-input" id="search-q" placeholder="Поиск по названию или формуле..." oninput="applyFilters()" />
|
|
<select class="filter-select" id="filter-cat" onchange="applyFilters()">
|
|
<option value="">Все категории</option>
|
|
<option value="inorganic">Неорганика</option>
|
|
<option value="organic">Органика</option>
|
|
<option value="biomolecule">Биомолекулы</option>
|
|
</select>
|
|
<button class="filter-chip" id="diff-0" onclick="setDiff(0)">Все</button>
|
|
<button class="filter-chip" id="diff-1" onclick="setDiff(1)"><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> Лёгкие</button>
|
|
<button class="filter-chip" id="diff-2" onclick="setDiff(2)"><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> Средние</button>
|
|
<button class="filter-chip" id="diff-3" onclick="setDiff(3)"><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> Сложные</button>
|
|
<span class="filter-count" id="filter-count"></span>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="lib-main">
|
|
<div class="lib-scroll" id="lib-scroll">
|
|
<div class="lib-grid" id="lib-grid"></div>
|
|
</div>
|
|
|
|
<!-- Detail panel -->
|
|
<div class="mol-detail" id="mol-detail">
|
|
<div class="detail-header">
|
|
<div class="detail-formula" id="det-formula">—</div>
|
|
<div class="detail-name" id="det-name">—</div>
|
|
<div class="detail-lat" id="det-lat"></div>
|
|
</div>
|
|
<div class="detail-canvas-wrap" style="position:relative">
|
|
<canvas id="det-canvas"></canvas>
|
|
<button id="det-3d-toggle" onclick="toggleDet3D()" title="2D / 3D"
|
|
style="position:absolute;top:8px;right:8px;z-index:2;height:26px;padding:0 10px;border-radius:7px;
|
|
border:1.5px solid rgba(255,255,255,.15);background:rgba(15,15,30,.85);color:#aaa;
|
|
font:700 .72rem Manrope,sans-serif;cursor:pointer">3D</button>
|
|
<div id="det-geom" style="position:absolute;bottom:6px;left:8px;right:8px;font:600 .66rem Manrope,sans-serif;color:#8aa;display:none;text-align:center"></div>
|
|
</div>
|
|
<div class="detail-section">
|
|
<div class="detail-label">Категория</div>
|
|
<div class="detail-value" id="det-cat">—</div>
|
|
</div>
|
|
<div class="detail-section" id="det-desc-sec" style="display:none">
|
|
<div class="detail-label">Описание</div>
|
|
<div class="detail-value" id="det-desc">—</div>
|
|
</div>
|
|
<div class="detail-section" id="det-tags-sec" style="display:none">
|
|
<div class="detail-label">Темы</div>
|
|
<div class="detail-tags" id="det-tags"></div>
|
|
</div>
|
|
<div class="detail-section" id="det-rxn-sec" style="display:none">
|
|
<div class="detail-label">Реакции</div>
|
|
<div id="det-rxn-list"></div>
|
|
</div>
|
|
<button class="detail-open-btn" id="det-open-btn" onclick="openInEditor()">
|
|
Открыть в конструкторе <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/js/api.js"></script>
|
|
<script src="/js/biochem-core.js"></script>
|
|
<script src="/js/sidebar.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
|
<script>
|
|
'use strict';
|
|
const { user, isTeacher, isAdmin } = LS.initPage();
|
|
if (!user) location.href = '/login';
|
|
|
|
const nav = document.getElementById('nav-user');
|
|
const ava = document.getElementById('nav-avatar');
|
|
if (nav) nav.textContent = user?.name?.split(' ')[0] || 'Пользователь';
|
|
LS.renderNavAvatar(ava, user);
|
|
if (isAdmin) document.getElementById('btn-admin').style.display = '';
|
|
if (isTeacher) document.getElementById('btn-classes').style.display = '';
|
|
LS.showBoardIfAllowed();
|
|
|
|
// ── Molecule thumbnail renderer (delegates to shared BIO.render2D) ──
|
|
function renderMolThumb(canvas, atoms, bonds) {
|
|
const W = canvas.width, H = canvas.height;
|
|
const ctx = canvas.getContext('2d');
|
|
if (atoms && atoms.length) {
|
|
BIO.render2D(ctx, atoms, bonds || [], { fit: true, padding: Math.min(W, H) * 0.16 });
|
|
return;
|
|
}
|
|
// flask outline fallback
|
|
ctx.clearRect(0, 0, W, H);
|
|
const fw = Math.min(W, H) * 0.32, fx = W/2, fy = H/2 + fw*0.05;
|
|
ctx.strokeStyle = '#555';
|
|
ctx.lineWidth = Math.max(1.5, fw * 0.07);
|
|
ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
|
ctx.beginPath();
|
|
ctx.moveTo(fx - fw*0.22, fy - fw*0.52);
|
|
ctx.lineTo(fx - fw*0.22, fy - fw*0.08);
|
|
ctx.lineTo(fx - fw*0.55, fy + fw*0.42);
|
|
ctx.quadraticCurveTo(fx, fy + fw*0.62, fx + fw*0.55, fy + fw*0.42);
|
|
ctx.lineTo(fx + fw*0.22, fy - fw*0.08);
|
|
ctx.lineTo(fx + fw*0.22, fy - fw*0.52);
|
|
ctx.moveTo(fx - fw*0.32, fy - fw*0.52);
|
|
ctx.lineTo(fx + fw*0.32, fy - fw*0.52);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// ── Data ──
|
|
let allMols = [];
|
|
let allReactions = [];
|
|
let filterDiff = 0;
|
|
let selectedId = null;
|
|
|
|
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>'];
|
|
|
|
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() {
|
|
document.getElementById('lib-grid').innerHTML = bcSkLibrary(12);
|
|
try {
|
|
[allMols, allReactions] = await Promise.all([
|
|
LS.biochemGetMolecules(),
|
|
LS.biochemGetReactions().catch(() => []),
|
|
]);
|
|
document.getElementById('subtitle').textContent = `${allMols.length} молекул в базе`;
|
|
applyFilters();
|
|
} catch(e) {
|
|
document.getElementById('lib-grid').innerHTML = `<div class="lib-empty"><p>Ошибка загрузки: ${e.message}</p></div>`;
|
|
}
|
|
}
|
|
|
|
function setDiff(d) {
|
|
filterDiff = d;
|
|
document.querySelectorAll('.filter-chip').forEach((btn, i) => btn.classList.toggle('active', i === d));
|
|
applyFilters();
|
|
}
|
|
|
|
function applyFilters() {
|
|
const q = document.getElementById('search-q').value.toLowerCase().trim();
|
|
const cat = document.getElementById('filter-cat').value;
|
|
const filtered = allMols.filter(m => {
|
|
if (q && !m.name_ru.toLowerCase().includes(q) && !(m.formula||'').toLowerCase().includes(q) &&
|
|
!(m.name_lat||'').toLowerCase().includes(q)) return false;
|
|
if (cat && m.category !== cat) return false;
|
|
if (filterDiff && m.difficulty !== filterDiff) return false;
|
|
return true;
|
|
});
|
|
document.getElementById('filter-count').textContent = `${filtered.length} молекул`;
|
|
renderGrid(filtered);
|
|
}
|
|
|
|
function renderGrid(mols) {
|
|
const grid = document.getElementById('lib-grid');
|
|
if (!mols.length) {
|
|
grid.innerHTML = `<div class="lib-empty">
|
|
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.3">
|
|
<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>
|
|
</svg>
|
|
<p>Ничего не найдено</p></div>`;
|
|
return;
|
|
}
|
|
grid.innerHTML = '';
|
|
mols.forEach((m, i) => {
|
|
const card = document.createElement('div');
|
|
card.className = 'mol-card';
|
|
card.style.setProperty('--i', i);
|
|
card.dataset.id = m.id;
|
|
if (m.id == selectedId) card.classList.add('selected');
|
|
|
|
card.innerHTML = `
|
|
<div class="mol-thumb-wrap">
|
|
<canvas class="mol-thumb" width="200" height="150"></canvas>
|
|
${(!m.atoms_json||!m.atoms_json.length)?'<div class="mol-thumb-placeholder"><svg class="ic" viewBox="0 0 24 24"><path d="M9 3h6m-4.5 0v5.5l-4 7.5a1 1 0 0 0 .9 1.5h8.2a1 1 0 0 0 .9-1.5l-4-7.5V3"/></svg></div>':''}
|
|
${m.category?`<div class="mol-cat-ribbon ${m.category||''}">${CAT_LABELS[m.category]||m.category}</div>`:''}
|
|
</div>
|
|
<div class="mol-card-body">
|
|
<div class="mol-formula">${m.formula}</div>
|
|
<div class="mol-name">${m.name_ru}</div>
|
|
${m.name_lat?`<div class="mol-name-lat">${m.name_lat}</div>`:''}
|
|
<div class="mol-card-foot">
|
|
<span class="mol-diff">${DIFF_STARS[m.difficulty]||''}</span>
|
|
</div>
|
|
<button class="mol-open-btn" onclick="event.stopPropagation();openInEditorId(${m.id})">
|
|
Открыть в конструкторе <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
|
|
</button>
|
|
</div>`;
|
|
|
|
card.addEventListener('click', () => selectMol(m.id));
|
|
grid.appendChild(card);
|
|
|
|
requestAnimationFrame(() => {
|
|
const cvs = card.querySelector('.mol-thumb');
|
|
if (cvs && m.atoms_json && m.atoms_json.length) {
|
|
renderMolThumb(cvs, m.atoms_json, m.bonds_json || []);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function selectMol(id) {
|
|
selectedId = id;
|
|
document.querySelectorAll('.mol-card').forEach(c => c.classList.toggle('selected', c.dataset.id == id));
|
|
|
|
const det = document.getElementById('mol-detail');
|
|
det.classList.add('visible');
|
|
|
|
try {
|
|
const m = await LS.biochemGetMolecule(id);
|
|
document.getElementById('det-formula').textContent = m.formula;
|
|
document.getElementById('det-name').textContent = m.name_ru;
|
|
document.getElementById('det-lat').textContent = m.name_lat || '';
|
|
document.getElementById('det-cat').textContent = CAT_LABELS[m.category] || m.category || '—';
|
|
|
|
const descSec = document.getElementById('det-desc-sec');
|
|
if (m.description) { document.getElementById('det-desc').textContent = m.description; descSec.style.display = ''; }
|
|
else descSec.style.display = 'none';
|
|
|
|
const tagsSec = document.getElementById('det-tags-sec');
|
|
const tags = Array.isArray(m.topic_tags) ? m.topic_tags : [];
|
|
if (tags.length) {
|
|
document.getElementById('det-tags').innerHTML = tags.map(t=>`<span class="detail-tag">${t}</span>`).join('');
|
|
tagsSec.style.display = '';
|
|
} else tagsSec.style.display = 'none';
|
|
|
|
// reactions where this molecule participates
|
|
const molRxns = allReactions.filter(r =>
|
|
(r.reactant_ids||[]).includes(m.id) || (r.product_ids||[]).includes(m.id)
|
|
);
|
|
const rxnSec = document.getElementById('det-rxn-sec');
|
|
const rxnList = document.getElementById('det-rxn-list');
|
|
if (molRxns.length) {
|
|
rxnList.innerHTML = molRxns.map(r => `
|
|
<div class="det-rxn-item" onclick="location.href='/biochem-reactions'">
|
|
<div class="det-rxn-name">${r.name_ru||'—'}</div>
|
|
<div class="det-rxn-eq">${r.equation||''}</div>
|
|
${r.type ? `<span class="det-rxn-type">${r.type}</span>` : ''}
|
|
</div>`).join('');
|
|
rxnSec.style.display = '';
|
|
} else {
|
|
rxnList.innerHTML = '<div style="font-size:.75rem;color:#444;padding:2px 0">Реакций не найдено</div>';
|
|
rxnSec.style.display = '';
|
|
}
|
|
|
|
document.getElementById('det-open-btn').dataset.id = id;
|
|
|
|
_detMol = m;
|
|
const cvs = document.getElementById('det-canvas');
|
|
cvs.width = cvs.offsetWidth || 252;
|
|
cvs.height = cvs.offsetHeight || 252;
|
|
_renderDetail();
|
|
} catch(e) {
|
|
document.getElementById('det-name').textContent = 'Ошибка загрузки';
|
|
}
|
|
}
|
|
|
|
// ── Detail 2D/3D view ──
|
|
let _detMol = null, _det3D = false, _det3dRotY = 0.4, _det3dRotX = 0.35, _detAnim = null;
|
|
function _stopDetAnim() { if (_detAnim) { cancelAnimationFrame(_detAnim); _detAnim = null; } }
|
|
|
|
function _renderDetail() {
|
|
_stopDetAnim();
|
|
const cvs = document.getElementById('det-canvas');
|
|
if (!cvs || !_detMol) return;
|
|
const ctx = cvs.getContext('2d');
|
|
const atoms = _detMol.atoms_json || [], bonds = _detMol.bonds_json || [];
|
|
const geomEl = document.getElementById('det-geom');
|
|
const tgl = document.getElementById('det-3d-toggle');
|
|
|
|
if (!_det3D || !atoms.length) {
|
|
geomEl.style.display = 'none';
|
|
if (tgl) { tgl.textContent = '3D'; tgl.style.color = '#aaa'; }
|
|
renderMolThumb(cvs, atoms, bonds);
|
|
return;
|
|
}
|
|
|
|
// 3D: build VSEPR geometry once, spin it
|
|
const g = BIO.vsepr(atoms, bonds);
|
|
if (g.shape) {
|
|
geomEl.style.display = 'block';
|
|
geomEl.textContent = [g.shape, g.hybridization, g.angle != null ? g.angle + '°' : '']
|
|
.filter(Boolean).join(' · ');
|
|
}
|
|
if (tgl) { tgl.textContent = '2D'; tgl.style.color = '#06D6E0'; }
|
|
let ext = 1;
|
|
for (const a of g.atoms3d) ext = Math.max(ext, Math.hypot(a.x, a.y, a.z));
|
|
const sc = Math.max(0.4, Math.min(3, (Math.min(cvs.width, cvs.height) * 0.40) / (ext * 1.6 + 30)));
|
|
const frame = () => {
|
|
if (!_det3D) return;
|
|
_det3dRotY += 0.012;
|
|
BIO.render3D(ctx, g.atoms3d, bonds, { rotX: _det3dRotX, rotY: _det3dRotY, scale: sc, W: cvs.width, H: cvs.height }, { vdw: false, bg: '#0a0a16' });
|
|
_detAnim = requestAnimationFrame(frame);
|
|
};
|
|
frame();
|
|
}
|
|
|
|
function toggleDet3D() { _det3D = !_det3D; _renderDetail(); }
|
|
|
|
function openInEditor() {
|
|
const id = document.getElementById('det-open-btn').dataset.id;
|
|
if (id) openInEditorId(id);
|
|
}
|
|
function openInEditorId(id) { location.href = `/biochem?mol=${id}`; }
|
|
|
|
// ── Boot ──
|
|
document.getElementById('diff-0').classList.add('active');
|
|
if (window.lucide) lucide.createIcons();
|
|
LS.notif?.init();
|
|
LS.hideDisabledFeatures?.();
|
|
init();
|
|
</script>
|
|
<script src="/js/notifications.js"></script>
|
|
<script src="/js/mobile.js"></script>
|
|
<script src="/js/biochem-nav.js"></script>
|
|
</body>
|
|
</html>
|