mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
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:
co-authored by
Colin Johnson
Peter Steinberger
Peter Steinberger
parent
c6a9f5339b
commit
f98de3fc25
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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" }),
|
||||
|
||||
Reference in New Issue
Block a user