diff --git a/dashboard-mockup.html b/dashboard-mockup.html index cc25cb8..61013c1 100644 --- a/dashboard-mockup.html +++ b/dashboard-mockup.html @@ -11,7 +11,6 @@ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } :root { - /* IBM Carbon Design System — Dark 10 */ --bg: #0a0a0a; --surface: #161616; --surface-hover: #1f1f1f; @@ -22,17 +21,14 @@ --text-tertiary: #6f6f6f; --accent: #0f6fff; --accent-hover: #3d8eff; - --accent-dim: rgba(15,111,255,0.1); --green: #16a34a; --green-dim: rgba(22,163,74,0.12); --red: #e21d1d; --red-dim: rgba(226,29,29,0.12); --yellow: #eac518; - --yellow-dim: rgba(234,197,24,0.12); --mono: 'IBM Plex Mono', monospace; --sans: 'IBM Plex Sans', -apple-system, sans-serif; - --ease-out: cubic-bezier(0.2, 0, 0, 1); - --ease-standard: cubic-bezier(0.2, 0, 0, 1); + --ease: cubic-bezier(0.2, 0, 0, 1); } html { font-size: 14px; } @@ -43,13 +39,10 @@ body { color: var(--text-primary); line-height: 1.5; -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; overflow-x: hidden; } -/* ============================================ - TOP BAR — IBM style - ============================================ */ +/* TOP BAR */ .topbar { display: flex; align-items: center; justify-content: space-between; padding: 0 32px; height: 48px; @@ -59,8 +52,7 @@ body { } .topbar-brand { font-family: var(--mono); font-size: 13px; font-weight: 600; - letter-spacing: 0.5px; color: var(--text-primary); - display: flex; align-items: center; gap: 10px; + letter-spacing: 0.5px; display: flex; align-items: center; gap: 10px; } .topbar-brand .icon { width: 20px; height: 20px; background: var(--accent); @@ -69,33 +61,23 @@ body { } .topbar-right { display: flex; align-items: center; gap: 20px; font-size: 12px; } .status { display: flex; align-items: center; gap: 6px; color: var(--green); font-weight: 500; } -.status-dot { width: 6px; height: 6px; background: var(--green); border-radius: 0; animation: blink 2s ease-in-out infinite; } +.status-dot { width: 6px; height: 6px; background: var(--green); animation: blink 2s ease-in-out infinite; } @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.4} } .clock { font-family: var(--mono); color: var(--text-tertiary); font-size: 11px; letter-spacing: 0.3px; } -/* ============================================ - PAGE HEADER — IBM heading style - ============================================ */ +/* PAGE HEADER */ .page-header { max-width: 1280px; margin: 0 auto; padding: 32px 32px 0; } .page-title { - font-family: var(--sans); font-size: 28px; font-weight: 700; - letter-spacing: -0.5px; - line-height: 1.2; - color: var(--text-primary); - margin-bottom: 8px; -} -.page-desc { - font-size: 14px; color: var(--text-secondary); - font-weight: 400; line-height: 1.5; + letter-spacing: -0.5px; line-height: 1.2; + color: var(--text-primary); margin-bottom: 8px; } +.page-desc { font-size: 14px; color: var(--text-secondary); font-weight: 400; } -/* ============================================ - IBM TABS — Horizontal navigation tabs - ============================================ */ +/* TABS */ .tabs-section { max-width: 1280px; margin: 0 auto; padding: 24px 32px 0; @@ -103,52 +85,31 @@ body { .tabs { display: flex; align-items: center; gap: 0; border-bottom: 1px solid var(--border-strong); - overflow-x: auto; - scrollbar-width: none; + overflow-x: auto; scrollbar-width: none; } .tabs::-webkit-scrollbar { display: none; } .tab-btn { - padding: 12px 20px; - border: none; background: transparent; - color: var(--text-secondary); - font-family: var(--sans); - font-size: 14px; - font-weight: 600; - cursor: pointer; - position: relative; - white-space: nowrap; - transition: color 0.15s var(--ease-standard); + padding: 12px 20px; border: none; background: transparent; + color: var(--text-secondary); font-family: var(--sans); + font-size: 14px; font-weight: 600; cursor: pointer; + position: relative; white-space: nowrap; + transition: color 0.15s var(--ease); } .tab-btn:hover { color: var(--text-primary); } .tab-btn::after { - content: ''; - position: absolute; - bottom: -1px; left: 0; right: 0; - height: 2px; - background: var(--accent); - transform: scaleX(0); - transform-origin: left; - transition: transform 0.2s var(--ease-standard); -} -.tab-btn.active { - color: var(--accent); -} -.tab-btn.active::after { - transform: scaleX(1); + content: ''; position: absolute; + bottom: -1px; left: 0; right: 0; height: 2px; + background: var(--accent); transform: scaleX(0); + transform-origin: left; transition: transform 0.2s var(--ease); } +.tab-btn.active { color: var(--accent); } +.tab-btn.active::after { transform: scaleX(1); } .tab-label { display: flex; align-items: center; gap: 8px; } -.tab-icon { - width: 16px; height: 16px; border-radius: 0; - display: inline-flex; align-items: center; justify-content: center; - font-size: 13px; -} -/* ============================================ - KPI GRID — IBM cards, brutal - ============================================ */ +/* KPI GRID — IBM Carbon style: single border around all */ .kpi-grid { display: grid; grid-template-columns: repeat(6, 1fr); - gap: 1px; padding: 24px 32px; max-width: 1280px; margin: 0 auto; + gap: 1px; padding: 0; max-width: 1280px; margin: 0 auto; background: var(--border); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; } @@ -174,49 +135,37 @@ body { } .kpi-badge.up { color: var(--green); background: var(--green-dim); } .kpi-badge.down { color: var(--red); background: var(--red-dim); } -.kpi-badge.neutral { color: var(--accent); background: var(--accent-dim); } +.kpi-badge.neutral { color: var(--accent); background: rgba(15,111,255,0.1); } .kpi-value { font-family: var(--mono); font-size: 24px; font-weight: 600; - letter-spacing: -0.5px; - line-height: 1.1; - color: var(--text-primary); - margin-bottom: 8px; + letter-spacing: -0.5px; line-height: 1.1; + color: var(--text-primary); margin-bottom: 8px; } .kpi-context { font-size: 11px; color: var(--text-tertiary); } -/* ============================================ - CHART PANELS — IBM structure - ============================================ */ +/* CHARTS WRAPPER */ .charts-wrap { max-width: 1280px; margin: 0 auto; padding: 0 32px 32px; +} +.charts-grid { display: grid; gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; } -.charts-row-main { grid-template-columns: 2fr 1fr; } -.charts-row-sub { grid-template-columns: 1fr 1fr 1fr; } - .chart-panel { background: var(--surface); padding: 24px; - min-height: 280px; } .chart-header { margin-bottom: 20px; } -.chart-title { - font-size: 14px; font-weight: 600; - color: var(--text-primary); - margin-bottom: 4px; -} -.chart-subtitle { - font-size: 12px; color: var(--text-tertiary); -} +.chart-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; } +.chart-subtitle { font-size: 12px; color: var(--text-tertiary); } .chart-canvas { width: 100%; height: 260px; } .chart-canvas.small { height: 200px; } -/* Metric list inside chart panel */ +/* Metric list */ .metric-list { display: flex; flex-direction: column; gap: 0; margin-top: 8px; } .metric-item { display: flex; align-items: center; @@ -224,75 +173,62 @@ body { border-bottom: 1px solid var(--border); } .metric-item:last-child { border-bottom: none; } -.metric-item:first-child { padding-top: 0; } -.metric-item:last-child { padding-bottom: 0; } -.m-label { font-size: 12px; color: var(--text-secondary); flex: 1; font-weight: 400; } +.m-label { font-size: 12px; color: var(--text-secondary); flex: 1; } .m-value { font-family: var(--mono); font-size: 13px; font-weight: 600; color: var(--text-primary); margin: 0 16px; min-width: 64px; text-align: right; } -.m-bar { width: 80px; height: 4px; background: var(--border-strong); border-radius: 0; overflow: hidden; } -.m-fill { height: 100%; width: 0; transition: width 0.8s var(--ease-standard); } +.m-bar { width: 80px; height: 4px; background: var(--border-strong); overflow: hidden; } +.m-fill { height: 100%; width: 0; transition: width 0.8s var(--ease); } .m-fill.green { background: var(--green); } .m-fill.red { background: var(--red); } .m-fill.yellow { background: var(--yellow); } .m-fill.blue { background: var(--accent); } -/* ============================================ - RESPONSIVE - ============================================ */ +/* RESPONSIVE */ @media(max-width: 1024px) { .kpi-grid { grid-template-columns: repeat(3, 1fr); } - .charts-row-main, .charts-row-sub { grid-template-columns: 1fr; } - .page-title { font-size: 22px; } } @media(max-width: 640px) { .kpi-grid { grid-template-columns: repeat(2, 1fr); } .topbar { padding: 0 16px; } - .page-header, .tabs-section, .kpi-grid, .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; } } -
-
-
R
- ROI24 -
+
R
ROI24
Live data
- -
- - - + + +
-
-
-
-
+
+ +
+
+ +
+
+
Выполнение плана
Ключевые метрики
+
@@ -424,13 +360,12 @@ function seg(key) { // KPIs const g = document.getElementById('kpi-grid'); - g.innerHTML = s.kpis.map((d,i) => { - const badgeClass = d.t; + g.innerHTML = s.kpis.map((d, i) => { const arrow = d.t === 'up' ? '▲' : d.t === 'down' ? '▼' : '●'; return `
${d.n} - ${arrow} ${d.d} + ${arrow} ${d.d}
${d.v}
${d.p}
@@ -438,49 +373,32 @@ function seg(key) { }).join(''); requestAnimationFrame(() => g.querySelectorAll('.kpi').forEach(c => c.classList.add('visible'))); - // Main chart area - const mcDom = document.getElementById('charts-main'); - const mc = s.mc; - if (!charts.mc) charts.mc = echarts.init(document.createElement('div')); - mcDom.innerHTML = `
-
-
${mc.title}
-
${mc.sub}
-
-
-
-
-
-
${s.sc.title}
-
${s.sc.sub}
-
-
-
`; - // Main chart + document.getElementById('mc-title').textContent = s.mc.title; + document.getElementById('mc-sub').textContent = s.mc.sub; const mcEl = document.getElementById('mc-canvas'); - charts.mc.dispose(); - charts.mc = echarts.init(mcEl); + if (!charts.mc) charts.mc = echarts.init(mcEl); + charts.mc.clear(); let mcOpt; - if (mc.type === 'dual') { + if (s.mc.type === 'dual') { mcOpt = { - tooltip: { trigger: 'axis', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 12, fontFamily: 'IBM Plex Mono' } }, - legend: { data: ['Доход', 'Расход'], textStyle: { color: '#6f6f6f', fontSize: 11, fontFamily: 'IBM Plex Sans' }, top: 0, itemWidth: 12, itemHeight: 2 }, + tooltip: { trigger: 'axis', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 12 } }, + legend: { data: ['Доход', 'Расход'], textStyle: { color: '#6f6f6f', fontSize: 11 }, top: 0, itemWidth: 12, itemHeight: 2 }, grid: { left: 48, right: 20, top: 32, bottom: 28 }, xAxis: { - type: 'category', data: mc.labels, + type: 'category', data: s.mc.labels, axisLine: { lineStyle: { color: '#262626' } }, - axisLabel: { color: '#6f6f6f', fontSize: 11, fontFamily: 'IBM Plex Mono' }, + axisLabel: { color: '#6f6f6f', fontSize: 11 }, axisTick: { show: false }, }, yAxis: { type: 'value', splitLine: { lineStyle: { color: '#1f1f1f' } }, - axisLabel: { color: '#6f6f6f', fontSize: 11, fontFamily: 'IBM Plex Mono', formatter: '{value}М' }, + axisLabel: { color: '#6f6f6f', fontSize: 11, formatter: '{value}М' }, }, series: [ { - name: 'Доход', type: 'line', data: mc.rev, smooth: 0.4, symbol: 'none', + name: 'Доход', type: 'line', data: s.mc.rev, smooth: 0.4, symbol: 'none', lineStyle: { width: 2.5, color: '#16a34a' }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ @@ -490,58 +408,54 @@ function seg(key) { } }, { - name: 'Расход', type: 'line', data: mc.ads, smooth: 0.4, symbol: 'none', + name: 'Расход', type: 'line', data: s.mc.ads, smooth: 0.4, symbol: 'none', lineStyle: { width: 1.5, color: '#0f6fff', type: 'dashed' }, }, ], animationDuration: 500, - animationEasing: 'cubicOut', }; } else { mcOpt = { - tooltip: { trigger: 'axis', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 12, fontFamily: 'IBM Plex Mono' } }, - legend: { data: ['План', 'Факт'], textStyle: { color: '#6f6f6f', fontSize: 11, fontFamily: 'IBM Plex Sans' }, top: 0, itemWidth: 12, itemHeight: 14 }, + tooltip: { trigger: 'axis', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 12 } }, + legend: { data: ['План', 'Факт'], textStyle: { color: '#6f6f6f', fontSize: 11 }, top: 0, itemWidth: 12, itemHeight: 14 }, grid: { left: 48, right: 20, top: 32, bottom: 28 }, xAxis: { - type: 'category', data: mc.labels, + type: 'category', data: s.mc.labels, axisLine: { lineStyle: { color: '#262626' } }, - axisLabel: { color: '#6f6f6f', fontSize: 11, fontFamily: 'IBM Plex Mono' }, + axisLabel: { color: '#6f6f6f', fontSize: 11 }, }, yAxis: { type: 'value', splitLine: { lineStyle: { color: '#1f1f1f' } }, - axisLabel: { color: '#6f6f6f', fontSize: 11, fontFamily: 'IBM Plex Mono', formatter: '{value}К' }, + axisLabel: { color: '#6f6f6f', fontSize: 11, formatter: '{value}К' }, }, series: [ - { name: 'План', type: 'bar', data: mc.plan, barWidth: '30%', itemStyle: { color: 'rgba(15,111,255,0.1)' } }, - { name: 'Факт', type: 'bar', data: mc.fact, barWidth: '30%', itemStyle: { color: '#0f6fff' } }, + { name: 'План', type: 'bar', data: s.mc.plan, barWidth: '30%', itemStyle: { color: 'rgba(15,111,255,0.1)' } }, + { name: 'Факт', type: 'bar', data: s.mc.fact, barWidth: '30%', itemStyle: { color: '#0f6fff' } }, ], animationDuration: 500, - animationEasing: 'cubicOut', }; } - charts.mc.setOption(mcOpt, true); + charts.mc.setOption(mcOpt); // Side chart - const sc = s.sc; - const scEl = document.getElementById('sc-canvas'); - if (!charts.sc) charts.sc = echarts.init(scEl); - charts.sc.dispose(); - charts.sc = echarts.init(scEl); - const scData = sc.data.reverse(); + document.getElementById('sc-title').textContent = s.sc.title; + document.getElementById('sc-sub').textContent = s.sc.sub; + const scData = s.sc.data.slice().reverse(); const scColors = ['#0f6fff', '#3d8eff', '#16a34a', '#06b6d4', '#a855f7', '#eac518']; - const scOpt = { - tooltip: { trigger: 'axis', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 11 } }, + if (!charts.sc) charts.sc = echarts.init(document.getElementById('sc-canvas')); + charts.sc.clear(); + charts.sc.setOption({ grid: { left: 90, right: 28, top: 8, bottom: 8 }, xAxis: { type: 'value', splitLine: { lineStyle: { color: '#1f1f1f' } }, - axisLabel: { color: '#6f6f6f', fontSize: 10, fontFamily: 'IBM Plex Mono', formatter: '{value}%' }, + axisLabel: { color: '#6f6f6f', fontSize: 10, formatter: '{value}%' }, }, yAxis: { type: 'category', data: scData.map(d => d.n), axisLine: { lineStyle: { color: '#262626' } }, - axisLabel: { color: '#a6a6a6', fontSize: 11, fontFamily: 'IBM Plex Sans' }, + axisLabel: { color: '#a6a6a6', fontSize: 11 }, }, series: [{ type: 'bar', data: scData.map((d, i) => ({ @@ -556,120 +470,66 @@ function seg(key) { barWidth: 28, }], animationDuration: 500, - }; - charts.sc.setOption(scOpt, true); - - // Sub row charts - const subEl = document.getElementById('charts-sub'); - const c1 = s.c1; - const c2 = s.c2; - - // Funnel chart - let funnelHtml = `
-
-
${c1.title}
-
${c1.sub}
-
-
-
`; - - // Second chart - let secondHtml; - if (c2.labels) { - const isPie = c2.data.length && c2.data.reduce((a, b) => a + b, 0) <= 100; - const pc = ['#0f6fff', '#16a34a', '#eac518', '#a855f7', '#e21d1d']; - if (isPie) { - secondHtml = `
-
-
${c2.title}
-
${c2.sub}
-
-
-
`; - } else { - secondHtml = `
-
-
${c2.title}
-
${c2.sub}
-
-
-
`; - } - } - - // Metrics panel - const metricsHtml = `
-
-
Выполнение плана
-
Ключевые метрики
-
-
-
`; - - subEl.innerHTML = funnelHtml + secondHtml + metricsHtml; + }); // Funnel + document.getElementById('c1-title').textContent = s.c1.title; + document.getElementById('c1-sub').textContent = s.c1.sub; const fc = ['#0f6fff', '#3d8eff', '#16a34a', '#e21d1d']; - const c1Dom = document.getElementById('c1-canvas'); - if (!charts.c1) charts.c1 = echarts.init(c1Dom); - charts.c1.dispose(); - charts.c1 = echarts.init(c1Dom); + if (!charts.c1) charts.c1 = echarts.init(document.getElementById('c1-canvas')); + charts.c1.clear(); charts.c1.setOption({ - tooltip: { trigger: 'item', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 11 } }, series: [{ - type: 'funnel', left: '8%', bottom: 5, width: '84%', min: 0, max: c1.data[0].v, + type: 'funnel', left: '8%', bottom: 5, width: '84%', min: 0, max: s.c1.data[0].v, minSize: '0%', maxSize: '100%', sort: 'descending', gap: 2, - label: { show: true, position: 'inside', color: '#f4f4f4', fontSize: 11, fontFamily: 'IBM Plex Mono', formatter: '{b}\n{d}%' }, + label: { show: true, position: 'inside', color: '#f4f4f4', fontSize: 11, formatter: '{b}\n{d}%' }, labelLine: { show: false }, itemStyle: { borderColor: '#161616', borderWidth: 2 }, - data: c1.data.map((d, i) => ({ name: d.n, value: d.v, itemStyle: { color: fc[i % fc.length] } })), + data: s.c1.data.map((d, i) => ({ name: d.n, value: d.v, itemStyle: { color: fc[i % fc.length] } })), }], animationDuration: 500, - }, true); + }); // Second chart - const c2Dom = document.getElementById('c2-canvas'); - if (!charts.c2) charts.c2 = echarts.init(c2Dom); - charts.c2.dispose(); - charts.c2 = echarts.init(c2Dom); - if (c2.labels) { - const isPie = c2.data.length && c2.data.reduce((a, b) => a + b, 0) <= 100; - const pc = ['#0f6fff', '#16a34a', '#eac518', '#a855f7', '#e21d1d']; - if (isPie) { - charts.c2.setOption({ - tooltip: { trigger: 'item', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 11 } }, - series: [{ - type: 'pie', radius: ['40%', '65%'], center: ['50%', '55%'], - itemStyle: { borderRadius: 0, borderColor: '#161616', borderWidth: 2 }, - label: { show: false }, labelLine: { show: false }, - data: c2.labels.map((l, i) => ({ name: l, value: c2.data[i], itemStyle: { color: pc[i % pc.length] } })), - }], - animationDuration: 500, - }, true); - } else { - charts.c2.setOption({ - tooltip: { trigger: 'axis', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 11 } }, - grid: { left: 60, right: 16, top: 8, bottom: 28 }, - xAxis: { - type: 'value', - splitLine: { lineStyle: { color: '#1f1f1f' } }, - axisLabel: { color: '#6f6f6f', fontSize: 10, formatter: '{value}%' }, - }, - yAxis: { - type: 'category', data: c2.labels, - axisLine: { lineStyle: { color: '#262626' } }, - axisLabel: { color: '#a6a6a6', fontSize: 11, fontFamily: 'IBM Plex Sans' }, - }, - series: [{ - type: 'bar', data: c2.data.map(v => ({ - value: v, - itemStyle: { color: v >= 100 ? '#16a34a' : '#eac518' } - })), - barWidth: 24, - }], - animationDuration: 500, - }, true); - } + document.getElementById('c2-title').textContent = s.c2.title; + document.getElementById('c2-sub').textContent = s.c2.sub; + const c2 = s.c2; + const isPie = c2.labels && c2.data.reduce((a, b) => a + b, 0) <= 100; + const pc = ['#0f6fff', '#16a34a', '#eac518', '#a855f7', '#e21d1d']; + if (!charts.c2) charts.c2 = echarts.init(document.getElementById('c2-canvas')); + charts.c2.clear(); + if (isPie) { + charts.c2.setOption({ + series: [{ + type: 'pie', radius: ['40%', '65%'], center: ['50%', '55%'], + itemStyle: { borderRadius: 0, borderColor: '#161616', borderWidth: 2 }, + label: { show: false }, labelLine: { show: false }, + data: c2.labels.map((l, i) => ({ name: l, value: c2.data[i], itemStyle: { color: pc[i % pc.length] } })), + }], + animationDuration: 500, + }); + } else { + charts.c2.setOption({ + grid: { left: 60, right: 16, top: 8, bottom: 28 }, + xAxis: { + type: 'value', + splitLine: { lineStyle: { color: '#1f1f1f' } }, + axisLabel: { color: '#6f6f6f', fontSize: 10, formatter: '{value}%' }, + }, + yAxis: { + type: 'category', data: c2.labels, + axisLine: { lineStyle: { color: '#262626' } }, + axisLabel: { color: '#a6a6a6', fontSize: 11 }, + }, + series: [{ + type: 'bar', data: c2.data.map(v => ({ + value: v, + itemStyle: { color: v >= 100 ? '#16a34a' : '#eac518' } + })), + barWidth: 24, + }], + animationDuration: 500, + }); } // Metrics list @@ -699,7 +559,7 @@ document.addEventListener('DOMContentLoaded', () => { String(n.getHours()).padStart(2, '0') + ':' + String(n.getMinutes()).padStart(2, '0') + ':' + String(n.getSeconds()).padStart(2, '0'); } tick(); setInterval(tick, 1000); - new ResizeObserver(() => Object.values(charts).forEach(c => c.resize())).observe(document.getElementById('charts-wrap')); + new ResizeObserver(() => Object.values(charts).forEach(c => c.resize())).observe(document.querySelector('.charts-grid')); });