@youtyan/code-viewer 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
package/web/app.js CHANGED
@@ -658,6 +658,13 @@
658
658
  lang: params.get("lang") || "en",
659
659
  section: params.get("section") || "overview"
660
660
  };
661
+ case "/doctor":
662
+ return {
663
+ screen: "repo",
664
+ ref: params.get("ref") || params.get("target") || "worktree",
665
+ path: params.get("path") || "",
666
+ range
667
+ };
661
668
  case "/history": {
662
669
  const commit = params.get("commit") || "";
663
670
  return {
@@ -757,6 +764,24 @@
757
764
  function buildRawFileUrl(target) {
758
765
  return "/_file?path=" + encodeURIComponent(target.path) + "&ref=" + encodeURIComponent(target.ref || "worktree");
759
766
  }
767
+ function parseDoctorOverlay(pathname, search) {
768
+ if (pathname === "/doctor")
769
+ return true;
770
+ const params = new URLSearchParams(search);
771
+ return params.get("doctor") === "open";
772
+ }
773
+ function withDoctorOverlay(url, open) {
774
+ const queryIdx = url.indexOf("?");
775
+ const base = queryIdx >= 0 ? url.slice(0, queryIdx) : url;
776
+ const query = queryIdx >= 0 ? url.slice(queryIdx + 1) : "";
777
+ const params = new URLSearchParams(query);
778
+ if (open)
779
+ params.set("doctor", "open");
780
+ else
781
+ params.delete("doctor");
782
+ const qs = params.toString();
783
+ return qs ? `${base}?${qs}` : base;
784
+ }
760
785
 
761
786
  // web-src/core/annotation-player-core.ts
762
787
  function createAnnotationPlayerCore(deps) {
@@ -20855,6 +20880,196 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20855
20880
  };
20856
20881
  }
20857
20882
 
20883
+ // web-src/views/doctor-view.ts
20884
+ var TEXT = {
20885
+ en: {
20886
+ title: "Environment doctor",
20887
+ refresh: "Re-run checks",
20888
+ refreshing: "Running checks…",
20889
+ loadFailed: "Failed to load doctor report",
20890
+ worstOk: "All checks passed.",
20891
+ worstWarn: "Some checks reported warnings.",
20892
+ worstError: "Some checks failed. See the items marked ERROR below.",
20893
+ empty: "No checks ran.",
20894
+ close: "Close",
20895
+ statusLabels: { ok: "OK", warn: "WARN", error: "ERROR" }
20896
+ },
20897
+ ja: {
20898
+ title: "環境ドクター",
20899
+ refresh: "再診断",
20900
+ refreshing: "診断中…",
20901
+ loadFailed: "doctor の結果を取得できませんでした",
20902
+ worstOk: "すべての診断項目が OK です。",
20903
+ worstWarn: "一部の項目に注意があります (WARN)。",
20904
+ worstError: "失敗している項目があります。下の ERROR を確認してください。",
20905
+ empty: "診断項目がありません。",
20906
+ close: "閉じる",
20907
+ statusLabels: { ok: "OK", warn: "警告", error: "エラー" }
20908
+ }
20909
+ };
20910
+ var viewGeneration = 0;
20911
+ function doctorText(lang) {
20912
+ return TEXT[lang];
20913
+ }
20914
+ function statusLabel(text2, status) {
20915
+ return text2.statusLabels[status];
20916
+ }
20917
+ function renderRow(text2, esc, row) {
20918
+ const pill = `<span class="doctor-row-pill doctor-row-pill-${row.status}">` + `${esc(statusLabel(text2, row.status))}</span>`;
20919
+ const body = `<div class="doctor-row-body">` + `<div class="doctor-row-title">${esc(row.title)}</div>` + (row.detail ? `<div class="doctor-row-detail">${esc(row.detail)}</div>` : "") + (row.hint ? `<div class="doctor-row-hint">${esc(row.hint)}</div>` : "") + `</div>`;
20920
+ return `<div class="doctor-row" data-status="${esc(row.status)}">${pill}${body}</div>`;
20921
+ }
20922
+ function renderGroup(text2, esc, group) {
20923
+ if (group.rows.length === 0)
20924
+ return "";
20925
+ const rows = group.rows.map((row) => renderRow(text2, esc, row)).join("");
20926
+ return `<section class="doctor-group" data-group="${esc(group.id)}">` + `<h2 class="doctor-group-title">${esc(group.title)}</h2>` + rows + `</section>`;
20927
+ }
20928
+ function renderReport(mount, report, text2, esc) {
20929
+ const summary = mount.querySelector(".doctor-summary");
20930
+ if (summary) {
20931
+ const message = report.worstStatus === "error" ? text2.worstError : report.worstStatus === "warn" ? text2.worstWarn : text2.worstOk;
20932
+ summary.textContent = message;
20933
+ }
20934
+ const content = mount.querySelector(".doctor-content");
20935
+ if (!content)
20936
+ return;
20937
+ if (!report.groups.length) {
20938
+ content.innerHTML = `<div class="doctor-empty">${esc(text2.empty)}</div>`;
20939
+ return;
20940
+ }
20941
+ content.innerHTML = report.groups.map((group) => renderGroup(text2, esc, group)).join("");
20942
+ }
20943
+ function ensureSkeleton(mount, text2, esc) {
20944
+ if (mount.querySelector(".doctor-header"))
20945
+ return;
20946
+ mount.innerHTML = `<div class="doctor-header">` + `<h1>${esc(text2.title)}</h1>` + `<button type="button" class="doctor-refresh">${esc(text2.refresh)}</button>` + `<button type="button" class="doctor-close" aria-label="${esc(text2.close)}" title="${esc(text2.close)}">×</button>` + `</div>` + `<div class="doctor-summary"></div>` + `<div class="doctor-content"></div>`;
20947
+ }
20948
+ function createDoctorView(deps) {
20949
+ function getMount() {
20950
+ return deps.$("#doctor-sheet");
20951
+ }
20952
+ function getOverlay() {
20953
+ return deps.$("#doctor-sheet-overlay");
20954
+ }
20955
+ function setRefreshState(mount, busy) {
20956
+ const btn = mount.querySelector(".doctor-refresh");
20957
+ if (!btn)
20958
+ return;
20959
+ btn.disabled = busy;
20960
+ }
20961
+ async function load(mount) {
20962
+ viewGeneration += 1;
20963
+ const myGen = viewGeneration;
20964
+ const lang = deps.getLanguage();
20965
+ const text2 = doctorText(lang);
20966
+ setRefreshState(mount, true);
20967
+ const content = mount.querySelector(".doctor-content");
20968
+ if (content)
20969
+ content.innerHTML = `<div class="doctor-empty">${deps.escapeHtml(text2.refreshing)}</div>`;
20970
+ const summary = mount.querySelector(".doctor-summary");
20971
+ if (summary)
20972
+ summary.textContent = "";
20973
+ try {
20974
+ const res = await deps.trackLoad(fetch("/_doctor"));
20975
+ if (myGen !== viewGeneration)
20976
+ return;
20977
+ if (!res.ok) {
20978
+ throw new Error(`HTTP ${res.status}`);
20979
+ }
20980
+ const data = await res.json();
20981
+ if (myGen !== viewGeneration)
20982
+ return;
20983
+ renderReport(mount, data, text2, deps.escapeHtml);
20984
+ deps.onWorstStatusChange?.(data.worstStatus);
20985
+ } catch (err) {
20986
+ if (myGen !== viewGeneration)
20987
+ return;
20988
+ if (content) {
20989
+ const message = err instanceof Error ? err.message : String(err);
20990
+ content.innerHTML = `<div class="doctor-empty">${deps.escapeHtml(`${text2.loadFailed}: ${message}`)}</div>`;
20991
+ }
20992
+ deps.onWorstStatusChange?.(null);
20993
+ } finally {
20994
+ if (myGen === viewGeneration)
20995
+ setRefreshState(mount, false);
20996
+ }
20997
+ }
20998
+ function applyLocalizedLabels(mount, text2) {
20999
+ const title = mount.querySelector(".doctor-header h1");
21000
+ if (title)
21001
+ title.textContent = text2.title;
21002
+ const refreshBtn = mount.querySelector(".doctor-refresh");
21003
+ if (refreshBtn)
21004
+ refreshBtn.textContent = text2.refresh;
21005
+ const closeBtn = mount.querySelector(".doctor-close");
21006
+ if (closeBtn) {
21007
+ closeBtn.setAttribute("aria-label", text2.close);
21008
+ closeBtn.setAttribute("title", text2.close);
21009
+ }
21010
+ }
21011
+ function bindOnce(mount) {
21012
+ if (mount.dataset.bound)
21013
+ return;
21014
+ mount.dataset.bound = "1";
21015
+ const text2 = doctorText(deps.getLanguage());
21016
+ ensureSkeleton(mount, text2, deps.escapeHtml);
21017
+ const refreshBtn = mount.querySelector(".doctor-refresh");
21018
+ refreshBtn?.addEventListener("click", (event) => {
21019
+ event.preventDefault();
21020
+ load(mount);
21021
+ });
21022
+ const closeBtn = mount.querySelector(".doctor-close");
21023
+ closeBtn?.addEventListener("click", (event) => {
21024
+ event.preventDefault();
21025
+ deps.onCloseRequest?.();
21026
+ });
21027
+ }
21028
+ function isOpen() {
21029
+ const mount = getMount();
21030
+ return mount ? !mount.hidden : false;
21031
+ }
21032
+ async function open() {
21033
+ const mount = getMount();
21034
+ if (!mount)
21035
+ return;
21036
+ bindOnce(mount);
21037
+ applyLocalizedLabels(mount, doctorText(deps.getLanguage()));
21038
+ mount.hidden = false;
21039
+ mount.setAttribute("aria-hidden", "false");
21040
+ mount.removeAttribute("inert");
21041
+ const overlay = getOverlay();
21042
+ if (overlay) {
21043
+ overlay.hidden = false;
21044
+ overlay.setAttribute("aria-hidden", "false");
21045
+ }
21046
+ document.body.classList.add("doctor-sheet-open");
21047
+ await load(mount);
21048
+ }
21049
+ function close() {
21050
+ const mount = getMount();
21051
+ if (!mount)
21052
+ return;
21053
+ mount.hidden = true;
21054
+ mount.setAttribute("aria-hidden", "true");
21055
+ mount.setAttribute("inert", "");
21056
+ const overlay = getOverlay();
21057
+ if (overlay) {
21058
+ overlay.hidden = true;
21059
+ overlay.setAttribute("aria-hidden", "true");
21060
+ }
21061
+ document.body.classList.remove("doctor-sheet-open");
21062
+ viewGeneration += 1;
21063
+ }
21064
+ async function refresh() {
21065
+ const mount = getMount();
21066
+ if (!mount)
21067
+ return;
21068
+ await load(mount);
21069
+ }
21070
+ return { open, close, isOpen, refresh };
21071
+ }
21072
+
20858
21073
  // web-src/views/empty-diff-pane.ts
20859
21074
  function showEmptyHistoryDiffPane(deps) {
20860
21075
  if (deps.diff)
@@ -21835,6 +22050,19 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21835
22050
  text: "In large repositories the sidebar loads folder children on demand. Folders you open are remembered and automatically re-expanded after a reload."
21836
22051
  }
21837
22052
  ]
