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.
- package/data/changelog/versions.json +25 -0
- package/data/registry/react/components/accordion/index.module.tsx +97 -0
- package/data/registry/react/components/accordion/styles.module.css +111 -0
- package/data/registry/react/components/avatar/index.module.tsx +73 -0
- package/data/registry/react/components/avatar/styles.module.css +36 -0
- package/data/registry/react/components/badge/index.module.tsx +40 -0
- package/data/registry/react/components/badge/styles.module.css +57 -0
- package/data/registry/react/components/breadcrumb/index.module.tsx +152 -0
- package/data/registry/react/components/breadcrumb/styles.module.css +82 -0
- package/data/registry/react/components/calendar/index.module.tsx +806 -0
- package/data/registry/react/components/calendar/styles.module.css +213 -0
- package/data/registry/react/components/carousel/index.module.tsx +430 -0
- package/data/registry/react/components/carousel/styles.module.css +155 -0
- package/data/registry/react/components/checkbox/index.module.tsx +96 -0
- package/data/registry/react/components/checkbox/styles.module.css +75 -0
- package/data/registry/react/components/code-editor/index.module.tsx +230 -0
- package/data/registry/react/components/code-editor/styles.module.css +76 -0
- package/data/registry/react/components/code-panel/index.module.tsx +191 -0
- package/data/registry/react/components/code-panel/styles.module.css +124 -0
- package/data/registry/react/components/color-picker/index.module.tsx +467 -0
- package/data/registry/react/components/color-picker/styles.module.css +166 -0
- package/data/registry/react/components/combobox/index.module.tsx +165 -0
- package/data/registry/react/components/combobox/styles.module.css +151 -0
- package/data/registry/react/components/context-menu/index.module.tsx +251 -0
- package/data/registry/react/components/context-menu/styles.module.css +140 -0
- package/data/registry/react/components/date-picker/index.module.tsx +520 -0
- package/data/registry/react/components/date-picker/styles.module.css +103 -0
- package/data/registry/react/components/dialog/index.module.tsx +95 -0
- package/data/registry/react/components/dialog/styles.module.css +127 -0
- package/data/registry/react/components/dropdown-menu/index.module.tsx +255 -0
- package/data/registry/react/components/dropdown-menu/styles.module.css +150 -0
- package/data/registry/react/components/file-upload/index.module.tsx +487 -0
- package/data/registry/react/components/file-upload/styles.module.css +170 -0
- package/data/registry/react/components/form/index.module.tsx +61 -0
- package/data/registry/react/components/form/styles.module.css +47 -0
- package/data/registry/react/components/header/index.module.tsx +805 -0
- package/data/registry/react/components/header/styles.module.css +350 -0
- package/data/registry/react/components/label/index.module.tsx +52 -0
- package/data/registry/react/components/label/styles.module.css +90 -0
- package/data/registry/react/components/markdown-editor/index.module.tsx +119 -0
- package/data/registry/react/components/markdown-editor/styles.module.css +160 -0
- package/data/registry/react/components/menubar/index.module.tsx +32 -0
- package/data/registry/react/components/menubar/styles.module.css +45 -0
- package/data/registry/react/components/numeric-input/index.module.tsx +148 -0
- package/data/registry/react/components/numeric-input/styles.module.css +56 -0
- package/data/registry/react/components/page-toc/index.module.tsx +174 -0
- package/data/registry/react/components/page-toc/styles.module.css +82 -0
- package/data/registry/react/components/pagination/index.module.tsx +269 -0
- package/data/registry/react/components/pagination/styles.module.css +105 -0
- package/data/registry/react/components/popover/index.module.tsx +113 -0
- package/data/registry/react/components/popover/styles.module.css +65 -0
- package/data/registry/react/components/progress/index.module.tsx +54 -0
- package/data/registry/react/components/progress/styles.module.css +41 -0
- package/data/registry/react/components/radio/index.module.tsx +65 -0
- package/data/registry/react/components/radio/styles.module.css +80 -0
- package/data/registry/react/components/rich-text-editor/index.module.tsx +348 -0
- package/data/registry/react/components/rich-text-editor/styles.module.css +196 -0
- package/data/registry/react/components/select/index.module.tsx +234 -0
- package/data/registry/react/components/select/styles.module.css +193 -0
- package/data/registry/react/components/separator/index.module.tsx +46 -0
- package/data/registry/react/components/separator/styles.module.css +15 -0
- package/data/registry/react/components/sidebar/index.module.tsx +1067 -0
- package/data/registry/react/components/sidebar/styles.module.css +502 -0
- package/data/registry/react/components/skeleton/index.module.tsx +22 -0
- package/data/registry/react/components/skeleton/styles.module.css +24 -0
- package/data/registry/react/components/slider/index.module.tsx +298 -0
- package/data/registry/react/components/slider/styles.module.css +64 -0
- package/data/registry/react/components/spinner/index.module.tsx +38 -0
- package/data/registry/react/components/spinner/styles.module.css +37 -0
- package/data/registry/react/components/switch/index.module.tsx +39 -0
- package/data/registry/react/components/switch/styles.module.css +83 -0
- package/data/registry/react/components/tabs/index.module.tsx +91 -0
- package/data/registry/react/components/tabs/styles.module.css +148 -0
- package/data/registry/react/components/textarea/index.module.tsx +23 -0
- package/data/registry/react/components/textarea/styles.module.css +54 -0
- package/data/registry/react/components/toast/index.module.tsx +258 -0
- package/data/registry/react/components/toast/styles.module.css +290 -0
- package/data/registry/react/components/toggle/index.module.tsx +131 -0
- package/data/registry/react/components/toggle/styles.module.css +85 -0
- package/data/registry/react/components/tooltip/index.module.tsx +83 -0
- package/data/registry/react/components/tooltip/styles.module.css +44 -0
- package/data/registry/react/registry.json +560 -0
- package/package.json +1 -1
- package/src/api.d.ts +4 -3
- 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 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
|
+
/**
|
|
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(styles.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(styles.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={styles.dialog__backdrop} />
|
|
57
|
+
<BaseDialog.Popup
|
|
58
|
+
ref={ref}
|
|
59
|
+
className={cn(styles.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(styles.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(styles.dialog__description, className)}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/* ── Backdrop ── */
|
|
2
|
+
|
|
3
|
+
.dialog__backdrop {
|
|
4
|
+
position: fixed;
|
|
5
|
+
inset: 0;
|
|
6
|
+
z-index: var(--z-overlay);
|
|
7
|
+
background: rgba(0, 0, 0, 0.25);
|
|
8
|
+
backdrop-filter: blur(8px);
|
|
9
|
+
transition: opacity var(--duration-slow) ease;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.dialog__backdrop[data-starting-style],
|
|
13
|
+
.dialog__backdrop[data-ending-style] {
|
|
14
|
+
opacity: 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* ── Content (Popup) ── */
|
|
18
|
+
|
|
19
|
+
.dialog__content {
|
|
20
|
+
position: fixed;
|
|
21
|
+
top: 50%;
|
|
22
|
+
left: 50%;
|
|
23
|
+
transform: translate(-50%, -50%);
|
|
24
|
+
z-index: var(--z-modal);
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
width: calc(100% - 2rem);
|
|
28
|
+
max-width: 28rem;
|
|
29
|
+
max-height: calc(100dvh - 4rem);
|
|
30
|
+
padding: var(--space-6);
|
|
31
|
+
background: var(--background);
|
|
32
|
+
color: var(--foreground);
|
|
33
|
+
border: 1px solid var(--border);
|
|
34
|
+
border-radius: var(--radius);
|
|
35
|
+
box-shadow: var(--shadow-xl);
|
|
36
|
+
outline: none;
|
|
37
|
+
overflow-y: auto;
|
|
38
|
+
transition:
|
|
39
|
+
opacity var(--duration-slow) ease,
|
|
40
|
+
transform var(--duration-slow) ease;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.dialog__content[data-starting-style] {
|
|
44
|
+
opacity: 0;
|
|
45
|
+
transform: translate(-50%, calc(-50% + 0.5rem)) scale(0.97);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.dialog__content[data-ending-style] {
|
|
49
|
+
opacity: 0;
|
|
50
|
+
transform: translate(-50%, calc(-50% + 0.25rem)) scale(0.98);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.dialog__content:focus-visible {
|
|
54
|
+
outline: var(--border-width-strong) solid var(--foreground);
|
|
55
|
+
outline-offset: 2px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* ── Title ── */
|
|
59
|
+
|
|
60
|
+
.dialog__title {
|
|
61
|
+
margin: 0 0 var(--space-1);
|
|
62
|
+
font-weight: var(--weight-semibold);
|
|
63
|
+
font-size: var(--text-lg);
|
|
64
|
+
line-height: 1.4;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ── Description ── */
|
|
68
|
+
|
|
69
|
+
.dialog__description {
|
|
70
|
+
margin: 0 0 var(--space-5);
|
|
71
|
+
color: var(--foreground-muted);
|
|
72
|
+
font-size: var(--text-sm);
|
|
73
|
+
line-height: 1.5;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ── Footer (액션 버튼 영역) ── */
|
|
77
|
+
|
|
78
|
+
.dialog__footer {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
justify-content: flex-end;
|
|
82
|
+
gap: var(--space-2);
|
|
83
|
+
padding-top: var(--space-4);
|
|
84
|
+
border-top: 1px solid var(--border);
|
|
85
|
+
margin-top: auto;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* ── Close (× 버튼) ── */
|
|
89
|
+
|
|
90
|
+
.dialog__close {
|
|
91
|
+
position: absolute;
|
|
92
|
+
top: var(--space-3);
|
|
93
|
+
right: var(--space-3);
|
|
94
|
+
display: inline-flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
justify-content: center;
|
|
97
|
+
width: 2rem;
|
|
98
|
+
height: 2rem;
|
|
99
|
+
border: 0;
|
|
100
|
+
border-radius: calc(var(--radius) - 2px);
|
|
101
|
+
background: transparent;
|
|
102
|
+
color: var(--foreground-muted);
|
|
103
|
+
font-size: var(--text-lg);
|
|
104
|
+
line-height: 1;
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
transition: background-color var(--duration-fast), color var(--duration-fast);
|
|
107
|
+
}
|
|
108
|
+
.dialog__close:hover {
|
|
109
|
+
background: var(--background-muted);
|
|
110
|
+
color: var(--foreground);
|
|
111
|
+
}
|
|
112
|
+
.dialog__close:focus-visible {
|
|
113
|
+
outline: var(--border-width-strong) solid var(--foreground);
|
|
114
|
+
outline-offset: 2px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@media (prefers-reduced-motion: reduce) {
|
|
118
|
+
.dialog__backdrop,
|
|
119
|
+
.dialog__content,
|
|
120
|
+
.dialog__close {
|
|
121
|
+
transition: none;
|
|
122
|
+
}
|
|
123
|
+
.dialog__content[data-starting-style],
|
|
124
|
+
.dialog__content[data-ending-style] {
|
|
125
|
+
transform: translate(-50%, -50%);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -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 styles from "./styles.module.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(styles.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={styles.dm__positioner}
|
|
58
|
+
side={side}
|
|
59
|
+
align={align}
|
|
60
|
+
sideOffset={sideOffset}
|
|
61
|
+
>
|
|
62
|
+
<BaseMenu.Popup
|
|
63
|
+
ref={ref}
|
|
64
|
+
className={cn(styles.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(styles.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(styles.dm__item, styles["dm__item--check"], className)}
|
|
104
|
+
{...props}
|
|
105
|
+
>
|
|
106
|
+
<span className={styles["dm__item-indicator"]} aria-hidden>
|
|
107
|
+
<BaseMenu.CheckboxItemIndicator>
|
|
108
|
+
<CheckIcon />
|
|
109
|
+
</BaseMenu.CheckboxItemIndicator>
|
|
110
|
+
</span>
|
|
111
|
+
<span className={styles["dm__item-text"]}>{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(styles.dm__item, styles["dm__item--check"], className)}
|
|
133
|
+
{...props}
|
|
134
|
+
>
|
|
135
|
+
<span className={styles["dm__item-indicator"]} aria-hidden>
|
|
136
|
+
<BaseMenu.RadioItemIndicator>
|
|
137
|
+
<DotIcon />
|
|
138
|
+
</BaseMenu.RadioItemIndicator>
|
|
139
|
+
</span>
|
|
140
|
+
<span className={styles["dm__item-text"]}>{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(styles.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(styles.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(styles.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(styles.dm__item, styles["dm__sub-trigger"], className)}
|
|
206
|
+
{...props}
|
|
207
|
+
>
|
|
208
|
+
<span className={styles["dm__item-text"]}>{children}</span>
|
|
209
|
+
<span className={styles["dm__sub-arrow"]} 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,150 @@
|
|
|
1
|
+
/* ───────── Trigger ─────────
|
|
2
|
+
* 기본 스타일을 최소화하여 사용자가 Button/아이콘 버튼 등 어떤 트리거든
|
|
3
|
+
* 덮어쓸 수 있게 한다. 포커스 링만 담당.
|
|
4
|
+
*/
|
|
5
|
+
.dm__trigger {
|
|
6
|
+
font: inherit;
|
|
7
|
+
cursor: pointer;
|
|
8
|
+
-webkit-tap-highlight-color: transparent;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.dm__trigger:focus-visible {
|
|
12
|
+
outline: var(--border-width-strong) solid var(--foreground);
|
|
13
|
+
outline-offset: 2px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* ───────── Positioner / Content ───────── */
|
|
17
|
+
|
|
18
|
+
.dm__positioner {
|
|
19
|
+
outline: none;
|
|
20
|
+
z-index: var(--z-dropdown);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.dm__content {
|
|
24
|
+
min-width: 10rem;
|
|
25
|
+
max-height: min(24rem, var(--available-height, 24rem));
|
|
26
|
+
overflow-y: auto;
|
|
27
|
+
padding: var(--space-1);
|
|
28
|
+
background: var(--background);
|
|
29
|
+
color: var(--foreground);
|
|
30
|
+
border: 1px solid var(--border);
|
|
31
|
+
border-radius: var(--radius);
|
|
32
|
+
box-shadow:
|
|
33
|
+
0 4px 6px -1px rgba(0, 0, 0, 0.08),
|
|
34
|
+
0 2px 4px -2px rgba(0, 0, 0, 0.05);
|
|
35
|
+
font-size: var(--text-sm);
|
|
36
|
+
transform-origin: var(--transform-origin);
|
|
37
|
+
animation: sh-ui-dm-in 140ms ease-out;
|
|
38
|
+
outline: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.dm__content[data-ending-style] {
|
|
42
|
+
animation: sh-ui-dm-out 100ms ease-in forwards;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@keyframes sh-ui-dm-in {
|
|
46
|
+
from { opacity: 0; transform: scale(0.96); }
|
|
47
|
+
to { opacity: 1; transform: scale(1); }
|
|
48
|
+
}
|
|
49
|
+
@keyframes sh-ui-dm-out {
|
|
50
|
+
from { opacity: 1; transform: scale(1); }
|
|
51
|
+
to { opacity: 0; transform: scale(0.96); }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* ───────── Item ───────── */
|
|
55
|
+
|
|
56
|
+
.dm__item {
|
|
57
|
+
position: relative;
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: var(--space-2);
|
|
61
|
+
padding: 0.5rem 0.75rem;
|
|
62
|
+
border-radius: calc(var(--radius) - 2px);
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
outline: none;
|
|
65
|
+
user-select: none;
|
|
66
|
+
transition: background-color 80ms;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.dm__item[data-highlighted],
|
|
70
|
+
.dm__item:hover {
|
|
71
|
+
background: var(--background-muted);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.dm__item[data-disabled] {
|
|
75
|
+
opacity: var(--opacity-disabled);
|
|
76
|
+
pointer-events: none;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.dm__item-text {
|
|
80
|
+
flex: 1;
|
|
81
|
+
min-width: 0;
|
|
82
|
+
overflow: hidden;
|
|
83
|
+
text-overflow: ellipsis;
|
|
84
|
+
white-space: nowrap;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* checkbox/radio item — 왼쪽 인디케이터 공간 확보 */
|
|
88
|
+
.dm__item--check {
|
|
89
|
+
padding-left: 1.75rem;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.dm__item-indicator {
|
|
93
|
+
position: absolute;
|
|
94
|
+
left: 0.5rem;
|
|
95
|
+
display: inline-flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: center;
|
|
98
|
+
width: 1rem;
|
|
99
|
+
height: 1rem;
|
|
100
|
+
color: var(--foreground);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ───────── Group / Label ───────── */
|
|
104
|
+
|
|
105
|
+
.dm__group {
|
|
106
|
+
padding: 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.dm__label {
|
|
110
|
+
padding: var(--space-2) var(--space-2) var(--space-1);
|
|
111
|
+
font-size: var(--text-xs);
|
|
112
|
+
font-weight: var(--weight-semibold);
|
|
113
|
+
color: var(--foreground-muted);
|
|
114
|
+
text-transform: uppercase;
|
|
115
|
+
letter-spacing: 0.04em;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* ───────── Separator ───────── */
|
|
119
|
+
|
|
120
|
+
.dm__separator {
|
|
121
|
+
height: 1px;
|
|
122
|
+
background: var(--border);
|
|
123
|
+
margin: var(--space-1) 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* ───────── Submenu trigger ───────── */
|
|
127
|
+
|
|
128
|
+
.dm__sub-arrow {
|
|
129
|
+
display: inline-flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
justify-content: center;
|
|
132
|
+
margin-left: auto;
|
|
133
|
+
color: var(--foreground-muted);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.dm__sub-trigger[data-popup-open] {
|
|
137
|
+
background: var(--background-muted);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ───────── Reduced motion ───────── */
|
|
141
|
+
|
|
142
|
+
@media (prefers-reduced-motion: reduce) {
|
|
143
|
+
.dm__content,
|
|
144
|
+
.dm__content[data-ending-style] {
|
|
145
|
+
animation: none;
|
|
146
|
+
}
|
|
147
|
+
.dm__item {
|
|
148
|
+
transition: none;
|
|
149
|
+
}
|
|
150
|
+
}
|