# File Diff

An inline diff card showing the additions and deletions the agent proposes.

- Category: Tool & Action States
- Source: AICSS (https://www.aicss.dev/components/file-diff)
- Author: @kvnkld (https://x.com/kvnkld)
- Styling: Self-contained CSS with CSS custom properties (design tokens). Theme-aware via [data-theme] (light/dark).

## Instructions

Add this component to the project. Keep the styling self-contained. Map the design tokens (CSS custom properties) to the project's theme if they are not already defined.

## Code

### React - `FileDiff.tsx`

```tsx
import styles from "./FileDiff.module.css";

const ROWS = [
  { old: 12, cur: 12, type: "ctx", text: "export function getToken() {" },
  { old: 13, cur: null, type: "del", text: "  return localStorage.token;" },
  { old: null, cur: 13, type: "add", text: '  const t = cookies.get("session");' },
  { old: null, cur: 14, type: "add", text: '  if (!t) throw new Error("no session");' },
  { old: null, cur: 15, type: "add", text: "  return t;" },
  { old: 14, cur: 16, type: "ctx", text: "}" },
];

export function FileDiff({ file = "src/auth.ts", rows = ROWS }) {
  const added = rows.filter((r) => r.type === "add").length;
  const removed = rows.filter((r) => r.type === "del").length;
  return (
    <div className={styles.diff}>
      <div className={styles.diffHead}>
        <span className={styles.diffFileWrap}>
          <svg className={styles.diffIcon} viewBox="0 0 24 24" width="15" height="15" aria-hidden="true">
            <path d="M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
          <span className={styles.diffFile}>{file}</span>
        </span>
        <span className={styles.diffStat}>
          <span className={styles.add}>+{added}</span>
          <span className={styles.del}>-{removed}</span>
        </span>
      </div>
      <div className={styles.diffBody}>
        {rows.map((r, i) => (
          <div key={i} className={styles.diffRow + " " + styles[r.type]}>
            <span className={styles.ln + " " + styles.old}>{r.old ?? ""}</span>
            <span className={styles.ln + " " + styles.new}>{r.cur ?? ""}</span>
            <span className={styles.sign}>
              {r.type === "add" ? "+" : r.type === "del" ? "-" : ""}
            </span>
            <code>{r.text}</code>
          </div>
        ))}
      </div>
    </div>
  );
}
```

### React - `FileDiff.module.css`

```css
.diff { border-radius: 12px; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.05), 0 2px 4px rgba(0,0,0,0.02), 0 0 0 0.5px rgba(0,0,0,0.08); padding: 12px 16px 16px; overflow: hidden; font-family: ui-monospace, "SF Mono", Menlo, monospace; }
.diffHead {
  display: flex; align-items: center; gap: 8px;
  margin: -12px -16px 0; padding: 10px 12px 10px 16px;
  background: transparent; border-bottom: 0.5px solid #e6e8ec; font-size: 12.5px;
}
.diffFileWrap { display: inline-flex; align-items: center; gap: 7px; }
.diffIcon { display: block; width: 15px; height: 15px; color: #a1a1a1; flex: none; }
.diffFile { color: #1a1a1a; line-height: 1; }
.diffStat { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; font-size: 12px; line-height: 1; }
.diffStat .add { color: #15a06a; }
.diffStat .del { color: #dc2626; }
.diffBody {
  position: relative;
  margin: 0 -16px -16px; overflow: hidden;
  padding: 4px 0; font-size: 12.5px; line-height: 20px;
}
/* full-height gutter divider at the line-number edge (32px + 32px), unaffected
   by the body padding; z-index keeps it above the tinted add/del rows */
.diffBody::before { content: ""; position: absolute; top: 0; bottom: 0; left: 64px; width: 0.5px; background: #e6e8eb; z-index: 1; }
.diffRow {
  position: relative;
  display: grid; grid-template-columns: 32px 32px 18px 1fr; align-items: stretch;
}
.diffRow .ln, .diffRow .sign { user-select: none; color: #a1a1a1; font-size: 11px; }
.diffRow .ln { text-align: right; padding: 0 7px; }
.diffRow .sign { text-align: center; }
.diffRow code { white-space: pre; padding: 0 12px 0 8px; color: #a1a1a1; }
/* left accent bar: solid green for additions, red hatch for deletions */
.diffRow.add::before, .diffRow.del::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 3px;
}
.diffRow.add::before { background: #15a06a; }
.diffRow.del::before {
  background: repeating-linear-gradient(45deg, #dc2626 0, #dc2626 1.5px, transparent 1.5px, transparent 3px);
}
.diffRow.add { background: rgba(26, 127, 55, 0.09); }
.diffRow.add .sign, .diffRow.add .new { color: #15a06a; }
.diffRow.add code { color: #1a1a1a; }
.diffRow.del { background: rgba(207, 34, 46, 0.09); }
.diffRow.del .sign, .diffRow.del .old { color: #dc2626; }
.diffRow.del code { color: #1a1a1a; }
@media (prefers-color-scheme: dark) {
  .diff { background: #1a1a1a; box-shadow: 0 1px 2px rgba(0,0,0,0.4), 0 2px 4px rgba(0,0,0,0.3), 0 0 0 0.5px rgba(255,255,255,0.12); }
  .diffHead { border-bottom-color: #303030; }
  .diffFile { color: #f5f5f5; }
  .diffStat .add { color: #34d399; }
  .diffStat .del { color: #f87171; }
  .diffBody::before { background: #303030; }
  .diffRow.add { background: rgba(63, 185, 80, 0.15); }
  .diffRow.add::before { background: #34d399; }
  .diffRow.add .sign, .diffRow.add .new { color: #34d399; }
  .diffRow.add code { color: #f5f5f5; }
  .diffRow.del { background: rgba(248, 81, 73, 0.15); }
  .diffRow.del::before { background: repeating-linear-gradient(45deg, #f87171 0, #f87171 1.5px, transparent 1.5px, transparent 3px); }
  .diffRow.del .sign, .diffRow.del .old { color: #f87171; }
  .diffRow.del code { color: #f5f5f5; }
}
```

### Vue - `FileDiff.vue`

```vue
<template>
  <div class="diff">
    <div class="diff-head">
      <span class="diff-file-wrap">
        <svg class="diff-icon" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true">
          <path d="M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
        </svg>
        <span class="diff-file">{{ file }}</span>
      </span>
      <span class="diff-stat">
        <span class="add">+{{ added }}</span>
        <span class="del">-{{ removed }}</span>
      </span>
    </div>
    <div class="diff-body">
      <div v-for="(r, i) in rows" :key="i" :class="['diff-row', r.type]">
        <span class="ln old">{{ r.old ?? "" }}</span>
        <span class="ln new">{{ r.cur ?? "" }}</span>
        <span class="sign">{{ r.type === "add" ? "+" : r.type === "del" ? "-" : "" }}</span>
        <code>{{ r.text }}</code>
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed } from "vue";

const ROWS = [
  { old: 12, cur: 12, type: "ctx", text: "export function getToken() {" },
  { old: 13, cur: null, type: "del", text: "  return localStorage.token;" },
  { old: null, cur: 13, type: "add", text: '  const t = cookies.get("session");' },
  { old: null, cur: 14, type: "add", text: '  if (!t) throw new Error("no session");' },
  { old: null, cur: 15, type: "add", text: "  return t;" },
  { old: 14, cur: 16, type: "ctx", text: "}" },
];

const props = defineProps({
  file: { type: String, default: "src/auth.ts" },
  rows: { type: Array, default: () => ROWS },
});
const added = computed(() => props.rows.filter((r) => r.type === "add").length);
const removed = computed(() => props.rows.filter((r) => r.type === "del").length);
</script>

<style scoped>
.diff { border-radius: 12px; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.05), 0 2px 4px rgba(0,0,0,0.02), 0 0 0 0.5px rgba(0,0,0,0.08); padding: 12px 16px 16px; overflow: hidden; font-family: ui-monospace, "SF Mono", Menlo, monospace; }
.diff-head {
  display: flex; align-items: center; gap: 8px;
  margin: -12px -16px 0; padding: 10px 12px 10px 16px;
  background: transparent; border-bottom: 0.5px solid #e6e8ec; font-size: 12.5px;
}
.diff-file-wrap { display: inline-flex; align-items: center; gap: 7px; }
.diff-icon { display: block; width: 15px; height: 15px; color: #a1a1a1; flex: none; }
.diff-file { color: #1a1a1a; line-height: 1; }
.diff-stat { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; font-size: 12px; line-height: 1; }
.diff-stat .add { color: #15a06a; }
.diff-stat .del { color: #dc2626; }
.diff-body {
  position: relative;
  margin: 0 -16px -16px; overflow: hidden;
  padding: 4px 0; font-size: 12.5px; line-height: 20px;
}
/* full-height gutter divider at the line-number edge (32px + 32px), unaffected
   by the body padding; z-index keeps it above the tinted add/del rows */
.diff-body::before { content: ""; position: absolute; top: 0; bottom: 0; left: 64px; width: 0.5px; background: #e6e8eb; z-index: 1; }
.diff-row {
  position: relative;
  display: grid; grid-template-columns: 32px 32px 18px 1fr; align-items: stretch;
}
.diff-row .ln, .diff-row .sign { user-select: none; color: #a1a1a1; font-size: 11px; }
.diff-row .ln { text-align: right; padding: 0 7px; }
.diff-row .sign { text-align: center; }
.diff-row code { white-space: pre; padding: 0 12px 0 8px; color: #a1a1a1; }
/* left accent bar: solid green for additions, red hatch for deletions */
.diff-row.add::before, .diff-row.del::before {
  content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 3px;
}
.diff-row.add::before { background: #15a06a; }
.diff-row.del::before {
  background: repeating-linear-gradient(45deg, #dc2626 0, #dc2626 1.5px, transparent 1.5px, transparent 3px);
}
.diff-row.add { background: rgba(26, 127, 55, 0.09); }
.diff-row.add .sign, .diff-row.add .new { color: #15a06a; }
.diff-row.add code { color: #1a1a1a; }
.diff-row.del { background: rgba(207, 34, 46, 0.09); }
.diff-row.del .sign, .diff-row.del .old { color: #dc2626; }
.diff-row.del code { color: #1a1a1a; }
@media (prefers-color-scheme: dark) {
  .diff { background: #1a1a1a; box-shadow: 0 1px 2px rgba(0,0,0,0.4), 0 2px 4px rgba(0,0,0,0.3), 0 0 0 0.5px rgba(255,255,255,0.12); }
  .diff-head { border-bottom-color: #303030; }
  .diff-file { color: #f5f5f5; }
  .diff-stat .add { color: #34d399; }
  .diff-stat .del { color: #f87171; }
  .diff-body::before { background: #303030; }
  .diff-row.add { background: rgba(63, 185, 80, 0.15); }
  .diff-row.add::before { background: #34d399; }
  .diff-row.add .sign, .diff-row.add .new { color: #34d399; }
  .diff-row.add code { color: #f5f5f5; }
  .diff-row.del { background: rgba(248, 81, 73, 0.15); }
  .diff-row.del::before { background: repeating-linear-gradient(45deg, #f87171 0, #f87171 1.5px, transparent 1.5px, transparent 3px); }
  .diff-row.del .sign, .diff-row.del .old { color: #f87171; }
  .diff-row.del code { color: #f5f5f5; }
}
</style>
```

### Svelte - `FileDiff.svelte`

```svelte
<script>
  const ROWS = [
    { old: 12, cur: 12, type: "ctx", text: "export function getToken() {" },
    { old: 13, cur: null, type: "del", text: "  return localStorage.token;" },
    { old: null, cur: 13, type: "add", text: '  const t = cookies.get("session");' },
    { old: null, cur: 14, type: "add", text: '  if (!t) throw new Error("no session");' },
    { old: null, cur: 15, type: "add", text: "  return t;" },
    { old: 14, cur: 16, type: "ctx", text: "}" },
  ];
  export let file = "src/auth.ts";
  export let rows = ROWS;
  $: added = rows.filter((r) => r.type === "add").length;
  $: removed = rows.filter((r) => r.type === "del").length;
</script>

<div class="diff">
  <div class="diff-head">
    <span class="diff-file-wrap">
      <svg class="diff-icon" viewBox="0 0 24 24" width="15" height="15" aria-hidden="true">
        <path d="M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
      </svg>
      <span class="diff-file">{file}</span>
    </span>
    <span class="diff-stat">
      <span class="add">+{added}</span>
      <span class="del">-{removed}</span>
    </span>
  </div>
  <div class="diff-body">
    {#each rows as r, i (i)}
      <div class="diff-row {r.type}">
        <span class="ln old">{r.old ?? ""}</span>
        <span class="ln new">{r.cur ?? ""}</span>
        <span class="sign">{r.type === "add" ? "+" : r.type === "del" ? "-" : ""}</span>
        <code>{r.text}</code>
      </div>
    {/each}
  </div>
</div>

<style>
  .diff { border-radius: 12px; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.05), 0 2px 4px rgba(0,0,0,0.02), 0 0 0 0.5px rgba(0,0,0,0.08); padding: 12px 16px 16px; overflow: hidden; font-family: ui-monospace, "SF Mono", Menlo, monospace; }
  .diff-head {
    display: flex; align-items: center; gap: 8px;
    margin: -12px -16px 0; padding: 10px 12px 10px 16px;
    background: transparent; border-bottom: 0.5px solid #e6e8ec; font-size: 12.5px;
  }
  .diff-file-wrap { display: inline-flex; align-items: center; gap: 7px; }
  .diff-icon { display: block; width: 15px; height: 15px; color: #a1a1a1; flex: none; }
  .diff-file { color: #1a1a1a; line-height: 1; }
  .diff-stat { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; font-size: 12px; line-height: 1; }
  .diff-stat .add { color: #15a06a; }
  .diff-stat .del { color: #dc2626; }
  .diff-body {
    position: relative;
    margin: 0 -16px -16px; overflow: hidden;
    padding: 4px 0; font-size: 12.5px; line-height: 20px;
  }
  /* full-height gutter divider at the line-number edge (32px + 32px), unaffected
     by the body padding; z-index keeps it above the tinted add/del rows */
  .diff-body::before { content: ""; position: absolute; top: 0; bottom: 0; left: 64px; width: 0.5px; background: #e6e8eb; z-index: 1; }
  .diff-row {
    position: relative;
    display: grid; grid-template-columns: 32px 32px 18px 1fr; align-items: stretch;
  }
  .diff-row .ln, .diff-row .sign { user-select: none; color: #a1a1a1; font-size: 11px; }
  .diff-row .ln { text-align: right; padding: 0 7px; }
  .diff-row .sign { text-align: center; }
  .diff-row code { white-space: pre; padding: 0 12px 0 8px; color: #a1a1a1; }
  /* left accent bar: solid green for additions, red hatch for deletions */
  .diff-row.add::before, .diff-row.del::before {
    content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 3px;
  }
  .diff-row.add::before { background: #15a06a; }
  .diff-row.del::before {
    background: repeating-linear-gradient(45deg, #dc2626 0, #dc2626 1.5px, transparent 1.5px, transparent 3px);
  }
  .diff-row.add { background: rgba(26, 127, 55, 0.09); }
  .diff-row.add .sign, .diff-row.add .new { color: #15a06a; }
  .diff-row.add code { color: #1a1a1a; }
  .diff-row.del { background: rgba(207, 34, 46, 0.09); }
  .diff-row.del .sign, .diff-row.del .old { color: #dc2626; }
  .diff-row.del code { color: #1a1a1a; }
  @media (prefers-color-scheme: dark) {
    .diff { background: #1a1a1a; box-shadow: 0 1px 2px rgba(0,0,0,0.4), 0 2px 4px rgba(0,0,0,0.3), 0 0 0 0.5px rgba(255,255,255,0.12); }
    .diff-head { border-bottom-color: #303030; }
    .diff-file { color: #f5f5f5; }
    .diff-stat .add { color: #34d399; }
    .diff-stat .del { color: #f87171; }
    .diff-body::before { background: #303030; }
    .diff-row.add { background: rgba(63, 185, 80, 0.15); }
    .diff-row.add::before { background: #34d399; }
    .diff-row.add .sign, .diff-row.add .new { color: #34d399; }
    .diff-row.add code { color: #f5f5f5; }
    .diff-row.del { background: rgba(248, 81, 73, 0.15); }
    .diff-row.del::before { background: repeating-linear-gradient(45deg, #f87171 0, #f87171 1.5px, transparent 1.5px, transparent 3px); }
    .diff-row.del .sign, .diff-row.del .old { color: #f87171; }
    .diff-row.del code { color: #f5f5f5; }
  }
</style>
```
