atomsystem 0.1.48 → 0.1.49
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 +153 -55
- package/atom.design.md +155 -0
- package/atom.uxPattern.md +431 -0
- package/components/ui/Badge.tsx +20 -0
- package/components/ui/Button.tsx +33 -0
- package/components/ui/Card.tsx +20 -0
- package/components/ui/Input.tsx +35 -0
- package/components/ui/Modal.tsx +54 -0
- package/components/ui/index.ts +5 -0
- package/css/atom-ui.css +216 -0
- package/dist/cli.cjs +105 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +97 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +7897 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +218 -0
- package/dist/index.d.ts +218 -0
- package/dist/index.js +7845 -451
- package/dist/index.js.map +1 -0
- package/package.json +120 -81
- package/presets/default.design.md +155 -0
- package/presets/uxPattern.md +431 -0
- package/skills/atomsystem.skill.md +104 -0
- package/tokens/token.json +319 -0
- package/tokens/tokens.json +318 -976
- package/app/templates/Animations/Animation.tsx +0 -282
- package/app/templates/Badge/Badge.tsx +0 -28
- package/app/templates/Badge/badge.css +0 -28
- package/app/templates/Button/Button.tsx +0 -31
- package/app/templates/Button/button.css +0 -135
- package/app/templates/Card/Card.tsx +0 -50
- package/app/templates/Card/card.css +0 -17
- package/app/templates/Checkbox/Checkbox.tsx +0 -55
- package/app/templates/Checkbox/checkbox.css +0 -38
- package/app/templates/CodeBlock/CodeBlock.tsx +0 -68
- package/app/templates/Collapse/Collapse.tsx +0 -207
- package/app/templates/Collapse/collapse.css +0 -24
- package/app/templates/Dropdown/Dropdown.css +0 -112
- package/app/templates/Dropdown/Dropdown.tsx +0 -103
- package/app/templates/Input/Input.tsx +0 -29
- package/app/templates/Input/input.css +0 -24
- package/app/templates/Label/Label.tsx +0 -22
- package/app/templates/Modal/Modal.tsx +0 -71
- package/app/templates/Modal/modal.css +0 -98
- package/app/templates/Navigation/Navigation.tsx +0 -169
- package/app/templates/Navigation/navigation.css +0 -59
- package/app/templates/Pagination/Pagination.tsx +0 -104
- package/app/templates/Pagination/pagination.css +0 -66
- package/app/templates/Radio/Radio.tsx +0 -45
- package/app/templates/Radio/radio.css +0 -29
- package/app/templates/Select/Select.tsx +0 -143
- package/app/templates/Select/select.css +0 -47
- package/app/templates/Sidebar/Sidebar.tsx +0 -139
- package/app/templates/Sidebar/sidebar.css +0 -4
- package/app/templates/TOC/TOC.tsx +0 -165
- package/app/templates/Tab/Tab.tsx +0 -117
- package/app/templates/Tab/tab.css +0 -56
- package/app/templates/Table/Table.tsx +0 -54
- package/app/templates/Table/table.css +0 -58
- package/app/templates/Theme/Theme.tsx +0 -122
- package/app/templates/Toast/Toast.tsx +0 -106
- package/app/templates/Toast/toast.css +0 -69
- package/build/build-tokens.js +0 -223
- package/build/css/_variables.css +0 -251
- package/build/typescript/theme.ts +0 -6
- package/css/text.css +0 -18
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/** 테이블에 표시할 데이터 배열. 각 객체는 하나의 행을 나타냅니다.
|
|
2
|
-
* 객체의 키는 테이블 헤더가 되고, 값은 해당 셀의 내용이 됩니다.
|
|
3
|
-
*/
|
|
4
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
-
const Table = ({ tableData }: { tableData: Record<string, any>[] }) => {
|
|
6
|
-
// tableData가 제공되지 않았거나 비어있는 경우, 데이터 없음 메시지를 표시합니다.
|
|
7
|
-
if (!tableData || tableData.length === 0) {
|
|
8
|
-
return (
|
|
9
|
-
<div className="table-empty">
|
|
10
|
-
<p>표시할 데이터가 없습니다.</p>
|
|
11
|
-
</div>
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const headers = Object.keys(tableData[0]);
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<div className="table-container">
|
|
19
|
-
<table className="table">
|
|
20
|
-
<thead className="table-header">
|
|
21
|
-
<tr>
|
|
22
|
-
{/* 헤더 배열을 순회하며 각 헤더에 대한 <th>태그를 생성합니다. */}
|
|
23
|
-
{headers.map((header, index) => {
|
|
24
|
-
return (
|
|
25
|
-
<th key={index}>
|
|
26
|
-
{header}
|
|
27
|
-
</th>
|
|
28
|
-
);
|
|
29
|
-
})}
|
|
30
|
-
</tr>
|
|
31
|
-
</thead>
|
|
32
|
-
{/* 테이블 바디 부분 */}
|
|
33
|
-
<tbody className="table-body">
|
|
34
|
-
{/* tableData 배열을 순회하며 각 데이터 객체(행)에 대한 <tr> 태그를 생성합니다. */}
|
|
35
|
-
{tableData.map((row, rowIndex) => {
|
|
36
|
-
return (
|
|
37
|
-
<tr key={rowIndex} className="table-row">
|
|
38
|
-
{/* 각 행 내에서 헤더 배열을 다시 순회하며 해당 헤더에 맞는 셀(<td>)을 생성합니다. */}
|
|
39
|
-
{headers.map((header, colIndex) => (
|
|
40
|
-
<td key={colIndex} className="table-cell">
|
|
41
|
-
{/* 현재 행 객체(row)에서 해당 헤더(키)에 해당하는 값을 가져와 셀에 표시합니다. */}
|
|
42
|
-
{row[header]}
|
|
43
|
-
</td>
|
|
44
|
-
))}
|
|
45
|
-
</tr>
|
|
46
|
-
);
|
|
47
|
-
})}
|
|
48
|
-
</tbody>
|
|
49
|
-
</table>
|
|
50
|
-
</div>
|
|
51
|
-
);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export default Table;
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
.table-container {
|
|
2
|
-
overflow: auto;
|
|
3
|
-
width: 100%;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
.table {
|
|
7
|
-
min-width: 100%;
|
|
8
|
-
border-collapse: collapse;
|
|
9
|
-
border-radius: var(--radius-common);
|
|
10
|
-
border: 1px solid var(--color-table-header-border);
|
|
11
|
-
overflow: hidden;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.table-header {
|
|
15
|
-
background-color: var(--color-table-header-bg);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.table-header th {
|
|
19
|
-
padding: var(--spacing-common-y) var(--spacing-common-x);
|
|
20
|
-
text-align: center;
|
|
21
|
-
color: var(--color-table-header-text);
|
|
22
|
-
font-weight: var(--font-weight-header);
|
|
23
|
-
font-size: var(--text-body);
|
|
24
|
-
letter-spacing: 0.05em;
|
|
25
|
-
border-bottom: 1px solid var(--color-table-header-border);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.table-body {
|
|
29
|
-
/* Body styles if needed */
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
.table-row {
|
|
33
|
-
background-color: var(--color-table-body-bg);
|
|
34
|
-
transition: background-color 0.2s ease;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.table-row:nth-child(even) {
|
|
38
|
-
background-color: var(--color-table-body-bgEven);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.table-row:hover {
|
|
42
|
-
filter: brightness(0.95);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
.table-cell {
|
|
46
|
-
padding: var(--spacing-sm-y) var(--spacing-common-x);
|
|
47
|
-
color: var(--color-table-body-text);
|
|
48
|
-
font-size: var(--text-body);
|
|
49
|
-
border-bottom: 1px solid var(--color-table-body-border);
|
|
50
|
-
text-align: left;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.table-empty {
|
|
54
|
-
padding: var(--spacing-common-x);
|
|
55
|
-
text-align: center;
|
|
56
|
-
color: var(--color-text-standard);
|
|
57
|
-
font-size: var(--text-body);
|
|
58
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
4
|
-
import { Radio } from "../Radio/Radio";
|
|
5
|
-
|
|
6
|
-
import { themes } from "@/build/typescript/theme";
|
|
7
|
-
import { Button } from "../Button/Button";
|
|
8
|
-
import { ModalContent } from "../Modal/Modal";
|
|
9
|
-
|
|
10
|
-
export const ThemeSelector = () => {
|
|
11
|
-
const [selectedThemes, setSelectedThemes] = useState<Record<string, string>>({});
|
|
12
|
-
const [tempThemes, setTempThemes] = useState<Record<string, string>>({});
|
|
13
|
-
const [isThemeLoading, setThemeLoading] = useState(false);
|
|
14
|
-
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
15
|
-
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
const chageThemeValue = () => {
|
|
18
|
-
const newThemes: Record<string, string> = {};
|
|
19
|
-
Object.entries(themes).forEach(([group, groupThemes]) => {
|
|
20
|
-
const storedKey = group === "global" ? "theme" : `theme-${group}`;
|
|
21
|
-
const storedTheme = localStorage.getItem(storedKey) || groupThemes[0];
|
|
22
|
-
newThemes[group] = storedTheme;
|
|
23
|
-
});
|
|
24
|
-
setSelectedThemes(newThemes);
|
|
25
|
-
setTempThemes(newThemes);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
chageThemeValue();
|
|
29
|
-
setThemeLoading(true);
|
|
30
|
-
}, []);
|
|
31
|
-
|
|
32
|
-
const handleTempThemeChange = (group: string, theme: string) => {
|
|
33
|
-
setTempThemes((prev) => ({ ...prev, [group]: theme }));
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const handleApply = () => {
|
|
37
|
-
if (typeof window !== "undefined") {
|
|
38
|
-
Object.entries(tempThemes).forEach(([group, theme]) => {
|
|
39
|
-
const storedKey = group === "global" ? "theme" : `theme-${group}`;
|
|
40
|
-
localStorage.setItem(storedKey, theme);
|
|
41
|
-
});
|
|
42
|
-
window.dispatchEvent(new Event("storage"));
|
|
43
|
-
setSelectedThemes(tempThemes);
|
|
44
|
-
setIsModalOpen(false);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const handleOpenModal = () => {
|
|
49
|
-
setTempThemes(selectedThemes); // Reset to current applied themes
|
|
50
|
-
setIsModalOpen(true);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
if (isThemeLoading === false) {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<>
|
|
59
|
-
<Button type="primary" onClick={handleOpenModal} style={{ width: "100%" }}>
|
|
60
|
-
<div className="flex items-center justify-center gap-2">
|
|
61
|
-
<span className="material-symbols-outlined" style={{ fontSize: "18px" }}>palette</span>
|
|
62
|
-
<span>테마 설정</span>
|
|
63
|
-
</div>
|
|
64
|
-
</Button>
|
|
65
|
-
<ModalContent
|
|
66
|
-
isOpen={isModalOpen}
|
|
67
|
-
onRequestClose={() => setIsModalOpen(false)}
|
|
68
|
-
modalTitle="테마 설정"
|
|
69
|
-
modalFooter={
|
|
70
|
-
<div className="flex justify-end gap-2 w-full mt-4">
|
|
71
|
-
<Button type="gray" onClick={() => setIsModalOpen(false)}>취소</Button>
|
|
72
|
-
<Button type="primary" onClick={handleApply}>적용</Button>
|
|
73
|
-
</div>
|
|
74
|
-
}
|
|
75
|
-
>
|
|
76
|
-
<div className="flex flex-col gap-6 p-4">
|
|
77
|
-
{Object.entries(themes).map(([group, groupThemes]) => (
|
|
78
|
-
<div key={group} className="flex flex-col gap-3">
|
|
79
|
-
<p className="font-bold capitalize">{group} Theme</p>
|
|
80
|
-
<div className="flex gap-4 flex-wrap">
|
|
81
|
-
{groupThemes.map((theme) => (
|
|
82
|
-
<Radio
|
|
83
|
-
name={`theme-${group}-modal`}
|
|
84
|
-
value={theme}
|
|
85
|
-
key={theme}
|
|
86
|
-
onChange={() => handleTempThemeChange(group, theme)}
|
|
87
|
-
checked={theme === tempThemes[group]}
|
|
88
|
-
/>
|
|
89
|
-
))}
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
))}
|
|
93
|
-
</div>
|
|
94
|
-
</ModalContent>
|
|
95
|
-
</>
|
|
96
|
-
);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
export const ThemeProvider = () => {
|
|
100
|
-
const [isThemeLoading, setThemeLoading] = useState(false);
|
|
101
|
-
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
const listenStorageChange = () => {
|
|
104
|
-
Object.entries(themes).forEach(([group, groupThemes]) => {
|
|
105
|
-
const storedKey = group === "global" ? "theme" : `theme-${group}`;
|
|
106
|
-
const currentTheme = localStorage.getItem(storedKey) || groupThemes[0];
|
|
107
|
-
|
|
108
|
-
document.documentElement.classList.remove(...groupThemes);
|
|
109
|
-
document.documentElement.classList.add(currentTheme);
|
|
110
|
-
});
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
listenStorageChange();
|
|
114
|
-
window.addEventListener("storage", listenStorageChange);
|
|
115
|
-
setThemeLoading(true);
|
|
116
|
-
return () => window.removeEventListener("storage", listenStorageChange);
|
|
117
|
-
}, []);
|
|
118
|
-
|
|
119
|
-
if (isThemeLoading === false) {
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
};
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState, useRef } from "react";
|
|
4
|
-
import gsap from "gsap";
|
|
5
|
-
import { useGSAP } from "@gsap/react";
|
|
6
|
-
|
|
7
|
-
gsap.registerPlugin(useGSAP);
|
|
8
|
-
|
|
9
|
-
// ✅ Toast Message 타입 정의
|
|
10
|
-
export type ToastMessage = {
|
|
11
|
-
id: string;
|
|
12
|
-
status?: "standard" | "primary" | "secondary" | "success" | "danger" | "warning" | "info" | "error";
|
|
13
|
-
message: string;
|
|
14
|
-
desc?: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// ✅ 전역 메시지 저장소와 구독 시스템
|
|
18
|
-
let messages: ToastMessage[] = [];
|
|
19
|
-
let listener: ((messages: ToastMessage[]) => void) | null = null;
|
|
20
|
-
|
|
21
|
-
export const addToast = (msg: Omit<ToastMessage, "id">) => {
|
|
22
|
-
const id = Math.random().toString(36).substring(2) + Date.now().toString(36);
|
|
23
|
-
messages = [...messages, { id, ...msg }];
|
|
24
|
-
// messages 값 listener에 반영
|
|
25
|
-
listener?.(messages);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const removeToast = (id: string) => {
|
|
29
|
-
messages = messages.filter((m) => m.id !== id);
|
|
30
|
-
listener?.(messages);
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// 콜백으로 message value 추적적
|
|
34
|
-
const subscribeToast = (cb: (messages: ToastMessage[]) => void) => {
|
|
35
|
-
listener = cb;
|
|
36
|
-
cb(messages);
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const unsubscribeToast = () => {
|
|
40
|
-
listener = null;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// ✅ useToast 훅 제공
|
|
44
|
-
export const useToast = () => {
|
|
45
|
-
return addToast;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// ✅ ToastContainer 컴포넌트 (내부 ToastItem 포함)
|
|
49
|
-
export const Toast = () => {
|
|
50
|
-
const [messages, setMessages] = useState<ToastMessage[]>([]);
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
subscribeToast(setMessages);
|
|
54
|
-
return () => unsubscribeToast();
|
|
55
|
-
}, []);
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<div className="toast-container">
|
|
59
|
-
{messages.map((message) => (
|
|
60
|
-
<ToastItem
|
|
61
|
-
key={message.id}
|
|
62
|
-
message={message}
|
|
63
|
-
remove={() => removeToast(message.id)}
|
|
64
|
-
/>
|
|
65
|
-
))}
|
|
66
|
-
</div>
|
|
67
|
-
);
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const ToastItem = ({
|
|
71
|
-
message,
|
|
72
|
-
remove,
|
|
73
|
-
}: {
|
|
74
|
-
message: ToastMessage;
|
|
75
|
-
remove: () => void;
|
|
76
|
-
}) => {
|
|
77
|
-
const elRef = useRef<HTMLDivElement>(null);
|
|
78
|
-
|
|
79
|
-
useGSAP(() => {
|
|
80
|
-
if (!elRef.current) return;
|
|
81
|
-
|
|
82
|
-
const tl = gsap.timeline({ onComplete: remove });
|
|
83
|
-
tl.from(elRef.current, {
|
|
84
|
-
opacity: 0,
|
|
85
|
-
y: 50,
|
|
86
|
-
ease: "power4.out",
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
tl.to(elRef.current, {
|
|
90
|
-
delay: 3,
|
|
91
|
-
y: 50,
|
|
92
|
-
opacity: 0,
|
|
93
|
-
ease: "power4.out",
|
|
94
|
-
});
|
|
95
|
-
}, []);
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<div
|
|
99
|
-
ref={elRef}
|
|
100
|
-
className={`toast-item ${message.status || "standard"}`}
|
|
101
|
-
>
|
|
102
|
-
<p>{message.message}</p>
|
|
103
|
-
{message.desc ? <p className="toast-desc">{message.desc}</p> : null}
|
|
104
|
-
</div>
|
|
105
|
-
);
|
|
106
|
-
};
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
.toast-container {
|
|
2
|
-
position: fixed;
|
|
3
|
-
display: flex;
|
|
4
|
-
flex-direction: column;
|
|
5
|
-
gap: var(--spacing-sm-y);
|
|
6
|
-
bottom: 24px;
|
|
7
|
-
right: 24px;
|
|
8
|
-
z-index: 50;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
.toast-item {
|
|
12
|
-
background-color: var(--toast-bg, var(--color-bg-card));
|
|
13
|
-
border: 1px solid var(--toast-border, var(--border-standard));
|
|
14
|
-
border-radius: var(--radius-common);
|
|
15
|
-
color: var(--toast-color, var(--text-standard));
|
|
16
|
-
padding: var(--spacing-common-x);
|
|
17
|
-
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
18
|
-
min-width: 250px;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/* Status Variants */
|
|
22
|
-
.toast-item.standard {
|
|
23
|
-
--toast-bg: var(--color-bg-card);
|
|
24
|
-
--toast-border: var(--border-standard);
|
|
25
|
-
--toast-color: var(--text-standard);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.toast-item.primary {
|
|
29
|
-
--toast-bg: var(--color-primary);
|
|
30
|
-
--toast-border: var(--color-primary);
|
|
31
|
-
--toast-color: var(--color-white);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
.toast-item.secondary {
|
|
35
|
-
--toast-bg: var(--color-secondary);
|
|
36
|
-
--toast-border: var(--color-secondary);
|
|
37
|
-
--toast-color: var(--color-white);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
.toast-item.success {
|
|
41
|
-
--toast-bg: var(--color-success);
|
|
42
|
-
--toast-border: var(--color-success);
|
|
43
|
-
--toast-color: var(--color-white);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.toast-item.danger,
|
|
47
|
-
.toast-item.error {
|
|
48
|
-
--toast-bg: var(--color-danger);
|
|
49
|
-
--toast-border: var(--color-danger);
|
|
50
|
-
--toast-color: var(--color-white);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
.toast-item.warning {
|
|
54
|
-
--toast-bg: var(--color-warning);
|
|
55
|
-
--toast-border: var(--color-warning);
|
|
56
|
-
--toast-color: var(--color-white);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.toast-item.info {
|
|
60
|
-
--toast-bg: var(--color-info);
|
|
61
|
-
--toast-border: var(--color-info);
|
|
62
|
-
--toast-color: var(--color-white);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.toast-desc {
|
|
66
|
-
font-size: var(--text-sm);
|
|
67
|
-
margin-top: var(--spacing-gap-small);
|
|
68
|
-
opacity: 0.9;
|
|
69
|
-
}
|
package/build/build-tokens.js
DELETED
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import StyleDictionary from "style-dictionary";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// 1. Style Dictionary 인스턴스 초기화
|
|
5
|
-
const myStyleDictionary = new StyleDictionary({
|
|
6
|
-
source: ["tokens/tokens.json"],
|
|
7
|
-
preprocessors: ['flatten-global'],
|
|
8
|
-
hooks: {
|
|
9
|
-
preprocessors: {
|
|
10
|
-
'flatten-global': (dictionary) => {
|
|
11
|
-
// 1. global 객체를 찾습니다.
|
|
12
|
-
const { global, ...rest } = dictionary;
|
|
13
|
-
|
|
14
|
-
// 2. global 내부의 내용을 루트로 끌어올려 병합합니다.
|
|
15
|
-
// 이렇게 하면 {palette.white} 참조가 루트 레벨에서 해석 가능해집니다.
|
|
16
|
-
return {
|
|
17
|
-
...global,
|
|
18
|
-
...rest
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
platforms: {
|
|
24
|
-
css: {
|
|
25
|
-
transformGroup: "css", // CSS 기본 변환 적용 (예: size/rem, color/hex 등)
|
|
26
|
-
buildPath: "build/css/",
|
|
27
|
-
files: [
|
|
28
|
-
{
|
|
29
|
-
destination: "_variables.css",
|
|
30
|
-
format: "myCustomFormat", // 아래에서 정의한 커스텀 포맷 사용
|
|
31
|
-
options: {
|
|
32
|
-
outputReferences: true, // CSS 변수 참조 활성화 (var(--name))
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
},
|
|
37
|
-
typescript: {
|
|
38
|
-
transformGroup: "js",
|
|
39
|
-
buildPath: "build/typescript/",
|
|
40
|
-
files: [
|
|
41
|
-
{
|
|
42
|
-
destination: "theme.ts",
|
|
43
|
-
format: "theme",
|
|
44
|
-
},
|
|
45
|
-
],
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
log: {
|
|
49
|
-
verbosity: 'verbose', // 어떤 reference를 못 찾는지 정확히 찍어줍니다.
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// 2. 커스텀 포맷 등록
|
|
54
|
-
// 토큰 그룹을 CSS 변수 블록으로 변환합니다.
|
|
55
|
-
// - 'global' 카테고리 -> @theme 블록
|
|
56
|
-
// - 그 외 카테고리 -> .{category} 클래스 블록
|
|
57
|
-
myStyleDictionary.registerFormat({
|
|
58
|
-
name: "myCustomFormat",
|
|
59
|
-
format: function ({ dictionary, options }) {
|
|
60
|
-
// [헬퍼 함수]
|
|
61
|
-
// 1. getName: 토큰의 계층(type, item, subitem)을 하이픈(-)으로 연결하여 CSS 변수명 생성
|
|
62
|
-
// 예: palette.gray.500 -> palette-gray-500
|
|
63
|
-
const getName = (token) => token.path.slice(1).join("-");
|
|
64
|
-
|
|
65
|
-
// 2. getSemanticName: 시멘틱 토큰명을 생성 (예: color-card)
|
|
66
|
-
// 이름 규칙: $type(color)-...path -> color-bg-card
|
|
67
|
-
const getSemanticName = (token) => {
|
|
68
|
-
if (token.$type === "color") {
|
|
69
|
-
return [token.$type, ...token.path.slice(1)].join("-");
|
|
70
|
-
} else {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// [참조 맵 생성]
|
|
76
|
-
// {global.palette.white} 같은 참조 경로를 실제 토큰 객체로 찾기 위한 Map 생성
|
|
77
|
-
const tokenMap = new Map(
|
|
78
|
-
dictionary.allTokens.map((t) => [t.path.join("."), t])
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
// [토큰 그룹화]
|
|
82
|
-
// tokens.json의 최상위 카테고리(global, white, dark)별로 토큰을 묶음
|
|
83
|
-
const grouped = dictionary.allTokens.reduce((acc, token) => {
|
|
84
|
-
const category = token.attributes.category;
|
|
85
|
-
if (!acc[category]) acc[category] = [];
|
|
86
|
-
acc[category].push(token);
|
|
87
|
-
return acc;
|
|
88
|
-
}, {});
|
|
89
|
-
|
|
90
|
-
// [시멘틱 변수 수집 로직]
|
|
91
|
-
// global이 아닌 테마별 토큰(white, dark 등)을 순회하며 시멘틱 변수 정의를 수집함
|
|
92
|
-
// 목적: @theme 블록 안에 "--color-card: var(--card);" 형태의 매핑을 한 번만 선언하기 위함.
|
|
93
|
-
// 각 테마 파일(.white, .dark)에서는 --card 변수만 재정의하면 됨.
|
|
94
|
-
const themeTokens = dictionary.allTokens.filter(t => t.attributes.category !== 'palette');
|
|
95
|
-
const semanticMap = new Map();
|
|
96
|
-
|
|
97
|
-
themeTokens.forEach(token => {
|
|
98
|
-
const semanticName = getSemanticName(token); // 예: color-card
|
|
99
|
-
const varName = getName(token); // 예: card
|
|
100
|
-
|
|
101
|
-
// 중복 선언 방지: 동일한 시멘틱 이름이 여러 테마에 있어도 한 번만 수집
|
|
102
|
-
if (semanticName && !semanticMap.has(semanticName)) {
|
|
103
|
-
semanticMap.set(semanticName, varName);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// 수집된 시멘틱 변수들을 CSS 문자열로 변환
|
|
108
|
-
const semanticCss = Array.from(semanticMap.entries())
|
|
109
|
-
.map(([name, val]) => ` --${name}: var(--${val});`)
|
|
110
|
-
.join('\n');
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// [최종 CSS 생성]
|
|
114
|
-
// 그룹별로 순회하며 CSS 블록 생성
|
|
115
|
-
return Object.entries(grouped)
|
|
116
|
-
.map(([category, tokens]) => {
|
|
117
|
-
// 선택자 결정:
|
|
118
|
-
// - 'global' 카테고리는 Tailwind CSS v4의 '@theme' 블록으로 변환
|
|
119
|
-
// - 나머지(white, dark)는 클래스 선택자(예: .white)로 변환
|
|
120
|
-
const selector = category === "palette" ? "@theme" : `.${category}`;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// 정렬
|
|
124
|
-
const typeOrder = ["color", "fontSizes", "fontWeights", "fontFamily", "borderRadius"];
|
|
125
|
-
|
|
126
|
-
const cssVariables = tokens
|
|
127
|
-
.sort((a, b) => {
|
|
128
|
-
const indexA = typeOrder.indexOf(a.$type);
|
|
129
|
-
const indexB = typeOrder.indexOf(b.$type);
|
|
130
|
-
if (indexA !== -1 && indexB !== -1) return indexA - indexB;
|
|
131
|
-
if (indexA !== -1) return -1;
|
|
132
|
-
if (indexB !== -1) return 1;
|
|
133
|
-
return 0;
|
|
134
|
-
})
|
|
135
|
-
.map((token) => {
|
|
136
|
-
const name = getName(token);
|
|
137
|
-
// 기본 값. 참조가 있다면 원본 값("{global.palette...}")을 가져옴
|
|
138
|
-
let value = token.$value || token.value;
|
|
139
|
-
const originalValue = token.original.$value || token.original.value;
|
|
140
|
-
|
|
141
|
-
// [수동 참조 해결]
|
|
142
|
-
// 값이 "{...}" 형태의 참조인 경우, 해당 참조를 var(--이름) 형태로 변환
|
|
143
|
-
if (
|
|
144
|
-
options?.outputReferences &&
|
|
145
|
-
typeof originalValue === "string" && // 값이 문자열이고
|
|
146
|
-
originalValue.includes("{") // '{'를 포함하면 참조로 간주
|
|
147
|
-
) {
|
|
148
|
-
value = originalValue.replace(/{([^}]+)}/g, (match, pathStr) => {
|
|
149
|
-
const refToken = tokenMap.get(pathStr);
|
|
150
|
-
if (refToken) {
|
|
151
|
-
return `var(--${getName(refToken)})`;
|
|
152
|
-
}
|
|
153
|
-
return match;
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (category === "palette") {
|
|
158
|
-
// $type을 확인하여 borderRadius나 dimension이면 px 추가
|
|
159
|
-
if(token.$type === "borderRadius" | token.$type === "fontSizes" | token.$type === "spacing" | token.$type === "sizing" | token.$type === "dimension") {
|
|
160
|
-
if (!String(value).endsWith("px")) {
|
|
161
|
-
return `--${name}: ${value}px;`;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return `--${name}: ${value};`;
|
|
165
|
-
}
|
|
166
|
-
// 그 외 카테고리(테마)
|
|
167
|
-
if (token.$type === "color") {
|
|
168
|
-
return `--${name}: ${value};`;
|
|
169
|
-
}
|
|
170
|
-
if (token.$type === "spacing" || token.$type === "sizing") {
|
|
171
|
-
if (!String(value).endsWith("px") && !String(value).includes("var(")) {
|
|
172
|
-
return `--${name}: ${value}px;`;
|
|
173
|
-
}
|
|
174
|
-
return `--${name}: ${value};`;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return null;
|
|
178
|
-
})
|
|
179
|
-
.filter(Boolean)
|
|
180
|
-
.join("\n");
|
|
181
|
-
|
|
182
|
-
// global(@theme)인 경우에만 수집해둔 시멘틱 변수(semanticCss)를 추가
|
|
183
|
-
// @theme: Tailwind 유틸리티 클래스 생성에 필요 (bg-primary, text-secondary 등)
|
|
184
|
-
// Tailwind CSS v4의 @theme은 빌드 시 자동으로 :root에도 등록되므로 별도 :root 선언 불필요
|
|
185
|
-
if (selector === "@theme") {
|
|
186
|
-
return `@theme {\n${cssVariables}\n\n${semanticCss}\n}`;
|
|
187
|
-
}
|
|
188
|
-
return `${selector} {\n${cssVariables}\n} `;
|
|
189
|
-
})
|
|
190
|
-
.join("\n\n");
|
|
191
|
-
},
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// 3. theme와 관련된 커스텀 포맷 등록
|
|
195
|
-
// theme.ts 파일을 위한 포맷 등록
|
|
196
|
-
myStyleDictionary.registerFormat({
|
|
197
|
-
name: "theme",
|
|
198
|
-
format: function ({ dictionary }) {
|
|
199
|
-
const groupedThemes = dictionary.allTokens.reduce((acc, token) => {
|
|
200
|
-
const category = token.attributes.category;
|
|
201
|
-
if (category === "palette") return acc;
|
|
202
|
-
|
|
203
|
-
if (category.includes("-")) {
|
|
204
|
-
const prefix = category.split("-")[0];
|
|
205
|
-
if (!acc[prefix]) acc[prefix] = [];
|
|
206
|
-
if (!acc[prefix].includes(category)) {
|
|
207
|
-
acc[prefix].push(category);
|
|
208
|
-
}
|
|
209
|
-
} else {
|
|
210
|
-
if (!acc.global) acc.global = [];
|
|
211
|
-
if (!acc.global.includes(category)) {
|
|
212
|
-
acc.global.push(category);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return acc;
|
|
216
|
-
}, { global: [] });
|
|
217
|
-
|
|
218
|
-
return `export const themes: Record<string, string[]> = ${JSON.stringify(groupedThemes, null, 2)};`;
|
|
219
|
-
},
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// 3. 모든 플랫폼 빌드
|
|
223
|
-
await myStyleDictionary.buildAllPlatforms();
|