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,154 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../foundation/sh_ui_tokens.dart';
3
+
4
+ /// sh-ui Checkbox — 토큰 기반 커스텀 체크박스.
5
+ /// indeterminate(중간) 상태도 지원한다.
6
+ class ShUiCheckbox extends StatefulWidget {
7
+ final bool checked;
8
+ final bool indeterminate;
9
+ final ValueChanged<bool>? onChanged;
10
+ final bool enabled;
11
+
12
+ const ShUiCheckbox({
13
+ super.key,
14
+ this.checked = false,
15
+ this.indeterminate = false,
16
+ this.onChanged,
17
+ this.enabled = true,
18
+ });
19
+
20
+ @override
21
+ State<ShUiCheckbox> createState() => _ShUiCheckboxState();
22
+ }
23
+
24
+ class _ShUiCheckboxState extends State<ShUiCheckbox> {
25
+ bool _hover = false;
26
+
27
+ @override
28
+ Widget build(BuildContext context) {
29
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
30
+ final colors = shUi.colors;
31
+ final active = widget.checked || widget.indeterminate;
32
+ final disabled = !widget.enabled || widget.onChanged == null;
33
+
34
+ final bg = active ? colors.primary : colors.background;
35
+ final borderColor = active ? colors.primary : (_hover ? colors.foregroundMuted : colors.border);
36
+ final iconColor = active ? colors.primaryForeground : Colors.transparent;
37
+
38
+ return Opacity(
39
+ opacity: disabled ? shUi.opacity.disabled : 1,
40
+ child: MouseRegion(
41
+ cursor: disabled ? SystemMouseCursors.basic : SystemMouseCursors.click,
42
+ onEnter: (_) => setState(() => _hover = true),
43
+ onExit: (_) => setState(() => _hover = false),
44
+ child: GestureDetector(
45
+ onTap: disabled ? null : () => widget.onChanged!(!widget.checked),
46
+ child: AnimatedContainer(
47
+ duration: shUi.duration.fast,
48
+ width: 18,
49
+ height: 18,
50
+ decoration: BoxDecoration(
51
+ color: bg,
52
+ border: Border.all(color: borderColor, width: 1.5),
53
+ borderRadius: BorderRadius.circular(4),
54
+ ),
55
+ child: Center(
56
+ child: CustomPaint(
57
+ size: const Size(12, 12),
58
+ painter: widget.indeterminate
59
+ ? _MinusIconPainter(color: iconColor)
60
+ : _CheckIconPainter(color: iconColor),
61
+ ),
62
+ ),
63
+ ),
64
+ ),
65
+ ),
66
+ );
67
+ }
68
+ }
69
+
70
+ class _CheckIconPainter extends CustomPainter {
71
+ final Color color;
72
+ _CheckIconPainter({required this.color});
73
+
74
+ @override
75
+ void paint(Canvas canvas, Size size) {
76
+ final paint = Paint()
77
+ ..color = color
78
+ ..strokeWidth = 2
79
+ ..style = PaintingStyle.stroke
80
+ ..strokeCap = StrokeCap.round
81
+ ..strokeJoin = StrokeJoin.round;
82
+
83
+ final path = Path()
84
+ ..moveTo(size.width * 0.15, size.height * 0.5)
85
+ ..lineTo(size.width * 0.4, size.height * 0.75)
86
+ ..lineTo(size.width * 0.85, size.height * 0.2);
87
+
88
+ canvas.drawPath(path, paint);
89
+ }
90
+
91
+ @override
92
+ bool shouldRepaint(covariant _CheckIconPainter old) => old.color != color;
93
+ }
94
+
95
+ class _MinusIconPainter extends CustomPainter {
96
+ final Color color;
97
+ _MinusIconPainter({required this.color});
98
+
99
+ @override
100
+ void paint(Canvas canvas, Size size) {
101
+ final paint = Paint()
102
+ ..color = color
103
+ ..strokeWidth = 2
104
+ ..style = PaintingStyle.stroke
105
+ ..strokeCap = StrokeCap.round;
106
+
107
+ canvas.drawLine(
108
+ Offset(size.width * 0.2, size.height * 0.5),
109
+ Offset(size.width * 0.8, size.height * 0.5),
110
+ paint,
111
+ );
112
+ }
113
+
114
+ @override
115
+ bool shouldRepaint(covariant _MinusIconPainter old) => old.color != color;
116
+ }
117
+
118
+ /// 체크박스 그룹 — vertical 또는 horizontal 배치.
119
+ enum ShUiCheckboxGroupOrientation { vertical, horizontal }
120
+
121
+ class ShUiCheckboxGroup extends StatelessWidget {
122
+ final List<Widget> children;
123
+ final ShUiCheckboxGroupOrientation orientation;
124
+ final double gap;
125
+
126
+ const ShUiCheckboxGroup({
127
+ super.key,
128
+ required this.children,
129
+ this.orientation = ShUiCheckboxGroupOrientation.vertical,
130
+ this.gap = 8,
131
+ });
132
+
133
+ @override
134
+ Widget build(BuildContext context) {
135
+ if (orientation == ShUiCheckboxGroupOrientation.horizontal) {
136
+ return Wrap(spacing: gap, runSpacing: gap, children: children);
137
+ }
138
+ return Column(
139
+ crossAxisAlignment: CrossAxisAlignment.start,
140
+ mainAxisSize: MainAxisSize.min,
141
+ children: _withGaps(children, gap),
142
+ );
143
+ }
144
+ }
145
+
146
+ List<Widget> _withGaps(List<Widget> children, double gap) {
147
+ if (children.length <= 1) return children;
148
+ final out = <Widget>[];
149
+ for (var i = 0; i < children.length; i++) {
150
+ out.add(children[i]);
151
+ if (i != children.length - 1) out.add(SizedBox(height: gap));
152
+ }
153
+ return out;
154
+ }
@@ -0,0 +1,264 @@
1
+ import 'dart:math';
2
+ import 'package:flutter/material.dart';
3
+ import '../foundation/sh_ui_tokens.dart';
4
+
5
+ /// sh-ui ColorPicker — HSV 기반 색상 선택기.
6
+ ///
7
+ /// ShUiColorPicker(
8
+ /// value: Color(0xFF3B82F6),
9
+ /// onChanged: (color) => setState(() => selectedColor = color),
10
+ /// )
11
+ class ShUiColorPicker extends StatefulWidget {
12
+ final Color? value;
13
+ final ValueChanged<Color>? onChanged;
14
+ final Color defaultValue;
15
+
16
+ const ShUiColorPicker({
17
+ super.key,
18
+ this.value,
19
+ this.onChanged,
20
+ this.defaultValue = const Color(0xFF000000),
21
+ });
22
+
23
+ @override
24
+ State<ShUiColorPicker> createState() => _ShUiColorPickerState();
25
+ }
26
+
27
+ class _ShUiColorPickerState extends State<ShUiColorPicker> {
28
+ late HSVColor _hsv;
29
+ late TextEditingController _hexController;
30
+
31
+ Color get _currentColor => widget.value ?? widget.defaultValue;
32
+
33
+ @override
34
+ void initState() {
35
+ super.initState();
36
+ _hsv = HSVColor.fromColor(_currentColor);
37
+ _hexController = TextEditingController(text: _colorToHex(_currentColor));
38
+ }
39
+
40
+ @override
41
+ void didUpdateWidget(covariant ShUiColorPicker old) {
42
+ super.didUpdateWidget(old);
43
+ if (widget.value != old.value && widget.value != null) {
44
+ _hsv = HSVColor.fromColor(widget.value!);
45
+ _hexController.text = _colorToHex(widget.value!);
46
+ }
47
+ }
48
+
49
+ @override
50
+ void dispose() {
51
+ _hexController.dispose();
52
+ super.dispose();
53
+ }
54
+
55
+ String _colorToHex(Color c) {
56
+ return '#${c.red.toInt().toRadixString(16).padLeft(2, '0')}'
57
+ '${c.green.toInt().toRadixString(16).padLeft(2, '0')}'
58
+ '${c.blue.toInt().toRadixString(16).padLeft(2, '0')}'
59
+ .toUpperCase();
60
+ }
61
+
62
+ void _emit(HSVColor hsv) {
63
+ setState(() => _hsv = hsv);
64
+ final color = hsv.toColor();
65
+ _hexController.text = _colorToHex(color);
66
+ widget.onChanged?.call(color);
67
+ }
68
+
69
+ void _onHexSubmit() {
70
+ final text = _hexController.text.trim();
71
+ final hex = text.startsWith('#') ? text : '#$text';
72
+ if (RegExp(r'^#[0-9a-fA-F]{6}$').hasMatch(hex)) {
73
+ final value = int.parse(hex.substring(1), radix: 16);
74
+ final color = Color(0xFF000000 | value);
75
+ _emit(HSVColor.fromColor(color));
76
+ } else {
77
+ _hexController.text = _colorToHex(_hsv.toColor());
78
+ }
79
+ }
80
+
81
+ @override
82
+ Widget build(BuildContext context) {
83
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
84
+ final colors = shUi.colors;
85
+ final pureHue = HSVColor.fromAHSV(1, _hsv.hue, 1, 1).toColor();
86
+
87
+ return Column(
88
+ mainAxisSize: MainAxisSize.min,
89
+ crossAxisAlignment: CrossAxisAlignment.stretch,
90
+ children: [
91
+ // SV area
92
+ ClipRRect(
93
+ borderRadius: BorderRadius.circular(shUi.radius.defaultRadius),
94
+ child: SizedBox(
95
+ height: 160,
96
+ child: LayoutBuilder(
97
+ builder: (context, constraints) {
98
+ return GestureDetector(
99
+ onPanStart: (d) => _onSVDrag(d.localPosition, constraints),
100
+ onPanUpdate: (d) => _onSVDrag(d.localPosition, constraints),
101
+ onTapDown: (d) => _onSVDrag(d.localPosition, constraints),
102
+ child: Stack(
103
+ children: [
104
+ // Background hue
105
+ Container(color: pureHue),
106
+ // Saturation gradient (white → transparent)
107
+ Container(
108
+ decoration: const BoxDecoration(
109
+ gradient: LinearGradient(
110
+ colors: [Colors.white, Colors.transparent],
111
+ begin: Alignment.centerLeft,
112
+ end: Alignment.centerRight,
113
+ ),
114
+ ),
115
+ ),
116
+ // Value gradient (transparent → black)
117
+ Container(
118
+ decoration: const BoxDecoration(
119
+ gradient: LinearGradient(
120
+ colors: [Colors.transparent, Colors.black],
121
+ begin: Alignment.topCenter,
122
+ end: Alignment.bottomCenter,
123
+ ),
124
+ ),
125
+ ),
126
+ // Thumb
127
+ Positioned(
128
+ left: _hsv.saturation * constraints.maxWidth - 7,
129
+ top: (1 - _hsv.value) * constraints.maxHeight - 7,
130
+ child: Container(
131
+ width: 14,
132
+ height: 14,
133
+ decoration: BoxDecoration(
134
+ shape: BoxShape.circle,
135
+ color: _hsv.toColor(),
136
+ border: Border.all(color: Colors.white, width: 2),
137
+ boxShadow: [
138
+ BoxShadow(
139
+ color: Colors.black.withValues(alpha: 0.3),
140
+ blurRadius: 4,
141
+ ),
142
+ ],
143
+ ),
144
+ ),
145
+ ),
146
+ ],
147
+ ),
148
+ );
149
+ },
150
+ ),
151
+ ),
152
+ ),
153
+ SizedBox(height: shUi.spacing.s3),
154
+ // Hue slider
155
+ SizedBox(
156
+ height: 16,
157
+ child: LayoutBuilder(
158
+ builder: (context, constraints) {
159
+ return GestureDetector(
160
+ onPanStart: (d) => _onHueDrag(d.localPosition, constraints),
161
+ onPanUpdate: (d) => _onHueDrag(d.localPosition, constraints),
162
+ onTapDown: (d) => _onHueDrag(d.localPosition, constraints),
163
+ child: ClipRRect(
164
+ borderRadius: BorderRadius.circular(8),
165
+ child: Stack(
166
+ children: [
167
+ Container(
168
+ decoration: const BoxDecoration(
169
+ gradient: LinearGradient(
170
+ colors: [
171
+ Color(0xFFFF0000),
172
+ Color(0xFFFFFF00),
173
+ Color(0xFF00FF00),
174
+ Color(0xFF00FFFF),
175
+ Color(0xFF0000FF),
176
+ Color(0xFFFF00FF),
177
+ Color(0xFFFF0000),
178
+ ],
179
+ ),
180
+ ),
181
+ ),
182
+ Positioned(
183
+ left: (_hsv.hue / 360) * constraints.maxWidth - 7,
184
+ top: 1,
185
+ child: Container(
186
+ width: 14,
187
+ height: 14,
188
+ decoration: BoxDecoration(
189
+ shape: BoxShape.circle,
190
+ color: pureHue,
191
+ border: Border.all(color: Colors.white, width: 2),
192
+ boxShadow: [
193
+ BoxShadow(
194
+ color: Colors.black.withValues(alpha: 0.3),
195
+ blurRadius: 2,
196
+ ),
197
+ ],
198
+ ),
199
+ ),
200
+ ),
201
+ ],
202
+ ),
203
+ ),
204
+ );
205
+ },
206
+ ),
207
+ ),
208
+ SizedBox(height: shUi.spacing.s3),
209
+ // Hex input + swatch
210
+ Row(
211
+ children: [
212
+ Container(
213
+ width: 32,
214
+ height: 32,
215
+ decoration: BoxDecoration(
216
+ color: _hsv.toColor(),
217
+ borderRadius: BorderRadius.circular(shUi.radius.defaultRadius - 2),
218
+ border: Border.all(color: colors.border),
219
+ ),
220
+ ),
221
+ SizedBox(width: shUi.spacing.s2),
222
+ Expanded(
223
+ child: Container(
224
+ height: shUi.control.sm,
225
+ padding: EdgeInsets.symmetric(horizontal: shUi.spacing.s2),
226
+ decoration: BoxDecoration(
227
+ color: colors.background,
228
+ border: Border.all(color: colors.border),
229
+ borderRadius: BorderRadius.circular(shUi.radius.defaultRadius - 2),
230
+ ),
231
+ child: TextField(
232
+ controller: _hexController,
233
+ onSubmitted: (_) => _onHexSubmit(),
234
+ onEditingComplete: _onHexSubmit,
235
+ style: TextStyle(
236
+ color: colors.foreground,
237
+ fontSize: 13,
238
+ fontFamily: 'monospace',
239
+ ),
240
+ decoration: const InputDecoration(
241
+ isCollapsed: true,
242
+ contentPadding: EdgeInsets.symmetric(vertical: 8),
243
+ border: InputBorder.none,
244
+ ),
245
+ ),
246
+ ),
247
+ ),
248
+ ],
249
+ ),
250
+ ],
251
+ );
252
+ }
253
+
254
+ void _onSVDrag(Offset local, BoxConstraints constraints) {
255
+ final s = (local.dx / constraints.maxWidth).clamp(0.0, 1.0);
256
+ final v = 1 - (local.dy / constraints.maxHeight).clamp(0.0, 1.0);
257
+ _emit(HSVColor.fromAHSV(1, _hsv.hue, s, v));
258
+ }
259
+
260
+ void _onHueDrag(Offset local, BoxConstraints constraints) {
261
+ final h = (local.dx / constraints.maxWidth).clamp(0.0, 1.0) * 360;
262
+ _emit(HSVColor.fromAHSV(1, h, _hsv.saturation, _hsv.value));
263
+ }
264
+ }