sh-ui-cli 0.45.2 → 0.46.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 (98) hide show
  1. package/data/changelog/versions.json +26 -0
  2. package/data/registry/react/components/accordion/index.tailwind.tsx +5 -7
  3. package/data/registry/react/components/accordion/index.tsx +5 -7
  4. package/data/registry/react/components/avatar/index.tailwind.tsx +4 -6
  5. package/data/registry/react/components/avatar/index.tsx +4 -6
  6. package/data/registry/react/components/badge/index.tailwind.tsx +2 -4
  7. package/data/registry/react/components/badge/index.tsx +2 -4
  8. package/data/registry/react/components/breadcrumb/index.tailwind.tsx +8 -10
  9. package/data/registry/react/components/breadcrumb/index.tsx +8 -10
  10. package/data/registry/react/components/button/index.module.tsx +45 -0
  11. package/data/registry/react/components/button/index.tailwind.tsx +2 -1
  12. package/data/registry/react/components/button/index.tsx +3 -4
  13. package/data/registry/react/components/button/styles.module.css +92 -0
  14. package/data/registry/react/components/calendar/index.tailwind.tsx +10 -12
  15. package/data/registry/react/components/calendar/index.tsx +9 -11
  16. package/data/registry/react/components/card/index.module.tsx +63 -0
  17. package/data/registry/react/components/card/index.tailwind.tsx +8 -10
  18. package/data/registry/react/components/card/index.tsx +8 -10
  19. package/data/registry/react/components/card/styles.module.css +73 -0
  20. package/data/registry/react/components/carousel/index.tailwind.tsx +7 -9
  21. package/data/registry/react/components/carousel/index.tsx +7 -9
  22. package/data/registry/react/components/checkbox/index.tailwind.tsx +3 -5
  23. package/data/registry/react/components/checkbox/index.tsx +3 -5
  24. package/data/registry/react/components/code-editor/index.tailwind.tsx +2 -4
  25. package/data/registry/react/components/code-editor/index.tsx +2 -4
  26. package/data/registry/react/components/code-panel/index.tailwind.tsx +5 -7
  27. package/data/registry/react/components/code-panel/index.tsx +5 -7
  28. package/data/registry/react/components/color-picker/index.tailwind.tsx +7 -6
  29. package/data/registry/react/components/color-picker/index.tsx +7 -6
  30. package/data/registry/react/components/combobox/index.tailwind.tsx +8 -10
  31. package/data/registry/react/components/combobox/index.tsx +8 -10
  32. package/data/registry/react/components/context-menu/index.tailwind.tsx +10 -12
  33. package/data/registry/react/components/context-menu/index.tsx +10 -12
  34. package/data/registry/react/components/date-picker/index.tailwind.tsx +7 -9
  35. package/data/registry/react/components/date-picker/index.tsx +7 -9
  36. package/data/registry/react/components/dialog/index.tailwind.tsx +6 -8
  37. package/data/registry/react/components/dialog/index.tsx +6 -8
  38. package/data/registry/react/components/dropdown-menu/index.tailwind.tsx +10 -12
  39. package/data/registry/react/components/dropdown-menu/index.tsx +10 -12
  40. package/data/registry/react/components/file-upload/index.tailwind.tsx +6 -8
  41. package/data/registry/react/components/file-upload/index.tsx +6 -8
  42. package/data/registry/react/components/form/field.tailwind.tsx +2 -1
  43. package/data/registry/react/components/form/field.tsx +2 -3
  44. package/data/registry/react/components/header/index.tailwind.tsx +17 -19
  45. package/data/registry/react/components/header/index.tsx +17 -19
  46. package/data/registry/react/components/input/index.module.tsx +486 -0
  47. package/data/registry/react/components/input/index.tailwind.tsx +4 -6
  48. package/data/registry/react/components/input/index.tsx +4 -6
  49. package/data/registry/react/components/input/styles.module.css +200 -0
  50. package/data/registry/react/components/label/index.tailwind.tsx +6 -8
  51. package/data/registry/react/components/label/index.tsx +6 -8
  52. package/data/registry/react/components/markdown-editor/index.tailwind.tsx +2 -4
  53. package/data/registry/react/components/markdown-editor/index.tsx +2 -4
  54. package/data/registry/react/components/menubar/index.tailwind.tsx +2 -4
  55. package/data/registry/react/components/menubar/index.tsx +2 -4
  56. package/data/registry/react/components/numeric-input/index.tailwind.tsx +2 -4
  57. package/data/registry/react/components/numeric-input/index.tsx +2 -4
  58. package/data/registry/react/components/page-toc/index.tailwind.tsx +3 -2
  59. package/data/registry/react/components/page-toc/index.tsx +2 -3
  60. package/data/registry/react/components/pagination/index.tailwind.tsx +8 -10
  61. package/data/registry/react/components/pagination/index.tsx +8 -10
  62. package/data/registry/react/components/popover/index.tailwind.tsx +4 -6
  63. package/data/registry/react/components/popover/index.tsx +4 -6
  64. package/data/registry/react/components/progress/index.tailwind.tsx +3 -5
  65. package/data/registry/react/components/progress/index.tsx +2 -4
  66. package/data/registry/react/components/radio/index.tailwind.tsx +3 -5
  67. package/data/registry/react/components/radio/index.tsx +3 -5
  68. package/data/registry/react/components/rich-text-editor/index.tailwind.tsx +3 -5
  69. package/data/registry/react/components/rich-text-editor/index.tsx +3 -5
  70. package/data/registry/react/components/select/index.tailwind.tsx +8 -10
  71. package/data/registry/react/components/select/index.tsx +8 -10
  72. package/data/registry/react/components/separator/index.tailwind.tsx +2 -4
  73. package/data/registry/react/components/separator/index.tsx +2 -4
  74. package/data/registry/react/components/sidebar/index.tailwind.tsx +32 -43
  75. package/data/registry/react/components/sidebar/index.tsx +29 -46
  76. package/data/registry/react/components/skeleton/index.tailwind.tsx +2 -4
  77. package/data/registry/react/components/skeleton/index.tsx +2 -4
  78. package/data/registry/react/components/slider/index.tailwind.tsx +5 -7
  79. package/data/registry/react/components/slider/index.tsx +5 -7
  80. package/data/registry/react/components/spinner/index.tailwind.tsx +3 -5
  81. package/data/registry/react/components/spinner/index.tsx +2 -4
  82. package/data/registry/react/components/switch/index.tailwind.tsx +3 -5
  83. package/data/registry/react/components/switch/index.tsx +2 -4
  84. package/data/registry/react/components/tabs/index.tailwind.tsx +6 -8
  85. package/data/registry/react/components/tabs/index.tsx +6 -8
  86. package/data/registry/react/components/textarea/index.tailwind.tsx +2 -4
  87. package/data/registry/react/components/textarea/index.tsx +2 -4
  88. package/data/registry/react/components/toggle/index.tailwind.tsx +4 -6
  89. package/data/registry/react/components/toggle/index.tsx +4 -6
  90. package/data/registry/react/components/tooltip/index.tailwind.tsx +2 -4
  91. package/data/registry/react/components/tooltip/index.tsx +2 -4
  92. package/data/registry/react/lib/cn.tailwind.ts +17 -0
  93. package/data/registry/react/peer-versions.json +3 -1
  94. package/data/registry/react/registry.json +202 -43
  95. package/data/tokens/build.mjs +4 -0
  96. package/package.json +1 -1
  97. package/src/add.mjs +37 -13
  98. package/templates/ui-app-template/sh-ui.config.json +5 -0
