diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index e64cefa..dee362d 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -9,6 +9,7 @@ - Project checks parse bounded Python/Node metadata and count bounded metadata-light Python files. Project scripts and executables are never run; metadata symlinks are refused. - GUI findings, project read notes, concise Copy for AI, and the technical report are localized for `zh-CN` and `en-US`. - Concise and technical reports use the same sanitizer for project/home/arbitrary-drive/UNC paths, URLs, credentials, and common token formats. +- The formal UI branch `codex/devdeck-light` applies the owner-approved DevDeck Light shell with P2 Cool Scientific tokens. The result view adds a Finding navigator, primary finding hierarchy, Current vs Required comparison, configuration/terminal evidence blocks, next-step callout, share bar, technical details, and compact status bar. This is presentation-only and does not expand diagnosis scope. ## Public remote and Preview 1 @@ -28,13 +29,14 @@ - README download guidance is version-independent: select the newest Preview marked Pre-release, then choose the stable setup/portable/MSI filename suffix and checksum file from that Release. - No Preview 2 tag or Release exists; publishing `v0.1.0-preview.2` is the next release step. -## Validation (2026-08-20) +## Validation (2026-08-22) - `cargo fmt --all -- --check`: passed. - `cargo test --all-targets`: 33 passed, 0 failed. - `cargo clippy --all-targets --all-features -- -D warnings`: passed. - `npm test`: 2 passed, 0 failed. - `npm run build`: passed. +- DevDeck Light branch: `npm test` 3 passed, `npm run build` passed, `cargo fmt --all -- --check` passed, `cargo test --all-targets` 33 passed, `cargo clippy --all-targets --all-features -- -D warnings` passed, `npm audit --audit-level=high` 0 vulnerabilities, and `npm run tauri:build` produced release EXE, NSIS setup, and MSI bundles. - `npm audit`: 0 vulnerabilities. - `npm run tauri build`: release EXE, NSIS setup, and MSI produced. - PR #2 required CI run `32360211432`: all required steps, including Windows release bundles, passed before merge. @@ -55,12 +57,12 @@ ## Next priorities -1. Use the committed Preview 2 notes to publish distinct `v0.1.0-preview.2` tag/assets; never mutate Preview 1. -2. After Preview 2 is published, begin evidence-driven visual identity and UI research without expanding the current product scope. +1. Draft PR #7 is open from `codex/devdeck-light`; wait for the Windows quality and release build, then review CI before any merge decision. +2. Capture and replace `docs/assets/envcompass-project-results.png` with a real DevDeck Light runtime-mismatch Tauri screenshot before PR review; do not reuse Design Lab or superseded Compass Atelier imagery. 3. Continue public dogfood before any scoped feature expansion. ## Git - Current `main` baseline: `73622c77f05d7e7d0d3297b4940bc5b4bd6d179c`. - Preview 2 hardening squash commit: `73622c77f05d7e7d0d3297b4940bc5b4bd6d179c`. -- Current state/action update: the commit containing this file. +- Current working branch: `codex/devdeck-light`; Issue #6; Draft PR #7; CodeQL checks passed and Windows quality/release CI is still in progress; latest handoff state is the commit containing this file. diff --git a/README.en.md b/README.en.md index 5c64741..3c81785 100644 --- a/README.en.md +++ b/README.en.md @@ -10,7 +10,7 @@ EnvCompass is a Windows development environment doctor. It checks project declar The current Preview targets Windows x64. Scans stay on the device: EnvCompass does not upload data, modify the system, execute project scripts, require an account, or collect telemetry. -![EnvCompass home screen](./docs/assets/envcompass-home.png) +![EnvCompass runtime mismatch result](./docs/assets/envcompass-project-results.png) ## Download and install @@ -39,7 +39,7 @@ The packaged application version for the current Preview line remains `0.1.0`; u The screenshot below uses the repository's synthetic fixture. It deliberately declares an unsatisfiable Node.js range to demonstrate an evidence-backed mismatch; it is not real user scan data. -![EnvCompass synthetic project diagnosis](./docs/assets/envcompass-project-results.png) +![EnvCompass home screen](./docs/assets/envcompass-home.png) ## Privacy and read-only boundaries diff --git a/README.md b/README.md index 4099767..969bbc4 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ EnvCompass 是一个 Windows 开发环境诊断工具。它会把项目声明与 当前 Preview 面向 Windows x64。扫描全部在本机完成;EnvCompass 不上传数据、不修改系统、不执行项目脚本,也没有账号或遥测。 -![EnvCompass 首页](./docs/assets/envcompass-home.png) +![EnvCompass runtime mismatch 结果](./docs/assets/envcompass-project-results.png) ## 下载与安装 @@ -39,7 +39,7 @@ EnvCompass 是一个 Windows 开发环境诊断工具。它会把项目声明与 下图使用仓库内的 synthetic fixture;它故意声明无法满足的 Node.js 版本范围,用于展示有证据的 mismatch,而不是用户真实扫描数据。 -![EnvCompass synthetic project 诊断结果](./docs/assets/envcompass-project-results.png) +![EnvCompass 首页](./docs/assets/envcompass-home.png) ## 隐私与只读边界 diff --git a/docs/assets/envcompass-home.png b/docs/assets/envcompass-home.png index 4f18ec3..7310491 100644 Binary files a/docs/assets/envcompass-home.png and b/docs/assets/envcompass-home.png differ diff --git a/docs/assets/envcompass-project-results.png b/docs/assets/envcompass-project-results.png index 864a0e9..e0832b6 100644 Binary files a/docs/assets/envcompass-project-results.png and b/docs/assets/envcompass-project-results.png differ diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c376ee6..29869f3 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -13,8 +13,8 @@ "windows": [ { "title": "EnvCompass", - "width": 1040, - "height": 760, + "width": 1200, + "height": 860, "minWidth": 820, "minHeight": 620 } diff --git a/src/App.css b/src/App.css index 2fe00c6..2e37fc9 100644 --- a/src/App.css +++ b/src/App.css @@ -1,1361 +1,427 @@ +/* ─── Design tokens ──────────────────────────────────────────────────── */ :root { - color: #17202a; - background: #f3f5f7; - font-family: "Segoe UI Variable", "Segoe UI", "Microsoft YaHei UI", sans-serif; - font-size: 15px; - line-height: 1.5; - color-scheme: light; - font-synthesis: none; -} - -* { - box-sizing: border-box; -} - + --bg: #eef2f5; + --panel: #fbfcfd; + --panel-2: #e6edf1; + --line: #cad7de; + --line-soft: #dde6eb; + --ink: #22313a; + --ink-2: #465860; + --muted: #61727a; + --faint: #6f8189; + --accent: #2d6f73; + --accent-soft: #dcebed; + --current: #b55c60; + --current-bg: #f8eeee; + --current-line: #e5c5c8; + --required: #4f8175; + --required-bg: #edf5f2; + --required-line: #c4ddd6; + --evidence: #4f7890; + --code-highlight: #e8f0f4; + --warning: #a77b3d; + --warning-bg: #f8f4eb; + --warning-line: #e5d6b8; + --term: #1d2a31; + --term-fg: #d7e1e3; + --mono: "Cascadia Code", "Cascadia Mono", Consolas, "JetBrains Mono", monospace; + --shadow: 0 18px 48px rgba(34, 49, 58, .12), 0 2px 8px rgba(34, 49, 58, .06); + /* UI Scale — set by JS from user preference; CSS zoom scales the whole tree */ + --ui-zoom: 1; +} + +/* ─── Reset / base ───────────────────────────────────────────────────── */ +* { box-sizing: border-box; } + +/* rem anchor: 15px base. All semantic text uses rem so UI Scale works. */ +html { font-size: 15px; zoom: var(--ui-zoom); } +html, body, #root { height: 100%; } body { margin: 0; - min-width: 0; - min-height: 100vh; - background: #f3f5f7; -} - -button, -summary { - font: inherit; -} - -button { - border: 1px solid #cbd2d9; - border-radius: 5px; - background: #ffffff; - color: #17202a; - padding: 8px 13px; - cursor: pointer; - transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease; -} - -button:hover:not(:disabled) { - border-color: #98a4af; - background: #f7f9fb; -} - -button:focus-visible, -summary:focus-visible { - outline: 2px solid #1769e0; - outline-offset: 2px; -} - -button:disabled { - cursor: not-allowed; - opacity: 0.55; -} - -button.primary { - border-color: #1769e0; - background: #1769e0; - color: #ffffff; - font-weight: 650; -} - -button.primary:hover:not(:disabled) { - border-color: #1059c5; - background: #1059c5; -} - -code { - font-family: "Cascadia Mono", "Cascadia Code", Consolas, monospace; - font-size: 0.9em; - overflow-wrap: anywhere; -} - -.app-shell { - min-height: 100vh; -} - -.topbar { - min-height: 66px; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 clamp(24px, 4vw, 54px); - border-bottom: 1px solid #d9dfe5; - background: #ffffff; -} - -.brand-button { - display: inline-flex; - align-items: center; - gap: 11px; - padding: 0; - border: 0; - background: transparent; - text-align: left; -} - -.brand-button:hover:not(:disabled) { - border: 0; - background: transparent; -} - -.brand-mark { - width: 31px; - height: 31px; - display: grid; - place-items: center; - border-radius: 6px; - background: #17202a; - color: #ffffff; - font-weight: 750; - letter-spacing: -0.04em; -} - -.brand-button > span:last-child { - display: grid; - line-height: 1.16; -} - -.brand-button strong { - font-size: 15px; - letter-spacing: 0.01em; -} - -.brand-button small { - margin-top: 3px; - color: #6b7681; - font-size: 11px; -} - -.topbar-meta { - display: flex; - align-items: center; - gap: 14px; -} - -.readonly-indicator { - display: inline-flex; - align-items: center; - gap: 7px; - color: #53606c; - font-size: 12px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.06em; -} - -.readonly-indicator::before { - content: ""; - width: 7px; - height: 7px; - border-radius: 50%; - background: #2c8b57; -} - -.lang-toggle { - min-width: 46px; - padding: 5px 9px; - font-size: 13px; -} - -.workspace { - width: min(100% - 48px, 1240px); - margin: 0 auto; - padding: 34px 0 54px; -} - -.eyebrow { - display: block; - margin-bottom: 8px; - color: #64717d; - font-size: 11px; - font-weight: 700; - letter-spacing: 0.11em; - text-transform: uppercase; -} - -.home { - max-width: 1120px; -} - -.home-intro { - max-width: 780px; - margin: 0 0 26px; -} - -.home-intro h1 { - margin: 0 0 10px; - font-size: clamp(28px, 3.5vw, 39px); - line-height: 1.2; - letter-spacing: -0.025em; -} - -.home-intro p { - max-width: 720px; - margin: 0; - color: #56636f; - font-size: 16px; -} - -.action-panel { - overflow: hidden; - border: 1px solid #cbd3db; - border-radius: 8px; - background: #ffffff; - box-shadow: 0 1px 2px rgb(17 28 39 / 6%); -} - -.action-row { - width: 100%; - min-height: 96px; - display: grid; - grid-template-columns: 50px minmax(0, 1fr) auto; - align-items: center; - gap: 18px; - padding: 19px 22px; - border: 0; - border-radius: 0; - text-align: left; -} - -.action-row + .action-row { - border-top: 1px solid #e1e5e9; -} - -.action-row:hover:not(:disabled) { - border-color: transparent; - background: #f8fafc; -} - -.action-primary { - box-shadow: inset 4px 0 #1769e0; -} - -.action-icon { - width: 44px; - height: 44px; - display: grid; - place-items: center; - border: 1px solid #c9d9f2; - border-radius: 7px; - background: #edf4ff; - color: #1769e0; -} - -.action-secondary .action-icon { - border-color: #d9dee3; - background: #f4f6f8; - color: #4d5b68; -} - -.action-icon svg { - width: 23px; - height: 23px; - fill: none; - stroke: currentColor; - stroke-linecap: round; - stroke-linejoin: round; - stroke-width: 1.7; -} - -.action-copy { - min-width: 0; - display: grid; - gap: 4px; -} - -.action-copy strong { - font-size: 17px; -} - -.action-copy small { - color: #62707c; -} - -.action-verb { - display: inline-flex; - align-items: center; - gap: 15px; - color: #1769e0; - font-weight: 650; - white-space: nowrap; -} - -.action-verb b { - font-size: 19px; - font-weight: 450; -} - -.check-scope { - margin-top: 30px; - padding-top: 24px; - border-top: 1px solid #d7dde3; -} - -.check-scope h2 { - margin: 0 0 17px; - font-size: 13px; - letter-spacing: 0.02em; -} - -.scope-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 26px; -} - -.scope-grid > div { - display: grid; - grid-template-columns: 29px 1fr; - column-gap: 11px; - align-content: start; -} - -.scope-grid span { - grid-row: 1 / 3; - color: #8a96a1; - font-family: "Cascadia Mono", Consolas, monospace; - font-size: 11px; - font-weight: 700; -} - -.scope-grid strong { - font-size: 14px; -} - -.scope-grid p { - margin: 3px 0 0; - color: #687581; - font-size: 13px; -} - -.privacy-promise, -.scanning-boundary { - margin: 24px 0 0; - color: #587063; - font-size: 12px; -} - -.error-banner, -.notice-banner { - margin: 16px 0; - padding: 10px 13px; - border-radius: 5px; -} - -.error-banner { - border: 1px solid #e1aaa7; - background: #fff4f3; - color: #992b26; -} - -.notice-banner { - border: 1px solid #9ccbad; - background: #effaf3; - color: #236c41; -} - -.scanning { - max-width: 1040px; -} - -.scanning-layout { - min-height: 520px; - display: grid; - grid-template-columns: minmax(260px, 0.8fr) minmax(430px, 1.2fr); - border: 1px solid #ccd4dc; - border-radius: 8px; - background: #ffffff; - overflow: hidden; -} - -.scanning-intro { - padding: 40px 34px; - border-right: 1px solid #e0e5e9; - background: #f8fafb; -} - -.live-indicator { - display: inline-flex; - align-items: center; - gap: 7px; - color: #1769e0; - font-size: 11px; - font-weight: 750; - letter-spacing: 0.1em; -} - -.live-indicator i { - width: 8px; - height: 8px; - border-radius: 50%; - background: #1769e0; - box-shadow: 0 0 0 4px #dceaff; -} - -.scanning-intro h1 { - margin: 26px 0 11px; - font-size: 29px; - line-height: 1.22; -} - -.scanning-intro p { - margin: 0; - color: #5c6975; -} - -.selected-path { - display: block; - margin-top: 20px; - padding-top: 16px; - border-top: 1px solid #dfe4e8; - color: #46535f; -} - -.stage-list { - list-style: none; - margin: 0; - padding: 22px 32px; -} - -.stage { - min-height: 62px; - display: grid; - grid-template-columns: 31px minmax(0, 1fr) auto; - align-items: center; - gap: 13px; - position: relative; -} - -.stage:not(:last-child)::after { - content: ""; - position: absolute; - left: 15px; - top: 46px; - bottom: -16px; - width: 1px; - background: #d8dee4; -} - -.stage-marker { - width: 31px; - height: 31px; - display: grid; - place-items: center; - z-index: 1; - border: 1px solid #cbd3da; - border-radius: 50%; - background: #ffffff; - color: #7a8792; - font-size: 12px; - font-weight: 700; -} - -.stage strong { - font-size: 14px; -} - -.stage small { - color: #7a8792; - font-size: 12px; -} - -.stage-active .stage-marker { - border-color: #1769e0; - background: #1769e0; - color: #ffffff; - box-shadow: 0 0 0 4px #e0ecff; -} - -.stage-active small { - color: #1769e0; - font-weight: 650; -} - -.stage-done .stage-marker { - border-color: #3b8b5e; - background: #e9f5ee; - color: #2d754c; -} - -.stage-done:not(:last-child)::after { - background: #9dc9ae; -} - -.stage-waiting { - color: #7b8792; -} - -.results { - max-width: 1240px; -} - -.results-topline { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 20px; - margin-bottom: 20px; -} - -.results-topline h1 { - margin: 0; - font-size: 28px; - line-height: 1.2; - overflow-wrap: anywhere; -} - -.results-nav { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; - gap: 7px; -} - -.results-nav button { - padding: 7px 10px; - font-size: 13px; -} - -.diagnosis-banner { - min-height: 112px; - display: grid; - grid-template-columns: 48px minmax(0, 1fr) auto; - align-items: center; - gap: 19px; - padding: 21px 24px; - border: 1px solid; - border-radius: 8px; - background: #ffffff; -} - -.diagnosis-clear { - border-color: #a9cdb6; - box-shadow: inset 4px 0 #2c8b57; -} - -.diagnosis-attention { - border-color: #dbb08b; - box-shadow: inset 4px 0 #bf641b; -} - -.diagnosis-icon { - width: 42px; - height: 42px; - display: grid; - place-items: center; - border-radius: 50%; - background: #e8f5ed; - color: #25734a; - font-size: 21px; - font-weight: 750; -} - -.diagnosis-attention .diagnosis-icon { - background: #fff0e3; - color: #a54f0d; -} - -.diagnosis-copy h2 { - margin: 0 0 5px; - font-size: 19px; -} - -.diagnosis-copy p { - margin: 0; - color: #5d6974; -} - -.diagnosis-counts { - display: flex; - gap: 2px; - border: 1px solid #d9dfe4; - border-radius: 6px; - overflow: hidden; -} - -.diagnosis-counts span { - min-width: 74px; - display: grid; - place-items: center; - padding: 8px 10px; - border-left: 1px solid #e0e4e8; - color: #68747f; - font-size: 11px; -} - -.diagnosis-counts span:first-child { - border-left: 0; -} - -.diagnosis-counts strong { - color: #1f2933; - font-size: 18px; -} - -.report-bar { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 16px; - margin-top: 14px; - padding: 12px 14px; - border: 1px solid #d4dbe1; - border-radius: 7px; - background: #ffffff; -} - -.report-primary-actions { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 8px; -} - -.report-primary-actions > span { - margin-left: 3px; - color: #6c7883; - font-size: 12px; -} - -.advanced-report { - min-width: 160px; - position: relative; -} - -.advanced-report > summary { - padding: 7px 8px; - color: #4f5d69; - cursor: pointer; - font-size: 13px; - font-weight: 650; - text-align: right; -} - -.advanced-report[open] { - width: 310px; -} - -.advanced-report p { - margin: 8px 0; - color: #687580; - font-size: 12px; -} - -.advanced-report div { - display: flex; - justify-content: flex-end; - gap: 7px; -} - -.advanced-report button { - padding: 6px 9px; - font-size: 12px; -} - -.privacy-bar { - margin: 9px 3px 20px; - color: #687580; - font-size: 11px; -} - -.project-panel, -.overview-panel, -.technical-shell, -.finding, -.suggestion-group { - border: 1px solid #d3dae0; - border-radius: 7px; - background: #ffffff; -} - -.project-panel { - margin-bottom: 20px; - padding: 20px 22px; -} - -.section-heading { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 18px; - margin-bottom: 14px; -} - -.section-heading h2 { - margin: 0; - font-size: 19px; -} - -.project-heading { - align-items: end; - padding-bottom: 15px; - border-bottom: 1px solid #e2e6ea; -} - -.project-heading h2 { - font-size: 21px; -} - -.project-path { - max-width: 60%; - color: #5d6974; - text-align: right; -} - -.project-facts { - display: flex; - flex-wrap: wrap; - gap: 28px; - margin: 17px 0 14px; -} - -.project-facts > div { - display: grid; - gap: 2px; -} - -.project-facts span, -.project-inline span { - color: #72808b; - font-size: 11px; - font-weight: 650; - letter-spacing: 0.04em; -} - -.project-facts strong { - font-size: 16px; -} - -.marker-list { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin: 12px 0 17px; -} - -.marker-list code, -.marker-list > span { - padding: 3px 7px; - border: 1px solid #d7dee4; - border-radius: 4px; - background: #f7f9fa; - color: #4c5965; - font-size: 11px; -} - -.requirements-block { - padding-top: 14px; - border-top: 1px solid #e2e6ea; -} - -.requirements-block h3 { - margin: 0 0 10px; - font-size: 13px; -} - -.requirements-block ul { - list-style: none; - margin: 0; - padding: 0; -} - -.requirements-block li { - display: grid; - grid-template-columns: minmax(180px, 1fr) auto auto; - align-items: center; - gap: 12px; - padding: 8px 0; - border-top: 1px solid #edf0f2; -} - -.requirement-value { - font-family: "Cascadia Mono", Consolas, monospace; -} - -.satisfaction { - padding: 2px 7px; - border-radius: 10px; - font-size: 11px; - font-weight: 650; -} - -.satisfaction-satisfied { - background: #e9f5ee; - color: #26734a; -} - -.satisfaction-not_satisfied { - background: #ffebea; - color: #b42d28; -} - -.satisfaction-unknown { - background: #edf0f2; - color: #5d6872; -} - -.unknown-note { - margin: 0; - padding: 9px 11px; - border-left: 3px solid #7c8b98; - background: #f5f7f8; - color: #4f5c67; -} - -.project-inline { - display: flex; - gap: 12px; - align-items: baseline; - margin: 11px 0 0; -} - -.project-notes, -.project-errors { - margin-top: 12px; - padding: 10px 12px; - border-radius: 5px; - background: #f6f8f9; - color: #55626d; - font-size: 12px; -} - -.project-errors { - background: #fff5f2; - color: #8c3d2f; -} - -.project-notes p, -.project-errors p { - margin: 4px 0 0; -} - -.no-project { - margin: 0 0 20px; - padding: 11px 13px; - border: 1px solid #d7dde2; - border-radius: 5px; - background: #f9fafb; - color: #64717c; -} - -.results-grid { - display: grid; - grid-template-columns: minmax(0, 1.65fr) minmax(280px, 0.85fr); - gap: 20px; - align-items: start; -} - -.finding-list { - display: grid; - gap: 10px; -} - -.finding { - padding: 16px 17px; - border-left-width: 4px; -} - -.finding-problem { - border-left-color: #c63d37; -} - -.finding-warning { - border-left-color: #c4771c; -} - -.finding-suggestion { - border-left-color: #7a8792; -} - -.finding-info { - border-left-color: #3d72a9; -} - -.finding-heading { - display: flex; - align-items: center; - gap: 8px; -} - -.severity { - display: inline-block; - padding: 2px 7px; - border-radius: 3px; - font-size: 10px; - font-weight: 750; - letter-spacing: 0.06em; - text-transform: uppercase; -} - -.severity-problem { - background: #ffebea; - color: #b82f29; -} - -.severity-warning { - background: #fff0de; - color: #9a510d; -} - -.severity-suggestion { - background: #edf0f2; - color: #56636e; -} - -.severity-info { - background: #e8f1fb; - color: #275f99; -} - -.finding-category { - color: #7a8792; - font-size: 11px; - font-weight: 650; - text-transform: uppercase; -} - -.finding h3 { - margin: 8px 0 5px; - font-size: 16px; -} - -.finding-summary { - margin: 0; - color: #4f5c67; -} - -.evidence-details { - margin-top: 10px; -} - -.evidence-details summary { - color: #285f98; - cursor: pointer; - font-size: 12px; - font-weight: 650; -} - -.evidence-details ul { - margin: 7px 0 0; - padding: 9px 10px 9px 26px; - border-radius: 4px; - background: #f5f7f9; -} - -.evidence-details li + li { - margin-top: 4px; -} - -.recommendation { - display: grid; - grid-template-columns: auto 1fr; - gap: 9px; - margin-top: 11px; - padding-top: 10px; - border-top: 1px solid #e7eaed; -} - -.recommendation > span { - color: #65727d; - font-size: 11px; - font-weight: 750; - text-transform: uppercase; -} - -.recommendation p { - margin: 0; -} - -.limitations { - margin: 8px 0 0; - color: #707c87; - font-size: 12px; -} - -.empty-attention { - display: flex; - align-items: center; - gap: 10px; - min-height: 64px; - padding: 13px 15px; - border: 1px solid #c5dccd; - border-radius: 6px; - background: #f4faf6; - color: #38634a; -} - -.empty-attention span { - width: 24px; - height: 24px; - display: grid; - place-items: center; - border-radius: 50%; - background: #dceee3; - font-weight: 750; -} - -.notes-section { - margin-top: 22px; -} - -.notes-section > h2 { - margin: 0 0 10px; - font-size: 17px; -} - -.suggestion-group { - margin-top: 16px; - overflow: hidden; -} - -.suggestion-group > summary { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 14px; - cursor: pointer; - font-weight: 650; - list-style: none; -} - -.suggestion-group > summary::-webkit-details-marker { - display: none; -} - -.suggestion-group > summary::before { - content: "+"; - margin-right: 8px; - color: #6c7882; -} - -.suggestion-group[open] > summary::before { - content: "−"; -} - -.suggestion-group > summary span { - flex: 1; -} - -.suggestion-group > summary strong { - min-width: 24px; - padding: 2px 7px; - border-radius: 10px; - background: #edf0f2; - text-align: center; - font-size: 11px; -} - -.suggestion-group > .finding-list { - padding: 0 10px 10px; -} - -.overview-panel { - padding: 17px; -} - -.system-overview { - display: grid; - grid-template-columns: 1fr auto; - gap: 2px 8px; - margin-bottom: 7px; - padding: 12px; - border-radius: 5px; - background: #f4f6f8; -} - -.system-overview > span { - grid-row: 1 / 3; - align-self: center; - color: #687681; - font-size: 12px; -} - -.system-overview strong, -.system-overview small { - text-align: right; -} - -.system-overview small { - color: #75818c; -} - -.overview-row { - min-height: 48px; - display: grid; - grid-template-columns: 9px minmax(72px, 1fr) auto auto; - align-items: center; - gap: 9px; - border-bottom: 1px solid #e6e9ec; -} - -.overview-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: #87939d; -} - -.overview-dot-available { - background: #2f8a57; -} - -.overview-dot-failed, -.overview-dot-timeout, -.overview-dot-malformed { - background: #c23e37; -} - -.overview-version { - font-family: "Cascadia Mono", Consolas, monospace; - font-size: 12px; -} - -.overview-status { - color: #6c7883; - font-size: 11px; -} - -.status-available { - color: #27744a; -} - -.status-failed, -.status-timeout, -.status-malformed { - color: #b5332e; -} - -.status-missing, -.status-unsupported { - color: #7b613b; -} - -.available-summary { - display: flex; - align-items: baseline; - gap: 7px; - margin-top: 15px; - color: #66737e; -} - -.available-summary strong { - color: #25313c; - font-size: 22px; -} - -.technical-shell { - margin-top: 22px; - overflow: hidden; -} - -.technical-shell > summary { - display: flex; - align-items: center; - justify-content: space-between; - gap: 14px; - padding: 15px 18px; - cursor: pointer; - list-style: none; - font-weight: 700; -} - -.technical-shell > summary::-webkit-details-marker { - display: none; -} - -.technical-shell > summary::after { - content: "+"; - color: #66737e; - font-size: 19px; - font-weight: 400; -} - -.technical-shell[open] > summary::after { - content: "−"; -} - -.technical-shell > summary small { - margin-left: auto; - color: #77838e; - font-weight: 400; -} - -.technical-content { - display: grid; - gap: 24px; - padding: 20px; - border-top: 1px solid #e0e5e9; -} - -.technical-content h3 { - margin: 0 0 10px; - font-size: 15px; -} - -.count-badge { - display: inline-block; - margin-left: 5px; - padding: 1px 6px; - border-radius: 9px; - background: #edf0f2; - color: #5e6b76; - font-size: 10px; -} - -.tool-list { - border-top: 1px solid #e4e8eb; -} - -.tool-row { - display: grid; - grid-template-columns: 180px minmax(0, 1fr); - gap: 16px; - padding: 10px 3px; - border-bottom: 1px solid #e7eaed; -} - -.tool-identity { - display: flex; - align-items: center; - gap: 8px; -} - -.tool-status { - padding: 1px 5px; - border-radius: 3px; - background: #edf0f2; - font-size: 10px; -} - -.tool-detail { - min-width: 0; - display: flex; - flex-wrap: wrap; - justify-content: flex-end; - gap: 5px 11px; - color: #64717c; - text-align: right; -} - -.technical-list { - margin: 0; - padding-left: 20px; -} - -.technical-list li { - margin: 5px 0; -} - -.technical-list code { - margin-left: 8px; -} - -.path-list { - max-height: 350px; - overflow: auto; - border: 1px solid #dce1e5; - border-radius: 5px; - background: #f8f9fa; -} - -.path-row { - display: flex; - justify-content: space-between; - gap: 16px; - padding: 7px 10px; - border-bottom: 1px solid #e5e8eb; -} - -.path-row:last-child { - border-bottom: 0; -} - -.path-row span { - color: #9b5424; - font-size: 11px; - white-space: nowrap; -} - -@media (max-width: 960px) { - .workspace { - width: min(100% - 32px, 1240px); - } - - .results-grid { - grid-template-columns: 1fr; - } - + background: #dfe7eb; + color: var(--ink); + font-family: "Segoe UI", "Microsoft YaHei", "Noto Sans SC", system-ui, sans-serif; + font-size: 1rem; + -webkit-font-smoothing: antialiased; +} +button, summary { font: inherit; } +button { color: inherit; } +button:disabled { cursor: wait; opacity: .6; } +code, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; } +:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 5px; } + +/* ─── App shell — viewport-filling desktop layout (Phase 3) ─────────── */ +.app-shell { height: 100%; display: flex; flex-direction: column; overflow: hidden; background: var(--bg); } + +/* ─── Topbar ─────────────────────────────────────────────────────────── */ +.topbar { min-height: 52px; display: flex; align-items: center; gap: 14px; padding: 0 18px; background: var(--panel); border-bottom: 1px solid var(--line); flex: 0 0 auto; } +.brand-button { display: inline-flex; align-items: center; gap: 9px; border: 0; padding: 0; background: transparent; cursor: pointer; text-align: left; } +.brand-mark { width: 29px; height: 29px; display: grid; place-items: center; border-radius: 8px; background: var(--accent); color: #fff; font: 700 23px/1 var(--mono); transform: rotate(-12deg); } +.brand-mark span { transform: rotate(12deg) translateY(-1px); } +.brand-copy { display: grid; line-height: 1.05; } +.brand-copy strong { font-size: 1rem; letter-spacing: .01em; } +.brand-copy small { margin-top: 3px; color: var(--faint); font-size: 0.73rem; } +.toolbar-context { max-width: 34vw; overflow: hidden; padding: 4px 9px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel-2); color: var(--ink-2); font: 0.73rem var(--mono); text-overflow: ellipsis; white-space: nowrap; } +.topbar-meta { display: flex; align-items: center; gap: 8px; margin-left: auto; } +.readonly-indicator { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); font-size: 0.73rem; } +.status-dot, .live-dot { width: 7px; height: 7px; display: inline-block; border-radius: 50%; background: var(--required); } +.live-dot { animation: livepulse 1.9s ease-in-out infinite; } + +/* ─── Buttons (Phase 2) — shared border/bg/cursor base ──────────────── */ +.lang-toggle, .results-actions button, .report-primary-actions button, .advanced-report button, .action-verb, .results-nav button { border: 1px solid var(--line); border-radius: 5px; background: var(--panel); color: var(--ink-2); cursor: pointer; } +.lang-toggle { min-width: 40px; min-height: 28px; padding: 0 10px; font: 700 0.73rem var(--mono); } +.lang-toggle:hover, .results-actions button:hover, .report-primary-actions button:hover, .advanced-report button:hover { background: var(--panel-2); } + +/* ─── UI Scale picker (Phase 5) ──────────────────────────────────────── */ +.scale-picker { display: inline-flex; border: 1px solid var(--line); border-radius: 5px; overflow: hidden; } +.scale-btn { min-height: 26px; padding: 0 6px; border: 0; border-right: 1px solid var(--line); background: var(--panel); color: var(--muted); cursor: pointer; font: 0.67rem var(--mono); transition: background .1s; } +.scale-btn:last-child { border-right: 0; } +.scale-btn:hover { background: var(--panel-2); } +.scale-btn.is-active { background: var(--accent-soft); color: var(--accent); font-weight: 700; } + +/* ─── Home ───────────────────────────────────────────────────────────── */ +.workspace { width: min(1920px, calc(100% - 48px)); margin: 0 auto; } +.home { padding: clamp(32px, 7vh, 78px) 0 28px; } +.home-composition { display: grid; grid-template-columns: minmax(0, 1fr) 280px; gap: clamp(38px, 7vw, 110px); align-items: center; } +.home-main { max-width: 760px; } +.home-intro { max-width: 680px; } +.eyebrow, .section-kicker { color: var(--faint); font-size: 0.73rem; font-weight: 750; letter-spacing: .12em; text-transform: uppercase; } +.home-intro h1 { max-width: 640px; margin: 12px 0 12px; font-size: clamp(29px, 4.5vw, 52px); line-height: 1.05; letter-spacing: -.045em; } +.home-intro p { max-width: 610px; margin: 0; color: var(--ink-2); font-size: 1rem; line-height: 1.65; } +.action-panel { display: grid; gap: 9px; margin-top: 31px; } +.action-row { display: grid; grid-template-columns: 42px minmax(0, 1fr) auto; align-items: center; gap: 14px; width: 100%; padding: 14px 15px; border: 1px solid var(--line); border-radius: 7px; background: var(--panel); box-shadow: 0 2px 7px rgba(34, 49, 58, .04); cursor: pointer; text-align: left; transition: border-color .15s, transform .15s, box-shadow .15s; } +.action-row:hover { border-color: var(--accent); box-shadow: 0 7px 18px rgba(45, 111, 115, .1); transform: translateY(-1px); } +.action-primary { border-left: 3px solid var(--accent); } +.action-secondary { border-left: 3px solid var(--line); } +.action-icon { width: 34px; height: 34px; display: grid; place-items: center; border-radius: 6px; background: var(--accent-soft); color: var(--accent); } +.action-secondary .action-icon { background: var(--panel-2); color: var(--muted); } +.action-icon svg { width: 19px; height: 19px; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; } +.action-copy { min-width: 0; display: grid; gap: 4px; } +.action-copy strong { font-size: 0.93rem; } +.action-copy small { color: var(--muted); font-size: 0.73rem; line-height: 1.4; } +.action-verb { display: inline-flex; align-items: center; gap: 9px; padding: 5px 9px; font-size: 0.73rem; white-space: nowrap; } +.action-verb b { color: var(--accent); font-size: 1rem; } +.home-visual { display: grid; place-items: center; gap: 12px; color: var(--faint); font: 0.7rem var(--mono); letter-spacing: .08em; text-transform: uppercase; } +.home-visual::before { content: ""; width: 1px; height: 120px; background: var(--line); } +.home-visual::after { content: ""; width: 110px; height: 110px; border: 1px solid var(--line); border-radius: 50%; position: absolute; opacity: .45; } +.home-visual svg { width: 106px; height: 106px; color: var(--accent); opacity: .75; } +.check-scope { margin-top: clamp(52px, 9vh, 92px); border-top: 1px solid var(--line); padding-top: 16px; } +.check-scope h2 { margin: 0 0 15px; color: var(--muted); font-size: 0.73rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; } +.scope-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 28px; } +.scope-grid > div { display: grid; grid-template-columns: 28px 1fr; column-gap: 8px; } +.scope-grid span { grid-row: span 2; color: var(--accent); font: 700 0.73rem var(--mono); } +.scope-grid strong { font-size: 0.8rem; } +.scope-grid p { margin: 3px 0 0; color: var(--muted); font-size: 0.73rem; line-height: 1.4; } +.privacy-promise, .scanning-boundary, .privacy-bar { display: flex; align-items: center; gap: 7px; margin: 23px 0 0; color: var(--faint); font-size: 0.73rem; } +.privacy-promise::before, .scanning-boundary::before, .privacy-bar::before { content: "▣"; color: var(--accent); } +.error-banner, .notice-banner { margin-top: 15px; padding: 10px 12px; border: 1px solid var(--current-line); border-left: 3px solid var(--current); background: var(--current-bg); color: var(--current); font-size: 0.73rem; } + +/* ─── Scanning ───────────────────────────────────────────────────────── */ +.scanning { padding: clamp(28px, 7vh, 70px) 0; } +.scanning-layout { display: grid; grid-template-columns: minmax(260px, .65fr) minmax(0, 1.35fr); min-height: 390px; border: 1px solid var(--line); background: var(--panel); box-shadow: var(--shadow); } +.scanning-intro { display: flex; flex-direction: column; justify-content: center; padding: 42px; border-right: 1px solid var(--line); } +.live-indicator { display: inline-flex; align-items: center; gap: 7px; color: var(--required); font: 700 0.7rem var(--mono); letter-spacing: .1em; text-transform: uppercase; } +.live-indicator i { width: 7px; height: 7px; display: inline-block; border-radius: 50%; background: currentColor; } +.scanning-intro h1 { margin: 17px 0 9px; font-size: clamp(24px, 3vw, 38px); letter-spacing: -.03em; } +.scanning-intro p { margin: 0; color: var(--muted); line-height: 1.6; } +.selected-path { max-width: 100%; margin-top: 24px; overflow: hidden; color: var(--faint); font-size: 0.73rem; text-overflow: ellipsis; white-space: nowrap; } +.stage-list { display: grid; align-content: center; gap: 0; margin: 0; padding: 24px 36px; list-style: none; } +.stage { position: relative; display: grid; grid-template-columns: 28px 1fr auto; align-items: center; gap: 11px; min-height: 48px; border-bottom: 1px solid var(--line-soft); } +.stage:last-child { border-bottom: 0; } +.stage-marker { width: 21px; height: 21px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 50%; color: var(--faint); font: 0.7rem var(--mono); } +.stage strong { font-size: 0.8rem; } +.stage small { color: var(--faint); font-size: 0.73rem; } +.stage-active .stage-marker { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); } +.stage-active strong { color: var(--accent); } +.stage-done .stage-marker { border-color: var(--required); color: var(--required); } +.stage-done strong { color: var(--ink-2); } + +/* ─── Results — viewport-filling (Phase 3) ───────────────────────────── */ +.results { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; width: 100%; padding: 0 18px; } +.process-bar { width: min(1920px, 100%); margin: 0 auto; flex: 0 0 auto; min-height: 36px; display: flex; align-items: center; gap: 14px; border-bottom: 1px solid var(--line); } +.process-bar nav { display: flex; align-items: center; gap: 8px; color: var(--faint); font-size: 0.73rem; } +.process-bar nav b { color: var(--line); font-size: 1rem; font-weight: 400; } +.process-done { color: var(--required); } +.process-active { padding: 3px 7px; border-radius: 4px; background: var(--accent-soft); color: var(--accent); font-weight: 700; } +.results-actions { display: flex; align-items: center; gap: 6px; margin-left: auto; } +.results-actions button { min-height: 28px; padding: 0 10px; font-size: 0.8rem; } +.results-actions .primary, .report-primary-actions .primary { border-color: var(--accent); background: var(--accent); color: #fff; } +.results-actions .primary { min-height: 32px; padding: 0 14px; font-size: 0.8rem; font-weight: 600; } +.results-actions .primary:hover, .report-primary-actions .primary:hover { background: #245b5f; } +.results-layout { flex: 1 1 0; min-height: 0; width: min(1920px, 100%); margin: 0 auto; display: grid; grid-template-columns: 264px minmax(0, 1fr); } +.finding-navigator { min-width: 0; overflow-y: auto; padding: 16px 12px 17px 0; border-right: 1px solid var(--line); } +.navigator-heading { display: flex; align-items: center; justify-content: space-between; padding: 0 9px 11px; border-bottom: 1px solid var(--line); } +.navigator-count, .evidence-count { min-width: 20px; padding: 2px 5px; border-radius: 10px; background: var(--panel-2); color: var(--muted); font: 700 0.7rem var(--mono); text-align: center; } +.navigator-group { padding: 14px 0 4px; } +.navigator-group-label { display: flex; justify-content: space-between; padding: 0 9px 6px; color: var(--faint); font-size: 0.73rem; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; } +.navigator-item { width: 100%; min-width: 0; display: grid; grid-template-columns: 21px minmax(0, 1fr) auto; align-items: center; gap: 7px; padding: 8px; border: 1px solid transparent; border-radius: 5px; background: transparent; cursor: pointer; text-align: left; } +button.navigator-item:hover, .navigator-item.is-active { border-color: var(--current-line); background: var(--current-bg); } +.navigator-runtime { cursor: default; } +.navigator-symbol { width: 20px; height: 20px; display: grid; place-items: center; border-radius: 4px; background: var(--current-bg); color: var(--current); font: 700 0.8rem var(--mono); } +.navigator-warning .navigator-symbol { background: var(--warning-bg); color: var(--warning); } +.navigator-runtime .navigator-symbol { background: var(--required-bg); color: var(--required); } +.navigator-copy { min-width: 0; display: grid; gap: 2px; } +.navigator-copy strong { overflow: hidden; font-size: 0.87rem; font-weight: 650; text-overflow: ellipsis; white-space: nowrap; } +.navigator-copy small { overflow: hidden; color: var(--faint); font: 0.8rem var(--mono); text-overflow: ellipsis; white-space: nowrap; } +.navigator-tag { padding: 2px 5px; border-radius: 3px; background: var(--current-bg); color: var(--current); font-size: 0.7rem; font-weight: 750; text-transform: uppercase; } +.navigator-warning .navigator-tag { background: var(--warning-bg); color: var(--warning); } +.navigator-tag.status-available { background: var(--required-bg); color: var(--required); } +.navigator-empty { margin: 0; padding: 8px 9px; color: var(--muted); font-size: 0.8rem; line-height: 1.45; } +.navigator-foot { margin-top: 14px; padding: 9px; border-top: 1px solid var(--line); color: var(--faint); font-size: 0.73rem; } +.result-detail { min-width: 0; overflow-y: auto; padding: 17px 0 24px 24px; } +.results-topline { display: flex; align-items: end; justify-content: space-between; gap: 18px; } +.results-topline h1 { margin: 5px 0 0; font-size: 1.47rem; letter-spacing: -.025em; } +.scan-meta { display: inline-flex; align-items: center; gap: 6px; color: var(--faint); font-size: 0.73rem; } +.scan-meta .live-dot { animation: none; } + +/* ─── Diagnosis banner ───────────────────────────────────────────────── */ +.diagnosis-banner { display: grid; grid-template-columns: 32px minmax(0, 1fr) auto; align-items: center; gap: 12px; margin-top: 16px; padding: 13px 14px; border: 1px solid var(--line); border-left: 3px solid var(--current); background: var(--panel); } +.diagnosis-clear { border-left-color: var(--required); } +.diagnosis-icon { width: 28px; height: 28px; display: grid; place-items: center; border-radius: 5px; background: var(--current-bg); color: var(--current); font: 700 16px var(--mono); } +.diagnosis-clear .diagnosis-icon { background: var(--required-bg); color: var(--required); } +.diagnosis-copy { min-width: 0; } +.diagnosis-copy h2 { margin: 3px 0 2px; font-size: 1rem; } +.diagnosis-copy p { margin: 0; color: var(--muted); font-size: 0.8rem; line-height: 1.45; } +.diagnosis-counts { display: flex; gap: 12px; color: var(--muted); font-size: 0.73rem; white-space: nowrap; } +.diagnosis-counts span { display: inline-flex; align-items: baseline; gap: 4px; } +.diagnosis-counts strong { color: var(--ink); font: 700 0.93rem var(--mono); } +.diagnosis-attention .diagnosis-counts span:first-child strong { color: var(--current); } +.primary-diagnosis { margin-top: 16px; } + +/* ─── Finding cards ──────────────────────────────────────────────────── */ +.finding { position: relative; min-width: 0; border: 1px solid var(--line); border-left: 3px solid var(--current); background: var(--panel); box-shadow: 0 2px 6px rgba(34, 49, 58, .035); } +.finding-warning { border-left-color: var(--warning); } +.finding-suggestion, .finding-info { border-left-color: var(--accent); } +.finding-body { padding: 15px 16px 14px; } +.finding-heading { display: flex; align-items: center; gap: 9px; } +.severity { display: inline-flex; align-items: center; gap: 5px; color: var(--current); font-size: 0.73rem; font-weight: 750; letter-spacing: .08em; text-transform: uppercase; } +.severity-warning { color: var(--warning); } +.severity-suggestion, .severity-info { color: var(--accent); } +.severity-symbol { width: 16px; height: 16px; display: grid; place-items: center; border: 1px solid currentColor; border-radius: 4px; font: 700 11px var(--mono); } +.finding-category { color: var(--faint); font-size: 0.73rem; font-weight: 650; letter-spacing: .08em; text-transform: uppercase; } +.finding h2 { margin: 8px 0 5px; font-size: 1.2rem; line-height: 1.25; letter-spacing: -.02em; } +.finding-primary h2 { font-size: 1.4rem; } +.finding-summary { margin: 0; color: var(--ink-2); font-size: 0.87rem; line-height: 1.55; } + +/* ─── Runtime comparison (Current / Required) ────────────────────────── */ +.runtime-comparison { display: grid; grid-template-columns: minmax(0, 1fr) 42px minmax(0, 1fr); align-items: stretch; margin-top: 15px; } +.runtime-cell { min-width: 0; padding: 12px 13px; border: 1px solid; display: grid; gap: 4px; } +.current-cell { border-color: var(--current-line); background: var(--current-bg); } +.required-cell { border-color: var(--required-line); background: var(--required-bg); } +.runtime-label { color: var(--muted); font-size: 0.73rem; font-weight: 750; letter-spacing: .1em; text-transform: uppercase; } +.runtime-cell strong { overflow: hidden; font: 700 1.47rem var(--mono); text-overflow: ellipsis; white-space: nowrap; } +.current-cell strong { color: var(--current); } +.required-cell strong { color: var(--required); } +.runtime-cell code { overflow: hidden; color: var(--muted); font-size: 0.73rem; text-overflow: ellipsis; white-space: nowrap; } +.comparison-divider { display: grid; place-items: center; color: var(--current); font: 700 13px var(--mono); } +.comparison-divider span { width: 24px; height: 24px; display: grid; place-items: center; border: 1px solid currentColor; border-radius: 50%; background: var(--panel); } + +/* ─── Evidence ───────────────────────────────────────────────────────── */ +.evidence-block { margin-top: 15px; } +.evidence-heading { display: flex; align-items: center; gap: 7px; margin-bottom: 7px; } +.evidence-count { background: var(--accent-soft); color: var(--evidence); } +.evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; } +.evidence-card { min-width: 0; overflow: hidden; border: 1px solid var(--line); background: var(--panel); } +.evidence-card-head { display: flex; align-items: center; gap: 6px; padding: 7px 9px; border-bottom: 1px solid var(--line-soft); background: var(--panel-2); color: var(--muted); font: 700 0.73rem var(--mono); text-transform: uppercase; } +.evidence-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--evidence); } +.evidence-terminal .evidence-dot { background: var(--required); } +.evidence-card code { display: block; min-height: 42px; padding: 9px; overflow: hidden; background: var(--code-highlight); color: var(--ink-2); font-size: 0.73rem; line-height: 1.5; text-overflow: ellipsis; white-space: pre-wrap; word-break: break-word; } +.evidence-terminal code { background: var(--term); color: var(--term-fg); } + +/* ─── Recommendation / Next step ─────────────────────────────────────── */ +.recommendation { display: grid; grid-template-columns: 21px minmax(0, 1fr); gap: 8px; margin-top: 14px; padding: 10px 11px; border: 1px solid var(--warning-line); background: var(--warning-bg); } +.next-step-mark { width: 19px; height: 19px; display: grid; place-items: center; border-radius: 4px; background: #eee5d1; color: var(--warning); font: 700 13px var(--mono); } +.recommendation > div > span { display: block; color: var(--warning); font-size: 0.73rem; font-weight: 750; letter-spacing: .1em; text-transform: uppercase; } +.recommendation p { margin: 3px 0 0; color: var(--ink-2); font-size: 0.8rem; line-height: 1.45; } +.limitations { margin: 8px 0 0; color: var(--muted); font-size: 0.73rem; line-height: 1.45; } +.limitations strong { color: var(--ink-2); } +.finding-list { display: grid; gap: 9px; } +.remaining-findings { margin-top: 17px; } +.section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; margin-bottom: 9px; } +.section-heading h2 { margin: 4px 0 0; font-size: 1rem; } +.empty-attention { display: flex; align-items: center; gap: 10px; padding: 14px; border: 1px solid var(--required-line); background: var(--required-bg); color: var(--required); } +.empty-attention > span { font: 700 22px var(--mono); } +.empty-attention div { display: grid; gap: 3px; } +.empty-attention div span { color: var(--muted); font-size: 0.73rem; } + +/* ─── Context grid (project + overview panels) ───────────────────────── */ +.context-grid { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(250px, .75fr); gap: 26px; align-items: start; margin-top: 23px; padding-top: 18px; border-top: 1px solid var(--line); } +.project-panel, .overview-panel { min-width: 0; } +.project-heading { align-items: end; padding-bottom: 9px; border-bottom: 1px solid var(--line-soft); } +.project-heading h2 { font-size: 1.13rem; } +.project-path { max-width: 55%; overflow: hidden; color: var(--faint); font-size: 0.7rem; text-overflow: ellipsis; white-space: nowrap; } +.project-facts { display: flex; flex-wrap: wrap; gap: 22px; margin: 11px 0; } +.project-facts > div { display: grid; gap: 2px; } +.project-facts span, .project-inline span { color: var(--faint); font-size: 0.7rem; font-weight: 750; letter-spacing: .07em; text-transform: uppercase; } +.project-facts strong { font-size: 0.87rem; } +.marker-list { display: flex; flex-wrap: wrap; gap: 5px; margin: 8px 0 11px; } +.marker-list code, .marker-list > span { padding: 3px 6px; border: 1px solid var(--line); border-radius: 4px; background: var(--panel-2); color: var(--ink-2); font-size: 0.7rem; } +.requirements-block { padding-top: 10px; border-top: 1px solid var(--line-soft); } +.requirements-block h3 { margin: 0 0 6px; font-size: 0.73rem; } +.requirements-block ul { margin: 0; padding: 0; list-style: none; } +.requirements-block li { display: grid; grid-template-columns: minmax(120px, 1fr) auto auto; align-items: center; gap: 8px; padding: 6px 0; border-top: 1px solid var(--line-soft); font-size: 0.73rem; } +.requirement-value { font-family: var(--mono); } +.satisfaction { padding: 2px 6px; border-radius: 8px; font-size: 0.7rem; font-weight: 700; } +.satisfaction-satisfied { background: var(--required-bg); color: var(--required); } +.satisfaction-not_satisfied { background: var(--current-bg); color: var(--current); } +.satisfaction-unknown { background: var(--warning-bg); color: var(--warning); } +.unknown-note, .project-notes, .project-errors { margin: 0; padding: 8px 9px; border-left: 2px solid var(--warning); background: var(--warning-bg); color: var(--muted); font-size: 0.73rem; line-height: 1.45; } +.project-notes, .project-errors { margin-top: 8px; } +.project-errors { border-left-color: var(--current); background: var(--current-bg); color: var(--current); } +.project-notes p, .project-errors p { margin: 3px 0 0; } +.project-inline { display: flex; align-items: baseline; gap: 9px; margin: 7px 0 0; font-size: 0.73rem; } + +/* ─── Overview panel (right) ─────────────────────────────────────────── */ +.system-overview { display: grid; grid-template-columns: 1fr auto; gap: 2px 8px; padding: 9px 0; border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); } +.system-overview > span { grid-row: 1 / 3; align-self: center; color: var(--faint); font-size: 0.73rem; } +.system-overview strong, .system-overview small { text-align: right; } +.system-overview strong { font-size: 0.73rem; } +.system-overview small { color: var(--faint); font-size: 0.7rem; } +.overview-row { min-height: 36px; display: grid; grid-template-columns: 10px minmax(65px, 1fr) auto auto; align-items: center; gap: 7px; border-bottom: 1px solid var(--line-soft); font-size: 0.8rem; } +.overview-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--warning); } +.overview-dot-available { background: var(--required); } +.overview-version { font: 0.73rem var(--mono); } +.overview-status { color: var(--faint); font-size: 0.73rem; } +.status-available { color: var(--required); } +.status-failed, .status-timeout, .status-malformed { color: var(--current); } +.status-missing, .status-unsupported { color: var(--warning); } +.available-summary { display: flex; align-items: baseline; gap: 6px; margin: 10px 0 0; color: var(--muted); font-size: 0.73rem; } +.available-summary strong { color: var(--ink); font: 700 1.13rem var(--mono); } +.no-project { margin: 0; padding: 10px 0; border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); color: var(--muted); font-size: 0.73rem; } + +/* ─── Supporting findings / technical / share ────────────────────────── */ +.supporting-findings { margin-top: 18px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } +.supporting-findings > summary, .technical-shell > summary { display: flex; align-items: center; gap: 8px; padding: 10px 2px; cursor: pointer; list-style: none; } +.supporting-findings > summary::-webkit-details-marker, .technical-shell > summary::-webkit-details-marker, .advanced-report > summary::-webkit-details-marker { display: none; } +.supporting-findings > summary::before, .technical-shell > summary::after { color: var(--accent); content: "+"; font: 15px var(--mono); } +.supporting-findings[open] > summary::before, .technical-shell[open] > summary::after { content: "−"; } +.supporting-findings > summary strong { margin-left: auto; color: var(--muted); font: 0.73rem var(--mono); } +.supporting-findings > .finding-list { padding-bottom: 11px; } +.report-bar { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 18px; padding: 12px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } +.share-heading { display: flex; align-items: center; gap: 9px; min-width: 0; } +.share-mark { width: 22px; height: 22px; display: grid; place-items: center; border-radius: 5px; background: var(--accent-soft); color: var(--accent); font: 700 14px var(--mono); } +.share-heading div { display: grid; gap: 2px; } +.share-heading strong { font-size: 0.8rem; } +.share-heading span { color: var(--faint); font-size: 0.73rem; } +.report-primary-actions { display: flex; align-items: center; gap: 7px; } +.report-primary-actions button { min-height: 28px; padding: 0 10px; font-size: 0.8rem; } +.report-primary-actions .primary { min-height: 32px; padding: 0 14px; font-size: 0.8rem; font-weight: 600; } +.advanced-report { position: relative; } +.advanced-report > summary { min-height: 28px; display: flex; align-items: center; padding: 0 10px; border: 1px solid var(--line); border-radius: 5px; background: var(--panel); color: var(--ink-2); cursor: pointer; font-size: 0.73rem; } +.advanced-report[open] { min-width: 320px; } +.advanced-report > div { display: flex; justify-content: flex-end; gap: 5px; margin-top: 6px; } +.advanced-report button { min-height: 24px; padding: 0 8px; font-size: 0.73rem; } +.advanced-report p { margin: 5px 0 0; color: var(--faint); font-size: 0.73rem; text-align: right; } +.privacy-bar { margin: 8px 1px 0; } +.technical-shell { margin-top: 12px; border-top: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); } +.technical-shell > summary > span { font-size: 0.73rem; font-weight: 700; } +.technical-shell > summary small { margin-left: auto; color: var(--faint); font-size: 0.7rem; } +.technical-content { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px 34px; padding: 14px 0 4px; border-top: 1px solid var(--line-soft); } +.technical-content section:last-child { grid-column: 1 / -1; } +.technical-content h3 { margin: 0 0 7px; font-size: 0.73rem; } +.tool-list { border-top: 1px solid var(--line-soft); } +.tool-row { display: grid; grid-template-columns: 140px minmax(0, 1fr); gap: 10px; padding: 7px 2px; border-bottom: 1px solid var(--line-soft); font-size: 0.7rem; } +.tool-identity { display: flex; align-items: center; gap: 6px; } +.tool-status { padding: 2px 4px; border-radius: 3px; background: var(--panel-2); font-size: 0.7rem; } +.tool-detail { min-width: 0; display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 3px 8px; color: var(--muted); text-align: right; } +.technical-list { margin: 0; padding-left: 17px; font-size: 0.7rem; } +.technical-list li { margin: 4px 0; } +.technical-list code { margin-left: 7px; } +.path-list { max-height: 310px; overflow: auto; border: 1px solid var(--line); background: var(--panel-2); } +.path-row { display: flex; justify-content: space-between; gap: 12px; padding: 5px 8px; border-bottom: 1px solid var(--line-soft); font-size: 0.7rem; } +.path-row:last-child { border-bottom: 0; } +.path-row span { color: var(--warning); white-space: nowrap; } + +/* ─── Status bar ─────────────────────────────────────────────────────── */ +.statusbar { flex: 0 0 auto; width: min(1920px, 100%); min-height: 28px; display: flex; align-items: center; gap: 8px; margin: 0 auto; padding: 0 10px; background: var(--term); color: #9eafb4; font-size: 0.73rem; } +.statusbar .live-dot { width: 6px; height: 6px; animation: none; } +.status-separator { width: 1px; height: 11px; background: #3b4c54; } +.status-spacer { flex: 1; } + +/* ─── Animations ─────────────────────────────────────────────────────── */ +@keyframes livepulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(79,129,117,.34); } 60% { box-shadow: 0 0 0 5px rgba(79,129,117,0); } } + +/* ─── Responsive — 1440px+ (Phase 4) ────────────────────────────────── */ +@media (min-width: 1440px) { + .results-layout { grid-template-columns: 288px minmax(0, 1fr); } + .context-grid { grid-template-columns: minmax(0, 1.4fr) minmax(260px, 0.6fr); } +} + +/* ─── Responsive — 1900px+ three-column layout (Phase 4) ────────────── */ +@media (min-width: 1900px) { + .results-layout { grid-template-columns: 264px minmax(0, 1fr) 300px; } + /* Pull context-grid children into the three-column parent grid */ + .context-grid { display: contents; border-top: 0; padding-top: 0; } + .project-panel { padding-top: 18px; border-top: 1px solid var(--line); } .overview-panel { - order: -1; - } - - .scanning-layout { - grid-template-columns: 0.85fr 1.15fr; - } - - .diagnosis-banner { - grid-template-columns: 42px 1fr; - } - - .diagnosis-counts { - grid-column: 1 / -1; - width: fit-content; - margin-left: 61px; + grid-column: 3; + grid-row: 1 / 999; + padding: 17px 16px; + border-left: 1px solid var(--line); + overflow-y: auto; } } -@media (max-width: 860px) { - .topbar { - padding: 0 16px; - } - - .readonly-indicator { - display: none; - } - - .scope-grid, - .scanning-layout { - grid-template-columns: 1fr; - } - - .scanning-intro { - border-right: 0; - border-bottom: 1px solid #e0e5e9; - } - - .action-row { - grid-template-columns: 44px 1fr; - } - - .action-verb { - grid-column: 2; - } - - .results-topline, - .report-bar { - flex-direction: column; - } - - .results-nav { - justify-content: flex-start; - } - - .project-heading { - align-items: flex-start; - flex-direction: column; - } - - .project-path { - max-width: 100%; - text-align: left; - } - - .requirements-block li, - .tool-row { - grid-template-columns: 1fr; - } - - .tool-detail { - justify-content: flex-start; - text-align: left; - } -} +/* ─── Responsive — ≤1040px ───────────────────────────────────────────── */ +@media (max-width: 1040px) { + .workspace { width: calc(100% - 30px); } + .results { padding-left: 15px; padding-right: 15px; } + .results-layout, .process-bar { width: 100%; } + .home-composition { grid-template-columns: minmax(0, 1fr) 200px; gap: 30px; } + .context-grid { grid-template-columns: minmax(0, 1fr) minmax(220px, .8fr); gap: 18px; } + .result-detail { padding-left: 18px; } +} + +/* ─── Responsive — ≤820px (compact single-column) ───────────────────── */ +@media (max-width: 820px) { + .toolbar-context { max-width: 24vw; } + .home-composition { grid-template-columns: 1fr; } + .home-visual { display: none; } + .home { padding-top: 34px; } + .results-layout { display: block; overflow-y: auto; } + .finding-navigator { display: none; } + .result-detail { padding-left: 0; overflow-y: visible; } + .process-bar { min-height: 44px; } + .process-bar nav { display: none; } + .results-actions { width: 100%; justify-content: flex-end; } + .context-grid { grid-template-columns: 1fr; } + .diagnosis-banner { grid-template-columns: 29px minmax(0, 1fr); } + .diagnosis-counts { grid-column: 2; justify-content: flex-start; } +} + +/* ─── Responsive — ≤620px ────────────────────────────────────────────── */ +@media (max-width: 620px) { + .topbar { padding: 0 11px; gap: 8px; } + .brand-copy small, .readonly-indicator { display: none; } + .toolbar-context { max-width: 34vw; } + .workspace { width: calc(100% - 22px); } + .results { padding: 0 11px; } + .results-topline { align-items: flex-start; flex-direction: column; gap: 5px; } + .action-row { grid-template-columns: 35px minmax(0, 1fr); gap: 10px; } + .action-verb { grid-column: 2; justify-self: start; } + .scope-grid { grid-template-columns: 1fr; gap: 14px; } + .scanning-layout { grid-template-columns: 1fr; } + .scanning-intro { padding: 28px; border-right: 0; border-bottom: 1px solid var(--line); } + .stage-list { padding: 16px 22px; } + .stage { grid-template-columns: 26px 1fr; } + .stage small { grid-column: 2; } + .runtime-comparison { grid-template-columns: 1fr; gap: 0; } + .comparison-divider { height: 28px; } + .evidence-grid { grid-template-columns: 1fr; } + .project-heading { align-items: flex-start; flex-direction: column; } + .project-path { max-width: 100%; } + .requirements-block li, .tool-row { grid-template-columns: 1fr; } + .report-bar { align-items: flex-start; flex-direction: column; } + .report-primary-actions { width: 100%; flex-wrap: wrap; } + .advanced-report[open] { min-width: 100%; } + .advanced-report > div, .advanced-report p { justify-content: flex-start; text-align: left; } + .technical-content { grid-template-columns: 1fr; } + .technical-content section:last-child { grid-column: 1; } + .tool-detail { justify-content: flex-start; text-align: left; } + .statusbar { overflow: hidden; white-space: nowrap; } + .status-spacer, .statusbar > :last-child { display: none; } + /* Hide scale picker on very narrow screens */ + .scale-picker { display: none; } +} + +@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; } } diff --git a/src/App.tsx b/src/App.tsx index fb1073d..a82d6e0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { open, save } from "@tauri-apps/plugin-dialog"; @@ -7,6 +7,7 @@ import { writeText } from "@tauri-apps/plugin-clipboard-manager"; import { detectLanguage, type Lang, type Messages, messages } from "./i18n"; import { primaryTool, + runtimeComparisonForFinding, splitFindings, summarizeReport, } from "./presentation"; @@ -126,40 +127,63 @@ function FindingCard({ finding, lang, t, + report, + primary = false, }: { finding: Finding; lang: Lang; t: Messages; + report: ScanReport; + primary?: boolean; }) { + const comparison = runtimeComparisonForFinding(finding, report); + const hasConfigurationEvidence = finding.id.startsWith("project.") || finding.evidence.some((item) => /package\.json|pyproject|requirements|\.nvmrc|\.node-version|\.python-version/i.test(`${item.en_us} ${item.zh_cn}`)); return ( -
+
+ {severityLabel(finding.severity, t)} - - {categoryLabel(finding.category, t)} - + {categoryLabel(finding.category, t)}
-

