# File Diff

Unified file diff with a path top bar, addition/deletion counts, dual line-number gutters, and per-row syntax highlighting via highlight.js with a built-in GitHub palette; theme="custom" skips the token colors for any highlight.js theme stylesheet.

- Package: `@mielui/svelte`
- Component version: `1.3.1`
- Depends on Mielui components: _internal/utils, _internal/highlight
- Shared utilities: components/_internal/inset-layout, hugeicons-icon, utils.cn, utils.createContext

## Install

~~~~sh
pnpm dlx @mielui/svelte add file-diff
~~~~

## API

This reference is generated at build time from the component manifest, public `index.ts`, and documentation examples below. Changes to those source files are reflected here in the published Markdown. Standard Svelte and HTML attributes accepted by the exported prop types are supported.


### Content

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| class | (string & (ClassValue \| null)) \| undefined | - | No | No | Classes added to the element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Filename

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| file | string \| undefined | - | No | No | File path. Falls back to the Root `file` when omitted. |
| class | (string & (ClassValue \| null)) \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### LineNumber

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| value | number \| null \| undefined | - | No | No | Line number. Renders an empty gutter cell when nullish. |
| tone | FileDiffChangeType \| undefined | 'context' | No | No | Number tone. Rows pass their own type; standalone use defaults to context. |
| class | (string & (ClassValue \| null)) \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### PlusMinus

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| additions | number \| undefined | - | No | No | Addition count. Falls back to the Root count when omitted. |
| deletions | number \| undefined | - | No | No | Deletion count. Falls back to the Root count when omitted. |
| class | (string & (ClassValue \| null)) \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Root

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| labels | FileDiffLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |
| file | string \| undefined | '' | No | No | File path shown in the top bar, e.g. "src/auth.ts". |
| lang | string \| undefined | '' | No | No | Highlight.js language id used for every row unless a row overrides it. |
| additions | number \| undefined | - | No | No | Addition count. Defaults to the number of `add` lines in `diff`. |
| deletions | number \| undefined | - | No | No | Deletion count. Defaults to the number of `remove` lines in `diff`. |
| diff | FileDiffLine[] \| undefined | - | No | No | High-level unified lines. When set, Root renders TopBar and Content itself. |
| showLineNumbers | boolean \| undefined | true | No | No | Render the old/new line-number gutters. |
| theme | FileDiffTheme \| undefined | 'mielui' | No | No | Syntax paint: - `mielui` (default): the built-in GitHub palette. - `custom`: no token colors; load any `highlight.js/styles/*` theme instead. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| class | (string & (ClassValue \| null)) \| undefined | - | No | No | Classes added to the element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Row

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| type | FileDiffChangeType \| undefined | 'context' | No | No | Row kind. Drives the tint, line-number tone, and sign. |
| oldLine | number \| null \| undefined | - | No | No | Old-file line number. Empty when nullish. |
| newLine | number \| null \| undefined | - | No | No | New-file line number. Empty when nullish. |
| code | string \| undefined | '' | No | No | Raw source for the row. Highlighted with the Root (or row) language. |
| lang | string \| undefined | - | No | No | Row-level language override. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| class | (string & (ClassValue \| null)) \| undefined | - | No | No | Classes added to the element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### TopBar

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| file | string \| undefined | - | No | No | File path. Falls back to the Root `file` when omitted. |
| additions | number \| undefined | - | No | No | Addition count. Falls back to the Root count when omitted. |
| deletions | number \| undefined | - | No | No | Deletion count. Falls back to the Root count when omitted. |
| children | Snippet<[]> \| undefined | - | No | No | Replaces the default row. The filename and counts are omitted, so compose them back with `Filename` and `PlusMinus` around any actions. Content rendered inside. |
| class | (string & (ClassValue \| null)) \| undefined | - | No | No | Classes added to the element. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Public types

~~~~ts
import type { DefaultProps } from '@mielui/svelte/utils';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';

import Root from './file-diff.svelte';
import Content from './file-diff-content.svelte';
import Filename from './file-diff-filename.svelte';
import LineNumber from './file-diff-line-number.svelte';
import PlusMinus from './file-diff-plus-minus.svelte';
import Row from './file-diff-row.svelte';
import TopBar from './file-diff-top-bar.svelte';

export type FileDiffChangeType = 'context' | 'add' | 'remove';

export type FileDiffLine = {
    type: FileDiffChangeType;
    oldLineNumber?: number | null;
    newLineNumber?: number | null;
    content: string;
};

/**
 * Syntax paint for the highlighted row tokens.
 * - `mielui`: the built-in GitHub palette (light in light mode, dark in dark mode).
 * - `custom`: no token colors; load any `highlight.js/styles/*` theme instead.
 */