22053
+ },
22054
+ {
22055
+ title: "Environment doctor",
22056
+ blocks: [
22057
+ {
22058
+ kind: "paragraph",
22059
+ text: "Toggle the \uD83E\uDE7A icon in the header to slide in a diagnostic sheet from the right. It works on top of any screen (Repository, Diff, History, Datastores) and the open state is preserved in the URL as ?doctor=open so links are reproducible."
22060
+ },
22061
+ {
22062
+ kind: "paragraph",
22063
+ text: "Each row reports OK / WARN / ERROR with a remediation hint when relevant. The check groups are Runtime (Node / Bun / NODE_MODULE_VERSION), Package (version + execution origin including npx cache), SQLite driver, Snapshot store, Git, Discovery summary, Docker / Compose (CLI / v2 plugin / daemon / compose config dry-parse / compose ps health per discovered service), and Server. The most common hint is the npx cache fix for better-sqlite3 NODE_MODULE_VERSION mismatch (rm -rf ~/.npm/_npx then re-run with npx -y @youtyan/code-viewer@latest)."
22064
+ }
22065
+ ]
21838
22066
  }
21839
22067
  ]
21840
22068
  },
@@ -22398,6 +22626,19 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
22398
22626
  text: "大きいリポジトリではサイドバーがフォルダの中身を必要に応じて読み込みます。開いたフォルダは記憶され、次回のリロード時に同じ状態で展開し直されます。"
