From e4acae11f0f5b72a76207cf8240cf288b6153b75 Mon Sep 17 00:00:00 2001 From: FrigaT Date: Sun, 28 Dec 2025 11:53:21 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BE=D0=BF=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D0=B0=D1=8F=20=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=81=D0=B8=D1=8F=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?=20=D0=B4=D0=B8=D0=B7=D0=B0=D0=B9=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SQLLinter.CLI/Program.cs | 28 +- .../Infrastructure/Diagram/BpmnBuilder.cs | 1 - .../Diagram/SqlDiagramProcessor.cs | 1 + .../Html/v1}/HtmlReportFormatter_v1.cs | 4 +- .../Formatters/Html/v2/HtmlFormatter_v2.css | 1683 +++++++++++++ .../Formatters/Html/v2/HtmlFormatter_v2.js | 1894 ++++++++++++++ .../Html/v2}/HtmlReportFormatter_v2.cs | 8 +- .../Formatters/Html/v3/HtmlFormatter_v3.css | 542 ++++ .../Formatters/Html/v3/HtmlFormatter_v3.js | 486 ++++ .../Html/v3/HtmlReportFormatter_v3.cs | 291 +++ .../Static/HtmlFormatter - Копировать.css | 1681 +++++++++++++ .../Reporters/Static/HtmlFormatter.css | 1862 +++----------- .../Reporters/Static/HtmlFormatter.js | 2223 +++-------------- .../Reporters/Static/HtmlFormatterOld.js | 1895 ++++++++++++++ SQLLinter/SQLLinter.csproj | 6 + 15 files changed, 9263 insertions(+), 3342 deletions(-) rename SQLLinter/Infrastructure/Reporters/{ => Formatters/Html/v1}/HtmlReportFormatter_v1.cs (98%) create mode 100644 SQLLinter/Infrastructure/Reporters/Formatters/Html/v2/HtmlFormatter_v2.css create mode 100644 SQLLinter/Infrastructure/Reporters/Formatters/Html/v2/HtmlFormatter_v2.js rename SQLLinter/Infrastructure/Reporters/{ => Formatters/Html/v2}/HtmlReportFormatter_v2.cs (97%) create mode 100644 SQLLinter/Infrastructure/Reporters/Formatters/Html/v3/HtmlFormatter_v3.css create mode 100644 SQLLinter/Infrastructure/Reporters/Formatters/Html/v3/HtmlFormatter_v3.js create mode 100644 SQLLinter/Infrastructure/Reporters/Formatters/Html/v3/HtmlReportFormatter_v3.cs create mode 100644 SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter - Копировать.css create mode 100644 SQLLinter/Infrastructure/Reporters/Static/HtmlFormatterOld.js diff --git a/SQLLinter.CLI/Program.cs b/SQLLinter.CLI/Program.cs index 41a3a5a..7cb035c 100644 --- a/SQLLinter.CLI/Program.cs +++ b/SQLLinter.CLI/Program.cs @@ -2,6 +2,7 @@ using SQLLinter.Infrastructure.Diagram; using SQLLinter.Infrastructure.Parser; using SQLLinter.Infrastructure.Reporters; +using F = SQLLinter.Infrastructure.Reporters.Formatters.Html; namespace SQLLinter.CLI { @@ -56,22 +57,35 @@ namespace SQLLinter.CLI using (StreamReader reader = new StreamReader(@"C:\Users\frost\Downloads\Telegram Desktop\test.sql")) { - 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 }, + var name = "test-qwewq-asdcxczc-asdsa -s--sadsasd-dsads-dsa-d-sd--dsa - 0.sql"; + + Dictionary files = new(); + + for (int i = 0; i < 2; i++) + { + files[name + i + ".sql"] = reader.BaseStream; + } - }; linter.Run(files); //diagramer.Run("test.sql", reader.BaseStream); } //linter.Run(@"C:\Users\frost\Desktop\DISTR-2599\test.sql"); - var formatter = new HtmlReportFormatter_v2(); + IReportFormatter formatter = new F.v3.HtmlReportFormatter(); var content = formatter.Format(rep.Violations, null); - File.WriteAllText(@"C:\Users\frost\Downloads\Telegram Desktop\test.html", content); + File.WriteAllText(@"C:\Users\frost\Downloads\Telegram Desktop\test3.html", content); + + + formatter = new F.v2.HtmlReportFormatter(); + content = formatter.Format(rep.Violations, null); + File.WriteAllText(@"C:\Users\frost\Downloads\Telegram Desktop\test2.html", content); + + + formatter = new F.v1.HtmlReportFormatter(); + content = formatter.Format(rep.Violations, null); + File.WriteAllText(@"C:\Users\frost\Downloads\Telegram Desktop\test1.html", content); } } } diff --git a/SQLLinter/Infrastructure/Diagram/BpmnBuilder.cs b/SQLLinter/Infrastructure/Diagram/BpmnBuilder.cs index c51eb1e..e0d2d20 100644 --- a/SQLLinter/Infrastructure/Diagram/BpmnBuilder.cs +++ b/SQLLinter/Infrastructure/Diagram/BpmnBuilder.cs @@ -25,7 +25,6 @@ public static class BpmnBuilder { var visitor = new BpmnVisitor(diagram); fragment.Accept(visitor); - visitor.Diagram.AddMissingProcessEdges(); return visitor.Diagram; } diff --git a/SQLLinter/Infrastructure/Diagram/SqlDiagramProcessor.cs b/SQLLinter/Infrastructure/Diagram/SqlDiagramProcessor.cs index a09f015..85c87d5 100644 --- a/SQLLinter/Infrastructure/Diagram/SqlDiagramProcessor.cs +++ b/SQLLinter/Infrastructure/Diagram/SqlDiagramProcessor.cs @@ -58,6 +58,7 @@ public class SqlDiagramProcessor : ISqlDiagramProcessor } BpmnBuilder.Build(fragment, _bpmnDiagram); + _bpmnDiagram.AddMissingProcessEdges(); } private Stream GetFileContents(string filePath) diff --git a/SQLLinter/Infrastructure/Reporters/HtmlReportFormatter_v1.cs b/SQLLinter/Infrastructure/Reporters/Formatters/Html/v1/HtmlReportFormatter_v1.cs similarity index 98% rename from SQLLinter/Infrastructure/Reporters/HtmlReportFormatter_v1.cs rename to SQLLinter/Infrastructure/Reporters/Formatters/Html/v1/HtmlReportFormatter_v1.cs index 9e883bc..bf0bbd5 100644 --- a/SQLLinter/Infrastructure/Reporters/HtmlReportFormatter_v1.cs +++ b/SQLLinter/Infrastructure/Reporters/Formatters/Html/v1/HtmlReportFormatter_v1.cs @@ -2,9 +2,9 @@ using SQLLinter.Infrastructure.Diagram; using System.Text; -namespace SQLLinter.Infrastructure.Reporters; +namespace SQLLinter.Infrastructure.Reporters.Formatters.Html.v1; -public class HtmlReportFormatter_v1 : IReportFormatter +public class HtmlReportFormatter : IReportFormatter { public string Format(List violations) => Format(violations, null); diff --git a/SQLLinter/Infrastructure/Reporters/Formatters/Html/v2/HtmlFormatter_v2.css b/SQLLinter/Infrastructure/Reporters/Formatters/Html/v2/HtmlFormatter_v2.css new file mode 100644 index 0000000..f8dde88 --- /dev/null +++ b/SQLLinter/Infrastructure/Reporters/Formatters/Html/v2/HtmlFormatter_v2.css @@ -0,0 +1,1683 @@ +:root { + /* Colors - Light Theme */ + --color-primary: #0078d4; + --color-primary-dark: #106ebe; + --color-primary-darker: #005a9e; + --color-primary-light: #c7e0f4; + --color-primary-lighter: #deecf9; + --color-text-primary: #323130; + --color-text-secondary: #605e5c; + --color-text-tertiary: #a19f9d; + --color-text-disabled: #c8c6c4; + --color-background: #ffffff; + --color-background-alt: #faf9f8; + --color-background-neutral: #f3f2f1; + --color-background-neutral-dark: #edebe9; + --color-border: #edebe9; + --color-border-strong: #d2d0ce; + --color-border-input: #8a8886; + --color-critical: #d13438; + --color-critical-bg: #fde7e9; + --color-warning: #ffaa44; + --color-warning-bg: #fff4ce; + --color-success: #107c10; + --color-success-bg: #dff6dd; + --color-info: #0078d4; + --color-info-bg: #c7e0f4; + --color-mermaid-node: #f0f8ff; + --color-mermaid-node-border: #0078d4; + --color-mermaid-cluster: #f3f2f1; + --color-mermaid-edge: #8a8886; + --color-mermaid-text: #323130; + /* Shadows (облегчённые) */ + --shadow-2: 0 1px 2px rgba(0, 0, 0, 0.08); + --shadow-4: 0 2px 4px rgba(0, 0, 0, 0.12); + --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; + --spacing-lg: 16px; + --spacing-xl: 20px; + --spacing-xxl: 24px; + --spacing-xxxl: 32px; + /* Border Radius */ + --border-radius-small: 2px; + --border-radius-medium: 4px; + --border-radius-large: 6px; + --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; + --font-size-lg: 16px; + --font-size-xl: 18px; + --font-size-xxl: 20px; + --font-size-xxxl: 24px; + /* Transitions */ + --transition-fast: 0.1s cubic-bezier(0.4, 0, 0.2, 1); + --transition-medium: 0.2s cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: 0.3s cubic-bezier(0.4, 0, 0.2, 1); + /* Z-index */ + --z-index-base: 1; + --z-index-dropdown: 100; + --z-index-sticky: 200; + --z-index-fixed: 300; + --z-index-modal: 400; + --z-index-popover: 500; + --z-index-tooltip: 600; +} + +/* Dark Theme Variables */ +@media (prefers-color-scheme: dark) { + :root { + --color-primary: #2899f5; + --color-primary-dark: #3aa0f3; + --color-primary-darker: #6cb8f6; + --color-primary-light: #0f2b4d; + --color-primary-lighter: #1a365d; + --color-text-primary: #f3f2f1; + --color-text-secondary: #a19f9d; + --color-text-tertiary: #797775; + --color-text-disabled: #605e5c; + --color-background: #1e1e1e; + --color-background-alt: #2d2d2d; + --color-background-neutral: #3c3c3c; + --color-background-neutral-dark: #424242; + --color-border: #424242; + --color-border-strong: #616161; + --color-border-input: #797775; + --color-critical: #d13438; + --color-critical-bg: #4c191b; + --color-warning: #ffaa44; + --color-warning-bg: #4c3b1a; + --color-success: #107c10; + --color-success-bg: #1c3b1c; + --color-info: #2899f5; + --color-info-bg: #0f2b4d; + --color-mermaid-node: #2d2d2d; + --color-mermaid-node-border: #2899f5; + --color-mermaid-cluster: #201f1e; + --color-mermaid-edge: #797775; + --color-mermaid-text: #f3f2f1; + } +} + +/* --------------------------------------------------------- + RESET & BASE STYLES +--------------------------------------------------------- */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + scroll-behavior: smooth; +} + +body { + font-family: var(--font-family); + font-size: var(--font-size-md); + line-height: 1.5; + color: var(--color-text-primary); + background-color: var(--color-background-neutral); + margin: 0; + padding: 0; + overflow-x: hidden; +} + +/* --------------------------------------------------------- + TYPOGRAPHY +--------------------------------------------------------- */ +h3 { + font-size: var(--font-size-xl); + font-weight: 600; + line-height: 1.3; + margin-bottom: var(--spacing-md); + color: var(--color-text-primary); +} + +/* --------------------------------------------------------- + FILE REPORTS & LAYOUT +--------------------------------------------------------- */ + +main#main-content { + min-height: 100vh; +} + +/* Основной контейнер отчёта по файлу */ +.file-report { + display: none; + padding-bottom: 120px; + animation: fadeIn var(--transition-slow); +} + + .file-report.active { + display: block; + } + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(6px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Заголовок файла */ +.file-title-container { + padding: var(--spacing-xl) var(--spacing-xl) var(--spacing-lg); + border-bottom: 1px solid var(--color-border); + background-color: var(--color-background); + position: sticky; + top: 0; + z-index: var(--z-index-sticky); + box-shadow: var(--shadow-2); +} + +.file-title { + display: flex; + align-items: center; + gap: var(--spacing-md); + font-size: var(--font-size-xl); + font-weight: 600; + color: var(--color-text-primary); +} + + .file-title svg { + flex-shrink: 0; + opacity: 0.8; + } + +.file-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Состояние без нарушений */ +.no-violations { + display: flex; + align-items: center; + justify-content: center; + min-height: 300px; + padding: var(--spacing-xl); +} + +.no-violations-content { + text-align: center; + max-width: 400px; +} + +.no-violations-icon { + font-size: 48px; + margin-bottom: var(--spacing-md); + color: var(--color-success); +} + +.no-violations-title { + font-size: 20px; + font-weight: 600; + margin-bottom: var(--spacing-sm); + color: var(--color-text-primary); +} + +.no-violations-description { + color: var(--color-text-secondary); + line-height: 1.6; +} + +/* --------------------------------------------------------- + SEVERITY SECTIONS +--------------------------------------------------------- */ +.severity-section { + margin: var(--spacing-xl) var(--spacing-xxxl); + padding: var(--spacing-xl); + background-color: var(--color-background); + border-radius: var(--border-radius-large); + box-shadow: var(--shadow-4); + border-top: 4px solid transparent; + transition: transform var(--transition-medium), box-shadow var(--transition-medium); +} + + .severity-section:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-8); + } + + .severity-section.critical { + border-top-color: var(--color-critical); + } + + .severity-section.warning { + border-top-color: var(--color-warning); + } + + .severity-section.info { + border-top-color: var(--color-info); + } + +.severity-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--spacing-xl); + padding-bottom: var(--spacing-md); + border-bottom: 1px solid var(--color-border); +} + +.severity-title { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.severity-count { + background-color: var(--color-background-neutral); + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + font-weight: 600; + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: var(--border-radius-medium); + min-width: 24px; + text-align: center; + border: 1px solid var(--color-border); +} + +.severity-section.critical .severity-count { + background-color: var(--color-critical-bg); + color: var(--color-critical); + border-color: var(--color-critical); +} + +.severity-section.warning .severity-count { + background-color: var(--color-warning-bg); + color: var(--color-warning); + border-color: var(--color-warning); +} + +.severity-section.info .severity-count { + background-color: var(--color-info-bg); + color: var(--color-info); + border-color: var(--color-info); +} + +/* --------------------------------------------------------- + TABLES +--------------------------------------------------------- */ +.table-container { + overflow-x: auto; + border-radius: var(--border-radius-medium); + border: 1px solid var(--color-border); + background-color: var(--color-background); + position: relative; +} + + .table-container table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + font-size: var(--font-size-sm); + min-width: 100%; + } + + .table-container thead { + background-color: var(--color-background-alt); + position: sticky; + top: calc(var(--z-index-sticky) + 70px); /* Компенсируем высоту заголовка файла */ + z-index: var(--z-index-fixed); + border-bottom: 2px solid var(--color-border); + } + +th { + font-weight: 600; + text-align: left; + color: var(--color-text-primary); + padding: var(--spacing-md) var(--spacing-lg); + border-bottom: 2px solid var(--color-border); + white-space: nowrap; + background-color: var(--color-background-alt); +} + +td { + padding: var(--spacing-md) var(--spacing-lg); + border-bottom: 1px solid var(--color-border); + color: var(--color-text-primary); + vertical-align: top; +} + +tbody tr { + transition: background-color var(--transition-fast); +} + + tbody tr:last-child td { + border-bottom: none; + } + +/* Table cell specific styles */ +td.line, +td.column { + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: var(--font-size-xs); + color: var(--color-text-secondary); + text-align: center; + width: 60px; + font-feature-settings: "tnum"; + font-variant-numeric: tabular-nums; +} + +td.index { + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: var(--font-size-xs); + color: var(--color-text-tertiary); + text-align: center; + width: 40px; + font-feature-settings: "tnum"; + font-variant-numeric: tabular-nums; +} + +td.rule { + font-weight: 600; + color: var(--color-text-primary); + width: 250px; +} + +td.description { + color: var(--color-text-primary); + line-height: 1.4; +} + +/* Подсветка строк в зависимости от severity */ +.severity-section.critical tbody tr { + border-left: 3px solid var(--color-critical); +} + + .severity-section.critical tbody tr:hover { + background-color: rgba(209, 52, 56, 0.06); + } + +.severity-section.warning tbody tr { + border-left: 3px solid var(--color-warning); +} + + .severity-section.warning tbody tr:hover { + background-color: rgba(255, 170, 68, 0.06); + } + +.severity-section.info tbody tr { + border-left: 3px solid var(--color-info); +} + + .severity-section.info tbody tr:hover { + background-color: rgba(0, 120, 212, 0.06); + } + +/* --------------------------------------------------------- + TABS NAVIGATION +--------------------------------------------------------- */ +.tabs-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: var(--color-background); + border-top: 1px solid var(--color-border); + box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.08); + z-index: var(--z-index-fixed); + padding: var(--spacing-sm) var(--spacing-xxxl); +} + +.tabs { + 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-sm) var(--spacing-md); + background: none; + border: none; + border-bottom: 2px solid transparent; + color: var(--color-text-secondary); + font-family: var(--font-family); + font-size: var(--font-size-md); + font-weight: 600; + cursor: pointer; + 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: auto; + display: flex; + 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 { + background-color: var(--color-background-neutral); + color: var(--color-text-primary); + } + + .tab.active { + color: var(--color-primary); + border-bottom-color: var(--color-primary); + background-color: var(--color-background-neutral); + } + +.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: 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-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; +} + +/* --------------------------------------------------------- + DIAGRAM VIEWER +--------------------------------------------------------- */ +#mermaid.file-report { + position: fixed; + inset: 0; + width: 100vw; + height: 100vh; + background-color: var(--color-background); + z-index: var(--z-index-modal); + display: none; + padding: 0 !important; + margin: 0 !important; +} + + #mermaid.file-report.active { + display: block; + } + +/* Для диаграммы убираем заголовок */ +#mermaid .file-title-container { + display: none; +} + +/* Diagram Toolbar */ +.diagram-toolbar { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 52px; + display: flex; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-sm) var(--spacing-xxxl); + background-color: var(--color-background); + border-bottom: 1px solid var(--color-border); + box-shadow: var(--shadow-4); + z-index: var(--z-index-fixed); +} + +.toolbar-search { + flex: 1; + max-width: 400px; + position: relative; +} + + .toolbar-search input { + width: 100%; + padding: var(--spacing-md) var(--spacing-lg); + padding-right: 40px; + border: 1px solid var(--color-border-input); + border-radius: var(--border-radius-medium); + font-family: var(--font-family); + font-size: var(--font-size-md); + background-color: var(--color-background); + color: var(--color-text-primary); + transition: border-color var(--transition-medium), box-shadow var(--transition-medium), background-color var(--transition-medium); + } + + .toolbar-search input:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 1px var(--color-primary-light); + } + + .toolbar-search input::placeholder { + color: var(--color-text-tertiary); + } + +.search-clear { + position: absolute; + right: var(--spacing-md); + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--color-text-tertiary); + cursor: pointer; + padding: var(--spacing-xs); + font-size: var(--font-size-sm); + opacity: 0.7; + transition: opacity var(--transition-fast), background-color var(--transition-fast); + border-radius: 50%; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; +} + + .search-clear:hover { + opacity: 1; + background-color: var(--color-background-neutral); + } + +.toolbar-actions { + display: flex; + align-items: center; + gap: var(--spacing-sm); + margin-left: auto; +} + +.toolbar-button { + padding: var(--spacing-md) var(--spacing-lg); + border: none; + border-radius: var(--border-radius-medium); + background-color: var(--color-primary); + color: #ffffff; + font-family: var(--font-family); + font-size: var(--font-size-md); + font-weight: 600; + cursor: pointer; + transition: background-color var(--transition-medium), box-shadow var(--transition-medium), transform var(--transition-medium); + display: inline-flex; + align-items: center; + gap: var(--spacing-sm); + min-height: 40px; +} + + .toolbar-button:hover { + background-color: var(--color-primary-dark); + box-shadow: var(--shadow-4); + } + + .toolbar-button:active { + background-color: var(--color-primary-darker); + transform: translateY(1px); + } + + .toolbar-button.secondary { + background-color: transparent; + color: var(--color-text-primary); + border: 1px solid var(--color-border); + } + + .toolbar-button.secondary:hover { + background-color: var(--color-background-neutral); + border-color: var(--color-border-strong); + } + +/* Diagram Container */ +#diagramContainer { + position: absolute; + top: 52px; + left: 0; + width: 100vw; + height: calc(100vh - 112px); /* 52px тулбар + ~60px табы */ + overflow: hidden; + background-color: var(--color-background); +} + +#diagramSvgContainer { + width: 100%; + height: 100%; + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + + #diagramSvgContainer svg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + cursor: grab; + overflow: visible; + display: block; + transition: transform 0.3s ease-out; /* Плавный переход при изменении масштаба */ + } + + #diagramSvgContainer svg:active { + cursor: grabbing; + } + + /* Индикатор загрузки для диаграммы */ + #diagramSvgContainer::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 40px; + height: 40px; + border: 3px solid var(--color-border); + border-top-color: var(--color-primary); + border-radius: 50%; + animation: spin 1s linear infinite; + z-index: 100; + opacity: 0; + transition: opacity 0.3s; + } + + #diagramSvgContainer.loading::before { + opacity: 1; + } + +@keyframes spin { + to { + transform: translate(-50%, -50%) rotate(360deg); + } +} + +/* Minimap */ +#minimap { + position: absolute; + right: var(--spacing-xxl); + bottom: 120px; + width: 200px; + height: 140px; + border: 1px solid var(--color-border-strong); + background-color: var(--color-background); + /* убран backdrop-filter для производительности */ + border-radius: var(--border-radius-large); + box-shadow: var(--shadow-8); + z-index: var(--z-index-popover); + overflow: hidden; +} + +@media (prefers-color-scheme: dark) { + #minimap { + background-color: var(--color-background-alt); + border-color: var(--color-border-input); + } +} + +.minimap-viewport { + position: absolute; + border: 2px solid var(--color-primary); + background-color: rgba(0, 120, 212, 0.15); + pointer-events: none; + transition: all 0.1s ease; + box-sizing: border-box; +} + +@media (prefers-color-scheme: dark) { + .minimap-viewport { + border-color: var(--color-primary); + background-color: rgba(40, 153, 245, 0.2); + } +} + +/* Упрощаем отображение элементов в миникарте */ +#minimap svg { + width: 100%; + height: 100%; +} + +#minimap .node rect { + stroke-width: 1px !important; + rx: 2px !important; + ry: 2px !important; +} + +#minimap .edgePath path { + stroke-width: 1px !important; +} + +#minimap text { + font-size: 4px !important; +} + +/* Search Results Info */ +.search-results-info { + position: absolute; + top: 70px; + left: var(--spacing-xxl); + background: var(--color-background); + padding: var(--spacing-md) var(--spacing-lg); + border-radius: var(--border-radius-large); + box-shadow: var(--shadow-8); + border: 1px solid var(--color-border); + font-size: var(--font-size-sm); + z-index: var(--z-index-popover); + display: flex; + align-items: center; + gap: var(--spacing-md); +} + +/* --------------------------------------------------------- + MERMAID DIAGRAM STYLING +--------------------------------------------------------- */ + +#diagramSvgContainer svg { + font-family: var(--font-family); + font-size: var(--font-size-md); +} + + #diagramSvgContainer svg .label { + font-family: var(--font-family); + } + +/* Nodes */ +svg .node rect { + fill: var(--color-mermaid-node); + stroke: var(--color-mermaid-node-border); + stroke-width: 2px; + rx: 6px; + ry: 6px; +} + +svg .node circle, +svg .node ellipse, +svg .node polygon { + fill: var(--color-primary-lighter); + stroke: var(--color-mermaid-node-border); + stroke-width: 2px; +} + +svg .node text { + fill: var(--color-mermaid-text); + font-family: var(--font-family); + font-size: var(--font-size-md); + font-weight: 500; +} + +/* Clusters */ +svg .cluster rect { + fill: rgba(243, 242, 241, 0.9); + stroke: var(--color-text-tertiary); + stroke-width: 1.5px; + stroke-dasharray: 5, 5; + rx: 8px; + ry: 8px; +} + +svg .cluster text { + fill: var(--color-text-secondary); + font-size: var(--font-size-lg); + font-weight: 600; +} + +/* Arrow and Line Styling */ +svg marker path { + fill: var(--color-mermaid-edge); +} + +svg .arrowheadPath { + fill: var(--color-mermaid-edge) !important; +} + +svg .edgePath path { + stroke: var(--color-mermaid-edge); + stroke-width: 2px; + fill: none; +} + +svg .edgeLabel text { + fill: var(--color-mermaid-text); + font-size: var(--font-size-sm); +} + +svg .edgeLabel rect { + fill: var(--color-background); + stroke: var(--color-border); + stroke-width: 1px; + rx: 4px; + ry: 4px; +} + +/* Text Labels */ +svg .label text { + font-size: var(--font-size-sm); + font-weight: 400; +} + +/* Dark Theme Mermaid Styles */ +@media (prefers-color-scheme: dark) { + svg .node rect { + fill: var(--color-mermaid-node); + stroke: var(--color-mermaid-node-border); + } + + svg .node circle, + svg .node ellipse, + svg .node polygon { + fill: var(--color-background-neutral); + stroke: var(--color-mermaid-node-border); + } + + svg .node text { + fill: var(--color-mermaid-text); + font-weight: 400; + } + + svg .cluster rect { + fill: rgba(32, 31, 30, 0.9); + stroke: var(--color-text-tertiary); + stroke-dasharray: 5, 5; + } + + svg .cluster text { + fill: var(--color-text-tertiary); + font-weight: 500; + } + + svg .edgePath path { + stroke: var(--color-mermaid-edge); + } + + svg .edgeLabel text { + fill: var(--color-mermaid-text); + } + + svg .edgeLabel rect { + fill: var(--color-background-alt); + stroke: var(--color-border-strong); + } + + svg .label text { + fill: var(--color-text-tertiary); + } + + #diagramSvgContainer svg, + #diagramContainer { + background-color: var(--color-background); + } +} + +/* Node Type Specific Styles */ +svg .node.condition rect { + fill: var(--color-warning-bg); + stroke: var(--color-warning); +} + +svg .node.process rect { + fill: var(--color-info-bg); + stroke: var(--color-info); +} + +svg .node.start rect, +svg .node.end rect { + fill: var(--color-primary-lighter); + stroke: var(--color-primary); +} + +svg .node.import rect { + fill: var(--color-success-bg); + stroke: var(--color-success); +} + +svg .node.exec rect { + fill: #f0f0ff; + stroke: #4b4bd8; +} + +@media (prefers-color-scheme: dark) { + svg .node.start rect, + svg .node.end rect { + fill: var(--color-primary-light); + stroke: var(--color-primary); + } + + svg .node.exec rect { + fill: #2d2a4d; + stroke: #4b4bd8; + } +} + +/* Node Highlighting (упрощённо, без тяжёлых фильтров) */ +.node-search-match rect { + stroke: #ffc107 !important; + stroke-width: 3px !important; +} + +.node-highlight rect { + stroke: #ff5722 !important; + stroke-width: 3px !important; +} + +.node-search-active rect { + stroke: #4caf50 !important; + stroke-width: 3px !important; +} + +/* Усиление видимости линий */ +svg .edgePath, +svg .edgePath path, +svg .flowchart-link { + stroke: var(--color-mermaid-edge) !important; + stroke-width: 2.4px !important; + fill: none !important; + opacity: 1 !important; + vector-effect: non-scaling-stroke !important; +} + + svg .edgePath:hover .path, + svg .edgePath:hover path { + stroke-width: 3px !important; + } + +/* --------------------------------------------------------- + TOAST NOTIFICATIONS +--------------------------------------------------------- */ +.toast { + position: fixed; + top: 80px; + right: var(--spacing-xxl); + background: var(--color-background); + color: var(--color-text-primary); + padding: var(--spacing-md) var(--spacing-lg); + border-radius: var(--border-radius-large); + box-shadow: var(--shadow-8); + border: 1px solid var(--color-border); + z-index: var(--z-index-tooltip); + display: flex; + align-items: center; + gap: var(--spacing-md); + max-width: 400px; + border-left: 4px solid var(--color-primary); +} + +/* --------------------------------------------------------- + SCROLLBAR STYLING +--------------------------------------------------------- */ +::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +::-webkit-scrollbar-track { + background: var(--color-background-neutral); + border-radius: var(--border-radius-medium); +} + +::-webkit-scrollbar-thumb { + background: var(--color-border-strong); + border-radius: var(--border-radius-medium); + border: 3px solid var(--color-background-neutral); +} + + ::-webkit-scrollbar-thumb:hover { + background: var(--color-text-tertiary); + } + +::-webkit-scrollbar-corner { + background: var(--color-background-neutral); +} + +/* For Firefox */ +* { + scrollbar-width: thin; + scrollbar-color: var(--color-border-strong) var(--color-background-neutral); +} + +/* --------------------------------------------------------- + RESPONSIVE ADJUSTMENTS +--------------------------------------------------------- */ +@media (max-width: 768px) { + .severity-section { + margin: var(--spacing-lg); + padding: var(--spacing-lg); + } + + .tabs-container { + padding: var(--spacing-sm) var(--spacing-lg); + } + + .diagram-toolbar { + padding: var(--spacing-sm) var(--spacing-lg); + } + + .toolbar-actions { + flex-wrap: wrap; + justify-content: flex-end; + } + + .toolbar-button { + padding: var(--spacing-sm) var(--spacing-md); + font-size: var(--font-size-sm); + } + + #minimap { + right: var(--spacing-lg); + bottom: 100px; + width: 150px; + height: 105px; + } + + .search-results-info { + left: var(--spacing-lg); + right: var(--spacing-lg); + max-width: none; + } + + .toast { + right: var(--spacing-lg); + left: var(--spacing-lg); + max-width: none; + } + + .file-title-container { + padding: var(--spacing-lg); + } + + .file-title { + font-size: var(--font-size-lg); + } + + #diagramContainer { + height: calc(100vh - 102px); + } +} + +@media (max-width: 480px) { + :root { + --spacing-xxxl: 24px; + --spacing-xxl: 20px; + --spacing-xl: 16px; + --spacing-lg: 12px; + --spacing-md: 8px; + --spacing-sm: 6px; + --spacing-xs: 4px; + } + + .severity-header { + flex-direction: column; + align-items: flex-start; + gap: var(--spacing-md); + } + + .toolbar-search { + max-width: none; + } + + .toolbar-actions { + width: 100%; + justify-content: space-between; + } + + .tabs { + gap: 2px; + } + + .tab { + padding: var(--spacing-sm) var(--spacing-md); + font-size: var(--font-size-sm); + } +} + +/* --------------------------------------------------------- + PRINT STYLES +--------------------------------------------------------- */ +@media print { + .tabs-container, + .diagram-toolbar, + #minimap, + .search-results-info, + .toast { + display: none !important; + } + + body { + background-color: #ffffff; + color: #000000; + } + + .file-report { + padding-bottom: 0; + display: block !important; + } + + .severity-section { + break-inside: avoid; + box-shadow: none; + border: 1px solid #ddd; + } + + table { + break-inside: avoid; + } +} + +/* --------------------------------------------------------- + ACCESSIBILITY +--------------------------------------------------------- */ +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; + border-radius: var(--border-radius-small); +} + +::selection { + background-color: var(--color-primary); + color: #ffffff; +} + +::-moz-selection { + background-color: var(--color-primary); + color: #ffffff; +} + +/* Убираем blue highlight на mobile tap */ +* { + -webkit-tap-highlight-color: transparent; +} + + +/* Summary page styles */ +.summary-section { + margin: var(--spacing-xl) var(--spacing-xxxl); + padding: var(--spacing-xl); + background-color: var(--color-background); + border-radius: var(--border-radius-large); + box-shadow: var(--shadow-4); + transition: transform var(--transition-medium), box-shadow var(--transition-medium); +} + + .summary-section:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-8); + } + +.summary-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--spacing-xl); + padding-bottom: var(--spacing-md); + border-bottom: 1px solid var(--color-border); +} + +.summary-title { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.summary-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--spacing-lg); + margin-bottom: var(--spacing-xl); +} + +.stat-card { + padding: var(--spacing-lg); + border-radius: var(--border-radius-medium); + background-color: var(--color-background-alt); + border: 1px solid var(--color-border); + transition: all var(--transition-medium); +} + + .stat-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-8); + } + + .stat-card.critical { + border-top: 4px solid var(--color-critical); + } + + .stat-card.warning { + border-top: 4px solid var(--color-warning); + } + + .stat-card.info { + border-top: 4px solid var(--color-info); + } + + .stat-card.success { + border-top: 4px solid var(--color-success); + } + +.stat-value { + font-size: var(--font-size-xxxl); + font-weight: 700; + margin-bottom: var(--spacing-xs); + line-height: 1; +} + +.stat-label { + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + margin-bottom: var(--spacing-xs); +} + +.stat-change { + font-size: var(--font-size-xs); + display: flex; + align-items: center; + gap: 2px; +} + + .stat-change.positive { + color: var(--color-success); + } + + .stat-change.negative { + color: var(--color-critical); + } + +.files-overview { + margin-top: var(--spacing-xxl); +} + +.files-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: var(--spacing-lg); +} + +.file-card { + padding: var(--spacing-lg); + border-radius: var(--border-radius-medium); + background-color: var(--color-background-alt); + border: 1px solid var(--color-border); + transition: all var(--transition-medium); + display: flex; + flex-direction: column; +} + + .file-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-8); + } + +.file-card-header { + display: flex; + align-items: flex-start; /* Change from center to flex-start */ + justify-content: space-between; + margin-bottom: var(--spacing-md); + gap: var(--spacing-sm); /* Add gap between name and badges */ +} + + +.file-name-small { + font-weight: 600; + color: var(--color-text-primary); + /* Remove truncation properties */ + overflow: visible; + text-overflow: clip; + white-space: normal; /* Change from nowrap to normal */ + word-wrap: break-word; + word-break: break-word; + flex: 1; + line-height: 1.4; + max-height: none; /* Remove max-height if exists */ + display: block; /* Change from -webkit-box */ + -webkit-line-clamp: unset; /* Remove line clamp */ + -webkit-box-orient: unset; +} + +.file-violations { + display: flex; + gap: var(--spacing-xs); + justify-content: flex-end; + flex-shrink: 0; + align-items: flex-start; /* Align badges to top */ +} + +.violation-badge { + font-size: var(--font-size-xs); + padding: var(--spacing-xxs) var(--spacing-xxs); + border-radius: var(--border-radius-small); + font-weight: 600; + min-width: 22px; + height: 22px; + text-align: center; +} + + .violation-badge.critical { + background-color: var(--color-critical-bg); + color: var(--color-critical); + border: 1px solid var(--color-critical); + } + + .violation-badge.warning { + background-color: var(--color-warning-bg); + color: var(--color-warning); + border: 1px solid var(--color-warning); + } + + .violation-badge.info { + background-color: var(--color-info-bg); + color: var(--color-info); + border: 1px solid var(--color-info); + } + +.progress-bar { + height: 8px; + background-color: var(--color-background-neutral); + border-radius: var(--border-radius-small); + overflow: hidden; + margin-top: var(--spacing-md); + display: flex; /* Изменено с block на flex */ +} + +.progress-fill { + height: 100%; + transition: width var(--transition-medium); + flex-shrink: 0; /* Предотвращает сжатие */ +} + + .progress-fill.critical { + background-color: var(--color-critical); + order: 1; + } + + .progress-fill.warning { + background-color: var(--color-warning); + order: 2; + } + + .progress-fill.info { + background-color: var(--color-info); + order: 3; + } + +@media (max-width: 768px) { + .summary-section { + margin: var(--spacing-lg); + padding: var(--spacing-lg); + } + + .summary-stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .files-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 480px) { + .summary-stats-grid { + grid-template-columns: 1fr; + } + + .stat-value { + font-size: var(--font-size-xxl); + } +} \ No newline at end of file diff --git a/SQLLinter/Infrastructure/Reporters/Formatters/Html/v2/HtmlFormatter_v2.js b/SQLLinter/Infrastructure/Reporters/Formatters/Html/v2/HtmlFormatter_v2.js new file mode 100644 index 0000000..f96eafe --- /dev/null +++ b/SQLLinter/Infrastructure/Reporters/Formatters/Html/v2/HtmlFormatter_v2.js @@ -0,0 +1,1894 @@ + +import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs"; + +/* --------------------------------------------------------- + REPORT RENDERER - динамическое создание таблиц и табов +--------------------------------------------------------- */ +class ReportRenderer { + constructor(data) { + this.data = data; + this.files = data.files || data.f || []; + this.rules = data.rules || data.r || {}; + this.diagram = data.diagram || data.d || {}; + this.currentFileIndex = 0; + + this.reportsContainer = document.getElementById('reports-container'); + this.tabsList = document.getElementById('tabs-list'); + + if (!this.reportsContainer || !this.tabsList) { + console.error('Не найдены контейнеры для отчета'); + return; + } + } + + init() { + this.renderTabs(); + this.setupTabsDragScroll(); + this.renderFileReports(); + this.setupTabNavigation(); + this.activateTab(0); + } + + renderTabs() { + const tabsList = document.getElementById('tabs-list'); + if (!tabsList) return; + + this.tabsList.innerHTML = ''; + + //Add file tabs + 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 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 || ''} +
+ `; + + tab.innerHTML = ` +
+ ${this.escapeHtml(file.n)} + ${countersHTML} +
+ `; + + this.tabsList.appendChild(tab); + }); + + // Add Summary tab + const summaryTab = document.createElement('button'); + summaryTab.className = 'tab'; + summaryTab.dataset.target = 'summary_report'; + summaryTab.innerHTML = `
Summary
`; + tabsList.appendChild(summaryTab); + + //Add diagram tab if exists + if (this.diagram.h || this.diagram.hasDiagram) { + const diagramTab = document.createElement('button'); + diagramTab.className = 'tab'; + diagramTab.dataset.target = 'mermaid'; + // Создаем пустые счетчики для диаграммы + + diagramTab.innerHTML = ` +
+ Диаграмма +
+ `; + + 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; + + this.reportsContainer.innerHTML = ''; + + // Рендеринг отчетов по файлам + this.files.forEach((file, index) => { + const reportDiv = document.createElement('div'); + reportDiv.id = `file_${index}`; + reportDiv.className = 'file-report'; + reportDiv.style.display = 'none'; + + const fileName = file.n || file.fileName || ''; + const violations = file.v || file.violations || {}; + + // Заголовок файла + reportDiv.innerHTML = ` +
+
+ + + + + ${this.escapeHtml(fileName)} +
+
+ `; + + // Добавляем секции нарушений + if (file.v.c?.length > 0) { + reportDiv.appendChild(this.createViolationSection('critical', file.v.c)); + } + if (file.v.w?.length > 0) { + reportDiv.appendChild(this.createViolationSection('warning', file.v.w)); + } + if (file.v.i?.length > 0) { + reportDiv.appendChild(this.createViolationSection('info', file.v.i)); + } + + this.reportsContainer.appendChild(reportDiv); + }); + + // Контейнер для диаграммы (если есть) + if (this.diagram.h || this.diagram.hasDiagram) { + const diagramContent = this.diagram.c || this.diagram.Content || ''; + const diagramDiv = document.createElement('div'); + diagramDiv.id = 'mermaid'; + diagramDiv.className = 'file-report'; + diagramDiv.style.display = 'none'; + + diagramDiv.innerHTML = ` +
+ +
+ + +
+
+
+
+
+
+ + `; + + this.reportsContainer.appendChild(diagramDiv); + } + + this.renderSummaryReport(); + } + + createViolationSection(severity, violations) { + const severityTitle = { + critical: 'Critical', + warning: 'Warning', + info: 'Info' + }[severity] || 'Unknown'; + + const section = document.createElement('div'); + section.className = `severity-section ${severity}`; + + const tableRows = violations.map(violation => { + // Получаем правило по ID + const ruleId = violation.r || violation.ruleId; + const rule = this.rules[ruleId] || { n: 'Unknown', t: 'Описание отсутствует' }; + + // Формируем текст с подстановкой параметров + let text = rule.t || ''; + const args = violation.a || violation.args || []; + + if (args.length > 0 && text.includes('{')) { + args.forEach((arg, index) => { + text = text.replace(new RegExp(`\\{${index}\\}`, 'g'), arg); + }); + } + + return ` + + ${violation.i || violation.index} + ${violation.l || violation.line} + ${violation.c || violation.column} + ${this.escapeHtml(rule.n || rule.name)} + ${this.escapeHtml(text)} + + `; + }).join(''); + + section.innerHTML = ` +
+
+

