sh-ui-cli 0.14.0 → 0.21.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 (162) hide show
  1. package/bin/sh-ui.mjs +6 -0
  2. package/data/changelog/versions.json +354 -0
  3. package/data/registry/flutter/foundation/sh_ui_tokens.dart +385 -0
  4. package/data/registry/flutter/registry.json +336 -0
  5. package/data/registry/flutter/widgets/sh_ui_accordion.dart +255 -0
  6. package/data/registry/flutter/widgets/sh_ui_app_shell.dart +267 -0
  7. package/data/registry/flutter/widgets/sh_ui_avatar.dart +95 -0
  8. package/data/registry/flutter/widgets/sh_ui_badge.dart +82 -0
  9. package/data/registry/flutter/widgets/sh_ui_breadcrumb.dart +107 -0
  10. package/data/registry/flutter/widgets/sh_ui_button.dart +201 -0
  11. package/data/registry/flutter/widgets/sh_ui_card.dart +159 -0
  12. package/data/registry/flutter/widgets/sh_ui_carousel.dart +204 -0
  13. package/data/registry/flutter/widgets/sh_ui_checkbox.dart +154 -0
  14. package/data/registry/flutter/widgets/sh_ui_color_picker.dart +264 -0
  15. package/data/registry/flutter/widgets/sh_ui_combobox.dart +614 -0
  16. package/data/registry/flutter/widgets/sh_ui_context_menu.dart +71 -0
  17. package/data/registry/flutter/widgets/sh_ui_date_picker.dart +648 -0
  18. package/data/registry/flutter/widgets/sh_ui_dialog.dart +567 -0
  19. package/data/registry/flutter/widgets/sh_ui_dropdown_menu.dart +251 -0
  20. package/data/registry/flutter/widgets/sh_ui_file_upload.dart +200 -0
  21. package/data/registry/flutter/widgets/sh_ui_header.dart +488 -0
  22. package/data/registry/flutter/widgets/sh_ui_input.dart +664 -0
  23. package/data/registry/flutter/widgets/sh_ui_label.dart +145 -0
  24. package/data/registry/flutter/widgets/sh_ui_menubar.dart +98 -0
  25. package/data/registry/flutter/widgets/sh_ui_pagination.dart +276 -0
  26. package/data/registry/flutter/widgets/sh_ui_popover.dart +248 -0
  27. package/data/registry/flutter/widgets/sh_ui_progress.dart +47 -0
  28. package/data/registry/flutter/widgets/sh_ui_radio.dart +108 -0
  29. package/data/registry/flutter/widgets/sh_ui_select.dart +904 -0
  30. package/data/registry/flutter/widgets/sh_ui_separator.dart +42 -0
  31. package/data/registry/flutter/widgets/sh_ui_sidebar.dart +1116 -0
  32. package/data/registry/flutter/widgets/sh_ui_skeleton.dart +129 -0
  33. package/data/registry/flutter/widgets/sh_ui_slider.dart +147 -0
  34. package/data/registry/flutter/widgets/sh_ui_spinner.dart +56 -0
  35. package/data/registry/flutter/widgets/sh_ui_switch.dart +109 -0
  36. package/data/registry/flutter/widgets/sh_ui_tabs.dart +329 -0
  37. package/data/registry/flutter/widgets/sh_ui_textarea.dart +126 -0
  38. package/data/registry/flutter/widgets/sh_ui_toast.dart +362 -0
  39. package/data/registry/flutter/widgets/sh_ui_toggle.dart +229 -0
  40. package/data/registry/flutter/widgets/sh_ui_tooltip.dart +62 -0
  41. package/data/registry/react/components/accordion/index.tsx +85 -0
  42. package/data/registry/react/components/accordion/styles.css +94 -0
  43. package/data/registry/react/components/animations/animations.css +51 -0
  44. package/data/registry/react/components/avatar/index.tsx +75 -0
  45. package/data/registry/react/components/avatar/styles.css +36 -0
  46. package/data/registry/react/components/badge/index.tsx +42 -0
  47. package/data/registry/react/components/badge/styles.css +57 -0
  48. package/data/registry/react/components/base/base.css +102 -0
  49. package/data/registry/react/components/breadcrumb/index.tsx +154 -0
  50. package/data/registry/react/components/breadcrumb/styles.css +82 -0
  51. package/data/registry/react/components/breakpoints/breakpoints.css +17 -0
  52. package/data/registry/react/components/button/index.tsx +47 -0
  53. package/data/registry/react/components/button/styles.css +93 -0
  54. package/data/registry/react/components/card/index.tsx +86 -0
  55. package/data/registry/react/components/card/styles.css +73 -0
  56. package/data/registry/react/components/carousel/index.tsx +432 -0
  57. package/data/registry/react/components/carousel/styles.css +155 -0
  58. package/data/registry/react/components/checkbox/index.tsx +98 -0
  59. package/data/registry/react/components/checkbox/styles.css +75 -0
  60. package/data/registry/react/components/code-panel/copy.tsx +56 -0
  61. package/data/registry/react/components/code-panel/index.tsx +193 -0
  62. package/data/registry/react/components/code-panel/styles.css +124 -0
  63. package/data/registry/react/components/color-picker/index.tsx +466 -0
  64. package/data/registry/react/components/color-picker/styles.css +166 -0
  65. package/data/registry/react/components/combobox/index.tsx +167 -0
  66. package/data/registry/react/components/combobox/styles.css +151 -0
  67. package/data/registry/react/components/context-menu/index.tsx +253 -0
  68. package/data/registry/react/components/context-menu/styles.css +140 -0
  69. package/data/registry/react/components/date-picker/index.tsx +757 -0
  70. package/data/registry/react/components/date-picker/styles.css +279 -0
  71. package/data/registry/react/components/dialog/index.tsx +97 -0
  72. package/data/registry/react/components/dialog/styles.css +127 -0
  73. package/data/registry/react/components/dropdown-menu/index.tsx +257 -0
  74. package/data/registry/react/components/dropdown-menu/styles.css +150 -0
  75. package/data/registry/react/components/file-upload/index.tsx +489 -0
  76. package/data/registry/react/components/file-upload/styles.css +170 -0
  77. package/data/registry/react/components/focus-ring/focus-ring.css +23 -0
  78. package/data/registry/react/components/form/context.ts +92 -0
  79. package/data/registry/react/components/form/field.test.tsx +230 -0
  80. package/data/registry/react/components/form/field.tsx +236 -0
  81. package/data/registry/react/components/form/focus-first-error.ts +54 -0
  82. package/data/registry/react/components/form/form.section.test.tsx +58 -0
  83. package/data/registry/react/components/form/form.test.tsx +146 -0
  84. package/data/registry/react/components/form/form.tsx +180 -0
  85. package/data/registry/react/components/form/index.tsx +61 -0
  86. package/data/registry/react/components/form/steps.test.tsx +106 -0
  87. package/data/registry/react/components/form/steps.tsx +193 -0
  88. package/data/registry/react/components/form/store.test.ts +206 -0
  89. package/data/registry/react/components/form/store.ts +318 -0
  90. package/data/registry/react/components/form/styles.css +47 -0
  91. package/data/registry/react/components/form/types.ts +104 -0
  92. package/data/registry/react/components/form/use-sh-ui-form.ts +15 -0
  93. package/data/registry/react/components/form/utils.test.ts +44 -0
  94. package/data/registry/react/components/form/utils.ts +49 -0
  95. package/data/registry/react/components/form/validation.test.ts +67 -0
  96. package/data/registry/react/components/form/validation.ts +64 -0
  97. package/data/registry/react/components/form-rhf/README.md +27 -0
  98. package/data/registry/react/components/form-rhf/index.tsx +289 -0
  99. package/data/registry/react/components/form-rhf/rhf.test.tsx +42 -0
  100. package/data/registry/react/components/form-tanstack/README.md +27 -0
  101. package/data/registry/react/components/form-tanstack/index.tsx +352 -0
  102. package/data/registry/react/components/form-tanstack/tanstack.test.tsx +45 -0
  103. package/data/registry/react/components/form-yup/README.md +22 -0
  104. package/data/registry/react/components/form-yup/index.tsx +50 -0
  105. package/data/registry/react/components/form-yup/yup.test.ts +27 -0
  106. package/data/registry/react/components/header/index.tsx +257 -0
  107. package/data/registry/react/components/header/styles.css +190 -0
  108. package/data/registry/react/components/input/index.tsx +517 -0
  109. package/data/registry/react/components/input/styles.css +203 -0
  110. package/data/registry/react/components/label/index.tsx +54 -0
  111. package/data/registry/react/components/label/styles.css +90 -0
  112. package/data/registry/react/components/menubar/index.tsx +34 -0
  113. package/data/registry/react/components/menubar/styles.css +45 -0
  114. package/data/registry/react/components/pagination/index.tsx +271 -0
  115. package/data/registry/react/components/pagination/styles.css +105 -0
  116. package/data/registry/react/components/popover/index.tsx +115 -0
  117. package/data/registry/react/components/popover/styles.css +65 -0
  118. package/data/registry/react/components/progress/index.tsx +56 -0
  119. package/data/registry/react/components/progress/styles.css +41 -0
  120. package/data/registry/react/components/radio/index.tsx +67 -0
  121. package/data/registry/react/components/radio/styles.css +80 -0
  122. package/data/registry/react/components/select/index.tsx +236 -0
  123. package/data/registry/react/components/select/styles.css +193 -0
  124. package/data/registry/react/components/separator/index.tsx +48 -0
  125. package/data/registry/react/components/separator/styles.css +15 -0
  126. package/data/registry/react/components/sidebar/index.tsx +1084 -0
  127. package/data/registry/react/components/sidebar/styles.css +502 -0
  128. package/data/registry/react/components/skeleton/index.tsx +24 -0
  129. package/data/registry/react/components/skeleton/styles.css +24 -0
  130. package/data/registry/react/components/slider/index.tsx +300 -0
  131. package/data/registry/react/components/slider/styles.css +64 -0
  132. package/data/registry/react/components/spinner/index.tsx +40 -0
  133. package/data/registry/react/components/spinner/styles.css +37 -0
  134. package/data/registry/react/components/switch/index.tsx +41 -0
  135. package/data/registry/react/components/switch/styles.css +83 -0
  136. package/data/registry/react/components/tabs/index.tsx +93 -0
  137. package/data/registry/react/components/tabs/styles.css +148 -0
  138. package/data/registry/react/components/textarea/index.tsx +25 -0
  139. package/data/registry/react/components/textarea/styles.css +54 -0
  140. package/data/registry/react/components/theme/index.tsx +91 -0
  141. package/data/registry/react/components/toast/index.tsx +257 -0
  142. package/data/registry/react/components/toast/styles.css +290 -0
  143. package/data/registry/react/components/toggle/index.tsx +133 -0
  144. package/data/registry/react/components/toggle/styles.css +85 -0
  145. package/data/registry/react/components/tooltip/index.tsx +85 -0
  146. package/data/registry/react/components/tooltip/styles.css +44 -0
  147. package/data/registry/react/components/z-index/z-index.css +16 -0
  148. package/data/registry/react/hooks/use-active-section.ts +104 -0
  149. package/data/registry/react/hooks/use-media-query.ts +27 -0
  150. package/data/registry/react/lib/cn.ts +39 -0
  151. package/data/registry/react/registry.json +835 -0
  152. package/data/summaries/flutter.json +42 -0
  153. package/data/summaries/react.json +50 -0
  154. package/data/tokens/build.mjs +553 -0
  155. package/data/tokens/src/primitives.json +146 -0
  156. package/data/tokens/src/semantic.json +146 -0
  157. package/package.json +13 -4
  158. package/src/add.mjs +13 -12
  159. package/src/list.mjs +3 -11
  160. package/src/mcp.mjs +308 -0
  161. package/src/paths.mjs +52 -0
  162. package/src/remove.mjs +4 -11
