diff --git a/.changeset/refresh-web-icons.md b/.changeset/refresh-web-icons.md new file mode 100644 index 000000000..6d6d022af --- /dev/null +++ b/.changeset/refresh-web-icons.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Refresh the web UI icon set and unify the message copy and undo button hover states and tooltips. diff --git a/apps/kimi-web/design/design-system.html b/apps/kimi-web/design/design-system.html index 261b678d4..9ca0de3b8 100644 --- a/apps/kimi-web/design/design-system.html +++ b/apps/kimi-web/design/design-system.html @@ -1039,79 +1039,82 @@

图标 Icon

-

图标一律从集中注册表 lib/icons.ts 取用:模板中用 <Icon name size /> 组件(components/ui/Icon.vue),v-html 场景(如工具 glyph)用 iconSvg(name, size)禁止手写 <svg>——scripts/check-style.mjsicon-from-registry 规则会对游离 SVG 告警。默认线稿风格:描边 1.5px、stroke="currentColor"stroke-linecap/linejoin: round、无 fill,颜色跟随文字;尺寸走下方三档 token。少数「实心即语义」的图标(play / stop / star)为 fill 例外。

+

图标一律从集中注册表 lib/icons.ts 取用:模板中用 <Icon name size /> 组件(components/ui/Icon.vue),v-html 场景(如工具 glyph)用 iconSvg(name, size)禁止手写 <svg>——scripts/check-style.mjsicon-from-registry 规则会对游离 SVG 告警。图标来自 Remix Icon(Apache-2.0),统一为 fill 风格(fill="currentColor"、24×24 源网格),颜色跟随文字;尺寸走下方三档 token。注册表由 scripts/gen-icon-data.mjs@iconify-json/ri 按需生成,离线且 tree-shaken。

尺寸阶梯 Size scale

-
sm · 14
-
md · 16
-
lg · 20
+
sm · 14
+
md · 16
+
lg · 20

图标库 Icon library

-

当前注册的图标(按用途分组)。新增图标时,把图形以 name → body 加入 lib/icons.ts,按需补 viewBox(默认 16×16)/fill,再用 node --experimental-strip-types scripts/gen-icon-catalog.mjs 重新生成本目录,保证注册表与文档不漂移。

+

当前注册的图标(按用途分组)。图标数据由 scripts/gen-icon-data.mjs 中的 NAME_TO_REMIX 映射决定,运行 pnpm gen:icons 生成 lib/icon-data.ts;本目录由 node --experimental-strip-types scripts/gen-icon-catalog.mjs 重新生成,保证注册表与文档不漂移。

-
Actions
-
plus
-
close
-
check
-
search
-
copy
-
link
-
external-link
-
download
-
undo
-
send
-
image
-
settings
-
sliders
-
log-in
-
Navigation & layout
-
chevron-down
-
chevron-right
-
arrow-up
-
arrow-down
-
arrow-right
-
minus
-
panel-collapse
-
panel-expand
-
expand
-
collapse
-
list
-
Files & tools
-
folder
-
folder-closed
-
folder-plus
-
folder-solid
-
file
-
file-text
-
file-plus
-
file-off
-
image-off
-
code
-
terminal
-
pencil
-
glob
-
globe
-
check-list
-
bolt
-
git-pull-request
-
Communication
-
message
-
mail
-
user
-
Status & media
-
info
-
help-circle
-
alert-triangle
-
clock
-
sparkles
-
play
-
stop
-
star
-
star-outline
-
dots-vertical
-
dots-horizontal
+
Actions
+
plus
+
chat-new
+
close
+
check
+
search
+
copy
+
link
+
external-link
+
download
+
undo
+
send
+
image
+
settings
+
sliders
+
log-in
+
Navigation & layout
+
chevron-down
+
chevron-right
+
arrow-up
+
arrow-down
+
arrow-right
+
minus
+
panel-collapse
+
panel-expand
+
expand
+
collapse
+
list
+
Files & tools
+
folder
+
folder-closed
+
folder-plus
+
folder-solid
+
file
+
file-text
+
file-plus
+
file-off
+
image-off
+
code
+
terminal
+
pencil
+
glob
+
globe
+
check-list
+
bolt
+
git-pull-request
+
Communication
+
message
+
mail
+
user
+
Status & media
+
info
+
help-circle
+
alert-triangle
+
clock
+
sparkles
+
play
+
stop
+
star
+
star-outline
+
dots-horizontal
+
Other
+
sort
+
grip

禁止用 emoji 充当功能图标(唯一例外:月相 🌑…🌘,仅用于「等待 Agent 响应」聊天态)。Kimi 品牌标记(32×22 的眼睛 logo)属于品牌资产,不在此线稿图标体系内。