@@ -3,6 +3,7 @@
3
3
  import * as React from "react";
4
4
  import "./styles.css";
5
5
 
6
+ import { cn } from "@SH_UI_UTILS@";
6
7
  export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "prefix"> {
7
8
  /** input 우측에 부착할 보조 노드(아이콘·단위·버튼 등). 더 많은 슬롯이 필요하면 InputGroup 사용. */
8
9
  suffix?: React.ReactNode;
@@ -10,9 +11,6 @@ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
10
11
  prefix?: React.ReactNode;
11
12
  }
12
13
 
13
- function cx(...args: (string | undefined | null | false)[]) {
14
- return args.filter(Boolean).join(" ");
15
- }
16
14
 
17
15
  /* ───────── InputGroup + InputAdornment (compound) ─────────
18
16
  * <InputGroup>
@@ -87,7 +85,7 @@ export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
87
85
  <InputGroupContext.Provider value={{ inGroup: true }}>
88
86
  <div
89
87
  ref={mergedRef}
90
- className={cx("sh-ui-input-group", className)}
88
+ className={cn("sh-ui-input-group", className)}
91
89
  data-disabled={disabled || undefined}
92
90
  aria-invalid={ariaInvalid}
93
91
  onClick={handleClick}
@@ -123,7 +121,7 @@ export const InputAdornment = React.forwardRef<
123
121
  return (
124
122
  <span
125
123
  ref={ref}
126
- className={cx("sh-ui-input-group__adornment", className)}
124
+ className={cn("sh-ui-input-group__adornment", className)}
127
125
  data-interactive={interactive || undefined}
128
126
  {...props}
129
127
  />
@@ -143,7 +141,7 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
143
141
  <input
144
142
  ref={ref}
145
143
  type={type}
146
- className={cx(
144
+ className={cn(
147
145
  "sh-ui-input",
148
146
  !!prefix && "sh-ui-input--with-prefix",
149
147
  !!suffix && "sh-ui-input--with-suffix",
@@ -0,0 +1,200 @@
1
+ .input {
2
+ display: block;
3
+ width: 100%;
4
+ height: var(--control-md);
5
+ padding: 0 var(--space-3);
6
+ background: var(--background);
7
+ color: var(--foreground);
8
+ border: 1px solid var(--border);
9
+ border-radius: var(--radius);
10
+ font-family: inherit;
11
+ font-size: var(--text-sm);
12
+ line-height: 1;
13
+ transition: border-color var(--duration-fast), box-shadow var(--duration-fast);
14
+ -webkit-tap-highlight-color: transparent;
15
+ }
16
+
17
+ /* 모바일/터치: 최소 탭 영역 + iOS 자동 줌 방지(16px) */
18
+ @media (hover: none) and (pointer: coarse) {
19
+ .input {
20
+ height: 2.75rem;
21
+ font-size: var(--text-base);
22
+ }
23
+ }
24
+
25
+ .input::placeholder {
26
+ color: var(--foreground-subtle);
27
+ }
28
+
29
+ .input:hover:not(:disabled):not(:focus) {
30
+ border-color: var(--border-strong);
31
+ }
32
+
33
+ .input:focus {
34
+ outline: none;
35
+ border-color: var(--foreground);
36
+ box-shadow: 0 0 0 1px var(--foreground);
37
+ }
38
+
39
+ .input:disabled {
40
+ opacity: var(--opacity-disabled);
41
+ cursor: not-allowed;
42
+ background: var(--background-subtle);
43
+ }
44
+
45
+ .input:read-only {
46
+ background: var(--background-subtle);
47
+ }
48
+
49
+ /* 숫자 input 화살표 제거 */
50
+ .input[type="number"]::-webkit-outer-spin-button,
51
+ .input[type="number"]::-webkit-inner-spin-button {
52
+ -webkit-appearance: none;
53
+ margin: 0;
54
+ }
55
+ .input[type="number"] {
56
+ -moz-appearance: textfield;
57
+ }
58
+
59
+ /* ───────── prefix / suffix ───────── */
60
+
61
+ .inputWrap {
62
+ position: relative;
63
+ width: 100%;
64
+ display: block;
65
+ }
66
+
67
+ .withPrefix { padding-left: var(--space-10); }
68
+ .withSuffix { padding-right: var(--space-10); }
69
+
70
+ .affix {
71
+ position: absolute;
72
+ top: 50%;
73
+ transform: translateY(-50%);
74
+ display: inline-flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ color: var(--foreground-muted);
78
+ pointer-events: none;
79
+ }
80
+ .affixPrefix { left: var(--space-3); }
81
+ .affixSuffix { right: var(--space-1); }
82
+
83
+ .affix > * { pointer-events: auto; }
84
+
85
+ /* 비밀번호 토글 버튼 */
86
+ .toggle {
87
+ display: inline-flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ width: 2rem;
91
+ height: 2rem;
92
+ padding: 0;
93
+ background: transparent;
94
+ border: none;
95
+ border-radius: calc(var(--radius) - 2px);
96
+ color: var(--foreground-muted);
97
+ cursor: pointer;
98
+ transition: color var(--duration-fast), background-color var(--duration-fast);
99
+ -webkit-tap-highlight-color: transparent;
100
+ }
101
+ .toggle:hover { color: var(--foreground); background: var(--background-muted); }
102
+ .toggle:focus-visible {
103
+ outline: var(--border-width-strong) solid var(--foreground);
104
+ outline-offset: 2px;
105
+ }
106
+
107
+ /* 에러 상태 — aria-invalid="true" 기반 */
108
+ .input[aria-invalid="true"] {
109
+ border-color: var(--danger);
110
+ }
111
+ .input[aria-invalid="true"]:focus {
112
+ box-shadow: 0 0 0 1px var(--danger);
113
+ }
114
+
115
+ /* ───────── InputGroup + InputAdornment ───────── */
116
+
117
+ .group {
118
+ display: flex;
119
+ align-items: center;
120
+ width: 100%;
121
+ min-height: var(--control-md);
122
+ padding: 0 var(--space-2);
123
+ gap: var(--space-2);
124
+ background: var(--background);
125
+ color: var(--foreground);
126
+ border: 1px solid var(--border);
127
+ border-radius: var(--radius);
128
+ transition: border-color var(--duration-fast), box-shadow var(--duration-fast);
129
+ cursor: text;
130
+ -webkit-tap-highlight-color: transparent;
131
+ }
132
+
133
+ @media (hover: none) and (pointer: coarse) {
134
+ .group {
135
+ min-height: 2.75rem;
136
+ }
137
+ }
138
+
139
+ .group:hover:not([data-disabled]):not(:focus-within) {
140
+ border-color: var(--border-strong);
141
+ }
142
+
143
+ .group:focus-within {
144
+ border-color: var(--foreground);
145
+ box-shadow: 0 0 0 1px var(--foreground);
146
+ }
147
+
148
+ .group[aria-invalid="true"] {
149
+ border-color: var(--danger);
150
+ }
151
+ .group[aria-invalid="true"]:focus-within {
152
+ box-shadow: 0 0 0 1px var(--danger);
153
+ }
154
+
155
+ .group[data-disabled] {
156
+ opacity: var(--opacity-disabled);
157
+ cursor: not-allowed;
158
+ background: var(--background-subtle);
159
+ }
160
+
161
+ /* 그룹 내부의 Input은 자체 보더/배경/포커스 링을 모두 감춘다 */
162
+ .input[data-in-group] {
163
+ flex: 1 1 auto;
164
+ min-width: 0;
165
+ height: auto;
166
+ padding: 0;
167
+ background: transparent;
168
+ border: none;
169
+ border-radius: 0;
170
+ box-shadow: none;
171
+ }
172
+
173
+ .input[data-in-group]:focus,
174
+ .input[data-in-group]:hover {
175
+ border: none;
176
+ box-shadow: none;
177
+ outline: none;
178
+ }
179
+
180
+ .input[data-in-group]:disabled {
181
+ background: transparent;
182
+ }
183
+
184
+ .inputWrap[data-in-group] {
185
+ flex: 1 1 auto;
186
+ min-width: 0;
187
+ }
188
+
189
+ .adornment {
190
+ display: inline-flex;
191
+ align-items: center;
192
+ justify-content: center;
193
+ flex: 0 0 auto;
194
+ color: var(--foreground-muted);
195
+ padding: 0 var(--space-1);
196
+ }
197
+
198
+ .adornment[data-interactive] {
199
+ padding: 0;
200
+ }
@@ -1,5 +1,6 @@
1
1
  import * as React from "react";
