sh-ui-cli 0.39.0 → 0.40.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 +19 -0
- package/data/registry/react/components/accordion/index.tsx +24 -10
- package/data/registry/react/components/accordion/styles.css +20 -3
- package/data/registry/react/components/numeric-input/index.tsx +150 -0
- package/data/registry/react/components/numeric-input/styles.css +56 -0
- package/data/registry/react/registry.json +16 -0
- package/data/summaries/react.json +2 -1
- package/package.json +1 -1
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$description": "sh-ui 릴리즈 노트 단일 소스. docs(React)와 showcase(Flutter)가 함께 읽는다. 새 릴리즈마다 맨 앞에 추가.",
|
|
4
4
|
"versions": [
|
|
5
|
+
{
|
|
6
|
+
"version": "0.40.0",
|
|
7
|
+
"date": "2026-04-29",
|
|
8
|
+
"title": "NumericInput 컴포넌트 + 토큰 편집기 UX 대폭 개선 (Accordion size, ShadowBuilder, XY 패드)",
|
|
9
|
+
"type": "minor",
|
|
10
|
+
"highlights": [
|
|
11
|
+
"**NumericInput** — 정식 sh-ui 컴포넌트 신설. 슬라이더 동반·토큰 편집기 같은 컴팩트 컨텍스트용 숫자 입력. type=text + inputMode=decimal + buffer state — Chrome 의 type=number select() 미지원 회피, '-'/'1.' 같은 transient 입력 허용, ArrowUp/Down step. NumberInput(폼·금액용) 과 책임 분리. `npx sh-ui-cli add numeric-input`",
|
|
12
|
+
"**Accordion size prop** — `size: 'sm' | 'md'`. sm 은 padding 8/4 + font 12px + chevron 12px 로 좁은 사이드바·다중 섹션에 적합. 페이지별 CSS override 대신 컴포넌트 레벨 variant 로 승격",
|
|
13
|
+
"**ShadowBuilder** (playground) — raw CSS `0 4px 12px rgba(...)` 직접 입력 → Figma 스타일 시각 편집기. X/Y/Blur/Spread NumericInput + ColorPicker + alpha %",
|
|
14
|
+
"**XY 패드 드래그** — ShadowBuilder 의 미리보기 박스가 ColorPicker 색공간처럼 클릭/드래그 가능. pointer 위치 → X/Y offset (1:1 px, ±50 클램프). pointer capture + touch-action: none 으로 모바일 터치 드래그도 지원",
|
|
15
|
+
"**advanced 모드 8 섹션** native details → sh-ui Accordion size=sm 으로 교체. chevron 인디케이터로 클릭 가능 신호 명확. 내보내기 섹션도 동일 패턴",
|
|
16
|
+
"**Accordion trigger hover** underline → background var(--background-muted) 틴트, disabled 가드(`:not([disabled]):not([data-disabled])`) — disabled 항목은 hover 무효과",
|
|
17
|
+
"**슬라이더 동반 키보드 입력** — 8 카테고리 31 슬라이더 + 그라데이션 9 슬라이더 옆 값 표시를 NumericInput 으로 교체. 드래그 + 키보드 입력 둘 다 지원. Slider docs 의 ControlledDemo 도 양방향 동기화로 업그레이드",
|
|
18
|
+
"**초기화 3분기** — '전체 초기화' / 'Light 초기화' / 'Dark 초기화'. 이전엔 모드별 한 버튼 (색만 reset) 또는 전체 한 버튼만 — 모드 색만 빠르게 되돌리는 use case + 모든 카테고리 nuke 둘 다 지원",
|
|
19
|
+
"**encodeTheme UTF-8 안전** — shadow/ease/gradient 가 자유 string 이라 한글 IME 입력 시 btoa(Latin1-only) InvalidCharacterError 회귀. TextEncoder UTF-8 바이트 → byte 별 String.fromCharCode → btoa 패턴으로 교체",
|
|
20
|
+
"**docs 페이지** — /components/numeric-input 신규 (server component + _demos 분리), Accordion size 예시 + API Reference 보강, '스타일 커스터마이즈' 에 hover 효과 override 안내"
|
|
21
|
+
],
|
|
22
|
+
"url": "https://github.com/sanghyeonKim0201/sh-ui/releases/tag/v0.40.0"
|
|
23
|
+
},
|
|
5
24
|
{
|
|
6
25
|
"version": "0.39.0",
|
|
7
26
|
"date": "2026-04-29",
|
|
@@ -8,16 +8,30 @@ function cx(...args: (string | undefined | false)[]) {
|
|
|
8
8
|
|
|
9
9
|
type WithStringClassName<T> = Omit<T, "className"> & { className?: string };
|
|
10
10
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
export type AccordionSize = "sm" | "md";
|
|
12
|
+
|
|
13
|
+
type AccordionProps = WithStringClassName<
|
|
14
|
+
React.ComponentPropsWithoutRef<typeof BaseAccordion.Root>
|
|
15
|
+
> & {
|
|
16
|
+
/**
|
|
17
|
+
* 트리거 + chevron + content 의 패딩·폰트 크기 묶음.
|
|
18
|
+
* - `md` (기본) — padding 16/4, font 15px, chevron 16px
|
|
19
|
+
* - `sm` — padding 8/4, font 12px, chevron 12px. 좁은 사이드바·다중 섹션에 적합.
|
|
20
|
+
* @default "md"
|
|
21
|
+
*/
|
|
22
|
+
size?: AccordionSize;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const Accordion = React.forwardRef<HTMLDivElement, AccordionProps>(
|
|
26
|
+
({ className, size = "md", ...props }, ref) => (
|
|
27
|
+
<BaseAccordion.Root
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cx("sh-ui-accordion", className)}
|
|
30
|
+
data-size={size}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
),
|
|
34
|
+
);
|
|
21
35
|
Accordion.displayName = "Accordion";
|
|
22
36
|
|
|
23
37
|
export const AccordionItem = React.forwardRef<
|
|
@@ -32,12 +32,13 @@
|
|
|
32
32
|
line-height: 1.4;
|
|
33
33
|
text-align: left;
|
|
34
34
|
cursor: pointer;
|
|
35
|
+
transition: background-color var(--duration-fast) var(--ease-standard);
|
|
35
36
|
-webkit-tap-highlight-color: transparent;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
/* hover는 enabled 일 때만. background tint — 다른 hover 효과를 원하면 className/style 로 override. */
|
|
40
|
+
.sh-ui-accordion__trigger:not([disabled]):not([data-disabled]):hover {
|
|
41
|
+
background: var(--background-muted);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
.sh-ui-accordion__trigger:focus-visible {
|
|
@@ -86,6 +87,22 @@
|
|
|
86
87
|
color: var(--foreground-muted);
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/* size="sm" — 좁은 사이드바·다중 섹션에 적합한 컴팩트 변형. */
|
|
91
|
+
.sh-ui-accordion[data-size="sm"] .sh-ui-accordion__trigger {
|
|
92
|
+
padding: var(--space-2) var(--space-1);
|
|
93
|
+
font-size: var(--text-xs);
|
|
94
|
+
line-height: 1.2;
|
|
95
|
+
}
|
|
96
|
+
.sh-ui-accordion[data-size="sm"] .sh-ui-accordion__chevron {
|
|
97
|
+
width: 12px;
|
|
98
|
+
height: 12px;
|
|
99
|
+
}
|
|
100
|
+
.sh-ui-accordion[data-size="sm"] .sh-ui-accordion__content {
|
|
101
|
+
padding: 0 var(--space-1) var(--space-2);
|
|
102
|
+
font-size: var(--text-xs);
|
|
103
|
+
line-height: 1.5;
|
|
104
|
+
}
|
|
105
|
+
|
|
89
106
|
@media (prefers-reduced-motion: reduce) {
|
|
90
107
|
.sh-ui-accordion__panel,
|
|
91
108
|
.sh-ui-accordion__chevron {
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import "./styles.css";
|
|
5
|
+
|
|
6
|
+
function cx(...args: (string | undefined | null | false)[]) {
|
|
7
|
+
return args.filter(Boolean).join(" ");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface NumericInputProps
|
|
11
|
+
extends Omit<
|
|
12
|
+
React.InputHTMLAttributes<HTMLInputElement>,
|
|
13
|
+
"value" | "defaultValue" | "onChange" | "type" | "min" | "max" | "step"
|
|
14
|
+
> {
|
|
15
|
+
/** 제어 모드 값. */
|
|
16
|
+
value?: number;
|
|
17
|
+
/** 비제어 모드 초기값. */
|
|
18
|
+
defaultValue?: number;
|
|
19
|
+
/** 값 변경 콜백. min/max 범위로 자동 clamp 된 값이 전달된다. */
|
|
20
|
+
onValueChange?: (value: number) => void;
|
|
21
|
+
/** 허용 최솟값. 입력값이 이보다 작으면 자동 clamp. */
|
|
22
|
+
min?: number;
|
|
23
|
+
/** 허용 최댓값. 입력값이 이보다 크면 자동 clamp. */
|
|
24
|
+
max?: number;
|
|
25
|
+
/** 화살표 키 step 폭. 디폴트 1. */
|
|
26
|
+
step?: number;
|
|
27
|
+
/** 값 우측에 부착할 단위 표시 (px / ms / % / ° 등). */
|
|
28
|
+
unit?: React.ReactNode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 슬라이더 동반·토큰 편집 등 컴팩트 컨텍스트에 적합한 숫자 입력.
|
|
33
|
+
*
|
|
34
|
+
* 구현 특이점:
|
|
35
|
+
* - `type="text"` + `inputMode="decimal"` — type=number 가 Chrome 에서 select() 와
|
|
36
|
+
* selectionStart/End 를 지원하지 않아 "0 위에 2 타이핑 → 02" 회귀가 발생함.
|
|
37
|
+
* text 로 바꾸고 우리가 직접 숫자 검증/클램프.
|
|
38
|
+
* - 내부 buffer state — "-", "1.", "" 같이 입력 중간 transient 상태 허용. 유효한
|
|
39
|
+
* 숫자가 되는 순간 onValueChange 즉시 호출. 포커스 잃을 때 정규화.
|
|
40
|
+
* - focus 시 setTimeout(0) → select() — mouseup 의 커서 재배치 이후에 selection
|
|
41
|
+
* 적용되도록.
|
|
42
|
+
* - ArrowUp/Down 으로 step 조정, Enter 로 blur(commit).
|
|
43
|
+
*
|
|
44
|
+
* 일반 폼 입력에는 `Input` / `NumberInput` 사용 권장.
|
|
45
|
+
*/
|
|
46
|
+
export const NumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
|
47
|
+
(
|
|
48
|
+
{
|
|
49
|
+
value,
|
|
50
|
+
defaultValue,
|
|
51
|
+
onValueChange,
|
|
52
|
+
min,
|
|
53
|
+
max,
|
|
54
|
+
step = 1,
|
|
55
|
+
unit,
|
|
56
|
+
className,
|
|
57
|
+
onFocus,
|
|
58
|
+
onBlur,
|
|
59
|
+
onKeyDown,
|
|
60
|
+
...props
|
|
61
|
+
},
|
|
62
|
+
ref,
|
|
63
|
+
) => {
|
|
64
|
+
const isControlled = value !== undefined;
|
|
65
|
+
const [internal, setInternal] = React.useState<number>(defaultValue ?? 0);
|
|
66
|
+
const current = isControlled ? value! : internal;
|
|
67
|
+
|
|
68
|
+
const [buffer, setBuffer] = React.useState<string>(() => String(current));
|
|
69
|
+
const focusedRef = React.useRef(false);
|
|
70
|
+
|
|
71
|
+
// 포커스 잡지 않은 동안 외부 value 변경되면 buffer 동기화.
|
|
72
|
+
React.useEffect(() => {
|
|
73
|
+
if (!focusedRef.current) setBuffer(String(current));
|
|
74
|
+
}, [current]);
|
|
75
|
+
|
|
76
|
+
const clamp = (n: number) => {
|
|
77
|
+
let v = n;
|
|
78
|
+
if (min !== undefined && v < min) v = min;
|
|
79
|
+
if (max !== undefined && v > max) v = max;
|
|
80
|
+
return v;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const commit = (n: number): number => {
|
|
84
|
+
const c = clamp(n);
|
|
85
|
+
if (!isControlled) setInternal(c);
|
|
86
|
+
onValueChange?.(c);
|
|
87
|
+
return c;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<span className="sh-ui-numeric-input">
|
|
92
|
+
<input
|
|
93
|
+
ref={ref}
|
|
94
|
+
type="text"
|
|
95
|
+
inputMode="decimal"
|
|
96
|
+
className={cx("sh-ui-numeric-input__input", className)}
|
|
97
|
+
value={buffer}
|
|
98
|
+
onChange={(e) => {
|
|
99
|
+
const raw = e.target.value;
|
|
100
|
+
setBuffer(raw);
|
|
101
|
+
// 입력 중간 상태("", "-", ".", "-.") 는 commit 안 함 — 사용자 타이핑 흐름 유지.
|
|
102
|
+
if (raw === "" || raw === "-" || raw === "." || raw === "-.") return;
|
|
103
|
+
const n = Number(raw);
|
|
104
|
+
if (Number.isFinite(n)) commit(n);
|
|
105
|
+
}}
|
|
106
|
+
onFocus={(e) => {
|
|
107
|
+
focusedRef.current = true;
|
|
108
|
+
const t = e.currentTarget;
|
|
109
|
+
// setTimeout 0 로 미뤄야 mouseup 의 커서 재배치 이후에 select 가 적용됨.
|
|
110
|
+
setTimeout(() => t.select(), 0);
|
|
111
|
+
onFocus?.(e);
|
|
112
|
+
}}
|
|
113
|
+
onBlur={(e) => {
|
|
114
|
+
focusedRef.current = false;
|
|
115
|
+
const n = Number(buffer);
|
|
116
|
+
if (buffer !== "" && Number.isFinite(n)) {
|
|
117
|
+
const c = commit(n);
|
|
118
|
+
setBuffer(String(c));
|
|
119
|
+
} else {
|
|
120
|
+
// 비어있거나 NaN — 마지막 유효 값으로 복원
|
|
121
|
+
setBuffer(String(current));
|
|
122
|
+
}
|
|
123
|
+
onBlur?.(e);
|
|
124
|
+
}}
|
|
125
|
+
onKeyDown={(e) => {
|
|
126
|
+
if (e.key === "ArrowUp") {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
const next = commit(current + step);
|
|
129
|
+
setBuffer(String(next));
|
|
130
|
+
} else if (e.key === "ArrowDown") {
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
const next = commit(current - step);
|
|
133
|
+
setBuffer(String(next));
|
|
134
|
+
} else if (e.key === "Enter") {
|
|
135
|
+
e.currentTarget.blur();
|
|
136
|
+
}
|
|
137
|
+
onKeyDown?.(e);
|
|
138
|
+
}}
|
|
139
|
+
{...props}
|
|
140
|
+
/>
|
|
141
|
+
{unit !== undefined && unit !== "" && (
|
|
142
|
+
<span className="sh-ui-numeric-input__unit" aria-hidden>
|
|
143
|
+
{unit}
|
|
144
|
+
</span>
|
|
145
|
+
)}
|
|
146
|
+
</span>
|
|
147
|
+
);
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
NumericInput.displayName = "NumericInput";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/* NumericInput — 컴팩트 monospace 숫자 입력. 슬라이더 동반·토큰 편집기 같은
|
|
2
|
+
좁은 영역용. 일반 폼 입력은 Input / NumberInput 사용. */
|
|
3
|
+
|
|
4
|
+
.sh-ui-numeric-input {
|
|
5
|
+
display: inline-flex;
|
|
6
|
+
align-items: baseline;
|
|
7
|
+
gap: 2px;
|
|
8
|
+
min-width: 3rem;
|
|
9
|
+
justify-content: flex-end;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.sh-ui-numeric-input__input {
|
|
13
|
+
width: 2.5rem;
|
|
14
|
+
padding: 2px 4px;
|
|
15
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
16
|
+
font-size: var(--text-xs);
|
|
17
|
+
line-height: 1.2;
|
|
18
|
+
text-align: right;
|
|
19
|
+
border: 1px solid transparent;
|
|
20
|
+
border-radius: calc(var(--radius) - 4px);
|
|
21
|
+
background: transparent;
|
|
22
|
+
color: var(--foreground);
|
|
23
|
+
appearance: textfield;
|
|
24
|
+
-moz-appearance: textfield;
|
|
25
|
+
transition: border-color var(--duration-fast) var(--ease-standard),
|
|
26
|
+
background-color var(--duration-fast) var(--ease-standard);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* WebKit 의 스피너 버튼 숨김 — 컴팩트 영역에 노이즈. 키보드 step 은 유지됨. */
|
|
30
|
+
.sh-ui-numeric-input__input::-webkit-inner-spin-button,
|
|
31
|
+
.sh-ui-numeric-input__input::-webkit-outer-spin-button {
|
|
32
|
+
-webkit-appearance: none;
|
|
33
|
+
margin: 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.sh-ui-numeric-input__input:hover:not(:disabled):not(:focus) {
|
|
37
|
+
border-color: var(--border);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.sh-ui-numeric-input__input:focus,
|
|
41
|
+
.sh-ui-numeric-input__input:focus-visible {
|
|
42
|
+
outline: none;
|
|
43
|
+
border-color: var(--foreground);
|
|
44
|
+
background: var(--background);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.sh-ui-numeric-input__input:disabled {
|
|
48
|
+
cursor: not-allowed;
|
|
49
|
+
opacity: var(--opacity-disabled);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.sh-ui-numeric-input__unit {
|
|
53
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
54
|
+
font-size: var(--text-xs);
|
|
55
|
+
color: var(--foreground-muted);
|
|
56
|
+
}
|
|
@@ -49,6 +49,22 @@
|
|
|
49
49
|
"dependencies": [],
|
|
50
50
|
"registryDependencies": []
|
|
51
51
|
},
|
|
52
|
+
"numeric-input": {
|
|
53
|
+
"name": "numeric-input",
|
|
54
|
+
"type": "component",
|
|
55
|
+
"files": [
|
|
56
|
+
{
|
|
57
|
+
"src": "components/numeric-input/index.tsx",
|
|
58
|
+
"dest": "{components}/numeric-input/index.tsx"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"src": "components/numeric-input/styles.css",
|
|
62
|
+
"dest": "{components}/numeric-input/styles.css"
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
"dependencies": [],
|
|
66
|
+
"registryDependencies": []
|
|
67
|
+
},
|
|
52
68
|
"file-upload": {
|
|
53
69
|
"name": "file-upload",
|
|
54
70
|
"type": "component",
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
"summaries": {
|
|
4
4
|
"button": "기본 버튼 — variant(primary/secondary/ghost/danger/link) + size(sm/md/lg).",
|
|
5
5
|
"card": "카드 컨테이너 — compound (Card.Header / Card.Body / Card.Footer / Card.Title / Card.Description).",
|
|
6
|
-
"input": "단일 행 텍스트 입력 — hasError 지원.",
|
|
6
|
+
"input": "단일 행 텍스트 입력 — hasError 지원. 같은 모듈에 NumberInput / PasswordInput / PhoneInput / BusinessNumberInput 변형 포함.",
|
|
7
|
+
"numeric-input": "슬라이더 동반·토큰 편집기용 컴팩트 숫자 입력 — onChange 즉시 min/max clamp, focus select-all, 단위(px/ms/%/° 등) suffix. 일반 폼 입력은 NumberInput 권장.",
|
|
7
8
|
"textarea": "여러 행 텍스트 입력 — rows, autoResize.",
|
|
8
9
|
"label": "폼 레이블 — htmlFor로 입력과 연결.",
|
|
9
10
|
"checkbox": "체크박스 — indeterminate 지원 (Base UI).",
|