sh-ui-cli 0.15.0 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/bin/sh-ui.mjs +6 -0
  2. package/data/changelog/versions.json +366 -0
  3. package/data/registry/flutter/foundation/sh_ui_tokens.dart +385 -0
  4. package/data/registry/flutter/registry.json +336 -0
  5. package/data/registry/flutter/widgets/sh_ui_accordion.dart +255 -0
  6. package/data/registry/flutter/widgets/sh_ui_app_shell.dart +267 -0
  7. package/data/registry/flutter/widgets/sh_ui_avatar.dart +95 -0
  8. package/data/registry/flutter/widgets/sh_ui_badge.dart +82 -0
  9. package/data/registry/flutter/widgets/sh_ui_breadcrumb.dart +107 -0
  10. package/data/registry/flutter/widgets/sh_ui_button.dart +201 -0
  11. package/data/registry/flutter/widgets/sh_ui_card.dart +159 -0
  12. package/data/registry/flutter/widgets/sh_ui_carousel.dart +204 -0
  13. package/data/registry/flutter/widgets/sh_ui_checkbox.dart +154 -0
  14. package/data/registry/flutter/widgets/sh_ui_color_picker.dart +264 -0
  15. package/data/registry/flutter/widgets/sh_ui_combobox.dart +614 -0
  16. package/data/registry/flutter/widgets/sh_ui_context_menu.dart +71 -0
  17. package/data/registry/flutter/widgets/sh_ui_date_picker.dart +648 -0
  18. package/data/registry/flutter/widgets/sh_ui_dialog.dart +567 -0
  19. package/data/registry/flutter/widgets/sh_ui_dropdown_menu.dart +251 -0
  20. package/data/registry/flutter/widgets/sh_ui_file_upload.dart +200 -0
  21. package/data/registry/flutter/widgets/sh_ui_header.dart +488 -0
  22. package/data/registry/flutter/widgets/sh_ui_input.dart +664 -0
  23. package/data/registry/flutter/widgets/sh_ui_label.dart +145 -0
  24. package/data/registry/flutter/widgets/sh_ui_menubar.dart +98 -0
  25. package/data/registry/flutter/widgets/sh_ui_pagination.dart +276 -0
  26. package/data/registry/flutter/widgets/sh_ui_popover.dart +248 -0
  27. package/data/registry/flutter/widgets/sh_ui_progress.dart +47 -0
  28. package/data/registry/flutter/widgets/sh_ui_radio.dart +108 -0
  29. package/data/registry/flutter/widgets/sh_ui_select.dart +904 -0
  30. package/data/registry/flutter/widgets/sh_ui_separator.dart +42 -0
  31. package/data/registry/flutter/widgets/sh_ui_sidebar.dart +1116 -0
  32. package/data/registry/flutter/widgets/sh_ui_skeleton.dart +129 -0
  33. package/data/registry/flutter/widgets/sh_ui_slider.dart +147 -0
  34. package/data/registry/flutter/widgets/sh_ui_spinner.dart +56 -0
  35. package/data/registry/flutter/widgets/sh_ui_switch.dart +109 -0
  36. package/data/registry/flutter/widgets/sh_ui_tabs.dart +329 -0
  37. package/data/registry/flutter/widgets/sh_ui_textarea.dart +126 -0
  38. package/data/registry/flutter/widgets/sh_ui_toast.dart +362 -0
  39. package/data/registry/flutter/widgets/sh_ui_toggle.dart +229 -0
  40. package/data/registry/flutter/widgets/sh_ui_tooltip.dart +62 -0
  41. package/data/registry/react/components/accordion/index.tsx +85 -0
  42. package/data/registry/react/components/accordion/styles.css +94 -0
  43. package/data/registry/react/components/animations/animations.css +51 -0
  44. package/data/registry/react/components/avatar/index.tsx +75 -0
  45. package/data/registry/react/components/avatar/styles.css +36 -0
  46. package/data/registry/react/components/badge/index.tsx +42 -0
  47. package/data/registry/react/components/badge/styles.css +57 -0
  48. package/data/registry/react/components/base/base.css +102 -0
  49. package/data/registry/react/components/breadcrumb/index.tsx +154 -0
  50. package/data/registry/react/components/breadcrumb/styles.css +82 -0
  51. package/data/registry/react/components/breakpoints/breakpoints.css +17 -0
  52. package/data/registry/react/components/button/index.tsx +47 -0
  53. package/data/registry/react/components/button/styles.css +93 -0
  54. package/data/registry/react/components/card/index.tsx +86 -0
  55. package/data/registry/react/components/card/styles.css +73 -0
  56. package/data/registry/react/components/carousel/index.tsx +432 -0
  57. package/data/registry/react/components/carousel/styles.css +155 -0
  58. package/data/registry/react/components/checkbox/index.tsx +98 -0
  59. package/data/registry/react/components/checkbox/styles.css +75 -0
  60. package/data/registry/react/components/code-panel/copy.tsx +56 -0
  61. package/data/registry/react/components/code-panel/index.tsx +193 -0
  62. package/data/registry/react/components/code-panel/styles.css +124 -0
  63. package/data/registry/react/components/color-picker/index.tsx +466 -0
  64. package/data/registry/react/components/color-picker/styles.css +166 -0
  65. package/data/registry/react/components/combobox/index.tsx +167 -0
  66. package/data/registry/react/components/combobox/styles.css +151 -0
  67. package/data/registry/react/components/context-menu/index.tsx +253 -0
  68. package/data/registry/react/components/context-menu/styles.css +140 -0
  69. package/data/registry/react/components/date-picker/index.tsx +757 -0
  70. package/data/registry/react/components/date-picker/styles.css +279 -0
  71. package/data/registry/react/components/dialog/index.tsx +97 -0
  72. package/data/registry/react/components/dialog/styles.css +127 -0
  73. package/data/registry/react/components/dropdown-menu/index.tsx +257 -0
  74. package/data/registry/react/components/dropdown-menu/styles.css +150 -0
  75. package/data/registry/react/components/file-upload/index.tsx +489 -0
  76. package/data/registry/react/components/file-upload/styles.css +170 -0
  77. package/data/registry/react/components/focus-ring/focus-ring.css +23 -0
  78. package/data/registry/react/components/form/context.ts +92 -0
  79. package/data/registry/react/components/form/field.test.tsx +230 -0
  80. package/data/registry/react/components/form/field.tsx +236 -0
  81. package/data/registry/react/components/form/focus-first-error.ts +54 -0
  82. package/data/registry/react/components/form/form.section.test.tsx +58 -0
  83. package/data/registry/react/components/form/form.test.tsx +146 -0
  84. package/data/registry/react/components/form/form.tsx +180 -0
  85. package/data/registry/react/components/form/index.tsx +61 -0
  86. package/data/registry/react/components/form/steps.test.tsx +106 -0
  87. package/data/registry/react/components/form/steps.tsx +193 -0
  88. package/data/registry/react/components/form/store.test.ts +206 -0
  89. package/data/registry/react/components/form/store.ts +318 -0
  90. package/data/registry/react/components/form/styles.css +47 -0
  91. package/data/registry/react/components/form/types.ts +104 -0
  92. package/data/registry/react/components/form/use-sh-ui-form.ts +15 -0
  93. package/data/registry/react/components/form/utils.test.ts +44 -0
  94. package/data/registry/react/components/form/utils.ts +49 -0
  95. package/data/registry/react/components/form/validation.test.ts +67 -0
  96. package/data/registry/react/components/form/validation.ts +64 -0
  97. package/data/registry/react/components/form-rhf/README.md +27 -0
  98. package/data/registry/react/components/form-rhf/index.tsx +289 -0
  99. package/data/registry/react/components/form-rhf/rhf.test.tsx +42 -0
  100. package/data/registry/react/components/form-tanstack/README.md +27 -0
  101. package/data/registry/react/components/form-tanstack/index.tsx +352 -0
  102. package/data/registry/react/components/form-tanstack/tanstack.test.tsx +45 -0
  103. package/data/registry/react/components/form-yup/README.md +22 -0
  104. package/data/registry/react/components/form-yup/index.tsx +50 -0
  105. package/data/registry/react/components/form-yup/yup.test.ts +27 -0
  106. package/data/registry/react/components/header/index.tsx +257 -0
  107. package/data/registry/react/components/header/styles.css +190 -0
  108. package/data/registry/react/components/input/index.tsx +517 -0
  109. package/data/registry/react/components/input/styles.css +203 -0
  110. package/data/registry/react/components/label/index.tsx +54 -0
  111. package/data/registry/react/components/label/styles.css +90 -0
  112. package/data/registry/react/components/menubar/index.tsx +34 -0
  113. package/data/registry/react/components/menubar/styles.css +45 -0
  114. package/data/registry/react/components/pagination/index.tsx +271 -0
  115. package/data/registry/react/components/pagination/styles.css +105 -0
  116. package/data/registry/react/components/popover/index.tsx +115 -0
  117. package/data/registry/react/components/popover/styles.css +65 -0
  118. package/data/registry/react/components/progress/index.tsx +56 -0
  119. package/data/registry/react/components/progress/styles.css +41 -0
  120. package/data/registry/react/components/radio/index.tsx +67 -0
  121. package/data/registry/react/components/radio/styles.css +80 -0
  122. package/data/registry/react/components/select/index.tsx +236 -0
  123. package/data/registry/react/components/select/styles.css +193 -0
  124. package/data/registry/react/components/separator/index.tsx +48 -0
  125. package/data/registry/react/components/separator/styles.css +15 -0
  126. package/data/registry/react/components/sidebar/index.tsx +1084 -0
  127. package/data/registry/react/components/sidebar/styles.css +502 -0
  128. package/data/registry/react/components/skeleton/index.tsx +24 -0
  129. package/data/registry/react/components/skeleton/styles.css +24 -0
  130. package/data/registry/react/components/slider/index.tsx +300 -0
  131. package/data/registry/react/components/slider/styles.css +64 -0
  132. package/data/registry/react/components/spinner/index.tsx +40 -0
  133. package/data/registry/react/components/spinner/styles.css +37 -0
  134. package/data/registry/react/components/switch/index.tsx +41 -0
  135. package/data/registry/react/components/switch/styles.css +83 -0
  136. package/data/registry/react/components/tabs/index.tsx +93 -0
  137. package/data/registry/react/components/tabs/styles.css +148 -0
  138. package/data/registry/react/components/textarea/index.tsx +25 -0
  139. package/data/registry/react/components/textarea/styles.css +54 -0
  140. package/data/registry/react/components/theme/index.tsx +91 -0
  141. package/data/registry/react/components/toast/index.tsx +257 -0
  142. package/data/registry/react/components/toast/styles.css +290 -0
  143. package/data/registry/react/components/toggle/index.tsx +133 -0
  144. package/data/registry/react/components/toggle/styles.css +85 -0
  145. package/data/registry/react/components/tooltip/index.tsx +85 -0
  146. package/data/registry/react/components/tooltip/styles.css +44 -0
  147. package/data/registry/react/components/z-index/z-index.css +16 -0
  148. package/data/registry/react/hooks/use-active-section.ts +104 -0
  149. package/data/registry/react/hooks/use-media-query.ts +27 -0
  150. package/data/registry/react/lib/cn.ts +39 -0
  151. package/data/registry/react/peer-versions.json +10 -0
  152. package/data/registry/react/registry.json +835 -0
  153. package/data/summaries/flutter.json +42 -0
  154. package/data/summaries/react.json +50 -0
  155. package/data/tokens/build.mjs +553 -0
  156. package/data/tokens/src/primitives.json +146 -0
  157. package/data/tokens/src/semantic.json +146 -0
  158. package/package.json +9 -2
  159. package/src/add.mjs +41 -15
  160. package/src/list.mjs +3 -11
  161. package/src/mcp.mjs +308 -0
  162. package/src/paths.mjs +59 -0
  163. package/src/remove.mjs +4 -11
