Вендоры в корзине МОНТ
Актуальная матрица вендоров, продуктов и категорий. Выбирайте вендоров или категории, чтобы видеть релевантные продуктовые линейки в Инфраструктуре и ИБ.
-diff --git a/favicon.png b/favicon.png index 68f0026..ad5ffbf 100644 Binary files a/favicon.png and b/favicon.png differ diff --git a/static/css/index.css b/static/css/index.css index 491b091..2488bf5 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -26,37 +26,39 @@ } body.scope-ib { background: - radial-gradient(1200px 700px at -10% -10%, #ffb3b3 0%, transparent 58%), - radial-gradient(900px 500px at 110% -20%, #ffbeb3 0%, transparent 52%), - linear-gradient(160deg, #ffd7d7 0%, #ffb8b8 100%); + radial-gradient(1200px 700px at -10% -10%, #b8f3cf 0%, transparent 58%), + radial-gradient(900px 500px at 110% -20%, #b2efd6 0%, transparent 52%), + linear-gradient(160deg, #daf8e8 0%, #bdeecf 100%); } body.scope-ib .hero { background: linear-gradient(160deg, rgba(255,255,255,.09), rgba(255,255,255,0) 45%), - linear-gradient(125deg, #7a1f2a 0%, #b43444 55%, #d34d57 100%); + linear-gradient(125deg, #1f7a4a 0%, #2f9c61 55%, #47b879 100%); } body.scope-ib .hero::after { - background-color: rgba(90, 15, 28, .45); + background-color: rgba(10, 74, 45, .42); } body.scope-ib .hero::before { background: linear-gradient(to top, rgba(255,255,255,.9) 0 2px, rgba(255,255,255,0) 2px), - rgba(74, 9, 24, .42); + rgba(10, 61, 38, .40); } body.scope-ib .mode-btn.active { - background: linear-gradient(140deg, #9a2331, #c03d4c); - box-shadow: 0 8px 18px rgba(158, 33, 51, .35); + background: linear-gradient(140deg, #1d8d54, #2fac6d); + box-shadow: 0 8px 18px rgba(24, 124, 72, .35); } .wrap { width: min(1400px, calc(100% - 32px)); margin: 18px auto 28px; + padding-top: 68px; } .brand-strip { - margin-bottom: 12px; - display: flex; - justify-content: flex-start; + position: fixed; + top: 14px; + left: 16px; + z-index: 200; } .hero { @@ -106,10 +108,14 @@ font-size: clamp(26px, 4.8vw, 48px); letter-spacing: .3px; line-height: 1.02; + background: linear-gradient(115deg, #ffffff 25%, #b8d8ff 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; } .brand-logo { - width: clamp(170px, 24vw, 280px); + width: clamp(119px, 16.8vw, 196px); padding: 0; border: 0; background: transparent; @@ -138,6 +144,14 @@ gap: 16px; } + .mode-switch-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 10px; + margin-top: 12px; + } + .mode-switch { display: inline-flex; gap: 6px; @@ -146,7 +160,36 @@ background: #ffffff; border: 1px solid #d9e6ff; box-shadow: 0 8px 24px rgba(26, 58, 118, .08); - margin-top: 12px; + } + + .polygon-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 9px 16px; + border-radius: 10px; + margin-left: auto; + font-size: 13px; + font-weight: 800; + letter-spacing: .2px; + color: #fff; + background: linear-gradient(140deg, #e05c1f, #e08c39); + box-shadow: 0 8px 18px rgba(200, 80, 20, .28); + text-decoration: none; + transition: .18s ease; + white-space: nowrap; + } + + .polygon-btn::after { + content: "↗"; + font-size: 12px; + opacity: .85; + } + + .polygon-btn:hover { + background: linear-gradient(140deg, #c94f15, #d4782e); + box-shadow: 0 10px 22px rgba(200, 80, 20, .38); + transform: translateY(-1px); } .mode-btn { @@ -180,11 +223,23 @@ .card h2 { margin: 0 0 10px; - font-size: 14px; + font-size: 12px; text-transform: uppercase; - letter-spacing: .9px; + letter-spacing: 1.1px; color: #234782; font-weight: 800; + display: flex; + align-items: center; + gap: 7px; + } + .card h2::before { + content: ""; + display: inline-block; + width: 3px; + height: 14px; + border-radius: 2px; + background: linear-gradient(180deg, #3978e0, #1f4ea3); + flex-shrink: 0; } .search { @@ -256,9 +311,18 @@ border-radius: 10px; background: linear-gradient(140deg, #0d7e59, #0a6648); color: #fff; - padding: 9px 14px; - font-weight: 700; + padding: 9px 18px; + font-weight: 800; + font-size: 13px; + font-family: Manrope, sans-serif; cursor: pointer; + transition: .18s ease; + letter-spacing: .2px; + } + button.action:hover { + transform: translateY(-1px); + box-shadow: 0 8px 18px rgba(10, 100, 70, .30); + background: linear-gradient(140deg, #0e8d64, #0c7551); } .result { @@ -274,14 +338,65 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 12px; + margin-bottom: 14px; gap: 12px; + padding-bottom: 12px; + border-bottom: 1px solid #e2ecff; } .result-head h3 { margin: 0; - font-size: 17px; + font-size: 16px; color: #1f3f77; + font-weight: 800; + letter-spacing: .2px; + } + .active-filters { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 0 0 12px; + padding: 9px; + border-radius: 12px; + border: 1px solid #c9dafd; + background: linear-gradient(145deg, #e8f0ff, #dbe8ff); + box-shadow: inset 0 1px 0 rgba(255,255,255,.65); + } + .active-filter { + display: inline-flex; + align-items: center; + gap: 7px; + border-radius: 999px; + padding: 6px 8px 6px 10px; + border: 1px solid #cfe0ff; + background: linear-gradient(145deg, #f8fbff, #edf4ff); + color: #21457f; + font-size: 12px; + font-weight: 700; + } + .active-filter .kind { + opacity: .7; + font-weight: 600; + letter-spacing: .2px; + } + .active-filter .remove { + display: inline-grid; + place-items: center; + width: 20px; + height: 20px; + border: 0; + border-radius: 999px; + background: #dbe9ff; + color: #1f4a8f; + font-size: 14px; + line-height: 1; + cursor: pointer; + padding: 0; + transition: .16s ease; + } + .active-filter .remove:hover { + background: #c4dbff; + transform: scale(1.05); } .rows { @@ -295,17 +410,46 @@ background: linear-gradient(180deg, #ffffff, #f5f9ff); border-radius: 12px; padding: 10px; + position: relative; + overflow: hidden; + transition: .2s ease; + } + .row-card::before { + content: ""; + position: absolute; + top: 0; left: 0; right: 0; + height: 2px; + background: linear-gradient(90deg, #3978e0 0%, #7bb4ff 60%, transparent 100%); + } + .row-card:hover { + transform: translateY(-2px); + box-shadow: 0 10px 28px rgba(24, 56, 116, .13); + border-color: #c4d8ff; } - .row-card strong { color: #1a3e79; font-size: 14px; } + .row-card strong { + color: #1a3e79; + font-size: 13px; + font-weight: 800; + display: block; + padding-bottom: 8px; + margin-bottom: 8px; + border-bottom: 1px solid #e8f0ff; + letter-spacing: .1px; + } .tags { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; } .tag { font-size: 12px; border-radius: 999px; - background: #eaf2ff; - color: #234b89; - padding: 4px 8px; - border: 1px solid #d2e3ff; + background: linear-gradient(135deg, #f0f6ff, #e4efff); + color: #1e4a8d; + padding: 4px 10px; + border: 1px solid #c8dcff; + font-weight: 600; + transition: .16s ease; + } + .tag:hover { + background: linear-gradient(135deg, #e4efff, #d4e6ff); } a.tag { text-decoration: none; @@ -316,7 +460,12 @@ a.tag::after { content: "↗"; font-size: 11px; - opacity: .85; + opacity: .8; + } + a.tag:hover { + background: linear-gradient(135deg, #daeaff, #c8ddff); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(30, 74, 141, .12); } .credit { @@ -340,10 +489,181 @@ } @media (max-width: 980px) { - .brand-logo { max-width: 240px; } + .brand-logo { max-width: 160px; } .board { grid-template-columns: 1fr; } .hero { padding: 20px; } .credit { right: 8px; bottom: 6px; } .credit .name { font-size: 8px; } .credit a { font-size: 6px; } } + + @media (max-width: 768px) { + .wrap { + width: calc(100% - 16px); + margin: 10px auto 18px; + } + .brand-strip { + margin-bottom: 8px; + } + .brand-logo { + width: clamp(132px, 42vw, 190px); + } + .hero { + padding: 16px 14px 20px; + border-radius: 16px; + } + .hero h1 { + font-size: clamp(22px, 7vw, 32px); + line-height: 1.06; + } + .hero p { + font-size: 14px; + } + .mode-switch-row { + flex-direction: column; + align-items: stretch; + } + .mode-switch { + width: 100%; + display: grid; + grid-template-columns: 1fr 1fr; + } + .polygon-btn { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + .mode-btn { + width: 100%; + min-height: 42px; + } + .card { + min-height: 0; + padding: 12px; + } + .chip-grid { + max-height: 260px; + } + .chip { + font-size: 12px; + padding: 8px 10px; + } + .footer-bar { + flex-direction: column; + align-items: stretch; + gap: 8px; + background: rgba(255, 255, 255, .78); + border: 1px solid #dbe7ff; + border-radius: 12px; + padding: 10px; + } + button.action { + width: 100%; + min-height: 42px; + } + .result { + margin-top: 12px; + padding: 12px; + } + .active-filters { + margin-bottom: 10px; + } + .active-filter { + width: 100%; + justify-content: space-between; + } + .result-head h3 { + font-size: 15px; + } + .rows { + grid-template-columns: 1fr; + gap: 10px; + } + .credit { + right: 8px; + bottom: 8px; + background: rgba(255, 255, 255, .86); + border: 1px solid #dbe6ff; + border-radius: 10px; + padding: 4px 6px; + } + } + + @media (max-width: 420px) { + .hero::before, + .hero::after { + opacity: .72; + } + .search { + padding: 10px 11px; + } + .tag { + font-size: 11px; + } + } + + /* Custom scrollbar */ + .chip-grid { + scrollbar-width: thin; + scrollbar-color: #c5d8f7 transparent; + } + .chip-grid::-webkit-scrollbar { width: 5px; } + .chip-grid::-webkit-scrollbar-track { background: transparent; } + .chip-grid::-webkit-scrollbar-thumb { background: #c5d8f7; border-radius: 4px; } + .chip-grid::-webkit-scrollbar-thumb:hover { background: #a8c4f0; } + + /* Stat pills */ + .stat-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + background: rgba(30, 74, 141, .06); + border: 1px solid #d0e0ff; + border-radius: 999px; + font-size: 12px; + color: #2a4e8d; + font-weight: 600; + } + .stat-pill b { + color: #1a3e79; + font-weight: 800; + } + + /* Footer stats row */ + #stats { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + + /* Entrance animations */ + @keyframes fadeUp { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } + } + .hero { animation: fadeUp .35s ease both; } + .board { animation: fadeUp .45s ease .06s both; } + .footer-bar { animation: fadeUp .45s ease .09s both; } + .result { animation: fadeUp .45s ease .12s both; } + + /* Chip count badge */ + .card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; + } + .card-header h2 { margin: 0; } + .count-badge { + font-size: 11px; + font-weight: 700; + color: #3978e0; + background: #e8f0ff; + border: 1px solid #cddeff; + border-radius: 999px; + padding: 2px 8px; + min-width: 28px; + text-align: center; + } + diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000..ad5ffbf Binary files /dev/null and b/static/favicon.png differ diff --git a/static/js/index.js b/static/js/index.js index c3118b8..ea1132d 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -17,6 +17,11 @@ clearBtn: document.getElementById("clearBtn"), modeInfra: document.getElementById("modeInfra"), modeIb: document.getElementById("modeIb"), + resultSection: document.querySelector(".result"), + filtersSection: document.querySelector(".board"), + activeFilters: document.getElementById("activeFilters"), + vendorBadge: document.getElementById("vendorBadge"), + categoryBadge: document.getElementById("categoryBadge"), }; let clickAudioCtx = null; @@ -130,9 +135,12 @@ else if (!allowedVendors.has(vendor.id)) node.classList.add("dim"); node.textContent = vendor.name; node.addEventListener("click", () => { - if (state.selectedVendors.has(vendor.id)) state.selectedVendors.delete(vendor.id); + const wasSelected = state.selectedVendors.has(vendor.id); + if (wasSelected) state.selectedVendors.delete(vendor.id); else state.selectedVendors.add(vendor.id); render(); + if (wasSelected) scrollAfterDeselect(); + else scrollToResultsSmooth(); }); el.vendorList.appendChild(node); } @@ -148,14 +156,20 @@ else if (!allowedCategories.has(category.id)) node.classList.add("dim"); node.textContent = category.name; node.addEventListener("click", () => { - if (state.selectedCategories.has(category.id)) state.selectedCategories.delete(category.id); + const wasSelected = state.selectedCategories.has(category.id); + if (wasSelected) state.selectedCategories.delete(category.id); else state.selectedCategories.add(category.id); render(); + if (wasSelected) scrollAfterDeselect(); + else scrollToResultsSmooth(); }); el.categoryList.appendChild(node); } - el.stats.textContent = `Вендоров: ${allowedVendors.size}/${state.data.vendors.length} | Категорий: ${allowedCategories.size}/${state.data.categories.length} | Продуктов: ${visibleSets().allowedProducts.size}/${state.data.products.length}`; + const ps = visibleSets().allowedProducts.size; + el.stats.innerHTML = `Вендоры ${allowedVendors.size} / ${state.data.vendors.length}Категории ${allowedCategories.size} / ${state.data.categories.length}Продукты ${ps} / ${state.data.products.length}`; + if (el.vendorBadge) el.vendorBadge.textContent = allowedVendors.size; + if (el.categoryBadge) el.categoryBadge.textContent = allowedCategories.size; } function renderResults() { @@ -213,9 +227,82 @@ el.modeIb.classList.toggle("active", state.scope === "ib"); document.body.classList.toggle("scope-ib", state.scope === "ib"); renderChips(); + renderActiveFilters(); renderResults(); } + function scrollToResultsSmooth() { + if (!el.resultSection) return; + const top = Math.max(el.resultSection.getBoundingClientRect().top + window.scrollY - 10, 0); + window.scrollTo({ top, behavior: "smooth" }); + } + + function scrollToFiltersSmooth() { + if (!el.filtersSection) return; + const top = Math.max(el.filtersSection.getBoundingClientRect().top + window.scrollY - 8, 0); + window.scrollTo({ top, behavior: "smooth" }); + } + + function scrollToTopSmooth() { + window.scrollTo({ top: 0, behavior: "smooth" }); + } + + function scrollAfterDeselect() { + if (window.innerWidth > 980) { + scrollToTopSmooth(); + return; + } + scrollToFiltersSmooth(); + } + + function renderActiveFilters() { + if (!el.activeFilters) return; + el.activeFilters.innerHTML = ""; + + const vendorsById = new Map(state.data.vendors.map(v => [v.id, v.name])); + const categoriesById = new Map(state.data.categories.map(c => [c.id, c.name])); + + const items = []; + for (const id of state.selectedVendors) { + const name = vendorsById.get(id); + if (name) items.push({ kind: "Вендор", name, id, type: "vendor" }); + } + for (const id of state.selectedCategories) { + const name = categoriesById.get(id); + if (name) items.push({ kind: "Категория", name, id, type: "category" }); + } + + if (items.length === 0) { + el.activeFilters.style.display = "none"; + return; + } + el.activeFilters.style.display = "flex"; + + for (const item of items) { + const node = document.createElement("div"); + node.className = "active-filter"; + + const text = document.createElement("span"); + text.innerHTML = `${item.kind}: ${item.name}`; + node.appendChild(text); + + const remove = document.createElement("button"); + remove.className = "remove"; + remove.type = "button"; + remove.setAttribute("aria-label", `Убрать фильтр ${item.name}`); + remove.textContent = "×"; + remove.addEventListener("click", () => { + if (item.type === "vendor") state.selectedVendors.delete(item.id); + else state.selectedCategories.delete(item.id); + render(); + scrollToFiltersSmooth(); + }); + node.appendChild(remove); + + el.activeFilters.appendChild(node); + } + } + async function loadScopeData(scope) { const res = await fetch(`/api/data?scope=${encodeURIComponent(scope)}`); state.data = await res.json(); @@ -258,6 +345,7 @@ el.vendorSearch.value = ""; el.categorySearch.value = ""; render(); + scrollAfterDeselect(); }); el.modeInfra.addEventListener("click", () => { diff --git a/static/mont_logo.png b/static/mont_logo.png new file mode 100644 index 0000000..49805c4 Binary files /dev/null and b/static/mont_logo.png differ diff --git a/templates/index.html b/templates/index.html index 3790483..d408122 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,7 +4,7 @@
Актуальная матрица вендоров, продуктов и категорий. Выбирайте вендоров или категории, чтобы видеть релевантные продуктовые линейки в Инфраструктуре и ИБ.
-