sh-ui-cli 0.48.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/data/changelog/versions.json +27 -0
  2. package/data/registry/react/components/accordion/index.vanilla-extract.tsx +97 -0
  3. package/data/registry/react/components/accordion/styles.css.ts +131 -0
  4. package/data/registry/react/components/avatar/index.vanilla-extract.tsx +73 -0
  5. package/data/registry/react/components/avatar/styles.css.ts +68 -0
  6. package/data/registry/react/components/badge/index.vanilla-extract.tsx +40 -0
  7. package/data/registry/react/components/badge/styles.css.ts +71 -0
  8. package/data/registry/react/components/breadcrumb/index.vanilla-extract.tsx +152 -0
  9. package/data/registry/react/components/breadcrumb/styles.css.ts +95 -0
  10. package/data/registry/react/components/button/index.vanilla-extract.tsx +45 -0
  11. package/data/registry/react/components/button/styles.css.ts +120 -0
  12. package/data/registry/react/components/calendar/index.vanilla-extract.tsx +806 -0
  13. package/data/registry/react/components/calendar/styles.css.ts +250 -0
  14. package/data/registry/react/components/card/index.vanilla-extract.tsx +63 -0
  15. package/data/registry/react/components/card/styles.css.ts +88 -0
  16. package/data/registry/react/components/carousel/index.vanilla-extract.tsx +430 -0
  17. package/data/registry/react/components/carousel/styles.css.ts +169 -0
  18. package/data/registry/react/components/checkbox/index.vanilla-extract.tsx +96 -0
  19. package/data/registry/react/components/checkbox/styles.css.ts +74 -0
  20. package/data/registry/react/components/code-editor/index.vanilla-extract.tsx +230 -0
  21. package/data/registry/react/components/code-editor/styles.css.ts +97 -0
  22. package/data/registry/react/components/code-panel/index.vanilla-extract.tsx +191 -0
  23. package/data/registry/react/components/code-panel/styles.css.ts +151 -0
  24. package/data/registry/react/components/color-picker/index.vanilla-extract.tsx +467 -0
  25. package/data/registry/react/components/color-picker/styles.css.ts +169 -0
  26. package/data/registry/react/components/combobox/index.vanilla-extract.tsx +165 -0
  27. package/data/registry/react/components/combobox/styles.css.ts +174 -0
  28. package/data/registry/react/components/context-menu/index.vanilla-extract.tsx +251 -0
  29. package/data/registry/react/components/context-menu/styles.css.ts +167 -0
  30. package/data/registry/react/components/date-picker/index.vanilla-extract.tsx +520 -0
  31. package/data/registry/react/components/date-picker/styles.css.ts +111 -0
  32. package/data/registry/react/components/dialog/index.vanilla-extract.tsx +95 -0
  33. package/data/registry/react/components/dialog/styles.css.ts +140 -0
  34. package/data/registry/react/components/dropdown-menu/index.vanilla-extract.tsx +255 -0
  35. package/data/registry/react/components/dropdown-menu/styles.css.ts +175 -0
  36. package/data/registry/react/components/file-upload/index.vanilla-extract.tsx +487 -0
  37. package/data/registry/react/components/file-upload/styles.css.ts +193 -0
  38. package/data/registry/react/components/form/index.vanilla-extract.tsx +61 -0
  39. package/data/registry/react/components/form/styles.css.ts +56 -0
  40. package/data/registry/react/components/header/index.vanilla-extract.tsx +805 -0
  41. package/data/registry/react/components/header/styles.css.ts +413 -0
  42. package/data/registry/react/components/input/index.vanilla-extract.tsx +425 -0
  43. package/data/registry/react/components/input/styles.css.ts +202 -0
  44. package/data/registry/react/components/label/index.vanilla-extract.tsx +52 -0
  45. package/data/registry/react/components/label/styles.css.ts +141 -0
  46. package/data/registry/react/components/markdown-editor/index.vanilla-extract.tsx +119 -0
  47. package/data/registry/react/components/markdown-editor/styles.css.ts +231 -0
  48. package/data/registry/react/components/menubar/index.vanilla-extract.tsx +32 -0
  49. package/data/registry/react/components/menubar/styles.css.ts +53 -0
  50. package/data/registry/react/components/numeric-input/index.vanilla-extract.tsx +148 -0
  51. package/data/registry/react/components/numeric-input/styles.css.ts +65 -0
  52. package/data/registry/react/components/page-toc/index.vanilla-extract.tsx +174 -0
  53. package/data/registry/react/components/page-toc/styles.css.ts +97 -0
  54. package/data/registry/react/components/pagination/index.vanilla-extract.tsx +269 -0
  55. package/data/registry/react/components/pagination/styles.css.ts +113 -0
  56. package/data/registry/react/components/popover/index.vanilla-extract.tsx +113 -0
  57. package/data/registry/react/components/popover/styles.css.ts +78 -0
  58. package/data/registry/react/components/progress/index.vanilla-extract.tsx +54 -0
  59. package/data/registry/react/components/progress/styles.css.ts +53 -0
  60. package/data/registry/react/components/radio/index.vanilla-extract.tsx +65 -0
  61. package/data/registry/react/components/radio/styles.css.ts +79 -0
  62. package/data/registry/react/components/rich-text-editor/index.vanilla-extract.tsx +348 -0
  63. package/data/registry/react/components/rich-text-editor/styles.css.ts +243 -0
  64. package/data/registry/react/components/select/index.vanilla-extract.tsx +234 -0
  65. package/data/registry/react/components/select/styles.css.ts +225 -0
  66. package/data/registry/react/components/separator/index.vanilla-extract.tsx +46 -0
  67. package/data/registry/react/components/separator/styles.css.ts +24 -0
  68. package/data/registry/react/components/sidebar/index.vanilla-extract.tsx +1067 -0
  69. package/data/registry/react/components/sidebar/styles.css.ts +578 -0
  70. package/data/registry/react/components/skeleton/index.vanilla-extract.tsx +22 -0
  71. package/data/registry/react/components/skeleton/styles.css.ts +30 -0
  72. package/data/registry/react/components/slider/index.vanilla-extract.tsx +298 -0
  73. package/data/registry/react/components/slider/styles.css.ts +75 -0
  74. package/data/registry/react/components/spinner/index.vanilla-extract.tsx +38 -0
  75. package/data/registry/react/components/spinner/styles.css.ts +60 -0
  76. package/data/registry/react/components/switch/index.vanilla-extract.tsx +39 -0
  77. package/data/registry/react/components/switch/styles.css.ts +87 -0
  78. package/data/registry/react/components/tabs/index.vanilla-extract.tsx +91 -0
  79. package/data/registry/react/components/tabs/styles.css.ts +145 -0
  80. package/data/registry/react/components/textarea/index.vanilla-extract.tsx +23 -0
  81. package/data/registry/react/components/textarea/styles.css.ts +55 -0
  82. package/data/registry/react/components/toast/index.vanilla-extract.tsx +258 -0
  83. package/data/registry/react/components/toast/styles.css.ts +307 -0
  84. package/data/registry/react/components/toggle/index.vanilla-extract.tsx +131 -0
  85. package/data/registry/react/components/toggle/styles.css.ts +109 -0
  86. package/data/registry/react/components/tooltip/index.vanilla-extract.tsx +83 -0
  87. package/data/registry/react/components/tooltip/styles.css.ts +59 -0
  88. package/data/registry/react/peer-versions.json +1 -0
  89. package/data/registry/react/registry.json +922 -42
  90. package/data/tokens/build.mjs +3 -0
  91. package/package.json +1 -1
  92. package/src/api.d.ts +4 -3
  93. package/src/constants.js +4 -3