${severityTitle}

+ ${violations.length} +
+
+
+ + + + + + + + + + + + ${tableRows} + +
#СтрокаКолонкаПравилоОписание
+
+ `; + + return section; + } + + setupTabNavigation() { + const tabs = this.tabsList.querySelectorAll('.tab'); + + tabs.forEach(tab => { + tab.addEventListener('click', (e) => { + e.preventDefault(); + + const targetId = tab.dataset.target; + const index = tab.dataset.index; + + // Обновляем активный таб + tabs.forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + + // Скрываем все отчеты + document.querySelectorAll('.file-report').forEach(report => { + report.style.display = 'none'; + }); + + // Показываем выбранный отчет + const targetReport = document.getElementById(targetId); + if (targetReport) { + targetReport.style.display = 'block'; + + // Если это диаграмма, инициализируем ее + if (targetId === 'mermaid' && window.viewer) { + window.viewer.render().catch(console.error); + } + + // Прокрутка к верху + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }); + }); + } + + activateTab(index) { + const tab = this.tabsList.querySelector(`.tab[data-index="${index}"]`); + if (tab) { + tab.click(); + } + } + + calculateTotalViolations(file) { + return (file.v.c?.length || 0) + (file.v.w?.length || 0) + (file.v.i?.length || 0); + } + + renderSummaryReport() { + const summaryDiv = document.createElement('div'); + summaryDiv.id = 'summary_report'; + summaryDiv.className = 'file-report'; + summaryDiv.style.display = 'none'; + + // Calculate total statistics + let totalCritical = 0; + let totalWarning = 0; + let totalInfo = 0; + let totalFiles = this.files.length; + + this.files.forEach(file => { + totalCritical += file.v.c?.length || 0; + totalWarning += file.v.w?.length || 0; + totalInfo += file.v.i?.length || 0; + }); + + const totalViolations = totalCritical + totalWarning + totalInfo; + + // Calculate percentages for progress bars - FIXED + let criticalPercent = totalViolations > 0 ? (totalCritical / totalViolations * 100) : 0; + let warningPercent = totalViolations > 0 ? (totalWarning / totalViolations * 100) : 0; + let infoPercent = totalViolations > 0 ? (totalInfo / totalViolations * 100) : 0; + + // Ensure sum equals 100% + const totalPercent = criticalPercent + warningPercent + infoPercent; + if (totalPercent > 0 && Math.abs(totalPercent - 100) > 0.01) { + // Adjust the largest segment to make sum = 100% + const maxVal = Math.max(criticalPercent, warningPercent, infoPercent); + if (maxVal === criticalPercent) { + criticalPercent = 100 - warningPercent - infoPercent; + } else if (maxVal === warningPercent) { + warningPercent = 100 - criticalPercent - infoPercent; + } else { + infoPercent = 100 - criticalPercent - warningPercent; + } + } + + // Format for display + const criticalPercentDisplay = criticalPercent.toFixed(1); + const warningPercentDisplay = warningPercent.toFixed(1); + const infoPercentDisplay = infoPercent.toFixed(1); + + summaryDiv.innerHTML = ` +
+
+ + + + Сводный отчет +
+
+
+
+
+

Общая статистика

+
+
+
+
+
${totalFiles}
+
Всего файлов
+
+
+
${totalCritical}
+
Critical нарушений
+
+
+
${totalWarning}
+
Warning нарушений
+
+
+
${totalInfo}
+
Info нарушений
+
+
+
+
+
+
+
+
+
+ Critical: ${totalCritical} (${criticalPercentDisplay}%) + Warning: ${totalWarning} (${warningPercentDisplay}%) + Info: ${totalInfo} (${infoPercentDisplay}%) +
+
+
+
+
+
+

Статистика файлов