22399
22627
  }
22400
22628
  ]
22629
+ },
22630
+ {
22631
+ title: "環境ドクター",
22632
+ blocks: [
22633
+ {
22634
+ kind: "paragraph",
22635
+ text: "ヘッダ右の \uD83E\uDE7A アイコンで、右からスライドする診断シートを開きます。Repository / Diff / History / Datastores などどの画面の上にも重ねて表示でき、開閉状態は URL の ?doctor=open に同期されるのでリンク共有で復元できます。"
22636
+ },
22637
+ {
22638
+ kind: "paragraph",
22639
+ text: "各項目は OK / WARN / ERROR で表示され、必要に応じて対処手順のヒントが付きます。診断グループは Runtime (Node / Bun / NODE_MODULE_VERSION)、Package (バージョン + 実行元: npx cache / global / local / bunx)、SQLite driver、Snapshot store、Git、Discovery summary、Docker / Compose (CLI / v2 plugin / daemon / compose config dry parse / compose ps による各サービスのヘルス)、Server (待ち受けポート) の 8 つ。よくあるヒントは npx キャッシュ起因の better-sqlite3 NODE_MODULE_VERSION 不一致で、rm -rf ~/.npm/_npx の後に npx -y @youtyan/code-viewer@latest を再実行する手順を表示します。"
22640
+ }
22641
+ ]
22401
22642
  }
