サービス管理 · 新しいリソース

ServiceTitan Anvil デザインシステム.

3層のトークンモデル、検証済みのライト/ダークのロール値、技術者・時間・確認の各状態が同期する完全なサービス割り当て操作によって、Anvil2を具体化します。

OpenDesign ガイド7 章リファレンス項目 38 件オリジナルアプリケーションガイド
01

ServiceTitan Anvil デザインシステムのインターフェースプレビュー

ライトとダークの表示を切り替え、利用可能なスタッフを選択し、同期されたローカルプランを確認します。

サービス訪問に対応する技術者を割り当てる完全な例を開く
Hammer token 3.5.0 のロール値を使用した独自のHTML。・ 実装リファレンス
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 overridesBackgroundColorPrimary などのロールを変更します。
theme.componentcomponent token overridesButtonPrimaryBackgroundColor など、コンポーネントの役割に変更対象を絞ります。
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 デザインシステムの3層トークンアーキテクチャ

プリミティブはセマンティックロールの下位に保つ

Anvil2 では、生のプリミティブ、セマンティックロール、コンポーネント固有のエイリアスを分けています。通常、製品ではプリミティブの青色をすべてのコントロールにそのまま適用するのではなく、インターフェース上の意味を表すロールを選択します。参照表には正確なエクスポート値とダーク表示での対応値が記載されているため、テーマ変更をペアで確認できます。

この OpenDesign ガイドから

実行時にエイリアスが再計算されると考えない

公式のテーマ設定ガイドによると、プリミティブの参照はビルド時に解決されます。実行時プロバイダーでプリミティブを変更しても、セマンティック層が自動的に再生成されるわけではありません。アプリケーションの色を変更するには、該当するセマンティックロールをオーバーライドします。単一のコンポーネントを変更するには、コンポーネント層を使用します。これにより、関連するコントロール間で一貫性が崩れる部分的なテーマ変更を防げます。

ServiceTitan Anvil デザインシステムのライトとダークの表示

前景色と背景色の変更を組み合わせる

主アクションでは、BackgroundColorPrimaryとForegroundColorOnPrimaryを組み合わせます。ライト表示では青地に白文字、ダーク表示では明るい青地に暗い文字色になります。塗りの色だけを切り替えると、配色の組み合わせに一貫性がなくなります。HTMLの完全なサンプルでは、color-schemeの切り替えとlight-dark()を使ってこれらの値を示しています。ThemeProviderだと誤解されないようにしています。

ステータスの色相には、意味を明示する

Anvil2では、色の系統にステータスの意味を割り当てています。このローカルの確認画面では成功を示すロールを使いつつ、何が起きたかを正確に伝えています。利用できない技術者はテキストで示し、ラジオボタンを無効にしています。サービス訪問が手配されたと誤解させるための緑色の色味は使っていません。この例は、手配用のバックエンドには接続されません。

ServiceTitan Anvil デザインシステム:サービスの割り当てと状態

空き状況を次の選択肢に反映する

技術者を選択すると、開始時刻の選択肢がその人の架空の空き状況に応じて切り替わります。対応できない技術者も一覧には表示されますが、選択はできません。これは2つのコントロール間の依存関係です。最初の選択を変更したときは、別の人に紐づいた時刻が残らないよう、2つ目の選択を有効な値にリセットする必要があります。

計画と実行を分ける

割り当ての確認では計画をローカルに記録し、訪問は手配されていないことを明示します。本番のワークフローで確定するには、権限の確認、最新のスケジュールデータの取得、競合への対処が必要です。この例ではその境界を明確にしつつ、実際に操作して応用できる一連のやり取りを提供しています。

ServiceTitan Anvil デザインシステム:業務画面の情報密度と読みやすさ

選択肢の近くに作業の情報を置く

コンパクトな作業概要には、タスク、所要時間、エリア、優先度をまとめています。技術者の選択肢は読みやすい縦並びにし、予定する訪問は独立したパネルに表示します。ニュートラルな境界線と控えめなアクセントカラーを使い、選択箇所と主アクションを見分けやすくしています。スケジュールバーは参考用であることを明記し、実際の時刻はセレクトで選択します。

モバイルでも読み進める順序を保つ

ServiceTitan Anvil デザインシステム:ネイティブ連携と完全なHTMLの代替実装

ドキュメントに記載されたプロバイダーの形式を使う

