feat: Bloomberg Refresh 2024 table style for E-commerce tab

- Added Bloomberg-style table with Follow buttons, ticker IDs, values, and change arrows
- Two sections: Revenue by channel + Marketing metrics
- SVG arrows (12x10) with fill=currentcolor for up/down/neutral states
- Monospace typography for IDs, values, and change amounts
- Responsive grid layout (desktop → tablet → mobile)
- Follow button toggle with blue active state
This commit is contained in:
neo committed 2026-06-21 18:01:04 +05:00
1 parent 6d59cf206f
commit 0b49915420
1 file changed
+113
+113
View File
@@ -196,15 +196,61 @@ body {
.m-fill.yellow { background: var(--yellow); } .m-fill.yellow { background: var(--yellow); }
.m-fill.blue { background: var(--accent); } .m-fill.blue { background: var(--accent); }
/* BLOOMBERG TABLE (E-commerce) */
.b-table-section { display: none; }
.b-table-section.active { display: block; }
.b-table {
width: 100%; border-collapse: collapse; font-size: 13px;
background: #0e0e0e; border: 1px solid #1e1e1e; border-radius: 12px; overflow: hidden;
}
.b-table-header { display: grid; grid-template-columns: 44px 110px 1fr 120px 120px; padding: 10px 18px; border-bottom: 1px solid #1e1e1e; }
.b-th { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: #555; }
.b-th.right { text-align: right; }
.b-row { display: grid; grid-template-columns: 44px 110px 1fr 120px 120px; padding: 10px 18px; transition: background 0.15s; }
.b-row:hover { background: #131313; }
.b-row + .b-row { border-top: 1px solid #141414; }
.b-row:last-child { border-radius: 0 0 11px 11px; }
.b-follow { display: flex; align-items: center; justify-content: center; cursor: pointer; }
.b-follow-btn {
width: 22px; height: 22px; border: 1px solid #2a2a2a; border-radius: 6px;
background: transparent; color: #444; font-size: 12px; display: flex;
align-items: center; justify-content: center; transition: all 0.15s;
}
.b-follow-btn:hover { color: #f4f4f4; border-color: #444; background: #1a1a1a; }
.b-follow-btn.active { color: #0f6fff; border-color: #0f6fff; background: rgba(15,111,255,0.08); }
.b-id { font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: #555; white-space: nowrap; }
.b-name { font-size: 13px; color: #d4d4d4; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.b-value { font-family: 'IBM Plex Mono', monospace; font-size: 14px; font-weight: 600; color: #f4f4f4; text-align: right; letter-spacing: -0.3px; }
.b-change { display: flex; align-items: center; justify-content: flex-end; gap: 4px; font-family: 'IBM Plex Mono', monospace; font-size: 12px; font-weight: 400; }
.b-change.up { color: #16a34a; }
.b-change.down { color: #e21d1d; }
.b-change.neutral { color: #3b82f6; }
.b-arrow { width: 12px; height: 10px; fill: currentColor; flex-shrink: 0; }
.b-section-title {
font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em;
color: #444; padding: 20px 0 8px; margin-top: 8px; border-top: 1px solid #1a1a1a;
}
.b-section-title:first-child { border-top: none; margin-top: 0; }
.b-table-caption {
display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px;
}
.b-table-caption-title { font-size: 14px; font-weight: 600; color: #d4d4d4; }
.b-table-caption-sub { font-size: 12px; color: #555; }
/* RESPONSIVE */ /* RESPONSIVE */
@media(max-width: 1024px) { @media(max-width: 1024px) {
.kpi-grid { grid-template-columns: repeat(3, 1fr); } .kpi-grid { grid-template-columns: repeat(3, 1fr); }
.b-table-header, .b-row { grid-template-columns: 44px 80px 1fr 90px 90px; }
} }
@media(max-width: 640px) { @media(max-width: 640px) {
.kpi-grid { grid-template-columns: repeat(2, 1fr); } .kpi-grid { grid-template-columns: repeat(2, 1fr); }
.topbar { padding: 0 16px; } .topbar { padding: 0 16px; }
.page-header, .tabs-section, .charts-wrap { padding-left: 16px; padding-right: 16px; } .page-header, .tabs-section, .charts-wrap { padding-left: 16px; padding-right: 16px; }
.tab-btn { padding: 12px 14px; font-size: 13px; } .tab-btn { padding: 12px 14px; font-size: 13px; }
.b-table-header, .b-row { grid-template-columns: 36px 70px 1fr 70px 70px; padding: 8px 12px; }
.b-name { font-size: 12px; }
.b-value { font-size: 12px; }
.b-change { font-size: 11px; }
} }
</style> </style>
</head> </head>
@@ -233,6 +279,26 @@ body {
<div class="kpi-grid" id="kpi-grid"></div> <div class="kpi-grid" id="kpi-grid"></div>
<!-- Bloomberg-style table (E-commerce only) -->
<div class="charts-wrap">
<div class="b-table-section active" id="b-table-section">
<div class="b-table-caption">
<span class="b-table-caption-title">Каналы и метрики</span>
<span class="b-table-caption-sub">Обновлено только что</span>
</div>
<div class="b-table-caption-sub b-section-title">Выручка по каналам</div>
<table class="b-table" id="b-table-channels">
<thead><tr><th class="b-th"></th><th class="b-th">ID</th><th class="b-th">Канал</th><th class="b-th right">Выручка</th><th class="b-th right">Изменение</th></tr></thead>
<tbody></tbody>
</table>
<div class="b-table-caption-sub b-section-title">Маркетинговые метрики</div>
<table class="b-table" id="b-table-marketing">
<thead><tr><th class="b-th"></th><th class="b-th">ID</th><th class="b-th">Метрика</th><th class="b-th right">Значение</th><th class="b-th right">Изменение</th></tr></thead>
<tbody></tbody>
</table>
</div>
</div>
<div class="charts-wrap"> <div class="charts-wrap">
<div class="charts-grid"> <div class="charts-grid">
<!-- Row 1: Main + Side --> <!-- Row 1: Main + Side -->
@@ -388,6 +454,53 @@ function seg(key) {
}).join(''); }).join('');
requestAnimationFrame(() => g.querySelectorAll('.kpi').forEach(c => c.classList.add('visible'))); requestAnimationFrame(() => g.querySelectorAll('.kpi').forEach(c => c.classList.add('visible')));
// Bloomberg table — channels + marketing
const arrowUp = '<svg class="b-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 0L24 20L0 20L12 0Z"/></svg>';
const arrowDown = '<svg class="b-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 20L0 0L24 0L12 20Z"/></svg>';
const arrowNeutral = '<svg class="b-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 0L24 20L0 20L12 0Z"/></svg>';
function changeCell(val, t) {
const cls = t === 'up' ? 'up' : t === 'down' ? 'down' : 'neutral';
const arrow = t === 'up' ? arrowUp : t === 'down' ? arrowDown : arrowNeutral;
const sign = t === 'up' ? '+' : t === 'down' ? '−' : '±';
return `<div class="b-change ${cls}">${arrow} ${sign}${val}</div>`;
}
const channels = [
{ id:'SEO:CH', name:'SEO-трафик', value:'1.2М ₽', change:'18', t:'up' },
{ id:'PPC:CH', name:'Контекстная реклама', value:'980К ₽', change:'12', t:'up' },
{ id:'SMM:CH', name:'Соцсети', value:'640К ₽', change:'−3', t:'down' },
{ id:'EMAIL:CH',name:'Email-рассылки', value:'420К ₽', change:'8', t:'up' },
{ id:'ORG:CH', name:'Органический', value:'380К ₽', change:'2', t:'up' },
{ id:'REF:CH', name:'Реферальный', value:'156К ₽', change:'24', t:'up' },
{ id:'AFF:CH', name:'Партнёрские', value:'98К ₽', change:'−12', t:'down' },
];
const marketing = [
{ id:'CAC', name:'Стоимость клиента', value:'680 ₽', change:'8', t:'down' },
{ id:'AOV', name:'Ср. чек', value:'4 820 ₽', change:'3', t:'up' },
{ id:'CVR', name:'Конверсия', value:'3.8%', change:'0.5', t:'up' },
{ id:'CLV', name:'Пож. ценность', value:'12 400 ₽', change:'18', t:'up' },
{ id:'ROAS', name:'ROAS', value:'4.2×', change:'15', t:'up' },
{ id:'Bounce', name:'Показ. отказов', value:'32%', change:'1', t:'down' },
];
function renderTable(body, data) {
body.innerHTML = data.map((d, i) => `
<tr class="b-row" style="animation-delay:${i*40}ms">
<td class="b-follow"><button class="b-follow-btn" title="Follow">+</button></td>
<td class="b-id">${d.id}</td>
<td class="b-name">${d.name}</td>
<td class="b-value">${d.value}</td>
<td>${changeCell(d.change, d.t)}</td>
</tr>`).join('');
// Follow toggle
body.querySelectorAll('.b-follow-btn').forEach(btn => {
btn.addEventListener('click', () => btn.classList.toggle('active'));
});
}
const tbody1 = document.querySelector('#b-table-channels tbody');
const tbody2 = document.querySelector('#b-table-marketing tbody');
if (tbody1) renderTable(tbody1, channels);
if (tbody2) renderTable(tbody2, marketing);
// Sparkline charts // Sparkline charts
const sparkColors = []; const sparkColors = [];
s.kpis.forEach(d => { sparkColors.push(d.t === 'up' ? '#22c55e' : d.t === 'down' ? '#ef4444' : '#3b82f6'); }); s.kpis.forEach(d => { sparkColors.push(d.t === 'up' ? '#22c55e' : d.t === 'down' ? '#ef4444' : '#3b82f6'); });