2
2
 
3
+ import { cn } from "@SH_UI_UTILS@";
3
4
  export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
4
5
  /**
5
6
  * 필수 필드 표시. `true`면 LabelTitle 뒤에 `*` 표시.
@@ -10,15 +11,12 @@ export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement>
10
11
  isRequired?: boolean;
11
12
  }
12
13
 
13
- function cx(...args: (string | undefined | false)[]) {
14
- return args.filter(Boolean).join(" ");
15
- }
16
14
 
17
15
  export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
18
16
  ({ className, children, isRequired, ...props }, ref) => (
19
17
  <label
20
18
  ref={ref}
21
- className={cx(
19
+ className={cn(
22
20
  "flex flex-col gap-0.5 text-[length:var(--text-sm)] font-medium leading-snug text-foreground cursor-pointer select-none not-has-[[data-sh-ui-label-part]]:block",
23
21
  // 필수 표시 — title 이 있으면 title 뒤, 없으면 label 뒤에 * 부착
24
22
  isRequired &&
@@ -38,7 +36,7 @@ export function LabelTitle({ className, ...props }: React.HTMLAttributes<HTMLSpa
38
36
  return (
39
37
  <span
40
38
  data-sh-ui-label-part="title"
41
- className={cx("font-semibold text-[length:var(--text-sm)] text-foreground", className)}
39
+ className={cn("font-semibold text-[length:var(--text-sm)] text-foreground", className)}
42
40
  {...props}
43
41
  />
44
42
  );
@@ -48,7 +46,7 @@ export function LabelSubtitle({ className, ...props }: React.HTMLAttributes<HTML
48
46
  return (
49
47
  <span
50
48
  data-sh-ui-label-part="subtitle"
51
- className={cx("font-normal text-[0.8125rem] text-foreground", className)}
49
+ className={cn("font-normal text-[0.8125rem] text-foreground", className)}
52
50
  {...props}
53
51
  />
54
52
  );
@@ -58,7 +56,7 @@ export function LabelDescription({ className, ...props }: React.HTMLAttributes<H
58
56
  return (
59
57
  <p
60
58
  data-sh-ui-label-part="description"
61
- className={cx("m-0 font-normal text-[0.8125rem] leading-snug text-foreground-muted", className)}
59
+ className={cn("m-0 font-normal text-[0.8125rem] leading-snug text-foreground-muted", className)}
62
60
  {...props}
63
61
  />
64
62
  );
@@ -68,7 +66,7 @@ export function LabelCaption({ className, ...props }: React.HTMLAttributes<HTMLP
68
66
  return (
69
67
  <p
70
68
  data-sh-ui-label-part="caption"
71
- className={cx(
69
+ className={cn(
72
70
  "m-0 font-normal text-[length:var(--text-xs)] leading-tight text-[var(--foreground-subtle,var(--foreground-muted))] opacity-75",
73
71
  className,
74
72
  )}
@@ -1,6 +1,7 @@
1
1
  import * as React from "react";
2
2
  import "./styles.css";
3
3
 
4
+ import { cn } from "@SH_UI_UTILS@";
4
5
  export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
5
6
  /**
6
7
  * 필수 필드 표시. `true`면 `::after`로 `*` 표시가 붙는다.
@@ -11,9 +12,6 @@ export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement>
11
12
  isRequired?: boolean;
12
13
  }
13
14
 
14
- function cx(...args: (string | undefined | false)[]) {
15
- return args.filter(Boolean).join(" ");
16
- }
17
15
 
18
16
  /**
19
17
  * 폼 컨트롤과 1:1로 연결되는 레이블. `htmlFor`로 컨트롤의 `id`와 매칭하거나
@@ -23,7 +21,7 @@ export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
23
21
  ({ className, children, isRequired, ...props }, ref) => (
24
22
  <label
25
23
  ref={ref}
26
- className={cx("sh-ui-label", className)}
24
+ className={cn("sh-ui-label", className)}
27
25
  data-required={isRequired || undefined}
28
26
  {...props}
29
27
  >
@@ -35,20 +33,20 @@ Label.displayName = "Label";
35
33
 
36
34
  /** Label 안의 주 라벨 텍스트. 구조적 그룹핑이 필요할 때 Label과 함께 사용. */
