# UI 套件与样式

> 随心所欲地设计卡片样式，或借助基于应用实时主题的可选 ui.css 套件呈现原生外观——类名、变量，以及沙箱对样式、字体和图片的规则。

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

在自己的方框内，卡片就是一个普通网页：任何框架、任何 CSS、canvas、WebGL、WebAssembly 都行。设计样式有两种方式：

- **原生外观**——可选的 `ui.css` 套件：一套基于应用实时主题的小型深色按钮、输入框、列表和徽章。用它做出的卡片看起来就像 NeuroSquad 的一部分，并跟随它的主题。
- **你自己的设计**——不用这个套件。主题变量依然存在，想借用某个颜色时随时可用。

## UI 套件

```html
<link rel="stylesheet" href="vendor/ui.css" />   <!-- plain template -->
```

```ts
import '@neurosquad/card-sdk/ui.css'              // with a bundler (React template)
```

在 `<body>` 上加 `class="ns-kit"` 即可获得基础排版、背景和滚动条，然后使用这些类：

```html
<body class="ns-kit">
  <header class="ns-spread">
    <h1 class="ns-title ns-truncate">Test radar</h1>
    <span class="ns-badge ns-badge--danger">3 failed</span>
  </header>

  <ul class="ns-list ns-scroll">
    <li class="ns-list-item"><span class="ns-dot ns-dot--danger"></span> checkout › pays</li>
    <li class="ns-list-item"><span class="ns-dot ns-dot--success"></span> auth › logs in</li>
  </ul>

  <label class="ns-field">
    <span class="ns-label">Filter</span>
    <input class="ns-input" placeholder="checkout" />
  </label>

  <div class="ns-row">
    <button class="ns-btn ns-btn--primary">Run</button>
    <button class="ns-btn ns-btn--ghost ns-btn--sm">Clear</button>
  </div>
</body>
```

| 分组 | 类名 |
| --- | --- |
| 布局 | `ns-kit`、`ns-stack`（纵向）、`ns-row`（横向）、`ns-spread`（两端对齐）、`ns-grow`、`ns-scroll`、`ns-pad`、`ns-divider` |
| 容器 | `ns-surface`、`ns-surface--inset`、`ns-callout`、`ns-callout--success`、`ns-callout--danger`、`ns-empty` |
| 文本 | `ns-title`、`ns-subtitle`、`ns-muted`、`ns-small`、`ns-mono`、`ns-truncate`、`ns-kbd`、`ns-help`、`ns-error` |
| 按钮 | `ns-btn` + `--primary`、`--secondary`、`--outline`、`--ghost`、`--danger`、`--sm`、`--lg`、`--icon` |
| 表单 | `ns-field`、`ns-label`、`ns-input`、`ns-textarea`、`ns-select`、`ns-switch` |
| 列表 | `ns-list`、`ns-list-item` |
| 状态 | `ns-badge` + `--accent`、`--success`、`--warning`、`--danger`；`ns-dot` + 同样的修饰；`ns-spinner`、`ns-progress` |

在应用之外（浏览器预览中），套件会回退到应用的深色主题，所以预览效果也是正确的。

## 主题变量

