FileInput | CoUI
본문으로 건너뛰기
LogoCoUI

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#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 사용자가 파일을 업로드해야 할 때 (이미지, 문서 등)
  • 프로필 사진, 첨부파일 등 파일 선택이 필요할 때
  • 폼에서 파일 필드를 포함해야 할 때

대신 다른 컴포넌트를 사용하세요:

  • TextField: 파일 데이터 대신 문자열을 입력받을 때
  • Button: 드롭존·파일 목록이 필요 없고 호출부가 파일 선택 동작을 직접 배선할 때

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 라벨 넘침 처리
hintString?null보조 힌트
icon W? null → 업로드 아이콘 커스텀 아이콘
browseButtonText String? null → 로케일 파일 선택 버튼 레이블
selectedFiles List<String>? null 부모가 전달하는 표시용 파일 이름 목록
name String? null Web native 파일 입력 이름
fileInputStyle CoreFileInputStyle? null chrome 단일 진입점

CoreFileInputStyle 필드#

필드타입설명
paddingCoreEdgeInsets?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:)
  • 같은 이름의 파일이 여러 번 들어오면 마지막 것이 저장 대상입니다

드래그 앤 드롭#

  • 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 는 Web aria-disabled="true" / Flutter Semantics(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", Flutter Clickable) 그 안에 파일 선택 Button 이 또 들어 있어, 같은 "파일 열기" 동작이 버튼 두 개로 안내되고 Tab 을 두 번 눌러야 지나갑니다.
  • 선택된 파일이 바뀌어도 통보되지 않습니다. selectedFiles 목록은 양 플랫폼 모두 라이브 리전이 아닌 평범한 텍스트 행으로 렌더되므로, 파일을 끌어다 놓거나 고른 뒤 결과를 확인하려면 사용자가 직접 그 영역으로 이동해 읽어야 합니다.

알려진 제약#

  • 파일별 저장 버튼은 16×16 으로 그려지지만, 포인터는 최소 24 까지 받습니다. 그리는 크기는 그대로 두고 닿는 범위만 넓힙니다 — 빗나간 탭이 드롭존으로 떨어져 파일 선택기가 열리는 것을 막기 위해서입니다. Web 은 네 방향 모두 넓어집니다. Flutter 는 버튼이 파일 행의 끝에 있고 행 높이가 버튼과 같아서 그 확장이 파일명 쪽으로만 닿습니다 — 행 박스가 끝나는 자리 너머로는 어떤 하한도 뻗지 못합니다. 그리는 크기를 키우려면 saveButtonStyle.

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
파일 결과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 는 배선 결손이며 지원되는 저장 차이로 사용하지 않습니다.

  • TextField: 텍스트 입력. FileInput은 파일 선택 전용
  • Form: 폼 컨테이너. FileInput을 Form 내에서 사용
  • Button: 내부 Browse files 버튼으로 Button(variant: outline) 사용