sh-ui-cli 0.113.0 → 0.115.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 +27 -0
- package/data/registry/react/components/form/field.test.tsx +106 -1
- package/data/registry/react/components/form/field.tsx +179 -23
- package/data/registry/react/components/form/use-sh-ui-form.ts +14 -0
- package/data/registry/react/components/form-rhf/README.md +138 -8
- package/data/registry/react/components/form-rhf/index.tsx +75 -0
- package/data/registry/react/components/form-rhf/rhf.test.tsx +53 -1
- package/data/registry/react/components/label/index.tailwind.tsx +5 -1
- package/data/registry/react/components/label/styles.css +9 -5
- package/data/registry/react/components/label/styles.module.css +7 -5
- package/data/registry/react/components/rich-text-editor/index.module.tsx +523 -171
- package/data/registry/react/components/rich-text-editor/index.tailwind.tsx +596 -70
- package/data/registry/react/components/rich-text-editor/index.tsx +523 -171
- package/data/registry/react/components/rich-text-editor/styles.css +103 -5
- package/data/registry/react/components/rich-text-editor/styles.module.css +103 -5
- package/data/registry/react/components/sidebar/index.module.tsx +57 -0
- package/data/registry/react/components/sidebar/index.tailwind.tsx +68 -1
- package/data/registry/react/components/sidebar/index.tsx +57 -0
- package/data/registry/react/components/sidebar/styles.css +77 -0
- package/data/registry/react/components/sidebar/styles.module.css +77 -0
- package/data/registry/react/registry.json +319 -963
- package/data/registry/react/tokens-used.json +4 -1
- package/package.json +1 -1
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/* 본문 텍스트 색 팔레트 — 라이트/다크 추종 (RTE Color 확장이 var() 로 저장) */
|
|
2
|
+
:root {
|
|
3
|
+
--sh-ui-rte-c-moss: #2f5d4f;
|
|
4
|
+
--sh-ui-rte-c-red: #c0392b;
|
|
5
|
+
--sh-ui-rte-c-orange: #c2410c;
|
|
6
|
+
--sh-ui-rte-c-blue: #1d4ed8;
|
|
7
|
+
}
|
|
8
|
+
.dark {
|
|
9
|
+
--sh-ui-rte-c-moss: #5ba886;
|
|
10
|
+
--sh-ui-rte-c-red: #f1736a;
|
|
11
|
+
--sh-ui-rte-c-orange: #f0935a;
|
|
12
|
+
--sh-ui-rte-c-blue: #6ba2f2;
|
|
13
|
+
}
|
|
14
|
+
@media (prefers-color-scheme: dark) {
|
|
15
|
+
:root:not(.light):not(.dark) {
|
|
16
|
+
--sh-ui-rte-c-moss: #5ba886;
|
|
17
|
+
--sh-ui-rte-c-red: #f1736a;
|
|
18
|
+
--sh-ui-rte-c-orange: #f0935a;
|
|
19
|
+
--sh-ui-rte-c-blue: #6ba2f2;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
1
23
|
.sh-ui-rte {
|
|
2
24
|
display: flex;
|
|
3
25
|
flex-direction: column;
|
|
@@ -18,13 +40,15 @@
|
|
|
18
40
|
|
|
19
41
|
/* ─── Toolbar ─── */
|
|
20
42
|
.sh-ui-rte__toolbar {
|
|
43
|
+
background: var(--background-muted);
|
|
44
|
+
border-bottom: 1px solid var(--border);
|
|
45
|
+
}
|
|
46
|
+
.sh-ui-rte__toolbar-row {
|
|
21
47
|
display: flex;
|
|
22
48
|
flex-wrap: wrap;
|
|
23
49
|
align-items: center;
|
|
24
50
|
gap: 0.125rem;
|
|
25
51
|
padding: var(--space-1) var(--space-2);
|
|
26
|
-
background: var(--background-muted);
|
|
27
|
-
border-bottom: 1px solid var(--border);
|
|
28
52
|
}
|
|
29
53
|
.sh-ui-rte__btn {
|
|
30
54
|
display: inline-flex;
|
|
@@ -70,6 +94,64 @@
|
|
|
70
94
|
background: var(--border);
|
|
71
95
|
}
|
|
72
96
|
|
|
97
|
+
/* ─── Secondary panel (color · link) ─── */
|
|
98
|
+
.sh-ui-rte__panel {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
gap: var(--space-1);
|
|
102
|
+
padding: var(--space-1) var(--space-2);
|
|
103
|
+
border-top: 1px solid var(--border);
|
|
104
|
+
}
|
|
105
|
+
.sh-ui-rte__panel--swatches {
|
|
106
|
+
flex-wrap: wrap;
|
|
107
|
+
}
|
|
108
|
+
.sh-ui-rte__swatch {
|
|
109
|
+
display: inline-flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
justify-content: center;
|
|
112
|
+
width: 1.5rem;
|
|
113
|
+
height: 1.5rem;
|
|
114
|
+
border-radius: 9999px;
|
|
115
|
+
border: 1px solid var(--border);
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
transition: border-color var(--duration-fast);
|
|
118
|
+
-webkit-tap-highlight-color: transparent;
|
|
119
|
+
}
|
|
120
|
+
.sh-ui-rte__swatch:hover {
|
|
121
|
+
border-color: var(--border-strong);
|
|
122
|
+
}
|
|
123
|
+
.sh-ui-rte__swatch:focus-visible {
|
|
124
|
+
outline: var(--border-width-strong) solid var(--ring);
|
|
125
|
+
outline-offset: 1px;
|
|
126
|
+
}
|
|
127
|
+
.sh-ui-rte__swatch.is-active {
|
|
128
|
+
border-color: var(--foreground);
|
|
129
|
+
}
|
|
130
|
+
.sh-ui-rte__swatch-a {
|
|
131
|
+
font-size: 10px;
|
|
132
|
+
font-weight: 600;
|
|
133
|
+
color: var(--foreground-muted);
|
|
134
|
+
}
|
|
135
|
+
.sh-ui-rte__link-input {
|
|
136
|
+
flex: 1;
|
|
137
|
+
min-width: 0;
|
|
138
|
+
height: 1.75rem;
|
|
139
|
+
padding: 0 var(--space-2);
|
|
140
|
+
font-size: 0.875rem;
|
|
141
|
+
background: var(--background);
|
|
142
|
+
color: var(--foreground);
|
|
143
|
+
border: 1px solid var(--border);
|
|
144
|
+
border-radius: calc(var(--radius) - 2px);
|
|
145
|
+
outline: none;
|
|
146
|
+
}
|
|
147
|
+
.sh-ui-rte__link-input:focus {
|
|
148
|
+
border-color: var(--foreground);
|
|
149
|
+
}
|
|
150
|
+
.sh-ui-rte__link-input:focus-visible {
|
|
151
|
+
outline: var(--border-width-strong) solid var(--ring);
|
|
152
|
+
outline-offset: 1px;
|
|
153
|
+
}
|
|
154
|
+
|
|
73
155
|
/* ─── Content ─── */
|
|
74
156
|
.sh-ui-rte__viewport {
|
|
75
157
|
display: flex;
|
|
@@ -108,15 +190,27 @@
|
|
|
108
190
|
font-weight: 600;
|
|
109
191
|
line-height: 1.3;
|
|
110
192
|
}
|
|
111
|
-
.sh-ui-rte__content h1 {
|
|
112
|
-
|
|
113
|
-
|
|
193
|
+
.sh-ui-rte__content h1 {
|
|
194
|
+
font-size: 1.5rem;
|
|
195
|
+
}
|
|
196
|
+
.sh-ui-rte__content h2 {
|
|
197
|
+
font-size: 1.25rem;
|
|
198
|
+
}
|
|
199
|
+
.sh-ui-rte__content h3 {
|
|
200
|
+
font-size: 1.125rem;
|
|
201
|
+
}
|
|
114
202
|
|
|
115
203
|
.sh-ui-rte__content ul,
|
|
116
204
|
.sh-ui-rte__content ol {
|
|
117
205
|
margin: 0 0 var(--space-3);
|
|
118
206
|
padding-inline-start: var(--space-5);
|
|
119
207
|
}
|
|
208
|
+
.sh-ui-rte__content ul {
|
|
209
|
+
list-style: disc;
|
|
210
|
+
}
|
|
211
|
+
.sh-ui-rte__content ol {
|
|
212
|
+
list-style: decimal;
|
|
213
|
+
}
|
|
120
214
|
.sh-ui-rte__content li {
|
|
121
215
|
margin-bottom: var(--space-1);
|
|
122
216
|
}
|
|
@@ -174,6 +268,10 @@
|
|
|
174
268
|
.sh-ui-rte__content a:hover {
|
|
175
269
|
text-decoration-thickness: 2px;
|
|
176
270
|
}
|
|
271
|
+
.sh-ui-rte__content u {
|
|
272
|
+
text-decoration: underline;
|
|
273
|
+
text-underline-offset: 2px;
|
|
274
|
+
}
|
|
177
275
|
|
|
178
276
|
/* Placeholder (Tiptap extension) */
|
|
179
277
|
.sh-ui-rte__content p.is-editor-empty:first-child::before,
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/* 본문 텍스트 색 팔레트 — 라이트/다크 추종 (RTE Color 확장이 var() 로 저장) */
|
|
2
|
+
:root {
|
|
3
|
+
--sh-ui-rte-c-moss: #2f5d4f;
|
|
4
|
+
--sh-ui-rte-c-red: #c0392b;
|
|
5
|
+
--sh-ui-rte-c-orange: #c2410c;
|
|
6
|
+
--sh-ui-rte-c-blue: #1d4ed8;
|
|
7
|
+
}
|
|
8
|
+
.dark {
|
|
9
|
+
--sh-ui-rte-c-moss: #5ba886;
|
|
10
|
+
--sh-ui-rte-c-red: #f1736a;
|
|
11
|
+
--sh-ui-rte-c-orange: #f0935a;
|
|
12
|
+
--sh-ui-rte-c-blue: #6ba2f2;
|
|
13
|
+
}
|
|
14
|
+
@media (prefers-color-scheme: dark) {
|
|
15
|
+
:root:not(.light):not(.dark) {
|
|
16
|
+
--sh-ui-rte-c-moss: #5ba886;
|
|
17
|
+
--sh-ui-rte-c-red: #f1736a;
|
|
18
|
+
--sh-ui-rte-c-orange: #f0935a;
|
|
19
|
+
--sh-ui-rte-c-blue: #6ba2f2;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
1
23
|
.rte {
|
|
2
24
|
display: flex;
|
|
3
25
|
flex-direction: column;
|
|
@@ -18,13 +40,15 @@
|
|
|
18
40
|
|
|
19
41
|
/* ─── Toolbar ─── */
|
|
20
42
|
.rte__toolbar {
|
|
43
|
+
background: var(--background-muted);
|
|
44
|
+
border-bottom: 1px solid var(--border);
|
|
45
|
+
}
|
|
46
|
+
.rte__toolbar-row {
|
|
21
47
|
display: flex;
|
|
22
48
|
flex-wrap: wrap;
|
|
23
49
|
align-items: center;
|
|
24
50
|
gap: 0.125rem;
|
|
25
51
|
padding: var(--space-1) var(--space-2);
|
|
26
|
-
background: var(--background-muted);
|
|
27
|
-
border-bottom: 1px solid var(--border);
|
|
28
52
|
}
|
|
29
53
|
.rte__btn {
|
|
30
54
|
display: inline-flex;
|
|
@@ -70,6 +94,64 @@
|
|
|
70
94
|
background: var(--border);
|
|
71
95
|
}
|
|
72
96
|
|
|
97
|
+
/* ─── Secondary panel (color · link) ─── */
|
|
98
|
+
.rte__panel {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
gap: var(--space-1);
|
|
102
|
+
padding: var(--space-1) var(--space-2);
|
|
103
|
+
border-top: 1px solid var(--border);
|
|
104
|
+
}
|
|
105
|
+
.rte__panel--swatches {
|
|
106
|
+
flex-wrap: wrap;
|
|
107
|
+
}
|
|
108
|
+
.rte__swatch {
|
|
109
|
+
display: inline-flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
justify-content: center;
|
|
112
|
+
width: 1.5rem;
|
|
113
|
+
height: 1.5rem;
|
|
114
|
+
border-radius: 9999px;
|
|
115
|
+
border: 1px solid var(--border);
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
transition: border-color var(--duration-fast);
|
|
118
|
+
-webkit-tap-highlight-color: transparent;
|
|
119
|
+
}
|
|
120
|
+
.rte__swatch:hover {
|
|
121
|
+
border-color: var(--border-strong);
|
|
122
|
+
}
|
|
123
|
+
.rte__swatch:focus-visible {
|
|
124
|
+
outline: var(--border-width-strong) solid var(--ring);
|
|
125
|
+
outline-offset: 1px;
|
|
126
|
+
}
|
|
127
|
+
.rte__swatch.is-active {
|
|
128
|
+
border-color: var(--foreground);
|
|
129
|
+
}
|
|
130
|
+
.rte__swatch-a {
|
|
131
|
+
font-size: 10px;
|
|
132
|
+
font-weight: 600;
|
|
133
|
+
color: var(--foreground-muted);
|
|
134
|
+
}
|
|
135
|
+
.rte__link-input {
|
|
136
|
+
flex: 1;
|
|
137
|
+
min-width: 0;
|
|
138
|
+
height: 1.75rem;
|
|
139
|
+
padding: 0 var(--space-2);
|
|
140
|
+
font-size: 0.875rem;
|
|
141
|
+
background: var(--background);
|
|
142
|
+
color: var(--foreground);
|
|
143
|
+
border: 1px solid var(--border);
|
|
144
|
+
border-radius: calc(var(--radius) - 2px);
|
|
145
|
+
outline: none;
|
|
146
|
+
}
|
|
147
|
+
.rte__link-input:focus {
|
|
148
|
+
border-color: var(--foreground);
|
|
149
|
+
}
|
|
150
|
+
.rte__link-input:focus-visible {
|
|
151
|
+
outline: var(--border-width-strong) solid var(--ring);
|
|
152
|
+
outline-offset: 1px;
|
|
153
|
+
}
|
|
154
|
+
|
|
73
155
|
/* ─── Content ─── */
|
|
74
156
|
.rte__viewport {
|
|
75
157
|
display: flex;
|
|
@@ -108,15 +190,27 @@
|
|
|
108
190
|
font-weight: 600;
|
|
109
191
|
line-height: 1.3;
|
|
110
192
|
}
|
|
111
|
-
.rte__content h1 {
|
|
112
|
-
|
|
113
|
-
|
|
193
|
+
.rte__content h1 {
|
|
194
|
+
font-size: 1.5rem;
|
|
195
|
+
}
|
|
196
|
+
.rte__content h2 {
|
|
197
|
+
font-size: 1.25rem;
|
|
198
|
+
}
|
|
199
|
+
.rte__content h3 {
|
|
200
|
+
font-size: 1.125rem;
|
|
201
|
+
}
|
|
114
202
|
|
|
115
203
|
.rte__content ul,
|
|
116
204
|
.rte__content ol {
|
|
117
205
|
margin: 0 0 var(--space-3);
|
|
118
206
|
padding-inline-start: var(--space-5);
|
|
119
207
|
}
|
|
208
|
+
.rte__content ul {
|
|
209
|
+
list-style: disc;
|
|
210
|
+
}
|
|
211
|
+
.rte__content ol {
|
|
212
|
+
list-style: decimal;
|
|
213
|
+
}
|
|
120
214
|
.rte__content li {
|
|
121
215
|
margin-bottom: var(--space-1);
|
|
122
216
|
}
|
|
@@ -174,6 +268,10 @@
|
|
|
174
268
|
.rte__content a:hover {
|
|
175
269
|
text-decoration-thickness: 2px;
|
|
176
270
|
}
|
|
271
|
+
.rte__content u {
|
|
272
|
+
text-decoration: underline;
|
|
273
|
+
text-underline-offset: 2px;
|
|
274
|
+
}
|
|
177
275
|
|
|
178
276
|
/* Placeholder (Tiptap extension) */
|
|
179
277
|
.rte__content p.is-editor-empty:first-child::before,
|
|
@@ -743,6 +743,63 @@ export const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenu
|
|
|
743
743
|
}
|
|
744
744
|
);
|
|
745
745
|
|
|
746
|
+
/* ───────────── Menu trailing slots (badge / action) ─────────────
|
|
747
|
+
* SidebarMenuButton 의 `> span { flex: 1 }` 흐름 밖(absolute)에 위치해 라벨을
|
|
748
|
+
* 밀어내지 않는다. SidebarMenuItem 이 trailing 슬롯 존재를 :has() 로 감지해
|
|
749
|
+
* 내부 button/anchor 에 우측 패딩을 확보한다 (styles.module.css 참고). */
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* SidebarMenuItem 의 우측 trailing 배지 (안 읽음 수 등). SidebarMenuButton 의
|
|
753
|
+
* 형제로 둔다 — absolute 라 라벨을 밀어내지 않고, pointer-events:none 으로 클릭은
|
|
754
|
+
* 행 전체 button 으로 통과한다.
|
|
755
|
+
*
|
|
756
|
+
* <SidebarMenuItem>
|
|
757
|
+
* <SidebarMenuButton render={<Link href='/x'>채팅</Link>} />
|
|
758
|
+
* <SidebarMenuBadge>8</SidebarMenuBadge>
|
|
759
|
+
* </SidebarMenuItem>
|
|
760
|
+
*/
|
|
761
|
+
export function SidebarMenuBadge({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
762
|
+
return (
|
|
763
|
+
<div
|
|
764
|
+
data-sidebar="menu-badge"
|
|
765
|
+
className={cn(styles["sidebar__menu-badge"], className)}
|
|
766
|
+
{...props}
|
|
767
|
+
/>
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
export interface SidebarMenuActionProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
772
|
+
/** 행 hover/포커스 시에만 노출. 기본은 항상 표시. */
|
|
773
|
+
showOnHover?: boolean;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* SidebarMenuItem 의 우측 trailing 액션 버튼 (… 메뉴, 추가 등). badge 와 달리
|
|
778
|
+
* 클릭 가능. `showOnHover` 면 행 hover/포커스 시에만 노출 (CSS 부모 hover 셀렉터).
|
|
779
|
+
*
|
|
780
|
+
* <SidebarMenuItem>
|
|
781
|
+
* <SidebarMenuButton render={<Link href='/x'>채널</Link>} />
|
|
782
|
+
* <SidebarMenuAction showOnHover aria-label='채널 설정'><MoreIcon /></SidebarMenuAction>
|
|
783
|
+
* </SidebarMenuItem>
|
|
784
|
+
*/
|
|
785
|
+
export const SidebarMenuAction = React.forwardRef<HTMLButtonElement, SidebarMenuActionProps>(
|
|
786
|
+
function SidebarMenuAction({ className, showOnHover, ...props }, ref) {
|
|
787
|
+
return (
|
|
788
|
+
<button
|
|
789
|
+
ref={ref}
|
|
790
|
+
type="button"
|
|
791
|
+
data-sidebar="menu-action"
|
|
792
|
+
className={cn(
|
|
793
|
+
styles["sidebar__menu-action"],
|
|
794
|
+
showOnHover && styles["sidebar__menu-action--hover"],
|
|
795
|
+
className,
|
|
796
|
+
)}
|
|
797
|
+
{...props}
|
|
798
|
+
/>
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
);
|
|
802
|
+
|
|
746
803
|
/* ───────────── Sub menu ───────────── */
|
|
747
804
|
|
|
748
805
|
/** 메뉴 항목 내부의 서브 메뉴 리스트. SidebarMenuItem 안에 둔다. */
|
|
@@ -445,7 +445,18 @@ export function SidebarMenu({ className, ...props }: React.HTMLAttributes<HTMLUL
|
|
|
445
445
|
}
|
|
446
446
|
|
|
447
447
|
export function SidebarMenuItem({ className, ...props }: React.HTMLAttributes<HTMLLIElement>) {
|
|
448
|
-
return
|
|
448
|
+
return (
|
|
449
|
+
<li
|
|
450
|
+
className={cn(
|
|
451
|
+
// group/menu-item — SidebarMenuAction 의 showOnHover hover 타겟.
|
|
452
|
+
// trailing slot(badge/action)이 있으면 내부 button/anchor 에 우측 패딩을
|
|
453
|
+
// 확보해 라벨이 trailing 요소 밑으로 들어가지 않게 한다.
|
|
454
|
+
"group/menu-item relative m-0 has-[[data-sidebar=menu-badge]]:[&>a]:pr-8 has-[[data-sidebar=menu-badge]]:[&>button]:pr-8 has-[[data-sidebar=menu-action]]:[&>a]:pr-8 has-[[data-sidebar=menu-action]]:[&>button]:pr-8",
|
|
455
|
+
className,
|
|
456
|
+
)}
|
|
457
|
+
{...props}
|
|
458
|
+
/>
|
|
459
|
+
);
|
|
449
460
|
}
|
|
450
461
|
|
|
451
462
|
export interface SidebarMenuButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
@@ -509,6 +520,62 @@ export const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenu
|
|
|
509
520
|
}
|
|
510
521
|
);
|
|
511
522
|
|
|
523
|
+
/**
|
|
524
|
+
* SidebarMenuItem 의 우측 trailing 배지 (안 읽음 수 등). SidebarMenuButton 의
|
|
525
|
+
* 형제로 둔다 — button 의 `[&>span]:flex-1` 흐름 밖(absolute)이라 라벨을 밀어내지
|
|
526
|
+
* 않는다. 클릭 통과(pointer-events-none) — 행 전체 클릭이 button 으로 간다.
|
|
527
|
+
*
|
|
528
|
+
* <SidebarMenuItem>
|
|
529
|
+
* <SidebarMenuButton render={<Link href='/x'>채팅</Link>} />
|
|
530
|
+
* <SidebarMenuBadge>8</SidebarMenuBadge>
|
|
531
|
+
* </SidebarMenuItem>
|
|
532
|
+
*/
|
|
533
|
+
export function SidebarMenuBadge({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
534
|
+
return (
|
|
535
|
+
<div
|
|
536
|
+
data-sidebar="menu-badge"
|
|
537
|
+
className={cn(
|
|
538
|
+
"pointer-events-none absolute right-[var(--space-2)] top-1/2 flex h-5 min-w-5 -translate-y-1/2 select-none items-center justify-center px-1 text-[length:var(--text-xs)] font-medium tabular-nums text-[var(--sidebar-fg)] [[data-state=collapsed][data-collapsible=icon]_&]:hidden",
|
|
539
|
+
className,
|
|
540
|
+
)}
|
|
541
|
+
{...props}
|
|
542
|
+
/>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* SidebarMenuItem 의 우측 trailing 액션 버튼 (… 메뉴, 추가 등). badge 와 달리
|
|
548
|
+
* 클릭 가능. `showOnHover` 면 행 hover/포커스 시에만 노출.
|
|
549
|
+
*
|
|
550
|
+
* <SidebarMenuItem>
|
|
551
|
+
* <SidebarMenuButton render={<Link href='/x'>채널</Link>} />
|
|
552
|
+
* <SidebarMenuAction showOnHover aria-label='채널 설정'><MoreIcon /></SidebarMenuAction>
|
|
553
|
+
* </SidebarMenuItem>
|
|
554
|
+
*/
|
|
555
|
+
export interface SidebarMenuActionProps
|
|
556
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
557
|
+
showOnHover?: boolean;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export const SidebarMenuAction = React.forwardRef<HTMLButtonElement, SidebarMenuActionProps>(
|
|
561
|
+
function SidebarMenuAction({ className, showOnHover, ...props }, ref) {
|
|
562
|
+
return (
|
|
563
|
+
<button
|
|
564
|
+
ref={ref}
|
|
565
|
+
type="button"
|
|
566
|
+
data-sidebar="menu-action"
|
|
567
|
+
className={cn(
|
|
568
|
+
"absolute right-[var(--space-1)] top-1/2 flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-[calc(var(--radius)-2px)] border-none bg-transparent text-[var(--foreground-muted)] cursor-pointer transition-[background-color,color] duration-[var(--duration-fast)] hover:bg-[var(--sidebar-accent)] hover:text-[var(--sidebar-accent-fg)] focus-visible:outline-[length:var(--border-width-strong)] focus-visible:outline-ring [&>svg]:h-4 [&>svg]:w-4 [&>svg]:shrink-0 [[data-state=collapsed][data-collapsible=icon]_&]:hidden motion-reduce:transition-none",
|
|
569
|
+
showOnHover &&
|
|
570
|
+
"opacity-0 focus-visible:opacity-100 group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[active]:opacity-100",
|
|
571
|
+
className,
|
|
572
|
+
)}
|
|
573
|
+
{...props}
|
|
574
|
+
/>
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
);
|
|
578
|
+
|
|
512
579
|
export function SidebarMenuSub({ className, ...props }: React.HTMLAttributes<HTMLUListElement>) {
|
|
513
580
|
return (
|
|
514
581
|
<ul
|
|
@@ -790,6 +790,63 @@ export const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenu
|
|
|
790
790
|
}
|
|
791
791
|
);
|
|
792
792
|
|
|
793
|
+
/* ───────────── Menu trailing slots (badge / action) ─────────────
|
|
794
|
+
* SidebarMenuButton 의 `> span { flex: 1 }` 흐름 밖(absolute)에 위치해 라벨을
|
|
795
|
+
* 밀어내지 않는다. SidebarMenuItem 이 trailing 슬롯 존재를 :has() 로 감지해
|
|
796
|
+
* 내부 button/anchor 에 우측 패딩을 확보한다 (styles.css 참고). */
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* SidebarMenuItem 의 우측 trailing 배지 (안 읽음 수 등). SidebarMenuButton 의
|
|
800
|
+
* 형제로 둔다 — absolute 라 라벨을 밀어내지 않고, pointer-events:none 으로 클릭은
|
|
801
|
+
* 행 전체 button 으로 통과한다.
|
|
802
|
+
*
|
|
803
|
+
* <SidebarMenuItem>
|
|
804
|
+
* <SidebarMenuButton render={<Link href='/x'>채팅</Link>} />
|
|
805
|
+
* <SidebarMenuBadge>8</SidebarMenuBadge>
|
|
806
|
+
* </SidebarMenuItem>
|
|
807
|
+
*/
|
|
808
|
+
export function SidebarMenuBadge({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
|
809
|
+
return (
|
|
810
|
+
<div
|
|
811
|
+
data-sidebar="menu-badge"
|
|
812
|
+
className={cn("sh-ui-sidebar__menu-badge", className)}
|
|
813
|
+
{...props}
|
|
814
|
+
/>
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
export interface SidebarMenuActionProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
819
|
+
/** 행 hover/포커스 시에만 노출. 기본은 항상 표시. */
|
|
820
|
+
showOnHover?: boolean;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* SidebarMenuItem 의 우측 trailing 액션 버튼 (… 메뉴, 추가 등). badge 와 달리
|
|
825
|
+
* 클릭 가능. `showOnHover` 면 행 hover/포커스 시에만 노출 (CSS 부모 hover 셀렉터).
|
|
826
|
+
*
|
|
827
|
+
* <SidebarMenuItem>
|
|
828
|
+
* <SidebarMenuButton render={<Link href='/x'>채널</Link>} />
|
|
829
|
+
* <SidebarMenuAction showOnHover aria-label='채널 설정'><MoreIcon /></SidebarMenuAction>
|
|
830
|
+
* </SidebarMenuItem>
|
|
831
|
+
*/
|
|
832
|
+
export const SidebarMenuAction = React.forwardRef<HTMLButtonElement, SidebarMenuActionProps>(
|
|
833
|
+
function SidebarMenuAction({ className, showOnHover, ...props }, ref) {
|
|
834
|
+
return (
|
|
835
|
+
<button
|
|
836
|
+
ref={ref}
|
|
837
|
+
type="button"
|
|
838
|
+
data-sidebar="menu-action"
|
|
839
|
+
className={cn(
|
|
840
|
+
"sh-ui-sidebar__menu-action",
|
|
841
|
+
showOnHover && "sh-ui-sidebar__menu-action--hover",
|
|
842
|
+
className,
|
|
843
|
+
)}
|
|
844
|
+
{...props}
|
|
845
|
+
/>
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
);
|
|
849
|
+
|
|
793
850
|
/* ───────────── Sub menu ───────────── */
|
|
794
851
|
|
|
795
852
|
/** 메뉴 항목 내부의 서브 메뉴 리스트. SidebarMenuItem 안에 둔다. */
|
|
@@ -433,6 +433,15 @@
|
|
|
433
433
|
margin: 0;
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
+
/* trailing 슬롯(badge/action)이 있으면 내부 button/anchor 에 우측 패딩을 확보해
|
|
437
|
+
* 라벨이 trailing 요소 밑으로 들어가지 않게 한다. */
|
|
438
|
+
.sh-ui-sidebar__menu-item:has(.sh-ui-sidebar__menu-badge) > a,
|
|
439
|
+
.sh-ui-sidebar__menu-item:has(.sh-ui-sidebar__menu-badge) > button,
|
|
440
|
+
.sh-ui-sidebar__menu-item:has(.sh-ui-sidebar__menu-action) > a,
|
|
441
|
+
.sh-ui-sidebar__menu-item:has(.sh-ui-sidebar__menu-action) > button {
|
|
442
|
+
padding-inline-end: 2rem;
|
|
443
|
+
}
|
|
444
|
+
|
|
436
445
|
.sh-ui-sidebar__menu-button {
|
|
437
446
|
display: flex;
|
|
438
447
|
width: 100%;
|
|
@@ -499,6 +508,73 @@
|
|
|
499
508
|
font-size: 0.9375rem;
|
|
500
509
|
}
|
|
501
510
|
|
|
511
|
+
/* ───────────── Menu trailing slots (badge / action) ───────────── */
|
|
512
|
+
.sh-ui-sidebar__menu-badge {
|
|
513
|
+
position: absolute;
|
|
514
|
+
right: var(--space-2);
|
|
515
|
+
top: 50%;
|
|
516
|
+
transform: translateY(-50%);
|
|
517
|
+
display: flex;
|
|
518
|
+
align-items: center;
|
|
519
|
+
justify-content: center;
|
|
520
|
+
height: 1.25rem;
|
|
521
|
+
min-width: 1.25rem;
|
|
522
|
+
padding: 0 0.25rem;
|
|
523
|
+
font-size: var(--text-xs);
|
|
524
|
+
font-weight: var(--weight-medium);
|
|
525
|
+
font-variant-numeric: tabular-nums;
|
|
526
|
+
color: var(--sidebar-fg);
|
|
527
|
+
pointer-events: none;
|
|
528
|
+
user-select: none;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.sh-ui-sidebar__menu-action {
|
|
532
|
+
position: absolute;
|
|
533
|
+
right: var(--space-1);
|
|
534
|
+
top: 50%;
|
|
535
|
+
transform: translateY(-50%);
|
|
536
|
+
display: flex;
|
|
537
|
+
align-items: center;
|
|
538
|
+
justify-content: center;
|
|
539
|
+
width: 1.5rem;
|
|
540
|
+
height: 1.5rem;
|
|
541
|
+
border: none;
|
|
542
|
+
border-radius: calc(var(--radius) - 2px);
|
|
543
|
+
background: transparent;
|
|
544
|
+
color: var(--foreground-muted);
|
|
545
|
+
cursor: pointer;
|
|
546
|
+
transition: background-color var(--duration-fast), color var(--duration-fast), opacity var(--duration-fast);
|
|
547
|
+
}
|
|
548
|
+
.sh-ui-sidebar__menu-action > svg {
|
|
549
|
+
width: 1rem;
|
|
550
|
+
height: 1rem;
|
|
551
|
+
flex-shrink: 0;
|
|
552
|
+
}
|
|
553
|
+
.sh-ui-sidebar__menu-action:hover {
|
|
554
|
+
background: var(--sidebar-accent);
|
|
555
|
+
color: var(--sidebar-accent-fg);
|
|
556
|
+
}
|
|
557
|
+
.sh-ui-sidebar__menu-action:focus-visible {
|
|
558
|
+
outline: var(--border-width-strong) solid var(--ring);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/* showOnHover — 행 hover/포커스 시에만 노출. 부모 menu-item hover/focus-within 셀렉터. */
|
|
562
|
+
.sh-ui-sidebar__menu-action--hover {
|
|
563
|
+
opacity: 0;
|
|
564
|
+
}
|
|
565
|
+
.sh-ui-sidebar__menu-item:hover .sh-ui-sidebar__menu-action--hover,
|
|
566
|
+
.sh-ui-sidebar__menu-item:focus-within .sh-ui-sidebar__menu-action--hover,
|
|
567
|
+
.sh-ui-sidebar__menu-action--hover:focus-visible,
|
|
568
|
+
.sh-ui-sidebar__menu-action--hover[data-active] {
|
|
569
|
+
opacity: 1;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/* collapsed=icon 모드에서는 trailing 슬롯 숨김 (라벨도 숨겨지므로) */
|
|
573
|
+
.sh-ui-sidebar[data-state="collapsed"][data-collapsible="icon"] .sh-ui-sidebar__menu-badge,
|
|
574
|
+
.sh-ui-sidebar[data-state="collapsed"][data-collapsible="icon"] .sh-ui-sidebar__menu-action {
|
|
575
|
+
display: none;
|
|
576
|
+
}
|
|
577
|
+
|
|
502
578
|
/* ───────────── Sub menu ───────────── */
|
|
503
579
|
.sh-ui-sidebar__menu-sub {
|
|
504
580
|
list-style: none;
|
|
@@ -607,6 +683,7 @@
|
|
|
607
683
|
.sh-ui-sidebar__trigger,
|
|
608
684
|
.sh-ui-sidebar__menu-button,
|
|
609
685
|
.sh-ui-sidebar__menu-sub-button,
|
|
686
|
+
.sh-ui-sidebar__menu-action,
|
|
610
687
|
.sh-ui-sidebar__panel-close,
|
|
611
688
|
.sh-ui-sidebar__chevron,
|
|
612
689
|
.sh-ui-sidebar__collapsible-content {
|
|
@@ -412,6 +412,15 @@
|
|
|
412
412
|
margin: 0;
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
+
/* trailing 슬롯(badge/action)이 있으면 내부 button/anchor 에 우측 패딩을 확보해
|
|
416
|
+
* 라벨이 trailing 요소 밑으로 들어가지 않게 한다. */
|
|
417
|
+
.sidebar__menu-item:has(.sidebar__menu-badge) > a,
|
|
418
|
+
.sidebar__menu-item:has(.sidebar__menu-badge) > button,
|
|
419
|
+
.sidebar__menu-item:has(.sidebar__menu-action) > a,
|
|
420
|
+
.sidebar__menu-item:has(.sidebar__menu-action) > button {
|
|
421
|
+
padding-inline-end: 2rem;
|
|
422
|
+
}
|
|
423
|
+
|
|
415
424
|
.sidebar__menu-button {
|
|
416
425
|
display: flex;
|
|
417
426
|
width: 100%;
|
|
@@ -478,6 +487,73 @@
|
|
|
478
487
|
font-size: 0.9375rem;
|
|
479
488
|
}
|
|
480
489
|
|
|
490
|
+
/* ───────────── Menu trailing slots (badge / action) ───────────── */
|
|
491
|
+
.sidebar__menu-badge {
|
|
492
|
+
position: absolute;
|
|
493
|
+
right: var(--space-2);
|
|
494
|
+
top: 50%;
|
|
495
|
+
transform: translateY(-50%);
|
|
496
|
+
display: flex;
|
|
497
|
+
align-items: center;
|
|
498
|
+
justify-content: center;
|
|
499
|
+
height: 1.25rem;
|
|
500
|
+
min-width: 1.25rem;
|
|
501
|
+
padding: 0 0.25rem;
|
|
502
|
+
font-size: var(--text-xs);
|
|
503
|
+
font-weight: var(--weight-medium);
|
|
504
|
+
font-variant-numeric: tabular-nums;
|
|
505
|
+
color: var(--sidebar-fg);
|
|
506
|
+
pointer-events: none;
|
|
507
|
+
user-select: none;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.sidebar__menu-action {
|
|
511
|
+
position: absolute;
|
|
512
|
+
right: var(--space-1);
|
|
513
|
+
top: 50%;
|
|
514
|
+
transform: translateY(-50%);
|
|
515
|
+
display: flex;
|
|
516
|
+
align-items: center;
|
|
517
|
+
justify-content: center;
|
|
518
|
+
width: 1.5rem;
|
|
519
|
+
height: 1.5rem;
|
|
520
|
+
border: none;
|
|
521
|
+
border-radius: calc(var(--radius) - 2px);
|
|
522
|
+
background: transparent;
|
|
523
|
+
color: var(--foreground-muted);
|
|
524
|
+
cursor: pointer;
|
|
525
|
+
transition: background-color var(--duration-fast), color var(--duration-fast), opacity var(--duration-fast);
|
|
526
|
+
}
|
|
527
|
+
.sidebar__menu-action > svg {
|
|
528
|
+
width: 1rem;
|
|
529
|
+
height: 1rem;
|
|
530
|
+
flex-shrink: 0;
|
|
531
|
+
}
|
|
532
|
+
.sidebar__menu-action:hover {
|
|
533
|
+
background: var(--sidebar-accent);
|
|
534
|
+
color: var(--sidebar-accent-fg);
|
|
535
|
+
}
|
|
536
|
+
.sidebar__menu-action:focus-visible {
|
|
537
|
+
outline: var(--border-width-strong) solid var(--ring);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/* showOnHover — 행 hover/포커스 시에만 노출. 부모 menu-item hover/focus-within 셀렉터. */
|
|
541
|
+
.sidebar__menu-action--hover {
|
|
542
|
+
opacity: 0;
|
|
543
|
+
}
|
|
544
|
+
.sidebar__menu-item:hover .sidebar__menu-action--hover,
|
|
545
|
+
.sidebar__menu-item:focus-within .sidebar__menu-action--hover,
|
|
546
|
+
.sidebar__menu-action--hover:focus-visible,
|
|
547
|
+
.sidebar__menu-action--hover[data-active] {
|
|
548
|
+
opacity: 1;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/* collapsed=icon 모드에서는 trailing 슬롯 숨김 (라벨도 숨겨지므로) */
|
|
552
|
+
.sidebar[data-state="collapsed"][data-collapsible="icon"] .sidebar__menu-badge,
|
|
553
|
+
.sidebar[data-state="collapsed"][data-collapsible="icon"] .sidebar__menu-action {
|
|
554
|
+
display: none;
|
|
555
|
+
}
|
|
556
|
+
|
|
481
557
|
/* ───────────── Sub menu ───────────── */
|
|
482
558
|
.sidebar__menu-sub {
|
|
483
559
|
list-style: none;
|
|
@@ -575,6 +651,7 @@
|
|
|
575
651
|
.sidebar__trigger,
|
|
576
652
|
.sidebar__menu-button,
|
|
577
653
|
.sidebar__menu-sub-button,
|
|
654
|
+
.sidebar__menu-action,
|
|
578
655
|
.sidebar__panel-close,
|
|
579
656
|
.sidebar__chevron {
|
|
580
657
|
transition: none;
|