CodeSnippet
코드를 구문 강조와 함께 표시하는 코드 블록 컴포넌트
목차
코드를 구문 강조(syntax highlighting)와 함께 표시하는 컴포넌트입니다. 줄 번호 표시, 줄 톤 띠, 복사 버튼, 다양한 테마를 지원합니다.
Flutter와 Web 모두 순수 Dart 패키지 syntax_highlight_lite를 사용합니다.
Flutter는 내장 Dart 문법에 JSON·YAML·SQL 문법을 추가 등록하며,
강조 결과를 Flutter TextSpan으로 변환합니다. 복사 기능은 Flutter 기본 Clipboard를 사용합니다.
기본 사용법 (Basic Usage)#
// 기본 코드 블록
CodeSnippet(
code: '''
void main() {
print('Hello, CoUI!');
}''',
mode: 'dart',
)
// 줄 번호와 복사 버튼 포함
CodeSnippet(
code: '''
import 'package:flutter/material.dart';
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
return const Text('Hello');
}
}''',
mode: 'dart',
showLineNumbers: true,
showCopyButton: true,
)
// 기본 코드 블록
CodeSnippet(
code: '''
void main() {
print('Hello, CoUI!');
}''',
mode: 'dart',
)
// 줄 번호 표시
CodeSnippet(
code: '''
import 'package:jaspr/jaspr.dart';
void main() {
runApp(App());
}''',
mode: 'dart',
showLineNumbers: true,
)
빠른 오버라이드 (Chain)#
이미 만든 CodeSnippet 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
.radius8처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius8 ==
CoreRadius.radius8) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.
class CodeSnippetChainExample extends StatefulWidget {
const CodeSnippetChainExample({super.key});
@override
State<CodeSnippetChainExample> createState() => _CodeSnippetChainExampleState();
}
class _CodeSnippetChainExampleState extends State<CodeSnippetChainExample> {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
CodeSnippet(
code: "void main() {\n print('Hello, CoUI!');\n}",
mode: 'dart',
showLineNumbers: true,
showCopyButton: true,
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
CodeSnippet(
code: "void main() {\n print('Hello, CoUI!');\n}",
mode: 'dart',
showLineNumbers: true,
showCopyButton: true,
).withStyle(
const CoreCodeSnippetStyle(
backgroundColor: CoreColor.token(CoreColors.tertiary),
codeTextStyle: CoreTextStyle(
color: CoreColor.token(CoreColors.onTertiary),
),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space24,
vertical: CoreSpace.space8,
),
),
),
],
);
}
}
class CodeSnippetChainExample extends StatefulComponent {
const CodeSnippetChainExample({super.key});
@override
State<CodeSnippetChainExample> createState() => _CodeSnippetChainExampleState();
}
class _CodeSnippetChainExampleState extends State<CodeSnippetChainExample> {
@override
Component build(BuildContext context) {
return div(
[
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
CodeSnippet(
code: "void main() {\n print('Hello, CoUI!');\n}",
mode: 'dart',
showLineNumbers: true,
showCopyButton: true,
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
CodeSnippet(
code: "void main() {\n print('Hello, CoUI!');\n}",
mode: 'dart',
showLineNumbers: true,
showCopyButton: true,
).withStyle(
const CoreCodeSnippetStyle(
backgroundColor: CoreColor.token(CoreColors.tertiary),
codeTextStyle: CoreTextStyle(
color: CoreColor.token(CoreColors.onTertiary),
),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space24,
vertical: CoreSpace.space8,
),
),
),
],
classes: 'flex flex-col items-start',
);
}
}
줄 톤 (Line Tones)#
바뀐 줄 · 경고가 난 줄 · 오류가 난 줄을 띠로 가리킵니다. lineTones 에 줄 번호(1부터)와 톤을 적으면 success ·
warning · error 는 그 줄 뒤에 띠를 깔고, 나머지 톤(neutral · muted
· link)은 아무것도 그리지 않습니다. 코드 밖의 줄 번호도 그리지 않습니다.
코드 보기
class CodeSnippetLineTonesExample extends StatelessComponent {
const CodeSnippetLineTonesExample({super.key});
@override
Component build(BuildContext context) {
// 바뀐 줄은 success, 오류가 난 줄은 error — 줄 번호는 1부터 셉니다.
return const CodeSnippet(
code: "void main() {\n final greeting = 'Hello, CoUI!';\n print(greting);\n}",
mode: 'dart',
showLineNumbers: true,
lineTones: {2: CoreCodeLineTone.success, 3: CoreCodeLineTone.error},
);
}
}
class CodeSnippetLineTonesExample extends StatelessWidget {
const CodeSnippetLineTonesExample({super.key});
@override
Widget build(BuildContext context) {
// 바뀐 줄은 success, 오류가 난 줄은 error — 줄 번호는 1부터 셉니다.
return const CodeSnippet(
code: "void main() {\n final greeting = 'Hello, CoUI!';\n print(greting);\n}",
mode: 'dart',
showLineNumbers: true,
lineTones: {2: CoreCodeLineTone.success, 3: CoreCodeLineTone.error},
);
}
}
띠는 그 줄의 줄 상자 높이이고, 스니펫 폭 전체 — 줄 번호 · 코드 · 양옆 패딩 아래 — 를 덮습니다. 긴 줄이 있으면 띠도 그 줄 끝까지 이어지고, 가로로 스크롤하면 줄과 함께 움직입니다. 색은
successLineColor · warningLineColor · errorLineColor 가 정하며 기본값은
successContainer · warningContainer · errorContainer 입니다 — CodeDiff
가 추가 · 삭제된 줄에 까는 것과 같은 톤입니다.
CodeSnippet(
code: "void main() {\n final greeting = 'Hello, CoUI!';\n print(greting);\n}",
mode: 'dart',
showLineNumbers: true,
lineTones: {2: CoreCodeLineTone.success, 3: CoreCodeLineTone.error},
)
CodeSnippet(
code: "void main() {\n final greeting = 'Hello, CoUI!';\n print(greting);\n}",
mode: 'dart',
showLineNumbers: true,
lineTones: {2: CoreCodeLineTone.success, 3: CoreCodeLineTone.error},
)
줄마다 추가 · 삭제 데이터가 있고 + / - 표시까지 필요하면 CodeDiff
를 씁니다. 줄 톤은 보통의 코드에서 몇 줄을 가리킬 때 씁니다.
Live Preview#
코드 보기
class CodeSnippetDefaultExample extends StatefulComponent {
const CodeSnippetDefaultExample({super.key});
@override
State<CodeSnippetDefaultExample> createState() =>
_CodeSnippetDefaultExampleState();
}
class _CodeSnippetDefaultExampleState
extends State<CodeSnippetDefaultExample> {
@override
Component build(BuildContext context) {
return CodeSnippet(
code: "void main() {\n print('Hello, CoUI!');\n}",
mode: 'dart',
showLineNumbers: true,
showCopyButton: true,
);
}
}
class CodeSnippetDefaultExample extends StatefulWidget {
const CodeSnippetDefaultExample({super.key});
@override
State<CodeSnippetDefaultExample> createState() =>
_CodeSnippetDefaultExampleState();
}
class _CodeSnippetDefaultExampleState
extends State<CodeSnippetDefaultExample> {
@override
Widget build(BuildContext context) {
return CodeSnippet(
code: "void main() {\n print('Hello, CoUI!');\n}",
mode: 'dart',
showLineNumbers: true,
showCopyButton: true,
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 문서, 튜토리얼, 도움말 페이지에서 코드 예시를 보여줄 때
- API 응답이나 설정 파일 내용을 사용자에게 표시할 때
- 코드 복사 기능이 필요한 개발자 도구나 대시보드를 구현할 때
대신 다른 컴포넌트를 사용하세요:
- Kbd: 코드 블록 대신 키보드 키 조합의 힌트를 표시할 때
- CodeDiff: 줄마다 추가·삭제·유지 데이터가 있어 변경 표시가 필요할 때
- Text: 단순 텍스트 정보를 표시할 때 (코드가 아닌 경우)
Props / Parameters#
W 는 Flutter의 Widget, Web의 Jaspr Component 입니다. List<W>
는 콘텐츠 목록입니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
code |
String |
필수 | 표시할 코드 문자열 |
mode |
String |
필수 | 구문 강조 언어 (dart, json, yaml, sql, js, ts) |
showLineNumbers |
bool |
false |
줄 번호 표시 여부 |
lineTones |
Map<int, CoreCodeLineTone> |
{} |
줄 번호(1부터)별 톤 —
success
·
warning
·
error
는 그 줄 뒤에 띠를 깝니다.
줄 톤
참고
|
showCopyButton |
bool |
true |
복사 버튼 표시 여부 |
actions |
List<W> |
[] |
추가 액션 위젯 |
onCopy |
VoidCallback? / CoreVoidCallback? |
null |
복사 후 추가 콜백 |
codeSnippetStyle |
CoreCodeSnippetStyle? |
null |
chrome + nested 슬롯 오버라이드 (아래 표 참고) |
CoreCodeSnippetStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
backgroundColor |
CoreColor? |
Container background colour.
null
defers to [defaultBackgroundColor]. The face also decides whether the highlighter's role colours apply: on a surface or container tone they do, on any other face (a solid role fill, a raw colour, a token at less than full opacity) every span reads the code colour instead — see
CoreSyntaxFaces.admits
. On a solid role fill the line numbers, the copy glyph and the copy button's focus ring take the face's ink as well ([inkOnFace]).
|
borderColor |
CoreColor? |
Outer border stroke colour. null defers to [defaultBorderColor]. |
borderWidth |
double? |
Outer border stroke width (logical px). |
borderRadius |
CoreBorderRadius? |
Outer border radius. null defers to [defaultBorderRadius]. |
padding |
CoreEdgeInsets? |
Inner padding applied to the code area. null defers to [defaultPadding]. |
codeTypography |
CoreTextStyle? |
Code body typography role. null defers to [defaultCodeTypography]. |
codeTextStyle |
CoreTextStyle? |
Rendered code text style override (layered on [codeTypography]). Carries the code foreground colour via [CoreTextStyle.color] — the ink of the plain text and of every span the highlighter leaves uncoloured.
null
defers to [defaultCodeTextStyle].
|
lineNumberTextStyle |
CoreTextStyle? |
Line-number gutter text style override. Its colour is the gutter number's ink;
null
defers to [defaultLineNumberTextStyle] (
onSurfaceVariant
). A colour stated here replaces the default outright. The gutter does not derive from [codeTextStyle]: restating the code colour leaves the numbers where they were, and a caller who wants the numbers recoloured states this slot. On a solid role face the default is the face's ink ([inkOnFace]) instead of
onSurfaceVariant
.
|
preMargin |
double? |
<pre>
wrapper outer margin (logical px).
null
defers to [defaultPreMargin].
|
lineNumberMargin |
double? |
Line-number gutter right margin (logical px). null defers to [defaultLineNumberMargin]. |
lineNumberAlpha |
double? |
Deprecated (since 0.162) — Unused — the gutter number is an opaque token: state lineNumberTextStyle.color (default defaultLineNumberTextStyle, onSurfaceVariant). Hierarchy is the token, not an alpha. Retired: read by no platform. The gutter number is an opaque token — state [lineNumberTextStyle] (colour) instead; hierarchy is the token chosen, not an alpha laid over the code colour. |
copyButtonOffset |
double? |
Copy button corner offset — top + right (logical px).
null
defers to [defaultCopyButtonOffset]. This is layout positioning (the absolute offset of the button within its corner wrapper), not button chrome — it stays flat alongside the nested [copyButtonStyle].
|
minWidth |
double? |
Minimum width of the code display area (logical px).
null
leaves the area unconstrained on that edge — there is no design-system default because "no size cap" is the design, so these four fields deliberately have no
defaultX
counterpart (same shape as
CoreTextAreaStyle.maxHeight
/
maxWidth
). Flutter maps the four to a
BoxConstraints
around the scroll area; Web maps them to
min-width
/
max-width
/
min-height
/
max-height
on the
<pre>
scroll wrapper. A default on ANY of the four is a rendering change, because both platforms decide on their nullness rather than reading a value: · Flutter builds
codeAreaConstraints
as
null
exactly while all four are unset, and the widget passes that straight to
Container(constraints:)
—
null
is a no-op wrapper. One non-null default makes the struct non-null, so every snippet starts being measured against caps instead of taking its intrinsic size. · Web emits each cap only when it is set (
_remOrNull
yields nothing for
null
), and [maxHeight] additionally gates
overflow-y: auto
, so a default there turns every snippet into a clamped scroller.
|
maxWidth |
double? |
Maximum width of the code display area (logical px).
null
leaves the area unconstrained on that edge. No
default*
— see [minWidth] for why a value here would change what every snippet renders.
|
minHeight |
double? |
Minimum height of the code display area (logical px).
null
leaves the area unconstrained on that edge. No
default*
— see [minWidth].
|
maxHeight |
double? |
Maximum height of the code display area (logical px).
null
leaves the area unconstrained on that edge. When set, the code area scrolls vertically past the cap on both platforms (Flutter's enclosing
SingleChildScrollView
, Web's
overflow-y: auto
). No
default*
— and this is the most load-bearing of the four, since Web keys
overflow-y: auto
on it being non-null: a default would make every snippet a fixed-height scroll box. See [minWidth].
|
copyButtonStyle |
CoreButtonStyle? |
Nested [CoreButtonStyle] slot for the copy affordance — raw-forwarded to
Button(variant:.plain, buttonStyle: …)
on both platforms. Recursively merged onto [defaultCopyButtonStyle]; carries the copy button's size / radius / padding / foreground / transition chrome (child-component-composition). On a solid role face the focus ring's colour is the face's ink ([inkOnFace]); a ring colour stated here replaces it.
|
copyIconStyle |
CoreIconStyle? |
Copy button icon style override, merged on top of [defaultCopyIconStyle]:
size
defers to it.
color
has no default — the glyph follows the copy button's foreground through its states — so a colour stated here pins the glyph in every state. A solid role face states its ink here ([inkOnFace]), and the glyph keeps it through hover.
|
actionGapStyle |
CoreGapStyle? |
Gap slot between sibling action widgets in the top-right area.
null
defers to [defaultActionGapStyle].
|
successLineColor |
CoreColor? |
Band colour override for [CoreCodeLineTone.success] lines. Only the three status tones draw a band, each from its own slot (
null
defers to its
default*LineColor
); [lineColorFor] says which slot a tone reads.
neutral
,
muted
and
link
are the colours of a line's text in
MockupCode
and draw nothing behind a line here.
|
warningLineColor |
CoreColor? |
Band colour override for [CoreCodeLineTone.warning] lines. |
errorLineColor |
CoreColor? |
Band colour override for [CoreCodeLineTone.error] lines. |
동작 스펙 (Behavior)#
인터랙션#
-
복사 버튼 클릭: 양쪽 모두 클립보드에
code를 쓰고 체크 표시를CoreCodeSnippetContract.copiedFeedbackDuration동안 보여줍니다. 다시 복사하면 타이머를 다시 시작합니다. 자동 토스트는 없습니다 - 스크롤: 코드 영역 내에서 수직/수평 스크롤 가능
-
호버 (복사 버튼): 글리프가
onSurfaceVariant에서onSurface로 바뀝니다. 글리프는 자기 색을 갖지 않고 복사 버튼(Button의plain변형)의 전경색을 그대로 받기 때문입니다. 솔리드 역할 면(.primary등)에서는 글리프가 면의 짝 색에 고정되어 호버로 변하지 않고,copyIconStyle.color를 직접 지정해도 같습니다. 복사 버튼은 비활성이 되지 않습니다
토큰 기반 스타일링#
모든 chrome 기본값은 coui_core 의 CoreCodeSnippetStyle 안 static const default*
한 곳에서 나오고, Flutter 와 Web resolver 가 같은 값을 읽습니다. 인스턴스별로 바꿀 때는
codeSnippetStyle 슬롯에 필요한 필드만 넘깁니다 (위 필드 표 참고).
CodeSnippet(
code: "print('hi');",
mode: 'dart',
codeSnippetStyle: CoreCodeSnippetStyle(
padding: CoreEdgeInsets.all(CoreSpace.space24),
borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
),
)
프로젝트 전체 기본값은 CoreCodeSnippetTheme.style 로 주입합니다.
면과 구문 강조#
구문 강조 색(syntaxKeyword·syntaxType·syntaxFunction·syntaxString, 그리고 구두점·주석의
onSurfaceVariant, 오류의 onErrorContainer)은 절대 슬롯입니다. 명암 감사가 4.5:1 을 보증하는 면 —
surface, surfaceContainerLow, surfaceContainer, surfaceContainerHigh, 네 상태 컨테이너 — 위에서만 읽히고, 솔리드 역할 면 위에서는 1.0~3.4:1 이 나옵니다. 그래서 면이 이 여덟 토큰 중 하나이고 불투명하면(CoreSyntaxFaces.admits) 강조가 켜지고, 그 밖의 면에서는 모든 span 이 코드 색(codeTextStyle.color) 하나로 나옵니다. 판정은 면의 토큰 이름만 보고 내려지므로 테마를 읽지 않고, Flutter 와 Web 이 같은 함수를 부릅니다. 감사의 기준은 CoUI 자체 색 소스입니다 — 표준 매핑 위의 브랜드는 자기 팔레트를 가져오고, 감사 baseline 에는 그 팔레트의 일부 슬롯이 이 톤 위에서 4.5:1 아래로 남아 있습니다. 토큰 이름으로 내리는 판정은 그것을 볼 수 없어 같은 톤에서 강조를 켭니다.
| 면 | 구문 강조 | 코드 | 줄 번호 | 복사 글리프 |
|---|---|---|---|---|
기본 면 (
surfaceContainerLow
),
.surface
,
.surfaceContainer
, 위 여덟 토큰을 직접 지정한 면
|
켜짐 | onSurface |
onSurfaceVariant |
복사 버튼 전경색 (호버 시 onSurface) |
솔리드 역할 면 —
.primary
…
.outline
여덟 getter, 또는
backgroundColor
로 같은 토큰을 불투명하게 지정한 면
|
꺼짐 |
getter 가 적은 짝 색 (
onPrimary
…
surface
),
withStyle
로 면만 고르면 호출자가 적은 색 (기본
onSurface
)
|
면의 짝 색 | 면의 짝 색, 호버로 변하지 않음 |
그 밖의 면 — 위 여덟 톤 밖의 토큰 (
surfaceContainerHighest
,
surfaceDim
,
inverseSurface
,
primaryContainer
등), 투명도 1 미만인 토큰,
CoreColor.transparent
를 포함한 원색
|
꺼짐 | onSurface |
onSurfaceVariant |
복사 버튼 전경색 |
줄 번호와 글리프의 짝 색은 getter 가 아니라 resolver 가 면에서 읽습니다(CoreCodeSnippetStyle.inkOnFace). 그래서 면을 어떻게 골랐는지 — getter,
withStyle(backgroundColor: …), 프로젝트 테마 — 와 getter 를 이어 쓰는 순서에 상관없이 마지막에 남은 면에 맞습니다. .primary.surface
는 줄 번호와 글리프가 기본값으로 돌아오고 .surface.primary 는 둘 다 onPrimary 가 됩니다. 호출자가 lineNumberTextStyle·copyIconStyle
의 color 를 직접 적으면 그 색이 짝 색을 이깁니다.
코드 색은 다릅니다. getter 는 면과 코드 색을 한 패치로 적고, 마지막 getter 가 둘을 함께 정합니다. withStyle 로 솔리드 면만 고르면 코드는 기본
onSurface 로 남으므로 짝 색(codeTextStyle.color)을 호출자가 적습니다. 원색 면 위에서 코드 색이 읽히는지는 컴포넌트가 알 수 없으므로 호출자의 몫입니다.
사용 가이드라인 (Usage Guidelines)#
Do#
언어 명시로 구문 강조 최적화
CodeSnippet(
code: 'SELECT * FROM users WHERE active = true;',
mode: 'sql',
showCopyButton: true,
)
언어를 명시하면 코드를 더 쉽게 읽을 수 있고, 사용자가 코드의 종류를 즉시 파악할 수 있습니다.
Don't#
지나치게 긴 코드를 제한 없이 표시 금지
// 높이 제약은 Style 슬롯으로 — 양 플랫폼 동일
CodeSnippet(
code: entireFileContent,
mode: 'dart',
codeSnippetStyle: CoreCodeSnippetStyle(maxHeight: 300),
)
긴 코드는 높이를 제한하여 스크롤 가능하게 해야 페이지 레이아웃이 유지됩니다.
접근성 (Accessibility)#
역할#
-
Flutter: 스니펫 전체를 이름 없는
Semantics(container: true)경계로 감쌉니다. 복사 버튼은Semantics(button: true, label: CoUILocalizations.of(context).codeSnippetCopyLabel)로 이름을 받습니다 — 아이콘 자식인Icon은 시맨틱을 내지 않습니다. -
Web: 루트
<div>에는 호출자가 넘긴attributes외에 아무 role/aria 도 붙지 않습니다 (루트<div>는component.componentAttributes만 받습니다). 코드 본문은<pre><code>네이티브 구조로 나가고, 복사 버튼은 네이티브<button type="button">에aria-label로 같은 로컬라이즈 문자열을 답니다.
키보드#
| 키 | Flutter | Web |
|---|---|---|
Tab |
액션·복사 버튼으로 포커스 — Button → Clickable 의 FocusableActionDetector |
액션·복사 버튼으로 포커스 — 네이티브 <button> |
Enter / Space |
복사 실행 — Clickable 의 SingleActivator(.enter) / (.space) |
복사 실행 — 네이티브 <button> 활성화 |
코드 영역 자체를 위한 키는 없습니다. 두 파일 어디에도 키 핸들러가 없고, 컴포넌트가 코드 영역에 만드는 Tab 정지점도 없습니다 — Web 의 <pre>
에는 tabindex 가 없고, Flutter 의 코드 본문은 m.SelectableText 라 그 내부 FocusNode
가 skipTraversal: true 입니다.
스크린 리더#
같은 로컬라이즈 문자열(codeSnippetCopyLabel — 영문 "Copy code", 한국어 "코드 복사")이 양쪽 모두 복사 버튼의 이름입니다.
코드 내용은 양쪽 모두 그대로 읽힙니다 — Web 은 <code>, Flutter 는 m.SelectableText 의 텍스트입니다.
줄 톤은 색으로만 전하지 않습니다. 띠마다 그 줄과 톤을 말로 붙입니다 — codeSnippetSuccessLineLabel · codeSnippetWarningLineLabel
· codeSnippetErrorLineLabel(영문 "Line 2: success", 한국어 "2번째 줄: 성공"). Web 은 띠 안의
sr-only 텍스트, Flutter 는 띠 자리에 놓인 시맨틱 노드이고, 두 플랫폼 모두 줄 순서대로 코드보다 먼저 읽힙니다. 띠를 그려도 Flutter 의 줄 번호 · 코드 노드는 띠가 없을 때와 같습니다.
복사 성공은 통보되지 않습니다. 피드백은 _copied 상태에 따른 아이콘 글리프 교체뿐이고, 버튼의 이름이나 live region 은 바뀌지 않습니다.
포커스 관리#
오버레이가 아니므로 진입·이탈 시 포커스 이동이 없고 트랩도 없습니다. 복사 후에도 포커스는 복사 버튼에 그대로 남습니다 — 두 handleCopy
모두 setState 로 글리프만 바꿉니다.
복사 버튼의 포커스 링은 표준 링입니다 — 링과 버튼 사이 2px 간격은 투명이고 밴드를 칠하지 않습니다. 기본 면과 다른 컨테이너 톤에서는 링 색(focusRing)이 면 위에서 읽히기 때문에
CoreCodeSnippetStyle.defaultCopyButtonStyle 은 링을 따로 지정하지 않습니다. 솔리드 역할 면은 그 톤이 아닙니다 — focusRing
은 .primary·.tertiary 에서 면과 같은 색이고 나머지 솔리드 면에서도 3:1 에 못 미칩니다. 그래서 솔리드 면에서는 링 색이 줄 번호·복사 글리프와 같은 면의 짝 색(onPrimary
등, .outline 은 surface)이 됩니다(CoreCodeSnippetStyle.inkOnFace). CoUI 프리셋의 네 모드에서 모두 4.65:1 이상입니다(브랜드 팔레트는 재지 않았습니다). 링의 굵기·간격·투명 밴드는 그대로이고,
copyButtonStyle.focusOutlineStyle 에 색을 적으면 그 색이 이깁니다. 복사 버튼 스타일은 Button 에 인스턴스 스타일로 넘어가므로, 프로젝트 Button 테마(variantStyles[plain])에 적은 링 색은 솔리드 면에서 짝 색에 집니다 — 솔리드 면의 링 색을 바꾸려면 CodeSnippet 의
copyButtonStyle(또는 CodeSnippet 테마)에 적습니다.
알려진 제약#
-
스니펫 영역에는 이름이 없습니다 — 양쪽 모두 영역 이름을 따로 붙이지 않습니다. 대안: Web 은
attributespassthrough 가 열려 있어 루트에attributes: {'role': 'region', 'aria-label': '예제 코드'}로 영역 이름을 줄 수 있습니다. Flutter 는 호출부가Semantics(label: …)로 감쌉니다. -
보는 사람에게 줄 톤은 띠의 색으로만 구분됩니다 — 줄 앞 표시(글리프)를 그리지 않습니다. 보조기술에는 톤이 말로 전해지지만, 색을 구분하기 어려운 사용자에게는 성공 띠와 오류 띠가 비슷해 보일 수 있습니다. 대안: 톤의 의미를 코드 주석이나 스니펫 밖의 문장으로 함께 적거나,
+/-표시가 필요하면 CodeDiff 를 씁니다. -
showLineNumbers: true면 줄 번호가 함께 읽힙니다 — 번호 노드가 접근성 트리에서 제외되지 않습니다. Web 은<span>에aria-hidden이 없고, Flutter 는Text('$i')를ExcludeSemantics없이 쌓습니다. 코드 앞뒤로 숫자 나열이 그대로 낭독됩니다. 대안: 스크린 리더 사용자가 주 독자인 문서에서는showLineNumbers: false(기본값)를 유지합니다. -
스크롤되는 코드 영역에 컴포넌트가 만드는 키보드 경로가 없습니다 —
codeSnippetStyle.maxHeight를 주면 Web 은overflow-y: auto를 켜고 Flutter 는 세로·가로SingleChildScrollView를 중첩합니다. 그런데 어느 쪽에도 그 스크롤러에 도달할 Tab 정지점이 없어, 잘린 코드 뒷부분이 키보드만 쓰는 사용자에게 닿지 않을 수 있습니다. 대안: Web 루트에tabindex를 얹어도 실제 스크롤러는 자손인<pre>라 소용이 없습니다. 긴 코드는maxHeight를 두지 않고 전부 펼치는 것이 확실한 회피입니다. -
mode(언어)가 접근성 트리에 노출되지 않습니다 —<code>에lang이나data-language를 붙이지 않고, Flutter 도 언어를 시맨틱으로 내보내지 않습니다. 어떤 언어의 코드인지가 시각적 구문 강조로만 전달됩니다. 대안: 코드 블록 앞에 언어를 밝히는 텍스트를 호출부가 배치합니다.
이 컴포넌트가 직접 다루지 않는 대비·모션 등 전역 축은 전역 접근성 축 에서 한 곳으로 관리됩니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 구문 강조 | mode에 맞춘 highlighter와 SelectableText | 현재 Dart highlighter 고정 + DOM span |
| 클립보드 복사 | Clipboard.setData 완료 뒤 onCopy |
브라우저 쓰기 요청 직후 onCopy |
| 수평 스크롤 | SingleChildScrollView | CSS overflow-x: auto |
| 크기 제약 | codeSnippetStyle 의 min/max 필드 → BoxConstraints |
같은 필드 → CSS min-* / max-* |
| 줄 톤 띠 | 코드 뒤 CustomPaint — 코드와 같은 글꼴 · 줄 높이로 줄 상자를 재서 그 자리에 칠합니다 |
코드 뒤 1lh 높이 행의 열 — 행마다 한 줄이고 띠 줄만 색을 받습니다 |
| 줄 톤 낭독 | 띠 자리의 시맨틱 노드 (순서 키로 코드 앞) | 띠 행 안의 sr-only 텍스트 (마크업 순서로 코드 앞) |
Web mode 는 현재 highlighter 선택에 반영되지 않습니다. 지원 언어를 플랫폼별 제품 옵션으로 정한 것이 아니라 공개 필드의 배선 결손입니다.
레거시 vs 통일 비교 (Migration Notes)#
이전 버전의 Web CoUI 를 참조하는 코드에는 이 컴포넌트가 CodeBlock 이라는 이름이었을 수 있습니다. Flutter 쪽 이름(CodeSnippet)에 맞춰 Web 도
CodeSnippet 으로 개명되었습니다.
| 항목 | 레거시 (구 Web CodeBlock) | 통일 CodeSnippet |
|---|---|---|
| API | 동일 named properties | 동일 named properties |
| 이름만 변경 | — | Flutter 와 동일 이름으로 통일 |
마이그레이션: CodeBlock(...) → CodeSnippet(...) — 파라미터는 그대로이므로 클래스명만 바꾸면 됩니다.