+ ${totalFiles} +
+
+
+ ${this.files.map(file => { + const critical = file.v.c?.length || 0; + const warning = file.v.w?.length || 0; + const info = file.v.i?.length || 0; + const total = critical + warning + info; + + // Calculate percentages for file progress bar - FIXED + let fileCriticalPercent = total > 0 ? (critical / total * 100) : 0; + let fileWarningPercent = total > 0 ? (warning / total * 100) : 0; + let fileInfoPercent = total > 0 ? (info / total * 100) : 0; + + // Ensure sum equals 100% + const fileTotalPercent = fileCriticalPercent + fileWarningPercent + fileInfoPercent; + if (fileTotalPercent > 0 && Math.abs(fileTotalPercent - 100) > 0.01) { + // Adjust the largest segment to make sum = 100% + const maxVal = Math.max(fileCriticalPercent, fileWarningPercent, fileInfoPercent); + if (maxVal === fileCriticalPercent) { + fileCriticalPercent = 100 - fileWarningPercent - fileInfoPercent; + } else if (maxVal === fileWarningPercent) { + fileWarningPercent = 100 - fileCriticalPercent - fileInfoPercent; + } else { + fileInfoPercent = 100 - fileCriticalPercent - fileWarningPercent; + } + } + + return `
+
+ ${this.escapeHtml(file.n)} +
+
+
+
+
+
+
+ Всего: ${total} +
+ ${critical} + ${warning} + ${info} +
+
+
`; + }).join('')} +
+
+ `; + + this.reportsContainer.appendChild(summaryDiv); + } + + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } +} + +/* --------------------------------------------------------- + ГЛОБАЛЬНАЯ ИНИЦИАЛИЗАЦИЯ +--------------------------------------------------------- */ +function initReport() { + if (!window.reportData) { + console.error('Данные отчета не найдены'); + return; + } + + const renderer = new ReportRenderer(window.reportData); + renderer.init(); + + // Сохраняем рендерер в глобальной области видимости + window.reportRenderer = renderer; +} + +// Экспортируем функции для глобального использования +window.initReport = initReport; + +/* --------------------------------------------------------- + TABS MANAGEMENT +--------------------------------------------------------- */ +function initTabs(onTabActivated) { + const tabs = document.querySelectorAll(".tab"); + const reports = document.querySelectorAll(".file-report"); + + tabs.forEach(tab => { + tab.addEventListener("click", () => { + const id = tab.dataset.target; + + // Add ripple effect + const ripple = document.createElement('span'); + const rect = tab.getBoundingClientRect(); + const size = Math.max(rect.width, rect.height); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + + ripple.style.cssText = ` + position: absolute; + border-radius: 50%; + background: rgba(0, 120, 212, 0.2); + transform: scale(0); + animation: ripple 0.6s linear; + width: ${size}px; + height: ${size}px; + left: ${x - size / 2}px; + top: ${y - size / 2}px; + `; + + /* + tab.style.position = 'relative'; + tab.style.overflow = 'hidden'; + */ + tab.appendChild(ripple); + + setTimeout(() => { + if (ripple.parentElement === tab) { + ripple.remove(); + } + }, 600); + + tabs.forEach(t => t.classList.remove("active")); + reports.forEach(r => r.classList.remove("active")); + + tab.classList.add("active"); + const report = document.getElementById(id); + if (report) { + report.classList.add("active"); + + // Scroll to top when switching tabs + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + + if (onTabActivated) onTabActivated(id); + }); + }); +} + +// Исправление для табов на странице диаграммы +document.addEventListener('DOMContentLoaded', function () { + const tabsContainer = document.querySelector('.tabs-container'); + if (tabsContainer) { + tabsContainer.style.zIndex = '1001'; + tabsContainer.style.position = 'fixed'; + tabsContainer.style.bottom = '0'; + tabsContainer.style.left = '0'; + tabsContainer.style.right = '0'; + } + + // Гарантируем, что табы всегда поверх всего + const tabs = document.querySelectorAll('.tab'); + tabs.forEach(tab => { + tab.style.zIndex = '1002'; + }); +}); + +/* --------------------------------------------------------- + DIAGRAM VIEWER CLASS +--------------------------------------------------------- */ +class DiagramViewer { + constructor() { + this.container = document.getElementById("diagramSvgContainer"); + this.minimap = document.getElementById("minimap"); + this.searchInput = document.getElementById("diagramSearch"); + this.resetBtn = document.getElementById("resetViewBtn"); + + this.svg = null; + this.viewportGroup = null; + this.nodes = []; + + this.scale = 1; + this.tx = 0; + this.ty = 0; + this.minScale = 0.3; + this.maxScale = 12; + + this.originalViewBox = null; + this.minimapSvg = null; + this.minimapViewport = null; + + this.needsRender = false; + this.isPanning = false; + this.lastSearchTerm = ''; + this.searchResults = []; + this.currentSearchIndex = -1; + + this.init(); + } + + init() { + // Add CSS for ripple animation + if (!document.querySelector('#ripple-styles')) { + const style = document.createElement('style'); + style.id = 'ripple-styles'; + style.textContent = ` + @keyframes ripple { + to { + transform: scale(4); + opacity: 0; + } + } + + @keyframes fadeIn { + from {opacity: 0; transform: translateY(-10px); } + to {opacity: 1; transform: translateY(0); } + } + `; + document.head.appendChild(style); + } + } + + async render() { + this.container.classList.add('loading'); + + + try { + + const mermaidSource = document.getElementById("mermaidSource").textContent.trim(); + const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + + mermaid.initialize({ + startOnLoad: false, + theme: isDark ? "dark" : "default", + themeVariables: isDark ? { + fontFamily: "'Segoe UI', system-ui, -apple-system, sans-serif", + primaryColor: "#2d2d2d", + primaryBorderColor: "#2899f5", + primaryTextColor: "#f3f2f1", + lineColor: "#797775", + lineWidth: 2.5, + secondaryColor: "#3c3c3c", + tertiaryColor: "#484644", + background: "#1e1e1e", + clusterBkg: "#201f1e", + clusterBorder: "#605e5c", + edgeLabelBackground: "#252423", + nodeBkg: "#2d2d2d", + nodeBorder: "#2899f5", + nodeTextColor: "#f3f2f1", + mainBkg: "#1e1e1e", + textColor: "#f3f2f1", + arrowheadColor: "#797775", + fontSize: "14px" + } : { + fontFamily: "'Segoe UI', system-ui, -apple-system, sans-serif", + primaryColor: "#f0f8ff", + primaryBorderColor: "#0078d4", + primaryTextColor: "#323130", + lineColor: "#8a8886", + lineWidth: 2.5, + secondaryColor: "#deecf9", + tertiaryColor: "#c7e0f4", + background: "#ffffff", + clusterBkg: "#f3f2f1", + clusterBorder: "#a19f9d", + edgeLabelBackground: "#ffffff", + nodeBkg: "#f0f8ff", + nodeBorder: "#0078d4", + nodeTextColor: "#323130", + mainBkg: "#ffffff", + textColor: "#323130", + arrowheadColor: "#8a8886", + fontSize: "14px" + }, + flowchart: { + useMaxWidth: false, + htmlLabels: true, + curve: "basis" + } + }); + + try { + const { svg } = await mermaid.render("diagram", mermaidSource); + + // Убираем только встроенные стили Mermaid, оставляя структуру + const cleanedSvg = svg.replace(/]*>[\s\S]*?<\/style>/gi, ""); + + this.container.innerHTML = cleanedSvg; + this.svg = this.container.querySelector("svg"); + + if (this.svg) { + // Добавляем небольшую задержку для гарантированной отрисовки + await new Promise(resolve => setTimeout(resolve, 50)); + + // Добавляем классы для разных типов узлов + this._addNodeClasses(); + this._afterSvgLoaded(); + this._initSearch(); + this._attachEventListeners(); + this._setupResizeObserver(); + this.container.classList.remove('loading'); + } + } catch (error) { + console.error('Mermaid rendering error:', error); + this._showError(error); + } + } catch (error) { + this.container.classList.remove('loading'); + this._showError(error); + } + } + + recalculateBounds() { + this._calculateRealBounds(); + this._updateMinimapViewport(); + } + + _showError(error) { + this.container.innerHTML = ` +
+
+
📊
+

+ Не удалось загрузить диаграмму +

+

+ ${error.message} +

+ +
+
+ `; + } + + _addNodeClasses() { + // Добавляем классы к узлам на основе текста + const nodes = this.svg.querySelectorAll('.node'); + nodes.forEach(node => { + const textElement = node.querySelector('text'); + if (textElement) { + const text = textElement.textContent.toLowerCase(); + + if (text.includes('if') || text.includes('condition') || text.includes('условие')) { + node.classList.add('condition'); + } else if (text.includes('start') || text.includes('end') || + text.includes('начало') || text.includes('конец')) { + node.classList.add('start'); + } else if (text.includes('exec') || text.includes('execute') || + text.includes('выполнить') || text.includes('выполнение')) { + node.classList.add('exec'); + } else if (text.includes('select') || text.includes('insert') || + text.includes('update') || text.includes('delete') || + text.includes('выбрать') || text.includes('вставить') || + text.includes('обновить') || text.includes('удалить')) { + node.classList.add('process'); + } else if (text.includes('import') || text.includes('export') || + text.includes('импорт') || text.includes('экспорт')) { + node.classList.add('import'); + } + } + }); + } + + _setupResizeObserver() { + const resizeObserver = new ResizeObserver(() => { + if (this.svg) { + this._updateMinimapViewport(); + } + }); + + resizeObserver.observe(this.container); + } + + _attachEventListeners() { + // Add double click to reset zoom + this.svg.addEventListener('dblclick', (e) => { + e.preventDefault(); + this.resetView(); + }); + + // Add keyboard shortcuts + document.addEventListener('keydown', (e) => { + if (e.target === this.searchInput || e.target.matches('input, textarea')) return; + + switch (e.key) { + case '+': + case '=': + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + this.zoomIn(); + } + break; + case '-': + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + this.zoomOut(); + } + break; + case '0': + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + this.resetView(); + } + break; + case 'Escape': + this.clearSearch(); + break; + case 'F3': + case 'Enter': + if (e.shiftKey) { + e.preventDefault(); + this.prevSearchResult(); + } else if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + this.nextSearchResult(); + } + break; + } + }); + + // Reset button + if (this.resetBtn) { + this.resetBtn.addEventListener('click', () => { + this.resetView(); + this._showToast('Вид диаграммы сброшен', 'info'); + }); + } + } + + _initSearch() { + if (this.searchInput) { + // Clear button + const clearBtn = document.createElement('button'); + clearBtn.innerHTML = '✕'; + clearBtn.title = 'Очистить поиск'; + clearBtn.style.cssText = ` + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: 4px; + font-size: 12px; + opacity: 0.7; + transition: opacity 0.2s; + border-radius: 50%; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + `; + + clearBtn.addEventListener('mouseover', () => { + clearBtn.style.opacity = '1'; + clearBtn.style.backgroundColor = 'var(--surface-neutral)'; + }); + + clearBtn.addEventListener('mouseout', () => { + clearBtn.style.opacity = '0.7'; + clearBtn.style.backgroundColor = 'transparent'; + }); + + clearBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.clearSearch(); + this.searchInput.value = ''; + this.searchInput.focus(); + this._showToast('Поиск очищен', 'info'); + }); + + const searchContainer = this.searchInput.parentElement; + searchContainer.style.position = 'relative'; + searchContainer.appendChild(clearBtn); + + this.searchInput.addEventListener('keyup', (e) => { + if (e.key === 'Enter') { + const term = this.searchInput.value.trim(); + if (term) { + this.searchNodes(term); + } else { + this.clearSearch(); + } + } else if (e.key === 'Escape') { + this.clearSearch(); + this.searchInput.value = ''; + } + }); + + // Show clear button only when there's text + this.searchInput.addEventListener('input', () => { + clearBtn.style.display = this.searchInput.value ? 'flex' : 'none'; + }); + + clearBtn.style.display = 'none'; + } + } + + searchNodes(term) { + if (!this.svg || !term) { + this.clearSearch(); + return; + } + + // Clear previous highlights + this.clearSearch(); + + this.lastSearchTerm = term.toLowerCase(); + const nodes = this.svg.querySelectorAll('.node'); + this.searchResults = []; + + nodes.forEach((node) => { + const textElement = node.querySelector('text'); + if (textElement && textElement.textContent.toLowerCase().includes(this.lastSearchTerm)) { + this.searchResults.push(node); + } + }); + + if (this.searchResults.length > 0) { + this.currentSearchIndex = 0; + this._highlightCurrentSearchResult(); + this._showSearchResultsInfo(); + + // Center view on first result + this._centerOnNode(this.searchResults[0]); + } else { + this._showToast('Ничего не найдено', 'warning'); + } + } + + _highlightCurrentSearchResult() { + // Clear all highlights first + this.searchResults.forEach((node, index) => { + node.classList.remove('node-search-match', 'node-highlight', 'node-search-active'); + + if (index === this.currentSearchIndex) { + node.classList.add('node-search-active', 'node-highlight'); + } else { + node.classList.add('node-search-match'); + } + }); + } + + nextSearchResult() { + if (this.searchResults.length === 0) return; + + this.currentSearchIndex = (this.currentSearchIndex + 1) % this.searchResults.length; + this._highlightCurrentSearchResult(); + this._centerOnNode(this.searchResults[this.currentSearchIndex]); + this._updateSearchResultsInfo(); + } + + prevSearchResult() { + if (this.searchResults.length === 0) return; + + this.currentSearchIndex = (this.currentSearchIndex - 1 + this.searchResults.length) % this.searchResults.length; + this._highlightCurrentSearchResult(); + this._centerOnNode(this.searchResults[this.currentSearchIndex]); + this._updateSearchResultsInfo(); + } + + _centerOnNode(node) { + if (!node || !this.svg) return; + + const bbox = node.getBBox(); + const container = document.getElementById("diagramContainer"); + + // Calculate center of node in SVG coordinates + const centerX = bbox.x + bbox.width / 2; + const centerY = bbox.y + bbox.height / 2; + + // Calculate transform to center this point + this.tx = container.clientWidth / 2 - centerX * this.scale; + this.ty = container.clientHeight / 2 - centerY * this.scale; + + this._scheduleRender(); + + // Add a little bounce effect + node.style.transform = 'scale(1.05)'; + setTimeout(() => { + node.style.transform = ''; + }, 300); + } + + _showSearchResultsInfo() { + // Remove existing info if any + this._removeSearchResultsInfo(); + + const info = document.createElement('div'); + info.className = 'search-results-info'; + info.style.cssText = ` + position: absolute; + top: 60px; + left: 16px; + background: var(--surface-default); + padding: var(--spacing-s) var(--spacing-m); + border-radius: var(--border-radius-medium); + box-shadow: var(--shadow-16); + border: 1px solid var(--border-default); + font-size: 13px; + z-index: 1000; + animation: fadeIn 0.3s ease; + display: flex; + align-items: center; + gap: var(--spacing-m); + `; + + info.innerHTML = ` + + Найдено: ${this.searchResults.length} узлов + + ${this.searchResults.length > 1 ? ` +
+ + +
+ + ${this.currentSearchIndex + 1} из ${this.searchResults.length} + + ` : ''} + + `; + + this.container.appendChild(info); + } + + _updateSearchResultsInfo() { + const info = this.container.querySelector('.search-results-info'); + if (info && this.searchResults.length > 1) { + const counter = info.querySelector('span:last-of-type'); + if (counter) { + counter.textContent = `${this.currentSearchIndex + 1} из ${this.searchResults.length}`; + } + } + } + + _removeSearchResultsInfo() { + const info = this.container.querySelector('.search-results-info'); + if (info) { + info.remove(); + } + } + + clearSearch() { + if (this.svg) { + const nodes = this.svg.querySelectorAll('.node'); + nodes.forEach(node => { + node.classList.remove('node-search-match', 'node-highlight', 'node-search-active'); + node.style.transform = ''; + }); + } + + this.searchResults = []; + this.currentSearchIndex = -1; + this._removeSearchResultsInfo(); + } + + _showToast(message, type = 'info') { + const toast = document.createElement('div'); + toast.textContent = message; + toast.style.cssText = ` + position: fixed; + top: 80px; + right: 20px; + background: var(--surface-default); + color: var(--text-primary); + padding: var(--spacing-m) var(--spacing-l); + border-radius: var(--border-radius-medium); + box-shadow: var(--shadow-16); + border: 1px solid var(--border-default); + z-index: 9999; + animation: fadeIn 0.3s ease; + font-size: 14px; + border-left: 4px solid ${type === 'error' ? '#d13438' : + type === 'warning' ? '#ffaa44' : + type === 'success' ? '#107c10' : + 'var(--primary-color)' + }; + `; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.style.opacity = '0'; + toast.style.transform = 'translateY(-10px)'; + setTimeout(() => { + if (toast.parentElement) { + toast.remove(); + } + }, 300); + }, 3000); + } + + zoomIn() { + const zoomFactor = 1.2; + const newScale = Math.min(this.maxScale, this.scale * zoomFactor); + this.scale = newScale; + this._scheduleRender(); + this._showToast(`Масштаб: ${Math.round(this.scale * 100)}%`, 'info'); + } + + zoomOut() { + const zoomFactor = 0.8; + const newScale = Math.max(this.minScale, this.scale * zoomFactor); + this.scale = newScale; + this._scheduleRender(); + this._showToast(`Масштаб: ${Math.round(this.scale * 100)}%`, 'info'); + } + + _afterSvgLoaded() { + const vb = this.svg.viewBox.baseVal; + this.originalViewBox = { + x: vb.x, + y: vb.y, + width: vb.width, + height: vb.height + }; + + this._wrapContent(); + this._initZoomPan(); + + // Используем requestAnimationFrame для получения реальных границ после отрисовки + requestAnimationFrame(() => { + this._calculateRealBounds(); + this._initMinimap(); + this.resetView(); + this._scheduleRender(); + }); + } + + _calculateRealBounds() { + if (!this.viewportGroup) return; + + try { + // Пробуем получить реальные границы содержимого + let bbox; + + // Метод 1: Пробуем получить bbox всей группы + try { + bbox = this.viewportGroup.getBBox(); + } catch (e) { + bbox = null; + } + + // Метод 2: Если getBBox не работает или возвращает 0, используем viewBox + if (!bbox || bbox.width === 0 || bbox.height === 0 || + bbox.x === Infinity || bbox.y === Infinity) { + console.warn("getBBox failed, using originalViewBox"); + this.diagramBounds = { ...this.originalViewBox }; + return; + } + + // Метод 3: Если getBBox работает, но границы слишком малы, проверяем ручной расчет + if (bbox.width < 10 || bbox.height < 10) { + // Пробуем ручной расчет через содержимое + const elements = this.viewportGroup.querySelectorAll('*[x], *[y], *[cx], *[cy]'); + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + let hasValidCoords = false; + + elements.forEach(el => { + let x = parseFloat(el.getAttribute('x') || el.getAttribute('cx') || '0'); + let y = parseFloat(el.getAttribute('y') || el.getAttribute('cy') || '0'); + let width = parseFloat(el.getAttribute('width') || '0'); + let height = parseFloat(el.getAttribute('height') || '0'); + let r = parseFloat(el.getAttribute('r') || '0'); + + if (!isNaN(x) && !isNaN(y)) { + if (el.tagName === 'circle') { + x = x - r; + y = y - r; + width = height = r * 2; + } else if (el.tagName === 'ellipse') { + const rx = parseFloat(el.getAttribute('rx') || '0'); + const ry = parseFloat(el.getAttribute('ry') || '0'); + x = x - rx; + y = y - ry; + width = rx * 2; + height = ry * 2; + } + + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x + width); + maxY = Math.max(maxY, y + height); + hasValidCoords = true; + } + }); + + if (hasValidCoords && minX !== Infinity) { + bbox = { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY + }; + } + } + + // Добавляем отступ в 5% от размера диаграммы + const padding = Math.max(bbox.width, bbox.height) * 0.05; + this.diagramBounds = { + x: bbox.x - padding, + y: bbox.y - padding, + width: bbox.width + padding * 2, + height: bbox.height + padding * 2 + }; + + console.log("Diagram bounds calculated:", this.diagramBounds); + + } catch (error) { + console.error("Error calculating diagram bounds:", error); + this.diagramBounds = { ...this.originalViewBox }; + } + } + + _wrapContent() { + const children = Array.from(this.svg.children); + this.viewportGroup = document.createElementNS("http://www.w3.org/2000/svg", "g"); + this.viewportGroup.setAttribute("id", "viewport-group"); + + children.forEach(child => { + if (child.tagName.toLowerCase() !== "defs") { + this.viewportGroup.appendChild(child.cloneNode(true)); + child.remove(); + } + }); + + this.svg.appendChild(this.viewportGroup); + } + + _initZoomPan() { + let lastX = 0; + let lastY = 0; + let isMouseDown = false; + let lastTouchDistance = 0; + + // Перевод dx/dy из экранных пикселей в SVG-координаты + const screenDeltaToSvgDelta = (dx, dy) => { + const pt1 = this.svg.createSVGPoint(); + const pt2 = this.svg.createSVGPoint(); + pt1.x = 0; + pt1.y = 0; + pt2.x = dx; + pt2.y = dy; + + const ctm = this.svg.getScreenCTM(); + if (!ctm) return { dxSvg: dx, dySvg: dy }; + + const inv = ctm.inverse(); + const w1 = pt1.matrixTransform(inv); + const w2 = pt2.matrixTransform(inv); + + return { + dxSvg: w2.x - w1.x, + dySvg: w2.y - w1.y + }; + }; + + /* --------------------------------------------------------- + PAN (mouse) + --------------------------------------------------------- */ + this.svg.addEventListener("mousedown", (e) => { + if (e.button !== 0) return; + e.preventDefault(); + + isMouseDown = true; + this.isPanning = true; + + lastX = e.clientX; + lastY = e.clientY; + + this.svg.style.cursor = "grabbing"; + }); + + window.addEventListener("mousemove", (e) => { + if (!isMouseDown) return; + + const dxScreen = e.clientX - lastX; + const dyScreen = e.clientY - lastY; + + const { dxSvg, dySvg } = screenDeltaToSvgDelta(dxScreen, dyScreen); + + this.tx += dxSvg; + this.ty += dySvg; + + lastX = e.clientX; + lastY = e.clientY; + + this._scheduleRender(); + }); + + window.addEventListener("mouseup", () => { + isMouseDown = false; + this.isPanning = false; + this.svg.style.cursor = "grab"; + }); + + /* --------------------------------------------------------- + PAN + PINCH (touch) + --------------------------------------------------------- */ + this.svg.addEventListener("touchstart", (e) => { + if (e.touches.length === 1) { + const t = e.touches[0]; + lastX = t.clientX; + lastY = t.clientY; + this.isPanning = true; + } else if (e.touches.length === 2) { + this.isPanning = false; + lastTouchDistance = this._getTouchDistance(e); + } + }, { passive: false }); + + this.svg.addEventListener("touchmove", (e) => { + if (e.touches.length === 1 && this.isPanning) { + e.preventDefault(); + const t = e.touches[0]; + + const dxScreen = t.clientX - lastX; + const dyScreen = t.clientY - lastY; + + const { dxSvg, dySvg } = screenDeltaToSvgDelta(dxScreen, dyScreen); + + this.tx += dxSvg; + this.ty += dySvg; + + lastX = t.clientX; + lastY = t.clientY; + + this._scheduleRender(); + } else if (e.touches.length === 2) { + e.preventDefault(); + + const newDist = this._getTouchDistance(e); + const zoomFactor = newDist / lastTouchDistance; + lastTouchDistance = newDist; + + const rect = this.svg.getBoundingClientRect(); + const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2 - rect.left; + const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2 - rect.top; + + const worldX = (midX - this.tx) / this.scale; + const worldY = (midY - this.ty) / this.scale; + + const newScale = Math.min(this.maxScale, Math.max(this.minScale, this.scale * zoomFactor)); + + this.tx = midX - worldX * newScale; + this.ty = midY - worldY * newScale; + this.scale = newScale; + + this._scheduleRender(); + } + }, { passive: false }); + + this.svg.addEventListener("touchend", () => { + this.isPanning = false; + }); + + /* --------------------------------------------------------- + ZOOM (wheel) — zoom-to-cursor + --------------------------------------------------------- */ + this.svg.addEventListener("wheel", (e) => { + e.preventDefault(); + + const rect = this.svg.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + const zoomFactor = e.deltaY < 0 ? 1.1 : 0.9; + const newScale = Math.min(this.maxScale, Math.max(this.minScale, this.scale * zoomFactor)); + + const worldX = (mouseX - this.tx) / this.scale; + const worldY = (mouseY - this.ty) / this.scale; + + this.tx = mouseX - worldX * newScale; + this.ty = mouseY - worldY * newScale; + this.scale = newScale; + + this._scheduleRender(); + }, { passive: false }); + } + + _getTouchDistance(e) { + const dx = e.touches[0].clientX - e.touches[1].clientX; + const dy = e.touches[0].clientY - e.touches[1].clientY; + return Math.sqrt(dx * dx + dy * dy); + } + + _getWorldRect() { + const container = document.getElementById("diagramContainer"); + const ctm = this.viewportGroup.getCTM(); + if (!ctm) return { x: 0, y: 0, width: 100, height: 100 }; + + const inv = ctm.inverse(); + + const p1 = this.svg.createSVGPoint(); + p1.x = 0; + p1.y = 0; + const tl = p1.matrixTransform(inv); + + const p2 = this.svg.createSVGPoint(); + p2.x = container.clientWidth; + p2.y = container.clientHeight; + const br = p2.matrixTransform(inv); + + return { + x: tl.x, + y: tl.y, + width: br.x - tl.x, + height: br.y - tl.y + }; + } + + _applyTransform() { + if (!this.viewportGroup) return; + + this.viewportGroup.setAttribute( + "transform", + `translate(${this.tx}, ${this.ty}) scale(${this.scale})` + ); + + + // ОБЯЗАТЕЛЬНО обновляем миникарту после трансформации + this._updateMinimapViewport(); + } + + + _scheduleRender() { + if (this.needsRender) return; + this.needsRender = true; + + requestAnimationFrame(() => { + this.needsRender = false; + this._applyTransform(); + }); + } + + _initMinimap() { + if (!this.minimap || !this.svg) return; + + this.minimap.innerHTML = ""; + + // Клонируем SVG для миникарты + this.minimapSvg = this.svg.cloneNode(true); + + // Убираем трансформации с клонированных элементов + const cloneViewport = this.minimapSvg.querySelector("#viewport-group"); + if (cloneViewport) { + cloneViewport.removeAttribute("transform"); + } + + // Используем diagramBounds для viewBox миникарты + const diagram = this.diagramBounds || this.originalViewBox; + + // Устанавливаем viewBox для миникарты + this.minimapSvg.setAttribute( + "viewBox", + `${diagram.x} ${diagram.y} ${diagram.width} ${diagram.height}` + ); + + this.minimapSvg.setAttribute("preserveAspectRatio", "xMidYMid meet"); + this.minimapSvg.style.width = "100%"; + this.minimapSvg.style.height = "100%"; + this.minimapSvg.style.position = "absolute"; + this.minimapSvg.style.opacity = "0.7"; + + // Убираем интерактивность + this.minimapSvg.querySelectorAll('*').forEach(el => { + el.style.pointerEvents = 'none'; + }); + + this.minimap.appendChild(this.minimapSvg); + + // Создаем viewport элемент + this.minimapViewport = document.createElement("div"); + this.minimapViewport.className = "minimap-viewport"; + this.minimapViewport.style.cssText = ` + position: absolute; + border: 2px solid var(--color-primary); + background-color: rgba(0, 120, 212, 0.15); + pointer-events: none; + transition: all 0.1s ease; + box-sizing: border-box; + z-index: 10; + `; + + this.minimap.appendChild(this.minimapViewport); + + // Обработчик клика по миникарте + this.minimap.addEventListener("click", (e) => { + if (!this.minimapSvg) return; + + const rect = this.minimap.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + // Процентное положение в миникарте + const percentX = x / rect.width; + const percentY = y / rect.height; + + const diagram = this.diagramBounds || this.originalViewBox; + + // Мировые координаты в диаграмме + const worldX = diagram.x + diagram.width * percentX; + const worldY = diagram.y + diagram.height * percentY; + + const container = document.getElementById("diagramContainer"); + if (!container) return; + + // Центрируем view на точке клика + this.tx = container.clientWidth / 2 - worldX * this.scale; + this.ty = container.clientHeight / 2 - worldY * this.scale; + + this._scheduleRender(); + }); + } + + _updateMinimapViewport() { + if (!this.minimapViewport || !this.minimapSvg || !this.minimap || !this.svg) { + return; + } + + try { + // Получаем размеры миникарты + const minimapRect = this.minimap.getBoundingClientRect(); + if (minimapRect.width === 0 || minimapRect.height === 0) return; + + // Получаем текущую трансформацию viewportGroup + const ctm = this.viewportGroup ? this.viewportGroup.getCTM() : null; + if (!ctm) return; + + // Получаем размеры контейнера диаграммы + const container = document.getElementById("diagramContainer"); + if (!container) return; + const containerRect = container.getBoundingClientRect(); + + // Рассчитываем видимую область в мировых координатах + const inv = ctm.inverse(); + + // Левый верхний угол контейнера (0,0 в координатах SVG viewport) + const topLeftScreen = this.svg.createSVGPoint(); + topLeftScreen.x = 0; + topLeftScreen.y = 0; + const topLeftWorld = topLeftScreen.matrixTransform(inv); + + // Правый нижний угол контейнера + const bottomRightScreen = this.svg.createSVGPoint(); + bottomRightScreen.x = containerRect.width; + bottomRightScreen.y = containerRect.height; + const bottomRightWorld = bottomRightScreen.matrixTransform(inv); + + // Определяем границы видимой области + const viewportWorld = { + left: Math.min(topLeftWorld.x, bottomRightWorld.x), + top: Math.min(topLeftWorld.y, bottomRightWorld.y), + right: Math.max(topLeftWorld.x, bottomRightWorld.x), + bottom: Math.max(topLeftWorld.y, bottomRightWorld.y) + }; + + const viewportWidth = viewportWorld.right - viewportWorld.left; + const viewportHeight = viewportWorld.bottom - viewportWorld.top; + + // Получаем границы всей диаграммы (из diagramBounds) + const diagram = this.diagramBounds || this.originalViewBox; + + // Рассчитываем масштаб для миникарты + const scaleX = minimapRect.width / diagram.width; + const scaleY = minimapRect.height / diagram.height; + + // Преобразуем мировые координаты в координаты миникарты + const viewportLeft = (viewportWorld.left - diagram.x) * scaleX; + const viewportTop = (viewportWorld.top - diagram.y) * scaleY; + const viewportWidthPx = viewportWidth * scaleX; + const viewportHeightPx = viewportHeight * scaleY; + + // Применяем стили к viewport'у миникарты + this.minimapViewport.style.left = `${viewportLeft}px`; + this.minimapViewport.style.top = `${viewportTop}px`; + this.minimapViewport.style.width = `${viewportWidthPx}px`; + this.minimapViewport.style.height = `${viewportHeightPx}px`; + + // Для отладки (раскомментируйте если нужно) + // console.log("Minimap viewport:", { + // diagram, + // viewportWorld, + // viewportLeft, viewportTop, viewportWidthPx, viewportHeightPx, + // scaleX, scaleY, + // containerRect: { width: containerRect.width, height: containerRect.height } + // }); + + } catch (error) { + console.error("Error in _updateMinimapViewport:", error); + } + } + async resetView() { + const container = document.getElementById("diagramContainer"); + if (!container || !this.viewportGroup) return; + + const cw = container.clientWidth; + const ch = container.clientHeight; + + if (cw === 0 || ch === 0) { + setTimeout(() => this.resetView(), 50); + return; + } + + // 1. Временно ставим scale = 1, чтобы измерить реальный размер диаграммы + this.scale = 1; + this.tx = 0; + this.ty = 0; + this._applyTransform(); + + // Даем браузеру дорендерить (важно!) + await new Promise(r => requestAnimationFrame(r)); + + // 2. Получаем реальные пиксельные размеры диаграммы + const rect = this.viewportGroup.getBoundingClientRect(); + const diagramPxWidth = rect.width; + const diagramPxHeight = rect.height; + + // 3. Вычисляем zoom-to-fit в пикселях + const scaleX = cw / diagramPxWidth; + const scaleY = ch / diagramPxHeight; + const zoomToFit = Math.min(scaleX, scaleY) * 0.9; + + // 4. Устанавливаем новый пользовательский zoom + this.scale = Math.max(Math.min(this.maxScale, zoomToFit), this.minScale); + + // 5. Центрируем диаграмму + const dx = Math.max(0, (cw - diagramPxWidth * this.scale) / 2); + const dy = Math.max(0, (ch - diagramPxHeight * this.scale) / 2); + + // 6. Преобразуем в юниты + const diagram = this.diagramBounds; + + this.tx = diagramPxWidth / diagram.width * dx; + this.ty = diagramPxHeight / diagram.height * dy; + + this._applyTransform(); + } + + + refresh() { + this._scheduleRender(); + } +} + +/* --------------------------------------------------------- + EXPORT FUNCTION +--------------------------------------------------------- */ +function exportPng() { + const svgElement = document.querySelector('#diagramSvgContainer svg'); + if (!svgElement) { + alert('Диаграмма не найдена для экспорта'); + return; + } + + try { + const svgData = new XMLSerializer().serializeToString(svgElement); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const img = new Image(); + + // Get actual dimensions + const bbox = svgElement.getBBox(); + canvas.width = bbox.width * 2; + canvas.height = bbox.height * 2; + + const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' }); + const url = URL.createObjectURL(svgBlob); + + img.onload = function () { + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + + const pngUrl = canvas.toDataURL('image/png'); + const downloadLink = document.createElement('a'); + downloadLink.href = pngUrl; + downloadLink.download = `diagram_${new Date().toISOString().slice(0, 10)}.png`; + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + + URL.revokeObjectURL(url); + + // Show success toast + if (window.viewer && viewer._showToast) { + viewer._showToast('Диаграмма экспортирована как PNG', 'success'); + } + }; + + img.onerror = function () { + alert('Ошибка при экспорте изображения'); + URL.revokeObjectURL(url); + }; + + img.src = url; + } catch (error) { + console.error('Export error:', error); + alert('Ошибка при экспорте: ' + error.message); + } +} + +/* --------------------------------------------------------- + HANDLE SEARCH KEY PRESS +--------------------------------------------------------- */ +function handleSearchKeyPress(event) { + if (event.key === 'Enter') { + event.preventDefault(); + if (window.viewer && viewer.searchInput) { + const term = viewer.searchInput.value.trim(); + if (term) { + viewer.searchNodes(term); + } else { + viewer.clearSearch(); + } + } + } +} + +/* --------------------------------------------------------- + THEME CHANGE HANDLER +--------------------------------------------------------- */ +function setupThemeChangeHandler() { + const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + + darkModeMediaQuery.addEventListener('change', (e) => { + if (window.viewer && document.getElementById('mermaid').classList.contains('active')) { + viewer.render().catch(console.error); + } + }); +} + +/* --------------------------------------------------------- + MAIN INITIALIZATION +--------------------------------------------------------- */ +let viewer = null; + +document.addEventListener("DOMContentLoaded", () => { + window.reportData = JSON.parse(document.getElementById('report-data').textContent); + window.hasDiagram = true; + + // Инициализация отчета + if (window.initReport) { + window.initReport(); + } + + + initTabs((id) => { + if (id === "mermaid") { + if (!viewer) { + viewer = new DiagramViewer(); + viewer.render().catch(console.error); + } else { + viewer.refresh(); + } + } + }); + + // Auto-click first tab + document.querySelector(".tab.active").click(); + + // Setup theme change handler + setupThemeChangeHandler(); + + // Make viewer globally available for button callbacks + window.viewer = viewer; + window.exportPng = exportPng; + window.handleSearchKeyPress = handleSearchKeyPress; +}); \ No newline at end of file diff --git a/SQLLinter/Infrastructure/Reporters/HtmlReportFormatter_v2.cs b/SQLLinter/Infrastructure/Reporters/Formatters/Html/v2/HtmlReportFormatter_v2.cs similarity index 97% rename from SQLLinter/Infrastructure/Reporters/HtmlReportFormatter_v2.cs rename to SQLLinter/Infrastructure/Reporters/Formatters/Html/v2/HtmlReportFormatter_v2.cs index 72f9c85..21aef11 100644 --- a/SQLLinter/Infrastructure/Reporters/HtmlReportFormatter_v2.cs +++ b/SQLLinter/Infrastructure/Reporters/Formatters/Html/v2/HtmlReportFormatter_v2.cs @@ -7,9 +7,9 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -namespace SQLLinter.Infrastructure.Reporters; +namespace SQLLinter.Infrastructure.Reporters.Formatters.Html.v2; -public class HtmlReportFormatter_v2 : IReportFormatter +public class HtmlReportFormatter : IReportFormatter { public string Format(List violations) => Format(violations, null); @@ -168,7 +168,7 @@ public class HtmlReportFormatter_v2 : IReportFormatter
"); } @@ -186,7 +186,7 @@ public class HtmlReportFormatter_v2 : IReportFormatter """); // Загружаем основной JS - sb.AppendLine(LoadResource("HtmlFormatter.js")); + sb.AppendLine(LoadResource("HtmlFormatter_v2.js")); sb.AppendLine(""" diff --git a/SQLLinter/Infrastructure/Reporters/Formatters/Html/v3/HtmlFormatter_v3.css b/SQLLinter/Infrastructure/Reporters/Formatters/Html/v3/HtmlFormatter_v3.css new file mode 100644 index 0000000..8c7a191 --- /dev/null +++ b/SQLLinter/Infrastructure/Reporters/Formatters/Html/v3/HtmlFormatter_v3.css @@ -0,0 +1,542 @@ +/* --------------------------------------------------------- + FLUENT UI 2 — BASE VARIABLES +--------------------------------------------------------- */ +:root { + --color-primary: #0f6cbd; + --color-primary-hover: #115ea3; + --color-primary-light: #d0e7ff; + --color-bg: #ffffff; + --color-bg-alt: #f5f5f5; + --color-bg-neutral: #f3f2f1; + --color-border: #d1d1d1; + --color-border-strong: #b5b5b5; + --color-text: #1a1a1a; + --color-text-secondary: #5e5e5e; + --color-text-tertiary: #8a8a8a; + --color-critical: #d13438; + --color-critical-bg: #f8d7da; + --color-warning: #ffaa44; + --color-warning-bg: #fff4ce; + --color-info: #0f6cbd; + --color-info-bg: #d0e7ff; + --radius-s: 4px; + --radius-m: 6px; + --radius-l: 8px; + --space-xs: 4px; + --space-s: 8px; + --space-m: 12px; + --space-l: 16px; + --space-xl: 20px; + --space-xxl: 28px; + --font-s: 12px; + --font-m: 14px; + --font-l: 16px; + --font-xl: 18px; + --z-tabs: 1000; + --z-header: 900; +} + +/* --------------------------------------------------------- + RESET +--------------------------------------------------------- */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: "Segoe UI", sans-serif; + background: var(--color-bg-neutral); + color: var(--color-text); + line-height: 1.5; +} + +/* --------------------------------------------------------- + FILE REPORT WRAPPER +--------------------------------------------------------- */ +.file-report { + display: none; + padding-bottom: 120px; +} + + .file-report.active { + display: block; + } + +/* --------------------------------------------------------- + FILE TITLE (STICKY) +--------------------------------------------------------- */ +.file-title-container { + position: sticky; + top: 0; + z-index: var(--z-header); + background: var(--color-bg); + border-bottom: 1px solid var(--color-border); + padding: var(--space-l) var(--space-xl); +} + +.file-title { + font-size: var(--font-xl); + font-weight: 600; + color: var(--color-text); +} + +/* --------------------------------------------------------- + SEVERITY SECTIONS (FLUENT UI 2 STYLE) +--------------------------------------------------------- */ +.severity-section { + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-l); + padding: var(--space-xl); + margin: var(--space-xl) var(--space-xl); +} + + .severity-section.critical { + border-left: 4px solid var(--color-critical); + } + + .severity-section.warning { + border-left: 4px solid var(--color-warning); + } + + .severity-section.info { + border-left: 4px solid var(--color-info); + } + +.severity-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--space-l); + padding-bottom: var(--space-s); + border-bottom: 1px solid var(--color-border); +} + +.severity-title h2 { + font-size: var(--font-l); + font-weight: 600; +} + +.severity-count { + background: var(--color-bg-alt); + padding: var(--space-xs) var(--space-s); + border-radius: var(--radius-s); + font-size: var(--font-s); + color: var(--color-text-secondary); +} + +/* --------------------------------------------------------- + GRID TABLE (REPLACES ) +--------------------------------------------------------- */ +.grid-table { + display: grid; + gap: var(--space-xs); +} + +.grid-header, +.grid-row { + display: grid; + grid-template-columns: 40px 60px 60px 250px 1fr; + gap: var(--space-xs); + padding: var(--space-s); + border-radius: var(--radius-s); +} + +.grid-header { + background: var(--color-bg-alt); + font-weight: 600; + color: var(--color-text-secondary); + font-size: var(--font-s); +} + +.grid-row { + background: var(--color-bg); + border: 1px solid var(--color-border); + font-size: var(--font-m); + transition: background-color 0.15s ease; +} + + .grid-row:hover { + background: var(--color-bg-alt); + } + +/* --------------------------------------------------------- + SUMMARY SECTION +--------------------------------------------------------- */ +.summary-section { + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-l); + padding: var(--space-xl); + margin: var(--space-xl); +} + +.summary-header h2 { + font-size: var(--font-l); + margin-bottom: var(--space-m); +} + +.summary-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: var(--space-m); + margin-bottom: var(--space-l); +} + +.stat-card { + background: var(--color-bg-alt); + border-radius: var(--radius-m); + padding: var(--space-m); + text-align: center; +} + +.stat-value { + font-size: var(--font-xl); + font-weight: 600; +} + +.stat-label { + font-size: var(--font-s); + color: var(--color-text-secondary); +} + +/* Progress bar */ +.progress-bar { + display: flex; + height: 8px; + border-radius: var(--radius-s); + overflow: hidden; + background: var(--color-border); +} + +.progress-fill.critical { + background: var(--color-critical); +} + +.progress-fill.warning { + background: var(--color-warning); +} + +.progress-fill.info { + background: var(--color-info); +} + +/* --------------------------------------------------------- + FILE CARDS IN SUMMARY +--------------------------------------------------------- */ +.files-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: var(--space-m); +} + +.file-card { + background: var(--color-bg-alt); + border-radius: var(--radius-m); + padding: var(--space-m); + transition: background-color 0.2s ease, transform 0.2s ease; +} + + .file-card:hover { + transform: translateY(-2px); + background: var(--color-bg-neutral); + } + +.file-name-small { + font-weight: 600; + font-size: var(--font-m); +} + +.file-card-bottom { + display: flex; + justify-content: space-between; + margin-top: var(--space-s); +} + +.file-violations { + display: flex; + gap: var(--space-xs); +} + +.violation-badge { + padding: var(--space-xs) var(--space-s); + border-radius: var(--radius-s); + font-size: var(--font-s); + color: var(--color-text); +} + + .violation-badge.critical { + background: var(--color-critical-bg); + color: var(--color-critical); + } + + .violation-badge.warning { + background: var(--color-warning-bg); + color: var(--color-warning); + } + + .violation-badge.info { + background: var(--color-info-bg); + color: var(--color-info); + } + +/* --------------------------------------------------------- + TABS CONTAINER (STICKY BOTTOM) +--------------------------------------------------------- */ +.tabs-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: var(--color-bg); + border-top: 1px solid var(--color-border); + padding: var(--space-s) var(--space-l); + z-index: var(--z-tabs); +} + +/* --------------------------------------------------------- + TABS — HORIZONTAL SCROLL + VISIBLE SCROLLBAR +--------------------------------------------------------- */ +.tabs { + display: flex; + flex-wrap: nowrap; + gap: var(--space-s); + overflow-x: auto; + overflow-y: hidden; + white-space: nowrap; + scroll-behavior: smooth; + padding-bottom: 4px; + /* Firefox scrollbar */ + scrollbar-width: thin; + scrollbar-color: var(--color-border-strong) var(--color-bg-alt); +} + + /* Chrome / Edge / Safari scrollbar */ + .tabs::-webkit-scrollbar { + height: 8px; + } + + .tabs::-webkit-scrollbar-track { + background: var(--color-bg-alt); + border-radius: 4px; + } + + .tabs::-webkit-scrollbar-thumb { + background: var(--color-border-strong); + border-radius: 4px; + } + + .tabs::-webkit-scrollbar-thumb:hover { + background: var(--color-text-tertiary); + } + +/* --------------------------------------------------------- + TAB BASE STYLES (COMPACT, AUTO WIDTH, MAX 2 LINES) +--------------------------------------------------------- */ +.tab { + flex: 0 0 auto; /* растягивается под контент, не ломая одну линию */ + min-width: 0; + padding: 6px 12px; + line-height: 1.25; + display: flex; + align-items: flex-start; + background: var(--color-bg-alt); + border: 1px solid var(--color-border); + border-radius: var(--radius-m); + cursor: pointer; + font-size: var(--font-m); + font-weight: 500; + color: var(--color-text-secondary); + transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease; +} + + .tab:hover { + background: var(--color-bg-neutral); + transform: translateY(-1px); + } + + .tab.active { + background: var(--color-primary-light); + border-color: var(--color-primary); + color: var(--color-primary); + transform: translateY(-2px); + } + +/* Внутренний layout таба */ +.tab-inner { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 6px; + width: 100%; +} + +/* Текст таба — максимум 2 строки, с переносами */ +.tab-text { + font-size: 13px; + line-height: 1.25; + white-space: normal; + word-break: break-word; + overflow-wrap: break-word; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + max-height: calc(1.25em * 2); + overflow: hidden; +} + +/* Счётчики — компактные */ +.tab-counters { + display: flex; + flex-direction: column; + gap: 2px; + flex-shrink: 0; +} + +.tab-counter { + font-size: 10px; + min-width: 14px; + height: 14px; + padding: 0 3px; + line-height: 14px; + border-radius: var(--radius-s); + text-align: center; +} + + .tab-counter.critical { + background: var(--color-critical-bg); + color: var(--color-critical); + } + + .tab-counter.warning { + background: var(--color-warning-bg); + color: var(--color-warning); + } + + .tab-counter.info { + background: var(--color-info-bg); + color: var(--color-info); + } + + .tab-counter.empty { + opacity: 0.3; + } + +/* Мобильная компактность */ +@media (max-width: 480px) { + .tab { + padding: 4px 8px; + } + + .tab-text { + font-size: 12px; + max-height: calc(1.2em * 2); + -webkit-line-clamp: 2; + } + + .tab-counter { + min-width: 12px; + height: 12px; + font-size: 9px; + padding: 0 2px; + } +} + +/* --------------------------------------------------------- + FLUENT UI 2 — DARK THEME (PREFERS-COLOR-SCHEME) +--------------------------------------------------------- */ +@media (prefers-color-scheme: dark) { + :root { + --color-bg: #1f1f1f; + --color-bg-alt: #2a2a2a; + --color-bg-neutral: #2d2d2d; + --color-text: #f3f3f3; + --color-text-secondary: #c8c8c8; + --color-text-tertiary: #9a9a9a; + --color-border: #3a3a3a; + --color-border-strong: #4a4a4a; + --color-primary: #3aa0f3; + --color-primary-hover: #2899f5; + --color-primary-light: #0f2b4d; + --color-critical-bg: #4c191b; + --color-warning-bg: #4c3b1a; + --color-info-bg: #0f2b4d; + } + + .file-title-container { + background: var(--color-bg); + } + + .severity-section { + background: var(--color-bg); + border-color: var(--color-border); + } + + .grid-row { + background: var(--color-bg-alt); + border-color: var(--color-border); + } + + .grid-row:hover { + background: var(--color-bg-neutral); + } + + .summary-section { + background: var(--color-bg); + border-color: var(--color-border); + } + + .file-card { + background: var(--color-bg-alt); + } + + .tabs-container { + background: var(--color-bg); + border-color: var(--color-border); + } + + .tab { + background: var(--color-bg-alt); + border-color: var(--color-border); + color: var(--color-text-secondary); + } + + .tab.active { + background: var(--color-primary-light); + border-color: var(--color-primary); + color: var(--color-primary); + } + + /* Dark scrollbar colors */ + .tabs { + scrollbar-color: var(--color-border-strong) var(--color-bg); + } + + .tabs::-webkit-scrollbar-track { + background: var(--color-bg); + } + + .tabs::-webkit-scrollbar-thumb { + background: var(--color-border-strong); + } + + .tabs::-webkit-scrollbar-thumb:hover { + background: var(--color-text-secondary); + } +} + +/* --------------------------------------------------------- + TAB TRANSITIONS & FILE REPORT ANIMATION +--------------------------------------------------------- */ +.file-report { + opacity: 0; + transform: translateY(6px); + transition: opacity 0.25s ease, transform 0.25s ease; +} + + .file-report.active { + opacity: 1; + transform: translateY(0); + } diff --git a/SQLLinter/Infrastructure/Reporters/Formatters/Html/v3/HtmlFormatter_v3.js b/SQLLinter/Infrastructure/Reporters/Formatters/Html/v3/HtmlFormatter_v3.js new file mode 100644 index 0000000..0ec5302 --- /dev/null +++ b/SQLLinter/Infrastructure/Reporters/Formatters/Html/v3/HtmlFormatter_v3.js @@ -0,0 +1,486 @@ +document.addEventListener("DOMContentLoaded", function () { + const dataEl = document.getElementById("report-data"); + if (!dataEl) return console.error("report-data not found"); + + const data = JSON.parse(dataEl.textContent || "{}"); + const files = data.files || data.f || []; + const rules = data.rules || data.r || {}; + + const tabsList = document.getElementById("tabs-list"); + const reportsContainer = document.getElementById("reports-container"); + + if (!tabsList || !reportsContainer) { + console.error("Missing tabs-list or reports-container"); + return; + } + + /* --------------------------------------------------------- + UTILS + --------------------------------------------------------- */ + + function escapeHtml(str) { + if (str == null) return ""; + return String(str) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); + } + + function countViolations(v) { + return { + c: v?.c?.length || 0, + w: v?.w?.length || 0, + i: v?.i?.length || 0 + }; + } + + const rendered = new Set(); // ленивый рендер + + /* --------------------------------------------------------- + RENDER TABS + --------------------------------------------------------- */ + + function renderTabs() { + const frag = document.createDocumentFragment(); + + files.forEach((file, index) => { + const v = countViolations(file.v); + + const btn = document.createElement("button"); + btn.className = "tab"; + btn.dataset.target = `file_${index}`; + btn.dataset.index = index; + + const inner = document.createElement("div"); + inner.className = "tab-inner"; + + const text = document.createElement("span"); + text.className = "tab-text"; + text.title = file.n; + text.textContent = file.n; + + const counters = document.createElement("div"); + counters.className = "tab-counters"; + + const c1 = document.createElement("span"); + c1.className = "tab-counter critical" + (v.c ? "" : " empty"); + c1.textContent = v.c || ""; + + const c2 = document.createElement("span"); + c2.className = "tab-counter warning" + (v.w ? "" : " empty"); + c2.textContent = v.w || ""; + + const c3 = document.createElement("span"); + c3.className = "tab-counter info" + (v.i ? "" : " empty"); + c3.textContent = v.i || ""; + + counters.append(c1, c2, c3); + inner.append(text, counters); + btn.append(inner); + frag.append(btn); + }); + + // Summary tab + const summaryBtn = document.createElement("button"); + summaryBtn.className = "tab"; + summaryBtn.dataset.target = "summary_report"; + + const inner = document.createElement("div"); + inner.className = "tab-inner"; + + const text = document.createElement("span"); + text.className = "tab-text"; + text.textContent = "Summary"; + + inner.append(text); + summaryBtn.append(inner); + frag.append(summaryBtn); + + tabsList.innerHTML = ""; + tabsList.append(frag); + } + + /* --------------------------------------------------------- + GRID ROW CREATOR (VIOLATIONS) + --------------------------------------------------------- */ + + function createGridRow(v, rule) { + const row = document.createElement("div"); + row.className = "grid-row"; + + const cIndex = document.createElement("div"); + cIndex.textContent = v.i; + + const cLine = document.createElement("div"); + cLine.textContent = v.l; + + const cCol = document.createElement("div"); + cCol.textContent = v.c; + + const cRule = document.createElement("div"); + cRule.textContent = rule.n; + + let text = rule.t || ""; + const args = v.a || v.args; + if (Array.isArray(args)) { + for (let i = 0; i < args.length; i++) { + text = text.replace(`{${i}}`, args[i]); + } + } + + const cDesc = document.createElement("div"); + cDesc.textContent = text; + + row.append(cIndex, cLine, cCol, cRule, cDesc); + return row; + } + + /* --------------------------------------------------------- + FILE REPORT + --------------------------------------------------------- */ + + function renderFileReport(index) { + const file = files[index]; + const v = file.v || {}; + + const root = document.createElement("div"); + + // Title + const titleWrap = document.createElement("div"); + titleWrap.className = "file-title-container"; + + const title = document.createElement("div"); + title.className = "file-title"; + + const name = document.createElement("span"); + name.className = "file-name"; + name.textContent = file.n; + + title.append(name); + titleWrap.append(title); + root.append(titleWrap); + + // Sections + function addSection(label, list, cls) { + if (!Array.isArray(list) || list.length === 0) return; + + const section = document.createElement("div"); + section.className = `severity-section ${cls}`; + + const header = document.createElement("div"); + header.className = "severity-header"; + + const hTitle = document.createElement("div"); + hTitle.className = "severity-title"; + + const h2 = document.createElement("h2"); + h2.textContent = label; + + const count = document.createElement("span"); + count.className = "severity-count"; + count.textContent = list.length; + + hTitle.append(h2, count); + header.append(hTitle); + section.append(header); + + const grid = document.createElement("div"); + grid.className = "grid-table"; + + // header row + const headerRow = document.createElement("div"); + headerRow.className = "grid-header"; + + ["#", "Строка", "Колонка", "Правило", "Описание"].forEach(t => { + const cell = document.createElement("div"); + cell.textContent = t; + headerRow.append(cell); + }); + + grid.append(headerRow); + + // rows + for (let i = 0; i < list.length; i++) { + const vItem = list[i]; + const rule = rules[vItem.r] || { n: "Unknown", t: "Описание отсутствует" }; + grid.append(createGridRow(vItem, rule)); + } + + section.append(grid); + root.append(section); + } + + addSection("Critical", v.c, "critical"); + addSection("Warning", v.w, "warning"); + addSection("Info", v.i, "info"); + + return root; + } + + /* --------------------------------------------------------- + SUMMARY + --------------------------------------------------------- */ + + function renderSummary() { + let totalC = 0, totalW = 0, totalI = 0; + + for (let i = 0; i < files.length; i++) { + const v = countViolations(files[i].v); + totalC += v.c; + totalW += v.w; + totalI += v.i; + } + + const total = totalC + totalW + totalI || 1; + + const root = document.createElement("div"); + + // Title + const titleWrap = document.createElement("div"); + titleWrap.className = "file-title-container"; + + const title = document.createElement("div"); + title.className = "file-title"; + + const name = document.createElement("span"); + name.className = "file-name"; + name.textContent = "Сводный отчет"; + + title.append(name); + titleWrap.append(title); + root.append(titleWrap); + + // Stats + const section = document.createElement("div"); + section.className = "summary-section"; + + const header = document.createElement("div"); + header.className = "summary-header"; + + const hTitle = document.createElement("div"); + hTitle.className = "summary-title"; + + const h2 = document.createElement("h2"); + h2.textContent = "Общая статистика"; + + hTitle.append(h2); + header.append(hTitle); + section.append(header); + + const grid = document.createElement("div"); + grid.className = "summary-stats-grid"; + + function statCard(cls, value, label) { + const card = document.createElement("div"); + card.className = `stat-card ${cls}`; + + const v = document.createElement("div"); + v.className = "stat-value"; + v.textContent = value; + + const l = document.createElement("div"); + l.className = "stat-label"; + l.textContent = label; + + card.append(v, l); + return card; + } + + grid.append( + statCard("success", files.length, "Всего файлов"), + statCard("critical", totalC, "Critical нарушений"), + statCard("warning", totalW, "Warning нарушений"), + statCard("info", totalI, "Info нарушений") + ); + + section.append(grid); + + // Progress + const dist = document.createElement("div"); + dist.className = "violation-distribution"; + + const bar = document.createElement("div"); + bar.className = "progress-bar"; + + const pc = (totalC / total) * 100; + const pw = (totalW / total) * 100; + const pi = (totalI / total) * 100; + + const f1 = document.createElement("div"); + f1.className = "progress-fill critical"; + f1.style.width = pc + "%"; + + const f2 = document.createElement("div"); + f2.className = "progress-fill warning"; + f2.style.width = pw + "%"; + + const f3 = document.createElement("div"); + f3.className = "progress-fill info"; + f3.style.width = pi + "%"; + + bar.append(f1, f2, f3); + dist.append(bar); + + section.append(dist); + root.append(section); + + // Files overview + const section2 = document.createElement("div"); + section2.className = "summary-section files-overview"; + + const header2 = document.createElement("div"); + header2.className = "summary-header"; + + const hTitle2 = document.createElement("div"); + hTitle2.className = "summary-title"; + + const h22 = document.createElement("h2"); + h22.textContent = "Статистика файлов"; + + const count = document.createElement("span"); + count.className = "severity-count"; + count.textContent = files.length; + + hTitle2.append(h22, count); + header2.append(hTitle2); + section2.append(header2); + + const filesGrid = document.createElement("div"); + filesGrid.className = "files-grid"; + + for (let i = 0; i < files.length; i++) { + const f = files[i]; + const v = countViolations(f.v); + const t = v.c + v.w + v.i || 1; + + const card = document.createElement("div"); + card.className = "file-card"; + + const header = document.createElement("div"); + header.className = "file-card-header"; + + const name = document.createElement("span"); + name.className = "file-name-small"; + name.textContent = f.n; + + header.append(name); + card.append(header); + + const bar = document.createElement("div"); + bar.className = "progress-bar small"; + + const fc = document.createElement("div"); + fc.className = "progress-fill critical"; + fc.style.width = (v.c / t * 100) + "%"; + + const fw = document.createElement("div"); + fw.className = "progress-fill warning"; + fw.style.width = (v.w / t * 100) + "%"; + + const fi = document.createElement("div"); + fi.className = "progress-fill info"; + fi.style.width = (v.i / t * 100) + "%"; + + bar.append(fc, fw, fi); + card.append(bar); + + const bottom = document.createElement("div"); + bottom.className = "file-card-bottom"; + + const total = document.createElement("span"); + total.className = "file-total"; + total.textContent = "Total: " + (v.c + v.w + v.i); + + const badges = document.createElement("div"); + badges.className = "file-violations"; + + function badge(cls, val) { + const b = document.createElement("span"); + b.className = `violation-badge ${cls}`; + b.textContent = val; + return b; + } + + badges.append( + badge("critical", v.c), + badge("warning", v.w), + badge("info", v.i) + ); + + bottom.append(total, badges); + card.append(bottom); + + filesGrid.append(card); + } + + section2.append(filesGrid); + root.append(section2); + + return root; + } + + /* --------------------------------------------------------- + TAB HANDLER + --------------------------------------------------------- */ + + tabsList.addEventListener("click", function (e) { + const tab = e.target.closest(".tab"); + if (!tab) return; + + const targetId = tab.dataset.target; + + // activate tab + tabsList.querySelectorAll(".tab").forEach(t => t.classList.remove("active")); + tab.classList.add("active"); + + // hide all reports + reportsContainer.innerHTML = ""; + + // lazy render + let content; + if (!rendered.has(targetId)) { + if (targetId === "summary_report") { + content = renderSummary(); + } else { + const index = Number(tab.dataset.index); + content = renderFileReport(index); + } + rendered.add(targetId); + } else { + // already rendered → but we removed DOM → re-render + if (targetId === "summary_report") { + content = renderSummary(); + } else { + const index = Number(tab.dataset.index); + content = renderFileReport(index); + } + } + + reportsContainer.append(content); + window.scrollTo({ top: 0, behavior: "smooth" }); + }); + + /* --------------------------------------------------------- + INIT + --------------------------------------------------------- */ + + renderTabs(); + + const first = tabsList.querySelector(".tab"); + if (first) first.click(); + + const tabs = document.querySelector('.tabs'); + + tabs.addEventListener('wheel', (e) => { + if (e.deltaY !== 0) { + e.preventDefault(); + tabs.scrollBy({ + left: e.deltaY * 4, + behavior: "smooth" + }); + } + }, { passive: false }); + + +}); \ No newline at end of file diff --git a/SQLLinter/Infrastructure/Reporters/Formatters/Html/v3/HtmlReportFormatter_v3.cs b/SQLLinter/Infrastructure/Reporters/Formatters/Html/v3/HtmlReportFormatter_v3.cs new file mode 100644 index 0000000..d488b3c --- /dev/null +++ b/SQLLinter/Infrastructure/Reporters/Formatters/Html/v3/HtmlReportFormatter_v3.cs @@ -0,0 +1,291 @@ +using SQLLinter.Common; +using SQLLinter.Infrastructure.Diagram; +using SQLLinter.Infrastructure.Rules.RuleViolations; +using System.Data; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace SQLLinter.Infrastructure.Reporters.Formatters.Html.v3; + +public class HtmlReportFormatter : IReportFormatter +{ + public string Format(List violations) + => Format(violations, null); + + public string Format(List violations, BpmnDiagram? diagram) + { + var sb = new StringBuilder(); + GenerateBeginningHtml(sb); + + // Подготовка данных для передачи в JS + var reportData = PrepareReportData(violations, diagram); + var jsonData = JsonSerializer.Serialize(reportData, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + + if (violations.Count == 0 && diagram == null) + { + // Случай без нарушений + sb.AppendLine("
"); + sb.AppendLine("
"); + sb.AppendLine("
"); + sb.AppendLine("

Проверка завершена

"); + sb.AppendLine("

Нарушений правил SQL не обнаружено.

"); + sb.AppendLine("
"); + sb.AppendLine("
"); + + GenerateEndingHtml(sb, false, HtmlMinifier.CompressJson(jsonData)); + return sb.ToString(); + } + + // Основной контейнер для отчета + sb.AppendLine(""" + +
+ + +
+ +
+ + +
+
+
+ + """); + + GenerateEndingHtml(sb, diagram != null, jsonData); + + var html = HtmlMinifier.MinifyHtml(sb.ToString()); + return html; + } + + private ReportData PrepareReportData(List violations, BpmnDiagram? diagram) + { + var reportData = new ReportData(); + + // Группировка по файлам + var groupedByFile = violations + .GroupBy(v => v.FileName) + .OrderBy(g => g.Key) + .ToList(); + + foreach (var fileGroup in groupedByFile) + { + var fileData = new FileReport + { + Name = fileGroup.Key, + }; + + + // Группировка по severity + var severityGroups = fileGroup + .GroupBy(v => v.Severity) + .OrderByDescending(g => g.Key) + .ToList(); + + foreach (var violation in fileGroup.Select(t => t).OrderBy(v => v.Line).ThenBy(v => v.Column)) + { + int ruleId; + List args = new(); + + if (violation is RuleTemplateViolation templateRule) + { + args = templateRule.Params.Select(p => EscapeHtml(p)).ToList(); + + if (reportData.Rules.Any(t => t.Value.Name == templateRule.RuleName)) + { + ruleId = reportData.Rules.First(t => t.Value.Name == templateRule.RuleName).Key; + } + else + { + ruleId = reportData.Rules.Count + 1; + reportData.Rules.Add(ruleId, new Rule + { + Name = templateRule.RuleName, + Template = templateRule.RuleTemplate + }); + } + } + else + { + ruleId = reportData.Rules.Count + 1; + reportData.Rules.Add(ruleId, new Rule + { + Name = violation.RuleName, + Template = violation.Text, + }); + } + + var v = new Violation() + { + RuleId = ruleId, + Args = args, + Column = violation.Column, + Line = violation.Line, + }; + + if (violation.Severity == RuleViolationSeverity.Critical) + { + v.Index = fileData.Violations.Critical.Count + 1; + fileData.Violations.Critical.Add(v); + } + + else if (violation.Severity == RuleViolationSeverity.Warning) + { + v.Index = fileData.Violations.Warning.Count + 1; + fileData.Violations.Warning.Add(v); + } + + else if (violation.Severity == RuleViolationSeverity.Info) + { + v.Index = fileData.Violations.Info.Count + 1; + fileData.Violations.Info.Add(v); + } + } + + reportData.Files.Add(fileData); + } + + // Добавление диаграммы, если есть + if (diagram != null) + { + reportData.Diagram = new Diagram + { + Content = MermaidRenderer.ToMermaidContent(diagram), + HasDiagram = true + }; + } + + return reportData; + } + + private void GenerateBeginningHtml(StringBuilder sb) + { + sb.AppendLine(""" + + + + + + Отчёт по SQL‑проверкам +
"); + } + + private void GenerateEndingHtml(StringBuilder sb, bool hasDiagram, string jsonData) + { + // Вставка JSON данных + sb.AppendLine($""" + + """); + + sb.AppendLine(""" + + + + """); + } + + private static string LoadResource(string endsWith) + { + var assembly = Assembly.GetExecutingAssembly(); + var name = assembly.GetManifestResourceNames() + .First(n => n.EndsWith(endsWith, StringComparison.OrdinalIgnoreCase)); + using var stream = assembly.GetManifestResourceStream(name); + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + + private static string EscapeHtml(string text) + { + return System.Net.WebUtility.HtmlEncode(text); + } + + // Классы для сериализации + private class ReportData + { + [JsonPropertyName("f")] // files + public List Files { get; set; } = new(); + + [JsonPropertyName("r")] // rules + public Dictionary Rules { get; set; } = new(); + + [JsonPropertyName("d")] // diagram + public Diagram Diagram { get; set; } = new(); + } + + private class FileReport + { + [JsonPropertyName("n")] // name + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("v")] // violations + public Violations Violations { get; set; } = new(); + } + + private class Violations + { + [JsonPropertyName("c")] // critical + public List Critical { get; set; } = new(); + + [JsonPropertyName("w")] // warning + public List Warning { get; set; } = new(); + + [JsonPropertyName("i")] // info + public List Info { get; set; } = new(); + } + + private class Violation + { + [JsonPropertyName("i")] // index + public int Index { get; set; } + + [JsonPropertyName("l")] // line + public int Line { get; set; } + + [JsonPropertyName("c")] // column + public int Column { get; set; } + + [JsonPropertyName("r")] // ruleId + public int RuleId { get; set; } + + [JsonPropertyName("a")] // args (optional) + public List? Args { get; set; } + } + + private class Rule + { + [JsonPropertyName("n")] // name + public string Name { get; set; } = string.Empty; + + [JsonPropertyName("t")] // template + public string Template { get; set; } = string.Empty; + } + + private class Diagram + { + [JsonPropertyName("c")] // content + public string Content { get; set; } = string.Empty; + + [JsonPropertyName("h")] // hasDiagram + public bool HasDiagram { get; set; } + } +} \ No newline at end of file diff --git a/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter - Копировать.css b/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter - Копировать.css new file mode 100644 index 0000000..799400a --- /dev/null +++ b/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter - Копировать.css @@ -0,0 +1,1681 @@ +:root { + /* Colors - Light Theme */ + --color-primary: #0078d4; + --color-primary-dark: #106ebe; + --color-primary-darker: #005a9e; + --color-primary-light: #c7e0f4; + --color-primary-lighter: #deecf9; + --color-text-primary: #323130; + --color-text-secondary: #605e5c; + --color-text-tertiary: #a19f9d; + --color-text-disabled: #c8c6c4; + --color-background: #ffffff; + --color-background-alt: #faf9f8; + --color-background-neutral: #f3f2f1; + --color-background-neutral-dark: #edebe9; + --color-border: #edebe9; + --color-border-strong: #d2d0ce; + --color-border-input: #8a8886; + --color-critical: #d13438; + --color-critical-bg: #fde7e9; + --color-warning: #ffaa44; + --color-warning-bg: #fff4ce; + --color-success: #107c10; + --color-success-bg: #dff6dd; + --color-info: #0078d4; + --color-info-bg: #c7e0f4; + --color-mermaid-node: #f0f8ff; + --color-mermaid-node-border: #0078d4; + --color-mermaid-cluster: #f3f2f1; + --color-mermaid-edge: #8a8886; + --color-mermaid-text: #323130; + /* Shadows (облегчённые) */ + --shadow-2: 0 1px 2px rgba(0, 0, 0, 0.08); + --shadow-4: 0 2px 4px rgba(0, 0, 0, 0.12); + --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; + --spacing-lg: 16px; + --spacing-xl: 20px; + --spacing-xxl: 24px; + --spacing-xxxl: 32px; + /* Border Radius */ + --border-radius-small: 2px; + --border-radius-medium: 4px; + --border-radius-large: 6px; + --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; + --font-size-lg: 16px; + --font-size-xl: 18px; + --font-size-xxl: 20px; + --font-size-xxxl: 24px; + /* Transitions */ + --transition-fast: 0.1s cubic-bezier(0.4, 0, 0.2, 1); + --transition-medium: 0.2s cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: 0.3s cubic-bezier(0.4, 0, 0.2, 1); + /* Z-index */ + --z-index-base: 1; + --z-index-dropdown: 100; + --z-index-sticky: 200; + --z-index-fixed: 300; + --z-index-modal: 400; + --z-index-popover: 500; + --z-index-tooltip: 600; +} + +/* Dark Theme Variables */ +@media (prefers-color-scheme: dark) { + :root { + --color-primary: #2899f5; + --color-primary-dark: #3aa0f3; + --color-primary-darker: #6cb8f6; + --color-primary-light: #0f2b4d; + --color-primary-lighter: #1a365d; + --color-text-primary: #f3f2f1; + --color-text-secondary: #a19f9d; + --color-text-tertiary: #797775; + --color-text-disabled: #605e5c; + --color-background: #1e1e1e; + --color-background-alt: #2d2d2d; + --color-background-neutral: #3c3c3c; + --color-background-neutral-dark: #424242; + --color-border: #424242; + --color-border-strong: #616161; + --color-border-input: #797775; + --color-critical: #d13438; + --color-critical-bg: #4c191b; + --color-warning: #ffaa44; + --color-warning-bg: #4c3b1a; + --color-success: #107c10; + --color-success-bg: #1c3b1c; + --color-info: #2899f5; + --color-info-bg: #0f2b4d; + --color-mermaid-node: #2d2d2d; + --color-mermaid-node-border: #2899f5; + --color-mermaid-cluster: #201f1e; + --color-mermaid-edge: #797775; + --color-mermaid-text: #f3f2f1; + } +} + +/* --------------------------------------------------------- + RESET & BASE STYLES +--------------------------------------------------------- */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + scroll-behavior: smooth; +} + +body { + font-family: var(--font-family); + font-size: var(--font-size-md); + line-height: 1.5; + color: var(--color-text-primary); + background-color: var(--color-background-neutral); + margin: 0; + padding: 0; + overflow-x: hidden; +} + +/* --------------------------------------------------------- + TYPOGRAPHY +--------------------------------------------------------- */ +h3 { + font-size: var(--font-size-xl); + font-weight: 600; + line-height: 1.3; + margin-bottom: var(--spacing-md); + color: var(--color-text-primary); +} + +/* --------------------------------------------------------- + FILE REPORTS & LAYOUT +--------------------------------------------------------- */ + +main#main-content { + min-height: 100vh; +} + +/* Основной контейнер отчёта по файлу */ +.file-report { + display: none; + padding-bottom: 120px; + animation: fadeIn var(--transition-slow); +} + + .file-report.active { + display: block; + } + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(6px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Заголовок файла */ +.file-title-container { + padding: var(--spacing-xl) var(--spacing-xl) var(--spacing-lg); + border-bottom: 1px solid var(--color-border); + background-color: var(--color-background); + position: sticky; + top: 0; + z-index: var(--z-index-sticky); + box-shadow: var(--shadow-2); +} + +.file-title { + display: flex; + align-items: center; + gap: var(--spacing-md); + font-size: var(--font-size-xl); + font-weight: 600; + color: var(--color-text-primary); +} + + .file-title svg { + flex-shrink: 0; + opacity: 0.8; + } + +.file-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Состояние без нарушений */ +.no-violations { + display: flex; + align-items: center; + justify-content: center; + min-height: 300px; + padding: var(--spacing-xl); +} + +.no-violations-content { + text-align: center; + max-width: 400px; +} + +.no-violations-icon { + font-size: 48px; + margin-bottom: var(--spacing-md); + color: var(--color-success); +} + +.no-violations-title { + font-size: 20px; + font-weight: 600; + margin-bottom: var(--spacing-sm); + color: var(--color-text-primary); +} + +.no-violations-description { + color: var(--color-text-secondary); + line-height: 1.6; +} + +/* --------------------------------------------------------- + SEVERITY SECTIONS +--------------------------------------------------------- */ +.severity-section { + margin: var(--spacing-xl) var(--spacing-xxxl); + padding: var(--spacing-xl); + background-color: var(--color-background); + border-radius: var(--border-radius-large); + box-shadow: var(--shadow-4); + border-top: 4px solid transparent; + transition: transform var(--transition-medium), box-shadow var(--transition-medium); +} + + .severity-section:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-8); + } + + .severity-section.critical { + border-top-color: var(--color-critical); + } + + .severity-section.warning { + border-top-color: var(--color-warning); + } + + .severity-section.info { + border-top-color: var(--color-info); + } + +.severity-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--spacing-xl); + padding-bottom: var(--spacing-md); + border-bottom: 1px solid var(--color-border); +} + +.severity-title { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.severity-count { + background-color: var(--color-background-neutral); + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + font-weight: 600; + padding: var(--spacing-xs) var(--spacing-sm); + border-radius: var(--border-radius-medium); + min-width: 24px; + text-align: center; + border: 1px solid var(--color-border); +} + +.severity-section.critical .severity-count { + background-color: var(--color-critical-bg); + color: var(--color-critical); + border-color: var(--color-critical); +} + +.severity-section.warning .severity-count { + background-color: var(--color-warning-bg); + color: var(--color-warning); + border-color: var(--color-warning); +} + +.severity-section.info .severity-count { + background-color: var(--color-info-bg); + color: var(--color-info); + border-color: var(--color-info); +} + +/* --------------------------------------------------------- + TABLES +--------------------------------------------------------- */ +.table-container { + overflow-x: auto; + border-radius: var(--border-radius-medium); + border: 1px solid var(--color-border); + background-color: var(--color-background); +} + + .table-container table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + font-size: var(--font-size-sm); + min-width: 100%; + } + + .table-container thead { + background-color: var(--color-background-alt); + position: sticky; + top: 0; + z-index: var(--z-index-base); + } + +th { + font-weight: 600; + text-align: left; + color: var(--color-text-primary); + padding: var(--spacing-md) var(--spacing-lg); + border-bottom: 2px solid var(--color-border); + white-space: nowrap; + position: sticky; + top: 0; + z-index: 10; + background-color: var(--color-background-alt); +} + +td { + padding: var(--spacing-md) var(--spacing-lg); + border-bottom: 1px solid var(--color-border); + color: var(--color-text-primary); + vertical-align: top; +} + +tbody tr { + transition: background-color var(--transition-fast); +} + + tbody tr:last-child td { + border-bottom: none; + } + +/* Table cell specific styles */ +td.line, +td.column { + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: var(--font-size-xs); + color: var(--color-text-secondary); + text-align: center; + width: 60px; + font-feature-settings: "tnum"; + font-variant-numeric: tabular-nums; +} + +td.index { + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + font-size: var(--font-size-xs); + color: var(--color-text-tertiary); + text-align: center; + width: 40px; + font-feature-settings: "tnum"; + font-variant-numeric: tabular-nums; +} + +td.rule { + font-weight: 600; + color: var(--color-text-primary); + width: 250px; +} + +td.description { + color: var(--color-text-primary); + line-height: 1.4; +} + +/* Подсветка строк в зависимости от severity */ +.severity-section.critical tbody tr { + border-left: 3px solid var(--color-critical); +} + + .severity-section.critical tbody tr:hover { + background-color: rgba(209, 52, 56, 0.06); + } + +.severity-section.warning tbody tr { + border-left: 3px solid var(--color-warning); +} + + .severity-section.warning tbody tr:hover { + background-color: rgba(255, 170, 68, 0.06); + } + +.severity-section.info tbody tr { + border-left: 3px solid var(--color-info); +} + + .severity-section.info tbody tr:hover { + background-color: rgba(0, 120, 212, 0.06); + } + +/* --------------------------------------------------------- + TABS NAVIGATION +--------------------------------------------------------- */ +.tabs-container { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: var(--color-background); + border-top: 1px solid var(--color-border); + box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.08); + z-index: var(--z-index-fixed); + padding: var(--spacing-sm) var(--spacing-xxxl); +} + +.tabs { + 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-sm) var(--spacing-md); + background: none; + border: none; + border-bottom: 2px solid transparent; + color: var(--color-text-secondary); + font-family: var(--font-family); + font-size: var(--font-size-md); + font-weight: 600; + cursor: pointer; + 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: auto; + display: flex; + 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 { + background-color: var(--color-background-neutral); + color: var(--color-text-primary); + } + + .tab.active { + color: var(--color-primary); + border-bottom-color: var(--color-primary); + background-color: var(--color-background-neutral); + } + +.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: 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-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; +} + +/* --------------------------------------------------------- + DIAGRAM VIEWER +--------------------------------------------------------- */ +#mermaid.file-report { + position: fixed; + inset: 0; + width: 100vw; + height: 100vh; + background-color: var(--color-background); + z-index: var(--z-index-modal); + display: none; + padding: 0 !important; + margin: 0 !important; +} + + #mermaid.file-report.active { + display: block; + } + +/* Для диаграммы убираем заголовок */ +#mermaid .file-title-container { + display: none; +} + +/* Diagram Toolbar */ +.diagram-toolbar { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 52px; + display: flex; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-sm) var(--spacing-xxxl); + background-color: var(--color-background); + border-bottom: 1px solid var(--color-border); + box-shadow: var(--shadow-4); + z-index: var(--z-index-fixed); +} + +.toolbar-search { + flex: 1; + max-width: 400px; + position: relative; +} + + .toolbar-search input { + width: 100%; + padding: var(--spacing-md) var(--spacing-lg); + padding-right: 40px; + border: 1px solid var(--color-border-input); + border-radius: var(--border-radius-medium); + font-family: var(--font-family); + font-size: var(--font-size-md); + background-color: var(--color-background); + color: var(--color-text-primary); + transition: border-color var(--transition-medium), box-shadow var(--transition-medium), background-color var(--transition-medium); + } + + .toolbar-search input:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 1px var(--color-primary-light); + } + + .toolbar-search input::placeholder { + color: var(--color-text-tertiary); + } + +.search-clear { + position: absolute; + right: var(--spacing-md); + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--color-text-tertiary); + cursor: pointer; + padding: var(--spacing-xs); + font-size: var(--font-size-sm); + opacity: 0.7; + transition: opacity var(--transition-fast), background-color var(--transition-fast); + border-radius: 50%; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; +} + + .search-clear:hover { + opacity: 1; + background-color: var(--color-background-neutral); + } + +.toolbar-actions { + display: flex; + align-items: center; + gap: var(--spacing-sm); + margin-left: auto; +} + +.toolbar-button { + padding: var(--spacing-md) var(--spacing-lg); + border: none; + border-radius: var(--border-radius-medium); + background-color: var(--color-primary); + color: #ffffff; + font-family: var(--font-family); + font-size: var(--font-size-md); + font-weight: 600; + cursor: pointer; + transition: background-color var(--transition-medium), box-shadow var(--transition-medium), transform var(--transition-medium); + display: inline-flex; + align-items: center; + gap: var(--spacing-sm); + min-height: 40px; +} + + .toolbar-button:hover { + background-color: var(--color-primary-dark); + box-shadow: var(--shadow-4); + } + + .toolbar-button:active { + background-color: var(--color-primary-darker); + transform: translateY(1px); + } + + .toolbar-button.secondary { + background-color: transparent; + color: var(--color-text-primary); + border: 1px solid var(--color-border); + } + + .toolbar-button.secondary:hover { + background-color: var(--color-background-neutral); + border-color: var(--color-border-strong); + } + +/* Diagram Container */ +#diagramContainer { + position: absolute; + top: 52px; + left: 0; + width: 100vw; + height: calc(100vh - 112px); /* 52px тулбар + ~60px табы */ + overflow: hidden; + background-color: var(--color-background); +} + +#diagramSvgContainer { + width: 100%; + height: 100%; + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + + #diagramSvgContainer svg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + cursor: grab; + overflow: visible; + display: block; + transition: transform 0.3s ease-out; /* Плавный переход при изменении масштаба */ + } + + #diagramSvgContainer svg:active { + cursor: grabbing; + } + + /* Индикатор загрузки для диаграммы */ + #diagramSvgContainer::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 40px; + height: 40px; + border: 3px solid var(--color-border); + border-top-color: var(--color-primary); + border-radius: 50%; + animation: spin 1s linear infinite; + z-index: 100; + opacity: 0; + transition: opacity 0.3s; + } + + #diagramSvgContainer.loading::before { + opacity: 1; + } + +@keyframes spin { + to { + transform: translate(-50%, -50%) rotate(360deg); + } +} + +/* Minimap */ +#minimap { + position: absolute; + right: var(--spacing-xxl); + bottom: 120px; + width: 200px; + height: 140px; + border: 1px solid var(--color-border-strong); + background-color: var(--color-background); + /* убран backdrop-filter для производительности */ + border-radius: var(--border-radius-large); + box-shadow: var(--shadow-8); + z-index: var(--z-index-popover); + overflow: hidden; +} + +@media (prefers-color-scheme: dark) { + #minimap { + background-color: var(--color-background-alt); + border-color: var(--color-border-input); + } +} + +.minimap-viewport { + position: absolute; + border: 2px solid var(--color-primary); + background-color: rgba(0, 120, 212, 0.15); + pointer-events: none; + transition: all 0.1s ease; + box-sizing: border-box; +} + +@media (prefers-color-scheme: dark) { + .minimap-viewport { + border-color: var(--color-primary); + background-color: rgba(40, 153, 245, 0.2); + } +} + +/* Упрощаем отображение элементов в миникарте */ +#minimap svg { + width: 100%; + height: 100%; +} + +#minimap .node rect { + stroke-width: 1px !important; + rx: 2px !important; + ry: 2px !important; +} + +#minimap .edgePath path { + stroke-width: 1px !important; +} + +#minimap text { + font-size: 4px !important; +} + +/* Search Results Info */ +.search-results-info { + position: absolute; + top: 70px; + left: var(--spacing-xxl); + background: var(--color-background); + padding: var(--spacing-md) var(--spacing-lg); + border-radius: var(--border-radius-large); + box-shadow: var(--shadow-8); + border: 1px solid var(--color-border); + font-size: var(--font-size-sm); + z-index: var(--z-index-popover); + display: flex; + align-items: center; + gap: var(--spacing-md); +} + +/* --------------------------------------------------------- + MERMAID DIAGRAM STYLING +--------------------------------------------------------- */ + +#diagramSvgContainer svg { + font-family: var(--font-family); + font-size: var(--font-size-md); +} + + #diagramSvgContainer svg .label { + font-family: var(--font-family); + } + +/* Nodes */ +svg .node rect { + fill: var(--color-mermaid-node); + stroke: var(--color-mermaid-node-border); + stroke-width: 2px; + rx: 6px; + ry: 6px; +} + +svg .node circle, +svg .node ellipse, +svg .node polygon { + fill: var(--color-primary-lighter); + stroke: var(--color-mermaid-node-border); + stroke-width: 2px; +} + +svg .node text { + fill: var(--color-mermaid-text); + font-family: var(--font-family); + font-size: var(--font-size-md); + font-weight: 500; +} + +/* Clusters */ +svg .cluster rect { + fill: rgba(243, 242, 241, 0.9); + stroke: var(--color-text-tertiary); + stroke-width: 1.5px; + stroke-dasharray: 5, 5; + rx: 8px; + ry: 8px; +} + +svg .cluster text { + fill: var(--color-text-secondary); + font-size: var(--font-size-lg); + font-weight: 600; +} + +/* Arrow and Line Styling */ +svg marker path { + fill: var(--color-mermaid-edge); +} + +svg .arrowheadPath { + fill: var(--color-mermaid-edge) !important; +} + +svg .edgePath path { + stroke: var(--color-mermaid-edge); + stroke-width: 2px; + fill: none; +} + +svg .edgeLabel text { + fill: var(--color-mermaid-text); + font-size: var(--font-size-sm); +} + +svg .edgeLabel rect { + fill: var(--color-background); + stroke: var(--color-border); + stroke-width: 1px; + rx: 4px; + ry: 4px; +} + +/* Text Labels */ +svg .label text { + font-size: var(--font-size-sm); + font-weight: 400; +} + +/* Dark Theme Mermaid Styles */ +@media (prefers-color-scheme: dark) { + svg .node rect { + fill: var(--color-mermaid-node); + stroke: var(--color-mermaid-node-border); + } + + svg .node circle, + svg .node ellipse, + svg .node polygon { + fill: var(--color-background-neutral); + stroke: var(--color-mermaid-node-border); + } + + svg .node text { + fill: var(--color-mermaid-text); + font-weight: 400; + } + + svg .cluster rect { + fill: rgba(32, 31, 30, 0.9); + stroke: var(--color-text-tertiary); + stroke-dasharray: 5, 5; + } + + svg .cluster text { + fill: var(--color-text-tertiary); + font-weight: 500; + } + + svg .edgePath path { + stroke: var(--color-mermaid-edge); + } + + svg .edgeLabel text { + fill: var(--color-mermaid-text); + } + + svg .edgeLabel rect { + fill: var(--color-background-alt); + stroke: var(--color-border-strong); + } + + svg .label text { + fill: var(--color-text-tertiary); + } + + #diagramSvgContainer svg, + #diagramContainer { + background-color: var(--color-background); + } +} + +/* Node Type Specific Styles */ +svg .node.condition rect { + fill: var(--color-warning-bg); + stroke: var(--color-warning); +} + +svg .node.process rect { + fill: var(--color-info-bg); + stroke: var(--color-info); +} + +svg .node.start rect, +svg .node.end rect { + fill: var(--color-primary-lighter); + stroke: var(--color-primary); +} + +svg .node.import rect { + fill: var(--color-success-bg); + stroke: var(--color-success); +} + +svg .node.exec rect { + fill: #f0f0ff; + stroke: #4b4bd8; +} + +@media (prefers-color-scheme: dark) { + svg .node.start rect, + svg .node.end rect { + fill: var(--color-primary-light); + stroke: var(--color-primary); + } + + svg .node.exec rect { + fill: #2d2a4d; + stroke: #4b4bd8; + } +} + +/* Node Highlighting (упрощённо, без тяжёлых фильтров) */ +.node-search-match rect { + stroke: #ffc107 !important; + stroke-width: 3px !important; +} + +.node-highlight rect { + stroke: #ff5722 !important; + stroke-width: 3px !important; +} + +.node-search-active rect { + stroke: #4caf50 !important; + stroke-width: 3px !important; +} + +/* Усиление видимости линий */ +svg .edgePath, +svg .edgePath path, +svg .flowchart-link { + stroke: var(--color-mermaid-edge) !important; + stroke-width: 2.4px !important; + fill: none !important; + opacity: 1 !important; + vector-effect: non-scaling-stroke !important; +} + + svg .edgePath:hover .path, + svg .edgePath:hover path { + stroke-width: 3px !important; + } + +/* --------------------------------------------------------- + TOAST NOTIFICATIONS +--------------------------------------------------------- */ +.toast { + position: fixed; + top: 80px; + right: var(--spacing-xxl); + background: var(--color-background); + color: var(--color-text-primary); + padding: var(--spacing-md) var(--spacing-lg); + border-radius: var(--border-radius-large); + box-shadow: var(--shadow-8); + border: 1px solid var(--color-border); + z-index: var(--z-index-tooltip); + display: flex; + align-items: center; + gap: var(--spacing-md); + max-width: 400px; + border-left: 4px solid var(--color-primary); +} + +/* --------------------------------------------------------- + SCROLLBAR STYLING +--------------------------------------------------------- */ +::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +::-webkit-scrollbar-track { + background: var(--color-background-neutral); + border-radius: var(--border-radius-medium); +} + +::-webkit-scrollbar-thumb { + background: var(--color-border-strong); + border-radius: var(--border-radius-medium); + border: 3px solid var(--color-background-neutral); +} + + ::-webkit-scrollbar-thumb:hover { + background: var(--color-text-tertiary); + } + +::-webkit-scrollbar-corner { + background: var(--color-background-neutral); +} + +/* For Firefox */ +* { + scrollbar-width: thin; + scrollbar-color: var(--color-border-strong) var(--color-background-neutral); +} + +/* --------------------------------------------------------- + RESPONSIVE ADJUSTMENTS +--------------------------------------------------------- */ +@media (max-width: 768px) { + .severity-section { + margin: var(--spacing-lg); + padding: var(--spacing-lg); + } + + .tabs-container { + padding: var(--spacing-sm) var(--spacing-lg); + } + + .diagram-toolbar { + padding: var(--spacing-sm) var(--spacing-lg); + } + + .toolbar-actions { + flex-wrap: wrap; + justify-content: flex-end; + } + + .toolbar-button { + padding: var(--spacing-sm) var(--spacing-md); + font-size: var(--font-size-sm); + } + + #minimap { + right: var(--spacing-lg); + bottom: 100px; + width: 150px; + height: 105px; + } + + .search-results-info { + left: var(--spacing-lg); + right: var(--spacing-lg); + max-width: none; + } + + .toast { + right: var(--spacing-lg); + left: var(--spacing-lg); + max-width: none; + } + + .file-title-container { + padding: var(--spacing-lg); + } + + .file-title { + font-size: var(--font-size-lg); + } + + #diagramContainer { + height: calc(100vh - 102px); + } +} + +@media (max-width: 480px) { + :root { + --spacing-xxxl: 24px; + --spacing-xxl: 20px; + --spacing-xl: 16px; + --spacing-lg: 12px; + --spacing-md: 8px; + --spacing-sm: 6px; + --spacing-xs: 4px; + } + + .severity-header { + flex-direction: column; + align-items: flex-start; + gap: var(--spacing-md); + } + + .toolbar-search { + max-width: none; + } + + .toolbar-actions { + width: 100%; + justify-content: space-between; + } + + .tabs { + gap: 2px; + } + + .tab { + padding: var(--spacing-sm) var(--spacing-md); + font-size: var(--font-size-sm); + } +} + +/* --------------------------------------------------------- + PRINT STYLES +--------------------------------------------------------- */ +@media print { + .tabs-container, + .diagram-toolbar, + #minimap, + .search-results-info, + .toast { + display: none !important; + } + + body { + background-color: #ffffff; + color: #000000; + } + + .file-report { + padding-bottom: 0; + display: block !important; + } + + .severity-section { + break-inside: avoid; + box-shadow: none; + border: 1px solid #ddd; + } + + table { + break-inside: avoid; + } +} + +/* --------------------------------------------------------- + ACCESSIBILITY +--------------------------------------------------------- */ +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; + border-radius: var(--border-radius-small); +} + +::selection { + background-color: var(--color-primary); + color: #ffffff; +} + +::-moz-selection { + background-color: var(--color-primary); + color: #ffffff; +} + +/* Убираем blue highlight на mobile tap */ +* { + -webkit-tap-highlight-color: transparent; +} + + +/* Summary page styles */ +.summary-section { + margin: var(--spacing-xl) var(--spacing-xxxl); + padding: var(--spacing-xl); + background-color: var(--color-background); + border-radius: var(--border-radius-large); + box-shadow: var(--shadow-4); + transition: transform var(--transition-medium), box-shadow var(--transition-medium); +} + + .summary-section:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-8); + } + +.summary-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--spacing-xl); + padding-bottom: var(--spacing-md); + border-bottom: 1px solid var(--color-border); +} + +.summary-title { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.summary-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--spacing-lg); + margin-bottom: var(--spacing-xl); +} + +.stat-card { + padding: var(--spacing-lg); + border-radius: var(--border-radius-medium); + background-color: var(--color-background-alt); + border: 1px solid var(--color-border); + transition: all var(--transition-medium); +} + + .stat-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-8); + } + + .stat-card.critical { + border-top: 4px solid var(--color-critical); + } + + .stat-card.warning { + border-top: 4px solid var(--color-warning); + } + + .stat-card.info { + border-top: 4px solid var(--color-info); + } + + .stat-card.success { + border-top: 4px solid var(--color-success); + } + +.stat-value { + font-size: var(--font-size-xxxl); + font-weight: 700; + margin-bottom: var(--spacing-xs); + line-height: 1; +} + +.stat-label { + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + margin-bottom: var(--spacing-xs); +} + +.stat-change { + font-size: var(--font-size-xs); + display: flex; + align-items: center; + gap: 2px; +} + + .stat-change.positive { + color: var(--color-success); + } + + .stat-change.negative { + color: var(--color-critical); + } + +.files-overview { + margin-top: var(--spacing-xxl); +} + +.files-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: var(--spacing-lg); +} + +.file-card { + padding: var(--spacing-lg); + border-radius: var(--border-radius-medium); + background-color: var(--color-background-alt); + border: 1px solid var(--color-border); + transition: all var(--transition-medium); + display: flex; + flex-direction: column; +} + + .file-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-8); + } + +.file-card-header { + display: flex; + align-items: flex-start; /* Change from center to flex-start */ + justify-content: space-between; + margin-bottom: var(--spacing-md); + gap: var(--spacing-sm); /* Add gap between name and badges */ +} + + +.file-name-small { + font-weight: 600; + color: var(--color-text-primary); + /* Remove truncation properties */ + overflow: visible; + text-overflow: clip; + white-space: normal; /* Change from nowrap to normal */ + word-wrap: break-word; + word-break: break-word; + flex: 1; + line-height: 1.4; + max-height: none; /* Remove max-height if exists */ + display: block; /* Change from -webkit-box */ + -webkit-line-clamp: unset; /* Remove line clamp */ + -webkit-box-orient: unset; +} + +.file-violations { + display: flex; + gap: var(--spacing-xs); + justify-content: flex-end; + flex-shrink: 0; + align-items: flex-start; /* Align badges to top */ +} + +.violation-badge { + font-size: var(--font-size-xs); + padding: var(--spacing-xxs) var(--spacing-xs); + border-radius: var(--border-radius-small); + font-weight: 600; +} + + .violation-badge.critical { + background-color: var(--color-critical-bg); + color: var(--color-critical); + border: 1px solid var(--color-critical); + } + + .violation-badge.warning { + background-color: var(--color-warning-bg); + color: var(--color-warning); + border: 1px solid var(--color-warning); + } + + .violation-badge.info { + background-color: var(--color-info-bg); + color: var(--color-info); + border: 1px solid var(--color-info); + } + +.progress-bar { + height: 8px; + background-color: var(--color-background-neutral); + border-radius: var(--border-radius-small); + overflow: hidden; + margin-top: var(--spacing-md); + display: flex; /* Изменено с block на flex */ +} + +.progress-fill { + height: 100%; + transition: width var(--transition-medium); + flex-shrink: 0; /* Предотвращает сжатие */ +} + + .progress-fill.critical { + background-color: var(--color-critical); + order: 1; + } + + .progress-fill.warning { + background-color: var(--color-warning); + order: 2; + } + + .progress-fill.info { + background-color: var(--color-info); + order: 3; + } + +@media (max-width: 768px) { + .summary-section { + margin: var(--spacing-lg); + padding: var(--spacing-lg); + } + + .summary-stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .files-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 480px) { + .summary-stats-grid { + grid-template-columns: 1fr; + } + + .stat-value { + font-size: var(--font-size-xxl); + } +} \ No newline at end of file diff --git a/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.css b/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.css index 799400a..eaad3ab 100644 --- a/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.css +++ b/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.css @@ -1,1681 +1,533 @@ -:root { - /* Colors - Light Theme */ - --color-primary: #0078d4; - --color-primary-dark: #106ebe; - --color-primary-darker: #005a9e; - --color-primary-light: #c7e0f4; - --color-primary-lighter: #deecf9; - --color-text-primary: #323130; - --color-text-secondary: #605e5c; - --color-text-tertiary: #a19f9d; - --color-text-disabled: #c8c6c4; - --color-background: #ffffff; - --color-background-alt: #faf9f8; - --color-background-neutral: #f3f2f1; - --color-background-neutral-dark: #edebe9; - --color-border: #edebe9; - --color-border-strong: #d2d0ce; - --color-border-input: #8a8886; +/* --------------------------------------------------------- + FLUENT UI 2 — BASE VARIABLES +--------------------------------------------------------- */ +:root { + --color-primary: #0f6cbd; + --color-primary-hover: #115ea3; + --color-primary-light: #d0e7ff; + --color-bg: #ffffff; + --color-bg-alt: #f5f5f5; + --color-bg-neutral: #f3f2f1; + --color-border: #d1d1d1; + --color-border-strong: #b5b5b5; + --color-text: #1a1a1a; + --color-text-secondary: #5e5e5e; + --color-text-tertiary: #8a8a8a; --color-critical: #d13438; - --color-critical-bg: #fde7e9; + --color-critical-bg: #f8d7da; --color-warning: #ffaa44; --color-warning-bg: #fff4ce; - --color-success: #107c10; - --color-success-bg: #dff6dd; - --color-info: #0078d4; - --color-info-bg: #c7e0f4; - --color-mermaid-node: #f0f8ff; - --color-mermaid-node-border: #0078d4; - --color-mermaid-cluster: #f3f2f1; - --color-mermaid-edge: #8a8886; - --color-mermaid-text: #323130; - /* Shadows (облегчённые) */ - --shadow-2: 0 1px 2px rgba(0, 0, 0, 0.08); - --shadow-4: 0 2px 4px rgba(0, 0, 0, 0.12); - --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; - --spacing-lg: 16px; - --spacing-xl: 20px; - --spacing-xxl: 24px; - --spacing-xxxl: 32px; - /* Border Radius */ - --border-radius-small: 2px; - --border-radius-medium: 4px; - --border-radius-large: 6px; - --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; - --font-size-lg: 16px; - --font-size-xl: 18px; - --font-size-xxl: 20px; - --font-size-xxxl: 24px; - /* Transitions */ - --transition-fast: 0.1s cubic-bezier(0.4, 0, 0.2, 1); - --transition-medium: 0.2s cubic-bezier(0.4, 0, 0.2, 1); - --transition-slow: 0.3s cubic-bezier(0.4, 0, 0.2, 1); - /* Z-index */ - --z-index-base: 1; - --z-index-dropdown: 100; - --z-index-sticky: 200; - --z-index-fixed: 300; - --z-index-modal: 400; - --z-index-popover: 500; - --z-index-tooltip: 600; -} - -/* Dark Theme Variables */ -@media (prefers-color-scheme: dark) { - :root { - --color-primary: #2899f5; - --color-primary-dark: #3aa0f3; - --color-primary-darker: #6cb8f6; - --color-primary-light: #0f2b4d; - --color-primary-lighter: #1a365d; - --color-text-primary: #f3f2f1; - --color-text-secondary: #a19f9d; - --color-text-tertiary: #797775; - --color-text-disabled: #605e5c; - --color-background: #1e1e1e; - --color-background-alt: #2d2d2d; - --color-background-neutral: #3c3c3c; - --color-background-neutral-dark: #424242; - --color-border: #424242; - --color-border-strong: #616161; - --color-border-input: #797775; - --color-critical: #d13438; - --color-critical-bg: #4c191b; - --color-warning: #ffaa44; - --color-warning-bg: #4c3b1a; - --color-success: #107c10; - --color-success-bg: #1c3b1c; - --color-info: #2899f5; - --color-info-bg: #0f2b4d; - --color-mermaid-node: #2d2d2d; - --color-mermaid-node-border: #2899f5; - --color-mermaid-cluster: #201f1e; - --color-mermaid-edge: #797775; - --color-mermaid-text: #f3f2f1; - } + --color-info: #0f6cbd; + --color-info-bg: #d0e7ff; + --radius-s: 4px; + --radius-m: 6px; + --radius-l: 8px; + --space-xs: 4px; + --space-s: 8px; + --space-m: 12px; + --space-l: 16px; + --space-xl: 20px; + --space-xxl: 28px; + --font-s: 12px; + --font-m: 14px; + --font-l: 16px; + --font-xl: 18px; + --z-tabs: 1000; + --z-header: 900; } /* --------------------------------------------------------- - RESET & BASE STYLES + RESET --------------------------------------------------------- */ -*, -*::before, -*::after { - box-sizing: border-box; +* { margin: 0; padding: 0; -} - -html { - font-size: 100%; - -webkit-text-size-adjust: 100%; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - scroll-behavior: smooth; + box-sizing: border-box; } body { - font-family: var(--font-family); - font-size: var(--font-size-md); + font-family: "Segoe UI", sans-serif; + background: var(--color-bg-neutral); + color: var(--color-text); line-height: 1.5; - color: var(--color-text-primary); - background-color: var(--color-background-neutral); - margin: 0; - padding: 0; - overflow-x: hidden; } /* --------------------------------------------------------- - TYPOGRAPHY + FILE REPORT WRAPPER --------------------------------------------------------- */ -h3 { - font-size: var(--font-size-xl); - font-weight: 600; - line-height: 1.3; - margin-bottom: var(--spacing-md); - color: var(--color-text-primary); -} - -/* --------------------------------------------------------- - FILE REPORTS & LAYOUT ---------------------------------------------------------- */ - -main#main-content { - min-height: 100vh; -} - -/* Основной контейнер отчёта по файлу */ .file-report { display: none; padding-bottom: 120px; - animation: fadeIn var(--transition-slow); } .file-report.active { display: block; } -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(6px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -/* Заголовок файла */ +/* --------------------------------------------------------- + FILE TITLE (STICKY) +--------------------------------------------------------- */ .file-title-container { - padding: var(--spacing-xl) var(--spacing-xl) var(--spacing-lg); - border-bottom: 1px solid var(--color-border); - background-color: var(--color-background); position: sticky; top: 0; - z-index: var(--z-index-sticky); - box-shadow: var(--shadow-2); + z-index: var(--z-header); + background: var(--color-bg); + border-bottom: 1px solid var(--color-border); + padding: var(--space-l) var(--space-xl); } .file-title { - display: flex; - align-items: center; - gap: var(--spacing-md); - font-size: var(--font-size-xl); + font-size: var(--font-xl); font-weight: 600; - color: var(--color-text-primary); -} - - .file-title svg { - flex-shrink: 0; - opacity: 0.8; - } - -.file-name { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -/* Состояние без нарушений */ -.no-violations { - display: flex; - align-items: center; - justify-content: center; - min-height: 300px; - padding: var(--spacing-xl); -} - -.no-violations-content { - text-align: center; - max-width: 400px; -} - -.no-violations-icon { - font-size: 48px; - margin-bottom: var(--spacing-md); - color: var(--color-success); -} - -.no-violations-title { - font-size: 20px; - font-weight: 600; - margin-bottom: var(--spacing-sm); - color: var(--color-text-primary); -} - -.no-violations-description { - color: var(--color-text-secondary); - line-height: 1.6; + color: var(--color-text); } /* --------------------------------------------------------- - SEVERITY SECTIONS + SEVERITY SECTIONS (FLUENT UI 2 STYLE) --------------------------------------------------------- */ .severity-section { - margin: var(--spacing-xl) var(--spacing-xxxl); - padding: var(--spacing-xl); - background-color: var(--color-background); - border-radius: var(--border-radius-large); - box-shadow: var(--shadow-4); - border-top: 4px solid transparent; - transition: transform var(--transition-medium), box-shadow var(--transition-medium); + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-l); + padding: var(--space-xl); + margin: var(--space-xl) var(--space-xl); } - .severity-section:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-8); - } - .severity-section.critical { - border-top-color: var(--color-critical); + border-left: 4px solid var(--color-critical); } .severity-section.warning { - border-top-color: var(--color-warning); + border-left: 4px solid var(--color-warning); } .severity-section.info { - border-top-color: var(--color-info); + border-left: 4px solid var(--color-info); } .severity-header { display: flex; - align-items: center; justify-content: space-between; - margin-bottom: var(--spacing-xl); - padding-bottom: var(--spacing-md); + align-items: center; + margin-bottom: var(--space-l); + padding-bottom: var(--space-s); border-bottom: 1px solid var(--color-border); } -.severity-title { - display: flex; - align-items: center; - gap: var(--spacing-sm); +.severity-title h2 { + font-size: var(--font-l); + font-weight: 600; } .severity-count { - background-color: var(--color-background-neutral); + background: var(--color-bg-alt); + padding: var(--space-xs) var(--space-s); + border-radius: var(--radius-s); + font-size: var(--font-s); color: var(--color-text-secondary); - font-size: var(--font-size-sm); - font-weight: 600; - padding: var(--spacing-xs) var(--spacing-sm); - border-radius: var(--border-radius-medium); - min-width: 24px; - text-align: center; - border: 1px solid var(--color-border); -} - -.severity-section.critical .severity-count { - background-color: var(--color-critical-bg); - color: var(--color-critical); - border-color: var(--color-critical); -} - -.severity-section.warning .severity-count { - background-color: var(--color-warning-bg); - color: var(--color-warning); - border-color: var(--color-warning); -} - -.severity-section.info .severity-count { - background-color: var(--color-info-bg); - color: var(--color-info); - border-color: var(--color-info); } /* --------------------------------------------------------- - TABLES + GRID TABLE (REPLACES
) --------------------------------------------------------- */ -.table-container { - overflow-x: auto; - border-radius: var(--border-radius-medium); - border: 1px solid var(--color-border); - background-color: var(--color-background); +.grid-table { + display: grid; + gap: var(--space-xs); } - .table-container table { - width: 100%; - border-collapse: separate; - border-spacing: 0; - font-size: var(--font-size-sm); - min-width: 100%; - } +.grid-header, +.grid-row { + display: grid; + grid-template-columns: 40px 60px 60px 250px 1fr; + gap: var(--space-xs); + padding: var(--space-s); + border-radius: var(--radius-s); +} - .table-container thead { - background-color: var(--color-background-alt); - position: sticky; - top: 0; - z-index: var(--z-index-base); - } - -th { +.grid-header { + background: var(--color-bg-alt); font-weight: 600; - text-align: left; - color: var(--color-text-primary); - padding: var(--spacing-md) var(--spacing-lg); - border-bottom: 2px solid var(--color-border); - white-space: nowrap; - position: sticky; - top: 0; - z-index: 10; - background-color: var(--color-background-alt); -} - -td { - padding: var(--spacing-md) var(--spacing-lg); - border-bottom: 1px solid var(--color-border); - color: var(--color-text-primary); - vertical-align: top; -} - -tbody tr { - transition: background-color var(--transition-fast); -} - - tbody tr:last-child td { - border-bottom: none; - } - -/* Table cell specific styles */ -td.line, -td.column { - font-family: 'Consolas', 'Monaco', 'Courier New', monospace; - font-size: var(--font-size-xs); color: var(--color-text-secondary); - text-align: center; - width: 60px; - font-feature-settings: "tnum"; - font-variant-numeric: tabular-nums; + font-size: var(--font-s); } -td.index { - font-family: 'Consolas', 'Monaco', 'Courier New', monospace; - font-size: var(--font-size-xs); - color: var(--color-text-tertiary); - text-align: center; - width: 40px; - font-feature-settings: "tnum"; - font-variant-numeric: tabular-nums; +.grid-row { + background: var(--color-bg); + border: 1px solid var(--color-border); + font-size: var(--font-m); } -td.rule { - font-weight: 600; - color: var(--color-text-primary); - width: 250px; -} - -td.description { - color: var(--color-text-primary); - line-height: 1.4; -} - -/* Подсветка строк в зависимости от severity */ -.severity-section.critical tbody tr { - border-left: 3px solid var(--color-critical); -} - - .severity-section.critical tbody tr:hover { - background-color: rgba(209, 52, 56, 0.06); - } - -.severity-section.warning tbody tr { - border-left: 3px solid var(--color-warning); -} - - .severity-section.warning tbody tr:hover { - background-color: rgba(255, 170, 68, 0.06); - } - -.severity-section.info tbody tr { - border-left: 3px solid var(--color-info); -} - - .severity-section.info tbody tr:hover { - background-color: rgba(0, 120, 212, 0.06); + .grid-row:hover { + background: var(--color-bg-alt); } /* --------------------------------------------------------- - TABS NAVIGATION + SUMMARY SECTION +--------------------------------------------------------- */ +.summary-section { + background: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-l); + padding: var(--space-xl); + margin: var(--space-xl); +} + +.summary-header h2 { + font-size: var(--font-l); + margin-bottom: var(--space-m); +} + +.summary-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: var(--space-m); + margin-bottom: var(--space-l); +} + +.stat-card { + background: var(--color-bg-alt); + border-radius: var(--radius-m); + padding: var(--space-m); + text-align: center; +} + +.stat-value { + font-size: var(--font-xl); + font-weight: 600; +} + +.stat-label { + font-size: var(--font-s); + color: var(--color-text-secondary); +} + +/* Progress bar */ +.progress-bar { + display: flex; + height: 8px; + border-radius: var(--radius-s); + overflow: hidden; + background: var(--color-border); +} + +.progress-fill.critical { + background: var(--color-critical); +} + +.progress-fill.warning { + background: var(--color-warning); +} + +.progress-fill.info { + background: var(--color-info); +} + +/* --------------------------------------------------------- + FILE CARDS IN SUMMARY +--------------------------------------------------------- */ +.files-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: var(--space-m); +} + +.file-card { + background: var(--color-bg-alt); + border-radius: var(--radius-m); + padding: var(--space-m); +} + +.file-name-small { + font-weight: 600; + font-size: var(--font-m); +} + +.file-card-bottom { + display: flex; + justify-content: space-between; + margin-top: var(--space-s); +} + +.file-violations { + display: flex; + gap: var(--space-xs); +} + +.violation-badge { + padding: var(--space-xs) var(--space-s); + border-radius: var(--radius-s); + font-size: var(--font-s); + color: var(--color-text); +} + + .violation-badge.critical { + background: var(--color-critical-bg); + color: var(--color-critical); + } + + .violation-badge.warning { + background: var(--color-warning-bg); + color: var(--color-warning); + } + + .violation-badge.info { + background: var(--color-info-bg); + color: var(--color-info); + } + +/* --------------------------------------------------------- + TABS (STICKY BOTTOM) --------------------------------------------------------- */ .tabs-container { position: fixed; bottom: 0; left: 0; right: 0; - background-color: var(--color-background); + background: var(--color-bg); border-top: 1px solid var(--color-border); - box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.08); - z-index: var(--z-index-fixed); - padding: var(--spacing-sm) var(--spacing-xxxl); + padding: var(--space-s) var(--space-l); + z-index: var(--z-tabs); } .tabs { display: flex; - gap: var(--spacing-xs); + gap: var(--space-s); 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; /* Минимальная высота */ + padding-bottom: var(--space-xs); } - .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-sm) var(--spacing-md); - background: none; - border: none; - border-bottom: 2px solid transparent; - color: var(--color-text-secondary); - font-family: var(--font-family); - font-size: var(--font-size-md); - font-weight: 600; + background: var(--color-bg-alt); + border: 1px solid var(--color-border); + border-radius: var(--radius-m); + padding: var(--space-s) var(--space-m); cursor: pointer; - 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: auto; - display: flex; - 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; /* Минимальная ширина для читаемости */ + font-size: var(--font-m); + font-weight: 500; + color: var(--color-text-secondary); + min-width: 140px; text-align: left; - line-height: 1.4; } .tab:hover { - background-color: var(--color-background-neutral); - color: var(--color-text-primary); + background: var(--color-bg-neutral); } .tab.active { + background: var(--color-primary-light); + border-color: var(--color-primary); color: var(--color-primary); - border-bottom-color: var(--color-primary); - background-color: var(--color-background-neutral); } .tab-inner { display: flex; - align-items: flex-start; justify-content: space-between; - width: 100%; - min-width: 0; /* Важно для работы text-overflow */ - gap: 8px; + gap: var(--space-s); } .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: 16px; - height: 16px; - padding: 0 var(--spacing-xxs); - font-size: var(--font-size-xxs); - font-weight: 400; - border-radius: var(--border-radius-small); + font-size: var(--font-s); + padding: 2px 4px; + border-radius: var(--radius-s); 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-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); + .tab-counter.critical { + background: var(--color-critical-bg); color: var(--color-critical); - border-color: var(--color-critical); } - .tab-counter.warning:not(.empty) { - background-color: var(--color-warning-bg); + .tab-counter.warning { + background: var(--color-warning-bg); color: var(--color-warning); - border-color: var(--color-warning); } - .tab-counter.info:not(.empty) { - background-color: var(--color-info-bg); + .tab-counter.info { + background: 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; + opacity: 0.3; } - .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; -} /* --------------------------------------------------------- - DIAGRAM VIEWER + FLUENT UI 2 — DARK THEME --------------------------------------------------------- */ -#mermaid.file-report { - position: fixed; - inset: 0; - width: 100vw; - height: 100vh; - background-color: var(--color-background); - z-index: var(--z-index-modal); - display: none; - padding: 0 !important; - margin: 0 !important; -} - - #mermaid.file-report.active { - display: block; - } - -/* Для диаграммы убираем заголовок */ -#mermaid .file-title-container { - display: none; -} - -/* Diagram Toolbar */ -.diagram-toolbar { - position: fixed; - top: 0; - left: 0; - right: 0; - height: 52px; - display: flex; - align-items: center; - gap: var(--spacing-md); - padding: var(--spacing-sm) var(--spacing-xxxl); - background-color: var(--color-background); - border-bottom: 1px solid var(--color-border); - box-shadow: var(--shadow-4); - z-index: var(--z-index-fixed); -} - -.toolbar-search { - flex: 1; - max-width: 400px; - position: relative; -} - - .toolbar-search input { - width: 100%; - padding: var(--spacing-md) var(--spacing-lg); - padding-right: 40px; - border: 1px solid var(--color-border-input); - border-radius: var(--border-radius-medium); - font-family: var(--font-family); - font-size: var(--font-size-md); - background-color: var(--color-background); - color: var(--color-text-primary); - transition: border-color var(--transition-medium), box-shadow var(--transition-medium), background-color var(--transition-medium); - } - - .toolbar-search input:focus { - outline: none; - border-color: var(--color-primary); - box-shadow: 0 0 0 1px var(--color-primary-light); - } - - .toolbar-search input::placeholder { - color: var(--color-text-tertiary); - } - -.search-clear { - position: absolute; - right: var(--spacing-md); - top: 50%; - transform: translateY(-50%); - background: none; - border: none; - color: var(--color-text-tertiary); - cursor: pointer; - padding: var(--spacing-xs); - font-size: var(--font-size-sm); - opacity: 0.7; - transition: opacity var(--transition-fast), background-color var(--transition-fast); - border-radius: 50%; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; -} - - .search-clear:hover { - opacity: 1; - background-color: var(--color-background-neutral); - } - -.toolbar-actions { - display: flex; - align-items: center; - gap: var(--spacing-sm); - margin-left: auto; -} - -.toolbar-button { - padding: var(--spacing-md) var(--spacing-lg); - border: none; - border-radius: var(--border-radius-medium); - background-color: var(--color-primary); - color: #ffffff; - font-family: var(--font-family); - font-size: var(--font-size-md); - font-weight: 600; - cursor: pointer; - transition: background-color var(--transition-medium), box-shadow var(--transition-medium), transform var(--transition-medium); - display: inline-flex; - align-items: center; - gap: var(--spacing-sm); - min-height: 40px; -} - - .toolbar-button:hover { - background-color: var(--color-primary-dark); - box-shadow: var(--shadow-4); - } - - .toolbar-button:active { - background-color: var(--color-primary-darker); - transform: translateY(1px); - } - - .toolbar-button.secondary { - background-color: transparent; - color: var(--color-text-primary); - border: 1px solid var(--color-border); - } - - .toolbar-button.secondary:hover { - background-color: var(--color-background-neutral); - border-color: var(--color-border-strong); - } - -/* Diagram Container */ -#diagramContainer { - position: absolute; - top: 52px; - left: 0; - width: 100vw; - height: calc(100vh - 112px); /* 52px тулбар + ~60px табы */ - overflow: hidden; - background-color: var(--color-background); -} - -#diagramSvgContainer { - width: 100%; - height: 100%; - position: relative; - display: flex; - align-items: center; - justify-content: center; -} - - #diagramSvgContainer svg { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - cursor: grab; - overflow: visible; - display: block; - transition: transform 0.3s ease-out; /* Плавный переход при изменении масштаба */ - } - - #diagramSvgContainer svg:active { - cursor: grabbing; - } - - /* Индикатор загрузки для диаграммы */ - #diagramSvgContainer::before { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 40px; - height: 40px; - border: 3px solid var(--color-border); - border-top-color: var(--color-primary); - border-radius: 50%; - animation: spin 1s linear infinite; - z-index: 100; - opacity: 0; - transition: opacity 0.3s; - } - - #diagramSvgContainer.loading::before { - opacity: 1; - } - -@keyframes spin { - to { - transform: translate(-50%, -50%) rotate(360deg); - } -} - -/* Minimap */ -#minimap { - position: absolute; - right: var(--spacing-xxl); - bottom: 120px; - width: 200px; - height: 140px; - border: 1px solid var(--color-border-strong); - background-color: var(--color-background); - /* убран backdrop-filter для производительности */ - border-radius: var(--border-radius-large); - box-shadow: var(--shadow-8); - z-index: var(--z-index-popover); - overflow: hidden; -} - @media (prefers-color-scheme: dark) { - #minimap { - background-color: var(--color-background-alt); - border-color: var(--color-border-input); - } -} - -.minimap-viewport { - position: absolute; - border: 2px solid var(--color-primary); - background-color: rgba(0, 120, 212, 0.15); - pointer-events: none; - transition: all 0.1s ease; - box-sizing: border-box; -} - -@media (prefers-color-scheme: dark) { - .minimap-viewport { - border-color: var(--color-primary); - background-color: rgba(40, 153, 245, 0.2); - } -} - -/* Упрощаем отображение элементов в миникарте */ -#minimap svg { - width: 100%; - height: 100%; -} - -#minimap .node rect { - stroke-width: 1px !important; - rx: 2px !important; - ry: 2px !important; -} - -#minimap .edgePath path { - stroke-width: 1px !important; -} - -#minimap text { - font-size: 4px !important; -} - -/* Search Results Info */ -.search-results-info { - position: absolute; - top: 70px; - left: var(--spacing-xxl); - background: var(--color-background); - padding: var(--spacing-md) var(--spacing-lg); - border-radius: var(--border-radius-large); - box-shadow: var(--shadow-8); - border: 1px solid var(--color-border); - font-size: var(--font-size-sm); - z-index: var(--z-index-popover); - display: flex; - align-items: center; - gap: var(--spacing-md); -} - -/* --------------------------------------------------------- - MERMAID DIAGRAM STYLING ---------------------------------------------------------- */ - -#diagramSvgContainer svg { - font-family: var(--font-family); - font-size: var(--font-size-md); -} - - #diagramSvgContainer svg .label { - font-family: var(--font-family); - } - -/* Nodes */ -svg .node rect { - fill: var(--color-mermaid-node); - stroke: var(--color-mermaid-node-border); - stroke-width: 2px; - rx: 6px; - ry: 6px; -} - -svg .node circle, -svg .node ellipse, -svg .node polygon { - fill: var(--color-primary-lighter); - stroke: var(--color-mermaid-node-border); - stroke-width: 2px; -} - -svg .node text { - fill: var(--color-mermaid-text); - font-family: var(--font-family); - font-size: var(--font-size-md); - font-weight: 500; -} - -/* Clusters */ -svg .cluster rect { - fill: rgba(243, 242, 241, 0.9); - stroke: var(--color-text-tertiary); - stroke-width: 1.5px; - stroke-dasharray: 5, 5; - rx: 8px; - ry: 8px; -} - -svg .cluster text { - fill: var(--color-text-secondary); - font-size: var(--font-size-lg); - font-weight: 600; -} - -/* Arrow and Line Styling */ -svg marker path { - fill: var(--color-mermaid-edge); -} - -svg .arrowheadPath { - fill: var(--color-mermaid-edge) !important; -} - -svg .edgePath path { - stroke: var(--color-mermaid-edge); - stroke-width: 2px; - fill: none; -} - -svg .edgeLabel text { - fill: var(--color-mermaid-text); - font-size: var(--font-size-sm); -} - -svg .edgeLabel rect { - fill: var(--color-background); - stroke: var(--color-border); - stroke-width: 1px; - rx: 4px; - ry: 4px; -} - -/* Text Labels */ -svg .label text { - font-size: var(--font-size-sm); - font-weight: 400; -} - -/* Dark Theme Mermaid Styles */ -@media (prefers-color-scheme: dark) { - svg .node rect { - fill: var(--color-mermaid-node); - stroke: var(--color-mermaid-node-border); - } - - svg .node circle, - svg .node ellipse, - svg .node polygon { - fill: var(--color-background-neutral); - stroke: var(--color-mermaid-node-border); - } - - svg .node text { - fill: var(--color-mermaid-text); - font-weight: 400; - } - - svg .cluster rect { - fill: rgba(32, 31, 30, 0.9); - stroke: var(--color-text-tertiary); - stroke-dasharray: 5, 5; - } - - svg .cluster text { - fill: var(--color-text-tertiary); - font-weight: 500; - } - - svg .edgePath path { - stroke: var(--color-mermaid-edge); - } - - svg .edgeLabel text { - fill: var(--color-mermaid-text); - } - - svg .edgeLabel rect { - fill: var(--color-background-alt); - stroke: var(--color-border-strong); - } - - svg .label text { - fill: var(--color-text-tertiary); - } - - #diagramSvgContainer svg, - #diagramContainer { - background-color: var(--color-background); - } -} - -/* Node Type Specific Styles */ -svg .node.condition rect { - fill: var(--color-warning-bg); - stroke: var(--color-warning); -} - -svg .node.process rect { - fill: var(--color-info-bg); - stroke: var(--color-info); -} - -svg .node.start rect, -svg .node.end rect { - fill: var(--color-primary-lighter); - stroke: var(--color-primary); -} - -svg .node.import rect { - fill: var(--color-success-bg); - stroke: var(--color-success); -} - -svg .node.exec rect { - fill: #f0f0ff; - stroke: #4b4bd8; -} - -@media (prefers-color-scheme: dark) { - svg .node.start rect, - svg .node.end rect { - fill: var(--color-primary-light); - stroke: var(--color-primary); - } - - svg .node.exec rect { - fill: #2d2a4d; - stroke: #4b4bd8; - } -} - -/* Node Highlighting (упрощённо, без тяжёлых фильтров) */ -.node-search-match rect { - stroke: #ffc107 !important; - stroke-width: 3px !important; -} - -.node-highlight rect { - stroke: #ff5722 !important; - stroke-width: 3px !important; -} - -.node-search-active rect { - stroke: #4caf50 !important; - stroke-width: 3px !important; -} - -/* Усиление видимости линий */ -svg .edgePath, -svg .edgePath path, -svg .flowchart-link { - stroke: var(--color-mermaid-edge) !important; - stroke-width: 2.4px !important; - fill: none !important; - opacity: 1 !important; - vector-effect: non-scaling-stroke !important; -} - - svg .edgePath:hover .path, - svg .edgePath:hover path { - stroke-width: 3px !important; - } - -/* --------------------------------------------------------- - TOAST NOTIFICATIONS ---------------------------------------------------------- */ -.toast { - position: fixed; - top: 80px; - right: var(--spacing-xxl); - background: var(--color-background); - color: var(--color-text-primary); - padding: var(--spacing-md) var(--spacing-lg); - border-radius: var(--border-radius-large); - box-shadow: var(--shadow-8); - border: 1px solid var(--color-border); - z-index: var(--z-index-tooltip); - display: flex; - align-items: center; - gap: var(--spacing-md); - max-width: 400px; - border-left: 4px solid var(--color-primary); -} - -/* --------------------------------------------------------- - SCROLLBAR STYLING ---------------------------------------------------------- */ -::-webkit-scrollbar { - width: 12px; - height: 12px; -} - -::-webkit-scrollbar-track { - background: var(--color-background-neutral); - border-radius: var(--border-radius-medium); -} - -::-webkit-scrollbar-thumb { - background: var(--color-border-strong); - border-radius: var(--border-radius-medium); - border: 3px solid var(--color-background-neutral); -} - - ::-webkit-scrollbar-thumb:hover { - background: var(--color-text-tertiary); - } - -::-webkit-scrollbar-corner { - background: var(--color-background-neutral); -} - -/* For Firefox */ -* { - scrollbar-width: thin; - scrollbar-color: var(--color-border-strong) var(--color-background-neutral); -} - -/* --------------------------------------------------------- - RESPONSIVE ADJUSTMENTS ---------------------------------------------------------- */ -@media (max-width: 768px) { - .severity-section { - margin: var(--spacing-lg); - padding: var(--spacing-lg); - } - - .tabs-container { - padding: var(--spacing-sm) var(--spacing-lg); - } - - .diagram-toolbar { - padding: var(--spacing-sm) var(--spacing-lg); - } - - .toolbar-actions { - flex-wrap: wrap; - justify-content: flex-end; - } - - .toolbar-button { - padding: var(--spacing-sm) var(--spacing-md); - font-size: var(--font-size-sm); - } - - #minimap { - right: var(--spacing-lg); - bottom: 100px; - width: 150px; - height: 105px; - } - - .search-results-info { - left: var(--spacing-lg); - right: var(--spacing-lg); - max-width: none; - } - - .toast { - right: var(--spacing-lg); - left: var(--spacing-lg); - max-width: none; + :root { + --color-bg: #1f1f1f; + --color-bg-alt: #2a2a2a; + --color-bg-neutral: #2d2d2d; + --color-text: #f3f3f3; + --color-text-secondary: #c8c8c8; + --color-text-tertiary: #9a9a9a; + --color-border: #3a3a3a; + --color-border-strong: #4a4a4a; + --color-primary: #3aa0f3; + --color-primary-hover: #2899f5; + --color-primary-light: #0f2b4d; + --color-critical-bg: #4c191b; + --color-warning-bg: #4c3b1a; + --color-info-bg: #0f2b4d; } .file-title-container { - padding: var(--spacing-lg); - } - - .file-title { - font-size: var(--font-size-lg); - } - - #diagramContainer { - height: calc(100vh - 102px); - } -} - -@media (max-width: 480px) { - :root { - --spacing-xxxl: 24px; - --spacing-xxl: 20px; - --spacing-xl: 16px; - --spacing-lg: 12px; - --spacing-md: 8px; - --spacing-sm: 6px; - --spacing-xs: 4px; - } - - .severity-header { - flex-direction: column; - align-items: flex-start; - gap: var(--spacing-md); - } - - .toolbar-search { - max-width: none; - } - - .toolbar-actions { - width: 100%; - justify-content: space-between; - } - - .tabs { - gap: 2px; - } - - .tab { - padding: var(--spacing-sm) var(--spacing-md); - font-size: var(--font-size-sm); - } -} - -/* --------------------------------------------------------- - PRINT STYLES ---------------------------------------------------------- */ -@media print { - .tabs-container, - .diagram-toolbar, - #minimap, - .search-results-info, - .toast { - display: none !important; - } - - body { - background-color: #ffffff; - color: #000000; - } - - .file-report { - padding-bottom: 0; - display: block !important; + background: var(--color-bg); } .severity-section { - break-inside: avoid; - box-shadow: none; - border: 1px solid #ddd; + background: var(--color-bg); + border-color: var(--color-border); } - table { - break-inside: avoid; + .grid-row { + background: var(--color-bg-alt); + border-color: var(--color-border); } + + .grid-row:hover { + background: var(--color-bg-neutral); + } + + .summary-section { + background: var(--color-bg); + border-color: var(--color-border); + } + + .file-card { + background: var(--color-bg-alt); + } + + .tabs-container { + background: var(--color-bg); + border-color: var(--color-border); + } + + .tab { + background: var(--color-bg-alt); + border-color: var(--color-border); + color: var(--color-text-secondary); + } + + .tab.active { + background: var(--color-primary-light); + border-color: var(--color-primary); + color: var(--color-primary); + } } /* --------------------------------------------------------- - ACCESSIBILITY + TAB TRANSITIONS --------------------------------------------------------- */ -@media (prefers-reduced-motion: reduce) { - * { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; +.file-report { + opacity: 0; + transform: translateY(6px); + transition: opacity 0.25s ease, transform 0.25s ease; +} + + .file-report.active { + opacity: 1; + transform: translateY(0); } + +/* --------------------------------------------------------- + TAB HOVER & ACTIVE ANIMATION +--------------------------------------------------------- */ +.tab { + transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, transform 0.15s ease; } -:focus-visible { - outline: 2px solid var(--color-primary); - outline-offset: 2px; - border-radius: var(--border-radius-small); -} + .tab:hover { + transform: translateY(-1px); + } -::selection { - background-color: var(--color-primary); - color: #ffffff; -} - -::-moz-selection { - background-color: var(--color-primary); - color: #ffffff; -} - -/* Убираем blue highlight на mobile tap */ -* { - -webkit-tap-highlight-color: transparent; -} - - -/* Summary page styles */ -.summary-section { - margin: var(--spacing-xl) var(--spacing-xxxl); - padding: var(--spacing-xl); - background-color: var(--color-background); - border-radius: var(--border-radius-large); - box-shadow: var(--shadow-4); - transition: transform var(--transition-medium), box-shadow var(--transition-medium); -} - - .summary-section:hover { + .tab.active { transform: translateY(-2px); - box-shadow: var(--shadow-8); } -.summary-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: var(--spacing-xl); - padding-bottom: var(--spacing-md); - border-bottom: 1px solid var(--color-border); -} - -.summary-title { - display: flex; - align-items: center; - gap: var(--spacing-sm); -} - -.summary-stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: var(--spacing-lg); - margin-bottom: var(--spacing-xl); -} - -.stat-card { - padding: var(--spacing-lg); - border-radius: var(--border-radius-medium); - background-color: var(--color-background-alt); - border: 1px solid var(--color-border); - transition: all var(--transition-medium); -} - - .stat-card:hover { - transform: translateY(-4px); - box-shadow: var(--shadow-8); - } - - .stat-card.critical { - border-top: 4px solid var(--color-critical); - } - - .stat-card.warning { - border-top: 4px solid var(--color-warning); - } - - .stat-card.info { - border-top: 4px solid var(--color-info); - } - - .stat-card.success { - border-top: 4px solid var(--color-success); - } - -.stat-value { - font-size: var(--font-size-xxxl); - font-weight: 700; - margin-bottom: var(--spacing-xs); - line-height: 1; -} - -.stat-label { - font-size: var(--font-size-sm); - color: var(--color-text-secondary); - margin-bottom: var(--spacing-xs); -} - -.stat-change { - font-size: var(--font-size-xs); - display: flex; - align-items: center; - gap: 2px; -} - - .stat-change.positive { - color: var(--color-success); - } - - .stat-change.negative { - color: var(--color-critical); - } - -.files-overview { - margin-top: var(--spacing-xxl); -} - -.files-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - gap: var(--spacing-lg); +.grid-row { + transition: background-color 0.15s ease; } .file-card { - padding: var(--spacing-lg); - border-radius: var(--border-radius-medium); - background-color: var(--color-background-alt); - border: 1px solid var(--color-border); - transition: all var(--transition-medium); - display: flex; - flex-direction: column; + transition: background-color 0.2s ease, transform 0.2s ease; } .file-card:hover { - transform: translateY(-4px); - box-shadow: var(--shadow-8); + transform: translateY(-2px); + background: var(--color-bg-neutral); } -.file-card-header { - display: flex; - align-items: flex-start; /* Change from center to flex-start */ - justify-content: space-between; - margin-bottom: var(--spacing-md); - gap: var(--spacing-sm); /* Add gap between name and badges */ + +/* --------------------------------------------------------- + FIX TAB HEIGHT — MAX 2 LINES +--------------------------------------------------------- */ + +/* Уменьшаем вертикальные отступы */ +.tab { + padding: 6px 10px; + min-height: unset; + height: auto; + line-height: 1.25; } - -.file-name-small { - font-weight: 600; - color: var(--color-text-primary); - /* Remove truncation properties */ - overflow: visible; - text-overflow: clip; - white-space: normal; /* Change from nowrap to normal */ - word-wrap: break-word; - word-break: break-word; - flex: 1; - line-height: 1.4; - max-height: none; /* Remove max-height if exists */ - display: block; /* Change from -webkit-box */ - -webkit-line-clamp: unset; /* Remove line clamp */ - -webkit-box-orient: unset; +/* Контейнер таба — выравниваем по верхнему краю */ +.tab-inner { + align-items: flex-start; + gap: 6px; } -.file-violations { - display: flex; - gap: var(--spacing-xs); - justify-content: flex-end; - flex-shrink: 0; - align-items: flex-start; /* Align badges to top */ -} - -.violation-badge { - font-size: var(--font-size-xs); - padding: var(--spacing-xxs) var(--spacing-xs); - border-radius: var(--border-radius-small); - font-weight: 600; -} - - .violation-badge.critical { - background-color: var(--color-critical-bg); - color: var(--color-critical); - border: 1px solid var(--color-critical); - } - - .violation-badge.warning { - background-color: var(--color-warning-bg); - color: var(--color-warning); - border: 1px solid var(--color-warning); - } - - .violation-badge.info { - background-color: var(--color-info-bg); - color: var(--color-info); - border: 1px solid var(--color-info); - } - -.progress-bar { - height: 8px; - background-color: var(--color-background-neutral); - border-radius: var(--border-radius-small); +/* Текст таба — максимум 2 строки */ +.tab-text { + font-size: 13px; + line-height: 1.25; + display: -webkit-box; + -webkit-line-clamp: 2; /* максимум 2 строки */ + -webkit-box-orient: vertical; overflow: hidden; - margin-top: var(--spacing-md); - display: flex; /* Изменено с block на flex */ + max-height: calc(1.25em * 2); /* ровно 2 строки */ + white-space: normal; } -.progress-fill { - height: 100%; - transition: width var(--transition-medium); - flex-shrink: 0; /* Предотвращает сжатие */ +/* Счётчики — компактнее */ +.tab-counters { + gap: 2px; + min-height: auto; } - .progress-fill.critical { - background-color: var(--color-critical); - order: 1; - } - - .progress-fill.warning { - background-color: var(--color-warning); - order: 2; - } - - .progress-fill.info { - background-color: var(--color-info); - order: 3; - } - -@media (max-width: 768px) { - .summary-section { - margin: var(--spacing-lg); - padding: var(--spacing-lg); - } - - .summary-stats-grid { - grid-template-columns: repeat(2, 1fr); - } - - .files-grid { - grid-template-columns: 1fr; - } +/* Сами счётчики — меньше высота */ +.tab-counter { + min-width: 14px; + height: 14px; + font-size: 10px; + padding: 0 3px; + line-height: 14px; } +/* На мобильных — ещё компактнее */ @media (max-width: 480px) { - .summary-stats-grid { - grid-template-columns: 1fr; + .tab { + padding: 4px 8px; } - .stat-value { - font-size: var(--font-size-xxl); + .tab-text { + font-size: 12px; + max-height: calc(1.2em * 2); + -webkit-line-clamp: 2; + } + + .tab-counter { + min-width: 12px; + height: 12px; + font-size: 9px; + padding: 0 2px; } } \ No newline at end of file diff --git a/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.js b/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.js index a40f2e3..978480d 100644 --- a/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.js +++ b/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatter.js @@ -1,1895 +1,472 @@ - -import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs"; +document.addEventListener("DOMContentLoaded", function () { + const dataEl = document.getElementById("report-data"); + if (!dataEl) return console.error("report-data not found"); -/* --------------------------------------------------------- - REPORT RENDERER - динамическое создание таблиц и табов ---------------------------------------------------------- */ -class ReportRenderer { - constructor(data) { - this.data = data; - this.files = data.files || data.f || []; - this.rules = data.rules || data.r || {}; - this.diagram = data.diagram || data.d || {}; - this.currentFileIndex = 0; + const data = JSON.parse(dataEl.textContent || "{}"); + const files = data.files || data.f || []; + const rules = data.rules || data.r || {}; - this.reportsContainer = document.getElementById('reports-container'); - this.tabsList = document.getElementById('tabs-list'); + const tabsList = document.getElementById("tabs-list"); + const reportsContainer = document.getElementById("reports-container"); - if (!this.reportsContainer || !this.tabsList) { - console.error('Не найдены контейнеры для отчета'); - return; - } - } - - init() { - this.renderTabs(); - this.setupTabsDragScroll(); - this.renderFileReports(); - this.setupTabNavigation(); - this.activateTab(0); - } - - renderTabs() { - const tabsList = document.getElementById('tabs-list'); - if (!tabsList) return; - - this.tabsList.innerHTML = ''; - - //Add file tabs - 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 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)} - ${countersHTML} -
- `; - - this.tabsList.appendChild(tab); - }); - - // Add Summary tab - const summaryTab = document.createElement('button'); - summaryTab.className = 'tab'; - summaryTab.dataset.target = 'summary_report'; - summaryTab.innerHTML = `
Summary
`; - tabsList.appendChild(summaryTab); - - //Add diagram tab if exists - if (this.diagram.h || this.diagram.hasDiagram) { - const diagramTab = document.createElement('button'); - diagramTab.className = 'tab'; - diagramTab.dataset.target = 'mermaid'; - // Создаем пустые счетчики для диаграммы - - diagramTab.innerHTML = ` -
- Диаграмма -
- `; - - 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; - - this.reportsContainer.innerHTML = ''; - - // Рендеринг отчетов по файлам - this.files.forEach((file, index) => { - const reportDiv = document.createElement('div'); - reportDiv.id = `file_${index}`; - reportDiv.className = 'file-report'; - reportDiv.style.display = 'none'; - - const fileName = file.n || file.fileName || ''; - const violations = file.v || file.violations || {}; - - // Заголовок файла - reportDiv.innerHTML = ` -
-
- - - - - ${this.escapeHtml(fileName)} -
-
- `; - - // Добавляем секции нарушений - if (file.v.c?.length > 0) { - reportDiv.appendChild(this.createViolationSection('critical', file.v.c)); - } - if (file.v.w?.length > 0) { - reportDiv.appendChild(this.createViolationSection('warning', file.v.w)); - } - if (file.v.i?.length > 0) { - reportDiv.appendChild(this.createViolationSection('info', file.v.i)); - } - - this.reportsContainer.appendChild(reportDiv); - }); - - // Контейнер для диаграммы (если есть) - if (this.diagram.h || this.diagram.hasDiagram) { - const diagramContent = this.diagram.c || this.diagram.Content || ''; - const diagramDiv = document.createElement('div'); - diagramDiv.id = 'mermaid'; - diagramDiv.className = 'file-report'; - diagramDiv.style.display = 'none'; - - diagramDiv.innerHTML = ` -
- -
- - -
-
-
-
-
-
- - `; - - this.reportsContainer.appendChild(diagramDiv); - } - - this.renderSummaryReport(); - } - - createViolationSection(severity, violations) { - const severityTitle = { - critical: 'Critical', - warning: 'Warning', - info: 'Info' - }[severity] || 'Unknown'; - - const section = document.createElement('div'); - section.className = `severity-section ${severity}`; - - const tableRows = violations.map(violation => { - // Получаем правило по ID - const ruleId = violation.r || violation.ruleId; - const rule = this.rules[ruleId] || { n: 'Unknown', t: 'Описание отсутствует' }; - - // Формируем текст с подстановкой параметров - let text = rule.t || ''; - const args = violation.a || violation.args || []; - - if (args.length > 0 && text.includes('{')) { - args.forEach((arg, index) => { - text = text.replace(new RegExp(`\\{${index}\\}`, 'g'), arg); - }); - } - - return ` - - - - - - - - `; - }).join(''); - - section.innerHTML = ` -
-
-

