fix(ui): Match PyCharm project view, matched tags and text weight

- Project widget initials follow IDEA's AvatarUtils.initials ("idea" → "I").
- "Show directories with changed descendants" is off by default, as in IDEA.
- FileEntry gains `excluded`: only excluded folders get the excluded icon and
  dimming; git-ignored ones keep the plain icon and the ignored colour.
- `.github` folders get IDEA's GitHub folder icon.
- Matched tag names are never bold and use their own background
  (MATCHED_TAG_NAME); matched braces are bold in dark schemes only.
- Revert Greyscale antialiasing to `antialiased` for UI and editor; `auto`
  rendered text heavier than PyCharm.

Claude-Session: https://claude.ai/code/session_01SaPPsVPuk6ZTNsoUCNCbMv
Цей коміт міститься в:
l0ng-ai committed 2026-10-24 22:56:48 +08:00
1 джерело cacddee19d
коміт 046fd37064
26 files changed
+143 -36

No files matched your search

+15
Переглянути файл
@@ -0,0 +1,15 @@
import { describe, expect, it } from "vitest";
import { projectInitials } from "./projectColor";
describe("projectInitials", () => {
it("follows IDEA's AvatarUtils.initials", () => {
expect(projectInitials("idea")).toBe("I");
expect(projectInitials("MyProject")).toBe("MP");
expect(projectInitials("OpenIDEA")).toBe("OI");
expect(projectInitials("py-demo")).toBe("PD");
expect(projectInitials("web demo")).toBe("WD");
expect(projectInitials("a.b.c")).toBe("AC");
expect(projectInitials("123abc")).toBe("A");
expect(projectInitials("")).toBe("?");
});
});
+23 -9
Переглянути файл
@@ -1,6 +1,6 @@
/**
* IDEA-style project identity: a colour slot (1–9, like RecentProject.ColorN)
* derived from the project name, and the two-letter initials shown in the
* derived from the project name, and the initials shown in the
* title bar's project widget.
*/
@@ -13,15 +13,29 @@ export function projectColorIndex(name: string): number {
return (Math.abs(h) % PROJECT_COLOR_COUNT) + 1;
}
/** "py-demo" → "PD", "openidea" → "OP", "MyProject" → "MP". */
/**
* IDEA's AvatarUtils.initials: two camel-case capitals, else the first letters
* of the first and last words, else one letter. "py-demo" → "PD",
* "MyProject" → "MP", "idea" → "I".
*/
export function projectInitials(name: string): string {
const words = name
.replace(/([a-z])([A-Z])/g, "$1 $2")
.split(/[^\p{L}\p{N}]+/u)
.filter(Boolean);
if (words.length === 0) return "?";
if (words.length === 1) return [...words[0]!].slice(0, 2).join("").toUpperCase();
return (words[0]![0]! + words[1]![0]!).toUpperCase();
const text = name.trim();
const camel = camelInitials(text);
if (camel.length === 2) return camel;
for (const separators of [" ", ",", "-", "_", ".", "`'\""]) {
const words = text.split(new RegExp(`[${separators}]`)).filter(Boolean);
if (words.length >= 2) return ([...words[0]!][0]! + [...words.at(-1)!][0]!).toUpperCase();
}
return camel || "?";
}
/** First letter plus the next capital of the leading word: "MyProject" → "MP", "idea" → "I". */
function camelInitials(text: string): string {
const word = /\p{L}[\p{L}\p{N}]*/u.exec(text)?.[0] ?? "";
const [first, ...rest] = [...word];
if (!first) return "";
const capital = rest.find((c) => c !== c.toLowerCase());
return (first + (capital ?? "")).toUpperCase();
}
/**