@@ -0,0 +1,129 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../foundation/sh_ui_tokens.dart';
3
+
4
+ /// shUi Skeleton — 데이터 로딩 중 자리를 대신 차지하는 플레이스홀더.
5
+ ///
6
+ /// 기본은 풀너비 직사각형이며, 은은한 shimmer 애니메이션이 적용된다.
7
+ /// 편의 생성자:
8
+ /// - [ShUiSkeleton.text] : 한 줄 텍스트 자리 (기본 높이 14px).
9
+ /// - [ShUiSkeleton.avatar]: 원형 아바타 자리.
10
+ /// - [ShUiSkeleton.block] : 블록 영역 자리 (카드 본문 등).
11
+ class ShUiSkeleton extends StatefulWidget {
12
+ /// 너비. `null`이면 가로를 가득 채운다.
13
+ final double? width;
14
+
15
+ /// 높이. `null`이면 부모가 주는 높이에 따른다 (기본 16px).
16
+ final double? height;
17
+
18
+ /// 모서리 둥글기. `null`이면 토큰의 기본값을 사용한다.
19
+ final double? borderRadius;
20
+
21
+ const ShUiSkeleton({
22
+ super.key,
23
+ this.width,
24
+ this.height,
25
+ this.borderRadius,
26
+ });
27
+
28
+ /// 한 줄 텍스트 자리. [width]/[height] 기본값을 살짝 낮춰 둔다.
29
+ factory ShUiSkeleton.text({
30
+ Key? key,
31
+ double? width,
32
+ double height = 14,
33
+ double? borderRadius,
34
+ }) {
35
+ return ShUiSkeleton(
36
+ key: key,
37
+ width: width,
38
+ height: height,
39
+ borderRadius: borderRadius,
40
+ );
41
+ }
42
+
43
+ /// 원형 아바타 자리. [size] × [size] 정사각형에 완전 원형.
44
+ factory ShUiSkeleton.avatar({
45
+ Key? key,
46
+ double size = 40,
47
+ }) {
48
+ return ShUiSkeleton(
49
+ key: key,
50
+ width: size,
51
+ height: size,
52
+ borderRadius: size / 2,
53
+ );
54
+ }
55
+
56
+ /// 풀너비 블록 자리. 카드 본문·이미지 자리 등.
57
+ factory ShUiSkeleton.block({
58
+ Key? key,
59
+ double height = 96,
60
+ double? borderRadius,
61
+ }) {
62
+ return ShUiSkeleton(
63
+ key: key,
64
+ height: height,
65
+ borderRadius: borderRadius,
66
+ );
67
+ }
68
+
69
+ @override
70
+ State<ShUiSkeleton> createState() => _ShUiSkeletonState();
71
+ }
72
+
73
+ class _ShUiSkeletonState extends State<ShUiSkeleton>
74
+ with SingleTickerProviderStateMixin {
75
+ late final AnimationController _controller;
76
+
77
+ @override
78
+ void initState() {
79
+ super.initState();
80
+ _controller = AnimationController(
81
+ vsync: this,
82
+ // 토큰 duration.slow (200ms) 기반 6배 — shimmer 사이클은 여유있게.
83
+ duration: const Duration(milliseconds: 1200),
84
+ )..repeat();
85
+ }
86
+
87
+ @override
88
+ void dispose() {
89
+ _controller.dispose();
90
+ super.dispose();
91
+ }
92
+
93
+ @override
94
+ Widget build(BuildContext context) {
95
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
96
+ final base = shUi.colors.backgroundMuted;
97
+ final highlight = Color.lerp(base, shUi.colors.background, 0.6) ?? base;
98
+ final radius = widget.borderRadius ?? shUi.radius.defaultRadius;
99
+
100
+ return ClipRRect(
101
+ borderRadius: BorderRadius.circular(radius),
102
+ child: SizedBox(
103
+ width: widget.width,
104
+ height: widget.height ?? 16,
105
+ child: AnimatedBuilder(
106
+ animation: _controller,
107
+ builder: (context, _) {
108
+ final t = _controller.value;
109
+ return DecoratedBox(
110
+ decoration: BoxDecoration(
111
+ color: base,
112
+ gradient: LinearGradient(
113
+ begin: Alignment(-1 + (2 * t) - 0.6, 0),
114
+ end: Alignment(-1 + (2 * t) + 0.6, 0),
115
+ colors: [
116
+ base,
117
+ highlight,
118
+ base,
119
+ ],
120
+ stops: const [0.0, 0.5, 1.0],
121
+ ),
122
+ ),
123
+ );
124
+ },
125
+ ),
126
+ ),
127
+ );
128
+ }
129
+ }
@@ -0,0 +1,147 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../foundation/sh_ui_tokens.dart';
3
+
4
+ /// sh-ui Slider — 범위 슬라이더.
5
+ class ShUiSlider extends StatefulWidget {
6
+ final double value;
7
+ final ValueChanged<double>? onChanged;
8
+ final double min;
9
+ final double max;
10
+ final double step;
11
+ final bool enabled;
12
+
13
+ const ShUiSlider({
14
+ super.key,
15
+ required this.value,
16
+ this.onChanged,
17
+ this.min = 0,
18
+ this.max = 100,
19
+ this.step = 1,
20
+ this.enabled = true,
21
+ });
22
+
23
+ @override
24
+ State<ShUiSlider> createState() => _ShUiSliderState();
25
+ }
26
+
27
+ class _ShUiSliderState extends State<ShUiSlider> {
28
+ bool _hover = false;
29
+ bool _dragging = false;
30
+
31
+ double _clamp(double v) => v.clamp(widget.min, widget.max);
32
+
33
+ double _snap(double v) {
34
+ if (widget.step <= 0) return v;
35
+ return widget.min +
36
+ ((v - widget.min) / widget.step).round() * widget.step;
37
+ }
38
+
39
+ void _setValue(double next) {
40
+ final snapped = _clamp(_snap(next));
41
+ if (snapped == widget.value) return;
42
+ widget.onChanged?.call(snapped);
43
+ }
44
+
45
+ void _handleDrag(Offset localPosition, double trackWidth) {
46
+ final ratio = (localPosition.dx / trackWidth).clamp(0.0, 1.0);
47
+ _setValue(widget.min + ratio * (widget.max - widget.min));
48
+ }
49
+
50
+ @override
51
+ Widget build(BuildContext context) {
52
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
53
+ final colors = shUi.colors;
54
+ final disabled = !widget.enabled || widget.onChanged == null;
55
+ final ratio = widget.max == widget.min
56
+ ? 0.0
57
+ : (widget.value - widget.min) / (widget.max - widget.min);
58
+
59
+ return Opacity(
60
+ opacity: disabled ? shUi.opacity.disabled : 1,
61
+ child: MouseRegion(
62
+ cursor: disabled ? SystemMouseCursors.basic : SystemMouseCursors.click,
63
+ onEnter: (_) => setState(() => _hover = true),
64
+ onExit: (_) => setState(() => _hover = false),
65
+ child: LayoutBuilder(
66
+ builder: (context, constraints) {
67
+ final trackWidth = constraints.maxWidth;
68
+ const trackHeight = 6.0;
69
+ const thumbSize = 18.0;
70
+ const totalHeight = thumbSize + 4;
71
+
72
+ return GestureDetector(
73
+ onHorizontalDragStart: disabled
74
+ ? null
75
+ : (d) {
76
+ setState(() => _dragging = true);
77
+ _handleDrag(d.localPosition, trackWidth);
78
+ },
79
+ onHorizontalDragUpdate: disabled
80
+ ? null
81
+ : (d) => _handleDrag(d.localPosition, trackWidth),
82
+ onHorizontalDragEnd: disabled
83
+ ? null
84
+ : (_) => setState(() => _dragging = false),
85
+ onTapDown: disabled
86
+ ? null
87
+ : (d) => _handleDrag(d.localPosition, trackWidth),
88
+ child: SizedBox(
89
+ width: trackWidth,
90
+ height: totalHeight,
91
+ child: Stack(
92
+ alignment: Alignment.centerLeft,
93
+ clipBehavior: Clip.none,
94
+ children: [
95
+ // Track
96
+ Container(
97
+ width: trackWidth,
98
+ height: trackHeight,
99
+ decoration: BoxDecoration(
100
+ color: colors.backgroundMuted,
101
+ borderRadius: BorderRadius.circular(trackHeight / 2),
102
+ ),
103
+ ),
104
+ // Range (filled portion)
105
+ Container(
106
+ width: ratio * trackWidth,
107
+ height: trackHeight,
108
+ decoration: BoxDecoration(
109
+ color: colors.primary,
110
+ borderRadius: BorderRadius.circular(trackHeight / 2),
111
+ ),
112
+ ),
113
+ // Thumb
114
+ Positioned(
115
+ left: ratio * trackWidth - thumbSize / 2,
116
+ child: AnimatedContainer(
117
+ duration: const Duration(milliseconds: 80),
118
+ width: thumbSize,
119
+ height: thumbSize,
120
+ decoration: BoxDecoration(
121
+ color: colors.background,
122
+ shape: BoxShape.circle,
123
+ border: Border.all(
124
+ color: colors.primary,
125
+ width: shUi.borderWidth.strong,
126
+ ),
127
+ boxShadow: (_hover || _dragging)
128
+ ? [
129
+ BoxShadow(
130
+ color: colors.primary.withValues(alpha: 0.2),
131
+ blurRadius: 8,
132
+ ),
133
+ ]
134
+ : [],
135
+ ),
136
+ ),
137
+ ),
138
+ ],
139
+ ),
140
+ ),
141
+ );
142
+ },
143
+ ),
144
+ ),
145
+ );
146
+ }
147
+ }
@@ -0,0 +1,56 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../foundation/sh_ui_tokens.dart';
3
+
4
+ enum ShUiSpinnerSize { sm, md, lg }
5
+
6
+ /// sh-ui Spinner — 짧은 비동기 작업의 로딩 표시.
7
+ ///
8
+ /// 버튼·입력 등에 인라인으로 사용. 200ms 이상 걸리는 작업에 즉시 피드백을
9
+ /// 주는 원칙에 맞춘다.
10
+ class ShUiSpinner extends StatelessWidget {
11
+ final ShUiSpinnerSize size;
12
+
13
+ /// 색상. 생략 시 현재 텍스트 색 따라감(foreground).
14
+ final Color? color;
15
+
16
+ final String? semanticLabel;
17
+
18
+ const ShUiSpinner({
19
+ super.key,
20
+ this.size = ShUiSpinnerSize.md,
21
+ this.color,
22
+ this.semanticLabel,
23
+ });
24
+
25
+ double _diameter() => switch (size) {
26
+ ShUiSpinnerSize.sm => 14.0,
27
+ ShUiSpinnerSize.md => 18.0,
28
+ ShUiSpinnerSize.lg => 24.0,
29
+ };
30
+
31
+ double _strokeWidth() => switch (size) {
32
+ ShUiSpinnerSize.sm => 1.5,
33
+ ShUiSpinnerSize.md => 2.0,
34
+ ShUiSpinnerSize.lg => 2.5,
35
+ };
36
+
37
+ @override
38
+ Widget build(BuildContext context) {
39
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
40
+ final effective = color ?? DefaultTextStyle.of(context).style.color ?? shUi.colors.foreground;
41
+ final diameter = _diameter();
42
+
43
+ return Semantics(
44
+ label: semanticLabel ?? '로딩 중',
45
+ liveRegion: true,
46
+ child: SizedBox(
47
+ width: diameter,
48
+ height: diameter,
49
+ child: CircularProgressIndicator(
50
+ strokeWidth: _strokeWidth(),
51
+ valueColor: AlwaysStoppedAnimation<Color>(effective),
52
+ ),
53
+ ),
54
+ );
55
+ }
56
+ }
@@ -0,0 +1,109 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../foundation/sh_ui_tokens.dart';
3
+
4
+ enum ShUiSwitchSize { sm, md }
5
+
6
+ /// sh-ui Switch — 토글 스위치. sm/md 사이즈.
7
+ class ShUiSwitch extends StatefulWidget {
8
+ final bool checked;
9
+ final ValueChanged<bool>? onChanged;
10
+ final ShUiSwitchSize size;
11
+ final bool enabled;
12
+
13
+ const ShUiSwitch({
14
+ super.key,
15
+ this.checked = false,
16
+ this.onChanged,
17
+ this.size = ShUiSwitchSize.md,
18
+ this.enabled = true,
19
+ });
20
+
21
+ @override
22
+ State<ShUiSwitch> createState() => _ShUiSwitchState();
23
+ }
24
+
25
+ class _ShUiSwitchState extends State<ShUiSwitch> {
26
+ bool _hover = false;
27
+
28
+ double get _trackWidth => switch (widget.size) {
29
+ ShUiSwitchSize.sm => 36,
30
+ ShUiSwitchSize.md => 44,
31
+ };
32
+
33
+ double get _trackHeight => switch (widget.size) {
34
+ ShUiSwitchSize.sm => 20,
35
+ ShUiSwitchSize.md => 24,
36
+ };
37
+
38
+ double get _thumbSize => switch (widget.size) {
39
+ ShUiSwitchSize.sm => 14,
40
+ ShUiSwitchSize.md => 18,
41
+ };
42
+
43
+ double get _thumbPadding => 3;
44
+
45
+ @override
46
+ Widget build(BuildContext context) {
47
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
48
+ final colors = shUi.colors;
49
+ final disabled = !widget.enabled || widget.onChanged == null;
50
+
51
+ final trackColor = widget.checked ? colors.primary : colors.backgroundMuted;
52
+ final thumbColor = widget.checked ? colors.primaryForeground : colors.foregroundMuted;
53
+ final thumbOffset = widget.checked
54
+ ? _trackWidth - _thumbSize - _thumbPadding * 2
55
+ : 0.0;
56
+
57
+ return Opacity(
58
+ opacity: disabled ? shUi.opacity.disabled : 1,
59
+ child: MouseRegion(
60
+ cursor: disabled ? SystemMouseCursors.basic : SystemMouseCursors.click,
61
+ onEnter: (_) => setState(() => _hover = true),
62
+ onExit: (_) => setState(() => _hover = false),
63
+ child: GestureDetector(
64
+ onTap: disabled ? null : () => widget.onChanged!(!widget.checked),
65
+ child: AnimatedContainer(
66
+ duration: const Duration(milliseconds: 150),
67
+ curve: Curves.easeOut,
68
+ width: _trackWidth,
69
+ height: _trackHeight,
70
+ decoration: BoxDecoration(
71
+ color: _hover && !disabled
72
+ ? Color.lerp(trackColor, colors.foreground, 0.08)!
73
+ : trackColor,
74
+ borderRadius: BorderRadius.circular(_trackHeight / 2),
75
+ border: Border.all(
76
+ color: widget.checked ? Colors.transparent : colors.border,
77
+ ),
78
+ ),
79
+ child: Stack(
80
+ children: [
81
+ AnimatedPositioned(
82
+ duration: const Duration(milliseconds: 150),
83
+ curve: Curves.easeOut,
84
+ left: _thumbPadding + thumbOffset,
85
+ top: _thumbPadding,
86
+ child: Container(
87
+ width: _thumbSize,
88
+ height: _thumbSize,
89
+ decoration: BoxDecoration(
90
+ color: widget.checked ? thumbColor : colors.background,
91
+ shape: BoxShape.circle,
92
+ boxShadow: [
93
+ BoxShadow(
94
+ color: Colors.black.withValues(alpha: 0.1),
95
+ blurRadius: 2,
96
+ offset: const Offset(0, 1),
97
+ ),
98
+ ],
99
+ ),
100
+ ),
101
+ ),
102
+ ],
103
+ ),
104
+ ),
105
+ ),
106
+ ),
107
+ );
108
+ }
109
+ }