@@ -0,0 +1,141 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const label = style({
4
+ display: "flex",
5
+ flexDirection: "column",
6
+ gap: "0.125rem",
7
+ fontSize: "var(--text-sm)",
8
+ fontWeight: "var(--weight-medium)",
9
+ lineHeight: 1.4,
10
+ color: "var(--foreground)",
11
+ cursor: "pointer",
12
+ userSelect: "none",
13
+ selectors: {
14
+ [`&:not(:has(${label__title}`]: {
15
+ display: "block",
16
+ },
17
+ [`&[data-required] > ${label__title}::after`]: {
18
+ content: "\" *\"",
19
+ color: "var(--danger)",
20
+ fontWeight: "var(--weight-semibold)",
21
+ },
22
+ [`&:has(+ .input:required) > ${label__title}::after`]: {
23
+ content: "\" *\"",
24
+ color: "var(--danger)",
25
+ fontWeight: "var(--weight-semibold)",
26
+ },
27
+ [`&:has(+ .input-wrap .input:required) > ${label__title}::after`]: {
28
+ content: "\" *\"",
29
+ color: "var(--danger)",
30
+ fontWeight: "var(--weight-semibold)",
31
+ },
32
+ [`&:has(+ .textarea:required) > ${label__title}::after`]: {
33
+ content: "\" *\"",
34
+ color: "var(--danger)",
35
+ fontWeight: "var(--weight-semibold)",
36
+ },
37
+ [`&:has(+ .combobox__input:required) > ${label__title}::after`]: {
38
+ content: "\" *\"",
39
+ color: "var(--danger)",
40
+ fontWeight: "var(--weight-semibold)",
41
+ },
42
+ [`&[data-required]:not(:has(${label__title}))::after`]: {
43
+ content: "\" *\"",
44
+ color: "var(--danger)",
45
+ fontWeight: "var(--weight-semibold)",
46
+ },
47
+ [`&:not(:has(${label__title})):has(+ .input:required)::after`]: {
48
+ content: "\" *\"",
49
+ color: "var(--danger)",
50
+ fontWeight: "var(--weight-semibold)",
51
+ },
52
+ [`&:not(:has(${label__title})):has(+ .input-wrap .input:required)::after`]: {
53
+ content: "\" *\"",
54
+ color: "var(--danger)",
55
+ fontWeight: "var(--weight-semibold)",
56
+ },
57
+ [`&:not(:has(${label__title})):has(+ .textarea:required)::after`]: {
58
+ content: "\" *\"",
59
+ color: "var(--danger)",
60
+ fontWeight: "var(--weight-semibold)",
61
+ },
62
+ [`&:not(:has(${label__title})):has(+ .combobox__input:required)::after`]: {
63
+ content: "\" *\"",
64
+ color: "var(--danger)",
65
+ fontWeight: "var(--weight-semibold)",
66
+ },
67
+ "&:has(+ .input:disabled)": {
68
+ opacity: "var(--opacity-disabled)",
69
+ cursor: "not-allowed",
70
+ },
71
+ "&:has(+ .input-wrap .input:disabled)": {
72
+ opacity: "var(--opacity-disabled)",
73
+ cursor: "not-allowed",
74
+ },
75
+ "&:has(+ .textarea:disabled)": {
76
+ opacity: "var(--opacity-disabled)",
77
+ cursor: "not-allowed",
78
+ },
79
+ "&:has(+ .select__trigger:disabled)": {
80
+ opacity: "var(--opacity-disabled)",
81
+ cursor: "not-allowed",
82
+ },
83
+ "&:has(+ .combobox__input:disabled)": {
84
+ opacity: "var(--opacity-disabled)",
85
+ cursor: "not-allowed",
86
+ },
87
+ "&:has(+ .date-picker__trigger:disabled)": {
88
+ opacity: "var(--opacity-disabled)",
89
+ cursor: "not-allowed",
90
+ },
91
+ "&:has(+ .file-upload .file-upload__dropzone--disabled)": {
92
+ opacity: "var(--opacity-disabled)",
93
+ cursor: "not-allowed",
94
+ },
95
+ },
96
+ });
97
+
98
+ export const label__subtitle = style({
99
+ display: "block",
100
+ fontWeight: "var(--weight-regular)",
101
+ fontSize: "0.8125rem",
102
+ color: "var(--foreground)",
103
+ });
104
+
105
+ export const label__description = style({
106
+ display: "block",
107
+ margin: 0,
108
+ fontWeight: "var(--weight-regular)",
109
+ fontSize: "0.8125rem",
110
+ lineHeight: 1.4,
111
+ color: "var(--foreground-muted)",
112
+ });
113
+
114
+ export const label__caption = style({
115
+ margin: 0,
116
+ fontWeight: "var(--weight-regular)",
117
+ fontSize: "var(--text-xs)",
118
+ lineHeight: 1.3,
119
+ color: "var(--foreground-subtle, var(--foreground-muted))",
120
+ opacity: 0.75,
121
+ selectors: {
122
+ "&))": {
123
+ display: "block",
124
+ },
125
+ },
126
+ });
127
+
128
+ export const label__title = style({
129
+ fontWeight: "var(--weight-semibold)",
130
+ fontSize: "var(--text-sm)",
131
+ color: "var(--foreground)",
132
+ });
133
+
134
+ /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
135
+ export const byKey: Record<string, string> = {
136
+ "label": label,
137
+ "label__subtitle": label__subtitle,
138
+ "label__description": label__description,
139
+ "label__caption": label__caption,
140
+ "label__title": label__title,
141
+ };
@@ -0,0 +1,119 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import ReactMarkdown from "react-markdown";
5
+ import remarkGfm from "remark-gfm";
6
+ import { CodeEditor } from "../code-editor";
7
+ import { byKey, mdEditor, mdEditorRight, mdEditorBottom, mdEditorNoPreview, mdEditorSource, mdEditorPreview, mdEditorPreviewInner } from "./styles.css";
8
+
9
+ import { cn } from "@SH_UI_UTILS@";
10
+ export interface MarkdownEditorProps {
11
+ /**
12
+ * Controlled — 현재 마크다운. 명시 시 외부 상태가 진실원천.
13
+ * 미지정이면 uncontrolled — 컴포넌트가 자체 내부 상태로 동작.
14
+ */
15
+ value?: string;
16
+ /**
17
+ * Uncontrolled 초기값. value 미지정 시에만 사용된다.
18
+ * @default ""
19
+ */
20
+ defaultValue?: string;
21
+ /** 마크다운이 바뀔 때마다 호출 (controlled · uncontrolled 모두). */
22
+ onChange?: (value: string) => void;
23
+ /** 비어 있을 때 표시할 placeholder. */
24
+ placeholder?: string;
25
+ /** 읽기 전용. 키 입력 차단, 미리보기는 그대로 렌더. */
26
+ readOnly?: boolean;
27
+ /**
28
+ * 미리보기 패널 표시 여부.
29
+ * @default true
30
+ */
31
+ preview?: boolean;
32
+ /**
33
+ * 미리보기 위치. 좁은 화면(<768px)에서는 항상 아래로 쌓임.
34
+ * @default "right"
35
+ */
36
+ previewPosition?: "right" | "bottom";
37
+ /** 에디터·미리보기 영역의 최소 높이 (CSS 길이 단위). */
38
+ minHeight?: string;
39
+ /** 에디터·미리보기 영역의 최대 높이. 초과 시 내부 스크롤. */
40
+ maxHeight?: string;
41
+ className?: string;
42
+ /** 에디터 영역에 부여할 aria-label. */
43
+ "aria-label"?: string;
44
+ }
45
+
46
+
47
+ /**
48
+ * 마크다운 에디터 — CodeEditor(소스) + react-markdown(라이브 프리뷰)의 합성.
49
+ *
50
+ * Controlled (value/onChange) · Uncontrolled (defaultValue) 모두 지원. 미리보기 패널이
51
+ * 현재 마크다운을 필요로 하므로 uncontrolled 모드에서도 내부 상태로 트래킹.
52
+ *
53
+ * 미리보기는 GFM(테이블·체크박스·strikethrough)을 지원하고, raw HTML은 기본적으로
54
+ * 차단(react-markdown 기본 동작)되어 사용자 입력으로부터의 XSS가 자동 방어된다.
55
+ */
56
+ export function MarkdownEditor({
57
+ value: valueProp,
58
+ defaultValue,
59
+ onChange,
60
+ placeholder,
61
+ readOnly,
62
+ preview = true,
63
+ previewPosition = "right",
64
+ minHeight,
65
+ maxHeight,
66
+ className,
67
+ "aria-label": ariaLabel = "Markdown editor",
68
+ }: MarkdownEditorProps) {
69
+ const isControlled = valueProp !== undefined;
70
+ const [internalValue, setInternalValue] = useState(valueProp ?? defaultValue ?? "");
71
+ const value = isControlled ? valueProp : internalValue;
72
+
73
+ const handleChange = (next: string) => {
74
+ if (!isControlled) setInternalValue(next);
75
+ onChange?.(next);
76
+ };
77
+
78
+ return (
79
+ <div
80
+ className={cn(
81
+ mdEditor,
82
+ preview && byKey[`md-editor--${previewPosition}`],
83
+ !preview && mdEditorNoPreview,
84
+ className,
85
+ )}
86
+ data-readonly={readOnly || undefined}
87
+ >
88
+ <div className={mdEditorSource}>
89
+ <CodeEditor
90
+ value={value}
91
+ onChange={handleChange}
92
+ language="markdown"
93
+ placeholder={placeholder}
94
+ readOnly={readOnly}
95
+ minHeight={minHeight}
96
+ maxHeight={maxHeight}
97
+ aria-label={ariaLabel}
98
+ />
99
+ </div>
100
+ {preview && (
101
+ <div
102
+ className={mdEditorPreview}
103
+ role="region"
104
+ aria-label="Preview"
105
+ style={
106
+ {
107
+ "--sh-ui-md-editor-min-height": minHeight,
108
+ "--sh-ui-md-editor-max-height": maxHeight,
109
+ } as React.CSSProperties
110
+ }
111
+ >
112
+ <div className={mdEditorPreviewInner}>
113
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{value}</ReactMarkdown>
114
+ </div>
115
+ </div>
116
+ )}
117
+ </div>
118
+ );
119
+ }
@@ -0,0 +1,231 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const mdEditor = style({
4
+ display: "grid",
5
+ gap: "var(--space-3)",
6
+ });
7
+
8
+ export const mdEditorRight = style({
9
+ gridTemplateColumns: "1fr 1fr",
10
+ "@media": {
11
+ "(max-width: 768px)": {
12
+ gridTemplateColumns: "1fr",
13
+ },
14
+ },
15
+ });
16
+
17
+ export const mdEditorBottom = style({
18
+ gridTemplateColumns: "1fr",
19
+ });
20
+
21
+ export const mdEditorNoPreview = style({
22
+ gridTemplateColumns: "1fr",
23
+ });
24
+
25
+ export const mdEditorSource = style({
26
+ minWidth: 0,
27
+ });
28
+
29
+ export const mdEditorPreview = style({
30
+ minWidth: 0,
31
+ border: "1px solid var(--border)",
32
+ borderRadius: "var(--radius)",
33
+ background: "var(--background)",
34
+ overflow: "hidden",
35
+ });
36
+
37
+ export const mdEditorPreviewInner = style({
38
+ padding: "var(--space-3) var(--space-4)",
39
+ minHeight: "var(--sh-ui-md-editor-min-height, 7.5rem)",
40
+ maxHeight: "var(--sh-ui-md-editor-max-height, 25rem)",
41
+ overflowY: "auto",
42
+ fontSize: "0.875rem",
43
+ lineHeight: 1.65,
44
+ color: "var(--foreground)",
45
+ selectors: {
46
+ "& > :first-child": {
47
+ marginTop: 0,
48
+ },
49
+ "& > :last-child": {
50
+ marginBottom: 0,
51
+ },
52
+ "& h1": {
53
+ marginTop: "var(--space-4)",
54
+ marginBottom: "var(--space-2)",
55
+ fontWeight: 600,
56
+ lineHeight: 1.3,
57
+ color: "var(--foreground)",
58
+ },
59
+ "& h2": {
60
+ marginTop: "var(--space-4)",
61
+ marginBottom: "var(--space-2)",
62
+ fontWeight: 600,
63
+ lineHeight: 1.3,
64
+ color: "var(--foreground)",
65
+ },
66
+ "& h3": {
67
+ marginTop: "var(--space-4)",
68
+ marginBottom: "var(--space-2)",
69
+ fontWeight: 600,
70
+ lineHeight: 1.3,
71
+ color: "var(--foreground)",
72
+ },
73
+ "& h4": {
74
+ marginTop: "var(--space-4)",
75
+ marginBottom: "var(--space-2)",
76
+ fontWeight: 600,
77
+ lineHeight: 1.3,
78
+ color: "var(--foreground)",
79
+ },
80
+ "& h5": {
81
+ marginTop: "var(--space-4)",
82
+ marginBottom: "var(--space-2)",
83
+ fontWeight: 600,
84
+ lineHeight: 1.3,
85
+ color: "var(--foreground)",
86
+ },
87
+ "& h6": {
88
+ marginTop: "var(--space-4)",
89
+ marginBottom: "var(--space-2)",
90
+ fontWeight: 600,
91
+ lineHeight: 1.3,
92
+ color: "var(--foreground)",
93
+ },
94
+ "& h1": {
95
+ fontSize: "1.5rem",
96
+ },
97
+ "& h2": {
98
+ fontSize: "1.25rem",
99
+ },
100
+ "& h3": {
101
+ fontSize: "1.125rem",
102
+ },
103
+ "& h4": {
104
+ fontSize: "1rem",
105
+ },
106
+ "& h5": {
107
+ fontSize: "1rem",
108
+ },
109
+ "& h6": {
110
+ fontSize: "1rem",
111
+ },
112
+ "& p": {
113
+ marginTop: 0,
114
+ marginBottom: "var(--space-3)",
115
+ },
116
+ "& ul": {
117
+ marginTop: 0,
118
+ marginBottom: "var(--space-3)",
119
+ },
120
+ "& ol": {
121
+ marginTop: 0,
122
+ marginBottom: "var(--space-3)",
123
+ },
124
+ "& blockquote": {
125
+ marginTop: 0,
126
+ marginBottom: "var(--space-3)",
127
+ },
128
+ "& pre": {
129
+ marginTop: 0,
130
+ marginBottom: "var(--space-3)",
131
+ },
132
+ "& table": {
133
+ marginTop: 0,
134
+ marginBottom: "var(--space-3)",
135
+ },
136
+ "& ul": {
137
+ paddingLeft: "var(--space-5)",
138
+ },
139
+ "& ol": {
140
+ paddingLeft: "var(--space-5)",
141
+ },
142
+ "& li": {
143
+ marginBottom: "var(--space-1)",
144
+ },
145
+ "& li > input[type="checkbox"]": {
146
+ marginRight: "var(--space-2)",
147
+ },
148
+ "& a": {
149
+ color: "var(--primary)",
150
+ textDecoration: "underline",
151
+ textUnderlineOffset: "2px",
152
+ },
153
+ "& a:hover": {
154
+ textDecorationThickness: "2px",
155
+ },
156
+ "& blockquote": {
157
+ padding: "var(--space-2) var(--space-3)",
158
+ borderLeft: "3px solid var(--border-strong)",
159
+ background: "var(--background-subtle)",
160
+ color: "var(--foreground-muted)",
161
+ borderRadius: "0 calc(var(--radius) - 2px) calc(var(--radius) - 2px) 0",
162
+ },
163
+ "& blockquote > :last-child": {
164
+ marginBottom: 0,
165
+ },
166
+ "& code": {
167
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
168
+ fontSize: "0.875em",
169
+ padding: "0.125rem 0.375rem",
170
+ borderRadius: "calc(var(--radius) - 4px)",
171
+ background: "var(--background-muted)",
172
+ color: "var(--foreground)",
173
+ },
174
+ "& pre": {
175
+ padding: "var(--space-3)",
176
+ border: "1px solid var(--border)",
177
+ borderRadius: "var(--radius)",
178
+ background: "var(--background-subtle)",
179
+ overflowX: "auto",
180
+ fontSize: "0.8125rem",
181
+ lineHeight: 1.6,
182
+ },
183
+ "& pre > code": {
184
+ padding: 0,
185
+ background: "transparent",
186
+ fontSize: "inherit",
187
+ },
188
+ "& hr": {
189
+ border: 0,
190
+ borderTop: "1px solid var(--border)",
191
+ margin: "var(--space-4) 0",
192
+ },
193
+ "& table": {
194
+ width: "100%",
195
+ borderCollapse: "collapse",
196
+ fontSize: "0.875rem",
197
+ },
198
+ "& th": {
199
+ padding: "var(--space-2) var(--space-3)",
200
+ border: "1px solid var(--border)",
201
+ textAlign: "left",
202
+ },
203
+ "& td": {
204
+ padding: "var(--space-2) var(--space-3)",
205
+ border: "1px solid var(--border)",
206
+ textAlign: "left",
207
+ },
208
+ "& thead": {
209
+ background: "var(--background-subtle)",
210
+ },
211
+ "& img": {
212
+ maxWidth: "100%",
213
+ height: "auto",
214
+ borderRadius: "calc(var(--radius) - 2px)",
215
+ },
216
+ "& del": {
217
+ color: "var(--foreground-muted)",
218
+ },
219
+ },
220
+ });
221
+
222
+ /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
223
+ export const byKey: Record<string, string> = {
224
+ "md-editor": mdEditor,
225
+ "md-editor--right": mdEditorRight,
226
+ "md-editor--bottom": mdEditorBottom,
227
+ "md-editor--no-preview": mdEditorNoPreview,
228
+ "md-editor__source": mdEditorSource,
229
+ "md-editor__preview": mdEditorPreview,
230
+ "md-editor__preview-inner": mdEditorPreviewInner,
231
+ };
@@ -0,0 +1,32 @@
1
+ import * as React from "react";
2
+ import { Menubar as BaseMenubar } from "@base-ui/react/menubar";
3
+ import { byKey, menubar } from "./styles.css";
4
+
5
+ import { cn } from "@SH_UI_UTILS@";
6
+ type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
7
+
8
+
9
+ /**
10
+ * 상단 앱 메뉴바(파일/편집/보기 등). 내부에 DropdownMenu를 나란히 배치하여
11
+ * 좌우 화살표로 메뉴 간 이동이 가능해진다.
12
+ *
13
+ * <Menubar>
14
+ * <DropdownMenu>
15
+ * <DropdownMenuTrigger>파일</DropdownMenuTrigger>
16
+ * <DropdownMenuContent>...</DropdownMenuContent>
17
+ * </DropdownMenu>
18
+ * <DropdownMenu>...</DropdownMenu>
19
+ * </Menubar>
20
+ */
21
+ export const Menubar = React.forwardRef<
22
+ HTMLDivElement,
23
+ WithStringClassName<React.ComponentPropsWithoutRef<typeof BaseMenubar>>
24
+ >(function Menubar({ className, ...props }, ref) {
25
+ return (
26
+ <BaseMenubar
27
+ ref={ref}
28
+ className={cn(menubar, className)}
29
+ {...props}
30
+ />
31
+ );
32
+ });
@@ -0,0 +1,53 @@
1
+ import { style } from "@vanilla-extract/css";
2
+
3
+ export const menubar = style({
4
+ display: "inline-flex",
5
+ alignItems: "center",
6
+ gap: "var(--space-1)",
7
+ padding: "var(--space-1)",
8
+ background: "var(--background)",
9
+ border: "1px solid var(--border)",
10
+ borderRadius: "var(--radius)",
11
+ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.04)",
12
+ selectors: {
13
+ "& .dm__trigger": {
14
+ display: "inline-flex",
15
+ alignItems: "center",
16
+ gap: "var(--space-1)",
17
+ padding: "var(--space-1) var(--space-3)",
18
+ height: "var(--control-md)",
19
+ border: 0,
20
+ borderRadius: "calc(var(--radius) - 2px)",
21
+ background: "transparent",
22
+ color: "var(--foreground)",
23
+ fontSize: "var(--text-sm)",
24
+ lineHeight: 1,
25
+ cursor: "pointer",
26
+ transition: "background-color var(--duration-fast), color var(--duration-fast)",
27
+ },
28
+ "& .dm__trigger:hover": {
29
+ background: "var(--background-muted)",
30
+ },
31
+ "& .dm__trigger[data-popup-open]": {
32
+ background: "var(--background-muted)",
33
+ },
34
+ "& .dm__trigger:focus-visible": {
35
+ outline: "var(--border-width-strong) solid var(--foreground)",
36
+ outlineOffset: "-1px",
37
+ },
38
+ },
39
+ "@media": {
40
+ "(prefers-reduced-motion: reduce)": {
41
+ selectors: {
42
+ "& .dm__trigger": {
43
+ transition: "none",
44
+ },
45
+ },
46
+ },
47
+ },
48
+ });
49
+
50
+ /** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
51
+ export const byKey: Record<string, string> = {
52
+ "menubar": menubar,
53
+ };