sh-ui-cli 0.48.0 → 0.50.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 +27 -0
  2. package/data/registry/react/components/accordion/index.vanilla-extract.tsx +97 -0
  3. package/data/registry/react/components/accordion/styles.css.ts +131 -0
  4. package/data/registry/react/components/avatar/index.vanilla-extract.tsx +73 -0
  5. package/data/registry/react/components/avatar/styles.css.ts +68 -0
  6. package/data/registry/react/components/badge/index.vanilla-extract.tsx +40 -0
  7. package/data/registry/react/components/badge/styles.css.ts +71 -0
  8. package/data/registry/react/components/breadcrumb/index.vanilla-extract.tsx +152 -0
  9. package/data/registry/react/components/breadcrumb/styles.css.ts +95 -0
  10. package/data/registry/react/components/button/index.vanilla-extract.tsx +45 -0
  11. package/data/registry/react/components/button/styles.css.ts +120 -0
  12. package/data/registry/react/components/calendar/index.vanilla-extract.tsx +806 -0
  13. package/data/registry/react/components/calendar/styles.css.ts +250 -0
  14. package/data/registry/react/components/card/index.vanilla-extract.tsx +63 -0
  15. package/data/registry/react/components/card/styles.css.ts +88 -0
  16. package/data/registry/react/components/carousel/index.vanilla-extract.tsx +430 -0
  17. package/data/registry/react/components/carousel/styles.css.ts +169 -0
  18. package/data/registry/react/components/checkbox/index.vanilla-extract.tsx +96 -0
  19. package/data/registry/react/components/checkbox/styles.css.ts +74 -0
  20. package/data/registry/react/components/code-editor/index.vanilla-extract.tsx +230 -0
  21. package/data/registry/react/components/code-editor/styles.css.ts +97 -0
  22. package/data/registry/react/components/code-panel/index.vanilla-extract.tsx +191 -0
  23. package/data/registry/react/components/code-panel/styles.css.ts +151 -0
  24. package/data/registry/react/components/color-picker/index.vanilla-extract.tsx +467 -0
  25. package/data/registry/react/components/color-picker/styles.css.ts +169 -0
  26. package/data/registry/react/components/combobox/index.vanilla-extract.tsx +165 -0
  27. package/data/registry/react/components/combobox/styles.css.ts +174 -0
  28. package/data/registry/react/components/context-menu/index.vanilla-extract.tsx +251 -0
  29. package/data/registry/react/components/context-menu/styles.css.ts +167 -0
  30. package/data/registry/react/components/date-picker/index.vanilla-extract.tsx +520 -0
  31. package/data/registry/react/components/date-picker/styles.css.ts +111 -0
  32. package/data/registry/react/components/dialog/index.vanilla-extract.tsx +95 -0
  33. package/data/registry/react/components/dialog/styles.css.ts +140 -0
  34. package/data/registry/react/components/dropdown-menu/index.vanilla-extract.tsx +255 -0
  35. package/data/registry/react/components/dropdown-menu/styles.css.ts +175 -0
  36. package/data/registry/react/components/file-upload/index.vanilla-extract.tsx +487 -0
  37. package/data/registry/react/components/file-upload/styles.css.ts +193 -0
  38. package/data/registry/react/components/form/index.vanilla-extract.tsx +61 -0
  39. package/data/registry/react/components/form/styles.css.ts +56 -0
  40. package/data/registry/react/components/header/index.vanilla-extract.tsx +805 -0
  41. package/data/registry/react/components/header/styles.css.ts +413 -0
  42. package/data/registry/react/components/input/index.vanilla-extract.tsx +425 -0
  43. package/data/registry/react/components/input/styles.css.ts +202 -0
  44. package/data/registry/react/components/label/index.vanilla-extract.tsx +52 -0
  45. package/data/registry/react/components/label/styles.css.ts +141 -0
  46. package/data/registry/react/components/markdown-editor/index.vanilla-extract.tsx +119 -0
  47. package/data/registry/react/components/markdown-editor/styles.css.ts +231 -0
  48. package/data/registry/react/components/menubar/index.vanilla-extract.tsx +32 -0
  49. package/data/registry/react/components/menubar/styles.css.ts +53 -0
  50. package/data/registry/react/components/numeric-input/index.vanilla-extract.tsx +148 -0
  51. package/data/registry/react/components/numeric-input/styles.css.ts +65 -0
  52. package/data/registry/react/components/page-toc/index.vanilla-extract.tsx +174 -0
  53. package/data/registry/react/components/page-toc/styles.css.ts +97 -0
  54. package/data/registry/react/components/pagination/index.vanilla-extract.tsx +269 -0
  55. package/data/registry/react/components/pagination/styles.css.ts +113 -0
  56. package/data/registry/react/components/popover/index.vanilla-extract.tsx +113 -0
  57. package/data/registry/react/components/popover/styles.css.ts +78 -0
  58. package/data/registry/react/components/progress/index.vanilla-extract.tsx +54 -0
  59. package/data/registry/react/components/progress/styles.css.ts +53 -0
  60. package/data/registry/react/components/radio/index.vanilla-extract.tsx +65 -0
  61. package/data/registry/react/components/radio/styles.css.ts +79 -0
  62. package/data/registry/react/components/rich-text-editor/index.vanilla-extract.tsx +348 -0
  63. package/data/registry/react/components/rich-text-editor/styles.css.ts +243 -0
  64. package/data/registry/react/components/select/index.vanilla-extract.tsx +234 -0
  65. package/data/registry/react/components/select/styles.css.ts +225 -0
  66. package/data/registry/react/components/separator/index.vanilla-extract.tsx +46 -0
  67. package/data/registry/react/components/separator/styles.css.ts +24 -0
  68. package/data/registry/react/components/sidebar/index.vanilla-extract.tsx +1067 -0
  69. package/data/registry/react/components/sidebar/styles.css.ts +578 -0
  70. package/data/registry/react/components/skeleton/index.vanilla-extract.tsx +22 -0
  71. package/data/registry/react/components/skeleton/styles.css.ts +30 -0
  72. package/data/registry/react/components/slider/index.vanilla-extract.tsx +298 -0
  73. package/data/registry/react/components/slider/styles.css.ts +75 -0
  74. package/data/registry/react/components/spinner/index.vanilla-extract.tsx +38 -0
  75. package/data/registry/react/components/spinner/styles.css.ts +60 -0
  76. package/data/registry/react/components/switch/index.vanilla-extract.tsx +39 -0
  77. package/data/registry/react/components/switch/styles.css.ts +87 -0
  78. package/data/registry/react/components/tabs/index.vanilla-extract.tsx +91 -0
  79. package/data/registry/react/components/tabs/styles.css.ts +145 -0
  80. package/data/registry/react/components/textarea/index.vanilla-extract.tsx +23 -0
  81. package/data/registry/react/components/textarea/styles.css.ts +55 -0
  82. package/data/registry/react/components/toast/index.vanilla-extract.tsx +258 -0
  83. package/data/registry/react/components/toast/styles.css.ts +307 -0
  84. package/data/registry/react/components/toggle/index.vanilla-extract.tsx +131 -0
  85. package/data/registry/react/components/toggle/styles.css.ts +109 -0
  86. package/data/registry/react/components/tooltip/index.vanilla-extract.tsx +83 -0
  87. package/data/registry/react/components/tooltip/styles.css.ts +59 -0
  88. package/data/registry/react/peer-versions.json +1 -0
  89. package/data/registry/react/registry.json +922 -42
  90. package/data/tokens/build.mjs +3 -0
  91. package/package.json +1 -1
  92. package/src/api.d.ts +4 -3
  93. package/src/constants.js +4 -3
