sh-ui-cli 0.15.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/bin/sh-ui.mjs +6 -0
  2. package/data/changelog/versions.json +354 -0
  3. package/data/registry/flutter/foundation/sh_ui_tokens.dart +385 -0
  4. package/data/registry/flutter/registry.json +336 -0
  5. package/data/registry/flutter/widgets/sh_ui_accordion.dart +255 -0
  6. package/data/registry/flutter/widgets/sh_ui_app_shell.dart +267 -0
  7. package/data/registry/flutter/widgets/sh_ui_avatar.dart +95 -0
  8. package/data/registry/flutter/widgets/sh_ui_badge.dart +82 -0
  9. package/data/registry/flutter/widgets/sh_ui_breadcrumb.dart +107 -0
  10. package/data/registry/flutter/widgets/sh_ui_button.dart +201 -0
  11. package/data/registry/flutter/widgets/sh_ui_card.dart +159 -0
  12. package/data/registry/flutter/widgets/sh_ui_carousel.dart +204 -0
  13. package/data/registry/flutter/widgets/sh_ui_checkbox.dart +154 -0
  14. package/data/registry/flutter/widgets/sh_ui_color_picker.dart +264 -0
  15. package/data/registry/flutter/widgets/sh_ui_combobox.dart +614 -0
  16. package/data/registry/flutter/widgets/sh_ui_context_menu.dart +71 -0
  17. package/data/registry/flutter/widgets/sh_ui_date_picker.dart +648 -0
  18. package/data/registry/flutter/widgets/sh_ui_dialog.dart +567 -0
  19. package/data/registry/flutter/widgets/sh_ui_dropdown_menu.dart +251 -0
  20. package/data/registry/flutter/widgets/sh_ui_file_upload.dart +200 -0
  21. package/data/registry/flutter/widgets/sh_ui_header.dart +488 -0
  22. package/data/registry/flutter/widgets/sh_ui_input.dart +664 -0
  23. package/data/registry/flutter/widgets/sh_ui_label.dart +145 -0
  24. package/data/registry/flutter/widgets/sh_ui_menubar.dart +98 -0
  25. package/data/registry/flutter/widgets/sh_ui_pagination.dart +276 -0
  26. package/data/registry/flutter/widgets/sh_ui_popover.dart +248 -0
  27. package/data/registry/flutter/widgets/sh_ui_progress.dart +47 -0
  28. package/data/registry/flutter/widgets/sh_ui_radio.dart +108 -0
  29. package/data/registry/flutter/widgets/sh_ui_select.dart +904 -0
  30. package/data/registry/flutter/widgets/sh_ui_separator.dart +42 -0
  31. package/data/registry/flutter/widgets/sh_ui_sidebar.dart +1116 -0
  32. package/data/registry/flutter/widgets/sh_ui_skeleton.dart +129 -0
  33. package/data/registry/flutter/widgets/sh_ui_slider.dart +147 -0
  34. package/data/registry/flutter/widgets/sh_ui_spinner.dart +56 -0
  35. package/data/registry/flutter/widgets/sh_ui_switch.dart +109 -0
  36. package/data/registry/flutter/widgets/sh_ui_tabs.dart +329 -0
  37. package/data/registry/flutter/widgets/sh_ui_textarea.dart +126 -0
  38. package/data/registry/flutter/widgets/sh_ui_toast.dart +362 -0
  39. package/data/registry/flutter/widgets/sh_ui_toggle.dart +229 -0
  40. package/data/registry/flutter/widgets/sh_ui_tooltip.dart +62 -0
  41. package/data/registry/react/components/accordion/index.tsx +85 -0
  42. package/data/registry/react/components/accordion/styles.css +94 -0
  43. package/data/registry/react/components/animations/animations.css +51 -0
  44. package/data/registry/react/components/avatar/index.tsx +75 -0
  45. package/data/registry/react/components/avatar/styles.css +36 -0
  46. package/data/registry/react/components/badge/index.tsx +42 -0
  47. package/data/registry/react/components/badge/styles.css +57 -0
  48. package/data/registry/react/components/base/base.css +102 -0
  49. package/data/registry/react/components/breadcrumb/index.tsx +154 -0
  50. package/data/registry/react/components/breadcrumb/styles.css +82 -0
  51. package/data/registry/react/components/breakpoints/breakpoints.css +17 -0
  52. package/data/registry/react/components/button/index.tsx +47 -0
  53. package/data/registry/react/components/button/styles.css +93 -0
  54. package/data/registry/react/components/card/index.tsx +86 -0
  55. package/data/registry/react/components/card/styles.css +73 -0
  56. package/data/registry/react/components/carousel/index.tsx +432 -0
  57. package/data/registry/react/components/carousel/styles.css +155 -0
  58. package/data/registry/react/components/checkbox/index.tsx +98 -0
  59. package/data/registry/react/components/checkbox/styles.css +75 -0
  60. package/data/registry/react/components/code-panel/copy.tsx +56 -0
  61. package/data/registry/react/components/code-panel/index.tsx +193 -0
  62. package/data/registry/react/components/code-panel/styles.css +124 -0
  63. package/data/registry/react/components/color-picker/index.tsx +466 -0
  64. package/data/registry/react/components/color-picker/styles.css +166 -0
  65. package/data/registry/react/components/combobox/index.tsx +167 -0
  66. package/data/registry/react/components/combobox/styles.css +151 -0
  67. package/data/registry/react/components/context-menu/index.tsx +253 -0
  68. package/data/registry/react/components/context-menu/styles.css +140 -0
  69. package/data/registry/react/components/date-picker/index.tsx +757 -0
  70. package/data/registry/react/components/date-picker/styles.css +279 -0
  71. package/data/registry/react/components/dialog/index.tsx +97 -0
  72. package/data/registry/react/components/dialog/styles.css +127 -0
  73. package/data/registry/react/components/dropdown-menu/index.tsx +257 -0
  74. package/data/registry/react/components/dropdown-menu/styles.css +150 -0
  75. package/data/registry/react/components/file-upload/index.tsx +489 -0
  76. package/data/registry/react/components/file-upload/styles.css +170 -0
  77. package/data/registry/react/components/focus-ring/focus-ring.css +23 -0
  78. package/data/registry/react/components/form/context.ts +92 -0
  79. package/data/registry/react/components/form/field.test.tsx +230 -0
  80. package/data/registry/react/components/form/field.tsx +236 -0
  81. package/data/registry/react/components/form/focus-first-error.ts +54 -0
  82. package/data/registry/react/components/form/form.section.test.tsx +58 -0
  83. package/data/registry/react/components/form/form.test.tsx +146 -0
  84. package/data/registry/react/components/form/form.tsx +180 -0
  85. package/data/registry/react/components/form/index.tsx +61 -0
  86. package/data/registry/react/components/form/steps.test.tsx +106 -0
  87. package/data/registry/react/components/form/steps.tsx +193 -0
  88. package/data/registry/react/components/form/store.test.ts +206 -0
  89. package/data/registry/react/components/form/store.ts +318 -0
  90. package/data/registry/react/components/form/styles.css +47 -0
  91. package/data/registry/react/components/form/types.ts +104 -0
  92. package/data/registry/react/components/form/use-sh-ui-form.ts +15 -0
  93. package/data/registry/react/components/form/utils.test.ts +44 -0
  94. package/data/registry/react/components/form/utils.ts +49 -0
  95. package/data/registry/react/components/form/validation.test.ts +67 -0
  96. package/data/registry/react/components/form/validation.ts +64 -0
  97. package/data/registry/react/components/form-rhf/README.md +27 -0
  98. package/data/registry/react/components/form-rhf/index.tsx +289 -0
  99. package/data/registry/react/components/form-rhf/rhf.test.tsx +42 -0
  100. package/data/registry/react/components/form-tanstack/README.md +27 -0
  101. package/data/registry/react/components/form-tanstack/index.tsx +352 -0
  102. package/data/registry/react/components/form-tanstack/tanstack.test.tsx +45 -0
  103. package/data/registry/react/components/form-yup/README.md +22 -0
  104. package/data/registry/react/components/form-yup/index.tsx +50 -0
  105. package/data/registry/react/components/form-yup/yup.test.ts +27 -0
  106. package/data/registry/react/components/header/index.tsx +257 -0
  107. package/data/registry/react/components/header/styles.css +190 -0
  108. package/data/registry/react/components/input/index.tsx +517 -0
  109. package/data/registry/react/components/input/styles.css +203 -0
  110. package/data/registry/react/components/label/index.tsx +54 -0
  111. package/data/registry/react/components/label/styles.css +90 -0
  112. package/data/registry/react/components/menubar/index.tsx +34 -0
  113. package/data/registry/react/components/menubar/styles.css +45 -0
  114. package/data/registry/react/components/pagination/index.tsx +271 -0
  115. package/data/registry/react/components/pagination/styles.css +105 -0
  116. package/data/registry/react/components/popover/index.tsx +115 -0
  117. package/data/registry/react/components/popover/styles.css +65 -0
  118. package/data/registry/react/components/progress/index.tsx +56 -0
  119. package/data/registry/react/components/progress/styles.css +41 -0
  120. package/data/registry/react/components/radio/index.tsx +67 -0
  121. package/data/registry/react/components/radio/styles.css +80 -0
  122. package/data/registry/react/components/select/index.tsx +236 -0
  123. package/data/registry/react/components/select/styles.css +193 -0
  124. package/data/registry/react/components/separator/index.tsx +48 -0
  125. package/data/registry/react/components/separator/styles.css +15 -0
  126. package/data/registry/react/components/sidebar/index.tsx +1084 -0
  127. package/data/registry/react/components/sidebar/styles.css +502 -0
  128. package/data/registry/react/components/skeleton/index.tsx +24 -0
  129. package/data/registry/react/components/skeleton/styles.css +24 -0
  130. package/data/registry/react/components/slider/index.tsx +300 -0
  131. package/data/registry/react/components/slider/styles.css +64 -0
  132. package/data/registry/react/components/spinner/index.tsx +40 -0
  133. package/data/registry/react/components/spinner/styles.css +37 -0
  134. package/data/registry/react/components/switch/index.tsx +41 -0
  135. package/data/registry/react/components/switch/styles.css +83 -0
  136. package/data/registry/react/components/tabs/index.tsx +93 -0
  137. package/data/registry/react/components/tabs/styles.css +148 -0
  138. package/data/registry/react/components/textarea/index.tsx +25 -0
  139. package/data/registry/react/components/textarea/styles.css +54 -0
  140. package/data/registry/react/components/theme/index.tsx +91 -0
  141. package/data/registry/react/components/toast/index.tsx +257 -0
  142. package/data/registry/react/components/toast/styles.css +290 -0
  143. package/data/registry/react/components/toggle/index.tsx +133 -0
  144. package/data/registry/react/components/toggle/styles.css +85 -0
  145. package/data/registry/react/components/tooltip/index.tsx +85 -0
  146. package/data/registry/react/components/tooltip/styles.css +44 -0
  147. package/data/registry/react/components/z-index/z-index.css +16 -0
  148. package/data/registry/react/hooks/use-active-section.ts +104 -0
  149. package/data/registry/react/hooks/use-media-query.ts +27 -0
  150. package/data/registry/react/lib/cn.ts +39 -0
  151. package/data/registry/react/registry.json +835 -0
  152. package/data/summaries/flutter.json +42 -0
  153. package/data/summaries/react.json +50 -0
  154. package/data/tokens/build.mjs +553 -0
  155. package/data/tokens/src/primitives.json +146 -0
  156. package/data/tokens/src/semantic.json +146 -0
  157. package/package.json +9 -2
  158. package/src/add.mjs +13 -12
  159. package/src/list.mjs +3 -11
  160. package/src/mcp.mjs +308 -0
  161. package/src/paths.mjs +52 -0
  162. package/src/remove.mjs +4 -11