${severityTitle}

- ${violations.length} -
-
-
-
${violation.i || violation.index}${violation.l || violation.line}${violation.c || violation.column}${this.escapeHtml(rule.n || rule.name)}${this.escapeHtml(text)}
- - - - - - - - - - - ${tableRows} - -
#СтрокаКолонкаПравилоОписание
- - `; - - return section; - } - - setupTabNavigation() { - const tabs = this.tabsList.querySelectorAll('.tab'); - - tabs.forEach(tab => { - tab.addEventListener('click', (e) => { - e.preventDefault(); - - const targetId = tab.dataset.target; - const index = tab.dataset.index; - - // Обновляем активный таб - tabs.forEach(t => t.classList.remove('active')); - tab.classList.add('active'); - - // Скрываем все отчеты - document.querySelectorAll('.file-report').forEach(report => { - report.style.display = 'none'; - }); - - // Показываем выбранный отчет - const targetReport = document.getElementById(targetId); - if (targetReport) { - targetReport.style.display = 'block'; - - // Если это диаграмма, инициализируем ее - if (targetId === 'mermaid' && window.viewer) { - window.viewer.render().catch(console.error); - } - - // Прокрутка к верху - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - }); - }); - } - - activateTab(index) { - const tab = this.tabsList.querySelector(`.tab[data-index="${index}"]`); - if (tab) { - tab.click(); - } - } - - calculateTotalViolations(file) { - return (file.v.c?.length || 0) + (file.v.w?.length || 0) + (file.v.i?.length || 0); - } - - renderSummaryReport() { - const summaryDiv = document.createElement('div'); - summaryDiv.id = 'summary_report'; - summaryDiv.className = 'file-report'; - summaryDiv.style.display = 'none'; - - // Calculate total statistics - let totalCritical = 0; - let totalWarning = 0; - let totalInfo = 0; - let totalFiles = this.files.length; - - this.files.forEach(file => { - totalCritical += file.v.c?.length || 0; - totalWarning += file.v.w?.length || 0; - totalInfo += file.v.i?.length || 0; - }); - - const totalViolations = totalCritical + totalWarning + totalInfo; - - // Calculate percentages for progress bars - FIXED - let criticalPercent = totalViolations > 0 ? (totalCritical / totalViolations * 100) : 0; - let warningPercent = totalViolations > 0 ? (totalWarning / totalViolations * 100) : 0; - let infoPercent = totalViolations > 0 ? (totalInfo / totalViolations * 100) : 0; - - // Ensure sum equals 100% - const totalPercent = criticalPercent + warningPercent + infoPercent; - if (totalPercent > 0 && Math.abs(totalPercent - 100) > 0.01) { - // Adjust the largest segment to make sum = 100% - const maxVal = Math.max(criticalPercent, warningPercent, infoPercent); - if (maxVal === criticalPercent) { - criticalPercent = 100 - warningPercent - infoPercent; - } else if (maxVal === warningPercent) { - warningPercent = 100 - criticalPercent - infoPercent; - } else { - infoPercent = 100 - criticalPercent - warningPercent; - } - } - - // Format for display - const criticalPercentDisplay = criticalPercent.toFixed(1); - const warningPercentDisplay = warningPercent.toFixed(1); - const infoPercentDisplay = infoPercent.toFixed(1); - - summaryDiv.innerHTML = ` -
-
- - - - Сводный отчет -
-
-
-
-
-

