# ServiceTitan Anvil 设计系统

## ServiceTitan Anvil 设计系统界面预览

切换浅色/深色外观、选择可用员工，并查看同步的本地计划。

## ServiceTitan Anvil 设计系统设计参考

@servicetitan/hammer-token 3.5.0 核心主题导出，以及 Anvil2 主题文档。

### 语义化浅色与深色配对值

| 名称 / 规则 | 值 / 配置 | 用途 |
| --- | --- | --- |
| BackgroundColorDefault | light #ffffff / dark #1a1a1a | 主要应用界面表面。 |
| BackgroundColorDefaultHover | light #f9f9f9 / dark #292929 | 悬停界面表面。 |
| BackgroundColorPrimary | light #0265dc / dark #70b1ff | 主操作填充。 |
| BackgroundColorPrimaryHover | light #1d4ca3 / dark #9ecaff | 原生主色悬停角色。 |
| BackgroundColorPrimaryFaint | light #fafcff / dark #000b19 | 克制的选中界面表面。 |
| ForegroundColorSubdued | light #707070 / dark #c4c4c4 | 辅助性文本。 |
| ForegroundColorOnPrimary | light #ffffff / dark #1a1a1a | 与主色填充配对的文本。 |
| ForegroundColorPrimary | light #0265dc / dark #70b1ff | 交互前景色。 |
| BorderColorSubdued | light #e6e6e6 / dark #404040 | 低强调度边界。 |
| BorderColorPrimary | light #0265dc / dark #9ecaff | 交互边界。 |
| StatusColorInfo | light #0265dc / dark #70b1ff | 信息状态。 |
| StatusColorDanger | light #d62100 / dark #ff5b3d | 需要立即关注。 |
| StatusColorSuccess | light #077e50 / dark #0bd085 | 本地审核已完成。 |
| StatusColorWarning | light #ffbe00 / dark #ffe278 | 非关键警示。 |
### 基础几何值与响应式数值

| 名称 / 规则 | 值 / 配置 | 用途 |
| --- | --- | --- |
| BreakpointSm | 640px | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
| BreakpointMd | 768px | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
| BreakpointLg | 1024px | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
| BreakpointXl | 1280px | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
| BreakpointXxl | 1536px | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
| DurationDefault | 200ms | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
| DurationInstant | 0ms | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
| DurationFast | 100ms | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
| DurationSlow | 300ms | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
| Radius0 | 0rem | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
| Radius1 | 0.1875rem | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
| Radius2 | 0.375rem | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
| Radius3 | 0.5625rem | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
| Radius4 | 0.75rem | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
| Radius5 | 0.9375rem | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
| Radius6 | 1.125rem | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
| Radius7 | 1.3125rem | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
| Radius8 | 1.5rem | 已发布的基础值；实际应用角色由语义别名和组件别名决定。 |
### ThemeProvider 覆盖契约

| 名称 / 规则 | 值 / 配置 | 用途 |
| --- | --- | --- |
| theme.semantic | semantic token overrides | 更改某个角色，例如 BackgroundColorPrimary。 |
| theme.component | component token overrides | 将更改范围缩小到某个组件角色，例如 ButtonPrimaryBackgroundColor。 |
| value | default/light token value | 提供浅色外观值。 |
| extensions.appearance.dark.value | dark appearance value | 指定对应的深色外观值。 |
| CSS output | --a2-<kebab-case-name> | ThemeProvider 会将设计令牌键序列化为 CSS 变量。 |
| Primitive cascade | resolved at build time | 在运行时覆盖基础值不会重新计算语义别名。 |
## ServiceTitan Anvil 设计系统完整指南

实用指南，介绍如何为真实界面选择、应用和评审此系统。

完整指导、实现起点和源代码链接。

## ServiceTitan Anvil 设计系统三层设计令牌架构

### 将基础值置于语义角色之下

> Anvil2 将原始基元、语义角色和组件专用别名分层管理。产品通常应选择能够表达界面含义的角色，而不是将原始蓝色复制到每个控件中。参考表列出了精确的导出值及其深色模式对应值，便于成对审查主题变更。

### 不要预期别名会在运行时重新计算

