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,201 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../foundation/sh_ui_tokens.dart';
|
|
3
|
+
|
|
4
|
+
/// 버튼 시각 위계.
|
|
5
|
+
/// - [primary] : 페이지의 1차 액션. 한 화면에 하나만 권장.
|
|
6
|
+
/// - [secondary] : 보조 액션. border + 약한 배경.
|
|
7
|
+
/// - [ghost] : 배경 없는 hover 강조 액션.
|
|
8
|
+
/// - [danger] : 파괴적 액션(삭제 등).
|
|
9
|
+
/// - [link] : 텍스트 링크처럼 보이는 인라인 버튼.
|
|
10
|
+
enum ShUiButtonVariant { primary, secondary, ghost, danger, link }
|
|
11
|
+
|
|
12
|
+
/// 버튼 크기.
|
|
13
|
+
enum ShUiButtonSize { sm, md, lg }
|
|
14
|
+
|
|
15
|
+
/// shUi Button — 사용자 액션을 트리거하는 기본 버튼.
|
|
16
|
+
///
|
|
17
|
+
/// [variant]로 시각 위계, [size]로 크기를 결정한다. [onPressed]가 null이면
|
|
18
|
+
/// 비활성 상태로 표시된다. 페이지 이동 목적이면 [ShUiButtonVariant.link]를 사용한다.
|
|
19
|
+
///
|
|
20
|
+
/// ```dart
|
|
21
|
+
/// ShUiButton(
|
|
22
|
+
/// onPressed: () {},
|
|
23
|
+
/// variant: ShUiButtonVariant.primary,
|
|
24
|
+
/// child: const Text('저장'),
|
|
25
|
+
/// )
|
|
26
|
+
/// ```
|
|
27
|
+
class ShUiButton extends StatefulWidget {
|
|
28
|
+
/// 버튼 안에 표시될 콘텐츠. 보통 [Text] 또는 [Icon]+[Text]를 [Row]로 묶어 사용.
|
|
29
|
+
final Widget child;
|
|
30
|
+
|
|
31
|
+
/// 탭 콜백. `null`이면 비활성 상태로 렌더되며 hover/press 효과도 제거된다.
|
|
32
|
+
final VoidCallback? onPressed;
|
|
33
|
+
|
|
34
|
+
/// 시각 위계.
|
|
35
|
+
/// - [ShUiButtonVariant.primary] — 페이지의 1차 액션 (기본)
|
|
36
|
+
/// - [ShUiButtonVariant.secondary] — 보조 액션
|
|
37
|
+
/// - [ShUiButtonVariant.ghost] — 배경 없는 hover 강조
|
|
38
|
+
/// - [ShUiButtonVariant.danger] — 파괴적 액션
|
|
39
|
+
/// - [ShUiButtonVariant.link] — 텍스트 링크 스타일
|
|
40
|
+
final ShUiButtonVariant variant;
|
|
41
|
+
|
|
42
|
+
/// 크기. [ShUiButtonSize.sm] / [ShUiButtonSize.md] (기본) / [ShUiButtonSize.lg].
|
|
43
|
+
final ShUiButtonSize size;
|
|
44
|
+
|
|
45
|
+
const ShUiButton({
|
|
46
|
+
super.key,
|
|
47
|
+
required this.child,
|
|
48
|
+
this.onPressed,
|
|
49
|
+
this.variant = ShUiButtonVariant.primary,
|
|
50
|
+
this.size = ShUiButtonSize.md,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
@override
|
|
54
|
+
State<ShUiButton> createState() => _ShUiButtonState();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class _ShUiButtonState extends State<ShUiButton> {
|
|
58
|
+
bool _hover = false;
|
|
59
|
+
bool _pressed = false;
|
|
60
|
+
|
|
61
|
+
_Colors _resolveColors(ShUiColorTokens t) {
|
|
62
|
+
switch (widget.variant) {
|
|
63
|
+
case ShUiButtonVariant.primary:
|
|
64
|
+
return _Colors(
|
|
65
|
+
bg: _hover ? t.primaryHover : t.primary,
|
|
66
|
+
fg: t.primaryForeground,
|
|
67
|
+
border: Colors.transparent,
|
|
68
|
+
);
|
|
69
|
+
case ShUiButtonVariant.secondary:
|
|
70
|
+
return _Colors(
|
|
71
|
+
bg: _hover ? t.backgroundSubtle : t.backgroundMuted,
|
|
72
|
+
fg: t.foreground,
|
|
73
|
+
border: t.border,
|
|
74
|
+
);
|
|
75
|
+
case ShUiButtonVariant.ghost:
|
|
76
|
+
return _Colors(
|
|
77
|
+
bg: _hover ? t.backgroundMuted : Colors.transparent,
|
|
78
|
+
fg: t.foreground,
|
|
79
|
+
border: Colors.transparent,
|
|
80
|
+
);
|
|
81
|
+
case ShUiButtonVariant.danger:
|
|
82
|
+
return _Colors(
|
|
83
|
+
bg: t.danger,
|
|
84
|
+
fg: t.dangerForeground,
|
|
85
|
+
border: Colors.transparent,
|
|
86
|
+
);
|
|
87
|
+
case ShUiButtonVariant.link:
|
|
88
|
+
return _Colors(
|
|
89
|
+
bg: Colors.transparent,
|
|
90
|
+
fg: _pressed ? t.foregroundMuted : t.foreground,
|
|
91
|
+
border: Colors.transparent,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
EdgeInsets _paddingOf(ShUiTheme shUi) => switch (widget.size) {
|
|
97
|
+
ShUiButtonSize.sm => EdgeInsets.symmetric(horizontal: shUi.spacing.s3),
|
|
98
|
+
ShUiButtonSize.md => EdgeInsets.symmetric(horizontal: shUi.spacing.s4),
|
|
99
|
+
ShUiButtonSize.lg => EdgeInsets.symmetric(horizontal: shUi.spacing.s5),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
double _heightOf(ShUiTheme shUi) => switch (widget.size) {
|
|
103
|
+
ShUiButtonSize.sm => shUi.control.sm,
|
|
104
|
+
ShUiButtonSize.md => shUi.control.md,
|
|
105
|
+
ShUiButtonSize.lg => shUi.control.lg,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
double _fontSizeOf(ShUiTheme shUi) => switch (widget.size) {
|
|
109
|
+
ShUiButtonSize.sm => shUi.text.sm,
|
|
110
|
+
ShUiButtonSize.md => shUi.text.sm,
|
|
111
|
+
ShUiButtonSize.lg => shUi.text.base,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
Widget _buildLink(ShUiTheme shUi, _Colors colors, bool disabled) {
|
|
115
|
+
return MouseRegion(
|
|
116
|
+
cursor: disabled ? SystemMouseCursors.basic : SystemMouseCursors.click,
|
|
117
|
+
onEnter: (_) => setState(() => _hover = true),
|
|
118
|
+
onExit: (_) => setState(() => _hover = false),
|
|
119
|
+
child: GestureDetector(
|
|
120
|
+
onTap: disabled ? null : widget.onPressed,
|
|
121
|
+
onTapDown: disabled ? null : (_) => setState(() => _pressed = true),
|
|
122
|
+
onTapUp: disabled ? null : (_) => setState(() => _pressed = false),
|
|
123
|
+
onTapCancel: disabled ? null : () => setState(() => _pressed = false),
|
|
124
|
+
child: DefaultTextStyle(
|
|
125
|
+
style: TextStyle(
|
|
126
|
+
color: colors.fg,
|
|
127
|
+
fontSize: _fontSizeOf(shUi),
|
|
128
|
+
fontWeight: shUi.weight.medium,
|
|
129
|
+
height: 1.2,
|
|
130
|
+
decoration: _hover ? TextDecoration.underline : TextDecoration.none,
|
|
131
|
+
decorationColor: colors.fg,
|
|
132
|
+
),
|
|
133
|
+
child: widget.child,
|
|
134
|
+
),
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@override
|
|
140
|
+
Widget build(BuildContext context) {
|
|
141
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
142
|
+
final colors = _resolveColors(shUi.colors);
|
|
143
|
+
final disabled = widget.onPressed == null;
|
|
144
|
+
|
|
145
|
+
if (widget.variant == ShUiButtonVariant.link) {
|
|
146
|
+
return Opacity(
|
|
147
|
+
opacity: disabled ? shUi.opacity.disabled : 1,
|
|
148
|
+
child: _buildLink(shUi, colors, disabled),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return Opacity(
|
|
153
|
+
opacity: disabled ? shUi.opacity.disabled : 1,
|
|
154
|
+
child: MouseRegion(
|
|
155
|
+
cursor: disabled ? SystemMouseCursors.basic : SystemMouseCursors.click,
|
|
156
|
+
onEnter: (_) => setState(() => _hover = true),
|
|
157
|
+
onExit: (_) => setState(() => _hover = false),
|
|
158
|
+
child: GestureDetector(
|
|
159
|
+
onTap: disabled ? null : widget.onPressed,
|
|
160
|
+
onTapDown: disabled ? null : (_) => setState(() => _pressed = true),
|
|
161
|
+
onTapUp: disabled ? null : (_) => setState(() => _pressed = false),
|
|
162
|
+
onTapCancel: disabled ? null : () => setState(() => _pressed = false),
|
|
163
|
+
child: AnimatedScale(
|
|
164
|
+
scale: _pressed ? 0.97 : 1.0,
|
|
165
|
+
duration: const Duration(milliseconds: 80),
|
|
166
|
+
curve: Curves.easeOut,
|
|
167
|
+
child: AnimatedContainer(
|
|
168
|
+
duration: shUi.duration.fast,
|
|
169
|
+
height: _heightOf(shUi),
|
|
170
|
+
padding: _paddingOf(shUi),
|
|
171
|
+
decoration: BoxDecoration(
|
|
172
|
+
color: _pressed
|
|
173
|
+
? Color.lerp(colors.bg, Colors.black, 0.08)!
|
|
174
|
+
: colors.bg,
|
|
175
|
+
border: Border.all(color: colors.border),
|
|
176
|
+
borderRadius: BorderRadius.circular(shUi.radius.defaultRadius),
|
|
177
|
+
),
|
|
178
|
+
alignment: Alignment.center,
|
|
179
|
+
child: DefaultTextStyle(
|
|
180
|
+
style: TextStyle(
|
|
181
|
+
color: colors.fg,
|
|
182
|
+
fontSize: _fontSizeOf(shUi),
|
|
183
|
+
fontWeight: shUi.weight.medium,
|
|
184
|
+
height: 1,
|
|
185
|
+
),
|
|
186
|
+
child: widget.child,
|
|
187
|
+
),
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
),
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
class _Colors {
|
|
197
|
+
final Color bg;
|
|
198
|
+
final Color fg;
|
|
199
|
+
final Color border;
|
|
200
|
+
const _Colors({required this.bg, required this.fg, required this.border});
|
|
201
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../foundation/sh_ui_tokens.dart';
|
|
3
|
+
|
|
4
|
+
/// shUi Card — 컴파운드 스타일.
|
|
5
|
+
///
|
|
6
|
+
/// React 쪽 compound children API와 달리 Flutter는 named params가 관용적이라
|
|
7
|
+
/// 동일한 의미 구조를 다음과 같이 표현한다:
|
|
8
|
+
///
|
|
9
|
+
/// ShUiCard(children: [
|
|
10
|
+
/// ShUiCardHeader(
|
|
11
|
+
/// title: ShUiCardTitle('Card Title'),
|
|
12
|
+
/// description: ShUiCardDescription('Card Description'),
|
|
13
|
+
/// action: ShUiButton(
|
|
14
|
+
/// variant: ShUiButtonVariant.link,
|
|
15
|
+
/// onPressed: () {},
|
|
16
|
+
/// child: Text('액션'),
|
|
17
|
+
/// ),
|
|
18
|
+
/// ),
|
|
19
|
+
/// ShUiCardContent(child: Text('본문')),
|
|
20
|
+
/// ShUiCardFooter(children: [Text('푸터')]),
|
|
21
|
+
/// ])
|
|
22
|
+
class ShUiCard extends StatelessWidget {
|
|
23
|
+
final List<Widget> children;
|
|
24
|
+
|
|
25
|
+
const ShUiCard({super.key, required this.children});
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
Widget build(BuildContext context) {
|
|
29
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
30
|
+
return Container(
|
|
31
|
+
decoration: BoxDecoration(
|
|
32
|
+
color: shUi.colors.background,
|
|
33
|
+
border: Border.all(color: shUi.colors.border),
|
|
34
|
+
borderRadius: BorderRadius.circular(shUi.radius.defaultRadius),
|
|
35
|
+
),
|
|
36
|
+
padding: EdgeInsets.symmetric(vertical: shUi.spacing.s6),
|
|
37
|
+
child: Column(
|
|
38
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
39
|
+
mainAxisSize: MainAxisSize.min,
|
|
40
|
+
children: _withGaps(children, shUi.spacing.s6),
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
List<Widget> _withGaps(List<Widget> children, double gap) {
|
|
47
|
+
if (children.length <= 1) return children;
|
|
48
|
+
final out = <Widget>[];
|
|
49
|
+
for (var i = 0; i < children.length; i++) {
|
|
50
|
+
out.add(children[i]);
|
|
51
|
+
if (i != children.length - 1) out.add(SizedBox(height: gap));
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class ShUiCardHeader extends StatelessWidget {
|
|
57
|
+
final Widget? title;
|
|
58
|
+
final Widget? description;
|
|
59
|
+
final Widget? action;
|
|
60
|
+
|
|
61
|
+
const ShUiCardHeader({super.key, this.title, this.description, this.action});
|
|
62
|
+
|
|
63
|
+
@override
|
|
64
|
+
Widget build(BuildContext context) {
|
|
65
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
66
|
+
final stack = Column(
|
|
67
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
68
|
+
mainAxisSize: MainAxisSize.min,
|
|
69
|
+
children: [
|
|
70
|
+
if (title != null) title!,
|
|
71
|
+
if (title != null && description != null) const SizedBox(height: 6),
|
|
72
|
+
if (description != null) description!,
|
|
73
|
+
],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return Padding(
|
|
77
|
+
padding: EdgeInsets.symmetric(horizontal: shUi.spacing.s6),
|
|
78
|
+
child: action == null
|
|
79
|
+
? stack
|
|
80
|
+
: Row(
|
|
81
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
82
|
+
children: [
|
|
83
|
+
Expanded(child: stack),
|
|
84
|
+
SizedBox(width: shUi.spacing.s4),
|
|
85
|
+
action!,
|
|
86
|
+
],
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
class ShUiCardTitle extends StatelessWidget {
|
|
93
|
+
final String text;
|
|
94
|
+
const ShUiCardTitle(this.text, {super.key});
|
|
95
|
+
|
|
96
|
+
@override
|
|
97
|
+
Widget build(BuildContext context) {
|
|
98
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
99
|
+
return Text(
|
|
100
|
+
text,
|
|
101
|
+
style: TextStyle(
|
|
102
|
+
color: shUi.colors.foreground,
|
|
103
|
+
fontSize: shUi.text.base,
|
|
104
|
+
fontWeight: shUi.weight.semibold,
|
|
105
|
+
height: 1.25,
|
|
106
|
+
letterSpacing: -0.16,
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
class ShUiCardDescription extends StatelessWidget {
|
|
113
|
+
final String text;
|
|
114
|
+
const ShUiCardDescription(this.text, {super.key});
|
|
115
|
+
|
|
116
|
+
@override
|
|
117
|
+
Widget build(BuildContext context) {
|
|
118
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
119
|
+
return Text(
|
|
120
|
+
text,
|
|
121
|
+
style: TextStyle(
|
|
122
|
+
color: shUi.colors.foregroundMuted,
|
|
123
|
+
fontSize: shUi.text.sm,
|
|
124
|
+
height: 1.5,
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class ShUiCardContent extends StatelessWidget {
|
|
131
|
+
final Widget child;
|
|
132
|
+
const ShUiCardContent({super.key, required this.child});
|
|
133
|
+
|
|
134
|
+
@override
|
|
135
|
+
Widget build(BuildContext context) {
|
|
136
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
137
|
+
return Padding(
|
|
138
|
+
padding: EdgeInsets.symmetric(horizontal: shUi.spacing.s6),
|
|
139
|
+
child: child,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
class ShUiCardFooter extends StatelessWidget {
|
|
145
|
+
final List<Widget> children;
|
|
146
|
+
const ShUiCardFooter({super.key, required this.children});
|
|
147
|
+
|
|
148
|
+
@override
|
|
149
|
+
Widget build(BuildContext context) {
|
|
150
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
151
|
+
return Padding(
|
|
152
|
+
padding: EdgeInsets.symmetric(horizontal: shUi.spacing.s6),
|
|
153
|
+
child: Row(
|
|
154
|
+
mainAxisSize: MainAxisSize.min,
|
|
155
|
+
children: _withGaps(children, shUi.spacing.s2),
|
|
156
|
+
),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../foundation/sh_ui_tokens.dart';
|
|
3
|
+
|
|
4
|
+
/// shUi Carousel — 가로 스와이프 캐러셀.
|
|
5
|
+
///
|
|
6
|
+
/// PageView 기반. 좌우 화살표 버튼([showControls])과 하단 도트 인디케이터
|
|
7
|
+
/// ([showIndicators])를 옵션으로 제공한다. [onIndexChanged]로 현재 인덱스를
|
|
8
|
+
/// 외부로 노출할 수 있다.
|
|
9
|
+
class ShUiCarousel extends StatefulWidget {
|
|
10
|
+
/// 슬라이드로 보여줄 위젯 목록.
|
|
11
|
+
final List<Widget> items;
|
|
12
|
+
|
|
13
|
+
/// 슬라이드 높이. 지정하지 않으면 컨텐츠 높이에 따른다 (기본 220).
|
|
14
|
+
final double? itemHeight;
|
|
15
|
+
|
|
16
|
+
/// 도트 인디케이터 노출 여부.
|
|
17
|
+
final bool showIndicators;
|
|
18
|
+
|
|
19
|
+
/// 좌/우 화살표 버튼 노출 여부.
|
|
20
|
+
final bool showControls;
|
|
21
|
+
|
|
22
|
+
/// 초기 인덱스.
|
|
23
|
+
final int? initialIndex;
|
|
24
|
+
|
|
25
|
+
/// 인덱스 변경 콜백.
|
|
26
|
+
final ValueChanged<int>? onIndexChanged;
|
|
27
|
+
|
|
28
|
+
const ShUiCarousel({
|
|
29
|
+
super.key,
|
|
30
|
+
required this.items,
|
|
31
|
+
this.itemHeight,
|
|
32
|
+
this.showIndicators = true,
|
|
33
|
+
this.showControls = true,
|
|
34
|
+
this.initialIndex,
|
|
35
|
+
this.onIndexChanged,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
@override
|
|
39
|
+
State<ShUiCarousel> createState() => _ShUiCarouselState();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class _ShUiCarouselState extends State<ShUiCarousel> {
|
|
43
|
+
late final PageController _controller;
|
|
44
|
+
late int _index;
|
|
45
|
+
|
|
46
|
+
@override
|
|
47
|
+
void initState() {
|
|
48
|
+
super.initState();
|
|
49
|
+
_index = widget.initialIndex ?? 0;
|
|
50
|
+
_controller = PageController(initialPage: _index);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@override
|
|
54
|
+
void dispose() {
|
|
55
|
+
_controller.dispose();
|
|
56
|
+
super.dispose();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
void _goTo(int i) {
|
|
60
|
+
if (i < 0 || i >= widget.items.length) return;
|
|
61
|
+
_controller.animateToPage(
|
|
62
|
+
i,
|
|
63
|
+
duration: const Duration(milliseconds: 280),
|
|
64
|
+
curve: Curves.easeOut,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@override
|
|
69
|
+
Widget build(BuildContext context) {
|
|
70
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
71
|
+
final colors = shUi.colors;
|
|
72
|
+
final height = widget.itemHeight ?? 220;
|
|
73
|
+
// 좁은 폭(mobile 사이즈) 에서는 컨트롤을 숨긴다.
|
|
74
|
+
final width = MediaQuery.of(context).size.width;
|
|
75
|
+
final compact = width < shUi.breakpoint.sm;
|
|
76
|
+
final showControls = widget.showControls && !compact;
|
|
77
|
+
|
|
78
|
+
return Column(
|
|
79
|
+
mainAxisSize: MainAxisSize.min,
|
|
80
|
+
children: [
|
|
81
|
+
SizedBox(
|
|
82
|
+
height: height,
|
|
83
|
+
child: Stack(
|
|
84
|
+
alignment: Alignment.center,
|
|
85
|
+
children: [
|
|
86
|
+
PageView.builder(
|
|
87
|
+
controller: _controller,
|
|
88
|
+
itemCount: widget.items.length,
|
|
89
|
+
onPageChanged: (i) {
|
|
90
|
+
setState(() => _index = i);
|
|
91
|
+
widget.onIndexChanged?.call(i);
|
|
92
|
+
},
|
|
93
|
+
itemBuilder: (context, i) {
|
|
94
|
+
return Padding(
|
|
95
|
+
padding: EdgeInsets.symmetric(
|
|
96
|
+
horizontal: shUi.spacing.s2,
|
|
97
|
+
),
|
|
98
|
+
child: widget.items[i],
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
),
|
|
102
|
+
if (showControls) ...[
|
|
103
|
+
Positioned(
|
|
104
|
+
left: shUi.spacing.s2,
|
|
105
|
+
child: _NavButton(
|
|
106
|
+
icon: Icons.chevron_left,
|
|
107
|
+
onPressed: _index > 0 ? () => _goTo(_index - 1) : null,
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
Positioned(
|
|
111
|
+
right: shUi.spacing.s2,
|
|
112
|
+
child: _NavButton(
|
|
113
|
+
icon: Icons.chevron_right,
|
|
114
|
+
onPressed: _index < widget.items.length - 1
|
|
115
|
+
? () => _goTo(_index + 1)
|
|
116
|
+
: null,
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
],
|
|
120
|
+
],
|
|
121
|
+
),
|
|
122
|
+
),
|
|
123
|
+
if (widget.showIndicators && widget.items.length > 1) ...[
|
|
124
|
+
SizedBox(height: shUi.spacing.s3),
|
|
125
|
+
Row(
|
|
126
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
127
|
+
children: [
|
|
128
|
+
for (var i = 0; i < widget.items.length; i++)
|
|
129
|
+
GestureDetector(
|
|
130
|
+
onTap: () => _goTo(i),
|
|
131
|
+
behavior: HitTestBehavior.opaque,
|
|
132
|
+
child: AnimatedContainer(
|
|
133
|
+
duration: shUi.duration.base,
|
|
134
|
+
curve: shUi.ease.standard,
|
|
135
|
+
margin: EdgeInsets.symmetric(horizontal: shUi.spacing.s1),
|
|
136
|
+
width: _index == i ? 18 : 6,
|
|
137
|
+
height: 6,
|
|
138
|
+
decoration: BoxDecoration(
|
|
139
|
+
color: _index == i
|
|
140
|
+
? colors.foreground
|
|
141
|
+
: colors.borderStrong,
|
|
142
|
+
borderRadius: BorderRadius.circular(3),
|
|
143
|
+
),
|
|
144
|
+
),
|
|
145
|
+
),
|
|
146
|
+
],
|
|
147
|
+
),
|
|
148
|
+
],
|
|
149
|
+
],
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
class _NavButton extends StatefulWidget {
|
|
155
|
+
final IconData icon;
|
|
156
|
+
final VoidCallback? onPressed;
|
|
157
|
+
|
|
158
|
+
const _NavButton({required this.icon, required this.onPressed});
|
|
159
|
+
|
|
160
|
+
@override
|
|
161
|
+
State<_NavButton> createState() => _NavButtonState();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
class _NavButtonState extends State<_NavButton> {
|
|
165
|
+
bool _hover = false;
|
|
166
|
+
|
|
167
|
+
@override
|
|
168
|
+
Widget build(BuildContext context) {
|
|
169
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
170
|
+
final colors = shUi.colors;
|
|
171
|
+
final disabled = widget.onPressed == null;
|
|
172
|
+
|
|
173
|
+
return Opacity(
|
|
174
|
+
opacity: disabled ? shUi.opacity.disabled : 1,
|
|
175
|
+
child: MouseRegion(
|
|
176
|
+
cursor: disabled ? SystemMouseCursors.basic : SystemMouseCursors.click,
|
|
177
|
+
onEnter: (_) => setState(() => _hover = true),
|
|
178
|
+
onExit: (_) => setState(() => _hover = false),
|
|
179
|
+
child: GestureDetector(
|
|
180
|
+
onTap: widget.onPressed,
|
|
181
|
+
child: AnimatedContainer(
|
|
182
|
+
duration: shUi.duration.fast,
|
|
183
|
+
width: 36,
|
|
184
|
+
height: 36,
|
|
185
|
+
decoration: BoxDecoration(
|
|
186
|
+
color: _hover && !disabled
|
|
187
|
+
? colors.backgroundSubtle
|
|
188
|
+
: colors.background,
|
|
189
|
+
border: Border.all(color: colors.border),
|
|
190
|
+
borderRadius: BorderRadius.circular(999),
|
|
191
|
+
boxShadow: shUi.shadow.sm,
|
|
192
|
+
),
|
|
193
|
+
alignment: Alignment.center,
|
|
194
|
+
child: Icon(
|
|
195
|
+
widget.icon,
|
|
196
|
+
size: 18,
|
|
197
|
+
color: colors.foreground,
|
|
198
|
+
),
|
|
199
|
+
),
|
|
200
|
+
),
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|