sh-ui-cli 0.45.3 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/data/changelog/versions.json +26 -0
  2. package/data/registry/react/components/accordion/index.module.tsx +97 -0
  3. package/data/registry/react/components/accordion/styles.module.css +111 -0
  4. package/data/registry/react/components/avatar/index.module.tsx +73 -0
  5. package/data/registry/react/components/avatar/styles.module.css +36 -0
  6. package/data/registry/react/components/badge/index.module.tsx +40 -0
  7. package/data/registry/react/components/badge/styles.module.css +57 -0
  8. package/data/registry/react/components/breadcrumb/index.module.tsx +152 -0
  9. package/data/registry/react/components/breadcrumb/styles.module.css +82 -0
  10. package/data/registry/react/components/button/index.module.tsx +45 -0
  11. package/data/registry/react/components/button/styles.module.css +92 -0
  12. package/data/registry/react/components/calendar/index.module.tsx +806 -0
  13. package/data/registry/react/components/calendar/styles.module.css +213 -0
  14. package/data/registry/react/components/card/index.module.tsx +63 -0
  15. package/data/registry/react/components/card/styles.module.css +73 -0
  16. package/data/registry/react/components/carousel/index.module.tsx +430 -0
  17. package/data/registry/react/components/carousel/styles.module.css +155 -0
  18. package/data/registry/react/components/checkbox/index.module.tsx +96 -0
  19. package/data/registry/react/components/checkbox/styles.module.css +75 -0
  20. package/data/registry/react/components/code-editor/index.module.tsx +230 -0
  21. package/data/registry/react/components/code-editor/styles.module.css +76 -0
  22. package/data/registry/react/components/code-panel/index.module.tsx +191 -0
  23. package/data/registry/react/components/code-panel/styles.module.css +124 -0
  24. package/data/registry/react/components/color-picker/index.module.tsx +467 -0
  25. package/data/registry/react/components/color-picker/styles.module.css +166 -0
  26. package/data/registry/react/components/combobox/index.module.tsx +165 -0
  27. package/data/registry/react/components/combobox/styles.module.css +151 -0
  28. package/data/registry/react/components/context-menu/index.module.tsx +251 -0
  29. package/data/registry/react/components/context-menu/styles.module.css +140 -0
  30. package/data/registry/react/components/date-picker/index.module.tsx +520 -0
  31. package/data/registry/react/components/date-picker/styles.module.css +103 -0
  32. package/data/registry/react/components/dialog/index.module.tsx +95 -0
  33. package/data/registry/react/components/dialog/styles.module.css +127 -0
  34. package/data/registry/react/components/dropdown-menu/index.module.tsx +255 -0
  35. package/data/registry/react/components/dropdown-menu/styles.module.css +150 -0
  36. package/data/registry/react/components/file-upload/index.module.tsx +487 -0
  37. package/data/registry/react/components/file-upload/styles.module.css +170 -0
  38. package/data/registry/react/components/form/index.module.tsx +61 -0
  39. package/data/registry/react/components/form/styles.module.css +47 -0
  40. package/data/registry/react/components/header/index.module.tsx +805 -0
  41. package/data/registry/react/components/header/styles.module.css +350 -0
  42. package/data/registry/react/components/input/index.module.tsx +486 -0
  43. package/data/registry/react/components/input/styles.module.css +200 -0
  44. package/data/registry/react/components/label/index.module.tsx +52 -0
  45. package/data/registry/react/components/label/styles.module.css +90 -0
  46. package/data/registry/react/components/markdown-editor/index.module.tsx +119 -0
  47. package/data/registry/react/components/markdown-editor/styles.module.css +160 -0
  48. package/data/registry/react/components/menubar/index.module.tsx +32 -0
  49. package/data/registry/react/components/menubar/styles.module.css +45 -0
  50. package/data/registry/react/components/numeric-input/index.module.tsx +148 -0
  51. package/data/registry/react/components/numeric-input/styles.module.css +56 -0
  52. package/data/registry/react/components/page-toc/index.module.tsx +174 -0
  53. package/data/registry/react/components/page-toc/styles.module.css +82 -0
  54. package/data/registry/react/components/pagination/index.module.tsx +269 -0
  55. package/data/registry/react/components/pagination/styles.module.css +105 -0
  56. package/data/registry/react/components/popover/index.module.tsx +113 -0
  57. package/data/registry/react/components/popover/styles.module.css +65 -0
  58. package/data/registry/react/components/progress/index.module.tsx +54 -0
  59. package/data/registry/react/components/progress/styles.module.css +41 -0
  60. package/data/registry/react/components/radio/index.module.tsx +65 -0
  61. package/data/registry/react/components/radio/styles.module.css +80 -0
  62. package/data/registry/react/components/rich-text-editor/index.module.tsx +348 -0
  63. package/data/registry/react/components/rich-text-editor/styles.module.css +196 -0
  64. package/data/registry/react/components/select/index.module.tsx +234 -0
  65. package/data/registry/react/components/select/styles.module.css +193 -0
  66. package/data/registry/react/components/separator/index.module.tsx +46 -0
  67. package/data/registry/react/components/separator/styles.module.css +15 -0
  68. package/data/registry/react/components/sidebar/index.module.tsx +1067 -0
  69. package/data/registry/react/components/sidebar/styles.module.css +502 -0
  70. package/data/registry/react/components/skeleton/index.module.tsx +22 -0
  71. package/data/registry/react/components/skeleton/styles.module.css +24 -0
  72. package/data/registry/react/components/slider/index.module.tsx +298 -0
  73. package/data/registry/react/components/slider/styles.module.css +64 -0
  74. package/data/registry/react/components/spinner/index.module.tsx +38 -0
  75. package/data/registry/react/components/spinner/styles.module.css +37 -0
  76. package/data/registry/react/components/switch/index.module.tsx +39 -0
  77. package/data/registry/react/components/switch/styles.module.css +83 -0
  78. package/data/registry/react/components/tabs/index.module.tsx +91 -0
  79. package/data/registry/react/components/tabs/styles.module.css +148 -0
  80. package/data/registry/react/components/textarea/index.module.tsx +23 -0
  81. package/data/registry/react/components/textarea/styles.module.css +54 -0
  82. package/data/registry/react/components/toast/index.module.tsx +258 -0
  83. package/data/registry/react/components/toast/styles.module.css +290 -0
  84. package/data/registry/react/components/toggle/index.module.tsx +131 -0
  85. package/data/registry/react/components/toggle/styles.module.css +85 -0
  86. package/data/registry/react/components/tooltip/index.module.tsx +83 -0
  87. package/data/registry/react/components/tooltip/styles.module.css +44 -0
  88. package/data/registry/react/registry.json +604 -1
  89. package/data/tokens/build.mjs +4 -0
  90. package/package.json +1 -1
  91. package/src/add.mjs +12 -12
  92. package/src/api.d.ts +4 -3
  93. package/src/constants.js +4 -3