官方主题指南指出，原始基元引用会在构建期间解析。通过运行时提供程序更改原始基元，不会自动重新生成语义层。要更改应用配色，请覆盖相关语义角色；要更改单个组件，请使用组件层级。这样可以避免主题只发生部分变化，导致相关控件之间不一致。

## ServiceTitan Anvil 设计系统的浅色与深色外观

### 成对调整前景色和背景色

主要操作使用 BackgroundColorPrimary 搭配 ForegroundColorOnPrimary。在浅色外观中，背景为蓝色，文字为白色；在深色外观中，背景为较浅的蓝色，标签则为深色。只切换填充色会导致颜色搭配不协调。完整的 HTML 示例使用 light-dark() 和 color-scheme 开关演示这些取值，但不会声称自己是 ThemeProvider。

### 为状态色调赋予明确含义

Anvil2 使用不同色系表达状态含义。本地示例采用成功角色，但仍会准确说明发生了什么。无法接单的技术人员会通过文字标明，并显示为禁用单选项。示例不会用绿色调暗示已派出上门服务；它也不会连接派单后端。

## ServiceTitan Anvil 设计系统的服务分配与状态

### 让可用性决定下一步选择

选择技术人员后，开始时间选项会替换为此人的虚构可用时间。无法接单的人员仍会显示，但不能选择。这体现了两个控件之间的依赖关系，因此更改第一个控件后，必须将第二个控件重置为有效值，而不能保留属于其他人员的时间。

### 将计划与执行分开

确认分配时会记录本地计划，并明确说明尚未派出上门服务。生产工作流在提交之前需要进行权限检查、获取最新排班信息并处理冲突。示例始终清楚呈现这一界限，同时提供完整、可操作且便于调整的交互。

## ServiceTitan Anvil 设计系统的操作密度与易读性

### 让任务信息靠近选择项

紧凑的任务摘要包含任务内容、时长、区域和优先级。技术人员选项以清晰易读的纵向列表呈现，拟定上门安排则放在独立面板中。界面采用中性色边界和克制的强调色，让所选项和主要操作一目了然。排班条标注为示意内容，实际时间以选择框为准。

### 在移动设备上保持阅读顺序

视口宽度小于 768px 时，拟定上门安排会显示在任务和技术人员列表之后。控件保持足够尺寸，标签也能自然换行。预览不使用固定高度的视口面板，因此短内容下方不会出现大片空白。单列布局与较宽屏幕上的决策顺序一致。

## ServiceTitan Anvil 设计系统的原生集成与完整 HTML 替代方案

### 使用文档中说明的提供程序结构

Anvil2 的 ThemeProvider 接受语义和组件设计令牌覆盖项，并支持值和深色外观扩展。下方的指南提供了完整的提供程序示例，内容来自公开文档。这是原生集成参考，与在本地运行的独立 HTML 示例不同。

### 避免声称未经验证的程序包运行时

本页面提供可运行的 HTML 实现和经过验证的设计令牌数据。原生提供程序示例是独立的集成参考，并非预览中展示的运行时。采用 React 组件时，请安装彼此兼容的 Anvil2 版本，包含所需样式，并在替换原生 HTML 控件之前验证组件 API。

## ServiceTitan Anvil 设计系统的无障碍性与状态验证

### 使用原生表单语义

技术人员选项使用带标签的原生单选按钮，开始时间使用带标签的选择框，外观控制则通过 aria-pressed 提供状态。确认消息使用状态区域。浏览器原生键盘交互方式负责处理单选操作；可见焦点样式独立于选中或悬停样式。

### 确认后检查变更

更换技术人员或时间会清除之前的审核反馈。切换外观会保留已选的技术人员和时间，因为更改视觉主题不应清除计划。测试应涵盖这两种状态转换，以及禁用选项和窄屏溢出，而不应只检查按钮是否可点击。

## ServiceTitan Anvil 设计系统 AI Agent 交接与实施

### 从完整状态模型入手

保存并运行完整的 HTML 示例以检查行为。其基于令牌的浅色/深色配对显示在 CSS 顶部，编写的可用性数据则显示在脚本中。原生的 React 实现可以保留相同的状态转换，同时改用经过验证的 Anvil2 控件。