`connect()` 会把应用的实时主题以 `--ns-<token>` 变量的形式写到 `<html>` 上——`--ns-background`、`--ns-surface`、`--ns-foreground`、
`--ns-muted`、`--ns-accent`、`--ns-success`、`--ns-warning`、`--ns-danger`，以及它们的 `-foreground` 和 `-soft` 变体、`--ns-border`、
`--ns-focus`、`--ns-field-*`、`--ns-radius`、`--ns-font-sans`、`--ns-font-mono` 等（完整列表见[主题](https://docs.neurosquad.ai/zh/card-sdk/api/environment#theme)）。
在你自己的 CSS 中使用它们，卡片就会跟随应用：

```css
:root { color-scheme: dark; }
body {
  margin: 0;
  background: var(--ns-background, #0b0b0f);
  color: var(--ns-foreground, #fafafa);
  font: 13px/1.45 var(--ns-font-sans, system-ui, sans-serif);
}
.chip {
  border-radius: calc(var(--ns-radius, 0.5rem) * 2);
  background: var(--ns-accent-soft);
  color: var(--ns-accent-soft-foreground);
}
```

如果页面也需要在应用之外渲染，请像上面那样给变量设置回退值。想让你自己的颜色不受影响，请使用 `connect({ theme: false })` 连接。

## 页面的沙箱规则

你的页面从它自己的卡片包中提供，并带有严格的内容安全策略（CSP）。实际上意味着：

- **不能有内联脚本或 `onclick="…"` 属性**——把代码放进 `.js` 文件。`eval` 和 `new Function` 也被禁止（WebAssembly 允许）。
- **不能引用外部文件。** 脚本、样式、字体和图片都必须在卡片包内；`<link href="https://fonts…">` 或 `<img src="https://…">` 会被拦截。
`data:` 和 `blob:` 图片可以用。远程数据请使用 [`card.net.fetch`](https://docs.neurosquad.ai/zh/card-sdk/api/network)，再把图片转成 `blob:` URL。
- **内联样式没问题**（`style="…"` 和 `<style>`）。
- **使用相对 URL。** 页面从 `nscard://<id>/<path>` 提供；构建时请使用相对资源路径（Vite：`base: './'`，React 模板已配置）。
- **没有 `localStorage`、Cookie、`alert`、弹窗、下载、全屏、摄像头或麦克风。** 请使用 `card.storage`、`card.ui.confirm`、`card.openLink`。
- **页面不能写入剪贴板。** `copy`/`cut` 处理函数和 `document.execCommand('copy')` 不起作用（否则任何卡片都能在一次点击时替换剪贴板）。
请使用 [`card.copyText`](https://docs.neurosquad.ai/zh/card-sdk/api/files#clipboard)；用户自己复制选中文本仍然可以。
- **不能嵌套框架。** `iframe`、`frame`、`object` 和 `embed` 元素会被移除。
- **只能用 blob Worker。** `new Worker('worker.js')` 在卡片的源中会失败；请先获取脚本，把它包装成 `Blob`，
再用 `new Worker(URL.createObjectURL(blob))` 启动。
- **永远不要信任 `message` 事件。** 其他卡片可以对你的框架调用 `postMessage`。

`npx @neurosquad/card-sdk validate` 会对入口页面中的内联脚本、内联事件处理器和外部文件发出警告。

> **表单永远不会跳转。** `<form>` 的 `submit` 事件会触发，你的处理函数也会运行，但应用会取消提交本身，
> `form.submit()` 也什么都不做——卡片页面无法向任何地方提交，也无法重新加载。无论如何，请在处理函数中调用
> `event.preventDefault()`，并用 JavaScript 完成工作。

## 为画布而设计

- **大小皆宜。** 卡片通常是 400×300，有时会展开铺满屏幕。两种状态都要做得完整：用 `card.lifecycle.onExpanded` 或 `useExpanded()` 切换布局。
- **空状态、加载中、出错。** 数据到达之前、没有数据时、调用失败时，都要显示合理的内容。
- **概览图块就是缩小后的你的卡片。** 用 [`setOverview`](https://docs.neurosquad.ai/zh/card-sdk/api/card-ui#overview) 持续展示最重要的那一条信息。
- **没人看时保持安静。** `usePaused()` 为 true 时停止动画——一块画布上可能有五十张卡片。
- **标题栏属于应用。** 不要在正文里重复卡片名称或关闭按钮；标题栏已经有了。

> 卡片正文里的一切都属于卡片自己，用户也被如此告知。不要模仿 NeuroSquad 的对话框，也不要在卡片里索要密码和密钥——
> 请使用 `secret` 设置，由应用在它自己的表单中询问。