@@ -0,0 +1,251 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../foundation/sh_ui_tokens.dart';
3
+
4
+ /// sh-ui DropdownMenu — 트리거에서 펼쳐지는 명령 메뉴.
5
+ ///
6
+ /// Flutter의 [PopupMenuButton]을 sh-ui 토큰으로 스타일링한 래퍼.
7
+ /// 체크박스·라디오 항목과 구분선·라벨을 지원한다.
8
+ ///
9
+ /// ShUiDropdownMenu<String>(
10
+ /// child: ShUiButton(onPressed: null, child: Text('메뉴')),
11
+ /// items: const [
12
+ /// ShUiDropdownMenuItem(value: 'new', label: '새로 만들기'),
13
+ /// ShUiDropdownMenuItem(value: 'open', label: '열기'),
14
+ /// ShUiDropdownMenuDivider(),
15
+ /// ShUiDropdownMenuItem(value: 'close', label: '닫기'),
16
+ /// ],
17
+ /// onSelected: (value) => print(value),
18
+ /// )
19
+ class ShUiDropdownMenu<T> extends StatelessWidget {
20
+ /// 트리거 위젯. 탭하면 메뉴가 펼쳐진다.
21
+ final Widget child;
22
+
23
+ /// 메뉴 항목 목록.
24
+ final List<ShUiDropdownMenuEntry<T>> items;
25
+
26
+ /// 항목 선택 콜백. null이면 비활성.
27
+ final ValueChanged<T>? onSelected;
28
+
29
+ /// 트리거에 대한 배치 방향. 기본 bottom.
30
+ final ShUiDropdownMenuSide side;
31
+
32
+ /// 툴팁 텍스트. PopupMenuButton의 기본 툴팁을 덮어쓴다.
33
+ final String? tooltip;
34
+
35
+ const ShUiDropdownMenu({
36
+ super.key,
37
+ required this.child,
38
+ required this.items,
39
+ this.onSelected,
40
+ this.side = ShUiDropdownMenuSide.bottom,
41
+ this.tooltip,
42
+ });
43
+
44
+ @override
45
+ Widget build(BuildContext context) {
46
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
47
+ final colors = shUi.colors;
48
+
49
+ return PopupMenuButton<T>(
50
+ tooltip: tooltip ?? '',
51
+ onSelected: onSelected,
52
+ enabled: onSelected != null,
53
+ color: colors.background,
54
+ surfaceTintColor: Colors.transparent,
55
+ shadowColor: Colors.black.withValues(alpha: 0.12),
56
+ elevation: 4,
57
+ position: side == ShUiDropdownMenuSide.top
58
+ ? PopupMenuPosition.over
59
+ : PopupMenuPosition.under,
60
+ shape: RoundedRectangleBorder(
61
+ borderRadius: BorderRadius.circular(shUi.radius.defaultRadius),
62
+ side: BorderSide(color: colors.border),
63
+ ),
64
+ padding: const EdgeInsets.all(4),
65
+ itemBuilder: (context) =>
66
+ items.map((e) => e.toPopupMenuEntry(context)).toList(),
67
+ child: child,
68
+ );
69
+ }
70
+ }
71
+
72
+ enum ShUiDropdownMenuSide { top, bottom }
73
+
74
+ /// DropdownMenu 항목의 베이스 인터페이스.
75
+ abstract class ShUiDropdownMenuEntry<T> {
76
+ const ShUiDropdownMenuEntry();
77
+ PopupMenuEntry<T> toPopupMenuEntry(BuildContext context);
78
+ }
79
+
80
+ /// 기본 명령 항목.
81
+ class ShUiDropdownMenuItem<T> extends ShUiDropdownMenuEntry<T> {
82
+ final T value;
83
+ final String? label;
84
+ final Widget? child;
85
+ final Widget? leading;
86
+ final bool disabled;
87
+
88
+ const ShUiDropdownMenuItem({
89
+ required this.value,
90
+ this.label,
91
+ this.child,
92
+ this.leading,
93
+ this.disabled = false,
94
+ }) : assert(label != null || child != null, 'label 또는 child 중 하나는 필요');
95
+
96
+ @override
97
+ PopupMenuEntry<T> toPopupMenuEntry(BuildContext context) {
98
+ return _buildItem(
99
+ context: context,
100
+ value: value,
101
+ disabled: disabled,
102
+ leading: leading,
103
+ label: label,
104
+ child: child,
105
+ );
106
+ }
107
+ }
108
+
109
+ /// 체크박스 항목 — checked 상태면 ✓ 인디케이터 표시.
110
+ class ShUiDropdownMenuCheckboxItem<T> extends ShUiDropdownMenuEntry<T> {
111
+ final T value;
112
+ final String? label;
113
+ final Widget? child;
114
+ final bool checked;
115
+ final bool disabled;
116
+
117
+ const ShUiDropdownMenuCheckboxItem({
118
+ required this.value,
119
+ this.label,
120
+ this.child,
121
+ required this.checked,
122
+ this.disabled = false,
123
+ }) : assert(label != null || child != null);
124
+
125
+ @override
126
+ PopupMenuEntry<T> toPopupMenuEntry(BuildContext context) {
127
+ return _buildItem(
128
+ context: context,
129
+ value: value,
130
+ disabled: disabled,
131
+ leading: checked
132
+ ? const Icon(Icons.check, size: 14)
133
+ : const SizedBox(width: 14),
134
+ label: label,
135
+ child: child,
136
+ );
137
+ }
138
+ }
139
+
140
+ /// 라디오 항목 — selected 상태면 ● 인디케이터 표시.
141
+ class ShUiDropdownMenuRadioItem<T> extends ShUiDropdownMenuEntry<T> {
142
+ final T value;
143
+ final String? label;
144
+ final Widget? child;
145
+ final bool selected;
146
+ final bool disabled;
147
+
148
+ const ShUiDropdownMenuRadioItem({
149
+ required this.value,
150
+ this.label,
151
+ this.child,
152
+ required this.selected,
153
+ this.disabled = false,
154
+ }) : assert(label != null || child != null);
155
+
156
+ @override
157
+ PopupMenuEntry<T> toPopupMenuEntry(BuildContext context) {
158
+ return _buildItem(
159
+ context: context,
160
+ value: value,
161
+ disabled: disabled,
162
+ leading: selected
163
+ ? const Icon(Icons.circle, size: 8)
164
+ : const SizedBox(width: 14),
165
+ label: label,
166
+ child: child,
167
+ );
168
+ }
169
+ }
170
+
171
+ /// 구분선.
172
+ class ShUiDropdownMenuDivider<T> extends ShUiDropdownMenuEntry<T> {
173
+ const ShUiDropdownMenuDivider();
174
+
175
+ @override
176
+ PopupMenuEntry<T> toPopupMenuEntry(BuildContext context) {
177
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
178
+ return PopupMenuDivider(
179
+ height: 1,
180
+ color: shUi.colors.border,
181
+ );
182
+ }
183
+ }
184
+
185
+ /// 섹션 라벨 — 탭 불가, 작은 대문자 제목.
186
+ class ShUiDropdownMenuLabel<T> extends ShUiDropdownMenuEntry<T> {
187
+ final String label;
188
+
189
+ const ShUiDropdownMenuLabel(this.label);
190
+
191
+ @override
192
+ PopupMenuEntry<T> toPopupMenuEntry(BuildContext context) {
193
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
194
+ return PopupMenuItem<T>(
195
+ enabled: false,
196
+ height: 28,
197
+ padding: const EdgeInsets.fromLTRB(12, 6, 12, 2),
198
+ child: Text(
199
+ label.toUpperCase(),
200
+ style: TextStyle(
201
+ color: shUi.colors.foregroundMuted,
202
+ fontSize: 11,
203
+ fontWeight: shUi.weight.semibold,
204
+ letterSpacing: 0.4,
205
+ ),
206
+ ),
207
+ );
208
+ }
209
+ }
210
+
211
+ PopupMenuEntry<T> _buildItem<T>({
212
+ required BuildContext context,
213
+ required T value,
214
+ required bool disabled,
215
+ required Widget? leading,
216
+ required String? label,
217
+ required Widget? child,
218
+ }) {
219
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
220
+ final colors = shUi.colors;
221
+
222
+ return PopupMenuItem<T>(
223
+ value: value,
224
+ enabled: !disabled,
225
+ height: 36,
226
+ padding: const EdgeInsets.symmetric(horizontal: 10),
227
+ child: Row(
228
+ mainAxisSize: MainAxisSize.min,
229
+ children: [
230
+ if (leading != null) ...[
231
+ IconTheme(
232
+ data: IconThemeData(color: colors.foreground, size: 14),
233
+ child: leading,
234
+ ),
235
+ const SizedBox(width: 8),
236
+ ],
237
+ Expanded(
238
+ child: DefaultTextStyle(
239
+ style: TextStyle(
240
+ color: disabled ? colors.foregroundMuted : colors.foreground,
241
+ fontSize: shUi.text.sm,
242
+ height: 1.2,
243
+ ),
244
+ overflow: TextOverflow.ellipsis,
245
+ child: child ?? Text(label ?? ''),
246
+ ),
247
+ ),
248
+ ],
249
+ ),
250
+ );
251
+ }
@@ -0,0 +1,200 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../foundation/sh_ui_tokens.dart';
3
+
4
+ /// 파일 정보 (플랫폼 독립적).
5
+ class ShUiFileInfo {
6
+ final String name;
7
+ final int size;
8
+
9
+ const ShUiFileInfo({required this.name, required this.size});
10
+ }
11
+
12
+ /// sh-ui FileUpload — 파일 업로드 UI.
13
+ ///
14
+ /// Flutter에서 실제 파일 선택은 file_picker 등 패키지에 의존한다.
15
+ /// 이 위젯은 UI만 제공하며, onPickFiles 콜백으로 파일 선택 로직을 위임한다.
16
+ ///
17
+ /// ShUiFileUpload(
18
+ /// files: uploadedFiles,
19
+ /// onPickFiles: () async {
20
+ /// final result = await FilePicker.platform.pickFiles(allowMultiple: true);
21
+ /// if (result != null) {
22
+ /// setState(() { uploadedFiles.addAll(result.files.map(...)); });
23
+ /// }
24
+ /// },
25
+ /// onRemove: (index) => setState(() => uploadedFiles.removeAt(index)),
26
+ /// )
27
+ class ShUiFileUpload extends StatefulWidget {
28
+ final List<ShUiFileInfo> files;
29
+ final VoidCallback? onPickFiles;
30
+ final ValueChanged<int>? onRemove;
31
+ final bool multiple;
32
+ final bool enabled;
33
+ final Widget? placeholder;
34
+ final String? hint;
35
+ final bool showFileList;
36
+
37
+ const ShUiFileUpload({
38
+ super.key,
39
+ this.files = const [],
40
+ this.onPickFiles,
41
+ this.onRemove,
42
+ this.multiple = false,
43
+ this.enabled = true,
44
+ this.placeholder,
45
+ this.hint,
46
+ this.showFileList = true,
47
+ });
48
+
49
+ @override
50
+ State<ShUiFileUpload> createState() => _ShUiFileUploadState();
51
+ }
52
+
53
+ class _ShUiFileUploadState extends State<ShUiFileUpload> {
54
+ bool _hover = false;
55
+
56
+ String _formatBytes(int bytes) {
57
+ if (bytes < 1024) return '$bytes B';
58
+ if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
59
+ if (bytes < 1024 * 1024 * 1024) {
60
+ return '${(bytes / 1024 / 1024).toStringAsFixed(1)} MB';
61
+ }
62
+ return '${(bytes / 1024 / 1024 / 1024).toStringAsFixed(1)} GB';
63
+ }
64
+
65
+ @override
66
+ Widget build(BuildContext context) {
67
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
68
+ final colors = shUi.colors;
69
+ final disabled = !widget.enabled;
70
+
71
+ return Column(
72
+ crossAxisAlignment: CrossAxisAlignment.stretch,
73
+ mainAxisSize: MainAxisSize.min,
74
+ children: [
75
+ // Dropzone
76
+ MouseRegion(
77
+ cursor: disabled ? SystemMouseCursors.basic : SystemMouseCursors.click,
78
+ onEnter: (_) => setState(() => _hover = true),
79
+ onExit: (_) => setState(() => _hover = false),
80
+ child: GestureDetector(
81
+ onTap: disabled ? null : widget.onPickFiles,
82
+ child: Opacity(
83
+ opacity: disabled ? shUi.opacity.disabled : 1,
84
+ child: AnimatedContainer(
85
+ duration: shUi.duration.fast,
86
+ padding:
87
+ EdgeInsets.symmetric(horizontal: shUi.spacing.s6, vertical: shUi.spacing.s8),
88
+ decoration: BoxDecoration(
89
+ color: _hover
90
+ ? colors.backgroundSubtle
91
+ : colors.background,
92
+ border: Border.all(
93
+ color: _hover ? colors.primary : colors.border,
94
+ width: 1.5,
95
+ ),
96
+ borderRadius:
97
+ BorderRadius.circular(shUi.radius.defaultRadius),
98
+ ),
99
+ child: Column(
100
+ mainAxisSize: MainAxisSize.min,
101
+ children: [
102
+ Icon(
103
+ Icons.upload_outlined,
104
+ size: 28,
105
+ color: colors.foregroundMuted,
106
+ ),
107
+ SizedBox(height: shUi.spacing.s2),
108
+ widget.placeholder ??
109
+ Text.rich(
110
+ TextSpan(children: [
111
+ TextSpan(
112
+ text: '파일을 선택',
113
+ style: TextStyle(
114
+ fontWeight: shUi.weight.semibold,
115
+ color: colors.foreground,
116
+ ),
117
+ ),
118
+ TextSpan(
119
+ text: '하거나 드래그하세요',
120
+ style: TextStyle(color: colors.foregroundMuted),
121
+ ),
122
+ ]),
123
+ style: TextStyle(fontSize: shUi.text.sm),
124
+ ),
125
+ if (widget.hint != null) ...[
126
+ SizedBox(height: shUi.spacing.s1),
127
+ Text(
128
+ widget.hint!,
129
+ style: TextStyle(
130
+ color: colors.foregroundMuted,
131
+ fontSize: shUi.text.xs,
132
+ ),
133
+ ),
134
+ ],
135
+ ],
136
+ ),
137
+ ),
138
+ ),
139
+ ),
140
+ ),
141
+
142
+ // File list
143
+ if (widget.showFileList && widget.files.isNotEmpty) ...[
144
+ SizedBox(height: shUi.spacing.s2),
145
+ ...List.generate(widget.files.length, (i) {
146
+ final file = widget.files[i];
147
+ return Padding(
148
+ padding: EdgeInsets.only(bottom: shUi.spacing.s1),
149
+ child: Container(
150
+ padding:
151
+ EdgeInsets.symmetric(horizontal: shUi.spacing.s3, vertical: shUi.spacing.s2),
152
+ decoration: BoxDecoration(
153
+ color: colors.backgroundSubtle,
154
+ borderRadius:
155
+ BorderRadius.circular(shUi.radius.defaultRadius - 2),
156
+ ),
157
+ child: Row(
158
+ children: [
159
+ Icon(Icons.insert_drive_file_outlined,
160
+ size: 16, color: colors.foregroundMuted),
161
+ SizedBox(width: shUi.spacing.s2),
162
+ Expanded(
163
+ child: Text(
164
+ file.name,
165
+ overflow: TextOverflow.ellipsis,
166
+ style: TextStyle(
167
+ color: colors.foreground,
168
+ fontSize: 13,
169
+ ),
170
+ ),
171
+ ),
172
+ SizedBox(width: shUi.spacing.s2),
173
+ Text(
174
+ _formatBytes(file.size),
175
+ style: TextStyle(
176
+ color: colors.foregroundMuted,
177
+ fontSize: shUi.text.xs,
178
+ ),
179
+ ),
180
+ if (widget.onRemove != null) ...[
181
+ SizedBox(width: shUi.spacing.s2),
182
+ GestureDetector(
183
+ onTap: disabled ? null : () => widget.onRemove!(i),
184
+ child: Icon(
185
+ Icons.close,
186
+ size: 14,
187
+ color: colors.foregroundMuted,
188
+ ),
189
+ ),
190
+ ],
191
+ ],
192
+ ),
193
+ ),
194
+ );
195
+ }),
196
+ ],
197
+ ],
198
+ );
199
+ }
200
+ }