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,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
+ }