{localized(finding.title, lang)}

+

{localized(finding.title, lang)}

{localized(finding.summary, lang)}

+ + {comparison ? ( +
+
+ {t.currentRuntime} + {comparison.tool?.version ?? t.statusMissing} + {comparison.tool?.executable ? {comparison.tool.executable} : null} +
+ +
+ {t.projectRequirement} + {comparison.requirement.raw} + {comparison.requirement.source} +
+
+ ) : null} + {finding.evidence.length > 0 ? ( -
- - {t.evidence} · {finding.evidence.length} - -
    +
    +
    {t.evidence}{finding.evidence.length}
    +
    {finding.evidence.map((item, index) => ( -
  • +
    +
    {localized(item, lang)} -
  • +
    ))} -
-
+ + ) : null}
- {t.recommendation} -

{localized(finding.recommendation, lang)}

+ +
{t.nextStep}

{localized(finding.recommendation, lang)}

{finding.limitations ? (

@@ -379,17 +403,79 @@ function TechnicalDetails({ report, t }: { report: ScanReport; t: Messages }) { ); } +function FindingNavigator({ + report, + lang, + t, + activeId, + onSelect, +}: { + report: ScanReport; + lang: Lang; + t: Messages; + activeId: string | null; + onSelect: (id: string) => void; +}) { + const groups = splitFindings(report.findings); + const runtimes = [ + { label: "Python", tool: primaryTool(report.tools, "python") }, + { label: "Node.js", tool: primaryTool(report.tools, "node") }, + { label: "Git", tool: primaryTool(report.tools, "git") }, + ]; + const checkCount = report.findings.length + runtimes.length + 1; + return ( +

+ ); +} + +const ZOOM_LEVELS = [0.9, 1.0, 1.1, 1.25] as const; +type ZoomLevel = typeof ZOOM_LEVELS[number]; + +function getInitialZoom(): ZoomLevel { + const saved = parseFloat(localStorage.getItem("ec-ui-zoom") ?? "1"); + return (ZOOM_LEVELS.includes(saved as ZoomLevel) ? saved : 1.0) as ZoomLevel; +} + function App() { const [lang, setLang] = useState(() => detectLanguage()); + const [uiZoom, setUiZoom] = useState(getInitialZoom); const [view, setView] = useState("home"); const [scanKind, setScanKind] = useState("project"); const [selectedPath, setSelectedPath] = useState(null); const [activeStage, setActiveStage] = useState(null); const [report, setReport] = useState(null); + const [activeFindingId, setActiveFindingId] = useState(null); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const [busy, setBusy] = useState(false); + // Apply UI zoom to html element and persist + useEffect(() => { + document.documentElement.style.setProperty("--ui-zoom", String(uiZoom)); + localStorage.setItem("ec-ui-zoom", String(uiZoom)); + }, [uiZoom]); + const t = messages[lang]; const counts = useMemo(() => summarizeReport(report), [report]); const findingGroups = useMemo( @@ -397,6 +483,8 @@ function App() { [report], ); const hasAttention = counts.problems > 0 || counts.warnings > 0; + const primaryFinding = findingGroups.attention.find((finding) => finding.id === activeFindingId) ?? findingGroups.attention[0]; + const remainingAttention = findingGroups.attention.filter((finding) => finding.id !== primaryFinding?.id); async function buildMarkdown( current: ScanReport, @@ -441,6 +529,7 @@ function App() { path: projectPath || "", }); setReport(nextReport); + setActiveFindingId(nextReport.findings.find((finding) => finding.severity === "problem" || finding.severity === "warning")?.id ?? null); setView("results"); } catch (scanError) { const message = @@ -493,6 +582,7 @@ function App() { setView("home"); setError(null); setNotice(null); + setActiveFindingId(null); } const stages = scanKind === "project" ? projectStages : machineStages; @@ -502,20 +592,32 @@ function App() {
+ {report?.project ? folderName(report.project.path) : view === "results" ? t.machineMode : t.projectMode}
- Read-only + +
+ {ZOOM_LEVELS.map((level) => ( + + ))} +
@@ -587,7 +689,7 @@ function App() {
- LIVE + {t.calibrating}

{t.scanningTitle}

{scanKind === "project" ? t.scanningProject : t.scanningMachine}

{selectedPath ? {selectedPath} : null} @@ -624,139 +726,36 @@ function App() { {view === "results" && report ? (
-
-
- {t.resultsTitle} -

{report.project ? folderName(report.project.path) : t.brandTagline}

-
-
- - - -
+
+ +
- -
- {hasAttention ? "!" : "✓"} -
-

{hasAttention ? t.attentionTitle : t.clearTitle}

-

{hasAttention ? t.attentionBody : t.clearBody}

-
-
- {counts.problems}{t.problems} - {counts.warnings}{t.warnings} - {counts.suggestions}{t.suggestions} -
-
- -
-
- - - {t.conciseReportHelp} -
-
- {t.advancedReport} -

{t.technicalReportHelp}

-
- - +
+ +
+
{t.resultsTitle}

{report.project ? folderName(report.project.path) : t.machineMode}

{t.statusAvailable}
+
+ +
{t.overallDiagnosis}

{hasAttention ? t.attentionTitle : t.clearTitle}

{hasAttention ? t.attentionBody : t.clearBody}

+
{counts.problems}{t.problems}{counts.warnings}{t.warnings}{counts.suggestions}{t.suggestions}
+
+ +
+ {primaryFinding ? :
{t.noAttention}{t.clearBody}
} +
+ {remainingAttention.length > 0 ?
{t.additionalFindings}

{t.needsAttention}

{remainingAttention.map((finding) => )}
: null} + +
+ {report.project ? :

{t.noProject}

} +
-
-
- - {notice ?
{notice}
: null} -

{t.privacyBar}

- {report.project ? :

{t.noProject}

} - -
-
-
-
- Diagnosis -

{t.needsAttention}

-
-
- {findingGroups.attention.length > 0 ? ( -
- {findingGroups.attention.map((finding) => ( - - ))} -
- ) : ( -
{t.noAttention}
- )} - - {findingGroups.notes.length > 0 ? ( -
-

{t.diagnosticNotes}

-
- {findingGroups.notes.map((finding) => ( - - ))} -
-
- ) : null} - - {findingGroups.suggestions.length > 0 ? ( -
- - {t.cleanupSuggestions} - {findingGroups.suggestions.length} - -
- {findingGroups.suggestions.map((finding) => ( - - ))} -
-
- ) : null} + {findingGroups.notes.length > 0 || findingGroups.suggestions.length > 0 ?
{t.supportingNotes}{findingGroups.notes.length + findingGroups.suggestions.length}
{[...findingGroups.notes, ...findingGroups.suggestions].map((finding) => )}
: null} +
{t.shareDiagnosis}{t.conciseReportHelp}
{t.moreShareOptions}

{t.technicalReportHelp}

+ {notice ?
{notice}
: null}

{t.privacyBar}

- -
- - +
{primaryFinding ? localized(primaryFinding.title, lang) : t.noAttention}{counts.problems} {t.problems}{counts.warnings} {t.warnings}{report.system.os} {report.system.arch}Inspect → Diagnose → Explain → Share
) : null}
diff --git a/src/i18n.ts b/src/i18n.ts index 6965c24..1812fc8 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -2,6 +2,9 @@ export type Lang = "zh-CN" | "en-US"; export interface Messages { brandTagline: string; + readOnly: string; + projectMode: string; + machineMode: string; homeTitle: string; homeSubtitle: string; diagnoseProject: string; @@ -21,6 +24,7 @@ export interface Messages { scanningTitle: string; scanningMachine: string; scanningProject: string; + calibrating: string; stageSystem: string; stagePath: string; stagePython: string; @@ -38,6 +42,9 @@ export interface Messages { newProjectScan: string; clearTitle: string; clearBody: string; + overallDiagnosis: string; + additionalFindings: string; + supportingNotes: string; attentionTitle: string; attentionBody: string; problems: string; @@ -45,6 +52,8 @@ export interface Messages { suggestions: string; availableTools: string; copyForAi: string; + shareDiagnosis: string; + moreShareOptions: string; saveReport: string; advancedReport: string; copyTechnical: string; @@ -79,6 +88,12 @@ export interface Messages { pathEntries: string; pathUnchecked: string; evidence: string; + configurationEvidence: string; + terminalEvidence: string; + currentRuntime: string; + projectRequirement: string; + runtimeComparison: string; + nextStep: string; recommendation: string; limitations: string; severityProblem: string; @@ -106,6 +121,9 @@ export interface Messages { export const messages: Record = { "zh-CN": { brandTagline: "开发环境诊断", + readOnly: "只读", + projectMode: "项目诊断", + machineMode: "本机检查", homeTitle: "从项目开始诊断", homeSubtitle: "选择跑不起来的项目,EnvCompass 会把项目声明与这台电脑上的 Python、Node.js 和 PATH 放在一起检查。", @@ -126,6 +144,7 @@ export const messages: Record = { scanningTitle: "正在诊断开发环境", scanningMachine: "正在检查这台电脑上的实际命令与 PATH。", scanningProject: "正在把项目声明与本机环境进行对照。", + calibrating: "正在检查", stageSystem: "读取 Windows 与系统架构", stagePath: "检查 PATH", stagePython: "检查 Python 与 pip", @@ -143,6 +162,9 @@ export const messages: Record = { newProjectScan: "诊断另一个项目", clearTitle: "没有发现明显会阻止项目运行的问题", clearBody: "基于当前可读取的证据,没有确认 runtime、项目要求或命令解析冲突。", + overallDiagnosis: "总体判断", + additionalFindings: "其他偏差", + supportingNotes: "判断说明与清理建议", attentionTitle: "发现需要处理的环境问题", attentionBody: "先处理下方的问题和警告,再重新运行项目。", problems: "问题", @@ -150,6 +172,8 @@ export const messages: Record = { suggestions: "清理建议", availableTools: "工具可用", copyForAi: "复制给 AI", + shareDiagnosis: "分享诊断", + moreShareOptions: "更多分享选项", saveReport: "保存简明报告", advancedReport: "技术报告", copyTechnical: "复制技术报告", @@ -184,6 +208,12 @@ export const messages: Record = { pathEntries: "项", pathUnchecked: "含未检查的网络路径", evidence: "证据", + configurationEvidence: "配置证据", + terminalEvidence: "终端证据", + currentRuntime: "当前 runtime", + projectRequirement: "项目要求", + runtimeComparison: "当前 runtime 与项目要求对照", + nextStep: "下一步", recommendation: "下一步", limitations: "判断边界", severityProblem: "问题", @@ -209,6 +239,9 @@ export const messages: Record = { }, "en-US": { brandTagline: "Development environment diagnosis", + readOnly: "Read-only", + projectMode: "Project diagnosis", + machineMode: "Machine scan", homeTitle: "Start with the project", homeSubtitle: "Choose the project that will not run. EnvCompass compares its declarations with the Python, Node.js, and PATH actually used on this PC.", @@ -229,6 +262,7 @@ export const messages: Record = { scanningTitle: "Diagnosing the environment", scanningMachine: "Checking the commands and PATH actually used on this PC.", scanningProject: "Comparing project declarations with the machine environment.", + calibrating: "Checking", stageSystem: "Read Windows and architecture", stagePath: "Inspect PATH", stagePython: "Check Python and pip", @@ -246,6 +280,9 @@ export const messages: Record = { newProjectScan: "Diagnose another project", clearTitle: "No clear issue likely to prevent the project from running", clearBody: "The available evidence does not confirm a runtime, project requirement, or command-resolution conflict.", + overallDiagnosis: "Overall diagnosis", + additionalFindings: "Additional deviations", + supportingNotes: "Diagnostic notes and cleanup", attentionTitle: "Environment issues need attention", attentionBody: "Address the problems and warnings below before trying the project again.", problems: "Problems", @@ -253,6 +290,8 @@ export const messages: Record = { suggestions: "Cleanup", availableTools: "Tools available", copyForAi: "Copy for AI", + shareDiagnosis: "Share diagnosis", + moreShareOptions: "More share options", saveReport: "Save concise report", advancedReport: "Technical report", copyTechnical: "Copy technical report", @@ -287,6 +326,12 @@ export const messages: Record = { pathEntries: "entries", pathUnchecked: "includes unchecked network paths", evidence: "Evidence", + configurationEvidence: "Configuration evidence", + terminalEvidence: "Terminal evidence", + currentRuntime: "Current runtime", + projectRequirement: "Project requirement", + runtimeComparison: "Current runtime and project requirement", + nextStep: "Next step", recommendation: "Next step", limitations: "Limitation", severityProblem: "Problem", diff --git a/src/presentation.test.ts b/src/presentation.test.ts index f45fecf..6cff346 100644 --- a/src/presentation.test.ts +++ b/src/presentation.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { splitFindings, summarizeReport } from "./presentation"; +import { runtimeComparisonForFinding, splitFindings, summarizeReport } from "./presentation"; import type { Finding, ScanReport } from "./types"; function finding(severity: Finding["severity"]): Finding { @@ -43,4 +43,23 @@ describe("results presentation", () => { expect(groups.suggestions).toHaveLength(1); expect(groups.notes).toHaveLength(1); }); + + it("maps a project mismatch to the unresolved runtime requirement", () => { + const mismatch = { + ...finding("problem"), + id: "project.mismatch.node.engines-node", + category: "project", + evidence: [{ zh_cn: "package.json engines.node: >=20 <23", en_us: "package.json engines.node: >=20 <23" }], + } as Finding; + const report = { + findings: [mismatch], + project: { + requirements: [{ kind: "node", source: "package.json engines.node", raw: ">=20 <23", parsed: null, satisfied: "not_satisfied" }], + }, + tools: [{ name: "node", status: "available", version: "24.0.0" }], + } as ScanReport; + const comparison = runtimeComparisonForFinding(mismatch, report); + expect(comparison?.tool?.version).toBe("24.0.0"); + expect(comparison?.requirement.raw).toBe(">=20 <23"); + }); }); diff --git a/src/presentation.ts b/src/presentation.ts index 0d687f0..1cbec61 100644 --- a/src/presentation.ts +++ b/src/presentation.ts @@ -1,4 +1,4 @@ -import type { Finding, ScanReport, ToolProbe } from "./types"; +import type { Finding, ProjectRequirement, ScanReport, ToolProbe } from "./types"; export interface FindingCounts { problems: number; @@ -42,3 +42,23 @@ export function primaryTool( ): ToolProbe | undefined { return tools.find((tool) => tool.name === name); } + +export interface RuntimeComparison { + tool: ToolProbe | undefined; + requirement: ProjectRequirement; +} + +function runtimeKindForFinding(finding: Finding): "python" | "node" | null { + if (finding.category === "python" || finding.category === "node") return finding.category; + const match = finding.id.match(/^project\.(?:mismatch|unknown)\.(python|node)\./); + return match?.[1] === "python" || match?.[1] === "node" ? match[1] : null; +} + +export function runtimeComparisonForFinding(finding: Finding, report: ScanReport): RuntimeComparison | null { + const kind = runtimeKindForFinding(finding); + if (!kind || !report.project) return null; + const candidates = report.project.requirements.filter((item) => item.kind === kind && item.satisfied !== "satisfied"); + const evidenceText = finding.evidence.flatMap((item) => [item.zh_cn, item.en_us]).join("\n"); + const requirement = candidates.find((item) => evidenceText.includes(item.source) && evidenceText.includes(item.raw)) ?? candidates[0]; + return requirement ? { tool: primaryTool(report.tools, kind), requirement } : null; +}