tkeron 5.3.0 → 6.0.1
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 +17 -16
- package/bun.lock +13 -196
- package/changelog.md +55 -0
- package/examples/init_sample/websrc/about.html +19 -0
- package/examples/init_sample/websrc/components/content/about-intro.com.md +7 -0
- package/examples/init_sample/websrc/components/layout/hero-section.com.html +88 -0
- package/examples/init_sample/websrc/components/layout/site-footer.com.html +20 -0
- package/examples/init_sample/websrc/components/layout/site-header.com.html +44 -0
- package/examples/init_sample/websrc/components/ui/counter-button.com.html +48 -0
- package/examples/init_sample/websrc/components/ui/crypto-prices-card.com.html +52 -0
- package/examples/init_sample/websrc/components/ui/html-components-card.com.html +10 -0
- package/examples/init_sample/websrc/{info-card.com.html → components/ui/info-card.com.html} +10 -6
- package/examples/init_sample/websrc/components/ui/markdown-card.com.html +21 -0
- package/examples/init_sample/websrc/{pre-render-card.com.html → components/ui/pre-render-card.com.html} +6 -3
- package/examples/init_sample/websrc/components/ui/quote-card.com.html +32 -0
- package/examples/init_sample/websrc/components/ui/styles-injector.com.ts +5 -0
- package/examples/init_sample/websrc/components/ui/ts-components-card.com.html +11 -0
- package/examples/init_sample/websrc/components/ui/user-badge.com.ts +30 -0
- package/examples/init_sample/websrc/docs.html +33 -0
- package/examples/init_sample/websrc/index.html +10 -209
- package/examples/init_sample/websrc/index.post.ts +70 -0
- package/examples/init_sample/websrc/index.pre.ts +10 -61
- package/examples/init_sample/websrc/index.ts +9 -7
- package/examples/init_sample/websrc/styles/global-styles.com.ts +5 -0
- package/examples/init_sample/websrc/styles/main.css +112 -0
- package/examples/init_sample/websrc/utils/api-service.ts +93 -0
- package/examples/with_global_styles/websrc/bad.html +23 -0
- package/examples/with_global_styles/websrc/global-styles.com.ts +5 -0
- package/examples/with_global_styles/websrc/good.html +22 -0
- package/examples/with_global_styles/websrc/index.html +25 -0
- package/examples/with_global_styles/websrc/styles.css +24 -0
- package/examples/with_style_dedup/websrc/tag-chip.com.html +15 -0
- package/examples/with_style_dedup/websrc/tag-chip.com.ts +2 -18
- package/index.ts +15 -0
- package/package.json +9 -13
- package/skills/tkeron/SKILL.md +287 -0
- package/skills/tkeron-components/SKILL.md +785 -0
- package/skills/tkeron-organization/SKILL.md +231 -0
- package/skills/tkeron-patterns/SKILL.md +587 -0
- package/skills/tkeron-testing/SKILL.md +194 -0
- package/skills/tkeron-troubleshooting/SKILL.md +263 -0
- package/src/build.ts +4 -6
- package/src/develop.ts +1 -2
- package/src/init.ts +1 -2
- package/src/skills.ts +139 -0
- package/src/skillsWrapper.ts +79 -0
- package/docs/mcp-server.md +0 -240
- package/examples/init_sample/websrc/api-service.ts +0 -98
- package/examples/init_sample/websrc/counter-card.com.html +0 -9
- package/examples/init_sample/websrc/favicon.ico +0 -0
- package/examples/init_sample/websrc/hero-section.com.html +0 -23
- package/examples/init_sample/websrc/html-components-card.com.html +0 -6
- package/examples/init_sample/websrc/ts-components-card.com.html +0 -6
- package/examples/init_sample/websrc/user-badge.com.ts +0 -17
- package/mcp-server.ts +0 -272
- package/src/cleanupOrphanedTempDirs.ts +0 -31
- package/src/promptUser.ts +0 -15
- package/src/setupSigintHandler.ts +0 -6
- /package/examples/init_sample/websrc/{profile.png → assets/profile.png} +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.crypto-prices-container {
|
|
3
|
+
display: grid;
|
|
4
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
5
|
+
gap: 1rem;
|
|
6
|
+
margin-top: 1rem;
|
|
7
|
+
}
|
|
8
|
+
.crypto-card {
|
|
9
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
10
|
+
padding: 1.25rem;
|
|
11
|
+
border-radius: 12px;
|
|
12
|
+
color: white;
|
|
13
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
14
|
+
transition: transform 0.2s;
|
|
15
|
+
}
|
|
16
|
+
.crypto-card:hover {
|
|
17
|
+
transform: translateY(-4px);
|
|
18
|
+
}
|
|
19
|
+
.crypto-header {
|
|
20
|
+
display: flex;
|
|
21
|
+
justify-content: space-between;
|
|
22
|
+
align-items: center;
|
|
23
|
+
margin-bottom: 0.75rem;
|
|
24
|
+
}
|
|
25
|
+
.crypto-name {
|
|
26
|
+
font-weight: 600;
|
|
27
|
+
font-size: 1rem;
|
|
28
|
+
}
|
|
29
|
+
.crypto-symbol {
|
|
30
|
+
background: rgba(255, 255, 255, 0.2);
|
|
31
|
+
padding: 0.25rem 0.5rem;
|
|
32
|
+
border-radius: 4px;
|
|
33
|
+
font-size: 0.75rem;
|
|
34
|
+
font-weight: 700;
|
|
35
|
+
}
|
|
36
|
+
.crypto-price {
|
|
37
|
+
font-size: 1.5rem;
|
|
38
|
+
font-weight: 700;
|
|
39
|
+
margin-bottom: 0.5rem;
|
|
40
|
+
}
|
|
41
|
+
.crypto-change {
|
|
42
|
+
font-size: 0.9rem;
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
}
|
|
45
|
+
</style>
|
|
46
|
+
<section style="grid-column: span 3">
|
|
47
|
+
<h2>💰 Asset Prices (Build Time)</h2>
|
|
48
|
+
<p style="font-size: 0.85rem; margin-bottom: 0.75rem">
|
|
49
|
+
Real cryptocurrency prices fetched from CoinGecko API at build time:
|
|
50
|
+
</p>
|
|
51
|
+
<div id="crypto-prices" class="crypto-prices-container"></div>
|
|
52
|
+
</section>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<section>
|
|
2
|
+
<h2>🧩 HTML Components</h2>
|
|
3
|
+
<p style="margin-bottom: 0.75rem">
|
|
4
|
+
Reusable components with <code>.com.html</code>:
|
|
5
|
+
</p>
|
|
6
|
+
<info-card></info-card>
|
|
7
|
+
<p style="font-size: 0.85rem; color: #666; margin-top: 0.75rem">
|
|
8
|
+
✓ Nested components support<br />✓ Build-time replacement
|
|
9
|
+
</p>
|
|
10
|
+
</section>
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
<
|
|
2
|
-
|
|
1
|
+
<style>
|
|
2
|
+
.info-card {
|
|
3
3
|
padding: 1rem;
|
|
4
4
|
background: #eff6ff;
|
|
5
5
|
border-left: 4px solid #2563eb;
|
|
6
6
|
border-radius: 4px;
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
}
|
|
8
|
+
.info-card p {
|
|
9
|
+
margin-top: 0.5rem;
|
|
10
|
+
}
|
|
11
|
+
</style>
|
|
12
|
+
<div class="info-card">
|
|
9
13
|
<strong>This is an HTML component!</strong>
|
|
10
|
-
<p
|
|
14
|
+
<p>
|
|
11
15
|
Defined in <code>info-card.com.html</code> and reused anywhere in your HTML
|
|
12
|
-
as <code>&
|
|
16
|
+
as <code><info-card></info-card></code>.
|
|
13
17
|
</p>
|
|
14
18
|
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.markdown-card .md-body {
|
|
3
|
+
padding: 0.75rem 1rem;
|
|
4
|
+
background: #fffbea;
|
|
5
|
+
border-left: 4px solid #f59e0b;
|
|
6
|
+
border-radius: 4px;
|
|
7
|
+
font-size: 0.9rem;
|
|
8
|
+
}
|
|
9
|
+
</style>
|
|
10
|
+
<section class="markdown-card">
|
|
11
|
+
<h2>📄 Markdown Components</h2>
|
|
12
|
+
<p style="margin-bottom: 0.75rem">
|
|
13
|
+
Author content with <code>.com.md</code>:
|
|
14
|
+
</p>
|
|
15
|
+
<div class="md-body">
|
|
16
|
+
<about-intro></about-intro>
|
|
17
|
+
</div>
|
|
18
|
+
<p style="font-size: 0.85rem; color: #666; margin-top: 0.75rem">
|
|
19
|
+
✓ Markdown rendered at build time<br />✓ Reusable like any component
|
|
20
|
+
</p>
|
|
21
|
+
</section>
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
<section id="pre-render-card">
|
|
2
|
-
<h2>📝 Pre
|
|
3
|
-
<p>
|
|
2
|
+
<h2>📝 Pre / Post Rendering</h2>
|
|
3
|
+
<p>
|
|
4
|
+
Run code at build time with <code>.pre.ts</code> (before components) and
|
|
5
|
+
<code>.post.ts</code> (after components):
|
|
6
|
+
</p>
|
|
4
7
|
<p
|
|
5
8
|
id="build-time"
|
|
6
9
|
style="font-weight: bold; color: #667eea; margin: 0.75rem 0"
|
|
7
|
-
|
|
10
|
+
>·</p>
|
|
8
11
|
<p style="font-size: 0.85rem; color: #666; margin-top: 0.75rem">
|
|
9
12
|
✓ Manipulate DOM before deployment<br />
|
|
10
13
|
✓ Import any TypeScript module<br />
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.quote-card #pre-rendered-quote {
|
|
3
|
+
margin: 1rem 0;
|
|
4
|
+
padding: 0.75rem;
|
|
5
|
+
background: #f8f9fa;
|
|
6
|
+
border-radius: 8px;
|
|
7
|
+
border-left: 4px solid #667eea;
|
|
8
|
+
}
|
|
9
|
+
.quote-card #build-metadata {
|
|
10
|
+
margin-top: 1rem;
|
|
11
|
+
padding-top: 0.75rem;
|
|
12
|
+
border-top: 1px solid #eee;
|
|
13
|
+
font-size: 0.75rem;
|
|
14
|
+
color: #999;
|
|
15
|
+
}
|
|
16
|
+
.quote-card blockquote p {
|
|
17
|
+
font-style: italic;
|
|
18
|
+
}
|
|
19
|
+
.quote-card blockquote footer {
|
|
20
|
+
margin-top: 0.5rem;
|
|
21
|
+
font-size: 0.85rem;
|
|
22
|
+
color: #666;
|
|
23
|
+
}
|
|
24
|
+
</style>
|
|
25
|
+
<section class="quote-card" style="grid-column: span 3">
|
|
26
|
+
<h2>💬 Quote of the Build</h2>
|
|
27
|
+
<p style="font-size: 0.85rem; margin-bottom: 0.75rem">
|
|
28
|
+
This content was fetched from an external API at build time:
|
|
29
|
+
</p>
|
|
30
|
+
<div id="pre-rendered-quote"></div>
|
|
31
|
+
<div id="build-metadata"></div>
|
|
32
|
+
</section>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<section>
|
|
2
|
+
<h2>⚡ TypeScript Components</h2>
|
|
3
|
+
<p style="margin-bottom: 0.75rem">
|
|
4
|
+
Dynamic generation with <code>.com.ts</code>:
|
|
5
|
+
</p>
|
|
6
|
+
<user-badge count="3"></user-badge>
|
|
7
|
+
<p style="font-size: 0.85rem; color: #666; margin-top: 0.75rem">
|
|
8
|
+
✓ Loops & conditionals<br />✓ Read attributes dynamically<br />✓
|
|
9
|
+
Build-time code execution
|
|
10
|
+
</p>
|
|
11
|
+
</section>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const count = com.getAttribute("count") || "3";
|
|
2
|
+
const numCount = parseInt(count, 10);
|
|
3
|
+
|
|
4
|
+
const items: string[] = [];
|
|
5
|
+
for (let i = 1; i <= numCount; i++) {
|
|
6
|
+
items.push(`<li>✓ Item ${i}</li>`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
com.innerHTML = `
|
|
10
|
+
<div class="user-badge">
|
|
11
|
+
<ul>
|
|
12
|
+
${items.join("")}
|
|
13
|
+
</ul>
|
|
14
|
+
</div>
|
|
15
|
+
<style>
|
|
16
|
+
.user-badge {
|
|
17
|
+
background: #f0fdf4;
|
|
18
|
+
border-left: 3px solid #22c55e;
|
|
19
|
+
padding: 0.75rem;
|
|
20
|
+
border-radius: 4px;
|
|
21
|
+
}
|
|
22
|
+
.user-badge ul {
|
|
23
|
+
list-style: none;
|
|
24
|
+
padding: 0;
|
|
25
|
+
margin: 0;
|
|
26
|
+
font-size: 0.9rem;
|
|
27
|
+
color: #166534;
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
30
|
+
`;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Docs — tkeron</title>
|
|
7
|
+
<styles-injector></styles-injector>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<site-header></site-header>
|
|
11
|
+
<main class="container">
|
|
12
|
+
<article class="page">
|
|
13
|
+
<h1>Docs</h1>
|
|
14
|
+
<p>Getting started with tkeron:</p>
|
|
15
|
+
<ul style="margin-left: 1.5rem; margin-top: 0.5rem">
|
|
16
|
+
<li><code>tk dev</code> — start the dev server on <code>:3000</code></li>
|
|
17
|
+
<li><code>tk build</code> — compile <code>websrc/</code> into <code>web/</code></li>
|
|
18
|
+
<li><code>tk init <name></code> — scaffold a new project</li>
|
|
19
|
+
</ul>
|
|
20
|
+
<p style="margin-top: 1rem">
|
|
21
|
+
Components live under <code>websrc/components/</code> grouped by domain:
|
|
22
|
+
<code>layout/</code>, <code>ui/</code>, <code>content/</code>.
|
|
23
|
+
</p>
|
|
24
|
+
<p>
|
|
25
|
+
Pages at the root of <code>websrc/</code> map directly to URLs —
|
|
26
|
+
<code>about.html</code> → <code>/about</code>,
|
|
27
|
+
<code>docs.html</code> → <code>/docs</code>.
|
|
28
|
+
</p>
|
|
29
|
+
</article>
|
|
30
|
+
</main>
|
|
31
|
+
<site-footer></site-footer>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
@@ -3,227 +3,28 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
|
7
6
|
<title>tkeron - Sample Project</title>
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
margin: 0;
|
|
11
|
-
padding: 0;
|
|
12
|
-
box-sizing: border-box;
|
|
13
|
-
}
|
|
14
|
-
body {
|
|
15
|
-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
16
|
-
line-height: 1.6;
|
|
17
|
-
color: #1a1a1a;
|
|
18
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
19
|
-
min-height: 100vh;
|
|
20
|
-
padding: 1rem;
|
|
21
|
-
display: flex;
|
|
22
|
-
align-items: center;
|
|
23
|
-
justify-content: center;
|
|
24
|
-
}
|
|
25
|
-
.container {
|
|
26
|
-
max-width: 1200px;
|
|
27
|
-
margin: 0 auto;
|
|
28
|
-
}
|
|
29
|
-
.grid {
|
|
30
|
-
display: grid;
|
|
31
|
-
grid-template-columns: repeat(3, 1fr);
|
|
32
|
-
grid-template-rows: repeat(2, 1fr);
|
|
33
|
-
gap: 1rem;
|
|
34
|
-
}
|
|
35
|
-
@media (max-width: 900px) {
|
|
36
|
-
.grid {
|
|
37
|
-
grid-template-columns: 1fr;
|
|
38
|
-
grid-template-rows: auto;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
.hero {
|
|
42
|
-
text-align: center;
|
|
43
|
-
color: white;
|
|
44
|
-
display: flex;
|
|
45
|
-
flex-direction: column;
|
|
46
|
-
align-items: center;
|
|
47
|
-
justify-content: center;
|
|
48
|
-
background: rgba(255, 255, 255, 0.1);
|
|
49
|
-
backdrop-filter: blur(10px);
|
|
50
|
-
padding: 1.5rem;
|
|
51
|
-
border-radius: 12px;
|
|
52
|
-
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
53
|
-
grid-row: span 2;
|
|
54
|
-
}
|
|
55
|
-
.hero .features {
|
|
56
|
-
margin-top: 1rem;
|
|
57
|
-
text-align: left;
|
|
58
|
-
width: 100%;
|
|
59
|
-
}
|
|
60
|
-
.hero .features li {
|
|
61
|
-
margin-bottom: 0.3rem;
|
|
62
|
-
padding-left: 0.5rem;
|
|
63
|
-
opacity: 0.9;
|
|
64
|
-
font-size: 0.85rem;
|
|
65
|
-
}
|
|
66
|
-
.logo {
|
|
67
|
-
width: 200px;
|
|
68
|
-
height: 200px;
|
|
69
|
-
margin-bottom: 1rem;
|
|
70
|
-
filter: drop-shadow(0 4px 6px rgba(0,0,0,0.2));
|
|
71
|
-
animation: float 3s ease-in-out infinite;
|
|
72
|
-
}
|
|
73
|
-
@keyframes float {
|
|
74
|
-
0%, 100% { transform: translateY(0); }
|
|
75
|
-
50% { transform: translateY(-10px); }
|
|
76
|
-
}
|
|
77
|
-
h1 {
|
|
78
|
-
font-size: 2rem;
|
|
79
|
-
margin-bottom: 0.25rem;
|
|
80
|
-
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
81
|
-
}
|
|
82
|
-
.subtitle {
|
|
83
|
-
font-size: 0.9rem;
|
|
84
|
-
opacity: 0.95;
|
|
85
|
-
}
|
|
86
|
-
section {
|
|
87
|
-
background: white;
|
|
88
|
-
padding: 1.25rem;
|
|
89
|
-
border-radius: 12px;
|
|
90
|
-
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
91
|
-
transition: transform 0.2s, box-shadow 0.2s;
|
|
92
|
-
}
|
|
93
|
-
section:hover {
|
|
94
|
-
transform: translateY(-4px);
|
|
95
|
-
box-shadow: 0 8px 12px rgba(0,0,0,0.15);
|
|
96
|
-
}
|
|
97
|
-
h2 {
|
|
98
|
-
color: #667eea;
|
|
99
|
-
margin-bottom: 0.75rem;
|
|
100
|
-
font-size: 1.2rem;
|
|
101
|
-
display: flex;
|
|
102
|
-
align-items: center;
|
|
103
|
-
gap: 0.5rem;
|
|
104
|
-
}
|
|
105
|
-
section p {
|
|
106
|
-
font-size: 0.9rem;
|
|
107
|
-
}
|
|
108
|
-
.counter {
|
|
109
|
-
margin-top: 1rem;
|
|
110
|
-
text-align: center;
|
|
111
|
-
}
|
|
112
|
-
button {
|
|
113
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
114
|
-
color: white;
|
|
115
|
-
border: none;
|
|
116
|
-
padding: 0.75rem 2rem;
|
|
117
|
-
border-radius: 8px;
|
|
118
|
-
cursor: pointer;
|
|
119
|
-
font-size: 1rem;
|
|
120
|
-
font-weight: 600;
|
|
121
|
-
transition: transform 0.1s, box-shadow 0.2s;
|
|
122
|
-
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
123
|
-
}
|
|
124
|
-
button:hover {
|
|
125
|
-
transform: scale(1.05);
|
|
126
|
-
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
|
127
|
-
}
|
|
128
|
-
button:active {
|
|
129
|
-
transform: scale(0.98);
|
|
130
|
-
}
|
|
131
|
-
.count {
|
|
132
|
-
font-size: 2rem;
|
|
133
|
-
font-weight: bold;
|
|
134
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
135
|
-
-webkit-background-clip: text;
|
|
136
|
-
-webkit-text-fill-color: transparent;
|
|
137
|
-
background-clip: text;
|
|
138
|
-
margin-top: 1rem;
|
|
139
|
-
}
|
|
140
|
-
code {
|
|
141
|
-
background: #f0f0f0;
|
|
142
|
-
padding: 0.2rem 0.4rem;
|
|
143
|
-
border-radius: 4px;
|
|
144
|
-
font-family: 'Courier New', monospace;
|
|
145
|
-
font-size: 0.9em;
|
|
146
|
-
}
|
|
147
|
-
p {
|
|
148
|
-
margin-bottom: 0.5rem;
|
|
149
|
-
}
|
|
150
|
-
a {
|
|
151
|
-
transition: transform 0.2s, box-shadow 0.2s;
|
|
152
|
-
}
|
|
153
|
-
a:hover {
|
|
154
|
-
transform: translateY(-2px);
|
|
155
|
-
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
|
|
156
|
-
}
|
|
157
|
-
.crypto-prices-container {
|
|
158
|
-
display: grid;
|
|
159
|
-
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
160
|
-
gap: 1rem;
|
|
161
|
-
margin-top: 1rem;
|
|
162
|
-
}
|
|
163
|
-
.crypto-card {
|
|
164
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
165
|
-
padding: 1.25rem;
|
|
166
|
-
border-radius: 12px;
|
|
167
|
-
color: white;
|
|
168
|
-
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
169
|
-
transition: transform 0.2s;
|
|
170
|
-
}
|
|
171
|
-
.crypto-card:hover {
|
|
172
|
-
transform: translateY(-4px);
|
|
173
|
-
}
|
|
174
|
-
.crypto-header {
|
|
175
|
-
display: flex;
|
|
176
|
-
justify-content: space-between;
|
|
177
|
-
align-items: center;
|
|
178
|
-
margin-bottom: 0.75rem;
|
|
179
|
-
}
|
|
180
|
-
.crypto-name {
|
|
181
|
-
font-weight: 600;
|
|
182
|
-
font-size: 1rem;
|
|
183
|
-
}
|
|
184
|
-
.crypto-symbol {
|
|
185
|
-
background: rgba(255,255,255,0.2);
|
|
186
|
-
padding: 0.25rem 0.5rem;
|
|
187
|
-
border-radius: 4px;
|
|
188
|
-
font-size: 0.75rem;
|
|
189
|
-
font-weight: 700;
|
|
190
|
-
}
|
|
191
|
-
.crypto-price {
|
|
192
|
-
font-size: 1.5rem;
|
|
193
|
-
font-weight: 700;
|
|
194
|
-
margin-bottom: 0.5rem;
|
|
195
|
-
}
|
|
196
|
-
.crypto-change {
|
|
197
|
-
font-size: 0.9rem;
|
|
198
|
-
font-weight: 600;
|
|
199
|
-
}
|
|
200
|
-
</style>
|
|
7
|
+
<meta id="build-stamp" name="build-stamp" content="" />
|
|
8
|
+
<styles-injector></styles-injector>
|
|
201
9
|
</head>
|
|
202
10
|
<body>
|
|
11
|
+
<site-header></site-header>
|
|
203
12
|
<div class="container">
|
|
204
13
|
<div class="grid">
|
|
205
14
|
<hero-section></hero-section>
|
|
206
15
|
<pre-render-card></pre-render-card>
|
|
207
16
|
<html-components-card></html-components-card>
|
|
208
17
|
<ts-components-card></ts-components-card>
|
|
209
|
-
<counter-card
|
|
210
|
-
|
|
211
|
-
<!-- Crypto Prices - fetched at build time -->
|
|
212
|
-
<section style="grid-column: span 3;">
|
|
213
|
-
<h2>💰 Asset Prices (Build Time)</h2>
|
|
214
|
-
<p style="font-size: 0.85rem; margin-bottom: 0.75rem;">Real cryptocurrency prices fetched from CoinGecko API at build time:</p>
|
|
215
|
-
<div id="crypto-prices" class="crypto-prices-container"></div>
|
|
216
|
-
</section>
|
|
217
|
-
|
|
218
|
-
<!-- Pre-rendered content with external API data -->
|
|
219
|
-
<section style="grid-column: span 3;">
|
|
220
|
-
<h2>💬 Quote of the Build</h2>
|
|
221
|
-
<p style="font-size: 0.85rem; margin-bottom: 0.75rem;">This content was fetched from an external API at build time:</p>
|
|
222
|
-
<div id="pre-rendered-quote" style="margin: 1rem 0; padding: 0.75rem; background: #f8f9fa; border-radius: 8px; border-left: 4px solid #667eea;"></div>
|
|
223
|
-
<div id="build-metadata" style="margin-top: 1rem; padding-top: 0.75rem; border-top: 1px solid #eee; font-size: 0.75rem; color: #999;"></div>
|
|
18
|
+
<section class="counter-card-wrap">
|
|
19
|
+
<counter-button></counter-button>
|
|
224
20
|
</section>
|
|
21
|
+
<markdown-card></markdown-card>
|
|
22
|
+
<crypto-prices-card></crypto-prices-card>
|
|
23
|
+
<quote-card></quote-card>
|
|
225
24
|
</div>
|
|
25
|
+
<user-badge count="4"></user-badge>
|
|
226
26
|
</div>
|
|
27
|
+
<site-footer></site-footer>
|
|
227
28
|
<script type="module" src="./index.ts"></script>
|
|
228
29
|
</body>
|
|
229
30
|
</html>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Post-render — runs at build time, AFTER components are resolved.
|
|
2
|
+
// Perfect for filling nodes that live inside components, plus final DOM
|
|
3
|
+
// rewrites such as rel="noopener" hardening.
|
|
4
|
+
import {
|
|
5
|
+
getRandomQuote,
|
|
6
|
+
getBuildMetadata,
|
|
7
|
+
getCryptoPrices,
|
|
8
|
+
escapeHtml,
|
|
9
|
+
} from "./utils/api-service";
|
|
10
|
+
|
|
11
|
+
const externalLinks = document.querySelectorAll('a[target="_blank"]');
|
|
12
|
+
externalLinks.forEach((link) => {
|
|
13
|
+
link.setAttribute("rel", "noopener noreferrer");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const buildTimeElement = document.getElementById("build-time");
|
|
17
|
+
if (buildTimeElement) {
|
|
18
|
+
buildTimeElement.innerHTML = `<strong>${escapeHtml(new Date().toLocaleString())}</strong>`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const [quote, metadata, prices] = await Promise.all([
|
|
22
|
+
getRandomQuote(),
|
|
23
|
+
Promise.resolve(getBuildMetadata()),
|
|
24
|
+
getCryptoPrices(),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const quoteElement = document.getElementById("pre-rendered-quote");
|
|
28
|
+
if (quoteElement) {
|
|
29
|
+
quoteElement.innerHTML = `
|
|
30
|
+
<blockquote>
|
|
31
|
+
<p>"${escapeHtml(quote.quote)}"</p>
|
|
32
|
+
<footer>— ${escapeHtml(quote.author)}</footer>
|
|
33
|
+
</blockquote>
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const metadataElement = document.getElementById("build-metadata");
|
|
38
|
+
if (metadataElement) {
|
|
39
|
+
metadataElement.innerHTML = `
|
|
40
|
+
<small>
|
|
41
|
+
Built: ${escapeHtml(metadata.timestamp)} |
|
|
42
|
+
tkeron: ${escapeHtml(metadata.tkeron)} |
|
|
43
|
+
Runtime: ${escapeHtml(metadata.runtime)} |
|
|
44
|
+
Platform: ${escapeHtml(metadata.platform)}
|
|
45
|
+
</small>
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const cryptoPricesElement = document.getElementById("crypto-prices");
|
|
50
|
+
if (cryptoPricesElement) {
|
|
51
|
+
cryptoPricesElement.innerHTML = prices
|
|
52
|
+
.map((crypto) => {
|
|
53
|
+
const positive = crypto.price_change_percentage_24h >= 0;
|
|
54
|
+
const changeColor = positive ? "#10b981" : "#ef4444";
|
|
55
|
+
const changeSymbol = positive ? "▲" : "▼";
|
|
56
|
+
return `
|
|
57
|
+
<div class="crypto-card">
|
|
58
|
+
<div class="crypto-header">
|
|
59
|
+
<span class="crypto-name">${escapeHtml(crypto.name)}</span>
|
|
60
|
+
<span class="crypto-symbol">${escapeHtml(crypto.symbol.toUpperCase())}</span>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="crypto-price">$${crypto.current_price.toLocaleString()}</div>
|
|
63
|
+
<div class="crypto-change" style="color: ${changeColor}">
|
|
64
|
+
${changeSymbol} ${Math.abs(crypto.price_change_percentage_24h).toFixed(2)}%
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
`;
|
|
68
|
+
})
|
|
69
|
+
.join("");
|
|
70
|
+
}
|
|
@@ -1,66 +1,15 @@
|
|
|
1
|
-
// Pre-
|
|
2
|
-
//
|
|
3
|
-
|
|
1
|
+
// Pre-render — runs at build time, BEFORE components are inlined.
|
|
2
|
+
// Use it for elements that exist directly in this page's HTML or for
|
|
3
|
+
// injecting custom elements that the component loop will then process.
|
|
4
|
+
import { getRandomQuote, escapeHtml } from "./utils/api-service";
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const now = new Date();
|
|
9
|
-
buildTimeElement.innerHTML = `<strong>${now.toLocaleString()}</strong>`;
|
|
6
|
+
const stamp = document.getElementById("build-stamp");
|
|
7
|
+
if (stamp) {
|
|
8
|
+
stamp.setAttribute("content", new Date().toISOString());
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (quoteElement) {
|
|
11
|
+
const titleElement = document.querySelector("title");
|
|
12
|
+
if (titleElement) {
|
|
15
13
|
const quote = await getRandomQuote();
|
|
16
|
-
|
|
17
|
-
<blockquote class="quote">
|
|
18
|
-
<p>"${quote.quote}"</p>
|
|
19
|
-
<footer>— ${quote.author}</footer>
|
|
20
|
-
</blockquote>
|
|
21
|
-
`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Add build metadata
|
|
25
|
-
const metadataElement = document.getElementById('build-metadata');
|
|
26
|
-
if (metadataElement) {
|
|
27
|
-
const metadata = await getBuildMetadata();
|
|
28
|
-
metadataElement.innerHTML = `
|
|
29
|
-
<div class="metadata">
|
|
30
|
-
<small>
|
|
31
|
-
Built: ${metadata.timestamp} |
|
|
32
|
-
tkeron: ${metadata.tkeron} |
|
|
33
|
-
Runtime: ${metadata.runtime} |
|
|
34
|
-
Platform: ${metadata.platform}
|
|
35
|
-
</small>
|
|
36
|
-
</div>
|
|
37
|
-
`;
|
|
14
|
+
titleElement.textContent = `tkeron — ${escapeHtml(quote.author)}`;
|
|
38
15
|
}
|
|
39
|
-
|
|
40
|
-
// Fetch crypto prices at build time
|
|
41
|
-
const cryptoPricesElement = document.getElementById('crypto-prices');
|
|
42
|
-
if (cryptoPricesElement) {
|
|
43
|
-
const prices = await getCryptoPrices();
|
|
44
|
-
|
|
45
|
-
const pricesHtml = prices.map(crypto => {
|
|
46
|
-
const changeColor = crypto.price_change_percentage_24h >= 0 ? '#10b981' : '#ef4444';
|
|
47
|
-
const changeSymbol = crypto.price_change_percentage_24h >= 0 ? '▲' : '▼';
|
|
48
|
-
|
|
49
|
-
return `
|
|
50
|
-
<div class="crypto-card">
|
|
51
|
-
<div class="crypto-header">
|
|
52
|
-
<span class="crypto-name">${crypto.name}</span>
|
|
53
|
-
<span class="crypto-symbol">${crypto.symbol.toUpperCase()}</span>
|
|
54
|
-
</div>
|
|
55
|
-
<div class="crypto-price">$${crypto.current_price.toLocaleString()}</div>
|
|
56
|
-
<div class="crypto-change" style="color: ${changeColor}">
|
|
57
|
-
${changeSymbol} ${Math.abs(crypto.price_change_percentage_24h).toFixed(2)}%
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
`;
|
|
61
|
-
}).join('');
|
|
62
|
-
|
|
63
|
-
cryptoPricesElement.innerHTML = pricesHtml;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
// Client-side TypeScript
|
|
1
|
+
// Client-side TypeScript — only fills data into already existing nodes.
|
|
2
2
|
let clickCount = 0;
|
|
3
3
|
|
|
4
|
-
const button = document.getElementById("increment")
|
|
5
|
-
const countDisplay = document.getElementById("count")
|
|
4
|
+
const button = document.getElementById("increment");
|
|
5
|
+
const countDisplay = document.getElementById("count");
|
|
6
6
|
|
|
7
|
-
button
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
if (button && countDisplay) {
|
|
8
|
+
button.addEventListener("click", () => {
|
|
9
|
+
clickCount++;
|
|
10
|
+
countDisplay.textContent = clickCount.toString();
|
|
11
|
+
});
|
|
12
|
+
}
|