diff --git a/docs/app.js b/docs/app.js index 2eff11b..dc9d572 100644 --- a/docs/app.js +++ b/docs/app.js @@ -112,13 +112,79 @@ function saveIgnoredObjects() { } function addIgnoredObject(name) { + const options = arguments[1] || {}; + const deferSelectionMs = Number(options.deferSelectionMs) || 0; const n = String(name || '').trim().toUpperCase(); if (!n) return; if (userIgnoredSet.has(n)) return; userIgnoredSet.add(n); saveIgnoredObjects(); renderIgnoredList(); - render(); + + const refreshSelectionAndRender = () => { + const visible = filteredEntries(); + if (!visible.some((entry) => entry.id === state.selectedId)) { + state.selectedId = visible[0]?.id || null; + } + render(); + }; + + if (state.pendingIgnoreSelectionTimer) { + window.clearTimeout(state.pendingIgnoreSelectionTimer); + state.pendingIgnoreSelectionTimer = null; + } + + if (deferSelectionMs > 0) { + // Atualiza a lista imediatamente, mas preserva a selecao atual por um instante + // para tornar o feedback visual de "ignorar" perceptivel ao usuario. + render(); + state.pendingIgnoreSelectionTimer = window.setTimeout(() => { + state.pendingIgnoreSelectionTimer = null; + refreshSelectionAndRender(); + }, deferSelectionMs); + return; + } + + refreshSelectionAndRender(); +} + +function showIgnoreObjectFeedback(type = 'added') { + const objectBox = elements.metaObject?.closest('.status-item'); + if (objectBox) { + objectBox.classList.remove('status-item-feedback-added', 'status-item-feedback-existing'); + const feedbackClass = type === 'existing' ? 'status-item-feedback-existing' : 'status-item-feedback-added'; + objectBox.classList.add(feedbackClass); + window.setTimeout(() => { + objectBox.classList.remove(feedbackClass); + }, 700); + } + + if (!elements.ignoreObjectLink) { + return; + } + + const originalText = elements.ignoreObjectLink.textContent || 'ignorar'; + const originalTitle = elements.ignoreObjectLink.title || ''; + if (type === 'existing') { + elements.ignoreObjectLink.textContent = 'ja ignorado'; + elements.ignoreObjectLink.title = 'Este objeto ja esta na lista de ignorados'; + } else { + elements.ignoreObjectLink.textContent = 'ignorado'; + elements.ignoreObjectLink.title = 'Objeto adicionado a lista de ignorados'; + } + + window.setTimeout(() => { + const entry = selectedEntry(); + const objectName = (entry?.meta4Object || '').trim().toUpperCase(); + if (!objectName) { + elements.ignoreObjectLink.textContent = originalText; + elements.ignoreObjectLink.title = originalTitle; + return; + } + const isIgnored = userIgnoredSet.has(objectName); + elements.ignoreObjectLink.textContent = isIgnored ? 'ignorado' : 'ignorar'; + elements.ignoreObjectLink.title = isIgnored ? 'Este objeto ja esta na lista de ignorados' : 'Adicionar objeto atual a lista de ignorados'; + }, 1200); } function removeIgnoredObject(name) { @@ -155,6 +221,7 @@ const state = { files: [], loadedFiles: [], source: 'default', + pendingIgnoreSelectionTimer: null, selectedId: null, filterFile: 'all', search: '', @@ -175,6 +242,9 @@ const elements = { selectedFile: document.querySelector('#selectedFile'), selectedTitle: document.querySelector('#selectedTitle'), metaObject: document.querySelector('#metaObject'), + ignoreObjectLink: document.querySelector('#ignoreObjectLink'), + nodeName: document.querySelector('#nodeName'), + roleName: document.querySelector('#roleName'), organization: document.querySelector('#organization'), date: document.querySelector('#date'), duration: document.querySelector('#duration'), @@ -353,6 +423,16 @@ function looksLikeResultRow(line) { return /^("?[^"]+"?|-?[0-9]|NULL\b|\b)/i.test(trimmed); } +function normalizeOracleDateLiterals(sql) { + return String(sql || '') + .replace(/\{\s*d\s*'(\d{4})-(\d{2})-(\d{2})'\s*\}/gi, (_match, year, month, day) => { + return `to_date('${day}/${month}/${year}', 'DD/MM/YYYY')`; + }) + .replace(/\{\s*ts\s*'(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})'\s*\}/gi, (_match, year, month, day, hour, minute, second) => { + return `to_date('${year}-${month}-${day} ${hour}:${minute}:${second}', 'YYYY-MM-DD HH24:MI:SS')`; + }); +} + function normalizeSql(sql) { return sql.replace(/\s+/g, ' ').trim(); } @@ -479,10 +559,24 @@ function createEntry(fileName, index, headerLine) { recordCount: null, durationMs: null, threadId: '', + searchText: '', rawLines: [headerLine] }; } +function buildEntrySearchText(entry) { + return [ + entry.fileName, + entry.meta4Object, + entry.node, + entry.recordSet, + entry.connection?.organization, + entry.connection?.date, + entry.sql, + entry.rows.flat().join(' ') + ].join(' ').toLowerCase(); +} + function cloneEntryForStatement(entry, index, statementLine) { return { ...createEntry(entry.fileName, index, entry.rawLines[0]), @@ -496,6 +590,10 @@ function cloneEntryForStatement(entry, index, statementLine) { }; } +function isSqlTerminationLine(trimmed) { + return /^(No Data Found\b|Num Items\b|Num Registers\b|Calculating Time\.|APISQL Statement in TI\b|#-+|#\s*[A-Za-z_][A-Za-z0-9_\s#]*#)/i.test(trimmed); +} + function parseLogContent(content, fileName) { const lines = content.split(/\r?\n/); const entries = []; @@ -506,8 +604,9 @@ function parseLogContent(content, fileName) { if (!current) { return; } - current.sql = current.sql.trim(); + current.sql = normalizeOracleDateLiterals(current.sql.trim()); current.normalizedSql = normalizeSql(current.sql); + current.searchText = buildEntrySearchText(current); entries.push(current); current = null; readingSql = false; @@ -556,6 +655,11 @@ function parseLogContent(content, fileName) { return; } + if (readingSql && isSqlTerminationLine(trimmed)) { + finishCurrent(); + return; + } + if (looksLikeResultRow(trimmed)) { const row = splitResultRow(trimmed); if (current.rows.length === 0 && looksLikeHeaderRow(row)) { @@ -719,13 +823,16 @@ function highlightSql(sql) { 'NVL', 'TO_DATE', 'TRUNC', 'REGEXP_SUBSTR', 'SYSDATE', 'COUNT', 'SUM', 'MIN', 'MAX', 'AVG', 'SUBSTR', 'DECODE', 'COALESCE', 'TO_CHAR', 'TO_NUMBER' ]); - const tokenPattern = /(--[^\n]*|'(?:''|[^'])*'|\b\d+(?:\.\d+)?\b|\b[A-Za-z_][A-Za-z0-9_$#]*\b|<>|<=|>=|:=|[=+\-*/(),.;])/g; + const tokenPattern = /(--[^\n]*|"(?:""|[^"])*"|'(?:''|[^'])*'|\b\d+(?:\.\d+)?\b|\b[A-Za-z_][A-Za-z0-9_$#]*\b|<>|<=|>=|:=|[=+\-*/(),.;])/g; return String(sql || '').replace(tokenPattern, (token) => { const escaped = escapeHtml(token); if (token.startsWith('--')) { return `${escaped}`; } + if (token.startsWith('"')) { + return `${escaped}`; + } if (token.startsWith("'")) { return `${escaped}`; } @@ -766,16 +873,7 @@ function selectedEntry() { } function entrySearchText(entry) { - return [ - entry.fileName, - entry.meta4Object, - entry.node, - entry.recordSet, - entry.connection?.organization, - entry.connection?.date, - entry.sql, - entry.rows.flat().join(' ') - ].join(' ').toLowerCase(); + return entry.searchText || buildEntrySearchText(entry); } function entryDisplayTitle(entry) { @@ -924,6 +1022,13 @@ function renderSelectedEntry() { elements.selectedFile.textContent = 'Nenhum item selecionado'; elements.selectedTitle.textContent = 'Selecione uma query'; elements.metaObject.textContent = '-'; + if (elements.ignoreObjectLink) { + elements.ignoreObjectLink.classList.remove('is-visible', 'is-disabled'); + elements.ignoreObjectLink.removeAttribute('aria-disabled'); + elements.ignoreObjectLink.dataset.object = ''; + } + elements.nodeName.textContent = '-'; + elements.roleName.textContent = '-'; elements.organization.textContent = '-'; elements.date.textContent = '-'; elements.duration.textContent = '-'; @@ -938,7 +1043,21 @@ function renderSelectedEntry() { elements.selectedFile.textContent = `${entry.fileName} | linha ${entry.lineNumber}`; elements.selectedTitle.textContent = entryDisplayTitle(entry); - elements.metaObject.textContent = entry.meta4Object || '-'; + const objectName = entry.meta4Object || '-'; + const normalizedObjectName = String(objectName).trim().toUpperCase(); + const isIgnoredObject = !!normalizedObjectName && normalizedObjectName !== '-' && userIgnoredSet.has(normalizedObjectName); + elements.metaObject.textContent = objectName; + if (elements.ignoreObjectLink) { + const shouldShowLink = !!normalizedObjectName && normalizedObjectName !== '-'; + elements.ignoreObjectLink.dataset.object = shouldShowLink ? objectName : ''; + elements.ignoreObjectLink.classList.toggle('is-visible', shouldShowLink); + elements.ignoreObjectLink.classList.toggle('is-disabled', isIgnoredObject); + elements.ignoreObjectLink.setAttribute('aria-disabled', isIgnoredObject ? 'true' : 'false'); + elements.ignoreObjectLink.textContent = isIgnoredObject ? 'ignorado' : 'ignorar'; + elements.ignoreObjectLink.title = isIgnoredObject ? 'Este objeto ja esta na lista de ignorados' : 'Adicionar objeto atual a lista de ignorados'; + } + elements.nodeName.textContent = entry.node || '-'; + elements.roleName.textContent = entry.connection?.role || '-'; elements.organization.textContent = entry.connection?.organization || '-'; elements.date.textContent = formatDate(entry.connection?.date); elements.duration.textContent = entry.durationMs == null ? '-' : `${entry.durationMs} ms`; @@ -946,7 +1065,8 @@ function renderSelectedEntry() { const sqlText = currentSqlText(entry); elements.sqlOutput.innerHTML = sqlText ? highlightSql(sqlText) : 'Sem SQL capturado.'; updateFormatSqlButton(); - elements.rawOutput.textContent = (entry.rawLines || []).join('\n'); + const rawText = (entry.rawLines || []).join('\n'); + elements.rawOutput.innerHTML = rawText ? highlightSql(rawText) : ''; renderTable(entry); } @@ -1098,11 +1218,17 @@ elements.onlyRealStmt.addEventListener('change', () => { elements.searchInput.addEventListener('input', () => { state.search = elements.searchInput.value; - const visible = filteredEntries(); - if (!visible.some((entry) => entry.id === state.selectedId)) { - state.selectedId = visible[0]?.id || null; + if (state.searchRenderTimer) { + window.clearTimeout(state.searchRenderTimer); } - render(); + state.searchRenderTimer = window.setTimeout(() => { + state.searchRenderTimer = null; + const visible = filteredEntries(); + if (!visible.some((entry) => entry.id === state.selectedId)) { + state.selectedId = visible[0]?.id || null; + } + render(); + }, 140); }); elements.fileInput.addEventListener('change', () => { @@ -1184,6 +1310,23 @@ if (elements.addIgnoredButton) { }); } +if (elements.ignoreObjectLink) { + elements.ignoreObjectLink.addEventListener('click', (event) => { + event.preventDefault(); + const objectName = elements.ignoreObjectLink.dataset.object || selectedEntry()?.meta4Object || ''; + if (!objectName) { + return; + } + const alreadyIgnored = userIgnoredSet.has(String(objectName).trim().toUpperCase()); + if (alreadyIgnored) { + showIgnoreObjectFeedback('existing'); + return; + } + addIgnoredObject(objectName, { deferSelectionMs: 1000 }); + showIgnoreObjectFeedback('added'); + }); +} + if (elements.ignoredList) { elements.ignoredList.addEventListener('click', (ev) => { const btn = ev.target.closest('.remove-ignored'); @@ -1228,28 +1371,37 @@ if (elements.formatSqlButton) { }); } -elements.copyButton.addEventListener('click', async () => { - const entry = selectedEntry(); - if (!entry?.sql) { - return; - } - await navigator.clipboard.writeText(currentSqlText(entry)); - const original = elements.copyButton.textContent; - elements.copyButton.textContent = 'Copiado'; - window.setTimeout(() => { - elements.copyButton.textContent = original; - }, 1200); -}); +if (elements.copyButton) { + elements.copyButton.addEventListener('click', async () => { + const entry = selectedEntry(); + if (!entry?.sql) { + return; + } + await navigator.clipboard.writeText(currentSqlText(entry)); + const original = elements.copyButton.textContent; + elements.copyButton.textContent = 'Copiado'; + window.setTimeout(() => { + elements.copyButton.textContent = original; + }, 1200); + }); +} -elements.refreshButton.addEventListener('click', reloadLoadedFiles); +if (elements.refreshButton) { + elements.refreshButton.addEventListener('click', reloadLoadedFiles); +} async function loadAppVersion() { + const appVersionElement = document.querySelector('#appVersion'); + if (!appVersionElement) { + return; + } + try { const response = await fetch('version.json', { cache: 'no-store' }); if (response.ok) { const data = await response.json(); const version = data.version || '0.0.0'; - document.querySelector('#appVersion').textContent = `v${version}`; + appVersionElement.textContent = `v${version}`; return; } } catch (e) { @@ -1258,7 +1410,7 @@ async function loadAppVersion() { const meta = document.querySelector('meta[name="app-version"]'); const v = (meta && meta.getAttribute('content')) || '0.0.0'; - document.querySelector('#appVersion').textContent = `v${v}`; + appVersionElement.textContent = `v${v}`; } initializeTheme(); diff --git a/docs/index.html b/docs/index.html index 5aa69c6..cfa0c6d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,151 +3,173 @@ - - M4 Log Visualizer - Análise de Logs Meta4, PeopleNet e Cegid - - + + + + M4 Log Visualizer – Visualizador de Logs Meta4, PeopleNet e Cegid + + - - - - + + + + - + + + + + - - - + + + + + + - -
-
-

Resultado retornado no log

- -
-
-
-
-
+
+
+

Nenhum item selecionado

+

Selecione uma query

+
+ + Objeto + + - + ignorar + + + Node- + Role- + Data- + Tempo- + + +
+
-
-
-

Trecho original

-
-

-      
+
+ +
+
+

SQL Oracle

+
+ + +
+
+
Aguardando seleção...
+
+ +
+
+

Resultado retornado no log

+ +
+
+
+
+
-
+ + + + + + diff --git a/docs/robots.txt b/docs/robots.txt index e4c18ff..92774de 100644 --- a/docs/robots.txt +++ b/docs/robots.txt @@ -1,10 +1,4 @@ -# Robots.txt for M4 Log Visualizer User-agent: * Allow: / -Allow: /M4LogVisualizer/ -# Sitemaps Sitemap: https://abrusco.github.io/M4LogVisualizer/sitemap.xml - -# Crawl delay -Crawl-delay: 1 diff --git a/docs/sitemap.xml b/docs/sitemap.xml index 91caace..ea131ac 100644 --- a/docs/sitemap.xml +++ b/docs/sitemap.xml @@ -2,8 +2,7 @@ https://abrusco.github.io/M4LogVisualizer/ - 2026-08-03 - weekly + monthly 1.0 diff --git a/docs/styles.css b/docs/styles.css index 4c889ae..7bdffca 100644 --- a/docs/styles.css +++ b/docs/styles.css @@ -1,144 +1,228 @@ -:root { +/* ═══════════════════════════════════════════════════════ + M4 Log Visualizer — Carbon Split design + ═══════════════════════════════════════════════════════ */ + +/* ── Design tokens ──────────────────────────────────── */ +:root { color-scheme: light; - --bg: #f4f7f8; - --panel: #ffffff; - --panel-soft: #eef4f5; - --sidebar: #ffffff; - --control: #ffffff; - --control-hover: #f8fbfb; - --table-head: #f8fbfb; - --raw-bg: #fbfcfc; - --raw-text: #3b4649; - --text: #172022; - --muted: #647276; - --line: #d9e2e5; - --accent: #176b72; - --accent-strong: #0d4f55; - --orange: #b84f18; - --shadow: 0 18px 45px rgba(20, 45, 50, 0.10); - font-family: "Segoe UI", Arial, sans-serif; + --bg: #f6f8fa; + --panel: #ffffff; + --panel-soft: #f0f3f6; + --sidebar: #f6f8fa; + --control: #ffffff; + --control-hover: #f0f3f6; + --table-head: #f6f8fa; + --raw-bg: #f8f9fa; + --raw-text: #3b4649; + --text: #1f2328; + --muted: #656d76; + --line: #d0d7de; + --accent: #0969da; + --accent-strong: #0550ae; + --orange: #bc4c00; + --shadow: 0 1px 3px rgba(31,35,40,.12), 0 4px 12px rgba(31,35,40,.06); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; } [data-theme="dark"] { color-scheme: dark; - --bg: #0f1416; - --panel: #171d20; - --panel-soft: #1f2b2e; - --sidebar: #12181b; - --control: #151c1f; - --control-hover: #1b2528; - --table-head: #1c2528; - --raw-bg: #111719; - --raw-text: #d2dcdf; - --text: #edf3f4; - --muted: #9aabad; - --line: #2c3a3e; - --accent: #3aaeb7; - --accent-strong: #75d3da; - --orange: #d56f2d; - --shadow: 0 18px 45px rgba(0, 0, 0, 0.36); -} -* { - box-sizing: border-box; -} - -body { - height: 100vh; - margin: 0; - display: grid; - grid-template-columns: minmax(320px, 420px) 1fr; - background: var(--bg); - color: var(--text); - overflow: hidden; -} - -button, -input { - font: inherit; -} - + --bg: #0d1117; + --panel: #161b22; + --panel-soft: #1c2128; + --sidebar: #0d1117; + --control: #21262d; + --control-hover: #30363d; + --table-head: #1c2128; + --raw-bg: #0d1117; + --raw-text: #c9d1d9; + --text: #e6edf3; + --muted: #8b949e; + --line: #30363d; + --accent: #58a6ff; + --accent-strong: #79c0ff; + --orange: #e3b341; + --shadow: 0 1px 3px rgba(0,0,0,.4), 0 4px 16px rgba(0,0,0,.3); +} + +/* ── Reset ──────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; } +body { margin: 0; } +h1, h2, h3, h4, p { margin: 0; } +ul { margin: 0; padding: 0; list-style: none; } +button, input { font: inherit; } + +/* ── Global button base ─────────────────────────────── */ button { - min-height: 38px; + min-height: 36px; border: 1px solid var(--line); border-radius: 6px; background: var(--control); color: var(--text); cursor: pointer; + transition: border-color .12s, background .12s; } +button:hover { border-color: var(--accent); } -button:hover { - border-color: var(--accent); +/* ── Layout ─────────────────────────────────────────── */ +body { + height: 100vh; + display: grid; + grid-template-rows: 52px 1fr; + background: var(--bg); + color: var(--text); + overflow: hidden; } -.sidebar { - height: 100vh; +.app-body { + display: grid; + grid-template-columns: minmax(300px, 380px) 1fr; min-height: 0; - border-right: 1px solid var(--line); - background: var(--sidebar); - display: flex; - flex-direction: column; overflow: hidden; } -.brand { +/* ── Top bar ────────────────────────────────────────── */ +.topbar { display: flex; - gap: 12px; align-items: center; - padding: 18px; - border-bottom: 1px solid var(--line); + padding: 0 10px; + gap: 0; + background: #24292f; + border-bottom: 1px solid #30363d; + overflow: hidden; } -.brand-title-row { +.topbar-brand { display: flex; - align-items: baseline; - gap: 10px; -} - -.app-version { - font-size: 13px; - color: var(--muted); - background: var(--panel-soft); - padding: 4px 8px; - border-radius: 999px; - border: 1px solid var(--line); + align-items: center; + gap: 9px; + flex-shrink: 0; + padding-right: 16px; + margin-right: 4px; + border-right: 1px solid rgba(255,255,255,.12); } .brand-mark { - width: 44px; - height: 44px; + width: 30px; + height: 30px; display: grid; place-items: center; - border-radius: 8px; + border-radius: 6px; background: var(--accent); color: #fff; font-weight: 800; + font-size: 12px; + flex-shrink: 0; } -h1, -h2, -h3, -p { - margin: 0; +.brand-name { + font-size: 14px; + font-weight: 600; + color: #e6edf3; + white-space: nowrap; } -h1 { - font-size: 20px; +.app-version { + font-size: 11px; + color: #8b949e; + background: rgba(255,255,255,.07); + padding: 2px 7px; + border-radius: 999px; + border: 1px solid rgba(255,255,255,.12); + white-space: nowrap; } -.brand p, -.eyebrow, -.summary, -.details-grid span, -.panel-header span { - color: var(--muted); +.topbar-title { + flex: 1; + min-width: 0; + padding: 0 14px; + overflow: hidden; +} + +.topbar-title .eyebrow { + font-size: 11px; + color: #8b949e; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.topbar-title h2 { + font-size: 14px; + font-weight: 600; + color: #e6edf3; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 2px; +} + +.topbar-actions { + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; + flex-shrink: 0; + padding-left: 14px; + border-left: 1px solid rgba(255,255,255,.12); +} + +/* Override generic button for topbar */ +.topbar-actions button { + height: 30px; + min-height: 30px; + padding: 0 10px; + border: 1px solid rgba(255,255,255,.18); + border-radius: 6px; + background: rgba(255,255,255,.06); + color: #e6edf3; font-size: 12px; } +.topbar-actions button:hover { + background: rgba(255,255,255,.14); + border-color: rgba(255,255,255,.3); +} + +.topbar-actions .icon-button { + width: 32px; + min-width: 32px; + padding: 0; + display: inline-grid; + place-items: center; + font-size: 18px; +} + +.theme-icon { + width: 22px; + height: 22px; + display: inline-grid; + place-items: center; + font-size: 18px; + line-height: 1; +} + +/* ── Sidebar ────────────────────────────────────────── */ +.sidebar { + height: 100%; + min-height: 0; + border-right: 1px solid var(--line); + background: var(--sidebar); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.sidebar-top { + flex-shrink: 0; + padding: 10px 10px 0; + display: flex; + flex-direction: column; + gap: 7px; +} .file-loader { - margin: 14px 18px 0; - padding: 12px; + padding: 9px 10px; border: 1px dashed var(--line); - border-radius: 8px; + border-radius: 7px; background: var(--control-hover); } @@ -149,10 +233,11 @@ h1 { .file-loader label { display: block; - margin-bottom: 8px; - font-size: 12px; + margin-bottom: 6px; + font-size: 11px; font-weight: 700; text-transform: uppercase; + letter-spacing: .05em; color: var(--muted); } @@ -161,50 +246,81 @@ h1 { font-size: 12px; } -.file-loader p { - margin-top: 8px; +.file-loader input[type="file"] { + display: block; + padding: 5px 0; color: var(--muted); - font-size: 12px; - line-height: 1.35; } -.file-loader strong { +.file-loader input[type="file"]::file-selector-button { + margin-right: 10px; + height: 30px; + padding: 0 12px; + border: 1px solid var(--line); + border-radius: 6px; + background: linear-gradient(180deg, var(--control) 0%, var(--control-hover) 100%); color: var(--text); + cursor: pointer; + transition: border-color .12s, background .12s, transform .12s; +} + +.file-loader input[type="file"]::file-selector-button:hover { + border-color: var(--accent); + background: var(--panel-soft); } -.search-box { - padding: 16px 18px 12px; + +.file-loader input[type="file"]::file-selector-button:active { + transform: translateY(1px); } -.search-box label { - display: block; - margin-bottom: 8px; - font-size: 12px; - font-weight: 700; - text-transform: uppercase; +.file-loader p { + margin-top: 5px; color: var(--muted); + font-size: 11px; + font-family: Consolas, "Courier New", monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .search-box input { width: 100%; - height: 42px; + height: 34px; border: 1px solid var(--line); border-radius: 6px; - padding: 0 12px; + padding: 0 10px; outline: none; background: var(--control); color: var(--text); + font-size: 13px; } - +.search-box input::placeholder { color: var(--muted); } .search-box input:focus { border-color: var(--accent); - box-shadow: 0 0 0 3px rgba(23, 107, 114, 0.14); + box-shadow: 0 0 0 3px rgba(9, 105, 218, .18); +} +[data-theme="dark"] .search-box input:focus { + box-shadow: 0 0 0 3px rgba(88, 166, 255, .18); } .filters { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)) 40px; - gap: 8px; - padding: 0 18px 14px; + grid-template-columns: repeat(3, minmax(72px, 1fr)) 34px; + gap: 5px; +} + +.filters button { + height: 30px; + min-height: 30px; + padding: 0; + font-size: 12px; + border: 1px solid var(--line); + border-radius: 5px; + background: var(--control); + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .filter.active { @@ -213,13 +329,10 @@ h1 { color: #fff; } - .sort-button { - min-width: 40px; - width: 40px; - padding: 0; - font-size: 18px; - line-height: 1; + width: 34px; + min-width: 34px; + font-size: 16px; } .sort-button.active { @@ -227,158 +340,140 @@ h1 { border-color: var(--accent); color: #fff; } + .system-filter { - margin: 0 18px 12px; display: flex; align-items: center; - gap: 8px; + gap: 7px; color: var(--muted); - font-size: 13px; + font-size: 12px; cursor: pointer; + user-select: none; + padding-bottom: 2px; } .system-filter input { - width: 16px; - height: 16px; + width: 14px; + height: 14px; accent-color: var(--accent); + cursor: pointer; } + .summary { - padding: 0 18px 12px; + color: var(--muted); + font-size: 11px; + line-height: 1.4; + padding-bottom: 8px; + border-bottom: 1px solid var(--line); } .entry-list { flex: 1; min-height: 0; overflow: auto; - padding: 0 10px 18px; + overflow-x: hidden; + padding: 6px 6px 16px; } .entry-item { width: 100%; - min-height: 82px; - margin-bottom: 8px; - padding: 10px; + margin-bottom: 2px; + padding: 7px 9px; display: block; text-align: left; - border-radius: 8px; + border-radius: 6px; + border: 1px solid transparent; + background: transparent; + cursor: pointer; + min-height: unset; + overflow: hidden; +} + +.entry-item:hover { + background: var(--control-hover); + border-color: var(--line); } .entry-item.active { - border-color: var(--accent); background: var(--panel-soft); + border-color: var(--accent); } .entry-title { display: flex; justify-content: space-between; - gap: 10px; - font-weight: 700; + align-items: baseline; + gap: 8px; + font-weight: 600; + font-size: 13px; + line-height: 1.3; +} + +.entry-title > span:last-child { + font-size: 11px; + font-weight: 400; + color: var(--muted); + white-space: nowrap; + flex-shrink: 0; } .entry-meta, .entry-sql { display: block; - margin-top: 6px; + margin-top: 3px; color: var(--muted); - font-size: 12px; - line-height: 1.35; + font-size: 11px; + line-height: 1.3; } .entry-sql { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + font-family: Consolas, "Courier New", monospace; } +/* ── Viewer ─────────────────────────────────────────── */ .viewer { - height: 100vh; + height: 100%; min-width: 0; - padding: 22px; - overflow: auto; -} - -.toolbar { - position: sticky; - top: -22px; - z-index: 5; - background: var(--bg); - padding-top: 22px; display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 16px; - margin-bottom: 16px; -} - -.toolbar h2 { - margin-top: 3px; - font-size: 26px; + flex-direction: column; + overflow: hidden; + background: var(--bg); } -.actions { - display: flex; - gap: 8px; - flex-wrap: wrap; - justify-content: flex-end; +.viewer-meta { + flex-shrink: 0; + padding: 10px 14px 0; + border-bottom: 1px solid var(--line); + background: var(--panel); } - -.icon-button { - width: 40px; - min-width: 40px; - padding: 0; - display: inline-grid; - place-items: center; +.viewer-meta-file { + font-size: 13px; + color: var(--muted); + margin-bottom: 4px; } -.theme-icon { - width: 22px; - height: 22px; - display: inline-grid; - place-items: center; - color: var(--text); +.viewer-meta-title { font-size: 20px; - line-height: 1; -} -.actions button { - padding: 0 14px; -} - -#themeButton { - padding: 0; -} - -#copyButton { - background: var(--orange); - border-color: var(--orange); - color: #fff; -} - -.details-grid { - display: grid; - grid-template-columns: repeat(5, minmax(120px, 1fr)); - gap: 10px; - margin-bottom: 16px; -} - -.details-grid div { - min-width: 0; - padding: 12px; - background: var(--panel); - border: 1px solid var(--line); - border-radius: 8px; + line-height: 1.1; + font-weight: 700; + letter-spacing: .01em; + margin-bottom: 8px; } -.details-grid strong { - display: block; - margin-top: 4px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +.viewer-scroll { + flex: 1; + min-height: 0; + overflow: auto; + padding: 14px; } +/* ── Panels ─────────────────────────────────────────── */ .panel { - margin-bottom: 16px; + margin-bottom: 10px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; @@ -387,23 +482,73 @@ h1 { } .panel-header { - min-height: 48px; - padding: 12px 14px; + min-height: 42px; + padding: 7px 12px; display: flex; justify-content: space-between; align-items: center; gap: 12px; border-bottom: 1px solid var(--line); + background: var(--panel-soft); } +.panel h3 { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .06em; + color: var(--muted); +} .panel-action { - min-height: 32px; - padding: 0 10px; + height: 28px; + min-height: 28px; + padding: 0 11px; font-size: 12px; + border: 1px solid var(--line); + border-radius: 5px; + background: var(--control); + color: var(--text); } -.panel h3 { - font-size: 15px; +.panel-action:hover { border-color: var(--accent); } + +#formatSqlButton { + background: #0f766e; + border-color: #0f766e; + color: #fff; + font-weight: 600; + box-shadow: 0 1px 0 rgba(31, 35, 40, .08); +} + +#formatSqlButton:hover { + background: #0b5f59; + border-color: #0b5f59; +} + +#formatSqlButton:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(9, 105, 218, .25); +} + +[data-theme="dark"] #formatSqlButton:focus-visible { + box-shadow: 0 0 0 3px rgba(88, 166, 255, .25); +} + +[data-theme="dark"] #formatSqlButton { + background: #1b9a8b; + border-color: #1b9a8b; + color: #f0fffb; +} + +[data-theme="dark"] #formatSqlButton:hover { + background: #147e72; + border-color: #147e72; +} + +.panel-actions { + display: flex; + align-items: center; + gap: 8px; } pre { @@ -412,125 +557,379 @@ pre { overflow: auto; white-space: pre-wrap; word-break: break-word; - font-family: Consolas, "Courier New", monospace; + font-family: Consolas, "Cascadia Code", "Courier New", monospace; font-size: 13px; - line-height: 1.5; + line-height: 1.6; + color: var(--text); } .sql-panel pre { - max-height: 310px; - white-space: pre-wrap; + max-height: min(56vh, 520px); overflow-wrap: anywhere; - word-break: break-word; } +/* ── SQL syntax highlighting ────────────────────────── */ +.sql-keyword { color: #cf222e; font-weight: 700; } +.sql-function { color: #8250df; font-weight: 600; } +.sql-string { color: #0a3069; } +.sql-identifier { color: #953800; font-weight: 600; } +.sql-number { color: #0550ae; } +.sql-comment { color: #57606a; font-style: italic; } +.sql-operator { color: #6e7781; font-weight: 600; } + +[data-theme="dark"] .sql-keyword { color: #ff7b72; font-weight: 700; } +[data-theme="dark"] .sql-function { color: #d2a8ff; font-weight: 600; } +[data-theme="dark"] .sql-string { color: #a5d6ff; } +[data-theme="dark"] .sql-identifier { color: #ffa657; font-weight: 600; } +[data-theme="dark"] .sql-number { color: #79c0ff; } +[data-theme="dark"] .sql-comment { color: #8b949e; font-style: italic; } +[data-theme="dark"] .sql-operator { color: #8b949e; font-weight: 600; } + +/* ── Raw output ─────────────────────────────────────── */ +.raw-panel pre { + max-height: 240px; + color: var(--raw-text); + background: var(--raw-bg); + font-size: 12px; +} -.sql-keyword { - color: #0b5cad; - font-weight: 700; +/* ── Result table ───────────────────────────────────── */ +.table-wrap { width: 100%; overflow: auto; } + +table { width: 100%; border-collapse: collapse; font-size: 12px; } + +th, td { + padding: 7px 12px; + border-bottom: 1px solid var(--line); + text-align: left; + vertical-align: top; } -.sql-function { - color: #7b3fb2; +tbody tr:nth-child(even) { + background: color-mix(in srgb, var(--panel-soft) 65%, transparent); +} + +th { + position: sticky; + top: 0; + background: var(--table-head); + color: var(--muted); + font-size: 11px; font-weight: 700; + text-transform: uppercase; + letter-spacing: .04em; } -.sql-string { - color: #b3261e; +td { font-family: Consolas, "Courier New", monospace; } + +.empty { padding: 16px; color: var(--muted); font-size: 13px; } + +/* ── Status bar ─────────────────────────────────────── */ +.status-bar { + flex-shrink: 0; + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 6px; + padding: 0 0 8px; } -.sql-number { - color: #8a4b00; +.status-item { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + border: 1px solid var(--line); + border-radius: 8px; + background: var(--panel-soft); + padding: 6px 10px; + min-height: 50px; } -.sql-comment { - color: #617176; - font-style: italic; +.status-label { + font-size: 9px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .05em; + color: var(--muted); } -.sql-operator { - color: #0b6f64; +.status-item strong { + display: block; + width: 100%; font-weight: 700; + font-size: 13px; + line-height: 1.05; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-variant-numeric: tabular-nums; } -[data-theme="dark"] .sql-keyword { - color: #7db7ff; +.status-item-primary { + background: #f4f8ff; + border-color: #b8cceb; + box-shadow: inset 0 0 0 1px rgba(9, 105, 218, .06); + transition: box-shadow .18s ease, border-color .18s ease, background-color .18s ease; } -[data-theme="dark"] .sql-function { - color: #d6a7ff; +.status-item-primary strong { + font-size: 14px; } -[data-theme="dark"] .sql-string { - color: #ff9f8f; +.status-object-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + width: 100%; } -[data-theme="dark"] .sql-number { - color: #f0c36a; +.status-object-row strong { + flex: 1; + min-width: 0; } -[data-theme="dark"] .sql-comment { - color: #7f9296; +.status-ignore-link { + display: none; + flex: 0 0 auto; + color: var(--accent); + text-decoration: underline; + text-underline-offset: 2px; + font-size: 12px; + line-height: 1; } -[data-theme="dark"] .sql-operator { - color: #58d8c9; +.status-ignore-link.is-visible { + display: inline; } -.raw-panel pre { - max-height: 260px; - color: var(--raw-text); - background: var(--raw-bg); + +.status-ignore-link.is-disabled { + color: var(--muted); + cursor: default; + pointer-events: none; + text-decoration: none; } -.table-wrap { - width: 100%; - overflow: auto; +.status-item-feedback-added { + border-color: #3ca66b; + box-shadow: inset 0 0 0 1px rgba(60, 166, 107, .25), 0 0 0 3px rgba(60, 166, 107, .12); + background: #eefbf3; } -table { - width: 100%; - border-collapse: collapse; - font-size: 13px; +.status-item-feedback-existing { + border-color: #ca7b1b; + box-shadow: inset 0 0 0 1px rgba(202, 123, 27, .2), 0 0 0 3px rgba(202, 123, 27, .1); + background: #fff7ea; } -th, -td { - padding: 10px 12px; - border-bottom: 1px solid var(--line); - text-align: left; - vertical-align: top; +[data-theme="dark"] .status-item-feedback-added { + border-color: #3ca66b; + box-shadow: inset 0 0 0 1px rgba(60, 166, 107, .35), 0 0 0 3px rgba(60, 166, 107, .15); + background: #173226; } -th { - position: sticky; +[data-theme="dark"] .status-item-feedback-existing { + border-color: #ca7b1b; + box-shadow: inset 0 0 0 1px rgba(202, 123, 27, .3), 0 0 0 3px rgba(202, 123, 27, .12); + background: #352712; +} + +.status-item-compact { + padding-right: 8px; +} + +.status-item-compact strong { + font-size: 12px; +} + +[data-theme="dark"] .status-item-primary { + background: #1b2535; + border-color: #2f4f75; + box-shadow: inset 0 0 0 1px rgba(88, 166, 255, .18); +} + +.status-hidden { + display: none; +} + +/* ── Settings drawer ────────────────────────────────── */ +.settings-panel { + position: fixed; top: 0; - background: var(--table-head); + right: 0; + width: 400px; + max-width: 100%; + height: 100vh; + z-index: 80; + background: var(--panel); + border-left: 1px solid var(--line); + box-shadow: -8px 0 32px rgba(0,0,0,.22); + display: flex; + flex-direction: column; + transform: translateX(0); + transition: transform .2s ease, visibility .2s; + visibility: visible; +} + +.settings-panel.hidden { + transform: translateX(100%); + visibility: hidden; + pointer-events: none; +} + +.settings-panel .panel-header { + flex-shrink: 0; +} + +.settings-content { + flex: 1; + min-height: 0; + overflow: auto; + padding: 14px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.settings-content > label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .05em; color: var(--muted); - font-size: 12px; +} + +.add-row { + display: flex; + gap: 8px; +} + +.add-row input { + flex: 1; + height: 34px; + padding: 0 10px; + border: 1px solid var(--line); + border-radius: 6px; + background: var(--control); + color: var(--text); + font-size: 13px; +} +.add-row input:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(9, 105, 218, .18); +} +[data-theme="dark"] .add-row input:focus { + box-shadow: 0 0 0 3px rgba(88, 166, 255, .18); +} + +.add-row button { + height: 34px; + min-height: 34px; + padding: 0 12px; +} + +.ignored-list-wrap { + flex: 1; + min-height: 100px; + max-height: 360px; + overflow: auto; + border: 1px solid var(--line); + border-radius: 6px; + background: var(--panel-soft); + padding: 8px; +} + +.ignored-list-wrap h4 { + font-size: 11px; + font-weight: 700; text-transform: uppercase; + letter-spacing: .04em; + color: var(--muted); + margin: 0 0 8px; + padding-bottom: 6px; + border-bottom: 1px solid var(--line); +} + +.ignored-list { + display: flex; + flex-direction: column; + gap: 4px; } -td { +.ignored-list li { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + padding: 5px 8px; + border-radius: 5px; + background: var(--control); + border: 1px solid var(--line); + font-size: 12px; font-family: Consolas, "Courier New", monospace; } -.empty { - padding: 20px; +.ignored-list .remove-ignored { + min-width: 26px; + height: 26px; + min-height: 26px; + padding: 0 5px; + font-size: 12px; + border: 1px solid var(--line); + border-radius: 4px; + background: var(--control); color: var(--muted); + flex-shrink: 0; +} +.ignored-list .remove-ignored:hover { + border-color: #cf222e; + color: #cf222e; + background: var(--control); +} + +.settings-actions { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +.settings-actions button { + height: 34px; + min-height: 34px; + padding: 0 14px; + font-size: 13px; +} + +/* ── Backdrop ───────────────────────────────────────── */ +#settingsBackdrop { + position: fixed; + inset: 0; + background: rgba(0,0,0,.4); + z-index: 70; } +#settingsBackdrop.hidden { display: none; } +/* ── Responsive ─────────────────────────────────────── */ @media (max-width: 980px) { body { - grid-template-columns: 1fr; + grid-template-rows: 52px auto; height: auto; min-height: 100vh; overflow: auto; } + .app-body { + grid-template-columns: 1fr; + overflow: visible; + height: auto; + } + .sidebar { height: auto; - min-height: auto; - border-right: 0; + overflow: visible; + border-right: none; border-bottom: 1px solid var(--line); } @@ -539,76 +938,26 @@ td { overflow: visible; } - .toolbar { - position: static; - padding-top: 0; - } - - .entry-list { - max-height: 320px; + .viewer-scroll { + overflow: visible; + height: auto; + flex: unset; } - .details-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } + .entry-list { max-height: 360px; } + .viewer-meta-title { font-size: 20px; } + .status-bar { grid-template-columns: repeat(3, minmax(0, 1fr)); } } -@media (max-width: 560px) { - .toolbar, - .panel-header { - display: block; - } - - .actions { - margin-top: 12px; - justify-content: flex-start; - } - - .filters, - .details-grid { - grid-template-columns: 1fr; - } - - .viewer { - padding: 14px; - } +@media (max-width: 600px) { + .brand-name { display: none; } + .filters { grid-template-columns: 1fr 1fr 34px; } + .viewer-scroll { padding: 10px; } + .viewer-meta { padding: 8px 10px 0; } + .viewer-meta-file { font-size: 12px; } + .viewer-meta-title { font-size: 17px; margin-bottom: 8px; } + .status-item { min-height: 48px; padding: 6px 9px; } + .status-label { font-size: 10px; } + .status-item strong { font-size: 13px; } + .status-item-primary strong { font-size: 14px; } } - -/* Settings panel */ -.settings-panel.hidden { display: none; } -.settings-panel { - position: fixed; - right: 24px; - top: 80px; - width: 420px; - max-width: calc(100% - 48px); - z-index: 80; - box-shadow: 0 20px 50px rgba(0,0,0,0.35); -} -.settings-panel .settings-content { padding: 14px; display: flex; flex-direction: column; gap: 12px; } -.settings-panel .add-row { display: flex; gap: 8px; } -.settings-panel .add-row input { flex: 1; padding: 8px; border: 1px solid var(--line); border-radius: 6px; background: var(--control); color: var(--text); } -.settings-panel .ignored-list-wrap { max-height: 260px; overflow: auto; border: 1px solid var(--line); background: var(--panel); padding: 10px; border-radius: 6px; } -.ignored-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; } -.ignored-list li { display: flex; justify-content: space-between; gap: 8px; align-items: center; padding: 8px; border-radius: 6px; background: var(--panel-soft); border: 1px solid var(--line); } -.ignored-list button { min-width: 32px; padding: 6px 8px; } -.settings-actions { display: flex; gap: 8px; justify-content: flex-end; } - -/* Backdrop used by settings panel */ -#settingsBackdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.36); z-index: 70; } -#settingsBackdrop.hidden { display: none; } - - - - - - - - - - - - - - - diff --git a/docs/version.json b/docs/version.json index f58a78c..d0024c3 100644 --- a/docs/version.json +++ b/docs/version.json @@ -1,3 +1,3 @@ { - "version": "0.9.2" + "version": "0.9.5" }