1. 运行完整的 HTML 示例。

2. 选择第二位技术人员，并确认开始时间选项随之变化。

3. 审核计划，然后更改时间并确认反馈已清除。

4. 切换外观，并确认当前选择仍然保留。

### 明确哪些内容需要真实数据

要求 AI Agent 保留相互依赖的时间列表、已禁用的员工、审核失效逻辑和外观行为。只有在具备真实数据契约后，才替换虚构的工单和员工。不要仅通过更改按钮标签，就把本地审核说成成功。

- 进行真实派单前，先检查可用性。

- 将语义覆盖与组件覆盖分开。

- 测试浅色和深色前景色/背景色配对。

## ServiceTitan Anvil 设计系统组件示例

### 完整的本地指派示例

原始的 HTML/CSS/JavaScript，采用基于令牌的外观和相互依赖的表单状态。

```
<!doctype html>
<html lang="en">
  <meta charset="utf-8" /><meta
    name="viewport"
    content="width=device-width,initial-scale=1"
  /><title>Dispatch assignment study</title
  ><style>
    :root {
      color-scheme: light;
      --surface: light-dark(#fff, #1a1a1a);
      --hover: light-dark(#f9f9f9, #292929);
      --text: light-dark(#040404, #fff);
      --muted: light-dark(#707070, #c4c4c4);
      --line: light-dark(#e6e6e6, #404040);
      --action: light-dark(#0265dc, #70b1ff);
      --on-action: light-dark(#fff, #1a1a1a);
      --faint: light-dark(#fafcff, #000b19);
      --success: light-dark(#077e50, #0bd085);
    }
    * {
      box-sizing: border-box;
    }
    body {
      margin: 0;
      padding: 24px;
      background: var(--surface);
      color: var(--text);
      font:
        16px/1.5 system-ui,
        sans-serif;
    }
    h1 {
      font-size: 28px;
      margin: 8px 0;
    }
    h2 {
      font-size: 20px;
      margin: 0 0 16px;
    }
    p {
      margin: 8px 0 16px;
    }
    .small {
      font-size: 13px;
      color: var(--muted);
    }
    header {
      display: flex;
      justify-content: space-between;
      gap: 24px;
      border-bottom: 1px solid var(--line);
      padding-bottom: 24px;
    }
    button,
    select {
      font: inherit;
      color: inherit;
      background: var(--surface);
      border: 1px solid var(--line);
      border-radius: 4px;
      padding: 10px 16px;
      min-height: 44px;
    }
    button {
      cursor: pointer;
    }
    button:hover {
      background: var(--hover);
    }
    button:focus-visible,
    select:focus-visible,
    input:focus-visible {
      outline: 3px solid var(--action);
      outline-offset: 3px;
    }
    main {
      display: grid;
      grid-template-columns: 1.4fr 1fr;
      gap: 24px;
      margin-top: 24px;
    }
    .board {
      border: 1px solid var(--line);
      border-radius: 8px;
      padding: 24px;
    }
    .job {
      padding: 16px;
      background: var(--faint);
      border-left: 4px solid var(--action);
      margin-bottom: 24px;
    }
    .job strong {
      display: block;
    }
    .people {
      display: grid;
      gap: 12px;
    }
    .person {
      display: grid;
      grid-template-columns: 24px 1fr auto;
      gap: 12px;
      padding: 16px;
      border: 1px solid var(--line);
      border-radius: 4px;
      align-items: center;
    }
    .person:has(:checked) {
      border-color: var(--action);
      background: var(--faint);
    }
    input {
      accent-color: var(--action);
      width: 18px;
      height: 18px;
    }
    .person small {
      display: block;
      color: var(--muted);
    }
    .person:has(:disabled) {
      background: var(--hover);
    }
    .timeline {
      display: flex;
      gap: 4px;
      margin: 20px 0;
    }
    .timeline span {
      height: 40px;
      flex: 1;
      background: var(--line);
      border-radius: 4px;
    }
    .timeline .open {
      background: var(--action);
    }
    .timeline-label {
      display: flex;
      justify-content: space-between;
      font-size: 13px;
      color: var(--muted);
    }
    label[for="time"] {
      display: block;
      margin: 16px 0 8px;
    }
    .primary {
      background: var(--action);
      color: var(--on-action);
      border-color: var(--action);
      margin-top: 24px;
    }
    .primary:hover {
      filter: brightness(0.95);
      background: var(--action);
    }
    #feedback {
      color: var(--success);
      padding-top: 16px;
    }
    footer {
      border-top: 1px solid var(--line);
      padding-top: 16px;
      margin-top: 24px;
      font-size: 13px;
      color: var(--muted);
    }
    @media (max-width: 767px) {
      body {
        padding: 16px;
      }
      main {
        grid-template-columns: 1fr;
      }
      .board {
        padding: 16px;
      }
      header {
        display: block;
      }
      header button {
        margin-top: 8px;
      }
      .person {
        grid-template-columns: 24px 1fr;
      }
      .person > span:last-child {
        grid-column: 2;
      }
    }
  </style>
  <body>
    <header>
      <div>
        <div class="small">SERVICE OPERATIONS / LOCAL PLANNING STUDY</div>
        <h1>Assign a service visit</h1>
        <p>Match a sample job with an available technician.</p>
      </div>
      <div>
        <button id="theme" aria-pressed="false">Dark appearance</button>
      </div>
    </header>
    <main>
      <section class="board">
        <h2>Job and technician</h2>
        <div class="job">
          <strong>Sample job #204 · Heating inspection</strong
          ><span>90 minutes · North district · Routine priority</span>
        </div>
        <div class="people" id="people"></div>
      </section>
      <aside class="board">
        <h2>Proposed visit</h2>
        <p id="technician"></p>
        <label for="time">Available start time</label
        ><select id="time"></select>
        <div class="timeline" aria-hidden="true">
          <span></span><span class="open"></span><span class="open"></span
          ><span></span><span></span><span></span>
        </div>
        <div class="timeline-label">
          <span>08:00</span><span>Illustrative day</span><span>17:00</span>
        </div>
        <p class="small">
          The time list is the source of available choices. The bars are a
          decorative schedule summary.
        </p>
        <button id="assign" class="primary">Review assignment</button>
        <p id="feedback" role="status"></p>
      </aside>
    </main>
    <footer>
      Original HTML using Anvil2 Hammer token 3.5.0 light/dark roles. Fictional
      technicians and schedules; no dispatch service connected.
    </footer>
    <script>
      const staff = [
        {
          id: "alex",
          name: "Alex Morgan",
          skill: "Heating specialist",
          slots: ["09:00", "11:00"],
        },
        {
          id: "sam",
          name: "Sam Rivera",
          skill: "General maintenance",
          slots: ["13:00", "15:00"],
        },
        { id: "lee", name: "Lee Chen", skill: "Unavailable today", slots: [] },
      ];
      let selected = "alex";
      const $ = (id) => document.getElementById(id);
      function update() {
        const p = staff.find((s) => s.id === selected);
        $("technician").textContent = p.name + " · " + p.skill;
        $("time").replaceChildren(...p.slots.map((t) => new Option(t, t)));
        $("feedback").textContent = "";
      }
      $("people").replaceChildren(
        ...staff.map((p) => {
          const l = document.createElement("label");
          l.className = "person";
          const input = document.createElement("input");
          input.type = "radio";
          input.name = "technician";
          input.checked = p.id === selected;
          input.disabled = p.slots.length === 0;
          input.onchange = () => {
            selected = p.id;
            update();
          };
          const text = document.createElement("span");
          text.innerHTML = `<strong>${p.name}</strong><small>${p.skill}</small>`;
          const availability = document.createElement("span");
          availability.textContent = p.slots.length
            ? p.slots.length + " start times"
            : "Unavailable";
          l.append(input, text, availability);
          return l;
        }),
      );
      $("time").onchange = () => {
        $("feedback").textContent = "";
      };
      $("assign").onclick = () => {
        $("feedback").textContent =
          "Local plan: " +
          staff.find((s) => s.id === selected).name +
          " at " +
          $("time").value +
          ". No visit has been dispatched.";
      };
      $("theme").onclick = () => {
        let dark = $("theme").getAttribute("aria-pressed") !== "true";
        $("theme").setAttribute("aria-pressed", dark);
        document.documentElement.style.colorScheme = dark ? "dark" : "light";
      };
      update();
    </script>
  </body>
</html>
```

