atomsystem 0.1.11 → 0.1.12

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.
@@ -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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atomsystem",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "atomsystem-init": "cli/init.js",
@@ -10,7 +10,8 @@
10
10
  "token.json",
11
11
  "tokens",
12
12
  "build",
13
- "tailwind.config.ts"
13
+ "tailwind.config.ts",
14
+ "cli/templates"
14
15
  ],
15
16
  "scripts": {
16
17
  "dev": "npm run build:token && next dev",