Tree
계층적 데이터를 트리 구조로 표시하고 탐색하는 컴포넌트
목차
파일 탐색기나 조직도처럼 계층적 데이터를 트리 구조로 표시하는 컴포넌트입니다.
기본 사용법 (Basic Usage)#
Tree(
nodes: [
CoreTreeNode(
label: 'Documents',
expanded: true,
children: [
CoreTreeNode(label: 'Resume.pdf'),
CoreTreeNode(label: 'Cover Letter.pdf'),
],
),
CoreTreeNode(
label: 'Images',
children: [
CoreTreeNode(label: 'photo.png'),
],
),
CoreTreeNode(label: 'README.md'),
],
onNodeSelect: (node) => print('selected: ${node.label}'),
onNodeToggle: (node) => print('toggled: ${node.label}'),
)
Live Preview#
코드 보기
class TreeDefaultExample extends StatelessComponent {
const TreeDefaultExample({super.key});
@override
Component build(BuildContext context) {
return const Tree(
nodes: [
CoreTreeNode(
label: 'Documents',
expanded: true,
children: [
CoreTreeNode(label: 'Resume.pdf'),
CoreTreeNode(label: 'Cover Letter.pdf'),
],
),
CoreTreeNode(
label: 'Images',
children: [
CoreTreeNode(label: 'photo.png'),
CoreTreeNode(label: 'banner.jpg'),
],
),
CoreTreeNode(label: 'README.md'),
],
);
}
}
class TreeDefaultExample extends StatelessWidget {
const TreeDefaultExample({super.key});
@override
Widget build(BuildContext context) {
return const Tree(
nodes: [
CoreTreeNode(
label: 'Documents',
expanded: true,
children: [
CoreTreeNode(label: 'Resume.pdf'),
CoreTreeNode(label: 'Cover Letter.pdf'),
],
),
CoreTreeNode(
label: 'Images',
children: [
CoreTreeNode(label: 'photo.png'),
CoreTreeNode(label: 'banner.jpg'),
],
),
CoreTreeNode(label: 'README.md'),
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 파일 탐색기처럼 폴더/파일 계층 구조를 표시할 때
- 조직도, 카테고리 분류처럼 부모-자식 관계의 데이터를 탐색할 때
대신 다른 컴포넌트를 사용하세요:
Accordion: 고정된 수의 섹션을 접기/펼치기로 표시할 때Menu: 계층 없이 평면적인 메뉴 목록에Collapsible: 단일 섹션의 접기/펼치기에
Props / Parameters#
노드 데이터는 CoreTreeNode로 공유합니다. 커스텀 빌더의 반환 콘텐츠 타입 W는 Flutter의 Widget, Web의 Component입니다. virtualRowHeight/virtualViewportHeight와 들여쓰기 치수는 논리 px 단위이며 노드 id와 화면 인덱스는 구분합니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
nodes |
List<CoreTreeNode> |
필수 | 트리 노드 목록 |
onNodeSelect |
ValueChanged<CoreTreeNode>? |
null |
자식이 없는 노드(leaf) 클릭 시 호출 |
onNodeToggle |
ValueChanged<CoreTreeNode>? |
null |
자식이 있는 노드가 펼침/접힘될 때 호출 |
onNodeRename |
CoreTreeRenameCallback? |
null |
인라인 이름 바꾸기 — 더블클릭·포커스된 행의 F2 로 입력칸을 열고, 확정된 새 이름을 전달 |
canRenameNode |
CoreTreeRenamePredicate? |
null |
이름을 바꿀 수 없는 행을 거르는 소비자 정책 (미지정 시 모든 행) |
controller |
CoreTreeController? |
null |
트리 밖에서 이름 바꾸기 시작 —
startRename(id)
가 그 행을 더블클릭·
F2
와 같이 입력칸으로 바꿈 (행 밖 키보드 모델용)
|
nodeContextMenu |
CoreTreeNodeMenuBuilder<W>? |
null |
행 우클릭(터치는 길게 누르기) 메뉴 — 그 노드의
CoreMenuItem
목록을
ContextMenu
로 포인터 위치에 띄움. 빈 목록이면 그 행은 메뉴 없음
|
selectFolders |
bool |
false |
폴더 행 선택과 셰브론 펼침을 분리 |
toggleFoldersOnSelect |
bool |
false |
selectFolders 와 함께 켜면 폴더 행 클릭이 선택과 펼침/접힘을 함께 함 (파일 탐색기 관례) |
expansionControlled |
bool |
false |
매 빌드 CoreTreeNode.expanded 를 따름 |
loadChildren |
Future<List<CoreTreeNode>> Function(CoreTreeNode)? |
null |
빈 가지를 펼칠 때 최초 한 번만 로드, 오류 후 재시도 가능 |
leadingBuilder / trailingBuilder |
CoreTreeNodeBuilder<W>? |
null |
펼침 여부를 받아 앞 아이콘·상태 배지를 그림. trailingBuilder 가 있으면 이름이 남은 폭을 차지해 trailing 이 줄 끝에 맞춰짐 |
loadingBuilder / errorBuilder |
CoreTreeLoadingBuilder<W>? / CoreTreeLoadErrorBuilder<W>? |
null |
지연 적재 상태를 그리는 슬롯 |
virtualRowHeight / virtualViewportHeight |
double? |
null |
둘 다 주면 고정 높이 창 안의 보이는 행만 렌더링 |
nodeKeyPrefix |
String? |
null |
안정적인 id가 있는 행의 Key/id 접두사 |
selectedId |
String? |
null |
지연 적재된 자식을 포함해 ID로 선택 상태 제어 (미지정 시 노드 selected 그대로) |
onNodeDrop |
CoreTreeDropCallback? |
null |
노드를 가지에 놓은 후 호출자에게 이동 의도 전달 |
canDrop |
CoreTreeDropPredicate? |
null |
권한·상태에 따른 소비자 추가 거부 조건 |
treeStyle |
CoreTreeStyle? |
null |
인스턴스 스타일 (Style 시스템 참조) |
스타일 시스템 (Style System)#
Tree 의 모든 chrome / dimensional / nested-slot 오버라이드는 CoreTreeStyle 단일 슬롯으로 흐릅니다. 시맨틱 (onNodeSelect,
onNodeToggle, nodes) 은 위젯 파라미터로 직접 전달합니다.
시맨틱 vs 스타일#
-
시맨틱 / behaviour: 위젯 파라미터로 직접 (
nodes,onNodeSelect,onNodeToggle) -
chrome / dimensional / 슬롯 스타일:
CoreTreeStyle한 곳으로 (nodeBackgroundColor/nodeSelectedColor/nodeHoverColor/nodePadding/nodeMinHeight/indent/indentLineColor/indentLineThickness/nodeTextStyle/iconStyle/leafIconStyle/chevronIconStyle)
Resolve chain#
design system default for tree
→ CoreTreeTheme.style // 프로젝트 공통
→ parent component slot override
→ widget.treeStyle // 인스턴스별
각 nested 슬롯 스타일 (nodeTextStyle / iconStyle / leafIconStyle / chevronIconStyle) 은 자기 컴포넌트 (CoreTextStyle
/ CoreIconStyle) 자체 resolve chain 으로 다시 한 번 머지됩니다.
CoreTreeStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
nodeBackgroundColor |
CoreColor? |
Node row background fill colour. Carries no
defaultNodeBackgroundColor
: an unselected, unhovered tree row paints
no fill
— the file-explorer idiom this component follows, and the reason [nodeSelectedColor] (which does have a default) is the only row fill the design system states. Both platforms express that as absence rather than as a transparent value: Flutter passes it to
BoxDecoration.color
, where null paints nothing, and Web skips
emitColor
entirely (
== null ? null : …
) so no
bg-
class and no inline
background-color
is emitted. A constant here would put a fill behind every row of every tree.
|
nodeSelectedColor |
CoreColor? |
Node row background colour when selected. null defers to [defaultNodeSelectedColor]. |
dropTargetColor |
CoreColor? |
Tint behind a folder row while a dragged node hovers it as a valid drop target.
null
defers to [defaultDropTargetColor]. Independent of [nodeSelectedColor]. A leaf row that is a valid target takes the top edge in [dropIndicatorColor] instead of a tint.
|
dropIndicatorColor |
CoreColor? |
Colour of the drop indicator line — the 2px insertion line between rows and the top edge of a leaf row that is a valid target.
null
defers to [defaultDropIndicatorColor]. Independent of [nodeSelectedColor].
|
nodeHoverColor |
CoreColor? |
Node row background colour on hover. Carries no
defaultNodeHoverColor
: absence means the row has no hover fill, and on Flutter it means the hover
listener is never installed
— the row composes
Clickable(onHover: nodeHover != null ? … : null)
, so a constant would both tint every row on hover and start a
setState
per row per pointer-enter on trees that never asked for it. Web emits
hover:bg-{token}
(or the scoped raw-colour rule) only when the slot is set, so the same constant would add a hover state to every tree there too. Hover feedback on a tree row is opt-in; the cursor and focus ring the composed
Clickable
owns are what a row gets for free.
|
nodePadding | CoreEdgeInsets? | Node row padding. |
nodeMinHeight |
double? |
Node row minimum height (logical px). Defaults to [defaultNodeMinHeight]. |
indent | double? | Per-depth indent (logical px). |
borderRadius |
CoreBorderRadius? |
Node row border radius. null defers to [defaultBorderRadius]. |
iconStyle |
CoreIconStyle? |
Leading folder / file icon style override.
size
defers to [defaultIconStyle]. It does not resize the disclosure chevron — that is [chevronIconStyle]. A
color
given here colours folder and file glyphs alike; [leafIconStyle] overrides the file glyph alone.
|
leafIconStyle |
CoreIconStyle? |
Leaf-row (file) icon style override, applied on top of [iconStyle] for leaf rows only. Chain for a leaf glyph, lowest to highest: the leading default ([defaultIconStyle]) → [defaultLeafIconStyle] (
onSurfaceSubtle
) → the caller's [iconStyle] → this slot. A branch (folder) row never reads it.
|
transitionDuration |
Duration? |
Chevron rotate transition duration. null defers to [defaultTransitionDuration]. |
indentLineColor |
CoreColor? |
Indent line colour. Carries no
defaultIndentLineColor
because this field is the
switch
for the whole indent guide, not just its colour. Web computes
hasIndentLine = indentLineColor != null && depth > 0
and that one boolean gates
border-left-style
,
border-left-width
and
border-left-color
— which is also why [indentLineThickness] can safely have a default while this cannot: the thickness default is only ever reached once this slot is set. A constant here would draw an indent guide down every nested row of every tree. Flutter resolves it (
ResolvedTree.indentLineColor
) but its widget does not paint an indent guide at all, so the guide is Web-only today. That asymmetry is a
cross-platform/api.md
gap to close by painting the guide on Flutter, not by giving this field a value.
|
indentLineThickness |
double? |
Indent line thickness (logical px). |
nodeTextStyle |
CoreTextStyle? |
Node label text style override — unselected state. Role and colour defer to [defaultNodeTextStyle] (
codeSm
,
onSurfaceVariant
).
|
nodeSelectedTextStyle |
CoreTextStyle? |
Node label text style override — selected state. Role and colour defer to [defaultNodeSelectedTextStyle] (
codeSm
,
onSurface
).
|
chevronIconStyle |
CoreIconStyle? |
Disclosure chevron icon style.
size
defers to [defaultChevronIconStyle] (12) — it is its own default and does
not
follow [iconStyle]'s size, so resizing the leading folder / file glyph leaves the chevron alone. The leaf-row [chevronPlaceholderGapStyle] reserves the effective chevron width, so a chevron resized here keeps leaf rows aligned with their siblings.
color
defers to [defaultChevronIconStyle] (
onSurfaceSubtle
) and is the same for every row state — selected or not. Set
color
here to recolour the chevron for every state.
|
chevronPlaceholderGapStyle |
CoreGapStyle? |
Leaf-row chevron slot. Its
size
is the width a leaf row reserves where a parent row draws the disclosure chevron, so leaf rows stay aligned with their siblings. The platform resolvers read it as a fixed slot rather than a spacing: it holds a glyph's place, so the density does not move it. Per-instance override is merged on top of [defaultChevronPlaceholderGapStyle].
|
iconGapStyle |
CoreGapStyle? |
Chevron-slot ↔ folder/file-icon spacer style. Forwarded straight to
Gap(gapStyle: …)
by the platform resolvers (raw forward —
Gap
applies its own scaling). Per-instance override is merged on top of [defaultIconGapStyle].
|
labelGapStyle |
CoreGapStyle? |
Folder/file-icon ↔ label spacer style. Forwarded straight to
Gap(gapStyle: …)
by the platform resolvers (raw forward —
Gap
applies its own scaling). Per-instance override is merged on top of [defaultLabelGapStyle].
|
clickableStyle |
CoreClickableStyle? |
Nested [CoreClickableStyle] slot for the composed per-row
Clickable
(press scale / durations / focus ring / disabled opacity). Merged on top of [defaultClickableStyle] and raw-forwarded — the Clickable's own resolver fills the rest.
|
동작 스펙 (Behavior)#
인터랙션#
- 자식이 있는 노드 클릭: 펼침/접힘 토글.
onNodeToggle호출 - 자식이 없는 노드(leaf) 클릭:
onNodeSelect가 제공된 경우 호출 -
폴더 선택 (
selectFolders): 폴더 행 클릭은onNodeSelect만 부르고 펼침/접힘은 셰브론이 맡습니다.toggleFoldersOnSelect: true를 함께 주면 행 어디를 눌러도 선택과 펼침/접힘이 함께 일어나고, 셰브론은 여전히 펼침/접힘만 합니다 — 폴더를 위임 대상으로 고르면서 행 전체로 여닫는 IDE 파일 트리 관례입니다. -
이름 바꾸기 (
onNodeRename): 행을 더블클릭하거나 행에 포커스가 있을 때F2를 누르면 라벨 자리가 현재 이름이 든 입력칸으로 바뀝니다. leaf 는 마지막 점 앞까지(main.dart→main), 가지는 전체가 선택된 채 열립니다.Enter를 누르거나 입력칸 밖을 누르면(또는 포커스가 빠지면) 확정,Esc는 취소입니다. 앞뒤 공백을 다듬은 이름이 비었거나 그대로면 보고하지 않습니다(CoreTreeRenameRules.committedLabel). 디스크·데이터 반영과 새 노드 전달은 드롭과 같이 소비자가 맡습니다. - 더블클릭과 단일 클릭: 더블클릭의 첫 클릭은 기다리지 않고 평소대로 동작하고(선택·펼침), 두 번째 클릭만 이름 바꾸기로 갑니다 — 단일 클릭이 느려지지 않습니다. 이름을 바꾸는 동안 그 행은 누름·드래그를 받지 않고, 입력칸의 키 입력은 트리 키보드 처리로 새지 않습니다.
-
밖에서 시작 (
controller): 키보드 모델이 행 밖에 있는 소비자(자기 커서를 움직이고 행에는 포커스를 주지 않는 IDE 탐색기 등)는 행 단위F2에 닿지 못합니다.CoreTreeController를 넘기고startRename(노드 id)를 부르면 그 행이 더블클릭·F2와 똑같이 열립니다. 보이지 않는 행(조상이 접힘·없는 id)과canRenameNode가 거부한 행은 무시합니다. 끝나면 포커스가 트리로 돌아옵니다 — Flutter 는 시작 전 포커스(바깥Focus)로, Web 은 그 행(로빙 탭 스톱)으로. -
노드 맥락 메뉴 (
nodeContextMenu): 행을 우클릭(터치는 길게 누르기)하면 그 노드의 항목이 포인터 위치 메뉴로 열립니다(ContextMenu). 우클릭은 행을 선택하지도 펼치지도 않습니다. 빈 목록을 돌려준 노드와 이름을 바꾸는 행에는 메뉴가 없습니다. 「이름 바꾸기」 항목은CoreTreeController.startRename(node.id)로 이으면 됩니다. -
아이콘: 자식 있음 → 펼침
folderOpen/ 접힘folderClosed+ chevron, 없음 →file.leadingBuilder를 주면 그쪽이 우선합니다. -
색: chevron 은 행이 선택됐는지와 상관없이 항상
onSurfaceSubtle입니다(chevronIconStyle.color로 덮음). 파일(leaf) 아이콘은onSurfaceSubtle(leafIconStyle.color로 덮음), 폴더 아이콘은onSurfaceVariant로 한 단계 진하게 그려져 폴더와 파일이 구분됩니다. 이름 글자만 선택 상태에 따라onSurfaceVariant/onSurface로 바뀝니다. -
줄 크기: 행 최소 높이 28px(
nodeMinHeight), 세로 패딩 0, 깊이당 들여쓰기 18px(indent)입니다. 기본 행 높이는 패딩이 아니라nodeMinHeight가 정하며, 라벨 줄 상자에 세로 패딩을 더한 높이가 이를 넘으면 그 높이로 커집니다. -
줄 끝 요소 (
trailingBuilder): 이름이 남은 폭을 모두 차지해(길면 말줄임) trailing 이 줄 끝에 맞춰지고, 이름과 trailing 사이에는labelGapStyle간격이 놓입니다. 깃 변경 표시(M·A·D)처럼 어떤 색·글자를 쓸지는 제품 내용이라 Core 스타일에 두지 않고, 호출 쪽이Text(...)체인으로 직접 그립니다. -
미적재 폴더:
canHaveChildren: true로 선언합니다.children: []만으로는 파일과 빈 폴더를 구분할 수 없습니다.loadChildren성공 후 빈 결과도 캐시하며, 오류 후 행의 재시도 동작을 제공합니다. -
노드 정체성:
id가 있으면 폴더 확장 상태와 행 Key가 형제의 추가·정렬에도 유지됩니다. 없으면 종전 인덱스·라벨 기준입니다. 파일 작업·데이터 갱신은 소비자가 소유합니다. -
드롭:
onNodeDrop을 주면 안정적인id가 있는 노드만 드래그할 수 있습니다. 폴더(canHaveChildren또는 이미 적재된 자식)만 대상이며 자신·적재된 자손 대상은 거부합니다. CoUI의DragScope·DragItem·DropZone이 포인터/키보드 입력과 표시를 맡고, 파일 이동·권한·충돌 검사와 새 노드 전달은 소비자가 맡습니다. 소비자의canDrop으로 작업 중인 항목 같은 추가 제약을 설정하세요.
Tree(
nodes: const [CoreTreeNode(id: 'src', label: 'src', canHaveChildren: true)],
loadChildren: (node) => repository.children(node.id!),
selectFolders: true,
onNodeSelect: (node) => select(node.id!),
leadingBuilder: (context, node, {required expanded}) =>
Icon(expanded ? LucideIcons.folderOpen : LucideIcons.folderClosed),
virtualRowHeight: 36,
virtualViewportHeight: 300,
)
애니메이션#
- 자식이 있는 노드의 chevron 회전:
Duration(milliseconds: CoreDuration.fast)(150ms) - Flutter
AnimatedRotation, Webtransition-transform동일 duration
사용 가이드라인 (Usage Guidelines)#
부모·자식 노드의 펼침·선택·지연 적재·이름 바꾸기가 필요하면 Tree를 사용합니다. 고정 콘텐츠 섹션의 접기/펼치기는 Accordion, 단일 영역은 Collapsible, 명령·이동 항목을 담는 메뉴는 Menu를 선택합니다. Tree는 파일 이동·이름 저장을 직접 수행하지 않고 소비자 콜백에 의도를 전달합니다.
✅ Do#
expanded 는 최초 마운트 시점의 초기값 — 이후 변경은 onNodeToggle 로 추적
Tree(
nodes: nodes, // CoreTreeNode.expanded 는 최초 펼침 상태만 지정
onNodeToggle: (node) {
// 펼침/접힘 결과를 앱 상태에도 반영하고 싶다면 여기서 받는다
trackExpanded(node);
},
)
Tree 는 initState 에서 각 노드의 expanded 값을 한 번만 읽어 내부 Set으로 관리합니다. 이후 펼침 상태를 제어하고 싶다면 onNodeToggle 로 변경을 받아 처리하세요.
❌ Don't#
nodes 를 다시 만들어 expanded 값만 바꾸면 트리가 반응할 거라 기대하지 않기
// ❌ 이미 마운트된 Tree는 새 expanded 값을 다시 읽지 않는다
setState(() {
nodes = [
CoreTreeNode(label: 'Documents', expanded: false, children: [...]),
];
});
화면은 이전 펼침 상태 그대로 남습니다 — expanded 는 첫 빌드에서만 소비되는 초기값이지, 이후 리렌더마다 다시 읽히는 제어값이 아닙니다.
✅ Do#
leaf 노드를 클릭 가능하게 하려면 onNodeSelect 를 명시적으로 전달
Tree(
nodes: [CoreTreeNode(label: 'README.md')], // leaf
onNodeSelect: (node) => openFile(node.label),
)
onNodeSelect 를 주면 자식이 없는 leaf 행이 Clickable 로 합성되어 호버 배경 · 클릭 커서 · 포커스 링을 전부 갖춥니다.
❌ Don't#
onNodeSelect 없이 leaf 행에 호버/클릭 피드백을 기대하지 않기
// ❌ onNodeSelect 를 안 주면 leaf 행은 애초에 Clickable로 감싸지지 않는다
Tree(nodes: nodes)
onNodeSelect 가 없는 leaf 행은 비활성화된 버튼이 아니라 처음부터 인터랙션이 없는 콘텐츠로 렌더링됩니다 — 호버 색이나 커서 변화가 나타나지 않습니다.
접근성 (Accessibility)#
역할#
-
Web —
role="tree"/role="treeitem"과aria-level·aria-posinset·aria-setsize(DOM 이 평평하므로 계층을 행마다 적습니다), 부모 행의aria-expanded, 선택 가능한 행의aria-selected. - Flutter — tree role 없음. Flutter 에는 대응 role 이 없어 계층은 각 노드의 확장 상태로 전달됩니다(설계상 의도된 비대칭).
키보드#
| 키 | Flutter | Web |
|---|---|---|
Enter / Space |
자식 있으면 확장/축소, leaf 면 선택 | 자식 있으면 확장/축소, leaf 면 선택 |
Tab |
노드마다 개별 tab stop | 트리 전체가 tab stop 하나(roving tabindex) |
ArrowUp/ArrowDown |
행 간 포커스 이동 (아래 주의) | 보이는 행 사이 이동 — 끝에서 멈춤 |
ArrowRight | 없음 | 닫힌 부모는 펼침, 열린 부모는 첫 자식으로 |
ArrowLeft | 없음 | 열린 부모는 접음, 그 밖에는 부모 행으로 |
Home/End | 없음 | 첫 행 / 마지막 보이는 행 |
| 문자 입력 | 없음 | 그 글자로 시작하는 다음 행으로(타이핑 점프) |
F2 |
포커스된 행의 이름 바꾸기 (onNodeRename 있을 때) |
활성 행의 이름 바꾸기 (onNodeRename 있을 때) |
이름 입력칸의 Enter / Esc |
확정 / 취소 | 확정 / 취소 — 끝나면 그 행으로 포커스가 돌아옴 |
Web 은 WAI-ARIA 트리 패턴을 따릅니다. 표시 전용 leaf(onNodeSelect 없음)도
treeitem 으로 순회에 포함됩니다 — 키보드가 닿지 못하는 행은 스크린 리더도
읽지 못합니다. 폴더 chevron(selectFolders: true)은 포인터 전용이라 별도 tab
stop 이 아닙니다.
Flutter 의 화살표 이동은 이 컴포넌트가 아니라 프레임워크의 방향성 포커스 순회입니다 — 트리에 스코프되지 않아 트리 밖 요소로도 넘어가고, 확장·축소도 하지 않습니다.
브라우저 검증 — 이 컴포넌트의 Web 키보드 이동은 야간 파리티 스윕의 키보드 프로브(
tool/parity/keyboard_probe.dart)가 하이드레이션된 문서 사이트에서 실제로 키를 눌러, 포커스가 기대한 항목으로 옮겨 갔는지 확인합니다. 핸들러가 달렸는지가 아니라 포커스가 어디에 있는지를 봅니다.
스크린 리더#
Web 은 라벨·treeitem·깊이·형제 중 위치·확장 상태를 읽습니다. aria-selected 는
선택이 의미 있는 행(선택 가능하거나 선택된 행)에만 붙습니다 — 아무도 선택하지 않는
트리에서 모든 행마다 "선택 안 됨"을 읽지 않습니다.
노드를 펼치거나 접은 결과를 알리는 live region 은 없습니다.
포커스 관리#
-
진입 / 트랩 — 트랩 없음.
Tab은 트리에 한 번 들어와 마지막으로 머문 행에 멈추고, 다음Tab은 트리 밖으로 나갑니다(Web). - 노드를 접을 때 — Web 은 tab stop 이 접힌 하위에 있었다면 접힌 부모로 옮깁니다. Flutter 는 사라진 행에 포커스가 있었다면 상위 스코프로 떨어집니다.
알려진 제약#
- Flutter 는 노드 수만큼 tab stop 이 생기고 화살표 확장/축소가 없습니다(Web 과 비대칭).
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | Tree | Tree |
| 렌더링 | Column + 재귀 Padding |
<div> + 재귀 padding-left |
| 아이콘 | Icon(LucideIcons.folderOpen/folderClosed/file) |
Icon(LucideIcons.folderOpen/folderClosed/file) |
| 회전 애니메이션 | AnimatedRotation | transition-transform |
| 테마 | CoreTreeTheme | CoreTreeTheme |
| 노드 빌더 | CoreTreeContract<Widget> — 첫 인자는 Flutter BuildContext |
CoreTreeContract<Component>
— 첫 인자는 Jaspr
BuildContext
· 이름과 나머지 인자는 계약이 두 플랫폼에 같게 고정
|
관련 컴포넌트 (Related Components)#
- Accordion: 고정된 수의 섹션을 그룹으로 접기/펼치기
- Menu: 계층 없는 평면 메뉴 목록
- Collapsible: 단일 섹션의 접기/펼치기
빠른 오버라이드 (Chain)#
이미 만든 Tree 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
.radius4처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius4 ==
CoreRadius.radius4) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.
class TreeChainExample extends StatelessWidget {
const TreeChainExample({super.key});
@override
Widget build(BuildContext context) {
return const Tree(
nodes: [
CoreTreeNode(
label: 'Documents',
expanded: true,
children: [
CoreTreeNode(label: 'Resume.pdf'),
CoreTreeNode(label: 'Cover Letter.pdf'),
],
),
CoreTreeNode(
label: 'Images',
children: [
CoreTreeNode(label: 'photo.png'),
CoreTreeNode(label: 'banner.jpg'),
],
),
CoreTreeNode(label: 'README.md'),
],
)
.withStyle(
const CoreTreeStyle(
nodeBackgroundColor: CoreColor.token(CoreColors.surfaceContainer),
nodePadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space12,
vertical: CoreSpace.space8,
),
indent: CoreSpace.space24,
),
)
.radius16;
}
}
class TreeChainExample extends StatelessComponent {
const TreeChainExample({super.key});
@override
Component build(BuildContext context) {
return const Tree(
nodes: [
CoreTreeNode(
label: 'Documents',
expanded: true,
children: [
CoreTreeNode(label: 'Resume.pdf'),
CoreTreeNode(label: 'Cover Letter.pdf'),
],
),
CoreTreeNode(
label: 'Images',
children: [
CoreTreeNode(label: 'photo.png'),
CoreTreeNode(label: 'banner.jpg'),
],
),
CoreTreeNode(label: 'README.md'),
],
)
.withStyle(
const CoreTreeStyle(
nodeBackgroundColor: CoreColor.token(CoreColors.surfaceContainer),
nodePadding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space12,
vertical: CoreSpace.space8,
),
indent: CoreSpace.space24,
),
)
.radius16;
}
}