代码参考

- 保存为 index.html；现代浏览器支持 light-dark()。这不是原生 Anvil2 运行环境。

### ThemeProvider 集成参考

完整 React 条目中记录的语义覆盖形式。请根据已安装的 Anvil2 版本进行验证。

```
import React from 'react';
import { createRoot } from 'react-dom/client';
import { ThemeProvider } from '@servicetitan/anvil2';

const theme = {
  semantic: {
    BackgroundColorPrimary: {
      value: '#7c3aed',
      extensions: { appearance: { dark: { value: '#a78bfa' } } },
    },
  },
};

function App() {
  return (
    <ThemeProvider theme={theme}>
      <main>
        <h1>Theme integration check</h1>
        <div style={{ background: 'var(--a2-background-color-primary)', padding: 24 }}>
          <span style={{ color: 'var(--a2-foreground-color-on-primary)' }}>
            Primary role with the provider’s matching foreground role
          </span>
        </div>
      </main>
    </ThemeProvider>
  );
}
createRoot(document.getElementById('root')).render(<App />);
```

代码参考

- 此 ThemeProvider 参考已对照官方文档核验；上方运行的示例是独立的 HTML。

## ServiceTitan Anvil 设计系统 AI Agent 设置

### 实现起点

运行的示例不需要 Anvil2 包。单独的 ThemeProvider 示例需要兼容的原生安装环境。

