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.
Files changed (23) hide show
  1. package/data/changelog/versions.json +27 -0
  2. package/data/registry/react/components/form/field.test.tsx +106 -1
  3. package/data/registry/react/components/form/field.tsx +179 -23
  4. package/data/registry/react/components/form/use-sh-ui-form.ts +14 -0
  5. package/data/registry/react/components/form-rhf/README.md +138 -8
  6. package/data/registry/react/components/form-rhf/index.tsx +75 -0
  7. package/data/registry/react/components/form-rhf/rhf.test.tsx +53 -1
  8. package/data/registry/react/components/label/index.tailwind.tsx +5 -1
  9. package/data/registry/react/components/label/styles.css +9 -5
  10. package/data/registry/react/components/label/styles.module.css +7 -5
  11. package/data/registry/react/components/rich-text-editor/index.module.tsx +523 -171
  12. package/data/registry/react/components/rich-text-editor/index.tailwind.tsx +596 -70
  13. package/data/registry/react/components/rich-text-editor/index.tsx +523 -171
  14. package/data/registry/react/components/rich-text-editor/styles.css +103 -5
  15. package/data/registry/react/components/rich-text-editor/styles.module.css +103 -5
  16. package/data/registry/react/components/sidebar/index.module.tsx +57 -0
  17. package/data/registry/react/components/sidebar/index.tailwind.tsx +68 -1
  18. package/data/registry/react/components/sidebar/index.tsx +57 -0
  19. package/data/registry/react/components/sidebar/styles.css +77 -0
  20. package/data/registry/react/components/sidebar/styles.module.css +77 -0
  21. package/data/registry/react/registry.json +319 -963
  22. package/data/registry/react/tokens-used.json +4 -1
  23. 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 { font-size: 1.5rem; }
112
- .sh-ui-rte__content h2 { font-size: 1.25rem; }
113
- .sh-ui-rte__content h3 { font-size: 1.125rem; }
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 { font-size: 1.5rem; }
112
- .rte__content h2 { font-size: 1.25rem; }
113
- .rte__content h3 { font-size: 1.125rem; }
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 <li className={cn("relative m-0", className)} {...props} />;
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;