# 卡片外框与对话框

> 卡片的标题栏——标题、状态、徽章——概览图块、提醒、调整大小、链接、镜头聚焦和创建卡片；提示条、确认对话框和卡片菜单。

Source: https://docs.neurosquad.ai/zh/card-sdk/api/card-ui

卡片的标题栏、概览图块和对话框都由**应用**绘制，而不是你的页面——因此它们看起来是原生的，在卡片暂停时依然有效，也会显示在手机上。你只负责设置内容。

**想调用多少次都行。** 应用会对这些更新限流（标题每分钟 30 次；状态、徽章和概览各每分钟 120 次；提醒每 10 秒一次），
而 SDK 会替你消化这些限制：对于 `setTitle`、`setStatus`、`setBadge`、`setOverview` 和 `attention`，每个方法同一时间只发送一个调用，
期间发起的调用会合并成一个、只保留最新的值，应用返回 `RATE_LIMITED` 时还会重试最新的值。它们永远不会因为限流而抛出异常——
每次渲染都更新也没问题。没有改变任何内容的更新不产生任何开销。

## 标题

```ts
await card.setTitle('Checkout tests') // null clears it
```

显示在标题栏、侧边栏和小队（Squad）卡片中的名称。如果用户给卡片改过名，以用户起的名字为准；两者都没有时显示清单中的 `displayName`。
≤ 120 个字符；控制字符、双向文本字符和不可见字符会被去除。

## 状态

```ts
await card.setStatus('Running tests…', { busy: true })       // spinner
await card.setStatus('3 failed', { tone: 'danger' })
await card.setStatus(null)                                    // clear
```

标题栏中的一个标签，≤ 80 个字符。色调：`default`、`accent`、`success`、`warning`、`danger`。

## 徽章

```ts
await card.setBadge(3)                          // a count
await card.setBadge('NEW', { tone: 'accent' })  // or a short text, ≤ 12 characters
await card.setBadge(null)                       // clear
```

## 概览图块

