@vrnclr/ui 0.0.1

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/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@vrnclr/ui",
3
+ "version": "0.0.1",
4
+ "main": "./dist/index.js",
5
+ "module": "./dist/index.mjs",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js"
12
+ },
13
+ "./styles.css": "./dist/styles.css"
14
+ },
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "dev": "tsup --watch"
18
+ },
19
+ "peerDependencies": {
20
+ "react": ">=19"
21
+ },
22
+ "devDependencies": {
23
+ "tsup": "^8.0.0",
24
+ "typescript": "^5.0.0",
25
+ "@types/react": "^19"
26
+ }
27
+ }
package/src/Avatar.tsx ADDED
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+
3
+ export interface AvatarProps extends React.HTMLAttributes<HTMLSpanElement> {
4
+ initials: string;
5
+ size?: "sm" | "md" | "lg";
6
+ }
7
+
8
+ export function Avatar({ initials, size = "md", className = "", ...props }: AvatarProps) {
9
+ return (
10
+ <span
11
+ className={["vrnclr-avatar", className].filter(Boolean).join(" ")}
12
+ data-size={size}
13
+ {...props}
14
+ >
15
+ {initials}
16
+ </span>
17
+ );
18
+ }
package/src/Badge.tsx ADDED
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+
3
+ export type BadgeVariant = "default" | "info" | "success" | "warning" | "error";
4
+
5
+ export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
6
+ variant?: BadgeVariant;
7
+ }
8
+
9
+ export function Badge({ variant = "default", className = "", ...props }: BadgeProps) {
10
+ return (
11
+ <span
12
+ className={["vrnclr-badge", className].filter(Boolean).join(" ")}
13
+ data-variant={variant}
14
+ {...props}
15
+ />
16
+ );
17
+ }
package/src/Button.tsx ADDED
@@ -0,0 +1,67 @@
1
+ import React from "react";
2
+
3
+ export type ButtonVariant = "primary" | "secondary" | "ghost" | "destructive";
4
+ export type ButtonSize = "sm" | "md" | "lg";
5
+
6
+ type BaseProps = {
7
+ variant?: ButtonVariant;
8
+ size?: ButtonSize;
9
+ className?: string;
10
+ };
11
+
12
+ type ButtonAsButtonProps = BaseProps &
13
+ React.ButtonHTMLAttributes<HTMLButtonElement> & {
14
+ asAnchor?: false;
15
+ };
16
+
17
+ type ButtonAsAnchorProps = BaseProps & {
18
+ asAnchor: true;
19
+ children: React.ReactElement<React.AnchorHTMLAttributes<HTMLAnchorElement>>;
20
+ };
21
+
22
+ export type ButtonProps = ButtonAsButtonProps | ButtonAsAnchorProps;
23
+
24
+ function isAnchorLike(type: unknown): boolean {
25
+ if (type === "a") return true;
26
+ if (typeof type !== "function" && typeof type !== "object") return false;
27
+ const name =
28
+ (type as any).displayName?.toLowerCase() ||
29
+ (type as any).name?.toLowerCase() ||
30
+ "";
31
+ return name.includes("link");
32
+ }
33
+
34
+ export function Button(props: ButtonProps) {
35
+ const { variant = "primary", size = "md", className = "" } = props;
36
+ const cls = ["vrnclr-button", className].filter(Boolean).join(" ");
37
+
38
+ if (props.asAnchor) {
39
+ const { children } = props;
40
+
41
+ if (process.env.NODE_ENV === "development") {
42
+ if (!isAnchorLike((children as any)?.type)) {
43
+ console.warn(
44
+ "[Button] asAnchor: 자식은 anchor 계열이어야 해요. (예: <a>, <Link>)"
45
+ );
46
+ }
47
+ }
48
+
49
+ const childClassName = (children.props as any).className;
50
+ return React.cloneElement(children, {
51
+ className: childClassName ? `${cls} ${childClassName}` : cls,
52
+ "data-variant": variant,
53
+ "data-size": size,
54
+ } as any);
55
+ }
56
+
57
+ const { asAnchor: _a, variant: _v, size: _s, className: _c, ...rest } =
58
+ props as ButtonAsButtonProps;
59
+ return (
60
+ <button
61
+ className={cls}
62
+ data-variant={variant}
63
+ data-size={size}
64
+ {...rest}
65
+ />
66
+ );
67
+ }
package/src/Card.tsx ADDED
@@ -0,0 +1,45 @@
1
+ import React from "react";
2
+
3
+ export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}
4
+
5
+ export function Card({ className = "", ...props }: CardProps) {
6
+ return (
7
+ <div
8
+ className={["vrnclr-card", className].filter(Boolean).join(" ")}
9
+ {...props}
10
+ />
11
+ );
12
+ }
13
+
14
+ export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
15
+
16
+ export function CardHeader({ className = "", ...props }: CardHeaderProps) {
17
+ return (
18
+ <div
19
+ className={["vrnclr-card-header", className].filter(Boolean).join(" ")}
20
+ {...props}
21
+ />
22
+ );
23
+ }
24
+
25
+ export interface CardContentProps extends React.HTMLAttributes<HTMLDivElement> {}
26
+
27
+ export function CardContent({ className = "", ...props }: CardContentProps) {
28
+ return (
29
+ <div
30
+ className={["vrnclr-card-content", className].filter(Boolean).join(" ")}
31
+ {...props}
32
+ />
33
+ );
34
+ }
35
+
36
+ export interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {}
37
+
38
+ export function CardFooter({ className = "", ...props }: CardFooterProps) {
39
+ return (
40
+ <div
41
+ className={["vrnclr-card-footer", className].filter(Boolean).join(" ")}
42
+ {...props}
43
+ />
44
+ );
45
+ }
@@ -0,0 +1,24 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+
5
+ export interface CheckboxProps
6
+ extends React.InputHTMLAttributes<HTMLInputElement> {
7
+ label?: string;
8
+ }
9
+
10
+ export function Checkbox({ label, className = "", ...props }: CheckboxProps) {
11
+ return (
12
+ <label
13
+ className={["vrnclr-checkbox-label", className].filter(Boolean).join(" ")}
14
+ data-disabled={props.disabled ? "true" : undefined}
15
+ >
16
+ <input
17
+ type="checkbox"
18
+ className="vrnclr-checkbox"
19
+ {...props}
20
+ />
21
+ {label}
22
+ </label>
23
+ );
24
+ }
package/src/Input.tsx ADDED
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+
3
+ export type InputState = "default" | "error";
4
+
5
+ export interface InputProps
6
+ extends React.InputHTMLAttributes<HTMLInputElement> {
7
+ state?: InputState;
8
+ }
9
+
10
+ export function Input({ state = "default", className = "", ...props }: InputProps) {
11
+ return (
12
+ <input
13
+ className={["vrnclr-input", className].filter(Boolean).join(" ")}
14
+ data-state={state}
15
+ {...props}
16
+ />
17
+ );
18
+ }
package/src/Label.tsx ADDED
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+
3
+ export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {}
4
+
5
+ export function Label({ className = "", ...props }: LabelProps) {
6
+ return (
7
+ <label
8
+ className={["vrnclr-label", className].filter(Boolean).join(" ")}
9
+ {...props}
10
+ />
11
+ );
12
+ }
@@ -0,0 +1,20 @@
1
+ import React from "react";
2
+
3
+ export interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
4
+ orientation?: "horizontal" | "vertical";
5
+ }
6
+
7
+ export function Separator({
8
+ orientation = "horizontal",
9
+ className = "",
10
+ ...props
11
+ }: SeparatorProps) {
12
+ return (
13
+ <div
14
+ role="separator"
15
+ className={["vrnclr-separator", className].filter(Boolean).join(" ")}
16
+ data-orientation={orientation}
17
+ {...props}
18
+ />
19
+ );
20
+ }
package/src/Toggle.tsx ADDED
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import React, { useState } from "react";
4
+
5
+ export interface ToggleProps {
6
+ defaultChecked?: boolean;
7
+ checked?: boolean;
8
+ onChange?: (checked: boolean) => void;
9
+ disabled?: boolean;
10
+ label?: string;
11
+ className?: string;
12
+ }
13
+
14
+ export function Toggle({
15
+ defaultChecked = false,
16
+ checked,
17
+ onChange,
18
+ disabled = false,
19
+ label,
20
+ className = "",
21
+ }: ToggleProps) {
22
+ const [internal, setInternal] = useState(defaultChecked);
23
+ const on = checked !== undefined ? checked : internal;
24
+
25
+ function handleClick() {
26
+ if (disabled) return;
27
+ const next = !on;
28
+ setInternal(next);
29
+ onChange?.(next);
30
+ }
31
+
32
+ return (
33
+ <label
34
+ className={["vrnclr-toggle-label", className].filter(Boolean).join(" ")}
35
+ data-disabled={disabled ? "true" : undefined}
36
+ >
37
+ <button
38
+ type="button"
39
+ role="switch"
40
+ aria-checked={on}
41
+ onClick={handleClick}
42
+ disabled={disabled}
43
+ className="vrnclr-toggle"
44
+ >
45
+ <span className="vrnclr-toggle-thumb" />
46
+ </button>
47
+ {label}
48
+ </label>
49
+ );
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ export { Button } from "./Button";
2
+ export type { ButtonProps, ButtonVariant, ButtonSize } from "./Button";
3
+
4
+ export { Input } from "./Input";
5
+ export type { InputProps, InputState } from "./Input";
6
+
7
+ export { Badge } from "./Badge";
8
+ export type { BadgeProps, BadgeVariant } from "./Badge";
9
+
10
+ export { Card, CardHeader, CardContent, CardFooter } from "./Card";
11
+ export type { CardProps, CardHeaderProps, CardContentProps, CardFooterProps } from "./Card";
12
+
13
+ export { Checkbox } from "./Checkbox";
14
+ export type { CheckboxProps } from "./Checkbox";
15
+
16
+ export { Toggle } from "./Toggle";
17
+ export type { ToggleProps } from "./Toggle";
18
+
19
+ export { Label } from "./Label";
20
+ export type { LabelProps } from "./Label";
21
+
22
+ export { Separator } from "./Separator";
23
+ export type { SeparatorProps } from "./Separator";
24
+
25
+ export { Avatar } from "./Avatar";
26
+ export type { AvatarProps } from "./Avatar";
package/src/styles.css ADDED
@@ -0,0 +1,291 @@
1
+ /* ─── Default token values ─────────────────────────────────────────────── */
2
+
3
+ :root {
4
+ --vrnclr-color-background: #fafafa;
5
+ --vrnclr-color-foreground: #09090b;
6
+ --vrnclr-color-primary: #2952cc;
7
+ --vrnclr-color-primary-foreground: #eff6ff;
8
+ --vrnclr-color-secondary: #f4f4f5;
9
+ --vrnclr-color-secondary-foreground: #18181b;
10
+ --vrnclr-color-muted: #f4f4f5;
11
+ --vrnclr-color-muted-foreground: #71717a;
12
+ --vrnclr-color-border: #e4e4e7;
13
+ --vrnclr-color-input: #e4e4e7;
14
+ --vrnclr-color-ring: #2952cc;
15
+ --vrnclr-color-destructive: #ef4444;
16
+ --vrnclr-color-destructive-foreground: #fafafa;
17
+
18
+ --vrnclr-radius-sm: 4px;
19
+ --vrnclr-radius-md: 8px;
20
+ --vrnclr-radius-lg: 16px;
21
+
22
+ --vrnclr-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
23
+ --vrnclr-shadow-md: 0 4px 6px 0 rgba(0, 0, 0, 0.07);
24
+ --vrnclr-shadow-lg: 0 10px 15px 0 rgba(0, 0, 0, 0.1);
25
+
26
+ --vrnclr-font-family: Inter, sans-serif;
27
+ --vrnclr-font-size-base: 16px;
28
+ --vrnclr-spacing-base: 4px;
29
+ }
30
+
31
+ /* ─── Button ───────────────────────────────────────────────────────────── */
32
+
33
+ .vrnclr-button {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ justify-content: center;
37
+ font-weight: 500;
38
+ border-radius: var(--vrnclr-radius-md);
39
+ border: none;
40
+ cursor: pointer;
41
+ transition: opacity 0.15s, background-color 0.15s;
42
+ text-decoration: none;
43
+ white-space: nowrap;
44
+ font-family: inherit;
45
+ }
46
+
47
+ .vrnclr-button:disabled {
48
+ opacity: 0.5;
49
+ pointer-events: none;
50
+ }
51
+
52
+ /* Sizes */
53
+ .vrnclr-button[data-size="sm"] { padding: 6px 12px; font-size: 12px; }
54
+ .vrnclr-button[data-size="md"] { padding: 8px 16px; font-size: 14px; }
55
+ .vrnclr-button[data-size="lg"] { padding: 10px 20px; font-size: 16px; }
56
+
57
+ /* Variants */
58
+ .vrnclr-button[data-variant="primary"] {
59
+ background-color: var(--vrnclr-color-primary);
60
+ color: var(--vrnclr-color-primary-foreground);
61
+ }
62
+ .vrnclr-button[data-variant="primary"]:hover { opacity: 0.9; }
63
+
64
+ .vrnclr-button[data-variant="secondary"] {
65
+ background-color: var(--vrnclr-color-background);
66
+ color: var(--vrnclr-color-foreground);
67
+ border: 1px solid var(--vrnclr-color-border);
68
+ }
69
+ .vrnclr-button[data-variant="secondary"]:hover {
70
+ background-color: var(--vrnclr-color-secondary);
71
+ }
72
+
73
+ .vrnclr-button[data-variant="ghost"] {
74
+ background-color: transparent;
75
+ color: var(--vrnclr-color-muted-foreground);
76
+ }
77
+ .vrnclr-button[data-variant="ghost"]:hover {
78
+ background-color: var(--vrnclr-color-secondary);
79
+ }
80
+
81
+ .vrnclr-button[data-variant="destructive"] {
82
+ background-color: var(--vrnclr-color-destructive);
83
+ color: var(--vrnclr-color-destructive-foreground);
84
+ }
85
+ .vrnclr-button[data-variant="destructive"]:hover { opacity: 0.9; }
86
+
87
+ /* ─── Input ────────────────────────────────────────────────────────────── */
88
+
89
+ .vrnclr-input {
90
+ padding: 8px 12px;
91
+ font-size: 14px;
92
+ border: 1px solid var(--vrnclr-color-border);
93
+ border-radius: var(--vrnclr-radius-md);
94
+ background-color: var(--vrnclr-color-background);
95
+ color: var(--vrnclr-color-foreground);
96
+ outline: none;
97
+ transition: border-color 0.15s, box-shadow 0.15s;
98
+ font-family: inherit;
99
+ width: 100%;
100
+ box-sizing: border-box;
101
+ }
102
+
103
+ .vrnclr-input:focus {
104
+ border-color: transparent;
105
+ box-shadow: 0 0 0 2px var(--vrnclr-color-ring);
106
+ }
107
+
108
+ .vrnclr-input:disabled {
109
+ background-color: var(--vrnclr-color-muted);
110
+ color: var(--vrnclr-color-muted-foreground);
111
+ cursor: not-allowed;
112
+ }
113
+
114
+ .vrnclr-input[data-state="error"] {
115
+ border-color: var(--vrnclr-color-destructive);
116
+ }
117
+
118
+ .vrnclr-input[data-state="error"]:focus {
119
+ box-shadow: 0 0 0 2px var(--vrnclr-color-destructive);
120
+ }
121
+
122
+ /* ─── Card ─────────────────────────────────────────────────────────────── */
123
+
124
+ .vrnclr-card {
125
+ background-color: var(--vrnclr-color-background);
126
+ border: 1px solid var(--vrnclr-color-border);
127
+ border-radius: var(--vrnclr-radius-lg);
128
+ }
129
+
130
+ .vrnclr-card-header {
131
+ padding: 20px;
132
+ display: flex;
133
+ flex-direction: column;
134
+ gap: 4px;
135
+ }
136
+
137
+ .vrnclr-card-content {
138
+ padding: 0 20px 20px;
139
+ }
140
+
141
+ .vrnclr-card-footer {
142
+ padding: 16px 20px;
143
+ border-top: 1px solid var(--vrnclr-color-border);
144
+ display: flex;
145
+ align-items: center;
146
+ gap: 8px;
147
+ }
148
+
149
+ /* ─── Badge ────────────────────────────────────────────────────────────── */
150
+
151
+ .vrnclr-badge {
152
+ display: inline-flex;
153
+ align-items: center;
154
+ padding: 2px 8px;
155
+ font-size: 12px;
156
+ font-weight: 500;
157
+ border-radius: 9999px;
158
+ }
159
+
160
+ .vrnclr-badge[data-variant="default"] {
161
+ background-color: var(--vrnclr-color-primary);
162
+ color: var(--vrnclr-color-primary-foreground);
163
+ }
164
+ .vrnclr-badge[data-variant="info"] { background-color: #dbeafe; color: #1d4ed8; }
165
+ .vrnclr-badge[data-variant="success"] { background-color: #dcfce7; color: #15803d; }
166
+ .vrnclr-badge[data-variant="warning"] { background-color: #fef9c3; color: #a16207; }
167
+ .vrnclr-badge[data-variant="error"] {
168
+ background-color: var(--vrnclr-color-destructive);
169
+ color: var(--vrnclr-color-destructive-foreground);
170
+ }
171
+
172
+ /* ─── Checkbox ─────────────────────────────────────────────────────────── */
173
+
174
+ .vrnclr-checkbox-label {
175
+ display: inline-flex;
176
+ align-items: center;
177
+ gap: 8px;
178
+ font-size: 14px;
179
+ cursor: pointer;
180
+ color: var(--vrnclr-color-foreground);
181
+ }
182
+
183
+ .vrnclr-checkbox-label[data-disabled="true"] {
184
+ color: var(--vrnclr-color-muted-foreground);
185
+ cursor: not-allowed;
186
+ }
187
+
188
+ .vrnclr-checkbox {
189
+ width: 16px;
190
+ height: 16px;
191
+ border-radius: var(--vrnclr-radius-sm);
192
+ border: 1px solid var(--vrnclr-color-border);
193
+ accent-color: var(--vrnclr-color-primary);
194
+ cursor: pointer;
195
+ }
196
+
197
+ .vrnclr-checkbox:disabled { cursor: not-allowed; }
198
+
199
+ /* ─── Toggle ───────────────────────────────────────────────────────────── */
200
+
201
+ .vrnclr-toggle-label {
202
+ display: inline-flex;
203
+ align-items: center;
204
+ gap: 12px;
205
+ font-size: 14px;
206
+ cursor: pointer;
207
+ color: var(--vrnclr-color-foreground);
208
+ }
209
+
210
+ .vrnclr-toggle-label[data-disabled="true"] {
211
+ color: var(--vrnclr-color-muted-foreground);
212
+ cursor: not-allowed;
213
+ }
214
+
215
+ .vrnclr-toggle {
216
+ width: 40px;
217
+ height: 24px;
218
+ border-radius: 9999px;
219
+ border: none;
220
+ cursor: pointer;
221
+ transition: background-color 0.15s;
222
+ padding: 0;
223
+ position: relative;
224
+ }
225
+
226
+ .vrnclr-toggle:disabled {
227
+ opacity: 0.5;
228
+ cursor: not-allowed;
229
+ }
230
+
231
+ .vrnclr-toggle[aria-checked="true"] { background-color: var(--vrnclr-color-primary); }
232
+ .vrnclr-toggle[aria-checked="false"] { background-color: var(--vrnclr-color-secondary); }
233
+
234
+ .vrnclr-toggle-thumb {
235
+ display: block;
236
+ width: 16px;
237
+ height: 16px;
238
+ background-color: var(--vrnclr-color-background);
239
+ border-radius: 9999px;
240
+ box-shadow: var(--vrnclr-shadow-sm);
241
+ transition: transform 0.15s;
242
+ position: absolute;
243
+ top: 4px;
244
+ left: 4px;
245
+ }
246
+
247
+ .vrnclr-toggle[aria-checked="true"] .vrnclr-toggle-thumb { transform: translateX(16px); }
248
+ .vrnclr-toggle[aria-checked="false"] .vrnclr-toggle-thumb { transform: translateX(0); }
249
+
250
+ /* ─── Label ────────────────────────────────────────────────────────────── */
251
+
252
+ .vrnclr-label {
253
+ font-size: 12px;
254
+ font-weight: 500;
255
+ color: var(--vrnclr-color-muted-foreground);
256
+ }
257
+
258
+ /* ─── Separator ────────────────────────────────────────────────────────── */
259
+
260
+ .vrnclr-separator {
261
+ background-color: var(--vrnclr-color-border);
262
+ border: none;
263
+ }
264
+
265
+ .vrnclr-separator[data-orientation="horizontal"] {
266
+ height: 1px;
267
+ width: 100%;
268
+ }
269
+
270
+ .vrnclr-separator[data-orientation="vertical"] {
271
+ width: 1px;
272
+ align-self: stretch;
273
+ }
274
+
275
+ /* ─── Avatar ───────────────────────────────────────────────────────────── */
276
+
277
+ .vrnclr-avatar {
278
+ display: inline-flex;
279
+ align-items: center;
280
+ justify-content: center;
281
+ border-radius: 9999px;
282
+ background-color: var(--vrnclr-color-primary);
283
+ color: var(--vrnclr-color-primary-foreground);
284
+ font-weight: 500;
285
+ user-select: none;
286
+ font-family: inherit;
287
+ }
288
+
289
+ .vrnclr-avatar[data-size="sm"] { width: 24px; height: 24px; font-size: 10px; }
290
+ .vrnclr-avatar[data-size="md"] { width: 32px; height: 32px; font-size: 12px; }
291
+ .vrnclr-avatar[data-size="lg"] { width: 40px; height: 40px; font-size: 14px; }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "esnext"],
5
+ "jsx": "react-jsx",
6
+ "module": "esnext",
7
+ "moduleResolution": "bundler",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "declaration": true,
11
+ "esModuleInterop": true
12
+ },
13
+ "include": ["src"]
14
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["cjs", "esm"],
6
+ dts: true,
7
+ external: ["react", "react-dom"],
8
+ clean: true,
9
+ publicDir: false,
10
+ async onSuccess() {
11
+ const { copyFileSync } = await import("fs");
12
+ copyFileSync("src/styles.css", "dist/styles.css");
13
+ },
14
+ });