fix(control-ui): preserve emoji in plugin fallback monograms (#105240)

* fix(control-ui): preserve emoji in plugin monograms

* test(ui): cover combining-mark monograms

Co-authored-by: zhang-guiping <zhang.guiping@xydigit.com>

---------

Co-authored-by: Colin Johnson <colin@solvely.net>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
This commit is contained in:
xingzhou
2026-07-16 00:59:37 -07:00
committed by GitHub
co-authored by Colin Johnson Peter Steinberger Peter Steinberger
parent c6a9f5339b
commit f98de3fc25
2 changed files with 60 additions and 1 deletions
+24 -1
View File
@@ -206,6 +206,27 @@ const FALLBACK_GRADIENTS: ReadonlyArray<readonly [string, string]> = [
["#fb7185", "#9f1239"],
];
const graphemeSegmenter =
typeof Intl.Segmenter === "function"
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
: null;
function takeGraphemes(input: string, limit: number): string {
if (!graphemeSegmenter) {
return Array.from(input).slice(0, limit).join("");
}
let result = "";
let count = 0;
for (const { segment } of graphemeSegmenter.segment(input)) {
result += segment;
count += 1;
if (count >= limit) {
break;
}
}
return result;
}
export function pluginFallbackGradient(id: string): readonly [string, string] {
let hash = 0;
for (const char of id) {
@@ -224,7 +245,9 @@ export function pluginMonogram(name: string): string {
}
const first = expectDefined(words[0], "plugin monogram first word");
const second = words[1];
const initials = second ? `${first.charAt(0)}${second.charAt(0)}` : first.slice(0, 2);
const initials = second
? `${takeGraphemes(first, 1)}${takeGraphemes(second, 1)}`
: takeGraphemes(first, 2);
return initials.toLocaleUpperCase();
}
+36
View File
@@ -146,6 +146,42 @@ describe("renderPlugins", () => {
).toContain("manifest invalid");
});
it("keeps plugin fallback monograms on complete grapheme clusters", () => {
const cases = [
{ id: "emoji-tools", name: "😀 Tools", expected: "😀T" },
{ id: "mixed-emoji", name: "A😀", expected: "A😀" },
{ id: "heart-tools", name: "❤️ Tools", expected: "❤️T" },
{ id: "flag-tools", name: "🇺🇸 Tools", expected: "🇺🇸T" },
{ id: "developer-tools", name: "👩‍💻 Tools", expected: "👩‍💻T" },
{ id: "developer-name", name: "👩‍💻Dev", expected: "👩‍💻D" },
{ id: "combining-mark", name: "é Tools", expected: "ÉT" },
];
const plugins = cases.map(({ id, name }) => createPlugin({ id, name, origin: "global" }));
const container = mount(createProps({ result: createResult(plugins) }));
for (const { id, expected } of cases) {
expect(
container.querySelector(`[data-plugin-id="${id}"] .plugins-tile--fallback > span`)
?.textContent,
).toBe(expected);
}
});
it("keeps plugin monograms usable when Intl.Segmenter is unavailable", async () => {
const originalSegmenter = Intl.Segmenter;
Object.defineProperty(Intl, "Segmenter", { configurable: true, value: undefined });
vi.resetModules();
try {
const { pluginMonogram } = await import("./presentation.ts");
expect(pluginMonogram("😀 Tools")).toBe("😀T");
expect(pluginMonogram("👩‍💻 Tools")).toBe("👩T");
} finally {
Object.defineProperty(Intl, "Segmenter", { configurable: true, value: originalSegmenter });
vi.resetModules();
}
});
it("filters the installed inventory by state", () => {
const plugins = [
createPlugin({ id: "on", name: "On", enabled: true, state: "enabled" }),