export type FileDiffTheme = 'mielui' | 'custom';

export type FileDiffLabels = {
    added?: string;
    removed?: string;
    unchanged?: string;
    line?: (line: number) => string;
};

export type FileDiffContext = {
    labels: FileDiffLabels | undefined;
    lang: string;
    showLineNumbers: boolean;
    file: string;
    additions: number;
    deletions: number;
    /** Token paint; `custom` skips the built-in colors for a stock theme stylesheet. */
    theme: FileDiffTheme;
};

export type FileDiffRootProps = {
    /**
     * Overrides the built-in text and accessible names. Every key is optional and English is the
     * fallback.
     */
    labels?: FileDiffLabels;
    /** File path shown in the top bar, e.g. "src/auth.ts". */
    file?: string;
    /** Highlight.js language id used for every row unless a row overrides it. */
    lang?: string;
    /** Addition count. Defaults to the number of `add` lines in `diff`. */
    additions?: number;
    /** Deletion count. Defaults to the number of `remove` lines in `diff`. */
    deletions?: number;
    /** High-level unified lines. When set, Root renders TopBar and Content itself. */
    diff?: FileDiffLine[];
    /** Render the old/new line-number gutters. */
    showLineNumbers?: boolean;
    /**
     * Syntax paint:
     * - `mielui` (default): the built-in GitHub palette.
     * - `custom`: no token colors; load any `highlight.js/styles/*` theme instead.
     */
    theme?: FileDiffTheme;
    /** Content rendered inside. */
    children?: Snippet;
} & DefaultProps &
    Omit<HTMLAttributes<HTMLElement>, 'children'>;

export type FileDiffTopBarProps = {
    /** File path. Falls back to the Root `file` when omitted. */
    file?: string;
    /** Addition count. Falls back to the Root count when omitted. */
    additions?: number;
    /** Deletion count. Falls back to the Root count when omitted. */
    deletions?: number;
    /**
     * Replaces the default row. The filename and counts are omitted, so compose them back with
     * `Filename` and `PlusMinus` around any actions.
     */
    children?: Snippet;
} & DefaultProps &
    Omit<HTMLAttributes<HTMLElement>, 'children'>;

export type FileDiffFilenameProps = {
    /** File path. Falls back to the Root `file` when omitted. */
    file?: string;
} & DefaultProps &
    Omit<HTMLAttributes<HTMLElement>, 'children'>;

export type FileDiffPlusMinusProps = {
    /** Addition count. Falls back to the Root count when omitted. */
    additions?: number;
    /** Deletion count. Falls back to the Root count when omitted. */
    deletions?: number;
} & DefaultProps &
    Omit<HTMLAttributes<HTMLElement>, 'children'>;

export type FileDiffContentProps = {
    /** Content rendered inside. */
    children?: Snippet;
} & DefaultProps &
    Omit<HTMLAttributes<HTMLElement>, 'children'>;

export type FileDiffRowProps = {
    /** Row kind. Drives the tint, line-number tone, and sign. */
    type?: FileDiffChangeType;
    /** Old-file line number. Empty when nullish. */
    oldLine?: number | null;
    /** New-file line number. Empty when nullish. */
    newLine?: number | null;
    /** Raw source for the row. Highlighted with the Root (or row) language. */
    code?: string;
    /** Row-level language override. */
    lang?: string;
    /** Content rendered inside. */
    children?: Snippet;
} & DefaultProps &
    Omit<HTMLAttributes<HTMLElement>, 'children'>;

export type FileDiffLineNumberProps = {
    /** Line number. Renders an empty gutter cell when nullish. */
    value?: number | null;
    /** Number tone. Rows pass their own type; standalone use defaults to context. */
    tone?: FileDiffChangeType;
} & DefaultProps &
    Omit<HTMLAttributes<HTMLSpanElement>, 'children'>;

export { Content, Filename, LineNumber, PlusMinus, Root, Row, TopBar };
export default Root;
~~~~

## Examples

### Compound

~~~~svelte
<script lang="ts">
    import { ArrowExpandIcon as Expand } from '@hugeicons/core-free-icons';
    import { Button } from '@mielui/svelte/components/button';
    import * as FileDiff from '@mielui/svelte/components/file-diff';
    import HugeiconsIcon from '@mielui/svelte/hugeicons-icon';
</script>