22402
22643
  ]
22403
22644
  },
@@ -29886,7 +30127,8 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
29886
30127
  } : { view: route.screen };
29887
30128
  }
29888
30129
  function replaceUrlWithCurrentRoute() {
29889
- const url = withAnnotationSessionParam(buildRoute(STATE.route));
30130
+ const base2 = withAnnotationSessionParam(buildRoute(STATE.route));
30131
+ const url = withDoctorOverlay(base2, parseDoctorOverlay(window.location.pathname, window.location.search));
29890
30132
  const current = window.location.pathname + window.location.search;
29891
30133
  if (url !== current) {
29892
30134
  history.replaceState(historyStateForRoute(STATE.route), "", url + window.location.hash);
@@ -29950,7 +30192,7 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
29950
30192
  if (nextRoute.screen === "repo" || nextRoute.screen === "file" && (nextRoute.view === "blob" || nextRoute.view === "blame" || nextRoute.view === "history")) {
29951
30193
  STATE.repoRef = nextRoute.ref || "worktree";
29952
30194
  }
29953
- const url = withAnnotationSessionParam(buildRoute(nextRoute));
30195
+ const url = withDoctorOverlay(withAnnotationSessionParam(buildRoute(nextRoute)), parseDoctorOverlay(window.location.pathname, window.location.search));
29954
30196
  const state = historyStateForRoute(nextRoute);
29955
30197
  if (replace2)
29956
30198
  history.replaceState(state, "", url);
@@ -30225,6 +30467,10 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
30225
30467
  $("#sb-expand-all").addEventListener("click", () => setAllSidebarDirsCollapsed(false));
30226
30468
  $("#sb-collapse-all").addEventListener("click", () => setAllSidebarDirsCollapsed(true));
30227
30469
  $("#viewer-settings")?.addEventListener("click", toggleScopeSettings);
30470
+ $("#doctor-btn")?.addEventListener("click", (event) => {
30471
+ event.preventDefault();
30472
+ toggleDoctorSheet();
30473
+ });
30228
30474
  $("#scope-settings-close")?.addEventListener("click", closeScopeSettings);
30229
30475
  $("#scope-omit-reset")?.addEventListener("click", resetScopeSettings);
30230
30476
  $("#viewer-language")?.addEventListener("change", (event) => {
@@ -30704,6 +30950,7 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
30704
30950
  } else
30705
30951
  load();
30706
30952
  syncLineRefPill();
30953
+ syncDoctorSheetFromUrl();
30707
30954
  });
30708
30955
  function syncRefInputs() {
30709
30956
  const fi = $("#ref-from"), ti = $("#ref-to");
@@ -30792,6 +31039,69 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
30792
31039
  getLanguage: () => STATE.language,
30793
31040
  trackLoad
30794
31041
  });
