atomsystem 0.1.42 → 0.1.44

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.
@@ -1,6 +1,12 @@
1
1
  "use client";
2
2
  import { usePathname } from "next/navigation";
3
3
  import { useEffect, useState } from "react";
4
+ import gsap from "gsap";
5
+ import { ScrollToPlugin } from "gsap/ScrollToPlugin";
6
+
7
+ if (typeof window !== "undefined") {
8
+ gsap.registerPlugin(ScrollToPlugin);
9
+ }
4
10
 
5
11
  type Heading = {
6
12
  id: string;
@@ -10,10 +16,18 @@ type Heading = {
10
16
 
11
17
  export default function TOC() {
12
18
  const [headings, setHeadings] = useState<Heading[]>([]);
19
+ const [activeId, setActiveId] = useState<string>("");
20
+ const [mounted, setMounted] = useState(false);
13
21
 
14
- const pathname = usePathname(); // ✅ 페이지 경로 추적
22
+ const pathname = usePathname();
23
+
24
+ useEffect(() => {
25
+ setMounted(true);
26
+ }, []);
15
27
 
16
28
  useEffect(() => {
29
+ if (!mounted) return;
30
+
17
31
  const timer = setTimeout(() => {
18
32
  /**
19
33
  * Document에서 읽히는 h1, h2, h3 태그를 array 형태로 정의
@@ -22,42 +36,130 @@ export default function TOC() {
22
36
  document.querySelectorAll("h1, h2, h3")
23
37
  ) as HTMLHeadingElement[];
24
38
 
39
+ const usedIds = new Set<string>();
25
40
  const generatedHeadings: Heading[] = headingElements.map((heading) => {
26
- // id가 없으면 자동 생성
27
- if (!heading.id) {
28
- heading.id =
41
+ let finalId = heading.id;
42
+
43
+ // ID가 없거나 중복된 경우 새로 생성
44
+ if (!finalId || usedIds.has(finalId)) {
45
+ const baseId =
29
46
  heading.textContent
30
47
  ?.toLowerCase()
48
+ .trim()
31
49
  .replace(/\s+/g, "-")
32
- .replace(/[^\w\-가-힣]+/g, "") ?? "";
50
+ .replace(/[^\w\-가-힣]+/g, "") || "heading";
51
+
52
+ finalId = baseId;
53
+ let counter = 1;
54
+ while (usedIds.has(finalId)) {
55
+ finalId = `${baseId}-${counter}`;
56
+ counter++;
57
+ }
58
+ heading.id = finalId;
33
59
  }
34
60
 
61
+ usedIds.add(finalId);
62
+
35
63
  return {
36
- id: heading.id,
64
+ id: finalId,
37
65
  text: heading.textContent || "",
38
66
  level: parseInt(heading.tagName.replace("H", "")),
39
67
  };
40
68
  });
41
69
 
42
70
  setHeadings(generatedHeadings);
43
- }, 50);
71
+ }, 100); // 딜레이를 약간 주어 DOM이 완전히 안정화된 후 실행
44
72
 
45
73
  return () => clearTimeout(timer);
46
- }, [pathname]); // 페이지 변경사항이 있다면 목차 재정의
74
+ }, [pathname, mounted]); // 페이지 변경사항이 있거나 마운트된 경우 실행
75
+
76
+ useEffect(() => {
77
+ if (!mounted || headings.length === 0) return;
78
+
79
+ const observer = new IntersectionObserver(
80
+ (entries) => {
81
+ const visibleEntries = entries.filter((entry) => entry.isIntersecting);
82
+
83
+ if (visibleEntries.length > 0) {
84
+ // 뷰포트 상단에 가장 가까운 요소를 찾아 활성화
85
+ const topVisible = visibleEntries.reduce((prev, curr) =>
86
+ curr.boundingClientRect.top < prev.boundingClientRect.top ? curr : prev
87
+ );
88
+ setActiveId(topVisible.target.id);
89
+ }
90
+ },
91
+ {
92
+ // 상단 10% ~ 20% 지점을 타겟으로 감지
93
+ rootMargin: "-10% 0% -80% 0%",
94
+ threshold: 0,
95
+ }
96
+ );
97
+
98
+ headings.forEach((heading) => {
99
+ const el = document.getElementById(heading.id);
100
+ if (el) observer.observe(el);
101
+ });
102
+
103
+ return () => observer.disconnect();
104
+ }, [headings, mounted]);
105
+
106
+ if (!mounted) {
107
+ return <nav className="space-y-2 flex flex-col min-h-[100px]" />;
108
+ }
47
109
 
48
110
  return (
49
- <nav className="space-y-2 flex flex-col">
50
- {headings.map((heading, idx) => {
51
- return (
52
- <a
53
- key={`${heading.id}-${idx}`}
54
- href={`#${heading.id}`}
55
- style={{ marginLeft: `${(heading.level - 1) * 8}px` }}
56
- >
57
- {heading.text}
58
- </a>
59
- );
60
- })}
111
+ <nav className="flex flex-col space-y-1 border-l border-border-standard ml-2 py-1">
112
+ <div className="text-[10px] font-bold text-text-sub/60 uppercase tracking-widest mb-3 px-4">
113
+ On this page
114
+ </div>
115
+ {headings.length > 0 ? (
116
+ headings.map((heading, idx) => {
117
+ const isActive = activeId === heading.id;
118
+ return (
119
+ <a
120
+ key={`${heading.id}-${idx}`}
121
+ href={`#${heading.id}`}
122
+ style={{ paddingLeft: `${(heading.level - 1) * 12 + 16}px` }}
123
+ className={`text-xs py-1.5 pr-4 border-l-2 transition-all duration-200 ease-in-out ${
124
+ isActive
125
+ ? "text-primary bg-primary/5 border-primary font-medium"
126
+ : "text-text-sub border-transparent hover:text-primary hover:bg-primary/5 hover:border-primary/30"
127
+ }`}
128
+ onClick={(e) => {
129
+ e.preventDefault();
130
+ const element = document.getElementById(heading.id);
131
+ if (element) {
132
+ // GSAP scrollTo를 사용하여 부드럽게 이동
133
+ // element.closest('.overflow-auto') 등을 통해 스크롤 컨테이너를 찾습니다.
134
+ const container = element.closest('.overflow-auto') || window;
135
+
136
+ gsap.to(container, {
137
+ duration: 0.8,
138
+ scrollTo: {
139
+ y: element,
140
+ offsetY: 80, // 상단 여백
141
+ autoKill: true
142
+ },
143
+ ease: "power2.inOut"
144
+ });
145
+
146
+ // 클릭 시 즉시 활성화 상태 업데이트
147
+ setActiveId(heading.id);
148
+
149
+ // URL 해시 업데이트
150
+ window.history.pushState(null, "", `#${heading.id}`);
151
+ }
152
+ }}
153
+ >
154
+ {heading.text}
155
+ </a>
156
+ );
157
+ })
158
+ ) : (
159
+ <div className="px-4 text-[10px] text-text-sub/40 italic mt-2">
160
+ 목차를 찾을 수 없습니다
161
+ </div>
162
+ )}
61
163
  </nav>
62
164
  );
63
165
  }
package/dist/index.js CHANGED
@@ -41,7 +41,9 @@ var logger = {
41
41
  };
42
42
 
43
43
  // src/commands/add.ts
44
- var templateDir = path2.join(process.cwd(), "app", "templates");
44
+ var isBuilt = path2.basename(__dirname) === "dist" || fs.existsSync(path2.join(__dirname, "index.js"));
45
+ var baseDir = isBuilt ? path2.join(__dirname, "..") : path2.join(__dirname, "..", "..");
46
+ var templateDir = path2.join(baseDir, "app", "templates");
45
47
  var components = [
46
48
  "Button",
47
49
  "Badge",
@@ -217,6 +219,15 @@ var init = new Command2().name("init").description("\uD504\uB85C\uC81D\uD2B8\uB9
217
219
  logger.error(err);
218
220
  return;
219
221
  }
222
+ const gsSpinner = ora("gsap Animation \uC124\uCE58 \uC911...").start();
223
+ try {
224
+ await execPromise("npm install gsap", { cwd: projectRoot });
225
+ gsSpinner.succeed("gsap Animation \uC124\uCE58 \uC644\uB8CC!");
226
+ } catch (err) {
227
+ gsSpinner.fail("gsap Animation \uC124\uCE58 \uC911 \uC624\uB958 \uBC1C\uC0DD");
228
+ logger.error(err);
229
+ return;
230
+ }
220
231
  try {
221
232
  const packageJson = JSON.parse(fs2.readFileSync(packageJsonPath, "utf8"));
222
233
  packageJson.type = "module";
@@ -356,9 +367,11 @@ export default config;`
356
367
  }
357
368
  }
358
369
  }
359
- const tokenFile = path3.join(__dirname, "..", "..", "tokens", "tokens.json");
360
- const buildFile = path3.join(__dirname, "..", "..", "build", "build-tokens.js");
361
- const textCssFile = path3.join(__dirname, "..", "..", "css", "text.css");
370
+ const isBuilt2 = path3.basename(__dirname) === "dist" || fs2.existsSync(path3.join(__dirname, "index.js"));
371
+ const baseDir2 = isBuilt2 ? path3.join(__dirname, "..") : path3.join(__dirname, "..", "..");
372
+ const tokenFile = path3.join(baseDir2, "tokens", "tokens.json");
373
+ const buildFile = path3.join(baseDir2, "build", "build-tokens.js");
374
+ const textCssFile = path3.join(baseDir2, "css", "text.css");
362
375
  copyFile(tokenFile, path3.join(projectRoot, "tokens", "tokens.json"));
363
376
  copyFile(buildFile, path3.join(projectRoot, "build", "build-tokens.js"));
364
377
  copyFile(textCssFile, path3.join(projectRoot, "css", "text.css"));
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "atomsystem",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
4
4
  "type": "module",
5
5
  "homepage": "https://system.atomground.com",
6
6
  "bin": {
7
7
  "atomsystem": "dist/index.js"
8
8
  },
9
9
  "files": [
10
+ "dist",
10
11
  "tokens",
11
12
  "tailwind.config.ts",
12
13
  "build",