sh-ui-cli 0.14.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.
- package/bin/sh-ui.mjs +6 -0
- package/data/changelog/versions.json +354 -0
- package/data/registry/flutter/foundation/sh_ui_tokens.dart +385 -0
- package/data/registry/flutter/registry.json +336 -0
- package/data/registry/flutter/widgets/sh_ui_accordion.dart +255 -0
- package/data/registry/flutter/widgets/sh_ui_app_shell.dart +267 -0
- package/data/registry/flutter/widgets/sh_ui_avatar.dart +95 -0
- package/data/registry/flutter/widgets/sh_ui_badge.dart +82 -0
- package/data/registry/flutter/widgets/sh_ui_breadcrumb.dart +107 -0
- package/data/registry/flutter/widgets/sh_ui_button.dart +201 -0
- package/data/registry/flutter/widgets/sh_ui_card.dart +159 -0
- package/data/registry/flutter/widgets/sh_ui_carousel.dart +204 -0
- package/data/registry/flutter/widgets/sh_ui_checkbox.dart +154 -0
- package/data/registry/flutter/widgets/sh_ui_color_picker.dart +264 -0
- package/data/registry/flutter/widgets/sh_ui_combobox.dart +614 -0
- package/data/registry/flutter/widgets/sh_ui_context_menu.dart +71 -0
- package/data/registry/flutter/widgets/sh_ui_date_picker.dart +648 -0
- package/data/registry/flutter/widgets/sh_ui_dialog.dart +567 -0
- package/data/registry/flutter/widgets/sh_ui_dropdown_menu.dart +251 -0
- package/data/registry/flutter/widgets/sh_ui_file_upload.dart +200 -0
- package/data/registry/flutter/widgets/sh_ui_header.dart +488 -0
- package/data/registry/flutter/widgets/sh_ui_input.dart +664 -0
- package/data/registry/flutter/widgets/sh_ui_label.dart +145 -0
- package/data/registry/flutter/widgets/sh_ui_menubar.dart +98 -0
- package/data/registry/flutter/widgets/sh_ui_pagination.dart +276 -0
- package/data/registry/flutter/widgets/sh_ui_popover.dart +248 -0
- package/data/registry/flutter/widgets/sh_ui_progress.dart +47 -0
- package/data/registry/flutter/widgets/sh_ui_radio.dart +108 -0
- package/data/registry/flutter/widgets/sh_ui_select.dart +904 -0
- package/data/registry/flutter/widgets/sh_ui_separator.dart +42 -0
- package/data/registry/flutter/widgets/sh_ui_sidebar.dart +1116 -0
- package/data/registry/flutter/widgets/sh_ui_skeleton.dart +129 -0
- package/data/registry/flutter/widgets/sh_ui_slider.dart +147 -0
- package/data/registry/flutter/widgets/sh_ui_spinner.dart +56 -0
- package/data/registry/flutter/widgets/sh_ui_switch.dart +109 -0
- package/data/registry/flutter/widgets/sh_ui_tabs.dart +329 -0
- package/data/registry/flutter/widgets/sh_ui_textarea.dart +126 -0
- package/data/registry/flutter/widgets/sh_ui_toast.dart +362 -0
- package/data/registry/flutter/widgets/sh_ui_toggle.dart +229 -0
- package/data/registry/flutter/widgets/sh_ui_tooltip.dart +62 -0
- package/data/registry/react/components/accordion/index.tsx +85 -0
- package/data/registry/react/components/accordion/styles.css +94 -0
- package/data/registry/react/components/animations/animations.css +51 -0
- package/data/registry/react/components/avatar/index.tsx +75 -0
- package/data/registry/react/components/avatar/styles.css +36 -0
- package/data/registry/react/components/badge/index.tsx +42 -0
- package/data/registry/react/components/badge/styles.css +57 -0
- package/data/registry/react/components/base/base.css +102 -0
- package/data/registry/react/components/breadcrumb/index.tsx +154 -0
- package/data/registry/react/components/breadcrumb/styles.css +82 -0
- package/data/registry/react/components/breakpoints/breakpoints.css +17 -0
- package/data/registry/react/components/button/index.tsx +47 -0
- package/data/registry/react/components/button/styles.css +93 -0
- package/data/registry/react/components/card/index.tsx +86 -0
- package/data/registry/react/components/card/styles.css +73 -0
- package/data/registry/react/components/carousel/index.tsx +432 -0
- package/data/registry/react/components/carousel/styles.css +155 -0
- package/data/registry/react/components/checkbox/index.tsx +98 -0
- package/data/registry/react/components/checkbox/styles.css +75 -0
- package/data/registry/react/components/code-panel/copy.tsx +56 -0
- package/data/registry/react/components/code-panel/index.tsx +193 -0
- package/data/registry/react/components/code-panel/styles.css +124 -0
- package/data/registry/react/components/color-picker/index.tsx +466 -0
- package/data/registry/react/components/color-picker/styles.css +166 -0
- package/data/registry/react/components/combobox/index.tsx +167 -0
- package/data/registry/react/components/combobox/styles.css +151 -0
- package/data/registry/react/components/context-menu/index.tsx +253 -0
- package/data/registry/react/components/context-menu/styles.css +140 -0
- package/data/registry/react/components/date-picker/index.tsx +757 -0
- package/data/registry/react/components/date-picker/styles.css +279 -0
- package/data/registry/react/components/dialog/index.tsx +97 -0
- package/data/registry/react/components/dialog/styles.css +127 -0
- package/data/registry/react/components/dropdown-menu/index.tsx +257 -0
- package/data/registry/react/components/dropdown-menu/styles.css +150 -0
- package/data/registry/react/components/file-upload/index.tsx +489 -0
- package/data/registry/react/components/file-upload/styles.css +170 -0
- package/data/registry/react/components/focus-ring/focus-ring.css +23 -0
- package/data/registry/react/components/form/context.ts +92 -0
- package/data/registry/react/components/form/field.test.tsx +230 -0
- package/data/registry/react/components/form/field.tsx +236 -0
- package/data/registry/react/components/form/focus-first-error.ts +54 -0
- package/data/registry/react/components/form/form.section.test.tsx +58 -0
- package/data/registry/react/components/form/form.test.tsx +146 -0
- package/data/registry/react/components/form/form.tsx +180 -0
- package/data/registry/react/components/form/index.tsx +61 -0
- package/data/registry/react/components/form/steps.test.tsx +106 -0
- package/data/registry/react/components/form/steps.tsx +193 -0
- package/data/registry/react/components/form/store.test.ts +206 -0
- package/data/registry/react/components/form/store.ts +318 -0
- package/data/registry/react/components/form/styles.css +47 -0
- package/data/registry/react/components/form/types.ts +104 -0
- package/data/registry/react/components/form/use-sh-ui-form.ts +15 -0
- package/data/registry/react/components/form/utils.test.ts +44 -0
- package/data/registry/react/components/form/utils.ts +49 -0
- package/data/registry/react/components/form/validation.test.ts +67 -0
- package/data/registry/react/components/form/validation.ts +64 -0
- package/data/registry/react/components/form-rhf/README.md +27 -0
- package/data/registry/react/components/form-rhf/index.tsx +289 -0
- package/data/registry/react/components/form-rhf/rhf.test.tsx +42 -0
- package/data/registry/react/components/form-tanstack/README.md +27 -0
- package/data/registry/react/components/form-tanstack/index.tsx +352 -0
- package/data/registry/react/components/form-tanstack/tanstack.test.tsx +45 -0
- package/data/registry/react/components/form-yup/README.md +22 -0
- package/data/registry/react/components/form-yup/index.tsx +50 -0
- package/data/registry/react/components/form-yup/yup.test.ts +27 -0
- package/data/registry/react/components/header/index.tsx +257 -0
- package/data/registry/react/components/header/styles.css +190 -0
- package/data/registry/react/components/input/index.tsx +517 -0
- package/data/registry/react/components/input/styles.css +203 -0
- package/data/registry/react/components/label/index.tsx +54 -0
- package/data/registry/react/components/label/styles.css +90 -0
- package/data/registry/react/components/menubar/index.tsx +34 -0
- package/data/registry/react/components/menubar/styles.css +45 -0
- package/data/registry/react/components/pagination/index.tsx +271 -0
- package/data/registry/react/components/pagination/styles.css +105 -0
- package/data/registry/react/components/popover/index.tsx +115 -0
- package/data/registry/react/components/popover/styles.css +65 -0
- package/data/registry/react/components/progress/index.tsx +56 -0
- package/data/registry/react/components/progress/styles.css +41 -0
- package/data/registry/react/components/radio/index.tsx +67 -0
- package/data/registry/react/components/radio/styles.css +80 -0
- package/data/registry/react/components/select/index.tsx +236 -0
- package/data/registry/react/components/select/styles.css +193 -0
- package/data/registry/react/components/separator/index.tsx +48 -0
- package/data/registry/react/components/separator/styles.css +15 -0
- package/data/registry/react/components/sidebar/index.tsx +1084 -0
- package/data/registry/react/components/sidebar/styles.css +502 -0
- package/data/registry/react/components/skeleton/index.tsx +24 -0
- package/data/registry/react/components/skeleton/styles.css +24 -0
- package/data/registry/react/components/slider/index.tsx +300 -0
- package/data/registry/react/components/slider/styles.css +64 -0
- package/data/registry/react/components/spinner/index.tsx +40 -0
- package/data/registry/react/components/spinner/styles.css +37 -0
- package/data/registry/react/components/switch/index.tsx +41 -0
- package/data/registry/react/components/switch/styles.css +83 -0
- package/data/registry/react/components/tabs/index.tsx +93 -0
- package/data/registry/react/components/tabs/styles.css +148 -0
- package/data/registry/react/components/textarea/index.tsx +25 -0
- package/data/registry/react/components/textarea/styles.css +54 -0
- package/data/registry/react/components/theme/index.tsx +91 -0
- package/data/registry/react/components/toast/index.tsx +257 -0
- package/data/registry/react/components/toast/styles.css +290 -0
- package/data/registry/react/components/toggle/index.tsx +133 -0
- package/data/registry/react/components/toggle/styles.css +85 -0
- package/data/registry/react/components/tooltip/index.tsx +85 -0
- package/data/registry/react/components/tooltip/styles.css +44 -0
- package/data/registry/react/components/z-index/z-index.css +16 -0
- package/data/registry/react/hooks/use-active-section.ts +104 -0
- package/data/registry/react/hooks/use-media-query.ts +27 -0
- package/data/registry/react/lib/cn.ts +39 -0
- package/data/registry/react/registry.json +835 -0
- package/data/summaries/flutter.json +42 -0
- package/data/summaries/react.json +50 -0
- package/data/tokens/build.mjs +553 -0
- package/data/tokens/src/primitives.json +146 -0
- package/data/tokens/src/semantic.json +146 -0
- package/package.json +13 -4
- package/src/add.mjs +13 -12
- package/src/list.mjs +3 -11
- package/src/mcp.mjs +308 -0
- package/src/paths.mjs +52 -0
- package/src/remove.mjs +4 -11
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../foundation/sh_ui_tokens.dart';
|
|
3
|
+
|
|
4
|
+
enum ShUiTabsVariant { underline, pill, plain }
|
|
5
|
+
|
|
6
|
+
enum ShUiTabsOrientation { horizontal, vertical }
|
|
7
|
+
|
|
8
|
+
/// sh-ui Tabs — 탭 네비게이션.
|
|
9
|
+
///
|
|
10
|
+
/// ShUiTabs(
|
|
11
|
+
/// variant: ShUiTabsVariant.underline,
|
|
12
|
+
/// orientation: ShUiTabsOrientation.horizontal,
|
|
13
|
+
/// tabs: [
|
|
14
|
+
/// ShUiTab(label: '개요', child: Text('개요 내용')),
|
|
15
|
+
/// ShUiTab(label: '설정', child: Text('설정 내용')),
|
|
16
|
+
/// ],
|
|
17
|
+
/// )
|
|
18
|
+
class ShUiTabs extends StatefulWidget {
|
|
19
|
+
final List<ShUiTab> tabs;
|
|
20
|
+
final int initialIndex;
|
|
21
|
+
final ValueChanged<int>? onChanged;
|
|
22
|
+
final ShUiTabsVariant variant;
|
|
23
|
+
final ShUiTabsOrientation orientation;
|
|
24
|
+
|
|
25
|
+
/// vertical 일 때 탭 리스트의 너비.
|
|
26
|
+
final double verticalListWidth;
|
|
27
|
+
|
|
28
|
+
const ShUiTabs({
|
|
29
|
+
super.key,
|
|
30
|
+
required this.tabs,
|
|
31
|
+
this.initialIndex = 0,
|
|
32
|
+
this.onChanged,
|
|
33
|
+
this.variant = ShUiTabsVariant.underline,
|
|
34
|
+
this.orientation = ShUiTabsOrientation.horizontal,
|
|
35
|
+
this.verticalListWidth = 160,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
@override
|
|
39
|
+
State<ShUiTabs> createState() => _ShUiTabsState();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class _ShUiTabsState extends State<ShUiTabs> {
|
|
43
|
+
late int _selectedIndex;
|
|
44
|
+
|
|
45
|
+
@override
|
|
46
|
+
void initState() {
|
|
47
|
+
super.initState();
|
|
48
|
+
_selectedIndex = widget.initialIndex;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
void _select(int index) {
|
|
52
|
+
setState(() => _selectedIndex = index);
|
|
53
|
+
widget.onChanged?.call(index);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@override
|
|
57
|
+
Widget build(BuildContext context) {
|
|
58
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
59
|
+
final colors = shUi.colors;
|
|
60
|
+
|
|
61
|
+
final tabList = _ShUiTabList(
|
|
62
|
+
tabs: widget.tabs,
|
|
63
|
+
selectedIndex: _selectedIndex,
|
|
64
|
+
onSelect: _select,
|
|
65
|
+
variant: widget.variant,
|
|
66
|
+
orientation: widget.orientation,
|
|
67
|
+
colors: colors,
|
|
68
|
+
radius: shUi.radius,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
final content = _selectedIndex < widget.tabs.length
|
|
72
|
+
? widget.tabs[_selectedIndex].child
|
|
73
|
+
: const SizedBox.shrink();
|
|
74
|
+
|
|
75
|
+
if (widget.orientation == ShUiTabsOrientation.vertical) {
|
|
76
|
+
return Row(
|
|
77
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
78
|
+
mainAxisSize: MainAxisSize.min,
|
|
79
|
+
children: [
|
|
80
|
+
SizedBox(width: widget.verticalListWidth, child: tabList),
|
|
81
|
+
SizedBox(width: shUi.spacing.s4),
|
|
82
|
+
Expanded(child: content),
|
|
83
|
+
],
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return Column(
|
|
88
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
89
|
+
mainAxisSize: MainAxisSize.min,
|
|
90
|
+
children: [
|
|
91
|
+
tabList,
|
|
92
|
+
content,
|
|
93
|
+
],
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class ShUiTab {
|
|
99
|
+
final String label;
|
|
100
|
+
final Widget? icon;
|
|
101
|
+
final Widget child;
|
|
102
|
+
|
|
103
|
+
const ShUiTab({
|
|
104
|
+
required this.label,
|
|
105
|
+
this.icon,
|
|
106
|
+
required this.child,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
class _ShUiTabList extends StatelessWidget {
|
|
111
|
+
final List<ShUiTab> tabs;
|
|
112
|
+
final int selectedIndex;
|
|
113
|
+
final ValueChanged<int> onSelect;
|
|
114
|
+
final ShUiTabsVariant variant;
|
|
115
|
+
final ShUiTabsOrientation orientation;
|
|
116
|
+
final ShUiColorTokens colors;
|
|
117
|
+
final ShUiRadiusTokens radius;
|
|
118
|
+
|
|
119
|
+
const _ShUiTabList({
|
|
120
|
+
required this.tabs,
|
|
121
|
+
required this.selectedIndex,
|
|
122
|
+
required this.onSelect,
|
|
123
|
+
required this.variant,
|
|
124
|
+
required this.orientation,
|
|
125
|
+
required this.colors,
|
|
126
|
+
required this.radius,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
@override
|
|
130
|
+
Widget build(BuildContext context) {
|
|
131
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
132
|
+
final isVertical = orientation == ShUiTabsOrientation.vertical;
|
|
133
|
+
|
|
134
|
+
final triggers = List.generate(tabs.length, (i) {
|
|
135
|
+
return _ShUiTabTrigger(
|
|
136
|
+
label: tabs[i].label,
|
|
137
|
+
icon: tabs[i].icon,
|
|
138
|
+
selected: i == selectedIndex,
|
|
139
|
+
onTap: () => onSelect(i),
|
|
140
|
+
variant: variant,
|
|
141
|
+
orientation: orientation,
|
|
142
|
+
colors: colors,
|
|
143
|
+
radius: radius,
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
Widget list = isVertical
|
|
148
|
+
? Column(
|
|
149
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
150
|
+
mainAxisSize: MainAxisSize.min,
|
|
151
|
+
children: triggers,
|
|
152
|
+
)
|
|
153
|
+
: SingleChildScrollView(
|
|
154
|
+
scrollDirection: Axis.horizontal,
|
|
155
|
+
physics: const BouncingScrollPhysics(),
|
|
156
|
+
child: Row(
|
|
157
|
+
mainAxisSize: MainAxisSize.min,
|
|
158
|
+
children: triggers,
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// underline variant 구분선
|
|
163
|
+
if (variant == ShUiTabsVariant.underline) {
|
|
164
|
+
list = Container(
|
|
165
|
+
decoration: BoxDecoration(
|
|
166
|
+
border: Border(
|
|
167
|
+
bottom: isVertical
|
|
168
|
+
? BorderSide.none
|
|
169
|
+
: BorderSide(color: colors.border, width: shUi.borderWidth.normal),
|
|
170
|
+
right: isVertical
|
|
171
|
+
? BorderSide(color: colors.border, width: shUi.borderWidth.normal)
|
|
172
|
+
: BorderSide.none,
|
|
173
|
+
),
|
|
174
|
+
),
|
|
175
|
+
child: list,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// pill variant 배경
|
|
180
|
+
if (variant == ShUiTabsVariant.pill) {
|
|
181
|
+
list = Container(
|
|
182
|
+
padding: EdgeInsets.all(shUi.spacing.s1),
|
|
183
|
+
decoration: BoxDecoration(
|
|
184
|
+
color: colors.backgroundMuted,
|
|
185
|
+
borderRadius: BorderRadius.circular(radius.defaultRadius),
|
|
186
|
+
),
|
|
187
|
+
child: list,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return list;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
class _ShUiTabTrigger extends StatefulWidget {
|
|
196
|
+
final String label;
|
|
197
|
+
final Widget? icon;
|
|
198
|
+
final bool selected;
|
|
199
|
+
final VoidCallback onTap;
|
|
200
|
+
final ShUiTabsVariant variant;
|
|
201
|
+
final ShUiTabsOrientation orientation;
|
|
202
|
+
final ShUiColorTokens colors;
|
|
203
|
+
final ShUiRadiusTokens radius;
|
|
204
|
+
|
|
205
|
+
const _ShUiTabTrigger({
|
|
206
|
+
required this.label,
|
|
207
|
+
this.icon,
|
|
208
|
+
required this.selected,
|
|
209
|
+
required this.onTap,
|
|
210
|
+
required this.variant,
|
|
211
|
+
required this.orientation,
|
|
212
|
+
required this.colors,
|
|
213
|
+
required this.radius,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
@override
|
|
217
|
+
State<_ShUiTabTrigger> createState() => _ShUiTabTriggerState();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
class _ShUiTabTriggerState extends State<_ShUiTabTrigger> {
|
|
221
|
+
bool _hover = false;
|
|
222
|
+
|
|
223
|
+
@override
|
|
224
|
+
Widget build(BuildContext context) {
|
|
225
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
226
|
+
final isVertical = widget.orientation == ShUiTabsOrientation.vertical;
|
|
227
|
+
|
|
228
|
+
Color bg;
|
|
229
|
+
Color fg;
|
|
230
|
+
BoxBorder? border;
|
|
231
|
+
|
|
232
|
+
switch (widget.variant) {
|
|
233
|
+
case ShUiTabsVariant.underline:
|
|
234
|
+
bg = Colors.transparent;
|
|
235
|
+
fg = widget.selected ? widget.colors.foreground : widget.colors.foregroundMuted;
|
|
236
|
+
if (widget.selected) {
|
|
237
|
+
border = isVertical
|
|
238
|
+
? Border(
|
|
239
|
+
right: BorderSide(
|
|
240
|
+
color: widget.colors.foreground,
|
|
241
|
+
width: shUi.borderWidth.strong,
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
: Border(
|
|
245
|
+
bottom: BorderSide(
|
|
246
|
+
color: widget.colors.foreground,
|
|
247
|
+
width: shUi.borderWidth.strong,
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
} else {
|
|
251
|
+
border = null;
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
case ShUiTabsVariant.pill:
|
|
255
|
+
bg = widget.selected ? widget.colors.background : Colors.transparent;
|
|
256
|
+
fg = widget.selected ? widget.colors.foreground : widget.colors.foregroundMuted;
|
|
257
|
+
border = null;
|
|
258
|
+
break;
|
|
259
|
+
case ShUiTabsVariant.plain:
|
|
260
|
+
bg = Colors.transparent;
|
|
261
|
+
fg = widget.selected ? widget.colors.foreground : widget.colors.foregroundMuted;
|
|
262
|
+
border = null;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (_hover && !widget.selected) {
|
|
267
|
+
fg = widget.colors.foreground;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
final rowChildren = <Widget>[
|
|
271
|
+
if (widget.icon != null) ...[
|
|
272
|
+
IconTheme(
|
|
273
|
+
data: IconThemeData(color: fg, size: 16),
|
|
274
|
+
child: widget.icon!,
|
|
275
|
+
),
|
|
276
|
+
const SizedBox(width: 6),
|
|
277
|
+
],
|
|
278
|
+
Text(
|
|
279
|
+
widget.label,
|
|
280
|
+
style: TextStyle(
|
|
281
|
+
color: fg,
|
|
282
|
+
fontSize: shUi.text.sm,
|
|
283
|
+
fontWeight: widget.selected ? shUi.weight.medium : shUi.weight.regular,
|
|
284
|
+
height: 1,
|
|
285
|
+
),
|
|
286
|
+
),
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
final trigger = AnimatedContainer(
|
|
290
|
+
duration: shUi.duration.fast,
|
|
291
|
+
height: isVertical ? shUi.control.md : null,
|
|
292
|
+
padding: EdgeInsets.symmetric(
|
|
293
|
+
horizontal: shUi.spacing.s4,
|
|
294
|
+
vertical: shUi.spacing.s2,
|
|
295
|
+
),
|
|
296
|
+
decoration: BoxDecoration(
|
|
297
|
+
color: bg,
|
|
298
|
+
border: border,
|
|
299
|
+
borderRadius: widget.variant == ShUiTabsVariant.pill
|
|
300
|
+
? BorderRadius.circular(widget.radius.defaultRadius - 2)
|
|
301
|
+
: null,
|
|
302
|
+
boxShadow: widget.variant == ShUiTabsVariant.pill && widget.selected
|
|
303
|
+
? [
|
|
304
|
+
BoxShadow(
|
|
305
|
+
color: Colors.black.withValues(alpha: 0.05),
|
|
306
|
+
blurRadius: 2,
|
|
307
|
+
offset: const Offset(0, 1),
|
|
308
|
+
),
|
|
309
|
+
]
|
|
310
|
+
: null,
|
|
311
|
+
),
|
|
312
|
+
alignment: isVertical ? Alignment.centerLeft : null,
|
|
313
|
+
child: Row(
|
|
314
|
+
mainAxisSize: MainAxisSize.min,
|
|
315
|
+
children: rowChildren,
|
|
316
|
+
),
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
return MouseRegion(
|
|
320
|
+
cursor: SystemMouseCursors.click,
|
|
321
|
+
onEnter: (_) => setState(() => _hover = true),
|
|
322
|
+
onExit: (_) => setState(() => _hover = false),
|
|
323
|
+
child: GestureDetector(
|
|
324
|
+
onTap: widget.onTap,
|
|
325
|
+
child: trigger,
|
|
326
|
+
),
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:flutter/services.dart';
|
|
3
|
+
import '../foundation/sh_ui_tokens.dart';
|
|
4
|
+
|
|
5
|
+
/// sh-ui Textarea — 여러 줄 텍스트 입력.
|
|
6
|
+
class ShUiTextarea extends StatefulWidget {
|
|
7
|
+
final TextEditingController? controller;
|
|
8
|
+
final String? placeholder;
|
|
9
|
+
final bool enabled;
|
|
10
|
+
final bool readOnly;
|
|
11
|
+
final bool invalid;
|
|
12
|
+
final int? maxLength;
|
|
13
|
+
final int minLines;
|
|
14
|
+
final int? maxLines;
|
|
15
|
+
final ValueChanged<String>? onChanged;
|
|
16
|
+
final FocusNode? focusNode;
|
|
17
|
+
final List<TextInputFormatter>? inputFormatters;
|
|
18
|
+
|
|
19
|
+
const ShUiTextarea({
|
|
20
|
+
super.key,
|
|
21
|
+
this.controller,
|
|
22
|
+
this.placeholder,
|
|
23
|
+
this.enabled = true,
|
|
24
|
+
this.readOnly = false,
|
|
25
|
+
this.invalid = false,
|
|
26
|
+
this.maxLength,
|
|
27
|
+
this.minLines = 3,
|
|
28
|
+
this.maxLines,
|
|
29
|
+
this.onChanged,
|
|
30
|
+
this.focusNode,
|
|
31
|
+
this.inputFormatters,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
@override
|
|
35
|
+
State<ShUiTextarea> createState() => _ShUiTextareaState();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class _ShUiTextareaState extends State<ShUiTextarea> {
|
|
39
|
+
late final FocusNode _focusNode;
|
|
40
|
+
bool _ownsFocusNode = false;
|
|
41
|
+
bool _hover = false;
|
|
42
|
+
|
|
43
|
+
@override
|
|
44
|
+
void initState() {
|
|
45
|
+
super.initState();
|
|
46
|
+
_focusNode = widget.focusNode ?? FocusNode();
|
|
47
|
+
_ownsFocusNode = widget.focusNode == null;
|
|
48
|
+
_focusNode.addListener(_onFocusChange);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@override
|
|
52
|
+
void dispose() {
|
|
53
|
+
_focusNode.removeListener(_onFocusChange);
|
|
54
|
+
if (_ownsFocusNode) _focusNode.dispose();
|
|
55
|
+
super.dispose();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
void _onFocusChange() => setState(() {});
|
|
59
|
+
|
|
60
|
+
Color _borderColor(ShUiColorTokens colors) {
|
|
61
|
+
if (widget.invalid) return colors.danger;
|
|
62
|
+
if (_focusNode.hasFocus) return colors.foreground;
|
|
63
|
+
if (_hover && widget.enabled && !widget.readOnly) return colors.foregroundMuted;
|
|
64
|
+
return colors.border;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@override
|
|
68
|
+
Widget build(BuildContext context) {
|
|
69
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
70
|
+
final colors = shUi.colors;
|
|
71
|
+
final focused = _focusNode.hasFocus;
|
|
72
|
+
|
|
73
|
+
final bg = (!widget.enabled || widget.readOnly)
|
|
74
|
+
? colors.backgroundSubtle
|
|
75
|
+
: colors.background;
|
|
76
|
+
|
|
77
|
+
return MouseRegion(
|
|
78
|
+
onEnter: (_) => setState(() => _hover = true),
|
|
79
|
+
onExit: (_) => setState(() => _hover = false),
|
|
80
|
+
child: AnimatedContainer(
|
|
81
|
+
duration: shUi.duration.fast,
|
|
82
|
+
padding: EdgeInsets.symmetric(horizontal: shUi.spacing.s3, vertical: shUi.spacing.s2),
|
|
83
|
+
decoration: BoxDecoration(
|
|
84
|
+
color: bg,
|
|
85
|
+
border: Border.all(
|
|
86
|
+
color: _borderColor(colors),
|
|
87
|
+
width: focused ? shUi.borderWidth.strong : shUi.borderWidth.normal,
|
|
88
|
+
),
|
|
89
|
+
borderRadius: BorderRadius.circular(shUi.radius.defaultRadius),
|
|
90
|
+
),
|
|
91
|
+
child: Opacity(
|
|
92
|
+
opacity: widget.enabled ? 1 : shUi.opacity.disabled,
|
|
93
|
+
child: TextField(
|
|
94
|
+
controller: widget.controller,
|
|
95
|
+
focusNode: _focusNode,
|
|
96
|
+
enabled: widget.enabled,
|
|
97
|
+
readOnly: widget.readOnly,
|
|
98
|
+
maxLength: widget.maxLength,
|
|
99
|
+
minLines: widget.minLines,
|
|
100
|
+
maxLines: widget.maxLines ?? widget.minLines + 5,
|
|
101
|
+
onChanged: widget.onChanged,
|
|
102
|
+
inputFormatters: widget.inputFormatters,
|
|
103
|
+
keyboardType: TextInputType.multiline,
|
|
104
|
+
style: TextStyle(
|
|
105
|
+
color: colors.foreground,
|
|
106
|
+
fontSize: shUi.text.sm,
|
|
107
|
+
height: 1.5,
|
|
108
|
+
),
|
|
109
|
+
cursorColor: colors.foreground,
|
|
110
|
+
decoration: InputDecoration(
|
|
111
|
+
isCollapsed: true,
|
|
112
|
+
contentPadding: EdgeInsets.zero,
|
|
113
|
+
border: InputBorder.none,
|
|
114
|
+
counterText: "",
|
|
115
|
+
hintText: widget.placeholder,
|
|
116
|
+
hintStyle: TextStyle(
|
|
117
|
+
color: colors.foregroundMuted,
|
|
118
|
+
fontSize: shUi.text.sm,
|
|
119
|
+
),
|
|
120
|
+
),
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|