서비스 관리 · 새 리소스

ServiceTitan Anvil 디자인 시스템.

3단계 토큰 모델, 검증된 라이트/다크 역할 값, 기술자·시간·검토 상태가 동기화되는 완전한 서비스 배정 상호작용으로 Anvil2를 구체화합니다.

OpenDesign 가이드7개 챕터참조 항목 38개독자적 적용 가이드
01

ServiceTitan Anvil 디자인 시스템 인터페이스 미리보기

밝은 모드와 어두운 모드를 전환하고, 사용 가능한 담당자를 선택해 동기화된 로컬 계획을 검토하세요.

서비스 방문에 적합한 기술자를 배정하세요전체 예시 열기
Hammer 토큰 3.5.0의 역할 값을 사용한 원본 HTML. · 구현 레퍼런스
02

ServiceTitan Anvil 디자인 시스템 디자인 참조

@servicetitan/hammer-token 3.5.0의 core 테마 내보내기와 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 예제에서는 ThemeProvider라고 주장하지 않으면서 color-scheme 전환과 함께 light-dark()를 사용해 이 값을 보여 줍니다.

의미를 문구로 명시한 상태 색상을 사용하세요

Anvil2는 색상 계열에 상태의 의미를 부여합니다. 로컬 검토에서는 성공 역할을 사용하지만, 어떤 일이 있었는지 문구로 정확히 설명합니다. 담당할 수 없는 기술자는 텍스트로 표시하고 라디오 버튼을 비활성화합니다. 서비스 방문이 배정되었다는 의미로 초록색을 사용하지 않습니다. 이 예제는 배정 백엔드에 연결되지 않습니다.

ServiceTitan Anvil 디자인 시스템의 서비스 배정 및 상태

가능 여부에 따라 다음 선택지를 제한하세요

기술자를 선택하면 시작 시간 선택지가 해당 기술자의 가상 일정으로 바뀝니다. 담당할 수 없는 기술자도 계속 표시되지만 선택할 수는 없습니다. 두 컨트롤이 서로 종속되어 있으므로 첫 번째 선택을 변경하면 두 번째 선택도 유효한 값으로 초기화해야 합니다. 다른 사람에게 해당하는 시간을 그대로 유지해서는 안 됩니다.

계획과 실행을 구분하세요

배정 검토는 로컬 계획을 기록하고 방문이 배정되지 않았음을 명시적으로 안내합니다. 실제 운영 워크플로에서는 확정 전에 권한 확인, 최신 일정 조회, 충돌 처리가 필요합니다. 이 예제는 그 경계를 분명히 유지하면서 직접 시험하고 적용할 수 있는 완전한 상호작용을 제공합니다.

ServiceTitan Anvil 디자인 시스템의 정보 밀도 및 가독성

작업 맥락을 선택 항목 가까이에 두세요

간결한 작업 요약에는 작업, 소요 시간, 구역, 우선순위가 포함됩니다. 기술자 선택지는 읽기 쉬운 세로 목록으로 표시하고, 예정 방문은 별도 패널에 배치합니다. 중립적인 경계선과 절제된 강조색을 사용해 선택 항목과 주요 동작을 쉽게 식별할 수 있습니다. 일정 막대에는 예시용이라고 표시하며, 실제 시간은 선택 메뉴를 기준으로 합니다.

모바일에서도 읽기 순서를 유지하세요

ServiceTitan Anvil 디자인 시스템의 네이티브 통합 및 전체 HTML 대안

문서에 명시된 제공자 구조를 사용하세요

Anvil2의 ThemeProvider는 value 및 다크 모양 확장 기능과 함께 의미별 토큰 및 컴포넌트 토큰 재정의를 지원합니다. 아래 가이드에는 공개 문서를 출처로 한 완전한 제공자 예제가 포함되어 있습니다. 이는 로컬에서 실행한 독립적인 HTML 예제와 구별되는 네이티브 통합 참고 자료입니다.

검증되지 않은 패키지 런타임을 사용한다고 주장하지 마세요

이 페이지에서는 작동하는 HTML 구현과 검증된 토큰 데이터를 제공합니다. 네이티브 제공자 예제는 별도의 통합 참고 자료이며 미리보기에 표시된 런타임이 아닙니다. React 컴포넌트를 도입할 때는 서로 호환되는 Anvil2 버전을 설치하고 필요한 스타일을 포함한 뒤, 네이티브 HTML 컨트롤을 교체하기 전에 컴포넌트 API를 검증하세요.

ServiceTitan Anvil 디자인 시스템의 접근성 및 상태 검증

네이티브 폼 의미 구조를 사용하세요

기술자 선택에는 라벨이 연결된 네이티브 라디오 버튼을, 시작 시간에는 라벨이 연결된 선택 메뉴를 사용하며, 모양 전환 컨트롤에는 aria-pressed를 제공합니다. 검토 메시지는 상태 영역입니다. 라디오 버튼 선택은 브라우저의 기본 키보드 동작으로 처리하며, 표시되는 포커스 스타일은 선택 또는 호버 스타일과 별도로 유지됩니다.

확인 후 변경 사항을 점검하세요

기술자나 시간을 변경하면 이전 검토 피드백을 지웁니다. 모양을 전환해도 선택한 기술자와 시간은 유지합니다. 시각적 테마 변경이 계획을 지워서는 안 되기 때문입니다. 버튼을 클릭할 수 있는지만 확인하지 말고 두 가지 전환과 비활성화된 선택, 좁은 화면에서의 오버플로도 테스트해야 합니다.

ServiceTitan Anvil 디자인 시스템의 AI 에이전트 핸드오프 및 구현

전체 상태 모델부터 살펴보세요

전체 HTML 예제를 저장하고 실행해 동작을 확인하세요. 토큰에서 가져온 라이트/다크 색상 쌍은 CSS 상단에 표시되며, 직접 작성한 가능 시간 데이터는 스크립트에서 확인할 수 있습니다. 네이티브 React 구현에서도 검증된 Anvil2 컨트롤로 바꾸면서 동일한 상태 전환을 유지할 수 있습니다.

  1. 전체 HTML 예제를 실행하세요.
  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 디자인 시스템 에이전트 설정

구현 시작점

실행된 예시에는 Anvil2 패키지가 필요하지 않습니다. 별도의 ThemeProvider 예시에는 호환되는 네이티브 설치가 필요합니다.

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

프로젝트를 위한 프롬프트

에이전트 프롬프트
Anvil2 시맨틱 역할을 사용해 서비스 배정 인터페이스를 만드세요. 완성된 예시의 라이트/다크 전경색 조합, 기술자에 따라 달라지는 시간 선택지, 예약할 수 없는 직원의 비활성화, 검토 상태 초기화를 유지하세요. 기본 HTML과 검증된 Anvil2 React 설치 중 하나를 명시적으로 선택하세요. 백엔드 응답 없이 배정이 완료됐다고 표시하지 마세요.
05

ServiceTitan Anvil 디자인 시스템 출처 및 범위

ServiceTitan Anvil2에 관한 OpenDesign 가이드입니다. 토큰 값은 공개된 Hammer 토큰 3.5.0에서 추출했습니다. 실행 가능한 예시는 Anvil2 React 런타임이 아니라 기본 HTML이며, 모든 기술자와 예약 가능 일정은 가상입니다.