Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .codex/skills/ipollowork-plugin-ui/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ description: 为 iPolloWork 新插件或插件工作区复用 UI 组件、颜色
- 宿主 React 使用原有 `@/components/ui/*` 入口;共享包控件入口为 `@ipollowork/ui/controls`。
- iframe 插件只从 `@ipollowork/ui/runtime-contract` 导入 `requireRuntime(1)`;实际 React/控件从返回的运行时对象取得。不要从 `plugin-runtime` 或宿主私有路径导入实现,也不要给生产插件再打包 React。
- 开发构建显式选择 `buildPluginRuntime('bundled')`,将运行时放在业务脚本之前;生产包不包含运行时,保留契约中的固定 meta 标记,由宿主注入。不能仅凭 `NODE_ENV` 推断加载方式。
- 当前运行时版本为 `1.3.0`。使用本轮颜色与字体规范时调用 `requireRuntime(1, required, 3)`,第三参数检查最低 minor;required 列出实际使用的全部函数导出(包括子组件)。旧版本或缺组件时停止初始化并提示更新客户端。不支持完整 semver 范围、最低 patch 或 manifest UI 版本协商,也不自动下载另一份组件库。
- 当前运行时版本为 `1.4.0`。使用本轮颜色与字体规范时调用 `requireRuntime(1, required, 3)`,第三参数检查最低 minor;使用公共 Icon 时调用 `requireRuntime(1, ['Button', 'Icon'], 4)`,尺寸 S/M/L 为14/16/20px,Button 前后图标直接作为 children 组合,纯图标按钮必须有 aria-label。required 列出实际使用的全部函数导出(包括子组件)。旧版本或缺组件时停止初始化并提示更新客户端。不支持完整 semver 范围、最低 patch 或 manifest UI 版本协商,也不自动下载另一份组件库。

