CodeDiff#
코드 변경 사항을 라인 단위로 비교해서 보여주는 read-only viewer입니다. 추가된 라인은 success 톤으로, 삭제된 라인은 error 톤으로 강조하며 변경 없는 라인은 그대로 둡니다. PR 리뷰, 변경 이력 표시, 마이그레이션 가이드 등에 사용합니다.
이미지나 텍스트 두 가지를 좌우로 비교하는 split-view UI 가 필요하다면
Diff컴포넌트를 사용하세요.
Live Preview#
class CodeDiffDefaultExample extends StatelessComponent {
const CodeDiffDefaultExample({super.key});
@override
Component build(BuildContext context) {
return const CodeDiff(
lines: [
CoreCodeDiffLine(
text: "import 'package:coui_web/coui_web.dart';",
type: CoreCodeDiffLineType.unchanged,
lineNumber: 1,
),
CoreCodeDiffLine(
text: '',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 2,
),
CoreCodeDiffLine(
text: 'class MyWidget extends StatelessComponent {',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 3,
),
CoreCodeDiffLine(
text: ' Component build(BuildContext context) {',
type: CoreCodeDiffLineType.removed,
lineNumber: 4,
),
CoreCodeDiffLine(
text: ' Component build(BuildContext context) {',
type: CoreCodeDiffLineType.added,
lineNumber: 4,
),
CoreCodeDiffLine(
text: " return text('Hello');",
type: CoreCodeDiffLineType.removed,
lineNumber: 5,
),
CoreCodeDiffLine(
text: " return Text('Hello').bodyMedium;",
type: CoreCodeDiffLineType.added,
lineNumber: 5,
),
CoreCodeDiffLine(
text: ' }',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 6,
),
CoreCodeDiffLine(
text: '}',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 7,
),
],
);
}
}
class CodeDiffDefaultExample extends StatelessWidget {
const CodeDiffDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return const CodeDiff(
lines: [
CoreCodeDiffLine(
text: "import 'package:coui_flutter/coui_flutter.dart';",
type: CoreCodeDiffLineType.unchanged,
lineNumber: 1,
),
CoreCodeDiffLine(
text: '',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 2,
),
CoreCodeDiffLine(
text: 'class MyWidget extends StatelessWidget {',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 3,
),
CoreCodeDiffLine(
text: ' Widget build(BuildContext context) {',
type: CoreCodeDiffLineType.removed,
lineNumber: 4,
),
CoreCodeDiffLine(
text: ' Widget build(BuildContext context) {',
type: CoreCodeDiffLineType.added,
lineNumber: 4,
),
CoreCodeDiffLine(
text: " return Text('Hello');",
type: CoreCodeDiffLineType.removed,
lineNumber: 5,
),
CoreCodeDiffLine(
text: " return Text('Hello').bodyMedium;",
type: CoreCodeDiffLineType.added,
lineNumber: 5,
),
CoreCodeDiffLine(
text: ' }',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 6,
),
CoreCodeDiffLine(
text: '}',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 7,
),
],
);
}
}
class CodeDiffChainExample extends StatelessComponent {
const CodeDiffChainExample({super.key});
@override
Component build(BuildContext context) {
return const CodeDiff(
lines: [
CoreCodeDiffLine(
text: "import 'package:coui_web/coui_web.dart';",
type: CoreCodeDiffLineType.unchanged,
lineNumber: 1,
),
CoreCodeDiffLine(
text: '',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 2,
),
CoreCodeDiffLine(
text: 'class MyWidget extends StatelessComponent {',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 3,
),
CoreCodeDiffLine(
text: ' Component build(BuildContext context) {',
type: CoreCodeDiffLineType.removed,
lineNumber: 4,
),
CoreCodeDiffLine(
text: ' Component build(BuildContext context) {',
type: CoreCodeDiffLineType.added,
lineNumber: 4,
),
CoreCodeDiffLine(
text: " return text('Hello');",
type: CoreCodeDiffLineType.removed,
lineNumber: 5,
),
CoreCodeDiffLine(
text: " return Text('Hello').bodyMedium;",
type: CoreCodeDiffLineType.added,
lineNumber: 5,
),
CoreCodeDiffLine(
text: ' }',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 6,
),
CoreCodeDiffLine(
text: '}',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 7,
),
],
).withStyle(
const CoreCodeDiffStyle(
cornerRadius: CoreBorderRadius.all(CoreRadius.radius4),
borderWidth: CoreStrokeWidth.stroke2,
outlineColor: CoreColor.token(CoreColors.primary),
addedColor: CoreColor.token(CoreColors.info),
removedColor: CoreColor.token(CoreColors.warning),
linePadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space16,
vertical: CoreSpace.space4,
),
),
);
}
}
class CodeDiffChainExample extends StatelessWidget {
const CodeDiffChainExample({super.key});
@override
Widget build(BuildContext context) {
return const CodeDiff(
lines: [
CoreCodeDiffLine(
text: "import 'package:coui_flutter/coui_flutter.dart';",
type: CoreCodeDiffLineType.unchanged,
lineNumber: 1,
),
CoreCodeDiffLine(
text: '',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 2,
),
CoreCodeDiffLine(
text: 'class MyWidget extends StatelessWidget {',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 3,
),
CoreCodeDiffLine(
text: ' Widget build(BuildContext context) {',
type: CoreCodeDiffLineType.removed,
lineNumber: 4,
),
CoreCodeDiffLine(
text: ' Widget build(BuildContext context) {',
type: CoreCodeDiffLineType.added,
lineNumber: 4,
),
CoreCodeDiffLine(
text: " return Text('Hello');",
type: CoreCodeDiffLineType.removed,
lineNumber: 5,
),
CoreCodeDiffLine(
text: " return Text('Hello').bodyMedium;",
type: CoreCodeDiffLineType.added,
lineNumber: 5,
),
CoreCodeDiffLine(
text: ' }',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 6,
),
CoreCodeDiffLine(
text: '}',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 7,
),
],
).withStyle(
const CoreCodeDiffStyle(
cornerRadius: CoreBorderRadius.all(CoreRadius.radius4),
borderWidth: CoreStrokeWidth.stroke2,
outlineColor: CoreColor.token(CoreColors.primary),
addedColor: CoreColor.token(CoreColors.info),
removedColor: CoreColor.token(CoreColors.warning),
linePadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space16,
vertical: CoreSpace.space4,
),
),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- PR / 커밋 변경 사항을 라인 단위로 보여줄 때
- 마이그레이션 가이드에서 "이전 → 이후" 코드를 강조할 때
- 변경 이력(audit log)을 코드 형태로 보여줄 때
대신 다른 컴포넌트를 사용하세요:
CodeSnippet: 단일 코드 블록을 보여줄 때 (변경 표시 없음)Diff: 이미지/텍스트 split-view 비교가 필요할 때
빠른 오버라이드 (Chain)#
이미 만든 CodeDiff 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class CodeDiffChainExample extends StatelessWidget {
const CodeDiffChainExample({super.key});
@override
Widget build(BuildContext context) {
return const CodeDiff(
lines: [
CoreCodeDiffLine(
text: "import 'package:coui_flutter/coui_flutter.dart';",
type: CoreCodeDiffLineType.unchanged,
lineNumber: 1,
),
CoreCodeDiffLine(
text: '',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 2,
),
CoreCodeDiffLine(
text: 'class MyWidget extends StatelessWidget {',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 3,
),
CoreCodeDiffLine(
text: ' Widget build(BuildContext context) {',
type: CoreCodeDiffLineType.removed,
lineNumber: 4,
),
CoreCodeDiffLine(
text: ' Widget build(BuildContext context) {',
type: CoreCodeDiffLineType.added,
lineNumber: 4,
),
CoreCodeDiffLine(
text: " return Text('Hello');",
type: CoreCodeDiffLineType.removed,
lineNumber: 5,
),
CoreCodeDiffLine(
text: " return Text('Hello').bodyMedium;",
type: CoreCodeDiffLineType.added,
lineNumber: 5,
),
CoreCodeDiffLine(
text: ' }',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 6,
),
CoreCodeDiffLine(
text: '}',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 7,
),
],
).withStyle(
const CoreCodeDiffStyle(
cornerRadius: CoreBorderRadius.all(CoreRadius.radius4),
borderWidth: CoreStrokeWidth.stroke2,
outlineColor: CoreColor.token(CoreColors.primary),
addedColor: CoreColor.token(CoreColors.info),
removedColor: CoreColor.token(CoreColors.warning),
linePadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space16,
vertical: CoreSpace.space4,
),
),
);
}
}
class CodeDiffChainExample extends StatelessComponent {
const CodeDiffChainExample({super.key});
@override
Component build(BuildContext context) {
return const CodeDiff(
lines: [
CoreCodeDiffLine(
text: "import 'package:coui_web/coui_web.dart';",
type: CoreCodeDiffLineType.unchanged,
lineNumber: 1,
),
CoreCodeDiffLine(
text: '',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 2,
),
CoreCodeDiffLine(
text: 'class MyWidget extends StatelessComponent {',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 3,
),
CoreCodeDiffLine(
text: ' Component build(BuildContext context) {',
type: CoreCodeDiffLineType.removed,
lineNumber: 4,
),
CoreCodeDiffLine(
text: ' Component build(BuildContext context) {',
type: CoreCodeDiffLineType.added,
lineNumber: 4,
),
CoreCodeDiffLine(
text: " return text('Hello');",
type: CoreCodeDiffLineType.removed,
lineNumber: 5,
),
CoreCodeDiffLine(
text: " return Text('Hello').bodyMedium;",
type: CoreCodeDiffLineType.added,
lineNumber: 5,
),
CoreCodeDiffLine(
text: ' }',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 6,
),
CoreCodeDiffLine(
text: '}',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 7,
),
],
).withStyle(
const CoreCodeDiffStyle(
cornerRadius: CoreBorderRadius.all(CoreRadius.radius4),
borderWidth: CoreStrokeWidth.stroke2,
outlineColor: CoreColor.token(CoreColors.primary),
addedColor: CoreColor.token(CoreColors.info),
removedColor: CoreColor.token(CoreColors.warning),
linePadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space16,
vertical: CoreSpace.space4,
),
),
);
}
}
Props / Parameters#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
lines |
List<CoreCodeDiffLine> |
필수 | 표시할 라인 데이터 |
showLineNumbers |
bool |
true |
라인 번호 거터 표시 여부 |
codeDiffStyle |
CoreCodeDiffStyle? |
null |
컨테이너·행·텍스트 chrome 오버라이드 (아래 표 참고) |
타이포그래피는 디자인 시스템의 시맨틱 토큰(bodySmall / labelSmall)을 그대로
따르며 sans 폰트로 렌더링됩니다. + / - /
prefix 정렬은 고정폭
라인 번호 거터와 두 글자 prefix span 으로 보장됩니다 (별도의 monospace 폰트
지정 없음).
CoreCodeDiffLine — 라인 데이터#
위젯 파라미터가 아니라 lines 에 담아 넘기는 plain Dart 데이터 클래스입니다
(양 플랫폼 동일 모양).
| 필드 | 타입 | 설명 |
|---|---|---|
text | String | 라인 텍스트 (필수) |
type |
CoreCodeDiffLineType |
added / removed / unchanged (필수) |
lineNumber | int? | 거터에 표시할 라인 번호 |
CoreCodeDiffStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
cornerRadius |
CoreBorderRadius? |
Outer container border-radius. Falls back to [defaultCornerRadius]. |
borderWidth |
double? |
Outer container border stroke width (logical px). Falls back to [defaultBorderWidth]. |
prefixFontWeight |
int? |
Font weight applied to the
+
/
-
/
prefix span. Falls back to [defaultPrefixFontWeight].
|
surfaceColor |
CoreColor? |
Outer container background colour. Falls back to [defaultSurfaceColor]. |
outlineColor |
CoreColor? |
Outer container border colour. Falls back to [defaultOutlineColor]. |
addedColor |
CoreColor? |
Background tint for added lines. The component applies the resolved [rowTintAlpha] when painting. Falls back to [defaultAddedColor]. |
removedColor |
CoreColor? |
Background tint for removed lines. The component applies the resolved [rowTintAlpha] when painting. Falls back to [defaultRemovedColor]. |
unchangedTextStyle |
CoreTextStyle? |
Text style for unchanged rows. Text colour is carried via [CoreTextStyle.color] inside this slot (sb8 — raw
unchangedTextColor
field removed). Falls back to [defaultUnchangedTextStyle].
|
lineNumberColor |
CoreColor? |
Line-number gutter base text colour (alpha applied at paint time). Falls back to [defaultLineNumberColor]. |
rowTintAlpha |
double? |
Background-tint alpha applied to added / removed rows. Falls back to [defaultRowTintAlpha]. |
lineNumberAlpha |
double? |
Alpha applied to the line-number text colour. Falls back to [defaultLineNumberAlpha]. |
linePadding |
CoreEdgeInsets? |
Padding inside each diff row. Falls back to [defaultLinePadding]. |
lineNumberPadding |
CoreEdgeInsets? |
Padding around the line-number text. Falls back to [defaultLineNumberPadding]. |
lineNumberWidth |
double? |
Width of the line-number gutter column (logical px, pre-scaling). Falls back to [defaultLineNumberWidth]. |
bodyTypography |
CoreTextStyle? |
Typography role for the diff body text. Falls back to [defaultBodyTypography]. |
gutterTypography |
CoreTextStyle? |
Typography role for the line-number gutter text. Falls back to [defaultGutterTypography]. |
bodyTextStyle |
CoreTextStyle? |
Body text style override (the diff content).
Deliberately has no
default*
— this is the overlay half of a pair whose other half already carries the default.
The typography comes from [bodyTypography] / [defaultBodyTypography]; this slot is merged
on top
of whatever that resolved to. A constant would therefore outrank the role a caller selected through [bodyTypography] — set the role to something larger and the default overlay's own size would win it back. On Web it would also move the typography off its class: the resolver adds
bodyTextStyle.toInlineCss
only inside
if (bodyTextStyle != null)
, so a non-null default emits an inline overlay on every diff row and shadows the
text-{role}
class that carries the role today.
|
gutterTextStyle |
CoreTextStyle? |
Line-number gutter text style override.
Deliberately has no default* — same pairing as [bodyTextStyle]
, against [gutterTypography] / [defaultGutterTypography]. Both platforms consume it the same way (Flutter merges it over the resolved role, Web emits it inline only when present), so a constant would both outrank a caller-chosen gutter role and put inline typography on every gutter cell.
|
예제#
CodeDiff(
lines: [
CoreCodeDiffLine(
text: 'class MyWidget extends StatelessWidget {',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 1,
),
CoreCodeDiffLine(
text: " return Text('Hello');",
type: CoreCodeDiffLineType.removed,
lineNumber: 2,
),
CoreCodeDiffLine(
text: " return Text('Hello').bodyMedium;",
type: CoreCodeDiffLineType.added,
lineNumber: 2,
),
],
)
사용 가이드라인 (Usage Guidelines)#
✅ Do#
추가/삭제가 있는 실제 변경 비교에만 사용
CodeDiff(
lines: [
CoreCodeDiffLine(
text: " return Text('Hello');",
type: CoreCodeDiffLineType.removed,
lineNumber: 5,
),
CoreCodeDiffLine(
text: " return Text('Hello').bodyMedium;",
type: CoreCodeDiffLineType.added,
lineNumber: 5,
),
],
)
+ / - / 공백 prefix 는 CSS 가 아니라 실제 텍스트 콘텐츠라, 변경 여부를 색상 없이도 전달합니다 — 이 구조는 실제 변경이 있을 때만 의미가 있습니다.
❌ Don't#
변경 없는 단일 코드 블록 표시에 사용하지 않기
// ❌ 변경 사항 없이 unchanged 라인만 나열
CodeDiff(
lines: [
CoreCodeDiffLine(
text: 'final x = 1;',
type: CoreCodeDiffLineType.unchanged,
lineNumber: 1,
),
],
)
// ✅ 단일 코드 블록은 CodeSnippet
CodeSnippet(code: 'final x = 1;')
CodeDiff 는 코드/diff 시맨틱을 전혀 emit 하지 않는(read-only, <pre>/<code>/role="table" 없음) 뷰어입니다 — 변경이 없는 코드에 쓰면 접근성 이점 없이 무게만 늘어납니다.
접근성 (Accessibility)#
CodeDiff 는 read-only 뷰어이고, 접근성 측면에서도 아무 역할·이름·포커스를
내보내지 않습니다. 아래 내용은 양 플랫폼이 동일합니다.
역할 (Semantics)#
없습니다. Flutter 는 Container / Column / Row / Text 만으로 구성되어
Semantics 노드를 하나도 만들지 않고, Web 루트는 평범한 <div>, 각 행도
div / span 입니다. <pre> · <code>
· <table> / role="table" ·
<ins> / <del> · aria-label
중 어느 것도 emit 하지 않습니다.
즉 리더는 이 블록을 일반 텍스트 흐름으로 지나가며, diff 를 하나의 단위로 찾거나 건너뛸 수 없습니다.
키보드#
키 처리가 없습니다. 양 플랫폼 모두 키 핸들러를 등록하지 않습니다.
포커스#
포커스 가능한 요소가 없습니다. FocusNode · tabindex · 포커스 링 모두 없습니다.
Web 루트는 overflow-hidden 이고 행이 줄바꿈되므로(whitespace-pre-wrap),
포커스 가능한 래퍼가 필요한 스크롤 컨테이너도 생기지 않습니다.
스크린 리더#
각 행은 순서대로 평평한 텍스트로 읽힙니다 — 라인 번호(showLineNumbers 가 켜져
있고 line.lineNumber 가 있을 때만), 그다음 '+ ' / '- '
/ ' ' prefix,
그다음 라인 텍스트.
이 prefix 는 CSS 가 아니라 실제 텍스트 콘텐츠입니다(양 플랫폼 동일). 덕분에 추가/삭제 구분이 색상에만 실려 있지는 않습니다. 다만 그 이상은 없어서, 나머지는 구분되지 않는 산문처럼 읽힙니다.
알려진 제약#
-
Web 에 코드 시맨틱이 없습니다 (
<pre>/<code>미사용). 리더가 코드 읽기 모드로 진입하지 않으므로 공백과 고정폭의 의미가 전달되지 않습니다. -
diff 전용 시맨틱이 없습니다 —
<ins>/<del>도, "추가된 줄" 같은 행 단위aria-label도 없습니다. 유일한 신호는 리터럴+/-문자이고, 이 문자를 읽어줄지는 리더와 상세도(verbosity) 설정에 따라 달라집니다. 변경 없는 행의 공백 두 칸 prefix 는 대개 통째로 무시됩니다. - 라인 번호가 해당 라인과 연결되어 있지 않습니다 — 형제 텍스트 노드일 뿐이라 "5번째 줄의 내용" 이라는 관계가 전달되지 않습니다.
- diff 전체에 이름도 역할도 없습니다. 랜드마크나 영역으로 찾아갈 수 없으므로, 이름 붙은 영역이 필요하면 감싸는 쪽에서 직접 역할과 이름을 부여해야 합니다.
전역으로 적용되는 축(동작 줄이기·고대비·색 강제 모드·최소 터치 타겟)은 전역 접근성 축에 있습니다.
관련 컴포넌트 (Related Components)#
- CodeSnippet: 단일 코드 블록 표시
- Diff: 이미지/텍스트 split-view 비교