atomsystem 0.1.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/README.md +5 -0
- package/app/component/Button/Button.tsx +46 -0
- package/app/component/Card/Card.tsx +57 -0
- package/app/component/CodeBlock/CodeBlock.tsx +30 -0
- package/app/component/GlobalNav/GlobalNav.tsx +111 -0
- package/app/component/Input/Input.tsx +306 -0
- package/app/component/Theme/Theme.tsx +118 -0
- package/app/component/component.css +42 -0
- package/build/dark.variables.css +275 -0
- package/build/digitalFont.variables.css +267 -0
- package/build/global.variables.css +267 -0
- package/build/inputCircle.variables.css +268 -0
- package/build/inputStandard.variables.css +268 -0
- package/build/standard.variables.css +293 -0
- package/package.json +39 -0
- package/token.json +1179 -0
- package/tokens/buildMergeCss.js +87 -0
- package/tokens/buildTokenSplit.js +81 -0
- package/tokens/setorder/token_dark.json +390 -0
- package/tokens/setorder/token_digitalFont.json +6 -0
- package/tokens/setorder/token_global.json +888 -0
- package/tokens/token/dark.json +904 -0
- package/tokens/token/digitalFont.json +882 -0
- package/tokens/token/global.json +882 -0
- package/tokens/token/inputCircle.json +888 -0
- package/tokens/token/inputStandard.json +888 -0
- package/tokens/token/standard.json +976 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
|
|
4
|
+
export const Button = ({
|
|
5
|
+
children,
|
|
6
|
+
type,
|
|
7
|
+
style,
|
|
8
|
+
disabled,
|
|
9
|
+
}: {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
type: "primary" | "secondary" | "success" | "danger" | "gray";
|
|
12
|
+
style?: React.CSSProperties;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
}) => {
|
|
15
|
+
return (
|
|
16
|
+
<button
|
|
17
|
+
className={clsx(
|
|
18
|
+
"px-4 py-2 rounded-common text-button-text transition-all",
|
|
19
|
+
{
|
|
20
|
+
"bg-button-primary-standard hover:bg-button-primary-hover disabled:bg-button-primary-disabled":
|
|
21
|
+
type == "primary",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"bg-button-secondary-standard hover:bg-button-secondary-hover disabled:bg-button-secondary-disabled ":
|
|
25
|
+
type == "secondary",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"bg-button-success-standard hover:bg-button-success-hover disabled:bg-button-success-disabled":
|
|
29
|
+
type == "success",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"bg-button-danger-standard hover:bg-button-danger-hover disabled:bg-button-danger-disabled":
|
|
33
|
+
type == "danger",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"bg-button-gray-standard hover:bg-button-gray-hover disabled:bg-button-gray-disabled":
|
|
37
|
+
type == "gray",
|
|
38
|
+
}
|
|
39
|
+
)}
|
|
40
|
+
style={style}
|
|
41
|
+
disabled={disabled}
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
</button>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export const Card = ({
|
|
4
|
+
children,
|
|
5
|
+
type = "standard",
|
|
6
|
+
className = "",
|
|
7
|
+
}: {
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
type?: "standard" | "group";
|
|
11
|
+
}) => {
|
|
12
|
+
// CardHeader와 CardContent의 존재 여부 확인
|
|
13
|
+
const hasCardContent = React.Children.toArray(children).some(
|
|
14
|
+
(child) =>
|
|
15
|
+
React.isValidElement(child) &&
|
|
16
|
+
(child.type === CardHeader ||
|
|
17
|
+
child.type === CardContent ||
|
|
18
|
+
child.type === CardFooter)
|
|
19
|
+
);
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className={`${type == "standard" ? "bg-fill-card border-fill-border" : type == "group" ? "bg-fill-group border-none" : ""} ${!hasCardContent ? "p-4" : ""} rounded border ${className} overflow-auto`}
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const CardHeader = ({
|
|
30
|
+
children,
|
|
31
|
+
noMargin,
|
|
32
|
+
}: {
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
noMargin?: boolean;
|
|
35
|
+
}) => {
|
|
36
|
+
return <div className={`${noMargin ? "p-0" : "p-4"}`}>{children}</div>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const CardContent = ({
|
|
40
|
+
children,
|
|
41
|
+
noMargin,
|
|
42
|
+
}: {
|
|
43
|
+
children: React.ReactNode;
|
|
44
|
+
noMargin?: boolean;
|
|
45
|
+
}) => {
|
|
46
|
+
return <div className={`${noMargin ? "p-0" : "p-4"}`}>{children}</div>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const CardFooter = ({
|
|
50
|
+
children,
|
|
51
|
+
noMargin,
|
|
52
|
+
}: {
|
|
53
|
+
children: React.ReactNode;
|
|
54
|
+
noMargin?: boolean;
|
|
55
|
+
}) => {
|
|
56
|
+
return <div className={`${noMargin ? "p-0" : "p-4"}`}>{children}</div>;
|
|
57
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import hljs from "highlight.js";
|
|
4
|
+
import "highlight.js/styles/atom-one-dark.css"; // 스타일 파일
|
|
5
|
+
|
|
6
|
+
const CodeBlock = ({
|
|
7
|
+
language,
|
|
8
|
+
children,
|
|
9
|
+
}: {
|
|
10
|
+
language: string;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
}) => {
|
|
13
|
+
const codeRef = useRef<HTMLElement | null>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (codeRef.current) {
|
|
17
|
+
hljs.highlightElement(codeRef.current); // 코드 블록 하이라이트
|
|
18
|
+
}
|
|
19
|
+
}, [children]);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<pre>
|
|
23
|
+
<code ref={codeRef} className={language}>
|
|
24
|
+
{children}
|
|
25
|
+
</code>
|
|
26
|
+
</pre>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default CodeBlock;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
|
|
6
|
+
const GlobalNav = () => {
|
|
7
|
+
const [windowSize, setWindowSize] = useState<number>(); // 초기값 설정
|
|
8
|
+
|
|
9
|
+
const [toggleMenu, setToggleMenu] = useState<boolean>(false);
|
|
10
|
+
|
|
11
|
+
// 윈도우 사이즈 추적
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const handleResize = () => {
|
|
14
|
+
setWindowSize(window.innerWidth); // 윈도우 사이즈 업데이트
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
window.addEventListener("resize", handleResize); // 리사이즈 이벤트 리스너 추가
|
|
18
|
+
|
|
19
|
+
return () => window.removeEventListener("resize", handleResize); // 컴포넌트 언마운트 시 리스너 제거
|
|
20
|
+
}, [windowSize]);
|
|
21
|
+
|
|
22
|
+
// 초기 토글 값 설정
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (window.innerWidth > 768) {
|
|
26
|
+
setToggleMenu(true);
|
|
27
|
+
} else {
|
|
28
|
+
setToggleMenu(false);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// console.log(
|
|
32
|
+
// `one Time Work!: ${toggleMenu}, windowSize:${window.innerWidth}`
|
|
33
|
+
// );
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
<nav
|
|
39
|
+
className={`fixed w-full max-w-80 h-full bg-fill-card text-text-standard z-20 left-0 top-0 md:relative ${toggleMenu ? "block" : "hidden"}`}
|
|
40
|
+
>
|
|
41
|
+
<div className="brand">
|
|
42
|
+
<div className="flex items-center justify-between">
|
|
43
|
+
<Link href={"/"}>
|
|
44
|
+
<Image
|
|
45
|
+
src={"/images/global/logo.png"}
|
|
46
|
+
alt="Logo"
|
|
47
|
+
width={180}
|
|
48
|
+
height={45}
|
|
49
|
+
draggable="false"
|
|
50
|
+
></Image>
|
|
51
|
+
</Link>
|
|
52
|
+
<button className="p-4">
|
|
53
|
+
<span
|
|
54
|
+
className="material-symbols-outlined"
|
|
55
|
+
onClick={() => setToggleMenu(false)}
|
|
56
|
+
style={{ fontSize: "16px" }}
|
|
57
|
+
>
|
|
58
|
+
close
|
|
59
|
+
</span>
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<ul>
|
|
64
|
+
<li>
|
|
65
|
+
<Link href={"/"} className="px-4 py-2 block">
|
|
66
|
+
시작하기
|
|
67
|
+
</Link>
|
|
68
|
+
</li>
|
|
69
|
+
<li>
|
|
70
|
+
<Link href={"/component/input"} className="px-4 py-2 block">
|
|
71
|
+
입력창
|
|
72
|
+
</Link>
|
|
73
|
+
</li>
|
|
74
|
+
<li>
|
|
75
|
+
<Link href={"/component/select"} className="px-4 py-2 block">
|
|
76
|
+
선택창
|
|
77
|
+
</Link>
|
|
78
|
+
</li>
|
|
79
|
+
<li>
|
|
80
|
+
<Link href={"/component/checkbox"} className="px-4 py-2 block">
|
|
81
|
+
체크박스
|
|
82
|
+
</Link>
|
|
83
|
+
</li>
|
|
84
|
+
<li>
|
|
85
|
+
<Link href={"/component/radio"} className="px-4 py-2 block">
|
|
86
|
+
라디오
|
|
87
|
+
</Link>
|
|
88
|
+
</li>
|
|
89
|
+
<li>
|
|
90
|
+
<Link href={"/component/button"} className="px-4 py-2 block">
|
|
91
|
+
버튼
|
|
92
|
+
</Link>
|
|
93
|
+
</li>
|
|
94
|
+
</ul>
|
|
95
|
+
</nav>
|
|
96
|
+
<button
|
|
97
|
+
className={`fixed shadow-md rounded-full bg-fill-card w-12 h-12 top-6 left-6 z-10 ${toggleMenu ? "hidden" : "block"} `}
|
|
98
|
+
>
|
|
99
|
+
<span
|
|
100
|
+
className="material-symbols-outlined"
|
|
101
|
+
onClick={() => setToggleMenu(true)}
|
|
102
|
+
style={{ fontSize: "16px" }}
|
|
103
|
+
>
|
|
104
|
+
menu
|
|
105
|
+
</span>
|
|
106
|
+
</button>
|
|
107
|
+
</>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export default GlobalNav;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
|
|
6
|
+
export const Input = ({
|
|
7
|
+
className,
|
|
8
|
+
defaultValue = "",
|
|
9
|
+
placeholder = "값을 입력해주세요",
|
|
10
|
+
label,
|
|
11
|
+
readOnly,
|
|
12
|
+
disabled,
|
|
13
|
+
pattern,
|
|
14
|
+
required,
|
|
15
|
+
}: {
|
|
16
|
+
className?: string;
|
|
17
|
+
defaultValue?: string;
|
|
18
|
+
placeholder?: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
readOnly?: boolean;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
pattern?: string;
|
|
23
|
+
required?: boolean;
|
|
24
|
+
}) => {
|
|
25
|
+
const [value, setValue] = useState(defaultValue);
|
|
26
|
+
const [isValue, setIsValue] = useState(false);
|
|
27
|
+
const [guide, setGuide] = useState("");
|
|
28
|
+
|
|
29
|
+
useEffect(() => {});
|
|
30
|
+
const handleValue = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
31
|
+
const inputValue = e.target.value;
|
|
32
|
+
if (defaultValue) {
|
|
33
|
+
setValue(defaultValue);
|
|
34
|
+
}
|
|
35
|
+
setValue(inputValue);
|
|
36
|
+
|
|
37
|
+
if (e.target.checkValidity()) {
|
|
38
|
+
setGuide("유효한 값입니다.");
|
|
39
|
+
setIsValue(true);
|
|
40
|
+
} else {
|
|
41
|
+
setGuide("유효하지 않은 값입니다.");
|
|
42
|
+
setIsValue(false);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
{label ? (
|
|
49
|
+
<p className={clsx("mb-1 text-text-standard", {})}>
|
|
50
|
+
<span
|
|
51
|
+
className={clsx("", {
|
|
52
|
+
"after:content-['*'] after:ml-1 after:text-danger": required,
|
|
53
|
+
})}
|
|
54
|
+
>
|
|
55
|
+
{label}
|
|
56
|
+
</span>
|
|
57
|
+
</p>
|
|
58
|
+
) : null}
|
|
59
|
+
<input
|
|
60
|
+
type="text"
|
|
61
|
+
className={clsx(
|
|
62
|
+
"transition-all border px-4 py-2 border-input-border-standard text-input-text-value rounded-input-rounded",
|
|
63
|
+
value ? "bg-input-background-value" : "bg-input-background-standard ",
|
|
64
|
+
{ "placeholder:text-input-text-placeholder": true },
|
|
65
|
+
{ "focus:border-input-border-focus": true },
|
|
66
|
+
{ "disabled:bg-input-background-disabled": true },
|
|
67
|
+
{
|
|
68
|
+
"invalid:border-input-border-error invalid:bg-input-background-error invalid:text-input-text-error":
|
|
69
|
+
required && pattern,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"valid:border-input-border-success valid:bg-input-background-success valid:text-input-text-success":
|
|
73
|
+
required && pattern,
|
|
74
|
+
},
|
|
75
|
+
className
|
|
76
|
+
)}
|
|
77
|
+
value={value}
|
|
78
|
+
onChange={handleValue}
|
|
79
|
+
placeholder={placeholder}
|
|
80
|
+
readOnly={readOnly}
|
|
81
|
+
disabled={disabled}
|
|
82
|
+
pattern={pattern}
|
|
83
|
+
required={required}
|
|
84
|
+
/>
|
|
85
|
+
{guide && required && pattern ? (
|
|
86
|
+
<p
|
|
87
|
+
className={`text-sm ${isValue ? "text-input-text-success" : "text-input-text-error"} ml-1 mt-2`}
|
|
88
|
+
>
|
|
89
|
+
{guide}
|
|
90
|
+
</p>
|
|
91
|
+
) : null}
|
|
92
|
+
</>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const Select = ({ options }: { options: string[] }) => {
|
|
97
|
+
const [filterValue, setFilterValue] = useState("");
|
|
98
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
99
|
+
const [menuPosition, setMenuPosition] = useState({
|
|
100
|
+
top: 0,
|
|
101
|
+
left: 0,
|
|
102
|
+
width: 0,
|
|
103
|
+
});
|
|
104
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
105
|
+
const dropdownRef = useRef<HTMLUListElement>(null);
|
|
106
|
+
const filteredOptions = options.filter((option) =>
|
|
107
|
+
option.toLowerCase().includes(filterValue.toLowerCase())
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (isOpen && inputRef.current) {
|
|
112
|
+
const rect = inputRef.current.getBoundingClientRect();
|
|
113
|
+
setMenuPosition({
|
|
114
|
+
top: rect.bottom + window.scrollY,
|
|
115
|
+
left: rect.left,
|
|
116
|
+
width: rect.width,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}, [isOpen]);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
// 마우스 벗어난 영역 클릭 시 드롭다운 닫기
|
|
123
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
124
|
+
if (
|
|
125
|
+
dropdownRef.current &&
|
|
126
|
+
!dropdownRef.current.contains(event.target as Node) &&
|
|
127
|
+
inputRef.current &&
|
|
128
|
+
!inputRef.current.contains(event.target as Node)
|
|
129
|
+
) {
|
|
130
|
+
setIsOpen(false);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
134
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div className="relative">
|
|
139
|
+
<label className="relative w-fit hidden md:block">
|
|
140
|
+
<input
|
|
141
|
+
ref={inputRef}
|
|
142
|
+
type="text"
|
|
143
|
+
value={filterValue}
|
|
144
|
+
placeholder="값을 선택해주세요..."
|
|
145
|
+
onChange={(e) => {
|
|
146
|
+
setFilterValue(e.target.value);
|
|
147
|
+
}}
|
|
148
|
+
onFocus={() => setIsOpen(true)}
|
|
149
|
+
className={clsx(
|
|
150
|
+
"transition-all bg-input-background-standard border px-4 py-2 border-input-border-standard text-input-text-value rounded-input-rounded",
|
|
151
|
+
{ "placeholder:text-input-text-placeholder": true },
|
|
152
|
+
{ "focus:border-input-border-focus": true },
|
|
153
|
+
{ "disabled:bg-input-background-disabled": true }
|
|
154
|
+
)}
|
|
155
|
+
/>
|
|
156
|
+
<div
|
|
157
|
+
className="absolute flex items-center right-0 top-0 h-full px-2"
|
|
158
|
+
draggable
|
|
159
|
+
>
|
|
160
|
+
<span className="material-symbols-outlined">
|
|
161
|
+
<span className="text-base input-text-value">arrow_drop_down</span>
|
|
162
|
+
</span>
|
|
163
|
+
</div>
|
|
164
|
+
</label>
|
|
165
|
+
{isOpen &&
|
|
166
|
+
createPortal(
|
|
167
|
+
<ul
|
|
168
|
+
ref={dropdownRef}
|
|
169
|
+
className="absolute w-full border rounded bg-input-background-standard border-input-border-standard shadow-md mt-1"
|
|
170
|
+
style={{
|
|
171
|
+
top: menuPosition.top,
|
|
172
|
+
left: menuPosition.left,
|
|
173
|
+
width: menuPosition.width,
|
|
174
|
+
}}
|
|
175
|
+
>
|
|
176
|
+
{filteredOptions.length > 0 ? (
|
|
177
|
+
filteredOptions.map((option, index) => (
|
|
178
|
+
<li
|
|
179
|
+
key={index}
|
|
180
|
+
className="px-4 py-2 text-input-text-value hover:bg-input-background-value cursor-pointer"
|
|
181
|
+
onClick={() => {
|
|
182
|
+
setFilterValue(option);
|
|
183
|
+
setIsOpen(false);
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
{option}
|
|
187
|
+
</li>
|
|
188
|
+
))
|
|
189
|
+
) : (
|
|
190
|
+
<li className="px-4 py-2 text-gray-500">No results</li>
|
|
191
|
+
)}
|
|
192
|
+
</ul>,
|
|
193
|
+
document.body
|
|
194
|
+
)}
|
|
195
|
+
<select
|
|
196
|
+
className={clsx(
|
|
197
|
+
" block transition-all bg-input-background-standard border px-4 py-2 border-input-border-standard text-input-text-value rounded-input-rounded",
|
|
198
|
+
{ "placeholder:text-input-text-placeholder": true },
|
|
199
|
+
{ "focus:border-input-border-focus": true },
|
|
200
|
+
{ "disabled:bg-input-background-disabled": true },
|
|
201
|
+
{ "md:hidden": true }
|
|
202
|
+
)}
|
|
203
|
+
>
|
|
204
|
+
{options.map((option, index) => {
|
|
205
|
+
return (
|
|
206
|
+
<option key={index} value={option}>
|
|
207
|
+
{option}
|
|
208
|
+
</option>
|
|
209
|
+
);
|
|
210
|
+
})}
|
|
211
|
+
</select>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export const Radio = ({
|
|
217
|
+
name,
|
|
218
|
+
value,
|
|
219
|
+
className,
|
|
220
|
+
onChange,
|
|
221
|
+
defaultChecked,
|
|
222
|
+
}: {
|
|
223
|
+
name: string;
|
|
224
|
+
value: string;
|
|
225
|
+
className?: string;
|
|
226
|
+
onChange?: () => void;
|
|
227
|
+
defaultChecked?: boolean;
|
|
228
|
+
}) => {
|
|
229
|
+
return (
|
|
230
|
+
<label className="flex items-center gap-2">
|
|
231
|
+
<input
|
|
232
|
+
type="radio"
|
|
233
|
+
className={clsx(
|
|
234
|
+
"relative appearance-none peer w-5 h-5 bg-input-background-standard border border-fill-border rounded-full",
|
|
235
|
+
{
|
|
236
|
+
"checked:bg-primary after:w-2.5 after:h-2.5 after:bg-fill-card after:content-[''] after:absolute after:rounded-full after:left-1 after:top-1":
|
|
237
|
+
true,
|
|
238
|
+
},
|
|
239
|
+
{ "focus:border-input-border-focus": true },
|
|
240
|
+
{ "disabled:bg-input-background-disabled": true },
|
|
241
|
+
className
|
|
242
|
+
)}
|
|
243
|
+
name={name}
|
|
244
|
+
onChange={onChange}
|
|
245
|
+
defaultChecked={defaultChecked}
|
|
246
|
+
/>
|
|
247
|
+
<span>{value}</span>
|
|
248
|
+
</label>
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export const Checkbox = ({
|
|
253
|
+
label,
|
|
254
|
+
className,
|
|
255
|
+
onChange,
|
|
256
|
+
disabled,
|
|
257
|
+
required,
|
|
258
|
+
defaultChecked,
|
|
259
|
+
}: {
|
|
260
|
+
label?: string;
|
|
261
|
+
className?: string;
|
|
262
|
+
onChange?: () => void;
|
|
263
|
+
disabled?: boolean;
|
|
264
|
+
required?: boolean;
|
|
265
|
+
defaultChecked?: boolean;
|
|
266
|
+
}) => {
|
|
267
|
+
return (
|
|
268
|
+
<label className="relative flex items-center gap-2">
|
|
269
|
+
<input
|
|
270
|
+
type="checkbox"
|
|
271
|
+
className={clsx(
|
|
272
|
+
"relative appearance-none peer w-5 h-5 bg-input-background-standard border border-fill-border rounded-common",
|
|
273
|
+
{ "checked:bg-primary": true },
|
|
274
|
+
{ "focus:border-input-border-focus": true },
|
|
275
|
+
{ "disabled:bg-input-background-disabled": true },
|
|
276
|
+
{
|
|
277
|
+
"after:content-['*'] after:text-danger after:absolute after:-right-1 after:-top-2":
|
|
278
|
+
required,
|
|
279
|
+
},
|
|
280
|
+
className
|
|
281
|
+
)}
|
|
282
|
+
onChange={onChange}
|
|
283
|
+
disabled={disabled}
|
|
284
|
+
required={required}
|
|
285
|
+
defaultChecked={defaultChecked}
|
|
286
|
+
/>
|
|
287
|
+
<span className="absolute left-0.5 top-1 invisible peer-checked:visible fill-fill-card ">
|
|
288
|
+
<svg
|
|
289
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
290
|
+
className="h-4 w-4"
|
|
291
|
+
viewBox="0 0 20 20"
|
|
292
|
+
strokeWidth="1"
|
|
293
|
+
>
|
|
294
|
+
<path
|
|
295
|
+
fillRule="evenodd"
|
|
296
|
+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
297
|
+
clipRule="evenodd"
|
|
298
|
+
></path>
|
|
299
|
+
</svg>
|
|
300
|
+
</span>
|
|
301
|
+
{label ? (
|
|
302
|
+
<span className="text-text-standard select-none">{label}</span>
|
|
303
|
+
) : null}
|
|
304
|
+
</label>
|
|
305
|
+
);
|
|
306
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Radio } from "../Input/Input";
|
|
5
|
+
|
|
6
|
+
export const ThemeSelector = ({
|
|
7
|
+
type,
|
|
8
|
+
}: {
|
|
9
|
+
type: "all" | "theme" | "input";
|
|
10
|
+
}) => {
|
|
11
|
+
const themes = ["standard", "dark"];
|
|
12
|
+
const [selectedTheme, setSelectedTheme] = useState<string | null>(null);
|
|
13
|
+
const inputThemes = ["inputStandard", "inputCircle"];
|
|
14
|
+
const [selectedInputTheme, setSelectedInputTheme] = useState<string | null>(
|
|
15
|
+
null
|
|
16
|
+
);
|
|
17
|
+
const [isThemeLoading, setThemeLoading] = useState(false);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const chageThemeValue = () => {
|
|
21
|
+
const storedTheme = localStorage.getItem("theme") || "standard";
|
|
22
|
+
const storedInputTheme =
|
|
23
|
+
localStorage.getItem("inputTheme") || "inputStandard";
|
|
24
|
+
|
|
25
|
+
setSelectedTheme(storedTheme);
|
|
26
|
+
setSelectedInputTheme(storedInputTheme);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
chageThemeValue();
|
|
30
|
+
setThemeLoading(true);
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
// main theme
|
|
34
|
+
const handleThemeChange = (theme: string) => {
|
|
35
|
+
if (typeof window !== "undefined") {
|
|
36
|
+
localStorage.setItem("theme", theme);
|
|
37
|
+
window.dispatchEvent(new Event("storage"));
|
|
38
|
+
setSelectedTheme(theme);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
// input theme
|
|
42
|
+
const handleInputThemeChange = (theme: string) => {
|
|
43
|
+
if (typeof window !== "undefined") {
|
|
44
|
+
localStorage.setItem("inputTheme", theme);
|
|
45
|
+
window.dispatchEvent(new Event("storage"));
|
|
46
|
+
setSelectedInputTheme(theme);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
if (isThemeLoading == false) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="flex flex-col gap-4">
|
|
55
|
+
<p>theme 선택</p>
|
|
56
|
+
<div className="flex flex-col gap-4">
|
|
57
|
+
{type == "all" || type == "theme" ? (
|
|
58
|
+
<div className="flex gap-4">
|
|
59
|
+
{themes.map((theme) => (
|
|
60
|
+
<Radio
|
|
61
|
+
name="mainTheme"
|
|
62
|
+
value={theme}
|
|
63
|
+
key={theme}
|
|
64
|
+
onChange={() => handleThemeChange(theme)}
|
|
65
|
+
defaultChecked={theme == selectedTheme}
|
|
66
|
+
/>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
) : null}
|
|
70
|
+
{type == "all" || type == "input" ? (
|
|
71
|
+
<div className="flex gap-4">
|
|
72
|
+
{inputThemes.map((inputTheme) => (
|
|
73
|
+
<Radio
|
|
74
|
+
name="inputTheme"
|
|
75
|
+
value={inputTheme}
|
|
76
|
+
key={inputTheme}
|
|
77
|
+
onChange={() => handleInputThemeChange(inputTheme)}
|
|
78
|
+
defaultChecked={inputTheme == selectedInputTheme}
|
|
79
|
+
/>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
) : null}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const ThemeWrapper = ({
|
|
89
|
+
children,
|
|
90
|
+
className,
|
|
91
|
+
}: {
|
|
92
|
+
children: React.ReactNode;
|
|
93
|
+
className: string;
|
|
94
|
+
}) => {
|
|
95
|
+
const [theme, setTheme] = useState<string | null>(null); // 테마 상태 추가
|
|
96
|
+
const [inputTheme, setInputTheme] = useState<string | null>(null); // 테마 상태 추가
|
|
97
|
+
const [isThemeLoading, setThemeLoading] = useState(false);
|
|
98
|
+
// theme setting
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
// 로컬 스토리지에서 테마 가져오기
|
|
101
|
+
const listenStorageChange = () => {
|
|
102
|
+
setTheme(localStorage.getItem("theme") || "standard");
|
|
103
|
+
setInputTheme(localStorage.getItem("inputTheme") || "inputStandard");
|
|
104
|
+
};
|
|
105
|
+
listenStorageChange();
|
|
106
|
+
window.addEventListener("storage", listenStorageChange);
|
|
107
|
+
setThemeLoading(true);
|
|
108
|
+
return () => window.removeEventListener("storage", listenStorageChange);
|
|
109
|
+
}, [theme]);
|
|
110
|
+
|
|
111
|
+
if (isThemeLoading == false) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<body className={`${className} ${theme} ${inputTheme}`}>{children}</body>
|
|
117
|
+
);
|
|
118
|
+
};
|