diff --git a/apps/kimi-web/package.json b/apps/kimi-web/package.json index 9d60122f7..1b2eb5f2b 100644 --- a/apps/kimi-web/package.json +++ b/apps/kimi-web/package.json @@ -10,11 +10,15 @@ "build": "vite build", "typecheck": "vue-tsc --noEmit", "test": "vitest run", - "check:style": "node scripts/check-style.mjs" + "check:style": "node scripts/check-style.mjs", + "gen:icons": "node scripts/gen-icon-data.mjs" }, "dependencies": { "@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/jetbrains-mono": "^5.2.8", + "@iconify-json/ri": "^1.2.10", + "@iconify/utils": "^3.1.3", + "@iconify/vue": "^5.0.1", "@xterm/addon-fit": "^0.11.0", "@xterm/xterm": "^6.0.0", "katex": "^0.17.0", diff --git a/apps/kimi-web/public/design-system.html b/apps/kimi-web/public/design-system.html index 8425e98cd..96da55a35 100644 --- a/apps/kimi-web/public/design-system.html +++ b/apps/kimi-web/public/design-system.html @@ -1038,79 +1038,82 @@

图标 Icon

-

图标一律从集中注册表 lib/icons.ts 取用:模板中用 <Icon name size /> 组件(components/ui/Icon.vue),v-html 场景(如工具 glyph)用 iconSvg(name, size)禁止手写 <svg>——scripts/check-style.mjsicon-from-registry 规则会对游离 SVG 告警。默认线稿风格:描边 1.5px、stroke="currentColor"stroke-linecap/linejoin: round、无 fill,颜色跟随文字;尺寸走下方三档 token。少数「实心即语义」的图标(play / stop / star)为 fill 例外。

+

图标一律从集中注册表 lib/icons.ts 取用:模板中用 <Icon name size /> 组件(components/ui/Icon.vue),v-html 场景(如工具 glyph)用 iconSvg(name, size)禁止手写 <svg>——scripts/check-style.mjsicon-from-registry 规则会对游离 SVG 告警。图标来自 Remix Icon(Apache-2.0),统一为 fill 风格(fill="currentColor"、24×24 源网格),颜色跟随文字;尺寸走下方三档 token。注册表由 scripts/gen-icon-data.mjs@iconify-json/ri 按需生成,离线且 tree-shaken。

尺寸阶梯 Size scale

-
sm · 14
-
md · 16
-
lg · 20
+
sm · 14
+
md · 16
+
lg · 20

图标库 Icon library

-

当前注册的图标(按用途分组)。新增图标时,把图形以 name → body 加入 lib/icons.ts,按需补 viewBox(默认 16×16)/fill,再用 node --experimental-strip-types scripts/gen-icon-catalog.mjs 重新生成本目录,保证注册表与文档不漂移。

+

当前注册的图标(按用途分组)。图标数据由 scripts/gen-icon-data.mjs 中的 NAME_TO_REMIX 映射决定,运行 pnpm gen:icons 生成 lib/icon-data.ts;本目录由 node --experimental-strip-types scripts/gen-icon-catalog.mjs 重新生成,保证注册表与文档不漂移。

-
Actions
-
plus
-
close
-
check
-
search
-
copy
-
link
-
external-link
-
download
-
undo
-
send
-
image
-
settings
-
sliders
-
log-in
-
Navigation & layout
-
chevron-down
-
chevron-right
-
arrow-up
-
arrow-down
-
arrow-right
-
minus
-
panel-collapse
-
panel-expand
-
expand
-
collapse
-
list
-
Files & tools
-
folder
-
folder-closed
-
folder-plus
-
folder-solid
-
file
-
file-text
-
file-plus
-
file-off
-
image-off
-
code
-
terminal
-
pencil
-
glob
-
globe
-
check-list
-
bolt
-
git-pull-request
-
Communication
-
message
-
mail
-
user
-
Status & media
-
info
-
help-circle
-
alert-triangle
-
clock
-
sparkles
-
play
-
stop
-
star
-
star-outline
-
dots-vertical
-
dots-horizontal
+
Actions
+
plus
+
chat-new
+
close
+
check
+
search
+
copy
+
link
+
external-link
+
download
+
undo
+
send
+
image
+
settings
+
sliders
+
log-in
+
Navigation & layout
+
chevron-down
+
chevron-right
+
arrow-up
+
arrow-down
+
arrow-right
+
minus
+
panel-collapse
+
panel-expand
+
expand
+
collapse
+
list
+
Files & tools
+
folder
+
folder-closed
+
folder-plus
+
folder-solid
+
file
+
file-text
+
file-plus
+
file-off
+
image-off
+
code
+
terminal
+
pencil
+
glob
+
globe
+
check-list
+
bolt
+
git-pull-request
+
Communication
+
message
+
mail
+
user
+
Status & media
+
info
+
help-circle
+
alert-triangle
+
clock
+
sparkles
+
play
+
stop
+
star
+
star-outline
+
dots-horizontal
+
Other
+
sort
+
grip

禁止用 emoji 充当功能图标(唯一例外:月相 🌑…🌘,仅用于「等待 Agent 响应」聊天态)。Kimi 品牌标记(32×22 的眼睛 logo)属于品牌资产,不在此线稿图标体系内。

diff --git a/apps/kimi-web/scripts/gen-icon-catalog.mjs b/apps/kimi-web/scripts/gen-icon-catalog.mjs index b4f6a33a1..79063bf06 100644 --- a/apps/kimi-web/scripts/gen-icon-catalog.mjs +++ b/apps/kimi-web/scripts/gen-icon-catalog.mjs @@ -1,24 +1,21 @@ // scripts/gen-icon-catalog.mjs — generate the design-system §02 icon catalog // HTML from the canonical registry (lib/icons.ts) so the two can never drift. // Run: node --experimental-strip-types scripts/gen-icon-catalog.mjs -import { ICONS } from '../src/lib/icons.ts'; +import { ICON_DATA } from '../src/lib/icon-data.ts'; // Display order + grouping. Names not listed here are appended under "Other". const GROUPS = [ - ['Actions', ['plus', 'close', 'check', 'search', 'copy', 'link', 'external-link', 'download', 'undo', 'send', 'image', 'settings', 'sliders', 'log-in']], + ['Actions', ['plus', 'chat-new', 'close', 'check', 'search', 'copy', 'link', 'external-link', 'download', 'undo', 'send', 'image', 'settings', 'sliders', 'log-in']], ['Navigation & layout', ['chevron-down', 'chevron-right', 'arrow-up', 'arrow-down', 'arrow-right', 'minus', 'panel-collapse', 'panel-expand', 'expand', 'collapse', 'list']], ['Files & tools', ['folder', 'folder-closed', 'folder-plus', 'folder-solid', 'file', 'file-text', 'file-plus', 'file-off', 'image-off', 'code', 'terminal', 'pencil', 'glob', 'globe', 'check-list', 'bolt', 'git-pull-request']], ['Communication', ['message', 'mail', 'user']], - ['Status & media', ['info', 'help-circle', 'alert-triangle', 'clock', 'sparkles', 'play', 'stop', 'star', 'star-outline', 'dots-vertical', 'dots-horizontal']], + ['Status & media', ['info', 'help-circle', 'alert-triangle', 'clock', 'sparkles', 'play', 'stop', 'star', 'star-outline', 'dots-horizontal']], ]; function render(name) { - const def = ICONS[name]; - const vb = def.viewBox ?? '0 0 16 16'; - const fillAttrs = def.fill - ? 'fill="currentColor"' - : 'fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"'; - return `${def.body}`; + const d = ICON_DATA[name]; + const vb = `0 0 ${d.width ?? 24} ${d.height ?? 24}`; + return `${d.body}`; } const seen = new Set(); @@ -31,7 +28,7 @@ for (const [label, names] of GROUPS) { lines.push(`
${render(name)}${name}
`); } } -const rest = Object.keys(ICONS).filter((n) => !seen.has(n)); +const rest = Object.keys(ICON_DATA).filter((n) => !seen.has(n)); if (rest.length) { lines.push('
Other
'); for (const name of rest) { diff --git a/apps/kimi-web/scripts/gen-icon-data.mjs b/apps/kimi-web/scripts/gen-icon-data.mjs new file mode 100644 index 000000000..49ed7dc45 --- /dev/null +++ b/apps/kimi-web/scripts/gen-icon-data.mjs @@ -0,0 +1,155 @@ +#!/usr/bin/env node +// scripts/gen-icon-data.mjs — generate src/lib/icon-data.ts, the tree-shaken +// Remix Icon (ri) subset that backs apps/kimi-web's icon registry. +// +// Single source of truth for "which existing icon name maps to which Remix +// icon" is the NAME_TO_REMIX map below. The SVG bytes are pulled straight from +// @iconify-json/ri at generation time, so the registry stays library-sourced +// (no hand-copied SVG) yet fully offline and tree-shaken (only the icons we +// list here end up in the bundle). +// +// Run after changing the map: pnpm gen:icons + +import { writeFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import ri from '@iconify-json/ri/icons.json' with { type: 'json' }; +import { getIconData } from '@iconify/utils'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const OUT = resolve(__dirname, '../src/lib/icon-data.ts'); + +// Existing icon name → Remix icon name (ri:, prefix omitted here). +// Keep keys sorted within their group; the generated IconName union follows +// this order. Every value must exist in @iconify-json/ri (validated below). +const GROUPS = [ + ['Actions', { + plus: 'add-line', + 'chat-new': 'chat-new-line', + close: 'close-line', + check: 'check-line', + search: 'search-line', + copy: 'file-copy-line', + link: 'links-line', + 'external-link': 'external-link-line', + download: 'download-line', + undo: 'arrow-go-back-line', + send: 'send-plane-line', + image: 'image-line', + settings: 'settings-3-line', + sliders: 'equalizer-line', + 'log-in': 'login-box-line', + }], + ['Navigation & layout', { + 'chevron-down': 'arrow-down-s-line', + 'chevron-right': 'arrow-right-s-line', + 'arrow-up': 'arrow-up-line', + 'arrow-down': 'arrow-down-line', + 'arrow-right': 'arrow-right-line', + minus: 'subtract-line', + 'panel-collapse': 'contract-left-line', + 'panel-expand': 'expand-right-line', + expand: 'expand-diagonal-line', + collapse: 'collapse-diagonal-line', + list: 'list-unordered', + sort: 'sort-desc', + grip: 'draggable', + }], + ['Files & tools', { + folder: 'folder-open-line', + 'folder-closed': 'folder-line', + 'folder-plus': 'folder-add-line', + 'folder-solid': 'folder-fill', + file: 'file-line', + 'file-text': 'file-text-line', + 'file-plus': 'file-add-line', + 'file-off': 'file-line', + 'image-off': 'image-line', + code: 'code-line', + terminal: 'terminal-box-line', + pencil: 'pencil-line', + glob: 'braces-line', + globe: 'global-line', + 'check-list': 'list-check', + bolt: 'flashlight-line', + 'git-pull-request': 'git-pull-request-line', + }], + ['Communication', { + message: 'message-line', + mail: 'mail-line', + user: 'user-line', + }], + ['Status & media', { + info: 'information-line', + 'help-circle': 'question-line', + 'alert-triangle': 'alert-line', + clock: 'time-line', + sparkles: 'sparkling-line', + play: 'play-fill', + stop: 'stop-fill', + star: 'star-fill', + 'star-outline': 'star-line', + 'dots-horizontal': 'more-line', + }], +]; + +const NAME_TO_REMIX = Object.assign({}, ...GROUPS.map(([, m]) => m)); + +// --- resolve + validate ---------------------------------------------------- +const missing = []; +const data = {}; +for (const [name, riName] of Object.entries(NAME_TO_REMIX)) { + const icon = getIconData(ri, riName); + if (!icon) { + missing.push(`${name} → ri:${riName}`); + continue; + } + data[name] = { body: icon.body, width: icon.width ?? 24, height: icon.height ?? 24 }; +} + +if (missing.length) { + console.error('gen-icon-data: the following Remix icons were not found in @iconify-json/ri:'); + for (const m of missing) console.error(` ${m}`); + process.exit(1); +} + +// --- emit ------------------------------------------------------------------ +const names = Object.keys(data); +const keyOf = (n) => (/^[a-zA-Z_$][\w$]*$/.test(n) ? n : JSON.stringify(n)); + +const lines = []; +lines.push('// GENERATED FILE — do not edit by hand.'); +lines.push('// Source of truth: scripts/gen-icon-data.mjs (run `pnpm gen:icons`).'); +lines.push('// Icons are Remix Icon (ri) — https://remixicon.com/ — Apache-2.0.'); +lines.push(''); +lines.push('export type IconName ='); +for (let i = 0; i < names.length; i++) { + const suffix = i === names.length - 1 ? ';' : ''; + lines.push(` | ${JSON.stringify(names[i])}${suffix}`); +} +lines.push(''); +lines.push('export interface IconData {'); +lines.push(' /** Inner SVG markup (paths/shapes), rendered inside our wrapper. */'); +lines.push(' body: string;'); +lines.push(' /** Source grid width in px. Remix icons are 24. */'); +lines.push(' width?: number;'); +lines.push(' /** Source grid height in px. Remix icons are 24. */'); +lines.push(' height?: number;'); +lines.push('}'); +lines.push(''); +lines.push('/** Existing name → fully-qualified Remix icon id. */'); +lines.push('export const NAME_TO_REMIX: Record = {'); +for (const name of names) lines.push(` ${keyOf(name)}: ${JSON.stringify('ri:' + NAME_TO_REMIX[name])},`); +lines.push('};'); +lines.push(''); +lines.push('/** Per-icon SVG data, pulled from @iconify-json/ri. */'); +lines.push('export const ICON_DATA: Record = {'); +for (const name of names) { + const { body, width, height } = data[name]; + lines.push(` ${keyOf(name)}: { body: ${JSON.stringify(body)}, width: ${width}, height: ${height} },`); +} +lines.push('};'); +lines.push(''); + +writeFileSync(OUT, lines.join('\n')); +console.log(`gen-icon-data: wrote ${names.length} icons to src/lib/icon-data.ts`); diff --git a/apps/kimi-web/src/components/Sidebar.vue b/apps/kimi-web/src/components/Sidebar.vue index 3ddae4bc4..f606ef9dd 100644 --- a/apps/kimi-web/src/components/Sidebar.vue +++ b/apps/kimi-web/src/components/Sidebar.vue @@ -582,7 +582,7 @@ onBeforeUnmount(() => {
@@ -924,7 +924,7 @@ onBeforeUnmount(() => { border: none; border-radius: var(--radius-md); background: transparent; - color: var(--color-text-muted); + color: var(--color-text); font: inherit; text-align: left; cursor: pointer; @@ -942,7 +942,7 @@ onBeforeUnmount(() => { .search-input { flex: 1; min-width: 0; - color: var(--faint); + color: var(--color-text); font-family: var(--mono); font-size: var(--ui-font-size); overflow: hidden; diff --git a/apps/kimi-web/src/components/WorkspaceGroup.vue b/apps/kimi-web/src/components/WorkspaceGroup.vue index 5b2c6d2d4..34a093eef 100644 --- a/apps/kimi-web/src/components/WorkspaceGroup.vue +++ b/apps/kimi-web/src/components/WorkspaceGroup.vue @@ -129,7 +129,7 @@ function onHeaderDragStart(event: DragEvent): void { :label="t('workspace.newInGroup')" @click.stop="emit('createInWorkspace', group.workspace.id)" > - +
@@ -259,12 +259,24 @@ function onHeaderDragStart(event: DragEvent): void { opacity: 1; } -/* More button — hidden until hover (or while its menu is open / focused). - `.gh .gh-more` out-specificities IconButton's display so the hidden default wins. */ -.gh .gh-more { display: none; } +/* More + add buttons — hidden until hover (or while the more menu is open / + focused). `.gh .gh-more` / `.gh .gh-add` out-specificity IconButton's display + so the hidden default wins. */ +.gh .gh-more, +.gh .gh-add { + opacity: 0; + pointer-events: none; +} .gh:hover .gh-more, +.gh:hover .gh-add, +.gh:focus-within .gh-more, +.gh:focus-within .gh-add, .gh-more.open, -.gh-more:focus-visible { display: inline-flex; } +.gh-more:focus-visible, +.gh-add:focus-visible { + opacity: 1; + pointer-events: auto; +} .gh-more.open { color: var(--color-text); background: var(--color-line); } .group-empty { diff --git a/apps/kimi-web/src/components/chat/ChatPane.vue b/apps/kimi-web/src/components/chat/ChatPane.vue index dd0123454..019bfa6dc 100644 --- a/apps/kimi-web/src/components/chat/ChatPane.vue +++ b/apps/kimi-web/src/components/chat/ChatPane.vue @@ -579,9 +579,9 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }): @@ -593,6 +593,7 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }): + + @@ -874,16 +878,6 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }): min-height: 22px; box-sizing: border-box; } -.u-meta .u-edit svg { - margin-top: -1.5px; -} -.u-meta .u-edit-text { - max-width: 0; - overflow: hidden; - white-space: nowrap; - transition: max-width 0.15s ease; -} -.u-meta .u-edit:hover .u-edit-text { max-width: 120px; } /* User input is shown verbatim — preserve newlines, break long tokens. */ .u-text { white-space: pre-wrap; @@ -896,7 +890,7 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }): .u-edit { display: inline-flex; align-items: center; - gap: 4px; + justify-content: center; padding: 2px 5px; background: none; border: none; @@ -904,6 +898,7 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }): color: var(--muted); font: inherit; font-size: var(--text-base); + line-height: 1; cursor: pointer; opacity: 0.7; transition: opacity 0.12s, color 0.12s, background-color 0.12s; @@ -912,7 +907,6 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }): display: block; flex: none; } -.u-edit span { line-height: 1; } .u-edit:hover { opacity: 1; color: var(--color-accent); background: var(--hover); } /* Copy button — icon-only, shares the undo button's muted→hover style. */ .u-copy { @@ -933,7 +927,7 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }): min-height: 22px; box-sizing: border-box; } -.u-copy svg { display: block; flex: none; transform: translateY(1.3px); } +.u-copy svg { display: block; flex: none; } .u-copy:hover { opacity: 1; color: var(--color-accent); background: var(--hover); } /* Mobile bubble layout: right-align the undo button below the bubble. */ .u-edit-wrap { display: flex; justify-content: flex-end; } @@ -1004,38 +998,35 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }): line-height: 1; } +/* Copy button — icon-only, shares the undo button's muted→hover style so the + message-stream action buttons (copy / undo) all read as one family. */ .a-cpbtn { display: inline-flex; align-items: center; - gap: 4px; + justify-content: center; + padding: 2px 5px; background: none; border: none; - color: var(--faint); - cursor: pointer; + border-radius: var(--radius-sm); + color: var(--muted); + font: inherit; font-size: var(--text-base); - padding: 2px 6px 2px 0; - border-radius: 4px; + line-height: 1; + cursor: pointer; + opacity: 0.7; + transition: opacity 0.12s, color 0.12s, background-color 0.12s; + min-height: 22px; + box-sizing: border-box; } .a-cpbtn:hover { - color: var(--color-text); -} -.a-cpbtn svg, -.a-cpbtn-text { - pointer-events: none; + opacity: 1; + color: var(--color-accent); + background: var(--hover); } .a-cpbtn svg { + display: block; flex: none; } -.a-cpbtn-text { - opacity: 0; - max-width: none; - overflow: visible; - white-space: nowrap; - transition: opacity 0.15s ease; -} -.a-cpbtn:hover .a-cpbtn-text { - opacity: 1; -} /* Touch devices: always show the copy buttons (no hover to reveal them) and give the bubble-layout button a comfortable tap size. */ @media (hover: none) { @@ -1174,10 +1165,6 @@ function isStreamingRenderBlock(turn: ChatTurn, block: { sourceIndex: number }): overflow: hidden; text-overflow: ellipsis; } - .a-cpbtn-text { - opacity: 1; - max-width: 120px; - } .u-edit-confirm { flex-wrap: wrap; justify-content: flex-end; diff --git a/apps/kimi-web/src/components/mobile/MobileSwitcherSheet.vue b/apps/kimi-web/src/components/mobile/MobileSwitcherSheet.vue index 0a54d13ff..56058794e 100644 --- a/apps/kimi-web/src/components/mobile/MobileSwitcherSheet.vue +++ b/apps/kimi-web/src/components/mobile/MobileSwitcherSheet.vue @@ -205,7 +205,7 @@ async function onDeleteWorkspace(ws: WorkspaceView): Promise { :label="t('sidebar.options')" @click.stop="toggleWsMenu(g.workspace.id)" > - + @@ -250,7 +250,7 @@ async function onDeleteWorkspace(ws: WorkspaceView): Promise { :label="t('sidebar.options')" @click.stop="toggleMenu(s.id)" > - + diff --git a/apps/kimi-web/src/lib/icon-data.ts b/apps/kimi-web/src/lib/icon-data.ts new file mode 100644 index 000000000..54a7a3647 --- /dev/null +++ b/apps/kimi-web/src/lib/icon-data.ts @@ -0,0 +1,196 @@ +// GENERATED FILE — do not edit by hand. +// Source of truth: scripts/gen-icon-data.mjs (run `pnpm gen:icons`). +// Icons are Remix Icon (ri) — https://remixicon.com/ — Apache-2.0. + +export type IconName = + | "plus" + | "chat-new" + | "close" + | "check" + | "search" + | "copy" + | "link" + | "external-link" + | "download" + | "undo" + | "send" + | "image" + | "settings" + | "sliders" + | "log-in" + | "chevron-down" + | "chevron-right" + | "arrow-up" + | "arrow-down" + | "arrow-right" + | "minus" + | "panel-collapse" + | "panel-expand" + | "expand" + | "collapse" + | "list" + | "sort" + | "grip" + | "folder" + | "folder-closed" + | "folder-plus" + | "folder-solid" + | "file" + | "file-text" + | "file-plus" + | "file-off" + | "image-off" + | "code" + | "terminal" + | "pencil" + | "glob" + | "globe" + | "check-list" + | "bolt" + | "git-pull-request" + | "message" + | "mail" + | "user" + | "info" + | "help-circle" + | "alert-triangle" + | "clock" + | "sparkles" + | "play" + | "stop" + | "star" + | "star-outline" + | "dots-horizontal"; + +export interface IconData { + /** Inner SVG markup (paths/shapes), rendered inside our wrapper. */ + body: string; + /** Source grid width in px. Remix icons are 24. */ + width?: number; + /** Source grid height in px. Remix icons are 24. */ + height?: number; +} + +/** Existing name → fully-qualified Remix icon id. */ +export const NAME_TO_REMIX: Record = { + plus: "ri:add-line", + "chat-new": "ri:chat-new-line", + close: "ri:close-line", + check: "ri:check-line", + search: "ri:search-line", + copy: "ri:file-copy-line", + link: "ri:links-line", + "external-link": "ri:external-link-line", + download: "ri:download-line", + undo: "ri:arrow-go-back-line", + send: "ri:send-plane-line", + image: "ri:image-line", + settings: "ri:settings-3-line", + sliders: "ri:equalizer-line", + "log-in": "ri:login-box-line", + "chevron-down": "ri:arrow-down-s-line", + "chevron-right": "ri:arrow-right-s-line", + "arrow-up": "ri:arrow-up-line", + "arrow-down": "ri:arrow-down-line", + "arrow-right": "ri:arrow-right-line", + minus: "ri:subtract-line", + "panel-collapse": "ri:contract-left-line", + "panel-expand": "ri:expand-right-line", + expand: "ri:expand-diagonal-line", + collapse: "ri:collapse-diagonal-line", + list: "ri:list-unordered", + sort: "ri:sort-desc", + grip: "ri:draggable", + folder: "ri:folder-open-line", + "folder-closed": "ri:folder-line", + "folder-plus": "ri:folder-add-line", + "folder-solid": "ri:folder-fill", + file: "ri:file-line", + "file-text": "ri:file-text-line", + "file-plus": "ri:file-add-line", + "file-off": "ri:file-line", + "image-off": "ri:image-line", + code: "ri:code-line", + terminal: "ri:terminal-box-line", + pencil: "ri:pencil-line", + glob: "ri:braces-line", + globe: "ri:global-line", + "check-list": "ri:list-check", + bolt: "ri:flashlight-line", + "git-pull-request": "ri:git-pull-request-line", + message: "ri:message-line", + mail: "ri:mail-line", + user: "ri:user-line", + info: "ri:information-line", + "help-circle": "ri:question-line", + "alert-triangle": "ri:alert-line", + clock: "ri:time-line", + sparkles: "ri:sparkling-line", + play: "ri:play-fill", + stop: "ri:stop-fill", + star: "ri:star-fill", + "star-outline": "ri:star-line", + "dots-horizontal": "ri:more-line", +}; + +/** Per-icon SVG data, pulled from @iconify-json/ri. */ +export const ICON_DATA: Record = { + plus: { body: "", width: 24, height: 24 }, + "chat-new": { body: "", width: 24, height: 24 }, + close: { body: "", width: 24, height: 24 }, + check: { body: "", width: 24, height: 24 }, + search: { body: "", width: 24, height: 24 }, + copy: { body: "", width: 24, height: 24 }, + link: { body: "", width: 24, height: 24 }, + "external-link": { body: "", width: 24, height: 24 }, + download: { body: "", width: 24, height: 24 }, + undo: { body: "", width: 24, height: 24 }, + send: { body: "", width: 24, height: 24 }, + image: { body: "", width: 24, height: 24 }, + settings: { body: "", width: 24, height: 24 }, + sliders: { body: "", width: 24, height: 24 }, + "log-in": { body: "", width: 24, height: 24 }, + "chevron-down": { body: "", width: 24, height: 24 }, + "chevron-right": { body: "", width: 24, height: 24 }, + "arrow-up": { body: "", width: 24, height: 24 }, + "arrow-down": { body: "", width: 24, height: 24 }, + "arrow-right": { body: "", width: 24, height: 24 }, + minus: { body: "", width: 24, height: 24 }, + "panel-collapse": { body: "", width: 24, height: 24 }, + "panel-expand": { body: "", width: 24, height: 24 }, + expand: { body: "", width: 24, height: 24 }, + collapse: { body: "", width: 24, height: 24 }, + list: { body: "", width: 24, height: 24 }, + sort: { body: "", width: 24, height: 24 }, + grip: { body: "", width: 24, height: 24 }, + folder: { body: "", width: 24, height: 24 }, + "folder-closed": { body: "", width: 24, height: 24 }, + "folder-plus": { body: "", width: 24, height: 24 }, + "folder-solid": { body: "", width: 24, height: 24 }, + file: { body: "", width: 24, height: 24 }, + "file-text": { body: "", width: 24, height: 24 }, + "file-plus": { body: "", width: 24, height: 24 }, + "file-off": { body: "", width: 24, height: 24 }, + "image-off": { body: "", width: 24, height: 24 }, + code: { body: "", width: 24, height: 24 }, + terminal: { body: "", width: 24, height: 24 }, + pencil: { body: "", width: 24, height: 24 }, + glob: { body: "", width: 24, height: 24 }, + globe: { body: "", width: 24, height: 24 }, + "check-list": { body: "", width: 24, height: 24 }, + bolt: { body: "", width: 24, height: 24 }, + "git-pull-request": { body: "", width: 24, height: 24 }, + message: { body: "", width: 24, height: 24 }, + mail: { body: "", width: 24, height: 24 }, + user: { body: "", width: 24, height: 24 }, + info: { body: "", width: 24, height: 24 }, + "help-circle": { body: "", width: 24, height: 24 }, + "alert-triangle": { body: "", width: 24, height: 24 }, + clock: { body: "", width: 24, height: 24 }, + sparkles: { body: "", width: 24, height: 24 }, + play: { body: "", width: 24, height: 24 }, + stop: { body: "", width: 24, height: 24 }, + star: { body: "", width: 24, height: 24 }, + "star-outline": { body: "", width: 24, height: 24 }, + "dots-horizontal": { body: "", width: 24, height: 24 }, +}; diff --git a/apps/kimi-web/src/lib/icons.test.ts b/apps/kimi-web/src/lib/icons.test.ts index 9d67a19da..ba65a2f32 100644 --- a/apps/kimi-web/src/lib/icons.test.ts +++ b/apps/kimi-web/src/lib/icons.test.ts @@ -14,16 +14,22 @@ describe('ICONS registry', () => { expect(def.body.toLowerCase(), `${name}`).not.toContain(' { + for (const [name, def] of Object.entries(ICONS)) { + expect(def.fill, `${name} fill`).toBe(true); + expect(def.viewBox, `${name} viewBox`).toBe('0 0 24 24'); + } + }); }); describe('iconSvg', () => { - it('renders a line icon with the registry defaults', () => { + it('renders a Remix icon with the registry defaults', () => { const svg = iconSvg('plus'); expect(svg.startsWith(' { expect(iconSvg('plus', 'lg')).toContain('width="20" height="20"'); }); - it('keeps a custom viewBox for off-grid icons', () => { - expect(iconSvg('settings')).toContain('viewBox="0 0 24 24"'); - }); - - it('renders filled icons with currentColor and no stroke', () => { + it('renders a filled icon with currentColor and no stroke', () => { const svg = iconSvg('star'); expect(svg).toContain('fill="currentColor"'); expect(svg).not.toContain('stroke='); diff --git a/apps/kimi-web/src/lib/icons.ts b/apps/kimi-web/src/lib/icons.ts index baef6ad61..d53740d1b 100644 --- a/apps/kimi-web/src/lib/icons.ts +++ b/apps/kimi-web/src/lib/icons.ts @@ -1,229 +1,57 @@ // apps/kimi-web/src/lib/icons.ts -// Single source of truth for apps/kimi-web line icons (design-system §02). +// Single source of truth for apps/kimi-web icons (design-system §02). // -// Icons are line glyphs: stroke="currentColor", stroke-width 1.5, round caps/joins, -// no fill — colour follows text. Most sit on a 16x16 grid; a few keep a different -// source grid (24x24 for settings / expand / collapse / panel-* / git-pull-request / -// star-*, 12x12 for info, 20x20 for image-off / file-off). The rendered size is always -// the token size, and the global `.kw-icon * { vector-effect: non-scaling-stroke }` rule -// (style.css) keeps the 1.5px stroke visually identical across grids. +// Icons come from Remix Icon (https://remixicon.com/, Apache-2.0). The per-icon +// SVG data is generated into ./icon-data.ts by scripts/gen-icon-data.mjs — a +// tree-shaken subset of @iconify-json/ri — so the registry is library-sourced, +// offline, and only bundles the icons we actually use. +// +// Remix icons are fill-based (fill="currentColor") on a 24x24 source grid; the +// rendered size is always the token size. Colour follows text. // // Two consumers share this registry: // - the Vue component (components/ui/Icon.vue) for template use; // - iconSvg() below, for v-html contexts (e.g. lib/toolMeta.ts). -export type IconSize = 'sm' | 'md' | 'lg'; +import { iconToSVG } from '@iconify/utils'; +import { ICON_DATA, type IconData, type IconName } from './icon-data'; -export interface IconDef { - /** Inner SVG markup only — no outer , no stroke/fill attributes (the - * renderer adds those). Mixed icons may override fill on individual shapes. */ - body: string; - /** Source grid. Defaults to "0 0 16 16". */ - viewBox?: string; - /** Solid icon (fill="currentColor", no stroke). Defaults to line style. */ - fill?: boolean; -} +export type { IconName } from './icon-data'; +export { NAME_TO_REMIX } from './icon-data'; + +export type IconSize = 'sm' | 'md' | 'lg'; export const SIZE_PX: Record = { sm: 14, md: 16, lg: 20 }; -export const ICONS = { - // --- Actions (16-grid, line) -------------------------------------------- - plus: { body: '' }, - close: { body: '' }, - check: { body: '' }, - search: { body: '' }, - folder: { - body: '', - }, - image: { - body: '', - }, - 'chevron-down': { body: '' }, - send: { body: '' }, - sort: { body: '' }, - grip: { - body: '', - fill: true, - }, +export interface IconDef { + /** Inner SVG markup only — no outer . Remix icons are fill-based. */ + body: string; + /** Source grid. Remix icons are "0 0 24 24". */ + viewBox?: string; + /** Solid icon (fill="currentColor", no stroke). Always true for Remix. */ + fill?: boolean; +} - // --- Tool glyphs (16-grid, line) ---------------------------------------- - 'file-text': { - body: '', - }, - terminal: { - body: '', - }, - pencil: { - body: '', - }, - 'file-plus': { - body: '', - }, - glob: { - body: '', - }, - globe: { - body: '', - }, - 'check-list': { - body: '', - }, - bolt: { body: '' }, +function viewBoxOf(d: IconData): string { + return `0 0 ${d.width ?? 24} ${d.height ?? 24}`; +} - // --- 24-grid line icons ------------------------------------------------- - 'panel-collapse': { - viewBox: '0 0 24 24', - body: '', - }, - settings: { - viewBox: '0 0 24 24', - body: '', - }, - expand: { - viewBox: '0 0 24 24', - body: '', - }, - collapse: { - viewBox: '0 0 24 24', - body: '', - }, +/** Back-compat registry view over the generated Remix data. */ +export const ICONS: Record = Object.fromEntries( + (Object.entries(ICON_DATA) as [IconName, IconData][]).map(([name, d]) => [ + name, + { body: d.body, viewBox: viewBoxOf(d), fill: true }, + ]), +) as Record; - // --- Mixed / solid ------------------------------------------------------ - info: { - viewBox: '0 0 12 12', - body: '', - }, - play: { body: '', fill: true }, - stop: { body: '', fill: true }, - star: { - viewBox: '0 0 24 24', - body: '', - fill: true, - }, - - // --- More actions (16-grid, line) --------------------------------------- - 'log-in': { - body: '', - }, - message: { - body: '', - }, - mail: { - body: '', - }, - code: { - body: '', - }, - file: { - body: '', - }, - copy: { - body: '', - }, - link: { - body: '', - }, - 'external-link': { - body: '', - }, - download: { - body: '', - }, - undo: { - body: '', - }, - clock: { - body: '', - }, - sparkles: { - body: '', - }, - list: { body: '' }, - user: { - body: '', - }, - 'folder-plus': { - body: '', - }, - 'folder-closed': { - body: '', - }, - - // --- Navigation & arrows (16-grid, line) -------------------------------- - 'chevron-right': { body: '' }, - 'arrow-up': { body: '' }, - 'arrow-down': { body: '' }, - 'arrow-right': { body: '' }, - minus: { body: '' }, - - // --- More 24-grid line icons -------------------------------------------- - 'panel-expand': { - viewBox: '0 0 24 24', - body: '', - }, - 'git-pull-request': { - viewBox: '0 0 24 24', - body: '', - }, - sliders: { - viewBox: '0 0 24 24', - body: '', - }, - 'star-outline': { - viewBox: '0 0 24 24', - body: '', - }, - - - // --- Mixed / solid ------------------------------------------------------ - 'alert-triangle': { - body: '', - }, - 'help-circle': { - body: '', - }, - 'dots-vertical': { - body: '', - fill: true, - }, - 'dots-horizontal': { - body: '', - fill: true, - }, - 'folder-solid': { - body: '', - fill: true, - }, - - // --- Placeholders (20-grid, line) --------------------------------------- - 'image-off': { - viewBox: '0 0 20 20', - body: '', - }, - 'file-off': { - viewBox: '0 0 20 20', - body: '', - }, -} as const satisfies Record; - -export type IconName = keyof typeof ICONS; - -const DEFAULT_VIEWBOX = '0 0 16 16'; - -/** Typed accessor: `ICONS[name]` is a narrow literal union (some entries lack - * optional fields), so we widen to IconDef here — safe because the registry - * is `satisfies Record`. */ export function getIcon(name: IconName): IconDef { - return ICONS[name] as IconDef; + return ICONS[name]; } /** Render an icon to a full string for v-html contexts. Mirrors . */ export function iconSvg(name: IconName, size: IconSize = 'md'): string { - const def = getIcon(name); const px = SIZE_PX[size]; - const viewBox = def.viewBox ?? DEFAULT_VIEWBOX; - if (def.fill) { - return ``; - } - return ``; + const svg = iconToSVG(ICON_DATA[name], { width: px, height: px }); + const viewBox = svg.attributes.viewBox; + return ``; } diff --git a/apps/kimi-web/src/style.css b/apps/kimi-web/src/style.css index dca3c9429..9c590f25a 100644 --- a/apps/kimi-web/src/style.css +++ b/apps/kimi-web/src/style.css @@ -213,16 +213,13 @@ summary { /* -- icon primitive (design-system §02) ------------------------------------- Shared by the component and iconSvg() v-html strings (lib/icons.ts). - non-scaling-stroke keeps the 1.5px stroke identical whether an icon's source - grid is 16, 24 or 12, so every icon renders at a uniform weight. */ + Icons are Remix Icon (fill="currentColor" on a 24x24 grid), so colour follows + text and the rendered size comes from the width/height attributes. */ .kw-icon { display: inline-block; flex: none; vertical-align: -0.15em; } -.kw-icon > * { - vector-effect: non-scaling-stroke; -} /* color-scheme drives UA-rendered parts (scrollbars, form controls, etc.). An explicit choice must pin it to a single value — otherwise a dark-mode diff --git a/flake.nix b/flake.nix index 8af851005..b8d781535 100644 --- a/flake.nix +++ b/flake.nix @@ -154,7 +154,7 @@ inherit (finalAttrs) pname version src pnpmWorkspaces; inherit pnpm; fetcherVersion = 3; - hash = "sha256-o753txNKfBcn/fiJOWFU0lyrSqTUJ/GU1ed1DPVPZ2U="; + hash = "sha256-+hTrX3pa8fjK+eh4q2BqOEAm9C7zvMXrKf4bwbJoG98="; }; nativeBuildInputs = [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d65cbed5..0377edc7d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -172,6 +172,15 @@ importers: '@fontsource-variable/jetbrains-mono': specifier: ^5.2.8 version: 5.2.8 + '@iconify-json/ri': + specifier: ^1.2.10 + version: 1.2.10 + '@iconify/utils': + specifier: ^3.1.3 + version: 3.1.3 + '@iconify/vue': + specifier: ^5.0.1 + version: 5.0.1(vue@3.5.35(typescript@6.0.2)) '@xterm/addon-fit': specifier: ^0.11.0 version: 0.11.0 @@ -1608,6 +1617,9 @@ packages: peerDependencies: hono: ^4 + '@iconify-json/ri@1.2.10': + resolution: {integrity: sha512-WWMhoncVVM+Xmu9T5fgu2lhYRrKTEWhKk3Com0KiM111EeEsRLiASjpsFKnC/SrB6covhUp95r2mH8tGxhgd5Q==} + '@iconify-json/simple-icons@1.2.84': resolution: {integrity: sha512-v4JVu6xIewGoETD4mm2k6UAdFAbTlY1duw5ZNSxYORfs2yFsHDhoU9Omn/BgrV0nR/ptWkF3ZIr/ZHoYXI/6Jw==} @@ -1617,6 +1629,11 @@ packages: '@iconify/utils@3.1.3': resolution: {integrity: sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==} + '@iconify/vue@5.0.1': + resolution: {integrity: sha512-aumwwooJlFJ5H5qYWB6ZTAyM0C8hpfcSVLB9/a3qnH1GGvIJ+FEbpEs4s/HfErYe/M5qZeLjwmESR5fFm3lXEw==} + peerDependencies: + vue: '>=3.0.0' + '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -8477,6 +8494,10 @@ snapshots: dependencies: hono: 4.12.14 + '@iconify-json/ri@1.2.10': + dependencies: + '@iconify/types': 2.0.0 + '@iconify-json/simple-icons@1.2.84': dependencies: '@iconify/types': 2.0.0 @@ -8489,6 +8510,11 @@ snapshots: '@iconify/types': 2.0.0 import-meta-resolve: 4.2.0 + '@iconify/vue@5.0.1(vue@3.5.35(typescript@6.0.2))': + dependencies: + '@iconify/types': 2.0.0 + vue: 3.5.35(typescript@6.0.2) + '@inquirer/external-editor@1.0.3(@types/node@22.19.17)': dependencies: chardet: 2.1.1 @@ -9877,7 +9903,7 @@ snapshots: obug: 2.1.1 std-env: 4.0.0 tinyrainbow: 3.1.0 - vitest: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@25.0.1)(vite@8.0.8(@types/node@22.19.17)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + vitest: 4.1.4(@types/node@22.19.17)(@vitest/coverage-v8@4.1.4)(jsdom@25.0.1)(vite@6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/expect@4.1.4': dependencies: