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,145 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../foundation/sh_ui_tokens.dart';
3
+
4
+ /// sh-ui Label — 폼 필드 레이블. title, subtitle, description, caption 하위 구성.
5
+ ///
6
+ /// ShUiLabel(
7
+ /// isRequired: true,
8
+ /// child: Column(children: [
9
+ /// ShUiLabelTitle('이름'),
10
+ /// ShUiLabelDescription('성과 이름을 입력해주세요'),
11
+ /// ]),
12
+ /// )
13
+ class ShUiLabel extends StatelessWidget {
14
+ final Widget child;
15
+ final bool isRequired;
16
+
17
+ const ShUiLabel({
18
+ super.key,
19
+ required this.child,
20
+ this.isRequired = false,
21
+ });
22
+
23
+ @override
24
+ Widget build(BuildContext context) {
25
+ return _ShUiLabelScope(
26
+ isRequired: isRequired,
27
+ child: child,
28
+ );
29
+ }
30
+ }
31
+
32
+ class ShUiLabelTitle extends StatelessWidget {
33
+ final String text;
34
+
35
+ const ShUiLabelTitle(this.text, {super.key});
36
+
37
+ @override
38
+ Widget build(BuildContext context) {
39
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
40
+ final scope = _ShUiLabelScope.of(context);
41
+ final isRequired = scope?.isRequired ?? false;
42
+
43
+ return Row(
44
+ mainAxisSize: MainAxisSize.min,
45
+ children: [
46
+ Text(
47
+ text,
48
+ style: TextStyle(
49
+ color: shUi.colors.foreground,
50
+ fontSize: shUi.text.sm,
51
+ fontWeight: shUi.weight.medium,
52
+ height: 1.4,
53
+ ),
54
+ ),
55
+ if (isRequired) ...[
56
+ const SizedBox(width: 2),
57
+ Text(
58
+ '*',
59
+ style: TextStyle(
60
+ color: shUi.colors.danger,
61
+ fontSize: shUi.text.sm,
62
+ fontWeight: shUi.weight.medium,
63
+ ),
64
+ ),
65
+ ],
66
+ ],
67
+ );
68
+ }
69
+ }
70
+
71
+ class ShUiLabelSubtitle extends StatelessWidget {
72
+ final String text;
73
+
74
+ const ShUiLabelSubtitle(this.text, {super.key});
75
+
76
+ @override
77
+ Widget build(BuildContext context) {
78
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
79
+ return Text(
80
+ text,
81
+ style: TextStyle(
82
+ color: shUi.colors.foregroundMuted,
83
+ fontSize: 13,
84
+ fontWeight: shUi.weight.regular,
85
+ height: 1.4,
86
+ ),
87
+ );
88
+ }
89
+ }
90
+
91
+ class ShUiLabelDescription extends StatelessWidget {
92
+ final String text;
93
+
94
+ const ShUiLabelDescription(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.foregroundMuted,
103
+ fontSize: 13,
104
+ height: 1.5,
105
+ ),
106
+ );
107
+ }
108
+ }
109
+
110
+ class ShUiLabelCaption extends StatelessWidget {
111
+ final String text;
112
+ final bool isError;
113
+
114
+ const ShUiLabelCaption(this.text, {super.key, this.isError = false});
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: isError ? shUi.colors.danger : shUi.colors.foregroundMuted,
123
+ fontSize: shUi.text.xs,
124
+ height: 1.5,
125
+ ),
126
+ );
127
+ }
128
+ }
129
+
130
+ class _ShUiLabelScope extends InheritedWidget {
131
+ final bool isRequired;
132
+
133
+ const _ShUiLabelScope({
134
+ required this.isRequired,
135
+ required super.child,
136
+ });
137
+
138
+ static _ShUiLabelScope? of(BuildContext context) {
139
+ return context.dependOnInheritedWidgetOfExactType<_ShUiLabelScope>();
140
+ }
141
+
142
+ @override
143
+ bool updateShouldNotify(covariant _ShUiLabelScope old) =>
144
+ isRequired != old.isRequired;
145
+ }
@@ -0,0 +1,98 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../foundation/sh_ui_tokens.dart';
3
+ import 'sh_ui_dropdown_menu.dart';
4
+
5
+ /// sh-ui Menubar — 상단 앱 메뉴바(파일/편집/보기 등).
6
+ ///
7
+ /// 여러 개의 "메뉴"를 가로로 나열한다. 각 메뉴는 trigger 라벨 + 항목 리스트.
8
+ ///
9
+ /// ShUiMenubar(
10
+ /// menus: [
11
+ /// ShUiMenubarMenu(
12
+ /// label: '파일',
13
+ /// items: const [
14
+ /// ShUiDropdownMenuItem(value: 'new', label: '새로 만들기'),
15
+ /// ShUiDropdownMenuItem(value: 'open', label: '열기…'),
16
+ /// ],
17
+ /// onSelected: (v) => ...,
18
+ /// ),
19
+ /// ShUiMenubarMenu(label: '편집', items: [...], onSelected: ...),
20
+ /// ],
21
+ /// )
22
+ class ShUiMenubar extends StatelessWidget {
23
+ final List<ShUiMenubarMenu> menus;
24
+
25
+ const ShUiMenubar({
26
+ super.key,
27
+ required this.menus,
28
+ });
29
+
30
+ @override
31
+ Widget build(BuildContext context) {
32
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
33
+ final colors = shUi.colors;
34
+
35
+ return Container(
36
+ padding: const EdgeInsets.all(4),
37
+ decoration: BoxDecoration(
38
+ color: colors.background,
39
+ border: Border.all(color: colors.border),
40
+ borderRadius: BorderRadius.circular(shUi.radius.defaultRadius),
41
+ boxShadow: [
42
+ BoxShadow(
43
+ color: Colors.black.withValues(alpha: 0.04),
44
+ blurRadius: 2,
45
+ offset: const Offset(0, 1),
46
+ ),
47
+ ],
48
+ ),
49
+ child: Row(
50
+ mainAxisSize: MainAxisSize.min,
51
+ children: menus
52
+ .map((m) => _MenubarItem(menu: m))
53
+ .toList(),
54
+ ),
55
+ );
56
+ }
57
+ }
58
+
59
+ /// Menubar 한 항목 — 라벨 + 드롭다운 항목 리스트.
60
+ class ShUiMenubarMenu {
61
+ final String label;
62
+ final List<ShUiDropdownMenuEntry<dynamic>> items;
63
+ final ValueChanged<dynamic>? onSelected;
64
+
65
+ const ShUiMenubarMenu({
66
+ required this.label,
67
+ required this.items,
68
+ this.onSelected,
69
+ });
70
+ }
71
+
72
+ class _MenubarItem extends StatelessWidget {
73
+ final ShUiMenubarMenu menu;
74
+
75
+ const _MenubarItem({required this.menu});
76
+
77
+ @override
78
+ Widget build(BuildContext context) {
79
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
80
+ final colors = shUi.colors;
81
+
82
+ return ShUiDropdownMenu<dynamic>(
83
+ items: menu.items,
84
+ onSelected: menu.onSelected,
85
+ child: Padding(
86
+ padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
87
+ child: Text(
88
+ menu.label,
89
+ style: TextStyle(
90
+ color: colors.foreground,
91
+ fontSize: shUi.text.sm,
92
+ height: 1,
93
+ ),
94
+ ),
95
+ ),
96
+ );
97
+ }
98
+ }
@@ -0,0 +1,276 @@
1
+ import 'package:flutter/material.dart';
2
+ import '../foundation/sh_ui_tokens.dart';
3
+
4
+ /// sh-ui Pagination — 페이지 단위 내비게이션.
5
+ ///
6
+ /// ShUiPagination(
7
+ /// page: 3,
8
+ /// pageCount: 10,
9
+ /// siblingCount: 1,
10
+ /// onPageChanged: (p) => ...,
11
+ /// )
12
+ ///
13
+ /// - [page] / [pageCount]는 1-based.
14
+ /// - [siblingCount] 만큼 현재 페이지 양옆을 펼치고, 그 바깥은 "..."로 접는다.
15
+ /// - 첫/마지막 페이지 번호는 항상 보인다.
16
+ class ShUiPagination extends StatelessWidget {
17
+ final int page;
18
+ final int pageCount;
19
+ final int siblingCount;
20
+ final ValueChanged<int>? onPageChanged;
21
+ final bool showPrevNext;
22
+
23
+ const ShUiPagination({
24
+ super.key,
25
+ required this.page,
26
+ required this.pageCount,
27
+ this.siblingCount = 1,
28
+ this.onPageChanged,
29
+ this.showPrevNext = true,
30
+ });
31
+
32
+ @override
33
+ Widget build(BuildContext context) {
34
+ final shUi = Theme.of(context).extension<ShUiTheme>() ?? ShUiTheme.light;
35
+ final colors = shUi.colors;
36
+
37
+ if (pageCount <= 0) return const SizedBox.shrink();
38
+
39
+ final tokens = _buildRange();
40
+
41
+ final children = <Widget>[];
42
+
43
+ if (showPrevNext) {
44
+ children.add(
45
+ _NavButton(
46
+ label: '이전',
47
+ icon: Icons.chevron_left,
48
+ iconLeading: true,
49
+ enabled: page > 1 && onPageChanged != null,
50
+ onTap: () => onPageChanged?.call(page - 1),
51
+ shUi: shUi,
52
+ colors: colors,
53
+ ),
54
+ );
55
+ }
56
+
57
+ for (final token in tokens) {
58
+ if (token is int) {
59
+ final isActive = token == page;
60
+ children.add(
61
+ _PageButton(
62
+ label: '$token',
63
+ isActive: isActive,
64
+ enabled: !isActive && onPageChanged != null,
65
+ onTap: () => onPageChanged?.call(token),
66
+ shUi: shUi,
67
+ colors: colors,
68
+ ),
69
+ );
70
+ } else {
71
+ children.add(_Ellipsis(colors: colors, shUi: shUi));
72
+ }
73
+ }
74
+
75
+ if (showPrevNext) {
76
+ children.add(
77
+ _NavButton(
78
+ label: '다음',
79
+ icon: Icons.chevron_right,
80
+ iconLeading: false,
81
+ enabled: page < pageCount && onPageChanged != null,
82
+ onTap: () => onPageChanged?.call(page + 1),
83
+ shUi: shUi,
84
+ colors: colors,
85
+ ),
86
+ );
87
+ }
88
+
89
+ return Semantics(
90
+ label: 'Pagination',
91
+ container: true,
92
+ child: Wrap(
93
+ alignment: WrapAlignment.center,
94
+ crossAxisAlignment: WrapCrossAlignment.center,
95
+ spacing: 4,
96
+ runSpacing: 4,
97
+ children: children,
98
+ ),
99
+ );
100
+ }
101
+
102
+ /// 현재 페이지 주변 ±siblingCount + 양 끝 + dots 토큰.
103
+ List<Object> _buildRange() {
104
+ final totalSlots = siblingCount * 2 + 5;
105
+ if (pageCount <= totalSlots) {
106
+ return [for (var i = 1; i <= pageCount; i++) i];
107
+ }
108
+
109
+ final leftSibling = (page - siblingCount).clamp(1, pageCount);
110
+ final rightSibling = (page + siblingCount).clamp(1, pageCount);
111
+
112
+ final showLeftDots = leftSibling > 2;
113
+ final showRightDots = rightSibling < pageCount - 1;
114
+
115
+ if (!showLeftDots && showRightDots) {
116
+ final leftCount = 3 + 2 * siblingCount;
117
+ return [
118
+ for (var i = 1; i <= leftCount; i++) i,
119
+ _Dots.instance,
120
+ pageCount,
121
+ ];
122
+ }
123
+
124
+ if (showLeftDots && !showRightDots) {
125
+ final rightCount = 3 + 2 * siblingCount;
126
+ return [
127
+ 1,
128
+ _Dots.instance,
129
+ for (var i = pageCount - rightCount + 1; i <= pageCount; i++) i,
130
+ ];
131
+ }
132
+
133
+ return [
134
+ 1,
135
+ _Dots.instance,
136
+ for (var i = leftSibling; i <= rightSibling; i++) i,
137
+ _Dots.instance,
138
+ pageCount,
139
+ ];
140
+ }
141
+ }
142
+
143
+ class _Dots {
144
+ static const _Dots instance = _Dots._();
145
+ const _Dots._();
146
+ }
147
+
148
+ class _PageButton extends StatelessWidget {
149
+ final String label;
150
+ final bool isActive;
151
+ final bool enabled;
152
+ final VoidCallback onTap;
153
+ final ShUiTheme shUi;
154
+ final ShUiColorTokens colors;
155
+
156
+ const _PageButton({
157
+ required this.label,
158
+ required this.isActive,
159
+ required this.enabled,
160
+ required this.onTap,
161
+ required this.shUi,
162
+ required this.colors,
163
+ });
164
+
165
+ @override
166
+ Widget build(BuildContext context) {
167
+ final bg = isActive ? colors.foreground : Colors.transparent;
168
+ final fg = isActive ? colors.background : colors.foreground;
169
+ final radius = BorderRadius.circular(shUi.radius.defaultRadius);
170
+
171
+ return Semantics(
172
+ selected: isActive,
173
+ button: !isActive,
174
+ label: isActive ? '현재 페이지 $label' : '페이지 $label',
175
+ child: Material(
176
+ color: bg,
177
+ borderRadius: radius,
178
+ child: InkWell(
179
+ onTap: enabled ? onTap : null,
180
+ borderRadius: radius,
181
+ child: Container(
182
+ constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
183
+ padding: const EdgeInsets.symmetric(horizontal: 10),
184
+ alignment: Alignment.center,
185
+ child: Text(
186
+ label,
187
+ style: TextStyle(
188
+ color: fg,
189
+ fontSize: shUi.text.sm,
190
+ fontWeight:
191
+ isActive ? shUi.weight.medium : FontWeight.normal,
192
+ ),
193
+ ),
194
+ ),
195
+ ),
196
+ ),
197
+ );
198
+ }
199
+ }
200
+
201
+ class _NavButton extends StatelessWidget {
202
+ final String label;
203
+ final IconData icon;
204
+ final bool iconLeading;
205
+ final bool enabled;
206
+ final VoidCallback onTap;
207
+ final ShUiTheme shUi;
208
+ final ShUiColorTokens colors;
209
+
210
+ const _NavButton({
211
+ required this.label,
212
+ required this.icon,
213
+ required this.iconLeading,
214
+ required this.enabled,
215
+ required this.onTap,
216
+ required this.shUi,
217
+ required this.colors,
218
+ });
219
+
220
+ @override
221
+ Widget build(BuildContext context) {
222
+ final radius = BorderRadius.circular(shUi.radius.defaultRadius);
223
+ final iconWidget = Icon(icon, size: 16, color: colors.foreground);
224
+ final textWidget = Text(
225
+ label,
226
+ style: TextStyle(color: colors.foreground, fontSize: shUi.text.sm),
227
+ );
228
+
229
+ return Opacity(
230
+ opacity: enabled ? 1 : shUi.opacity.disabled,
231
+ child: Material(
232
+ color: Colors.transparent,
233
+ borderRadius: radius,
234
+ child: InkWell(
235
+ onTap: enabled ? onTap : null,
236
+ borderRadius: radius,
237
+ child: Container(
238
+ constraints: const BoxConstraints(minHeight: 36),
239
+ padding: const EdgeInsets.symmetric(horizontal: 10),
240
+ alignment: Alignment.center,
241
+ child: Row(
242
+ mainAxisSize: MainAxisSize.min,
243
+ children: iconLeading
244
+ ? [iconWidget, const SizedBox(width: 6), textWidget]
245
+ : [textWidget, const SizedBox(width: 6), iconWidget],
246
+ ),
247
+ ),
248
+ ),
249
+ ),
250
+ );
251
+ }
252
+ }
253
+
254
+ class _Ellipsis extends StatelessWidget {
255
+ final ShUiColorTokens colors;
256
+ final ShUiTheme shUi;
257
+
258
+ const _Ellipsis({required this.colors, required this.shUi});
259
+
260
+ @override
261
+ Widget build(BuildContext context) {
262
+ return SizedBox(
263
+ width: 36,
264
+ height: 36,
265
+ child: Center(
266
+ child: Text(
267
+ '…',
268
+ style: TextStyle(
269
+ color: colors.foregroundMuted,
270
+ fontSize: shUi.text.base,
271
+ ),
272
+ ),
273
+ ),
274
+ );
275
+ }
276
+ }