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,362 @@
|
|
|
1
|
+
import 'dart:async';
|
|
2
|
+
import 'package:flutter/material.dart';
|
|
3
|
+
import '../foundation/sh_ui_tokens.dart';
|
|
4
|
+
|
|
5
|
+
enum ShUiToastVariant { defaultVariant, success, danger, warning }
|
|
6
|
+
|
|
7
|
+
enum ShUiToastPosition {
|
|
8
|
+
topLeft,
|
|
9
|
+
topRight,
|
|
10
|
+
topCenter,
|
|
11
|
+
bottomLeft,
|
|
12
|
+
bottomRight,
|
|
13
|
+
bottomCenter,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// 토스트 데이터.
|
|
17
|
+
class _ToastItem {
|
|
18
|
+
final String id;
|
|
19
|
+
final String? title;
|
|
20
|
+
final String? description;
|
|
21
|
+
final ShUiToastVariant variant;
|
|
22
|
+
final Duration duration;
|
|
23
|
+
final Widget? action;
|
|
24
|
+
|
|
25
|
+
const _ToastItem({
|
|
26
|
+
required this.id,
|
|
27
|
+
this.title,
|
|
28
|
+
this.description,
|
|
29
|
+
required this.variant,
|
|
30
|
+
required this.duration,
|
|
31
|
+
this.action,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// 토스트 입력.
|
|
36
|
+
class ShUiToastInput {
|
|
37
|
+
final String? title;
|
|
38
|
+
final String? description;
|
|
39
|
+
final ShUiToastVariant variant;
|
|
40
|
+
final Duration duration;
|
|
41
|
+
final Widget? action;
|
|
42
|
+
|
|
43
|
+
const ShUiToastInput({
|
|
44
|
+
this.title,
|
|
45
|
+
this.description,
|
|
46
|
+
this.variant = ShUiToastVariant.defaultVariant,
|
|
47
|
+
this.duration = const Duration(seconds: 4),
|
|
48
|
+
this.action,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// sh-ui Toast — 전역 토스트 알림 시스템.
|
|
53
|
+
///
|
|
54
|
+
/// 사용법:
|
|
55
|
+
/// 1) 앱 최상위에 ShUiToaster를 배치.
|
|
56
|
+
/// MaterialApp(builder: (_, child) => ShUiToaster(child: child!))
|
|
57
|
+
///
|
|
58
|
+
/// 2) 어디서든 호출.
|
|
59
|
+
/// ShUiToast.show(context, description: '저장됨');
|
|
60
|
+
/// ShUiToast.success(context, description: '완료!');
|
|
61
|
+
/// ShUiToast.danger(context, description: '오류 발생');
|
|
62
|
+
/// ShUiToast.warning(context, description: '주의');
|
|
63
|
+
class ShUiToast {
|
|
64
|
+
ShUiToast._();
|
|
65
|
+
|
|
66
|
+
static String show(
|
|
67
|
+
BuildContext context, {
|
|
68
|
+
String? title,
|
|
69
|
+
String? description,
|
|
70
|
+
ShUiToastVariant variant = ShUiToastVariant.defaultVariant,
|
|
71
|
+
Duration duration = const Duration(seconds: 4),
|
|
72
|
+
Widget? action,
|
|
73
|
+
}) {
|
|
74
|
+
final state = _ShUiToasterState._of(context);
|
|
75
|
+
return state._add(ShUiToastInput(
|
|
76
|
+
title: title,
|
|
77
|
+
description: description,
|
|
78
|
+
variant: variant,
|
|
79
|
+
duration: duration,
|
|
80
|
+
action: action,
|
|
81
|
+
));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static String success(BuildContext context, {String? title, String? description}) =>
|
|
85
|
+
show(context, title: title, description: description, variant: ShUiToastVariant.success);
|
|
86
|
+
|
|
87
|
+
static String danger(BuildContext context, {String? title, String? description}) =>
|
|
88
|
+
show(context, title: title, description: description, variant: ShUiToastVariant.danger);
|
|
89
|
+
|
|
90
|
+
static String warning(BuildContext context, {String? title, String? description}) =>
|
|
91
|
+
show(context, title: title, description: description, variant: ShUiToastVariant.warning);
|
|
92
|
+
|
|
93
|
+
static void dismiss(BuildContext context, String id) {
|
|
94
|
+
final state = _ShUiToasterState._of(context);
|
|
95
|
+
state._remove(id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// 토스트를 렌더링하는 Toaster 위젯. 앱 최상위에 배치.
|
|
100
|
+
class ShUiToaster extends StatefulWidget {
|
|
101
|
+
final Widget child;
|
|
102
|
+
final ShUiToastPosition position;
|
|
103
|
+
|
|
104
|
+
const ShUiToaster({
|
|
105
|
+
super.key,
|
|
106
|
+
required this.child,
|
|
107
|
+
this.position = ShUiToastPosition.bottomRight,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
@override
|
|
111
|
+
State<ShUiToaster> createState() => _ShUiToasterState();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
class _ShUiToasterState extends State<ShUiToaster> {
|
|
115
|
+
final List<_ToastItem> _toasts = [];
|
|
116
|
+
int _counter = 0;
|
|
117
|
+
|
|
118
|
+
static _ShUiToasterState _of(BuildContext context) {
|
|
119
|
+
final state = context.findAncestorStateOfType<_ShUiToasterState>();
|
|
120
|
+
if (state == null) {
|
|
121
|
+
throw FlutterError('ShUiToaster가 위젯 트리에 없습니다. 앱 최상위에 ShUiToaster를 배치하세요.');
|
|
122
|
+
}
|
|
123
|
+
return state;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
String _add(ShUiToastInput input) {
|
|
127
|
+
final id = 'sh-toast-${++_counter}';
|
|
128
|
+
setState(() {
|
|
129
|
+
_toasts.add(_ToastItem(
|
|
130
|
+
id: id,
|
|
131
|
+
title: input.title,
|
|
132
|
+
description: input.description,
|
|
133
|
+
variant: input.variant,
|
|
134
|
+
duration: input.duration,
|
|
135
|
+
action: input.action,
|
|
136
|
+
));
|
|
137
|
+
});
|
|
138
|
+
return id;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
void _remove(String id) {
|
|
142
|
+
setState(() {
|
|
143
|
+
_toasts.removeWhere((t) => t.id == id);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
Alignment _positionAlignment() {
|
|
148
|
+
switch (widget.position) {
|
|
149
|
+
case ShUiToastPosition.topLeft:
|
|
150
|
+
return Alignment.topLeft;
|
|
151
|
+
case ShUiToastPosition.topRight:
|
|
152
|
+
return Alignment.topRight;
|
|
153
|
+
case ShUiToastPosition.topCenter:
|
|
154
|
+
return Alignment.topCenter;
|
|
155
|
+
case ShUiToastPosition.bottomLeft:
|
|
156
|
+
return Alignment.bottomLeft;
|
|
157
|
+
case ShUiToastPosition.bottomRight:
|
|
158
|
+
return Alignment.bottomRight;
|
|
159
|
+
case ShUiToastPosition.bottomCenter:
|
|
160
|
+
return Alignment.bottomCenter;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@override
|
|
165
|
+
Widget build(BuildContext context) {
|
|
166
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
167
|
+
return Stack(
|
|
168
|
+
children: [
|
|
169
|
+
widget.child,
|
|
170
|
+
if (_toasts.isNotEmpty)
|
|
171
|
+
Positioned.fill(
|
|
172
|
+
child: IgnorePointer(
|
|
173
|
+
ignoring: false,
|
|
174
|
+
child: Align(
|
|
175
|
+
alignment: _positionAlignment(),
|
|
176
|
+
child: Padding(
|
|
177
|
+
padding: EdgeInsets.all(shUi.spacing.s4),
|
|
178
|
+
child: Column(
|
|
179
|
+
mainAxisSize: MainAxisSize.min,
|
|
180
|
+
crossAxisAlignment: CrossAxisAlignment.end,
|
|
181
|
+
children: _toasts.map((item) {
|
|
182
|
+
return Padding(
|
|
183
|
+
padding: EdgeInsets.only(bottom: shUi.spacing.s2),
|
|
184
|
+
child: _ShUiToastCard(
|
|
185
|
+
item: item,
|
|
186
|
+
onDismiss: () => _remove(item.id),
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
}).toList(),
|
|
190
|
+
),
|
|
191
|
+
),
|
|
192
|
+
),
|
|
193
|
+
),
|
|
194
|
+
),
|
|
195
|
+
],
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
class _ShUiToastCard extends StatefulWidget {
|
|
201
|
+
final _ToastItem item;
|
|
202
|
+
final VoidCallback onDismiss;
|
|
203
|
+
|
|
204
|
+
const _ShUiToastCard({required this.item, required this.onDismiss});
|
|
205
|
+
|
|
206
|
+
@override
|
|
207
|
+
State<_ShUiToastCard> createState() => _ShUiToastCardState();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
class _ShUiToastCardState extends State<_ShUiToastCard>
|
|
211
|
+
with SingleTickerProviderStateMixin {
|
|
212
|
+
late final AnimationController _controller;
|
|
213
|
+
late final Animation<double> _opacity;
|
|
214
|
+
late final Animation<Offset> _slide;
|
|
215
|
+
Timer? _timer;
|
|
216
|
+
|
|
217
|
+
@override
|
|
218
|
+
void initState() {
|
|
219
|
+
super.initState();
|
|
220
|
+
_controller = AnimationController(
|
|
221
|
+
vsync: this,
|
|
222
|
+
duration: ShUiTheme.light.duration.slow,
|
|
223
|
+
);
|
|
224
|
+
_opacity = Tween<double>(begin: 0, end: 1).animate(
|
|
225
|
+
CurvedAnimation(parent: _controller, curve: Curves.easeOut),
|
|
226
|
+
);
|
|
227
|
+
_slide = Tween<Offset>(
|
|
228
|
+
begin: const Offset(0, 0.3),
|
|
229
|
+
end: Offset.zero,
|
|
230
|
+
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
|
|
231
|
+
|
|
232
|
+
_controller.forward();
|
|
233
|
+
|
|
234
|
+
if (widget.item.duration.inMilliseconds > 0) {
|
|
235
|
+
_timer = Timer(widget.item.duration, _exit);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
void _exit() {
|
|
240
|
+
_controller.reverse().then((_) {
|
|
241
|
+
if (mounted) widget.onDismiss();
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
@override
|
|
246
|
+
void dispose() {
|
|
247
|
+
_timer?.cancel();
|
|
248
|
+
_controller.dispose();
|
|
249
|
+
super.dispose();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
Color _variantColor(ShUiColorTokens colors) {
|
|
253
|
+
switch (widget.item.variant) {
|
|
254
|
+
case ShUiToastVariant.defaultVariant:
|
|
255
|
+
return colors.foreground;
|
|
256
|
+
case ShUiToastVariant.success:
|
|
257
|
+
return const Color(0xFF22C55E);
|
|
258
|
+
case ShUiToastVariant.danger:
|
|
259
|
+
return colors.danger;
|
|
260
|
+
case ShUiToastVariant.warning:
|
|
261
|
+
return const Color(0xFFF59E0B);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
IconData? _variantIcon() {
|
|
266
|
+
switch (widget.item.variant) {
|
|
267
|
+
case ShUiToastVariant.defaultVariant:
|
|
268
|
+
return null;
|
|
269
|
+
case ShUiToastVariant.success:
|
|
270
|
+
return Icons.check_circle_outline;
|
|
271
|
+
case ShUiToastVariant.danger:
|
|
272
|
+
return Icons.cancel_outlined;
|
|
273
|
+
case ShUiToastVariant.warning:
|
|
274
|
+
return Icons.warning_amber_rounded;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
@override
|
|
279
|
+
Widget build(BuildContext context) {
|
|
280
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
281
|
+
final colors = shUi.colors;
|
|
282
|
+
final variantColor = _variantColor(colors);
|
|
283
|
+
final icon = _variantIcon();
|
|
284
|
+
|
|
285
|
+
return SlideTransition(
|
|
286
|
+
position: _slide,
|
|
287
|
+
child: FadeTransition(
|
|
288
|
+
opacity: _opacity,
|
|
289
|
+
child: Container(
|
|
290
|
+
constraints: const BoxConstraints(maxWidth: 360, minWidth: 280),
|
|
291
|
+
padding: EdgeInsets.symmetric(horizontal: shUi.spacing.s4, vertical: shUi.spacing.s3),
|
|
292
|
+
decoration: BoxDecoration(
|
|
293
|
+
color: colors.background,
|
|
294
|
+
border: Border.all(color: colors.border),
|
|
295
|
+
borderRadius: BorderRadius.circular(shUi.radius.defaultRadius),
|
|
296
|
+
boxShadow: [
|
|
297
|
+
BoxShadow(
|
|
298
|
+
color: Colors.black.withValues(alpha: 0.1),
|
|
299
|
+
blurRadius: 12,
|
|
300
|
+
offset: const Offset(0, 4),
|
|
301
|
+
),
|
|
302
|
+
],
|
|
303
|
+
),
|
|
304
|
+
child: Row(
|
|
305
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
306
|
+
children: [
|
|
307
|
+
if (icon != null) ...[
|
|
308
|
+
Icon(icon, size: 18, color: variantColor),
|
|
309
|
+
const SizedBox(width: 10),
|
|
310
|
+
],
|
|
311
|
+
Expanded(
|
|
312
|
+
child: Column(
|
|
313
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
314
|
+
mainAxisSize: MainAxisSize.min,
|
|
315
|
+
children: [
|
|
316
|
+
if (widget.item.title != null)
|
|
317
|
+
Text(
|
|
318
|
+
widget.item.title!,
|
|
319
|
+
style: TextStyle(
|
|
320
|
+
color: colors.foreground,
|
|
321
|
+
fontSize: shUi.text.sm,
|
|
322
|
+
fontWeight: shUi.weight.semibold,
|
|
323
|
+
height: 1.3,
|
|
324
|
+
),
|
|
325
|
+
),
|
|
326
|
+
if (widget.item.description != null) ...[
|
|
327
|
+
if (widget.item.title != null) const SizedBox(height: 2),
|
|
328
|
+
Text(
|
|
329
|
+
widget.item.description!,
|
|
330
|
+
style: TextStyle(
|
|
331
|
+
color: colors.foregroundMuted,
|
|
332
|
+
fontSize: 13,
|
|
333
|
+
height: 1.4,
|
|
334
|
+
),
|
|
335
|
+
),
|
|
336
|
+
],
|
|
337
|
+
],
|
|
338
|
+
),
|
|
339
|
+
),
|
|
340
|
+
if (widget.item.action != null) ...[
|
|
341
|
+
SizedBox(width: shUi.spacing.s2),
|
|
342
|
+
widget.item.action!,
|
|
343
|
+
],
|
|
344
|
+
SizedBox(width: shUi.spacing.s1),
|
|
345
|
+
GestureDetector(
|
|
346
|
+
onTap: _exit,
|
|
347
|
+
child: Text(
|
|
348
|
+
'×',
|
|
349
|
+
style: TextStyle(
|
|
350
|
+
color: colors.foregroundMuted,
|
|
351
|
+
fontSize: shUi.text.base,
|
|
352
|
+
height: 1,
|
|
353
|
+
),
|
|
354
|
+
),
|
|
355
|
+
),
|
|
356
|
+
],
|
|
357
|
+
),
|
|
358
|
+
),
|
|
359
|
+
),
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../foundation/sh_ui_tokens.dart';
|
|
3
|
+
|
|
4
|
+
enum ShUiToggleVariant { outline, ghost }
|
|
5
|
+
|
|
6
|
+
enum ShUiToggleSize { sm, md, lg }
|
|
7
|
+
|
|
8
|
+
enum ShUiToggleOrientation { horizontal, vertical }
|
|
9
|
+
|
|
10
|
+
/// sh-ui Toggle — 눌러서 on/off 전환하는 토글 버튼.
|
|
11
|
+
class ShUiToggle extends StatefulWidget {
|
|
12
|
+
final bool pressed;
|
|
13
|
+
final ValueChanged<bool>? onPressedChange;
|
|
14
|
+
final ShUiToggleVariant variant;
|
|
15
|
+
final ShUiToggleSize size;
|
|
16
|
+
final Widget child;
|
|
17
|
+
|
|
18
|
+
const ShUiToggle({
|
|
19
|
+
super.key,
|
|
20
|
+
this.pressed = false,
|
|
21
|
+
this.onPressedChange,
|
|
22
|
+
this.variant = ShUiToggleVariant.ghost,
|
|
23
|
+
this.size = ShUiToggleSize.md,
|
|
24
|
+
required this.child,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
State<ShUiToggle> createState() => _ShUiToggleState();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class _ShUiToggleState extends State<ShUiToggle> {
|
|
32
|
+
bool _hover = false;
|
|
33
|
+
|
|
34
|
+
EdgeInsets _paddingOf(ShUiTheme shUi) => switch (widget.size) {
|
|
35
|
+
ShUiToggleSize.sm => EdgeInsets.symmetric(horizontal: shUi.spacing.s2),
|
|
36
|
+
ShUiToggleSize.md => EdgeInsets.symmetric(horizontal: shUi.spacing.s3),
|
|
37
|
+
ShUiToggleSize.lg => EdgeInsets.symmetric(horizontal: shUi.spacing.s4),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
double _heightOf(ShUiTheme shUi) => switch (widget.size) {
|
|
41
|
+
ShUiToggleSize.sm => shUi.control.sm,
|
|
42
|
+
ShUiToggleSize.md => shUi.control.md,
|
|
43
|
+
ShUiToggleSize.lg => shUi.control.lg,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
double _fontSizeOf(ShUiTheme shUi) => switch (widget.size) {
|
|
47
|
+
ShUiToggleSize.sm => shUi.text.sm,
|
|
48
|
+
ShUiToggleSize.md => shUi.text.sm,
|
|
49
|
+
ShUiToggleSize.lg => shUi.text.base,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
@override
|
|
53
|
+
Widget build(BuildContext context) {
|
|
54
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
55
|
+
final colors = shUi.colors;
|
|
56
|
+
final disabled = widget.onPressedChange == null;
|
|
57
|
+
|
|
58
|
+
Color bg;
|
|
59
|
+
Color borderColor;
|
|
60
|
+
|
|
61
|
+
if (widget.pressed) {
|
|
62
|
+
bg = colors.backgroundMuted;
|
|
63
|
+
borderColor = widget.variant == ShUiToggleVariant.outline
|
|
64
|
+
? colors.border
|
|
65
|
+
: Colors.transparent;
|
|
66
|
+
} else if (_hover && !disabled) {
|
|
67
|
+
bg = colors.backgroundSubtle;
|
|
68
|
+
borderColor = widget.variant == ShUiToggleVariant.outline
|
|
69
|
+
? colors.border
|
|
70
|
+
: Colors.transparent;
|
|
71
|
+
} else {
|
|
72
|
+
bg = Colors.transparent;
|
|
73
|
+
borderColor = widget.variant == ShUiToggleVariant.outline
|
|
74
|
+
? colors.border
|
|
75
|
+
: Colors.transparent;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return Opacity(
|
|
79
|
+
opacity: disabled ? shUi.opacity.disabled : 1,
|
|
80
|
+
child: MouseRegion(
|
|
81
|
+
cursor: disabled ? SystemMouseCursors.basic : SystemMouseCursors.click,
|
|
82
|
+
onEnter: (_) => setState(() => _hover = true),
|
|
83
|
+
onExit: (_) => setState(() => _hover = false),
|
|
84
|
+
child: GestureDetector(
|
|
85
|
+
onTap: disabled
|
|
86
|
+
? null
|
|
87
|
+
: () => widget.onPressedChange!(!widget.pressed),
|
|
88
|
+
child: AnimatedContainer(
|
|
89
|
+
duration: shUi.duration.fast,
|
|
90
|
+
height: _heightOf(shUi),
|
|
91
|
+
padding: _paddingOf(shUi),
|
|
92
|
+
decoration: BoxDecoration(
|
|
93
|
+
color: bg,
|
|
94
|
+
border: Border.all(color: borderColor),
|
|
95
|
+
borderRadius: BorderRadius.circular(shUi.radius.defaultRadius),
|
|
96
|
+
),
|
|
97
|
+
alignment: Alignment.center,
|
|
98
|
+
child: DefaultTextStyle(
|
|
99
|
+
style: TextStyle(
|
|
100
|
+
color: widget.pressed ? colors.foreground : colors.foregroundMuted,
|
|
101
|
+
fontSize: _fontSizeOf(shUi),
|
|
102
|
+
fontWeight: shUi.weight.medium,
|
|
103
|
+
height: 1,
|
|
104
|
+
),
|
|
105
|
+
child: IconTheme(
|
|
106
|
+
data: IconThemeData(
|
|
107
|
+
color: widget.pressed ? colors.foreground : colors.foregroundMuted,
|
|
108
|
+
size: _fontSizeOf(shUi) + 2,
|
|
109
|
+
),
|
|
110
|
+
child: widget.child,
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// 토글 그룹 — 여러 토글을 묶어 하나만 또는 복수 선택 가능.
|
|
121
|
+
class ShUiToggleGroup<T> extends StatelessWidget {
|
|
122
|
+
final Set<T> value;
|
|
123
|
+
final ValueChanged<Set<T>>? onValueChange;
|
|
124
|
+
final bool multiple;
|
|
125
|
+
final ShUiToggleVariant variant;
|
|
126
|
+
final ShUiToggleSize size;
|
|
127
|
+
final ShUiToggleOrientation orientation;
|
|
128
|
+
final List<ShUiToggleGroupItem<T>> children;
|
|
129
|
+
|
|
130
|
+
const ShUiToggleGroup({
|
|
131
|
+
super.key,
|
|
132
|
+
required this.value,
|
|
133
|
+
this.onValueChange,
|
|
134
|
+
this.multiple = false,
|
|
135
|
+
this.variant = ShUiToggleVariant.ghost,
|
|
136
|
+
this.size = ShUiToggleSize.md,
|
|
137
|
+
this.orientation = ShUiToggleOrientation.horizontal,
|
|
138
|
+
required this.children,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
@override
|
|
142
|
+
Widget build(BuildContext context) {
|
|
143
|
+
final layout = orientation == ShUiToggleOrientation.vertical
|
|
144
|
+
? Column(
|
|
145
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
146
|
+
mainAxisSize: MainAxisSize.min,
|
|
147
|
+
children: children,
|
|
148
|
+
)
|
|
149
|
+
: Row(
|
|
150
|
+
mainAxisSize: MainAxisSize.min,
|
|
151
|
+
children: children,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return _ShUiToggleGroupScope(
|
|
155
|
+
value: value,
|
|
156
|
+
onValueChange: onValueChange,
|
|
157
|
+
multiple: multiple,
|
|
158
|
+
variant: variant,
|
|
159
|
+
size: size,
|
|
160
|
+
child: layout,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
class ShUiToggleGroupItem<T> extends StatelessWidget {
|
|
166
|
+
final T value;
|
|
167
|
+
final Widget child;
|
|
168
|
+
|
|
169
|
+
const ShUiToggleGroupItem({
|
|
170
|
+
super.key,
|
|
171
|
+
required this.value,
|
|
172
|
+
required this.child,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
@override
|
|
176
|
+
Widget build(BuildContext context) {
|
|
177
|
+
final scope = _ShUiToggleGroupScope.of<T>(context);
|
|
178
|
+
if (scope == null) return child;
|
|
179
|
+
|
|
180
|
+
final pressed = scope.value.contains(value);
|
|
181
|
+
|
|
182
|
+
return ShUiToggle(
|
|
183
|
+
pressed: pressed,
|
|
184
|
+
variant: scope.variant,
|
|
185
|
+
size: scope.size,
|
|
186
|
+
onPressedChange: scope.onValueChange == null
|
|
187
|
+
? null
|
|
188
|
+
: (_) {
|
|
189
|
+
final next = Set<T>.from(scope.value);
|
|
190
|
+
if (pressed) {
|
|
191
|
+
next.remove(value);
|
|
192
|
+
} else {
|
|
193
|
+
if (!scope.multiple) next.clear();
|
|
194
|
+
next.add(value);
|
|
195
|
+
}
|
|
196
|
+
scope.onValueChange!(next);
|
|
197
|
+
},
|
|
198
|
+
child: child,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
class _ShUiToggleGroupScope<T> extends InheritedWidget {
|
|
204
|
+
final Set<T> value;
|
|
205
|
+
final ValueChanged<Set<T>>? onValueChange;
|
|
206
|
+
final bool multiple;
|
|
207
|
+
final ShUiToggleVariant variant;
|
|
208
|
+
final ShUiToggleSize size;
|
|
209
|
+
|
|
210
|
+
const _ShUiToggleGroupScope({
|
|
211
|
+
required this.value,
|
|
212
|
+
this.onValueChange,
|
|
213
|
+
required this.multiple,
|
|
214
|
+
required this.variant,
|
|
215
|
+
required this.size,
|
|
216
|
+
required super.child,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
static _ShUiToggleGroupScope<T>? of<T>(BuildContext context) {
|
|
220
|
+
return context.dependOnInheritedWidgetOfExactType<_ShUiToggleGroupScope<T>>();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@override
|
|
224
|
+
bool updateShouldNotify(covariant _ShUiToggleGroupScope<T> old) =>
|
|
225
|
+
value != old.value ||
|
|
226
|
+
multiple != old.multiple ||
|
|
227
|
+
variant != old.variant ||
|
|
228
|
+
size != old.size;
|
|
229
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../foundation/sh_ui_tokens.dart';
|
|
3
|
+
|
|
4
|
+
/// sh-ui Tooltip — hover/포커스/long-press 시 짧은 설명을 표시.
|
|
5
|
+
///
|
|
6
|
+
/// Flutter의 [Tooltip]을 sh-ui 토큰으로 스타일링한 래퍼. 터치 기기에서는
|
|
7
|
+
/// long-press로, 데스크탑에서는 hover로 표시된다.
|
|
8
|
+
///
|
|
9
|
+
/// ShUiTooltip(
|
|
10
|
+
/// message: '변경 사항을 저장합니다',
|
|
11
|
+
/// child: ShUiButton(onPressed: () {}, child: Text('저장')),
|
|
12
|
+
/// )
|
|
13
|
+
class ShUiTooltip extends StatelessWidget {
|
|
14
|
+
final String message;
|
|
15
|
+
final Widget child;
|
|
16
|
+
|
|
17
|
+
/// 트리거 위에 표시할지 아래에 표시할지.
|
|
18
|
+
final bool preferBelow;
|
|
19
|
+
|
|
20
|
+
/// 표시까지 지연(ms).
|
|
21
|
+
final Duration waitDuration;
|
|
22
|
+
|
|
23
|
+
const ShUiTooltip({
|
|
24
|
+
super.key,
|
|
25
|
+
required this.message,
|
|
26
|
+
required this.child,
|
|
27
|
+
this.preferBelow = true,
|
|
28
|
+
this.waitDuration = const Duration(milliseconds: 300),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
Widget build(BuildContext context) {
|
|
33
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
34
|
+
final colors = shUi.colors;
|
|
35
|
+
|
|
36
|
+
return Tooltip(
|
|
37
|
+
message: message,
|
|
38
|
+
preferBelow: preferBelow,
|
|
39
|
+
waitDuration: waitDuration,
|
|
40
|
+
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
41
|
+
margin: const EdgeInsets.all(4),
|
|
42
|
+
verticalOffset: 16,
|
|
43
|
+
textStyle: TextStyle(
|
|
44
|
+
color: colors.background,
|
|
45
|
+
fontSize: shUi.text.xs,
|
|
46
|
+
height: 1.4,
|
|
47
|
+
),
|
|
48
|
+
decoration: BoxDecoration(
|
|
49
|
+
color: colors.foreground,
|
|
50
|
+
borderRadius: BorderRadius.circular(shUi.radius.defaultRadius - 2),
|
|
51
|
+
boxShadow: [
|
|
52
|
+
BoxShadow(
|
|
53
|
+
color: Colors.black.withValues(alpha: 0.12),
|
|
54
|
+
blurRadius: 12,
|
|
55
|
+
offset: const Offset(0, 4),
|
|
56
|
+
),
|
|
57
|
+
],
|
|
58
|
+
),
|
|
59
|
+
child: child,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|