@@ -0,0 +1,257 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import "./styles.css";
5
+
6
+ function cx(...args: (string | undefined | false)[]) {
7
+ return args.filter(Boolean).join(" ");
8
+ }
9
+
10
+ type HeaderCtx = {
11
+ open: boolean;
12
+ setOpen: (v: boolean) => void;
13
+ };
14
+
15
+ const HeaderContext = React.createContext<HeaderCtx | null>(null);
16
+
17
+ function useHeader(): HeaderCtx {
18
+ const ctx = React.useContext(HeaderContext);
19
+ if (!ctx) {
20
+ throw new Error("Header 하위 컴포넌트는 <Header> 안에서만 사용할 수 있습니다.");
21
+ }
22
+ return ctx;
23
+ }
24
+
25
+ /* ───────── Root ─────────
26
+ * mode 판정은 CSS 미디어 쿼리에 위임(`--bp-md` 기준).
27
+ * 이 컴포넌트는 open/close 상태만 관리한다.
28
+ */
29
+
30
+ /**
31
+ * 사이트 상단 헤더(`<header>`). 데스크탑에서는 inline nav, 모바일에서는 햄버거 + drawer로
32
+ * CSS가 자동 전환한다. 컴포넌트는 open/close 상태와 body 스크롤 잠금만 담당.
33
+ */
34
+ export const Header = React.forwardRef<
35
+ HTMLElement,
36
+ React.HTMLAttributes<HTMLElement> & {
37
+ /**
38
+ * 모바일 drawer 초기 상태 (비제어 모드).
39
+ * @default false
40
+ */
41
+ defaultOpen?: boolean;
42
+ /** 모바일 drawer 열림 상태 (제어 모드). 지정 시 내부 state 대신 이 값이 우선. */
43
+ open?: boolean;
44
+ /** drawer 열림 변경 콜백. 제어 모드에서는 이 안에서 외부 상태를 업데이트해야 한다. */
45
+ onOpenChange?: (open: boolean) => void;
46
+ }
47
+ >(function Header(
48
+ { children, className, defaultOpen = false, open: openProp, onOpenChange, ...props },
49
+ ref,
50
+ ) {
51
+ const isControlled = openProp !== undefined;
52
+ const [internal, setInternal] = React.useState(defaultOpen);
53
+ const open = isControlled ? openProp : internal;
54
+ const setOpen = React.useCallback(
55
+ (v: boolean) => {
56
+ if (!isControlled) setInternal(v);
57
+ onOpenChange?.(v);
58
+ },
59
+ [isControlled, onOpenChange],
60
+ );
61
+
62
+ // drawer 열림 동안 body 스크롤 잠금
63
+ React.useEffect(() => {
64
+ if (!open) return;
65
+ const prev = document.body.style.overflow;
66
+ document.body.style.overflow = "hidden";
67
+ return () => {
68
+ document.body.style.overflow = prev;
69
+ };
70
+ }, [open]);
71
+
72
+ return (
73
+ <HeaderContext.Provider value={{ open, setOpen }}>
74
+ <header
75
+ ref={ref}
76
+ className={cx("sh-ui-header", className)}
77
+ data-drawer-open={open ? "" : undefined}
78
+ {...props}
79
+ >
80
+ {children}
81
+ </header>
82
+ </HeaderContext.Provider>
83
+ );
84
+ });
85
+
86
+ /* ───────── Brand / Logo / Title ───────── */
87
+
88
+ /** 좌측 브랜드 영역. HeaderLogo + HeaderTitle을 묶을 때 사용. */
89
+ export const HeaderBrand = React.forwardRef<
90
+ HTMLDivElement,
91
+ React.HTMLAttributes<HTMLDivElement>
92
+ >(function HeaderBrand({ className, ...props }, ref) {
93
+ return <div ref={ref} className={cx("sh-ui-header__brand", className)} {...props} />;
94
+ });
95
+
96
+ /** 브랜드 로고 슬롯. SVG 또는 이미지를 자식으로 둔다. */
97
+ export const HeaderLogo = React.forwardRef<
98
+ HTMLSpanElement,
99
+ React.HTMLAttributes<HTMLSpanElement>
100
+ >(function HeaderLogo({ className, ...props }, ref) {
101
+ return <span ref={ref} className={cx("sh-ui-header__logo", className)} {...props} />;
102
+ });
103
+
104
+ /** 브랜드 텍스트 타이틀. */
105
+ export const HeaderTitle = React.forwardRef<
106
+ HTMLSpanElement,
107
+ React.HTMLAttributes<HTMLSpanElement>
108
+ >(function HeaderTitle({ className, ...props }, ref) {
109
+ return <span ref={ref} className={cx("sh-ui-header__title", className)} {...props} />;
110
+ });
111
+
112
+ /* ───────── Trigger ─────────
113
+ * 햄버거 버튼. 모바일에서만 표시(CSS로 제어). 클릭 시 drawer 토글.
114
+ */
115
+
116
+ /** 모바일 햄버거 토글 버튼. CSS 미디어 쿼리로 모바일에서만 노출되며 drawer 열림을 제어한다. */
117
+ export const HeaderTrigger = React.forwardRef<
118
+ HTMLButtonElement,
119
+ React.ButtonHTMLAttributes<HTMLButtonElement>
120
+ >(function HeaderTrigger({ className, onClick, children, ...props }, ref) {
121
+ const { open, setOpen } = useHeader();
122
+ return (
123
+ <button
124
+ ref={ref}
125
+ type="button"
126
+ className={cx("sh-ui-header__trigger", className)}
127
+ aria-label={open ? "메뉴 닫기" : "메뉴 열기"}
128
+ aria-expanded={open}
129
+ data-open={open ? "" : undefined}
130
+ onClick={(e) => {
131
+ setOpen(!open);
132
+ onClick?.(e);
133
+ }}
134
+ {...props}
135
+ >
136
+ {children ?? (open ? <CloseIcon /> : <MenuIcon />)}
137
+ </button>
138
+ );
139
+ });
140
+
141
+ /* ───────── Nav ─────────
142
+ * 자식(HeaderItem 등)을 두 곳에 렌더:
143
+ * - inline nav (wide 뷰포트)
144
+ * - drawer (narrow 뷰포트 + open 상태)
145
+ * CSS가 뷰포트에 맞춰 하나만 보이도록 처리한다.
146
+ */
147
+
148
+ /**
149
+ * 내비게이션 영역. 자식을 inline nav와 mobile drawer 두 곳에 동시에 렌더하며
150
+ * CSS가 뷰포트에 따라 한 쪽만 보여준다.
151
+ */
152
+ export const HeaderNav = React.forwardRef<
153
+ HTMLElement,
154
+ React.HTMLAttributes<HTMLElement>
155
+ >(function HeaderNav({ className, children, ...props }, ref) {
156
+ const { open, setOpen } = useHeader();
157
+ return (
158
+ <>
159
+ <nav
160
+ ref={ref}
161
+ className={cx("sh-ui-header__nav", className)}
162
+ {...props}
163
+ >
164
+ {children}
165
+ </nav>
166
+ {/* Drawer backdrop */}
167
+ <div
168
+ className="sh-ui-header__backdrop"
169
+ data-open={open ? "" : undefined}
170
+ onClick={() => setOpen(false)}
171
+ aria-hidden
172
+ />
173
+ {/* Drawer panel */}
174
+ <aside
175
+ className="sh-ui-header__drawer"
176
+ data-open={open ? "" : undefined}
177
+ aria-hidden={!open}
178
+ >
179
+ <div className="sh-ui-header__drawer-head">
180
+ <HeaderTrigger />
181
+ </div>
182
+ <nav className="sh-ui-header__drawer-nav">{children}</nav>
183
+ </aside>
184
+ </>
185
+ );
186
+ });
187
+
188
+ /* ───────── Item ───────── */
189
+
190
+ /** 내비 한 항목(`<a>`). 클릭 시 모바일 drawer가 자동으로 닫힌다. `active`로 현재 위치 강조. */
191
+ export const HeaderItem = React.forwardRef<
192
+ HTMLAnchorElement,
193
+ React.AnchorHTMLAttributes<HTMLAnchorElement> & {
194
+ /**
195
+ * 현재 페이지 표시. `true`면 시각적으로 강조된다.
196
+ * 라우터 활성 상태와 직접 연결해 사용 (예: `active={pathname === href}`).
197
+ *
198
+ * @default false
199
+ */
200
+ active?: boolean;
201
+ }
202
+ >(function HeaderItem({ className, active, onClick, href, ...props }, ref) {
203
+ const { setOpen } = useHeader();
204
+ return (
205
+ <a
206
+ ref={ref}
207
+ href={href}
208
+ className={cx("sh-ui-header__item", className)}
209
+ data-active={active ? "" : undefined}
210
+ onClick={(e) => {
211
+ setOpen(false);
212
+ onClick?.(e);
213
+ }}
214
+ {...props}
215
+ />
216
+ );
217
+ });
218
+
219
+ /* ───────── Actions (우측 트레일링) ───────── */
220
+
221
+ /** 우측 트레일링 액션 영역. 검색·테마 토글·로그인 버튼 등을 둔다. */
222
+ export const HeaderActions = React.forwardRef<
223
+ HTMLDivElement,
224
+ React.HTMLAttributes<HTMLDivElement>
225
+ >(function HeaderActions({ className, ...props }, ref) {
226
+ return (
227
+ <div ref={ref} className={cx("sh-ui-header__actions", className)} {...props} />
228
+ );
229
+ });
230
+
231
+ /* ───────── 기본 아이콘 ───────── */
232
+
233
+ function MenuIcon() {
234
+ return (
235
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden>
236
+ <path
237
+ d="M3 6h18M3 12h18M3 18h18"
238
+ stroke="currentColor"
239
+ strokeWidth="1.75"
240
+ strokeLinecap="round"
241
+ />
242
+ </svg>
243
+ );
244
+ }
245
+
246
+ function CloseIcon() {
247
+ return (
248
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden>
249
+ <path
250
+ d="M6 6l12 12M18 6L6 18"
251
+ stroke="currentColor"
252
+ strokeWidth="1.75"
253
+ strokeLinecap="round"
254
+ />
255
+ </svg>
256
+ );
257
+ }
@@ -0,0 +1,190 @@
1
+ /* ───── Root ───── */
2
+ .sh-ui-header {
3
+ position: relative;
4
+ display: flex;
5
+ align-items: center;
6
+ gap: var(--space-4);
7
+ height: var(--control-md);
8
+ padding: 0 var(--space-3);
9
+ background: var(--background);
10
+ border-bottom: 1px solid var(--border);
11
+ }
12
+
13
+ /* ───── Brand ───── */
14
+ .sh-ui-header__brand {
15
+ display: inline-flex;
16
+ align-items: center;
17
+ gap: var(--space-2);
18
+ flex-shrink: 0;
19
+ }
20
+
21
+ .sh-ui-header__logo {
22
+ display: inline-flex;
23
+ align-items: center;
24
+ color: var(--foreground);
25
+ }
26
+
27
+ .sh-ui-header__title {
28
+ font-size: var(--text-base);
29
+ font-weight: var(--weight-bold);
30
+ color: var(--foreground);
31
+ letter-spacing: -0.3px;
32
+ }
33
+
34
+ /* ───── Trigger (햄버거) ───── */
35
+ .sh-ui-header__trigger {
36
+ display: none;
37
+ align-items: center;
38
+ justify-content: center;
39
+ width: 2.25rem;
40
+ height: 2.25rem;
41
+ padding: 0;
42
+ background: transparent;
43
+ border: 0;
44
+ color: var(--foreground);
45
+ border-radius: calc(var(--radius) - 2px);
46
+ cursor: pointer;
47
+ transition: background-color var(--duration-fast);
48
+ }
49
+ .sh-ui-header__trigger:hover {
50
+ background: var(--background-muted);
51
+ }
52
+ .sh-ui-header__trigger:focus-visible {
53
+ outline: var(--border-width-strong) solid var(--foreground);
54
+ outline-offset: 2px;
55
+ }
56
+
57
+ /* ───── Inline Nav ───── */
58
+ .sh-ui-header__nav {
59
+ display: flex;
60
+ align-items: center;
61
+ gap: var(--space-1);
62
+ flex: 1;
63
+ min-width: 0;
64
+ overflow-x: auto;
65
+ scrollbar-width: none;
66
+ }
67
+ .sh-ui-header__nav::-webkit-scrollbar {
68
+ display: none;
69
+ }
70
+
71
+ /* ───── Item ───── */
72
+ .sh-ui-header__item {
73
+ display: inline-flex;
74
+ align-items: center;
75
+ gap: var(--space-1);
76
+ padding: var(--space-2) var(--space-3);
77
+ font-size: var(--text-sm);
78
+ font-weight: var(--weight-medium);
79
+ color: var(--foreground-muted);
80
+ text-decoration: none;
81
+ background: transparent;
82
+ border: 0;
83
+ border-radius: calc(var(--radius) - 2px);
84
+ cursor: pointer;
85
+ white-space: nowrap;
86
+ transition: color var(--duration-fast), background-color var(--duration-fast);
87
+ }
88
+ .sh-ui-header__item:hover {
89
+ color: var(--foreground);
90
+ background: var(--background-muted);
91
+ }
92
+ .sh-ui-header__item[data-active] {
93
+ color: var(--foreground);
94
+ font-weight: var(--weight-semibold);
95
+ }
96
+ .sh-ui-header__item:focus-visible {
97
+ outline: var(--border-width-strong) solid var(--foreground);
98
+ outline-offset: 2px;
99
+ }
100
+
101
+ /* ───── Actions ───── */
102
+ .sh-ui-header__actions {
103
+ display: inline-flex;
104
+ align-items: center;
105
+ gap: var(--space-2);
106
+ margin-left: auto;
107
+ flex-shrink: 0;
108
+ }
109
+
110
+ /* ───── Drawer (backdrop + panel) — 기본 숨김 ───── */
111
+ .sh-ui-header__backdrop,
112
+ .sh-ui-header__drawer {
113
+ display: none;
114
+ }
115
+
116
+ /* ───── Mobile (< breakpoint.md) ───── */
117
+ @media (max-width: 767px) {
118
+ /* 햄버거 노출 & 좌측 배치 */
119
+ .sh-ui-header__trigger {
120
+ display: inline-flex;
121
+ order: -1;
122
+ }
123
+ /* inline nav 숨김 */
124
+ .sh-ui-header__nav {
125
+ display: none;
126
+ }
127
+ /* 모바일에서 gap 조정 */
128
+ .sh-ui-header {
129
+ gap: var(--space-2);
130
+ }
131
+
132
+ /* backdrop */
133
+ .sh-ui-header__backdrop {
134
+ display: block;
135
+ position: fixed;
136
+ inset: 0;
137
+ background: rgba(0, 0, 0, 0.25);
138
+ backdrop-filter: blur(8px);
139
+ z-index: var(--z-overlay);
140
+ opacity: 0;
141
+ pointer-events: none;
142
+ transition: opacity var(--duration-base) var(--ease-standard);
143
+ }
144
+ .sh-ui-header__backdrop[data-open] {
145
+ opacity: 1;
146
+ pointer-events: auto;
147
+ }
148
+
149
+ /* drawer panel */
150
+ .sh-ui-header__drawer {
151
+ display: block;
152
+ position: fixed;
153
+ left: 0;
154
+ top: 0;
155
+ bottom: 0;
156
+ width: min(17.5rem, 85vw);
157
+ background: var(--background-subtle);
158
+ border-right: 1px solid var(--border);
159
+ z-index: var(--z-modal);
160
+ transform: translateX(-100%);
161
+ transition: transform var(--duration-base) var(--ease-standard);
162
+ display: flex;
163
+ flex-direction: column;
164
+ overflow-y: auto;
165
+ }
166
+ .sh-ui-header__drawer[data-open] {
167
+ transform: translateX(0);
168
+ }
169
+
170
+ .sh-ui-header__drawer-head {
171
+ display: flex;
172
+ align-items: center;
173
+ justify-content: flex-end;
174
+ padding: var(--space-2) var(--space-2);
175
+ border-bottom: 1px solid var(--border);
176
+ }
177
+
178
+ .sh-ui-header__drawer-nav {
179
+ display: flex;
180
+ flex-direction: column;
181
+ padding: var(--space-2);
182
+ gap: 1px;
183
+ }
184
+
185
+ .sh-ui-header__drawer .sh-ui-header__item {
186
+ padding: var(--space-3) var(--space-3);
187
+ font-size: var(--text-sm);
188
+ border-radius: calc(var(--radius) - 2px);
189
+ }
190
+ }