当用户把画布缩小到超过概览阈值时，每张卡片都会显示一个展示其关键信息的图块，而不是正文。你的卡片显示的就是你在这里设置的内容——
卡片暂停时、在[手机](https://docs.neurosquad.ai/zh/card-sdk/phone)上也会继续显示。应用会记住它。

```ts
await card.setOverview({
  primary: '3 failing',        // big line, ≤ 160
  secondary: 'checkout, auth', // small line, ≤ 160
  progress: 0.8,               // 0..1, a progress bar; null hides it
  tone: 'danger',
  icon: 'bug-ant'
})
```

请保持它是最新的：在繁忙的画布上，这是大多数人唯一会看到的你的卡片内容。

应用能绘制的**图标**（heroicons，outline 风格）：`academic-cap`、`arrow-path`、`beaker`、`bell`、
`bolt`、`book-open`、`bug-ant`、`calendar`、`camera`、`chart-bar`、`chart-pie`、
`chat-bubble-left-right`、`check-circle`、`clock`、`cloud`、`code-bracket`、`command-line`、
`cpu-chip`、`cube`、`currency-dollar`、`document-text`、`exclamation-triangle`、`film`、`fire`、
`flag`、`folder`、`globe-alt`、`heart`、`inbox`、`key`、`light-bulb`、`link`、`list-bullet`、`map`、
`megaphone`、`moon`、`musical-note`、`newspaper`、`paper-airplane`、`pause`、`photo`、`play`、
`puzzle-piece`、`rocket-launch`、`server`、`shield-check`、`signal`、`sparkles`、`star`、`stop`、
`sun`、`table-cells`、`tag`、`trash`、`trophy`、`users`、`wrench-screwdriver`（SDK 中的 `HOST_ICON_NAMES` 列表）。
在你自己的页面里，想画什么图标都可以。

## 提醒

```ts
await card.attention('needs-input', 'Pick a branch to deploy')
await card.attention('info')    // a softer pulse
await card.attention('none')    // clear
```

`needs-input` 会让卡片像等待用户的智能体一样闪烁，并把它列入**收件箱（Inbox）**。每 10 秒最多一次。第 1 版中没有声音，也没有系统通知。

**当前显示的是什么。** 应用会在重新加载、暂停和更新之间保留卡片的标题、状态、徽章、概览和提醒。`card.context.chrome`
告诉你的页面当前保存着什么，所以在重新加载前发出过提醒的卡片可以清除过时的闪烁：

```ts
const attention = card.context.chrome?.attention
if (attention && !stillNeedsTheUser()) await card.attention('none')

declare function stillNeedsTheUser(): boolean
```

在旧版本的应用中没有 `chrome`——请把这种情况当作“未知”。

## 调整大小

```ts
const applied = await card.requestResize({ w: 640, h: 480 })
render(applied.w, applied.h)
```

会被限制在清单的 `minSize`/`maxSize` 之间；返回实际应用的大小。它会像手动调整大小一样进入画布的撤销历史。
每 500 毫秒最多一次。用户也随时可以调整大小——用 [`card.lifecycle.onResized`](https://docs.neurosquad.ai/zh/card-sdk/api/environment#lifecycle) 监听。

## 打开链接

```ts
const opened = await card.openLink('https://github.com/acme/app/issues/42')
```

应用会在它自己覆盖整个窗口的对话框中显示完整地址；只有在用户点击 **Open link** 之后，链接才会在用户的浏览器中打开。
只允许 `https://`，并且只能在卡片可见时调用（否则为 `NOT_VISIBLE`）。用户拒绝时返回 `false`。卡片不能让自己跳转，也不能打开窗口。

## 飞到另一张卡片

```ts
await card.focusCard(cardId)
```

把镜头移到同一工作区的另一张卡片——适合做“带我去看失败的智能体”这类按钮。只能在你的卡片可见时调用，每 5 秒最多一次。

## 创建卡片

需要 [`canvas.spawn`](https://docs.neurosquad.ai/zh/card-sdk/permissions#canvas-spawn)。

```ts
// A note next to this card, connected with an arrow from this card.
const noteId = await card.spawn('note', { title: 'Test report' })
await card.ports.send(noteId, 'summary', '# Report\n\nAll green.')

// Another copy of this card, with data for its first start.
await card.spawn('self', { init: { suite: 'e2e' }, connect: false })
```

类型：`self`（你的卡片包的另一张卡片，首次启动时会以 `card.spawnInit` 收到 `init`）、`note`、`todo`、`kanban`、`sticky`。
新卡片会放在你的卡片旁边，并用一条从你的卡片出发的箭头相连，除非传入 `connect: false`。每张卡片最多 4 张，每分钟最多 4 张。

## 卡片信息

```ts
const info = await card.getInfo() // CardInstanceInfo, fresh from the app
render(info.instanceId, info.packageId, info.commit ?? 'dev folder', info.size)
```

## 提示条

```ts
await card.ui.toast('Copied', { tone: 'success', durationMs: 2000 })
```

卡片方框内的一条小提示，≤ 280 个字符，显示 1–15 秒，每 2 秒最多一条。

## 确认对话框

在卡片沙箱中，`alert`、`confirm` 和 `prompt` 什么也不做。请改为让应用来询问：

```ts
const ok = await card.ui.confirm({
  title: 'Delete all saved runs?',
  message: 'This removes 42 runs from this card. It cannot be undone.',
  confirmLabel: 'Delete',
  cancelLabel: 'Keep',
  tone: 'danger'
})
if (ok) await card.storage.clear()
```

应用会把它显示为**覆盖整个窗口的它自己的对话框**，而不是在你的卡片里：一个固定的标题说明你的卡片在请求确认，
你的 `title` 和 `message` 以引用的形式出现，然后是你的 `confirmLabel`（如果它读起来像取消/否/拒绝、提到了 NeuroSquad
或包含控制字符，就会被替换为应用的“Confirm”），以及应用自己的 Cancel。只能在卡片可见时调用，每分钟最多 10 次。
多张卡片的对话框会排队。

## 卡片菜单

在卡片的 **⋯** 菜单中，于应用自带的菜单项（Settings…、Reload、About…）之后添加最多 12 项。

```ts
await card.ui.setMenu([
  { id: 'rerun', label: 'Run again', icon: 'arrow-path', onSelect: () => void rerun() },
  { id: 'export', label: 'Copy report', icon: 'document-text', onSelect: () => void copyReport() },
  { id: 'reset', label: 'Reset', tone: 'danger', disabled: true }
])

// Or handle every choice in one place:
card.ui.onMenu((id) => card.log.info('menu', id))

declare function rerun(): Promise<void>
declare function copyReport(): Promise<void>
```

每次调用都会替换之前的菜单项（每分钟最多 30 次）。`onSelect` 留在你的卡片里——不会发送给应用。
标签 ≤ 60 个字符；`icon` 取自[宿主图标](#overview)。

> 本页中除对话框和链接以外的一切，在卡片不在屏幕上时同样有效。对话框、链接、镜头聚焦和权限申请需要用户正在看：
> 否则会以 `NOT_VISIBLE` 失败。
