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.
- 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 +9 -2
- 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,248 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../foundation/sh_ui_tokens.dart';
|
|
3
|
+
|
|
4
|
+
enum ShUiPopoverSide { top, right, bottom, left }
|
|
5
|
+
|
|
6
|
+
enum ShUiPopoverAlign { start, center, end }
|
|
7
|
+
|
|
8
|
+
/// sh-ui Popover — 트리거 위젯에 붙는 플로팅 팝업.
|
|
9
|
+
///
|
|
10
|
+
/// ShUiPopover(
|
|
11
|
+
/// trigger: (show) => ShUiButton(onPressed: show, child: Text('열기')),
|
|
12
|
+
/// content: (close) => Padding(
|
|
13
|
+
/// padding: EdgeInsets.all(16),
|
|
14
|
+
/// child: Text('팝오버 내용'),
|
|
15
|
+
/// ),
|
|
16
|
+
/// )
|
|
17
|
+
class ShUiPopover extends StatefulWidget {
|
|
18
|
+
/// 트리거 빌더. show 콜백을 받아 팝오버를 연다.
|
|
19
|
+
final Widget Function(VoidCallback show) trigger;
|
|
20
|
+
|
|
21
|
+
/// 컨텐츠 빌더. close 콜백을 받아 팝오버를 닫는다.
|
|
22
|
+
final Widget Function(VoidCallback close) content;
|
|
23
|
+
|
|
24
|
+
final ShUiPopoverSide side;
|
|
25
|
+
final ShUiPopoverAlign align;
|
|
26
|
+
final double sideOffset;
|
|
27
|
+
|
|
28
|
+
const ShUiPopover({
|
|
29
|
+
super.key,
|
|
30
|
+
required this.trigger,
|
|
31
|
+
required this.content,
|
|
32
|
+
this.side = ShUiPopoverSide.bottom,
|
|
33
|
+
this.align = ShUiPopoverAlign.start,
|
|
34
|
+
this.sideOffset = 8,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
@override
|
|
38
|
+
State<ShUiPopover> createState() => _ShUiPopoverState();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class _ShUiPopoverState extends State<ShUiPopover> {
|
|
42
|
+
final LayerLink _layerLink = LayerLink();
|
|
43
|
+
OverlayEntry? _overlayEntry;
|
|
44
|
+
|
|
45
|
+
void _show() {
|
|
46
|
+
if (_overlayEntry != null) return;
|
|
47
|
+
|
|
48
|
+
_overlayEntry = OverlayEntry(
|
|
49
|
+
builder: (context) => _PopoverOverlay(
|
|
50
|
+
link: _layerLink,
|
|
51
|
+
side: widget.side,
|
|
52
|
+
align: widget.align,
|
|
53
|
+
sideOffset: widget.sideOffset,
|
|
54
|
+
onDismiss: _close,
|
|
55
|
+
content: widget.content(_close),
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
Overlay.of(context).insert(_overlayEntry!);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
void _close() {
|
|
63
|
+
_overlayEntry?.remove();
|
|
64
|
+
_overlayEntry = null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@override
|
|
68
|
+
void dispose() {
|
|
69
|
+
_close();
|
|
70
|
+
super.dispose();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@override
|
|
74
|
+
Widget build(BuildContext context) {
|
|
75
|
+
return CompositedTransformTarget(
|
|
76
|
+
link: _layerLink,
|
|
77
|
+
child: widget.trigger(_show),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class _PopoverOverlay extends StatelessWidget {
|
|
83
|
+
final LayerLink link;
|
|
84
|
+
final ShUiPopoverSide side;
|
|
85
|
+
final ShUiPopoverAlign align;
|
|
86
|
+
final double sideOffset;
|
|
87
|
+
final VoidCallback onDismiss;
|
|
88
|
+
final Widget content;
|
|
89
|
+
|
|
90
|
+
const _PopoverOverlay({
|
|
91
|
+
required this.link,
|
|
92
|
+
required this.side,
|
|
93
|
+
required this.align,
|
|
94
|
+
required this.sideOffset,
|
|
95
|
+
required this.onDismiss,
|
|
96
|
+
required this.content,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
Offset _computeOffset() {
|
|
100
|
+
switch (side) {
|
|
101
|
+
case ShUiPopoverSide.bottom:
|
|
102
|
+
return Offset(0, sideOffset);
|
|
103
|
+
case ShUiPopoverSide.top:
|
|
104
|
+
return Offset(0, -sideOffset);
|
|
105
|
+
case ShUiPopoverSide.right:
|
|
106
|
+
return Offset(sideOffset, 0);
|
|
107
|
+
case ShUiPopoverSide.left:
|
|
108
|
+
return Offset(-sideOffset, 0);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
Alignment _targetAnchor() {
|
|
113
|
+
switch (side) {
|
|
114
|
+
case ShUiPopoverSide.bottom:
|
|
115
|
+
switch (align) {
|
|
116
|
+
case ShUiPopoverAlign.start:
|
|
117
|
+
return Alignment.bottomLeft;
|
|
118
|
+
case ShUiPopoverAlign.center:
|
|
119
|
+
return Alignment.bottomCenter;
|
|
120
|
+
case ShUiPopoverAlign.end:
|
|
121
|
+
return Alignment.bottomRight;
|
|
122
|
+
}
|
|
123
|
+
case ShUiPopoverSide.top:
|
|
124
|
+
switch (align) {
|
|
125
|
+
case ShUiPopoverAlign.start:
|
|
126
|
+
return Alignment.topLeft;
|
|
127
|
+
case ShUiPopoverAlign.center:
|
|
128
|
+
return Alignment.topCenter;
|
|
129
|
+
case ShUiPopoverAlign.end:
|
|
130
|
+
return Alignment.topRight;
|
|
131
|
+
}
|
|
132
|
+
case ShUiPopoverSide.right:
|
|
133
|
+
return Alignment.centerRight;
|
|
134
|
+
case ShUiPopoverSide.left:
|
|
135
|
+
return Alignment.centerLeft;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
Alignment _followerAnchor() {
|
|
140
|
+
switch (side) {
|
|
141
|
+
case ShUiPopoverSide.bottom:
|
|
142
|
+
switch (align) {
|
|
143
|
+
case ShUiPopoverAlign.start:
|
|
144
|
+
return Alignment.topLeft;
|
|
145
|
+
case ShUiPopoverAlign.center:
|
|
146
|
+
return Alignment.topCenter;
|
|
147
|
+
case ShUiPopoverAlign.end:
|
|
148
|
+
return Alignment.topRight;
|
|
149
|
+
}
|
|
150
|
+
case ShUiPopoverSide.top:
|
|
151
|
+
switch (align) {
|
|
152
|
+
case ShUiPopoverAlign.start:
|
|
153
|
+
return Alignment.bottomLeft;
|
|
154
|
+
case ShUiPopoverAlign.center:
|
|
155
|
+
return Alignment.bottomCenter;
|
|
156
|
+
case ShUiPopoverAlign.end:
|
|
157
|
+
return Alignment.bottomRight;
|
|
158
|
+
}
|
|
159
|
+
case ShUiPopoverSide.right:
|
|
160
|
+
return Alignment.centerLeft;
|
|
161
|
+
case ShUiPopoverSide.left:
|
|
162
|
+
return Alignment.centerRight;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@override
|
|
167
|
+
Widget build(BuildContext context) {
|
|
168
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
169
|
+
final colors = shUi.colors;
|
|
170
|
+
|
|
171
|
+
return Stack(
|
|
172
|
+
children: [
|
|
173
|
+
Positioned.fill(
|
|
174
|
+
child: GestureDetector(
|
|
175
|
+
onTap: onDismiss,
|
|
176
|
+
behavior: HitTestBehavior.opaque,
|
|
177
|
+
child: const SizedBox.expand(),
|
|
178
|
+
),
|
|
179
|
+
),
|
|
180
|
+
CompositedTransformFollower(
|
|
181
|
+
link: link,
|
|
182
|
+
targetAnchor: _targetAnchor(),
|
|
183
|
+
followerAnchor: _followerAnchor(),
|
|
184
|
+
offset: _computeOffset(),
|
|
185
|
+
child: Material(
|
|
186
|
+
color: Colors.transparent,
|
|
187
|
+
child: Container(
|
|
188
|
+
constraints: const BoxConstraints(maxWidth: 320),
|
|
189
|
+
decoration: BoxDecoration(
|
|
190
|
+
color: colors.background,
|
|
191
|
+
border: Border.all(color: colors.border),
|
|
192
|
+
borderRadius:
|
|
193
|
+
BorderRadius.circular(shUi.radius.defaultRadius),
|
|
194
|
+
boxShadow: [
|
|
195
|
+
BoxShadow(
|
|
196
|
+
color: Colors.black.withValues(alpha: 0.08),
|
|
197
|
+
blurRadius: 8,
|
|
198
|
+
offset: const Offset(0, 4),
|
|
199
|
+
),
|
|
200
|
+
],
|
|
201
|
+
),
|
|
202
|
+
child: content,
|
|
203
|
+
),
|
|
204
|
+
),
|
|
205
|
+
),
|
|
206
|
+
],
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
class ShUiPopoverTitle extends StatelessWidget {
|
|
212
|
+
final String text;
|
|
213
|
+
|
|
214
|
+
const ShUiPopoverTitle(this.text, {super.key});
|
|
215
|
+
|
|
216
|
+
@override
|
|
217
|
+
Widget build(BuildContext context) {
|
|
218
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
219
|
+
return Text(
|
|
220
|
+
text,
|
|
221
|
+
style: TextStyle(
|
|
222
|
+
color: shUi.colors.foreground,
|
|
223
|
+
fontSize: shUi.text.sm,
|
|
224
|
+
fontWeight: shUi.weight.semibold,
|
|
225
|
+
height: 1.3,
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
class ShUiPopoverDescription extends StatelessWidget {
|
|
232
|
+
final String text;
|
|
233
|
+
|
|
234
|
+
const ShUiPopoverDescription(this.text, {super.key});
|
|
235
|
+
|
|
236
|
+
@override
|
|
237
|
+
Widget build(BuildContext context) {
|
|
238
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
239
|
+
return Text(
|
|
240
|
+
text,
|
|
241
|
+
style: TextStyle(
|
|
242
|
+
color: shUi.colors.foregroundMuted,
|
|
243
|
+
fontSize: 13,
|
|
244
|
+
height: 1.5,
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../foundation/sh_ui_tokens.dart';
|
|
3
|
+
|
|
4
|
+
/// sh-ui Progress — 작업 진행도를 가로 바로 표시.
|
|
5
|
+
///
|
|
6
|
+
/// [value]가 `null`이면 indeterminate(무한 루프). 0.0 ~ 1.0 범위의 값을 주면
|
|
7
|
+
/// determinate로 동작.
|
|
8
|
+
class ShUiProgress extends StatelessWidget {
|
|
9
|
+
/// 0.0 ~ 1.0 사이의 진행률. null이면 indeterminate.
|
|
10
|
+
final double? value;
|
|
11
|
+
|
|
12
|
+
/// 바 높이. 기본 8px.
|
|
13
|
+
final double height;
|
|
14
|
+
|
|
15
|
+
/// 접근성: 스크린리더 라벨.
|
|
16
|
+
final String? semanticLabel;
|
|
17
|
+
|
|
18
|
+
const ShUiProgress({
|
|
19
|
+
super.key,
|
|
20
|
+
this.value,
|
|
21
|
+
this.height = 8,
|
|
22
|
+
this.semanticLabel,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
@override
|
|
26
|
+
Widget build(BuildContext context) {
|
|
27
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
28
|
+
final colors = shUi.colors;
|
|
29
|
+
|
|
30
|
+
return Semantics(
|
|
31
|
+
label: semanticLabel,
|
|
32
|
+
value: value != null ? '${(value! * 100).round()}%' : null,
|
|
33
|
+
child: ClipRRect(
|
|
34
|
+
borderRadius: BorderRadius.circular(999),
|
|
35
|
+
child: SizedBox(
|
|
36
|
+
height: height,
|
|
37
|
+
child: LinearProgressIndicator(
|
|
38
|
+
value: value,
|
|
39
|
+
backgroundColor: colors.backgroundMuted,
|
|
40
|
+
valueColor: AlwaysStoppedAnimation<Color>(colors.primary),
|
|
41
|
+
minHeight: height,
|
|
42
|
+
),
|
|
43
|
+
),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../foundation/sh_ui_tokens.dart';
|
|
3
|
+
|
|
4
|
+
/// sh-ui Radio — 토큰 기반 라디오 버튼.
|
|
5
|
+
class ShUiRadio<T> extends StatefulWidget {
|
|
6
|
+
final T value;
|
|
7
|
+
final T? groupValue;
|
|
8
|
+
final ValueChanged<T>? onChanged;
|
|
9
|
+
final bool enabled;
|
|
10
|
+
|
|
11
|
+
const ShUiRadio({
|
|
12
|
+
super.key,
|
|
13
|
+
required this.value,
|
|
14
|
+
required this.groupValue,
|
|
15
|
+
this.onChanged,
|
|
16
|
+
this.enabled = true,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
@override
|
|
20
|
+
State<ShUiRadio<T>> createState() => _ShUiRadioState<T>();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class _ShUiRadioState<T> extends State<ShUiRadio<T>> {
|
|
24
|
+
bool _hover = false;
|
|
25
|
+
|
|
26
|
+
@override
|
|
27
|
+
Widget build(BuildContext context) {
|
|
28
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
29
|
+
final colors = shUi.colors;
|
|
30
|
+
final selected = widget.value == widget.groupValue;
|
|
31
|
+
final disabled = !widget.enabled || widget.onChanged == null;
|
|
32
|
+
|
|
33
|
+
final borderColor = selected
|
|
34
|
+
? colors.primary
|
|
35
|
+
: (_hover ? colors.foregroundMuted : colors.border);
|
|
36
|
+
|
|
37
|
+
return Opacity(
|
|
38
|
+
opacity: disabled ? shUi.opacity.disabled : 1,
|
|
39
|
+
child: MouseRegion(
|
|
40
|
+
cursor: disabled ? SystemMouseCursors.basic : SystemMouseCursors.click,
|
|
41
|
+
onEnter: (_) => setState(() => _hover = true),
|
|
42
|
+
onExit: (_) => setState(() => _hover = false),
|
|
43
|
+
child: GestureDetector(
|
|
44
|
+
onTap: disabled ? null : () => widget.onChanged!(widget.value),
|
|
45
|
+
child: AnimatedContainer(
|
|
46
|
+
duration: shUi.duration.fast,
|
|
47
|
+
width: 18,
|
|
48
|
+
height: 18,
|
|
49
|
+
decoration: BoxDecoration(
|
|
50
|
+
color: colors.background,
|
|
51
|
+
border: Border.all(color: borderColor, width: 1.5),
|
|
52
|
+
shape: BoxShape.circle,
|
|
53
|
+
),
|
|
54
|
+
child: Center(
|
|
55
|
+
child: AnimatedContainer(
|
|
56
|
+
duration: shUi.duration.fast,
|
|
57
|
+
width: selected ? 8 : 0,
|
|
58
|
+
height: selected ? 8 : 0,
|
|
59
|
+
decoration: BoxDecoration(
|
|
60
|
+
color: colors.primary,
|
|
61
|
+
shape: BoxShape.circle,
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// 라디오 그룹 — vertical 또는 horizontal 배치.
|
|
73
|
+
enum ShUiRadioGroupOrientation { vertical, horizontal }
|
|
74
|
+
|
|
75
|
+
class ShUiRadioGroup extends StatelessWidget {
|
|
76
|
+
final List<Widget> children;
|
|
77
|
+
final ShUiRadioGroupOrientation orientation;
|
|
78
|
+
final double gap;
|
|
79
|
+
|
|
80
|
+
const ShUiRadioGroup({
|
|
81
|
+
super.key,
|
|
82
|
+
required this.children,
|
|
83
|
+
this.orientation = ShUiRadioGroupOrientation.vertical,
|
|
84
|
+
this.gap = 8,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
@override
|
|
88
|
+
Widget build(BuildContext context) {
|
|
89
|
+
if (orientation == ShUiRadioGroupOrientation.horizontal) {
|
|
90
|
+
return Wrap(spacing: gap, runSpacing: gap, children: children);
|
|
91
|
+
}
|
|
92
|
+
return Column(
|
|
93
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
94
|
+
mainAxisSize: MainAxisSize.min,
|
|
95
|
+
children: _withGaps(children, gap),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
List<Widget> _withGaps(List<Widget> children, double gap) {
|
|
101
|
+
if (children.length <= 1) return children;
|
|
102
|
+
final out = <Widget>[];
|
|
103
|
+
for (var i = 0; i < children.length; i++) {
|
|
104
|
+
out.add(children[i]);
|
|
105
|
+
if (i != children.length - 1) out.add(SizedBox(height: gap));
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|