31042
+ const DOCTOR_VIEW = createDoctorView({
31043
+ $: (sel) => document.querySelector(sel),
31044
+ escapeHtml: escapeHtml3,
31045
+ trackLoad,
31046
+ getLanguage: () => STATE.language,
31047
+ onWorstStatusChange: (status) => {
31048
+ const badge = document.getElementById("doctor-badge");
31049
+ if (!badge)
31050
+ return;
31051
+ if (status === "error") {
31052
+ badge.hidden = false;
31053
+ badge.dataset.level = "error";
31054
+ } else if (status === "warn") {
31055
+ badge.hidden = false;
31056
+ badge.dataset.level = "warn";
31057
+ } else {
31058
+ badge.hidden = true;
31059
+ badge.removeAttribute("data-level");
31060
+ }
31061
+ },
31062
+ onCloseRequest: () => closeDoctorSheet()
31063
+ });
31064
+ function isDoctorOverlayOpen() {
31065
+ return parseDoctorOverlay(window.location.pathname, window.location.search);
31066
+ }
31067
+ function updateUrlForDoctorOverlay(open) {
31068
+ const current = window.location.pathname + window.location.search;
31069
+ const next = withDoctorOverlay(current, open);
31070
+ if (next !== current) {
31071
+ history.replaceState(history.state, "", next + window.location.hash);
31072
+ }
31073
+ }
31074
+ function openDoctorSheet() {
31075
+ updateUrlForDoctorOverlay(true);
31076
+ DOCTOR_VIEW.open();
31077
+ }
31078
+ function closeDoctorSheet() {
31079
+ DOCTOR_VIEW.close();
31080
+ updateUrlForDoctorOverlay(false);
31081
+ }
31082
+ function toggleDoctorSheet() {
31083
+ if (isDoctorOverlayOpen() || DOCTOR_VIEW.isOpen())
31084
+ closeDoctorSheet();
31085
+ else
31086
+ openDoctorSheet();
31087
+ }
31088
+ function syncDoctorSheetFromUrl() {
31089
+ const shouldOpen = isDoctorOverlayOpen();
31090
+ const open = DOCTOR_VIEW.isOpen();
31091
+ if (shouldOpen && !open)
31092
+ DOCTOR_VIEW.open();
31093
+ else if (!shouldOpen && open)
31094
+ DOCTOR_VIEW.close();
31095
+ }
31096
+ document.getElementById("doctor-sheet-overlay")?.addEventListener("click", () => closeDoctorSheet());
31097
+ document.addEventListener("keydown", (event) => {
31098
+ if (event.key !== "Escape")
31099
+ return;
31100
+ if (!DOCTOR_VIEW.isOpen())
31101
+ return;
31102
+ event.preventDefault();
31103
+ closeDoctorSheet();
31104
+ });
30795
31105
  const DATABASE_VIEW = createDatabaseView({
30796
31106
  setRoute,
30797
31107
  setPageMode,
@@ -30852,6 +31162,7 @@ code-viewer query diff rows --id diff-xyz789 --table users --type inserted --lim
30852
31162
  syncRefInputs();
30853
31163
  syncHeaderMenu();
30854
31164
  syncLineRefPill();
31165
+ syncDoctorSheetFromUrl();
30855
31166
  if (isSameBlobFileRoute(previousRoute, STATE.route) && routeBlobPreview(previousRoute) !== routeBlobPreview(STATE.route) && switchSourceTab(routeBlobPreview(STATE.route) ? "preview" : "code", {
30856
31167
  updateRoute: false
30857
31168
  })) {
package/web/index.html CHANGED
@@ -33,6 +33,7 @@
33
33
  <div class="global-actions">
34
34
  <button id="annotations-toggle" class="global-icon-action" title="code annotations" aria-label="code annotations">💬<span id="annotations-count" hidden></span></button>
35
35
  <button id="viewer-settings" class="global-icon-action" title="viewer settings" aria-label="viewer settings"></button>
36
+ <button id="doctor-btn" class="global-icon-action" title="environment doctor" aria-label="environment doctor">🩺<span id="doctor-badge" class="doctor-badge" hidden></span></button>
36
37
  <button id="auto-update" class="global-icon-action active" title="auto update on file change">auto</button>
37
38
  <button id="cancel-requests" class="global-icon-action" title="no in-flight requests" aria-label="cancel in-flight requests" disabled>stop</button>
38
39
  <button id="theme" title="toggle theme">🌗</button>
@@ -251,6 +252,8 @@
251
252
  <div id="query-history-content"></div>
252
253
  </aside>
253
254
 
255
+ <div id="doctor-sheet-overlay" class="doctor-sheet-overlay" hidden aria-hidden="true"></div>
256
+ <aside id="doctor-sheet" class="doctor-sheet" hidden aria-hidden="true" inert aria-label="environment doctor"></aside>
254
257
  <main id="content">
255
258
  <section id="history-commit-info" hidden></section>
256
259
  <div id="empty" class="empty hidden">
package/web/style.css CHANGED
@@ -9699,3 +9699,200 @@ body.db-resizing {
9699
9699
  .gdp-blame-error {
9700
9700
  color: var(--danger, #c33);
9701
9701
  }
9702
+
9703
+ /* ===== Doctor sheet (right-side drawer) ===== */
9704
+ .doctor-sheet {
9705
+ position: fixed;
9706
+ top: 0;
9707
+ right: 0;
9708
+ bottom: 0;
9709
+ width: min(560px, 92vw);
9710
+ max-width: 100vw;
9711
+ background: var(--bg, #fff);
9712
+ border-left: 1px solid var(--border, #d0d7de);
9713
+ box-shadow: -8px 0 24px rgba(0, 0, 0, 0.18);
9714
+ z-index: 60;
9715
+ overflow-y: auto;
9716
+ padding: 16px 20px 32px;
9717
+ box-sizing: border-box;
9718
+ transform: translateX(100%);
9719
+ transition: transform 200ms ease;
9720
+ }
9721
+ .doctor-sheet:not([hidden]) {
9722
+ transform: translateX(0);
9723
+ }
9724
+ .doctor-sheet[hidden] {
9725
+ display: block;
9726
+ pointer-events: none;
9727
+ }
9728
+ .doctor-sheet-overlay {
9729
+ position: fixed;
9730
+ inset: 0;
9731
+ background: rgba(0, 0, 0, 0.28);
9732
+ z-index: 55;
9733
+ opacity: 0;
9734
+ transition: opacity 200ms ease;
9735
+ pointer-events: none;
9736
+ }
9737
+ .doctor-sheet-overlay:not([hidden]) {
9738
+ opacity: 1;
9739
+ pointer-events: auto;
9740
+ }
9741
+ .doctor-sheet-overlay[hidden] {
9742
+ display: block;
9743
+ opacity: 0;
9744
+ pointer-events: none;
9745
+ }
9746
+ .doctor-header {
9747
+ display: flex;
9748
+ align-items: center;
9749
+ gap: 12px;
9750
+ padding: 8px 0 16px;
9751
+ border-bottom: 1px solid var(--border, #d0d7de);
9752
+ margin-bottom: 16px;
9753
+ }
9754
+ .doctor-close {
9755
+ font: inherit;
9756
+ font-size: 18px;
9757
+ line-height: 1;
9758
+ width: 28px;
9759
+ height: 28px;
9760
+ border-radius: 6px;
9761
+ border: 1px solid transparent;
9762
+ background: transparent;
9763
+ color: var(--fg-muted, #57606a);
9764
+ cursor: pointer;
9765
+ }
9766
+ .doctor-close:hover {
9767
+ background: var(--bg-mute, #eaeef2);
9768
+ color: var(--fg, #1f2328);
9769
+ }
9770
+ .doctor-header h1 {
9771
+ font-size: 18px;
9772
+ margin: 0;
9773
+ flex: 1;
9774
+ }
9775
+ .doctor-refresh {
9776
+ font: inherit;
9777
+ font-size: 12px;
9778
+ padding: 4px 12px;
9779
+ border-radius: 6px;
9780
+ border: 1px solid var(--border, #d0d7de);
9781
+ background: var(--bg-soft, #f6f8fa);
9782
+ color: var(--fg);
9783
+ cursor: pointer;
9784
+ }
9785
+ .doctor-refresh:hover {
9786
+ background: var(--bg-mute, #eaeef2);
9787
+ }
9788
+ .doctor-refresh[disabled] {
9789
+ cursor: progress;
9790
+ opacity: 0.6;
9791
+ }
9792
+ .doctor-summary {
9793
+ font-size: 12px;
9794
+ color: var(--fg-muted);
9795
+ margin-bottom: 16px;
9796
+ }
9797
+ .doctor-group {
9798
+ border: 1px solid var(--border, #d0d7de);
9799
+ border-radius: 8px;
9800
+ padding: 12px 16px;
9801
+ margin-bottom: 12px;
9802
+ background: var(--bg, #fff);
9803
+ }
9804
+ .doctor-group-title {
9805
+ font-size: 13px;
9806
+ font-weight: 600;
9807
+ margin: 0 0 8px 0;
9808
+ color: var(--fg-muted);
9809
+ text-transform: uppercase;
9810
+ letter-spacing: 0.04em;
9811
+ }
9812
+ .doctor-row {
9813
+ display: grid;
9814
+ grid-template-columns: 90px 1fr;
9815
+ gap: 12px;
9816
+ padding: 8px 0;
9817
+ border-top: 1px solid var(--bg-mute, #eaeef2);
9818
+ align-items: start;
9819
+ }
9820
+ .doctor-row:first-of-type {
9821
+ border-top: 0;
9822
+ }
9823
+ .doctor-row-pill {
9824
+ font-size: 10px;
9825
+ font-weight: 700;
9826
+ text-transform: uppercase;
9827
+ letter-spacing: 0.06em;
9828
+ padding: 2px 8px;
9829
+ border-radius: 999px;
9830
+ border: 1px solid transparent;
9831
+ text-align: center;
9832
+ align-self: start;
9833
+ min-width: 56px;
9834
+ }
9835
+ .doctor-row-pill-ok {
9836
+ background: rgba(46, 160, 67, 0.15);
9837
+ border-color: rgba(46, 160, 67, 0.4);
9838
+ color: var(--success, #2da44e);
9839
+ }
9840
+ .doctor-row-pill-warn {
9841
+ background: rgba(212, 167, 44, 0.18);
9842
+ border-color: rgba(212, 167, 44, 0.45);
9843
+ color: var(--attn, #9a6700);
9844
+ }
9845
+ .doctor-row-pill-error {
9846
+ background: rgba(207, 34, 46, 0.15);
9847
+ border-color: rgba(207, 34, 46, 0.4);
9848
+ color: var(--danger, #cf222e);
9849
+ }
9850
+ .doctor-row-body {
9851
+ display: flex;
9852
+ flex-direction: column;
9853
+ gap: 4px;
9854
+ min-width: 0;
9855
+ }
9856
+ .doctor-row-title {
9857
+ font-weight: 600;
9858
+ font-size: 13px;
9859
+ color: var(--fg);
9860
+ }
9861
+ .doctor-row-detail {
9862
+ font-size: 12px;
9863
+ color: var(--fg-muted);
9864
+ word-break: break-word;
9865
+ white-space: pre-wrap;
9866
+ }
9867
+ .doctor-row-hint {
9868
+ font-size: 12px;
9869
+ color: var(--fg);
9870
+ background: rgba(151, 119, 0, 0.08);
9871
+ border-left: 3px solid var(--attn, #9a6700);
9872
+ padding: 8px 10px;
9873
+ border-radius: 4px;
9874
+ margin-top: 4px;
9875
+ white-space: pre-wrap;
9876
+ word-break: break-word;
9877
+ }
9878
+ .doctor-row-pill-error ~ .doctor-row-body .doctor-row-hint {
9879
+ background: rgba(207, 34, 46, 0.08);
9880
+ border-left-color: var(--danger, #cf222e);
9881
+ }
9882
+ .doctor-empty {
9883
+ padding: 32px;
9884
+ text-align: center;
9885
+ color: var(--fg-muted);
9886
+ font-size: 13px;
9887
+ }
9888
+ .doctor-badge {
9889
+ display: inline-flex;
9890
+ margin-left: 4px;
9891
+ width: 8px;
9892
+ height: 8px;
9893
+ border-radius: 50%;
9894
+ background: var(--danger, #cf222e);
9895
+ }
9896
+ .doctor-badge[data-level="warn"] {
9897
+ background: var(--attn, #9a6700);
9898
+ }