diff --git a/SQLLinter.CLI/Program.cs b/SQLLinter.CLI/Program.cs index 2737344..41a3a5a 100644 --- a/SQLLinter.CLI/Program.cs +++ b/SQLLinter.CLI/Program.cs @@ -56,8 +56,14 @@ namespace SQLLinter.CLI using (StreamReader reader = new StreamReader(@"C:\Users\frost\Downloads\Telegram Desktop\test.sql")) { - linter.Run("test.sql", reader.BaseStream); - diagramer.Run("test.sql", reader.BaseStream); + Dictionary files = new Dictionary + { + { "test-qwewq-asdcxczc-asdsa -s--sadsasd-dsads-dsa-d-sd--dsa - 1.sql", reader.BaseStream }, + { "test-qwewq-asdcxczc-asdsa -s--sadsasd-dsads-dsa-d-sd--dsa - 2.sql", reader.BaseStream }, + + }; + linter.Run(files); + //diagramer.Run("test.sql", reader.BaseStream); } //linter.Run(@"C:\Users\frost\Desktop\DISTR-2599\test.sql"); diff --git a/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.css b/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.css index 6a70bfb..82a8d63 100644 --- a/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.css +++ b/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.css @@ -35,6 +35,7 @@ --shadow-8: 0 4px 8px rgba(0, 0, 0, 0.14); --shadow-16: 0 8px 16px rgba(0, 0, 0, 0.16); /* Spacing */ + --spacing-xxs: 1px; --spacing-xs: 4px; --spacing-sm: 8px; --spacing-md: 12px; @@ -49,6 +50,7 @@ --border-radius-xlarge: 8px; /* Typography */ --font-family: 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif; + --font-size-xxs: 10px; --font-size-xs: 12px; --font-size-sm: 13px; --font-size-md: 14px; @@ -445,13 +447,47 @@ td.description { display: flex; gap: var(--spacing-xs); overflow-x: auto; + overflow-y: hidden; scrollbar-width: thin; padding: var(--spacing-xs) 0; + cursor: grab; + scroll-behavior: smooth; + flex-wrap: nowrap; + width: 100%; + -webkit-overflow-scrolling: touch; /* Для плавного скролла на iOS */ + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -webkit-touch-callout: none; + /*white-space: nowrap;*/ + height: auto; /* Автоматическая высота */ + min-height: 50px; /* Минимальная высота */ } + .tabs:active { + cursor: grabbing; + } + + .tabs::-webkit-scrollbar { + height: 6px; + } + + .tabs::-webkit-scrollbar-track { + background: var(--color-background-neutral); + border-radius: var(--border-radius-medium); + } + + .tabs::-webkit-scrollbar-thumb { + background: var(--color-border-strong); + border-radius: var(--border-radius-medium); + } + .tabs::-webkit-scrollbar-thumb:hover { + background: var(--color-text-tertiary); + } .tab { position: relative; - padding: var(--spacing-md) var(--spacing-lg); + padding: var(--spacing-sm) var(--spacing-md); background: none; border: none; border-bottom: 2px solid transparent; @@ -460,14 +496,23 @@ td.description { font-size: var(--font-size-md); font-weight: 600; cursor: pointer; - white-space: nowrap; + white-space: normal; /* Изменено с nowrap на normal */ transition: color var(--transition-medium), background-color var(--transition-medium), border-color var(--transition-medium), transform var(--transition-medium); border-radius: var(--border-radius-medium) var(--border-radius-medium) 0 0; - min-height: 44px; + min-height: auto; display: flex; - align-items: center; - justify-content: center; + align-items: flex-start; /* Важно: flex-start вместо center */ z-index: 1002; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + justify-content: space-between; + flex-shrink: 0; + max-width: 300px; /* Ограничиваем максимальную ширину */ + min-width: 150px; /* Минимальная ширина для читаемости */ + text-align: left; + line-height: 1.4; } .tab:hover { @@ -481,25 +526,217 @@ td.description { background-color: var(--color-background-neutral); } -.tab-badge { +.tab-inner { + display: flex; + align-items: flex-start; + justify-content: space-between; + width: 100%; + min-width: 0; /* Важно для работы text-overflow */ + gap: 8px; +} + +.tab-counters { + display: flex; + flex-direction: column; + gap: 2px; + margin-left: 8px; + justify-content: flex-start; + align-items: flex-end; + flex-shrink: 0; + min-height: 32px; /* Минимальная высота для трех счетчиков с отступами */ +} + +.tab-counter { display: inline-flex; align-items: center; justify-content: center; - min-width: 20px; - height: 20px; - padding: 0 var(--spacing-xs); - margin-left: var(--spacing-xs); - font-size: var(--font-size-xs); - font-weight: 600; - background-color: var(--color-background-neutral-dark); - color: var(--color-text-secondary); - border-radius: 10px; - transition: background-color var(--transition-fast), color var(--transition-fast); + min-width: 16px; + height: 16px; + padding: 0 var(--spacing-xxs); + font-size: var(--font-size-xxs); + font-weight: 400; + border-radius: var(--border-radius-small); + text-align: center; + color: white; + line-height: 1; + border: 1px solid var(--color-border); + font-feature-settings: "tnum"; + font-variant-numeric: tabular-nums; + flex-shrink: 0; + box-sizing: border-box; } -.tab.active .tab-badge { - background-color: var(--color-primary); - color: #ffffff; + .tab-counter.empty { + background-color: transparent !important; + color: transparent !important; + border-style: dashed !important; + opacity: 0.5; + cursor: default; + box-shadow: none !important; + text-shadow: none !important; + background-image: none !important; + } + + /* Цветные пунктирные границы для пустых счетчиков */ + .tab-counter.critical.empty { + border-color: var(--color-critical) !important; + } + + .tab-counter.warning.empty { + border-color: var(--color-warning) !important; + } + + .tab-counter.info.empty { + border-color: var(--color-info) !important; + } + + .tab-counter.critical:not(.empty) { + background-color: var(--color-critical-bg); + color: var(--color-critical); + border-color: var(--color-critical); + } + + .tab-counter.warning:not(.empty) { + background-color: var(--color-warning-bg); + color: var(--color-warning); + border-color: var(--color-warning); + } + + .tab-counter.info:not(.empty) { + background-color: var(--color-info-bg); + color: var(--color-info); + border-color: var(--color-info); + } + +/* При наведении на таб не менять стили пустых счетчиков */ +.tab:hover .tab-counter.empty { + background-color: transparent !important; + color: transparent !important; + opacity: 0.5; +} + +/* Активный таб тоже не должен влиять на пустые счетчики */ +.tab.active .tab-counter.empty { + background-color: transparent !important; + color: transparent !important; + opacity: 0.5; +} + +/* Сохраняем прозрачный текст для пустых счетчиков */ +.tab-counter.empty::after { + content: ''; + display: block; + width: 0; + height: 0; +} + +.tab-text { + display: block; + overflow-wrap: break-word; /* Используем вместо word-break */ + word-wrap: break-word; /* Для старых браузеров */ + hyphens: auto; /* Автоматическое добавление переносов */ + text-align: left; + line-height: 1.3; + white-space: normal; + flex-grow: 1; + min-width: 0; + word-break: break-word; /* Разрешаем разрыв длинных слов */ + max-height: 2.8em; /* Примерно 2 строки текста */ + display: -webkit-box; + -webkit-line-clamp: 2; /* Ограничиваем 2 строками */ + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* Адаптивные стили для мобильных */ +@media (max-width: 768px) { + .tab { + padding: 8px 10px; + font-size: 13px; + min-width: 120px; + max-width: 200px; + } + + .tab-text { + font-size: 13px; + line-height: 1.2; + } + + .tab-counters { + margin-left: 8px; + gap: 2px; + min-height: 55px; /* Немного меньше для мобильных */ + } + + .tab-counter { + min-width: 18px; + height: 18px; + font-size: 10px; + padding: 0 4px; + } + .tab-counter.empty { + opacity: 0.2; + } +} + +@media (max-width: 480px) { + .tab { + padding: 6px 8px; + font-size: 12px; + min-width: 100px; + max-width: 160px; + } + + .tab-text { + font-size: 12px; + max-height: 2.4em; + } + + .tab-counters { + margin-left: 6px; + gap: 1px; + min-height: 50px; + } + + .tab-counter { + min-width: 16px; + height: 16px; + font-size: 9px; + padding: 0 3px; + } + .tab-counter.empty { + border-width: 1px !important; /* Уменьшаем толщину границы */ + } +} + +/* Усилить видимость пунктирных границ на темных темах */ +@media (prefers-color-scheme: dark) { + .tab-counter.empty { + opacity: 0.7; + } + + .tab-counter.critical.empty { + border-color: var(--color-critical) !important; + } + + .tab-counter.warning.empty { + border-color: var(--color-warning) !important; + } + + .tab-counter.info.empty { + border-color: var(--color-info) !important; + } +} + +/* Усилить видимость пунктирных границ при наведении на таб */ +.tab:hover .tab-counter.empty { + opacity: 0.8; + border-style: dashed !important; +} + +/* Активный таб - усилить видимость */ +.tab.active .tab-counter.empty { + opacity: 0.8; } /* --------------------------------------------------------- diff --git a/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.js b/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.js index 9dccf8a..16a1ae8 100644 --- a/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.js +++ b/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.js @@ -23,6 +23,7 @@ class ReportRenderer { init() { this.renderTabs(); + this.setupTabsDragScroll(); this.renderFileReports(); this.setupTabNavigation(); this.activateTab(0); @@ -34,33 +35,161 @@ class ReportRenderer { this.tabsList.innerHTML = ''; - // Табы для файлов this.files.forEach((file, index) => { const tab = document.createElement('button'); tab.className = `tab ${index === 0 ? 'active' : ''}`; tab.dataset.target = `file_${index}`; tab.dataset.index = index; - const total = (file.v.c?.length || 0) + (file.v.w?.length || 0) + (file.v.i?.length || 0); + // Подсчет нарушений по типам + const criticalCount = file.v.c?.length || 0; + const warningCount = file.v.w?.length || 0; + const infoCount = file.v.i?.length || 0; + + // Создание HTML с раздельными счетчиками + const countersHTML = ` +
+ ${criticalCount || ''} + ${warningCount || ''} + ${infoCount || ''} +
+ `; tab.innerHTML = ` - ${this.escapeHtml(file.n)} - ${total} +
+ ${this.escapeHtml(file.n)} + ${countersHTML} +
`; this.tabsList.appendChild(tab); }); - // Таб для диаграммы (если есть) if (this.diagram.h || this.diagram.hasDiagram) { const diagramTab = document.createElement('button'); diagramTab.className = 'tab'; diagramTab.dataset.target = 'mermaid'; - diagramTab.textContent = 'Диаграмма'; + // Создаем пустые счетчики для диаграммы + const diagramCountersHTML = ` +
+ + + +
+ `; + + diagramTab.innerHTML = ` +
+ Диаграмма + ${diagramCountersHTML} +
+ `; + tabsList.appendChild(diagramTab); } } + // Drag-to-scroll для табов + setupTabsDragScroll() { + const tabs = document.querySelector('.tabs'); + if (!tabs) return; + + let isDown = false; + let startX; + let scrollLeft; + let lastTimestamp = 0; + const SCROLL_SPEED = 1.5; + const FRAME_TIME = 16; // ~60 FPS + + // Улучшенная прокрутка колесиком + tabs.addEventListener('wheel', (e) => { + if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) { + e.preventDefault(); + + const now = Date.now(); + if (now - lastTimestamp < FRAME_TIME) return; // Ограничиваем частоту + + lastTimestamp = now; + + // Плавная прокрутка с инерцией + tabs.style.scrollBehavior = 'auto'; + const currentScroll = tabs.scrollLeft; + const targetScroll = currentScroll + e.deltaY * SCROLL_SPEED; + + // Используем requestAnimationFrame для плавности + const animateScroll = () => { + const diff = targetScroll - tabs.scrollLeft; + const step = diff * 0.3; // Эффект плавного замедления + + tabs.scrollLeft += step; + + if (Math.abs(diff) > 0.5) { + requestAnimationFrame(animateScroll); + } + }; + + animateScroll(); + } + }, { passive: false }); + + // Улучшенный drag-scroll + const startDrag = (clientX) => { + isDown = true; + tabs.style.cursor = 'grabbing'; + tabs.style.scrollBehavior = 'auto'; + startX = clientX - tabs.getBoundingClientRect().left; + scrollLeft = tabs.scrollLeft; + }; + + const endDrag = () => { + isDown = false; + tabs.style.cursor = 'grab'; + }; + + const doDrag = (clientX) => { + if (!isDown) return; + + const x = clientX - tabs.getBoundingClientRect().left; + const walk = (x - startX) * 2; + + // Плавное обновление позиции + requestAnimationFrame(() => { + tabs.scrollLeft = scrollLeft - walk; + }); + }; + + // Мышь + tabs.addEventListener('mousedown', (e) => { + if (e.button !== 0) return; // Только левая кнопка + startDrag(e.clientX); + }); + + tabs.addEventListener('mouseleave', endDrag); + tabs.addEventListener('mouseup', endDrag); + + tabs.addEventListener('mousemove', (e) => { + if (!isDown) return; + e.preventDefault(); + doDrag(e.clientX); + }); + + // Сенсорные устройства + tabs.addEventListener('touchstart', (e) => { + if (e.touches.length === 1) { + startDrag(e.touches[0].clientX); + } + }, { passive: true }); + + tabs.addEventListener('touchend', endDrag); + tabs.addEventListener('touchcancel', endDrag); + + tabs.addEventListener('touchmove', (e) => { + if (!isDown || e.touches.length !== 1) return; + e.preventDefault(); + doDrag(e.touches[0].clientX); + }, { passive: false }); + } + renderFileReports() { const container = document.getElementById('reports-container'); if (!container) return; @@ -189,7 +318,7 @@ class ReportRenderer { section.innerHTML = `
-

${severityTitle}

+

${severityTitle}

${violations.length}
@@ -258,9 +387,7 @@ class ReportRenderer { } calculateTotalViolations(file) { - return (file.criticalViolations?.length || 0) + - (file.warningViolations?.length || 0) + - (file.infoViolations?.length || 0); + return (file.v.c?.length || 0) + (file.v.w?.length || 0) + (file.v.i?.length || 0); } escapeHtml(text) { @@ -319,8 +446,10 @@ function initTabs(onTabActivated) { top: ${y - size / 2}px; `; + /* tab.style.position = 'relative'; tab.style.overflow = 'hidden'; + */ tab.appendChild(ripple); setTimeout(() => {