Общая статистика

-
-
-
-
-
${totalFiles}
-
Всего файлов
-
-
-
${totalCritical}
-
Critical нарушений
-
-
-
${totalWarning}
-
Warning нарушений
-
-
-
${totalInfo}
-
Info нарушений
-
-
-
-
-
-
-
-
-
- Critical: ${totalCritical} (${criticalPercentDisplay}%) - Warning: ${totalWarning} (${warningPercentDisplay}%) - Info: ${totalInfo} (${infoPercentDisplay}%) -
-
-
-
-
-
-

Статистика файлов

- ${totalFiles} -
-
-
- ${this.files.map(file => { - const critical = file.v.c?.length || 0; - const warning = file.v.w?.length || 0; - const info = file.v.i?.length || 0; - const total = critical + warning + info; - - // Calculate percentages for file progress bar - FIXED - let fileCriticalPercent = total > 0 ? (critical / total * 100) : 0; - let fileWarningPercent = total > 0 ? (warning / total * 100) : 0; - let fileInfoPercent = total > 0 ? (info / total * 100) : 0; - - // Ensure sum equals 100% - const fileTotalPercent = fileCriticalPercent + fileWarningPercent + fileInfoPercent; - if (fileTotalPercent > 0 && Math.abs(fileTotalPercent - 100) > 0.01) { - // Adjust the largest segment to make sum = 100% - const maxVal = Math.max(fileCriticalPercent, fileWarningPercent, fileInfoPercent); - if (maxVal === fileCriticalPercent) { - fileCriticalPercent = 100 - fileWarningPercent - fileInfoPercent; - } else if (maxVal === fileWarningPercent) { - fileWarningPercent = 100 - fileCriticalPercent - fileInfoPercent; - } else { - fileInfoPercent = 100 - fileCriticalPercent - fileWarningPercent; - } - } - - return `
-
- ${this.escapeHtml(file.n)} -
-
-
-
-
-
-
- Total: ${total} -
- ${critical} - ${warning} - ${info} -
-
-
`; - }).join('')} -
-
- `; - - this.reportsContainer.appendChild(summaryDiv); - } - - escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } -} - -/* --------------------------------------------------------- - ГЛОБАЛЬНАЯ ИНИЦИАЛИЗАЦИЯ ---------------------------------------------------------- */ -function initReport() { - if (!window.reportData) { - console.error('Данные отчета не найдены'); + if (!tabsList || !reportsContainer) { + console.error("Missing tabs-list or reports-container"); return; } - const renderer = new ReportRenderer(window.reportData); - renderer.init(); + /* --------------------------------------------------------- + UTILS + --------------------------------------------------------- */ - // Сохраняем рендерер в глобальной области видимости - window.reportRenderer = renderer; -} - -// Экспортируем функции для глобального использования -window.initReport = initReport; - -/* --------------------------------------------------------- - TABS MANAGEMENT ---------------------------------------------------------- */ -function initTabs(onTabActivated) { - const tabs = document.querySelectorAll(".tab"); - const reports = document.querySelectorAll(".file-report"); - - tabs.forEach(tab => { - tab.addEventListener("click", () => { - const id = tab.dataset.target; - - // Add ripple effect - const ripple = document.createElement('span'); - const rect = tab.getBoundingClientRect(); - const size = Math.max(rect.width, rect.height); - const x = event.clientX - rect.left; - const y = event.clientY - rect.top; - - ripple.style.cssText = ` - position: absolute; - border-radius: 50%; - background: rgba(0, 120, 212, 0.2); - transform: scale(0); - animation: ripple 0.6s linear; - width: ${size}px; - height: ${size}px; - left: ${x - size / 2}px; - top: ${y - size / 2}px; - `; - - /* - tab.style.position = 'relative'; - tab.style.overflow = 'hidden'; - */ - tab.appendChild(ripple); - - setTimeout(() => { - if (ripple.parentElement === tab) { - ripple.remove(); - } - }, 600); - - tabs.forEach(t => t.classList.remove("active")); - reports.forEach(r => r.classList.remove("active")); - - tab.classList.add("active"); - const report = document.getElementById(id); - if (report) { - report.classList.add("active"); - - // Scroll to top when switching tabs - window.scrollTo({ top: 0, behavior: 'smooth' }); - } - - if (onTabActivated) onTabActivated(id); - }); - }); -} - -// Исправление для табов на странице диаграммы -document.addEventListener('DOMContentLoaded', function () { - const tabsContainer = document.querySelector('.tabs-container'); - if (tabsContainer) { - tabsContainer.style.zIndex = '1001'; - tabsContainer.style.position = 'fixed'; - tabsContainer.style.bottom = '0'; - tabsContainer.style.left = '0'; - tabsContainer.style.right = '0'; + function escapeHtml(str) { + if (str == null) return ""; + return String(str) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); } - // Гарантируем, что табы всегда поверх всего - const tabs = document.querySelectorAll('.tab'); - tabs.forEach(tab => { - tab.style.zIndex = '1002'; - }); -}); - -/* --------------------------------------------------------- - DIAGRAM VIEWER CLASS ---------------------------------------------------------- */ -class DiagramViewer { - constructor() { - this.container = document.getElementById("diagramSvgContainer"); - this.minimap = document.getElementById("minimap"); - this.searchInput = document.getElementById("diagramSearch"); - this.resetBtn = document.getElementById("resetViewBtn"); - - this.svg = null; - this.viewportGroup = null; - this.nodes = []; - - this.scale = 1; - this.tx = 0; - this.ty = 0; - this.minScale = 0.3; - this.maxScale = 12; - - this.originalViewBox = null; - this.minimapSvg = null; - this.minimapViewport = null; - - this.needsRender = false; - this.isPanning = false; - this.lastSearchTerm = ''; - this.searchResults = []; - this.currentSearchIndex = -1; - - this.init(); - } - - init() { - // Add CSS for ripple animation - if (!document.querySelector('#ripple-styles')) { - const style = document.createElement('style'); - style.id = 'ripple-styles'; - style.textContent = ` - @keyframes ripple { - to { - transform: scale(4); - opacity: 0; - } - } - - @keyframes fadeIn { - from {opacity: 0; transform: translateY(-10px); } - to {opacity: 1; transform: translateY(0); } - } - `; - document.head.appendChild(style); - } - } - - async render() { - this.container.classList.add('loading'); - - - try { - - const mermaidSource = document.getElementById("mermaidSource").textContent.trim(); - const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; - - mermaid.initialize({ - startOnLoad: false, - theme: isDark ? "dark" : "default", - themeVariables: isDark ? { - fontFamily: "'Segoe UI', system-ui, -apple-system, sans-serif", - primaryColor: "#2d2d2d", - primaryBorderColor: "#2899f5", - primaryTextColor: "#f3f2f1", - lineColor: "#797775", - lineWidth: 2.5, - secondaryColor: "#3c3c3c", - tertiaryColor: "#484644", - background: "#1e1e1e", - clusterBkg: "#201f1e", - clusterBorder: "#605e5c", - edgeLabelBackground: "#252423", - nodeBkg: "#2d2d2d", - nodeBorder: "#2899f5", - nodeTextColor: "#f3f2f1", - mainBkg: "#1e1e1e", - textColor: "#f3f2f1", - arrowheadColor: "#797775", - fontSize: "14px" - } : { - fontFamily: "'Segoe UI', system-ui, -apple-system, sans-serif", - primaryColor: "#f0f8ff", - primaryBorderColor: "#0078d4", - primaryTextColor: "#323130", - lineColor: "#8a8886", - lineWidth: 2.5, - secondaryColor: "#deecf9", - tertiaryColor: "#c7e0f4", - background: "#ffffff", - clusterBkg: "#f3f2f1", - clusterBorder: "#a19f9d", - edgeLabelBackground: "#ffffff", - nodeBkg: "#f0f8ff", - nodeBorder: "#0078d4", - nodeTextColor: "#323130", - mainBkg: "#ffffff", - textColor: "#323130", - arrowheadColor: "#8a8886", - fontSize: "14px" - }, - flowchart: { - useMaxWidth: false, - htmlLabels: true, - curve: "basis" - } - }); - - try { - const { svg } = await mermaid.render("diagram", mermaidSource); - - // Убираем только встроенные стили Mermaid, оставляя структуру - const cleanedSvg = svg.replace(/]*>[\s\S]*?<\/style>/gi, ""); - - this.container.innerHTML = cleanedSvg; - this.svg = this.container.querySelector("svg"); - - if (this.svg) { - // Добавляем небольшую задержку для гарантированной отрисовки - await new Promise(resolve => setTimeout(resolve, 50)); - - // Добавляем классы для разных типов узлов - this._addNodeClasses(); - this._afterSvgLoaded(); - this._initSearch(); - this._attachEventListeners(); - this._setupResizeObserver(); - this.container.classList.remove('loading'); - } - } catch (error) { - console.error('Mermaid rendering error:', error); - this._showError(error); - } - } catch (error) { - this.container.classList.remove('loading'); - this._showError(error); - } - } - - recalculateBounds() { - this._calculateRealBounds(); - this._updateMinimapViewport(); - } - - _showError(error) { - this.container.innerHTML = ` -
-
-
📊
-

- Не удалось загрузить диаграмму -

-

- ${error.message} -

- -
-
- `; - } - - _addNodeClasses() { - // Добавляем классы к узлам на основе текста - const nodes = this.svg.querySelectorAll('.node'); - nodes.forEach(node => { - const textElement = node.querySelector('text'); - if (textElement) { - const text = textElement.textContent.toLowerCase(); - - if (text.includes('if') || text.includes('condition') || text.includes('условие')) { - node.classList.add('condition'); - } else if (text.includes('start') || text.includes('end') || - text.includes('начало') || text.includes('конец')) { - node.classList.add('start'); - } else if (text.includes('exec') || text.includes('execute') || - text.includes('выполнить') || text.includes('выполнение')) { - node.classList.add('exec'); - } else if (text.includes('select') || text.includes('insert') || - text.includes('update') || text.includes('delete') || - text.includes('выбрать') || text.includes('вставить') || - text.includes('обновить') || text.includes('удалить')) { - node.classList.add('process'); - } else if (text.includes('import') || text.includes('export') || - text.includes('импорт') || text.includes('экспорт')) { - node.classList.add('import'); - } - } - }); - } - - _setupResizeObserver() { - const resizeObserver = new ResizeObserver(() => { - if (this.svg) { - this._updateMinimapViewport(); - } - }); - - resizeObserver.observe(this.container); - } - - _attachEventListeners() { - // Add double click to reset zoom - this.svg.addEventListener('dblclick', (e) => { - e.preventDefault(); - this.resetView(); - }); - - // Add keyboard shortcuts - document.addEventListener('keydown', (e) => { - if (e.target === this.searchInput || e.target.matches('input, textarea')) return; - - switch (e.key) { - case '+': - case '=': - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - this.zoomIn(); - } - break; - case '-': - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - this.zoomOut(); - } - break; - case '0': - if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - this.resetView(); - } - break; - case 'Escape': - this.clearSearch(); - break; - case 'F3': - case 'Enter': - if (e.shiftKey) { - e.preventDefault(); - this.prevSearchResult(); - } else if (e.ctrlKey || e.metaKey) { - e.preventDefault(); - this.nextSearchResult(); - } - break; - } - }); - - // Reset button - if (this.resetBtn) { - this.resetBtn.addEventListener('click', () => { - this.resetView(); - this._showToast('Вид диаграммы сброшен', 'info'); - }); - } - } - - _initSearch() { - if (this.searchInput) { - // Clear button - const clearBtn = document.createElement('button'); - clearBtn.innerHTML = '✕'; - clearBtn.title = 'Очистить поиск'; - clearBtn.style.cssText = ` - position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); - background: none; - border: none; - color: var(--text-secondary); - cursor: pointer; - padding: 4px; - font-size: 12px; - opacity: 0.7; - transition: opacity 0.2s; - border-radius: 50%; - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - `; - - clearBtn.addEventListener('mouseover', () => { - clearBtn.style.opacity = '1'; - clearBtn.style.backgroundColor = 'var(--surface-neutral)'; - }); - - clearBtn.addEventListener('mouseout', () => { - clearBtn.style.opacity = '0.7'; - clearBtn.style.backgroundColor = 'transparent'; - }); - - clearBtn.addEventListener('click', (e) => { - e.stopPropagation(); - this.clearSearch(); - this.searchInput.value = ''; - this.searchInput.focus(); - this._showToast('Поиск очищен', 'info'); - }); - - const searchContainer = this.searchInput.parentElement; - searchContainer.style.position = 'relative'; - searchContainer.appendChild(clearBtn); - - this.searchInput.addEventListener('keyup', (e) => { - if (e.key === 'Enter') { - const term = this.searchInput.value.trim(); - if (term) { - this.searchNodes(term); - } else { - this.clearSearch(); - } - } else if (e.key === 'Escape') { - this.clearSearch(); - this.searchInput.value = ''; - } - }); - - // Show clear button only when there's text - this.searchInput.addEventListener('input', () => { - clearBtn.style.display = this.searchInput.value ? 'flex' : 'none'; - }); - - clearBtn.style.display = 'none'; - } - } - - searchNodes(term) { - if (!this.svg || !term) { - this.clearSearch(); - return; - } - - // Clear previous highlights - this.clearSearch(); - - this.lastSearchTerm = term.toLowerCase(); - const nodes = this.svg.querySelectorAll('.node'); - this.searchResults = []; - - nodes.forEach((node) => { - const textElement = node.querySelector('text'); - if (textElement && textElement.textContent.toLowerCase().includes(this.lastSearchTerm)) { - this.searchResults.push(node); - } - }); - - if (this.searchResults.length > 0) { - this.currentSearchIndex = 0; - this._highlightCurrentSearchResult(); - this._showSearchResultsInfo(); - - // Center view on first result - this._centerOnNode(this.searchResults[0]); - } else { - this._showToast('Ничего не найдено', 'warning'); - } - } - - _highlightCurrentSearchResult() { - // Clear all highlights first - this.searchResults.forEach((node, index) => { - node.classList.remove('node-search-match', 'node-highlight', 'node-search-active'); - - if (index === this.currentSearchIndex) { - node.classList.add('node-search-active', 'node-highlight'); - } else { - node.classList.add('node-search-match'); - } - }); - } - - nextSearchResult() { - if (this.searchResults.length === 0) return; - - this.currentSearchIndex = (this.currentSearchIndex + 1) % this.searchResults.length; - this._highlightCurrentSearchResult(); - this._centerOnNode(this.searchResults[this.currentSearchIndex]); - this._updateSearchResultsInfo(); - } - - prevSearchResult() { - if (this.searchResults.length === 0) return; - - this.currentSearchIndex = (this.currentSearchIndex - 1 + this.searchResults.length) % this.searchResults.length; - this._highlightCurrentSearchResult(); - this._centerOnNode(this.searchResults[this.currentSearchIndex]); - this._updateSearchResultsInfo(); - } - - _centerOnNode(node) { - if (!node || !this.svg) return; - - const bbox = node.getBBox(); - const container = document.getElementById("diagramContainer"); - - // Calculate center of node in SVG coordinates - const centerX = bbox.x + bbox.width / 2; - const centerY = bbox.y + bbox.height / 2; - - // Calculate transform to center this point - this.tx = container.clientWidth / 2 - centerX * this.scale; - this.ty = container.clientHeight / 2 - centerY * this.scale; - - this._scheduleRender(); - - // Add a little bounce effect - node.style.transform = 'scale(1.05)'; - setTimeout(() => { - node.style.transform = ''; - }, 300); - } - - _showSearchResultsInfo() { - // Remove existing info if any - this._removeSearchResultsInfo(); - - const info = document.createElement('div'); - info.className = 'search-results-info'; - info.style.cssText = ` - position: absolute; - top: 60px; - left: 16px; - background: var(--surface-default); - padding: var(--spacing-s) var(--spacing-m); - border-radius: var(--border-radius-medium); - box-shadow: var(--shadow-16); - border: 1px solid var(--border-default); - font-size: 13px; - z-index: 1000; - animation: fadeIn 0.3s ease; - display: flex; - align-items: center; - gap: var(--spacing-m); - `; - - info.innerHTML = ` - - Найдено: ${this.searchResults.length} узлов - - ${this.searchResults.length > 1 ? ` -
- - -
- - ${this.currentSearchIndex + 1} из ${this.searchResults.length} - - ` : ''} - - `; - - this.container.appendChild(info); - } - - _updateSearchResultsInfo() { - const info = this.container.querySelector('.search-results-info'); - if (info && this.searchResults.length > 1) { - const counter = info.querySelector('span:last-of-type'); - if (counter) { - counter.textContent = `${this.currentSearchIndex + 1} из ${this.searchResults.length}`; - } - } - } - - _removeSearchResultsInfo() { - const info = this.container.querySelector('.search-results-info'); - if (info) { - info.remove(); - } - } - - clearSearch() { - if (this.svg) { - const nodes = this.svg.querySelectorAll('.node'); - nodes.forEach(node => { - node.classList.remove('node-search-match', 'node-highlight', 'node-search-active'); - node.style.transform = ''; - }); - } - - this.searchResults = []; - this.currentSearchIndex = -1; - this._removeSearchResultsInfo(); - } - - _showToast(message, type = 'info') { - const toast = document.createElement('div'); - toast.textContent = message; - toast.style.cssText = ` - position: fixed; - top: 80px; - right: 20px; - background: var(--surface-default); - color: var(--text-primary); - padding: var(--spacing-m) var(--spacing-l); - border-radius: var(--border-radius-medium); - box-shadow: var(--shadow-16); - border: 1px solid var(--border-default); - z-index: 9999; - animation: fadeIn 0.3s ease; - font-size: 14px; - border-left: 4px solid ${type === 'error' ? '#d13438' : - type === 'warning' ? '#ffaa44' : - type === 'success' ? '#107c10' : - 'var(--primary-color)' - }; - `; - - document.body.appendChild(toast); - - setTimeout(() => { - toast.style.opacity = '0'; - toast.style.transform = 'translateY(-10px)'; - setTimeout(() => { - if (toast.parentElement) { - toast.remove(); - } - }, 300); - }, 3000); - } - - zoomIn() { - const zoomFactor = 1.2; - const newScale = Math.min(this.maxScale, this.scale * zoomFactor); - this.scale = newScale; - this._scheduleRender(); - this._showToast(`Масштаб: ${Math.round(this.scale * 100)}%`, 'info'); - } - - zoomOut() { - const zoomFactor = 0.8; - const newScale = Math.max(this.minScale, this.scale * zoomFactor); - this.scale = newScale; - this._scheduleRender(); - this._showToast(`Масштаб: ${Math.round(this.scale * 100)}%`, 'info'); - } - - _afterSvgLoaded() { - const vb = this.svg.viewBox.baseVal; - this.originalViewBox = { - x: vb.x, - y: vb.y, - width: vb.width, - height: vb.height - }; - - this._wrapContent(); - this._initZoomPan(); - - // Используем requestAnimationFrame для получения реальных границ после отрисовки - requestAnimationFrame(() => { - this._calculateRealBounds(); - this._initMinimap(); - this.resetView(); - this._scheduleRender(); - }); - } - - _calculateRealBounds() { - if (!this.viewportGroup) return; - - try { - // Пробуем получить реальные границы содержимого - let bbox; - - // Метод 1: Пробуем получить bbox всей группы - try { - bbox = this.viewportGroup.getBBox(); - } catch (e) { - bbox = null; - } - - // Метод 2: Если getBBox не работает или возвращает 0, используем viewBox - if (!bbox || bbox.width === 0 || bbox.height === 0 || - bbox.x === Infinity || bbox.y === Infinity) { - console.warn("getBBox failed, using originalViewBox"); - this.diagramBounds = { ...this.originalViewBox }; - return; - } - - // Метод 3: Если getBBox работает, но границы слишком малы, проверяем ручной расчет - if (bbox.width < 10 || bbox.height < 10) { - // Пробуем ручной расчет через содержимое - const elements = this.viewportGroup.querySelectorAll('*[x], *[y], *[cx], *[cy]'); - let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; - let hasValidCoords = false; - - elements.forEach(el => { - let x = parseFloat(el.getAttribute('x') || el.getAttribute('cx') || '0'); - let y = parseFloat(el.getAttribute('y') || el.getAttribute('cy') || '0'); - let width = parseFloat(el.getAttribute('width') || '0'); - let height = parseFloat(el.getAttribute('height') || '0'); - let r = parseFloat(el.getAttribute('r') || '0'); - - if (!isNaN(x) && !isNaN(y)) { - if (el.tagName === 'circle') { - x = x - r; - y = y - r; - width = height = r * 2; - } else if (el.tagName === 'ellipse') { - const rx = parseFloat(el.getAttribute('rx') || '0'); - const ry = parseFloat(el.getAttribute('ry') || '0'); - x = x - rx; - y = y - ry; - width = rx * 2; - height = ry * 2; - } - - minX = Math.min(minX, x); - minY = Math.min(minY, y); - maxX = Math.max(maxX, x + width); - maxY = Math.max(maxY, y + height); - hasValidCoords = true; - } - }); - - if (hasValidCoords && minX !== Infinity) { - bbox = { - x: minX, - y: minY, - width: maxX - minX, - height: maxY - minY - }; - } - } - - // Добавляем отступ в 5% от размера диаграммы - const padding = Math.max(bbox.width, bbox.height) * 0.05; - this.diagramBounds = { - x: bbox.x - padding, - y: bbox.y - padding, - width: bbox.width + padding * 2, - height: bbox.height + padding * 2 - }; - - console.log("Diagram bounds calculated:", this.diagramBounds); - - } catch (error) { - console.error("Error calculating diagram bounds:", error); - this.diagramBounds = { ...this.originalViewBox }; - } - } - - _wrapContent() { - const children = Array.from(this.svg.children); - this.viewportGroup = document.createElementNS("http://www.w3.org/2000/svg", "g"); - this.viewportGroup.setAttribute("id", "viewport-group"); - - children.forEach(child => { - if (child.tagName.toLowerCase() !== "defs") { - this.viewportGroup.appendChild(child.cloneNode(true)); - child.remove(); - } - }); - - this.svg.appendChild(this.viewportGroup); - } - - _initZoomPan() { - let lastX = 0; - let lastY = 0; - let isMouseDown = false; - let lastTouchDistance = 0; - - // Перевод dx/dy из экранных пикселей в SVG-координаты - const screenDeltaToSvgDelta = (dx, dy) => { - const pt1 = this.svg.createSVGPoint(); - const pt2 = this.svg.createSVGPoint(); - pt1.x = 0; - pt1.y = 0; - pt2.x = dx; - pt2.y = dy; - - const ctm = this.svg.getScreenCTM(); - if (!ctm) return { dxSvg: dx, dySvg: dy }; - - const inv = ctm.inverse(); - const w1 = pt1.matrixTransform(inv); - const w2 = pt2.matrixTransform(inv); - - return { - dxSvg: w2.x - w1.x, - dySvg: w2.y - w1.y - }; - }; - - /* --------------------------------------------------------- - PAN (mouse) - --------------------------------------------------------- */ - this.svg.addEventListener("mousedown", (e) => { - if (e.button !== 0) return; - e.preventDefault(); - - isMouseDown = true; - this.isPanning = true; - - lastX = e.clientX; - lastY = e.clientY; - - this.svg.style.cursor = "grabbing"; - }); - - window.addEventListener("mousemove", (e) => { - if (!isMouseDown) return; - - const dxScreen = e.clientX - lastX; - const dyScreen = e.clientY - lastY; - - const { dxSvg, dySvg } = screenDeltaToSvgDelta(dxScreen, dyScreen); - - this.tx += dxSvg; - this.ty += dySvg; - - lastX = e.clientX; - lastY = e.clientY; - - this._scheduleRender(); - }); - - window.addEventListener("mouseup", () => { - isMouseDown = false; - this.isPanning = false; - this.svg.style.cursor = "grab"; - }); - - /* --------------------------------------------------------- - PAN + PINCH (touch) - --------------------------------------------------------- */ - this.svg.addEventListener("touchstart", (e) => { - if (e.touches.length === 1) { - const t = e.touches[0]; - lastX = t.clientX; - lastY = t.clientY; - this.isPanning = true; - } else if (e.touches.length === 2) { - this.isPanning = false; - lastTouchDistance = this._getTouchDistance(e); - } - }, { passive: false }); - - this.svg.addEventListener("touchmove", (e) => { - if (e.touches.length === 1 && this.isPanning) { - e.preventDefault(); - const t = e.touches[0]; - - const dxScreen = t.clientX - lastX; - const dyScreen = t.clientY - lastY; - - const { dxSvg, dySvg } = screenDeltaToSvgDelta(dxScreen, dyScreen); - - this.tx += dxSvg; - this.ty += dySvg; - - lastX = t.clientX; - lastY = t.clientY; - - this._scheduleRender(); - } else if (e.touches.length === 2) { - e.preventDefault(); - - const newDist = this._getTouchDistance(e); - const zoomFactor = newDist / lastTouchDistance; - lastTouchDistance = newDist; - - const rect = this.svg.getBoundingClientRect(); - const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2 - rect.left; - const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2 - rect.top; - - const worldX = (midX - this.tx) / this.scale; - const worldY = (midY - this.ty) / this.scale; - - const newScale = Math.min(this.maxScale, Math.max(this.minScale, this.scale * zoomFactor)); - - this.tx = midX - worldX * newScale; - this.ty = midY - worldY * newScale; - this.scale = newScale; - - this._scheduleRender(); - } - }, { passive: false }); - - this.svg.addEventListener("touchend", () => { - this.isPanning = false; - }); - - /* --------------------------------------------------------- - ZOOM (wheel) — zoom-to-cursor - --------------------------------------------------------- */ - this.svg.addEventListener("wheel", (e) => { - e.preventDefault(); - - const rect = this.svg.getBoundingClientRect(); - const mouseX = e.clientX - rect.left; - const mouseY = e.clientY - rect.top; - - const zoomFactor = e.deltaY < 0 ? 1.1 : 0.9; - const newScale = Math.min(this.maxScale, Math.max(this.minScale, this.scale * zoomFactor)); - - const worldX = (mouseX - this.tx) / this.scale; - const worldY = (mouseY - this.ty) / this.scale; - - this.tx = mouseX - worldX * newScale; - this.ty = mouseY - worldY * newScale; - this.scale = newScale; - - this._scheduleRender(); - }, { passive: false }); - } - - _getTouchDistance(e) { - const dx = e.touches[0].clientX - e.touches[1].clientX; - const dy = e.touches[0].clientY - e.touches[1].clientY; - return Math.sqrt(dx * dx + dy * dy); - } - - _getWorldRect() { - const container = document.getElementById("diagramContainer"); - const ctm = this.viewportGroup.getCTM(); - if (!ctm) return { x: 0, y: 0, width: 100, height: 100 }; - - const inv = ctm.inverse(); - - const p1 = this.svg.createSVGPoint(); - p1.x = 0; - p1.y = 0; - const tl = p1.matrixTransform(inv); - - const p2 = this.svg.createSVGPoint(); - p2.x = container.clientWidth; - p2.y = container.clientHeight; - const br = p2.matrixTransform(inv); - + function countViolations(v) { return { - x: tl.x, - y: tl.y, - width: br.x - tl.x, - height: br.y - tl.y + c: v?.c?.length || 0, + w: v?.w?.length || 0, + i: v?.i?.length || 0 }; } - _applyTransform() { - if (!this.viewportGroup) return; - - this.viewportGroup.setAttribute( - "transform", - `translate(${this.tx}, ${this.ty}) scale(${this.scale})` - ); - + const rendered = new Set(); // ленивый рендер - // ОБЯЗАТЕЛЬНО обновляем миникарту после трансформации - this._updateMinimapViewport(); - } + /* --------------------------------------------------------- + RENDER TABS + --------------------------------------------------------- */ + function renderTabs() { + const frag = document.createDocumentFragment(); - _scheduleRender() { - if (this.needsRender) return; - this.needsRender = true; + files.forEach((file, index) => { + const v = countViolations(file.v); - requestAnimationFrame(() => { - this.needsRender = false; - this._applyTransform(); + const btn = document.createElement("button"); + btn.className = "tab"; + btn.dataset.target = `file_${index}`; + btn.dataset.index = index; + + const inner = document.createElement("div"); + inner.className = "tab-inner"; + + const text = document.createElement("span"); + text.className = "tab-text"; + text.title = file.n; + text.textContent = file.n; + + const counters = document.createElement("div"); + counters.className = "tab-counters"; + + const c1 = document.createElement("span"); + c1.className = "tab-counter critical" + (v.c ? "" : " empty"); + c1.textContent = v.c || ""; + + const c2 = document.createElement("span"); + c2.className = "tab-counter warning" + (v.w ? "" : " empty"); + c2.textContent = v.w || ""; + + const c3 = document.createElement("span"); + c3.className = "tab-counter info" + (v.i ? "" : " empty"); + c3.textContent = v.i || ""; + + counters.append(c1, c2, c3); + inner.append(text, counters); + btn.append(inner); + frag.append(btn); }); + + // Summary tab + const summaryBtn = document.createElement("button"); + summaryBtn.className = "tab"; + summaryBtn.dataset.target = "summary_report"; + + const inner = document.createElement("div"); + inner.className = "tab-inner"; + + const text = document.createElement("span"); + text.className = "tab-text"; + text.textContent = "Summary"; + + inner.append(text); + summaryBtn.append(inner); + frag.append(summaryBtn); + + tabsList.innerHTML = ""; + tabsList.append(frag); } - _initMinimap() { - if (!this.minimap || !this.svg) return; + /* --------------------------------------------------------- + GRID ROW CREATOR (VIOLATIONS) + --------------------------------------------------------- */ - this.minimap.innerHTML = ""; + function createGridRow(v, rule) { + const row = document.createElement("div"); + row.className = "grid-row"; - // Клонируем SVG для миникарты - this.minimapSvg = this.svg.cloneNode(true); + const cIndex = document.createElement("div"); + cIndex.textContent = v.i; - // Убираем трансформации с клонированных элементов - const cloneViewport = this.minimapSvg.querySelector("#viewport-group"); - if (cloneViewport) { - cloneViewport.removeAttribute("transform"); + const cLine = document.createElement("div"); + cLine.textContent = v.l; + + const cCol = document.createElement("div"); + cCol.textContent = v.c; + + const cRule = document.createElement("div"); + cRule.textContent = rule.n; + + let text = rule.t || ""; + const args = v.a || v.args; + if (Array.isArray(args)) { + for (let i = 0; i < args.length; i++) { + text = text.replace(`{${i}}`, args[i]); + } } - // Используем diagramBounds для viewBox миникарты - const diagram = this.diagramBounds || this.originalViewBox; + const cDesc = document.createElement("div"); + cDesc.textContent = text; - // Устанавливаем viewBox для миникарты - this.minimapSvg.setAttribute( - "viewBox", - `${diagram.x} ${diagram.y} ${diagram.width} ${diagram.height}` + row.append(cIndex, cLine, cCol, cRule, cDesc); + return row; + } + + /* --------------------------------------------------------- + FILE REPORT + --------------------------------------------------------- */ + + function renderFileReport(index) { + const file = files[index]; + const v = file.v || {}; + + const root = document.createElement("div"); + + // Title + const titleWrap = document.createElement("div"); + titleWrap.className = "file-title-container"; + + const title = document.createElement("div"); + title.className = "file-title"; + + const name = document.createElement("span"); + name.className = "file-name"; + name.textContent = file.n; + + title.append(name); + titleWrap.append(title); + root.append(titleWrap); + + // Sections + function addSection(label, list, cls) { + if (!Array.isArray(list) || list.length === 0) return; + + const section = document.createElement("div"); + section.className = `severity-section ${cls}`; + + const header = document.createElement("div"); + header.className = "severity-header"; + + const hTitle = document.createElement("div"); + hTitle.className = "severity-title"; + + const h2 = document.createElement("h2"); + h2.textContent = label; + + const count = document.createElement("span"); + count.className = "severity-count"; + count.textContent = list.length; + + hTitle.append(h2, count); + header.append(hTitle); + section.append(header); + + const grid = document.createElement("div"); + grid.className = "grid-table"; + + // header row + const headerRow = document.createElement("div"); + headerRow.className = "grid-header"; + + ["#", "Строка", "Колонка", "Правило", "Описание"].forEach(t => { + const cell = document.createElement("div"); + cell.textContent = t; + headerRow.append(cell); + }); + + grid.append(headerRow); + + // rows + for (let i = 0; i < list.length; i++) { + const vItem = list[i]; + const rule = rules[vItem.r] || { n: "Unknown", t: "Описание отсутствует" }; + grid.append(createGridRow(vItem, rule)); + } + + section.append(grid); + root.append(section); + } + + addSection("Critical", v.c, "critical"); + addSection("Warning", v.w, "warning"); + addSection("Info", v.i, "info"); + + return root; + } + + /* --------------------------------------------------------- + SUMMARY + --------------------------------------------------------- */ + + function renderSummary() { + let totalC = 0, totalW = 0, totalI = 0; + + for (let i = 0; i < files.length; i++) { + const v = countViolations(files[i].v); + totalC += v.c; + totalW += v.w; + totalI += v.i; + } + + const total = totalC + totalW + totalI || 1; + + const root = document.createElement("div"); + + // Title + const titleWrap = document.createElement("div"); + titleWrap.className = "file-title-container"; + + const title = document.createElement("div"); + title.className = "file-title"; + + const name = document.createElement("span"); + name.className = "file-name"; + name.textContent = "Сводный отчет"; + + title.append(name); + titleWrap.append(title); + root.append(titleWrap); + + // Stats + const section = document.createElement("div"); + section.className = "summary-section"; + + const header = document.createElement("div"); + header.className = "summary-header"; + + const hTitle = document.createElement("div"); + hTitle.className = "summary-title"; + + const h2 = document.createElement("h2"); + h2.textContent = "Общая статистика"; + + hTitle.append(h2); + header.append(hTitle); + section.append(header); + + const grid = document.createElement("div"); + grid.className = "summary-stats-grid"; + + function statCard(cls, value, label) { + const card = document.createElement("div"); + card.className = `stat-card ${cls}`; + + const v = document.createElement("div"); + v.className = "stat-value"; + v.textContent = value; + + const l = document.createElement("div"); + l.className = "stat-label"; + l.textContent = label; + + card.append(v, l); + return card; + } + + grid.append( + statCard("success", files.length, "Всего файлов"), + statCard("critical", totalC, "Critical нарушений"), + statCard("warning", totalW, "Warning нарушений"), + statCard("info", totalI, "Info нарушений") ); - this.minimapSvg.setAttribute("preserveAspectRatio", "xMidYMid meet"); - this.minimapSvg.style.width = "100%"; - this.minimapSvg.style.height = "100%"; - this.minimapSvg.style.position = "absolute"; - this.minimapSvg.style.opacity = "0.7"; + section.append(grid); - // Убираем интерактивность - this.minimapSvg.querySelectorAll('*').forEach(el => { - el.style.pointerEvents = 'none'; - }); + // Progress + const dist = document.createElement("div"); + dist.className = "violation-distribution"; - this.minimap.appendChild(this.minimapSvg); + const bar = document.createElement("div"); + bar.className = "progress-bar"; - // Создаем viewport элемент - this.minimapViewport = document.createElement("div"); - this.minimapViewport.className = "minimap-viewport"; - this.minimapViewport.style.cssText = ` - position: absolute; - border: 2px solid var(--color-primary); - background-color: rgba(0, 120, 212, 0.15); - pointer-events: none; - transition: all 0.1s ease; - box-sizing: border-box; - z-index: 10; - `; + const pc = (totalC / total) * 100; + const pw = (totalW / total) * 100; + const pi = (totalI / total) * 100; - this.minimap.appendChild(this.minimapViewport); + const f1 = document.createElement("div"); + f1.className = "progress-fill critical"; + f1.style.width = pc + "%"; - // Обработчик клика по миникарте - this.minimap.addEventListener("click", (e) => { - if (!this.minimapSvg) return; + const f2 = document.createElement("div"); + f2.className = "progress-fill warning"; + f2.style.width = pw + "%"; - const rect = this.minimap.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; + const f3 = document.createElement("div"); + f3.className = "progress-fill info"; + f3.style.width = pi + "%"; - // Процентное положение в миникарте - const percentX = x / rect.width; - const percentY = y / rect.height; + bar.append(f1, f2, f3); + dist.append(bar); - const diagram = this.diagramBounds || this.originalViewBox; + section.append(dist); + root.append(section); - // Мировые координаты в диаграмме - const worldX = diagram.x + diagram.width * percentX; - const worldY = diagram.y + diagram.height * percentY; + // Files overview + const section2 = document.createElement("div"); + section2.className = "summary-section files-overview"; - const container = document.getElementById("diagramContainer"); - if (!container) return; + const header2 = document.createElement("div"); + header2.className = "summary-header"; - // Центрируем view на точке клика - this.tx = container.clientWidth / 2 - worldX * this.scale; - this.ty = container.clientHeight / 2 - worldY * this.scale; + const hTitle2 = document.createElement("div"); + hTitle2.className = "summary-title"; - this._scheduleRender(); - }); - } + const h22 = document.createElement("h2"); + h22.textContent = "Статистика файлов"; - _updateMinimapViewport() { - if (!this.minimapViewport || !this.minimapSvg || !this.minimap || !this.svg) { - return; - } + const count = document.createElement("span"); + count.className = "severity-count"; + count.textContent = files.length; - try { - // Получаем размеры миникарты - const minimapRect = this.minimap.getBoundingClientRect(); - if (minimapRect.width === 0 || minimapRect.height === 0) return; + hTitle2.append(h22, count); + header2.append(hTitle2); + section2.append(header2); - // Получаем текущую трансформацию viewportGroup - const ctm = this.viewportGroup ? this.viewportGroup.getCTM() : null; - if (!ctm) return; + const filesGrid = document.createElement("div"); + filesGrid.className = "files-grid"; - // Получаем размеры контейнера диаграммы - const container = document.getElementById("diagramContainer"); - if (!container) return; - const containerRect = container.getBoundingClientRect(); + for (let i = 0; i < files.length; i++) { + const f = files[i]; + const v = countViolations(f.v); + const t = v.c + v.w + v.i || 1; - // Рассчитываем видимую область в мировых координатах - const inv = ctm.inverse(); + const card = document.createElement("div"); + card.className = "file-card"; - // Левый верхний угол контейнера (0,0 в координатах SVG viewport) - const topLeftScreen = this.svg.createSVGPoint(); - topLeftScreen.x = 0; - topLeftScreen.y = 0; - const topLeftWorld = topLeftScreen.matrixTransform(inv); + const header = document.createElement("div"); + header.className = "file-card-header"; - // Правый нижний угол контейнера - const bottomRightScreen = this.svg.createSVGPoint(); - bottomRightScreen.x = containerRect.width; - bottomRightScreen.y = containerRect.height; - const bottomRightWorld = bottomRightScreen.matrixTransform(inv); + const name = document.createElement("span"); + name.className = "file-name-small"; + name.textContent = f.n; - // Определяем границы видимой области - const viewportWorld = { - left: Math.min(topLeftWorld.x, bottomRightWorld.x), - top: Math.min(topLeftWorld.y, bottomRightWorld.y), - right: Math.max(topLeftWorld.x, bottomRightWorld.x), - bottom: Math.max(topLeftWorld.y, bottomRightWorld.y) - }; + header.append(name); + card.append(header); - const viewportWidth = viewportWorld.right - viewportWorld.left; - const viewportHeight = viewportWorld.bottom - viewportWorld.top; + const bar = document.createElement("div"); + bar.className = "progress-bar small"; - // Получаем границы всей диаграммы (из diagramBounds) - const diagram = this.diagramBounds || this.originalViewBox; + const fc = document.createElement("div"); + fc.className = "progress-fill critical"; + fc.style.width = (v.c / t * 100) + "%"; - // Рассчитываем масштаб для миникарты - const scaleX = minimapRect.width / diagram.width; - const scaleY = minimapRect.height / diagram.height; + const fw = document.createElement("div"); + fw.className = "progress-fill warning"; + fw.style.width = (v.w / t * 100) + "%"; - // Преобразуем мировые координаты в координаты миникарты - const viewportLeft = (viewportWorld.left - diagram.x) * scaleX; - const viewportTop = (viewportWorld.top - diagram.y) * scaleY; - const viewportWidthPx = viewportWidth * scaleX; - const viewportHeightPx = viewportHeight * scaleY; + const fi = document.createElement("div"); + fi.className = "progress-fill info"; + fi.style.width = (v.i / t * 100) + "%"; - // Применяем стили к viewport'у миникарты - this.minimapViewport.style.left = `${viewportLeft}px`; - this.minimapViewport.style.top = `${viewportTop}px`; - this.minimapViewport.style.width = `${viewportWidthPx}px`; - this.minimapViewport.style.height = `${viewportHeightPx}px`; + bar.append(fc, fw, fi); + card.append(bar); - // Для отладки (раскомментируйте если нужно) - // console.log("Minimap viewport:", { - // diagram, - // viewportWorld, - // viewportLeft, viewportTop, viewportWidthPx, viewportHeightPx, - // scaleX, scaleY, - // containerRect: { width: containerRect.width, height: containerRect.height } - // }); + const bottom = document.createElement("div"); + bottom.className = "file-card-bottom"; - } catch (error) { - console.error("Error in _updateMinimapViewport:", error); - } - } - async resetView() { - const container = document.getElementById("diagramContainer"); - if (!container || !this.viewportGroup) return; + const total = document.createElement("span"); + total.className = "file-total"; + total.textContent = "Total: " + (v.c + v.w + v.i); - const cw = container.clientWidth; - const ch = container.clientHeight; + const badges = document.createElement("div"); + badges.className = "file-violations"; - if (cw === 0 || ch === 0) { - setTimeout(() => this.resetView(), 50); - return; - } - - // 1. Временно ставим scale = 1, чтобы измерить реальный размер диаграммы - this.scale = 1; - this.tx = 0; - this.ty = 0; - this._applyTransform(); - - // Даем браузеру дорендерить (важно!) - await new Promise(r => requestAnimationFrame(r)); - - // 2. Получаем реальные пиксельные размеры диаграммы - const rect = this.viewportGroup.getBoundingClientRect(); - const diagramPxWidth = rect.width; - const diagramPxHeight = rect.height; - - // 3. Вычисляем zoom-to-fit в пикселях - const scaleX = cw / diagramPxWidth; - const scaleY = ch / diagramPxHeight; - const zoomToFit = Math.min(scaleX, scaleY) * 0.9; - - // 4. Устанавливаем новый пользовательский zoom - this.scale = Math.max(Math.min(this.maxScale, zoomToFit), this.minScale); - - // 5. Центрируем диаграмму - const dx = Math.max(0, (cw - diagramPxWidth * this.scale) / 2); - const dy = Math.max(0, (ch - diagramPxHeight * this.scale) / 2); - - // 6. Преобразуем в юниты - const diagram = this.diagramBounds; - - this.tx = diagramPxWidth / diagram.width * dx; - this.ty = diagramPxHeight / diagram.height * dy; - - this._applyTransform(); - } - - - refresh() { - this._scheduleRender(); - } -} - -/* --------------------------------------------------------- - EXPORT FUNCTION ---------------------------------------------------------- */ -function exportPng() { - const svgElement = document.querySelector('#diagramSvgContainer svg'); - if (!svgElement) { - alert('Диаграмма не найдена для экспорта'); - return; - } - - try { - const svgData = new XMLSerializer().serializeToString(svgElement); - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - const img = new Image(); - - // Get actual dimensions - const bbox = svgElement.getBBox(); - canvas.width = bbox.width * 2; - canvas.height = bbox.height * 2; - - const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' }); - const url = URL.createObjectURL(svgBlob); - - img.onload = function () { - ctx.drawImage(img, 0, 0, canvas.width, canvas.height); - - const pngUrl = canvas.toDataURL('image/png'); - const downloadLink = document.createElement('a'); - downloadLink.href = pngUrl; - downloadLink.download = `diagram_${new Date().toISOString().slice(0, 10)}.png`; - document.body.appendChild(downloadLink); - downloadLink.click(); - document.body.removeChild(downloadLink); - - URL.revokeObjectURL(url); - - // Show success toast - if (window.viewer && viewer._showToast) { - viewer._showToast('Диаграмма экспортирована как PNG', 'success'); + function badge(cls, val) { + const b = document.createElement("span"); + b.className = `violation-badge ${cls}`; + b.textContent = val; + return b; } - }; - img.onerror = function () { - alert('Ошибка при экспорте изображения'); - URL.revokeObjectURL(url); - }; + badges.append( + badge("critical", v.c), + badge("warning", v.w), + badge("info", v.i) + ); - img.src = url; - } catch (error) { - console.error('Export error:', error); - alert('Ошибка при экспорте: ' + error.message); + bottom.append(total, badges); + card.append(bottom); + + filesGrid.append(card); + } + + section2.append(filesGrid); + root.append(section2); + + return root; } -} -/* --------------------------------------------------------- - HANDLE SEARCH KEY PRESS ---------------------------------------------------------- */ -function handleSearchKeyPress(event) { - if (event.key === 'Enter') { - event.preventDefault(); - if (window.viewer && viewer.searchInput) { - const term = viewer.searchInput.value.trim(); - if (term) { - viewer.searchNodes(term); + /* --------------------------------------------------------- + TAB HANDLER + --------------------------------------------------------- */ + + tabsList.addEventListener("click", function (e) { + const tab = e.target.closest(".tab"); + if (!tab) return; + + const targetId = tab.dataset.target; + + // activate tab + tabsList.querySelectorAll(".tab").forEach(t => t.classList.remove("active")); + tab.classList.add("active"); + + // hide all reports + reportsContainer.innerHTML = ""; + + // lazy render + let content; + if (!rendered.has(targetId)) { + if (targetId === "summary_report") { + content = renderSummary(); } else { - viewer.clearSearch(); + const index = Number(tab.dataset.index); + content = renderFileReport(index); } - } - } -} - -/* --------------------------------------------------------- - THEME CHANGE HANDLER ---------------------------------------------------------- */ -function setupThemeChangeHandler() { - const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); - - darkModeMediaQuery.addEventListener('change', (e) => { - if (window.viewer && document.getElementById('mermaid').classList.contains('active')) { - viewer.render().catch(console.error); - } - }); -} - -/* --------------------------------------------------------- - MAIN INITIALIZATION ---------------------------------------------------------- */ -let viewer = null; - -document.addEventListener("DOMContentLoaded", () => { - window.reportData = JSON.parse(document.getElementById('report-data').textContent); - window.hasDiagram = true; - - // Инициализация отчета - if (window.initReport) { - window.initReport(); - } - - - initTabs((id) => { - if (id === "mermaid") { - if (!viewer) { - viewer = new DiagramViewer(); - viewer.render().catch(console.error); + rendered.add(targetId); + } else { + // already rendered → but we removed DOM → re-render + if (targetId === "summary_report") { + content = renderSummary(); } else { - viewer.refresh(); + const index = Number(tab.dataset.index); + content = renderFileReport(index); } } + + reportsContainer.append(content); + window.scrollTo({ top: 0, behavior: "smooth" }); }); - // Auto-click first tab - document.querySelector(".tab.active").click(); + /* --------------------------------------------------------- + INIT + --------------------------------------------------------- */ - // Setup theme change handler - setupThemeChangeHandler(); + renderTabs(); - // Make viewer globally available for button callbacks - window.viewer = viewer; - window.exportPng = exportPng; - window.handleSearchKeyPress = handleSearchKeyPress; + const first = tabsList.querySelector(".tab"); + if (first) first.click(); }); \ No newline at end of file diff --git a/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatterOld.js b/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatterOld.js new file mode 100644 index 0000000..a40f2e3 --- /dev/null +++ b/SQLLinter/Infrastructure/Reporters/Static/HtmlFormatterOld.js @@ -0,0 +1,1895 @@ + +import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs"; + +/* --------------------------------------------------------- + REPORT RENDERER - динамическое создание таблиц и табов +--------------------------------------------------------- */ +class ReportRenderer { + constructor(data) { + this.data = data; + this.files = data.files || data.f || []; + this.rules = data.rules || data.r || {}; + this.diagram = data.diagram || data.d || {}; + this.currentFileIndex = 0; + + this.reportsContainer = document.getElementById('reports-container'); + this.tabsList = document.getElementById('tabs-list'); + + if (!this.reportsContainer || !this.tabsList) { + console.error('Не найдены контейнеры для отчета'); + return; + } + } + + init() { + this.renderTabs(); + this.setupTabsDragScroll(); + this.renderFileReports(); + this.setupTabNavigation(); + this.activateTab(0); + } + + renderTabs() { + const tabsList = document.getElementById('tabs-list'); + if (!tabsList) return; + + this.tabsList.innerHTML = ''; + + //Add file tabs + 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 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)} + ${countersHTML} +
+ `; + + this.tabsList.appendChild(tab); + }); + + // Add Summary tab + const summaryTab = document.createElement('button'); + summaryTab.className = 'tab'; + summaryTab.dataset.target = 'summary_report'; + summaryTab.innerHTML = `
Summary
`; + tabsList.appendChild(summaryTab); + + //Add diagram tab if exists + if (this.diagram.h || this.diagram.hasDiagram) { + const diagramTab = document.createElement('button'); + diagramTab.className = 'tab'; + diagramTab.dataset.target = 'mermaid'; + // Создаем пустые счетчики для диаграммы + + diagramTab.innerHTML = ` +
+ Диаграмма +
+ `; + + 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; + + this.reportsContainer.innerHTML = ''; + + // Рендеринг отчетов по файлам + this.files.forEach((file, index) => { + const reportDiv = document.createElement('div'); + reportDiv.id = `file_${index}`; + reportDiv.className = 'file-report'; + reportDiv.style.display = 'none'; + + const fileName = file.n || file.fileName || ''; + const violations = file.v || file.violations || {}; + + // Заголовок файла + reportDiv.innerHTML = ` +
+
+ + + + + ${this.escapeHtml(fileName)} +
+
+ `; + + // Добавляем секции нарушений + if (file.v.c?.length > 0) { + reportDiv.appendChild(this.createViolationSection('critical', file.v.c)); + } + if (file.v.w?.length > 0) { + reportDiv.appendChild(this.createViolationSection('warning', file.v.w)); + } + if (file.v.i?.length > 0) { + reportDiv.appendChild(this.createViolationSection('info', file.v.i)); + } + + this.reportsContainer.appendChild(reportDiv); + }); + + // Контейнер для диаграммы (если есть) + if (this.diagram.h || this.diagram.hasDiagram) { + const diagramContent = this.diagram.c || this.diagram.Content || ''; + const diagramDiv = document.createElement('div'); + diagramDiv.id = 'mermaid'; + diagramDiv.className = 'file-report'; + diagramDiv.style.display = 'none'; + + diagramDiv.innerHTML = ` +
+ +
+ + +
+
+
+
+
+
+ + `; + + this.reportsContainer.appendChild(diagramDiv); + } + + this.renderSummaryReport(); + } + + createViolationSection(severity, violations) { + const severityTitle = { + critical: 'Critical', + warning: 'Warning', + info: 'Info' + }[severity] || 'Unknown'; + + const section = document.createElement('div'); + section.className = `severity-section ${severity}`; + + const tableRows = violations.map(violation => { + // Получаем правило по ID + const ruleId = violation.r || violation.ruleId; + const rule = this.rules[ruleId] || { n: 'Unknown', t: 'Описание отсутствует' }; + + // Формируем текст с подстановкой параметров + let text = rule.t || ''; + const args = violation.a || violation.args || []; + + if (args.length > 0 && text.includes('{')) { + args.forEach((arg, index) => { + text = text.replace(new RegExp(`\\{${index}\\}`, 'g'), arg); + }); + } + + return ` + + ${violation.i || violation.index} + ${violation.l || violation.line} + ${violation.c || violation.column} + ${this.escapeHtml(rule.n || rule.name)} + ${this.escapeHtml(text)} + + `; + }).join(''); + + section.innerHTML = ` +
+
+