1.2 核心扩展及 Alert/Toast 浅背景已本地整合到 Carrie,未提交、推送或发布;隔离分支仍保留。核心样板以浏览器沙箱验证,真实客户端安装验证另记在接入契约,不把两者等同。签名包上传、打包客户端和跨客户端版本升级未验收。推荐目录的 62 个条目不等于运行时已提供 62 个独立组件。

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
| 核心范围 | 共享公共组件 |
| --- | --- |
| 输入与选择 | Label、Field、Input、InputGroup、Textarea、Select、Autocomplete、Checkbox、RadioGroup、Switch、Toggle、ToggleGroup |
| 动作与菜单 | Button、DescriptiveButton、DropdownMenu、ContextMenu、Command(含 CommandInput / CommandDialog) |
| 动作与菜单 | Icon(运行时1.4,32个Lucide公共名字)、Button、DescriptiveButton、DropdownMenu、ContextMenu、Command(含 CommandInput / CommandDialog) |
| 浮层 | Dialog、AlertDialog、Sheet、Popover、HoverCard、Tooltip |
| 反馈与状态 | Alert(Info/Success/Warning/Error)、Toaster/toast、Badge、Progress、Skeleton、Empty |

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"figmaFileKey": "kpx32oBzY6ecHJZX11mAvr",
"runtimeVersion": "1.3.0",
"runtimeVersion": "1.4.0",
"foundations": {
"figmaCollectionId": "VariableCollectionId:237:1443",
"modes": { "Light": "237:0", "Dark": "237:1" },
Expand Down Expand Up @@ -33,6 +33,15 @@
},
"tokenSource": "packages/ui/src/common/tokens.css",
"components": [
{
"export": "Icon",
"relatedExports": ["ICON_NAMES"],
"package": "@ipollowork/ui/icon",
"source": "packages/ui/src/react/icon.tsx",
"figmaMainId": null,
"htmlMarker": null,
"scope": "32 curated Lucide glyphs; React/runtime composition; no dedicated Figma component identity"
},
{
"export": "Button",
"package": "@ipollowork/ui/controls",
Expand Down
24 changes: 23 additions & 1 deletion .codex/skills/ipollowork-plugin-ui/references/plugin-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ HTML 的 `<head>` 保留这个原样标记:

运行时样式已包含共享 palette/tokens/theme 与控件 utilities;插件不要再次手写控件 CSS。样板把业务 CSS 放在 `@layer plugin`,避免通用 button/input 样式覆盖共享控件。更新客户端运行时后,重新打开或重载工作区才取得新实现;已打开的文档没有热替换协议。Figma 编辑不会直接修改线上插件。

无构建步骤的原始 HTML 包可在 module script 中使用固定公共导入 `import { requireRuntime } from "@ipollowork/ui/runtime-contract";`。宿主 `withSharedUiRuntime` 仅对带标记的 HTML 内联此无 React 的检查函数;独立开发预览也需显式注入 bundled 运行时。不支持任意裸模块导入,也不放宽 CSP。

## 两种环境共用的业务接法

原生 HTML 在容器内标记真实 button/input/textarea,然后调用 `enhance(container)`:它查找容器的后代,不包含容器自身。
Expand Down Expand Up @@ -191,7 +193,7 @@ Select 保存中禁用并关联字段错误;风格提示失败恢复持久化

## 版本兼容规则

当前 `UI_RUNTIME_VERSION = '1.3.0'`;独立于客户端与插件版本。1.3增加公共颜色/字体变量、保证七档字号类生成并增加最低minor检查;保留原v1组件入口与Props。短片包0.1.3采用新规范,插件ID和数据路径不变。生产业务脚本不包含组件实现;旧体积统计属于1.2快照,不当作本轮产物实测。
当前 `UI_RUNTIME_VERSION = '1.4.0'`(1.4新增公共Icon;1.3规范保持兼容);独立于客户端与插件版本。1.3增加公共颜色/字体变量、保证七档字号类生成并增加最低minor检查;保留原v1组件入口与Props。短片包0.1.3采用新规范,插件ID和数据路径不变。生产业务脚本不包含组件实现;旧体积统计属于1.2快照,不当作本轮产物实测。

| 运行时情况 | 当前代码行为 | 插件要求 |
| --- | --- | --- |
Expand Down Expand Up @@ -251,3 +253,23 @@ Select 保存中禁用并关联字段错误;风格提示失败恢复持久化
使用插件已支持的宿主能力进行打开链接、发送上下文或展示模式切换。页面按钮不能直接操纵宿主 DOM,也不能仅凭 Figma 展示推断宿主 API 可用。异步操作的最终状态来自真实结果。

首次接入优先验证一条完整小流程:填写 → 选择 → 提交中 → 成功或失败 → 重试;另验证主题更新和容器缩放。控件预览通过、模拟桥接通过与安装到宿主后通过分别记录。

## 公共图标与按钮 · 运行时1.4

共享源码使用 `import { Icon, type IconName } from '@ipollowork/ui/icon'`,或从core导入;iframe使用 `requireRuntime(1, ['Button', 'Icon'], 4)`。公共范围为 `ICON_NAMES` 列出的32个具名Lucide图标,不导出Lucide全集,不支持任意字符串。新图标必须加入同一映射并补齐验证。图标为线性描边、currentColor,默认M16px;S14px、L20px为固定像素,不随根字号或Button默认SVG选择器缩放。

```js
import { requireRuntime } from '@ipollowork/ui/runtime-contract';
const ui = requireRuntime(1, ['Button', 'Icon'], 4);
const h = ui.React.createElement;
const leading = h(ui.Button, { size: 'sm', onClick: save },
h(ui.Icon, { name: 'Plus', size: 's', 'data-icon': 'inline-start' }), '新建');
const trailing = h(ui.Button, { onClick: download }, '下载',
h(ui.Icon, { name: 'Download', 'data-icon': 'inline-end' }));
const iconOnly = h(ui.Button, { size: 'icon', 'aria-label': '搜索', onClick: search },
h(ui.Icon, { name: 'Search' }));
```

Button继续使用原children API:sm28px、default32px、lg36px;icon-sm/icon/icon-lg分别同尺寸,xs24兼容。图标默认aria-hidden装饰;独立表达状态的图标提供label,生成role=img和可访问名称。纯图标按钮名称放在Button的aria-label,内部Icon保持装饰,不重复朗读。加载由调用方管理disabled/aria-busy与LoaderCircle的animate-spin,操作成功后恢复;Icon不自动触发动作或异步任务。

验收范围为现有shared-ui-core样板和双模式流程:所有公共名字SVG非空、S/M/L、前置/后置/纯图标、真实鼠标与键盘、禁用/加载、亮暗窄容器及13/16px根字号。此处描述验证契约,完成状态以本轮fraimz证据为准。
83 changes: 45 additions & 38 deletions apps/app/src/app/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -203,20 +203,34 @@ body {
}

[data-chat-readable-text="true"] {
font-size: 14px;
line-height: 1.5;
}

[data-chat-readable-text="true"] :is(p, li) {
line-height: 1.5;
}

[data-assistant-result] .markdown-content > * {
margin-block: 0;
}

[data-assistant-result] .markdown-content > * + * {
margin-top: 12px;
font-family: var(--ipollowork-font-sans);
font-size: var(--ui-body-size);
line-height: var(--ui-body-line);
font-weight: 400;
}

[data-chat-readable-text="true"] :is(p, li) { line-height: inherit; }
[data-chat-readable-text="true"] :is(strong, b) { font-weight: 600; }
[data-chat-readable-text="true"].markdown-content :where(p, ul, ol, li, blockquote, pre) { margin-block: 0; }
[data-chat-readable-text="true"].markdown-content > * { margin-block: 0; }
[data-chat-readable-text="true"].markdown-content > * + * { margin-top: 8px; }
[data-chat-readable-text="true"].markdown-content li + li,
[data-chat-readable-text="true"].markdown-content li > :is(ul, ol) { margin-top: 4px; }
[data-chat-readable-text="true"].markdown-content h1 {
font-size: var(--ui-page-title-size);
line-height: var(--ui-page-title-line);
}
[data-chat-readable-text="true"].markdown-content :is(h2, h3, h4, h5, h6) {
font-size: var(--ui-section-title-size);
line-height: var(--ui-section-title-line);
}
[data-chat-readable-text="true"].markdown-content :is(h1, h2, h3, h4, h5, h6) { margin-block: 0; font-weight: 600; }
[data-chat-readable-text="true"].markdown-content > :is(h1, h2, h3, h4, h5, h6):not(:first-child) { margin-top: 16px; }
[data-chat-readable-text="true"].markdown-content :is(pre, code, [data-ipollowork-shiki]) {
font-family: var(--ipollowork-font-mono);
font-size: var(--ui-body-size);
line-height: var(--ui-body-line);
font-weight: 400;
}

.chat-thinking-dots {
Expand All @@ -242,6 +256,9 @@ body {
.chat-thinking-dots span:nth-child(2) { animation-delay: 0.2s; }
.chat-thinking-dots span:nth-child(3) { animation-delay: 0.4s; }

/* Interrupted and historical tools can retain an in-flight protocol state. */
[data-process-active="false"] .animate-spin { animation: none; }

.chat-live-activity {
animation: chat-live-activity 1.8s ease-in-out infinite;
}
Expand All @@ -262,26 +279,15 @@ body {
.chat-live-activity { animation: none; opacity: 0.75; }
}

/* Keep small labels, tool details, code blocks, and file titles in the conversation readable. */
[data-chat-transcript] :is(.text-xs, .chat-output-title) {
font-size: 14px;
[data-chat-transcript] .text-xs,
.chat-process-heading {
font-size: var(--ui-meta-size);
line-height: var(--ui-meta-line);
}

.chat-tool-action[open] > summary .chat-tool-action-chevron {
transform: rotate(90deg);
}
.chat-tool-action > summary { font-size: var(--ui-control-size); line-height: var(--ui-control-line); }

html:lang(zh).ipollowork-electron.ipollowork-platform-mac [data-chat-readable-text="true"] {
font-weight: 500;
}

html:lang(zh).ipollowork-electron.ipollowork-platform-mac [data-chat-readable-text="true"] :is(strong, b) {
font-weight: 600;
}

html:lang(zh) .chat-process-heading {
font-family: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", var(--ipollowork-font-sans);
}
.chat-tool-action[open] > summary .chat-tool-action-chevron { transform: rotate(90deg); }

/* Match the existing macOS Chinese readability correction in settings/chat.
PingFang's 400 fallback can render as ExtraLight in Electron; keep captions
Expand Down Expand Up @@ -770,29 +776,30 @@ html:lang(zh).ipollowork-electron.ipollowork-platform-mac .media-composer-contro
}

/* Chat file links and response outputs share one visual specification. */
.chat-output-card { position:relative; display:inline-flex; align-items:center; gap:10px; width:320px; max-width:100%; height:56px; padding:10px; border:1px solid var(--border); border-radius:12px; background:var(--background); color:var(--foreground); box-shadow:none; vertical-align:middle; text-align:left; }
.chat-output-card:hover { background:var(--muted); }
.chat-output-card { position:relative; display:inline-flex; align-items:center; gap:10px; width:320px; max-width:100%; min-height:64px; padding:9px 10px; border:1px solid var(--border); border-radius:12px; background:var(--background); color:var(--foreground); box-shadow:none; vertical-align:middle; text-align:left; }
.chat-output-card:hover { border-color:color-mix(in srgb,var(--muted-foreground) 30%,var(--border)); }
.chat-output-open { display:flex; align-items:center; gap:10px; flex:1; min-width:0; height:100%; padding:0; text-align:left; }
.chat-output-icon { display:grid; place-items:center; width:32px; height:32px; flex-shrink:0; border-radius:8px; background:var(--muted); color:var(--muted-foreground); }
.chat-output-content { display:flex; flex:1; min-width:0; flex-direction:column; gap:2px; }
.chat-output-title { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12px; font-weight:500; line-height:16px; }
.chat-output-description { display:block; font-size:10px; font-weight:400; line-height:14px; color:var(--muted-foreground); }
.chat-output-title { display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; text-overflow:ellipsis; white-space:normal; font-size:var(--ui-control-size); font-weight:500; line-height:var(--ui-control-line); }
.chat-output-description { display:block; font-size:var(--ui-caption-size); font-weight:400; line-height:var(--ui-caption-line); color:var(--muted-foreground); }
.chat-output-more { display:grid; place-items:center; width:28px; height:28px; flex-shrink:0; border-radius:8px; color:var(--muted-foreground); }
.chat-output-more:hover { background:var(--background); color:var(--foreground); }

[data-testid="artifact-file-shell"] > .chat-output-card { padding-right:76px; }
.chat-output-title code { font:inherit; background:transparent; padding:0; }

/* File list actions stay inside the same card at every panel width. */
[data-testid="artifact-file-shell"] > .chat-output-card { width:100%; }
.chat-output-icon svg { color:var(--muted-foreground); stroke-width:1.8; }
[data-testid="artifact-file-actions"] { right:10px; }
[data-testid="artifact-file-actions"] button { box-shadow:none; }

/* File cards use the same responsive rows in Markdown and output lists. */
.chat-output-grid { display:grid; min-width:0; grid-template-columns:repeat(auto-fill,minmax(min(100%,16rem),1fr)); gap:12px; }
.chat-output-grid .chat-output-card { display:flex; width:100%; min-width:0; }

.chat-output-card { padding-top:7px; padding-bottom:7px; }
.chat-output-icon { position:relative; width:40px; height:40px; overflow:hidden; }
.chat-output-icon .artifact-thumbnail { position:absolute; inset:0; display:block; width:40px; height:40px; max-width:none; margin:0; object-fit:cover; border-radius:8px; }
.chat-output-icon { position:relative; overflow:hidden; }
.chat-output-icon[data-artifact-thumbnail] { width:64px; height:44px; border-radius:6px; }
.chat-output-icon .artifact-thumbnail { position:absolute; inset:0; display:block; width:100%; height:100%; max-width:none; margin:0; object-fit:contain; border-radius:6px; }
.chat-output-icon .artifact-thumbnail-play { position:absolute; right:2px; bottom:2px; width:14px; height:14px; padding:2px; border-radius:4px; background:rgba(0,0,0,.55); color:white; fill:currentColor; stroke:none; }
19 changes: 6 additions & 13 deletions apps/app/src/components/chat/artifact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ interface ArtifactButtonProps {
onOpenVideoStudio?: (displayName?: string) => void
}

const MAX_ARTIFACT_TITLE_LENGTH = 32;
const EMPTY_WORKSPACE_FILES: iPolloWorkWorkspaceCatalogEntry[] = [];

export type ArtifactRequestNaming = {
Expand Down Expand Up @@ -250,12 +249,6 @@ export function filterWorkspaceFileTree(nodes: readonly WorkspaceFileTreeNode[],
return matches;
}

function compactArtifactTitle(name: string) {
return name.length > MAX_ARTIFACT_TITLE_LENGTH
? `${name.slice(0, MAX_ARTIFACT_TITLE_LENGTH - 1)}...`
: name;
}

function ArtifactButton({ artifact, displayName, client, workspaceId, sessionId, artifactContext, onOpenVideoStudio }: ArtifactButtonProps) {
const thumbnailRoot = useRef<HTMLSpanElement>(null);
const { loadWorkspaceThumbnail } = useOpenTargets();
Expand Down Expand Up @@ -301,7 +294,7 @@ function ArtifactButton({ artifact, displayName, client, workspaceId, sessionId,
const presentedArtifact = presentedName === artifact.name
? artifact
: { ...artifact, name: presentedName, target: { ...artifact.target, name: presentedName } };
const title = compactArtifactTitle(presentedName);
const title = presentedName;
const typeLabel = getArtifactTypeLabel(studioTarget?.surface === "video" ? "video" : artifact.type);
const extension = artifact.name.includes(".") ? artifact.name.slice(artifact.name.lastIndexOf(".") + 1).toUpperCase() : typeLabel;
const canDownload = Boolean(client && workspaceId && artifact.target.kind === "file");
Expand Down Expand Up @@ -358,12 +351,12 @@ function ArtifactButton({ artifact, displayName, client, workspaceId, sessionId,
}

return (
<div className={cn("group/output relative max-w-full", "h-14 w-full min-w-0")} data-testid="artifact-file-shell">
<div className={cn("group/output relative max-w-full", "min-h-16 w-full min-w-0")} data-testid="artifact-file-shell">
<DescriptiveButton
disabled={!canActivate}
data-testid="artifact-file-card"
data-artifact-path={artifact.path}
className={cn("max-w-full items-center whitespace-nowrap", "chat-output-card pr-20")}
className={cn("max-w-full items-center whitespace-nowrap", "chat-output-card")}
onClick={() => {
if (opensCurrentVideoStudio) {
onOpenVideoStudio?.(presentedName);
Expand All @@ -378,12 +371,12 @@ function ArtifactButton({ artifact, displayName, client, workspaceId, sessionId,
{content}
</DescriptiveButton>
{(
<div className="pointer-events-none absolute right-3 top-1/2 flex -translate-y-1/2 items-center gap-0.5 opacity-0 transition-opacity group-hover/output:pointer-events-auto group-hover/output:opacity-100 group-focus-within/output:pointer-events-auto group-focus-within/output:opacity-100" data-testid="artifact-file-actions">
<div className="pointer-events-none absolute right-3 top-1/2 flex -translate-y-1/2 items-center gap-0.5 bg-background opacity-0 transition-opacity group-hover/output:pointer-events-auto group-hover/output:opacity-100 group-focus-within/output:pointer-events-auto group-focus-within/output:opacity-100" data-testid="artifact-file-actions">
{canDownload ? (
<Button
variant="ghost"
size="icon-sm"
className="size-7 rounded-lg bg-background/90 text-muted-foreground hover:bg-muted hover:text-foreground"
className="size-7 rounded-md bg-transparent text-muted-foreground/70 hover:bg-muted/40 hover:text-foreground [&_svg]:stroke-[1.5]"
aria-label={t("artifact.download_artifact")}
title={t("artifact.download_artifact")}
disabled={downloading}
Expand All @@ -398,7 +391,7 @@ function ArtifactButton({ artifact, displayName, client, workspaceId, sessionId,
<Button
variant="ghost"
size="icon-sm"
className="size-7 rounded-lg bg-background/90 text-muted-foreground hover:bg-muted hover:text-foreground"
className="size-7 rounded-md bg-transparent text-muted-foreground/70 hover:bg-muted/40 hover:text-foreground [&_svg]:stroke-[1.5]"
aria-label={t("session.outputs.more_actions")}
data-testid="artifact-file-more"
title={t("session.outputs.more_actions")}
Expand Down
Loading
Loading