sh-ui-cli 0.52.1 → 0.52.3
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 +27 -0
- package/data/registry/react/components/_smoke/vanilla-extract.test.ts +33 -0
- package/data/registry/react/components/input/styles.css.ts +6 -6
- package/data/registry/react/registry.json +35 -852
- package/package.json +1 -1
- package/src/api.d.ts +3 -4
- package/src/constants.js +9 -5
- package/src/mcp.mjs +0 -1
- package/data/registry/react/components/accordion/index.vanilla-extract.tsx +0 -97
- package/data/registry/react/components/accordion/styles.css.ts +0 -131
- package/data/registry/react/components/avatar/index.vanilla-extract.tsx +0 -73
- package/data/registry/react/components/avatar/styles.css.ts +0 -68
- package/data/registry/react/components/badge/index.vanilla-extract.tsx +0 -40
- package/data/registry/react/components/badge/styles.css.ts +0 -71
- package/data/registry/react/components/breadcrumb/index.vanilla-extract.tsx +0 -152
- package/data/registry/react/components/breadcrumb/styles.css.ts +0 -95
- package/data/registry/react/components/calendar/index.vanilla-extract.tsx +0 -806
- package/data/registry/react/components/calendar/styles.css.ts +0 -250
- package/data/registry/react/components/carousel/index.vanilla-extract.tsx +0 -430
- package/data/registry/react/components/carousel/styles.css.ts +0 -169
- package/data/registry/react/components/checkbox/index.vanilla-extract.tsx +0 -96
- package/data/registry/react/components/checkbox/styles.css.ts +0 -74
- package/data/registry/react/components/code-editor/index.vanilla-extract.tsx +0 -230
- package/data/registry/react/components/code-editor/styles.css.ts +0 -97
- package/data/registry/react/components/code-panel/index.vanilla-extract.tsx +0 -191
- package/data/registry/react/components/code-panel/styles.css.ts +0 -151
- package/data/registry/react/components/color-picker/index.vanilla-extract.tsx +0 -467
- package/data/registry/react/components/color-picker/styles.css.ts +0 -169
- package/data/registry/react/components/combobox/index.vanilla-extract.tsx +0 -165
- package/data/registry/react/components/combobox/styles.css.ts +0 -174
- package/data/registry/react/components/context-menu/index.vanilla-extract.tsx +0 -251
- package/data/registry/react/components/context-menu/styles.css.ts +0 -167
- package/data/registry/react/components/date-picker/index.vanilla-extract.tsx +0 -520
- package/data/registry/react/components/date-picker/styles.css.ts +0 -111
- package/data/registry/react/components/dialog/index.vanilla-extract.tsx +0 -95
- package/data/registry/react/components/dialog/styles.css.ts +0 -140
- package/data/registry/react/components/dropdown-menu/index.vanilla-extract.tsx +0 -255
- package/data/registry/react/components/dropdown-menu/styles.css.ts +0 -175
- package/data/registry/react/components/file-upload/index.vanilla-extract.tsx +0 -487
- package/data/registry/react/components/file-upload/styles.css.ts +0 -193
- package/data/registry/react/components/form/index.vanilla-extract.tsx +0 -61
- package/data/registry/react/components/form/styles.css.ts +0 -56
- package/data/registry/react/components/header/index.vanilla-extract.tsx +0 -805
- package/data/registry/react/components/header/styles.css.ts +0 -413
- package/data/registry/react/components/label/index.vanilla-extract.tsx +0 -52
- package/data/registry/react/components/label/styles.css.ts +0 -141
- package/data/registry/react/components/markdown-editor/index.vanilla-extract.tsx +0 -119
- package/data/registry/react/components/markdown-editor/styles.css.ts +0 -231
- package/data/registry/react/components/menubar/index.vanilla-extract.tsx +0 -32
- package/data/registry/react/components/menubar/styles.css.ts +0 -53
- package/data/registry/react/components/numeric-input/index.vanilla-extract.tsx +0 -148
- package/data/registry/react/components/numeric-input/styles.css.ts +0 -65
- package/data/registry/react/components/page-toc/index.vanilla-extract.tsx +0 -174
- package/data/registry/react/components/page-toc/styles.css.ts +0 -97
- package/data/registry/react/components/pagination/index.vanilla-extract.tsx +0 -269
- package/data/registry/react/components/pagination/styles.css.ts +0 -113
- package/data/registry/react/components/popover/index.vanilla-extract.tsx +0 -113
- package/data/registry/react/components/popover/styles.css.ts +0 -78
- package/data/registry/react/components/progress/index.vanilla-extract.tsx +0 -54
- package/data/registry/react/components/progress/styles.css.ts +0 -53
- package/data/registry/react/components/radio/index.vanilla-extract.tsx +0 -65
- package/data/registry/react/components/radio/styles.css.ts +0 -79
- package/data/registry/react/components/rich-text-editor/index.vanilla-extract.tsx +0 -348
- package/data/registry/react/components/rich-text-editor/styles.css.ts +0 -243
- package/data/registry/react/components/select/index.vanilla-extract.tsx +0 -234
- package/data/registry/react/components/select/styles.css.ts +0 -225
- package/data/registry/react/components/separator/index.vanilla-extract.tsx +0 -46
- package/data/registry/react/components/separator/styles.css.ts +0 -24
- package/data/registry/react/components/sidebar/index.vanilla-extract.tsx +0 -1067
- package/data/registry/react/components/sidebar/styles.css.ts +0 -578
- package/data/registry/react/components/skeleton/index.vanilla-extract.tsx +0 -22
- package/data/registry/react/components/skeleton/styles.css.ts +0 -30
- package/data/registry/react/components/slider/index.vanilla-extract.tsx +0 -298
- package/data/registry/react/components/slider/styles.css.ts +0 -75
- package/data/registry/react/components/spinner/index.vanilla-extract.tsx +0 -38
- package/data/registry/react/components/spinner/styles.css.ts +0 -60
- package/data/registry/react/components/switch/index.vanilla-extract.tsx +0 -39
- package/data/registry/react/components/switch/styles.css.ts +0 -87
- package/data/registry/react/components/tabs/index.vanilla-extract.tsx +0 -91
- package/data/registry/react/components/tabs/styles.css.ts +0 -145
- package/data/registry/react/components/textarea/index.vanilla-extract.tsx +0 -23
- package/data/registry/react/components/textarea/styles.css.ts +0 -55
- package/data/registry/react/components/toast/index.vanilla-extract.tsx +0 -258
- package/data/registry/react/components/toast/styles.css.ts +0 -307
- package/data/registry/react/components/toggle/index.vanilla-extract.tsx +0 -131
- package/data/registry/react/components/toggle/styles.css.ts +0 -109
- package/data/registry/react/components/tooltip/index.vanilla-extract.tsx +0 -83
- package/data/registry/react/components/tooltip/styles.css.ts +0 -59
|
@@ -1,467 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import { cn } from "@SH_UI_UTILS@";
|
|
5
|
-
import { byKey, colorPicker, colorPickerSv, colorPickerSvSaturation, colorPickerSvValue, colorPickerSvThumb, colorPickerHue, colorPickerHueThumb, colorPickerAlpha, colorPickerAlphaTrack, colorPickerRow, colorPickerSwatch, colorPickerHex, colorPickerSwatches, colorPickerSwatchBtn } from "./styles.css";
|
|
6
|
-
|
|
7
|
-
/* ───────────── types ───────────── */
|
|
8
|
-
|
|
9
|
-
interface HSV {
|
|
10
|
-
h: number; // 0~360
|
|
11
|
-
s: number; // 0~1
|
|
12
|
-
v: number; // 0~1
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface HSVA extends HSV {
|
|
16
|
-
a: number; // 0~1
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface ColorPickerProps
|
|
20
|
-
extends Omit<
|
|
21
|
-
React.HTMLAttributes<HTMLDivElement>,
|
|
22
|
-
"onChange" | "defaultValue" | "children"
|
|
23
|
-
> {
|
|
24
|
-
/** 제어 모드 색상값 (hex, 예: `"#FF8800"`). 6자리 / 3자리 / `#` 생략 모두 허용. */
|
|
25
|
-
value?: string;
|
|
26
|
-
/** 색상 변경 콜백. 항상 6자리 대문자 hex(`"#RRGGBB"`)로 통일되어 전달된다. */
|
|
27
|
-
onChange?: (hex: string) => void;
|
|
28
|
-
/**
|
|
29
|
-
* 비제어 모드 초기값.
|
|
30
|
-
* @default "#000000"
|
|
31
|
-
*/
|
|
32
|
-
defaultValue?: string;
|
|
33
|
-
/**
|
|
34
|
-
* compound 모드. 미지정 시 기본 레이아웃(Saturation + Hue + Hex)이 자동 렌더된다.
|
|
35
|
-
* 직접 조립하려면 `ColorPickerSaturation`/`Hue`/`Alpha`/`Hex`/`Swatches`를 자식으로 넘긴다.
|
|
36
|
-
*/
|
|
37
|
-
children?: React.ReactNode;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/* ───────────── color math ───────────── */
|
|
41
|
-
|
|
42
|
-
function clamp(n: number, min: number, max: number) {
|
|
43
|
-
return Math.min(max, Math.max(min, n));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function hexToRgb(hex: string): [number, number, number] {
|
|
47
|
-
const m = hex.replace("#", "");
|
|
48
|
-
const full = m.length === 3 ? m.split("").map((c) => c + c).join("") : m;
|
|
49
|
-
const n = parseInt(full, 16);
|
|
50
|
-
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function rgbToHex(r: number, g: number, b: number): string {
|
|
54
|
-
const toHex = (n: number) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0");
|
|
55
|
-
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function rgbToHsv(r: number, g: number, b: number): HSV {
|
|
59
|
-
const rn = r / 255, gn = g / 255, bn = b / 255;
|
|
60
|
-
const max = Math.max(rn, gn, bn);
|
|
61
|
-
const min = Math.min(rn, gn, bn);
|
|
62
|
-
const d = max - min;
|
|
63
|
-
let h = 0;
|
|
64
|
-
if (d !== 0) {
|
|
65
|
-
if (max === rn) h = ((gn - bn) / d) % 6;
|
|
66
|
-
else if (max === gn) h = (bn - rn) / d + 2;
|
|
67
|
-
else h = (rn - gn) / d + 4;
|
|
68
|
-
h *= 60;
|
|
69
|
-
if (h < 0) h += 360;
|
|
70
|
-
}
|
|
71
|
-
const s = max === 0 ? 0 : d / max;
|
|
72
|
-
const v = max;
|
|
73
|
-
return { h, s, v };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function hsvToRgb({ h, s, v }: HSV): [number, number, number] {
|
|
77
|
-
const c = v * s;
|
|
78
|
-
const hh = h / 60;
|
|
79
|
-
const x = c * (1 - Math.abs((hh % 2) - 1));
|
|
80
|
-
let r = 0, g = 0, b = 0;
|
|
81
|
-
if (hh >= 0 && hh < 1) [r, g, b] = [c, x, 0];
|
|
82
|
-
else if (hh < 2) [r, g, b] = [x, c, 0];
|
|
83
|
-
else if (hh < 3) [r, g, b] = [0, c, x];
|
|
84
|
-
else if (hh < 4) [r, g, b] = [0, x, c];
|
|
85
|
-
else if (hh < 5) [r, g, b] = [x, 0, c];
|
|
86
|
-
else [r, g, b] = [c, 0, x];
|
|
87
|
-
const m = v - c;
|
|
88
|
-
return [(r + m) * 255, (g + m) * 255, (b + m) * 255];
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function hexToHsv(hex: string): HSV {
|
|
92
|
-
const [r, g, b] = hexToRgb(hex);
|
|
93
|
-
return rgbToHsv(r, g, b);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function hsvToHex(hsv: HSV): string {
|
|
97
|
-
const [r, g, b] = hsvToRgb(hsv);
|
|
98
|
-
return rgbToHex(r, g, b);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const HEX_RE = /^#?[0-9a-f]{6}$/i;
|
|
102
|
-
|
|
103
|
-
/* ───────────── drag hook ───────────── */
|
|
104
|
-
|
|
105
|
-
function useDrag(onMove: (e: PointerEvent, el: HTMLElement) => void) {
|
|
106
|
-
const ref = React.useRef<HTMLDivElement>(null);
|
|
107
|
-
|
|
108
|
-
const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
109
|
-
const el = ref.current;
|
|
110
|
-
if (!el) return;
|
|
111
|
-
el.setPointerCapture(e.pointerId);
|
|
112
|
-
onMove(e.nativeEvent, el);
|
|
113
|
-
|
|
114
|
-
const onPointerMove = (ev: PointerEvent) => onMove(ev, el);
|
|
115
|
-
const onPointerUp = (ev: PointerEvent) => {
|
|
116
|
-
el.releasePointerCapture(ev.pointerId);
|
|
117
|
-
el.removeEventListener("pointermove", onPointerMove);
|
|
118
|
-
el.removeEventListener("pointerup", onPointerUp);
|
|
119
|
-
};
|
|
120
|
-
el.addEventListener("pointermove", onPointerMove);
|
|
121
|
-
el.addEventListener("pointerup", onPointerUp);
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
return { ref, onPointerDown };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/* ───────────── context ───────────── */
|
|
128
|
-
|
|
129
|
-
interface ColorPickerContextValue {
|
|
130
|
-
hsva: HSVA;
|
|
131
|
-
hex: string;
|
|
132
|
-
/** 현재 hue에 해당하는 순색(pure) hex. SV 배경용. */
|
|
133
|
-
pureHueHex: string;
|
|
134
|
-
setHsv: (next: Partial<HSV>) => void;
|
|
135
|
-
setAlpha: (a: number) => void;
|
|
136
|
-
commitHex: (raw: string) => boolean;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const ColorPickerContext = React.createContext<ColorPickerContextValue | null>(null);
|
|
140
|
-
|
|
141
|
-
function useColorPicker() {
|
|
142
|
-
const ctx = React.useContext(ColorPickerContext);
|
|
143
|
-
if (!ctx) {
|
|
144
|
-
throw new Error(
|
|
145
|
-
"ColorPicker 하위 컴포넌트는 <ColorPicker> 내부에서만 사용할 수 있습니다.",
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
return ctx;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/* ───────────── root ───────────── */
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* HSV 모델 기반 색상 선택기. children을 생략하면 기본 레이아웃(SV + Hue + Hex)이 자동 렌더되고,
|
|
155
|
-
* 직접 조립하려면 ColorPickerSaturation/Hue/Alpha/Hex/Swatches를 자식으로 넘긴다.
|
|
156
|
-
* 외부 노출값은 항상 6자리 대문자 hex(`#RRGGBB`).
|
|
157
|
-
*/
|
|
158
|
-
export function ColorPicker({
|
|
159
|
-
value: valueProp,
|
|
160
|
-
onChange,
|
|
161
|
-
defaultValue = "#000000",
|
|
162
|
-
className,
|
|
163
|
-
children,
|
|
164
|
-
...rest
|
|
165
|
-
}: ColorPickerProps) {
|
|
166
|
-
const isControlled = valueProp !== undefined;
|
|
167
|
-
const [internal, setInternal] = React.useState(defaultValue);
|
|
168
|
-
const value = isControlled ? valueProp! : internal;
|
|
169
|
-
|
|
170
|
-
const [hsva, setHsva] = React.useState<HSVA>(() => ({ ...hexToHsv(value), a: 1 }));
|
|
171
|
-
|
|
172
|
-
/* 외부 value 변경 시 hsv 동기화 (우리가 내놓은 hex는 무시 — 무한 루프 방지) */
|
|
173
|
-
const lastEmittedRef = React.useRef(value);
|
|
174
|
-
React.useEffect(() => {
|
|
175
|
-
if (value === lastEmittedRef.current) return;
|
|
176
|
-
setHsva((prev) => ({ ...hexToHsv(value), a: prev.a }));
|
|
177
|
-
}, [value]);
|
|
178
|
-
|
|
179
|
-
const emit = React.useCallback(
|
|
180
|
-
(next: HSVA) => {
|
|
181
|
-
const hex = hsvToHex(next);
|
|
182
|
-
lastEmittedRef.current = hex;
|
|
183
|
-
setHsva(next);
|
|
184
|
-
if (!isControlled) setInternal(hex);
|
|
185
|
-
onChange?.(hex);
|
|
186
|
-
},
|
|
187
|
-
[isControlled, onChange],
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
const setHsv = React.useCallback(
|
|
191
|
-
(partial: Partial<HSV>) => {
|
|
192
|
-
const next: HSVA = { ...hsva, ...partial };
|
|
193
|
-
const hex = hsvToHex(next);
|
|
194
|
-
lastEmittedRef.current = hex;
|
|
195
|
-
setHsva(next);
|
|
196
|
-
if (!isControlled) setInternal(hex);
|
|
197
|
-
onChange?.(hex);
|
|
198
|
-
},
|
|
199
|
-
[hsva, isControlled, onChange],
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
const setAlpha = React.useCallback((a: number) => {
|
|
203
|
-
setHsva((prev) => ({ ...prev, a: clamp(a, 0, 1) }));
|
|
204
|
-
}, []);
|
|
205
|
-
|
|
206
|
-
const commitHex = React.useCallback(
|
|
207
|
-
(raw: string) => {
|
|
208
|
-
const v = raw.trim();
|
|
209
|
-
if (!HEX_RE.test(v)) return false;
|
|
210
|
-
const normalized = (v.startsWith("#") ? v : `#${v}`).toUpperCase();
|
|
211
|
-
const nextHsv = hexToHsv(normalized);
|
|
212
|
-
emit({ ...nextHsv, a: hsva.a });
|
|
213
|
-
return true;
|
|
214
|
-
},
|
|
215
|
-
[emit, hsva.a],
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
const pureHueHex = React.useMemo(
|
|
219
|
-
() => hsvToHex({ h: hsva.h, s: 1, v: 1 }),
|
|
220
|
-
[hsva.h],
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
const ctx = React.useMemo<ColorPickerContextValue>(
|
|
224
|
-
() => ({
|
|
225
|
-
hsva,
|
|
226
|
-
hex: value,
|
|
227
|
-
pureHueHex,
|
|
228
|
-
setHsv,
|
|
229
|
-
setAlpha,
|
|
230
|
-
commitHex,
|
|
231
|
-
}),
|
|
232
|
-
[hsva, value, pureHueHex, setHsv, setAlpha, commitHex],
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
return (
|
|
236
|
-
<ColorPickerContext.Provider value={ctx}>
|
|
237
|
-
<div
|
|
238
|
-
className={cn(colorPicker, className)}
|
|
239
|
-
{...rest}
|
|
240
|
-
>
|
|
241
|
-
{children ?? (
|
|
242
|
-
<>
|
|
243
|
-
<ColorPickerSaturation />
|
|
244
|
-
<ColorPickerHue />
|
|
245
|
-
<ColorPickerHex />
|
|
246
|
-
</>
|
|
247
|
-
)}
|
|
248
|
-
</div>
|
|
249
|
-
</ColorPickerContext.Provider>
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/* ───────────── parts ───────────── */
|
|
254
|
-
|
|
255
|
-
export interface ColorPickerSaturationProps
|
|
256
|
-
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onPointerDown"> {}
|
|
257
|
-
|
|
258
|
-
/** 채도(S)와 명도(V)를 동시에 조절하는 2D 박스. 포인터 드래그로 조작. */
|
|
259
|
-
export function ColorPickerSaturation({
|
|
260
|
-
className,
|
|
261
|
-
style,
|
|
262
|
-
...rest
|
|
263
|
-
}: ColorPickerSaturationProps) {
|
|
264
|
-
const { hsva, hex, pureHueHex, setHsv } = useColorPicker();
|
|
265
|
-
const drag = useDrag((e, el) => {
|
|
266
|
-
const r = el.getBoundingClientRect();
|
|
267
|
-
const x = clamp((e.clientX - r.left) / r.width, 0, 1);
|
|
268
|
-
const y = clamp((e.clientY - r.top) / r.height, 0, 1);
|
|
269
|
-
setHsv({ s: x, v: 1 - y });
|
|
270
|
-
});
|
|
271
|
-
return (
|
|
272
|
-
<div
|
|
273
|
-
ref={drag.ref}
|
|
274
|
-
onPointerDown={drag.onPointerDown}
|
|
275
|
-
className={cn(colorPickerSv, className)}
|
|
276
|
-
style={{ background: pureHueHex, ...style }}
|
|
277
|
-
role="slider"
|
|
278
|
-
aria-label="채도/명도"
|
|
279
|
-
aria-valuemin={0}
|
|
280
|
-
aria-valuemax={100}
|
|
281
|
-
aria-valuenow={Math.round(hsva.s * 100)}
|
|
282
|
-
{...rest}
|
|
283
|
-
>
|
|
284
|
-
<div className={colorPickerSvSaturation} />
|
|
285
|
-
<div className={colorPickerSvValue} />
|
|
286
|
-
<div
|
|
287
|
-
className={colorPickerSvThumb}
|
|
288
|
-
style={{
|
|
289
|
-
left: `${hsva.s * 100}%`,
|
|
290
|
-
top: `${(1 - hsva.v) * 100}%`,
|
|
291
|
-
background: hex,
|
|
292
|
-
}}
|
|
293
|
-
/>
|
|
294
|
-
</div>
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
export interface ColorPickerHueProps
|
|
299
|
-
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onPointerDown"> {}
|
|
300
|
-
|
|
301
|
-
/** 색상(H, 0~360°) 슬라이더. 무지개 그라데이션 위에 thumb이 위치. */
|
|
302
|
-
export function ColorPickerHue({ className, ...rest }: ColorPickerHueProps) {
|
|
303
|
-
const { hsva, setHsv } = useColorPicker();
|
|
304
|
-
const drag = useDrag((e, el) => {
|
|
305
|
-
const r = el.getBoundingClientRect();
|
|
306
|
-
const x = clamp((e.clientX - r.left) / r.width, 0, 1);
|
|
307
|
-
setHsv({ h: x * 360 });
|
|
308
|
-
});
|
|
309
|
-
return (
|
|
310
|
-
<div
|
|
311
|
-
ref={drag.ref}
|
|
312
|
-
onPointerDown={drag.onPointerDown}
|
|
313
|
-
className={cn(colorPickerHue, className)}
|
|
314
|
-
role="slider"
|
|
315
|
-
aria-label="색조"
|
|
316
|
-
aria-valuemin={0}
|
|
317
|
-
aria-valuemax={360}
|
|
318
|
-
aria-valuenow={Math.round(hsva.h)}
|
|
319
|
-
{...rest}
|
|
320
|
-
>
|
|
321
|
-
<div
|
|
322
|
-
className={colorPickerHueThumb}
|
|
323
|
-
style={{ left: `${(hsva.h / 360) * 100}%` }}
|
|
324
|
-
/>
|
|
325
|
-
</div>
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
export interface ColorPickerAlphaProps
|
|
330
|
-
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onPointerDown"> {}
|
|
331
|
-
|
|
332
|
-
/** 투명도(A, 0~100%) 슬라이더. 외부에 알파를 노출하지 않는 hex 모드와는 시각 표시용. */
|
|
333
|
-
export function ColorPickerAlpha({ className, style, ...rest }: ColorPickerAlphaProps) {
|
|
334
|
-
const { hsva, hex, setAlpha } = useColorPicker();
|
|
335
|
-
const drag = useDrag((e, el) => {
|
|
336
|
-
const r = el.getBoundingClientRect();
|
|
337
|
-
const x = clamp((e.clientX - r.left) / r.width, 0, 1);
|
|
338
|
-
setAlpha(x);
|
|
339
|
-
});
|
|
340
|
-
const gradient = `linear-gradient(to right, rgba(0,0,0,0) 0%, ${hex} 100%)`;
|
|
341
|
-
return (
|
|
342
|
-
<div
|
|
343
|
-
ref={drag.ref}
|
|
344
|
-
onPointerDown={drag.onPointerDown}
|
|
345
|
-
className={cn(colorPickerAlpha, className)}
|
|
346
|
-
role="slider"
|
|
347
|
-
aria-label="투명도"
|
|
348
|
-
aria-valuemin={0}
|
|
349
|
-
aria-valuemax={100}
|
|
350
|
-
aria-valuenow={Math.round(hsva.a * 100)}
|
|
351
|
-
style={style}
|
|
352
|
-
{...rest}
|
|
353
|
-
>
|
|
354
|
-
<div
|
|
355
|
-
className={colorPickerAlphaTrack}
|
|
356
|
-
style={{ backgroundImage: gradient }}
|
|
357
|
-
/>
|
|
358
|
-
<div
|
|
359
|
-
className={colorPickerHueThumb}
|
|
360
|
-
style={{ left: `${hsva.a * 100}%` }}
|
|
361
|
-
/>
|
|
362
|
-
</div>
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
export interface ColorPickerHexProps
|
|
367
|
-
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
368
|
-
/**
|
|
369
|
-
* input 좌측에 현재 색상 미리보기 swatch 표시 여부.
|
|
370
|
-
* @default true
|
|
371
|
-
*/
|
|
372
|
-
showSwatch?: boolean;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/** Hex 직접 입력 + 좌측 swatch. blur·Enter 시 검증·커밋되며 잘못된 값은 이전 값으로 되돌린다. */
|
|
376
|
-
export function ColorPickerHex({
|
|
377
|
-
className,
|
|
378
|
-
showSwatch = true,
|
|
379
|
-
...rest
|
|
380
|
-
}: ColorPickerHexProps) {
|
|
381
|
-
const { hex, commitHex } = useColorPicker();
|
|
382
|
-
const [draft, setDraft] = React.useState(hex);
|
|
383
|
-
|
|
384
|
-
// 외부 hex 변경 시 draft 동기화 (단, 포커스 중이 아닐 때만)
|
|
385
|
-
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
386
|
-
React.useEffect(() => {
|
|
387
|
-
if (document.activeElement !== inputRef.current) setDraft(hex);
|
|
388
|
-
}, [hex]);
|
|
389
|
-
|
|
390
|
-
const onCommit = () => {
|
|
391
|
-
if (!commitHex(draft)) setDraft(hex);
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
return (
|
|
395
|
-
<div
|
|
396
|
-
className={cn(colorPickerRow, className)}
|
|
397
|
-
{...rest}
|
|
398
|
-
>
|
|
399
|
-
{showSwatch && (
|
|
400
|
-
<div
|
|
401
|
-
className={colorPickerSwatch}
|
|
402
|
-
style={{ background: hex }}
|
|
403
|
-
aria-hidden
|
|
404
|
-
/>
|
|
405
|
-
)}
|
|
406
|
-
<input
|
|
407
|
-
ref={inputRef}
|
|
408
|
-
type="text"
|
|
409
|
-
className={colorPickerHex}
|
|
410
|
-
value={draft}
|
|
411
|
-
onChange={(e) => setDraft(e.target.value)}
|
|
412
|
-
onBlur={onCommit}
|
|
413
|
-
onKeyDown={(e) => {
|
|
414
|
-
if (e.key === "Enter") {
|
|
415
|
-
e.preventDefault();
|
|
416
|
-
(e.target as HTMLInputElement).blur();
|
|
417
|
-
}
|
|
418
|
-
}}
|
|
419
|
-
spellCheck={false}
|
|
420
|
-
aria-label="Hex"
|
|
421
|
-
/>
|
|
422
|
-
</div>
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
export interface ColorPickerSwatchesProps
|
|
427
|
-
extends React.HTMLAttributes<HTMLDivElement> {
|
|
428
|
-
/**
|
|
429
|
-
* 표시할 hex 색상 목록. 항목 클릭 시 해당 색상으로 즉시 commit된다.
|
|
430
|
-
* 형식은 `"#RRGGBB"` 권장 (입력 시 대소문자는 자동 정규화).
|
|
431
|
-
*/
|
|
432
|
-
colors: string[];
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/** 미리 정의된 색상 팔레트 그리드. 각 항목 클릭 시 그 색상으로 즉시 커밋한다. */
|
|
436
|
-
export function ColorPickerSwatches({
|
|
437
|
-
className,
|
|
438
|
-
colors,
|
|
439
|
-
...rest
|
|
440
|
-
}: ColorPickerSwatchesProps) {
|
|
441
|
-
const { hex, commitHex } = useColorPicker();
|
|
442
|
-
return (
|
|
443
|
-
<div
|
|
444
|
-
role="group"
|
|
445
|
-
aria-label="미리 준비된 색상"
|
|
446
|
-
className={cn(colorPickerSwatches, className)}
|
|
447
|
-
{...rest}
|
|
448
|
-
>
|
|
449
|
-
{colors.map((c) => {
|
|
450
|
-
const normalized = c.toUpperCase();
|
|
451
|
-
const selected = normalized === hex.toUpperCase();
|
|
452
|
-
return (
|
|
453
|
-
<button
|
|
454
|
-
key={c}
|
|
455
|
-
type="button"
|
|
456
|
-
className={colorPickerSwatchBtn}
|
|
457
|
-
aria-label={c}
|
|
458
|
-
aria-pressed={selected}
|
|
459
|
-
data-selected={selected || undefined}
|
|
460
|
-
style={{ background: c }}
|
|
461
|
-
onClick={() => commitHex(c)}
|
|
462
|
-
/>
|
|
463
|
-
);
|
|
464
|
-
})}
|
|
465
|
-
</div>
|
|
466
|
-
);
|
|
467
|
-
}
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { style } from "@vanilla-extract/css";
|
|
2
|
-
|
|
3
|
-
export const colorPicker = style({
|
|
4
|
-
display: "flex",
|
|
5
|
-
flexDirection: "column",
|
|
6
|
-
gap: "0.625rem",
|
|
7
|
-
width: "100%",
|
|
8
|
-
userSelect: "none",
|
|
9
|
-
WebkitUserSelect: "none",
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
export const colorPickerSv = style({
|
|
13
|
-
position: "relative",
|
|
14
|
-
width: "100%",
|
|
15
|
-
aspectRatio: "4 / 3",
|
|
16
|
-
borderRadius: "var(--radius)",
|
|
17
|
-
cursor: "crosshair",
|
|
18
|
-
overflow: "hidden",
|
|
19
|
-
touchAction: "none",
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
export const colorPickerSvSaturation = style({
|
|
23
|
-
position: "absolute",
|
|
24
|
-
inset: 0,
|
|
25
|
-
background: "linear-gradient(to right, #fff, transparent)",
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
export const colorPickerSvValue = style({
|
|
29
|
-
position: "absolute",
|
|
30
|
-
inset: 0,
|
|
31
|
-
background: "linear-gradient(to top, #000, transparent)",
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
export const colorPickerSvThumb = style({
|
|
35
|
-
position: "absolute",
|
|
36
|
-
width: "0.875rem",
|
|
37
|
-
height: "0.875rem",
|
|
38
|
-
marginLeft: "-0.4375rem",
|
|
39
|
-
marginTop: "-0.4375rem",
|
|
40
|
-
border: "2px solid #fff",
|
|
41
|
-
borderRadius: "50%",
|
|
42
|
-
boxShadow: "0 0 0 1px rgba(0, 0, 0, 0.4)",
|
|
43
|
-
pointerEvents: "none",
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
export const colorPickerHue = style({
|
|
47
|
-
position: "relative",
|
|
48
|
-
width: "100%",
|
|
49
|
-
height: "0.875rem",
|
|
50
|
-
borderRadius: "999px",
|
|
51
|
-
cursor: "pointer",
|
|
52
|
-
touchAction: "none",
|
|
53
|
-
background: "linear-gradient(\n to right,\n #f00 0%,\n #ff0 16.66%,\n #0f0 33.33%,\n #0ff 50%,\n #00f 66.66%,\n #f0f 83.33%,\n #f00 100%\n )",
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
export const colorPickerHueThumb = style({
|
|
57
|
-
position: "absolute",
|
|
58
|
-
top: "50%",
|
|
59
|
-
width: "0.875rem",
|
|
60
|
-
height: "0.875rem",
|
|
61
|
-
marginLeft: "-0.4375rem",
|
|
62
|
-
transform: "translateY(-50%)",
|
|
63
|
-
background: "#fff",
|
|
64
|
-
borderRadius: "50%",
|
|
65
|
-
boxShadow: "0 0 0 1px rgba(0, 0, 0, 0.4)",
|
|
66
|
-
pointerEvents: "none",
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
export const colorPickerAlpha = style({
|
|
70
|
-
position: "relative",
|
|
71
|
-
width: "100%",
|
|
72
|
-
height: "0.875rem",
|
|
73
|
-
borderRadius: "999px",
|
|
74
|
-
cursor: "pointer",
|
|
75
|
-
touchAction: "none",
|
|
76
|
-
backgroundColor: "#fff",
|
|
77
|
-
backgroundImage: "linear-gradient(45deg, #ccc 25%, transparent 25%),\n linear-gradient(-45deg, #ccc 25%, transparent 25%),\n linear-gradient(45deg, transparent 75%, #ccc 75%),\n linear-gradient(-45deg, transparent 75%, #ccc 75%)",
|
|
78
|
-
backgroundSize: "8px 8px",
|
|
79
|
-
backgroundPosition: "0 0, 0 4px, 4px -4px, -4px 0",
|
|
80
|
-
overflow: "hidden",
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
export const colorPickerAlphaTrack = style({
|
|
84
|
-
position: "absolute",
|
|
85
|
-
inset: 0,
|
|
86
|
-
borderRadius: "inherit",
|
|
87
|
-
pointerEvents: "none",
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
export const colorPickerRow = style({
|
|
91
|
-
display: "flex",
|
|
92
|
-
alignItems: "center",
|
|
93
|
-
gap: "var(--space-2)",
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
export const colorPickerSwatch = style({
|
|
97
|
-
width: "1.75rem",
|
|
98
|
-
height: "1.75rem",
|
|
99
|
-
borderRadius: "calc(var(--radius) - 2px)",
|
|
100
|
-
border: "1px solid var(--border)",
|
|
101
|
-
flexShrink: 0,
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
export const colorPickerHex = style({
|
|
105
|
-
flex: 1,
|
|
106
|
-
minWidth: 0,
|
|
107
|
-
height: "1.75rem",
|
|
108
|
-
padding: "0 var(--space-2)",
|
|
109
|
-
border: "1px solid var(--border)",
|
|
110
|
-
borderRadius: "calc(var(--radius) - 2px)",
|
|
111
|
-
background: "var(--background)",
|
|
112
|
-
color: "var(--foreground)",
|
|
113
|
-
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
114
|
-
fontSize: "0.8125rem",
|
|
115
|
-
textTransform: "uppercase",
|
|
116
|
-
selectors: {
|
|
117
|
-
"&:focus": {
|
|
118
|
-
outline: "none",
|
|
119
|
-
borderColor: "var(--foreground)",
|
|
120
|
-
boxShadow: "0 0 0 1px var(--foreground)",
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
export const colorPickerSwatches = style({
|
|
126
|
-
display: "flex",
|
|
127
|
-
flexWrap: "wrap",
|
|
128
|
-
gap: "0.375rem",
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
export const colorPickerSwatchBtn = style({
|
|
132
|
-
width: "1.25rem",
|
|
133
|
-
height: "1.25rem",
|
|
134
|
-
padding: 0,
|
|
135
|
-
border: "1px solid var(--border)",
|
|
136
|
-
borderRadius: "calc(var(--radius) - 4px)",
|
|
137
|
-
cursor: "pointer",
|
|
138
|
-
transition: "transform var(--duration-fast), box-shadow var(--duration-fast)",
|
|
139
|
-
selectors: {
|
|
140
|
-
"&:hover": {
|
|
141
|
-
transform: "scale(1.08)",
|
|
142
|
-
},
|
|
143
|
-
"&:focus-visible": {
|
|
144
|
-
outline: "var(--border-width-strong) solid var(--foreground)",
|
|
145
|
-
outlineOffset: "2px",
|
|
146
|
-
},
|
|
147
|
-
"&[data-selected]": {
|
|
148
|
-
boxShadow: "0 0 0 2px var(--background), 0 0 0 3.5px var(--foreground)",
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
/** 동적 키로 클래스 참조용 — `byKey[\`badge--${variant}\`]` 같은 패턴 지원. */
|
|
154
|
-
export const byKey: Record<string, string> = {
|
|
155
|
-
"color-picker": colorPicker,
|
|
156
|
-
"color-picker__sv": colorPickerSv,
|
|
157
|
-
"color-picker__sv-saturation": colorPickerSvSaturation,
|
|
158
|
-
"color-picker__sv-value": colorPickerSvValue,
|
|
159
|
-
"color-picker__sv-thumb": colorPickerSvThumb,
|
|
160
|
-
"color-picker__hue": colorPickerHue,
|
|
161
|
-
"color-picker__hue-thumb": colorPickerHueThumb,
|
|
162
|
-
"color-picker__alpha": colorPickerAlpha,
|
|
163
|
-
"color-picker__alpha-track": colorPickerAlphaTrack,
|
|
164
|
-
"color-picker__row": colorPickerRow,
|
|
165
|
-
"color-picker__swatch": colorPickerSwatch,
|
|
166
|
-
"color-picker__hex": colorPickerHex,
|
|
167
|
-
"color-picker__swatches": colorPickerSwatches,
|
|
168
|
-
"color-picker__swatch-btn": colorPickerSwatchBtn,
|
|
169
|
-
};
|