Commit bd61e8e6 authored by edy's avatar edy

feat(ui): add colorful expert icons

parent 763794d2
Pipeline #18480 failed
......@@ -31,6 +31,7 @@ typecheck_windows:
- node --version
- corepack --version
- corepack pnpm install --frozen-lockfile --store-dir "$env:PNPM_STORE_DIR"
- corepack pnpm --filter @qjclaw/ui run test:icons
- corepack pnpm typecheck
package_windows_installer:
......
......@@ -8,6 +8,7 @@
"clean": "rimraf dist",
"dev": "vite",
"lint": "tsc --noEmit",
"test:icons": "node --test test/expertIconSource.test.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
......
......@@ -93,10 +93,11 @@ export function DouyinNoteIcon() {
function BrowserExpertIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M4 8.25A2.25 2.25 0 0 1 6.25 6h11.5A2.25 2.25 0 0 1 20 8.25v7.5A2.25 2.25 0 0 1 17.75 18H6.25A2.25 2.25 0 0 1 4 15.75v-7.5Z" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" />
<path d="M4.75 9.25h14.5" fill="none" stroke="currentColor" strokeLinecap="round" strokeWidth="1.6" />
<circle cx="7.4" cy="7.7" r="0.75" fill="currentColor" />
<circle cx="10.1" cy="7.7" r="0.75" fill="currentColor" opacity="0.78" />
<path d="M4 8.25A2.25 2.25 0 0 1 6.25 6h11.5A2.25 2.25 0 0 1 20 8.25v7.5A2.25 2.25 0 0 1 17.75 18H6.25A2.25 2.25 0 0 1 4 15.75v-7.5Z" fill="#EFF6FF" stroke="#3B82F6" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.6" />
<path d="M4.75 9.25h14.5" fill="none" stroke="#60A5FA" strokeLinecap="round" strokeWidth="1.45" />
<circle cx="7.4" cy="7.7" r="0.75" fill="#EF4444" />
<circle cx="10.1" cy="7.7" r="0.75" fill="#F59E0B" />
<path d="M8 13.5h4.1M8 15.55h7.7" fill="none" stroke="#22C55E" strokeLinecap="round" strokeWidth="1.35" />
</svg>
);
}
......@@ -104,9 +105,11 @@ function BrowserExpertIcon() {
function PlannerExpertIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M5 6.25A2.25 2.25 0 0 1 7.25 4h9.5A2.25 2.25 0 0 1 19 6.25v11.5A2.25 2.25 0 0 1 16.75 20h-9.5A2.25 2.25 0 0 1 5 17.75V6.25Z" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" />
<path d="M8 8.25h8M8 12h5.5M8 15.75h3.5" fill="none" stroke="currentColor" strokeLinecap="round" strokeWidth="1.7" />
<circle cx="16.9" cy="15.8" r="1.6" fill="currentColor" opacity="0.88" />
<path d="M5 6.25A2.25 2.25 0 0 1 7.25 4h9.5A2.25 2.25 0 0 1 19 6.25v11.5A2.25 2.25 0 0 1 16.75 20h-9.5A2.25 2.25 0 0 1 5 17.75V6.25Z" fill="#FFF7ED" stroke="#F97316" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.6" />
<path d="M8.2 8.1h5.7M8.2 11.1h4.5M8.2 16.1h3.1" fill="none" stroke="#2563EB" strokeLinecap="round" strokeWidth="1.45" />
<circle cx="15.8" cy="15.6" r="2.2" fill="#DBEAFE" stroke="#2563EB" strokeWidth="1.2" />
<path d="M15.8 13.4v2.2l1.7 1" fill="none" stroke="#F97316" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.15" />
<path d="M7.8 4v2.3M16.2 4v2.3" fill="none" stroke="#FB923C" strokeLinecap="round" strokeWidth="1.5" />
</svg>
);
}
......@@ -114,8 +117,9 @@ function PlannerExpertIcon() {
function ZhihuExpertIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M5.25 6.25A2.25 2.25 0 0 1 7.5 4h9A2.25 2.25 0 0 1 18.75 6.25v8.1a2.25 2.25 0 0 1-2.25 2.25h-3.4l-2.95 2.45c-.48.4-1.2.06-1.2-.57V16.6H7.5a2.25 2.25 0 0 1-2.25-2.25v-8.1Z" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" />
<path d="M8.2 9.1h7.6M8.2 12.2h5.4" fill="none" stroke="currentColor" strokeLinecap="round" strokeWidth="1.7" />
<path d="M5.25 6.25A2.25 2.25 0 0 1 7.5 4h9A2.25 2.25 0 0 1 18.75 6.25v8.1a2.25 2.25 0 0 1-2.25 2.25h-3.4l-2.95 2.45c-.48.4-1.2.06-1.2-.57V16.6H7.5a2.25 2.25 0 0 1-2.25-2.25v-8.1Z" fill="#1769FF" stroke="#0F4FD8" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.35" />
<path d="M8 8.35h5.2M8 11.15h7.9M8 13.95h4.6" fill="none" stroke="#ffffff" strokeLinecap="round" strokeWidth="1.45" />
<circle cx="15.65" cy="8.35" r="1.15" fill="#ffffff" opacity="0.92" />
</svg>
);
}
......@@ -123,8 +127,8 @@ function ZhihuExpertIcon() {
function WechatExpertIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M10 5C6.69 5 4 7.27 4 10.08c0 1.52.8 2.88 2.07 3.8L5.4 16.5l2.7-1.35c.6.14 1.23.21 1.9.21 3.32 0 6-2.27 6-5.08S13.32 5 10 5Z" fill="currentColor" opacity="0.92" />
<path d="M15.3 9.3c2.6 0 4.7 1.8 4.7 4.02 0 1.2-.62 2.27-1.62 3l.54 2.08-2.13-1.06c-.47.11-.97.17-1.5.17-2.6 0-4.7-1.8-4.7-4.02S12.7 9.3 15.3 9.3Z" fill="currentColor" opacity="0.68" />
<path d="M10 5C6.69 5 4 7.27 4 10.08c0 1.52.8 2.88 2.07 3.8L5.4 16.5l2.7-1.35c.6.14 1.23.21 1.9.21 3.32 0 6-2.27 6-5.08S13.32 5 10 5Z" fill="#22C55E" />
<path d="M15.3 9.3c2.6 0 4.7 1.8 4.7 4.02 0 1.2-.62 2.27-1.62 3l.54 2.08-2.13-1.06c-.47.11-.97.17-1.5.17-2.6 0-4.7-1.8-4.7-4.02S12.7 9.3 15.3 9.3Z" fill="#16A34A" />
<circle cx="8.35" cy="9.95" r="0.9" fill="#ffffff" />
<circle cx="11.55" cy="9.95" r="0.9" fill="#ffffff" />
<circle cx="14.05" cy="13.25" r="0.8" fill="#ffffff" />
......@@ -136,7 +140,8 @@ function WechatExpertIcon() {
function XPlatformExpertIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M6 5.25h3.4l2.86 4.2 3.53-4.2H18l-4.72 5.61L18.5 18h-3.4l-3.11-4.55L8.1 18H6l5.02-5.97L6 5.25Z" fill="currentColor" />
<rect x="4.25" y="4.25" width="15.5" height="15.5" rx="4" fill="#050505" />
<path d="M7 6.9h3.35l2.34 3.24 2.84-3.24h1.5l-3.65 4.17L17.25 17h-3.3l-2.58-3.66L8.17 17H6.65l3.99-4.57L7 6.9Zm1.9 1.25 5.68 7.58h.76L9.69 8.15H8.9Z" fill="#ffffff" />
</svg>
);
}
......@@ -144,8 +149,9 @@ function XPlatformExpertIcon() {
function TikTokExpertIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M13.75 4.25v8.5a3.75 3.75 0 1 1-2-3.3V7.1c1.42.05 2.63-.33 3.78-1.18.25-.19.62-.01.62.3v1.35c0 1.15.63 2.2 1.64 2.73.35.19.72.33 1.11.43" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" />
<circle cx="8.1" cy="15.7" r="2" fill="currentColor" opacity="0.24" />
<path d="M12.85 4.35v8.65a3.85 3.85 0 1 1-2.05-3.39V7.23c1.43.05 2.68-.34 3.84-1.2.25-.19.62-.01.62.31v1.37c0 1.17.64 2.23 1.66 2.77.36.19.74.34 1.14.44" fill="none" stroke="#25F4EE" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.25" />
<path d="M14.25 4.35v8.65a3.85 3.85 0 1 1-2.05-3.39V7.23c1.43.05 2.68-.34 3.84-1.2.25-.19.62-.01.62.31v1.37c0 1.17.64 2.23 1.66 2.77.36.19.74.34 1.14.44" fill="none" stroke="#FE2C55" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.25" />
<path d="M13.55 4.35v8.65a3.85 3.85 0 1 1-2.05-3.39V7.23c1.43.05 2.68-.34 3.84-1.2.25-.19.62-.01.62.31v1.37c0 1.17.64 2.23 1.66 2.77.36.19.74.34 1.14.44" fill="none" stroke="#111827" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" />
</svg>
);
}
......@@ -153,9 +159,10 @@ function TikTokExpertIcon() {
function PosterExpertIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M6 4.75A1.75 1.75 0 0 1 7.75 3h8.5A1.75 1.75 0 0 1 18 4.75v14.5A1.75 1.75 0 0 1 16.25 21h-8.5A1.75 1.75 0 0 1 6 19.25V4.75Z" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" />
<path d="M8.6 8.2h6.8M8.6 11.4h6.8M8.6 14.6h4.4" fill="none" stroke="currentColor" strokeLinecap="round" strokeWidth="1.7" />
<circle cx="14.9" cy="14.7" r="1.1" fill="currentColor" opacity="0.88" />
<path d="M6 4.75A1.75 1.75 0 0 1 7.75 3h8.5A1.75 1.75 0 0 1 18 4.75v14.5A1.75 1.75 0 0 1 16.25 21h-8.5A1.75 1.75 0 0 1 6 19.25V4.75Z" fill="#FDF2F8" stroke="#EC4899" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.55" />
<path d="M8.4 7.4h7.2v4.1H8.4z" fill="#F59E0B" opacity="0.9" />
<path d="M8.5 14.1h4.8M8.5 16.55h3.25" fill="none" stroke="#DB2777" strokeLinecap="round" strokeWidth="1.35" />
<circle cx="15.1" cy="15.65" r="1.35" fill="#2563EB" />
</svg>
);
}
......@@ -163,9 +170,10 @@ function PosterExpertIcon() {
function GeoExpertIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="7.25" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="M12 4.75c2.05 1.95 3.25 4.55 3.25 7.25S14.05 17.3 12 19.25C9.95 17.3 8.75 14.7 8.75 12S9.95 6.7 12 4.75Z" fill="none" stroke="currentColor" strokeWidth="1.7" />
<path d="M5 12h14" fill="none" stroke="currentColor" strokeLinecap="round" strokeWidth="1.6" />
<circle cx="11" cy="11" r="6.7" fill="#DBEAFE" stroke="#2563EB" strokeWidth="1.55" />
<path d="M11 4.3c1.9 1.8 3 4.2 3 6.7s-1.1 4.9-3 6.7C9.1 15.9 8 13.5 8 11s1.1-4.9 3-6.7Z" fill="#D1FAE5" stroke="#10B981" strokeWidth="1.25" />
<path d="M4.7 11h12.6" fill="none" stroke="#2563EB" strokeLinecap="round" strokeWidth="1.35" />
<path d="m15.8 15.8 3.1 3.1" fill="none" stroke="#059669" strokeLinecap="round" strokeWidth="1.9" />
</svg>
);
}
......@@ -173,10 +181,11 @@ function GeoExpertIcon() {
function LeadsExpertIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="6.9" fill="none" stroke="currentColor" strokeWidth="1.8" />
<circle cx="12" cy="12" r="3.6" fill="none" stroke="currentColor" strokeWidth="1.7" opacity="0.86" />
<circle cx="12" cy="12" r="1.4" fill="currentColor" />
<path d="M12 3.8v2.1M20.2 12h-2.1M12 20.2v-2.1M3.8 12h2.1" fill="none" stroke="currentColor" strokeLinecap="round" strokeWidth="1.5" />
<circle cx="12" cy="12" r="7" fill="#FEF2F2" stroke="#EF4444" strokeWidth="1.65" />
<circle cx="12" cy="12" r="4" fill="#FFFBEB" stroke="#F59E0B" strokeWidth="1.45" />
<circle cx="12" cy="12" r="1.45" fill="#EF4444" />
<path d="M12 3.8v2M20.2 12h-2M12 20.2v-2M3.8 12h2" fill="none" stroke="#DC2626" strokeLinecap="round" strokeWidth="1.35" />
<path d="m15.7 7.7 2.15-1.25M16.3 8.9h2.45" fill="none" stroke="#2563EB" strokeLinecap="round" strokeWidth="1.25" />
</svg>
);
}
......@@ -184,10 +193,10 @@ function LeadsExpertIcon() {
function SalesChampionExpertIcon() {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M8 5.25h8v2.1a4 4 0 0 1-3 3.88v2.02h2.1a1.4 1.4 0 0 1 1.4 1.4V16H7.5v-1.35a1.4 1.4 0 0 1 1.4-1.4H11v-2.02a4 4 0 0 1-3-3.88v-2.1Z" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" />
<path d="M8 6.2H5.9A1.65 1.65 0 0 0 4.25 7.85c0 1.74 1.41 3.15 3.15 3.15H8m8-4.8h2.1a1.65 1.65 0 0 1 1.65 1.65c0 1.74-1.41 3.15-3.15 3.15H16" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.6" />
<path d="M9.8 19h4.4M8.9 16h6.2v3H8.9z" fill="currentColor" opacity="0.18" />
<path d="m12 7.55.68 1.38 1.52.22-1.1 1.07.26 1.51L12 11.03l-1.36.7.26-1.51-1.1-1.07 1.52-.22L12 7.55Z" fill="currentColor" />
<path d="M8 5.25h8v2.1a4 4 0 0 1-3 3.88v2.02h2.1a1.4 1.4 0 0 1 1.4 1.4V16H7.5v-1.35a1.4 1.4 0 0 1 1.4-1.4H11v-2.02a4 4 0 0 1-3-3.88v-2.1Z" fill="#FDE68A" stroke="#F59E0B" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.55" />
<path d="M8 6.2H5.9A1.65 1.65 0 0 0 4.25 7.85c0 1.74 1.41 3.15 3.15 3.15H8m8-4.8h2.1a1.65 1.65 0 0 1 1.65 1.65c0 1.74-1.41 3.15-3.15 3.15H16" fill="none" stroke="#D97706" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.45" />
<path d="M9.8 19h4.4M8.9 16h6.2v3H8.9z" fill="#92400E" opacity="0.24" />
<path d="m12 7.55.68 1.38 1.52.22-1.1 1.07.26 1.51L12 11.03l-1.36.7.26-1.51-1.1-1.07 1.52-.22L12 7.55Z" fill="#F59E0B" />
</svg>
);
}
......
import test from "node:test"
import assert from "node:assert/strict"
import { readFileSync } from "node:fs"
const uiPackageSource = readFileSync(new URL("../package.json", import.meta.url), "utf8")
const iconSource = readFileSync(new URL("../src/components/icons/AppIcons.tsx", import.meta.url), "utf8")
function functionBlock(name: string): string {
const start = iconSource.indexOf(`function ${name}()`)
assert.notEqual(start, -1, `Missing icon function: ${name}`)
const nextFunction = iconSource.indexOf("\nfunction ", start + 1)
const renderFunction = iconSource.indexOf("\nexport function renderExpertIcon", start + 1)
const candidates = [nextFunction, renderFunction].filter((index) => index > start)
const end = Math.min(...candidates)
assert.notEqual(end, Infinity, `Missing block end for icon function: ${name}`)
return iconSource.slice(start, end)
}
test("expert icon source test is exposed as a package script", () => {
const uiPackage = JSON.parse(uiPackageSource) as { scripts?: Record<string, string> }
assert.equal(uiPackage.scripts?.["test:icons"], "node --test test/expertIconSource.test.ts")
})
test("browser expert icon avoids reusable svg ids", () => {
const block = functionBlock("BrowserExpertIcon")
assert.doesNotMatch(block, /<defs|id=|url\(#/, "BrowserExpertIcon should avoid duplicate SVG ids across repeated renders")
})
test("non-redbook and non-douyin expert icons use fixed brand colors", () => {
const expectedColorsByIcon = new Map([
["BrowserExpertIcon", ["#3B82F6", "#22C55E"]],
["PlannerExpertIcon", ["#F97316", "#2563EB"]],
["ZhihuExpertIcon", ["#1769FF", "#ffffff"]],
["WechatExpertIcon", ["#22C55E", "#16A34A"]],
["XPlatformExpertIcon", ["#050505", "#ffffff"]],
["TikTokExpertIcon", ["#25F4EE", "#FE2C55"]],
["PosterExpertIcon", ["#EC4899", "#F59E0B"]],
["GeoExpertIcon", ["#2563EB", "#10B981"]],
["LeadsExpertIcon", ["#EF4444", "#F59E0B"]],
["SalesChampionExpertIcon", ["#F59E0B", "#FDE68A"]]
])
for (const [iconName, colors] of expectedColorsByIcon) {
const block = functionBlock(iconName)
assert.doesNotMatch(block, /currentColor/, `${iconName} should not inherit a monochrome text color`)
for (const color of colors) {
assert.match(block, new RegExp(color, "i"), `${iconName} should include ${color}`)
}
}
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment