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,290 @@
1
+ /* ── Viewport (고정 컨테이너) ── */
2
+
3
+ .sh-ui-toast-viewport {
4
+ position: fixed;
5
+ z-index: var(--z-toast);
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: var(--space-2);
9
+ max-width: 24rem;
10
+ width: 100%;
11
+ pointer-events: none;
12
+ }
13
+
14
+ /* ── Position variants ── */
15
+
16
+ .sh-ui-toast-viewport[data-position="bottom-right"] {
17
+ bottom: var(--space-4);
18
+ right: var(--space-4);
19
+ flex-direction: column-reverse;
20
+ }
21
+
22
+ .sh-ui-toast-viewport[data-position="bottom-left"] {
23
+ bottom: var(--space-4);
24
+ left: var(--space-4);
25
+ flex-direction: column-reverse;
26
+ }
27
+
28
+ .sh-ui-toast-viewport[data-position="bottom-center"] {
29
+ bottom: var(--space-4);
30
+ left: 50%;
31
+ transform: translateX(-50%);
32
+ flex-direction: column-reverse;
33
+ }
34
+
35
+ .sh-ui-toast-viewport[data-position="top-right"] {
36
+ top: var(--space-4);
37
+ right: var(--space-4);
38
+ }
39
+
40
+ .sh-ui-toast-viewport[data-position="top-left"] {
41
+ top: var(--space-4);
42
+ left: var(--space-4);
43
+ }
44
+
45
+ .sh-ui-toast-viewport[data-position="top-center"] {
46
+ top: var(--space-4);
47
+ left: 50%;
48
+ transform: translateX(-50%);
49
+ }
50
+
51
+ @media (max-width: 40rem) {
52
+ .sh-ui-toast-viewport {
53
+ max-width: 100%;
54
+ padding: var(--space-4);
55
+ }
56
+
57
+ .sh-ui-toast-viewport[data-position="bottom-right"],
58
+ .sh-ui-toast-viewport[data-position="bottom-left"],
59
+ .sh-ui-toast-viewport[data-position="bottom-center"] {
60
+ right: 0;
61
+ left: 0;
62
+ bottom: 0;
63
+ transform: none;
64
+ }
65
+
66
+ .sh-ui-toast-viewport[data-position="top-right"],
67
+ .sh-ui-toast-viewport[data-position="top-left"],
68
+ .sh-ui-toast-viewport[data-position="top-center"] {
69
+ right: 0;
70
+ left: 0;
71
+ top: 0;
72
+ transform: none;
73
+ }
74
+ }
75
+
76
+ /* ── Single Toast ── */
77
+
78
+ .sh-ui-toast {
79
+ position: relative;
80
+ display: flex;
81
+ align-items: flex-start;
82
+ gap: 0.625rem;
83
+ width: 100%;
84
+ padding: var(--space-3) 2.25rem var(--space-3) var(--space-3);
85
+ background: var(--background);
86
+ color: var(--foreground);
87
+ border: 1px solid var(--border);
88
+ border-radius: var(--radius);
89
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
90
+ pointer-events: auto;
91
+ }
92
+
93
+ /* ── Enter / Exit animations by position ── */
94
+
95
+ /* Right positions: slide from right */
96
+ [data-position="bottom-right"] .sh-ui-toast,
97
+ [data-position="top-right"] .sh-ui-toast {
98
+ animation: sh-ui-toast-enter-right var(--duration-slow) cubic-bezier(0.16, 1, 0.3, 1) forwards;
99
+ }
100
+
101
+ [data-position="bottom-right"] .sh-ui-toast[data-exiting],
102
+ [data-position="top-right"] .sh-ui-toast[data-exiting] {
103
+ animation: sh-ui-toast-exit-right 150ms ease-in forwards;
104
+ }
105
+
106
+ /* Left positions: slide from left */
107
+ [data-position="bottom-left"] .sh-ui-toast,
108
+ [data-position="top-left"] .sh-ui-toast {
109
+ animation: sh-ui-toast-enter-left var(--duration-slow) cubic-bezier(0.16, 1, 0.3, 1) forwards;
110
+ }
111
+
112
+ [data-position="bottom-left"] .sh-ui-toast[data-exiting],
113
+ [data-position="top-left"] .sh-ui-toast[data-exiting] {
114
+ animation: sh-ui-toast-exit-left 150ms ease-in forwards;
115
+ }
116
+
117
+ /* Bottom center: slide from bottom */
118
+ [data-position="bottom-center"] .sh-ui-toast {
119
+ animation: sh-ui-toast-enter-bottom var(--duration-slow) cubic-bezier(0.16, 1, 0.3, 1) forwards;
120
+ }
121
+
122
+ [data-position="bottom-center"] .sh-ui-toast[data-exiting] {
123
+ animation: sh-ui-toast-exit-bottom 150ms ease-in forwards;
124
+ }
125
+
126
+ /* Top center: slide from top */
127
+ [data-position="top-center"] .sh-ui-toast {
128
+ animation: sh-ui-toast-enter-top var(--duration-slow) cubic-bezier(0.16, 1, 0.3, 1) forwards;
129
+ }
130
+
131
+ [data-position="top-center"] .sh-ui-toast[data-exiting] {
132
+ animation: sh-ui-toast-exit-top 150ms ease-in forwards;
133
+ }
134
+
135
+ /* ── Keyframes ── */
136
+
137
+ @keyframes sh-ui-toast-enter-right {
138
+ from { opacity: 0; transform: translateX(100%); }
139
+ to { opacity: 1; transform: translateX(0); }
140
+ }
141
+
142
+ @keyframes sh-ui-toast-exit-right {
143
+ from { opacity: 1; transform: translateX(0); }
144
+ to { opacity: 0; transform: translateX(100%); }
145
+ }
146
+
147
+ @keyframes sh-ui-toast-enter-left {
148
+ from { opacity: 0; transform: translateX(-100%); }
149
+ to { opacity: 1; transform: translateX(0); }
150
+ }
151
+
152
+ @keyframes sh-ui-toast-exit-left {
153
+ from { opacity: 1; transform: translateX(0); }
154
+ to { opacity: 0; transform: translateX(-100%); }
155
+ }
156
+
157
+ @keyframes sh-ui-toast-enter-bottom {
158
+ from { opacity: 0; transform: translateY(100%); }
159
+ to { opacity: 1; transform: translateY(0); }
160
+ }
161
+
162
+ @keyframes sh-ui-toast-exit-bottom {
163
+ from { opacity: 1; transform: translateY(0); }
164
+ to { opacity: 0; transform: translateY(100%); }
165
+ }
166
+
167
+ @keyframes sh-ui-toast-enter-top {
168
+ from { opacity: 0; transform: translateY(-100%); }
169
+ to { opacity: 1; transform: translateY(0); }
170
+ }
171
+
172
+ @keyframes sh-ui-toast-exit-top {
173
+ from { opacity: 1; transform: translateY(0); }
174
+ to { opacity: 0; transform: translateY(-100%); }
175
+ }
176
+
177
+ /* Mobile: always slide vertically based on top/bottom */
178
+ @media (max-width: 40rem) {
179
+ [data-position="bottom-right"] .sh-ui-toast,
180
+ [data-position="bottom-left"] .sh-ui-toast,
181
+ [data-position="bottom-center"] .sh-ui-toast {
182
+ animation-name: sh-ui-toast-enter-bottom;
183
+ }
184
+
185
+ [data-position="bottom-right"] .sh-ui-toast[data-exiting],
186
+ [data-position="bottom-left"] .sh-ui-toast[data-exiting],
187
+ [data-position="bottom-center"] .sh-ui-toast[data-exiting] {
188
+ animation-name: sh-ui-toast-exit-bottom;
189
+ }
190
+
191
+ [data-position="top-right"] .sh-ui-toast,
192
+ [data-position="top-left"] .sh-ui-toast,
193
+ [data-position="top-center"] .sh-ui-toast {
194
+ animation-name: sh-ui-toast-enter-top;
195
+ }
196
+
197
+ [data-position="top-right"] .sh-ui-toast[data-exiting],
198
+ [data-position="top-left"] .sh-ui-toast[data-exiting],
199
+ [data-position="top-center"] .sh-ui-toast[data-exiting] {
200
+ animation-name: sh-ui-toast-exit-top;
201
+ }
202
+ }
203
+
204
+ /* ── Variant colors ── */
205
+
206
+ .sh-ui-toast__icon {
207
+ flex-shrink: 0;
208
+ display: inline-flex;
209
+ align-items: center;
210
+ margin-top: 0.125rem;
211
+ }
212
+
213
+ .sh-ui-toast--success .sh-ui-toast__icon { color: var(--success, #16a34a); }
214
+ .sh-ui-toast--danger .sh-ui-toast__icon { color: var(--danger); }
215
+ .sh-ui-toast--warning .sh-ui-toast__icon { color: var(--warning, #d97706); }
216
+
217
+ /* ── Body ── */
218
+
219
+ .sh-ui-toast__body {
220
+ flex: 1;
221
+ min-width: 0;
222
+ }
223
+
224
+ .sh-ui-toast__title {
225
+ margin: 0;
226
+ font-size: var(--text-sm);
227
+ font-weight: var(--weight-semibold);
228
+ line-height: 1.4;
229
+ }
230
+
231
+ .sh-ui-toast__description {
232
+ margin: 0;
233
+ font-size: 0.8125rem;
234
+ line-height: 1.4;
235
+ color: var(--foreground-muted);
236
+ }
237
+
238
+ .sh-ui-toast__title + .sh-ui-toast__description {
239
+ margin-top: 0.125rem;
240
+ }
241
+
242
+ /* ── Action slot ── */
243
+
244
+ .sh-ui-toast__action {
245
+ flex-shrink: 0;
246
+ display: inline-flex;
247
+ align-items: center;
248
+ margin-left: auto;
249
+ }
250
+
251
+ /* ── Close ── */
252
+
253
+ .sh-ui-toast__close {
254
+ position: absolute;
255
+ top: 0.375rem;
256
+ right: 0.375rem;
257
+ display: inline-flex;
258
+ align-items: center;
259
+ justify-content: center;
260
+ width: 1.5rem;
261
+ height: 1.5rem;
262
+ padding: 0;
263
+ border: none;
264
+ border-radius: calc(var(--radius) - 2px);
265
+ background: transparent;
266
+ color: var(--foreground-muted);
267
+ font-size: var(--text-sm);
268
+ line-height: 1;
269
+ cursor: pointer;
270
+ transition: background-color var(--duration-fast), color var(--duration-fast);
271
+ }
272
+
273
+ .sh-ui-toast__close:hover {
274
+ background: var(--background-muted);
275
+ color: var(--foreground);
276
+ }
277
+
278
+ .sh-ui-toast__close:focus-visible {
279
+ outline: var(--border-width-strong) solid var(--foreground);
280
+ outline-offset: 2px;
281
+ }
282
+
283
+ @media (prefers-reduced-motion: reduce) {
284
+ .sh-ui-toast {
285
+ animation: none !important;
286
+ }
287
+ .sh-ui-toast__close {
288
+ transition: none;
289
+ }
290
+ }
@@ -0,0 +1,133 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Toggle as BaseToggle } from "@base-ui-components/react/toggle";
5
+ import { ToggleGroup as BaseToggleGroup } from "@base-ui-components/react/toggle-group";
6
+ import "./styles.css";
7
+
8
+ function cx(...args: (string | undefined | false)[]) {
9
+ return args.filter(Boolean).join(" ");
10
+ }
11
+
12
+ /* ───────────── Toggle ───────────── */
13
+
14
+ export type ToggleVariant = "outline" | "ghost";
15
+ export type ToggleSize = "sm" | "md" | "lg";
16
+
17
+ export type ToggleProps = Omit<
18
+ React.ComponentPropsWithoutRef<typeof BaseToggle>,
19
+ "className"
20
+ > & {
21
+ className?: string;
22
+ /**
23
+ * 외형 변형.
24
+ * - `ghost` — 배경 없음, 눌림 시만 강조 (기본)
25
+ * - `outline` — 항상 border 표시
26
+ *
27
+ * @default "ghost"
28
+ */
29
+ variant?: ToggleVariant;
30
+ /**
31
+ * 크기. `sm` / `md` / `lg`.
32
+ *
33
+ * @default "md"
34
+ */
35
+ size?: ToggleSize;
36
+ };
37
+
38
+ /**
39
+ * 눌린 상태(pressed)를 가진 버튼. 툴바의 "굵게/기울임" 같은 즉시 토글 액션에 적합.
40
+ * 시각만으로 상태를 구분하지 말고 `aria-label`이나 아이콘 옆 텍스트로 의미를 명확히 할 것.
41
+ */
42
+ export const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
43
+ ({ className, variant = "ghost", size = "md", ...props }, ref) => (
44
+ <BaseToggle
45
+ ref={ref}
46
+ className={cx(
47
+ "sh-ui-toggle",
48
+ `sh-ui-toggle--${variant}`,
49
+ `sh-ui-toggle--${size}`,
50
+ className,
51
+ )}
52
+ {...props}
53
+ />
54
+ ),
55
+ );
56
+ Toggle.displayName = "Toggle";
57
+
58
+ /* ───────────── ToggleGroup ───────────── */
59
+
60
+ export type ToggleGroupProps = Omit<
61
+ React.ComponentPropsWithoutRef<typeof BaseToggleGroup>,
62
+ "className"
63
+ > & {
64
+ className?: string;
65
+ /**
66
+ * 그룹 내 모든 항목에 적용될 외형. 자식 ToggleGroupItem이 자동 상속한다.
67
+ * @default "ghost"
68
+ */
69
+ variant?: ToggleVariant;
70
+ /**
71
+ * 그룹 내 모든 항목에 적용될 크기. 자식 ToggleGroupItem이 자동 상속한다.
72
+ * @default "md"
73
+ */
74
+ size?: ToggleSize;
75
+ };
76
+
77
+ interface ToggleGroupContextValue {
78
+ variant: ToggleVariant;
79
+ size: ToggleSize;
80
+ }
81
+
82
+ const ToggleGroupContext = React.createContext<ToggleGroupContextValue>({
83
+ variant: "ghost",
84
+ size: "md",
85
+ });
86
+
87
+ export const useToggleGroupStyle = () => React.useContext(ToggleGroupContext);
88
+
89
+ /**
90
+ * 여러 ToggleGroupItem을 묶는 컨테이너. `toggleMultiple` 옵션으로 단일/다중 선택을
91
+ * 결정하고, 그룹 단위로 variant·size를 적용한다. 항목들은 반드시 `ToggleGroupItem`을 사용할 것.
92
+ */
93
+ export const ToggleGroup = React.forwardRef<HTMLDivElement, ToggleGroupProps>(
94
+ ({ className, variant = "ghost", size = "md", ...props }, ref) => (
95
+ <ToggleGroupContext.Provider value={{ variant, size }}>
96
+ <BaseToggleGroup
97
+ ref={ref}
98
+ className={cx("sh-ui-toggle-group", className)}
99
+ {...props}
100
+ />
101
+ </ToggleGroupContext.Provider>
102
+ ),
103
+ );
104
+ ToggleGroup.displayName = "ToggleGroup";
105
+
106
+ /* ───────────── ToggleGroupItem ───────────── */
107
+
108
+ export type ToggleGroupItemProps = Omit<
109
+ React.ComponentPropsWithoutRef<typeof BaseToggle>,
110
+ "className"
111
+ > & {
112
+ className?: string;
113
+ };
114
+
115
+ /** ToggleGroup의 자식 항목. 부모 그룹의 variant·size를 자동으로 상속한다. */
116
+ export const ToggleGroupItem = React.forwardRef<HTMLButtonElement, ToggleGroupItemProps>(
117
+ ({ className, ...props }, ref) => {
118
+ const { variant, size } = useToggleGroupStyle();
119
+ return (
120
+ <BaseToggle
121
+ ref={ref}
122
+ className={cx(
123
+ "sh-ui-toggle",
124
+ `sh-ui-toggle--${variant}`,
125
+ `sh-ui-toggle--${size}`,
126
+ className,
127
+ )}
128
+ {...props}
129
+ />
130
+ );
131
+ },
132
+ );
133
+ ToggleGroupItem.displayName = "ToggleGroupItem";
@@ -0,0 +1,85 @@
1
+ /* ───────────── Toggle ───────────── */
2
+
3
+ .sh-ui-toggle {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+ gap: 0.375rem;
8
+ border: 1px solid transparent;
9
+ border-radius: var(--radius);
10
+ font-weight: var(--weight-medium);
11
+ line-height: 1;
12
+ cursor: pointer;
13
+ color: var(--foreground-muted);
14
+ background: transparent;
15
+ transition: background-color var(--duration-fast), color var(--duration-fast), border-color var(--duration-fast);
16
+ user-select: none;
17
+ -webkit-tap-highlight-color: transparent;
18
+ }
19
+
20
+ /* sizes */
21
+ .sh-ui-toggle--sm { height: var(--control-sm); padding: 0 0.625rem; font-size: var(--text-sm); }
22
+ .sh-ui-toggle--md { height: var(--control-md); padding: 0 var(--space-3); font-size: var(--text-sm); }
23
+ .sh-ui-toggle--lg { height: var(--control-lg); padding: 0 var(--space-4); font-size: var(--text-base); }
24
+
25
+ /* 모바일/터치 */
26
+ @media (hover: none) and (pointer: coarse) {
27
+ .sh-ui-toggle--sm { height: 2.25rem; }
28
+ .sh-ui-toggle--md { height: 2.75rem; }
29
+ }
30
+
31
+ /* variant: outline */
32
+ .sh-ui-toggle--outline {
33
+ border-color: var(--border);
34
+ }
35
+
36
+ .sh-ui-toggle--outline:hover:not(:disabled):not([data-pressed]) {
37
+ background: var(--background-muted);
38
+ color: var(--foreground);
39
+ }
40
+
41
+ /* variant: ghost */
42
+ .sh-ui-toggle--ghost:hover:not(:disabled):not([data-pressed]) {
43
+ background: var(--background-muted);
44
+ color: var(--foreground);
45
+ }
46
+
47
+ /* pressed */
48
+ .sh-ui-toggle[data-pressed] {
49
+ background: var(--background-muted);
50
+ color: var(--foreground);
51
+ }
52
+
53
+ .sh-ui-toggle--outline[data-pressed] {
54
+ border-color: var(--border-strong);
55
+ }
56
+
57
+ /* focus */
58
+ .sh-ui-toggle:focus-visible {
59
+ outline: var(--border-width-strong) solid var(--foreground);
60
+ outline-offset: 2px;
61
+ }
62
+
63
+ /* disabled */
64
+ .sh-ui-toggle:disabled {
65
+ opacity: var(--opacity-disabled);
66
+ pointer-events: none;
67
+ }
68
+
69
+ /* ───────────── ToggleGroup ───────────── */
70
+
71
+ .sh-ui-toggle-group {
72
+ display: inline-flex;
73
+ align-items: center;
74
+ gap: var(--space-1);
75
+ }
76
+
77
+ .sh-ui-toggle-group[data-orientation="vertical"] {
78
+ flex-direction: column;
79
+ }
80
+
81
+ @media (prefers-reduced-motion: reduce) {
82
+ .sh-ui-toggle {
83
+ transition: none;
84
+ }
85
+ }
@@ -0,0 +1,85 @@
1
+ import * as React from "react";
2
+ import { Tooltip as BaseTooltip } from "@base-ui-components/react/tooltip";
3
+ import "./styles.css";
4
+
5
+ type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
6
+
7
+ function cx(...args: (string | undefined | false | null)[]) {
8
+ return args.filter(Boolean).join(" ");
9
+ }
10
+
11
+ /** 여러 Tooltip이 공통 delay를 공유하도록 묶는다. 앱 루트에 한 번 두는 것을 권장. */
12
+ export const TooltipProvider = BaseTooltip.Provider;
13
+
14
+ /** Tooltip 루트. Trigger + Content를 자식으로 갖는다. */
15
+ export const Tooltip = BaseTooltip.Root;
16
+
17
+ /** 호버/포커스로 tooltip을 표시할 엘리먼트를 감싼다. render prop으로 Button 등과 결합. */
18
+ export const TooltipTrigger = BaseTooltip.Trigger;
19
+
20
+ export interface TooltipContentProps
21
+ extends WithStringClassName<
22
+ React.ComponentPropsWithoutRef<typeof BaseTooltip.Popup>
23
+ > {
24
+ /**
25
+ * Trigger 기준 배치 방향. 공간 부족 시 자동으로 반대편으로 뒤집힌다.
26
+ * @default "top"
27
+ */
28
+ side?: "top" | "right" | "bottom" | "left";
29
+ /**
30
+ * 트리거 축에서의 정렬.
31
+ * @default "center"
32
+ */
33
+ align?: "start" | "center" | "end";
34
+ /**
35
+ * Trigger와 Popup 사이 간격(px).
36
+ * @default 6
37
+ */
38
+ sideOffset?: number;
39
+ /**
40
+ * Trigger를 가리키는 화살표 표시 여부.
41
+ * @default false
42
+ */
43
+ showArrow?: boolean;
44
+ /**
45
+ * Portal이 마운트될 DOM 노드.
46
+ * @default document.body
47
+ */
48
+ container?: React.ComponentPropsWithoutRef<
49
+ typeof BaseTooltip.Portal
50
+ >["container"];
51
+ }
52
+
53
+ /**
54
+ * Tooltip의 본문. portal로 마운트되어 트리거 옆에 자동 위치 조정된다.
55
+ * 내용은 짧게 — 긴 설명이 필요하면 Popover를 사용할 것.
56
+ */
57
+ export const TooltipContent = React.forwardRef<
58
+ HTMLDivElement,
59
+ TooltipContentProps
60
+ >(function TooltipContent(
61
+ { className, children, side, align, sideOffset = 6, showArrow, container, ...props },
62
+ ref,
63
+ ) {
64
+ return (
65
+ <BaseTooltip.Portal container={container}>
66
+ <BaseTooltip.Positioner
67
+ className="sh-ui-tooltip__positioner"
68
+ side={side}
69
+ align={align}
70
+ sideOffset={sideOffset}
71
+ >
72
+ <BaseTooltip.Popup
73
+ ref={ref}
74
+ className={cx("sh-ui-tooltip__content", className)}
75
+ {...props}
76
+ >
77
+ {showArrow && (
78
+ <BaseTooltip.Arrow className="sh-ui-tooltip__arrow" />
79
+ )}
80
+ {children}
81
+ </BaseTooltip.Popup>
82
+ </BaseTooltip.Positioner>
83
+ </BaseTooltip.Portal>
84
+ );
85
+ });
@@ -0,0 +1,44 @@
1
+ .sh-ui-tooltip__positioner {
2
+ z-index: var(--z-tooltip, var(--z-popover));
3
+ outline: none;
4
+ }
5
+
6
+ .sh-ui-tooltip__content {
7
+ padding: 0.375rem 0.625rem;
8
+ background: var(--foreground);
9
+ color: var(--background);
10
+ border-radius: calc(var(--radius) - 2px);
11
+ font-size: var(--text-xs);
12
+ line-height: 1.4;
13
+ max-width: 20rem;
14
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
15
+ transform-origin: var(--transform-origin);
16
+ outline: none;
17
+ transition:
18
+ opacity 120ms ease,
19
+ transform 120ms ease;
20
+ }
21
+
22
+ .sh-ui-tooltip__content[data-starting-style],
23
+ .sh-ui-tooltip__content[data-ending-style] {
24
+ opacity: 0;
25
+ transform: scale(0.96);
26
+ }
27
+
28
+ .sh-ui-tooltip__arrow {
29
+ color: var(--foreground);
30
+ }
31
+
32
+ .sh-ui-tooltip__arrow svg {
33
+ display: block;
34
+ }
35
+
36
+ @media (prefers-reduced-motion: reduce) {
37
+ .sh-ui-tooltip__content {
38
+ transition: none;
39
+ }
40
+ .sh-ui-tooltip__content[data-starting-style],
41
+ .sh-ui-tooltip__content[data-ending-style] {
42
+ transform: none;
43
+ }
44
+ }
@@ -0,0 +1,16 @@
1
+ /* sh-ui z-index 스케일 — 레이어 충돌을 방지하기 위한 의미적 z-index 토큰.
2
+ *
3
+ * 사용 예:
4
+ * .my-dropdown { z-index: var(--z-dropdown); }
5
+ */
6
+
7
+ :root {
8
+ --z-base: 0;
9
+ --z-sticky: 100; /* sticky header, sidebar 등 */
10
+ --z-dropdown: 200; /* select/menu 팝업 */
11
+ --z-overlay: 300; /* modal/sheet 배경 dimmer */
12
+ --z-modal: 400; /* modal/dialog/sheet 본체 */
13
+ --z-popover: 500; /* popover, tooltip의 컨테이너 */
14
+ --z-toast: 600; /* 항상 최상단 알림 */
15
+ --z-tooltip: 700;
16
+ }