atomsystem 0.1.16 → 0.1.18
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/Button/Button.tsx +6 -6
- package/app/templates/Card/Card.tsx +1 -1
- package/app/templates/CodeBlock/CodeBlock.tsx +28 -6
- package/app/templates/GlobalNav/GlobalNav.tsx +30 -38
- package/app/templates/Input/Input.tsx +123 -119
- package/app/templates/TOC/TOC.tsx +60 -0
- package/app/templates/Tab/Tab.tsx +93 -0
- package/app/templates/Theme/Theme.tsx +22 -22
- package/app/templates/Toast/Toast.tsx +106 -0
- package/cli/init.js +1 -16
- package/package.json +58 -51
- package/token.json +154 -1115
- package/tailwind.config.ts +0 -160
- package/tokens/buildMergeCss.js +0 -87
- package/tokens/buildTokenSplit.js +0 -81
- package/tokens/setorder/token_dark.json +0 -390
- package/tokens/setorder/token_digitalFont.json +0 -6
- package/tokens/setorder/token_global.json +0 -888
- package/tokens/token/dark.json +0 -904
- package/tokens/token/digitalFont.json +0 -882
- package/tokens/token/global.json +0 -882
- package/tokens/token/inputCircle.json +0 -888
- package/tokens/token/inputStandard.json +0 -888
- package/tokens/token/standard.json +0 -976
|
@@ -15,25 +15,25 @@ export const Button = ({
|
|
|
15
15
|
return (
|
|
16
16
|
<button
|
|
17
17
|
className={clsx(
|
|
18
|
-
"px-4 py-2 rounded-common text-
|
|
18
|
+
"px-4 py-2 rounded-common text-white transition-all cursor-pointer rounded-[var(--global-rounded)]",
|
|
19
19
|
{
|
|
20
|
-
"bg-button-primary-standard hover:bg-button-primary-hover
|
|
20
|
+
"bg-[var(--button-main-primary-standard)] hover:bg-[var(--button-main-primary-hover)] active:bg-[var(--button-main-primary-active)] standard:bg-[var(--buttion-main-primary-standard)]/20":
|
|
21
21
|
type == "primary",
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
|
-
"bg-button-secondary-standard hover:bg-button-secondary-hover
|
|
24
|
+
"bg-[var(--button-main-secondary-standard)] hover:bg-[var(--button-main-secondary-hover)] active:bg-[var(--button-main-secondary-active)] standard:bg-[var(--buttion-main-secondary-standard)]/20 ":
|
|
25
25
|
type == "secondary",
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
|
-
"bg-button-success-standard hover:bg-button-success-hover
|
|
28
|
+
"bg-[var(--button-main-success-standard)] hover:bg-[var(--button-main-success-hover)] active:bg-[var(--button-main-success-active)] standard:bg-[var(--buttion-main-success-standard)]/20":
|
|
29
29
|
type == "success",
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
|
-
"bg-button-danger-standard hover:bg-button-danger-hover
|
|
32
|
+
"bg-[var(--button-main-danger-standard)] hover:bg-[var(--button-main-danger-hover)] active:bg-[var(--button-main-danger-active)] standard:bg-[var(--buttion-main-danger-standard)]/20":
|
|
33
33
|
type == "danger",
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
|
-
"bg-button-gray-standard hover:bg-button-gray-hover
|
|
36
|
+
"bg-[var(--button-main-gray-standard)] hover:bg-[var(--button-main-gray-hover)] active:bg-[var(--button-main-gray-active)] standard:bg-[var(--buttion-main-gray-standard)]/20":
|
|
37
37
|
type == "gray",
|
|
38
38
|
}
|
|
39
39
|
)}
|
|
@@ -19,7 +19,7 @@ export const Card = ({
|
|
|
19
19
|
);
|
|
20
20
|
return (
|
|
21
21
|
<div
|
|
22
|
-
className={`${type == "standard" ? "bg-
|
|
22
|
+
className={`${type == "standard" ? "bg-[var(--background-card)] border-[var(--color-border)]" : type == "group" ? "bg-fill-group border-none" : ""} ${!hasCardContent ? "p-4" : ""} rounded border ${className} overflow-auto`}
|
|
23
23
|
>
|
|
24
24
|
{children}
|
|
25
25
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import hljs from "highlight.js";
|
|
4
4
|
import "highlight.js/styles/atom-one-dark.css"; // 스타일 파일
|
|
5
5
|
|
|
@@ -11,6 +11,7 @@ const CodeBlock = ({
|
|
|
11
11
|
children: React.ReactNode;
|
|
12
12
|
}) => {
|
|
13
13
|
const codeRef = useRef<HTMLElement | null>(null);
|
|
14
|
+
const [copied, setCopied] = useState(false);
|
|
14
15
|
|
|
15
16
|
useEffect(() => {
|
|
16
17
|
if (codeRef.current) {
|
|
@@ -18,12 +19,33 @@ const CodeBlock = ({
|
|
|
18
19
|
}
|
|
19
20
|
}, [children]);
|
|
20
21
|
|
|
22
|
+
const handleCopy = async () => {
|
|
23
|
+
if (!codeRef.current) return;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
await navigator.clipboard.writeText(codeRef.current.innerText);
|
|
27
|
+
setCopied(true);
|
|
28
|
+
setTimeout(() => setCopied(false), 1500); // 1.5초 후 메시지 제거
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.error("복사 실패:", err);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
21
34
|
return (
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
<div className="relative">
|
|
36
|
+
<pre>
|
|
37
|
+
<code ref={codeRef} className={language}>
|
|
38
|
+
{children}
|
|
39
|
+
</code>
|
|
40
|
+
</pre>
|
|
41
|
+
|
|
42
|
+
<button
|
|
43
|
+
onClick={handleCopy}
|
|
44
|
+
className="absolute top-2 right-2 bg-white text-black text-xs px-2 py-1 rounded hover:bg-gray-200"
|
|
45
|
+
>
|
|
46
|
+
{copied ? "Copied!" : "Copy"}
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
27
49
|
);
|
|
28
50
|
};
|
|
29
51
|
|
|
@@ -2,12 +2,25 @@
|
|
|
2
2
|
import Link from "next/link";
|
|
3
3
|
import Image from "next/image";
|
|
4
4
|
import { useEffect, useState } from "react";
|
|
5
|
+
import { usePathname } from "next/navigation";
|
|
6
|
+
import clsx from "clsx";
|
|
5
7
|
|
|
6
8
|
const GlobalNav = () => {
|
|
7
9
|
const [windowSize, setWindowSize] = useState<number>(); // 초기값 설정
|
|
8
10
|
|
|
9
11
|
const [toggleMenu, setToggleMenu] = useState<boolean>(false);
|
|
10
12
|
|
|
13
|
+
const pathname = usePathname();
|
|
14
|
+
const navMenu = [
|
|
15
|
+
{ name: "시작하기", path: "/" },
|
|
16
|
+
{ name: "설치하기", path: "/install" },
|
|
17
|
+
{ name: "입력창", path: "/component/input" },
|
|
18
|
+
{ name: "선택창", path: "/component/select" },
|
|
19
|
+
{ name: "체크박스", path: "/component/checkbox" },
|
|
20
|
+
{ name: "라디오", path: "/component/radio" },
|
|
21
|
+
{ name: "버튼", path: "/component/button" },
|
|
22
|
+
];
|
|
23
|
+
|
|
11
24
|
// 윈도우 사이즈 추적
|
|
12
25
|
useEffect(() => {
|
|
13
26
|
const handleResize = () => {
|
|
@@ -20,7 +33,6 @@ const GlobalNav = () => {
|
|
|
20
33
|
}, [windowSize]);
|
|
21
34
|
|
|
22
35
|
// 초기 토글 값 설정
|
|
23
|
-
|
|
24
36
|
useEffect(() => {
|
|
25
37
|
if (window.innerWidth > 768) {
|
|
26
38
|
setToggleMenu(true);
|
|
@@ -36,7 +48,7 @@ const GlobalNav = () => {
|
|
|
36
48
|
return (
|
|
37
49
|
<>
|
|
38
50
|
<nav
|
|
39
|
-
className={`fixed w-full max-w-80 h-full bg-
|
|
51
|
+
className={`fixed top-0 md:relative w-full max-w-80 h-full bg-[var(--background-card)] text-text-standard z-20 left-0 ${toggleMenu ? "block" : "hidden"}`}
|
|
40
52
|
>
|
|
41
53
|
<div className="brand">
|
|
42
54
|
<div className="flex items-center justify-between">
|
|
@@ -60,46 +72,26 @@ const GlobalNav = () => {
|
|
|
60
72
|
</button>
|
|
61
73
|
</div>
|
|
62
74
|
</div>
|
|
75
|
+
|
|
63
76
|
<ul>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
</li>
|
|
79
|
-
<li>
|
|
80
|
-
<Link href={"/component/select"} className="px-4 py-2 block">
|
|
81
|
-
선택창
|
|
82
|
-
</Link>
|
|
83
|
-
</li>
|
|
84
|
-
<li>
|
|
85
|
-
<Link href={"/component/checkbox"} className="px-4 py-2 block">
|
|
86
|
-
체크박스
|
|
87
|
-
</Link>
|
|
88
|
-
</li>
|
|
89
|
-
<li>
|
|
90
|
-
<Link href={"/component/radio"} className="px-4 py-2 block">
|
|
91
|
-
라디오
|
|
92
|
-
</Link>
|
|
93
|
-
</li>
|
|
94
|
-
<li>
|
|
95
|
-
<Link href={"/component/button"} className="px-4 py-2 block">
|
|
96
|
-
버튼
|
|
97
|
-
</Link>
|
|
98
|
-
</li>
|
|
77
|
+
{navMenu.map((menu) => {
|
|
78
|
+
return (
|
|
79
|
+
<li key={menu.name}>
|
|
80
|
+
<Link
|
|
81
|
+
href={menu.path}
|
|
82
|
+
className={clsx("px-4 py-2 block", {
|
|
83
|
+
"text-primary": menu.path == pathname,
|
|
84
|
+
})}
|
|
85
|
+
>
|
|
86
|
+
{menu.name}
|
|
87
|
+
</Link>
|
|
88
|
+
</li>
|
|
89
|
+
);
|
|
90
|
+
})}
|
|
99
91
|
</ul>
|
|
100
92
|
</nav>
|
|
101
93
|
<button
|
|
102
|
-
className={`fixed shadow-md rounded-full bg-
|
|
94
|
+
className={`fixed shadow-md rounded-full bg-[var(--background-card)] w-12 h-12 top-6 left-6 z-10 ${toggleMenu ? "hidden" : "block"} `}
|
|
103
95
|
>
|
|
104
96
|
<span
|
|
105
97
|
className="material-symbols-outlined"
|
|
@@ -4,45 +4,21 @@ import { useEffect, useRef, useState } from "react";
|
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
|
|
6
6
|
export const Input = ({
|
|
7
|
-
|
|
8
|
-
defaultValue = "",
|
|
7
|
+
type = "text",
|
|
9
8
|
placeholder = "값을 입력해주세요",
|
|
10
9
|
label,
|
|
11
10
|
readOnly,
|
|
12
11
|
disabled,
|
|
13
|
-
pattern,
|
|
14
12
|
required,
|
|
15
13
|
}: {
|
|
14
|
+
type?: string;
|
|
16
15
|
className?: string;
|
|
17
|
-
defaultValue?: string;
|
|
18
16
|
placeholder?: string;
|
|
19
17
|
label?: string;
|
|
20
18
|
readOnly?: boolean;
|
|
21
19
|
disabled?: boolean;
|
|
22
|
-
pattern?: string;
|
|
23
20
|
required?: boolean;
|
|
24
21
|
}) => {
|
|
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
22
|
return (
|
|
47
23
|
<>
|
|
48
24
|
{label ? (
|
|
@@ -57,43 +33,36 @@ export const Input = ({
|
|
|
57
33
|
</p>
|
|
58
34
|
) : null}
|
|
59
35
|
<input
|
|
60
|
-
type=
|
|
61
|
-
className=
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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}
|
|
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)] "
|
|
38
|
+
style={{
|
|
39
|
+
color: "var(--input-text-standard)",
|
|
40
|
+
paddingLeft: "var(--global-padding-x)",
|
|
41
|
+
paddingRight: "var(--global-padding-x)",
|
|
42
|
+
paddingTop: "var(--global-padding-y)",
|
|
43
|
+
paddingBottom: "var(--global-padding-y)",
|
|
44
|
+
borderRadius: "var(--input-rounded)",
|
|
45
|
+
}}
|
|
79
46
|
placeholder={placeholder}
|
|
80
47
|
readOnly={readOnly}
|
|
81
48
|
disabled={disabled}
|
|
82
|
-
pattern={pattern}
|
|
83
49
|
required={required}
|
|
84
50
|
/>
|
|
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
51
|
</>
|
|
93
52
|
);
|
|
94
53
|
};
|
|
95
54
|
|
|
96
|
-
export const Select = ({
|
|
55
|
+
export const Select = ({
|
|
56
|
+
options,
|
|
57
|
+
label,
|
|
58
|
+
required,
|
|
59
|
+
disabled,
|
|
60
|
+
}: {
|
|
61
|
+
options: string[];
|
|
62
|
+
label?: string;
|
|
63
|
+
required?: boolean;
|
|
64
|
+
disabled?: boolean;
|
|
65
|
+
}) => {
|
|
97
66
|
const [filterValue, setFilterValue] = useState("");
|
|
98
67
|
const [isOpen, setIsOpen] = useState(false);
|
|
99
68
|
const [menuPosition, setMenuPosition] = useState({
|
|
@@ -136,6 +105,17 @@ export const Select = ({ options }: { options: string[] }) => {
|
|
|
136
105
|
|
|
137
106
|
return (
|
|
138
107
|
<div className="relative">
|
|
108
|
+
{label ? (
|
|
109
|
+
<p className={clsx("mb-1 text-text-standard", {})}>
|
|
110
|
+
<span
|
|
111
|
+
className={clsx("", {
|
|
112
|
+
"after:content-['*'] after:ml-1 after:text-danger": required,
|
|
113
|
+
})}
|
|
114
|
+
>
|
|
115
|
+
{label}
|
|
116
|
+
</span>
|
|
117
|
+
</p>
|
|
118
|
+
) : null}
|
|
139
119
|
<label className="relative w-fit hidden md:block">
|
|
140
120
|
<input
|
|
141
121
|
ref={inputRef}
|
|
@@ -147,11 +127,15 @@ export const Select = ({ options }: { options: string[] }) => {
|
|
|
147
127
|
}}
|
|
148
128
|
onFocus={() => setIsOpen(true)}
|
|
149
129
|
className={clsx(
|
|
150
|
-
"transition-all bg-input-background-standard border px-4 py-2 border-
|
|
151
|
-
{ "placeholder:text
|
|
152
|
-
{ "focus:border-input-border-focus": true },
|
|
153
|
-
{
|
|
130
|
+
"transition-all bg-[var(--input-background-standard)] border px-4 py-2 border-[var(--color-border)] text-[var(--text-standard)] rounded-[var(--input-rounded)]",
|
|
131
|
+
{ "placeholder:text[var(--input-text-placeholder)]": true },
|
|
132
|
+
{ "focus:border-[var(--input-border-focus)]": true },
|
|
133
|
+
{
|
|
134
|
+
"disabled:bg-[var(--input-background-disabled)] disabled:cursor-not-allowed":
|
|
135
|
+
disabled,
|
|
136
|
+
}
|
|
154
137
|
)}
|
|
138
|
+
disabled={disabled}
|
|
155
139
|
/>
|
|
156
140
|
<div
|
|
157
141
|
className="absolute flex items-center right-0 top-0 h-full px-2"
|
|
@@ -166,7 +150,7 @@ export const Select = ({ options }: { options: string[] }) => {
|
|
|
166
150
|
createPortal(
|
|
167
151
|
<ul
|
|
168
152
|
ref={dropdownRef}
|
|
169
|
-
className="absolute w-full border rounded bg-input-background-standard border-input-border-standard shadow-md mt-1"
|
|
153
|
+
className="absolute w-full border rounded bg-[var(--input-background-standard)] border-[var(--input-border-standard)] shadow-md mt-1"
|
|
170
154
|
style={{
|
|
171
155
|
top: menuPosition.top,
|
|
172
156
|
left: menuPosition.left,
|
|
@@ -177,7 +161,7 @@ export const Select = ({ options }: { options: string[] }) => {
|
|
|
177
161
|
filteredOptions.map((option, index) => (
|
|
178
162
|
<li
|
|
179
163
|
key={index}
|
|
180
|
-
className="px-4 py-2 text-
|
|
164
|
+
className="px-4 py-2 text-[var(--text-standard)] hover:bg-primary/10 cursor-pointer"
|
|
181
165
|
onClick={() => {
|
|
182
166
|
setFilterValue(option);
|
|
183
167
|
setIsOpen(false);
|
|
@@ -194,10 +178,10 @@ export const Select = ({ options }: { options: string[] }) => {
|
|
|
194
178
|
)}
|
|
195
179
|
<select
|
|
196
180
|
className={clsx(
|
|
197
|
-
" block transition-all bg-input-background-standard border px-4 py-2 border-
|
|
181
|
+
" block transition-all bg-[(--input-background-standard)] border px-4 py-2 border-[var(--color-border)] text-[var(--text-standard)] rounded-input-rounded",
|
|
198
182
|
{ "placeholder:text-input-text-placeholder": true },
|
|
199
|
-
{ "focus:border-input-border-focus": true },
|
|
200
|
-
{ "disabled:bg-input-background-disabled": true },
|
|
183
|
+
{ "focus:border-[var(--input-border-focus)]": true },
|
|
184
|
+
{ "disabled:bg-[var(--input-background-disabled)]": true },
|
|
201
185
|
{ "md:hidden": true }
|
|
202
186
|
)}
|
|
203
187
|
>
|
|
@@ -216,40 +200,46 @@ export const Select = ({ options }: { options: string[] }) => {
|
|
|
216
200
|
export const Radio = ({
|
|
217
201
|
name,
|
|
218
202
|
value,
|
|
203
|
+
label,
|
|
219
204
|
className,
|
|
220
205
|
onChange,
|
|
221
206
|
defaultChecked,
|
|
222
207
|
}: {
|
|
223
208
|
name: string;
|
|
224
209
|
value: string;
|
|
210
|
+
label?: string;
|
|
225
211
|
className?: string;
|
|
226
212
|
onChange?: () => void;
|
|
227
213
|
defaultChecked?: boolean;
|
|
228
214
|
}) => {
|
|
229
215
|
return (
|
|
230
|
-
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
"
|
|
235
|
-
{
|
|
236
|
-
"
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
216
|
+
<>
|
|
217
|
+
{label ? <p>{label}</p> : null}
|
|
218
|
+
<label className="flex items-center gap-2">
|
|
219
|
+
<input
|
|
220
|
+
type="radio"
|
|
221
|
+
className={clsx(
|
|
222
|
+
"relative appearance-none peer w-5 h-5 bg-[var(--input-background-standard)] border border-[var(--color-border)] rounded-full",
|
|
223
|
+
{
|
|
224
|
+
"checked:bg-primary after:w-2.5 after:h-2.5 after:bg-[var(--input-background-standard)] after:content-[''] after:absolute after:rounded-full after:left-1 after:top-1":
|
|
225
|
+
true,
|
|
226
|
+
},
|
|
227
|
+
{ "focus:border-input-border-focus": true },
|
|
228
|
+
{ "disabled:bg-input-background-disabled": true },
|
|
229
|
+
className
|
|
230
|
+
)}
|
|
231
|
+
name={name}
|
|
232
|
+
onChange={onChange}
|
|
233
|
+
defaultChecked={defaultChecked}
|
|
234
|
+
/>
|
|
235
|
+
<span>{value}</span>
|
|
236
|
+
</label>
|
|
237
|
+
</>
|
|
249
238
|
);
|
|
250
239
|
};
|
|
251
240
|
|
|
252
241
|
export const Checkbox = ({
|
|
242
|
+
text,
|
|
253
243
|
label,
|
|
254
244
|
className,
|
|
255
245
|
onChange,
|
|
@@ -257,6 +247,7 @@ export const Checkbox = ({
|
|
|
257
247
|
required,
|
|
258
248
|
defaultChecked,
|
|
259
249
|
}: {
|
|
250
|
+
text?: string;
|
|
260
251
|
label?: string;
|
|
261
252
|
className?: string;
|
|
262
253
|
onChange?: () => void;
|
|
@@ -265,42 +256,55 @@ export const Checkbox = ({
|
|
|
265
256
|
defaultChecked?: boolean;
|
|
266
257
|
}) => {
|
|
267
258
|
return (
|
|
268
|
-
|
|
269
|
-
<
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
"
|
|
273
|
-
{
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
259
|
+
<>
|
|
260
|
+
{label ? <p>{label}</p> : null}
|
|
261
|
+
<label className="relative flex flex-nowrap items-center gap-1">
|
|
262
|
+
<input
|
|
263
|
+
type="checkbox"
|
|
264
|
+
className={clsx(
|
|
265
|
+
"relative appearance-none peer min-w-5 min-h-5 bg-[var(--input-background-standard)] border border-[var(--color-border)] rounded-common",
|
|
266
|
+
{ "checked:bg-primary": true },
|
|
267
|
+
{ "focus:border-[var(--input-border-focus)]": true },
|
|
268
|
+
{
|
|
269
|
+
"disabled:bg-[var(--input-background-disabled)] disabled:cursor-not-allowed":
|
|
270
|
+
true,
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
"after:content-['*'] after:text-danger after:absolute after:-right-1 after:-top-2":
|
|
274
|
+
required,
|
|
275
|
+
},
|
|
276
|
+
className
|
|
277
|
+
)}
|
|
278
|
+
onChange={onChange}
|
|
279
|
+
disabled={disabled}
|
|
280
|
+
required={required}
|
|
281
|
+
defaultChecked={defaultChecked}
|
|
282
|
+
/>
|
|
283
|
+
<span className="absolute left-0.5 top-1 invisible peer-checked:visible fill-[var(--input-background-standard)] ">
|
|
284
|
+
<svg
|
|
285
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
286
|
+
className="h-4 w-4"
|
|
287
|
+
viewBox="0 0 20 20"
|
|
288
|
+
strokeWidth="1"
|
|
289
|
+
>
|
|
290
|
+
<path
|
|
291
|
+
fillRule="evenodd"
|
|
292
|
+
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"
|
|
293
|
+
clipRule="evenodd"
|
|
294
|
+
></path>
|
|
295
|
+
</svg>
|
|
296
|
+
</span>
|
|
297
|
+
{text ? (
|
|
298
|
+
<span
|
|
299
|
+
className={clsx(
|
|
300
|
+
"text-[var(--text-standard)] text-nowrap select-none",
|
|
301
|
+
{ "!text-[var(--input-text-disabled)]": disabled }
|
|
302
|
+
)}
|
|
303
|
+
>
|
|
304
|
+
{text}
|
|
305
|
+
</span>
|
|
306
|
+
) : null}
|
|
307
|
+
</label>
|
|
308
|
+
</>
|
|
305
309
|
);
|
|
306
310
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { usePathname } from "next/navigation";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
type Heading = {
|
|
6
|
+
id: string;
|
|
7
|
+
text: string;
|
|
8
|
+
level: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default function TOC() {
|
|
12
|
+
const [headings, setHeadings] = useState<Heading[]>([]);
|
|
13
|
+
|
|
14
|
+
const pathname = usePathname(); // ✅ 페이지 경로 추적
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const timer = setTimeout(() => {
|
|
18
|
+
const headingElements = Array.from(
|
|
19
|
+
document.querySelectorAll("h1, h2, h3")
|
|
20
|
+
) as HTMLHeadingElement[];
|
|
21
|
+
|
|
22
|
+
const generatedHeadings: Heading[] = headingElements.map((heading) => {
|
|
23
|
+
// id가 없으면 자동 생성
|
|
24
|
+
if (!heading.id) {
|
|
25
|
+
heading.id =
|
|
26
|
+
heading.textContent
|
|
27
|
+
?.toLowerCase()
|
|
28
|
+
.replace(/\s+/g, "-")
|
|
29
|
+
.replace(/[^\w\-]+/g, "") ?? "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
id: heading.id,
|
|
34
|
+
text: heading.textContent || "",
|
|
35
|
+
level: parseInt(heading.tagName.replace("H", "")),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
setHeadings(generatedHeadings);
|
|
40
|
+
}, 50);
|
|
41
|
+
|
|
42
|
+
return () => clearTimeout(timer);
|
|
43
|
+
}, [pathname]);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<nav className="space-y-2 flex flex-col">
|
|
47
|
+
{headings.map((heading, idx) => {
|
|
48
|
+
return (
|
|
49
|
+
<a
|
|
50
|
+
key={`${heading.id}-${idx}`}
|
|
51
|
+
href={`#${heading.id}`}
|
|
52
|
+
style={{ marginLeft: `${(heading.level - 1) * 8}px` }}
|
|
53
|
+
>
|
|
54
|
+
{heading.text}
|
|
55
|
+
</a>
|
|
56
|
+
);
|
|
57
|
+
})}
|
|
58
|
+
</nav>
|
|
59
|
+
);
|
|
60
|
+
}
|