tkeron 6.0.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/changelog.md +18 -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/{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/{ts-components-card.com.html → components/ui/ts-components-card.com.html} +2 -2
- 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 -242
- package/examples/init_sample/websrc/index.post.ts +70 -0
- package/examples/init_sample/websrc/index.pre.ts +10 -66
- 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/{api-service.ts → utils/api-service.ts} +32 -37
- 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/package.json +1 -1
- package/skills/tkeron/SKILL.md +4 -3
- package/skills/tkeron-organization/SKILL.md +6 -5
- package/skills/tkeron-patterns/SKILL.md +109 -17
- package/skills/tkeron-troubleshooting/SKILL.md +7 -3
- package/examples/init_sample/websrc/counter-card.com.html +0 -12
- package/examples/init_sample/websrc/favicon.ico +0 -0
- package/examples/init_sample/websrc/hero-section.com.html +0 -53
- package/examples/init_sample/websrc/user-badge.com.ts +0 -17
- /package/examples/init_sample/websrc/{profile.png → assets/profile.png} +0 -0
- /package/examples/init_sample/websrc/{html-components-card.com.html → components/ui/html-components-card.com.html} +0 -0
|
@@ -3,260 +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%,
|
|
75
|
-
100% {
|
|
76
|
-
transform: translateY(0);
|
|
77
|
-
}
|
|
78
|
-
50% {
|
|
79
|
-
transform: translateY(-10px);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
h1 {
|
|
83
|
-
font-size: 2rem;
|
|
84
|
-
margin-bottom: 0.25rem;
|
|
85
|
-
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
86
|
-
}
|
|
87
|
-
.subtitle {
|
|
88
|
-
font-size: 0.9rem;
|
|
89
|
-
opacity: 0.95;
|
|
90
|
-
}
|
|
91
|
-
section {
|
|
92
|
-
background: white;
|
|
93
|
-
padding: 1.25rem;
|
|
94
|
-
border-radius: 12px;
|
|
95
|
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
96
|
-
transition:
|
|
97
|
-
transform 0.2s,
|
|
98
|
-
box-shadow 0.2s;
|
|
99
|
-
}
|
|
100
|
-
section:hover {
|
|
101
|
-
transform: translateY(-4px);
|
|
102
|
-
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
|
|
103
|
-
}
|
|
104
|
-
h2 {
|
|
105
|
-
color: #667eea;
|
|
106
|
-
margin-bottom: 0.75rem;
|
|
107
|
-
font-size: 1.2rem;
|
|
108
|
-
display: flex;
|
|
109
|
-
align-items: center;
|
|
110
|
-
gap: 0.5rem;
|
|
111
|
-
}
|
|
112
|
-
section p {
|
|
113
|
-
font-size: 0.9rem;
|
|
114
|
-
}
|
|
115
|
-
.counter {
|
|
116
|
-
margin-top: 1rem;
|
|
117
|
-
text-align: center;
|
|
118
|
-
}
|
|
119
|
-
button {
|
|
120
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
121
|
-
color: white;
|
|
122
|
-
border: none;
|
|
123
|
-
padding: 0.75rem 2rem;
|
|
124
|
-
border-radius: 8px;
|
|
125
|
-
cursor: pointer;
|
|
126
|
-
font-size: 1rem;
|
|
127
|
-
font-weight: 600;
|
|
128
|
-
transition:
|
|
129
|
-
transform 0.1s,
|
|
130
|
-
box-shadow 0.2s;
|
|
131
|
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
132
|
-
}
|
|
133
|
-
button:hover {
|
|
134
|
-
transform: scale(1.05);
|
|
135
|
-
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
|
136
|
-
}
|
|
137
|
-
button:active {
|
|
138
|
-
transform: scale(0.98);
|
|
139
|
-
}
|
|
140
|
-
.count {
|
|
141
|
-
font-size: 2rem;
|
|
142
|
-
font-weight: bold;
|
|
143
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
144
|
-
-webkit-background-clip: text;
|
|
145
|
-
-webkit-text-fill-color: transparent;
|
|
146
|
-
background-clip: text;
|
|
147
|
-
margin-top: 1rem;
|
|
148
|
-
}
|
|
149
|
-
code {
|
|
150
|
-
background: #f0f0f0;
|
|
151
|
-
padding: 0.2rem 0.4rem;
|
|
152
|
-
border-radius: 4px;
|
|
153
|
-
font-family: "Courier New", monospace;
|
|
154
|
-
font-size: 0.9em;
|
|
155
|
-
}
|
|
156
|
-
p {
|
|
157
|
-
margin-bottom: 0.5rem;
|
|
158
|
-
}
|
|
159
|
-
a {
|
|
160
|
-
transition:
|
|
161
|
-
transform 0.2s,
|
|
162
|
-
box-shadow 0.2s;
|
|
163
|
-
}
|
|
164
|
-
a:hover {
|
|
165
|
-
transform: translateY(-2px);
|
|
166
|
-
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
|
167
|
-
}
|
|
168
|
-
.crypto-prices-container {
|
|
169
|
-
display: grid;
|
|
170
|
-
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
171
|
-
gap: 1rem;
|
|
172
|
-
margin-top: 1rem;
|
|
173
|
-
}
|
|
174
|
-
.crypto-card {
|
|
175
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
176
|
-
padding: 1.25rem;
|
|
177
|
-
border-radius: 12px;
|
|
178
|
-
color: white;
|
|
179
|
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
180
|
-
transition: transform 0.2s;
|
|
181
|
-
}
|
|
182
|
-
.crypto-card:hover {
|
|
183
|
-
transform: translateY(-4px);
|
|
184
|
-
}
|
|
185
|
-
.crypto-header {
|
|
186
|
-
display: flex;
|
|
187
|
-
justify-content: space-between;
|
|
188
|
-
align-items: center;
|
|
189
|
-
margin-bottom: 0.75rem;
|
|
190
|
-
}
|
|
191
|
-
.crypto-name {
|
|
192
|
-
font-weight: 600;
|
|
193
|
-
font-size: 1rem;
|
|
194
|
-
}
|
|
195
|
-
.crypto-symbol {
|
|
196
|
-
background: rgba(255, 255, 255, 0.2);
|
|
197
|
-
padding: 0.25rem 0.5rem;
|
|
198
|
-
border-radius: 4px;
|
|
199
|
-
font-size: 0.75rem;
|
|
200
|
-
font-weight: 700;
|
|
201
|
-
}
|
|
202
|
-
.crypto-price {
|
|
203
|
-
font-size: 1.5rem;
|
|
204
|
-
font-weight: 700;
|
|
205
|
-
margin-bottom: 0.5rem;
|
|
206
|
-
}
|
|
207
|
-
.crypto-change {
|
|
208
|
-
font-size: 0.9rem;
|
|
209
|
-
font-weight: 600;
|
|
210
|
-
}
|
|
211
|
-
</style>
|
|
7
|
+
<meta id="build-stamp" name="build-stamp" content="" />
|
|
8
|
+
<styles-injector></styles-injector>
|
|
212
9
|
</head>
|
|
213
10
|
<body>
|
|
11
|
+
<site-header></site-header>
|
|
214
12
|
<div class="container">
|
|
215
13
|
<div class="grid">
|
|
216
14
|
<hero-section></hero-section>
|
|
217
15
|
<pre-render-card></pre-render-card>
|
|
218
16
|
<html-components-card></html-components-card>
|
|
219
17
|
<ts-components-card></ts-components-card>
|
|
220
|
-
<counter-card
|
|
221
|
-
|
|
222
|
-
<!-- Crypto Prices - fetched at build time -->
|
|
223
|
-
<section style="grid-column: span 3">
|
|
224
|
-
<h2>💰 Asset Prices (Build Time)</h2>
|
|
225
|
-
<p style="font-size: 0.85rem; margin-bottom: 0.75rem">
|
|
226
|
-
Real cryptocurrency prices fetched from CoinGecko API at build time:
|
|
227
|
-
</p>
|
|
228
|
-
<div id="crypto-prices" class="crypto-prices-container"></div>
|
|
229
|
-
</section>
|
|
230
|
-
|
|
231
|
-
<!-- Pre-rendered content with external API data -->
|
|
232
|
-
<section style="grid-column: span 3">
|
|
233
|
-
<h2>💬 Quote of the Build</h2>
|
|
234
|
-
<p style="font-size: 0.85rem; margin-bottom: 0.75rem">
|
|
235
|
-
This content was fetched from an external API at build time:
|
|
236
|
-
</p>
|
|
237
|
-
<div
|
|
238
|
-
id="pre-rendered-quote"
|
|
239
|
-
style="
|
|
240
|
-
margin: 1rem 0;
|
|
241
|
-
padding: 0.75rem;
|
|
242
|
-
background: #f8f9fa;
|
|
243
|
-
border-radius: 8px;
|
|
244
|
-
border-left: 4px solid #667eea;
|
|
245
|
-
"
|
|
246
|
-
></div>
|
|
247
|
-
<div
|
|
248
|
-
id="build-metadata"
|
|
249
|
-
style="
|
|
250
|
-
margin-top: 1rem;
|
|
251
|
-
padding-top: 0.75rem;
|
|
252
|
-
border-top: 1px solid #eee;
|
|
253
|
-
font-size: 0.75rem;
|
|
254
|
-
color: #999;
|
|
255
|
-
"
|
|
256
|
-
></div>
|
|
18
|
+
<section class="counter-card-wrap">
|
|
19
|
+
<counter-button></counter-button>
|
|
257
20
|
</section>
|
|
21
|
+
<markdown-card></markdown-card>
|
|
22
|
+
<crypto-prices-card></crypto-prices-card>
|
|
23
|
+
<quote-card></quote-card>
|
|
258
24
|
</div>
|
|
25
|
+
<user-badge count="4"></user-badge>
|
|
259
26
|
</div>
|
|
27
|
+
<site-footer></site-footer>
|
|
260
28
|
<script type="module" src="./index.ts"></script>
|
|
261
29
|
</body>
|
|
262
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,71 +1,15 @@
|
|
|
1
|
-
// Pre-
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
getBuildMetadata,
|
|
6
|
-
getCryptoPrices,
|
|
7
|
-
} from "./api-service";
|
|
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";
|
|
8
5
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const now = new Date();
|
|
13
|
-
buildTimeElement.innerHTML = `<strong>${now.toLocaleString()}</strong>`;
|
|
6
|
+
const stamp = document.getElementById("build-stamp");
|
|
7
|
+
if (stamp) {
|
|
8
|
+
stamp.setAttribute("content", new Date().toISOString());
|
|
14
9
|
}
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (quoteElement) {
|
|
11
|
+
const titleElement = document.querySelector("title");
|
|
12
|
+
if (titleElement) {
|
|
19
13
|
const quote = await getRandomQuote();
|
|
20
|
-
|
|
21
|
-
<blockquote class="quote">
|
|
22
|
-
<p>"${quote.quote}"</p>
|
|
23
|
-
<footer>— ${quote.author}</footer>
|
|
24
|
-
</blockquote>
|
|
25
|
-
`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Add build metadata
|
|
29
|
-
const metadataElement = document.getElementById("build-metadata");
|
|
30
|
-
if (metadataElement) {
|
|
31
|
-
const metadata = await getBuildMetadata();
|
|
32
|
-
metadataElement.innerHTML = `
|
|
33
|
-
<div class="metadata">
|
|
34
|
-
<small>
|
|
35
|
-
Built: ${metadata.timestamp} |
|
|
36
|
-
tkeron: ${metadata.tkeron} |
|
|
37
|
-
Runtime: ${metadata.runtime} |
|
|
38
|
-
Platform: ${metadata.platform}
|
|
39
|
-
</small>
|
|
40
|
-
</div>
|
|
41
|
-
`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Fetch crypto prices at build time
|
|
45
|
-
const cryptoPricesElement = document.getElementById("crypto-prices");
|
|
46
|
-
if (cryptoPricesElement) {
|
|
47
|
-
const prices = await getCryptoPrices();
|
|
48
|
-
|
|
49
|
-
const pricesHtml = prices
|
|
50
|
-
.map((crypto) => {
|
|
51
|
-
const changeColor =
|
|
52
|
-
crypto.price_change_percentage_24h >= 0 ? "#10b981" : "#ef4444";
|
|
53
|
-
const changeSymbol = crypto.price_change_percentage_24h >= 0 ? "▲" : "▼";
|
|
54
|
-
|
|
55
|
-
return `
|
|
56
|
-
<div class="crypto-card">
|
|
57
|
-
<div class="crypto-header">
|
|
58
|
-
<span class="crypto-name">${crypto.name}</span>
|
|
59
|
-
<span class="crypto-symbol">${crypto.symbol.toUpperCase()}</span>
|
|
60
|
-
</div>
|
|
61
|
-
<div class="crypto-price">$${crypto.current_price.toLocaleString()}</div>
|
|
62
|
-
<div class="crypto-change" style="color: ${changeColor}">
|
|
63
|
-
${changeSymbol} ${Math.abs(crypto.price_change_percentage_24h).toFixed(2)}%
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
`;
|
|
67
|
-
})
|
|
68
|
-
.join("");
|
|
69
|
-
|
|
70
|
-
cryptoPricesElement.innerHTML = pricesHtml;
|
|
14
|
+
titleElement.textContent = `tkeron — ${escapeHtml(quote.author)}`;
|
|
71
15
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
* {
|
|
2
|
+
margin: 0;
|
|
3
|
+
padding: 0;
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
body {
|
|
8
|
+
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
|
9
|
+
line-height: 1.6;
|
|
10
|
+
color: #1a1a1a;
|
|
11
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
12
|
+
min-height: 100vh;
|
|
13
|
+
padding: 1rem;
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
align-items: center;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.container {
|
|
20
|
+
max-width: 1200px;
|
|
21
|
+
margin: 0 auto;
|
|
22
|
+
width: 100%;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.grid {
|
|
26
|
+
display: grid;
|
|
27
|
+
grid-template-columns: repeat(3, 1fr);
|
|
28
|
+
grid-template-rows: repeat(2, 1fr);
|
|
29
|
+
gap: 1rem;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@media (max-width: 900px) {
|
|
33
|
+
.grid {
|
|
34
|
+
grid-template-columns: 1fr;
|
|
35
|
+
grid-template-rows: auto;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.page {
|
|
40
|
+
background: white;
|
|
41
|
+
border-radius: 12px;
|
|
42
|
+
padding: 2rem;
|
|
43
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
44
|
+
margin-top: 1rem;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.page h1 {
|
|
48
|
+
color: #4c51bf;
|
|
49
|
+
margin-bottom: 0.5rem;
|
|
50
|
+
font-size: 1.75rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.page p {
|
|
54
|
+
margin-bottom: 0.5rem;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
h1 {
|
|
58
|
+
font-size: 2rem;
|
|
59
|
+
margin-bottom: 0.25rem;
|
|
60
|
+
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
h2 {
|
|
64
|
+
color: #667eea;
|
|
65
|
+
margin-bottom: 0.75rem;
|
|
66
|
+
font-size: 1.2rem;
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: 0.5rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
p {
|
|
73
|
+
margin-bottom: 0.5rem;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
section {
|
|
77
|
+
background: white;
|
|
78
|
+
padding: 1.25rem;
|
|
79
|
+
border-radius: 12px;
|
|
80
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
81
|
+
transition:
|
|
82
|
+
transform 0.2s,
|
|
83
|
+
box-shadow 0.2s;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
section:hover {
|
|
87
|
+
transform: translateY(-4px);
|
|
88
|
+
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
section p {
|
|
92
|
+
font-size: 0.9rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
code {
|
|
96
|
+
background: #f0f0f0;
|
|
97
|
+
padding: 0.2rem 0.4rem;
|
|
98
|
+
border-radius: 4px;
|
|
99
|
+
font-family: "Courier New", monospace;
|
|
100
|
+
font-size: 0.9em;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
a {
|
|
104
|
+
transition:
|
|
105
|
+
transform 0.2s,
|
|
106
|
+
box-shadow 0.2s;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
a:hover {
|
|
110
|
+
transform: translateY(-2px);
|
|
111
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
|
112
|
+
}
|
|
@@ -1,48 +1,46 @@
|
|
|
1
|
-
// External API service - can be imported in .pre.ts files
|
|
2
|
-
// Demonstrates that pre-rendering can use any TypeScript module
|
|
3
|
-
|
|
4
1
|
export interface Quote {
|
|
5
2
|
id: number;
|
|
6
3
|
quote: string;
|
|
7
4
|
author: string;
|
|
8
5
|
}
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
export const escapeHtml = (str: string): string =>
|
|
8
|
+
str
|
|
9
|
+
.replace(/&/g, "&")
|
|
10
|
+
.replace(/</g, "<")
|
|
11
|
+
.replace(/>/g, ">")
|
|
12
|
+
.replace(/"/g, """)
|
|
13
|
+
.replace(/'/g, "'");
|
|
17
14
|
|
|
15
|
+
export const getRandomQuote = async (): Promise<Quote> => {
|
|
16
|
+
const controller = new AbortController();
|
|
17
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
18
|
+
try {
|
|
19
|
+
const response = await fetch("https://dummyjson.com/quotes/random", {
|
|
20
|
+
signal: controller.signal,
|
|
21
|
+
});
|
|
18
22
|
if (!response.ok) {
|
|
19
23
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
20
24
|
}
|
|
21
|
-
|
|
22
25
|
const data = await response.json();
|
|
23
26
|
return data as Quote;
|
|
24
|
-
} catch
|
|
25
|
-
// Fallback quote if API fails
|
|
26
|
-
console.warn("API call failed, using fallback quote");
|
|
27
|
+
} catch {
|
|
27
28
|
return {
|
|
28
29
|
id: 0,
|
|
29
30
|
quote: "The only way to do great work is to love what you do.",
|
|
30
31
|
author: "Steve Jobs",
|
|
31
32
|
};
|
|
33
|
+
} finally {
|
|
34
|
+
clearTimeout(timeout);
|
|
32
35
|
}
|
|
33
|
-
}
|
|
36
|
+
};
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
runtime: `Bun ${Bun.version}`,
|
|
42
|
-
tkeron: process.env.TKERON_VERSION || "unknown",
|
|
43
|
-
platform: process.platform,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
38
|
+
export const getBuildMetadata = () => ({
|
|
39
|
+
timestamp: new Date().toLocaleString(),
|
|
40
|
+
runtime: `Bun ${Bun.version}`,
|
|
41
|
+
tkeron: process.env.TKERON_VERSION || "unknown",
|
|
42
|
+
platform: process.platform,
|
|
43
|
+
});
|
|
46
44
|
|
|
47
45
|
export interface CryptoPrice {
|
|
48
46
|
id: string;
|
|
@@ -52,25 +50,20 @@ export interface CryptoPrice {
|
|
|
52
50
|
price_change_percentage_24h: number;
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*/
|
|
59
|
-
export async function getCryptoPrices(): Promise<CryptoPrice[]> {
|
|
53
|
+
export const getCryptoPrices = async (): Promise<CryptoPrice[]> => {
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
60
56
|
try {
|
|
61
57
|
const response = await fetch(
|
|
62
58
|
"https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=bitcoin,ethereum,solana&order=market_cap_desc&sparkline=false",
|
|
59
|
+
{ signal: controller.signal },
|
|
63
60
|
);
|
|
64
|
-
|
|
65
61
|
if (!response.ok) {
|
|
66
62
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
67
63
|
}
|
|
68
|
-
|
|
69
64
|
const data = await response.json();
|
|
70
65
|
return data as CryptoPrice[];
|
|
71
|
-
} catch
|
|
72
|
-
// Fallback data if API fails
|
|
73
|
-
console.warn("Crypto API call failed, using fallback data");
|
|
66
|
+
} catch {
|
|
74
67
|
return [
|
|
75
68
|
{
|
|
76
69
|
id: "bitcoin",
|
|
@@ -94,5 +87,7 @@ export async function getCryptoPrices(): Promise<CryptoPrice[]> {
|
|
|
94
87
|
price_change_percentage_24h: 5.8,
|
|
95
88
|
},
|
|
96
89
|
];
|
|
90
|
+
} finally {
|
|
91
|
+
clearTimeout(timeout);
|
|
97
92
|
}
|
|
98
|
-
}
|
|
93
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
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>Bad — link rel stylesheet</title>
|
|
7
|
+
<link rel="stylesheet" href="./styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<h1>Bad: <code><link rel="stylesheet"></code></h1>
|
|
11
|
+
<p>
|
|
12
|
+
The CSS is referenced via a <code><link></code> tag. tkeron bundles
|
|
13
|
+
it into a separate <code>.css</code> file — an extra HTTP request on every
|
|
14
|
+
page load.
|
|
15
|
+
</p>
|
|
16
|
+
<p>
|
|
17
|
+
Check <code>web/bad.html</code>: you will find a
|
|
18
|
+
<code><link rel="stylesheet" href="./bad.css"></code> and a
|
|
19
|
+
companion <code>bad.css</code> file next to it.
|
|
20
|
+
</p>
|
|
21
|
+
<a href="index.html">Back to index</a>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|