服务管理 · 新建资源

ServiceTitan Anvil 设计系统.

通过三级令牌模型、经过验证的明暗主题角色值,以及完整的服务分配交互,让 Anvil2 更具体:技师、时间和审核状态保持同步。

OpenDesign 指南7 个章节38 条参考条目原始应用指南
01

ServiceTitan Anvil 设计系统界面预览

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

为服务上门匹配技术人员打开完整示例
原创 HTML,采用 Hammer token 3.5.0 角色值。· 实现参考
02

ServiceTitan Anvil 设计系统设计参考

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

语义化浅色与深色配对值

来源
名称 / 规则值 / 配置用途
BackgroundColorDefaultlight #ffffff / dark #1a1a1a主要应用界面表面。
BackgroundColorDefaultHoverlight #f9f9f9 / dark #292929悬停界面表面。
BackgroundColorPrimarylight #0265dc / dark #70b1ff主操作填充。
BackgroundColorPrimaryHoverlight #1d4ca3 / dark #9ecaff原生主色悬停角色。
BackgroundColorPrimaryFaintlight #fafcff / dark #000b19克制的选中界面表面。
ForegroundColorSubduedlight #707070 / dark #c4c4c4辅助性文本。
ForegroundColorOnPrimarylight #ffffff / dark #1a1a1a与主色填充配对的文本。
ForegroundColorPrimarylight #0265dc / dark #70b1ff交互前景色。
BorderColorSubduedlight #e6e6e6 / dark #404040低强调度边界。
BorderColorPrimarylight #0265dc / dark #9ecaff交互边界。
StatusColorInfolight #0265dc / dark #70b1ff信息状态。
StatusColorDangerlight #d62100 / dark #ff5b3d需要立即关注。
StatusColorSuccesslight #077e50 / dark #0bd085本地审核已完成。
StatusColorWarninglight #ffbe00 / dark #ffe278非关键警示。

基础几何值与响应式数值

来源
名称 / 规则值 / 配置用途
BreakpointSm640px已发布的基础值;实际应用角色由语义别名和组件别名决定。
BreakpointMd768px已发布的基础值;实际应用角色由语义别名和组件别名决定。
BreakpointLg1024px已发布的基础值;实际应用角色由语义别名和组件别名决定。
BreakpointXl1280px已发布的基础值;实际应用角色由语义别名和组件别名决定。
BreakpointXxl1536px已发布的基础值;实际应用角色由语义别名和组件别名决定。
DurationDefault200ms已发布的基础值;实际应用角色由语义别名和组件别名决定。
DurationInstant0ms已发布的基础值;实际应用角色由语义别名和组件别名决定。
DurationFast100ms已发布的基础值;实际应用角色由语义别名和组件别名决定。
DurationSlow300ms已发布的基础值;实际应用角色由语义别名和组件别名决定。
Radius00rem已发布的基础值;实际应用角色由语义别名和组件别名决定。
Radius10.1875rem已发布的基础值;实际应用角色由语义别名和组件别名决定。
Radius20.375rem已发布的基础值;实际应用角色由语义别名和组件别名决定。
Radius30.5625rem已发布的基础值;实际应用角色由语义别名和组件别名决定。
Radius40.75rem已发布的基础值;实际应用角色由语义别名和组件别名决定。
Radius50.9375rem已发布的基础值;实际应用角色由语义别名和组件别名决定。
Radius61.125rem已发布的基础值;实际应用角色由语义别名和组件别名决定。
Radius71.3125rem已发布的基础值;实际应用角色由语义别名和组件别名决定。
Radius81.5rem已发布的基础值;实际应用角色由语义别名和组件别名决定。

ThemeProvider 覆盖契约

来源
名称 / 规则值 / 配置用途
theme.semanticsemantic token overrides更改某个角色,例如 BackgroundColorPrimary。
theme.componentcomponent token overrides将更改范围缩小到某个组件角色,例如 ButtonPrimaryBackgroundColor。
valuedefault/light token value提供浅色外观值。
extensions.appearance.dark.valuedark appearance value指定对应的深色外观值。
CSS output--a2-<kebab-case-name>ThemeProvider 会将设计令牌键序列化为 CSS 变量。
Primitive cascaderesolved at build time在运行时覆盖基础值不会重新计算语义别名。
03

ServiceTitan Anvil 设计系统完整指南

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

DESIGN-servicetitan-anvil.md

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

下载指南

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

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

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

摘自本 OpenDesign 指南

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

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

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

成对调整前景色和背景色

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

为状态色调赋予明确含义

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

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

让可用性决定下一步选择

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

将计划与执行分开

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

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

让任务信息靠近选择项

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

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

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,采用基于令牌的外观和相互依赖的表单状态。

html
<!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 版本进行验证。

jsx
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。
04

ServiceTitan Anvil 设计系统 AI Agent 设置

实现起点

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

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

适用于你项目的提示词

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

ServiceTitan Anvil 设计系统来源与范围

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