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.
- package/data/changelog/versions.json +26 -0
- package/data/registry/react/components/accordion/index.tailwind.tsx +5 -7
- package/data/registry/react/components/accordion/index.tsx +5 -7
- package/data/registry/react/components/avatar/index.tailwind.tsx +4 -6
- package/data/registry/react/components/avatar/index.tsx +4 -6
- package/data/registry/react/components/badge/index.tailwind.tsx +2 -4
- package/data/registry/react/components/badge/index.tsx +2 -4
- package/data/registry/react/components/breadcrumb/index.tailwind.tsx +8 -10
- package/data/registry/react/components/breadcrumb/index.tsx +8 -10
- package/data/registry/react/components/button/index.module.tsx +45 -0
- package/data/registry/react/components/button/index.tailwind.tsx +2 -1
- package/data/registry/react/components/button/index.tsx +3 -4
- package/data/registry/react/components/button/styles.module.css +92 -0
- package/data/registry/react/components/calendar/index.tailwind.tsx +10 -12
- package/data/registry/react/components/calendar/index.tsx +9 -11
- package/data/registry/react/components/card/index.module.tsx +63 -0
- package/data/registry/react/components/card/index.tailwind.tsx +8 -10
- package/data/registry/react/components/card/index.tsx +8 -10
- package/data/registry/react/components/card/styles.module.css +73 -0
- package/data/registry/react/components/carousel/index.tailwind.tsx +7 -9
- package/data/registry/react/components/carousel/index.tsx +7 -9
- package/data/registry/react/components/checkbox/index.tailwind.tsx +3 -5
- package/data/registry/react/components/checkbox/index.tsx +3 -5
- package/data/registry/react/components/code-editor/index.tailwind.tsx +2 -4
- package/data/registry/react/components/code-editor/index.tsx +2 -4
- package/data/registry/react/components/code-panel/index.tailwind.tsx +5 -7
- package/data/registry/react/components/code-panel/index.tsx +5 -7
- package/data/registry/react/components/color-picker/index.tailwind.tsx +7 -6
- package/data/registry/react/components/color-picker/index.tsx +7 -6
- package/data/registry/react/components/combobox/index.tailwind.tsx +8 -10
- package/data/registry/react/components/combobox/index.tsx +8 -10
- package/data/registry/react/components/context-menu/index.tailwind.tsx +10 -12
- package/data/registry/react/components/context-menu/index.tsx +10 -12
- package/data/registry/react/components/date-picker/index.tailwind.tsx +7 -9
- package/data/registry/react/components/date-picker/index.tsx +7 -9
- package/data/registry/react/components/dialog/index.tailwind.tsx +6 -8
- package/data/registry/react/components/dialog/index.tsx +6 -8
- package/data/registry/react/components/dropdown-menu/index.tailwind.tsx +10 -12
- package/data/registry/react/components/dropdown-menu/index.tsx +10 -12
- package/data/registry/react/components/file-upload/index.tailwind.tsx +6 -8
- package/data/registry/react/components/file-upload/index.tsx +6 -8
- package/data/registry/react/components/form/field.tailwind.tsx +2 -1
- package/data/registry/react/components/form/field.tsx +2 -3
- package/data/registry/react/components/header/index.tailwind.tsx +17 -19
- package/data/registry/react/components/header/index.tsx +17 -19
- package/data/registry/react/components/input/index.module.tsx +486 -0
- package/data/registry/react/components/input/index.tailwind.tsx +4 -6
- package/data/registry/react/components/input/index.tsx +4 -6
- package/data/registry/react/components/input/styles.module.css +200 -0
- package/data/registry/react/components/label/index.tailwind.tsx +6 -8
- package/data/registry/react/components/label/index.tsx +6 -8
- package/data/registry/react/components/markdown-editor/index.tailwind.tsx +2 -4
- package/data/registry/react/components/markdown-editor/index.tsx +2 -4
- package/data/registry/react/components/menubar/index.tailwind.tsx +2 -4
- package/data/registry/react/components/menubar/index.tsx +2 -4
- package/data/registry/react/components/numeric-input/index.tailwind.tsx +2 -4
- package/data/registry/react/components/numeric-input/index.tsx +2 -4
- package/data/registry/react/components/page-toc/index.tailwind.tsx +3 -2
- package/data/registry/react/components/page-toc/index.tsx +2 -3
- package/data/registry/react/components/pagination/index.tailwind.tsx +8 -10
- package/data/registry/react/components/pagination/index.tsx +8 -10
- package/data/registry/react/components/popover/index.tailwind.tsx +4 -6
- package/data/registry/react/components/popover/index.tsx +4 -6
- package/data/registry/react/components/progress/index.tailwind.tsx +3 -5
- package/data/registry/react/components/progress/index.tsx +2 -4
- package/data/registry/react/components/radio/index.tailwind.tsx +3 -5
- package/data/registry/react/components/radio/index.tsx +3 -5
- package/data/registry/react/components/rich-text-editor/index.tailwind.tsx +3 -5
- package/data/registry/react/components/rich-text-editor/index.tsx +3 -5
- package/data/registry/react/components/select/index.tailwind.tsx +8 -10
- package/data/registry/react/components/select/index.tsx +8 -10
- package/data/registry/react/components/separator/index.tailwind.tsx +2 -4
- package/data/registry/react/components/separator/index.tsx +2 -4
- package/data/registry/react/components/sidebar/index.tailwind.tsx +32 -43
- package/data/registry/react/components/sidebar/index.tsx +29 -46
- package/data/registry/react/components/skeleton/index.tailwind.tsx +2 -4
- package/data/registry/react/components/skeleton/index.tsx +2 -4
- package/data/registry/react/components/slider/index.tailwind.tsx +5 -7
- package/data/registry/react/components/slider/index.tsx +5 -7
- package/data/registry/react/components/spinner/index.tailwind.tsx +3 -5
- package/data/registry/react/components/spinner/index.tsx +2 -4
- package/data/registry/react/components/switch/index.tailwind.tsx +3 -5
- package/data/registry/react/components/switch/index.tsx +2 -4
- package/data/registry/react/components/tabs/index.tailwind.tsx +6 -8
- package/data/registry/react/components/tabs/index.tsx +6 -8
- package/data/registry/react/components/textarea/index.tailwind.tsx +2 -4
- package/data/registry/react/components/textarea/index.tsx +2 -4
- package/data/registry/react/components/toggle/index.tailwind.tsx +4 -6
- package/data/registry/react/components/toggle/index.tsx +4 -6
- package/data/registry/react/components/tooltip/index.tailwind.tsx +2 -4
- package/data/registry/react/components/tooltip/index.tsx +2 -4
- package/data/registry/react/lib/cn.tailwind.ts +17 -0
- package/data/registry/react/peer-versions.json +3 -1
- package/data/registry/react/registry.json +202 -43
- package/data/tokens/build.mjs +4 -0
- package/package.json +1 -1
- package/src/add.mjs +37 -13
- 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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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={
|
|
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>
|