${severityTitle}

+ ${violations.length} +
+
+
+ + + + + + + + + + + + ${tableRows} + +
#СтрокаКолонкаПравилоОписание
+
+ `; + + return section; + } + + setupTabNavigation() { + const tabs = this.tabsList.querySelectorAll('.tab'); + + tabs.forEach(tab => { + tab.addEventListener('click', (e) => { + e.preventDefault(); + + const targetId = tab.dataset.target; + const index = tab.dataset.index; + + // Обновляем активный таб + tabs.forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + + // Скрываем все отчеты + document.querySelectorAll('.file-report').forEach(report => { + report.style.display = 'none'; + }); + + // Показываем выбранный отчет + const targetReport = document.getElementById(targetId); + if (targetReport) { + targetReport.style.display = 'block'; + + // Если это диаграмма, инициализируем ее + if (targetId === 'mermaid' && window.viewer) { + window.viewer.render().catch(console.error); + } + + // Прокрутка к верху + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + }); + }); + } + + activateTab(index) { + const tab = this.tabsList.querySelector(`.tab[data-index="${index}"]`); + if (tab) { + tab.click(); + } + } + + calculateTotalViolations(file) { + return (file.v.c?.length || 0) + (file.v.w?.length || 0) + (file.v.i?.length || 0); + } + + renderSummaryReport() { + const summaryDiv = document.createElement('div'); + summaryDiv.id = 'summary_report'; + summaryDiv.className = 'file-report'; + summaryDiv.style.display = 'none'; + + // Calculate total statistics + let totalCritical = 0; + let totalWarning = 0; + let totalInfo = 0; + let totalFiles = this.files.length; + + this.files.forEach(file => { + totalCritical += file.v.c?.length || 0; + totalWarning += file.v.w?.length || 0; + totalInfo += file.v.i?.length || 0; + }); + + const totalViolations = totalCritical + totalWarning + totalInfo; + + // Calculate percentages for progress bars - FIXED + let criticalPercent = totalViolations > 0 ? (totalCritical / totalViolations * 100) : 0; + let warningPercent = totalViolations > 0 ? (totalWarning / totalViolations * 100) : 0; + let infoPercent = totalViolations > 0 ? (totalInfo / totalViolations * 100) : 0; + + // Ensure sum equals 100% + const totalPercent = criticalPercent + warningPercent + infoPercent; + if (totalPercent > 0 && Math.abs(totalPercent - 100) > 0.01) { + // Adjust the largest segment to make sum = 100% + const maxVal = Math.max(criticalPercent, warningPercent, infoPercent); + if (maxVal === criticalPercent) { + criticalPercent = 100 - warningPercent - infoPercent; + } else if (maxVal === warningPercent) { + warningPercent = 100 - criticalPercent - infoPercent; + } else { + infoPercent = 100 - criticalPercent - warningPercent; + } + } + + // Format for display + const criticalPercentDisplay = criticalPercent.toFixed(1); + const warningPercentDisplay = warningPercent.toFixed(1); + const infoPercentDisplay = infoPercent.toFixed(1); + + summaryDiv.innerHTML = ` +
+
+ + + + Сводный отчет +
+
+
+
+
+