```
# Save the complete HTML example as index.html
python3 -m http.server 8080
# Open http://localhost:8080
```

### 适用于你项目的提示词

```
使用 Anvil2 语义角色构建服务指派界面。保留完整示例中的浅色/深色前景色配对、依赖技术人员选择的时间选项、已禁用的不可用员工以及审核失效逻辑。明确选择原始的 HTML，还是经过验证的 Anvil2 React 安装环境。没有后端响应时，绝不要声称已完成派单。
```

## ServiceTitan Anvil 设计系统来源与范围

ServiceTitan Anvil2 的 OpenDesign 指南。令牌值取自公开的 Hammer token 3.5.0。运行示例采用原始的 HTML，而非 Anvil2 React 运行环境；所有技术人员和可用性信息均为虚构。

- 主题 令牌层级、覆盖与外观值。

- 颜色 状态角色与交互配色指南。

- 间距 间距比例与实用工具集成。

- 尺寸 尺寸基础。

- 库架构 Anvil2 包与库的边界。

## ServiceTitan Anvil 设计系统相关资源

### Siemens iX

使用明确的严重程度词汇、适配主题的颜色角色和 iX Event List API 检查工业事件。本指南将构建本地设备记录簿，并清楚区分已审核记录与已解决的机器警报。

### Sprout Social Seeds

通过完整的编辑审核队列，检查 Seeds 实际公开的颜色、间距、产品字体排印和边框令牌；队列支持混合选择、筛选后的空状态和撤销操作。

### SumUp Circuit

一份 Circuit 指南，包含精确的语义令牌、原生校验属性、完整的商家商品编辑器和图示化的客户侧预览。


- [实现参考](https://anvil.servicetitan.com/docs/web/foundations/theming)
- [来源](https://anvil.servicetitan.com/docs/web/foundations/theming)
- [来源](https://anvil.servicetitan.com/docs/web/foundations/sizing)
- [代码参考](https://anvil.servicetitan.com/docs/web/foundations/theming)
- [主题 令牌层级、覆盖与外观值。](https://anvil.servicetitan.com/docs/web/foundations/theming)
- [颜色 状态角色与交互配色指南。](https://anvil.servicetitan.com/docs/web/foundations/colors)
- [间距 间距比例与实用工具集成。](https://anvil.servicetitan.com/docs/web/foundations/spacing)
- [尺寸 尺寸基础。](https://anvil.servicetitan.com/docs/web/foundations/sizing)
- [库架构 Anvil2 包与库的边界。](https://anvil.servicetitan.com/docs/library-architecture)