@@ -0,0 +1,290 @@
1
+ /* ── Viewport (고정 컨테이너) ── */
2
+
3
+ .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
+ .toast-viewport[data-position="bottom-right"] {
17
+ bottom: var(--space-4);
18
+ right: var(--space-4);
19
+ flex-direction: column-reverse;
20
+ }
21
+
22
+ .toast-viewport[data-position="bottom-left"] {
23
+ bottom: var(--space-4);
24
+ left: var(--space-4);
25
+ flex-direction: column-reverse;
26
+ }
27
+
28
+ .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
+ .toast-viewport[data-position="top-right"] {
36
+ top: var(--space-4);
37
+ right: var(--space-4);
38
+ }
39
+
40
+ .toast-viewport[data-position="top-left"] {
41
+ top: var(--space-4);
42
+ left: var(--space-4);
43
+ }
44
+
45
+ .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
+ .toast-viewport {
53
+ max-width: 100%;
54
+ padding: var(--space-4);
55
+ }
56
+
57
+ .toast-viewport[data-position="bottom-right"],
58
+ .toast-viewport[data-position="bottom-left"],
59
+ .toast-viewport[data-position="bottom-center"] {
60
+ right: 0;
61
+ left: 0;
62
+ bottom: 0;
63
+ transform: none;
64
+ }
65
+
66
+ .toast-viewport[data-position="top-right"],
67
+ .toast-viewport[data-position="top-left"],
68
+ .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
+ .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"] .toast,
97
+ [data-position="top-right"] .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"] .toast[data-exiting],
102
+ [data-position="top-right"] .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"] .toast,
108
+ [data-position="top-left"] .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"] .toast[data-exiting],
113
+ [data-position="top-left"] .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"] .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"] .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"] .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"] .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"] .toast,
180
+ [data-position="bottom-left"] .toast,
181
+ [data-position="bottom-center"] .toast {
182
+ animation-name: sh-ui-toast-enter-bottom;
183
+ }
184
+
185
+ [data-position="bottom-right"] .toast[data-exiting],
186
+ [data-position="bottom-left"] .toast[data-exiting],
187
+ [data-position="bottom-center"] .toast[data-exiting] {
188
+ animation-name: sh-ui-toast-exit-bottom;
189
+ }
190
+
191
+ [data-position="top-right"] .toast,
192
+ [data-position="top-left"] .toast,
193
+ [data-position="top-center"] .toast {
194
+ animation-name: sh-ui-toast-enter-top;
195
+ }
196
+
197
+ [data-position="top-right"] .toast[data-exiting],
198
+ [data-position="top-left"] .toast[data-exiting],
199
+ [data-position="top-center"] .toast[data-exiting] {
200
+ animation-name: sh-ui-toast-exit-top;
201
+ }
202
+ }
203
+
204
+ /* ── Variant colors ── */
205
+
206
+ .toast__icon {
207
+ flex-shrink: 0;
208
+ display: inline-flex;
209
+ align-items: center;
210
+ margin-top: 0.125rem;
211
+ }
212
+
213
+ .toast--success .toast__icon { color: var(--success, #16a34a); }
214
+ .toast--danger .toast__icon { color: var(--danger); }
215
+ .toast--warning .toast__icon { color: var(--warning, #d97706); }
216
+
217
+ /* ── Body ── */
218
+
219
+ .toast__body {
220
+ flex: 1;
221
+ min-width: 0;
222
+ }
223
+
224
+ .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
+ .toast__description {
232
+ margin: 0;
233
+ font-size: 0.8125rem;
234
+ line-height: 1.4;
235
+ color: var(--foreground-muted);
236
+ }
237
+
238
+ .toast__title + .toast__description {
239
+ margin-top: 0.125rem;
240
+ }
241
+
242
+ /* ── Action slot ── */
243
+
244
+ .toast__action {
245
+ flex-shrink: 0;
246
+ display: inline-flex;
247
+ align-items: center;
248
+ margin-left: auto;
249
+ }
250
+
251
+ /* ── Close ── */
252
+
253
+ .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
+ .toast__close:hover {
274
+ background: var(--background-muted);
275
+ color: var(--foreground);
276
+ }
277
+
278
+ .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
+ .toast {
285
+ animation: none !important;
286
+ }
287
+ .toast__close {
288
+ transition: none;
289
+ }
290
+ }
@@ -0,0 +1,131 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Toggle as BaseToggle } from "@base-ui/react/toggle";
5
+ import { ToggleGroup as BaseToggleGroup } from "@base-ui/react/toggle-group";
6
+ import styles from "./styles.module.css";
7
+
8
+
9
+ import { cn } from "@SH_UI_UTILS@";
10
+ /* ───────────── Toggle ───────────── */
11
+
12
+ export type ToggleVariant = "outline" | "ghost";
13
+ export type ToggleSize = "sm" | "md" | "lg";
14
+
15
+ export type ToggleProps = Omit<
16
+ React.ComponentPropsWithoutRef<typeof BaseToggle>,
17
+ "className"
18
+ > & {
19
+ className?: string;
20
+ /**
21
+ * 외형 변형.
22
+ * - `ghost` — 배경 없음, 눌림 시만 강조 (기본)
23
+ * - `outline` — 항상 border 표시
24
+ *
25
+ * @default "ghost"
26
+ */
27
+ variant?: ToggleVariant;
28
+ /**
29
+ * 크기. `sm` / `md` / `lg`.
30
+ *
31
+ * @default "md"
32
+ */
33
+ size?: ToggleSize;
34
+ };
35
+
36
+ /**
37
+ * 눌린 상태(pressed)를 가진 버튼. 툴바의 "굵게/기울임" 같은 즉시 토글 액션에 적합.
38
+ * 시각만으로 상태를 구분하지 말고 `aria-label`이나 아이콘 옆 텍스트로 의미를 명확히 할 것.
39
+ */
40
+ export const Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(
41
+ ({ className, variant = "ghost", size = "md", ...props }, ref) => (
42
+ <BaseToggle
43
+ ref={ref}
44
+ className={cn(
45
+ styles.toggle,
46
+ styles[`toggle--${variant}`],
47
+ styles[`toggle--${size}`],
48
+ className,
49
+ )}
50
+ {...props}
51
+ />
52
+ ),
53
+ );
54
+ Toggle.displayName = "Toggle";
55
+
56
+ /* ───────────── ToggleGroup ───────────── */
57
+
58
+ export type ToggleGroupProps = Omit<
59
+ React.ComponentPropsWithoutRef<typeof BaseToggleGroup>,
60
+ "className"
61
+ > & {
62
+ className?: string;
63
+ /**
64
+ * 그룹 내 모든 항목에 적용될 외형. 자식 ToggleGroupItem이 자동 상속한다.
65
+ * @default "ghost"
66
+ */
67
+ variant?: ToggleVariant;
68
+ /**
69
+ * 그룹 내 모든 항목에 적용될 크기. 자식 ToggleGroupItem이 자동 상속한다.
70
+ * @default "md"
71
+ */
72
+ size?: ToggleSize;
73
+ };
74
+
75
+ interface ToggleGroupContextValue {
76
+ variant: ToggleVariant;
77
+ size: ToggleSize;
78
+ }
79
+
80
+ const ToggleGroupContext = React.createContext<ToggleGroupContextValue>({
81
+ variant: "ghost",
82
+ size: "md",
83
+ });
84
+
85
+ export const useToggleGroupStyle = () => React.useContext(ToggleGroupContext);
86
+
87
+ /**
88
+ * 여러 ToggleGroupItem을 묶는 컨테이너. `toggleMultiple` 옵션으로 단일/다중 선택을
89
+ * 결정하고, 그룹 단위로 variant·size를 적용한다. 항목들은 반드시 `ToggleGroupItem`을 사용할 것.
90
+ */
91
+ export const ToggleGroup = React.forwardRef<HTMLDivElement, ToggleGroupProps>(
92
+ ({ className, variant = "ghost", size = "md", ...props }, ref) => (
93
+ <ToggleGroupContext.Provider value={{ variant, size }}>
94
+ <BaseToggleGroup
95
+ ref={ref}
96
+ className={cn(styles["toggle-group"], className)}
97
+ {...props}
98
+ />
99
+ </ToggleGroupContext.Provider>
100
+ ),
101
+ );
102
+ ToggleGroup.displayName = "ToggleGroup";
103
+
104
+ /* ───────────── ToggleGroupItem ───────────── */
105
+
106
+ export type ToggleGroupItemProps = Omit<
107
+ React.ComponentPropsWithoutRef<typeof BaseToggle>,
108
+ "className"
109
+ > & {
110
+ className?: string;
111
+ };
112
+
113
+ /** ToggleGroup의 자식 항목. 부모 그룹의 variant·size를 자동으로 상속한다. */
114
+ export const ToggleGroupItem = React.forwardRef<HTMLButtonElement, ToggleGroupItemProps>(
115
+ ({ className, ...props }, ref) => {
116
+ const { variant, size } = useToggleGroupStyle();
117
+ return (
118
+ <BaseToggle
119
+ ref={ref}
120
+ className={cn(
121
+ styles.toggle,
122
+ styles[`toggle--${variant}`],
123
+ styles[`toggle--${size}`],
124
+ className,
125
+ )}
126
+ {...props}
127
+ />
128
+ );
129
+ },
130
+ );
131
+ ToggleGroupItem.displayName = "ToggleGroupItem";
@@ -0,0 +1,85 @@
1
+ /* ───────────── Toggle ───────────── */
2
+
3
+ .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
+ .toggle--sm { height: var(--control-sm); padding: 0 0.625rem; font-size: var(--text-sm); }
22
+ .toggle--md { height: var(--control-md); padding: 0 var(--space-3); font-size: var(--text-sm); }
23
+ .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
+ .toggle--sm { height: 2.25rem; }
28
+ .toggle--md { height: 2.75rem; }
29
+ }
30
+
31
+ /* variant: outline */
32
+ .toggle--outline {
33
+ border-color: var(--border);
34
+ }
35
+
36
+ .toggle--outline:hover:not(:disabled):not([data-pressed]) {
37
+ background: var(--background-muted);
38
+ color: var(--foreground);
39
+ }
40
+
41
+ /* variant: ghost */
42
+ .toggle--ghost:hover:not(:disabled):not([data-pressed]) {
43
+ background: var(--background-muted);
44
+ color: var(--foreground);
45
+ }
46
+
47
+ /* pressed */
48
+ .toggle[data-pressed] {
49
+ background: var(--background-muted);
50
+ color: var(--foreground);
51
+ }
52
+
53
+ .toggle--outline[data-pressed] {
54
+ border-color: var(--border-strong);
55
+ }
56
+
57
+ /* focus */
58
+ .toggle:focus-visible {
59
+ outline: var(--border-width-strong) solid var(--foreground);
60
+ outline-offset: 2px;
61
+ }
62
+
63
+ /* disabled */
64
+ .toggle:disabled {
65
+ opacity: var(--opacity-disabled);
66
+ pointer-events: none;
67
+ }
68
+
69
+ /* ───────────── ToggleGroup ───────────── */
70
+
71
+ .toggle-group {
72
+ display: inline-flex;
73
+ align-items: center;
74
+ gap: var(--space-1);
75
+ }
76
+
77
+ .toggle-group[data-orientation="vertical"] {
78
+ flex-direction: column;
79
+ }
80
+
81
+ @media (prefers-reduced-motion: reduce) {
82
+ .toggle {
83
+ transition: none;
84
+ }
85
+ }
@@ -0,0 +1,83 @@
1
+ import * as React from "react";
2
+ import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
3
+ import styles from "./styles.module.css";
4
+
5
+ import { cn } from "@SH_UI_UTILS@";
6
+ type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
7
+
8
+
9
+ /** 여러 Tooltip이 공통 delay를 공유하도록 묶는다. 앱 루트에 한 번 두는 것을 권장. */
10
+ export const TooltipProvider = BaseTooltip.Provider;
11
+
12
+ /** Tooltip 루트. Trigger + Content를 자식으로 갖는다. */
13
+ export const Tooltip = BaseTooltip.Root;
14
+
15
+ /** 호버/포커스로 tooltip을 표시할 엘리먼트를 감싼다. render prop으로 Button 등과 결합. */
16
+ export const TooltipTrigger = BaseTooltip.Trigger;
17
+
18
+ export interface TooltipContentProps
19
+ extends WithStringClassName<
20
+ React.ComponentPropsWithoutRef<typeof BaseTooltip.Popup>
21
+ > {
22
+ /**
23
+ * Trigger 기준 배치 방향. 공간 부족 시 자동으로 반대편으로 뒤집힌다.
24
+ * @default "top"
25
+ */
26
+ side?: "top" | "right" | "bottom" | "left";
27
+ /**
28
+ * 트리거 축에서의 정렬.
29
+ * @default "center"
30
+ */
31
+ align?: "start" | "center" | "end";
32
+ /**
33
+ * Trigger와 Popup 사이 간격(px).
34
+ * @default 6
35
+ */
36
+ sideOffset?: number;
37
+ /**
38
+ * Trigger를 가리키는 화살표 표시 여부.
39
+ * @default false
40
+ */
41
+ showArrow?: boolean;
42
+ /**
43
+ * Portal이 마운트될 DOM 노드.
44
+ * @default document.body
45
+ */
46
+ container?: React.ComponentPropsWithoutRef<
47
+ typeof BaseTooltip.Portal
48
+ >["container"];
49
+ }
50
+
51
+ /**
52
+ * Tooltip의 본문. portal로 마운트되어 트리거 옆에 자동 위치 조정된다.
53
+ * 내용은 짧게 — 긴 설명이 필요하면 Popover를 사용할 것.
54
+ */
55
+ export const TooltipContent = React.forwardRef<
56
+ HTMLDivElement,
57
+ TooltipContentProps
58
+ >(function TooltipContent(
59
+ { className, children, side, align, sideOffset = 6, showArrow, container, ...props },
60
+ ref,
61
+ ) {
62
+ return (
63
+ <BaseTooltip.Portal container={container}>
64
+ <BaseTooltip.Positioner
65
+ className={styles.tooltip__positioner}
66
+ side={side}
67
+ align={align}
68
+ sideOffset={sideOffset}
69
+ >
70
+ <BaseTooltip.Popup
71
+ ref={ref}
72
+ className={cn(styles.tooltip__content, className)}
73
+ {...props}
74
+ >
75
+ {showArrow && (
76
+ <BaseTooltip.Arrow className={styles.tooltip__arrow} />
77
+ )}
78
+ {children}
79
+ </BaseTooltip.Popup>
80
+ </BaseTooltip.Positioner>
81
+ </BaseTooltip.Portal>
82
+ );
83
+ });
@@ -0,0 +1,44 @@
1
+ .tooltip__positioner {
2
+ z-index: var(--z-tooltip, var(--z-popover));
3
+ outline: none;
4
+ }
5
+
6
+ .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
+ .tooltip__content[data-starting-style],
23
+ .tooltip__content[data-ending-style] {
24
+ opacity: 0;
25
+ transform: scale(0.96);
26
+ }
27
+
28
+ .tooltip__arrow {
29
+ color: var(--foreground);
30
+ }
31
+
32
+ .tooltip__arrow svg {
33
+ display: block;
34
+ }
35
+
36
+ @media (prefers-reduced-motion: reduce) {
37
+ .tooltip__content {
38
+ transition: none;
39
+ }
40
+ .tooltip__content[data-starting-style],
41
+ .tooltip__content[data-ending-style] {
42
+ transform: none;
43
+ }
44
+ }