Anvil2のThemeProviderは、valueとdarkの外部拡張に対応した、セマンティックトークンとコンポーネントトークンの上書きを受け付けます。ガイドには、公開ドキュメントを参照した完全なプロバイダーの例を掲載しています。これは、ローカルで実行した独立したHTMLのサンプルとは別の、ネイティブ連携の参考例です。

未検証のパッケージ実行環境について断定しない

このページでは、動作するHTMLの実装と、検証済みのトークンデータを紹介しています。ネイティブプロバイダーの例は独立した連携の参考情報であり、プレビューで実行されている環境ではありません。Reactのコンポーネントを導入する場合は、互換性のあるAnvil2のバージョンをインストールし、必要なスタイルを読み込んだうえで、ネイティブのHTMLコントロールを置き換える前にコンポーネントAPIを確認してください。

ServiceTitan Anvil デザインシステム:アクセシビリティと状態の検証

フォームにはネイティブのセマンティクスを使う

技術者の選択にはラベル付きのネイティブなラジオボタンを使い、開始時刻にはラベル付きのセレクトを使っています。表示モードのコントロールはaria-pressedを公開し、確認メッセージはステータス領域に表示します。ラジオボタンの選択にはブラウザー標準のキーボード操作を使います。フォーカスの視覚的なスタイルは、選択状態やホバー状態とは独立しています。

確認後の変更もテストする

技術者または時刻を変更すると、以前の確認フィードバックは消去されます。表示モードを切り替えても、視覚テーマの変更で計画が失われないよう、選択中の技術者と時刻は維持されます。ボタンをクリックできることだけでなく、両方の変更操作、選択不可の状態、狭い画面でのはみ出しもテストしてください。

ServiceTitan Anvil デザインシステム:AIエージェントへの引き継ぎと実装

まず状態モデル全体を確認する

HTMLの完全な例を保存して実行し、動作を確認してください。トークンから導出されたライト/ダークの配色はCSSの上部に、定義された空き状況データはスクリプト内にあります。ネイティブのReact実装でも、検証済みのAnvil2コントロールに置き換えながら、同じ状態遷移を維持できます。

  1. HTMLの完全なサンプルを実行する。
  2. 2人目の技術者を選び、開始時刻の選択肢が切り替わることを確認する。
  3. 計画を確認した後、時刻を変更し、フィードバックが消えることを確認する。
  4. 表示モードを切り替え、現在の選択が維持されることを確認する。

実データが必要な箇所を明示する

依存関係のある時刻リスト、選択できないスタッフ、確認内容の無効化、表示モードの動作を維持するよう、AIエージェントに指示してください。実際のデータ仕様が用意できた場合にのみ、架空の作業やスタッフを置き換えます。ボタンのラベルを変更しただけで、ローカルの確認を成功したかのように見せないでください。

  • 実際に手配する前に、空き状況を確認する。
  • セマンティックトークンとコンポーネントトークンの上書きを分ける。
  • ライトとダークの両方で、文字色と背景色の組み合わせをテストする。

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 />);

コードリファレンス

  • このプロバイダーの参考例は公式ドキュメントに照らして確認済みです。上で動作するサンプルは、これとは独立したHTMLです。
04

ServiceTitan Anvil デザインシステム:AIエージェントのセットアップ

実装の出発点

実行済みのサンプルにAnvil2のパッケージは不要です。別途掲載しているThemeProviderの例には、互換性のあるネイティブ環境のインストールが必要です。

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

あなたのプロジェクト向けプロンプト

AI エージェント用プロンプト
Anvil2のセマンティックな役割を使って、サービス割り当てインターフェースを構築します。完全なサンプルにある、ライト/ダークの前景色の組み合わせ、技術者に応じた時間の選択、対応できないスタッフの無効化、変更時の確認状態の無効化を維持してください。独自のHTMLを使用するか、検証済みのAnvil2 Reactを導入するかを明確に選択してください。バックエンドからの応答がない限り、派遣が確定したと表示してはいけません。
05

ServiceTitan Anvilデザインシステムのソースと適用範囲

ServiceTitan Anvil2のOpenDesignガイドです。トークン値は公開されているHammer token 3.5.0から抽出しています。動作するサンプルは元のHTMLであり、Anvil2のReactランタイムではありません。技術者と対応状況はすべて架空です。