sh-ui-cli 0.46.0 → 0.48.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 (85) hide show
  1. package/data/changelog/versions.json +25 -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/calendar/index.module.tsx +806 -0
  11. package/data/registry/react/components/calendar/styles.module.css +213 -0
  12. package/data/registry/react/components/carousel/index.module.tsx +430 -0
  13. package/data/registry/react/components/carousel/styles.module.css +155 -0
  14. package/data/registry/react/components/checkbox/index.module.tsx +96 -0
  15. package/data/registry/react/components/checkbox/styles.module.css +75 -0
  16. package/data/registry/react/components/code-editor/index.module.tsx +230 -0
  17. package/data/registry/react/components/code-editor/styles.module.css +76 -0
  18. package/data/registry/react/components/code-panel/index.module.tsx +191 -0
  19. package/data/registry/react/components/code-panel/styles.module.css +124 -0
  20. package/data/registry/react/components/color-picker/index.module.tsx +467 -0
  21. package/data/registry/react/components/color-picker/styles.module.css +166 -0
  22. package/data/registry/react/components/combobox/index.module.tsx +165 -0
  23. package/data/registry/react/components/combobox/styles.module.css +151 -0
  24. package/data/registry/react/components/context-menu/index.module.tsx +251 -0
  25. package/data/registry/react/components/context-menu/styles.module.css +140 -0
  26. package/data/registry/react/components/date-picker/index.module.tsx +520 -0
  27. package/data/registry/react/components/date-picker/styles.module.css +103 -0
  28. package/data/registry/react/components/dialog/index.module.tsx +95 -0
  29. package/data/registry/react/components/dialog/styles.module.css +127 -0
  30. package/data/registry/react/components/dropdown-menu/index.module.tsx +255 -0
  31. package/data/registry/react/components/dropdown-menu/styles.module.css +150 -0
  32. package/data/registry/react/components/file-upload/index.module.tsx +487 -0
  33. package/data/registry/react/components/file-upload/styles.module.css +170 -0
  34. package/data/registry/react/components/form/index.module.tsx +61 -0
  35. package/data/registry/react/components/form/styles.module.css +47 -0
  36. package/data/registry/react/components/header/index.module.tsx +805 -0
  37. package/data/registry/react/components/header/styles.module.css +350 -0
  38. package/data/registry/react/components/label/index.module.tsx +52 -0
  39. package/data/registry/react/components/label/styles.module.css +90 -0
  40. package/data/registry/react/components/markdown-editor/index.module.tsx +119 -0
  41. package/data/registry/react/components/markdown-editor/styles.module.css +160 -0
  42. package/data/registry/react/components/menubar/index.module.tsx +32 -0
  43. package/data/registry/react/components/menubar/styles.module.css +45 -0
  44. package/data/registry/react/components/numeric-input/index.module.tsx +148 -0
  45. package/data/registry/react/components/numeric-input/styles.module.css +56 -0
  46. package/data/registry/react/components/page-toc/index.module.tsx +174 -0
  47. package/data/registry/react/components/page-toc/styles.module.css +82 -0
  48. package/data/registry/react/components/pagination/index.module.tsx +269 -0
  49. package/data/registry/react/components/pagination/styles.module.css +105 -0
  50. package/data/registry/react/components/popover/index.module.tsx +113 -0
  51. package/data/registry/react/components/popover/styles.module.css +65 -0
  52. package/data/registry/react/components/progress/index.module.tsx +54 -0
  53. package/data/registry/react/components/progress/styles.module.css +41 -0
  54. package/data/registry/react/components/radio/index.module.tsx +65 -0
  55. package/data/registry/react/components/radio/styles.module.css +80 -0
  56. package/data/registry/react/components/rich-text-editor/index.module.tsx +348 -0
  57. package/data/registry/react/components/rich-text-editor/styles.module.css +196 -0
  58. package/data/registry/react/components/select/index.module.tsx +234 -0
  59. package/data/registry/react/components/select/styles.module.css +193 -0
  60. package/data/registry/react/components/separator/index.module.tsx +46 -0
  61. package/data/registry/react/components/separator/styles.module.css +15 -0
  62. package/data/registry/react/components/sidebar/index.module.tsx +1067 -0
  63. package/data/registry/react/components/sidebar/styles.module.css +502 -0
  64. package/data/registry/react/components/skeleton/index.module.tsx +22 -0
  65. package/data/registry/react/components/skeleton/styles.module.css +24 -0
  66. package/data/registry/react/components/slider/index.module.tsx +298 -0
  67. package/data/registry/react/components/slider/styles.module.css +64 -0
  68. package/data/registry/react/components/spinner/index.module.tsx +38 -0
  69. package/data/registry/react/components/spinner/styles.module.css +37 -0
  70. package/data/registry/react/components/switch/index.module.tsx +39 -0
  71. package/data/registry/react/components/switch/styles.module.css +83 -0
  72. package/data/registry/react/components/tabs/index.module.tsx +91 -0
  73. package/data/registry/react/components/tabs/styles.module.css +148 -0
  74. package/data/registry/react/components/textarea/index.module.tsx +23 -0
  75. package/data/registry/react/components/textarea/styles.module.css +54 -0
  76. package/data/registry/react/components/toast/index.module.tsx +258 -0
  77. package/data/registry/react/components/toast/styles.module.css +290 -0
  78. package/data/registry/react/components/toggle/index.module.tsx +131 -0
  79. package/data/registry/react/components/toggle/styles.module.css +85 -0
  80. package/data/registry/react/components/tooltip/index.module.tsx +83 -0
  81. package/data/registry/react/components/tooltip/styles.module.css +44 -0
  82. package/data/registry/react/registry.json +560 -0
  83. package/package.json +1 -1
  84. package/src/api.d.ts +4 -3
  85. 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
+ }