FileInput
파일 입력 컴포넌트
목차
파일 선택/업로드를 위한 점선 테두리 드롭존 컴포넌트입니다. Flutter/Web 양쪽에서 동일한 API(FileInput)를 제공합니다.
기본 사용법 (Basic Usage)#
Flutter에서는 package:coui_flutter/coui_flutter.dart에서 그대로 import합니다.
파일 선택은 file_picker ^12.2.0을 사용하며 iOS 14.0 이상이 필요합니다.
onFilesSelected는 List<XFile>을 받습니다. 선택을 취소하면 호출되지 않으며,
파일 내용은 필요할 때 readAsBytes() 또는 openRead()로 읽을 수 있습니다.
// 기본 파일 입력
FileInput(
onFilesSelected: (files) => print('Selected: ${files.length}'),
)
// 이미지만 허용
FileInput(
accept: ['image/*'],
onFilesSelected: handleImageSelected,
)
// 다중 파일 + 선택 목록 표시
FileInput(
multiple: true,
accept: ['.pdf', '.doc', '.docx'],
hint: 'PDF, DOC up to 10MB',
selectedFiles: selectedFileNames,
onFilesSelected: handleFilesSelected,
)
// 기본 파일 입력
FileInput(
onFilesSelected: (fileList) => print('Files: ${fileList.length}'),
)
// 이미지만 허용
FileInput(
accept: ['image/*'],
onFilesSelected: handleImageSelected,
)
// 다중 파일 + 힌트 + 선택 목록
FileInput(
multiple: true,
accept: ['.pdf', '.doc', '.docx'],
hint: 'PDF, DOC up to 10MB',
selectedFiles: selectedFileNames,
onFilesSelected: handleFilesSelected,
)
빠른 오버라이드 (Chain)#
이미 만든 FileInput 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
.radius16처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius16 ==
CoreRadius.radius16) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.
class FileInputChainExample extends StatefulWidget {
const FileInputChainExample({super.key});
@override
State<FileInputChainExample> createState() => _FileInputChainExampleState();
}
class _FileInputChainExampleState extends State<FileInputChainExample> {
List<String> _selectedFiles = const [];
void handleFilesSelected(List<XFile> files) {
setState(() {
_selectedFiles = files.map((f) => f.name).toList();
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
FileInput(
onFilesSelected: handleFilesSelected,
multiple: true,
hint: 'PNG, JPG, PDF up to 10MB',
selectedFiles: _selectedFiles,
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
FileInput(
onFilesSelected: handleFilesSelected,
multiple: true,
hint: 'PNG, JPG, PDF up to 10MB',
selectedFiles: _selectedFiles,
).withStyle(
const CoreFileInputStyle(
backgroundColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space24,
vertical: CoreSpace.space8,
),
),
),
],
);
}
}
class FileInputChainExample extends StatefulComponent {
const FileInputChainExample({super.key});
@override
State<FileInputChainExample> createState() => _FileInputChainExampleState();
}
class _FileInputChainExampleState extends State<FileInputChainExample> {
List<String> _selectedFiles = const [];
@override
Component build(BuildContext context) {
return div(
[
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
FileInput(
onFilesSelected: (files) {
setState(() {
_selectedFiles = [
for (var i = 0; i < files.length; i++) files.item(i)!.name,
];
});
},
multiple: true,
hint: 'PNG, JPG, PDF up to 10MB',
selectedFiles: _selectedFiles,
).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(padding)까지 한 번에.
FileInput(
onFilesSelected: (files) {
setState(() {
_selectedFiles = [
for (var i = 0; i < files.length; i++) files.item(i)!.name,
];
});
},
multiple: true,
hint: 'PNG, JPG, PDF up to 10MB',
selectedFiles: _selectedFiles,
).withStyle(
const CoreFileInputStyle(
backgroundColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space24,
vertical: CoreSpace.space8,
),
),
),
],
classes: 'flex flex-col items-start',
);
}
}
Live Preview#
코드 보기
class FileInputDefaultExample extends StatefulComponent {
const FileInputDefaultExample({super.key});
@override
State<FileInputDefaultExample> createState() =>
_FileInputDefaultExampleState();
}
class _FileInputDefaultExampleState extends State<FileInputDefaultExample> {
List<String> _selectedFiles = const [];
@override
Component build(BuildContext context) {
return FileInput(
onFilesSelected: (files) {
setState(() {
_selectedFiles = [
for (var i = 0; i < files.length; i++) files.item(i)!.name,
];
});
},
multiple: true,
hint: 'PNG, JPG, PDF up to 10MB',
selectedFiles: _selectedFiles,
);
}
}
class FileInputDefaultExample extends StatefulWidget {
const FileInputDefaultExample({super.key});
@override
State<FileInputDefaultExample> createState() =>
_FileInputDefaultExampleState();
}
class _FileInputDefaultExampleState extends State<FileInputDefaultExample> {
List<String> _selectedFiles = const [];
void handleFilesSelected(List<XFile> files) {
setState(() {
_selectedFiles = files.map((f) => f.name).toList();
});
}
@override
Widget build(BuildContext context) {
return FileInput(
onFilesSelected: handleFilesSelected,
multiple: true,
hint: 'PNG, JPG, PDF up to 10MB',
selectedFiles: _selectedFiles,
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 사용자가 파일을 업로드해야 할 때 (이미지, 문서 등)
- 프로필 사진, 첨부파일 등 파일 선택이 필요할 때
- 폼에서 파일 필드를 포함해야 할 때
대신 다른 컴포넌트를 사용하세요:
Props / Parameters#
W 는 Flutter Widget / Web Jaspr Component 입니다. 파일 콜백의 F
는 Flutter List<XFile> / Web FileList 로, 파일 데이터의 플랫폼 표현을 뜻하는 설명용 기호입니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
onFilesSelected |
void Function(F)? |
null |
필터를 통과한 파일 데이터 전달 |
accept |
List<String>? |
null |
허용 확장자·MIME (예: ['image/*', '.pdf']) |
multiple |
bool |
false |
파일 선택 다이얼로그의 다중 선택 |
allowDirectory |
bool |
false |
폴더 선택 버튼 표시 |
allowSave |
bool |
false |
데이터가 기록된 받은 파일 행의 저장 버튼 |
enabled |
bool |
true |
선택·드롭 가능 여부 |
label |
String? |
null → 로케일 |
드롭존 안내 문구 |
labelMaxLines |
int? |
null |
라벨 줄 수 제한 |
labelOverflow |
CoreTextOverflow? |
null |
라벨 넘침 처리 |
hint | String? | null | 보조 힌트 |
icon |
W? |
null → 업로드 아이콘 |
커스텀 아이콘 |
browseButtonText |
String? |
null → 로케일 |
파일 선택 버튼 레이블 |
selectedFiles |
List<String>? |
null |
부모가 전달하는 표시용 파일 이름 목록 |
name |
String? |
null |
Web native 파일 입력 이름 |
fileInputStyle |
CoreFileInputStyle? |
null |
chrome 단일 진입점 |
CoreFileInputStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
padding | CoreEdgeInsets? | Inner padding. |
itemSpacing |
double? |
Spacing between vertical elements (icon / label / button / hint) — drives
Column.spacing
(Flutter) and CSS
gap
on the flex column root (Web).
|
textSpacing |
double? |
Spacing between the title and the hint inside the text block (logical px).
null
→ [defaultTextSpacing].
|
actionSpacing |
double? |
Spacing between the text block (title + hint) and the action buttons (logical px).
null
→ [defaultActionSpacing].
|
iconBoxSize |
double? |
Edge length of the circular box the upload icon sits in (logical px, pre-scaling).
null
→ [defaultIconBoxSize].
|
iconBoxColor |
CoreColor? |
Fill of the icon box at rest. null → [defaultIconBoxColor]. |
dragActiveIconBoxColor |
CoreColor? |
Fill of the icon box while a file is dragged over the zone.
null
→ [defaultDragActiveIconBoxColor].
|
iconBoxBorderRadius |
CoreBorderRadius? |
Corner radius of the icon box.
null
→ [defaultIconBoxBorderRadius] (fully rounded — a circle).
|
borderRadius |
CoreBorderRadius? |
Outer border radius. |
borderWidth |
double? |
Outer border stroke width (logical px). |
dashLength |
double? |
Dash segment length of the dashed drop-zone border (logical px).
null
→ [defaultDashLength].
|
dashSpacing |
double? |
Gap between dash segments (logical px). null → [defaultDashSpacing]. |
backgroundColor |
CoreColor? |
Container background colour. |
borderColor |
CoreColor? |
Outer border stroke colour. |
hoverBorderColor |
CoreColor? |
Outer border stroke colour while the pointer hovers the zone and no file is being dragged over it.
null
→ [defaultHoverBorderColor]. Drag-over keeps its own [dragOverBorderColor]; a disabled zone never takes this.
|
fileItemSpacing |
double? |
Spacing (and bottom margin) between selected file items — drives
Row.spacing
(Flutter) and CSS
gap
on each per-file row (Web), plus the bottom margin of each row.
|
transitionDuration |
Duration? |
Transition duration for the hover / focus colour crossfade on the drop-zone container. |
iconStyle |
CoreIconStyle? |
Icon slot style (size + colour). |
fileIconStyle |
CoreIconStyle? |
Selected-file icon slot style (size + colour) — drives the per-file row icon glyph. |
labelTextStyle |
CoreTextStyle? |
Primary label text style override. |
hintTextStyle |
CoreTextStyle? |
Hint text style override. |
clickableStyle |
CoreClickableStyle? |
Nested style for the composed drop-zone
Clickable
— carries any press / focus-ring / cursor chrome overrides for the drop-zone affordance. Merged on top of [defaultClickableStyle] and raw-forwarded to
Clickable(clickableStyle:)
.
|
saveButtonStyle |
CoreButtonStyle? |
Nested style for the per-file save affordance's composed
Button
— merged on top of [defaultSaveButtonStyle] and raw-forwarded to
Button(buttonStyle:)
. Null means "no per-instance override"; the affordance still renders with the design-system default.
|
contentColor |
CoreColor? |
Primary-label content colour override. null → [defaultContentColor]. |
disabledContentColor |
CoreColor? |
Disabled content colour override. null → [defaultDisabledContentColor]. |
hintColor |
CoreColor? |
Hint content colour override. null → [defaultHintColor]. |
fileNameColor |
CoreColor? |
Selected-file name colour override. null → [defaultFileNameColor]. |
subtleIconColor |
CoreColor? |
Subtle upload-icon colour override (enabled). null → [defaultSubtleIconColor]. |
dragActiveIconColor |
CoreColor? |
Upload-icon colour while a file is dragged over the zone (enabled).
null
→ [defaultDragActiveIconColor].
|
fileIconColor |
CoreColor? |
Selected-file icon colour override (enabled). null → [defaultFileIconColor]. |
disabledBorderColor |
CoreColor? |
Disabled dashed-border colour override. null → [defaultDisabledBorderColor]. |
disabledBackgroundColor |
CoreColor? |
Disabled zone fill override. null → [defaultDisabledBackgroundColor]. |
disabledIconBoxColor |
CoreColor? |
Disabled icon-box fill override. null → [defaultDisabledIconBoxColor]. |
dragOverBorderColor |
CoreColor? |
Drag-over dashed-border colour override. null → [defaultDragOverBorderColor]. |
dragActiveBackgroundColor |
CoreColor? |
Drag-active background tint override. null → [defaultDragActiveBackgroundColor]. |
fileTextStyle |
CoreTextStyle? |
Selected-file name text style override. null → [defaultFileTextStyle]. |
스타일 시스템 (Style System)#
테마 커스터마이징 (Theme)#
CoreFileInputTheme은 style 슬롯 하나를 가지며, 프로젝트 공통 chrome 이 그 슬롯으로 흐릅니다.
CoreComponentTheme(
fileInput: CoreFileInputTheme(
style: CoreFileInputStyle(
borderRadius: CoreBorderRadius.all(CoreRadius.radius12),
padding: CoreEdgeInsets.symmetric(
vertical: CoreSpace.space32,
horizontal: CoreSpace.space24,
),
itemSpacing: CoreSpace.space12,
iconStyle: CoreIconStyle(size: CoreIconSize.size24),
),
),
)
Resolve 우선순위: widget.fileInputStyle > CoreFileInputTheme.style
> CoreFileInputStyle.default*.
동작 스펙 (Behavior)#
파일 선택#
- 드롭존 전체 영역을 클릭하면 시스템 파일 선택 다이얼로그가 열립니다
accept로 허용 파일 타입 필터링multiple: true시 여러 파일 동시 선택 가능- 선택된 파일은
onFilesSelected콜백으로 전달 -
선택 취소·필터 결과가 비면 새 콜백을 내지 않습니다. 업로드 요청이나
selectedFiles목록 갱신은 호출부가 담당하며multiple: false가 드롭·폴더 결과 목록까지 한 개로 잘라 주지는 않습니다
폴더 선택#
-
allowDirectory: true면 드롭존에 폴더 선택 버튼이 추가됩니다. 폴더를 고르면 그 안의 파일이 재귀적으로onFilesSelected로 전달됩니다 -
Flutter 는
dir_picker(macOS · Windows · Linux · Android SAF · iOS), Web 은 File System Access API(showDirectoryPicker)를 씁니다. FSA 를 지원하지 않는 브라우저(Firefox · Safari)에서는 버튼이 비활성으로 렌더됩니다 - 폴더 안 파일도
accept필터를 거칩니다 — 목록에 맞지 않는 파일은 조용히 제외되고, 하나도 맞지 않으면 콜백이 호출되지 않습니다 -
Android 의 SAF 는 파일 경로 대신
content://URI 를 줍니다. 이 경우XFile의 경로가 그 URI 이므로, 바이트가 필요하면 앱의 SAF 레이어로 읽어야 합니다
파일 저장#
-
allowSave: true면 컴포넌트가 실제로 받은 파일(선택 · 드롭 · 폴더)의 행에 저장 버튼이 붙습니다. 누르면 Flutter 는 시스템 저장 다이얼로그(file_saver_ffi), Web 은 브라우저 다운로드로 내보냅니다 -
앱이 밖에서 넣은
selectedFiles(이름만)에는 파일 데이터가 없어 저장 버튼이 붙지 않습니다 — 그 경우는CoFileSaver유틸을 직접 쓰세요- Flutter:
CoFileSaver.saveBytes(bytes:, fileName:, mimeType:)·CoFileSaver.saveFile(path:, fileName:) - Web:
CoFileSaver.saveBytes(bytes:, fileName:, mimeType:)
- Flutter:
- 같은 이름의 파일이 여러 번 들어오면 마지막 것이 저장 대상입니다
드래그 앤 드롭#
OS 파일을 드롭존 위로 끌어오면 테두리·배경이 drag-over 상태로 바뀌고, 놓으면
onFilesSelected로 전달됩니다-
Flutter는
desktop_drop(macOS · Windows · Linux · Web · Android preview), Web은 HTML5 drop 으로 받습니다. 드롭을 지원하지 않는 플랫폼(iOS)에서는 파일 선택 버튼만 동작합니다 Web에서 폴더를 떨어뜨리면 그 안의 파일을 재귀적으로 모아 전달합니다
드롭된 파일에도
accept필터가 적용됩니다 — 목록에 맞지 않는 파일은 조용히 제외되고, 하나도 맞지 않으면 콜백이 호출되지 않습니다-
드래그 하이라이트는 실제 드래그 진입에만 반응합니다 (포인터 호버로는 켜지지 않습니다 — 호버는 아래
onSurface테두리로만 반응하고,primary테두리와 배경 틴트는 드래그 전용입니다) -
Figma 의
Show file은 코드 파라미터가 아니라selectedFiles가 비었는지에 대응하는 캔버스 표현입니다 — 켜면 선택된 파일 줄이 보이고, 끄면selectedFiles가null· 빈 목록일 때의 모양입니다
시각 피드백#
-
호버 시: 쉬고 있는 점선 테두리가
outlineVariant에서onSurface로 한 단계 진해집니다 (hoverBorderColor). 배경은 변하지 않습니다. 우선순위는disabled > drag-over > hover > idle— 비활성이면 호버 단계가 없고, 실제 드래그 중에는primary테두리가 포인터 아래에서도 유지됩니다 - 드래그 진입 시: 테두리 색상이
primary로 강조되고, 배경에primary/5틴트가 적용됩니다 -
비활성 시: 영역과 아이콘 상자는
disabledContainer면, 점선 테두리는disabledOutline, 글자와 아이콘은disabledContent토큰으로 칠해지고(투명도를 따로 걸지 않습니다), 포인터 입력은 차단됩니다 -
점선 테두리: 점 6 / 틈 4 / 두께 1 의 드롭 영역 고유 리듬(
CoreFileInputStyle.defaultDashLength·defaultDashSpacing)으로 Flutter/Web 동일한 시각 렌더링. 쉴 때는outlineVariant, 파일을 끌어 올리면primary. 공용CoreDashedBorder(3 / 2)는 그대로라 다른 점선은 움직이지 않습니다 -
구조: 지름 48 의 원형 아이콘 상자(
surfaceContainerHigh, 드래그 중에는primaryContainer) 안에 24 아이콘 → 12 → 제목 · 안내문(사이 4) → 16 → 버튼 순서로 쌓입니다. 영역은 위아래 여백 32 · 좌우 여백 24 · 모서리 12 입니다 (iconBoxSize·iconBoxColor·dragActiveIconBoxColor·iconBoxBorderRadius·textSpacing·actionSpacing·padding). 안내문(hint)은 버튼 아래가 아니라 제목 바로 아래에 놓입니다 -
대비: 영역 경계는 옅은 점선(
outlineVariant)이 의도입니다. 조작부의 경계(WCAG 1.4.11)는 안의 Browse files 버튼 자체 테두리가 충족하므로, 점선을 더 진하게 되돌리지 않습니다
사용 가이드라인 (Usage Guidelines)#
✅ Do#
accept 속성으로 허용 파일 타입을 명시하세요.
// 이미지만
FileInput(accept: ['image/*'], onFilesSelected: handleImage)
// 특정 문서 형식
FileInput(accept: ['.pdf', '.doc', '.docx'], onFilesSelected: handleDoc)
사용자가 잘못된 파일을 선택하는 실수를 줄입니다.
❌ Don't#
파일 크기 검증 없이 업로드를 허용하지 마세요.
onFilesSelected 콜백에서 파일 크기를 확인하고, 초과 시 사용자에게 안내하세요.
✅ Do#
선택된 파일 목록을 selectedFiles로 표시하세요.
FileInput(
multiple: true,
selectedFiles: selectedFileNames,
onFilesSelected: (files) {
setState(() {
selectedFileNames = files.map((f) => f.name).toList();
});
},
)
사용자가 어떤 파일을 선택했는지 즉시 확인할 수 있습니다.
접근성 (Accessibility)#
역할#
-
Web: 드롭존 자체가 활성화 가능한 컨트롤입니다 — 루트
<div>에role="button"·tabindex="0"·aria-label(값은label, 없으면 로컬라이즈된 드롭 문구)이 붙고,enabled: false면aria-disabled="true"와data-disabled가 함께 실립니다. 실제 파일 선택을 맡는<input type="file">은tabindex="-1"·aria-hidden="true"로 감춰지며accept/multiple/name/disabled를 그 요소가 들고 있습니다. 대시 보더<svg>도aria-hidden="true"이고, 안쪽 파일 선택 버튼은 평범한<button type="button">입니다. -
Flutter: 드롭존은
Clickable이 얹는Semantics(button: true, enabled: ...)이며 라벨은 비어 있습니다. 파일 선택은 숨은 native 입력이 아니라 OS 파일 선택기를 직접 여는 경로라 대응하는 감춰진 요소가 없습니다. -
Web 루트
attributes는 컴포넌트가 미리 넣은 값보다 뒤에 병합되므로, 호출부가aria-label이나role을 덮어쓸 수 있습니다.
키보드#
| 키 | 동작 |
|---|---|
Tab | 드롭존으로, 이어서 그 안의 파일 선택 버튼으로 이동 |
Shift+Tab | 역순으로 이동 |
Enter / Space | 포커스된 쪽(드롭존 또는 파일 선택 버튼)에서 파일 선택 대화상자 열기 |
Web 의 드롭존은 자기 자신이 포커스일 때만 키 활성화를 받아들여, 안쪽 버튼에서 올라온 키 입력으로 대화상자가 두 번 열리지 않습니다. 드래그 앤 드롭에는 키보드 등가 동작이 필요하지 않습니다 — 같은 결과(파일 선택)를 위 키로 얻을 수 있습니다.
스크린 리더#
-
접근 가능한 이름: Web 은 드롭존의
aria-label이 이름이 되고 값은label파라미터(없으면 로케일의 "Drop files here")입니다. Flutter 에는 그 이름이 없습니다 — 아래 제약 참조. -
화면에 보이는 라벨 ·
hint·selectedFiles목록은 드롭존 버튼 노드에 병합되지 않는 별개의 텍스트로, 버튼을 지난 뒤 따로 읽힙니다. -
enabled: false는 Webaria-disabled="true"/ FlutterSemantics(enabled: false)로 전달됩니다. - 선택 결과를 알리는 라이브 리전은 없습니다 — 아래 제약 참조.
포커스 관리#
- 진입: 오버레이가 아니어서 렌더 시 포커스를 옮기지 않습니다.
- 이탈: 파일 선택 대화상자는 OS / 브라우저가 소유하므로 컴포넌트가 포커스를 넘기지도, 닫힌 뒤 되돌리지도 않습니다. 대화상자를 닫은 다음 포커스가 어디로 가는지는 플랫폼이 결정합니다.
- 트랩: 없습니다.
Tab은 드롭존과 파일 선택 버튼을 지나 그대로 바깥으로 나갑니다.
알려진 제약#
-
숨은
<input type="file">은 보조기술에 노출되지 않습니다(Web). 그 요소에는tabindex="-1"과aria-hidden="true"가 함께 붙어 있어, 거기 실린accept/multiple/name은 스크린 리더가 읽지 못합니다. 실제로 포커스를 받고 안내되는 것은 바깥 드롭존이고 그쪽에는 허용 파일 타입·다중 선택 여부가 담기지 않으므로, 그 정보가 필요하면hint나label텍스트로 직접 적어 주어야 합니다. -
Flutter 드롭존에는 접근 가능한 이름이 없습니다. Web 은 드롭존에
aria-label을 붙이지만, Flutter 는Clickable이 얹는Semantics(enabled:, button:)뿐이라 라벨이 비어 있어 리더가 "버튼" 이라고만 읽습니다. 드롭 라벨 텍스트는 그 버튼 노드에 병합되지 않는 별개의 자식이며,FileInput이 시맨틱 슬롯을 노출하지 않아 호출부가 이름을 채울 방법이 없습니다. -
하나의 동작에 tab stop 이 두 개 생깁니다(양 플랫폼). 드롭존 자체가 활성화 가능한 컨트롤인데(Web
role="button"+tabindex="0", FlutterClickable) 그 안에 파일 선택Button이 또 들어 있어, 같은 "파일 열기" 동작이 버튼 두 개로 안내되고 Tab 을 두 번 눌러야 지나갑니다. -
선택된 파일이 바뀌어도 통보되지 않습니다.
selectedFiles목록은 양 플랫폼 모두 라이브 리전이 아닌 평범한 텍스트 행으로 렌더되므로, 파일을 끌어다 놓거나 고른 뒤 결과를 확인하려면 사용자가 직접 그 영역으로 이동해 읽어야 합니다.
알려진 제약#
-
파일별 저장 버튼은 16×16 으로 그려지지만, 포인터는 최소 24 까지 받습니다.
그리는 크기는 그대로 두고 닿는 범위만 넓힙니다 — 빗나간 탭이 드롭존으로 떨어져
파일 선택기가 열리는 것을 막기 위해서입니다. Web 은 네 방향 모두 넓어집니다.
Flutter 는 버튼이 파일 행의 끝에 있고 행 높이가 버튼과 같아서 그 확장이 파일명
쪽으로만 닿습니다 — 행 박스가 끝나는 자리 너머로는 어떤 하한도 뻗지 못합니다.
그리는 크기를 키우려면
saveButtonStyle.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 파일 결과 | List<XFile> | 브라우저 FileList |
| 파일 선택 | 플랫폼 FilePicker | 숨은 <input type="file"> |
| OS 드롭 | desktop_drop의 파일 이벤트 | HTML5 dataTransfer |
| 호버 감지 | Clickable.onHover가 위젯 상태로 전달 |
CSS :hover — 위젯 상태 없음 |
| 폴더 선택 | DirPicker · 플랫폼 URI/파일 수집 | 지원되는 File System Access API |
| 저장 | 시스템 저장 다이얼로그 | 브라우저 다운로드 |
Web 폴더 선택 경로는 현재 받은 파일을 저장용 _received 맵에 기록하지 않습니다. 폴더에서 받은 파일의 allowSave 는 배선 결손이며 지원되는 저장 차이로 사용하지 않습니다.