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,648 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import '../foundation/sh_ui_tokens.dart';
|
|
3
|
+
|
|
4
|
+
/// sh-ui DatePicker — 날짜 선택.
|
|
5
|
+
///
|
|
6
|
+
/// ShUiDatePicker(
|
|
7
|
+
/// value: selectedDate,
|
|
8
|
+
/// onValueChange: (date) => setState(() => selectedDate = date),
|
|
9
|
+
/// placeholder: '날짜 선택',
|
|
10
|
+
/// )
|
|
11
|
+
class ShUiDatePicker extends StatefulWidget {
|
|
12
|
+
final DateTime? value;
|
|
13
|
+
final ValueChanged<DateTime?>? onValueChange;
|
|
14
|
+
final String placeholder;
|
|
15
|
+
final DateTime? min;
|
|
16
|
+
final DateTime? max;
|
|
17
|
+
final String Function(DateTime)? formatDate;
|
|
18
|
+
final bool enabled;
|
|
19
|
+
|
|
20
|
+
const ShUiDatePicker({
|
|
21
|
+
super.key,
|
|
22
|
+
this.value,
|
|
23
|
+
this.onValueChange,
|
|
24
|
+
this.placeholder = '날짜 선택',
|
|
25
|
+
this.min,
|
|
26
|
+
this.max,
|
|
27
|
+
this.formatDate,
|
|
28
|
+
this.enabled = true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
State<ShUiDatePicker> createState() => _ShUiDatePickerState();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class _ShUiDatePickerState extends State<ShUiDatePicker> {
|
|
36
|
+
final LayerLink _layerLink = LayerLink();
|
|
37
|
+
OverlayEntry? _overlay;
|
|
38
|
+
bool _isOpen = false;
|
|
39
|
+
late DateTime _focusedMonth;
|
|
40
|
+
|
|
41
|
+
@override
|
|
42
|
+
void initState() {
|
|
43
|
+
super.initState();
|
|
44
|
+
_focusedMonth = widget.value ?? DateTime.now();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
String _defaultFormat(DateTime d) =>
|
|
48
|
+
'${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
|
|
49
|
+
|
|
50
|
+
void _open() {
|
|
51
|
+
if (_overlay != null) return;
|
|
52
|
+
_focusedMonth = widget.value ?? DateTime.now();
|
|
53
|
+
_overlay = OverlayEntry(builder: (_) => _buildOverlay());
|
|
54
|
+
Overlay.of(context).insert(_overlay!);
|
|
55
|
+
setState(() => _isOpen = true);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
void _close() {
|
|
59
|
+
_overlay?.remove();
|
|
60
|
+
_overlay = null;
|
|
61
|
+
if (mounted) setState(() => _isOpen = false);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
void _select(DateTime date) {
|
|
65
|
+
widget.onValueChange?.call(date);
|
|
66
|
+
_close();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@override
|
|
70
|
+
void dispose() {
|
|
71
|
+
_close();
|
|
72
|
+
super.dispose();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
Widget _buildOverlay() {
|
|
76
|
+
return _DatePickerOverlay(
|
|
77
|
+
link: _layerLink,
|
|
78
|
+
selected: widget.value,
|
|
79
|
+
focusedMonth: _focusedMonth,
|
|
80
|
+
onFocusedMonthChange: (d) {
|
|
81
|
+
_focusedMonth = d;
|
|
82
|
+
_overlay?.markNeedsBuild();
|
|
83
|
+
},
|
|
84
|
+
onSelect: _select,
|
|
85
|
+
onDismiss: _close,
|
|
86
|
+
min: widget.min,
|
|
87
|
+
max: widget.max,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@override
|
|
92
|
+
Widget build(BuildContext context) {
|
|
93
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
94
|
+
final colors = shUi.colors;
|
|
95
|
+
final disabled = !widget.enabled;
|
|
96
|
+
final fmt = widget.formatDate ?? _defaultFormat;
|
|
97
|
+
final displayText = widget.value != null ? fmt(widget.value!) : null;
|
|
98
|
+
|
|
99
|
+
return CompositedTransformTarget(
|
|
100
|
+
link: _layerLink,
|
|
101
|
+
child: Opacity(
|
|
102
|
+
opacity: disabled ? shUi.opacity.disabled : 1,
|
|
103
|
+
child: GestureDetector(
|
|
104
|
+
onTap: disabled ? null : () => _isOpen ? _close() : _open(),
|
|
105
|
+
child: Container(
|
|
106
|
+
height: shUi.control.md,
|
|
107
|
+
padding: EdgeInsets.symmetric(horizontal: shUi.spacing.s3),
|
|
108
|
+
decoration: BoxDecoration(
|
|
109
|
+
color: colors.background,
|
|
110
|
+
border: Border.all(
|
|
111
|
+
color: _isOpen ? colors.foreground : colors.border,
|
|
112
|
+
width: _isOpen ? shUi.borderWidth.strong : shUi.borderWidth.normal,
|
|
113
|
+
),
|
|
114
|
+
borderRadius: BorderRadius.circular(shUi.radius.defaultRadius),
|
|
115
|
+
),
|
|
116
|
+
child: Row(
|
|
117
|
+
children: [
|
|
118
|
+
Expanded(
|
|
119
|
+
child: Text(
|
|
120
|
+
displayText ?? widget.placeholder,
|
|
121
|
+
style: TextStyle(
|
|
122
|
+
color: displayText != null
|
|
123
|
+
? colors.foreground
|
|
124
|
+
: colors.foregroundMuted,
|
|
125
|
+
fontSize: shUi.text.sm,
|
|
126
|
+
),
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
Icon(Icons.calendar_today_outlined,
|
|
130
|
+
size: 16, color: colors.foregroundMuted),
|
|
131
|
+
],
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// sh-ui DateRangePicker — 날짜 범위 선택.
|
|
141
|
+
class ShUiDateRangePicker extends StatefulWidget {
|
|
142
|
+
final DateTimeRange? value;
|
|
143
|
+
final ValueChanged<DateTimeRange?>? onValueChange;
|
|
144
|
+
final String placeholder;
|
|
145
|
+
final DateTime? min;
|
|
146
|
+
final DateTime? max;
|
|
147
|
+
final String Function(DateTime)? formatDate;
|
|
148
|
+
final bool enabled;
|
|
149
|
+
|
|
150
|
+
const ShUiDateRangePicker({
|
|
151
|
+
super.key,
|
|
152
|
+
this.value,
|
|
153
|
+
this.onValueChange,
|
|
154
|
+
this.placeholder = '시작일 ~ 종료일',
|
|
155
|
+
this.min,
|
|
156
|
+
this.max,
|
|
157
|
+
this.formatDate,
|
|
158
|
+
this.enabled = true,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
@override
|
|
162
|
+
State<ShUiDateRangePicker> createState() => _ShUiDateRangePickerState();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
class _ShUiDateRangePickerState extends State<ShUiDateRangePicker> {
|
|
166
|
+
final LayerLink _layerLink = LayerLink();
|
|
167
|
+
OverlayEntry? _overlay;
|
|
168
|
+
bool _isOpen = false;
|
|
169
|
+
late DateTime _focusedMonth;
|
|
170
|
+
DateTime? _picking; // 첫 번째 날짜 선택 후 두 번째 대기 중
|
|
171
|
+
|
|
172
|
+
@override
|
|
173
|
+
void initState() {
|
|
174
|
+
super.initState();
|
|
175
|
+
_focusedMonth = widget.value?.start ?? DateTime.now();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
String _defaultFormat(DateTime d) =>
|
|
179
|
+
'${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
|
|
180
|
+
|
|
181
|
+
void _open() {
|
|
182
|
+
if (_overlay != null) return;
|
|
183
|
+
_picking = null;
|
|
184
|
+
_focusedMonth = widget.value?.start ?? DateTime.now();
|
|
185
|
+
_overlay = OverlayEntry(builder: (_) => _buildOverlay());
|
|
186
|
+
Overlay.of(context).insert(_overlay!);
|
|
187
|
+
setState(() => _isOpen = true);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
void _close() {
|
|
191
|
+
_overlay?.remove();
|
|
192
|
+
_overlay = null;
|
|
193
|
+
if (mounted) setState(() => _isOpen = false);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
void _select(DateTime date) {
|
|
197
|
+
if (_picking == null) {
|
|
198
|
+
_picking = date;
|
|
199
|
+
_overlay?.markNeedsBuild();
|
|
200
|
+
} else {
|
|
201
|
+
final from = _picking!.isBefore(date) ? _picking! : date;
|
|
202
|
+
final to = _picking!.isBefore(date) ? date : _picking!;
|
|
203
|
+
widget.onValueChange?.call(DateTimeRange(start: from, end: to));
|
|
204
|
+
_picking = null;
|
|
205
|
+
_close();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@override
|
|
210
|
+
void dispose() {
|
|
211
|
+
_close();
|
|
212
|
+
super.dispose();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
Widget _buildOverlay() {
|
|
216
|
+
return _DatePickerOverlay(
|
|
217
|
+
link: _layerLink,
|
|
218
|
+
selected: _picking,
|
|
219
|
+
focusedMonth: _focusedMonth,
|
|
220
|
+
onFocusedMonthChange: (d) {
|
|
221
|
+
_focusedMonth = d;
|
|
222
|
+
_overlay?.markNeedsBuild();
|
|
223
|
+
},
|
|
224
|
+
onSelect: _select,
|
|
225
|
+
onDismiss: _close,
|
|
226
|
+
min: widget.min,
|
|
227
|
+
max: widget.max,
|
|
228
|
+
rangeFrom: _picking ?? widget.value?.start,
|
|
229
|
+
rangeTo: _picking != null ? null : widget.value?.end,
|
|
230
|
+
hint: _picking != null ? '종료일을 선택하세요' : null,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@override
|
|
235
|
+
Widget build(BuildContext context) {
|
|
236
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
237
|
+
final colors = shUi.colors;
|
|
238
|
+
final disabled = !widget.enabled;
|
|
239
|
+
final fmt = widget.formatDate ?? _defaultFormat;
|
|
240
|
+
final displayText = widget.value != null
|
|
241
|
+
? '${fmt(widget.value!.start)} ~ ${fmt(widget.value!.end)}'
|
|
242
|
+
: null;
|
|
243
|
+
|
|
244
|
+
return CompositedTransformTarget(
|
|
245
|
+
link: _layerLink,
|
|
246
|
+
child: Opacity(
|
|
247
|
+
opacity: disabled ? shUi.opacity.disabled : 1,
|
|
248
|
+
child: GestureDetector(
|
|
249
|
+
onTap: disabled ? null : () => _isOpen ? _close() : _open(),
|
|
250
|
+
child: Container(
|
|
251
|
+
height: shUi.control.md,
|
|
252
|
+
padding: EdgeInsets.symmetric(horizontal: shUi.spacing.s3),
|
|
253
|
+
decoration: BoxDecoration(
|
|
254
|
+
color: colors.background,
|
|
255
|
+
border: Border.all(
|
|
256
|
+
color: _isOpen ? colors.foreground : colors.border,
|
|
257
|
+
width: _isOpen ? shUi.borderWidth.strong : shUi.borderWidth.normal,
|
|
258
|
+
),
|
|
259
|
+
borderRadius: BorderRadius.circular(shUi.radius.defaultRadius),
|
|
260
|
+
),
|
|
261
|
+
child: Row(
|
|
262
|
+
children: [
|
|
263
|
+
Expanded(
|
|
264
|
+
child: Text(
|
|
265
|
+
displayText ?? widget.placeholder,
|
|
266
|
+
style: TextStyle(
|
|
267
|
+
color: displayText != null
|
|
268
|
+
? colors.foreground
|
|
269
|
+
: colors.foregroundMuted,
|
|
270
|
+
fontSize: shUi.text.sm,
|
|
271
|
+
),
|
|
272
|
+
),
|
|
273
|
+
),
|
|
274
|
+
Icon(Icons.calendar_today_outlined,
|
|
275
|
+
size: 16, color: colors.foregroundMuted),
|
|
276
|
+
],
|
|
277
|
+
),
|
|
278
|
+
),
|
|
279
|
+
),
|
|
280
|
+
),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* ───────── Calendar overlay (shared) ───────── */
|
|
286
|
+
|
|
287
|
+
class _DatePickerOverlay extends StatelessWidget {
|
|
288
|
+
final LayerLink link;
|
|
289
|
+
final DateTime? selected;
|
|
290
|
+
final DateTime focusedMonth;
|
|
291
|
+
final ValueChanged<DateTime> onFocusedMonthChange;
|
|
292
|
+
final ValueChanged<DateTime> onSelect;
|
|
293
|
+
final VoidCallback onDismiss;
|
|
294
|
+
final DateTime? min;
|
|
295
|
+
final DateTime? max;
|
|
296
|
+
final DateTime? rangeFrom;
|
|
297
|
+
final DateTime? rangeTo;
|
|
298
|
+
final String? hint;
|
|
299
|
+
|
|
300
|
+
const _DatePickerOverlay({
|
|
301
|
+
required this.link,
|
|
302
|
+
this.selected,
|
|
303
|
+
required this.focusedMonth,
|
|
304
|
+
required this.onFocusedMonthChange,
|
|
305
|
+
required this.onSelect,
|
|
306
|
+
required this.onDismiss,
|
|
307
|
+
this.min,
|
|
308
|
+
this.max,
|
|
309
|
+
this.rangeFrom,
|
|
310
|
+
this.rangeTo,
|
|
311
|
+
this.hint,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
@override
|
|
315
|
+
Widget build(BuildContext context) {
|
|
316
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
317
|
+
final colors = shUi.colors;
|
|
318
|
+
|
|
319
|
+
return Stack(
|
|
320
|
+
children: [
|
|
321
|
+
Positioned.fill(
|
|
322
|
+
child: GestureDetector(
|
|
323
|
+
onTap: onDismiss,
|
|
324
|
+
behavior: HitTestBehavior.opaque,
|
|
325
|
+
child: const SizedBox.expand(),
|
|
326
|
+
),
|
|
327
|
+
),
|
|
328
|
+
CompositedTransformFollower(
|
|
329
|
+
link: link,
|
|
330
|
+
offset: const Offset(0, 44),
|
|
331
|
+
child: Material(
|
|
332
|
+
color: Colors.transparent,
|
|
333
|
+
child: Container(
|
|
334
|
+
width: 280,
|
|
335
|
+
padding: EdgeInsets.all(shUi.spacing.s3),
|
|
336
|
+
decoration: BoxDecoration(
|
|
337
|
+
color: colors.background,
|
|
338
|
+
border: Border.all(color: colors.border),
|
|
339
|
+
borderRadius: BorderRadius.circular(shUi.radius.defaultRadius),
|
|
340
|
+
boxShadow: [
|
|
341
|
+
BoxShadow(
|
|
342
|
+
color: Colors.black.withValues(alpha: 0.08),
|
|
343
|
+
blurRadius: 8,
|
|
344
|
+
offset: const Offset(0, 4),
|
|
345
|
+
),
|
|
346
|
+
],
|
|
347
|
+
),
|
|
348
|
+
child: Column(
|
|
349
|
+
mainAxisSize: MainAxisSize.min,
|
|
350
|
+
children: [
|
|
351
|
+
if (hint != null) ...[
|
|
352
|
+
Text(
|
|
353
|
+
hint!,
|
|
354
|
+
style: TextStyle(
|
|
355
|
+
color: colors.foregroundMuted,
|
|
356
|
+
fontSize: shUi.text.xs,
|
|
357
|
+
),
|
|
358
|
+
),
|
|
359
|
+
SizedBox(height: shUi.spacing.s2),
|
|
360
|
+
],
|
|
361
|
+
_ShUiCalendar(
|
|
362
|
+
focusedMonth: focusedMonth,
|
|
363
|
+
selected: selected,
|
|
364
|
+
onSelect: onSelect,
|
|
365
|
+
onFocusedMonthChange: onFocusedMonthChange,
|
|
366
|
+
min: min,
|
|
367
|
+
max: max,
|
|
368
|
+
rangeFrom: rangeFrom,
|
|
369
|
+
rangeTo: rangeTo,
|
|
370
|
+
colors: colors,
|
|
371
|
+
radius: shUi.radius,
|
|
372
|
+
),
|
|
373
|
+
],
|
|
374
|
+
),
|
|
375
|
+
),
|
|
376
|
+
),
|
|
377
|
+
),
|
|
378
|
+
],
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/* ───────── Calendar grid ───────── */
|
|
384
|
+
|
|
385
|
+
const _weekLabels = ['일', '월', '화', '수', '목', '금', '토'];
|
|
386
|
+
|
|
387
|
+
bool _isSameDay(DateTime a, DateTime b) =>
|
|
388
|
+
a.year == b.year && a.month == b.month && a.day == b.day;
|
|
389
|
+
|
|
390
|
+
class _ShUiCalendar extends StatelessWidget {
|
|
391
|
+
final DateTime focusedMonth;
|
|
392
|
+
final DateTime? selected;
|
|
393
|
+
final ValueChanged<DateTime> onSelect;
|
|
394
|
+
final ValueChanged<DateTime> onFocusedMonthChange;
|
|
395
|
+
final DateTime? min;
|
|
396
|
+
final DateTime? max;
|
|
397
|
+
final DateTime? rangeFrom;
|
|
398
|
+
final DateTime? rangeTo;
|
|
399
|
+
final ShUiColorTokens colors;
|
|
400
|
+
final ShUiRadiusTokens radius;
|
|
401
|
+
|
|
402
|
+
const _ShUiCalendar({
|
|
403
|
+
required this.focusedMonth,
|
|
404
|
+
this.selected,
|
|
405
|
+
required this.onSelect,
|
|
406
|
+
required this.onFocusedMonthChange,
|
|
407
|
+
this.min,
|
|
408
|
+
this.max,
|
|
409
|
+
this.rangeFrom,
|
|
410
|
+
this.rangeTo,
|
|
411
|
+
required this.colors,
|
|
412
|
+
required this.radius,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
List<_DayCell> _getDaysGrid() {
|
|
416
|
+
final year = focusedMonth.year;
|
|
417
|
+
final month = focusedMonth.month;
|
|
418
|
+
final first = DateTime(year, month, 1);
|
|
419
|
+
final startDay = first.weekday % 7; // 0=Sun
|
|
420
|
+
final daysInMonth = DateTime(year, month + 1, 0).day;
|
|
421
|
+
final prevDays = DateTime(year, month, 0).day;
|
|
422
|
+
|
|
423
|
+
final cells = <_DayCell>[];
|
|
424
|
+
|
|
425
|
+
for (var i = startDay - 1; i >= 0; i--) {
|
|
426
|
+
cells.add(_DayCell(
|
|
427
|
+
date: DateTime(year, month - 1, prevDays - i),
|
|
428
|
+
current: false,
|
|
429
|
+
));
|
|
430
|
+
}
|
|
431
|
+
for (var d = 1; d <= daysInMonth; d++) {
|
|
432
|
+
cells.add(_DayCell(date: DateTime(year, month, d), current: true));
|
|
433
|
+
}
|
|
434
|
+
final remaining = 7 - (cells.length % 7);
|
|
435
|
+
if (remaining < 7) {
|
|
436
|
+
for (var d = 1; d <= remaining; d++) {
|
|
437
|
+
cells.add(_DayCell(date: DateTime(year, month + 1, d), current: false));
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return cells;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
bool _isDisabled(DateTime date) {
|
|
445
|
+
if (min != null && date.isBefore(DateTime(min!.year, min!.month, min!.day))) {
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
if (max != null && date.isAfter(DateTime(max!.year, max!.month, max!.day))) {
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
bool _isInRange(DateTime date) {
|
|
455
|
+
if (rangeFrom == null) return false;
|
|
456
|
+
final end = rangeTo;
|
|
457
|
+
if (end == null) return false;
|
|
458
|
+
final start = rangeFrom!.isBefore(end) ? rangeFrom! : end;
|
|
459
|
+
final finish = rangeFrom!.isBefore(end) ? end : rangeFrom!;
|
|
460
|
+
return !date.isBefore(start) && !date.isAfter(finish);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
@override
|
|
464
|
+
Widget build(BuildContext context) {
|
|
465
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
466
|
+
final year = focusedMonth.year;
|
|
467
|
+
final month = focusedMonth.month;
|
|
468
|
+
final cells = _getDaysGrid();
|
|
469
|
+
final today = DateTime.now();
|
|
470
|
+
final monthLabel = '$year년 $month월';
|
|
471
|
+
|
|
472
|
+
return Column(
|
|
473
|
+
mainAxisSize: MainAxisSize.min,
|
|
474
|
+
children: [
|
|
475
|
+
// Header
|
|
476
|
+
Row(
|
|
477
|
+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
478
|
+
children: [
|
|
479
|
+
GestureDetector(
|
|
480
|
+
onTap: () {
|
|
481
|
+
final m = month - 1;
|
|
482
|
+
onFocusedMonthChange(
|
|
483
|
+
m < 1 ? DateTime(year - 1, 12, 1) : DateTime(year, m, 1),
|
|
484
|
+
);
|
|
485
|
+
},
|
|
486
|
+
child: Icon(Icons.chevron_left, size: 20, color: colors.foreground),
|
|
487
|
+
),
|
|
488
|
+
Text(
|
|
489
|
+
monthLabel,
|
|
490
|
+
style: TextStyle(
|
|
491
|
+
color: colors.foreground,
|
|
492
|
+
fontSize: shUi.text.sm,
|
|
493
|
+
fontWeight: shUi.weight.semibold,
|
|
494
|
+
),
|
|
495
|
+
),
|
|
496
|
+
GestureDetector(
|
|
497
|
+
onTap: () {
|
|
498
|
+
final m = month + 1;
|
|
499
|
+
onFocusedMonthChange(
|
|
500
|
+
m > 12 ? DateTime(year + 1, 1, 1) : DateTime(year, m, 1),
|
|
501
|
+
);
|
|
502
|
+
},
|
|
503
|
+
child: Icon(Icons.chevron_right, size: 20, color: colors.foreground),
|
|
504
|
+
),
|
|
505
|
+
],
|
|
506
|
+
),
|
|
507
|
+
SizedBox(height: shUi.spacing.s2),
|
|
508
|
+
// Weekday labels
|
|
509
|
+
Row(
|
|
510
|
+
children: _weekLabels
|
|
511
|
+
.map((l) => Expanded(
|
|
512
|
+
child: Center(
|
|
513
|
+
child: Text(
|
|
514
|
+
l,
|
|
515
|
+
style: TextStyle(
|
|
516
|
+
color: colors.foregroundMuted,
|
|
517
|
+
fontSize: shUi.text.xs,
|
|
518
|
+
fontWeight: shUi.weight.medium,
|
|
519
|
+
),
|
|
520
|
+
),
|
|
521
|
+
),
|
|
522
|
+
))
|
|
523
|
+
.toList(),
|
|
524
|
+
),
|
|
525
|
+
SizedBox(height: shUi.spacing.s1),
|
|
526
|
+
// Day grid
|
|
527
|
+
...List.generate((cells.length / 7).ceil(), (row) {
|
|
528
|
+
return Row(
|
|
529
|
+
children: List.generate(7, (col) {
|
|
530
|
+
final idx = row * 7 + col;
|
|
531
|
+
if (idx >= cells.length) return const Expanded(child: SizedBox());
|
|
532
|
+
final cell = cells[idx];
|
|
533
|
+
final disabled = _isDisabled(cell.date);
|
|
534
|
+
final isSelected = selected != null && _isSameDay(cell.date, selected!);
|
|
535
|
+
final isToday = _isSameDay(cell.date, today);
|
|
536
|
+
final inRange = _isInRange(cell.date);
|
|
537
|
+
|
|
538
|
+
return Expanded(
|
|
539
|
+
child: _DayButton(
|
|
540
|
+
day: cell.date.day,
|
|
541
|
+
current: cell.current,
|
|
542
|
+
selected: isSelected,
|
|
543
|
+
today: isToday,
|
|
544
|
+
inRange: inRange,
|
|
545
|
+
disabled: disabled,
|
|
546
|
+
onTap: disabled ? null : () => onSelect(cell.date),
|
|
547
|
+
colors: colors,
|
|
548
|
+
radius: radius,
|
|
549
|
+
),
|
|
550
|
+
);
|
|
551
|
+
}),
|
|
552
|
+
);
|
|
553
|
+
}),
|
|
554
|
+
],
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
class _DayCell {
|
|
560
|
+
final DateTime date;
|
|
561
|
+
final bool current;
|
|
562
|
+
_DayCell({required this.date, required this.current});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
class _DayButton extends StatefulWidget {
|
|
566
|
+
final int day;
|
|
567
|
+
final bool current;
|
|
568
|
+
final bool selected;
|
|
569
|
+
final bool today;
|
|
570
|
+
final bool inRange;
|
|
571
|
+
final bool disabled;
|
|
572
|
+
final VoidCallback? onTap;
|
|
573
|
+
final ShUiColorTokens colors;
|
|
574
|
+
final ShUiRadiusTokens radius;
|
|
575
|
+
|
|
576
|
+
const _DayButton({
|
|
577
|
+
required this.day,
|
|
578
|
+
required this.current,
|
|
579
|
+
required this.selected,
|
|
580
|
+
required this.today,
|
|
581
|
+
required this.inRange,
|
|
582
|
+
required this.disabled,
|
|
583
|
+
this.onTap,
|
|
584
|
+
required this.colors,
|
|
585
|
+
required this.radius,
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
@override
|
|
589
|
+
State<_DayButton> createState() => _DayButtonState();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
class _DayButtonState extends State<_DayButton> {
|
|
593
|
+
bool _hover = false;
|
|
594
|
+
|
|
595
|
+
@override
|
|
596
|
+
Widget build(BuildContext context) {
|
|
597
|
+
Color bg;
|
|
598
|
+
Color fg;
|
|
599
|
+
|
|
600
|
+
if (widget.selected) {
|
|
601
|
+
bg = widget.colors.primary;
|
|
602
|
+
fg = widget.colors.primaryForeground;
|
|
603
|
+
} else if (widget.inRange) {
|
|
604
|
+
bg = widget.colors.primary.withValues(alpha: 0.1);
|
|
605
|
+
fg = widget.colors.foreground;
|
|
606
|
+
} else if (_hover && !widget.disabled) {
|
|
607
|
+
bg = widget.colors.backgroundMuted;
|
|
608
|
+
fg = widget.colors.foreground;
|
|
609
|
+
} else {
|
|
610
|
+
bg = Colors.transparent;
|
|
611
|
+
fg = widget.current ? widget.colors.foreground : widget.colors.foregroundMuted;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
|
|
615
|
+
return MouseRegion(
|
|
616
|
+
onEnter: (_) => setState(() => _hover = true),
|
|
617
|
+
onExit: (_) => setState(() => _hover = false),
|
|
618
|
+
child: GestureDetector(
|
|
619
|
+
onTap: widget.onTap,
|
|
620
|
+
child: Container(
|
|
621
|
+
height: shUi.control.sm,
|
|
622
|
+
margin: const EdgeInsets.all(1),
|
|
623
|
+
decoration: BoxDecoration(
|
|
624
|
+
color: bg,
|
|
625
|
+
borderRadius: BorderRadius.circular(widget.radius.defaultRadius - 2),
|
|
626
|
+
border: widget.today && !widget.selected
|
|
627
|
+
? Border.all(color: widget.colors.primary, width: shUi.borderWidth.normal)
|
|
628
|
+
: null,
|
|
629
|
+
),
|
|
630
|
+
alignment: Alignment.center,
|
|
631
|
+
child: Opacity(
|
|
632
|
+
opacity: widget.disabled ? 0.3 : 1,
|
|
633
|
+
child: Text(
|
|
634
|
+
'${widget.day}',
|
|
635
|
+
style: TextStyle(
|
|
636
|
+
color: fg,
|
|
637
|
+
fontSize: 13,
|
|
638
|
+
fontWeight: widget.today || widget.selected
|
|
639
|
+
? shUi.weight.semibold
|
|
640
|
+
: shUi.weight.regular,
|
|
641
|
+
),
|
|
642
|
+
),
|
|
643
|
+
),
|
|
644
|
+
),
|
|
645
|
+
),
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
}
|