# @servicetitan/anvil2-ai-kit

> [!NOTE]
> This package was migrated from the [`servicetitan/carto`](https://github.com/servicetitan/carto) repository in [#2589](https://github.com/servicetitan/hammer/pull/2589).

Carto React component kit. Ships as ESM with type declarations.

## Install

```sh
pnpm add @servicetitan/anvil2-ai-kit
```

Peer requirements: `react@>=18 <20`, `react-dom@>=18 <20`.

## Usage

```tsx
import { Button, CartoTheme } from "@servicetitan/anvil2-ai-kit";
import { IconPencil } from "@servicetitan/anvil2-icons";
import "@servicetitan/anvil2-ai-kit/styles.css";

<CartoTheme>
  <Button label="Save" onPress={handleSave} />
  <Button label="Edit" icon={<IconPencil />} onPress={handleEdit} />
</CartoTheme>;
```

Always import the CSS bundle once at your app's entry point so vanilla-extract styles are loaded. Icon sizing styles load with the `@servicetitan/anvil2-icons` import itself, so no separate icon stylesheet import is needed.

Icons ship from `@servicetitan/anvil2-icons` as individual components — `IconPencil`, `IconChevronDown`, etc. Each accepts the same locked prop surface (`size`, `color`, `label`, `className`, `ref`). The allowlist (Lucide-backed and custom SVGs alike) is owned by VizD.

## Fonts

Carto components render in **Inter** (Google Fonts) and **Sofia Pro Bold** (self-hosted, bundled from `@servicetitan/anvil-fonts`). For the fastest first paint on Inter, add the following `<link>` tags to your app shell `<head>` so the fetch starts during HTML parse, before any Carto JS runs:

```html
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
  rel="stylesheet"
  href="https://fonts.googleapis.com/css?family=Inter:400,400i,500,500i,600,600i,700,700i&display=optional"
/>
```

Sofia Pro Bold has no equivalent host-side tag — it's bundled into the kit at build time rather than loaded from a CDN, since Carto can run inside an MFE outside the monolith host and can't assume the font is already available there. The kit injects both Inter's stylesheet link and Sofia Pro's `@font-face` at module load as a runtime safety net, so components don't render in fallback indefinitely if you skip the Inter shell setup — but the shell pattern is still preferable for Inter: it arrives before paint instead of after. See `.storybook/preview-head.html` for the canonical Inter tags.

## Source layout

Component code lives in `src/components/<Component>/`. Each component is a sibling group: `<Component>.tsx` (render only), `<Component>.css.ts` (vanilla-extract recipe), `types.ts` (props + variant unions), `<Component>.test.tsx`, and `stories/<Component>.stories.tsx`. Icons live in `@servicetitan/anvil2-icons` (shared with RN); see that package's README for the custom-icon workflow.

Common tasks (build, test, typecheck, typedoc, storybook) run from the repo root via `pnpm build:anvil2-ai-kit`, `pnpm test:anvil2-ai-kit`, etc. See the root `package.json` for the full list.

## Local testing

Vitest is split into two projects (see `vitest.config.ts`):

- **`unit`** — jsdom, runs every `*.test.tsx`. This is what `pnpm test` and CI's `Test` job run.
- **`storybook`** — real Chromium via `@vitest/browser-playwright`, runs every `*.stories.tsx` through `@storybook/addon-vitest`. This is what the **Run tests** button in the Storybook UI and `pnpm test-storybook` use locally. CI does not run this project — Chromatic catches play-function regressions in the cloud.

`chromatic-ai-kit` publishes to the Hammer Chromatic project via `CHROMATIC_PROJECT_ANVIL2_AI_KIT` (TurboSnap on, same `onlyChanged` policy as the other jobs).

The storybook project needs Playwright's Chromium binary installed locally — it lives outside `node_modules` at `~/Library/Caches/ms-playwright/` (macOS) or `~/.cache/ms-playwright/` (Linux), ~150 MB. Run once per machine:

```sh
pnpm --filter @servicetitan/anvil2-ai-kit exec playwright install chromium
```

You only need this if you (a) click **Run tests** in the Storybook UI or (b) run `pnpm test-storybook` locally. Browsing stories with `pnpm storybook` and running unit tests with `pnpm test` do **not** need it.

## Storybook

```sh
pnpm storybook:anvil2-ai-kit
```

Stories render in light DOM by default. The Storybook toolbar has a **Shadow** toggle that wraps the story in an open shadow root (`data-mfe-name="test-mfe"`), mirroring how Carto components run inside MFE shadow roots in the host monolith. Switch to Shadow mode when you specifically want to validate CSS isolation against monolith styles — a red dotted outline flags the special mode.

Shadow mode clones `document.head` styles into the shadow root, surfacing collisions where monolith global selectors match Carto markup. It is **not** a fidelity environment for hover/focus testing — non-bubbling DOM events (`pointerenter`, `mouseenter`) can't cross shadow boundaries and React 18's polyfill can't bridge them across the outer-Storybook / inner-portal split. See `src/internal/components/Shadow/Shadow.tsx` for details; develop hover-driven components in Fragment mode.

### Monolith CSS snapshot

`preview-head.html` loads real monolith stylesheets so the Storybook page environment matches what an MFE inherits from the host. A snapshot lives at `public/monolith-css/`. To refresh it:

```sh
pnpm refresh-monolith-css:anvil2-ai-kit
```

The script sources the bundles two different ways:

- **app-css, sui, kendo-css, main** — pulled from the public ServiceTitan CDN (`https://go.servicetitan.com/scripts/build/`). Hashes are resolved on the fly via the `desktop` and `desktop-legacy` webpack manifests, so refreshes pick up the current monolith build automatically.
- **anvil2, design-system** — built locally from the `@servicetitan/anvil2` and `@servicetitan/design-system` npm devDeps. anvil2 is concatenated from `dist/*.css` (excluding bundles the package vendors from old monolith builds); design-system uses `dist/system.css` directly. The `refresh-monolith-css` script always runs `pnpm update @servicetitan/anvil2` first so anvil2 is on the latest published version before the snapshot is rebuilt. design-system is frozen so it stays where it's pinned.

No monolith checkout is required.