<FileDiff.Root file="src/auth.ts" lang="ts" additions={3} deletions={1} class="max-w-2xl">
    <FileDiff.TopBar>
        <FileDiff.Filename />
        <FileDiff.PlusMinus />
        <Button variant="ghost" size="icon" class="size-7" aria-label="Expand diff">
            <HugeiconsIcon icon={Expand} size={14} />
        </Button>
    </FileDiff.TopBar>
    <FileDiff.Content>
        <FileDiff.Row
            type="context"
            oldLine={12}
            newLine={12}
            code={'export function getToken() {'}
        />
        <FileDiff.Row type="remove" oldLine={13} code="  return localStorage.token;" />
        <FileDiff.Row type="add" newLine={13} code={'  const t = cookies.get("session");'} />
        <FileDiff.Row type="add" newLine={14} code={'  if (!t) throw new Error("no session");'} />
        <FileDiff.Row type="add" newLine={15} code="  return t;" />
        <FileDiff.Row type="context" oldLine={14} newLine={16} code={'}'} />
    </FileDiff.Content>
</FileDiff.Root>
~~~~

### Live

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import type { FileDiffLine } from '@mielui/svelte/components/file-diff';
    import * as FileDiff from '@mielui/svelte/components/file-diff';

    let revised = $state(false);

    const original: FileDiffLine[] = [
        {
            type: 'add',
            newLineNumber: 1,
            content: 'const retries = 1;'
        }
    ];
    const revision: FileDiffLine[] = [
        {
            type: 'remove',
            oldLineNumber: 1,
            content: 'const retries = 1;'
        },
        {
            type: 'add',
            newLineNumber: 1,
            content: 'const retries = 3;'
        }
    ];
    const diff = $derived(revised ? revision : original);
</script>

<div class="flex w-full max-w-2xl flex-col gap-4">
    <Button
        variant="outline"
        class="w-fit"
        onclick={() => {
            revised = !revised;
        }}
    >
        {revised ? 'Show original patch' : 'Revise patch'}
    </Button>
    <FileDiff.Root file="src/config.ts" lang="ts" {diff} />
</div>
~~~~

### Stacked

~~~~svelte
<script lang="ts">
    import type { FileDiffLine } from '@mielui/svelte/components/file-diff';
    import * as FileDiff from '@mielui/svelte/components/file-diff';

    const authDiff: FileDiffLine[] = [
        {
            type: 'context',
            oldLineNumber: 12,
            newLineNumber: 12,
            content: 'export function getToken() {'
        },
        {
            type: 'remove',
            oldLineNumber: 13,
            content: '  return localStorage.token;'
        },
        {
            type: 'add',
            newLineNumber: 13,
            content: '  const t = cookies.get("session");'
        },
        {
            type: 'add',
            newLineNumber: 14,
            content: '  if (!t) throw new Error("no session");'
        },
        {
            type: 'add',
            newLineNumber: 15,
            content: '  return t;'
        },
        {
            type: 'context',
            oldLineNumber: 14,
            newLineNumber: 16,
            content: '}'
        }
    ];

    const stylesDiff: FileDiffLine[] = [
        {
            type: 'context',
            oldLineNumber: 4,
            newLineNumber: 4,
            content: '  color: var(--color-foreground);'
        },
        {
            type: 'remove',
            oldLineNumber: 5,
            content: '  background: #f4f4f0;'
        },
        {
            type: 'add',
            newLineNumber: 5,
            content: '  background: var(--color-card);'
        },
        {
            type: 'context',
            oldLineNumber: 6,
            newLineNumber: 6,
            content: '  border: 1px solid var(--color-border);'
        }
    ];
</script>

<div class="flex w-full max-w-2xl flex-col gap-4">
    <FileDiff.Root file="src/auth.ts" lang="ts" diff={authDiff} />
    <FileDiff.Root file="src/styles.css" lang="css" diff={stylesDiff} />
</div>
~~~~

### Without Line Numbers

~~~~svelte
<script lang="ts">
    import type { FileDiffLine } from '@mielui/svelte/components/file-diff';
    import * as FileDiff from '@mielui/svelte/components/file-diff';

    const diff: FileDiffLine[] = [
        {
            type: 'context',
            oldLineNumber: 4,
            newLineNumber: 4,
            content: '  color: var(--color-foreground);'
        },
        {
            type: 'remove',
            oldLineNumber: 5,
            content: '  background: #f4f4f0;'
        },
        {
            type: 'add',
            newLineNumber: 5,
            content: '  background: var(--color-card);'
        },
        {
            type: 'context',
            oldLineNumber: 6,
            newLineNumber: 6,
            content: '  border: 1px solid var(--color-border);'
        }
    ];
</script>

<FileDiff.Root file="src/styles.css" lang="css" {diff} showLineNumbers={false} class="max-w-2xl" />
~~~~

For the rendered reference, visit [/docs/components/file-diff](/docs/components/file-diff).