@@ -0,0 +1,95 @@
1
+ import * as React from "react";
2
+ import { Dialog as BaseDialog } from "@base-ui/react/dialog";
3
+ import { byKey, dialog__backdrop, dialog__content, dialog__title, dialog__description, dialog__footer, dialog__close } from "./styles.css";
4
+
5
+ import { cn } from "@SH_UI_UTILS@";
6
+ type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
7
+
8
+
9
+ /**
10
+ * 모달 다이얼로그 루트. 열림 상태를 가지며 자식으로 Trigger·Content를 둔다.
11
+ * 주의를 강제하는 흐름(확인/입력)에 사용하고, 단순 안내는 Popover나 Toast를 권장.
12
+ */
13
+ export const Dialog = BaseDialog.Root;
14
+
15
+ /** Dialog를 여는 트리거. 보통 Button을 감싸 사용. */
16
+ export const DialogTrigger = BaseDialog.Trigger;
17
+
18
+ /** 클릭 시 Dialog를 닫는 요소. footer의 취소 버튼 등에 사용. */
19
+ export const DialogClose = BaseDialog.Close;
20
+
21
+ /** 우상단에 배치되는 X 닫기 버튼. `aria-label="닫기"`가 자동 부여된다. */
22
+ export function DialogCloseX({ className, children, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
23
+ return (
24
+ <BaseDialog.Close
25
+ className={cn(dialog__close, className)}
26
+ aria-label="닫기"
27
+ {...props}
28
+ >
29
+ {children ?? "×"}
30
+ </BaseDialog.Close>
31
+ );
32
+ }
33
+
34
+ /** Dialog 본문 하단의 액션 버튼 영역. 보통 [취소, 확인] 순서로 배치. */
35
+ export function DialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
36
+ return <div className={cn(dialog__footer, className)} {...props} />;
37
+ }
38
+
39
+ export interface DialogContentProps
40
+ extends WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseDialog.Popup>> {
41
+ /**
42
+ * Portal이 마운트될 DOM 노드. 모달이 다른 stacking context에 갇혀야 할 때 지정한다.
43
+ * @default document.body
44
+ */
45
+ container?: React.ComponentPropsWithoutRef<typeof BaseDialog.Portal>["container"];
46
+ }
47
+
48
+ /**
49
+ * Dialog의 실제 콘텐츠. Portal로 body 끝에 마운트되며 backdrop·focus trap·ESC 닫힘 등이 자동 처리된다.
50
+ * 접근성: 안에 반드시 `DialogTitle`을 두고, 추가 설명은 `DialogDescription`으로 연결할 것.
51
+ */
52
+ export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
53
+ function DialogContent({ className, children, container, ...props }, ref) {
54
+ return (
55
+ <BaseDialog.Portal container={container}>
56
+ <BaseDialog.Backdrop className={dialog__backdrop} />
57
+ <BaseDialog.Popup
58
+ ref={ref}
59
+ className={cn(dialog__content, className)}
60
+ {...props}
61
+ >
62
+ {children}
63
+ </BaseDialog.Popup>
64
+ </BaseDialog.Portal>
65
+ );
66
+ },
67
+ );
68
+
69
+ /** Dialog의 제목. 접근성을 위해 DialogContent 안에 항상 포함시킬 것. */
70
+ export const DialogTitle = React.forwardRef<
71
+ HTMLHeadingElement,
72
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseDialog.Title>>
73
+ >(function DialogTitle({ className, ...props }, ref) {
74
+ return (
75
+ <BaseDialog.Title
76
+ ref={ref}
77
+ className={cn(dialog__title, className)}
78
+ {...props}
79
+ />
80
+ );
81
+ });
82
+
83
+ /** Dialog의 보조 설명. 제목만으로 맥락이 부족할 때 사용한다. */
84
+ export const DialogDescription = React.forwardRef<
85
+ HTMLParagraphElement,
86
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseDialog.Description>>
87
+ >(function DialogDescription({ className, ...props }, ref) {
88
+ return (
89
+ <BaseDialog.Description
90
+ ref={ref}
91
+ className={cn(dialog__description, className)}
92
+ {...props}
93
+ />
94
+ );
95
+ });
@@ -0,0 +1,140 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const dialog__backdrop = style({
4
+ position: "fixed",
5
+ inset: 0,
6
+ zIndex: "var(--z-overlay)",
7
+ background: "rgba(0, 0, 0, 0.25)",
8
+ backdropFilter: "blur(8px)",
9
+ transition: "opacity var(--duration-slow) ease",
10
+ selectors: {
11
+ "&[data-starting-style]": {
12
+ opacity: 0,
13
+ },
14
+ "&[data-ending-style]": {
15
+ opacity: 0,
16
+ },
17
+ },
18
+ "@media": {
19
+ "(prefers-reduced-motion: reduce)": {
20
+ transition: "none",
21
+ },
22
+ },
23
+ });
24
+
25
+ export const dialog__content = style({
26
+ position: "fixed",
27
+ top: "50%",
28
+ left: "50%",
29
+ transform: "translate(-50%, -50%)",
30
+ zIndex: "var(--z-modal)",
31
+ display: "flex",
32
+ flexDirection: "column",
33
+ width: "calc(100% - 2rem)",
34
+ maxWidth: "28rem",
35
+ maxHeight: "calc(100dvh - 4rem)",
36
+ padding: "var(--space-6)",
37
+ background: "var(--background)",
38
+ color: "var(--foreground)",
39
+ border: "1px solid var(--border)",
40
+ borderRadius: "var(--radius)",
41
+ boxShadow: "var(--shadow-xl)",
42
+ outline: "none",
43
+ overflowY: "auto",
44
+ transition: "opacity var(--duration-slow) ease,\n transform var(--duration-slow) ease",
45
+ selectors: {
46
+ "&[data-starting-style]": {
47
+ opacity: 0,
48
+ transform: "translate(-50%, calc(-50% + 0.5rem)) scale(0.97)",
49
+ },
50
+ "&[data-ending-style]": {
51
+ opacity: 0,
52
+ transform: "translate(-50%, calc(-50% + 0.25rem)) scale(0.98)",
53
+ },
54
+ "&:focus-visible": {
55
+ outline: "var(--border-width-strong) solid var(--foreground)",
56
+ outlineOffset: "2px",
57
+ },
58
+ },
59
+ "@media": {
60
+ "(prefers-reduced-motion: reduce)": {
61
+ transition: "none",
62
+ selectors: {
63
+ "&[data-starting-style]": {
64
+ transform: "translate(-50%, -50%)",
65
+ },
66
+ "&[data-ending-style]": {
67
+ transform: "translate(-50%, -50%)",
68
+ },
69
+ },
70
+ },
71
+ },
72
+ });
73
+
74
+ export const dialog__title = style({
75
+ margin: "0 0 var(--space-1)",
76
+ fontWeight: "var(--weight-semibold)",
77
+ fontSize: "var(--text-lg)",
78
+ lineHeight: 1.4,
79
+ });
80
+
81
+ export const dialog__description = style({
82
+ margin: "0 0 var(--space-5)",
83
+ color: "var(--foreground-muted)",
84
+ fontSize: "var(--text-sm)",
85
+ lineHeight: 1.5,
86
+ });
87
+
88
+ export const dialog__footer = style({
89
+ display: "flex",
90
+ alignItems: "center",
91
+ justifyContent: "flex-end",
92
+ gap: "var(--space-2)",
93
+ paddingTop: "var(--space-4)",
94
+ borderTop: "1px solid var(--border)",
95
+ marginTop: "auto",
96
+ });
97
+
98
+ export const dialog__close = style({
99
+ position: "absolute",
100
+ top: "var(--space-3)",
101
+ right: "var(--space-3)",
102
+ display: "inline-flex",
103
+ alignItems: "center",
104
+ justifyContent: "center",
105
+ width: "2rem",
106
+ height: "2rem",
107
+ border: 0,
108
+ borderRadius: "calc(var(--radius) - 2px)",
109
+ background: "transparent",
110
+ color: "var(--foreground-muted)",
111
+ fontSize: "var(--text-lg)",
112
+ lineHeight: 1,
113
+ cursor: "pointer",
114
+ transition: "background-color var(--duration-fast), color var(--duration-fast)",
115
+ selectors: {
116
+ "&:hover": {
117
+ background: "var(--background-muted)",
118
+ color: "var(--foreground)",
119
+ },
120
+ "&:focus-visible": {
121
+ outline: "var(--border-width-strong) solid var(--foreground)",
122
+ outlineOffset: "2px",
123
+ },
124
+ },
125
+ "@media": {
126
+ "(prefers-reduced-motion: reduce)": {
127
+ transition: "none",
128
+ },
129
+ },
130
+ });
131
+
132
+ /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
133
+ export const byKey: Record<string, string> = {
134
+ "dialog__backdrop": dialog__backdrop,
135
+ "dialog__content": dialog__content,
136
+ "dialog__title": dialog__title,
137
+ "dialog__description": dialog__description,
138
+ "dialog__footer": dialog__footer,
139
+ "dialog__close": dialog__close,
140
+ };
@@ -0,0 +1,255 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Menu as BaseMenu } from "@base-ui/react/menu";
5
+ import { byKey, dm__trigger, dm__positioner, dm__content, dm__item, dmItemText, dmItemCheck, dmItemIndicator, dm__group, dm__label, dm__separator, dmSubArrow, dmSubTrigger } from "./styles.css";
6
+
7
+ import { cn } from "@SH_UI_UTILS@";
8
+ type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
9
+
10
+
11
+ /* ───────── Root ───────── */
12
+
13
+ export const DropdownMenu = BaseMenu.Root;
14
+
15
+ /* ───────── Trigger ───────── */
16
+
17
+ export const DropdownMenuTrigger = React.forwardRef<
18
+ HTMLButtonElement,
19
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.Trigger>>
20
+ >(function DropdownMenuTrigger({ className, ...props }, ref) {
21
+ return (
22
+ <BaseMenu.Trigger
23
+ ref={ref}
24
+ className={cn(dm__trigger, className)}
25
+ {...props}
26
+ />
27
+ );
28
+ });
29
+
30
+ /* ───────── Content ─────────
31
+ * Portal + Positioner + Popup을 한 컴포넌트로 묶는다. Popover와 동일한 관용.
32
+ */
33
+
34
+ export interface DropdownMenuContentProps
35
+ extends WithStringClassName<
36
+ React.ComponentPropsWithoutRef<typeof BaseMenu.Popup>
37
+ > {
38
+ side?: "top" | "right" | "bottom" | "left";
39
+ align?: "start" | "center" | "end";
40
+ /** Trigger-Popup 간격(px). 기본 6. */
41
+ sideOffset?: number;
42
+ container?: React.ComponentPropsWithoutRef<
43
+ typeof BaseMenu.Portal
44
+ >["container"];
45
+ }
46
+
47
+ export const DropdownMenuContent = React.forwardRef<
48
+ HTMLDivElement,
49
+ DropdownMenuContentProps
50
+ >(function DropdownMenuContent(
51
+ { className, children, side, align, sideOffset = 6, container, ...props },
52
+ ref,
53
+ ) {
54
+ return (
55
+ <BaseMenu.Portal container={container}>
56
+ <BaseMenu.Positioner
57
+ className={dm__positioner}
58
+ side={side}
59
+ align={align}
60
+ sideOffset={sideOffset}
61
+ >
62
+ <BaseMenu.Popup
63
+ ref={ref}
64
+ className={cn(dm__content, className)}
65
+ {...props}
66
+ >
67
+ {children}
68
+ </BaseMenu.Popup>
69
+ </BaseMenu.Positioner>
70
+ </BaseMenu.Portal>
71
+ );
72
+ });
73
+
74
+ /* ───────── Item ───────── */
75
+
76
+ export const DropdownMenuItem = React.forwardRef<
77
+ HTMLDivElement,
78
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.Item>>
79
+ >(function DropdownMenuItem({ className, ...props }, ref) {
80
+ return (
81
+ <BaseMenu.Item
82
+ ref={ref}
83
+ className={cn(dm__item, className)}
84
+ {...props}
85
+ />
86
+ );
87
+ });
88
+
89
+ /* ───────── CheckboxItem ───────── */
90
+
91
+ export interface DropdownMenuCheckboxItemProps
92
+ extends WithStringClassName<
93
+ React.ComponentPropsWithoutRef<typeof BaseMenu.CheckboxItem>
94
+ > {}
95
+
96
+ export const DropdownMenuCheckboxItem = React.forwardRef<
97
+ HTMLDivElement,
98
+ DropdownMenuCheckboxItemProps
99
+ >(function DropdownMenuCheckboxItem({ className, children, ...props }, ref) {
100
+ return (
101
+ <BaseMenu.CheckboxItem
102
+ ref={ref}
103
+ className={cn(dm__item, dmItemCheck, className)}
104
+ {...props}
105
+ >
106
+ <span className={dmItemIndicator} aria-hidden>
107
+ <BaseMenu.CheckboxItemIndicator>
108
+ <CheckIcon />
109
+ </BaseMenu.CheckboxItemIndicator>
110
+ </span>
111
+ <span className={dmItemText}>{children}</span>
112
+ </BaseMenu.CheckboxItem>
113
+ );
114
+ });
115
+
116
+ /* ───────── RadioGroup / RadioItem ───────── */
117
+
118
+ export const DropdownMenuRadioGroup = BaseMenu.RadioGroup;
119
+
120
+ export interface DropdownMenuRadioItemProps
121
+ extends WithStringClassName<
122
+ React.ComponentPropsWithoutRef<typeof BaseMenu.RadioItem>
123
+ > {}
124
+
125
+ export const DropdownMenuRadioItem = React.forwardRef<
126
+ HTMLDivElement,
127
+ DropdownMenuRadioItemProps
128
+ >(function DropdownMenuRadioItem({ className, children, ...props }, ref) {
129
+ return (
130
+ <BaseMenu.RadioItem
131
+ ref={ref}
132
+ className={cn(dm__item, dmItemCheck, className)}
133
+ {...props}
134
+ >
135
+ <span className={dmItemIndicator} aria-hidden>
136
+ <BaseMenu.RadioItemIndicator>
137
+ <DotIcon />
138
+ </BaseMenu.RadioItemIndicator>
139
+ </span>
140
+ <span className={dmItemText}>{children}</span>
141
+ </BaseMenu.RadioItem>
142
+ );
143
+ });
144
+
145
+ /* ───────── Group / Label ───────── */
146
+
147
+ export const DropdownMenuGroup = React.forwardRef<
148
+ HTMLDivElement,
149
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.Group>>
150
+ >(function DropdownMenuGroup({ className, ...props }, ref) {
151
+ return (
152
+ <BaseMenu.Group
153
+ ref={ref}
154
+ className={cn(dm__group, className)}
155
+ {...props}
156
+ />
157
+ );
158
+ });
159
+
160
+ export const DropdownMenuLabel = React.forwardRef<
161
+ HTMLDivElement,
162
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenu.GroupLabel>>
163
+ >(function DropdownMenuLabel({ className, ...props }, ref) {
164
+ return (
165
+ <BaseMenu.GroupLabel
166
+ ref={ref}
167
+ className={cn(dm__label, className)}
168
+ {...props}
169
+ />
170
+ );
171
+ });
172
+
173
+ /* ───────── Separator ─────────
174
+ * Base UI Menu에 전용 Separator가 없어 role="separator" div로 대체.
175
+ */
176
+
177
+ export const DropdownMenuSeparator = React.forwardRef<
178
+ HTMLDivElement,
179
+ React.HTMLAttributes<HTMLDivElement>
180
+ >(function DropdownMenuSeparator({ className, ...props }, ref) {
181
+ return (
182
+ <div
183
+ ref={ref}
184
+ role="separator"
185
+ aria-orientation="horizontal"
186
+ className={cn(dm__separator, className)}
187
+ {...props}
188
+ />
189
+ );
190
+ });
191
+
192
+ /* ───────── Submenu ───────── */
193
+
194
+ export const DropdownMenuSub = BaseMenu.SubmenuRoot;
195
+
196
+ export const DropdownMenuSubTrigger = React.forwardRef<
197
+ HTMLDivElement,
198
+ WithStringClassName<
199
+ React.ComponentPropsWithoutRef<typeof BaseMenu.SubmenuTrigger>
200
+ >
201
+ >(function DropdownMenuSubTrigger({ className, children, ...props }, ref) {
202
+ return (
203
+ <BaseMenu.SubmenuTrigger
204
+ ref={ref}
205
+ className={cn(dm__item, dmSubTrigger, className)}
206
+ {...props}
207
+ >
208
+ <span className={dmItemText}>{children}</span>
209
+ <span className={dmSubArrow} aria-hidden>
210
+ <ChevronRightIcon />
211
+ </span>
212
+ </BaseMenu.SubmenuTrigger>
213
+ );
214
+ });
215
+
216
+ /** Submenu 내용물 — 최상위 Content와 동일한 포털 구조. */
217
+ export const DropdownMenuSubContent = DropdownMenuContent;
218
+
219
+ /* ───────── 기본 아이콘 ───────── */
220
+
221
+ function CheckIcon() {
222
+ return (
223
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden>
224
+ <path
225
+ d="M3.5 8.5l3 3 6-7"
226
+ stroke="currentColor"
227
+ strokeWidth="1.75"
228
+ strokeLinecap="round"
229
+ strokeLinejoin="round"
230
+ />
231
+ </svg>
232
+ );
233
+ }
234
+
235
+ function DotIcon() {
236
+ return (
237
+ <svg width="8" height="8" viewBox="0 0 8 8" fill="currentColor" aria-hidden>
238
+ <circle cx="4" cy="4" r="3" />
239
+ </svg>
240
+ );
241
+ }
242
+
243
+ function ChevronRightIcon() {
244
+ return (
245
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden>
246
+ <path
247
+ d="M6 4l4 4-4 4"
248
+ stroke="currentColor"
249
+ strokeWidth="1.5"
250
+ strokeLinecap="round"
251
+ strokeLinejoin="round"
252
+ />
253
+ </svg>
254
+ );
255
+ }
@@ -0,0 +1,175 @@
1
+ import { style, keyframes } from "@vanilla-extract/css";
2
+
3
+ export const shUiDmIn = keyframes({
4
+ "from": {
5
+ opacity: 0,
6
+ transform: "scale(0.96)",
7
+ },
8
+ "to": {
9
+ opacity: 1,
10
+ transform: "scale(1)",
11
+ },
12
+ });
13
+
14
+ export const shUiDmOut = keyframes({
15
+ "from": {
16
+ opacity: 1,
17
+ transform: "scale(1)",
18
+ },
19
+ "to": {
20
+ opacity: 0,
21
+ transform: "scale(0.96)",
22
+ },
23
+ });
24
+
25
+ export const dm__trigger = style({
26
+ font: "inherit",
27
+ cursor: "pointer",
28
+ WebkitTapHighlightColor: "transparent",
29
+ selectors: {
30
+ "&:focus-visible": {
31
+ outline: "var(--border-width-strong) solid var(--foreground)",
32
+ outlineOffset: "2px",
33
+ },
34
+ },
35
+ });
36
+
37
+ export const dm__positioner = style({
38
+ outline: "none",
39
+ zIndex: "var(--z-dropdown)",
40
+ });
41
+
42
+ export const dm__content = style({
43
+ minWidth: "10rem",
44
+ maxHeight: "min(24rem, var(--available-height, 24rem))",
45
+ overflowY: "auto",
46
+ padding: "var(--space-1)",
47
+ background: "var(--background)",
48
+ color: "var(--foreground)",
49
+ border: "1px solid var(--border)",
50
+ borderRadius: "var(--radius)",
51
+ boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.08),\n 0 2px 4px -2px rgba(0, 0, 0, 0.05)",
52
+ fontSize: "var(--text-sm)",
53
+ transformOrigin: "var(--transform-origin)",
54
+ animation: "sh-ui-dm-in 140ms ease-out",
55
+ outline: "none",
56
+ selectors: {
57
+ "&[data-ending-style]": {
58
+ animation: "sh-ui-dm-out 100ms ease-in forwards",
59
+ },
60
+ },
61
+ "@media": {
62
+ "(prefers-reduced-motion: reduce)": {
63
+ animation: "none",
64
+ selectors: {
65
+ "&[data-ending-style]": {
66
+ animation: "none",
67
+ },
68
+ },
69
+ },
70
+ },
71
+ });
72
+
73
+ export const dm__item = style({
74
+ position: "relative",
75
+ display: "flex",
76
+ alignItems: "center",
77
+ gap: "var(--space-2)",
78
+ padding: "0.5rem 0.75rem",
79
+ borderRadius: "calc(var(--radius) - 2px)",
80
+ cursor: "pointer",
81
+ outline: "none",
82
+ userSelect: "none",
83
+ transition: "background-color 80ms",
84
+ selectors: {
85
+ "&[data-highlighted]": {
86
+ background: "var(--background-muted)",
87
+ },
88
+ "&:hover": {
89
+ background: "var(--background-muted)",
90
+ },
91
+ "&[data-disabled]": {
92
+ opacity: "var(--opacity-disabled)",
93
+ pointerEvents: "none",
94
+ },
95
+ },
96
+ "@media": {
97
+ "(prefers-reduced-motion: reduce)": {
98
+ transition: "none",
99
+ },
100
+ },
101
+ });
102
+
103
+ export const dmItemText = style({
104
+ flex: 1,
105
+ minWidth: 0,
106
+ overflow: "hidden",
107
+ textOverflow: "ellipsis",
108
+ whiteSpace: "nowrap",
109
+ });
110
+
111
+ export const dmItemCheck = style({
112
+ paddingLeft: "1.75rem",
113
+ });
114
+
115
+ export const dmItemIndicator = style({
116
+ position: "absolute",
117
+ left: "0.5rem",
118
+ display: "inline-flex",
119
+ alignItems: "center",
120
+ justifyContent: "center",
121
+ width: "1rem",
122
+ height: "1rem",
123
+ color: "var(--foreground)",
124
+ });
125
+
126
+ export const dm__group = style({
127
+ padding: 0,
128
+ });
129
+
130
+ export const dm__label = style({
131
+ padding: "var(--space-2) var(--space-2) var(--space-1)",
132
+ fontSize: "var(--text-xs)",
133
+ fontWeight: "var(--weight-semibold)",
134
+ color: "var(--foreground-muted)",
135
+ textTransform: "uppercase",
136
+ letterSpacing: "0.04em",
137
+ });
138
+
139
+ export const dm__separator = style({
140
+ height: "1px",
141
+ background: "var(--border)",
142
+ margin: "var(--space-1) 0",
143
+ });
144
+
145
+ export const dmSubArrow = style({
146
+ display: "inline-flex",
147
+ alignItems: "center",
148
+ justifyContent: "center",
149
+ marginLeft: "auto",
150
+ color: "var(--foreground-muted)",
151
+ });
152
+
153
+ export const dmSubTrigger = style({
154
+ selectors: {
155
+ "&[data-popup-open]": {
156
+ background: "var(--background-muted)",
157
+ },
158
+ },
159
+ });
160
+
161
+ /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
162
+ export const byKey: Record<string, string> = {
163
+ "dm__trigger": dm__trigger,
164
+ "dm__positioner": dm__positioner,
165
+ "dm__content": dm__content,
166
+ "dm__item": dm__item,
167
+ "dm__item-text": dmItemText,
168
+ "dm__item--check": dmItemCheck,
169
+ "dm__item-indicator": dmItemIndicator,
170
+ "dm__group": dm__group,
171
+ "dm__label": dm__label,
172
+ "dm__separator": dm__separator,
173
+ "dm__sub-arrow": dmSubArrow,
174
+ "dm__sub-trigger": dmSubTrigger,
175
+ };