From 950fa9c2f3b148b5d311295c6a86bc3eda5c2a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=85=E5=87=A4?= Date: Wed, 14 Oct 2026 02:53:48 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(ui):=20=E6=96=B0=E5=A2=9E=20CodePanel?= =?UTF-8?q?=20=E4=BB=A3=E7=A0=81=E5=9D=97=E4=B8=8E=20CopyButton=20?= =?UTF-8?q?=E5=A4=8D=E5=88=B6=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把宿主聊天代码块的外框与复制按钮收进组件库,供聊天代码块和 「连接电脑」对话框共用: - CodePanel:只读代码 + 复制按钮,menu-shell 圆角(与 SettingsSection / SurfaceCard 同档),width=fit|full,wrap 折行长命令 - CopyButton:宿主 copy-action-button 原样搬入,文案改为 props - ui-owners skill 登记 CodePanel --- skills/ui-owners/SKILL.md | 10 ++ src/components/code-panel/CodePanel.vue | 72 +++++++++ src/components/code-panel/index.ts | 1 + .../copy-button/CopyAnimatedCheckIcon.vue | 53 +++++++ src/components/copy-button/CopyButton.vue | 148 ++++++++++++++++++ src/components/copy-button/index.ts | 1 + src/index.ts | 2 + 7 files changed, 287 insertions(+) create mode 100644 src/components/code-panel/CodePanel.vue create mode 100644 src/components/code-panel/index.ts create mode 100644 src/components/copy-button/CopyAnimatedCheckIcon.vue create mode 100644 src/components/copy-button/CopyButton.vue create mode 100644 src/components/copy-button/index.ts diff --git a/skills/ui-owners/SKILL.md b/skills/ui-owners/SKILL.md index 83edad0..ff82321 100644 --- a/skills/ui-owners/SKILL.md +++ b/skills/ui-owners/SKILL.md @@ -223,6 +223,15 @@ Props: `code`, `verificationUri`, `expiresAt?`, `hint`, `retryLoading?`,以及 不要合并。已知同形状旧写法:bots ACP 面板、onboarding Step4 —— 迁移前先做各自的 行为盘点(ACP 有真 cancel API 和错误态)。 +**CodePanel** · `@felinic/ui`(`src/components/code-panel`) +只读代码块 + 复制按钮:聊天代码块、要粘到终端运行的命令、配置片段都用它。外框是 +menu-shell 圆角(与 SettingsSection / SurfaceCard 同档,放在设置卡片旁边对得齐)。 +Props: `code`(复制的原文)、`copyLabel` / `copiedLabel` / `copyFailedMessage`(caller +i18n)、`width: 'fit' | 'full'`(fit 贴合代码,聊天用;full 撑满行,对话框用)、`wrap` +(一行长命令折行而不是横向滚动,含不可断的 key 也不会撑宽宿主)。语法高亮归 caller: +把渲染好的内容放进默认 slot,不放则原样显示 `code`。不要再手写 `pre` + 绝对定位复制按钮。 +复制按钮单独是 **CopyButton**(同目录 `copy-button`),图标按钮 + 复制后打勾动效。 + **LabelSwap**(`@felinic/ui` 原语,非 apps/web owner,顺带登记防重造) 会"换字"的内容槽:每个候选 label 一个具名 slot,`active` 指名当前项;各 label 保持 自然宽度,切换时外壳宽度按 house spring 平滑过渡、label 交叉淡出。给 Connect↔Cancel @@ -415,6 +424,7 @@ A stat / number tile? → MetricReadout (you own the g A vertical, centered entity/add tile? → PersonaTile An error / warning / info notice? → CalloutBanner (bare inside a host surface) A device-code "enter this code" moment? → DeviceCodePanel +Read-only code / a command to copy? → CodePanel A label that swaps with state (Connect↔Cancel)? → LabelSwap (@felinic/ui) The page frame itself? → PageShell A pane with nothing to show (loading/empty)? → PanePlaceholder (centered fill) diff --git a/src/components/code-panel/CodePanel.vue b/src/components/code-panel/CodePanel.vue new file mode 100644 index 0000000..93c5bb0 --- /dev/null +++ b/src/components/code-panel/CodePanel.vue @@ -0,0 +1,72 @@ + + + diff --git a/src/components/code-panel/index.ts b/src/components/code-panel/index.ts new file mode 100644 index 0000000..e4b3f30 --- /dev/null +++ b/src/components/code-panel/index.ts @@ -0,0 +1 @@ +export { default as CodePanel } from './CodePanel.vue' diff --git a/src/components/copy-button/CopyAnimatedCheckIcon.vue b/src/components/copy-button/CopyAnimatedCheckIcon.vue new file mode 100644 index 0000000..c31e7dc --- /dev/null +++ b/src/components/copy-button/CopyAnimatedCheckIcon.vue @@ -0,0 +1,53 @@ + + + + + + diff --git a/src/components/copy-button/CopyButton.vue b/src/components/copy-button/CopyButton.vue new file mode 100644 index 0000000..1c1ae60 --- /dev/null +++ b/src/components/copy-button/CopyButton.vue @@ -0,0 +1,148 @@ + + + + + + diff --git a/src/components/copy-button/index.ts b/src/components/copy-button/index.ts new file mode 100644 index 0000000..22b919c --- /dev/null +++ b/src/components/copy-button/index.ts @@ -0,0 +1 @@ +export { default as CopyButton } from './CopyButton.vue' diff --git a/src/index.ts b/src/index.ts index 3949377..86bece8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ export * from './components/button-group/index' export * from './components/callout-banner/index' export * from './components/card/index' export * from './components/checkbox/index' +export * from './components/code-panel/index' export * from './components/collapsible/index' export * from './components/command/index' export { menuItemClass, menuLabelClass, menuContentClass, menuViewportClass, menuChromeClass, virtualListboxClass, menuAlignOffset, menuSearchHeaderClass, menuSearchInputClass, menuSeparatorClass, menuSlideClass } from './lib/menu' @@ -18,6 +19,7 @@ export { useClipboard } from './lib/clipboard' export * from './components/confirm-delete-dialog/index' export * from './components/confirm-popover/index' export * from './components/context-menu/index' +export * from './components/copy-button/index' export * from './components/copy-connected-icon/index' export * from './components/date-range-picker/index' export * from './components/device-code-panel/index' From ccc5e306951f2d193fe8fd87bba672f0cc4bb506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=85=E5=87=A4?= Date: Wed, 14 Oct 2026 17:54:03 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(ui):=20CodePanel=20=E9=81=B5=E5=BE=AA?= =?UTF-8?q?=E6=B7=B1=E8=89=B2=E8=BE=B9=E6=A1=86=E8=A7=84=E5=88=99=E5=B9=B6?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=E5=A4=8D=E5=88=B6=E6=8C=89=E9=92=AE=E7=84=A6?= =?UTF-8?q?=E7=82=B9=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CodePanel 改用 surfaceEdgeClass,新增 bordered 供嵌在卡片表面内时保留边框 - surfaceEdgeClass 改用 border-1,避免 markstream-vue 的 .border 规则在 markdown 内重新加回深色边框 - 去掉复制按钮的 focus-visible:ring-0,保留键盘焦点提示 --- src/components/code-panel/CodePanel.vue | 13 +++++++++---- src/lib/surface.ts | 7 ++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/components/code-panel/CodePanel.vue b/src/components/code-panel/CodePanel.vue index 93c5bb0..4acafb1 100644 --- a/src/components/code-panel/CodePanel.vue +++ b/src/components/code-panel/CodePanel.vue @@ -5,7 +5,8 @@ // chat-code-block.vue, now a caller of this) so every code surface shares one // frame. It owns ONLY the frame — the menu-shell radius (same rung as // SettingsSection / SurfaceCard, so a code block next to a settings card lines -// up), fill, edge, inset, mono type and the copy button. Highlighting is the +// up), fill, the shared card edge rule (surfaceEdgeClass: no edge in dark +// mode unless `bordered`), inset, mono type and the copy button. Highlighting is the // caller's: pass rendered markup through the default slot; without one the // `code` text is shown as-is. // @@ -13,6 +14,7 @@ // is a stable trailing sibling (top-aligned), so the block stays // fit-to-content without a reserved gap and the button does not jitter while // streaming content grows the width. +import { surfaceEdgeClass } from '#/lib/surface' import { CopyButton } from '../copy-button' withDefaults(defineProps<{ @@ -37,17 +39,20 @@ withDefaults(defineProps<{ * to separate real code lines. */ wrap?: boolean + /** Keep the edge in dark mode when nested inside another card-colored surface (a dialog body). */ + bordered?: boolean }>(), { width: 'full', wrap: false, + bordered: false, }) diff --git a/src/lib/surface.ts b/src/lib/surface.ts index 1e8ded8..0837f21 100644 --- a/src/lib/surface.ts +++ b/src/lib/surface.ts @@ -11,6 +11,11 @@ // Only the edge WIDTH is set here. The color is `--border` from the base layer // (`* { @apply border-border }`), and ActionCard animates that color in // style.css — a `border-border` utility here would outrank and freeze it. +// +// `border-1` rather than the bare `border` token: markstream-vue ships an +// unlayered `.markstream-vue .border { border-width: 1px }`, which outranks +// every layered utility and re-adds the dark-mode edge to any card rendered +// inside markdown (the chat code fence). Same CSS, a token it does not match. export function surfaceEdgeClass(bordered = false): string { - return bordered ? 'border' : 'border dark:border-0' + return bordered ? 'border-1' : 'border-1 dark:border-0' }