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..4acafb1 --- /dev/null +++ b/src/components/code-panel/CodePanel.vue @@ -0,0 +1,77 @@ + + + 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' 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' }