atomsystem 0.1.20 → 0.1.21
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/app/templates/Badge/Badge.tsx +30 -0
- package/app/templates/GlobalNav/GlobalNav.tsx +80 -61
- package/app/templates/Input/Input.tsx +1 -1
- package/app/templates/Modal/Modal.tsx +71 -0
- package/app/templates/Pagination/Pagination.tsx +6 -6
- package/app/templates/Theme/Theme.tsx +1 -1
- package/app/templates/Toast/Toast.tsx +1 -1
- package/cli/add.js +2 -0
- package/package.json +3 -1
- package/token.json +39 -4
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const Badge = ({
|
|
2
|
+
children,
|
|
3
|
+
color,
|
|
4
|
+
type,
|
|
5
|
+
className,
|
|
6
|
+
}: {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
color: "primary" | "secondary" | "warning" | "danger";
|
|
9
|
+
type?: "default" | "tint";
|
|
10
|
+
className?: string;
|
|
11
|
+
}) => {
|
|
12
|
+
switch (type) {
|
|
13
|
+
case "tint":
|
|
14
|
+
return (
|
|
15
|
+
<span
|
|
16
|
+
className={`text-sm px-1.5 rounded ${color === "primary" ? "bg-primary/20 !text-primary" : color === "secondary" ? "bg-secondary/20 !text-secondary" : color === "warning" ? "bg-warning/20 !text-warning" : color === "danger" ? "bg-danger/20 !text-danger" : null} ${className}`}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</span>
|
|
20
|
+
);
|
|
21
|
+
default:
|
|
22
|
+
return (
|
|
23
|
+
<span
|
|
24
|
+
className={`text-sm px-1.5 rounded ${color === "primary" ? "bg-primary !text-white" : color === "secondary" ? "bg-secondary !text-white" : color === "warning" ? "bg-warning !text-white" : color === "danger" ? "bg-danger !text-white" : null} ${className}`}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
</span>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
@@ -12,8 +12,9 @@ const GlobalNav = ({
|
|
|
12
12
|
type?: "sidebar" | "topmenu" | "mobile";
|
|
13
13
|
navMenu: {
|
|
14
14
|
name: string;
|
|
15
|
-
path
|
|
15
|
+
path?: string;
|
|
16
16
|
subItems?: { name: string; path: string }[];
|
|
17
|
+
label?: boolean;
|
|
17
18
|
}[];
|
|
18
19
|
}) => {
|
|
19
20
|
const [windowSize, setWindowSize] = useState<number>(); // 초기값 설정
|
|
@@ -52,7 +53,7 @@ const GlobalNav = ({
|
|
|
52
53
|
<nav
|
|
53
54
|
className={clsx(
|
|
54
55
|
`fixed top-0 md:relative w-full bg-[var(--background-card)] text-text-standard z-20 left-0 ${toggleMenu ? "block" : "hidden"}`,
|
|
55
|
-
{ "h-full max-w-80": type == "sidebar" },
|
|
56
|
+
{ "h-full max-w-80 overflow-auto": type == "sidebar" },
|
|
56
57
|
{ "h-auto flex flex-row items-center": type == "topmenu" }
|
|
57
58
|
)}
|
|
58
59
|
>
|
|
@@ -88,77 +89,95 @@ const GlobalNav = ({
|
|
|
88
89
|
"flex flex-row": type == "topmenu",
|
|
89
90
|
})}
|
|
90
91
|
>
|
|
91
|
-
{navMenu.map((menu) => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
className=
|
|
98
|
-
"px-4 py-2 block text-[var(--text-standard)]",
|
|
99
|
-
{
|
|
100
|
-
"text-primary":
|
|
101
|
-
menu.path == pathname ||
|
|
102
|
-
menu.subItems?.some((sub) => sub.path === pathname),
|
|
103
|
-
}
|
|
104
|
-
)}
|
|
92
|
+
{navMenu.map((menu, idx) => {
|
|
93
|
+
switch (true) {
|
|
94
|
+
case menu.label === true && type == "sidebar":
|
|
95
|
+
return (
|
|
96
|
+
<p
|
|
97
|
+
key={menu.name + idx}
|
|
98
|
+
className="px-4 text-sm text-sub mt-2 mb-0"
|
|
105
99
|
>
|
|
106
100
|
{menu.name}
|
|
107
|
-
</
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
101
|
+
</p>
|
|
102
|
+
);
|
|
103
|
+
default:
|
|
104
|
+
if (!menu.path) return null;
|
|
105
|
+
return (
|
|
106
|
+
<li className="relative group" key={menu.name}>
|
|
107
|
+
<div
|
|
108
|
+
className={clsx("flex justify-between items-center pr-4")}
|
|
112
109
|
>
|
|
113
|
-
<
|
|
110
|
+
<Link
|
|
111
|
+
href={menu.path}
|
|
114
112
|
className={clsx(
|
|
115
|
-
"
|
|
113
|
+
"px-4 py-2 block text-[var(--text-standard)]",
|
|
116
114
|
{
|
|
117
|
-
"
|
|
115
|
+
"text-primary":
|
|
116
|
+
menu.path == pathname ||
|
|
117
|
+
menu.subItems?.some(
|
|
118
|
+
(sub) => sub.path === pathname
|
|
119
|
+
),
|
|
118
120
|
}
|
|
119
121
|
)}
|
|
120
|
-
style={{ fontSize: "16px" }}
|
|
121
122
|
>
|
|
122
|
-
|
|
123
|
-
</
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
"block",
|
|
131
|
-
{
|
|
132
|
-
"hidden absolute top-[42px] group-hover:block hover:block bg-[var(--background-card)]":
|
|
133
|
-
type == "topmenu",
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
hidden: !openSubMenu[menu.name] && type == "sidebar",
|
|
137
|
-
}
|
|
138
|
-
)}
|
|
139
|
-
>
|
|
140
|
-
{menu.subItems.map((submenu) => {
|
|
141
|
-
return (
|
|
142
|
-
<li key={submenu.name}>
|
|
143
|
-
<Link
|
|
123
|
+
{menu.name}
|
|
124
|
+
</Link>
|
|
125
|
+
{menu.subItems && type == "sidebar" ? (
|
|
126
|
+
<button
|
|
127
|
+
className="cursor-pointer"
|
|
128
|
+
onClick={() => toggleSubMenu(menu.name)}
|
|
129
|
+
>
|
|
130
|
+
<span
|
|
144
131
|
className={clsx(
|
|
145
|
-
"
|
|
132
|
+
"material-symbols-outlined text-[var(--text-sub)] transition-transform",
|
|
146
133
|
{
|
|
147
|
-
"
|
|
148
|
-
}
|
|
149
|
-
{ "pl-12 ": type == "sidebar" }
|
|
134
|
+
"rotate-90": openSubMenu[menu.name],
|
|
135
|
+
}
|
|
150
136
|
)}
|
|
151
|
-
|
|
137
|
+
style={{ fontSize: "16px" }}
|
|
152
138
|
>
|
|
153
|
-
|
|
154
|
-
</
|
|
155
|
-
</
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
139
|
+
keyboard_arrow_right
|
|
140
|
+
</span>
|
|
141
|
+
</button>
|
|
142
|
+
) : null}
|
|
143
|
+
</div>
|
|
144
|
+
{menu.subItems ? (
|
|
145
|
+
<ul
|
|
146
|
+
className={clsx(
|
|
147
|
+
"block",
|
|
148
|
+
{
|
|
149
|
+
"hidden absolute top-[42px] group-hover:block hover:block bg-[var(--background-card)]":
|
|
150
|
+
type == "topmenu",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
hidden:
|
|
154
|
+
!openSubMenu[menu.name] && type == "sidebar",
|
|
155
|
+
}
|
|
156
|
+
)}
|
|
157
|
+
>
|
|
158
|
+
{menu.subItems.map((submenu) => {
|
|
159
|
+
return (
|
|
160
|
+
<li key={submenu.name}>
|
|
161
|
+
<Link
|
|
162
|
+
className={clsx(
|
|
163
|
+
"px-4 py-2 block text-[var(--text-standard)]",
|
|
164
|
+
{
|
|
165
|
+
"text-primary": submenu.path == pathname,
|
|
166
|
+
},
|
|
167
|
+
{ "pl-12 ": type == "sidebar" }
|
|
168
|
+
)}
|
|
169
|
+
href={submenu.path}
|
|
170
|
+
>
|
|
171
|
+
{submenu.name}
|
|
172
|
+
</Link>
|
|
173
|
+
</li>
|
|
174
|
+
);
|
|
175
|
+
})}
|
|
176
|
+
</ul>
|
|
177
|
+
) : null}
|
|
178
|
+
</li>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
162
181
|
})}
|
|
163
182
|
</ul>
|
|
164
183
|
</nav>
|
|
@@ -34,7 +34,7 @@ export const Input = ({
|
|
|
34
34
|
) : null}
|
|
35
35
|
<input
|
|
36
36
|
type={type}
|
|
37
|
-
className="border rounded focus:outline-none focus:ring-0 bg-[var(--input-background-standard)] disabled:bg-[var(--input-background-disabled)] disabled:cursor-not-allowed border-[var(--input-border-standard)] focus:border-[var(--input-border-focus)] "
|
|
37
|
+
className="border rounded focus:outline-none focus:ring-0 bg-[var(--input-background-standard)] disabled:bg-[var(--input-background-disabled)] disabled:cursor-not-allowed border-[var(--input-border-standard)] focus:border-[var(--input-border-focus)] w-full"
|
|
38
38
|
style={{
|
|
39
39
|
color: "var(--input-text-standard)",
|
|
40
40
|
paddingLeft: "var(--global-padding-x)",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import ReactModal from "react-modal";
|
|
4
|
+
import React, { useEffect } from "react";
|
|
5
|
+
|
|
6
|
+
type ModalProps = {
|
|
7
|
+
isOpen: boolean;
|
|
8
|
+
onRequestClose: () => void;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
modalTitle: string;
|
|
11
|
+
modalFooter: React.ReactNode;
|
|
12
|
+
size?: "normal" | "middle" | "large" | "xl" | "full" | "full-screen";
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const ModalContent = ({
|
|
16
|
+
isOpen,
|
|
17
|
+
onRequestClose,
|
|
18
|
+
children,
|
|
19
|
+
modalTitle,
|
|
20
|
+
modalFooter,
|
|
21
|
+
size = "normal",
|
|
22
|
+
}: ModalProps) => {
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
ReactModal.setAppElement("#modal-root"); // 접근성 설정
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<ReactModal
|
|
29
|
+
isOpen={isOpen}
|
|
30
|
+
onRequestClose={onRequestClose}
|
|
31
|
+
shouldCloseOnEsc={true}
|
|
32
|
+
overlayClassName="fixed inset-0 bg-black/40 flex justify-center items-center z-50"
|
|
33
|
+
className={`bg-[var(--modal-background)] p-0 rounded shadow-md w-full flex flex-col ${size === "normal" ? "max-w-md" : size === "middle" ? "max-w-xl" : size === "large" ? "max-w-5xl" : size === "xl" ? "max-w-6xl" : size === "full" ? "max-w-screen" : size === "full-screen" ? "w-screen h-screen" : null}`}
|
|
34
|
+
>
|
|
35
|
+
<ModalTitle onRequestClose={onRequestClose} title={modalTitle} />
|
|
36
|
+
<div className="p-4 w-full h-full">{children}</div>
|
|
37
|
+
<ModalFooter modalFooter={modalFooter} />
|
|
38
|
+
</ReactModal>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const Modal = () => {
|
|
43
|
+
return <div id="modal-root"></div>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type ModalTitleProps = {
|
|
47
|
+
title: string;
|
|
48
|
+
onRequestClose: () => void;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const ModalTitle = ({ title, onRequestClose }: ModalTitleProps) => {
|
|
52
|
+
return (
|
|
53
|
+
<div className="flex justify-between py-3 px-4">
|
|
54
|
+
<h6>{title}</h6>
|
|
55
|
+
<button
|
|
56
|
+
onClick={onRequestClose}
|
|
57
|
+
className="material-symbols-outlined cursor-pointer"
|
|
58
|
+
>
|
|
59
|
+
close
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const ModalFooter = ({
|
|
66
|
+
modalFooter,
|
|
67
|
+
}: {
|
|
68
|
+
modalFooter: React.ReactNode;
|
|
69
|
+
}) => {
|
|
70
|
+
return <div className="flex justify-end py-3 px-4">{modalFooter}</div>;
|
|
71
|
+
};
|
|
@@ -17,15 +17,15 @@ const Pagination = ({
|
|
|
17
17
|
/** 페이지네이션을 보여줄 최대 개수정보를 받습니다. */
|
|
18
18
|
pageRange: number;
|
|
19
19
|
/** totlaCount로 받아 계산하거나 전체 페이지네이션의 토탈 정보를 받습니다. */
|
|
20
|
-
totalCount
|
|
20
|
+
totalCount?: number;
|
|
21
21
|
/** totlaCount로 받아 계산하거나 전체 페이지네이션의 토탈 정보를 받습니다. */
|
|
22
22
|
pageTotal?: number;
|
|
23
23
|
}) => {
|
|
24
24
|
const totalPages = pageTotal
|
|
25
25
|
? pageTotal
|
|
26
26
|
: totalCount
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
? Math.ceil(totalCount / pageViewRange)
|
|
28
|
+
: 0;
|
|
29
29
|
const [pageStart, setPageStart] = useState(1);
|
|
30
30
|
const scrollTop = () => {
|
|
31
31
|
setTimeout(() => {
|
|
@@ -34,7 +34,7 @@ const Pagination = ({
|
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
return (
|
|
37
|
-
<div className="text-[#999] flex
|
|
37
|
+
<div className="text-[#999] flex">
|
|
38
38
|
<button
|
|
39
39
|
disabled={pageStart === 1}
|
|
40
40
|
onClick={() => {
|
|
@@ -47,7 +47,7 @@ const Pagination = ({
|
|
|
47
47
|
>
|
|
48
48
|
<span className="material-symbols-outlined">keyboard_arrow_left</span>
|
|
49
49
|
</button>
|
|
50
|
-
<ul className="
|
|
50
|
+
<ul className="flex w-full">
|
|
51
51
|
{[...Array(pageRange)].map((_, i) => {
|
|
52
52
|
const page = pageStart + i;
|
|
53
53
|
|
|
@@ -74,7 +74,7 @@ const Pagination = ({
|
|
|
74
74
|
}
|
|
75
75
|
scrollTop();
|
|
76
76
|
}}
|
|
77
|
-
className={` w-full text-center py-8 cursor-pointer ${
|
|
77
|
+
className={` w-full text-center py-8 cursor-pointer px-2 ${
|
|
78
78
|
currentPage === page ? "text-primary" : ""
|
|
79
79
|
}`}
|
|
80
80
|
>
|
|
@@ -9,7 +9,7 @@ export const ThemeSelector = ({
|
|
|
9
9
|
type: "all" | "theme" | "input";
|
|
10
10
|
}) => {
|
|
11
11
|
const themes = ["light", "dark"];
|
|
12
|
-
const [selectedTheme, setSelectedTheme] = useState<string | null>(
|
|
12
|
+
const [selectedTheme, setSelectedTheme] = useState<string | null>("light");
|
|
13
13
|
// const inputThemes = ["inputStandard", "inputCircle"];
|
|
14
14
|
// const [selectedInputTheme, setSelectedInputTheme] = useState<string | null>(
|
|
15
15
|
// null
|
|
@@ -19,7 +19,7 @@ let messages: ToastMessage[] = [];
|
|
|
19
19
|
let listener: ((messages: ToastMessage[]) => void) | null = null;
|
|
20
20
|
|
|
21
21
|
export const addToast = (msg: Omit<ToastMessage, "id">) => {
|
|
22
|
-
const id =
|
|
22
|
+
const id = Math.random().toString(36).substring(2) + Date.now().toString(36);
|
|
23
23
|
messages = [...messages, { id, ...msg }];
|
|
24
24
|
// messages 값 listener에 반영
|
|
25
25
|
listener?.(messages);
|
package/cli/add.js
CHANGED
|
@@ -16,6 +16,7 @@ const templateDir = path.join(__dirname, "..", "app", "templates");
|
|
|
16
16
|
// 사용할 수 있는 컴포넌트 목록
|
|
17
17
|
const components = [
|
|
18
18
|
"Button",
|
|
19
|
+
"Badge",
|
|
19
20
|
"Card",
|
|
20
21
|
"CodeBlock",
|
|
21
22
|
"GlobalNav",
|
|
@@ -25,6 +26,7 @@ const components = [
|
|
|
25
26
|
"Toast",
|
|
26
27
|
"Pagination",
|
|
27
28
|
"Table",
|
|
29
|
+
"Modal",
|
|
28
30
|
]; // 여기 추가할 컴포넌트 목록
|
|
29
31
|
|
|
30
32
|
// 명령어에서 컴포넌트 이름 가져오기
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atomsystem",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"homepage": "https://system.atomground.com",
|
|
6
6
|
"bin": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"next": "^15.2.4",
|
|
40
40
|
"react": "^18.0.0",
|
|
41
41
|
"react-dom": "^18.0.0",
|
|
42
|
+
"react-modal": "^3.16.3",
|
|
42
43
|
"sd-tailwindcss-transformer": "^2.0.0",
|
|
43
44
|
"style-dictionary": "^4.2.0"
|
|
44
45
|
},
|
|
@@ -46,6 +47,7 @@
|
|
|
46
47
|
"@types/node": "^20",
|
|
47
48
|
"@types/react": "^18",
|
|
48
49
|
"@types/react-dom": "^18",
|
|
50
|
+
"@types/react-modal": "^3.16.3",
|
|
49
51
|
"@vitest/browser": "^3.1.1",
|
|
50
52
|
"@vitest/coverage-v8": "^3.1.1",
|
|
51
53
|
"eslint": "^8",
|
package/token.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"color": {
|
|
3
3
|
"primary": { "value": "#2196f3", "type": "color" },
|
|
4
|
+
"secondary": { "value": "#162c4e", "type": "color" },
|
|
5
|
+
"danger": { "value": "#ff4a4a", "type": "color" },
|
|
6
|
+
"warning": { "value": "#ffa300", "type": "color" },
|
|
4
7
|
"border": {
|
|
5
8
|
"value": {
|
|
6
9
|
"light": "#ccc",
|
|
@@ -25,7 +28,7 @@
|
|
|
25
28
|
},
|
|
26
29
|
"text": {
|
|
27
30
|
"h1": {
|
|
28
|
-
"value":
|
|
31
|
+
"value": 38,
|
|
29
32
|
"type": "typography"
|
|
30
33
|
},
|
|
31
34
|
"h1-line-height": {
|
|
@@ -37,18 +40,42 @@
|
|
|
37
40
|
"type": "typography"
|
|
38
41
|
},
|
|
39
42
|
"h2": {
|
|
40
|
-
"value":
|
|
43
|
+
"value": 32,
|
|
41
44
|
"type": "typography"
|
|
42
45
|
},
|
|
43
46
|
"h2-line-height": {
|
|
44
|
-
"value":
|
|
47
|
+
"value": 42,
|
|
45
48
|
"type": "typography"
|
|
46
49
|
},
|
|
47
50
|
"h3": {
|
|
48
|
-
"value":
|
|
51
|
+
"value": 30,
|
|
49
52
|
"type": "typography"
|
|
50
53
|
},
|
|
51
54
|
"h3-line-height": {
|
|
55
|
+
"value": 38,
|
|
56
|
+
"type": "typography"
|
|
57
|
+
},
|
|
58
|
+
"h4": {
|
|
59
|
+
"value": 24,
|
|
60
|
+
"type": "typography"
|
|
61
|
+
},
|
|
62
|
+
"h4-line-height": {
|
|
63
|
+
"value": 38,
|
|
64
|
+
"type": "typography"
|
|
65
|
+
},
|
|
66
|
+
"h5": {
|
|
67
|
+
"value": 20,
|
|
68
|
+
"type": "typography"
|
|
69
|
+
},
|
|
70
|
+
"h5-line-height": {
|
|
71
|
+
"value": 32,
|
|
72
|
+
"type": "typography"
|
|
73
|
+
},
|
|
74
|
+
"h6": {
|
|
75
|
+
"value": 16,
|
|
76
|
+
"type": "typography"
|
|
77
|
+
},
|
|
78
|
+
"h6-line-height": {
|
|
52
79
|
"value": 24,
|
|
53
80
|
"type": "typography"
|
|
54
81
|
},
|
|
@@ -250,5 +277,13 @@
|
|
|
250
277
|
}
|
|
251
278
|
}
|
|
252
279
|
}
|
|
280
|
+
},
|
|
281
|
+
"modal": {
|
|
282
|
+
"background": {
|
|
283
|
+
"value": {
|
|
284
|
+
"light": "#fafafa",
|
|
285
|
+
"dark": "#333333"
|
|
286
|
+
}
|
|
287
|
+
}
|
|
253
288
|
}
|
|
254
289
|
}
|