37
35
  export function LabelTitle({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) {
38
- return <span className={cx("sh-ui-label__title", className)} {...props} />;
36
+ return <span className={cn("sh-ui-label__title", className)} {...props} />;
39
37
  }
40
38
 
41
39
  /** 라벨 옆에 약하게 표시되는 보조 텍스트(예: "선택 사항"). */
42
40
  export function LabelSubtitle({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) {
43
- return <span className={cx("sh-ui-label__subtitle", className)} {...props} />;
41
+ return <span className={cn("sh-ui-label__subtitle", className)} {...props} />;
44
42
  }
45
43
 
46
44
  /** 라벨 아래에 붙는 안내 문구. 컨트롤과 `aria-describedby`로 연결할 것. */
47
45
  export function LabelDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
48
- return <p className={cx("sh-ui-label__description", className)} {...props} />;
46
+ return <p className={cn("sh-ui-label__description", className)} {...props} />;
49
47
  }
50
48
 
51
49
  /** 라벨 아래의 보조 캡션(예: 입력 형식 예시, 글자 수 제한). */
52
50
  export function LabelCaption({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
53
- return <p className={cx("sh-ui-label__caption", className)} {...props} />;
51
+ return <p className={cn("sh-ui-label__caption", className)} {...props} />;
54
52
  }
@@ -5,6 +5,7 @@ import ReactMarkdown from "react-markdown";
5
5
  import remarkGfm from "remark-gfm";
6
6
  import { CodeEditor } from "../code-editor";
7
7
 
8
+ import { cn } from "@SH_UI_UTILS@";
8
9
  export interface MarkdownEditorProps {
9
10
  value?: string;
10
11
  defaultValue?: string;
@@ -19,9 +20,6 @@ export interface MarkdownEditorProps {
19
20
  "aria-label"?: string;
20
21
  }
21
22
 
22
- function cx(...args: (string | undefined | false | null)[]) {
23
- return args.filter(Boolean).join(" ");
24
- }
25
23
 
26
24
  /**
27
25
  * 마크다운 에디터 (Tailwind 변종) — react-markdown 의 출력 HTML 트리에 대한
@@ -50,7 +48,7 @@ export function MarkdownEditor({
50
48
 
51
49
  return (
52
50
  <div
53
- className={cx("grid gap-[var(--space-3)]", layoutClass, className)}
51
+ className={cn("grid gap-[var(--space-3)]", layoutClass, className)}
54
52
  data-readonly={readOnly || undefined}
55
53
  >
56
54
  <div className="min-w-0">
@@ -6,6 +6,7 @@ import remarkGfm from "remark-gfm";
6
6
  import { CodeEditor } from "../code-editor";
7
7
  import "./styles.css";
8
8
 
9
+ import { cn } from "@SH_UI_UTILS@";
9
10
  export interface MarkdownEditorProps {
10
11
  /**
11
12
  * Controlled — 현재 마크다운. 명시 시 외부 상태가 진실원천.
@@ -42,9 +43,6 @@ export interface MarkdownEditorProps {
42
43
  "aria-label"?: string;
43
44
  }
44
45
 
45
- function cx(...args: (string | undefined | false | null)[]) {
46
- return args.filter(Boolean).join(" ");
47
- }
48
46
 
49
47
  /**
50
48
  * 마크다운 에디터 — CodeEditor(소스) + react-markdown(라이브 프리뷰)의 합성.
@@ -79,7 +77,7 @@ export function MarkdownEditor({
79
77
 
80
78
  return (
81
79
  <div
82
- className={cx(
80
+ className={cn(
83
81
  "sh-ui-md-editor",
84
82
  preview && `sh-ui-md-editor--${previewPosition}`,
85
83
  !preview && "sh-ui-md-editor--no-preview",
@@ -1,11 +1,9 @@
1
1
  import * as React from "react";
2
2
  import { Menubar as BaseMenubar } from "@base-ui/react/menubar";
3
3
 
4
+ import { cn } from "@SH_UI_UTILS@";
4
5
  type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
5
6
 
6
- function cx(...args: (string | undefined | false | null)[]) {
7
- return args.filter(Boolean).join(" ");
8
- }
9
7
 
10
8
  /**
11
9
  * 상단 앱 메뉴바 (Tailwind 변종). DropdownMenu 와 함께 사용 — DropdownMenu 의
@@ -22,7 +20,7 @@ export const Menubar = React.forwardRef<
22
20
  return (
23
21
  <BaseMenubar
24
22
  ref={ref}
25
- className={cx(
23
+ className={cn(
26
24
  "inline-flex items-center gap-[var(--space-1)] p-[var(--space-1)] bg-background border border-border rounded-[var(--radius)] shadow-[0_1px_2px_rgba(0,0,0,0.04)]",
27
25
  className,
28
26
  )}
@@ -2,11 +2,9 @@ import * as React from "react";
2
2
  import { Menubar as BaseMenubar } from "@base-ui/react/menubar";
3
3
  import "./styles.css";
4
4
 
5
+ import { cn } from "@SH_UI_UTILS@";
5
6
  type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
6
7
 
7
- function cx(...args: (string | undefined | false | null)[]) {
8
- return args.filter(Boolean).join(" ");
9
- }
10
8
 
11
9
  /**
12
10
  * 상단 앱 메뉴바(파일/편집/보기 등). 내부에 DropdownMenu를 나란히 배치하여
@@ -27,7 +25,7 @@ export const Menubar = React.forwardRef<
27
25
  return (
28
26
  <BaseMenubar
29
27
  ref={ref}
30
- className={cx("sh-ui-menubar", className)}
28
+ className={cn("sh-ui-menubar", className)}
31
29
  {...props}
32
30
  />
33
31
  );
@@ -2,10 +2,8 @@
2
2
 
3
3
  import * as React from "react";
4
4
 
5
- function cx(...args: (string | undefined | null | false)[]) {
6
- return args.filter(Boolean).join(" ");
7
- }
8
5
 
6
+ import { cn } from "@SH_UI_UTILS@";
9
7
  export interface NumericInputProps
10
8
  extends Omit<
11
9
  React.InputHTMLAttributes<HTMLInputElement>,
@@ -59,7 +57,7 @@ export const NumericInput = React.forwardRef<HTMLInputElement, NumericInputProps
59
57
  ref={ref}
60
58
  type="text"
61
59
  inputMode="decimal"
62
- className={cx(inputClasses, className)}
60
+ className={cn(inputClasses, className)}
63
61
  value={buffer}
64
62
  onChange={(e) => {
65
63
  const raw = e.target.value;
@@ -3,10 +3,8 @@
3
3
  import * as React from "react";
4
4
  import "./styles.css";
5
5
 
6
- function cx(...args: (string | undefined | null | false)[]) {
7
- return args.filter(Boolean).join(" ");
8
- }
9
6
 
7
+ import { cn } from "@SH_UI_UTILS@";
10
8
  export interface NumericInputProps
11
9
  extends Omit<
12
10
  React.InputHTMLAttributes<HTMLInputElement>,
@@ -93,7 +91,7 @@ export const NumericInput = React.forwardRef<HTMLInputElement, NumericInputProps
93
91
  ref={ref}
94
92
  type="text"
95
93
  inputMode="decimal"
96
- className={cx("sh-ui-numeric-input__input", className)}
94
+ className={cn("sh-ui-numeric-input__input", className)}
97
95
  value={buffer}
98
96
  onChange={(e) => {
99
97
  const raw = e.target.value;
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as React from "react";
4
4
 
5
+ import { cn } from "@SH_UI_UTILS@";
5
6
  export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
6
7
 
7
8
  export interface PageTOCProps {
@@ -120,7 +121,7 @@ export function PageTOC({
120
121
 
121
122
  return (
122
123
  <nav
123
- className={cx(
124
+ className={cn(
124
125
  "fixed top-20 right-6 w-56 max-h-[calc(100vh-7rem)] overflow-y-auto pl-4 pr-2 py-3 border-l border-border text-[0.8125rem] z-[5] max-[80rem]:hidden",
125
126
  className,
126
127
  )}
@@ -135,7 +136,7 @@ export function PageTOC({
135
136
  <a
136
137
  href={`#${item.id}`}
137
138
  onClick={(e) => handleClick(e, item.id)}
138
- className={cx(linkBase, linkClassesForLevel(item.level))}
139
+ className={cn(linkBase, linkClassesForLevel(item.level))}
139
140
  data-active={activeId === item.id ? "true" : undefined}
140
141
  aria-current={activeId === item.id ? "true" : undefined}
141
142
  >
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import * as React from "react";
4
+ import { cn } from "@SH_UI_UTILS@";
4
5
  import "./styles.css";
5
6
 
6
7
  export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
@@ -52,8 +53,6 @@ interface TocItem {
52
53
  level: HeadingLevel;
53
54
  }
54
55
 
55
- const cx = (...args: (string | undefined | false | null)[]) =>
56
- args.filter(Boolean).join(" ");
57
56
 
58
57
  /**
59
58
  * 페이지 내 자동 목차 (On this page).
@@ -151,7 +150,7 @@ export function PageTOC({
151
150
 
152
151
  return (
153
152
  <nav
154
- className={cx("sh-ui-page-toc", className)}
153
+ className={cn("sh-ui-page-toc", className)}
155
154
  aria-label={typeof label === "string" ? label : "목차"}
156
155
  >
157
156
  <div className="sh-ui-page-toc__label">{label}</div>
@@ -1,16 +1,14 @@
1
1
  import * as React from "react";
2
2
 
3
- function cx(...args: (string | undefined | false | null)[]) {
4
- return args.filter(Boolean).join(" ");
5
- }
6
3
 
4
+ import { cn } from "@SH_UI_UTILS@";
7
5
  export const Pagination = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement>>(
8
6
  function Pagination({ className, ...props }, ref) {
9
7
  return (
10
8
  <nav
11
9
  ref={ref}
12
10
  aria-label="Pagination"
13
- className={cx("flex justify-center text-[length:var(--text-sm)] text-foreground", className)}
11
+ className={cn("flex justify-center text-[length:var(--text-sm)] text-foreground", className)}
14
12
  {...props}
15
13
  />
16
14
  );
@@ -22,7 +20,7 @@ export const PaginationContent = React.forwardRef<HTMLUListElement, React.HTMLAt
22
20
  return (
23
21
  <ul
24
22
  ref={ref}
25
- className={cx("flex flex-wrap items-center gap-1 m-0 p-0 list-none", className)}
23
+ className={cn("flex flex-wrap items-center gap-1 m-0 p-0 list-none", className)}
26
24
  {...props}
27
25
  />
28
26
  );
@@ -31,7 +29,7 @@ export const PaginationContent = React.forwardRef<HTMLUListElement, React.HTMLAt
31
29
 
32
30
  export const PaginationItem = React.forwardRef<HTMLLIElement, React.LiHTMLAttributes<HTMLLIElement>>(
33
31
  function PaginationItem({ className, ...props }, ref) {
34
- return <li ref={ref} className={cx("inline-flex items-center", className)} {...props} />;
32
+ return <li ref={ref} className={cn("inline-flex items-center", className)} {...props} />;
35
33
  },
36
34
  );
37
35
 
@@ -51,7 +49,7 @@ export const PaginationLink = React.forwardRef<HTMLAnchorElement, PaginationLink
51
49
  aria-current={isActive ? "page" : undefined}
52
50
  data-active={isActive ? "" : undefined}
53
51
  data-size={size}
54
- className={cx(linkBase, className)}
52
+ className={cn(linkBase, className)}
55
53
  {...props}
56
54
  />
57
55
  );
@@ -61,7 +59,7 @@ export const PaginationLink = React.forwardRef<HTMLAnchorElement, PaginationLink
61
59
  export const PaginationPrevious = React.forwardRef<HTMLAnchorElement, PaginationLinkProps>(
62
60
  function PaginationPrevious({ className, children, ...props }, ref) {
63
61
  return (
64
- <PaginationLink ref={ref} aria-label="이전 페이지" className={cx("px-2.5", className)} {...props}>
62
+ <PaginationLink ref={ref} aria-label="이전 페이지" className={cn("px-2.5", className)} {...props}>
65
63
  <ChevronLeftIcon />
66
64
  {children ?? <span>이전</span>}
67
65
  </PaginationLink>
@@ -72,7 +70,7 @@ export const PaginationPrevious = React.forwardRef<HTMLAnchorElement, Pagination
72
70
  export const PaginationNext = React.forwardRef<HTMLAnchorElement, PaginationLinkProps>(
73
71
  function PaginationNext({ className, children, ...props }, ref) {
74
72
  return (
75
- <PaginationLink ref={ref} aria-label="다음 페이지" className={cx("px-2.5", className)} {...props}>
73
+ <PaginationLink ref={ref} aria-label="다음 페이지" className={cn("px-2.5", className)} {...props}>
76
74
  {children ?? <span>다음</span>}
77
75
  <ChevronRightIcon />
78
76
  </PaginationLink>
@@ -87,7 +85,7 @@ export const PaginationEllipsis = React.forwardRef<HTMLSpanElement, React.HTMLAt
87
85
  ref={ref}
88
86
  role="presentation"
89
87
  aria-hidden="true"
90
- className={cx("inline-flex items-center justify-center w-9 h-9 text-foreground-muted", className)}
88
+ className={cn("inline-flex items-center justify-center w-9 h-9 text-foreground-muted", className)}
91
89
  {...props}
92
90
  >
93
91
  <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden>