Lashtw commited on
Commit
8367ea0
·
verified ·
1 Parent(s): 0be1983

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +525 -19
index.html CHANGED
@@ -1,19 +1,525 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-TW">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Vibecoding 萬用指令包</title>
7
+ <meta name="description" content="AI 程式教學輔助工具 - 引導式 Prompt 指令包">
8
+
9
+ <!-- Google Fonts -->
10
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Noto+Sans+TC:wght@300;500;700&display=swap" rel="stylesheet">
11
+
12
+ <!-- FontAwesome Icons -->
13
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
14
+
15
+ <!-- Tailwind CSS (via CDN) -->
16
+ <script src="https://cdn.tailwindcss.com"></script>
17
+
18
+ <!-- Tailwind Configuration -->
19
+ <script>
20
+ tailwind.config = {
21
+ theme: {
22
+ extend: {
23
+ colors: {
24
+ dark: {
25
+ 900: '#0f172a', // Main background
26
+ 800: '#1e293b', // Card background
27
+ 750: '#27354f', // Sub-card background
28
+ 700: '#334155', // Border
29
+ },
30
+ neon: {
31
+ cyan: '#06b6d4', // Start
32
+ purple: '#a855f7', // Process
33
+ green: '#10b981', // Polish
34
+ },
35
+ type: {
36
+ general: '#3b82f6', // Blue for General Account
37
+ edu: '#f97316' // Orange for Edu Account
38
+ }
39
+ },
40
+ fontFamily: {
41
+ sans: ['"Noto Sans TC"', 'sans-serif'],
42
+ mono: ['"JetBrains Mono"', 'monospace'],
43
+ }
44
+ }
45
+ }
46
+ }
47
+ </script>
48
+
49
+ <style>
50
+ /* Custom Scrollbar */
51
+ ::-webkit-scrollbar {
52
+ width: 10px;
53
+ }
54
+ ::-webkit-scrollbar-track {
55
+ background: #0f172a;
56
+ }
57
+ ::-webkit-scrollbar-thumb {
58
+ background: #334155;
59
+ border-radius: 5px;
60
+ }
61
+ ::-webkit-scrollbar-thumb:hover {
62
+ background: #475569;
63
+ }
64
+
65
+ /* Smooth Scroll globally */
66
+ html {
67
+ scroll-behavior: smooth;
68
+ }
69
+
70
+ body {
71
+ background-color: #0f172a;
72
+ color: #e2e8f0;
73
+ overflow-x: hidden;
74
+ }
75
+
76
+ /* Timeline Line */
77
+ .timeline-line {
78
+ position: absolute;
79
+ left: 24px;
80
+ top: 0;
81
+ bottom: 0;
82
+ width: 2px;
83
+ background: linear-gradient(180deg, #06b6d4 0%, #a855f7 50%, #10b981 100%);
84
+ z-index: 0;
85
+ }
86
+
87
+ /* Mobile adjustment for timeline */
88
+ @media (max-width: 640px) {
89
+ .timeline-line {
90
+ left: 16px;
91
+ }
92
+ }
93
+
94
+ /* Code Block Styling */
95
+ .code-block {
96
+ background-color: #0c111c;
97
+ border: 1px solid #334155;
98
+ position: relative;
99
+ }
100
+
101
+ /* Active Nav Link Styling */
102
+ .nav-link.active {
103
+ background-color: rgba(255, 255, 255, 0.1);
104
+ font-weight: 700;
105
+ }
106
+ .nav-link.active[data-target="section-start"] { border-bottom: 2px solid #06b6d4; color: #06b6d4; }
107
+ .nav-link.active[data-target="section-process"] { border-bottom: 2px solid #a855f7; color: #a855f7; }
108
+ .nav-link.active[data-target="section-polish"] { border-bottom: 2px solid #10b981; color: #10b981; }
109
+
110
+ /* Toast Animation */
111
+ @keyframes slideUpFade {
112
+ from { transform: translate(-50%, 20px); opacity: 0; }
113
+ to { transform: translate(-50%, 0); opacity: 1; }
114
+ }
115
+ .toast-enter {
116
+ animation: slideUpFade 0.3s ease-out forwards;
117
+ }
118
+ </style>
119
+ </head>
120
+ <body class="font-sans selection:bg-neon-cyan selection:text-black">
121
+
122
+ <!-- Sticky Navigation -->
123
+ <nav class="fixed top-0 w-full z-50 bg-dark-900/80 backdrop-blur-md border-b border-dark-700 shadow-lg">
124
+ <div class="max-w-4xl mx-auto px-4 h-16 flex items-center justify-between">
125
+ <div class="flex items-center gap-2">
126
+ <i class="fa-solid fa-code text-neon-cyan text-xl"></i>
127
+ <span class="text-xl font-bold tracking-wider text-white">Vibe<span class="text-neon-cyan">coding</span></span>
128
+ </div>
129
+
130
+ <div class="flex space-x-1 sm:space-x-4">
131
+ <a href="#section-start" data-target="section-start" class="nav-link px-3 py-4 text-sm sm:text-base text-gray-400 hover:text-white transition-colors">
132
+ 起點
133
+ </a>
134
+ <a href="#section-process" data-target="section-process" class="nav-link px-3 py-4 text-sm sm:text-base text-gray-400 hover:text-white transition-colors">
135
+ 過程
136
+ </a>
137
+ <a href="#section-polish" data-target="section-polish" class="nav-link px-3 py-4 text-sm sm:text-base text-gray-400 hover:text-white transition-colors">
138
+ 優化
139
+ </a>
140
+ </div>
141
+ </div>
142
+ </nav>
143
+
144
+ <!-- Main Content -->
145
+ <main class="max-w-4xl mx-auto px-4 pt-24 pb-20 relative">
146
+
147
+ <!-- Continuous Timeline Line -->
148
+ <div class="timeline-line hidden sm:block"></div>
149
+
150
+ <!-- Dynamic Content Container -->
151
+ <div id="app-container">
152
+ <!-- Content will be injected here by JavaScript -->
153
+ </div>
154
+
155
+ </main>
156
+
157
+ <!-- Footer -->
158
+ <footer class="text-center py-8 text-gray-500 text-sm border-t border-dark-800 bg-dark-900 relative z-10">
159
+ <p class="mb-2">Built for AI Learners • Vibecoding Toolkit</p>
160
+ <div class="flex flex-col gap-1">
161
+ <p>設計者:新竹縣精華國中 藍星宇老師</p>
162
+ <p>
163
+ <i class="fa-brands fa-facebook text-blue-500 mr-1"></i>
164
+ FB教育社群:<a href="https://www.facebook.com/groups/1554372228718393" target="_blank" rel="noopener noreferrer" class="text-neon-cyan hover:text-white transition-colors underline decoration-dotted underline-offset-4">萬物皆數</a>
165
+ </p>
166
+ </div>
167
+ </footer>
168
+
169
+ <!-- Toast Notification -->
170
+ <div id="toast" class="fixed bottom-8 left-1/2 transform -translate-x-1/2 bg-white text-dark-900 px-6 py-3 rounded-full shadow-2xl flex items-center gap-3 opacity-0 pointer-events-none transition-opacity duration-300 z-50">
171
+ <i class="fa-solid fa-circle-check text-green-500 text-lg"></i>
172
+ <span class="font-bold">已複製指令到剪貼簿!</span>
173
+ </div>
174
+
175
+ <!-- JavaScript -->
176
+ <script>
177
+ // Data Configuration
178
+ const appData = [
179
+ {
180
+ id: "section-start",
181
+ title: "起點 Start",
182
+ step: "01",
183
+ desc: "萬丈高樓平地起,規格寫好最省力",
184
+ colorClass: "text-neon-cyan",
185
+ bgClass: "bg-neon-cyan",
186
+ borderColor: "border-neon-cyan",
187
+ prompts: [
188
+ {
189
+ title: "萬用起手式 (Meta-Prompt)",
190
+ desc: "剛開始製作新專案時使用,讓 AI 幫你寫出最精確的規格書。",
191
+ code: `你現在是一位專業的網頁前端工程師,擅長遊戲化教學,同時也是一位AI的提示詞工程師。\n我想做一個 [程式的主題] 的互動式網頁,[程式的內容]\n請幫我撰寫一段結構清晰、專業的提示詞,讓我可以直接貼到 Gemini Canvas中,一次生成高品質的程式碼。`
192
+ }
193
+ ]
194
+ },
195
+ {
196
+ id: "section-process",
197
+ title: "過程 Process",
198
+ step: "02",
199
+ desc: "遇到問題不氣餒,引導 AI 來修復",
200
+ colorClass: "text-neon-purple",
201
+ bgClass: "bg-neon-purple",
202
+ borderColor: "border-neon-purple",
203
+ prompts: [
204
+ {
205
+ title: "除錯修正 (Debug)",
206
+ desc: "當程式跑不動、報錯或按鈕沒反應時使用。",
207
+ code: `我發現這個網頁有以下問題:[描述錯誤狀況,如:按鈕沒反應、計算錯誤]。請幫我檢查程式邏輯並修正。`
208
+ },
209
+ {
210
+ title: "新增功能 (Add Feature)",
211
+ desc: "想要增加新功能時使用(建議一次只加 1-2 個)。",
212
+ code: `請依照我的說明新增以下功能:\n1. [要修正的功能]\n2. [要修正的功能]`,
213
+ note: "注意:不宜一次更新太多功能"
214
+ },
215
+ {
216
+ title: "鬼打牆救援 (Rescue)",
217
+ desc: "當 AI 修不好時,請依照您的帳號類型選擇合適的救援方式。",
218
+ isGroup: true, // Special flag for grouped layout
219
+ subCards: [
220
+ {
221
+ subTitle: "一般帳號 (General)",
222
+ colorClass: "text-type-general",
223
+ borderClass: "border-type-general",
224
+ badgeBg: "bg-type-general",
225
+ desc: "當 AI 修不好時,開新對話,用「分享連結」讓新 AI 接手。",
226
+ code: `請讀取這個 Canvas 程式,並說明這個程式的目的及功能。`,
227
+ note: "注意:確認目的及功能相符後再繼續製作,若有缺漏則需要提醒AI。"
228
+ },
229
+ {
230
+ subTitle: "教育帳號 (Education)",
231
+ colorClass: "text-type-edu",
232
+ borderClass: "border-type-edu",
233
+ badgeBg: "bg-type-edu",
234
+ desc: "當 AI 修不好時,複製程式碼開新對話。",
235
+ code: `這是目前的程式碼,請協助讓我繼續編修。`
236
+ }
237
+ ]
238
+ }
239
+ ]
240
+ },
241
+ {
242
+ id: "section-polish",
243
+ title: "優化 Polish",
244
+ step: "03",
245
+ desc: "功能完善只是及格,精緻體驗才是滿分",
246
+ colorClass: "text-neon-green",
247
+ bgClass: "bg-neon-green",
248
+ borderColor: "border-neon-green",
249
+ prompts: [
250
+ {
251
+ title: "視覺美化師 (UI/UX)",
252
+ desc: "功能做好了,但畫面太醜?用這個讓它變漂亮。",
253
+ code: `你現在是一位獲得國際大獎的 UI/UX 設計師。目前的網頁功能運作正常,但視覺設計太過簡陋。\n請幫我重新設計 CSS 樣式,目標風格是 [請填入風格,如:活潑童趣、現代極簡、科技賽博龐克]。\n請加入適當的陰影 (Box-shadow)、圓角 (Border-radius) 和過渡動畫 (Transition),讓按鈕和互動元素看起來更精緻,並請確保配色舒適協調。`
254
+ },
255
+ {
256
+ title: "教學專業建議 (Actionable)",
257
+ desc: "請 AI 直接動手將教學理論實作進去。",
258
+ code: `現在你是一位擅長遊戲化教學的資深教師以及前端工程師。\n請分析目前的網頁,並提供 3 個具體的優化建議,讓這個工具能更有效提升學生的學習動機或成效。如果可以,請直接幫我把覺得最重要的一點實作進去。`
259
+ },
260
+ {
261
+ title: "簡易優化 (General Advice)",
262
+ desc: "單純請 AI 腦力激盪,給予優化方向。",
263
+ code: `現在你是一位擅長遊戲化教學的老師以及程式工程師,請你為這個網頁程式提供優化建議。`
264
+ }
265
+ ]
266
+ }
267
+ ];
268
+
269
+ // Helper: Generate Code Block HTML
270
+ function generateCodeBlockHtml(code, note, sectionBgClass) {
271
+ let noteHtml = '';
272
+ if (note) {
273
+ noteHtml = `
274
+ <div class="mt-3 flex items-start gap-2 text-yellow-400/90 text-sm bg-yellow-400/10 p-2 rounded border border-yellow-400/20">
275
+ <i class="fa-solid fa-circle-exclamation mt-1"></i>
276
+ <span>${note}</span>
277
+ </div>
278
+ `;
279
+ }
280
+
281
+ // Fix:
282
+ // 1. trim() code to remove surrounding whitespace
283
+ // 2. move whitespace-pre-wrap to code element
284
+ // 3. change text color to text-amber-300
285
+ return `
286
+ <div class="code-block rounded-lg p-4 font-mono text-sm bg-dark-900/50 border border-dark-700 relative overflow-hidden group-hover:border-opacity-100 transition-colors">
287
+ <div class="absolute top-0 left-0 w-1 h-full ${sectionBgClass} opacity-50"></div>
288
+ <code class="prompt-text text-amber-300 whitespace-pre-wrap leading-relaxed block font-medium">${escapeHtml(code.trim())}</code>
289
+ </div>
290
+ ${noteHtml}
291
+ `;
292
+ }
293
+
294
+ // Render Application
295
+ function renderApp() {
296
+ const container = document.getElementById('app-container');
297
+ let html = '';
298
+
299
+ appData.forEach((section, index) => {
300
+ let cardsHtml = '';
301
+
302
+ // Iterate through prompts to generate cards
303
+ section.prompts.forEach(prompt => {
304
+
305
+ if (prompt.isGroup) {
306
+ // Logic for "Ghost Wall Rescue" (Grouped Card)
307
+ const subCardsHtml = prompt.subCards.map(sub => `
308
+ <div class="bg-dark-750 rounded-lg border border-dark-700 p-4 mb-4 last:mb-0 hover:border-gray-500 transition-colors">
309
+ <div class="flex justify-between items-center mb-2">
310
+ <div class="flex items-center gap-2">
311
+ <span class="w-2 h-2 rounded-full ${sub.badgeBg}"></span>
312
+ <h4 class="font-bold text-gray-200 ${sub.colorClass}">${sub.subTitle}</h4>
313
+ </div>
314
+ <button onclick="copyToClipboard(this)" class="bg-dark-700 hover:bg-dark-900 text-gray-300 hover:text-white px-3 py-1.5 rounded text-sm transition-colors border border-gray-600 flex items-center gap-2 group/btn shrink-0 ml-2">
315
+ <i class="fa-regular fa-copy"></i>
316
+ <span class="hidden md:inline">複製</span>
317
+ </button>
318
+ </div>
319
+ <p class="text-gray-400 text-xs mb-3">${sub.desc}</p>
320
+ ${generateCodeBlockHtml(sub.code, sub.note, sub.badgeBg)}
321
+ </div>
322
+ `).join('');
323
+
324
+ cardsHtml += `
325
+ <div class="bg-dark-800 rounded-xl border border-dark-700 p-6 shadow-lg">
326
+ <h3 class="text-xl font-bold text-white mb-1">${prompt.title}</h3>
327
+ <p class="text-gray-400 text-sm mb-4">${prompt.desc}</p>
328
+ <div class="flex flex-col gap-2">
329
+ ${subCardsHtml}
330
+ </div>
331
+ </div>
332
+ `;
333
+
334
+ } else {
335
+ // Standard Card
336
+ cardsHtml += `
337
+ <div class="bg-dark-800 rounded-xl border border-dark-700 p-6 hover:border-${section.colorClass.split('-')[2]}-500/50 transition-all duration-300 shadow-lg group">
338
+ <div class="flex justify-between items-start mb-4">
339
+ <div>
340
+ <h3 class="text-xl font-bold text-white mb-1 group-hover:${section.colorClass} transition-colors">${prompt.title}</h3>
341
+ <p class="text-gray-400 text-sm">${prompt.desc}</p>
342
+ </div>
343
+ <button onclick="copyToClipboard(this)" class="bg-dark-700 hover:bg-dark-900 text-gray-300 hover:text-white p-2 rounded-lg transition-colors border border-gray-600 flex items-center gap-2 group/btn shrink-0 ml-2" aria-label="Copy code">
344
+ <i class="fa-regular fa-copy"></i>
345
+ <span class="text-xs hidden md:inline">複製</span>
346
+ </button>
347
+ </div>
348
+
349
+ ${generateCodeBlockHtml(prompt.code, prompt.note, section.bgClass)}
350
+ </div>
351
+ `;
352
+ }
353
+ });
354
+
355
+ html += `
356
+ <section id="${section.id}" class="mb-24 relative scroll-mt-24">
357
+ <!-- Section Header -->
358
+ <div class="flex items-start mb-8 relative z-10 pl-0 sm:pl-16">
359
+ <!-- Step Bubble (Desktop) -->
360
+ <div class="hidden sm:flex absolute left-0 top-0 w-12 h-12 rounded-full ${section.bgClass} items-center justify-center font-bold text-dark-900 shadow-[0_0_15px_rgba(0,0,0,0.5)] border-4 border-dark-900">
361
+ ${section.step}
362
+ </div>
363
+
364
+ <!-- Mobile Step Header -->
365
+ <div class="w-full">
366
+ <div class="flex items-center gap-3 sm:hidden mb-2">
367
+ <span class="px-2 py-1 text-xs font-bold rounded ${section.bgClass} text-dark-900">STEP ${section.step}</span>
368
+ <div class="h-px bg-gray-700 flex-grow"></div>
369
+ </div>
370
+
371
+ <h2 class="text-3xl md:text-4xl font-bold text-white mb-2 flex items-center gap-3">
372
+ <i class="fa-solid ${getSectionIcon(index)} ${section.colorClass} text-2xl"></i>
373
+ ${section.title}
374
+ </h2>
375
+ <p class="text-gray-400 text-lg">${section.desc}</p>
376
+ </div>
377
+ </div>
378
+
379
+ <!-- Cards Container -->
380
+ <div class="grid grid-cols-1 gap-6 relative z-10 pl-0 sm:pl-16">
381
+ ${cardsHtml}
382
+ </div>
383
+ </section>
384
+ `;
385
+ });
386
+
387
+ container.innerHTML = html;
388
+ }
389
+
390
+ // Helper: Get Icon based on index
391
+ function getSectionIcon(index) {
392
+ const icons = ['fa-rocket', 'fa-screwdriver-wrench', 'fa-wand-magic-sparkles'];
393
+ return icons[index] || 'fa-circle';
394
+ }
395
+
396
+ // Helper: Escape HTML to prevent XSS (though content is static here)
397
+ function escapeHtml(text) {
398
+ return text
399
+ .replace(/&/g, "&amp;")
400
+ .replace(/</g, "&lt;")
401
+ .replace(/>/g, "&gt;")
402
+ .replace(/"/g, "&quot;")
403
+ .replace(/'/g, "&#039;");
404
+ }
405
+
406
+ // Copy Function (Robust method using temporary textarea)
407
+ function copyToClipboard(btn) {
408
+ // Traverse up to find the closest code block container
409
+ const parent = btn.closest('.bg-dark-750') || btn.closest('.bg-dark-800');
410
+ const codeText = parent.querySelector('.prompt-text').innerText;
411
+
412
+ // Create temporary textarea
413
+ const textarea = document.createElement('textarea');
414
+ textarea.value = codeText;
415
+ textarea.setAttribute('readonly', '');
416
+ textarea.style.position = 'absolute';
417
+ textarea.style.left = '-9999px';
418
+ document.body.appendChild(textarea);
419
+
420
+ // Select and Copy
421
+ textarea.select();
422
+ try {
423
+ document.execCommand('copy');
424
+ showToast();
425
+
426
+ // Button Feedback
427
+ const originalIcon = btn.innerHTML;
428
+ btn.innerHTML = '<i class="fa-solid fa-check text-green-500"></i> <span class="text-green-500 text-xs hidden md:inline">已複製</span>';
429
+ btn.classList.add('border-green-500');
430
+
431
+ setTimeout(() => {
432
+ btn.innerHTML = originalIcon;
433
+ btn.classList.remove('border-green-500');
434
+ }, 2000);
435
+
436
+ } catch (err) {
437
+ console.error('無法複製', err);
438
+ alert('複製失敗,請手動選取複製');
439
+ }
440
+
441
+ document.body.removeChild(textarea);
442
+ }
443
+
444
+ // Show Toast Notification
445
+ function showToast() {
446
+ const toast = document.getElementById('toast');
447
+ toast.classList.remove('opacity-0', 'pointer-events-none');
448
+ toast.classList.add('toast-enter');
449
+
450
+ // Clear existing timeout if any
451
+ if (window.toastTimeout) clearTimeout(window.toastTimeout);
452
+
453
+ window.toastTimeout = setTimeout(() => {
454
+ toast.classList.add('opacity-0', 'pointer-events-none');
455
+ toast.classList.remove('toast-enter');
456
+ }, 3000);
457
+ }
458
+
459
+ // Initialize
460
+ document.addEventListener('DOMContentLoaded', () => {
461
+ renderApp();
462
+
463
+ // Intersection Observer for Sticky Nav Highlighting
464
+ const sections = document.querySelectorAll('section');
465
+ const navLinks = document.querySelectorAll('.nav-link');
466
+
467
+ // 修正 Observer 選項,將觸發線調整至視窗上方
468
+ // rootMargin: '-15% 0px -80% 0px' 代表:
469
+ // 當元素進入視窗頂部 15% ~ 20% 的範圍內時,就會觸發 active
470
+ // 這樣在元素捲動到標題下方時,能更快偵測到
471
+ const observerOptions = {
472
+ root: null,
473
+ rootMargin: '-15% 0px -80% 0px',
474
+ threshold: 0
475
+ };
476
+
477
+ const observer = new IntersectionObserver((entries) => {
478
+ entries.forEach(entry => {
479
+ if (entry.isIntersecting) {
480
+ // Remove active class from all
481
+ navLinks.forEach(link => {
482
+ link.classList.remove('active');
483
+ // Reset color styles
484
+ link.style.borderBottom = '';
485
+ link.style.color = '';
486
+ });
487
+
488
+ // Add active class to current
489
+ const id = entry.target.getAttribute('id');
490
+ const activeLink = document.querySelector(`.nav-link[data-target="${id}"]`);
491
+ if (activeLink) {
492
+ activeLink.classList.add('active');
493
+ }
494
+ }
495
+ });
496
+ }, observerOptions);
497
+
498
+ sections.forEach(section => {
499
+ observer.observe(section);
500
+ });
501
+
502
+ // 新增:點擊導覽列連結時,立即手動切換 active 樣式
503
+ // 這能解決「捲動時間差」造成的視覺延遲
504
+ navLinks.forEach(link => {
505
+ link.addEventListener('click', (e) => {
506
+ // 1. 移除所有 active
507
+ navLinks.forEach(l => {
508
+ l.classList.remove('active');
509
+ l.style.borderBottom = '';
510
+ l.style.color = '';
511
+ });
512
+
513
+ // 2. 立即將被點擊的連結設為 active
514
+ const target = e.currentTarget;
515
+ target.classList.add('active');
516
+
517
+ // 注意:頁面捲動後 Observer 仍會觸發,但因為我們已經調整了 rootMargin
518
+ // 所以最終狀態會保持一致。
519
+ });
520
+ });
521
+ });
522
+
523
+ </script>
524
+ </body>
525
+ </html>