Общая статистика

+
+
+
+
+
${totalFiles}
+
Всего файлов
+
+
+
${totalCritical}
+
Critical нарушений
+
+
+
${totalWarning}
+
Warning нарушений
+
+
+
${totalInfo}
+
Info нарушений
+
+
+
+
+
+
+
+
+
+ Critical: ${totalCritical} (${criticalPercentDisplay}%) + Warning: ${totalWarning} (${warningPercentDisplay}%) + Info: ${totalInfo} (${infoPercentDisplay}%) +
+
+
+
+
+
+

Статистика файлов

+ ${totalFiles} +
+
+
+ ${this.files.map(file => { + const critical = file.v.c?.length || 0; + const warning = file.v.w?.length || 0; + const info = file.v.i?.length || 0; + const total = critical + warning + info; + + // Calculate percentages for file progress bar - FIXED + let fileCriticalPercent = total > 0 ? (critical / total * 100) : 0; + let fileWarningPercent = total > 0 ? (warning / total * 100) : 0; + let fileInfoPercent = total > 0 ? (info / total * 100) : 0; + + // Ensure sum equals 100% + const fileTotalPercent = fileCriticalPercent + fileWarningPercent + fileInfoPercent; + if (fileTotalPercent > 0 && Math.abs(fileTotalPercent - 100) > 0.01) { + // Adjust the largest segment to make sum = 100% + const maxVal = Math.max(fileCriticalPercent, fileWarningPercent, fileInfoPercent); + if (maxVal === fileCriticalPercent) { + fileCriticalPercent = 100 - fileWarningPercent - fileInfoPercent; + } else if (maxVal === fileWarningPercent) { + fileWarningPercent = 100 - fileCriticalPercent - fileInfoPercent; + } else { + fileInfoPercent = 100 - fileCriticalPercent - fileWarningPercent; + } + } + + return `
+
+ ${this.escapeHtml(file.n)} +
+
+
+
+
+
+
+ Total: ${total} +
+ ${critical} + ${warning} + ${info} +
+
+
`; + }).join('')} +
+
+ `; + + this.reportsContainer.appendChild(summaryDiv); + } + + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } +} + +/* --------------------------------------------------------- + ГЛОБАЛЬНАЯ ИНИЦИАЛИЗАЦИЯ +--------------------------------------------------------- */ +function initReport() { + if (!window.reportData) { + console.error('Данные отчета не найдены'); + return; + } + + const renderer = new ReportRenderer(window.reportData); + renderer.init(); + + // Сохраняем рендерер в глобальной области видимости + window.reportRenderer = renderer; +} + +// Экспортируем функции для глобального использования +window.initReport = initReport; + +/* --------------------------------------------------------- + TABS MANAGEMENT +--------------------------------------------------------- */ +function initTabs(onTabActivated) { + const tabs = document.querySelectorAll(".tab"); + const reports = document.querySelectorAll(".file-report"); + + tabs.forEach(tab => { + tab.addEventListener("click", () => { + const id = tab.dataset.target; + + // Add ripple effect + const ripple = document.createElement('span'); + const rect = tab.getBoundingClientRect(); + const size = Math.max(rect.width, rect.height); + const x = event.clientX - rect.left; + const y = event.clientY - rect.top; + + ripple.style.cssText = ` + position: absolute; + border-radius: 50%; + background: rgba(0, 120, 212, 0.2); + transform: scale(0); + animation: ripple 0.6s linear; + width: ${size}px; + height: ${size}px; + left: ${x - size / 2}px; + top: ${y - size / 2}px; + `; + + /* + tab.style.position = 'relative'; + tab.style.overflow = 'hidden'; + */ + tab.appendChild(ripple); + + setTimeout(() => { + if (ripple.parentElement === tab) { + ripple.remove(); + } + }, 600); + + tabs.forEach(t => t.classList.remove("active")); + reports.forEach(r => r.classList.remove("active")); + + tab.classList.add("active"); + const report = document.getElementById(id); + if (report) { + report.classList.add("active"); + + // Scroll to top when switching tabs + window.scrollTo({ top: 0, behavior: 'smooth' }); + } + + if (onTabActivated) onTabActivated(id); + }); + }); +} + +// Исправление для табов на странице диаграммы +document.addEventListener('DOMContentLoaded', function () { + const tabsContainer = document.querySelector('.tabs-container'); + if (tabsContainer) { + tabsContainer.style.zIndex = '1001'; + tabsContainer.style.position = 'fixed'; + tabsContainer.style.bottom = '0'; + tabsContainer.style.left = '0'; + tabsContainer.style.right = '0'; + } + + // Гарантируем, что табы всегда поверх всего + const tabs = document.querySelectorAll('.tab'); + tabs.forEach(tab => { + tab.style.zIndex = '1002'; + }); +}); + +/* --------------------------------------------------------- + DIAGRAM VIEWER CLASS +--------------------------------------------------------- */ +class DiagramViewer { + constructor() { + this.container = document.getElementById("diagramSvgContainer"); + this.minimap = document.getElementById("minimap"); + this.searchInput = document.getElementById("diagramSearch"); + this.resetBtn = document.getElementById("resetViewBtn"); + + this.svg = null; + this.viewportGroup = null; + this.nodes = []; + + this.scale = 1; + this.tx = 0; + this.ty = 0; + this.minScale = 0.3; + this.maxScale = 12; + + this.originalViewBox = null; + this.minimapSvg = null; + this.minimapViewport = null; + + this.needsRender = false; + this.isPanning = false; + this.lastSearchTerm = ''; + this.searchResults = []; + this.currentSearchIndex = -1; + + this.init(); + } + + init() { + // Add CSS for ripple animation + if (!document.querySelector('#ripple-styles')) { + const style = document.createElement('style'); + style.id = 'ripple-styles'; + style.textContent = ` + @keyframes ripple { + to { + transform: scale(4); + opacity: 0; + } + } + + @keyframes fadeIn { + from {opacity: 0; transform: translateY(-10px); } + to {opacity: 1; transform: translateY(0); } + } + `; + document.head.appendChild(style); + } + } + + async render() { + this.container.classList.add('loading'); + + + try { + + const mermaidSource = document.getElementById("mermaidSource").textContent.trim(); + const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + + mermaid.initialize({ + startOnLoad: false, + theme: isDark ? "dark" : "default", + themeVariables: isDark ? { + fontFamily: "'Segoe UI', system-ui, -apple-system, sans-serif", + primaryColor: "#2d2d2d", + primaryBorderColor: "#2899f5", + primaryTextColor: "#f3f2f1", + lineColor: "#797775", + lineWidth: 2.5, + secondaryColor: "#3c3c3c", + tertiaryColor: "#484644", + background: "#1e1e1e", + clusterBkg: "#201f1e", + clusterBorder: "#605e5c", + edgeLabelBackground: "#252423", + nodeBkg: "#2d2d2d", + nodeBorder: "#2899f5", + nodeTextColor: "#f3f2f1", + mainBkg: "#1e1e1e", + textColor: "#f3f2f1", + arrowheadColor: "#797775", + fontSize: "14px" + } : { + fontFamily: "'Segoe UI', system-ui, -apple-system, sans-serif", + primaryColor: "#f0f8ff", + primaryBorderColor: "#0078d4", + primaryTextColor: "#323130", + lineColor: "#8a8886", + lineWidth: 2.5, + secondaryColor: "#deecf9", + tertiaryColor: "#c7e0f4", + background: "#ffffff", + clusterBkg: "#f3f2f1", + clusterBorder: "#a19f9d", + edgeLabelBackground: "#ffffff", + nodeBkg: "#f0f8ff", + nodeBorder: "#0078d4", + nodeTextColor: "#323130", + mainBkg: "#ffffff", + textColor: "#323130", + arrowheadColor: "#8a8886", + fontSize: "14px" + }, + flowchart: { + useMaxWidth: false, + htmlLabels: true, + curve: "basis" + } + }); + + try { + const { svg } = await mermaid.render("diagram", mermaidSource); + + // Убираем только встроенные стили Mermaid, оставляя структуру + const cleanedSvg = svg.replace(/]*>[\s\S]*?<\/style>/gi, ""); + + this.container.innerHTML = cleanedSvg; + this.svg = this.container.querySelector("svg"); + + if (this.svg) { + // Добавляем небольшую задержку для гарантированной отрисовки + await new Promise(resolve => setTimeout(resolve, 50)); + + // Добавляем классы для разных типов узлов + this._addNodeClasses(); + this._afterSvgLoaded(); + this._initSearch(); + this._attachEventListeners(); + this._setupResizeObserver(); + this.container.classList.remove('loading'); + } + } catch (error) { + console.error('Mermaid rendering error:', error); + this._showError(error); + } + } catch (error) { + this.container.classList.remove('loading'); + this._showError(error); + } + } + + recalculateBounds() { + this._calculateRealBounds(); + this._updateMinimapViewport(); + } + + _showError(error) { + this.container.innerHTML = ` +
+
+
📊
+

+ Не удалось загрузить диаграмму +

+

+ ${error.message} +

+ +
+
+ `; + } + + _addNodeClasses() { + // Добавляем классы к узлам на основе текста + const nodes = this.svg.querySelectorAll('.node'); + nodes.forEach(node => { + const textElement = node.querySelector('text'); + if (textElement) { + const text = textElement.textContent.toLowerCase(); + + if (text.includes('if') || text.includes('condition') || text.includes('условие')) { + node.classList.add('condition'); + } else if (text.includes('start') || text.includes('end') || + text.includes('начало') || text.includes('конец')) { + node.classList.add('start'); + } else if (text.includes('exec') || text.includes('execute') || + text.includes('выполнить') || text.includes('выполнение')) { + node.classList.add('exec'); + } else if (text.includes('select') || text.includes('insert') || + text.includes('update') || text.includes('delete') || + text.includes('выбрать') || text.includes('вставить') || + text.includes('обновить') || text.includes('удалить')) { + node.classList.add('process'); + } else if (text.includes('import') || text.includes('export') || + text.includes('импорт') || text.includes('экспорт')) { + node.classList.add('import'); + } + } + }); + } + + _setupResizeObserver() { + const resizeObserver = new ResizeObserver(() => { + if (this.svg) { + this._updateMinimapViewport(); + } + }); + + resizeObserver.observe(this.container); + } + + _attachEventListeners() { + // Add double click to reset zoom + this.svg.addEventListener('dblclick', (e) => { + e.preventDefault(); + this.resetView(); + }); + + // Add keyboard shortcuts + document.addEventListener('keydown', (e) => { + if (e.target === this.searchInput || e.target.matches('input, textarea')) return; + + switch (e.key) { + case '+': + case '=': + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + this.zoomIn(); + } + break; + case '-': + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + this.zoomOut(); + } + break; + case '0': + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + this.resetView(); + } + break; + case 'Escape': + this.clearSearch(); + break; + case 'F3': + case 'Enter': + if (e.shiftKey) { + e.preventDefault(); + this.prevSearchResult(); + } else if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + this.nextSearchResult(); + } + break; + } + }); + + // Reset button + if (this.resetBtn) { + this.resetBtn.addEventListener('click', () => { + this.resetView(); + this._showToast('Вид диаграммы сброшен', 'info'); + }); + } + } + + _initSearch() { + if (this.searchInput) { + // Clear button + const clearBtn = document.createElement('button'); + clearBtn.innerHTML = '✕'; + clearBtn.title = 'Очистить поиск'; + clearBtn.style.cssText = ` + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--text-secondary); + cursor: pointer; + padding: 4px; + font-size: 12px; + opacity: 0.7; + transition: opacity 0.2s; + border-radius: 50%; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + `; + + clearBtn.addEventListener('mouseover', () => { + clearBtn.style.opacity = '1'; + clearBtn.style.backgroundColor = 'var(--surface-neutral)'; + }); + + clearBtn.addEventListener('mouseout', () => { + clearBtn.style.opacity = '0.7'; + clearBtn.style.backgroundColor = 'transparent'; + }); + + clearBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.clearSearch(); + this.searchInput.value = ''; + this.searchInput.focus(); + this._showToast('Поиск очищен', 'info'); + }); + + const searchContainer = this.searchInput.parentElement; + searchContainer.style.position = 'relative'; + searchContainer.appendChild(clearBtn); + + this.searchInput.addEventListener('keyup', (e) => { + if (e.key === 'Enter') { + const term = this.searchInput.value.trim(); + if (term) { + this.searchNodes(term); + } else { + this.clearSearch(); + } + } else if (e.key === 'Escape') { + this.clearSearch(); + this.searchInput.value = ''; + } + }); + + // Show clear button only when there's text + this.searchInput.addEventListener('input', () => { + clearBtn.style.display = this.searchInput.value ? 'flex' : 'none'; + }); + + clearBtn.style.display = 'none'; + } + } + + searchNodes(term) { + if (!this.svg || !term) { + this.clearSearch(); + return; + } + + // Clear previous highlights + this.clearSearch(); + + this.lastSearchTerm = term.toLowerCase(); + const nodes = this.svg.querySelectorAll('.node'); + this.searchResults = []; + + nodes.forEach((node) => { + const textElement = node.querySelector('text'); + if (textElement && textElement.textContent.toLowerCase().includes(this.lastSearchTerm)) { + this.searchResults.push(node); + } + }); + + if (this.searchResults.length > 0) { + this.currentSearchIndex = 0; + this._highlightCurrentSearchResult(); + this._showSearchResultsInfo(); + + // Center view on first result + this._centerOnNode(this.searchResults[0]); + } else { + this._showToast('Ничего не найдено', 'warning'); + } + } + + _highlightCurrentSearchResult() { + // Clear all highlights first + this.searchResults.forEach((node, index) => { + node.classList.remove('node-search-match', 'node-highlight', 'node-search-active'); + + if (index === this.currentSearchIndex) { + node.classList.add('node-search-active', 'node-highlight'); + } else { + node.classList.add('node-search-match'); + } + }); + } + + nextSearchResult() { + if (this.searchResults.length === 0) return; + + this.currentSearchIndex = (this.currentSearchIndex + 1) % this.searchResults.length; + this._highlightCurrentSearchResult(); + this._centerOnNode(this.searchResults[this.currentSearchIndex]); + this._updateSearchResultsInfo(); + } + + prevSearchResult() { + if (this.searchResults.length === 0) return; + + this.currentSearchIndex = (this.currentSearchIndex - 1 + this.searchResults.length) % this.searchResults.length; + this._highlightCurrentSearchResult(); + this._centerOnNode(this.searchResults[this.currentSearchIndex]); + this._updateSearchResultsInfo(); + } + + _centerOnNode(node) { + if (!node || !this.svg) return; + + const bbox = node.getBBox(); + const container = document.getElementById("diagramContainer"); + + // Calculate center of node in SVG coordinates + const centerX = bbox.x + bbox.width / 2; + const centerY = bbox.y + bbox.height / 2; + + // Calculate transform to center this point + this.tx = container.clientWidth / 2 - centerX * this.scale; + this.ty = container.clientHeight / 2 - centerY * this.scale; + + this._scheduleRender(); + + // Add a little bounce effect + node.style.transform = 'scale(1.05)'; + setTimeout(() => { + node.style.transform = ''; + }, 300); + } + + _showSearchResultsInfo() { + // Remove existing info if any + this._removeSearchResultsInfo(); + + const info = document.createElement('div'); + info.className = 'search-results-info'; + info.style.cssText = ` + position: absolute; + top: 60px; + left: 16px; + background: var(--surface-default); + padding: var(--spacing-s) var(--spacing-m); + border-radius: var(--border-radius-medium); + box-shadow: var(--shadow-16); + border: 1px solid var(--border-default); + font-size: 13px; + z-index: 1000; + animation: fadeIn 0.3s ease; + display: flex; + align-items: center; + gap: var(--spacing-m); + `; + + info.innerHTML = ` + + Найдено: ${this.searchResults.length} узлов + + ${this.searchResults.length > 1 ? ` +
+ + +
+ + ${this.currentSearchIndex + 1} из ${this.searchResults.length} + + ` : ''} + + `; + + this.container.appendChild(info); + } + + _updateSearchResultsInfo() { + const info = this.container.querySelector('.search-results-info'); + if (info && this.searchResults.length > 1) { + const counter = info.querySelector('span:last-of-type'); + if (counter) { + counter.textContent = `${this.currentSearchIndex + 1} из ${this.searchResults.length}`; + } + } + } + + _removeSearchResultsInfo() { + const info = this.container.querySelector('.search-results-info'); + if (info) { + info.remove(); + } + } + + clearSearch() { + if (this.svg) { + const nodes = this.svg.querySelectorAll('.node'); + nodes.forEach(node => { + node.classList.remove('node-search-match', 'node-highlight', 'node-search-active'); + node.style.transform = ''; + }); + } + + this.searchResults = []; + this.currentSearchIndex = -1; + this._removeSearchResultsInfo(); + } + + _showToast(message, type = 'info') { + const toast = document.createElement('div'); + toast.textContent = message; + toast.style.cssText = ` + position: fixed; + top: 80px; + right: 20px; + background: var(--surface-default); + color: var(--text-primary); + padding: var(--spacing-m) var(--spacing-l); + border-radius: var(--border-radius-medium); + box-shadow: var(--shadow-16); + border: 1px solid var(--border-default); + z-index: 9999; + animation: fadeIn 0.3s ease; + font-size: 14px; + border-left: 4px solid ${type === 'error' ? '#d13438' : + type === 'warning' ? '#ffaa44' : + type === 'success' ? '#107c10' : + 'var(--primary-color)' + }; + `; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.style.opacity = '0'; + toast.style.transform = 'translateY(-10px)'; + setTimeout(() => { + if (toast.parentElement) { + toast.remove(); + } + }, 300); + }, 3000); + } + + zoomIn() { + const zoomFactor = 1.2; + const newScale = Math.min(this.maxScale, this.scale * zoomFactor); + this.scale = newScale; + this._scheduleRender(); + this._showToast(`Масштаб: ${Math.round(this.scale * 100)}%`, 'info'); + } + + zoomOut() { + const zoomFactor = 0.8; + const newScale = Math.max(this.minScale, this.scale * zoomFactor); + this.scale = newScale; + this._scheduleRender(); + this._showToast(`Масштаб: ${Math.round(this.scale * 100)}%`, 'info'); + } + + _afterSvgLoaded() { + const vb = this.svg.viewBox.baseVal; + this.originalViewBox = { + x: vb.x, + y: vb.y, + width: vb.width, + height: vb.height + }; + + this._wrapContent(); + this._initZoomPan(); + + // Используем requestAnimationFrame для получения реальных границ после отрисовки + requestAnimationFrame(() => { + this._calculateRealBounds(); + this._initMinimap(); + this.resetView(); + this._scheduleRender(); + }); + } + + _calculateRealBounds() { + if (!this.viewportGroup) return; + + try { + // Пробуем получить реальные границы содержимого + let bbox; + + // Метод 1: Пробуем получить bbox всей группы + try { + bbox = this.viewportGroup.getBBox(); + } catch (e) { + bbox = null; + } + + // Метод 2: Если getBBox не работает или возвращает 0, используем viewBox + if (!bbox || bbox.width === 0 || bbox.height === 0 || + bbox.x === Infinity || bbox.y === Infinity) { + console.warn("getBBox failed, using originalViewBox"); + this.diagramBounds = { ...this.originalViewBox }; + return; + } + + // Метод 3: Если getBBox работает, но границы слишком малы, проверяем ручной расчет + if (bbox.width < 10 || bbox.height < 10) { + // Пробуем ручной расчет через содержимое + const elements = this.viewportGroup.querySelectorAll('*[x], *[y], *[cx], *[cy]'); + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + let hasValidCoords = false; + + elements.forEach(el => { + let x = parseFloat(el.getAttribute('x') || el.getAttribute('cx') || '0'); + let y = parseFloat(el.getAttribute('y') || el.getAttribute('cy') || '0'); + let width = parseFloat(el.getAttribute('width') || '0'); + let height = parseFloat(el.getAttribute('height') || '0'); + let r = parseFloat(el.getAttribute('r') || '0'); + + if (!isNaN(x) && !isNaN(y)) { + if (el.tagName === 'circle') { + x = x - r; + y = y - r; + width = height = r * 2; + } else if (el.tagName === 'ellipse') { + const rx = parseFloat(el.getAttribute('rx') || '0'); + const ry = parseFloat(el.getAttribute('ry') || '0'); + x = x - rx; + y = y - ry; + width = rx * 2; + height = ry * 2; + } + + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x + width); + maxY = Math.max(maxY, y + height); + hasValidCoords = true; + } + }); + + if (hasValidCoords && minX !== Infinity) { + bbox = { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY + }; + } + } + + // Добавляем отступ в 5% от размера диаграммы + const padding = Math.max(bbox.width, bbox.height) * 0.05; + this.diagramBounds = { + x: bbox.x - padding, + y: bbox.y - padding, + width: bbox.width + padding * 2, + height: bbox.height + padding * 2 + }; + + console.log("Diagram bounds calculated:", this.diagramBounds); + + } catch (error) { + console.error("Error calculating diagram bounds:", error); + this.diagramBounds = { ...this.originalViewBox }; + } + } + + _wrapContent() { + const children = Array.from(this.svg.children); + this.viewportGroup = document.createElementNS("http://www.w3.org/2000/svg", "g"); + this.viewportGroup.setAttribute("id", "viewport-group"); + + children.forEach(child => { + if (child.tagName.toLowerCase() !== "defs") { + this.viewportGroup.appendChild(child.cloneNode(true)); + child.remove(); + } + }); + + this.svg.appendChild(this.viewportGroup); + } + + _initZoomPan() { + let lastX = 0; + let lastY = 0; + let isMouseDown = false; + let lastTouchDistance = 0; + + // Перевод dx/dy из экранных пикселей в SVG-координаты + const screenDeltaToSvgDelta = (dx, dy) => { + const pt1 = this.svg.createSVGPoint(); + const pt2 = this.svg.createSVGPoint(); + pt1.x = 0; + pt1.y = 0; + pt2.x = dx; + pt2.y = dy; + + const ctm = this.svg.getScreenCTM(); + if (!ctm) return { dxSvg: dx, dySvg: dy }; + + const inv = ctm.inverse(); + const w1 = pt1.matrixTransform(inv); + const w2 = pt2.matrixTransform(inv); + + return { + dxSvg: w2.x - w1.x, + dySvg: w2.y - w1.y + }; + }; + + /* --------------------------------------------------------- + PAN (mouse) + --------------------------------------------------------- */ + this.svg.addEventListener("mousedown", (e) => { + if (e.button !== 0) return; + e.preventDefault(); + + isMouseDown = true; + this.isPanning = true; + + lastX = e.clientX; + lastY = e.clientY; + + this.svg.style.cursor = "grabbing"; + }); + + window.addEventListener("mousemove", (e) => { + if (!isMouseDown) return; + + const dxScreen = e.clientX - lastX; + const dyScreen = e.clientY - lastY; + + const { dxSvg, dySvg } = screenDeltaToSvgDelta(dxScreen, dyScreen); + + this.tx += dxSvg; + this.ty += dySvg; + + lastX = e.clientX; + lastY = e.clientY; + + this._scheduleRender(); + }); + + window.addEventListener("mouseup", () => { + isMouseDown = false; + this.isPanning = false; + this.svg.style.cursor = "grab"; + }); + + /* --------------------------------------------------------- + PAN + PINCH (touch) + --------------------------------------------------------- */ + this.svg.addEventListener("touchstart", (e) => { + if (e.touches.length === 1) { + const t = e.touches[0]; + lastX = t.clientX; + lastY = t.clientY; + this.isPanning = true; + } else if (e.touches.length === 2) { + this.isPanning = false; + lastTouchDistance = this._getTouchDistance(e); + } + }, { passive: false }); + + this.svg.addEventListener("touchmove", (e) => { + if (e.touches.length === 1 && this.isPanning) { + e.preventDefault(); + const t = e.touches[0]; + + const dxScreen = t.clientX - lastX; + const dyScreen = t.clientY - lastY; + + const { dxSvg, dySvg } = screenDeltaToSvgDelta(dxScreen, dyScreen); + + this.tx += dxSvg; + this.ty += dySvg; + + lastX = t.clientX; + lastY = t.clientY; + + this._scheduleRender(); + } else if (e.touches.length === 2) { + e.preventDefault(); + + const newDist = this._getTouchDistance(e); + const zoomFactor = newDist / lastTouchDistance; + lastTouchDistance = newDist; + + const rect = this.svg.getBoundingClientRect(); + const midX = (e.touches[0].clientX + e.touches[1].clientX) / 2 - rect.left; + const midY = (e.touches[0].clientY + e.touches[1].clientY) / 2 - rect.top; + + const worldX = (midX - this.tx) / this.scale; + const worldY = (midY - this.ty) / this.scale; + + const newScale = Math.min(this.maxScale, Math.max(this.minScale, this.scale * zoomFactor)); + + this.tx = midX - worldX * newScale; + this.ty = midY - worldY * newScale; + this.scale = newScale; + + this._scheduleRender(); + } + }, { passive: false }); + + this.svg.addEventListener("touchend", () => { + this.isPanning = false; + }); + + /* --------------------------------------------------------- + ZOOM (wheel) — zoom-to-cursor + --------------------------------------------------------- */ + this.svg.addEventListener("wheel", (e) => { + e.preventDefault(); + + const rect = this.svg.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + const zoomFactor = e.deltaY < 0 ? 1.1 : 0.9; + const newScale = Math.min(this.maxScale, Math.max(this.minScale, this.scale * zoomFactor)); + + const worldX = (mouseX - this.tx) / this.scale; + const worldY = (mouseY - this.ty) / this.scale; + + this.tx = mouseX - worldX * newScale; + this.ty = mouseY - worldY * newScale; + this.scale = newScale; + + this._scheduleRender(); + }, { passive: false }); + } + + _getTouchDistance(e) { + const dx = e.touches[0].clientX - e.touches[1].clientX; + const dy = e.touches[0].clientY - e.touches[1].clientY; + return Math.sqrt(dx * dx + dy * dy); + } + + _getWorldRect() { + const container = document.getElementById("diagramContainer"); + const ctm = this.viewportGroup.getCTM(); + if (!ctm) return { x: 0, y: 0, width: 100, height: 100 }; + + const inv = ctm.inverse(); + + const p1 = this.svg.createSVGPoint(); + p1.x = 0; + p1.y = 0; + const tl = p1.matrixTransform(inv); + + const p2 = this.svg.createSVGPoint(); + p2.x = container.clientWidth; + p2.y = container.clientHeight; + const br = p2.matrixTransform(inv); + + return { + x: tl.x, + y: tl.y, + width: br.x - tl.x, + height: br.y - tl.y + }; + } + + _applyTransform() { + if (!this.viewportGroup) return; + + this.viewportGroup.setAttribute( + "transform", + `translate(${this.tx}, ${this.ty}) scale(${this.scale})` + ); + + + // ОБЯЗАТЕЛЬНО обновляем миникарту после трансформации + this._updateMinimapViewport(); + } + + + _scheduleRender() { + if (this.needsRender) return; + this.needsRender = true; + + requestAnimationFrame(() => { + this.needsRender = false; + this._applyTransform(); + }); + } + + _initMinimap() { + if (!this.minimap || !this.svg) return; + + this.minimap.innerHTML = ""; + + // Клонируем SVG для миникарты + this.minimapSvg = this.svg.cloneNode(true); + + // Убираем трансформации с клонированных элементов + const cloneViewport = this.minimapSvg.querySelector("#viewport-group"); + if (cloneViewport) { + cloneViewport.removeAttribute("transform"); + } + + // Используем diagramBounds для viewBox миникарты + const diagram = this.diagramBounds || this.originalViewBox; + + // Устанавливаем viewBox для миникарты + this.minimapSvg.setAttribute( + "viewBox", + `${diagram.x} ${diagram.y} ${diagram.width} ${diagram.height}` + ); + + this.minimapSvg.setAttribute("preserveAspectRatio", "xMidYMid meet"); + this.minimapSvg.style.width = "100%"; + this.minimapSvg.style.height = "100%"; + this.minimapSvg.style.position = "absolute"; + this.minimapSvg.style.opacity = "0.7"; + + // Убираем интерактивность + this.minimapSvg.querySelectorAll('*').forEach(el => { + el.style.pointerEvents = 'none'; + }); + + this.minimap.appendChild(this.minimapSvg); + + // Создаем viewport элемент + this.minimapViewport = document.createElement("div"); + this.minimapViewport.className = "minimap-viewport"; + this.minimapViewport.style.cssText = ` + position: absolute; + border: 2px solid var(--color-primary); + background-color: rgba(0, 120, 212, 0.15); + pointer-events: none; + transition: all 0.1s ease; + box-sizing: border-box; + z-index: 10; + `; + + this.minimap.appendChild(this.minimapViewport); + + // Обработчик клика по миникарте + this.minimap.addEventListener("click", (e) => { + if (!this.minimapSvg) return; + + const rect = this.minimap.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + // Процентное положение в миникарте + const percentX = x / rect.width; + const percentY = y / rect.height; + + const diagram = this.diagramBounds || this.originalViewBox; + + // Мировые координаты в диаграмме + const worldX = diagram.x + diagram.width * percentX; + const worldY = diagram.y + diagram.height * percentY; + + const container = document.getElementById("diagramContainer"); + if (!container) return; + + // Центрируем view на точке клика + this.tx = container.clientWidth / 2 - worldX * this.scale; + this.ty = container.clientHeight / 2 - worldY * this.scale; + + this._scheduleRender(); + }); + } + + _updateMinimapViewport() { + if (!this.minimapViewport || !this.minimapSvg || !this.minimap || !this.svg) { + return; + } + + try { + // Получаем размеры миникарты + const minimapRect = this.minimap.getBoundingClientRect(); + if (minimapRect.width === 0 || minimapRect.height === 0) return; + + // Получаем текущую трансформацию viewportGroup + const ctm = this.viewportGroup ? this.viewportGroup.getCTM() : null; + if (!ctm) return; + + // Получаем размеры контейнера диаграммы + const container = document.getElementById("diagramContainer"); + if (!container) return; + const containerRect = container.getBoundingClientRect(); + + // Рассчитываем видимую область в мировых координатах + const inv = ctm.inverse(); + + // Левый верхний угол контейнера (0,0 в координатах SVG viewport) + const topLeftScreen = this.svg.createSVGPoint(); + topLeftScreen.x = 0; + topLeftScreen.y = 0; + const topLeftWorld = topLeftScreen.matrixTransform(inv); + + // Правый нижний угол контейнера + const bottomRightScreen = this.svg.createSVGPoint(); + bottomRightScreen.x = containerRect.width; + bottomRightScreen.y = containerRect.height; + const bottomRightWorld = bottomRightScreen.matrixTransform(inv); + + // Определяем границы видимой области + const viewportWorld = { + left: Math.min(topLeftWorld.x, bottomRightWorld.x), + top: Math.min(topLeftWorld.y, bottomRightWorld.y), + right: Math.max(topLeftWorld.x, bottomRightWorld.x), + bottom: Math.max(topLeftWorld.y, bottomRightWorld.y) + }; + + const viewportWidth = viewportWorld.right - viewportWorld.left; + const viewportHeight = viewportWorld.bottom - viewportWorld.top; + + // Получаем границы всей диаграммы (из diagramBounds) + const diagram = this.diagramBounds || this.originalViewBox; + + // Рассчитываем масштаб для миникарты + const scaleX = minimapRect.width / diagram.width; + const scaleY = minimapRect.height / diagram.height; + + // Преобразуем мировые координаты в координаты миникарты + const viewportLeft = (viewportWorld.left - diagram.x) * scaleX; + const viewportTop = (viewportWorld.top - diagram.y) * scaleY; + const viewportWidthPx = viewportWidth * scaleX; + const viewportHeightPx = viewportHeight * scaleY; + + // Применяем стили к viewport'у миникарты + this.minimapViewport.style.left = `${viewportLeft}px`; + this.minimapViewport.style.top = `${viewportTop}px`; + this.minimapViewport.style.width = `${viewportWidthPx}px`; + this.minimapViewport.style.height = `${viewportHeightPx}px`; + + // Для отладки (раскомментируйте если нужно) + // console.log("Minimap viewport:", { + // diagram, + // viewportWorld, + // viewportLeft, viewportTop, viewportWidthPx, viewportHeightPx, + // scaleX, scaleY, + // containerRect: { width: containerRect.width, height: containerRect.height } + // }); + + } catch (error) { + console.error("Error in _updateMinimapViewport:", error); + } + } + async resetView() { + const container = document.getElementById("diagramContainer"); + if (!container || !this.viewportGroup) return; + + const cw = container.clientWidth; + const ch = container.clientHeight; + + if (cw === 0 || ch === 0) { + setTimeout(() => this.resetView(), 50); + return; + } + + // 1. Временно ставим scale = 1, чтобы измерить реальный размер диаграммы + this.scale = 1; + this.tx = 0; + this.ty = 0; + this._applyTransform(); + + // Даем браузеру дорендерить (важно!) + await new Promise(r => requestAnimationFrame(r)); + + // 2. Получаем реальные пиксельные размеры диаграммы + const rect = this.viewportGroup.getBoundingClientRect(); + const diagramPxWidth = rect.width; + const diagramPxHeight = rect.height; + + // 3. Вычисляем zoom-to-fit в пикселях + const scaleX = cw / diagramPxWidth; + const scaleY = ch / diagramPxHeight; + const zoomToFit = Math.min(scaleX, scaleY) * 0.9; + + // 4. Устанавливаем новый пользовательский zoom + this.scale = Math.max(Math.min(this.maxScale, zoomToFit), this.minScale); + + // 5. Центрируем диаграмму + const dx = Math.max(0, (cw - diagramPxWidth * this.scale) / 2); + const dy = Math.max(0, (ch - diagramPxHeight * this.scale) / 2); + + // 6. Преобразуем в юниты + const diagram = this.diagramBounds; + + this.tx = diagramPxWidth / diagram.width * dx; + this.ty = diagramPxHeight / diagram.height * dy; + + this._applyTransform(); + } + + + refresh() { + this._scheduleRender(); + } +} + +/* --------------------------------------------------------- + EXPORT FUNCTION +--------------------------------------------------------- */ +function exportPng() { + const svgElement = document.querySelector('#diagramSvgContainer svg'); + if (!svgElement) { + alert('Диаграмма не найдена для экспорта'); + return; + } + + try { + const svgData = new XMLSerializer().serializeToString(svgElement); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const img = new Image(); + + // Get actual dimensions + const bbox = svgElement.getBBox(); + canvas.width = bbox.width * 2; + canvas.height = bbox.height * 2; + + const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' }); + const url = URL.createObjectURL(svgBlob); + + img.onload = function () { + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + + const pngUrl = canvas.toDataURL('image/png'); + const downloadLink = document.createElement('a'); + downloadLink.href = pngUrl; + downloadLink.download = `diagram_${new Date().toISOString().slice(0, 10)}.png`; + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + + URL.revokeObjectURL(url); + + // Show success toast + if (window.viewer && viewer._showToast) { + viewer._showToast('Диаграмма экспортирована как PNG', 'success'); + } + }; + + img.onerror = function () { + alert('Ошибка при экспорте изображения'); + URL.revokeObjectURL(url); + }; + + img.src = url; + } catch (error) { + console.error('Export error:', error); + alert('Ошибка при экспорте: ' + error.message); + } +} + +/* --------------------------------------------------------- + HANDLE SEARCH KEY PRESS +--------------------------------------------------------- */ +function handleSearchKeyPress(event) { + if (event.key === 'Enter') { + event.preventDefault(); + if (window.viewer && viewer.searchInput) { + const term = viewer.searchInput.value.trim(); + if (term) { + viewer.searchNodes(term); + } else { + viewer.clearSearch(); + } + } + } +} + +/* --------------------------------------------------------- + THEME CHANGE HANDLER +--------------------------------------------------------- */ +function setupThemeChangeHandler() { + const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + + darkModeMediaQuery.addEventListener('change', (e) => { + if (window.viewer && document.getElementById('mermaid').classList.contains('active')) { + viewer.render().catch(console.error); + } + }); +} + +/* --------------------------------------------------------- + MAIN INITIALIZATION +--------------------------------------------------------- */ +let viewer = null; + +document.addEventListener("DOMContentLoaded", () => { + window.reportData = JSON.parse(document.getElementById('report-data').textContent); + window.hasDiagram = true; + + // Инициализация отчета + if (window.initReport) { + window.initReport(); + } + + + initTabs((id) => { + if (id === "mermaid") { + if (!viewer) { + viewer = new DiagramViewer(); + viewer.render().catch(console.error); + } else { + viewer.refresh(); + } + } + }); + + // Auto-click first tab + document.querySelector(".tab.active").click(); + + // Setup theme change handler + setupThemeChangeHandler(); + + // Make viewer globally available for button callbacks + window.viewer = viewer; + window.exportPng = exportPng; + window.handleSearchKeyPress = handleSearchKeyPress; +}); \ No newline at end of file diff --git a/SQLLinter/SQLLinter.csproj b/SQLLinter/SQLLinter.csproj index 1872582..cc43d8f 100644 --- a/SQLLinter/SQLLinter.csproj +++ b/SQLLinter/SQLLinter.csproj @@ -23,6 +23,12 @@ + + + + + +