AI output
File changes
A compact row of chips, one per file an agent changed, with lines added and removed, where each chip grows in after the run and opens a short diff preview on hover or focus.
Live preview
Loading preview
An agent finishes a run and five changed files appear as chips, three shown and a more button; hovering a chip opens its diff preview, and the more button shows the other two. Replay runs the run end again, and a second demo shows a count rising while the agent is still editing.
Installation
Run this in your app after the one-time setup on the Getting started page.
$ npx @ganisigit/lunia-ds add file-changesUsage
The demo above, as code.
'use client';
import * as React from 'react';
import { type DiffLine, type FileChange, FileChanges } from './file-changes';
const themeLines: DiffLine[] = [
{ kind: 'context', text: 'export const theme = {' },
{ kind: 'remove', text: " radius: 'sm'," },
{ kind: 'add', text: " radius: 'md'," },
{ kind: 'add', text: " motion: 'calm'," },
{ kind: 'context', text: '};' },
];
const buttonLines: DiffLine[] = [
{ kind: 'context', text: 'export function Button(props) {' },
{ kind: 'remove', text: ' return <button {...props} />;' },
{ kind: 'add', text: ' const label = props.label ?? "Save";' },
{ kind: 'add', text: ' return <button {...props}>{label}</button>;' },
{ kind: 'context', text: '}' },
];
const menuLines: DiffLine[] = [
{ kind: 'add', text: 'export function openMenu(id: string) {' },
{ kind: 'add', text: ' return menus.get(id)?.open();' },
{ kind: 'add', text: '}' },
];
const FILES: FileChange[] = [
{ path: 'src/theme.ts', additions: 14, deletions: 3, lines: themeLines },
{ path: 'src/components/button.tsx', additions: 8, deletions: 2, lines: buttonLines },
{ path: 'src/menu.ts', additions: 22, deletions: 0, lines: menuLines },
{ path: 'tests/menu.test.ts', additions: 31, deletions: 0 },
{ path: 'README.md', additions: 3, deletions: 1 },
];
const LIVE_END = 27;
export default function FileChangesDemo({ replay = 0 }: { replay?: number }) {
const [live, setLive] = React.useState(LIVE_END);
// Replay runs the end of a run again, and a count that rises while the agent is still editing.
React.useEffect(() => {
if (replay === 0) return;
setLive(4);
const timer = window.setInterval(() => {
setLive((n) => {
if (n >= LIVE_END) {
window.clearInterval(timer);
return LIVE_END;
}
return Math.min(LIVE_END, n + 5);
});
}, 450);
return () => window.clearInterval(timer);
}, [replay]);
return (
<div className="flex w-full max-w-[560px] flex-col gap-6 rounded-xl border border-secondary bg-primary p-6">
<div className="flex flex-col gap-3">
<p className="text-sm font-medium text-primary">Run finished, 5 files changed</p>
<FileChanges key={replay} files={FILES} />
</div>
<div className="flex flex-col gap-3">
<p className="text-sm font-medium text-primary">Agent is editing</p>
<FileChanges files={[{ path: 'src/app.tsx', additions: live, deletions: 2 }]} />
</div>
</div>
);
}
API
Parts: root (list), file chip (button): file name, lines added, lines removed, more button, diff preview (popover): header with path and counts, diff lines with a + or - gutter.
| Prop | Type | Default | Description |
|---|---|---|---|
| files | Array<{ path: string; additions: number; deletions: number; lines?: Array<{ text: string; kind: "add" | "remove" | "context" }> }> | [] | Changed files in the order the agent changed them. lines feeds the preview; without it the chip has no preview. |
| visible | number | 3 | Chips shown before the more button. |
| expanded | boolean | undefined | Controlled state of the full list. |
| defaultExpanded | boolean | false | Full list shown at first when not controlled. |
| previewDelay | number | 300 | Hover time in ms before the preview opens; focus opens it at once. |
| Event | Description |
|---|---|
| fileOpen | The person pressed a chip (click, Enter or Space). Payload: the file. Use it to open diff-review or the file. |
| expandedChange | The full list opened or closed. Payload: the new state. |
| Slot | Description |
|---|---|
| preview | Custom preview body instead of the default diff lines. |
Motion
Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.
| ID | Trigger | Element | Property | From → to | Timing | Reduced motion |
|---|---|---|---|---|---|---|
| FCH1 | List appears or a file is added | file chip | opacity | 0 → 1 | duration-md (260)ease-outdelay stagger-step x chip | Shows at once |
| FCH2 | List appears or a file is added | file chip | scale | scale-press → 1 | duration-xl (380)ease-settledelay stagger-step x chip | Shows at once |
| FCH3 | List appears | more button | opacity | 0 → 1 | duration-md (260)ease-outdelay stagger-step x visible | Shows at once |
| FCH4 | Counts change while the agent works | lines added, lines removed | number roll | old count → new count | duration-5xl (640)ease-out | Swaps at once |
| FCH5 | Hover or press | file chip, more button | background color | bg-primary_alt → bg-tertiary | duration-2xs (120)ease-out | Swaps at once |
| FCH6 | Press | file chip | scale | 1 → scale-press | duration-2xs (120)ease-out | Stays still |
| FCH7 | Preview opens | diff preview | opacity | 0 → 1 | duration-sm (200)ease-out | Shows at once |
| FCH8 | Preview opens | diff preview (origin: chip corner) | scale, translate y | scale-press, distance-rise → 1, 0 | duration-md (260)ease-settle | Shows at once |
| FCH9 | Preview closes | diff preview | opacity | 1 → 0 | duration-2xs (120)ease-exit | Hides at once |
| FCH10 | Expand or collapse the list | root | height | one row → content height | duration-2xl (420)ease-settle | Resizes at once |
| FCH11 | Expand | extra file chips | opacity | 0 → 1 | duration-md (260)ease-outdelay duration-2xs + stagger-step x chip | Shows at once |
| FCH12 | Collapse | extra file chips | opacity | 1 → 0 | duration-2xs (120)ease-exit | Hides at once |
| FCH13 | Expand or collapse | more button label | text morph | +2 more → Show fewer | duration-3xl (460)ease-out | Swaps at once |
Accessibility
- The root is a list labeled "Changed files"; each chip is a list item holding one button.
- Each chip button has an accessible name with the numbers in words, for example "menu.ts, 8 lines added, 2 lines removed"; the visible + and - signs carry the meaning, so color is never the only cue.
- The preview opens on hover after previewDelay and at once on keyboard focus, closes on Escape, blur and pointer leave, and is linked to its chip with aria-describedby; it holds no focusable content, so Tab moves to the next chip.
- The more button has aria-expanded and an accessible name that says the count, for example "Show 2 more files"; after collapse, focus stays on it.
- Chips and the more button are 28px tall (target at least 24px) with a visible focus ring (Focus rings/focus-ring).
- Diff lines use utility-green-700 on bg-success-primary and utility-red-700 on bg-error-primary, both at least 4.5:1 in light and dark, plus the + and - gutter.